<?xml version="1.0" encoding="UTF-8"?> <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> <channel> <title>Charles Xu</title> <description>Essays, books, wiki on technologies, career, markets, and more.</description> <link>/</link> <atom:link href="/feed.xml" rel="self" type="application/rss+xml"/> <pubDate>Wed, 26 Nov 2025 08:36:04 +0000</pubDate> <lastBuildDate>Wed, 26 Nov 2025 08:36:04 +0000</lastBuildDate> <generator>Jekyll v4.3.2</generator> <item> <title>3.3x Faster HuggingFace Tokenizers for Single Sequence</title> <description>&lt;p&gt;I made &lt;a href=&quot;https://github.com/huggingface/tokenizers&quot;&gt;HuggingFace tokenizers&lt;/a&gt; 3.3x faster by parallelizing single-input tokenization with overlapping chunks, zero-copy offset operations, SIMD-accelerated boundary detection, and cache-hierarchy-aware chunking. The result is bit-identical to serial encoding. Fast tokenization is critical to achieve low TTFT given long context.&lt;/p&gt; &lt;h2 id=&quot;the-long-context-revolution-has-a-bottleneck&quot;&gt;The Long Context Revolution Has a Bottleneck&lt;/h2&gt; &lt;p&gt;LLM models now routinely handle 1M+ token contexts. This isn’t just a bigger number; it’s enabling fundamentally new use cases:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;AI Agents&lt;/strong&gt;: Load entire codebases into context instead of fumbling with RAG retrieval. Why search when you can reason over everything?&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Document Analysis&lt;/strong&gt;: Process entire legal contracts, medical records, or research papers in one shot—no chunking, no context loss.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Displacing RAG&lt;/strong&gt;: Google’s “infinite context” approach suggests a future where retrieval augmentation becomes unnecessary.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;But there’s a bottleneck nobody talks about: &lt;strong&gt;tokenization&lt;/strong&gt;.&lt;/p&gt; &lt;p&gt;Everyone optimizes inference—quantization, KV cache, flash attention, speculative decoding, etc. But it takes 1.1 seconds to tokenize a 4MB document (about 1M tokens).&lt;/p&gt; &lt;p&gt;As AI agents scale with tool use and long context with multi-turns, the tokenization latency hurts TTFT (Time to First Token) and time to completion.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;The challenge&lt;/strong&gt;: Tokenization is inherently sequential. You process one token, then find the next, then the next. How to parallelize this without breaking correctness?&lt;/p&gt; &lt;h2 id=&quot;the-core-insight-overlapping-chunks&quot;&gt;The Core Insight: Overlapping Chunks&lt;/h2&gt; &lt;p&gt;The breakthrough is simple: &lt;strong&gt;we can chunk, parallelize, and merge&lt;/strong&gt;.&lt;/p&gt; &lt;p&gt;The naive approach of splitting text and tokenizing in parallel doesn’t work, because you’ll get different results than serial encoding given tokens can span split boundaries. My solution uses overlapping chunks with a deterministic merge algorithm detailed in Appendix A.&lt;/p&gt; &lt;h2 id=&quot;key-optimizations&quot;&gt;Key Optimizations&lt;/h2&gt; &lt;h3 id=&quot;optimization-1-recursive-divide-and-conquer-strategy&quot;&gt;Optimization 1: Recursive Divide-and-Conquer Strategy&lt;/h3&gt; &lt;p&gt;We use binary splitting with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rayon::join&lt;/code&gt; for work-stealing parallelism:&lt;/p&gt; &lt;div class=&quot;language-rust highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;encode_recursive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;depth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;usize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;max_depth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;usize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Encoding&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;depth&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;max_depth&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;encode_serial&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Base case&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;find_split_point&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;left_chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right_chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;create_overlapping_chunks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Parallel execution via rayon&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;left_encoding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right_encoding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;rayon&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;encode_recursive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;left_chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;depth&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;max_depth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;encode_recursive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;right_chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;depth&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;max_depth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;merge_at_midpoint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;left_encoding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right_encoding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;&lt;strong&gt;Auto-tuning depth&lt;/strong&gt; based on input size and CPU cores:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;100KB input → depth 2 (4 parallel chunks)&lt;/li&gt; &lt;li&gt;500KB input → depth 3 (8 parallel chunks)&lt;/li&gt; &lt;li&gt;1MB input → depth 4 (16 parallel chunks)&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;Why binary splitting?&lt;/strong&gt; Clean merge semantics and work-stealing efficiency. Rayon automatically balances work across available cores.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Results&lt;/strong&gt;: 1.7-2.1x speedup for 100KB-500KB inputs.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Sweet spot&lt;/strong&gt;: Medium-sized documents like research papers, API documentation, or individual source files.&lt;/p&gt; &lt;h3 id=&quot;optimization-2-cache-block-streaming-for-large-inputs&quot;&gt;Optimization 2: Cache-Block Streaming for Large Inputs&lt;/h3&gt; &lt;p&gt;At 1MB+, recursive splitting starts causing cache thrashing. Different threads access scattered memory regions, evicting each other’s data from cache.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;The solution&lt;/strong&gt;: Process input in L1-cache-sized blocks sequentially:&lt;/p&gt; &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;Input (1MB): ├───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬──────┤ │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │10 │11 │12 │ ... │ │8KB│8KB│8KB│8KB│8KB│8KB│8KB│8KB│8KB│8KB│8KB│8KB│8KB│ │ └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴──────┘ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ Encode all blocks in parallel, merge sequentially &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;&lt;strong&gt;Why 8KB blocks?&lt;/strong&gt; We benchmarked extensively:&lt;/p&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt; &lt;th&gt;Block Size&lt;/th&gt; &lt;th&gt;Time (4MB)&lt;/th&gt; &lt;th&gt;Throughput&lt;/th&gt; &lt;th&gt;vs 64KB&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;&lt;strong&gt;4KB&lt;/strong&gt;&lt;/td&gt; &lt;td&gt;&lt;strong&gt;386ms&lt;/strong&gt;&lt;/td&gt; &lt;td&gt;&lt;strong&gt;10.4 MiB/s&lt;/strong&gt;&lt;/td&gt; &lt;td&gt;&lt;strong&gt;+31% faster&lt;/strong&gt; ⭐&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;&lt;strong&gt;8KB&lt;/strong&gt;&lt;/td&gt; &lt;td&gt;&lt;strong&gt;382ms&lt;/strong&gt;&lt;/td&gt; &lt;td&gt;&lt;strong&gt;10.5 MiB/s&lt;/strong&gt;&lt;/td&gt; &lt;td&gt;&lt;strong&gt;+32% faster&lt;/strong&gt; ⭐&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;16KB&lt;/td&gt; &lt;td&gt;421ms&lt;/td&gt; &lt;td&gt;9.5 MiB/s&lt;/td&gt; &lt;td&gt;+20% faster&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;32KB&lt;/td&gt; &lt;td&gt;495ms&lt;/td&gt; &lt;td&gt;8.1 MiB/s&lt;/td&gt; &lt;td&gt;+2% faster&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;64KB&lt;/td&gt; &lt;td&gt;505ms&lt;/td&gt; &lt;td&gt;7.9 MiB/s&lt;/td&gt; &lt;td&gt;baseline&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;Smaller blocks are faster! Why?&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;L1 cache residency&lt;/strong&gt;: 8KB fits entirely in L1 cache (typically 32-64KB) with room for vocabulary lookups&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Maximum parallelism&lt;/strong&gt;: 1MB = 125 independent work items vs. 16 with 64KB blocks&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Sequential memory access&lt;/strong&gt;: Each block is processed sequentially, so the CPU prefetcher thrives&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Less cache eviction&lt;/strong&gt;: Vocabulary data stays hot in cache across small blocks&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Cache hierarchy matters:&lt;/p&gt; &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;8KB blocks: [L1: block + vocab] → process → next block Full L1 residency, maximum parallelism 64KB blocks: [L2 cache: block] → process → next block L2 latency, fewer parallel blocks, cache thrashing &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;&lt;strong&gt;Results&lt;/strong&gt;: 2.2-3.2x speedup at 100KB-1MB, &lt;strong&gt;3.3x at 4MB&lt;/strong&gt;.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Auto-selection&lt;/strong&gt;: The system automatically picks streaming mode for inputs ≥1MB.&lt;/p&gt; &lt;h3 id=&quot;optimization-3-lazyencoding-zero-copy-offset&quot;&gt;Optimization 3: LazyEncoding: Zero-Copy Offset&lt;/h3&gt; &lt;p&gt;Here’s a subtle but critical problem: after tokenizing each chunk, we need to:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;Shift all token offsets to global coordinates (O(n))&lt;/li&gt; &lt;li&gt;Filter tokens by position (O(n))&lt;/li&gt; &lt;li&gt;Merge arrays (O(n))&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;At depth 4 recursion (16 chunks), this would mean &lt;strong&gt;~30 O(n) passes&lt;/strong&gt; through the data. That’s a lot of wasted work.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;The solution&lt;/strong&gt;: Defer everything until final materialization.&lt;/p&gt; &lt;div class=&quot;language-rust highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LazyEncoding&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;encoding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Encoding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;base_offset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;usize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Offset to add (applied lazily)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;start_idx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;usize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Virtual range start&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;end_idx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;usize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Virtual range end&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;impl&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LazyEncoding&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// O(1) - just update metadata&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;shift_offset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;mut&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;delta&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;usize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;.base_offset&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;delta&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// O(log n) - binary search instead of linear scan&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;filter_starting_before&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;mut&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;usize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cut_idx&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;binary_search_offsets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;.end_idx&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;.start_idx&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cut_idx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// O(n) - but only called ONCE at the very end&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;materialize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Encoding&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Apply base_offset and extract range in one pass&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;apply_and_extract&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;.encoding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;.base_offset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;.start_idx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;.end_idx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;&lt;strong&gt;Impact&lt;/strong&gt;: Turns O(n × depth) into O(n) total complexity.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Benchmark evidence&lt;/strong&gt; (500KB input):&lt;/p&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt; &lt;th&gt;Depth&lt;/th&gt; &lt;th&gt;Without LazyEncoding&lt;/th&gt; &lt;th&gt;With LazyEncoding&lt;/th&gt; &lt;th&gt;Improvement&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;1&lt;/td&gt; &lt;td&gt;93ms&lt;/td&gt; &lt;td&gt;93ms&lt;/td&gt; &lt;td&gt;baseline&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;2&lt;/td&gt; &lt;td&gt;87ms&lt;/td&gt; &lt;td&gt;66ms&lt;/td&gt; &lt;td&gt;+32% faster&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;3&lt;/td&gt; &lt;td&gt;84ms&lt;/td&gt; &lt;td&gt;59ms&lt;/td&gt; &lt;td&gt;+42% faster&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;4&lt;/td&gt; &lt;td&gt;82ms&lt;/td&gt; &lt;td&gt;58ms&lt;/td&gt; &lt;td&gt;&lt;strong&gt;+60% faster&lt;/strong&gt;&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;At depth 4, LazyEncoding is &lt;strong&gt;60% faster&lt;/strong&gt; than naive offset manipulation.&lt;/p&gt; &lt;h3 id=&quot;optimization-4-simd-accelerated-smart-boundaries&quot;&gt;Optimization 4: SIMD-Accelerated Smart Boundaries&lt;/h3&gt; &lt;p&gt;We need to split text at “safe” positions where tokenization is predictable. Naive approach: scan byte-by-byte for whitespace. Slow.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Better solution&lt;/strong&gt;: Use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memchr&lt;/code&gt; crate for SIMD-accelerated searches (AVX2/SSE2):&lt;/p&gt; &lt;div class=&quot;language-rust highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;k&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;memchr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;memchr_iter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 10-20x faster than byte-by-byte scanning&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;memchr_iter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sc&quot;&gt;b&apos;\n&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;text_bytes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;is_paragraph_break&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SplitPoint&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;is_safe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;&lt;strong&gt;Smart boundary detection&lt;/strong&gt; (priority order):&lt;/p&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt; &lt;th&gt;Priority&lt;/th&gt; &lt;th&gt;Boundary Type&lt;/th&gt; &lt;th&gt;Pattern&lt;/th&gt; &lt;th&gt;Safety Level&lt;/th&gt; &lt;th&gt;Overlap Size&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;1&lt;/td&gt; &lt;td&gt;Paragraph breaks&lt;/td&gt; &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\n\n&lt;/code&gt;&lt;/td&gt; &lt;td&gt;Guaranteed token boundary&lt;/td&gt; &lt;td&gt;100-500 bytes&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;2&lt;/td&gt; &lt;td&gt;Sentence endings&lt;/td&gt; &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;. &lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;! &lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;? &lt;/code&gt;&lt;/td&gt; &lt;td&gt;Safe boundary&lt;/td&gt; &lt;td&gt;100-500 bytes&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;3&lt;/td&gt; &lt;td&gt;Any whitespace&lt;/td&gt; &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\n&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\t&lt;/code&gt;&lt;/td&gt; &lt;td&gt;Fallback&lt;/td&gt; &lt;td&gt;500-5000 bytes&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;&lt;strong&gt;Why “safe” boundaries matter&lt;/strong&gt;:&lt;/p&gt; &lt;p&gt;At a paragraph break or sentence ending, we know tokenization will be consistent. The overlap only needs to handle the longest possible token (~100 bytes for most vocabularies).&lt;/p&gt; &lt;p&gt;At an arbitrary whitespace, we need more overlap to handle context-dependent tokenization (e.g., “ the” vs “the”).&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Impact&lt;/strong&gt;:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;SIMD search&lt;/strong&gt;: 10-20x faster than byte-by-byte&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Smart overlap&lt;/strong&gt;: 80-90% reduction in redundant tokenization &lt;ul&gt; &lt;li&gt;Regular boundaries: ~5-15% redundant work&lt;/li&gt; &lt;li&gt;Safe boundaries: ~1-2% redundant work&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt; (depth 4, 16 chunks):&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Regular overlap: 16 chunks × 5KB overlap = 80KB redundant tokenization&lt;/li&gt; &lt;li&gt;Safe boundaries: 16 chunks × 500 bytes = 8KB redundant tokenization&lt;/li&gt; &lt;li&gt;&lt;strong&gt;10x less wasted work&lt;/strong&gt;&lt;/li&gt; &lt;/ul&gt; &lt;h2 id=&quot;the-benchmark-story&quot;&gt;The Benchmark Story&lt;/h2&gt; &lt;h3 id=&quot;headline-numbers&quot;&gt;Headline Numbers&lt;/h3&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt; &lt;th&gt;Input Size&lt;/th&gt; &lt;th&gt;Serial&lt;/th&gt; &lt;th&gt;Best Parallel&lt;/th&gt; &lt;th&gt;Speedup&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;100KB&lt;/td&gt; &lt;td&gt;23ms&lt;/td&gt; &lt;td&gt;&lt;strong&gt;11ms&lt;/strong&gt;&lt;/td&gt; &lt;td&gt;&lt;strong&gt;2.2x&lt;/strong&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;500KB&lt;/td&gt; &lt;td&gt;122ms&lt;/td&gt; &lt;td&gt;&lt;strong&gt;39ms&lt;/strong&gt;&lt;/td&gt; &lt;td&gt;&lt;strong&gt;3.1x&lt;/strong&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;1MB&lt;/td&gt; &lt;td&gt;264ms&lt;/td&gt; &lt;td&gt;&lt;strong&gt;87ms&lt;/strong&gt;&lt;/td&gt; &lt;td&gt;&lt;strong&gt;3.0x&lt;/strong&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;4MB&lt;/td&gt; &lt;td&gt;1.10s&lt;/td&gt; &lt;td&gt;&lt;strong&gt;335ms&lt;/strong&gt;&lt;/td&gt; &lt;td&gt;&lt;strong&gt;3.3x&lt;/strong&gt;&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;&lt;strong&gt;Test environment&lt;/strong&gt;: M-series Mac, release build with criterion benchmarks, BERT tokenizer.&lt;/p&gt; &lt;h2 id=&quot;deliberate-trade-off-word-ids&quot;&gt;Deliberate Trade-off: Word IDs&lt;/h2&gt; &lt;p&gt;We intentionally &lt;strong&gt;do not compute word IDs&lt;/strong&gt; in parallel mode. Here’s why:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;Word IDs are rarely needed&lt;/strong&gt;: Most use cases (LLM inference, embeddings) don’t need them&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Computing them would reduce speedup&lt;/strong&gt;: Would require additional O(n) work or complex heuristics&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Clear fallback&lt;/strong&gt;: If you need word IDs, use serial encoding: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tokenizer.encode()&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: 99% of users get 3x speedup. The 1% who need word IDs can use serial mode.&lt;/p&gt; &lt;h2 id=&quot;how-to-use-it&quot;&gt;How to Use It&lt;/h2&gt; &lt;h3 id=&quot;rust-api&quot;&gt;Rust API&lt;/h3&gt; &lt;div class=&quot;language-rust highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;k&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;tokenizers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Tokenizer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Load your tokenizer&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tokenizer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Tokenizer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;from_file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;tokenizer.json&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Option 1: Auto mode (recommended) - picks best strategy&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;encoding&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tokenizer&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.encode_parallel_single&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Option 2: Force streaming for huge inputs&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;encoding&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tokenizer&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.encode_streaming&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Option 3: Custom configuration&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;tokenizers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nn&quot;&gt;tokenizer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nn&quot;&gt;parallel_encode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ParallelConfig&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ParallelMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;config&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;ParallelConfig&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;streaming&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;.with_block_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 8KB blocks (optimal)&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;.with_threshold&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;50_000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Minimum size for parallelism&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;encoding&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tokenizer&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.encode_parallel_with_config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;h3 id=&quot;python-api&quot;&gt;Python API&lt;/h3&gt; &lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tokenizers&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Tokenizer&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Load your tokenizer &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tokenizer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Tokenizer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;from_file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;tokenizer.json&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Option 1: Auto mode - just works &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;long_text&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;large_document.txt&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;encoding&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tokenizer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;encode_parallel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;long_text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Option 2: Streaming mode for multi-MB inputs &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;huge_text&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;massive_document.txt&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# 5MB+ &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;encoding&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tokenizer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;encode_streaming&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;huge_text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Access results (same as regular encode) &lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;encoding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ids&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;encoding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tokens&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;encoding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;offsets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;h3 id=&quot;try-it-yourself&quot;&gt;Try It Yourself&lt;/h3&gt; &lt;p&gt;The code lives on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;parallel&lt;/code&gt; branch on &lt;a href=&quot;https://github.com/cxuu/tokenizers/tree/parallel&quot;&gt;github.com/cxuu/tokenizers&lt;/a&gt;.&lt;/p&gt; &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;c&quot;&gt;# Clone the repository&lt;/span&gt; git clone https://github.com/cxuu/tokenizers.git &lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;tokenizers git checkout parallel &lt;span class=&quot;c&quot;&gt;# Run the benchmarks&lt;/span&gt; cargo bench &lt;span class=&quot;nt&quot;&gt;--bench&lt;/span&gt; parallel_single_benchmark &lt;span class=&quot;c&quot;&gt;# Run the tests&lt;/span&gt; cargo &lt;span class=&quot;nb&quot;&gt;test &lt;/span&gt;parallel &lt;span class=&quot;c&quot;&gt;# Try the Python bindings&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;bindings/python pip &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;maturin maturin develop &lt;span class=&quot;nt&quot;&gt;--release&lt;/span&gt; python &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot; from tokenizers import Tokenizer tokenizer = Tokenizer.from_pretrained(&apos;bert-base-uncased&apos;) text = &apos;Hello world! &apos; * 10000 encoding = tokenizer.encode_parallel(text) print(f&apos;Encoded {len(encoding.ids)} tokens&apos;) &quot;&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;&lt;strong&gt;Feedback welcome&lt;/strong&gt;: This is ready for community testing. Try it on your workloads and let me know how it performs! &lt;br /&gt; &lt;br /&gt; &lt;br /&gt;&lt;/p&gt; &lt;hr /&gt; &lt;h2 id=&quot;appendix-a-the-algorithm-split-encode-filter-merge&quot;&gt;Appendix A: The Algorithm: Split, Encode, Filter, Merge&lt;/h2&gt; &lt;p&gt;Let’s walk through a concrete example:&lt;/p&gt; &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;Original Input (100 bytes): &quot;The quick brown fox jumps over the lazy dog. The dog was very lazy indeed.&quot; ├─────────────────────────────────────────────────────────────────────────┤ 0 50 100 &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;&lt;strong&gt;Step 1: Split with Overlap&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;We split at position 50, but encode overlapping regions:&lt;/p&gt; &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;Left chunk (bytes 0-60): &quot;The quick brown fox jumps over the lazy dog. The dog wa&quot; Right chunk (bytes 40-100): &quot;lazy dog. The dog was very lazy indeed.&quot; └─────┘ 20-byte overlap (bytes 40-60) Why overlap? The midpoint might fall in the middle of a token! &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;&lt;strong&gt;Step 2: Encode Both Chunks in Parallel&lt;/strong&gt;&lt;/p&gt; &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;Left encoding: ┌─────┬───────┬───────┬─────┬───────┬──────┬─────┬──────┬─────┬─────┬─────┬─────┬─────┬─────┐ │ The │ quick │ brown │ fox │ jumps │ over │ the │ lazy │ dog │ . │ The │ dog │ was │ wa │ └─────┴───────┴───────┴─────┴───────┴──────┴─────┴──────┴─────┴─────┴─────┴─────┴─────┴─────┘ 0 4 10 16 20 26 31 35 40 44 46 50 54 58 Right encoding (offsets relative to byte 40): ┌──────┬─────┬─────┬─────┬─────┬─────┬──────┬──────┬───────┬─────┐ │ lazy │ dog │ . │ The │ dog │ was │ very │ lazy │ indeed│ . │ └──────┴─────┴─────┴─────┴─────┴─────┴──────┴──────┴───────┴─────┘ 0 5 9 11 15 19 23 28 33 39 (relative offsets - will shift to global) &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;&lt;strong&gt;Step 3: Shift Right Encoding to Global Coordinates&lt;/strong&gt;&lt;/p&gt; &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;Right encoding after shifting by 40: ┌──────┬─────┬─────┬─────┬─────┬─────┬──────┬──────┬───────┬─────┐ │ lazy │ dog │ . │ The │ dog │ was │ very │ lazy │ indeed│ . │ └──────┴─────┴─────┴─────┴─────┴─────┴──────┴──────┴───────┴─────┘ 40 45 49 51 55 59 63 68 73 79 &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;&lt;strong&gt;Step 4: Filter at Midpoint&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;Here’s the key insight: &lt;strong&gt;we keep tokens that “belong” to each side based on where they start&lt;/strong&gt;.&lt;/p&gt; &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;Filtering rule: - From LEFT: Keep tokens where start_offset &amp;lt; midpoint (50) - From RIGHT: Keep tokens where start_offset &amp;gt;= midpoint (50) Left after filtering (keep start &amp;lt; 50): ┌─────┬───────┬───────┬─────┬───────┬──────┬─────┬──────┬─────┬─────┬─────┐ │ The │ quick │ brown │ fox │ jumps │ over │ the │ lazy │ dog │ . │ The │ └─────┴───────┴───────┴─────┴───────┴──────┴─────┴──────┴─────┴─────┴─────┘ 0 4 10 16 20 26 31 35 40 44 46 ✓ All &amp;lt; 50 Right after filtering (keep start &amp;gt;= 50): ┌─────┬─────┬──────┬──────┬───────┬─────┐ │ dog │ was │ very │ lazy │ indeed│ . │ └─────┴─────┴──────┴──────┴───────┴─────┘ 55 59 63 68 73 79 ✓ All &amp;gt;= 50 &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;&lt;strong&gt;Step 5: Concatenate&lt;/strong&gt;&lt;/p&gt; &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;Final result: ┌─────┬───────┬───────┬─────┬───────┬──────┬─────┬──────┬─────┬─────┬─────┬─────┬─────┬──────┬──────┬───────┬─────┐ │ The │ quick │ brown │ fox │ jumps │ over │ the │ lazy │ dog │ . │ The │ dog │ was │ very │ lazy │ indeed│ . │ └─────┴───────┴───────┴─────┴───────┴──────┴─────┴──────┴─────┴─────┴─────┴─────┴─────┴──────┴──────┴───────┴─────┘ 0 4 10 16 20 26 31 35 40 44 46 50 54 59 64 69 75 This is IDENTICAL to what serial encoding would produce! &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;h3 id=&quot;correctness-proof&quot;&gt;Correctness Proof&lt;/h3&gt; &lt;p&gt;&lt;strong&gt;Theorem&lt;/strong&gt;: The parallel encoding with overlapping chunks produces identical results to serial encoding.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Proof&lt;/strong&gt;:&lt;/p&gt; &lt;p&gt;Let’s define our problem formally:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Input string &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;S&lt;/code&gt; of length &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;n&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Serial tokenization function &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;T(s)&lt;/code&gt; that produces a sequence of tokens with start/end offsets&lt;/li&gt; &lt;li&gt;Midpoint position &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;m&lt;/code&gt; where we split&lt;/li&gt; &lt;li&gt;Overlap size &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ω&lt;/code&gt; (omega)&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;Claim 1&lt;/strong&gt;: For any input substring, tokenization is deterministic.&lt;/p&gt; &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;∀ substring s: T(s) always produces the same token sequence &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;This is true by definition—tokenizers are deterministic state machines.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Claim 2&lt;/strong&gt;: The overlap is sufficient.&lt;/p&gt; &lt;p&gt;We choose overlap &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ω ≥ max_token_length&lt;/code&gt; to ensure:&lt;/p&gt; &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;For split at position m: - Left chunk: S[0 : m + ω] - Right chunk: S[m - ω : n] - Overlap region: S[m - ω : m + ω] &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Any token that spans position &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;m&lt;/code&gt; must:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Start at some position &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;s&lt;/code&gt; where &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;m - max_token_length &amp;lt; s &amp;lt; m + max_token_length&lt;/code&gt;&lt;/li&gt; &lt;li&gt;End at some position &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;e&lt;/code&gt; where &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;m - max_token_length &amp;lt; e &amp;lt; m + max_token_length&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Since &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ω ≥ max_token_length&lt;/code&gt;, both boundaries are captured by our overlap.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Claim 3&lt;/strong&gt;: The filtering rule is correct.&lt;/p&gt; &lt;p&gt;Define:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;L = T(S[0 : m + ω])&lt;/code&gt; - tokens from left chunk&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;R = T(S[m - ω : n])&lt;/code&gt; - tokens from right chunk (shifted by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;m - ω&lt;/code&gt;)&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;L_filtered&lt;/code&gt; = tokens from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;L&lt;/code&gt; where &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;start_offset &amp;lt; m&lt;/code&gt;&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;R_filtered&lt;/code&gt; = tokens from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;R&lt;/code&gt; where &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;start_offset ≥ m&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;We need to prove: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;L_filtered ⊕ R_filtered = T(S)&lt;/code&gt; (where ⊕ is concatenation)&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Proof by construction&lt;/strong&gt;:&lt;/p&gt; &lt;p&gt;Consider any token &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;t&lt;/code&gt; in the serial encoding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;T(S)&lt;/code&gt; with start offset &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;s&lt;/code&gt;:&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Case 1&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;s &amp;lt; m&lt;/code&gt; (token starts before midpoint)&lt;/p&gt; &lt;p&gt;The token is completely determined by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;S[s : s + len(t)]&lt;/code&gt;. Since the left chunk includes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;S[0 : m + ω]&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;s &amp;lt; m&lt;/code&gt;, we have &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;s + len(t) &amp;lt; m + max_token_length ≤ m + ω&lt;/code&gt;. Therefore, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;S[s : s + len(t)] ⊂ S[0 : m + ω]&lt;/code&gt;, so the token appears in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;L&lt;/code&gt;. Since &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;s &amp;lt; m&lt;/code&gt;, it passes the filter and appears in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;L_filtered&lt;/code&gt;. ✓&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Case 2&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;s ≥ m&lt;/code&gt; (token starts at or after midpoint)&lt;/p&gt; &lt;p&gt;The token is completely determined by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;S[s : s + len(t)]&lt;/code&gt;. Since the right chunk includes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;S[m - ω : n]&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;s ≥ m&lt;/code&gt;, we have &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;s ≥ m &amp;gt; m - ω&lt;/code&gt;. Therefore, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;S[s : s + len(t)] ⊂ S[m - ω : n]&lt;/code&gt;, so the token appears in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;R&lt;/code&gt; (after shifting by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;m - ω&lt;/code&gt;). Since &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;s ≥ m&lt;/code&gt; (after shifting to global coordinates), it passes the filter and appears in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;R_filtered&lt;/code&gt;. ✓&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Case 3&lt;/strong&gt;: No token appears in both filters&lt;/p&gt; &lt;p&gt;Suppose a token &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;t&lt;/code&gt; appears in both &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;L_filtered&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;R_filtered&lt;/code&gt;. Then:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;From &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;L_filtered&lt;/code&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;start(t) &amp;lt; m&lt;/code&gt;&lt;/li&gt; &lt;li&gt;From &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;R_filtered&lt;/code&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;start(t) ≥ m&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;This is a contradiction. Therefore, no duplicate tokens. ✓&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;: Every token from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;T(S)&lt;/code&gt; appears exactly once in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;L_filtered ⊕ R_filtered&lt;/code&gt;, in the correct order with correct offsets. QED.&lt;/p&gt; &lt;h3 id=&quot;why-this-actually-works-in-practice&quot;&gt;Why This Actually Works in Practice&lt;/h3&gt; &lt;p&gt;The theoretical proof is nice, but here’s the practical insight:&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Tokenization has no long-range dependencies&lt;/strong&gt;. A BPE tokenizer processes text with a sliding window of at most &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max_token_length&lt;/code&gt; bytes. By overlapping more than this maximum, we ensure that:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;&lt;strong&gt;Every token appears in at least one chunk&lt;/strong&gt;: No token is “cut off” by the split&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Boundary tokens appear in both chunks&lt;/strong&gt;: The overlap captures them&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Filtering is deterministic&lt;/strong&gt;: We keep each token exactly once based on where it starts&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Order is preserved&lt;/strong&gt;: Left tokens come before right tokens by construction&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;&lt;strong&gt;Concrete example&lt;/strong&gt; of a boundary token:&lt;/p&gt; &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 13 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;Input: &quot;...lazy dog. The dog...&quot; ↑ Split here (position 50) Left chunk: &quot;...lazy dog. The&quot; → produces tokens [..., &quot;lazy&quot;, &quot; dog&quot;, &quot;.&quot;, &quot; The&quot;] Right chunk: &quot; dog. The dog...&quot; → produces tokens [&quot; dog&quot;, &quot;.&quot;, &quot; The&quot;, &quot; dog&quot;, ...] Token &quot; dog&quot; (starting at position 45) appears in BOTH encodings. Filtering: - In left encoding: start=45 &amp;lt; 50 → KEEP ✓ - In right encoding: start=45 &amp;lt; 50 → DISCARD ✗ Result: Token appears exactly once in final output. &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;h3 id=&quot;handling-edge-cases&quot;&gt;Handling Edge Cases&lt;/h3&gt; &lt;p&gt;&lt;strong&gt;Q: What if the split falls in the middle of a multi-byte UTF-8 character?&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;&lt;strong&gt;A&lt;/strong&gt;: We adjust to the nearest character boundary before creating chunks. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;find_split_point()&lt;/code&gt; function ensures splits only happen at valid UTF-8 boundaries.&lt;/p&gt; &lt;div class=&quot;language-rust highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;find_char_boundary_forward&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;usize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;usize&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;mut&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.min&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.is_char_boundary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;&lt;strong&gt;Q: What about tokenizers with special token handling?&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;&lt;strong&gt;A&lt;/strong&gt;: Special tokens ([CLS], [SEP], etc.) are added via post-processing AFTER parallel encoding. The core encoding produces tokens without special tokens, then the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;post_process()&lt;/code&gt; function adds them identically to serial mode.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Q: How do we ensure overlap is large enough?&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;&lt;strong&gt;A&lt;/strong&gt;: We query the vocabulary for the longest token:&lt;/p&gt; &lt;div class=&quot;language-rust highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;max_token_byte_length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;usize&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.get_vocab&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;.keys&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;.map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;.max&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;.unwrap_or&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;For BERT: ~25 bytes For GPT-2: ~50 bytes For byte-level BPE: ~100 bytes&lt;/p&gt; &lt;p&gt;We use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;overlap = max_token_length × 3 + safety_margin&lt;/code&gt; to be conservative. At safe boundaries (sentence/paragraph ends), we reduce to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max_token_length + safety_margin&lt;/code&gt; because tokenization is guaranteed to be consistent.&lt;/p&gt; &lt;h3 id=&quot;recursive-application&quot;&gt;Recursive Application&lt;/h3&gt; &lt;p&gt;The same merge logic applies recursively:&lt;/p&gt; &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;Depth 0: Full input (1MB) │ ├─ Split at 500KB │ Depth 1: ├─ Left (0-500KB) ├─ Right (500KB-1MB) │ │ │ │ │ ├─ Split at 250KB │ ├─ Split at 750KB │ │ │ │ Depth 2: │ ├─L1 (0-250KB) ├─R1 │ ├─L2 ├─R2 │ │ │ │ │ │ │ encode() encode() │ encode() encode() │ ↓ ↓ │ ↓ ↓ │ filter + merge │ filter + merge │ ↓ │ ↓ │ merged_L │ merged_R │ ↓ │ ↓ └────────────┴──────────────┴────────────┘ filter + merge ↓ Final encoding ✓ &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Each merge applies the same filter-and-concatenate logic. The correctness proof composes: if each level produces correct results, the final result is correct.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;The challenge&lt;/strong&gt;: Making this practical requires five key optimizations that reduce overhead and maximize cache efficiency.&lt;/p&gt; </description> <pubDate>Tue, 25 Nov 2025 00:00:00 +0000</pubDate> <link>/parallel-tokenizer/</link> <guid isPermaLink="true">/parallel-tokenizer/</guid> <category>artificial intelligence</category> <category>llm</category> </item> <item> <title>Accelerate LLM Inference with Speculative Decoding</title> <description>&lt;p&gt;Many inference speedup techniques mirror the classic systems regime—such as caching, paging, tiling, pipelining, and speculative execution (e.g. branch prediction and cache prefetch). Speculative decoding, generalizing speculative execution to stochastic settings, produces several tokens in each forward pass, without changing the output distribution (model quality) or model parameters.&lt;/p&gt; &lt;p&gt;This post discusses two approaches of Speculative Decoding:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;Speculative sampling with a draft model (2023 &lt;a href=&quot;https://arxiv.org/abs/2211.17192&quot;&gt;paper&lt;/a&gt;)&lt;/li&gt; &lt;li&gt;Multiple decoding heads with Tree Attention (2024 &lt;a href=&quot;https://arxiv.org/abs/2401.10774&quot;&gt;paper&lt;/a&gt;)&lt;/li&gt; &lt;/ol&gt; &lt;h3 id=&quot;speculative-sampling-with-a-draft-model&quot;&gt;Speculative Sampling with a Draft Model&lt;/h3&gt; &lt;p&gt;Speculative sampling, or rejection sampling, uses an approximation/draft model—smaller and faster than the model you want to accelerate—to generate $k$ tokens autoregressively and then uses the larger model to verify the $k$ tokens in one pass.&lt;/p&gt; &lt;h4 id=&quot;how-to-verify-each-speculative-token&quot;&gt;How to verify each speculative token&lt;/h4&gt; &lt;p&gt;Let $M_p$ be the target model and $M_q$ be the approximation model. &lt;br /&gt; Let $p(x)$ be the probability of $x$ under $M_p$ (a shorthand for $p(x_t | x_1, \ldots, x_{t-1})$). &lt;br /&gt; Let $q(x)$ be the probability of $x$ under $M_q$.&lt;/p&gt; &lt;p&gt;Sample a token $x$ from $M_q$. Don’t wait for the big model to verify $x$. Continue to sample the next $k$ tokens from $M_q$.&lt;/p&gt; &lt;p&gt;Then, for each speculative token $x$: &lt;br /&gt; If $p(x) \geq q(x)$, accept $x$. &lt;br /&gt; If $p(x) &amp;lt; q(x)$, accept $x$ with probability $p(x) / q(x)$; if $x$ not accepted, sample again from an adjusted distribution of $M_p$, where $p’(x) = norm(max(0, p(x) - q(x)))$.&lt;/p&gt; &lt;p&gt;It is &lt;a href=&quot;https://arxiv.org/abs/2211.17192&quot;&gt;proven&lt;/a&gt; that $x$ sampled this way ensures $x \sim p(x)$.&lt;/p&gt; &lt;h4 id=&quot;verify-in-one-pass&quot;&gt;Verify in one pass&lt;/h4&gt; &lt;p&gt;The key insight is that the big model can verify the $k$ speculative tokens in one pass. The reason is that in the self-attention mechanism, the big model computes contextualized representations of all prefixes in parallel, i.e. the model outputs&lt;/p&gt; \[p(x_1 | \text{prefix}), \quad p(x_2 | \text{prefix} + x_1), \quad \ldots, \quad p(x_k | \text{prefix} + x_1 + x_2 + \ldots + x_{k-1})\] &lt;p&gt;simultaneously.&lt;/p&gt; &lt;h3 id=&quot;multiple-decoding-heads-with-tree-attention&quot;&gt;Multiple Decoding Heads with Tree Attention&lt;/h3&gt; &lt;p&gt;Choosing the right draft model is hard. What if we just reuse the same model?&lt;/p&gt; &lt;p&gt;&lt;a href=&quot;https://arxiv.org/abs/2401.10774&quot;&gt;Medusa&lt;/a&gt; proposes attaching multiple decoding heads to the same model. In the case of 2 heads, head 1 predicts the immediate next token, and head 2 predicts the second token after the prefix. Two heads output at the same time.&lt;/p&gt; &lt;div style=&quot;text-align: center&quot;&gt; &lt;p&gt;&lt;img src=&quot;/assets/images/speculative-decoding/medusa.png&quot; width=&quot;600&quot; /&gt;&lt;/p&gt; &lt;/div&gt; &lt;p&gt;(image &lt;a href=&quot;https://sites.google.com/view/medusa-llm&quot;&gt;source&lt;/a&gt;)&lt;/p&gt; &lt;h4 id=&quot;training-multiple-decoding-heads&quot;&gt;Training multiple decoding heads&lt;/h4&gt; &lt;p&gt;The standard model only has one decoding head tasked to predict the next token. Thus, to use multiple decoding heads, we need to train the extra heads. The training needs:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;loss function&lt;/li&gt; &lt;li&gt;tree attention &amp;amp; adjusted positional encoding&lt;/li&gt; &lt;/ul&gt; &lt;h4 id=&quot;loss-function&quot;&gt;Loss function&lt;/h4&gt; &lt;p&gt;Use the cross-entropy loss between the prediction of extra heads and the ground truth. To quote the paper:&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;Given the ground truth token $y_{t+k+1}$ at position $t+k+1$, the loss for the $k$-th head is $L_k = - \log p_t^{(k)} (y_{t+k+1})$, where $p_t^{(k)} (y)$ denotes the probability of token $y$ predicted by the $k$-th head.&lt;/p&gt; &lt;p&gt;We also observe that $L_k$ is larger when $k$ is larger, which is reasonable since the prediction of the $k$-th head is more uncertain when $k$ is larger. Therefore, we can add a weight $\lambda_k$ to $L_k$ to balance the loss of different heads. The final loss is:&lt;/p&gt; \[\mathcal{L}_{\text{MEDUSA-1}} = \sum_{k=1}^{K} -\lambda_k \log p_t^{(k)}(y_{t+k+1}).\] &lt;/blockquote&gt; &lt;h4 id=&quot;tree-attention--adjusted-positional-encoding&quot;&gt;Tree attention &amp;amp; adjusted positional encoding&lt;/h4&gt; &lt;p&gt;Suppose the first decoding head predicts 2 candidates for the next token, and the second decoding head predicts 3 candidates for the next next token. We have a tree of $2 \times 3 = 6$ branches. The tree structure creates two challenges:&lt;/p&gt; &lt;ol&gt; &lt;li&gt; &lt;p&gt;We need to adjust attention mask such that a token generated from a specific candidate path can only attend to previous tokens within that same path and should ignore other branches.&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;We need to adjust positional encoding because there are multiple candidates for the same position (as in depth in the tree).&lt;/p&gt; &lt;/li&gt; &lt;/ol&gt; &lt;p&gt;The solution is Tree Attention shown below. Note that the attention mask exclusively permits attention flow from the current token back to its antecedent tokens.&lt;/p&gt; &lt;div style=&quot;text-align: center&quot;&gt; &lt;p&gt;&lt;img src=&quot;/assets/images/speculative-decoding/tree_attn.png&quot; width=&quot;600&quot; /&gt;&lt;/p&gt; &lt;/div&gt; &lt;p&gt;(image &lt;a href=&quot;https://sites.google.com/view/medusa-llm&quot;&gt;source&lt;/a&gt;)&lt;/p&gt; </description> <pubDate>Tue, 11 Mar 2025 00:00:00 +0000</pubDate> <link>/speculative-decoding/</link> <guid isPermaLink="true">/speculative-decoding/</guid> <category>artificial intelligence</category> <category>llm</category> </item> <item> <title>Parallelizing Multi-Head Attention</title> <description>&lt;p&gt;In the multi-head attention mechanism, why after reshaping the projection matrices for Q/K/V from 3 dimensions to 4, we need to transpose the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tokens&lt;/code&gt; dimension with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heads&lt;/code&gt; dimension?&lt;/p&gt; &lt;p&gt;Using the canonical example code for Attention Heads below as an example, why do we need &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Q = self.W_q(x).reshape(B, T, self.num_heads, self.head_dim).transpose(1, 2)&lt;/code&gt;?&lt;/p&gt; &lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;torch&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;torch.nn&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nn&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MultiHeadSelfAttention&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Module&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;embed_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_heads&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;super&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num_heads&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_heads&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;head_dim&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;embed_size&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;//&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_heads&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Split embeddings across heads &lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Linear layers for Q, K, V &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;W_q&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Linear&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;embed_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;embed_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;W_k&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Linear&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;embed_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;embed_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;W_v&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Linear&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;embed_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;embed_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Fully connected output layer, i.e. $W^O$ &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fc_out&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Linear&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;embed_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;embed_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;forward&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;D&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shape&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Batch, Seq_len, Embedding_dim &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Q&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;W_q&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;reshape&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num_heads&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;head_dim&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;transpose&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;K&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;W_k&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;reshape&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num_heads&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;head_dim&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;transpose&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;V&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;W_v&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;reshape&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num_heads&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;head_dim&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;transpose&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Compute attention scores (scaled dot-product attention) &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;scores&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;torch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;matmul&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Q&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;K&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;transpose&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;head_dim&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;attn&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;torch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;softmax&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;scores&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dim&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Normalize scores &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;output&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;torch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;matmul&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;attn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;V&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Apply attention to values &lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Concatenate and project back to embedding size &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;output&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;transpose&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;contiguous&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;reshape&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;D&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fc_out&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Example usage &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;torch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;randn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;512&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Batch size 1, Sequence length 10, Embedding size 512 &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;attention_layer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MultiHeadSelfAttention&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;embed_size&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;512&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_heads&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;output&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;attention_layer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shape&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Should be [1, 10, 512] &lt;/span&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;h3 id=&quot;review-multi-head-attention&quot;&gt;Review Multi-head Attention&lt;/h3&gt; &lt;p&gt;Instead of using a single attention head, transformers use multiple heads. Each attention head has its own set of projection matrices for Q, K, and V. Each head learns to focus on different types of relationships. For example:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;One head might focus on long-range dependencies, such as linking a subject to its verb.&lt;/li&gt; &lt;li&gt;Another might focus on local context, such as detecting adjective-noun pairs.&lt;/li&gt; &lt;li&gt;Another might specialize in syntax or semantics.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Each head has independent learnable weight/projection matrices $W_i^Q, W_i^K, W_i^V$, where $i$ is the head index. Each weight matrix has shape $(D, D/h)$ where $D$ is the embedding dimension and $h$ is the number of heads.&lt;/p&gt; &lt;p&gt;For input embeddings $X$ of shape $(B, T, D)$, the head $i$ computes:&lt;/p&gt; \[Q_i = X W_i^Q, \quad K_i = X W_i^K, \quad V_i = X W_i^V\] &lt;p&gt;The multi-head attention then concatenates the per-head attention outputs and linearly mixes them:&lt;/p&gt; \[\text{MultiHeadAttention}(Q, K, V) = \text{Concat}\left(\text{head}_1, \text{head}_2, \ldots, \text{head}_h\right)W^O\] &lt;p&gt;Each head has output shape $(B, T, D/h)$. The concatenated output has shape $(B, T, D)$. $W^O$ is trainable and has shape $(D, D)$. $W^O$ mixes information across heads and refines the final representation before passing it to the next layer.&lt;/p&gt; &lt;h3 id=&quot;reshaping-for-parallelization&quot;&gt;Reshaping for Parallelization&lt;/h3&gt; &lt;p&gt;Let’s break down &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Q = self.W_q(x).reshape(B, T, self.num_heads, self.head_dim).transpose(1, 2)&lt;/code&gt;:&lt;/p&gt; &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self.W_q&lt;/code&gt; is a linear layer (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nn.Linear(embed_size, embed_size)&lt;/code&gt;) that projects &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;x&lt;/code&gt; into a new representation, specifically the query (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Q&lt;/code&gt;) in multi-head attention. This linear transformation does not change the shape of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;x&lt;/code&gt;, which remains &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(B, T, D)&lt;/code&gt;.&lt;/p&gt; &lt;p&gt;In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.reshape(B, T, self.num_heads, self.head_dim)&lt;/code&gt;, we split the embedding dimension &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;D&lt;/code&gt; into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self.num_heads&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self.head_dim&lt;/code&gt; (where &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self.head_dim = D / self.num_heads&lt;/code&gt;). For example, if &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;D = 512&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self.num_heads = 8&lt;/code&gt;, then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self.head_dim = 512 / 8 = 64&lt;/code&gt;. The shape becomes: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(B, T, 8, 64)&lt;/code&gt;.&lt;/p&gt; &lt;p&gt;In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.transpose(1, 2)&lt;/code&gt;, we swap the sequence length dimension (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;T&lt;/code&gt;) and the number of heads dimension (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;num_heads&lt;/code&gt;). Note that index 1 is the second column. This changes the shape:&lt;/p&gt; &lt;p&gt;Before: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(B, T, num_heads, head_dim)&lt;/code&gt; &lt;br /&gt; After: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(B, num_heads, T, head_dim)&lt;/code&gt;&lt;/p&gt; &lt;p&gt;This rearrangement makes attention more efficient, because matrix multiplications can now parallelize across heads (each head operates independently on different parts of the embedding). The reason is the following.&lt;/p&gt; &lt;p&gt;In the code &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scores = torch.matmul(Q, K.transpose(-1, -2)) / (self.head_dim ** 0.5)&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;K.transpose(-1, -2)&lt;/code&gt; swaps the last two dimensions so that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;K&lt;/code&gt; has the shape &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;K^T: (B, num_heads, head_dim, T)&lt;/code&gt;.&lt;/p&gt; &lt;p&gt;Then, the matrix multiplication &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Q @ K^T&lt;/code&gt; is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(B, num_heads, T, head_dim) @ (B, num_heads, head_dim, T)&lt;/code&gt;, which results in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(B, num_heads, T, T)&lt;/code&gt;.&lt;/p&gt; &lt;p&gt;Thus, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Q @ K^T&lt;/code&gt; becomes more efficient after &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;transpose(1, 2)&lt;/code&gt; because:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;Parallel Computation for Each Head&lt;/strong&gt;: By keeping &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;num_heads&lt;/code&gt; as the second dimension, each head’s computation happens independently but in parallel across the batch, using optimized GPU kernels.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Better Memory Access Patterns&lt;/strong&gt;: GPUs are highly optimized for contiguous memory access. The transpose(1, 2) operation ensures that each head’s data is grouped together, improving cache efficiency during matrix multiplication.&lt;/li&gt; &lt;/ul&gt; </description> <pubDate>Fri, 28 Feb 2025 00:00:00 +0000</pubDate> <link>/multi-head-attention/</link> <guid isPermaLink="true">/multi-head-attention/</guid> <category>artificial intelligence</category> <category>llm</category> </item> <item> <title>Notes: The Lean Startup</title> <description>&lt;p&gt;Careful planning and execution work for general management but not for startups. Perfect execution is futile if you end up building something nobody wants (waste). The real progress for startups is not how many JIRA tickets we closed but how fast we gain validated learnings—what creates value for customers and their willingness to pay—while minimizing waste.&lt;/p&gt; &lt;p&gt;Systematically break down a business plan and test each part—define a clear hypothesis (predictions about what is supposed to happen) then A/B test to prove the predictions. Two leap-of-faith assumptions:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Value hypothesis: do customers find value&lt;/li&gt; &lt;li&gt;Growth hypothesis: how new customers discover the product&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Test assumptions with MVP that targets early adopters, in the Build-Measure-Learn loop. Any work beyond what was required to learn is waste, no matter how important it seemed. If we do not know who the customer is, we do not know what quality is. Even a “low-quality” MVP can act in service of building a great high-quality product. Plan the Build-Measure-Learn loop in reverse order: decide what we need to learn, then figure out what we need to measure, then see what product we need to build to experiment. Launch MVP under a different brand name if you worry about branding risk. Commit to iteration: don’t give up hope due to bad news from MVPs, but experiment more, learn more, and maybe pivot.&lt;/p&gt; &lt;p&gt;Do not blindly copy successful startups’ techniques. Charging customers from day one works for Stripe but not Facebook. Low-quality early prototypes works for Facebook but not mission-critical industries. Always experiment to see what works in our unique circumstances.&lt;/p&gt; &lt;p&gt;Eventually a successful startup will compete with fast followers. A head start is rarely large enough to matter. The only way to win is to learn faster than everyone else.&lt;/p&gt; &lt;p&gt;Vanity metrics, such as gross number of customers, are not actionable. We cannot conclude whether metrics growth is due to&lt;/p&gt; &lt;ol&gt; &lt;li&gt;latest product development, or&lt;/li&gt; &lt;li&gt;driven by decisions the team had made and that current initiatives have no impact.&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;Use cohort-based metrics (e.g. among users who signed up in June, what percentage exhibits behaviors we want), and use A/B test to conclude causality. Measure team’s productivity in units of validated learning, not the production of new features.&lt;/p&gt; &lt;p&gt;Pivot: test a new fundamental hypothesis in business model, product road map, partnership, customer segments, engine of growth. The decision to pivot depends on data &amp;amp; intuition. Misguided decision to persevere is value destructive. Signs of time to pivot: the decreasing effectiveness of product experiments and the general feeling that product development should be more productive. Startup’s runway is the number of pivots it can still make. To extend runway, achieve the same amount of validated learning at lower cost in shorter time. Schedule in advance regular “pivot or persevere” meetings. In pivots, don’t throw out everything and start over. Repurpose what has been built and learned.&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;My personal thought: &lt;br /&gt; The search for PMF is like gradiant descent, a combination of intuition and data. Gradiant descent is an optimization algorithm: start at a point (your best guess), then iteratively descent in the direction of “slope”. The process is almost mechanical, but you need to start with an intuitive guess. Pivot means to find a new starting point. You need to pivot when &lt;br /&gt; 1) the “slope” around the current point is flat in all directions. Nothing you do seems to improve the metrics, or &lt;br /&gt; 2) you are trapped in a local minimum (saturating early adoptors) and want to find a better local minimum (mainstream customers).&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;Once you have found success with early adopters, you want to sell to mainstream customers. Early adopter market is saturated quickly despite prior “up and to the right” results. Mainstream customers have different and more demanding requirements. This is a customer segment pivot. The actions we need to win mainstream customers is different from how we won early adopters. Pivot requires new MVP. Just as lean manufacturing uses just-in-time production to reduce in-process inventory, Lean Startups practice just-in-time scalability, conducting product experiments with small batch size. Imagine that the letters didn’t fit in the envelopes. With the large-batch approach, we wouldn’t find that out until nearly the end. With small batches, we’d know almost immediately. Smaller batch size (small diff in product code change) means shorter Build-Measure-Learn cycle and less WIP waste.&lt;/p&gt; &lt;p&gt;&lt;b&gt;Sustainable growth&lt;/b&gt;: new customers come from the actions of past customers: word of mouth, product usage (wearing designer cloths), funded advertising, repeat purchase.&lt;/p&gt; &lt;p&gt;&lt;b&gt;Sticky Engine of Growth&lt;/b&gt;: repeat usage; use customer retention rate to test growth hypothesis. Other metrics like activation rate and revenue per customer can test value hypothesis but has little impact on growth. If the rate of new customer acquisition exceeds the churn rate, the product will grow.&lt;/p&gt; &lt;p&gt;&lt;b&gt;Viral Engine of Growth&lt;/b&gt;: focus on increasing the viral coefficient. Many viral products do not charge customers but advertisers, because viral products cannot afford to have any friction impede the process of signing customers up.&lt;/p&gt; &lt;p&gt;&lt;b&gt;Paid Engine of Growth&lt;/b&gt;: advertising, outbound sales, foot traffic. Use LTV/CAC to test growth hypothesis. Over time, CAC is bid up by competition.&lt;/p&gt; &lt;p&gt;A startup can evaluate PMF by evaluating each Build-Measure-Learn iteration using innovation accounting. Every engine is tied to a set of customers and their habits, preferences, channels, and interconnections and thus eventually runs out of gas.&lt;/p&gt; &lt;p&gt;If the boss tends to split the difference, the best way to influence the boss is to take the most extreme position. Your opponants will do the same. Over time, everyone will take the most polarized positions. Don’t split the difference. Instead create a sandbox for innovation that will contain the impact but not methods of the new innovation. It works as follows:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;Any team can create a true split-test experiment that affects only the sandboxed parts of the product or service (for a multipart product) or only certain customer segments or territories (for a new product).&lt;/li&gt; &lt;li&gt;One team must see the whole experiment through from end to end.&lt;/li&gt; &lt;li&gt;No experiment can run longer than a specified amount of time.&lt;/li&gt; &lt;li&gt;No experiment can affect more than a specified percentage of customers.&lt;/li&gt; &lt;li&gt;Every experiment has to be evaluated on the basis of a single standard report of five to ten actionable metrics.&lt;/li&gt; &lt;li&gt;Every team that works inside the sandbox use the same metrics to evaluate success.&lt;/li&gt; &lt;li&gt;Any team that creates an experiment must monitor the metrics and customer reactions (support calls, social media reaction, forum threads, etc.) while the experiment is in progress and abort it if something catastrophic happens.&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;If you like notes like this, check out my &lt;a href=&quot;/bookshelf&quot;&gt;bookshelf&lt;/a&gt;.&lt;/p&gt; </description> <pubDate>Sun, 21 Jan 2024 00:00:00 +0000</pubDate> <link>/lean-startup/</link> <guid isPermaLink="true">/lean-startup/</guid> <category>startup</category> </item> <item> <title>Notes: Venture Deals</title> <description>&lt;p&gt;&lt;b&gt;Before Fundraise&lt;/b&gt;: Allow minimum three to six months to raise money. Have a clean cut from last job to avoid IP disputes. Prepare data site (Certificate of Incorporation, Bylaws, board minutes, cap table, customer list, product roadmap, org chart, employment agreements, budgets, financial statements). Some VC deals fail to close because of one missing IP assignment agreement. If you want money, ask for advice. Develop relationships with VC before fundraising. Research &amp;amp; make the outreach personal. Find mentors, not fundraising advisors asking for a cut.&lt;/p&gt; &lt;p&gt;&lt;b&gt;During Fundraise&lt;/b&gt;: Do not email a teaser, hoping to share more in meeting. Whatever you send a VC may be your last, so send the full yet concise pitch. The presentation is to communicate the same info in the executive summary but with more examples and visuals. Aim for 10 slides or fewer. Offer a prototype or demo that VC can interact. Demo is the best way to show your vision. Watch VC reactions during demo. Did their eyes light up? Do they understand the domain? Raise enough to get to the next milestone, plus buffer. Hire experienced startup lawyer with fee cap. Don’t let VC talk you out of your lawyer choice. After you’ve had a second meeting, ask what the process is going forward. Get multiple term sheets to create competition. If a VC passes, insist on feedback, which improves your next pitch.&lt;/p&gt; &lt;p&gt;&lt;b&gt;Decision Maker&lt;/b&gt;: Have a lead investor representing the entire syndicate. If party round, set up a special-purpose limited partnership, not to chase down 75 signatures. At every VC, find out &amp;amp; talk to the decision makers. Reference check the VC: founders who went through hard times, like fired as CEO, learn how the VC handled tough situations. Add more to data site, but watch out for busywork that associates assigns, and the risk of leaking to a competing portco. Again, make sure decision makers are involved.&lt;/p&gt; &lt;p&gt;&lt;b&gt;Option Pool&lt;/b&gt;: Typical size of early-stage company option pool is 10% to 20%. Smaller pools for later stage. “We have enough options to cover our needs. If we need to expand the pool before the next financing, we will provide full antidilution protection for you.” VCs want to minimize future dilution by enlarging the option pool up front. Founders should push back with an option budget that lists out futures hires until next financing and the option grant to land each hire.&lt;/p&gt; &lt;p&gt;&lt;b&gt;Liquidation Preference&lt;/b&gt;: In early stage, it’s in the best interest of both VC and founder to have a simple liquidation preference and no participation. In future rounds, the terms are often inherited from the early stage terms. If the seed investor doesn’t invest in future rounds, his economics in many outcomes could be worse with participating and end up looking like the common holders (in terms of returns), since their preference amounts are so small.&lt;/p&gt; &lt;p&gt;&lt;b&gt;Pay to Play&lt;/b&gt;: Investors must invest pro-ratably in future financings (paying) not to have their preferred stock converted to common (playing) or lose anti-dilution rights. Not a lifetime guarantee but an incentive to follow on, if other investors decide to invest in next round. Pay to play reduces liquidation preferences for the nonparticipating investors and ensures only committed investors have preferred stock. If VC pushes back, “Why? Are you not going to fund the company in the future if other investors agree to?” Avoid the pay-to-play scenario where VC has the right to force a recapitalization (e.g. financing at a $0 pre-money valuation) if fellow investors don’t play in the new round.&lt;/p&gt; &lt;p&gt;&lt;b&gt;Founder Vesting&lt;/b&gt;: Negociate to treat vesting as a clawback with an 83(b) election. Single-trigger: accelerated vesting upon M&amp;amp;A. Double-trigger: accelerated vesting upon M&amp;amp;A and being fired. Balanced approach: double trigger with one-year acceleration.&lt;/p&gt; &lt;p&gt;&lt;b&gt;Anti-Dilution&lt;/b&gt;: protect prior investors in a down round (equity issued at lower price). Flavors: weighted average (normal), or full ratchet (rare; reduce earlier round price to new price). Antidilution is often implemented as a price reduction for conversion to common. More exceptions in antidilution carve-outs, the better for founders.&lt;/p&gt; &lt;p&gt;&lt;b&gt;Board&lt;/b&gt;: Be wary of observers. Question what values they bring. Often they are associates. Sometimes they disclose board topics to brag to their friends. Get a small board. Independent board members usually get stock options 0.25% to 0.5% vest over 2 to 4 years. Observers don’t get options. Instead of controlling the board, VC uses protective provisions (veto rights on certain actions). Next-round investors want protective provisions too, but founders should push for all Preferred voting as a single class, instead of each Series voting separately. VCs charge all expenses associated with board meetings to the company. Mandate frugality. Place a cap early on the percentage of directors who can be VCs (not independent). Preemptively offer observer rights to dethroned director, or establish an executive committee of the board that can meet without everyone else.&lt;/p&gt; &lt;p&gt;&lt;b&gt;Drag Along&lt;/b&gt;: A compromise is to grant drag-along rights to the majority of the common stock, not the preferred. Preferred can convert some to common to force a majority at the cost of lowering the overall liquidation preference. IPO: preferred convert to common. Never give different automatic conversion terms for different series of preferred. Push for low threshold to conversion.&lt;/p&gt; &lt;p&gt;&lt;b&gt;Redemption&lt;/b&gt;: Ensure dividends require board majority approval. Allow investors to sell shares back to the company for a guaranteed return. Never agree to “Adverse Change Redemption” because it is vague, punitive, and investors can act on arbitrary judgments.&lt;/p&gt; &lt;p&gt;&lt;b&gt;No-shop&lt;/b&gt;: Do not agree to pay for legal fees until deal done. Avoid pre-financing contingence: 1. “Approval by Investors’ partnerships” means the term sheet has not been approved. 2. “Employment Agreements acceptable to investors”: review &amp;amp; negotiate full terms (e.g. what happens on termination?) before signing term sheet. Limit the no-shop period to 30 days (worst case 60 days), automatically canceled if VC terminates. Commitment should be bidirectional. You agree not to shop deals, VC agrees to close timely. Ask for exception for acquisitions. Frequently financings and acquisitions follow each other.&lt;/p&gt; &lt;p&gt;&lt;b&gt;Registration Rights&lt;/b&gt;: Always offered to investors. Lawyers often make innocuous edits on this section. Unnecessary. Upon IPO when the rights apply, investment bankers will restructure the deal.&lt;/p&gt; &lt;p&gt;&lt;b&gt;Right of First Refusal&lt;/b&gt;: Define “major investor”, only give such right to them and only if they play in subsequent rounds. Enforce stock sale also transfers obligations the original owner signed up for.&lt;/p&gt; &lt;p&gt;&lt;b&gt;Co-sale Agreement&lt;/b&gt;: This right says if a founder sells shares, investors can sell too. Hard to remove this right, but founders should ask for a floor. Why should VC hold it up if a founder just sells a small amount to pay off mortgage?&lt;/p&gt; &lt;p&gt;&lt;b&gt;SAFE&lt;/b&gt;: Some VCs consider valuation cap as a price ceiling to the next round, so do not disclose seed-round terms until you have negociated new price. Legal fee for priced equity round has dropped, no more than SAFE.&lt;/p&gt; &lt;p&gt;&lt;b&gt;Zombie VC&lt;/b&gt;: VC who past their investment period (usually 5 years) and did not raise a new fund. They can’t invest but still meet you. Waste time. Ask “when was your last investment” (more than a year = zombie). “How many more investments will you make out of the current fund?”&lt;/p&gt; &lt;p&gt;&lt;b&gt;Reserve&lt;/b&gt;: Fund approaching end of life creates pressure for liquidity. Underreserved VC may resist new financings, limit round size, or push for sale of company to limit dilution, even when more funding is right for commons. Pay-to-play creates more resistance in this case. Follow-on might come from new fund, or a different fund vehicle (“Opportunity” funds)&lt;/p&gt; &lt;p&gt;&lt;b&gt;Corporate VC&lt;/b&gt;: They look for more control, such as right of first refusal on acquisition, which you should never give.&lt;/p&gt; &lt;p&gt;&lt;b&gt;Negociation&lt;/b&gt;: Goals: achieving a good and fair result, not killing your personal relationship. Preparation is key. Focus on valuation, option pool, liquidation preferences, board, and voting controls. Know what concetions you are ok with and when to walk away. Get to know the other side ahead of time, play to their strengths, weaknesses, biases, curiosities, and insecurities. First-time founder has one advantage over seasoned VC: time. They got family, LPs, portcos to deal with. You got one company and this negocitation. Ask what the 3 most important terms are for VC. Explain yours too. Call them out if they pound hard on minor points. Don’t make threat you can’t deliver. Don’t say who else you are pitching to. Never provide term sheet from other VC. Don’t address deal points in order but focus on whole picture. “That’s the way it is because it’s market.” probe on why the market condition applies to you. Talk to other founders to get market intelligence. Push back with “Wait a minute, this term creates incentive misalignment. Let’s avoid a divisive relationship.” If stuck with terms you don’t love, next-round investors may fix them because they want your team happy and motivated. After big wins and some time together, renegociate with existing investors for founder-friend terms.&lt;/p&gt; &lt;p&gt;&lt;b&gt;Price&lt;/b&gt;: High valuation is risky because 1) VCs hold out for a higher exit (by big perf stack, or forbid sales below $X), then founders can’t sell at a price they would have been happy with. 2) At higher price, sophisticated investors demand more structure, resulting in significant outcome misalignment between early and late stage investors.&lt;/p&gt; &lt;p&gt;&lt;b&gt;Investment Banks&lt;/b&gt;: Avoid them at early stage. Hire them in acquisition. They maximize exit value. Best source of bankers: your board members, investors, colleagues, and other senior executives you trust. Hire a banker who knows your sector, like “enterprise SaaS”.&lt;/p&gt; &lt;p&gt;&lt;b&gt;Daily Operation&lt;/b&gt;: Hire an employment lawyer when a founder or exec leaves. Make sure equity &amp;amp; IP are settled to protect future fundraising and acquisition. If a company used a professional valuation firm, the valuation would be assumed to be correct unless the IRS could prove otherwise. File 83(b) election to start earlier the clock for long-term capital gains. Pay at least minimum-wage cash comp to full-time founders &amp;amp; execs.&lt;/p&gt; &lt;p&gt;If you like notes like this, check out my &lt;a href=&quot;/bookshelf&quot;&gt;bookshelf&lt;/a&gt;.&lt;/p&gt; </description> <pubDate>Fri, 19 Jan 2024 00:00:00 +0000</pubDate> <link>/venture-deals/</link> <guid isPermaLink="true">/venture-deals/</guid> <category>startup</category> <category>investment</category> </item> <item> <title>Scaling Istio</title> <description>&lt;p&gt;In a large, busy cluster, how do you scale Istio to address Istio-proxy Container being OOM-Killed and Istiod crashes if too many connected istio-proxies?&lt;/p&gt; &lt;h3 id=&quot;istio-proxy-container-oom-killed&quot;&gt;Istio-proxy Container OOM-Killed&lt;/h3&gt; &lt;h4 id=&quot;problem&quot;&gt;Problem&lt;/h4&gt; &lt;p&gt;If istio-proxy dies, Pod disconnects from the world, because istio routes the Pod’s ingress and egress through the istio-proxy container. Thus, the main application container cannot communicate with other services, and clients cannot reach the application either. This disrupts existing connections and risks cascading failure when loads shift to other replicas.&lt;/p&gt; &lt;p&gt;Out-of-memory kill is #1 reason for the istio-proxy death. The istio-proxy is configured with resource limits for CPU and memory, to avoid starving other workloads sharing the k8s Node. The istio-proxy is killed once it exceeds the memory limit.&lt;/p&gt; &lt;p&gt;Restarting istio-proxy won’t help: By default, Kubernetes uses the restart policy “Always” for Pods. Thus, if the istio-proxy container is OOM killed, Kubernetes will restart it. However, because the usage pattern has not changed, istio-proxy will enter OOMKilled again. This forms a crash loop and continued disruption to applications.&lt;/p&gt; &lt;p&gt;To keep bumping the memory limit is expensive and whack-a-mole. Overtime, you have increased the memory limit from 256Mi to 2Gi, which is per istio-proxy container. Given tens of thousands of Pods in istio mesh cross the hundreds of clusters, it is expensive to keep raising the limit. Furthermore, many people only increase the limit is when the oncall got paged about crash-looping Pods, which already impact customer traffic.&lt;/p&gt; &lt;h4 id=&quot;solution&quot;&gt;Solution&lt;/h4&gt; &lt;h5 id=&quot;use-sidecar-object-to-trim-unused-xds-config&quot;&gt;Use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Sidecar&lt;/code&gt; object to trim unused xDS config&lt;/h5&gt; &lt;p&gt;By default, Istio programs all sidecar proxies with the configuration to reach every workload in the mesh, as well as accept traffic on all the ports associated with the workload.&lt;/p&gt; &lt;p&gt;But if you have a locked down Istio mesh, and if a tenant must request for allow-listing such source namespace using some onboarding config, then the istio-proxy container does not need the full mesh config.&lt;/p&gt; &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Sidecar&lt;/code&gt; API object can restrict the set of services that the proxy can reach. Adopting the Sidecar objects will reduce the number of xDS pushes and overall xDS config size. You could templatize the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Sidecar&lt;/code&gt; objects and render them based on the per-namespace onboarding configs.&lt;/p&gt; &lt;p&gt;Below is an example Sidecar, which allows istio-proxies in the namespace “observability-cortex” to egress to four other namespaces.&lt;/p&gt; &lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;networking.istio.io/v1beta1&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Sidecar&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;myapp&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;egress&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;hosts&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;istio-system/*&quot;&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;my-upstream-ns/*&quot;&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;kube-system/*&quot;&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;observability/*&quot;&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;h5 id=&quot;use-telemetry-object-to-reduce-metrics-generation&quot;&gt;Use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Telemetry&lt;/code&gt; object to reduce metrics generation&lt;/h5&gt; &lt;p&gt;Istio collects and exports a wide range of Prometheus metrics. Metrics collection impacts memory usage. Istio-proxy doesn’t need to generate all metrics but only those we use. Consider customizing the metrics that Istio collects and exports.&lt;/p&gt; &lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;telemetry.istio.io/v1alpha1&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Telemetry&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;drop-unused-metrics-and-tags&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;istio-system&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# no selector specified, applies to all workloads in the namespace&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;metrics&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;providers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;prometheus&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;overrides&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;metric&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ALL_METRICS&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;tagOverrides&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;connection_security_policy&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;operation&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;REMOVE&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;destination_app&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;operation&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;REMOVE&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;destination_canonical_service&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;operation&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;REMOVE&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;destination_canonical_revision&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;operation&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;REMOVE&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;destination_principal&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;operation&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;REMOVE&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;metric&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;REQUEST_DURATION&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;metric&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;REQUEST_SIZE&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;metric&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;RESPONSE_SIZE&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;metric&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;TCP_CLOSED_CONNECTIONS&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;h5 id=&quot;istio-ambient-mesh&quot;&gt;Istio Ambient Mesh&lt;/h5&gt; &lt;p&gt;We can solve sidecar problems if we don’t run sidecar at all. Istio &lt;a href=&quot;https://istio.io/latest/blog/2022/introducing-ambient-mesh/&quot;&gt;ambient mesh&lt;/a&gt; is a sidecar-less approach to service mesh, replacing sidecar proxies with per-node and (not always necessary) per-namespace proxies. With fewer proxies, it will save us lots of money in CPU/Memory and provide shorter latency.&lt;/p&gt; &lt;p&gt;The general problems with sidecars and benefits of ambient mesh:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Kubernetes does not have first-class support for sidecars (&lt;a href=&quot;https://kubernetes.io/blog/2023/08/25/native-sidecar-containers/&quot;&gt;until k8s 1.28&lt;/a&gt;). App container might start before proxy ready, decide itself is unhealthy, and be in a restart loop. Short-lived Pods (Job) need to explicitly kill proxy for Pod to complete.&lt;/li&gt; &lt;li&gt;Istio upgrade requires restarting every pod to inject newer-version Istio proxies&lt;/li&gt; &lt;li&gt;Sidecar resources are underutilized&lt;/li&gt; &lt;li&gt;Difficult to calculate namespace quotas (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ResourceQuotas&lt;/code&gt;) because sidecars are transparent to tenants but consume namespace quotas.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;If you use Calico to enforce L4 NetworkPolicy for Pods, you might face a blocker to adopting ambient mesh because of conflicting IPTables rules that Calico owned (GitHub &lt;a href=&quot;https://github.com/istio/istio/issues/40973&quot;&gt;issue&lt;/a&gt; still open). But I encourage you to do another proof of concept, because someone (GitHub &lt;a href=&quot;https://github.com/istio/istio/issues/43871&quot;&gt;issue&lt;/a&gt;) used eBPF instead of IPTables to redirect traffic to ambient-mode proxies, thus working around the conflicting Calico IPTables rules.&lt;/p&gt; &lt;h3 id=&quot;istiod-crash-if-too-many-connected-istio-proxies&quot;&gt;Istiod crash if too many connected istio-proxies&lt;/h3&gt; &lt;h4 id=&quot;problem-1&quot;&gt;Problem&lt;/h4&gt; &lt;p&gt;Istiod is the control plane of istio. All istio-proxies connect to istiod. Istiod may crash when there were too many connected istio-proxies, specifically if they all were added at the same time by a tenant workload scaling out.&lt;/p&gt; &lt;p&gt;Most people run Istiod as a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Deployment&lt;/code&gt; with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HorizontalPodAutoscaler&lt;/code&gt; (HPA). You could mitigate the scaling issue by setting a high minimum for HPA, but doing so leads to low resource utilization at night and weekends, at odds with the very purpose of autoscaling. Moreover, istiod is still at risk when the tenants scale out aggressively.&lt;/p&gt; &lt;h4 id=&quot;solution-1&quot;&gt;Solution&lt;/h4&gt; &lt;h5 id=&quot;use-discoveryselectors-to-watch-in-mesh-namespaces-only&quot;&gt;Use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;discoverySelectors&lt;/code&gt; to watch in-mesh Namespaces only&lt;/h5&gt; &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;discoverySelectors&lt;/code&gt; configuration enables us to dynamically restrict the set of namespaces that are part of the mesh. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;discoverySelectors&lt;/code&gt; configuration declares what Istio control plane watches and processes. Not all tenant namespaces enable istio, so istiod could benefit from having to process less k8s events.&lt;/p&gt; &lt;h5 id=&quot;fine-tune-hpa&quot;&gt;Fine-tune HPA&lt;/h5&gt; &lt;p&gt;The default scale-up stabilization window is 300 seconds. We should reduce it to 10 seconds to be more responsive, but keep the scale-down stabilization window at 300s to avoid threshing.&lt;/p&gt; &lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt; apiVersion: autoscaling/v2beta2 kind: HorizontalPodAutoscaler metadata: name: istiod namespace: istio-system labels: app: istiod release: istio istio.io/rev: system install.operator.istio.io/owning-resource: unknown operator.istio.io/component: &quot;Pilot&quot; spec: maxReplicas: 48 &lt;span class=&quot;gd&quot;&gt;- minReplicas: 32 &lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+ minReplicas: 3 &lt;/span&gt; scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: istiod &lt;span class=&quot;gi&quot;&gt;+ behavior: + scaleUp: + stabilizationWindowSeconds: 10s # default is 300s + scaleDown: + stabilizationWindowSeconds: 300s &lt;/span&gt; metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 65 &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;h5 id=&quot;distribute-istio-proxy-connections-across-istiod-pods&quot;&gt;Distribute istio-proxy connections across Istiod Pods&lt;/h5&gt; &lt;p&gt;Istio doesn’t explicitly set a default maximum connection time between istio-proxy sidecars and istiod. Typically, the connections from the sidecars to istiod are long-lived gRPC connections used for service discovery, configuration updates, and certificate rotation, and they are expected to be maintained as long as istiod and the sidecars are running. This creates uneven distribution of loads on istiod Pods over time.&lt;/p&gt; &lt;p&gt;One idea is to set a max connection idle timeout for the istio-proxy to istiod connections, so the proxy will reconnect over time, hopefully landing on a new istiod Pods.&lt;/p&gt; &lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;networking.istio.io/v1alpha3&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;EnvoyFilter&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;istio-proxy-to-istiod-timeouts&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;istio-system&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;workloadSelector&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;{}&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;configPatches&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;applyTo&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;HTTP_ROUTE&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;SIDECAR_OUTBOUND&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;routeConfiguration&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;vhost&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;istiod.istio-system.svc.cluster.local:443&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;patch&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;operation&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;MERGE&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;typed_config&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;@type&apos;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;common_http_protocol_options&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;idle_timeout&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;300s&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; </description> <pubDate>Sun, 22 Oct 2023 00:00:00 +0000</pubDate> <link>/scaling-istio/</link> <guid isPermaLink="true">/scaling-istio/</guid> <category>kubernetes</category> <category>cloud</category> <category>networking</category> <category>istio</category> <category>microservices</category> </item> <item> <title>Work Around Max Count of Security Group Rules on EKS</title> <description>&lt;p&gt;AWS EKS on VPC networks need AWS Security Group Rules (SG) to receipt ingress traffic. But what if you reach the max rules count in your SG?&lt;/p&gt; &lt;h3 id=&quot;background&quot;&gt;Background&lt;/h3&gt; &lt;h4 id=&quot;loadbalancer-type-service-and-security-group-rules&quot;&gt;LoadBalancer-type Service and Security Group Rules&lt;/h4&gt; &lt;p&gt;Kubernetes users can expose a Service in two ways:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Register with the Istio ingress gateways—the golden path for most tenants&lt;/li&gt; &lt;li&gt;Create a dedicated LoadBalancer-type Service object, which tells the cloud provider to create a load balancer and set up health checks.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;EKS recommends &lt;a href=&quot;https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.5/&quot;&gt;aws-load-balancer-controller&lt;/a&gt; to react to updates to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LoadBalancer&lt;/code&gt;-type Service objects and set up NLB accordingly. For example, suppose a Service object exposes ports 80 and 443, the controller will create five Security Group (SG) Rules on EKS worker Nodes:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;allow ingress source &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.0.0.0/0&lt;/code&gt; to the corresponding &lt;a href=&quot;https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport&quot;&gt;NodePort&lt;/a&gt; for port &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;80&lt;/code&gt;&lt;/li&gt; &lt;li&gt;allow ingress source &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.0.0.0/0&lt;/code&gt; to the corresponding NodePort for port &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;443&lt;/code&gt;&lt;/li&gt; &lt;li&gt;allow EKS zonal subnet in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;us-west-2a&lt;/code&gt; to ingress to the health-check NodePort.&lt;/li&gt; &lt;li&gt;allow EKS zonal subnet in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;us-west-2b&lt;/code&gt; to ingress to the health-check NodePort&lt;/li&gt; &lt;li&gt;allow EKS zonal subnet in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;us-west-2c&lt;/code&gt; to ingress to the health-check NodePort&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;Note: health-check will fail if a) the Node does not host any target Pods or b) none of the target Pods on this Node is ready, determined by the Pod’s readiness probe&lt;/p&gt; &lt;p&gt;The SG Rules are added to an SG attached to all worker Nodes in the given EKS.&lt;/p&gt; &lt;h4 id=&quot;security-group-limits&quot;&gt;Security Group Limits&lt;/h4&gt; &lt;p&gt;For each AWS account, there are two quota limits on Security Groups:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;Max number of inbound rules per SG&lt;/li&gt; &lt;li&gt;Max number of SGs per network interface&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;These limits can be adjusted subject to the constraint that the product of the two quotas cannot exceed 1000 (AWS &lt;a href=&quot;https://docs.aws.amazon.com/vpc/latest/userguide/amazon-vpc-limits.html#vpc-limits-security-groups&quot;&gt;doc&lt;/a&gt;). It means a network interface can not have more than 1000 SG rules.&lt;/p&gt; &lt;h3 id=&quot;problem&quot;&gt;Problem&lt;/h3&gt; &lt;p&gt;Once your EKS cluster approaches the limit of SG rules, it restricts your ability to create new load balancers. It means you won’t be able to perform blue-green upgrade of the load balancer, because you need to provision two sets of load balancers simultaneously. The lack of headroom also means you can no longer onboard more applications that requires a dedicated load balancer.&lt;/p&gt; &lt;h3 id=&quot;solutions&quot;&gt;Solutions&lt;/h3&gt; &lt;p&gt;The following solutions are not mutually exclusive. They can be used together.&lt;/p&gt; &lt;h4 id=&quot;second-dedicated-sg-for-each-node-pools&quot;&gt;Second dedicated SG for each node pools&lt;/h4&gt; &lt;p&gt;Suppose your current setup is that all worker Nodes, regardless node pool, has a shared SG attached named “worker”. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aws-load-balancer-controller&lt;/code&gt; adds new rules to the “worker” SG.&lt;/p&gt; &lt;p&gt;You can keep the shared “worker” SG to store common rules but create a new SG for each node pool, and use the new SG for NLBs ingress. You need to change the node pool launch template to attach the new SG.&lt;/p&gt; &lt;p&gt;If you decide to continue letting the AWS LB controller manage SG rules for us, you should tag the new SG with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubernetes.io/cluster/{{ .ClusterName }}: shared&lt;/code&gt;. This is necessary when there are multiple security groups attached to an ENI, so that the controller knows which SG to add new rules to. Because the existing “worker” SG has this tag already, we need to create a duplicate SG, say “worker2”, which does NOT have the SG tag for NLB. Then, we will attach to the node pool the “worker2” SG and the per-pool SG.&lt;/p&gt; &lt;h4 id=&quot;optimize-sg-rules-outside-of-aws-lb-controller&quot;&gt;Optimize SG rules outside of aws LB controller&lt;/h4&gt; &lt;p&gt;Recall the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aws-load-balancer-controller&lt;/code&gt; implementation creates 5 inbound SG rules per envoy-ingress Service. We can optimize this by managing the SG rules ourselves and asking the controller to skip SG rules creation. We can reduce the need to 2 inbound SG rules per envoy-ingress Service.&lt;/p&gt; &lt;p&gt;Add the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;service.beta.kubernetes.io/aws-load-balancer-manage-backend-security-group-rules: false&lt;/code&gt;` annotation to the LoadBalancer-type Service object. Documentation about this annotation is &lt;a href=&quot;https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.4/guide/service/annotations/#manage-backend-sg-rules&quot;&gt;here&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Reserve 3 static NodePorts for each Service.&lt;/strong&gt; One for NLB to health check the EKS nodes. One for frontend port 80. One for frontend port 443. You &lt;a href=&quot;https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/#preserving-the-client-source-ip&quot;&gt;can choose&lt;/a&gt; a static &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;healthCheckNodePort&lt;/code&gt; if you set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;externalTrafficPolicy: Local&lt;/code&gt; (which comes with the benefits to preserve source IP address). The two regular NodePorts can be static regardless.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;The two regular NodePorts should be consecutive&lt;/strong&gt;, so one SG rule can cover both. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;healthCheckNodePort&lt;/code&gt; does not need to be consecutive, because the source IP range in the SG rule is different (i.e. only allow NLB to healthcheck the nodes).&lt;/p&gt; &lt;p&gt;Consider the following example:&lt;/p&gt; &lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt; apiVersion: v1 kind: Service metadata: annotations: external-dns.alpha.kubernetes.io/hostname: acmecorp.com service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: instance service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing service.beta.kubernetes.io/aws-load-balancer-type: external &lt;span class=&quot;gi&quot;&gt;+ service.beta.kubernetes.io/aws-load-balancer-manage-backend-security-group-rules: false &lt;/span&gt; name: myapp namespace: myapp spec: externalTrafficPolicy: Local &lt;span class=&quot;gi&quot;&gt;+ healthCheckNodePort: 30218 &lt;/span&gt; ports: - name: https &lt;span class=&quot;gi&quot;&gt;+ nodePort: 30212 &lt;/span&gt; port: 443 protocol: TCP targetPort: 8095 - name: http &lt;span class=&quot;gi&quot;&gt;+ nodePort: 30213 &lt;/span&gt; port: 80 protocol: TCP targetPort: 8089 selector: app: myapp type: LoadBalancer &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;The optimized SG rules would be:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;del style=&quot;color: #9c9c9c&quot;&gt;allow ingress source &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.0.0.0/0&lt;/code&gt; to the corresponding NodePort for port &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;80&lt;/code&gt;&lt;/del&gt;&lt;/li&gt; &lt;li&gt;&lt;del style=&quot;color: #9c9c9c&quot;&gt;allow ingress source &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.0.0.0/0&lt;/code&gt; to the corresponding NodePort for port &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;443&lt;/code&gt;&lt;/del&gt;&lt;/li&gt; &lt;li&gt; &lt;p&gt;allow source &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.0.0.0/&lt;/code&gt;0 to ingress to NodePort range from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;30212&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;30213&lt;/code&gt;&lt;/p&gt; &lt;/li&gt; &lt;li&gt;&lt;del style=&quot;color: #9c9c9c&quot;&gt;allow EKS zonal subnet in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;us-west-2a&lt;/code&gt; to ingress to the health-check NodePort&lt;/del&gt;&lt;/li&gt; &lt;li&gt;&lt;del style=&quot;color: #9c9c9c&quot;&gt;allow EKS zonal subnet in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;us-west-2b&lt;/code&gt; to ingress to the health-check NodePort&lt;/del&gt;&lt;/li&gt; &lt;li&gt;&lt;del style=&quot;color: #9c9c9c&quot;&gt;allow EKS zonal subnet in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;us-west-2c&lt;/code&gt; to ingress to the health-check NodePort&lt;/del&gt;&lt;/li&gt; &lt;li&gt;allow EKS VPC network in region &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;us-west-2&lt;/code&gt; to ingress to the health-check NodePort&lt;/li&gt; &lt;/ul&gt; &lt;h4 id=&quot;raise-max-inbound-rules-per-sg-by-reducing-sg-count-per-eni&quot;&gt;Raise max inbound rules per SG by reducing SG count per ENI&lt;/h4&gt; &lt;p&gt;The solution picks a different point on the trade-off spectrum between #Inbound rules per SG and #SG per ENI.&lt;/p&gt; &lt;p&gt;SG quota is set for each and whole AWS account, so any adjustment will affect other workloads in the same account. Thus, we need to verify whether the existing AWS account has ENI with max number of SGs attached already.&lt;/p&gt; &lt;h4 id=&quot;build-eks-clusters-in-a-separate-aws-account&quot;&gt;Build EKS clusters in a separate AWS account&lt;/h4&gt; &lt;p&gt;Building new clusters and shifting tenants over are expensive. Try other solutions first.&lt;/p&gt; </description> <pubDate>Tue, 26 Sep 2023 00:00:00 +0000</pubDate> <link>/eks-sg/</link> <guid isPermaLink="true">/eks-sg/</guid> <category>kubernetes</category> <category>cloud</category> <category>networking</category> </item> <item> <title>Layer-4 Load Balancer &amp; Zero-downtime Autoscaling and Upgrade</title> <description>&lt;p&gt;Your Kubernetes cluster probably has a shared ingress for north-south traffic, coming from a cloud load balancer and lands on your favorite proxies like Envoy, or Istio gateways, or Nginx.&lt;/p&gt; &lt;p&gt;If you&lt;/p&gt; &lt;ul&gt; &lt;li&gt;use a LoadBalancer-type &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Service&lt;/code&gt; to create a Layer-4 Load Balancer fronting your Kubernetes ingress&lt;/li&gt; &lt;li&gt;retain source IP address by setting &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;externalTrafficPolicy: Local&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Then horizontal autoscaling (scale-in) and rolling upgrade will incur some downtime for you.&lt;/p&gt; &lt;p&gt;This post&lt;/p&gt; &lt;ul&gt; &lt;li&gt;explains why there is partial disruption, and how much disruption to expect&lt;/li&gt; &lt;li&gt;discusses several options to achieve zero downtime upgrade and autoscaling&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;For simplicity, the rest of the doc assumes Envoy as the ingress gateway.&lt;/p&gt; &lt;h3 id=&quot;background&quot;&gt;Background&lt;/h3&gt; &lt;h4 id=&quot;layer-4-cloud-load-balancer&quot;&gt;Layer-4 cloud load balancer&lt;/h4&gt; &lt;p&gt;The routing of traffic to Envoy is facilitated by a layer-4 (L4) cloud load balancer, known as Network Load Balancer (NLB) in AWS terminology. The &lt;a href=&quot;https://github.com/kubernetes-sigs/aws-load-balancer-controller&quot;&gt;aws-load-balancer-controller&lt;/a&gt; provisions such load balancer (LB) by watching LoadBalancer-type Service objects in Kubernetes. Each Service object opens dedicated NodePort on all Nodes in selected Envoy node pools. Traffic to Envoy will first be routed to NodePort on the Node hosting Envoy Pod, then DNAT-ed (iptables) to the Pod on the same Node, as shown in the following diagram.&lt;/p&gt; &lt;div style=&quot;text-align: center&quot;&gt; &lt;p&gt;&lt;img src=&quot;/assets/images/source-ip-autoscale/1.png&quot; width=&quot;680&quot; /&gt;&lt;/p&gt; &lt;/div&gt; &lt;p&gt;(image &lt;a href=&quot;https://kubernetes.io/blog/2022/12/30/advancements-in-kubernetes-traffic-engineering/#traffic-loss-from-load-balancers-during-rolling-updates&quot;&gt;source&lt;/a&gt;)&lt;/p&gt; &lt;p&gt;The LB periodically check the HealthCheck NodePort. The HealthCheck NodePort will fail if&lt;/p&gt; &lt;ul&gt; &lt;li&gt;the Node does not host any target Pods, or&lt;/li&gt; &lt;li&gt;none of the target Pods on this Node is ready, determined by the Pod’s readiness probe&lt;/li&gt; &lt;/ul&gt; &lt;h4 id=&quot;externaltrafficpolicy-local&quot;&gt;externalTrafficPolicy: Local&lt;/h4&gt; &lt;p&gt;Suppose the Kubernetes Service object is configured with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;externalTrafficPolicy: Local&lt;/code&gt;. Then, the kube-proxy directs packets exclusively to Envoy Pods residing on the same Node, even if there are other Nodes running Envoy. This setup has two benefits: one less hop (lower latency) and preserving source IP address (for allowlist or rate limiting).&lt;/p&gt; &lt;p&gt;But &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;externalTrafficPolicy: Local&lt;/code&gt; is problematic during rolling upgrades or scale-in. The reason is that traffic arriving at NodePort will be dropped by kube-proxy if node has no ready Envoy Pods. LB will keep forwarding traffic to this Node until LB detects the HealthCheck NodePort is failing. Then, LB will mark the Node as unhealthy.&lt;/p&gt; &lt;p&gt;There is a certain delay between two key events in this setup:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;An Envoy Pod becoming NotReady (for example, if it enters the “Terminating” state during a rolling upgrade).&lt;/li&gt; &lt;li&gt;The subsequent periodic health check carried out by the load balancer.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;During such delay, client traffic to this Node is blackholed.&lt;/p&gt; &lt;div style=&quot;text-align: center&quot;&gt; &lt;p&gt;&lt;img src=&quot;/assets/images/source-ip-autoscale/2.png&quot; width=&quot;680&quot; /&gt;&lt;/p&gt; &lt;/div&gt; &lt;p&gt;(image &lt;a href=&quot;https://kubernetes.io/blog/2022/12/30/advancements-in-kubernetes-traffic-engineering/#traffic-loss-from-load-balancers-during-rolling-updates&quot;&gt;source&lt;/a&gt;)&lt;/p&gt; &lt;h3 id=&quot;partial-downtime-during-upgrade-and-autoscale-in&quot;&gt;Partial downtime during upgrade and autoscale-in&lt;/h3&gt; &lt;h4 id=&quot;why-is-there-some-downtime&quot;&gt;Why is there some downtime&lt;/h4&gt; &lt;p&gt;As discussed in the previous section, client traffic to an Envoy Node is blackholed during the time between the envoy Pod on such Node enters the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Terminating&lt;/code&gt; state and the LB performs the next health check. Kube-proxy will remove forwarding rules from NodePort to the Pod once the Pod enters the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Terminating&lt;/code&gt; state. Kubernetes 1.24 and 1.25 considers the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Terminating&lt;/code&gt; state as not ready.&lt;/p&gt; &lt;p&gt;For the same reason, horizontal scale-in will also cause downtime. For a while, I was just running Envoy as a DaemonSet on a node pool that does not autoscale.&lt;/p&gt; &lt;h4 id=&quot;why-is-the-downtime-partial&quot;&gt;Why is the downtime partial&lt;/h4&gt; &lt;p&gt;This downtime only affects one Node at a time, because currently, Envoy DaemonSet has the following upgrade strategy:&lt;/p&gt; &lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt; &lt;span class=&quot;na&quot;&gt;updateStrategy&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;RollingUpdate&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;rollingUpdate&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;maxUnavailable&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;maxSurge&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Thus, Kubernetes will terminate one Pod at a time, then create new Pod on the same Node. There are 6 Pods in each DaemonSet, so not all envoy Pods are down at the same time.&lt;/p&gt; &lt;p&gt;The reason for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maxSurge: 0&lt;/code&gt; is that envoy-ingress Pods run on host networking. It means we cannot have 2 envoy Pods running on the same Node, because they both bind to the same ports. Thus, the current update strategy is to kill a Pod, then start a new one.&lt;/p&gt; &lt;h4 id=&quot;why-host-networking&quot;&gt;Why host networking&lt;/h4&gt; &lt;p&gt;Running Envoy in host networking means traffic bypasses the Pod overlay network (Normally, each Pod runs in separate network namespaces). Thus, host networking reduces the overhead of network hops and encapsulation due to overlays. This results in lower latency and higher throughput.&lt;/p&gt; &lt;p&gt;But how much performance gain exactly? It depends on many factors like hardware and bandwidth. Cilium did some &lt;a href=&quot;https://cilium.io/blog/2021/05/11/cni-benchmark&quot;&gt;benchmark&lt;/a&gt;—take this marketing with a grain of salt—that suggests host networking could improve throughput by 20% and latency by 25%. They didn’t say how many iptables rules (which scale linearly) are on the given hosts.&lt;/p&gt; &lt;h4 id=&quot;how-much-downtime&quot;&gt;How much downtime&lt;/h4&gt; &lt;p&gt;After NLB detects in its target group an unhealthy instance, NLB will stop creating new connections to that target. However, existing connections are not immediately terminated until a default 300s of draining timeout, or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RST&lt;/code&gt; by clients or Envoy. Thus, in the worst case, the blackhole period per Pod is 310 seconds.&lt;/p&gt; &lt;p&gt;In practice, the startup time of a new Envoy Pod on the same Node will be shorter than 300s. NLB continues health-checking the unhealthy Node, and will mark the Node as healthy as gain once the new Pod is ready. But for the worst-case analysis, let’s assume the blackhole period per Node is 310 seconds.&lt;/p&gt; &lt;p&gt;Given 6 Nodes, Envoy DaemonSet will exhibit a 16.7% error rate for a total of 310 * 6 seconds, which is 1860 seconds, or 31 minutes in the worst case.&lt;/p&gt; &lt;p&gt;The 16.7% error rate comes from the fact that 1 of the 6 Pods are in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Terminating&lt;/code&gt; state. Still, 16.7% is an appropriation, because another downside of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;externalTrafficPolicy: Local&lt;/code&gt; is that connections may not distribute evenly, especially if there are long-running connections on the Terminating Pod. NLB does not support the least-connections load balancing scheme.&lt;/p&gt; &lt;h3 id=&quot;solutions&quot;&gt;Solutions&lt;/h3&gt; &lt;h4 id=&quot;use-pod-ips-as-lb-backends&quot;&gt;Use Pod IPs as LB backends&lt;/h4&gt; &lt;p&gt;In this case, NLB sends traffic directly to the Pods selected by k8s Service. The benefits are:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Eliminate the extra network hop (NodePort) through the worker Nodes&lt;/li&gt; &lt;li&gt;Allow NLB to keep sending traffic to Pods in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Terminating&lt;/code&gt; state but mark the target as Draining&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;AWS load balancer controller supports this feature natively with “NLB IP-mode”. On other cloud, you can implement such controller yourself, watching Pod events and reconcile with L4 LB target groups.&lt;/p&gt; &lt;p&gt;To enable IP-mode, we just need to update the Service annotations&lt;/p&gt; &lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;gd&quot;&gt;-service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: instance &lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip &lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;# Health check the Pods directly &lt;span class=&quot;gi&quot;&gt;+service.beta.kubernetes.io/aws-load-balancer-healthcheck-protocol: http +service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: &quot;9901&quot; +service.beta.kubernetes.io/aws-load-balancer-healthcheck-path: /ready &lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;# NLB with IP targets by default does not pass the client source IP address, # unless we specifically configure the target group attributes. &lt;span class=&quot;gi&quot;&gt;+service.beta.kubernetes.io/aws-load-balancer-target-group-attributes: preserve_client_ip.enabled=true &lt;/span&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;To achieve zero-downtime upgrade, we need to additionally configure on the envoy Pod a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preStop&lt;/code&gt; hook like below. When Pod enters the Terminating state, k8s will execute the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preStop&lt;/code&gt; hook and keep the Pod in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Terminating&lt;/code&gt; until the preS`top hook completes.&lt;/p&gt; &lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;c1&quot;&gt;# We must define a longer terminationGracePeriodSeconds, which by default&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# is 30s, upon which the Pod is killed even if preStop has not completed.&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;terminationGracePeriodSeconds&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;305&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;envoy&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;lifecycle&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;preStop&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;exec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# The default target group attribute&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# “deregistration_delay.timeout_seconds” is 300s, configurable&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# through Service annotation.&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/bin/sh&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;curl -X POST http://localhost:9901/healthcheck/fail &amp;amp;&amp;amp; sleep &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;300&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;By failing the envoy health check but keeping envoy running in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Terminating&lt;/code&gt; state, envoy can still process traffic. Once NLB deems the Envoy Pod unhealthy, it halts new request routing to the Pod but maintains existing connections. Consequently, active TCP connections persist, with client requests continuing to the now-unhealthy NLB target (Envoy Pod) until either client or envoy closes the connection or idle timeout expiry, defaulting to 300 seconds for NLB.&lt;/p&gt; &lt;h4 id=&quot;proxyterminatingendpoints&quot;&gt;ProxyTerminatingEndpoints&lt;/h4&gt; &lt;p&gt;&lt;a href=&quot;https://github.com/kubernetes/enhancements/blob/master/keps/sig-network/1669-proxy-terminating-endpoints/README.md&quot;&gt;ProxyTerminatingEndpoints&lt;/a&gt; is a new beta feature in Kubernetes version 1.26. It is enabled by default.&lt;/p&gt; &lt;p&gt;When there is a rolling update and a Node only contains terminating Pods, kube-proxy will route traffic to the terminating Pods based on their readiness. At the same time, kube-proxy will actively fail the health check NodePort if there are only terminating Pods available. By doing so, kube-proxy alerts the external load balancer that new connections should not be sent to that Node but will gracefully handle requests for existing connections.&lt;/p&gt; &lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;c1&quot;&gt;# We must define a longer terminationGracePeriodSeconds, which by default&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# is 30s, upon which the Pod is killed even if preStop has not completed.&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;terminationGracePeriodSeconds&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;305&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;envoy&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;lifecycle&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;preStop&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;exec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# The default target group attribute&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# “deregistration_delay.timeout_seconds” is 300s, configurable&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# through Service annotation.&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/bin/sh&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;sleep &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;300&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Note that here we must NOT call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST http://localhost:9901/healthcheck/fail&lt;/code&gt; on Envoy, different from what NLB IP-mode needs. The reason is that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Terminating&lt;/code&gt; Pods need to pass the readiness probe to continue receiving traffic, so we cannot fail the envoy health check. Since kube-proxy will actively fail the health check NodePort if there are only terminating Pods available on the Node, NLB will start the draining process.&lt;/p&gt; &lt;h4 id=&quot;customize-nlb-keep-host-networking&quot;&gt;Customize NLB, keep host networking&lt;/h4&gt; &lt;p&gt;Forget about the NodePort and HealthCheck NodePort opened by kube-proxy. We can create the NLB not through k8s Service object, but using infra-as-code tools such as pulumi. This bypasses the kube-proxy. The NLB will look like this&lt;/p&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt; &lt;th style=&quot;text-align: left&quot;&gt;NLB frontend port&lt;/th&gt; &lt;th style=&quot;text-align: left&quot;&gt;Target port (=NodePort =Pod port because of host networking)&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td style=&quot;text-align: left&quot;&gt;443&lt;/td&gt; &lt;td style=&quot;text-align: left&quot;&gt;8443&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td style=&quot;text-align: left&quot;&gt;80&lt;/td&gt; &lt;td style=&quot;text-align: left&quot;&gt;8080&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;The NLB will find all Nodes running envoy Pods using the autoscaling group for the envoy-ingress node pool. Yes, we can autoscale with solution 4.3. This setup is similar to Section 4.1.1 NLB IP-mode, except the NLB is not created by Kubernetes. We need the following Pod spec change.&lt;/p&gt; &lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;c1&quot;&gt;# We must define a longer terminationGracePeriodSeconds, which by default&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# is 30s, upon which the Pod is killed even if preStop has not completed.&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;terminationGracePeriodSeconds&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;305&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;envoy&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;lifecycle&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;preStop&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;exec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# The default target group attribute&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# “deregistration_delay.timeout_seconds” is 300s, configurable&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# through Service annotation.&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/bin/sh&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;curl -X POST http://localhost:9901/healthcheck/fail &amp;amp;&amp;amp; sleep &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;300&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;We also need to expose the “/ready” endpoint from envoy to the host. Then, we need to update the Service annotations like the following.&lt;/p&gt; &lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;# Health check the Pods directly through NodePort 9901 &lt;span class=&quot;gi&quot;&gt;+service.beta.kubernetes.io/aws-load-balancer-healthcheck-protocol: http +service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: &quot;9901&quot; +service.beta.kubernetes.io/aws-load-balancer-healthcheck-path: /ready &lt;/span&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; </description> <pubDate>Sun, 06 Aug 2023 00:00:00 +0000</pubDate> <link>/source-ip-autoscale/</link> <guid isPermaLink="true">/source-ip-autoscale/</guid> <category>kubernetes</category> <category>cloud</category> <category>networking</category> </item> <item> <title>Enterprise Sales</title> <description>&lt;h3 id=&quot;how-to-do-product-led-growth-and-hands-on-outbound-sales-at-the-same-time&quot;&gt;How to do product-led growth and hands-on outbound sales at the same time?&lt;/h3&gt; &lt;blockquote&gt; &lt;p&gt;Every PLG company eventually has to embrace enterprise.&lt;/p&gt; &lt;p&gt;– Annie Pearl, Chef Product Officer at Calendly&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;The upper limit of PLG seems to be $100M to $200M ARR (e.g. DataDog around IPO). Beyond triple-digit million ARR, you quickly saturate the market of users who buy things by pulling out their credit cards. The growth of the PLG channel naturally slows at some point. &lt;!-- As you expend from selling to individuals/teams to selling to cross-functional organizations, you need t --&gt;&lt;/p&gt; &lt;p&gt;Most companies start with self-serve PLG and then layer in enterprise sales. This may not work well. Your entry-level product could cannibalize your enterprise product. &lt;mark&gt;Think hard about differentiation that justifies the premium. &lt;/mark&gt; You cannot just add single-sign-on (SSO) and call it enterprise edition.&lt;/p&gt; &lt;p&gt;Some common enterprise features are:&lt;/p&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt; &lt;th style=&quot;text-align: left&quot;&gt;Feature&lt;/th&gt; &lt;th style=&quot;text-align: left&quot;&gt;Description&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td style=&quot;text-align: left&quot;&gt;Customer supports&lt;/td&gt; &lt;td style=&quot;text-align: left&quot;&gt;Support is what makes or breaks the enterprise product. Your company success is defined solely by customer success. 24/7 or business hours? Email, chat, or phone support? SLA on response time?&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td style=&quot;text-align: left&quot;&gt;Dedicated account manager&lt;/td&gt; &lt;td style=&quot;text-align: left&quot;&gt;Evangelize best practices and champion support.&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td style=&quot;text-align: left&quot;&gt;Fine-grain authorization policy&lt;/td&gt; &lt;td style=&quot;text-align: left&quot;&gt;Roles and permissions.&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td style=&quot;text-align: left&quot;&gt;Audit logs&lt;/td&gt; &lt;td style=&quot;text-align: left&quot;&gt;User and workload identity, actions, timestamp, easy export.&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td style=&quot;text-align: left&quot;&gt;Compliance&lt;/td&gt; &lt;td style=&quot;text-align: left&quot;&gt;For example, FedRAMP, HIPAA, GDPR, SOC2.&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td style=&quot;text-align: left&quot;&gt;High availability&lt;/td&gt; &lt;td style=&quot;text-align: left&quot;&gt;For example, multi-region disaster recovery, cross-region replication, four-nine uptime SLA.&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td style=&quot;text-align: left&quot;&gt;Private network&lt;/td&gt; &lt;td style=&quot;text-align: left&quot;&gt;Private link, VPC peering, single-tenant deployment.&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td style=&quot;text-align: left&quot;&gt;Self-host&lt;/td&gt; &lt;td style=&quot;text-align: left&quot;&gt;User-managed private deployment&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td style=&quot;text-align: left&quot;&gt;Data retention period&lt;/td&gt; &lt;td style=&quot;text-align: left&quot;&gt;For example, DataDog allows free accounts to view metrics from last 24h&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td style=&quot;text-align: left&quot;&gt;Performance&lt;/td&gt; &lt;td style=&quot;text-align: left&quot;&gt;For example, run the same pytorch model 100x faster.&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;Another pro tip: &lt;mark&gt;within minutes of self-serve signups, directly reach out to that new user&lt;/mark&gt;, assuming they pass certain criteria about company size and title. The reason is, your product is top of mind for the user right now, who clearly has been doing research about this space. If they give you the phone number, call them directly.&lt;/p&gt; &lt;h3 id=&quot;when-to-layer-in-enterprise-sales&quot;&gt;When to layer in enterprise sales?&lt;/h3&gt; &lt;p&gt;Similarly, when to make the first sales hire?&lt;/p&gt; &lt;p&gt;You need product-market fit (PMF) and annual recurring revenue (ARR). PMF is shown through retention, exponential and organic customer growth, and net promoter score (NPS, or how upset would you be if this product disappears? How likely would you recommend this product to your friends?). To learn more about PMF, I love this &lt;a href=&quot;https://www.lennysnewsletter.com/p/how-to-know-if-youve-got-productmarket&quot;&gt;post&lt;/a&gt; by Lenny Rachitsky.&lt;/p&gt; &lt;p&gt;Rule of thumb: you need at least $1M ARR to show you can sell outside of your network (friends and family).&lt;/p&gt; &lt;h3 id=&quot;should-your-first-sales-hire-be-head-of-sales-or-junior-sales-rep&quot;&gt;Should your first sales hire be head of sales or junior sales rep?&lt;/h3&gt; &lt;p&gt;You probably don’t yet need a head of sales because&lt;/p&gt; &lt;ul&gt; &lt;li&gt;The target customers and the deal size remain uncertain, so it is hard to know if a candidate is the right person.&lt;/li&gt; &lt;li&gt;Head of sales is expensive and often commands a structure in marketing, finance, legal, etc. Big overhead.&lt;/li&gt; &lt;li&gt;Head of sales probably has not been down in the weeds for a while.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;You should not hire anyone junior because it takes your time to train them, such as how to write emails and pricing proposal.&lt;/p&gt; &lt;p&gt;An ideal candidate would be an account executive (AE) at a hypergrowth company with great track record, or someone recently moved into front-line management who used to be an AE and who desires the opportunity to grow into the head of sales role.&lt;/p&gt; &lt;p&gt;Some revealing questions to ask:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Tell me about a deal that you lost.&lt;/li&gt; &lt;li&gt;Let’s do a mock discovery call together.&lt;/li&gt; &lt;li&gt;Out of the 8 SDRs on your team, where did you come up on the leaderboard? What are the folks front-running you doing differently?&lt;/li&gt; &lt;/ul&gt; &lt;h3 id=&quot;when-do-you-start-outbound&quot;&gt;When do you start outbound?&lt;/h3&gt; &lt;p&gt;According to Maggie Hott, Director of Sales at Webflow, &lt;mark&gt;the answer is always now. The goal of cold outbound is not to close deals but to build brand awareness and educate potential customers&lt;/mark&gt;. You almost never hit the prospects at the right time, but when they are ready to buy, prior outbounds make you part of the evaluation. Whereas marketing targets a market segment, outbounds are personalized to each prospect.&lt;/p&gt; &lt;p&gt;Maggie recommends a 10-80-10 outbound strategy:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;First 10% is personalized and explains how the prospect could relate to your product. &lt;ul&gt; &lt;li&gt;For example, “Hey John, amazing talk at KubeCon last week with such great lessons for multi-cloud adoption. I am curious how your team approaches multi-cloud observability and cost attribution. All the folks I talked to are struggling with this. I think our product can really help.”&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;li&gt;Mid 80% is repeatable product marketing.&lt;/li&gt; &lt;li&gt;Last 10% is to ask for that call or meeting, and end with a playful closing.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Lauren Schwartz, VP of Sales at Fivetran, stressed the importance of having multiple sponsors in the prospect’s organization. People change jobs and have different types of influence.&lt;/p&gt; &lt;h3 id=&quot;should-we-give-discounts-for-testimonials&quot;&gt;Should we give discounts for testimonials?&lt;/h3&gt; &lt;p&gt;In the early days, &lt;mark&gt;worry less about the contract size but focus on getting those logos&lt;/mark&gt;. The bulk of the enterprise market is the early majority and late majority. They are almost never early adopters. They need testimony and success stories.&lt;/p&gt; &lt;p&gt;Maybe you could get more testimonials through discounts, but be judicious about discounts, because people talk, and soon more customers will ask for discounts. &lt;mark&gt;The best reason to give discounts is to control the close date and payment structure of the deal&lt;/mark&gt;. You can always bring up the ask for testimonials if the prospect comes back asking for more discounts.&lt;/p&gt; &lt;!-- Frank: don&apos;t have CS team. / Add notes from Annie Pearl --&gt; </description> <pubDate>Sun, 01 Jan 2023 00:00:00 +0000</pubDate> <link>/sales-lessons/</link> <guid isPermaLink="true">/sales-lessons/</guid> <category>startup</category> </item> <item> <title>More Career Advices</title> <description>&lt;p&gt;&lt;em&gt;Make sure to check out the previous post: &lt;a href=&quot;/advices/&quot;&gt;Advices I wish I got at the start of my career&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt; &lt;h3 id=&quot;ask-for-help&quot;&gt;Ask for help&lt;/h3&gt; &lt;blockquote&gt; &lt;p&gt;Son, your ego is writing checks your body can’t cash.&lt;/p&gt; &lt;p&gt;– Captain Tom “Stinger” Jordan, Top Gun&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;The number one reason why senior people fail is that they do not ask for help. We are all shareholders. You do whatever you can to unblock yourself. It is about time to market and showing results.&lt;/p&gt; &lt;h3 id=&quot;promote-thought-leadership&quot;&gt;Promote thought leadership&lt;/h3&gt; &lt;blockquote&gt; &lt;p&gt;If you want to build a ship, don’t drum up the men to gather wood, divide the work and give orders. Instead, teach them to yearn for the vast and endless sea.&lt;/p&gt; &lt;p&gt;Antoine de Saint-Exupery, author of The Little Prince&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;In tech, most decisions are based on influence, not hierarchy. Thought leadership is a great way to gain influence. To be a leader, you should start by acting like one. Be confident, give tech talks, and voice your opinions in planning, design review, and postmortems. Doing so makes you the obvious choice for the next big project or the next leadership roles.&lt;/p&gt; &lt;p&gt;The outcome and how you drove that outcome are both important. Be careful with your reputation.&lt;/p&gt; &lt;h3 id=&quot;befriend-jeff-bezos-before-he-gets-rich&quot;&gt;Befriend Jeff Bezos before he gets rich&lt;/h3&gt; &lt;p&gt;The best time to become friends with Jeff Bezos is before he becomes rich and famous. Networking does not mean you must reach upward. Invest in your peers, who are more receptive to getting to know you. Imagine you are Stripe in 2012. Rather than running into walls with F500 enterprises, you should onboard hundreds of startups, because among them are the next Airbnb and Lyft. Relationships compound. Start early.&lt;/p&gt; &lt;p&gt;Do informational interviews. Ask people at other firms what they like and don’t like about their job. Always follow up to maintain weak ties, such as&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Saying hello to someone you met at a conference last year. Or asking if they’ll be attending after this year’s agenda is published.&lt;/li&gt; &lt;li&gt;Share interesting news about your old company with a former colleague.&lt;/li&gt; &lt;li&gt;Send them news, event, commentary related to their interests. Examples: &lt;ul&gt; &lt;li&gt;“Tom, I just read this great white paper on blockchain. I know you’d get a lot out of this.”&lt;/li&gt; &lt;li&gt;“Alice, congrats on the new job. Enclosed please find a copy of the best book I’ve read on starting a new job, The First 90 Days. Call me if you want to compare notes.”&lt;/li&gt; &lt;li&gt;“James, I just got an invite to a private class at this new gym but can’t go. You mentioned you love Crossfit — want my ticket?”&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h3 id=&quot;stop-productivity-porn-bias-towards-action&quot;&gt;Stop productivity porn. Bias towards action.&lt;/h3&gt; &lt;p&gt;Watching others lifting weights is not going to make you fit. Many of us spend so much time collecting books we want to read but haven’t, or studying how others slice up their days to get more done. You get more done by doing and by starting now.&lt;/p&gt; &lt;h3 id=&quot;develope-relationship-skills&quot;&gt;Develope relationship skills&lt;/h3&gt; &lt;blockquote&gt; &lt;p&gt;A major reason change efforts so often fail is that successful implementation eventually requires people to have difficult conversations … With everyone taking for granted that their own view is right, and readily assuming that others’ opposition is self-interested, progress quickly grinds to a halt. Decisions are delayed, and when finally made they are often imposed without buy-in from those who have to implement them. Relationships sour. Eventually people give up in frustration, and those driving the effort get distracted by new challenges or the next next big thing. The ability to manage difficult conversations effectively is foundational, then, to achieving almost any significant change.&lt;/p&gt; &lt;p&gt;– Douglas Stone, Author of “Difficult Conversations: How to Discuss What Matters”&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;Relationship problems are at the heart of every organization. Take product managers (PM) and engineering managers (EM) for example. PM &amp;amp; EM have overlapping scopes by definition. When you seek better scoping, you don’t have a scoping problem, you have a relationship problem.&lt;/p&gt; &lt;p&gt;Here are some tips:&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Be a good listener&lt;/strong&gt;. People rarely change. People just want to be understood. Listening is more persuasive than talking. Listening fosters a reciprocal relationship. Listening is not just about paraphrasing back. Ask deep and relevant questions, take notes, and maintain eye contact. For many, listening needs to be a trained response. When you are frustrated, you are the least curious—you have so much noise in your internal head that left little space to worry about what’s on the other person’s head. Learn to lean into the conflicts, just like firefighters learn to run towards the fire.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Lean into conflicts&lt;/strong&gt;. Avoiding conflict is the worst kind of measure of relationship health. Staying quiet creates resentment. You must confront, but do so with skills and preparation. It is mature to share your feeling and inquire about others’. Doing so builds trust. For example: “Bob, I feel frustrated. It seems this conversation is not getting anywhere, and I want to understand why.” Acknowledge the differences between the two parties, not who is right or better. I love this example “Jill, you and I seem to have different preferences about when code reviews should be done. I wonder if that’s something we could talk about?”&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Resolve email conflicts in person&lt;/strong&gt;. If a conflict starts on email, it is hard to solve on email. In this case, just meet in person or pick up the phone. It is hard to communicate emotions through email—no tone, no voice, no facial expression, no body language.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Apology diffuses the tension&lt;/strong&gt;. In most conflicts, blaming does not help. Most of the time, you share part of the blame, even just 5%. Apologizing and acknowledging the fault on your side could really diffuse the tension. Apology is an underrated and underutilized skill. Apology needs to be genuine. Saying “I am sorry that you feel that way” is not genuine. Here is how to make a good apology:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;Acknowledge the harm. “I am sorry that I interrupted you in the meeting.”&lt;/li&gt; &lt;li&gt;Say why it is wrong. “It was disrespectful and discourage the full exchange of ideas.”&lt;/li&gt; &lt;li&gt;Say what you &lt;em&gt;will&lt;/em&gt; do next time, not what you won’t do. “I will make sure to let you finish before I chime in.”&lt;/li&gt; &lt;li&gt;Ask for forgiveness. Bring cupcakes.&lt;/li&gt; &lt;/ol&gt; &lt;h3 id=&quot;go-straight-to-the-job-you-want&quot;&gt;Go straight to the job you want&lt;/h3&gt; &lt;p&gt;Don’t let inertia drive you. Take some risks when you are young. No one in their 40s said they took too much risk.&lt;/p&gt; &lt;p&gt;If you are unhappy about your job, move on. Every job change you make, you always wish you make it 6 months earlier. Life is so short. Do not spend time on jobs that you do not like. You will be so productive in jobs you like. Don’t assume that you have to do this job, then get that job and then that job, and then you can do what you really want. Go direct.&lt;/p&gt; &lt;p&gt;Know your alternative. Negotiate hard on your second best offer, then negotiate with your first choice, knowing what you can walk away towards. Pay attention to details. Be specific about equity grant date, vesting schedule, etc.&lt;/p&gt; &lt;h3 id=&quot;what-to-look-for-in-the-next-job-growth&quot;&gt;What to look for in the next job: Growth&lt;/h3&gt; &lt;p&gt;With the right opportunities, you can 10x your impact every decade. Because of compounding, what seems to be golden handcuffs today is dwarfed by the opportunity to accelerate growth. The exponential curve actually consists of many little S-curves. If you find yourself approaching the flattening end of the S-curve, it is time for a change.&lt;/p&gt; </description> <pubDate>Tue, 06 Dec 2022 00:00:00 +0000</pubDate> <link>/more-advices/</link> <guid isPermaLink="true">/more-advices/</guid> <category>career</category> </item> </channel> </rss>