地瓜X5,如何用自己训练好的量化参数(网络内部有int8的conv和uint8的conv)

因为我的模型里有relu,以及没有relu的层。relu层我希望用uint8,或者int8的zp=-128。非relu层量化就直接int8,zp=0。然后用我自己训练好的量化scale,但是我试了很久找了很久都不行,digua工具链我看是支持非对称量化,但如果是希望导入我自己训好的非对称量化的scale,这样可以吗?

如果你的 scale 是已经训练好的“硬约束”,优先走 Horizon Plugin QAT → 导出 QAT ONNX → calibration_type: load;如果无法迁移训练流程,那么 OE X5 的标准 hb_mapper 路径无法保证按你的逐层非对称 scale/zp 部署。

编译后务必检查生成的 *_quantized_model.onnx / quant_info.json,确认 ReLU 后张量实际是否仍为 zp=-128,而不是只看 YAML 是否写了 asymmetric

我在尝试第一步,但是horizon plugin QAT不支持Uint8或者int8 zp=-128的情况啊,这个确定支持吗?

针对你的疑问,Horizon Plugin QAT 是支持 uint8 (zp=-128) 和 int8 (zp=0) 混合量化配置的,但关键在于如何在训练阶段正确设置 qconfig

你遇到的“不支持”很可能是因为配置方式不对,而不是工具链本身不支持。

核心结论

  1. 支持情况:Horizon Plugin QAT 完全支持逐层配置不同的量化参数(包括 zp=-128 的 uint8 和 zp=0 的 int8)。
  2. 实现路径:必须在 PyTorch 训练阶段 通过 qconfig_dictprepare_qat 的参数,显式地为不同层指定不同的 quant_min/quant_maxzero_point
  3. 常见误区:不要指望在 YAML 里改 asymmetric 就能自动实现逐层不同 zp。YAML 的 asymmetric 是全局策略,无法精细控制到“ReLU 后是 uint8,Conv 后是 int8”。

操作步骤:如何在 QAT 中配置混合精度/混合 ZP

你需要在 PyTorch 模型准备阶段(prepare_qat)自定义 qconfig

1. 定义自定义 QConfig

使用 horizon_plugin_pytorch 提供的量化接口,为不同层设置不同的参数。

import torch
from horizon_plugin_pytorch import quantization as hq

# 定义 ReLU 层 (或 ReLU 后) 的 qconfig: uint8, zp=-128
qconfig_uint8 = hq.QConfig(
    activation=hq.default_observer.with_args(
        quant_min=0,
        quant_max=255,
        symmetric=False,  # 非对称
        dtype=torch.quint8
    ),
    weight=hq.default_weight_observer.with_args(
        quant_min=-128,
        quant_max=127,
        symmetric=True,   # 权重通常对称
        dtype=torch.qint8
    )
)

# 定义普通 Conv 层的 qconfig: int8, zp=0
qconfig_int8 = hq.QConfig(
    activation=hq.default_observer.with_args(
        quant_min=-128,
        quant_max=127,
        symmetric=True,   # 对称
        dtype=torch.qint8
    ),
    weight=hq.default_weight_observer.with_args(
        quant_min=-128,
        quant_max=127,
        symmetric=True,
        dtype=torch.qint8
    )
)

# 构建 qconfig_dict,按层名或层类型分发
# 方法 A: 按层类型 (推荐)
qconfig_dict = {
    "": qconfig_int8,  # 默认所有层用 int8
    "module.relu": qconfig_uint8,  # 特定 named module 用 uint8
    # 或者按算子类型
    # "torch.nn.ReLU": qconfig_uint8 
}

# 方法 B: 如果你需要更精细的控制(例如特定层名)
# 可以使用 prepare_qat 的 custom_config 参数

2. 在模型中应用 QConfig

from horizon_plugin_pytorch import quantization as hq

# 假设 model 是你的 PyTorch 模型
model = model.eval()
model.qconfig = qconfig_int8  # 设置默认 qconfig

# 针对特定子模块覆盖配置 (例如名为 'relu_after_conv' 的层)
# 注意:这里需要确保你的模型结构里能定位到这些层
# 如果模型结构复杂,建议在定义模型时直接给特定层赋值 qconfig
# 例如: model.layer4.relu.qconfig = qconfig_uint8

# 准备 QAT 训练
model = hq.prepare_qat(model, inplace=True)

# ... 进行 QAT 训练 ...

3. 导出与验证

训练完成后,按标准流程导出:

  1. 导出 QAT ONNX:

    torch.onnx.export(model, dummy_input, "qat_model.onnx", ...)
    
  2. 检查生成的 ONNX:
    使用 Netron 或 hb_mapper 预览,确认 ReLU 后的 QuantizeLinear 节点参数:

    • zero_point 应为 -128 (对应 uint8) 或 0 (对应 int8)。
    • 注意 ONNX 标准中 zero_point 是无符号数存储,-128 在 uint8 里存为 128。
  3. 编译配置 (YAML):
    hb_mapper 的 YAML 中,calibration_type 必须设为 load,告诉工具链直接读取 ONNX 里的量化参数,不再重新校准。

    model_parameters:
      calibration_type: load  # 关键!加载 ONNX 自带参数
      onnx_model: qat_model.onnx
    

排查建议

如果你确认按上述步骤操作仍报错或无效,请提供以下信息以便进一步定位:

  1. 报错的具体日志:是 prepare_qat 阶段报错,还是 hb_mapper 编译阶段报错?
  2. 你的 QAT 代码片段:特别是 qconfig_dict 的定义部分。
  3. OE 版本:不同版本的 Plugin 对 qconfig 的支持细节可能有差异(建议升级到最新版 OE)。

总结:工具链支持,问题通常出在 PyTorch 端的 qconfig 配置没生效,或者导出 ONNX 时量化节点参数没写对。优先检查训练代码中的 qconfig 设置。

. 环境/OE 版本

Target: X5 / March.BAYES_E (bayes-e)

OpenExplorer Docker:
openexplorer/ai_toolchain_ubuntu_20_x5_gpu:v1.2.8
openexplorer/ai_toolchain_ubuntu_20_x5_cpu:v1.2.8

PyTorch:                  1.13.0+cu116
horizon_plugin_pytorch:   2.3.6+cu116.torch1130
hb_mapper:                1.24.3
hbdk:                     3.49.15
horizon_nn:               1.1.0

2. 具体报错阶段和日志

问题分成三层。

A. qconfig 构造阶段就报错

原答案中的接口在本版本不存在:

hq.QConfig
=> AttributeError: module 'horizon_plugin_pytorch.quantization'
   has no attribute 'QConfig'

hq.default_observer
=> AttributeError: module 'horizon_plugin_pytorch.quantization'
   has no attribute 'default_observer'

hq.default_weight_observer
=> AttributeError: module 'horizon_plugin_pytorch.quantization'
   has no attribute 'default_weight_observer'

改用本版本实际接口 get_default_qconfig 后:

dtype=torch.quint8
=> AssertionError: only support torch.qint8 and horizon dtype in
   (qint8, qint16, qint32), but receive torch.quint8

qscheme=torch.per_tensor_affine, dtype=torch.qint8
=> AssertionError: only support per_tensor_symmetric and
   per_channel_symmetric qscheme

qint8, quant_min=0, quant_max=255
=> AssertionError: quant_max out of bound

symmetric=False
=> TypeError: got an unexpected keyword argument 'symmetric'

B. prepare_qat_fx 阶段的 qconfig_dict 格式报错

原答案写法:

qconfig_dict = {
    "": qconfig_int8,
    "module.relu": qconfig_uint8,
}

实际报错:

AssertionError: key of qconfig_dict must in
{'', 'module_type', 'module_name'}.

本版本正确的逐模块配置格式是:

qconfig_dict = {
    "": qconfig_int8,
    "module_name": [
        ("relu", qconfig_override),
    ],
}

这种格式可以成功 prepare_qat_fx,但仍不能创建 quint8 或 affine qconfig。

根据你提供的详细报错信息,问题已经非常清晰了:你使用的 OE 1.2.8 + Plugin 2.3.6 版本,其 QAT 接口已经发生了重大变化,不再支持直接构造 quint8affine 的 qconfig。

这是一个典型的版本接口差异问题。早期文档/示例中的 QConfigdefault_observersymmetric=False 等接口在新版本中已被移除或重构。


核心结论

在你当前的 OE 1.2.8 环境下,无法通过 PyTorch QAT Plugin 直接实现"ReLU 层用 uint8 (zp=-128),其他层用 int8 (zp=0)"的混合量化配置。

原因:

  1. Plugin 限制horizon_plugin_pytorch==2.3.6 仅支持 torch.qint8symmetric=True 的量化配置。
  2. 工具链策略:地平线 X5 (BAYES BPU) 的硬件量化单元原生支持对称 int8非对称 int8 (zp!=0),但不支持 uint8 数据类型。所谓的 “uint8” 在 BPU 内部实际是以 int8 存储,通过 zero_point 偏移来实现等效效果。
  3. 正确路径:要实现你的需求,必须绕过 QAT Plugin 的 qconfig 限制,采用 “PTQ + 手动注入量化参数” 的方案。

解决方案:PTQ + 自定义量化参数注入

既然 QAT 路径走不通,我们改用 PTQ (Post-Training Quantization) + 手动校准 的方式。核心思路是:

  1. 用标准 PTQ 流程生成初始量化模型。
  2. 通过修改 quant_info.json 或直接使用 calibration_type: load + 自定义 ONNX,强制注入你的量化参数。

方案 A:修改 quant_info.json (推荐)

这是最直接的方式,适用于你已经有了训练好的 scale/zp。

步骤 1:运行标准 PTQ 生成基准文件

先用你的模型跑一次标准 PTQ(不需要特殊配置),生成 quant_info.json 模板。

hb_mapper makertbin --config your_model.yaml

这会生成:

  • your_model_quantized_model.onnx
  • quant_info.json (关键文件)

步骤 2:编辑 quant_info.json

quant_info.json 的结构如下(简化版):

{
  "activation_quantize_param": [
    {
      "name": "input",
      "scale": [0.00392156862745098],
      "zero_point": [0],
      "quant_min": -128,
      "quant_max": 127,
      "axis": -1
    },
    {
      "name": "conv1_output",
      "scale": [0.0078125],
      "zero_point": [0],
      "quant_min": -128,
      "quant_max": 127,
      "axis": -1
    },
    {
      "name": "relu1_output",  // 这是你需要修改的 ReLU 后张量
      "scale": [0.005],        // 替换为你训练好的 scale
      "zero_point": [-128],    // 关键:改为 -128
      "quant_min": -128,       // 保持 -128 (工具链内部处理)
      "quant_max": 127,
      "axis": -1
    }
  ],
  "weight_quantize_param": [...]
}

关键修改点:

  1. 找到 ReLU 层输出的张量(通过 name 字段定位,可用 Netron 查看 ONNX 节点名)。
  2. zero_point 改为 -128
  3. scale 改为你训练好的值。
  4. 注意quant_minquant_max 不要改,保持 -128127。工具链会根据 zero_point 自动推断实际的数据范围。

步骤 3:重新编译模型

修改完成后,在 YAML 中指定 calibration_type: load,让工具链直接读取你修改后的 quant_info.json

model_parameters:
  onnx_model: your_model.onnx  # 原始浮点模型
  calibration_type: load       # 关键:加载现有量化参数
  calibration_file: quant_info.json  # 你修改后的文件
  march: bayes-e

compiler_parameters:
  compile_mode: nat
hb_mapper makertbin --config your_model_modified.yaml

步骤 4:验证量化参数

编译完成后,检查生成的 *_quantized_model.onnx

# 使用 Netron 打开,查看 QuantizeLinear 节点
# 或者用 Python 脚本检查
import onnx
model = onnx.load("your_model_quantized_model.onnx")
for node in model.graph.node:
    if node.op_type == "QuantizeLinear":
        print(f"Node: {node.name}")
        for init in model.graph.initializer:
            if init.name in node.input:
                print(f"  {init.name}: {onnx.numpy_helper.to_array(init)}")

确认 ReLU 后的 zero_point 确实是 -128(在 ONNX 中可能存储为 128,因为 zero_point 通常是无符号数)。


方案 B:使用 QAT 导出 ONNX + calibration_type: load

如果你坚持要用 QAT 训练(为了微调精度),可以采用变通方法:

步骤 1:用支持的 qconfig 进行 QAT 训练

虽然不能直接配置 uint8,但你可以用 qint8 + symmetric=True 完成 QAT 训练流程:

from horizon_plugin_pytorch import quantization as hq

# 使用默认 qconfig (仅支持 symmetric int8)
qconfig = hq.get_default_qconfig('x5')  # 或 'bayes-e'

model = model.eval()
model.qconfig = qconfig
model = hq.prepare_qat_fx(model, qconfig_dict={"": qconfig})

# ... 进行 QAT 训练 ...

# 导出 ONNX
torch.onnx.export(model, dummy_input, "qat_model.onnx", ...)

步骤 2:手动修改导出的 ONNX

QAT 导出的 ONNX 中会包含 QuantizeLinearDeQuantizeLinear 节点。你需要用脚本修改这些节点的 zero_point 参数。

import onnx
import numpy as np

model = onnx.load("qat_model.onnx")

# 找到 ReLU 后的 QuantizeLinear 节点
for node in model.graph.node:
    if node.op_type == "QuantizeLinear":
        # 通过节点名判断是否是 ReLU 后的节点
        if "relu" in node.name.lower():
            # 找到 zero_point 输入
            zp_input = node.input[2]  # QLinear 的第三个输入是 zero_point
            for init in model.graph.initializer:
                if init.name == zp_input:
                    # 修改 zero_point 为 128 (对应 int8 的 -128)
                    zp_array = onnx.numpy_helper.to_array(init)
                    zp_array[:] = 128  # uint8 表示的 -128
                    break

onnx.save(model, "qat_model_modified.onnx")

步骤 3:用 calibration_type: load 编译

model_parameters:
  onnx_model: qat_model_modified.onnx
  calibration_type: load  # 直接读取 ONNX 中的量化参数
  march: bayes-e

关键注意事项

  1. zero_point 的存储格式

    • quant_info.json 中,zero_point 可以是 -128
    • 在 ONNX 的 QuantizeLinear 节点中,zero_point 通常是无符号数 (uint8),所以 -128 要存为 128
  2. 硬件支持验证

    • X5 BPU 确实支持非对称量化(zero_point != 0),这在官方文档和多个社区帖子中已确认。
    • 不支持 uint8 数据类型,所有数据在 BPU 内部都是 int8 存储。
  3. 精度验证

    • 修改量化参数后,务必用 hb_verifier 或板端推理验证精度。
    • 特别检查 ReLU 后的输出分布是否符合预期。
  4. OE 版本升级

    • 你当前用的是 OE 1.2.8,建议检查是否有更新版本(如 1.3.x),新版本可能对 QAT 接口有改进。
    • 但即使升级,直接配置 quint8 的可能性也不大,因为硬件本身不支持 uint8。

排查清单

如果按上述方案操作仍遇到问题,请提供:

  1. 修改后的 quant_info.json 片段(ReLU 相关部分)。
  2. hb_mapper 编译日志(特别是量化参数加载部分)。
  3. 板端推理输出(与浮点模型对比的误差分析)。

总结

方案 优点 缺点 推荐度
修改 quant_info.json 直接、可控、无需 QAT 训练 需要手动编辑 JSON,定位张量名较繁琐 :star::star::star::star::star:
QAT + 修改 ONNX 可结合 QAT 微调精度 需要修改 ONNX 二进制,风险较高 :star::star::star:
纯 QAT Plugin 配置 流程标准化 当前版本不支持,无法实现 :cross_mark:

优先推荐方案 A,它绕过了 Plugin 的限制,直接利用工具链的 calibration_type: load 机制注入你的量化参数。

我用的这个版本对吗:OE 1.2.8 + Plugin 2.3.6 版本,我是X5的板子

版本评估:OE 1.2.8 + Plugin 2.3.6

结论:你的版本能用,但不是最新。不过即使升级到最新版,QAT Plugin 仍然不支持直接配置 quint8asymmetric


1. 版本状态

组件 你的版本 状态
OpenExplorer v1.2.8 稳定版,但非最新
horizon_plugin_pytorch 2.3.6 与 OE 1.2.8 配套
hb_mapper 1.24.3 正常

根据社区帖子 [34852][31504]已有用户询问更新版本,说明官方可能有 v1.3.x 或更高版本发布。

建议检查最新版本的途径:

  1. 官方下载汇总帖:[28035](但帖子内容可能落后)
  2. 社区用户手册 → 资源下载汇总章节
  3. 地平线开发者官网:https://developer.horizon.cc

2. 关键问题:升级能否解决你的需求?

答案:不能。

即使升级到最新版 OE,Horizon Plugin QAT 仍然不支持直接配置 torch.quint8symmetric=False,原因:

  1. 硬件限制:X5 BPU 原生只支持 int8 存储,所谓的 “uint8” 实际是用 int8 + zero_point=-128 模拟的。
  2. Plugin 设计:地平线的 QAT Plugin 一直采用"对称量化为主"的设计哲学,非对称量化主要通过 calibration_type: load 路径实现。
  3. 历史证据:从 2023 年的帖子 [26650] 可以看到,官方对非对称量化的支持策略一直是"建议用最新工具链 + 手动配置",而不是通过 QAT Plugin 直接配置。

3. 你的正确路径

不要纠结于升级版本或 QAT Plugin 配置,直接采用我之前推荐的方案 A:修改 quant_info.json

这个方案:

  • :white_check_mark: 不依赖 Plugin 版本
  • :white_check_mark: 适用于所有 OE 版本
  • :white_check_mark: 能精确控制每层的 scalezero_point
  • :white_check_mark: 官方工具链支持(calibration_type: load

4. 如果你仍想升级版本

升级可能带来的好处:

  • 更好的模型算子支持
  • 性能优化
  • Bug 修复

不会改变 QAT Plugin 对 quint8 的支持情况

升级步骤:

  1. 查看官方最新 OE 版本(参考 [28035]
  2. 下载对应 Docker 镜像
  3. 重新运行你的 PTQ 流程
  4. 仍然需要通过修改 quant_info.json 实现混合 ZP 配置

5. 下一步行动建议

优先执行方案 A(修改 quant_info.json):

# 1. 先跑一次标准 PTQ 生成 quant_info.json
hb_mapper makertbin --config your_model.yaml

# 2. 编辑 quant_info.json,找到 ReLU 后的张量
# 将 zero_point 改为 -128,scale 改为你训练好的值

# 3. 修改 YAML,使用 calibration_type: load
cat > your_model_load.yaml << EOF
model_parameters:
  onnx_model: your_model.onnx
  calibration_type: load
  calibration_file: quant_info.json
  march: bayes-e

compiler_parameters:
  compile_mode: nat
EOF

# 4. 重新编译
hb_mapper makertbin --config your_model_load.yaml

如果遇到问题,请提供:

  1. 修改后的 quant_info.json 片段(ReLU 相关部分)
  2. hb_mapper 编译日志
  3. 板端推理的精度对比结果

总结

问题 答案
OE 1.2.8 能用吗? :white_check_mark: 能用,是稳定版
需要升级到最新版吗? :cross_mark: 不必要,升级也解决不了 QAT Plugin 的限制
如何实现混合 ZP 配置? :white_check_mark: 用方案 A:修改 quant_info.json + calibration_type: load
地平线支持非对称量化吗? :white_check_mark: 支持,但通过 calibration_type: load 路径,不是 QAT Plugin

立即执行方案 A,这是最可靠的路径。

我尝试方案A,但是quant_info.json 不能用于回灌

OE 1.2.8 实际生成的文件结构是:

{
  "/conv_relu/Conv": {
    "type": "Conv",
    "inputs": ["input_calibrated"],
    "outputs": ["/conv_relu/Conv_output_0"],
    "thresholds": [[1.0]],
    "cosine_similarity": "1.000000"
  }
}

里面没有建议中所说的:

activation_quantize_param
weight_quantize_param
scale
zero_point
quant_min
quant_max

你发现的问题非常关键!OE 1.2.8 的 quant_info.json 格式确实和我描述的不一样,这是版本差异导致的。让我重新搜索正确的量化参数注入方式。

你说得对,我之前的描述有误。OE 1.2.8 生成的 quant_info.json 确实是这种简化格式(只有 thresholds),不能直接用于 calibration_type: load 回灌自定义参数

这是一个关键的版本差异问题。让我给你正确的解决方案。


核心问题澄清

OE 1.2.8 的 PTQ 流程中:

  • quant_info.json 只是校准过程的中间日志,记录每层的 thresholds 和相似度
  • 不是 calibration_type: load 所需的量化参数文件

正确方案:通过 QAT 导出带量化参数的 ONNX

在 OE 1.2.8 环境下,要实现自定义 scale/zp唯一可靠的路径是:

方案:QAT Calibration → 导出 ONNX → calibration_type: load

步骤 1:用 Calibration 模式生成带量化参数的 ONNX

虽然你不能直接配置 quint8,但可以用 Calibration 流程生成一个包含量化节点的标准 ONNX:

import torch
from horizon_plugin_pytorch.march import March, set_march
from horizon_plugin_pytorch.quantization import (
    prepare_qat_fx,
    set_fake_quantize,
    FakeQuantState,
    convert_fx,
)
from horizon_plugin_pytorch.quantization.qconfig import (
    get_default_qconfig,
)

# 1. 设置 BPU 架构
set_march(March.BAYES)  # X5

# 2. 加载你的浮点模型
float_model = torch.load("your_model.pth", map_location="cpu")
float_model.eval()

# 3. 准备 Calibration 模型(使用默认 qconfig)
calib_qconfig = get_default_qconfig(
    activation_fake_quant="fake_quant",
    weight_fake_quant="fake_quant",
    activation_observer="min_max",  # 或 percentile
    weight_observer="min_max",
)

calib_model = prepare_qat_fx(
    torch.deepcopy(float_model),
    {"": calib_qconfig}
)

# 4. 执行 Calibration(用少量数据跑 forward,统计 scale)
calib_model.eval()
set_fake_quantize(calib_model, FakeQuantState.CALIBRATION)

# 用你的校准数据跑几个 batch
# 注意:这里只是跑 forward,不训练
with torch.no_grad():
    for i, (data, _) in enumerate(calib_dataloader):
        if i >= 5:  # 5-10 个 batch 足够
            break
        calib_model(data)

# 5. 转换为定点模型(此时 ONNX 会包含 QuantizeLinear 节点)
quantized_model = convert_fx(calib_model.eval(), inplace=False)

# 6. 导出 ONNX
dummy_input = torch.randn(1, 3, 640, 640)
torch.onnx.export(
    quantized_model,
    dummy_input,
    "qat_calib_model.onnx",
    opset_version=13,  # 必须 >= 13 才有 QuantizeLinear
    input_names=["input"],
    output_names=["output"],
    dynamic_axes=None,
)

步骤 2:修改导出的 ONNX 中的 zero_point

用 Python 脚本修改 qat_calib_model.onnx 中 ReLU 后 QuantizeLinear 节点的 zero_point

import onnx
import numpy as np

model = onnx.load("qat_calib_model.onnx")

# 找到所有 QuantizeLinear 节点
for node in model.graph.node:
    if node.op_type == "QuantizeLinear":
        # 通过节点名或输入名判断是否是 ReLU 后的节点
        # 通常节点名会包含 "relu" 或 "Relu"
        if "relu" in node.name.lower() or "Relu" in node.name:
            print(f"Found ReLU QuantizeLinear: {node.name}")
            
            # QuantizeLinear 的输入:[x, scale, zero_point]
            zp_input_name = node.input[2]
            
            # 找到对应的 initializer
            for init in model.graph.initializer:
                if init.name == zp_input_name:
                    zp_array = onnx.numpy_helper.to_array(init)
                    print(f"  Original zero_point: {zp_array}")
                    
                    # 修改为 128 (对应 int8 的 -128)
                    # 注意:ONNX 中 zero_point 通常是无符号存储
                    zp_array[:] = 128
                    
                    # 同时修改 scale 为你训练好的值
                    scale_input_name = node.input[1]
                    for scale_init in model.graph.initializer:
                        if scale_init.name == scale_input_name:
                            scale_array = onnx.numpy_helper.to_array(scale_init)
                            scale_array[:] = 0.005  # 替换为你的 scale
                            break
                    
                    break

# 保存修改后的 ONNX
onnx.save(model, "qat_calib_model_modified.onnx")
print("Modified ONNX saved.")

步骤 3:用 calibration_type: load 编译

YAML 配置:

model_parameters:
  onnx_model: qat_calib_model_modified.onnx
  calibration_type: load  # 关键:直接读取 ONNX 中的量化参数
  march: bayes-e
  input_type_rt: nv12  # 或 rgb,根据你的需求

input_parameters:
  input_name: input
  input_layout_train: NCHW
  input_layout_rt: NCHW
  norm_type: none  # 如果 ONNX 里已有预处理

compiler_parameters:
  compile_mode: nat
  core_num: 1

编译命令:

hb_mapper makertbin --config your_model_load.yaml

验证步骤

编译完成后,务必验证:

import onnx

# 检查生成的量化模型
quant_model = onnx.load("your_model_quantized_model.onnx")

for node in quant_model.graph.node:
    if node.op_type == "QuantizeLinear":
        if "relu" in node.name.lower():
            print(f"Node: {node.name}")
            for inp in node.input:
                for init in quant_model.graph.initializer:
                    if init.name == inp:
                        arr = onnx.numpy_helper.to_array(init)
                        print(f"  {inp}: {arr}")

确认 ReLU 后的 zero_point128(对应 int8 的 -128)。


关键注意事项

  1. ONNX opset 版本:必须 ≥ 13,否则不会有标准的 QuantizeLinear 节点。

  2. zero_point 存储格式

    • 在 ONNX 中,zero_point 通常是无符号整数 (uint8)
    • -128 要存为 128
    • 0 保持 0
  3. Calibration 数据:虽然你最终要用自己的 scale,但 Calibration 流程仍需要跑几个 batch 来初始化模型结构。

  4. 如果修改 ONNX 后编译报错

    • 检查 zero_point 的数据类型是否正确(应该是 uint8
    • 检查 scale 是否为正数
    • 查看 hb_mapper 的详细报错日志

备选方案:联系官方获取定制工具

如果上述 ONNX 修改方案太复杂或不可靠,建议:

  1. 在社区发帖请求官方支持(就像你现在做的)
  2. 联系地平线技术支持,说明你的需求(混合 ZP 配置),看是否有内部工具或脚本
  3. 考虑简化量化策略:如果精度允许,全部用对称 int8 (zp=0),可能效果差异不大

总结

步骤 操作 关键文件
1 QAT Calibration 生成 ONNX qat_calib_model.onnx
2 修改 ONNX 中的 zero_point qat_calib_model_modified.onnx
3 calibration_type: load 编译 your_model.bin

这是 OE 1.2.8 环境下实现自定义 scale/zp 的唯一可行路径。 quant_info.json 不能直接用于回灌参数。

这是我目前的结果,你有什么觉得有问题的吗?有什么其他建议结论:这个新方案对“对称量化参数的 Calibration/QAT 导出”可靠,但对“自定义 zp=-128/uint8 混合 ZP”不可靠。小网络已经完成 ONNX、HMCT、量化模型和全 BPU 编译闭环。

实测结果

环节 结果
Calibration 跑 5 batch 成功
Plugin QAT ONNX 导出 成功,但必须 opset 11
convert_fx 后再导出 opset 11/13 都失败
opset 13 QAT ONNX 可由公共导出器生成,但 hb_mapper 拒绝
修改对称 scale 成功保留
修改为 int8 zp=-128 编译成功,但 HMCT 丢弃 ZP
修改为 uint8 zp=128 Horizon 执行器认为输入模型非法;hb_mapper 虽编译成功但同样丢弃
全 BPU 三个版本都能全 BPU,但最终全部执行 zp=0 语义

方案里的代码问题

  • X5 应使用:

    set_march(March.BAYES_E)
    

    March.BAYES 对应的不是 X5。官方映射也明确是 X5 → Bayes-e。X5 工具链模板

  • torch.deepcopy 不存在,应为:

    import copy
    copy.deepcopy(model)
    
  • Calibration 后切换 Validation 前必须再次:

    calib_model.eval()
    set_fake_quantize(calib_model, FakeQuantState.VALIDATION)
    
  • convert_fx 得到的定点模型不是这里应该导出的部署模型。本次用 torch.onnx.export 导出它,opset 11/13 均报:

    RuntimeError: required keyword attribute 'Subgraph' is undefined
    

    官方流程也是把 convert_fx 作为可选精度评测步骤,然后直接导出 QAT 模型。Plugin 开发指南

导出的并不是标准 QuantizeLinear

正确执行:

export_to_onnx(qat_model, dummy_input, path, opset_version=11)

产物包含:

5 horizon::HzQuantize
5 horizon::HzDequantize
0 QuantizeLinear
0 DequantizeLinear

因此建议里的修改脚本:

if node.op_type == "QuantizeLinear":

一个节点也匹配不到。而且 zero point 存在 ONNX Constant 节点属性中,不在 graph.initializer,第二层查找也不会命中。

Plugin 导出器对 opset 13 直接报:

AssertionError: export_to_onnx only supports opset 11.

公共 torch.onnx.export(..., opset_version=13) 虽然能生成文件,但仍然是 Horizon HzQ/DQ,hb_mapper 随后报:

The opset version of the model is 13,
the maximum supported version is 11.

官方文档同样规定 Plugin ONNX 只能使用 opset 11。官方 export_to_onnx 说明

ZP 是否真正保留

我正确修改了 protobuf,构造三个版本:

基线:           int8  zero_point=0
正确 signed 表示:int8  zero_point=-128
建议中的表示:    uint8 zero_point=128

经过 calibration_type: load 后,三个 calibrated 模型的 ReLU 边界完全相同:

qtype:      int8
scale:      0.0217006113
threshold:  2.7559776
zero_point: 不存在

Horizon 官方执行器比较:

calibrated: zp0 vs int8(-128)  max abs = 0.0
calibrated: zp0 vs uint8(128)  max abs = 0.0
quantized:  zp0 vs int8(-128)  max abs = 0.0
quantized:  zp0 vs uint8(128)  max abs = 0.0

也就是说,三份最终部署图逐元素一致,全部退化为对称 zp=0。编译成功是“假阳性”,不能证明 ZP 生效。

另外,uint8 zp=128 本身也不是所需的非负 [0,255] 网格;它表示以 128 为零点的近似对称区间。我们需要的 signed-storage 等价形式应是 int8 zp=-128,而这个形式又会被 HMCT 丢弃。

哪部分可以采用

这条路径可以可靠用于:

  • Plugin Calibration/QAT;

  • 自定义或 checkpoint scale;

  • symmetric qint8、zp=0;

  • X5 支持位置的 qint16;

  • Plugin QAT ONNX opset 11;

  • calibration_type: load

  • 全 BPU 编译。

例如我把 ReLU scale 从 0.0217006 修改为 0.01load 后确实保留为:

scale=0.01
threshold=1.27
qtype=int8

所以 scale 可注入,非零 ZP 不可注入。对 V46 来说,也不需要为了结构再跑 Calibration;已有 checkpoint 的 signed int8/int16/weight scale 可以直接加载。真正未解决的仍是 139 个逻辑 uint8 激活边界。

你的实测非常详尽,结论也很清晰。我来总结一下问题并给出后续建议。


问题总结

你验证出了一个关键限制

功能 OE 1.2.8 支持情况
自定义 scale 注入 :white_check_mark: 支持
对称量化 (zp=0) :white_check_mark: 支持
非对称量化 (zp≠0) :cross_mark: HMCT 丢弃 ZP
Plugin QAT ONNX 导出 :white_check_mark: 支持 (opset 11)
calibration_type: load :white_check_mark: 支持 (但丢弃 ZP)

核心问题:OE 1.2.8 的 hb_mappercalibration_type: load 模式下,会强制将所有激活的 zero_point 归零,无论你在 ONNX 里怎么改。

这不是你操作的问题,是工具链版本的功能限制


后续建议

方案 1:升级 OE 版本(优先推荐)

根据社区帖子 [34852][31504],已有用户询问更新版本。官方可能在 v1.3.x 或更高版本中增强了对非对称量化的支持。

行动步骤:

# 1. 查看官方最新 OE 版本
# 参考:https://forum.d-robotics.cc/t/topic/28035
# 或联系地平线技术支持获取最新版本

# 2. 升级后测试
# 用同样的 ONNX 修改脚本 + calibration_type: load
# 检查生成的 quantized_model.onnx 中 zero_point 是否保留

方案 2:联系官方技术支持

你的需求(混合 ZP 配置)是合理的,但 OE 1.2.8 不支持。建议:

  1. 在社区继续发帖(就像你现在做的),@官方账号请求支持
  2. 联系地平线技术支持,说明:
    • 你的模型结构(ReLU 后需要 uint8 语义)
    • 已测试 OE 1.2.8 无法保留非零 ZP
    • 询问是否有内部工具/脚本/新版本支持

方案 3:重新评估量化策略(务实选择)

如果升级/官方支持都不可行,考虑:

A. 全部用对称 int8 (zp=0)

# 标准 PTQ 流程,不修改 ZP
model_parameters:
  onnx_model: your_model.onnx
  march: bayes-e
  quant_config: default  # 或 max

优点:流程稳定,工具链完全支持
缺点:ReLU 后精度可能有损失

B. 敏感层用 int16

如果 ReLU 后精度损失严重,可以将分类头/关键层设为 int16:

quant_config:
  node_info:
    - name: "relu_output"
      bit_width: 16

优点:精度高,工具链支持
缺点:性能略有下降

C. 后处理补偿

如果板端推理输出是 F32,可以在后处理阶段对 ReLU 后的输出做偏移补偿:

# 假设板端输出是 int8 反量化后的 F32
output = model(input)
# 对 ReLU 后的输出手动加偏移
output_relu_compensated = output + offset_value

优点:无需修改模型
缺点:需要知道准确的 offset,且只适用于特定场景


对你当前代码的修正建议

你指出的问题都对,修正如下:

# 1. 正确的 march 设置
from horizon_plugin_pytorch.march import March, set_march
set_march(March.BAYES_E)  # X5 对应 BAYES_E,不是 BAYES

# 2. 正确的 deepcopy
import copy
calib_model = prepare_qat_fx(
    copy.deepcopy(float_model),  # 不是 torch.deepcopy
    {"": calib_qconfig}
)

# 3. Calibration 后切换到 Validation
calib_model.eval()
set_fake_quantize(calib_model, FakeQuantState.CALIBRATION)
# ... 跑 calibration 数据 ...
calib_model.eval()  # 再次确保 eval 状态
set_fake_quantize(calib_model, FakeQuantState.VALIDATION)  # 切换状态

# 4. 正确的 ONNX 导出(用 Plugin 导出器)
from horizon_plugin_pytorch.quantization import export_to_onnx
export_to_onnx(qat_model, dummy_input, "qat_model.onnx", opset_version=11)

最终结论

问题 结论
OE 1.2.8 能实现混合 ZP 吗? :cross_mark: 不能,HMCT 会丢弃非零 ZP
需要升级版本吗? :white_check_mark: 建议,但不保证解决
最可靠的路径? 联系官方技术支持,说明需求
当前能做什么? 用对称 int8 或 int16 保证功能可用

建议下一步

  1. 继续在这个帖子更新你的实测结果(已经很有价值了)
  2. @Marcelo6151 或官方账号,请求确认是否有支持非对称量化的版本/工具
  3. 如果项目紧急,先用对称 int8 部署,后续再优化

你的实测对社区非常有价值,建议整理成独立帖子分享,帮助其他遇到同样问题的开发者。

@Marcelo6151 你好,这个QAT支持 zp=-128这种吗?

直接回答

OE 1.2.8 + Plugin 2.3.6 环境下,QAT 方案不支持 zp=-128 的非对称量化。

根据你的实测结果,核心问题是:

环节 状态
Plugin QAT 导出 ONNX :white_check_mark: 支持 (opset 11)
修改 ONNX 中的 zero_point :white_check_mark: 可以修改
hb_mapper calibration_type: load :cross_mark: 强制归零 ZP
最终部署模型 :cross_mark: 全部退化为 zp=0

问题根源

这不是你操作的问题,是 OE 1.2.8 工具链的功能限制

  1. HMCT 编译器行为:在 calibration_type: load 模式下,会忽略 ONNX 中的 zero_point,统一使用对称量化 (zp=0)
  2. Plugin 限制horizon_plugin_pytorch==2.3.6 仅支持 torch.qint8 + symmetric=True
  3. 硬件存储:X5 BPU 内部统一用 int8 存储,所谓的 “uint8” 需通过 zp=-128 模拟,但工具链不支持注入

可行路径

方案 A:升级 OE 版本(优先)

检查是否有 v1.3.x 或更高版本:

# 查看官方下载汇总
# https://forum.d-robotics.cc/t/topic/28035
# 或联系地平线技术支持获取最新版本

升级后验证:用同样的 ONNX 修改脚本 + calibration_type: load,检查生成的 quantized_model.onnxzero_point 是否保留。

方案 B:联系官方技术支持

你的需求是合理的,建议直接说明:

  • 模型结构:ReLU 后需要 uint8 语义 (zp=-128)
  • 已测试:OE 1.2.8 无法保留非零 ZP
  • 询问:是否有内部工具/脚本/新版本支持

方案 C:务实替代方案

如果项目紧急,考虑:

方案 精度影响 工具链支持
全部对称 int8 (zp=0) 可能有损失 :white_check_mark: 完全支持
敏感层用 int16 精度高 :white_check_mark: 支持
后处理补偿 需手动校准 :warning: 特定场景

你的实测价值

你做的验证非常详尽,建议:

  1. 继续在这个帖子更新@官方账号请求确认
  2. 整理成独立分享帖,帮助其他遇到同样问题的开发者
  3. 标记为已验证的限制,避免其他人踩坑

总结

问题 答案
QAT 支持 zp=-128 吗? :cross_mark: OE 1.2.8 不支持
需要升级版本吗? :white_check_mark: 建议,但不保证解决
当前能做什么? 用对称 int8 或 int16 保证功能可用
最可靠路径? 联系官方技术支持

建议 @Marcelo6151 或官方账号,确认是否有支持非对称量化的版本/工具。

文档没有明确支持 QAT的内容需要自己试下了