Balancing the Compute Throughput & Latency in Async Programming

Petrick Liu, Jiang Shao, NVIDIA DevTech Team | AI Open Day / 2025.05.30

目录

快速回顾:Hopper之前的异步编程

在Hopper架构之前,典型的分块通用矩阵乘法(Blocked GEMM)通过将数据在不同层级内存(全局内存、共享内存、寄存器文件)之间移动,并利用CUDA核心/张量核心(Tensor Cores)进行计算。其基本流程是通过多级分块(Thread Block Tile, Warp Tile, Thread Tile)来管理数据。

Page 4 - Hopper之前异步编程的数据流和计算模式
Page 4 - Hopper之前异步编程的数据流和计算模式

如上图所示,基本的计算循环包含:
1. 从共享内存加载数据A (load_A_tile) 和数据B (load_B_tile) 到寄存器文件。
2. 线程同步 (__syncthreads())。
3. 从寄存器文件加载片段 (load_A_frag, load_B_frag)。
4. 执行矩阵乘加运算 (mma)。
5. 再次线程同步。

这种模式中,数据加载(LDGSTS,从全局内存到共享内存)和计算(MMA)交替进行,导致张量核心(TC Active)存在空闲时间,因为计算必须等待数据加载完成。

为了建立一个稳定的计算流水线,需要一个“序言”(Prologue)阶段来预加载数据。如下图所示,在计算开始前,需要执行一系列的全局内存加载指令(LDGSTS Ktile0Ktile4)来填充数据缓冲区。这个过程会引入启动延迟(Gmem Latency),在此期间张量核心处于非活动状态。

Page 5 - 建立稳定流水线所需的序言阶段
Page 5 - 建立稳定流水线所需的序言阶段

为了隐藏和重叠异步操作的延迟,需要精心设计的流水线。关键的优化思路是:
- 采用高度流水线化的设计来最大化吞吐量。
- "序言"阶段是实现高吞吐量所需付出的代价。
- 在主循环(Mainloop)中,通过RF双缓冲(RF double buffer)等技术,实现全局内存加载(Gmem Loading)、共享内存加载(Smem Loading)与张量核心计算(TC computing)的完全重叠。

Page 6 - 通过流水线和双缓冲重叠延迟
Page 6 - 通过流水线和双缓冲重叠延迟

从Ampere到Hopper架构的演进

不同架构在处理计算任务时采用了不同的调度和资源分配策略,这直接影响了效率和延迟隐藏。

Page 7 - Ampere与Hopper架构执行模型对比
Page 7 - Ampere与Hopper架构执行模型对比
从Ampere到Hopper的GEMM执行模型对比
从Ampere到Hopper的GEMM执行模型对比

Mbarrier:异步编程的关键促成因素

Hopper架构引入了Mbarrier,这是一种用于Warp间通信和同步的强大机制,是实现高效异步编程的关键。它支持创建生产者-消费者(Producer-Consumer)模型,特别是在Warp专业化的GEMM中。

Page 9 - Hopper架构下基于TMA和Mbarrier的生产者-消费者模型
Page 9 - Hopper架构下基于TMA和Mbarrier的生产者-消费者模型

Hopper Warp专业化GEMM流程

  1. 生产者Warp (TMA Warps):

    • 负责数据加载,使用CollectiveMma::load_a等接口。
    • 等待 Smem_empty 屏障,确保消费者已使用完上一批数据。
    • 发出TMA(Tensor Memory Accelerator)指令,将矩阵A和B的数据块从全局内存异步加载到共享内存,并更新 Smem_full 屏障。
    • 到达 Smem_full 屏障,通知消费者数据已准备好。
  2. 消费者Warp (TC Warps):

    • 负责计算,使用CollectiveMma::mma等接口,并且是持久化的。
    • 等待 Smem_full 屏障,直到生产者准备好数据。
    • 发出WGMM (Warp Group Matrix Multiply) 指令进行计算。
    • 计算完成后,到达 Smem_empty 屏障,通知生产者可以加载新数据。
    • 将计算结果写回寄存器文件,并最终通过TMA指令写回全局内存。

共享内存作为数据缓冲区,通过Mbarrier对象(包含Smem_emptySmem_full状态)进行同步,实现了数据加载和计算的高度流水化。

Mbarrier与TMA编程模式详解

以下通过一个逐步示例解释Mbarrier的工作机制。

1. 初始化

Page 10 - Mbarrier初始化状态
Page 10 - Mbarrier初始化状态

2. TMA发出加载和到达指令
- TMA Warp中的一个线程 (if(tma_thread)) 发出TMA_bulk_load指令,请求加载16KB数据。
- 接着发出mbarrier_arrive_expect(&bar, 16KB),通知Mbarrier本次事务预期传输16KB数据。
- Mbarrier状态更新:Expect Trans_Bytes变为16KBActual Arrv Cnt根据执行线程数更新。

Page 11 - TMA Warp发出加载指令
Page 11 - TMA Warp发出加载指令

3. TMA数据传输
- TMA开始将数据从全局内存加载到共享内存。
- Mbarrier会追踪实际已传输的字节数。图中显示已传输1KB,Actual Trans_Bytes更新为1KB。TC Warp仍然在try_wait处阻塞。

Page 12 - Mbarrier追踪数据传输进度 (1KB)
Page 12 - Mbarrier追踪数据传输进度 (1KB)
Page 13 - Mbarrier追踪数据传输进度 (4KB)
Page 13 - Mbarrier追踪数据传输进度 (4KB)

4. 事务完成
- 当全部16KB数据传输完成时,Mbarrier记录整个事务完成。Actual Trans_Bytes变为16KB

Page 14 - Mbarrier记录16KB事务完成
Page 14 - Mbarrier记录16KB事务完成

5. Phase翻转与消费者唤醒
- 由于事务完成,MbarrierPhase0翻转到1
- TC Warp的try_wait(&bar, phase)条件满足,跳出循环("Pass here!")。
- TC Warp开始使用共享内存中的数据进行WGMMA计算。
- Mbarrier状态被重置,为下一轮数据传输做准备(例如,Expect Trans_Bytes重置为0)。

Page 15 - Phase翻转,TC Warp被唤醒开始计算
Page 15 - Phase翻转,TC Warp被唤醒开始计算

Mbarrier 与 TMA 编程模式

Mbarrier 状态转换

该编程模式涉及一个TMA Warp(生产者)和一个或多个TC WarpGroup(消费者),通过两个mbarrier对象(bar_fullbar_empty)进行同步。

初始状态与第一阶段 (Page 16)
- 初始化:
- Init_mbarrier(&bar_full, 1): 初始化bar_full,期望到达数为1(来自TMA Warp)。
- Init_mbarrier(&bar_empty, 128): 初始化bar_empty,期望到达数为128(来自TC WarpGroups)。
- mbarrier_fence(): 确保初始化完成。

Mbarrier和TMA编程模式 - 阶段1
Mbarrier和TMA编程模式 - 阶段1

数据就绪与消费 (Page 17)

Mbarrier和TMA编程模式 - 阶段1完成
Mbarrier和TMA编程模式 - 阶段1完成

SMEM释放与循环 (Page 18)

Mbarrier和TMA编程模式 - 阶段0完成
Mbarrier和TMA编程模式 - 阶段0完成

第二次迭代 (Page 19)

Mbarrier和TMA编程模式 - 第二次迭代
Mbarrier和TMA编程模式 - 第二次迭代

Hopper Warp Specialized GEMM

TMA-TC 生产者-消费者模型

这是一种利用Hopper架构特性(如TMA、WGMMA、Mbarrier)实现的高效GEMM计算模型,其核心思想是生产者和消费者的解耦。

Hopper Warp Specialized GEMM 生产者消费者模型
Hopper Warp Specialized GEMM 生产者消费者模型

CUTLASS中的流水线实现

CUTLASS库提供了用于实现这种复杂流水线模型的原生组件。

CUTLASS PipelineState 代码示例
CUTLASS PipelineState 代码示例
CUTLASS TMA-TC模型原语
CUTLASS TMA-TC模型原语

Hopper Tensor Core 基础概念

Hopper WGMMA 指令

Hopper架构引入了Warp Group MMA (WGMMA),以 warp group(128个线程)为单位进行矩阵乘法。

Hopper Tensor Core 基础概念与数据布局
Hopper Tensor Core 基础概念与数据布局

典型的WGMMA指令序列

一个典型的WGMMA计算流程如下:

  1. wgmma.fence.sync.aligned;

    • 栅栏指令,确保所有线程的SMEM和RFs都已就绪。
  2. wgmma.mma_async.aligned.m64n128k16.f32.f16.f16; ...

    • 发出若干条异步的wgmma指令。这些指令被分组执行。
  3. wgmma.commit_group.sync.aligned;

    • 提交上述指令组,使其开始执行。
  4. wgmma.wait_group.sync.aligned 0;

    • 等待组号为0的指令组完成计算。
典型的WGMMA指令序列
典型的WGMMA指令序列

最大化Hopper Tensor Core吞吐量

延迟问题

Tensor Core 空闲周期
Tensor Core 空闲周期

使用流水线隐藏延迟

使用流水线消除Tensor Core空闲周期
使用流水线消除Tensor Core空闲周期

CUTLASS中的WGMMA流水线实现

CUTLASS提供了WGMMA多级流水线的实现。代码逻辑分为两个主要部分:

  1. MMA多级流水线序言 (Prologue):

    • 在主循环开始前,预取多个阶段的数据,以填满流水线。
    • 这部分代码会等待数据可用 (pipeline.consumer_wait),然后执行WGMMA计算。
  2. MMA多级流水线主循环 (Mainloop):

    • 在主循环的每次迭代中,对当前k分块进行计算,同时异步获取下一个k分块的数据。
    • 核心逻辑是:等待下一份数据 (consumer_wait) -> 对当前数据进行计算 (tiled_mma) -> 释放之前用过的缓冲区 (consumer_release) -> 推进流水线 (smem_pipe.release)。
    • 通过这种方式,只在流水线的末尾释放SMEM,使数据在需要时一直保留在SMEM中(Keep MMAs in flight)。
CUTLASS中的WGMMA流水线代码实现
CUTLASS中的WGMMA流水线代码实现
WGMMA Prologue代码分割
WGMMA Prologue代码分割

Warp 组执行排序

为了错开两个Warp组的执行,可以使用 OrderedSequenceBarrier。以下代码片段展示了如何命令两个数学Warp组(Math WG)的MMA(矩阵乘法累加)操作,这有助于隐藏尾声(epilogue)的开销。

代码逻辑如下:

  1. 主循环:当有工作瓦片(work tile)时,计算M、N、K坐标。
  2. 内存分配:为(M,N)块形状的累加器分配内存。
  3. 排序MMA:使用 math_wg_order.barrier.wait() 来命令两个数学Warp组的MMA操作。
  4. 主循环流水线:执行 collective_mainloop.mma,处理主循环流水线、消费者状态、累加器等。
  5. 启动下一个MMA:使用 math_wg_order.barrier.arrive() 为下一个数学Warp组的MMA做准备。
  6. 同步:确保所有指令完成且自由缓冲区清空,然后进入主循环。
  7. 更新流水线:更新主循环流水线的消费者状态。
  8. 尾声与存储:在循环外,执行尾声(epilogue)并存储结果。这包括加载流水线、消费者状态、存储流水线、问题形状、块坐标、累加器等。
  9. 更新加载/存储状态:为下一个瓦片更新加载/存储流水线的状态。
  10. 等待:等待所有TMA存储完成。
  11. 调度:获取下一个工作瓦片并调度。
Page 31: Warp组执行排序代码示例
Page 31: Warp组执行排序代码示例

从 Hopper 到 Blackwell 的演进

新特性/关键特性

SM100 (Hopper)

SM120 (Blackwell)

Page 33: Hopper与Blackwell特性对比
Page 33: Hopper与Blackwell特性对比

轻松最大化 Blackwell Tensor Core 吞吐量

下图展示了Warp调度器如何处理 tcgen05.mma 指令。硬件(HW)会异步跟踪TC(Tensor Core)的完成情况,并通过 mBarrier 进行更新。Wait Smem0 表示等待共享内存数据,Commit 表示提交任务,Issue 表示分发MMA指令。

Page 34: Blackwell Tensor Core 异步执行流程
Page 34: Blackwell Tensor Core 异步执行流程

静态分块调度 (Static Tile Scheduling)

如下图所示,当SM 2被另一个网格(Other Grid)占用时,原先分配给它的分块(如Tile 102, 202, 302)必须等待SM 2空闲后才能被处理,这造成了整体执行时间的延长。

Page 35: 静态分块调度及其问题
Page 35: 静态分块调度及其问题

动态分块调度 (Dynamic Tile Scheduling)

如下图所示,当SM 2被占用时,动态调度器会将原本分配给它的任务(如Tile 301)重新分配给其他空闲的SM(如SM 1),从而避免了执行延迟,优化了资源利用率。

Page 36: 动态分块调度机制
Page 36: 动态分块调度机制

Warp专用持久化内核:Hopper vs Blackwell 对比

Hopper Ping-pong Warp Specialization

Blackwell Warp Specialization

Page 37: Hopper与Blackwell Warp专用持久化内核对比
Page 37: Hopper与Blackwell Warp专用持久化内核对比

Blackwell 持久化内核高层视图

这是一个SM100内核的视图,展示了不同类型的Warp(线程束)如何协作:
- Sch Warp (WarpId = 3):调度Warp,负责管理工作负载(Workld)。当工作负载为空(Workld_Empty)时,需要从外部获取输入偏移(input offset);当工作负载满(Workld_Full)时,通知工作负载流水线消费者。它也负责在需要时停止其他Warp。
- TC Warp (WarpId = 2):Tensor Core Warp,执行核心计算任务。它是TMA主循环流水线的消费者和尾声流水线的生产者。
- TMA Warps (WarpId = 0, 1):负责数据加载。它们是TMA主循环流水线的生产者。
- EpilogueWarps (WarpId = 4,5,6,7):负责尾声处理。它们是尾声流水线的消费者。

这些Warp通过共享内存(SMEM)和张量内存(TMEM)的状态(Full/Empty)以及工作负载队列进行同步和通信,形成一个高效的生产者-消费者流水线模型。

Page 38: Blackwell持久化内核高层架构图
Page 38: Blackwell持久化内核高层架构图

从Hopper到Blackwell(SM_120)的性能演进

此图比较了不同架构和调度策略下的执行时间线,其中不同的Warp被分配了不同的任务(prolog, mainloop, epilog)。

Blackwell架构通过动态调度和专门的Warp任务分配,有效缩短了计算时间。

Page 39: Hopper到Blackwell SM_120性能演进对比
Page 39: Hopper到Blackwell SM_120性能演进对比

案例研究:W4A8 Hopper Grouped Gemm Kernel

混合数据类型分组GEMM (Mixed Data Type Grouped GEMM)

Page 41: 混合数据类型分组GEMM计算流程
Page 41: 混合数据类型分组GEMM计算流程

混合数据类型分组GEMM - CUTLASS 实现

下图展示了将INT4的矩阵B先convert为FP16,再进行scaling_B,最后convert为FP8,然后与FP8的矩阵A进行MMA运算。

Page 42: CUTLASS混合数据类型分组GEMM实现细节
Page 42: CUTLASS混合数据类型分组GEMM实现细节

混合数据类型分组GEMM - 优化实现

此页展示了对前一页流程的优化。通过使用查找表转换(lookup_table_convert),将INT4矩阵B的转换和缩放操作合并为一步(convert & scaling_B),直接生成FP8格式的数据。这减少了中间步骤和数据移动,提高了效率。

Page 43: 优化的混合数据类型分组GEMM实现
Page 43: 优化的混合数据类型分组GEMM实现

混合数据类型分组GEMM - 工作分配

下图展示了如何将两个组(Group 0和Group 1)的计算任务进行划分。每个组的输出矩阵C被划分为多个Block(Block 0-3等),每个Block由一个CUDA线程块负责计算。输入矩阵A和B也相应地按TileShape进行划分。

Page 44: 混合数据类型分组GEMM的工作分配策略
Page 44: 混合数据类型分组GEMM的工作分配策略

混合数据类型分组GEMM - 块内执行

下图左侧展示了线程块如何分阶段(Stage 0, Stage 1)处理K维度的不同块(k_block 0, k_block 1)。右侧图则展示了用于计算的TileShape

Page 45: 线程块内部的执行流程
Page 45: 线程块内部的执行流程

实现细节与优化

流水线设计

为了高效执行混合精度矩阵乘法,设计了一个流水线(Pipeline)机制。其核心思想是重叠数据加载、转换和计算操作。

下图展示了一个流水线执行流程。矩阵B(INT4)通过lookup_table_convert进行转换和缩放,变为FP8格式。然后与FP8格式的矩阵A进行MMA操作,累加结果为FP32。最后,对累加结果进行缩放和转换,得到FP16格式的输出矩阵C。整个过程在多个kblock上以流水线方式执行,并使用双缓冲(double buffer)来隐藏数据传输延迟。

Page 46
Page 46

该流程分为两个主要阶段(Stage 0 和 Stage 1),并由copy_tensors_MKdequantize_A_kblockcute::gemm等核函数以及warpgroup_wait同步原语协调。

Page 47
Page 47

Hopper Tensor Core MMA 指令

Hopper架构引入了特定的Tensor Core指令 wgmma.mma_async,用于实现异步的矩阵乘法与累加操作。

在混合专家(MoE)模型推理中,有两个关键点:

  1. INT4数据的转换需要在寄存器文件(RF)中进行。
  2. 每个专家处理的token数量通常小于64。

下图右侧展示了.m64n16k32指令如何将128个线程(T0-T127)组织成4个warp来执行计算。

Page 48
Page 48

使用 CuTe 进行数据布局

CuTe是一个基于C++的库,用于描述和操作张量在GPU内存中的布局,是CUTLASS 3.x的核心组件。

下图直观地展示了A和B矩阵在逻辑上的Tile划分。

Page 49
Page 49

分区策略 (Partitioning Strategy)

使用CuTe,可以将全局的Tile划分为每个线程和warpgroup负责处理的数据分片。

// 获取当前线程/warpgroup在MMA操作中的数据分片
auto mma_thread_slice = tiled_mma.get_thread_slice(thread_idx);
auto mma_warpgroup_slice = tiled_mma.get_warpgroup_slice(warp_group_idx);

// 为A和B分配张量片段和描述符
Tensor tCrA_mma = mma_thread_slice.partition_fragment_A(sA(_,_,Int<0>{})); // MMA_M, MMA_N, MMA_K, PIPE
Tensor tCsA = mma_thread_slice.partition_A(sA);
...
Tensor tCrB_mma = mma_warpgroup_slice.make_fragment_B(tCsB); // MMA_M, MMA_N, MMA_K, PIPE
Tensor tCsB = mma_warpgroup_slice.partition_B(sB);
Page 50
Page 50
Page 51
Page 51

数据布局详解

下图展示了数据如何从32位格式(包含4个FP8值)加载到Warp0中各个线程的寄存器中。

Page 52
Page 52

下图更直观地展示了数据从SMEM中的A Tile到线程寄存器(RF)的映射过程。编号的箭头表示数据加载和处理的逻辑步骤。

Page 53
Page 53

累加器 (Accumulator)

GEMM操作的核心是cute::gemm,它将A和B的分片相乘并累加到累加器(accum)中。

cute::gemm(tiled_mma, tCrA_mma, tCrB_(_:_,_:k_block), accum);

下图展示了Warp0的累加器寄存器布局,其中每个64位寄存器存储2个FP32值。

Page 54
Page 54

MoE 模型中的应用

Page 55
Page 55
Page 56
Page 56

优化的流水线

基于上述优化方案,流水线被重新设计。

这个新的流水线通过在计算过程中间引入缩放步骤,提高了效率。warpgroup_wait的依赖关系也相应调整为3 -> 0

Page 57
Page 57