LLM推理优化实战(四):Prefix Caching原理详解与TTFT性能实测
一、前言前两篇文章分别建立了 BF16 基线和 AWQ INT4 量化实验也验证了 KV Cache 是高并发场景下的显存瓶颈以及 AWQ 量化通过释放 KV Cache 空间实现吞吐翻倍的根本原因。本文在此基础上继续探索另一个常见但容易被忽视的推理优化手段——Prefix Caching。在实际生产中大量请求共享相同的 system prompt 或上下文前缀而这部分 KV 每次都被重复计算。Prefix Caching 的思路是把这部分计算结果缓存下来跨请求复用直接减少 Prefill 开销降低 TTFT。本文通过两个实验回答以下问题Prefix Caching 能将 TTFT 降低多少命中率如何共享前缀越长收益是否线性增长上限在哪里先看结论实验关键发现实验一开关对比~1500 tokens 前缀TTFT 降低 18.5%命中率 99.2%实验二前缀长度梯度3533961 tokensTTFT 降幅从 67.1% 增长至 92.6%斜率下降 96.7%最重要的洞察Prefix Caching 的收益与共享前缀长度高度相关——前缀越长可复用的 Prefill 计算越多TTFT 降幅越大。在 RAG、Agent、长 System Prompt 等典型生产场景中这项默认开启的优化往往能将首 Token 延迟压缩 80% 以上。二、Prefix Caching2.1 什么是 Prefix Caching在实际生产环境中大量请求往往共享相同的前缀。比如一个客服机器人所有用户的请求都会带上同一段 system prompt请求 A[你是一个专业的客服助手请遵循以下规则...] [我的订单什么时候发货] 请求 B[你是一个专业的客服助手请遵循以下规则...] [如何申请退款] 请求 C[你是一个专业的客服助手请遵循以下规则...] [你们的营业时间是] ↑ 完全相同的前缀 ↑ 不同的用户输入没有 Prefix Caching 时每个请求都要在 Prefill 阶段重新计算一遍 system prompt 的 KV这是纯粹的重复计算。Prefix Caching 的思路很简单把首次计算好的前缀 KV Cache 保留下来后续遇到相同前缀时直接复用跳过重复的 Prefill 计算。2.2 工作原理整个过程分为三步第一步首次请求计算并缓存请求 A 到达时vLLM 正常执行 Prefill计算所有 token 的K KK、V VV并将前缀部分的 Block计算 hash 值后标记为可复用存入缓存池请求 A Prefill [system prompt: 1488 tokens] [用户问题: 16 tokens] → 计算全部 1504 tokens 的 KV → system prompt 的 93 个 Block 被标记 hash存入缓存第二步后续请求命中缓存请求 B 到达时vLLM 对前缀逐 Block 计算 hash发现与缓存匹配直接跳过这部分 Prefill请求 B Prefill [system prompt: 1488 tokens] → hash 匹配复用已有的 93 个 Block [用户问题: 13 tokens] → 只需计算这 13 个 token 的 KV第三步缓存保留与淘汰请求结束后普通 KV Cache 被释放但 prefix Block 保留在缓存池中等待后续请求复用。当空间不足时按 LRU 策略淘汰最久未使用的 prefix 缓存。2.3 Prefix Cache 在 KV Cache 中的位置回顾 PagedAttention 的 Block 结构Prefix Cache 覆盖的是 KV Cache 中前缀对应的那些 Block一个完整请求的 KV Cache 布局block_size16 Block 0 ~ Block 92system prompt 的 KV1488 tokens← 缓存可跨请求复用 Block 93 ~ Block 94用户问题的 KV16 tokens ← 每个请求独有 Block 95 ~ ... 生成输出的 KV逐步增长 ← 每个请求独有当多个请求共享同一 system prompt 时它们的 Block 0~92指向同一组物理 Block不会重复存储请求 A[Block 0~92共享] → [Block 100~102独有] 请求 B[Block 0~92共享] → [Block 200~203独有] 请求 C[Block 0~92共享] → [Block 300~301独有] ↑ 只存一份节省了 2 × 93 186 个 Block 的显存2.4 适用场景Prefix Caching 的收益取决于共享前缀的长度和请求的重复率。场景共享前缀前缀长度收益客服 / 助手类应用固定 system prompt几百几千 tokens高几乎所有请求命中RAG相同检索文档几千 tokens中高同一文档的多个问题命中代码补全同一文件上下文几百几千 tokens中同一文件的连续补全命中多轮对话历史对话记录逐轮增长中每轮新增部分无法命中随机独立请求无共享前缀0无收益收益最大的场景是长 system prompt 大量短问题——前缀越长跳过的 Prefill 计算越多TTFT 节省越明显。vLLM 默认开启 Prefix Cachingenable_prefix_cachingTrue无需额外配置。三、实验一Prefix Caching 开关对比3.1 实验设计目标通过控制变量量化 Prefix Caching 对 TTFT 的影响。项目设置模型Qwen2.5-7B-InstructSystem Prompt约 1488 tokens固定规则说明重复 20 次用户问题8 个不同的历史问题输出长度64 tokens对照组开启 / 关闭 Prefix Caching观测指标TTFT启动命令# 开启 Prefix CachingvLLM 默认vllm serve /root/autodl-tmp/models/qwen2.5-7b-instruct\--served-model-name qwen2.5-7b\--gpu-memory-utilization0.85--max-model-len4096# 关闭 Prefix Cachingvllm serve /root/autodl-tmp/models/qwen2.5-7b-instruct\--served-model-name qwen2.5-7b\--gpu-memory-utilization0.85--max-model-len4096\--no-enable-prefix-caching3.2 测试脚本脚本的核心逻辑是顺序发送 8 个请求每次携带完全相同的 system prompt记录从发出请求到收到完整响应的时间即 TTFT。同时从/metrics接口读取prefix_cache_queries_total和prefix_cache_hits_total两个计数器计算命中率。实验开始前先读取一次初始计数结束后取差值排除历史请求的干扰。# prefix_cache_test.pyimportrequestsimporttime SYSTEM_PROMPT你是一个专业的AI助手。你需要遵循以下规则 1. 回答必须准确、客观、有依据 2. 如果不确定要明确说明 3. 回答要结构化使用适当的标题和段落 4. 对于技术问题要给出代码示例 5. 对于历史问题要注明时间和来源 *20# 重复 20 次约 1488 tokensdefsend_request(question):starttime.time()rrequests.post(http://localhost:8000/v1/chat/completions,json{model:qwen2.5-7b,messages:[{role:system,content:SYSTEM_PROMPT},{role:user,content:question}],max_tokens:64,temperature:0})elapsedtime.time()-start datar.json()returnelapsed*1000,data[usage][prompt_tokens]defget_prefix_cache_stats():rrequests.get(http://localhost:8000/metrics)queries,hits0.0,0.0forlineinr.text.split(\n):ifline.startswith(vllm:prefix_cache_queries_total{):queriesfloat(line.split(} )[1])ifline.startswith(vllm:prefix_cache_hits_total{):hitsfloat(line.split(} )[1])returnqueries,hits questions[秦始皇统一六国的过程是怎样的,汉武帝的主要功绩有哪些,唐朝的开元盛世是怎么回事,宋朝的经济发展有哪些特点,明朝的海禁政策是什么,清朝的洋务运动取得了哪些成果,辛亥革命的意义是什么,五四运动的背景和影响,]init_queries,init_hitsget_prefix_cache_stats()fori,qinenumerate(questions):elapsed,tokenssend_request(q)print(f{i1:4}{q[:18]:20}{elapsed:10.1f}ms{tokens:6}tokens)final_queries,final_hitsget_prefix_cache_stats()total_new_queriesfinal_queries-init_queries total_new_hitsfinal_hits-init_hits hit_ratetotal_new_hits/total_new_queries*100iftotal_new_queries0else0print(f\n命中率:{hit_rate:.1f}%节省{total_new_hits:.0f}tokens 的 KV 计算)3.3 实验结果组一开启 Prefix CachingvLLM 默认序号问题TTFT (ms)Prompt Tokens1秦始皇统一六国的过程是怎样的1300.215042汉武帝的主要功绩有哪些1269.315013唐朝的开元盛世是怎么回事1269.915004宋朝的经济发展有哪些特点1271.615005明朝的海禁政策是什么1275.215006清朝的洋务运动取得了哪些成果1275.515027辛亥革命的意义是什么1275.514998五四运动的背景和影响1276.51500Prefix Cache 命中率99.2%节省 11904 tokens 的 KV 计算。组二关闭 Prefix Caching序号问题TTFT (ms)Prompt Tokens1秦始皇统一六国的过程是怎样的1666.415042汉武帝的主要功绩有哪些1561.115013唐朝的开元盛世是怎么回事1560.115004宋朝的经济发展有哪些特点1565.015005明朝的海禁政策是什么1566.915006清朝的洋务运动取得了哪些成果1565.815027辛亥革命的意义是什么1561.314998五四运动的背景和影响1556.51500Prefix Cache 命中率0%每次 Prefill 都重新计算全部 1500 tokens 的 KV。3.4 结果分析去掉第一个请求后对比第 2~8 个请求的平均 TTFT配置第 2~8 次平均 TTFT差值降幅关闭 Prefix Cache1562 ms——开启 Prefix Cache1274 ms-288 ms-18.5%为什么第一个请求有差异关闭 cache 时第 1 次1666ms比后续1562ms慢约 100ms是 CUDA kernel 首次编译的预热开销与 prefix cache 无关。开启 cache 时第 1 次1300ms比关闭时快很多原因是实验开始前已积累了 10416 次命中记录——同一 system prompt 已在历史请求中被缓存第一个请求同样命中了缓存没有出现冷启动延迟。为什么 TTFT 没有降低 99%每次请求中1488 tokens 的 system prompt 命中缓存只有约 12~16 tokens 的用户问题需要重新计算 KV跳过的 Prefill 比例约为1488 1504 ≈ 98.9 % \frac{1488}{1504} \approx 98.9\%15041488≈98.9%但 TTFT 不只是 Prefill还包含排队等待和 Decode 第一个 token 的固定开销TTFT 排队等待 Prefill Decode 第 1 token 关闭 cache1562 ms 排队 1500 tokens Prefill decode 开启 cache1274 ms 排队 12 tokens Prefill decode 差值 288 ms ≈ 1488 tokens 的 Prefill 计算时间节省的 288ms 就是 1488 tokens Prefill 的时间剩余的 ~1274ms 是排队和 decode 第一个 token 的固定开销Prefix Caching 无法优化这部分。Prefix Caching 的收益随前缀长度线性增长。本实验的 system prompt 约 1488 tokensTTFT 降低 288ms。如果前缀更长如 RAG 场景下的 4000 tokens 检索文档节省的时间会等比例增加TTFT 降幅可达 50% 以上。这正是下一个实验的验证目标。四、实验二前缀长度对收益的影响4.1 实验设计实验一验证了 Prefix Caching 的有效性但没有回答Prefix 越长收益是否线性增长为此构造 5 种不同长度的 system prompt分别在开启和关闭 Prefix Cache 两种配置下测量 TTFT。项目设置模型Qwen2.5-7B-InstructPrefix 长度256 / 512 / 1024 / 2048 / 3072 tokens输出长度1 token压缩 Decode 开销使 TTFT ≈ Prefill 时间用户问题固定约 15 tokens每组重复次数5取均值设置max_tokens1是为了尽量隔离 Decode 阶段的影响使测量结果主要反映 Prefill 开销的变化。4.2 测试脚本system prompt 通过重复同一段约 64 tokens 的规则说明来构造用重复次数控制前缀长度。对于开启 Prefix Cache 的实验每种前缀长度首先发送一次冷启动请求构建缓存随后连续发 5 次并取均值对于关闭 Prefix Cache 的实验每次都是完整 Prefill。# prefix_length_exp.pyimportargparseimportjsonimporttimefrompathlibimportPathfromstatisticsimportmeanimportrequests BASE_URLhttp://localhost:8000MODEL_NAMEqwen2.5-7bRESULTS_DIRPath(/root/autodl-tmp/results/prefix_length_exp)BASE_UNIT(你是一个专业的AI助手。你需要遵循以下规则\n1. 回答必须准确、客观、有依据。\n2. 如果不确定要明确说明不确定的原因。\n3. 回答要结构化使用适当的标题和段落。\n4. 对于技术问题要给出可运行的代码示例。\n5. 对于历史问题要注明具体时间节点和史料来源。\n)TOKENS_PER_UNIT64TARGET_PREFIX_TOKENS[256,512,1024,2048,3072]USER_QUESTION请简要介绍一下汉武帝的主要历史功绩。REPEAT_PER_LENGTH5defbuild_system_prompt(target_tokens:int)-str:repeatmax(1,round(target_tokens/TOKENS_PER_UNIT))returnBASE_UNIT*repeatdefsend_request(system_prompt:str)-tuple[float,int]:starttime.perf_counter()resprequests.post(f{BASE_URL}/v1/chat/completions,json{model:MODEL_NAME,messages:[{role:system,content:system_prompt},{role:user,content:USER_QUESTION},],max_tokens:1,temperature:0,},timeout60,)elapsed_ms(time.perf_counter()-start)*1000prompt_tokensresp.json()[usage][prompt_tokens]returnelapsed_ms,prompt_tokensdefwarmup():print(预热中...)send_request(你好)time.sleep(1)print(预热完成。\n)defrun_experiment(mode:str):RESULTS_DIR.mkdir(parentsTrue,exist_okTrue)warmup()results[]fortargetinTARGET_PREFIX_TOKENS:system_promptbuild_system_prompt(target)print(f 前缀目标{target}tokens )ifmodecache_on:ttft,ptsend_request(system_prompt)print(f [冷启动] TTFT{ttft:.1f}ms prompt_tokens{pt})ttfts[]foriinrange(REPEAT_PER_LENGTH):ttft,ptsend_request(system_prompt)ttfts.append(ttft)print(f [第{i1}次] TTFT{ttft:.1f}ms prompt_tokens{pt})time.sleep(0.3)avgmean(ttfts)record{target_prefix_tokens:target,mode:mode,avg_ttft_ms:round(avg,1),min_ttft_ms:round(min(ttfts),1),max_ttft_ms:round(max(ttfts),1),}results.append(record)print(f → 平均 TTFT:{avg:.1f}ms\n)out_pathRESULTS_DIR/fresults_{mode}.jsonwithopen(out_path,w,encodingutf-8)asf:json.dump(results,f,ensure_asciiFalse,indent2)print(f结果已保存至{out_path})if__name____main__:parserargparse.ArgumentParser()parser.add_argument(--cache,choices[on,off],requiredTrue)argsparser.parse_args()run_experiment(cache_onifargs.cacheonelsecache_off)4.3 实验结果Prompt Tokens关闭 Cache TTFT (ms)开启 Cache TTFT (ms)节省 (ms)降幅353111.436.774.767.1%681168.743.0125.774.5%1337279.445.1234.383.9%2649574.056.1517.990.2%3961852.762.8789.992.6%图 (a) 展示了 TTFT 随 Prompt Tokens 的变化趋势。关闭 Prefix Cache 时蓝色实线TTFT 从 353 tokens 下的 111ms 线性增长至 3961 tokens 下的 853ms绿色虚线拟合斜率为 207.6 ms/k tokens线性关系拟合极好说明 Prefill 计算量与输入长度严格正比。开启 Prefix Cache 后橙色实线TTFT 几乎没有随前缀长度变化始终维持在 37~63ms 的低位粉色虚线拟合斜率仅 6.9 ms/k tokens两条曲线的差距随前缀增长持续扩大。图 (b) 展示了 TTFT 降幅随前缀长度的变化。在 353 tokens 时降幅已达 67.1%随前缀增长单调递增到 3961 tokens 时达到 92.6%。降幅的持续增长反映了一个事实前缀越长被命中缓存省掉的计算量越大而不可优化的固定开销排队 decode 第一个 token在 TTFT 中的占比越来越低。4.4 线性拟合分析对实验数据进行线性回归以L LL单位千 tokens为自变量关闭 Prefix CacheT T F T off 207.6 × L 38.1 (ms) TTFT_{\text{off}} 207.6 \times L 38.1 \quad \text{(ms)}TTFToff207.6×L38.1(ms)每增加 1000 tokens 的前缀TTFT 平均增加约 207.6 ms。开启 Prefix CacheT T F T on 6.9 × L 34.3 (ms) TTFT_{\text{on}} 6.9 \times L 34.3 \quad \text{(ms)}TTFTon6.9×L34.3(ms)每增加 1000 tokens 的前缀TTFT 仅增加约 6.9 ms。斜率下降幅度1 − 6.9 207.6 96.7 % 1 - \frac{6.9}{207.6} 96.7\%1−207.66.996.7%原理很直接设共享前缀长度为N NN用户输入长度为U UU。关闭 Cache 时 Prefill 开销正比于N U NUNU开启 Cache 后只需计算U UU部分N NN已完全命中缓存。因此关闭 Cache 的 TTFT 随N NN线性增长而开启 Cache 的 TTFT 对N NN几乎不敏感。两条拟合直线的截距接近38.1 ms vs 34.3 ms反映的正是那部分无法被 Prefix Caching 优化的固定开销——排队等待和 decode 第一个 token 的时间在两种配置下基本相同。五、总结5.1 本文完成的工作内容结果Prefix Caching 原理解析✅ 完成hash 机制、Block 复用、LRU 淘汰实验一开关对比~1500 tokens 前缀✅ TTFT 降低 18.5%命中率 99.2%实验二前缀长度梯度实验✅ 验证线性关系4000 tokens 前缀下降幅 92.6%线性拟合分析✅ OFF 斜率 207.6 ms/k tokON 斜率 6.9 ms/k tok5.2 关键结论结论 1Prefix Caching 的收益与前缀长度高度相关关闭 Prefix Cache 时TTFT 随前缀长度近似线性增长207.6 ms / 千 tokens开启后增长斜率下降96.7%仅 6.9 ms / 千 tokens。在约 4000 tokens 的共享前缀下TTFT 降幅达到92.6%。结论 2TTFT 降幅存在上限Prefix Caching 只能优化 Prefill 阶段无法消除排队等待和 decode 第一个 token 的固定开销。两条拟合线的截距~35ms揭示了这个不可优化的下界。在生产环境中系统负载越高、排队时间越长Prefix Caching 对 TTFT 的相对贡献也会相应缩小。结论 3长上下文场景是 Prefix Caching 的主战场System Prompt ~350 tokens TTFT 降幅 67.1% RAG 上下文 ~4000 tokens TTFT 降幅 92.6%Agent 系统的长指令、RAG 的检索文档、企业应用的大型 System Prompt、多轮对话的共享历史——这些场景都是 Prefix Caching 效益最显著的地方。共享前缀越长、复用次数越高收益越大这也解释了 vLLM 将其作为默认开启项的原因。5.3 三篇文章的完整对照表 完整实验记录 基线 v0 (BF16) 实验 1 (AWQ INT4) 硬件: RTX 3090 24GB RTX 3090 24GB 模型权重: ~14 GB ~4 GB KV Cache 容量: 5306 blocks (4.6GB) 15839 blocks (13.9GB) 精度: HellaSwag: 80.5% 79.6% (-0.9%) ARC-Easy: 81.0% 78.8% (-2.2%) ARC-Challenge: 55.5% 54.5% (-1.0%) GSM8K: 71.3% 69.2% (-2.1%) 性能: TPOT P50 (conc1): 19.7 ms 6.84 ms (-65%) Out_TPS (conc32): 978 tok/s 2126 tok/s (117%) 严格 SLO Goodput: 826 tok/s 4547 tok/s (450%) Prefix Caching本文BF16 模型: ~1500 tokens 前缀 TTFT 降低 18.5%命中率 99.2% ~4000 tokens 前缀 TTFT 降低 92.6% OFF 拟合斜率 207.6 ms / 千 tokens ON 拟合斜率 6.9 ms / 千 tokens下降 96.7%5.4 下一步计划实验方法预期收益投机采样Speculative Decoding小模型草稿 大模型验证加速 decode低并发 TPOT 降 30~50%精度零损失Chunked Prefill将长 Prefill 分块执行与 decode 交错调度降低 Prefill 对在途请求 TPOT 的干扰本系列下一篇[投机采样实验精度零损失的加速方案]待更新BF16 基线部署从零建立精度与性能基准AWQ INT4 量化TPOT 从 19.7ms 到 6.84msKV Cache 深度解析吞吐翻倍的真正推手Prefix CachingTTFT 降低 92.6% 的免费优化本文投机采样一次有价值的失败实验系列收官RTX 3090 上的 LLM 推理优化全景参考资料Efficient Memory Management for Large Language Model Serving with PagedAttentionSOSP 2023Automatic Prefix Caching — vLLM 官方文档vLLM: Easy, Fast, and Cheap LLM Serving with PagedAttentionSGLang: Efficient Execution of Structured Language Model ProgramsRadixAttention 同类机制对比参考