Skip to content

69. Prefix Caching Benchmark | 前缀缓存基准 ​

难度: Hard | 环境: CPU-first | 标签: 推理优化, Prefix Cache, 基准对比 | 目标人群: 项目决策练习者

🚀 云端运行环境

本章节的实战代码可以点击以下链接在免费 GPU 算力平台上直接运行:

Open In ColabOpen In Studio (国内推荐:魔搭社区免费实例)


本节导读 ​

多个请求如果共享相同的开头,前缀缓存可以让后续请求跳过已经完成的部分 Prefill。学习时要从请求分布出发,观察哪些前缀真的会重复、命中后节省了多少工作,以及缓存维护和失效带来了什么代价。本节沿着请求样本、baseline、对照指标和部署判断逐步完成评估。

关键词: prefix cache, hit rate, TTFT, overhead, deployment


前置阅读 ​

导语: 进入本节前,先理解 KV Cache 如何保存请求状态,以及 PagedAttention、RadixAttention 如何组织这些状态。阅读时重点观察共享前缀如何被识别、命中和复用,再把这种复用与 TTFT、吞吐和缓存容量联系起来。

Step 1: 建立前缀复用的整体视野 ​

本项目从一组已经 token 化的请求样本出发,沿着“共享前缀 → token block → 匹配与命中 → 复用或淘汰 → 性能与资源代价”理解 prefix cache。后续实验只改变 cache policy,观察这条机制链是否带来稳定收益。

概念环节学习时先回答的问题
请求输入哪些请求共享相同的前缀,后缀和生成长度如何变化?
缓存机制前缀如何切成 token block,哪些完整 block 可以被复用?
状态变化命中、未命中、容量不足和淘汰分别如何改变缓存状态?
实验观察复用减少了多少工作,维护它又增加了哪些代价?

前缀缓存 benchmark:请求复用、缓存策略与可观测证据

Step 2: 准备请求分布并建立 baseline ​

先从一组可复现的请求开始,确认 token 化后的共享前缀、生成长度和并发设置符合预期。随后运行关闭缓存的 G0,再开启一种 cache policy 运行 G1,把两组结果放在同一张对照表中:

固定项G0 baselineG1 candidate
请求输入相同 shared-prefix workload与 G0 相同
模型与 backend固定版本与 G0 相同
batch / concurrency固定与 G0 相同
唯一变量cache offprefix cache policy

Step 3: 读懂复用收益与系统代价 ​

学习时把“是否命中”“请求是否变快”和“缓存是否值得维护”放在一起观察。每组结果至少要同时记录:

指标组字段判断问题
复用与证据hit rate、reused tokens、prefill work reduction、backend metrics 或日志重复计算减少了多少,命中是否有直接证据
性能TTFT、TPOT、E2E、throughput、P99请求链路是否真正变快
资源peak memory、cache blocks、eviction缓存维护是否带来额外压力
质量status、task quality、OOM是否保持同一输出约束

Step 4(CPU 代码练习):实现缓存模拟、比较与决策 ​

这一 Step 把前面的前缀匹配和分块机制写成四个相互衔接的 TODO:先模拟 token block 的命中与淘汰,再汇总运行结果、比较 baseline,最后生成项目决策。完成后先运行 CPU 测试,检查返回字段和判断方向;Step 5 再把这些字段对应到真实 backend 的实验记录。

类型函数你要完成或阅读的内容运行后检查什么
TODO 1simulate_prefix_cache模拟完整 token block 的命中、复用、容量和淘汰hit_rate、reused_tokens、eviction_count、cache_size_blocks
TODO 2summarize_prefix_cache汇总多次运行的命中率、TTFT 和最佳运行run_count、平均指标、best_hit_rate_run
TODO 3compare_prefix_cache_to_baseline统一 candidate 相对 baseline 的增益和差值方向hit_rate_gain、ttft_delta_ms、overhead_delta
TODO 4recommend_prefix_cache_run按当前 workload 的命中率增益、TTFT 和维护开销门槛形成项目决策accept / tune / reject、reason、next_action;命中率或收益不足时不能直接 accept

图解:22-24-34 如何收束到 69 前缀缓存基准 ​

69 把缓存机制从单点技巧收成一个可部署的 benchmark。

text
22 vLLM             paged KV and serving behavior
      │
24 RadixAttention   prefix reuse and tree lookup
      │
34 Prefix caching   chunked prefill and cache hit behavior
      │
      ▼
69 Prefix benchmark hit rate + TTFT + maintenance cost + delivery decision

项目页最小产物:

模块必须记录用途
baseline请求重用模式、TTFT、缓存关闭口径保证比较合法
candidatehit rate、TTFT、维护开销解释缓存收益来源
对比命中增益、延迟变化、开销变化判断是否值得 adopt
决策accept / tune / reject输出 benchmark 结论
python
from typing import Dict, List
python
# 4 个核心 TODO:缓存模拟、workload 汇总、baseline 对比、项目判断
# 目标:先用 CPU 验证缓存生命周期,再把结果转成可比较的 benchmark 报告。
# CPU 题目区验证 token block 的复用和驱逐;真实 KV Cache、TTFT 和 backend 命中率属于 GPU 扩展。
def simulate_prefix_cache(token_sequences: List[List[int]], block_size: int = 4, capacity_blocks: int = 8) -> Dict[str, object]:
    """模拟按完整 token block 复用的前缀缓存;不测真实 KV Cache 显存或延迟。

    token_sequences 必须已经使用同一 tokenizer 转成 token id;相同字符串但 token 序列不同不视为命中。
    """
    if block_size <= 0 or capacity_blocks <= 0:
        raise ValueError('block_size 和 capacity_blocks 必须为正数')
    # ==========================================
    # TODO 1:补全 block 命中、复用和驱逐的关键状态更新;参数校验和返回结构保持现有骨架。
    # 提示:先生成完整 blocks,再用 key = (block_index, block) 查找连续命中。
    # blocks = ???
    # key = ???
    # hit_blocks = ???
    # reused_tokens = ???
    # eviction_count = ???
    #       只复用完整 block,尾部不足 block_size 的 token 不计入命中;
    #       返回 hit_rate、token_reuse_rate、prefill_work_reduction 等字段。
    # 返回 total_requests、total_prompt_tokens、reused_tokens、miss_tokens、
    # hit_rate、token_reuse_rate、prefill_work_reduction、eviction_count、cache_size_blocks。
    # ==========================================
    raise NotImplementedError("请先完成 TODO 代码!")

def summarize_prefix_cache(runs: List[Dict[str, float]]) -> Dict[str, object]:
    """汇总同一请求分布下的缓存运行结果。

    输入至少应包含 name、hit_rate 和 ttft_ms;返回运行次数、平均命中率、
    平均 TTFT 和命中率最高的运行。空列表只返回空摘要,不构成性能结论。
    缺失指标不能静默解释为真实的 0;命中率是 token/block 口径时必须保持一致。"""
    # TODO 2:汇总多次 workload 运行结果。
    # 提示:遍历 runs,读取 name、hit_rate 和 ttft_ms。
    # run_count = ???
    # avg_hit_rate = ???
    # avg_ttft_ms = ???
    # best_hit_rate_run = ???
    # 空列表返回空摘要,缺失字段应明确报错。
    raise NotImplementedError("请先完成 TODO 代码!")

def compare_prefix_cache_to_baseline(baseline: Dict[str, float], candidate: Dict[str, float]) -> Dict[str, float]:
    """计算 candidate 相对 baseline 的命中率、TTFT 和开销变化。

    命中率增益应为正,TTFT 和开销差值越低越好;两组结果必须使用同一 workload_id。
    结果只服务于报告比较,不能单独证明 backend 已产生真实缓存命中。"""
    # TODO 3:比较 baseline 与 candidate 的收益和代价。
    # 提示:先确认 baseline 与 candidate 的 workload_id 一致。
    # hit_rate_gain = ???
    # ttft_delta_ms = ???
    # overhead_delta = ???
    # 命中率、TTFT 和开销的差值方向要保持可解释。
    raise NotImplementedError("请先完成 TODO 代码!")

def recommend_prefix_cache_run(baseline: Dict[str, float], candidate: Dict[str, float], min_hit_rate_gain: float, max_overhead: float) -> Dict[str, object]:
    """按命中率、TTFT 收益和维护开销给出教学决策。

    min_hit_rate_gain、max_overhead 是当前 workload 的门槛,不是通用常数。
    返回 decision、reason、next_action;不能替代真实 backend 验证。"""
    # TODO 4:先得到 comparison,再使用 min_hit_rate_gain、max_overhead 判断。
    # 提示:分别判断命中率增益、TTFT 和维护开销,再输出决策。
    # hit_rate_ok = ???
    # ttft_ok = ???
    # overhead_ok = ???
    # decision = ???
    # next_action = ???
    # TODO 4 对应决策:命中率增益未达门槛时不能仅因 TTFT 改善就 accept。
    #         只有命中率、TTFT 和维护开销同时满足门槛才 accept;否则区分 tune / reject。
    #         reason 和 next_action 必须解释当前证据不足的原因与下一步。
    raise NotImplementedError("请先完成 TODO 代码!")
python
# 测试你的实现
def test_prefix_cache_benchmark_template():
    sequences = [
        [1, 2, 3, 4, 5, 6, 7, 8],
        [1, 2, 3, 4, 5, 6, 9, 10],
        [20, 21, 22, 23, 24, 25, 26, 27],
    ]
    simulated = simulate_prefix_cache(sequences, block_size=2, capacity_blocks=4)
    assert simulated['total_requests'] == 3
    assert simulated['total_prompt_tokens'] == 24
    assert simulated['reused_tokens'] == 6
    assert simulated['miss_tokens'] == 18
    assert simulated['hit_rate'] == 1 / 3
    assert simulated['token_reuse_rate'] == 6 / 24
    assert simulated['eviction_count'] > 0
    empty = simulate_prefix_cache([[]], block_size=2, capacity_blocks=1)
    assert empty['total_requests'] == 0 and empty['hit_rate'] == 0.0, "空请求应返回零计数摘要!"
    tail_case = simulate_prefix_cache([[1, 2, 3, 4, 5], [1, 2, 3, 4, 9]], block_size=4, capacity_blocks=8)
    assert tail_case['reused_tokens'] == 4 and tail_case['hit_rate'] == 0.5, "尾块不应计入命中,但完整前缀应复用!"
    print('CPU Prefix Cache simulation:', simulated)
    for invalid in ({'block_size': 0}, {'capacity_blocks': 0}):
        try:
            simulate_prefix_cache(sequences, **invalid)
        except ValueError:
            pass
        else:
            raise AssertionError('非法缓存参数应明确拒绝!')

    baseline = {'name': 'baseline', 'workload_id': 'shared-prefix-v1', 'hit_rate': 0.0, 'ttft_ms': 130, 'overhead': 0.0}
    candidate = {'name': 'cache', 'workload_id': 'shared-prefix-v1', 'hit_rate': 0.68, 'ttft_ms': 92, 'overhead': 0.08}
    summary = summarize_prefix_cache([baseline, candidate])
    assert summary['run_count'] == 2
    assert summary['best_hit_rate_run'] == 'cache'
    comparison = compare_prefix_cache_to_baseline(baseline, candidate)
    assert comparison['hit_rate_gain'] == 0.68
    assert comparison['ttft_delta_ms'] == -38
    assert comparison['overhead_delta'] == 0.08
    mismatched = dict(candidate, workload_id='other-workload')
    try:
        compare_prefix_cache_to_baseline(baseline, mismatched)
    except ValueError:
        pass
    else:
        raise AssertionError('不同 workload 不应直接比较缓存收益')
    decision = recommend_prefix_cache_run(baseline, candidate, min_hit_rate_gain=0.5, max_overhead=0.1)
    assert decision['decision'] == 'accept'
    assert decision['next_action'] == 'promote_to_serving_eval'


test_prefix_cache_benchmark_template()
print('测试通过:前缀缓存基准模板可以工作。')

🛑 STOP HERE 🛑









请先尝试自己完成代码并跑通测试。
如果你正在 Colab 中运行,并且遇到困难没有思路,可以向下滚动查看参考答案。










CPU 机制验证与结果解析 ​

CPU 代码只验证 token 前缀匹配、完整 block 复用、缓存容量和淘汰逻辑;它不代表真实 KV Cache 显存、CUDA 延迟或 backend 命中率。

参考代码与解析 ​

代码 ​

python
def simulate_prefix_cache(token_sequences: List[List[int]], block_size: int = 4, capacity_blocks: int = 8) -> Dict[str, object]:
    """模拟完整 token block 的前缀复用和 LRU 驱逐。

    `token_sequences` 是已经完成 tokenization 的请求序列;本函数只验证
    block 命中、复用和淘汰,不计算真实 KV Cache 的字节数或 backend 延迟。"""
    if block_size <= 0 or capacity_blocks <= 0:
        raise ValueError('block_size 和 capacity_blocks 必须为正数')
    if not isinstance(token_sequences, list):
        raise TypeError('token_sequences 必须是 list[list[int]]')
    if not all(isinstance(tokens, list) for tokens in token_sequences):
        raise TypeError('token_sequences 的每个元素必须是 token list')
    cache = {}
    clock = 0
    reused_tokens = 0
    total_prompt_tokens = 0
    hit_requests = 0
    eviction_count = 0
    for tokens in token_sequences:
        if not tokens:
            continue
        total_prompt_tokens += len(tokens)
        # TODO 1 对应变量 blocks:只生成完整 token block,尾块不进入命中统计。
        blocks = [tuple(tokens[start:start + block_size]) for start in range(0, len(tokens) - block_size + 1, block_size)]
        hit_blocks = 0
        for block_index, block in enumerate(blocks):
            # TODO 1 对应变量 key:位置与 token 内容共同构成 cache key。
            key = (block_index, block)
            if key not in cache:
                break
            # TODO 1 对应变量 hit_blocks:只累计从位置 0 开始的连续命中 block。
            hit_blocks += 1
            clock += 1
            cache[key] = clock
        if hit_blocks:
            hit_requests += 1
        # TODO 1 对应变量 reused_tokens:把完整命中 block 转成复用 token 数。
        reused_tokens += hit_blocks * block_size
        for block_index, block in enumerate(blocks):
            clock += 1
            cache[(block_index, block)] = clock
            while len(cache) > capacity_blocks:
                oldest = min(cache, key=cache.get)
                # TODO 1 对应变量 eviction_count:容量超限时累计被驱逐的 block。
                del cache[oldest]
                eviction_count += 1
    miss_tokens = total_prompt_tokens - reused_tokens
    request_count = len([tokens for tokens in token_sequences if tokens])
    return {
        'total_requests': request_count,
        'total_prompt_tokens': total_prompt_tokens,
        'reused_tokens': reused_tokens,
        'miss_tokens': miss_tokens,
        'hit_rate': hit_requests / request_count if request_count else 0.0,
        'token_reuse_rate': reused_tokens / total_prompt_tokens if total_prompt_tokens else 0.0,
        'prefill_work_reduction': reused_tokens / total_prompt_tokens if total_prompt_tokens else 0.0,
        'eviction_count': eviction_count,
        'cache_size_blocks': len(cache),
    }


def summarize_prefix_cache(runs: List[Dict[str, float]]) -> Dict[str, object]:
    """汇总同一请求分布下的缓存运行结果。

    每条 run 应包含 name、hit_rate 和 ttft_ms;空列表返回空摘要。
    缺失指标不能静默解释为真实的 0。"""
    if not isinstance(runs, list):
        raise TypeError('runs 必须是 list[dict]')
    required = {'name', 'hit_rate', 'ttft_ms'}
    for index, item in enumerate(runs):
        if not isinstance(item, dict) or not required.issubset(item):
            raise ValueError(f'第 {index} 条 run 必须包含 {sorted(required)}')
    # TODO 2 对应变量 run_count:统计 workload 运行次数。
    # TODO 2 对应变量 avg_hit_rate / avg_ttft_ms:计算同一口径下的两个均值。
    # TODO 2 对应变量 best_hit_rate_run:定位命中率最高的运行。
    best = None
    run_count = len(runs)
    avg_hit_rate = sum(item.get('hit_rate', 0.0) for item in runs) / run_count if run_count else 0.0
    avg_ttft_ms = sum(item.get('ttft_ms', 0.0) for item in runs) / run_count if run_count else 0.0
    for item in runs:
        if best is None or item.get('hit_rate', 0.0) > best.get('hit_rate', 0.0):
            best = item
    return {
        'run_count': run_count,
        'avg_hit_rate': avg_hit_rate,
        'best_hit_rate_run': best.get('name', 'run') if best else None,
        'avg_ttft_ms': avg_ttft_ms,
    }


def compare_prefix_cache_to_baseline(baseline: Dict[str, float], candidate: Dict[str, float]) -> Dict[str, float]:
    """计算 candidate 相对 baseline 的命中率、TTFT 和开销变化。

    命中率增益越高越好,TTFT 与开销差值越低越好;两组结果必须使用同一 workload_id。"""
    required = {'hit_rate', 'ttft_ms', 'overhead', 'workload_id'}
    for name, item in (('baseline', baseline), ('candidate', candidate)):
        if not isinstance(item, dict) or not required.issubset(item):
            raise ValueError(f'{name} 必须包含 {sorted(required)}')
    if baseline['workload_id'] != candidate['workload_id']:
        raise ValueError('baseline 和 candidate 必须使用同一 workload_id')
    # TODO 3 对应校验:先确认 baseline 与 candidate 的 workload_id 一致。
    # TODO 3 对应变量 hit_rate_gain:计算 candidate 相对 baseline 的命中率增益。
    # TODO 3 对应变量 ttft_delta_ms / overhead_delta:计算延迟和维护开销变化。
    return {
        'hit_rate_gain': candidate.get('hit_rate', 0.0) - baseline.get('hit_rate', 0.0),
        'ttft_delta_ms': candidate.get('ttft_ms', 0.0) - baseline.get('ttft_ms', 0.0),
        'overhead_delta': candidate.get('overhead', 0.0) - baseline.get('overhead', 0.0),
    }


def recommend_prefix_cache_run(baseline: Dict[str, float], candidate: Dict[str, float], min_hit_rate_gain: float, max_overhead: float) -> Dict[str, object]:
    """按命中率、TTFT 收益和维护开销给出教学决策。

    阈值属于当前 workload 的配置,不是通用常数;该函数不能替代真实 backend 验证。"""
    if not 0 <= min_hit_rate_gain <= 1:
        raise ValueError('min_hit_rate_gain 必须位于 [0, 1]')
    if max_overhead < 0:
        raise ValueError('max_overhead 不能为负数')
    # TODO 4 对应实现:先校验门槛,再输出 accept / tune / reject。
    comparison = compare_prefix_cache_to_baseline(baseline, candidate)
    hit_rate_ok = comparison['hit_rate_gain'] >= min_hit_rate_gain
    ttft_ok = comparison['ttft_delta_ms'] < 0
    overhead_ok = comparison['overhead_delta'] <= max_overhead
    if hit_rate_ok and ttft_ok and overhead_ok:
        return {'decision': 'accept', 'reason': '命中率、延迟收益和维护开销都达标', 'next_action': 'promote_to_serving_eval'}
    if hit_rate_ok and ttft_ok:
        return {'decision': 'tune', 'reason': '命中率和延迟收益可用,但维护开销仍偏高', 'next_action': 'refine_chunk_or_eviction_policy'}
    return {'decision': 'reject', 'reason': '命中率不足或延迟收益不明显', 'next_action': 'fallback_to_no_cache'}

解析 ​

这页按 simulate -> summarize -> compare -> decide 组织 prefix cache 的最小项目闭环。

1. TODO 1:模拟 Prefix Cache

  • 实现方式:将 token 序列按 block_size 切分,只把完整 block 放入缓存;查询时从位置 0 开始寻找连续命中的最长前缀。
  • 关键点:缓存 key 同时包含 block 位置和 token 内容,避免相同 token 出现在不同位置时被错误复用;容量不足时按最近最少使用顺序驱逐。
  • 技术细节:hit_rate 按请求统计,token_reuse_rate 和 prefill_work_reduction 按 token 统计;尾部不足一个 block 的 token 不进入命中统计。

2. TODO 2:汇总 workload 运行结果

  • 实现方式:汇总运行次数、平均命中率和平均 TTFT,再找出命中率最高的运行。
  • 关键点:每条记录都要包含 name、hit_rate 和 ttft_ms;空列表返回空摘要,缺失字段不能默认为实测 0。
  • 技术细节:best_hit_rate_run 用于定位优先回看的配置,不直接替代最终 benchmark 决策。

3. TODO 3:比较 baseline 与 candidate

  • 实现方式:先确认两组记录的 workload_id 一致,再计算 hit_rate_gain、ttft_delta_ms 和 overhead_delta。
  • 关键点:命中率增益使用 candidate - baseline;TTFT 和维护开销使用 candidate - baseline,正负方向要结合指标含义阅读。
  • 技术细节:这一步把缓存命中、请求延迟和维护开销放入同一个对照结果,供下一步决策函数使用。

4. TODO 4:形成项目决策

  • 实现方式:根据 hit_rate_ok、ttft_ok 和 overhead_ok,结合当前 workload 的门槛输出 accept / tune / reject。
  • 关键点:只有命中率和 TTFT 收益成立且维护开销可接受时才 accept;部分条件成立时进入 tune。
  • 技术细节:min_hit_rate_gain 和 max_overhead 是当前请求分布下的实验配置,不是所有 backend 都通用的常数。

Step 5(可选):GPU/backend 实验——真实 prefix cache 对照 ​

5.1 环境、输入与固定条件 ​

本次实验回答一个具体问题:在同一组共享前缀请求上,开启 prefix cache 是否带来可观测收益。先记录实验契约,再让 G0 与 G1 只改变 cache policy;模型、backend、请求分布、生成长度、并发和测量流程保持一致。

实验要素本轮契约G0 / G1 如何保持一致证据输出
实验对象G0 cache off;G1 prefix cache使用同一模型 revision、backend 和服务版本两组可比较结果
固定 workloadshared-prefix 请求文件、生成长度、batch / concurrency使用同一请求顺序和 token 化结果统一 workload 记录
测量流程warmup、重复次数、超时和输出长度两组使用相同流程可复查的 JSON 结果
唯一变量cache policyG0 关闭,G1 只开启 prefix cache命中、延迟、吞吐和资源对照

前缀缓存 GPU/backend 对照实验契约

5.2 环境启动检查(可独立运行) ​

先确认模型、backend、GPU、驱动、CUDA、workload 文件和服务端口能够共同启动。更换 vLLM、SGLang、GPU 或 CUDA 后,应重新完成预检和 smoke test;普通服务启动成功不代表 prefix cache 已经启用。

检查项当前记录如何解读
模型、backend、版本待填写G0/G1 必须使用同一模型和服务版本
GPU、驱动、CUDA、dtype待填写更换硬件或运行时后重新采集
workload、端口和缓存开关待填写确认 shared-prefix 请求可被服务读取
命中率证据来源backend metrics、日志或专用接口不能只用 TTFT 或吞吐推断命中率

5.3 配置实验条件 ​

配置 G0、G1 和可选 G2:G0 关闭 prefix cache,G1 只开启一种 cache policy,G2 再比较不同 backend 或缓存策略。模型、请求、生成长度、并发和 dtype 先保持一致。

5.4 执行实验并保存 JSON ​

配置代码分别启动 G0/G1,并保存 69_prefix_cache_g0_cache_off.json、69_prefix_cache_g1_prefix_cache.json 和总清单 69_prefix_cache.json。分组文件遵循 69_prefix_cache_<group>.json 命名;若 backend 没有暴露命中指标,结果中必须保留 hit_rate_evidence=backend_metrics_or_logs_required、failure_reason 或 retest_path。

5.5 实测记录与结果表 ​

读取 JSON 后,先核对实测环境与统一口径,再比较 G0/G1 的命中证据、延迟、吞吐、显存和淘汰开销。历史结果只能展示记录格式,不能替代当前硬件上的命中率或延迟证据。所有项目结果还要保留统一的 baseline/candidate、workload、质量、证据等级、失败原因和复测路径字段。

实测环境与统一口径

项目当前记录如何解读
模型、backend、服务版本待填写G0/G1 必须使用同一版本和模型 revision
GPU、驱动、PyTorch/CUDA、dtype待填写更换运行环境后重新采集,不能混用结果
workload、生成长度、并发、warmup / repeats待填写两组使用同一请求分布和测量流程
命中率证据来源backend metrics、日志或专用接口不能只用 TTFT 或吞吐推断命中率

统一项目证据字段

公共字段记录内容本节要求
project / role项目编号、baseline 或 candidateG0/G1/G2 的角色和 cache policy 明确可追溯
workload / config模型、backend、dtype、prompt、generated tokens、batch、concurrency、seed、repeatG0 与 candidate 使用同一 workload
metricsTTFT、TPOT、throughput、P95/P99、peak memory、queue wait缺失项写 not_available,不能由延迟推断 hit rate
strategy_metricsacceptance / hit rate、reused tokens、transfer / handoff cost本节至少记录 hit rate 及证据来源
quality / status输出质量、运行状态、失败原因记录 ok、unsupported、OOM 或 failed
evidence_level / decision证据等级、accept / tune / reject、复测路径结果不足时进入 tune,不能静默通过

主线结果与复测记录

| 实验组 / role | workload / config | model / backend / dtype | prompt / generated tokens | batch / concurrency | cache policy | hit rate / evidence | reused tokens | TTFT | TPOT | throughput / P95 / P99 | peak memory / queue wait | transfer / handoff | quality / status | evidence level | decision | |---|---|---😐---😐---|---|---😐---😐---😐---😐---😐---😐---😐---|---| | G0 / baseline | 固定 workload / cache off | 待填写 | 待填写 | 1 / 1 | off | 不适用 | 0 | 待采集 | 待采集 | 待采集 / 待采集 / 待采集 | 待采集 / 待采集 | 不适用 | 待填写 / ok | gpu_baseline_smoke | 待判断 | | G1 / candidate | 固定 workload / prefix cache | 待填写 | 待填写 | 1 / 1 | prefix cache | 待采集 / metrics 或日志 | 待采集 | 待采集 | 待采集 | 待采集 / 待采集 / 待采集 | 待采集 / 待采集 | 待采集 | 待填写 / ok | real_backend_smoke | 待判断 | | G2 / candidate | 固定 workload / 单变量变化 | 待填写 | 待填写 | 待填写 | 待填写 | 待采集 / 证据来源 | 待采集 | 待采集 | 待采集 | 待采集 / 待采集 / 待采集 | 待采集 / 待采集 | 待采集 | 待填写 / ok | real_backend_smoke | 待判断 |

5.6 结果解释与项目决策 ​

vLLM / SGLang 的 prefix-cache 开关、服务版本和 cache policy 可能不同;决策时同时查看命中率证据、TTFT、吞吐、显存和失效开销。只有证据充分时才输出 accept / tune / reject。

python
try:
    from tools.inference_project_runtime import locate_repo_root
    REPO_ROOT = locate_repo_root()
    from tools.inference_project_runtime import (
        shared_project_config, runtime_snapshot, start_optional_vllm,
        stop_optional_vllm, run_backend_benchmark,
    )
except ModuleNotFoundError:
    RUN_REAL_BACKEND = False
    def shared_project_config(**kwargs): return kwargs
    def save_project_result(*args, **kwargs): raise RuntimeError('需要从仓库根目录运行真实 backend 入口')

import json
from pathlib import Path
MODEL_ID = 'Qwen/Qwen2.5-0.5B-Instruct'  # G0/G1 使用同一基座模型。
WORKLOAD = 'benchmarks/workloads/prefix_reuse.jsonl'  # 共享前缀、变化后缀的请求分布。
RESULT_PATH = 'benchmarks/results/69_prefix_cache.json'  # 保存对照清单,不覆盖分组结果。
NUM_PROMPTS = 5  # smoke 规模;正式结论应提高请求数并重复运行。
CONCURRENCY = 1  # G0/G1 必须保持一致;并发扫描另存为独立实验。
WARMUP = 1
GROUPS = (
    {'name': 'g0_cache_off', 'cache_policy': 'off', 'enabled': False},
    {'name': 'g1_prefix_cache', 'cache_policy': 'prefix_cache', 'enabled': True},
)
project_config = shared_project_config(
    model=MODEL_ID, backend='vllm', dtype='auto', generated_tokens=64,
    workload=WORKLOAD, num_prompts=NUM_PROMPTS, concurrency=CONCURRENCY,
    warmup=WARMUP, groups=[item['name'] for item in GROUPS],
)
print(project_config)
RUN_REAL_BACKEND = False  # 改为 True 才启动 G0/G1;默认不下载模型、不占用 GPU。
if RUN_REAL_BACKEND:
    reports = []
    for group in GROUPS:
        result_path = f"benchmarks/results/69_prefix_cache_{group['name']}.json"
        server, log_path, port, selected_dtype, model_path = start_optional_vllm(
            model_id=MODEL_ID, model_source='auto', dtype='auto',
            served_model_name=MODEL_ID, enable_prefix_caching=group['enabled'],
        )
        try:
            report = run_backend_benchmark(
                project='69', base_url=f'http://127.0.0.1:{port}', model=MODEL_ID,
                label=f"vllm-{group['name']}", output=result_path, workload=WORKLOAD,
                backend='vllm', batch=1, max_tokens=64,
                num_prompts=NUM_PROMPTS, concurrency=CONCURRENCY, warmup=WARMUP,
                dtype=selected_dtype, cache_policy=group['cache_policy'],
            )
            normalized = report.get('normalized_result') or {}
            reports.append({
                'group': group,
                'report_path': result_path,
                'status': report.get('status', 'ok'),
                'quality': report.get('quality', {'status': 'not_evaluated'}),
                'evidence_level': 'real_backend_smoke',
                'normalized_result': normalized,
            })
            print(reports[-1])
        finally:
            stop_optional_vllm(server, log_path)
    def metric_value(report, metric, percentile='p50'):
        values = ((report.get('normalized_result') or {}).get('metrics') or {}).get(metric)
        if isinstance(values, dict):
            return values.get(percentile)
        return values

    by_group = {item['group']['name']: item for item in reports}
    g0 = by_group.get('g0_cache_off', {})
    g1 = by_group.get('g1_prefix_cache', {})
    paired_summary = {
        'g0_ttft_p50_ms': metric_value(g0, 'ttft_ms'),
        'g1_ttft_p50_ms': metric_value(g1, 'ttft_ms'),
        'g0_throughput_tokens_per_s': metric_value(g0, 'throughput_tokens_per_s', None),
        'g1_throughput_tokens_per_s': metric_value(g1, 'throughput_tokens_per_s', None),
        'hit_rate': None,
        'hit_rate_evidence': 'backend_metrics_or_logs_required',
        'quality': {'status': 'not_evaluated', 'reason': 'prefix-cache 命中率尚未取得 backend 指标'},
        'status': 'ok' if all(item.get('status') == 'ok' for item in reports) else 'failed',
        'evidence_level': 'real_backend_smoke',
        'decision': 'tune',
        'reason': '没有直接命中率证据,不能仅凭 TTFT 或吞吐下结论。',
    }
    manifest_path = Path(RESULT_PATH)
    manifest_path.parent.mkdir(parents=True, exist_ok=True)
    manifest_path.write_text(json.dumps({
        'project': '69', 'config': project_config,
        'environment': runtime_snapshot(),
        'groups': reports,
        'paired_summary': paired_summary,
        'hit_rate_evidence': 'backend_metrics_or_logs_required',
    }, ensure_ascii=False, indent=2), encoding='utf-8')
    print(f'对照清单已保存: {manifest_path}')

相关阅读 ​

完成前缀缓存 benchmark 后,可用 70 继续检查缓存命中与请求调度之间的关系,并保持相同的 workload 记录方式。下面的实现文档和论文用于继续追踪真实系统中的缓存组织、命中和显存管理。

注意:OpenAI-compatible API 通常不会直接返回 prefix-cache hit rate。真实 backend 实验可以用共享前缀 workload 比较 TTFT 和吞吐,但不能仅凭延迟变化反推出命中率;命中率必须来自 backend metrics、日志或专用观测接口。

Released under the MIT License.