2. 量化感知训练 (QAT)

2.1. 基础概念

1. 什么是 plugin?

答:Horizon Plugin Pytorch (后文简称 Plugin ) 是地平线参考 PyTorch 官方量化接口和思路设计的 QAT 量化训练工具, 相关文档请参考:工具链用户手册第 7 章节


2.2. 训练环境

1. Docker 容器无法使用 Nvidia 资源

答:可能为 Docker 启动时未指定 nvidia 参数导致,建议使用以下形式启动:

nvidia-docker run -it --shm-size="15g" \
    -v `pwd`: openexplorer/ai_toolchain_centos_7_j5:v$version$ /bin/bash

2. Docker 容器中无法 import hat

答:若您在 GPU Docker 中运行示例提示 GPU RuntimeError,或者加载 hat 出现 ModuleNotFoundError: No module named 'hat'Segmentation fault (core dumped) 等类似报错,那么可以按照如下两个方向进行排查:
  1. 使用 OE 包中提供的 run_docker.sh 脚本启动 GPU Docker 容器;

  2. 在容器中执行如下脚本来检测 torch 和 cuda 是否正常。若 CUDA 无法调用,可以使用 nvidia-smi 检查下驱动版本是否符合要求。因为 OE 包自 v1.1.60 版本开始,升级了 torch 环境为 1.1.30+cu116,对应的驱动要求可见:用户手册 4.1.2.1 章节

import torch
import time
print(torch.__version__)
print(torch.cuda.is_available())
a = torch.randn(10000, 1000)
b = torch.randn(1000, 2000)
t0 = time.time()
c = torch.matmul(a, b)
t1 = time.time()
print(a.device, t1 - t0, c.norm(2))
device = torch.device('cuda')
a = a.to(device)
b = b.to(device)
t0 = time.time()
c = torch.matmul(a, b)
t2 = time.time()
print(a.device, t2 - t0, c.norm(2))
t0 = time.time()
c = torch.matmul(a, b)
t2 = time.time()
print(a.device, t2 - t0, c.norm(2))

2.3. QAT量化训练

1. 为什么开启多机训练后精度表现变差?

答:开启多机训练后 batchsize 成倍增大,此时需要同步调整 LR 等超参来进行平衡。

2. Qconfig 是否需要用户干预?

答:地平线提供的 Qconfig 定义了 activation 和 weight 如何进行量化,目前支持 FakeQuantizeLSQPACT 等量化算法。 若都不能达到很好的效果也支持用户自定义,但这可能会触发 QAT 精度、工具链不支持后续转换等问题, 因此建议改动前先与地平线沟通。

3. 如何导出各阶段的 ONNX 模型?

答:请参考如下代码实现:

from horizon_plugin_pytorch.utils import onnx_helper as horizon_onnx_helper

# [Optional] Export float 、qat net to ONNX
# --------------------------------------------------------------------
logging.info("Export qat model to ONNX...")
data = torch.rand((1, 3, 228, 228), device=device)
horizon_onnx_helper.export_to_onnx(qat_net, data, "resnet_qat.onnx")

# [Optional] Export quantized_model to ONNX
# --------------------------------------------------------------------
horizon_onnx_helper.export_quantized_onnx(
    quantized_model, data, "resnet_quantized.onnx"
)

4. 为什么 QAT 训练时存在 nan?

答:该问题的影响因素较多,建议从以下几个方面检查:
  1. 检查输入数据是否含 nan;

  2. 检查浮点模型是否收敛。未收敛的浮点模型针对某些微量化误差可能会导致很大波动;

  3. 检查是否开启 calib。建议开启,可以给模型更好的初始系数;

  4. 检查训练策略是否适合。不适合的训练策略也 会导致出现 NAN 值,例如学习率 lr 过大(可通过调低学习率或使用梯度截断方式)等。在训练策略上,默认 QAT 与浮点保持一致,如果浮点训练采用的是 OneCycle 等会影响 LR 设置的优化器,建议使用 SGD 替换。

5. 配置 int16 节点或高精度输出节点无效

答:该现象可能是因为错误配置 module_name 导致,module_name 字段只支持 string,不支持按 index 索引进行配置。

6. 如何查看某一层是否开启了高精度输出?

答:可以打印 qat_model 的所在层,查看该层是否有 (activation_post_process): FakeQuantize,若没有,则说明其为高精度输出。 例如 int32 高精度 conv 打印如下:

(1): ConvModule2d(
  (0): Conv2d(
    64, 3, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)
    (weight_fake_quant): FakeQuantize(
      fake_quant_enabled=tensor([1], dtype=torch.uint8), observer_enabled=tensor([1], dtype=torch.uint8),
      quant_min=-128, quant_max=127, dtype=qint8, qscheme=torch.per_channel_symmetric, ch_axis=0,
      scale=tensor([1., 1., 1.]), zero_point=tensor([0, 0, 0])
      (activation_post_process): MovingAveragePerChannelMinMaxObserver(min_val=tensor([]), max_val=tensor([]))
    )
  )
)

int8 低精度 conv 打印如下:

(0): ConvModule2d(
  (0): ConvReLU2d(
    64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)
    (weight_fake_quant): FakeQuantize(
      fake_quant_enabled=tensor([1], dtype=torch.uint8), observer_enabled=tensor([1], dtype=torch.uint8),
      quant_min=-128, quant_max=127, dtype=qint8, qscheme=torch.per_channel_symmetric, ch_axis=0,
      scale=tensor([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
                    1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
                    1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
                    1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]),
      zero_point=tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
      (activation_post_process): MovingAveragePerChannelMinMaxObserver(min_val=tensor([]), max_val=tensor([]))
    )
    (activation_post_process): FakeQuantize(
      fake_quant_enabled=tensor([1], dtype=torch.uint8), observer_enabled=tensor([1], dtype=torch.uint8),
      quant_min=-128, quant_max=127, dtype=qint8, qscheme=torch.per_tensor_symmetric, ch_axis=-1,
      scale=tensor([1.]), zero_point=tensor([0])
      (activation_post_process): MovingAverageMinMaxObserver(min_val=tensor([]), max_val=tensor([]))
    )
  )
)

7. 辅助分支能否插入伪量化节点?

答:建议仅对部署上板的模型部分插入伪量化节点。由于 QAT 训练为全局训练,辅助分支的存在会导致训练难度增加,若辅助分支处的数据分布与其他分支差异较大还会加大精度风险,建议去除。

8. 如何将地平线 gridsample 算子改写为 torch 公版实现?

答:horizon_plugin_pytorch 中的 gridsample 算子非公版实现,其 grid 输入(输入2)是 int16 类型的绝对坐标,而torch 公版是 float32 类型的归一化坐标,范围是 [-1, 1]

因此,从 torch.nn.functional.grid_sample 路径导入 grid_sample 算子后,可以通过如下方式归一化 grid:

def norm_grid(x, grid):
  n = grid.size(0)
  h = grid.size(1)
  w = grid.size(2)

  base_coord_y = (
      torch.arange(h, dtype=grid.dtype, device=grid.device)
      .unsqueeze(-1)
      .unsqueeze(0)
      .expand(n, h, w)
  )

  base_coord_x = (
      torch.arange(w, dtype=grid.dtype, device=grid.device)
      .unsqueeze(0)
      .unsqueeze(0)
      .expand(n, h, w)
  )

  absolute_grid_x = grid[:, :, :, 0] + base_coord_x
  absolute_grid_y = grid[:, :, :, 1] + base_coord_y
  norm_grid_x = absolute_grid_x * 2 / (x.size(3) - 1) - 1
  norm_grid_y = absolute_grid_y * 2 / (x.size(2) - 1) - 1
  norm_grid = torch.stack((norm_grid_x, norm_grid_y), dim=-1)

  return norm_grid

9. load calibration 后的 QAT 训练为什么权重参数不更新?

答:可以依次检查如下:
  1. prepare_qat 是否在 optimizer 定义之前。因为 prepare_qat 会进行算子融合,导致模型结构发生变化;

  2. fake_quant_enabled 和 observe_enabled 是否为 1;

  3. module 中的 training 变量是否为 True。

10. 如何处理 prepare_fx 不支持的 float 类型常量 +-*/ 操作?

答:由于模型在转换为静态图时,float 操作无法被 fx 记录,所以会触发 add 不支持,float 未转换为 qtensor 等报错。 此时需要将常量修改为 tensor,并将输入的 tensor 做量化(插入 quantstub),即可将符号运算转换为 FloatFunction。 以下提供一个参考示例。

修改前:

#init
def __init__(self, epsilon = 1e-4):
self.p5_w1 = nn.Parameter(torch.ones(2, dtype=torch.float32), requires_grad=True)
self.act = nn.ReLU()

#forward
def forward(self, x):
    x = self.quant_stub(x)
    p5_w1=self.quant_stub(self.p5_w1)
    p5_w1 = self.act(p5_w1)
    p1=torch.sum(p5_w1.unsqueeze(1), dim=0, keepdim=True)
    cc_p5 = p1+epsilon

修改后:

#init
def __init__(self, ):
self.p5_w1 = nn.Parameter(torch.ones(2, dtype=torch.float32), requires_grad=True)
self.act = nn.ReLU()
self.epsilon = torch.Tensor([1e-4])
self.mul_fu = torch.nn.quantized.FloatFunctional()

#forward
def forward(self, x):
    x = self.quant_stub(x)
    p5_w1=self.quant_stub(self.p5_w1)
    epsilon=self.quant_stub(self.epsilon)
    p5_w1 = self.act(p5_w1)
    p1=torch.sum(p5_w1.unsqueeze(1), dim=0, keepdim=True)
    cc_p5 = self.mul_fu.add(p1, epsilon)

2.4. 常见故障

1. Cannot find the extension library(_C.so)

答:主要发生在 horizon_plugin_pytorch 安装成功但 import 失败,解决方案如下:
  1. 确定 plugin 版本和 cuda 版本是对应的;

  2. 在 python3 中,找到 horizon-plugin-pytorch 的执行路径,检测该目录下是否有 .so 文件。可能同时存在多个horizon-plugin-pytorch 的版本,需要卸载只保留一个需要的版本。

2. RuntimeError: Cannot load custom ops. Please rebuild the horizon_plugin_pytorch.

答:请确认本地 CUDA 环境是否正常,如路径、版本是否符合预期。

3. RuntimeError: Only Tensors created explicitly by the user (graph leaves) support the deepcopy protocol at the moment

答:主要发生在无法正常 prepare_calibration/qat 阶段,一般是由于模型中包含 non-leaf tensor 导致的,请将prepare_calibration/qat 的 inplace 配置为 True。

4. TypeError: when calling function <built-in method conv2d of type object at >

答:主要发生在 prepare_qat 后无法正常进行 qat 训练,可能原因是使用继承的方式自定义了 module, 导致没有被成功转成 qat module,例如以下 Conv1d 示例的 L7 行:

class Conv1d(nn.Conv2d):
    def __init__(self, in_channels, ...):
        super().__init__(in_channels, ...)
    def forward(self, x):
        x = torch.unsqueeze(x, -2)
        x = super().forward(x)
        x = torch.squeeze(x, -2)    # 此处引入报错
        return x

建议使用 submodule 的方式调用 conv2d,例如:

class Conv1d(nn.Module):
    def __init__(self, in_channels, ...):
        super().__init__()
        self.conv = nn.Conv2d(in_channels, ...)
    def forward(self, x):
        x = torch.unsqueeze(x, -2)
        x = self().conv(x)
        x = torch.squeeze(x, -2)
        return x

5. torch.multiprocessing.spawn.ProcessExitedException: process 0 terminated with signal SIGKILL

答:可能是多线程,python 程序没有完全杀干净导致的。

6. HBDK model check fail

答:编译报错,可能是硬件不支持或超出限制导致, 请配置环境变量 export HNN_MODEL_EXPORT=1 后重新编译模型,并将生成的 hbir 模型文件提供给地平线技术团队做进一步分析。

7. hbdk: no output tensor found. empty model!

答:编译报错,trace 无输出,请检查是否正确 return 结果;或在逻辑中是否包含判断分支导致无输出。

8. hbdk: torch.jit.trace fail. Please make sure the model is traceable.

答:编译报错,导致原因较多,请分别检查以下两点:
  1. 打印模型来确认模型中是否包含后处理(loss,target…),若包含则将后处理部分移除;

  2. 是否存在某层未配置 qconfig,若未配置则在实现的 class 中添加 set_qconfig,structure 中的 set_qconfig 确认是否会调用。

9. AttributeError: ‘NoneType’ object has no attribute ‘numel’

答:该报错主要发生在插入伪量化节点阶段,是算子的输入 scale 为 None 导致。 造成原因可能是输出层 conv 插入 Dequant 后又接了某个 op,存在类似于 conv+dequant+conv 的结构;或者是配置了高精度输出的 conv 后又接了其他算子导致。 此时请检查 dequant 算子或高精度输出配置是否使用正确。

10. symbolically traced variables cannot be used as inputs to control flow

答:该报错是在 fx 模式下使用了 if、循环等动态控制流导致。目前 fx 模式仅支持静态控制流,因此需要避免在 forward 中使用 if、for、assert 等动态语句。

11. ‘unsupported node’, %data5.1 : Tensor = aten::select(%1253, %1753, %1753)

答:该类报错主要是因为不支持索引操作,您可以使用切片的方式进行替换,例如使用 input_points[:1] 来替代 input_points[0]。

12. NotImplementedError: function <method ‘neg’ of ‘torch._C._TensorBase’ objects> is not implemented for QTensor.

答:该报错可能发生在 fx 模式下的 Calibration 阶段,是因为 fx 模式不支持 (-x) 形式的计算导致,请将 (-x) 修改为 (-1)*(x)。

13. NotimplementedError: function <function Tensor.__rsub__ at 0x7f5a7cdiee50> is not implemented for QTensor.

答:该报错可能发生在 fx 模式下的 Calibration 阶段,是因为 fx 模式下算子替换的逻辑是如果减法中的被减数是常量,就不自动进行算子替换,所以需要将减法修改为加法,例如将 (1-x) 修改为 (x+(-1))*(-1)。