想象你经营一家开在写字楼里的粤菜餐厅。灶台只有三平方米,但每晚要出三百道菜。厨师不可能把三百道菜的备料全堆在灶台上——放不下,火星还可能燎着菜叶。于是,最常用的十盘料放在手边灶台,用得少的放后厨冰箱,几乎不用的放地下冷库。真正让餐厅运转流畅的,是一位备菜员:他总能在厨师转身的一瞬间,把下一道菜需要的料从冷库端上灶台,绝不让人干等。 大模型的推理过程,就是一场“连续炒菜”:每生成一个新词(一道新菜),模型都要把此前所有词(已备好的料)都“看”一遍——这些料,就是 KV Cache。显存是灶台,CPU 内存是冰箱,SSD 是地下冷库。KV Cache 卸载(offloading)要解决的,正是“灶台放不下、又不让厨师停下”的难题。本文从“为什么要缓存”讲起,逐层拆解显存瓶颈、分层存储、卸载策略、预取机制、性能权衡和工程实践,让你完整看懂这套显存与内存的分层优化大法。
一句话职业价值:掌握显存/内存分层卸载与预取,你就能在固定预算的 GPU 上服务更长上下文、更高并发,直接降低推理成本与部署门槛。
目录
可视化演示
本节配有交互动效(进入视口后自动加载,约 3KB)。
GPU 显存 CPU 内存 Offload ↓ ↑ Prefetch KV Cache 分层卸载:热数据驻留 GPU 显存,冷数据异步卸载至 CPU 内存,在推理计算间隙预取所需 KV 块,平衡显存占用与推理延迟。
一、缓存缘起:为什么每道菜都要备料
后厨做“白切鸡”,最费时的是杀鸡、褪毛、过冰水——真正下锅只要几十秒。聪明的厨师会一次性备好十份料,客人点单时只需下锅。大模型也一样:处理一段上千字的文档,最贵的一步是“预填充”(prefill),把每个词都过一次网络,算好所有中间表示。这就像头一天的备料,又慢又花工夫。
当模型逐词生成时(增量解码 decode),每生成一个新词,注意力机制都要把它与前面所有词“配对打分”。如果不缓存,模型就得把前面每个词的 Key 和 Value 重新算一遍——等于每上一道菜都把鸡重新杀一遍。KV Cache 就是把这些算好的 Key、Value 投影存起来,供后续 token 重复使用。这正是“键值重复利用”:用一份显存,换掉每次 O(序列长度) 的重复矩阵乘法。
KV Cache 有多大?它随序列长度线性膨胀,公式如下:
所以缓存是“省了重复劳动,占了宝贵空间”。备料越多,灶台越挤——KV Cache 帮我们省了算力,却把压力转嫁给了显存。下一节就来算这笔账。
- 系数 2(两份投影)每个 token 需要保存 Key 和 Value 两组矩阵,体积直接翻倍。
- 层数 $N_{layer}$Transformer 每一层都有独立的注意力投影,层数越多缓存越厚。
- 注意力头数 $N_{head}$多头注意力中每个头单独维护一组 K/V,头越多缓存条数越多。
- 每头维度 $d_{head}$每个注意力头的投影向量维度,决定单个头存多少个数。
- 已生成 token 数 $N_{tok}$序列越长,堆在“灶台”上的备料盘越多——线性增长的来源。
- 每元素字节数 $b$FP16 精度下每个数占 2 字节,直接影响总字节数。
二、显存瓶颈:灶台只有三平方
假设餐厅只有一张一米二的小灶台,却接到一个三百人的婚宴订单。备料充足,但灶台放不下三十盘菜,厨师连转身都难,火候一乱,整桌菜全废。大模型的处境一模一样:GPU 显存就是那张灶台,KV Cache 的盘子正一盘接一盘端上来。
技术侧的具体数字更触目惊心。以 32 层、32 头、每头 128 维、FP16 精度(每数 2 字节)的模型为例,每个 token 的 KV 缓存约 512 KB。当上下文达到 128K 时,单条序列的 KV Cache 就接近 64 GB——一块 80 GB 的 A100 刚够装,但模型权重和激活值也要占显存,直接爆掉。KV Cache 随序列长度的线性增长,是“上下文限制”的根源。
显存满了会怎样?批大小(batch size)被压缩到 1——一桌只能坐一位客人;GPU 算力大量闲置——灶台空转;上下文被截断——菜单砍半。出路不是把灶台变大,而是给冰箱和冷库派活:分层存储,这正是 KV Cache offloading 的核心思想。
- 总占用 $M_{KV}(T)$序列长度为 T 时 KV Cache 的总字节数。
- 存储单价 $\alpha$每增加一个 token 平均增加的字节数,是“线性增长”的斜率。
- 层数与头数相乘层数 × 头数决定一份 K/V 需要存多少份副本。
- 维度乘字节$d_{head} \times b$ 决定单份投影向量占几个字节。
- 线性增长后果T 翻倍则 M 翻倍,这是长序列把显存打爆的根本原因。
三、分层存储:灶台、冰箱与冷库
餐厅有一条铁律:今晚要出的菜放灶台,明天要用的放冰箱,一个月后用的放冷库。取料速度完全不同——灶台秒取,冰箱十几秒,冷库要五分钟。KV Cache 也一样:GPU 显存最快但最小,CPU 内存次之但大得多,SSD 最慢却几乎无限。所谓“分层存储”,就是把缓存按访问热度放进不同层。
offloading 的基本动作很朴素:热数据(最近生成的几块 KV)驻留显存,冷数据(早期 token 的 KV)卸载到 CPU 内存甚至 SSD。KV 以“块”为单位管理,而不是逐 token 搬运——这就是“块粒度管理”,每个块包含固定数量的 token 键值对,便于定位、搬运和驱逐。分层越多,能装的上下文越长,代价是跨层访问变慢。
于是需要一个指标衡量分层方案的好坏——平均访问延迟:
命中率越高,整体越接近显存速度。所以分层存储的命门,是“谁住灶台、谁下冷库”的决策——这就是卸载策略要回答的问题。
- 平均访问延迟 $T_{avg}$把命中显存与未命中(去内存/SSD)两种情况按概率加权。
- 命中率 $p_{hit}$注意力读取 KV 时,目标块恰好驻留显存的比例。
- 显存延迟 $T_{vram}$直接从显存读 KV 的耗时,纳秒级。
- 未命中概率 $(1 - p_{hit})$目标块不在显存、需要跨层搬运的概率。
- 内存开销 $T_{mem}$跨 PCIe 搬运并读取的耗时,是显存的数十到数百倍。
四、卸载策略:谁走谁留
厨师长立下规矩:灶台固定放十盘,新菜上来了,就把最久没动过的那盘收进冰箱;冰箱也满,再把次久没动的推到冷库。这个“最久没用先走”的直觉,在系统里叫 LRU(Least Recently Used,最近最少使用)。KV Cache 卸载同样遵循这个原则:近处常有新 token 来抢占显存,总得有块让位。
但实践中仅看“新旧”不够。系统提示词(system prompt)可能生成后被反复长距离关注,虽然“不新”却“高频”;有些块刚被访问过一次就再也没人碰。因此工程实现会用“优先级打分”综合两个因素:最近使用时间 recency 和累计访问频率 freq,分数最低的块最先被驱逐。
打完分还要设一道“容量预算”:显存中 KV 驻留总量不得超过预算(例如显存的 40%),超了就从分数最低的块开始驱逐。算完新 token 的 KV 后按需卸载、按预算驱逐,这套联动就是“按需卸载 + 优先级驱逐 + 块粒度管理”的经典架构。
不过,光有“赶人”还不够——被赶走的冷块,每隔一阵还会被长距离注意力“点名”。为了不让厨师干等,需要一位有远见的备菜员:预取机制。
- 优先级得分 $score_i$每个 KV 块留在显存的“资格分”,越低越先被驱逐。
- 最近使用 $recency_i$越新被访问,分数越高,保护刚用过的热块。
- 访问频率 $freq_i$长期高频使用的块(如系统提示词)即使不新也值得留下。
- 权重 $\lambda$手工调节“新”与“热”哪个更重要,一般取 0.5~0.8。
五、预取机制:备菜员的远见
从冷库取料要五分钟,如果等客人点了才去拿,厨师就得干瞪眼五分钟。好餐厅的备菜员会盯着订单屏幕:下一道菜要什么,提前五分钟去拿。等厨师伸手时,料已经摆在灶台边。KV Cache 预取的本质,就是这位备菜员。
预取发生在“计算间隙”:生成下一个 token 时,GPU 要跑注意力计算,这段时间 PCIe 总线和 CPU 内存通道是闲着的。预取调度器就在这间隙里,把下一轮注意力需要的 KV 块从内存/SSD 异步搬回显存,传输和计算两条流水线重叠进行。只要预取指令在计算开始前发出,传输时间就能被“藏”在计算背后。
注意“约等于”三个字——预取要准。预测错了(prefetch miss)就白搬一趟,还挤占带宽;重叠不充分时,传输时间仍会漏进关键路径。备菜员不是万能的,下单预测错了,一样白跑腿。
- 单步耗时 $T_{step}$生成一个 token 的平均端到端时间。
- 计算时间 $T_{compute}$GPU 做注意力计算的时间。
- 传输时间 $T_{transfer}$把一块 KV 从 CPU/SSD 搬到显存的时间。
- max 而非加两条流水线并行,总耗时由较慢的一环决定。
- 无预取对比串行执行时是两者相加,预取的核心收益正在于此。
六、性能权衡:省地方还是省时间
用冰箱料做的菜更新鲜,还是冷库料更省电?没有免费的午餐:卸载腾出了灶台空间,却多了搬运开销。系统设计者要算三笔账:第一,延迟隐藏——预取能把传输藏多少;第二,带宽瓶颈——PCIe 是有限通道,搬运本身占用资源;第三,命中率与调度开销——管理块列表、打分、驱逐都要消耗 CPU 时间。
一条现实数据:PCIe 4.0 ×16 约 32 GB/s,GPU 显存带宽却高达 2 TB/s 量级。把 20 GB 冷 KV 从 CPU 内存拉回显存需要接近一秒——如果预取没藏住,这段延迟会直接暴露给用户。因此“把热窗口做小、把命中率做高”是卸载调优的第一原则;命中率达到 90% 以上时,绝大多数请求根本感知不到卸载的存在。
权衡到最后,这是一道经营题:灶台够用就别开冰箱,冰箱不够用才开冷库。工程上由显存预算、目标延迟和请求并发共同决定 p 值(驻留比例)与 β 值(重叠率)的设定。
- 加速比 $S_{speedup}$预取比无预取快多少倍。
- 分子(无预取)没有重叠时,计算完再传输,串行相加。
- 分母(有预取)只有没被盖住的传输部分 $(1 - \beta) T_{transfer}$ 暴露在关键路径上。
- 重叠率 $\beta$被计算时间覆盖的传输比例,取值 0~1。
- 极限情形β=1 时加速比最大;β=0 时无提升。
七、工程实践:开一家不限量的餐厅
现实中,vLLM、DeepSpeed Zero-Inference、FlexGen 等方案已经把“备菜员”武装到了牙齿:分页 KV(像操作系统管理虚拟内存一样按页管理)、异步拷贝队列、多级卸载流水线。它们的共同目标是:在资源受限的显卡上跑更长上下文,以更高并发服务更多请求,并摊薄每一 token 的推理成本。
工程上最核心的收益公式是并发上限。假设显存总量减去模型占用后,剩余空间能装多少条请求的 KV:
举个例子:单条请求 KV 完整体积 10 GB,可用显存 40 GB,不卸载(p=1)只能并发 4;卸载到 p=0.3 后并发提升到约 13,吞吐翻了三倍。离线批处理场景可以把 p 压得更低搏吞吐,在线对话则保持高 p 保延迟。经营账由此算清:同样的卡,接更多客人,摊薄每单成本——这就是 KV Cache offloading 的商业价值。
- 并发上限 $C_{max}$显存能同时容纳的请求条数。
- 可用显存 $M_{VRAM} - M_{model}$显存总量减去模型权重与激活占用的剩余空间。
- 单请求驻留量 $p \times S_{seq}$卸载后单条请求平均只占 p 比例的完整 KV 体积。
- 驻留比例 $p$热数据占比,p 越小腾出的空间越多,但冷数据搬运越频繁。
- 向下取整 $\lfloor \cdot \rfloor$并发条数必须是整数。
AI 应用场景
场景一:长上下文客服机器人
阶段:推理服务部署 / 显存优化
解决的问题:传统方案下,128K 上下文的客服机器人要把全部对话历史的 KV Cache 放在显存,动辄几十 GB,24 GB 消费级显卡直接 OOM,只能忍痛截断历史,导致用户前面说过的话“被遗忘”。采用分层卸载后,最新几轮对话的 KV 驻留显存,早期轮次卸载到 CPU 内存甚至 SSD,再配合预取,机器人可以完整记住 128K 上下文而不爆显存。
具体案例:某电商客服团队用 30B 模型加 KV offloading,上下文从 16K 扩展到 96K,单卡吞吐提升约 3 倍,晚高峰 P95 延迟仅增加 8%,用户满意度回升明显。
场景二:离线批量文档总结
阶段:批处理推理调度
解决的问题:定时跑上千份长文档摘要时,延迟不敏感但吞吐和成本敏感。传统全显存驻留需要大量显卡;FlexGen 式策略把冷 KV 放 SSD,用流水线预取最大化带宽利用,让四张卡干八张卡的活。
具体案例:某金融公司每周生成 500 份研报摘要,原需 8 张 A100,采用 offloading 后 4 张 A100 完成,硬件成本减半;单篇耗时从 40 秒升到 55 秒,完全在批处理的容忍范围内。
场景三:多租户在线推理平台
阶段:在线推理服务 / 并发调度
解决的问题:SaaS 平台在固定 GPU 上服务大量用户,显存被 KV Cache 占满导致并发上不去。卸载腾出显存空间,让更多请求的 KV 块“挤”进显存,配合同步的预取调度,并发数和单位成本同时优化。
具体案例:某 MaaS 平台用 vLLM + KV offload,把驻留比例 p 设为 0.4,单卡并发从 8 提升至 18,每 token 成本下降约 55%,并通过 QoS 调度保证在线请求的关键路径延迟不超标。
Python 演示
"""KV Cache 分层卸载与预取策略的简化模拟对比"无预取"与"带预取"两种方案的总耗时。""" class OffloadSim: def __init__(self, vram_cap=3, cpu_cap=8, bandwidth=2.0, compute=1.0, prefetch=False): self.vram_cap = vram_cap self.cpu_cap = cpu_cap self.bandwidth = bandwidth self.compute = compute self.prefetch = prefetch self.vram = set() self.cpu = set() self.last_use = {} self.clock = 0 self.total_time = 0.0 def load(self, bid): """把块搬入显存;超过容量则按 LRU 驱逐到 CPU/SSD""" if bid in self.vram: return 0.0 if bid in self.cpu: self.cpu.discard(bid) cost = 1.0 else: cost = 1.5 # 从 SSD 读取更慢 self.vram.add(bid) while len(self.vram) > self.vram_cap: victim = min(self.vram, key=lambda b: self.last_use.get(b, -1)) self.vram.discard(victim) if len(self.cpu) < self.cpu_cap: self.cpu.add(victim) return cost def step(self, need, prefetch_bids=()): """处理一个 token:访问 need 中的块,并尝试预取 prefetch_bids""" transfer = 0.0 for b in need: transfer += self.load(b) self.clock += 1 self.last_use[b] = self.clock for b in prefetch_bids: if b not in self.vram: transfer += self.load(b) self.last_use[b] = self.clock if self.prefetch: self.total_time += max(self.compute, transfer / self.bandwidth) else: self.total_time += self.compute + transfer / self.bandwidth def main(): times = {} for prefetch in (False, True): sim = OffloadSim(prefetch=prefetch) for t in range(240): bid = t % 12 need = {bid, (t - 1) % 12} if t % 4 == 0: need.add((t - 6) % 12) future = {(t + 1) % 12} if prefetch else () sim.step(need, future) times[prefetch] = sim.total_time print('无预取总耗时: %.1f 单位' % times[False]) print('带预取总耗时: %.1f 单位' % times[True]) print('预取加速比: %.2fx' % (times[False] / times[True])) if __name__ == '__main__': main()
self.vram = set()用集合管理显存中驻留的 KV 块,相当于“灶台上放了哪几盘料”。victim = min(self.vram, key=lambda b: self.last_use.get(b, -1))LRU 驱逐:优先赶走最久没用过的块,对应“最久没用先走”的厨师长规矩。cost = 1.5 # 从 SSD 读取更慢从冷库取料比冰箱贵,模拟 SSD 与内存的带宽差异。transfer += self.load(b)每次访问冷块都产生搬运成本,这是 offloading 绕不开的开销。future = {(t + 1) % 12} if prefetch else ()备菜员提前把下一轮要用的块列入预取清单。self.total_time += max(self.compute, transfer / self.bandwidth)预取时传输与计算重叠,耗时取 max 而非加,这正是核心收益来源。
常见误区
误区把 KV 搬到内存/SSD,模型不就“记忆模糊”了吗?生成质量肯定会变差,所以卸载只适合不重要的场景。
正解不会。卸载移动的是缓存数据本身,不涉及压缩或量化,K/V 数值完全不变,注意力计算结果和全驻留显存时逐位一致。变的只是读取速度:命中显存快,命中内存慢。质量不变,速度有差异——这就像备料从冷库拿还是灶台拿,菜的味道一样,只是上菜快慢不同。
为何常见:人们把“系统变慢”误当成“模型变笨”,又看到“卸载”这个词联想到降质,实际二者毫无关系。
误区显存访问最快,KV Cache 当然全部放显存最好;卸载只是显存不够的无奈妥协。
正解这是典型的“只算延迟、不算吞吐”。很多场景显存“刚好能装下”全部 KV,但装下后 batch=1,GPU 算力闲置 90%。主动把冷 KV 卸载一部分,腾出的空间容纳更多请求,总吞吐反而更高。卸载不仅是“装不下”的兜底,更是“提高资源利用率”的主动优化——就像灶台放不下时才开冰箱?不,好厨师的冰箱永远是开着的。
为何常见:初学者只盯着单请求延迟,忽略了并发和单位成本这两个更大的变量。
误区加了预取,传输时间就变成 0 了,跟全驻留显存一样快。
正解预取只能让传输与计算重叠,把延迟“藏”在计算后面,并不能让传输资源凭空消失。带宽仍被占用,可能挤压其他作业;预测不准还会白搬一趟(prefetch miss)。当传输时间超过计算时间时,每步耗时仍由传输主导。备菜员跑得再快,也变不出第二条 PCIe 通道。
为何常见:把“重叠”误解为“消失”,忽略了物理带宽的上限。
误区KV Cache 和模型权重一样,是训练好的参数,应该保存下来长期复用。
正解KV Cache 是推理中间产物:针对某条输入序列,把它的 Key/Value 投影缓存下来避免重复计算。换一条输入,缓存完全失效;请求结束,缓存随之释放。模型权重才是跨请求复用的持久化参数。就像备好的菜料只属于今晚这场宴席,明天换菜单就得重新备。
为何常见:两者都叫“Cache“,有人便类比预训练权重,混淆了”持久参数“和”会话中间态”。
误区7B 这种小模型显存装得下 KV Cache,根本不用学卸载。
正解是否需要卸载取决于“KV Cache 相对显存是否紧张”,而不是模型大小。7B 模型读 200K token 的长篇小说,KV 照样几十 GB;再加上多请求并发,KV 总量急剧膨胀,小模型一样打爆 24 GB 显卡。offloading 是显存资源管理的通用手段,与模型参数量无必然关系。
为何常见:只按“单请求 × 单模型”估算显存,漏掉了上下文长度和并发数这两个乘数。
面试题
Q1. KV Cache 在推理中缓存的主要内容是?
- A.模型的全部权重参数
- B.已生成 token 的键值投影← 正确答案
- C.优化器里的梯度状态
- D.词表的嵌入向量表
答案:B。KV Cache 缓存的是已生成 token 的 Key 和 Value 投影矩阵,用于后续 token 的注意力计算,避免重复投影。模型权重是持久参数,梯度只在训练中出现,词表嵌入是输入映射,都与“缓存历史 token 中间结果”无关。就像备好的料是“菜料”而非“菜刀”。
Q2. KV Cache 总大小随序列长度怎样变化?
- A.线性增长← 正确答案
- B.对数增长
- C.平方级增长
- D.保持不变
答案:A。每个新 token 都会新增一份 K/V,总量与序列长度成正比,即线性增长。不是平方(注意力计算量才是平方级),也不是对数或恒定。灶台上的备料盘是“每道菜多一盘”,而不是“每道菜多一桌”。
Q3. 卸载策略中“热数据”通常指?
- A.离当前生成位置较近的 KV← 正确答案
- B.序列最开头的 KV 块
- C.词频较高的词汇投影
- D.显存中的所有缓存块
答案:A。热数据指最近生成、马上还要被注意力多次读取的 KV 块,通常就是离当前生成位置最近的若干块。序列开头的 KV 往往已变冷,词频是文本统计概念,与缓存热度无关;显存中并非所有块都热,否则就不需要分层了。
Q4. KV Cache 分层卸载常见的存储层级有?
- A.GPU 显存← 正确答案
- B.CPU 内存← 正确答案
- C.SSD 磁盘← 正确答案
- D.路由器缓存
答案:ABC。主流分层是 GPU 显存(灶台)、CPU 内存(冰箱)、SSD 磁盘(冷库)三级。路由器缓存不属于推理服务器的存储体系,不参与 KV Cache 的驻留与卸载。层级选择原则是“越快越小、越慢越大”。
Q5. 预取机制的主要收益来自哪里?
- A.传输与计算在时间上重叠← 正确答案
- B.把 KV Cache 压缩得更小
- C.减少模型参数的计算量
- D.提高注意力计算的精度
答案:A。预取在“计算间隙”异步搬运 KV 块,让传输时间和 GPU 计算时间重叠,从而使单步耗时从“计算+传输”降为“两者取 max”。它不压缩缓存、不减少计算量,也不改变精度——只是把备菜员的取料动作藏进厨师炒菜的时间里。
Q6. 理想流水线重叠时,单步生成耗时近似为?
- A.计算时间加传输时间
- B.两者中较大的那个值← 正确答案
- C.两者中较小的那个值
- D.传输时间的两倍数值
答案:B。传输与计算完全并行时,关键路径由较慢的环节决定,因此单步耗时 ≈ max(计算时间,传输时间)。相加是“无预取”的串行情形;取较小值或两倍都没有物理意义。好比备菜员和厨师同时干活,总工时看谁先干完。
Q7. 下列哪些因素决定单个 token 的 KV 占用?
- A.Transformer 的层数← 正确答案
- B.注意力头的数量← 正确答案
- C.每个头的向量维度← 正确答案
- D.词表的整体大小
答案:ABC。单 token 的 KV 体积 = 2 × 层数 × 头数 × 每头维度 × 字节数。层数、头数、维度都直接乘进体积公式。词表大小影响的是输入输出嵌入层,与 KV 缓存无关——它决定“菜名有多少种叫法”,不决定“一盘菜备多少料”。
Q8. 显存容量不足时,通常优先驱逐哪类块?
- A.最久未被访问的块← 正确答案
- B.最近被访问的热块
- C.体积最大的 KV 块
- D.随机挑选的任意块
- E.计算时间加传输时间之和
- F.计算时间减传输时间之差
- G.只算传输时间忽略计算
- H.只算计算时间忽略传输
答案:A。无预取时,注意力要先等冷块从 SSD 搬进显存才能计算,两步串行,总耗时 ≈ 计算时间 + 传输时间。且 SSD 带宽远低于显存,传输时间可能占大头。这正解释了为什么没有预取的 naive offloading 会让人“干等冷库取料”。
Q9. KV Cache 卸载的典型收益包括?
- A.支持更长的上下文← 正确答案
- B.提高并发请求数← 正确答案
- C.降低单卡显存门槛← 正确答案
- D.减少模型的推理计算量
答案:ABC。卸载让 KV 不占满显存,从而支持更长上下文、容纳更多并发请求,也降低了单卡显存门槛。但它不减少推理计算量——注意力矩阵乘法该算还是得算。卸载优化的是“存储布局”,不是“算术量”。好比换更大的仓库不改变做菜的工序,只改变料放哪里。