Prhub

#46245 [Bugfix][Model Runner V2] Preserve all allowed_token_ids in the logit bias kernel

原始 PR 作者 Sunt-ing 合并时间 2026-06-23 15:01 文件变更 2 提交数 3 评论 1 代码增减 +12 / -0

执行摘要

修复 allowed_token_ids 静默丢失的竞态 bug

在 V2 Model Runner 中,allowed_token_ids 功能存在严重缺陷:部分允许的 token 会被静默忽略,甚至当所有允许 token 都丢失时,引擎会采样出 token id 0。PR body 给出了具体的复现脚本和输出:对[902]允许列表得到 token 0(不在列表中),对[6303, 2518, 902, 9834]得到 9834(902 和 2518 丢失)。Isseu 尚未关联,但从 bug 严重性看,其影响用户可控的生成行为。

该 PR 改动简洁、定位精准,值得所有 V2 Model Runner 使用者关注。推荐的阅读重点:

  • _bias_kernel 中竞态的分析方法(不同线程对相同地址的读写顺序问题)是 Triton 编程中一类典型 bug,值得学习。
  • 屏障插入位置的权衡,以及为何不使用更重的同步原语。
  • 扩展的测试用例设计,覆盖了从低到高的跨 vocab token id。
讨论亮点

该 PR 只有一条来自合并者 WoosukKwon 的审批评论 "LGTM! thanks for catching the bug.",无其他讨论。PR body 中已详细分析了竞态发生的根本原因和复现方法。

实现拆解

  1. vllm/v1/worker/gpu/sample/logit_bias.py_bias_kernel 函数中,在保存 logits 的 tl.load 之后插入 tl.debug_barrier():确保所有线程对允许 token 的 logits 读取完成后再开始 -inf 覆写。
  2. 在 -inf 覆写循环之后插入 tl.debug_barrier():确保所有 -inf 写入全局可见后,再执行恢复保存 logits 的 tl.store
  3. tests/v1/sample/test_sampling_params_e2e.pytest_allowed_token_ids 函数中扩展测试:在原有单 token 测试基础上,增加跨 vocab 散列分布的 7 个 token id(1, 5, 100, 500, 2518, 9834, 31999)的单 token 允许列表测试,每个都断言产生的 token 必须等于允许的 token。
  4. 无需配置或部署配套改动,仅两处源码+一处测试,改动量极小。
文件 模块 状态 重要度
vllm/v1/worker/gpu/sample/logit_bias.py 采样器 modified 5.7
tests/v1/sample/test_sampling_params_e2e.py 测试 modified 5.0

关键符号

_bias_kernel test_allowed_token_ids

关键源码片段

vllm/v1/worker/gpu/sample/logit_bias.py core-logic

核心修复文件,在 Triton kernel 中插入两个 tl.debug_barrier() 解决竞态。

# vllm/v1/worker/gpu/sample/logit_bias.py
# _bias_kernel 中的 allowed_token_ids 处理片段(已包含修复)def _bias_kernel(...):
    token_idx = tl.program_id(0)
    req_state_idx = tl.load(expanded_idx_mapping_ptr + token_idx)
    block = tl.arange(0, BLOCK_SIZE)
​
    # Allowed token IDs.
    num_allowed_token_ids = tl.load(num_allowed_token_ids_ptr + req_state_idx)
    if num_allowed_token_ids > 0:
        block = tl.arange(0, BLOCK_SIZE)
        mask = block < num_allowed_token_ids
​
        # 1. 保存允许 token 的原始 logits
        allowed_token_ids = tl.load(
            allowed_token_ids_ptr + req_state_idx * allowed_token_ids_stride + block,
            mask=mask,
        )
        logits = tl.load(
            logits_ptr + token_idx * logits_stride + allowed_token_ids, mask=mask
        )
​
        # 屏障①:确保所有线程的 tl.load 都完成,再开始 -inf 覆写
        tl.debug_barrier()
​
        # 2. 将整行 logits 置为 -inf
        for i in range(0, vocab_size, LOGITS_BLOCK_SIZE):
            offset = i + tl.arange(0, LOGITS_BLOCK_SIZE)
            tl.store(
                logits_ptr + token_idx * logits_stride + offset,
                -float("inf"),
                mask=offset < vocab_size,
            )
​
        # 屏障②:确保所有 -inf 写入全局可见,再恢复保存的 logits
        tl.debug_barrier()
​
        # 3. 恢复允许 token 的原始 logits
        tl.store(
            logits_ptr + token_idx * logits_stride + allowed_token_ids,
            logits,
            mask=mask,
        )
tests/v1/sample/test_sampling_params_e2e.py test-coverage

扩展 end-to-end 测试,覆盖跨 vocab 的多个单 token 允许列表,确保修复有效且无回归。

# tests/v1/sample/test_sampling_params_e2e.py
def test_allowed_token_ids(llm):
    """Check that we can use allowed_token_ids."""
    TOKEN_ID = 10
    allowed_token_ids = [TOKEN_ID]
    output = llm.generate(PROMPT, SamplingParams(allowed_token_ids=allowed_token_ids))
    assert output[0].outputs[0].token_ids[-1] == TOKEN_ID
​
    # 新增:跨 vocab 的多个单 token 允许列表,kernel 原本会丢失部分 token
    for token_id in (1, 5, 100, 500, 2518, 9834, 31999):
        output = llm.generate(
            PROMPT,
            SamplingParams(temperature=0, max_tokens=1, allowed_token_ids=[token_id]),
        )
        assert output[0].outputs[0].token_ids[-1] == token_id
​
    # Reject empty allowed_token_ids.
    with pytest.raises(ValueError):
        _ = llm.generate(PROMPT, SamplingParams(allowed_token_ids=[]))
    # ... 原有边界测试保持不变

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

风险极低:

  • 仅新增两个 tl.debug_barrier() 调用,语义与 Triton 官方示例 causal_conv1d.py 一致,是标准的同步原语。
  • 屏障不会改变计算逻辑,仅确保内存操作的顺序。
  • 潜在的性能影响:tl.debug_barrier() 会引入少量同步开销,但远低于 CUDA __syncthreads() 且只发生在有 allowed_token_ids 的请求路径上,对绝大多数无约束的推理流程无影响。
  • 测试已覆盖跨 vocab 散列的 token id,回归风险低。
  • 不存在安全、兼容性或部署方面的风险。

直接修复了 V2 Model Runner 中 allowed_token_ids 功能的竞态 bug,影响所有使用该功能的用户和场景,包括结构化生成、约束采样等。修复后,模型将严格遵循 allowlist 生成 token,不再产生越界 token。该功能在 V1 模型中也可能存在,但本 PR 限定了 V2 模型运行器。由于是竞态问题,之前某些用户可能已经遇到但以为是模型或配置问题,修复后行为可预期。

核心路径变更 缺少讨论

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论