Batch、多 worker 与多模态路径#
3.1 和 3.2 都是按“单请求、单 worker、纯文本”这个最小闭环来讲的。这一节补的是把这条闭环放大之后会发生什么:当请求变成 batch、当 tokenizer worker 不止一个、当输入不再只有文本,这条路径会在哪些 handoff 点上变形。
这一节只关心三个变化:
- batch 如何改变发送和回包方式;
- 多 tokenizer worker 如何改变 request routing 和 response routing;
- 多模态输入如何改变 tokenization 之前的准备阶段。
一张图先看放大后的主线#
flowchart TB
A["GenerateReqInput<br/>batch / multimodal"] --> B["TokenizerManager"]
B --> C["single request path"]
B --> D["batch request path"]
B --> E["multi-worker routing"]
B --> F["mm_processor / encoder path"]
D --> G["BatchTokenizedGenerateReqInput"]
E --> H["http_worker_ipc / SenderWrapper"]
F --> I["mm_inputs / input_ids rewrite"]这张图里最重要的一点是:放大后的路径不是“原主链复制很多份”。一旦进入 batch、多 worker 或 multimodal 模式,输入对象、发送方式和回包对位方式都会发生变化。
batch 不是简单地把单请求循环 N 次#
最容易先误解的地方,是把 batch 理解成“把单请求路径重复执行 N 次”。TokenizerManager._handle_batch_request
说明事情并不是这么简单。
它至少有两条正式路径:
- 真正的 batch tokenization 路径
先_batch_tokenize_and_process(...),再_send_batch_request(tokenized_objs),最后为每个 request 建对应的响应生成器。 - 退化成顺序 tokenization 的路径
对每个 request 单独_tokenize_one_request(...)再发送。
这意味着 batch 是否成立,不只是看输入有没有多条 request,还要看 tokenizer 和输入形态是否适合批量处理。
真正被送到 scheduler 的批量对象也已经不是单个 TokenizedGenerateReqInput,而是 BatchTokenizedGenerateReqInput
。
parallel sampling 会进一步扭曲 batch 语义#
_handle_batch_request(...) 里还有一条很容易被忽略的分叉:parallel_sample_num > 1。这时逻辑不会只是“每个样本多生成几个输出”,而是:
- 先 tokenize 所有 request;
- 先跑一次
max_new_tokens = 0的公共前缀缓存路径; - 再为每个并行 sample 重新生成 rid,并重新发送。
也就是说,parallel sampling 会把 batch 从“多条请求一起走”变成“共享前缀 + 多个逻辑请求展开”的形式。后面如果你在返回链里看到 index、rid 或 token 统计不符合单请求直觉,往往就和这里有关。
多 tokenizer worker 改写的是路由#
当 tokenizer_worker_num == 1 时,TokenizerManager 直接把请求推到 scheduler_input_ipc_name。但当 tokenizer_worker_num > 1 时,逻辑会变成 TokenizerManager multi-worker routing
里的 SenderWrapper 模式。
关键注释已经把意图说得很直白:
# Make sure that each request carries the tokenizer_ipc_name for response routing
self.send_to_scheduler = SenderWrapper(port_args, send_to_scheduler)这里最重要的不是“谁先 tokenize”,而是“结果回来时怎样回到正确的 HTTP worker / tokenizer worker”。也正因为如此,TokenizerManager 在接收请求阶段还会补一层 TokenizerManager._attach_multi_http_worker_info
。
所以多 worker 路径改写的不是业务语义,而是 request / response 的路由语义。
多模态真正改写的是 tokenization 前的准备阶段#
多模态路径最容易被误读成“scheduler 后面多做一点事”。实际上它最先改写的是 tokenization 之前的准备阶段,而不是后面的 sampling。
这件事直接发生在 TokenizerManager._tokenize_one_request
里:
- 文本为空但存在多模态输入时,可以先用空 placeholder;
mm_processor会参与process_mm_data_async(...);- 结果可能直接回填新的
input_ids和token_type_ids。
也就是说,多模态不是“在已有 token 序列旁边再挂点附件”,而是在 tokenization 之前就可能重写输入序列本身。
更进一步,到了 Scheduler.handle_generate_request
里,多模态路径还会继续膨胀输入:
- 展开 image token;
- 调整 offsets;
- 重新计算长度;
- 如果膨胀后超过
max_req_input_len,直接 abort。
所以多模态路径的关键不是“多了图片”,而是“输入长度、offset、甚至是否还能进入 waiting queue”都可能因此变化。
这三类放大路径的共同点#
batch、多 worker 和 multimodal 看起来属于不同主题,但它们有一个共同点:都在改变最小主链的 handoff 形状。
- batch 改变的是“一个请求如何变成一组请求”;
- 多 worker 改变的是“结果怎样回到正确的 worker”;
- 多模态改写的是“tokenization 之前的输入准备阶段”。
把这三件事放在一起看,更容易先意识到:它们首先改写的是请求路径本身,而不是单纯的后端执行优化。
调试这类放大路径时先看哪里#
更稳的调试顺序通常是:
- 先确认这是单请求、batch、parallel sampling 还是 multimodal 请求;
- 再确认请求是否进入了 batch tokenization 路径,还是退化成顺序 tokenization;
- 如果是多 worker,确认
http_worker_ipc/tokenizer_ipc_name是否带对了; - 如果是多模态,确认
mm_processor是否重写了input_ids,以及长度膨胀是否触发 abort。
这里最常见的误读,是把“回包 index 混乱”“多模态 prompt 变长”“batch 性能没起来”都当成 scheduler 的问题。实际上,这些现象很多在 scheduler 之前就已经发生了。
小结#
这一节讲的不是三种功能,而是三种会放大主链复杂度的路径:
- batch 改变发送和回包的组织方式;
- 多 worker 改变 request / response 的路由方式;
- 多模态改写 tokenization 之前的输入准备。
理解了这三条放大路径,后面再看调度、执行和调试章节时,就不会把所有问题都误归到 scheduler 头上。
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。