多模态输入、mm_processor 与 encoder 路径#
这章解决什么问题#
前面的 request lifecycle 已经讲了输入规范化、batch fan-out、控制请求分叉和元数据传播,但仍然有一条非常值得单独展开的路径:当输入里真正带了 image_data、audio_data、video_data 时,请求怎样从“普通文本请求”变成“需要 processor / mm_processor / encoder receiver 参与的多模态请求”?
这一章就是把这条路径讲清楚。
为什么这层值得单独成章#
因为多模态输入不是“在普通请求前面再多做一点预处理”那么简单。从 TokenizerManager._tokenize_one_request(...)、io_struct.py 和 encode_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_datavideo_dataaudio_data
都是请求对象的一等字段,且 contains_mm_input() 会显式判断是否真的存在有效多模态数据。也就是说,多模态分支不是隐式猜测,而是请求对象自己公开承认“我带了多模态负载”。
这也解释了为什么本书前面反复强调“请求对象的形状稳定”很重要。这里就是一个典型例子。
_tokenize_one_request(...) 如何在多模态路径上分叉#
这段逻辑特别值得技术书展开,因为它清楚展示了文本与多模态的分叉点:
- 先处理 text / input_ids / input_embeds。
- 如果
self.mm_processor and obj.contains_mm_input(),就进入多模态分支。 - 在这里会先把
image_data / video_data / audio_data统一变成 list 形态。 - 然后根据
language_only、encoder_transfer_backend、need_wait_for_mm_inputs等条件,选择:- 本地
mm_processor.process_mm_data_async(...) - 或
mm_receiver.recv_mm_data(...)
- 本地
这说明所谓“多模态 tokenization”,其实已经不只是 tokenization,而是“文本输入与多模态特征联合准备”。
mm_processor 和 mm_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 多模态输入对象。
如果你在排查多模态请求异常,先怎么走#
建议按这个顺序:
- 先确认请求对象里多模态字段是否被规范化成预期形状。
- 看
_tokenize_one_request(...)是否走到了mm_processor分支。 - 看是否进入了 encoder dispatch /
mm_receiver路径。 - 再看 scheduler 一侧是否成功 materialize / broadcast 了
MultimodalInputs。 - 最后才深入具体模型或 vision/audio processor 细节。
小结#
这一章真正要补齐的,是 request lifecycle 对多模态输入这条支线的覆盖:
- 多模态输入不是文本请求的小补丁
- 它会真实改变请求准备、等待和进入 scheduler 的方式
- 只有把这条分支单独讲出来,请求生命周期才真正像一本覆盖现实运行时的技术书
到这里,本节对“请求到底长成什么样、怎样进入系统”就更完整了。
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。