Prhub

#25953 [perf] skip add_special_tokens=False kwarg on chat-template tokenize for slow tokenizers

原始 PR 作者 kpham-sgl 合并时间 2026-05-22 13:45 文件变更 1 提交数 1 评论 8 代码增减 +39 / -9

执行摘要

优化慢速 tokenizer 的 chat 模板编码性能

Kimi-K2.5 的 TikTokenTokenizer(is_fast=False)将任何 encode() 的额外参数视为慢速路径触发信号,导致 add_special_tokens=False 参数使其回退到 PreTrainedTokenizer.encode(),重新执行分词工作并将每个 token 经过字符串转换。对于 80k 提示词,该步骤耗时约 254ms,优化后可降至约 20ms,显著降低 TTFT。

值得精读。这是一个典型的性能优化实践:通过探测运行时行为而非硬编码条件,实现了通用性和正确性保障。其设计模式(探测-缓存-条件跳转)可复用于类似场景。

讨论亮点

Reviewer JustinTong0323 认可方案并指出修正 lint 后即可合并。Qiaolin-Yu 最初建议将探测逻辑封装为函数并缓存返回值,后意识到放在 __init__ 中即可,作者 kpham-sgl 采用了 @cached_property 的变体。最终代码将探测逻辑直接放在 __init__ 中,使用实例变量。

实现拆解

  1. 初始化时探测 tokenizer 行为:在 OpenAIServingChat.__init__ 中,调用 tokenizer.encode("") 并检查返回长度。若长度 > 0,则 tokenizer 会自动添加特殊 token(如 BOS),标记 _tokenizer_auto_adds_specials = True;否则标记为 False。异常时默认 True(保守行为)。
  2. 拆分 chat 模板编码:在 _apply_jinja_template 中,将 apply_chat_template(tokenize=True, ...) 拆分为两步:先 apply_chat_template(tokenize=False) 渲染文本,再 tokenizer.encode(rendered, **encode_kwargs) 进行编码。encode_kwargs 根据 _tokenizer_auto_adds_specials 决定是否传入 add_special_tokens=False
  3. 同步更新异常回退路径TemplateError 的 fallback 路径也使用相同拆分逻辑,确保一致性。
  4. 错误保护:对 encode("") 探测使用 try/except,异常时默认 True,保证向后兼容。
文件 模块 状态 重要度
python/sglang/srt/entrypoints/openai/serving_chat.py OpenAI 端点 modified 6.89

关键符号

__init__ _apply_jinja_template

关键源码片段

python/sglang/srt/entrypoints/openai/serving_chat.py core-logic

核心变更文件。修改了 `__init__` 和 `_apply_jinja_template` 方法,添加探测逻辑和拆分编码步骤。

# python/sglang/srt/entrypoints/openai/serving_chat.py
# 在 __init__ 中探测 tokenizer 行为
try:
    # 通过 encode("") 结果长度判断 tokenizer 是否自动添加特殊 token
    self._tokenizer_auto_adds_specials = (
        len(self.tokenizer_manager.tokenizer.encode("")) > 0
    )
except Exception:
    # 异常时保守默认 True,保持旧行为
    self._tokenizer_auto_adds_specials = True# 在 _apply_jinja_template 中,拆分编码步骤
# 根据探测结果决定是否传递 add_special_tokens=False
encode_kwargs = (
    {"add_special_tokens": False}
    if self._tokenizer_auto_adds_specials
    else {}
)
try:
    # 第一步:渲染 chat 模板为文本
    rendered_prompt = self.tokenizer_manager.tokenizer.apply_chat_template(
        openai_compatible_messages,
        tokenize=False, # 仅渲染,不编码
        add_generation_prompt=True,
        tools=tools,
        return_dict=False,
        **extra_template_kwargs,
    )
    # 第二步:对渲染后的文本进行编码,动态控制参数
    prompt_ids = self.tokenizer_manager.tokenizer.encode(
        rendered_prompt, **encode_kwargs
    )
except Exception as e:
    # 异常回退路径(如工具格式不匹配)同样采用拆分逻辑
    # (代码省略,结构相同)
    ...

评论区精华

探测逻辑应封装为函数并缓存 设计

Qiaolin-Yu 建议将 tokenizer 探测逻辑封装为函数并缓存返回值;后意识到放在 __init__ 中即可。

结论:最终采用实例变量,在 __init__ 中直接探测并赋值。 · 已解决

Base 分支合并后需重新审查 other

作者 kpham-sgl 提醒由于合并了 base 分支,拆分 `apply_chat_template` 和 `tokenizer.encode` 的变更已显现,请审查。

结论:Reviewer 已批准。 · 已解决

风险与影响

风险较低。探测逻辑由 try/except 保护,异常时默认 True(保留原行为)。拆分为两步调用后,apply_chat_template(tokenize=False)tokenizer.encode() 均在原 try/except 范围内,异常处理不变。仅对不自动添加特殊 token 的 tokenizer 跳过 add_special_tokens=False,对于其他 tokenizer(如 LLaMA 系列)行为完全一致。无测试文件变更,但 PR 作者计划进行手动验证。

直接影响 OpenAI 兼容的 /v1/chat/completions 端点的聊天模板编码阶段。对于 Kimi-K2.5 等慢速 tokenizer,编码速度提升约 10 倍,对于 80k 长提示词可节省约 234ms。对其他 tokenizer(包括快速 tokenizer)无影响。该优化仅作用于请求处理线程收到请求后的第一个步骤,不涉及推理核心,风险隔离良好。

缺少测试覆盖 核心路径变更

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论