KV Cache 优化实战:6GB 显存上的每一 MB 都算数

副标题: 量化、Flash Attention、模型架构------实测 6 种配置,告诉你 KV Cache 能省多少

日期: 2026年7月1日


一、引子:你每天都在用 KV Cache,但你未必知道

在上一篇博文中,我对比了 Ollama 和 llama.cpp 的性能。当时埋了一个伏笔------

复制代码
                    llama.cpp           Ollama
VRAM 占用           5,336 MiB           ~5,000 MiB

相差不大,对吧?但注意一件事:5,336 MiB 里,模型的参数占了 4,688 MiB(4.7GB),那剩下的 600 多 MiB 去哪了?

答案就是 KV Cache------LLM 推理过程中用来缓存"之前看过的内容"的中间数据。没有它,模型每生成一个 token 就要重新算一遍之前的注意力,速度慢几十倍。

而 KV Cache 最大的问题是:它随上下文线性增长。

复制代码
6GB 显存 = 6,144 MiB
模型参数 = 4,688 MiB(Q4_K_M)
计算缓存 = ~800 MiB
剩余给 KV Cache = ~656 MiB

当你把上下文从 4096 扩到 8192,KV Cache 翻倍。再扩到 16384,再翻倍。

很快,你的显存就不够用了。

本文的目标: 用同一台 GTX 1660 Ti(6GB),跑同一个 Qwen3-8B,实测 6 种 KV Cache 优化配置------量化、Flash Attention、模型架构------看看到底能省多少、速度会不会降。


二、KV Cache 到底是什么

一句话:生成第 N 个 token 时,不需要重新计算前 N-1 个 token 的 Key 和 Value 向量。

这个过程用文字说不直观,看个类比:

复制代码
传统 transformer(没有 KV Cache):

生成 token 1:算全部 → 输出"我"         ✅
生成 token 2:算全部 → 输出"是"         ❌ token 1 重算了一遍
生成 token 3:算全部 → 输出"谁"         ❌ token 1、2 重算了一遍

有 KV Cache:

生成 token 1:算 token 1 → 存(K,V)₁ → 输出"我"  ✅
生成 token 2:算 token 2 → 存(K,V)₂ → 输出"是"  ✅ (K,V)₁ 直接复用
生成 token 3:算 token 3 → 存(K,V)₃ → 输出"谁"  ✅ (K,V)₁₂ 直接复用

KV Cache 的大小有一个精确的计算公式:

复制代码
KV Cache = 2 × layers × kv_heads × d_head × ctx × dtype_size
                                        ↑
                                  上下文越长,越大

以 Qwen3-8B 为例:

参数
layers 36
kv_heads 8(GQA,32 个 Q 头分为 4 组)
d_head 128
dtype FP16(2 bytes)

代入公式:

复制代码
KV Cache = 2 × 36 × 8 × 128 × ctx × 2
         = 147,456 × ctx bytes
上下文 KV Cache 占 6GB 显存的比例
4,096 576 MiB 9.4%
8,192 1,152 MiB 18.8%
16,384 2,304 MiB 37.5%
32,768 4,608 MiB 75%

注意 32,768 这一行------KV Cache 本身就要吃掉 4.6GB,比模型参数(4.7GB)还大。 这就是为什么长上下文推理那么难。

这里有个细节:KV Cache 可以选择放在 GPU 还是 CPU。llama.cpp 默认全放 GPU(最快),但 6GB 显存下 ctx=8192 时已经快要溢出了。加 --no-kv-offload 可以把 KV Cache 放到 CPU 内存里,牺牲一点速度换更长上下文------Ollama 就是这么做的。


三、实验一:KV Cache 量化------近乎"免费"的显存

KV Cache 默认存储为 FP16(每个值 2 bytes)。但如果我用更少的精度来存呢?

最常见的方案是 INT8 量化(q8_0)------每个值只用 1 byte,直接省一半。

llama.cpp 提供 --cache-type-k--cache-type-v 两个参数,分别控制 K 和 V 的量化类型。我测试了 6 种组合:

数据

复制代码
Qwen3-8B · ctx=4096 · GTX 1660 Ti
═══════════════════════════════════════════

配置                    K 缓存       V 缓存      总 KV Cache   节省
──────────────────────  ──────────  ──────────  ────────────  ────
FP16(基线)             288 MiB     288 MiB     576 MiB        ---
K q8_0                  153 MiB     288 MiB     441 MiB       -23%
V q4_0                  288 MiB      81 MiB     369 MiB       -36%
V q8_0 + FA❶            288 MiB     153 MiB     441 MiB       -23%
K q8_0 + V q8_0 + FA❶   153 MiB     153 MiB     306 MiB       -47% ✅

❶ FA = Flash Attention(V 量化需要 FA 配合)

关键发现

① K 单独量化是最安全的标配

只加 --cache-type-k q8_0,KV Cache 从 576 MiB 降到 441 MiB(-23%),性能几乎不变:

复制代码
                    pp512            tg128
FP16:              148.72 tok/s     24.08 tok/s
K q8_0:            147.41 tok/s     24.58 tok/s
                     ↓ -0.9%         ↑ +2%

这是最简单的优化------一行参数,零成本,省 135 MiB 显存。

② V 量化有坑------需要 Flash Attention,且 Turing 卡有兼容问题

V 的量化比 K 更敏感。llama.cpp 要求 --cache-type-v q8_0 必须同时开启 --flash-attn 1。问题是:

GTX 1660 Ti 用的是 Turing 架构,没有 Tensor Core。 Flash Attention 在 Turing 上没有加速,反而会因为 kernel 路径不同导致 prompt eval 速度下降 46%

复制代码
V q8_0 + FA on:   pp512 = 80.51 tok/s  ← 比基线慢一倍
                    tg128 = 22.86 tok/s  ← -5%

③ 但 K+V 同时量化表现反而最好

当 K 和 V 都量化时,结果出乎意料:

复制代码
K q8_0 + V q8_0 + FA on:
  pp512 = 154.69 tok/s  ↑ +4%
  tg128 = 26.00 tok/s   ↑ +8%
  KV Cache = 306 MiB    ↓ -47%

又快又省。 为什么?我推测是因为当 K 和 V 都是 q8_0 时,注意力计算可以全在 INT8 路径上完成,不需要来回转换格式。而单独量化 V(K 是 FP16)反而导致额外的格式转换开销。

④ 质量验证:量化后模型输出会变差吗?

省显存不代表可以牺牲质量。我拿了同一个 prompt(计算 12345 × 67890写 fibonacci 函数),在 4 种配置下各跑了一次(temp=0,对比输出):

复制代码
                           math 测试                    code 测试 (fibonacci)
                           ───────────────────────      ───────────────────────
K q8_0                    ✅ 思维链措辞略不同,         ✅ 同上
                           不影响最终输出路径
K+V q8_0 + FA             ✅ 125 tokens 完全一致        ✅ 思路一致,仅索引基
                           与基线一字不差                础讨论有细微差异
V q4_0                    ❌ 输出为空!                  ❌ 输出为空!

V 量化到 INT4(q4_0)超出了安全边界 ------模型产生了空输出,说明 V 的精度损失大到注意力计算无法正常工作。V 的量化底线是 q8_0

K 和 K+V 量化的输出质量与基线基本一致。这是因为 KV Cache 存的是"当前对话的中间状态"而非"模型学到的知识"------少量精度损失不影响模型的理解能力。思维链中措辞的微小差异(如 five-digit vs five)属于正常的浮点误差累积,不影响最终结论。

给 GTX 1660 Ti 用户的建议: 不要单独用 --cache-type-v q8_0,直接上 --cache-type-k q8_0 --cache-type-v q8_0 --flash-attn 1,配置组合效果最好。


四、实验二:GQA 架构差异------选对模型等于赢在起跑线

KV Cache 公式里有两个模型架构参数:层数KV head 数。如果你还没下载模型,这两个指标几乎决定了你能跑多长的上下文。

把 Qwen3-8B 和 DeepSeek-R1-Distill-Qwen-7B 放在一起比:

复制代码
                     Qwen3-8B         DeepSeek-R1-Distill-Qwen-7B
                     ────────────     ──────────────────────────
层数                  36               28
Q heads              32               28
KV heads              8                4
GQA 分组              ×4              ×7
每 token KV Cache    36 × 2 × 8 × 128  28 × 2 × 4 × 128
                    = 73,728 bytes    = 28,672 bytes

在 ctx=8192 下实测:

复制代码
                    Qwen3-8B          DeepSeek-7B       差距
KV Cache (ctx=8192) 1,152 MiB         448 MiB           ×2.6 倍
pp512               148.72 tok/s      162.19 tok/s      +9%
tg128               24.08 tok/s       27.55 tok/s       +14%

DeepSeek 的 KV Cache 只有 Qwen3 的 39%------层数少了 22%,KV head 少了 50%,两者相乘就是 28/36 × 4/8 = 0.39。

这意味着什么?

复制代码
同样 6GB 显存,同样的 -ngl 全量 offload,同样的量化配置:

      Qwen3-8B               DeepSeek-7B
  ctx  │ 剩余显存           ctx  │ 剩余显存
 ──────┼──────────          ──────┼──────────
  4096 │  ~600 MiB           4096 │ ~1,300 MiB
  8192 │  OOM ❌             8192 │  ~850 MiB
                            16384 │  OOM ❌

DeepSeek 在 8192 ctx 下仍然能跑,Qwen3 已经爆了。这就是 GQA 分组数带来的差距。


五、实战配置对照表

基于以上所有实验,不同场景的最佳配置:

复制代码
┌──────────────────┬──────────────────────────────┬──────────────────────────┐
│ 场景             │ 推荐配置(llama.cpp 参数)    │ 能跑多长上下文           │
├──────────────────┼──────────────────────────────┼──────────────────────────┤
│ 日常聊天         │ 默认设置,什么都不加          │ 8K(用完 KV Cache 后    │
│ (128-512 tok)  │                              │ 再长也不影响聊天质量)   │
├──────────────────┼──────────────────────────────┼──────────────────────────┤
│ 文档分析/RAG     │ --cache-type-k q8_0          │ 16K(Qwen3)             │
│ (4K-16K tok)   │ --cache-type-v q8_0          │ 32K(DeepSeek)          │
│                  │ --flash-attn 1               │                          │
├──────────────────┼──────────────────────────────┼──────────────────────────┤
│ 代码仓库分析     │ --cache-type-k q8_0          │ 8K(Qwen3,6GB 极限)    │
│ (16K-32K tok)  │ --cache-type-v q8_0          │ 16K(DeepSeek)          │
│                  │ --flash-attn 1               │                          │
│                  │ --no-kv-offload              │                          │
├──────────────────┼──────────────────────────────┼──────────────────────────┤
│ 多轮对话         │ --cache-type-k q8_0          │ 取决于每轮长度           │
│ (多轮累积)      │ --cache-type-v q8_0          │ 量化后能多撑 2-3 轮      │
│                  │ --flash-attn 1               │                          │
├──────────────────┼──────────────────────────────┼──────────────────────────┤
│ 极致省显存       │ 选 DeepSeek 架构模型         │ 同样显存,多跑 2× 长度   │
│ (6GB 卡极限)   │ + 上述量化 + FA              │                          │
└──────────────────┴──────────────────────────────┴──────────────────────────┘

六、一张图看懂所有优化

复制代码
KV Cache 优化全景(Qwen3-8B @ ctx=4096 · GTX 1660 Ti)
═════════════════════════════════════════════════════════════════

基线 ─────────────────────────── 576 MiB, 148.72 / 24.08 tok/s
                                    │
                                    ▼
只量化 K      ────────────── 441 MiB (-23%), 147.41 / 24.58 tok/s
只量化 V      ────────────── 441 MiB (-23%),  80.51 / 22.86 tok/s ⚠️
K+V 都量化    ────────────── 306 MiB (-47%), 154.69 / 26.00 tok/s ✅
                                    │
                                    ▼
换 DeepSeek + K+V 量化 ────  ctx=8192 时仅 448 MiB
                                  Qwen3 在 ctx=8192 已 OOM

七、总结

KV Cache 优化给我的最大感受是:它不像模型量化那样有"降质风险"。

量化模型参数(Q4_K_M vs Q8_0)会改变推理质量,需要权衡。但 KV Cache 量化几乎不影响输出质量------它存的不是"模型学到的知识",而是"当前对话的中间状态"。精度低一点,推理结果几乎不变。

所以这是一道送分题:

  1. 如果你想省显存: --cache-type-k q8_0 --cache-type-v q8_0 --flash-attn 1 --- 省 47%
  2. 如果你想完全不损失性能: --cache-type-k q8_0 --- 省 23%,性能不变
  3. 如果你想一劳永逸: 选 GQA 分组大的模型(KV head 少的)------DeepSeek-7B 在 8192 ctx 下的 KV Cache 只有 Qwen3-8B 的 39%

你的 6GB 显存很珍贵------帮它减负,它回报你更长的上下文和更快的推理。


附:本文所有数据均在同一硬件上实测获得

硬件 参数
GPU NVIDIA GeForce GTX 1660 Ti 6GB
驱动 570.133.07 / CUDA 12.8
CPU 12 核 / 15GB RAM
llama.cpp commit d414db02, build 7152
测试工具 llama-cli, llama-bench

测试命令:

bash 复制代码
# KV Cache 大小查看
llama-cli -m qwen3-8b-q4_k_m.gguf -ngl 36 -c 4096 -p "hello" -n 1

# 量化后查看
llama-cli -m qwen3-8b-q4_k_m.gguf -ngl 36 -c 4096 \
  --cache-type-k q8_0 --cache-type-v q8_0 --flash-attn 1 -p "hello" -n 1

# 性能基准
llama-bench -m qwen3-8b-q4_k_m.gguf -ngl 36 -p 512 -n 128 -r 2 \
  --cache-type-k q8_0 --cache-type-v q8_0 --flash-attn 1

# 查看 KV Cache 信息(在 llama-cli 输出中找):
#   llama_kv_cache: size =  306.00 MiB (4096 cells, 36 layers)

系列全篇(CSDN):

  1. 从零到一:用 AI Agent 辅助在 6GB 显卡上本地部署大模型实战 --- 部署全流程
  2. 只有 B 级能力的大模型,怎么干出 A 级的活? --- 任务拆解方法论
  3. Agent 不是更聪明的模型,而是长了手脚的模型 --- Agent 能力框架
  4. 从 Ollama 到 llama.cpp:一次"降一层"的本地推理探索 --- 推理引擎对比
  5. KV Cache 优化实战:6GB 显存上的每一 MB 都算数 --- 本文