Cris Cecka, Mike Rubbelke (NVIDIA GTC | March 21, 2024)
CUTLASS 是一个用于在各种规模和尺寸上进行密集计算的 CUDA C++ 模板库。
Blackwell 硬件新特性概览:
Blackwell Tensor Cores 相比 Hopper 实现了显著的性能和功能提升:
下表对比了 Hopper 和 Blackwell 在 Tensor Core 操作上的主要区别:
| 阶段 | Hopper | Blackwell |
|---|---|---|
| 操作数加载 | 从寄存器加载 | 从张量内存加载 |
| 累加 | 寄存器 | 张量内存 |
| 指令发布 | 等待组内指令完成 | 异步执行,与 Epilogue 重叠 |
| 指令完成 | 同步提交 | 同步提交 |
Blackwell 架构将单个 Tensor Core 指令的执行范围扩展到了 2 个流式多处理器(SM)。
一个 2x1 的合作线程数组(CTA)集群中的一对 CTA 会被链接起来,并跨越 2 个 SM 执行。
这对 CTA(CTA 0 和 CTA 1)协同工作,CTA 0 作为“领导者”,CTA 1 作为“链接者”,共同完成一次 MMA 操作。
Blackwell Tensor Core 硬件原生支持新的块缩放数据类型:MXFP8、MXFP6、MXFP4 和 MXINT8。
性能提升:
A 和 B 操作数的缩放因子矩阵需要从张量内存(TMEM)中获取。
下表总结了新的块缩放格式:
| 格式名称 | 数据格式 | 缩放格式 | 可用于 |
|---|---|---|---|
| MXFP8 | FP8 E5M2 或 E4M3 | FP32 或 None | A, B, C, D |
| MXFP6 | FP6 E3M2 | FP32 或 None | A, B, C, D |
| MXFP4 | FP4 E2M1 | FP32 或 None | A, B, C, D |
| MXINT8 | INT8 | FP32 | A, B |
TMEM 是每个 SM 上的内联内存,专用于 Tensor Core 的操作数输入和输出。
分配方式:
cuda::memcpy_async 显式地分配和管理 TMEM。专用性: TMEM 专用于 Tensor Core (TC) 操作,不支持 SMT(同步多线程)操作。
ldmatrix 和 stmatrix 指令与 TMEM 进行数据交换,类似于 Hopper 架构。这是 CUTLASS 在 Hopper 架构上使用的持久化调度方法。
Blackwell 架构引入了新的硬件功能来解决静态调度的问题。
- 机制: Blackwell 允许用户在 SM 上通过 cuda::cluster_arrive_relaxed::fetch_add 指令以编程方式获取新的线程块集群。
- CUTLASS 实现: CUTLASS 利用此功能实现了动态持久化调度器(Dynamic Persistent Scheduler)。
- 输出分块到 SM 的映射是完全动态的,取决于各个 SM 的执行进度。当一个 SM 完成其任务后,它会主动获取新的任务。
- 这是 CUTLASS 在 Blackwell 上的默认调度器。
Blackwell Tensor Cores:
Tensor Memory (TMEM):
新的调度能力:
本章介绍如何使用 CuTe 库来为 NVIDIA Blackwell 架构的新特性进行编程,主要内容包括:
CuTe 的 MMA 原子操作 (atom) 为使用 MMA (Matrix Multiply-Accumulate) 提供了 PTX (Parallel Thread Execution) 和元数据。
MMA_Op:一个 PTX tile
Make_Traits:MMA 的模板
MMA_Atom 的默认 DispatchHopper 与 Blackwell 的区别
tiled_mma 使用线程ID(tid)在 CTA(Cooperative Thread Array)内部进行分区。tiled_mma 使用 CTA ID(cta_id)在 CTAs 之间进行分区。用户选择一个 MMA_Op 来创建一个 TiledMMA。CuTe 为每个 M100 Tensor Core 指令都提供了相应的 TiledMMA。
下图展示了如何从一个 MMA_Op 构建 TiledMMA,它描述了 CTA 级别的计算分块。右侧的代码示例演示了如何用指定的 MMA_Atom 和操作数布局来创建一个 TiledMMA。
这是一个完整的通用矩阵乘法(GEMM)示例,展示了从数据布局到计算执行的完整流程。
首先,定义输入矩阵 A、B 和输出/累加矩阵 C 在全局内存(Global Memory)中的布局。
- 矩阵 A: (M, K)
- 矩阵 B: (K, N)
- 矩阵 C: (M, N)
使用 Tile 来创建全局内存张量的分区视图。在 Blackwell 架构中,我们使用 mma_tiler 而不是 Hopper 中常用的 cta_tiler。这个 tiler 定义了计算任务如何在 CTA 网格上进行划分。
根据 global_coord(而不是 cta_id)从分区视图中为每个 CTA 切分出对应的数据块。这一步确定了每个 CTA 需要处理的 A、B 和 C 矩阵的具体部分。
使用 TiledMma 对象来将整个 MMA 计算任务在不同的 CTA 之间进行划分。这决定了每个 CTA 内部线程块的计算范围和数据分工。
为 CTA 本地的数据(A 和 B 的分块)创建共享内存(Shared Memory, SMEM)布局,并分配相应的内存空间。数据将从全局内存加载到共享内存,以供 Tensor Core 高效访问。
为 MMA 操作创建寄存器级别的 "Fragments"。这些 Fragments 是 MMA 指令直接消耗的数据单元,代表了将从共享内存加载到寄存器的数据。
使用 cooperative_copy 将数据从全局内存(GMEM)异步拷贝到共享内存(SMEM)。这是一个协同操作,由 CTA 内的所有线程共同完成。
最后,执行 gemm 操作。数据从共享内存加载到寄存器(Fragments),然后由 Tensor Core 执行矩阵乘加运算,结果累加到 C 的 Fragments 中。
TMA 是用于高效数据传输的硬件单元。
CuTe 的 TMA 原子操作为使用 TMA 提供了 PTX 和元数据支持。
TMA_Op - PTX for TMA:
COPY: 基本拷贝COPY_MULTICAST: 组播拷贝IM2COL: Im2col 转换FILL: 填充操作TMA atom:
TMA OpTile 以获取 TMA Tiling 信息以下是 TMA_Op 和 Copy_Atom 的代码示例:
为了在 Blackwell 架构上利用张量内存加速器(TMA)进行通用矩阵乘法(GEMM),我们需要对现有的基于全局内存(GMEM)和共享内存(SMEM)的 GEMM 核函数进行修改。
第一步是修改全局内存张量的定义方式。标准的 cute::make_tensor 用于创建通用张量视图。为了让 TMA 能够识别和处理这些张量,我们需要使用一个专门的构造函数 cute::make_tma_tensor。此函数位于 cute/tensor_map.hpp 中,它会为张量附加必要的信息,以便 TMA 硬件能够正确地执行加载操作。
定义了 TMA 张量后,下一步是规划数据加载。
TMA partitioning)来确定每个 tile 需要一个或多个 TMA 操作来完成数据加载。这通过 make_tma_copy 和 partition_S 来实现。cute::TMA_Barrier 创建一个屏障对象,并在主循环中使用 tma_arrive_and_wait(tma_bar, ...) 等待 TMA 操作完成。通过这些修改,原有的 copy(gA, tAgA, ...) 操作被替换为 copy(tma_load_a, ...),并配合同步原语,从而将数据加载任务卸载到 TMA 硬件上。
Blackwell 架构引入了 .2sm 后缀的指令,允许单个 MMA(矩阵乘法累加)或 TMA 操作跨越两个流式多处理器(Streaming Multiprocessors, SMs)执行。这使得两个协作的 CTA(Cooperative Thread Array)可以共同处理一个更大的计算任务。
CuTe 通过其“原子操作”(Atoms)抽象来支持这些新的硬件特性。
mma.2sm。SmShape = Shape<_2,_1> 来实现。这表示操作将在两个 SM 组成的集群中执行,布局为 2 行 1 列。TMA.2SM:
tma.2sm。.2sm 版本的 TMA copy 操作需要额外的调度信息:
CtaSchd (cute::CtaSchd):用于在 SM 间进行分区。Multicast (cute::Multicast):一个掩码,用于确定到 SM 的映射。在 CuTe 中,通过向 make_tma_copy 函数传递这些额外的调度参数来构造一个 2-SM 的 TMA 操作。
为了在 GEMM 内核中启用 2-SM 操作,需要进行以下修改:
使用 blockIdx.z 进行 SM 映射:
CUDA grid 的 Z 维度(blockIdx.z)现在被用作 SM 集群内的索引。例如,对于一个 2-SM 操作,cta_id_z 为 0 的 CTA 运行在一个 SM 上,cta_id_z 为 1 的 CTA 运行在另一个协作的 SM 上。
Leader CTA:
集群中的一个 CTA(通常是 cta_id_z == 0 的那个)被指定为 "leader"。is_leader 变量用于识别 leader CTA。
调度与同步:
cute::gemm 计算循环仅由 leader CTA 执行。cute::cluster_sync(),这是一个新的同步原语,用于同步集群内所有协作的 CTAs。Blackwell 架构引入了张量内存(Tensor Memory, TMEM),这是一个由 warp 寻址的、显式管理的内存空间,旨在作为寄存器和共享/全局内存之间的高效数据交换媒介,特别适用于 GEMM 的 Epilogue(收尾)阶段。
tex.mbarrier.init.shared.b64,分配粒度为 512B (16B rows x 32 columns),返回一个 64-bit 的本地 TMEM 地址。tex.mbarrier.invalidate.shared.b64。tmem.load.global 和 tmem.store.global。SM100_TMEM_LOAD_L2_ENABLED_DESC_A)来指定。不同的描述符对应不同的数据布局(行主序、列主序、转置等)。CuTe 为 TMEM 提供了高层抽象,简化了其使用:
- Copy_Atom 和 TiledCopy: CuTe 提供了用于 TMEM 的原子操作和 TiledCopy 特性,封装了 TMEM 的布局和复制逻辑。
- TmemTensor 创建: 可以使用 make_fragment_A, _B, _C 等函数创建与 MMA 操作布局一致的 TmemTensor。
- TiledCopy 创建: make_tiled_copy 函数可以基于 TmemTensor 创建一个 TiledCopy 对象,用于执行实际的数据传输。
以下步骤展示了如何在 GEMM epilogue 中使用 TMEM 将累加器中的结果(矩阵 C)写回到全局内存(矩阵 D),同时可能进行量化等操作。
准备阶段:
分区与张量创建:
local_tile 从全局输出张量 D_Global 中划分出当前 CTA 负责的局部视图 D_local。make_fragment_C 将存储在寄存器中的累加器 rC 转换为一个 TMEM 张量 tC。这一步是逻辑上的转换,它为寄存器数据赋予了 TMEM 的布局信息,为后续的物理拷贝做准备。接下来的步骤将是使用 cute::copy 指令,通过先前定义的 TiledCopy 对象,将寄存器中的 rC 复制到 TMEM,然后再从 TMEM 复制到全局内存的 D_local 中。TMEM 在此过程中充当了一个高性能的暂存区。
本章介绍如何利用 CUTLASS 库来充分发挥 Blackwell 架构的新特性。
CUTLASS 3.x 建立在一个分层的概念体系之上,底层是 CuTe 库,提供了对硬件指令的精细控制,顶层则提供了高生产力的编程接口。
Collective Builder 帮助实例化最优的 collectives。CUTLASS 的设计目标之一是支持大规模的 Kernel 组合,以应对不同的算法、数据类型、数据布局和性能优化选项。这导致了数百种不同的 Kernel 变体。
构建一个 CUTLASS Kernel 通常遵循以下三个步骤:
1. 选择 Mainloop: 定义核心的 MMA 计算以及输入数据的流入方式。这通过 CollectiveMainloop 来实现。
2. 选择 Epilogue: 定义如何对 MMA 的输出进行后处理。这通过 CollectiveEpilogue 来实现。
3. 组合成 Kernel: 使用调度策略将 Mainloop 和 Epilogue 组合在一起,形成 内核层 (Kernel Layer)。
将一个为 Hopper (SM90) 架构编写的 CUTLASS Kernel 迁移到 Blackwell (SM100) 架构非常直接,主要涉及以下修改:
ArchTag 从 cutlass::arch::Sm90 修改为 cutlass::arch::Sm100_...。DispatchPolicy 从 Hopper 的 ...Sm90TmaGmma... 修改为 Blackwell 的 ...Sm100TmaWgmma...,以使用 Blackwell 引入的 Warp-Group MMA (WGMMA) 指令。TileShape 可能需要从基于 CTA 的定义调整为基于 MMA 的定义,以更好地匹配新硬件的特性。如代码高亮所示,迁移工作主要集中在修改几个关键的类型别名(using 声明),而 Kernel 的主体逻辑保持不变。
cute::TiledMma 和 cute::TiledCopy 实现。它内部实现了流水线机制(如双缓冲),以重叠数据传输和计算,并通过定义同步点(如 tma_load, mma_prologue)来协调操作。CollectiveMainloop 类型。这极大地简化了 Kernel 的定义过程。// 使用 CollectiveBuilder 简化 Mainloop 的定义
using Mainloop = typename CollectiveBuilder<
cutlass::arch::Sm100,
cutlass::gemm::MainloopSm100TmaWgmmaFp16Crosswise,
Shape<_128, _128, _32>, // TileShape_MNK
/* ... other params ... */
>::CollectiveType;
为支持新的硬件特性,CUTLASS 提供了新的 Collectives 和 Mainloop 实现。
Blackwell 架构引入了新的 WGMMA 指令,所有的 Mainloop 都基于 TmaWGMMA。CUTLASS 为此提供了新的 Collectives,以支持不同规模的 MMA 操作。
- 非线程块级 MMA (Per-Warp): 适用于 MMA 计算在单个 Warp 内部完成的场景。这通过使用 MainloopSm100TmaWgmma*PerWarp* 调度策略来实现。
- 线程块级 MMA (Block-wide/Crosswise): 适用于需要整个线程块协作完成的更大规模 MMA 计算。这通过使用 MainloopSm100TmaWgmma*Crosswise* 调度策略来实现。
CUTLASS 3.6 预调优了以下集合:
- 针对非块缩放 MMA 的带有 TMA 加载的密集 GEMM
- MainloopSm90TmaGmmaRmemAsync
针对块缩放 MMA 的带有 TMA 加载的密集 GEMM
MainloopSm100TmaGmmaRmemAsync密集分组 GEMM
MainloopSm90TmaGmmaRmemAsyncGrouped带有 TMA 加载的 h1688gemm 卷积
MainloopSm90TmaGmmaRmemAsyncImplicitGemm快速模拟 FP32 密集 GEMM
MainloopSm90TmaGmmaSimtRmemAsync带有软件块缩放的密集 GEMM
MainloopSm90TmaGmmaRmemAsyncSoftwareScaling运行时 - 首选线程块集群 (Runtime - Preferred Thread Block Clusters)
运行时数据类型 (Runtime Data Types)
- 支持将数据类型作为运行时参数指定给内核。
- 无需为具有相同位宽但类型不同的参数(例如 s4 和 u4)编译单独的内核,两者都由 cute::uint_sub_byte_t 分派。
Collective Epilogue
- 使用 cute::Tensor 进行灵活的输出后处理。
- Hopper 支持基于 TMA 的收尾(Sm90Tma)。
- Blackwell 完全支持 bfloat 类型和基于 TMT 的收尾。
Collective Builder
- 基于问题大小、设备、切片形状和数据并行策略,在运行时选择最优的集合。
- 调度内部的启发式方法 (Heuristics)。
为 Blackwell 架构引入了新的收尾(epilogue)实现,这些实现从 TMA 和 adaptor::CUT 进行了重构。
支持 sm90/sm100 的 TMA 存储收尾及其对应的 ema/grouped GEMM
Sm100TmaSpecializedSm100EmaTmaFusionSpecializedSm100TmaGroupedSpecializedCUT 融合。不使用任何共享内存进行 D 输出存储的 TMT (Direct) 存储收尾
Sm100TmtSpecializedSm100EmaTmtFusionSpecializedCUT 融合。Blackwell 架构引入了张量内存 (TMEM) 来优化 Epilogue 阶段的操作。该流程旨在将累加器中的数据高效地写回全局内存 (GMEM)。
Epilogue 流程的输入是 MMA 计算完成后存储在寄存器中的累加器 tC(D)。
// 为累加器定义一个 Gmem 布局
auto tC_gmem = make_tensor(make_gmem_ptr(C), LayoutC{});
为了将累加器的数据输出到 GMEM,需要创建一个 TmaCopy 对象来管理通过 TMEM 进行的数据传输。
// 1. 创建一个 TmaCopy 对象
auto gmem_tma = make_tma_copy(
TmaMode{},
tC_gmem);
// 2. 获取一个用于存放 TmaCopy 输入数据的 Fragment
auto tC_frg = make_fragment(tC_acc, tile_shape_MN);
在执行拷贝之前,需要根据线程索引对目标 GMEM 张量进行分区,以确保每个线程正确写入其负责的数据块。
// 3. 对 Gmem 张量进行分区
auto tC_gmem_part = partition_C(gmem_tma, thread_idx);
最后,调用 copy 函数,将累加器片段中的数据通过 TMEM 异步拷贝到已分区的 GMEM 目标位置。
// 4. 将累加器片段拷贝到已分区的 Gmem 目标
copy(gmem_tma, tC_gmem_part, tC_frg);
内核层将所有组件(主循环、收尾)整合在一起。
切片调度 (Tile scheduling):内核结构是 Warp-specialized (Warp 专用)。
StreamK 和 Data Parallelism (数据并行)。内核结构 (Kernel Structure):
Main 专业化内核用于 Blackwell,以处理 TMA 和 Epilogue。KernelTmaWarpSpecialized:当不需要后处理 MMA 结果时使用(例如,仅进行 GEMM)。KernelTmaWarpSpecializedCooperative:当需要后处理 MMA 结果时使用(例如,在 Epilogue 中进行融合操作)。该图对比了 Hopper 和 Blackwell 架构在 Warp 专用持久化内核上的执行模型。
Hopper: Ping-pong Mainloop 优化
Blackwell: 真正并发 (True Concurrency)
为 Blackwell 提供了全面的高性能支持。
特性 (Features):
TMA 和所有新的 Blackwell 拷贝指令:TMMA, TMT。JCTM, CLT。内核 (Kernels):
- 适用于所有 Hopper 类型的密集 GEMM 内核(FP32、TF32、FP16、BF16、FP8),性能比 Hopper 提升 2 倍。
- 适用于新 Hopper 数据类型的密集 GEMM 内核(benchmarking a la fp6/fp4),性能比 Hopper 提升高达 4 倍。
- 适用于所有 Hopper 类型的密集隐式 GEMM 卷积内核(FP32、TF32、FP16、BF16、FP8),性能比 Hopper 提升 2 倍。
- 使用 Blackwell Tensor Cores 的分组 GEMM 内核,支持块缩放类型。
- 使用 M16 矩阵的模拟 FP32 内核,利用 Blackwell Tensor Cores。
CUTLASS 4.x - 将 CUTLASS 设备级与 Python 互操作性结合
CUTLASS C++
- 关键特性
- 使用 Blackwell Tensor Cores 的 Blackwell 专用稠密 GEMM 内核。
- Blackwell fp4/fp6 支持,完全与 cuBLASLt 早期版本对齐。
- Blackwell INT2/INT1 支持,带有新的 MINT 块缩放 GEMM。
- 稀疏性支持:建立在 fp4/fp6 块缩放 GEMM、INT2/INT1 和 cuSPARSELt 之上。
GEMM 支持。BF16 分组 GEMM 内核。INT8/FP8 复杂 GEMM 内核。INT8/FP8 Transposed/Non-Transposed GEMM 的分组 GEMM 支持。GEMM 和 cuBLASLt。INT8 SD-A。向以下社区和开发者致谢: