Prhub

#49751 [BugFix][MRV2] Don't create dummy requests longer than `max_model_len`

原始 PR 作者 njhill 合并时间 2026-07-27 10:04 文件变更 4 提交数 4 评论 3 代码增减 +72 / -7

执行摘要

限制 dummy request token 数不超过 max_model_len

PR body 明确指出,InputBatch.make_dummyGPUModelRunner._dummy_run 将全部余数加到最后一个 request,导致其 seq_len 可能达到 num_tokens - num_reqs + 1,远超 max_model_len。当 #49364 在 dummy batch 上执行 attention(cudagraph piecewise capture)时,块表越界读取垃圾页 ID,在 16GB MIG 环境稳定崩溃,迫使 #49364 被 revert(#49451)。修复后 #49364 可无需修改即重新落地。

值得精读:表面上是一次简单的余数分配调整,但它是理解 vLLM 中 dummy batch 与 CUDA graph 捕获机制交互的绝佳切入点。设计决策(如为何必须将多 token request 放在 batch 末尾)涉及 attention kernel 的 block-table 布局和 paged attention 的访问模式,是重要的边界条件案例。此外,该 PR 展示了如何通过集中修复一个底层 bug 来解锁上游优化 PR(#49364),体现了依赖管理的价值。

讨论亮点
  • WoosukKwon 提出:是否应该按 num_tokens 排序,使 decode requests 在前、prefill 在后?
  • njhill 先表示同意("Ah good point"),后回复 "now updated",确认已对 input_batch.py 做出调整,将多 token 的 request 放在 batch 末尾(与测试中的排序断言一致)。
  • 无其他争议,WoosukKwon 最终批准。

实现拆解

  1. vllm/v1/worker/gpu/input_batch.pymake_dummy 方法:将原来 num_scheduled_tokens[-1] += num_tokens % num_reqs 改为计算 base_tokens = num_tokens // num_reqsnum_extra = num_tokens % num_reqs,若有余数则从尾部开始均匀分配(num_scheduled_tokens[-num_extra:] += 1)。同时按相同模式设置 seq_lens,使各字段一致。
  2. vllm/v1/worker/gpu/model_runner.py_dummy_run 方法:将列表推导式 [num_tokens // num_reqs] * num_reqs 改为 [num_tokens // num_reqs + (i >= num_reqs - num_tokens % num_reqs) for i in range(num_reqs)],实现均匀分布。
  3. vllm/v1/worker/gpu/lora_utils.pycreate_lora_capture_hook 内的 hook:移除 num_scheduled[-1] += num_tokens % num_reqs,改用 num_scheduled[-num_extra:] += 1,与 make_dummy 保持一致。
  4. tests/v1/worker/test_gpu_input_batch_v2.py:新增参数化测试 test_make_dummy_distributes_remainder,覆盖余数大于0、无余数、少请求等场景,断言每个 request 的 token 数不超过 ceil(num_tokens/num_reqs) 且总和正确。
  5. 无其他配置或部署变更,改动仅影响 dummy batch 生成逻辑,不涉及推理路径。
文件 模块 状态 重要度
vllm/v1/worker/gpu/input_batch.py 输入批处理 modified 5.99
vllm/v1/worker/gpu/model_runner.py 模型运行器 modified 5.48
vllm/v1/worker/gpu/lora_utils.py LoRA 工具 modified 5.35
tests/v1/worker/test_gpu_input_batch_v2.py 测试 added 6.25

关键符号

InputBatch.make_dummy GPUModelRunner._dummy_run create_lora_capture_hook ( 内部 hook) test_make_dummy_distributes_remainder

关键源码片段

vllm/v1/worker/gpu/input_batch.py core-logic

核心修复:`make_dummy` 中调整 token 分配逻辑,确保每个 dummy request 的 token 数不超过 max_model_len。

# vllm/v1/worker/gpu/input_batch.py
# 在 make_dummy 中,原来将全部余数给最后一个 request,现改为均匀分布
@classmethod
def make_dummy(
    cls,
    num_reqs: int,
    num_tokens: int,
    input_buffers: InputBuffers,
) -> "InputBatch":
    assert 0 < num_reqs <= num_tokens
    device = input_buffers.device
​
    # ... ( 省略 req_ids、idx_mapping 等不变部分 )
​
    # 计算基准 tokens 和余数
    base_tokens = num_tokens // num_reqs
    num_extra = num_tokens % num_reqs
    # 均匀分布:前 n - num_extra 个 request 分得 base_tokens,后 num_extra 个多 1
    num_scheduled_tokens = np.full(num_reqs, base_tokens, dtype=np.int32)
    if num_extra > 0:
        num_scheduled_tokens[-num_extra:] += 1
    assert int(num_scheduled_tokens.sum()) == num_tokens
​
    # seq_lens 同步调整
    input_buffers.seq_lens[: num_reqs - num_extra] = base_tokens
    input_buffers.seq_lens[num_reqs - num_extra : num_reqs] = base_tokens + 1
    input_buffers.seq_lens[num_reqs:] = 0 # padding for full CUDA graph
    seq_lens = input_buffers.seq_lens[:num_reqs]
​
    # query_start_loc、input_ids、positions 等不变 ...
tests/v1/worker/test_gpu_input_batch_v2.py test-coverage

新增单元测试,覆盖各种 token/request 组合,验证均匀分布的正确性及边界条件。

# tests/v1/worker/test_gpu_input_batch_v2.py
# 测试 make_dummy 均匀分布余数,确保不产生超长 dummy request
@pytest.mark.parametrize(
    "num_reqs,num_tokens",
    [
        (256, 496), # 余数 240,原实现会塞给最后一个 request 241 tokens
        (128, 512), # 无余数
        (3, 8),
        (1, 7),
    ],
)
def test_make_dummy_distributes_remainder(num_reqs: int, num_tokens: int):
    buffers = InputBuffers(
        max_num_reqs=num_reqs, max_num_tokens=num_tokens, device=torch.device("cuda")
    )
    batch = InputBatch.make_dummy(num_reqs, num_tokens, buffers)
​
    max_per_req = -(-num_tokens // num_reqs) # ceil division
    # 总和必须等于 num_tokens
    assert batch.num_scheduled_tokens.sum() == num_tokens
    # 最大不超过 ceil
    assert batch.num_scheduled_tokens.max() == max_per_req
    # 最小不低于 floor
    assert batch.num_scheduled_tokens.min() >= num_tokens // num_reqs
    # 多 token 的 request 排在末尾(非递减顺序)
    assert (batch.num_scheduled_tokens[:-1] <= batch.num_scheduled_tokens[1:]).all()
​
    # 验证 query_len == seq_len (prefill 模式 )
    query_lens = batch.query_start_loc_np[1:] - batch.query_start_loc_np[:-1]
    assert (query_lens == batch.num_scheduled_tokens).all()
    assert torch.equal(
        batch.seq_lens, torch.from_numpy(batch.num_scheduled_tokens).to(DEVICE)
    )
    assert batch.query_start_loc_np[-1] == num_tokens
    assert torch.equal(
        batch.query_start_loc.cpu(), torch.from_numpy(batch.query_start_loc_np)
    )

评论区精华

是否应将多 token request 排序到 batch 末尾 设计

WoosukKwon 在 review 中提问:是否应该按 num_tokens 排序,使 decode requests 在前、prefill 在后?njhill 先同意(Ah good point),随后更新实现并在对话中回复“now updated”。

结论:已采纳排序建议:多 token 的 request 放置在 batch 的末尾(-num_extra:),测试中也包含顺序断言。 · 已解决

风险与影响

风险极低:变更核心是均匀分布余数,逻辑简单且与原有模式高度一致。主要风险是 LoRA 路径 (lora_utils.py) 可能未同步,但已确认同步。新增测试覆盖了典型边界,包括余数、无余数、单 request、小 batch 等场景,可防止回归。此外,若 num_tokens % num_reqs 结果与 _dummy_runi 的排序逻辑产生不一致(理论上 i 从 0 开始,尾部分配,与 input_batch 的 -num_extra: 一致),需确认两者顺序匹配。测试中 num_scheduled_tokens[:-1] <= num_scheduled_tokens[1:] 确保了非递减序,与 LoRA hook 中的 -num_extra: 赋值顺序一致(较大的在尾部)。

用户影响:修复了 cudagraph capture 时偶发的非法内存访问错误,消除 CI 中 5 个测试(test_logprobs_mode x4, test_prompt_logprobs_mode)在特定硬件(16GB MIG)上的可靠崩溃。对普通推理无直接影响,但提升了大模型预填充阶段 CUDA graph 捕获的稳定性。系统影响:无性能退化;改动位于非热路径(dummy run 仅在 cudagraph capture 时执行)。团队影响:为 #49364 的重新落地扫清障碍,该 PR 本意是分段 cudagraph capture 优化,因本 bug 被 revert。

核心路径变更(dummy batch 影响 cudagraph capture) 多路径同步(input_batch / model_runner / lora_utils 需一致) 回归风险低(逻辑简单且覆盖测试)

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论