征程 6 H/P 工具链 QAT 精度调优

一、QAT 调优流程

流程总览:

针对征程 6H/P 的硬件特性,以 int8+int16+fp16 的混合精度量化为主要调优配置,会增加较多的 fp16 设置来优化量化精度

注意:

征程 6H/P 上会用到更多 fp16 高精度和 GEMM 类算子双 int16 等的配置,为了配置方式更加简单灵活,QAT 量化工具提供了一套新的 qconfig 量化配置模板,具体使用方式和注意事项参考:

【地平线 J6 工具链入门教程】QAT 新版 qconfig 量化模板使用教程

调优原则:

如上是一个标准的对称量化公式,产生误差的地方主要有:

  1. round 产生的舍入误差。例如:当采用 int8 量化,scale 为 0.0078 时,浮点数值 0.0157 对应的定点值为 round(0.0157 / 0.0078) = round(2.0128) = 2,浮点数值 0.0185 对应的定点值为 round(0.0185 / 0.0078) = round(2.3718) = 2,两者均产生了舍入误差,且由于舍入误差的存在,两者的定点值一致。 对于舍入误差,可以使用更小的 scale,这样可以使得单个定点值对应的浮点值范围变小。由于直接减小 scale 会导致截断误差,所以常用的方法是使用更高的精度类型,比如:将 int8 换成 int16,由于定点值范围变大, scale 将减小。
  2. clamp 产生的截断误差。当 qmax * scale 无法覆盖需要量化的数值范围时,可能产生较大截断误差。例如:当采用 int8 量化,scale 为 0.0078 时,qmax * scale = 127 * 0.0078 = 0.9906,大于 0.9906 的值对应的定点值将被截断到 127。 对于截断误差,可以使用更大的 scale。scale 一般是由量化工具使用统计方法得到,scale 偏小的原因是校准数据不够全,校准方法不对,导致 scale 统计的不合理。比如:某一输入的理论范围为 [-1, 1],但校准或 qat 过程中,没有观测到最大值为 1 或最小值为 -1 的样本或观测到此类样本的次数太少。应该增加此类数据或者根据数值范围,手动设置固定 scale。在截断误差不大的情况下,可以调整校准参数,通过不同的校准方法和超参缓解截断误差。

因此,QAT 量化精度调优以减少上述两种误差为基本原则,下文将针对 QAT 每个阶段做调优介绍:

注意:

征程 6H/P 平台的浮点模型量化友好设计以及 QAT 模型改造等内容和征程 6E/M 一致,仍可参考该文章对应章节:

【地平线 J6 工具链进阶教程】J6 E/M 工具链 QAT 精度调优

1.1 模型检查

完成模型改造和量化配置后,调用 Prepare 接口时会对模型做算子支持和量化配置上的检查,这些检查一定程度上反映了模型量化存在的问题。对于不支持的算子将以报错的形式提醒用户,一般有两种情况:

  1. 未正确进行模型的量化改造。Prepare 过程中 QAT 量化工具会对模型进行 trace 来获取完整的计算图,在这个过程中会完成算子替换等的优化,对于这些已替换的算子,输入输出类型如果是 torch.tensor 而非经过 QuantStub 转化后的 qtensor,则会触发不支持算子的报错,表现为 xxx is not implemented for QTensor
  2. 确实存在不支持的算子。工具链已支持业界大量的常用算子,但对于部分非常见算子的不支持情况,需考虑进行算子替换或者作为算子需求向工具链团队导入。

Prepare 运行成功后会在当前目录下自动保存模型检查文件 model_check_result.txtfx_graph.txt,建议参考下列解读顺序:

  1. 算子融合检查。算子融合作为 QAT 量化工具的标准优化手段,常见的融合组合为 Conv+ReLU+BN 和 Conv+Add 等,未融合的算子会在 txt 文件中给出,未按预期融合的算子可能是因为共享没有融合成功或者是 QAT 量化工具的融合逻辑变更(针对新版 qconfig 量化模板 enable_optimize=True 情况,见【地平线 J6 工具链入门教程】QAT 新版 qconfig 量化模板使用教程),需要检查代码,确认未融合的情况是否符合预期:
# 示例:未融合的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'>

未融合的算子对模型性能会有一定影响,对于精度的影响需视量化敏感度具体分析,一般来说,Conv/Linear+ReLU+BN 可能会因为算子复用导致未融合,此时建议手动修改融合;在 OE 3.5.0 以及之后版本使用新 qconfig 模板下,Conv+Add 默认不会融合,可不修改

  1. 共享模块检查。一个 module 只有一组量化参数,多次使用将会共享同一组量化参数,多次数据分布差异较大时,会产生较大误差:
# 示例:该共享模块被调用8次
Each module called times:
name      called times
---------  --------------   
...
model.map_head.sparse_head.decoder.gen_sineembed_for_position.div.reciprocal                          
8

called times > 1 的模块可能有很多个,全部改写成非共享是一劳永逸的。对于修改简单且精度影响大的共享算子如 QuantStub,强烈建议取消共享;对于 DeQuantStub 算子,共享不会对模型精度产生影响,但是会影响 Debug 结果的分析,也建议取消共享,修改方式参考征程 6E/M“模型改造”章节。

例如下面的共享模块,量化表示的最大值为 128 * 0.0446799 ≈ 5.719,在第一次使用中,输出范围明显小于 [-5.719, 5.719],误差较小, 第二次使用中,输出范围超出 [-5.719, 5.719],数值被截断,产生了较大误差。两次数值范围的差异也造成了统计出的 scale 不准确,因此该共享模块必须修改

+-+-+-+-+-+-+--+-+-+-+-+|   | 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)...

对于不带权重的 function 类算子都可以参考上面的拆分方式,但是也存在部分共享算子或模块带有权重参数拆分起来比较复杂,是否需要拆分建议先根据量化敏感度进行分析。带有权重参数算子拆分时需要复制权重,拆分方式可以参考:

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                2

此外,未调用的模块也会在文件中体现,called times 为 0,当 Calibration/QAT/模型导出出现 miss_key 时,可以检查模型中是否有模块未被 trace。

  1. 量化配置检查。txt 文件中会给出模型量化精度的统计信息:
# 算子输入量化精度统计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 转定点,建议获取最新版本
  • 对于 GEMM 类算子(Conv/Matmul/Linear)作为模型输出时支持高精度输出(征程 6E/M 支持 int32 输出,征程 6B/H/P 支持浮点输出),体现到这里则是 <class 'horizon_plugin_pytorch.nn.qat.stubs.DeQuantStub'> 的 input dtype 应为 torch.float16torch.float32,对于 qint8qint16 输入的 DeQuantStub 需要检查是否符合高精度输出的条件,符合条件但未高精度输出的需修改。此外对于下面左图的结构,也建议优化为右图结构来保证高精度输出的优化

  • qint8 和 qint16 算子的占比,可以协助判断是否配置全 int16 生效;torch.float16 算子的占比,可以协助判断是否配置 fp16 生效

txt 文件同时会给出逐层的量化配置信息:

# 激活逐层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) |

重点检查的信息有:

  • 每层算子的输入输出 dtype、权重的 dtype,是否符合量化配置;若和量化配置不符合,比如配置了 int16,但是算子显示为 int8,则需要关注下算子回退信息,例如在旧模板下 Conv+Add 融合时 Conv 不支持 int16 输入,会导致前序算子输出回退到 int8。新的 qconfig 量化配置模板下算子回退过程需查看 qconfig_changelogs.txt,详细参考:https://developer.horizon.auto/blog/13112
  • 配置了 fix scale 的算子,是否正确显示 FixedScaleObserver 信息,scale 值是否正确
  • 逐层算子的 observer 是否正确:权重默认 MinMaxObserver,QAT 校准时激活默认 MSEObserver,QAT 训练时激活默认 MinMaxObserver
  • 若为 QAT 训练阶段且配置了固定校准的激活 scale,查看 averaging_constant,判断是否生效,生效为 averaging_constant=0(即不更新 scale),默认为 0.01(更新 scale)

对于 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)  {}...

重点关注的 Graph 信息:

  • opcode 为算子调用类型
  • name 为当前算子名称,需注意和 model_check_result.txt 中的 module.submodule 名称区别
  • target 为算子输出
  • args 为算子输入

1.2 QAT 校准

1.2.1 int8+int16+fp16 混合精度调优

如果模型中吸收了前后处理的相关算子和操作,这部分默认需要 fp16 精度进行量化

对于 int8+int16+fp16 混合精度而言,主要的量化配置如下(配置方式参考【地平线 J6 工具链入门教程】QAT 新版 qconfig 量化模板使用教程):

  • 基础配置: TAE 算子(Conv/Matmul/Linear)双 int8、其他算子 fp16
  • 精度优化配置: TAE 算子(Conv/Matmul/Linear)单 int16(部分双 int16)、其他算子 fp16
  • 精度上限配置: TAE 算子(Conv/Matmul/Linear)双 int16、其他算子 fp16
  • 性能上限配置: 全局 int8,建议仅在测试模型最优性能(精度无保证)或作为高精度耗时优化的对比参考时配置

同样的对于较难量化的模型而言,初始应使用精度上限配置,在这个配置下解决量化流程可能的问题,优化量化风险较大的算子/模块,往往通过 Debug 工具进行定位,但在使用 Debug 工具较难定位到量化瓶颈时,可以使用分步量化的小技巧(参考本文最后章节"调优技巧"),也即对选中算子取消量化后对比精度,如定位到前后处理的算子/模块产生明显掉点,建议从模型中剥离;定位到模型中算子/模块,可以使用设置 fix_scale 和拆分共享模块等方式,或者从量化友好角度修改浮点模型(参考征程 6E/M 量化调优对应章节:【地平线 J6 工具链进阶教程】J6 E/M 工具链 QAT 精度调优

精度上限配置下的模型较难满足部署侧的延时要求,因此解决掉上述的量化瓶颈后需要回归到基础配置。在基础配置上通过敏感度的分析结果,增加 TAE 的 int16 算子,也就是精度优化配置。在基础配置和精度优化配置下精度达标的模型,视延时情况可能需要进一步做性能优化,主要方向为:

  1. 基础配置下,回退 fp16 性能瓶颈算子到低精度 int8
  2. 精度优化配置下,回退双 int16 的 TAE 算子到单 int16,回退 fp16 性能瓶颈算子到低精度 int8

精度优化配置下如果 int16 算子比例已超出部署预期但精度仍有一定差距,则可以考虑回退部分 int16 算子后尝试 QAT 训练;基础配置下精度表现距离浮点差距较小(量化精度/浮点精度 > 90%,经验值),直接尝试 QAT 训练,在 量化精度/浮点精度 >= 95%(经验值)的情况下,建议优先尝试固定校准激活 scale 的 QAT 训练(仅调整权重感知量化误差)

对于不同精度配置下的 QAT 校准,都有一些校准超参可以调整,需要用户结合具体模型去做调参优化,其中主要的参数有校准数据的 batch size、校准的 steps,详细的参数参考:

  1. 基础调优手段:调优指南_基础调优手段
  2. 高级调优手段:调优指南_高级调优手段

由于征程 6H/P 平台使用了较多浮点 FP16 精度,该精度下数值范围超限场景有以下常见的优化方法和优缺点总结:

image

总结:

int8+int16+fp16 混合精度调优的重点应放在 TAE 双 int16+ 其他算子 fp16 的调优上,这里需要把使用问题,量化不友好模块等等各种千奇百怪的问题都解决,看到模型的精度上限,然后根据模型部署的性能要求进行 TAE int8 和 int16 混合精度的调优,最后对非 TAE 算子进行 int8+fp16 混合精度的调优,最终达成部署精度和部署性能的平衡。

1.2.2 Debug 产出物解读

征程 6H/P 平台 Debug 产出物的解读和征程 6E/M 一致,仍可参考该文章对应章节:【地平线 J6 工具链进阶教程】J6 E/M 工具链 QAT 精度调优

Badcase 调优

对于实车或回灌反馈的可视化 badcase,利用 Debug 工具的调优流程为:

1.3 QAT 训练

大部分模型仅通过 QAT 校准就可以获得较好的量化精度,对于部分较难调优的模型,以及还需要继续优化误差类指标的模型,通常校准设置的高精度比例导致延时超过部署上限,但精度仍无法达标,这种情况可以尝试 QAT 训练来获得满足预期性能-精度平衡的量化模型。

根据前文所述,在 QAT 校准 量化精度/浮点精度 >= 95%(经验值) 的情况下,充分利用校准阶段较好的激活量化参数,优先尝试固定校准激活 scale 的 QAT 训练(仅调整权重感知量化误差),设置方式具体参考征程 6E/M 精度调优的“模型改造”章节:【地平线 J6 工具链进阶教程】J6 E/M 工具链 QAT 精度调优

参考浮点训练,QAT 训练在大部分配置保持和浮点训练一致的基础上,也涉及到部分超参的调整来提升量化训练的精度,例如 QAT 的学习率、weight_decay、迭代次数等,详细的参数调整策略参考:

  1. 基础调优手段:调优指南_基础调优手段
  2. 高级调优手段:调优指南_高级调优手段

浮点和 QAT 训练中都涉及到对 BN 的状态控制,在浮点训练中可能会采用 FreezeBN fine-tune 的方式来提升模型精度,在多任务训练中也会采用 FreezeBN 的技巧。因此在 QAT 训练中,提供了 FuseBN 和 WithBN 两种训练方式:

  1. FuseBN 即在 Prepare 后,QAT 训练前将 BN 的 weight 和 bias 吸收到 Conv 的 weight 和 bias 中,在训练过程中不再单独更新,这一吸收过程是无损的。FuseBN 也是 QAT 默认的训练方式。
  2. WithBN 则是在 QAT 训练阶段保持 Conv+BN 不融合,带着 BN 进行训练,BN 的参数单独更新,在训练结束后转成部署模型时再做融合。浮点训练阶段如果采用了 FreezeBN 的训练方式,QAT 训练时需设置 WithBN 来对齐浮点训练方式,设置方式如下:
from horizon_plugin_pytorch.qat_mode import QATMode, set_qat_mode
set_qat_mode(QATMode.WithBN)

通过观察 QAT 训练过程的 Loss 变化来初步判断 QAT 训练的量化效果,一般来说和浮点最后的 Loss 结果越接近越好,Loss 过大可能难以收敛,Loss 过小可能影响泛化性,对于异常的 Loss 建议的优化手段:

  1. 异常 INF 和 NAN 的 Loss 值,或者初始 Loss 极大且无收敛迹象,按如下顺序排查:
    1. 去掉 prepare 模型的步骤,用 qat pipeline finetune 浮点模型,排除训练 pipeline 的问题,Loss 如果仍异常,需要检查训练链路的配置如优化器 optimizer 和 lr_updater 等
    2. 保持当前 QAT 训练配置,只关闭伪量化节点后观察训练的 Loss 现象,理论上和浮点有微小差异
from horizon_plugin_pytorch.quantization import set_fake_quantize, FakeQuantState
...
set_fake_quantize(qat_model, FakeQuantState._FLOAT)
train(qat_model, qat_dataloader)
  1. 在排查完链路问题后出现初始 Loss 较大,有收敛迹象但收敛较慢,这种情况可以尝试调整学习率,延长 QAT 迭代次数,因为 QAT 训练本质上是对已收敛浮点模型的 fine-tune,本身存在一定的随机性,用较大的学习率可以快速波动到一个理想精度(依赖一些中间权重的评测)
  2. 对于少数模型,QAT 训练以及尝试了多次超参调整后精度仍无法达标,建议回归 QAT 校准阶段增加少量高精度算子(增加 GEMM 类算子 int16,以及其他算子增加 FP16)、回归浮点结构检查是否还存在量化不友好的结构如使用了大量 GeLU 等(参考征程 6E/M 精度调优对应章节【地平线 J6 工具链进阶教程】J6 E/M 工具链 QAT 精度调优

1.3.1 QAT 训练效率

由于 QAT 训练过程需要感知模型量化所带来的损失,因此模型中会被插入必要的量化相关的节点:数据观测节点 Observer 和伪量化节点 FakeQuant。数据观测节点会不断统计模型中数据的数值范围,伪量化节点会根据量化公式对数据做模拟量化和反量化,两者都会存在开销,此外就是 QAT 工具内部会对部分算子例如 LN 层做拆分算子的实现,因此相同配置下的 QAT 训练效率是会略低于浮点训练效率,具体还和模型参数规模、算子数量等有关。

对于用户可明显感知到的 QAT 训练效率降低,建议的优化手段有:

  1. 使用 QAT 工具提供的算子,这些算子优化了训练效率,例如 MultiScaleDeformableAttention(参考手册
  2. 更新到最新的 horizon-plugin-pytorch 版本,新版本会有持续的 bug fix 和新特性优化,如模型中某些结构或者算子训练耗时增加明显,可以向工具链团队导入

1.4. 模型导出部署

完成 QAT 精度调优后得到的模型仍是 PyTorch 模型,需要使用简单易用的接口来一步步导出编译成部署模型:PyTorch模型 -> export -> convert-> compile

export 得到 qat.bc; convert 得到 quantized.bc; compile 得到 hbm

由于导出生成物中计算差异的存在,对于每个生成物需简单验证其精度,可通过单张可视化或 mini 数据集,过程中如存在精度掉点,请参考【地平线 J6 工具链进阶教程】J6 E/M 工具链 QAT 精度一致性问题分析流程

二.调优技巧

2.1 分部量化

下面这种方式仅适用于 Calib 阶段,QAT 阶段因为模型已经适应了量化误差,关闭伪量化精度无法保证

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 训练

模型 QAT 训练时,要求模型为 train() 状态,此时若部分层冻结,则需要对应修改状态,参考代码如下:

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 值定位

出现 NaN 值可通过下面的修改在 calib/qat forward 过程中报错,从而定位到具体的算子:

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)

常见的可能出现 NaN 值的结构:

Multi-head Attention 的 attn mask,需要手动做数值的 clamp

posted @ 2026-01-20 12:09  地平线智能驾驶开发者  阅读(0)  评论(0)    收藏  举报