副标题: 量化、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 量化几乎不影响输出质量------它存的不是"模型学到的知识",而是"当前对话的中间状态"。精度低一点,推理结果几乎不变。
所以这是一道送分题:
- 如果你想省显存:
--cache-type-k q8_0 --cache-type-v q8_0 --flash-attn 1--- 省 47% - 如果你想完全不损失性能:
--cache-type-k q8_0--- 省 23%,性能不变 - 如果你想一劳永逸: 选 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):
- 从零到一:用 AI Agent 辅助在 6GB 显卡上本地部署大模型实战 --- 部署全流程
- 只有 B 级能力的大模型,怎么干出 A 级的活? --- 任务拆解方法论
- Agent 不是更聪明的模型,而是长了手脚的模型 --- Agent 能力框架
- 从 Ollama 到 llama.cpp:一次"降一层"的本地推理探索 --- 推理引擎对比
- KV Cache 优化实战:6GB 显存上的每一 MB 都算数 --- 本文