想象你经营一家面馆,招牌汤底要用大骨熬八小时。糟糕的是,店里规定每位顾客点单时,后厨都得从零开始重新熬一锅汤——哪怕十个顾客点的都是同一款招牌面。结果后厨累得冒烟,顾客等得拍桌,煤气费高得吓人。直到有一天店长开窍:高汤配方明明一样,为什么不提前熬好一大锅放着?顾客来了直接舀汤,只煮面、加浇头就行。 大模型的推理服务正在经历同样的“重复熬汤”:系统提示词和 Few-shot 示例对每个请求都一字不差,但传统做法让每个请求独立计算并缓存整段 KV Cache,把完全相同的“高汤”重复熬了成百上千次。前缀缓存(Prefix Cache)做的就是面馆店长的那次开窍——识别出不同请求共享的前缀,把前缀的 KV Cache 只算一次并复用,新请求只需计算自己独特的那一部分。在这个故事里,厨师是 GPU,高汤是 KV Cache,顾客的浇头是用户输入,而“公共高汤锅”就是前缀缓存。

岗位关联

掌握 Prefix Cache,你就握住了让大模型推理“省钱省时”的钥匙:在面试与真实部署中,它都是推理优化里性价比最高的技能之一。

目录
  1. 可视化演示
  2. 重复计算困境
  3. 前缀缓存原理
  4. 命中判定逻辑
  5. 系统集成实现
  6. 收益量化分析
  7. 适用场景特征
  8. 工程权衡考量
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

动态演示大模型KV Cache前缀缓的核心机制,通过开始/执行/监控/完成四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

大模型KV Cache前缀缓:前缀识别 前缀识别与Radix树匹配算法 缓存命中率优化与前缀对齐策略 核心机制可视化 大模型KV Cache前缀缓整体架构与关键组件

重复计算困境

面馆开业初期,店长立下规矩:“每位顾客来了先熬汤。”不管顾客点牛肉面还是排骨面,高汤配方一模一样,但每单都要从洗锅、放水、下料重新开始。后厨忙得脚不沾地,顾客却等得拍桌子。店长月底算账:一天 1000 单,每单熬汤两小时,其中九成工序是在重复做同一件事,煤气费全烧在“重来一次”上。

大模型服务就是这样。一个典型的 Chat 应用,每个请求都带着相同的系统提示词(“你是一个乐于助人的助手……”),偶尔还有 Few-shot 示例。这些前缀 token 在每次请求里一字不差。模型在预填充阶段要逐个 token 计算注意力,把每个 token 的 K 向量和 V 向量算出来并存入 KV Cache——这个动作对每个请求完整重复,哪怕那 500 个前缀 token 昨天已经被算了十万次。

KV Cache 是 Transformer 自回归解码的产物:预测下一个 token 时,模型要“回看”前面全部 token,把 K/V 缓存下来避免重复计算。问题是缓存“按请求隔离”:请求 A 算好的底料,请求 B 一点用不上。以 7B 模型为例,32 层 × 32 头 × 128 维再乘上几千 token 的序列长度,一个请求的 KV Cache 就能吃掉几百 MB 显存;一千个请求各自重复算出同样的前缀 KV,浪费就这样悄悄变成了昂贵的账单。

店长猛然醒悟:熬汤的钱付了一千次,其中九百次都是重复劳动。与其让每口锅都从零开始,不如把这锅“高汤”变成厨房的公共资产——这正是前缀缓存的第一步思考。

Mkv=2×nlayer×nhead×dhead×L×b\displaystyle M_{\text{kv}} = 2 \times n_{\text{layer}} \times n_{\text{head}} \times d_{\text{head}} \times L \times b
这条公式算的是:一条长度为 L 的序列,它的 KV Cache 总共占多少显存。每个乘数都对应模型结构或存储格式的一个硬参数。
  1. 系数 2每个 token 都要同时存 K 向量和 V 向量两份矩阵,这是“两份底料”。
    22
  2. 层数 n_layerTransformer 每一层注意力都要自己保有一份 KV,层数越多存得越多。
    nlayern_{\text{layer}}
  3. 头数与维度 n_head × d_head注意力被切成多个头并行计算,每个头都有独立的 K/V 向量。
    nhead×dheadn_{\text{head}} \times d_{\text{head}}
  4. 序列长度 L所有 token 的 KV 都要按位置排好,序列越长缓存越大。
    LL
  5. 字节数 b元素用 FP16 存储时每个占 2 字节,用 FP8 则占 1 字节。
    bb

前缀缓存原理

店长推出新方案:厨房窗口挂一块“今日高汤已备好”的牌子。顾客点单时,先看高汤锅——有现成的汤直接舀;只有浇头(用户个性化部分)需要现场做。高汤的配方、熬法、火候对每个顾客都一样,所以它成了所有订单共用的“底座”。

Prefix Cache 的思路完全一致:把请求分解成“共享前缀”和“专属后缀”。共享前缀通常由系统提示词、Few-shot 示例、任务说明组成,在同一应用的所有请求里几乎不变;专属后缀则是每个用户真实输入的内容。前缀缓存把共享前缀 token 的 K/V 值算好、存在显存公共区;新请求进来先做一次“对牌”,匹配上了就直接把缓存装进计算流,跳过预填充阶段那几百甚至上千 token 的注意力计算。

在实现层面,前缀缓存通常不是“逐 token 匹配”,而是把前缀切成固定大小的块(block),以块为单位做哈希索引。块内 token 完整算出 KV 后,连同该块的内容哈希一起存入缓存表。命中时,调度器把对应块指针接进新请求的 KV 页表——这正是 vLLM 里 PagedAttention 与 prefix caching 结合的底层思路;未命中的块才从预填充开始算。这整个机制,和操作系统里“页缓存命中”的逻辑几乎同构。

高汤锅越大(共享前缀越长),直接舀汤的顾客占比越高(命中率越高),后厨就越轻松。前缀缓存把“每单重熬”变成了“一锅共用”,原理朴素,收益却直击要害。

η=LsharedLprompt\displaystyle \eta = \frac{L_{\text{shared}}}{L_{\text{prompt}}}
命中率 η 表示一条请求里有多大比例的 token 是“直接舀汤”而不是“现熬”。
  1. 共享前缀长度 L_shared请求里与缓存中已有前缀完全一致的部分,是“可以复用”的 token 数量。
    LsharedL_{\text{shared}}
  2. 提示总长 L_prompt整条请求的 token 总数,它决定了不做缓存时需要计算多少步注意力。
    LpromptL_{\text{prompt}}
  3. 比值 η命中率越高,需要新计算的 token 越少,首 token 延迟越低。
    η=LsharedLprompt\eta = \frac{L_{\text{shared}}}{L_{\text{prompt}}}

命中判定逻辑

为了快速判断高汤是不是同一锅,店长给每锅汤挂了一块“配方编号牌”。新订单来了不用尝味道,对一下编号就知道是不是自家那锅高汤。但编号怎么打才能又快又不出错?这就是厨房的算法问题。

前缀缓存同样需要“对牌”机制。最朴素的做法是逐 token 比对:新请求的前 1000 个 token 和缓存里的 1000 个 token 逐个比较。但每次请求都做一次 O(L) 级别的比对,本身就是不小的开销。工程上通用的方案是“块级哈希”:把前缀切成固定块,每块用内容哈希(如 xxhash / SHA 摘要)作为编号,查找时逐块对哈希,命中连续若干块就复用对应 KV。

判定逻辑的核心是“前缀对齐”——只有当两个序列从开头起有连续完全相同的 token 才算有效命中。中间断掉一块,后面即使文字相同也不能复用,因为注意力上下文已经改变,KV 必须重算。vLLM 的做法是:每个块在存储时记录“整条前缀中间所有块的哈希链”,查询请求从前往后逐块匹配,遇到第一个哈希不一致的块就停止;此前的块全部复用,此后的块走正常预填充。这就是“部分命中”。

就像服务员按编号牌连看七张,前六张都对上、第七张对不上——那前六锅汤直接用,第七锅重新熬。编号牌(哈希)让“对牌”从分钟级变成毫秒级,这就是部分命中的工程智慧。

Lhit=max⁡{k:  ∀ i≤k, Hash(Bireq)=Hash(Bicache)}\displaystyle L_{\text{hit}} = \max \left\{ k : \;\forall\, i \le k,\ \mathrm{Hash}(B_i^{\text{req}}) = \mathrm{Hash}(B_i^{\text{cache}}) \right\}
命中长度 L_hit 是“连续对牌成功”的最长前缀块数:前 k 块全部哈希相等,第 k+1 块起失效。
  1. 块哈希相等每个块内容做哈希,只有哈希相等才可能复用。
    Hash(Bireq)=Hash(Bicache)\mathrm{Hash}(B_i^{\text{req}}) = \mathrm{Hash}(B_i^{\text{cache}})
  2. 全称量词 ∀i≤k要求从第 1 块到第 k 块全部匹配,中间任意一块不匹配就终止。
    ∀ i≤k\forall\, i \le k
  3. 取最大值 maxL_hit 是可匹配前缀块里最长的那一个,决定了能复用多少 KV。
    Lhit=max⁡{⋅}L_{\text{hit}} = \max \{ \cdot \}

系统集成实现

面馆要落地“公共高汤锅”,得改造整个流程:设一个高汤区、一个煮面区,服务员下单先查高汤区有没有货,没有才通知后厨开火。店长还要规定高汤锅最多放几口,放不下时优先倒掉“最近没人点”的汤。

在真实推理框架(如 vLLM)中,这套流程对应三个模块:块管理器(Block Manager)、缓存控制器(Cache Controller)和调度器(Scheduler)。块管理器负责按固定 token 数切分前缀,并为每块分配显存页;缓存控制器维护哈希表,记录每块的内容哈希、引用计数和最近访问时间;调度器在每批请求进来时先查缓存表,把命中块直接插进新请求的 KV 页表。

一个关键细节是“引用计数”——同一块 KV 可能被多个进行中的请求同时引用(例如同一个系统提示词同时服务 10 个用户),块不能随意释放。最近最少使用(LRU)策略在这里天然适用:记录每块最后一次被命中的时间,显存紧张时优先驱逐最久没被用过的块。另外,模型权重更新、提示词模板改版都会让缓存“失效”,所以缓存控制器还要支持按版本号整体失效。

面馆的“高汤区”从此独立于“煮面区”,成为公共设施:服务员查单、后厨补汤、旺季排队全按这套流程走。系统集成做扎实了,整套服务才能稳定高效地运转。

Msaved=∑i=1Nηi×Mprompt,i\displaystyle M_{\text{saved}} = \sum_{i=1}^{N} \eta_i \times M_{\text{prompt},i}
总节省显存等于所有请求的(命中率 × 各自完整前缀 KV 代价)之和。
  1. 单请求 KV 代价 M_prompt,i第 i 条请求如果不做缓存,需要为全部前缀 token 计算的 KV 显存或算力。
    Mprompt,iM_{\text{prompt},i}
  2. 命中率 η_i第 i 条请求实际复用掉的占比,来自前面的命中判定结果。
    ηi\eta_i
  3. 求和符号把系统中所有请求的节省累加,得到整体收益。
    ∑i=1N\sum_{i=1}^{N}

收益量化分析

改造一个月后,店长拉出报表:顾客从下单到第一口面入口的时间(首口延迟)从 15 分钟降到 6 分钟;同一时段能接待的顾客数(吞吐量)翻了一倍。钱省在哪?省在“没重复熬汤”。

LLM 推理的核心指标是 TTFT(Time To First Token,首 token 延迟)和吞吐量。预填充阶段占据 TTFT 的大头,而预填充计算量与 prompt 长度成正比。Prefix Cache 命中后,预填充只剩“专属后缀”那一段,计算量从 L_prompt 降到 L_prompt − L_hit——节省比例恰好等于命中率。

考虑一个典型场景:系统提示词 512 token,用户输入 128 token。不做缓存时预填充要算 640 个 token;命中后只需算 128 个,预填充计算量降为原来的五分之一。如果 100 个用户共享同一个前缀,GPU 原本要为 100 × 512 = 51,200 个 token 做重复计算,现在只需算 1 次加 100 × 128 次。在长上下文场景(如 8K 系统提示词 + 对话历史)里,加速比可以轻松突破 10 倍,吞吐量也随之上升,因为 GPU 的空转时间被大幅压缩。

报表上的数字不会骗人:没重复熬汤,后厨的煤气费(算力)降了,顾客翻台率(吞吐)升了。这一锅“公共高汤”直接改写了面馆的盈亏线。

speedup=TcoldTwarm=LpromptLprompt−Lhit\displaystyle \text{speedup} = \frac{T_{\text{cold}}}{T_{\text{warm}}} = \frac{L_{\text{prompt}}}{L_{\text{prompt}} - L_{\text{hit}}}
加速比是冷启动耗时与命中后耗时的比值;忽略缓存查找开销的近似下,它等于总长度除以新增长度。
  1. 冷启动耗时 T_cold无缓存时的预填充耗时,正比于整条 prompt 长度。
    Tcold∝LpromptT_{\text{cold}} \propto L_{\text{prompt}}
  2. 命中后耗时 T_warm只计算未命中的后缀部分。
    Twarm∝Lprompt−LhitT_{\text{warm}} \propto L_{\text{prompt}} - L_{\text{hit}}
  3. 加速比比值越大,说明复用带来的收益越明显。
    speedup=LpromptLprompt−Lhit\text{speedup} = \frac{L_{\text{prompt}}}{L_{\text{prompt}} - L_{\text{hit}}}

适用场景特征

高汤策略也不是对谁都有效——如果顾客天南海北、点单完全随机,每锅汤配方都不一样,熬一大锅高汤反而占地方。店长总结:高汤只适合“点单模式固定”的客群。

同理,Prefix Cache 的收益完全取决于“前缀复用率”。三个最典型的高复用场景:第一,多轮对话——每轮请求的前缀是“系统提示词 + 全部历史消息”,轮次越多公共前缀越长,缓存命中率越高;第二,模板化推理——医疗报告、法律文书、代码审查等场景,请求结构是“固定模板 + 少量字段”,模板部分天然共享;第三,批量评测与批量生成——同一 prompt 并发发给 N 个用户(如 A/B 测试、排行榜评测),共享前缀被高并发访问,命中率极高。

这里要特别强调:“长系统提示词 + 短用户输入”是最佳组合——共享前缀越长,专属后缀越短,命中率越接近 1。反过来,如果每条请求开头都是完全不同的内容(如无系统提示词的自由问答),前缀根本不存在,缓存自然帮不上忙。另外,缓存条目数会随前缀种类线性增长,前缀种类越少且访问越集中,缓存的“性价比”越高。

面馆后来还发现一个意外之喜:经常来的老顾客(多轮对话)续汤越来越快——因为他们喝的还是那锅底汤,只是换了浇头。场景对上了,缓存的威力才能完全释放。

ηavg=∑i=1NLshared,i∑i=1NLprompt,i\displaystyle \eta_{\text{avg}} = \frac{\sum_{i=1}^{N} L_{\text{shared},i}}{\sum_{i=1}^{N} L_{\text{prompt},i}}
系统级平均命中率 = 所有请求共享前缀总长 / 所有请求提示总长。
  1. 分子求和所有请求里被命中的 token 总数。
    ∑i=1NLshared,i\sum_{i=1}^{N} L_{\text{shared},i}
  2. 分母求和所有请求需要处理的 token 总数。
    ∑i=1NLprompt,i\sum_{i=1}^{N} L_{\text{prompt},i}
  3. 平均化的意义单条命中率高不算数,批量业务的平均命中率才决定部署收益。
    ηavg\eta_{\text{avg}}

工程权衡考量

高汤锅也有烦恼:冰箱只有那么大,放十口高汤锅,就放不下新鲜食材了;而且高汤放久了会变味,必须定时倒掉重熬。公共高汤不是免费的。

Prefix Cache 的代价同样直接。第一,显存占用——每段被缓存的前缀 KV 都要常驻 GPU 显存,这会挤占给生成阶段预留的 KV 空间;第二,管理开销——哈希表、引用计数、LRU 结构都要消耗 CPU 与内存资源;第三,失效风险——提示词模板改一个词,或模型权重升级,所有相关缓存全部失效,命中率瞬间归零。

因此工程上要在“缓存多少”和“命中多少”之间找平衡。常见做法包括:设置缓存显存上限,超出后按 LRU 驱逐;区分“热前缀”与“冷前缀”,只缓存访问频率高于阈值的前缀;前缀被驱逐后,对应 KV 页回收到公共池继续给生成阶段使用。对于动态前缀(如 RAG 检索中每请求不同的文档前缀),缓存收益不稳定,需要单独评估是否值得缓存。在分布式推理中,多卡共享缓存池还要保证淘汰与更新的原子性。

聪明的店长最后定下规矩:高汤锅最多留三口,只留点单率最高的三种;三天没人点的汤直接倒掉,腾地方给新鲜食材。缓存不是越多越好,而是越“热”越好。

Mcache=m×Mprefix\displaystyle M_{\text{cache}} = m \times M_{\text{prefix}}
缓存区总显存约等于缓存的前缀种类数乘以单份前缀 KV 的显存。
  1. 单份前缀显存 M_prefix一段被缓存前缀的 KV 总量,由前面的单 token 公式乘以前缀长度得到。
    MprefixM_{\text{prefix}}
  2. 前缀种类 m缓存里不同的前缀条数,前缀越多样 m 越大。
    mm
  3. 乘积关系显存代价随前缀种类线性增长,这正是引入淘汰策略的根本原因。
    Mcache=m×MprefixM_{\text{cache}} = m \times M_{\text{prefix}}

AI 应用场景

场景一:智能客服多轮对话加速

阶段:推理部署与优化

解决的问题:传统客服机器人的每次请求都包含“系统提示词 + 完整历史对话”作为前缀。多轮对话下前缀越来越长,重复计算的 KV 把 GPU 塞满。Prefix Cache 让共享前缀只计算一次,后续轮次直接命中。

具体案例:某银行智能客服把 2000 token 的提示模板加上每轮历史消息作为前缀,上线 vLLM 前缀缓存后,第 2 轮起命中率达 60%,首 token 延迟从 1.8 秒降至 0.6 秒,单卡并发数提升约 3 倍。

场景二:Few-shot 模板批量评测

阶段:模型评估与批量推理

解决的问题:评测集通常用“4 个 few-shot 示例 + 1 个测试问题”拼成请求,模板部分占 70% 以上。不做缓存时,1000 条评测要对模板重复计算 56 万 token 的 KV,浪费大量算力。

具体案例:某算法团队用 vLLM 跑 benchmark,每条请求 = 560 token 示例 + 80 token 问题。开启 Prefix Cache 后,评测总耗时下降 65%,GPU 利用率从 35% 提升至 78%。

场景三:同前缀高并发内容生成

阶段:应用服务后端

解决的问题:营销文案平台用同一套“品牌语气 + 格式要求”模板为数千名用户生成不同变体,模板部分完全相同,高并发下重复计算严重。

具体案例:某内容平台在生成服务中开启前缀缓存后,同一模板的并发请求命中率超过 85%,平均首 token 响应时间从 2.5 秒降至 0.8 秒,用户体感明显改善。

Python 演示

python66 行
"""模拟 LLM 推理中 Prefix Cache 复用共享前缀 KV 的过程."""import hashlib  class PrefixCache:    """前缀缓存: 存共享前缀 KV 块, 支持按内容哈希查找与命中统计."""     def __init__(self):        self._store = {}        self._hit_count = 0        self._total_count = 0     def lookup(self, prefix_tokens):        """按前缀 token 序列取缓存 KV, 命中返回 KV 块, 未命中返回 None."""        self._total_count += 1        key = self._hash(prefix_tokens)        kv = self._store.get(key)        if kv is not None:            self._hit_count += 1        return kv     def store(self, prefix_tokens, kv_blocks):        """首次计算的共享前缀 KV 写入缓存表."""        self._store[self._hash(prefix_tokens)] = kv_blocks     def _hash(self, tokens):        return hashlib.sha256(' '.join(map(str, tokens)).encode()).hexdigest()     @property    def hit_rate(self):        if self._total_count == 0:            return 0.0        return self._hit_count / float(self._total_count)  def compute_kv(tokens, cache, shared_prefix):    """模拟一次请求: 前缀命中则复用缓存 KV, 否则全量计算并写入缓存."""    if tokens[:len(shared_prefix)] == shared_prefix:        hit = cache.lookup(shared_prefix)        if hit is not None:            suffix = tokens[len(shared_prefix):]            merged = dict(hit)            merged.update({t: 'kv_new' for t in suffix})            return merged, True    kv = {t: 'kv_new' for t in tokens}    prefix_kv = {t: 'kv_cached' for t in shared_prefix}    cache.store(shared_prefix, prefix_kv)    return kv, False  def main():    cache = PrefixCache()    shared_prefix = [1, 2, 3, 4]    requests = [        [1, 2, 3, 4, 100, 101],        [1, 2, 3, 4, 200, 201],        [5, 6, 7, 8, 300, 301],        [1, 2, 3, 4, 400, 401],    ]    for idx, req in enumerate(requests, 1):        _, hit = compute_kv(req, cache, shared_prefix)        print('请求%d: %s, 命中率=%.2f%%' % (idx, '前缀命中' if hit else '重新计算', cache.hit_rate * 100))  if __name__ == '__main__':    main()
目的:用几十行代码复现 Prefix Cache 的核心逻辑——共享前缀首次全量计算并存入缓存,后续相同前缀请求直接复用缓存 KV,并实时统计命中率。
  1. key = self._hash(prefix_tokens)把前缀 token 序列压缩成哈希“配方牌”,作为缓存查找的钥匙,避免逐 token 比对。
  2. kv = self._store.get(key)在公共显存表里查这张“配方牌”,命中就直接取出 KV 块。
  3. if tokens[:len(shared_prefix)] == shared_prefix:请求级入口先判断“前端是否带了相同的公共前缀”,相当于确认顾客点的汤底是不是同一锅。
  4. hit = cache.lookup(shared_prefix)只拿共享前缀去查缓存,用户输入的专属部分不参与匹配。
  5. cache.store(shared_prefix, prefix_kv)首次计算后把共享前缀的 KV 存入公共缓存,供后续请求复用。
  6. merged = dict(hit)命中时把缓存的 KV 块复制出来,与新增 token 的 KV 合并成完整缓存,模拟部分命中流程。
  7. for idx, req in enumerate(requests, 1):依次处理四条模拟请求,观察第二次、第四次请求命中,第三次因前缀不同而“重新熬汤”。

常见误区

误区我一直以为 Prefix Cache 就是把整条 prompt 的结果缓存下来,下次遇到一模一样的请求直接返回,请求内容差一个字符就完全没用。

正解Prefix Cache 缓存的是“共享前缀”部分的 KV Cache,而不是整条请求的生成结果。只要请求开头的若干 token 与已缓存前缀连续一致,就能复用这部分 KV,只计算用户输入那一段。就像面馆里顾客换了浇头,碗底的高汤还是一样舀。

为何常见:因为直觉里“缓存”指浏览器缓存整页这类“全有或全无”的机制,忽略了前缀缓存是“部分命中、部分复用”。

误区看到评测说开启前缀缓存延迟降低 10 倍,就以为线上随便开个开关所有请求都快了。

正解加速效果取决于前缀占比。自由问答、无系统提示词、每个请求开头互不相同的场景里,共享前缀为 0,命中率趋近 0,不仅没收益,哈希表和缓存显存的开销反而是负担。10 倍加速都是模板化、长前缀场景的数字。

为何常见:评测基准为了展示能力,通常选用最有利的模板化场景,读者容易把“场景特定收益”推广成“全局收益”。

误区缓存不是省了计算吗?省下的显存应该更多才对,怎么会拖慢推理?我一直觉得前缀缓存是零成本白赚的。

正解缓存的前缀 KV 必须常驻 GPU 显存,缓存 1000 种不同的前缀就要多占 1000 份前缀显存。显存上限设置不当会挤占生成阶段的 KV 空间,反而降低批大小和吞吐,所以才需要 LRU 淘汰“冷汤”。

为何常见:只看到“省计算”的一面,忽略了“占存储”的另一面——天下没有免费的午餐。

误区只要前缀里的文字一样,命中条件不就满足了吗?位置什么的应该无所谓吧。

正解Transformer 的旋转位置编码(RoPE)会把位置信息融进 K 向量,相同 token 在不同位置产生的 KV 不同。命中要求“从第 0 个 token 开始、逐块连续相同”,中间任何一块对不上,后面的 KV 都因上下文改变而必须重算——这正是“前缀对齐”的含义。

为何常见:把 KV 想成词表的静态查表结果,忽略了它其实是“位置 + 上下文”共同决定的动态向量。

面试题

Q1. 前缀缓存(Prefix Cache)的核心思想是什么?

单选题 · easy

  • A.缓存所有历史请求的完整回复
  • B.复用共享前缀的 KV Cache← 正确答案
  • C.直接减少模型的参数量
  • D.压缩缓存中 K 和 V 的位宽

答案:B。Prefix Cache 的本质是复用共享前缀的 KV Cache,像面馆复用同一锅高汤。A 缓存的是完整生成结果,属于响应缓存而非 KV 缓存;C 是模型压缩手段;D 是 KV 量化,属于另一类优化方向。

Q2. 下列哪类请求最适合用前缀缓存加速?

单选题 · easy

  • A.完全随机的长文本问题
  • B.共享长系统提示词请求← 正确答案
  • C.长度很短的单个提问
  • D.无提示词的单轮问答

答案:B。前缀缓存收益来自共享前缀的复用。B 中共享前缀很长且反复出现,命中率最高。A 每次开头不同、C 前缀太短、D 没有系统提示词,都几乎没有可复用的共享部分。

Q3. 单个 token 的 KV Cache 显存大小与哪项无关?

单选题 · mid

  • A.模型的层数与头数
  • B.注意力头的维度大小
  • C.模型训练数据规模← 正确答案
  • D.KV 中每个元素的字节数

答案:C。单 token KV 显存 = 2 × 层数 × 头数 × 头维度 × 元素字节数。层数、头维度、存储字节数都直接相关,而训练数据规模只影响模型训练阶段,与推理期 KV 占用量无关。

Q4. 工程上判断前缀能否复用的常用手段是什么?

单选题 · mid

  • A.逐字比较全部前缀 token
  • B.按块哈希值进行匹配← 正确答案
  • C.随机抽取部分字符比对
  • D.依赖人工审核与标注

答案:B。逐字比对复杂度太高,工程上把前缀切成等长块并记录内容哈希,命中判定就是逐块比对哈希。块级哈希支持“部分命中”——前面连续匹配的块复用,断开之后的块重算,成本低又可扩展。

Q5. 多轮对话场景中前缀缓存收益明显的主要原因是什么?

单选题 · mid

  • A.每轮请求前缀越来越长← 正确答案
  • B.生成的结果文本会变短
  • C.模型的参数量被冻结
  • D.采样时的温度更稳定

答案:A。多轮对话每轮请求的前缀 = 系统提示词 + 全部历史消息,轮次越多共享前缀越长,命中率与收益同步上升。这与生成长度、参数冻结、采样温度都没有关系。

Q6. 关于缓存淘汰策略,下列叙述正确的是?

单选题 · hard

  • A.缓存永不淘汰以保证命中
  • B.采用 LRU 淘汰低频前缀← 正确答案
  • C.淘汰策略与显存大小无关
  • D.命中率不受淘汰策略影响

答案:B。显存有限,缓存不可能无限增长。LRU(最近最少使用)按最近命中时间驱逐最冷的前缀,是最常见的做法。A 会撑爆显存;C 和 D 都错误——淘汰策略恰恰是为了适配显存并直接影响命中率。

Q7. 前缀缓存命中后,主要节省了哪部分开销?

单选题 · easy

  • A.模型权重存储的显存空间
  • B.前缀 token 的注意力计算← 正确答案
  • C.词汇表查找的时间开销
  • D.采样生成阶段的解码次数

答案:B。命中后,共享前缀 token 的预填充计算被跳过,节省的是前缀 token 的注意力计算(K/V 计算与存储)。模型权重显存、词表查找、解码次数都与前缀缓存无直接关系。

Q8. 下列哪些场景适合使用前缀缓存?

多选题 · hard

  • A.固定系统提示的对话服务← 正确答案
  • B.Few-shot 模板批量评测← 正确答案
  • C.每次完全独立的新问题
  • D.相同提示词的大批量生成← 正确答案

答案:ABD。A、B、D 都有稳定且反复出现的共享前缀,命中率高,收益明显。C 每条请求开头互不相同,共享前缀为 0,缓存无法命中,属于典型的“高汤策略无效”场景。

Q9. 缓存前缀需要付出哪些代价?

多选题 · hard

  • A.占用额外的 GPU 显存← 正确答案
  • B.维护哈希索引的开销← 正确答案
  • C.降低生成文本的质量
  • D.引入淘汰策略的管理← 正确答案

答案:ABD。缓存 KV 常驻显存、哈希表与引用计数消耗管理资源、LRU 等淘汰机制带来调度开销,这些都要付出代价。但缓存不会改变模型权重与解码逻辑,因此不会降低生成文本质量。

Q10. 关于前缀缓存命中率,下列说法正确的是?

多选题 · mid

  • A.命中率是共享前缀占比← 正确答案
  • B.命中率越高首 token 越快← 正确答案
  • C.命中率始终接近百分百
  • D.前缀多样性会降低命中率← 正确答案

答案:ABD。单条请求命中率 = 共享前缀长度 / 提示总长;命中率越高,预填充越短,首 token 越快。但命中率受前缀多样性影响很大——前缀种类越多、分布越分散,缓存越难命中,C 的说法不成立。