7.1. 环境配置类

7.1.1. 如何查看当前系统的各种软件版本信息

  • cat /etc/version 查看当前系统的SDK版本和编译时间

# cat /etc/version
LNX6.1.12_PL5.1_V0.0.8_20240319-1108
  • uname -a 查看内核版本

# uname -a
Linux buildroot 6.1.12-rt7+ #1 SMP PREEMPT Tue Mar 19 11:12:22 CST 2024 aarch64 GNU/Linux
  • strings /dev/block/platform/by-name/uboot | grep "U-Boot 2022.10" 查看uboot版本

# strings /dev/block/platform/by-name/uboot | grep "U-Boot 2022.10"
U-Boot 2022.10-g4432fbb999
U-Boot 2022.10-g4432fbb999 (Mar 19 2024 - 17:22:42 +0800)

7.1.2. 如何设置uboot的bootargs

通过串口连接,在系统启动时,快速持续按下空格键,进入uboot命令行模式,然后通过以下方式设置bootargs:

  • 配置临时的启动项, 修改以下命令的 <items to add>为你需要的参数,配置后需要在uboot命令行下执行 boot 命令引导内核启动,重启设备则配置失效:

setenv bootargs <items to add>
boot
# 例如打开kernel的日志earlycon
setenv bootargs earlycon=uart8250,mmio32,0x32120000
boot
  • 配置长期有效的启动项,修改以下命令的 <items to add> 为你需要的参数,执行saveenv 保存配置到env分区,即使设备断电重启也能保持配置有效。

setenv bootargs <itmes to add>
saveenv
reset
  • 通过配置文件来修改启动项,在SDK源码目录的uboot/tools路径下创建一个uboot.env配置文件,按照键值对的格式写入需要配置的参数,通过整编译./bd.sh或单编uboot./bd.sh uboot在out/product路径下会生成ubootenv.img镜像文件。使用fastboot工具烧写ubootenv.img,即使设备断电重启也能保持配置有效。

#fastboot烧写ubootenv
fastboot flash ubootenv ubootenv.img

注:以下配置项不能通过配置文件的方式来修改

Hobot>
boot_device=emmc
bootcmd=run ab_select_cmd;run avb_boot;
dev_index=0
dev_name=mmc
ethaddr=46:a1:a3:75:e4:a4
fdtcontroladdr=8beb2680
hb_board_id=0x0202
reset_reason=COLD_BOOT
stderr=serial@32120000
stdin=serial@32120000
stdout=serial@32120000

7.1.3. 为什么程序可以手动执行,添加到/etc/init.d下自启动就不行

如果开发的是多媒体、BPU相关的程序,因为这两部分的动态库文件都存放在 /usr/hobot/ 目录下,手动执行的时候,/usr/hobot/ 会在启动的时候自动添加到shell环境中,但是自启动时,这部分配置不一定会配置,所以需要在自启动程序的配置脚本里面添加环境设置,例如S90cam-service里面的配置:

export LD_LIBRARY_PATH="/usr/hobot/lib:${LD_LIBRARY_PATH}"

或者,在程序启动前通过 source /etc/profile.d/environment.sh 把多媒体、BPU、app相关的所有动态库、可执行程序都添加到环境变量中。

7.2. 系统软件类

7.2.1. 如何查看系统温度,cpu 和 bpu 运行频率的统计信息

hrut_somstatus

7.2.2. 如何读取芯片uid命令

cat /sys/class/socinfo/soc_uid

7.2.3. 如何查看bpu支持的频率

cat /sys/class/devfreq/3a000000.bpu/available_frequencies

7.2.4. 如何查看cpu调度模式

cat /sys/devices/system/cpu/cpufreq/policy0/scaling_governor

7.2.5. 如何查看cpu支持的频率和当前运行的频率

cat /sys/devices/system/cpu/cpufreq/policy0/scaling_available_frequencies
cat /sys/devices/system/cpu/cpufreq/policy0/cpuinfo_cur_freq

7.2.6. 如何查看cpu降频温度

cat /sys/devices/virtual/thermal/thermal_zone1/trip_point_1_temp

7.2.7. 关闭cpu核的方法

echo 0 > /sys/devices/system/cpu/cpu1/online

# 需要关哪个CPU核,修改以下命令的 <x> 为对应数字即可
echo 0 > /sys/devices/system/cpu/cpu<x>/online

注意:cpu0 无法关闭,x 的取值范围为 1~7。

关闭 cpu 核后可以使用 lscpu 命令查看当前的 cpu 状态:

root@buildroot:~# echo 0 > /sys/devices/system/cpu/cpu1/online
root@buildroot:~# lscpu
Architecture:             aarch64
  CPU op-mode(s):         32-bit, 64-bit
  Byte Order:             Little Endian
CPU(s):                   8
  On-line CPU(s) list:    0,2-7
  Off-line CPU(s) list:   1
......  

上述日志可以看到,cpu1 已经离线了,说明已经成功关闭了 cpu1。

7.2.8. 中断绑定cpu核方法

注意:

下列命令中使用的中断号及 CPU 仅供参考,请用户根据实际需求选取需要绑定的CPU及中断号。

#绑定到CPU3,echo后的8对应CPU3,1/2/4/8/16/64/128/256分别对应CPU0-7,175是中断号
echo 8 > /proc/irq/175/smp_affinity

7.2.9. eMMC烧录器文件

eMMC的空片烧录,可以直接使用disk*.img文件,这个文件是二进制文件,可以用烧录器直接烧写到eMMC(注意空片烧录需要完整得烧录一次disk.img之后,才能再单独烧录uboot.img等分区)

如果是批量生产,建议先烧录后贴片:

1)烧录的时候首先确认工厂烧录器支持eMMC型号,如果支持,则可以直接烧录。

2)如果烧录器不支持,但是可以支持相同封装的,则可以让烧录器厂家把eMMC型号添加进去。

3)如果此类封装eMMC的烧录器都不支持,则可以找专门的第三方烧录厂。

4)烧录界面的配置,可以咨询烧录器厂家,不同的烧录器有些差别。

7.2.10. 如何查看和修改芯片寄存器

使用 devmem命令可以直接访问物理地址,读写寄存器,常规用法:

devmem ADDRESS [WIDTH [VALUE]]

# 例如读管脚 HSIO_GPIO_00 的复用寄存器的值
devmem 0x3505005C 32

7.2.11. i2c命令使用

# 列举 I2C bus 和上面所有的设备
i2cdetect -l
# 查看 bus 1 上有哪些探测到的外设
i2cdetect -y -r 1
# 写: 0x36为I2C设备的地址, 0x5081为要写的寄存器地址, 0x01为写入的值。
i2ctransfer -f -y 1 w3@0x36 0x50 0x81 0x01
# 读: 0x36为I2C设备的地址, 0x300A为要读的寄存器地址, r3为连续读3Byte, 0x56 0x08 0x41 为读到的寄存器的值。
i2ctransfer -f -y 1 w2@0x36 0x30 0x0A r3

7.2.12. 增强驱动能力(以I2C4举例)

首先更新设备树,相关内容一般在kernel/arch/arm64/boot/dts/hobot/pinmux-func.dtsi设备树文件中,以下是部分截取内容:

......

pconf_drv_pu_ds7_1v8: pconf-dev-pu-ds7-1v8 {
  bias-pull-up;
  power-source = <HORIZON_IO_PAD_VOLTAGE_1V8>;
  drive-strength = <7>;/*驱动强度*/
};

......

pinctrl_i2c4: i2c4grp {
  horizon,pins = <
    LSIO_I2C4_SCL  LSIO_PINMUX_3 BIT_OFFSET0  MUX_ALT0 &pconf_drv_pu_ds5_1v8
    LSIO_I2C4_SDA  LSIO_PINMUX_3 BIT_OFFSET2  MUX_ALT0 &pconf_drv_pu_ds5_1v8
  >;
};
......

编译烧录之后,在系统中可以使用cat /sys/kernel/debug/pinctrl/34180000.lsio_iomuxc/pinconf-pins查询修改是否生效。

root@ubuntu:~# cat /sys/kernel/debug/pinctrl/34180000.lsio_iomuxc/pinconf-pins
Pin config settings per pin
Format: pin (name): configs
......
......
pin 47 (lsio_i2c4_scl): input bias pull down (0 ohms), input bias pull up (0 ohms), output drive strength (10 mA), input enabled, input schmitt enabled, pin output (1 level)
pin 48 (lsio_i2c4_sda): input bias pull down (2097152 ohms), input bias pull up (2097152 ohms), output drive strength (10 mA), input enabled, input schmitt enabled, pin output (1 level)

drive-strength = <2>X5平台一般对应的是3mA,选择drive-strength = <7>对应的节点之后可以看到驱动电流是10mA。

7.2.13. 内核是否支持 PREEMPT_RT 内核 patch

支持RT Patch (rt7)。

7.2.14. 能否自定义根文件系统?

目前支持buildroot文件系统,用户可以按需重新构建。

7.2.15. 是否支持数字音频接口(Audio PDM)?

支持,配置如下:

引脚端口确认

芯片引脚定义引脚如下,使用前请排查是否有被复用。

pdm_pin_mux_check

芯片引脚定义引脚如下,使用前请排查是否有被复用。

设备树配置确认

pinctrl 这部分在 BSP 源码中的 pinmux-func.dtsi 文件中;

pinctrl_dsp_pdm_cko: pdmckogrp {
  horizon,pins = <
    DSP_PDM_CKO  DSP_PINMUX_0  BIT_OFFSET24  MUX_ALT0 &pconf_drv_pu_ds2_1v8
  >;/* PDM时钟输出的引脚配置,定义了引脚的复用功能和电源配置,使用的时候需要检查是否有复用*/
};

pinctrl_dsp_pdm_in: pdmingrp {
  horizon,pins = <
    DSP_PDM_IN0  DSP_PINMUX_0  BIT_OFFSET26  MUX_ALT0 &pconf_drv_pu_ds2_1v8
    DSP_PDM_IN1  DSP_PINMUX_0  BIT_OFFSET28  MUX_ALT0 &pconf_drv_pu_ds2_1v8
    DSP_PDM_IN2  DSP_PINMUX_0  BIT_OFFSET30  MUX_ALT0 &pconf_drv_pu_ds2_1v8
    DSP_PDM_IN3  DSP_PINMUX_1  BIT_OFFSET0  MUX_ALT0 &pconf_drv_pu_ds2_1v8
  >;/*这是PDM输入的引脚配置,定义了四个输入引脚的复用功能和电源配置,使用的时候需要检查是否有复用*/
};

pdm 绑定 dsp 部分在 BSP 源码中的 x5.dtsi 文件中;

archband_pdm: archband_pdm@320d0000 {
      compatible = "archband,pdm-driver";
      reg = <0x320d0000 0x00010000>;
      clocks = <&dspclks X5_DSP_PDM_HMCLKA_CLK>, <&dspclks X5_DSP_PDM_APB_CLK>;
      clock-names = "pdmclk", "pdm_pclk";
      pinctrl-names = "default";
      pinctrl-0 = <&pinctrl_dsp_pdm_cko &pinctrl_dsp_pdm_in>;
      arb-syscon = <&dsp_crm_syscon 0x10>;
      arb,osr = <1>;  
      dmas = <&dsp_axi_dma 20>, <&dsp_axi_dma 19>, <&dsp_axi_dma 18>, <&dsp_axi_dma 17>;
      dma-names = "rx0", "rx1", "rx2", "rx3";
      channel = <2>;
      #sound-dai-cells = <0>;
      status = "disabled";
      resets = <&dsprst DSP_PDM_RESET>;
};

打开相应的 config 选项

audio_pdm_deconfig_setting

上图是图形化界面选择。 对应平台端有如下 config 需要确认:

CONFIG_SND_ARCHBAND_PDM
CONFIG_SND_VIRT_CODEC

CONFIG_SND_SOC_HOBOT_SIMPLE_CARD
CONFIG_SND_DUPLEX_CARD
CONFIG_SND_HOBOT_SOUND_MACHINE
CONFIG_SND_HOBOT_SOUND_DUPLEX_HOST

CONFIG_SND_SIMPLE_CARD_UTILS

验证参考

请根据实际使用的配置和 codec,参考使用以下命令:

modprobe designware_i2s i2s_ms=1 /*选择i2s的主从模式*/
modprobe es7210
modprobe es8156
modprobe snd-soc-hobot-sound-duplex-host
arecord –l /*查看声卡,确认pdm录制时使用的声卡*/
arecord -Dhw:1,2 -c 2 -r 16000 -f S24_LE -t wav -d 5 /userdata/test.wav	/*录制命令*/

录制完毕之后检查/userdata/test.wav 文件是否正常。

pdm使用限制说明

通道数:8chn(走 alsa 框架测试每次只能验证2通道,也就是一个 HIFI_PIN_INx,原因是每个数字 mic 的 data 输出需要占用一个 dma handshake。ALSA 框架不支持单根输入接多个 dma handshake。) 采样率:16/32/48k 位宽:24bit

7.2.16. 为什么fastboot烧录miniboot后无法启动

错误的烧录方式:

fastboot flash addr:0x0 miniboot.img

问题原因: 旧版本中miniboot包含了miniboot.img,分区表misc分区,而由于现版本烧录脚本发生了改变,miniboot_all.img = gpt.img + mbr.img + miniboot.img + misc.img ,也就是旧版本的miniboot,而现版本miniboot.img = bl2.img + bl3x.img,所以miniboot的烧录有两种方式。

烧录miniboot分区 正确的烧录命令:

fastboot flash miniboot miniboot.img

烧录miniboot以及完整分区表和misc 正确的烧录命令:

fastboot flash addr:0x0 miniboot_all.img

7.2.17. SDK编译时出现 internal compiler error: Illegal instruction

错误日志:

image-20240319224834955

问题原因: gcc arm-gnu-toolchain-11.3.rel1 版本在老的 CPU 型号上存在兼容性问题,无法处理浮点数据。详情可以查阅:Bug 5825 - Illegal instruction: 4 on questionable float conversion

解决: 一般需要更新开发主机的,使用较新的 CPU 型号。

7.2.18. 为什么单独编译了hbre的模块,在hbre.img中没有包含

问题现象: 使用./bd.sh hbre camsys/libcam 编译后,hbre.img 里面没有包含我的修改内容

原因及解决: 使用./bd.sh hbre 或者./bd.sh app 不带模块名是默认会编译所有模块后重新生成对应的hbre.img和app.img,但是加了模块名之后,例如./bd.sh hbre camsys/libcam 或者 ./bd.sh hbre liblog则只会编译该模块,但是不会做打包动作。这样做的原因是可以支持用户单独编译后,把产物下载到板端独立调试。如需要编译单独模块后重新生成image文件,可以在编译命令末尾加上 pack 选项,例如./bd.sh hbre camsys/libcam pack, 这样在编译完 camsys/libcam后,会重新打包 hbre.img 分区镜像。或者,可以在完成单独模块的编译后,执行 ./bd.sh hbre pack 单纯的完成hbre分区镜像的打包。

7.2.19. 使用buildroot制作根文件系统时提示 you are leaving 9 commits behind

问题现象: Warning: you are leaving 9 commits behind, not connected to

image-20240319224825834

问题原因: 这是在重新编译buildroot的时候会提示的警告。原因是第一次编译buildroot时,会把hb_patch_buildroot下的patch打到buildroot的源码下,再次编译的时候则会提示这个警告。

解决办法: 对编译过程没有影响,编译脚本会恢复源码到初始状态后重新打patch。

7.2.20. 使用buildroot制作根文件系统时提示 LD_LIBRARY_PATH Error

问题现象: 编译buildroot生成根文件系统时提示以下Error:

You seem to have the current working directory in your LD_LIBRARY_PATH environment variable. This doesn’t work.

image-20240319224814069

问题原因: 这个提示是说用户环境变量里面已经配置了LD_LIBRARY_PATH,在编译buildroot时,这个配置是不允许的设置的。

解决办法: 查看~/.profile 和 ~/.bashrc 的内容,找到 LD_LIBRARY_PATH 配置,注释掉,然后重新登录终端。echo ${LD_LIBRARY_PATH} 看一下还有没有值,没有值就可以正常了。

7.2.21. 使用buildroot制作system根文件系统,提示mount not found

问题现象: ../output/build/libglib2-2.72.3/meson.build:2124:2: ERROR: Dependency “mount” not found, tried pkgconfig and cmake

image-20240319224801158

问题原因: 在制作initramfs系统后没有执行clean,立即继续制作system的系统的情况下,会报这个错。

解决办法: 制作initramfs后,把 framework/output 删除,重新编译system。

7.2.22. bd.sh lunch时找不到 utils_funcs.sh 文件

问题现象: ./bd.sh lunch

./bd.sh: line 7: /home/work/x5_bjs1/utils_funcs.sh: No such file or directory

问题原因: 在执行 ./bd.sh 时,会通过以下方式获取xbuild.sh的绝对路径:

SCRIPT_DIR=”$( cd “$( dirname “$(readlink -f “${BASH_SOURCE[0]}”)” )” && pwd )”

source “$SCRIPT_DIR/utils_funcs.sh”

bd.sh 是链接到 build/xbuild.sh 的软连接,SCRIPT_DIR 是 ./build 目录的绝对路径。

但是在sdk打包和拷贝的过程中有可能会把软连接搞成实体文件, 那获取的SCRIPT_DIR目录就可能不对,因此会提示找不到依赖的文件。

解决办法

恢复 bd.sh 为软连接文件

ln -sf build/xbuild.sh bd.sh

Linux下的文件禁止使用zip打包,会改变软连接文件, 也有可能会改变文件的权限属性。

执行cp命令时带上 -a 选项,可以保持软连接状态。

7.2.23. 在ZSH的shell环境下使用快捷命令模式编译提示无法找到bd.sh

问题现象: 如题,在zsh 的环境下按照文档source build/quickcmd.sh后,使用b命令编译报错如下图:

image-20240319224849099

问题原因: 导入bd.sh位置为PATH环境变量时,由于zsh没有BASH_SOURCE这个全局变量,导致路径设置错误。

解决办法:参考 ${BASH_SOURCE[0]} equivalent in zsh? 在找不到BASH_SOURCE这个环境变量时,退化成${(%):-%x}}

img

7.2.24. 如果只修改内核里面的dts,可以不烧录整个镜像吗

问题: 需要熟悉分区表定义和内核编译打包的过程。dts和kernel会一起打包成fit格式的镜像,文件存储的its定义在device/horizon/x5/board_cfg/soc/x5.its 文件里面,该文件会描述board_id 与内核镜像、设备树的匹配方式。

解决办法: kernel和dts一起打包在boot.img里面,单独更新boot分区即可。

7.2.25. 在app目录下面增加了新的代码目录,要怎么样让它参与进整体编译

解决办法: app 目录下的代码由 build/mk_app.sh 脚本负责编译,如果需要添加新的代码目录,并参与整体编译,需要在 build/mk_app.sh 程序中的 components 添加对应的目录名。

image-20240319225201040

build/mk_app.sh 会遍历 components , 在对应的目录下寻找 build.sh(优先匹配) 和 Makefile,如果找到了就会编译。

注意: app目录下的代码因为是独立的测试程序,会随时根据需要增加和删除,这个list里面目录如果不存在,或者找不到build.sh 和 Makefile,也不会导致整体编译失败,仅提示模块不存在的警告。

7.3. 芯片规格类

7.3.1. 芯片的内存大小上限

最大支持8GB内存。

7.3.2. BPU 的内存访问模式

BPU与CPU共享内存, 在系统中会Reserve一块连续物理内存给BPU使用。

7.4. 编解码类

7.4.1. 视频码流头部信息不正确报错

[ERROR][MM][src/vdi/linux/vdi_osal.c:174] [ERROR][869.56942][3344:3543][VideoDecoder] DecodeHeader:1554 FAILED TO DEC_PIC_HDR: ret(1), SEQERR(00005000) [ERROR][MM][src/vdi/linux/vdi_osal.c:174] [ERROR][869.56980][3344:3543][COMPONENT] Component wave_decoder will be terminated

需要输入给解码模块的码流的第一个帧,给sps,pps,idr,如果只给sps,就会报如上错误。

7.5. 外设类

7.5.1. 如何判断sensor硬件连接是否正常

  • 首先需要使能sensor的供电,一般会有多路供电(例如 1.8V, 2.8V等),使能供电的方式依据开发板的情况各有不同

  • 然后需要使能sensor 的 MCLK, 否则sensor的i2c无法正确探测到sensor

echo 1 > /sys/class/vps/mipi_host1/param/snrclk_en
echo 24000000 > /sys/class/vps/mipi_host1/param/snrclk_freq
echo 1 > /sys/class/vps/mipi_host0/param/snrclk_en
echo 24000000 > /sys/class/vps/mipi_host0/param/snrclk_freq
  • 通过 i2cdetect -y -f <i2c_bus> 命令探测sensor, 根据硬件设计情况填写 i2c_bus 编号

7.5.2. ETH phy寄存器读取方法

uboot下可以直接使用mii命令,例如:

mii dump
mii read

7.5.3. Usb软件切换主从模式的方法

usb通过软件切换主从模式方法有两种,使用debugfs和class/usb_role切换USB控制器的工作模式,通过软件切换,可以实现更灵活的USB连接方式,方便测试和调试。

* 利用debugfs
echo device > /sys/kernel/debug/usb/35100000.usb/mode                     #  usb3.0 port
echo host > /sys/kernel/debug/usb/35100000.usb/mode                       #  usb3.0 port

echo device > /sys/kernel/debug/usb/35300000.usb/mode                     #  usb2.0 port
echo host > /sys/kernel/debug/usb/35300000.usb/mode                       #  usb2.0 port

* 利用class/usb_role
echo device > /sys/class/usb_role/35100000.usb-role-switch/role            # usb3.0 port
echo host > /sys/class/usb_role/35100000.usb-role-switch/role              # usb3.0 port

echo device > /sys/class/usb_role/35300000.usb-role-switch/role            # usb2.0 port
echo host > /sys/class/usb_role/35300000.usb-role-switch/role              # usb2.0 port

7.6. 算法工具链类

7.6.1. 常见故障处理

7.6.1.1. hb_mapper checker 常见故障

背景信息:模型检查命令( hb_mapper checker

在实际工程中,由于并非所有浮点模型均能够转为量化模型,因此在转换之前需要进行一次检查,这个check过程,会完成一遍模型转换的过程, 但是对于比较耗时的步骤,进行了简化处理。该命令在完成模型的检查后,会输出检查结果和OP在设备上的部署情况。

故障场景:以下为在使用 hb_mapper checker 时常见的故障场景:

1.故障现象一:

  ERROR The shape of model input:input is [xxx] which has dimensions of 0.
  Please specify input-shape parameter.

故障可能原因:发生此故障的原因可能是模型输入为动态shape。

解决建议:针对此故障,您可使用参数 --input-shape "input_name input_shape" 来指定输入节点的shape信息。

2.故障现象二:

  ERROR HorizonRT not support these cpu operators: {op_type}

故障可能原因:发生此故障的原因可能是使用的CPU算子为地瓜不支持的CPU算子。

解决建议:针对此故障,您可以根据我们提供的算子支持列表中的内容对算子进行替换;若不被支持的CPU算子为模型核心算子,请您联系地瓜对此进行开发评估。

3.故障现象三:

  Unsupported op {op_type}

故障可能原因:发生此故障的原因可能是使用的BPU算子为地瓜不支持的BPU算子。

解决建议:针对此故障,若模型整体性能可满足需要,您可以忽略该日志;若模型整体性能不能达到您的预期,您可以根据我们提供的算子支持列表中的内容对算子进行替换。

4.故障现象四:

  ERROR nodes:['{op_type}'] are specified as domain:xxx, which are not supported by official onnx.
  Please check whether these ops are official onnx ops or defined by yourself

故障可能原因:发生此故障的原因可能是使用的自定义算子为地瓜不支持的自定义算子。

解决建议:针对此故障,您可以根据我们提供的算子支持列表中的内容对算子进行替换或参考 自定义算子开发 中的内容完成自定义CPU算子注册。

7.6.1.2. hb_mapper makertbin常见故障

背景信息:模型编译命令( hb_mapper makertbin

该命令根据配置文件和模型的种类,会生成ONNX量化模型以及仿真上板情况的runtime模型。

故障场景:以下为在使用 hb_mapper makertbin 时常见的故障场景:

1.故障现象一:

  Layer {op_name}
    xxx expect data shape range:[[xxx][xxx]], but the data shape is [xxx]
  Layer {op_name}
    Tensor xxx expects be n dimensions, but m provided

故障可能原因:发生此故障的原因可能是,{op_name}算子超过支持限制被回退到CPU计算。

解决建议:针对此故障,若CPU算子带来的性能损耗您可接受,则无需关注该信息;若性能不能达到您的要求,您可以根据我们提供的算子支持列表中的内容将该op修改至BPU可支持的范围。

2.故障现象二:

  ERROR There is an error in pass: {op_name}. Error message:xxx

故障可能原因:发生此故障的原因可能是,{op_name}算子优化失败。

解决建议:针对此故障,请您将模型以及.log文件收集好后提供给地瓜技术人员进行分析处理。

3.故障现象三:

  Error There is an error in pass:constant_folding.
  Error message: Could not find an implementation for the node {op_name}

故障可能原因:发生此故障的原因可能是该算子onnxruntime暂未支持。

解决建议:针对此故障,您可以根据我们提供的算子支持列表中的内容对算子进行替换,如不被支持的算子为核心算子,请您联系地瓜对此进行开发评估。

4.故障现象四:

  Start to parse the onnx model
  core dump

故障可能原因:发生此故障的原因可能是模型解析失败(可能是导出模型时只为一个output/input节点指定了name)。

解决建议:针对此故障,建议您重新导出onnx并确认其有效性(导出onnx模型时不指定output/input name,或者依次为每个output/input节点指定名称)。

5.故障现象五:

  Start to calibrate/quantize the model
  core dump

  Start to compile the model
  core dump

故障可能原因:发生此故障的原因可能是模型量化/编译失败。

解决建议:针对此故障,请您将模型以及.log文件收集好后提供给地瓜技术人员进行分析处理。

6.故障现象六:

  ERROR model conversion faild: Inferred shape and existing shape differ in dimension x: (n) vs (m)

故障可能原因:发生此故障的原因可能是onnx模型的输入shape非法,或者是工具优化pass有误。

解决建议:针对此故障,请您确保onnx模型的有效性,若onnx模型可正常推理,请将模型提供给地瓜技术人员进行分析处理。

7.故障现象七:

  WARNING got unexpected input/output/sumin threshold on conv {op_name}! value: xxx

故障可能原因:发生此故障的原因可能是数据预处理有误,或该节点weight值太小/太大。

解决建议:针对此故障,请您检查数据预处理是否有误;我们建议您使用BN算子优化数据分布。

8.故障现象八:

  ERROR hbdk-cc compile hbir model failed with returncode -n

故障可能原因:发生此故障的原因可能是模型编译失败。

解决建议:针对此故障,请您将模型以及.log文件收集好后提供给地瓜技术人员进行分析处理。

9.故障现象九:

  ERROR {op_type}  only support 4 dim input

故障可能原因:发生此故障的原因可能是工具链暂不支持该op输入维度为非四维。

解决建议:针对此故障,我们建议您将该op输入维度调整为四维输入。

10.故障现象十:

  ERROR {op_type} Not support this attribute/mode=xxx

故障可能原因:发生此故障的原因可能是工具链暂不支持op的该属性。

解决建议:针对此故障,您可以根据我们提供的算子支持列表中的内容进行替换或联系地瓜对此进行开发评估。

11.故障现象十一:

  ERROR There is no node can execute on BPU in this model,
  please make sure the model has at least one conv node which is supported by BPU.

故障可能原因:发生此故障的原因可能是模型中没有可量化的BPU节点。

解决建议:针对此故障,请您确保onnx模型的有效性,且模型中至少使用了一个conv;若前述条件均已满足,请您将模型以及.log文件收集好后提供给地瓜技术人员进行分析处理。

12.故障现象十二:

  ERROR The opset version of the onnx model is n, only model with opset_version 10/11 is supported

故障可能原因:发生此故障的原因可能是模型opset版本超出工具链支持限制。

解决建议:针对此故障,请您重新导出模型,确保opset_version=10或者11。

13.故障现象十三:

在使用run_on_bpu后转换报错。

故障可能原因:发生此故障的原因可能是目前暂不支持将该算子run_on_bpu。

解决建议:run_on_bpu暂仅支持指定模型中Relu/Softmax/Reshape/pooling(maxpool、avgpool等)算子以及CPU*+Transpose组合(可通过声明Transpose节点名称,将CPU*+Transpose都运行在BPU上,CPU*特指BPU支持的op), 若满足前述条件但仍run_on_bpu失败,请您联系地瓜技术人员对此进行分析处理;若不满足前述条件,可联系地瓜技术人员对此进行开发评估。

14.故障现象十四:

  ERROR unsupported model: BAYES-E not support excute one model on 2core simultaneously now

故障可能原因:发生此故障的原因是X5目前暂不支持编译双核模型。

解决建议:针对此故障,建议您将yaml配置文件中的core_num设置为1。

15.故障现象十五:

  ERROR : There is an ERROR during shape inference,···,The error model has been saved as shape_inference_fail.onnx

故障可能原因:发生此故障的原因可能是模型非法或工具解析失败。

解决建议:请您将 .log 文件及生成的 shape_inference_fail.onnx 提供给地瓜技术人员进行原因分析。

7.6.1.3. hb_model_modifier常见故障

背景信息hb_model_modifier 工具用于对指定的runtime模型中输入端的Transpose、Quantize节点和输出端的Transpose、Dequantize、DequantizeFilter、Cast、Reshape、Softmax节点进行删除操作, 并将删除节点的信息存放在BIN模型中,可以通过 hb_model_info 进行查看。

故障场景:以下为在使用 hb_model_modifier 时常见的故障场景:

故障现象:

  ERROR Can not find value info {op_name}

故障可能原因:该问题为地瓜已知问题,于OE1.1.14版本进行了修复。

解决建议:针对此故障,请您完整更新OE开发包或将horizon-tc-ui升级至1.7.8。

7.6.1.4. hb_verifier常见故障

背景信息hb_verifier 工具是用于对指定的定点模型和runtime模型进行结果验证的工具。

若您使用工具前指定了图片,则 hb_verifier 工具会使用指定图片进行定点模型推理、runtime模型板端和X86端模拟器上的推理,并对结果进行两两比较, 给出是否通过的结论(此过程支持自选,您可以根据需要选择进行对比的内容)。

若您在使用工具前未指定图片,则 hb_verifier 工具会默认使用随机生成的tensor数据进行推理。

故障场景:以下为在使用 hb_verifier 时常见的故障场景:

故障现象:

  ERROR Quanti onnx and Arm result Strict check FAILED

故障可能原因:发生此故障的原因可能是模型一致性比对失败。

解决建议:针对此故障,请您将模型提供给地瓜技术人员进行分析处理。

7.6.1.5. hb_onnxruntime常见故障

背景信息hb_onnxruntime 主要是用于onnx模型推理的类。

故障场景:以下为在使用 hb_onnxruntime 时常见的故障场景:

1.故障现象一:

  ERROR [ONNXRuntimeError] : 2:INVALID_ARGUMENT : Unexpected input data type.
  Actual: (N11onnxruntime17PrimitiveDataTypexxx), expected: (N11onnxruntime17PrimitiveDataTypexxx)

故障可能原因:发生此故障的原因可能是输入数据格式与模型不匹配。

解决建议:针对此故障,一般来说浮点onnx模型的输入格式为float32,量化后模型输入格式为int8。可使用可视化工具查看onnx模型input节点的属性。

2.故障现象二:

  [libprotobuf FATAL google/protobuf/stubs/common.cc:83] This program was compiled against version 3.6.1 of the Protocol Buffer runtime library,
  which is not compatible with the installed version on (3.19.4).

故障可能原因:发生此故障的原因可能是torch使用的protobuf版本和horizon使用的protobuf版本有冲突,需要在torch前import。

解决建议:针对此故障,您可以将from horizon_tc_ui import HB_ONNXRuntime放在第一行import。import其他API出现相同报错也同等适用。

7.6.1.6. libDNN常见故障

背景信息libDNN 主要是用于地瓜模型的推理库。

故障场景:以下为在使用 libDNN 时常见的故障场景:

1.故障现象一:

  (common.h:79): HR:ERROR: op_name:xxx invalid attr key xxx

故障可能原因:发生此故障的原因可能是libDNN暂不支持该op的某个属性(后续我们将逐步把算子约束前移至模型转换阶段提醒)。

解决建议:针对此故障,您可以根据我们提供的算子支持列表中的内容进行替换或联系地瓜对此进行开发评估。

2.故障现象二:

  (hb_dnn_ndarray.cpp:xxx): data type of ndarray do not match specified type. NDArray dtype_: n, given:m

故障可能原因:发生此故障的原因可能是libDNN暂不支持该输入类型(后续我们将逐步把算子约束前移至模型转换阶段提醒)。

解决建议:针对此故障,您可以根据我们提供的算子支持列表中的内容进行替换或联系地瓜对此进行开发评估。

3.故障现象三:

  (validate_util.cpp:xxx):tensor aligned shape size is xxx , but tensor hbSysMem memSize is xxx,
  tensor hbSysMem memSize should >= tensor aligned shape size!

故障可能原因:发生此故障的原因可能是输入数据申请内存不足。

解决建议:针对此故障,由于使用hrt_model_exec model_info查看模型input节点的aligned shape,是按aligned shape*size_of(tensor type)来申请内存空间的。 此处我们建议:若您的libDNN版本高于 1.5.4b,建议直接使用hbDNNTensorProperties.alignedByteSize来申请内存空间,若您的 libDNN 版本低于 1.5.4b 版本则直接使用aligned shape*size_of(tensor type)来申请内存空间。

4.故障现象四:

  (bpu_model_info.cpp:xxx): HR:ERROR: hbm model input feature names must be equal to graph node input names

故障可能原因:该问题为地瓜已知问题,属于hb_model_modifer工具已知问题,已于OE1.1.14版本进行了修复。

解决建议:针对此故障,请您完整更新OE开发包或将horizon-tc-ui升级至1.7.8版本。

7.6.2. 模型量化及上板使用技巧

7.6.2.1. Transformer使用说明

本章节将对各个transformer的概念及参数进行说明,并为您提供参考使用示例,方便您进行tranformer操作。

在文档内容开始阅读前,以下内容请您注意:

  • 图片数据为 三维数据,但地瓜提供的transformer都是以 四维数据 的方式来进行获取和处理的,transformer只会对输入数据中的 第0张 图片做该操作。

AddTransformer

说明

对输入图片中的所有像素值做增加value的操作。该transformer会在输出时, 将数据格式转为float32。

参数

  • value: 对每个像素做增加的数值, 注意value的取值可以为负数, 如 -128。

使用举例

  # 对图像数据做减去128的操作
  AddTransformer(-128)

  # 对图像数据做增加127的操作
  AddTransformer(127)

MeanTransformer

说明

对输入图片中的所有像素值做减去 mean_value 的操作。

参数

  • means: 对每个像素做增加的数值, 注意value的取值可以为负数, 如 -128。

  • data_format: 输入的layout类型,取值范围为[”CHW”,”HWC”], 默认 “CHW”。

使用举例

  # 每个像素减去128.0 输入的类型为CHW
  MeanTransformer(np.array([128.0, 128.0, 128.0]))

  # 每个像素减去不同的数值,103.94, 116.78, 123.68,输入的类型为 HWC
  MeanTransformer(np.array([103.94, 116.78, 123.68]), data_format="HWC")

ScaleTransformer

说明

对输入图片中的所有像素值做乘以data_scale系数的操作。

参数

  • scale_value: 需要乘以的系数,如0.0078125 或者1/128。

使用举例

  # 将取值范围-128~127,所有的像素的调整到-1~1之间
  ScaleTransformer(0.0078125)
  # 或者
  ScaleTransformer(1/128)

NormalizeTransformer

说明

用于对输入图片进行归一化的操作。该transformer会在输出时, 将数据格式转为float32。

参数

  • std:输入的第一张图片,需要除以的数值。

使用举例

  # 将取值范围[-128, 127] 所有的像素的调整到-1~1之间
  NormalizeTransformer(128)

TransposeTransformer

说明

用于做layout转换的操作。

参数

  • order: 对输入图片做layout转换后的顺序(顺序与原有的layout顺序有关)。如:HWC的顺序为0,1,2,需要转为CHW时,order为(2,0,1)。

使用举例

  # HWC转到CHW
  TransposeTransformer((2, 0, 1))
  # CHW转到HWC
  TransposeTransformer((1, 2, 0))

HWC2CHWTransformer

说明

用于将NHWC转换为NCHW的操作。

参数:不涉及。

使用举例

  # NHWC转到NCHW
  HWC2CHWTransformer()

CHW2HWCTransformer

说明

用于将NCHW转换为NHWC的操作。

参数:不涉及。

使用举例

  # NCHW转到 NHWC
  CHW2HWCTransformer()

CenterCropTransformer

说明

以直接截断取值的方式从图片中心裁剪出一个正方形的图片的操作。该transformer会在输出时, 将数据格式转为float32。当data_type的值为uint8时,输出为uint8。

参数

  • crop_size: 中心裁剪的正方形的边长size。

  • data_type: 输出结果的类型,取值范围为[”float”, “uint8”]。

使用举例

  # 以224*224的方式,做中心裁剪,默认输出类型为float32
  CenterCropTransformer(crop_size=224)

  # 以224*224的方式,做中心裁剪,输出类型为uint8
  CenterCropTransformer(crop_size=224, data_type="uint8")

PILCenterCropTransformer

说明

使用PIL的方式从图片中心裁剪出一个正方形的图片的操作。该transformer会在输出时, 将数据格式转为float32。

参数

  • size: 中心裁剪的正方形的边长size。

使用举例

  # 以224*224的方式,使用PIL的方式做中心裁剪
  PILCenterCropTransformer(size=224)

LongSideCropTransformer

说明

用于做长边裁剪的操作。该 transformer 会在输出时, 将数据格式转为float32。

当宽度比高度的数值大时,会裁剪出一个中心以高度大小为准的正方形,如宽100,高70,裁剪之后大小为70*70。

当高度比宽度的数值大时,会裁剪出一个中心以宽度大小不变,高度为差值的一半+宽度 的长方形,如宽70,高100,裁剪之后大小为 70*(100-70)/2+70 ,即70* 85大小的长方形。

参数:不涉及。

使用举例

  LongSideCropTransformer()

PadResizeTransformer

说明

使用填充的方式做图像放大的操作。该 transformer 会在输出时, 将数据格式转为float32。

参数

  • target_size:目标大小,值为元组,如(240,240)。

  • pad_value:填充到数组中的值,默认值为127。

  • pad_position:填充的位置,取值范围为[”boundary”, “bottom_right”],默认值为 “boundary”。

使用举例

  # 裁剪一个大小为512*512,填充到右下角,填充值为0
  PadResizeTransformer((512, 512), pad_position='bottom_right', pad_value=0)

  # 裁剪一个大小为608*608,填充到边框,填充值为 127
  PadResizeTransformer(target_size=(608, 608))

ResizeTransformer

说明

用于调整图像大小的操作。

参数

  • target_size:目标大小,值为元组,如(240,240)。

  • mode:图片处理模式,取值范围为(”skimage”,”opencv”),默认值为 “skimage”。

  • method:插值的方法,此参数仅在mode为skimage时生效。取值范围为0-5,默认值为1,其中:

    • 0代表Nearest-neighbor;

    • 1代表Bi-linear(default);

    • 2代表Bi-quadratic;

    • 3代表Bi-cubic;

    • 4代表Bi-quartic;

    • 5代表Bi-quintic。

  • data_type:输出的类型,取值范围为(uint8,float),默认为float类型。当被设置为uint8时,输出类型为uint8 ,其他情况为float32。

  • interpolation:插值的方法,此参数仅在mode为opencv时生效。默认为空,取值范围为(opencv的插值方式), 目前interpolation仅支持为空或opencv中的INTER_CUBIC两种插值方法,当interpolation为空时,默认使用INTER_LINEAR方式。

    以下为opencv中支持的插值方式及说明(目前未支持的插值方式将在后续迭代中逐步支持):

    • INTER_NEAREST,最近邻插值;

    • INTER_LINEAR,双线性插值,当interpolation为空时,默认使用这种方法。

    • INTER_CUBIC,双三次插值4x4像素邻域内的双立方插值。

    • INTER_AREA,使用像素面积关系重采样。它可能是图像抽取的首选方法,因为它可以提供无莫尔条纹的结果。但是当图像被缩放时,它类似于INTER_NEAREST方法。

    • INTER_LANCZOS4,8x8邻域的Lanczos插值。

    • INTER_LINEAR_EXACT,位精确双线性插值。

    • INTER_NEAREST_EXACT,位精确最近邻插值。这将产生与PIL、scikit-image或Matlab中的最近邻方法相同的结果。

    • INTER_MAX,插值代码的掩码。

    • WARP_FILL_OUTLIERS,标志,填充所有目标图像像素。如果其中一些对应于源图像中的异常值,则将它们设置为零。

    • WARP_INVERSE_MAP,标志,逆变换。

使用举例

  # 将输入图片大小调整为224*224,采用 opencv 的方式处理图片,插值的方式为双线性,输出为float32
  ResizeTransformer(target_size=(224, 224), mode='opencv', method=1)

  # 将输入图片大小调整为256*256,采用skimage的方式处理图片,插值的方式为双线性,输出为float32
  ResizeTransformer(target_size=(256, 256))

  # 将输入图片大小调整为256*256,采用skimage的方式处理图片,插值的方式为双线性,输出为uint8
  ResizeTransformer(target_size=(256, 256), data_type="uint8")

PILResizeTransformer

说明

使用PIL库做调整图像大小的操作。

参数

  • size:目标大小,值为元组,如(240,240)。

  • interpolation:指定插值的方式,取值范围:(Image.NEAREST,Image.BILINEAR,Image.BICUBIC,Image.LANCZOS), 默认值为Image.BILINEAR。

    • Image.NEAREST:最近邻采样;

    • Image.BILINEAR:线性插值;

    • Image.BICUBIC:三次样条插值;

    • Image.LANCZOS:高质量下采样滤波器。

使用举例

  # 将输入图片大小调整为256*256 插值的方式为线性插值
  PILResizeTransformer(size=256)

  # 将输入图片大小调整为256*256 插值的方式为高质量下采样滤波器
  PILResizeTransformer(size=256, interpolation=Image.LANCZOS)

ShortLongResizeTransformer

说明

按照原比例对输入图片进行缩放的操作,新图片的大小与设置的参数有关。操作方式如下:

  1. 先以short_size的大小除以原图片的宽和高里最小值,以这个值为缩放比例系数。

  2. 当缩放比例系数乘以原图片的宽和高中的最大值,得到的结果大于long_size的数值时,缩放比例系数将变更为long_size除以原图片的宽和高中的最大值。

  3. 使用opencv中的resize方法,根据上方得到的缩放比例系数重新裁剪图片。

参数

  • short_size:预期裁剪后的短边的长度。

  • long_size:预期裁剪后的长边的长度。

  • include_im:默认值为True,设置为True时, 会在返回时除了返回处理后的图片, 还会返回原图片。

使用举例

  # 短边长度为20,长边长度为100,返回处理后的图片及原图片
  ShortLongResizeTransformer(short_size=20, long_size=100)

PadTransformer

说明

通过用目标大小的size值除以输入图片宽或者高里的最大值为系数,然后使用这个系数乘以原有的宽高,resize图片。 然后根据新图片的大小,除以size_divisor后向上取整后,再乘以size_divisor,为新的宽高,生成新的图片的操作。

参数

  • size_divisor:大小除数 ,默认值为128。

  • target_size:目标大小,默认值为512。

使用举例

  # pad大小为1024*1024
  PadTransformer(size_divisor=1024, target_size=1024)

ShortSideResizeTransformer

说明

根据期望的短边的长度,使用现在的长短边的比例,中心裁剪出新的图片大小的操作。

参数

  • short_size:预期的短边的长度。

  • data_type:输出结果的类型,取值范围为(”float”,”uint8”),默认取值”float32”, 以 float32 类型输出,设置为uint8时,输出类型将为uint8。

  • interpolation:指定插值的方式,取值范围为 opencv 中采用的插值方式,默认为空。

    目前interpolation仅支持为空或opencv中的INTER_CUBIC两种插值方法,当interpolation为空时,默认使用INTER_LINEAR方式。

    以下为opencv中支持的插值方式及说明(目前未支持的插值方式将在后续迭代中逐步支持):

    • INTER_NEAREST,最近邻插值;

    • INTER_LINEAR,双线性插值,当interpolation为空时,默认使用这种方法。

    • INTER_CUBIC,双三次插值4x4像素邻域内的双立方插值。

    • INTER_AREA,使用像素面积关系重采样。它可能是图像抽取的首选方法,因为它可以提供无莫尔条纹的结果。但是当图像被缩放时,它类似于INTER_NEAREST方法。

    • INTER_LANCZOS4,8x8邻域的Lanczos插值。

    • INTER_LINEAR_EXACT,位精确双线性插值。

    • INTER_NEAREST_EXACT,位精确最近邻插值。这将产生与PIL、scikit-image或Matlab中的最近邻方法相同的结果。

    • INTER_MAX,插值代码的掩码。

    • WARP_FILL_OUTLIERS,标志,填充所有目标图像像素。如果其中一些对应于源图像中的异常值,则将它们设置为零。

    • WARP_INVERSE_MAP,标志,逆变换。

使用举例

  # 将短边大小调整为256,插值方式为双线性插值
  ShortSideResizeTransformer(short_size=256)

  # 将短边大小调整为256,插值方式为8x8像素邻域内的Lanczos插值
  ShortSideResizeTransformer(short_size=256, interpolation=Image.LANCZOS4)

PaddedCenterCropTransformer

说明

使用填充的方式对图片中心进行裁剪的操作。

.. attention::

仅适用于EfficientNet-lite相关实例模型。

计算方式为:

  1. 计算系数,int((float( image_size ) / ( image_size + crop_pad ))。

  2. 计算中心size的大小, 系数 * np.minimum( 原始图片的高度, 原始图片的宽度 ))。

  3. 根据计算出来的size大小,做中心裁剪。

参数

  • image_size:图片的大小,默认值为224。

  • crop_pad:中心填充的大小,默认值为32。

使用举例

  # 裁剪大小为240*240,填充值为32
  PaddedCenterCropTransformer(image_size=240, crop_pad=32)

  # 裁剪大小为224*224,填充值为32
  PaddedCenterCropTransformer()

BGR2RGBTransformer

说明

将输入格式由BGR转成RGB的操作。

参数

  • data_format:数据格式,取值范围为(CHW,HWC),默认值为CHW。

使用举例

  # layout为NCHW时,做BGR转为RGB
  BGR2RGBTransformer()

  # layout为NHWC时,做BGR转为RGB
  BGR2RGBTransformer(data_format="HWC")

RGB2BGRTransformer

说明

将输入格式由RGB转成BGR的操作。

参数

  • data_format:数据格式,取值范围为(CHW,HWC),默认值为CHW。

使用举例

  # layout为NCHW时,做RGB转成BGR
  RGB2BGRTransformer()

  # layout为NHWC时,做RGB转成BGR
  RGB2BGRTransformer(data_format="HWC")

RGB2GRAYTransformer

说明

将输入格式由RGB转成GRAY的操作。

参数

  • data_format:输入的layout类型,取值范围(”CHW”,”HWC”),默认为”CHW”。

使用举例

  # layout为NCHW时,做RGB转成GRAY
  RGB2GRAYTransformer(data_format='CHW')

  # layout为NHWC时,做RGB转成GRAY
  RGB2GRAYTransformer(data_format='HWC')

BGR2GRAYTransformer

说明

将输入格式由 BGR 转成 GRAY 的操作。

参数

  • data_format:输入的layout类型,取值范围 [”CHW”,”HWC”],默认值为”CHW”。

使用举例

  # layout为NCHW时,做BGR转成GRAY
  BGR2GRAYTransformer(data_format='CHW')

  # layout为NHWC时,做BGR转成GRAY
  BGR2GRAYTransformer(data_format='HWC')

RGB2GRAY_128Transformer

说明

输入格式由RGB转成GRAY_128的操作。GRAY_128取值范围为(-128,127)。

参数

  • data_format:输入的layout类型,取值范围为[”CHW”,”HWC”],默认值为”CHW”,此项为必填项。

使用举例

  # layout为NCHW时,做RGB转成GRAY_128
  RGB2GRAY_128Transformer(data_format='CHW')

  # layout为NHWC时,做RGB转成GRAY_128
  RGB2GRAY_128Transformer(data_format='HWC')

RGB2YUV444Transformer

说明

将输入格式由RGB转成YUV444的操作。

参数

  • data_format:输入的layout类型,取值范围为[”CHW”, “HWC”],默认值为”CHW”,此项为必填项。

使用举例

  # layout为NCHW时,做BGR转成YUV444
  BGR2YUV444Transformer(data_format='CHW')

  # layout为NHWC时,做BGR转成YUV444
  BGR2YUV444Transformer(data_format='HWC')

BGR2YUV444Transformer

说明

将输入格式由BGR转成YUV444的操作。

参数

  • data_format:输入的layout类型,取值范围为[”CHW”,”HWC”],默认值为 “CHW”,此项为必填项。

使用举例

  # layout为NCHW时,做BGR转成YUV444
  BGR2YUV444Transformer(data_format='CHW')

  # layout为NHWC时,做BGR转成YUV444
  BGR2YUV444Transformer(data_format='HWC')

BGR2YUV444_128Transformer

说明

将输入格式由BGR转成YUV444_128的操作。YUV444_128取值范围为(-128,127)。

参数

  • data_format:输入的layout类型,取值范围为[”CHW”,”HWC”],默认值为 “CHW”,此项为必填项。

使用举例

  # layout为NCHW时,做BGR转成YUV444_128
  BGR2YUV444_128Transformer(data_format='CHW')

  # layout为NHWC时,做BGR转成YUV444_128
  BGR2YUV444_128Transformer(data_format='HWC')

RGB2YUV444_128Transformer

说明

将输入格式由RGB转成YUV444_128的操作。YUV444_128取值范围为(-128,127)。

参数

  • data_format:输入的layout类型,取值范围为[”CHW”,”HWC”],默认值为”CHW”,此项为必填项。

使用举例

  # layout为NCHW时,做RGB转成 YUV444_128
  RGB2YUV444_128Transformer(data_format='CHW')

  # layout为NHWC时,做RGB转成 YUV444_128
  RGB2YUV444_128Transformer(data_format='HWC')

BGR2YUVBT601VIDEOTransformer

说明

将输入格式由BGR转成YUV_BT601_Video_Range的操作。

YUV_BT601_Video_Range,某些摄像头输入数据都是YUV BT601(Video Range)格式的,取值范围为16~235,该transformer就是适配这种格式的数据产生的。

参数

  • data_format:输入的layout类型,取值范围为[”CHW”,”HWC”],默认值为”CHW”,此项为必填项。

使用举例

  # layout为 NCHW时,做BGR转成YUV_BT601_Video_Range
  BGR2YUVBT601VIDEOTransformer(data_format='CHW')

  # layout为NHWC时,做BGR转成YUV_BT601_Video_Range
  BGR2YUVBT601VIDEOTransformer(data_format='HWC')

RGB2YUVBT601VIDEOTransformer

说明

将输入格式由RGB转成YUV_BT601_Video_Range的操作。

YUV_BT601_Video_Range,某些摄像头输入数据都是YUV BT601(Video Range)格式的,取值范围为16~235,该transformer就是适配这种格式的数据产生的。

参数

  • data_format:输入的layout类型,取值范围为[”CHW”,”HWC”],默认值为”CHW”,此项为必填项。

使用举例

  # layout为NCHW时,做RGB转成YUV_BT601_Video_Range
  RGB2YUVBT601VIDEOTransformer(data_format='CHW')

  # layout为NHWC时,做RGB转成YUV_BT601_Video_Range
  RGB2YUVBT601VIDEOTransformer(data_format='HWC')

YUVTransformer

说明

将输入格式转成YUV444的操作。

参数

  • color_sequence:颜色序列,此项为必填项。

使用举例

  # 将BGR读入的图片转为YUV444
  YUVTransformer(color_sequence="BGR")

  # 将RGB读入的图片转为YUV444
  YUVTransformer(color_sequence="RGB")

ReduceChannelTransformer

说明

将C通道缩减为单通道的操作。该transformer主要是针对于C通道,如shape为1*3*224*224 改为1*1*224*224。 使用时layout一定要和data_format值对齐,避免造成删错通道。

参数

  • data_format:输入的layout类型,取值范围为[”CHW”, “HWC”],默认值为”CHW”。

使用举例

  # 删除layout为NCHW的C通道
  ReduceChannelTransformer()
  # 或者
  ReduceChannelTransformer(data_format="CHW")

  # 删除layout为NHWC的C通道
  ReduceChannelTransformer(data_format="HWC")

BGR2NV12Transformer

说明

将输入格式由BGR转成NV12的操作。

参数

  • data_format:输入的layout类型,取值范围为[”CHW”,”HWC”],默认值为”CHW”。

  • cvt_mode:cvt模式,取值范围为(rgb_calc,opencv),默认值为rgb_calc。

    • rgb_calc,采用mergeUV的方式处理图片;

    • opencv,采用opencv的方式处理图片。

使用举例

  # layout为NCHW时,由BGR转为NV12,采用rgb_calc模式处理图片
  BGR2NV12Transformer()
  # 或者
  BGR2NV12Transformer(data_format="CHW")

  # layout为NHWC时,由BGR转为NV12,采用opencv模式处理图片
  BGR2NV12Transformer(data_format="HWC", cvt_mode="opencv")

RGB2NV12Transformer

说明

将输入格式由RGB转成NV12的操作。

参数

  • data_format:输入的 layout 类型,取值范围 [”CHW”, “HWC”], 默认值为”CHW”。

  • cvt_mode:cvt模式,取值范围为(rgb_calc,opencv),默认值为rgb_calc。

    • rgb_calc,采用mergeUV的方式处理图片;

    • opencv,采用opencv的方式处理图片。

使用举例

  # layout为NCHW时,有RGB转为NV12,采用rgb_calc模式处理图片
  RGB2NV12Transformer()
  # 或者
  RGB2NV12Transformer(data_format="CHW")

  # layout为NHWC时,有RGB转为NV12,采用opencv模式处理图片
  RGB2NV12Transformer(data_format="HWC", cvt_mode="opencv")

NV12ToYUV444Transformer

说明

将输入格式由NV12转成YUV444的操作。

参数

  • target_size:目标大小,值为元组,如(240,240)。

  • yuv444_output_layout:yuv444输出的layout,取值范围为(HWC,CHW),默认值为”HWC”。

使用举例

  # layout为NCHW ,大小为768*768, nv12转yuv444
  NV12ToYUV444Transformer(target_size=(768, 768))

  # layout为NHWC ,大小为224*224, nv12转yuv444
  NV12ToYUV444Transformer((224, 224), yuv444_output_layout="HWC")

WarpAffineTransformer

说明

用于做图像仿射变换的操作。

参数

  • input_shape:输入的shape值。

  • scale:乘以的系数。

使用举例

  # 大小为512*512,长边长度为1.0
  WarpAffineTransformer((512, 512), 1.0)

F32ToS8Transformer

说明

用于做输入格式从float32转换为int8的操作。

参数:不涉及。

使用举例

  # 输入格式从 float32转为 int8
  F32ToS8Transformer()

F32ToU8Transformer

说明

用于做输入格式从float32转换为uint8的操作。

参数:不涉及。

使用举例

  # 输入格式从 float32 转为 uint8
  F32ToU8Transformer()

7.6.2.2. 示例YOLOv5x模型使用说明

  1. YOLOv5x模型:

  • 可以从URL:yolov5-2.0 中下载相应的pt文件。

    在clone代码时,请确认您使用的Tags是 v2.0 ,否则将导致转换失败。

  • md5sum码:

md5sum File
2e296b5e31bf1e1b6b8ea4bf36153ea5 yolov5l.pt
16150e35f707a2f07e7528b89c032308 yolov5m.pt
42c681cf466c549ff5ecfe86bcc491a0 yolov5s.pt
069a6baa2a741dec8a2d44a9083b6d6e yolov5x.pt
  • 为了更好地适配后处理代码,我们在ONNX模型导出前对Github代码做了如下修改 (代码参见:https://github.com/ultralytics/yolov5/blob/v2.0/models/yolo.py):


    def forward(self, x):
        # x = x.copy()  # for profiling
        z = []  # inference output
        self.training |= self.export
        for i in range(self.nl):
            x[i] = self.m[i](x[i])  # conv
            bs, _, ny, nx = x[i].shape  # x(bs,255,20,20) to x(bs,3,20,20,85)
            #  x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
            x[i] = x[i].permute(0, 2, 3, 1).contiguous()
  • 说明: 去除了每个输出分支尾部从4维到5维的reshape(即不将channel从255拆分成3x85),然后将layout从NHWC转换成NCHW再输出。

    以下左图为修改前的模型某一输出节点的可视化图,右图则为修改后的对应输出节点可视化图。

    yolov5

  • 下载完成后通过脚本 https://github.com/ultralytics/yolov5/blob/v2.0/models/export.py 进行pt文件到ONNX文件的转换。

  • 注意事项

    在使用export.py脚本时,请注意:

    1. 由于地瓜AI工具链支持的ONNX opset版本为 1011,请将 torch.onnx.exportopset_version 参数根据您要使用的版本进行修改。

    2. torch.onnx.export 部分的默认输入名称参数由 'images' 改为 'data',与模型转换示例包的YOLOv5x示例脚本保持一致。

    3. parser.add_argument 部分中默认的数据输入尺寸640x640改为模型转换示例包YOLOv5x示例中的672x672。

7.6.2.3. 模型精度调优checklist

请严格按照下图中步骤1-5来进行模型精度验证并保留每个步骤的代码和结果:

model_accuracy_check

在进行排查前,请确认当前模型转换所用的Docker镜像或转换环境版本,并保留版本信息

1. 验证浮点onnx模型的推理结果

进入模型转换环境,来测试浮点onnx模型(特指从DL框架导出的onnx模型)的单张结果,此步骤结果应与训练后的模型推理结果完全一致(nv12格式除外,可能会引入少许差异)

可参考如下示例代码步骤,来确认浮点onnx模型的推理的步骤、数据预处理、后处理代码是否正确!


  from horizon_tc_ui import HB_ONNXRuntime
  import numpy as np
  import cv2

  def preprocess(input_name):
      # BGR->RGB、Resize、CenterCrop···
      # HWC->CHW
      # normalization
      return data

  def main():
      # 加载模型文件
      sess = HB_ONNXRuntime(model_file=MODEL_PATH)
      # 获取输入&输出节点名称
      input_names = [input.name for input in sess.get_inputs()]
      output_names = [output.name for output in sess.get_outputs()]
      # 准备模型输入数据
      feed_dict = dict()
      for input_name in input_names:
          feed_dict[input_name] = preprocess(input_name)

      # 原始浮点onnx,数据dtype=float32, 开始模型推理,推理的返回值是一个list,依次与output_names指定名称一一对应
      outputs = sess.run(output_names, feed_dict)

      # 后处理
      postprocess(outputs)

  if __name__ == '__main__':
      main()

2. 验证yaml配置文件以及前、后处理代码的正确性

测试 original_float.onnx 模型的单张结果,应与浮点onnx模型推理结果完全一致(nv12格式除外,由于nv12数据本身有损,可能会引入少许差异)

使用开源工具 Netron 打开 original_float.onnx 模型,并查看预处理节点 HzPreprocess 算子的详细属性,获取我们 数据预处理 需要的参数:data_formatinput_type

由于HzPreprocess节点的存在,会使得转换后的模型其预处理操作可能会和原始模型有所不同,该算子是在进行模型转换时,根据yaml配置文件中的配置参数(input_type_rt、input_type_train以及norm_type、mean_value、scale_value)来决定是否为模型加入HzPreprocess节点,预处理节点的生成细节,请参考PTQ原理及步骤详解章节的 norm_type 配置参数说明 内容,另外预处理节点会出现在转换过程产生的所有产物中。

理想状态下,这个HzPreprocess节点应该完成 input_type_rt 到 input_type_train 的完整转换, 但实际情况是整个type转换过程需要使用地瓜AI芯片硬件完成,但ONNX模型里面并没有包含硬件转换的部分,因此ONNX的真实输入类型会使用一种中间类型,这种中间类型就是硬件对 input_type_rt 的处理结果类型, 故针对图像输入数据类型为:RGB/BGR/NV12/YUV444/GRAY,并且数据dtype= uint8的模型时,在预处理代码中需要做 -128 的操作,featuremap 数据类型因为使用的是float32,因此预处理代码中 不需要-128 的操作; original_float.onnx的数据layout(NCHW/NHWC)会保持和原始浮点模型的输入layout一致。

可参考如下示例代码步骤,来确认 original_float.onnx 模型的推理的步骤、数据预处理、后处理代码是否正确!

数据预处理部分建议参考使用地瓜模型转换 horizon_model_convert_sample 示例包中的caffe、onnx等示例模型的预处理步骤方法


  from horizon_tc_ui import HB_ONNXRuntime
  import numpy as np
  import cv2

  def preprocess(input_name):
      # BGR->RGB、Resize、CenterCrop···
      # HWC->CHW(通过onnx模型输入节点的具体shape来判断是否需要做layout转换)
      # normalization(若已通过yaml文件将norm操作放入了模型中,则不要在预处理中做重复操作)
      #-128(除featuremap输入外,其他类型输入均需做-128操作,即由unit8转为int8)
      return data

  def main():
      # 加载模型文件
      sess = HB_ONNXRuntime(model_file=MODEL_PATH)
      # 获取输入&输出节点名称
      input_names = [input.name for input in sess.get_inputs()]
      output_names = [output.name for output in sess.get_outputs()]
      # 准备模型输入数据
      feed_dict = dict()
      for input_name in input_names:
          feed_dict[input_name] = preprocess(input_name)
      #图像输入的模型(RGB/BGR/NV12/YUV444/GRAY),数据dtype= uint8, featuremap模型,数据dtype=float32,
      outputs = sess.run(output_names, feed_dict)

      # 后处理
      postprocess(outputs)

  if __name__ == '__main__':
      main()

3. 验证模型的图优化阶段未引入精度误差

测试 optimize_float.onnx 模型的单张结果,应与original_float.onnx推理结果完全一致

使用开源工具 Netron 打开 optimize_float.onnx 模型,并查看预处理节点 HzPreprocess 算子的详细属性,获取我们数据预处理需要的参数:data_formatinput_type;

optimize_float.onnx模型的推理可参考如下示例代码步骤,来确认 optimize_float.onnx 模型的推理的步骤、数据预处理、后处理代码是否正确!

数据预处理部分建议参考使用地瓜模型转换 horizon_model_convert_sample 示例包中的caffe、onnx等示例模型的预处理步骤方法


  from horizon_tc_ui import HB_ONNXRuntime
  import numpy as np
  import cv2

  def preprocess(input_name):
      # BGR->RGB、Resize、CenterCrop···
      # HWC->CHW(通过onnx模型输入节点的具体shape来判断是否需要做layout转换)
      # normalization(若已通过yaml文件将norm操作放入了模型中,则不要在预处理中做重复操作)
      #-128(除featuremap输入外,其他类型输入均需做-128操作,即由unit8转为int8)
      return data

  def main():
      # 加载模型文件
      sess = HB_ONNXRuntime(model_file=MODEL_PATH)
      # 获取输入&输出节点名称
      input_names = [input.name for input in sess.get_inputs()]
      output_names = [output.name for output in sess.get_outputs()]
      # 准备模型输入数据
      feed_dict = dict()
      for input_name in input_names:
          feed_dict[input_name] = preprocess(input_name)
      #图像输入的模型(RGB/BGR/NV12/YUV444/GRAY),数据dtype= uint8, featuremap模型,数据dtype=float32
      outputs = sess.run(output_names, feed_dict)

      # 后处理
      postprocess(outputs)

  if __name__ == '__main__':
      main()

4. 验证量化精度是否满足预期

测试quantized.onnx的精度指标。

使用开源工具 Netron 打开 quantized.onnx 模型,并查看预处理节点 HzPreprocess 算子的详细属性,获取我们数据预处理需要的参数:data_formatinput_type;

quantized.onnx模型的推理可参考如下示例代码步骤,来确认 quantized.onnx 模型的推理的步骤、数据预处理、后处理代码是否正确!

数据预处理部分建议参考使用地瓜模型转换 horizon_model_convert_sample 示例包中的caffe、onnx等示例模型的预处理步骤方法


  from horizon_tc_ui import HB_ONNXRuntime
  import numpy as np
  import cv2

  def preprocess(input_name):
      # BGR->RGB、Resize、CenterCrop···
      # HWC->CHW(通过onnx模型输入节点的具体shape来判断是否需要做layout转换)
      # normalization(若已通过yaml文件将norm操作放入了模型中,则不要在预处理中做重复操作)
      #-128(除featuremap输入外,其他类型输入均需做-128操作,即由unit8转为int8)
      return data

  def main():
      # 加载模型文件
      sess = HB_ONNXRuntime(model_file=MODEL_PATH)
      # 获取输入&输出节点名称
      input_names = [input.name for input in sess.get_inputs()]
      output_names = [output.name for output in sess.get_outputs()]
      # 准备模型输入数据
      feed_dict = dict()
      for input_name in input_names:
          feed_dict[input_name] = preprocess(input_name)
      #图像输入的模型(RGB/BGR/NV12/YUV444/GRAY),数据dtype= uint8,featuremap模型,数据dtype=float32
      outputs = sess.run(output_names, feed_dict)

      # 后处理
      postprocess(outputs)

  if __name__ == '__main__':
      main()

5. 确保模型编译过程无误且板端推理代码正确

使用 hb_verifier 工具验证quantized.onnx和.bin的一致性,模型输出应至少满足小数点后2-3位对齐

hb_verifier 工具(详细介绍可参考)的使用方法,请参考PTQ原理及步骤详解章节的 hb_verifier 工具 内容。

若模型一致性校验通过,则请仔细检查开发板端的前、后处理代码!

若quantized.onnx与.bin模型一致性校验失败,请联系地瓜技术人员

7.6.2.4. 模型量化yaml配置文件模板

Caffe模型量化yaml文件模板

请新建 caffe_config.yaml 文件,并直接拷贝以下内容,然后只需填写标记为 必选参数 的参数即可进行模型转换,若需了解更多参数的使用说明,可参考 yaml配置文件详解 章节内容。


# Copyright (c) 2020 Horizon Robotics.All Rights Reserved.

# 模型转化相关的参数
model_parameters:

  # 必选参数
  # Caffe浮点网络数据模型文件, 例如:caffe_model: './horizon_ultra_caffe.caffemodel'
  caffe_model: ''

  # 必选参数
  # Caffe网络描述文件, 例如:prototxt: './horizon_ultra_caffe.prototxt'
  prototxt: ''

  march: "bayes-e"
  layer_out_dump: False
  working_dir: 'model_output'
  output_model_file_prefix: 'horizon_x5'

# 模型输入相关参数
input_parameters:

  input_name: ""
  input_shape: ''
  input_type_rt: 'nv12'
  input_layout_rt: ''

  # 必选参数
  # 原始浮点模型训练框架中所使用训练的数据类型,可选的值为rgb/bgr/gray/featuremap/yuv444, 例如:input_type_train: 'bgr'
  input_type_train: ''

  # 必选参数
  # 原始浮点模型训练框架中所使用训练的数据排布, 可选值为 NHWC/NCHW, 例如:input_layout_train: 'NHWC'
  input_layout_train: ''

  #input_batch: 1

  # 必选参数
  # 原始浮点模型训练框架中所使用数据预处理方法,可配置:no_preprocess/data_mean/data_scale/data_mean_and_scale
  # no_preprocess 不做任何操作,对应的 mean_value  或者 scale_value 均无需配置
  # data_mean 减去通道均值mean_value,对应的 mean_value 需要配置,并注释掉scale_value
  # data_scale 对图像像素乘以data_scale系数,对应的 scale_value需要配置,并注释掉mean_value
  # data_mean_and_scale 减去通道均值后再乘以scale系数,标识下方对应的 mean_value  和 scale_value 均需配置
  norm_type: ''

  # 必选参数
  # 图像减去的均值, 如果是通道均值,value之间必须用空格分隔
  # 例如:mean_value: 128.0 或者 mean_value: 111.0 109.0 118.0
  mean_value:

  # 必选参数
  # 图像预处理缩放比例,如果是通道缩放比例,value之间必须用空格分隔,计算公式:scale = 1/std
  # 例如:scale_value: 0.0078125 或者 scale_value: 0.0078125 0.001215 0.003680
  scale_value:

# 模型量化相关参数
calibration_parameters:

  # 必选参数
  # 模型量化的参考图像的存放目录,图片格式支持Jpeg、Bmp等格式,图片来源一般是从测试集中选择100张图片,并要覆盖典型场景,不要是偏僻场景,如过曝光、饱和、模糊、纯黑、纯白等图片
  # 请根据 02_preprocess.sh 脚本中的文件夹路径来配置,例如:cal_data_dir: './calibration_data_yuv_f32'
  cal_data_dir: ''

  cal_data_type: 'float32'
  calibration_type: 'default'

# 编译器相关参数
compiler_parameters:

  compile_mode: 'latency'
  debug: False
  optimize_level: 'O3'

ONNX模型量化yaml文件模板

请新建 onnx_config.yaml 文件,并直接拷贝以下内容,然后只需填写标记为 必选参数 的参数即可进行模型转换,若需了解更多参数的使用说明,可参考 yaml配置文件详解 章节内容。


# Copyright (c) 2020 Horizon Robotics.All Rights Reserved.

# 模型转化相关的参数
model_parameters:

  # 必选参数
  # Onnx浮点网络数据模型文件, 例如:onnx_model: './horizon_ultra_onnx.onnx'
  onnx_model: ''

  march: "bayes-e"
  layer_out_dump: False
  working_dir: 'model_output'
  output_model_file_prefix: 'horizon_ultra'

# 模型输入相关参数
input_parameters:

  input_name: ""
  input_shape: ''
  input_type_rt: 'nv12'
  input_layout_rt: ''

  # 必选参数
  # 原始浮点模型训练框架中所使用训练的数据类型,可选的值为rgb/bgr/gray/featuremap/yuv444, 例如:input_type_train: 'bgr'
  input_type_train: ''

  # 必选参数
  # 原始浮点模型训练框架中所使用训练的数据排布, 可选值为 NHWC/NCHW, 例如:input_layout_train: 'NHWC'
  input_layout_train: ''

  #input_batch: 1

  # 必选参数
  # 原始浮点模型训练框架中所使用数据预处理方法,可配置:no_preprocess/data_mean/data_scale/data_mean_and_scale
  # no_preprocess 不做任何操作,对应的 mean_value  或者 scale_value 均无需配置
  # data_mean 减去通道均值mean_value,对应的 mean_value 需要配置,并注释掉scale_value
  # data_scale 对图像像素乘以data_scale系数,对应的 scale_value需要配置,并注释掉mean_value
  # data_mean_and_scale 减去通道均值后再乘以scale系数,标识下方对应的 mean_value  和 scale_value 均需配置
  norm_type: ''

  # 必选参数
  # 图像减去的均值, 如果是通道均值,value之间必须用空格分隔
  # 例如:mean_value: 128.0 或者 mean_value: 111.0 109.0 118.0
  mean_value:

  # 必选参数
  # 图像预处理缩放比例,如果是通道缩放比例,value之间必须用空格分隔,计算公式:scale = 1/std
  # 例如:scale_value: 0.0078125 或者 scale_value: 0.0078125 0.001215 0.003680
  scale_value:

# 模型量化相关参数
calibration_parameters:

  # 必选参数
  # 模型量化的参考图像的存放目录,图片格式支持Jpeg、Bmp等格式,图片来源一般是从测试集中选择100张图片,并要覆盖典型场景,不要是偏僻场景,如过曝光、饱和、模糊、纯黑、纯白等图片
  # 请根据 02_preprocess.sh 脚本中的文件夹路径来配置,例如:cal_data_dir: './calibration_data_yuv_f32'
  cal_data_dir: ''

  cal_data_type: 'float32'
  calibration_type: 'default'

# 编译器相关参数
compiler_parameters:

  compile_mode: 'latency'
  debug: False
  optimize_level: 'O3'

7.6.2.5. 定点.bin模型上板多batch使用说明

  • 1.模型转换时,在yaml配置文件里通过input_batch配置batch_size;

  • 2.上板bin模型输入时,以原始模型维度1×3×224×224,修改input_batch为10,也就是10×3×224×224这个维度举例:

  • 准备数据:

    Image图像数据:设置 aligned_shape = valid_shape ,然后按单张数据准备的方式,把10张图片依次按顺序写入申请的内存空间;

    FeatureMap数据:按aligned_shape把数据padding好,然后按单batch数据准备的方式,把10份数据依次按顺序写入申请的内存空间,模型推理流程和单batch模型推理流程一致;

7.6.2.6. 自定义算子开发说明

简介

地瓜工具链中已经支持了丰富的算子,在大多数情况下,您的模型应该可以通过前文所述模型转换顺利部署到地瓜计算平台上。 已支持的算子情况可以参考 算子支持约束列表 章节。 少部分算子不支持情况下,我们强烈建议您先尝试下替换算子的可能性,这样有利于将地瓜计算平台能力充分发挥出来,且开发成本会更低。

自定义算子只提供CPU上算子开发能力,一个完整的自定义算子应用过程包括创建模板、算子实现、算子编译、含自定义算子模型转换和运行含自定义op模型几个阶段。具体流程如下图所示:

custom_op_development

如图所示,定义自定义OP需要有两部分的任务:在模型转换阶段,需要提供自定义OP的python代码;在模拟器/上板运行推理阶段,需要提供自定义OP的C++代码。 要确保这两部分的代码运算的一致性。

含自定义算子的模型转换

模型文件修改

在准备好自定义算子实现后,为了将算子应用起来,您需要从原始模型文件和模型转换配置两个方面做出相应调整 (下面分别以 Caffe 模型和 ONNX 模型为例)。

Caffe 模型

原始模型文件中,将自定义算子对应的算子类型标记为 Custom,并提供一组 custom_param,示例如下。


  layer {
    name: "hr_op"
    type: "Custom"
    bottom: "res3d_in"
    top: "res3d"
    custom_param {
      kind: "CustomIdentity"
      shape {
        dim: 1
        dim: 512
        dim: 28
        dim: 28
      }
      params: "'kernel_size': 10 \n'threshold': 0.5"
    }
  }

以上完整 custom_param 示例中:

  • kind 是自定义算子的内部实现名称,该自定义OP为恒等OP,因此命名为 CustomIdentity,该名称在后续Python及C++代码中均会体现。

  • shape 是算子的输出尺寸,需要完整指定。

  • params 是算子的传入参数指定形式为 'param_name': param_value,多个参数之间使用 \n 分隔。

在模型转换配置中,使用自定义算子需要在配置文件中加入一个新的自定义op参数组如下:


  #...

  custom_op:
    # 自定义op的校准方式
    custom_op_method: register

    # 自定义OP的实现文件
    op_register_files: sample_custom.py

对于 Caffe 模型,以上参数组中的两个参数都是必须配置的。 custom_op_method 固定使用 registerop_register_files 是自定义算子计算的实现文件,请使用相对路径。

完成这些配置后,模型转换的后续步骤与其他一般模型转换过程一致。

ONNX 模型

1.含有自定义算子的Onnx模型的获取:

  • 从pytorch等其他框架转换而来


import torch
from horizon_nn.horizon_onnx.onnx_pb import TensorProto
from torch.onnx.symbolic_helper import parse_args
from torch.onnx.utils import register_custom_op_symbolic
from torch import Tensor

model = torch.hub.load('pytorch/vision:v0.10.0', 'googlenet', pretrained=True)

def _transform_input(x: Tensor) -> Tensor:
    return x

model._transform_input = _transform_input

@parse_args("v", "v")
def horizon_pool(g, input, output_size):
    return g.op(
        'horizon.custom::PyOp', #required, ! must be 'horizon.custom' domain !
        input,
        class_name_s="GlobalAveragePool",  #required ! must match the class def name in sample_custom python file !
        compute_s="compute",  #optional, 'compute' by default
        module_s="sample_custom",  #required ! must match the file name of the "op_register_files" !
        input_types_i=[TensorProto.FLOAT],  #required
        output_types_i=[TensorProto.FLOAT],  #required
        output_shape_s=["1, 1024, 1, 1"]) #required

d_input = torch.rand(1, 3, 224, 224)
register_custom_op_symbolic('::adaptive_avg_pool2d',
                            horizon_pool,
                            opset_version=11)
torch.onnx.export(model, d_input, "googlenet_cop.onnx", opset_version=11)
  • 直接生成onnx模型

参考代码:


import onnx
import numpy as np
from onnx import helper, checker, shape_inference, numpy_helper, TensorProto


def make_normal_data(shape):
    return np.random.normal(loc=0.0, scale=1.0, size=shape).astype(np.float32)


# conv
def make_simple_model():

    # create nodes
    conv_input_shape = (1, 3, 224, 224)
    conv_output_shape = (1, 3, 224, 224)

    add_param_shape = (1, 3, 224, 224)
    add_1_param_data = np.zeros(add_param_shape).astype(np.float32)
    add_2_param_data = np.ones(add_param_shape).astype(np.float32)

    conv_weight_shape = (3, 3, 3, 3)
    conv_output_shape = (1, 3, 224, 224)
    conv_weight_data = make_normal_data(conv_weight_shape)

    add_1_node = helper.make_node(
        "PyOp",  # required, 类型必须是'PyOp'
        name="add_1",  # required, 不同的op名称不能相同
        inputs=["input0", "add_1_param"],  # required, 需要是一个list, 且需要与实现文件中的输入数量保持一致
        outputs=["add_1_out"],  # required, 需要是一个list, 且需要与实现文件中的输出数量保持一致
        domain="horizon.cop1",  # required, 不同实现逻辑的自定义算子实现需要通过不同的domain名称来实现
        class_name="Cop1",  # required, 需要与自定义算子的实现文件中的class名称一致
        module="custom_op.horizon_ops",  # required, 需要与包含自定义算子的实现文件的路径一致
        compute="compute",  # required, 需要与自定义算子实现class中的计算逻辑函数一致
        input_types=[
            TensorProto.FLOAT,
            TensorProto.FLOAT,
        ],  # required, 需要是一个list, 其长度需要与该算子的inputs属性数量一致, 且与实现文件中的输入数量保持一致
        output_types=[
            TensorProto.FLOAT
        ],  # required, 需要是一个list, 其长度需要与该算子的outputs属性数量一致, 且与实现文件中的输出数量保持一致
        output_shape=["1, 3, 224, 224"],  # optional, 在模型中未添加pyop的输出 value_info时, 必须填写
    )

    add_2_node = helper.make_node(
        "PyOp",
        name="add_2",
        inputs=["input1", "add_1_out", "add_2_param"],
        outputs=["add_2_out", "output0"],
        domain="horizon.cop2",
        class_name="Cop2",
        module="custom_op.horizon_ops",
        compute='compute',
        input_types=[TensorProto.FLOAT, TensorProto.FLOAT,
                      TensorProto.FLOAT],  #required
        output_types=[TensorProto.FLOAT, TensorProto.FLOAT],  #required
        output_shape=["1, 3, 224, 224", "1, 3, 224, 224"])

    conv_1_node = helper.make_node("Conv",
                                    inputs=["add_2_out", "W0"],
                                    outputs=["output1"],
                                    dilations=(1, 1),
                                    group=1,
                                    kernel_shape=(3, 3),
                                    pads=(1, 1, 1, 1),
                                    name="conv_1")
    # nodes
    nodes = [add_1_node, add_2_node, conv_1_node]

    # inputs
    model_input_1 = helper.make_tensor_value_info("input0", TensorProto.FLOAT,
                                                  conv_input_shape)
    model_input_2 = helper.make_tensor_value_info("input1", TensorProto.FLOAT,
                                                  conv_input_shape)

    # Outputs
    model_output_1 = helper.make_tensor_value_info("output0",
                                                    TensorProto.FLOAT,
                                                    conv_output_shape)
    model_output_2 = helper.make_tensor_value_info("output1",
                                                    TensorProto.FLOAT,
                                                    conv_output_shape)

    # Intermediate tensors
    add_1_out = helper.make_tensor_value_info("add_1_out", TensorProto.FLOAT,
                                              conv_output_shape)
    add_2_out = helper.make_tensor_value_info("add_2_out", TensorProto.FLOAT,
                                              conv_output_shape)

    # create constant tensor
    W0_tensor = helper.make_tensor("W0", TensorProto.FLOAT, conv_weight_shape,
                                    conv_weight_data.flatten())

    add_1_param = helper.make_tensor("add_1_param",
                                      TensorProto.FLOAT, add_param_shape,
                                      add_1_param_data.flatten())
    add_2_param = helper.make_tensor("add_2_param",
                                      TensorProto.FLOAT, add_param_shape,
                                      add_2_param_data.flatten())

    # make graph
    graph = helper.make_graph(
        nodes,
        "simple_conv_model",
        inputs=[model_input_1, model_input_2],  # input
        outputs=[model_output_1, model_output_2],  # output
        initializer=[W0_tensor, add_1_param, add_2_param],  # initializer
        value_info=[add_1_out, add_2_out],  # value_info
    )

    # make model
    onnx_model = helper.make_model(graph,
                                    opset_imports=[
                                        helper.make_opsetid("", 11),
                                        helper.make_opsetid("horizon.cop1", 1),
                                        helper.make_opsetid("horizon.cop2", 1)
                                    ],
                                    producer_name="onnx-test")

    # shape inference
    onnx_model = shape_inference.infer_shapes(onnx_model)

    # # model check
    checker.check_model(onnx_model)

    # save model
    onnx.save(onnx_model, "custom_op.onnx")

注意:

Onnx模型中PyOp属性的注意点:

  • domain属性一定要设置,不然的话会被默认成onnx标准domain从而报错。不同实现的自定义算子需要设置在不同的domain下。

  • module需要与注册时使用的注册文件同名。若注册文件在当前目录的子文件夹中,则需要修改module内容。例如: 若 sample_custom.py 在当前路径的custom_op 文件夹中,则该module应设置为 custom_op.sample_custom

  • 目前仅onnx模型支持多种类型的自定义算子,如您需要在其他框架中支持多种类型的自定义算子请联系地瓜技术支持人员。

2.与 Caffe 模型一致,需要在模型转换配置中,使用自定义算子需要在配置文件中加入一个新的自定义op参数组如下:


#...

custom_op:
  # 自定义op的校准方式
  custom_op_method: register

  # 自定义OP的实现文件
  op_register_files: sample_custom.py

对于 ONNX 模型,以上参数组中的两个参数都是必须配置的。 custom_op_method 固定使用 registerop_register_files 是自定义算子计算的实现文件,请使用相对路径。

完成这些配置后,模型转换的后续步骤与其他一般模型转换过程一致。

算子实现

在模型转换阶段,需要提供自定义算子的Python实现,工具会利用该实现函数完成模型校准必需的推理阶段。


  请注意,由于工具在PTQ转换过程中会以 ``working_dir`` 为工作目录,我们强烈建议算子实现中涉及工作目录的配置时,配置为绝对路径,
  如需要配置为相对路径,请以 ``working_dir`` 为工作目录进行相对路径的指定。

Python模板文件(sample_custom.py)如下:


from horizon_nn.custom.op_registration import op_implement_register, op_shape_infer_register

@op_implement_register("CustomIdentity")
class CustomIdentity(object):
    def __init__(self, kernel_size, threshold):
        self._kernel_size = kernel_size
        self._default_threshold = threshold

    def compute(self, X):
        return X

@op_shape_infer_register("CustomIdentity")
def infer_shape(inputs_shape):
    outputs_shape = inputs_shape
    return outputs_shape

custom_op示例中的配置文件(horizon_ops.py)如下:


from horizon_nn.custom.op_registration import op_implement_register

@op_implement_register("Cop1")
class Cop1(object):
    def __init__(self, ):
        pass

    def compute(self, x1, x2):
        out = x1 + x2 + 1
        return out


@op_implement_register("Cop2")
class Cop2(object):
    def __init__(self, ):
        pass

    def compute(self, x1, x2, x3):
        out = x1 + x2 + x3 + 1
        return out, out

该文件的名字(sample_custom.py)需要填入模型转换的yaml配置文件中 op_register_files ,否则工具无法正常import自定义算子定义, 并且修饰器 op_implement_register 注册的custom op类的名称 CustomIdentity 需要与Caffe自定义OP的属性 kind 或者Onnx自定义OP的属性 class_name 一致。

对于 Caffe 模型, init 函数中的参数(kernel_size, threshold)都是通过prototxt文件中的 params 传入的, 用于自定义op模块的初始化。 op_shape_infer_register 用于Caffe模型的算子shape注册。

对于 Onnx 模型,自定义op的shape解析有两种方式,可以通过在创建onnx模型时,将pyop输出的value_info添加到onnx模型中, 或者在对应的pyop中创建output_shape属性。 还需要注意自定义算子中的 module 必须与存放自定义算子实现的文件保持一致, 如果属性设置为 custom_op.horizon_ops , 则自定义算子实现的文件名称为 horizon_ops ,且要放在 custom_op文件夹中, 保持与onnx模型的层级关系。 由于同一个domain中的同名算子实现必须相同, 因此不同的自定义算子的 domain 属性需要不同。

上述操作完成后即可进行浮点转定点的操作,得到相应的bin文件。

含自定义算子的上板运行

在拿到bin文件后,还不能直接在开发板上运行。在运行之前需要先提供自定义算子的C++代码实现。 您可以使用下文提供的模板进行修改并添加到示例代码中进行使用。

如果您只是希望测试自定义算子的功能,也可以直接使用我们提供的模板文件,模板文件中将输入直接赋值为输出使用, 所以这个自定义算子并不会对结果造成任何影响。

自定义算子C++模板

Runtime模板文件如下:

  // custom_identity_add1.h
  #ifndef ADVANCED_SAMPLES_CUSTOM_IDENTITY_ADD1_H_
  #define ADVANCED_SAMPLES_CUSTOM_IDENTITY_ADD1_H_

  #include <string>
  #include <vector>

  #include "dnn/hb_dnn.h"
  #include "dnn/plugin/hb_dnn_layer.h"
  #include "dnn/plugin/hb_dnn_ndarray.h"

  namespace hobot {
  namespace dnn {

  Layer *Cop1_layer_creator();

  class Cop1 : public Layer {
  public:
    Cop1() = default;
    ~Cop1() override = default;

  public:
    int32_t Init(const Attribute &attributes) override;

    int32_t Forward(const std::vector<NDArray *> &bottomBlobs,
                    std::vector<NDArray *> &topBlobs,
                    const hbDNNInferCtrlParam *inferCtrlParam) override;

    std::string GetType() const override { return "Cop1"; }

    uint32_t GetInputCount() const override { return num_args_; }

  private:
    std::string custom_op_name_;
    int32_t num_args_;
  };

  }  // namespace dnn
  }  // namespace hobot

  #endif
 // custom_identity_add1.cpp
  #include "custom_identity_add1.h"

  namespace hobot {
  namespace dnn {

  Layer *Cop1_layer_creator() { return new Cop1; }

  int32_t Cop1::Init(const Attribute &attributes) {
    // unused attribute, just demonstrating
    attributes.GetAttributeValue(&custom_op_name_, "custom_op_name");
    // node's input count
    attributes.GetAttributeValue(&num_args_, "num_args");
    return 0;
  }

  int32_t Cop1::Forward(const std::vector<NDArray *> &bottomBlobs,
                        std::vector<NDArray *> &topBlobs,
                        const hbDNNInferCtrlParam *inferCtrlParam) {
    const NDArray *input0 = bottomBlobs[0];
    const NDArray *input1 = bottomBlobs[1];
    NDArray *out = topBlobs[0];

    const auto *input0_data = input0->Dptr<float>();
    const auto *input1_data = input1->Dptr<float>();

    auto *out_data = out->Dptr<float>();
    uint32_t size = out->Size();

    for (uint32_t i = 0U; i < size; i++) {
      out_data[i] = input0_data[i] + input1_data[i] + 1;
    }
    return 0;
  }
  }  // namespace dnn
  }  // namespace hobot
  // custom_identity_add2.h
  #ifndef ADVANCED_SAMPLES_CUSTOM_IDENTITY_ADD2_H_
  #define ADVANCED_SAMPLES_CUSTOM_IDENTITY_ADD2_H_

  #include <string>
  #include <vector>

  #include "dnn/hb_dnn.h"
  #include "dnn/plugin/hb_dnn_layer.h"
  #include "dnn/plugin/hb_dnn_ndarray.h"

  namespace hobot {
  namespace dnn {

  Layer *Cop2_layer_creator();

  class Cop2 : public Layer {
  public:
    Cop2() = default;
    ~Cop2() override = default;

  public:
    int32_t Init(const Attribute &attributes) override;

    int32_t Forward(const std::vector<NDArray *> &bottomBlobs,
                    std::vector<NDArray *> &topBlobs,
                    const hbDNNInferCtrlParam *inferCtrlParam) override;

    std::string GetType() const override { return "Cop2"; }

    uint32_t GetInputCount() const override { return num_args_; }

    uint32_t GetOutputCount() const override { return 2U; }

  private:
    std::string custom_op_name_;
    int32_t num_args_;
  };

  }  // namespace dnn
  }  // namespace hobot

  #endif
// custom_identity_add2.cpp
  #include "custom_identity_add2.h"

  namespace hobot {
  namespace dnn {

  Layer *Cop2_layer_creator() { return new Cop2; }

  int32_t Cop2::Init(const Attribute &attributes) {
    // unused attribute, just demonstrating
    attributes.GetAttributeValue(&custom_op_name_, "custom_op_name");
    // node's input count
    attributes.GetAttributeValue(&num_args_, "num_args");
    return 0;
  }

  int32_t Cop2::Forward(const std::vector<NDArray *> &bottomBlobs,
                        std::vector<NDArray *> &topBlobs,
                        const hbDNNInferCtrlParam *inferCtrlParam) {
    const NDArray *input0 = bottomBlobs[0];
    const NDArray *input1 = bottomBlobs[1];
    const NDArray *input2 = bottomBlobs[2];
    NDArray *out0 = topBlobs[0];
    NDArray *out1 = topBlobs[1];

    const auto *input0_data = input0->Dptr<float>();
    const auto *input1_data = input1->Dptr<float>();
    const auto *input2_data = input2->Dptr<float>();

    auto *out0_data = out0->Dptr<float>();
    auto *out1_data = out1->Dptr<float>();

    uint32_t size = out0->Size();

    for (uint32_t i = 0U; i < size; i++) {
      out0_data[i] = input0_data[i] + input1_data[i] + input2_data[i] + 1;
      out1_data[i] = out0_data[i];
    }
    return 0;
  }
  }  // namespace dnn
  }  // namespace hobot

备注: 该函数名称的前缀(即 Cop1Cop2) 需要与自定义OP的类型( Kind )一致, 其传入的参数为:

  • bottom_blobs :自定义OP节点输入数据。

  • top_blobs :自定义OP节点输出数据。

  • inferCtrlParam :自定义算子初始化阶段的输入参数。

  模板中的运算规则均为输出等于所有输入数据累加后再加上数值1,因此后续若需要定义其他行为,则需相应的更改运算规则即可。
自定义算子注册

当您完成C++版本模板的修改后,仅需要在示例的CMakeLists.txt中添加对模板文件的包含, 并在应用程序中增加对算子的注册即可,注册请参考以下代码:

  #include "custom_identity_add1.h"
  #include "custom_identity_add2.h"

  hbDNNRegisterLayerCreator("Cop1", hobot::dnn::Cop1_layer_creator);
  hbDNNRegisterLayerCreator("Cop2", hobot::dnn::Cop2_layer_creator);
  ....

当您完成对模板文件的依赖及算子注册后,即可对含有自定义算子的模型进行执行等操作。

  在使用前,请您确认模型的自定义算子名称与注册的算子名称是相同的。

参考文档,请参阅Runtime示例中的 advanced_samples