Prhub

#7632 [vllm] fix: raise max_num_batched_tokens to max_model_len when chunked prefill is disabled

原始 PR 作者 ETOgaosion 合并时间 2026-08-31 18:43 文件变更 22 提交数 6 评论 2 代码增减 +42 / -50

执行摘要

修复禁用 chunked prefill 时 vLLM 启动崩溃并清理旧配置

PR body 明确指出:vLLM 在 enable_chunked_prefill=False 时要求 max_num_batched_tokens >= max_model_len,而 verl 始终向 vLLM 显式传入 max_num_batched_tokens(默认 8192),覆盖了 vLLM 自身在非 chunked 场景下默认取 max_model_len 的行为,导致 Qwen2.5-VL-3B-Instruct 等长上下文模型启动时抛出 ValidationError: max_num_batched_tokens (8192) is smaller than max_model_len (128000)。该问题由 geo3k VLM E2E 测试在 vllm 0.24 上触发。同时,示例脚本中残留的 enable_chunked_prefill=False 原本是绕开 vLLM VLM 占位符缺陷(vllm#15185,2025 年 3 月已修复)的 workaround,继续保留会对新版本造成启动失败,需随 #7629/#7624 一并清理。

值得精读。核心价值不在 diff 规模,而在 _validate_configs 中如何用“warning + 自动提升”平衡 vLLM 约束与用户显式配置;同时展示了跨示例、CI、测试的连锁清理方法,以及测试在行为不确定性下如何改为观察式断言。建议重点关注 vllm_async_server.py 的安全网分支和 agent-loop 测试的改写思路。

讨论亮点

wuxibin89:From #7629 #7624, I think we should provide max_model_len explicitly if enable_chunked_prefill=Falseactor_rollout_ref.rollout.max_model_len="${MAX_MODEL_LEN}"。BTW,can we always enable_chunked_prefill=True for newer vllm version?

ETOgaosion:I think we can make this enabled by default.

结论:两条路线都落地——引擎侧用自动提升兜底非 chunked 场景,示例与 E2E 默认走 chunked prefill;维护者对“显式传 max_model_len”的偏好通过脚本中保留 MAX_MODEL_LEN 推导而部分保留。

实现拆解

  1. 引擎侧安全网(核心修复):在 verl/workers/rollout/vllm_rollout/vllm_async_server.py_validate_configs 中新增分支:当 enable_chunked_prefill=Falsemax_num_batched_tokens < max_model_len 时,先通过 logger.warning 说明原因,再将 max_num_batched_tokens 就地提升为 max_model_len。该逻辑对齐 vLLM 自身的默认行为,且只影响非 chunked 场景;用户显式配置的更大值不会被覆盖。sglang / trtllm 后端不传递该参数,不受影响。
  2. 示例脚本清理:沿 #7629/#7624 的思路,从 examples/grpo_trainer 下的 run_glm4_1v_9b_fsdp.shrun_minicpm_o_2_6_fsdp.shrun_qwen2_5_32b_fsdp.shrun_qwen2_5_vl_7b_fsdp.shrun_qwen3_4b_fsdp.shrun_qwen3_5_2b_openr1_fsdp.shrun_qwen3_vl_30b_moe_veomni.shrun_qwen3_vl_8b_fsdp.sh 以及 examples/profile 下的 run_qwen2_5_vl_7b_torch_memory.shrun_qwen3_8b_npu_profile_discrete.sh 中删除 enable_chunked_prefill=False;由于 rollout.yaml 已默认 True,示例恢复开箱即用。torch profiler 脚本 run_qwen2_5_7b_torch_profile.sh 为获取干净 trace 保留禁用,并补上 max_model_len 显式推导。NPU 脚本均未改动。
  3. E2E 与 CI 配置清理tests/special_e2e/ppo_trainer/run_function_reward.sh 移除对 vllm#15185 的注释引用,geo3k 路由不再强制 ENABLE_CHUNKED_PREFILL=Falsetests/special_e2e/run_ppo_trainer_veomni.shtests/special_e2e/run_ppo_trainer_torchtitan.sh 同步删除参数;.github/workflows/e2e_ppo_trainer_megatron_sglang_2.ymle2e_ppo_trainer_megatron_vllm_2.yml 中的 geo3k 任务也移除了该环境变量。
  4. agent-loop 负载均衡测试适配:#7613 将并列最少负载的副本选择改为随机,tests/experimental/agent_loop/test_basic_agent_loop.pytest_new_requests_route_to_least_loadedtest_get_inflight_counttest_removed_server_invalidates_sticky_session 原来硬编码 s0/s1/s2,现改为先观察实际分配副本再断言,消除 flaky。
  5. 配套格式修复verl/utils/model.pyfrom transformers import PreTrainedModel 后补上缺失空行,满足 ruff format 的 CI 检查。
文件 模块 状态 重要度
verl/workers/rollout/vllm_rollout/vllm_async_server.py 引擎校验 modified 6.13
tests/experimental/agent_loop/test_basic_agent_loop.py 负载均衡 modified 4.94
verl/utils/model.py 工具类 modified 3.63
tests/special_e2e/ppo_trainer/run_function_reward.sh E2E 脚本 modified 3.66
tests/special_e2e/run_ppo_trainer_veomni.sh E2E 脚本 modified 3.59
tests/special_e2e/run_ppo_trainer_torchtitan.sh E2E 脚本 modified 2.88
examples/profile/run_qwen2_5_7b_torch_profile.sh 分析脚本 modified 2.71
.github/workflows/e2e_ppo_trainer_megatron_vllm_2.yml CI 配置 modified 2.85

关键符号

_validate_configs test_new_requests_route_to_least_loaded test_removed_server_invalidates_sticky_session test_get_inflight_count

关键源码片段

verl/workers/rollout/vllm_rollout/vllm_async_server.py core-logic

核心修复所在:在引擎配置校验阶段自动提升 max_num_batched_tokens,解决非 chunked 模式下长上下文模型的启动崩溃,是本次变更的主路径。

def _validate_configs(self) -> None:
    """校验 config / model_config,并修正与 vLLM 启动约束冲突的参数。"""
    max_position_embeddings = get_max_position_embeddings(self.model_config.hf_config)
    if self.config.max_model_len is None:
        # 未显式指定时,回退到模型位置编码的最大长度
        self.config.max_model_len = max_position_embeddings
    else:
        if self.config.max_model_len > max_position_embeddings:
            raise ValueError(
                f"max_model_len ({self.config.max_model_len}) should be less than or equal to "
                f"max_position_embeddings ({max_position_embeddings})"
            )
​
    # 关闭 chunked prefill 时,vLLM 要求单个 prefill 必须能放进一次调度迭代,
    # 因此必须满足 max_num_batched_tokens >= max_model_len。
    # verl 总是显式传入 max_num_batched_tokens(默认 8192),
    # 反而覆盖了 vLLM 自身在非 chunked 场景下“默认为 max_model_len”的行为,
    # 导致 128k 长上下文模型在引擎启动时直接抛错。
    if not self.config.enable_chunked_prefill and self.config.max_num_batched_tokens < self.config.max_model_len:
        logger.warning(
            "enable_chunked_prefill=False requires max_num_batched_tokens >= max_model_len "
            f"({self.config.max_model_len}); raising max_num_batched_tokens from "
            f"{self.config.max_num_batched_tokens} to {self.config.max_model_len}."
        )
        # 自动提升为用户兜底,用户显式配置的更大值不会被覆盖
        self.config.max_num_batched_tokens = self.config.max_model_len
tests/experimental/agent_loop/test_basic_agent_loop.py test-coverage

适配 #7613 随机平局打破,修复三个 GPU CI 测试的 flaky 问题,是本次除引擎修复外的第二处关键改动。

def test_new_requests_route_to_least_loaded(self, ray_for_lb):
    lb = ray.remote(GlobalRequestLoadBalancer).remote(servers={"s0": None, "s1": None, "s2": None})
    # 第一次 acquire 可能命中任意副本:并列最少负载时平局为随机选择,
    # 因此先记录实际分配到的副本,再用同一个 request_id 堆积 inflight。
    s_heavy = ray.get(lb.acquire_server.remote(request_id="a"))[0]
    ray.get(lb.acquire_server.remote(request_id="a"))[0]
    ray.get(lb.acquire_server.remote(request_id="a"))[0]
    # 第二个 request 必须落在另一副本上(其 inflight 为 1)
    s_mid = ray.get(lb.acquire_server.remote(request_id="b"))[0]
    assert s_mid != s_heavy
    # 剩余副本 inflight 为 0,新请求应被路由到该空闲副本
    idle = ({"s0", "s1", "s2"} - {s_heavy, s_mid}).pop()
    s_new = ray.get(lb.acquire_server.remote(request_id="d"))[0]
    assert s_new == idle

评论区精华

关闭 chunked prefill 时是否应显式传 max_model_len,以及新 vLLM 能否默认开启 设计

维护者 wuxibin89 建议在 enable_chunked_prefill=False 时显式提供 max_model_len,并询问新版本 vLLM 是否可总是启用 chunked prefill;作者 ETOgaosion 回应认为可以默认启用。

结论:采用引擎侧安全网自动提升 max_num_batched_tokens,同时示例与 E2E 回归默认 chunked prefill;显式 MAX_MODEL_LEN 推导在脚本中保留。 · 已解决

负载均衡测试在随机平局打破下的确定性 测试

#7613 将并列最少负载的副本选择改为随机,旧的 s0/s1/s2 硬编码断言在 GPU CI 中 flaky;通过观察实际分配副本恢复确定性。

结论:更新三个测试,先记录实际分配再验证 least-loaded 语义,避免依赖 dict 顺序。 · 已解决

风险与影响

  1. 启动路径核心变更vllm_async_server._validate_configs 是引擎启动必经路径,自动放大 max_num_batched_tokens 会提高单次调度迭代的上限,可能增加峰值显存 / KV cache 占用;仅影响非 chunked 场景,且用户显式配置更大值不会被覆盖,风险可控。
  2. 示例默认行为变化:GPU 示例从 enable_chunked_prefill=False 回归默认 True,若使用旧版 vLLM(仍受 VLM 占位符限制)可能复现 vllm#15185;该 issue 已关闭且在 vllm 0.24 下验证,风险较低但存在版本依赖。
  3. 测试确定性削弱:agent-loop 测试从“断言确定性分配”改为“观察后断言”,对随机平局打破了更健壮,但减弱了对异常路由行为的探测能力,平衡器行为回归时测试未必能捕获。
  4. 覆盖广但缺乏专项单测:安全网逻辑没有专门的单元测试,主要依赖 geo3k E2E 与 CI 验证;由于改动分散在 22 个文件,脚本清理若遗漏某处残留仍会静默影响对应工作流。

对用户:关闭 chunked prefill 的长上下文 vLLM 用户不再遭遇引擎启动崩溃,max_num_batched_tokens 行为与 vLLM 原生默认对齐;GPU 示例脚本恢复默认 chunked prefill,长 prompt 与 VLM 任务的吞吐和兼容性改善。对系统:geo3k VLM E2E 与 vLLM agent-loop CI 作业恢复确定性成功率,消除因默认参数不一致导致的“脚本在启动阶段即失败”问题。对团队:确立了“引擎参数自愈 + 显式配置优先”的模式,可作为后续处理同类 vLLM 约束冲突的参考,也示范了在上游 issue 修复后及时清理 workaround 的节奏。

启动路径核心变更 示例默认行为变化 测试确定性降低 缺少专项单测

关联 Issue

#15185 [Bug]: qwen2.5-vl 7b, vllm 0.8.1, vllm ValueError: Attempted to assign 841 + 1032 + 841 + 1032 = 3746 multimodal tokens to 3794 placeholders

完整报告

参与讨论