多模态输入、mm_processor 与 encoder 路径#

这章解决什么问题#

前面的 request lifecycle 已经讲了输入规范化、batch fan-out、控制请求分叉和元数据传播,但仍然有一条非常值得单独展开的路径:当输入里真正带了 image_dataaudio_datavideo_data 时,请求怎样从“普通文本请求”变成“需要 processor / mm_processor / encoder receiver 参与的多模态请求”?

这一章就是把这条路径讲清楚。

为什么这层值得单独成章#

因为多模态输入不是“在普通请求前面再多做一点预处理”那么简单。从 TokenizerManager._tokenize_one_request(...)io_struct.pyencode_receiver.py 看,它会真实改变:

  • 输入规范化方式
  • 是否先由 processor / mm_processor 处理
  • 是否要把一部分工作 dispatch 到 encoder 侧
  • scheduler 侧是否还要重新 materialize MultimodalInputs

也就是说,它已经不是局部细节,而是 request lifecycle 的一条正式分支。

一张图:多模态请求在真正进入 runtime 前,多了一条 processor/encoder 链#

这张图解决的理解障碍是:很多读者会默认多模态只是“文本 tokenization 之前再转一下图像”,而源码实际已经长出了一条更完整的支路。

flowchart LR
    Input["text + image/audio/video"] --> Tok["TokenizerManager._tokenize_one_request()"]
    Tok --> MM["mm_processor / process_mm_data_async()"]
    MM --> Enc["optional encoder dispatch / mm_receiver"]
    Enc --> Req["tokenized request + mm_inputs"]
    Req --> Scheduler["scheduler / MultimodalInputs"]

这张图比纯文字多解释的一点是:多模态输入并不是只在 tokenizer 内部被吃掉,而是会带着额外状态继续进入 scheduler。

GenerateReqInput.contains_mm_input() 为什么是这条路径的第一道门#

io_struct.py 里已经明确:

  • image_data
  • video_data
  • audio_data

都是请求对象的一等字段,且 contains_mm_input() 会显式判断是否真的存在有效多模态数据。也就是说,多模态分支不是隐式猜测,而是请求对象自己公开承认“我带了多模态负载”。

这也解释了为什么本书前面反复强调“请求对象的形状稳定”很重要。这里就是一个典型例子。

_tokenize_one_request(...) 如何在多模态路径上分叉#

这段逻辑特别值得技术书展开,因为它清楚展示了文本与多模态的分叉点:

  1. 先处理 text / input_ids / input_embeds。
  2. 如果 self.mm_processor and obj.contains_mm_input(),就进入多模态分支。
  3. 在这里会先把 image_data / video_data / audio_data 统一变成 list 形态。
  4. 然后根据 language_onlyencoder_transfer_backendneed_wait_for_mm_inputs 等条件,选择:
    • 本地 mm_processor.process_mm_data_async(...)
    • mm_receiver.recv_mm_data(...)

这说明所谓“多模态 tokenization”,其实已经不只是 tokenization,而是“文本输入与多模态特征联合准备”。

mm_processormm_receiver 的边界为什么要区分#

这是多模态路径里最容易混淆、也最值得单独讲的一点:

  • mm_processor 更像本地多模态解释与特征准备器
  • mm_receiver 更像在 encoder disaggregation 场景下接收外部 encoder 结果的入口

对读者来说,分清这两者特别重要,因为它决定你在排障时该先看本地 processor 逻辑,还是先看 encoder dispatch / receiver。

什么时候会 dispatch 到 encoder#

TokenizerManager 里还有 _should_dispatch_to_encoder(...) 与相关逻辑,用来决定:

  • 是否应该把多模态数据交给 encoder
  • 还是直接在本地处理

这说明系统并不把“有多模态输入”自动等同于“必须 dispatch 到 encoder”,而是会按当前输入形态、配置和数量做判断。这种动态分支本身就是 request lifecycle 应该覆盖的内容,而不是 implementation trivia。

need_wait_for_mm_inputs 为什么是关键状态位#

这个字段非常有价值,因为它说明请求可能在进入 scheduler 前,还得等多模态输入准备完毕。也就是说,多模态请求的“准备完成”条件比纯文本请求更强。

这让 request lifecycle 在这里真正多出一个“等待多模态侧就绪”的阶段,而不是简单沿用普通文本请求的时序。

scheduler 这一侧又做了什么#

前面很容易让人误以为多模态输入在 tokenizer 侧就完全解决了,但 scheduler.py 明确还有:

  • _get_multimodal_inputs(...)
  • _process_and_broadcast_mm_inputs(...)
  • _maybe_compute_mrope_positions(...)

这说明多模态路径不是“在 tokenizer 侧做完就结束”,而是:

  • tokenizer 侧先把 mm_inputs 或其原始输出准备好
  • scheduler 侧再决定是否要在 TP ranks 间广播、materialize 或补 M-RoPE 位置

这是一个非常好的跨章回扣:请求生命周期说明“请求怎样进入这里”,运行时架构再解释“为什么这里还要继续处理”。

MultimodalInputs.from_processor_output(...) 为什么值得提#

这说明在 scheduler 视角看,多模态输入最终会被收口成更统一的内部对象,而不是始终保持原始 processor 输出。也就是说,多模态路径和前面写过的 protocol.py -> io_struct.py 桥是同一种思路:不同上游表示,最终要收敛成 runtime 更稳定的对象形状。

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

1. 把多模态路径理解成“只是在 tokenization 前多一步”#

实际上它会持续影响 tokenizer、encoder receiver、scheduler 和 M-RoPE 处理。

2. 以为有多模态输入就一定 dispatch 到 encoder#

源码明确会根据条件决定是否本地处理。

3. 以为 scheduler 完全不关心多模态输入#

它后面仍然会 materialize 和 broadcast 多模态输入对象。

如果你在排查多模态请求异常,先怎么走#

建议按这个顺序:

  1. 先确认请求对象里多模态字段是否被规范化成预期形状。
  2. _tokenize_one_request(...) 是否走到了 mm_processor 分支。
  3. 看是否进入了 encoder dispatch / mm_receiver 路径。
  4. 再看 scheduler 一侧是否成功 materialize / broadcast 了 MultimodalInputs
  5. 最后才深入具体模型或 vision/audio processor 细节。

小结#

这一章真正要补齐的,是 request lifecycle 对多模态输入这条支线的覆盖:

  • 多模态输入不是文本请求的小补丁
  • 它会真实改变请求准备、等待和进入 scheduler 的方式
  • 只有把这条分支单独讲出来,请求生命周期才真正像一本覆盖现实运行时的技术书

到这里,本节对“请求到底长成什么样、怎样进入系统”就更完整了。