移动终端和物联网的快速发展对数据处理即时性的要求越来越高,单纯追求高精度的人工智能算法已经不再能满足人们的需求。算法运行的硬件平台逐渐向各类小型移动设备和可穿戴设备转移,开发具有高实时性、低能耗的人工智能系统成为研究热点。如何在各类移动终端的处理器平台上移植各种主流的深度学习算法亦成为当前的重要需求。
YOLO是目标检测(Object Detection)领域的重要算法。经过近年的快速迭代,2020年4月其最新版本YOLOv4已经发布,算法保留了其一贯的快速高效的优点,同时在准确率、健壮性方面又有了显著进步,在实时检测领域的竞争力进一步提升。YOLO网络使用其作者开发的Darknet框架,可以在Windows和Linux上运行,并支持GPU和OpenCV。Darknet实时检测的优势使得它具有应用到嵌入式平台的潜力,同时由于主体框架完全由C语言写成,在移植上有一定的便利性。本专栏首先尝试将YOLOv3所使用的Darknet-53主体框架运行在RISC-V架构的Linux操作系统上,使用QEMU模拟器进行仿真,在此基础上后续将尝试将其移植到RISC-V裸机平台上运行。 本文将详细介绍搭建RISC-V QEMU上Linux系统,以及在该系统上运行Darknet-53的完整过程,
GCC工具链可以选择下载开源代码并自行编译,也可以下载已经编译好的二进制文件。使用前者,从Github上克隆源码:
git clone --recursive https://github.com/riscv/riscv-gnu-toolchain
工具链提供裸机(newlib)和Linux(glibc)两种版本,虽然本文的程序运行于操作系统上,但后续也将尝试嵌入式裸机平台的移植,因此两种版本均安装。在克隆的项目路径下,执行以下代码安装裸机工具链:
./configure --prefix=/opt/riscv-newlib --enable-multilib
make
执行以下代码安装Linux工具链:
./configure --prefix=/opt/riscv-glibc --enable-multilib
make linux
完成后添加环境变量和系统路径即可直接使用。
QEMU(Quick Emulator)是一套GPL开源协议下的多架构计算机仿真器和虚拟机,作为仿真器可以在本地计算机上运行为另一种架构编写制作的系统和程序。 如果在安装riscv-gnu-toolchain
的时候从Github上面下载了工具链的完整源代码,则目录riscv-gnu-toolchain/qemu
下面已经包含了QEMU的源码,不需要再次下载。否则可执行以下命令下载源码:
git clone https://git.qemu.org/git/qemu.git
在QEMU文件路径下设定配置信息并编译安装即可:
../configure -–prefix=/opt/riscv-qemu –-target-list=riscv64-softmmu
make
make install
添加系统路径后即可使用qemu-system-riscv64
进行仿真。
Buildroot是一个用于创建嵌入式Linux系统的框架,通过独立的配置文件和使用交叉工具链,Buildroot可以制作一个完整的Linux文件系统。
Buildroot的最新版本可以从其官方网站下载,本文使用2020.02.2版本。解压后的文件夹中,configs/
路径下包含了软件为各种不同架构和使用场景预生成的默认配置,输入以下命令,可以导入针对RISC-V64架构的默认配置文件。
make qemu_riscv64_virt_defconfig
在文件夹中可以找到生成的.config
配置文件,使用该默认的方式,Buildroot创建的文件系统大小一般比较小(根文件系统大小默认为60M,可查找字段BR2_TARGET_ROOTFS_EXT2_SIZE
),且可使用的shell命令比较少,不一定能满足用户的使用需求。此时用户可以按照格式自行修改.config
中的字段值来更改配置,但官方一般不建议直接修改该文件,更好的方法是使用Buildroot提供的图形化配置界面。在终端输入以下命令即可启动:
make menuconfig
该图像菜单需要NCurses
软件库的支持,如果缺失可用apt
进行安装。打开后一级菜单下共有10个类别可以进行设置,现对各类的主要作用和部分重要的参数项进行说明。
为目标机器的架构及应用程序二进制标准等相关选项,主要修改:
Target Architecture: RISCV
设置该字段后,本页其他字段会自动调整,一般保持默认状态即可。其中Target Architecture Size
应为64-bit,Target ABI
应为lp64d。
该目录下通过Toolchain type
提供两种工具链选项,设置为External toolchain
可以使用外部工具链,用户可以指定下载链接。C library
字段指定C库,可选uClibc、glibc、musl,它们的区别可以参阅相关文档,本文选择GNU标准的glibc作为C库。Darknet-53框架的主体部分由C语言构成,在不移植OpenCV相关特性时无需考虑C++相关特性,但为后续使用方便可以勾选勾选Enable C++ support
可为工具链增加C++支持。其他字段可保持默认值。
用于设定Linux内核版本,首先勾选Linux Kernel
项,生成的子菜单中默认使用最新的内核或手动输入要使用的版本,编译时将自动下载相应版本的内核。选择Custom Git repository
则可从指定的代码仓库下载自定义的内核。Kernel binary format
设置内核的镜像格式,RISCV64下支持Image和vmlinux两种格式,两者均未经过压缩,前者删除了重定位信息和符号表,本文选择Image格式。
Linux文件系统设置,勾选ext2/3/4 root filesystem
,RISCV64的默认配置文件使用ext2,经过测试选用ext4也是可行的。exact size
设置根文件目录的大小,视实际使用需求修改为一个较大值。
System configuration
下为Linux系统配置,包含主机名、root密码、shell类型和DHCP网络接口等。Bootladers
设定引导程序,此项必须设定为opensbi下的qemu/virt
。 此外,使用默认设置安装操作系统只提供/bin
目录下的几个最基本的shell命令,逐一地下载安装额外的软件是比较麻烦的。Target packages
选项则一定程度上解决了这个问题,它提供了大量可选择的程序包,勾选后Buildroot将批量安装这些应用,如git、pkgconf、binutils等常用包均可在其中找到,根据需要选择即可。
完成设置后,使用命令make
即可完成编译,在/output/images/
路径下可找到制作完成的二进制文件。此后在Buildroot根目录下使用QEMU启动镜像:
qemu-system-riscv64
-m 4G -smp 4
-M virt -nographic
-bios output/images/fw_jump.elf
-kernel output/images/Image
-append "root=/dev/vda ro"
-drive file=output/images/rootfs.ext2,format=raw,id=hd0
-device virtio-blk-device,drive=hd0
-netdev user,id=net0 -device virtio-net-device,netdev=net0
-bios
,-kernel
和-drive
选项需要和Buildroot生成的文件路径、文件名项匹配,-m
和-smp
指定运行内存和分配CPU核心数,由于Darknet-53消耗较多的系统资源,使用默认的内存大小可能会导致程序无法正常运行。成功后将终端将进入Linux登陆界面,此时可以通过root身份登入,并查看系统相关信息。
系统搭建完成,可以着手运行Darknet-53.
YOLOv3的开源代码可以从Github上的Darknet项目下载,项目的基本结构可梳理为:
-Darknet
-cfg # 网络参数,配置文件
-data # 示例图片和数据
-examples # 调用底层代码实现的应用
-include # darknet.h头文件
-python
-scripts
-src # 底层实现的源代码
Makefile,LICENSE,etc
这个项目中除了网络的底层实现外,还包括一些python脚本以及调用底层代码实现的具体应用,同时包含一些Darknet-53本身不会用到的冗余代码。/python
和/scripts
文件暂不考虑,网络移植的核心是关注/src
和/examples
两个文件中的源码。
项目有一个写好的Makefile,使用时需要对其中的一些参数进行修改。将在QEMU上模拟的是一个在CPU运行、无CUDA和OpenCV的版本,因此文件开头设置为:
GPU=0
CUDNN=0
OPENCV=0
OPENMP=0
DEBUG=1
为便于调试启用了DEBUG
项。工具链需要修改为响应的RISCV64交叉工具链:
CROSS_COMPILE = riscv64-unknown-linux-gun-
CC = $(CROSS_COMPILE)gcc
AR = $(CROSS_COMPILE)ar
# CPP =
# NVCC =
对于CPP,如果安装了C++支持可以启用工具,但在项目中仅有/src
路径有唯一的C++程序image_opencv.cpp
,该文件在本例中不会使用,所以可以禁用CPP工具链,此外需要在Makefile的OBJ
变量中将该文件去除。
编译参数涉及的改动较小,可以设置为:
ARFLAGS=rcs
OPTS=-Ofast
LDFLAGS= -lm
COMMON= -Iinclude/ -Isrc/
CFLAGS=-Wall -Wno-unused-result -Wno-unknown-pragmas -Wfatal-errors -fPIC -mabi lp64d
ABI接口应当与Buildroot构建Linux系统时的设定保持一致。关于LDFLAGS
的-pthread
参数将在后面说明。
在交叉编译时,以下行的代码可能会报错:
network net = parse_network_cfg(cfgfile)
network
是Darknet定义的结构体,用于储存和建模单层网络信息的抽象模型,上述函数解析一个网络的参数。在/examples
文件夹下的多个文件都包含该形式的函数调用,现在查看函数parse_network_cfg
的定义为:
network *parse_network_cfg(char *filename)
可以看到这是一个指针问题,parse_network_cfg
返回的是一个network*
类型的指针,但是该指针被直接赋给了一个network对象。检查/examples
部分的源码后发现,作者在一部分源码文件中(如yolo.c
、 darknet.c
)中使用了正确的指针形式,一部分源码(如dice.c
、swag.c
)中则没有。修改的方法时,将所有上述类型的变量定义修改为:
network* net = parse_network_cfg(cfgfile)
此外,对后续的结构成员变量的引用格式均由net.member
改为net->member
,这部分涉及的小改动较多,需要比较仔细。修改后的程序不会再出现类似的错误信息。 有趣的是,这个错误是否出现视编译器的版本而有所不同,在Windows和通用的x86 Linux系统上使用8.0以上的GCC一般不会提示出错。此外,由于主函数入口是/examples
路径下的darknet.c
生成的,主要的测试工作都围绕该函数进行,而这个程序当中的指针使用是正确的,因此使用Darknet时可能不会发现这个问题。
经过预处理的图像数据可以被网络正确读入,Darknet通过配置文件解析网络结构并用对应结构体生成网络树后,就正式开始读取待训练和预测的数据文件。Darknet以多线程的方式乱序读入图像数据,这个部分主要由程序/src/data.c
来完成。YOLO使用了一个比较曲折的过程来完成数据读入,其数据模型可以归纳为下图:
模型包含四个线程相关函数,函数间通过深拷贝一个内建结构体load_args
来传递线程和网络参数。顶层函数负责发起总线程,用于进行一次独立的数据读入任务,这个总线程仅开辟一个新线程load_threads
,负责具体的分配工作。load_threads
函数获取将要读入的总图像数量,以及可供使用的待开辟子线程数量,并尽可能平均地把图像读入的任务分配到各个子任务块上。分配方式按下式计算:
上式中除法使用整数除法。该式可以巧妙地完成任务分割工作,在实际使用中不可避免地会发生总输入图像数量不能够被线程数量整除的情况,这时在希望分配尽量均匀的情况下要保证所有n的和与总的输入数量一致。上式恰好解决了这个问题,以73张图片的8线程分配为例,分配的结果将是9,9,9,9,9,9,9,8。
分配完成后的子任务各自发起一个单独的线程,并通过随机函数乱序地映射到具体的图像物理存储上,开始顺次读入数据。在读入期间主函数挂起并以阻塞方式等待子线程执行,直到所有读入工作全部完成。注意到被解析的文件路径信息以行向量的形式储存于二维数组中,这些路径可能会被多个不同的线程同时访问,因此需要使用线程锁保护路径信息,确保同一时刻路径只能被读取一次。
上述过程的相关操作都使用POSIX标准的线程模型,函数预定于头文件
,在本文使用的RISC-V Linux操作系统上,在Makefile
中的LDFLAGS
上加上-pthread
选项通常不会引起什么问题,得到的程序也可以执行。但如果要使用newlib进行裸机程序riscv64-unknown-elf-
编译则是无法通过的,考虑到后续裸机移植的需要,需要对源码读取数据流的模型进行一些修改,去除其对于
的依赖,原本的代码在函数间传递线程ID,而在尽量保持原有的函数名和调用关系不变的情况下,需要保持函数间正确传递深拷贝的结构信息,修改后的代码应该在去除-pthread
编译选项的情况下正确通过编译。
完成各种基础性的改动后,使用riscv-gnu-toolchain
编译成功Darknet的主程序将可以在RISC-V QEMU上的Linux运行。项目的编译地点有两种选择,一种是在QEMU Linux文件系统上完成,该方式需要在虚拟机上有可用的RISCV工具链,且由于缺少高效的编程环境,在编译出现问题的情况下会比较难调试。另一种方式是在运行QEMU的主机(Host)上完成开发,然后将二进制文件和相关依赖放进虚拟机进行仿真。可以通过mount
工具设置一个挂载点来进行主机到QEMU的文件移动,在Buildroot值作的文件系统路径output/images/
下,建立两个文件夹rootfs
和tmpfs
,将要拷贝的文件放在rootfs
下,使用命令:
sudo mount -t ext4 ./rootfs.ext4 ./tmpfs -o loop
sudo cp -r ./rootfs/* ./tmpfs
sudo umount ./tmpfs
即可完成文件移动,注意文件系统的格式以及名字要和Buildroot的配置相一致。 此后即可运行示例程序,以yolov3-tiny
的预训练权重为例,执行以下命令:
./darknet detect cfg/yolov3-tiny.cfg yolov3-tiny.weights data/dog.jpg
可以看到网络正确加载权重并获得预测结果:
运行示例注意受到Darknet消耗系统资源的影响,启动QEMU时需要为其分配足够的空间。默认的内存大小可能可以正常加载yolov3-tiny
网络参数,但在完整加载yolov3
的权重数据时则可能出现段溢出等相关错误。
本文完整地介绍了搭建RISC-V QEMU并在其上运行Linux系统的全过程,并在该系统上运行了Darknet的基础功能,过程中没有涉及网络的训练。QEMU采用软件动态翻译的方式模拟其他硬件指令集的运行,在未优化的系统上运行这样一套程序速度是相当慢的,并不能体现出YOLO算法的速度优势,但这部分工作以及该过程中笔者对项目结构的梳理也为裸机移植工作打下了基础,后续将尝试在一个RISC-V架构的裸机平台上实现YOLO算法的运行工作。