OpenAI 的 tiktoken 和 HuggingFace 的 tokenizers 都是 Rust 写的工业级分词库,速度都在每秒几十 MB 量级,但设计哲学几乎相反:tiktoken 是"一个算法做到底"的专一刀法,HF tokenizers 是"每个环节可插拔"的框架思维。选型或读源码之前,值得把两者的数据结构和边界一次看清。
tiktoken:合并表哈希 + 按 rank 贪心
tiktoken 的核心数据结构简单到可以默写:一张 HashMap<(u32, u32), u32>,key 是可合并的相邻 token id 对,value 是这对的 rank(合并优先级,就是训练时的合并顺序号)。词表本身是从 token id 到字节串的数组。.tiktoken 模型文件就是 base64 的"字节串 + rank"行列表,加载即哈希建表,没有训练能力,没有管线概念。
编码过程对应 BPE 的经典贪心:对预切分后的每个片段,反复找当前序列中 rank 最小的相邻对,合并,直到没有可合并对。找最小 rank 用线性扫是 $O(L^2)$,tiktoken 的优化是把所有相邻对进二叉堆,合并后只有新产生的左右邻居需要重新入堆,复杂度压到 $O(L\log L)$。预切分正则(cl100k 那套按语言分支的模式)把文本切成片段后,再有一层"片段 → token 序列"的缓存:相同片段(常见词、固定代码模式)第二次起 $O(1)$ 命中——分词处在每个请求的热路径上,这层缓存是首 token 延迟的胜负手。
字节处理走 GPT-2 的 bytes_to_unicode 映射(3060 讲过),所以 tiktoken 的 token 序列在内存里始终是"可见字符",落盘成文本文件也不炸。整个库不暴露 offset、不管 padding、不做截断——它假设调用方只要 id 序列。这个极简是有代价的:想知道第 5 个 token 对应原文哪个区间,只能 decode 回来自己字符串对齐,不可靠。
HF tokenizers:五个环节的管线
HF tokenizers 把分词拆成五个 trait 环节:Normalizer(NFC、小写、去音标)→ PreTokenizer(正则切分、byte-level、按词切)→ Model(BPE / Unigram / WordPiece)→ PostProcessor(加 BOS/EOS、配对模板)→ Decoder(编码的逆操作)。每个环节独立可换,Tokenizer 对象把它们串起来,整个配置序列化进单个 tokenizer.json。
这套结构的价值在"全生命周期可复现":训练一个 BPE(BpeTrainer 直接吃语料迭代器)和加载一个训好的 BPE 是同一份 Model 代码;encode 返回的不只是 id,还有每个 token 在原文中的 offsets 区间——因为 PreTokenizer 和 Model 每个环节都记录字符偏移,NER、分词标注、highlight 这类需要原文对齐的任务直接受益。再加 truncation、padding、overflow 拆分,训练侧的数据预处理需求全覆盖。
代价是抽象层的厚度:各环节边界上的字符/字节转换(尤其 byte-level 的偏移换算)是 bug 高发区,不同组合的边界行为需要实测确认;对每个请求走一遍五个虚函数派发,单次编码的常数开销大于 tiktoken 的硬编码路径——吞吐量级相同,延迟敏感路径上 tiktoken 更占便宜。
边界行为差异实录
几个实测会踩的差异。其一,空字符串:tiktoken 编码空串返回空列表;HF 默认返回含 BOS 的列表(取决于 PostProcessor),批量接口里这个差异会让形状对不上。其二,非法 UTF-8:tiktoken 走字节天然免疫;HF 在 byte-level 模式下同样免疫,但纯字符模式会报 unk 或错误。其三,特殊 token:tiktoken 的 encode(..., allowed_special=) 要显式声明才放行 <|im_start|> 这类条目,默认把它们拆成普通文本——这是防注入设计;HF 对应物是 added_tokens 匹配,行为由 PostProcessor 决定,默认直接识别。同一段对话数据在两个库上能编出不同序列,迁移时必须逐条对拍。
训练侧只有 HF 可选(tiktoken 没有训练功能),而且 HF 的训练器就是生产配置:tokenizers.train 迭代语料,指定词表大小、预切分、归一化,产出 tokenizer.json 直接进 transformers.AutoTokenizer。tiktoken 的训练只有教学版(tiktoken/_educational.py),演示思路可以,生产数据跑不动。
性能与选型
两者吞吐都远超需求端(瓶颈永远在 GPU),选型看的是接口形状而不是 benchmark:需要 offsets、训练、多模型管理(tokenizer.json 单文件走仓库管线),选 HF tokenizers;只需要 OpenAI 模型的编码对齐(复现 API 行为、算 token 账单)、延迟最敏感,选 tiktoken。一个常见组合是混用:HF 管训练和数据侧,tiktoken 做线上计费的基准标尺——两个库对同一段文本编出不同 id 是正常的,词表不同,对拍时比的是 token 数而非 id 序列。
小结
tiktoken 把 BPE 的三个动作(预切分、rank 合并、缓存)压缩到最短路径,换来极致延迟和零依赖,代价是放弃训练与对齐能力;HF tokenizers 用五环节管线换来全生命周期覆盖和 offsets,代价是抽象复杂度和常数开销。这不是快慢之争,是"专一工具 vs 通用框架"的又一次实例——分词库的选择,本质是你在整条链路里愿意把复杂度放在哪一端。