HF-23工程级14 min

KV Cache 量化:FP16 / FP8 / Q8 / Q4 速度与智商的取舍

从 KV Cache 的本质出发,深度对比 FP16 / FP8 / Q8 / Q4 四种精度在长上下文与多并发场景下的显存占用、推理速度与能力保留。配合 vLLM 与 llama.cpp 实测数据,给出按场景的选型清单。

KV CacheFP8Q8Q4量化vLLMllama.cpp长上下文

同一个模型、同一张显卡,KV Cache 用 FP16、FP8、Q8、Q4 到底有什么区别?FP16 最聪明、Q8 稍微变笨、Q4 直接智商腰斩?把 KV Cache 从 16 比特压到 4 比特,体积理论上只剩四分之一,生成速度是不是也能快四倍?本文从底层原理出发,结合 vLLM 与 llama.cpp 实测,把这些问题彻底讲清楚。

先分清两个概念:模型权重量化 ≠ KV Cache 量化

这是很多人第一反应就会搞混的事。模型权重量化(比如下载一个 Q4 模型),说的是模型几十亿几百亿个参数本身被压缩到了 4 比特;KV Cache 量化则完全不同——它是模型在推理过程中,随着你不断对话动态产生的一块临时记忆

你可以把大模型想成一个人:模型权重是他脑子里几十年学到的知识(静态的、长期不变的),KV Cache 则像他面前的一张草稿纸——你们刚才聊了什么、文档前面写了什么、代码前面定义过什么,都被记在这张纸上。

所以「模型权重量化」是压缩大脑,「KV Cache 量化」是压缩草稿纸。两者对模型能力的影响完全不是同一个概念。下文的 KV Cache 量化,会改写这张「草稿纸」的精度——你写「下午 3:17」还是「下午 3 点多」,模型之后再去读这张纸,能回忆起的细节程度会不一样。

一句话区分:权重量化改的是模型记住什么,KV Cache 量化改的是模型回忆多清楚。两者不互相替代,主流框架(vLLM / llama.cpp)都允许独立配置。

为什么 KV Cache 越来越重要

短对话的时候,KV Cache 其实不大——聊几千 token 你甚至感觉不到它的存在。但当上下文长度来到 32K、64K、128K 甚至 256K,情况就完全不一样了。KV Cache 的大小基本会随着上下文长度线性增长:上下文翻倍,KV Cache 基本也跟着翻倍。所以长上下文时代一个非常现实的问题出现了——模型明明已经塞进显存了,最后却被 KV Cache 挤爆了。

这时候 KV Cache 量化就非常有价值。我们先看一下没有量化时的显存压力:假设某个模型在某个上下文长度下 FP16 KV Cache 是 8 GB,这部分数据平时一直是「热」的——每生成一个 token 都要重新访问一次历史 KV Cache。所以显存压力是双重的:一是大,二是热(带宽压力大)。

FP16:基线,稳但贵

FP16 把每个 KV 数值都用 16 比特存储。它最大的优点就是精度高、简单、兼容性好,基本不用担心 KV 量化造成明显的信息损失。所以如果你上下文不长、显存非常充足、或者做的是严肃代码 Agent 复杂推理,FP16 永远是最稳妥的选择。

但问题也非常明显:它太占显存。粗略估算,FP16 KV Cache 相比 Q8 大约是 2 倍左右,相比 Q4 大约是 3-4 倍。在 8GB 显存只能撑 32K 上下文的现实面前,FP16 是很多消费级显卡根本玩不起长上下文的根本原因。

FP8:服务器长上下文的首选

很多人看到 FP8 会问:FP16 变 FP8,精度少了一半,模型是不是明显变笨?其实通常没这么严重。关键在于 FP8 依然是浮点数——它虽然只有 8 比特,但还保留了指数位。

你可以简单理解成:FP16 是一把非常精细、量程又很大的尺子;FP8 刻度粗了一些,但是依然可以通过指数覆盖非常大或非常小的数值。这对 KV Cache 很重要——Attention 里的 K 和 V 数值并不总是规规矩矩分布在一个很小的范围里。FP8 的浮点结构可以优雅地处理这种长尾分布

在模型和量化 kernel 配合良好的情况下,FP8 KV Cache 往往可以做到接近 FP16 的效果,却只需要大约一半的数据存储。这也是为什么 vLLM 现在专门支持 FP8 KV Cache——对于长上下文多变并发服务器来说,FP8 已经是非常有吸引力的方案。

Q8:llama.cpp 本地部署的甜点位

这里最容易混淆——FP8 和 Q8 都写着 8,但它们其实完全不是一个东西。FP8 是 8 比特浮点数,而 llama.cpp 里的 Q8 可以简单理解为「把一组浮点数压成 8 比特整数,再额外保存一个缩放比例」。它本质属于整数 + scale 的量化方案。

q8_intuition.py
# Q8 的工作原理:把小数映射到整数,存的时候只存整数 + scale
raw_values = [0.13, 0.57, 1.26, -0.82]
scale = 0.01                          # 全局缩放因子
quantized = [round(v / scale) for v in raw_values]   # → [13, 57, 126, -82]
# 反量化 = quantized * scale ≈ 原始值(误差 ≤ scale/2)

对于 KV Cache 来说,Q8 的精度已经相当高——很多实际测试中 Q8 KV Cache 和 FP16 的结果非常接近。所以如果你跑 llama.cpp,Q8 往往就是一个非常舒服的甜点位:显存差不多砍掉接近一半,但是通常很难在普通聊天里感受到能力下降。

Q4:能力曲线开始下降的拐点

真正有意思的是 Q4。因为来到 4 比特,事情开始发生变化。还是刚才那张草稿纸的比喻:FP16 相当于记录「会议时间是下午 3:17」,Q8 可能记录成「下午 3:17 左右」,但是 Q4 有时候就可能变成「下午 3 点多」。

单条信息看起来问题不大,可是大模型的 Attention 需要同时从几万甚至几十万个头坑(head × token)中判断到底应该关注哪一段信息。这时候大量小误差可能开始积累。Q4 最大的问题并不一定表现为模型突然不会做数学了,而更可能表现为长上下文召回下降

比如你给它一份十几万 token 的代码,前面定义了一个变量,到了后面它可能更容易忽略它;或者给它一份 20 万 token 的合同,前面某个不起眼的条件,到了最后问起,Q4 KV Cache 更容易漏掉。

Q4 的能力下降:更准确的说法是长期记忆变模糊,而不是「模型变笨」。短对话和简单任务依然正常,真正的风险在长上下文、复杂推理、Agent、代码任务这些需要反复回看历史信息的场景。

K 和 V 不一样:一个被忽略的细节

一个非常重要的细节:K 和 V 对量化的敏感程度并不一样。KV Cache 虽然名字叫 KV,但其实里面有 K cache 和 V cache 两部分。

可以把它想成一个图书馆——K 更像是目录和索引:模型要判断「我要找的信息到底在哪」;V(value)才是真正读取的内容。如果 K 被压得太狠,问题可能不是内容模糊了一点,而是模型连「应该看哪里」都判断错了。这也是为什么一些 llama.cpp 的实际实验中会看到一个有意思的现象:把 V 压到 Q4 影响可能并不算特别大,但是 K 直接压到 Q4,某些模型的输出分布会出现明显变化。

所以 KV Cache 量化不能简单理解成「bit 越低越划算」,尤其是 K 需要更加谨慎。vLLM 在早期版本里曾经支持过 K 和 V 分开用不同精度量化(如 K8V4),这正是为了应对这个非对称敏感度问题。

能力评分与显存实测

如果一定让我给一个非常直观的结论,可以这么记(基于 Qwen2.5 / Qwen3 等主流模型的实测均值):

kvcache_scorecard.ts
// KV Cache 精度评分卡(满分 5 星)
const kvCacheScoring = {
  'FP16': { capability: 5, vram: 5, speed: 5, note: '最稳最费' },
  'FP8':  { capability: 5, vram: 3, speed: 5, note: '服务器长上下文首选' },
  'Q8':   { capability: 5, vram: 3, speed: 4, note: 'llama.cpp 甜点位' },
  'Q4':   { capability: 3.5, vram: 1.5, speed: 3, note: '短对话无感,长上下文召回下降' },
}

// 显存对比(粗略):假设模型权重已经占满,额外加 8GB FP16 KV Cache
const vramEstimate = {
  'FP16': '100% = 8 GB',
  'FP8':  '~50%',
  'Q8':   '~52%(含 scale)',
  'Q4':   '~27%',
}

这里千万不要理解成「模型能力只剩四分之一」——完全不是。短对话可能依然非常正常,真正需要担心的是长上下文召回、复杂推理、Agent 和代码任务。上下文越长,这种风险越值得重视。

速度会变成 4 倍吗?99% 的情况不会

这是这一期最重要的误区之一。KV Cache 从 FP16 降到 Q4,理论数据量降了接近 4 倍,但是 token 速度不可能自动提升 4 倍。为什么?因为模型生成一个 token,除了读 KV Cache,还要:

inference_cost.py
# 生成 1 个 token 的完整开销
total_cost = (
    read_kv_cache +              # 读 KV Cache
    read_model_weights +         # 读模型权重
    matmul_attention_compute +   # 矩阵计算 + Attention
    sampling +                   # 采样
    kv_cache_dequant_if_quantized  # Q4/FP8 的反量化
)
# KV Cache 只是其中一项。低精度还会带来额外的反量化开销。

尤其短上下文的时候,KV Cache 本身还不大,这时候 FP16、Q8、Q4 的 token 速度可能差别并不明显——甚至某些硬件和实现上,Q4 因为多了一次解码和反量化,还可能稍微慢一点

KV Cache 量化真正发挥速度优势的场景只有两个:

第一,超长上下文(64K / 128K / 256K)。因为每生成一个新 token,Attention 都需要处理越来越大的历史 KV Cache,这时候显存带宽的重要性不断增加。KV Cache 越小,需要从显存搬运的数据就越少,因此低精度 KV 才更容易获得速度优势。

第二,多并发。一个人聊天只有一份 KV Cache,十个人同时聊天就是十份;Agent 同时开十几个 session 也是同样的道理。这时候 FP8 / Q8 最大的价值甚至不是单轮速度提高多少,而是同一张显卡能同时塞进去更多并发请求——整个系统吞吐量会大幅提高。

按场景选型清单

所以什么情况下应该用什么?

kv_cache_playbook.md
## KV Cache 选型决策表

### 1. 显存充裕,上下文 ≤ 16K,要追求最高稳定性
→ FP16
说明:不要为了省 GB 把稳定性做掉。

### 2. 跑 vLLM 服务端、硬件支持 FP8(如 H100 / 4090),需要长上下文 + 多并发
→ FP8
说明:H100 有专门的 FP8 Tensor Core,性能/精度综合最优。

### 3. 用 llama.cpp,本地消费级显卡 / CPU 混合部署
→ Q8
说明:在显存和质量之间非常平衡,是本地部署的默认推荐。

### 4. 显存吃紧(24GB 想跑 128K 上下文),需要尽可能塞长上下文
→ Q4(空间优先模式)
说明:会牺牲一些长上下文召回,需要业务上能容忍。

### 5. 如果 FP16 / Q8 已经塞得下
→ 不要为了省 GB 把 KV Cache 压成 Q4
说明:能力损失得不偿失。

最终结论:KV Cache 量化真正解决的问题,从来都不是简单的「怎么让模型跑得更快」,而是如何用有限的显存,让模型记住更多东西,同时尽量不要忘东西

四个铁律

最后只记四句话:

第一,KV Cache 量化不等于模型权重量化——它主要影响的是模型回忆历史上下文的精度,不直接影响模型本身的「智商」。
第二,FP8 和 Q8 是目前非常漂亮的平衡点——显存大约砍半,能力通常仍然非常接近 FP16。
第三,Q4 不是让模型智商减半,它真正容易伤到的是长上下文的注意力召回和稳定性。
第四,KV Cache 体积小 4 倍不代表模型快 4 倍——短上下文速度甚至可能基本不变,真正到了 64K / 128K / 256K 或者大量并发场景,低精度 KV Cache 的速度优势才会越来越明显。