Batch、多 worker 与多模态路径#

3.13.2 都是按“单请求、单 worker、纯文本”这个最小闭环来讲的。这一节补的是把这条闭环放大之后会发生什么:当请求变成 batch、当 tokenizer worker 不止一个、当输入不再只有文本,这条路径会在哪些 handoff 点上变形。

这一节只关心三个变化:

  1. batch 如何改变发送和回包方式;
  2. 多 tokenizer worker 如何改变 request routing 和 response routing;
  3. 多模态输入如何改变 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 说明事情并不是这么简单。

它至少有两条正式路径:

  1. 真正的 batch tokenization 路径
    _batch_tokenize_and_process(...),再 _send_batch_request(tokenized_objs),最后为每个 request 建对应的响应生成器。
  2. 退化成顺序 tokenization 的路径
    对每个 request 单独 _tokenize_one_request(...) 再发送。

这意味着 batch 是否成立,不只是看输入有没有多条 request,还要看 tokenizer 和输入形态是否适合批量处理。

真正被送到 scheduler 的批量对象也已经不是单个 TokenizedGenerateReqInput,而是 BatchTokenizedGenerateReqInput

parallel sampling 会进一步扭曲 batch 语义#

_handle_batch_request(...) 里还有一条很容易被忽略的分叉:parallel_sample_num > 1。这时逻辑不会只是“每个样本多生成几个输出”,而是:

  1. 先 tokenize 所有 request;
  2. 先跑一次 max_new_tokens = 0 的公共前缀缓存路径;
  3. 再为每个并行 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_idstoken_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 之前的输入准备阶段”。

把这三件事放在一起看,更容易先意识到:它们首先改写的是请求路径本身,而不是单纯的后端执行优化。

调试这类放大路径时先看哪里#

更稳的调试顺序通常是:

  1. 先确认这是单请求、batch、parallel sampling 还是 multimodal 请求;
  2. 再确认请求是否进入了 batch tokenization 路径,还是退化成顺序 tokenization;
  3. 如果是多 worker,确认 http_worker_ipc / tokenizer_ipc_name 是否带对了;
  4. 如果是多模态,确认 mm_processor 是否重写了 input_ids,以及长度膨胀是否触发 abort。

这里最常见的误读,是把“回包 index 混乱”“多模态 prompt 变长”“batch 性能没起来”都当成 scheduler 的问题。实际上,这些现象很多在 scheduler 之前就已经发生了。

小结#

这一节讲的不是三种功能,而是三种会放大主链复杂度的路径:

  • batch 改变发送和回包的组织方式;
  • 多 worker 改变 request / response 的路由方式;
  • 多模态改写 tokenization 之前的输入准备。

理解了这三条放大路径,后面再看调度、执行和调试章节时,就不会把所有问题都误归到 scheduler 头上。