PyTorch 模型量化常见问题

训练环境

Docker 容器无法使用 Nvidia 资源?

可能原因:GPU Docker 环境未正确配置。

优先排查:优先检查 环境部署 中 GPU Docker 部分的说明。

处理建议:建议确认 Docker 启动方式、GPU 设备映射和驱动环境是否符合文档要求。

QAT 量化训练

Calibration 精度差,是否说明 QAT 精度大概率也会偏低?

可能原因:

  • 对同一模型而言,Calibration 精度与 QAT 精度通常呈正相关关系。

  • 不同模型对量化误差的敏感程度不同,因此这种相关性没有统一的判断指标。

优先排查:

  • 优先检查 Calibration 精度是否已经明显低于预期。

  • 进一步检查模型是否存在量化不友好的结构,例如高风险算子、共享模块或多输入分布差异过大。

  • 如果已经开始 QAT,还应同步检查 quantized model 的精度,而不是仅观察 QAT model。

  • 如需继续缩小范围,可结合 模型校准构建浮点模型部署一致性分析说明 继续分析。

处理建议:如果 Calibration 精度已经明显偏低,建议优先回到 Calibration 阶段排查,不宜直接寄希望于 QAT 完全修复问题。

Calibration 做到什么程度,可以开始 QAT?

可能原因:

  • 单纯调整校准参数,通常只能带来有限提升。

  • 是否继续停留在 Calibration 阶段,应结合当前精度和一次小规模 QAT 试跑的结果共同判断。

优先排查:

  • 优先检查 Calibration 精度与目标精度之间的差距。

  • 可进一步进行一次小规模 QAT 训练,同时观察 QAT model 和 quantized model 的结果。

处理建议:

  1. 如果 Calibration 精度已经非常接近目标,继续调整校准参数通常仍有价值。

  2. 如果 QAT 精度高于 Calibration,但离目标仍有明显差距,建议继续留在 Calibration 阶段排查量化不友好的问题。

  3. 如果 QAT 精度反而低于 Calibration,建议优先检查 QAT pipeline 和训练策略,而不是继续盲目训练。

QAT 训练时的现象,是否应与 float 训练基本一致?

可能原因:

  • 一般情况下,loss、精度等整体趋势应与浮点训练基本一致。

  • 如果差异较大,通常说明 QAT pipeline、训练状态控制或量化配置存在问题。

优先排查:

  • 优先检查 loss 曲线是否与 float 训练末期差异过大。

  • 进一步检查 train() / eval()set_fake_quantize() 的状态切换是否正确。

  • 同时确认是否正确加载了 Calibration 阶段的初始化参数,以及 prepare 后的模型是否又被继续修改。

处理建议:如果训练现象与 float 明显不一致,建议优先排查 QAT pipeline 本身,而不是直接调整大量训练超参数。

量化模型精度异常,应优先查看哪些专题?

可能原因:QAT model 或 HBIR 模型精度不符合预期,往往涉及量化配置、训练策略或训练部署一致性中的一个或多个环节。

优先排查:

处理建议:建议先判断问题发生在训练阶段、导出阶段还是部署阶段,再选择对应专题继续排查。

QAT 初始 loss 很大、训练中出现 NAN / INF,应如何处理?

可能原因:

  • 输入数据本身存在 NAN。

  • 浮点模型尚未收敛。

  • 未正确执行 Calibration 初始化。

  • 学习率过大、训练策略不合适,或 fake quant / observer 状态配置异常。

优先排查:

  • 优先检查输入数据是否含有 NAN / INF。

  • 进一步检查浮点模型是否已经收敛。

  • 进一步检查是否开启了 Calibration,以及是否正确加载了 Calibration 模型参数。

  • 同时检查 model_check_result.txtqconfig_dtypes.pt.py,确认图结构和 dtype 没有明显异常。

  • 必要时再检查训练策略,例如学习率、优化器和梯度裁剪方式。

处理建议:

  1. 建议先确认输入数据没有异常值。

  2. 建议先确认浮点模型已经收敛,否则微小量化误差也可能放大成明显波动。

  3. 建议先完成 Calibration,以获得更稳定的初始系数。

  4. 如果学习率过大,可优先调低学习率,必要时增加梯度裁剪。

  5. 如果浮点训练使用了 OneCycle 等对学习率影响较大的策略,QAT 阶段通常更适合先切换为较稳定的 SGD 策略再验证。

QAT 精度不升反降,甚至比 Calibration 更差,应如何处理?

可能原因:

  • 训练策略不合适。

  • BN 处理方式不匹配。

  • scale 更新方式不合理。

  • quantized model 精度也在同步下降。

优先排查:

  • 优先同步检查 QAT model 和 quantized model 的精度,不要只观察 QAT model。

  • 进一步检查 BN 策略是否与当前浮点训练方式一致。

  • 进一步检查量化参数应固定还是继续更新。

  • 如果问题在导出后进一步放大,可继续查看 部署一致性分析说明

处理建议:

  • 如果 QAT 精度低于 Calibration,建议优先检查训练策略、BN 处理方式和 scale 更新方式。

  • 如果 quantized model 精度也同步下降,说明问题通常不只是训练表象,而是会真实影响后续部署效果。

开启多机训练后精度变差,应如何处理?

可能原因:开启多机训练后,总 batch size 成倍增大,但学习率等超参数未同步调整。

优先排查:

  • 优先检查总 batch size 是否发生明显变化。

  • 进一步检查 LR、weight decay 等超参数是否仍沿用单机配置。

处理建议:开启多机训练后,建议同步调整 LR 等超参数进行平衡,而不是直接沿用原配置。

Qconfig 是否需要用户干预?

可能原因:Qconfig 直接定义 activation 和 weight 的量化方式,如果对其基本作用缺少判断,后续排查容易失去依据。

优先排查:

  • 优先确认当前使用的 qconfig 模板、量化算法和最终 dtype 配置是否符合模型与平台要求。

  • 如需进一步了解配置方法,可参考 构建 QConfig

处理建议:建议至少明确 activation 和 weight 的量化方式,以及当前使用的核心模板配置。当前支持 FakeQuantizeLSQPACT 等量化算法。

如何导出各阶段的 ONNX 模型?

确认当前需要导出的对象是 qat_model 还是 quantized_model,确认后可按如下方式分别导出 QAT 模型和 quantized model 的 ONNX:

from horizon_plugin_pytorch.utils import onnx_helper as horizon_onnx_helper

# [Optional] Export float 、qat net to ONNX
# --------------------------------------------------------------------
logging.info("Export qat model to ONNX...")
data = torch.rand((1, 3, 228, 228), device=device)
horizon_onnx_helper.export_to_onnx(qat_net, data, "resnet_qat.onnx")

# [Optional] Export quantized_model to ONNX
# --------------------------------------------------------------------
horizon_onnx_helper.export_quantized_onnx(
    quantized_model, data, "resnet_quantized.onnx"
)

int16 / fp16 配置没有按预期生效,应如何处理?

可能原因:

  • module_name 配置错误。

  • 配置被后续模板覆盖。

  • 算子发生回退,导致最终 dtype 与预期不一致。

优先排查:

  • 优先检查 module_name 是否正确。该字段只支持 string,不支持按 index 索引。

  • 进一步检查 model_check_result.txtqconfig_dtypes.pt.pyqconfig_changelogs.txt,确认最终算子 dtype 是否符合预期。

  • 如果平台策略本身仍不明确,可继续查看 构建 QConfig

处理建议:

  • 建议先确认配置命中的是正确模块。

  • 如果已经配置 int16 / fp16,但最终结果没有生效,建议优先检查模板覆盖顺序和回退日志,而不是仅查看配置代码本身。

如何确认某一层是否开启了高精度输出?

可能原因:模型输出节点未正确配置为高精度输出。

优先排查:打印 qat_model 中对应层,检查该层是否包含 (activation_post_process): FakeQuantize

例如 int32 高精度 conv 打印如下:

(1): ConvModule2d(
  (0): Conv2d(
    64, 3, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)
    (weight_fake_quant): FakeQuantize(
      fake_quant_enabled=tensor([1], dtype=torch.uint8), observer_enabled=tensor([1], dtype=torch.uint8),
      quant_min=-128, quant_max=127, dtype=qint8, qscheme=torch.per_channel_symmetric, ch_axis=0,
      scale=tensor([1., 1., 1.]), zero_point=tensor([0, 0, 0])
      (activation_post_process): MovingAveragePerChannelMinMaxObserver(min_val=tensor([]), max_val=tensor([]))
    )
  )
)

int8 低精度 conv 打印如下:

(0): ConvModule2d(
  (0): ConvReLU2d(
    64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)
    (weight_fake_quant): FakeQuantize(
      fake_quant_enabled=tensor([1], dtype=torch.uint8), observer_enabled=tensor([1], dtype=torch.uint8),
      quant_min=-128, quant_max=127, dtype=qint8, qscheme=torch.per_channel_symmetric, ch_axis=0,
      scale=tensor([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
                    1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
                    1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
                    1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]),
      zero_point=tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
      (activation_post_process): MovingAveragePerChannelMinMaxObserver(min_val=tensor([]), max_val=tensor([]))
    )
    (activation_post_process): FakeQuantize(
      fake_quant_enabled=tensor([1], dtype=torch.uint8), observer_enabled=tensor([1], dtype=torch.uint8),
      quant_min=-128, quant_max=127, dtype=qint8, qscheme=torch.per_tensor_symmetric, ch_axis=-1,
      scale=tensor([1.]), zero_point=tensor([0])
      (activation_post_process): MovingAverageMinMaxObserver(min_val=tensor([]), max_val=tensor([]))
    )
  )
)

处理建议:

  • 若没有 activation_post_process: FakeQuantize,通常说明该层为高精度输出。

  • 若本应高精度输出却仍显示为低精度,建议优先检查 qconfig 配置和输出模板是否正确设置。

  • 如果配置看起来正确但最终 dtype 仍不符合预期,建议继续检查 qconfig_dtypes.pt.pyqconfig_changelogs.txt

QAT 与 quantized 模型精度不一致,应如何处理?

可能原因:

  • QAT 阶段无法完全模拟 HBIR 中的纯定点计算逻辑。

  • 导出、convert 或查表转定点过程引入了额外误差。

优先排查:

  • 优先验证 quantized HBIR 模型精度,而不是仅观察 QAT torch module。

  • 如果导出后精度掉点,可先用 pre_export 判断问题是否出在查表转定点阶段。

  • 进一步查看 部署一致性分析说明 和量化精度调优工具中的逐层对比结果,例如 compare_per_layer_out.txt

处理建议:

  • 建议在 QAT 之后至少验证一次 quantized model 的精度。

  • 如果 QAT 精度正常但 quantized 精度异常,建议优先按导出一致性问题处理,而不是继续盲目训练。

上板后出现掉点,应优先排查哪些环节?

可能原因:

  • quantized.bc 与 hbm 之间存在一致性问题。

  • 前处理、输入域、颜色空间转换或部署链路引入了额外误差。

优先排查:

  • 优先确认 quantized.bc 精度是否正常。

  • 如果 quantized.bc 正常而 hbm 或上板结果掉点,优先查看 部署一致性分析说明

  • 同时检查输入数据格式是否与训练阶段一致,例如是否从 RGB 变成了 centered YUV444 / nv12。

  • 如果更偏向部署侧问题,还应继续检查前处理、后处理、插入节点和板端输入输出适配是否一致。

处理建议:不宜直接将上板掉点归因于 QAT 训练本身,应先区分问题发生在 QAT、导出、convert、compile 还是板端输入准备阶段。

load calibration 之后,QAT 训练为什么权重参数不更新?

可能原因:

  • prepare 放在 optimizer 定义之后。

  • fake_quant_enabledobserve_enabled 状态不正确。

  • module 的 training 状态不正确。

优先排查:

  • 优先检查 prepare 是否在 optimizer 定义之前。

  • 进一步检查 fake_quant_enabledobserve_enabled 是否为 1。

  • 进一步检查 module 中的 training 变量是否为 True

处理建议:

  • 建议优先确认 QAT 模型结构和 optimizer 绑定的是同一份 prepare 后模型,再继续检查训练状态切换是否正确。

  • 如果仍无法定位,可回看 Prepare 章节确认模型替换和训练状态是否符合预期。

辅助分支能否插入伪量化节点?

可能原因:辅助分支不用于部署,但却被一并纳入了全局 QAT 训练。

优先排查:

  • 优先确认该分支是否确实属于部署上板路径。

  • 进一步检查该分支的数据分布是否与主干其他分支存在明显差异。

处理建议:

  • 一般仅建议对需要部署上板的模型部分插入伪量化节点。

  • 辅助分支通常会增加训练难度,如果该分支与其他分支的数据分布差异较大,还会进一步增加精度风险,通常建议去除。

如何将地平线 gridsample 算子改写为 torch 公版实现?

可能原因:horizon_plugin_pytorch 中的 gridsample 算子与 torch 公版实现的 grid 定义方式不同。

优先排查:

  • 优先确认当前 grid 输入是否为 int16 类型的绝对坐标。

  • 如果计划切换为 torch.nn.functional.grid_sample,还需确认是否已完成坐标归一化。

处理建议:horizon_plugin_pytorch 中的 gridsample 算子使用 int16 类型的绝对坐标作为 grid 输入,torch 公版实现使用 float32 类型的归一化坐标,取值范围为 [-1, 1]。从 torch.nn.functional.grid_sample 路径导入算子后,可通过如下方式完成归一化。

def norm_grid(x, grid):
  n = grid.size(0)
  h = grid.size(1)
  w = grid.size(2)

  base_coord_y = (
      torch.arange(h, dtype=grid.dtype, device=grid.device)
      .unsqueeze(-1)
      .unsqueeze(0)
      .expand(n, h, w)
  )

  base_coord_x = (
      torch.arange(w, dtype=grid.dtype, device=grid.device)
      .unsqueeze(0)
      .unsqueeze(0)
      .expand(n, h, w)
  )

  absolute_grid_x = grid[:, :, :, 0] + base_coord_x
  absolute_grid_y = grid[:, :, :, 1] + base_coord_y
  norm_grid_x = absolute_grid_x * 2 / (x.size(3) - 1) - 1
  norm_grid_y = absolute_grid_y * 2 / (x.size(2) - 1) - 1
  norm_grid = torch.stack((norm_grid_x, norm_grid_y), dim=-1)

  return norm_grid

设置类错误

模型输出节点未设置为高精度输出,导致量化精度不符合预期,应如何处理?

可能原因:模型输出节点仍按默认低精度配置执行,导致结果不符合预期。

优先排查:优先检查输出节点对应的 qconfig 设置,以及是否启用了输出高精度相关模板优化。

错误示例如下。

假设模型定义如下:

class ToyNet(nn.Module):
    def __init__(self):
        self.conv0 = nn.Conv2d(4,4,3,3)
        self.relu0 = nn.ReLU()
        self.classifier = nn.Conv2d(4,4,3,3)

    def forward(self, x):
        out = self.conv0(x)
        out = self.relu(out)
        out = self.classifier(out)
        return out

# 错误的设置 qconfig 示例:

float_model = ToyNet()
# 整网设置成 int8 量化
float_model.qconfig = default_qat_8bit_fake_quant_qconfig
qat_model = prepare(float_model, example_input)

处理建议:如果希望模型输出节点保持高精度,可通过 QconfigSetter 和模板优化方式进行配置,例如:

qat_model = horizon.quantization.prepare(
    float_model,
    example_input,
    qconfig_setter=QconfigSetter(
        get_qconfig(),
        templates=[
            ModuleNameTemplate({"": qint8}),
            ConvDtypeTemplate(),
        ],
        enable_optimize=True,
    ),
)

为什么某些模块无需量化,却设置了非 None 的 qconfig?

可能原因:前后处理、loss function 等本不需要量化的模块,被错误设置了 qconfig。

优先排查:优先检查这些模块是否确实属于部署路径。

处理建议:建议仅对需要量化的模块设置 qconfig。

没有正确设置 march,会带来什么问题?

可能原因:march 设置与实际部署处理器不一致。

优先排查:优先检查当前目标平台对应的 BPU 架构。

处理建议:应根据目标处理器选择正确的 march,例如 J6 需要使用 Nash。

horizon.march.set_march("nash-e")

方法类错误

模型输入使用 RGB 等非 centered YUV444 格式,为什么可能导致部署精度不一致?

可能原因:训练输入格式与 Horizon 硬件支持的输入格式不一致。

优先排查:优先检查训练输入格式是否与部署侧保持一致。

处理建议:建议从模型训练开始即直接使用 centered YUV444 作为网络输入。

为什么仅使用 qat model 做精测和训练监控,可能无法及时发现部署侧精度异常?

可能原因:QAT 阶段无法完全模拟 HBIR 中的纯定点计算逻辑,仅观察 qat model 结果可能掩盖后续部署侧误差。

优先排查:

  • 优先补充 quantized model 或 quantized HBIR 模型的精度验证结果。

  • 如果两者结果不一致,可回到 部署一致性分析说明 继续定位。

处理建议:建议在 QAT 训练过程中同步观察 quantized 方向的验证结果,而不是仅以 qat model 作为精测和监控依据。

quantized_hbir_model = hbdk4.compiler.convert(qat_hbir_model)
acc = evaluate(quantized_hbir_model, eval_data_loader)

算子类错误

为什么多次调用同一个 FloatFunctional() 成员会有问题?

可能原因:同一个 FloatFunctional 被多次调用时,会共享同一组量化参数,容易引入额外误差。

优先排查:优先检查 forward 中是否多次调用同一个通过 FloatFunctional() 定义的成员。

错误示例如下:

class ToyNet(nn.Module):
    def __init__(self):
        self.add = FloatFunctional()

    def forward(self, x, y, z)
        out = self.add.add(x, y)
        return self.add.add(out, z)

处理建议:不建议在 forward 中多次调用同一个 FloatFunctional() 变量,通常应拆分为多个独立成员。

class ToyNet(nn.Module):
    def __init__(self):
        self.add0 = FloatFunctional()
        self.add1 = FloatFunctional()

    def forward(self, x, y, z)
        out = self.add0.add(x, y)
        return self.add1.add(out, z)

Quantized 模型中部分算子未经过 calibration 或 QAT,为什么可能导致推理失败或部署精度异常?

可能原因:部分算子在定点阶段需要依赖 Calibration 或 QAT 获得真实量化参数。如果缺少前期量化过程,可能无法保证推理正确性和精度稳定性。

优先排查:

  • 优先确认新增算子是否已在 Calibration 或 QAT 阶段参与量化。

  • 如果该算子属于后处理或部署阶段新增节点,还需确认它是否属于允许直接添加的算子类型。

处理建议:Quantized 阶段并非完全不能直接添加算子,例如颜色空间转换等算子可按文档要求处理。但并非所有算子都适合直接添加,例如 cat 这类算子通常需要依赖 Calibration 或 QAT 阶段统计得到的真实量化参数。若需要在网络中增加类似算子,建议优先评估其量化路径是否完整。

模型类错误

浮点模型过拟合会影响 QAT 吗?

可能原因:浮点模型本身过拟合,导致输入稍有扰动就会引起明显输出变化,量化误差更容易被放大。

模型过拟合常见判定方法:

优先排查:

  • 优先检查输入稍作变化后输出是否变化很大。

  • 进一步检查模型参数赋值是否较大。

  • 进一步检查模型 activation 是否较大。

处理建议:建议优先解决浮点模型过拟合问题,再继续进行 QAT。

模型导出

导出为什么会导致精度损失?

可能原因:QAT 阶段的查表仍然是前后带伪量化的浮点算子,导出后才真正转换为查表定点实现。

优先排查:

  • 优先用 pre_export 验证是否为查表转定点导致的精度损失。

  • 进一步查看 部署一致性分析说明 章节,区分问题发生在 export、convert 还是 compile 之后。

可通过如下代码验证是否是查表算子导致了精度损失:

# 只有查表转定点,其他为 qat torch。
from horizon_plugin_pytorch.quantization.hbdk4 import pre_export
qat_export_pt = pre_export(qat_model)

# 查表转定点后模型的推理结果,可以验证此时精度或可视化是否损失。
qat_lut_ret = self.qat_model(self.example_input)

处理建议:如果导出后精度下降,建议先确认问题出现在 export 前后,还是出现在 convert / compile 之后,再决定下一步排查方向。

导出和训练代码不一致时,怎样减少代码侵入?

可能原因:模型中同时包含训练逻辑、验证逻辑和部署逻辑,导致 forward 路径不一致。

优先排查:

  • 优先检查模型是否只包含部署逻辑。

  • 如果不是,再检查 forward 是否已通过 training 变量或部署标志做区分。

如果模型只包含部署逻辑,就不存在导出和训练代码不一致的问题。否则,可通过下面的方法处理:

# 通过 module 的 training 变量和自定义的部署链路标志控制。
def forward(self, x):
    if self.training
        ...
    elif self.deploy:
        ...
    else:
        ...

# 定义不同的 forward,不推荐这种方法,三个 forward 都要适配,从维护代码的角度来说也不如上一种方法。
def train_forward(self, x):
    ...

def val_forward(self, x):
    ...

def deploy_forward(self, x):
    ...

处理建议:一般更建议通过 module 的 training 变量和自定义部署标志控制 forward 路径,而不是拆分为多个独立 forward。