<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.4">Jekyll</generator><link href="https://peterbhabra.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://peterbhabra.com/" rel="alternate" type="text/html" /><updated>2026-08-04T15:18:19+00:00</updated><id>https://peterbhabra.com/feed.xml</id><title type="html">Peter Bhabra</title><subtitle>Peter Bhabra&apos;s blog.</subtitle><author><name>Peter Bhabra</name></author><entry><title type="html">Gigatoken: making tokenisation care about the hardware</title><link href="https://peterbhabra.com/inside-gigatoken/" rel="alternate" type="text/html" title="Gigatoken: making tokenisation care about the hardware" /><published>2026-07-30T00:00:00+00:00</published><updated>2026-07-30T00:00:00+00:00</updated><id>https://peterbhabra.com/inside-gigatoken</id><content type="html" xml:base="https://peterbhabra.com/inside-gigatoken/"><![CDATA[<p>The AI industry pours engineering effort into GPU kernels and treats CPU preprocessing as an afterthought. Gigatoken shows how much performance that neglect can leave on the table.</p>

<p>In my <a href="/gigatoken-thousand-times-faster/">first Gigatoken post</a>, I tested its 1,000x claim against my tokeniser workload. On that workload, the tokeniser core ran <strong>30x to 40x</strong> faster, including <strong>37.6x</strong> on a million-token request split into 1,024 segments. The result was well below 1,000x. I wanted to understand where the gain came from.</p>

<p>I followed the path Gigatoken optimises most aggressively: tokenisers that apply BPE (byte pair encoding) to UTF-8 bytes, including GPT-2 and the tiktoken family. This is the path behind its headline throughput and the one exercised by my benchmark. The project has no technical paper yet, so what follows is my interpretation of Marcel Rød’s source comments, optimisation diary, profiling reports and commit history.<sup id="fnref:source"><a href="#fn:source" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> Gigatoken describes SentencePiece as less optimised and does not support WordPiece.<sup id="fnref:scope"><a href="#fn:scope" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p>

<h2 id="how-bpe-turns-bytes-into-tokens">How BPE turns bytes into tokens</h2>

<p>The tokenisers in this article turn UTF-8 text into a sequence of integers. Their vocabularies map byte sequences to token IDs. A token might represent a whole word, part of a word, punctuation or a single byte.</p>

<p>Encoding runs as a pipeline. <em>Pretokenisation</em> is the coarse split immediately before BPE: it divides the continuous byte stream into spans using model-specific rules, commonly expressed as a regular expression. Each intermediate span is a <em>pretoken</em>, which BPE consumes to produce the final model tokens. For GPT-2’s r50k tokeniser:</p>

<div class="language-text gigatoken-pretoken-example highlighter-rouge"><div class="highlight"><pre class="highlight"><code>" Gigatoken optimises pretokenisation for CPU microarchitectures."
→ [" Gigatoken", " optimises", " pretokenisation", " for", " CPU", " microarchitectures", "."]
</code></pre></div></div>

<p>BPE turns those seven regex spans into fifteen GPT-2 model tokens. The distinction matters because one pretoken can produce several model tokens.</p>

<figure style="width:100%;max-width:calc(100vw - 3rem);margin:2.5rem 0;overflow-x:auto;">
<svg id="bpe-pipeline" viewBox="0 0 900 450" width="100%" style="height:auto;min-width:700px;display:block;margin:0 auto;font-family:-apple-system,'Segoe UI',system-ui,sans-serif" role="img" aria-labelledby="bpe-pipeline-title bpe-pipeline-desc">
<title id="bpe-pipeline-title">One GPT-2 pretoken becomes four model tokens</title>
<desc id="bpe-pipeline-desc">A 64-byte sentence splits into seven GPT-2 pretokens. The microarchitectures pretoken is then shown passing through BPE and becoming four model tokens.</desc>
<defs><marker id="arrow-pipeline" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" refX="12" refY="6" orient="auto"><path d="M0,0 L12,6 L0,12 L4,6 Z" fill="#b87a18" /></marker></defs>
<rect x="25" y="18" width="850" height="78" rx="8" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<text x="450" y="44" text-anchor="middle" fill="#2c2825" font-size="15" font-weight="650">One 64-byte GPT-2 input block</text>
<text x="450" y="70" text-anchor="middle" fill="#2c2825" font-size="12.5" font-family="ui-monospace,'SFMono-Regular',Consolas,monospace">·Gigatoken·optimises·pretokenisation·for·CPU·microarchitectures.</text>
<text x="450" y="88" text-anchor="middle" fill="#8a7f70" font-size="11">· = space byte</text>
<path d="M450 96 V119" fill="none" stroke="#b87a18" stroke-width="2.5" stroke-dasharray="15 1000" marker-end="url(#arrow-pipeline)" />
<text x="28" y="138" fill="#2c2825" font-size="14" font-weight="650">GPT-2 regex: 7 pretokens</text>
<rect x="28" y="150" width="116" height="48" rx="6" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<rect x="148" y="150" width="116" height="48" rx="6" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<rect x="268" y="150" width="180" height="48" rx="6" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<rect x="452" y="150" width="60" height="48" rx="6" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<rect x="516" y="150" width="60" height="48" rx="6" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<rect x="580" y="150" width="250" height="48" rx="6" fill="#fffaf1" stroke="#2c2825" stroke-width="2.5" />
<rect x="834" y="150" width="38" height="48" rx="6" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<text x="86" y="179" text-anchor="middle" fill="#2c2825" font-size="11.5" font-family="ui-monospace,'SFMono-Regular',Consolas,monospace">·Gigatoken</text>
<text x="206" y="179" text-anchor="middle" fill="#2c2825" font-size="11.5" font-family="ui-monospace,'SFMono-Regular',Consolas,monospace">·optimises</text>
<text x="358" y="179" text-anchor="middle" fill="#2c2825" font-size="11.5" font-family="ui-monospace,'SFMono-Regular',Consolas,monospace">·pretokenisation</text>
<text x="482" y="179" text-anchor="middle" fill="#2c2825" font-size="10.5" font-family="ui-monospace,'SFMono-Regular',Consolas,monospace">·for</text>
<text x="546" y="179" text-anchor="middle" fill="#2c2825" font-size="10.5" font-family="ui-monospace,'SFMono-Regular',Consolas,monospace">·CPU</text>
<text x="705" y="179" text-anchor="middle" fill="#2c2825" font-size="11.5" font-family="ui-monospace,'SFMono-Regular',Consolas,monospace">·microarchitectures</text>
<text x="853" y="179" text-anchor="middle" fill="#2c2825" font-size="12" font-family="ui-monospace,'SFMono-Regular',Consolas,monospace">.</text>
<path d="M705 198 V226 H450 V250" fill="none" stroke="#b87a18" stroke-width="2.5" stroke-dasharray="299 1000" marker-end="url(#arrow-pipeline)" />
<text x="624" y="220" text-anchor="middle" fill="#8a7f70" font-size="11.5">focus one pretoken</text>
<rect x="70" y="255" width="760" height="165" rx="8" fill="#fffaf1" stroke="#2c2825" stroke-width="2" />
<text x="450" y="282" text-anchor="middle" fill="#2c2825" font-size="15" font-weight="650">BPE inside one pretoken</text>
<text x="450" y="307" text-anchor="middle" fill="#2c2825" font-size="13" font-family="ui-monospace,'SFMono-Regular',Consolas,monospace">·microarchitectures</text>
<text x="450" y="332" text-anchor="middle" fill="#8a7f70" font-size="12.5">late merge: [it] + [ect] → [itect]</text>
<rect x="115" y="347" width="150" height="52" rx="6" fill="#f1e7d3" stroke="#b9ab95" stroke-width="1.5" />
<rect x="285" y="347" width="130" height="52" rx="6" fill="#f1e7d3" stroke="#b9ab95" stroke-width="1.5" />
<rect x="435" y="347" width="150" height="52" rx="6" fill="#f1e7d3" stroke="#b9ab95" stroke-width="1.5" />
<rect x="605" y="347" width="150" height="52" rx="6" fill="#f1e7d3" stroke="#b9ab95" stroke-width="1.5" />
<text x="190" y="369" text-anchor="middle" fill="#2c2825" font-size="13" font-family="ui-monospace,'SFMono-Regular',Consolas,monospace">[·micro]</text>
<text x="350" y="369" text-anchor="middle" fill="#2c2825" font-size="13" font-family="ui-monospace,'SFMono-Regular',Consolas,monospace">[arch]</text>
<text x="510" y="369" text-anchor="middle" fill="#2c2825" font-size="13" font-family="ui-monospace,'SFMono-Regular',Consolas,monospace">[itect]</text>
<text x="680" y="369" text-anchor="middle" fill="#2c2825" font-size="13" font-family="ui-monospace,'SFMono-Regular',Consolas,monospace">[ures]</text>
<text x="190" y="390" text-anchor="middle" fill="#8a7f70" font-size="11">ID 4580</text>
<text x="350" y="390" text-anchor="middle" fill="#8a7f70" font-size="11">ID 998</text>
<text x="510" y="390" text-anchor="middle" fill="#8a7f70" font-size="11">ID 5712</text>
<text x="680" y="390" text-anchor="middle" fill="#8a7f70" font-size="11">ID 942</text>
<text x="450" y="443" text-anchor="middle" fill="#2c2825" font-size="13.5" font-weight="650">1 pretoken → 4 model tokens · 7 pretokens → 15 model tokens overall</text>
</svg>
<figcaption style="margin-top:0.75rem;color:#8a7f70;font-size:0.85rem;line-height:1.5;">The 64-byte example splits into seven GPT-2 pretokens, then BPE turns them into fifteen model tokens. The selected pretoken alone produces four: IDs 4580, 998, 5712 and 942.</figcaption>
</figure>

<p>The hard boundaries also explain why the spans are independent. BPE is forbidden to merge across them, so a merge inside one pretoken cannot create or remove a candidate pair in another. The encoder can process each pretoken separately, then concatenate their token IDs in the original order.</p>

<p>Inside one pretoken, BPE starts from bytes or initial symbols. Its merge table was learned when the tokeniser was trained and is fixed during encoding. Each legal adjacent pair has a numeric rank, and the lower rank wins. For the ` microarchitectures<code class="language-plaintext highlighter-rouge"> pretoken, one of the later steps merges </code>[it] [ect]<code class="language-plaintext highlighter-rouge"> into </code>[itect]<code class="language-plaintext highlighter-rouge">; the final GPT-2 pieces are </code>[ micro] [arch] [itect] [ures]`. The encoder looks up the affected neighbouring pairs again after every merge, chooses the next best candidate and repeats until no legal merge remains. The surviving symbols map to token IDs.<sup id="fnref:bpe"><a href="#fn:bpe" class="footnote" rel="footnote" role="doc-noteref">3</a></sup></p>

<h2 id="what-gigatoken-changes-inside-the-rust-core">What Gigatoken changes inside the Rust core</h2>

<p>Traditional Python tokeniser APIs inspect Python input objects and assemble Python-facing results; Gigatoken can read borrowed byte buffers and assemble a flat token buffer in Rust. The service I benchmarked already uses a Rust tokeniser core, so I exclude those API-boundary gains and examine the CPU and memory work inside tokenisation.<sup id="fnref:native-api"><a href="#fn:native-api" class="footnote" rel="footnote" role="doc-noteref">4</a></sup></p>

<p>I group the Rust-core changes into four families: 64-byte boundary classification, cached pretoken encodings, compact pair-rank tables for misses and parallel work split at safe pretoken boundaries. These are the four main hot-path changes for the BPE tokenisers in scope. The available measurements use different machines, cache states and controls, which prevents a clean apportionment of the overall speed-up. Each result needs its own baseline.</p>

<h2 id="1-pretokenise-64-bytes-at-a-time">1. Pretokenise 64 bytes at a time</h2>

<p>A general-purpose regex engine finds one matching span after another. Gigatoken’s <em>mask scanner</em> asks which positions in a <strong>64-byte input block</strong> begin pretokens. The block sets the scanner’s working width. Pretokens can continue across its edges.</p>

<p>The first classification pass handles ASCII, where one byte represents one character and simple byte comparisons can identify letters, digits, spaces, newlines, apostrophes and other categories. SIMD performs these comparisons across several byte positions at once. Each position is called a lane, and each lane holds one byte here. Four 16-byte NEON loads cover the batch on ARM, two 32-byte AVX2 loads cover it on x86, and AVX-512 can load all 64 bytes at once. The comparisons become separate 64-bit class masks, with one bit for each input byte. Bytes at or above <code class="language-plaintext highlighter-rouge">0x80</code> go to a second Unicode pass.<sup id="fnref:scanner"><a href="#fn:scanner" class="footnote" rel="footnote" role="doc-noteref">5</a></sup></p>

<p>Gigatoken’s loader recognises a fixed set of tokeniser patterns and selects dedicated Rust code for each one. For each mask-scanner family, the implementation expresses the regex’s boundary rules as operations over the class masks. The code treats each mask as a row of 64 on/off positions. A shift slides one row left or right so each byte lines up with its neighbour. AND keeps positions where two conditions are true, OR combines alternatives, and NOT selects positions outside a class. SIMD has finished once it creates the masks; ordinary 64-bit integer operations then combine them into boundary bits.</p>

<p>The author’s detailed optimisation notes and isolated scanner measurements use GPT-2’s r50k pretokeniser, so I use the same worked example here.<sup id="fnref:scanner:1"><a href="#fn:scanner" class="footnote" rel="footnote" role="doc-noteref">5</a></sup> Its 64 ASCII bytes fill one complete scanner block. Under r50k’s rules, a leading space can join the letter, number or punctuation run after it. Shifting the class masks supplies the previous-position relationship for every byte at once. The boundary rules mark starts at byte offsets <code class="language-plaintext highlighter-rouge">0</code>, <code class="language-plaintext highlighter-rouge">10</code>, <code class="language-plaintext highlighter-rouge">20</code>, <code class="language-plaintext highlighter-rouge">36</code>, <code class="language-plaintext highlighter-rouge">40</code>, <code class="language-plaintext highlighter-rouge">44</code> and <code class="language-plaintext highlighter-rouge">63</code>, producing <code class="language-plaintext highlighter-rouge">·Gigatoken</code>, <code class="language-plaintext highlighter-rouge">·optimises</code>, <code class="language-plaintext highlighter-rouge">·pretokenisation</code>, <code class="language-plaintext highlighter-rouge">·for</code>, <code class="language-plaintext highlighter-rouge">·CPU</code>, <code class="language-plaintext highlighter-rouge">·microarchitectures</code> and <code class="language-plaintext highlighter-rouge">.</code>; here <code class="language-plaintext highlighter-rouge">·</code> represents a space byte.</p>

<figure style="width:100%;max-width:calc(100vw - 3rem);margin:2.5rem 0;overflow-x:auto;">
<svg id="gigatoken-mask-scanner" viewBox="0 0 900 690" width="100%" style="height:auto;min-width:700px;display:block;margin:0 auto;font-family:-apple-system,'Segoe UI',system-ui,sans-serif" role="img" aria-labelledby="gigatoken-mask-title gigatoken-mask-desc">
<title id="gigatoken-mask-title">The common 64-byte mask-scanner path</title>
<desc id="gigatoken-mask-desc">A 64-byte ASCII sentence passes through SIMD classification and tokeniser-specific boundary rules. Seven start offsets partition the block into proportional pretoken spans.</desc>
<defs><marker id="arrow-mask" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" refX="12" refY="6" orient="auto"><path d="M0,0 L12,6 L0,12 L4,6 Z" fill="#b87a18" /></marker></defs>
<rect x="75" y="18" width="750" height="90" rx="8" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<text x="450" y="45" text-anchor="middle" fill="#2c2825" font-size="16" font-weight="650">One complete 64-byte ASCII block</text>
<text x="450" y="73" text-anchor="middle" fill="#2c2825" font-size="12.5" font-family="ui-monospace,'SFMono-Regular',Consolas,monospace">·Gigatoken·optimises·pretokenisation·for·CPU·microarchitectures.</text>
<text x="450" y="96" text-anchor="middle" fill="#8a7f70" font-size="11.5">byte positions 0 to 63 · · = space byte</text>
<path d="M450 108 V133" fill="none" stroke="#b87a18" stroke-width="2.5" stroke-dasharray="17 1000" marker-end="url(#arrow-mask)" />
<rect x="150" y="138" width="600" height="70" rx="8" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<text x="450" y="166" text-anchor="middle" fill="#2c2825" font-size="16" font-weight="650">SIMD byte comparisons</text>
<text x="450" y="190" text-anchor="middle" fill="#8a7f70" font-size="13">classify many positions as letters, spaces and other byte classes</text>
<path d="M450 208 V233" fill="none" stroke="#b87a18" stroke-width="2.5" stroke-dasharray="17 1000" marker-end="url(#arrow-mask)" />
<rect x="150" y="238" width="600" height="70" rx="8" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<text x="450" y="266" text-anchor="middle" fill="#2c2825" font-size="16" font-weight="650">64-bit class masks</text>
<text x="450" y="290" text-anchor="middle" fill="#8a7f70" font-size="13">one mask per class · bit i describes byte i</text>
<path d="M450 308 V333" fill="none" stroke="#b87a18" stroke-width="2.5" stroke-dasharray="17 1000" marker-end="url(#arrow-mask)" />
<rect x="150" y="338" width="600" height="78" rx="8" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<text x="450" y="367" text-anchor="middle" fill="#2c2825" font-size="16" font-weight="650">Tokeniser-specific boundary rules in Rust</text>
<text x="450" y="392" text-anchor="middle" fill="#8a7f70" font-size="13">shift masks to align neighbours · combine them with AND, OR and NOT</text>
<path d="M450 416 V441" fill="none" stroke="#b87a18" stroke-width="2.5" stroke-dasharray="17 1000" marker-end="url(#arrow-mask)" />
<rect x="175" y="446" width="550" height="78" rx="8" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<text x="450" y="474" text-anchor="middle" fill="#2c2825" font-size="16" font-weight="650">Pretoken-start mask</text>
<text x="450" y="501" text-anchor="middle" fill="#2c2825" font-size="13" font-family="ui-monospace,'SFMono-Regular',Consolas,monospace">start offsets: 0 · 10 · 20 · 36 · 40 · 44 · 63</text>
<path d="M450 524 V549" fill="none" stroke="#b87a18" stroke-width="2.5" stroke-dasharray="17 1000" marker-end="url(#arrow-mask)" />
<text x="50" y="568" fill="#2c2825" font-size="14" font-weight="650">Seven ordered pretokens, scaled by byte length</text>
<rect x="50" y="580" width="125" height="58" rx="5" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<rect x="175" y="580" width="125" height="58" rx="5" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<rect x="300" y="580" width="200" height="58" rx="5" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<rect x="500" y="580" width="50" height="58" rx="5" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<rect x="550" y="580" width="50" height="58" rx="5" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<rect x="600" y="580" width="237.5" height="58" rx="5" fill="#fffaf1" stroke="#2c2825" stroke-width="2" />
<rect x="837.5" y="580" width="12.5" height="58" rx="3" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<text x="112.5" y="614" text-anchor="middle" fill="#2c2825" font-size="11" font-family="ui-monospace,'SFMono-Regular',Consolas,monospace">·Gigatoken</text>
<text x="237.5" y="614" text-anchor="middle" fill="#2c2825" font-size="11" font-family="ui-monospace,'SFMono-Regular',Consolas,monospace">·optimises</text>
<text x="400" y="614" text-anchor="middle" fill="#2c2825" font-size="11" font-family="ui-monospace,'SFMono-Regular',Consolas,monospace">·pretokenisation</text>
<text x="525" y="614" text-anchor="middle" fill="#2c2825" font-size="9" font-family="ui-monospace,'SFMono-Regular',Consolas,monospace">·for</text>
<text x="575" y="614" text-anchor="middle" fill="#2c2825" font-size="9" font-family="ui-monospace,'SFMono-Regular',Consolas,monospace">·CPU</text>
<text x="718.75" y="614" text-anchor="middle" fill="#2c2825" font-size="11" font-family="ui-monospace,'SFMono-Regular',Consolas,monospace">·microarchitectures</text>
<text x="843.75" y="614" text-anchor="middle" fill="#2c2825" font-size="9" font-family="ui-monospace,'SFMono-Regular',Consolas,monospace">.</text>
<text x="450" y="672" text-anchor="middle" fill="#8a7f70" font-size="12">segment lengths in bytes: 10 · 10 · 16 · 4 · 4 · 19 · 1</text>
</svg>
<figcaption style="margin-top:0.75rem;color:#8a7f70;font-size:0.85rem;line-height:1.5;">The sentence fills one 64-byte block. Its seven start offsets partition the input into ordered pretokens; <code>·</code> represents the space byte <code>0x20</code>. Carry and lookahead allow other pretokens to continue across block edges.</figcaption>
</figure>

<p>Bit-parallel regex evaluation and SIMD block classification predate Gigatoken. Its contribution is specialising those techniques for the fixed patterns used by model tokenisers.<sup id="fnref:bitparallel"><a href="#fn:bitparallel" class="footnote" rel="footnote" role="doc-noteref">6</a></sup></p>

<p>The mask scanner must reproduce the regex’s boundaries. A pretoken can cross a 64-byte edge, so the scanner carries information from the preceding character and looks beyond the right edge where a rule needs it. Each batch returns the starts it can prove. Gigatoken calls any uncertain stretch a <em>bad zone</em> and re-derives its boundaries exactly. Invalid UTF-8 can create a bad zone, as can ordinary text whose boundary conditions remain ambiguous at the edge of a block.</p>

<p>Differential tests compare the combined mask and fallback output with the reference across crafted edge cases, 4,000 generated inputs and OpenWebText samples.<sup id="fnref:scanner:2"><a href="#fn:scanner" class="footnote" rel="footnote" role="doc-noteref">5</a></sup></p>

<h3 id="unicode-stays-exact">Unicode stays exact</h3>

<p>About 21% of batches in OpenWebText, a web-text corpus, contain at least one non-ASCII byte. UTF-8 breaks the ASCII pass’s one-byte-per-character assumption: one character can occupy several bytes, and those individual bytes do not reveal whether the character is a letter, number or whitespace. The SIMD pass therefore records their positions in a non-ASCII-byte mask for a second classification pass.</p>

<p>The second pass completes the same 64-position masks. For this r50k path, Gigatoken finds each UTF-8 lead byte, decodes its code point and uses a packed table of about 272 KiB to classify the character as a letter, number, whitespace or other. It stamps that class across every byte of the UTF-8 character, preventing a continuation byte from becoming a false boundary, then adds those results to the masks used by the tokeniser’s boundary rules.</p>

<p>Any region the masks cannot settle falls back to Gigatoken’s <em>scalar walker</em>. The walker uses ordinary integer instructions and advances one span boundary at a time. It also handles CPUs without the required SIMD features, the incomplete tail of a buffer and bad zones such as invalid UTF-8 or ambiguous batch-edge cases. For r50k on the OpenWebText sample, about 0.4% of batches require scalar re-derivation.<sup id="fnref:unicode"><a href="#fn:unicode" class="footnote" rel="footnote" role="doc-noteref">7</a></sup></p>

<figure style="width:100%;max-width:calc(100vw - 3rem);margin:2.5rem 0;overflow-x:auto;">
<svg id="gigatoken-unicode-path" viewBox="0 0 900 745" width="100%" style="height:auto;min-width:700px;display:block;margin:0 auto;font-family:-apple-system,'Segoe UI',system-ui,sans-serif" role="img" aria-labelledby="gigatoken-unicode-title gigatoken-unicode-desc">
<title id="gigatoken-unicode-title">How Unicode rejoins the mask path</title>
<desc id="gigatoken-unicode-desc">SIMD creates ASCII class masks and a mask of non-ASCII byte positions. If that second mask is non-empty, Gigatoken decodes UTF-8 code points, looks up their classes and stamps each class across the character's bytes. Updated masks feed the tokeniser's boundary rules. An ordered walker reads proven start bits and uses exact scalar advance only through uncertain gaps.</desc>
<defs><marker id="arrow-unicode" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" refX="12" refY="6" orient="auto"><path d="M0,0 L12,6 L0,12 L4,6 Z" fill="#b87a18" /></marker></defs>
<rect x="215" y="18" width="470" height="70" rx="10" fill="#f1e7d3" stroke="#b9ab95" stroke-width="2" />
<text x="450" y="46" text-anchor="middle" fill="#2c2825" font-size="16" font-weight="650">SIMD byte classification</text>
<text x="450" y="70" text-anchor="middle" fill="#8a7f70" font-size="13">ASCII class masks + non-ASCII-byte mask</text>
<path d="M450 88 V114" fill="none" stroke="#b87a18" stroke-width="2.5" stroke-dasharray="18 1000" marker-end="url(#arrow-unicode)" />
<polygon points="450,118 575,163 450,208 325,163" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<text x="450" y="157" text-anchor="middle" fill="#2c2825" font-size="14" font-weight="650">Any byte at or</text>
<text x="450" y="178" text-anchor="middle" fill="#2c2825" font-size="14">above 0x80?</text>
<path d="M325 163 H170 V252" fill="none" stroke="#b87a18" stroke-width="2.5" stroke-dasharray="236 1000" marker-end="url(#arrow-unicode)" />
<text x="239" y="150" text-anchor="middle" fill="#8a7f70" font-size="12">no</text>
<rect x="35" y="256" width="270" height="70" rx="10" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<text x="170" y="284" text-anchor="middle" fill="#2c2825" font-size="15" font-weight="650">Use the ASCII class masks</text>
<text x="170" y="308" text-anchor="middle" fill="#8a7f70" font-size="12.5">nothing else to classify</text>
<path d="M575 163 H720 V222" fill="none" stroke="#b87a18" stroke-width="2.5" stroke-dasharray="196 1000" marker-end="url(#arrow-unicode)" />
<text x="646" y="150" text-anchor="middle" fill="#8a7f70" font-size="12">yes</text>
<rect x="550" y="226" width="340" height="142" rx="10" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<text x="720" y="254" text-anchor="middle" fill="#2c2825" font-size="15" font-weight="650">Extend the masks for Unicode</text>
<text x="720" y="281" text-anchor="middle" fill="#8a7f70" font-size="12.5">1. find UTF-8 leads and decode code points</text>
<text x="720" y="307" text-anchor="middle" fill="#8a7f70" font-size="12.5">2. look up each character's packed class</text>
<text x="720" y="333" text-anchor="middle" fill="#8a7f70" font-size="12.5">3. stamp that class across its UTF-8 bytes</text>
<text x="720" y="354" text-anchor="middle" fill="#b87a18" font-size="12">letter · number · whitespace · other</text>
<path d="M170 326 V399 H330" fill="none" stroke="#b87a18" stroke-width="2.5" stroke-dasharray="225 1000" marker-end="url(#arrow-unicode)" />
<path d="M720 368 V399 H570" fill="none" stroke="#b87a18" stroke-width="2.5" stroke-dasharray="173 1000" marker-end="url(#arrow-unicode)" />
<rect x="330" y="374" width="240" height="58" rx="10" fill="#f1e7d3" stroke="#b9ab95" stroke-width="2" />
<text x="450" y="409" text-anchor="middle" fill="#2c2825" font-size="15" font-weight="650">Updated class masks</text>
<path d="M450 432 V459" fill="none" stroke="#b87a18" stroke-width="2.5" stroke-dasharray="19 1000" marker-end="url(#arrow-unicode)" />
<rect x="275" y="463" width="350" height="66" rx="10" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<text x="450" y="490" text-anchor="middle" fill="#2c2825" font-size="15" font-weight="650">Tokeniser-specific boundary rules</text>
<text x="450" y="513" text-anchor="middle" fill="#8a7f70" font-size="12.5">operate over all 64 byte positions</text>
<path d="M450 529 V556" fill="none" stroke="#b87a18" stroke-width="2.5" stroke-dasharray="19 1000" marker-end="url(#arrow-unicode)" />
<rect x="250" y="560" width="400" height="66" rx="10" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<text x="450" y="587" text-anchor="middle" fill="#2c2825" font-size="15" font-weight="650">Batch result</text>
<text x="450" y="610" text-anchor="middle" fill="#8a7f70" font-size="12.5">proven start bits + bad-zone bits</text>
<path d="M450 626 V653" fill="none" stroke="#b87a18" stroke-width="2.5" stroke-dasharray="19 1000" marker-end="url(#arrow-unicode)" />
<rect x="90" y="657" width="720" height="70" rx="10" fill="#f1e7d3" stroke="#b9ab95" stroke-width="2" />
<text x="450" y="684" text-anchor="middle" fill="#2c2825" font-size="15" font-weight="650">One ordered boundary walker</text>
<text x="450" y="708" text-anchor="middle" fill="#8a7f70" font-size="12.5">read proven start bits · scalar-advance exactly through any uncertain gap · emit pretoken spans</text>
</svg>
<figcaption style="margin-top:0.75rem;color:#8a7f70;font-size:0.85rem;line-height:1.5;">Non-ASCII bytes receive a second classification pass that updates the masks. The scalar walker handles the remaining bad zones, then continues the same ordered boundary stream.</figcaption>
</figure>

<p>When boundaries are requested one at a time, the scanner finds the lowest set bit with <code class="language-plaintext highlighter-rouge">trailing_zeros</code> and clears it with <code class="language-plaintext highlighter-rouge">mask &amp;= mask - 1</code>.<sup id="fnref:clear-bit"><a href="#fn:clear-bit" class="footnote" rel="footnote" role="doc-noteref">8</a></sup> The hot encode path converts each mask into a flat buffer of boundary offsets, collects up to 256 spans, then processes them in a counted loop. The counted loop reduces data-dependent control flow in boundary discovery and consumption. Cache probes, BPE misses and token emission still carry their own branches and dependent work.</p>

<p>On GPT-2/r50k over a 1 GB OpenWebText sample, the mask scanner reached <strong>2,460 to 2,600 MB/s</strong> against 983 MB/s for Gigatoken’s scalar reference, an isolated <strong>2.5x to 2.6x</strong> improvement.<sup id="fnref:scanner:3"><a href="#fn:scanner" class="footnote" rel="footnote" role="doc-noteref">5</a></sup> That figure covers boundary detection. Turning each span into token IDs remains downstream work.</p>

<h2 id="2-cache-the-final-ids-for-each-pretoken">2. Cache the final IDs for each pretoken</h2>

<p>After pretokenisation, an encoder normally runs the BPE merge loop for each span. Gigatoken memoises the final token-ID sequence for the exact bytes of each ordinary pretoken. For an unseeded pretoken, the first occurrence computes the tokeniser’s normal answer; later occurrences copy the saved IDs. Each cache entry stores one pretoken’s final IDs; merge histories and whole-request outputs stay outside the cache.</p>

<p>Pretokens up to 15 bytes use a custom short-key table, while longer ones use a separate map. The short table is seeded with exact results for vocabulary byte strings from 1 to 15 bytes. A reused tokeniser instance keeps its results across calls and continues warming.</p>

<p>For a fixed tokeniser, the same pretoken bytes always produce the same IDs, so the cache can replay them without changing the result. The tokeniser’s own rules generate seed values because a vocabulary entry’s ID can differ from the required answer. The 128-bit key contains the complete short byte string and its length, and the table compares that full key after hashing. A hash collision triggers another probe. Full-key comparison prevents it from returning another pretoken’s tokens. Differential tests compare the cached path with uncached encoding across the supported tokeniser families.<sup id="fnref:campaign"><a href="#fn:campaign" class="footnote" rel="footnote" role="doc-noteref">9</a></sup><sup id="fnref:cache"><a href="#fn:cache" class="footnote" rel="footnote" role="doc-noteref">10</a></sup></p>

<p>On the author’s 1 GB GPT-2/OpenWebText run, the table accumulated about 1.3 million unique short pretokens and served <strong>99.4%</strong> of lookups as hits. About 90% of pretoken occurrences emitted one token and 98% emitted no more than two.<sup id="fnref:cache:1"><a href="#fn:cache" class="footnote" rel="footnote" role="doc-noteref">10</a></sup> In that workload, most spans skip the pair-rank lookups, merge decisions and scratch-state updates described in the next section. A hit finds the cached entry and copies its token IDs.</p>

<h3 id="fetch-the-key-and-answer-together">Fetch the key and answer together</h3>

<p>Once caching removes the merge algorithm from the common path, the remaining cost is a largely random table lookup. Processors fetch memory in fixed blocks called cache lines. The x86 machines discussed here use 64-byte lines; the Apple M3 Pro in my benchmark uses 128-byte lines. L1 is the smallest and fastest cache near each core; L2 and the shared last-level cache hold more data at higher latency. A load that misses them may leave the core waiting for main memory.</p>

<p>A short pretoken and its length fit in one 128-bit key. A hash chooses an aligned home pair in the open-addressed table. Where available, Gigatoken calculates the hash with CPU checksum instructions; other targets use a portable arithmetic fallback. Each entry is 32 bytes, so both candidates form one 64-byte probe bucket that fits within a hardware cache line on both architectures. Gigatoken loads both keys and inline values together, compares the complete keys, and selects the match in registers. The common probe requests the bucket’s cache line and avoids a metadata fetch followed by a dependent random load of the value.</p>

<p>Collisions probe later buckets. Up to four token IDs live inside an entry; larger answers spill into a separate append-only token buffer. Offsets and lengths keep cache entries valid across buffer reallocations.</p>

<p>Gigatoken processes up to 256 pretokens as a group. While it discovers their spans, it asks the CPU to prefetch each future target line into L2. During the probe pass it requests promotion into L1 sixteen entries before use. The CPU may ignore a prefetch hint. When it honours one, independent work overlaps the memory fetch and reduces the chance that the probe waits for that line.</p>

<p>The common inline emit path also spends a few extra stores to remove control flow. It reserves room for four token IDs and writes all four lanes from the cache entry. The cursor advances only by the true count, so unused lanes are overwritten by the next result or truncated at the end. That avoids a count-dependent ladder of one-token, two-token, three-token and four-token branches.</p>

<figure style="width:100%;max-width:calc(100vw - 3rem);margin:2.5rem 0;overflow-x:auto;">
<svg id="gigatoken-cache-line" viewBox="0 0 900 420" width="100%" style="height:auto;min-width:740px;display:block;margin:0 auto;font-family:-apple-system,'Segoe UI',system-ui,sans-serif" role="img" aria-labelledby="gigatoken-cache-title gigatoken-cache-desc">
<title id="gigatoken-cache-title">Gigatoken's pretoken cache layout and prefetch ladder</title>
<desc id="gigatoken-cache-desc">Two 32-byte entries form one 64-byte probe bucket. A 256-span pipeline first requests the future target line in L2, then requests it in L1 sixteen probes before it is consumed.</desc>
<defs><marker id="arrow-cache" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" refX="12" refY="6" orient="auto"><path d="M0,0 L12,6 L0,12 L4,6 Z" fill="#b87a18" /></marker></defs>
<text x="25" y="34" fill="#2c2825" font-size="16" font-weight="650">One 64-byte probe bucket</text>
<rect x="25" y="50" width="850" height="112" rx="10" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<rect x="25" y="50" width="425" height="112" rx="10" fill="#f1e7d3" stroke="#b9ab95" stroke-width="2" />
<rect x="450" y="50" width="425" height="112" rx="10" fill="#f1e7d3" stroke="#b9ab95" stroke-width="2" />
<line x1="237" y1="50" x2="237" y2="162" stroke="#c2b4a0" stroke-width="2" />
<line x1="662" y1="50" x2="662" y2="162" stroke="#c2b4a0" stroke-width="2" />
<text x="131" y="82" text-anchor="middle" fill="#2c2825" font-size="14" font-weight="650">Entry 0 key</text>
<text x="131" y="107" text-anchor="middle" fill="#8a7f70" font-size="13">u128 · 16 B</text>
<text x="343" y="82" text-anchor="middle" fill="#2c2825" font-size="14" font-weight="650">inline IDs + spill ref</text>
<text x="343" y="107" text-anchor="middle" fill="#8a7f70" font-size="13">up to 4 token IDs</text>
<text x="343" y="131" text-anchor="middle" fill="#8a7f70" font-size="12">16 B</text>
<text x="556" y="82" text-anchor="middle" fill="#2c2825" font-size="14" font-weight="650">Entry 1 key</text>
<text x="556" y="107" text-anchor="middle" fill="#8a7f70" font-size="13">u128 · 16 B</text>
<text x="768" y="82" text-anchor="middle" fill="#2c2825" font-size="14" font-weight="650">inline IDs + spill ref</text>
<text x="768" y="107" text-anchor="middle" fill="#8a7f70" font-size="13">up to 4 token IDs</text>
<text x="768" y="131" text-anchor="middle" fill="#8a7f70" font-size="12">16 B</text>
<text x="25" y="211" fill="#2c2825" font-size="16" font-weight="650">Memory-latency pipeline over 256 pretokens</text>
<rect x="25" y="235" width="240" height="92" rx="10" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<text x="145" y="266" text-anchor="middle" fill="#2c2825" font-size="14" font-weight="650">Discover span</text>
<text x="145" y="290" text-anchor="middle" fill="#8a7f70" font-size="13">pack key + hash</text>
<text x="145" y="311" text-anchor="middle" fill="#b87a18" font-size="13">request target line in L2</text>
<path d="M265 281 H345" fill="none" stroke="#b87a18" stroke-width="2.5" stroke-dasharray="72 1000" marker-end="url(#arrow-cache)" />
<rect x="350" y="235" width="230" height="92" rx="10" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<text x="465" y="266" text-anchor="middle" fill="#2c2825" font-size="14" font-weight="650">16 probes ahead</text>
<text x="465" y="290" text-anchor="middle" fill="#8a7f70" font-size="13">request line in L1</text>
<text x="465" y="311" text-anchor="middle" fill="#b87a18" font-size="13">prefetch hint</text>
<path d="M580 281 H660" fill="none" stroke="#b87a18" stroke-width="2.5" stroke-dasharray="72 1000" marker-end="url(#arrow-cache)" />
<rect x="665" y="235" width="210" height="92" rx="10" fill="#f1e7d3" stroke="#b9ab95" stroke-width="2" />
<text x="770" y="266" text-anchor="middle" fill="#2c2825" font-size="14" font-weight="650">Probe + emit</text>
<text x="770" y="290" text-anchor="middle" fill="#8a7f70" font-size="13">compare both keys</text>
<text x="770" y="311" text-anchor="middle" fill="#8a7f70" font-size="13">write 1 to 4 IDs</text>
<rect x="214" y="362" width="472" height="42" rx="9" fill="#fffaf1" stroke="#bf9a5a" stroke-width="2" stroke-dasharray="6 5" />
<text x="450" y="388" text-anchor="middle" fill="#8a7f70" font-size="12.5">displaced hit, spill, long pretoken or miss → slow path</text>
</svg>
<figcaption style="margin-top:0.75rem;color:#8a7f70;font-size:0.85rem;line-height:1.5;">The key and the usual answer sit in the same 64-byte probe bucket, which fits within one hardware cache line. Software prefetches try to make that line resident before the probe needs it.</figcaption>
</figure>

<p>The published A/B measures the combined probe-and-emit design: memoisation, staged prefetch, inline four-token values and flat output. On the campaign’s cold 10 GB GPT-2/OpenWebText benchmark, that combination was <strong>27.6% faster</strong> in the single-threaded path that materialised the token IDs and <strong>6.2% faster</strong> on the multithreaded path.<sup id="fnref:campaign:1"><a href="#fn:campaign" class="footnote" rel="footnote" role="doc-noteref">9</a></sup></p>

<h3 id="reduce-address-translation-work-on-linux">Reduce address-translation work on Linux</h3>

<p>The cache works on ordinary memory pages. On Linux, huge pages can reduce the address-translation overhead as a randomly probed table grows. Before a core can load a cache entry, it must translate the program’s virtual address into a physical address. The processor keeps recent translations in a translation lookaside buffer, or TLB. If the translation is absent, a page-table walk must find it first. On Zen, a software prefetch that misses the data TLB may be dropped, weakening the prefetch ladder described above.</p>

<p>A 64 MiB table occupies 16,384 ordinary 4 KiB pages. Full backing by 2 MiB huge pages reduces that to 32 pages. Gigatoken therefore aligns a large short-cache allocation to 2 MiB on Linux and calls <code class="language-plaintext highlighter-rouge">MADV_HUGEPAGE</code> before the memory is first touched. If Linux honours the hint, fewer translations have to cover the same table.</p>

<p>In a separate Zen 5 whole-encode comparison, huge pages reduced warm page walks from about 28.6 million to about 2,300 per pass and improved warm throughput by <strong>7.3%</strong>.<sup id="fnref:pages"><a href="#fn:pages" class="footnote" rel="footnote" role="doc-noteref">11</a></sup> Because the A/B covered the input and output, the result captures address translation across the whole path. The allocation hint is Linux-specific and does nothing on macOS.</p>

<h2 id="3-make-pretoken-cache-misses-cheaper">3. Make pretoken-cache misses cheaper</h2>

<p>A pretoken-cache hit skips BPE. On a miss, the encoder must run the merge loop for that pretoken. Each merge changes up to two neighbouring candidates, so the next choice depends on the previous one. Gigatoken keeps that ordering serial while shortening pair-rank lookups and reusing temporary storage.</p>

<p>Some BPE vocabularies assign merged token IDs in merge-priority order, allowing the ID to rank a candidate. Others store merge rank separately from token ID.<sup id="fnref:rank-order"><a href="#fn:rank-order" class="footnote" rel="footnote" role="doc-noteref">12</a></sup> Gigatoken preserves whichever ordering the tokeniser defines. For an ID-as-rank vocabulary that fits its packed representation, pairs whose IDs are both below 2,048 use a dense 16 MiB grid; other pairs use a packed sparse table. Both replace a general map with a shorter chain of dependent memory loads.<sup id="fnref:miss"><a href="#fn:miss" class="footnote" rel="footnote" role="doc-noteref">13</a></sup></p>

<p>Short and medium spans use fixed local rank and neighbour arrays with a linear scan. Long spans use reusable index arrays as a linked list plus a minimum heap, a priority queue that returns the lowest rank. On the ID-as-rank path, Gigatoken retains those arrays and the heap capacity between calls, avoiding fresh allocations for each cache miss. Both paths preserve merge priorities and choose the leftmost candidate when ranks tie.</p>

<p>On a 1 GB GPT-2 run on Zen 5, widening the dense grid to 16 MiB improved whole-encoder single-threaded throughput by <strong>2.8%</strong> with a cold pretoken cache.<sup id="fnref:miss:1"><a href="#fn:miss" class="footnote" rel="footnote" role="doc-noteref">13</a></sup> Once warm, the cache’s 99.4% hit rate starves this path of work, and the gain disappears.<sup id="fnref:cache:2"><a href="#fn:cache" class="footnote" rel="footnote" role="doc-noteref">10</a></sup></p>

<h2 id="4-parallelise-across-proven-boundaries">4. Parallelise across proven boundaries</h2>

<p>Hugging Face Tokenizers and tiktoken parallelise across caller-supplied inputs, so a million-token document remains one item. Gigatoken finds proven boundaries inside that document and assigns its chunks to several workers.<sup id="fnref:parallel"><a href="#fn:parallel" class="footnote" rel="footnote" role="doc-noteref">14</a></sup></p>

<p>Gigatoken cuts at proven pretoken boundaries, which BPE cannot cross. Added and special tokens stay intact. Inputs with no safe cut stay serial. Tests compare the parallel output with the serial token IDs in their original order.<sup id="fnref:parallel:1"><a href="#fn:parallel" class="footnote" rel="footnote" role="doc-noteref">14</a></sup></p>

<h3 id="keep-coordination-outside-the-token-loop">Keep coordination outside the token loop</h3>

<p>Workers share immutable vocabulary and pair-rank tables. Each worker owns its pretoken cache and scratch buffers, keeping locks and cross-core traffic from shared writes out of the per-pretoken loop.</p>

<p>Workers claim chunks through an atomic counter. Because the chunks are arranged from largest to smallest, the largest are claimed first while smaller tail chunks keep cores busy near the end. Strict ordering prevents a large chunk from starting late and becoming the final straggler.<sup id="fnref:parallel:2"><a href="#fn:parallel" class="footnote" rel="footnote" role="doc-noteref">14</a></sup></p>

<h3 id="copy-results-while-the-tail-is-still-encoding">Copy results while the tail is still encoding</h3>

<p>Gigatoken reserves flat output space and uses a commit cursor to copy the ready prefix while later chunks are still encoding. This overlaps result copying and first-write page allocation with useful work. If the reservation is too small, it gathers the completed chunk buffers after encoding.<sup id="fnref:parallel:3"><a href="#fn:parallel" class="footnote" rel="footnote" role="doc-noteref">14</a></sup></p>

<figure style="width:100%;max-width:calc(100vw - 3rem);margin:2.5rem 0;overflow-x:auto;">
<svg id="gigatoken-parallel" viewBox="0 0 900 500" width="100%" style="height:auto;min-width:740px;display:block;margin:0 auto;font-family:-apple-system,'Segoe UI',system-ui,sans-serif" role="img" aria-labelledby="gigatoken-parallel-title gigatoken-parallel-desc">
<title id="gigatoken-parallel-title">Gigatoken's coarse parallel scheduling and output assembly</title>
<desc id="gigatoken-parallel-desc">A large input is cut at pretoken-safe boundaries into large early chunks and smaller tail chunks. Worker tasks pull chunks through an atomic index, use exclusive mutable state, then copy ready chunks into a flat output buffer in input order.</desc>
<defs><marker id="arrow-parallel" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" refX="12" refY="6" orient="auto"><path d="M0,0 L12,6 L0,12 L4,6 Z" fill="#b87a18" /></marker></defs>
<text x="25" y="31" fill="#2c2825" font-size="15" font-weight="650">One large input, cut only at safe boundaries</text>
<rect x="25" y="48" width="190" height="54" rx="7" fill="#b87a18" />
<rect x="218" y="48" width="190" height="54" rx="7" fill="#b87a18" />
<rect x="411" y="48" width="190" height="54" rx="7" fill="#b87a18" />
<rect x="604" y="48" width="95" height="54" rx="7" fill="#bf9a5a" />
<rect x="702" y="48" width="78" height="54" rx="7" fill="#bf9a5a" />
<rect x="783" y="48" width="78" height="54" rx="7" fill="#bf9a5a" />
<text x="313" y="81" text-anchor="middle" fill="#fffaf1" font-size="13">large head chunks</text>
<text x="733" y="81" text-anchor="middle" fill="#fffaf1" font-size="12">small tail</text>
<path d="M450 102 V142" fill="none" stroke="#b87a18" stroke-width="2.5" stroke-dasharray="32 1000" marker-end="url(#arrow-parallel)" />
<rect x="275" y="147" width="350" height="52" rx="9" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<text x="450" y="178" text-anchor="middle" fill="#2c2825" font-size="14" font-weight="650">large head chunks first, then the small tail</text>
<path d="M450 199 V220" fill="none" stroke="#b87a18" stroke-width="2.5" stroke-dasharray="13 1000" marker-end="url(#arrow-parallel)" />
<rect x="300" y="225" width="300" height="55" rx="9" fill="#f1e7d3" stroke="#b9ab95" stroke-width="2" />
<text x="450" y="248" text-anchor="middle" fill="#2c2825" font-size="14" font-weight="650">shared model + atomic chunk index</text>
<text x="450" y="268" text-anchor="middle" fill="#8a7f70" font-size="12">tasks pull one chunk at a time</text>
<path d="M370 280 L170 321" fill="none" stroke="#b87a18" stroke-width="2.5" stroke-dasharray="196.159 1000" marker-end="url(#arrow-parallel)" />
<path d="M450 280 V321" fill="none" stroke="#b87a18" stroke-width="2.5" stroke-dasharray="33 1000" marker-end="url(#arrow-parallel)" />
<path d="M530 280 L730 321" fill="none" stroke="#b87a18" stroke-width="2.5" stroke-dasharray="196.159 1000" marker-end="url(#arrow-parallel)" />
<rect x="55" y="326" width="230" height="92" rx="10" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<rect x="335" y="326" width="230" height="92" rx="10" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<rect x="615" y="326" width="230" height="92" rx="10" fill="#fffaf1" stroke="#b9ab95" stroke-width="2" />
<text x="170" y="353" text-anchor="middle" fill="#2c2825" font-size="14" font-weight="650">Worker task 0</text>
<text x="170" y="377" text-anchor="middle" fill="#8a7f70" font-size="12">exclusive cache + scratch</text>
<text x="170" y="399" text-anchor="middle" fill="#b87a18" font-size="12">chunk token buffer</text>
<text x="450" y="353" text-anchor="middle" fill="#2c2825" font-size="14" font-weight="650">Worker task 1</text>
<text x="450" y="377" text-anchor="middle" fill="#8a7f70" font-size="12">exclusive cache + scratch</text>
<text x="450" y="399" text-anchor="middle" fill="#b87a18" font-size="12">chunk token buffer</text>
<text x="730" y="353" text-anchor="middle" fill="#2c2825" font-size="14" font-weight="650">Worker task N</text>
<text x="730" y="377" text-anchor="middle" fill="#8a7f70" font-size="12">exclusive cache + scratch</text>
<text x="730" y="399" text-anchor="middle" fill="#b87a18" font-size="12">chunk token buffer</text>
<path d="M170 418 V438 H380 L390 448" fill="none" stroke="#b87a18" stroke-width="2.5" stroke-dasharray="236.142 1000" marker-end="url(#arrow-parallel)" />
<path d="M450 418 V448" fill="none" stroke="#b87a18" stroke-width="2.5" stroke-dasharray="22 1000" marker-end="url(#arrow-parallel)" />
<path d="M730 418 V438 H520 L510 448" fill="none" stroke="#b87a18" stroke-width="2.5" stroke-dasharray="236.142 1000" marker-end="url(#arrow-parallel)" />
<rect x="285" y="451" width="330" height="42" rx="9" fill="#f1e7d3" stroke="#b9ab95" stroke-width="2" />
<text x="450" y="477" text-anchor="middle" fill="#2c2825" font-size="13.5" font-weight="650">commit cursor copies chunks in input order</text>
</svg>
<figcaption style="margin-top:0.75rem;color:#8a7f70;font-size:0.85rem;line-height:1.5;">Each active task holds one mutable state slot exclusively. Shared work distribution and output assembly operate at chunk granularity.</figcaption>
</figure>

<p>In separate A/B comparisons, the 16-thread path was <strong>6.2% faster</strong> with strict handout plus parallel gathering and <strong>4.4% faster</strong> with opportunistic prefix copying, each against its own control.<sup id="fnref:parallel:4"><a href="#fn:parallel" class="footnote" rel="footnote" role="doc-noteref">14</a></sup></p>

<p>The closest published total comes from the campaign’s final same-session comparison on a cold 10 GB GPT-2/OpenWebText encode. Its documented single-thread path reached <strong>1,039 MB/s</strong>, while the 16-thread path reached <strong>8,792 MB/s</strong>. That is about <strong>8.5x the wall throughput</strong>, equivalent to cutting the 10 GB encode from roughly <strong>9.6 to 1.14 seconds</strong>. Each worker runs the scanner, cache and miss paths described above, so parallelism scales the faster core.<sup id="fnref:parallel:5"><a href="#fn:parallel" class="footnote" rel="footnote" role="doc-noteref">14</a></sup></p>

<h3 id="what-parallelism-costs">What parallelism costs</h3>

<p>Exclusive worker state removes shared-cache locking from the per-pretoken hot path. The trade-off is a cache per worker and duplicated warm-up work. In the author’s 16-worker profile, the state slots accumulated about 16 million distinct entries between them, compared with 5.5 million for a single cache. Aggregate multithreaded CPU time was 14.7 seconds against roughly 11 seconds for Gigatoken’s single-thread run, even though wall time fell sharply. The request finished sooner by spending more aggregate CPU work and memory.</p>

<p>Initial short-cache sizing is clamped between roughly 2 and 128 MiB per state slot, depending on the predicted share of the batch. The tables can continue to grow, however. The short cache has no eviction, and the long-key maps and token arenas are append-only. The pool keeps that memory across requests. One user processing several terabytes reports in an open issue that Gigatoken eventually consumed the RAM and swap of a 1 TB server.<sup id="fnref:memory"><a href="#fn:memory" class="footnote" rel="footnote" role="doc-noteref">15</a></sup></p>

<p>The wall-time gain therefore comes with a production requirement: long-running workloads with continually changing input need a way to bound or evict retained cache state.</p>

<h2 id="what-the-rust-core-achieved">What the Rust core achieved</h2>

<p>Gigatoken’s 1,000x headline compares its native whole-buffer API with Hugging Face Tokenizers through its Python-facing batch API. Hugging Face’s encoder also runs multithreaded Rust. The headline includes the advantages of handing Gigatoken one 11.9 GB byte buffer, letting it find its own split points and avoiding compatibility work at the Python boundary.<sup id="fnref:headline"><a href="#fn:headline" class="footnote" rel="footnote" role="doc-noteref">16</a></sup></p>

<p>My service already used a multithreaded Rust tokeniser core. My benchmark therefore compared two Rust cores. As I reported in <a href="/gigatoken-thousand-times-faster/">my first Gigatoken post</a>, the timed million-token, 1,024-segment count path fell from about 159 milliseconds to 4.24 milliseconds, a <strong>37.6x speed-up</strong>.</p>

<p>Together, the four mechanisms show how Gigatoken changes the way tokenisation runs on the hardware: classify 64 bytes at once, lay common cache probes out in 64-byte buckets, shorten the BPE miss path and parallelise at safe boundaries. GPU kernels receive this scrutiny as a matter of course. CPU preprocessing should too.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:source">
      <p>This article follows the <a href="https://github.com/marcelroed/gigatoken/tree/542367a3efed134883fb4f1140b49c04e6fad3a3">Gigatoken source revision used in my benchmark</a>. Marcel Rød’s <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/profiling/campaign_report.md">profiling campaign</a> records the measurements and rejected experiments. <a href="#fnref:source" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:scope">
      <p>Gigatoken’s <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/README.md#L261-L265">README</a> distinguishes its heavily optimised BPE tokeniser path from SentencePiece and lists WordPiece as unsupported. <a href="#fnref:scope" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:bpe">
      <p>Byte pair encoding was introduced as a compression technique and adapted for subword tokenisation in <a href="https://arxiv.org/abs/1508.07909">Neural Machine Translation of Rare Words with Subword Units</a>. OpenAI’s educational <a href="https://github.com/openai/tiktoken/blob/main/tiktoken/_educational.py#L23-L37">tiktoken implementation</a> shows the repeated lowest-rank adjacent-pair merges during encoding. <a href="#fnref:bpe" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:native-api">
      <p>The cleanest controlled measurement I found covers the input boundary. Gigatoken replaced a corpus pre-split into per-document Python objects with borrowed byte buffers and separator splitting inside Rust. The author measured it <a href="https://github.com/marcelroed/gigatoken/commit/97678e8d1dd2426035909235e03bbafeeefe6cc1"><strong>11% to 16% faster</strong> on 300 MB and the full 11.9 GB OpenWebText corpus, with identical token IDs</a>. Output materialisation and a full native-versus-compatibility multiplier remain unmeasured. <a href="#fnref:native-api" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:scanner">
      <p>The source documents the shared <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/pretokenize/fast/mask.rs#L1-L33">mask-scanner architecture</a>, <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/pretokenize/fast/mask.rs#L174-L328">architecture-specific SIMD front ends</a> and <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/pretokenize/fast/r50k.rs#L164-L240">GPT-2 boundary algebra</a>. The <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/pretokenize/fast/r50k.rs#L1-L35">r50k module notes</a> define the scalar path and report the isolated throughput. Separate mask implementations cover <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/pretokenize/fast/qwen2.rs#L1-L18">Qwen 2/3</a> and <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/pretokenize/fast/o200k.rs#L1-L35">GPT-4o/o200k</a>, while <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/pretokenize/fast/deepseek_v3.rs#L1-L22">DeepSeek uses a specialised scalar walker</a>. The r50k module also contains <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/pretokenize/fast/r50k.rs#L759-L894">edge-case, fuzz and OpenWebText differential tests</a>. <a href="#fnref:scanner" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:scanner:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a> <a href="#fnref:scanner:2" class="reversefootnote" role="doc-backlink">&#8617;<sup>3</sup></a> <a href="#fnref:scanner:3" class="reversefootnote" role="doc-backlink">&#8617;<sup>4</sup></a></p>
    </li>
    <li id="fn:bitparallel">
      <p>Cameron et al.’s 2014 paper, <a href="https://www2.cs.sfu.ca/~ashriram/papers/2014_PACT_GREP.pdf"><em>Bitwise Data Parallelism in Regular Expression Matching</em></a>, presents a general regex algorithm built from bitwise logic, shifts and one-bit-per-input-position streams. Langdale and Lemire’s <a href="https://arxiv.org/abs/1902.08318">simdjson paper</a> describes a two-stage parser built around SIMD classification of input blocks; Gigatoken’s source calls its own movemask primitive <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/pretokenize/fast/mask.rs#L85-L88">simdjson-style</a>. Gigatoken applies these established techniques to fixed tokeniser patterns. <a href="#fnref:bitparallel" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:unicode">
      <p>Gigatoken’s <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/pretokenize/unicode.rs#L50-L240">packed Unicode tables</a> and <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/pretokenize/fast/mask.rs#L402-L548">Unicode mask fill</a> preserve the fast mask representation. The <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/pretokenize/fast/r50k.rs#L243-L411">r50k fallback</a> handles ambiguous regions. <a href="#fnref:unicode" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:clear-bit">
      <p>Here <code class="language-plaintext highlighter-rouge">&amp;=</code> means “replace the value on the left with the result of a bitwise AND”. For a non-zero mask, subtracting one changes its lowest <code class="language-plaintext highlighter-rouge">1</code> bit to <code class="language-plaintext highlighter-rouge">0</code> and all the lower <code class="language-plaintext highlighter-rouge">0</code> bits to <code class="language-plaintext highlighter-rouge">1</code>. ANDing that with the original clears only the lowest set bit: <code class="language-plaintext highlighter-rouge">1011000 &amp; 1010111 = 1010000</code>. <code class="language-plaintext highlighter-rouge">trailing_zeros</code> finds that bit’s position before it is cleared, so the next iteration can move to the following boundary. <a href="#fnref:clear-bit" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:campaign">
      <p>See the <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/profiling/campaign_report.md#L1-L47">campaign summary and method</a>, its <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/profiling/campaign_report.md#L110-L184">round-by-round results</a> and the <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/profiling/campaign_report.md#L225-L269">whole-encoder speculation profile</a>. <a href="#fnref:campaign" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:campaign:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p>
    </li>
    <li id="fn:cache">
      <p>The cache module records the <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/bpe/pretoken_cache.rs#L1-L31">1 GB OpenWebText distribution and layout rationale</a>. The implementation documents <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/bpe/tiktoken.rs#L343-L417">exact cache seeding</a>, <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/pretokenize/mod.rs#L81-L213">key packing and hardware CRC hashing</a>, <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/bpe/pretoken_cache.rs#L381-L464">paired cache probes</a> and the <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/bpe/tiktoken.rs#L1040-L1162">staged prefetch loop</a>. <a href="#fnref:cache" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:cache:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a> <a href="#fnref:cache:2" class="reversefootnote" role="doc-backlink">&#8617;<sup>3</sup></a></p>
    </li>
    <li id="fn:pages">
      <p>The <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/profiling/zen5_st_profile.md#L121-L202">Zen 5 profile</a> diagnoses the translation cost and measures the fix. The Linux kernel documentation explains <a href="https://www.kernel.org/doc/html/latest/admin-guide/mm/transhuge.html">transparent huge pages</a>. <a href="#fnref:pages" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:rank-order">
      <p>In an ID-as-rank vocabulary, merged token IDs follow merge priority. Other BPE vocabularies can assign the two independently. If <code class="language-plaintext highlighter-rouge">b + c</code> is rank 0 and produces ID 350 while <code class="language-plaintext highlighter-rouge">a + b</code> is rank 1 and produces ID 300, <code class="language-plaintext highlighter-rouge">b + c</code> must still merge first. Gigatoken’s loader <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/load_tokenizer/hf.rs#L744-L769">checks whether merged IDs follow rank order</a>, and the source includes a <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/load_tokenizer/hf.rs#L931-L960">reversed-ID test</a>. <a href="#fnref:rank-order" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:miss">
      <p>The source explains the <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/bpe/mod.rs#L108-L250">dense and sparse PairRankTable layout</a> and the <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/bpe/tiktoken.rs#L20-L44">explicit-rank alternative</a>. The Zen 5 profiling notes record the <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/profiling/x86_port_plan.md#L385-L416">16 MiB dense-grid A/B</a>. <a href="#fnref:miss" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:miss:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p>
    </li>
    <li id="fn:parallel">
      <p>Hugging Face’s <a href="https://github.com/huggingface/tokenizers/blob/v0.21.4/tokenizers/src/tokenizer/mod.rs#L1307-L1318"><code class="language-plaintext highlighter-rouge">encode_batch_fast</code></a> parallelises over the input vector, one encode call per item. Gigatoken’s <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/README.md#L218-L224">benchmark notes</a> say that Hugging Face and tiktoken receive pre-split documents while Gigatoken receives the whole file and discovers its own split boundaries. The implementation documents <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/pretokenize/mod.rs#L570-L659">safe split points</a>, <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/pretokenize/mod.rs#L716-L832">equivalence and added-token tests</a>, <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/batch.rs#L164-L189">tail-aware chunk sizing</a>, <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/batch.rs#L521-L613">strict work handout</a> and <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/batch.rs#L335-L517">opportunistic output commits</a>. The parallel path is compared with serial output in <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/batch.rs#L1061-L1124">unit tests</a> and an <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/batch.rs#L1399-L1495">ignored 1 GB OpenWebText test with added tokens</a>. The profiling campaign measures <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/profiling/campaign_report.md#L146-L163">6.2% for strict handout plus parallel gather</a> and <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/profiling/campaign_report.md#L190-L213">4.4% for opportunistic prefix copying</a>. Its <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/profiling/campaign_report.md#L17-L34">final same-session comparison</a> reports 8,792 MB/s for the 16-thread ragged path and 1,039 MB/s for the single-thread materialising path on the same cold 10 GB campaign. The two benchmark entry points produce slightly different token counts in their <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/profiling/campaign_report.md#L61-L67">100 MB identity checks</a>, so the 8.5x figure cannot isolate parallel scaling. Gigatoken implements a <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/src/batch.rs#L783-L807">token-identical serial ragged path</a>; no timing is published for it. <a href="#fnref:parallel" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:parallel:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a> <a href="#fnref:parallel:2" class="reversefootnote" role="doc-backlink">&#8617;<sup>3</sup></a> <a href="#fnref:parallel:3" class="reversefootnote" role="doc-backlink">&#8617;<sup>4</sup></a> <a href="#fnref:parallel:4" class="reversefootnote" role="doc-backlink">&#8617;<sup>5</sup></a> <a href="#fnref:parallel:5" class="reversefootnote" role="doc-backlink">&#8617;<sup>6</sup></a></p>
    </li>
    <li id="fn:memory">
      <p>The <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/profiling/mt4_analysis/mt_round4_findings.md#L55-L69">multithreaded profile</a> quantifies duplicated cache work. The current cache has no eviction; <a href="https://github.com/marcelroed/gigatoken/issues/36">issue #36</a> reports unbounded growth in a long-running terabyte-scale job. <a href="#fnref:memory" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:headline">
      <p>Gigatoken’s <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/README.md#L1-L58">README</a> describes the headline comparison, notes that Hugging Face Tokenizers already runs multithreaded Rust and says Gigatoken’s compatibility mode falls short of 1,000x. The <a href="https://github.com/marcelroed/gigatoken/blob/542367a3efed134883fb4f1140b49c04e6fad3a3/benchmarks/compare/measure.py#L19-L27">benchmark method</a> gives Hugging Face pre-split Python strings and Gigatoken one unsplit byte buffer. <a href="#fnref:headline" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Peter Bhabra</name></author><category term="ai" /><category term="performance" /><category term="tokenisation" /><category term="systems" /><summary type="html"><![CDATA[A first-principles tour of the SIMD scanners, cache-aware layouts, huge pages and worker scheduling behind Gigatoken.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://peterbhabra.com/assets/images/gigatoken-engine-teardown-cover-clean.webp" /><media:content medium="image" url="https://peterbhabra.com/assets/images/gigatoken-engine-teardown-cover-clean.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Gigatoken: A Thousand Times Faster?</title><link href="https://peterbhabra.com/gigatoken-thousand-times-faster/" rel="alternate" type="text/html" title="Gigatoken: A Thousand Times Faster?" /><published>2026-07-24T00:00:00+00:00</published><updated>2026-07-24T00:00:00+00:00</updated><id>https://peterbhabra.com/gigatoken-thousand-times-faster</id><content type="html" xml:base="https://peterbhabra.com/gigatoken-thousand-times-faster/"><![CDATA[<p><a href="https://github.com/marcelroed/gigatoken">Gigatoken</a> claims to be roughly <strong>1,000 times faster</strong> than Hugging Face Tokenizers. The implementation is impressive, but 1,000x does not describe the speedup I measured.</p>

<p>Gigatoken’s figure measures its fastest path over an 11.9 GB OpenWebText file. My use case is an online tokenizer: mixed text, requests ranging up to a one-million-token context window, sometimes split into 1,024 segments, with an HTTP boundary around the work.</p>

<p>I measured between <strong>30x and 40x</strong> in the tokenizer core. That is a large improvement, but the gap from 1,000x is too wide to dismiss as benchmark noise.</p>

<h2 id="where-the-thousand-comes-from">Where the thousand comes from</h2>

<p>A tokenizer splits text into pieces, repeatedly merges byte pairs and emits token IDs. The normal implementation is already fast Rust, but parts of its hot path still behave like general software. A regex engine performs pretokenization. Bounds checks and unpredictable branches sit inside tight loops. Threads exchange work. Language bindings move data between Python and Rust.</p>

<p>Its pretokenizers replace general regex execution with specialised state machines. On the current fast path, architecture-specific SIMD classifies 64-byte blocks into bit masks, then bitwise operations derive most boundaries in parallel. Ambiguous Unicode regions take an exact scalar path. Common byte-pair results are cached so repeated words can skip most of the merge work. Predictable branches, local memory access and independent workers reduce CPU stalls and coordination overhead.<sup id="fnref:implementation"><a href="#fn:implementation" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<p>The benchmark script loads the corpus into memory before timing. Gigatoken receives it as one large byte document and finds safe boundaries where it can divide the work between cores. This avoids pre-splitting the input through Python and gives the scheduler an enormous, regular slab of work. It is an in-memory encoding benchmark, rather than a file-I/O benchmark. On a 144-core AMD EPYC machine, Gigatoken reports 24.53 GB/s for GPT-2 against 24.8 MB/s for Hugging Face, or 989x. On an Apple M4 Max it reports 8.79 GB/s against 6.9 MB/s, or 1,268x.<sup id="fnref:benchmark"><a href="#fn:benchmark" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p>

<p>Both results use the workload best suited to Gigatoken’s native API. Gigatoken’s own documentation says its compatibility mode is slower and does not reach 1,000x. The headline is therefore a best case, rather than a general expectation for replacing an existing tokenizer.</p>

<h2 id="what-happened-in-my-workload">What happened in my workload</h2>

<p>I compared Gigatoken with the Hugging Face <a href="https://github.com/huggingface/tokenizers"><code class="language-plaintext highlighter-rouge">tokenizers</code></a> Rust library using identical prepared bytes. Before timing anything, I required the two implementations to produce the same token IDs. The parity checks passed across representative BPE, byte-fallback and tiktoken-style model families.</p>

<p>The preliminary direct results below use a GLM tokenizer on an Apple M3 Pro. Each workload contains mixed prose, code and structured text.</p>

<table>
  <thead>
    <tr>
      <th>Workload</th>
      <th style="text-align: right">Tokens per batch</th>
      <th style="text-align: right">Segments</th>
      <th style="text-align: right">Hugging Face</th>
      <th style="text-align: right">Gigatoken</th>
      <th style="text-align: right">Speedup</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Small suffix</td>
      <td style="text-align: right">2,017</td>
      <td style="text-align: right">8</td>
      <td style="text-align: right">4.2M tok/s</td>
      <td style="text-align: right">168.3M tok/s</td>
      <td style="text-align: right"><strong>40.1x</strong></td>
    </tr>
    <tr>
      <td>Chat prefix</td>
      <td style="text-align: right">32,189</td>
      <td style="text-align: right">64</td>
      <td style="text-align: right">6.3M tok/s</td>
      <td style="text-align: right">199.4M tok/s</td>
      <td style="text-align: right"><strong>31.8x</strong></td>
    </tr>
    <tr>
      <td>Long prefix</td>
      <td style="text-align: right">257,218</td>
      <td style="text-align: right">384</td>
      <td style="text-align: right">6.7M tok/s</td>
      <td style="text-align: right">201.4M tok/s</td>
      <td style="text-align: right"><strong>30.0x</strong></td>
    </tr>
    <tr>
      <td>Million-token segmented</td>
      <td style="text-align: right">1,009,322</td>
      <td style="text-align: right">1,024</td>
      <td style="text-align: right">6.3M tok/s</td>
      <td style="text-align: right">238.2M tok/s</td>
      <td style="text-align: right"><strong>37.6x</strong></td>
    </tr>
  </tbody>
</table>

<p>Gigatoken sustained roughly 168 to 238 million tokens per second as the batch grew towards the context limit. The baseline stayed near 4 to 7 million.</p>

<h2 id="the-missing-970x">The missing 970x</h2>

<p>My service cannot hand Gigatoken an 11.9 GB file and disappear. It receives bounded requests, preserves segment boundaries, returns nested results and pays for HTTP, serialisation and scheduling. The baseline is also called through its native Rust interface, so Gigatoken cannot win by removing Python overhead that was never present.</p>

<p>Small requests expose fixed costs. Segmentation limits how freely work can be divided. At the HTTP boundary, faster tokenization leaves a larger fraction of time in everything surrounding it. This is <a href="https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/index.html#strong-scaling-and-amdahl-s-law">Amdahl’s law</a>: speeding up one part of a system has diminishing effect once the unchanged work dominates the total time.</p>

<p>The 1,000x headline did not survive this workload. The million-token segmented case was <strong>37.6x faster</strong>, cutting core processing time from about 159ms to 4ms while preserving every token ID. The claim did not transfer to production, but the speedup did.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:implementation">
      <p>Gigatoken’s source documents the current <a href="https://github.com/marcelroed/gigatoken/blob/main/src/pretokenize/fast/mask.rs">64-byte mask scanner</a> and <a href="https://github.com/marcelroed/gigatoken/blob/main/src/pretokenize/fast/r50k.rs">GPT-2 boundary algebra</a>. Marcel Rød’s <a href="https://github.com/marcelroed/gigatoken/blob/main/pretokenizer_optimization_log.md">optimisation diary</a> records the earlier lookup-table, SWAR and dual-cursor stages that led to it. <a href="#fnref:implementation" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:benchmark">
      <p>The <a href="https://github.com/marcelroed/gigatoken#benchmarks">Gigatoken benchmark table</a> reports results by tokenizer and CPU. Its <a href="https://github.com/marcelroed/gigatoken/blob/main/benchmarks/compare/measure.py">comparison script</a> reads the corpus before timing. The Hugging Face comparison uses a pre-split 100 MB subset, while Gigatoken processes the complete 11.9 GB buffer and discovers boundaries itself. <a href="#fnref:benchmark" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Peter Bhabra</name></author><category term="ai" /><category term="performance" /><category term="tokenization" /><summary type="html"><![CDATA[Gigatoken claims a 1,000x tokenizer speedup. My production-shaped tests delivered 30x to 40x gains in the tokenizer core.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://peterbhabra.com/assets/images/gigatoken-cover.webp" /><media:content medium="image" url="https://peterbhabra.com/assets/images/gigatoken-cover.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The swarm that designs itself</title><link href="https://peterbhabra.com/the-swarm-that-designs-itself/" rel="alternate" type="text/html" title="The swarm that designs itself" /><published>2026-06-16T00:00:00+00:00</published><updated>2026-06-16T00:00:00+00:00</updated><id>https://peterbhabra.com/the-swarm-that-designs-itself</id><content type="html" xml:base="https://peterbhabra.com/the-swarm-that-designs-itself/"><![CDATA[<p>Faced with a hard task, the instinct is to reach for more: a smarter model, a longer context window, one capable agent that can hold the whole problem in its head at once. The entire frontier is racing along that axis, chasing more intelligence and more context, and it has handed me a great tool. For deep, sequential problems, a single long-context agent is a superb one: the best hammer I’ve ever had.</p>

<p>So I reach for it on everything. When it can’t crack a task, I rarely stop to ask whether a hammer was the right tool. I just wait for a bigger one, the next model with a longer window and a higher benchmark. But hand someone a hammer and everything starts to look like a nail. Some tasks were never nails.</p>

<p>For one shape of work, there is another way. A lot of real work isn’t deep and sequential. It’s <em>wide and shardable</em>: audit every file in this repo, review every dependency, document every subsystem, check every source. Point a single long-context agent at that and it’ll get there, but you will pay dearly for the privilege.</p>

<p>That question is why I built the <a href="https://github.com/doublewordai/swarm">Doubleword Agent Swarm</a>, my open-source reimplementation of the agent swarm Moonshot introduced in the <a href="https://arxiv.org/abs/2602.02276">Kimi K2.5 report</a>: an LLM orchestrator <strong>designs its own team</strong> of bounded-context workers and fans them out in parallel over a task. This post is the story of how I built it, and what happened when I pointed it at a real codebase, side by side with a single long-context agent.</p>

<p>For wide, shardable work, a swarm of bounded agents beats one long-context agent on cost and on output. The model designs the team. I build the scaffolding, and it’s small.</p>

<h2 id="what-the-hammer-costs">What the hammer costs</h2>

<p>To make it concrete, I picked a job I needed done: a security audit of <a href="https://github.com/doublewordai/control-layer">control-layer</a>, Doubleword’s open-source AI gateway, 512 source files and about 2.4M tokens of unique source. Find real vulnerabilities (injection, leaked secrets, broken auth, unsafe file handling). I ran it both ways, one long-context agent and the swarm.</p>

<p>I ran the single agent first: Claude Opus, a 1M-token window, no chunking, just “audit the repo”. It works. The trouble is what it costs. An agent loop re-sends the growing transcript with every turn, so by the time my metered run had covered ~7% of the repo, it had already burned 27.7M tokens, 95% of them cache reads, the same transcript shipped back again and again.<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> Projected over the full repo, the audit lands around <strong>300M tokens</strong>: a 2.4M-token codebase, amplified ×125.</p>

<figure style="margin:2rem 0;">
<svg viewBox="0 0 760 130" width="100%" style="height:auto;max-width:640px;display:block;margin:0 auto;font-family:-apple-system,'Segoe UI',system-ui,sans-serif" role="img" aria-label="Tokens to audit control-layer: read once 2.4M versus solo agent projected 300M">
<text x="0" y="40" font-size="13" fill="#2c2825">Read once · the corpus</text>
<rect x="200" y="24" width="4" height="24" rx="2" fill="#b3a896"></rect>
<text x="212" y="40" font-size="13" fill="#8a7f70">2.4M</text>
<text x="0" y="90" font-size="13" fill="#2c2825">Solo agent · projected</text>
<rect x="200" y="74" width="460" height="24" rx="2" fill="#b87a18"></rect>
<text x="672" y="90" font-size="13" font-weight="600" fill="#2c2825">~300M</text>
</svg>
<figcaption style="text-align:center;color:#8a7f70;font-size:0.85rem;font-style:italic;margin-top:0.6rem;font-family:-apple-system,system-ui,sans-serif">Reading the 2.4M-token corpus once, against the ~300M a single long-context agent re-sends: a ×125 amplification.</figcaption>
</figure>

<p>Reading the codebase once costs 2.4M tokens. The remaining ~297M is the agent re-reading what it has already seen.</p>

<h2 id="the-alternative-a-swarm">The alternative: a swarm</h2>

<p>Don’t make one agent re-read everything. Split the repo across many bounded workers, each reading only its own slice, once, all at the same time. That’s an agent swarm.</p>

<p>It’s how every company already works. The CEO is the most capable (and most expensive) person in the building, and the wrong one to personally trawl through every file. So they don’t. They hire specialists with tight remits, hand each a bounded task, and never see the mountain of material those specialists wade through. What comes back is a short, high-level summary. The raw work stays with the specialist. The same setup works here, just with agents: the orchestrator plays CEO, the workers are its specialists, and only their conclusions ever travel back up.</p>

<p>Who designs the team?</p>

<p>In February 2026, Moonshot published the Kimi K2.5 technical report.<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> Its agent-swarm result is the framework I built on: scale <em>out</em>, not just up. A trainable orchestrator spawns specialised sub-agents and runs them in parallel, trained with PARL (Parallel-Agent Reinforcement Learning), where only the orchestrator learns and the sub-agents stay frozen. The headline numbers: 4.5× lower latency than a single agent, and +17.8 points on BrowseComp.<sup id="fnref:3"><a href="#fn:3" class="footnote" rel="footnote" role="doc-noteref">3</a></sup> What’s new is that <strong>the swarm designs itself</strong>: decomposition and team width are the model’s call, not a hand-written workflow.</p>

<p>What’s in the weights is only the orchestration <em>instinct</em>: how to decompose, delegate, reconcile. The runtime that makes a swarm real (spawn, isolate, parallelise, aggregate) lives in Moonshot’s hosted product, not in the open weights. An open endpoint gives me what it has always given me: messages and tools in, tool calls and text out.</p>

<p>That gap became the project: the weights bring the instinct, I build the body. <a href="https://github.com/doublewordai/swarm">doublewordai/swarm</a> is that body, my from-scratch interpretation of Moonshot’s swarm, built on the <a href="https://openresponses.org">Open Responses API</a>, model-agnostic (default: <code class="language-plaintext highlighter-rouge">moonshotai/Kimi-K2.6</code>).<sup id="fnref:credit"><a href="#fn:credit" class="footnote" rel="footnote" role="doc-noteref">4</a></sup></p>

<h2 id="the-architecture">The architecture</h2>

<p>Everything I kept from the paper, and everything I added, compresses to four principles:</p>

<ol>
  <li><strong>A self-designing orchestrator.</strong> The model decides the team and the decomposition, not me.</li>
  <li><strong>Bounded local context.</strong> Each worker sees only its slice, and returns only results.</li>
  <li><strong>Structural anti-groupthink.</strong> Independent verification before any finding counts.</li>
  <li><strong>Synthesis.</strong> One final pass reconciles everything into a deliverable.</li>
</ol>

<figure style="margin:2rem 0;overflow-x:auto;">
<svg viewBox="0 0 940 300" width="100%" style="height:auto;min-width:640px;display:block;margin:0 auto;font-family:-apple-system,'Segoe UI',system-ui,sans-serif" role="img" aria-label="Swarm pipeline: repo, orchestrator, parallel workers, verifiers, synthesizer, report">
<defs><marker id="sw-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#c2b4a0"></path></marker></defs>
<rect x="16" y="114" width="92" height="60" rx="8" fill="none" stroke="#b9ab95" stroke-width="1.5"></rect>
<text x="62" y="142" text-anchor="middle" font-size="16" fill="#2c2825">repo</text>
<text x="62" y="160" text-anchor="middle" font-size="12" fill="#8a7f70">→ map</text>
<line x1="112" y1="144" x2="134" y2="144" stroke="#c2b4a0" stroke-width="1.5" marker-end="url(#sw-arrow)"></line>
<text x="140" y="100" font-size="12" letter-spacing="0.14em" fill="#b87a18">01</text>
<rect x="140" y="108" width="140" height="72" rx="8" fill="#f1e7d3" stroke="#b87a18" stroke-width="1.5"></rect>
<text x="210" y="142" text-anchor="middle" font-size="17" fill="#2c2825">orchestrator</text>
<text x="210" y="161" text-anchor="middle" font-size="12" fill="#8a7f70">designs the team</text>
<line x1="284" y1="144" x2="322" y2="90" stroke="#c2b4a0" stroke-width="1.25" marker-end="url(#sw-arrow)"></line>
<line x1="284" y1="144" x2="322" y2="144" stroke="#c2b4a0" stroke-width="1.25" marker-end="url(#sw-arrow)"></line>
<line x1="284" y1="144" x2="322" y2="198" stroke="#c2b4a0" stroke-width="1.25" marker-end="url(#sw-arrow)"></line>
<text x="322" y="52" font-size="12" letter-spacing="0.14em" fill="#b87a18">02 · WAVE · PARALLEL</text>
<rect x="322" y="70" width="150" height="40" rx="7" fill="#f1e7d3" stroke="#b87a18" stroke-width="1.25"></rect>
<text x="338" y="94" font-size="13" fill="#2c2825">worker</text>
<text x="458" y="94" text-anchor="end" font-size="11" fill="#8a7f70">scoped task</text>
<rect x="322" y="124" width="150" height="40" rx="7" fill="#f1e7d3" stroke="#b87a18" stroke-width="1.25"></rect>
<text x="338" y="148" font-size="13" fill="#2c2825">worker</text>
<text x="458" y="148" text-anchor="end" font-size="11" fill="#8a7f70">scoped task</text>
<rect x="322" y="178" width="150" height="40" rx="7" fill="#f1e7d3" stroke="#b87a18" stroke-width="1.25"></rect>
<text x="338" y="202" font-size="13" fill="#2c2825">worker</text>
<text x="458" y="202" text-anchor="end" font-size="11" fill="#8a7f70">scoped task</text>
<text x="397" y="236" text-anchor="middle" font-size="12" font-style="italic" fill="#8a7f70">…hundreds in parallel · bounded context</text>
<line x1="472" y1="90" x2="502" y2="144" stroke="#c2b4a0" stroke-width="1.25" marker-end="url(#sw-arrow)"></line>
<line x1="472" y1="144" x2="502" y2="144" stroke="#c2b4a0" stroke-width="1.25" marker-end="url(#sw-arrow)"></line>
<line x1="472" y1="198" x2="502" y2="144" stroke="#c2b4a0" stroke-width="1.25" marker-end="url(#sw-arrow)"></line>
<text x="500" y="120" text-anchor="end" font-size="11" fill="#8a7f70">findings only</text>
<text x="508" y="100" font-size="12" letter-spacing="0.14em" fill="#b87a18">03</text>
<rect x="508" y="108" width="120" height="72" rx="8" fill="#f1e7d3" stroke="#b87a18" stroke-width="1.5"></rect>
<text x="568" y="142" text-anchor="middle" font-size="17" fill="#2c2825">verifiers</text>
<text x="568" y="161" text-anchor="middle" font-size="12" fill="#8a7f70">refute · survive</text>
<line x1="632" y1="144" x2="660" y2="144" stroke="#c2b4a0" stroke-width="1.5" marker-end="url(#sw-arrow)"></line>
<text x="666" y="100" font-size="12" letter-spacing="0.14em" fill="#b87a18">04</text>
<rect x="666" y="108" width="126" height="72" rx="8" fill="#f1e7d3" stroke="#b87a18" stroke-width="1.5"></rect>
<text x="729" y="142" text-anchor="middle" font-size="17" fill="#2c2825">synthesizer</text>
<text x="729" y="161" text-anchor="middle" font-size="12" fill="#8a7f70">reconcile</text>
<line x1="796" y1="144" x2="820" y2="144" stroke="#c2b4a0" stroke-width="1.5" marker-end="url(#sw-arrow)"></line>
<rect x="826" y="114" width="100" height="60" rx="8" fill="none" stroke="#b9ab95" stroke-width="1.5"></rect>
<text x="876" y="149" text-anchor="middle" font-size="14" fill="#2c2825" font-family="'SF Mono','Cascadia Code',monospace">report.md</text>
<path d="M397 220 C 397 272, 210 272, 210 186" fill="none" stroke="#bf9a5a" stroke-width="1.25" stroke-dasharray="4 4" marker-end="url(#sw-arrow)"></path>
<text x="304" y="288" text-anchor="middle" font-size="11" fill="#9a8f80">status + unreported files → gap-fill wave</text>
</svg>
</figure>

<p>Left to right: the repo is cloned and compressed into a budgeted map; the orchestrator designs a team and dispatches scoped tasks in parallel <em>waves</em>; ephemeral workers investigate and return findings only; findings are deduped, challenged by verifiers, and reconciled by a synthesizer into <code class="language-plaintext highlighter-rouge">report.md</code>. Four blocks, one per principle.</p>

<h3 id="block-1-the-orchestrator-designs-the-team">Block 1: the orchestrator designs the team</h3>

<p>The orchestrator gets the repo map up front and can probe with <code class="language-plaintext highlighter-rouge">read_file</code> and <code class="language-plaintext highlighter-rouge">grep</code> before committing to a plan. Then it builds its team with two tools, <code class="language-plaintext highlighter-rouge">create_subagent(name, system_prompt)</code> and <code class="language-plaintext highlighter-rouge">assign_task(agent, prompt)</code>, the literal tool surface Kimi K2.5/K2.6 were RL-trained on.<sup id="fnref:4"><a href="#fn:4" class="footnote" rel="footnote" role="doc-noteref">5</a></sup> It authors each specialist’s system prompt itself. In one real audit run it invented a persona I never asked for:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># the orchestrator wrote this prompt (persona registered once)
</span><span class="nf">create_subagent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="sh">"</span><span class="s">injection-filesystem</span><span class="sh">"</span><span class="p">,</span>
    <span class="n">system_prompt</span><span class="o">=</span><span class="sh">"</span><span class="s">You hunt injection and unsafe file access…</span><span class="sh">"</span><span class="p">,</span>
<span class="p">)</span>

<span class="c1"># every task spawns a fresh agent with that persona
</span><span class="nf">assign_task</span><span class="p">(</span><span class="sh">"</span><span class="s">injection-filesystem</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">Audit cli.py …</span><span class="sh">"</span><span class="p">)</span>
<span class="nf">assign_task</span><span class="p">(</span><span class="sh">"</span><span class="s">injection-filesystem</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">Trace cost.py …</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div>

<p>The division of labour matters: the model decides <em>who</em> does <em>what</em>; the harness decides which tools each role may hold. Each dispatching turn is a wave: width is parallelism, follow-up waves fill gaps.</p>

<h3 id="block-2-workers-see-only-their-slice">Block 2: workers see only their slice</h3>

<p>This is the paper’s key lever, the one that pays for everything: <strong>context sharding</strong>. Each task spawns a fresh, throwaway agent. It self-gathers exactly the context it needs (<code class="language-plaintext highlighter-rouge">read_file</code>, <code class="language-plaintext highlighter-rouge">grep</code>, plus whatever capability tools its role grants), works for a few rounds, calls <code class="language-plaintext highlighter-rouge">submit_results</code>, and is gone. Only schema-valid results and a status line return to the orchestrator; the worker’s research is discarded, never re-sent.</p>

<p>Context integrity and cost turn out to be the same lever pulled once: no single context ever overflows, <em>and</em> per-agent token usage stays low. That’s why fanning out hundreds of workers stays cheap, and why the 300M-token bill from earlier never materialises. (The v1 toolset is deliberately read-only, so it’s safe to point at any repo.)</p>

<h3 id="block-3-every-finding-meets-a-skeptic">Block 3: every finding meets a skeptic</h3>

<p>A swarm of enthusiastic hunters produces enthusiastic false positives. So before anything counts, each candidate finding is handed to an independent verifier whose only job is to <strong>refute it</strong>, and which defaults to “not real” when unsure. Survivors ship with adjusted severity; refuted findings are dropped and counted.</p>

<p>This stage is my addition; the paper’s orchestrator reconciles inline. It’s optional and per-brief; <code class="language-plaintext highlighter-rouge">--verify-votes N</code> turns it into a majority-vote panel.</p>

<h3 id="block-4-one-pass-writes-the-report">Block 4: one pass writes the report</h3>

<p>Finally, a single tool-free synthesis call reconciles the confirmed findings into the deliverable: <code class="language-plaintext highlighter-rouge">report.md</code> for humans, <code class="language-plaintext highlighter-rouge">findings.json</code> for machines. Its shape comes from the brief, not the engine, which brings me to the part I like most.</p>

<h2 id="swap-the-brief-keep-the-engine">Swap the brief, keep the engine</h2>

<p>Nothing in the engine mentions auditing. The loop (orchestrate, shard, verify, synthesize) is byte-identical for every task. What a swarm <em>does</em> is a <strong>brief</strong>: ~50 lines of data binding prompts to roles, a result schema (enforced at <code class="language-plaintext highlighter-rouge">submit_results</code>: invalid items are dropped, not trusted), and a tool selection per role:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># src/briefs/onboarding.py (abridged)
</span><span class="kn">from</span> <span class="n">.</span> <span class="kn">import</span> <span class="n">Brief</span><span class="p">,</span> <span class="n">register</span>

<span class="nf">register</span><span class="p">(</span><span class="nc">Brief</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="sh">"</span><span class="s">onboarding</span><span class="sh">"</span><span class="p">,</span>
    <span class="n">description</span><span class="o">=</span><span class="sh">"</span><span class="s">Document a codebase</span><span class="sh">'</span><span class="s">s subsystems for newcomers.</span><span class="sh">"</span><span class="p">,</span>
    <span class="n">orchestrator_prompt</span><span class="o">=</span><span class="sh">"</span><span class="s">You are the lead author … call dispatch_workers once …</span><span class="sh">"</span><span class="p">,</span>
    <span class="n">worker_prompt</span><span class="o">=</span><span class="sh">"</span><span class="s">Document ONLY your assigned files: purpose, key components, deps …</span><span class="sh">"</span><span class="p">,</span>
    <span class="n">synthesis_prompt</span><span class="o">=</span><span class="sh">"</span><span class="s">Assemble an onboarding guide: overview, per-subsystem sections …</span><span class="sh">"</span><span class="p">,</span>
    <span class="n">result_schema</span><span class="o">=</span><span class="p">{...},</span>
    <span class="n">result_key</span><span class="o">=</span><span class="sh">"</span><span class="s">sections</span><span class="sh">"</span><span class="p">,</span>
    <span class="n">worker_tools</span><span class="o">=</span><span class="p">(</span><span class="sh">"</span><span class="s">read_file</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">grep</span><span class="sh">"</span><span class="p">),</span>
    <span class="n">verifier_prompt</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span>   <span class="c1"># set a prompt to switch the adversarial verify stage on
</span><span class="p">))</span>
</code></pre></div></div>

<p>Two briefs ship in the box, <code class="language-plaintext highlighter-rouge">audit</code> and <code class="language-plaintext highlighter-rouge">onboarding</code>, and the ones I could write are an afternoon each: a dependency review (one worker per dependency: version drift, advisories, upgrade risk), a refactor plan (workers map usage per module, the synthesizer sequences the steps), wide research (one worker per source; verifiers refute unsupported claims).</p>

<h2 id="the-receipts">The receipts</h2>

<p>The two runs, side by side:</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Solo agent (Claude Opus)</th>
      <th>Swarm (Kimi K2.6)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Tokens</td>
      <td>~300M · projected</td>
      <td>5.6M · measured</td>
    </tr>
    <tr>
      <td>Cost</td>
      <td>~$300</td>
      <td>~$6.70</td>
    </tr>
    <tr>
      <td>vs. the 2.4M read-once floor</td>
      <td>×125</td>
      <td>×2.3</td>
    </tr>
  </tbody>
</table>

<figure style="margin:2rem 0;">
<svg viewBox="0 0 760 130" width="100%" style="height:auto;max-width:640px;display:block;margin:0 auto;font-family:-apple-system,'Segoe UI',system-ui,sans-serif" role="img" aria-label="Same audit: solo agent projected 300M tokens versus swarm measured 5.6M">
<text x="0" y="40" font-size="13" fill="#2c2825">Solo agent · projected</text>
<rect x="200" y="24" width="460" height="24" rx="2" fill="#b3a896"></rect>
<text x="672" y="40" font-size="13" fill="#8a7f70">~300M</text>
<text x="0" y="90" font-size="13" fill="#2c2825">Swarm · measured</text>
<rect x="200" y="74" width="9" height="24" rx="2" fill="#b87a18"></rect>
<text x="217" y="90" font-size="13" font-weight="600" fill="#b87a18">5.6M</text>
</svg>
<figcaption style="text-align:center;color:#8a7f70;font-size:0.85rem;font-style:italic;margin-top:0.6rem;font-family:-apple-system,system-ui,sans-serif">The same audit: the solo agent's projected ~300M tokens against the swarm's measured 5.6M, about 53× fewer.</figcaption>
</figure>

<p>The swarm’s run, measured: 348 API calls, 5.2M tokens in, ~450k out, about <strong>53× fewer tokens and 45× cheaper</strong> at Doubleword’s Kimi K2.6 pricing ($0.95/M input, $4/M output).<sup id="fnref:5"><a href="#fn:5" class="footnote" rel="footnote" role="doc-noteref">6</a></sup> The output held up, too. The verifier stage refuted and dropped roughly half of the candidate findings before any of them reached me, so what survived came with severity, <code class="language-plaintext highlighter-rouge">file:line</code>, and a suggested fix attached.</p>

<h2 id="nobodys-waiting-the-flex-tier">Nobody’s waiting: the flex tier</h2>

<p>A swarm is a <a href="https://blog.doubleword.ai/inference-when-no-one-is-waiting">just-get-it-done workload</a>: hundreds of concurrent calls, and no human watching any single one. It’s throughput-bound: what matters is when the whole wave lands, not when each call returns.</p>

<p>Doubleword’s flex tier is priced for this. Individual calls may run longer, but global throughput holds, so end-to-end wall-clock stays roughly the same, at ~30% off.<sup id="fnref:6"><a href="#fn:6" class="footnote" rel="footnote" role="doc-noteref">7</a></sup></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">service_tier</span> <span class="o">=</span> <span class="sh">"</span><span class="s">flex</span><span class="sh">"</span>   <span class="c1"># was "priority"
</span></code></pre></div></div>

<h2 id="run-it-on-your-repo">Run it on your repo</h2>

<p>The quickest path is the <a href="https://docs.doubleword.ai">dw CLI</a>, which sets up auth, endpoint, and model in one step:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dw login
dw examples clone swarm
<span class="nb">cd </span>swarm
dw project setup
</code></pre></div></div>

<p>Then point a brief at a repo:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dw project run audit      <span class="nt">--</span> <span class="nt">--repo</span> psf/requests <span class="nt">--max-files</span> 20         <span class="c"># audit a GitHub repo</span>
dw project run onboarding <span class="nt">--</span> <span class="nt">--path</span> ./my-service                        <span class="c"># document a local directory</span>
dw project run audit      <span class="nt">--</span> <span class="nt">--repo</span> psf/requests <span class="nt">--service-tier</span> flex    <span class="c"># run on flex tier for 30% cost saving!</span>
dw project run report                                                   <span class="c"># print the latest run's report</span>
</code></pre></div></div>

<p>Each run writes <code class="language-plaintext highlighter-rouge">results/&lt;brief&gt;-&lt;slug&gt;/</code>: the synthesized <code class="language-plaintext highlighter-rouge">report.md</code>, structured <code class="language-plaintext highlighter-rouge">findings.json</code>, <code class="language-plaintext highlighter-rouge">swarm-tree.json</code> (the team the orchestrator designed, worth reading at least once), and <code class="language-plaintext highlighter-rouge">summary.json</code> with tokens, cost, and step counts.</p>

<p>Everything is open: the harness is <a href="https://github.com/doublewordai/swarm">on GitHub</a>, it speaks the <a href="https://openresponses.org">Open Responses API</a>, and it’s model-agnostic: <code class="language-plaintext highlighter-rouge">-m</code> any tool-caller you like, Doubleword’s or otherwise.</p>

<p><a href="https://doubleword.ai">Doubleword</a> is built for this kind of high-throughput inference, and I’d love to see what you fan out. Clone the swarm, write a brief, and go run parallel agents.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>These numbers come from an actual metered run. The agent repeatedly fills its window, hits the compaction ceiling, summarises, and grows again. With prompt caching, the projected full-repo bill is ~$300; without it, ~$1,800. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2">
      <p><a href="https://arxiv.org/abs/2602.02276">Kimi K2.5: Visual Agentic Intelligence</a>, Kimi Team, February 2026. See also Moonshot’s <a href="https://www.kimi.com/blog/agent-swarm">agent swarm post</a>. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3">
      <p>60.6 → 78.4 on BrowseComp, a deep-research benchmark, versus the single-agent baseline. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:credit">
      <p>Full credit to Moonshot for the pattern. The repo’s README has a <a href="https://github.com/doublewordai/swarm#faithful-to-kimi">“Faithful to Kimi”</a> section spelling out what I reproduced (the self-designing orchestrator, context sharding, the critical-steps metric), what I deliberately dropped (PARL training, the mutating toolbox), and what’s mine. <a href="#fnref:credit" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4">
      <p>K2.5 technical report, Appendix E.8: this is <code class="language-plaintext highlighter-rouge">--interface kimi</code>, which the repo ships as the default. I also ship <code class="language-plaintext highlighter-rouge">--interface structured</code>, where the orchestrator instead calls <code class="language-plaintext highlighter-rouge">dispatch_workers([{role, focus, paths}])</code> and the harness preloads each worker’s files, decomposing by directory rather than by task, which keeps the planning turn small on very large repos. <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:5">
      <p>Solo figures are projected from the metered partial run, with prompt caching priced in; swarm figures are measured. Every run also writes <code class="language-plaintext highlighter-rouge">summary.json</code> with tokens, cost, coverage, and the paper’s critical-vs-total step counts: <code class="language-plaintext highlighter-rouge">speedup = total / critical</code> scores how well the orchestrator actually parallelised, the way the paper scores it. <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:6">
      <p>Tier discounts are per-model; ~30% is Doubleword’s flex pricing for Kimi K2.6 at the time of the run. <code class="language-plaintext highlighter-rouge">swarm compare &lt;brief&gt; --repo …</code> runs the identical workload on both tiers and writes the wall-clock / token / cost table, so you can measure the trade on your own job. <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Peter Bhabra</name></author><category term="ai" /><category term="agents" /><category term="inference" /><category term="kimi" /><summary type="html"><![CDATA[I rebuilt Moonshot's Kimi agent swarm and pointed it at a real codebase: ~53× fewer tokens and ~45× cheaper than one long-context agent.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://peterbhabra.com/assets/images/bees-styled.jpg" /><media:content medium="image" url="https://peterbhabra.com/assets/images/bees-styled.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>