Prhub

#48630 [MRV2][Spec Decode] Avoid rejection sampler OOM by chunking

原始 PR 作者 mgoin 合并时间 2026-07-23 18:13 文件变更 12 提交数 7 评论 15 代码增减 +390 / -67

执行摘要

分块拒绝采样避免 OOM

在推测解码中,大 batch 拒绝采样需要为所有候选 token 物化 FP32 logits 缓冲区,可能高达数 GB 导致 OOM。本 PR 通过分块复用 1GB 固定缓冲区解决该问题,同时保持数值行为一致。

建议精读 rejection_sampler.py 中的分块实现,特别是 _iter_request_chunks 和 _verify_in_chunks 的设计,该方法通过固定缓冲区避免 OOM 且保持无同步循环,对未来类似问题有借鉴意义。

讨论亮点
  • njhill 建议将 logprob 拼接逻辑抽象为 LogprobsTensors.cat 静态方法,并复用至 prompt_logprob;mgoin 采纳并实现。
  • TheEpicDolphin 询问单 chunk 时能否避免 torch.cat;mgoin 回应现有单 chunk 返回已跳过 cat,无需额外优化。
  • njhill 提出若干代码风格微调(冗余括号、返回语句简化),mgoin 全部吸纳。

实现拆解

  1. 在 rejection_sampler.py 中定义 MAX_CHUNK_BYTES = 1GB 上限,基于 FP32 字节数计算每块最大 logits 数 max_chunk_logits,并新增 _iter_request_chunks 工具函数用于生成不分割请求的 chunk 区间。
  2. 将原 call 拆分为 _verify(处理单块采样逻辑)和 _verify_in_chunks(遍历 chunk 区间、循环调用 _verify 并拼接 sampled、num_sampled 和 logprobs 结果)。_verify 从 input_batch 参数改为直接接收所需张量,使其可独立于 InputBatch 对象调用。
  3. 在 outputs.py 的 LogprobsTensors 类中新增 cat 静态方法,支持拼接分块产生的 logprob 张量(logprob_token_ids、logprobs、selected_token_ranks),并在 prompt_logprob.py 中复用此方法。
  4. 在 vllm/config/model.py 中添加 PROCESSED_LOGPROBS_MODES 常量,统一多处对 processed_logits / processed_logprobs 字面量的判断,同时修改 sampler.py、prompt_logprob.py 等文件以使用该常量。
  5. 新增单元测试文件 test_gpu_rejection_sampler_chunking.py,覆盖 chunk 边界不分割请求、分块分数与全 batch 一致性;在 test_rejection_sampler_utils.py 中增加 test_chunked_requests_match_full_batch 验证底层 rejection_sample 的分块等效性。
文件 模块 状态 重要度
vllm/v1/worker/gpu/spec_decode/rejection_sampler.py 拒绝采样 modified 8.72
vllm/v1/outputs.py 输出层 modified 6.92
tests/v1/worker/test_gpu_rejection_sampler_chunking.py 拒绝采样测试 added 7.25
tests/v1/spec_decode/test_rejection_sampler_utils.py 采样工具测试 modified 6.07

关键符号

_iter_request_chunks _verify _verify_in_chunks RejectionSampler.__call__ LogprobsTensors.cat

关键源码片段

vllm/v1/worker/gpu/spec_decode/rejection_sampler.py core-logic

核心实现文件,重构了拒绝采样器,新增 chunk 迭代和分块调度逻辑。

# 分块拒绝采样的核心迭代器函数
def _iter_request_chunks(cu_num_logits: np.ndarray, max_chunk_logits: int) -> Iterator[tuple[int, int]]:
    """根据 cu_num_logits 数组生成不分割请求的 chunk 区间 (start, end)"""
    assert max_chunk_logits > 0
    num_reqs = cu_num_logits.size - 1
    start = 0
    while start < num_reqs:
        max_logit = int(cu_num_logits[start]) + max_chunk_logits
        # 找到第一个大于 max_logit 的索引,减 1 得到最后请求的 end
        end = int(np.searchsorted(cu_num_logits, max_logit, side='right') - 1)
        # 至少包含一个请求
        end = min(num_reqs, max(start + 1, end))
        yield start, end
        start = end
vllm/v1/outputs.py core-logic

新增 LogprobsTensors.cat 静态方法,支持分块 logprob 拼接。

@staticmethod
def cat(
    tensors: Sequence['LogprobsTensors'],
    cu_num_generated_tokens: list[int] | None = None,
) -> 'LogprobsTensors':
    """拼接扁平化的 logprob 张量"""
    assert tensors
    assert cu_num_generated_tokens is not None or all(
        tensor.cu_num_generated_tokens is None for tensor in tensors
    )
    # 单元素时直接返回(若需更新 cu_num_generated_tokens 则替换)
    if len(tensors) == 1:
        tensor = tensors[0]
        if cu_num_generated_tokens is None:
            return tensor
        return tensor._replace(cu_num_generated_tokens=cu_num_generated_tokens)
    # 多元素时逐字段 torch.cat
    return LogprobsTensors(
        logprob_token_ids=torch.cat([t.logprob_token_ids for t in tensors]),
        logprobs=torch.cat([t.logprobs for t in tensors]),
        selected_token_ranks=torch.cat([t.selected_token_ranks for t in tensors]),
        cu_num_generated_tokens=cu_num_generated_tokens,
    )

评论区精华

将 logprob 拼接逻辑重构到 LogprobsTensors.cat 设计

njhill 建议将拼接逻辑移到 LogprobsTensors.cat 静态方法,mgoin 表示同意并已实现,还复用于 prompt logprobs。

结论:已采纳,移入 LogprobsTensors.cat。 · 已解决

单 chunk 时避免不必要 cat 性能

TheEpicDolphin 询问是否可以在不需要分块时提前返回以避免 torch.cat。mgoin 回应说现有单 chunk 返回已经跳过了 cat。

结论:已有逻辑处理,无需额外修改。 · 已解决

代码风格微调 style

njhill 指出冗余括号,并建议简化返回语句。mgoin 已本地修改。

结论:采纳建议并修改。 · 已解决

风险与影响

核心路径变更(拒绝采样是推测解码关键环节),分块引入额外 cat 操作可能带来微小性能开销,但测试显示平均接受长度未下降甚至略有提升,且单 chunk 时跳过 cat。固定缓冲区大小(1GB)为硬编码值,若极端场景需更多内存可能仍不足,但已比之前无限制安全。无同步循环依赖 np.searchsorted 和 cu_num_logits 的正确性。

影响所有使用推测解码(spec_decode)的用户,尤其是大 batch 场景。不再因 rejection sampler 临时缓冲区过大而 OOM,KV cache 可用量提升(测试中从 47.94 GiB 恢复至 52.02 GiB)。数值行为一致,gsm8k 准确率不变,平均接受长度从 3.06 提升至 3.18。开发者可从分块设计模式受益。

核心路径变更 固定缓冲区大小 无同步循环

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论