AREX Feed Article
Hugging Face 发布 tokenizers v1 候选版:自测单线程 CPU 编码快 3~30 倍,开源 tokbench 供复测
Hugging Face 于 9 月 21 日在宣布,tokenizers v1 的发布候选版(release candidate)已上架 crates.io。 显示当前最新预发布版本为 1.0.0-rc.2、创建于同日;显示稳定版仍为 0.23.2,这个自 2019 年 8 月上架的 crate 累计下载已超过 3,300 万次。这轮更新把重点压在性能上:在 Apple M4 Max 上用单线程编码十个模型家族的文本,v1 比 v0.23 快 3~30 倍,低端是 t5-base,高端是 gpt2;八个工作线程下的扩展效率达到线性的 76%;v1 产出的 token ID 与 v0.23 完全一致。
与公告一同开源的还有基准仓库 ,附带在读者自己硬件上重跑全部基准的命令。博客同时致谢 gigatoken、tiktoken-rs、kitoken 等七个生态项目,以及 IBM、NVIDIA 和 ExecuTorch 团队提交的补丁与跨硬件测试。上述性能数字均为 Hugging Face 单方自测口径。
bitcannon、词缓存与重写的合并循环:快从哪里来
博客把分词管线拆成四个阶段:归一化处理大小写和 Unicode 形式,预分词把文本切成更小的预-token(pre-token),模型阶段把每个预-token 变成词表里的 ID,后处理补上模型需要的特殊 token。受测的十个模型家族中,八个在模型阶段使用字节对编码(BPE):从预-token 的字节出发,反复合并排名最高的相邻对,直到无可合并,合并从不跨过预-token 边界;其余两个家族分别用 WordPiece 和 Unigram。
这轮改动集中在模型阶段,但四个阶段都有对应改动。博客列出的关键变更:
- workspace 拆分:单一 crate 拆成 workspace,tk-encode 是必需的运行时,tk-serialize、tk-convert、tk-train 只在应用用到时才链接;
- bitcannon:BPE 原本靠正则引擎把文本切成预-token,而这条切分规则是模型自带的固定参数、运行时不变,因此可以手写等价函数。bitcannon 把输入字节看成并行的比特流,用 SIMD(单指令多数据)指令做布尔运算找出边界,一次寄存器操作判定 64 字节;博客称这与 Parabix、simdjson 的思路同源(此前先上线的有限状态机方案被替换,PR #2201、#2317);
- 合并循环重写:旧实现对每次调用分配新内存、为每个预-token 新建优先队列;v1 复用调用方的临时缓冲区、去掉这些重复分配,把符号放进扁平数组、串成侵入式双向链表,一次合并只更新两个索引而不搬数据。每个候选对打包成一个 64 位值、合并排名放在高位,比较候选变成比较两个整数,「此处不合并」编码为最大值,循环不用分支就能找到下一个合并点;
- 词缓存(word cache):线程本地的备忘录,把预-token 的字节映射到最终 ID(PR #2262),重复词只合并一次,新词出现时才会未命中;
- 原生并行(PR #2365):同一个分词器实例可被多线程同时调用,每个线程从自己的子池取临时缓冲区(scratch buffer)和词缓存,不再在单把锁上排队。
博客同时写明了两处边界:bitcannon 依赖认出切分模式,RC 版覆盖 GPT-2、cl100k、o200k、Tekken 和 DeepSeek 五种,模式不在名单里的分词器继续走正则路径、拿不到这份加速。博客说这就是各模型收益差异如此之大的原因;词缓存对重复预-token 多的输入收益最大,重复少的输入可能只付出查询成本、换不到多少命中。
tokbench 的测量规则与「暖缓存」陷阱
博客用一张规则表约束引擎对比:所有引擎跑同一条计时循环;词表加载单独计时、不混进编码;对输出 ID 做 FNV-1a 哈希校验,与基线不一致就不进排名;中位数只统计每个引擎都跑通并校验过的单元;每轮重复起一个新进程;工作线程钉在八个独立物理核上、不用 SMT(同时多线程)兄弟线程;另用独立的 Jobs 度量主机间差异。
博客还专门处理了一个基准陷阱:反复编码同一份文档,和编码一串不同文档,测的不是同一种负载:前者吃满缓存,后者边处理新输入边保留已见过的预-token,两种情况有时都被叫作「warm」。这次头条数字用不同文档,且整个语料大到装不进缓存。博客的提醒是:分词基准应当写明自己测的是哪种负载,因为这个选择足以主导结果。
另给出一组不同条件的自测数字:Apple M3 Max、单线程、warm、gpt2 与 llama-3 共 16 个单元,新编码路径(表内代号 pipeline)中位吞吐 90 MB/s,tokenizers 0.23.1 参照实现为 8 MB/s,表中倍数为 10.7 倍;同一张表里 wordchipper 和 fastokens 为 50 MB/s,tiktoken 27,kitoken 25,tokie 21。README 另用一组复现率实验量化缓存的收益边界:带预-token 缓存的引擎把重复变成吞吐,真实英文语料的预-token 重复率是 8.3 倍,中文只有 1.31 倍;在这个实验里,gigatoken 在英文上领先 v1 路径 1.26 倍,中文上则反过来、以 90 MB/s 对 112 MB/s 落后。README 还记下一笔教训:把预热数据混进计时切片,曾把 gigatoken 高估 256 倍。语料公开在 ,code-mixed 和 agentic-traces 两类不可再分发、仅内部使用;README 自述这个仓库大量代码由 AI agent 写成,欢迎为公平性提交补丁。
兼容前提:同样的调用、同样的 ID
博客把 v1 的目标写成一句话:保住输出、API、词表和合并排名,改进其余一切可以改进的东西。库不特化 BPE,v0.23 能加载的 v1 都能加载。安装命令是 cargo add tokenizers --pre;训练藏在默认开启的 feature 里、连带一个 C++ 依赖,只要编码可以按博客给出的命令关掉默认 feature 只留 http。博客示例加载 deepseek-ai/DeepSeek-V4-Flash 的分词器,编码 "The tokenizer is no longer the bottleneck." 得到 ID 序列 [671, 17840, 9160, 344, 1119, 5827, 270, 111127, 16];批量编码走 encode_batch,正是多核扩展曲线所测的入口。
博客提醒使用者:Python 绑定包的是同一份 Rust 代码,但每次调用有额外开销,文中所有测量都不包含这部分。
致谢名单:七个生态项目与 IBM、NVIDIA、ExecuTorch 团队
博客把这次重构归功于生态,点名了 gigatoken、tiktoken(crates.io 上的 tiktoken-rs)、kitoken、tokie、fastokens、wordchipper 和 npm 上的 ai-tokenizer 七个项目,并写道文中的几个点子是别的项目先证明值得尝试、才进入 tokenizers。博客也承认,重构前 tokenizers 的性能远低于它本可以达到的水平,向它贡献代码「可能显得不值得」——这次重构是想表明这个库值得贡献。平台支持方面,IBM、NVIDIA 和 ExecuTorch 团队提交补丁、协助在大量硬件上测试。
离 1.0.0 还差什么
RC 清单里除前述改动,还有 FlatCache、MPHF RankStore、增量合并与 BucketVocabStore(PR #2190、#2188),可复用的模型内存(#2175、#2183),后处理暴露为 STAGE_POST 阶段(#2182),单次调用处理一批预-token(#2304),以及更快的解码:解码字节直接写进可复用缓冲、省掉中间字符串、加速 token 查表、支持缓冲流式输出、批量并行解码;另有 role_to_token 支持(#2343)和 Node.js 绑定(#2281)。
按博客列出的计划,1.0.0 前还要做:训练校验改用 tk-encode,让训练和推理无法产出不同的分词结果;offsets 和 masks 改为按需计算;重写 normalizer;基于 atomnorm 的 bitnorm(#2209);spm 预编译;简化 Python 绑定(减少锁、包装类型和手写分发,保留子类化、序列化、自定义解码器与可变行为,并支持 free-threaded CPython);给 ExecuTorch 和 llama.cpp 出仅推理的 C/C++ 绑定,JVM、Swift、Go 绑定可能跟上。1.0.0 之后,团队计划探索 tok-devices——把 GPU 编码和批量解码做进可选组件、文本和 token ID 都留在设备上,这一项还需原型验证和测量。
下一步的顺序写在博客里:先把更多模型家族迁上新合并循环(1.0.0 之前);候选版稳定后,再把这套改进带进 transformers 和依赖 tokenizers 的其余生态。博客末尾说明,这篇由 tokbench 结果生成的文章(源文件托管在 )将随支持范围扩大持续更新。