vLLM FP8 量化 KV Cache 完整指南:格式选择、Scale 校准与 llm-compressor 实操
【免费下载链接】vllmA high-throughput and memory-efficient inference and serving engine for LLMs项目地址: https://gitcode.com/GitHub_Trending/vl/vllm
KV Cache 是长上下文推理中 GPU 显存的主要消耗来源之一。把 Key/Value 缓存量化为 FP8,可以将缓存显存占用减半左右,让同一块 GPU 装下更多 token,从而直接提升吞吐并支撑更长的上下文窗口。本文以 vLLM 官方量化文档 quantized_kvcache.md 为主体,结合 CacheConfig、EngineArgs 参数定义、Attention 层实现 与 FlashAttention 后端 等源码,系统讲解 vLLM FP8 KV Cache 的量化粒度、存储格式、三种 scale 获取方式、按层跳过量化等机制,并给出可直接运行的 Python 与命令行示例。
FP8 量化 KV Cache 的价值与适用场景
LLM 推理过程中,KV Cache 的大小随并发序列数、上下文长度线性增长,往往是显存中最大的动态占用项。将 K/V 张量以 FP8(8 位浮点,相对 BF16/FP16 减半)存储,可以在几乎不改变计算图的前提下显著缩小缓存体积。收益主要体现在两方面:
- 同卡可容纳更多 token:KV Cache 更小 → 可分配的 cache block 更多 → 单批可承载的有效 token 数更大;
- 支持更长上下文窗口:在
gpu_memory_utilization约束下,更紧凑的缓存让长序列请求更容易获得完整可用的 KV 空间。
量化是有代价的:精度下降可能带来端到端准确率损失,尤其是当 scale(缩放系数)不合适时。因此 vLLM 在 CacheConfig 中会以日志明确提示用户:使用量化 KV Cache dtype 会降低显存占用并提升性能,但 "may cause accuracy drop without a proper scaling factor"(缺少合适缩放因子时可能产生精度损失)。
注意(FlashAttention 3 后端):当使用 FA3 后端 + FP8 KV Cache 时,注意力计算本身也在量化(FP8)域中进行,此时 Query 也会和 Key/Value 一样被量化为 FP8。这是文档中特别提示的一个后端行为差异。
kv_cache_dtype:FP8 缓存存储格式怎么选
KV Cache 的存储类型由kv_cache_dtype控制。核心取值定义在 CacheConfig.CacheDType,与 FP8 相关的选项如下:
| 取值 | 含义 | 平台支持 |
|---|---|---|
auto | 使用模型默认数据类型(等价于不做显式量化) | 通用 |
fp8 | 短别名,等价于fp8_e4m3 | 取决于平台支持 |
fp8_e4m3 | FP8 E4M3 格式(精度更高,推荐首选) | CUDA 11.8+、ROCm(AMD GPU) |
fp8_e5m2 | FP8 E5M2 格式(动态范围更大、精度较低) | CUDA 11.8+ |
从 torch_utils.py 可以看到别名映射关系:"fp8" -> "fp8_e4m3",因此直接写fp8与写fp8_e4m3等价。在 vLLM 内部,所有 fp8 前缀的缓存 dtype 都会被 is_quantized_kv_cache 识别为量化缓存;在部分注意力量化实现里fp8*dtype 对应的底层张量类型为torch.uint8(见 STR_DTYPE_TO_TORCH_DTYPE),实际以 FP8 语义解释位模式。
需要说明的是,E4M3 与 E5M2 的可用性取决于后端实现与硬件算子。除上述选项外,当前仓库的 CacheDType 还枚举了fp8_inc(Intel Gaudi)、fp8_ds_mla(DeepSeek MLA 相关)以及fp8_per_token_head、int4_per_token_head等每 token-头动态 scale 类型,以及 NVFP4 系列(nvfp4、nvfp4_4over6),可视为同一配置体系下的扩展面,本文聚焦 FP8 主路径。
CLI 与编程接口两种配置方式
CLI 服务端使用缓存参数组中定义的--kv-cache-dtype(见 arg_utils.py 参数组注册):
vllm serve <model> --kv-cache-dtype fp8离线/编程接口在构造LLM(或AsyncLLM)时传入同名关键字,二者底层最终汇入同一个CacheConfig:
from vllm import LLM llm = LLM( model="meta-llama/Llama-3.1-8B-Instruct", kv_cache_dtype="fp8", # 等价于 fp8_e4m3 )当kv_cache_dtype="auto"时,引擎启动阶段会通过resolve_kv_cache_dtype_string(arg_utils.py)结合模型配置把 "auto" 解析为实际 dtype 后写入CacheConfig.cache_dtype。同时,若模型 checkpoint 中自带 FP8 KV Cache 量化声明(见下文 llm-compressor 校准),且用户没有显式指定 dtype,attention.py 会自动把缓存 dtype 解析为fp8,保证"校准过就生效"。
两种 FP8 量化粒度:Per-Tensor 与 Per-Attention-Head
对同一个 FP8 缓存,vLLM 支持两种量化/反量化 scale 粒度:
| 方案 | scale 形状 | 说明 |
|---|---|---|
| Per-tensor 量化 | q/k/v_scale = [1] | Q、K、V 各自整体共享一个标量 scale,开销最小,精度相对粗 |
| Per-attention-head 量化 | q_scale = [num_heads],k/v_scale = [num_kv_heads] | 每个注意力头独立 scale,精度更高,需要保存/计算的头级 scale 张量更多 |
其中 Per-attention-head 方案:
- 当前仅 FlashAttention 后端支持;
- 需要由llm-compressor提供的校准路径预先产出各头 scale(无法在 vLLM 运行时凭空推断)。
从源码看,Attention 层在初始化时会读取量化配置中的kv_cache_scheme,当strategy == "attn_head"时置use_per_head_quant_scales = True(attention.py),后续将头级 scale 张量交由注意力后端使用;在 FlashAttention 后端 中可以见到对layer._q_scale、layer._k_scale等做 shape 扩展(expand(descale_shape))以匹配每头解量化的实现痕迹,这正是每头 scale 落到 kernel 层的通路。
Scale 校准的两种配置途径(以及第三种隐式途径)
"选哪个 dtype"只是第一步,更关键的问题是量化 scale 从哪里来。scale 若全为 1.0,本质是截断式量化,误差最大;scale 越贴合真实激活分布,精度越好。vLLM 提供以下方式:
方式一:不做校准(默认 scale = 1.0)
仅通过kv_cache_dtype="fp8"开启量化,所有 scale 一律取1.0:
llm = LLM(model="meta-llama/Llama-2-7b-chat-hf", kv_cache_dtype="fp8")这种方式部署最简单、零额外依赖,适合快速验证显存/吞吐收益;但由于缺少与真实激活匹配的 scale,精度损失通常最明显。引擎也会在 CacheConfig 校验 时打印对应提示。
方式二:用校准数据集估算 scale(推荐)
对追求质量的生产场景,vLLM 官方推荐使用llm-compressor(vLLM 生态下的模型压缩库)在一次性(one-shot)校准过程中,用小规模有代表性的数据集统计出 Q/K/V 激活分布,从而估计出最优 scale。它同时是开启 per-attention-head 高级量化的前提。具体操作见本文后面的"端到端实操示例"。
第三种"隐式"途径:直接消费 checkpoint 里固化好的 scale
llm-compressor 校准完成后,会把量化方案(含kv_cache_scheme与 scale 系数)随模型一起保存(save_pretrained(..., save_compressed=True))。之后在 vLLM 中加载该模型时:
- 如果用户没有显式传
kv_cache_dtype(即auto),attention.py 会读取quant_config.kv_cache_scheme并自动切换到fp8缓存; - 若 scheme 的 strategy 为
attn_head,则启用 per-head scale 通路(同一文件 L288-L292)。
这意味着校准工作只需要做一次,产出的模型即可在后续部署中自动复用 scale,无需每次启动重复校准。
对量化敏感层单独豁免:--kv-cache-dtype-skip-layers
并非所有注意力层都适合量化。例如sliding-window 注意力层(常被用于长序列局部建模)对 KV Cache 量化更敏感,量化后质量下降更明显。kv_cache_dtype_skip_layers允许你把指定层留在模型原生 dtype,而其余层继续使用所选量化 dtype,实现"量化与精度"的折中。
该参数在 CacheConfig 中定义为字符串列表,接受两类输入:
- 注意力类型名:如
sliding_window,表示匹配所有该类层; - 层序号:如
0、1、23,表示只豁免指定索引的层。
命令行用法
# 跳过所有 sliding-window 注意力层 vllm serve <model> \ --kv-cache-dtype fp8 \ --kv-cache-dtype-skip-layers sliding_window # 跳过第 0、1、23 层(按层索引) vllm serve <model> \ --kv-cache-dtype fp8 \ --kv-cache-dtype-skip-layers 0 1 23编程接口用法
from vllm import LLM llm = LLM( model="meta-llama/Llama-3.1-8B-Instruct", kv_cache_dtype="fp8", kv_cache_dtype_skip_layers=["sliding_window"], )底层实现机制
这个功能的判定逻辑位于 Attention 层初始化,其执行流程为:
- 若该层存在 sliding-window(
sliding_window is not None)且 skip 列表包含"sliding_window"→ 命中; - 否则用模块前缀名(如
model.layers.0.self_attn)经 extract_layer_index 提取层索引,若该索引以字符串形式出现在 skip 列表 → 命中; - 命中后该层的
kv_cache_dtype被改写为"auto"(即回退到模型原生 dtype),其他层不受影响。
从 CacheConfig 的注释还能看到配套机制:跳过层仍要参与 block-size 对齐,必要时通过skip_page_size_padded把未量化层补齐到量化主层 page 大小,保证混合缓存布局在页级可对齐。测试用例(如tests/kernels/attention/test_attention.py对kv_cache_dtype相关矩阵的覆盖)会验证 skip 列表与不同 dtype 组合的行为一致性。
实操示例一:无校准直接开启 FP8
最简上手路径,将示例模型改为你环境中实际可下载的模型即可(仓库内示例使用 Llama-2-7B-chat):
from vllm import LLM, SamplingParams sampling_params = SamplingParams(temperature=0.7, top_p=0.8) llm = LLM( model="meta-llama/Llama-2-7b-chat-hf", kv_cache_dtype="fp8", ) prompt = "London is the capital of" out = llm.generate(prompt, sampling_params)[0].outputs[0].text print(out)运行后对比同模型auto与fp8两档:fp8档的 KV Cache 占用显著下降,可观测的峰值显存随之降低;同时由于所有 scale 均为 1.0,输出质量可能略降——这正是需要方式二(校准)的理由。
实操示例二:llm-compressor 数据集校准(推荐)
高质量量化路径。以下脚本用HuggingFaceH4/ultrachat_200k的少量样本对 Llama 3.1 8B 做一次性校准,将注意力激活(Q)与 KV Cache同时量化为 FP8,并支持在tensor与attn_head两种 strategy 间切换。
安装依赖
pip install llmcompressorllm-compressor 还会依赖datasets、transformers等(示例脚本 import 中使用),如缺失请一并安装。
校准脚本
""" Quantize Llama attention + KV cache to FP8 (choose either 'tensor' or 'attn_head' strategy) using llm-compressor one-shot calibration. """ from datasets import load_dataset from transformers import AutoModelForCausalLM, AutoTokenizer from llmcompressor import oneshot from llmcompressor.modifiers.quantization import QuantizationModifier from compressed_tensors.quantization import QuantizationScheme, QuantizationArgs # ----------------------------- # Config # ----------------------------- MODEL_ID = "meta-llama/Llama-3.1-8B-Instruct" DATASET_ID = "HuggingFaceH4/ultrachat_200k" DATASET_SPLIT = "train_sft" STRATEGY = "tensor" # or "attn_head" NUM_CALIB_SAMPLES = 512 # Good starting value MAX_SEQ_LEN = 2048 # ----------------------------- # Helpers # ----------------------------- def process_and_tokenize(example, tokenizer: AutoTokenizer): """Convert chat messages to tokens.""" text = tokenizer.apply_chat_template(example["messages"], tokenize=False) return tokenizer( text, padding=False, max_length=MAX_SEQ_LEN, truncation=True, add_special_tokens=False, ) def build_recipe(strategy: str) -> QuantizationModifier: fp8_args = QuantizationArgs(num_bits=8, type="float", strategy=strategy) return QuantizationModifier( config_groups={ "attention": QuantizationScheme( targets=["LlamaAttention"], # Quantize queries: q_scale input_activations=fp8_args, ) }, kv_cache_scheme=fp8_args, # Quantize KV cache: k/v_scale ) # ----------------------------- # Main # ----------------------------- def main(): model = AutoModelForCausalLM.from_pretrained(MODEL_ID, torch_dtype="auto") tokenizer = AutoTokenizer.from_pretrained(MODEL_ID) ds = load_dataset(DATASET_ID, split=f"{DATASET_SPLIT}[:{NUM_CALIB_SAMPLES}]") ds = ds.shuffle(seed=42) ds = ds.map( lambda ex: process_and_tokenize(ex, tokenizer), remove_columns=ds.column_names, ) recipe = build_recipe(STRATEGY) oneshot( model=model, dataset=ds, recipe=recipe, max_seq_length=MAX_SEQ_LEN, num_calibration_samples=NUM_CALIB_SAMPLES, ) save_dir = f"{MODEL_ID.rstrip('/').split('/')[-1]}-kvattn-fp8-{STRATEGY}" model.save_pretrained(save_dir, save_compressed=True) tokenizer.save_pretrained(save_dir) if __name__ == "__main__": main()脚本要点拆解:
QuantizationModifier+config_groups:把LlamaAttention的输入激活(即 Q)按fp8_args量化为 FP8,产出 per-tensor 或 per-head 的q_scale;kv_cache_scheme=fp8_args:对 KV Cache 应用同一 FP8 量化参数,产出k/v_scale;strategy="tensor"vs"attn_head":前者每张 Q/K/V 张量一个 scale;后者为每个头一个 scale,量化粒度更细、精度更好,但仅在 vLLM 的 FlashAttention 后端下被消费支持;save_compressed=True:把量化配置(含kv_cache_scheme)与权重一起写入 checkpoint,即上文"第三种隐式途径"所依赖的载体;- 校准样本数与序列长度是质量-耗时权衡点:
NUM_CALIB_SAMPLES从 512 起步通常是不错的起点;MAX_SEQ_LEN应贴近你的真实部署长度。
在 vLLM 中加载校准产物
校准并保存后,即可在 vLLM 中直接加载该目录,无需再次指定 FP8:
from vllm import LLM llm = LLM(model="Llama-3.1-8B-Instruct-kvattn-fp8-tensor")引擎会自动识别 checkpoint 中固化的 FP8 KV Cache 方案并启用(对应 attention.py 的 auto 解析路径)。若要显式覆盖(例如退回 bfloat16 验证基线),则显式传kv_cache_dtype="bfloat16",此时用户显式取值优先于 checkpoint 声明。
提示:llm-compressor 的量化 KV Cache 示例会随其版本持续演进,建议以其仓库的
examples/quantization_kv_cache目录为最新参考;本仓库仅说明 vLLM 侧的消费与配置语义。
落到引擎内部:缓存 dtype 是如何贯穿配置到 kernel 的
把上文涉及的各条线索串起来,一次 FP8 KV Cache 请求的完整链路如下:
- 参数层:用户经
LLM(...)或vllm serve --kv-cache-dtype ...传入kv_cache_dtype与kv_cache_dtype_skip_layers;引擎在 create_engine_config 中将二者解析/透传给CacheConfig; - 配置层:
CacheConfig(cache.py)持有cache_dtype,在 validators 中依据 is_quantized_kv_cache 打印"显存降低 + 可能精度损失"的运行提示; - 模型层:每个 Attention 层初始化时按 skip 规则决定自身 dtype,并结合 checkpoint 的
kv_cache_scheme决定是否启用 per-head scale(attention.py); - 后端层:注意力后端按 dtype 选择 kernel 路径;FP8 路径下 FlashAttention 类实现会把
key_cache/value_cache以 FP8 语义读取,并利用_q_scale/_k_scale做反量化对齐(flash_attn.py)。
这一层抽象意味着:用户在配置层看到的kv_cache_dtype,最终是由平台能力(CUDA/ROCm/HPU 各自支持的具体格式)、Attention 后端能力(是否支持 per-head scale)与模型/校准产物共同决定的。
选型与注意事项小结
- 首次尝试:用
kv_cache_dtype="fp8"(无校准)先量级评估显存与吞吐收益,同时用原模型做精度对照; - 追求质量:用 llm-compressor 数据集校准,优先
strategy="tensor",需要更高精度时再切attn_head(注意其 FlashAttention 后端限定与模型结构限定,示例仅覆盖 Llama 类架构的LlamaAttention); - 混合模型/长上下文:若模型含 sliding-window 层,用
--kv-cache-dtype-skip-layers sliding_window或指定层索引做定点豁免; - 平台前提:
fp8_e4m3需 CUDA 11.8+ 或 ROCm;fp8_e5m2需 CUDA 11.8+;auto始终可用; - 精度兜底:量化缓存本质上是以精度换显存/吞吐,务必在真实业务样本上回归评测,量化不总是无损。
更完整、随版本更新的校准示例请以 llm-compressor 官方示例为准,vLLM 侧的完整参数语义可继续研读 CacheConfig、EngineArgs 与 Attention 层相关源码,以及仓库内 test_attention.py、test_cache.py 等针对缓存 dtype 与 kernel 的测试用例。
【免费下载链接】vllmA high-throughput and memory-efficient inference and serving engine for LLMs项目地址: https://gitcode.com/GitHub_Trending/vl/vllm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考