<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>前缀缓存 &#8211; Blog of Code</title>
	<atom:link href="https://www.cztcode.com/tag/%E5%89%8D%E7%BC%80%E7%BC%93%E5%AD%98/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.cztcode.com</link>
	<description>模型推理加速与编程实践</description>
	<lastBuildDate>Sat, 05 Sep 2026 02:29:54 +0000</lastBuildDate>
	<language>zh-Hans</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://www.cztcode.com/wp-content/uploads/2024/02/cropped-logo-32x32.webp</url>
	<title>前缀缓存 &#8211; Blog of Code</title>
	<link>https://www.cztcode.com</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">217219486</site>	<item>
		<title>前缀缓存命中率很高，首字为什么还是慢？</title>
		<link>https://www.cztcode.com/2026/5359/</link>
					<comments>https://www.cztcode.com/2026/5359/#respond</comments>
		
		<dc:creator><![CDATA[Jellow]]></dc:creator>
		<pubDate>Sat, 05 Sep 2026 02:29:54 +0000</pubDate>
				<category><![CDATA[模型推理加速]]></category>
		<category><![CDATA[vLLM]]></category>
		<category><![CDATA[前缀缓存]]></category>
		<category><![CDATA[性能分析]]></category>
		<category><![CDATA[推理加速]]></category>
		<guid isPermaLink="false">https://www.cztcode.com/?p=5359</guid>

					<description><![CDATA[前缀缓存命中率达到 90%，首 token 仍然慢，问题可能出在统计口径和排队。用一组可复算请求区分命中次数、token 复用率与计算节省，再推导缓存后剩余注意力的成本，给出真实流量下的对照方法。]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><blockquote>
<p><strong>Jellow 编辑发布，AI 辅助生成并完成技术核对。</strong> 计算示例和实测结果会在文中分别说明。</p>
</blockquote>
<p>假设一项问答服务打开了前缀缓存，监控上显示 90% 的请求命中，用户却仍要等很久才看到回答。缓存是不是没起作用？</p>
<p>还不能这么判断。“有多少请求碰到了缓存”没有回答两个更关键的问题：省掉了多少工作，以及这份工作原本占等待时间的多少。</p>
<p>本文沿着这两个问题展开。讨论范围是完整因果注意力模型、可复用的前缀 KV，以及普通自回归生成。滑动窗口、混合注意力和跨节点缓存传输都有额外条件，不能直接照搬这里的块数和成本估计。</p>
<h2>命中的是 token 前缀，不是相似的问题</h2>
<p>前缀缓存保存已经计算过的 KV。新请求如果使用相同前缀，可以跳过相应的 Prefill。它不会因为两句话意思接近，就把一句话的 KV 交给另一句话使用。<a href="https://docs.vllm.ai/en/stable/features/automatic_prefix_caching/" target="_blank" rel="noopener">vLLM 的 APC 功能说明</a>明确把收益定位在共享前缀的计算复用，而非后续生成过程的直接提速。</p>
<p>判断是否相同，应看 chat template、tokenizer 等处理之后真正送进模型的输入。肉眼看起来一样的正文，如果模板、分隔符或前面的内容不同，最终前缀可能不同。</p>
<p>在 vLLM 的哈希块设计中，一个块的身份还依赖前面的块，并包含必要的额外标识，例如 LoRA、多模态输入或缓存隔离信息；该设计只缓存完整块。<a href="https://docs.vllm.ai/en/stable/design/prefix_caching/" target="_blank" rel="noopener">官方设计文档</a>给出了这些条件。</p>
<p>由此可以推导一个很实用的排查顺序：先确认输入序列和运行上下文真的可复用，再讨论缓存是否足够大。如果一个会变化的时间戳被拼在提示词最前面，后面几千 token 的稳定说明也不能作为相同前缀直接命中。</p>
<p>对于适合调整的业务模板，可以把稳定说明、固定文档放在前面，把本次问题放在后面。但不要只为命中率重排会改变含义的指令，也不要把不同权限或不同租户的数据混在一个共享范围里。模板改动之后仍要检查回答质量。</p>
<p>完整块还有一个取整问题。假设块长为 16 token，两次请求共享的前缀长 1000 token，那么完整共享块覆盖的是：</p>
<pre><code class="language-text">floor(1000 / 16) × 16 = 992 token</code></pre>
<p>这里的 16 是示例参数，不是对所有后端默认值的声明。实际可复用量还受块是否仍驻留、运行配置是否兼容等条件影响。另外，<a href="https://github.com/vllm-project/vllm/blob/main/vllm/v1/core/kv_cache_manager.py" target="_blank" rel="noopener">vLLM 的 KV 管理代码</a>说明，为获得最后位置的 logits，完整命中时仍需重算；块对齐还可能扩大重算范围。因此不能据此断言 Prefill 耗时必为零。</p>
<h2>90% 命中，可能只省了 1.54% 的输入 token</h2>
<p>为避免与某个框架的监控命名混淆，先自行定义两个统计口径。第 i 个请求的输入长度为 nᵢ，实际复用长度为 hᵢ：</p>
<pre><code class="language-text">请求命中率 = hᵢ &gt; 0 的请求数 / 总请求数
token 复用率 = Σhᵢ / Σnᵢ</code></pre>
<p>有些监控指标按块查询次数计数，既不是第一个，也不是第二个。看到 <code>hit_rate</code> 时，先查分子、分母和统计窗口。</p>
<p>构造 100 个请求：</p>
<table>
<thead>
<tr>
<th>请求组</th>
<th style="text-align: right">数量</th>
<th style="text-align: right">每次输入长度</th>
<th style="text-align: right">每次实际复用长度</th>
</tr>
</thead>
<tbody>
<tr>
<td>短问答</td>
<td style="text-align: right">90</td>
<td style="text-align: right">128 token</td>
<td style="text-align: right">16 token</td>
</tr>
<tr>
<td>长文档</td>
<td style="text-align: right">10</td>
<td style="text-align: right">8192 token</td>
<td style="text-align: right">0 token</td>
</tr>
</tbody>
</table>
<p>请求命中率的确为 90%。但总输入有 93,440 token，实际只复用了 1,440 token，token 复用率约为 1.54%。</p>
<p>这并不矛盾。大量短请求各碰到一个小前缀，就足以让“命中的请求数”很好看；真正占输入大头的长请求可能一次都没复用。</p>
<p>如果那 10% 未命中的长请求又恰好最慢，整体 p95 仍可能落在它们当中。缓存帮助了多数请求，也不保证最慢一段用户的体验同步改善。这里应按长度和是否命中分组看延迟分布，不能对几组 p95 做加权平均来拼出整体 p95。</p>
<h2>token 复用率也不等于计算节省比例</h2>
<p>即使知道复用了多少 token，仍然少了一步：原来每个 token 要做的工作并不完全相同。</p>
<p>对长度为 n 的完整因果注意力，注意力中有效的 query-key 位置对数量是 <code>n(n+1)/2</code>。为了区分随 token 数增长的工作和随位置对增长的工作，构造一个成本模型：</p>
<pre><code class="language-text">C(n) = a × n + b × n(n+1)/2</code></pre>
<p>a 汇总模型各层中按 token 线性增长的工作，b 表示每个有效注意力位置对的相对成本。它们是简化系数，不是通用硬件常数；矩阵形状、算子分块和 GPU 利用率都会让真实时间偏离这条曲线。</p>
<p>若前 h 个 token 已经缓存，后面还剩 m 个，其中 <code>m=n-h</code>。新 token 仍然需要关注前面的 h 个位置，所以剩余成本应为：</p>
<pre><code class="language-text">Cremaining = a × m + b × [m × h + m(m+1)/2]
           = C(n) - C(h)</code></pre>
<p>不能写成 <code>C(n-h)</code>。那会漏掉后缀对已有前缀的注意力。缓存省掉的是前缀自身的计算，不是从注意力视野中删除这段上下文。</p>
<p>因此，假如缓存了 8192 token 输入的一半，在这个模型里线性部分省掉一半，而注意力位置对只省掉约四分之一。最终时间能省多少，还取决于两部分的比例和执行效率。这也解释了为什么“命中了 50% token”不应直接写成“Prefill 快了一倍”。</p>
<p>下面把前面的 100 请求例子完整算一遍。</p>
<pre><code class="language-python">requests = [(128, 16)] * 90 + [(8192, 0)] * 10

def cost(n, a=1.0, b=1.0 / 1024):
    # 人为选择的成本单位，不是微秒或 GPU FLOPs。
    return a * n + b * n * (n + 1) / 2

total_tokens = sum(n for n, h in requests)
reused_tokens = sum(h for n, h in requests)
request_hits = sum(h &gt; 0 for n, h in requests)
uncached_work = sum(cost(n) for n, h in requests)
saved_work = sum(cost(h) for n, h in requests)

print(f"request_hit_pct={100 * request_hits / len(requests):.2f}")
print(f"token_reuse_pct={100 * reused_tokens / total_tokens:.2f}")
print(f"model_work_saved_pct={100 * saved_work / uncached_work:.3f}")

n, h = 8192, 4096
m = n - h
remaining = m + (m * h + m * (m + 1) / 2) / 1024
print("remaining_work:", remaining)
print("incorrect_suffix_only:", cost(m))
assert remaining == cost(n) - cost(h)
assert remaining &gt; cost(m)</code></pre>
<p>运行后，三个比例依次为 <code>90.00%</code>、<code>1.54%</code>、<code>0.344%</code>；后两行工作量为 <code>28674.0</code> 和 <code>12290.0</code>。示例中长输入占据大量注意力工作，却完全未命中，按这个成本模型算出的收益便比 token 复用率还小。</p>
<p>这些数字只验证推导。改变 a、b 或请求长度分布，结论的具体幅度会变；是否值得部署，仍要测真实 Prefill 时间。</p>
<h2>首 token 的账里，还有排队</h2>
<p>用户感知的 TTFT，是发出请求到收到第一个有效输出之间的时间。为了诊断，可以把关键路径概念性地拆成：</p>
<pre><code class="language-text">TTFT ≈ 输入处理与网络 + 排队 + 缓存查找/必要传输
       + 剩余 Prefill + 输出头与采样 + 首次回传</code></pre>
<p>真实系统中一些阶段可以重叠，trace 里的字段也可能包含彼此；相加之前要先对齐口径。对 Prefill、Decode 与客户端指标不熟悉，可以结合<a href="https://www.cztcode.com/2026/5348/">第一篇的指标说明</a>阅读。</p>
<p>假设一次请求排队 800 ms，Prefill 为 200 ms，其他开销暂记为零。缓存把 Prefill 降到 20 ms，而排队没有变化，那么 TTFT 从 1000 ms 变成 820 ms，只提升约 1.22 倍。</p>
<p>这不意味着缓存最多只有这点作用。如果缓存减少了全站计算压力，排队也可能随之下降。上面的例子特意固定排队，只是为了证明：<strong>单请求省下的计算时间，与整个服务最终少等的时间，需要分别测量。</strong></p>
<p>多副本部署还会带来另一个取舍。假设副本 A 有缓存，但预计要排队 600 ms、剩余 Prefill 40 ms；副本 B 没有缓存，只排队 20 ms、完整 Prefill 200 ms。忽略其他开销，A 是 640 ms，B 是 220 ms。此时只按“去有缓存的副本”路由，会选错。</p>
<p>这也是一个假设案例，不是在推荐具体路由算法。它给出的决策条件是：缓存省掉的计算与传输，是否大于额外排队和路由成本。热点前缀的局部命中率再漂亮，也需要放回这个比较中。</p>
<h2>怎样做一次不会高估收益的实验</h2>
<p>先准备一份固定请求序列，保留真实的输入长度、共享前缀长度和到达顺序。只反复发送同一个长提示词，测到的是近乎理想的复用条件；它可以验证功能，却不能代表生产收益。</p>
<p>用同样的到达负载做三个对照：缓存关闭、缓存开启但从空状态开始、缓存开启且经过符合真实流量的预热。记录预热请求，但与正式窗口分开计时。不要用提前知道未来全部请求的方式构造“完美缓存”，再把结果当成正常服务表现。</p>
<p>每次请求至少关联这些数据：</p>
<table>
<thead>
<tr>
<th>数据</th>
<th>要回答的问题</th>
</tr>
</thead>
<tbody>
<tr>
<td>输入 token 数、实际复用 token 数</td>
<td>命中的是短前缀，还是主要计算部分</td>
</tr>
<tr>
<td>副本、模型与模板版本、前缀分组标识</td>
<td>是否因为输入或路由变化失去复用</td>
</tr>
<tr>
<td>排队时间、Prefill 时间、客户端 TTFT</td>
<td>收益出现在哪一段，是否被其他等待抵消</td>
</tr>
<tr>
<td>缓存容量压力、淘汰或重算情况</td>
<td>缓存是否在真实混合流量下仍能留住热点</td>
</tr>
<tr>
<td>错误、超时和请求到达率</td>
<td>是否通过少接请求或剔除失败美化了结果</td>
</tr>
</tbody>
</table>
<p>缓存分组可用内部标识关联，通常不必把完整用户提示词写进性能日志。</p>
<p>当结果不理想时，顺序也很重要。复用 token 本来就少，先检查模板和流量结构；复用多但 Prefill 没降，检查实际是否重算以及成本模型是否适合该架构；Prefill 已明显下降而 TTFT 不动，再看队列、路由、传输和客户端缓冲。</p>
<p>缓存优化最终应该交付一件具体的东西：在相同请求到达条件下，哪些请求的等待下降了多少，哪些仍然没有受益。命中率是解释这个结果的线索，不是结果本身。</p>
<hr />
<p>技术资料核对日期：2026-09-05。代码在 CPU 上验证统计口径和成本恒等式；文中耗时均为假设示例，未进行 GPU 服务实测。</p>
<p><a href="https://www.cztcode.com/llm-inference-acceleration/">模型推理加速专题</a> · <a href="https://www.cztcode.com/ai-content-policy/">内容生成、审核与纠错说明</a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.cztcode.com/2026/5359/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5359</post-id>	</item>
	</channel>
</rss>
