查看: 1941|回复: 0
收起左侧

我把 ncnn 移植到 RISC-V 啦!

[复制链接]

  离线 

  • TA的每日心情
    拍拍
    2022-6-27 11:09
  • 签到天数: 25 天

    [LV.4]

    发表于 2020-9-16 09:56:36 | 显示全部楼层 |阅读模式

    有人预言,RISC-V或将是继Intel和Arm之后的第三大主流处理器体系。欢迎访问全球首家只专注于RISC-V单片机行业应用的中文网站

    您需要 登录 才可以下载或查看,没有帐号?立即注册

    x
    本帖最后由 sky 于 2020-9-16 10:15 编辑

    国内芯片技术交流-我把 ncnn 移植到 RISC-V 啦!risc-v单片机中文社区(1)


    可以转载,但不准删改内容!

    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

    1. $ export PATH=/opt/riscv/bin:$PATH

    2. $ 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

    3. $ git clone https://github.com/riscv/riscv-gnu-toolchain.git
    4. $ cd riscv-gnu-toolchain
    5. $ git submodule update --init

    6. $ ./configure --prefix=/opt/riscv
    7. $ make -j4
    复制代码

    https://github.com/riscv/riscv-pk

    然后是 pk,proxy kernel

    1. $ git clone https://github.com/riscv/riscv-pk.git
    2. $ cd riscv-pk

    3. $ mkdir build && cd build
    4. $ ../configure --prefix=/opt/riscv --host=riscv64-unknown-elf
    5. $ make -j4
    6. $ make install
    复制代码

    https://github.com/riscv/riscv-isa-sim

    最后是仿真器,又名 spike


    1. $ sudo apt-get install device-tree-compiler

    2. $ git clone https://github.com/riscv/riscv-isa-sim.git
    3. $ cd riscv-isa-sim

    4. $ mkdir build && cd build
    5. $ ../configure --prefix=/opt/riscv
    6. $ make -j4
    7. $ make install
    复制代码

    正常情况下不会出错,/opt/riscv 在 install 的时候需要 root 权限

    搭建完成就来编译个 hello。先写个 hello.c,用 riscv gcc 编译为 riscv 二进制,然后用 spike 仿真器在 Linux x86 跑 riscv 程序,成功了

    1. #include <stdio.h>

    2. int main()
    3. {
    4.     fprintf(stderr, "hello\n");
    5.     return 0;
    6. }
    复制代码
    1. $ riscv64-unknown-elf-gcc hello.c -o hello
    2. $ spike /opt/riscv/riscv64-unknown-elf/bin/pk ./hello
    3. bbl loader
    4. hello
    复制代码

    移植 ncnn
    第一件事,安排个 riscv64-unknown-elf.toolchain.cmake

    https://github.com/Tencent/ncnn/blob/master/toolchains/riscv64-unknown-elf.toolchain.cmake

    第二件事,编译起来,已经预料到没有 protobuf opencv,newlib 也没有 openmp,那么禁用掉
    1. $ cmake -DCMAKE_TOOLCHAIN_FILE=../riscv64-unknown-elf.toolchain.cmake -DNCNN_OPENMP=OFF -DNCNN_BUILD_TOOLS=OFF -DNCNN_BUILD_EXAMPLES=OFF ..
    2. $ make -j4
    复制代码

    • platform.h 里的 Mutex ConditionVariable Thread 依赖 pthread,newlib 是没有的,新加一个 NCNN_THREADS 开关,彻底屏蔽一切和线程相关的代码
    • posix_memalign 和 sleep 属于 posix 函数,newlib 也是没有的,新加条件判断 defined(__unix__) || defined(__APPLE__) 绕过

    就改了这两个地方,似乎并没有什么困难嘛...

    1. $ cmake -DCMAKE_TOOLCHAIN_FILE=../riscv64-unknown-elf.toolchain.cmake -DNCNN_THREADS=OFF -DNCNN_OPENMP=OFF -DNCNN_BUILD_TOOLS=OFF -DNCNN_BUILD_EXAMPLES=OFF ..
    2. $ 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
    1. $ 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

    1. $ cd riscv-gnu-toolchain
    2. # rvv-0.9.x = 5842fde8ee5bb3371643b60ed34906eff7a5fa31
    3. $ git checkout 5842fde8ee5bb3371643b60ed34906eff7a5fa31
    4. $ 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 把这个条件删掉,重新编译一遍,通过

    1. # rvv-0.9.x fix undefined reference to '__math_oflowf'
    2. 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
    1. asm volatile("vle8.v  v0, (a1)");
    复制代码

    直接这么写一行 riscv v 指令,能通过编译,运行会报错 Illegal Instruction,我以为是工具链或 spike 编译的问题,倒腾了好久,幸亏大佬解答了一番

    https://github.com/isrc-cas/plct-spike/issues/3

    如果是 x86 sse/avx 或 arm neon 优化,循环通常会写成这个样子,一次取8个数或4个数,最后剩余的用一个 naive 循环处理

    1. int i = 0;
    2. #if __AVX___
    3. for (; i + 7 < N; i += 8)
    4. {
    5. }
    6. #endif // __AVX___
    7. #if __SSE___
    8. for (; i + 3 < N; i += 4)
    9. {
    10. }
    11. #endif // __SSE___
    12. for (; i < N; i++)
    13. {
    14. }
    复制代码

    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也是可以的

    1. asm volatile(
    2.     "L0:                        \n"
    3.     "vsetvli    t0, %1, e32, m8 \n"// t0 = vsetvli(remain, 32bit x 8)
    4.     "vle32.v    v0, (%0)        \n"// load ptr to v0
    5.     "vfmax.vf   v0, v0, %4      \n"
    6.     "vfmin.vf   v0, v0, %5      \n"
    7.     "vse32.v    v0, (%0)        \n"// store v0 to ptr
    8.     "slli       t1, t0, 2       \n"
    9.     "add        %0, %0, t1      \n"// ptr += t0 * sizeof(float)
    10.     "sub        %1, %1, t0      \n"// remain -= t0
    11.     "bnez       %1, L0          \n"
    12.     : "=r"(ptr),   // %0
    13.       "=r"(remain) // %1
    14.     : "0"(ptr),
    15.       "1"(remain),
    16.       "f"(min), // %4
    17.       "f"(max)  // %5
    18.     : "cc", "memory", "t0", "t1"
    19. );
    复制代码

    https://github.com/Tencent/ncnn/blob/master/src/layer/riscv/clip_riscv.cpp

    也许 V 扩展太 flexible,标准也没有正式定稿,目前还没有看到完整实现 V 扩展的内核,这些优化代码只能在 spike/qemu 上面运行
    期待将来有真实的硬件产品实现,latyas 说 k510 没有 V ... qwq


    增加 RISC-V 32位 编译

    去年参加 riscv 的开源活动,NXP 送了我一块 vega-lite 开发板,CPU 是 rv32imc,NXP 真是太棒了 QvQ

    国内芯片技术交流-我把 ncnn 移植到 RISC-V 啦!risc-v单片机中文社区(2)


    趁热打铁,也编译一套 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 有编译就行了,测试就放弃治疗了





    上一篇:闲说开源硬件之七:STM32平台
    下一篇:14亿人的战争:中国人用了30年望见计算力的珠峰
    RISCV作者优文
    全球首家只专注于RISC-V单片机行业应用的中文网站
    回复

    使用道具 举报

    高级模式
    B Color Image Link Quote Code Smilies

    本版积分规则

    关闭

    RISC-V单片机中文网上一条 /2 下一条



    版权及免责声明|RISC-V单片机中文网 |网站地图

    GMT+8, 2024-11-6 08:04 , Processed in 0.448889 second(s), 48 queries .

    快速回复 返回顶部 返回列表