最近在学习Gemmini,记录了一些笔记,分享出来。因此本系列博客是涵盖了Gemmini相关内容的一系列笔记,包括:Chipyard、Gemmini、buddy-mlir等。
本系列其他博客:
- 【Gemmini系列】【一】Chipyard文档(版本“HEAD”)
- 【Gemmini系列】【二】Chipyard SoC开发框架及其在先进研究中的应用
- 【Gemmini系列】【三】对Gemmini脉动阵列加速器的架构深度解析
- 【Gemmini系列】【四】在Chipyard生态系统中用Buddy-MLIR为Gemmini加速器进行编译指南
在Chipyard生态系统中用Buddy-MLIR为Gemmini加速器进行编译指南
引言:敏捷SoC设计与现代编译器基础设施的融合
当代计算领域正面临一个根本性的转折点。一方面,摩尔定律的经济和物理效益逐渐减弱,依赖通用处理器性能提升的传统模式已难以为继。另一方面,深度学习等计算密集型应用的需求呈爆炸式增长,对算力提出了前所未有的要求。这一矛盾催生了计算架构的范式转变:从通用计算转向专用计算,领域特定加速器(Domain-Specific Accelerators, DSAs)应运而生。
然而,设计高效的DSA硬件只是挑战的一部分,如何为其高效、便捷地编程,是更为严峻的难题。高级机器学习框架(如PyTorch、TensorFlow)在抽象的计算图和张量层面进行操作,而定制硬件的底层指令集架构(ISA)则极为具体和低级,两者之间存在巨大的“软件-硬件鸿沟”。弥合这一鸿沟,是释放DSA全部潜能的关键。
为应对这一挑战,学术界和工业界涌现出了一系列开创性的开源技术,它们共同构成了一个面向未来的硬件-软件协同设计生态系统。本报告将聚焦于其中的三个核心支柱:
- Chipyard:一个用于敏捷开发基于RISC-V的片上系统(SoC)的开源框架。它能够从高级描述中快速生成和仿真复杂的异构系统,极大地缩短了硬件迭代周期。
- Gemmini:一个高度可配置的、基于脉动阵列的矩阵乘法加速器生成器。它被设计为RISC-V定制协处理器(RoCC),可无缝集成到Chipyard生成的SoC中,为深度学习工作负载提供核心算力。
buddy-mlir
:一个构建于MLIR(多级中间表示)基础设施之上的编译器框架。其明确目标是搭建从领域特定语言(DSLs)到领域特定架构(DSAs)的桥梁,为硬件-软件协同设计创建一个统一的生态系统。
本报告旨在阐述如何利用 buddy-mlir
工具链,将高级神经网络操作编译到Chipyard框架内的Gemmini加速器上。这一集成化的工作流程代表了一种全新的加速器编程范式,它为设计空间探索和性能优化带来了前所未有的敏捷性,是实现真正意义上的硬件-软件协同设计的关键途径。
第一部分:基础技术架构深度解析
在深入探讨具体的部署流程之前,必须首先对构成该生态系统的三大核心技术——Chipyard、Gemmini和buddy-mlir
——的架构原理和设计哲学进行深度解析。理解它们各自的功能、相互之间的关系以及共同构成的协同设计体系,是成功应用该技术栈的前提。
1.1 Chipyard生态系统:一个用于可组合SoC的框架
Chipyard并非一个单一的工具,而是一个集成了众多开源工具和RTL生成器的综合性框架,其核心是实现敏捷的SoC开发。
核心原则:Chipyard的基石是基于生成器的方法论。它广泛采用由伯克利大学开发的Chisel硬件描述语言(HDL)及其高级中间表示FIRRTL。开发者使用高级的、面向对象的语言Scala来描述硬件参数和结构,Chisel编译器则将这些描述转换成低级的、可综合的Verilog代码。这种方法使得硬件设计高度参数化,开发者可以通过修改几行Scala代码,就能生成架构截然不同的SoC,例如改变核心数量、缓存大小或总线宽度。
关键组件:Chipyard框架整合了一个丰富的、经过验证的开源IP核库。这包括多种RISC-V处理器核心(如顺序执行的Rocket Core和乱序执行的BOOM Core)、支持缓存一致性的多级内存系统,以及一系列标准外设(如UART、SPI等)。这些组件通过开放的TileLink互联协议连接在一起,形成一个完整的SoC。这种可组合性使得研究人员可以像搭积木一样,快速构建出满足特定需求的定制化SoC。
集成的仿真与VLSI流程:Chipyard的强大之处不仅在于硬件生成,更在于其提供了一个从设计、验证到物理实现的端到端统一环境。它支持多种验证方法学,包括:
- 软件RTL仿真:使用开源的Verilator工具,将生成的Verilog代码编译成C++模型,进行周期精确的软件仿真。
- FPGA加速仿真:通过集成的FireSim平台,可以将SoC设计部署到云端(如AWS F1实例)的FPGA上,实现比软件仿真快几个数量级的仿真速度。
- 自动化VLSI流程:借助Hammer工具,Chipyard能够将SoC设计自动化地推向物理实现,生成用于芯片制造的GDSII版图文件。
这一集成的流程形成了一个紧密的“设计-验证-实现”闭环,完美契合了敏捷开发的理念。
有关Chipyard的详细介绍,请查看:【Gemmini系列】【二】Chipyard SoC开发框架及其在先进研究中的应用
1.2 Gemmini:一个高度参数化的脉动阵列加速器
Gemmini是Chipyard生态系统中的一个明星IP,它是一个专门为深度学习中的矩阵乘法运算而设计的DSA生成器。
架构剖析:Gemmini在SoC中被实例化为一个RoCC(Rocket Custom Co-processor)加速器。它通过RoCC端口与一个RISC-V核心(如Rocket或BOOM)紧密耦合,并利用该核心执行其定制的非标准RISC-V指令。数据通路方面,Gemmini默认通过系统总线(System Bus)直接与L2缓存相连,以获取高带宽的内存访问。其核心计算单元是一个脉动阵列,该阵列具有两级层次结构:阵列由多个“Tile”网格化构成,Tile之间有流水线寄存器,而每个Tile内部则是纯组合逻辑。除了计算单元,Gemmini还包含专用的片上存储器(Scratchpad Memory)用于暂存输入和权重,一个累加器存储器(Accumulator Memory)用于存放部分和,以及一个高效的DMA引擎,负责在主存和其本地存储之间搬运数据。
参数化的力量:Gemmini的真正威力在于其在硬件生成时的高度可配置性。开发者可以在SoC的配置文件中,通过修改一系列参数来定制Gemmini的微架构,从而探索不同硬件设计对性能的影响。这些关键参数包括:
- 脉动阵列维度:通过
tileRows
、tileColumns
、meshRows
和meshColumns
参数,可以精确定义脉动阵列的大小和形状。 - 数据流:
dataflow
参数决定了脉动阵列采用“输出固定”(Output-Stationary)还是“权重固定”(Weight-Stationary)的数据流,甚至可以在运行时动态选择。 - 存储器参数:
sp_capacity
和acc_capacity
定义了暂存存储器和累加器存储器的大小(以KiB为单位),而sp_banks
则决定了暂存存储器被划分成的Bank数量,影响其并发访问能力。 - 数据类型:
inputType
、outputType
和accType
分别定义了输入数据、PE间传递数据以及累加器中使用的数据类型。例如,可以配置为8位定点输入和32位整数累加,以支持量化神经网络。 - 存取-执行解耦队列:通过
ld_queue_length
、st_queue_length
和ex_queue_length
等参数,可以调整指令队列的深度,以隐藏内存访问延迟。
基线编程模型:要直接为Gemmini编程,最基本的方式是通过其提供的C语言头文件gemmini.h
。这个库将底层的、二进制编码复杂的Gemmini定制指令封装成了一系列C函数,如矩阵乘法、卷积等。这种方式是硬件开发和底层测试的基石,也是后续与buddy-mlir
编译器方法进行对比的重要参照点。
有关Chipyard的详细介绍,请查看:【Gemmini系列】【三】对Gemmini脉动阵列加速器的架构深度解析
1.3 Buddy-MLIR:一个面向硬件-软件协同设计的编译器框架
如果说Chipyard和Gemmini解决了“如何敏捷地构建专用硬件”的问题,那么buddy-mlir
则致力于解决“如何高效地为这些专用硬件编程”的难题。
MLIR范式:MLIR(Multi-Level Intermediate Representation)是LLVM项目孵化的下一代编译器基础设施。它提供了一个统一、可扩展的框架,用于在多个抽象层次上表示代码。其核心思想是“方言”(Dialect)和“渐进式下降”(Progressive Lowering)。一个方言是一组自定义的操作(Operations)、类型(Types)和属性(Attributes),用于表示特定领域的语义。编译过程就是一个将程序从高级、领域特定的方言,通过一系列变换,逐步下降到底层、接近硬件的方言的过程。
buddy-mlir
的使命:buddy-mlir
项目正是利用MLIR的强大能力,来专门应对从DSL到DSA的编译挑战。它旨在基于MLIR的模块化和RISC-V ISA的可扩展性,创建一个紧密的协同设计生态系统,从而解锁更多的软硬件优化机会。
工具链组成:buddy-mlir
框架提供了一套关键工具,这些工具将在后续的部署流程中被频繁使用:
buddy-opt
:这是应用buddy-mlir
中定义的优化和下降Pass(编译过程中的变换步骤)的主要驱动程序。buddy-translate
:一个用于将MLIR表示转换成其他表示(最典型的是LLVM IR)的工具。buddy-llc
:LLVM静态编译器,用于从LLVM IR生成目标机器的目标文件(object code)。
此外,该框架为不同领域定义了特定的方言,例如用于数字图像处理的dip
方言,以及本报告关注的核心——用于与Gemmini硬件交互的gemmini
方言。
1.4 敏捷性、专用化与抽象化的三位一体
将这三种技术放在一起分析,可以发现它们并非简单的工具堆砌,而是形成了一个共生的“三位一体”结构,分别代表了现代系统设计的三个关键支柱:敏捷的硬件生成(Chipyard)、领域专用的性能(Gemmini)和高级的编程抽象(buddy-mlir
)。
这一关系的逻辑链条如下:
-
用户的最终目标是在Gemmini上运行程序。然而,Gemmini并非一个固定的现成硬件,它是一个生成器。它的具体形态(如阵列大小、数据类型)依赖于像Chipyard这样的框架,通过配置将其实例化为一个具体的SoC设计。这就建立了Chipyard与Gemmini之间密不可分的联系。
-
直接通过Gemmini的定制指令集或其C语言库(
gemmini.h
)进行编程是可行的,但这种方式非常脆弱,且与高级的机器学习框架完全脱节。为每个不同的Gemmini配置手写优化代码是不现实的,这形成了一个巨大的“可编程性鸿沟”。 -
buddy-mlir
的诞生正是为了弥合这一鸿沟。它提供了必要的编译器基础设施,能够将高级、通用的表示(如linalg
方言)自动地、系统地转换成Gemmini硬件能够理解的特定操作序列(gemmini
方言)。这就建立了Gemmini与buddy-mlir
之间的关键链接。
因此,完整的端到端工作流程不仅仅是一系列步骤的串联,它体现了一种先进的设计哲学:使用Chipyard进行敏捷的硬件原型设计,使用Gemmini获得领域专用的加速性能,并使用buddy-mlir
使这种专用硬件能够从高层次抽象进行访问和编程。这个三位一体的结构,使得在硬件和软件之间进行快速迭代和联合优化成为可能,从而找到系统级的最优解,这正是硬件-软件协同设计的精髓所在。
第二部分:编译器-加速器接口:buddy-mlir
到Gemmini的下降流水线
要理解buddy-mlir
如何为Gemmini工作,核心在于理解其编译流程,即一个高级的、与硬件无关的计算描述是如何被逐步转换成Gemmini加速器可以执行的低级指令序列的。这个过程被称为“下降”(Lowering),是MLIR框架的精髓。
2.1 原理阐述:基于编译器的抽象 vs. 基于库的抽象
在为Gemmini这样的DSA编程时,存在两种主流的抽象方法:基于库(Library-based)和基于编译器(Compiler-based)。
基于库的方法 (gemmini.h
):这种方法通过提供一个C语言函数库来封装底层的硬件指令。开发者直接调用这些库函数(如gemmini_matmul
)来控制硬件。
- 优点:它提供了对硬件直接、可预测的控制。对于编写高度优化的手工内核、硬件功能验证和初期测试来说,这种方法非常有效。
- 缺点:它的问题也同样突出。首先是可移植性差:代码与特定的Gemmini硬件配置(如阵列维度、存储器大小)紧密耦合,一旦硬件参数改变,软件代码可能需要重写。其次是可组合性差:很难将这些低级库函数与大型、复杂的软件系统(如一个完整的AI框架)平滑地集成。最后,它需要开发者手动将高级模型中的操作映射到库函数调用,这是一个繁琐且容易出错的过程。
基于编译器的方法 (buddy-mlir
):这种方法引入了一个更高级的抽象层。开发者使用一种通用的、与硬件无关的方言(如MLIR的linalg
方言)来描述计算任务。编译器则负责将这些高级描述自动地映射到具体的Gemmini硬件上 。
- 优点:可移植性强:同一份
linalg
代码,理论上可以被重新编译以适应不同配置的Gemmini,甚至其他完全不同的硬件后端。可组合性好:高级方言可以更容易地与现有软件生态集成。最重要的是,它开启了自动化优化的大门:编译器可以在下降过程中应用复杂的变换,如循环分块、数据布局优化等,而这些对上层开发者是透明的。 - 缺点:其代价是编译器基础设施本身的复杂性。构建和维护一个能够正确、高效地执行这种下降的编译器是一项巨大的工程挑战。
buddy-mlir
选择的是第二条道路,它相信通过构建强大的编译器基础设施,可以从根本上解决DSA的可编程性难题。
2.2 gemmini
方言:通往加速器的语义桥梁
gemmini
方言是buddy-mlir
与Gemmini硬件之间的语义桥梁,是整个集成工作的基石。它在编译器内部以一种结构化的方式,对Gemmini加速器的功能和指令集进行了建模。这个方言的定义位于buddy-mlir
源码树的midend/include/Dialect/Gemmini/Gemmini.td
文件中,使用MLIR的TableGen工具进行描述。
该方言主要包含两类操作:
- 高级操作(High-Level Operations):这些操作对应于Gemmini硬件的复杂行为,通常需要多个时钟周期和内部状态机来完成。它们是上层方言(如
linalg
)下降的主要目标。gemmini.tile_matmul
:表示一个分块矩阵乘法操作,封装了在脉动阵列上执行核心计算的逻辑 。gemmini.tile_conv
:表示一个分块卷积操作,同样映射到脉动阵列的计算能力上。
- 低级控制与数据移动操作(Low-Level Control & Data Movement Operations):也称为固有操作,这些操作更直接地映射到Gemmini的单条或几条定制指令,用于配置加速器和搬运数据。
gemmini.mvin
/gemmini.mvin2
/gemmini.mvin3
:对应于将数据从主存移动到Gemmini的暂存存储器的DMA操作。gemmini.mvout
:对应于将数据从累加器存储器移出到主存的DMA操作。gemmini.config_ld
/gemmini.config_st
/gemmini.config_ex
:分别用于配置加载、存储和执行单元的指令。gemmini.flush
:用于清空流水线,确保之前的操作已完成。gemmini.preload
/gemmini.compute_preloaded
:用于实现预加载和计算的流水线操作。
通过这个方言,编译器获得了一种“语言”来描述它希望Gemmini硬件执行的任务。
2.3 渐进式下降路径:从linalg
到Gemmini ISA
这是buddy-mlir
的核心编译流程,是一个多阶段的转换过程,它将一个通用的线性代数操作,逐步细化,最终转换成一串Gemmini专用的指令编码。
第一阶段:从linalg方言到gemmini方言
编译流程的起点是MLIR中的标准方言,如linalg,它被广泛用于表示机器学习中的张量计算。buddy-mlir提供了一个名为-convert-linalg-to-gemmini的编译Pass。当这个Pass被应用时,它会识别出代码中的linalg.matmul等操作,并将其替换为硬件感知的gemmini.tile_matmul操作。这个过程不仅仅是名称的替换,它还可能涉及到数据布局的转换(例如,从NHWC到NCHW)和分块参数的计算。这一关键逻辑主要实现在buddy-mlir源码的midend/lib/Conversion/LowerLinalgToGemmini/LowerLinalgToGemmini.cpp文件中。
第二阶段:从gemmini方言到gemmini内部操作(Intrinsic Operations)
此时,程序中已经没有了通用的linalg操作,取而代之的是高级的gemmini操作。下一步是将其进一步分解。例如,一个gemmini.tile_matmul操作需要配置DMA地址、数据大小,然后触发DMA搬运数据,之后配置执行单元,最后触发计算。这个阶段由-lower-gemmini这个Pass完成,它将gemmini.tile_matmul这样的高级操作,分解为一连串更原语的gemmini内部操作(intrinsic ops),如gemmini.config_ld、gemmini.mvin、gemmini.config_ex和gemmini.compute_preloaded的序列。这个Pass的逻辑主要位于midend/lib/Dialect/Gemmini/Transforms/LegalizeForLLVMExport.cpp和midend/lib/Conversion/LowerGemmini/LowerGemminiPass.cpp。完成这一步后,所有高级的gemmini操作都被标记为“非法”(illegal),剩下的只有与硬件指令接近一一对应的内部操作。
第三阶段:从gemmini内部操作到LLVM IR
这是编译器后端的最后一步。buddy-mlir将这些gemmini内部操作翻译成LLVM IR。需要强调的是,这并非标准的LLVM IR。它使用了LLVM的内部函数(Intrinsic)机制来表示Gemmini的定制指令。例如,gemmini.compute_accumulated操作会被转换成一个名为llvm.riscv.gemmini.compute.accumulated的LLVM内部函数调用。这个翻译过程由TableGen生成的描述文件(GemminiConversions.inc)指导,并在midend/lib/Target/LLVMIR/Dialect/Gemmini/GemminiToLLVMIRTranslation.cpp中实现。最终,当这个带有自定义内部函数的LLVM IR被送入支持Gemmini扩展的RISC-V工具链(如GCC/LLVM后端)时,汇编器会识别这些特殊的内部函数,并将它们转换成正确的Gemmini指令的32位二进制编码 。
通过这三个阶段的渐进式下降,buddy-mlir
成功地将一个与硬件无关的高级数学概念,系统性地、自动化地转换成了为特定Gemmini配置量身定制的底层机器指令。
2.4 概念之间的转换关系
我们可以把这些概念想象成一个层层递进的“翻译”链条,每一环都比前一环更接近硬件。
-
MLIR (多级中间表示):
- 角色: 一个基础设施框架,像一个“乐高工具箱”。它本身不特指某一种语言,而是提供了一套规则和工具,让我们可以方便地创造出各种用于编译过程的“中间语言”。
- 特点: 它的核心优势是允许在同一个环境中存在并转换多种不同抽象级别的“方言”。
-
Gemmini 方言 (Gemmini Dialect):
- 角色: 使用 MLIR 工具箱创造出来的一套专属“乐高积木”,专门用来描述 Gemmini 加速器的各种操作。它是在 MLIR 框架内定义的一种特定语言。
- 包含: 它内部又分为两种抽象级别的操作:
- 高级操作 (e.g.,
gemmini.tile_matmul
): 表达一个完整的、有业务逻辑的硬件功能,比如“执行一次分块矩阵乘法”。它对上层(如 Linalg)友好。 - 固有操作 (Intrinsic Operation) (e.g.,
gemmini.ComputeAccumulated_IntrOp
): 表达一个更底层的、接近硬件指令的原子操作。它是连接到下一阶段 (LLVM IR) 的桥梁。
- 高级操作 (e.g.,
-
LLVM IR (LLVM 中间表示):
- 角色: 一个比 MLIR 更低级、更接近传统汇编的通用中间表示。它是 MLIR 编译流程的主要输出目标。LLVM 拥有一套非常成熟的、与具体硬件架构无关的优化流程。
-
固有指令 (Intrinsic Instruction):
- 角色: 在 LLVM IR 层面,用来表示那些标准指令集里没有的、目标硬件专属的特殊指令。它是一个“占位符”。例如,
llvm.riscv.compute.accumulated
就是 Gemmini 自定义指令在 LLVM IR 中的名字。
- 角色: 在 LLVM IR 层面,用来表示那些标准指令集里没有的、目标硬件专属的特殊指令。它是一个“占位符”。例如,
转换关系链如下:
MLIR 框架
┃
┗━━▶
Linalg 方言 (通用计算,如 linalg.matmul
)
┃
▿ (降阶 Lowering)
▼
┗━━▶
Gemmini 方言 (高级操作) (如 gemmini.tile_matmul
)
┃
▿ (降阶 Lowering)
▼
┗━━▶
Gemmini 方言 (固有操作) (如 gemmini.ComputeAccumulated_IntrOp
)
┃
▿ (翻译 Translation)
▼
┗━━▶
LLVM IR (包含固有指令) (如 llvm.riscv.compute.accumulated
)
┃
▿ (LLVM 后端处理)
▼
┗━━▶
RISC-V 汇编代码 (包含自定义指令)
┃
▿ (汇编/链接)
▼
┗━━▶
最终的二进制机器码
2.5 从高级语言到硬件的完整转换过程
下面我们以一个在 PyTorch 中定义的矩阵乘法为例,走一遍完整的旅程:
第 1 步:高级框架层
你在 Python 中写下 c = torch.matmul(a, b)
。PyTorch 将这个计算图捕获下来。
第 2 步:前端导入到 MLIR
Buddy-MLIR 的前端将 PyTorch 的计算图转换成 MLIR 中与硬件无关的通用计算方言,最经典的就是 linalg
方言。此时,代码在 MLIR 中表示为 linalg.matmul
。它只描述了“要做一个矩阵乘法”,不关心由谁来做、怎么做。
第 3 步:第一次降阶 (硬件目标选定)
编译流程中的 Pass (优化通道) 识别出 linalg.matmul
,并根据编译选项判断目标硬件是 Gemmini。于是,它将通用的 linalg.matmul
降阶为 Gemmini 方言中的高级操作 gemmini.tile_matmul
。
- 转换:
linalg.matmul
->gemmini.tile_matmul
- 意义:编译决策已经做出——“这个矩阵乘法,要用 Gemmini 的分块矩阵乘法功能来高效执行”。
第 4 步:第二次降阶 (硬件操作细化)
gemmini.tile_matmul
还是一个比较抽象的概念。LegalizeForLLVMExport
这个 Pass 会将它进一步降阶为一系列更底层的 Gemmini 固有操作。这相当于把“做一顿饭”这个高级指令,分解成“切菜”、“开火”、“炒菜”等具体步骤。
- 转换:
gemmini.tile_matmul
-> 一系列的gemmini.config_ld
,gemmini.mvin
,gemmini.compute_preloaded
,gemmini.mvout
等固有操作。 - 意义:此时,代码已经精确描述了 Gemmini 硬件执行该操作所需的完整指令序列和数据流。
第 5 步:翻译到 LLVM IR
MLIR 的任务基本完成。现在需要将 MLIR 的代码翻译成 LLVM IR。在这个阶段,每一个 Gemmini 固有操作都会被转换成 LLVM IR 中对应的固有指令。这个转换是基于 GemminiConversions.inc
这个映射文件,它像一个字典,告诉编译器 gemmini.ComputeAccumulated_IntrOp
对应 llvm.riscv.compute.accumulated
。
- 转换:
gemmini.ComputeAccumulated_IntrOp
->llvm.riscv.compute.accumulated
- 意义:代码进入了通用的 LLVM 世界,可以利用 LLVM 成熟的优化能力。
第 6 步:LLVM 后端处理
LLVM 后端接管工作,它会对 LLVM IR 进行优化(如寄存器分配),然后进入最关键的指令选择阶段。
第 7 步:生成机器码并执行
LLVM 后端将 llvm.riscv.compute.accumulated
这类固有指令,转换成 Gemmini 对应的二进制机器码(一条真正的 RISC-V 自定义指令)。最终,生成可执行文件。当 CPU 执行到这条指令时,它会通过 RoCC 接口将指令和数据发送给 Gemmini 加速器,由 Gemmini 完成计算。
2.6 LLVM后端如何对应到硬件?
这是一个非常关键的问题。LLVM 后端并不是一个“黑箱”,它能对应到具体硬件,核心在于它拥有一个关于目标硬件的详细描述。这个描述通常是通过一系列的 .td
(TableGen) 文件来定义的。
可以把 LLVM 后端想象成一个机器人,你想让它为你的新奇硬件(比如 Gemmini)生成代码,你必须给它一本详细的“硬件说明书”,这本说明书就是 .td
文件。
这个过程主要分为以下几步:
-
定义目标特性 (Target-Specific Definition):
在backend/include/llvm/IR/IntrinsicsRISCVBuddyExt.td
这类文件中,开发者会定义 Gemmini 的自定义指令。比如,他们会写明:- 固有指令名称:
int_riscv_compute_accumulated
- 它在LLVM IR中的表示:
llvm.riscv.compute.accumulated
- 它对应的汇编助记符:
compute.accumulated
- 它的二进制编码格式: 例如,它属于 R-type 指令,操作码 (opcode) 是
0b0001011
等。
- 固有指令名称:
-
指令选择 (Instruction Selection):
当 LLVM 后端的优化器处理到一条llvm.riscv.compute.accumulated
固有指令时,它会去查阅这本“硬件说明书”(.td
文件)。- 说明书告诉它:“哦,这个
llvm.riscv.compute.accumulated
并非一个虚拟操作,它在我的目标硬件(带 Gemmini 扩展的 RISC-V)上有一条真实存在的、可以直接映射的机器指令”。 - 于是,LLVM 就会选择这条真实的机器指令作为最终的输出。
- 说明书告诉它:“哦,这个
-
机器码发射 (Machine Code Emission):
在确定了要使用哪条机器指令后,LLVM 会根据.td
文件中定义的二进制编码格式,将这条指令(包括操作码、寄存器地址等)“翻译”成最终的二进制数字(0和1)。这些二进制码被写入目标文件,最终成为可执行程序的一部分。
总结一下:LLVM 后端通过 TableGen (.td
) 文件 这个机制,学习和理解了目标硬件的指令集、寄存器结构、ABI(应用二进制接口)等所有细节。正是这本为特定硬件量身定制的“说明书”,使得通用的 LLVM 框架能够精确地为成百上千种不同的硬件(包括像 Gemmini 这样的专用加速器)生成高效的、原生的机器码。
表 2.1: Linalg到Gemmini方言的关键映射关系
下表总结了从linalg
方言到gemmini
方言的核心转换关系,以帮助理解编译流程的第一步。
Linalg 操作 | 对应的 gemmini 方言操作 | 底层Gemmini硬件概念 | 关键 buddy-mlir Pass |
---|---|---|---|
linalg.matmul | gemmini.tile_matmul | 脉动阵列上的分块矩阵乘法 | -convert-linalg-to-gemmini |
linalg.conv_2d_nchw_fchw | gemmini.tile_conv | 脉动阵列上的分块卷积(im2col) | -convert-linalg-to-gemmini |
linalg.conv_2d_nhwc_hwcf | gemmini.tile_conv | 脉动阵列上的分块卷积(im2col) | -convert-linalg-to-gemmini |
linalg.batch_matmul | 软件循环 + gemmini.tile_matmul | 通过软件循环处理批处理维度,每次迭代调用脉动阵列进行矩阵乘法 | -convert-linalg-to-gemmini |
第三部分:端到端部署与执行:分步实施指南
本部分将提供一个详尽的、可复现的指南,指导用户从零开始搭建完整的开发与仿真环境,并最终编译和执行一个面向Gemmini的MLIR程序。
3.1 环境准备与系统依赖
成功部署该复杂工具链的第一步是确保开发环境的正确配置。
操作系统与系统软件包:强烈建议使用基于Linux的操作系统,如Ubuntu 20.04或更高版本。根据Chipyard官方文档,需要安装一系列基础开发包。在基于Ubuntu/Debian的系统上,可以通过以下命令安装:
sudo apt-get update
sudo apt-get install -y build-essential bison flex git curl
sudo apt-get install -y libgmp-dev libmpfr-dev libmpc-dev zlib1g-dev
sudo apt-get install -y default-jdk default-jre sbt texinfo gengetopt
sudo apt-get install -y libexpat1-dev libusb-1.0-0-dev libncurses5-dev cmake
sudo apt-get install -y python3 python3-pip patch diffstat texi2html subversion chrpath wget
sudo apt-get install -y gtk3-devel # for QEMU GUI
sudo apt-get install -y dtc # Device Tree Compiler
这些软件包为后续编译RISC-V工具链、Chisel项目以及其他依赖项提供了基础。
Conda环境设置:Chipyard框架强烈推荐使用Conda包管理器来处理其复杂的依赖关系,这可以创建一个隔离的环境,避免与系统级的工具链发生冲突。
-
安装Conda:
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh bash Miniconda3-latest-Linux-x86_64.s
安装后,重新打开终端或执行
source ~/.bashrc
使conda
命令生效。 -
配置Conda以加速求解:为了加快后续环境创建的速度,建议安装并使用
libmamba
作为依赖求解器。Bash
conda install -n base conda-libmamba-solver conda config --set solver libmamba
3.2 构建支持Gemmini的SoC及仿真器
现在,我们将进入Chipyard框架,生成包含Gemmini加速器的SoC,并为其构建仿真器。参考:Gemmini README
第一步:克隆并初始化Chipyard
git clone https://github.com/ucb-bar/chipyard.git
cd chipyard
./build-setup.sh
build-setup.sh
脚本是Chipyard环境设置的核心。它会自动执行以下任务:
- 初始化所有的git子模块(包括Rocket Chip、Gemmini、Spike等)。
- 使用Conda创建一个名为
chipyard
的独立环境。 - 在该环境中安装所有必要的RISC-V工具链(包括GCC、Binutils和Spike模拟器)以及其他依赖项,如Verilator。这个过程需要较长时间。
第二步:激活环境
在进行任何编译或仿真工作之前,必须激活Chipyard环境。
source./env.sh
这个命令会激活之前创建的chipyard
Conda环境,并设置一系列关键的环境变量,如PATH
、RISCV
和LD_LIBRARY_PATH
,确保后续的make
命令能够找到正确的编译器和库。每次打开新的终端会话进行Chipyard相关工作时,都需要执行此命令。
第三步:生成硬件并构建仿真器
Chipyard使用CONFIG变量来选择要生成的SoC配置。我们将使用预定义的GemminiRocketConfig,它包含一个Rocket核心和一个默认配置的Gemmini加速器。
-
构建Verilator周期精确仿真器:
cd sims/verilator make CONFIG=GemminiRocketConfig
这个命令会触发Chisel从Scala配置生成SoC的Verilog代码,然后Verilator会将这些Verilog代码编译成一个C++可执行文件,即周期精确的仿真器。该文件位于
sims/verilator/simulator-chipyard-GemminiRocketConfig
。 -
验证Spike功能仿真器:
build-setup.sh
脚本构建的RISC-V工具链中已经包含了Spike。Chipyard的流程确保了它在编译时已经启用了对Gemmini扩展的支持。Spike是一个功能仿真器,它不保证周期精确,但执行速度快,非常适合软件开发和功能验证。
3.3 构建buddy-mlir
编译器工具链
在拥有了硬件仿真环境后,下一步是构建能够为该硬件生成代码的编译器。参考:buddy-mlir README
第一步:克隆buddy-mlir
及其依赖
# 在Chipyard目录之外的另一个位置
git clone git@github.com:buddy-compiler/buddy-mlir.git
cd buddy-mlir
git submodule update --init
git submodule update --init
命令至关重要,它会克隆buddy-mlir
所依赖的特定版本的llvm-project
。使用正确的LLVM版本是成功编译的关键。
第二步:构建LLVM/MLIR
buddy-mlir需要一个预先构建好的LLVM和MLIR库。
cd buddy-mlir/llvm
mkdir build
cd build
# 使用cmake配置构建
cmake -G Ninja ../llvm \
-DLLVM_ENABLE_PROJECTS="mlir;clang" \
-DLLVM_TARGETS_TO_BUILD="host;RISCV" \
-DLLVM_ENABLE_ASSERTIONS=ON \
-DCMAKE_BUILD_TYPE=RELEASE
# 使用ninja进行编译
ninja check-mlir check-clang
这个配置确保了MLIR和Clang项目被构建,并且生成了RISC-V后端的代码。
第三步:构建buddy-mlir
现在,使用刚刚构建好的LLVM/MLIR来编译buddy-mlir本身。
cd /path/to/buddy-mlir # 返回buddy-mlir的根目录
mkdir build
cd build
# 使用cmake配置构建,并指定MLIR和LLVM的路径
cmake -G Ninja .. \
-DMLIR_DIR=/path/to/buddy-mlir/llvm-project/build/lib/cmake/mlir \
-DLLVM_DIR=/path/to/buddy-mlir/llvm-project/build/lib/cmake/llvm \
-DLLVM_ENABLE_ASSERTIONS=ON \
-DCMAKE_BUILD_TYPE=RELEASE
# 使用ninja进行编译
ninja check-buddy
编译成功后,buddy-opt
、buddy-translate
等核心工具将位于buddy-mlir/build/bin/
目录下。
第四部分:高级主题与协同设计探索
4.1 Gemmini编程模型的比较分析
buddy-mlir
的文档中揭示了三种与Gemmini交互的编程模型,它们代表了不同的抽象层次和开发权衡。
- 直接C库调用:这是最底层的方法。开发者在C/C++代码中包含
gemmini.h
,并直接调用如gemmini_config_ex
、gemmini_compute_preloaded
等函数。这些函数通过内联汇编或宏生成Gemmini的定制指令。- 优点:对硬件的控制粒度最细,性能可预测性最高,适合编写极致优化的内核和进行硬件验证。
- 缺点:开发效率极低,代码与硬件配置高度绑定,完全不可移植,且与上层AI框架脱节。
- MLIR生成的库与C++宿主程序:这是混合编程模型。开发者将计算密集型部分(如
matmul
)用MLIR编写,然后使用buddy-mlir
工具链将其编译成一个目标文件(.o
)。之后,一个C++主程序(宿主程序)链接这个目标文件,并像调用普通函数一样调用MLIR中定义的函数。- 优点:实现了计算核心与业务逻辑的分离。可以利用MLIR的优化能力,同时将生成的核心无缝集成到现有的C++项目中。这是一个非常实用的折衷方案。
- 缺点:需要处理MLIR与C++之间的接口(ABI)问题,并维护两套构建系统。
- 纯MLIR应用:这是最高级的抽象。整个应用程序,包括
main
函数,都用MLIR编写。如3.4节所示,整个程序通过buddy-mlir
和RISC-V工具链直接编译成一个独立的可执行文件。- 优点:提供了最统一的开发体验,整个程序的逻辑都在MLIR的语义下,便于进行全局优化和分析。可移植性最强。
- 缺点:对于涉及复杂控制流、文件I/O或系统交互的应用,用MLIR来表达可能比用C++更繁琐。
选择哪种模型取决于具体的应用场景。对于库开发者,模型2可能是最佳选择。对于希望探索端到端编译器优化的研究人员,模型3则更具吸引力。
4.2 使用buddy-benchmark
进行性能评估
定性分析之后,需要定量的数据来评估性能。buddy-benchmark
是Buddy Compiler项目提供的官方基准测试框架,它基于Google Benchmark构建,提供了一套可扩展的机制来评估不同后端上的性能。
配置与构建:要为Gemmini运行基准测试,需要在buddy-benchmark
的CMake配置中指定相关选项。
# 假设已在chipyard环境中 (source env.sh)
git clone git@github.com:buddy-compiler/buddy-benchmark.git
cd buddy-benchmark
git submodule update --init
mkdir build && cd build
cmake -G Ninja .. \
-DCMAKE_BUILD_TYPE=RELEASE \
-DGEMMINI_BENCHMARKS=ON \
-DBUDDY_MLIR_BUILD_DIR=/path/to/buddy-mlir/build/
ninja
-DGEMMINI_BENCHMARKS=ON
会启用与Gemmini相关的测试目标。CMake会自动查找Chipyard环境中的RISC-V工具链和Spike。
运行基准测试:编译完成后,可以在build/bin
目录下找到相应的测试可执行文件。例如,运行一个ResNet-101的基准测试:
cd /path/to/buddy-benchmark/build/bin
spike --extension=gemmini pk Gemmini-ResNet-101
buddy-benchmark项目正在发展中,未来将支持更复杂的性能指标,如在周期精确仿真器上运行并报告周期数。
4.3 未来方向与协同设计反馈回路
这个工具链的终极价值不在于仅仅运行一个固定的工作负载,而在于它所开启的硬件-软件协同设计反馈回路。这使得研究人员能够快速地探索设计空间,并用量化数据来指导决策。
这个反馈回路的流程如下:
- 硬件参数调整:研究人员在Chipyard的SoC配置文件(一个Scala文件)中修改Gemmini的某个参数。例如,将累加器存储器的容量
acc_capacity
加倍。 - 硬件与仿真器再生:在
chipyard/sims/verilator
目录下重新执行make CONFIG=...
。Chipyard的生成器会产生一个新的、具有更大累加器存储器的SoC Verilog描述,并编译生成一个新的仿真器。同时,一个至关重要的副产品也被更新了:gemmini_params.h
头文件。这个文件包含了新硬件的配置信息,供软件栈使用。 - 编译器策略调整:
buddy-mlir
的编译Pass可以(或被修改为)读取gemmini_params.h
中的信息,并据此调整其代码生成策略。例如,一个更大的累加器可能意味着编译器可以在-convert-linalg-to-gemmini
Pass中选择一个更大的分块大小(tiling size),从而减少对主存的访问次数,提升计算强度。 - 软件重编译与性能再评估:研究人员使用
buddy-mlir
重新编译他们的MLIR程序,生成新的可执行文件。 - 性能测量与分析:将新的可执行文件在新的仿真器上运行,并使用
buddy-benchmark
来获取新的性能数据(如执行周期、指令数等)。
通过比较修改前后的性能数据,研究人员可以量化地判断“将累加器容量加倍”这一硬件改动所带来的实际效益。这个从硬件修改到性能反馈的完整闭环可以在数小时内完成,而不是传统硬件设计流程中需要的数周或数月。这极大地加速了创新步伐。
Buddy Compiler团队在其开放项目列表中也指明了未来的发展方向,包括进一步完善Gemmini后端,构建一个针对Gemmini的即时(JIT)编译引擎,以及增强针对DSA的优化Pass等。这些都为社区贡献者和研究人员提供了广阔的探索空间。