【X5】
量化后使用perf_model发现每一个torch.cat算子都会附加产生大量的hz_cat_1_rescale_1这样的算子,影响模型性能,请问应该如何规避呢?torch.cat的多个输入使用的都是相同的qconfig配置(均为默认default_qat_8bit_fake_quant_qconfig)
【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 节点进行量化参数对齐。
在 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 # 具体实现需要根据模型结构调整
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
根据 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 更高效
hb_model_modifier 后处理量化完成后,使用工具手动移除冗余的 rescale 节点:
# 使用 hb_model_modifier 检查并优化模型
hb_model_modifier \
--input your_quantized_model.onnx \
--output optimized_model.onnx \
--remove-redundant-ops \
--march bernoulli2
在 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}")
perf_model 确认 hz_cat_1_rescale_1 的数量和位置perf_model 对比性能建议先在开发板上用 hb_profiler 或 perf_model 定位具体是哪些 cat 算子产生了最多的 rescale,然后针对性优化。
如果问题仍未解决,建议提供:
方案一报错: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 节点来对齐量化参数。
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)
torch.ao.quantization 的 share_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)
# 在 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)
根据 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. 重新量化并验证
相同 qconfig 对象 ≠ 相同量化参数:即使传入同一个 qconfig 对象,calibration 时每个模块仍会独立计算 scale/zero_point。必须手动共享 observer 或在模型设计时确保数据分布一致。
QAT 版本差异:不同版本的 horizon_plugin_pytorch API 可能有差异,请确认你的工具链版本:
import horizon_plugin_pytorch
print(horizon_plugin_pytorch.__version__)
如果上述方案无效,请提供:
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。
感谢回答,我这边先尝试一下
Marcelo6151 提到的共享 QuantStub/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 节点的方案,有结果随时同步 ![]()
请问我这样实现正确吗:
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)
你的代码有几个关键错误:
| 问题 | 说明 |
|---|---|
default_qat_8bit_fake_quant_qconfig 缺少括号 |
应该是 () 调用函数,不是引用 |
x1.qconfig = ... 在 forward 中无效 |
x1 是 Tensor 不是 Module,没有 qconfig 属性 |
qconfig 必须在 prepare_qat_fx 之前设置到 Module 上 |
|
FakeQuantize 模块 |
需要创建 FakeQuantize 实例并在 forward 中调用 |
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)
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 的数量
| 要点 | 说明 |
|---|---|
FakeQuantize 是 Module |
需要在 __init__ 中创建实例 |
self.shared_fake_quant(x) 而不是设置属性 |
|
prepare_qat_fx 会读取 Module 的 qconfig 属性 |
|
| 同一个 FakeQuantize 实例会共享量化参数 |
请提供:
pip show horizon-plugin-pytorch 版本信息perf_model 输出中 hz_cat_1_rescale_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 映射。
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)
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,
}
}
)
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 值相同,说明共享成功。
| 要点 | 说明 |
|---|---|
QConfig 不是直接传给 FakeQuantize |
QConfig 是 prepare_qat_fx 的配置参数 |
多个模块的 .qconfig 属性指向同一个 QConfig 对象 |
|
prepare_qat_fx 之前设置 |
prepare 时会读取模块的 qconfig 属性 |
copy.deepcopy |
避免原模型被修改 |
请提供:
pip show horizon-plugin-pytorch 的版本cat 操作前后的具体结构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 机制。
torch.ao.quantization 的 share_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。
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)
如果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"
正如 Marcelo6151 提到的,QAT 的深度优化需要 FAE 支持。不同版本的 horizon_plugin_pytorch API 差异很大,建议:
确认版本:
pip show horizon-plugin-pytorch
查看版本文档:
import horizon_plugin_pytorch
print(horizon_plugin_pytorch.__version__)
# 查看 FakeQuantize 的构造方式
import inspect
print(inspect.signature(horizon_plugin_pytorch.quantization.FakeQuantize.__init__))
提供以下信息给 FAE 或社区:
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}")
请先运行上述验证代码,告诉我:
horizon-plugin-pytorch 的版本FakeQuantize.__init__ 的参数签名FakeQuantize.from_qconfig)这样我才能给出准确的代码示例 ![]()
这种方式会报错:AttributeError: ‘MovingAverageMinMaxObserver’ object has no attribute ‘scale’
MovingAverageMinMaxObserver 在 calibration 完成前 确实没有 scale 属性,这是正常的。scale 是在 calibration 过程中通过统计数据计算出来的。
出现这个错误说明 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"
)
| 步骤 | 说明 |
|---|---|
set_fake_quantize(model, FakeQuantState.CALIBRATION) |
calibration 前必须设置,否则 observer 不更新统计量 |
model.eval() |
calibration 和 validation 都需要 eval 模式 |
set_fake_quantize(model, FakeQuantState.VALIDATION) |
calibration 后才能访问 scale |
| 单批数据可能导致统计量不准确 |
请运行以下诊断代码,告诉我输出:
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)}")
如果上述方案因版本问题无法使用,建议:
参考资源:
请先确认你的工具链版本,我们再针对性调整代码 ![]()
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 操作在量化图上不支持。这是版本兼容性问题。
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)
根据官方文档,不要手动插入 FakeQuantize,而是通过 prepare_qat_fx 的 qconfig_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)
根据 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(换 MinMaxObserver),有结果随时同步 ![]()
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,导致冲突。
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"
)
torch.ao.quantization 的 share_qconfig(如果版本支持)某些版本的 plugin 支持在 qconfig_dict 中使用特殊语法,但需要确认你的版本是否支持。
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"
| 要点 | 说明 |
|---|---|
QuantStub |
会导致 scale 冲突 |
QuantStub |
避免 trace 时的 scale 检查失败 |
| 强制共享统计量 | |
| 确保统计量准确 |
请提供:
pip show horizon-plugin-pytorch 的版本k_3x3, k_7x7, k_9x9 的来源)perf_model 输出的详细日志建议参考官方文档:QAT 快速上手(fx mode)
如果问题复杂,建议联系 FAE 支持(如 Marcelo6151 所说)![]()
pip show horizon-plugin-pytorch执行结果:
Name: horizon-plugin-pytorch
Version: 2.3.6+cu116.torch1130
Summary: Quantization aware training tools for horizon BPU