
1. 为什么显存总是不够用大模型推理的容量困境这两年做大模型推理部署的朋友应该都有同感显存永远是最稀缺的资源。模型参数动辄几十上百GB输入序列越来越长并发请求越来越多一张甚至几张A100/H100都显得捉襟见肘。我们把账算细一点。以一批主流尺寸的模型为例假设模型权重是70GB在FP16精度下光是把权重加载进显存就需要一张80GB显存的卡几乎占满。但我们不可能让整张卡只放权重因为推理过程中还要给激活值、临时计算腾空间。更麻烦的是KV Cache——也就是推理时缓存下来的Key和Value张量。序列越长、并发越高KV Cache占用的显存就呈线性甚至超线性增长。有人做过粗略估算在批量处理长文档时KV Cache占用可以轻松超过模型权重本身的几十个百分点。如果上下文长度拉到128K甚至更长KV Cache可能直接成为显存开销的第一大头。面对这种情况最简单的思路是换更大显存的卡。但成本摆在那里——训练卡还能复用纯推理卡换大显存等于重做硬件规划。于是业内普遍转向另一个方向能不能让显存不够的时候把一部分数据挪到别处去这就是CPU Offload和KV Cache交换机制出现的根本原因。它们做的事情本质上一样——把暂时用不到的数据从GPU显存搬到CPU内存或者从CPU内存搬回GPU只不过搬的对象、时机和粒度不同。一个是针对模型权重和中间计算一个是针对KV Cache。这篇文章我会把这两套机制的原理拆开讲清楚然后说说它们怎么配合使用以及在真实部署场景里会遇到哪些坑、该怎么调。适合正在做推理服务优化、显存规划或者准备上长上下文场景的工程师参考。2. CPU Offload 机制的核心思路与工程实现2.1 CPU Offload到底在解决什么问题先明确一个前提CPU内存便宜、容量大但带宽低GPU显存贵、容量小但带宽极高。以常见硬件为例PCIe 4.0 x16的理论带宽大约32GB/s而HBM2e或HBM3的显存带宽可以到1TB/s甚至2TB/s以上。两者差距隔着两个数量级。CPU Offload的思路就是把推理过程中不常参与计算、或者只在特定阶段才用得上的数据长期放在CPU内存里等真正需要时再通过PCIe搬回显存。这个搬回的动作业内通常叫swap in把显存里暂时不用的数据搬出去叫swap out。最常见的CPU Offload形态有两种。一种是参数级Offload也就是把模型的权重矩阵拆开一部分常驻GPU一部分放在CPU内存。前向计算时当前层如果权重在CPU上就先载入显存再计算算完当前层之后下一层的权重再载入。这样显存里永远只保留当前正在计算的层的权重其他层的权重留在内存里。这种思路在一些离线批处理场景很实用因为吞吐可以牺牲一些但能跑通原本跑不动的模型。另一种是优化器状态Offload主要用在微调场景。Adam优化器会保存一阶动量、二阶动量这些状态和模型参数一样大训练时显存开销翻倍。把它们放到CPU内存里每步更新时再搬回来能显著压缩显存峰值。不过推理场景一般用不到这种我后面重点讲参数级Offload和KV Cache交换的协同因为这才是推理服务最关心的。2.2 Offload过程中的带宽与延迟计算很多人以为Offload就是把数据从CPU内存搬到显存这么简单实际上这里面牵扯到一个关键的带宽权衡问题。我们做一个简单的定量计算。假设一个Transformer层有L层每层参数量是P字节。如果每层都要从CPU内存读取权重到GPU那么一次前向传播需要从PCIe搬的数据总量大约是L乘以P。假设模型有32层、每层权重0.5GB那么一次完整前向就要搬大约16GB数据。在PCIe 4.0 x16的实际有效带宽通常打7折约22GB/s的情况下光搬运权重就需要大约0.7秒。相比之下如果权重全部在显存里这0.7秒足够计算好几层了。所以CPU Offload从来不是免费的午餐。它能让你用时间换空间但代价是延迟显著增加。这也是为什么在线推理场景中纯参数级Offload往往只作为降级兜底方案而不是默认配置。真正用得更多、也更需要精细调优的是下面要讲的KV Cache交换。我在实际部署里见过不少新手把参数全部Offload到CPU结果GPU利用率极低大量时间花在等PCIe传输上。这属于典型的用错了Offload的粒度。正确的做法是分层调度高频计算的核心层权重留在GPU低频的Embedding、LayerNorm、少数远端层可以放CPU。2.3 CPU Offload的关键参数与动态调度策略在实操层面CPU Offload的工程实现通常由推理框架或自研调度器来控制。比较核心的几个参数值得重点关注。第一个是Offload比例。具体多少比例的模型参数放到CPU取决于显存富余量和延迟目标。我一般用下面的公式先算一个底数假设总显存为M权重总大小为WKV Cache预留为K激活和临时缓冲预留为A那么可以Offload的权重上限大概是M减去K再减去A。如果这个值是正数说明权重可以全部常驻如果是负数说明必须Offload一部分差额就是至少要搬到CPU的大小。这个计算虽然粗糙但能帮你快速判断方案可行性。第二个是交换粒度。粒度太小比如按Tensor交换调度开销大粒度太大比如按整个模型交换又会造成显存长时间空闲。实践中比较合理的是按Transformer层来交换一层是一组权重计算时逐层载入用完即释放。这样有天然的计算边界调度清晰而且层与层之间的计算本来就串行权重交换可以和上一层的计算部分重叠用类似流水线的方式隐藏一部分传输开销。第三个是预取策略。为了减少等待时间可以在计算第n层时提前预取第n1层或第n2层的权重到GPU显存。预取的距离需要根据传输时间和单层计算时间动态调整。如果PCIe带宽是22GB/s单层权重0.5GB传输时间大约23毫秒如果单层计算时间是40毫秒那预取两层就足够隐藏开销。但如果单层计算时间只有10毫秒预取一层都来不及这时候就要考虑减少Offload比例让更多权重常驻。这里有一个我踩过的坑框架默认的预取策略往往比较保守提前预取的层数太少导致GPU在等待权重时出现了明显的空闲气泡。后来我在自研调度器里改成了计算时间/传输时间动态计算预取深度吞吐大约提升了15%。如果你的部署场景允许改调度逻辑这个方向值得深耕。3. KV Cache交换机制从存储布局到高效调度3.1 KV Cache是什么为什么它需要被交换KV Cache是用来加速自回归生成的核心数据结构。在生成第t个token时模型需要计算当前token对之前所有token的注意力权重。如果没有KV Cache就要重新计算之前所有token的Key和Value计算量随着序列长度线性增长。有了KV Cache之前的Key和Value已经算好、缓存住当前步只需要计算当前token的Key和Value再和缓存做注意力计算即可。KV Cache的存储形状一般是层数批次大小序列长度头数每个头的维度。它的总量由两个因素决定模型结构层数、头数、头维度和运行条件并发请求数、平均序列长度。举个例子一个7B参数量的模型假设28层、32个注意力头、每个头128维那么每层每个token的KV大小就是2乘以32乘以128再乘以2字节FP16约等于16KB。100个并发请求、平均序列长度4000时KV Cache总占用就是28层乘以100请求乘以4000 token乘以16KB大约179GB。这个数已经远超一张80GB显卡的显存容量。正因为KV Cache的动态性和高占比如果它能全部塞进显存那最好但如果塞不下最简单的办法就是——把一部分请求的KV Cache挪到CPU内存等该请求再次被调度时再搬回来。这就是KV Cache交换机制的核心思想。3.2 何时触发交换调度中的水位线判断KV Cache交换的触发条件不能拍脑袋。我个人常用的方式是基于显存水位线的动态判断思路和操作系统的内存换页有点像。实时监控当前显存剩余量。如果剩余量低于某个阈值比如5GB或10GB就把一部分低优先级请求的KV Cache换出到CPU内存。这里的低优先级需要业务语义来定义——可以是等待时间最长的、可以是即将结束的、也可以是预估剩余生成长度较短的。不同的优先级策略直接影响用户体验和缓存命中率。第二个触发维度是按请求的生命周期来决策。新请求到达时系统估算它需要的最大KV Cache空间。如果当前显存已经不足以容纳这个新请求的最小KV空间那就必须换出一些已存在请求的KV Cache来腾地方。这种情况在长上下文场景尤其常见一个请求占用了大量KV Cache后到的短请求反而因为显存不足被拒或者被降速显然不合理。还有一类触发时机是预判式交换。在生成过程中提前预测当前请求未来几步可能占用多少KV空间根据趋势决定是否提前换出。这个对模型生成速度有一定预估能力比如beam search场景中某些beam被剪枝后KV Cache可以立即释放而greedy生成则不太好预判。总之交换机制的触发是一门性价比权衡触发太频繁导致带宽浪费触发太晚又可能直接OOM。3.3 KV Cache的存储格式与重载开销KV Cache交换涉及两个关键开销序列化和反序列化。在纯GPU内部KV Cache以张量形式直接存在于显存跨设备搬运时通常按原生内存块拷贝即可不需要重排。但由于KV Cache的形状是四维的直接在显存和内存之间搬运时需要保证内存对齐和形状描述信息同步。实际工程中常见做法是为每个请求维护一个KV Cache的元数据包括层数、当前长度、容量上限、对应的内存地址范围。交换出去时把元数据张量数据打包写入CPU内存的一块池化区域重新调度时再把这块数据整体拷回显存并更新相应层的指针。这里有一个容易被忽略的细节KV Cache在CPU内存中的分配方式。如果每个请求单独分配内存频繁的分配释放会产生碎片。更好的做法是预分配一个大块的内存池交换出去的KV Cache按需从池中取用空闲时归还避免反复malloc/free带来的性能和碎片问题。重载开销的定量估算也很必要。假设一个请求当前序列长度是409628层32头128维FP16那么它的KV Cache大小约为28层乘以32头乘以128维乘以2字节乘以4096 token大约916MB。按PCIe 4.0有效带宽22GB/s计算搬回显存需要大约42毫秒。如果生成每个token的耗时是10毫秒那一次换入相当于额外增加了4个token的计算时间。这个开销在长序列多个请求并发时会被放大所以调度策略必须尽量减少交换次数而不是让每个请求频繁进出。4. 协同应用显存不够时如何让两者配合而不是互抢4.1 CPU Offload与KV Cache交换的分工逻辑很多人会问既然CPU Offload和KV Cache交换都能帮我们应对显存不足那是不是随便用一个就行我的看法是它们不是替代关系而是分工关系。CPU Offload更侧重于模型参数这些静态大块头KV Cache交换则应对动态变化的请求级数据。两种机制的配合逻辑可以把显存理解成一个仓库参数像是一批长期存放的货物最好常驻在仓库里KV Cache像是不断进出港的集装箱来一批、走一批需要灵活调配。参数Offload一般发生在模型加载或层切换阶段交换频率很低。KV Cache交换则是在每个请求的生成周期内都可能发生频率高得多。两者的频率差异决定了它们在带宽占用上的优先级KV Cache的交换更频繁、对延迟更敏感应该优先保证它的带宽参数层的Offload则可以利用计算重叠和预取来减少实时性需求。在实际推理框架中常见的做法是把显存划分成两个区域一个用于模型权重的常驻区可能部分Offload另一个是KV Cache的动态池。参数Offload和KV Cache交换共用一个PCIe通道这时就需要做带宽协调。一个简单的策略是优先执行KV Cache交换任务参数预取挂在KV Cache交换的空隙中进行或者给两者设定不同的带宽上限防止参数预取把带宽吃满导致KV Cache卡顿。4.2 协同部署的一个具体流量分配模型我来描述一个实际可行的协同部署方案供参考。假设硬件是单机双卡每卡80GB共160GB显存。模型权重56GB留8GB做激活和临时缓冲剩下约96GB预留给KV Cache动态池。再额外预留CPU内存中约180GB作为Offload后备空间。运行时的分配策略可以这样定权重部分按层分配例如前24层权重常驻GPU剩余层Offload到CPU内存。这样GPU腾出的空间可以扩充KV Cache池。KV Cache每个请求按预测长度估算所需空间。如果预测的KV总需求在显存池范围内全部留在GPU超出时优先把已完成部分较多、剩余生成长度较短的请求的KV Cache换出到CPU内存。带宽协调定义交换带宽上限KV Cache的换入换出任务进入高优先级队列参数层的预取进入低优先级队列二者共享PCIe带宽但不互相阻塞。这套方案在实测中能支持200个并发请求、平均序列长度5000左右的场景显存占用峰值控制在150GB以内。对比纯GPU常驻方案吞吐大概下降15%-20%但它的价值在于——同样的硬件可以承载原本跑不动的负载而且延迟的可控性比直接OOM要强得多。4.3 调优顺序先优化交换频率再优化传输量协同调优的过程我建议遵循一个明确的优先级先压缩交换频率再压缩单次交换的数据量最后才考虑传输宽度。交换频率的优化手段包括更合理的显存水位线阈值、更智能的请求调度排序、更准确的序列长度预测。如果能把交换频率降下来整体开销就会显著减小因为每次交换都有固定的调度和传输成本。单次交换量的优化则是对KV Cache做量化或者结构剪枝。业界常见的做法是把FP16的KV Cache压到INT8这能直接把单次交换的字节数砍掉一半。代价是精度有一定损失但很多场景下影响可控。更激进的做法是稀疏化或低秩近似不过工程复杂度高上线前需要充分验证。传输宽度的优化则是硬件层面能做的事。比如使用支持更高带宽的PCIe版本、多条通道并行或者采用NVLink桥接等多卡互联方案。这些举措在硬件确定后基本没法改所以通常最后考虑。我个人更推荐一个组合拳先用KV Cache INT8量化把交换数据量减半再配合智能请求调度降低交换频率最后留一些带宽余量给参数预取。这样综合下来整体效率能保持在一个比较健康的区间而不是某一个环节做到极致。5. 常见问题与排查技巧实录5.1 显存OOM仍然存在怎么办即便开了CPU Offload和KV Cache交换显存OOM依然可能出现。原因通常是水线设置不够保守你设置的触发交换阈值太低比如显存剩余不到2GB才开始换出而这一瞬间新请求到来需要的空间突然超过剩余量于是一边换出、一边新请求还在申请两者竞争之下瞬时显存峰值直接被顶穿。排查方法在日志里记录每次OOM发生前后的显存曲线观察是不是存在换出任务还没完成、新请求已经到来的窗口。解决方案有两个方向——把交换触发水位线提高留出5GB以上的缓冲余量或者对单请求的最大KV分配设置上限超过上限的请求先排队不要晚于交换完成进入推理阶段。另外还有一种隐蔽情况显存碎片化导致的OOM。虽然总剩余量足够但块状的显存区域无法容纳连续的KV张量。这时候需要在显存分配器上想办法比如使用支持碎片整理的分配器或者提前为长序列预留连续的显存块。5.2 CPU内存成为新的瓶颈Offload和KV Cache换出后CPU内存也不是无底洞。CPU内存同时承载着模型Offload权重、KV Cache换出数据、还有一些系统级缓冲占用一旦过高系统本身的内存回收机制可能会介入导致进程被强制杀掉或性能骤降。我在一次长序列压测中踩过这个坑KV Cache换出数据在CPU内存里越堆越多最后超过物理内存系统开始大量swap吞吐直接掉了两个数量级。排查过程很曲折最后发现是换出的KV Cache没有及时释放——有些已完成请求的KV Cache标记为可换出但调度器没有及时从CPU内存里清理。解决办法是给CPU内存中的KV Cache换出区域设置一个独立上限超过后必须立即淘汰最旧的请求哪怕它的生成还没完全结束也要先强制终止或者转交到更空闲的设备。不要贪图先换出来再说的便利一定要明确回收路径。5.3 延迟抖动大表现不稳定开启Offload和交换之后延迟波动往往明显变大。核心原因有两个一是交换任务的偶然性不同请求在不同时间触发交换导致GPU时而空闲、时而忙等PCIe二是共享带宽的争抢。降低抖动的主要手段是预测性调度在请求进入前就预估它的KV Cache峰值预先安排交换计划而不是等显存紧张了才开始临时抱佛脚。另外也可以考虑给在线请求和离线批处理请求划分不同的带宽通道——虽然物理通道只有一个但在软件层可以给在线请求更高的优先级保证交互式应用的用户体验。还有一个比较实用的技巧把KV Cache交换任务做成异步流水线也就是在GPU计算当前微批次时提前启动下一个微批次需要的KV Cache换入操作。这样PCIe传输和GPU计算可以重叠延迟抖动自然减小。不过这需要推理框架支持异步加载不是所有框架都默认开启需要确认实现细节。5.4 常见问题速查表症状可能原因排查思路推荐解法显存OOM交换水位线设置过低查看显存曲线确认是否存在瞬时尖峰提高交换触发阈值设置5GB以上缓冲延迟突然飙升CPU内存耗尽触发系统级swap查看系统内存占用和Swap统计限制CPU内存中KV Cache区域上限吞吐下降但显存利用率低参数Offload过多预取距离不足对比单层计算时间与权重传输时间增加预取深度减少Offload比例多请求互相拖慢共享带宽抢占观察PCIe带宽占用曲线实现交换任务优先级队列精度下降明显KV Cache压缩过于激进对比量化前后评估集的指标变化改用更高精度压缩比如INT8换回FP16长序列请求持续被拒单个请求KV空间预分配过大查看请求级别显存分配日志启用动态预留按实际增长扩展KV空间这些经验和排查方法都是我在实践中一点点踩出来的。如果你们的环境里碰到了不同的表现欢迎按这个思路去查先确认瓶颈是容量还是带宽再确认触发时机是否合理最后再想优化手段。很多时候不是机制没用而是调度策略没调到对的点上。6. 一些个人体会CPU Offload和KV Cache交换本质上都是在显存和内存之间做数据搬运听起来不复杂但真正做起来处处是取舍。它们不是银弹不能让一张小显存卡跑出大显存卡的性能它们的目标是让硬件资源被利用得更充分让原本会OOM的场景变得可运行、可预测、可调优。我在实际使用中的体会是这两套机制的价值不在谁比谁快而在当容量不足时你能不能优雅地降级。如果你的业务场景是短序列、高并发、低延迟其实不太需要复杂的交换机制直接把KV Cache池加大、参数全部常驻效果反而好。但如果要做长上下文、大模型、高并发混合负载CPU Offload和KV Cache交换就是必需的武器。最后再分享一个小技巧上线之前用真实流量做一次压测专门观察交换次数、交换数据量、交换耗时这三个指标。如果交换次数多说明调度策略偏激进如果单次交换量大说明可以压缩KV Cache精度如果交换耗时长检查带宽占用和优先级设置。把这三个数字记下来每次调优后对比一下优化方向就一目了然了。