Prhub

#48535 [Front-end] [Messages] Populate `num_cache_creation_tokens`

原始 PR 作者 yzong-rh 合并时间 2026-07-19 01:04 文件变更 15 提交数 4 评论 6 代码增减 +201 / -127

执行摘要

填充 Messages API 的缓存创建 token 数

40912 引入了 prompt_tokens_details 支持,但缺少区分缓存命中 (cached_tokens) 和缓存创建 (created_cache_tokens) 的能力。用户在使用 Anthropic Messages API 时需要准确的缓存创建 token 数,以便正确计费和理解缓存行为。PR body 指出:"When --enable-prompt-tokens-details is toggled, compute the number of prompt tokens written to prefix cache and pass it through to OpenAI Chat Completions and eventually to Anthropic Messages API. Ensures num_cache_creation_tokens is populated."

推荐精读 KVCacheManager.estimate_cached_tokens 和 PrefillStats.finalize 的设计,它们展示了如何在避免昂贵全量匹配的前提下近似估计缓存创建 token 数。另外,scheduler.py 中 prefill_stats 获取时机的选择(在块释放前)值得注意,体现了对资源生命周期的精细管理。

讨论亮点

Review 中最核心的讨论围绕性能展开:

  • tlrmchlsmth 指出在调度器里调用 find_longest_cache_hit 对每个请求开销太大,建议在 cache_blocks() 中维护高水位标记,或仅在启用 --enable-prompt-tokens-details 时调用。
  • yzong-rh 回应说已改用 KVCacheManager.estimate_cached_tokens 方法,通过遍历块并取 block.block_hash_num_tokens 最大值来估计,避免了全量哈希,性能可接受。该实现最终被采纳。

此外,作者还澄清了在 update_from_output 中提前获取 prefill_stats 的原因:确保在 KVCacheBlocks 被释放前还能访问它们的缓存状态。

实现拆解

实现过程包括以下步骤:

  1. 补充 PrefillStats 模型:在 vllm/v1/metrics/stats.py 的 PrefillStats 数据类中新增 num_cache_creation_tokens: int 字段,并添加 finalize(num_cached_tokens) 方法,该方法根据 num_cached_tokens(估计的请求缓存 token 数)和已有的 num_cached_tokens(调度时确定的缓存 token 数)计算出本次创建的新缓存 token 数。

  2. 添加 estimate_cached_tokens 方法:在 vllm/v1/core/kv_cache_manager.py 的 KVCacheManager 中新增 estimate_cached_tokens(request) 方法。它遍历每个 KV cache 组的块,取每个组内 block.block_hash_num_tokens 的最大值作为该组的缓存 token 数,然后对所有组取最小值作为整体估计。这种方法避免了昂贵的哈希全量匹配,性能较好。

  3. 修改调度器输出逻辑:在 vllm/v1/core/sched/scheduler.py 的 update_from_output 中,当确定需要输出(有新 token、pooler 输出或停止)时,调用 request.take_prefill_stats() 获取 PrefillStats,然后调用其 finalize 方法,传入 self.kv_cache_manager.estimate_cached_tokens(request),从而计算出 num_cache_creation_tokens。这一步在块被释放前执行,确保能访问到缓存状态。

  4. 改造 Anthropic 使用量计算:在 vllm/entrypoints/anthropic/serving.py 中重写了 _build_anthropic_usage 函数,直接从 UsageInfo.prompt_tokens_details 中提取 cached_tokens(缓存读取)和 created_cache_tokens(缓存创建),并据此调整 input_tokens 的计算公式(input_tokens = prompt_tokens - cache_read - cache_creation)。同时更新了 messages_full_converter 的调用方式,简化了参数传递。

  5. 更新测试覆盖:修改了 tests/entrypoints/anthropic/test_anthropic_messages_conversion.py 中的 TestBuildAnthropicUsage 测试类,添加了 test_zero_cached_tokens、test_all_tokens_cached 等用例,并移除了不再需要的 TestGetCachedTokens 辅助测试类。新增了 tests/entrypoints/anthropic/test_messages.py 中的 test_anthropic_streaming_cache_usage 集成测试,通过发送两次请求(冷启动和预热)验证 cache_read_input_tokens 和 cache_creation_input_tokens 的正确性。

文件 模块 状态 重要度
vllm/entrypoints/anthropic/serving.py Anthropic 入口 modified 7.48
vllm/v1/core/kv_cache_manager.py 缓存层 modified 7.28
vllm/v1/core/sched/scheduler.py 调度器 modified 6.64
vllm/v1/metrics/stats.py 统计 modified 6.1
tests/entrypoints/anthropic/test_anthropic_messages_conversion.py 测试 modified 7.28

关键符号

_build_anthropic_usage KVCacheManager.estimate_cached_tokens PrefillStats.finalize Scheduler.update_from_output

关键源码片段

vllm/entrypoints/anthropic/serving.py core-logic

这是 Anthropic Messages API 入口,核心函数 `_build_anthropic_usage` 被完全重写,现在直接从 `UsageInfo.prompt_tokens_details` 提取 `cached_tokens` 和 `created_cache_tokens`,实现了 `num_cache_creation_tokens` 的填充。

def _build_anthropic_usage(
    usage: UsageInfo | None,
) -> AnthropicUsage:
    '''Build an AnthropicUsage from UsageInfo.
​
    Anthropic defines total_input == input_tokens + cache_read + cache_creation.
    vLLM's prompt_tokens is the total, so input_tokens = prompt_tokens - cache_read - cache_creation.
    Cache fields are taken from UsageInfo.prompt_tokens_details.
    When cache info is absent, cache fields are left unset so exclude_unset=True serialization omits them entirely.
    completion_tokens may be None on intermediate stream chunks; we coerce to 0.
    '''
    kwargs = {}
    if usage is None:
        kwargs['input_tokens'] = 0
        kwargs['output_tokens'] = 0
    else:
        kwargs['output_tokens'] = usage.completion_tokens or 0
        input_tokens = usage.prompt_tokens
​
        # 从 prompt_tokens_details 中提取缓存命中与缓存创建信息
        if (details := usage.prompt_tokens_details) is not None:
            if (cache_read := details.cached_tokens) is not None:
                # 缓存读取 token 数,从 input_tokens 中扣除
                input_tokens -= cache_read
                kwargs['cache_read_input_tokens'] = cache_read
​
            if (cache_creation := details.created_cache_tokens) is not None:
                # 缓存创建 token 数,从 input_tokens 中扣除
                input_tokens -= cache_creation
                kwargs['cache_creation_input_tokens'] = cache_creation
​
        # 最终 input_tokens 不能为负数
        kwargs['input_tokens'] = max(0, input_tokens)
    return AnthropicUsage(**kwargs)
vllm/v1/core/kv_cache_manager.py core-logic

新增 `estimate_cached_tokens` 方法,用于估计请求已缓存的 token 数,该方法被调度器调用来计算 `num_cache_creation_tokens`。

def estimate_cached_tokens(self, request: Request) -> int:
    '''Estimate the number of tokens cached by the request.'''
    cached_tokens: int | None = None
    for group, blocks in zip(
        self.kv_cache_config.kv_cache_groups,
        self.get_blocks(request.request_id).blocks,
    ):
        if isinstance(
            group.kv_cache_spec,
            (CrossAttentionSpec, EncoderOnlyAttentionSpec),
        ):
            # Cross-attention 和 encoder-only 组不参与前缀缓存
            continue
​
        # 取当前组内所有块中最大的 block_hash_num_tokens 作为该组的缓存 token 数
        group_cached_tokens = 0
        for block in blocks:
            group_cached_tokens = max(
                group_cached_tokens,
                block.block_hash_num_tokens or 0,
            )
​
        # 对每个组取最小值为总体估计,确保不会高估(最弱的组决定整体缓存程度)
        cached_tokens = (
            group_cached_tokens
            if cached_tokens is None
            else min(cached_tokens, group_cached_tokens)
        )
​
    return cached_tokens or 0

评论区精华

estimate_cached_tokens 性能影响 性能

tlrmchlsmth 指出在 scheduler.py 中调用 `find_longest_cache_hit` 对每个请求开销太大,建议在 `cache_blocks()` 中维护高水位标记,或仅在启用 `--enable-prompt-tokens-details` 时调用。

结论:作者 yzong-rh 回应改用 `KVCacheManager.estimate_cached_tokens` 方法,通过遍历块并取 `block.block_hash_num_tokens` 最大值来估计,避免了全量哈希,性能可接受。最终采纳该方案。 · 已解决

风险与影响

  1. 性能风险:estimate_cached_tokens 遍历所有块,对于长 context 有 O(N) 开销。但已通过仅取最大值(而非全表哈希)控制代价。另可通过 --enable-prompt-tokens-details 开关控制是否调用该路径。
  2. 正确性风险:estimate_cached_tokens 使用 block.block_hash_num_tokens 估计,该值反映块内前缀缓存匹配的 token 数,但取最大值再取最小值可能低估或多估(但实际上保守估计了最弱的组)。对于跨组非均匀缓存的情况,可能得到略低的缓存 token 数,但不会导致逻辑错误。
  3. 兼容性风险:新增的 num_cache_creation_tokens 字段在 Anthropic 响应中为可选,--enable-prompt-tokens-details 关闭时不填充,不影响旧行为。OpenAI 响应通过 PromptTokenUsageInfo.created_cache_tokens 传递,同样可选。
  4. 核心路径变更:调度器输出逻辑和缓存管理器新增方法属于核心路径,需仔细回归测试。
  • 用户影响:使用 Anthropic Messages API 的用户现在可以获取 cache_creation_input_tokens 和 cache_read_input_tokens,使缓存使用统计更精细。OpenAI Chat Completions 用户也可通过 prompt_tokens_details.created_cache_tokens 获取缓存创建信息。所有变更仅在启用 --enable-prompt-tokens-details 时生效,向后兼容。
  • 系统影响:调度器在每次输出时调用 estimate_cached_tokens,增加小幅计算开销。内部统计更准确,有助于缓存策略优化。
  • 团队影响:维护成本较低,新增方法有明确用途,且测试覆盖充分。
性能风险:estimate_cached_tokens 遍历所有块 核心路径变更:调度器输出逻辑修改 兼容性:新增字段可选,向后兼容

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论