读 io_struct.py:请求对象、批对象与输出对象家族#

这章解决什么问题#

前面的代码导读已经在 7.9 读 protocol.pyio_struct.py 里解释了“公开协议怎样变成内部对象”,但还有一层更适合单独讲清的内容:io_struct.py 自己并不是一堆零散 dataclass,而是一套层次非常稳定的对象家族。

如果只知道:

  • GenerateReqInput
  • TokenizedGenerateReqInput
  • BatchTokenIDOutput
  • BatchStrOutput

这些名字存在,却不知道它们为什么要分成这么多层,那么整本书前面建立的 request lifecycle、调度、执行和输出链路,到了源码级就仍然会显得割裂。

这章的目标,就是把 io_struct.py 压成一张对象家族地图。

为什么 io_struct.py 值得再单独成章#

7.9 更关注的是“协议桥”,也就是:

  • protocol.py 里的对外 schema
  • serving_*._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 是什么#

如果要读稳,应该先看这两个最小基类:

  • BaseReq
  • BaseBatchReq

它们最重要的作用不是提供很多行为,而是统一“请求身份”这件事:

  • 单请求用 rid
  • 批对象用 rids
  • 以及 http_worker_ipc / http_worker_ipcs

从系统设计角度看,这说明 io_struct.py 最先统一的不是业务字段,而是跨进程身份与回包路由字段。对 inference runtime 而言,这个优先级非常合理,因为没有稳定 request identity,后面所有状态桥都搭不起来。

第二层:入口请求对象家族#

这一层最值得记住的是:

  • GenerateReqInput
  • EmbeddingReqInput

它们共同的特点是:还处在“面向入口”的阶段。也就是说,这些对象虽然已经是内部 dataclass,但还保留了大量调用方语义:

  • text
  • input_ids
  • multimodal data
  • sampling_params
  • lora_path
  • priority
  • routing_key
  • external_trace_header
  • background

这说明它们不是“最底层执行对象”,而是协议层意图进入 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 请求对象家族#

这层的代表类包括:

  • TokenizedGenerateReqInput
  • TokenizedEmbeddingReqInput

这一步的关键语义变化是:

  • 入口请求已经不再只是调用方意图
  • 而是已经跨过 tokenizer / preprocessing 边界

因此你会看到这些对象开始更明显地携带:

  • input_ids
  • tokenized 相关长度与处理结果
  • 更接近 runtime 的 LoRA / time stats / routing 字段

这也解释了为什么 TokenizerManager 不会直接把 GenerateReqInput 原样发给 scheduler。scheduler 更需要的是“已经过编译的请求”。

第四层:batch 请求对象家族#

这层最典型的是:

  • BatchTokenizedGenerateReqInput
  • BatchTokenizedEmbeddingReqInput

它们的重要性不在“只是 list 包一层”,而在于这一步对象的抽象重心变了:

  • 单请求对象关注的是“一个请求有哪些属性”
  • batch 对象关注的是“这一批请求如何被整体推进”

也正因为这样,batch 类通常更轻,主要承担容器职责,但这层抽象是不可省的。因为 scheduler 的主语已经是 batch,而不再是单请求。

第五层:输出对象家族#

这里最关键的是:

  • BatchTokenIDOutput
  • BatchStrOutput

它们已经出现在前面的 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 不只装请求和输出,还装了很多控制面对象,例如:

  • PauseGenerationReqInput
  • AbortReq
  • OpenSessionReqInput

这非常值得技术书讲出来,因为它揭示了一个设计判断:SGLang 把“控制请求”也当成正式 runtime 消息,而不是旁路 RPC。

从系统角度看,这是非常一致的做法:

  • 生成请求是一类消息
  • 输出是一类消息
  • 控制动作也是一类消息

都统一放进同一个对象家族文件里,意味着 runtime 的 IPC 语义是一体化设计的。

这份对象家族地图为什么对整本书很重要#

因为它把全书前面分散讲的几条线统一了:

  • request lifecycle 里讲的入口请求
  • scheduling 里讲的 batch 主语
  • execution model 里讲的 token 级输出
  • request tail 里讲的文本级输出
  • extension/debugging 里讲的 abort、pause、session 控制

这些内容在章节上分开讲是为了教学递进,但到了源码里,它们其实在 io_struct.py 这一个文件里重新汇合。

如果你要顺着对象家族读源码,推荐顺序是什么#

建议按下面顺序:

  1. BaseReq / BaseBatchReq
  2. GenerateReqInput / EmbeddingReqInput
  3. normalize_batch_and_arguments() 一类对象编译逻辑
  4. Tokenized*ReqInput
  5. BatchTokenized*ReqInput
  6. BatchTokenIDOutput / BatchStrOutput
  7. 控制面对象,如 AbortReqPauseGenerationReqInputOpenSessionReqInput

这样读,你得到的是一张对象家族地图,而不是一份 dataclass 清单。

这一层最容易出现的误判#

1. 以为 io_struct.py 只是“内部版本的 protocol.py`#

它更像 runtime 消息总谱,而不是协议镜像。

2. 以为 batch 对象只是 list 包装#

它们标志着抽象主语从“单请求”转向“整批推进”。

3. 以为输出对象只是输入对象的反面#

它们携带的是执行尾部和文本收口所需的另一组语义。

小结#

这一章真正要补齐的,是代码导读里对 io_struct.py 的第二层理解:

  • 它不只是协议桥的终点
  • 也是整个 runtime 对象家族的总谱
  • request、tokenized、batch、output、control 五类对象都在这里分层共存

读懂这张对象家族地图之后,全书前面那些看似分散的抽象,就会在源码层自然重新对齐。