概述

本例主要是利用resnet18模型,实现一个完整的分类应用。 本例模型和代码在 http://219.142.246.77:65000/sharing/NVUS3acJ7

下载到本地 resnet18_classify.tar.gz,并解压:

tar zxvf resnet18_classify.tar.gz

resnet18_classify目录中包含以下文件:

  • model/resnet18.onnx resnet18原始模型

  • images/ 测试图片集

  • ILSVRC2012/ 量化用数据集

  • scripts/ 本示例中脚本文件

  • src/ 应用源码目录

  • CMakeLists.txt 构建脚本

编译与量化模型

初始化tpu-nntc环境

在 docker 环境使用工具链软件,最新版本的 docker 可以参考 官方教程 进行安装。安装完成后,执行下面的脚本将当前用户加入 docker 组,获得 docker 执行权限。

sudo usermod -aG docker $USER
newgrp docker

从官网上下载tpu-nntc的压缩包,命名如 tpu-nntc_vx.y.z-<hash>-<date>.tar.gz

mkdir tpu-nntc
# 将压缩包解压到tpu-nntc
tar zxvf tpu-nntc_vx.y.z-<hash>-<date>.tar.gz --strip-components=1 -C tpu-nntc

tpu-nntc使用的docker是sophgo/tpuc_dev:v2.1, docker镜像和tpu-nntc有绑定关系,少数情况下有可能更新了tpu-nntc,需要新的镜像

cd tpu-nntc

# 进入docker,如果当前系统没有对应镜像,会自动从docker hub上下载
# 将tpu-nntc的上一级目录映射到docker内的/workspace目录
# 这里用了8001到8001端口映射,之后在使用ufw可视化工具会用到
# 如果端口已经占用,请更换其他未占用端口,后面根据需要更换进行调整
docker run -v $PWD/..:/workspace -p 8001:8001 -it sophgo/tpuc_dev:v2.1

# 此时已经进入docker,并在/workspace目录下
# 下面初始化软件环境
cd /workspace/tpu-nntc
source scripts/envsetup.sh

其他docker初始化方式:

如果是离线环境,可以先提前下载,然后导入到目标机上,命令如下:

# 在可以联网的机器上, 保存镜像
docker pull sophgo/tpuc_dev:v2.1
docker save sophgo/tpuc_dev:v2.1 -o sophon_tpuc_dev.docker

# 将sophon_tpuc_dev_docker.tar复制到目标机器上后
docker load -i sophon_tpuc_dev.docker

此外,tpu-nntc在scripts目录提供了docker_setup.sh脚本, 供参考和方便基本使用,实际使用中可根据需求自行管理。使用方法如下:

# docker_setup.sh 后面参数来指定哪个目录作为工作目录,映射到docker里的/workspace
# 这里用 tpu-nntc 的上一级目录
scripts/docker_setup.sh ..

# 会输出以下信息,如果8001端口占用,会自动选择其他未占用端口
# ...
# OPEN 'http://localhost:8001' IN WEB BROWSER WHEN 'python3 -m ufw.tools.app' RUNNING IN DOCKER

注意:此时 resnet18_classify 目录与 tpu-nntc 目录是并列的。将 resnet18_classify 放到其他目录也是可以的,但要注意调整后文相关的命令。

环境初始化完成后,进入resnet18_classify目录:

cd /workspace/resnet18_classify

# 为了方便清理,建议创建一个空的工作目录
mkdir -p workdir && cd workdir

编译FP32 BMODEL

由于转换的是onnx模型,所以需要bmneto前端,转换命令如下:

# 此时在resnet18_classify/workdir目录中
python3 -m bmneto --model ../model/resnet18.onnx \
                --input_names "input" \
                --shapes "[[1,3,224,224]]" \
                --target BM1684X \
                --outdir bmodel/fp32

--target 用于指定芯片型号,目前支持 BM1684BM1684X

查看下fp32 bmodel的信息:

tpu_model --info bmodel/fp32/compilation.bmodel

输出模型信息如下:

bmodel version: B.2.2
chip: BM1684X
create time: Thu Jul 21 06:14:45 2022

==========================================
net 0: [network]  static
------------
stage 0:
input: input, [1, 3, 224, 224], float32, scale: 1
output: output, [1, 1000], float32, scale: 1

device mem size: 49782688 (coeff: 46968736, instruct: 0, runtime: 2813952)
host mem size: 0 (coeff: 0, runtime: 0)

自动量化生成INT8 BMODEL

自动量化工具会自动处理图片数据集将其转为lmdb数据集,并使用不同量化策略量化多次,自动生成bmodel。

本例中,量化采用了ILSVRC2012的部分图片。

mkdir -p auto_cali_out

# 注意cali_image_preprocess参数,要和模型原始应用的预处理一致
# 否则会出现模型量化精度高,但在应用上精度低的情况
python3 -m ufw.cali.cali_model \
  --net_name "resnet18" \
  --model ../model/resnet18.onnx \
  --cali_image_path ../ILSVRC2012/ \
  --cali_image_preprocess '
      resize_h=224,resize_w=224;
      mean_value=123.675:116.28:103.53;
      scale=0.0171:0.0175:0.0174;
      bgr2rgb=True' \
  --input_names 'input' \
  --input_shapes '[1,3,224,224]' \
  --target BM1684X \
  --outdir auto_cali_out

cp -r auto_cali_out/resnet18_batch1 bmodel/auto-int8

--target 用于指定芯片型号,目前支持 BM1684BM1684X

查看下 bmodel 的信息:

tpu_model --info bmodel/auto-int8/compilation.bmodel

输出如下, 可以看到输入和输出类型是 int8:

bmodel version: B.2.2
chip: BM1684X
create time: Thu Jul 21 06:37:36 2022

==========================================
net 0: [resnet18]  static
------------
stage 0:
input: input, [1, 3, 224, 224], int8, scale: 48.21
output: output, [1, 1000], int8, scale: 0.214865

device mem size: 14270368 (coeff: 12009376, instruct: 0, runtime: 2260992)
host mem size: 0 (coeff: 0, runtime: 0)
# 查看量化精度情况(可选)
python3 -m ufw.tools.app --port 8001

这时在本机的 8001 端口(启动docker时显示的端口)可以打开网页,如果是本机直接打开 http://localhost:8001 ,如果是远程服务器请将 localhost 替换成服务器 IP。按图中(1)(2)步操作,等待程序运行完成,可以看到如下结果:

../_images/autocali-result.png

图中(a)(b)(c)(d)说明了各部分的作用。从图中可以看到,最终的余弦距离已经达到0.998003,能够保证分类结果的单调性,从而保证分类的准确性。

注意:这里可视化工具是在量化数据集中随机找一张图进行一次推理,并把数据显示出来。每次显示时会不一样,但比较接近。

如果最终精度或在业务上验证精度不满足要求,可以考虑增加迭代次数或其他参数。

回到开发环境,Ctrl+C结束精度显示服务。

分步量化生成INT8 BMODEL(可选)

分步量化步骤相对较多,但很多的细节可控,如调整模型结构,按layer来指定量化参数等,需要一定的经验和技巧,通常是应对复杂结构的网络。这里只介绍基本的命令和步骤,具体可以参考我们的开发手册。

生成LMDB的输入数据

图片数据集生成lmdb脚本可以参考../scripts/convert_imageset.py,主要功能是调用opencv进行预处理,然后保存成lmdb。在这里直接使用该脚本转换:

python3 ../scripts/convert_imageset.py \
       --imageset_rootfolder=../ILSVRC2012 \
       --imageset_lmdbfolder=./image_lmdb \
       --resize_height=224 \
       --resize_width=224 \
       --shuffle=True \
       --mean=[123.675,116.28,103.53] \
       --scale=[0.0171,0.0175,0.0174] \
       --bgr2rgb=True \
       --gray=False

运行完成后会在当前目录下生成image_lmdb/data.mdb文件。该文件由于存储的是解码后的数据,相对原来图片总大小,会大很多。

导出FP32 UMODEL

# 此时在resnet18_classify/workdir目录中
# 注意和编译FP32 bmodel相比,增加了--mode GenUmodel参数,并且--target是无用的
# 此步会生成umodel/network_bmneto.fp32umodel
python3 -m bmneto --model ../model/resnet18.onnx \
                --mode GenUmodel \
                --input_names "input" \
                --shapes "[[1,3,224,224]]" \
                --outdir umodel

# 此步将umodel/network_bmneto.fp32umodel中的模型结构生成出来,并设定数据集为之前生成的image_lmdb
# 会生成umodel/network_bmneto_test_fp32.prototxt
python3 -m ufw.tools.to_umodel \
    -u umodel/network_bmneto.fp32umodel \
    -D image_lmdb

这里需要说明一下,完整的FP32 UMODEL包含.prototxt和.fp32umodel两个文件, 其中:

  • prototxt文件用于描述网络结构,并且会将数据集路径、前处理一些过程保存到里面, 也可以手工精调每层的量化参数

  • fp32umodel主要用于保存网络权重。

量化FP32 UMODEL为INT8 UMODEL

# 这里迭代了50次
# save_test_proto 用于生成可视化数据,如果没加,会无法可视化结果,但不会影响量化过程
calibration_use_pb quantize \
      --model umodel/network_bmneto_test_fp32.prototxt \
      --weights umodel/network_bmneto.fp32umodel \
      --target BM1684X \
      --iterations=50 \
      --save_test_proto=true

--target 用于指定芯片型号,目前支持 BM1684BM1684X

量化完成后,会在umodel目录生成network_bmneto_deploy_int8_unique_top.prototxt和network_bmneto.int8umodel两个文件,作为INT8 UMODEL, 最终编译会用到。

之后,也可以进行可视化显示。

# 查看量化精度情况(可选)
python3 -m ufw.tools.app --port 8001

同时在浏览器中打开http://localhost:8001(docker初始化时显示的端口),按图中(1)(2)步操作,等待程序运行完成,可以看到如下结果:

../_images/cali-result.png

可以看到余弦距离达到0.986552,满足分类要求。

回到开发环境,Ctrl+C结束精度显示服务。

编译出INT8 BMODEL

将umodel转换为bmodel要用到bmnetu工具:

bmnetu --model umodel/network_bmneto_deploy_int8_unique_top.prototxt \
       --weight umodel/network_bmneto.int8umodel \
       --target BM1684X \
       --outdir "bmodel/step-int8"

--target 用于指定芯片型号,目前支持 BM1684BM1684X

查看下bmodel的信息:

tpu_model --info bmodel/step-int8/compilation.bmodel

输出如下, 可以看到输入和输出类型是int8:

bmodel version: B.2.2
chip: BM1684X
create time: Thu Jul 21 10:59:22 2022

==========================================
net 0: [network_bmneto]  static
------------
stage 0:
input: input, [1, 3, 224, 224], int8, scale: 52.3023
output: output, [1, 1000], int8, scale: 0.197092

device mem size: 12771232 (coeff: 12009376, instruct: 0, runtime: 761856)
host mem size: 0 (coeff: 0, runtime: 0)

清理环境

完成所有bmodel生成工作后,可以退出tpu-nntc环境。

exit

程序开发部署

安装libsophon环境

请参考 libsophon 使用手册安装 libsophon。 此外,由于本例的前处理用到了opencv的库,建议安装libopencv-dev。

sudo apt install libopencv-dev

检查BMODEL正确性和性能

安装好libsophon后,可以使用bmrt_test来测试编译出的bmodel的正确性及性能。

可以根据bmrt_test输出的性能结果,来估算模型最大的fps,来选择合适的模型

# 下面测试上面编译出的bmodel
# --context_dir参数后面接文件夹,里面必须有compilation.bmodel、input_ref_data.dat、output_ref.data.dat, 后面两个文件是编译时产生,用于运行时做数据比对
cd resnet18_classify/workdir
bmrt_test --context_dir bmodel/fp32
bmrt_test --context_dir bmodel/auto-int8
bmrt_test --context_dir bmodel/step-int8

以最后一个命令输出为例:

01 [BMRT][load_bmodel:1019] INFO:Loading bmodel from [bmodel/step-int8//compilation.bmodel]. Thanks for your patience...
02 [BMRT][load_bmodel:983] INFO:pre net num: 0, load net num: 1
03 [BMRT][show_net_info:1358] INFO: ########################
04 [BMRT][show_net_info:1359] INFO: NetName: network_bmneto, Index=0
05 [BMRT][show_net_info:1361] INFO: ---- stage 0 ----
06 [BMRT][show_net_info:1364] INFO:   Input 0) 'input' shape=[ 1 3 224 224 ] dtype=INT8 scale=52.3023
07 [BMRT][show_net_info:1373] INFO:   Output 0) 'output' shape=[ 1 1000 ] dtype=INT8 scale=0.197092
08 [BMRT][show_net_info:1381] INFO: ########################
09 [BMRT][bmrt_test:769] INFO:==> running network #0, name: network_bmneto, loop: 0
10 [BMRT][bmrt_test:834] INFO:reading input #0, bytesize=150528
11 [BMRT][bmrt_test:987] INFO:reading output #0, bytesize=1000
12 [BMRT][bmrt_test:1018] INFO:net[network_bmneto] stage[0], launch total time is 420 us (npu 327 us, cpu 93 us)
13 [BMRT][bmrt_test:1022] INFO:+++ The network[network_bmneto] stage[0] output_data +++
14 [BMRT][bmrt_test:1038] INFO:==>comparing #0 output ...
15 [BMRT][bmrt_test:1043] INFO:+++ The network[network_bmneto] stage[0] cmp success +++
16 [BMRT][bmrt_test:1063] INFO:load input time(s): 0.000099
17 [BMRT][bmrt_test:1064] INFO:calculate  time(s): 0.000422
18 [BMRT][bmrt_test:1065] INFO:get output time(s): 0.000022
19 [BMRT][bmrt_test:1066] INFO:compare    time(s): 0.000022

从上面输出可以看到以下信息:

  1. 02-08行是bmodel的网络输入输出信息

  2. 12行是在TPU上运行的时间,其中TPU用时327us,CPU用时93us。这里CPU用时主要是指在HOST端调用等待时间

  3. 15行表示数据运行比对成功

  4. 16行是加载数据到NPU的DDR的时间

  5. 17行相当于12行的总时间

  6. 18行是输出数据取回时间

应用开发

初始化设备及加载bmodel

bm_handle_t handle;
// open device
status = bm_dev_request(&handle, 0);
if (BM_SUCCESS != status) {
  return false;
}
// create bmruntime handle and load_bmodel
void *p_bmrt = bmrt_create(handle);
string bmodel_path = "compilation.bmodel";
if(argc > 2) bmodel_path = argv[2];
bmrt_load_bmodel(p_bmrt, bmodel_path.c_str());

前处理

前处理主要是准备数据,可以采用OpenCV等其他数据处理的库,只要最终把数据放到tensor中即可。

注意量化模型要在fp32模型输入的基础上乘以input_scale,然后再转换为int8,才能作为量化模型的输入。

std::string image_name = argv[1];
cv::Mat image = cv::imread(image_name.c_str());
if(image.empty()){
  std::cout<<"Failed to read image: "<<image_name<<std::endl;
  return -1;
}

auto cv_type = input_dtype == BM_FLOAT32? CV_32FC3: CV_8UC3;
cv::Mat resized;
cv::resize(image, resized, cv::Size(ws, hs), 0, 0, cv::INTER_NEAREST);
resized.convertTo(resized, cv_type,
        0.0078125 * input_scale,
        -127.5 * 0.0078125 * input_scale);

// Change input memory storage format form Opencv to BMTPU
std::vector<float> input_data(ws*hs*c);
std::vector<cv::Mat> input_channels;
if(cv_type == CV_32FC3) cv_type = CV_32FC1;
if(cv_type == CV_8UC3) cv_type = CV_8UC1;
// map input_data into input_channels
WrapInputLayer(input_channels, input_data.data(), c, hs, ws, cv_type);
cv::split(resized, input_channels);

bm_tensor_t input_tensor;
bmrt_tensor(&input_tensor, p_bmrt, input_dtype, input_shape);
bm_memcpy_s2d(handle, input_tensor.device_mem, input_data.data());

bm_tensor_t output_tensor;

模型推理

模型推理函数是异步的,调用完成后,需要再调用bm_thread_sync进行同步等待。

double start_time = what_time_is_it_now();
bool ret = bmrt_launch_tensor(p_bmrt, net_name.c_str(), &input_tensor, input_num,
                              &output_tensor, output_num);
if (!ret) {
  printf("launch failed!");
  exit(-1);
}
// sync, wait for finishing inference or get an undefined output
status = bm_thread_sync(handle);
if (BM_SUCCESS != status) {
  printf("thread sync failed!");
  exit(-1);
}

后处理

对于分类来说,后处理主要是取top值来得到正确类别。

注意量化模型的输出处理,取出的数据是量化的输出,需要乘上对应的output_scale值才是对应于fp32模型的输出。

size_t size = bmrt_tensor_bytesize(&output_tensor);
std::vector<char> raw_output(size);

// Get output data from device
bm_memcpy_d2s_partial(handle, raw_output.data(), output_tensor.device_mem, size);

// Free output and input device memory
bm_free_device(handle, output_tensor.device_mem);
bm_free_device(handle, input_tensor.device_mem);


// real post-process the output data
// here is to get the top one
auto output_count = bmrt_shape_count(&output_tensor.shape);
auto output_dtype = network_info->output_dtypes[0];
float output_scale = network_info->output_scales[0];

int max_index = 0;
float max_score = -1;
for(int i=0; i<output_count; i++){
  float score = 0;
  if(output_dtype == BM_FLOAT32) {
    score = ((float*)raw_output.data())[i];
  } else if(output_dtype == BM_INT8) {
    // multiply the output_scale only for quantized model
    // for classification, the scale may be useless
    score = raw_output[i] * output_scale;
  } else {
    assert(0);
  }
  if(score>max_score) {
    max_score = score;
    max_index = i;
  }
}

cout <<"IMAGE "<< image_name
     <<" TOP SCORE IS " << max_score
     << " AT " << max_index << endl;

double start_end = what_time_is_it_now();
printf("TOTAL TIME %f s\n", start_end - start_time);

释放设备

// Destroy bmruntime context
bmrt_destroy(p_bmrt);

程序编译脚本

如下是应用的CMake构建脚本具体内容,推荐用find_package来提供环境信息:

cmake_minimum_required(VERSION 3.10)
project(resnet18-classify)

# opencv for preprocess
find_package(OpenCV REQUIRED)
include_directories(${OpenCV_INCLUDE_DIRS})

# use libbmrt libbmlib
find_package(libsophon REQUIRED)
include_directories(${LIBSOPHON_INCLUDE_DIRS})
message(STATUS ${LIBSOPHON_LIBS})

aux_source_directory(src SRC_FILES)
add_executable(classify ${SRC_FILES})
target_link_libraries(classify ${OpenCV_LIBS} ${the_libbmlib.so} ${the_libbmrt.so})

install(TARGETS classify DESTINATION bin)
install(FILES
  ${CMAKE_CURRENT_SOURCE_DIR}/workdir/bmodel/auto-int8/compilation.bmodel
  DESTINATION bmodel)

完成构建脚本编写后,开始编译和运行了:

# 此时在resnet18-classify目录下
mkdir build && cd build
cmake ../ -DCMAKE_INSTALL_PREFIX=`pwd`/install && make && make install

此时会在当前生成classify程序,运行命令如下:

./classify ../images/cat.jpg ../workdir/bmodel/fp32/compilation.bmodel
./classify ../images/cat.jpg ../workdir/bmodel/auto-int8/compilation.bmodel
./classify ../images/cat.jpg ../workdir/bmodel/step-int8/compilation.bmodel

打包部署

当尝试不同的bmodel满足业务需求后,可将编译出来的classify程序和bmodel打包,发布到业务系统中。

如之后原始模型有更改,仅需要重新编译bmodel,替换原bmodel即可。

另外, bmodel是与平台无关的,不同平台的相同应用都可以使用同一bmodel。