征程 6 H/P 工具链 QAT 精度调优
流程总览: 注意: 征程 6H/P 上会用到更多 fp16 高精度和 GEMM 类算子双 int16 等的配置,为了配置方式更加简单灵活,QAT 量化工具提供了一套新的 qconfig 量化配置模板,具体使用方式和注意事项参考: <u>【地平线 J6 工具链入门教程】QAT 新版 qconfig 量化模板使用教程</u> 调优原则: 如上是一个标准的对称量化公式,产生误差的地方主要有: 因此,QAT 量化精度调优以减少上述两种误差为基本原则,下文将针对 QAT 每个阶段做调优介绍: 注意: 征程 6H/P 平台的浮点模型量化友好设计以及 QAT 模型改造等内容和征程 6E/M 一致,仍可参考该文章对应章节: <u>【地平线 J6 工具链进阶教程】J6 E/M 工具链 QAT 精度调优</u> 完成模型改造和量化配置后,调用 Prepare 接口时会对模型做算子支持和量化配置上的检查,这些检查一定程度上反映了模型量化存在的问题。对于不支持的算子将以报错的形式提醒用户,一般有两种情况: Prepare 运行成功后会在当前目录下自动保存模型检查文件 未融合的算子对模型性能会有一定影响,对于精度的影响需视量化敏感度具体分析,一般来说,Conv/Linear+ReLU+BN 可能会因为算子复用导致未融合,此时建议手动修改融合;在 OE 3.5.0 以及之后版本使用新 qconfig 模板下,Conv+Add 默认不会融合,可不修改 called times > 1 的模块可能有很多个,全部改写成非共享是一劳永逸的。对于修改简单且精度影响大的共享算子如 QuantStub,强烈建议取消共享;对于 DeQuantStub 算子,共享不会对模型精度产生影响,但是会影响 Debug 结果的分析,也建议取消共享,修改方式参考征程 6E/M“模型改造”章节。 例如下面的共享模块,量化表示的最大值为 128 * 0.0446799 ≈ 5.719,在第一次使用中,输出范围明显小于 [-5.719, 5.719],误差较小, 第二次使用中,输出范围超出 [-5.719, 5.719],数值被截断,产生了较大误差。两次数值范围的差异也造成了统计出的 scale 不准确,因此该共享模块必须修改 上面共享算子的修改方式可以参考: 对于不带权重的 function 类算子都可以参考上面的拆分方式,但是也存在部分共享算子或模块带有权重参数拆分起来比较复杂,是否需要拆分建议先根据量化敏感度进行分析。带有权重参数算子拆分时需要复制权重,拆分方式可以参考: 上述共享算子修改生效后,在 此外,未调用的模块也会在文件中体现, 重点检查的信息有: txt 文件同时会给出逐层的量化配置信息: 重点检查的信息有: 对于 重点关注的 Graph 信息: 对于 int8+int16+fp16 混合精度而言,主要的量化配置如下(配置方式参考<u>【地平线 J6 工具链入门教程】QAT 新版 qconfig 量化模板使用教程</u>): 同样的对于较难量化的模型而言,初始应使用精度上限配置,在这个配置下解决量化流程可能的问题,优化量化风险较大的算子/模块,往往通过 Debug 工具进行定位,但在使用 Debug 工具较难定位到量化瓶颈时,可以使用分步量化的小技巧(参考本文最后章节"调优技巧"),也即对选中算子取消量化后对比精度,如定位到前后处理的算子/模块产生明显掉点,建议从模型中剥离;定位到模型中算子/模块,可以使用设置 fix\_scale 和拆分共享模块等方式,或者从量化友好角度修改浮点模型(参考征程 6E/M 量化调优对应章节:<u>【地平线 J6 工具链进阶教程】J6 E/M 工具链 QAT 精度调优</u>) 精度上限配置下的模型较难满足部署侧的延时要求,因此解决掉上述的量化瓶颈后需要回归到基础配置。在基础配置上通过敏感度的分析结果,增加 TAE 的 int16 算子,也就是精度优化配置。在基础配置和精度优化配置下精度达标的模型,视延时情况可能需要进一步做性能优化,主要方向为: 精度优化配置下如果 int16 算子比例已超出部署预期但精度仍有一定差距,则可以考虑回退部分 int16 算子后尝试 QAT 训练;基础配置下精度表现距离浮点差距较小( 对于不同精度配置下的 QAT 校准,都有一些校准超参可以调整,需要用户结合具体模型去做调参优化,其中主要的参数有校准数据的 batch size、校准的 steps,详细的参数参考: 由于征程 6H/P 平台使用了较多浮点 FP16 精度,该精度下数值范围超限场景有以下常见的优化方法和优缺点总结: 总结: int8+int16+fp16 混合精度调优的重点应放在 TAE 双 int16+ 其他算子 fp16 的调优上,这里需要把使用问题,量化不友好模块等等各种千奇百怪的问题都解决,看到模型的精度上限,然后根据模型部署的性能要求进行 TAE int8 和 int16 混合精度的调优,最后对非 TAE 算子进行 int8+fp16 混合精度的调优,最终达成部署精度和部署性能的平衡。 征程 6H/P 平台 Debug 产出物的解读和征程 6E/M 一致,仍可参考该文章对应章节:<u>【地平线 J6 工具链进阶教程】J6 E/M 工具链 QAT 精度调优</u> 对于实车或回灌反馈的可视化 badcase,利用 Debug 工具的调优流程为: 大部分模型仅通过 QAT 校准就可以获得较好的量化精度,对于部分较难调优的模型,以及还需要继续优化误差类指标的模型,通常校准设置的高精度比例导致延时超过部署上限,但精度仍无法达标,这种情况可以尝试 QAT 训练来获得满足预期性能-精度平衡的量化模型。 根据前文所述,在 QAT 校准 参考浮点训练,QAT 训练在大部分配置保持和浮点训练一致的基础上,也涉及到部分超参的调整来提升量化训练的精度,例如 QAT 的学习率、weight\_decay、迭代次数等,详细的参数调整策略参考: 浮点和 QAT 训练中都涉及到对 BN 的状态控制,在浮点训练中可能会采用 FreezeBN fine-tune 的方式来提升模型精度,在多任务训练中也会采用 FreezeBN 的技巧。因此在 QAT 训练中,提供了 FuseBN 和 WithBN 两种训练方式: 通过观察 QAT 训练过程的 Loss 变化来初步判断 QAT 训练的量化效果,一般来说和浮点最后的 Loss 结果越接近越好,Loss 过大可能难以收敛,Loss 过小可能影响泛化性,对于异常的 Loss 建议的优化手段: 异常 INF 和 NAN 的 Loss 值,或者初始 Loss 极大且无收敛迹象,按如下顺序排查: 由于 QAT 训练过程需要感知模型量化所带来的损失,因此模型中会被插入必要的量化相关的节点:数据观测节点 Observer 和伪量化节点 FakeQuant。数据观测节点会不断统计模型中数据的数值范围,伪量化节点会根据量化公式对数据做模拟量化和反量化,两者都会存在开销,此外就是 QAT 工具内部会对部分算子例如 LN 层做拆分算子的实现,因此相同配置下的 QAT 训练效率是会略低于浮点训练效率,具体还和模型参数规模、算子数量等有关。 对于用户可明显感知到的 QAT 训练效率降低,建议的优化手段有: 完成 QAT 精度调优后得到的模型仍是 PyTorch 模型,需要使用简单易用的接口来一步步导出编译成部署模型: 由于导出生成物中计算差异的存在,对于每个生成物需简单验证其精度,可通过单张可视化或 mini 数据集,过程中如存在精度掉点,请参考<u>【地平线 J6 工具链进阶教程】J6 E/M 工具链 QAT 精度一致性问题分析流程</u> 下面这种方式仅适用于 Calib 阶段,QAT 阶段因为模型已经适应了量化误差,关闭伪量化精度无法保证 模型 QAT 训练时,要求模型为 train() 状态,此时若部分层冻结,则需要对应修改状态,参考代码如下: 出现 NaN 值可通过下面的修改在 calib/qat forward 过程中报错,从而定位到具体的算子: 常见的可能出现 NaN 值的结构: Multi-head Attention 的 attn mask,需要手动做数值的 clamp一、QAT 调优流程
针对征程 6H/P 的硬件特性,以 int8+int16+fp16 的混合精度量化为主要调优配置,会增加较多的 fp16 设置来优化量化精度


1.1 模型检查
xxx is not implemented for QTensor;model_check_result.txt 和 fx_graph.txt,建议参考下列解读顺序:# 示例:未融合的Conv+Add算子
Fusable modules are listed below:
name type------ -------------------------
model.view_transformation.input_proj.0.0(shared)
<class'horizon_plugin_pytorch.nn.qat.conv2d.Conv2d'>
model.view_transformation._generated_add_0
<class'horizon_plugin_pytorch.nn.qat.functional_modules.FloatFunctional'># 示例:该共享模块被调用8次
Each module called times:
name called times
--------- --------------
...
model.map_head.sparse_head.decoder.gen_sineembed_for_position.div.reciprocal
8+-+-+-+-+-+-+--+-+-+-+-+| | mod_name | base_op_type | analy_op_type | shape | quant_dtype | qscale |base_model_min | analy_model_min | base_model_max | analy_model_max ||-+-+--+-+-+-+-+-+-+-+-+...| 1227 | model.map_head.sparse_head.decoder.gen_sineembed_for_position.div | horizon_plugin_pytorch.nn.div.Div | horizon_plugin_pytorch.nn.qat.functional_modules.FloatFunctional.mul | torch.Size([1, 1600, 128])| qint8 | 0.0446799 | 0.0002146 | 0.0000000 | 4.5935526 | 4.5567998 |...| 1520 | model.map_head.sparse_head.decoder.gen_sineembed_for_position.div | horizon_plugin_pytorch.nn.div.Div | horizon_plugin_pytorch.nn.qat.functional_modules.FloatFunctional.mul | torch.Size([1, 1600, 128]) | qint8 | 0.0446799 | 0.0000000 | 0.0000000 | 6.2831225 | 5.7190272 |...class Model(nn.Module):def __init__(self, ) -> None:super().__init__()...
self.steps = 2for step in range(self.steps):setattr(self, f'div{step}', FloatFunctional())def forward(self, data):...for step in range(self.steps):
data = getattr(self, f'div{step}').div(x)...class Model(nn.Module):def __init__(self, ) -> None:super().__init__()...
self.steps = 3
self.conv0 = nn.Conv2d(...)
shared_weight = self.conv0.weight
shared_bias = self.conv0.bias
for step in range(1, self.steps):setattr(self, f'conv{step}', nn.Conv2d(...))getattr(self, f'conv{step}').weight = shared_weight
getattr(self, f'conv{step}').bias = shared_bias
def forward(self, data):...for step in range(self.steps):
data = getattr(self, f'conv{step}')(x)...model_check_result.txt 文件中可见到无该算子共享相关的信息:# 修改生效后下面信息将不再显示
Modules below are used multi times:
name called times
------ --------------
xxxxx 2called times 为 0,当 Calibration/QAT/模型导出出现 miss\_key 时,可以检查模型中是否有模块未被 trace。# 算子输入量化精度统计input dtype statistics:+---+--+--+--+| module type | torch.float32 | qint8 | qint16 ||---+---+--+--+| <class 'horizon_plugin_pytorch.nn.qat.stubs.QuantStub'> | 290 | 15 | 0 || <class 'horizon_plugin_pytorch.nn.qat.linear.Linear'> | 5 | 117 | 9 || <class 'horizon_plugin_pytorch.nn.qat.stubs.DeQuantStub'> | 0 | 8 | 0 |...# 算子输出量化精度统计
output dtype statistics:+---+--+--+--+| module type | torch.float32 | qint8 | qint16 ||---+--+--+--+| <class 'horizon_plugin_pytorch.nn.qat.stubs.QuantStub'> | 0 | 123 | 182 |...# 使用fp16量化精度的算子,量化精度统计+---+--+--+--+--+| module type | torch.float32 | qint8 | qint16 | torch.float16 ||-----+--+--+--+--|| <class 'horizon_plugin_pytorch.nn.qat.stubs.QuantStub'> | 34 | 0 | 0 | 0 || <class 'torch.nn.modules.padding.ZeroPad2d'> | 0 | 11 | 0 | 0 || <class 'horizon_plugin_pytorch.nn.qat.functional_modules.FloatFunctional'> | 48 | 14 | 9 | 50 |...<class 'horizon_plugin_pytorch.nn.qat.stubs.QuantStub'> 的 input dtype 应为 torch.float32,对于 qint8 或者 qint16 的 input dtype,一般是冗余的 QuantStub 算子可以改掉,不会对精度产生影响但可能会对部署模型性能有影响(算子数量)torch.float32 的输入精度(除下文 c 情况),如上图的 <class 'horizon_plugin_pytorch.nn.qat.linear.Linear'>,需要检查是否漏插 QuantStub 未转定点,未转定点的算子在导出部署模型时会 cpu 计算从而影响模型性能。对于模型中的一些浮点常量 tensor,工具已支持自动插入 QuantStub 转定点,建议获取最新版本<class 'horizon_plugin_pytorch.nn.qat.stubs.DeQuantStub'> 的 input dtype 应为 torch.float16 或 torch.float32,对于 qint8 或 qint16 输入的 DeQuantStub 需要检查是否符合高精度输出的条件,符合条件但未高精度输出的需修改。此外对于下面左图的结构,也建议优化为右图结构来保证高精度输出的优化
# 激活逐层qconfig
Each layer out qconfig:+--+--+--+--+--+--+| Module Name| Module Type | Input dtype | out dtype | ch_axis | observer ||--+--+--+--+--+---|# 固定scale| quant | <class 'horizon_plugin_pytorch.nn.qat.stubs.QuantStub'> | [torch.float32] | ['qint16']| -1 | FixedScaleObserver(scale=tensor([3.0518e-05], device='cuda:0'),zero_point=tensor([0], device='cuda:0')) |# QAT训练激活scale更新| mod2.1.attn.q | <class 'horizon_plugin_pytorch.nn.qat.conv2d.Conv2d'> | ['qint16'] | ['qint16'] | -1 | MinMaxObserver(averaging_constant=0.01) |# QAT训练激活scale不更新| mod2.1.FFN.out_conv.1.0| <class 'horizon_plugin_pytorch.nn.qat.conv2d.Conv2d'> | ['qint16']| ['qint16']| -1| MinMaxObserver(averaging_constant=0) |# 激活fp16 qconfig| bev_fusion.multi_view_cross_attn.32.global_cross_window_attn._generated_add_2[add]| <class 'horizon_plugin_pytorch.nn.qat.functional_modules.FloatFunctional'> | [torch.float16, torch.float32] | [torch.float16] | FakeCast(dtype=torch.float16, min_val=-0.0009765625, max_val=0.0009765625) | |# 权重逐层qconfig
Weight qconfig:+-----+----+-----+------+---+| Module Name | Module Type | weight dtype|ch_axis|observer ||---+-------+----+----+---|| mod1.0 | <class 'horizon_plugin_pytorch.nn.qat.conv2d.Conv2d'> |qint8 | 0 | MinMaxObserver(averaging_constant=0.01) |fx_graph.txt,可以从中获取到模型中 op/module 的上下游调用关系,例如当存在算子 called times 为 0 未被调用的情况,可以通过 Graph 定位到上下文算子从而定位未被调用的原因(通常因为在 init 函数中定义了但在 forward 中没有调用,也可能存在逻辑判断或循环次数变化的情况);此外当出现导出的部署模型(bc 模型)精度异常,也可以通过 Graph 信息来排查是否是导出计算图改变导致的# 模型Graph图结构信息
Graph:
opcode name target args kwargs
---- ----- ------- ------- -------
placeholder input_0 input_0 () {}
call_module quant quant (input_0,) {}
call_module traj_decoder_src_proj_0_0 traj_decoder_src_proj.0.0 (quant,) {}
call_function scope_end <function Tracer.scope_end at 0x7f4477d7dc60> ('traj_decoder_src_proj.0',) {}
call_function __get__ <method-wrapper '__get__' of getset_descriptor object at 0x7f460922b800> (traj_decoder_src_proj_0_0,) {}
call_function __getitem__ <slot wrapper '__getitem__' of 'torch.Size' objects> (__get__, 0) {}
call_function __getitem___1 <slot wrapper '__getitem__' of 'torch.Size' objects> (__get__, 1) {}
call_function __getitem___2 <slot wrapper '__getitem__' of 'torch.Size' objects> (__get__, 2) {}
call_function __getitem___3 <slot wrapper '__getitem__' of 'torch.Size' objects> (__get__, 3) {}
call_function permute <method 'permute' of 'torch._C.TensorBase' objects> (traj_decoder_src_proj_0_0, 0, 2, 3, 1) {}...opcode 为算子调用类型name 为当前算子名称,需注意和 model_check_result.txt 中的 module.submodule 名称区别target 为算子输出args 为算子输入1.2 QAT 校准
1.2.1 int8+int16+fp16 混合精度调优
如果模型中吸收了前后处理的相关算子和操作,这部分默认需要 fp16 精度进行量化
量化精度/浮点精度 > 90%,经验值),直接尝试 QAT 训练,在 量化精度/浮点精度 >= 95%(经验值)的情况下,建议优先尝试固定校准激活 scale 的 QAT 训练(仅调整权重感知量化误差)
1.2.2 Debug 产出物解读
Badcase 调优

1.3 QAT 训练
量化精度/浮点精度 >= 95%(经验值) 的情况下,充分利用校准阶段较好的激活量化参数,优先尝试固定校准激活 scale 的 QAT 训练(仅调整权重感知量化误差),设置方式具体参考征程 6E/M 精度调优的“模型改造”章节:<u>【地平线 J6 工具链进阶教程】J6 E/M 工具链 QAT 精度调优</u>from horizon_plugin_pytorch.qat_mode import QATMode, set_qat_mode
set_qat_mode(QATMode.WithBN)from horizon_plugin_pytorch.quantization import set_fake_quantize, FakeQuantState
...
set_fake_quantize(qat_model, FakeQuantState._FLOAT)
train(qat_model, qat_dataloader)1.3.1 QAT 训练效率
1.4. 模型导出部署
PyTorch模型 -> export -> convert-> compileexport 得到 qat.bc; convert 得到 quantized.bc; compile 得到 hbm
二.调优技巧
2.1 分部量化
from horizon_plugin_pytorch.utils.quant_switch import GlobalFakeQuantSwitch
class Model(nn.Module):
def _init_(...):
def forward(self, x):
x = self.quant(x)
x = self.backbone(x)
x = self.neck(x)
GlobalFakeQuantSwitch.disable() # 使伪量化失效 # --------- float32 ---------
x = self.head(x)
# ---------------------------
GlobalFakeQuantSwitch.enable() # 重新打开伪量化 return self.dequant(x)2.2 部分层冻结下的 QAT 训练
from horizon_plugin_pytorch.quantization import (
QuantStub,
prepare,
set_fake_quantize,
FakeQuantState,)
qat_model = prepare(model, example_inputs=xxx, qconfig_setter=(xxx))
qat_model.load_state_dict("calib_model_ckpt.pth")
qat_model.train()# 关闭requires_grad可固定权重不更新,但Drop、BN仍然会更新for param in qat_model.backbone.parameters():
param.requires_grad = False# 配置eval()可固定Drop、BN不更新,但不会固定权重,因此两者需要配合使用
qat_model.backbone.eval()
set_fake_quantize(qat_model.backbone, FakeQuantState.VALIDATION)#配置head的FakeQuant为QAT状态
set_fake_quantize(qat_model.head, FakeQuantState.QAT)2.3 Calib/QAT 过程 NaN 值定位
from horizon_plugin_pytorch.quantization.fake_quantize import FakeQuantize
FakeQuantize.check_nan_scale='forward'#默认为save,在torch.save时检查是否有nan,有nan会报错
qat_model = prepare(model, (input), default_qat_qconfig_setter)