【AI】LLM的硬件视角 -- 分块
在Prefill阶段,即便是处理一个Prompt,其产生的 Q 、 K 、 V 矩阵(例如 seq_len x d_model )也常常因为太大而无法一次性放入片上内存(SRAM),更不用说一次性被计算单元处理了。因此, 分块(Tiling / Blocking) 是解决这个问题的核心技术。我们来详细拆解一下“一个大矩阵如何被拆分”以及“指令集该如何设计”。
在Prefill阶段,即便是处理一个Prompt,其产生的 Q 、 K 、 V 矩阵(例如 seq_len x d_model )也常常因为太大而无法一次性放入片上内存(SRAM),更不用说一次性被计算单元处理了。
因此, 分块(Tiling / Blocking) 是解决这个问题的核心技术。我们来详细拆解一下“一个大矩阵如何被拆分”以及“指令集该如何设计”。
1. 核心思想:分块 (Tiling) 以匹配硬件能力
根本原因在于存储层次结构:
- HBM (片外) : 容量大 (GBs),但访问延迟高,带宽相对有限。
- SRAM (片上) : 容量小 (KBs 或 MBs),但访问延迟极低,带宽极高。
- 计算单元 (MAC Array) : 拥有海量乘加器,渴望被持续“喂饱”数据。
目标是: 最大化数据在SRAM中的复用,最小化对HBM的访问次数。
分块就是实现这一目标的手段。它将一个不适合硬件直接处理的大问题(大矩阵乘法),分解为一系列硬件可以高效处理的小问题(小矩阵乘法)。
2. 大矩阵如何拆分?——以一个MMA指令为例
我们假设你的AI芯片设计了一个核心指令,叫做 MMA.16x16x16.FP16 (Matrix-Multiply-Accumulate)。
- 功能 : 这个指令可以读取两个 16x16 的FP16矩阵,执行矩阵乘法,并将结果累加到一个 16x16 的结果矩阵上。
- 硬件对应 : 这个指令通常由一个 16x16 的脉动阵列(Systolic Array)或其他形式的MAC阵列来执行。执行该指令所需的数据(两个输入矩阵和一个累加矩阵)都必须提前加载到SRAM的寄存器或特定区域。
现在,我们要计算一个大的矩阵乘法 C(M, N) = A(M, K) * B(K, N) 。假设 M, N, K 都是 1024 。
这个 1024x1024 的大矩阵会被编译器(或硬件调度器)自动拆分为 16x16 的小块(Tiles)。
- 矩阵 A 被看作是 (1024/16) x (1024/16) = 64x64 个小块的网格。
- 矩阵 B 和 C 同理。
计算流程(以计算 C 的一个 16x16 小块 C_ij 为例):
为了计算出 C 矩阵中位于 (i, j) 位置的 16x16 小块 C_ij ,我们需要:
- A 矩阵的第 i 行所有小块 ( A_i0, A_i1, …, A_i63 )。
- B 矩阵的第 j 列所有小块 ( B_0j, B_1j, …, B_63j )。
计算公式为: C_ij = Σ (A_ik * B_kj) ,其中 k 从 0 到 63 。
硬件执行的伪代码与数据流:
// 1. 在SRAM中为C的一个小块分配累加器,并清零
accumulator_tile[16][16] = 0;
// 2. 沿着K维度进行循环,这是数据复用的关键
for k in 0 to 63:
// 3. 从HBM加载一个小块到SRAM
load A_ik from HBM into SRAM_A_tile;
load B_kj from HBM into SRAM_B_tile;
// 4. 执行核心指令,硬件在内部完成256次乘加
// MMA.16x16x16 accumulator_tile, SRAM_A_tile, SRAM_B_tile
// 这条指令会计算 SRAM_A_tile * SRAM_B_tile,并将结果累加到 accumulator_tile
accumulator_tile += SRAM_A_tile * SRAM_B_tile;
// 5. 所有K维度的块都计算完毕后,将最终结果写回HBM
write accumulator_tile to C_ij in HBM;
这个过程清晰地展示了:一个巨大的矩阵乘法被分解成了一系列由 load-compute-accumulate 组成的循环,而循环的核心就是那条硬件可以直接执行的 MMA 指令。
3. 指令集应该如何设计?
针对这种应用,指令集的设计通常有几个层次,体现了不同的设计哲学和取舍:
层次一:低级指令 (Scalar/Vector FMA)
- 指令示例 : FMA.FP16 f1, f2, f3 (Fused Multiply-Add, f1 = f2 * f3 + f1 )。
- 设计思想 : 提供最基础的计算原语。由编译器负责将矩阵乘法分解成成千上万条这样的指令,并手动管理数据加载、寄存器分配和循环。
- 优点 : 灵活性最高,可以用于任何计算。
- 缺点 :
- 指令开销巨大 : 控制逻辑复杂,指令译码和分发成为瓶颈。
- 编译器极度复杂 : 优化难度极高,难以充分利用硬件。
- 数据流不清晰 : 硬件难以根据指令预测未来的数据需求,无法做好数据预取。 层次二:中级指令 (Matrix MMA) - 当前主流
- 指令示例 : MMA.16x16x16.FP16 Rd, Ra, Rb, Rc ( Rd = Ra * Rb + Rc )。
- 设计思想 : 将最常用、最核心的计算模式——小矩阵乘法累加——固化为一条指令。这正是我们上面例子中使用的指令。
- 优点 :
- 高效 : 一条指令触发一个高度优化的硬件模块(如脉动阵列)执行成百上千次运算,极大降低了指令开销。
- 编译器友好 : 编译器的目标从生成FMA指令变为生成 MMA 指令,并优化 load/store ,任务大大简化。
- 硬件优化 : 硬件可以为这条指令做深度优化,例如设计专门的数据通路、流水线和预取机制。
- 缺点 : 灵活性降低。硬件为 16x16 优化,如果遇到 17x17 的计算就需要编译器拆解,效率会降低。 层次三:高级指令 (GEMM Dispatch)
- 指令示例 : GEMM M, N, K, ptr_A, ptr_B, ptr_C 。
- 设计思想 : 将整个通用矩阵乘法(GEMM)作为一个任务交给硬件。指令只提供矩阵的维度和在HBM中的地址指针。
- 优点 :
- 极度简化 : CPU或主控制器只需发一条指令,硬件(一个专门的GEMM协处理器)会处理所有分块、数据加载、计算和写回。
- 完全卸载 : CPU可以去做其他任务。
- 缺点 :
- 灵活性最差 : 硬件被固化成一个GEMM“黑盒”。如果想在矩阵乘法中间插入一些非线性的操作(如ReLU),就很难实现。这种操作被称为“算子融合”(Operator Fusion),对于提升性能至关重要。
对芯片设计的启示
- 指令是软硬件的契约 : MMA 这样的中级指令是现代AI芯片设计的甜点。它为编译器提供了足够清晰的优化目标,同时又给了硬件足够的空间去进行内部优化。
- 数据流是核心 : 芯片设计不仅仅是设计计算单元,更重要的是设计数据在HBM、SRAM、计算单元之间流动的方式(Dataflow)。脉动阵列(Systolic Array)之所以高效,就是因为它是一种数据流驱动的架构,最大化了每个从SRAM中读取的数据的计算次数。
- 编译器与硬件协同设计 : 必须有一个能理解 MMA 指令、SRAM大小、HBM带宽的编译器。编译器需要智能地选择最优的分块大小(不一定总是 16x16 ,可能是它的倍数),并生成最优的 load/store 指令序列,以确保计算单元始终有数据可用,避免“饿死”。
总而言之,通过 分块 和设计类似 MMA 的 矩阵指令 ,硬件可以用有限的片上资源,高效地完成看似无限大的矩阵运算任务,而这正是LLM推理芯片设计的精髓所在。
更多推荐


所有评论(0)