Prefix Cache 实验附录:环境、复现与详细统计
本文是博客正文的配套材料。实验数据采集于 2026-10-11、2026-10-13,附录整理于 2026-10-14。完整脚本、操作记录和原始结果打包为实验附件;解压后保留experiments/目录结构。下文的证据路径均相对于附件根目录。
A. 环境与配置
平台为 HiDevLab A3。机制实验和两轮性能实验都只使用可见逻辑设备0,TP=1、PP=1;npu-smi原始型号为Ascend910,每芯片 HBM 总容量显示为65536 MB。这不是实验实际峰值占用,也不据此推断 910B/910C 等子型号。
10 月 13 日两轮性能实验的 14 份前后环境快照中,驱动和 Toolkit 版本文件内容一致:
driver/version.info: Version=26.1.1 package_version=26.1.1 Innerversion=V100R001C10SPC009B220 ascendhal_version=7.35.23 ascend_toolkit_install.info: version=9.1.0 innerversion=V100R001C11SPC001B24310 月 11 日机制实验的npu-smi也报告版本26.1.1,但未单独保存 Toolkit 安装版本文件。实际固件版本未采集;驱动文件中的固件兼容范围不是已安装固件的版本。
环境原始记录:
- 机制:
experiments/prefix-cache-01/evidence/prefix_cache_20261011_02/environment.json - 原 1K 性能:
experiments/prefix-cache-perf-01/evidence/prefix_cache_perf_20261013_01/environment_before.json,以及 02、03 的前后快照。 - 长度扫描:
experiments/prefix-cache-perf-length-01/evidence/prefix_cache_perf_length_20261013_01/environment_before.json,以及总实验和各run_01~run_03的前后快照。
| 项目 | 记录 |
|---|---|
| Python | 3.12.13 |
| PyTorch / torch-npu | 2.10.0+cpu / 2.10.0.post4 |
| vLLM / vLLM-Ascend | 0.26.0+empty / 0.26.0rc1 |
| Transformers | 5.14.1 |
| 模型 | 本地 Qwen3-0.6B;模型与 tokenizer 的不可变上游 revision 未知,原始环境记录保存了文件 SHA256 |
| 运行 vLLM | 568afb3a13806beb53bb2e6bd518269357b237c0 |
| 运行 vLLM-Ascend | f2f74a16c3c50a76f4349d807918e83edec1e35c |
| 公共执行条件 | BF16、Eager、同步调度、并发 1、贪心采样、忽略 EOS、块大小 128、Chunked Prefill 开启 |
三轮配置不能混为一组:
| 实验 | B 输入 / 输出 token | 长度上限 / token 预算 | 缓存条件与重复 |
|---|---|---|---|
| 机制对照 | 1038 / 3 | 2048 / 2048 | OFF、COLD、A→WARM、A→修改前缀 |
| 原 1K 性能 | 1038 / 64 | 2048 / 2048 | Prefix Cache 均开启;3 次启动,各 3 对预热 + 8 对正式冷/热 |
| 长度性能 | 1038、4110、8206、16398 / 64 | 18432 / 18432 | 3 次启动,每长度各 3 对预热 + 8 对正式冷/热 |
长度扫描里的 1K 是重新测量的结果,没有与原 2048 配置的 1K 混算。
B. 复现实验与证据
完整执行过程保存在三组实验各自的execution-walkthrough.md,包括输入构造、实际命令、启动问题及修复、结果字段和源码对应。下面提炼能复现实验设计的步骤;实验由助手执行,原记录保留了执行者与学习验收的区分。
B.1 机制:先确认命中与输出
在具有上述软件环境的独立实验目录中,使用附件的run_prefix_cache.py。加载现有 Toolkit/ATB 环境,指定可见设备 0;原实验模型位置为/workspace/models/Qwen3-0.6B,复现时按自己的模型位置替换:
source/usr/local/Ascend/ascend-toolkit/set_env.shsource/usr/local/Ascend/nnal/atb/set_env.shexportPATH=/usr/local/python3.12.13/bin:$PATHexportLD_LIBRARY_PATH=/usr/local/Ascend/driver/lib64/common:/usr/local/Ascend/driver/lib64/driver:$LD_LIBRARY_PATHexportASCEND_RT_VISIBLE_DEVICES=0exportHF_HUB_OFFLINE=1exportTRANSFORMERS_OFFLINE=1python3.12-urun_prefix_cache.py\--model/workspace/models/Qwen3-0.6B--modeoff --output-dir off python3.12-urun_prefix_cache.py\--model/workspace/models/Qwen3-0.6B--modeon --output-dir on实际包装脚本为experiments/prefix-cache-01/run_a3.sh。ON 脚本内部按“reset→B”“reset→A→B”“reset→A→B′”顺序执行;B′ 修改下标 17 的 token。运行结果与输入保存在experiments/prefix-cache-01/evidence/prefix_cache_20261011_02/:
on/inputs.json:固定的输入 IDs、共同前缀与修改位置。on/05_WARM_B.json:B 输入 1038、命中 1024、输出[73670, 65770, 16872]。on/08_PREFIX_CHANGED_Bprime.json:修改前缀后命中 0。off/:B 与 B′ 各自关闭缓存的输出参照。off.log、on.log、summary.json:运行日志与核对结果。
B.2 性能:固定输入与输出,再改变缓存状态
原 1K 性能脚本读取上一步保存的on/inputs.json,复制到新实验目录并命名为inputs.json。同目录还需run_prefix_cache_perf.py;完整环境采集与日志包装见该组run_a3.sh。
python3.12-urun_prefix_cache_perf.py\--model/workspace/models/Qwen3-0.6B--inputsinputs.json\--output-dir results --output-tokens64--pairs8--warmup-pairs3在三个新目录中分别启动引擎,共三次。每对冷、热都先运行 A;冷组在 A 完成后再次清缓存,热组保留缓存。B 的计时从add_request前开始,到step()返回后取时间。A、reset、模型加载和分词均不计入 B。
长输入实验同样使用这份基础输入,另需run_length_sweep.py与perf_common.py:
python3.12-urun_length_sweep.py\--model/workspace/models/Qwen3-0.6B--inputsinputs.json\--output-dir run_01/results --run-index1\--max-model-len18432--output-tokens64--pairs8--warmup-pairs3再分别使用run_02/results --run-index 2和run_03/results --run-index 3独立启动。脚本按 run-index 改变四种长度的顺序,每个长度内部交替冷、热顺序。每次都执行全部预定样本,没有只留下热缓存更快的请求。
原 1K 的三组结果在experiments/prefix-cache-perf-01/evidence/prefix_cache_perf_20261013_01/~03/;每组results/保存 A、B、reset、原始输出事件与 summary。长度扫描结果在experiments/prefix-cache-perf-length-01/evidence/prefix_cache_perf_length_20261013_01/;其下run_01~run_03/results/length_1024等目录按启动与长度分组。
B.3 不连接 NPU,也能复算保存的数据
解压实验附件,在附件根目录使用 Python 3.9 或以上运行以下标准库分析程序。输出目录需尚未存在,避免覆盖原统计:
python experiments/prefix-cache-01/validate_results.py\experiments/prefix-cache-01/evidence/prefix_cache_20261011_02 python experiments/prefix-cache-perf-01/analyze_results.py\--output-dir recomputed-1k python experiments/prefix-cache-perf-length-01/analyze_length_results.py\--output-dir recomputed-length两份measurements.csv都保留预热与正式请求;分析以phase区分,只用正式样本计算正文统计。analysis.json保存按启动、长度和配对的完整结果。原始事件可用于重算 TTFT、首到末输出耗时、TPOT 与请求总耗时;这些事件不是 Scheduler 逐轮任务。
C. 第 7 章的详细统计
以下是原 2048 配置、每次 8 对正式请求的分组中位数,单位为 ms:
| 独立运行 | 冷 TTFT | 热 TTFT | 冷首到末输出 | 热首到末输出 | 冷总耗时 | 热总耗时 |
|---|---|---|---|---|---|---|
| 01 | 38.29 | 38.52 | 2157.08 | 2179.16 | 2195.37 | 2217.57 |
| 02 | 38.55 | 37.42 | 2109.98 | 2116.82 | 2150.13 | 2154.59 |
| 03 | 38.51 | 38.21 | 2136.76 | 2141.44 | 2175.07 | 2179.84 |
再对每一对计算“冷耗时 − 热耗时”;正数表示这一对热组更快:
| 独立运行 | TTFT 配对差中位数 | 配对差最小~最大 | 热 TTFT 更快对数 | 总耗时配对差中位数 | 热总耗时更快对数 |
|---|---|---|---|---|---|
| 01 | +0.16 ms | −3.78~+2.57 ms | 4 / 8 | −6.02 ms | 3 / 8 |
| 02 | +0.74 ms | −0.73~+3.32 ms | 7 / 8 | +15.55 ms | 6 / 8 |
| 03 | +0.14 ms | −0.75~+0.84 ms | 4 / 8 | −4.86 ms | 2 / 8 |
“逐对相减后取中位数”与“两个组的中位数相减”不是同一统计量。因此,第二次多数配对的总耗时偏向热组,与第一张表的组中位数方向不同,并不矛盾。三次合看,未出现跨启动一致的整请求改善。
完整精度、范围和各对差值保存在附件的experiments/prefix-cache-perf-01/analysis.json;长度扫描的同类统计保存在experiments/prefix-cache-perf-length-01/analysis.json。原审计分别通过 1711 项与 87215 项检查;检查数表示数据校验,不是性能提升或统计显著性的证据。
长度实验正文中的“6.74 倍”是 16K 冷/热 TTFT组中位数之比;配对比值中位数约为 6.73,两个口径不能混用。各长度的 24 对来自 3 次引擎启动,不代表 24 个独立部署。
D. 固定版本的源码入口
正文第 5 章按本地阅读提交解释,以下链接固定到公开仓库的同一提交,不随主分支漂移。它们是静态阅读依据,不能替代未采集的 Scheduler 或设备 trace。
| 要找的问题 | 本地阅读版本的固定源码 |
|---|---|
| 请求保存块哈希 | Request.update_block_hashes,request.py:278 |
| 完整块生成链式哈希 | get_request_block_hasher,kv_cache_utils.py:796;哈希组合式,第 648 行 |
| 将完整块登记到索引 | cache_full_blocks,block_pool.py:225;插入索引,第 638 行 |
| 允许查找与查找入口 | prefix_cache_lookup_enabled,第 230 行;get_computed_blocks,第 245 行 |
| 从第一个块连续查找 | find_longest_cache_hit,single_type_kv_cache_manager.py:771;实际查索引,block_pool.py:217 |
| Scheduler 接收新请求命中 | scheduler.py:914 |
| 计算待算量与预算裁剪 | scheduler.py:1036;第 1076 行 |
| 申请块、写入任务单 | scheduler.py:1155;第 1257 行 |
计时与 Ascend 执行分支同时提供阅读版、运行版的对应入口:
| 位置 | 本地阅读版本 | 远程运行版本 |
|---|---|---|
| 提交请求 | llm_engine.py:223 | llm_engine.py:218 |
| 获取并整理结果 | llm_engine.py:312 | llm_engine.py:304 |
| 缓存命中路径取 KV、块表和上下文长度 | attention_v1.py:817 | attention_v1.py:1238 |
| 截取已准备的有效 query | attention_v1.py:855 | attention_v1.py:1294 |
有效 query 截取发生在调度决定待算输入之后;它自身不负责决定哪些 token 需要重新计算。静态代码能说明热请求仍使用历史上下文,但本实验没有 profiler,不能将热 TTFT 的增长全部归因于读取 KV。