Rust Tokenizer跑出24GB/s——比HuggingFace快1000倍
Tokenization 从来不是 LLM 训练的瓶颈。至少大家是这么认为的。
GPU 吃掉了 99% 的训练时间,谁会在意 CPU 上的分词环节?HuggingFace 的 tokenizers 库是 Rust 写的,OpenAI 的 tiktoken 也是 Rust 写的——多线程,够快了。
直到有人真的 profile 了一下。
Marcel Rød,Stanford 三年级博士生,用 8 个月时间重写了 BPE tokenization 的核心路径。结果:GPT-2 的分词速度从 24.8 MB/s 飙到 24.53 GB/s——989 倍。在 AMD EPYC 9565(144 核)上,11.9 GB 的 OpenWebText 数据集只需 0.49 秒就能完成分词。按照这个速度,「整个 Common Crawl」(130 万亿 token)只需不到 6.5 小时。
项目叫 Gigatoken,Rust 实现,MIT 协议,2.9K stars。2026 年 7 月 22 日登上 Hacker News 首页。
● ● ●
为什么之前的 Tokenizer 这么慢?
不是正则引擎不够快,是把整个预分词都外包给了它。
HuggingFace 的 tokenizers 和 OpenAI 的 tiktoken 都依赖正则引擎来做预分词(pretokenization)——把文本切成一个个"词单元",然后对每个单元做 BPE 合并。正则引擎的问题在于:它是通用的模式匹配引擎,不知道自己在做什么。每次匹配都要走完整的状态机,而且是逐字节的。
Gigatoken 的做法完全不同。核心思路是三层管线:
Gigatoken架构
1. SIMD 预分词取代正则引擎
不调 regex 库。直接手写 SIMD:
一个 256 字节的查找表(LUT),看一眼首字节就知道这个 token 是字母/数字/空格/符号,O(1) 分发到对应的扫描函数 扫描函数用 SWAR(SIMD Within A Register)技术——一个 u64算术操作同时检查 8 个字节是否是字母((b | 0x20) - b'a'的结果小于 26),比逐字节 + 分支快得多AVX-512/NEON 批量分类:一次处理 64 字节,用 SIMD 比较指令生成掩码,然后提取边界位置
在 M4 Max 上,预分词这一个环节就从 ~47 MiB/s(fancy-regex)优化到了单线程 ~950 MiB/s。这是 20 倍的提升——在还没进入 BPE 合并之前。
2. 缓存层级:99.4% 的预分词不用重新计算
这是 Gigatoken 最巧妙的设计。
自然语言里绝大多数"词"是重复的——"the"、"and"、"of"在文本中反复出现。传统 tokenizer 每次遇到都要重新走一遍 BPE 合并。Gigatoken 在预分词和 BPE 合并之间插了一层缓存:
一个自定义的开放寻址哈希表,2 MiB 大页对齐,64 MB 容量 键是预分词的前 15 个字节直接打包到 u128(一次pcmpeqb比较),值是编码后的 token ID 序列表容量 64 MB 远超 L2/L3,所以用了两级预取管线——phase B 预取到 L2,emit 循环预取到 L1 在 OpenWebText 上命中率 99.4%
这意味着一亿个词里,只有 60 万个真正需要走 BPE 合并。其余的直接从缓存表里查出结果,一次加载。
那 0.6% 的未命中怎么办?Gigatoken 也为这个路径做了极致优化。
3. PairRankTable:两级扁平化合并查找
BPE 合并的核心是一个查找操作:给定两个 token ID (a, b),它们能合并成什么?传统做法是用一个 HashMap,键为 (u32, u32) 对、值为 u32——每次查找两次哈希计算 + 探测。
Gigatoken 替换成了两层结构:
- Dense 层
:一个 2048×2048 的 16 MiB 网格,覆盖所有初始字节 token 对。合并循环里约一半的查找落在这一层——一个移位指令 + 一次 L3 加载,无哈希、无探测。 - Flat 层
:剩余合并对放在一个自定义开放寻址表中,每个槽 8 字节压缩( (key, merged_id)打包为一个 u64),一次乘法 + 一次加载 + 一次比较。因为负载因子不超过 50%,miss 通常停在第一个空槽。
再加上针对短序列的栈上双向链表合并(零堆分配),整个 miss 路径也被压缩到了极致。
● ● ●
实际有多快?
在 OpenWebText 11.9 GB 数据集上的编码吞吐(GPT-2 tokenizer):
| 24.53 GB/s | 989× | |||
| 8.79 GB/s | 1,268× | |||
| 6.27 GB/s | 106× |
注意:HF tokenizers 和 tiktoken 本身就是多线程 Rust。Gigatoken 快在算法,不是快在语言。
覆盖范围也远超预期——支持 30+ 种主流 tokenizer:GPT-2/4、Llama 3/4、Qwen 2-Qwen3.6、DeepSeek V3/R1/V4、GLM 4/5、Gemma 1-4、Phi-3/4、Kimi K2 等等。几乎涵盖所有常见模型。
● ● ●
集成到现有管线
Gigatoken 提供了三层接口:
最快:Gigatoken API
import gigatoken as gt tokenizer = gt.Tokenizer("Qwen/Qwen3-8B") tokens = tokenizer.encode_files(gt.TextFileSource(["data.txt"]))Rust 直接 mmap 读文件,零 Python 开销,989 倍。
兼容模式:HF Tokenizers 替换
tokenizer = gt.Tokenizer(hf_tokenizer).as_hf() tokens = tokenizer.encode_batch(texts) # 200-300× 快一行代码切换,接口完全兼容。
tiktoken 替换
tokenizer = gt.Tokenizer(tiktokenizer).as_tiktoken()还提供 CLI 快速验证:
uvx gigatoken bench 'openai-community/gpt2' data.txt --validate● ● ●
这意味着什么?
Gigatoken 不是"又一个 tokenizer"——它刷新了我们对 tokenization 性能上限的认知。之前的共识是"tokenization 不是瓶颈,GPU 才是"。Gigatoken 证明:不是 tokenization 不够快,是从来没有人认真优化过它。
这有几个实际影响:
- 01训练数据预处理不再是瓶颈
。当分词比下载数据还快的时候,预处理管线可以大幅简化。 - 02本地推理的数据预处理会受益
。在边缘设备上,每一毫秒的 CPU 时间都很珍贵。 - 03DuckDB + tokenization 成为可能
。把 Gigatoken 嵌入 DuckDB 扩展,文本数据的读取→清洗→分词可以在 SQL 里一个查询完成,无需 Python 往返(我们在做这个:duckdb_gigatoken)。
● ● ●
已知限制
- SentencePiece 后端慢
:Gemma、Mistral 等 SentencePiece tokenizer 只快了 10-20 倍(vs BPE 的 500-1000 倍)。作者说低优先级——主要用于 Google 系模型。 - 单人项目
:100% 提交来自 Marcel Rød。代码质量极高(profiling 文档是教科书级的),但 bus factor 是 1。 - WordPiece 未实现
、Windows 测试少、无 file sinks。
但这些不影响它的核心价值:证明了 tokenization 的上限远高于我们认为的水平。
● ● ●
技术细节值得一看
Gigatoken 的源码不仅是工业级的 SIMD 优化示范,还是性能剖析驱动开发的教科书。仓库里有两份深度分析报告:
profiling/report.md:M4 Max 单线程 10 GB OWT 的 samply + xctrace CPU Counters 分析——精确到每条热指令的微架构归因 profiling/zen5_st_profile.md:Zen 5 的 perf冷/暖阶段分解——发现了一个 THP ordering bug(madvise 在 memset 之后调用,导致大页从未生效),修复后 +15.4%
每个优化都有 A/B 测试支持,每一步都精确测量。这种"先测量、再优化、后记录"的工程文化,比 Gigatoken 本身更值得学习。
Gigatoken 证明了:在 AI 基础设施里,没有哪个环节是"已经足够快"的。只是还没人认真量过。
试试 uvx gigatoken bench 'openai-community/gpt2' your_data.txt --validate,跑通的话告诉我你的加速比。
你之前留意过 tokenization 的性能吗?还是和我一样默认"这不是瓶颈"?