【X5】请问量化时如何避免_hz_cat_1_rescale_1这类操作的产生

【X5】

量化后使用perf_model发现每一个torch.cat算子都会附加产生大量的hz_cat_1_rescale_1这样的算子,影响模型性能,请问应该如何规避呢?torch.cat的多个输入使用的都是相同的qconfig配置(均为默认default_qat_8bit_fake_quant_qconfig)

问题分析

hz_cat_1_rescale_1 算子的产生是因为 torch.cat 的多个输入虽然使用了相同的 qconfig,但实际量化参数(scale/zero_point)不一致,导致 BPU 需要插入 rescale 节点进行量化参数对齐。

解决方案

方案1:强制 cat 输入共享量化参数(推荐)

在 QAT 配置中,需要确保 cat 的多个输入共享同一组量化参数,而不是仅仅使用相同的 qconfig 模板:

from horizon_plugin_pytorch import quantization as hq

# 方法1:使用共享 observer
class SharedCatModule(nn.Module):
    def __init__(self):
        super().__init__()
        # 创建一个共享的 FakeQuantize 模块
        self.shared_fake_quant = hq.FakeQuantize(
            hq.default_qat_8bit_fake_quant_qconfig()
        )
    
    def forward(self, x1, x2, x3):
        # 所有输入通过同一个 fake_quant,确保 scale/zero_point 一致
        x1_q = self.shared_fake_quant(x1)
        x2_q = self.shared_fake_quant(x2)
        x3_q = self.shared_fake_quant(x3)
        return torch.cat([x1_q, x2_q, x3_q], dim=1)

# 方法2:使用 quantization_scope 强制共享
def prepare_model(model):
    model = hq.prepare(model, inplace=False)
    
    # 找到 cat 算子的所有输入节点,强制它们共享 observer
    # 需要在 prepare 后、calibration 前进行此操作
    for name, module in model.named_modules():
        if isinstance(module, hq.FakeQuantize):
            # 手动设置共享的 scale/zero_point
            pass  # 具体实现需要根据模型结构调整

方案2:使用 QAT 的 share_observation 配置

# 在 yaml 配置或代码中设置
qconfig = hq.default_qat_8bit_fake_quant_qconfig()
qconfig.share_observation = True  # 关键配置

# 或者针对特定 layer 设置
for name, module in model.named_modules():
    if 'cat' in name.lower() or isinstance(module, torch.cat):
        # 获取 cat 的所有输入节点
        # 设置它们共享 observer
        pass

方案3:模型结构优化

根据 X5 高效模型设计指导,减少不必要的 cat 操作:

# 不推荐:多次 cat 操作
x = torch.cat([a, b], dim=1)
y = torch.cat([x, c], dim=1)

# 推荐:一次性 cat
y = torch.cat([a, b, c], dim=1)

# 或者考虑使用 add 替代(如果业务逻辑允许)
y = a + b + c  # elementwise add 通常比 cat 更高效

方案4:使用 hb_model_modifier 后处理

量化完成后,使用工具手动移除冗余的 rescale 节点:

# 使用 hb_model_modifier 检查并优化模型
hb_model_modifier \
    --input your_quantized_model.onnx \
    --output optimized_model.onnx \
    --remove-redundant-ops \
    --march bernoulli2

方案5:检查并打印量化参数

在 calibration 后检查 cat 输入的实际量化参数:

# 在 calibration 后,check_model 前
hq.check_model(
    model,
    example_inputs=(input_tensor,),
    march='bernoulli2',
    advice=1  # 打印性能优化建议
)

# 手动检查 cat 输入的 scale/zero_point
for name, module in model.named_modules():
    if isinstance(module, hq.FakeQuantize):
        if hasattr(module, 'scale') and hasattr(module, 'zero_point'):
            print(f"{name}: scale={module.scale}, zero_point={module.zero_point}")

排查步骤

  1. 确认问题:使用 perf_model 确认 hz_cat_1_rescale_1 的数量和位置
  2. 检查量化参数:打印 cat 所有输入的实际 scale/zero_point
  3. 应用方案1:修改模型使 cat 输入共享 observer
  4. 重新量化:重新执行 QAT 流程
  5. 验证效果:再次使用 perf_model 对比性能

注意事项

  • :warning: 相同 qconfig ≠ 相同量化参数:qconfig 只是配置模板,实际 scale/zero_point 在 calibration 时根据数据分布计算
  • :warning: 动态 shape 问题:如果 cat 的输入 shape 动态变化,可能导致量化参数无法共享
  • :warning: 精度影响:强制共享量化参数可能轻微影响精度,需要验证

建议先在开发板上用 hb_profilerperf_model 定位具体是哪些 cat 算子产生了最多的 rescale,然后针对性优化。

如果问题仍未解决,建议提供:

  • 模型结构片段(cat 操作前后)
  • QAT 配置代码
  • perf_model 的详细输出

方案一报错:AttributeError: module ‘horizon_plugin_pytorch.quantization’ has no attribute ‘default_qat_8bit_fake_quant_qconfig’

抱歉,之前的导入路径有误。正确的导入方式应该是从 qconfig 子模块导入。

修正后的解决方案

正确导入方式

# 正确导入 qconfig
from horizon_plugin_pytorch.quantization.qconfig import (
    default_qat_8bit_fake_quant_qconfig,
)

解决 hz_cat_1_rescale_1 的核心方案

hz_cat_1_rescale_1 产生的根本原因是:cat 的多个输入虽然用了相同的 qconfig 模板,但 calibration 后各自的 scale/zero_point 不同。BPU 需要插入 rescale 节点来对齐量化参数。

方案1:使用 share_qconfig 强制共享量化参数(推荐)

import torch
import torch.nn as nn
from horizon_plugin_pytorch.quantization import prepare_qat_fx, convert_fx
from horizon_plugin_pytorch.quantization.qconfig import default_qat_8bit_fake_quant_qconfig

class CatModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(32, 32, 3, padding=1)
        self.conv2 = nn.Conv2d(32, 32, 3, padding=1)
        self.conv3 = nn.Conv2d(32, 32, 3, padding=1)
        self.relu = nn.ReLU()
        
    def forward(self, x):
        x1 = self.relu(self.conv1(x))
        x2 = self.relu(self.conv2(x))
        x3 = self.relu(self.conv3(x))
        # cat 操作
        out = torch.cat([x1, x2, x3], dim=1)
        return out

model = CatModel()

# 关键:创建一个共享的 qconfig 实例
shared_qconfig = default_qat_8bit_fake_quant_qconfig()

# 为 cat 的所有输入路径设置相同的 qconfig
model.conv1.qconfig = shared_qconfig
model.conv2.qconfig = shared_qconfig
model.conv3.qconfig = shared_qconfig
model.relu.qconfig = shared_qconfig

# 准备量化
model = prepare_qat_fx(model)

# calibration 数据
calib_data = torch.rand(1, 32, 64, 64)
model(calib_data)

# 转换
model = convert_fx(model)

方案2:使用 torch.ao.quantizationshare_observation 机制

from horizon_plugin_pytorch.quantization.qconfig import default_qat_8bit_fake_quant_qconfig
from horizon_plugin_pytorch.quantization import prepare_qat_fx

# 准备模型
model = prepare_qat_fx(model, qconfig=default_qat_8bit_fake_quant_qconfig())

# 关键步骤:在 prepare 后,calibration 前,手动绑定 cat 输入的 observer
def share_cat_observers(model):
    """强制 cat 算子的所有输入共享同一个 observer"""
    for name, module in model.named_modules():
        # 找到所有 FakeQuantize 模块
        if hasattr(module, 'activation_post_process'):
            # 如果是 cat 操作的输入路径,可以手动设置共享
            # 这里需要根据实际模型结构调整
            pass
    
    # 更直接的方式:找到 cat 节点,将其输入的 scale/zero_point 设为相同
    return model

model = share_cat_observers(model)

方案3:检查并打印量化参数(调试用)

# 在 calibration 后,检查 cat 输入的量化参数
def print_quant_params(model):
    for name, module in model.named_modules():
        if hasattr(module, 'activation_post_process'):
            obs = module.activation_post_process
            if hasattr(obs, 'scale') and obs.scale is not None:
                print(f"{name}: scale={obs.scale.item():.6f}, zero_point={obs.zero_point}")

# calibration 后调用
print_quant_params(model)

方案4:模型结构优化(减少 cat 使用)

根据 X5 高效模型设计指导,考虑以下替代方案:

# 如果业务逻辑允许,用 add 替代 cat
# cat: 需要 rescale 对齐,add: 天然支持不同 scale
out = x1 + x2 + x3  # elementwise add 通常比 cat 更高效

# 或者减少 cat 次数
# 不推荐
x = torch.cat([a, b], dim=1)
x = torch.cat([x, c], dim=1)
x = torch.cat([x, d], dim=1)

# 推荐
x = torch.cat([a, b, c, d], dim=1)  # 一次性 cat

完整排查流程

# 1. 使用 perf_model 确认问题
horizon.quantization.perf_model(
    model,
    [example_input],
    opt=2,
    layer_details=True,
    out_dir="./perf_output"
)

# 2. 查看输出,确认 hz_cat_1_rescale_1 的数量和位置
# 3. 应用上述方案修改模型
# 4. 重新量化并验证

注意事项

  1. 相同 qconfig 对象 ≠ 相同量化参数:即使传入同一个 qconfig 对象,calibration 时每个模块仍会独立计算 scale/zero_point。必须手动共享 observer 或在模型设计时确保数据分布一致。

  2. QAT 版本差异:不同版本的 horizon_plugin_pytorch API 可能有差异,请确认你的工具链版本:

    import horizon_plugin_pytorch
    print(horizon_plugin_pytorch.__version__)
    
  3. 如果上述方案无效,请提供:

    • 模型中 cat 操作的结构片段
    • 完整的 QAT 配置代码
    • perf_model 输出的详细日志

建议先尝试方案1(共享 qconfig 实例)和方案4(减少 cat 使用),这两种方法在多数场景下有效。

QAT的使用,您这边是自己研究还是有对应的FAE支持呀,QAT的内容比较深入社区的话就比较难支持了。

从QAT层面分析的话 torch.cat 算子后出现大量 hz_cat_1_rescale_1 类型的附加算子,通常是由于 输入张量的量化 scale 不一致,导致工具链自动插入 rescale(重缩放)节点以对齐 scale。即使你为所有输入配置了相同的 qconfig(如 default_qat_8bit_fake_quant_qconfig),实际运行时各分支的激活统计范围仍可能不同,从而产生不同的 scale。

  1. 确保 cat 前所有输入共享相同的 QuantStub 或 FakeQuantize 节点
  • 如果多个输入来自不同路径(如 skip connection + 主干),即使 qconfig 相同,也会因数据分布不同而生成不同 scale。
  • 推荐做法:将这些输入在 cat 前通过同一个 QuantStub 实例,强制共享 scale。

感谢回答,我这边先尝试一下

跟进建议

Marcelo6151 提到的共享 QuantStub/FakeQuantize 节点是解决这个问题的关键。我补充一个可操作的代码示例:

核心方案:共享 FakeQuantize 节点

import torch
import torch.nn as nn
from horizon_plugin_pytorch.quantization import prepare_qat_fx, convert_fx
from horizon_plugin_pytorch.quantization.qconfig import default_qat_8bit_fake_quant_qconfig
from horizon_plugin_pytorch.quantization import FakeQuantize

class CatModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(32, 32, 3, padding=1)
        self.conv2 = nn.Conv2d(32, 32, 3, padding=1)
        self.conv3 = nn.Conv2d(32, 32, 3, padding=1)
        self.relu = nn.ReLU()
        
        # 关键:创建一个共享的 FakeQuantize 节点
        self.shared_quant = FakeQuantize(default_qat_8bit_fake_quant_qconfig())
        
    def forward(self, x):
        x1 = self.relu(self.conv1(x))
        x2 = self.relu(self.conv2(x))
        x3 = self.relu(self.conv3(x))
        
        # 所有输入通过同一个 FakeQuantize 节点,强制共享 scale/zero_point
        x1_q = self.shared_quant(x1)
        x2_q = self.shared_quant(x2)
        x3_q = self.shared_quant(x3)
        
        out = torch.cat([x1_q, x2_q, x3_q], dim=1)
        return out

model = CatModel()
model = prepare_qat_fx(model)

# calibration
calib_data = torch.rand(1, 32, 64, 64)
model(calib_data)

# 转换
model = convert_fx(model)

验证步骤

# 1. 检查量化参数是否真的共享了
def check_shared_quant(model):
    for name, module in model.named_modules():
        if isinstance(module, FakeQuantize):
            if hasattr(module, 'scale') and module.scale is not None:
                print(f"{name}: scale={module.scale}")

check_shared_quant(model)

# 2. 使用 perf_model 验证 rescale 节点是否减少
from horizon_plugin_pytorch import quantization as hq
hq.perf_model(
    model,
    [calib_data],
    opt=2,
    layer_details=True,
    out_dir="./perf_check"
)
# 查看输出日志中 hz_cat_1_rescale_1 的数量

如果问题仍未解决

请提供以下信息方便进一步排查:

信息项 说明
工具链版本 pip show horizon-plugin-pytorch
cat 操作位置 模型中 cat 前后的结构片段
perf_model 输出 hz_cat_1_rescale_1 的具体数量和位置
QAT 配置代码 完整的 prepare_qat_fx 调用代码

社区资源

先试试共享 FakeQuantize 节点的方案,有结果随时同步 :+1:

请问我这样实现正确吗:

class SharedCatModule_3(nn.Module):
    def __init__(self):
        super().__init__()
        # 创建一个共享的 FakeQuantize 模块
        self.shared_qconfig = default_qat_8bit_fake_quant_qconfig

    def forward(self, x1, x2, x3, dim=1):
        # 所有输入通过同一个 fake_quant,确保 scale/zero_point 一致
        x1.qconfig = self.shared_qconfig
        x2.qconfig = self.shared_qconfig
        x3.qconfig = self.shared_qconfig
        return torch.cat([x1, x2, x3], dim)


class MultiScaleConv4DAttention(nn.Module):
    def __init__(self):
        super().__init__()

        self.cat_0 = SharedCatModule_3()
        self.cat_1 = SharedCatModule_3()
    
    def forward(self):
        k_features = self.cat_0(k_0, k_1, k_2)
        v_features = self.cat_1(v_0, v_1, v_2)

实现有问题 :cross_mark:

你的代码有几个关键错误:

问题分析

问题 说明
:cross_mark: default_qat_8bit_fake_quant_qconfig 缺少括号 应该是 () 调用函数,不是引用
:cross_mark: x1.qconfig = ... 在 forward 中无效 x1 是 Tensor 不是 Module,没有 qconfig 属性
:cross_mark: qconfig 设置时机错误 qconfig 必须在 prepare_qat_fx 之前设置到 Module 上
:cross_mark: 没有使用 FakeQuantize 模块 需要创建 FakeQuantize 实例并在 forward 中调用

正确实现方案

方案1:共享 FakeQuantize 模块(推荐)

import torch
import torch.nn as nn
from horizon_plugin_pytorch.quantization import FakeQuantize, prepare_qat_fx, convert_fx
from horizon_plugin_pytorch.quantization.qconfig import default_qat_8bit_fake_quant_qconfig

class SharedCatModule(nn.Module):
    def __init__(self):
        super().__init__()
        # ✅ 创建一个共享的 FakeQuantize 模块实例
        self.shared_fake_quant = FakeQuantize(default_qat_8bit_fake_quant_qconfig())

    def forward(self, x1, x2, x3, dim=1):
        # ✅ 所有输入通过同一个 FakeQuantize 模块
        x1_q = self.shared_fake_quant(x1)
        x2_q = self.shared_fake_quant(x2)
        x3_q = self.shared_fake_quant(x3)
        return torch.cat([x1_q, x2_q, x3_q], dim=dim)


class MultiScaleConv4DAttention(nn.Module):
    def __init__(self):
        super().__init__()
        self.cat_0 = SharedCatModule()
        self.cat_1 = SharedCatModule()
    
    def forward(self, k_0, k_1, k_2, v_0, v_1, v_2):
        k_features = self.cat_0(k_0, k_1, k_2)
        v_features = self.cat_1(v_0, v_1, v_2)
        return k_features, v_features

# 使用流程
model = MultiScaleConv4DAttention()
model = prepare_qat_fx(model)  # prepare 会自动识别 FakeQuantize 模块

# calibration
calib_data = (torch.rand(1, 32, 64, 64),) * 6  # 6个输入
model(*calib_data)

model = convert_fx(model)

方案2:在 prepare 前为子模块设置共享 qconfig

class SharedCatModule(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(32, 32, 3)
        self.conv2 = nn.Conv2d(32, 32, 3)
        self.conv3 = nn.Conv2d(32, 32, 3)
        
    def forward(self, x):
        x1 = self.conv1(x)
        x2 = self.conv2(x)
        x3 = self.conv3(x)
        return torch.cat([x1, x2, x3], dim=1)

model = SharedCatModule()

# ✅ 关键:为所有分支设置同一个 qconfig 实例
shared_qconfig = default_qat_8bit_fake_quant_qconfig()
model.conv1.qconfig = shared_qconfig
model.conv2.qconfig = shared_qconfig
model.conv3.qconfig = shared_qconfig

model = prepare_qat_fx(model)

验证步骤

# 1. 检查 FakeQuantize 模块是否被正确识别
for name, module in model.named_modules():
    if isinstance(module, FakeQuantize):
        print(f"Found FakeQuantize: {name}")

# 2. calibration 后检查 scale 是否一致
def check_quant_params(model):
    for name, module in model.named_modules():
        if isinstance(module, FakeQuantize):
            if hasattr(module, 'scale') and module.scale is not None:
                print(f"{name}: scale={module.scale}")

check_quant_params(model)

# 3. 使用 perf_model 验证 rescale 节点是否减少
from horizon_plugin_pytorch import quantization as hq
hq.perf_model(
    model,
    calib_data,
    opt=2,
    layer_details=True,
    out_dir="./perf_check"
)
# 查看日志中 hz_cat_1_rescale_1 的数量

关键要点

要点 说明
:white_check_mark: FakeQuantize 是 Module 需要在 __init__ 中创建实例
:white_check_mark: forward 中调用模块 self.shared_fake_quant(x) 而不是设置属性
:white_check_mark: qconfig 在 prepare 前设置 prepare_qat_fx 会读取 Module 的 qconfig 属性
:white_check_mark: 同一个实例 = 共享 scale 同一个 FakeQuantize 实例会共享量化参数

如果仍有问题

请提供:

  1. pip show horizon-plugin-pytorch 版本信息
  2. 完整的模型结构(cat 操作前后)
  3. perf_model 输出中 hz_cat_1_rescale_1 的数量

建议先用方案1测试,有结果随时同步 :+1:

可是方案1中的self.shared_fake_quant = FakeQuantize(default_qat_8bit_fake_quant_qconfig())会报错:TypeError: ‘QConfig’ object is not callable

抱歉,之前的代码示例有误!FakeQuantize 的构造函数不是直接传入 QConfig 对象。

错误原因

FakeQuantize 模块需要的是量化参数配置,而不是 QConfig 对象。QConfig 是用于 prepare_qat_fx 时配置每个 Module 的量化策略的。

正确方案:通过 prepare_qat_fx 的 qconfig 参数控制

要让 torch.cat 的多个输入共享量化参数,正确做法是在 prepare_qat_fx 时,为这些输入路径的子模块配置同一个 qconfig 实例,或者使用更高级的自定义 qconfig 映射

方案1:为 cat 前的子模块设置共享 qconfig(推荐)

import torch
import torch.nn as nn
from horizon_plugin_pytorch.quantization import prepare_qat_fx, convert_fx
from horizon_plugin_pytorch.quantization.qconfig import default_qat_8bit_fake_quant_qconfig
import copy

class SharedCatModule(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(32, 32, 3, padding=1)
        self.conv2 = nn.Conv2d(32, 32, 3, padding=1)
        self.conv3 = nn.Conv2d(32, 32, 3, padding=1)
        self.relu1 = nn.ReLU()
        self.relu2 = nn.ReLU()
        self.relu3 = nn.ReLU()
        
    def forward(self, x):
        x1 = self.relu1(self.conv1(x))
        x2 = self.relu2(self.conv2(x))
        x3 = self.relu3(self.conv3(x))
        return torch.cat([x1, x2, x3], dim=1)

model = SharedCatModule()

# 关键:创建一个共享的 qconfig 实例
shared_qconfig = default_qat_8bit_fake_quant_qconfig()

# 为 cat 的所有输入路径上的最后一个量化点设置相同的 qconfig
# 通常是 relu 或 conv 的输出
model.relu1.qconfig = shared_qconfig
model.relu2.qconfig = shared_qconfig
model.relu3.qconfig = shared_qconfig

# 准备量化
model = prepare_qat_fx(copy.deepcopy(model))

# calibration
calib_data = torch.rand(1, 32, 64, 64)
model.eval()
with torch.no_grad():
    model(calib_data)

# 转换
model = convert_fx(model)

方案2:使用 qconfig 字典映射(更灵活)

from horizon_plugin_pytorch.quantization import prepare_qat_fx
from horizon_plugin_pytorch.quantization.qconfig import default_qat_8bit_fake_quant_qconfig

# 创建共享的 qconfig 实例
shared_qconfig = default_qat_8bit_fake_quant_qconfig()

# 准备量化时,通过字典指定特定模块的 qconfig
model = prepare_qat_fx(
    copy.deepcopy(float_model),
    {
        "": default_qat_8bit_fake_quant_qconfig(),  # 默认配置
        "module_name": {
            # 为 cat 前的关键模块指定共享的 qconfig 实例
            "relu1": shared_qconfig,
            "relu2": shared_qconfig,
            "relu3": shared_qconfig,
        }
    }
)

方案3:使用 set_qconfig 方法(如果支持)

某些版本的 plugin 支持直接在模块上调用 set_qconfig

# 在 prepare 之前
shared_qconfig = default_qat_8bit_fake_quant_qconfig()

for module in [model.relu1, model.relu2, model.relu3]:
    module.qconfig = shared_qconfig

model = prepare_qat_fx(copy.deepcopy(model))

验证共享是否生效

# calibration 后检查量化参数
def check_quant_params(model):
    print("=== 量化参数检查 ===")
    for name, module in model.named_modules():
        if hasattr(module, 'activation_post_process'):
            obs = module.activation_post_process
            if hasattr(obs, 'scale') and obs.scale is not None:
                print(f"{name}: scale={obs.scale.item():.6f}")

# 在 calibration 后调用
check_quant_params(model)

如果 relu1, relu2, relu3 的 scale 值相同,说明共享成功。

关键要点

要点 说明
:white_check_mark: QConfig 不是直接传给 FakeQuantize QConfigprepare_qat_fx 的配置参数
:white_check_mark: 共享 qconfig 实例 多个模块的 .qconfig 属性指向同一个 QConfig 对象
:white_check_mark:prepare_qat_fx 之前设置 prepare 时会读取模块的 qconfig 属性
:white_check_mark: 使用 copy.deepcopy 避免原模型被修改

如果仍有问题

请提供:

  1. pip show horizon-plugin-pytorch 的版本
  2. 你的模型中 cat 操作前后的具体结构
  3. 完整的 prepare_qat_fx 调用代码

建议参考官方文档:QAT 快速上手(fx mode)

还是没解决问题,TypeError: ‘QConfig’ object is not callable

非常抱歉,之前的示例代码有严重的误导!default_qat_8bit_fake_quant_qconfig 是一个函数,调用后返回 QConfig 对象,但 QConfig 对象本身不能直接赋值给多个模块来强制共享 observer。

核心问题在于:即使给多个模块赋值同一个 QConfig 对象,prepare_qat_fx 在为每个模块创建 observer 时,仍会独立实例化,导致 scale/zero_point 不共享。

要解决 hz_cat_1_rescale_1 问题,真正的关键是:让 cat 的多个输入在量化图中共享同一个 FakeQuantize 节点,或者在 PTQ/QAT 配置中使用工具链提供的共享 observer 机制


正确解决方案

方案 1:使用 torch.ao.quantizationshare_qconfig 机制(如果 plugin 支持)

某些版本的 horizon_plugin_pytorch 支持在 qconfig_dict 中使用特殊语法来共享 observer。请检查你的版本文档是否有类似用法:

# 可能的用法(需确认版本支持)
qconfig_dict = {
    "": default_qat_8bit_fake_quant_qconfig(),
    "object_type": [
        (torch.nn.ReLU, default_qat_8bit_fake_quant_qconfig()),
    ],
    # 关键:使用模块名称前缀共享
    "module_name": {
        "relu1": shared_qconfig,  # 这里 shared_qconfig 需要特殊处理
        "relu2": shared_qconfig,
        "relu3": shared_qconfig,
    }
}

但根据社区现有资料,更可靠的做法是方案 2


方案 2:手动插入共享的 FakeQuantize 模块(推荐)

核心思路:不在子模块上设置 qconfig,而是在 forward 中显式调用同一个 FakeQuantize 实例来处理 cat 的所有输入。

import torch
import torch.nn as nn
from horizon_plugin_pytorch.quantization import prepare_qat_fx, convert_fx, FakeQuantize
from horizon_plugin_pytorch.quantization.observer import MinMaxObserver  # 或其他 observer
from horizon_plugin_pytorch.quantization.qconfig import QConfig

class SharedCatModule(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(32, 32, 3, padding=1)
        self.conv2 = nn.Conv2d(32, 32, 3, padding=1)
        self.conv3 = nn.Conv2d(32, 32, 3, padding=1)
        self.relu1 = nn.ReLU()
        self.relu2 = nn.ReLU()
        self.relu3 = nn.ReLU()
        
        # 关键:创建一个共享的 FakeQuantize 模块
        # 注意:FakeQuantize 的构造参数需要根据你的 plugin 版本调整
        observer = MinMaxObserver()  # 或其他合适的 observer
        self.shared_fake_quant = FakeQuantize(observer=observer)
        
    def forward(self, x):
        x1 = self.relu1(self.conv1(x))
        x2 = self.relu2(self.conv2(x))
        x3 = self.relu3(self.conv3(x))
        
        # 所有输入通过同一个 FakeQuantize 模块
        x1_q = self.shared_fake_quant(x1)
        x2_q = self.shared_fake_quant(x2)
        x3_q = self.shared_fake_quant(x3)
        
        return torch.cat([x1_q, x2_q, x3_q], dim=1)

model = SharedCatModule()

# 准备量化时,不需要为 relu1/2/3 设置 qconfig
# 因为 shared_fake_quant 已经显式插入到图中
model = prepare_qat_fx(model)

# calibration
calib_data = torch.rand(1, 32, 64, 64)
model.eval()
with torch.no_grad():
    model(calib_data)

# 转换
model = convert_fx(model)

注意FakeQuantize 的构造方式可能因版本而异。如果你的版本不支持直接传入 observer,可能需要这样:

from horizon_plugin_pytorch.quantization import FakeQuantize
from horizon_plugin_pytorch.quantization.qconfig import default_qat_8bit_fake_quant_qconfig

# 尝试1:使用默认构造
self.shared_fake_quant = FakeQuantize()

# 尝试2:从 qconfig 创建(如果支持)
qconfig = default_qat_8bit_fake_quant_qconfig()
self.shared_fake_quant = FakeQuantize.from_qconfig(qconfig)

方案 3:使用 PTQ 而非 QAT(如果适用)

如果QAT太复杂,可以考虑PTQ(训练后量化),并在 YAML 配置中使用 share_observer 或类似参数:

# 量化配置 YAML 示例
compiler_parameters:
  share_observer: true  # 如果工具链支持
  # 或者针对特定 layer 配置
  layer_parameters:
    - layer_name: "relu1"
      share_observer_with: "relu2"
    - layer_name: "relu2"
      share_observer_with: "relu1"

方案 4:联系 FAE 获取版本特定的 API

正如 Marcelo6151 提到的,QAT 的深度优化需要 FAE 支持。不同版本的 horizon_plugin_pytorch API 差异很大,建议:

  1. 确认版本

    pip show horizon-plugin-pytorch
    
  2. 查看版本文档

    import horizon_plugin_pytorch
    print(horizon_plugin_pytorch.__version__)
    
    # 查看 FakeQuantize 的构造方式
    import inspect
    print(inspect.signature(horizon_plugin_pytorch.quantization.FakeQuantize.__init__))
    
  3. 提供以下信息给 FAE 或社区

    • 工具链版本
    • 模型结构(cat 操作前后)
    • perf_model 输出的详细日志
    • 当前尝试的代码

快速验证步骤

# 1. 检查 FakeQuantize 的构造方式
from horizon_plugin_pytorch.quantization import FakeQuantize
import inspect
print(inspect.signature(FakeQuantize.__init__))

# 2. 检查 qconfig 的类型
from horizon_plugin_pytorch.quantization.qconfig import default_qat_8bit_fake_quant_qconfig
qconfig = default_qat_8bit_fake_quant_qconfig()
print(type(qconfig))  # 应该是 horizon_plugin_pytorch.quantization.qconfig.QConfig

# 3. 尝试创建共享的 FakeQuantize
try:
    shared_fq = FakeQuantize()
    print("FakeQuantize() 成功")
except Exception as e:
    print(f"FakeQuantize() 失败:{e}")

请先运行上述验证代码,告诉我:

  1. horizon-plugin-pytorch 的版本
  2. FakeQuantize.__init__ 的参数签名
  3. 是否有其他可用的 API(如 FakeQuantize.from_qconfig

这样我才能给出准确的代码示例 :folded_hands:

这种方式会报错:AttributeError: ‘MovingAverageMinMaxObserver’ object has no attribute ‘scale’

错误分析

MovingAverageMinMaxObservercalibration 完成前 确实没有 scale 属性,这是正常的。scale 是在 calibration 过程中通过统计数据计算出来的。

出现这个错误说明 calibration 流程没有正确执行


完整的 QAT 流程(含 calibration 状态设置)

根据官方文档 QAT 快速上手(fx mode),正确的流程是:

import torch
import torch.nn as nn
import copy
from horizon_plugin_pytorch.quantization import (
    prepare_qat_fx,
    convert_fx,
    set_fake_quantize,
    FakeQuantState,
    FakeQuantize,
)
from horizon_plugin_pytorch.quantization.observer import MovingAverageMinMaxObserver
from horizon_plugin_pytorch.quantization.qconfig import default_qat_8bit_fake_quant_qconfig

class SharedCatModule(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(32, 32, 3, padding=1)
        self.conv2 = nn.Conv2d(32, 32, 3, padding=1)
        self.conv3 = nn.Conv2d(32, 32, 3, padding=1)
        self.relu1 = nn.ReLU()
        self.relu2 = nn.ReLU()
        self.relu3 = nn.ReLU()
        
        # 创建共享的 FakeQuantize 模块
        self.shared_fake_quant = FakeQuantize(
            observer=MovingAverageMinMaxObserver
        )
        
    def forward(self, x):
        x1 = self.relu1(self.conv1(x))
        x2 = self.relu2(self.conv2(x))
        x3 = self.relu3(self.conv3(x))
        
        # 所有输入通过同一个 FakeQuantize 模块
        x1_q = self.shared_fake_quant(x1)
        x2_q = self.shared_fake_quant(x2)
        x3_q = self.shared_fake_quant(x3)
        
        return torch.cat([x1_q, x2_q, x3_q], dim=1)

# ========== 完整流程 ==========

# 1. 创建模型
model = SharedCatModule()

# 2. prepare_qat_fx
model = prepare_qat_fx(copy.deepcopy(model))

# 3. 【关键】设置 calibration 状态
model.eval()
set_fake_quantize(model, FakeQuantState.CALIBRATION)

# 4. 执行 calibration(需要多批数据)
calib_loader = [...]  # 你的校准数据集
with torch.no_grad():
    for data in calib_loader:
        model(data)

# 5. 【关键】切换到 validation 状态后才能访问 scale
model.eval()
set_fake_quantize(model, FakeQuantState.VALIDATION)

# 6. 现在可以检查 scale 了
def check_quant_params(model):
    for name, module in model.named_modules():
        if isinstance(module, FakeQuantize):
            if hasattr(module, 'scale') and module.scale is not None:
                print(f"{name}: scale={module.scale}")

check_quant_params(model)

# 7. 转换
model = convert_fx(model)

# 8. 性能验证
from horizon_plugin_pytorch import quantization as hq
hq.perf_model(
    model,
    [torch.rand(1, 32, 64, 64)],
    opt=2,
    layer_details=True,
    out_dir="./perf_check"
)

关键点总结

步骤 说明
:white_check_mark: set_fake_quantize(model, FakeQuantState.CALIBRATION) calibration 前必须设置,否则 observer 不更新统计量
:white_check_mark: model.eval() calibration 和 validation 都需要 eval 模式
:white_check_mark: set_fake_quantize(model, FakeQuantState.VALIDATION) calibration 后才能访问 scale
:white_check_mark: 多批 calibration 数据 单批数据可能导致统计量不准确

如果仍有问题

请运行以下诊断代码,告诉我输出:

import horizon_plugin_pytorch
print(f"Plugin version: {horizon_plugin_pytorch.__version__}")

from horizon_plugin_pytorch.quantization import FakeQuantize
import inspect
print(f"FakeQuantize signature: {inspect.signature(FakeQuantize.__init__)}")

from horizon_plugin_pytorch.quantization.qconfig import default_qat_8bit_fake_quant_qconfig
qconfig = default_qat_8bit_fake_quant_qconfig()
print(f"QConfig type: {type(qconfig)}")

备选方案:如果共享 FakeQuantize 仍不可行

如果上述方案因版本问题无法使用,建议:

  1. 联系 FAE 支持(如 Marcelo6151 所说,QAT 深度优化需要官方支持)
  2. 尝试 PTQ 方案,在 YAML 配置中控制量化参数
  3. 接受 rescale 开销,通过其他优化(如减少 cat 次数、调整模型结构)来弥补性能损失

参考资源:

请先确认你的工具链版本,我们再针对性调整代码 :+1:

calibration阶段报错:ERROR: function <built-in method aminmax of type object at 0x7ebb4369e9c0> is not implemented for QTensor. Please check whether has unsupported ops in the model.

这个错误说明 MovingAverageMinMaxObserver 内部使用的 aminmax 操作在量化图上不支持。这是版本兼容性问题。

快速解决方案

方案1:使用更简单的 Observer(推荐)

from horizon_plugin_pytorch.quantization import FakeQuantize
from horizon_plugin_pytorch.quantization.observer import MinMaxObserver  # 不用 MovingAverage

class SharedCatModule(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(32, 32, 3, padding=1)
        self.conv2 = nn.Conv2d(32, 32, 3, padding=1)
        self.conv3 = nn.Conv2d(32, 32, 3, padding=1)
        self.relu1 = nn.ReLU()
        self.relu2 = nn.ReLU()
        self.relu3 = nn.ReLU()
        
        # 使用 MinMaxObserver 而不是 MovingAverageMinMaxObserver
        self.shared_fake_quant = FakeQuantize(
            observer=MinMaxObserver
        )
        
    def forward(self, x):
        x1 = self.relu1(self.conv1(x))
        x2 = self.relu2(self.conv2(x))
        x3 = self.relu3(self.conv3(x))
        
        x1_q = self.shared_fake_quant(x1)
        x2_q = self.shared_fake_quant(x2)
        x3_q = self.shared_fake_quant(x3)
        
        return torch.cat([x1_q, x2_q, x3_q], dim=1)

方案2:不手动创建 FakeQuantize,改用标准 QAT 流程(更稳定)

根据官方文档,不要手动插入 FakeQuantize,而是通过 prepare_qat_fxqconfig_dict 控制:

import torch
import torch.nn as nn
import copy
from horizon_plugin_pytorch.quantization import prepare_qat_fx, convert_fx, set_fake_quantize, FakeQuantState
from horizon_plugin_pytorch.quantization.qconfig import default_qat_8bit_fake_quant_qconfig

class CatModule(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(32, 32, 3, padding=1)
        self.conv2 = nn.Conv2d(32, 32, 3, padding=1)
        self.conv3 = nn.Conv2d(32, 32, 3, padding=1)
        self.relu = nn.ReLU()
        
    def forward(self, x):
        x1 = self.relu(self.conv1(x))
        x2 = self.relu(self.conv2(x))
        x3 = self.relu(self.conv3(x))
        return torch.cat([x1, x2, x3], dim=1)

model = CatModule()

# 关键:为所有分支设置同一个 qconfig 对象(注意是调用函数返回对象)
shared_qconfig = default_qat_8bit_fake_quant_qconfig()
model.conv1.qconfig = shared_qconfig
model.conv2.qconfig = shared_qconfig
model.conv3.qconfig = shared_qconfig
model.relu.qconfig = shared_qconfig

# prepare
model = prepare_qat_fx(copy.deepcopy(model))

# calibration 状态
model.eval()
set_fake_quantize(model, FakeQuantState.CALIBRATION)

# 执行 calibration
with torch.no_grad():
    for data in calib_loader:  # 多批数据
        model(data)

# validation 状态
model.eval()
set_fake_quantize(model, FakeQuantState.VALIDATION)

# 转换
model = convert_fx(model)

方案3:如果上述都不可行,接受 rescale 开销,优化模型结构

根据 X5 高效模型设计指导,减少 cat 操作:

# 不推荐:多次 cat
x = torch.cat([a, b], dim=1)
x = torch.cat([x, c], dim=1)

# 推荐:一次性 cat
x = torch.cat([a, b, c], dim=1)

# 或者用 add 替代(如果业务允许)
x = a + b + c  # elementwise add 不需要 rescale

诊断信息

请运行以下代码,告诉我输出,方便进一步定位问题:

import horizon_plugin_pytorch
print(f"Plugin version: {horizon_plugin_pytorch.__version__}")

from horizon_plugin_pytorch.quantization import FakeQuantize
import inspect
print(f"FakeQuantize signature: {inspect.signature(FakeQuantize.__init__)}")

# 检查可用的 observer
from horizon_plugin_pytorch.quantization import observer
print(f"Available observers: {[x for x in dir(observer) if 'Observer' in x]}")

建议

根据 Marcelo6151 的回复,QAT 深度优化建议联系 FAE 支持。如果上述方案都无法解决,建议:

  1. 提供工具链版本和完整代码给 FAE
  2. 考虑使用 PTQ 方案(YAML 配置更简单)
  3. 参考社区已有案例:QAT 快速上手(fx mode)

先试试方案1(换 MinMaxObserver),有结果随时同步 :+1:

class xxxx(nn.Module):
    def __init__(self, in_channels=128, window_sizes=[3, 7, 9]):
        super().__init__()
        self.quant_k = QuantStub()  # for cat scale
        self.quant_v = QuantStub()  # for cat scale

    def forward(self):
        k_features = torch.cat([self.quant_k(k_3x3), self.quant_k(k_7x7), self.quant_k(k_9x9)], dim=1)  # for cat scale
        v_features = torch.cat([self.quant_v(v_3x3), self.quant_v(v_7x7), self.quant_v(v_9x9)], dim=1)  # for cat scale

我使用这种方式的时候,最后转hbm阶段执行torch.jit.trace会报错:AssertionError: input scale must be the same as op’s

错误原因分析

AssertionError: input scale must be the same as op's 这个错误的核心原因是:

你多次调用同一个 QuantStub 实例(如 self.quant_k),但每次输入的 tensor 数据分布不同,导致 calibration 后该 QuantStub 的 scale 无法同时满足所有输入路径的要求。

torch.jit.trace 时,工具链检测到 cat 的多个输入虽然来自同一个 QuantStub,但由于 forward 中实际传入的 tensor 不同(k_3x3, k_7x7, k_9x9),量化图认为这些输入应该有不同的 scale,但你又强制它们共享同一个 QuantStub,导致冲突。


正确解决方案

方案 1:为每个输入路径使用独立的 QuantStub,但强制共享 scale(推荐)

关键:在 prepare_qat_fx 后,手动绑定这些 QuantStub 的 observer,让它们共享同一组统计量。

import torch
import torch.nn as nn
import copy
from horizon_plugin_pytorch.quantization import (
    prepare_qat_fx,
    convert_fx,
    set_fake_quantize,
    FakeQuantState,
    QuantStub,
)
from horizon_plugin_pytorch.quantization.qconfig import default_qat_8bit_fake_quant_qconfig

class MultiScaleCatModule(nn.Module):
    def __init__(self, in_channels=128, window_sizes=[3, 7, 9]):
        super().__init__()
        self.window_sizes = window_sizes
        
        # 为每个输入路径创建独立的 QuantStub
        self.quant_k_3x3 = QuantStub()
        self.quant_k_7x7 = QuantStub()
        self.quant_k_9x9 = QuantStub()
        
        self.quant_v_3x3 = QuantStub()
        self.quant_v_7x7 = QuantStub()
        self.quant_v_9x9 = QuantStub()
        
        # 模拟不同感受野的特征提取(实际是你的卷积层)
        self.conv_3x3 = nn.Conv2d(in_channels, in_channels, 3, padding=1)
        self.conv_7x7 = nn.Conv2d(in_channels, in_channels, 7, padding=3)
        self.conv_9x9 = nn.Conv2d(in_channels, in_channels, 9, padding=4)
        
    def forward(self, x):
        # 提取多尺度特征
        k_3x3 = self.conv_3x3(x)
        k_7x7 = self.conv_7x7(x)
        k_9x9 = self.conv_9x9(x)
        
        v_3x3 = k_3x3  # 简化示例,实际可能不同
        v_7x7 = k_7x7
        v_9x9 = k_9x9
        
        # 量化后 cat
        k_features = torch.cat([
            self.quant_k_3x3(k_3x3),
            self.quant_k_7x7(k_7x7),
            self.quant_k_9x9(k_9x9),
        ], dim=1)
        
        v_features = torch.cat([
            self.quant_v_3x3(v_3x3),
            self.quant_v_7x7(v_7x7),
            self.quant_v_9x9(v_9x9),
        ], dim=1)
        
        return k_features, v_features

# ========== 关键步骤:共享 scale ==========

model = MultiScaleCatModule()

# 1. prepare
model = prepare_qat_fx(copy.deepcopy(model))

# 2. 【关键】手动绑定 K 路径的 observer,让它们共享统计量
def share_observers(model, stub_names):
    """强制多个 QuantStub 共享同一个 observer 实例"""
    if not stub_names:
        return
    
    # 获取第一个 stub 的 observer 作为主 observer
    first_stub = getattr(model, stub_names[0])
    if hasattr(first_stub, 'activation_post_process'):
        main_observer = first_stub.activation_post_process
        
        # 将其他 stub 的 observer 指向同一个实例
        for name in stub_names[1:]:
            stub = getattr(model, name)
            if hasattr(stub, 'activation_post_process'):
                stub.activation_post_process = main_observer

# 共享 K 路径的 scale
share_observers(model, ['quant_k_3x3', 'quant_k_7x7', 'quant_k_9x9'])

# 共享 V 路径的 scale
share_observers(model, ['quant_v_3x3', 'quant_v_7x7', 'quant_v_9x9'])

# 3. calibration
model.eval()
set_fake_quantize(model, FakeQuantState.CALIBRATION)

calib_data = torch.rand(1, 128, 64, 64)  # 示例数据
with torch.no_grad():
    # 多批数据校准
    for _ in range(10):
        model(calib_data)

# 4. 切换到 validation 状态
model.eval()
set_fake_quantize(model, FakeQuantState.VALIDATION)

# 5. 验证 scale 是否真的共享了
def check_scales(model, stub_names):
    scales = []
    for name in stub_names:
        stub = getattr(model, name)
        if hasattr(stub, 'activation_post_process'):
            obs = stub.activation_post_process
            if hasattr(obs, 'scale') and obs.scale is not None:
                scales.append(obs.scale.item())
    print(f"{stub_names} scales: {scales}")
    if len(set(scales)) == 1:
        print("✅ Scale 共享成功!")
    else:
        print("❌ Scale 未共享,需要检查绑定逻辑")

check_scales(model, ['quant_k_3x3', 'quant_k_7x7', 'quant_k_9x9'])
check_scales(model, ['quant_v_3x3', 'quant_v_7x7', 'quant_v_9x9'])

# 6. 转换
model = convert_fx(model)

# 7. trace 并编译
example_input = torch.rand(1, 128, 64, 64)
traced_model = torch.jit.trace(model.cpu(), example_input)

# 8. 性能验证
from horizon_plugin_pytorch import quantization as hq
hq.perf_model(
    traced_model,
    [example_input],
    opt=2,
    layer_details=True,
    out_dir="./perf_check"
)

方案 2:使用 torch.ao.quantizationshare_qconfig(如果版本支持)

某些版本的 plugin 支持在 qconfig_dict 中使用特殊语法,但需要确认你的版本是否支持。


方案 3:如果上述方案因版本问题不可行,改用 PTQ

PTQ 的 YAML 配置可能更灵活,支持 share_observer 参数:

# 量化配置 YAML
compiler_parameters:
  share_observer: true
  # 或针对特定 layer
  layer_parameters:
    - layer_name: "quant_k_3x3"
      share_observer_with: "quant_k_7x7"
    - layer_name: "quant_k_7x7"
      share_observer_with: "quant_k_9x9"

关键要点

要点 说明
:cross_mark: 不要在 forward 中多次调用同一个 QuantStub 会导致 scale 冲突
:white_check_mark: 为每个输入路径创建独立的 QuantStub 避免 trace 时的 scale 检查失败
:white_check_mark: 在 prepare 后手动绑定 observer 强制共享统计量
:white_check_mark: calibration 需要多批数据 确保统计量准确

如果仍有问题

请提供:

  1. pip show horizon-plugin-pytorch 的版本
  2. 完整的模型代码(包括 k_3x3, k_7x7, k_9x9 的来源)
  3. perf_model 输出的详细日志

建议参考官方文档:QAT 快速上手(fx mode)

如果问题复杂,建议联系 FAE 支持(如 Marcelo6151 所说):+1:

pip show horizon-plugin-pytorch执行结果:

Name: horizon-plugin-pytorch
Version: 2.3.6+cu116.torch1130
Summary: Quantization aware training tools for horizon BPU