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 部署检查点。

相关推荐
chaojihaomai3 小时前
AI电商商品图生成中的材质保真问题——扩散模型重建优先级分析与锁定式方案
人工智能·材质
大模型探索者5 小时前
2026 企业智能体平台选型指南:从通用到行业深耕,怎么选?
大数据·人工智能
深圳行云创新5 小时前
北极星知识图谱平台|太空越来越“拥挤”,知识图谱如何预测空间碎片碰撞风险?
人工智能·知识图谱
Am-Chestnuts5 小时前
国产AI多轮对话归档与多格式导出实践
人工智能·word
麻瓜生活睁不开眼6 小时前
Android16修改全局桌面视图边框四直角显示为弧边圆角
android·java·深度学习
workflower6 小时前
装备企业的 AI 路线图
人工智能·深度学习·机器学习·设计模式·机器人
AKAMAI6 小时前
Linode接口及默认防火墙现已全面开放使用
人工智能·云计算
前端开发江鸟7 小时前
RAG 回答错了,问题到底出在召回、重排,还是生成?
人工智能
美狐美颜SDK开放平台7 小时前
直播app开发如何实现主播级美颜效果?美颜sdk开发方案详解
人工智能·音视频·美颜sdk·视频美颜sdk·美颜api
东方小月7 小时前
从零开发一个 Coding Agent(四):使用状态机校验大模型事件流
前端·人工智能·后端