Prhub

#41411 [bugfix] Fix prompt logprobs on request eviction during chunked prefill

原始 PR 作者 joa-stdn 合并时间 2026-05-05 02:46 文件变更 5 提交数 9 评论 7 代码增减 +19 / -13

执行摘要

修复 chunked prefill 请求驱逐时 prompt logprobs 丢失

PR body 明确指出:computed_prefill < prompt_lens - 1 检查错误地跳过了最后一个 prompt token,导致当该 token 恰好是 chunked prefill 的最后一个分块时,prompt logprobs 不会被计算。同时,将中间状态存储在 InputBatch 字典中在请求被驱逐后无法保留,必须绑定到请求对象自身。

值得精读这个小巧但关键的数据流修复。它展示了将临时状态与请求生命周期绑定的设计模式,并体现了如何通过边界调整和状态归属重构解决并发下的数据一致性问题。团队在类似 feature(如 speculative decoding 状态)的维护时可参考此模式。

讨论亮点

与 reviewer njhill 的讨论:

  • MRV2 一致性:njhill 询问 GPU model runner v2 是否也存在相同 bug。作者回复已在 MRV2 中验证无此问题。
  • 测试增强:njhill 指出新增的测试可能会失败,因为需要修改 logprob 比较函数以处理 prompt logprobs 项。作者随后修改了 _logprobs_match 使其接受 None 并正确处理。
  • 注释细节:njhill 建议新字段的注释保持简洁,仅保留 # To accumulate prompt logprobs tensor chunks across prefill steps. 即可,作者照做。

实现拆解

  1. 状态归属迁移vllm/v1/worker/gpu_input_batch.py):在 CachedRequestState 数据类中新增 in_progress_prompt_logprobs_cpu: LogprobsTensors | None 字段,用于跨 prefill 步骤累积 prompt logprobs 的 CPU 张量。移除 InputBatch 中的 in_progress_prompt_logprobs_cpu 字典,并在 remove_request 中删除对应清理代码,因为届时状态会随 CachedRequestState 自动回收。
  2. 生产者适配vllm/v1/worker/gpu_model_runner.py):在 _get_prompt_logprobs_dict 方法中,将原先从 self.input_batch.in_progress_prompt_logprobs_cpu 获取字典的逻辑改为直接访问 request.in_progress_prompt_logprobs_cpu;条件判断从 logprobs_tensors = in_progress_dict.get(req_id) + if not logprobs_tensors 调整为 logprobs_tensors = request.in_progress_prompt_logprobs_cpu + if logprobs_tensors is None;完成预填充后通过 self.requests[req_id].in_progress_prompt_logprobs_cpu = None 清空而非 del in_progress_dict[req_id]
  3. 边界修复vllm/v1/worker/gpu/sample/prompt_logprob.py):在 compute_prompt_logprobs 中将 includes_prompt = computed_prefill < prompt_lens - 1 改为 includes_prompt = computed_prefill < prompt_lens,消除对最后一个 prompt token 的 one-off 错误。
  4. 测试配套tests/v1/e2e/general/test_async_scheduling.py, tests/conftest.py):在 test_without_spec_decodingtest_with_eagle3_spec_decoding 的采样参数列表中加入 dict(prompt_logprobs=2)dict(prompt_logprobs=2, logprobs=2);修改 _logprobs_match 函数签名以支持 None,当任一参数为 None 时返回 相等性比较lps_a is lps_b);在 VllmRunner.generate 中将 prompt_logprobs 数据也扩展到 req_logprobs 列表中。
文件 模块 状态 重要度
vllm/v1/worker/gpu_model_runner.py 模型执行器 modified 6.09
vllm/v1/worker/gpu_input_batch.py 输入批处理 modified 5.19
vllm/v1/worker/gpu/sample/prompt_logprob.py 采样 modified 5.07
tests/v1/e2e/general/test_async_scheduling.py 异步调度测试 modified 4.7
tests/conftest.py 测试配置 modified 3.42

关键符号

_get_prompt_logprobs_dict PromptLogprobCompute.compute_prompt_logprobs CachedRequestState.__init__ InputBatch.__init__ InputBatch.remove_request _logprobs_match VllmRunner.generate

关键源码片段

vllm/v1/worker/gpu_model_runner.py data-contract

核心生产者:修改了 `_get_prompt_logprobs_dict` 方法,将状态访问从 InputBatch 字典切换到 CachedRequestState 字段,并调整了清理逻辑。

def _get_prompt_logprobs_dict(
    self,
    hidden_states: torch.Tensor,
    num_scheduled_tokens: dict[str, int],
) -> dict[str, LogprobsTensors | None]:
    num_prompt_logprobs_dict = self.num_prompt_logprobs
    if not num_prompt_logprobs_dict:
        return {}
    # 不再从 InputBatch 中读取 in_progress_dict
    prompt_logprobs_dict: dict[str, LogprobsTensors | None] = {}
    completed_prefill_reqs = []
    for req_id, num_prompt_logprobs in num_prompt_logprobs_dict.items():
        num_tokens = num_scheduled_tokens.get(req_id)
        if num_tokens is None:
            continue # 请求被 preempted
        request = self.requests[req_id]
        if request.prompt_token_ids is None:
            continue # 不兼容 prompt embeddings
        num_prompt_tokens = len(request.prompt_token_ids)
        prompt_token_ids = torch.tensor(request.prompt_token_ids).to(
            self.device, non_blocking=True
        )
        # 直接从 request 取得或创建 logprobs_tensors
        logprobs_tensors = request.in_progress_prompt_logprobs_cpu
        if logprobs_tensors is None:
            logprobs_tensors = LogprobsTensors.empty_cpu(
                num_prompt_tokens - 1, num_prompt_logprobs + 1
            )
            request.in_progress_prompt_logprobs_cpu = logprobs_tensors
        # ... 后续计算逻辑不变 ...
    # 清理已完成的请求
    for req_id in completed_prefill_reqs:
        del num_prompt_logprobs_dict[req_id]
        self.requests[req_id].in_progress_prompt_logprobs_cpu = None # 原为 del in_progress_dict[req_id]
    return prompt_logprobs_dict
vllm/v1/worker/gpu/sample/prompt_logprob.py core-logic

直接修复 bug:修改 compute_prompt_logprobs 中的边界条件,确保最后一个 prompt token 也被包含。

class PromptLogprobCompute:
    # ...
    def compute_prompt_logprobs(
        self,
        # ... 参数列表
    ) -> dict[str, LogprobsTensors]:
        idx_mapping_np = input_batch.idx_mapping_np
        needs_prompt_logprobs = self.uses_prompt_logprobs[idx_mapping_np]
        if not np.any(needs_prompt_logprobs):
            return {}
        num_prompt_logprobs = self.num_prompt_logprobs[idx_mapping_np]
        prompt_lens = prompt_lens[idx_mapping_np]
        computed_prefill = num_computed_prefill_tokens[idx_mapping_np]
        # 关键修复:此前为 prompt_lens - 1,导致最后一个 token 的 logprob
        # 在 chunked prefill 的 final chunk 中被错误跳过
        includes_prompt = computed_prefill < prompt_lens
        resumed_after_prompt = prompt_lens < prefill_lens[idx_mapping_np]
        needs_prompt_logprobs &= includes_prompt & ~resumed_after_prompt
        if not np.any(needs_prompt_logprobs):
            return {}
        # ... 后续计算

评论区精华

检查 MRV2 是否受同一 bug 影响 正确性

njhill 在审核时询问:'Hopefully this same bug isn't in model runner v2, maybe you could check that too?' 作者回复已在 MRV2 中验证无此问题。

结论:确认 MRV2 无此 bug,无需额外修复。 · 已解决

测试修改后失败,需要更新 logprobs 比较函数 测试

njhill 在 Issue 评论中指出新增的测试会失败,因为比较函数未处理 prompt logprobs 项。作者随后修改了 _logprobs_match 使其可接受 None,并在 VllmRunner.generate 中附加 prompt_logprobs 数据。

结论:通过修改 _logprobs_match 和 generate 方法,测试通过。 · 已解决

风险与影响

  1. 状态迁移导致的内存泄漏CachedRequestState 可能在请求结束后未被正确清理(如异常路径),若 in_progress_prompt_logprobs_cpu 持有大张量,可能造成内存泄漏。不过 PR 中在 completed_prefill_reqs 循环末尾显式将字段置为 None,降低了风险。
  2. 边界修订的回归prompt_lens - 1 改为 prompt_lens 可能影响非 chunked prefill 或 spec decode 场景下的行为,但测试覆盖了这些组合,且 MRV2 中也同样修复(从讨论中得知),降低了回归概率。
  3. 测试覆盖局限:仅测试了 prompt_logprobs=2prompt_logprobs=2, logprobs=2 两种配置,未覆盖 -1(全部 logprobs)或与 preemption 同时触发的边界情况,可能遗漏深层问题。

直接影响在 V1 引擎中使用 prompt_logprobs 参数且发生 chunked prefill 或请求驱逐的用户:原先在这些场景下最后一个 prompt token 的 logprob 会缺失,修复后正确返回。不影响不使用 prompt_logprobs 的用户。对性能无显著影响,因为该功能本身很少被使用且代码路径保持简单。MRV2 用户不受此 PR 影响(因已在 MRV2 验证无此 bug)。

状态所有权变更 边界修复风险 测试覆盖有限

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论