一、引言
你有一张 RTX 4090,24GB 显存。你下载了 Llama-3.1-70B-Instruct,想在本机跑推理。然后发现:模型参数 70B,哪怕用 FP16(2 bytes per param),光是加载参数就需要 70×10⁹×2 = 140 GB。这还没算 KV Cache 和中间激活值。4090 连模型参数都放不下。
如果有人告诉你:把参数的精度从 16bit 降到 4bit,模型就从 140GB 缩到 35GB。再买一张 4090,两张 48GB 刚好够——而 4bit 下的模型质量,在很多任务上只比 FP16 低不到 1%。
这就是量化要做的事。
这篇文章将解释量化的基本原理:为什么大模型推理的速度瓶颈在显存带宽而非算力、量化在数学上到底做了什么、不同量化方案的粒度与范式如何划分。我们还会配合 PyTorch 代码示例和 vLLM 源码片段,读完就能动手写出自己的量化函数。
二、大模型推理的内存瓶颈
2.1 模型参数占多少显存?
一个参数所占的字节数取决于用什么精度存储它。以下是常见精度的参数占用:
| 精度 | 字节/参数 | 7B 模型 | 13B 模型 | 70B 模型 |
|---|---|---|---|---|
| FP32 | 4 | 28 GB | 52 GB | 280 GB |
| FP16 / BF16 | 2 | 14 GB | 26 GB | 140 GB |
| INT8 | 1 | 7 GB | 13 GB | 70 GB |
| INT4 | 0.5 | 3.5 GB | 6.5 GB | 35 GB |
计算公式很简单:
model_size = params × bytes_per_param
以 Llama-3.1-70B 为例:70×10⁹ 个参数,FP16 下每个 2 字节,共 140GB。这还没算推理过程中动态分配的 KV Cache(键值缓存)。
作为参考,Llama-3.1-7B 的 KV Cache 每 1K 上下文约占 0.5GB(Multi-Head Attention),而 Llama-3.1-70B 使用 GQA(分组查询注意力)压缩了 KV Cache,每 1K 上下文约占 0.3GB。
无论如何,你的 24GB 4090 连模型参数都塞不下。
2.2 为什么推理速度也被内存拖住了?
这里有一个不怎么符合直觉的事实:大模型推理的速度瓶颈不是 GPU 算力,而是显存带宽。
大语言模型是自回归生成的:每生成一个 token,必须把这 70B 个参数全部从显存读到 GPU 的计算单元(SM),算一次,然后把结果写回去。然后生成下一个 token 时,再次把全部参数读一遍——周而复始。
我们来看两个数字的对比(以 NVIDIA A100 80GB 为例):
- 算力:312 TFLOPS(FP16 Tensor Core)
- 显存带宽:2 TB/s
生成一个 token 需要多少计算?
compute ≈ 2 × N_params = 2 × 70×10⁹ = 140 GFLOP
A100 的 312 TFLOPS 算力,跑 140 GFLOP 只意味着 ~0.45 毫秒的矩阵乘法时间。但在此之前,需要从显存中读出全部权重:
70×10⁹ × 2 bytes = 140 GB
140 GB / 2 TB/s ≈ 70 ms
70 毫秒等数据,0.45 毫秒算数据。99% 以上的时间,计算单元都在空闲等待。
这就是 memory-bound 的含义:你的 GPU 算得飞快,坐在传送带前开心地摸鱼。
常见误解是 GPU 算力不够快所以推理慢。实际情况是:无论你堆多少 TFLOPS,只要显存带宽跟不上,token 生成速度就上不去。这就是为什么 H100 相比 A100 最主要改进之一是 HBM3 带宽从 2 TB/s 提升到 3.35 TB/s(SXM 版本)。
2.3 量化做什么
既然每个 token 都要把全部权重读一遍,那减少权重占的字节数,就等于直接压缩需要传输的数据量。
从 FP16(2 bytes)降到 INT4(0.5 bytes),数据传输量减少到原来的 1/4。理论加速接近 4×,因为读取 35GB 只需要 17.5ms 而非 70ms。同时显存占用也从 140GB 降到 35GB——单张 4090 放不下 70B,但两张可以。
简单来说,省下了带宽就节省了时间。
三、什么是量化:从数学到代码
3.1 直觉建立
量化的本质是把一个高精度的连续值映射到一个低精度的离散集合。
你可以这样理解:一张 24-bit 真彩色照片(每像素 3 字节,1670 万色)转成 8-bit 索引色(每像素 1 字节,256 色),文件缩小 3 倍,但视觉上仍然能辨认主体。
这里有一个关键的 insight:神经网络的权重通常呈平滑的类正态分布,0 附近的值密集,极端值稀疏。FP16 的 2¹⁶ = 65536 种可能值远超描述这种分布所需的信息量。用更少的比特数完全可以近似。
3.2 量化的数学定义
设原始值为浮点数 x,目标是用 n 个 bit 来表示它。我们定义:
scale(缩放因子):
s = (x_max - x_min) / (q_max - q_min)
zero point(零点):
z = q_min - x_min / s
其中 q_min, q_max 是量化后整数范围的边界。对于 INT8(signed),q_min = −128,q_max = 127。对于 INT4(signed),q_min = −8,q_max = 7。
量化(forward):
q = clamp(round(x / s) + z, q_min, q_max)
反量化(inverse):
x′ = (q − z) × s
几个要点:
- scale 的作用:把浮点世界的「数值间距」换算成整数世界的「1 个台阶」。比如 x_max = 0.5,用 INT8,则 s = 1.0 / 255 ≈ 0.00392——每差 0.00392,量化值就多 1。
- zero_point 的作用:标记浮点的 0.0 对应哪个整数值。这对于非对称量化很重要(见下文),对称量化下 z = 0。
- clamp:负责把超出 q_min / q_max 的值截断——这就是离群值造成量化误差的主要来源。
- round 操作引入的误差在 ± s/2 之间,是量化的固有信息损失。
3.3 对称量化 vs 非对称量化
对称量化(symmetric quantization):强制 z = 0。取 x 绝对值的最大值来算 scale,保证量化范围以 0 为中心对称。
- 优点:反量化时只需 x′ = q × s,少一次减法,计算更快。
- 缺点:如果 x 的分布不对称(比如 ReLU 输出全是非负数),有一半量化范围被浪费。
非对称量化(asymmetric quantization):
- 优点:充分利用量化范围,没有浪费。
- 缺点:每次反量化都要做 x′ = (q − z) × s,多一个操作。对矩阵乘法来说,这个额外的 z 项会被吸收进 bias 项从而基本无开销——但对逐元素运算则不同。
在对称量化中,零点的 FP 值映射到 INT 零;在非对称量化中,最小 FP 值映射到最小 INT 值。选择哪种取决于数据分布。
3.4 量化误差从哪来
量化后的值 x′ 与原始值 x 之间的误差有两个来源:
来源一:round 误差。 这是量化本身的结构性信息损失。一步 round 意味着原来在 [0, s)、[s, 2s) 等区间内的所有值被压缩成一个整数,最大误差为 s/2。
来源二:clamp 误差。 如果数据中存在离群值,x_max 被大幅拉高 → s 变大 → 每个量化台阶变宽 → 所有非离群值的 round 误差都变大了。
这就是为什么量化前常常要做「截断」:主动舍弃少量离群值,让中间的密集区域获得更精细的量化分辨率。
举个例子:一个 tensor,99% 的值在 [−1, 1] 之间,但有一个值是 10。如果用 max(|x|) = 10 来算 scale,s = 10/127 ≈ 0.0787,每个台阶约 0.08。而如果直接把 10 截断到 1 再算,s = 1/127 ≈ 0.00787,精度立刻提升 10 倍——代价是那个离群值被「砍掉」了。
这个权衡——截断离群值 vs 保留完整范围——是 AWQ(第三篇文章主题)和 SmoothQuant 的算法起点。
四、Weight-Only 与 Weight+Activation 量化
在 LLM 推理中,量化方案分为两大范式:只量化权重和权重激活都量化。
4.1 Weight-Only Quantization(W4A16 / W8A16)
- 权重量化为 INT4 或 INT8,压缩存储。
- 激活值(activation)保持 FP16 / BF16,不量化。
- Forward 过程计算时先将权重 dequant → FP16,再做常规 FP16 矩阵乘法。
典型代表是 GPTQ 和 AWQ(W4A16),以及 bitsandbytes(W8A16)。这是目前社区最主流的方案,因为:
- 不需要校准激活的量化参数——只管权重量化,实现简单。
- 对大部分 LLM 任务,W4A16 精度损失很小(<1% perplexity 退化)。
- vLLM 的 Marlin 内核针对 INT4 → FP16 的 dequant+matmul 做了极致优化。
缺点:矩阵乘法仍然是 FP16 的——只是权重存储压缩了。对于新一代支持 FP8/INT8 Tensor Core 的硬件(H100+),这不一定是性能最优路径。
4.2 Weight+Activation Quantization(W8A8)
- 权重和激活都量化,通常为 8-bit。
- Forward 过程使用整数或 FP8 矩阵乘法指令,无需在计算前 dequant。
代表方案:FP8 推理(vLLM 的 Fp8Config)、SmoothQuant。
理论上利用 INT8/FP8 Tensor Core 可以获得更高的吞吐。但代价是:
- 激活值的分布对输入数据敏感,每一层的每个 batch 可能波动很大。需要精心设计校准方法。
- 实现复杂度更高——量化参数从 1 套(权重)变成 2 套(权重 + 激活),且两者需要协同。
4.3 对比
| W4A16 | W8A8 | |
|---|---|---|
| 权重精度 | 4-bit | 8-bit |
| 激活精度 | 16-bit(浮点) | 8-bit(量化) |
| 模型大小 | 极低(4× 压缩) | 中等(2× 压缩) |
| 推理精度 | 高 | 中等 |
| 矩阵乘类型 | FP16 | INT8 / FP8 |
| 适用硬件 | Ampere+(A100, 4090) | Hopper+(H100, H200) |
| 代表算法 | GPTQ, AWQ, Marlin | FP8, SmoothQuant |
不过这并不是二选一的问题——同一个模型可以不同层用不同方案(vLLM 支持混合精度部署)。理解这两大流派后,后续文章的具体算法就有了坐标系。
五、量化数据类型一览
不同的量化方案输出不同类型的数据。vLLM 用 ScalarType 类(vllm/scalar_type.py:22-355)统一描述所有量化数据类型。我们不必遍历源码,但有必要认识几种核心类型。
5.1 浮点基准:FP16 与 BF16
FP16(IEEE 754 half precision,scalar_type.py:336 的 float16_e5m10):
- 5 位指数 + 10 位尾数 + 1 位符号 = 16 位
- 动态范围:±65504
- 精度:约 3.3 位十进制有效数字
BF16(Brain Floating Point,scalar_type.py:335 的 float16_e8m7):
- 8 位指数 + 7 位尾数 + 1 位符号 = 16 位
- 动态范围:同 FP32(±3.4×10³⁸)
- 精度:约 2 位十进制有效数字
BF16 对于 LLM 推理的意义在于延迟防溢出:大模型的激活值可能在计算过程中暴增(尤其在 attention 中),BF16 的大指数位保证不会变成 +Inf。
但这两个都是 16-bit「全精度」,不是我们要做的量化。
5.2 整数量化:INT8 与 INT4
- INT8:整数范围 [−128, 127],量化后每个参数 1 字节。最成熟的整数量化格式,广泛应用于 CV 模型和传统 ML 推理。scalar_type.py:330 的 int8。
- INT4:整数范围 [−8, 7],每个参数 0.5 字节——两个参数打包进 1 字节。GPTQ 和 AWQ 的标准输出。scalar_type.py:328 的 int4。
- uint4b8(scalar_type.py:350):这是 GPTQ 中实际使用的 4-bit 类型,值得单独说明。它是一个无符号 4-bit 整数 + bias = 8:
from vllm.scalar_type import ScalarType, scalar_types
# uint4b8: unsigned 4-bit with bias 8
t = scalar_types.uint4b8
print(t) # uint4b8
print(t.size_bits) # 4
print(t.is_integer()) # True
print(t.has_bias()) # True
print(t.bias) # 8
print(t.min(), t.max()) # -8 7
为什么用 unsigned + bias 而非 signed int? Unsigned 4-bit 的存储范围是 [0, 15]。减去 bias=8 后,实际表示值变成 [−8, 7]。等价于 signed int4,但硬件上存储 unsigned 更直接(CUDA 的 4-bit 打包格式默认按 unsigned 处理)。这就是 scalar_type.py 中 bias 字段的设计意图:不引入 sign bit,用 bias 平移即可实现对称范围。
5.3 浮点量化:FP8 E4M3 与 E5M2
FP8 是 Hopper 架构(H100/H200)引入的新量化格式——它不是整数,而是缩到 8-bit 的浮点数。
E4M3(scalar_type.py:332 的 float8_e4m3fn):
- 4 位指数 + 3 位尾数 + 1 位符号 = 8 位
- 精度高(尾数 3 位 → 每个二进制区间内细分为 2³ = 8 个台阶,相邻值的最大相对间距约 12.5%)
- 适合权重量化
E5M2(scalar_type.py:333 的 float8_e5m2):
- 5 位指数 + 2 位尾数 + 1 位符号 = 8 位
- 动态范围大(指数多 1 位 → 覆盖范围扩大一倍,最大约 57344)
- 适合激活值量化(因为激活值的波动比权重大得多)
两者的互补关系很像 BF16 和 FP16:
- E4M3 = 精度优先(类似 FP16,尾数多)
- E5M2 = 范围优先(类似 BF16,指数多)
在 vLLM 中,Fp8Config 和 Fp8OnlineLinearMethod(quantization/fp8.py)管理 FP8 量化逻辑——从 checkpoint 中读取 scale 或在加载时动态计算 scale。后续第四篇文章会完整拆解。
5.4 特殊格式:NF4 与 MX 系列
NF4(4-bit NormalFloat) 是 bitsandbytes 库引入的方案。它与均匀整数量化的核心区别是:量化台阶不是均匀分布的,而是按照标准正态分布的累积分布函数(CDF)来划分——密度高处台阶窄、密度低处台阶宽。这使得 NF4 对正态分布的 LLM 权重有更好的保真度。
MX 系列(Microscaling) 是 OCP(Open Compute Project)提出的标准,包括 MXFP8、MXFP6、MXFP4。它的核心设计是将 FP8/FP6/FP4 的 tile 共享一个指数位——一个 32×32 的浮点矩阵 tile 共用 1 个 8-bit 指数 scale,而非每个元素独立存储指数。这大幅减少了量化参数的存储开销,且天然对齐到 GPU 矩阵乘法的 warp tile。
vLLM 的 scalar_types 已预定义了 MX 类型(scalar_type.py:340-345 的 float6_e3m2f、float6_e2m3f、float4_e2m1f),以及 MX 格式对应的 ScalarType 构造方法。
5.5 速览:vLLM 支持的所有量化方法
vllm/model_executor/layers/quantization/init.py:12-46 的 QuantizationMethods 类型列出了平台支持的全部量化方案(30+ 种)。这里给出分类速览而非逐个解释:
| 类别 | 方法名 | 含义 |
|---|---|---|
| INT4 权重量化 | gptq, gptq_marlin, awq, awq_marlin | GPTQ/AWQ + Marlin 内核 |
| FP8 量化 | fp8, modelopt, modelopt_fp4 | FP8 静态 / 动态量化 |
| Online 量化 | online, fp8_per_tensor, fp8_per_block | 加载时动态量化 |
| 压缩格式 | compressed-tensors, bitsandbytes | Neural Magic 方案与 NF4 |
| 社区方案 | gguf, torchao, quark, auto-round | llama.cpp, PyTorch AO 等 |
| MoE 专用 | experts_int8, moe_wna16 | MoE expert 层量化 |
| MX 格式 | mxfp4, gpt_oss_mxfp4 | Microscaling FP4 |
这个清单让读者对整个领域的方案有一个全局认知。其中最核心的几个(GPTQ、AWQ、FP8),后续再单独成文深入。
六、PTQ 与 QAT:量化的两条路线
拿到一个训练好的模型,你要么直接在它上面做量化,要么在训练/微调时就嵌入量化意识。这两条路分别叫 PTQ 和 QAT。
6.1 Post-Training Quantization(PTQ,训练后量化)
- 流程:训练好的 FP16 模型 → 跑少量的校准数据(几百到几千条)→ 计算量化参数 → 直接转换权重。
- 耗时:几分钟到几小时,取决于模型大小和算法复杂度。
- 精度损失:通常 0.5%–2%(perplexity 退化),很多场景下可接受。
- 代表:GPTQ、AWQ、bitsandbytes 的 8/4-bit 方案、FP8 静态量化。
- 无需训练资源:一台显卡即可完成。
PTQ 是本系列的核心——GPTQ 用 Hessian 矩阵逐层补偿误差,AWQ 用激活值分析离群通道,FP8 用静态校准加在线动态量化。它们都属于 PTQ 范畴。
6.2 Quantization-Aware Training(QAT,量化感知训练)
- 流程:在训练时就「模拟」量化效果——前向传播用量化后的值,反向传播用 STE(Straight-Through Estimator)让梯度「穿过」不可微的 round → 使模型主动适应量化的信息损失。
- 耗时:需要完整训练或至少几千步微调。
- 精度损失:几乎可以忽略(<0.3%)。
- 代价:需要训练数据和算力。
- 用途:通常是 PTQ 效果不达标的「补救方案」,或用在超大 batch 场景(如推荐系统)中从头训练。
6.3 选择标准
对于绝大多数 LLM 推理用户,PTQ 就够了。你的起点是一个 HuggingFace 上已训练好的模型,你不需要(也没有资源)重新训练它。你只需要一条命令完成量化,然后部署。
因此本系列全部聚焦于 PTQ 算法及其在 vLLM 中的实现。
七、本章小结
这篇文章建立了理解模型量化所需的基础概念框架:
- 为什么量化:大模型推理是 memory-bound 的——生成每个 token 需要把全部权重从显存读一遍,而显存带宽远远跟不上算力。量化通过降低每个参数的字节数,直接压缩需要传输的数据量。
- 量化在做什么:用 scale 和 zero_point 把浮点值映射到整数空间,数学上就是一次放缩、一次取整、一次截断。对称/非对称、bit 数、截断策略决定了误差的大小。
- 量化粒度:是对 scale 管多大范围的权衡:per-tensor 最简单但误差大,per-channel 是底线,per-group(128)是 GPTQ/AWQ 的精度保证,per-block 是 FP8 时代的标准。
- 两大范式:weight-only(W4A16,只压缩存储)和 weight+activation(W8A8,利用硬件整数单元)。不同的量化算法在不同范式下运作。
- 数据类型:从 INT8/INT4 到 FP8 E4M3/E5M2 再到 NF4/MX 格式,每种类型在精度、范围、硬件支持上各有侧重。vLLM 的 ScalarType 是这些类型的统一抽象。
- PTQ vs QAT:本系列全部聚焦于 PTQ——训练后、用少量校准数据完成量化。