CUTLASS-Cute 初步(3.1):TiledCopy 以及 TiledMMA 配置示例

  // Configure data type.
  using TA = cute::half_t;
  using TB = cute::half_t;
  using TC = cute::half_t;

  // Configure static "shared memory".
  // The "shared memory" is actually on host for preview purpose.
  // For tiled mma, the shared memory layout has to be static.
  constexpr int bM{128 * 2 / sizeof(TA)};
  constexpr int bN{128 * 2 / sizeof(TB)};
  constexpr int bK{32};
  auto const blk_M = cute::Int<bM>{};
  auto const blk_N = cute::Int<bN>{};
  auto const blk_K = cute::Int<bK>{};

  auto const smem_shape_A{cute::make_shape(blk_M, blk_K)};
  auto const smem_shape_B{cute::make_shape(blk_N, blk_K)};
  auto const smem_shape_C{cute::make_shape(blk_M, blk_N)};
  auto const smem_stride_A{cute::make_stride(cute::Int<1>{}, blk_M)};        // Column-major
  auto const smem_stride_B{cute::make_stride(cute::Int<1>{}, blk_N)};        // Column-major
  auto const smem_stride_C{cute::make_stride(cute::Int<1>{}, blk_M)};        // Column-major
  auto const smem_layout_A{cute::make_layout(smem_shape_A, smem_stride_A)};  // (blk_M, blk_K)
  auto const smem_layout_B{cute::make_layout(smem_shape_B, smem_stride_B)};  // (blk_N, blk_K)
  auto const smem_layout_C{cute::make_layout(smem_shape_C, smem_stride_C)};  // (blk_M, blk_N)

  auto const size_a{blk_M * blk_K};
  auto const size_b{blk_N * blk_K};
  auto const size_c{blk_M * blk_N};

  auto h_A = thrust::host_vector<TA>(size_a);
  auto h_B = thrust::host_vector<TB>(size_b);
  auto h_C = thrust::host_vector<TC>(size_c);

  // Make tensor for smem_A and smem_B.
  auto smem_tensor_A{cute::make_tensor(h_A.data(), smem_layout_A)};
  auto smem_tensor_B{cute::make_tensor(h_B.data(), smem_layout_B)};
  auto smem_tensor_C{cute::make_tensor(h_C.data(), smem_layout_C)};

1. TiledMMA 配置

位于 SMEM 中的 tile 大小为 $M \times N \times K = 128 \times 128 \times 32$,其中:

  • A 矩阵为 $M \times K = 128 \times 32$,row-major layout;
  • B 矩阵为 $K \times N = 32 \times 128$,column-major layout;
  • C 矩阵为 $M \times N = 128 \times 128$。

1.1. MMA_Atom 配置

MMA_Atom 使用的配置为 cute::SM80_16x8x16_F16F16F16F16_TN,使用一个 warp,即 32 个线程处理这个 MMA Atom。处理的 MNK 规模为:$M’ \times N’ \times K’ = 16 \times 8 \times 16$,其中:

  • A sub-tile 为 $M’ \times K’ = 16 \times 16$;
  • B sub-tile 为 $K’ \times N’ = 16 \times 8$;
  • C sub-tile 为 $M’ \times N’ = 16 \times 8$。
mma_atom
MMA_Atom
  ThrID:      _32:_1
  Shape_MNK:  (_16,_8,_16)
  LayoutA_TV: ((_4,_8),(_2,_2,_2)):((_32,_1),(_16,_8,_128))
  LayoutB_TV: ((_4,_8),(_2,_2)):((_16,_1),(_8,_64))
  LayoutC_TV: ((_4,_8),(_2,_2)):((_32,_1),(_16,_8))

分配到线程,每个线程处理的元素数量为:A 矩阵为 2 x 2 x 2 = 8 个元素,B 矩阵为 2 x 2 = 4 个元素,得到 C 矩阵中 2 x 2 = 4 个元素。

inverse_tv_layout_SM80_16x8x16_F16F16F16F16_TN

1.2. Tiled MMA 配置

  // Configure tiled MMA.
  using MmaTraits           = cute::MMA_Traits<cute::SM80_16x8x16_F16F16F16F16_TN>;
  using MmaAtomShape        = MmaTraits::Shape_MNK;
  auto const mma_atom       = cute::MMA_Atom<MmaTraits>{};
  auto const mma_atom_shape = MmaAtomShape{};
  // Repeating the mma atom along the M, N, and K dimensions.
  // This increases the number of threads to process the tiled MMA.
  constexpr int MMA_LAYOUT_M{2};
  constexpr int MMA_LAYOUT_N{2};
  constexpr int MMA_LAYOUT_K{1};
  auto mma_layout{cute::make_layout(
    cute::make_shape(cute::Int<MMA_LAYOUT_M>{}, cute::Int<MMA_LAYOUT_N>{}, cute::Int<MMA_LAYOUT_K>{}))};
  // Repeating the mma processing along the M, N, and K dimensions.
  // This does not increase the number of threads to process the tiled MMA.
  // But the number of registers required for processing the tiled MMA increases.
  constexpr int NUM_MMA_TILE_M{1};
  constexpr int NUM_MMA_TILE_N{2};
  constexpr int NUM_MMA_TILE_K{1};
  constexpr int MMA_TILE_M{cute::get<0>(mma_atom_shape) * MMA_LAYOUT_M * NUM_MMA_TILE_M};
  constexpr int MMA_TILE_N{cute::get<1>(mma_atom_shape) * MMA_LAYOUT_N * NUM_MMA_TILE_N};
  constexpr int MMA_TILE_K{cute::get<2>(mma_atom_shape) * MMA_LAYOUT_K * NUM_MMA_TILE_K};
  auto mma_tile{cute::make_tile(cute::Int<MMA_TILE_M>{}, cute::Int<MMA_TILE_N>{}, cute::Int<MMA_TILE_K>{})};
  auto tiled_mma{cute::make_tiled_mma(mma_atom, mma_layout, mma_tile)};

在 M 维度上,MMA Atom 重复 2 次,在 N 维度上重复 2 次,在 K 维度上重复 1 次。一共需要 2 x 2 x 1 = 4 个 MMA Atom 来处理这个 tiled MMA。每个 Atom 由一个 warp(32 个线程)处理,整个 tiled MMA 由 4 个 warp(128 个线程)处理。即 ThrLayoutVMNK = (_32,_2,_2,_1):(_1,_32,_64,_0)。经过此配置后,能处理的 MNK 规模为 $(M’ \times 2) \times (N’ \times 2) \times (K’ \times 1) = 32 \times 16 \times 16$。

另外,通过配置 PermutationMNK(对应以前版本的 ValLayoutMNK),使得一个 tiled MMA 在 M/N/K 方向上处理更多的元素(即一个线程处理更多的元素)。这里配置 N 维度上乘以 2,得到该 tiled MMA 处理的 MNK 规模为 $32 \times 32 \times 16$,即 PermutationMNK: (_32,_32,_16)

tiled_mma
TiledMMA
  ThrLayoutVMNK:  (_32,_2,_2,_1):(_1,_32,_64,_0)
  PermutationMNK: (_32,_32,_16)
MMA_Atom
  ThrID:      _32:_1
  Shape_MNK:  (_16,_8,_16)
  LayoutA_TV: ((_4,_8),(_2,_2,_2)):((_32,_1),(_16,_8,_128))
  LayoutB_TV: ((_4,_8),(_2,_2)):((_16,_1),(_8,_64))
  LayoutC_TV: ((_4,_8),(_2,_2)):((_32,_1),(_16,_8))

tile_mma_SM80_16x8x16_F16F16F16F16_TN

1.3. Tiled MMA 划分总结

以$M \times N \times K = 128 \times 128 \times 32$,以及 MMA Atom SM80_16x8x16_F16F16F16F16_TN 为例,MNK 三个维度分块划分(计算公式),可以按如下几种:

  • $\frac{M}{M’} \times \frac{N}{N’} \times \frac{K}{K’}$ = $\frac{128}{16} \times \frac{128}{8} \times \frac{32}{16}$ = $8 \times 16 \times 2$ = $256$,即 MMA Atom 在 M 维度重复 8 次,N 维度 重复16 次,K 维度重复 2 次。Atom 总共需要循环 256 次。
  • $\frac{M}{M’ \times MMA_LAYOUT_M} \times \frac{N}{N’ \times MMA_LAYOUT_N} \times \frac{K}{K’ \times MMA_LAYOUT_K}$ = $\frac{128}{16 \times 2} \times \frac{128}{8 \times 2} \times \frac{32}{16 \times 1}$ = $4 \times 8 \times 2$ = $64$,即 tiled MMA 在 M 维度重复 2 次,N 维度重复 2 次,K 维度重复 1 次。Atom 总共需要循环 64 次。
  • 其他配置方式,类似上面两种计算方式。

一个 thread block 如何划分一个 tiled MMA,从性能上需要综合考虑以下几个因素:

  • SM 上可用的寄存器数量,来确定一个 thread block 中可以有多少个线程来处理这个 tiled MMA。每个线程处理的元素数量越多,需要的寄存器数量就越多。
  • 在寄存器够用的情况下,尽量让一个 thread block 中的线程数量能够充分利用 SM 上的计算资源(即 CUDA Core、Tensor Core)。每个 tiled MMA 需要多少个线程来处理,取决于 MMA Atom 的配置以及 tiled MMA 的配置。

2. 内存分块以及 TiledCopy

在将 thread block 对应的 tile 内存再次分解为线程 sub-tile 过程中,使用 CuTe 分块,有三种方式:

  • 使用 partition 分块,得到 SMEM / GMEM slice。
  • 使用 partition_fragment 分块,得到寄存器片段(register fragment)。
  • 使用 TiledCopy / ldmatrix 进行分块以及传输。

2.1. 使用 partition、partition_fragment 分块

partition_A/B/C

使用 partition 方法,获取原始 layout 的分块,即生成一个 slice。分块之后,数据还是在 SMEM / GMEM 中,且保留了原有的 tile 的 stride 信息。

  auto thread_mma{tiled_mma.get_slice(THREAD_IDX)};

  auto thread_layout_C_smem_tensor_A_no_tiled_copy{thread_mma.partition_A(smem_tensor_A)};  // (MMA, MMA_M, MMA_K)
  auto thread_layout_C_smem_tensor_B_no_tiled_copy{thread_mma.partition_B(smem_tensor_B)};  // (MMA, MMA_N, MMA_K)
  auto thread_layout_C_smem_tensor_C_no_tiled_copy{thread_mma.partition_C(smem_tensor_C)};  // (MMA, MMA_M, MMA_N)

打印信息如下:

thread_layout_C_smem_tensor_A_no_tiled_copy
ptr[16b](0x5c6c073e78c0) o ((_2,_2,_2),_4,_2):((_128,_8,_1024),_32,_2048)
thread_layout_C_smem_tensor_B_no_tiled_copy
ptr[16b](0x5c6c073e98d0) o ((_2,_2),_8,_2):((_128,_1024),_16,_2048)
thread_layout_C_smem_tensor_C_no_tiled_copy
ptr[16b](0x5c6c073eb8e0) o ((_2,_2),_4,_8):((_128,_8),_32,_2048)

其中,含义如下:

维度 含义
MMA (第0维) 一次 tiled MMA 计算中该线程负责的元素
MMA_M (第1维) 沿 M 方向需要循环的次数
MMA_K (第2维) 沿 K 方向需要循环的次数

具体到 A/B/C 矩阵上,含义如下:

矩阵 shape 解释
A ((_2,_2,_2), _4, _2) MMA=8个元素, M循环4次, K循环2次
B ((_2,_2), _8, _2) MMA=4个元素, N循环8次, K循环2次
C ((_2,_2), _4, _8) MMA=4个元素, M循环4次, N循环8次

按线程切分之后,保留的第一个 mode,另外两个 mode 含义是(以 A 为例),MMA Atom 按 M 维度循环 4 次,K 维度循环 2 次。对于 B/C 矩阵类似。

从打印信息看出,不论是直接使用 thread_layout_C_smem_tensor_A/B_no_tiled_copy,还是将其拷贝到寄存器,由于不连续,导致线程每次访问 2 _ 2 _ 2 = 8 个元素时,需要分开拷贝,即分 8 次访问 SMEM / GMEM 来加载数据到寄存器中。

partition_fragment_A/B/C

partition_fragment 则创建寄存器片段(register fragment),以复用寄存器数据。分块之后,数据在寄存器中,且不保留原有 tile 的 stride 信息,而是变为紧凑型布局。线程在做 gemm 之前,使用 copy 将数据从 SMEM 中加载到寄存器中。

  auto thread_layout_C_register_tensor_A{thread_mma.partition_fragment_A(smem_tensor_A)};  // (MMA, MMA_M, MMA_K)
  auto thread_layout_C_register_tensor_B{thread_mma.partition_fragment_B(smem_tensor_B)};  // (MMA, MMA_N, MMA_K)
  auto thread_layout_C_register_tensor_C{thread_mma.partition_fragment_C(smem_tensor_C)};  // (MMA, MMA_M, MMA_N)

打印信息如下:

thread_layout_C_register_tensor_A
ptr[16b](0x7ffc34e465f0) o ((_2,_2,_2),_4,_2):((_1,_2,_4),_8,_32)
thread_layout_C_register_tensor_B
ptr[16b](0x7ffc34e46670) o ((_2,_2),_8,_2):((_1,_2),_4,_32)
thread_layout_C_register_tensor_C
ptr[16b](0x7ffc34e466f0) o ((_2,_2),_4,_8):((_1,_2),_4,_16)

总结

使用 partition / partition_fragment 分块,分块数据可能不连续,导致需要多次访问 SMEM / GMEM。使用合适的 ldmatrix 可以一次将一次计算所需要的 sub-tile 数据拷贝到寄存器。

2.2. 使用 TiledCopy / ldmatrix 进行分块以及传输

前面使用的 partition / partition_fragment 方式,直接使用 TiledMMA/ThrMMA 分块得到的。使用 TiledCopy / ldmatrix 进行分块以及传输。设置 TiledCopy 需要的 Copy_Atom、CopyTraits(layout 信息),并使其传输的 sub-tile 大小与 tiled MMA 的计算需求一致。

2.2.1. Copy Atom 配置

针对 A、B,使用 cute::SM75_U16x8_LDSM_T 生成 Copy Atom 来复制 A、B 的 sub-tile,其对应的 PTX 为:

ldmatrix.sync.aligned.m8n8.x4.trans.shared.b16 {r0, r1, r2, r3}, [addr];

即使用一个 warp(32 个线程)同时加载一个 sub-tile 的数据到寄存器中。

ldmatrix 只支持16位数据,不支持32位数据;另外 ldmatrix 是 PTX 指令,对应到 ncu 中看到的 LDSM 指令。x1 表示使用前 8 个线程,x2、x4 依次类推。m8n8 表示 load 操作的子块大小为 8x8,x4 则表示同时加载 4 个子块。

cute::SM75_U16x8_LDSM_T 打印信息如下:

copy_atom_A
Copy_Atom
  ThrID:        _32:_1
  ValLayoutSrc: (_32,_8):(_8,_1)
  ValLayoutDst: ((_4,_8),(_1,_2,_4)):((_16,_1),(_1,_8,_64))
  ValLayoutRef: ((_4,_8),(_1,_2,_4)):((_16,_1),(_1,_8,_64))
  ValueType:    16b

copy_atom_B
Copy_Atom
  ThrID:        _32:_1
  ValLayoutSrc: (_32,_8):(_8,_1)
  ValLayoutDst: ((_4,_8),(_1,_2,_4)):((_16,_1),(_1,_8,_64))
  ValLayoutRef: ((_4,_8),(_1,_2,_4)):((_16,_1),(_1,_8,_64))
  ValueType:    16b

在前面 TiledMMA 的配置中,经过 AtomLayout 以及 Permutation,最终得到的 tiled MMA 处理的 sub-tile 大小为 $M’ \times N’ \times K’ = 32 \times 32 \times 16$。其中 A 矩阵的 sub-tile 大小为 $M’ \times K’ = 32 \times 16$,B 矩阵的 sub-tile 大小为 $K’ \times N’ = 16 \times 32$。

使用SM75_U16x8_LDSM_T,其一次拷贝的 sub-tile 大小为 $32 \times 8 = 256$,即对应上面打印信息中的 ValLayoutSrc: (_32,_8):(_8,_1)。因此,TiledCopy 的 Copy Atom 配置满足一次加载 执行一个 Tiled MMA 所需要的数据。

另外,在 Tiled MMA 配置中,B 配置了 permutation,使得 B 矩阵的 sub-tile 大小与 A 矩阵大小一致,否则针对 B,就需要选择其他的 Copy Atom 来满足 tiled MMA 的计算需求。

2.2.2. TiledCopy 以及 ThreadCopy 创建

创建 TiledCopy,Copy_Atom 配置如上SM75_U16x8_LDSM_T,TV-Layout 则由上述的 Tiled MMA 给出。代码如下:

  auto copy_atom_A = cute::Copy_Atom<cute::SM75_U16x8_LDSM_T, TA>{};
  auto copy_atom_B = cute::Copy_Atom<cute::SM75_U16x8_LDSM_T, TB>{};

  auto smem_tiled_copy_A{cute::make_tiled_copy_A(copy_atom_A, tiled_mma)};
  auto smem_tiled_copy_B{cute::make_tiled_copy_B(copy_atom_B, tiled_mma)};

Tiled Copy 信息如下:

smem_tiled_copy_A
TiledCopy
  Tiler_MN:       (_32,_16)
  TiledLayout_TV: ((_4,_8,_2,_2),((_2,_2,_2),(_1,_1))):((_64,_1,_16,_0),((_32,_8,_256),(_0,_0)))
Copy_Atom
  ThrID:        _32:_1
  ValLayoutSrc: (_32,_8):(_8,_1)
  ValLayoutDst: ((_4,_8),(_1,_2,_4)):((_16,_1),(_1,_8,_64))
  ValLayoutRef: ((_4,_8),(_1,_2,_4)):((_16,_1),(_1,_8,_64))
  ValueType:    16b

smem_tiled_copy_B
TiledCopy
  Tiler_MN:       (_32,_16)
  TiledLayout_TV: ((_4,_8,_2,_2),((_2,_2),(_2,_1))):((_64,_1,_0,_8),((_32,_256),(_16,_0)))
Copy_Atom
  ThrID:        _32:_1
  ValLayoutSrc: (_32,_8):(_8,_1)
  ValLayoutDst: ((_4,_8),(_1,_2,_4)):((_16,_1),(_1,_8,_64))
  ValLayoutRef: ((_4,_8),(_1,_2,_4)):((_16,_1),(_1,_8,_64))
  ValueType:    16b

使用 Thread Copy 以及创建本线程 sub-tile

分为两步,首先使用 ThrCopy<…>::partition_S 获取线程的 tensor,再使用 ThrCopy<…>::retile_D 获取调整 shape 之后的 tensor。代码如下:

  auto smem_thread_copy_A{smem_tiled_copy_A.get_slice(THREAD_IDX)};
  auto smem_thread_copy_B{smem_tiled_copy_B.get_slice(THREAD_IDX)};

  auto thread_layout_C_smem_tensor_A_tiled_copy{smem_thread_copy_A.partition_S(smem_tensor_A)};
  auto thread_layout_C_smem_tensor_B_tiled_copy{smem_thread_copy_B.partition_S(smem_tensor_B)};

  auto thread_layout_C_register_tensor_A_copy_view{smem_thread_copy_A.retile_D(thread_layout_C_register_tensor_A)};
  auto thread_layout_C_register_tensor_B_copy_view{smem_thread_copy_B.retile_D(thread_layout_C_register_tensor_B)};

thread_layout_C_smem_tensor_A/B_tiled_copy 作为源 tensor,他们的信息如下:

thread_layout_C_smem_tensor_A_tiled_copy
ptr[16b](0x57b7b93248c0) o ((_8,_1),_4,_2):((_1,_0),_32,_2048)
thread_layout_C_smem_tensor_B_tiled_copy
ptr[16b](0x57b7b93268d0) o ((_8,_1),_4,_2):((_1,_0),_32,_2048)

作为目的 tensor 的 thread_layout_C_register_tensor_A/B(由 ThrMMA<…>::partition_fragment_A/B 获取),其信息如下:

thread_layout_C_register_tensor_A
ptr[16b](0x7ffc34e465f0) o ((_2,_2,_2),_4,_2):((_1,_2,_4),_8,_32)
thread_layout_C_register_tensor_B
ptr[16b](0x7ffc34e46670) o ((_2,_2),_8,_2):((_1,_2),_4,_32)

所以需要对目的 tensor 进行调整,使得其 shape 与源 tensor 一致。调整之后的目的 tensor thread_layout_C_register_tensor_A/B_copy_view 的信息如下:

thread_layout_C_register_tensor_A_copy_view
ptr[16b](0x7ffd7a129350) o ((_8,_1),_4,_2):((_1,_0),_8,_32)
thread_layout_C_register_tensor_B_copy_view
ptr[16b](0x7ffd7a1293d0) o ((_8,_1),_4,_2):((_1,_0),_8,_32)

参考及资料




    Enjoy Reading This Article?

    Here are some more articles you might like to read next:

  • al-folio 本地部署记录(Ubuntu 24.04)