GigaToken 說自己在 AMD EPYC 上比 HuggingFace tokenizers 快 989 倍、在 Apple M4 Max 上快 1,268 倍——同一份 README 也寫著:那次測試,HuggingFace 只分到 11.9 GB 檔案裡的前 100 MB,GigaToken 讀的是整個檔案。
GigaToken 的 1000 倍:一次對 tokenizer benchmark 的追問
跑一次 pip install gigatoken,把 tokenizer 換成官方推薦的相容模式,README 打出的招牌數字是「~1000x faster than HuggingFace's tokenizers, drop-in replacement」。翻到最下面收合的 benchmark 區塊,數字確實嚇人:AMD EPYC 9565(144 核、雙路)上,GPT-2 tokenizer 從 HuggingFace 的 24.8 MB/s 衝到 GigaToken 的 24.53 GB/s,換算下來是 989 倍。頁面最上方那張速度圖下面,還有一句不太顯眼的但書:「Note that both HF tokenizers and tiktoken are already running multithreaded Rust」——換句話說,這不是「Rust 打敗 Python」的簡單故事,對手本來就是多執行緒的 Rust 實作。這個專案掛在 GitHub 上,近千顆星,MIT 授權,語言比例是 Rust 66.2%、Python 33.3%、Shell 0.5%,標籤是 nlp、tokenizer、tokenization、llm——看起來是一個認真做效能的專案,不是空手套白狼。真正該問的問題也就浮出來了:989 倍、1,268 倍,這些數字到底是怎麼量出來的?
同一顆 GPT-2 tokenizer,倍數從 106 跳到 1,268
把同一份 README 往下翻,benchmark 收合區塊裡列了三顆 CPU 各自跑 GPT-2 的成績。AMD EPYC 9565(144 核、雙路)上是 989 倍;換到 Apple M4 Max(16 核),倍數不減反增,衝到 1,268 倍;換到桌上型的 AMD Ryzen 7 9800X3D(同樣 16 核),倍數卻掉到只剩 106 倍。三顆 CPU 的核心數分別是 144、16、16——如果 989 倍、1,268 倍、106 倍純粹反映硬體平行度,M4 Max 跟 Ryzen 核心數一樣,倍數卻能差到 12 倍,這已經不是「核心多就跑得快」能解釋的落差。把三顆 CPU 的原始數字攤開來看會更清楚:EPYC 是 24.53 GB/s 對 24.8 MB/s(HF)與 36.0 MB/s(tiktoken);M4 Max 是 8.79 GB/s 對 6.9 MB/s 與 62.8 MB/s;Ryzen 是 6.27 GB/s 對 59.0 MB/s 與 92.1 MB/s。GigaToken 自己的吞吐量從 EPYC 到 Ryzen 只掉了不到 4 倍(24.53 降到 6.27),但「vs HF」的倍數卻從 989 掉到 106,掉了 9 倍多。倍數下降的速度跟 GigaToken 自己變慢的速度對不上,這代表分母(HuggingFace 的 MB/s)本身在三顆 CPU 上的表現差異也很大,而且差異的方向跟 GigaToken 不完全一致。倍數的落差如果不是單純來自硬體核心數、也不是來自程式語言的先天差距,剩下能查的地方,就是這三場測試各自量的到底是什麼樣的工作量。
如果假設 GigaToken 的吞吐量會隨核心數線性增加,144 核的 EPYC 理論上應該比 16 核的 Ryzen 快 9 倍左右;但實際數字是 24.53 GB/s 對 6.27 GB/s,只差了 3.9 倍。M4 Max 同樣是 16 核,吞吐量卻是 8.79 GB/s,比 16 核的 Ryzen 快了四成多。核心數相同、吞吐量卻有落差,代表除了核心數以外,還有微架構、記憶體頻寬、SIMD 指令集在起作用——README 的 AI Use Disclosure 裡也提到,「Porting SIMD strategies between AVX512/AVX2/NEON」是後期靠 AI 協助完成的工作之一,等於承認不同指令集之間的移植本身就有落差。這些因素同樣會影響 HuggingFace 跟 tiktoken 的表現,只是影響的方向跟幅度不保證一樣,這也是為什麼同一個核心數、倍數卻能差 12 倍這件事,光靠核心數本身解釋不通。
如果把 GPT-2 以外的 tokenizer 也拉進來看,這個「倍數對硬體特別敏感」的現象不是特例。OLMo 2 / 3 在 EPYC 上的倍數是 833 倍,換到 M4 Max 卻衝到 1,299 倍,比 GPT-2 在同一組硬體上的落差還誇張;Phi-4 在 EPYC 是 801 倍,DeepSeek V3 / R1 / V4 是 750 倍。GPT-OSS 這一列比較特別,是少數三顆 CPU 都同時填了 tiktoken 數字的例子:EPYC 上是 482 倍(對 HF)、560 倍(對 tiktoken),M4 Max 是 306 倍、71 倍,Ryzen 是 71 倍、50 倍——同一顆 tokenizer、同一份檔案,三顆 CPU 上兩種倍數的排列組合完全不同。同一份表格裡,Kimi K2 那一列 HuggingFace 和 tiktoken 兩欄都留白,沒有給任何對照數字,不是每一顆 tokenizer 都真的跑過同一套比較。這些散落在表格各處的細節,合起來指向同一個方向:把「倍數」當成單一、穩定的常數來引用,本身就跟這份 benchmark 自己攤出來的變異程度對不上。
Gemma 4 這一列很特別:它在 EPYC 上的 GigaToken 吞吐量是 4.82 GB/s,看起來遠低於 GPT-2 的 24.53 GB/s;但 HuggingFace 跑 Gemma 4 本身是 334.1 MB/s,遠高於跑 GPT-2 的 24.8 MB/s——兩邊都變快了,只是 HuggingFace 變快的幅度更大,最後倍數只剩 14 倍。這說明「GigaToken 自己跑多快」和「倍數有多高」是兩件不完全同步的事,後者還取決於對照組本身在同一顆 tokenizer 上表現如何。Llama 3 / 3.1 / 3.2 跟 Llama 3.3 也是類似情況:前者在 EPYC 上是 457 倍,後者卻是 431 倍,數字相近,仍不是同一個。ModernBERT 則是另一個極端對照組——它跟 GPT-2 一樣不是 SentencePiece,倍數卻只有 155 倍,遠低於 GPT-2 的 989 倍,顯示「是不是 SentencePiece」這個二分法本身也不能完全解釋倍數落差,中間還有其他沒被 README 點名的因素在起作用。
藏在收合區塊裡的那句話
答案不在表格裡,在表格下面另一個標成「Benchmark details」的收合區塊——大多數讀者滑過表格看到倍數就關掉分頁,不會點開這一段。裡面第一句話就是自白:「Gigatoken encodes the whole file un-split, and is thus doing more work than the other tokenizers to find the split boundaries and automatically parallelize.」緊接著第二句話交代了對手實際拿到的資料量:「HuggingFace tokenizers (encode_batch_fast) gets the first 100 MB and tiktoken (encode_ordinary_batch) the first 1 GB, both presplit on <|endoftext|>.」也就是說,同一份 11.9 GB 的 owt_train.txt(README 說選這份 OpenWebText 語料是因為「it's roughly representative of the text you get after extraction from CommonCrawl documents」),GigaToken 讀了全部,HuggingFace 只分到前 100 MB——整份檔案的 0.84%,tiktoken 分到前 1 GB——8.4%,而且這兩段還是先按文件邊界切好的,GigaToken 自己得從頭掃過原始位元組去找這些邊界,等於多做了一輪對手不用做的工。
表格裡兩個對照組各自呼叫的方法名稱,其實已經寫出了它們分到的資料量:HuggingFace 側量的是
encode_batch_fast
README 原文:「gets the first 100 MB … both presplit on <|endoftext|>」——只吃了 11.9 GB 檔案裡最前面的 100 MB,而且已經先切好文件邊界。
,tiktoken 側量的是
encode_ordinary_batch
README 原文:「tiktoken (encode_ordinary_batch) the first 1 GB」——分到前 1 GB,是 HuggingFace 那份的十倍大,但仍只是整份檔案的 8.4%。
。GigaToken 那一欄沒有對應的方法名稱限制,因為它讀的本來就是整個檔案。
「encode_batch_fast」這個名稱裡的「fast」二字,暗示它不是 HuggingFace tokenizers 函式庫裡相對慢的純 Python 路徑,而是已經呼叫底層 Rust 實作的批次介面。也就是說,GigaToken 拿來對比的,已經是 HuggingFace 自己效能最好的呼叫方式,不是刻意挑一個慢版本來當墊背。benchmark details 那個收合區塊,還有一句常被忽略的但書:「Tiktoken rows are currently only filled in for tokenizers with official support.」這也是為什麼表格裡大多數列的 tiktoken 欄是空的,只有 GPT-2 跟 GPT-OSS 這兩顆有官方支援、填得出數字;其餘 20 種 tokenizer,「vs tiktoken」這一欄從一開始就無從比較。換句話說,989 倍跟 681 倍是這份表格裡少數幾個能同時對兩邊做比較的例子,不是常態。
README 對這個落差給的解釋是:「This is fair because neither of the compared tokenizers do caching, meaning the speed is roughly uniform throughout processing.」——邏輯是:HuggingFace 和 tiktoken 都不做快取,所以不管量測 100 MB 還是 11.9 GB,它們每秒能吃的位元組數應該差不多,跑更大的樣本不會讓 MB/s 這個數字本身跑掉。這個推論在對手那一側沒有問題,但它沒有回答另一半的問題:GigaToken 自己在讀到完整、連續的大檔案時,是不是才吃得到 SIMD 掃描與跨執行緒平行化的完整效益?合理的推測是,如果答案是「會」,「公平」這句話就只講了故事的一半——對手的速度確實跟樣本大小無關,但 GigaToken 自己的速度,很可能正好跟樣本大小有關,README 並沒有另外給出「GigaToken 只讀 100 MB」時的對照數字來排除這個可能。
100 MB 是 11.9 GB 的 0.84%,1 GB 是 8.4%——換算成秒數更直覺:EPYC 那次測試,HuggingFace 只花 4.033 秒,GigaToken 花 0.486 秒卻讀了 119 倍大的資料。「automatically parallelize」這幾個字也值得注意:GigaToken 要在一份沒有事先標記邊界的連續位元組流裡,自己找出文件分界、再把工作分給多個執行緒,這件事本身在檔案越大、核心數越多的時候,越有機會把每個執行緒該處理的區塊切得夠大、夠平均;而 HuggingFace 跟 tiktoken 從一開始就拿到已經切好的 100 MB 或 1 GB,不需要自己做這一步。這也呼應了前面「GigaToken 讀到完整連續大檔案才吃得到 SIMD 與平行化完整效益」的推測——規模本身可能就是效益的一部分,不只是工作量的差異。
換一個基準,989 倍馬上不一樣
README 的 FAQ 裡附了一段可以直接複製貼上執行的指令,順便把上面這個落差攤在陽光下。指令會下載 owt_train.txt,然後跑 uvx --with tokenizers gigatoken bench 'openai-community/gpt2' owt_train.txt --validate --doc-separator "<|endoftext|>" 做一次驗證。README 附的實際輸出,跑在 EPYC 上是這樣:
cpu: AMD EPYC 9565 72-Core Processor, 144 cores, 2 sockets
gigatoken: 0.486 s | 11920.51 MB at 24532.45 MB/s | 2701.65 Mtok at 5564.94 Mtok/s
hf: 4.033 s | 100.00 MB at 24.80 MB/s | 22.76 Mtok at 5.63 Mtok/s
gigatoken is 989.21x faster than hf
validation OK: 20401 documents match
同一次執行、同一行輸出裡,gigatoken 那一列讀了 11920.51 MB——幾乎就是整份 11.9 GB 檔案,hf 那一列讀了 100.00 MB,兩者算出的「989.21x」,分子跟分母從一開始就不是在量同一件事。README 自己也講白了為什麼:「This example uses the train sample from this dataset, and the CLI by default subsets to the first 100MB of the file for validation and comparison with HF.」——CLI 預設就是只給 HuggingFace 前 100 MB 做比較。那如果換一個基準呢?tiktoken 那邊分到的是前 1 GB,比 HuggingFace 的 100 MB 大十倍,同一顆 CPU、同一顆 GPT-2 tokenizer,倍數會怎麼動?
同一份 FAQ 也附了跑在 Apple M4 Max 上的版本:
cpu: Apple M4 Max, 16 cores
gigatoken: 1.432 s | 11920.51 MB at 8327.05 MB/s | 2701.65 Mtok at 1887.23 Mtok/s
hf: 16.250 s | 100.00 MB at 6.15 MB/s | 22.76 Mtok at 1.40 Mtok/s
gigatoken is 1353.13x faster than hf
validation OK: 20401 documents match
兩份輸出的規律一模一樣:gigatoken 那一列永遠讀 11920.51 MB,hf 那一列永遠讀 100.00 MB。有意思的是,這個 quickstart 範例算出來的是 1353.13 倍,跟前面表格裡 M4 Max 的 1,268 倍並不完全一樣——同一顆硬體、同一顆 tokenizer,兩次測到的倍數本身就有出入,說明這類跑分數字本來就有測量誤差,不是釘死不動的常數。README 也提醒了一個容易忽略的細節:「You might need to run your commands twice on macOS to get a good reading, since the first run will always perform a security scan, which will slow down the Rust code.」連「怎麼量」這件事本身,在不同作業系統上都得多想一層。兩次 CLI 驗證的最後一行都寫著「validation OK: 20401 documents match」——這代表 GigaToken 算出來的 token 序列,跟 HuggingFace tokenizer 在這 20401 份文件上逐一比對後完全一致,不是「快但結果不一樣」的取巧。這也呼應了 Usage 那段提到的:「A substantial amount of effort has been put into making sure the outputs match exactly with what you would get with HuggingFace Tokenizers」——正確性跟速度是兩件分開被驗證的事,README 沒有含糊帶過。
下面這張圖把三顆 CPU、兩種基準都放進去,可以自己切換看看。
拖曳滑桿切換 CPU · 3 顆硬體、2 種基準
三顆 CPU 呈現的是同一個模式:換成 1 GB 的基準,倍數就掉了一截——EPYC 從 989 倍降到 681 倍,Ryzen 從 106 倍降到 68 倍。M4 Max 的降幅特別大,從 1,268 倍掉到只剩 140 倍,掉了 9 倍多,代表 tiktoken 在 Apple 晶片上跟在 x86 上的表現落差,比 HuggingFace 大得多,但 README 沒有進一步解釋原因。可以確定的是:光是換掉分母,「1000 倍」這個招牌數字就先自己動了一次,這還沒把「整檔 vs 只吃一段」的落差算進去。
如果只看秒數而不看倍數,落差感覺沒那麼戲劇化:EPYC 那次測試,GigaToken 花 0.486 秒,HuggingFace 花 4.033 秒,兩者都在個位數秒內結束。真正讓「1000 倍」這個數字有意義的場景,是資料量遠大於 11.9 GB 的情境——訓練用的語料動輒數 TB 到數十 TB,這時候 1000 倍或 100 倍的差距,會直接反映在小時甚至天數的等待時間上。但反過來說,如果一個團隊的資料集本來就只有幾百 MB 到幾 GB,兩種工具跑起來都是秒級,「1000 倍」在這種場景裡幾乎沒有實際意義,倍數大小終究取決於你的資料量落在哪個尺度。
把 CPU 釘死,光換 tokenizer,倍數還能跳一百多倍
presplit 的落差之外,還有一個跟它完全無關、卻同樣把「1000 倍」稀釋掉的變因:換一顆 tokenizer。把 CPU 釘死在同一顆 EPYC 9565,拿掉硬體變因,README 完整表格裡列了 22 種 tokenizer 對 HuggingFace 的倍數。最低的一端是 Gemma 1,只有 7.3 倍;Gemma 3 是 9.6 倍,CodeLlama 是 10.0 倍,Mistral 7B v0.3 是 10 倍,TinyLlama / Phi-3(Llama 2 詞表)是 11 倍——這幾顆全部卡在個位數到十幾倍之間。往上到 ModernBERT 是 155 倍,Llama 3.3 是 431 倍,最高的一端,GPT-2 是 989 倍。同一份 GigaToken 程式碼、同一顆 CPU、同一份 11.9 GB 檔案,光是選哪一顆 tokenizer,倍數就能從 7.3 一路跳到 989,相差 135 倍。README 自己點出了原因:「The slowest rows are the SentencePiece-based tokenizers, which are not well optimized in Gigatoken.」——Gemma、Mistral、CodeLlama 這一批用的都是 SentencePiece 分詞方式,GigaToken 沒有像對待 BPE 系那樣認真優化它們,倍數自然壓不上去。
AI Use Disclosure 裡還提到,AI 協助的其中一塊工作是「Widening of compatibility, for instance generalizing and porting the pretokenizer implementations to support more tokenizers, less interesting features like padding/truncation/unicode normalization」——也就是說,表格裡能同時列出 22 種 tokenizer,本身就是後期擴充相容性的成果,不是從一開始設計時就均勻覆蓋所有架構。這也部分解釋了為什麼優化程度會隨 tokenizer 種類不均:核心的 SIMD pretokenization 跟 pretoken 快取,顯然是針對 BPE 系統先打磨出來的,SentencePiece 系統則是後來才補上相容,還沒享受到同等程度的優化。表格附註也解釋了為什麼 22 種 tokenizer 可以涵蓋這麼多模型:每一列其實代表一整個家族。像「Llama 3 / 3.1 / 3.2」這一列,同時涵蓋了「DeepSeek-R1-Distill-Llama、Hermes 3、Saiga」這些用同一套 vocab、merges、pretokenizer 的微調版本;「Qwen 2 / 2.5」那一列則涵蓋了「Qwen3-Coder、Qwen3-VL、DeepSeek-R1 Qwen distills」等衍生模型。換句話說,README 拿 22 顆 tokenizer 測試,實際上覆蓋的模型數量遠不止 22 個——但這也代表,選中的這 22 個代表值本身,決定了「1000 倍」這個數字最終會落在哪個區間。這個「換 tokenizer 就能讓倍數跳一百多倍」的模式,換到 M4 Max 上也一樣成立:SentencePiece 系的 Gemma 系列同樣卡在十幾到二十幾倍,BPE 系的 OLMo 2 / 3、DeepSeek V3 / R1 / V4 則衝上七百倍以上。presplit 造成的落差、硬體造成的落差、tokenizer 選擇造成的落差,是三個彼此獨立、卻同時作用在同一個「1000 倍」標題上的變因。Phi-4 跟 Phi-4-mini 這兩列也值得對照:Phi-4 在 EPYC 上是 801 倍,Phi-4-mini 卻只有 726 倍——同樣屬於 Phi 家族、優化程度應該接近的兩顆 tokenizer,倍數還是有一段差距,說明就算排除 presplit 爭議、排除硬體差異,光是模型版本的細微差別,都可能讓「這顆 tokenizer 歸類到哪個效能區間」出現不確定性。
拖曳滑桿切換 tokenizer · 同一顆 EPYC,8 個代表性樣本
換句話說,「1000 倍」這個標題數字,其實是挑了 GigaToken 表現最好的一顆 tokenizer(GPT-2)、表現最好的一顆硬體(M4 Max 的 1,268 倍)湊出來的上限,不是這個工具的平均表現,也不是任何一次隨機抽樣會拿到的數字。
技術是真的——問題是「1000 倍」量的是哪一段
把上面幾層落差都攤開之後,剩下的問題是:GigaToken 到底有沒有真材實料?答案是有。FAQ 對「為什麼這麼快」給的解釋是:「The major improvements are in optimizing heavily an implementation that usually is outsourced to a Regex engine (pretokenization) using SIMD, minimizing branching and other tricks, as well as heavily optimizing caching of pretoken mappings (if a word has been seen before, look it up its encoded tokens efficiently).」也就是把大多數 tokenizer 外包給 regex engine 處理的 pretokenization 這一步,改成手寫的 SIMD 掃描,同時把「這個詞看過沒有、看過的話直接查表」這件事也做了重度快取。README 也老實承認快取不好做:「Caching is a very hard problem in this domain since the cache grows very quickly, and pretoken distributions are very long-tailed.」長尾分布代表快取會一直長大,不會白白省事。除此之外還有一句:「Some gains are also achieved from minimizing interactions with Python, and avoiding communication between threads.」——減少跟 Python 交界處的開銷,也減少執行緒之間互相等待。
三個真實存在的優化,各自負責什麼
點任一張卡片看詳細說明
SIMD pretokenization
「The major improvements are in optimizing heavily an implementation that usually is outsourced to a Regex engine (pretokenization) using SIMD, minimizing branching and other tricks」——大多數 tokenizer 把斷詞前處理外包給 regex engine,GigaToken 自己手寫 SIMD 掃描取代它。
pretoken 快取
「heavily optimizing caching of pretoken mappings … Caching is a very hard problem in this domain since the cache grows very quickly, and pretoken distributions are very long-tailed.」
減少 Python 互動
「Some gains are also achieved from minimizing interactions with Python, and avoiding communication between threads.」但 Known Issues 也承認:「Python iteration is handled in Rust, but uses ABI3, which is slower than using internal version-specific CPython APIs … Early experiments show a 2x speed improvement for overhead-bound cases.」
不過,「1000 倍」也不是隨便怎麼用都拿得到。README 在 Usage 那段講得很清楚:相容模式——直接包住既有的 HuggingFace tokenizer,輸出保證逐位元組一致——「is at a non-negligible cost to performance. You can still expect way faster performance across the board, but not quite the 1000x you will get with the Gigatoken API.」真的要拿到招牌數字,得改用 GigaToken 自己的原生 API,讓 Rust 直接讀檔案、跳過 Python 那一層;而「drop-in replacement」這句話被逐字實現的時候,速度反而要打折扣。作者自己還算過一筆帳:「At the rates we see on the EPYC CPU, you could tokenize the entirety of Common Crawl (often considered to be the entire internet, 130 trillion tokens) in just under 6.5 hours!」這句話是拿 EPYC 那個 989 倍場景直接外推出來的,跟前面談到的基準落差是同一個數字,用起來得記得它背後的但書。專案的 AI Use Disclosure 也寫得坦白:「A majority of this code base was crafted by hand without any use of AI」,AI 只在最後階段參與了「the last ~4x worth of performance from eliminating branching and improving the pretoken cache hierarchy」這類收尾工作。技術是真的,工程也是紮實的,只是那個貼在標題上的「1000 倍」,量的是特定 tokenizer、特定硬體、特定 API 模式底下的上限,不是隨手拿來套用的通用倍數。
對一個真的要導入 GigaToken 的工程團隊來說,這個落差有實際後果:如果專案要求輸出跟現有 HuggingFace tokenizer 逐位元組一致(多數上線中的訓練 pipeline 都有這個要求),能拿到的是「相容模式」的效能,而不是 README 封面上的 1000 倍;只有能接受換掉整條 tokenizer 介面、直接呼叫 GigaToken 原生 API、放棄逐位元組相容保證的場景,才有機會摸到那個上限。換句話說,評估這個工具值不值得換,得先問清楚自己的 pipeline 屬於哪一種場景,而不是直接拿封面數字去乘。README 的 Known Issues 欄還有幾筆帳:「File sinks are not yet implemented in the Gigatoken API」「WordPiece is not yet supported」「Windows has not been tested much, so for now prefer using WSL」——這些都不是隱藏起來的小字,是作者主動列出的待辦清單。Citation 區塊給的 bibtex 標題也很直白:《Gigatoken: SIMD and Cache Hierarchies for 1000x Faster Byte-Pair Encoding Tokenization on Modern CPUs》,連論文標題都把「1000x」寫了進去——這個數字不只是行銷用語,是作者對這個專案自我定位的核心主張。回到最開頭那近千顆星、MIT 授權、Rust 六成六、Python 三成三的專案簡介——這些數字說明的是一個真實、有人在維護的開源專案,不是一次性的行銷網頁。正因為如此,它的 benchmark methodology 更值得被認真拆解,而不是被「1000 倍」這三個字直接帶過。
Take-away:下次看到「Nx 加速」這種標題數字,先去找 benchmark details 那個收合區塊——真正該問的不是「這個數字是不是造假」,是「分子跟分母,到底量的是不是同一份工作量」。