KV cache 量化的精度代价与 --kv-cache-dtype 取值

在 **** 的 ****(8 卡,单卡 64GB HBM,卡间互联 392 GB/s)上用 vLLM 起服务,把 KV cache 数据类型从默认值改为 --kv-cache-dtype fp8。启动日志里 KV 池可用块数明显上升,--max-model-len 能起得更高。短上下文问答(512 token 档)输出与改动前一致,切到长上下文(32K 档)后输出开始异常:出现重复片段、串行错乱或提前终止。同一组参数下短上下文正常、长上下文才坏,问题落在 KV 量化本身:KV cache 每个元素由 2 字节降到 1 字节,精度损失随上下文长度被放大,同时目标 attention 后端对 KV dtype 的 guard 可能已经失格回退。

--kv-cache-dtype 的取值

取值 含义 每元素字节数
auto 与模型 dtype 一致,不做 KV 量化 2(FP16 / BF16)
fp8 浮点 8 bit,细分格式由后端决定 1
fp8_e4m3 4 位阶码 + 3 位尾数,尾数长,利于精度 1
fp8_e5m2 5 位阶码 + 2 位尾数,动态范围更大 1
float16 / bfloat16 显式指定不量化 2

auto 是默认值,不引入额外误差。fp8 之下另有 fp8_e4m3 与 fp8_e5m2 两个细分:e4m3 尾数长、能表示的数值更精细,e5m2 阶码长、动态范围更大但精度更低。两者的取舍在数值分布上:范围不大、追求精度的场景用 e4m3,范围跨度大、怕溢出的场景用 e5m2。部分后端只接受其中一个。

占用:每个元素 2 字节降到 1 字节

KV cache 占用式(单层、单序列)为:

K 与 V 合计 = 2 × H_kv × D × S × b
b = 2(FP16 / BF16)或 1(FP8)

H_kv 是每层 KV 头数,D 是单头维度,S 是序列长度,b 是 KV 每元素字节数,由 KV dtype 决定。转 fp8 后 b 从 2 降到 1,占用近似减半,同一块显存在同一 max_model_len 下能容纳的序列数近似翻倍。收益来自两处:decode 阶段每步读取 KV 的字节数减半,长上下文下的容量压力与带宽压力同时下降。代价是 KV 被截断到 8 bit,与权重量化一样引入量化误差,只是作用对象是每步都要读写的 KV。

精度损失为何在长上下文下放大

注意力为每个 query 计算它与历史全部 key 的 score,再按 softmax 权重做加权求和。上下文越长,参与累加的项越多,单元素量化误差在累加路径上被重复带入的次数也越多。短上下文里被平均掉的误差,在长上下文里累积成可见偏差,表现为长文生成中的重复、指代错乱或提前终止。这解释了现场的主要形态:同一组 KV dtype,短上下文质量与 FP16 持平,长上下文才出现异常。推断长上下文的结论不能拿短上下文的结果替代。

KV 量化与权重量化的差别

权重在推理期间不变,量化误差是一次性的,可被校准吸收;KV 是运行时逐 token 写入再读出的张量,误差在每个 decode 步重新产生,没有事后校准的机会。因此 KV 量化的精度代价不能照搬权重量化的结论,两者要分开测、分开记。

KV 还有一个权重没有的特点:它的数值范围随上下文变化。长上下文里较早写入的 key 与较新写入的 key 分布可能不同,整块共用一个 scale 时,分布漂移会让部分位置的误差偏大。per-head scale 要缓解的正是这一类误差,作用面在误差的分布而非总量。

与 attention 后端的兼容约束

kv_cache_dtype 直接参与后端 guard,目标后端不支持的 KV dtype 一律失格。取错值不会报错,只会让框架回退到另一条 attention 实现。后端支持区间示例如下(字段名示意,取值以目标版本为准):

后端 支持的 KV Dtypes
FLASH_ATTN(FA2) auto, float16, bfloat16(不含 fp8)
FLASH_ATTN(FA3,Hopper 及同族设备) auto, float16, bfloat16, fp8, fp8_e4m3
TRITON_ATTN auto, float16, bfloat16, fp8, fp8_e4m3, fp8_e5m2, int8_per_token_head, fp8_per_token_head

两点结论:FA2 不接受 fp8,把 KV dtype 设为 fp8 会使其失格;FA3 接受 fp8_e4m3 但不接受 fp8_e5m2。TRITON_ATTN 多出的 int8_per_token_head、fp8_per_token_head 是带 per-head scale 的取值,属于下一节的处置选项。行内后端的版本分支(FA2 / FA3 / FA4)由 --attention-config 传入的 flash_attn_version 一类开关选择,默认值随硬件代际变化,核对时应读实际命中结果而不是假设取值。

触发条件

负载 / 配置形态 现象
长上下文(32K、128K 档) 精度损失被放大,输出异常最先在这一档出现
短上下文(512、2K 档) 与 FP16 基本持平,问题被掩盖
目标后端本就不接受该 KV dtype guard 失格,静默回退到另一条实现
只追求容量、未做精度对照 容量兑现,精度未测,偏差在长文任务中暴露
高并发长上下文 容量收益与精度代价同时放大

现象

失格在日志里以 KV dtype 相关的 guard 失败出现。失败模式名以目标版本为准,日志行示例如下:

# 示意:KV dtype 不被目标后端接受时的日志行(字段名示意,非原文;debug 级,默认不打印)
Some attention backends are not valid for <device> with AttentionSelectorConfig(...). Reasons: {FLASH_ATTN: [kv_cache_dtype not supported]}.

定位步骤

  1. 分级测质量。固定采样配置与评测任务,按上下文长度分 512 / 2K / 8K / 32K / 128K 档,分别比对 FP16 与 FP8 两组的输出质量,确认偏差从哪一档开始出现。评测项要选对长度敏感的:长上下文检索命中率、指令遵循、结构化输出,比开放式问答更容易暴露偏差。只看端到端均值会掩盖长上下文长度档的偏差。
  2. 确认 KV dtype 实际生效值。读启动日志里 KV cache 相关那一行,确认 b 与可用块数,避免命令行取值与后端选定值不一致。
  3. 确认后端命中。比对 target_backend 与 resolved_backend,确认 KV dtype 没有触发 guard 失格。
  4. 分开记录。容量收益按 KV 池可用 token 数确认,精度代价按分级质量确认,两项分别记录。
# 示意:显式指定 KV dtype 与后端,便于看清命中路径(取值按本机规格填)
vllm serve <model-path> \
  --tensor-parallel-size 8 \
  --max-model-len 32768 \
  --kv-cache-dtype fp8_e4m3 \
  --attention-backend FLASH_ATTN \
  --enforce-eager

处置

动作 作用 代价 / 前提
换 fp8_e4m3 尾数更长,精度优于 e5m2 后端须支持该取值
回到 float16 / bfloat16 精度回到基准 KV 占用回到 2 字节,容量收益消失
换支持 per-head scale 的后端 按 head 补偿量化误差 需重测命中路径与性能
改 --attention-backend 命中支持目标 KV dtype 的后端 显式指定不兼容会直接报错,不静默替换

per-head scale 的作用面是量化误差的分布。KV 各 head 的数值范围不同,整块共用一个 scale 时,范围小的 head 误差偏大;按 head(或 per-token-head)给一组 scale,能把误差压到各 head 自身的范围内。TRITON_ATTN 的 int8_per_token_head 属于这一类取值,代价是后端切换后要重新确认命中路径与性能。

记录字段

# 一条 KV 量化记录(字段名示意)
hardware_sku:              # 卡型(8 卡,单卡 64GB HBM,互联 392 GB/s)
model_name:
kv_cache_dtype:            # auto / fp8 / fp8_e4m3 / fp8_e5m2
attention_backend:         # 目标后端
resolved_backend:          # 实际命中后端
context_length_bins:       # 分级测试的上下文长度档
quality_by_bin:            # 每档相对 FP16 的质量差异
kv_cache_capacity:         # 可用块数 / 可容纳 token 数
throughput_before:         # 量化前吞吐
throughput_after:          # 量化后吞吐
status:                    # 容量兑现且精度达标 / 容量兑现精度不达标 / 后端失格回退

小结

--kv-cache-dtype 的 fp8 把 KV 每元素从 2 字节降到 1 字节,占用近似减半,收益落在长上下文的容量与带宽上。精度损失随上下文长度放大,短上下文正常不代表长上下文正常,定位时要按上下文长度分级测质量。KV dtype 直接参与 attention 后端的 guard,FA2 不接受 fp8、FA3 与 FA4 不接受 fp8_e5m2,取值不兼容即回退。精度不达标时可换 fp8_e4m3、换带 per-head scale 的后端,或退回 FP16。