Workshop

实战工坊:用 vLLM 压扁 KV Cache——FP8、TurboQuant 与华为 KVarN

6 min read ·

💡 一句话总结:长上下文推理真正的内存墙不是权重,而是随上下文线性膨胀的 KV Cache。vLLM 里一行参数就能开 FP8 量化拿到约 2 倍容量、接近无损;但要记住它换的是显存不是吞吐,且任何更激进的方案都必须用 NIAH 验证长上下文不掉点。

一、被忽视的内存墙

聊量化,大多数人第一反应是 AWQ、GPTQ——把模型权重压到 4-bit。但在真实的长上下文服务里,权重往往不是最吃显存的那块。

原因在 KV Cache:自回归解码时,每生成一个 token,注意力机制就要把这一步的 Key、Value 缓存下来,供后续步骤复用。这个缓存的大小,正比于「层数 × 注意力头数 × 头维度 × 上下文长度 × batch」。权重是固定的一次性开销,KV Cache 却随上下文长度线性增长

举个直观的数:一个中等规模模型,跑到 128K 上下文、几十路并发,KV Cache 能轻松吃掉几十 GB——经常比权重本身还大。这就是为什么长上下文服务一上量就 OOM,而你盯着权重量化怎么调都不解决问题。短上下文看权重,长上下文看 KV Cache。

好消息是:KV Cache 和权重压的是两个不同的内存池,互补而非互斥。你可以先 AWQ 压权重,再在其上叠 FP8 压 KV Cache,收益相加。

二、量化谱系:从 FP8 到 4-bit 以下

KV Cache 量化这几年方案不少,按压缩力度大致排成一条谱系:

记住一个判断原则:压缩比越高,越要警惕长上下文检索和多步推理这两个失效区。困惑度这种平均指标看不出问题,针对性测试才能。

三、实战:在 vLLM 里开 FP8 KV Cache

vLLM 原生支持 FP8 KV Cache,开启只要一个参数。最简单的离线推理:

from vllm import LLM, SamplingParams

llm = LLM(
    model="meta-llama/Llama-3.1-8B-Instruct",
    kv_cache_dtype="fp8",        # 开启 FP8 KV Cache(默认 E4M3)
    max_model_len=131072,         # 长上下文才是 KV 量化的主场
)

out = llm.generate(
    ["把这份长文档总结成 5 条要点:..."],
    SamplingParams(temperature=0.2, max_tokens=512),
)
print(out[0].outputs[0].text)

起服务则是:

vllm serve meta-llama/Llama-3.1-8B-Instruct \
  --kv-cache-dtype fp8 \
  --max-model-len 131072

注意一个限制:vLLM 目前只支持 FP8(E4M3/E5M2)KV Cache,不支持 INT8 KV Cache。如果你看到某些框架对比里 INT8 KV 的数据,那是 TensorRT-LLM 等其他引擎的能力,别照搬到 vLLM 配置上。

四、scale 怎么定:三种校准方式

FP8 量化要把浮点值映射到 8-bit,需要一个缩放因子 scale。vLLM 给了三档,从省事到精确:

  1. 未校准(scale = 1.0):什么都不做,per-tensor 固定 scale。这是最坏情况下限,胜在零成本、可复现。vLLM 官方的评测就常用这档来给「下限」托底。
  2. 随机 token 在线校准:warmup 阶段用一批随机 token 自动估出 scale 然后固定。配置 calculate_kv_scales=True
llm = LLM(
    model="meta-llama/Llama-3.1-8B-Instruct",
    kv_cache_dtype="fp8",
    calculate_kv_scales=True,     # 用随机 token 在线估 scale
)
  1. 校准数据集离线生成 scale:用一批贴近真实分布的语料离线跑出每层/每头的 scale,写进权重附带的量化配置。最准,但要多一步离线流程。

经验法则:先用 scale=1.0 测出下限,再用随机 token 校准看能不能拉回来;只有当业务对精度极敏感、且简单校准不够时,才上数据集离线校准。 别一上来就堆最重的方案。

五、验证:NIAH 才是试金石

量化后最危险的不是平均质量掉一点,而是长上下文深处的信息悄悄丢了。困惑度、几道常规 benchmark 都可能看不出来。

正确的验证是 Needle-In-A-Haystack(NIAH)

  1. 准备一段很长的背景文本(覆盖你的目标上下文长度,比如 8K、32K、128K)。
  2. 在文本里插入一条具体、随机、与背景无关的「针」,例如「2026 年巴黎的隐藏密码是 7391」。
  3. 把针放到不同深度(开头、10%、50%、90%、结尾),分别让模型回答「密码是多少」。
  4. 对每个(上下文长度 × 深度)组合记录是否命中,画成热力图。

伪代码:

def niah_probe(llm, depth_ratio, ctx_len):
    needle = "2026 年巴黎的隐藏密码是 7391。"
    bg = make_background(ctx_len)          # 拼到目标长度
    pos = int(len(bg) * depth_ratio)
    prompt = bg[:pos] + needle + bg[pos:] + "\n请问隐藏密码是多少?"
    ans = llm.generate([prompt]).outputs[0].text
    return "7391" in ans                    # 命中判定

# 跑满矩阵:FP8 与 BF16 各跑一遍,对比热力图

把 FP8 的热力图和 BF16 基线叠一起看。FP8 通常整张图全绿、和基线几乎一致;一旦你换上 4-bit 以下的激进方案,深处的格子开始变红——这正是 TurboQuant 这类方案需要保留首尾层 FP16 的原因。

六、一个反直觉的事实:你换的是显存,不是吞吐

很多人开 FP8 KV Cache 是冲着「更快」去的,结果跑完 benchmark 一脸困惑:吞吐没涨,prefill-heavy 场景甚至略降。

这不是 bug。在 vLLM 上,FP8 KV Cache 的主要收益是省显存,不是直接加速:解码时算的仍是反量化后的注意力,量化/反量化本身还要花时间。它真正的价值是间接的——省下的显存让你能开更大的 batch、更长的上下文、更高的并发,整体吞吐和服务容量随之上去。

如果你要的是「KV 量化直接换吞吐」,那是 TensorRT-LLM 的强项——它的 FP8/INT8 KV Cache 在吞吐上有更明显的正收益。所以选型前先问自己一句:我现在缺的是显存,还是算力? 答案不同,结论完全不同。

七、KVarN:当 FP8 不够省时

如果 FP8 的 2 倍还填不满你的长上下文需求,下一步往往是 4-bit 以下,而这正是华为 KVarN 想解决的场景。它的思路是方差归一化:低位量化误差会随 token 累积,KVarN 在量化前对键值按方差做归一化,让分布更适合低位表示,从而压住误差累积——尤其是在多步推理任务里,激进压缩最容易崩的地方。

概念性伪代码(表达思路,非可运行实现):

def kvarn_quantize(kv, bits=4):
    # 1. 按通道估方差,做归一化,让分布更"齐整"
    var = kv.var(dim=-1, keepdim=True)
    kv_norm = kv / (var.sqrt() + 1e-6)
    # 2. 在归一化后的空间做低位量化
    q, scale = low_bit_quant(kv_norm, bits=bits)
    # 3. 反量化时把方差缩放还原回去
    return q, scale, var

要不要上?给个决策清单:

八、小结

KV Cache 才是长上下文推理的内存墙。vLLM 上一行 kv_cache_dtype=fp8 就能拿到约 2 倍容量、接近无损的确定收益,记住它换的是显存而非吞吐。任何更激进的方案——TurboQuant、KVarN——都别只看压缩比,先用 NIAH 和你的真实推理任务把长上下文这关守住。先拿稳的,再追狠的。

Frequently asked questions

KV Cache 量化和权重量化(AWQ/GPTQ)是一回事吗?
不是,它们压的是两个不同的内存池,且互补而非互斥。权重量化(AWQ、GPTQ)压的是模型参数,在加载时一次性生效,和上下文长度无关。KV Cache 量化压的是推理时每生成一个 token 就累积的注意力键值缓存,它随上下文长度线性增长。你完全可以先用 AWQ 把权重压到 4-bit,再在其上叠加 FP8 KV Cache 量化,两者的显存收益相加。短上下文时权重是大头,长上下文时 KV Cache 才是真正的内存墙。
vLLM 里开 FP8 KV Cache 为什么吞吐没涨甚至略降?
因为 FP8 KV Cache 在 vLLM 上的主要收益是省显存,不是直接加速。GPU 在解码阶段算的还是反量化后的注意力,量化/反量化本身有开销,prefill-heavy 场景下甚至可能略微拖慢。它的价值在于:腾出的显存让你能跑更大的 batch、更长的上下文,从而提升整体吞吐和并发。如果你要的是 KV 量化直接换吞吐,TensorRT-LLM 的 FP8/INT8 KV Cache 在这点上表现更明显。所以先想清楚你缺的是显存还是算力。
E4M3 和 E5M2 这两种 FP8 格式该选哪个?
KV Cache 量化默认且推荐 E4M3。FP8 有两种排布:E4M3 是 4 位指数 3 位尾数,动态范围小但精度高;E5M2 是 5 位指数 2 位尾数,范围大但精度低。KV Cache 里的键值分布相对集中,更看重精度而非极端动态范围,所以 E4M3 几乎总是更好的选择。E5M2 通常留给对动态范围敏感的梯度等场景。vLLM 中 kv_cache_dtype 设为 fp8 时默认走 E4M3(fp8_e4m3)。
怎么确认量化后长上下文没有悄悄掉点?
用 Needle-In-A-Haystack(NIAH)这类长上下文检索测试,而不是只看困惑度。做法是在一段很长的背景文本里,把一条具体、随机的事实(针)插到不同深度,然后让模型在全文里把它捞出来。逐个深度、逐个上下文长度地跑,画成热力图。KV Cache 激进压缩最典型的失效模式正是长上下文检索——困惑度看着没事,但深处的针找不回来了。FP8 通常能扛住,4-bit 以下方案必须靠 NIAH 才能暴露问题。
TurboQuant、KVarN 这类 4-bit 以下方案现在该上生产吗?
谨慎。FP8 是当下的稳妥默认:约 2 倍 KV 容量、接近无损、生态成熟。TurboQuant(Google)把压缩推到 5-6 倍,但激进压缩在长上下文检索上容易失效,需要逐场景验证。KVarN(华为)用方差归一化缓解了量化误差累积,论文显示在推理任务上比同类更稳,但落地依赖框架支持,很多还停在论文/参考实现阶段。建议:生产先上 FP8 拿确定收益,把 4-bit 方案放在显存极度紧张的离线/长上下文批处理场景里灰度验证。
// next.txt ›

Some outbound links in this post are affiliate links — see disclosure.