什么是量化?
量化简单来说,量化就是对模型的权重和激活值做舍入,把高精度的数值压缩到低精度。
大语言模型的权重通常以 FP16 或 BF16 格式存储,每个参数占 2 字节。量化的目标是用更少的比特来表示这些权重,从而减小模型体积、降低显存需求、加速推理。
量化决策流程
在选择量化方案时,遵循以下决策路径:
flowchart TD A(开始) --> B[选择 Compression Scheme] B --> C[选择量化算法] C --> D{算法需要<br>校准数据集?} D -->|否| H[执行量化] D -->|是| E[准备校准数据集] E --> F{需要<br>离群值抑制?} F -->|是| F1[离群值隔离 / 平滑缩放<br>e.g., SmoothQuant] F -->|否| G{是否采用<br>混合量化?} F1 --> G G -->|是| G1[层敏感度分析<br>与位宽分配] G -->|否| H G1 --> H H --> I(结束)
量化位宽
量化位宽定义了要对哪些数据做量化以及量化到什么精度。可量化数据有以下几类:
| 量化目标 | 特性 | 何时确定 |
|---|---|---|
Weights(权重) | 固定不变,量化后永久确定 | 压缩时静态计算,保存到 checkpoint |
Activations(激活) | 动态变化,取决于输入数据 | 压缩时静态校准或推理时动态计算 |
KV Cache | 动态变化,取决于生成序列 | 推理时动态计算 |
核心区别:
Weights是模型的参数,训练完成后就不再改变,所以量化也是离线静态完成的——算出scale和zero_point后直接存到checkpoint里。Activations是每层前向计算的中间结果,随输入而变化。可以在压缩时用校准集观察分布做静态量化,也可以在推理时实时计算量化参数做动态量化。KV Cache是推理时动态生成的键值缓存,在生成过程中逐步填充,通常采用动态量化。
量化权重的命名规则为 W{weight_bits}A{activation_bits},如 W4A16 表示权重用 4-bit、激活用 16-bit。
静态量化 vs 动态量化
上面表格里的「何时确定」一列,引出了量化的第三个独立维度——scale 和 zero_point 是离线一次算好,还是推理时实时算。这与「量化什么」「量化到多少位」是正交的轴。
| 类型 | scale / zero_point 何时算 | 典型对象 | 优势 | 代价 |
|---|---|---|---|---|
| 静态量化 | 离线一次性算好,存进 checkpoint | 权重;激活(搭配校准集) | 推理时无额外开销,最快 | 需校准集;真实分布若偏离校准分布,精度会掉 |
| 动态量化 | 推理时根据当前张量现场算 | 激活;KV Cache | 完美适配当前输入,精度更稳 | 每次前向都要跑 min/max,引入延迟 |
具体到三类量化目标:
- 权重:永远静态。训练完就不变了,没必要重算。
- 激活:两种都常见。SmoothQuant 等追求吞吐的方案用静态,简单框架(如 PyTorch dynamic quantization)用动态。
- KV Cache:几乎总是动态。生成时逐步填充,每个新 token 的分布无法离线预知。
需要强调的是,下一章「量化原理」的那套公式对静态动态一视同仁——数学是同一套,区别只在执行时机。
静态量化会在一套校准数据集上“演习”一下推理场景,看看激活值的范围,提前算出 scale 和 zeropoint。
数据格式参考
以下以 FP16 / BF16(16 位)作为基准(1x),因为当前绝大多数开源大模型下载后默认都是此格式。
FP16与BF16的区别
FP16(Float16)是传统的半精度浮点数,精度较高,但数值范围较窄(最大值仅 65504)。若激活中出现极端离群值(如 100000),会溢出为NaN,导致模型崩溃。
BF16(Bfloat16 / Brain Float 16)是 Google 专为深度学习设计的格式。它牺牲小数精度,将省下的位数分配给指数部分,使其数值范围与FP32相当,几乎不会溢出。目前较新的模型(如Llama-3、Qwen2)基本都默认推荐使用BF16。
| 数据格式 | 全称 / 俗称 | 实际位宽 | 相对 FP16 压缩率 | 显存预估(7B 模型) | 典型场景与特点 |
|---|---|---|---|---|---|
FP32 | 单精度浮点 | 32-bit | 0.5x(膨胀) | ~28 GB | 传统训练。LLM 极少使用,太吃显存。 |
FP16 / BF16 | 半精度浮点 | 16-bit | 1x(基准) | ~14 GB | 模型出厂默认格式。无损推理与常规微调。 |
INT8 / FP8 | 8 位整型 / 8 位浮点 | 8-bit | 2x | ~7 GB | 高质量推理。INT8 用于 SmoothQuant 等近无损推理;FP8 用于 H100 / RTX 40 系训练。 |
INT4 / NF4 | 4 位整型 / 4 位正态浮点 | 4-bit | 4x | ~3.5 GB | 消费级显卡黄金标准。GPTQ、AWQ、GGUF(Q4)、QLoRA 均用此格式,保留 95%+ 能力。 |
INT3 | 3 位整型 | 3-bit | ~5.3x | ~2.6 GB | 极致压缩。常见于 GGUF Q3_K_M,适合显存吃紧时跑更大模型。 |
INT2 | 2 位整型 | 2-bit | 8x | ~1.75 GB | 极限实验。模型明显降智,输出不稳定。 |
1.58-bit | 三进制量化(Ternary) | ~1.58-bit | ~10x | ~1.4 GB | 前沿架构(BitNet)。权重仅取 {-1,0,1},需从头训练,非后量化。 |
注意:
WxAy只指位宽,不指定具体数据类型
W4A16中的 4 和 16 仅代表位宽(Bit-width)——即存储容器的物理大小。它并没有规定容器里装的是什么数据格式(Data Type)。同样是 4-bit 权重,可以是INT4、NF4或FP4,具体取决于使用的量化算法或工具。但是不特别说明,这里一般都是指的INT类型。
显存占用快速估算公式
举例:8B 模型用 INT4(4-bit)推理:(纯权重;实际运行还需额外 1~2 GB 给 Activations / KV Cache)。
量化原理
在比较各种算法之前,先看清楚量化背后的核心数学。
静态量化工作流
静态权重量化的基本原理用一句话概括:将连续的浮点数空间,通过线性映射转换到离散的低位整数空间。 为了实现这个映射,我们需要找到两个关键参数:缩放因子(Scale, )和零点(Zero Point, )。
假设我们有一组原始的 FP32 浮点数权重 ,要把它量化成 位的整数 (例如 8-bit INT8,数值范围是 )。
第一步:寻找极值与计算参数(Offline 阶段)
因为权重在推理时是不变的,我们可以在量化前先遍历一遍这层神经网络的权重,找出最大值 和最小值 ,然后计算 和 :
缩放因子 Scale ():代表整数空间里每走”1”步,在浮点空间里代表多大的距离。
零点 Zero Point ():代表浮点数里的”0”被映射到了整数空间里的哪个具体整数值。它保证了原始权重中的 0 能够被精确表示,这对神经网络中的 Padding 或 ReLU 激活非常重要。
第二步:量化过程(Quantization)
有了 和 ,就可以把任意一个浮点权重 转换成整数 :
其中 round 用于四舍五入取整,clip 用于防止越界截断。
第三步:反量化过程(Dequantization — 推理阶段)
在 GPU 计算矩阵乘法时,需要把存在内存里的低位整数 还原回浮点数(通常是 FP16)再参与计算:
因为 round 取整这一步的存在, 不会完全等于最初的 ,这就是量化误差(Quantization Error)的来源。
对称量化 vs 非对称量化
上述公式描述的是一般形式的非对称量化(Asymmetric Quantization),,能精确表示浮点 0。很多推理框架也使用对称量化(Symmetric Quantization),强制 、,公式退化为:
对称量化省去了 的存储和计算开销,在硬件上实现更高效,是 INT8 推理的默认选择。代价是当权重分布不对称(如 ReLU 后的激活值全为正)时,会浪费一半的量化范围。
Round to Nearest:并非四舍五入
在深度学习和底层硬件(GPU/CPU 指令集)中,round 通常指 IEEE 754 标准的 Round to Nearest, ties to even(银行家舍入法),而非小学课本的”四舍五入”:
| 输入 | 四舍五入 | Round to Nearest, ties to even |
|---|---|---|
| 12.4 | 12 | 12(最近) |
| 12.6 | 13 | 13(最近) |
| 12.5 | 13(逢五进一) | 12(向偶数靠拢) |
| 13.5 | 14(逢五进一) | 14(向偶数靠拢) |
为什么要额外设计这个规则?因为”逢五进一”存在统计学正向偏差(Bias):
- 0~4:5 个数,向下舍
- 5~9:5 个数,向上入
- 但 0.5 这个最居中的数总是被强行拉高
对于拥有百亿级参数、进行海量乘加运算的 LLM 来说,如果所有 0.5 都向上舍入,这个正向偏差会在矩阵乘法中不断累积,最终导致模型输出产生严重的系统性偏移。向偶数舍入让向上和向下的概率在大规模统计上各占 50%,在宏观上消除了量化的系统性偏差。
推理中的计算方式
在大型语言模型的实际推理过程中,量化后的权重具体的计算方式主要取决于量化策略:是仅量化权重还是权重与激活值同时量化。
在推理之前,高精度的浮点权重(FP16 或 BF16)已经被转换为低精度整数(INT8 或 INT4)。推理时需要利用 、 和 恢复原始数值的近似值——即刚刚介绍的反量化过程:
仅量化权重(WxA16)
- 显存读取:从显存中读取低精度的 、缩放因子 和零点 。
- 反量化:数据加载到处理单元的寄存器或 SRAM 后,硬件立即执行反量化运算,将 INT 还原为 FP16/BF16:
- 浮点矩阵乘法:使用未量化的激活值 和反量化后的权重 进行标准浮点运算:
仅量化权重量化中,实际的乘加运算依然是浮点运算。量化的主要目的是加速数据搬运,减少显存带宽压力,而不是加速计算本身。
权重与激活值双量化
- 输入特征 到达后,根据其静态或动态统计的 Scale 被实时量化为 INT8:
- 整数矩阵乘法:硬件加速单元(如 Tensor Cores 或 NPU 矩阵计算单元)直接执行纯整数点积运算。累加器通常使用 INT32 防止溢出:
- 反量化结果(Rescaling):将 INT32 结果转换回 FP16/BF16,传递给下一层。此时需结合权重和激活值的 Scale:
此模式下,实际的乘加运算发生在低精度整数域,反量化操作被推迟到矩阵乘法完成之后。
分组量化的工程实践
在实际工程中,为了平衡精度损失和显存占用,通常不会让整个张量共享一个 Scale,而是采用分组量化(Group-wise Quantization)——例如每 128 个权重共享一组 。
Per-Tensor: W_q [全部] + 1 组 (S, Z) ← 精度最低,开销最小
Per-Group: W_q [128个] + 1 组 (S, Z) ← AWQ/GPTQ 默认(group_size=128)
Per-Channel: W_q [1列] + 1 组 (S, Z) ← INT8 权重量化默认
底层算子开发的核心挑战,正是如何将上述的 INT 读取 → Dequant → FP 乘法或 INT 乘法 → FP 转换过程优化。
量化模型的存储格式
量化不仅改变了计算方式,也彻底改变了模型权重在磁盘上的存储结构。以主流的 GPTQ / AWQ 量化模型为例,其 safetensors 文件中原本单一的 .weight 张量会被替换为一组新的键值。
量化三件套:qweight、scales、qzeros
以 Llama 模型中的某一层 q_proj(Query 投影层)为例,量化前后的存储结构对比如下:
| 非量化模型(FP16) | 量化模型(INT4 AWQ/GPTQ) | |
|---|---|---|
| 权重数据 | q_proj.weight(FP16) | q_proj.qweight(INT32,打包存储) |
| 缩放因子 | — | q_proj.scales(FP16) |
| 零点 | — | q_proj.qzeros(INT32,打包存储) |
| 分组索引 | — | q_proj.g_idx(INT32,部分 GPTQ 模型) |
逐个解释这些新张量的含义:
qweight(量化权重) —— 核心数据,数据类型为 INT32,但存储的实际上是更低位数数据。1 个 INT32(32 位)刚好可以塞进 8 个 INT4(4 位)数字或4个INT8数字。这样做的好处是显存连续读取效率极高——一次 32 位读取就能拿到几个权重值。
scales(缩放因子) —— 元数据,数据类型为 FP16。存储每个分组对应的缩放因子 ,用于推理时反量化:。
qzeros(量化零点) —— 元数据,数据类型为 INT32,同样经过打包处理。存储每个分组对应的零点 。
g_idx(分组索引) —— 仅部分 GPTQ 模型包含,数据类型为 INT32。用于记录哪些权重属于哪一个 Scale,主要用于特定的乱序量化优化(Act-order)。AWQ 模型通常不包含此键。
核心结论:在量化模型的 safetensors 中,原始的 .weight 键消失了,取而代之的是由 qweight(核心数据)、scales 和 qzeros(元数据)组成的”量化三件套”。
实践
我们打开 Qwen3-8B 的 model.safetensors.index.json 和其量化版本做对比:
"weight_map": {
"lm_head.weight": "model-00005-of-00005.safetensors",
"model.embed_tokens.weight": "model-00001-of-00005.safetensors",
"model.layers.0.input_layernorm.weight": "model-00001-of-00005.safetensors",
"model.layers.0.mlp.down_proj.weight": "model-00001-of-00005.safetensors",
"model.layers.0.mlp.gate_proj.weight": "model-00001-of-00005.safetensors",
"model.layers.0.mlp.up_proj.weight": "model-00001-of-00005.safetensors",
"model.layers.0.post_attention_layernorm.weight": "model-00001-of-00005.safetensors",
"model.layers.0.self_attn.k_norm.weight": "model-00001-of-00005.safetensors",
"model.layers.0.self_attn.k_proj.weight": "model-00001-of-00005.safetensors",
"model.layers.0.self_attn.o_proj.weight": "model-00001-of-00005.safetensors",
"model.layers.0.self_attn.q_norm.weight": "model-00001-of-00005.safetensors",
"model.layers.0.self_attn.q_proj.weight": "model-00001-of-00005.safetensors",
"model.layers.0.self_attn.v_proj.weight": "model-00001-of-00005.safetensors","weight_map": {
"lm_head.weight": "model-00002-of-00002.safetensors",
"model.embed_tokens.weight": "model-00001-of-00002.safetensors",
"model.layers.0.input_layernorm.weight": "model-00001-of-00002.safetensors",
"model.layers.0.mlp.down_proj.qweight": "model-00001-of-00002.safetensors",
"model.layers.0.mlp.down_proj.qzeros": "model-00001-of-00002.safetensors",
"model.layers.0.mlp.down_proj.scales": "model-00001-of-00002.safetensors",
"model.layers.0.mlp.gate_proj.qweight": "model-00001-of-00002.safetensors",
"model.layers.0.mlp.gate_proj.qzeros": "model-00001-of-00002.safetensors",
"model.layers.0.mlp.gate_proj.scales": "model-00001-of-00002.safetensors",
"model.layers.0.mlp.up_proj.qweight": "model-00001-of-00002.safetensors",
"model.layers.0.mlp.up_proj.qzeros": "model-00001-of-00002.safetensors",
"model.layers.0.mlp.up_proj.scales": "model-00001-of-00002.safetensors",
"model.layers.0.post_attention_layernorm.weight": "model-00001-of-00002.safetensors",
"model.layers.0.self_attn.k_norm.weight": "model-00001-of-00002.safetensors",
"model.layers.0.self_attn.k_proj.qweight": "model-00001-of-00002.safetensors",
"model.layers.0.self_attn.k_proj.qzeros": "model-00001-of-00002.safetensors",
"model.layers.0.self_attn.k_proj.scales": "model-00001-of-00002.safetensors",
"model.layers.0.self_attn.o_proj.qweight": "model-00001-of-00002.safetensors",
"model.layers.0.self_attn.o_proj.qzeros": "model-00001-of-00002.safetensors",
"model.layers.0.self_attn.o_proj.scales": "model-00001-of-00002.safetensors",
"model.layers.0.self_attn.q_norm.weight": "model-00001-of-00002.safetensors",
"model.layers.0.self_attn.q_proj.qweight": "model-00001-of-00002.safetensors",
"model.layers.0.self_attn.q_proj.qzeros": "model-00001-of-00002.safetensors",
"model.layers.0.self_attn.q_proj.scales": "model-00001-of-00002.safetensors",
"model.layers.0.self_attn.v_proj.qweight": "model-00001-of-00002.safetensors",
"model.layers.0.self_attn.v_proj.qzeros": "model-00001-of-00002.safetensors",
"model.layers.0.self_attn.v_proj.scales": "model-00001-of-00002.safetensors",多出来的就是量化用的额外数据。当然这是 AWQ 的量化方案,msmodelslim 的量化方案是还多一个 quant_model_description.json 用于记录张量的数据类型:
{
"model.layers.0.linear_attn.dt_bias": "FLOAT",
"model.layers.0.linear_attn.A_log": "FLOAT",
"model.layers.0.linear_attn.conv1d.weight": "FLOAT",
"model.layers.0.linear_attn.in_proj_qkvz.weight": "W8A8_DYNAMIC",
"model.layers.0.linear_attn.in_proj_qkvz.weight_scale": "W8A8_DYNAMIC",
"model.layers.0.linear_attn.in_proj_qkvz.weight_offset": "W8A8_DYNAMIC",
"model.layers.0.linear_attn.in_proj_ba.weight": "FLOAT",
"model.layers.0.linear_attn.norm.weight": "FLOAT",
"model.layers.0.linear_attn.out_proj.weight": "FLOAT",
"model.layers.0.mlp.gate.weight": "FLOAT",
"model.layers.0.mlp.experts.0.gate_proj.weight": "W8A8_DYNAMIC",
"model.layers.0.mlp.experts.0.gate_proj.weight_scale": "W8A8_DYNAMIC",
"model.layers.0.mlp.experts.0.gate_proj.weight_offset": "W8A8_DYNAMIC",
"model.layers.0.mlp.experts.0.up_proj.weight": "W8A8_DYNAMIC",为什么多了元数据,体积反而更小?
直觉上,多了 scales 和 qzeros 这些”额外”数据,体积应该变大才对。但实际上量化后的模型体积大幅缩小。秘密在于 分组量化(Group-wise Quantization)——Scale 和 Zero 并不是为每一个权重单独配备的,而是让一批权重共享一组。
这个”一批”的数量称为 Group Size(分组大小),通常设定为 64 或 128。
以 Group Size = 128、INT4 量化为例,算一笔显存账:
量化前(FP16):128 个权重 × 16 bit = 2048 bits
量化后(INT4 + 共享元数据):
- 128 个权重被压成 INT4:128 × 4 bit = 512 bits
- 这 128 个数字共享 1 个 Scale(FP16):1 × 16 bit = 16 bits
- 这 128 个数字共享 1 个 Zero(INT4):1 × 4 bit = 4 bits
总计:512 + 16 + 4 = 532 bits
平均每个权重从 16 bit 降到了约 4.16 bit,压缩率接近 3.85 倍。元数据的开销(Scale + Zero)分摊到 128 个权重上,每个权重只增加了 bit,几乎可以忽略。
影响这一平衡的关键参数是 Group Size:
| Group Size | 精度 | 元数据开销 | 典型场景 |
|---|---|---|---|
| 32 | 更高 | 较大(~0.625 bit/weight) | 精度优先 |
| 64 | 较高 | 适中(~0.313 bit/weight) | 均衡配置 |
| 128 | 标准 | 较小(~0.156 bit/weight) | GPTQ / AWQ 默认 |
| 256 | 略低 | 极小(~0.078 bit/weight) | 极致压缩 |
| Per-Tensor(∞) | 最低 | 趋近于零 | 仅 INT8 可用 |
Group Size 越小 → 每组权重越少 → Scale 和 Zero 的精细度越高 → 精度越好,但元数据占比越大。Group Size = 128 是实践中精度与压缩比的最优平衡点。
权重量化算法
对大语言模型来说,权重量化已经是一个相对成熟的领域,因为权重是静态的——训练完成后就固定不变,你可以花时间慢慢分析、反复尝试。下面介绍主流方案,以及它们各自如何使用校准集。
RTN(Round to Nearest)—— 基线
RTN 就是把「量化原理」那套公式原封不动地应用——找极值、算 scale、套公式舍入,结束。没有校准集、没有误差补偿、没有通道保护。后面的 GPTQ / AWQ / AutoRound 都是在 RTN 之上叠加各种优化层。
精度损失在高位宽(INT8)时几乎可忽略,但在 INT4 以下明显降智。它的价值在于最快、最简单,同时也是衡量其他算法增益的天然对照组——任何复杂方案如果跑得不比 RTN 好,就说明哪里出了问题。
GPTQ(Gradient-based Post-Training Quantization)
基于 Optimal Brain Surgeon(OBS) 框架——每次量化一个权重后,调整剩下的权重来补偿这次量化造成的误差,而不是独立地处理每个权重。
关键流程:
- 从校准集采集一小批激活数据
- 按 Hessian 矩阵(损失对权重的二阶导数)排序,决定先量化哪些权重
- 每量化一个权重,用最优闭式解更新其余未量化权重
校准集的作用:构建 Hessian 矩阵。 GPTQ 对校准集的要求最高(相对而言),因为它要用校准数据构造损失函数的 Hessian 矩阵,决定每步量化哪个权重以及如何补偿。GPTQ 使用同一批校准样本反复迭代——会将校准集分成若干微批次(micro-batches),每量化完一列权重后回传误差,修正剩余权重的值。校准集数量和质量直接影响 Hessian 估计的准确性:太少则 Hessian 估计噪声大,太多则过拟合导致泛化下降。
这种方法使得 GPTQ 在 INT4 上的表现远超朴素的 RTN,也是早期最流行的 4-bit 量化方案。但它计算量较大(需要跑校准集 + 矩阵求逆),对校准集的数据分布也比较敏感。
AWQ(Activation-Aware Weight Quantization)
AWQ 的关键观察:权重的重要性并不均等——一小部分权重对应着激活值中很大的特征通道,它们对模型输出影响更大。如果把那些”重要通道”的权重视作同等的精度去量化,损失会集中在关键信息上。
AWQ 的做法:
- 跑一小段校准集,观察每个输出通道的激活值大小
- 对激活值大的”重要通道”,在量化前将其权重乘以一个大于 1 的保护系数(如 )
- 量化后再除以该系数恢复原值——通过这种数学等价变换,重要通道获得了更高的有效精度
校准集的作用:识别重要通道。 AWQ 不需要 Hessian,不需要梯度,只需要跑一次前向传播,观察每个 token 在每个通道上的激活值大小。激活值大的通道被标记为”重要通道”,量化时给予保护系数。AWQ 的校准开销极低:跑一遍前向 + 统计每通道激活均值 + 算出保护系数,在校准集上过一遍通常只需要几十秒。
AWQ 的速度比 GPTQ 快得多(不需要 Hessian 计算),精度相当甚至略优,是目前消费级显卡 W4A16 推理(如 vLLM、llama.cpp)的事实标准。
激活量化挑战
如果说权重量化只是”谨慎地取舍”,那激活量化就是真正的地狱级难度。原因只有一个:离群值(Outliers)。
离群值问题
2022 年的论文《LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale》发现了一个现象:当模型参数量超过 6.7B 时,激活值中会突然出现大量极度离群的特征通道。
这些离群值的特点:
| 特点 | 说明 |
|---|---|
| 数值极大 | 可达正常值的 10~100 倍,如正常值 ~0.1,离群值 ~500 |
| 数量极少 | 通常只占所有特征通道的 ~1% |
| 位置固定 | 离群通道在序列维度上一致出现,不是随机噪声 |
| 随规模涌现 | 6.7B 以下模型基本没有,7B 以上突然涌现 |
| 集中在特定层 | 主要集中在 transformer 的早期和晚期层 |
为什么离群值让量化如此困难?原因很简单:
假如 99% 的激活值在 [-1, 1] 之间,但有 1% 的离群值达到了 [-500, 500]。做 INT8 量化时,你需要选择一个 scale 把整个范围映射到 [-128, 127]:
- 方案 A:
scale = 500 / 128 ≈ 3.9— 覆盖了离群值,但正常值0.1量化后直接变成0,信息完全丢失 - 方案 B:
scale = 1 / 128 ≈ 0.008— 保留了正常值的精度,但离群值500被截断到127,灾难性精度损失 - 两难:无论怎么选,离群值都会破坏量化效果
解决思路一:细化量化粒度
最直接的应对方案是不为整个张量使用同一个 scale,而是分得更细:
| 粒度 | 含义 | 适用对象 | 效果 |
|---|---|---|---|
| Per-Tensor | 整个张量共享一个 scale 和 zero_point | 小模型、高位宽 | 简单但极易被离群值破坏 |
| Per-Channel | 权重按输出通道算 scale(每列一个) | 权重量化 | 能有效处理权重的不均匀分布 |
| Per-Token | 激活按每个 token 单独算 scale | 动态激活量化 | 每个 token 独立适配,效果好 |
| Per-Token + Per-Channel | 激活按 token、权重按 channel 各自独立 | W4A8 W8A8 | 当前主流配置 |
“细化粒度”的本质是把问题拆小——离群值只污染它所在的那个组,不会拖垮全局的量化精度。
但粒度越细,存储 scale 的开销越大:Per-Token 量化每生成一个 token 就要多存一组 scale,增加了 KV Cache 的存储压力。
解决思路二:平滑化(SmoothQuant)
SmoothQuant 是量化前的等价变换预处理技术,不是量化算法。 它的输入是 FP16 模型,输出还是 FP16 模型(只是数值分布重新分配过),中间没有产生任何低比特数据。真正做量化还得靠后续的 RTN、GPTQ 等算法。
如果离群值躲不掉,能不能把它移到别处?这就是 SmoothQuant 的核心思想。
数学上,一个线性层可以改写成等价形式:
这里 是一个对角迁移矩阵。通过它将激活的量化难度转移到权重上:
flowchart TD A["量化前:激活 X 很难量化<br/>(离群值在激活侧)"] A --> B["乘以 diag(s)⁻¹<br/>把离群峰值压低"] B --> C["权重 W 乘以 diag(s)<br/>补偿回来"] C --> D["激活 X' = X · diag(s)⁻¹<br/>很容易量化"] C --> E["权重 W' = diag(s) · W<br/>虽然更难量化了——<br/>但权重是静态的<br/>可以离线花足够时间慢慢量化"]
用一个通俗的比喻:
你和一个胖子过窄门。胖子过不去。解决方案不是把门拆了,而是让胖子把肚子上的肉先挪到你身上——你带着肉侧身先过,过了再把肉还给他。
迁移量由一个超参数 α 控制(通常在 [0, 1] 之间):
- α = 0:完全不迁移,等价于直接量化激活——激活依然很难
- α = 1:把所有困难转移到权重——激活非常好量化,但权重变得极其难量化
- α = 0.5:经典配置,两边各分担一半
校准集的作用:搜索最优 α。 SmoothQuant 在每个候选 α 下,分别量化激活和权重,观察模型输出的 perplexity 变化,选择损失最小的 α。校准集规模对 SmoothQuant 的影响相对较小,因为 α 是一个标量超参数(而不是像 GPTQ 那样逐层逐列调整),搜索空间只有 [0, 1] 之间的几个候选值,校准的目的是验证而非训练。
SmoothQuant 使得 W8A8 在 7B~70B 模型上都能做到近无损量化(perplexity 损失 < 0.1),是当前 INT8 / FP8 推理的事实标准预处理技术。
解决思路三:混合精度
LLM.int8() 的思路更直接:探测到离群值通道后,区别对待。
flowchart TD A[对每个 Transformer Layer 的输入] --> B[前向传播] B --> C{检测:激活中是否有<br>超过阈值的离群值?} C -->|是| D[离群通道 → FP16<br>正常计算] C -->|否| E[直接 INT8 即可] D --> F[正常通道 → INT8<br>用矩阵乘法加速] F --> G[合并 FP16 + INT8 结果] E --> G
这个方案的好处是精度完全不损失,但问题也很明显——混合精度的实现复杂度高,且 FP16 通路破坏了量化带来的加速效果。实践中 LLM.int8() 的推理速度并不一定比纯 FP16 快。
解决思路四:异常值保护(SpQR、QoQ)
SpQR(Sparse Quantized Representation)的思路又进了一步:
- 跑校准集,用 Hessian 矩阵识别出对量化最敏感的那些权重和激活通道
- 把这些”敏感参数”单独分离出来,以更高精度(甚至
FP16)存储 - 其余大量”不敏感参数”用低比特量化
这样做的效果接近无损,但代价是存储格式不统一,推理系统的实现复杂度大幅上升。QoQ(QuantoQ)等项目在此基础上进一步优化了硬件适配。
混合精度量化
“混合精度”这个词在不同上下文有不同含义,在量化这里,它特指对模型的不同部分使用不同的量化精度。它的核心洞察很简单:模型的所有层对量化的敏感度不一样。
按层混合:有些层天生脆弱
大量实验表明,在同一个大模型中,不同层的量化难易程度差异巨大:
- Embedding 层:极度敏感。把 embedding 量化到 INT4,perplexity 会飙升。通常是唯一留在 FP16 不量化的层。
- 中间层(Mid Layers):最不敏感。这部分学到的是比较通用的特征表示,量化容忍度最高。
- 最后几层(Last Layers):比较敏感。它们直接输出 logits,累积误差在这里集中爆发。
- Attention 层 vs MLP 层:通常 Attention 层的量化敏感度低于 MLP 层。Attention 的主要计算是矩阵乘法(有统计平滑效应),而 MLP 中的激活函数(SiLU/GELU)对精度更敏感。
利用这一规律可以做 按层混合量化——敏感层用高位宽(INT8/FP16),不敏感层用低位宽(INT4)。
Layer 0 (Embedding): FP16 ← 跳过量化
Layer 1-3: INT8 ← 敏感:高位宽保护
Layer 4-24: INT4 ← 不敏感:极致压缩
Layer 25-27: INT8 ← 最后几层:高位宽保护
Layer 28 (LM Head): INT8 ← 本质也是 embedding,敏感
msmodelslim 实践指南
回顾本文开头的 mermaid 流程图,每一步在 msmodelslim 中的落点:
选择 Compression Scheme → 决定 w_bit/a_bit,选择量化模式
选择量化算法 → 决定 method: minmax / ssz / gptq / autoround
需要校准数据集? → 准备 JSONL 校准文件(lab_calib/ 参考格式)
需要离群值抑制? → 选择处理器: iter_smooth / smooth_quant / awq / quarot
是否混合量化? → msmodelslim analyze → exclude 回退敏感层
执行量化 → msmodelslim quant(一键)或 msmodelslim tune(自动调优)
配置案例:Qwen3-Next-80B-A3B W8A8
以下是一个完整的、经验验证的 msmodelslim YAML 配置,逐块拆解其与本文概念的对应关系。
apiversion: modelslim_v1
metadata:
config_id: qwen3_next_80b_a3b_w8a8
score: 90
verified_model_types:
- Qwen3-Next-80B-A3B-Instruct
label:
w_bit: 8
a_bit: 8
is_sparse: False
kv_cache: False
default_w8a8_dynamic: &default_w8a8_dynamic
act:
scope: "per_token"
dtype: "int8"
symmetric: True
method: "minmax"
weight:
scope: "per_channel"
dtype: "int8"
symmetric: True
method: "minmax"
spec:
process:
- type: "flex_smooth_quant"
enable_subgraph_type:
- 'norm-linear'
include:
- '*'
- type: "group"
configs:
- type: "linear_quant"
qconfig: *default_w8a8_dynamic
include: ["*self_attn*"]
exclude: ["*self_attn.o_proj*"]
- type: "linear_quant"
qconfig: *default_w8a8_dynamic
include: ["*mlp.experts*"]
- type: "linear_quant"
qconfig: *default_w8a8_dynamic
include: ["*linear_attn.in_proj_qkvz*"]
save:
- type: "ascendv1_saver"
part_file_size: 4元数据块
apiversion: modelslim_v1
metadata:
config_id: qwen3_next_80b_a3b_w8a8
score: 90
verified_model_types:
- Qwen3-Next-80B-A3B-Instruct
label:
w_bit: 8
a_bit: 8
is_sparse: False
kv_cache: False| 字段 | 含义 | 对应本文概念 |
|---|---|---|
label.w_bit: 8 / a_bit: 8 | W8A8 方案 | Compression Scheme 章节的 W8A8 |
label.kv_cache: False | 本次不量化 KV Cache | KV Cache 是独立可选量化目标 |
量化参数锚点
default_w8a8_dynamic: &default_w8a8_dynamic
act:
scope: "per_token"
dtype: "int8"
symmetric: True
method: "minmax"
weight:
scope: "per_channel"
dtype: "int8"
symmetric: True
method: "minmax"&default_w8a8_dynamic 是 YAML 锚点语法,定义后可被多处引用(*default_w8a8_dynamic),避免重复。
| 配置项 | 对应本文概念 | 为什么这样选 |
|---|---|---|
act.scope: "per_token" | 动态量化——“推理时根据当前张量现场算 scale” | 激活有离群值,per-token 为每个 token 独立适配,精度更稳 |
act.symmetric: True | 对称量化 $S = \frac{ | r |
act.method: "minmax" | 极值统计确定量化范围 | INT8 激活 minmax 通用首选 |
weight.scope: "per_channel" | Per-Channel 粒度——“每列一个 scale” | 权重按输出通道分布差异大,逐通道更精细 |
weight.symmetric: True | 对称量化,Z=0 | INT8 推理默认,硬件实现高效 |
weight.method: "minmax" | RTN 基线——“找极值、算 scale、套公式舍入” | INT8 高位宽时 RTN 精度损失可忽略,速度最快。降到 INT4 才需要 ssz/gptq/autoround |
为什么 INT8 敢用 minmax(RTN)? 这正是权重量化算法章节的结论:RTN 在 INT8 高位宽时精度损失几乎可忽略,是天然首选。
为什么激活用 per_token(动态)? Qwen3-Next 是 MoE 模型,每个 token 只激活约 3B 参数,动态量化的额外计算开销在总前向耗时中占比很低,用 per_token 换精度是划算的。
量化流水线
Step 1:离群值抑制
- type: "flex_smooth_quant"
enable_subgraph_type:
- 'norm-linear'
include:
- '*'| 配置项 | 对应本文概念 |
|---|---|
type: "flex_smooth_quant" | 思路二 SmoothQuant 的增强版——二阶段网格搜索最优 α/β,自动找到最佳迁移系数 |
enable_subgraph_type: ['norm-linear'] | SmoothQuant 数学等价变换 ,只在 RMSNorm→Linear 间迁移 |
include: ['*'] | 全局应用 |
为什么选 Flex Smooth Quant 而非 Iterative Smooth? Qwen3-Next 是 80B 级 MoE 模型,架构复杂(混合 attention + MoE + MLA),Flex 的二阶段网格搜索能自动适配各层的异质性,比手工设 alpha=0.5 更稳。代价是量化时间更长,但 80B 模型值得这个投入。
为什么只开 norm-linear 子图? SmoothQuant 类算法只在归一化层和其下游线性层之间做迁移。Attention 内部的 QKV 结构、MoE 路由门控等不适合用 smooth 处理,留给后续量化参数解决。
Step 2:分组量化
group 处理器将多个量化配置打包,每个配置用不同 include/exclude 覆盖模型不同子结构。这是 msmodelslim 实现按结构混合精度的方式。
Self-Attention 层(排除 o_proj)
- type: "linear_quant"
qconfig: *default_w8a8_dynamic
include: ["*self_attn*"]
exclude: ["*self_attn.o_proj*"]量化目标:所有 self-attention 线性层 → INT8,但 o_proj 除外(保持 FP16)。
为什么排除 o_proj? 对应混合精度章节的经验规律——o_proj 直接输出 multi-head 聚合结果,误差会通过残差连接传播到后续所有层,累积效应强。保留 FP16 是以极小显存代价换取精度保障的典型做法。
MoE 专家层
- type: "linear_quant"
qconfig: *default_w8a8_dynamic
include: ["*mlp.experts*"]量化目标:所有 MoE 专家的 FFN 线性层 → INT8。
为什么专家全量量化? 对应混合精度章节的 MoE 特有机会——每个 token 只激活少数专家(如 8 选 2),单个专家的量化误差只影响被路由到它的 token 子集,不会泛化到全部输入。专家量化容忍度普遍高于共享层。
Linear Attention 的 QKVZ 投影
- type: "linear_quant"
qconfig: *default_w8a8_dynamic
include: ["*linear_attn.in_proj_qkvz*"]量化目标:线性注意力中 Q/K/V/Z 的输入投影 → INT8。Qwen3-Next 使用混合注意力架构,Linear attention 的 in_proj_qkvz 是核心计算路径,量化后吞吐提升显著,且 INT8 对矩阵乘法精度影响很小。
谁没被量化?(隐式混合精度)
这个配置没有写全局 exclude,但通过 include 的精确匹配,以下结构自然保持 FP16:
| 结构 | 原因 |
|---|---|
| Embedding 层 | 混合精度章节明确:embedding 极度敏感,量化到 INT8 会显著拉高 perplexity,通常唯一留在 FP16 |
| LM Head | 本质也是 embedding,同理 |
| RMSNorm / LayerNorm | 参数极少(每个 hidden_dim 仅 1 个 weight),量化收益为零,保留 FP16 |
| o_proj | 显式排除,防止残差路径误差放大 |
| MoE Router / Gate | 路由决策对精度敏感,门控扰动会改变专家选择,连锁影响大 |
| 共享专家 | 不在 include 列表中(仅 *mlp.experts*,未匹配 shared),所有 token 共享,误差会泛化 |
这正体现了混合精度章节的核心原则:不是所有层的量化容忍度都一样。这个 YAML 精准地把 INT8 用在了”量化收益大、精度风险小”的地方。