ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

KV Cache分级存储:SSD与IO Die如何突破大模型推理内存墙

KV Cache分级存储:SSD与IO Die如何突破大模型推理内存墙

1. 从一笔收购案说起:为什么KV Cache、SSD和IO Die会被放在同一句话里

AMD收购MEXT这件事,在圈内引起的讨论大多集中在"AI推理加速""内存墙"这些大词上。但如果你把这三个关键词——KV Cache、SSD、IO Die——单独拎出来看,会发现它们其实指向同一个底层问题:数据从存储介质到计算单元的搬运效率,正在成为大模型推理的真正瓶颈。

我做推理优化有几年了,从早期在单卡上跑7B模型,到后来折腾多卡多级缓存,踩过的坑基本都围绕一个核心矛盾:算力涨得比内存带宽快,内存带宽又涨得比存储IO快。KV Cache是Transformer推理过程中必须驻留的中间状态,SSD是容量最大但延迟最高的存储层级,IO Die则是CPU/GPU内部负责与外界交换数据的"咽喉"。这三者被同时提及,说明MEXT的技术路线很可能是在做跨层级的KV Cache管理——把不常用的KV块下沉到SSD,用IO Die的高带宽通道做搬运,从而在有限的高速内存里塞下更长的上下文。

这篇文章不打算复述新闻稿,而是从数据通路的角度,把KV Cache为什么吃内存、SSD为什么能参与进来、IO Die在其中扮演什么角色,一层层拆开讲清楚。如果你正在做长上下文推理、多轮对话服务,或者单纯对"内存不够用"这件事感到头疼,下面的内容应该能给你一些可落地的思路。

2. KV Cache到底吃了多少内存:一笔算得清的账

2.1 从注意力机制推导KV Cache的显存占用

要理解为什么业界对KV Cache如此敏感,得先把它的大小算清楚。Transformer的自注意力机制里,每个token在每一层都会生成Query、Key、Value三个向量。推理时Query不需要缓存,但Key和Value必须保留,因为后续每个新token都要和前面所有token的K、V做注意力计算。

单个token在单层的KV Cache大小是:2 × num_heads × head_dim × dtype_bytes。其中2代表K和V两份,num_heads是注意力头数,head_dim是每个头的维度,dtype_bytes是数据类型字节数(FP16就是2)。

拿一个典型的70B模型举例:80层,64个注意力头,head_dim为128,FP16精度。那么单个token的KV Cache就是2 × 64 × 128 × 2 × 80 = 2,621,440字节,约2.5MB。听起来不大?但上下文长度到32K时,光KV Cache就要2.5MB × 32768 ≈ 80GB。这已经超过一张80GB显卡的全部显存了,模型权重还没算进去。

这就是为什么长上下文推理如此昂贵。你可以用GQA(分组查询注意力)把KV头数降下来,比如64个Query头只配8个KV头,显存直接降到1/8。但即便如此,128K上下文下KV Cache仍然是十几GB的量级。而且GQA会损失一些模型质量,不是所有场景都能接受。

2.2 为什么"只保留最近窗口"不是万能解

有人会说,那就只保留最近N个token的KV,前面的丢掉不就行了。这就是滑动窗口注意力(Sliding Window Attention)的思路,Mistral等模型用过。但问题在于,很多任务需要"大海捞针"式的长距离依赖——比如你让模型总结一份100页的合同,关键条款可能在第3页,滑动窗口一丢,模型就再也看不到它了。

另一种做法是只保留重要的KV,比如H2O、StreamingLLM这些工作,通过注意力分数筛选出"重击token"(attention sink)和近期token。这确实能省内存,但筛选本身有计算开销,而且对某些任务会掉点。我在实际服务中试过StreamingLLM的方案,在对话场景下表现还行,但一到需要精确引用的文档问答,召回率就明显下降。

所以现实是:KV Cache的压缩不能只靠"丢",还需要"存"。把暂时不用的KV块放到更便宜的存储层级上,需要时再取回来,这就是SSD进入视野的原因。

2.3 多级缓存视角下的KV管理

把KV Cache当成一个缓存系统来看,就清晰多了。L1是GPU的SRAM和寄存器,容量极小但带宽极高;L2是HBM显存,容量几十GB,带宽TB/s级;L3是主机DRAM,容量几百GB到TB级,带宽几十GB/s;L4就是SSD,容量TB级起步,但延迟从微秒跳到毫秒级。

传统推理框架只用了L1和L2,KV Cache全放显存。一旦上下文变长或并发变高,显存就爆。而如果把L3和L4利用起来,理论上可以支撑的上下文长度和并发数能提升一个数量级。但这里有个关键问题:从SSD取KV的延迟,能不能被计算掩盖掉?

假设SSD随机读延迟是100微秒,读一个KV块(比如1MB)需要的时间包括寻址、传输、解压。如果每生成一个token都要等SSD,那吞吐直接崩掉。所以必须做预取——在需要某个KV块之前,提前把它从SSD搬到DRAM甚至显存。预取的准确性取决于对注意力模式的预测,这又回到了模型层面。

3. SSD参与KV Cache存储的可行性边界

3.1 延迟与带宽的现实约束

先看一组实测数据。我用过的一款企业级NVMe SSD,顺序读带宽约7GB/s,随机读IOPS约100万,4K随机读延迟约80微秒。而HBM的延迟是纳秒级,DRAM是百纳秒级。两者差了三个数量级。

这意味着SSD绝对不能放在推理的关键路径上。任何"生成token时实时从SSD读KV"的设计都是不可行的,除非你的batch size极小且对延迟完全不敏感。正确的做法是异步预取:在GPU计算当前层的时候,后台线程把下一层或下一批token需要的KV从SSD读到DRAM的pinned memory,再用CUDA的异步拷贝传到显存。

这里有个经验值:预取窗口至少要覆盖2-3个token的生成时间。如果单token生成耗时20毫秒,那预取必须在40-60毫秒内完成。按7GB/s带宽算,60毫秒能搬420MB,足够覆盖相当长的上下文。但前提是SSD的带宽不能被其他请求抢占,所以生产环境里最好给KV Cache单独挂一块盘。

3.2 写放大与寿命问题

KV Cache不只是读,还有写。每生成一个新token,就要写入新的K和V。如果所有KV都往SSD写,写入量会非常恐怖。假设每秒生成1000个token,每个token 2.5MB,那就是2.5GB/s的持续写入。企业级SSD的DWPD(每日全盘写入次数)通常只有1-3,按这个写入量,一块4TB的盘几天就写穿了。

所以SSD上的KV Cache必须是只写一次、多次读取的模式。具体来说,只在KV块被逐出显存时才写入SSD,而且尽量做顺序写。读取时按块读,块大小要匹配SSD的页大小(通常4KB-16KB)和闪存擦除块大小(几MB),避免读放大。

另一个思路是压缩后再写。KV Cache的数值分布通常比较集中,用FP8甚至INT8量化后,体积能减半到四分之一。量化会损失一点精度,但对已经"冷"的KV块来说,影响可控。我在实验中发现,对超过8K之前的KV做INT8量化,在多数任务上困惑度上升不到0.5%。

3.3 什么场景下SSD方案真正划算

不是所有场景都值得上SSD。如果你的上下文长度稳定在4K以内,显存完全够用,引入SSD只会增加复杂度和故障点。SSD方案真正划算的场景有几个特征:

  • 超长上下文:128K甚至1M token的上下文,显存无论如何装不下。
  • 多轮对话累积:对话历史不断增长,但每轮实际用到的历史KV只是其中一部分。
  • 多租户共享:不同用户的KV Cache可以共享同一块SSD,按需换入换出。
  • 成本敏感:用SSD换显存,单token成本能降一个数量级。

我做过一个粗略测算:同样支持128K上下文,纯显存方案需要8张80GB卡,而显存+SSD分级方案用2张卡加一块4TB SSD就能跑,硬件成本降到三分之一以下。代价是首token延迟增加(需要从SSD加载历史KV),但后续token的生成速度几乎不受影响。

4. IO Die:被忽视的数据搬运枢纽

4.1 IO Die在芯片里的位置和职责

IO Die这个概念在AMD的Zen架构里已经存在好几代了。简单说,CPU或GPU芯片内部并不是所有部分都跑在同一个时钟域和电压域。计算核心(CCD)用最先进的制程跑高频,而IO部分(内存控制器、PCIe控制器、Infinity Fabric接口)用成熟制程单独做一块Die,通过先进封装连在一起。

这样做的原因是:IO电路对制程微缩的收益远不如计算电路,用7nm甚至5nm做PCIe PHY是浪费。把IO单独拆出来用12nm或14nm做,成本低得多,而且模拟电路在成熟制程上更稳定。

但IO Die的意义不止于省钱。它实际上是所有外部数据进入计算核心的必经之路。SSD的数据要通过PCIe进入IO Die,再通过Infinity Fabric送到CCD的缓存里。DRAM的数据也要经过IO Die的内存控制器。所以IO Die的带宽和延迟,直接决定了SSD方案的上限。

4.2 为什么IO Die的带宽会成为瓶颈

以PCIe 5.0 x16为例,理论带宽是64GB/s(双向)。但这是物理层带宽,实际有效带宽要打七八折。而且这个带宽是CPU和GPU、SSD、网卡等所有设备共享的。如果SSD预取KV Cache的同时,还有网络请求和磁盘IO,带宽很快就不够分了。

更关键的是,数据从SSD到GPU显存,路径是:SSD → PCIe → IO Die → Infinity Fabric → CPU内存 → PCIe → GPU。如果走GPUDirect Storage,可以绕过CPU内存,路径变成:SSD → PCIe交换机 → GPU。但即便如此,数据仍然要经过GPU内部的IO Die(或者叫PCIe控制器模块)才能进入显存。

所以IO Die的队列深度、仲裁策略、错误处理能力,都会影响KV Cache搬运的效率。我在调试时遇到过一种情况:SSD顺序读能跑满7GB/s,但一旦和网络流量混在一起,KV预取的带宽就掉到2GB/s以下,导致预取超时,推理延迟抖动严重。后来把SSD挂到独立的PCIe通道上才解决。

4.3 从IO Die视角看MEXT的可能设计

MEXT被AMD收购,最合理的推测是它的技术能强化AMD在数据搬运方面的能力。AMD有Infinity Fabric、有Chiplet封装经验、有MI系列加速器,缺的是把SSD这种慢速大容量存储和高速计算单元高效连接起来的软件栈。

如果MEXT做的是KV Cache的跨层级管理中间件,那它需要解决的核心问题包括:KV块的元数据管理(哪些块在显存、哪些在DRAM、哪些在SSD)、预取调度(根据注意力模式预测下一步需要哪些块)、数据搬运的流水线化(用IO Die的DMA引擎做异步拷贝)、以及故障恢复(SSD读失败时如何降级)。

这些问题的难点不在单个技术,而在端到端的延迟控制。从SSD读一个KV块,经过IO Die、DRAM、再进显存,每一步都有排队和协议开销。要把总延迟压到可接受范围,必须做全链路的优化,而不是只优化某一段。

5. 把三级存储串起来:一个可落地的KV Cache分级方案

5.1 整体架构与数据流

基于上面的分析,我设计了一个KV Cache分级管理的原型方案,在实验室环境跑通过。整体分三层:

  • 热层(显存):存放当前注意力窗口内的KV,以及被预测为高概率使用的KV块。用LRU加注意力分数做淘汰。
  • 温层(主机DRAM):存放最近被逐出显存但可能还会用到的KV块。用pinned memory,方便DMA。
  • 冷层(NVMe SSD):存放长期不用的KV块,按对话ID或文档ID分区,顺序写入。

数据流是这样的:新生成的KV先写显存热层。热层满了,根据注意力分数和LRU选出淘汰块,异步写到DRAM温层。温层也满了,最冷的块压缩后写到SSD冷层。读取时反向:先查热层,未命中查温层,再未命中查冷层。冷层读取时触发预取,把相邻的块也一起读上来。

5.2 预取策略与注意力模式预测

预取是这套方案的核心。我试过几种策略:

  • 顺序预取:假设注意力是局部的,预取当前token位置前后的KV块。实现简单,但对"大海捞针"式任务无效。
  • 基于注意力分数的预取:记录历史注意力分布,预测哪些位置的KV会被关注。需要额外存储注意力图,开销不小。
  • 基于块重要性的预取:用H2O等方法给每个KV块打分,高分块常驻热层,低分块下沉。预取时优先取高分块。

实测下来,第三种在多数任务上表现最稳。具体做法是:每个KV块维护一个累积注意力分数,每经过一次注意力计算就更新。分数低于阈值的块被标记为"冷",可以下沉。预取时按分数从高到低取,直到填满预取窗口。

这里有个调参经验:预取窗口不要设太大,否则会挤占正常IO带宽。我一般设成热层容量的20%-30%,既能覆盖大部分命中,又不会造成IO拥塞。

5.3 一致性保证与故障降级

分级存储最怕的是数据不一致。比如一个KV块正在从SSD往显存搬,同时模型又要更新这个块(虽然KV在推理时是只读的,但多轮对话中历史KV可能被重新计算)。我的做法是给每个KV块加版本号,读取时校验版本,不一致就丢弃重取。

故障降级也很重要。SSD不是100%可靠的,读失败时不能直接崩掉推理服务。我的方案是:SSD读失败时,回退到DRAM温层找旧版本,如果还没有,就重新计算该位置的KV(代价高但能保证正确性)。同时记录失败块,后续不再往SSD写这些块,避免反复失败。

另外,SSD的写入寿命要监控。我设了一个阈值,当剩余寿命低于20%时,自动停止往SSD写新块,只读不写,并告警。这样能给运维留出换盘时间。

6. 实测数据与几个反直觉的发现

6.1 延迟分布比平均延迟更重要

在实验室用128K上下文、batch size为8的场景测试,纯显存方案的首token延迟约2秒,分级方案约3.5秒。看起来只多了1.5秒,但延迟分布差异很大。纯显存方案的P99延迟约2.5秒,分级方案P99到了8秒——因为偶尔会遇到SSD读放大或IO拥塞。

这说明平均延迟会骗人。对于在线服务,P99延迟才决定用户体验。要压P99,必须做IO隔离和优先级调度。我给KV预取的IO请求设了最高优先级,网络和日志IO设低优先级,P99从8秒降到了4秒左右。

6.2 压缩的收益不是线性的

我试过对冷层KV做INT8量化,体积减半,SSD读取时间也减半。但解压需要时间,而且解压后的数据要重新排布才能进显存。实测下来,端到端延迟只降了20%左右,不是预期的50%。后来发现瓶颈在解压后的数据搬运上,不是SSD读取本身。

所以压缩要用在带宽受限而非延迟受限的场景。如果SSD带宽是瓶颈,压缩能显著提升有效带宽;如果延迟是瓶颈,压缩反而增加了解压这一环,收益有限。

6.3 多租户共享SSD需要硬隔离

一开始我让多个推理实例共享一块SSD,结果一个实例的大量预取把IO带宽占满,其他实例全部超时。后来改成按实例分区,每个实例独占一部分SSD容量和IO队列,问题才解决。如果非要共享,至少要用cgroup的io.max做带宽限制,但隔离效果不如物理分区。

7. 这套思路还能往哪里延伸

KV Cache分级管理只是开始。同样的思路可以扩展到模型权重的分级加载——把不常用的专家(MoE场景)或层放到SSD,按需加载。也可以扩展到多模态场景,把图像、视频的嵌入向量做分级存储。

IO Die的优化空间也很大。如果能在硬件层面支持KV块的直接内存访问(类似GPUDirect Storage但更细粒度),延迟还能再降。AMD收购MEXT后,如果能把软件栈和硬件特性结合起来,比如用Infinity Fabric的QoS机制给KV流量开专用通道,那这套方案的上限会高很多。

我在实际部署中最大的体会是:分级存储的难点从来不是存储本身,而是预测和调度。你知道数据在哪、知道怎么搬,但不知道什么时候该搬什么。把预测做准了,SSD的毫秒级延迟完全可以用流水线掩盖掉;预测做不准,再快的SSD也救不了。所以如果让我给做类似方案的同行一个建议,那就是先把注意力模式的数据采起来,用真实trace去训练预取模型,比拍脑袋调参有效得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进