专栏算法工具链【地平线 J6 工具链进阶教程】J6 H/P 工具链 QAT 精度调优

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

芯链情报局2025-10-22
295
0

QAT调优流程

流程总览:

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

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

[【地平线 J6工具链入门教程】QAT新版qconfig量化模板使用教程](https://developer.horizon.auto/blog/13112 '地平线 J6工具链入门教程】QAT新版qconfig量化模板使用教程')

调优原则:

Description

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

  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 将减小。

  1. 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每个阶段做调优介绍:

注意:
J6H/P平台的浮点模型量化友好设计以及QAT模型改造等内容和J6E/M一致,仍可参考该文章对应章节:
[【地平线 J6工具链进阶教程】J6 E/M工具链QAT精度调优](https://developer.horizon.auto/blog/13132 '【地平线 J6工具链进阶教程】J6 E/M工具链QAT精度调优')

1. 模型检查

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

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

Prepare运行成功后会在当前目录下自动保存模型检查文件model_check_result.txt和fx_graph.txt,建议参考下列解读顺序:
  1. 算子融合检查。算子融合作为QAT量化工具的标准优化手段,常见的融合组合为Conv+ReLU+BN和Conv+Add等,未融合的算子会在txt文件中给出,未按预期融合的算子可能是因为共享没有融合成功或者是QAT量化工具的融合逻辑变更(针对新版qconfig量化模板enable_optimize=True情况,见【地平线 J6工具链入门教程】QAT新版qconfig量化模板使用教程),需要检查代码,确认未融合的情况是否符合预期:

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

  1. 共享模块检查。一个 module 只有一组量化参数,多次使用将会共享同一组量化参数,多次数据分布差异较大时,会产生较大误差:

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

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

上面共享算子的修改方式可以参考:

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

上述共享算子修改生效后,在model_check_result.txt文件中可见到无该算子共享相关的信息:
此外,未调用的模块也会在文件中体现,called times为0,当Calibration/QAT/模型导出出现miss_key时,可以检查模型中是否有模块未被trace。
  1. 量化配置检查。txt文件中会给出模型量化精度的统计信息:

重点检查的信息有:

  • a. <class 'horizon_plugin_pytorch.nn.qat.stubs.QuantStub'>的input dtype应为torch.float32,对于qint8或者qint16的input dtype,一般是冗余的QuantStub算子可以改掉,不会对精度产生影响但可能会对部署模型性能有影响(算子数量)
  • b. 正常来说模型中的算子不应出现torch.float32的输入精度(除下文c情况),如上图的<class 'horizon_plugin_pytorch.nn.qat.linear.Linear'>,需要检查是否漏插QuantStub未转定点,未转定点的算子在导出部署模型时会cpu计算从而影响模型性能。对于模型中的一些浮点常量tensor,工具已支持自动插入QuantStub转定点,建议获取最新版本
  • c. 对于GEMM类算子(Conv/Matmul/Linear)作为模型输出时支持高精度输出(J6E/M支持int32输出,J6B/H/P支持浮点输出),体现到这里则是<class 'horizon_plugin_pytorch.nn.qat.stubs.DeQuantStub'>的input dtype应为torch.float16或torch.float32,对于qint8或qint16输入的DeQuantStub需要检查是否符合高精度输出的条件,符合条件但未高精度输出的需修改。此外对于下面左图的结构,也建议优化为右图结构来保证高精度输出的优化
    Description
  • d. qint8和qint16算子的占比,可以协助判断是否配置全int16生效;torch.float16算子的占比,可以协助判断是否配置fp16生效

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

重点检查的信息有:

  • a. 每层算子的输入输出dtype、权重的dtype,是否符合量化配置;若和量化配置不符合,比如配置了int16,但是算子显示为int8,则需要关注下算子回退信息,例如在旧模板下Conv+Add融合时Conv不支持int16输入,会导致前序算子输出回退到int8。新的qconfig量化配置模板下算子回退过程需查看qconfig_changelogs.txt,详细参考:https://developer.horizon.auto/blog/13112
  • b. 配置了fix scale的算子,是否正确显示FixedScaleObserver信息,scale值是否正确

  • c. 逐层算子的observer是否正确:权重默认MinMaxObserver,QAT校准时激活默认MSEObserver,QAT训练时激活默认MinMaxObserver

  • d. 若为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信息:

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

2. QAT校准

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

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

对于int8+int16+fp16混合精度而言,主要的量化配置如下(配置方式参考【地平线 J6工具链入门教程】QAT新版qconfig量化模板使用教程):
  • 基础配置: TAE算子(Conv/Matmul/Linear)双int8、其他算子fp16、Sin/Cos算子fp32
  • 精度优化配置: TAE算子(Conv/Matmul/Linear)单int16(部分双int16)、其他算子fp16、Sin/Cos算子fp32
  • 精度上限配置: TAE算子(Conv/Matmul/Linear)双int16、其他算子fp16、Sin/Cos算子fp32
  • 性能上限配置: 全局int8,建议仅在测试模型最优性能(精度无保证)或作为高精度耗时优化的对比参考时配置
同样的对于较难量化的模型而言,初始应使用精度上限配置,在这个配置下解决量化流程可能的问题,优化量化风险较大的算子/模块,往往通过Debug工具进行定位,但在使用Debug工具较难定位到量化瓶颈时,可以使用分步量化的小技巧(参考本文最后章节"调优技巧"),也即对选中算子取消量化后对比精度,如定位到前后处理的算子/模块产生明显掉点,建议从模型中剥离;定位到模型中算子/模块,可以使用设置fix_scale和拆分共享模块等方式,或者从量化友好角度修改浮点模型(参考J6E/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. 高级调优手段:调优指南_高级调优手段

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

方案

优点

缺点

直接手动 clip(仅数值范围超限)

1. 简单有效

1. 适用范围有限(grid 范围,sum 等)

使用 FP32

1. 直接有效
2. 方便流程自动化
1. 当前算子的输入输出都需要 FP32,bpu支持的算子范围小
2. 最坏情况下,需要设置多个连续的 FP32 算子,量化模型性能存在风险

回退 int16

1. 在某些值域范围内,表示精度高于 FP16

1. 存在 INT16 和 FP16 交错出现的情况,会出现较多 quant/dequant,存在性能风险
2. 出现截断时,需要手动设置固定 scale

放缩

1. 性能优于 FP32

1. 需要手动修改模型结构,难以自动化
2. 仅适用线性变化

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

2.2 Debug产出物解读

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

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

Description

3. QAT训练

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

根据前文所述,在QAT校准量化精度/浮点精度 >= 95%(经验值)的情况下,充分利用校准阶段较好的激活量化参数,优先尝试固定校准激活scale的QAT训练(仅调整权重感知量化误差),设置方式具体参考J6E/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来对齐浮点训练方式,设置方式如下:

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

  1. 异常INF和NAN的Loss值,或者初始Loss极大且无收敛迹象,按如下顺序排查:

    • a. 去掉 prepare 模型的步骤,用 qat pipeline finetune 浮点模型,排除训练 pipeline 的问题,Loss如果仍异常,需要检查训练链路的配置如优化器optimizer和lr_updater等

    • b. 保持当前QAT训练配置,只关闭伪量化节点后观察训练的Loss现象,理论上和浮点有微小差异

    • c. lr 设置为 0,进行 qat 训练,排除参数调整不到位的问题。qat 训练的精度应该与 calibration 精度几乎一致

    • d. 此外还需要检查是否使用了特殊的数据增强策略(如旋转、马赛克等会改变真实的数据分布)

  2. 在排查完链路问题后出现初始Loss较大,有收敛迹象但收敛较慢,这种情况可以尝试调整学习率,延长QAT迭代次数,因为QAT训练本质上是对已收敛浮点模型的fine-tune,本身存在一定的随机性,用较大的学习率可以快速波动到一个理想精度(依赖一些中间权重的评测)

  3. 对于少数模型,QAT训练以及尝试了多次超参调整后精度仍无法达标,建议回归QAT校准阶段增加少量高精度算子(增加GEMM类算子int16,以及其他算子增加FP16)、回归浮点结构检查是否还存在量化不友好的结构如使用了大量GeLU等(参考J6E/M精度调优对应章节【地平线 J6工具链进阶教程】J6 E/M工具链QAT精度调优

3.1 QAT训练效率

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

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

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

4. 模型导出部署

完成QAT精度调优后得到的模型仍是PyTorch模型,需要使用简单易用的接口来一步步导出编译成部署模型:
PyTorch模型 -> export -> convert-> compile
export得到qat.bc;
convert得到quantized.bc;
compile得到hbm
由于导出生成物中计算差异的存在,对于每个生成物需简单验证其精度,可通过单张可视化或mini数据集,过程中如存在精度掉点,请参考【地平线 J6工具链进阶教程】J6 E/M工具链QAT 精度一致性问题分析流程

调优技巧

1. 分部量化

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

2. 部分层冻结下的QAT训练

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

3. Calib/QAT过程NaN值定位

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

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

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

Description
算法工具链
社区征文官方教程征程6
评论0
0/1000