什么是量化?

量化简单来说,量化就是对模型的权重和激活值做舍入,把高精度的数值压缩到低精度。

大语言模型的权重通常以 FP16BF16 格式存储,每个参数占 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 是模型的参数,训练完成后就不再改变,所以量化也是离线静态完成的——算出 scalezero_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 的分布无法离线预知。

需要强调的是,下一章「量化原理」的那套公式对静态动态一视同仁——数学是同一套,区别只在执行时机。

静态量化会在一套校准数据集上“演习”一下推理场景,看看激活值的范围,提前算出 scalezeropoint

数据格式参考

以下以 FP16 / BF16(16 位)作为基准(1x),因为当前绝大多数开源大模型下载后默认都是此格式。

FP16BF16 的区别

FP16(Float16)是传统的半精度浮点数,精度较高,但数值范围较窄(最大值仅 65504)。若激活中出现极端离群值(如 100000),会溢出为 NaN,导致模型崩溃。

BF16(Bfloat16 / Brain Float 16)是 Google 专为深度学习设计的格式。它牺牲小数精度,将省下的位数分配给指数部分,使其数值范围与 FP32 相当,几乎不会溢出。目前较新的模型(如 Llama-3Qwen2)基本都默认推荐使用 BF16

数据格式全称 / 俗称实际位宽相对 FP16 压缩率显存预估(7B 模型)典型场景与特点
FP32单精度浮点32-bit0.5x(膨胀)~28 GB传统训练。LLM 极少使用,太吃显存。
FP16 / BF16半精度浮点16-bit1x(基准)~14 GB模型出厂默认格式。无损推理与常规微调。
INT8 / FP88 位整型 / 8 位浮点8-bit2x~7 GB高质量推理。INT8 用于 SmoothQuant 等近无损推理;FP8 用于 H100 / RTX 40 系训练。
INT4 / NF44 位整型 / 4 位正态浮点4-bit4x~3.5 GB消费级显卡黄金标准。GPTQAWQGGUFQ4)、QLoRA 均用此格式,保留 95%+ 能力。
INT33 位整型3-bit~5.3x~2.6 GB极致压缩。常见于 GGUF Q3_K_M,适合显存吃紧时跑更大模型。
INT22 位整型2-bit8x~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 权重,可以是 INT4NF4FP4,具体取决于使用的量化算法或工具。但是不特别说明,这里一般都是指的 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.41212(最近)
12.61313(最近)
12.513(逢五进一)12(向偶数靠拢)
13.514(逢五进一)14(向偶数靠拢)

为什么要额外设计这个规则?因为”逢五进一”存在统计学正向偏差(Bias):

  • 0~4:5 个数,向下舍
  • 5~9:5 个数,向上入
  • 但 0.5 这个最居中的数总是被强行拉高

对于拥有百亿级参数、进行海量乘加运算的 LLM 来说,如果所有 0.5 都向上舍入,这个正向偏差会在矩阵乘法中不断累积,最终导致模型输出产生严重的系统性偏移。向偶数舍入让向上和向下的概率在大规模统计上各占 50%,在宏观上消除了量化的系统性偏差。

推理中的计算方式

在大型语言模型的实际推理过程中,量化后的权重具体的计算方式主要取决于量化策略:是仅量化权重还是权重与激活值同时量化。

在推理之前,高精度的浮点权重(FP16 或 BF16)已经被转换为低精度整数(INT8 或 INT4)。推理时需要利用 恢复原始数值的近似值——即刚刚介绍的反量化过程:

仅量化权重(WxA16

  1. 显存读取:从显存中读取低精度的 、缩放因子 和零点
  2. 反量化:数据加载到处理单元的寄存器或 SRAM 后,硬件立即执行反量化运算,将 INT 还原为 FP16/BF16:
  3. 浮点矩阵乘法:使用未量化的激活值 和反量化后的权重 进行标准浮点运算:

仅量化权重量化中,实际的乘加运算依然是浮点运算。量化的主要目的是加速数据搬运,减少显存带宽压力,而不是加速计算本身。

权重与激活值双量化

  1. 输入特征 到达后,根据其静态或动态统计的 Scale 被实时量化为 INT8:
  2. 整数矩阵乘法:硬件加速单元(如 Tensor Cores 或 NPU 矩阵计算单元)直接执行纯整数点积运算。累加器通常使用 INT32 防止溢出:
  3. 反量化结果(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(核心数据)、scalesqzeros(元数据)组成的”量化三件套”。

实践

我们打开 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",

为什么多了元数据,体积反而更小?

直觉上,多了 scalesqzeros 这些”额外”数据,体积应该变大才对。但实际上量化后的模型体积大幅缩小。秘密在于 分组量化(Group-wise Quantization)——Scale 和 Zero 并不是为每一个权重单独配备的,而是让一批权重共享一组。

这个”一批”的数量称为 Group Size(分组大小),通常设定为 64128

以 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) 框架——每次量化一个权重后,调整剩下的权重来补偿这次量化造成的误差,而不是独立地处理每个权重。

关键流程:

  1. 从校准集采集一小批激活数据
  2. 按 Hessian 矩阵(损失对权重的二阶导数)排序,决定先量化哪些权重
  3. 每量化一个权重,用最优闭式解更新其余未量化权重

校准集的作用:构建 Hessian 矩阵。 GPTQ 对校准集的要求最高(相对而言),因为它要用校准数据构造损失函数的 Hessian 矩阵,决定每步量化哪个权重以及如何补偿。GPTQ 使用同一批校准样本反复迭代——会将校准集分成若干微批次(micro-batches),每量化完一列权重后回传误差,修正剩余权重的值。校准集数量和质量直接影响 Hessian 估计的准确性:太少则 Hessian 估计噪声大,太多则过拟合导致泛化下降。

这种方法使得 GPTQINT4 上的表现远超朴素的 RTN,也是早期最流行的 4-bit 量化方案。但它计算量较大(需要跑校准集 + 矩阵求逆),对校准集的数据分布也比较敏感。

AWQ(Activation-Aware Weight Quantization)

AWQ 的关键观察:权重的重要性并不均等——一小部分权重对应着激活值中很大的特征通道,它们对模型输出影响更大。如果把那些”重要通道”的权重视作同等的精度去量化,损失会集中在关键信息上。

AWQ 的做法:

  1. 跑一小段校准集,观察每个输出通道的激活值大小
  2. 对激活值大的”重要通道”,在量化前将其权重乘以一个大于 1 的保护系数(如
  3. 量化后再除以该系数恢复原值——通过这种数学等价变换,重要通道获得了更高的有效精度

校准集的作用:识别重要通道。 AWQ 不需要 Hessian,不需要梯度,只需要跑一次前向传播,观察每个 token 在每个通道上的激活值大小。激活值大的通道被标记为”重要通道”,量化时给予保护系数。AWQ 的校准开销极低:跑一遍前向 + 统计每通道激活均值 + 算出保护系数,在校准集上过一遍通常只需要几十秒。

AWQ 的速度比 GPTQ 快得多(不需要 Hessian 计算),精度相当甚至略优,是目前消费级显卡 W4A16 推理(如 vLLMllama.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)的思路又进了一步:

  1. 跑校准集,用 Hessian 矩阵识别出对量化最敏感的那些权重和激活通道
  2. 把这些”敏感参数”单独分离出来,以更高精度(甚至 FP16)存储
  3. 其余大量”不敏感参数”用低比特量化

这样做的效果接近无损,但代价是存储格式不统一,推理系统的实现复杂度大幅上升。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: 8W8A8 方案Compression Scheme 章节的 W8A8
label.kv_cache: False本次不量化 KV CacheKV 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=0INT8 推理默认,硬件实现高效
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 用在了”量化收益大、精度风险小”的地方。