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 池容量与
max_model_len看起来都兑现了,attention 路径却已回退到非预期实现。 - 开了 KV 量化,
resolved_backend与预期不一致,日志里只有一行后端选择结果。 - 端到端评测分数下降幅度很小,长上下文长度档的单点质量却已明显变坏,均值掩盖偏差。
失格在日志里以 KV dtype 相关的 guard 失败出现。失败模式名以目标版本为准,日志行示例如下:
# 示意:KV dtype 不被目标后端接受时的日志行(字段名示意,非原文;debug 级,默认不打印)
Some attention backends are not valid for <device> with AttentionSelectorConfig(...). Reasons: {FLASH_ATTN: [kv_cache_dtype not supported]}.
定位步骤
- 分级测质量。固定采样配置与评测任务,按上下文长度分 512 / 2K / 8K / 32K / 128K 档,分别比对 FP16 与 FP8 两组的输出质量,确认偏差从哪一档开始出现。评测项要选对长度敏感的:长上下文检索命中率、指令遵循、结构化输出,比开放式问答更容易暴露偏差。只看端到端均值会掩盖长上下文长度档的偏差。
- 确认 KV dtype 实际生效值。读启动日志里 KV cache 相关那一行,确认
b与可用块数,避免命令行取值与后端选定值不一致。 - 确认后端命中。比对
target_backend与resolved_backend,确认 KV dtype 没有触发 guard 失格。 - 分开记录。容量收益按 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。