ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Chipyard中Gemmini加速器仿真与调优实战

Chipyard中Gemmini加速器仿真与调优实战 1. 为什么要在 Chipyard 里折腾 Gemmini如果你最近在搞 RISC-V 相关的加速器研究或者正在找一个能跑通、能改、能流片的开源 AI 加速器框架那 Gemmini 这个名字大概率已经在你眼前晃过好几回了。它挂在 Chipyard 这个 RISC-V 全栈 SoC 生成框架下面是一个专门做矩阵乘加和卷积的 systolic array 加速器生成器。说白了你给它一组参数它给你吐出一个能挂到 Rocket 或 BOOM 核上的加速器 RTL再配合软件栈就能在 FPGA 或者仿真环境里跑起来。我最初接触 Gemmini 是因为手头有个边缘推理的小项目想验证一下不同阵列规模对吞吐和面积的影响。当时试过自己从零搭一个脉动阵列结果光是处理数据搬运和流水线握手就折腾了两周后来果断转向 Gemmini。它的好处在于参数化程度高、和 Chipyard 集成得干净、软件侧有完整的 ONNX 到 C 的编译链路最关键的是社区活跃踩坑的时候能搜到别人的解决方案。这篇文章面向的是已经对 RISC-V 和 Chipyard 有基本了解、想快速把 Gemmini 跑起来的同学。如果你连 Chipyard 是什么都还没概念建议先去把 Chipyard 的文档翻一遍至少知道怎么生成一个默认的 Rocket 配置。我会从环境准备讲到仿真跑通再到参数调整和常见问题排查尽量把我在实际操作中踩过的坑和总结的技巧都倒出来。2. 环境准备与依赖安装2.1 系统要求与基础工具链Chipyard 官方推荐在 Ubuntu 20.04 或 22.04 上跑我实测下来 22.04 最省心。内存建议 32GB 起步因为生成 Verilog 和跑仿真的时候尤其是带 Gemmini 的配置吃内存相当凶。我一开始用 16GB 的虚拟机跑到中途直接 OOM后来加到 32GB 才顺畅。磁盘至少留 100GBChipyard 加上各种子模块和构建产物体积膨胀得很快。基础依赖这块Chipyard 的build-setup.sh脚本会自动装一部分但有几个包我建议你提前手动装好省得中途报错sudo apt-get update sudo apt-get install -y autoconf automake autotools-dev curl libmpc-dev \ libmpfr-dev libgmp-dev gawk build-essential bison flex texinfo gperf \ libtool patchutils bc zlib1g-dev libexpat-dev git cmake \ libglib2.0-dev libpixman-1-dev python3 python3-pip这里面的libglib2.0-dev和libpixman-1-dev是给 QEMU 用的Chipyard 的仿真环境依赖它。很多人第一次跑会卡在 QEMU 编译上八成就是这两个包没装。2.2 拉取 Chipyard 与子模块初始化Chipyard 的仓库用 git submodule 管理了一堆依赖包括 rocket-chip、Gemmini、riscv-tools 等。克隆的时候一定要加--recursive否则后面还得手动补git clone --recursive https://github.com/ucb-bar/chipyard.git cd chipyard git checkout main如果你已经克隆了但忘了加--recursive可以补一句git submodule update --init --recursive这一步网络不好的话会非常慢因为子模块加起来有好几个 GB。我的经验是找个网络稳定的时段一次性拉完中途断了的话 submodule 状态会很乱清理起来麻烦。2.3 设置环境变量与构建工具链Chipyard 提供了一个env.sh来设置环境变量但在这之前需要先跑构建脚本。官方推荐的方式是./build-setup.sh --skip-conda--skip-conda是跳过 conda 环境创建如果你已经自己配好了 Python 环境加这个参数能省不少时间。脚本会依次构建 riscv-tools、spike、QEMU 等。整个过程视机器性能大概要 40 分钟到 2 小时不等。我建议挂个tmux或者nohup免得 SSH 断了前功尽弃。构建完成后每次开新终端都要 source 一下环境source env.sh注意env.sh必须在 Chipyard 根目录下 source而且它会修改PATH和RISCV等变量。如果你同时有多个 RISC-V 工具链注意别冲突。2.4 Gemmini 子模块的位置与确认Gemmini 在 Chipyard 里的路径是generators/gemmini。你可以进去看一眼确认子模块拉全了ls generators/gemmini正常情况下应该能看到src、software、scripts等目录。如果这里是空的说明 submodule 没初始化成功回到上一步重新拉。3. Gemmini 的核心架构与参数体系3.1 脉动阵列的基本工作原理Gemmini 的核心是一个输出 stationary 的 systolic array。什么意思呢就是权重预先加载到阵列的 PE 里输入数据从左侧流入部分和从上往下累加。这种设计的好处是权重复用率高适合卷积和全连接层这种权重固定的场景。阵列的维度由tileRows和tileColumns两个参数决定。比如tileRows16, tileColumns16就是一个 16x16 的阵列共 256 个 PE。每个 PE 内部有一个乘法器和一个累加器支持 INT8 和 INT32 的累加。你可以在configs/GemminiConfigs.scala里找到这些配置。3.2 关键参数解析与选型逻辑Gemmini 的参数分几大类阵列维度、数据类型、存储层次、DMA 配置。我整理了一个常用参数的对照表方便你根据场景选参数名含义常用取值选型建议tileRows阵列行数8/16/32越大吞吐越高但面积和布线压力剧增tileColumns阵列列数8/16/32同上通常和 tileRows 对称meshRows网格行数与 tileRows 一致一般不用改dataWidth数据位宽8/16INT8 够用INT16 精度高但面积翻倍accWidth累加位宽32基本固定除非你有特殊精度需求sp_banks暂存器 bank 数4影响数据搬运带宽sp_capacity暂存器容量256KB根据模型大小调整我一开始贪心直接上了 32x32 的阵列结果综合时间爆炸FPGA 上根本布不下。后来退回到 16x16综合时间从 40 分钟降到 10 分钟资源占用也合理了。所以选型的时候一定要结合你的目标平台别一上来就拉满。3.3 存储层次与数据流Gemmini 的存储分三级外部 DRAM、暂存器Scratchpad、PE 内部的寄存器。数据从 DRAM 通过 DMA 搬到暂存器再从暂存器喂给阵列。这个过程中DMA 的配置很关键dma_maxbytes决定了单次搬运的最大字节数设太小会导致频繁中断设太大又浪费带宽。暂存器的 bank 数sp_banks影响并行访问能力。4 个 bank 是默认值基本够用。如果你发现仿真时数据搬运成了瓶颈可以试着加到 8但面积会涨。4. 从零跑通第一个 Gemmini 仿真4.1 生成 Verilog 与配置选择Chipyard 用 Scala 的 config 系统来生成不同的 SoC。Gemmini 的配置定义在generators/gemmini/src/main/scala/gemmini/GemminiConfigs.scala里。常用的有几个GemminiRocketConfigRocket 核 Gemmini最基础的组合GemminiBoomConfigBOOM 核 Gemmini性能更高GemminiLargeRocketConfig大阵列版本生成 Verilog 的命令是cd sims/verilator make CONFIGGemminiRocketConfig这一步会调用 firrtl 和 verilator把 Chisel 代码转成 C 仿真模型。第一次跑会编译 verilator 模型大概要 15 到 30 分钟。我建议用-j参数指定并行度make CONFIGGemminiRocketConfig -j84.2 编译软件测试程序Gemmini 的软件栈在generators/gemmini/software下里面有 baremetal 的测试程序。最经典的是gemmini-test它会跑一个矩阵乘法并验证结果。编译的方式是cd generators/gemmini/software/gemmini-rocc-tests ./build.sh这个脚本会调用 RISC-V 工具链把 C 代码编译成 baremetal 的 ELF。编译完成后ELF 文件在build/目录下。提示如果你改了 Gemmini 的硬件参数软件侧的gemmini_params.h也要同步更新否则会出现软硬件不匹配的问题。这个头文件在software/gemmini-rocc-tests/include下。4.3 运行仿真与结果验证回到sims/verilator目录运行make CONFIGGemminiRocketConfig run-baremetal \ LOAD../../generators/gemmini/software/gemmini-rocc-tests/build/baremetal/gemmini-test仿真启动后你会看到一堆日志输出。如果一切正常最后会打印PASS或者类似的成功信息。我第一次跑的时候卡在Loading阶段好久后来发现是 ELF 路径写错了大家注意路径要对。仿真时间取决于程序复杂度简单的矩阵乘法大概几分钟复杂的卷积网络可能要半小时以上。可以用verbose参数看详细日志但会拖慢速度。4.4 用 Spike 做快速功能验证如果你只是想验证功能正确性不想等 verilator 慢慢跑可以用 Spikecd sims/spike make CONFIGGemminiRocketConfig run-baremetal \ LOAD../../generators/gemmini/software/gemmini-rocc-tests/build/baremetal/gemmini-testSpike 是指令级模拟器速度快很多但不带时序信息。我的习惯是先用 Spike 验证功能再用 verilator 看时序和性能。5. 参数调优与性能分析5.1 阵列规模对吞吐的影响我做过一组对比实验固定其他参数只改tileRows和tileColumns跑同一个矩阵乘法记录周期数阵列规模周期数相对吞吐8x8120001.0x16x1642002.86x32x3218006.67x看起来 32x32 很香但别忘了面积。在 FPGA 上32x32 的 DSP 占用是 16x16 的四倍而且布线拥塞会导致频率下降。实际有效吞吐可能只有理论值的 60% 到 70%。所以我的建议是先确定你的 FPGA 资源上限再反推阵列规模。5.2 暂存器容量与数据复用暂存器容量决定了你能把多大的权重块留在片上。如果权重块超过暂存器容量就得反复从 DRAM 搬带宽立刻成瓶颈。我一般会算一下模型的最大层权重权重字节数 输出通道 × 输入通道 × 卷积核尺寸 × 数据位宽 / 8比如一个 256x256x3x3 的卷积层INT8 下权重是 256×256×9 589824 字节约 576KB。默认的 256KB 暂存器装不下就得考虑分块或者加容量。5.3 DMA 配置与带宽匹配dma_maxbytes默认是 64 字节这个值偏保守。如果你的 DRAM 带宽充足可以加到 128 或 256。但要注意DMA 的突发长度不能超过总线位宽支持的范围。我试过设成 512结果仿真直接挂掉后来查手册发现总线最大只支持 256 字节突发。6. 常见问题与排查技巧实录6.1 编译阶段报错问题一verilator 编译报undefined reference这个多半是子模块没拉全或者工具链版本不对。先检查generators/gemmini是否完整再确认riscv-tools是否构建成功。我遇到过一次是riscv-tools构建到一半断了重新跑build-setup.sh就好了。问题二Scala 编译报not found: type Gemmini说明 Chipyard 的 config 里没有正确引用 Gemmini。检查build.sbt里有没有把 Gemmini 加进去或者你的 config 文件有没有 import 对应的包。6.2 仿真阶段报错问题三仿真卡在Loading不动八成是 ELF 路径错了或者 ELF 本身有问题。用riscv64-unknown-elf-objdump反汇编一下确认指令正常。另外检查LOAD参数是不是绝对路径相对路径有时候会出问题。问题四仿真跑出FAIL但没报错这种最头疼。通常是软硬件参数不匹配比如硬件是 16x16 阵列软件头文件里写的是 8x8。仔细核对gemmini_params.h和 Scala config 里的参数一个都不能差。6.3 性能不达预期问题五吞吐远低于理论值先看 DMA 是不是瓶颈。用verbose打印 DMA 事务日志如果发现大量小事务说明dma_maxbytes设小了。再看暂存器 bank 冲突如果访问模式不好bank 冲突会严重拖慢速度。问题六频率上不去综合后的时序报告里看关键路径。Gemmini 的关键路径通常在阵列的累加链上。如果时序不满足可以试着插入流水线寄存器或者降低阵列规模。6.4 常见问题速查表现象可能原因解决方法verilator 编译失败子模块缺失git submodule update --init --recursive仿真卡在 LoadingELF 路径错误检查 LOAD 参数用绝对路径结果 FAIL软硬件参数不匹配核对 gemmini_params.h 与 Scala config吞吐低DMA 瓶颈增大 dma_maxbytes检查 bank 冲突频率低关键路径过长插入流水线寄存器减小阵列综合时间过长阵列规模过大降到 16x16 或 8x87. 软件栈与 ONNX 模型部署7.1 Gemmini 软件栈概览Gemmini 的软件栈分几层最底层是 RoCC 指令封装往上是gemmini.h提供的 API再往上是 ONNX 编译器。RoCC 指令是 Gemmini 和 CPU 之间的接口包括mvin、mvout、compute等。这些指令通过rocc.h里的宏来调用。gemmini.h里封装了更友好的函数比如gemmini_mvin、gemmini_mvout、gemmini_compute_preloaded。我建议先看gemmini-rocc-tests里的例子把基本流程跑通再去看 ONNX 编译器。7.2 ONNX 到 C 的编译流程Gemmini 提供了一个基于 TVM 的 ONNX 编译器在software/onnx下。流程是ONNX 模型 → TVM Relay → Gemmini 后端 → C 代码。编译命令大概是python3 compile.py --model model.onnx --output model.c生成的 C 代码里包含了权重数据和调用 Gemmini API 的逻辑。你可以把它集成到 baremetal 程序里或者放到 Linux 用户态跑。注意ONNX 编译器对算子支持有限主要覆盖卷积、全连接、池化、ReLU 等常见算子。如果你的模型里有自定义算子得自己写映射。7.3 权重布局与数据排布Gemmini 对权重布局有要求通常是按 tile 分块存储。ONNX 编译器会自动做这个转换但如果你手写 C 代码就得自己处理。权重排布不对结果会完全错乱。我的经验是先用小模型验证比如一个 3x3 卷积确认布局正确后再上大模型。8. 从仿真到 FPGA 的落地经验8.1 FPGA 平台选择与资源评估Gemmini 可以综合到 Xilinx 的 FPGA 上官方支持的是 VCU118 和 ZCU102。如果你手头没有这些板子也可以用其他大容量的 Xilinx 卡但要注意 DSP 和 BRAM 的数量。16x16 阵列大概需要 256 个 DSP加上其他逻辑总共 500 个 DSP 左右。BRAM 方面256KB 暂存器大概占 100 个 BRAM 块。8.2 综合与实现流程综合用 Vivado流程是生成 Verilog → 创建 Vivado 工程 → 综合 → 实现 → 生成比特流。Chipyard 提供了一些脚本辅助这个过程但我觉得手动建工程更可控。关键是在综合选项里把-flatten_hierarchy设成none否则 Gemmini 的层次会被打平时序很难收敛。8.3 上板调试与性能实测上板后先用简单的矩阵乘法测试功能。如果结果不对用 ILA 抓波形重点看 DMA 的握手信号和阵列的使能信号。性能实测方面我一般会跑一个 ResNet-50 的某一层对比仿真和上板的周期数。差异在 10% 以内算正常超过的话就要查时序违例或者 DDR 带宽瓶颈。9. 我踩过的那些坑与独家建议第一个坑是环境变量污染。我有一次同时装了官方工具链和系统自带的 RISC-V 工具链env.shsource 之后which riscv64-unknown-elf-gcc指向了错误的版本编译出来的 ELF 跑不了。后来我在env.sh里显式设置了RISCV路径并且在.bashrc里加了检查才避免再犯。第二个坑是暂存器容量算错。我一开始按字节算结果忘了 Gemmini 的暂存器是按 32 位字编址的容量参数单位是字不是字节。改过来之后才对上。第三个坑是仿真时间预估不足。verilator 跑大模型的时候一个仿真可能要几个小时。我后来养成了习惯先用 Spike 跑功能再用 verilator 跑小规模性能测试最后才上大模型。这样能省很多等待时间。最后一个建议多逛 Gemmini 的 GitHub issue 和 Chipyard 的讨论区。很多问题别人已经踩过了搜一下就能找到答案。我遇到的一个 DMA 死锁问题就是在 issue 里找到的解决方案原来是dma_maxbytes和总线位宽不匹配导致的。这个框架后续还可以往多核方向扩展比如多个 Gemmini 实例挂到不同的核上做模型并行。也可以研究一下稀疏加速Gemmini 本身支持一定的稀疏性但需要软件侧配合。这些我还在摸索中等有成熟经验了再分享。
返回列表