Prhub

#23751 [3/N][Sync sglang-miles] TITO Support

原始 PR 作者 zyzshishui 合并时间 2026-06-04 09:45 文件变更 7 提交数 7 评论 11 代码增减 +217 / -9

执行摘要

新增 TITO 风格 chat:支持 pre-tokenized input_ids 和返回 prompt_token_ids

PR body 说明 Synced from https://github.com/sgl-project/sglang/commit/69018ba16e5d8ef56559d076066377c127fedb27 和 https://github.com/sgl-project/sglang/commit/3606aecea3215ece32fb774f0b8e2a7c780df741,目标是支持 TITO 风格的 chat 请求,具体包括:

1) 支持通过 input_ids 传入预分词 token,跳过 chat 模板;
2) 允许非流式 chat 响应返回 prompt_token_ids 和可选的 meta_info

建议精读 serving_chat.py_convert_to_internal_request_build_chat_response 的实现,了解请求转换管线中字段优先级和错误处理的权衡。protocol.py 的自定义序列化方式也值得参考。对于部署运维,需注意 skip_tokenizer_init 场景的兼容性。

讨论亮点
  1. content 默认值变更:guapisolo 提出将 contentNone 改为 "" 是否可接受,担心 chat 模板将 None 渲染为 "None"。JustinTong0323 表示赞同(+1)。最终该变更被接受。

  2. multimodal 路径优先级:JustinTong0323 指出在 is_multimodal=Trueinput_ids 路径会被 text="" 覆盖,导致模型收到空 prompt。作者随后调整了条件判断顺序,优先使用 request.input_ids

  3. batch 子请求字段传递:chatgpt-codex 报告 GenerateReqInput.__getitem__ 未复制 return_prompt_token_ids,导致批量请求中该标志恒为 False。JustinTong0323 建议向 EmbeddingReqInput 也添加该字段以避免使用 getattr。最终通过显式添加字段解决。

  4. skip_tokenizer_init 兼容性:chatgpt-codex 指出 input_ids 分支中调用了 tokenizer.decode,在 skip_tokenizer_init=True 部署下 tokenizer 可能为 None,导致崩溃。作者需要额外处理此情况(当前 patch 未体现,可能已修改或待后续)。

实现拆解

  1. 协议层扩展python/sglang/srt/entrypoints/openai/protocol.py):在 ChatCompletionRequest 中新增 input_idsOptional[List[int]])、return_prompt_token_idsbool)和 return_meta_infobool)字段;在 ChatCompletionResponseChoice 中新增 prompt_token_idsOptional[List[int]])和 meta_infoOptional[Dict[str, Any]])字段,并通过自定义 model_serializer 实现 None 字段自动排除,保持向后兼容。

  2. 请求转换与拒绝规则python/sglang/srt/entrypoints/openai/serving_chat.py):在 _convert_to_internal_request 中,首先检查 stream=True 时是否同时设置了 return_prompt_token_idsreturn_meta_info,若是则抛出 ValueError;在 _process_messages 中,当 request.input_ids 不为 None 时跳过 chat 模板,直接以 input_ids 作为 prompt_ids;在 prompt_kwargs 构建中优先使用 request.input_ids 路径(置于 is_multimodal 判断之前),确保多模态模型也能正确处理预分词输入;在 _build_chat_response 中根据 request 标记从 ret_item 提取对应字段并填入 choice 对象。

  3. 状态管理python/sglang/srt/managers/tokenizer_manager.py):在 ReqState 中新增 prompt_token_ids 字段用于暂存 tokenize 后的 prompt token IDs;在 generate_request_handle_batch_request 中,若 obj.return_prompt_token_ids 为 True 则将 tokenized_obj.input_ids 的副本存入 state;在 _handle_batch_output 中当 out_dict 非空且 state 有值时将 prompt_token_ids 加入输出字典。

  4. 内部数据结构python/sglang/srt/managers/io_struct.py):在 GenerateReqInputEmbeddingReqInput 中新增 return_prompt_token_ids 字段,并在各自的 __getitem__ 方法中同步传递,确保 n>1 或批处理场景下该标志不会丢失。

  5. 配套测试test_protocol.py 验证协议字段序列化;test_serving_chat.py 验证流式拒绝、input_ids 跳过模板、非流式响应的 prompt_token_ids 返回;test_io_struct.py 验证 getitem 正确保留标志。

文件 模块 状态 重要度
python/sglang/srt/entrypoints/openai/serving_chat.py 请求处理 modified 6.9
python/sglang/srt/entrypoints/openai/protocol.py 协议层 modified 5.99
python/sglang/srt/managers/tokenizer_manager.py 分词管理 modified 6.15
python/sglang/srt/managers/io_struct.py IO 结构 modified 5.31
test/registered/unit/entrypoints/openai/test_serving_chat.py 测试 modified 6.86
test/registered/unit/entrypoints/openai/test_protocol.py 测试 modified 5.71
test/registered/unit/managers/test_io_struct.py 测试 modified 4.75

关键符号

_convert_to_internal_request _process_messages _build_chat_response generate_request _handle_batch_request _handle_batch_output test_convert_to_internal_request_rejects_stream_return_prompt_token_ids test_chat_completion_tito_extensions test_prompt_token_ids_and_meta_info_serialization test_getitem_preserves_return_prompt_token_ids

关键源码片段

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

核心变更文件,实现 TITO 请求转换、流式拒绝、input_ids 优先级和多模态兼容逻辑。

# _convert_to_internal_request 方法中的流式限制和 input_ids 优先处理
if request.stream:
    if request.return_prompt_token_ids:
        raise ValueError(
            "return_prompt_token_ids is not supported with streaming. "
            "Please set stream=false when using return_prompt_token_ids=true."
        )
    if request.return_meta_info:
        raise ValueError(
            "return_meta_info is not supported with streaming. "
            "Please set stream=false when using return_meta_info=true."
        )is_multimodal = self.tokenizer_manager.model_config.is_multimodal
processed_messages = self._process_messages(request, is_multimodal)
sampling_params = request.to_sampling_params(
    stop=processed_messages.stop,
    model_generation_config=self.default_sampling_params,
    tool_call_constraint=processed_messages.tool_call_constraint,
)# 优先使用用户提供的 input_ids,避免多模态路径覆盖
if request.input_ids is not None:
    prompt_kwargs = {"input_ids": processed_messages.prompt_ids}
elif is_multimodal:
    prompt_kwargs = {"text": processed_messages.prompt}
else:
    if isinstance(processed_messages.prompt_ids, str):
        prompt_kwargs = {"text": processed_messages.prompt_ids}
    else:
        prompt_kwargs = {"input_ids": processed_messages.prompt_ids}
python/sglang/srt/entrypoints/openai/protocol.py core-logic

协议层定义新增字段和序列化逻辑,是 TITO 的数据契约基础。

class ChatCompletionResponseChoice(BaseModel):
    # ... 其他字段 ...
    # 新增可选字段,仅当请求要求时才填充
    prompt_token_ids: Optional[List[int]] = None
    meta_info: Optional[Dict[str, Any]] = None
​
    @model_serializer(mode="wrap")
    def _serialize(self, handler):
        data = handler(self)
        # 排除 None 字段,保持 response 干净且向后兼容
        if self.hidden_states is None:
            data.pop("hidden_states", None)
        if self.prompt_token_ids is None:
            data.pop("prompt_token_ids", None)
        if self.meta_info is None:
            data.pop("meta_info", None)
        return data

评论区精华

Content 默认值变更:None → "" 设计

guapisolo 询问将 content 从 None 改为空串是否可接受,担心 chat template 将 None 渲染为 'None' 字符串。JustinTong0323 表示赞同。

结论:接受变更,认为改为空串更合理。 · 已解决

Multimodal 路径优先级 正确性

JustinTong0323 指出当 is_multimodal=True 时,input_ids 路径会被空 text 覆盖,导致模型收到空 prompt。

结论:作者将 input_ids 检查提前到 is_multimodal 之前,确保预分词输入优先。 · 已解决

Batch 子请求 missing return_prompt_token_ids 正确性

chatgpt-codex 报告 GenerateReqInput.__getitem__ 未复制 return_prompt_token_ids,导致 batch/n>1 场景下标志丢失。JustinTong0323 建议向 EmbeddingReqInput 也添加字段。

结论:显式添加字段到 GenerateReqInput 和 EmbeddingReqInput 的 __getitem__。 · 已解决

skip_tokenizer_init 兼容性 正确性

chatgpt-codex 指出 input_ids 分支调用 tokenizer.decode,在 skip_tokenizer_init=True 时 tokenizer 为 None 会崩溃。

结论:未直接在当前 patch 中修复,需后续关注。 · unresolved

使用 getattr 还是显式字段 style

JustinTong0323 建议不要使用 getattr,而是在 EmbeddingReqInput 中显式添加 return_prompt_token_ids 字段。

结论:采用显式字段方案。 · 已解决

风险与影响

  • 批量请求数据一致性:若 GenerateReqInput.__getitem__ 未正确传递 return_prompt_token_ids,则 n>1 的请求会静默丢失 prompt token IDs。已通过显式添加字段和测试缓解。
  • multimodal 模型空 prompt:若 input_ids 路径在 is_multimodal 检查之前执行则正常,但原始代码中 multimodal 分支后于 input_ids 分支,可能存在顺序问题,已修复。
  • skip_tokenizer_init 部署失败:如 review 所述,当 tokenizer 为 None 时调用 .decode() 会崩溃。此风险需要确认当前实现是否处理(patch 中可能未处理,建议关注)。
  • content 默认值变更:将 None 改为 "" 可能影响依赖 None 检查的客户端或下游处理,但 chat 模板渲染行为得到改善(避免 "None" 字符串)。
  • 用户影响:新功能对现有请求完全向后兼容,新增字段默认均为 FalseNone。需要 TITO 能力的用户可以开始使用 input_idsreturn_prompt_token_ids 参数。
  • 系统影响:变更集中在请求处理管线,不涉及模型推理、KV Cache 等性能敏感路径,对吞吐和延迟无显著影响。增加了一个存储 prompt token IDs 的字段和条件拷贝,内存开销极小。
  • 团队影响:需要维护新增字段的一致性(尤其是 __getitem__ 和序列化逻辑),增加少量技术债务。
batch 数据一致性 multimodal 路径矛盾 tokenizer 依赖崩溃 content 默认值变更

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论