读 protocol.py 与 io_struct.py:公开协议怎样变成内部对象#
这章解决什么问题#
代码导读前面已经把 entrypoints/openai 的 handler 骨架讲出来了,但还有一层非常值得单独补:请求在协议层用的是 Pydantic request/response model,而进入 runtime 之后又会变成 dataclass 风格的内部对象。这条桥如果不讲清楚,读者会很容易在 protocol.py 和 io_struct.py 之间反复跳,却始终不确定“这两个文件到底是不是在描述同一条请求”。
这一章就是把这条桥讲透。
为什么这层对读仓库特别重要#
如果你只读 protocol.py,你会以为系统关心的是 OpenAI-compatible request schema;如果你只读 io_struct.py,你又会以为系统关心的是 manager 之间传递的运行时对象。真正的情况是:这两者分别站在同一条请求的外层和内层。
这也是为什么这章更适合放在代码导读而不是 API 章节里。它不是解释协议本身,而是解释“协议对象怎样真正走进运行时内部”。
一张图:协议对象与内部对象之间的桥#
这张图解决的理解障碍是:很多读者会把 ChatCompletionRequest 和 GenerateReqInput 看成两套平行系统,而不是同一条变换链上的前后状态。
flowchart LR
Proto["protocol.py Pydantic models"] --> Serve["serving_*._convert_to_internal_request()"]
Serve --> IO["io_struct.py request objects"]
IO --> Batch["tokenized / batch output structs"]
Batch --> Runtime["scheduler / detokenizer / tokenizer manager"]图比纯文字多解释的一点是:protocol.py 和 io_struct.py 不是重复定义,而是协议对象与运行时对象的两端。
protocol.py 更像什么#
entrypoints/openai/protocol.py 明确写着自己是 “Pydantic models for OpenAI API protocol”。这里的对象重点在于:
- 对外请求/响应 schema
- 字段默认值和兼容性
- 请求参数归一化
- 某些 surface-specific validator
例如 ChatCompletionRequest 里会同时包含:
- OpenAI-compatible 字段
- SGLang 扩展字段
response_formatchat_template_kwargsrouted_dp_ranklora_path
这说明协议层不是纯 OpenAI 镜像,而是兼容层加扩展层。
io_struct.py 更像什么#
io_struct.py 的文件头直接说明:这里定义的是在 TokenizerManager、DetokenizerManager、Scheduler 之间传递的对象。也就是说,它的关注点已经不是“客户端想表达什么”,而是“运行时各进程之间需要交换什么”。
从结构上看,这里至少包括:
GenerateReqInputTokenizedGenerateReqInputBatchTokenizedGenerateReqInputEmbeddingReqInputBatchTokenIDOutputBatchStrOutput
这已经很清楚地说明,io_struct.py 不是协议层镜像,而是运行时消息对象层。
ChatCompletionRequest -> GenerateReqInput 这条桥的阅读价值#
serving_chat.py::_convert_to_internal_request(...) 正是最值得盯住的桥:
- 先处理
reasoning_effort、message preprocessing、tool call constraint。 - 再
request.to_sampling_params(...)。 - 最后构造
GenerateReqInput(...),把 prompt、多模态数据、sampling params、LoRA、routing、priority 等统一塞进内部请求对象。
这说明最有价值的不是背字段名,而是看“协议字段在这一层被解释成了什么内部语义”。
为什么 protocol.py 里会有这么多“看起来像运行时字段”的东西#
比如:
top_kmin_pregexebnfcustom_logit_processorrouted_dp_rank
这些字段在协议层出现,不代表协议层真的理解它们的运行时后果。更准确的说法是:协议层负责允许调用方表达这些意图,真正的运行时含义在 _convert_to_internal_request(...) 和 io_struct.py 里才落地。
这正是这章最值得技术书强调的地方:字段出现的位置,不等于语义真正生效的位置。
io_struct.py 里最值得优先记住的几个对象#
GenerateReqInput#
这是请求进入 runtime 前最重要的统一对象。文本、多模态、sampling、LoRA、routing、session、priority 都在这里收口。
TokenizedGenerateReqInput#
这一步意味着请求已经跨过“解释/规范化”边界,进入更接近 tokenized runtime 的状态。
BatchTokenIDOutput / BatchStrOutput#
这两个对象则站在请求回程链上,分别代表 token 级输出和文本级输出。
把这几个对象抓住,你就能把请求的“外层表示”和“内层表示”连接起来。
为什么这条桥能显著提升读仓库效率#
因为很多读者读大型系统时都会被一种假象拖住:以为不同目录里出现的是不同系统。protocol.py 和 io_struct.py 正好是典型例子。只要把这条桥看清,很多阅读动作都会变简单:
- 看到请求字段时,知道去哪里找它的运行时落点。
- 看到内部对象时,知道它最初来自哪个公开 surface。
- 排障时,知道问题更可能出在 schema、转换还是运行时对象推进。
这一层最容易出现的误判#
1. 把 protocol.py 当成“完整语义定义”#
它定义的是对外 shape,不是全部运行时语义。
2. 把 io_struct.py 当成“纯内部细节”#
实际上很多公开功能是否成立,都要在这里找到真正落点。
3. 看到字段重复出现就以为设计重复#
很多时候这是对象跨层演化,而不是重复定义。
如果你要顺着一条请求往里追,先怎么查#
建议按这个顺序:
- 先在
protocol.py找公开字段和 validator。 - 再在
serving_*._convert_to_internal_request(...)看这些字段被怎样翻译。 - 再到
io_struct.py看内部对象的真正承载形态。 - 最后才进入 tokenizer / scheduler / detokenizer 路径。
小结#
这一章真正想补齐的,是代码导读里经常缺失的数据模型桥:
protocol.py描述请求对外怎样说。io_struct.py描述运行时内部怎样接住它。_convert_to_internal_request(...)则是两者之间最值得读的桥。
到这里,代码导读部分就不仅在讲“哪些目录重要”,也开始讲“同一条请求在不同层到底长成什么样”。
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。