同一份长文档被连续提问十次,模型是否必须把文档重新计算十遍?在LLM推理服务中,前缀缓存能够复用相同开头对应的KV状态,让后续请求少做重复的提示词计算。
这和之前讲过的RAG答案缓存处于不同层。答案缓存直接复用业务结果,前缀缓存复用推理中间状态,后面的新问题和新答案仍然需要处理。今天以vLLM V1的Automatic Prefix Caching为例,把命中条件、分块结构和实际接入思路串起来。
一、先分清Prefill与Decode
对常见的自回归Transformer推理,可以把请求理解为两个阶段:Prefill处理输入Token并建立各层的Key、Value状态;Decode在已有状态上逐步生成后续Token。
如果新请求的开头与已处理请求完全一致,就能复用相应前缀的KV状态。vLLM官方将这种机制称为APC。它主要减少输入处理工作,不会把后续答案生成整个跳过。vLLM功能说明
| 缓存位置 | 复用内容 | 新请求还要做什么 |
|---|---|---|
| 检索缓存 | 候选证据 | 上下文构造与生成 |
| 答案缓存 | 已生成答案 | 适用性与有效性检查 |
| 前缀缓存 | 相同Token前缀的KV状态 | 新增输入计算与后续生成 |
因此,一篇文档上的不同问题可以共享文档前缀,但仍产生不同答案。若请求很短、答案很长,节省输入计算并不意味着端到端耗时会按相同比例下降。
二、相似文字不等于相同前缀
前缀匹配面对的是模型实际接收到的Token序列。语义相近、交换段落顺序、在开头插入时间戳,都可能改变匹配结果。
设两次请求分别是"固定说明+同一文档+问题A"和"固定说明+同一文档+问题B"。它们在问题出现前有共同前缀。若改成"问题A+固定说明+同一文档",开头的问题已经不同,后面再次出现相同文档也不自动恢复成可共享前缀。
这是因果注意力带来的上下文依赖:后面Token的状态由前面的内容共同决定。只比较当前块的文字,不比较它之前的内容,会复用错误的状态。

实际部署还要固定模型、分词器、聊天模板和适配器等条件。应用层看到相同字符串,不代表最终消息模板中的角色标记、特殊Token和其他输入也一致。
三、为什么块的标识要包含父块
vLLM的设计文档给出基于哈希的块识别方式:当前块Token、前一个块的哈希以及需要区分的额外信息共同组成标识。这样前部发生变化时,后续块的标识也随之变化。块缓存设计
text
块1标识 = H(起点, 块1内容, 隔离信息)
块2标识 = H(块1标识, 块2内容, 隔离信息)
块3标识 = H(块2标识, 块3内容, 隔离信息)
按这里讨论的完整块复用模型,块未填满时不作为完整缓存块共享。假设块大小为4,两个请求共享前10个Token,可复用的完整块是前8个;剩下的部分继续计算。4只是下面演示用的块大小,部署时由实际引擎和模型配置决定。

额外信息也很重要:多模态输入不能只比较图片占位Token,适配器变化也不能沿用旧模型状态。服务端应让缓存键覆盖实际影响计算的输入条件。
四、用一个小程序验证命中规则
下面用数字代表Token ID,保存块标识,演示完整块、父链与隔离域。程序不保存真实KV张量,输出的是规则模型中的可复用Token数量。
python
import hashlib
import json
BLOCK = 4
cache = set()
def keys(tokens, namespace):
parent = namespace
for start in range(0, len(tokens) - BLOCK + 1, BLOCK):
block = tokens[start:start + BLOCK]
payload = json.dumps([parent, block], separators=(',', ':')).encode()
parent = hashlib.sha256(payload).hexdigest()
yield parent
def run(tokens, namespace='model-A:tenant-1'):
chain = list(keys(tokens, namespace))
hits = 0
for key in chain:
if key not in cache:
break
hits += BLOCK
cache.update(chain)
return hits, len(tokens) - hits
# 数字代表教学用Token ID,不调用分词器或推理模型。
prefix = list(range(1, 9))
cases = [
('cold', prefix + [90, 91], 'model-A:tenant-1'),
('new_question', prefix + [92, 93], 'model-A:tenant-1'),
('early_change', [99] + prefix[1:] + [92, 93], 'model-A:tenant-1'),
('other_tenant', prefix + [92, 93], 'model-A:tenant-2'),
]
actual = []
for name, tokens, namespace in cases:
result = run(tokens, namespace)
actual.append(result)
print(f'{name}: reusable={result[0]}, remaining={result[1]}')
assert actual == [(0, 10), (8, 2), (0, 10), (0, 10)]
assert len(list(keys(list(range(10)), 'x'))) == 2
assert list(keys([1,2,3,4,5,6,7,8], 'x'))[1] != list(keys([9,2,3,4,5,6,7,8], 'x'))[1]
cache.clear()
assert run(prefix + [92,93]) == (0,10)
print('PASS: exact prefix, parent chain, full blocks, namespace, eviction')
实际运行输出:
text
cold: reusable=0, remaining=10
new_question: reusable=8, remaining=2
early_change: reusable=0, remaining=10
other_tenant: reusable=0, remaining=10
PASS: exact prefix, parent chain, full blocks, namespace, eviction
第一次是冷请求,没有已存在的块。第二次只改变末尾问题,前8个Token命中。第三次改变第一个Token,后续块即使局部内容一样,父链也不同。第四次更换隔离域,内容一样也不会共享。
remaining表示这份规则模型中未被复用的输入数量,不是GPU耗时,也不包含答案生成。真实引擎还涉及调度、末尾计算、内存管理与模型结构,不能用这几个整数直接换算加速倍数。
五、提示词怎样组织才有复用机会
把真正稳定的公共说明放在前部,把请求特有内容放在后部,通常有利于共享较长前缀。例如"固定系统规则→固定文档→当前问题"。但顺序仍应服从任务语义和模型效果,调整后要重新验证回答质量。
在RAG里,每次检索结果可能不同,文档排列也会变化。不要为了提高缓存命中率强行加入不相关内容。可以先稳定模板格式、避免不必要的随机标记,再分析真实共享长度分布,判断优化空间。
为每次请求在开头添加随机请求ID,会很早打断匹配。追踪ID应留在请求元数据和日志中,除非它确实是模型必须理解的内容。
多轮会话通常存在历史前缀共享机会;历史摘要、工具结果重写、模板升级都会改变Token序列,需要重新计算对应部分。缓存不是永久资产,内存压力下被驱逐后,再次请求会重新计算。
六、接入与观测看什么
在具备兼容模型与推理环境的vLLM实例中,可按官方接口启用:
python
from vllm import LLM, SamplingParams
llm = LLM(model="你的模型路径", enable_prefix_caching=True)
params = SamplingParams(max_tokens=64, temperature=0)
shared = "固定说明与长文档内容......"
first = llm.generate([shared + "问题一:核心结论是什么?"], params)
second = llm.generate([shared + "问题二:有哪些限制条件?"], params)
这段代码用于真实推理环境接入;前一节的标准库程序可以直接在普通Python环境运行。聊天模型应使用匹配的聊天模板组织输入,不能为了拼接方便忽略模型要求的格式。
验证优化时,记录冷请求和热请求的输入长度、可复用前缀、首Token延迟、整体延迟及输出长度;保持模型、硬件、并发和负载分布一致。只测第二次请求却不说明已有缓存,会把预热效果混入对比。
要看整批流量的分位数,不只挑共享最长的一条请求。队列等待可能遮住Prefill改善,输出很长时Decode也可能占据大部分时间。缓存容量有限,短时间高命中不代表长期负载下仍有相同表现。
七、共享范围需要主动设计
vLLM提供cache_salt来隔离前缀缓存共享范围。受信任的服务端可以按信任域设置隔离值,使不同域的相同输入不共用缓存。隔离值必须由服务端控制,不能让调用者任意选取其他租户的域。缓存隔离说明
缓存隔离不会替代文档权限检查。RAG检索仍要先验证用户能否读取证据;前缀缓存只处理已经进入推理请求的数据。模型或适配器切换、上下文模板变更,也应明确对应的缓存兼容边界。
八、🧠 思维导图
#mermaid-svg-0FWCaETJOvXYRWP3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0FWCaETJOvXYRWP3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0FWCaETJOvXYRWP3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0FWCaETJOvXYRWP3 .error-icon{fill:#552222;}#mermaid-svg-0FWCaETJOvXYRWP3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0FWCaETJOvXYRWP3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0FWCaETJOvXYRWP3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0FWCaETJOvXYRWP3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0FWCaETJOvXYRWP3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0FWCaETJOvXYRWP3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0FWCaETJOvXYRWP3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0FWCaETJOvXYRWP3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0FWCaETJOvXYRWP3 .marker.cross{stroke:#333333;}#mermaid-svg-0FWCaETJOvXYRWP3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0FWCaETJOvXYRWP3 p{margin:0;}#mermaid-svg-0FWCaETJOvXYRWP3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0FWCaETJOvXYRWP3 .cluster-label text{fill:#333;}#mermaid-svg-0FWCaETJOvXYRWP3 .cluster-label span{color:#333;}#mermaid-svg-0FWCaETJOvXYRWP3 .cluster-label span p{background-color:transparent;}#mermaid-svg-0FWCaETJOvXYRWP3 .label text,#mermaid-svg-0FWCaETJOvXYRWP3 span{fill:#333;color:#333;}#mermaid-svg-0FWCaETJOvXYRWP3 .node rect,#mermaid-svg-0FWCaETJOvXYRWP3 .node circle,#mermaid-svg-0FWCaETJOvXYRWP3 .node ellipse,#mermaid-svg-0FWCaETJOvXYRWP3 .node polygon,#mermaid-svg-0FWCaETJOvXYRWP3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0FWCaETJOvXYRWP3 .rough-node .label text,#mermaid-svg-0FWCaETJOvXYRWP3 .node .label text,#mermaid-svg-0FWCaETJOvXYRWP3 .image-shape .label,#mermaid-svg-0FWCaETJOvXYRWP3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-0FWCaETJOvXYRWP3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0FWCaETJOvXYRWP3 .rough-node .label,#mermaid-svg-0FWCaETJOvXYRWP3 .node .label,#mermaid-svg-0FWCaETJOvXYRWP3 .image-shape .label,#mermaid-svg-0FWCaETJOvXYRWP3 .icon-shape .label{text-align:center;}#mermaid-svg-0FWCaETJOvXYRWP3 .node.clickable{cursor:pointer;}#mermaid-svg-0FWCaETJOvXYRWP3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0FWCaETJOvXYRWP3 .arrowheadPath{fill:#333333;}#mermaid-svg-0FWCaETJOvXYRWP3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0FWCaETJOvXYRWP3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0FWCaETJOvXYRWP3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0FWCaETJOvXYRWP3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0FWCaETJOvXYRWP3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0FWCaETJOvXYRWP3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0FWCaETJOvXYRWP3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0FWCaETJOvXYRWP3 .cluster text{fill:#333;}#mermaid-svg-0FWCaETJOvXYRWP3 .cluster span{color:#333;}#mermaid-svg-0FWCaETJOvXYRWP3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0FWCaETJOvXYRWP3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0FWCaETJOvXYRWP3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-0FWCaETJOvXYRWP3 .icon-shape,#mermaid-svg-0FWCaETJOvXYRWP3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0FWCaETJOvXYRWP3 .icon-shape p,#mermaid-svg-0FWCaETJOvXYRWP3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0FWCaETJOvXYRWP3 .icon-shape .label rect,#mermaid-svg-0FWCaETJOvXYRWP3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0FWCaETJOvXYRWP3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0FWCaETJOvXYRWP3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0FWCaETJOvXYRWP3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} vLLM前缀缓存
复用对象
命中条件
工程设计
观测
KV状态与Prefill
新增输入和生成继续计算
Token前缀与父块哈希
完整块与隔离域
稳定模板和正确权限
冷暖请求及延迟分位数
九、总结
总结要点
缓存的是共同前缀的计算状态。 新问题仍要处理,答案仍要生成;它与检索缓存、答案缓存可以分层协作。
命中依赖精确输入和上下文关系。 父块哈希保留前缀依赖,完整块与隔离域共同限制共享范围。语义相近不能替代Token一致。
优化要回到实际负载。 稳定模板、合理共享和正确权限先做好,再用冷暖请求、首Token延迟和端到端分位数判断收益。
下一篇聊推测解码:小模型先起草、大模型再验证,怎样在保持目标分布的同时减少生成等待。
👉 如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享!😊