我把 ncnn 移植到 RISC-V 啦!
本帖最后由 sky 于 2020-9-16 10:15 编辑可以转载,但不准删改内容!
RISC-V,我喜欢缩写成 riscv,能少按一次 shift 和减号,是一个基于精简指令集(RISC)原则的开源指令集架构(ISA)。作为完全开源的指令集,天生自带开源的光环基因,纵使当今 x86 ARM 几乎绝对市场垄断,依然生机勃勃,持续发展着
https://en.wikipedia.org/wiki/RISC-V
如果要问我,为什么要把 ncnn 移植到 riscv 上面跑?那就是开源文化基因的力量,英文单词 meme 的魔法
其实移植过程中还是踩了一些坑的,感谢中科院软件所智能软件研究中心的大佬热心解答我的提问
一,编译工具链,pk,仿真器
https://github.com/riscv/riscv-gnu-toolchain
其实第一次搭建环境,照着 README 的命令就足够了,首先是编译工具链,时间比较久,make 完会自动安装到 /opt/riscv,不需要 make install
$ export PATH=/opt/riscv/bin:$PATH
$ sudo apt-get install autoconf automake autotools-dev curl python3 libmpc-dev libmpfr-dev libgmp-dev gawk build-essential bison flex texinfo gperf libtool patchutils bc zlib1g-dev libexpat-dev
$ git clone https://github.com/riscv/riscv-gnu-toolchain.git
$ cd riscv-gnu-toolchain
$ git submodule update --init
$ ./configure --prefix=/opt/riscv
$ make -j4
https://github.com/riscv/riscv-pk
然后是 pk,proxy kernel
$ git clone https://github.com/riscv/riscv-pk.git
$ cd riscv-pk
$ mkdir build && cd build
$ ../configure --prefix=/opt/riscv --host=riscv64-unknown-elf
$ make -j4
$ make install
https://github.com/riscv/riscv-isa-sim
最后是仿真器,又名 spike
$ sudo apt-get install device-tree-compiler
$ git clone https://github.com/riscv/riscv-isa-sim.git
$ cd riscv-isa-sim
$ mkdir build && cd build
$ ../configure --prefix=/opt/riscv
$ make -j4
$ make install
正常情况下不会出错,/opt/riscv 在 install 的时候需要 root 权限
搭建完成就来编译个 hello。先写个 hello.c,用 riscv gcc 编译为 riscv 二进制,然后用 spike 仿真器在 Linux x86 跑 riscv 程序,成功了
#include <stdio.h>
int main()
{
fprintf(stderr, "hello\n");
return 0;
}$ riscv64-unknown-elf-gcc hello.c -o hello
$ spike /opt/riscv/riscv64-unknown-elf/bin/pk ./hello
bbl loader
hello
移植 ncnn
第一件事,安排个 riscv64-unknown-elf.toolchain.cmake
https://github.com/Tencent/ncnn/blob/master/toolchains/riscv64-unknown-elf.toolchain.cmake
第二件事,编译起来,已经预料到没有 protobuf opencv,newlib 也没有 openmp,那么禁用掉
$ cmake -DCMAKE_TOOLCHAIN_FILE=../riscv64-unknown-elf.toolchain.cmake -DNCNN_OPENMP=OFF -DNCNN_BUILD_TOOLS=OFF -DNCNN_BUILD_EXAMPLES=OFF ..
$ make -j4
[*]platform.h 里的 Mutex ConditionVariable Thread 依赖 pthread,newlib 是没有的,新加一个 NCNN_THREADS 开关,彻底屏蔽一切和线程相关的代码
[*]posix_memalign 和 sleep 属于 posix 函数,newlib 也是没有的,新加条件判断 defined(__unix__) || defined(__APPLE__) 绕过
就改了这两个地方,似乎并没有什么困难嘛...
$ cmake -DCMAKE_TOOLCHAIN_FILE=../riscv64-unknown-elf.toolchain.cmake -DNCNN_THREADS=OFF -DNCNN_OPENMP=OFF -DNCNN_BUILD_TOOLS=OFF -DNCNN_BUILD_EXAMPLES=OFF ..
$ make -j4
编译通过了,但是跑 ctest 会因为 test_xxx 无法直接运行全部 Failed。riscv 的测试程序需要像 hello 一样,用 spike 仿真器跑,上网搜索一番找到这个方案,魔改一番,加上 spike 和 pk 参数
https://stackoverflow.com/questions/28812533/how-to-pass-command-line-arguments-in-ctest-at-runtime
$ TESTS_EXECUTABLE_LOADER=spike TESTS_EXECUTABLE_LOADER_ARGUMENTS=/opt/riscv/riscv64-unknown-elf/bin/pk ctest
单元测试通过了,感觉速度比 qemu 这类的快
开启 RISC-V V 扩展(SIMD)
前面编译的三大件,默认架构 rv64imafdc,也就是 rv64gc,也就是 k210 上面用的架构,是没有 SIMD 指令的。 ncnn 的优化代码中使用大量的 SIMD 指令实现 cpu 加速,打开 riscv SIMD 扩展指令相当必要。
riscv 的 V 扩展就是 riscv 的 SIMD 标准,目前最新版本是 0.9,下一个版本 1.0 很可能就是正式版。1.0 和 0.9 看起来是完全兼容的,没有重大改动,并且 riscv-gnu-toolchain git 只有 rvv-0.9 分支,spike 也声明支持 0.9 版本 V 扩展,那么就用 0.9
$ cd riscv-gnu-toolchain
# rvv-0.9.x = 5842fde8ee5bb3371643b60ed34906eff7a5fa31
$ git checkout 5842fde8ee5bb3371643b60ed34906eff7a5fa31
$ git submodule update --init
[*]riscv-gnu-toolchain 和 riscv-pk 编译时 ./configure 添加 --with-arch=rv64gcv 参数启用 V 扩展
[*]riscv-isa-sim 编译时 ./configure 添加 --with-isa=rv64gcv 参数设置默认启用 V 扩展
编译 ncnn 链接时出错,报错 undefined reference to 'math_oflowf',经过寻找发现,这个math_oflowf 函数实现在 newlib 中,并且被 __OBSOLETE_MATH 条件屏蔽了,代码里用到 exp() 会报这个错。一行 sed 把这个条件删掉,重新编译一遍,通过
# rvv-0.9.x fix undefined reference to '__math_oflowf'
sed -i '/__OBSOLETE_MATH/d' riscv-newlib/newlib/libm/common/math_errf.c
简单优化一个 riscv op
有了支持 V 扩展的 gcc 和 spike,当然要试试看效果,就简单优化个 riscv clip
SIMD 指令优化有三种方式,intrinsic/inline assembly/assembly,嫌弃 assembly 麻烦,ncnn 一直是用前两种实现方式。正常的话,clip这种形式简单不怎么耗寄存器的 op,适合用 intrinsic,简单方便效果好。可是找了一圈工具链文件夹没找到 intrinsic 头文件,原来 riscv-gnu-toolchain 并没有实现 V 扩展 intrinsic,我看 isrc-cas/rvv-llvm 正在开发相关的 intrinsic,不清楚是怎样的状态,暂时退而求其次 inline assembly 实现一下
https://github.com/isrc-cas/rvv-llvm
asm volatile("vle8.vv0, (a1)");
直接这么写一行 riscv v 指令,能通过编译,运行会报错 Illegal Instruction,我以为是工具链或 spike 编译的问题,倒腾了好久,幸亏大佬解答了一番
https://github.com/isrc-cas/plct-spike/issues/3
如果是 x86 sse/avx 或 arm neon 优化,循环通常会写成这个样子,一次取8个数或4个数,最后剩余的用一个 naive 循环处理
int i = 0;
#if __AVX___
for (; i + 7 < N; i += 8)
{
}
#endif // __AVX___
#if __SSE___
for (; i + 3 < N; i += 4)
{
}
#endif // __SSE___
for (; i < N; i++)
{
}
https://github.com/riscv/riscv-v-spec/releases/tag/0.9
riscv v 是全新的变长 SIMD 设计,据说是沿袭 arm sve,最大能支持 8192bit。这样的好处就是代码可以只写一个循环,里面到底是展开8个数还是4个数,是自动的!再看看 riscv v 的其他指令,好些运算指令支持 mask 寄存器,这设计也是相当 modern 的,大概是从 avx512 学来的,可以用这个 mask 实现很多原先要多条指令才能实现的骚操作,真的很 flexible
这段代码里的 vsetvli 控制循环步进,t0 就是 cpu 告诉我他想一次性处理 fp32 的个数。remain 是我告诉 cpu 还有多少个数需要处理,你不准搞多了。m8 是我建议 cpu 一次性处理8个数,cpu 可以不听,返回4也是可以的
asm volatile(
"L0: \n"
"vsetvli t0, %1, e32, m8 \n"// t0 = vsetvli(remain, 32bit x 8)
"vle32.v v0, (%0) \n"// load ptr to v0
"vfmax.vf v0, v0, %4 \n"
"vfmin.vf v0, v0, %5 \n"
"vse32.v v0, (%0) \n"// store v0 to ptr
"slli t1, t0, 2 \n"
"add %0, %0, t1 \n"// ptr += t0 * sizeof(float)
"sub %1, %1, t0 \n"// remain -= t0
"bnez %1, L0 \n"
: "=r"(ptr), // %0
"=r"(remain) // %1
: "0"(ptr),
"1"(remain),
"f"(min), // %4
"f"(max)// %5
: "cc", "memory", "t0", "t1"
);
https://github.com/Tencent/ncnn/blob/master/src/layer/riscv/clip_riscv.cpp
也许 V 扩展太 flexible,标准也没有正式定稿,目前还没有看到完整实现 V 扩展的内核,这些优化代码只能在 spike/qemu 上面运行期待将来有真实的硬件产品实现,latyas 说 k510 没有 V ... qwq
https://github.com/riscv/riscv-cores-list
增加 RISC-V 32位 编译
去年参加 riscv 的开源活动,NXP 送了我一块 vega-lite 开发板,CPU 是 rv32imc,NXP 真是太棒了 QvQ
趁热打铁,也编译一套 riscv 32位 ncnn,方法和前面 V 扩展基本一致,区别就是 rv64gcv 换成 rv32imc。rv32imc 缺少 A 扩展,ncnn 代码里用到 __atomic_fetch_add 导致链接报错,添加一份无 atomic 实现就行
最后,整理出 rv64gv rv64gcv rv32imc 三种架构,放到 github action ci,自动编译测试。rv32imc 缺少 F 扩展,没有浮点计算能力,跑起来慢得出乎想象,隔壁 rv64gc 几分钟跑完全部测试,这边 rv32imc 跑一个 test_convolution 就跑了一个多小时,算了,ci 有编译就行了,测试就放弃治疗了
页:
[1]