标记器 v1:编码、解码与性能缩放深度解析
作者: Arthur Zucker (ArthurZ), Simon Brandeis (sbrandeis), Luc Georges (mcpotato), Lysandre (lysandre)

发布日期: 2026年9月21日
长期以来,标记器(Tokenizer)并非机器学习工作流中的计算瓶颈。相较于模型训练和推理中繁重的矩阵运算,文本分词在算力消耗上显得微不足道。然而,随着模型迭代加速以及数据规模呈指数级增长,这一平衡正在被打破。在大规模数据集训练、高并发请求服务或处理超长上下文输入的场景下,标记器可能成为限制整体吞吐量的关键因素,导致 GPU 因等待 CPU 完成分词而处于闲置状态。
为解决这一问题,Hugging Face 团队重点优化了即将发布的 tokenizers v1 版本。核心设计理念是让分词过程变得轻量且具备线性扩展能力——确保 GPU 永远不会因为等待 CPU 的分词结果而停滞。
本文旨在深入剖析 v1 版本相较于 v0.23 的性能提升来源。这项工作的推进得益于开源生态的共同努力,包括 gigatoken, tiktoken, kitoken, tokie, fastokens, wordchipper 和 ai-tokenizer 等库的贡献。此外,IBM、NVIDIA 和 ExecuTorch 团队也提供了补丁支持,并协助在多种硬件平台上进行测试以扩大兼容性。
基准测试结果概览
我们使用 tokbench 仓库对标记器 v1 发布候选版与其他广泛使用的替代方案进行了对比测试。评估维度涵盖模型兼容性、语言支持、延迟、解码吞吐量、内存堆栈占用以及二进制文件大小。
v1 版本的技术架构与改进
v1 版本的核心目标是保持向后兼容性:它生成的 Token ID 与 v0.23 完全一致,API、词汇表及合并规则均保持不变,仅在底层实现上进行全面重构以提升速度。该库仍属于通用标记器家族,而非仅针对 BPE 算法,因此能够加载 v0.23 支持的所有格式。
标记器将文本转换为模型可理解的整数列表,这一过程主要包含四个阶段:
- 标准化(Normalization): 对原始文本进行小写转换或 Unicode 标准化等操作。
- 预分词(Pre-tokenization): 将文本分割为更小的单元,即预 Token。
- 模型映射(Model): 将每个预 Token 转换为最终的 Token ID。这是计算最密集的阶段。
- 后处理(Post-processing): 添加模型所需的特殊 Token。
在测试的十个模型家族中,八个采用字节对编码(BPE)。BPE 算法从预 Token 的字节开始,反复合并排名最高的相邻对,直到无法再合并为止。由于合并规则在训练时固定,相同的文本始终产生相同的 ID。其余两个模型家族分别使用 WordPiece 和 Unigram 算法。
v1 版本在每个阶段都引入了关键优化,具体变化如下表所示:
| 技术变更 | 作用描述 |
|---|---|
| 工作区拆分 | 单一 crate 拆分为 tk-encode(运行时必需)、tk-serialize、tk-convert 和 tk-train。应用程序仅链接所需模块,减少二进制体积。 |
| 无分配模式 | 合并工作组存在于调用者拥有的 scratch 缓冲区中;主循环不再频繁触发内存分配器。 |
| Bitcannon(比特炮) | 分割逻辑从正则表达式引擎转变为基于 SIMD 指令的位流布尔运算,显著提升 UTF-8 文本处理速度。 |
| 合并循环重写 | 被合并的部分在预分配缓冲区内形成侵入式双向链表,合并操作仅需更新索引而非移动数据。 |
| 单词缓存 | 建立线程本地的备忘录,缓存从预 Token 字节到最终 ID 的映射,避免重复单词的重新合并。 |
| 原生并行性 | 共享标记器可从多线程同时编码。每个线程拥有独立的 scratch 缓冲区和单词缓存,消除了单锁竞争(参见 Issue #2365)。 |
1. 分割优化:用 Bitcannon 取代 Regex
BPE 模型通常依赖正则表达式将输入文本分割为预 Token。由于这些正则表达式是模型的固定参数,且在运行时从不改变,因此无需通用的正则引擎进行动态解释。
v1 引入的 Bitcannon 技术通过手写函数替代了正则引擎。它利用现代 CPU 的 SIMD(单指令多数据)指令集,将输入字节视为平行比特流。边界识别通过整个寄存器的布尔运算完成,而非逐字符扫描。这种机制使得每个寄存器操作能处理 64 个字节,其原理类似于 Parabix 文本处理和 simdjson 的 JSON 解析。
需要注意的是,Bitcannon 的收益取决于能否识别出固定的分割模式。对于大多数字节级 BPE 模型,该技术效果显著;而对于缺乏明确模式的标记器,系统会回退至原有的正则路径,此时不会获得加速。
2. 单词缓存机制
真实世界的文本中包含大量重复词汇。鉴于 BPE 对给定预 Token 总是生成相同的 ID,v1 实现了线程本地缓存,将预 Token 的字节映射到其对应的 Token ID。后续遇到相同输入时,可直接跳过合并过程。
随着输入长度增加,唯一预 Token 的数量增长速度慢于总字数,这意味着重复单词占比上升,缓存命中率提高。这也解释了为何在处理长文档时,性能提升尤为明显。
tokbench measure prefix-sharing \
--engine pipeline \
--engine hf-tokenizers \
--compare-to pipeline-no-cache \
--corpus agentic_swe
当输入包含大量重复预 Token 时,缓存优势最大;若重复率极低,搜索缓存本身的开销可能会抵消部分收益。
3. 合并循环的重构
BPE 合并循环的主要成本在于频繁的内存分配和优先级队列维护。旧实现在每次调用时都会分配新内存并为每个预 Token 构建新的队列。
v1 采取了以下措施:
- 复用缓冲区: 重用调用者提供的 scratch 缓冲区,消除重复分配。
- 扁平数组存储: 符号存储在平面数组中,通过位置索引链接相邻符号,降低合并时的更新成本。
- 批量处理: 单次模型调用处理一批预 Token,而非逐个处理。
- 位打包: 候选对被封装为单个 64 位值,高位存储排名。“无合并”对应最大值,使循环能在无分支预测失败的情况下找到下一个合并点。
基准测试方法论
微小的测试设计差异可能导致巨大的性能偏差。为确保不同引擎间比较的一致性,我们遵循以下严格规则:
| 规则 | 原因 |
|---|---|
| 统一计时循环 | 所有引擎在同一代码结构中运行,杜绝“快速通道”作弊。 |
| 排除加载时间 | 词汇表加载单独计时,不计入编码耗时。 |
| ID 哈希验证 | 输出 ID 必须通过 FNV-1a 哈希校验,确保与基线完全一致。 |
| 完整进程扫描 | 每次重复测试启动新进程,保留所有单元格数据。 |
| 物理核心绑定 | 工作线程固定在 8 个不同的物理核心上,避免 SMT 兄弟线程干扰。 |
| 独立作业测量 | 单独测量主机间的方差,确保结果稳定性。 |
值得注意的是,“热缓存”场景(重复编码同一文档)与“冷缓存”场景(编码一系列不同文档)表现迥异。我们的头条结果基于后者,即完整的文档太大无法全部驻留缓存,这更能反映真实生产环境中新数据的处理性能。
性能提升幅度
在 Apple M4 Max 芯片的单线程测试中,v1 版本的编码速度比 v0.23 快 3 到 30 倍。低端增益出现在 T5 Base 模型,高端增益则体现在 GPT-2 模型。在 8 个工作线程下,v1 实现了 76% 的线性扩展效率。在所有测试变体中,v1 产生的 Token ID 与现有库完全一致。
整体性能的飞跃源于多项优化的协同作用:手写分割器取代正则引擎、单词缓存避免重复合并、无分配合并循环、以及批量化模型调用。每一项优化都减少了流水线特定阶段的计算负载。
接下来的重点是扩展对新模型家族的支持。在 1.0.0 正式发布前,我们将把更多模型迁移至新的合并循环架构。随后,优化成果将被整合进 transformers 库及其他依赖 tokenizers 的生态组件中。
如何获取与安装
v1 的发布候选版已上线 crates.io。用户无需修改 API 调用代码,只需更换安装源即可体验性能提升。
标准安装命令:
cargo add tokenizers --pre
默认情况下,训练功能会引入 C++ 依赖。如果仅需编码功能,可通过关闭默认特性来精简依赖:
cargo add tokenizers --pre --no-default-features --features http
Rust 代码示例:
use tokenizers::tokenizer::{Result, Tokenizer};
fn main() -> Result<()> {
let tokenizer = Tokenizer::from_pretrained("deepseek-ai/DeepSeek-V4-Flash", None)?;
let encoding = tokenizer.encode("The tokenizer is no longer the bottleneck.", false)?;
println!("{:?}", encoding.get_ids());
Ok(())
}
对于批量处理,encode_batch 方法可利用多核优势,这也是上述缩放测试所衡量的接口。
let encodings = tokenizer.encode_batch(documents, false)?;
注:Python 绑定封装了相同的 Rust 代码,但会增加每次调用的开销,本文中的数据未包含 Python 绑定的额外损耗。
v1 开发路线图
已完成(发布候选版)
- 工作区拆分: 划分为
tk-encode,tk-serialize,tk-convert,tk-train。 - Bitcannon: 替换 GPT-2, cl100k, o200k, Tekken 和 DeepSeek 等模型的正则分割逻辑(Issue #2201, #2317)。
- WordCache: 复用已处理预 Token 的 ID(Issue #2262, commit af5a3e3)。
- 更快的查找与合并结构: 引入 FlatCache, MPHF RankStore, 增量合并及 BucketVocabStore(Issue #2190, #2188)。
- 可复用的模型内存: 将临时状态移至 scratch 缓冲区,避免每次调用分配内存(Issue #2175, #2183)。
- 管道后处理: 暴露
STAGE_POST作为独立阶段(Issue #2182)。 - 批量模型调用: 单次调用处理多个预 Token 跨度(Issue #2304)。
- 更快的解码: 直接写入可复用缓冲区,避免中间字符串拷贝,支持并行批量解码。
- 其他: 支持
role_to_token(Issue #2343),新增 Node.js 绑定(Issue #2281)。
1.0.0 目标
- 统一编码实现: 训练验证期间使用
tk-encode,确保训练与推理的分词结果绝对一致。 - 可选偏移量与掩码: 仅在请求时计算元数据,保持纯 ID 路径的高效。
- 标准化器重构: 优化 Normalizers 性能。
- Bitnorm 支持: 基于 atomnorm 构建(Issue #2209)。
- SPM 预编译: SentencePiece 模型预处理优化。
- 简化 Python 绑定: 减少锁竞争和包装类型开销,同时保留子类化、序列化及自由线程 CPython 支持。
- C/C++ 推理绑定: 专为 ExecuTorch 和 llama.cpp 提供,未来计划支持 JVM, Swift 和 Go。
1.0.0 之后展望
- tok-devices: 探索 GPU 编码与批量解码。设想将文本和 Token ID 保留在设备端,解码器上传词汇表后并行计算输出位置并在 GPU 上收集字节。此功能将作为可选组件,适用于大批量数据处理场景,目前仍处于原型设计与测量阶段。





