這週看到 GigaToken 的標題,我第一個反應是先找測試機規格。

「比 Hugging Face Tokenizers 快約 1000 倍」很難不點。但官方那筆 989 倍跑在雙路 AMD EPYC 9565,共 144 核心;另一筆 Apple M4 Max 甚至超過 1200 倍。這些數字是真的,直接搬到一般開發機上就很可疑。

我讓 Codex 在我的 Windows 筆電建一個乾淨環境,拿實際開發專案裡的公開套件程式碼重跑。最後沒有 1000 倍。資料先讀進記憶體時,套件 CLI 回報的三次倍率落在 39.86~49.77 倍,中位數 43.55 倍;把磁碟讀取也算進去,中位數剩 15.57 倍。

少掉一個零之後,速度仍然很兇。

我的測試環境

這次用的是一台已經不新的開發筆電:

  • Windows 11
  • AMD Ryzen 7 4800H,8 核心、16 執行緒
  • 16 GB 記憶體
  • Python 3.12.2
  • gigatoken 0.10.0
  • tokenizers 0.23.1
  • GPT-2 tokenizer

語料來自 Hexo 專案 node_modules 裡的 JS、TS、JSON 與 Markdown。我排除 minified 檔、source map、lockfile 和超過 256 KiB 的單檔,留下 8,783 份文件,原始檔約 37.67 MB,編碼後共 1,617 萬個 token。

這不是官方使用的 11.9 GB OpenWebText。它更接近我會拿 tokenizer 處理的東西:程式碼、設定檔、README,以及大量長短不一的小文件。

第一個坑:README 的相容模式範例跑不起來

我先照 GigaToken 0.10.0 的 PyPI 說明執行:

1
2
3
4
import gigatoken as gt

tokenizer = gt.Tokenizer(hf_tokenizer).as_hf()
tokens = tokenizer.encode_batch(["first document", "second document"])

結果直接噴:

1
AttributeError: 'HFCompat' object has no attribute 'encode_batch'

HFCompat 實作的是 Transformers 風格的 tokenizer(...),不是底層 tokenizers.Tokenizer.encode_batch()。0.10.0 能跑的寫法是:

1
2
3
4
5
6
7
8
9
10
11
import gigatoken as gt
from tokenizers import Tokenizer

hf = Tokenizer.from_pretrained("openai-community/gpt2")
compat = gt.Tokenizer(hf).as_hf()

tokens = compat(
["first document", "second document"],
add_special_tokens=False,
return_attention_mask=False,
)["input_ids"]

0.10.0 是兩天前才發布的 beta 版本,README 和 API 沒對齊不算意外。問題是它把自己標成 drop-in replacement,文件範例就該能直接執行。評估新工具時,我現在會先跑最短範例;連這關都沒過,後面的遷移成本要多留一點空間。

用套件自己的 benchmark 重跑

我沒有自己發明計時方式。GigaToken 內建 bench --validate,會同時跑自己的實作與 Hugging Face encode_batch(),再逐份比對 token ID。

整理過的指令如下,developer-corpus.txt 是前面那 8,783 份文件合成的語料:

1
2
3
4
gigatoken bench "openai-community/gpt2" "developer-corpus.txt" `
--validate `
--comparison-limit none `
--doc-separator "<|giga_bench_document_20260727|>"

這條指令預設會在計時前把檔案讀進記憶體。GigaToken 走 encode_batch(BytesSource),Hugging Face 則先在 Python 切成一份份字串,再呼叫 encode_batch()。所以接下來的 43.55 倍是記憶體內原生資料路徑,沒有磁碟 I/O,也不是 encode_files() 的成績。

我連跑三次:

次數 GigaToken Hugging Face CLI 回報倍率
1 0.160 秒 6.904 秒 43.55×
2 0.121 秒 5.958 秒 49.77×
3 0.170 秒 6.743 秒 39.86×

三次都得到 validation OK: 8783 documents match。GigaToken 處理的原始檔包含分隔符,CLI 顯示 37.67 MB;Hugging Face 收到切開後的文件,合計 37.39 MB。兩邊輸出的文件數和 1,617 萬個 token ID 一致,但輸入位元組不是完全相等。表裡的數字是 CLI 依各自 throughput 回報的倍率,這點不能藏在表格下面。

我再加上 --stream-from-disk 重跑。這時 GigaToken 才會走 encode_files();兩邊計時都包含讀檔、切文件與編碼:

次數 GigaToken Hugging Face CLI 回報倍率
1 0.752 秒 11.619 秒 15.57×
2 0.733 秒 10.511 秒 14.45×
3 0.462 秒 8.356 秒 18.20×

三次的波動不小。我沒有固定 Windows 電源模式、暫停所有背景程式或控制 CPU 溫度,連跑時也可能命中 Windows 檔案快取。這只能算同一台開發機上的快速實測,43.55 倍與 15.57 倍都不是穩定常數。

我另外把相容模式單獨計時,關掉 attention mask,並用同一批資料和 Hugging Face 的 encode_batch_fast 比較。三輪中位數是 0.431 秒對 6.046 秒,這組呼叫快 14.04 倍。兩邊輸出契約不同:HFCompat.__call__() 回傳 Python input_idsencode_batch_fast() 回傳 Encoding 物件。因此 14.04 倍只描述這次遷移寫法,不能當成所有相容模式的固定收益;我只對前 128 份樣本逐筆驗證輸出一致。

三種測試路徑的結果差很多:

  • 這組相容層呼叫約 14 倍,但輸出契約並非完全相同。
  • 記憶體內原生路徑由我用套件 CLI 量到約 40~50 倍。
  • 真正走 encode_files() 並計入磁碟 I/O,中位數約 16 倍。

「1000 倍」描述的是特定硬體、tokenizer、語料與 API 路徑的組合。它不是安裝套件後每個呼叫都會自動拿到的固定倍率。

速度差在哪裡

Hugging Face Tokenizers 本來就是多執行緒 Rust 程式。GigaToken 還能拉開距離,靠的不是把 Python 改寫成 Rust 這麼簡單。

依照 專案作者的說明,主要差異有三塊:

  1. 它替常見 tokenizer 寫了 SIMD pretokenizer,減少 regex、分支與資料搬運的成本。
  2. 它會快取常見 pretoken 的編碼結果;同一批語料反覆出現的字詞,不必每次重做 BPE merge。
  3. 原生資料結構與 encode_files() 能減少 Python 物件往返;走串流模式時,Rust 也負責讀檔與切文件。

這些設計都可能影響相容模式與原生模式的差距,但我的兩組呼叫連輸出型別都不同,不能用單次跑分把 14 倍與 43 倍的差額全算在 Python list 上。

不同 tokenizer 的差距也很大。官方在 Ryzen 7 9800X3D 測 GPT-2 是 106 倍,Gemma 4 只有 18 倍,Gemma 3 則是 13 倍。首頁那筆 989 倍則讓 GigaToken 跑完整 11.9 GB,Hugging Face 只跑前 100 MB,再用 throughput 換算;這適合看穩態吞吐,不代表兩邊真的在同一輪花完相同工作量。專案也把 SentencePiece 效能列為 known issue,坦白 Windows 測得不多、目前偏好 WSL。只看首頁最大的數字,會漏掉這些條件。

你的 API 服務不一定會快 43 倍

Tokenizer 在大型語料前處理、模型訓練、離線 embedding 或索引建置裡可能是主角。一般聊天 API 則常常只占很小一段時間,網路和模型推論才是大頭。

假設 tokenization 原本只占一次請求的 10%,就算那段快 43 倍,整體也只有約 1.11 倍:

1
2
3
4
5
6
def total_speedup(tokenizer_share: float, tokenizer_speedup: float) -> float:
remaining = 1 - tokenizer_share
return 1 / (remaining + tokenizer_share / tokenizer_speedup)


print(total_speedup(0.10, 43)) # 1.108...

真正值得換的場景,我會看三個條件:

  • CPU profile 已經證明 tokenization 占了可觀時間。
  • 資料量與執行頻率夠大,省下的 tokenization 時間能抵過遷移成本。
  • 團隊願意替實際 tokenizer 建 golden sample,升級時逐筆驗 token ID。

最後一條不能省。Tokenizer 輸出只要差一個 ID,後面的訓練資料、chunk 邊界或 token 計費都可能一起變。這次 CLI 原生路徑的 8,783 份文件全部通過驗證,我才把速度數字留下來;相容模式目前只驗了 128 份樣本,若真的遷移還要補全量 golden test。

我會不會用

會,但範圍很窄。

下次要做大型語料清理或批次索引,我會先拿 GigaToken 跑一份真實資料,再決定要不要改成原生介面。一般 Web API 專案沒有 profile 證據,我不會為了首頁的 1000 倍重構 tokenizer 層。

我原本想拆穿一個誇張跑分,實測後的結論反而比較尷尬:1000 倍沒有出現在我的筆電上,記憶體內 43 倍、含讀檔約 16 倍,兩個數字都大到不能當行銷話術略過。這個工具值得測,README 還不值得盲信。