Prhub

#54449 [Perf][Rust Frontend] Count the tokenizer vocabulary once at construction

原始 PR 作者 FeathBow 合并时间 2026-09-01 09:25 文件变更 1 提交数 3 评论 8 代码增减 +21 / -4

执行摘要

分词器词表大小改构造期缓存,大词表每请求省约 112 ms

PR body 明确指出:HuggingFaceTokenizer::vocab_size() 通过调用 tokenizers 后端的 get_vocab_size(true) 计数,而该调用每次都会构建 merged base-plus-added vocabulary map 来统计长度;Rust 前端在每次请求的 TextRequestProcessor::prepare 中都用 TextBackend::tokenizer_vocab_size() 填充 SamplingLimits,在 262k 词表(Gemma 4)上每请求要花数十毫秒 host time,且发生在 prompt 到达引擎之前。作者强调该计数在构造后从不变化,因此值得在装载时算一次并缓存。

值得快速精读,尤其关注两点:一是 benchmark 方法(black_box、多进程取中位数与 p90),二是"把不可变统计量从热路径移到构造期"的通用优化思路。单文件、低风险、语义不变,适合作为 Rust 前端延迟优化的参考样例。

讨论亮点

维护者 BugenZhao 触发 Codex 自动审查,结论为 Didn't find any major issues;两次 /ci run 的 Buildkite CI(#86429、#86456)均通过,BugenZhao 以 Thanks @FeathBow 批准合入。全程没有出现设计争议或未解决疑虑,唯一被反复确认的是缓存值与后端原值语义一致、无需模型级评估。

实现拆解

  1. 数据结构扩展:在 rust/src/tokenizer/src/hf.rs 的 HuggingFaceTokenizer 结构体中新增 vocab_size: usize 字段,与已有 special_token_ids、added_vocab 一样在构造期确定、之后只读。
  2. HF 后端缓存:from_hf_backend 在装载时调用一次 tokenizer.get_vocab_size(true) 并写入字段,替换原先 vocab_size() 每次调用都向后端查询的行为;返回的整数与后端原值完全一致。
  3. fastokens 后端缓存:from_fastokens_backend 同样在构造时调用一次 tokenizer.vocab_size() 缓存;该后端原本开销就很小,此处缓存主要为了统一两种后端行为、消除热路径上的后端分发。
  4. 查询方法简化:Tokenizer trait 的 vocab_size(&self) 实现从 match &self.backend 分发改为直接返回 self.vocab_size,热路径只剩一次字段读取。
  5. 测试配套:新增 crate 级单元测试 hf_vocab_size_counts_added_tokens,构造含 <|im_end|> 特殊 token 的 tokenizer.json,断言经 HF 后端加载后 wrapper 的 vocab_size() 等于 get_vocab_size(true)(保证 added tokens 计入)。vllm-tokenizer 63 个测试全过,下游 vllm-text 89 通过 1 跳过与基线一致,clippy 无警告。
  6. 配套改动:无配置 / schema / 部署变更;维护者通过 /ci run 触发两次 Buildkite CI(#86429、#86456)并全部通过。commit 2 由 BugenZhao 补充了 HF 缓存语义注释(说明为何要物化统计)。
文件 模块 状态 重要度
rust/src/tokenizer/src/hf.rs 分词器 modified 6.44

关键符号

from_hf_backend from_fastokens_backend vocab_size hf_vocab_size_counts_added_tokens

分析完成后,这里会展示 LLM 生成的相对完整源码片段和详细注释。

评论区精华

自动审查与 CI 验证 other

维护者 BugenZhao 通过 `@codex review` 触发 Codex 自动审查,Codex 对提交 `699983f` 未发现主要问题(`Didn't find any major issues`);随后两次 `/ci run` 分别触发 Buildkite CI #86429 与 #86456。

结论:无阻塞性意见,CI 通过后 BugenZhao 以 `Thanks @FeathBow` 批准合入。 · 已解决

风险与影响

  1. 语义风险:缓存成立的前提是构造后词表不再变化。vLLM 运行时不会向 tokenizers 后端注册新的 added tokens,因此当前安全;若未来支持运行时动态修改词表,需要同步失效该缓存。
  2. 覆盖风险:新增测试只覆盖 HF 后端路径;fastokens 后端缓存没有专项测试,不过其值直接来自后端同一 vocab_size() 方法,且该后端原有开销可忽略,实际回归风险很低。
  3. 回归风险:vocab_size() 从后端调用退化为字段读取,涉及面仅一个文件;下游 vllm-text 89 个用例与基线一致,未发现契约变化。
  4. 性能影响:装载阶段多付一次统计(5.0–5.4 秒加载时间内的毫秒级增量),换来每请求约 112 ms 的 host time 节省,净收益显著。
  1. 用户侧:使用 Rust 前端 + HF tokenizers 后端的大型词表模型(如 Gemma 4 262k)每请求首 token 前延迟显著下降;小型词表模型与 fastokens 路径基本无感。
  2. 系统侧:释放每请求固定 CPU 开销,对高 QPS serving 场景有稳定收益,并缓解 #50718 一类 Rust 前端 first-token 延迟问题的其中一项确定来源。
  3. 团队侧:确立了"构造期缓存不可变词表统计"的模式,后续其他每请求重复调用后端昂贵查询的地方可仿照;commit 2 补充的注释降低了后人误删缓存的风险。
缓存语义依赖构造后词表不变 fastokens 路径无新增专项测试

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论