Xilinx Vitis AI量化部署Yolov5至DPU (PYNQ)

Dorapocket 2024-07-16 10:01:07 阅读 96

本文及后续更新都会放在个人主页~ 欢迎来看看

https://lgyserver.top/index.php/2023/05/08/xilinx-vitis-ai%e9%87%8f%e5%8c%96%e9%83%a8%e7%bd%b2yolov5%e8%87%b3dpu-pynq/

概要

本文章记述了从YOLOv5源代码使用Xilinx Vitis AI进行量化并部署到DPU上的全流程。在开Pynq环境下运行测试通过。

环境

主机: Ubuntu 22.04 + Vivado 2022.2 + Vitis AI 2.5.0 (使用Docker安装)+ CUDA 11.3

开发板:Xilinx Kria KV260 + Pynq 3.0 + DPU Pynq 2.5.1

版本是很重要的!

本代码使用Pynq作为程序界面,因此DPU-PYNQ的版本支持性决定了大多数的版本需求。Ubuntu和Vivado的版本一般来讲没有关系,但要注意Vivado是否支持Kria的芯片型号。截止本文发文时,DPU-PYNQ的包版本为2.5.1,在官方描述中只支持到Vitis

AI 2.5.0 和Pynq 3.0。笔者经过测试发现在最新的Vitis AI

3.0.0量化编译出来的xmodel没有办法被DPU-PYNQ调用,具体体现为python kernel直接挂掉且没有报错信息,Vitis AI 2.5.0就没有这个问题。

量化模型

在如下Vitis AI的Github官方仓库中有很多模型的运行表现,并有很多已经量化好的模型供测试,位于model zoo文件夹下,并且官方对常见的模型进行了benchmark,使用方法详见文档。

再开始之前,我们要先clone一下Vitis AI的仓库,下面的操作都是在Vitis AI仓库的根目录执行的

<code>git clone Xilinx/Vitis-AI

安装Vitis AI环境

先给出官方文档,比较推荐的方式是采用Docker安装。

如果你只想安装一个CPU版本的Vitis AI用于编译用途,事情就简单了,Xilinx为CPU平台已经构建了Docker Image, 在Ubuntu安装好Docker后直接运行以下指令就好了(安装Docker的方式请参阅网上其他教程):

docker pull xilinx/vitis-ai-<Framework>-<Arch>:<Version>

其中,预构建支持的Framework和Arch如下:

Desired Docker Framework Arch
PyTorch cpu-only pytorch cpu
TensorFlow 2 cpu-only tensorflow2 cpu
TensorFlow 1.15 cpu-only tensorflow cpu
PyTorch ROCm pytorch rocm
TensorFlow 2 ROCm tensorflow2 rocm
PyTorch with AI Optimizer ROCm opt-pytorch rocm
TF2 with AI Optimizer ROCm opt-tensorflow2 rocm

注意这里的Version一定要考虑其他依赖支不支持这个版本的Vitis AI,别上来直接latest。本文中拉取Pytorch的CPU版镜像(2.5.0)用于编译,运行

<code>docker pull xilinx/vitis-ai-pytorch-cpu:2.5.0

(有小伙伴指出好像这个docker没有了,但是xilinx/vitis-ai-cpu:2.5.0还有)

但是,如果你想使用起来电脑上NVIDIA GPU的CUDA核,就要采取一些复杂的操作了,需要用Xilinx的Dockerfile构建自己的镜像。可以参见这里的官方文档。(可能需要修改Dockerfile适应中国大陆网络)

进入Vitis AI根目录,修改一下docker_run.sh

找到docker_run_params , 删掉不存在的挂载参数

# -v /opt/xilinx/dsa:/opt/xilinx/dsa \

# -v /opt/xilinx/overlaybins:/opt/xilinx/overlaybins \

执行下面指令即可进入Vitis AI环境:

./docker_run.sh xilinx/vitis-ai-pytorch-cpu:latest

在这里插入图片描述

如果你没有修改过其他参数,那么<code>Docker内的/workspace目录就是主机的Vitis-AI仓库根目录。

量化编译yolov5

该部分可参考UG1414文档。大体流程如下:

在这里插入图片描述

在这里插入图片描述

首先克隆原始的Yolov5仓库,这里用的是ultralytics/yolov5,虽然ultralytics/ultralytics也有yolov5,但因为增加了很多训练trick,导致源代码比较难修改,故采用前者。

克隆完后,安装所需要的依赖

<code>pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt

修改模型

关于yolov5模型结构的介绍有很多,在此不一一介绍,一个比较显著的特点是,yolov5调整了激活函数,由ReLU改为了SiLU,但SiLU函数是不被DPU支持的,因此在训练之前,需要把激活函数换成ReLU或者LeakyReLU,找到yolov5仓库下的models文件夹,修改目标网络的yaml文件,加上以下文字:

act: nn.ReLU()

训练&finetune

可以参考yolov5仓库下的说明文档,在其他机器上训练好或者finetune好导出成pytorch的pt文件即可。

量化

模型的量化可以参考Vitis-AI给出的demo, 可以看到量化分为calib和test两步,calib负责生成模型校准信息,test负责量化后xmodel的导出。

量化之前,需要对yolov5的代码再做一点修改,官方文档中指出量化模型应该仅包含forward方法,但在yolov5的源文件中(models/yolo.py),可以看到以下代码

def forward(self, x):

z = [] # inference output

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()

if not self.training: # inference

if self.dynamic or self.grid[i].shape[2:4] != x[i].shape[2:4]:

self.grid[i], self.anchor_grid[i] = self._make_grid(nx, ny, i)

if isinstance(self, Segment): # (boxes + masks)

xy, wh, conf, mask = x[i].split((2, 2, self.nc + 1, self.no - self.nc - 5), 4)

xy = (xy.sigmoid() * 2 + self.grid[i]) * self.stride[i] # xy

wh = (wh.sigmoid() * 2) ** 2 * self.anchor_grid[i] # wh

y = torch.cat((xy, wh, conf.sigmoid(), mask), 4)

else: # Detect (boxes only)

xy, wh, conf = x[i].sigmoid().split((2, 2, self.nc + 1), 4)

xy = (xy * 2 + self.grid[i]) * self.stride[i] # xy

wh = (wh * 2) ** 2 * self.anchor_grid[i] # wh

y = torch.cat((xy, wh, conf), 4)

z.append(y.view(bs, self.na * nx * ny, self.no))

return x if self.training else (torch.cat(z, 1),) if self.export else (torch.cat(z, 1), x)

这段代码在inference的时候,将预测的xy相对位置加上了grid坐标乘以stride来映射回原图,返回一个检测头的输出。但在这些步骤应该属于后处理的一部分,在量化的时候需要删掉,仅仅保留最纯正的网络输出x就可以了。

def forward(self, x):

z = [] # inference output

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)

if self.grid[i].shape[2:4] != x[i].shape[2:4]:

self.grid[i], self.anchor_grid[i] = self._make_grid(nx, ny, i)

return x

修改完后就可以开始写量化程序了,调用vai_q_pytorch的包可以很轻松的完成这一点,

import os

import sys

import argparse

import random

import torch

import torchvision

import torch.nn as nn

import torch.nn.functional as F

from pytorch_nndct.apis import torch_quantizer, dump_xmodel

from common import *

from models.common import DetectMultiBackend

from models.yolo import Model

DIVIDER = '-----------------------------------------'

def quantize(build_dir,quant_mode,batchsize):

dset_dir = build_dir + '/dataset'

float_model = build_dir + '/float_model'

quant_model = build_dir + '/quant_model'

# use GPU if available

if (torch.cuda.device_count() > 0):

print('You have',torch.cuda.device_count(),'CUDA devices available')

for i in range(torch.cuda.device_count()):

print(' Device',str(i),': ',torch.cuda.get_device_name(i))

print('Selecting device 0..')

device = torch.device('cuda:0')

else:

print('No CUDA devices available..selecting CPU')

device = torch.device('cpu')

# load trained model

model = DetectMultiBackend("./v5n_ReLU_best.pt", device=device)

# force to merge BN with CONV for better quantization accuracy

optimize = 1

# override batchsize if in test mode

if (quant_mode=='test'):

batchsize = 1

rand_in = torch.randn([batchsize, 3, 960, 960])

quantizer = torch_quantizer(quant_mode, model, (rand_in), output_dir=quant_model)

quantized_model = quantizer.quant_model

# create a Data Loader

test_dataset = CustomDataset('../../train/JPEGImages',transform=test_transform)

test_loader = torch.utils.data.DataLoader(test_dataset,

batch_size=batchsize,

shuffle=False)

t_loader = torch.utils.data.DataLoader(test_dataset,

batch_size=1 if quant_mode == 'test' else 10,

shuffle=False)

# evaluate

test(quantized_model, device, t_loader)

# export config

if quant_mode == 'calib':

quantizer.export_quant_config()

if quant_mode == 'test':

quantizer.export_xmodel(deploy_check=False, output_dir=quant_model)

return

def run_main():

# construct the argument parser and parse the arguments

ap = argparse.ArgumentParser()

ap.add_argument('-d', '--build_dir', type=str, default='build', help='Path to build folder. Default is build')code>

ap.add_argument('-q', '--quant_mode', type=str, default='calib', choices=['calib','test'], help='Quantization mode (calib or test). Default is calib')code>

ap.add_argument('-b', '--batchsize', type=int, default=50, help='Testing batchsize - must be an integer. Default is 100')code>

args = ap.parse_args()

print('\n'+DIVIDER)

print('PyTorch version : ',torch.__version__)

print(sys.version)

print(DIVIDER)

print(' Command line options:')

print ('--build_dir : ',args.build_dir)

print ('--quant_mode : ',args.quant_mode)

print ('--batchsize : ',args.batchsize)

print(DIVIDER)

quantize(args.build_dir,args.quant_mode,args.batchsize)

return

if __name__ == '__main__':

run_main()

量化的代码里面,调用了torch_quantizer来进行量化,量化后的模型一定要用数据集运行一遍(evaluate),可以是没有标签的纯图片,因为这些图片仅用于校准量化参数,不进行反向传播。

执行python文件来生成量化配置

python quantize.py -q calib

https://lgyserver.top/wp-content/uploads/2023/05/image-4-1024x906.png

关注在此期间产生的Warning,比如未能识别的OP,这都是导致后面需要DPU分子图执行的原因。此时build/quant_model已经有生成的py了,需要接着运行test来生成xmodel

<code>python quantize.py -q test -b 1

https://lgyserver.top/wp-content/uploads/2023/05/image-5-1024x66.png

有了这个xmodel,我们需要用xilinx提供的compiler去把这个xmodel编译成DPU支持的,基于XIR的xmodel,运行如下指令:

<code>vai_c_xir -x ./build/quant_model/DetectMultiBackend_int.xmodel -a /opt/vitis_ai/compiler/arch/DPUCZDX8G/KV260/arch.json -o ./ -n my_model

https://lgyserver.top/wp-content/uploads/2023/05/image-6-1024x261.png

不知道是不是因为我安装的问题,我编译的GPU版本Vitis AI Docker找不到vai_c_xir这条指令,因此我使用GPU的Vitis AI量化生成xmodel以后,用CPU的预构建Docker来生成的最终xmodel

注意观察最终出来的DPU subgraph number是不是1,不是1的话请检查你的模型是不是有DPU不支持的OP,在遇到不支持的OP的时候,DPU会分为多个子图执行,由PS处理完后再发送给DPU,拖慢效率。生成的xmodel可以用netron查看网络输入输出结构。

仔细查看你的模型结构!

一定要用netron查看网络输入输出结构,这点非常重要,因为输出以后的xmodel是一个量化模型,和原模型直接python跑不一样,在上板子部署的过程中需要将输入图片量化,并将量化输出转为浮点后再进行NMS等后处理流程。

我采用了自己训练的12分类的yolov5n模型,观察upload节点的输入如下图:

https://lgyserver.top/wp-content/uploads/2023/05/image-7.png

也就是说,输入图片是一个xint8的定点数,小数点在第6位,size是1960960*3。

有了生成的xmodel,模型部分的任务就结束了,接下来要进行部署

部署

部署前准备

首先我们需要一个DPU Design 的 Hardware,可以用Vivado手动Block Design搭一个,不过这会牵扯到很多麻烦的地址设置,我会另写文章单独讲,在这里我们简单用一下Xilinx搭建的标准DPU Hardware,在DPU-PYNQ仓库的boards文件夹下就有。根据README构建Design,需要安装xrt和Vitis。官方的脚本写的比较死板,只认2022.1的版本,可以编辑<code>check_env.sh绕过检查

cd DPU-PYNQ/boards

source <vitis-install-path>/Vitis/2022.2/settings64.sh

source <xrt-install-path>/xilinx/xrt/setup.sh

make BOARD=kv260_som

我在综合的时候出现了timing不符合的错误,导致综合失败,我的解决方法是更换综合Strategy,修改/prj_config,[vivado]部分的最下面增加prop=run.impl_1.strategy=Performance_Explore,可以综合成功,不知道原来哪里出了问题。

脚本运行完后,会生成三个文件

dpu.bitdpu.hwhdpu.xclbin

再加上之前生成的

my_model.xmodel

需要的文件都准备完成,接下来可以在pynq上进行部署。

安装DPU-PYNQ

配置好PYNQ环境后,需要单独安装DPU-PYNQ,这是一个包,提供了控制DPU的Python接口,位于下面的仓库中

可以直接通过pip安装

pip install pynq-dpu --no-build-isolation

cd $PYNQ_JUPYTER_NOTEBOOKS

pynq get-notebooks pynq-dpu -p .

运行后就可以使用pynq_dpu包了,并且会出现使用pynq_dpu的示例文件。

部署Yolov5

终于来到了激动人心的部署环节!为了让模型能运行起来,在ps需要做的是

部署DPU overlay,加载模型前处理 + 量化输入运行DPU推理反量化输出 + 后处理我们将一个个解决这些问题。

首先引入pynq-dpu包,这个包是针对pynq的封装,DpuOverlay是继承了Pynq Overlay的

from pynq_dpu import DpuOverlay

overlay = DpuOverlay("yolo5.bit")

overlay.load_model("yolo5.xmodel")

几个需要注意的点:

DpuOverlay的参数需要是bit文件,并且在同一路径下应该有同名的.xclbin和.hwh文件编译生成的xmodel需要是上文vai_c_xir生成的xmodel,用test阶段生成的xmodel没用,且对于pynq-dpu来说,目前仅支持Vitis AI 2.5.0生成的xmodel。高版本编译生成的xmodel会导致notebook内核挂起。

然后定义输入输出的缓冲区:

dpu = overlay.runner

inputTensors = dpu.get_input_tensors()

outputTensors = dpu.get_output_tensors()

shapeIn = tuple(inputTensors[0].dims)

shapeOut0 = (tuple(outputTensors[0].dims))

shapeOut1 = (tuple(outputTensors[1].dims))

shapeOut2 = (tuple(outputTensors[2].dims))

outputSize0 = int(outputTensors[0].get_data_size() / shapeIn[0])

outputSize1 = int(outputTensors[1].get_data_size() / shapeIn[0])

outputSize2 = int(outputTensors[2].get_data_size() / shapeIn[0])

input_data = [np.empty(shapeIn, dtype=np.int8, order="C")]code>

output_data = [np.empty(shapeOut0, dtype=np.int8, order="C"), code>

np.empty(shapeOut1, dtype=np.int8, order="C"),code>

np.empty(shapeOut2, dtype=np.int8, order="C")]code>

image = input_data[0]

在上面的代码中,outputTensors的大小应该和Netron中的一致。在本文中是1*120*120*361*60*60*361*30*30*36,对应yolov5-nano的三个检测头。

在netron中,DPU计算后输出的outputTensors表示为download节点的数据类型,不是最后节点中的fix2float后的类型,这一步需要在cpu上自己做,如下图。

https://lgyserver.top/wp-content/uploads/2023/06/image.png

然后可以根据原来的全精度模型的推理代码写出DPU版本的推理代码,首先要对输入图像进行前处理。yolov5的输入是归一化后的像素值,且大小恒定,因此我们用原始代码中的letterbox进行大小裁剪后在对其进行归一化,然后对其进行int8量化。

<code>im0 = cv2.imread('a.jpg')

im = letterbox(im0, new_shape=(960,960), stride=32)[0] # padded resize

im = im.transpose((2, 0, 1)) # HWC to CHW

im = np.ascontiguousarray(im) # contiguous

im = np.transpose(im,(1, 2, 0)).astype(np.float32) / 255 * (2**6) # norm & quant

if len(im.shape) == 3:

im = im[None] # expand for batch dim

在这段代码中,将图片进行padding后转置(opencv的通道和torch的通道位置不一样),并在最后一步/255归一化后*2^6,为什么是6次方呢?这时候就要用到图里的信息,上面贴过upload节点的数据,小数点是第六位的,因此是6次方,这里需要根据你的模型自己调整。

接下来将处理后的图像reshape成DPU input shape后送入DPU执行就可以了

image[0,...] = im.reshape(shapeIn[1:])

job_id = dpu.execute_async(input_data, output_data) # image below is input_data[0]

dpu.wait(job_id)

执行完毕后还需要对DPU结果进行反量化和整形,回顾一下在量化过程中的原始代码

def forward(self, x):

z = [] # inference output

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()

if not self.training: # inference

if self.dynamic or self.grid[i].shape[2:4] != x[i].shape[2:4]:

self.grid[i], self.anchor_grid[i] = self._make_grid(nx, ny, i)

if isinstance(self, Segment): # (boxes + masks)

xy, wh, conf, mask = x[i].split((2, 2, self.nc + 1, self.no - self.nc - 5), 4)

xy = (xy.sigmoid() * 2 + self.grid[i]) * self.stride[i] # xy

wh = (wh.sigmoid() * 2) ** 2 * self.anchor_grid[i] # wh

y = torch.cat((xy, wh, conf.sigmoid(), mask), 4)

else: # Detect (boxes only)

xy, wh, conf = x[i].sigmoid().split((2, 2, self.nc + 1), 4)

xy = (xy * 2 + self.grid[i]) * self.stride[i] # xy

wh = (wh * 2) ** 2 * self.anchor_grid[i] # wh

y = torch.cat((xy, wh, conf), 4)

z.append(y.view(bs, self.na * nx * ny, self.no))

return x if self.training else (torch.cat(z, 1),) if self.export else (torch.cat(z, 1), x)

DPU的输出其实是模型输出,也就是x[i] = self.m[i]的这一部分输出,下面的reshape部分(x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2))我们要自己补一下。

对于本模型的1*120*120*32的检测头,在yolov5后处理部分接受的数据是1*3*120*120*12的,因此需要先把32的维度提上来,进行拆分后再把12的部分转回去。

conv_out0 = np.transpose(output_data[0].astype(np.float32) / 4, (0, 3, 1, 2)).view(1, 3, 12, 120, 120).transpose(0, 1, 3, 4, 2)

conv_out1 = np.transpose(output_data[1].astype(np.float32) / 8, (0, 3, 1, 2)).view(1, 3, 12, 60, 60).transpose(0, 1, 3, 4, 2)

conv_out2 = np.transpose(output_data[2].astype(np.float32) / 4, (0, 3, 1, 2)).view(1, 3, 12, 30, 30).transpose(0, 1, 3, 4, 2)

pred = [conv_out0, conv_out1, conv_out2]

在上面的代码中 从output_data拿出数据以后先进行了反量化,至于为什么/4,和量化的时候一样

https://lgyserver.top/wp-content/uploads/2023/06/image-1.png

Download节点表明120大小的检测头输出的是小数点在第二位量化的结果,因此<code>/42^2

接下来套用原来的后处理和nms即可,nms这里可能需要dump一下原模型的anchor信息,可以在原模型代码里直接访问模型参数拿到

model = DetectMultiBackend('yolov5.pt', device=device)

print("nc: ",model.model.model[-1].nc)

print("anchors: ",model.model.model[-1].anchors)

print("nl: ",model.model.model[-1].nl)

print("na: ",model.model.model[-1].na)

print("stride: ",model.model.model[-1].stride)

后面就跟原始代码一样了,不过多赘述

结语

DPU最后这个模型的运行速度能达到50fps左右,已经很快了。但是主要卡在前处理上,如果模型过大的话resize会耗时,归一化的步骤也很耗时。不知道以后DPU会不会支持自己归一化,后面想搞一个把resize ip和DPU放在一起的设计,应该能加速很多。

感谢大家阅读。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。