读 io_struct.py:请求对象、批对象与输出对象家族#
这章解决什么问题#
前面的代码导读已经在 7.9 读 protocol.py 与 io_struct.py 里解释了“公开协议怎样变成内部对象”,但还有一层更适合单独讲清的内容:io_struct.py 自己并不是一堆零散 dataclass,而是一套层次非常稳定的对象家族。
如果只知道:
GenerateReqInputTokenizedGenerateReqInputBatchTokenIDOutputBatchStrOutput
这些名字存在,却不知道它们为什么要分成这么多层,那么整本书前面建立的 request lifecycle、调度、执行和输出链路,到了源码级就仍然会显得割裂。
这章的目标,就是把 io_struct.py 压成一张对象家族地图。
为什么 io_struct.py 值得再单独成章#
7.9 更关注的是“协议桥”,也就是:
protocol.py里的对外 schemaserving_*._convert_to_internal_request(...)io_struct.py里的内部对象
而这章更关注 io_struct.py 内部自身的分层逻辑:
- 哪些对象面向入口请求
- 哪些对象面向 tokenized request
- 哪些对象面向 batch
- 哪些对象面向输出与控制面
换句话说,上一章讲“怎么进来”,这一章讲“进来之后对象怎样沿 runtime 主线继续演化”。
一张图:io_struct.py 里的对象不是平铺列表,而是家族树#
这张图解决的理解障碍是:很多读者第一次打开 io_struct.py,会把它看成一大串 dataclass 清单,但源码其实已经按 runtime 阶段自然分层。
flowchart TD
Base["BaseReq / BaseBatchReq"] --> Req["GenerateReqInput / EmbeddingReqInput"]
Req --> Tok["Tokenized*ReqInput"]
Tok --> Batch["BatchTokenized*ReqInput"]
Batch --> Exec["Schedule / Forward / runtime processing"]
Exec --> Out1["BatchTokenIDOutput"]
Out1 --> Out2["BatchStrOutput / BatchEmbeddingOutput"]
Base --> Ctl["AbortReq / PauseGenerationReqInput / OpenSessionReqInput ..."]图比纯文字多解释的一点是:io_struct.py 里的类不是按“功能模块”随意摆放,而是按 runtime 阶段分层。
第一层:BaseReq / BaseBatchReq 是什么#
如果要读稳,应该先看这两个最小基类:
BaseReqBaseBatchReq
它们最重要的作用不是提供很多行为,而是统一“请求身份”这件事:
- 单请求用
rid - 批对象用
rids - 以及
http_worker_ipc/http_worker_ipcs
从系统设计角度看,这说明 io_struct.py 最先统一的不是业务字段,而是跨进程身份与回包路由字段。对 inference runtime 而言,这个优先级非常合理,因为没有稳定 request identity,后面所有状态桥都搭不起来。
第二层:入口请求对象家族#
这一层最值得记住的是:
GenerateReqInputEmbeddingReqInput
它们共同的特点是:还处在“面向入口”的阶段。也就是说,这些对象虽然已经是内部 dataclass,但还保留了大量调用方语义:
textinput_ids- multimodal data
sampling_paramslora_pathpriorityrouting_keyexternal_trace_headerbackground
这说明它们不是“最底层执行对象”,而是协议层意图进入 runtime 之后的第一站。
GenerateReqInput 为什么特别重#
因为它是生成路径上真正的总收口点。只看字段量就能看出它承担了很多边界:
- 文本 / token / embeds 输入
- 多模态输入
- session 与 disaggregation
- LoRA
- 路由与优先级
- trace / metrics 附加信息
这也解释了为什么 request lifecycle 和 structured generation 两部分最后都要落回这里。
EmbeddingReqInput 为什么不能被看成“简化版生成请求”#
它和生成请求家族共享一些边界,但它并不只是“少几个字段的 GenerateReqInput”。它代表的是另一条 execution人格:
- embedding
- classify
- rerank / score
把它单独放在请求家族中,能帮助你避免把全书都读成“只有 token generation 一条主线”。
normalize_batch_and_arguments() 为什么是对象家族真正开始分化的地方#
GenerateReqInput.normalize_batch_and_arguments() 很值得从教学角度强调。它做的不是普通参数整理,而是一次对象语义编译:
- 验证输入互斥关系
- 判断单请求还是 batch
- 处理并行采样扩张
- 规范化
rid - 扩展 LoRA、多模态、sampling 参数
也就是说,同一个 GenerateReqInput 在进入 manager 前,还处在“可变形”的阶段;经过这一步后,它才真正确定自己在 runtime 里是一条请求还是一组请求。
这就是为什么对象家族不能只按类名记,而要按“所处阶段”记。
第三层:tokenized 请求对象家族#
这层的代表类包括:
TokenizedGenerateReqInputTokenizedEmbeddingReqInput
这一步的关键语义变化是:
- 入口请求已经不再只是调用方意图
- 而是已经跨过 tokenizer / preprocessing 边界
因此你会看到这些对象开始更明显地携带:
input_ids- tokenized 相关长度与处理结果
- 更接近 runtime 的 LoRA / time stats / routing 字段
这也解释了为什么 TokenizerManager 不会直接把 GenerateReqInput 原样发给 scheduler。scheduler 更需要的是“已经过编译的请求”。
第四层:batch 请求对象家族#
这层最典型的是:
BatchTokenizedGenerateReqInputBatchTokenizedEmbeddingReqInput
它们的重要性不在“只是 list 包一层”,而在于这一步对象的抽象重心变了:
- 单请求对象关注的是“一个请求有哪些属性”
- batch 对象关注的是“这一批请求如何被整体推进”
也正因为这样,batch 类通常更轻,主要承担容器职责,但这层抽象是不可省的。因为 scheduler 的主语已经是 batch,而不再是单请求。
第五层:输出对象家族#
这里最关键的是:
BatchTokenIDOutputBatchStrOutput
它们已经出现在前面的 execution model 和 output tail 章节里,但放回 io_struct.py 再看一次,你会更清楚一件事:输入对象家族和输出对象家族在这里是对称的。
BatchTokenIDOutput#
这是 token 级输出的正式跨进程对象。它携带的不是纯文本,而是:
- finish reasons
- decoded text 缓冲信息
- token counts
- logprobs
- reasoning tokens
- cached token details
- dp ranks
- scheduler time stats
这说明它其实是执行尾部的“富对象”,而不是只装 token id 的薄壳。
BatchStrOutput#
这是再往后一步的文本级输出对象。它保留了必要元信息,但抽象中心已经转向:
output_strs- 文本级 finish 语义
- 供
TokenizerManager侧继续收敛与回包
也就是说,这一层对象家族清楚地映射了输出尾部 pipeline。
控制面对象为什么也放在这里#
io_struct.py 不只装请求和输出,还装了很多控制面对象,例如:
PauseGenerationReqInputAbortReqOpenSessionReqInput
这非常值得技术书讲出来,因为它揭示了一个设计判断:SGLang 把“控制请求”也当成正式 runtime 消息,而不是旁路 RPC。
从系统角度看,这是非常一致的做法:
- 生成请求是一类消息
- 输出是一类消息
- 控制动作也是一类消息
都统一放进同一个对象家族文件里,意味着 runtime 的 IPC 语义是一体化设计的。
这份对象家族地图为什么对整本书很重要#
因为它把全书前面分散讲的几条线统一了:
- request lifecycle 里讲的入口请求
- scheduling 里讲的 batch 主语
- execution model 里讲的 token 级输出
- request tail 里讲的文本级输出
- extension/debugging 里讲的 abort、pause、session 控制
这些内容在章节上分开讲是为了教学递进,但到了源码里,它们其实在 io_struct.py 这一个文件里重新汇合。
如果你要顺着对象家族读源码,推荐顺序是什么#
建议按下面顺序:
BaseReq/BaseBatchReqGenerateReqInput/EmbeddingReqInputnormalize_batch_and_arguments()一类对象编译逻辑Tokenized*ReqInputBatchTokenized*ReqInputBatchTokenIDOutput/BatchStrOutput- 控制面对象,如
AbortReq、PauseGenerationReqInput、OpenSessionReqInput
这样读,你得到的是一张对象家族地图,而不是一份 dataclass 清单。
这一层最容易出现的误判#
1. 以为 io_struct.py 只是“内部版本的 protocol.py`#
它更像 runtime 消息总谱,而不是协议镜像。
2. 以为 batch 对象只是 list 包装#
它们标志着抽象主语从“单请求”转向“整批推进”。
3. 以为输出对象只是输入对象的反面#
它们携带的是执行尾部和文本收口所需的另一组语义。
小结#
这一章真正要补齐的,是代码导读里对 io_struct.py 的第二层理解:
- 它不只是协议桥的终点
- 也是整个 runtime 对象家族的总谱
- request、tokenized、batch、output、control 五类对象都在这里分层共存
读懂这张对象家族地图之后,全书前面那些看似分散的抽象,就会在源码层自然重新对齐。
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。