Prhub

#41800 [Bugfix] Account for truncate_prompt_tokens when computing max_tokens

原始 PR 作者 viktorpusTT 合并时间 2026-05-07 00:10 文件变更 6 提交数 3 评论 7 代码增减 +68 / -0

执行摘要

修复 truncate_prompt_tokens 导致 max_tokens 预算低估

PR 描述指出:"When a request sets truncate_prompt_tokens, the prompt is truncated before inference but get_max_tokens() was receiving the original (untruncated) prompt length. This caused the output token budget to be underestimated by (original_len - truncated_len) tokens."

值得精读,尤其关注如何将分散的重复逻辑收拢到核心公共函数中。测试用例对三种场景的覆盖设计清晰,可作为类似边界测试的参考。

讨论亮点

Review 中 gemini-code-assist[bot] 指出 chat_completion/serving.py 和 completion/serving.py 两处存在相同的截断逻辑重复,建议集中到共享工具类。DarkLight1337 回应 "Perhaps we should factor this out into get_max_tokens function",并提醒 "get_max_tokens is called in a couple other places so you should fix those as well"。作者采纳建议,将逻辑折叠进 get_max_tokens,并在第三轮提交中修复了 Responses API 等其他调用点。最终实现所有入口统一传入参数,没有重复代码。

实现拆解

修复分三步完成:

  1. 核心函数增强:在 vllm/entrypoints/utils.py 的 get_max_tokens() 中增加可选参数 truncate_prompt_tokens,在计算 model_max_tokens 之前对 input_length 进行缩窄处理:若参数为 -1 则截断至 max_model_len,否则截断至指定值,然后才执行原有逻辑。
  2. 调用点适配:在所有调用 get_max_tokens() 的 serving 入口(chat_completion/serving.py、completion/serving.py、responses/serving.py 两处、serve/render/serving.py 两处)传递对应的 truncate_prompt_tokens 参数,消除各入口的重复逻辑。
  3. 回归测试:在 tests/entrypoints/openai/chat_completion/test_serving_chat.py 中新增 test_serving_chat_truncate_prompt_tokens_max_token_accounting 测试,覆盖无截断、截断值为 5、截断值为 -1 三种场景,断言最终 max_tokens 正确。
文件 模块 状态 重要度
vllm/entrypoints/utils.py 公共函数 modified 5.95
tests/entrypoints/openai/chat_completion/test_serving_chat.py 服务测试 modified 6.01
vllm/entrypoints/openai/responses/serving.py 响应服务 modified 5.55
vllm/entrypoints/serve/render/serving.py 渲染服务 modified 4.89
vllm/entrypoints/openai/chat_completion/serving.py 聊天服务 modified 4.75
vllm/entrypoints/openai/completion/serving.py 补全服务 modified 4.75

关键符号

get_max_tokens test_serving_chat_truncate_prompt_tokens_max_token_accounting create_chat_completion create_completion create_responses _generate_with_builtin_tools render_chat_request render_completion_request

关键源码片段

vllm/entrypoints/utils.py core-logic

核心修改:在 get_max_tokens 函数中新增 truncate_prompt_tokens 参数,内部根据该参数缩窄 input_length,是修复的核心逻辑所在。

def get_max_tokens(
    max_model_len: int,
    max_tokens: int | None,
    input_length: int,
    default_sampling_params: dict,
    override_max_tokens: int | None = None,
    truncate_prompt_tokens: int | None = None, # 新增参数
) -> int:
    # 若设置了 truncate_prompt_tokens,则按规则缩窄 input_length
    if truncate_prompt_tokens is not None:
        limit = truncate_prompt_tokens
        # 值为 -1 表示使用 max_model_len 作为上限,否则使用指定值
        input_length = min(
            input_length,
            max_model_len if limit == -1 else limit,
        )
    # 后续逻辑不变
    if max_model_len < input_length:
        raise ValueError(...)
    model_max_tokens = max_model_len - input_length
    platform_max_tokens = current_platform.get_max_output_tokens(input_length)
    fallback_max_tokens = (
        max_tokens
        if max_tokens is not None
        else default_sampling_params.get("max_tokens")
    )
    return min(
        val
        for val in (
            model_max_tokens,
            fallback_max_tokens,
            override_max_tokens,
            platform_max_tokens,
        )
        if val is not None
    )
tests/entrypoints/openai/chat_completion/test_serving_chat.py test-coverage

新增回归测试,覆盖三种场景,确保修复正确且不退化已有行为。

@pytest.mark.asyncio
async def test_serving_chat_truncate_prompt_tokens_max_token_accounting():
    """当 truncate_prompt_tokens 设置后,max_tokens 必须使用截断后的
    prompt 长度计算,而非原始 prompt 长度。
    """
    mock_engine = MagicMock(spec=AsyncLLM)
    mock_engine.errored = False
    mock_engine.model_config = MockModelConfig()
    mock_engine.input_processor = MagicMock()
    mock_engine.renderer = _build_renderer(mock_engine.model_config)
    serving_chat = _build_serving_chat(mock_engine)
​
    messages = [{"role": "user", "content": "what is 1+1?"}] # 7 tokens
​
    # Baseline: 无 truncation,max_tokens = 100 - 7 = 93
    req = ChatCompletionRequest(model=MODEL_NAME, messages=messages)
    with suppress(Exception):
        await serving_chat.create_chat_completion(req)
    assert mock_engine.generate.call_args.args[1].max_tokens == 93
​
    # truncate_prompt_tokens=5 时,有效 prompt 长度为 5,
    # max_tokens = 100 - 5 = 95
    req = ChatCompletionRequest(
        model=MODEL_NAME,
        messages=messages,
        truncate_prompt_tokens=5,
    )
    with suppress(Exception):
        await serving_chat.create_chat_completion(req)
    assert mock_engine.generate.call_args.args[1].max_tokens == 95
​
    # truncate_prompt_tokens=-1 表示用 max_model_len 做上限,
    # 实际 prompt 只有 7,所以仍为 100 - 7 = 93
    req = ChatCompletionRequest(
        model=MODEL_NAME,
        messages=messages,
        truncate_prompt_tokens=-1,
    )
    with suppress(Exception):
        await serving_chat.create_chat_completion(req)
    assert mock_engine.generate.call_args.args[1].max_tokens == 93

评论区精华

逻辑重复与集中化建议 设计

gemini-code-assist[bot] 指出 chat 和 completion serving 中存在相同的截断逻辑重复,建议集中到共享工具类。DarkLight1337 进一步建议将逻辑提取到 get_max_tokens 函数中,并提醒还有其他调用点需要修复。

结论:作者采纳建议,在第二轮提交中将重复逻辑折叠进 get_max_tokens,第三轮提交修复了 Responses API 等其他调用点。 · 已解决

Responses API 的适配 正确性

DarkLight1337 在 utils.py 的代码审查中提醒 "This is also called in Responses API",指出 Responses 端点也需要同样的修复。

结论:作者在第三轮提交中为 responses/serving.py 的两处调用添加了 truncate_prompt_tokens 参数。 · 已解决

风险与影响

回归风险:get_max_tokens 是多个入口共用的公共函数,新增参数默认为 None,对未设置 truncate_prompt_tokens 的请求行为不变,回归风险低。
遗漏风险:若未来新增调用 get_max_tokens 的入口,可能忘记传入 truncate_prompt_tokens 参数,但参数默认为 None 可保证向后兼容。
性能风险:仅新增一次 min 计算和条件分支,无性能影响。
兼容风险:函数签名向后兼容,外部调用无需修改。

用户影响:设置 truncate_prompt_tokens 的请求现在能获得正确的输出 token 预算,不再因预算低估而被异常截断或报错。
系统影响:所有聊天、补全、Responses API 和渲染端点均受益,覆盖范围完整。
团队影响:统一逻辑到 get_max_tokens 降低了未来维护成本和逻辑分歧风险。

核心函数变更 外部 API 兼容性

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论