征程 6P/H 计算平台部署指南

1.前言

本文旨在提供 征程 6H/P 计算平台的部署指南,将会从硬件、软件两部分进行介绍,本文整理了我们推荐的使用流程,和大家可能会用到的一些工具特性,以便于您更好地理解工具链。某个工具具体详 l 细的使用说明,还请参考用户手册。

2.征程 6H/P 硬件配置

2.1 BPU®Nash

2.2 硬件规格

| | BPU | DSP |
|----------------|---------|----------------------|--------------------|---------|---------|---------|---------|
| ​ ​ | 算力 | TAE​ ​浮点输出 | VAE​ ​浮点 | VPU | SPU | APM | |
| 征程 6E | 80T | N | N | Y | Y | Y | Q8*1 |
| 征程 6M | 128T | N | N | Y | Y | Y | Q8*1 |
| 征程 ​6P | 560T | Y | Y | Y | Y | Y | Q8*2 |
| 征程 6H | 420T | Y | Y | Y | Y | Y | Q8*2 |
| 征程 6B-Base | 18T | Y | Y | N | N | Y | V130*1 |

BPU 内部器件:

TAE:BPU 内部的张量加速引擎,主要用于 Conv、MatMul、Linear 等 Gemm 类算子加速

VAE:BPU 内部的 SIMD 向量加速引擎,主要用于完成 vector 计算

VPU:BPU 内部的 SIMT 向量加速单元,主要用于完成 vector 计算

SPU:BPU 内部的 RISC-V 标量加速单元,主要用于实现 TopK 等算子

APM:BPU 内部另一块 RISC-V 标量加速单元,主要用于 BPU 任务调度等功能

L1M:一级缓存,BPU 核内共享

L2M:二级缓存,BPU 核间共享

2.3 与其他征程 6 计算平台的主要区别

TAE​:征程 6B/H/P 支持 fp16 和 fp32 输出,而征程 6E/M 不支持浮点输出。这使得在征程 6B/H/P 计算平台上配置模型尾部 conv 高精度输出,输出精度是 fp32,而在征程 6E/M 计算平台上配置模型尾部 conv 高精度输出,输出精度是 int32,后接一个反量化节点转 fp32。若在征程 6E/M 计算平台上是删除尾部反量化部署的话,迁移征程 6B/H/P 时软件代码需要注意适配。

征程 6E/M 尾部高精度 conv 输出 int32:

征程 6B/H/P 尾部高精度 conv 输出 float32:

VAE​:征程 6H/P 支持 fp16

L2M ​:征程 6H/P 支持 L2 缓存,多 BPU 核共用,征程 6E/M/B 单核,无 L2 缓存。通过命令 cat /sys/kernel/debug/ion/heaps/custom 查看 L2M 大小。

跨距对齐要求不同​:征程 6H/P 要求模型 nv12 输入 stride 要求满足 64 对齐;征程 6E/M/B 则是要求 32 对齐。同时模型其他输入输出节点的对齐要求也有可能不同。

针对 nv12 输入,金字塔配置文件需要注意修改 stride 参数,如果征程 6E/M/B 内存够的话,建议可直接按 64 对齐来申请,跨平台迁移时就无需更改配置。

针对其他输入输出 tensor,建议编译时打开编译参数:input_no_padding=True, output_no_padding=True。或是按推荐方式,结合 stride 和 valid_shape 来解析有效数据,也可避免跨平台迁移适配。

最小内存单元不同​:征程 6H/P tensor 最小申请内存是 256 字节,征程 6E/M 64 字节,征程 6B 128 字节。

3.新功能特性

若已有其他平台使用经验,可只关注本章节内容,了解征程 6H&P 与其他征程 6 计算平台的功能点差异即可。

相较于征程 6E/M/B,征程 6H/P 最主要区别是多核,TAE/VAE/VPU 器件能力的增强以及增加了 L2M,本章节将介绍这几点差异对于在征程 6H/P 平台上开发算法方案的影响。

3.1 多核部署

3.1.1 多核模型编译

征程 6H/P 硬件支持单帧多核的部署方式,但是当前多核模型(特指单次模型推理同时使用了两个及两个以上 BPU 核心的模型)功能还在开发中,目前支持了 resnet50 双核模型的 demo,性能数据见下表:

单核模型实测延时(ms) 双核模型实测延时(ms)
Resnet50 9.1187 5.7458

由于 BPU 是独占式硬件,若运行双核模型,则代表该模型运行期间,有两个 bpu 会被同时占用,无法运行其他任务;加上多核模型相较于单核能拿到的性能收益与模型结构紧密相关,很难确保理想的双核利用率。因此出于更高的跨平台迁移效率和硬件资源利用率等因素考虑,建议按下一节建议拆分模型部署。

3.1.2 pipeline 设计

征程 6H/P 分别提供了 3 个和 4 个 BPU 计算核心,给了应用调度更灵活的设计空间,通过多核充分并行,可有效减少系统端到端延时。以下方案仅为示例,并非是标准推荐:

算法架构示意图:

部署 pipeline 设计:

由于工具难以感知模型上下游关系,任务重要程度,不同设计帧率等信息,且多核模型利用率提升难度很大,因此建议用户手动拆分不同功能的模型以提高多核计算资源的利用率。拆分逻辑有如下建议:

  1. 多任务帧率不同:智驾系统中各个子任务设计帧率可能是不同的,建议拆分部署。
  2. 无上下游依赖:两个没有上下游输入输出数据依赖的模型,建议拆分部署,编译在一起也是顺次执行的。拆分后通过放在不同核心上部署,可以缩短整体系统的端到端延时。
  3. ​跨团队开发,提前做资源分配:​算法功能开发团队约定算力分配后可独立开发优化,独立上线测试,若编译在一起则每次发版都会有相互依赖。

3.2 合理使用 L2M

由于征程 6H/P 算法方案相对会比征程 6E/M/B 的复杂,包括接入的摄像头数目,模型前后处理和模型体量变大都会导致整个系统对带宽的需求要高很多。由于带宽争抢,不可避免当多核同时运行时会发现模型延时相较于独占硬件测试时会变长。为了缓解带宽争抢导致模型延时变长的现象,征程 6H/P 提供了 L2M,可使部分与 DDR 交换的数据被缓存在 L2M 内。建议在大部分模型都产出 hbm 后,甚至 pipeline 大致确定之后,使用如下方式离线评估所有模型的带宽资源使用情况,测试不同 l2 配置的带宽收益。

3.2.1 L2M 使用说明

需要更新到 OE3.5.0 及以上)。当前仅支持以 BPU 核为粒度配置 L2 大小,暂不支持运行时实时对单模型做配置。启用 L2M 涉及到模型编译,以及运行时正确制定环境变量两项工作。

开发板实际可用 l2m 大小请使用命令 :cat /sys/kernel/debug/ion/heaps/custom 进行查看。

3.2.1.1 模型编译

编译时通过指定参数控制模型可用的 l2 大小:

Plain 复制代码
from hbdk4.compiler import load, convert, compile
compile(quantized_bc, ···, max_l2m_size=l2m*1024*1024)
# max_l2m_size单位为bytes,l2m=0及为默认配置,不启用L2;l2m=6即为6M,l2m=12为12M。
# 详细说明请阅读用户手册 - 进阶内容 - HBDK Tool API Reference - compile
3.2.1.2 模型推理

无需改动推理代码,只需通过环境变量控制每个核可申请的 l2 大小(暂不支持运行时动态申请)

建议部署在相同 BPU 核上的模型,编译时指定相同的 L2M 大小,否则需要按最大需求来配置。

Plain 复制代码
export HB_DNN_USER_DEFINED_L2M_SIZES=6:6:6:6
# 每个核分配6M
export HB_DNN_USER_DEFINED_L2M_SIZES=12:0:0:12
# 核0和核3各分到12M

不正确的 L2M 使用可能导致如下问题:

  1. 未给对应核分配足够的 L2M 或是没有分配 L2M

推理将会失败,打印如下提示日志信息:

"model [{model name}] node [{node name}] L2 memory not enough, required l2 memspace info: [{model L2M}], user-assigned l2 memspace size: [{HB_DNN_USER_DEFINED_L2M_SIZES}], user-assigned cores: [{core_id}]"

比如模型编译时指定了 12M L2M,运行时只通过环境变量给该核分配了 6M;或是运行时忘记配置环境变量。

  1. 发现没有带宽收益,或者推理结果错乱

老版本 ucp 也能推理带 l2 的模型,只不过会出现推理结果不正确,并且没有带宽收益的问题,请从日志里确认 ucp 的版本已经升级到 OE3.5.0 及以上集成的版本。

3.2.2 统计并优化系统带宽

由于目前 hbm_perf 暂不支持 L2M(perf 看不出 l2m 的收益,预计 2025 年底的版本可支持),因此具体收益需要通过实测获取。按照经验,实测与预估偏差非常小(10% 以内),通过预估方式如果发现四个核带宽占用差不多,可以直接每个核平分 l2,如果核 0 和核 3 的带宽占用最为显著,可以直接将 l2 平均分给两个核的模型。

3.2.2.1 按实车 pipeline 设计预估平均带宽的方式

首先使用 hbm_perf 评测模型,从 html 或 json 中获取带宽信息,结合设计帧率评估模型上线后预计需要的带宽资源:

平均带宽(GB/s) = DDR bytes per second( for n FPS) / n * 设计帧率/2^30

以下面这个模型为例,实车设计帧率为 10FPS,则实车时该模型需要的平均带宽为:

68138917200/10.39*10/2^30 = 61.08GB/s

3.2.2.2 按实车 pipeline 实测平均带宽的方式
  1. 修改 hrt_model_exec 工具,支持按设计帧率 perf 模型(如何修改工具,以及带宽数据如何分析请参考社区文章:developer.horizon.auto/blog/13054)
  2. 找一个空闲的开发板,用 hrt_model_exec 工具按设计帧率 perf 模型:
Plain 复制代码
hrt_model_exec perf --model_file model.hbm --perf_fps 20 --frame_count 2000 --core_id 1
  1. 使用 hrut_ddr 获取 bpu 占用的平均带宽
Plain 复制代码
hrut_ddr -t bpu -p 1000000
# 统计周期拉长到1s,看平均值即可,默认-p是1000,即1ms采样一次,瞬时带宽受采样影响不太准确

3.3 量化配置

由于征程 6H/P 的硬件增强了浮点能力,为了降低量化难度,提高模型迭代效率,建议初始量化配置使用全局 float16,Conv 类算子回退 int8。

排除 int<->float 的量化反量化开销,征程 6H/P 上大多数 vector 计算,int16 精度和 float16 精度计算速度相当,因此建议 vector 计算精度直接使用 float16,若基础配置精度不达标,后续依据敏感度对 conv 类计算加 int16 即可。经实践证明,除了部分模型有中间计算数值范围太大超过了 fp16 表示范围需要切换 int16 之外,fp16 能有效降低 qat 量化难度。更多关于征程 6H/P 精度调优流程的说明请参考后文 4*.3.3 精度调优流程* 章节。

OE3.5.0 为了支持征程 6H/P 用户更便捷高效的配置浮点精度,horizon-plugin-pytorch 对 qconfig 配置做了优化,若您使用的是旧版本的配置方式,建议参考文档【地平线 征程 6 工具链入门教程】QAT 新版 qconfig 量化模板使用教程developer.horizon.auto/blog/13112)...

3.4 部署差异

3.4.1 模型输出精度可能不同

由于征程 6B/H/P 的 TAE 硬件支持 fp16 和 fp32 输出而征程 6E/M 不支持,因此若模型以 GEMM 类算子结尾的话,征程 6E/M 配置高精度输出是 int32,征程 6B/H/P 配置高精度输出是 fp32。因此征程 6E/M 模型直接编译到征程 6B/H/P,模型输出类型有可能会发生改变,软件代码需要注意适配。

3.4.2 跨距对齐要求不同

征程 6H/P 要求 nv12 stride 满足 64 对齐,征程 6E/M/B 是 32 对齐。且输入输出 tensor 的对齐规则也有可能不同。

从征程 6E/M/B 迁移征程 6H/P 需要注意金字塔配置文件的 stride 是否满足 64 对齐,如果征程 6E/M/B 内存够用的话,建议可直接按 64 对齐来申请,跨平台迁移时就无需更改配置。

若编译时配置了 input_no_padding=True, output_no_padding bool=True,则无需关心对齐开销;若编译时没有打开这两个参数,则跨平台编译模型会发现输入输出 tensor 的 stride 参数可能会不同,不过部署代码是通过 stride 和 valid_shape 信息来准备/解析数据,没有强依赖 stride 的 hard code,则也可忽略对齐带来的影响。

3.4.3 最小内存单元不同

征程 6H/P tensor 最小申请内存是 256 字节,征程 6E/M 64 字节,征程 6B 128 字节。这个差异会体现在模型的 aligned byte size 属性上,对于小于最小内存单元的数据,或者不满足最小内存单元整数倍的数据,会要求强制对齐。建议输入输出 tensor 内存大小按 aligned byte size 申请,不要写 hard code,避免迁移时遇到问题。

3.4.4 绑核推理

征程 6H/P 有两个 dsp 核,提交 dsp 任务时可以指定一下 backend

Plain 复制代码
hbUCPSchedParam sched_param;
HB_UCP_INITIALIZE_SCHED_PARAM(&sched_param);
sched_param.backend = dsp_core_id == 0 ? HB_UCP_DSP_CORE_0 : HB_UCP_DSP_CORE_1;
sched_param.priority = 0;

ret = hbUCPSubmitTask(task.task_handle, &sched_param);

征程 6H/P 有三/四个 bpu 核,建议所有任务做静态编排后,运行时做绑核(不建议使用 HB_UCP_BPU_CORE_ANY,会因系统调用导致 latency 跳变),减少使用抢占等会产生额外 ddr 开销的功能:

Plain 复制代码
hbUCPSchedParam sched_param;
HB_UCP_INITIALIZE_SCHED_PARAM(&sched_param);
sched_param.backend = HB_UCP_BPU_CORE_0;
sched_param.priority = 200;

ret = hbUCPSubmitTask(task.task_handle, &sched_param);

征程 6H/P 单核内支持的抢占策略与征程 6E/M 一致,多核已经为编排提供了足够的灵活度,建议多核计算平台上尽量避免使用硬件抢占,减少抢占引入的额外带宽消耗。

4.建议使用流程

在征程 6 计算平台上,我们建议前期初步做性能评测和性能优化时使用 PTQ 工具,只需要准备浮点 onnx 即可,较易上手。后续正式做量产迭代使用 QAT 量化工具,精度更有保障,对于多阶段模型,或者模型新增 head 等变化,可以更灵活复用已有 QAT 权重,有利于模型迭代更新,而 PTQ 则无法拼接历史量化 onnx。下图为 PTQ 和 QAT 量化产物对比:

4.1 性能评测

PTQ 环境搭建请参考用户手册-环境部署-Docker 容器部署。

4.1.1 快速性能评测(默认全 Int8)

Plain 复制代码
hb_compile --fast-perf  --model xxx.onnx --march nash-p

需要注意的是,fast-perf 默认会删除模型前后的 Quantize,Transpose,Dequantize,Cast,Reshape,Softmax 算子,如果模型输入输出节点较多,会与实际部署性能产生 gap,建议按下面的步骤,手动修改一下 yaml 文件:

执行上面那一行命令之后会在当前路径下生成。fast_perf 路径,路径下有 yaml 文件,打开 yaml 文件按照实际部署需要,去掉无需删除的节点,一般来说部署时只需要删除量化反量化:

修改完成后只要模型输入没有变化,则后续可一直复用该 yaml 文件,修改 onnx_model 路径即可:

Plain 复制代码
hb_compile -c xxx.yaml

4.1.2 int8_fp16 测试

Plain 复制代码
{
    "model_config": {
            "all_node_type": "float16"
    },
    "op_config": {
        "Conv": {
            "qtype": "int8"
        },
        "ConvTranspose": {
            "qtype": "int8"
        },
        "MatMul": {
            "qtype": "int8"
        },
        "Gemm": {
            "qtype": "int8"
        },
        "Resize": {
            "qtype": "int8"
        },
        "GridSample": {
            "qtype": "int8"
        },
        "GridSamplePlugin": {
            "qtype": "int8"
        }
    }
}
// Resize依据经验一般情况下不需要用到fp16精度,且fp16速度较慢,因此建议默认配置int8
// 公版GridSample和horizon 版本GridSamplePlugin都不支持fp16输入,因此需要手动回退int8,避免被lower到cpu
  1. 先生成模版:hb_compile --fast-perf --model xxx.onnx --march nash-p,默认生成在。fast_perf/隐藏目录下
  2. 修改 config:
Plain 复制代码
sed -i 's/remove_node_type: .*/remove_node_type: Quantize;Dequantize/' .fast_perf/xxx_config.yaml
sed -i 's/optimization: run_fast/calibration_type: skip/' .fast_perf/xxx_config.yaml
awk '/calibration_type: skip/ { print; print "  quant_config: ./fp16.json"; next } 1' .fast_perf/xxx_config.yaml > temp.yaml
  1. 编译:hb_compile --config temp.yaml

评测其他精度,如全 int16,softmax/layernorm fp16 等,修改上面的 fp16.json 文件即可,配置方式详细说明请参考用户手册 - 训练后量化(PTQ)- quant_config 说明。

4.1.3 板端模型性能测试工具

  1. 进入 OE 包目录:samples/ucp_tutorial/tools/hrt_model_exec,编译:
Plain 复制代码
sh build_aarch64.sh
  1. 将结果文件夹中的 output_shared_J6_aarch64/aarch64/bin/hrt_model_exec 以及 output_shared_J6_aarch64/aarch64/lib 拷贝到板端的{path}下。
  2. 新建/修改 setup.sh 文件:
Plain 复制代码
#!/bin/sh
#配置hrt_model_exec所在路径
export PATH={path}:${PATH}
#配置.so所在路径
export LD_LIBRARY_PATH={path}/lib:${LD_LIBRARY_PATH}
  1. 执行 source setup.sh,就可在板子上使用 hrt_model_exec 文件了
  2. 评测模型延时常用命令:
Plain 复制代码
hrt_model_exec perf --model_file xxx.hbm --thread_num 1 --frame_count 1000

4.2 性能分析及优化

相较于征程 6E/M,征程 6H/P 额外需要考虑的是引入了 FP 计算耗时以及多核的带宽争抢。

与平台无关,早期评测时建议参考上一章获取模型性能情况,后续量产过程中进行精度调试之前也建议先测试一下性能,并完成性能优化(部分性能优化策略可能数学不等价,导致需要重训浮点或 qat)。性能分析和优化建议参考如下步骤:

具体分析和优化过程请见《征程 6 性能分析带宽优化》。

4.3 量化训练

整个量化训练的过程,大致为如下流程:

  1. 改造浮点模型:在输入的地方插入 QuantStub,输出插入 DequantStub,标记模型需要量化的结构;
  2. calibration 一个 step 后导出 qat.bc,确认结构是否完整,是否有多余的结构,是否有不符合预期的 cpu 节点;
  3. 配置 GEMM 双 int16+ 其他 float16 做 calibraion,调整训练参数,fix scale 等直至无限接近浮点。若精度崩掉则先排查流程问题;精度达标的情况下,若延时也满足预期,则量化训练结束;
  4. 配置 GEMM 双 int8+ 其他 float16 做 calibraion,精度不达标的话进入精度 debug 的流程;若精度达标则量化训练结束;
  5. calibration 精度达到浮点 95% 以上,还想继续提升精度的话,可以进行 qat 训练;个别模型 calibration 精度较低,可通过 qat 训练得到较大提升;
  6. 测试 quantized.bc 或者 hbm 精度确认是否达标。
Plain 复制代码
from horizon_plugin_pytorch.quantization import prepare, QuantStub
from torch.quantization import DeQuantStub
from horizon_plugin_pytorch.quantization.qconfig_setter import *
from horizon_plugin_pytorch.quantization import observer_v2, get_qconfig
from horizon_plugin_pytorch.dtype import qint16, qint8
from horizon_plugin_pytorch.march import March, set_march
import torch
from torchvision.models.mobilenetv2 import MobileNetV2
from horizon_plugin_pytorch.quantization.hbdk4 import export
from hbdk4.compiler import save, load, convert, compile

class QATReadyMobileNetV2(MobileNetV2):
    def __init__(
        self,
        num_classes: int = 10,
        width_mult: float = 1.0,
        inverted_residual_setting: Optional[List[List[int]]] = None,
        round_nearest: int = 8,
    ):
        super().__init__(
            num_classes, width_mult, inverted_residual_setting, round_nearest
        )
        self.quant = QuantStub()
        self.dequant = DeQuantStub()

    def forward(self, x: Tensor) -> Tensor:
        x = self.quant(x)
        x = super().forward(x)
        x = self.dequant(x)

        return x

# 1.准备浮点模型
float_model = QATReadyMobileNetV2()
float_state_dict = torch.load(float_ckpt_path)
float_model.load_state_dict(float_state_dict)

# 2.数据校准
set_march("nash-p") # 在prepare之前设置计算架构
qconfig_template = [  
    ModuleNameTemplate({"": torch.float16}),  # 全局 feat fp16
    MatmulDtypeTemplate(  # gemm int8 input
        input_dtypes=[qint8, qint8],
    ),
    ConvDtypeTemplate(  # gemm int8 input
        input_dtype=qint8,
        weight_dtype=qint8,  
    ),
]
calibration_qconfig_qconfig_setter = QconfigSetter(
    reference_qconfig=get_qconfig(observer=observer_v2.MSEObserver),
    templates=qconfig_template,
    enable_optimize = True, 
    save_dir = "./qconfig",  
)
calib_model = prepare(
    float_model, example_input, calibration_qconfig_qconfig_setter
)
calib_model.eval()
set_fake_quantize(calib_model, FakeQuantState.CALIBRATION)
calibrate(calib_model)
# 评测数据校准精度
calib_model.eval()
set_fake_quantize(calib_model, FakeQuantState.VALIDATION)
evaluate(calib_model)
torch.save(calib_model.state_dict(), "calib-checkpoint.ckpt")

# 3.量化训练(若数据校准精度已达标,可跳过该步骤)
qat_qconfig_qconfig_setter = QconfigSetter(
    reference_qconfig=get_qconfig(observer=observer_v2.MinMaxObserver),
    templates=qconfig_template,
    enable_optimize = True, 
    save_dir = "./qconfig",  
)
qat_model = prepare(
    float_model, example_input, qat_qconfig_qconfig_setter
)
qat_model.load_state_dict(calib_model.state_dict())
qat_model.train()
set_fake_quantize(qat_model, FakeQuantState.QAT)
train(qat_model)
# 评测量化训练精度
qat_model.eval()
set_fake_quantize(qat_model, FakeQuantState.VALIDATION)
evaluate(qat_model)

# 4.模型导出
hbir_qat_model = export(qat_model, example_input, name="mobilenetv2", input_names=("input_name1","input_name2"), output_names=("output_name1","output_name2"), native_pytree=False)
save(hbir_qat_model, "qat.bc")
quantized_hbir = convert(hbir_qat_model, march="nash-p")

# 5.模型编译
compil(quantized_hbir,march="nash-p", path="model.hbm")

需要注意的是导出 qat.bc 模型时,建议指定一下模型输入输出节点名称以及模型名字,便于应用集成和后续 trace 分析,也避免 hbm 精度评测时同时加载多个名字相同的模型出错。

4.3.2 典型量化配置

4.3.2.1 基础模版(GEMM 双 int8+ 其他 float16 )
Plain 复制代码
from horizon_plugin_pytorch.quantization.qconfig_setter import *
from horizon_plugin_pytorch.quantization import observer_v2
from horizon_plugin_pytorch.dtype import qint16, qint8
import torch

model_qconfig_setter = QconfigSetter(
    reference_qconfig=get_qconfig(  # 1. 主要用于获取 observer
        observer=(
            observer_v2.MSEObserver if is_calib else observer_v2.MinMaxObserver
        )
    ),
    templates=[  
        ModuleNameTemplate({"": torch.float16}),  # 全局 feat fp16
        MatmulDtypeTemplate(  # gemm int8 input
            input_dtypes=[qint8, qint8],
        ),
        ConvDtypeTemplate(  # gemm int8 input
            input_dtype=qint8,
            weight_dtype=qint8,  
        ),
    ],
    enable_optimize = True, 
    save_dir = "./qconfig",  # qconfig 保存路径,有默认值,用户可以改
)
4.3.2.2 添加 fix scale(pyramid 和 resizer 输入请关注)

部署时模型输入来源为 pyramid 和 resizer 的模型,需要输入节点量化精度配置为 int8 类型,另外这类输入一般是经过归一化的,数值范围在[-1,1]或者[0,1],因此建议可以直接设置 fix scale。

此外还有一些模型中的节点有明确物理含义,建议也手动配置 fix scale,避免 qat 过程滑动取平均导致部分有效值域不完整。

Plain 复制代码
from horizon_plugin_pytorch.quantization.qconfig_setter import *
from horizon_plugin_pytorch.quantization import observer_v2
from horizon_plugin_pytorch.dtype import qint16, qint8
import torch

model_qconfig_setter = QconfigSetter(
    reference_qconfig=get_qconfig(  # 1. 主要用于获取 observer
        observer=(
            observer_v2.MSEObserver if is_calib else observer_v2.MinMaxObserver
        )
    ),
    templates=[  
        ModuleNameTemplate({
            "": torch.float16,
            "backbone.quant": {"dtype": qint8, "threshold": 1.0},
        }),  # 全局 feat fp16,输入节点配置int8,且固定scale=1.0/128
        MatmulDtypeTemplate(  
            input_dtypes=[qint8, qint8],
        ),
        ConvDtypeTemplate(  # gemm int8 input
            input_dtype=qint8,
            weight_dtype=qint8,  
        ),
    ],
    enable_optimize = True, 
    save_dir = "./qconfig",  # qconfig 保存路径,有默认值,用户可以改
)
4.3.2.3 通过敏感度增加高精度配置

敏感度文件 *sensitive_ops.pt 生成方式请见下一节-精度调优流程

Plain 复制代码
from horizon_plugin_pytorch.quantization.qconfig_setter import *
from horizon_plugin_pytorch.quantization import observer_v2
from horizon_plugin_pytorch.dtype import qint16, qint8
import torch

model_qconfig_setter = QconfigSetter(
    reference_qconfig=get_qconfig(  # 1. 主要用于获取 observer
        observer=(
            observer_v2.MSEObserver if is_calib else observer_v2.MinMaxObserver
        )
    ),
    templates=[  
        ModuleNameTemplate({"": torch.float16}),  # 全局 feat fp16
        MatmulDtypeTemplate(  # gemm int8 input
            input_dtypes=[qint8, qint8],
        ),
        ConvDtypeTemplate(  # gemm int8 input
            input_dtype=qint8,
            weight_dtype=qint8,  
        ),
        SensitivityTemplate(
            sensitive_table=torch.load("debug/output_0_ATOL_sensitive_ops.pt"),
            topk_or_ratio=10,# top10敏感节点配置int16
        )
    ],
    enable_optimize = True, 
    save_dir = "./qconfig",  # qconfig 保存路径,有默认值,用户可以改
)
4.3.2.4 多阶段模型量化配置

若多阶段模型在浮点训练时就是分开训的,则 qat 保持和浮点节点一致分为多阶段训练。第一阶段按照前面的配置正常 calib 就好(不要 qat,除非 calib 精度实在是达标不了,qat 之后权重变了,二阶段需要 finetune 浮点),二阶段使用如下方式,将一阶段设置成浮点,仅量化二阶段:

Plain 复制代码
from horizon_plugin_pytorch.quantization.qconfig_setter import *
from horizon_plugin_pytorch.quantization import observer_v2
from horizon_plugin_pytorch.dtype import qint16, qint8
import torch

stage2 = ["bev_stage2_vehicle_head.head","bev_stage2_vrumerge_head.head"]
model_qconfig_setter = QconfigSetter(
    reference_qconfig=get_qconfig(  # 1. 主要用于获取 observer
        observer=(
            observer_v2.MSEObserver if is_calib else observer_v2.MinMaxObserver
        )
    ),
    templates=[  
        ModuleNameTemplate({"": torch.float32}),  # 全局 feat fp32
        ModuleNameTemplate(
                {n: torch.float16 for n in stage2},# stage2为二阶段模型节点的关键字
            ),
        MatmulDtypeTemplate(  # gemm int8 input
            input_dtypes=[qint8, qint8],
        ),
        ConvDtypeTemplate(  # gemm int8 input
            input_dtype=qint8,
            weight_dtype=qint8,  
        ),
    ],
    enable_optimize = True, 
    save_dir = "./qconfig",  # qconfig 保存路径,有默认值,用户可以改
)

若想要一阶段和二阶段连接部分的 scale 相同,则 qat 阶段不要在两阶段连接部分加量化反量化,仅在导出模型时添加:

Plain 复制代码
class EncoderModule(nn.Module):
    def __init__(self, ) -> None:
        super().__init__()
        self.dequant = DeQuantStub()
        self.conv = ConvModule(...)
  
    def forward(self, input1, input2):
        input1 = self.conv(input1)
        output = input1 + input2
        if env.get("EXPORT_DEPLOY", 0) == 1:
            return self.dequant(output)
        return output

class DecoderModule(nn.Module):
    def __init__(self, ) -> None:
        super().__init__()
        self.quant = QuantStub()
        self.conv = ConvModule(...)
  
    def forward(self, data):
        if env.get("EXPORT_DEPLOY", 0) == 1:
            data = self.quant(data)
        data = self.conv(data)
        return data

class Model(nn.Module):
    def __init__(self, ) -> None:
        super().__init__()
        self.quant1 = QuantStub()
        self.quant2 = QuantStub()
        self.dequant = DeQuantStub()
        self.encoder = EncoderModule(...)
        self.decoder = DecoderModule(...)
  
    def forward(self, input1, input2):
        input1 = self.quant1(input1)
        input2 = self.quant2(input2)
        output = self.encoder(input1, input2)
        output = self.decoder(output)
        return self.dequant(output)

两阶段分别 calibration 完之后,使用如下脚本拼接得到完整的 calibration 权重,使用该权重完成后续的 qat 训练,若还有第三阶段,需要基于二阶段 qat 权重 finetune 浮点:

Plain 复制代码
stage1 = [
    "backbone",
    "bifpn_neck",
    "bev_stage1_head",
]
e2e_stage2 = [
    "task_bev_encoder.bev_quant_stub",
    "task_bev_encoder.bev_encoder.dynamic",
    "e2e_vehicle_head.head",
    "e2e_vrumerge_head.head",
]
def filter_ckpt(ckpt, prefix, exclude=[]):
    new_ckpt = OrderedDict()
    new_ckpt["state_dict"] = OrderedDict()
    new_ckpt["state_dict"]._metadata = OrderedDict()
    for k in ckpt["state_dict"].keys():
        if any([k.startswith(key) for key in prefix]) and not any([k.startswith(key) for key in exclude]):
            new_ckpt["state_dict"][k] = ckpt["state_dict"][k]
    for k in ckpt["state_dict"]._metadata.keys():
        if any([k.startswith(key) for key in prefix]) and not any([k.startswith(key) for key in exclude]):
            new_ckpt["state_dict"]._metadata[k] = ckpt["state_dict"]._metadata[k]
    return new_ckpt

def merge_ckpt_func(ckpt_list):
    new_ckpt = OrderedDict()
    new_ckpt["state_dict"] = OrderedDict()
    new_ckpt["state_dict"]._metadata = OrderedDict()
    for ckpt in ckpt_list:
        new_ckpt["state_dict"].update(ckpt["state_dict"])
        new_ckpt["state_dict"]._metadata.update(ckpt["state_dict"]._metadata)
    return new_ckpt

stage1_ckpt = filter_ckpt(torch.load(stage1_calibration_checkpoint_path, map_location="cpu"), stage1)
e2e_stage2_3_ckpt = filter_ckpt(torch.load(e2e_calibration_checkpoint_path, map_location="cpu"), e2e_stage2)
merge_ckpt = merge_ckpt_func([stage1_ckpt, e2e_stage2_3_ckpt])
torch.save(merge_ckpt, "merged_stage1-stage2.pth.tar")

4.3.3 精度调优流程

由于征程 6H/P 上大多数 vector 计算,int16 精度和 float16 精度计算速度相当,因此建议 vector 计算精度直接使用 float16,可有效减小量化调优的难度,提升迭代效率。若在其他平台上有全 int8 部署经验,或依据经验判断模型全 int8(或加少量 int16)无精度风险,为追求极致帧率,可不使用 float16。如下为征程 6H/P 的量化调优建议流程:

4.3.4 部署模型编译

由于模型输入输出格式训练和部署时可能存在区别,因此工具链提供了一些 api 用于在量化训练后调整模型以适配部署要求。差异主要是在图像输入格式,以及是否需要删除首尾量化反量化节点这两个方面。

4.3.4.1 pyramid 或 resizer 输入

该操作请在 convert 前完成,且 qat 训练时对应输入节点的 quant 需要是 int8 量化。下图为训练和部署编译时模型输入的差异:

将如下代码加到编译生成 hbm 的流程中,只需指定需要修改为 pyramid/resizer 的节点名字即可(注意 type 为训练时候的数据格式,mean 和 std 也需要结合训练前处理代码做配置)

Plain 复制代码
from hbdk4.compiler import load

model = load("qat_model.bc")  
func = model[0]
resizer_input = ["input0"]      # 部署时数据来源于resizer的输入节点名称列表
pyramid_input = ["input3"]         # 部署时数据来源于pyramid的输入节点名称列表

def channge_source(node, input_source, preprocess):
    mode = preprocess["type"]
    if mode == "rgb":
        mode = "yuvbt601full2rgb"
    elif mode == "bgr":
        mode = "yuvbt601full2bgr"
    elif mode == "yuv":
        mode = None
    divisor = preprocess["divisor"]
    mean = preprocess["mean"]
    std = preprocess["std"]
    node = node.insert_transpose(permutes=[0, 3, 1, 2])
    print(mode,divisor,mean,std)
    node = node.insert_image_preprocess(mode=mode, divisor=divisor, mean=mean, std=std)
    if input_source == "pyramid":
        node.insert_image_convert("nv12")
    elif input_source == "resizer":
        node.insert_roi_resize("nv12")

for input in func.flatten_inputs[::-1]:
    if input.name in pyramid_input:
        if input.type.shape[0] > 1:
            split_inputs = input.insert_split(0)
            for split_input in reversed(split_inputs):
                channge_source(split_input, "pyramid", {"type":"yuv","divisor":1,"mean":[128, 128, 128],"std":[128, 128, 128]})
    elif input.name in resizer_input:
        if input.type.shape[0] > 1:
            split_inputs = input.insert_split(0)
            for split_input in reversed(split_inputs):
                channge_source(split_input, "resizer", {"type":"yuv","divisor":1,"mean":[128, 128, 128],"std":[128, 128, 128]})

insert_image_preprocess 方法包括以下参数:

  • mode,可选值包含:
    • "yuvbt601full2rgb" YUVBT601Full 转 RGB (默认)
    • "yuvbt601full2bgr" YUVBT601Full 转 BGR
    • "yuvbt601video2rgb" YUVBT601Video 转 RGB 模式
    • "yuvbt601video2bgr" YUVBT601Video 转 BGR 模式
    • "bgr2rgb" BGR 转 RGB
    • "rgb2bgr" RGB 转 BGR
    • "none" 不进行图像格式的转换,仅进行 preprocess 处理
  • 数据转换除数 divisor,int 类型,默认为 255
  • 均值 mean,double 类型,长度与输入 c 方向对齐,默认为 [0.485, 0.456, 0.406]
  • 标准差值 std,double 类型,长度与输入 c 方向对齐,默认为 [0.229, 0.224, 0.225]
4.3.4.2 算子删除

该操作需要在 convert 后完成,因为 convert 前模型都还是浮点输入输出,没有生成量化反量化节点:

Plain 复制代码
quantized_model = convert(qat_model, march)
# remove_io_op会递归删除所有可被删除的节点
quantized_model[0].remove_io_op(op_types = ["Dequantize","Quantize","Cast","Transpose","Reshape"])

若进行了删除动作,需要在后处理中根据业务需要进行功能补全,例如实现量化、反量化的逻辑。

量化计算参考代码:

Plain 复制代码
torch.clamp(torch.round(x/scales), min=int16_min, max=int16_max).type(torch.int16)
Plain 复制代码
float32_t _round(float32_t input) {
  std::fesetround(FE_TONEAREST);
  float32_t result = nearbyintf(input);
  return result;
}

inline T int_quantize(float32_t value, float32_t scale, float32_t zero_point,
                    float32_t min, float32_t max) {
  value = _round(value / scale + zero_point);
  value = std::min(std::max(value, min), max);
  return static_cast<T>(value);
}

如果并不想去掉模型所有的量化反量化,只想删掉个别输入输出节点相连的 op,可采用下面的方法删除与某输入/输出节点直接相连的节点:

Plain 复制代码
def remove_op_by_ioname(func, io_name=None):
    for loc in func.inputs + func.outputs:
        if not loc.is_removable[0]:
            if io_name == loc.name:
                raise ValueError(f"Failed when deleting {io_name} ,which id unremovable")
            continue
        attached_op = loc.get_attached_op[0]
        removed = None
        output_name = attached_op.outputs[0].name
        input_name = attached_op.inputs[0].name
        
        if io_name in [output_name, input_name]:
            removed, diagnostic = loc.remove_attached_op()
        if removed is True:
            print(f"Remove node {io_name} successfully",flush=True)
        if removed is False:
            raise ValueError(
                f"Failed when deleting {attached_op.name} operator,"
                f"error: {diagnostic}")

remove_op_by_ioname(func,"_input_0")
remove_op_by_ioname(func,"_output_0")

4.3.5 定点模型精度评测

由于 qat 还是伪量化模型,从伪量化转换真正的定点模型有可能会产生误差,因此建议模型上线之前除了测试 qat torch module 精度之外,再测试一下定点模型的精度。定点模型精度可以基于 quantized.bc 或者。hbm 做测试,quantized.bc 和 hbm 在模型中无 cpu 算子,无 fp32 精度算子的情况下,模型输出是二进制一致的。

4.3.5.1 quantized.bc 推理
python

quantized.bc 推理输入格式为 dict,支持 tensor 和 np.array,输出格式与输入一致。当前只支持 cpu 推理,建议通过多进程加速推理过程。

Plain 复制代码
inputs = {inputs[0].name: Y, inputs[1].name: UV}
hbir_outputs = hbir[0].feed(inputs)
C++

与推理 hbm 接口使用无任何区别,便于用户在 x86 端测试系统集成效果,具体使用方式请参考后文第五章,。so 替换成 x86 的即可。

4.3.5.2 hbm 推理

由于本地使用 cpu 推理 hbm 速度非常慢,因此工具链提供了一个工具方便用户在服务器端给直连的开发板下发推理任务。本文只介绍最简单的单进程使用方式,多进程、多阶段模型输入输出传输优化,以及统计模型推理、网络传输耗时等请参考用户手册 hbm_infer 工具介绍

Plain 复制代码
hbm_model = HbmRpcSession(
    host="xx.xx.xx.xx",
    local_hbm_path="xx.hbm", #也可传入一个list,推理时通过指定model_name来选择推理哪个模型,可只传输一次推理所用的.so
    # core_id=2, #绑核推理,推理开启L2M模型时建议绑核,与环境变量对应
    # extra_server_cmd="export HB_DNN_USER_DEFINED_L2M_SIZES=0:0:12:0" # L2M模型推理所需环境变量
)
# 打印模型输入输出信息
hbm_model.show_input_output_info()
# 准备输入数据
input_data = {
    'img': torch.ones((1, 3, 224, 224), dtype=torch.int8)
}
# 执行推理并返回结果
output_data = hbm_model(input_data) 
# 若传入的是list,需要正确指定model_name
# output_data = hbm_model(input_data, model_name=model_name)
print([output_data[k].shape for k in output_data])
# 关闭server
hbm_model.close_server()
4.3.5.3 pyramid 输入模型测试建议

对于 pyramid 模型,由于部署和训练输入格式不一致,因此若要使用插入前处理节点后的 quantized.bc 或者 hbm 做精度测试的话,需要适配一下前处理代码,需要注意的是,把训练时的 rgb/bgr/yuv444 转换成 nv12,是存在信息损失的,若模型训练的时候前处理没有带上转 nv12 的过程,则有可能对这样的信息损失不够鲁棒,出现掉点的现象。因此若 pyramid 输入定点模型掉点超出预期,需要再测试一下不插入前处理节点的模型精度,若的确是 nv12 带来的损失,建议修改模型前处理重训浮点。

如下为将浮点模型输入 data 处理为 deploy 定点模型输入格式的示例代码,需要注意的是要修改一下评测前处理,只保留读图和 resize 的操作,​去掉归一化相关的前处理​(这部分通过前文部署模型编译章节的修改动作已经合入到了模型内部):

Plain 复制代码
def nv12_runtime(data):
    import cv2

    def img2nv12(input_image):
        image = input_image.astype(np.uint8)
        image = image.squeeze(0)
        image = np.transpose(image, (1, 2, 0))
        height, width = image.shape[0], image.shape[1]
        # 若读出的图片为BGR格式,请做对应修改
        yuv420p = cv2.cvtColor(image, cv2.COLOR_RGB2YUV_I420).reshape(
            (height * width * 3 // 2,)
        )
        y = yuv420p[: height * width]
        uv_planar = yuv420p[height * width :].reshape((2, height * width // 4))
        uv_packed = uv_planar.transpose((1, 0)).reshape((height * width // 2,))
        return torch.tensor(
            y.reshape(1, height, width, 1), dtype=torch.uint8
        ), torch.tensor(
            uv_packed.reshape(1, height // 2, width // 2, 2), dtype=torch.uint8
        )

    dict_data = {}
    for key in data.keys():
        if data[key].shape[0] == 1:
            dict_data[f"{key}_y"], dict_data[f"{key}_uv"] = img2nv12(
                data[key].cpu().numpy()
            )
        else:
            for i in range(data[key].shape[0]):
                (
                    dict_data[f"{key}_{i}_y"],
                    dict_data[f"{key}_{i}_uv"],
                ) = img2nv12(data[key][i : i + 1, :, :, :].cpu().numpy())
    return dict_data
  
input_6v_deploy = nv12_runtime(input_6v_float)

5.模型部署

5.1 UCP 简介

UCP(Unify Compute Platform,统一计算平台)定义了一套统一的异构编程接口, 将 SOC 上的功能硬件抽象出来并进行封装,对外提供基于功能的 API 进行调用。UCP 提供的具体功能包括:​视觉处理 ​(Vision Process)、​神经网络模型推理 ​(Neural Network)、​高性能计算库 ​(High Performance Library)、​自定义算子插件开发​。

UCP 支持的 Backbend:

5.2 模型推理快速上手

使用 UCP 推理模型的基本代码参考如下,详细信息可参考用户手册《统一计算平台-模型推理开发》、《模型部署实践指导-模型部署实践指导实例》、《UCP 通用 API 介绍》等相关章节。

Plain 复制代码
// 1. 加载模型并获取模型名称列表以及Handle
{
    hbDNNInitializeFromFiles(&packed_dnn_handle, &modelFileName, 1);
    hbDNNGetModelNameList(&model_name_list, &model_count, packed_dnn_handle);
    hbDNNGetModelHandle(&dnn_handle, packed_dnn_handle, model_name_list[0]);
}

// 2. 根据模型的输入输出信息准备张量
std::vector<hbDNNTensor> input_tensors;
std::vector<hbDNNTensor> output_tensors;
int input_count = 0;
int output_count = 0;
{
    hbDNNGetInputCount(&input_count, dnn_handle);
    hbDNNGetOutputCount(&output_count, dnn_handle);
    input_tensors.resize(input_count);
    output_tensors.resize(output_count);
    prepare_tensor(input_tensors.data(), output_tensors.data(), dnn_handle);
}

// 3. 准备输入数据并填入对应的张量中
{
    read_data_2_tensor(input_data, input_tensors);
    // 确保更新输入后进行Flush操作以确保BPU使用正确的数据
    for (int i = 0; i < input_count; i++) {
      hbUCPMemFlush(&input_tensors[i].sysMem, HB_SYS_MEM_CACHE_CLEAN);
    }
}

// 4. 创建任务并进行推理
{
    // 创建任务
    hbDNNInferV2(&task_handle, output_tensors.data(), input_tensors.data(), dnn_handle)
    
    // 提交任务
    hbUCPSchedParam sched_param;
    HB_UCP_INITIALIZE_SCHED_PARAM(&sched_param);
    sched_param.backend = HB_UCP_BPU_CORE_ANY;
    hbUCPSubmitTask(task_handle, &sched_param);
    
    // 等待任务完成
    hbUCPWaitTaskDone(task_handle, 0);
}

// 5. 处理输出数据
{
    // 确保处理输出前进行Flush操作以确保读取的不是缓存中的脏数据
    for (int i = 0; i < output_count; i++) {
      hbUCPMemFlush(&output_tensors[i].sysMem, HB_SYS_MEM_CACHE_INVALIDATE);
    }
    // 对输出进行后处理操作
}

// 6. 释放资源
{
    // 释放任务
    hbUCPReleaseTask(task_handle);
    // 释放输入内存
    for (int i = 0; i < input_count; i++) {
      hbUCPFree(&(input_tensors[i].sysMem));
    }
    // 释放输出内存
    for (int i = 0; i < output_count; i++) {
      hbUCPFree(&(output_tensors[i].sysMem));
    }
    // 释放模型
    hbDNNRelease(packed_dnn_handle);
}

5.3 Pyramid/Resizer 模型输入准备说明

由于 Pyramid/Resizer 模型相对特殊,其输入是动态 shape/stride,因此单独介绍一下其输入 tensor 准备的注意事项和技巧。下表是解析 Pyramid/Resizer 模型观察到的现象(-1 为占位符,表示为动态,Pyramid 输入的 stride 为动态;Resizer 输入的 H、W、stride 均为动态。) :

Resizer 输入的 ​HW 动态​,是因为原始输入的大小可以是任意的;

Pyramid/Resizer 输入的​​ stride 动态 ​,可以理解为是支持 Crop 功能(后文 5.4.1 节)

hrt_model_exec model_info

板端可执行程序工具

在征程 6H/P 平台上要求 Pyramid/Resizer 输入必须满足 W64 对齐,因此无论是金字塔配置还是模型输入准备,都需要满足对齐要求。

输入 tensor 准备:

Plain 复制代码
#define ALIGN(value, alignment) (((value) + ((alignment)-1)) & ~((alignment)-1))
#define ALIGN_64(value) ALIGN(value, 64)

int prepare_image_tensor(const std::vector<hbUCPSysMem> &image_mem, int input_h,
                         int input_w, hbDNNHandle_t dnn_handle,
                         std::vector<hbDNNTensor> &input_tensor) {
  // 准备Y、UV输入tensor
  for (int i = 0; i < 2; i++) {
    HB_CHECK_SUCCESS(hbDNNGetInputTensorProperties(&input_tensor[i].properties,
                                                   dnn_handle, i),
                     "hbDNNGetInputTensorProperties failed");
    // auto w_stride = ALIGN_64(input_w);
    // int32_t y_mem_size = input_h * w_stride;
    input_tensor[i].sysMem[0] = image_mem[i];

    // 配置原图大小,NHWC
    input_tensor[i].properties.validShape.dimensionSize[1] = input_h;
    input_tensor[i].properties.validShape.dimensionSize[2] = input_w;
    if (i == 1) {
      // UV输入大小为Y的1/2
      input_tensor[i].properties.validShape.dimensionSize[1] /= 2;
      input_tensor[i].properties.validShape.dimensionSize[2] /= 2;
    }

    // stride满足64对齐
    input_tensor[i].properties.stride[1] =
        ALIGN_64(input_tensor[i].properties.stride[2] *
                 input_tensor[i].properties.validShape.dimensionSize[2]);
    input_tensor[i].properties.stride[0] =
        input_tensor[i].properties.stride[1] *
        input_tensor[i].properties.validShape.dimensionSize[1];
  }
  return 0;
}

// 准备roi输入tensor
int prepare_roi_tensor(const hbUCPSysMem *roi_mem, hbDNNHandle_t dnn_handle,
                       int32_t roi_tensor_id, hbDNNTensor *roi_tensor) {
  HB_CHECK_SUCCESS(hbDNNGetInputTensorProperties(&roi_tensor->properties,
                                                 dnn_handle, roi_tensor_id),
                   "hbDNNGetInputTensorProperties failed");
  roi_tensor->sysMem[0] = *roi_mem;
  return 0;
}

int prepare_roi_mem(const std::vector<hbDNNRoi> &rois,
                    std::vector<hbUCPSysMem> &roi_mem) {
  auto roi_size = rois.size();
  roi_mem.resize(roi_size);
  for (auto i = 0; i < roi_size; ++i) {
    int32_t mem_size = 4 * sizeof(int32_t);
    HB_CHECK_SUCCESS(hbUCPMallocCached(&roi_mem[i], mem_size, 0),
                     "hbUCPMallocCached failed");
    int32_t *roi_data = reinterpret_cast<int32_t *>(roi_mem[i].virAddr);
    roi_data[0] = rois[i].left;
    roi_data[1] = rois[i].top;
    roi_data[2] = rois[i].right;
    roi_data[3] = rois[i].bottom;
    hbUCPMemFlush(&roi_mem[i], HB_SYS_MEM_CACHE_CLEAN);
  }
  return 0;
}

金字塔配置:

Plain 复制代码
{
    "ds_roi_layer": 2,
    "ds_roi_sel": 1,
    "ds_roi_start_top": 0,
    "ds_roi_start_left": 0,
    "ds_roi_region_width": 480,
    "ds_roi_region_height": 256,
    "ds_roi_wstride_y": 512, // 480不满足64对齐要求
    "ds_roi_wstride_uv": 512, // 480不满足64对齐要求
    "ds_roi_out_width": 480,
    "ds_roi_out_height": 256
 }

5.4 模型部署优化

5.4.1 通过地址偏移完成 crop

场景描述:

  • Y: validShape = (1,224,224,1), stride = (-1,-1,1,1)
  • UV: validShape = (1,112,112,2), stride = (-1,-1,2,1)

该模型的输入图片大小为 224x224,假设有一张 H x W = 376 x 384(其中 W 存在大小为 8 的 padding,因为 nv12 需要 W64 对齐)的图片,可以直接基于 stride 值进行 crop,没有额外的拷贝开销

Crop 功能使用:

原始图片 Y、UV 的 validShape、stride、指针如下:

  • Y: validShape = (1, 376, 384, 1), stride = (384*376, 384, 1, 1),内存指针为 y_data
  • UV: validShape = (1, 188, 192, 2), stride = (384*188, 384, 2, 1),内存指针为 uv_data

模型输入张量准备-Y:

  • Crop 起始点 [h, w] = [50, 64],则: 地址偏移为 y_offset = 50*384`` ``+ 64*1,内存指针为 y_data + y_offset
  • 模型输入应设置为 validShape=(1,224,224,1)stride = (224*384,384,1,1)

模型输入张量准备-UV:

  • 由于 UV 尺寸为 Y 的 1/2,因此裁剪起始点为 [25, 32],则: 地址偏移为 uv_offset = 25*384`` ``+ 32*2,内存指针为 uv_data + uv_offset
  • 模型输入应设置为 validShape=(1,112,112,2)stride = (112*384,384,2,1)

Crop 限制条件:

  • 图像:要求分辨率 ≥ 模型输入,w_stride 需要 64 字节对齐
  • 模型:要求输入 validShape 为固定值,stride 为动态值,这样能通过控制 stride 的大小对图像进行 Crop
  • 裁剪位置:由于裁剪是对图像内存进行偏移,而对于输入内存的首地址要求 64 对齐

示例​:

Plain 复制代码
ucp_tutorial/dnn/basic_samples/code/02_advanced_samples/crop/src/main.cc

5.4.2 小模型批处理

由于 BPU 是资源独占式硬件,所以对于 Latency 很小的模型而言,其框架调度开销占比会相对较大。在 征程 6 平台,UCP 支持通过复用 task_handle 的方式,将多个小模型任务一次性下发,全部执行完成后再一次性返回,从而可将 N 次框架调度开销合并为 1 次,以下为参考代码:

Plain 复制代码
// 获取模型指针并存储
std::vector<hbDNNHandle_t> model_handles;

// 准备各个模型的输入输出,准备过程省略
std::vector<std::vector<hbDNNTensor>> inputs;
std::vector<std::vector<hbDNNTensor>> outputs;

// 创建任务并进行推理
{
    // 创建并添加任务,复用task_handle
    hbUCPTaskHandle_t task_handle{nullptr};
    for(size_t task_id{0U}; task_id < inputs.size(); task_id++){
        hbDNNInferV2(&task_handle, outputs[task_id].data(), inputs[task_id].data(), model_handles[i]);
    }
    
    // 提交任务
    hbUCPSchedParam sche_param;
    HB_UCP_INITIALIZE_SCHED_PARAM(&sche_param);
    sche_param.backend = HB_UCP_BPU_CORE_ANY;
    hbUCPSubmitTask(task_handle, &sche_param);
    
    // 等待任务完成
    hbUCPWaitTaskDone(task_handle, 0);
}

5.4.3 优先级调度/抢占

UCP 支持任务优先级调度和抢占,可通过 hbUCPSchedParam 结构体进行配置,其中:

  • priority > customId > submit_time(任务提交时间)
  • priority 支持 [0, 255],对于模型任务而言:
    • 0, 253\] 为普通优先级,不可抢占其他任务,但在未执行时支持按优先级进行排队

    • 255 为 urgent 抢占任务,可抢占普通任务和 high 抢占任务
    • 可被中断抢占的低优任务,需要在模型编译阶段配置 max_time_per_fc 参数拆分模型指令
  • 其他 backend 任务,priority 支持 [0, 255],但不支持抢占,可以认为都是普通优先级

5.5 DSP 开发

为了简化用户开发,UCP 封装了一套基于 RPC 的开发框架,来实现 CPU 对 DSP 的功能调用,但具体 DSP 算子实现仍是调用 Cadence 接口去做开发。总体来说可分为三个步骤:

  1. 使用 Cadence 提供的工具及资料完成算子开发;
Plain 复制代码
int test_custom_op(void *input, void *output, void *tm) {
  // custom impl
  return 0;
}
  1. DSP 侧通过 UCP 提供的 API 注册算子,编译带自定义算子的镜像;
Plain 复制代码
// dsp镜像中注册自定义算子
hb_dsp_register_fn(cmd, test_custom_op, latency)
  1. ARM 侧通过 UCP 提供的算子调用接口,完成开发板上的部署使用。
Plain 复制代码
// 将输入输出的hbUCPSysMem映射为DSP可访问的内存地址
hbUCPSysMem in;
hbUCPMalloc(&in, in_size, 0)
hbDSPAddrMap(&in, &in)

hbUCPSysMem out;
hbUCPMalloc(&out, out_size, 0)
hbDSPAddrMap(&out, &out)

// 创建并提交DSP任务
hbUCPTaskHandle_t taskHandle{nullptr};
hbDSPRpcV2(&taskHandle, &in, &out, cmd)

hbUCPSchedParam ctrl_param;
HB_UCP_INITIALIZE_SCHED_PARAM(&ctrl_param);
ctrl_param.backend = HB_UCP_DSP_CORE_ANY;
hbUCPSubmitTask(task_handle, &ctrl_param);

// 等待任务完成
hbUCPWaitTaskDone(task_handle, 0);

更多信息可见用户手册《统一计算平台-自定义算子-DSP 算子开发》。

6. 相关基础知识

若需要了解 nv12 输入格式,模型量化等基础知识,可以在开发者社区《地平线算法工具链社区资源整合》developer.horizon.auto/blog/10364)...

相关推荐
Xの哲學2 小时前
Linux二层转发: 从数据包到网络之桥的深度解剖
linux·服务器·算法·架构·边缘计算
我也要当昏君3 小时前
计算机组成原理
算法
Fiona-Dong3 小时前
Louvain 算法
python·算法
维构lbs智能定位4 小时前
蓝牙信标、UWB等主流室内定位无线技术的参数对比、核心算法和选型指南详解(二)
算法·蓝牙信标·uwb·主流室内定位无线技术
灰灰勇闯IT4 小时前
【探索实战】Kurator多集群统一应用分发实战:从环境搭建到业务落地全流程
算法
鱼在树上飞4 小时前
乘积最大子数组
算法
H_z___5 小时前
Codeforces Round 1070 (Div. 2) A~D F
数据结构·算法
自学小白菜5 小时前
每周刷题 - 第三周 - 双指针专题 - 02
python·算法·leetcode
杜子不疼.5 小时前
【LeetCode76_滑动窗口】最小覆盖子串问题
算法·哈希算法