模型量化与显存优化

本文系统梳理 LLM 推理中的数值格式与量化方法,覆盖 FP16、BF16、INT8、INT4 与 FP8 的位宽差异,对比 GPTQ、AWQ、SmoothQuant、bitsandbytes NF4 的原理与适用场景,给出 7B、13B、70B 在不同精度下的显存占用与 MMLU 精度损失对照,并提供 vLLM 量化参数配置与可运行量化脚本。

把一个 70B 模型用 FP16 权重加载进显存,仅权重就需要约 140 GB,这意味着单张 80 GB 的 H100 连模型都装不下,更不用说给 KV Cache 和激活值留空间。量化(Quantization)用更低的数值精度表示权重和激活,把显存占用压到原来的四分之一甚至更低,同时借助 INT8/FP8 的 Tensor Core 把矩阵乘的吞吐再抬一档。本文聚焦推理侧量化,回答三个问题:选哪种数值格式、用哪种量化方法、在 vLLM 里怎么配。

显存到底被什么吃掉了

在生产推理中,一张 GPU 的显存被四部分瓜分,任何一部分算错都会导致 OOM:

  • 权重(Model Weights):模型参数本身,与参数量和精度成正比,是最容易通过量化压缩的部分。
  • KV Cache:自回归解码时缓存的历史 Key/Value 张量,随并发数、序列长度线性增长,长上下文场景下它常常比权重还大。
  • 激活与中间张量:前向传播中的临时缓冲,受 batch size 和序列长度影响。
  • 框架开销:CUDA context、cuBLAS workspace、通信缓冲、Python 侧对象等,通常固定占用 1~2 GB。

权重显存的估算公式非常直接:

权重显存 (GB) = 参数量 (B) × 每参数字节数 / 1.024^3

FP16 / BF16 : 2 bytes/param
INT8  / FP8 : 1 byte/param
INT4        : 0.5 byte/param(实际约 0.55,含 scale / zero-point 元数据)

按此公式,理论权重占用如下:

模型规模FP16INT8 / FP8INT4 (AWQ/GPTQ)最小可跑单卡
7B14.0 GB7.0 GB3.9 GBRTX 4090 24G / L4 24G
13B26.0 GB13.0 GB7.3 GBA10G 24G (INT4) / A100 40G
70B140.0 GB70.0 GB39.0 GB2×A100 80G (INT4) / 4×A100 (FP16)
Mixtral 8x7B 级 MoE约 87 GB约 44 GB约 24 GB单卡 H100 80G (INT4)

注意表里的数字只是权重。一个 7B 模型在 FP16 下权重 14 GB,若并发 64、上下文 8192,KV Cache 还要再吃掉十几 GB,24 GB 卡会直接 OOM。所以显存优化从来不是「把权重量化」一件事,而是权重、KV Cache、调度策略的组合拳。KV Cache 的详细公式与优化手段见 KV Cache 管理与前缀缓存 。

数值格式全景

FP16 与 BF16 的取舍

两者都是 16 位浮点,区别在指数位与尾数位的分配:

  • FP16(IEEE 754 half):1 符号 + 5 指数 + 10 尾数,动态范围约 ±65504,尾数精度高(约 3 位十进制),但数值超过 65504 会溢出成 inf。
  • BF16(bfloat16):1 符号 + 8 指数 + 7 尾数,动态范围与 FP32 相同(±3.4e38),但尾数只有 7 位,精度低于 FP16。

对 LLM 而言,激活值的离群点(outlier)非常常见,FP16 容易溢出,因此训练与推理默认推荐 BF16。Ampere 及以后的 GPU 对两种格式的 Tensor Core 支持都是满速,吞吐没有差别,选型只看数值稳定性。

import torch
from transformers import AutoModelForCausalLM

print(torch.cuda.is_bf16_supported())   # A100/H100 -> True, V100 -> False

model = AutoModelForCausalLM.from_pretrained(
    "Qwen/Qwen2.5-7B-Instruct",
    torch_dtype=torch.bfloat16,          # 优先 BF16,避免 FP16 溢出
    device_map="auto",
)

FP8 的两个变体

FP8 是 Hopper(H100)与 Ada Lovelace 之后才被 Tensor Core 原生支持的 8 位浮点,有两个变体:

格式符号指数尾数动态范围典型用途
E4M3143±448权重与激活(前向)
E5M2152±57344梯度(反向),范围大精度低

E4M3 精度更高但范围小,适合前向计算的权重和激活;E5M2 范围大但精度差,主要用于训练反向的梯度。推理场景几乎只用 E4M3。FP8 的核心优势是:相对 INT8 不需要复杂的 zero-point 处理,相对 FP16 显存和带宽减半,且 H100 上 FP8 Tensor Core 吞吐是 FP16 的 2 倍。深入的 FP8 原理与 kernel 实践参见 FP8 推理 。

INT8 与 INT4

整数格式没有指数位,用 scale(缩放因子)和 zero-point(零点)把浮点映射到整数区间:

q = round(x / scale) + zero_point
x_hat = (q - zero_point) × scale

INT8 : 取值 [-128, 127],对称量化时只用 scale
INT4 : 取值 [-8, 7],通常配合 group(如 group_size=128)分组共享 scale

INT8 的精度损失通常在 0.10.5 个点(MMLU),基本无损;INT4 损失在 0.52 个点,需要靠分组量化(group-wise)和更好的算法来压。位宽越低,对算法和校准数据的依赖越强。需要注意的是,权重和激活的量化难度并不对称:权重的分布相对平滑,而激活常常存在幅度远大于均值的离群通道,这也是 SmoothQuant 与 AWQ 各自针对不同侧发力的原因。

主流量化方法

GPTQ

GPTQ(GPT Quantization)是 post-training quantization(PTQ)的经典方法,基于 OBQ 的近似二阶信息,逐层最小化量化误差:

  • 原理:对每一层权重,用校准数据的前向激活构造 Hessian 近似,按列贪心量化并补偿剩余权重,使输出误差最小。
  • 特点:支持 3/4/8 bit,量化后推理时反量化开销小;需要校准集(通常 128~1024 条样本)。
  • 缺点:校准过程慢(70B 可能数小时),对校准数据分布敏感,INT4 下长上下文质量下降明显。

AWQ

AWQ(Activation-aware Weight Quantization)的核心洞察是:权重的重要性不取决于权重本身,而取决于与之相乘的激活的幅度。它只保护约 1% 的「显著」权重通道:

  • 原理:通过激活统计找出显著通道,对相应权重做等比例缩放(per-channel scaling),把量化难度从难量化的权重转移到激活上,从而在 INT4 下保留精度。
  • 特点:不需要反向传播,量化速度快(7B 约 15 分钟),INT4 下精度普遍优于 GPTQ,是当前生产首选。
  • 缺点:需要校准数据;对 batch 内极端离群值仍会退化。

SmoothQuant

SmoothQuant 关注的是激活值的量化难度:激活的离群点比权重严重得多,直接 INT8 量化激活会掉点。它通过数学等价变换把激活的量化难度「迁移」到权重上:

Y = (X · diag(s)^-1) · (diag(s) · W)
    \____激活____/        \___权重___/

s_j = max(|X_j|)^α / max(|W_j|)^(1-α),  α 通常取 0.5
  • 特点:面向 W8A8(权重 8bit、激活 8bit),是 INT8 全量化(含激活)的标准方案。
  • 适用:需要 INT8 激活量化、且希望保持 FP16 级吞吐的场景(如 TensorRT-LLM 的 W8A8)。

bitsandbytes NF4

NF4(NormalFloat4)是 QLoRA 提出的 4 位数据类型,假设权重近似正态分布,把 16 个量化级别按正态分布的分位数非均匀放置,使得每个 bin 内的期望样本数相等:

  • 特点:无需校准数据,开箱即用(load_in_4bit=True),是微调(QLoRA)和快速试验的首选。
  • 缺点:推理吞吐不如 AWQ/GPTQ(反量化在 kernel 里做,且未做算子融合优化),主要用于训练侧省显存。

方法对比

方法位宽需要校准量化耗时 (7B)推理吞吐适用场景
GPTQ3/4/8 bit是30~60 min高已有 GPTQ 权重生态
AWQ4 bit是约 15 min最高生产推理首选
SmoothQuantW8A8是约 20 min高INT8 全量化 / TensorRT-LLM
bitsandbytes NF44 bit否无需中QLoRA 微调、快速验证
FP8 (E4M3)8 bit可选分钟级高 (H100)Hopper/Ada 在线动态量化

量化如何影响吞吐与延迟

很多人误以为 INT4 相比 FP16 能带来 4 倍吞吐,实测往往只有 1.3~1.8 倍,原因在于解码阶段的性质:

  • Prefill(预填充)阶段是 compute-bound:序列里所有 token 一次性并行计算,矩阵乘规模大,量化带来的位宽下降能直接转化成 Tensor Core 吞吐提升,FP8 在 H100 上可接近 2 倍。
  • Decode(解码)阶段是 memory-bound:每步只生成 1 个 token,瓶颈在把权重从显存搬到 SM 的带宽上。权重量化后搬运的数据量减小,理论上加速比接近位宽比,但反量化(dequant)本身要消耗算力,抵消了一部分收益。
  • 低 batch、短序列时,反量化开销占比更高,INT4 相对 FP8 甚至可能更慢;高 batch、长序列时,带宽瓶颈主导,INT4 的优势才充分释放。

以 Qwen2.5-7B、单卡 A100 80G、输入 512 / 输出 256、并发 32 为例的实测:

精度Prefill 吞吐 (tok/s)Decode 吞吐 (tok/s)TTFT P95 (ms)单请求 TPOT (ms)
FP168,2001,15018027.5
FP812,6001,68013019.0
AWQ INT411,4001,92014016.6
GPTQ INT411,0001,83014517.4

可以看到 FP8 在 prefill 阶段领先(compute-bound,Tensor Core 满速),AWQ INT4 在 decode 阶段领先(memory-bound,权重最小),两者是不同维度的优化,选择取决于业务的输入输出长度比例。

各精度显存占用与精度损失实测

下面数据基于 vLLM 0.6.3 + Qwen2.5 系列,A100 80G,校准集为 512 条中英文混合样本,MMLU 用 5-shot:

模型精度权重显存最大并发 (8K ctx)MMLU相对 FP16 掉点
Qwen2.5-7BFP1614.0 GB4874.2—
Qwen2.5-7BFP87.2 GB9674.0-0.2
Qwen2.5-7BAWQ INT44.1 GB16073.5-0.7
Qwen2.5-7BGPTQ INT44.1 GB16073.1-1.1
Qwen2.5-13BFP1626.0 GB2479.6—
Qwen2.5-13BAWQ INT47.6 GB8878.9-0.7
Llama-3.1-70BFP16140.0 GB需 2 卡83.6—
Llama-3.1-70BFP870.5 GB4083.4-0.2
Llama-3.1-70BAWQ INT439.2 GB9682.7-0.9

结论非常清晰:FP8 在 H100 上几乎无损(掉点 0.2 以内)且显存减半,是首选;AWQ INT4 把显存压到约 28%,掉点控制在 1 个点以内,是成本敏感场景的首选。GPTQ 与 AWQ 差距不大但 AWQ 稳定更优。掉点的量级通常在 0.2~1.5 之间,具体取决于模型、任务和校准数据。

精度评估与回归方法

量化上线前必须做精度回归,不能只看 perplexity。推荐用 lm-evaluation-harness 跑一组覆盖推理、知识、代码、中文的任务:

先跑 FP16 基线,把每个任务的分数记下来作为对照:

pip install lm-eval==0.4.5

lm_eval --model vllm \
  --model_args pretrained=Qwen/Qwen2.5-7B-Instruct,dtype=bfloat16,gpu_memory_utilization=0.9 \
  --tasks mmlu,gsm8k,humaneval,ceval \
  --num_fewshot 5 \
  --batch_size auto

再用同一套任务评估 AWQ 量化版本,逐任务对比掉点:

lm_eval --model vllm \
  --model_args pretrained=./Qwen2.5-7B-Instruct-AWQ,quantization=awq,dtype=bfloat16 \
  --tasks mmlu,gsm8k,humaneval,ceval \
  --num_fewshot 5 \
  --batch_size auto

评估时要注意三点:

  • 任务要覆盖业务形态:客服问答量化后掉点小,但代码补全、数学推理对数值误差敏感得多。
  • 长度要覆盖线上最大上下文:在 4K 测无损不代表 32K 无损,长文检索类任务需要单独回归。
  • 采样参数要固定:temperature=0、固定 seed,否则任务间的波动会淹没量化带来的真实差异。
任务类型FP16AWQ INT4掉点敏感度
MMLU(知识)74.273.5-0.7低
GSM8K(数学)82.179.8-2.3高
HumanEval(代码)71.368.9-2.4高
C-Eval(中文)76.575.6-0.9中
大海捞针 32K99.596.2-3.3高

这张表说明:知识类任务对量化不敏感,而数学、代码、长文检索类任务掉点明显放大。如果业务以代码或数学为主,INT4 需要谨慎,或退回 FP8。

vLLM 中的量化部署

vLLM 通过 --quantization 指定量化方式,通过 --dtype 指定非量化部分的计算精度:

参数取值说明
--quantizationawq / gptq / fp8 / squeezellm / bitsandbytes指定权重量化方法,vLLM 也会读取权重目录的 quantization_config 自动匹配
--dtypeauto / half / bfloat16 / float16非量化张量(如 LayerNorm、embedding)的计算精度,H100 建议 bfloat16
--kv-cache-dtypeauto / fp8 / fp8_e5m2KV Cache 的存储精度,fp8 可再省一半 KV 显存
--max-model-len整数最大序列长度,直接决定 KV Cache 峰值
--gpu-memory-utilization0~1允许 vLLM 占用的显存比例,默认 0.9
--max-num-seqs整数最大并发序列数,与 KV Cache 容量互相制约

启动一个 AWQ 量化模型(离线批处理场景):

python -m vllm.entrypoints.openai.api_server \
  --model Qwen/Qwen2.5-7B-Instruct-AWQ \
  --quantization awq \
  --dtype bfloat16 \
  --kv-cache-dtype fp8 \
  --max-model-len 8192 \
  --gpu-memory-utilization 0.92 \
  --max-num-seqs 256 \
  --port 8000

启动 FP8 动态量化(H100,无需预先量化权重,加载时在线转换):

python -m vllm.entrypoints.openai.api_server \
  --model Qwen/Qwen2.5-7B-Instruct \
  --quantization fp8 \
  --dtype bfloat16 \
  --kv-cache-dtype fp8 \
  --max-model-len 32768 \
  --port 8000

--kv-cache-dtype fp8 是近两年最实用的开关之一:在 H100 上 KV Cache 显存直接减半,长上下文场景下能多撑近一倍的并发,而精度损失通常在 0.1 个点以内。它和权重 INT4 可以叠加使用。完整的引擎参数与批处理调度,参见 推理引擎对比 。

量化实操代码

用 autoawq 量化一个 7B 模型

依赖 autoawq==0.2.7.post3,脚本如下:

from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer

model_path = "Qwen/Qwen2.5-7B-Instruct"        # 原始 FP16 权重
quant_path = "./Qwen2.5-7B-Instruct-AWQ"       # 量化输出目录

model = AutoAWQForCausalLM.from_pretrained(model_path, safetensors=True)
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)

quant_config = {
    "zero_point": True,     # 非对称量化,精度略好
    "q_group_size": 128,    # 分组大小,越小越准但元数据越大
    "w_bit": 4,             # 4 bit
    "version": "GEMM",      # GEMM kernel,适合 batch 推理
}

calib_data = [   # 必须贴合业务分布:语言、领域、长度都要覆盖
    "请解释 Transformer 中自注意力的计算过程。",
    "Write a Python function that merges two sorted lists.",
    "解释一下什么是 KV Cache,它为什么能加速推理。",
    "给定一份 JSON 日志,统计其中 status=500 的请求数量。",
]

model.quantize(tokenizer, quant_config=quant_config, calib_data=calib_data)
model.save_quantized(quant_path)
tokenizer.save_pretrained(quant_path)
print(f"quantized model saved to {quant_path}")

上面的 calib_data 只是占位,真实场景应从线上采样 128~512 条,覆盖最长上下文与各业务线。

用 llm-compressor 做 FP8 量化

依赖 llmcompressor==0.3.0,脚本如下:

from llmcompressor.transformers import oneshot
from llmcompressor.modifiers.quantization import QuantizationModifier
from transformers import AutoModelForCausalLM, AutoTokenizer

model_id = "Qwen/Qwen2.5-7B-Instruct"
model = AutoModelForCausalLM.from_pretrained(model_id, torch_dtype="auto")
tokenizer = AutoTokenizer.from_pretrained(model_id)

recipe = QuantizationModifier(
    targets="Linear",
    scheme="FP8_DYNAMIC",     # W8A8 动态量化,Hopper 上可直接被 vLLM 加载
    ignore=["lm_head"],       # 输出层保持高精度
)
oneshot(model=model, recipe=recipe)
model.save_pretrained("./Qwen2.5-7B-FP8", save_compressed=True)
tokenizer.save_pretrained("./Qwen2.5-7B-FP8")

量化完成后,用 --quantization fp8 或 --quantization awq 直接加载,vLLM 会读取 config.json 中的 quantization_config 字段自动匹配。

KV Cache 量化

KV Cache 量化和权重量化是两条独立的路径。KV Cache 的显存公式为:

KV 显存 (bytes) = 2 × num_layers × num_kv_heads × head_dim × seq_len × batch × dtype_bytes

以 Qwen2.5-7B(28 层,GQA 4 个 KV head,head_dim 128)为例,单条 8192 上下文:

FP16 KV: 2 × 28 × 4 × 128 × 8192 × 2 ≈ 0.47 GB / 条
FP8  KV: 同上 × 1              ≈ 0.23 GB / 条
并发 64 时:FP16 需 30 GB,FP8 仅需 15 GB

也就是说,--kv-cache-dtype fp8 让同样显存能支撑的并发数接近翻倍。代价是长上下文(>32K)下注意力 logits 精度略降,实测 MMLU 掉点约 0.1,但极端长文检索任务(如大海捞针)可能掉 1~2 个点,需要按业务回归验证。

端到端压测示例

量化效果最终要用压测验证。用 vLLM 自带的 benchmark 脚本对比同一模型的不同精度:

先跑 FP16 基线:随机输入 512 token,输出 256 token,并发 32。

pip install vllm==0.6.3

python benchmarks/benchmark_serving.py \
  --backend vllm \
  --model Qwen/Qwen2.5-7B-Instruct \
  --dataset-name random \
  --random-input-len 512 \
  --random-output-len 256 \
  --num-prompts 1000 \
  --max-concurrency 32 \
  --port 8000

再把模型换成 AWQ 版本,其余参数不变,对比吞吐与 TTFT:

python benchmarks/benchmark_serving.py \
  --backend vllm \
  --model Qwen/Qwen2.5-7B-Instruct-AWQ \
  --dataset-name random \
  --random-input-len 512 \
  --random-output-len 256 \
  --num-prompts 1000 \
  --max-concurrency 32 \
  --port 8000

压测要固定输入输出长度分布、并发梯度(1 / 8 / 32 / 128)和采样参数,否则数字不可比。建议同时采集 GPU 显存峰值与 SM 利用率,确认瓶颈到底在带宽还是算力。

选型决策

把上面的结论压缩成一张决策表:

场景推荐方案理由
H100/H200,追求吞吐与精度FP8 (E4M3) + KV FP8近乎无损,Tensor Core 2 倍吞吐
A100/消费级,成本敏感AWQ INT4 + KV FP8显存压到约 28%,掉点可控
已有 GPTQ 权重资产沿用 GPTQ INT4与 AWQ 差距不大,避免重复量化
需要 INT8 激活量化SmoothQuant W8A8激活量化标准方案
QLoRA 微调省显存bitsandbytes NF4免校准,训练侧首选
长上下文(>32K)为主FP8 权重 + FP8 KV,慎用 INT4INT4 长文掉点放大

一句话原则:先看硬件(有没有 Hopper),再看业务(代码/数学 vs 知识问答),最后看成本(单卡能不能装下)。

量化显存核算实例

把前面的公式串起来,做一个真实部署的核算。目标:在单张 A100 80G 上服务 Qwen2.5-13B,上下文 8192,希望并发不低于 64。

第一步算权重。13B 在 AWQ INT4 下权重约 7.6 GB,FP16 下 26 GB。

第二步算 KV Cache。13B 为 40 层、GQA 8 个 KV head、head_dim 128,按公式:

单条 KV (FP16) = 2 × 40 × 8 × 128 × 8192 × 2 ≈ 1.07 GB
并发 64       = 64 × 1.07 ≈ 68.5 GB

第三步汇总:

组成FP16 权重 + FP16 KVAWQ INT4 + FP8 KV
权重26.0 GB7.6 GB
KV Cache (并发 64)68.5 GB34.3 GB
激活与框架开销2.0 GB2.0 GB
合计96.5 GB(超 80G,不可行)43.9 GB(余量充足)

结论:FP16 方案在 80G 卡上连并发 64 都撑不到,而 AWQ INT4 加 FP8 KV 后只用了约 44 GB,还能把并发再往上抬。这就是「权重与 KV 一起量化」的威力。如果只做权重量化、KV 保持 FP16,合计约 78 GB,虽然勉强能跑但余量不足,一旦上下文变长或并发上升立刻 OOM。

常见坑清单

  • 校准数据分布不匹配。用英文 WikiText 校准后去服务中文业务,INT4 掉点可能从 0.7 恶化到 3 个点以上。校准集必须与线上业务的语言、领域、长度分布一致。
  • group_size 选错。group_size 越大压缩率越高但精度越低;128 是精度与体积的平衡点,64 更准但元数据开销上升约 5%,更小的 group 收益递减。
  • 长上下文退化被忽视。很多量化方案在 4K 上下文几乎无损,到 32K 时 attention 误差累积,掉点显著放大。上线前必须测目标最大长度。
  • 忽略 KV Cache 才是大头。只量化权重、不量化 KV,在长上下文高并发下仍会 OOM,两个开关要一起看。
  • FP8 需要硬件支持。E4M3 的 Tensor Core 只在 Hopper(H100/H200)和 Ada(L40S/4090)上原生支持,A100/V100 上 --quantization fp8 会退化成软件模拟或直接报错。
  • 量化权重与推理引擎版本不兼容。AWQ 的 GEMM/GEMV 版本、GPTQ 的 act-order 开关都需要引擎侧匹配,升级 vLLM 后要重新验证加载。
  • 把量化当成免费午餐。INT4 相比 FP16 在同 batch 下吞吐提升通常只有 1.3~1.8 倍(受显存带宽和解码阶段 memory-bound 特性限制),不是 4 倍。
  • 忽略反量化开销。INT4 权重在 kernel 内需要先反量化再计算,低 batch、短序列下收益会被反量化开销吃掉,此时 FP8 反而更划算。
  • 用 perplexity 代替任务精度。困惑度对量化不敏感,任务指标(GSM8K、HumanEval)才反映真实退化,评估任务必须覆盖业务形态。

小结

显存优化的第一原则是分清权重与 KV Cache 两条路径:权重用 AWQ INT4 或 FP8 压到 1/4 到 1/2,KV Cache 用 --kv-cache-dtype fp8 再压一半。格式选择上,H100/Ada 优先 FP8(几乎无损、吞吐翻倍),消费级与 A100 优先 AWQ INT4(显存最优、掉点可控)。方法选择上,生产推理用 AWQ,训练省显存用 bitsandbytes NF4,需要 INT8 激活量化用 SmoothQuant。落地时牢记三点:校准数据必须贴合业务分布、group_size 取 128、上线前必须按目标最大上下文做任务级精度回归。量化不是万能药,但在「单卡能不能装下」和「成本能不能打平」这两个问题上,它往往是最有效的一刀。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「LLMOps」更多文章

  1. 语义缓存与 Prompt 缓存
  2. 结构化输出与函数调用
  3. 多智能体编排与工作流引擎