03_NVIDIA_ModelOpt-量化算法深入

03 · 量化算法深入

本文深入剖析 10 种校准算法与 AutoQuantize 自动搜索的实现细节。校准算法是量化精度的关键------同样 NVFP4 配置,max 与 local_hessian 的精度差异可达数个百分点。


目录

  1. 算法一览表
  2. [max 校准](#max 校准)
  3. [mse 校准与 NVFP4MSECalibrator](#mse 校准与 NVFP4MSECalibrator)
  4. [local_hessian 校准](#local_hessian 校准)
  5. [smoothquant 校准](#smoothquant 校准)
  6. [AWQ 系列:awq_lite / awq_clip / awq_full](#AWQ 系列:awq_lite / awq_clip / awq_full)
  7. [gptq 校准](#gptq 校准)
  8. [svdquant 校准](#svdquant 校准)
  9. layerwise_calibrate:分块校准与断点续算
  10. [AutoQuantize 自动搜索](#AutoQuantize 自动搜索)
  11. 本篇使用指南
  12. 本篇效果对比

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_liteawq_fullgptq;NVFP4 按 maxmselocal_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 没机会调 forwardweight_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 / qmaxqmax 是量化级别上界,如 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:

  1. 临时设 _amax = global_amax * scale

  2. 调用 _fake_quantize

  3. 计算重建误差 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_amaxglobal_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_hessianHessian 加权重建误差

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 拖累) 主分布精度损失
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_smooth1/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 除以 ss>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) 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_fullSequentialQuantizer.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,使得量化后的输出变化最小。算法步骤:

  1. 计算 Hessian H = XᵀX(X 是输入)
  2. 阻尼:H += λ * mean(diag(H)) * I(保证可逆)
  3. 计算 H_inv = H⁻¹
  4. 对 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 信息 在量化一个权重后,把误差补偿到尚未量化的后续权重

  1. 计算 Hessian H = XᵀX + λ·diag(H)·I(λ 阻尼保证可逆)
  2. H⁻¹(用 Cholesky 分解保证数值稳定)
  3. 逐列(或 block_size 列分批)量化:
    • 当前列 w 量化误差 e = Q(w) - w
    • eH⁻¹ 比例分摊到后续列:W_remaining -= (H⁻¹[:, i] / H⁻¹[i, i]) · e[i]
  4. 量化下一列,重复

直觉:如果一个权重量化后偏大,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 Σ_rlora_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

  1. 设 active = recipe
  2. 启用 fake_quant
  3. forward_backward_step
  4. 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_scaleget_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_fpgrad_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 模型),AutoQuantizeKLDivSearcherKL 散度 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_outputoutput_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 分钟 快速 baseline
mse -1.5% to -2.5% 30 分钟 1.5× 通用 NVFP4
local_hessian -0.5% to -1.5% 1.5 小时 NVFP4 极致精度
awq_full -1.0% to -2.0% 1 小时 NVFP4 + AWQ
gptq (layerwise) -0.3% to -1.0% 4 小时 极致精度(INT4 也可用)
svdquant -0.1% to -0.5% 6 小时 极致 + LoRA 后训练

数据来源 :基于 NVIDIA Developer Blog --- Optimizing LLMs for Performance and Accuracy with Post-Training QuantizationNVIDIA 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)
显存压缩 3.3×(部分层 FP8)
校准时间 30 分钟 1.5 小时(含敏感度估计)
用户决策成本 高(需试错配置) 低(仅指定 effective_bits
灵活性 完全自定义 受限于候选 formats
可重解 LP 是(get_auto_quantize_config

12.3 layerwise 开关对比

维度 关闭 layerwise 开启 layerwise
显存(30B 模型) OOM(>80GB) ~16GB
校准时间 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 部署检查点。

相关推荐
孤月葬花魂1 小时前
Token 正在吞掉程序员的钱包:一个月 AI 成本上万元,新时代的“生产资料”已经变了
人工智能
exarunner1 小时前
KV Cache 驱逐要不要上 Radix Tree?以 Mooncake模拟一下
人工智能
阿牛哥_GX1 小时前
Angular 页面自动化进阶:Select2 组件与表单提交
人工智能
橘子星1 小时前
🤖 从零造一个AI程序员:30行核心代码拆解Cursor背后的Agent原理
人工智能
大鱼>1 小时前
AI+货物追踪:智能快递柜追踪系统
人工智能·深度学习·算法·机器学习
程序喵大人1 小时前
【AI专栏】图解Transformer - 第04章:LLM生成
人工智能·深度学习·llm·transformer
1234567890@world1 小时前
知识管理 | 数字化 | APQC
大数据·数据库·人工智能
千维百策6661 小时前
AI 编码智能体平台如何提升研发效能:从 CI 修复到工程工作流自动化
人工智能·ci/cd·自动化
以和为贵1 小时前
🔥前端也能搞懂流式输出:从 SSE 到打字机效果
前端·人工智能·架构