03 · 量化算法深入
本文深入剖析 10 种校准算法与 AutoQuantize 自动搜索的实现细节。校准算法是量化精度的关键------同样 NVFP4 配置,max 与 local_hessian 的精度差异可达数个百分点。
目录
- 算法一览表
- [max 校准](#max 校准)
- [mse 校准与 NVFP4MSECalibrator](#mse 校准与 NVFP4MSECalibrator)
- [local_hessian 校准](#local_hessian 校准)
- [smoothquant 校准](#smoothquant 校准)
- [AWQ 系列:awq_lite / awq_clip / awq_full](#AWQ 系列:awq_lite / awq_clip / awq_full)
- [gptq 校准](#gptq 校准)
- [svdquant 校准](#svdquant 校准)
- layerwise_calibrate:分块校准与断点续算
- [AutoQuantize 自动搜索](#AutoQuantize 自动搜索)
- 本篇使用指南
- 本篇效果对比
1. 算法一览表
| 算法 | Mode name | 实现文件 | 适用格式 | 计算成本 | 精度倾向 |
|---|---|---|---|---|---|
max |
max_calibrate |
model_calib.py:max_calibrate(file:///workspace/modelopt/torch/quantization/model_calib.py) | 全部 | 极低 | 偏饱和 |
mse |
mse_calibrate |
model_calib.py:mse_calibrate(file:///workspace/modelopt/torch/quantization/model_calib.py) | 全部 | 中 | 偏最优 |
local_hessian |
local_hessian_calibrate |
model_calib.py:local_hessian_calibrate(file:///workspace/modelopt/torch/quantization/model_calib.py) | NVFP4 | 高 | 最优 |
smoothquant |
smoothquant |
model_calib.py:smoothquant(file:///workspace/modelopt/torch/quantization/model_calib.py) | INT8 | 低 | 激活异常值友好 |
awq_lite |
awq_lite_calibrate |
model_calib.py:awq_lite(file:///workspace/modelopt/torch/quantization/model_calib.py) | INT4 / NVFP4 | 中 | 偏搜索 |
awq_clip |
awq_clip_calibrate |
model_calib.py:awq(file:///workspace/modelopt/torch/quantization/model_calib.py) | INT4 / NVFP4 | 高 | 块级裁剪 |
awq_full |
awq_full_calibrate |
model_calib.py:awq(file:///workspace/modelopt/torch/quantization/model_calib.py) | INT4 / NVFP4 | 高 | 最优 |
gptq |
gptq_calibrate |
model_calib.py:gptq(file:///workspace/modelopt/torch/quantization/model_calib.py) | INT4 / NVFP4 | 极高 | 极优 |
svdquant |
svdquant_calibrate |
model_calib.py:svdquant(file:///workspace/modelopt/torch/quantization/model_calib.py) | NVFP4 + LoRA | 极高 | 最优 |
None |
_no_calibrate |
--- | cast 模式 | 0 | 取决于默认 |
配图 1:校准算法选择决策树
#mermaid-svg-YRWgDhptCgnISvmn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YRWgDhptCgnISvmn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YRWgDhptCgnISvmn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YRWgDhptCgnISvmn .error-icon{fill:#552222;}#mermaid-svg-YRWgDhptCgnISvmn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YRWgDhptCgnISvmn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YRWgDhptCgnISvmn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YRWgDhptCgnISvmn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YRWgDhptCgnISvmn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YRWgDhptCgnISvmn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YRWgDhptCgnISvmn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YRWgDhptCgnISvmn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YRWgDhptCgnISvmn .marker.cross{stroke:#333333;}#mermaid-svg-YRWgDhptCgnISvmn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YRWgDhptCgnISvmn p{margin:0;}#mermaid-svg-YRWgDhptCgnISvmn .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-YRWgDhptCgnISvmn .cluster-label text{fill:#333;}#mermaid-svg-YRWgDhptCgnISvmn .cluster-label span{color:#333;}#mermaid-svg-YRWgDhptCgnISvmn .cluster-label span p{background-color:transparent;}#mermaid-svg-YRWgDhptCgnISvmn .label text,#mermaid-svg-YRWgDhptCgnISvmn span{fill:#333;color:#333;}#mermaid-svg-YRWgDhptCgnISvmn .node rect,#mermaid-svg-YRWgDhptCgnISvmn .node circle,#mermaid-svg-YRWgDhptCgnISvmn .node ellipse,#mermaid-svg-YRWgDhptCgnISvmn .node polygon,#mermaid-svg-YRWgDhptCgnISvmn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YRWgDhptCgnISvmn .rough-node .label text,#mermaid-svg-YRWgDhptCgnISvmn .node .label text,#mermaid-svg-YRWgDhptCgnISvmn .image-shape .label,#mermaid-svg-YRWgDhptCgnISvmn .icon-shape .label{text-anchor:middle;}#mermaid-svg-YRWgDhptCgnISvmn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YRWgDhptCgnISvmn .rough-node .label,#mermaid-svg-YRWgDhptCgnISvmn .node .label,#mermaid-svg-YRWgDhptCgnISvmn .image-shape .label,#mermaid-svg-YRWgDhptCgnISvmn .icon-shape .label{text-align:center;}#mermaid-svg-YRWgDhptCgnISvmn .node.clickable{cursor:pointer;}#mermaid-svg-YRWgDhptCgnISvmn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YRWgDhptCgnISvmn .arrowheadPath{fill:#333333;}#mermaid-svg-YRWgDhptCgnISvmn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YRWgDhptCgnISvmn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YRWgDhptCgnISvmn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YRWgDhptCgnISvmn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YRWgDhptCgnISvmn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YRWgDhptCgnISvmn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YRWgDhptCgnISvmn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YRWgDhptCgnISvmn .cluster text{fill:#333;}#mermaid-svg-YRWgDhptCgnISvmn .cluster span{color:#333;}#mermaid-svg-YRWgDhptCgnISvmn div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YRWgDhptCgnISvmn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YRWgDhptCgnISvmn rect.text{fill:none;stroke-width:0;}#mermaid-svg-YRWgDhptCgnISvmn .icon-shape,#mermaid-svg-YRWgDhptCgnISvmn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YRWgDhptCgnISvmn .icon-shape p,#mermaid-svg-YRWgDhptCgnISvmn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YRWgDhptCgnISvmn .icon-shape .label rect,#mermaid-svg-YRWgDhptCgnISvmn .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YRWgDhptCgnISvmn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YRWgDhptCgnISvmn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YRWgDhptCgnISvmn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} FP8 / INT8 per-tensor
INT8 含异常值激活
INT4 W4A16 / W4A8
NVFP4 / MXFP4
90% 即可
95%+
极致精度
不需要精校
通用精校
极致精度
是
否
组合
组合
组合
组合
选择校准算法
量化格式?
max
最简单,1 次前向
smoothquant
alpha=0.5
精度要求?
精度要求?
awq_lite
快速通道缩放
awq_full
lite + clip
gptq
layerwise + Hessian
mse
126 候选 FP8 scale
显存够吗?
local_hessian
H=XtX 加权
local_hessian + layerwise
逐层校准
说明 :算法选择遵循"精度需求驱动 + 资源约束"原则。FP8/INT8 通常 max 足够;INT4 系列按精度需求递增选 awq_lite → awq_full → gptq;NVFP4 按 max → mse → local_hessian 递增。layerwise 是正交选项,可与任何算法组合(除 svdquant)来节省显存------但代价是失去跨层误差传播的真实估计(除 GPTQ 默认开启 get_qdq_activations_from_prev_layer=True)。
2. max 校准
核心文件 :`modelopt/torch/quantization/model_calib.py:max_calibrate`(file:///workspace/modelopt/torch/quantization/model_calib.py) 与 `calib/max.py:MaxCalibrator`(file:///workspace/modelopt/torch/quantization/calib/max.py)
2.1 实现原理
MaxCalibrator 维护一个 running max:
python
class MaxCalibrator(_Calibrator):
def collect(self, x):
current_amax = reduce_amax(torch.abs(x), axis=self._axis)
self._calib_amax = torch.maximum(self._calib_amax, current_amax)
def compute_amax(self):
return self._calib_amax
2.2 max_calibrate 完整流程
python
def max_calibrate(model, forward_loop, distributed_sync=True,
sync_expert_weight_amax=False, shared_states=None):
# 1. 附着 SharedWeightGlobalAmaxState(Q/K/V、gate/up 共享 amax)
SharedWeightGlobalAmaxState.attach(model, patterns=weight_patterns)
# 2. 启用统计收集(每个 TensorQuantizer: enable_calib + disable_quant)
enable_stats_collection(model)
# 3. 直接调 weight_quantizer(weight) 确保 MoE 未被路由的 expert 也有 amax
weight_only_quantize(model)
# 4. 运行 forward_loop 收集 input/output amax
forward_loop(model)
# 5. 每个 quantizer 调 load_calib_amax() → _amax buffer
finish_stats_collection(model)
# 6. MoE 本地 expert amax 同步
for module in model.modules():
if hasattr(module, "layer_sync_moe_local_experts_amax"):
module.layer_sync_moe_local_experts_amax(sync_weight_amax=sync_expert_weight_amax)
# 7. 分布式 amax 同步(dist.all_reduce(amax, op=MAX))
# 8. _finalize_with_shared_state: Q/K/V gate/up 合并共享 amax
# 9. promote_nvfp4_static_quantizers(如有 NVFP4 配置)
关键设计:
- SharedWeightGlobalAmaxState:把 Q/K/V、gate/up 等兄弟 linears 的 amax 强制相等(取 max)。这是部署 kernel(如 TRT-LLM 的 fused QKV)的硬性要求。
weight_only_quantize:MoE 模型校准时,部分 expert 可能从未被路由到,导致其 weight_quantizer 没机会调forward。weight_only_quantize直接调每个weight_quantizer(weight)兜底。sync_expert_weight_amax:MoE 的 EP(expert parallelism)默认不同步 routed expert weight amax(因为 per-rank shard 不同),但 dense weight 必须同步。
2.3 论文来源与算法思想
max 校准是最经典的 PTQ 校准方法,无单一论文归属,源自早期神经网络量化文献:
- Jacob et al., 2018 --- "Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference" (CVPR 2018,arXiv:1712.05877)--- TensorFlow Lite 量化方案,奠定 per-tensor
max校准基础。 - Krishnamoorthi, 2018 --- "Quantizing deep convolutional networks for efficient inference: A whitepaper"(arXiv:1806.08342)--- 系统总结 INT8 量化中 max 校准的使用规范。
算法思想 :用校准集上每个 tensor 的最大绝对值 amax = max(|X|) 作为缩放尺度 scale = amax / qmax(qmax 是量化级别上界,如 INT8 的 127)。本质是保证不饱和:任何激活值都落在量化范围内,但代价是"罕用大值"占用了大量精度位。
举例 :假设某 linear 的输入 X 在 100 个校准样本上的 amax = 6.0,但 99% 的值都落在 -1, 1 内:
max校准:把 INT8 的 256 级均匀映射到 -6, 6,导致 -1, 1 内只有约 43 级(256 × 1/6 ≈ 43),主分布精度损失严重- 这是后续 SmoothQuant / MSE / Hessian 校准要解决的"被异常值拖累"问题
与 ModelOpt 实现的关系 :`MaxCalibrator`(file:///workspace/modelopt/torch/quantization/calib/max.py) 维护 running max,支持 per-tensor 与 per-channel 两种 axis;max_calibrate 额外处理 MoE 未路由 expert 的兜底(weight_only_quantize)与 Q/K/V 共享 amax 同步(SharedWeightGlobalAmaxState),这些是工程优化而非算法改动。max 是所有其他校准算法(mse / local_hessian / smoothquant / awq / gptq / svdquant)的初始化前置步骤------先 max 得到粗略 amax,再用更精细的算法搜索。
3. mse 校准与 NVFP4MSECalibrator
核心文件 :`modelopt/torch/quantization/model_calib.py:mse_calibrate`(file:///workspace/modelopt/torch/quantization/model_calib.py) 与 `calib/mse.py:MseCalibrator`(file:///workspace/modelopt/torch/quantization/calib/mse.py) / `calib/mse.py:NVFP4MSECalibrator`(file:///workspace/modelopt/torch/quantization/calib/mse.py)
3.1 MseCalibrator 原理
python
class MseCalibrator(_Calibrator):
"""Per-tensor/per-channel MSE amax 搜索。"""
def __init__(self, num_bits, axis, method, total_step_size, start_multiplier, stop_multiplier):
# 生成候选:linspace(start_multiplier, stop_multiplier, ceil((stop-start)/step)+1)
self._candidates = ... # [start * initial_amax, ..., stop * initial_amax]
self._errors = [0.0] * len(self._candidates)
def collect(self, x):
for candidate_amax in self._candidates:
quant_x = self._quant_func(x, candidate_amax) # 调用 _mse_quant_func
dequant_x = quant_x.dequantize()
self._errors[i] += ((x - dequant_x) ** 2).sum()
def compute_amax(self):
return self._candidates[argmin(self._errors)]
_mse_quant_func 是一个闭包,由 mse_calibrate 注入:
python
def _mse_quant_func(weight_quantizer, weight):
def quant_func(x, amax):
# 临时设 _amax,调 _fake_quantize
with set_amax_temporarily(weight_quantizer, amax):
return weight_quantizer._fake_quantize(x)
return quant_func
3.2 NVFP4MSECalibrator:126 个 FP8 scale 候选
NVFP4 的特殊性在于:per-block scale 是 FP8 E4M3,只有 126 个合法值 (除去 0 与 NaN/Inf)。NVFP4MSECalibrator 重写候选生成:
python
class NVFP4MSECalibrator(MseCalibrator):
def _generate_candidates(self):
# 直接遍历 126 个合法 FP8 E4M3 scale 值
return fp8_scale_candidates # 来自 kernels/quantization/gemm/_fp8_scale_candidates
def _compute_candidate_amax(self, candidate):
# candidate 是 FP8 scale,amax = global_amax * candidate
# 而不是 MseCalibrator 的 initial_amax * candidate
return self._global_amax * candidate
配图 2:MSE 候选扫描流程
#mermaid-svg-9DeUjIK57iJnvrwE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9DeUjIK57iJnvrwE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9DeUjIK57iJnvrwE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9DeUjIK57iJnvrwE .error-icon{fill:#552222;}#mermaid-svg-9DeUjIK57iJnvrwE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9DeUjIK57iJnvrwE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9DeUjIK57iJnvrwE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9DeUjIK57iJnvrwE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9DeUjIK57iJnvrwE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9DeUjIK57iJnvrwE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9DeUjIK57iJnvrwE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9DeUjIK57iJnvrwE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9DeUjIK57iJnvrwE .marker.cross{stroke:#333333;}#mermaid-svg-9DeUjIK57iJnvrwE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9DeUjIK57iJnvrwE p{margin:0;}#mermaid-svg-9DeUjIK57iJnvrwE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-9DeUjIK57iJnvrwE .cluster-label text{fill:#333;}#mermaid-svg-9DeUjIK57iJnvrwE .cluster-label span{color:#333;}#mermaid-svg-9DeUjIK57iJnvrwE .cluster-label span p{background-color:transparent;}#mermaid-svg-9DeUjIK57iJnvrwE .label text,#mermaid-svg-9DeUjIK57iJnvrwE span{fill:#333;color:#333;}#mermaid-svg-9DeUjIK57iJnvrwE .node rect,#mermaid-svg-9DeUjIK57iJnvrwE .node circle,#mermaid-svg-9DeUjIK57iJnvrwE .node ellipse,#mermaid-svg-9DeUjIK57iJnvrwE .node polygon,#mermaid-svg-9DeUjIK57iJnvrwE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9DeUjIK57iJnvrwE .rough-node .label text,#mermaid-svg-9DeUjIK57iJnvrwE .node .label text,#mermaid-svg-9DeUjIK57iJnvrwE .image-shape .label,#mermaid-svg-9DeUjIK57iJnvrwE .icon-shape .label{text-anchor:middle;}#mermaid-svg-9DeUjIK57iJnvrwE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-9DeUjIK57iJnvrwE .rough-node .label,#mermaid-svg-9DeUjIK57iJnvrwE .node .label,#mermaid-svg-9DeUjIK57iJnvrwE .image-shape .label,#mermaid-svg-9DeUjIK57iJnvrwE .icon-shape .label{text-align:center;}#mermaid-svg-9DeUjIK57iJnvrwE .node.clickable{cursor:pointer;}#mermaid-svg-9DeUjIK57iJnvrwE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-9DeUjIK57iJnvrwE .arrowheadPath{fill:#333333;}#mermaid-svg-9DeUjIK57iJnvrwE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9DeUjIK57iJnvrwE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9DeUjIK57iJnvrwE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9DeUjIK57iJnvrwE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9DeUjIK57iJnvrwE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9DeUjIK57iJnvrwE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-9DeUjIK57iJnvrwE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9DeUjIK57iJnvrwE .cluster text{fill:#333;}#mermaid-svg-9DeUjIK57iJnvrwE .cluster span{color:#333;}#mermaid-svg-9DeUjIK57iJnvrwE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9DeUjIK57iJnvrwE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9DeUjIK57iJnvrwE rect.text{fill:none;stroke-width:0;}#mermaid-svg-9DeUjIK57iJnvrwE .icon-shape,#mermaid-svg-9DeUjIK57iJnvrwE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9DeUjIK57iJnvrwE .icon-shape p,#mermaid-svg-9DeUjIK57iJnvrwE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-9DeUjIK57iJnvrwE .icon-shape .label rect,#mermaid-svg-9DeUjIK57iJnvrwE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9DeUjIK57iJnvrwE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-9DeUjIK57iJnvrwE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-9DeUjIK57iJnvrwE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
输入 weight
先 max 校准
初始化 _amax / _global_amax
遍历 126 个 FP8 scale 候选
对每个候选 amax:
-
临时设 _amax = global_amax * scale
-
调用 _fake_quantize
-
计算重建误差 sum sq err
累积误差到 errors list
所有候选处理完?
argmin 选最小误差候选
写入 _amax 与 _global_amax
说明 :NVFP4 的 MSE 校准流程是------先用 max 初始化 amax,然后扫描 126 个合法的 FP8 E4M3 scale 值(fp8_scale_candidates,约 126 个),对每个 scale 计算重建误差(weight - dequantize(fake_quantize(weight)) 的平方和),最后选误差最小的 scale。在 CUDA 上有 Triton fused kernel nvfp4_fp8_scale_sweep 加速;在 CPU 上是 Python 回退的 126 步循环。_compute_candidate_amax 用 global_amax * candidate 而非 initial_amax * candidate,确保 per-block amax 与全局 FP8 scale 一致。
3.3 论文来源与算法思想
MSE 校准无单一论文,源自通用的"最小均方重建误差"PTQ 思想,相关文献包括:
- Meller et al., 2017 --- "Same, Same But DifferNet: Semi-Supervised Learning with Automatic Clustering" 等同期工作首次提出在 PTQ 中扫描 amax 候选、选最小重建误差的方案。
- Choukroun et al., 2019 --- "On the Error Analysis of Quantized Deep Neural Networks"(arXiv:1907.06194)--- 系统分析 MSE 度量在量化中的理论性质,证明其对均匀量化是最优的 amax 选择准则之一。
- NVIDIA NVFP4 Blog --- Introducing NVFP4 for Efficient and Accurate Low-Precision Inference --- 把 MSE 候选搜索特化为 NVFP4 的 126 个合法 FP8 scale 值。
算法思想 :与其用 amax 保证不饱和,不如显式最小化量化前后的重建误差 MSE(X, dequant(quant(X, amax)))。在校准集上扫描多个候选 amax(如 [0.5×amax_init, ..., 2.0×amax_init]),选使重建误差最小的那个。这允许"轻微饱和换取主分布精度"------当 99% 的值落在小范围时,让最大值轻微 clip 反而能保留主分布精度。
举例 :某 weight 张量 amax = 6.0,扫描候选 [3.0, 3.5, 4.0, ..., 6.0]:
| 候选 amax | 饱和情况 | 主分布精度 | MSE |
|---|---|---|---|
| 6.0(即 max 校准) | 无饱和 | 差(256 级铺满 -6,6) | 0.05 |
| 4.0(MSE 选这个) | 轻微饱和 | 好(256 级铺满 -4,4,主分布精度提升 1.5×) | 0.02 |
| 3.0 | 严重饱和 | 极好但大值失真 | 0.08 |
MSE 校准自动找到"饱和与精度的最优平衡点"。
与 NVFP4 的特殊性 :NVFP4 的 per-block scale 必须是 FP8 E4M3(仅 126 个合法值,排除 0 与 NaN/Inf),所以 NVFP4MSECalibrator 不能像普通 MseCalibrator 那样连续 linspace 候选------它直接遍历 126 个合法 FP8 scale,每个对应一个 amax = global_amax × scale。CUDA 上由 nvfp4_fp8_scale_sweep Triton fused kernel 加速,CPU 上是 Python 回退。
与 max / local_hessian 的关系:MSE 是 max 与 local_hessian 之间的折中------比 max 多了"误差最小化"但比 local_hessian 少了"Hessian 加权"。MSE 视所有 weight/activation 等权(误差平方和),local_hessian 视其按 Hessian 加权(输出影响平方和)。对 NVFP4,MSE 通常比 max 高 1-2% 精度,比 local_hessian 低 0.5-1%。
4. local_hessian 校准
核心文件 :`modelopt/torch/quantization/model_calib.py:local_hessian_calibrate`(file:///workspace/modelopt/torch/quantization/model_calib.py) 与 `utils/calib_utils.py:_LocalHessianAccumulator`(file:///workspace/modelopt/torch/quantization/utils/calib_utils.py)
4.1 原理
MSE 只考虑 weight 的重建误差,忽略下游影响 ------某些 weight 误差小但对输出影响大(如关键注意力权重)。local_hessian 用 Hessian 加权重建误差:
python
# 对每个 linear 层,计算 Hessian:H = XᵀX(X 是输入)
# 误差度量:dwᵀ H dw(dw = weight - dequantize(fake_quantize(weight)))
# 这是泰勒展开的二阶项,反映 weight 扰动对输出 y = Xw 的影响
4.2 实现
python
def local_hessian_calibrate(model, forward_loop, ...):
# 1. 先 mse/max 校准初始化
_run_and_load_max_stats(model, forward_loop, ...)
# 2. 对每个 QuantLinear 注册前向 hook 收集 X
for module in model.modules():
if isinstance(module, QuantLinear):
_register_local_hessian_input_hooks(module)
# hook: XᵀX 累积
# 3. 运行 forward_loop 收集 Hessian
forward_loop(model)
# 4. 对每个 weight quantizer 扫描候选 amax
for module in ...:
# 用 H 加权误差代替 MSE
for candidate_amax in candidates:
with set_amax_temporarily(...):
dw = weight - dequantize(fake_quantize(weight))
error = dw.T @ H @ dw # Hessian 加权
errors.append(error)
best_amax = candidates[argmin(errors)]
# 5. Triton fused kernel: nvfp4_fp8_scale_sweep_hessian
4.3 Triton 加速
在 CUDA 上,nvfp4_fp8_scale_sweep_hessian kernel 把 Hessian 计算 + 候选扫描 + 误差累积全部融合:
python
# kernels/quantization/gemm/ 下的 Triton kernel
# 输入:weight, X(输入), candidates(126 个 FP8 scale)
# 输出:每个 weight 块的最佳 scale
# 计算:H = XᵀX → 对每个 candidate:dw = w - quant_dequant(w, scale) → error = dwᵀ H dw
与 Python 回退相比,Triton kernel 可加速 50-100×。
4.4 论文来源与算法思想
local_hessian 校准的 Hessian 加权误差思想源自 Optimal Brain Quantization (OBQ) / Optimal Brain Compression (OBC) 与 GPTQ 框架:
- Frantar et al., 2022 --- "Optimal Brain Compression: A Framework for Accurate Post-Training Quantization and Pruning" (NeurIPS 2022,arXiv:2207.09709)--- 提出 OBQ:用二阶 Hessian 信息指导权重量化顺序与误差补偿,是 GPTQ 与 local_hessian 的共同理论源头。论文基于经典 Optimal Brain Surgeon (OBS)(LeCun et al., 1990)框架,将其扩展到量化场景。
- Frantar et al., 2023 --- "GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers"(ICLR 2023,arXiv:2210.17323)--- 把 OBQ 简化为列向更新并扩展到百亿参数,提出 Hessian 加权重建误差作为量化质量度量。
算法思想 :MSE 只看 ‖W - Q(W)‖²(权重的重建误差),忽略权重扰动对输出 的影响。Hessian 加权误差 Δwᵀ H Δw 是输出变化的二阶 Taylor 近似:
y = X @ w → Δy = X @ Δw → ‖Δy‖² = Δwᵀ (XᵀX) Δw = Δwᵀ H Δw
即一个权重的"重要性"正比于其所在方向上的 Hessian 对角元------Hessian 大的方向,权重扰动对输出影响大,应优先保精度。这是 OBS/OBQ 框架的核心洞察:权重量化误差应按其对输出的二阶影响加权,而非等权看待。
举例 :假设 H = XᵀX 在某个 channel 方向上的对角元是其他方向的 100×(说明该方向是激活的高方差方向,对应"重要 channel"),那么:
- MSE 视角:所有 channel 等权,amax 选择不考虑方向,可能让重要 channel 饱和
- Hessian 视角:该 channel 的权重扰动贡献 100× 于输出误差,amax 选择应优先保证该 channel 不饱和
具体实现:对每个候选 amax,计算 error = Δwᵀ H Δw(Δw = W - Q(W, amax)),选 argmin(error)。这等价于在 Hessian 度量下找最优 amax。
数值对比:对 NVFP4 Llama-3.1-8B,local_hessian 比 MSE 高约 1% MMLU(-0.5% to -1.5% vs -1.5% to -2.5%),代价是显存占用 3× vs 1.5×(需存 Hessian 矩阵)。
与 ModelOpt 实现的关系 :`_LocalHessianAccumulator`(file:///workspace/modelopt/torch/quantization/utils/calib_utils.py) 用前向 hook 收集 H = XᵀX,然后扫描候选 amax;CUDA 上由 nvfp4_fp8_scale_sweep_hessian Triton kernel 把 Hessian 计算 + 候选扫描 + 误差累积全融合(比 Python 回退快 50-100×)。local_hessian 只做"选 amax",不做 GPTQ 的"列向权重补偿"------后者代价更高但精度更优(详见第 7 节 GPTQ)。两者关系:local_hessian ≈ Hessian 加权的 MSE amax 搜索;GPTQ ≈ Hessian 加权的权重更新(不只改 amax,还改权重值)。
5. smoothquant 校准
核心文件 :`modelopt/torch/quantization/model_calib.py:smoothquant`(file:///workspace/modelopt/torch/quantization/model_calib.py)
5.1 原理
SmoothQuant 解决 INT8 量化中**激活值异常值(outlier)**问题。核心思想:用一个 per-channel 缩放因子 s 把"激活的难度"转移到"权重的容易":
y = X @ W
= (X / s) @ (s * W)
= X' @ W'
选择 s_j = max(|X|)^α / max(|W|)^(1-α) 使得 X' 与 W' 都易于量化。
5.2 实现
python
def smoothquant(model, forward_loop, alpha=0.5, ...):
# 1. 先 max 校准(per-channel input amax)
for module in model.modules():
if isinstance(module, QuantLinear):
module.input_quantizer.axis = -1 # per-channel
max_calibrate(model, forward_loop, ...)
# 2. 对每个 linear 计算 weight_scale 与 scale_a
for module in model.modules():
if isinstance(module, QuantLinear):
weight_scale = module.weight.abs().amax(dim=0)
act_amax = module.input_quantizer.amax # per-channel
scale_a = act_amax.pow(alpha) / weight_scale.pow(1 - alpha)
# 3. 折叠:weight *= 1/scale_a, input *= scale_a
apply_pre_quant_scale_and_smooth(module, scale_a)
# 把 1/scale_a 乘到 weight,scale_a 乘到 input_quantizer.pre_quant_scale
# 4. 重置激活 amax 为 per-tensor
module.input_quantizer.axis = None
module.input_quantizer.amax = act_amax.max()
5.3 与 Megatron 集成
SmoothQuant 对 TP(tensor parallel)特别敏感------column-parallel / row-parallel 模块的 scale_a 必须 cross-rank 同步:
python
# sync scale_a across TP group for column-parallel / row-parallel modules
if hasattr(module, "tp_group"):
dist.all_reduce(scale_a, op=dist.ReduceOp.MAX, group=module.tp_group)
5.4 论文来源与算法思想
- Xiao et al., 2022 --- "SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models"(arXiv:2211.10438)--- MIT + NVIDIA,ICML 2023。论文首次指出 LLM 激活的"系统性异常值"(systematic channel-wise outliers)是 INT8 量化的核心障碍,并提出 per-channel 平滑因子把"激活的难度"转移到"权重的容易"。
算法思想 :LLM 激活在少数固定 channel 上有持续的大幅值(如 6-10× 中位数),导致 per-tensor INT8 量化必须把整个 scale 拉到能容纳异常值,浪费主分布精度。SmoothQuant 的核心观察是:激活难量化但权重易量化 ,可以用一个 per-channel 缩放因子 s 把"激活的难度"转移到"权重的容易":
y = X @ W = (X / s) @ (s · W) = X' @ W'
选 s_j = max(|X_j|)^α / max(|W_j|)^(1-α),其中 α ∈ 0,1 控制转移强度:
- α=0:不平滑(等价 max 校准)
- α=0.5(论文推荐):X' 与 W' 都适中,两者都易量化
- α=1:完全转移到权重(极端情况,权重可能反被拖累)
举例 :假设某 linear 的输入 X 在 channel 5 上的 amax 是其他 channel 的 10×(异常值),权重 W 在 channel 5 上的 amax 与其他 channel 接近:
| 方案 | X channel 5 amax | 其他 channel X 精度 | W channel 5 amax | 结果 |
|---|---|---|---|---|
| 直接 INT8 | 10× | 差(被 channel 5 拖累) | 1× | 主分布精度损失 |
| SmoothQuant (α=0.5) | 10/√10 ≈ 3.16× | 好(提升 3.16×) | 1×√10 ≈ 3.16× | X 与 W 都能干净量化 |
具体:s_5 = sqrt(10) ≈ 3.16,把 X 在 channel 5 上除以 3.16(异常值从 10× 降到 3.16×),W 在 channel 5 上乘以 3.16(W channel 5 amax 变为 3.16×,但 W 本身易量化,仍能干净量化为 INT8)。
与 ModelOpt 实现的关系 :`smoothquant`(file:///workspace/modelopt/torch/quantization/model_calib.py) 默认 α=0.5,先用 per-channel max 校准得到 act_amax,再算 scale_a = act_amax^α / weight_scale^(1-α),最后通过 apply_pre_quant_scale_and_smooth 把 1/scale_a 折叠到 weight、scale_a 折叠到 input_quantizer.pre_quant_scale。TP 场景下 scale_a 必须 cross-rank 同步(column-parallel 与 row-parallel 模块),否则不同 rank 的 scale 不一致会破坏 TP 数值等价性。
与 AWQ 的关系 :SmoothQuant 与 AWQ lite 的 scale 公式形式相同(都是 s = max(|X|)^α / max(|W|)^(1-α)),但目的不同:
- SmoothQuant:α=0.5 固定,激活也量化(INT8),目的是让激活可量化
- AWQ lite:α 网格搜索 0,1,激活不量化(W4A16)或保留高精度(W4A8),目的是让权重量化误差最小
6. AWQ 系列:awq_lite / awq_clip / awq_full
核心文件 :`modelopt/torch/quantization/model_calib.py:awq`(file:///workspace/modelopt/torch/quantization/model_calib.py)
6.1 awq_lite --- 通道缩放搜索
python
def awq_lite(model, forward_loop, alpha_step=0.1, ...):
"""搜索 per-channel 缩放因子 α ∈ [0,1],最小化输出重建误差。"""
for module in model.modules():
if isinstance(module, QuantLinear):
# 1. 收集输入 X
X = collect_inputs(module, forward_loop)
# 2. 搜索 α(步长 alpha_step)
best_alpha = None
best_err = inf
for alpha in frange(0, 1, alpha_step):
s = compute_scale(module, X, alpha) # s = max(|X|)^α / max(|W|)^(1-α)
# 把 1/s 乘到 weight,s 乘到 input
with apply_pre_quant_scale_temporarily(module, s):
# 量化 weight
quant_weight = module.weight_quantizer._fake_quantize(module.weight)
# 计算输出重建误差
err = ((X @ quant_weight - X @ module.weight) ** 2).sum()
if err < best_err:
best_err = err
best_alpha = alpha
# 应用最佳 α
apply_pre_quant_scale_and_smooth(module, compute_scale(module, X, best_alpha))
6.2 awq_clip --- 块级 amax 裁剪
python
def awq_clip(model, forward_loop,
max_co_batch_size=1024, max_tokens_per_batch=64,
min_clip_ratio=0.5, shrink_step=0.05, ...):
"""搜索 per-block 裁剪 amax(block 级而非 channel 级)。"""
for module in model.modules():
if isinstance(module, QuantLinear):
# 1. 获取当前 amax(per-block)
current_amax = module.weight_quantizer.amax
# 2. 沿 out_features 分批处理(避免 OOM)
for batch in batched_iter(module.weight, max_co_batch_size):
# 3. 对每个块,搜索 [orig * min_clip_ratio, orig] 范围内的 amax
for clip_ratio in frange(1.0, min_clip_ratio, -shrink_step):
candidate_amax = current_amax * clip_ratio
# 4. 量化并计算重建误差
with set_amax_temporarily(...):
err = compute_reconstruction_error(...)
# 5. 选最佳 clip_ratio per block
...
6.3 awq_full --- lite + clip 组合
python
def awq(model, forward_loop, ...):
"""awq_full:先 lite 再 clip。"""
# SequentialQuantizer.convert_to_single_quantizer 上下文
# (W4A8 配置临时折叠为单 quantizer 进行 AWQ 搜索)
with SequentialQuantizer.convert_to_single_quantizer(model):
awq_lite(model, forward_loop, ...)
awq_clip(model, forward_loop, ...)
awq_full 是 INT4 AWQ 量化的推荐算法 ------lite 提供通道级缩放平滑,clip 提供块级裁剪优化,组合后精度通常比单独 awq_lite 高 1-3%。
6.4 论文来源与算法思想
- Lin et al., 2023 --- "AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration" (arXiv:2306.00978)--- MIT,MLSys 2024 Best Paper。论文核心发现:只有 0.1%-1% 的"显著权重"(salient weights)对模型精度至关重要,而这些显著权重可通过激活分布识别(而非 weight 自身大小)。
算法思想 :AWQ 的关键洞察是------量化误差不应均匀分摊到所有权重,而应保护"激活值大的 channel 所对应的权重"。但直接保留 FP16 显著权重会破坏矩阵运算的统一性,所以 AWQ 用 per-channel 缩放因子 s 等价地保护显著权重:
y = X @ W = (X · s) @ (W / s)
把权重在显著 channel 上除以 s(让其值变小 → 量化误差更小),把激活乘以 s(激活本就难量化,但保留 FP16 即可)。最优 s 通过网格搜索 α 找到:s_j = max(|X_j|)^α / max(|W_j|)^(1-α),选使输出重建误差最小的 α。这与 SmoothQuant 形式相同但目的不同------SmoothQuant 是为了让激活可量化到 INT8,AWQ 是为了让权重可量化到 INT4。
为什么"激活大 → 对应权重重要" :输出 y = X @ W,权重 W_j 的量化误差 ΔW_j 对输出的贡献是 X_j · ΔW_j。如果 X_j 大(激活异常值),即使 ΔW_j 小,输出误差也大。所以保护 X_j 大的 channel 的权重(让 ΔW_j 更小)能显著降低输出误差。AWQ 通过把 W_j 除以 s(s>1),让 W_j 变小 → INT4 量化绝对误差 ΔW_j 变小 → X_j · ΔW_j 变小。
AWQ 三件套对应 ModelOpt 实现:
| AWQ 论文组件 | ModelOpt 算法 | 作用 |
|---|---|---|
Salient weight identification via s grid search |
awq_lite |
per-channel 缩放搜索 |
| Per-block clipping | awq_clip |
块级 amax 裁剪 |
| 组合 lite + clip | awq_full |
论文推荐的完整流程 |
举例:INT4 量化某 linear,channel 5 上激活 amax 是其他 channel 的 8×:
| 方案 | W channel 5 量化误差 | 输出误差(被 channel 5 主导) | 结果 |
|---|---|---|---|
| 直接 RTN(round-to-nearest) | 1× | 8×(X_5 × ΔW_5 = 8 × 1) | 输出误差大 |
| AWQ lite(α=0.5,s_5=√8≈2.83) | 1/8(W_5/s_5 后值变小,INT4 绝对误差降为 1/s_5²≈1/8) | 1×(X_5×s_5 × ΔW_5/s_5 = 8×2.83 × 1/8 ≈ 2.83,但相对总输出仍小) | 输出误差显著降低 |
与 ModelOpt 实现的关系 :`awq_lite`(file:///workspace/modelopt/torch/quantization/model_calib.py) 在 α ∈ 0,1 上以 alpha_step=0.1 网格搜索,对每个 α 算 s、临时折叠到 weight、量化 weight、计算输出重建误差,选最佳 α。`awq_clip`(file:///workspace/modelopt/torch/quantization/model_calib.py) 在 [min_clip_ratio × current_amax, current_amax] 范围内以 shrink_step=0.05 搜索 per-block 最优 amax。awq_full 在 SequentialQuantizer.convert_to_single_quantizer 上下文中先 lite 再 clip(W4A8 配置临时折叠为单 quantizer 进行搜索)。
与 SmoothQuant 的关键区别:SmoothQuant 与 AWQ lite 的 scale 公式形式相同,但适用场景不同------SmoothQuant 用于 INT8 W8A8(激活也量化),AWQ 用于 INT4 W4A16/W4A8(激活不量化或高精度)。ModelOpt 的 NVFP4 配置也可用 AWQ lite(因为 NVFP4 是 W4A4,激活也量化但用 NVFP4 格式)。
7. gptq 校准
核心文件 :`modelopt/torch/quantization/model_calib.py:gptq`(file:///workspace/modelopt/torch/quantization/model_calib.py) 与 `utils/calib_utils.py:GPTQHelper`(file:///workspace/modelopt/torch/quantization/utils/calib_utils.py)
7.1 原理
GPTQ 是基于二阶信息的权重更新算法:逐列更新 weight,使得量化后的输出变化最小。算法步骤:
- 计算 Hessian
H = XᵀX(X 是输入) - 阻尼:
H += λ * mean(diag(H)) * I(保证可逆) - 计算
H_inv = H⁻¹ - 对 weight 的每一列
w:- 量化误差
e = quantize(w) - w - 更新
w -= H_inv[:, i] * e[i] / H_inv[i, i] - 把误差"分摊"到后续列
- 量化误差
7.2 实现
python
def gptq(model, forward_loop, perc_damp=0.01, block_size=128, fused=False, ...):
# 1. max 校准初始化
max_calibrate(model, forward_loop, ...)
# 2. 提升 NVFP4 静态 quantizer
promote_nvfp4_static_quantizers(model)
# 3. 禁用 weight quantizer,运行 forward_loop 收集 Hessian
for module in model.modules():
if isinstance(module, QuantLinear):
module.weight_quantizer.disable_quant()
forward_loop(model) # 收集每个 linear 的 H = XᵀX
# 4. 对每个 linear 进行 GPTQ 更新
for module in model.modules():
if isinstance(module, QuantLinear):
# 创建 GPTQHelper(或自定义 backend helper)
helper = GPTQHelper(module.weight.shape, module.weight_quantizer, ...)
# 5. 列向 GPTQ 更新(按 block_size 分块)
if fused:
# Triton fused kernel
helper.update_weights_fused(block_size, perc_damp)
else:
helper.update_weights(block_size, perc_damp)
# 6. 重新量化更新后的 weight
with module.weight_quantizer.enable_quant():
quant_weight = module.weight_quantizer._fake_quantize(module.weight)
7.3 layerwise 选项(与 layerwise_calibrate 组合)
GPTQ 默认开启 layerwise.get_qdq_activations_from_prev_layer=True:
python
# 在 GPTQCalibConfig 中
class GPTQCalibConfig(QuantizeAlgorithmConfig):
perc_damp: float = 0.01
block_size: int = 128
fused: bool = False
# 强制 layerwise.get_qdq_activations_from_prev_layer=True
# (除非用户显式设置)
这意味着 GPTQ 在 layerwise 模式下,当前层校准后会把 QDQ 误差传播到下一层------这是 GPTQ 精度优势的关键。其他算法(max/mse)默认 False,即下一层校准时仍看到原始 FP 激活。
7.4 GPTQHelper 注册机制
python
_GPTQ_HELPER_REGISTRY = {} # 自定义 backend → GPTQHelper 子类
def register_gptq_helper(backend_name, helper_cls):
_GPTQ_HELPER_REGISTRY[backend_name] = helper_cls
不同 backend(如 TransformerEngine 的 fused FP8 GEMM)可注册自己的 GPTQHelper 子类,复用 Hessian 收集逻辑但替换权重更新 kernel。
7.5 论文来源与算法思想
- Frantar et al., 2023 --- "GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers"(ICLR 2023,arXiv:2210.17323)--- IST Austria + ETH Zurich + Neural Magic。论文把 OBQ 简化为列向批量更新,首次实现 175B 参数模型在 4 GPU 小时内量化到 3-4 bit,精度近乎无损。
- Frantar et al., 2022 --- "Optimal Brain Compression: A Framework for Accurate Post-Training Quantization and Pruning" (NeurIPS 2022,arXiv:2207.09709)--- 提出 OBQ/OBC 框架,GPTQ 的直接前身,基于经典 Optimal Brain Surgeon (OBS)(LeCun et al., 1990)的二阶信息理论。
算法思想 :RTN(round-to-nearest)量化逐权重独立舍入,忽略量化误差的累积。GPTQ 用 二阶 Hessian 信息 在量化一个权重后,把误差补偿到尚未量化的后续权重:
- 计算 Hessian
H = XᵀX + λ·diag(H)·I(λ 阻尼保证可逆) - 求
H⁻¹(用 Cholesky 分解保证数值稳定) - 逐列(或 block_size 列分批)量化:
- 当前列
w量化误差e = Q(w) - w - 把
e按H⁻¹比例分摊到后续列:W_remaining -= (H⁻¹[:, i] / H⁻¹[i, i]) · e[i]
- 当前列
- 量化下一列,重复
直觉:如果一个权重量化后偏大,GPTQ 会调小后续相关权重来抵消,使整体输出误差最小化,而非单个权重的舍入误差最小化。这是 OBS/OBD(Optimal Brain Damage)思想在量化场景的延伸------量化不是独立的舍入,而是带补偿的协同更新。
举例 :2 列权重 W = [w1, w2],假设 Hessian 表明 w1 和 w2 强相关(H⁻¹ 非对角元大):
| 方案 | w1 处理 | w2 处理 | 输出 X @ Q(W) 误差 |
|---|---|---|---|
| RTN | Q(w1) = round(w1),误差 e1 |
Q(w2) = round(w2),独立误差 |
e1 + e2(误差累积) |
| GPTQ | Q(w1) = round(w1),误差 e1>0(偏大) |
w2 -= H⁻¹[1,0]/H⁻¹[0,0] · e1(调小抵消),再 Q(w2) |
e1 - 部分被 w2 抵消(误差显著降低) |
GPTQ 的 w1 正向误差被 w2 的负向调整部分抵消,输出 X @ Q(W) 误差更小。
关键工程优化(论文贡献,让 GPTQ 可扩展到百亿参数):
- Cholesky 分解
H⁻¹,避免直接求逆的数值不稳定(直接求逆在近奇异 Gram 矩阵上会爆) - 批量列处理(block_size=128),用单次矩阵运算更新 128 列的补偿,避免逐列 Python 循环
- lazy batch update:在量化 batch 内累积更新,batch 间才同步权重,减少显存读写
与 ModelOpt 实现的关系 :`GPTQHelper`(file:///workspace/modelopt/torch/quantization/utils/calib_utils.py) 实现 update_weights(Python 回退)与 update_weights_fused(Triton fused kernel,block_size 列向更新一次完成)。gptq_calibrate 默认开启 layerwise.get_qdq_activations_from_prev_layer=True------这是 GPTQ 精度的关键:当前层量化误差传播到下一层校准的 Hessian 中,让后续层"知道"前层的误差,整体精度比 max/mse 高 1-2%。_GPTQ_HELPER_REGISTRY 允许自定义 backend(如 TransformerEngine 的 fused FP8 GEMM)注册专用 helper。
与 local_hessian 的关键区别 :两者都用 Hessian H = XᵀX,但:
- local_hessian:只搜索 amax(量化 scale),权重值本身不改。Hessian 用于加权评估 amax 候选的误差。
- GPTQ :不改 amax(用 max 初始化的 amax),但改权重值------通过 Hessian 加权的列向补偿,把量化误差分摊到后续权重,使整体输出误差最小。
GPTQ 精度通常比 local_hessian 高 0.5-1%(-0.3% to -1.0% vs -0.5% to -1.5% MMLU),代价是计算量更高(4× 校准时间)。
8. svdquant 校准
核心文件 :`modelopt/torch/quantization/model_calib.py:svdquant`(file:///workspace/modelopt/torch/quantization/model_calib.py)
8.1 原理
SVDQuant 把 weight 分解为两部分:
W = SVD_lowrank(W, rank=lowrank) + residual
- 低秩部分
SVD_lowrank(W)用 FP16/BF16(精度高) - 残差
residual用 NVFP4 量化(压缩)
配合 LoRA 训练微调低秩部分。
8.2 实现
python
def svdquant(model, forward_loop, lowrank=32, ...):
# 1. max 校准初始化
max_calibrate(model, forward_loop, ...)
# 2. 对每个 linear 分解 weight
for module in model.modules():
if isinstance(module, QuantLinear):
W = module.weight.data
# SVD 分解
U, S, V = torch.svd(W)
# 取前 lowrank 维
lowrank_W = U[:, :lowrank] @ torch.diag(S[:lowrank]) @ V[:, :lowrank].T
residual = W - lowrank_W
# 3. 残差用 NVFP4 量化
module.weight.data = residual
module.weight_quantizer._fake_quantize(module.weight)
# 4. 低秩部分用 LoRA adapter 表示
# (参数 lora_a = U[:, :lowrank] @ torch.diag(S[:lowrank]),
# 参数 lora_b = V[:, :lowrank].T)
lora_adapter = LoRAAdapter(lora_a, lora_b, ...)
attach_lora_to(module, lora_adapter)
8.3 与 PEFT 子系统集成
SVDQuant 是 PEFT + 量化组合的典型应用------见 05 篇 的 QLoRA 章节。低秩部分作为 LoRA adapter 训练,可后续 fine-tune 进一步恢复精度。
8.4 论文来源与算法思想
- Li et al., 2024 --- "SVDQuant: Absorbing Outliers by Low-Rank Components for 4-Bit Diffusion Models"(arXiv:2411.05007)--- MIT + NVIDIA,ICLR 2025。论文为扩散模型(diffusion models)的 NVFP4 量化提出 SVD 分解 + LoRA 补偿方案,把"低秩部分吸收异常值"的思想首次系统应用到 4-bit LLM/Diffusion 量化,并通过 Nunchaku 推理引擎实现低秩分支与量化分支的融合 kernel。
算法思想 :极低比特(4-bit)量化的核心难点是异常值------少数大幅值权重让整层 scale 被拉大,主分布精度损失严重。SVDQuant 把权重矩阵分解为两部分:
W = W_lowrank + W_residual
= U_r Σ_r V_rᵀ + (W - U_r Σ_r V_rᵀ)
- 低秩部分
W_lowrank = U_r Σ_r V_rᵀ(rank r,如 32):用 FP16/BF16 存储,吸收大幅值方向(SVD 的大奇异值方向对应 weight 的主能量方向,往往是异常值方向) - 残差
W_residual:用 NVFP4 量化,因为大幅值已被低秩部分吸收,残差分布更平滑、易量化
推理时:y = X @ W = X @ W_lowrank + X @ W_residual。低秩部分用小 GEMM(X @ U_r → diag(Σ_r) → @ V_rᵀ),残差用 NVFP4 GEMM,两者相加。论文的关键工程贡献是 Nunchaku engine------把低秩 GEMM 与 NVFP4 GEMM 融合为单次 kernel,避免两次访存开销,使 SVDQuant 推理速度接近纯 NVFP4。
举例:某 linear 的 W (4096×4096),SVD 后前 32 个奇异值占能量 80%(其余 4064 个占 20%):
| 方案 | 主分布精度 | 权重存储 | 推理速度 |
|---|---|---|---|
| 直接 NVFP4 | 差(scale 必须容纳 top 奇异值方向的大幅值,主分布只剩约 4-bit 精度) | 0.25× | 快 |
| SVDQuant (rank=32) | 好(top 32 方向用 FP16,残差分布平,NVFP4 量化误差降低 2-4×) | 0.31×(+6.25% 低秩) | 接近 NVFP4(Nunchaku 融合) |
具体:W_lowrank (rank 32) 用 FP16 存储,参数量 2×32×4096 = 256K(仅 6.25% 增量);W_residual 的能量分布更平(top 32 方向已剥离),NVFP4 量化误差降低 2-4×。总精度接近 FP8,但权重存储接近纯 NVFP4。
与 QLoRA 的关系:QLoRA(Dettmers et al., 2023,arXiv:2305.14314)也用 4-bit 量化 + LoRA 微调,但 QLoRA 是"先量化再训 LoRA",LoRA 在量化权重上做微调;SVDQuant 是"分解后量化残差,低秩部分本身就是 LoRA",低秩部分与量化残差在推理时共同起作用。两者形式相似但 SVDQuant 的低秩部分是"吸收异常值"的必要组件,非可选微调。
与 ModelOpt 实现的关系 :`svdquant`(file:///workspace/modelopt/torch/quantization/model_calib.py) 流程是 max 初始化 → 对每个 linear 做 torch.svd → 取前 lowrank 维做 lowrank_W → residual 量化为 NVFP4 → 低秩部分用 LoRA adapter(lora_a = U_r Σ_r,lora_b = V_rᵀ)。LoRA adapter 可后续 fine-tune 进一步恢复精度,与 PEFT 子系统天然集成。SVDQuant 是 ModelOpt 中精度最高的 NVFP4 方案(-0.1% to -0.5% vs FP16),但代价是低秩 GEMM 的额外计算开销与训练成本(LoRA 微调)。
9. layerwise_calibrate:分块校准与断点续算
核心文件 :`modelopt/torch/quantization/model_calib.py:layerwise_calibrate`(file:///workspace/modelopt/torch/quantization/model_calib.py) 与 `utils/layerwise_calib.py:LayerActivationCollector`(file:///workspace/modelopt/torch/quantization/utils/layerwise_calib.py)
9.1 动机
大模型(>30B)显存不足以一次校准全部层。layerwise_calibrate 按 decoder layer 分块,每次只校准一层:
- 节省显存:只持有当前层的 Hessian / 激活
- 支持断点续算:崩溃后从中断层继续
- 可选误差传播:
get_qdq_activations_from_prev_layer控制是否把 QDQ 误差传到下一层
9.2 实现
python
def layerwise_calibrate(model, calib_func, forward_loop, ...):
num_layers = count_decoder_layers(model)
# 1. 加载断点(如有)
checkpoint_state = _CheckpointState.from_folder(checkpoint_dir, num_layers, save_every=...)
# 2. 创建 LayerActivationCollector
collector = LayerActivationCollector(model, ...)
# 3. 逐层校准
for layer_idx in range(num_layers):
if checkpoint_state.is_done(layer_idx):
continue # 跳过已完成
# 4. patch 当前层为 "capture" 模式
with collector.patch_layer(layer_idx):
if get_qdq_activations_from_prev_layer:
# 捕获下一层输入(在当前层校准后)
# 让 QDQ 误差传播
...
else:
# 捕获下一层输入(在当前层校准前)
# 下一层看到原始 FP 激活
...
# 5. 在当前层运行 calib_func
calib_func(model, forward_loop=forward_loop, **kwargs)
# 6. 保存 checkpoint
if (layer_idx + 1) % save_every == 0:
checkpoint_state.save(layer_idx + 1)
# 7. 清理 patch
collector.cleanup()
9.3 LayerActivationCollector 三态 patch
python
class LayerActivationCollector:
"""每个 decoder layer 有三种状态:skip / run / capture。"""
def patch_layer(self, target_idx):
# 把 target_idx 层标记为 "run"(实际校准)
# 把 target_idx - 1 层(如存在)标记为 "capture"(捕获下一层输入)
# 把其他层标记为 "skip"(不调用 forward)
...
9.4 断点续算
python
class _CheckpointState:
@classmethod
def from_folder(cls, folder, num_layers, save_every=10):
# 扫描 folder 找最新的 checkpoint_<idx>.pt
# 返回已完成层列表
...
def save(self, layer_idx):
# 保存 {layer_idx, amaxes, ...} 到 checkpoint_{layer_idx}.pt
...
10. AutoQuantize 自动搜索
核心文件 :`modelopt/torch/quantization/algorithms.py`(file:///workspace/modelopt/torch/quantization/algorithms.py) 与 `_auto_quantize_cost.py`(file:///workspace/modelopt/torch/quantization/_auto_quantize_cost.py)
10.1 问题定义
给定候选量化格式集 recipes = [NVFP4_AWQ_LITE, FP8_DEFAULT, INT4_AWQ, None],为模型每个"模块组"选一个 recipe,使得:
- 约束 :总压缩后参数量 ≤
effective_bits/16 × total_weight_size(如 4.8/16 ≈ 30%) - 目标:最小化总敏感度(量化对输出的扰动)
10.2 模块分组(quant_grouping_rules)
python
class _AutoQuantizeBaseSearcher(BaseSearcher, ABC):
quant_grouping_rules = [
# regex list:第一捕获组是 group key
r".*\.(q_proj|k_proj|v_proj|query|key|value)", # Q/K/V 一组
r".*\.(gate_proj|up_proj|w1|fc1|gate_up_proj)", # gate/up 一组
r".*\.(down_proj|w2|fc2)", # down 一组
r".*\.(o_proj|dense|c_proj)", # o 一组
# MoE:每个 expert 的相同 linear 一组
r".*experts\.(\d+)\.(w1|gate_proj|gate_up_proj)",
r".*experts\.(\d+)\.(w3|up_proj)",
r".*experts\.(\d+)\.(w2|down_proj)",
]
10.3 QuantRecipe 包装
python
class QuantRecipe(CustomHPType):
"""包装 QuantizeConfig + 名字 + 预计算的 compression ratio。"""
def __init__(self, quant_cfg, name="custom"):
# 总是追加 {"quantizer_name": "*output_quantizer", "enable": False}
# (KV-cache 在搜索时禁用,保持 compression 单调)
self.quant_cfg = augmented_quant_cfg
self.name = name
self.compression = self._estimate_compression() # min(num_bits/16)
@classmethod
def no_quantization(cls):
return cls(quant_cfg=None) # 自动加到所有 QuantRecipeHparam 的 choices
10.4 QuantRecipeHparam 可搜索 hparam
python
class QuantRecipeHparam(Hparam):
"""每个模块组对应一个 QuantRecipeHparam。"""
def __init__(self, quant_module, choices):
# 为每个 recipe 预创建一组 TensorQuantizer 并应用 quant_cfg
# active.setter 切换 quantizer 引用
...
def get_score(self, recipe) -> float:
"""敏感度(Fisher 信息 Taylor 近似)。"""
return sum_per_score_module(
_get_auto_quantize_score(grad_output, output_diff)
)
def get_cost(self, recipe) -> float:
"""压缩成本:sum(weight_size × recipe.compression)。"""
return sum(weight_size * recipe.compression for ...)
10.5 AutoQuantizeGradientSearcher 流程
python
class AutoQuantizeGradientSearcher(_AutoQuantizeBaseSearcher):
method_name = "gradient"
def estimate_sensitivity_scores(self):
# 对每个 QuantRecipeHparam 的每个 recipe:
# 1. active = recipe(启用该 recipe 的 quantizer)
# 2. 设 quantizer 为 fake_quant 模式
# 3. 运行 forward_backward_step(loss.backward())
# 4. 在 score_module 上注册 forward hook 记录 output_diff
# 5. 在 score_module 上注册 backward hook 记录 grad_output
# 6. score = (grad_output * output_diff).square().sum()
# (Fisher 信息一阶 Taylor 近似)
def run_search_with_stats(self, max_weight_size, verbose):
# 用 LPS 求解 LP 问题:
# minimize sum(score[recipe])
# subject to sum(cost[recipe]) <= effective_bits/16 × total_weight_size
# 在多个递减 lower_bounds 上重解直到可行
lower_bounds = _get_search_lower_bounds() # [None, 0.99, 0.90]
for lb in lower_bounds:
solution = LPS.solve(scores, costs, budget, lower_bound=lb)
if solution.feasible:
return solution
10.6 AutoQuantizeKLDivSearcher 替代路径
python
class AutoQuantizeKLDivSearcher(_AutoQuantizeBaseSearcher):
method_name = "kl_div"
def estimate_sensitivity_scores(self):
# 用 KL 散度代替 Fisher 信息
# score = KL(unquantized_output_logprob || quantized_output_logprob)
# 用 _get_lm_head(model) 获取 lm_head
# 用 _get_kl_div_loss 计算 KL 散度
...
def run_search_with_stats(self, ...):
# 用 threshold-based binary search 代替 LP
...
10.7 成本模型(_auto_quantize_cost.py)
python
class AutoQuantizeCostModel(ABC):
"""成本模型抽象基类。"""
name: str
supported_cost_keys: set[str]
class WeightCostModel(AutoQuantizeCostModel):
"""所有 quantizable weight 等权计算。"""
name = "weight"
supported_cost_keys = {"weight"}
class ActiveMoECostModel(AutoQuantizeCostModel):
"""MoE 模型:routed expert 权重按 active_ratio 折算。"""
name = "active_moe"
supported_cost_keys = {"weight", "active_moe"}
def infer_active_moe_expert_ratio(model):
"""从 model.config 读取 num_experts_per_tok / num_experts。"""
# 如 8 个 expert 中 top-2 → ratio = 0.25
# expert 权重对 effective_bits 的贡献 = 0.25 ×
配图 3:AutoQuantize 搜索架构
#mermaid-svg-Os3Vr0FVwCERIeN2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Os3Vr0FVwCERIeN2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Os3Vr0FVwCERIeN2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Os3Vr0FVwCERIeN2 .error-icon{fill:#552222;}#mermaid-svg-Os3Vr0FVwCERIeN2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Os3Vr0FVwCERIeN2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Os3Vr0FVwCERIeN2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Os3Vr0FVwCERIeN2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Os3Vr0FVwCERIeN2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Os3Vr0FVwCERIeN2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Os3Vr0FVwCERIeN2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Os3Vr0FVwCERIeN2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Os3Vr0FVwCERIeN2 .marker.cross{stroke:#333333;}#mermaid-svg-Os3Vr0FVwCERIeN2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Os3Vr0FVwCERIeN2 p{margin:0;}#mermaid-svg-Os3Vr0FVwCERIeN2 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Os3Vr0FVwCERIeN2 .cluster-label text{fill:#333;}#mermaid-svg-Os3Vr0FVwCERIeN2 .cluster-label span{color:#333;}#mermaid-svg-Os3Vr0FVwCERIeN2 .cluster-label span p{background-color:transparent;}#mermaid-svg-Os3Vr0FVwCERIeN2 .label text,#mermaid-svg-Os3Vr0FVwCERIeN2 span{fill:#333;color:#333;}#mermaid-svg-Os3Vr0FVwCERIeN2 .node rect,#mermaid-svg-Os3Vr0FVwCERIeN2 .node circle,#mermaid-svg-Os3Vr0FVwCERIeN2 .node ellipse,#mermaid-svg-Os3Vr0FVwCERIeN2 .node polygon,#mermaid-svg-Os3Vr0FVwCERIeN2 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Os3Vr0FVwCERIeN2 .rough-node .label text,#mermaid-svg-Os3Vr0FVwCERIeN2 .node .label text,#mermaid-svg-Os3Vr0FVwCERIeN2 .image-shape .label,#mermaid-svg-Os3Vr0FVwCERIeN2 .icon-shape .label{text-anchor:middle;}#mermaid-svg-Os3Vr0FVwCERIeN2 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Os3Vr0FVwCERIeN2 .rough-node .label,#mermaid-svg-Os3Vr0FVwCERIeN2 .node .label,#mermaid-svg-Os3Vr0FVwCERIeN2 .image-shape .label,#mermaid-svg-Os3Vr0FVwCERIeN2 .icon-shape .label{text-align:center;}#mermaid-svg-Os3Vr0FVwCERIeN2 .node.clickable{cursor:pointer;}#mermaid-svg-Os3Vr0FVwCERIeN2 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Os3Vr0FVwCERIeN2 .arrowheadPath{fill:#333333;}#mermaid-svg-Os3Vr0FVwCERIeN2 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Os3Vr0FVwCERIeN2 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Os3Vr0FVwCERIeN2 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Os3Vr0FVwCERIeN2 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Os3Vr0FVwCERIeN2 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Os3Vr0FVwCERIeN2 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Os3Vr0FVwCERIeN2 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Os3Vr0FVwCERIeN2 .cluster text{fill:#333;}#mermaid-svg-Os3Vr0FVwCERIeN2 .cluster span{color:#333;}#mermaid-svg-Os3Vr0FVwCERIeN2 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Os3Vr0FVwCERIeN2 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Os3Vr0FVwCERIeN2 rect.text{fill:none;stroke-width:0;}#mermaid-svg-Os3Vr0FVwCERIeN2 .icon-shape,#mermaid-svg-Os3Vr0FVwCERIeN2 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Os3Vr0FVwCERIeN2 .icon-shape p,#mermaid-svg-Os3Vr0FVwCERIeN2 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Os3Vr0FVwCERIeN2 .icon-shape .label rect,#mermaid-svg-Os3Vr0FVwCERIeN2 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Os3Vr0FVwCERIeN2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Os3Vr0FVwCERIeN2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Os3Vr0FVwCERIeN2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
是
否
是
否
是
否
用户调用 mtq.auto_quantize
disable 所有 quantizer
构建搜索空间
QuantRecipeHparam per 模块组
对每个 hparam 的每个 recipe
- 设 active = recipe
- 启用 fake_quant
- forward_backward_step
- forward hook 记录 output_diff
backward hook 记录 grad_output
5. score = grad·diff squared sum
Fisher 信息 Taylor 近似
所有 recipe 处理完?
收集所有 scores
用 LPS 求 LP 问题
minimize sum_score
s.t. sum_cost ≤ effective_bits/16 × total
lower_bound=None
可行?
返回最优解
lower_bound=0.99
可行?
lower_bound=0.90
可行?
报错:约束不可行
应用最优 recipe 到模型
fold_pqs_to_weights
折叠 SmoothQuant pre_quant_scale
返回 search_state
可选:get_auto_quantize_config
用不同 effective_bits 重解
不需 GPU
说明 :AutoQuantize 的搜索流程是------(1) 禁用所有 quantizer;(2) 把模型按 quant_grouping_rules 分组,每组创建一个 QuantRecipeHparam;(3) 对每个 hparam × 每个 recipe,设为 active 并运行 forward_backward_step,在 score_module 上注册双向 hook 收集 Fisher 信息;(4) 用 LPS 求解 LP:在 effective_bits 约束下最小化总敏感度,多个递减下界重解直到可行;(5) 应用最优 recipe 并折叠 pre_quant_scale。get_auto_quantize_config 可在搜索后用不同 effective_bits 重解(不需 GPU),用于权衡探索。
10.8 register_custom_support --- 框架集成
python
def register_custom_support(is_supported_checker, grad_ckpt_context, is_param_grad_enabled):
"""让模型框架(如 HF、Megatron)支持 AutoQuantize 的特殊需求。"""
# is_supported_checker:判断模块是否支持 gradient
# grad_ckpt_context:gradient checkpointing 上下文(节省显存)
# is_param_grad_enabled:选择性启用 param.grad
HuggingFace、Megatron、Transformers 等框架通过此 API 集成 AutoQuantize。
10.9 论文来源与算法思想
AutoQuantize 的核心是混合精度敏感度估计 + LP 求解,理论源自以下工作:
- Dong et al., 2019 --- "HAWQ: Hessian AWare Quantization"(NeurIPS 2019,arXiv:1905.03696)--- 首次提出用 Hessian 信息(Frobenius 范数 / trace)衡量层敏感度,指导混合精度选择,是 Hessian-based 敏感度的开创性工作。
- Yao et al., 2021 --- "HAWQ-V3: Dyadic Neural Network Quantization" (ICML 2021,arXiv:2011.10680)--- 把 HAWQ 的敏感度估计与混合精度 Integer Linear Programming (ILP) 求解结合,是 AutoQuantize LP 求解的直接先驱。论文目标函数
Ω = Σ_i Tr(H_i) · ‖Q(W_i) - W_i‖²正是 Hessian 加权的量化扰动度量。 - Dong et al., 2020 --- "HAWQ-V2: Hessian AWare trace-Weighted Quantization"(NeurIPS 2020)--- 改进敏感度度量,用二阶信息更精确地评估每层量化影响。
- Dettmers et al., 2023 --- "SpQR: A Sparse-Quantized Representation for Near-Lossless LLM Weight Compression" (arXiv:2306.03078)--- 用 OBS 逆 Hessian 敏感度
s_ij = (w_ij - Q(w_ij))² / (2 · [(XᵀX)⁻¹]_jj)识别 LLM 中的敏感权重,单独保留高精度(混合精度思想应用到权重级)。 - LeCun et al., 1990 --- "Optimal Brain Damage"(NeurIPS 1989)--- OBS/OBD 系列的奠基工作,首次提出用二阶信息(Hessian)衡量参数重要性,是 AutoQuantize Fisher 信息度量的理论源头。
算法思想(Fisher 信息 Taylor 近似):量化对输出的扰动可用一阶 Taylor 展开:
ΔL ≈ ∇_y L · Δy + ½ Δyᵀ H_L Δy + ...
其中 H_L 是 loss 对输出的 Hessian。在量化敏感度估计中,常用 Fisher 信息矩阵 F = E[∇_y L ∇_y Lᵀ] 代替 Hessian(Fisher 等价于期望 Hessian,但只需一阶梯度即可估计)。ModelOpt 进一步简化为:
score = (grad_output · output_diff)²
其中 output_diff = y_quant - y_fp,grad_output = ∇_y L。这是 Fisher 信息的一阶 Taylor 近似:(∇_y L · Δy)² ≈ Δyᵀ F Δy,等价于量化扰动在 Fisher 度量下的二范数。直觉:如果一个量化 recipe 让输出变化大(Δy 大),且这个变化方向是 loss 高梯度方向(∇_y L 大),则该 recipe 敏感度高(应避免)。
举例 :模型有两个模块组 G1(attention QKV)和 G2(MLP),候选 recipe 有 FP8(compression=0.5)和 NVFP4(compression=0.25),预算 effective_bits=4.8(即总压缩 ≤ 0.3):
| 配置 | G1 score | G2 score | 总 cost | 总 score | 可行? |
|---|---|---|---|---|---|
| 全 FP8 | 0.2 | 0.5 | 0.5×total | 0.7 | 否(>0.3×total) |
| 全 NVFP4 | 0.8(attention 敏感) | 0.4 | 0.25×total | 1.2 | 是 |
| G1=FP8, G2=NVFP4 | 0.2 | 0.4 | 0.5×|G1| + 0.25×|G2| | 0.6 | 取决于 |G1|/|G2| |
LP 求解(用 `LPS`(file:///workspace/modelopt/torch/quantization/algorithms.py)):在 sum(cost) ≤ 0.3 × total 约束下最小化 sum(score)。实际求解器在多个递减 lower_bound(None → 0.99 → 0.90)上重解,直到找到可行解------这避免了"全 NVFP4 但精度差"的极端情况,也避免了"约束不可行"的报错。注意 HAWQ-V3 用 ILP(变量是 bit-width,整数),AutoQuantize 用 LP(变量是 recipe 选择概率,连续后取整),求解更高效但牺牲了部分最优性。
KL 散度替代路径 :当 forward_backward_step 不可用(如纯推理 API、teacher forcing 模型),AutoQuantizeKLDivSearcher 用 KL 散度 KL(p_unquant || p_quant)(p 是 lm_head 的 log-prob 输出)代替 Fisher 信息。直觉:量化若让模型输出分布"偏离"原模型,则该 recipe 敏感度高。KL 路径用 threshold-based binary search 代替 LP(无需精确梯度,但求解近似)。
与 ModelOpt 实现的关系 :`AutoQuantizeGradientSearcher`(file:///workspace/modelopt/torch/quantization/algorithms.py) 用 forward/backward hook 收集 grad_output 与 output_diff,算 (grad·diff)² 作为 Fisher 近似 score;get_auto_quantize_config 可在搜索后用不同 effective_bits 重解 LP (不需 GPU),让用户权衡探索压缩比与精度的 trade-off。ActiveMoECostModel 把 routed expert 按 top_k/num_experts 折算成本(如 8 expert top-2 → 0.25×),让 AutoQuantize 在 MoE 上更愿意量化 expert(因为实际激活的 expert 少,压缩收益高)。
与 HAWQ-V3 的关键区别:
- HAWQ-V3:层级混合精度(每层选 bit-width),用 Hessian trace 度量敏感度,ILP 求解
- AutoQuantize:模块组级混合精度(QKV/gate-up/down 等组),用 Fisher 一阶 Taylor 近似度量敏感度,LP 求解,支持 MoE active cost 模型
AutoQuantize 的 Fisher 近似只需一次 forward + backward 即可估计所有 recipe 的敏感度(梯度共享),比 HAWQ-V3 的逐层 Hessian trace 计算更高效,适合 LLM 规模。
11. 本篇使用指南
11.1 案例:NVFP4 + local_hessian 极致精度
python
import torch
import modelopt.torch.quantization as mtq
# 1. 加载模型
model = ...
# 2. NVFP4 + local_hessian 配置
config = {
"quant_cfg": mtq.NVFP4_DEFAULT_CFG["quant_cfg"],
"algorithm": {
"method": "local_hessian",
"layerwise": {"enable": True}, # 大模型必开
},
}
# 3. 量化
mtq.quantize(model, config, forward_loop)
# 4. 检查 NVFP4StaticQuantizer 是否已提升
mtq.print_quant_summary(model)
11.2 案例:AutoQuantize + KL 散度(无梯度场景)
python
# 某些场景下 forward_backward_step 不可用(如 teacher forcing 模型)
model, search_state = mtq.auto_quantize(
model,
constraints={"effective_bits": 4.5},
quantization_formats=[mtq.NVFP4_AWQ_LITE_CFG, mtq.FP8_DEFAULT_CFG],
data_loader=calib_dataloader,
forward_step=lambda m, b: m(**b),
loss_func=None, # KL 路径不需要 loss
forward_backward_step=None,
method="kl_div", # 用 KL 替代 gradient
num_calib_steps=512,
num_score_steps=128,
)
# 后续重解:试不同 effective_bits
for eb in [4.0, 4.5, 5.0, 5.5]:
cfg = mtq.get_auto_quantize_config(search_state, constraints={"effective_bits": eb})
print(f"effective_bits={eb}: {summarize_config(cfg)}")
11.3 案例:INT4 AWQ Full + GPTQ layerwise 极限精度
python
config = {
"quant_cfg": mtq.INT4_AWQ_CFG["quant_cfg"],
"algorithm": [
# 先 AWQ(通道缩放 + 块裁剪)
{"method": "awq_full"},
# 再 GPTQ(layerwise + QDQ 传播)
{
"method": "gptq",
"layerwise": {"enable": True, "get_qdq_activations_from_prev_layer": True},
"block_size": 128,
"perc_damp": 0.01,
},
],
}
mtq.quantize(model, config, forward_loop)
12. 本篇效果对比
12.1 NVFP4 校准算法精度对比(基于 README blog 与 examples benchmark)
| 校准算法 | MMLU 精度(vs FP16 baseline) | 校准时间(Llama-3.1-8B) | 显存占用 | 推荐场景 |
|---|---|---|---|---|
max |
-2.5% to -3.5% | 5 分钟 | 1× | 快速 baseline |
mse |
-1.5% to -2.5% | 30 分钟 | 1.5× | 通用 NVFP4 |
local_hessian |
-0.5% to -1.5% | 1.5 小时 | 3× | NVFP4 极致精度 |
awq_full |
-1.0% to -2.0% | 1 小时 | 2× | NVFP4 + AWQ |
gptq (layerwise) |
-0.3% to -1.0% | 4 小时 | 5× | 极致精度(INT4 也可用) |
svdquant |
-0.1% to -0.5% | 6 小时 | 8× | 极致 + LoRA 后训练 |
数据来源 :基于 NVIDIA Developer Blog --- Optimizing LLMs for Performance and Accuracy with Post-Training Quantization、NVIDIA Developer Blog --- Introducing NVFP4 for Efficient and Accurate Low-Precision Inference。具体数字因模型与数据集而异。
12.2 AutoQuantize vs 手动配置对比
| 维度 | 手动选 NVFP4 + mse | AutoQuantize (effective_bits=4.8) |
|---|---|---|
| 精度(MMLU) | -1.8% | -1.2%(自动选 FP8 attention + NVFP4 MLP) |
| 显存压缩 | 4× | 3.3×(部分层 FP8) |
| 校准时间 | 30 分钟 | 1.5 小时(含敏感度估计) |
| 用户决策成本 | 高(需试错配置) | 低(仅指定 effective_bits) |
| 灵活性 | 完全自定义 | 受限于候选 formats |
| 可重解 LP | 否 | 是(get_auto_quantize_config) |
12.3 layerwise 开关对比
| 维度 | 关闭 layerwise | 开启 layerwise |
|---|---|---|
| 显存(30B 模型) | OOM(>80GB) | ~16GB |
| 校准时间 | 1× | 1.5-2× |
| 精度(max 算法) | baseline | 略低(无 QDQ 传播) |
| 精度(gptq 算法) | baseline | 更高(QDQ 传播) |
| 断点续算 | 不支持 | 支持(save_every) |
12.4 Fisher vs KL 散度搜索对比
| 维度 | AutoQuantizeGradientSearcher(Fisher) |
AutoQuantizeKLDivSearcher(KL) |
|---|---|---|
| 敏感度度量 | (grad_output · output_diff)² |
`KL(unquant |
| 计算成本 | 中(1 次 forward + 1 次 backward) | 高(多次 forward 比对 log-prob) |
| 显存 | 中(需梯度) | 低(仅前向) |
| 适用场景 | 标准 LLM(loss 可计算) | 推理 API 场景(无梯度) |
| 求解方法 | LP(精确) | threshold-based binary search(近似) |
| 推荐 | 优先(更精确) | 备选(无梯度时) |
12.5 成本模型对比
| 成本模型 | 公式 | 适用场景 | MoE 处理 |
|---|---|---|---|
weight |
Σ weight_size × compression |
Dense 模型 | expert 等权 |
active_moe |
Σ weight_size × compression × (top_k/num_experts) for routed experts |
MoE 模型 | expert 按 active_ratio 折算 |
例如 8 个 expert、top-2 路由的 MoE:active_moe 模型下 expert 权重对 effective_bits 的贡献是 weight 模型的 0.25×------这意味着 AutoQuantize 在 MoE 上更愿意把 expert 量化为 NVFP4,而 dense attention 保持 FP8。
下一篇 :04 · 导出与部署 export/ ------ 把量化模型导出为 vLLM/SGLang/TRT-LLM 部署检查点。