v4.5 tag update (#3202)

* Python DSL examples reorganization.

* v4.5 tag update.
This commit is contained in:
Junkai-Wu
2026-05-06 08:55:27 +08:00
committed by GitHub
parent f74fea9ce3
commit cb37157db5
351 changed files with 36688 additions and 8117 deletions

View File

@@ -0,0 +1,25 @@
# CUTLASS Tutorial Examples for Blackwell GEMM
This folder contains tutorial examples demonstrating how to write performant GEMM (General Matrix Multiplication) kernels using Tensor Cores on NVIDIA Blackwell GPUs.
## Overview
The examples showcase different scenarios and optimization techniques for implementing GEMM operations:
- Basic FP16 GEMM implementation
- Software Pipeline optimizations
- Tensor Core utilization
- Thread/warp/block level parallelism
## Examples
### tutorial_fp16_gemm_0.py
A basic example showing:
- FP16 GEMM implementation using Tensor Cores
- TMA (Tensor Memory Access) for efficient data loading
- SMEM (Shared Memory) layouts and access patterns
- Usage of ``cutlass.range(..., prefetch_stages=...)`` to replace boilerplate code for multi-stage software pipeline
With some minor optimization tricks
- Tiling Epilogue to avoid bursty write out and reduce register pressure

View File

@@ -0,0 +1,441 @@
# SPDX-FileCopyrightText: Copyright (c) 2024 - 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# NVIDIA CORPORATION, its affiliates and licensors retain all intellectual
# property and proprietary rights in and to this material, related
# documentation and any modifications thereto. Any use, reproduction,
# disclosure or distribution of this material and related documentation
# without an express license agreement from NVIDIA CORPORATION or
# its affiliates is strictly prohibited.
import argparse
from typing import Tuple
import cutlass
import cutlass.cute as cute
import cutlass.utils as utils
import cutlass.pipeline as pipeline
from cutlass.cute.nvgpu import cpasync, tcgen05
import cutlass.utils.blackwell_helpers as sm100_utils
from cutlass.cute.runtime import from_dlpack
"""
The first tutorial GEMM demonstrating a simple kernel implementation in CuTeDSL
This dense GEMM kernel is implemented in just over 200 lines of code.
With large tile sizes, it can achieve very high performance on 8k×8k×8k problem sizes.
It can serve as a starting point to help users quickly experiment
with optimizations for challenges that may arise with other problem sizes.
To run this example:
.. code-block:: bash
python examples/blackwell/tutorial_gemm/fp16_gemm_0.py \
--mnk 8192,8192,8192
Constraints for this example:
* The problem size of m and n must be divisible by the tile size m & n (128, 256)
"""
io_dtype = cutlass.Float16
acc_dtype = cutlass.Float32
mma_inst_shape_mnk = (128, 256, 16)
mma_tiler_mnk = (128, 256, 64)
threads_per_cta = 128
# Pipeline stage configuration
ab_stages = 4
acc_stage = 1
@cute.struct
class SharedStorage:
ab_mbar_ptr: cute.struct.MemRange[cutlass.Int64, ab_stages * 2]
acc_mbar_ptr: cute.struct.MemRange[cutlass.Int64, acc_stage * 2]
tmem_holding_buf: cutlass.Int32
@cute.kernel
def kernel(
tiled_mma: cute.TiledMma,
tma_atom_a: cute.CopyAtom,
mA_mkl: cute.Tensor,
tma_atom_b: cute.CopyAtom,
mB_nkl: cute.Tensor,
mC_mnl: cute.Tensor,
a_smem_layout: cute.ComposedLayout,
b_smem_layout: cute.ComposedLayout,
):
# Current thread/warp/block coordinates
tidx, _, _ = cute.arch.thread_idx()
warp_idx = cute.arch.warp_idx()
warp_idx = cute.arch.make_warp_uniform(warp_idx)
bidx, bidy, _ = cute.arch.block_idx()
mma_coord_mnk = (bidx, bidy, None)
#
# 1. Prepare args
#
# Allocate SMEM
smem = cutlass.utils.SmemAllocator()
storage = smem.allocate(SharedStorage)
sA = smem.allocate_tensor(
element_type=io_dtype,
layout=a_smem_layout.outer,
byte_alignment=128,
swizzle=a_smem_layout.inner,
)
sB = smem.allocate_tensor(
element_type=io_dtype,
layout=b_smem_layout.outer,
byte_alignment=128,
swizzle=b_smem_layout.inner,
)
# Allocate all TMEM columns
tmem_alloc_barrier = pipeline.NamedBarrier(
barrier_id=1,
num_threads=threads_per_cta,
)
tmem = utils.TmemAllocator(
storage.tmem_holding_buf.ptr,
barrier_for_retrieve=tmem_alloc_barrier,
)
num_tmem_cols = 512
tmem.allocate(num_tmem_cols)
# Prefetch tma descriptor
if warp_idx == 0:
cpasync.prefetch_descriptor(tma_atom_a)
cpasync.prefetch_descriptor(tma_atom_b)
# Pipeline configuration
num_tma_copy_bytes = cute.size_in_bytes(
io_dtype, cute.select(a_smem_layout, mode=[0, 1, 2])
) + cute.size_in_bytes(io_dtype, cute.select(b_smem_layout, mode=[0, 1, 2]))
ab_producer, ab_consumer = pipeline.PipelineTmaUmma.create(
num_stages=ab_stages,
producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),
consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),
tx_count=num_tma_copy_bytes,
barrier_storage=storage.ab_mbar_ptr.data_ptr(),
).make_participants()
acc_producer, acc_consumer = pipeline.PipelineUmmaAsync.create(
num_stages=acc_stage,
producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),
consumer_group=pipeline.CooperativeGroup(
pipeline.Agent.Thread,
threads_per_cta,
),
barrier_storage=storage.acc_mbar_ptr.data_ptr(),
).make_participants()
# Partition tensors for MMA and make fragments
# (bM, bK, RestK)
gA = cute.local_tile(mA_mkl, mma_tiler_mnk, mma_coord_mnk, proj=(1, None, 1))
# (bN, bK, RestK)
gB = cute.local_tile(mB_nkl, mma_tiler_mnk, mma_coord_mnk, proj=(None, 1, 1))
# (bM, bN)
gC = cute.local_tile(mC_mnl, mma_tiler_mnk, mma_coord_mnk, proj=(1, 1, None))
thr_mma = tiled_mma.get_slice(0)
# (MMA, MMA_M, MMA_K)
tCgA = thr_mma.partition_A(gA)
# (MMA, MMA_N, MMA_K)
tCgB = thr_mma.partition_B(gB)
# (MMA, MMA_M, MMA_N)
tCgC = thr_mma.partition_C(gC)
# (MMA, MMA_M, MMA_K)
tCrA = tiled_mma.make_fragment_A(sA)
# (MMA, MMA_N, MMA_K)
tCrB = tiled_mma.make_fragment_B(sB)
# (MMA, MMA_M, MMA_N)
acc_shape = tiled_mma.partition_shape_C(mma_tiler_mnk[:2])
# (MMA, MMA_M, MMA_N)
tCtAcc = tiled_mma.make_fragment_C(acc_shape)
# Partition tensors for TMA; This requires the tensors partitioned for MMA
tAsA, tAgA = cute.nvgpu.cpasync.tma_partition(
tma_atom_a,
0,
cute.make_layout(1),
cute.group_modes(sA, 0, 3),
cute.group_modes(tCgA, 0, 3),
)
tBsB, tBgB = cute.nvgpu.cpasync.tma_partition(
tma_atom_b,
0,
cute.make_layout(1),
cute.group_modes(sB, 0, 3),
cute.group_modes(tCgB, 0, 3),
)
# CTA-wide sync before retrieving the pointer to the start of the allocated TMEM
# Only warp 0 does the allocation so we need to sync before retrieving the TMEM start address
tmem.wait_for_alloc()
tmem_ptr = tmem.retrieve_ptr(acc_dtype)
# Swap the pointer in tCtAcc
tCtAcc = cute.make_tensor(tmem_ptr, tCtAcc.layout)
subtile_cnt = 4
# (EpiTile)
epi_tiler = (
(cute.size(tCtAcc, mode=[0, 0]), cute.size(tCtAcc, mode=[0, 1]) // subtile_cnt),
)
# (EpiTile, NumTiles)
tCtAcc_epi = cute.zipped_divide(tCtAcc, epi_tiler)
# (EpiTile, NumTiles)
gC_epi = cute.zipped_divide(tCgC, epi_tiler)
# Every thread loads 64 x fp32
tmem_atom = cute.make_copy_atom(
tcgen05.Ld32x32bOp(tcgen05.Repetition.x64),
cutlass.Float32,
)
tmem_tiled_copy = tcgen05.make_tmem_copy(tmem_atom, tCtAcc_epi[None, 0])
tmem_thr_copy = tmem_tiled_copy.get_slice(tidx)
# (TmemCpy,NumTmemCpy,NumTiles)
tDtC = tmem_thr_copy.partition_S(tCtAcc_epi)
# (TmemCpy,NumTmemCpy,NumTiles)
tDgC = tmem_thr_copy.partition_D(gC_epi)
# (TmemCpy,NumTmemCpy)
tCrAcc = cute.make_rmem_tensor(tDgC[None, None, 0].shape, acc_dtype)
# (TmemCpy,NumTmemCpy)
tCrC = cute.make_rmem_tensor(tDgC[None, None, 0].shape, io_dtype)
#
# 2. Main loop
#
num_k_tiles = cute.size(gA, mode=[2])
if warp_idx == 0:
# Wait for a empty accumulator buffer
acc_empty = acc_producer.acquire_and_advance()
for k_tile_idx in cutlass.range(num_k_tiles, prefetch_stages=ab_stages - 2):
# Issue TMA loads
ab_empty = ab_producer.acquire_and_advance()
cute.copy(
tma_atom_a,
tAgA[(None, ab_empty.count)],
tAsA[(None, ab_empty.index)],
tma_bar_ptr=ab_empty.barrier,
)
cute.copy(
tma_atom_b,
tBgB[(None, ab_empty.count)],
tBsB[(None, ab_empty.index)],
tma_bar_ptr=ab_empty.barrier,
)
# Execute one K-block worth of MMA instructions
ab_full = ab_consumer.wait_and_advance()
num_k_blocks = cute.size(tCrA, mode=[2])
for k_block_idx in cutlass.range_constexpr(num_k_blocks):
k_block_coord = (None, None, k_block_idx, ab_full.index)
cute.gemm(
tiled_mma,
tCtAcc,
tCrA[k_block_coord],
tCrB[k_block_coord],
tCtAcc,
)
tiled_mma.set(tcgen05.Field.ACCUMULATE, True)
# Signal that the A/B buffers have been consumed and are ready for the next load
ab_full.release()
# Signal that the accumulator is fully computed
acc_empty.commit()
#
# 3. Epilogue
#
# Release TMEM allocation lock
tmem.relinquish_alloc_permit()
# Wait for the accumulator buffer to be full
acc_full = acc_consumer.wait_and_advance()
# TMEM -> RMEM -> GEMM
# Sub-tiling for better instruction-level parallelism
for i in cutlass.range(cute.size(tDtC, mode=[2])):
cute.copy(tmem_tiled_copy, tDtC[None, None, i], tCrAcc)
tCrC.store(tCrAcc.load().to(io_dtype))
cute.autovec_copy(tCrC, tDgC[None, None, i])
acc_full.release()
# Deallocate TMEM
pipeline.sync(barrier_id=1)
tmem.free(tmem_ptr)
@cute.jit
def host_function(a: cute.Tensor, b: cute.Tensor, c: cute.Tensor):
# Construct tiled MMA
op = tcgen05.MmaF16BF16Op(
io_dtype,
acc_dtype,
mma_inst_shape_mnk,
tcgen05.CtaGroup.ONE,
tcgen05.OperandSource.SMEM,
tcgen05.OperandMajorMode.K,
tcgen05.OperandMajorMode.K,
)
tiled_mma = cute.make_tiled_mma(op)
# Construct SMEM layouts for A and B
a_smem_layout = sm100_utils.make_smem_layout_a(
tiled_mma,
mma_tiler_mnk,
a.element_type,
ab_stages,
)
b_smem_layout = sm100_utils.make_smem_layout_b(
tiled_mma,
mma_tiler_mnk,
b.element_type,
ab_stages,
)
a_smem_layout_one_stage = cute.select(a_smem_layout, mode=[0, 1, 2])
b_smem_layout_one_stage = cute.select(b_smem_layout, mode=[0, 1, 2])
# Construct TMA load atoms
op = cute.nvgpu.cpasync.CopyBulkTensorTileG2SOp(tcgen05.CtaGroup.ONE)
a_tma_atom, a_tma_tensor = cute.nvgpu.make_tiled_tma_atom_A(
op,
a,
a_smem_layout_one_stage,
mma_tiler_mnk,
tiled_mma,
)
b_tma_atom, b_tma_tensor = cute.nvgpu.make_tiled_tma_atom_B(
op,
b,
b_smem_layout_one_stage,
mma_tiler_mnk,
tiled_mma,
)
# Pretty prints kernel attributes useful for debugging
# print(f"a = {cute.pretty_str(a)}")
# print(f"b = {cute.pretty_str(b)}")
# print(f"c = {cute.pretty_str(c)}")
# print(f"tiled_mma = {cute.pretty_str(tiled_mma)}")
# print(f"a_tma_atom = {cute.pretty_str(a_tma_atom)}")
# print(f"b_tma_atom = {cute.pretty_str(b_tma_atom)}")
# print(f"a_tma_tensor = {cute.pretty_str(a_tma_tensor)}")
# print(f"b_tma_tensor = {cute.pretty_str(b_tma_tensor)}")
# Launch the kernel
grid_shape = cute.ceil_div((*c.layout.shape, 1), mma_tiler_mnk[:2])
kernel(
tiled_mma,
a_tma_atom,
a_tma_tensor,
b_tma_atom,
b_tma_tensor,
c,
a_smem_layout,
b_smem_layout,
).launch(
grid=grid_shape,
block=(threads_per_cta, 1, 1),
)
def run_dense_gemm(
mnk: Tuple[int, int, int],
tolerance: float,
):
global torch, cutlass_torch
import torch
import cutlass.torch as cutlass_torch
print("===================================================================")
print("Running Blackwell fp16 GEMM example 0 with:")
print(f" mnk: {mnk}")
print(f" tolerance: {tolerance}")
print("===================================================================")
print()
m, n, k = mnk
torch.manual_seed(1111)
# Make K-major tensors (torch tensors are row-major)
def make_tensors(mn, k, dtype):
shape = (mn, k)
return (
torch.empty(*shape, dtype=torch.int32)
.random_(-2, 2)
.to(dtype=dtype, device="cuda")
)
a = make_tensors(m, k, cutlass_torch.dtype(io_dtype))
b = make_tensors(n, k, cutlass_torch.dtype(io_dtype))
c = make_tensors(m, n, cutlass_torch.dtype(io_dtype))
a_tensor = (
from_dlpack(a, assumed_align=32)
.mark_layout_dynamic(leading_dim=1)
.mark_compact_shape_dynamic(mode=1, divisibility=k)
)
b_tensor = (
from_dlpack(b, assumed_align=32)
.mark_layout_dynamic(leading_dim=1)
.mark_compact_shape_dynamic(mode=1, divisibility=k)
)
c_tensor = (
from_dlpack(c, assumed_align=32)
.mark_layout_dynamic(leading_dim=1)
.mark_compact_shape_dynamic(mode=1, divisibility=n)
)
# Entry point to the host JIT function
host_function(a_tensor, b_tensor, c_tensor, no_cache=True)
# Compute reference result and verify
ref = (torch.einsum("mk,nk->mn", a.to(torch.float32), b.to(torch.float32))).cpu()
torch.testing.assert_close(
c.cpu(), ref.to(cutlass_torch.dtype(io_dtype)), atol=tolerance, rtol=1e-05
)
if __name__ == "__main__":
def parse_comma_separated_ints(s: str):
try:
return [int(x.strip()) for x in s.split(",")]
except ValueError:
raise argparse.ArgumentTypeError(
"Invalid format. Expected comma-separated integers."
)
from cuda.bindings import driver as cu_driver
cu_driver.cuInit(0)
err, device_count = cu_driver.cuDeviceGetCount()
if err != cu_driver.CUresult.CUDA_SUCCESS or device_count < 1:
raise RuntimeError("A GPU is required to run this example")
parser = argparse.ArgumentParser(description="Blackwell fp16 GEMM example 0")
parser.add_argument(
"--mnk",
type=parse_comma_separated_ints,
default=[8192, 8192, 8192],
help="MNK dimensions (comma-separated)",
)
parser.add_argument(
"--tolerance", type=float, default=1e-01, help="Tolerance for validation"
)
args = parser.parse_args()
if len(args.mnk) != 3:
parser.error("--mnk must contain exactly 3 values")
if args.mnk[0] % mma_tiler_mnk[0] != 0 or args.mnk[1] % mma_tiler_mnk[1] != 0:
parser.error("m n must be divisible by mma_tiler_mn")
run_dense_gemm(
args.mnk,
args.tolerance,
)
print("PASS")

View File

@@ -0,0 +1,533 @@
# SPDX-FileCopyrightText: Copyright (c) 2024 - 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# NVIDIA CORPORATION, its affiliates and licensors retain all intellectual
# property and proprietary rights in and to this material, related
# documentation and any modifications thereto. Any use, reproduction,
# disclosure or distribution of this material and related documentation
# without an express license agreement from NVIDIA CORPORATION or
# its affiliates is strictly prohibited.
# This is the second tutorial GEMM. It builds on the first tutorial by adding 2CTA MMA
# instructions with a 2x1 cluster.
import argparse
from typing import Tuple
import cutlass
import cutlass.cute as cute
import cutlass.utils as utils
import cutlass.pipeline as pipeline
from cutlass.cute.nvgpu import cpasync, tcgen05
import cutlass.utils.blackwell_helpers as sm100_utils
from cutlass.cute.runtime import from_dlpack
"""
The second tutorial GEMM demonstrating a simple kernel implementation in CuTeDSL
With large tile sizes, it can as well achieve very high performance on 8kx8kx8k problem sizes.
Compared with fp16_gemm_0.py, this example adds 2CTA MMA & TMA multicast supports.
For fp16_gemm_0.py running at relative high SM frequency, the dram latency will be a potential performance issue.
This example can achieve better performance than fp16_gemm_0.py due to:
1. The 2CTA MMA can reduce B tensor smem size which allows larger ab stages to hide dram latency.
For both 1CTA & 2CTA, one stage of A tensor smem size is 128x64xsizeof(float16)=16KB
Situation for B is different.
For 1CTA, one stage of B tensor smem size is 256x64xsizeof(float16)=32KB,
while for 2CTA, we can only take half size, i.e. 16KB.
So, the maxmimum AB stage for 1CTA is 227 // (16 + 32) = 4, while for 2CTA is 227 // (16 + 16) = 7.
The latency hiding capability is 512 * (4 - 1) = 1.5K cycles for 1CTA, while 512 * (7 - 1) = 3K cycles for 2CTA.
2. The L2 traffic is reduced due to the TMA multicast.
For a (m, n) cluster shape, the L2 traffic for one tile is 16KB / n + 32KB / m.
16KB / 1 + 32KB / 2 = 24KB for 2x1 cluster shape
16KB / 4 + 32KB / 4 = 12KB for 4x4 cluster shape
If no TMA multicast enabled, the L2 traffic for one tile is less than 16KB + 32KB = 48KB, which depends on hardware optimization.
The first one can provide large latency hiding capability while the second one can reduce the data ready time.
These two factors should be considered for latency/memory throughput bound cases.
To run this example:
.. code-block:: bash
python examples/blackwell/tutorial_gemm/fp16_gemm_1.py \
--mnk 8192,8192,8192
Constraints for this example:
* The problem size of m and n must be divisible by the tile size m & n (256, 256)
"""
io_dtype = cutlass.Float16
acc_dtype = cutlass.Float32
cluster_shape_mnk = (2, 1, 1)
mma_inst_shape_mnk = (256, 256, 16)
mma_tiler_mnk = (256, 256, 64)
threads_per_cta = 128
# Pipeline stage configuration
ab_stages = 7
acc_stage = 1
@cute.struct
class SharedStorage:
ab_mbar_ptr: cute.struct.MemRange[cutlass.Int64, ab_stages * 2]
acc_mbar_ptr: cute.struct.MemRange[cutlass.Int64, acc_stage * 2]
tmem_dealloc_mbar: cutlass.Int64
tmem_holding_buf: cutlass.Int32
@cute.kernel()
def kernel(
tiled_mma: cute.TiledMma,
tma_atom_a: cute.CopyAtom,
mA_mkl: cute.Tensor,
tma_atom_b: cute.CopyAtom,
mB_nkl: cute.Tensor,
mC_mnl: cute.Tensor,
a_smem_layout: cute.ComposedLayout,
b_smem_layout: cute.ComposedLayout,
cta_layout_vmnk: cute.Layout,
):
# Current thread/warp/block coordinates
tidx, _, _ = cute.arch.thread_idx()
warp_idx = cute.arch.warp_idx()
warp_idx = cute.arch.make_warp_uniform(warp_idx)
bidx, bidy, _ = cute.arch.block_idx()
cta_rank_in_cluster = cute.arch.block_idx_in_cluster()
cta_in_cluster_coord_vmnk = cta_layout_vmnk.get_flat_coord(cta_rank_in_cluster)
mma_coord_vmnk = (
bidx % cute.size(cta_layout_vmnk, mode=[0]),
bidx // cute.size(cta_layout_vmnk, mode=[0]),
bidy,
None,
)
mma_coord_mnk = mma_coord_vmnk[1:]
#
# 1. Prepare args
#
# Allocate SMEM
smem = cutlass.utils.SmemAllocator()
storage = smem.allocate(SharedStorage)
sA = smem.allocate_tensor(
element_type=io_dtype,
layout=a_smem_layout.outer,
byte_alignment=128,
swizzle=a_smem_layout.inner,
)
sB = smem.allocate_tensor(
element_type=io_dtype,
layout=b_smem_layout.outer,
byte_alignment=128,
swizzle=b_smem_layout.inner,
)
# Prefetch tma descriptor
if warp_idx == 0:
cpasync.prefetch_descriptor(tma_atom_a)
cpasync.prefetch_descriptor(tma_atom_b)
# Pipeline configuration
num_tma_copy_bytes = (
cute.size_in_bytes(io_dtype, cute.select(a_smem_layout, mode=[0, 1, 2]))
+ cute.size_in_bytes(io_dtype, cute.select(b_smem_layout, mode=[0, 1, 2]))
) * cute.size(cta_layout_vmnk, mode=[0])
num_mcast_ctas_a = cute.size(cta_layout_vmnk.shape[2])
num_mcast_ctas_b = cute.size(cta_layout_vmnk.shape[1])
num_tma_producer = num_mcast_ctas_a + num_mcast_ctas_b - 1
ab_producer, ab_consumer = pipeline.PipelineTmaUmma.create(
num_stages=ab_stages,
producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),
consumer_group=pipeline.CooperativeGroup(
pipeline.Agent.Thread, num_tma_producer
),
tx_count=num_tma_copy_bytes,
barrier_storage=storage.ab_mbar_ptr.data_ptr(),
cta_layout_vmnk=cta_layout_vmnk,
).make_participants()
acc_producer, acc_consumer = pipeline.PipelineUmmaAsync.create(
num_stages=acc_stage,
producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),
consumer_group=pipeline.CooperativeGroup(
pipeline.Agent.Thread,
cute.size(cta_layout_vmnk, mode=[0]) * threads_per_cta,
),
barrier_storage=storage.acc_mbar_ptr.data_ptr(),
cta_layout_vmnk=cta_layout_vmnk,
).make_participants()
# Partition tensors for MMA and make fragments
# (bM, bK, RestK)
gA = cute.local_tile(mA_mkl, mma_tiler_mnk, mma_coord_mnk, proj=(1, None, 1))
# (bN, bK, RestK)
gB = cute.local_tile(mB_nkl, mma_tiler_mnk, mma_coord_mnk, proj=(None, 1, 1))
# (bM, bN)
gC = cute.local_tile(mC_mnl, mma_tiler_mnk, mma_coord_mnk, proj=(1, 1, None))
thr_mma = tiled_mma.get_slice(mma_coord_vmnk[0])
# (MMA, MMA_M, MMA_K)
tCgA = thr_mma.partition_A(gA)
# (MMA, MMA_N, MMA_K)
tCgB = thr_mma.partition_B(gB)
# (MMA, MMA_M, MMA_N)
tCgC = thr_mma.partition_C(gC)
# (MMA, MMA_M, MMA_K)
tCrA = tiled_mma.make_fragment_A(sA)
# (MMA, MMA_N, MMA_K)
tCrB = tiled_mma.make_fragment_B(sB)
# (MMA, MMA_M, MMA_N)
acc_shape = tiled_mma.partition_shape_C(mma_tiler_mnk[:2])
# (MMA, MMA_M, MMA_N)
tCtAcc = tiled_mma.make_fragment_C(acc_shape)
# Partition tensors for TMA; This requires the tensors partitioned for MMA
tAsA, tAgA = cute.nvgpu.cpasync.tma_partition(
tma_atom_a,
cta_in_cluster_coord_vmnk[2],
cute.make_layout(cute.size(cta_layout_vmnk, mode=[2])),
cute.group_modes(sA, 0, 3),
cute.group_modes(tCgA, 0, 3),
)
tBsB, tBgB = cute.nvgpu.cpasync.tma_partition(
tma_atom_b,
cta_in_cluster_coord_vmnk[1],
cute.make_layout(cute.size(cta_layout_vmnk, mode=[1])),
cute.group_modes(sB, 0, 3),
cute.group_modes(tCgB, 0, 3),
)
tma_mcast_mask_a = cute.nvgpu.cpasync.create_tma_multicast_mask(
cta_layout_vmnk, cta_in_cluster_coord_vmnk, mcast_mode=2
)
tma_mcast_mask_b = cute.nvgpu.cpasync.create_tma_multicast_mask(
cta_layout_vmnk, cta_in_cluster_coord_vmnk, mcast_mode=1
)
# Allocate TMEM and swap the pointer in tCtAcc
tmem_alloc_barrier = pipeline.NamedBarrier(
barrier_id=1,
num_threads=threads_per_cta,
)
tmem = utils.TmemAllocator(
storage.tmem_holding_buf.ptr,
barrier_for_retrieve=tmem_alloc_barrier,
is_two_cta=cute.size(cta_layout_vmnk, mode=[0]) > 1,
two_cta_tmem_dealloc_mbar_ptr=storage.tmem_dealloc_mbar.ptr,
)
num_tmem_cols = 512
tmem.allocate(num_tmem_cols)
# CTA-wide sync before retrieving the pointer to the start of the allocated TMEM
# Only warp 0 does the allocation so we need to sync before retrieving the TMEM start address
tmem.wait_for_alloc()
tmem_ptr = tmem.retrieve_ptr(acc_dtype)
# Swap the pointer in tCtAcc
tCtAcc = cute.make_tensor(tmem_ptr, tCtAcc.layout)
subtile_cnt = 4
# (EpiTile)
epi_tiler = (
(cute.size(tCtAcc, mode=[0, 0]), cute.size(tCtAcc, mode=[0, 1]) // subtile_cnt),
)
# (EpiTile, NumTiles)
tCtAcc_epi = cute.zipped_divide(tCtAcc, epi_tiler)
# (EpiTile, NumTiles)
gC_epi = cute.zipped_divide(tCgC, epi_tiler)
# Every thread loads 64 x fp32
tmem_atom = cute.make_copy_atom(
tcgen05.Ld32x32bOp(tcgen05.Repetition.x64),
cutlass.Float32,
)
tmem_tiled_copy = tcgen05.make_tmem_copy(tmem_atom, tCtAcc_epi[None, 0])
tmem_thr_copy = tmem_tiled_copy.get_slice(tidx)
# (TmemCpy,NumTmemCpy,NumTiles)
tDtC = tmem_thr_copy.partition_S(tCtAcc_epi)
# (TmemCpy,NumTmemCpy,NumTiles)
tDgC = tmem_thr_copy.partition_D(gC_epi)
# (TmemCpy,NumTmemCpy)
tCrAcc = cute.make_rmem_tensor(tDgC[None, None, 0].shape, acc_dtype)
# (TmemCpy,NumTmemCpy)
tCrC = cute.make_rmem_tensor(tDgC[None, None, 0].shape, io_dtype)
#
# 2. Main loop
#
is_leader_cta = mma_coord_vmnk[0] == 0
num_k_tiles = cute.size(gA, mode=[2])
if warp_idx == 0:
# Wait for a empty accumulator buffer
if is_leader_cta:
acc_producer.acquire_and_advance()
for _ in cutlass.range(num_k_tiles, prefetch_stages=ab_stages - 2):
# Issue TMA loads
ab_empty = ab_producer.acquire_and_advance()
cute.copy(
tma_atom_a,
tAgA[(None, ab_empty.count)],
tAsA[(None, ab_empty.index)],
tma_bar_ptr=ab_empty.barrier,
mcast_mask=tma_mcast_mask_a,
)
cute.copy(
tma_atom_b,
tBgB[(None, ab_empty.count)],
tBsB[(None, ab_empty.index)],
tma_bar_ptr=ab_empty.barrier,
mcast_mask=tma_mcast_mask_b,
)
# Execute one K-block worth of MMA instructions
if is_leader_cta:
ab_full = ab_consumer.wait_and_advance()
# Execute one K-block worth of MMA instructions
num_k_blocks = cute.size(tCrA, mode=[2])
for k_block_idx in cutlass.range_constexpr(num_k_blocks):
k_block_coord = (None, None, k_block_idx, ab_full.index)
cute.gemm(
tiled_mma,
tCtAcc,
tCrA[k_block_coord],
tCrB[k_block_coord],
tCtAcc,
)
tiled_mma.set(tcgen05.Field.ACCUMULATE, True)
ab_full.release()
# Signal that the accumulator is fully computed
if is_leader_cta:
acc_producer.commit()
#
# 3. Epilogue
#
# Release TMEM allocation lock
tmem.relinquish_alloc_permit()
# Wait for the accumulator buffer to be full
acc_full = acc_consumer.wait_and_advance()
# TMEM -> RMEM -> GEMM
# Sub-tiling for better instruction-level parallelism
for i in cutlass.range(cute.size(tDtC, mode=[2])):
cute.copy(tmem_tiled_copy, tDtC[None, None, i], tCrAcc)
tCrC.store(tCrAcc.load().to(io_dtype))
cute.autovec_copy(tCrC, tDgC[None, None, i])
acc_full.release()
# Ensure used buffers are properly synchronized before producer exit.
# This could avoid the invalid dsmem access due to early leading CTA exit.
if warp_idx == 0:
ab_producer.tail()
if is_leader_cta:
acc_producer.tail()
# Deallocate TMEM
pipeline.sync(barrier_id=1)
tmem.free(tmem_ptr)
@cute.jit
def host_function(
a: cute.Tensor,
b: cute.Tensor,
c: cute.Tensor,
):
# Construct tiled MMA
op = tcgen05.MmaF16BF16Op(
io_dtype,
acc_dtype,
mma_inst_shape_mnk,
tcgen05.CtaGroup.TWO,
tcgen05.OperandSource.SMEM,
tcgen05.OperandMajorMode.K,
tcgen05.OperandMajorMode.K,
)
tiled_mma = cute.make_tiled_mma(op)
# Construct SMEM layouts for A and B
a_smem_layout = sm100_utils.make_smem_layout_a(
tiled_mma,
mma_tiler_mnk,
a.element_type,
ab_stages,
)
b_smem_layout = sm100_utils.make_smem_layout_b(
tiled_mma,
mma_tiler_mnk,
b.element_type,
ab_stages,
)
a_smem_layout_one_stage = cute.select(a_smem_layout, mode=[0, 1, 2])
b_smem_layout_one_stage = cute.select(b_smem_layout, mode=[0, 1, 2])
# Construct the VMNK layout
cta_layout_mnk = cute.make_layout(cluster_shape_mnk)
cta_layout_vmnk = cute.tiled_divide(cta_layout_mnk, (tiled_mma.thr_id,))
# Construct TMA load atoms
op = cute.nvgpu.cpasync.CopyBulkTensorTileG2SMulticastOp(tcgen05.CtaGroup.TWO)
a_tma_atom, a_tma_tensor = cute.nvgpu.make_tiled_tma_atom_A(
op,
a,
a_smem_layout_one_stage,
mma_tiler_mnk,
tiled_mma,
cta_layout_vmnk.shape, # take the layout and extract the shape internally
)
b_tma_atom, b_tma_tensor = cute.nvgpu.make_tiled_tma_atom_B(
op,
b,
b_smem_layout_one_stage,
mma_tiler_mnk,
tiled_mma,
cta_layout_vmnk.shape,
)
grid_shape = cute.round_up(
cute.ceil_div(
(*c.layout.shape, 1), (mma_tiler_mnk[0] // 2, *mma_tiler_mnk[1:])
),
cluster_shape_mnk,
)
# Pretty prints kernel attributes useful for debugging
# print(f"a = {cute.pretty_str(a)}")
# print(f"b = {cute.pretty_str(b)}")
# print(f"c = {cute.pretty_str(c)}")
# print(f"tiled_mma = {cute.pretty_str(tiled_mma)}")
# print(f"a_smem_layout = {cute.pretty_str(a_smem_layout)}")
# print(f"b_smem_layout = {cute.pretty_str(b_smem_layout)}")
# print(f"cta_layout_mnk = {cute.pretty_str(cta_layout_mnk)}")
# print(f"cta_layout_vmnk = {cute.pretty_str(cta_layout_vmnk)}")
# print(f"a_tma_atom = {cute.pretty_str(a_tma_atom)}")
# print(f"b_tma_atom = {cute.pretty_str(b_tma_atom)}")
# print(f"a_tma_tensor = {cute.pretty_str(a_tma_tensor)}")
# print(f"b_tma_tensor = {cute.pretty_str(b_tma_tensor)}")
# cute.printf("grid_shape = {}", grid_shape)
# Launch the kernel
kernel(
tiled_mma,
a_tma_atom,
a_tma_tensor,
b_tma_atom,
b_tma_tensor,
c,
a_smem_layout,
b_smem_layout,
cta_layout_vmnk,
).launch(
grid=grid_shape,
block=[threads_per_cta, 1, 1],
cluster=cluster_shape_mnk,
)
def run_dense_gemm(
mnk: Tuple[int, int, int],
tolerance: float,
):
global torch, cutlass_torch
import torch
import cutlass.torch as cutlass_torch
print("===================================================================")
print("Running Blackwell fp16 GEMM example 1 with:")
print(f" mnk: {mnk}")
print(f" tolerance: {tolerance}")
print("===================================================================")
print()
m, n, k = mnk
torch.manual_seed(1111)
# Make K-major tensors (torch tensors are row-major)
def make_tensors(mn, k, dtype):
shape = (mn, k)
return (
torch.empty(*shape, dtype=torch.int32)
.random_(-2, 2)
.to(device="cuda", dtype=dtype)
)
a = make_tensors(m, k, cutlass_torch.dtype(io_dtype))
b = make_tensors(n, k, cutlass_torch.dtype(io_dtype))
c = make_tensors(m, n, cutlass_torch.dtype(io_dtype))
a_tensor = (
from_dlpack(a, assumed_align=32)
.mark_layout_dynamic(leading_dim=1)
.mark_compact_shape_dynamic(mode=1, divisibility=k)
)
b_tensor = (
from_dlpack(b, assumed_align=32)
.mark_layout_dynamic(leading_dim=1)
.mark_compact_shape_dynamic(mode=1, divisibility=k)
)
c_tensor = (
from_dlpack(c, assumed_align=32)
.mark_layout_dynamic(leading_dim=1)
.mark_compact_shape_dynamic(mode=1, divisibility=n)
)
# Entry point to the host JIT function
host_function(
a_tensor,
b_tensor,
c_tensor,
no_cache=True,
)
# Compute reference result and verify
ref = (torch.einsum("mk,nk->mn", a.to(torch.float32), b.to(torch.float32))).cpu()
torch.testing.assert_close(
c.cpu(), ref.to(cutlass_torch.dtype(io_dtype)), atol=tolerance, rtol=1e-05
)
if __name__ == "__main__":
def parse_comma_separated_ints(s: str):
try:
return [int(x.strip()) for x in s.split(",")]
except ValueError:
raise argparse.ArgumentTypeError(
"Invalid format. Expected comma-separated integers."
)
from cuda.bindings import driver as cu_driver
cu_driver.cuInit(0)
err, device_count = cu_driver.cuDeviceGetCount()
if err != cu_driver.CUresult.CUDA_SUCCESS or device_count < 1:
raise RuntimeError("A GPU is required to run this example")
parser = argparse.ArgumentParser(description="Blackwell fp16 GEMM example 1")
parser.add_argument(
"--mnk",
type=parse_comma_separated_ints,
default=[8192, 8192, 8192],
help="MNK dimensions (comma-separated)",
)
parser.add_argument(
"--tolerance", type=float, default=1e-01, help="Tolerance for validation"
)
args = parser.parse_args()
if len(args.mnk) != 3:
parser.error("--mnk must contain exactly 3 values")
if args.mnk[0] % mma_tiler_mnk[0] != 0 or args.mnk[1] % mma_tiler_mnk[1] != 0:
parser.error("m n must be divisible by mma_tiler_mn")
run_dense_gemm(
args.mnk,
args.tolerance,
)
print("PASS")

View File

@@ -0,0 +1,687 @@
# SPDX-FileCopyrightText: Copyright (c) 2024 - 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# NVIDIA CORPORATION, its affiliates and licensors retain all intellectual
# property and proprietary rights in and to this material, related
# documentation and any modifications thereto. Any use, reproduction,
# disclosure or distribution of this material and related documentation
# without an express license agreement from NVIDIA CORPORATION or
# its affiliates is strictly prohibited.
# This is the third tutorial GEMM. It further enhances the second tutorial by adding warp
# specialization for TMA, MMA, and epilogue warps.
import argparse
from typing import Tuple
import cutlass
import cutlass.cute as cute
import cutlass.utils as utils
import cutlass.pipeline as pipeline
from cutlass.cute.nvgpu import cpasync, tcgen05
import cutlass.utils.blackwell_helpers as sm100_utils
from cutlass.cute.runtime import from_dlpack
"""
The third tutorial GEMM demonstrates a simple kernel implementation in CuTeDSL.
It further enhances fp16_gemm_1.py by adding warp specialization for TMA, MMA, and epilogue warps.
In the epilogue warp, we use TMA store instead of regular copy to store the result from registers to global memory.
This example can achieve better performance than fp16_gemm_1.py due to:
1. We use warp specialization(WS) to overlap the memory loads and MMA computations.
Core concept of WS is to specialize warps with different tasks (e.g., DMA, MMA, epilogue),
therefore, different warps in a CTA must communicate with each other.
Warp specialization's benefit comes from task parallelism between warps in the CTA.
For example, the DMA warps proceed to start loading A/B tensors for the next K-block as soon as they finish loading the current K-block.
While the MMA warps are computing the result of the current K-block. So the dram latency is hidden.
The dram latency can also be hidden by prefetch in non-WS version,
but WS version has better instruction level parallelism as different types of instructions are issued in different warps.
For example, in non-WS version, tmem allocation and TMA loads are both issued in the same warp, TMA loads only issue after tmem allocation is finished.
But in WS version, tmem allocation and TMA loads are issued in different warps, tmem allocation can be overlapped with TMA loads.
2. We use TMA store instead of regular copy to store the results from registers to global memory.
To store the results from registers to global memory using TMA actually requires two steps:
1). Write the tile from registers to shared memory
2). Write the tile from shared memory to global memory
Here we continue to use epiolgue subtiles, one reason is that it reduces the shared memory usage in the epilogue,
and another reason is that it can hide the st.shared latency, that is the st.shared of the next subtile can be overlapped with the TMA store of the current subtile.
For large mma tile size, the mainloop performance between Non-WS and WS version could be similar if there are enough ab_stages to hide the dram latency.
The performance gain of WS version mainly comes from the prologue and epilogue in this case.
That means, if k-dimension is small, then the performance of WS version will be obviously better than non-WS version.
For small mma tile size, we may also see better mainloop performance for WS version.
This is because there are ALU instructions (preparation work for MMA) for each MMA instruction, and ALU proportion is higher for small mma tile size.
In Non-WS version, warp 0 will issue the ALU operations for both TMA and MMA instruction, while in WS version, they are issued in different warps,
so less ALU instructions are issued in MMA warp, and mma instructions can be issued more efficiently.
To run this example:
.. code-block:: bash
python examples/blackwell/tutorial_gemm/fp16_gemm_2.py \
--mnk 8192,8192,8192
Constraints for this example:
* The problem size of m and n must be divisible by the tile size m & n (256, 256)
"""
io_dtype = cutlass.Float16
acc_dtype = cutlass.Float32
use_2cta_instrs = True
cluster_shape_mnk = (2, 1, 1) if use_2cta_instrs else (1, 1, 1)
mma_inst_shape_mnk = (256, 256, 16)
mma_tiler_mnk = (256, 256, 64)
threads_in_epilogue = 128 # epilogue threads per cta
# Pipeline stage configuration
ab_stages = 6
epi_stages = 2
acc_stages = 1
@cute.struct
class SharedStorage:
ab_mbar_ptr: cute.struct.MemRange[cutlass.Int64, ab_stages * 2]
acc_mbar_ptr: cute.struct.MemRange[cutlass.Int64, acc_stages * 2]
tmem_dealloc_mbar: cutlass.Int64
tmem_holding_buffer: cutlass.Int32
@cute.kernel()
def kernel(
tiled_mma: cute.TiledMma,
tma_atom_a: cute.CopyAtom,
mA_mkl: cute.Tensor,
tma_atom_b: cute.CopyAtom,
mB_nkl: cute.Tensor,
tma_atom_c: cute.CopyAtom,
mC_mnl: cute.Tensor,
a_smem_layout: cute.ComposedLayout,
b_smem_layout: cute.ComposedLayout,
c_smem_layout_kind: cutlass.Constexpr,
epi_smem_layout_staged: cute.ComposedLayout,
epi_tile: cute.Tile,
cta_layout_vmnk: cute.Layout,
):
warp_idx = cute.arch.warp_idx()
warp_idx = cute.arch.make_warp_uniform(warp_idx)
tidx, _, _ = cute.arch.thread_idx()
bidx, bidy, _ = cute.arch.block_idx()
cta_rank_in_cluster = cute.arch.block_idx_in_cluster()
cta_in_cluster_coord_vmnk = cta_layout_vmnk.get_flat_coord(cta_rank_in_cluster)
mma_coord_vmnk = (
bidx % cute.size(cta_layout_vmnk, mode=[0]),
bidx // cute.size(cta_layout_vmnk, mode=[0]),
bidy,
None,
)
mma_coord_mnk = mma_coord_vmnk[1:]
is_leader_cta = mma_coord_vmnk[0] == 0
epilogue_warp_ids = (
0,
1,
2,
3,
)
mma_warp_id = 4
tma_warp_id = 5
#
# 1. Prepare args
#
# Allocate SMEM
smem = cutlass.utils.SmemAllocator()
storage = smem.allocate(SharedStorage)
sA = smem.allocate_tensor(
element_type=io_dtype,
layout=a_smem_layout.outer,
byte_alignment=128,
swizzle=a_smem_layout.inner,
)
sB = smem.allocate_tensor(
element_type=io_dtype,
layout=b_smem_layout.outer,
byte_alignment=128,
swizzle=b_smem_layout.inner,
)
sC = smem.allocate_tensor(
element_type=io_dtype,
layout=epi_smem_layout_staged.outer,
byte_alignment=128,
swizzle=epi_smem_layout_staged.inner,
)
# Prefetch tma descriptor
if warp_idx == tma_warp_id:
cpasync.prefetch_descriptor(tma_atom_a)
cpasync.prefetch_descriptor(tma_atom_b)
cpasync.prefetch_descriptor(tma_atom_c)
# As many participants as the number of threads issuing the MMA in the same row and column
# Substract one to not count twice the same thread
num_mcast_participants = (
cute.size(cta_layout_vmnk, mode=[1]) + cute.size(cta_layout_vmnk, mode=[2]) - 1
)
# Mcast mask initialization
tma_mcast_mask_a = cute.nvgpu.cpasync.create_tma_multicast_mask(
cta_layout_vmnk, cta_in_cluster_coord_vmnk, mcast_mode=2
)
tma_mcast_mask_b = cute.nvgpu.cpasync.create_tma_multicast_mask(
cta_layout_vmnk, cta_in_cluster_coord_vmnk, mcast_mode=1
)
# Partition tensors for MMA and make fragments
# (bM, bK, RestK)
gA = cute.local_tile(mA_mkl, mma_tiler_mnk, mma_coord_mnk, proj=(1, None, 1))
# (bN, bK, RestK)
gB = cute.local_tile(mB_nkl, mma_tiler_mnk, mma_coord_mnk, proj=(None, 1, 1))
# (bM, bN)
gC = cute.local_tile(mC_mnl, mma_tiler_mnk, mma_coord_mnk, proj=(1, 1, None))
thr_mma = tiled_mma.get_slice(mma_coord_vmnk[0])
# (MMA, MMA_M, MMA_K, RestK)
tCgA = thr_mma.partition_A(gA)
# (MMA, MMA_N, MMA_K, RestK)
tCgB = thr_mma.partition_B(gB)
# (MMA, MMA_M, MMA_N)
tCgC = thr_mma.partition_C(gC)
# (MMA, MMA_M, MMA_K, STAGE)
tCrA = tiled_mma.make_fragment_A(sA)
# (MMA, MMA_N, MMA_K, STAGE)
tCrB = tiled_mma.make_fragment_B(sB)
# (MMA, MMA_M, MMA_N)
acc_shape = tiled_mma.partition_shape_C(mma_tiler_mnk[:2])
# (MMA, MMA_M, MMA_N)
tCtAcc_fake = tiled_mma.make_fragment_C(acc_shape)
# Barrier 1 for epilogue synchronization
epilogue_sync_barrier = pipeline.NamedBarrier(
barrier_id=1,
num_threads=threads_in_epilogue,
)
# Only MMA warp and epilogue warps participate in TMEM allocation synchronization
# TMA warp does NOT participate
tmem_alloc_barrier = pipeline.NamedBarrier(
barrier_id=2,
num_threads=32
* len((mma_warp_id, *epilogue_warp_ids)), # 5 warps = 160 threads
)
tmem = utils.TmemAllocator(
storage.tmem_holding_buffer.ptr,
barrier_for_retrieve=tmem_alloc_barrier,
allocator_warp_id=epilogue_warp_ids[0],
is_two_cta=True if use_2cta_instrs else False,
two_cta_tmem_dealloc_mbar_ptr=storage.tmem_dealloc_mbar.ptr,
)
# Partition tensors for TMA; This requires the tensors partitioned for MMA
tAsA, tAgA = cute.nvgpu.cpasync.tma_partition(
tma_atom_a,
cta_in_cluster_coord_vmnk[2],
cute.make_layout(cute.size(cta_layout_vmnk, mode=[2])),
cute.group_modes(sA, 0, 3),
cute.group_modes(tCgA, 0, 3),
)
tBsB, tBgB = cute.nvgpu.cpasync.tma_partition(
tma_atom_b,
cta_in_cluster_coord_vmnk[1],
cute.make_layout(cute.size(cta_layout_vmnk, mode=[1])),
cute.group_modes(sB, 0, 3),
cute.group_modes(tCgB, 0, 3),
)
# (EPI_TILE_M, EPI_TILE_N, EPI_M, EPI_N)
tCgC_epi = cute.flat_divide(tCgC[((None, None), 0, 0)], epi_tile)
tCsC, tCgC_tma = cute.nvgpu.cpasync.tma_partition(
tma_atom_c,
0,
cute.make_layout(1),
cute.group_modes(sC, 0, 2),
cute.group_modes(tCgC_epi, 0, 2),
)
num_tma_copy_bytes = (
cute.size_in_bytes(io_dtype, cute.select(a_smem_layout, mode=[0, 1, 2]))
+ cute.size_in_bytes(io_dtype, cute.select(b_smem_layout, mode=[0, 1, 2]))
) * cute.size(cta_layout_vmnk, mode=[0])
# Threads/warps participating in the mainloop pipeline
mainloop_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread)
mainloop_pipeline_consumer_group = pipeline.CooperativeGroup(
pipeline.Agent.Thread, size=num_mcast_participants
)
ab_producer, ab_consumer = pipeline.PipelineTmaUmma.create(
barrier_storage=storage.ab_mbar_ptr.data_ptr(),
num_stages=ab_stages,
producer_group=mainloop_pipeline_producer_group,
consumer_group=mainloop_pipeline_consumer_group,
tx_count=num_tma_copy_bytes,
cta_layout_vmnk=cta_layout_vmnk,
).make_participants()
# Threads/warps participating in the accumulator pipeline
acc_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread)
acc_pipeline_consumer_group = pipeline.CooperativeGroup(
pipeline.Agent.Thread,
size=cute.size(cta_layout_vmnk, mode=[0]) * len(epilogue_warp_ids),
)
acc_producer, acc_consumer = pipeline.PipelineUmmaAsync.create(
barrier_storage=storage.acc_mbar_ptr.data_ptr(),
num_stages=acc_stages,
producer_group=acc_pipeline_producer_group,
consumer_group=acc_pipeline_consumer_group,
cta_layout_vmnk=cta_layout_vmnk,
).make_participants()
#
# Main loop
#
num_k_tiles = cute.size(gA, mode=[2])
# TMA warp
if warp_idx == tma_warp_id:
for k_tile_idx in range(num_k_tiles):
# Wait for A/B buffers to be empty before loading into them
handle = ab_producer.acquire_and_advance()
# Issue TMA loads
cute.copy(
tma_atom_a,
tAgA[(None, k_tile_idx)],
tAsA[(None, handle.index)],
tma_bar_ptr=handle.barrier,
mcast_mask=tma_mcast_mask_a,
)
cute.copy(
tma_atom_b,
tBgB[(None, k_tile_idx)],
tBsB[(None, handle.index)],
tma_bar_ptr=handle.barrier,
mcast_mask=tma_mcast_mask_b,
)
# This mbarrier_wait is preventing threadblocks within a set of dependent threadblocks within the cluster
# (dependent in the context of the TMA/MMA synchronization pattern) to exit early making
# a late tcgen05 commit_arrive illegal
ab_producer.tail()
# MMA warp
elif warp_idx == mma_warp_id:
# Wait for TMEM allocation and retrieve pointer
tmem.wait_for_alloc()
tmem_ptr = tmem.retrieve_ptr(acc_dtype)
tCtAcc = cute.make_tensor(tmem_ptr, tCtAcc_fake.layout)
# Wait for an empty accumulator buffer
if is_leader_cta:
acc_empty = acc_producer.acquire_and_advance()
for k_tile_idx in range(num_k_tiles):
# Wait for TMA copies to complete
handle = ab_consumer.wait_and_advance()
# Execute one K-block worth of MMA instructions
num_k_blocks = cute.size(tCrA, mode=[2])
for k_block_idx in cutlass.range_constexpr(num_k_blocks):
k_block_coord = (None, None, k_block_idx, handle.index)
cute.gemm(
tiled_mma,
tCtAcc,
tCrA[k_block_coord],
tCrB[k_block_coord],
tCtAcc,
)
tiled_mma.set(tcgen05.Field.ACCUMULATE, True)
# Signal that the A/B buffers have been consumed and are ready for the next load
handle.release()
# Signal that the accumulator is fully computed
acc_empty.commit()
# Epilogue warps
elif warp_idx < mma_warp_id:
# Allocate TMEM (only epilogue warp 0 actually allocates)
num_tmem_cols = 512
tmem.allocate(num_tmem_cols)
# Wait for TMEM allocation and retrieve pointer
tmem.wait_for_alloc()
tmem_ptr = tmem.retrieve_ptr(acc_dtype)
tCtAcc = cute.make_tensor(tmem_ptr, tCtAcc_fake.layout)
# Initialize TMA store pipeline for epilogue
epilogue_pipeline_producer_group = pipeline.CooperativeGroup(
pipeline.Agent.Thread,
size=128,
)
epilogue_pipeline = pipeline.PipelineTmaStore.create(
num_stages=epi_stages,
producer_group=epilogue_pipeline_producer_group,
)
# Wait for the accumulator buffer to be full
acc_consumer.wait_and_advance()
copy_atom_t2r = cute.make_copy_atom(
tcgen05.Ld16x256bOp(tcgen05.Repetition.x8)
if mma_tiler_mnk[0] == 64
else tcgen05.Ld32x32bOp(tcgen05.Repetition.x32),
cutlass.Float32,
)
# (EPI_TILE_M, EPI_TILE_N, EPI_M, EPI_N)
tCtAcc_epi = cute.flat_divide(
tCtAcc[((None, None), 0, 0)],
epi_tile,
)
# Tiled copy for TMEM -> RMEM load
tiled_copy_t2r = tcgen05.make_tmem_copy(
copy_atom_t2r, tCtAcc_epi[(None, None, 0, 0)]
)
thr_copy_t2r = tiled_copy_t2r.get_slice(tidx)
# (T2R, T2R_M, T2R_N, EPI_M, EPI_N)
tTR_tAcc = thr_copy_t2r.partition_S(tCtAcc_epi)
# (T2R, T2R_M, T2R_N, EPI_M, EPI_N)
tTR_gC = thr_copy_t2r.partition_D(tCgC_epi)
# (T2R, T2R_M, T2R_N)
tTR_rAcc = cute.make_rmem_tensor(
tTR_gC[(None, None, None, 0, 0)].shape, cutlass.Float32
)
tTR_tAcc = cute.group_modes(tTR_tAcc, 3, cute.rank(tTR_tAcc))
# Copy atom and tiled copy for RMEM -> SMEM load
copy_atom_r2s = cutlass.utils.blackwell_helpers.get_smem_store_op(
c_smem_layout_kind, cutlass.Float32, cutlass.Float32, tiled_copy_t2r
)
tiled_copy_r2s = cute.make_tiled_copy_D(copy_atom_r2s, tiled_copy_t2r)
# (R2S, R2S_M, R2S_N, PIPE_D)
thr_copy_r2s = tiled_copy_r2s.get_slice(tidx)
tRS_sC = thr_copy_r2s.partition_D(sC)
tRS_rAcc = tiled_copy_r2s.retile(tTR_rAcc)
tRS_rC = cute.make_rmem_tensor(tRS_rAcc.shape, io_dtype)
tCgC_grouped = cute.group_modes(tCgC_tma, 1, cute.rank(tCgC_tma))
subtile_cnt = cute.size(tTR_tAcc.shape, mode=[3])
# Epilogue tiling loop
for subtile_idx in cutlass.range(subtile_cnt):
# TMEM -> RMEM
tTR_tAcc_slice = tTR_tAcc[(None, None, None, subtile_idx)]
cute.copy(tiled_copy_t2r, tTR_tAcc_slice, tTR_rAcc)
# RMEM -> SMEM
c_buffer = subtile_idx % epi_stages
tRS_sC_slice = tRS_sC[(None, None, None, c_buffer)]
# type conversion
tRS_rC.store(tRS_rAcc.load().to(io_dtype))
cute.copy(tiled_copy_r2s, tRS_rC, tRS_sC_slice)
# Memory fence and barrier to ensure shared memory stores are visible to TMA stores
cute.arch.fence_view_async_shared()
epilogue_sync_barrier.arrive_and_wait()
# SMEM -> GMEM
if warp_idx == epilogue_warp_ids[0]:
cute.copy(
tma_atom_c,
tCsC[(None, c_buffer)],
tCgC_grouped[(None, subtile_idx)],
)
epilogue_pipeline.producer_commit()
epilogue_pipeline.producer_acquire()
epilogue_sync_barrier.arrive_and_wait()
epilogue_pipeline.producer_tail()
# Dealloc the tensor memory buffer
tmem.relinquish_alloc_permit()
tmem.free(tmem_ptr)
@cute.jit
def host_function(
a: cute.Tensor,
b: cute.Tensor,
c: cute.Tensor,
):
#
# Construct tiled MMA
#
op = tcgen05.MmaF16BF16Op(
io_dtype,
acc_dtype,
mma_inst_shape_mnk,
tcgen05.CtaGroup.TWO if use_2cta_instrs else tcgen05.CtaGroup.ONE,
tcgen05.OperandSource.SMEM,
tcgen05.OperandMajorMode.K,
tcgen05.OperandMajorMode.K,
)
tiled_mma = cute.make_tiled_mma(op)
#
# Construct SMEM layouts for A and B
#
a_smem_layout = sm100_utils.make_smem_layout_a(
tiled_mma,
mma_tiler_mnk,
a.element_type,
ab_stages,
)
b_smem_layout = sm100_utils.make_smem_layout_b(
tiled_mma,
mma_tiler_mnk,
b.element_type,
ab_stages,
)
# c_smem_layout_kind is an enum for row/column major, not a CuTe layout
c_smem_layout_kind = utils.LayoutEnum.from_tensor(c)
#
# Construct the VMNK layout
#
cta_layout_mnk = cute.make_layout(cluster_shape_mnk)
cta_layout_vmnk = cute.tiled_divide(cta_layout_mnk, (tiled_mma.thr_id,))
#
# Construct TMA load atoms
#
op = cute.nvgpu.cpasync.CopyBulkTensorTileG2SMulticastOp(
tcgen05.CtaGroup.TWO if use_2cta_instrs else tcgen05.CtaGroup.ONE
)
a_smem_layout_slice = cute.slice_(a_smem_layout, (None, None, None, 0))
a_tma_atom, a_tma_tensor = cute.nvgpu.make_tiled_tma_atom_A(
op,
a,
a_smem_layout_slice,
mma_tiler_mnk,
tiled_mma,
cta_layout_vmnk.shape, # take the layout and extract the shape internally
)
b_smem_layout_slice = cute.slice_(b_smem_layout, (None, None, None, 0))
b_tma_atom, b_tma_tensor = cute.nvgpu.make_tiled_tma_atom_B(
op,
b,
b_smem_layout_slice,
mma_tiler_mnk,
tiled_mma,
cta_layout_vmnk.shape,
)
cta_tile_shape_mnk = (
mma_tiler_mnk[0] // cute.size(tiled_mma.thr_id),
mma_tiler_mnk[1],
mma_tiler_mnk[2],
)
epi_tile = utils.compute_epilogue_tile_shape(
cta_tile_shape_mnk,
use_2cta_instrs,
c_smem_layout_kind,
io_dtype,
)
epi_smem_layout_staged = cutlass.utils.blackwell_helpers.make_smem_layout_epi(
io_dtype,
c_smem_layout_kind,
epi_tile,
epi_stages,
)
epi_smem_layout = cute.slice_(epi_smem_layout_staged, (None, None, 0))
c_tma_atom, c_tma_tensor = cute.nvgpu.cpasync.make_tiled_tma_atom(
cute.nvgpu.cpasync.CopyBulkTensorTileS2GOp(),
c,
epi_smem_layout,
epi_tile,
)
#
# Launch the kernel
#
grid_shape = cute.round_up(
(
cute.ceil_div(
c.layout.shape[0], mma_tiler_mnk[0] // (2 if use_2cta_instrs else 1)
),
cute.ceil_div(c.layout.shape[1], mma_tiler_mnk[1]),
1,
),
cluster_shape_mnk,
)
kernel(
tiled_mma,
a_tma_atom,
a_tma_tensor,
b_tma_atom,
b_tma_tensor,
c_tma_atom,
c_tma_tensor,
a_smem_layout,
b_smem_layout,
c_smem_layout_kind,
epi_smem_layout_staged,
epi_tile,
cta_layout_vmnk,
).launch(
grid=grid_shape,
block=[192, 1, 1],
cluster=cluster_shape_mnk,
)
def run_dense_gemm(
mnk: Tuple[int, int, int],
tolerance: float,
):
global torch, cutlass_torch
import torch
import cutlass.torch as cutlass_torch
print("===================================================================")
print("Running Blackwell fp16 GEMM example 2 with:")
print(f" mnk: {mnk}")
print(f" tolerance: {tolerance}")
print("===================================================================")
print()
m, n, k = mnk
torch.manual_seed(1111)
# Make K-major tensors (torch tensors are row-major)
def make_tensors(mn, k, dtype):
shape = (mn, k)
return (
torch.empty(*shape, dtype=torch.int32)
.random_(-2, 2)
.to(device="cuda", dtype=dtype)
)
a = make_tensors(m, k, cutlass_torch.dtype(io_dtype))
b = make_tensors(n, k, cutlass_torch.dtype(io_dtype))
c = make_tensors(m, n, cutlass_torch.dtype(io_dtype))
a_memref = from_dlpack(a).mark_layout_dynamic()
b_memref = from_dlpack(b).mark_layout_dynamic()
c_memref = from_dlpack(c).mark_layout_dynamic()
# Entry point to the host JIT function
host_function(
a_memref,
b_memref,
c_memref,
no_cache=True,
)
# Compute reference result and verify
ref = (torch.einsum("mk,nk->mn", a, b)).cpu()
torch.testing.assert_close(
c.cpu(), ref.to(cutlass_torch.dtype(io_dtype)), atol=tolerance, rtol=1e-05
)
if __name__ == "__main__":
def parse_comma_separated_ints(s: str) -> list[int]:
try:
return [int(x.strip()) for x in s.split(",")]
except ValueError:
raise argparse.ArgumentTypeError(
"Invalid format. Expected comma-separated integers."
)
from cuda.bindings import driver as cu_driver
cu_driver.cuInit(0)
err, device_count = cu_driver.cuDeviceGetCount()
if err != cu_driver.CUresult.CUDA_SUCCESS or device_count < 1:
raise RuntimeError("A GPU is required to run this example")
parser = argparse.ArgumentParser(description="Blackwell fp16 GEMM example 2")
parser.add_argument(
"--mnk",
type=parse_comma_separated_ints,
default=(8192, 8192, 8192),
help="MNK dimensions (comma-separated)",
)
parser.add_argument(
"--tolerance", type=float, default=1e-01, help="Tolerance for validation"
)
args = parser.parse_args()
if len(args.mnk) != 3:
parser.error("--mnk must contain exactly 3 values")
run_dense_gemm(
args.mnk,
args.tolerance,
)
print("PASS")

View File

@@ -0,0 +1,779 @@
# SPDX-FileCopyrightText: Copyright (c) 2024 - 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# NVIDIA CORPORATION, its affiliates and licensors retain all intellectual
# property and proprietary rights in and to this material, related
# documentation and any modifications thereto. Any use, reproduction,
# disclosure or distribution of this material and related documentation
# without an express license agreement from NVIDIA CORPORATION or
# its affiliates is strictly prohibited.
# This is the third tutorial GEMM. It further enhances the second tutorial by adding warp
# specialization for TMA, MMA, and epilogue warps.
import argparse
from typing import Tuple
import cutlass
import cutlass.cute as cute
import cutlass.utils as utils
import cutlass.pipeline as pipeline
from cutlass.cute.nvgpu import cpasync, tcgen05
import cutlass.utils.blackwell_helpers as sm100_utils
from cutlass.cute.runtime import from_dlpack
from cutlass.pipeline import pipeline_init_arrive, pipeline_init_wait
"""
The third tutorial GEMM demonstrates a simple kernel implementation in CuTeDSL.
Compared to fp16_gemm_2.py, this kernel uses a static persistent tile scheduler (StaticPersistentTileScheduler).
The static scheduler simplifies work distribution by assigning tiles to CTAs in a fixed, deterministic order,
suitable for well-partitioned workloads. With static scheduling,
the persistent clusters can stay on the GPU throughout kernel execution and process multiple tiles, hiding prologue and epilogue costs.
Notes that the static scheduler is susceptible to workload imbalance if the resources of some SMs are unavailable,
which is why we add a dynamic scheduler in the next example (fp16_gemm_3_1.py).
Therefore, for larger problem sizes the performance will be more advantageous, since a larger problem size leads to more tiles,
and the prologue/epilogue can be hidden among different tiles.
This is especially true when the main loop is relatively short and the prologue/epilogue accounts for a large proportion of the work,
in which case the performance gains become even more significant.
To run this example:
.. code-block:: bash
python examples/blackwell/tutorial_gemm/fp16_gemm_3.py \
--mnk 8192,8192,8192
Constraints for this example:
* The problem size of m and n must be divisible by the tile size m & n (256, 256)
"""
io_dtype = cutlass.Float16
acc_dtype = cutlass.Float32
use_2cta_instrs = True
cluster_shape_mnk = (2, 1, 1) if use_2cta_instrs else (1, 1, 1)
mma_inst_shape_mnk = (256, 256, 16)
mma_tiler_mnk = (256, 256, 64)
threads_in_epilogue = 128 # epilogue threads per cta
# Pipeline stage configuration
ab_stages = 6
epi_stages = 2
acc_stages = 2
# Scheduler
scheduler_type = utils.StaticPersistentTileScheduler
@cute.struct
class SharedStorage:
ab_mbar_ptr: cute.struct.MemRange[cutlass.Int64, ab_stages * 2]
acc_mbar_ptr: cute.struct.MemRange[cutlass.Int64, acc_stages * 2]
tmem_dealloc_mbar: cutlass.Int64
tmem_holding_buffer: cutlass.Int32
@cute.kernel()
def kernel(
tiled_mma: cute.TiledMma,
tma_atom_a: cute.CopyAtom,
mA_mkl: cute.Tensor,
tma_atom_b: cute.CopyAtom,
mB_nkl: cute.Tensor,
tma_atom_c: cute.CopyAtom,
mC_mnl: cute.Tensor,
a_smem_layout: cute.ComposedLayout,
b_smem_layout: cute.ComposedLayout,
c_smem_layout_kind: cutlass.Constexpr,
epi_smem_layout_staged: cute.ComposedLayout,
epi_tile: cute.Tile,
cta_layout_vmnk: cute.Layout,
tile_sched_params: utils.PersistentTileSchedulerParams,
):
warp_idx = cute.arch.warp_idx()
warp_idx = cute.arch.make_warp_uniform(warp_idx)
tidx, _, _ = cute.arch.thread_idx()
bidx, _, _ = cute.arch.block_idx()
cta_rank_in_cluster = cute.arch.block_idx_in_cluster()
cta_in_cluster_coord_vmnk = cta_layout_vmnk.get_flat_coord(cta_rank_in_cluster)
mma_tile_coord_v = bidx % cute.size(cta_layout_vmnk, mode=[0])
is_leader_cta = mma_tile_coord_v == 0
epilogue_warp_ids = (
0,
1,
2,
3,
)
mma_warp_id = 4
tma_warp_id = 5
epilog_sync_bar_id = 1
tmem_alloc_sync_bar_id = 2
# Prefetch tma descriptor
if warp_idx == tma_warp_id:
cpasync.prefetch_descriptor(tma_atom_a)
cpasync.prefetch_descriptor(tma_atom_b)
cpasync.prefetch_descriptor(tma_atom_c)
# As many participants as the number of threads issuing the MMA in the same row and column
# Substract one to not count twice the same thread
num_mcast_participants = (
cute.size(cta_layout_vmnk, mode=[1]) + cute.size(cta_layout_vmnk, mode=[2]) - 1
)
# Mcast mask initialization
tma_mcast_mask_a = cute.nvgpu.cpasync.create_tma_multicast_mask(
cta_layout_vmnk, cta_in_cluster_coord_vmnk, mcast_mode=2
)
tma_mcast_mask_b = cute.nvgpu.cpasync.create_tma_multicast_mask(
cta_layout_vmnk, cta_in_cluster_coord_vmnk, mcast_mode=1
)
# Allocate SMEM
smem = cutlass.utils.SmemAllocator()
storage = smem.allocate(SharedStorage)
# Barrier 1 for epilogue synchronization
epilogue_sync_barrier = pipeline.NamedBarrier(
barrier_id=epilog_sync_bar_id,
num_threads=threads_in_epilogue,
)
# Only MMA warp and epilogue warps participate in TMEM allocation synchronization
# TMA warp does NOT participate
tmem_alloc_barrier = pipeline.NamedBarrier(
barrier_id=tmem_alloc_sync_bar_id,
num_threads=32
* len((mma_warp_id, *epilogue_warp_ids)), # 5 warps = 160 threads
)
tmem = utils.TmemAllocator(
storage.tmem_holding_buffer.ptr,
barrier_for_retrieve=tmem_alloc_barrier,
allocator_warp_id=epilogue_warp_ids[0],
is_two_cta=True,
two_cta_tmem_dealloc_mbar_ptr=storage.tmem_dealloc_mbar.ptr,
)
num_tma_copy_bytes = (
cute.size_in_bytes(io_dtype, cute.select(a_smem_layout, mode=[0, 1, 2]))
+ cute.size_in_bytes(io_dtype, cute.select(b_smem_layout, mode=[0, 1, 2]))
) * cute.size(cta_layout_vmnk, mode=[0])
# Threads/warps participating in the mainloop pipeline
mainloop_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread)
mainloop_pipeline_consumer_group = pipeline.CooperativeGroup(
pipeline.Agent.Thread, size=num_mcast_participants
)
ab_producer, ab_consumer = pipeline.PipelineTmaUmma.create(
barrier_storage=storage.ab_mbar_ptr.data_ptr(),
num_stages=ab_stages,
producer_group=mainloop_pipeline_producer_group,
consumer_group=mainloop_pipeline_consumer_group,
tx_count=num_tma_copy_bytes,
cta_layout_vmnk=cta_layout_vmnk,
).make_participants()
# Threads/warps participating in the accumulator pipeline
acc_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread)
acc_pipeline_consumer_group = pipeline.CooperativeGroup(
pipeline.Agent.Thread,
size=cute.size(cta_layout_vmnk, mode=[0]) * len(epilogue_warp_ids),
)
acc_producer, acc_consumer = pipeline.PipelineUmmaAsync.create(
barrier_storage=storage.acc_mbar_ptr.data_ptr(),
num_stages=acc_stages,
producer_group=acc_pipeline_producer_group,
consumer_group=acc_pipeline_consumer_group,
cta_layout_vmnk=cta_layout_vmnk,
).make_participants()
# Cluster arrive after barrier init
pipeline_init_arrive(cluster_shape_mn=cluster_shape_mnk, is_relaxed=True)
# Allocate SMEM
sA = smem.allocate_tensor(
element_type=io_dtype,
layout=a_smem_layout.outer,
byte_alignment=128,
swizzle=a_smem_layout.inner,
)
sB = smem.allocate_tensor(
element_type=io_dtype,
layout=b_smem_layout.outer,
byte_alignment=128,
swizzle=b_smem_layout.inner,
)
sC = smem.allocate_tensor(
element_type=io_dtype,
layout=epi_smem_layout_staged.outer,
byte_alignment=128,
swizzle=epi_smem_layout_staged.inner,
)
# Partition tensors for MMA and make fragments
# (bM, bK, RestM, RestK)
gA = cute.local_tile(
mA_mkl, cute.slice_(mma_tiler_mnk, (None, 0, None)), (None, None)
)
# (bN, bK, RestN, RestK)
gB = cute.local_tile(
mB_nkl, cute.slice_(mma_tiler_mnk, (0, None, None)), (None, None)
)
# (bM, bN, RestM, RestN)
gC = cute.local_tile(
mC_mnl, cute.slice_(mma_tiler_mnk, (None, None, 0)), (None, None)
)
thr_mma = tiled_mma.get_slice(mma_tile_coord_v)
# (MMA, MMA_M, MMA_K, RestM, RestK)
tCgA = thr_mma.partition_A(gA)
# (MMA, MMA_N, MMA_K, RestN, RestK)
tCgB = thr_mma.partition_B(gB)
# (MMA, MMA_M, MMA_N, RestM, RestN)
tCgC = thr_mma.partition_C(gC)
# (MMA, MMA_M, MMA_K, STAGE)
tCrA = tiled_mma.make_fragment_A(sA)
# (MMA, MMA_N, MMA_K, STAGE)
tCrB = tiled_mma.make_fragment_B(sB)
# (MMA, MMA_M, MMA_N)
acc_shape = tiled_mma.partition_shape_C(mma_tiler_mnk[:2])
# (MMA, MMA_M, MMA_N, STAGE)
tCtAcc_fake = tiled_mma.make_fragment_C(cute.append(acc_shape, acc_stages))
# Partition tensors for TMA; This requires the tensors partitioned for MMA
# ((atom_v, rest_v), STAGE)
# ((atom_v, rest_v), RestM, RestK)
tAsA, tAgA = cute.nvgpu.cpasync.tma_partition(
tma_atom_a,
cta_in_cluster_coord_vmnk[2],
cute.make_layout(cute.size(cta_layout_vmnk, mode=[2])),
cute.group_modes(sA, 0, 3),
cute.group_modes(tCgA, 0, 3),
)
# ((atom_v, rest_v), STAGE)
# ((atom_v, rest_v), RestN, RestK)
tBsB, tBgB = cute.nvgpu.cpasync.tma_partition(
tma_atom_b,
cta_in_cluster_coord_vmnk[1],
cute.make_layout(cute.size(cta_layout_vmnk, mode=[1])),
cute.group_modes(sB, 0, 3),
cute.group_modes(tCgB, 0, 3),
)
gC_epi = cute.flat_divide(tCgC[((None, None), 0, 0, None, None)], epi_tile)
tCsC, tCgC_tma = cute.nvgpu.cpasync.tma_partition(
tma_atom_c,
0,
cute.make_layout(1),
cute.group_modes(sC, 0, 2),
cute.group_modes(gC_epi, 0, 2),
)
# Cluster wait before starting work
pipeline_init_wait(cluster_shape_mn=cluster_shape_mnk)
tile_sched = scheduler_type.create(
tile_sched_params, cute.arch.block_idx(), cute.arch.grid_dim()
)
work_tile = tile_sched.initial_work_tile_info()
#
# Main loop
#
num_k_tiles = cute.size(gA, mode=[3])
# TMA warp
if warp_idx == tma_warp_id:
#
# Persistent tile scheduling loop
#
while work_tile.is_valid_tile:
# Get tile coord from tile scheduler
cur_tile_coord = work_tile.tile_idx
mma_tile_coord_mnl = (
cur_tile_coord[0] // cute.size(tiled_mma.thr_id.shape),
cur_tile_coord[1],
cur_tile_coord[2],
)
# Slice to per mma tile index
# ((atom_v, rest_v), RestK)
tAgA_slice = tAgA[(None, mma_tile_coord_mnl[0], None)]
# ((atom_v, rest_v), RestK)
tBgB_slice = tBgB[(None, mma_tile_coord_mnl[1], None)]
# Tma load loop
for k_tile_idx in range(num_k_tiles):
# Wait for A/B buffers to be empty before loading into them
handle = ab_producer.acquire_and_advance()
# Issue TMA loads
cute.copy(
tma_atom_a,
tAgA_slice[(None, k_tile_idx)],
tAsA[(None, handle.index)],
tma_bar_ptr=handle.barrier,
mcast_mask=tma_mcast_mask_a,
)
cute.copy(
tma_atom_b,
tBgB_slice[(None, k_tile_idx)],
tBsB[(None, handle.index)],
tma_bar_ptr=handle.barrier,
mcast_mask=tma_mcast_mask_b,
)
# Advance to next k_tile
tile_sched.advance_to_next_work()
work_tile = tile_sched.get_current_work()
# This mbarrier_wait is preventing threadblocks within a set of dependent threadblocks within the cluster
# (dependent in the context of the TMA/MMA synchronization pattern) to exit early making
# a late tcgen05 commit_arrive illegal
ab_producer.tail()
# MMA warp
elif warp_idx == mma_warp_id:
# Wait for TMEM allocation and retrieve pointer
tmem.wait_for_alloc()
tmem_ptr = tmem.retrieve_ptr(acc_dtype)
# (MMA, MMA_M, MMA_N, STAGE)
tCtAcc_base = cute.make_tensor(tmem_ptr, tCtAcc_fake.layout)
while work_tile.is_valid_tile:
if is_leader_cta:
# Wait for accumulator buffer empty
acc_empty = acc_producer.acquire_and_advance()
# Set tensor memory buffer for current tile
# (MMA, MMA_M, MMA_N)
tCtAcc = tCtAcc_base[(None, None, None, acc_empty.index)]
tiled_mma.set(tcgen05.Field.ACCUMULATE, False)
for k_tile_idx in range(num_k_tiles):
# Wait for TMA copies to complete
handle = ab_consumer.wait_and_advance()
# Execute one K-block worth of MMA instructions
num_k_blocks = cute.size(tCrA, mode=[2])
for k_block_idx in cutlass.range_constexpr(num_k_blocks):
k_block_coord = (None, None, k_block_idx, handle.index)
cute.gemm(
tiled_mma,
tCtAcc,
tCrA[k_block_coord],
tCrB[k_block_coord],
tCtAcc,
)
tiled_mma.set(tcgen05.Field.ACCUMULATE, True)
# Signal that the A/B buffers have been consumed and are ready for the next load
handle.release()
# Signal that the accumulator is fully computed
acc_empty.commit()
# Advance to next tile
tile_sched.advance_to_next_work()
work_tile = tile_sched.get_current_work()
# Wait for accumulator buffer empty
acc_producer.tail()
# Epilogue warps
elif warp_idx < mma_warp_id:
# Allocate TMEM (only epilogue warp 0 actually allocates)
num_tmem_cols = 512
tmem.allocate(num_tmem_cols)
# Wait for TMEM allocation and retrieve pointer
tmem.wait_for_alloc()
tmem_ptr = tmem.retrieve_ptr(acc_dtype)
# (MMA, MMA_M, MMA_N, STAGE)
tCtAcc_base = cute.make_tensor(tmem_ptr, tCtAcc_fake.layout)
# Initialize TMA store pipeline for epilogue
epilogue_pipeline_producer_group = pipeline.CooperativeGroup(
pipeline.Agent.Thread,
size=128,
)
epilogue_pipeline = pipeline.PipelineTmaStore.create(
num_stages=epi_stages,
producer_group=epilogue_pipeline_producer_group,
)
copy_atom_t2r = cute.make_copy_atom(
tcgen05.Ld32x32bOp(tcgen05.Repetition.x32, tcgen05.Pack.NONE),
cutlass.Float32,
)
while work_tile.is_valid_tile:
# Get tile coord from tile scheduler
cur_tile_coord = work_tile.tile_idx
mma_tile_coord_mnl = (
cur_tile_coord[0] // cute.size(tiled_mma.thr_id.shape),
cur_tile_coord[1],
cur_tile_coord[2],
)
# Wait for accumulator buffer full
acc_full = acc_consumer.wait_and_advance()
# Set tensor memory buffer for current tile
# (MMA, MMA_M, MMA_N)
tCtAcc = tCtAcc_base[(None, None, None, acc_full.index)]
# (EPI_TILE_M, EPI_TILE_N, EPI_M, EPI_N)
tCtAcc_epi = cute.flat_divide(
tCtAcc[((None, None), 0, 0)], # why 0,0 ?
epi_tile,
)
mma_tile_coord_mn = cute.slice_(mma_tile_coord_mnl, (None, None, 0))
# (EPI_TILE_M, EPI_TILE_N, EPI_M, EPI_N, RestM, RestN)
tCgC_epi = cute.flat_divide(
tCgC[((None, None), 0, 0, *mma_tile_coord_mn)], epi_tile
)
tCgC_tma_cur_tile = tCgC_tma[(None, None, None, *mma_tile_coord_mn)]
# Tiled copy for TMEM -> RMEM load
tiled_copy_t2r = tcgen05.make_tmem_copy(
copy_atom_t2r, tCtAcc_epi[(None, None, 0, 0)]
)
thr_copy_t2r = tiled_copy_t2r.get_slice(tidx)
# (T2R, T2R_M, T2R_N, EPI_M, EPI_N)
tTR_tAcc = thr_copy_t2r.partition_S(tCtAcc_epi)
# (T2R, T2R_M, T2R_N, EPI_M, EPI_N)
tTR_gC = thr_copy_t2r.partition_D(tCgC_epi)
# (T2R, T2R_M, T2R_N)
tTR_rAcc = cute.make_rmem_tensor(
tTR_gC[(None, None, None, 0, 0)].shape, cutlass.Float32
)
tTR_tAcc = cute.group_modes(tTR_tAcc, 3, cute.rank(tTR_tAcc))
# Copy atom and tiled copy for RMEM -> SMEM load
copy_atom_r2s = cutlass.utils.blackwell_helpers.get_smem_store_op(
c_smem_layout_kind, cutlass.Float32, cutlass.Float32, tiled_copy_t2r
)
tiled_copy_r2s = cute.make_tiled_copy_D(copy_atom_r2s, tiled_copy_t2r)
# (R2S, R2S_M, R2S_N, PIPE_D)
thr_copy_r2s = tiled_copy_r2s.get_slice(tidx)
tRS_sC = thr_copy_r2s.partition_D(sC)
tRS_rAcc = tiled_copy_r2s.retile(tTR_rAcc)
tRS_rC = cute.make_rmem_tensor(tRS_rAcc.shape, io_dtype)
tCgC_grouped = cute.group_modes(
tCgC_tma_cur_tile, 1, cute.rank(tCgC_tma_cur_tile)
)
subtile_cnt = cute.size(tTR_tAcc.shape, mode=[3])
# Epilogue tiling loop
for subtile_idx in cutlass.range(subtile_cnt):
# TMEM -> RMEM
tTR_tAcc_slice = tTR_tAcc[(None, None, None, subtile_idx)]
cute.copy(tiled_copy_t2r, tTR_tAcc_slice, tTR_rAcc)
# RMEM -> SMEM
c_buffer = subtile_idx % epi_stages
tRS_sC_slice = tRS_sC[(None, None, None, c_buffer)]
# type conversion
tRS_rC.store(tRS_rAcc.load().to(io_dtype))
cute.copy(tiled_copy_r2s, tRS_rC, tRS_sC_slice)
# Memory fence and barrier to ensure shared memory stores are visible to TMA stores
cute.arch.fence_view_async_shared()
epilogue_sync_barrier.arrive_and_wait()
# SMEM -> GMEM
if warp_idx == epilogue_warp_ids[0]:
cute.copy(
tma_atom_c,
tCsC[(None, c_buffer)],
tCgC_grouped[(None, subtile_idx)],
)
epilogue_pipeline.producer_commit()
epilogue_pipeline.producer_acquire()
epilogue_sync_barrier.arrive_and_wait()
# Async arrive accumulator buffer empty
with cute.arch.elect_one():
acc_full.release()
# Advance to next tile
tile_sched.advance_to_next_work()
work_tile = tile_sched.get_current_work()
# Wait for C store complete
epilogue_pipeline.producer_tail()
# Dealloc the tensor memory buffer
tmem.relinquish_alloc_permit()
tmem.free(tmem_ptr)
def compute_grid(
c: cute.Tensor,
mma_tiler_mnk: Tuple[int, int, int],
cluster_shape_mnk: Tuple[int, int, int],
max_active_clusters: cutlass.Constexpr,
) -> Tuple[
utils.PersistentTileSchedulerParams,
Tuple[int, int, int],
]:
c_shape = cute.slice_(mma_tiler_mnk, (None, None, 0))
gc = cute.zipped_divide(c, tiler=c_shape)
num_ctas_mn = gc[(0, (None, None))].shape
tile_sched_params = utils.PersistentTileSchedulerParams(
(*num_ctas_mn, 1), cluster_shape_mnk
)
grid = utils.StaticPersistentTileScheduler.get_grid_shape(
tile_sched_params, max_active_clusters
)
return tile_sched_params, grid
@cute.jit
def host_function(
a: cute.Tensor,
b: cute.Tensor,
c: cute.Tensor,
max_active_clusters: cutlass.Constexpr,
):
#
# Construct tiled MMA
#
op = tcgen05.MmaF16BF16Op(
io_dtype,
acc_dtype,
mma_inst_shape_mnk,
tcgen05.CtaGroup.TWO if use_2cta_instrs else tcgen05.CtaGroup.ONE,
tcgen05.OperandSource.SMEM,
tcgen05.OperandMajorMode.K,
tcgen05.OperandMajorMode.K,
)
tiled_mma = cute.make_tiled_mma(op)
#
# Construct SMEM layouts for A and B
#
a_smem_layout = sm100_utils.make_smem_layout_a(
tiled_mma,
mma_tiler_mnk,
a.element_type,
ab_stages,
)
b_smem_layout = sm100_utils.make_smem_layout_b(
tiled_mma,
mma_tiler_mnk,
b.element_type,
ab_stages,
)
# c_smem_layout_kind is an enum for row/column major, not a CuTe layout
c_smem_layout_kind = utils.LayoutEnum.from_tensor(c)
#
# Construct the VMNK layout
#
cta_layout_mnk = cute.make_layout(cluster_shape_mnk)
cta_layout_vmnk = cute.tiled_divide(cta_layout_mnk, (tiled_mma.thr_id,))
#
# Construct TMA load atoms
#
op = cute.nvgpu.cpasync.CopyBulkTensorTileG2SMulticastOp(
tcgen05.CtaGroup.TWO if use_2cta_instrs else tcgen05.CtaGroup.ONE
)
a_smem_layout_slice = cute.slice_(a_smem_layout, (None, None, None, 0))
a_tma_atom, a_tma_tensor = cute.nvgpu.make_tiled_tma_atom_A(
op,
a,
a_smem_layout_slice,
mma_tiler_mnk,
tiled_mma,
cta_layout_vmnk.shape,
)
b_smem_layout_slice = cute.slice_(b_smem_layout, (None, None, None, 0))
b_tma_atom, b_tma_tensor = cute.nvgpu.make_tiled_tma_atom_B(
op,
b,
b_smem_layout_slice,
mma_tiler_mnk,
tiled_mma,
cta_layout_vmnk.shape,
)
cta_tile_shape_mnk = (
mma_tiler_mnk[0] // cute.size(tiled_mma.thr_id),
mma_tiler_mnk[1],
mma_tiler_mnk[2],
)
epi_tile = utils.compute_epilogue_tile_shape(
cta_tile_shape_mnk,
use_2cta_instrs,
c_smem_layout_kind,
io_dtype,
)
epi_smem_layout_staged = cutlass.utils.blackwell_helpers.make_smem_layout_epi(
io_dtype,
c_smem_layout_kind,
epi_tile,
epi_stages,
)
epi_smem_layout = cute.slice_(epi_smem_layout_staged, (None, None, 0))
c_tma_atom, c_tma_tensor = cute.nvgpu.cpasync.make_tiled_tma_atom(
cute.nvgpu.cpasync.CopyBulkTensorTileS2GOp(),
c,
epi_smem_layout,
epi_tile,
)
#
# Launch the kernel
#
tile_sched_params, grid_shape = compute_grid(
c,
cta_tile_shape_mnk,
cluster_shape_mnk,
max_active_clusters,
)
kernel(
tiled_mma,
a_tma_atom,
a_tma_tensor,
b_tma_atom,
b_tma_tensor,
c_tma_atom,
c_tma_tensor,
a_smem_layout,
b_smem_layout,
c_smem_layout_kind,
epi_smem_layout_staged,
epi_tile,
cta_layout_vmnk,
tile_sched_params,
).launch(
grid=grid_shape,
block=[192, 1, 1],
cluster=cluster_shape_mnk,
)
def run_dense_gemm(
mnk: Tuple[int, int, int],
tolerance: float,
):
global torch, cutlass_torch
import torch
import cutlass.torch as cutlass_torch
print("===================================================================")
print("Running Blackwell fp16 GEMM example 3 with:")
print(f" mnk: {mnk}")
print(f" tolerance: {tolerance}")
print("===================================================================")
print()
m, n, k = mnk
torch.manual_seed(1111)
# Make K-major tensors (torch tensors are row-major)
def make_tensors(mn, k, dtype):
shape = (mn, k)
return (
# torch.empty(*shape, dtype=torch.int32)
# .random_(-2, 2)
torch.ones(*shape, dtype=torch.int32)
.to(device="cuda", dtype=dtype)
)
a = make_tensors(m, k, cutlass_torch.dtype(io_dtype))
b = make_tensors(n, k, cutlass_torch.dtype(io_dtype))
c = make_tensors(m, n, cutlass_torch.dtype(io_dtype))
a_memref = from_dlpack(a).mark_layout_dynamic()
b_memref = from_dlpack(b).mark_layout_dynamic()
c_memref = from_dlpack(c).mark_layout_dynamic()
max_active_clusters = utils.HardwareInfo().get_max_active_clusters(
cluster_shape_mnk[0] * cluster_shape_mnk[1]
)
# Entry point to the host JIT function
host_function(
a_memref,
b_memref,
c_memref,
max_active_clusters,
no_cache=True,
)
# Compute reference result and verify
ref = (torch.einsum("mk,nk->mn", a, b)).cpu()
torch.testing.assert_close(
c.cpu(), ref.to(cutlass_torch.dtype(io_dtype)), atol=tolerance, rtol=1e-05
)
if __name__ == "__main__":
def parse_comma_separated_ints(s: str):
try:
return [int(x.strip()) for x in s.split(",")]
except ValueError:
raise argparse.ArgumentTypeError(
"Invalid format. Expected comma-separated integers."
)
from cuda.bindings import driver as cu_driver
cu_driver.cuInit(0)
err, device_count = cu_driver.cuDeviceGetCount()
if err != cu_driver.CUresult.CUDA_SUCCESS or device_count < 1:
raise RuntimeError("A GPU is required to run this example")
parser = argparse.ArgumentParser(description="Blackwell fp16 GEMM example 3")
parser.add_argument(
"--mnk",
type=parse_comma_separated_ints,
default=(8192, 8192, 8192),
help="MNK dimensions (comma-separated)",
)
parser.add_argument(
"--tolerance", type=float, default=1e-01, help="Tolerance for validation"
)
args = parser.parse_args()
if len(args.mnk) != 3:
parser.error("--mnk must contain exactly 3 values")
run_dense_gemm(
args.mnk,
args.tolerance,
)
print("PASS")

View File

@@ -0,0 +1,891 @@
# SPDX-FileCopyrightText: Copyright (c) 2024 - 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# NVIDIA CORPORATION, its affiliates and licensors retain all intellectual
# property and proprietary rights in and to this material, related
# documentation and any modifications thereto. Any use, reproduction,
# disclosure or distribution of this material and related documentation
# without an express license agreement from NVIDIA CORPORATION or
# its affiliates is strictly prohibited.
# This is the third tutorial GEMM. It further enhances the second tutorial by adding warp
# specialization for TMA, MMA, and epilogue warps.
import argparse
from typing import Tuple, Union
import cutlass
import cutlass.cute as cute
import cutlass.utils as utils
import cutlass.pipeline as pipeline
from cutlass.cute.nvgpu import cpasync, tcgen05
import cutlass.utils.blackwell_helpers as sm100_utils
from cutlass.cute.runtime import from_dlpack
from cutlass.pipeline import pipeline_init_arrive, pipeline_init_wait
"""
The third tutorial GEMM demonstrates a simple kernel implementation in CuTeDSL.
Compared to fp16_gemm_3.py, this kernel uses a dynamic persistent tile scheduler (ClcDynamicPersistentTileScheduler).
The dynamic scheduler is more flexible than the static scheduler, as it can handle workload imbalance better.
To run this example:
.. code-block:: bash
python examples/blackwell/tutorial_gemm/fp16_gemm_3_1.py \
--mnk 8192,8192,8192
Constraints for this example:
* The problem size of m and n must be divisible by the tile size m & n (256, 256)
"""
io_dtype = cutlass.Float16
acc_dtype = cutlass.Float32
use_2cta_instrs = True
cluster_shape_mnk = (2, 1, 1) if use_2cta_instrs else (1, 1, 1)
mma_inst_shape_mnk = (256, 256, 16)
mma_tiler_mnk = (256, 256, 64)
threads_in_epilogue = 128 # epilogue threads per cta
# Pipeline stage configuration
ab_stages = 6
epi_stages = 2
acc_stages = 2
num_clc_stage = 1
# Scheduler
use_clc_dynamic_scheduler = True
scheduler_type = (
utils.ClcDynamicPersistentTileScheduler
if use_clc_dynamic_scheduler
else utils.StaticPersistentTileScheduler
)
# Response size is 4B * 4 elements
num_clc_response_bytes = 16
@cute.struct
class SharedStorage:
ab_mbar_ptr: cute.struct.MemRange[cutlass.Int64, ab_stages * 2]
acc_mbar_ptr: cute.struct.MemRange[cutlass.Int64, acc_stages * 2]
tmem_dealloc_mbar: cutlass.Int64
tmem_holding_buffer: cutlass.Int32
# Only for CLC Dynamic Scheduler
clc_mbar_ptr: cute.struct.MemRange[cutlass.Int64, 2]
clc_response: cute.struct.MemRange[cutlass.Int32, 4]
@cute.kernel()
def kernel(
tiled_mma: cute.TiledMma,
tma_atom_a: cute.CopyAtom,
mA_mkl: cute.Tensor,
tma_atom_b: cute.CopyAtom,
mB_nkl: cute.Tensor,
tma_atom_c: cute.CopyAtom,
mC_mnl: cute.Tensor,
a_smem_layout: cute.ComposedLayout,
b_smem_layout: cute.ComposedLayout,
c_smem_layout_kind: cutlass.Constexpr,
epi_smem_layout_staged: cute.ComposedLayout,
epi_tile: cute.Tile,
cta_layout_vmnk: cute.Layout,
tile_sched_params: Union[
utils.ClcDynamicPersistentTileSchedulerParams,
utils.PersistentTileSchedulerParams,
],
):
warp_idx = cute.arch.warp_idx()
warp_idx = cute.arch.make_warp_uniform(warp_idx)
tidx, _, _ = cute.arch.thread_idx()
bidx, _, _ = cute.arch.block_idx()
cta_rank_in_cluster = cute.arch.block_idx_in_cluster()
cta_in_cluster_coord_vmnk = cta_layout_vmnk.get_flat_coord(cta_rank_in_cluster)
mma_tile_coord_v = bidx % cute.size(cta_layout_vmnk, mode=[0])
is_leader_cta = mma_tile_coord_v == 0
epilogue_warp_ids = (
0,
1,
2,
3,
)
mma_warp_id = 4
tma_warp_id = 5
# sched_warp_id only for dynamic scheduler
sched_warp_id = 6
epilog_sync_bar_id = 1
tmem_alloc_sync_bar_id = 2
# Prefetch tma descriptor
if warp_idx == tma_warp_id:
cpasync.prefetch_descriptor(tma_atom_a)
cpasync.prefetch_descriptor(tma_atom_b)
cpasync.prefetch_descriptor(tma_atom_c)
# As many participants as the number of threads issuing the MMA in the same row and column
# Substract one to not count twice the same thread
num_mcast_participants = (
cute.size(cta_layout_vmnk, mode=[1]) + cute.size(cta_layout_vmnk, mode=[2]) - 1
)
# Mcast mask initialization
tma_mcast_mask_a = cute.nvgpu.cpasync.create_tma_multicast_mask(
cta_layout_vmnk, cta_in_cluster_coord_vmnk, mcast_mode=2
)
tma_mcast_mask_b = cute.nvgpu.cpasync.create_tma_multicast_mask(
cta_layout_vmnk, cta_in_cluster_coord_vmnk, mcast_mode=1
)
# Allocate SMEM
smem = cutlass.utils.SmemAllocator()
storage = smem.allocate(SharedStorage)
# Barrier 1 for epilogue synchronization
epilogue_sync_barrier = pipeline.NamedBarrier(
barrier_id=epilog_sync_bar_id,
num_threads=threads_in_epilogue,
)
# Only MMA warp and epilogue warps participate in TMEM allocation synchronization
# TMA warp does NOT participate
tmem_alloc_barrier = pipeline.NamedBarrier(
barrier_id=tmem_alloc_sync_bar_id,
num_threads=32
* len((mma_warp_id, *epilogue_warp_ids)), # 5 warps = 160 threads
)
tmem = utils.TmemAllocator(
storage.tmem_holding_buffer.ptr,
barrier_for_retrieve=tmem_alloc_barrier,
allocator_warp_id=epilogue_warp_ids[0],
is_two_cta=True,
two_cta_tmem_dealloc_mbar_ptr=storage.tmem_dealloc_mbar.ptr,
)
num_tma_copy_bytes = (
cute.size_in_bytes(io_dtype, cute.select(a_smem_layout, mode=[0, 1, 2]))
+ cute.size_in_bytes(io_dtype, cute.select(b_smem_layout, mode=[0, 1, 2]))
) * cute.size(cta_layout_vmnk, mode=[0])
# Threads/warps participating in the mainloop pipeline
mainloop_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread)
mainloop_pipeline_consumer_group = pipeline.CooperativeGroup(
pipeline.Agent.Thread, size=num_mcast_participants
)
ab_producer, ab_consumer = pipeline.PipelineTmaUmma.create(
barrier_storage=storage.ab_mbar_ptr.data_ptr(),
num_stages=ab_stages,
producer_group=mainloop_pipeline_producer_group,
consumer_group=mainloop_pipeline_consumer_group,
tx_count=num_tma_copy_bytes,
cta_layout_vmnk=cta_layout_vmnk,
).make_participants()
# Threads/warps participating in the accumulator pipeline
acc_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread)
acc_pipeline_consumer_group = pipeline.CooperativeGroup(
pipeline.Agent.Thread,
size=cute.size(cta_layout_vmnk, mode=[0]) * len(epilogue_warp_ids),
)
acc_producer, acc_consumer = pipeline.PipelineUmmaAsync.create(
barrier_storage=storage.acc_mbar_ptr.data_ptr(),
num_stages=acc_stages,
producer_group=acc_pipeline_producer_group,
consumer_group=acc_pipeline_consumer_group,
cta_layout_vmnk=cta_layout_vmnk,
).make_participants()
# Initialize clc_pipeline (barrier) and states
# ONLY for CLC Dynamic Scheduler
if cutlass.const_expr(use_clc_dynamic_scheduler):
clc_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread)
cluster_size = cute.size(cluster_shape_mnk)
num_clc_consumer_threads = 32 * len(
(
sched_warp_id,
*(
cluster_size
* (
mma_warp_id,
tma_warp_id,
*epilogue_warp_ids,
)
),
)
)
clc_pipeline_consumer_group = pipeline.CooperativeGroup(
pipeline.Agent.Thread, num_clc_consumer_threads
)
clc_pipeline = pipeline.PipelineClcFetchAsync.create(
barrier_storage=storage.clc_mbar_ptr.data_ptr(),
num_stages=num_clc_stage,
producer_group=clc_pipeline_producer_group,
consumer_group=clc_pipeline_consumer_group,
tx_count=num_clc_response_bytes,
cta_layout_vmnk=cta_layout_vmnk,
defer_sync=True,
)
# Initial clc response pointer
clc_response_ptr = storage.clc_response.data_ptr()
clc_consumer_state = pipeline.make_pipeline_state(
pipeline.PipelineUserType.Consumer, num_clc_stage
)
else:
clc_pipeline = None
clc_response_ptr = None
clc_consumer_state = None
# Cluster arrive after barrier init
pipeline_init_arrive(cluster_shape_mn=cluster_shape_mnk, is_relaxed=True)
# Allocate SMEM
sA = smem.allocate_tensor(
element_type=io_dtype,
layout=a_smem_layout.outer,
byte_alignment=128,
swizzle=a_smem_layout.inner,
)
sB = smem.allocate_tensor(
element_type=io_dtype,
layout=b_smem_layout.outer,
byte_alignment=128,
swizzle=b_smem_layout.inner,
)
sC = smem.allocate_tensor(
element_type=io_dtype,
layout=epi_smem_layout_staged.outer,
byte_alignment=128,
swizzle=epi_smem_layout_staged.inner,
)
# Partition tensors for MMA and make fragments
# (bM, bK, RestM, RestK)
gA = cute.local_tile(
mA_mkl, cute.slice_(mma_tiler_mnk, (None, 0, None)), (None, None)
)
# (bN, bK, RestN, RestK)
gB = cute.local_tile(
mB_nkl, cute.slice_(mma_tiler_mnk, (0, None, None)), (None, None)
)
# (bM, bN, RestM, RestN)
gC = cute.local_tile(
mC_mnl, cute.slice_(mma_tiler_mnk, (None, None, 0)), (None, None)
)
thr_mma = tiled_mma.get_slice(mma_tile_coord_v)
# (MMA, MMA_M, MMA_K, RestM, RestK)
tCgA = thr_mma.partition_A(gA)
# (MMA, MMA_N, MMA_K, RestN, RestK)
tCgB = thr_mma.partition_B(gB)
# (MMA, MMA_M, MMA_N, RestM, RestN)
tCgC = thr_mma.partition_C(gC)
# (MMA, MMA_M, MMA_K, STAGE)
tCrA = tiled_mma.make_fragment_A(sA)
# (MMA, MMA_N, MMA_K, STAGE)
tCrB = tiled_mma.make_fragment_B(sB)
# (MMA, MMA_M, MMA_N)
acc_shape = tiled_mma.partition_shape_C(mma_tiler_mnk[:2])
# (MMA, MMA_M, MMA_N, STAGE)
tCtAcc_fake = tiled_mma.make_fragment_C(cute.append(acc_shape, acc_stages))
# Partition tensors for TMA; This requires the tensors partitioned for MMA
# ((atom_v, rest_v), STAGE)
# ((atom_v, rest_v), RestM, RestK)
tAsA, tAgA = cute.nvgpu.cpasync.tma_partition(
tma_atom_a,
cta_in_cluster_coord_vmnk[2],
cute.make_layout(cute.size(cta_layout_vmnk, mode=[2])),
cute.group_modes(sA, 0, 3),
cute.group_modes(tCgA, 0, 3),
)
# ((atom_v, rest_v), STAGE)
# ((atom_v, rest_v), RestN, RestK)
tBsB, tBgB = cute.nvgpu.cpasync.tma_partition(
tma_atom_b,
cta_in_cluster_coord_vmnk[1],
cute.make_layout(cute.size(cta_layout_vmnk, mode=[1])),
cute.group_modes(sB, 0, 3),
cute.group_modes(tCgB, 0, 3),
)
gC_epi = cute.flat_divide(tCgC[((None, None), 0, 0, None, None)], epi_tile)
tCsC, tCgC_tma = cute.nvgpu.cpasync.tma_partition(
tma_atom_c,
0,
cute.make_layout(1),
cute.group_modes(sC, 0, 2),
cute.group_modes(gC_epi, 0, 2),
)
# Cluster wait before starting work
pipeline_init_wait(cluster_shape_mn=cluster_shape_mnk)
# Construct the scheduler
if cutlass.const_expr(use_clc_dynamic_scheduler):
tile_sched = scheduler_type.create(
tile_sched_params,
cute.arch.block_idx(),
cute.arch.grid_dim(),
clc_response_ptr,
)
else:
tile_sched = scheduler_type.create(
tile_sched_params, cute.arch.block_idx(), cute.arch.grid_dim()
)
work_tile = tile_sched.initial_work_tile_info()
#
# Main loop
#
num_k_tiles = cute.size(gA, mode=[3])
# TMA warp
if warp_idx == tma_warp_id:
#
# Persistent tile scheduling loop
#
while work_tile.is_valid_tile:
# Get tile coord from tile scheduler
cur_tile_coord = work_tile.tile_idx
mma_tile_coord_mnl = (
cur_tile_coord[0] // cute.size(tiled_mma.thr_id.shape),
cur_tile_coord[1],
cur_tile_coord[2],
)
# Slice to per mma tile index
# ((atom_v, rest_v), RestK)
tAgA_slice = tAgA[(None, mma_tile_coord_mnl[0], None)]
# ((atom_v, rest_v), RestK)
tBgB_slice = tBgB[(None, mma_tile_coord_mnl[1], None)]
# Tma load loop
for k_tile_idx in range(num_k_tiles):
# Wait for A/B buffers to be empty before loading into them
handle = ab_producer.acquire_and_advance()
# Issue TMA loads
cute.copy(
tma_atom_a,
tAgA_slice[(None, k_tile_idx)],
tAsA[(None, handle.index)],
tma_bar_ptr=handle.barrier,
mcast_mask=tma_mcast_mask_a,
)
cute.copy(
tma_atom_b,
tBgB_slice[(None, k_tile_idx)],
tBsB[(None, handle.index)],
tma_bar_ptr=handle.barrier,
mcast_mask=tma_mcast_mask_b,
)
# Advance to next k_tile
if cutlass.const_expr(use_clc_dynamic_scheduler):
clc_pipeline.consumer_wait(clc_consumer_state)
work_tile = tile_sched.get_current_work()
clc_pipeline.consumer_release(clc_consumer_state)
clc_consumer_state.advance()
else:
tile_sched.advance_to_next_work()
work_tile = tile_sched.get_current_work()
# This mbarrier_wait is preventing threadblocks within a set of dependent threadblocks within the cluster
# (dependent in the context of the TMA/MMA synchronization pattern) to exit early making
# a late tcgen05 commit_arrive illegal
ab_producer.tail()
# Sched warp (only for dynamic scheduler)
if cutlass.const_expr(use_clc_dynamic_scheduler):
is_first_cta_in_cluster = cta_rank_in_cluster == 0
if warp_idx == sched_warp_id and is_first_cta_in_cluster:
# Persistent tile scheduling loop
clc_producer_state = pipeline.make_pipeline_state(
pipeline.PipelineUserType.ProducerConsumer, num_clc_stage
)
while work_tile.is_valid_tile:
# Advance to next tile
clc_pipeline.producer_acquire(clc_producer_state)
mbarrier_addr = clc_pipeline.producer_get_barrier(clc_producer_state)
tile_sched.advance_to_next_work(mbarrier_addr)
clc_producer_state.advance()
clc_pipeline.consumer_wait(clc_consumer_state)
work_tile = tile_sched.get_current_work()
clc_pipeline.consumer_release(clc_consumer_state)
clc_consumer_state.advance()
clc_pipeline.producer_tail(clc_producer_state)
# MMA warp
if warp_idx == mma_warp_id:
# Wait for TMEM allocation and retrieve pointer
tmem.wait_for_alloc()
tmem_ptr = tmem.retrieve_ptr(acc_dtype)
# (MMA, MMA_M, MMA_N, STAGE)
tCtAcc_base = cute.make_tensor(tmem_ptr, tCtAcc_fake.layout)
while work_tile.is_valid_tile:
if is_leader_cta:
# Wait for accumulator buffer empty
acc_empty = acc_producer.acquire_and_advance()
# Set tensor memory buffer for current tile
# (MMA, MMA_M, MMA_N)
tCtAcc = tCtAcc_base[(None, None, None, acc_empty.index)]
tiled_mma.set(tcgen05.Field.ACCUMULATE, False)
for k_tile_idx in range(num_k_tiles):
# Wait for TMA copies to complete
handle = ab_consumer.wait_and_advance()
# Execute one K-block worth of MMA instructions
num_k_blocks = cute.size(tCrA, mode=[2])
for k_block_idx in cutlass.range_constexpr(num_k_blocks):
k_block_coord = (None, None, k_block_idx, handle.index)
cute.gemm(
tiled_mma,
tCtAcc,
tCrA[k_block_coord],
tCrB[k_block_coord],
tCtAcc,
)
tiled_mma.set(tcgen05.Field.ACCUMULATE, True)
# Signal that the A/B buffers have been consumed and are ready for the next load
handle.release()
# Signal that the accumulator is fully computed
acc_empty.commit()
# Advance to next tile
if cutlass.const_expr(use_clc_dynamic_scheduler):
clc_pipeline.consumer_wait(clc_consumer_state)
work_tile = tile_sched.get_current_work()
clc_pipeline.consumer_release(clc_consumer_state)
clc_consumer_state.advance()
else:
tile_sched.advance_to_next_work()
work_tile = tile_sched.get_current_work()
# Wait for accumulator buffer empty
acc_producer.tail()
# Epilogue warps
if warp_idx < mma_warp_id:
# Allocate TMEM (only epilogue warp 0 actually allocates)
num_tmem_cols = 512
tmem.allocate(num_tmem_cols)
# Wait for TMEM allocation and retrieve pointer
tmem.wait_for_alloc()
tmem_ptr = tmem.retrieve_ptr(acc_dtype)
# (MMA, MMA_M, MMA_N, STAGE)
tCtAcc_base = cute.make_tensor(tmem_ptr, tCtAcc_fake.layout)
# Initialize TMA store pipeline for epilogue
epilogue_pipeline_producer_group = pipeline.CooperativeGroup(
pipeline.Agent.Thread,
size=128,
)
epilogue_pipeline = pipeline.PipelineTmaStore.create(
num_stages=epi_stages,
producer_group=epilogue_pipeline_producer_group,
)
copy_atom_t2r = cute.make_copy_atom(
tcgen05.Ld32x32bOp(tcgen05.Repetition.x32, tcgen05.Pack.NONE),
cutlass.Float32,
)
while work_tile.is_valid_tile:
# Get tile coord from tile scheduler
cur_tile_coord = work_tile.tile_idx
mma_tile_coord_mnl = (
cur_tile_coord[0] // cute.size(tiled_mma.thr_id.shape),
cur_tile_coord[1],
cur_tile_coord[2],
)
# Wait for accumulator buffer full
acc_full = acc_consumer.wait_and_advance()
# Set tensor memory buffer for current tile
# (MMA, MMA_M, MMA_N)
tCtAcc = tCtAcc_base[(None, None, None, acc_full.index)]
# (EPI_TILE_M, EPI_TILE_N, EPI_M, EPI_N)
tCtAcc_epi = cute.flat_divide(
tCtAcc[((None, None), 0, 0)], # why 0,0 ?
epi_tile,
)
mma_tile_coord_mn = cute.slice_(mma_tile_coord_mnl, (None, None, 0))
# (EPI_TILE_M, EPI_TILE_N, EPI_M, EPI_N, RestM, RestN)
tCgC_epi = cute.flat_divide(
tCgC[((None, None), 0, 0, *mma_tile_coord_mn)], epi_tile
)
tCgC_tma_cur_tile = tCgC_tma[(None, None, None, *mma_tile_coord_mn)]
# Tiled copy for TMEM -> RMEM load
tiled_copy_t2r = tcgen05.make_tmem_copy(
copy_atom_t2r, tCtAcc_epi[(None, None, 0, 0)]
)
thr_copy_t2r = tiled_copy_t2r.get_slice(tidx)
# (T2R, T2R_M, T2R_N, EPI_M, EPI_N)
tTR_tAcc = thr_copy_t2r.partition_S(tCtAcc_epi)
# (T2R, T2R_M, T2R_N, EPI_M, EPI_N)
tTR_gC = thr_copy_t2r.partition_D(tCgC_epi)
# (T2R, T2R_M, T2R_N)
tTR_rAcc = cute.make_rmem_tensor(
tTR_gC[(None, None, None, 0, 0)].shape, cutlass.Float32
)
tTR_tAcc = cute.group_modes(tTR_tAcc, 3, cute.rank(tTR_tAcc))
# Copy atom and tiled copy for RMEM -> SMEM load
copy_atom_r2s = cutlass.utils.blackwell_helpers.get_smem_store_op(
c_smem_layout_kind, cutlass.Float32, cutlass.Float32, tiled_copy_t2r
)
tiled_copy_r2s = cute.make_tiled_copy_D(copy_atom_r2s, tiled_copy_t2r)
# (R2S, R2S_M, R2S_N, PIPE_D)
thr_copy_r2s = tiled_copy_r2s.get_slice(tidx)
tRS_sC = thr_copy_r2s.partition_D(sC)
tRS_rAcc = tiled_copy_r2s.retile(tTR_rAcc)
tRS_rC = cute.make_rmem_tensor(tRS_rAcc.shape, io_dtype)
tCgC_grouped = cute.group_modes(
tCgC_tma_cur_tile, 1, cute.rank(tCgC_tma_cur_tile)
)
subtile_cnt = cute.size(tTR_tAcc.shape, mode=[3])
# Epilogue tiling loop
for subtile_idx in cutlass.range(subtile_cnt):
# TMEM -> RMEM
tTR_tAcc_slice = tTR_tAcc[(None, None, None, subtile_idx)]
cute.copy(tiled_copy_t2r, tTR_tAcc_slice, tTR_rAcc)
# RMEM -> SMEM
c_buffer = subtile_idx % epi_stages
tRS_sC_slice = tRS_sC[(None, None, None, c_buffer)]
# type conversion
tRS_rC.store(tRS_rAcc.load().to(io_dtype))
cute.copy(tiled_copy_r2s, tRS_rC, tRS_sC_slice)
# Memory fence and barrier to ensure shared memory stores are visible to TMA stores
cute.arch.fence_view_async_shared()
epilogue_sync_barrier.arrive_and_wait()
# SMEM -> GMEM
if warp_idx == epilogue_warp_ids[0]:
cute.copy(
tma_atom_c,
tCsC[(None, c_buffer)],
tCgC_grouped[(None, subtile_idx)],
)
epilogue_pipeline.producer_commit()
epilogue_pipeline.producer_acquire()
epilogue_sync_barrier.arrive_and_wait()
# Async arrive accumulator buffer empty
with cute.arch.elect_one():
acc_full.release()
# Advance to next tile
if cutlass.const_expr(use_clc_dynamic_scheduler):
clc_pipeline.consumer_wait(clc_consumer_state)
work_tile = tile_sched.get_current_work()
clc_pipeline.consumer_release(clc_consumer_state)
clc_consumer_state.advance()
else:
tile_sched.advance_to_next_work()
work_tile = tile_sched.get_current_work()
# Wait for C store complete
epilogue_pipeline.producer_tail()
# Dealloc the tensor memory buffer
tmem.relinquish_alloc_permit()
tmem.free(tmem_ptr)
def compute_grid(
c: cute.Tensor,
mma_tiler_mnk: Tuple[int, int, int],
cluster_shape_mnk: Tuple[int, int, int],
scheduler_type: Union[
utils.StaticPersistentTileScheduler, utils.ClcDynamicPersistentTileScheduler
],
max_active_clusters: cutlass.Constexpr,
) -> Tuple[
Union[
utils.ClcDynamicPersistentTileSchedulerParams,
utils.PersistentTileSchedulerParams,
],
Tuple[int, int, int],
]:
c_shape = cute.slice_(mma_tiler_mnk, (None, None, 0))
gc = cute.zipped_divide(c, tiler=c_shape)
num_ctas_mn = gc[(0, (None, None))].shape
if cutlass.const_expr(
issubclass(scheduler_type, utils.ClcDynamicPersistentTileScheduler)
):
tile_sched_params = utils.ClcDynamicPersistentTileSchedulerParams(
(*num_ctas_mn, 1), cluster_shape_mnk
)
grid = utils.ClcDynamicPersistentTileScheduler.get_grid_shape(tile_sched_params)
else:
tile_sched_params = utils.PersistentTileSchedulerParams(
(*num_ctas_mn, 1), cluster_shape_mnk
)
grid = utils.StaticPersistentTileScheduler.get_grid_shape(
tile_sched_params, max_active_clusters
)
return tile_sched_params, grid
@cute.jit
def host_function(
a: cute.Tensor,
b: cute.Tensor,
c: cute.Tensor,
max_active_clusters: cutlass.Constexpr,
):
#
# Construct tiled MMA
#
op = tcgen05.MmaF16BF16Op(
io_dtype,
acc_dtype,
mma_inst_shape_mnk,
tcgen05.CtaGroup.TWO if use_2cta_instrs else tcgen05.CtaGroup.ONE,
tcgen05.OperandSource.SMEM,
tcgen05.OperandMajorMode.K,
tcgen05.OperandMajorMode.K,
)
tiled_mma = cute.make_tiled_mma(op)
#
# Construct SMEM layouts for A and B
#
a_smem_layout = sm100_utils.make_smem_layout_a(
tiled_mma,
mma_tiler_mnk,
a.element_type,
ab_stages,
)
b_smem_layout = sm100_utils.make_smem_layout_b(
tiled_mma,
mma_tiler_mnk,
b.element_type,
ab_stages,
)
# c_smem_layout_kind is an enum for row/column major, not a CuTe layout
c_smem_layout_kind = utils.LayoutEnum.from_tensor(c)
#
# Construct the VMNK layout
#
cta_layout_mnk = cute.make_layout(cluster_shape_mnk)
cta_layout_vmnk = cute.tiled_divide(cta_layout_mnk, (tiled_mma.thr_id,))
#
# Construct TMA load atoms
#
op = cute.nvgpu.cpasync.CopyBulkTensorTileG2SMulticastOp(
tcgen05.CtaGroup.TWO if use_2cta_instrs else tcgen05.CtaGroup.ONE
)
a_smem_layout_slice = cute.slice_(a_smem_layout, (None, None, None, 0))
a_tma_atom, a_tma_tensor = cute.nvgpu.make_tiled_tma_atom_A(
op,
a,
a_smem_layout_slice,
mma_tiler_mnk,
tiled_mma,
cta_layout_vmnk.shape,
)
b_smem_layout_slice = cute.slice_(b_smem_layout, (None, None, None, 0))
b_tma_atom, b_tma_tensor = cute.nvgpu.make_tiled_tma_atom_B(
op,
b,
b_smem_layout_slice,
mma_tiler_mnk,
tiled_mma,
cta_layout_vmnk.shape,
)
cta_tile_shape_mnk = (
mma_tiler_mnk[0] // cute.size(tiled_mma.thr_id),
mma_tiler_mnk[1],
mma_tiler_mnk[2],
)
epi_tile = utils.compute_epilogue_tile_shape(
cta_tile_shape_mnk,
use_2cta_instrs,
c_smem_layout_kind,
io_dtype,
)
epi_smem_layout_staged = cutlass.utils.blackwell_helpers.make_smem_layout_epi(
io_dtype,
c_smem_layout_kind,
epi_tile,
epi_stages,
)
epi_smem_layout = cute.slice_(epi_smem_layout_staged, (None, None, 0))
c_tma_atom, c_tma_tensor = cute.nvgpu.cpasync.make_tiled_tma_atom(
cute.nvgpu.cpasync.CopyBulkTensorTileS2GOp(),
c,
epi_smem_layout,
epi_tile,
)
#
# Launch the kernel
#
tile_sched_params, grid_shape = compute_grid(
c,
cta_tile_shape_mnk,
cluster_shape_mnk,
scheduler_type,
max_active_clusters,
)
kernel(
tiled_mma,
a_tma_atom,
a_tma_tensor,
b_tma_atom,
b_tma_tensor,
c_tma_atom,
c_tma_tensor,
a_smem_layout,
b_smem_layout,
c_smem_layout_kind,
epi_smem_layout_staged,
epi_tile,
cta_layout_vmnk,
tile_sched_params,
).launch(
grid=grid_shape,
block=[224, 1, 1] if use_clc_dynamic_scheduler else [192, 1, 1],
cluster=cluster_shape_mnk,
)
def run_dense_gemm(
mnk: Tuple[int, int, int],
tolerance: float,
):
global torch, cutlass_torch
import torch
import cutlass.torch as cutlass_torch
print("===================================================================")
print("Running Blackwell fp16 GEMM example 3_1 with:")
print(f" mnk: {mnk}")
print(f" tolerance: {tolerance}")
print("===================================================================")
print()
m, n, k = mnk
torch.manual_seed(1111)
# Make K-major tensors (torch tensors are row-major)
def make_tensors(mn, k, dtype):
shape = (mn, k)
return (
torch.empty(*shape, dtype=torch.int32)
.random_(-2, 2)
.to(device="cuda", dtype=dtype)
)
a = make_tensors(m, k, cutlass_torch.dtype(io_dtype))
b = make_tensors(n, k, cutlass_torch.dtype(io_dtype))
c = make_tensors(m, n, cutlass_torch.dtype(io_dtype))
a_memref = from_dlpack(a).mark_layout_dynamic()
b_memref = from_dlpack(b).mark_layout_dynamic()
c_memref = from_dlpack(c).mark_layout_dynamic()
max_active_clusters = utils.HardwareInfo().get_max_active_clusters(
cluster_shape_mnk[0] * cluster_shape_mnk[1]
)
# Entry point to the host JIT function
host_function(
a_memref,
b_memref,
c_memref,
max_active_clusters,
no_cache=True,
)
# Compute reference result and verify
ref = (torch.einsum("mk,nk->mn", a, b)).cpu()
torch.testing.assert_close(
c.cpu(), ref.to(cutlass_torch.dtype(io_dtype)), atol=tolerance, rtol=1e-05
)
if __name__ == "__main__":
def parse_comma_separated_ints(s: str):
try:
return [int(x.strip()) for x in s.split(",")]
except ValueError:
raise argparse.ArgumentTypeError(
"Invalid format. Expected comma-separated integers."
)
from cuda.bindings import driver as cu_driver
cu_driver.cuInit(0)
err, device_count = cu_driver.cuDeviceGetCount()
if err != cu_driver.CUresult.CUDA_SUCCESS or device_count < 1:
raise RuntimeError("A GPU is required to run this example")
parser = argparse.ArgumentParser(description="Blackwell fp16 GEMM example 3_1")
parser.add_argument(
"--mnk",
type=parse_comma_separated_ints,
default=(8192, 8192, 8192),
help="MNK dimensions (comma-separated)",
)
parser.add_argument(
"--tolerance", type=float, default=1e-01, help="Tolerance for validation"
)
args = parser.parse_args()
if len(args.mnk) != 3:
parser.error("--mnk must contain exactly 3 values")
run_dense_gemm(
args.mnk,
args.tolerance,
)
print("PASS")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,929 @@
# SPDX-FileCopyrightText: Copyright (c) 2024 - 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# NVIDIA CORPORATION, its affiliates and licensors retain all intellectual
# property and proprietary rights in and to this material, related
# documentation and any modifications thereto. Any use, reproduction,
# disclosure or distribution of this material and related documentation
# without an express license agreement from NVIDIA CORPORATION or
# its affiliates is strictly prohibited.
# This is the fifth tutorial GEMM (5). It extends fp16_gemm_3_1.py by adding TMA prefetch.
# TMA prefetch uses cute.prefetch() to bring data into L2 cache before TMA copy needs it,
# helping to hide DRAM latency for memory-bound workloads.
import argparse
from typing import Tuple, Union
import cutlass
import cutlass.cute as cute
import cutlass.utils as utils
import cutlass.pipeline as pipeline
from cutlass.cute.nvgpu import cpasync, tcgen05
import cutlass.utils.blackwell_helpers as sm100_utils
from cutlass.cute.runtime import from_dlpack
from cutlass.pipeline import pipeline_init_arrive, pipeline_init_wait
"""
The fifth tutorial GEMM (5) demonstrates TMA prefetch optimization in CuTeDSL.
TMA Prefetch uses cute.prefetch() to bring data from DRAM into L2 cache ahead of time.
This helps hide DRAM latency, which is particularly beneficial for memory-bound workloads.
TMA Prefetch consists of two phases:
1. Initial Phase: Before the TMA load loop starts, prefetch the first `prefetch_dist`
k-tiles into L2 cache. This primes the cache before any TMA copies begin.
2. Rolling Phase: During each iteration of the TMA load loop, after issuing TMA copy
for the current k-tile, prefetch the k-tile that is `prefetch_dist` ahead.
Key differences from fp16_gemm_3_1.py:
1. Added cute.prefetch() calls to bring data into L2 cache before TMA copy
2. Initial prefetch loop before the main TMA load loop
3. Rolling prefetch inside the TMA load loop to keep L2 primed
To run this example:
.. code-block:: bash
python examples/blackwell/tutorial_gemm/fp16_gemm_5.py \
--mnk 8192,8192,8192
Constraints for this example:
* The problem size of m and n must be divisible by the tile size m & n (256, 256)
"""
io_dtype = cutlass.Float16
acc_dtype = cutlass.Float32
use_2cta_instrs = True
cluster_shape_mnk = (2, 2, 1) if use_2cta_instrs else (1, 1, 1)
mma_inst_shape_mnk = (256, 64, 16)
mma_tiler_mnk = (256, 64, 64)
threads_in_epilogue = 128 # epilogue threads per cta
# Pipeline stage configuration
ab_stages = 10
epi_stages = 2
acc_stages = 2
num_clc_stage = 1
# Scheduler
use_clc_dynamic_scheduler = True
scheduler_type = (
utils.ClcDynamicPersistentTileScheduler
if use_clc_dynamic_scheduler
else utils.StaticPersistentTileScheduler
)
# Response size is 4B * 4 elements
num_clc_response_bytes = 16
@cute.struct
class SharedStorage:
ab_mbar_ptr: cute.struct.MemRange[cutlass.Int64, ab_stages * 2]
acc_mbar_ptr: cute.struct.MemRange[cutlass.Int64, acc_stages * 2]
tmem_dealloc_mbar: cutlass.Int64
tmem_holding_buffer: cutlass.Int32
# Only for CLC Dynamic Scheduler
clc_mbar_ptr: cute.struct.MemRange[cutlass.Int64, 2]
clc_response: cute.struct.MemRange[cutlass.Int32, 4]
@cute.kernel()
def kernel(
tiled_mma: cute.TiledMma,
tma_atom_a: cute.CopyAtom,
mA_mkl: cute.Tensor,
tma_atom_b: cute.CopyAtom,
mB_nkl: cute.Tensor,
tma_atom_c: cute.CopyAtom,
mC_mnl: cute.Tensor,
a_smem_layout: cute.ComposedLayout,
b_smem_layout: cute.ComposedLayout,
c_smem_layout_kind: cutlass.Constexpr,
epi_smem_layout_staged: cute.ComposedLayout,
epi_tile: cute.Tile,
cta_layout_vmnk: cute.Layout,
tile_sched_params: Union[
utils.ClcDynamicPersistentTileSchedulerParams,
utils.PersistentTileSchedulerParams,
],
):
warp_idx = cute.arch.warp_idx()
warp_idx = cute.arch.make_warp_uniform(warp_idx)
tidx, _, _ = cute.arch.thread_idx()
bidx, _, _ = cute.arch.block_idx()
cta_rank_in_cluster = cute.arch.block_idx_in_cluster()
cta_in_cluster_coord_vmnk = cta_layout_vmnk.get_flat_coord(cta_rank_in_cluster)
mma_tile_coord_v = bidx % cute.size(cta_layout_vmnk, mode=[0])
is_leader_cta = mma_tile_coord_v == 0
epilogue_warp_ids = (
0,
1,
2,
3,
)
mma_warp_id = 4
tma_warp_id = 5
# sched_warp_id only for dynamic scheduler
sched_warp_id = 6
epilog_sync_bar_id = 1
tmem_alloc_sync_bar_id = 2
# Prefetch tma descriptor
if warp_idx == tma_warp_id:
cpasync.prefetch_descriptor(tma_atom_a)
cpasync.prefetch_descriptor(tma_atom_b)
cpasync.prefetch_descriptor(tma_atom_c)
# As many participants as the number of threads issuing the MMA in the same row and column
# Substract one to not count twice the same thread
num_mcast_participants = (
cute.size(cta_layout_vmnk, mode=[1]) + cute.size(cta_layout_vmnk, mode=[2]) - 1
)
# Mcast mask initialization
tma_mcast_mask_a = cute.nvgpu.cpasync.create_tma_multicast_mask(
cta_layout_vmnk, cta_in_cluster_coord_vmnk, mcast_mode=2
)
tma_mcast_mask_b = cute.nvgpu.cpasync.create_tma_multicast_mask(
cta_layout_vmnk, cta_in_cluster_coord_vmnk, mcast_mode=1
)
# Allocate SMEM
smem = cutlass.utils.SmemAllocator()
storage = smem.allocate(SharedStorage)
# Barrier 1 for epilogue synchronization
epilogue_sync_barrier = pipeline.NamedBarrier(
barrier_id=epilog_sync_bar_id,
num_threads=threads_in_epilogue,
)
# Only MMA warp and epilogue warps participate in TMEM allocation synchronization
# TMA warp does NOT participate
tmem_alloc_barrier = pipeline.NamedBarrier(
barrier_id=tmem_alloc_sync_bar_id,
num_threads=32
* len((mma_warp_id, *epilogue_warp_ids)), # 5 warps = 160 threads
)
tmem = utils.TmemAllocator(
storage.tmem_holding_buffer.ptr,
barrier_for_retrieve=tmem_alloc_barrier,
allocator_warp_id=epilogue_warp_ids[0],
is_two_cta=True,
two_cta_tmem_dealloc_mbar_ptr=storage.tmem_dealloc_mbar.ptr,
)
num_tma_copy_bytes = (
cute.size_in_bytes(io_dtype, cute.select(a_smem_layout, mode=[0, 1, 2]))
+ cute.size_in_bytes(io_dtype, cute.select(b_smem_layout, mode=[0, 1, 2]))
) * cute.size(cta_layout_vmnk, mode=[0])
# Threads/warps participating in the mainloop pipeline
mainloop_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread)
mainloop_pipeline_consumer_group = pipeline.CooperativeGroup(
pipeline.Agent.Thread, size=num_mcast_participants
)
ab_producer, ab_consumer = pipeline.PipelineTmaUmma.create(
barrier_storage=storage.ab_mbar_ptr.data_ptr(),
num_stages=ab_stages,
producer_group=mainloop_pipeline_producer_group,
consumer_group=mainloop_pipeline_consumer_group,
tx_count=num_tma_copy_bytes,
cta_layout_vmnk=cta_layout_vmnk,
).make_participants()
# Threads/warps participating in the accumulator pipeline
acc_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread)
acc_pipeline_consumer_group = pipeline.CooperativeGroup(
pipeline.Agent.Thread,
size=cute.size(cta_layout_vmnk, mode=[0]) * len(epilogue_warp_ids),
)
acc_producer, acc_consumer = pipeline.PipelineUmmaAsync.create(
barrier_storage=storage.acc_mbar_ptr.data_ptr(),
num_stages=acc_stages,
producer_group=acc_pipeline_producer_group,
consumer_group=acc_pipeline_consumer_group,
cta_layout_vmnk=cta_layout_vmnk,
).make_participants()
# Initialize clc_pipeline (barrier) and states
# ONLY for CLC Dynamic Scheduler
if cutlass.const_expr(use_clc_dynamic_scheduler):
clc_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread)
cluster_size = cute.size(cluster_shape_mnk)
num_clc_consumer_threads = 32 * len(
(
sched_warp_id,
*(
cluster_size
* (
mma_warp_id,
tma_warp_id,
*epilogue_warp_ids,
)
),
)
)
clc_pipeline_consumer_group = pipeline.CooperativeGroup(
pipeline.Agent.Thread, num_clc_consumer_threads
)
clc_pipeline = pipeline.PipelineClcFetchAsync.create(
barrier_storage=storage.clc_mbar_ptr.data_ptr(),
num_stages=num_clc_stage,
producer_group=clc_pipeline_producer_group,
consumer_group=clc_pipeline_consumer_group,
tx_count=num_clc_response_bytes,
cta_layout_vmnk=cta_layout_vmnk,
defer_sync=True,
)
# Initial clc response pointer
clc_response_ptr = storage.clc_response.data_ptr()
clc_consumer_state = pipeline.make_pipeline_state(
pipeline.PipelineUserType.Consumer, num_clc_stage
)
else:
clc_pipeline = None
clc_response_ptr = None
clc_consumer_state = None
# Cluster arrive after barrier init
pipeline_init_arrive(cluster_shape_mn=cluster_shape_mnk, is_relaxed=True)
# Allocate SMEM
sA = smem.allocate_tensor(
element_type=io_dtype,
layout=a_smem_layout.outer,
byte_alignment=128,
swizzle=a_smem_layout.inner,
)
sB = smem.allocate_tensor(
element_type=io_dtype,
layout=b_smem_layout.outer,
byte_alignment=128,
swizzle=b_smem_layout.inner,
)
sC = smem.allocate_tensor(
element_type=io_dtype,
layout=epi_smem_layout_staged.outer,
byte_alignment=128,
swizzle=epi_smem_layout_staged.inner,
)
# Partition tensors for MMA and make fragments
# (bM, bK, RestM, RestK)
gA = cute.local_tile(
mA_mkl, cute.slice_(mma_tiler_mnk, (None, 0, None)), (None, None)
)
# (bN, bK, RestN, RestK)
gB = cute.local_tile(
mB_nkl, cute.slice_(mma_tiler_mnk, (0, None, None)), (None, None)
)
# (bM, bN, RestM, RestN)
gC = cute.local_tile(
mC_mnl, cute.slice_(mma_tiler_mnk, (None, None, 0)), (None, None)
)
thr_mma = tiled_mma.get_slice(mma_tile_coord_v)
# (MMA, MMA_M, MMA_K, RestM, RestK)
tCgA = thr_mma.partition_A(gA)
# (MMA, MMA_N, MMA_K, RestN, RestK)
tCgB = thr_mma.partition_B(gB)
# (MMA, MMA_M, MMA_N, RestM, RestN)
tCgC = thr_mma.partition_C(gC)
# (MMA, MMA_M, MMA_K, STAGE)
tCrA = tiled_mma.make_fragment_A(sA)
# (MMA, MMA_N, MMA_K, STAGE)
tCrB = tiled_mma.make_fragment_B(sB)
# (MMA, MMA_M, MMA_N)
acc_shape = tiled_mma.partition_shape_C(mma_tiler_mnk[:2])
# (MMA, MMA_M, MMA_N, STAGE)
tCtAcc_fake = tiled_mma.make_fragment_C(cute.append(acc_shape, acc_stages))
# Partition tensors for TMA; This requires the tensors partitioned for MMA
# ((atom_v, rest_v), STAGE)
# ((atom_v, rest_v), RestM, RestK)
tAsA, tAgA = cute.nvgpu.cpasync.tma_partition(
tma_atom_a,
cta_in_cluster_coord_vmnk[2],
cute.make_layout(cute.size(cta_layout_vmnk, mode=[2])),
cute.group_modes(sA, 0, 3),
cute.group_modes(tCgA, 0, 3),
)
# ((atom_v, rest_v), STAGE)
# ((atom_v, rest_v), RestN, RestK)
tBsB, tBgB = cute.nvgpu.cpasync.tma_partition(
tma_atom_b,
cta_in_cluster_coord_vmnk[1],
cute.make_layout(cute.size(cta_layout_vmnk, mode=[1])),
cute.group_modes(sB, 0, 3),
cute.group_modes(tCgB, 0, 3),
)
gC_epi = cute.flat_divide(tCgC[((None, None), 0, 0, None, None)], epi_tile)
tCsC, tCgC_tma = cute.nvgpu.cpasync.tma_partition(
tma_atom_c,
0,
cute.make_layout(1),
cute.group_modes(sC, 0, 2),
cute.group_modes(gC_epi, 0, 2),
)
# Cluster wait before starting work
pipeline_init_wait(cluster_shape_mn=cluster_shape_mnk)
# Construct the scheduler
if cutlass.const_expr(use_clc_dynamic_scheduler):
tile_sched = scheduler_type.create(
tile_sched_params,
cute.arch.block_idx(),
cute.arch.grid_dim(),
clc_response_ptr,
)
else:
tile_sched = scheduler_type.create(
tile_sched_params, cute.arch.block_idx(), cute.arch.grid_dim()
)
work_tile = tile_sched.initial_work_tile_info()
#
# Main loop
#
num_k_tiles = cute.size(gA, mode=[3])
# Prefetch distance: how many k-tiles ahead to prefetch into L2 cache
# This helps hide DRAM latency by bringing data to L2 before TMA copy needs it
prefetch_dist = ab_stages
# TMA warp with prefetch
if warp_idx == tma_warp_id:
#
# Persistent tile scheduling loop
#
while work_tile.is_valid_tile:
# Get tile coord from tile scheduler
cur_tile_coord = work_tile.tile_idx
mma_tile_coord_mnl = (
cur_tile_coord[0] // cute.size(tiled_mma.thr_id.shape),
cur_tile_coord[1],
cur_tile_coord[2],
)
# Slice to per mma tile index
# ((atom_v, rest_v), RestK)
tAgA_slice = tAgA[(None, mma_tile_coord_mnl[0], None)]
# ((atom_v, rest_v), RestK)
tBgB_slice = tBgB[(None, mma_tile_coord_mnl[1], None)]
# =========================================================
# TMA Prefetch - Initial Phase
# =========================================================
# Prefetch the first `prefetch_dist` k-tiles into L2 cache
# This primes the cache before TMA copies start
for pf_k_tile in cutlass.range(
cutlass.min(prefetch_dist, num_k_tiles), unroll=1
):
cute.prefetch(tma_atom_a, tAgA_slice[(None, pf_k_tile)])
cute.prefetch(tma_atom_b, tBgB_slice[(None, pf_k_tile)])
# =========================================================
# TMA Load Loop with Rolling Prefetch
# =========================================================
for k_tile_idx in range(num_k_tiles):
# Wait for A/B buffers to be empty before loading into them
handle = ab_producer.acquire_and_advance()
# Issue TMA loads (use k_tile_idx like fp16_gemm_3_1.py)
cute.copy(
tma_atom_a,
tAgA_slice[(None, k_tile_idx)],
tAsA[(None, handle.index)],
tma_bar_ptr=handle.barrier,
mcast_mask=tma_mcast_mask_a,
)
cute.copy(
tma_atom_b,
tBgB_slice[(None, k_tile_idx)],
tBsB[(None, handle.index)],
tma_bar_ptr=handle.barrier,
mcast_mask=tma_mcast_mask_b,
)
# Rolling prefetch: prefetch future k-tiles into L2 cache
# This keeps the L2 primed as we progress through the K dimension
if k_tile_idx + prefetch_dist < num_k_tiles:
future_k_tile = k_tile_idx + prefetch_dist
cute.prefetch(tma_atom_a, tAgA_slice[(None, future_k_tile)])
cute.prefetch(tma_atom_b, tBgB_slice[(None, future_k_tile)])
# Advance to next tile
if cutlass.const_expr(use_clc_dynamic_scheduler):
clc_pipeline.consumer_wait(clc_consumer_state)
work_tile = tile_sched.get_current_work()
clc_pipeline.consumer_release(clc_consumer_state)
clc_consumer_state.advance()
else:
tile_sched.advance_to_next_work()
work_tile = tile_sched.get_current_work()
# This mbarrier_wait is preventing threadblocks within a set of dependent threadblocks within the cluster
# (dependent in the context of the TMA/MMA synchronization pattern) to exit early making
# a late tcgen05 commit_arrive illegal
ab_producer.tail()
# Sched warp (only for dynamic scheduler)
if cutlass.const_expr(use_clc_dynamic_scheduler):
is_first_cta_in_cluster = cta_rank_in_cluster == 0
if warp_idx == sched_warp_id and is_first_cta_in_cluster:
# Persistent tile scheduling loop
clc_producer_state = pipeline.make_pipeline_state(
pipeline.PipelineUserType.ProducerConsumer, num_clc_stage
)
while work_tile.is_valid_tile:
# Advance to next tile
clc_pipeline.producer_acquire(clc_producer_state)
mbarrier_addr = clc_pipeline.producer_get_barrier(clc_producer_state)
tile_sched.advance_to_next_work(mbarrier_addr)
clc_producer_state.advance()
clc_pipeline.consumer_wait(clc_consumer_state)
work_tile = tile_sched.get_current_work()
clc_pipeline.consumer_release(clc_consumer_state)
clc_consumer_state.advance()
clc_pipeline.producer_tail(clc_producer_state)
# MMA warp
if warp_idx == mma_warp_id:
# Wait for TMEM allocation and retrieve pointer
tmem.wait_for_alloc()
tmem_ptr = tmem.retrieve_ptr(acc_dtype)
# (MMA, MMA_M, MMA_N, STAGE)
tCtAcc_base = cute.make_tensor(tmem_ptr, tCtAcc_fake.layout)
while work_tile.is_valid_tile:
if is_leader_cta:
# Wait for accumulator buffer empty
acc_empty = acc_producer.acquire_and_advance()
# Set tensor memory buffer for current tile
# (MMA, MMA_M, MMA_N)
tCtAcc = tCtAcc_base[(None, None, None, acc_empty.index)]
tiled_mma.set(tcgen05.Field.ACCUMULATE, False)
for k_tile_idx in range(num_k_tiles):
# Wait for TMA copies to complete
handle = ab_consumer.wait_and_advance()
# Execute one K-block worth of MMA instructions
num_k_blocks = cute.size(tCrA, mode=[2])
for k_block_idx in cutlass.range_constexpr(num_k_blocks):
k_block_coord = (None, None, k_block_idx, handle.index)
cute.gemm(
tiled_mma,
tCtAcc,
tCrA[k_block_coord],
tCrB[k_block_coord],
tCtAcc,
)
tiled_mma.set(tcgen05.Field.ACCUMULATE, True)
# Signal that the A/B buffers have been consumed and are ready for the next load
handle.release()
# Signal that the accumulator is fully computed
acc_empty.commit()
# Advance to next tile
if cutlass.const_expr(use_clc_dynamic_scheduler):
clc_pipeline.consumer_wait(clc_consumer_state)
work_tile = tile_sched.get_current_work()
clc_pipeline.consumer_release(clc_consumer_state)
clc_consumer_state.advance()
else:
tile_sched.advance_to_next_work()
work_tile = tile_sched.get_current_work()
# Wait for accumulator buffer empty
acc_producer.tail()
# Epilogue warps
if warp_idx < mma_warp_id:
# Allocate TMEM (only epilogue warp 0 actually allocates)
num_tmem_cols = 512
tmem.allocate(num_tmem_cols)
# Wait for TMEM allocation and retrieve pointer
tmem.wait_for_alloc()
tmem_ptr = tmem.retrieve_ptr(acc_dtype)
# (MMA, MMA_M, MMA_N, STAGE)
tCtAcc_base = cute.make_tensor(tmem_ptr, tCtAcc_fake.layout)
# Initialize TMA store pipeline for epilogue
epilogue_pipeline_producer_group = pipeline.CooperativeGroup(
pipeline.Agent.Thread,
size=128,
)
epilogue_pipeline = pipeline.PipelineTmaStore.create(
num_stages=epi_stages,
producer_group=epilogue_pipeline_producer_group,
)
copy_atom_t2r = cute.make_copy_atom(
tcgen05.Ld32x32bOp(tcgen05.Repetition.x32, tcgen05.Pack.NONE),
cutlass.Float32,
)
while work_tile.is_valid_tile:
# Get tile coord from tile scheduler
cur_tile_coord = work_tile.tile_idx
mma_tile_coord_mnl = (
cur_tile_coord[0] // cute.size(tiled_mma.thr_id.shape),
cur_tile_coord[1],
cur_tile_coord[2],
)
# Wait for accumulator buffer full
acc_full = acc_consumer.wait_and_advance()
# Set tensor memory buffer for current tile
# (MMA, MMA_M, MMA_N)
tCtAcc = tCtAcc_base[(None, None, None, acc_full.index)]
# (EPI_TILE_M, EPI_TILE_N, EPI_M, EPI_N)
tCtAcc_epi = cute.flat_divide(
tCtAcc[((None, None), 0, 0)], # why 0,0 ?
epi_tile,
)
mma_tile_coord_mn = cute.slice_(mma_tile_coord_mnl, (None, None, 0))
# (EPI_TILE_M, EPI_TILE_N, EPI_M, EPI_N, RestM, RestN)
tCgC_epi = cute.flat_divide(
tCgC[((None, None), 0, 0, *mma_tile_coord_mn)], epi_tile
)
tCgC_tma_cur_tile = tCgC_tma[(None, None, None, *mma_tile_coord_mn)]
# Tiled copy for TMEM -> RMEM load
tiled_copy_t2r = tcgen05.make_tmem_copy(
copy_atom_t2r, tCtAcc_epi[(None, None, 0, 0)]
)
thr_copy_t2r = tiled_copy_t2r.get_slice(tidx)
# (T2R, T2R_M, T2R_N, EPI_M, EPI_N)
tTR_tAcc = thr_copy_t2r.partition_S(tCtAcc_epi)
# (T2R, T2R_M, T2R_N, EPI_M, EPI_N)
tTR_gC = thr_copy_t2r.partition_D(tCgC_epi)
# (T2R, T2R_M, T2R_N)
tTR_rAcc = cute.make_rmem_tensor(
tTR_gC[(None, None, None, 0, 0)].shape, cutlass.Float32
)
tTR_tAcc = cute.group_modes(tTR_tAcc, 3, cute.rank(tTR_tAcc))
# Copy atom and tiled copy for RMEM -> SMEM load
copy_atom_r2s = cutlass.utils.blackwell_helpers.get_smem_store_op(
c_smem_layout_kind, cutlass.Float32, cutlass.Float32, tiled_copy_t2r
)
tiled_copy_r2s = cute.make_tiled_copy_D(copy_atom_r2s, tiled_copy_t2r)
# (R2S, R2S_M, R2S_N, PIPE_D)
thr_copy_r2s = tiled_copy_r2s.get_slice(tidx)
tRS_sC = thr_copy_r2s.partition_D(sC)
tRS_rAcc = tiled_copy_r2s.retile(tTR_rAcc)
tRS_rC = cute.make_rmem_tensor(tRS_rAcc.shape, io_dtype)
tCgC_grouped = cute.group_modes(
tCgC_tma_cur_tile, 1, cute.rank(tCgC_tma_cur_tile)
)
subtile_cnt = cute.size(tTR_tAcc.shape, mode=[3])
# Epilogue tiling loop
for subtile_idx in cutlass.range(subtile_cnt):
# TMEM -> RMEM
tTR_tAcc_slice = tTR_tAcc[(None, None, None, subtile_idx)]
cute.copy(tiled_copy_t2r, tTR_tAcc_slice, tTR_rAcc)
# RMEM -> SMEM
c_buffer = subtile_idx % epi_stages
tRS_sC_slice = tRS_sC[(None, None, None, c_buffer)]
# type conversion
tRS_rC.store(tRS_rAcc.load().to(io_dtype))
cute.copy(tiled_copy_r2s, tRS_rC, tRS_sC_slice)
# Memory fence and barrier to ensure shared memory stores are visible to TMA stores
cute.arch.fence_view_async_shared()
epilogue_sync_barrier.arrive_and_wait()
# SMEM -> GMEM
if warp_idx == epilogue_warp_ids[0]:
cute.copy(
tma_atom_c,
tCsC[(None, c_buffer)],
tCgC_grouped[(None, subtile_idx)],
)
epilogue_pipeline.producer_commit()
epilogue_pipeline.producer_acquire()
epilogue_sync_barrier.arrive_and_wait()
# Async arrive accumulator buffer empty
with cute.arch.elect_one():
acc_full.release()
# Advance to next tile
if cutlass.const_expr(use_clc_dynamic_scheduler):
clc_pipeline.consumer_wait(clc_consumer_state)
work_tile = tile_sched.get_current_work()
clc_pipeline.consumer_release(clc_consumer_state)
clc_consumer_state.advance()
else:
tile_sched.advance_to_next_work()
work_tile = tile_sched.get_current_work()
# Wait for C store complete
epilogue_pipeline.producer_tail()
# Dealloc the tensor memory buffer
tmem.relinquish_alloc_permit()
tmem.free(tmem_ptr)
def compute_grid(
c: cute.Tensor,
mma_tiler_mnk: Tuple[int, int, int],
cluster_shape_mnk: Tuple[int, int, int],
scheduler_type: Union[
utils.StaticPersistentTileScheduler, utils.ClcDynamicPersistentTileScheduler
],
max_active_clusters: cutlass.Constexpr,
) -> Tuple[
Union[
utils.ClcDynamicPersistentTileSchedulerParams,
utils.PersistentTileSchedulerParams,
],
Tuple[int, int, int],
]:
c_shape = cute.slice_(mma_tiler_mnk, (None, None, 0))
gc = cute.zipped_divide(c, tiler=c_shape)
num_ctas_mn = gc[(0, (None, None))].shape
if cutlass.const_expr(
issubclass(scheduler_type, utils.ClcDynamicPersistentTileScheduler)
):
tile_sched_params = utils.ClcDynamicPersistentTileSchedulerParams(
(*num_ctas_mn, 1), cluster_shape_mnk
)
grid = utils.ClcDynamicPersistentTileScheduler.get_grid_shape(tile_sched_params)
else:
tile_sched_params = utils.PersistentTileSchedulerParams(
(*num_ctas_mn, 1), cluster_shape_mnk
)
grid = utils.StaticPersistentTileScheduler.get_grid_shape(
tile_sched_params, max_active_clusters
)
return tile_sched_params, grid
@cute.jit
def host_function(
a: cute.Tensor,
b: cute.Tensor,
c: cute.Tensor,
max_active_clusters: cutlass.Constexpr,
):
#
# Construct tiled MMA
#
op = tcgen05.MmaF16BF16Op(
io_dtype,
acc_dtype,
mma_inst_shape_mnk,
tcgen05.CtaGroup.TWO if use_2cta_instrs else tcgen05.CtaGroup.ONE,
tcgen05.OperandSource.SMEM,
tcgen05.OperandMajorMode.K,
tcgen05.OperandMajorMode.K,
)
tiled_mma = cute.make_tiled_mma(op)
#
# Construct SMEM layouts for A and B
#
a_smem_layout = sm100_utils.make_smem_layout_a(
tiled_mma,
mma_tiler_mnk,
a.element_type,
ab_stages,
)
b_smem_layout = sm100_utils.make_smem_layout_b(
tiled_mma,
mma_tiler_mnk,
b.element_type,
ab_stages,
)
# c_smem_layout_kind is an enum for row/column major, not a CuTe layout
c_smem_layout_kind = utils.LayoutEnum.from_tensor(c)
#
# Construct the VMNK layout
#
cta_layout_mnk = cute.make_layout(cluster_shape_mnk)
cta_layout_vmnk = cute.tiled_divide(cta_layout_mnk, (tiled_mma.thr_id,))
#
# Construct TMA load atoms
#
op = cute.nvgpu.cpasync.CopyBulkTensorTileG2SMulticastOp(
tcgen05.CtaGroup.TWO if use_2cta_instrs else tcgen05.CtaGroup.ONE
)
a_smem_layout_slice = cute.slice_(a_smem_layout, (None, None, None, 0))
tma_atom_a, a_tma_tensor = cute.nvgpu.make_tiled_tma_atom_A(
op,
a,
a_smem_layout_slice,
mma_tiler_mnk,
tiled_mma,
cta_layout_vmnk.shape,
)
b_smem_layout_slice = cute.slice_(b_smem_layout, (None, None, None, 0))
tma_atom_b, b_tma_tensor = cute.nvgpu.make_tiled_tma_atom_B(
op,
b,
b_smem_layout_slice,
mma_tiler_mnk,
tiled_mma,
cta_layout_vmnk.shape,
)
cta_tile_shape_mnk = (
mma_tiler_mnk[0] // cute.size(tiled_mma.thr_id),
mma_tiler_mnk[1],
mma_tiler_mnk[2],
)
epi_tile = utils.compute_epilogue_tile_shape(
cta_tile_shape_mnk,
use_2cta_instrs,
c_smem_layout_kind,
io_dtype,
)
epi_smem_layout_staged = cutlass.utils.blackwell_helpers.make_smem_layout_epi(
io_dtype,
c_smem_layout_kind,
epi_tile,
epi_stages,
)
epi_smem_layout = cute.slice_(epi_smem_layout_staged, (None, None, 0))
tma_atom_c, c_tma_tensor = cute.nvgpu.cpasync.make_tiled_tma_atom(
cute.nvgpu.cpasync.CopyBulkTensorTileS2GOp(),
c,
epi_smem_layout,
epi_tile,
)
#
# Launch the kernel
#
tile_sched_params, grid_shape = compute_grid(
c,
cta_tile_shape_mnk,
cluster_shape_mnk,
scheduler_type,
max_active_clusters,
)
kernel(
tiled_mma,
tma_atom_a,
a_tma_tensor,
tma_atom_b,
b_tma_tensor,
tma_atom_c,
c_tma_tensor,
a_smem_layout,
b_smem_layout,
c_smem_layout_kind,
epi_smem_layout_staged,
epi_tile,
cta_layout_vmnk,
tile_sched_params,
).launch(
grid=grid_shape,
block=[224, 1, 1] if use_clc_dynamic_scheduler else [192, 1, 1],
cluster=cluster_shape_mnk,
)
def run_dense_gemm(
mnk: Tuple[int, int, int],
tolerance: float,
):
global torch, cutlass_torch
import torch
import cutlass.torch as cutlass_torch
print("===================================================================")
print("Running Blackwell fp16 GEMM example 5 (with TMA prefetch):")
print(f" mnk: {mnk}")
print(f" tolerance: {tolerance}")
print("===================================================================")
print()
m, n, k = mnk
torch.manual_seed(1111)
# Make K-major tensors (torch tensors are row-major)
def make_tensors(mn, k, dtype):
shape = (mn, k)
return (
torch.empty(*shape, dtype=torch.int32)
.random_(-2, 2)
.to(device="cuda", dtype=dtype)
)
a = make_tensors(m, k, cutlass_torch.dtype(io_dtype))
b = make_tensors(n, k, cutlass_torch.dtype(io_dtype))
c = make_tensors(m, n, cutlass_torch.dtype(io_dtype))
a_memref = from_dlpack(a).mark_layout_dynamic()
b_memref = from_dlpack(b).mark_layout_dynamic()
c_memref = from_dlpack(c).mark_layout_dynamic()
max_active_clusters = utils.HardwareInfo().get_max_active_clusters(
cluster_shape_mnk[0] * cluster_shape_mnk[1]
)
# Entry point to the host JIT function
host_function(
a_memref,
b_memref,
c_memref,
max_active_clusters,
no_cache=True,
)
# Compute reference result and verify
ref = (torch.einsum("mk,nk->mn", a, b)).cpu()
torch.testing.assert_close(
c.cpu(), ref.to(cutlass_torch.dtype(io_dtype)), atol=tolerance, rtol=1e-05
)
if __name__ == "__main__":
def parse_comma_separated_ints(s: str):
try:
return [int(x.strip()) for x in s.split(",")]
except ValueError:
raise argparse.ArgumentTypeError(
"Invalid format. Expected comma-separated integers."
)
from cuda.bindings import driver as cu_driver
cu_driver.cuInit(0)
err, device_count = cu_driver.cuDeviceGetCount()
if err != cu_driver.CUresult.CUDA_SUCCESS or device_count < 1:
raise RuntimeError("A GPU is required to run this example")
parser = argparse.ArgumentParser(
description="Blackwell fp16 GEMM example 5 (with TMA prefetch)"
)
parser.add_argument(
"--mnk",
type=parse_comma_separated_ints,
default=(8192, 8192, 8192),
help="MNK dimensions (comma-separated)",
)
parser.add_argument(
"--tolerance", type=float, default=1e-01, help="Tolerance for validation"
)
args = parser.parse_args()
if len(args.mnk) != 3:
parser.error("--mnk must contain exactly 3 values")
run_dense_gemm(
args.mnk,
args.tolerance,
)
print("PASS")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,778 @@
# Copyright (c) 2025 - 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import argparse
import os
import sys
from typing import Type, Tuple
import cuda.bindings.driver as cuda
import torch
import cutlass
import cutlass.cute as cute
import cutlass.utils as utils
import cutlass.pipeline as pipeline
from cutlass.cute.nvgpu import cpasync, tcgen05
import cutlass.torch as cutlass_torch
import cutlass.utils.blackwell_helpers as sm100_utils
import cutlass.utils.blockscaled_layout as blockscaled_utils
from cutlass.cute.runtime import make_ptr
if __name__ == "__main__":
current_dir = os.path.dirname(os.path.abspath(__file__))
examples_dir = os.path.join(current_dir, "..", "..", "..", "..")
if examples_dir not in sys.path:
sys.path.insert(0, examples_dir)
from cute.blackwell.tutorial.tutorial_gemm.utils import create_parser, run
mma_tiler_mn = (128, 256)
mma_inst_shape_k = 64
ab_dtype = cutlass.Float4E2M1FN
sf_dtype = cutlass.Float8E4M3FN
c_dtype = cutlass.Float16
sf_vec_size = 16
"""
The first tutorial NVFP4 block-scaled batched GEMM demonstrating a simple kernel implementation in CuTeDSL
This example demonstrates the kernel implementation of block-scaled batched GEMM with NVFP4 data type.
With large tile sizes (128x256x256), it can achieve very high performance on 8k×8k×8k problem sizes.
It can serve as a starting point to help users quickly experiment with optimizations for
challenges that may arise with other problem sizes.
To run this example:
.. code-block:: bash
python examples/blackwell/tutorial_gemm/nvfp4_gemm_0.py \
--mnkl 8192,8192,8192,1 --do_benchmark
Constraints for this example:
* The problem size of m, n and k must be divisible by the tile size m&n&k (128,256,256)
* The scaling factor vector size is 16.
* The A/B matrices have data contiguous on the k dimension.
* The C matrix has data contiguous on the n dimension.
* The A/B matrix data type is Float4E2M1FN.
* The SFA/SFB matrix data type is Float8E4M3FN.
"""
class Sm100BlockScaledDenseGemmKernel:
def __init__(self):
self.threads_per_cta = 128
self.smem_capacity = utils.get_smem_capacity_in_bytes("sm_100")
self.num_tmem_alloc_cols = 512
# set stages for ab_pipeline and acc_pipeline
self.num_acc_stage = 1
self.num_ab_stage = 4
@cute.jit
def __call__(
self,
a_ptr: cute.Pointer,
b_ptr: cute.Pointer,
sfa_ptr: cute.Pointer,
sfb_ptr: cute.Pointer,
c_ptr: cute.Pointer,
problem_size: tuple,
stream: cuda.CUstream,
epilogue_op: cutlass.Constexpr = lambda x: x,
):
# setup static attributes before smem/grid/tma computation
self.c_layout = utils.LayoutEnum.ROW_MAJOR
m, n, k, l = problem_size
# Setup attributes that depend on gemm inputs
mma_inst_tile_k = 4
self.mma_tiler = (
mma_tiler_mn[0],
mma_tiler_mn[1],
mma_inst_shape_k * mma_inst_tile_k,
)
self.cta_tile_shape_mnk = (
self.mma_tiler[0],
self.mma_tiler[1],
self.mma_tiler[2],
)
a_tensor = cute.make_tensor(
a_ptr,
cute.make_layout(
(m, cute.assume(k, 32), l),
stride=(cute.assume(k, 32), 1, cute.assume(m * k, 32)),
),
)
b_tensor = cute.make_tensor(
b_ptr,
cute.make_layout(
(n, cute.assume(k, 32), l),
stride=(cute.assume(k, 32), 1, cute.assume(n * k, 32)),
),
)
# make address offset of c_tensor 256bit aligned,
# so that epilogue could use vectorized store with larger vector size.
c_tensor = cute.make_tensor(
c_ptr,
cute.make_layout(
(cute.assume(m, 32), cute.assume(n, 16), l),
stride=(cute.assume(n, 16), 1, cute.assume(m * n, 512)),
),
)
# Setup sfa/sfb tensor by filling A/B tensor to scale factor atom layout
# ((Atom_M, Rest_M),(Atom_K, Rest_K),RestL)
sfa_layout = blockscaled_utils.tile_atom_to_shape_SF(
a_tensor.shape, sf_vec_size
)
sfa_tensor = cute.make_tensor(sfa_ptr, sfa_layout)
# ((Atom_N, Rest_N),(Atom_K, Rest_K),RestL)
sfb_layout = blockscaled_utils.tile_atom_to_shape_SF(
b_tensor.shape, sf_vec_size
)
sfb_tensor = cute.make_tensor(sfb_ptr, sfb_layout)
mma_op = tcgen05.MmaMXF4NVF4Op(
sf_dtype,
(*mma_tiler_mn, mma_inst_shape_k),
tcgen05.CtaGroup.ONE,
tcgen05.OperandSource.SMEM,
)
tiled_mma = cute.make_tiled_mma(mma_op)
self.cluster_layout_vmnk = cute.tiled_divide(
cute.make_layout((1, 1, 1)),
(tiled_mma.thr_id.shape,),
)
# Compute A/B/SFA/SFB/C shared memory layout
self.a_smem_layout_staged = sm100_utils.make_smem_layout_a(
tiled_mma,
self.mma_tiler,
ab_dtype,
self.num_ab_stage,
)
self.b_smem_layout_staged = sm100_utils.make_smem_layout_b(
tiled_mma,
self.mma_tiler,
ab_dtype,
self.num_ab_stage,
)
self.sfa_smem_layout_staged = blockscaled_utils.make_smem_layout_sfa(
tiled_mma,
self.mma_tiler,
sf_vec_size,
self.num_ab_stage,
)
self.sfb_smem_layout_staged = blockscaled_utils.make_smem_layout_sfb(
tiled_mma,
self.mma_tiler,
sf_vec_size,
self.num_ab_stage,
)
atom_thr_size = cute.size(tiled_mma.thr_id.shape)
# TMA load for A
a_smem_layout = cute.slice_(self.a_smem_layout_staged, (None, None, None, 0))
tma_atom_a, tma_tensor_a = cute.nvgpu.make_tiled_tma_atom_A(
cpasync.CopyBulkTensorTileG2SOp(tcgen05.CtaGroup.ONE),
a_tensor,
a_smem_layout,
self.mma_tiler,
tiled_mma,
self.cluster_layout_vmnk.shape,
)
# TMA load for B
b_smem_layout = cute.slice_(self.b_smem_layout_staged, (None, None, None, 0))
tma_atom_b, tma_tensor_b = cute.nvgpu.make_tiled_tma_atom_B(
cpasync.CopyBulkTensorTileG2SOp(tcgen05.CtaGroup.ONE),
b_tensor,
b_smem_layout,
self.mma_tiler,
tiled_mma,
self.cluster_layout_vmnk.shape,
)
# TMA load for SFA
sfa_smem_layout = cute.slice_(
self.sfa_smem_layout_staged, (None, None, None, 0)
)
tma_atom_sfa, tma_tensor_sfa = cute.nvgpu.make_tiled_tma_atom_A(
cpasync.CopyBulkTensorTileG2SOp(tcgen05.CtaGroup.ONE),
sfa_tensor,
sfa_smem_layout,
self.mma_tiler,
tiled_mma,
self.cluster_layout_vmnk.shape,
internal_type=cutlass.Int16,
)
# TMA load for SFB
sfb_smem_layout = cute.slice_(
self.sfb_smem_layout_staged, (None, None, None, 0)
)
tma_atom_sfb, tma_tensor_sfb = cute.nvgpu.make_tiled_tma_atom_B(
cpasync.CopyBulkTensorTileG2SOp(tcgen05.CtaGroup.ONE),
sfb_tensor,
sfb_smem_layout,
self.mma_tiler,
tiled_mma,
self.cluster_layout_vmnk.shape,
internal_type=cutlass.Int16,
)
# Compute TMA load bytes
a_copy_size = cute.size_in_bytes(ab_dtype, a_smem_layout)
b_copy_size = cute.size_in_bytes(ab_dtype, b_smem_layout)
sfa_copy_size = cute.size_in_bytes(sf_dtype, sfa_smem_layout)
sfb_copy_size = cute.size_in_bytes(sf_dtype, sfb_smem_layout)
self.num_tma_load_bytes = (
a_copy_size + b_copy_size + sfa_copy_size + sfb_copy_size
) * atom_thr_size
# Compute grid size
grid = (
cute.ceil_div(c_tensor.shape[0], self.cta_tile_shape_mnk[0]),
cute.ceil_div(c_tensor.shape[1], self.cta_tile_shape_mnk[1]),
c_tensor.shape[2],
)
# Launch the kernel synchronously
self.kernel(
tiled_mma,
tma_atom_a,
tma_tensor_a,
tma_atom_b,
tma_tensor_b,
tma_atom_sfa,
tma_tensor_sfa,
tma_atom_sfb,
tma_tensor_sfb,
c_tensor,
self.a_smem_layout_staged,
self.b_smem_layout_staged,
self.sfa_smem_layout_staged,
self.sfb_smem_layout_staged,
epilogue_op,
).launch(
grid=grid,
block=[self.threads_per_cta, 1, 1],
cluster=(1, 1, 1),
stream=stream,
)
return
# GPU device kernel
@cute.kernel
def kernel(
self,
tiled_mma: cute.TiledMma,
tma_atom_a: cute.CopyAtom,
mA_mkl: cute.Tensor,
tma_atom_b: cute.CopyAtom,
mB_nkl: cute.Tensor,
tma_atom_sfa: cute.CopyAtom,
mSFA_mkl: cute.Tensor,
tma_atom_sfb: cute.CopyAtom,
mSFB_nkl: cute.Tensor,
mC_mnl: cute.Tensor,
a_smem_layout_staged: cute.ComposedLayout,
b_smem_layout_staged: cute.ComposedLayout,
sfa_smem_layout_staged: cute.Layout,
sfb_smem_layout_staged: cute.Layout,
epilogue_op: cutlass.Constexpr,
):
"""
GPU device kernel performing the batched GEMM computation.
"""
warp_idx = cute.arch.warp_idx()
warp_idx = cute.arch.make_warp_uniform(warp_idx)
tidx, _, _ = cute.arch.thread_idx()
#
# Setup cta/thread coordinates
#
# Coords inside cluster
bidx, bidy, bidz = cute.arch.block_idx()
# Coords outside cluster
cta_coord = (bidx, bidy, bidz)
mma_tile_coord_mnl = (
cta_coord[0] // cute.size(tiled_mma.thr_id.shape),
cta_coord[1],
cta_coord[2],
)
#
# Define shared storage for kernel
#
@cute.struct
class SharedStorage:
ab_mbar_ptr: cute.struct.MemRange[cutlass.Int64, self.num_ab_stage * 2]
acc_mbar_ptr: cute.struct.MemRange[cutlass.Int64, self.num_acc_stage * 2]
tmem_holding_buf: cutlass.Int32
smem = utils.SmemAllocator()
storage = smem.allocate(SharedStorage)
# (MMA, MMA_M, MMA_K, STAGE)
sA = smem.allocate_tensor(
element_type=ab_dtype,
layout=a_smem_layout_staged.outer,
byte_alignment=128,
swizzle=a_smem_layout_staged.inner,
)
# (MMA, MMA_N, MMA_K, STAGE)
sB = smem.allocate_tensor(
element_type=ab_dtype,
layout=b_smem_layout_staged.outer,
byte_alignment=128,
swizzle=b_smem_layout_staged.inner,
)
# (MMA, MMA_M, MMA_K, STAGE)
sSFA = smem.allocate_tensor(
element_type=sf_dtype,
layout=sfa_smem_layout_staged,
byte_alignment=128,
)
# (MMA, MMA_N, MMA_K, STAGE)
sSFB = smem.allocate_tensor(
element_type=sf_dtype,
layout=sfb_smem_layout_staged,
byte_alignment=128,
)
#
# Initialize mainloop ab_pipeline, acc_pipeline and their states
#
ab_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread)
ab_pipeline_consumer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread, 1)
ab_producer, ab_consumer = pipeline.PipelineTmaUmma.create(
barrier_storage=storage.ab_mbar_ptr.data_ptr(),
num_stages=self.num_ab_stage,
producer_group=ab_pipeline_producer_group,
consumer_group=ab_pipeline_consumer_group,
tx_count=self.num_tma_load_bytes,
).make_participants()
acc_producer, acc_consumer = pipeline.PipelineUmmaAsync.create(
barrier_storage=storage.acc_mbar_ptr.data_ptr(),
num_stages=self.num_acc_stage,
producer_group=ab_pipeline_producer_group,
consumer_group=pipeline.CooperativeGroup(
pipeline.Agent.Thread,
self.threads_per_cta,
),
).make_participants()
#
# Local_tile partition global tensors
#
# (bM, bK, RestM, RestK, RestL)
gA_mkl = cute.local_tile(
mA_mkl, cute.slice_(self.mma_tiler, (None, 0, None)), (None, None, None)
)
# (bN, bK, RestN, RestK, RestL)
gB_nkl = cute.local_tile(
mB_nkl, cute.slice_(self.mma_tiler, (0, None, None)), (None, None, None)
)
gSFA_mkl = cute.local_tile(
mSFA_mkl, cute.slice_(self.mma_tiler, (None, 0, None)), (None, None, None)
)
gSFB_nkl = cute.local_tile(
mSFB_nkl, cute.slice_(self.mma_tiler, (0, None, None)), (None, None, None)
)
# (bM, bN, RestM, RestN, RestL)
gC_mnl = cute.local_tile(
mC_mnl, cute.slice_(self.mma_tiler, (None, None, 0)), (None, None, None)
)
k_tile_cnt = cute.size(gA_mkl, mode=[3])
#
# Partition global tensor for TiledMMA_A/B/SFA/SFB/C
#
# (MMA, MMA_M, MMA_K, RestK)
thr_mma = tiled_mma.get_slice(0)
# (MMA, MMA_M, MMA_K, RestM, RestK, RestL)
tCgA = thr_mma.partition_A(gA_mkl)
# (MMA, MMA_N, MMA_K, RestN, RestK, RestL)
tCgB = thr_mma.partition_B(gB_nkl)
# (MMA, MMA_M, MMA_K, RestM, RestK, RestL)
tCgSFA = thr_mma.partition_A(gSFA_mkl)
# (MMA, MMA_N, MMA_K, RestN, RestK, RestL)
tCgSFB = thr_mma.partition_B(gSFB_nkl)
# (MMA, MMA_M, MMA_N, RestM, RestN, RestL)
tCgC = thr_mma.partition_C(gC_mnl)
#
# Partition global/shared tensor for TMA load A/B/SFA/SFB
#
# TMA load A partition_S/D
# ((atom_v, rest_v), STAGE)
# ((atom_v, rest_v), RestM, RestK, RestL)
tAsA, tAgA = cpasync.tma_partition(
tma_atom_a,
0,
cute.make_layout(1),
cute.group_modes(sA, 0, 3),
cute.group_modes(tCgA, 0, 3),
)
# TMA load B partition_S/D
# ((atom_v, rest_v), STAGE)
# ((atom_v, rest_v), RestN, RestK, RestL)
tBsB, tBgB = cpasync.tma_partition(
tma_atom_b,
0,
cute.make_layout(1),
cute.group_modes(sB, 0, 3),
cute.group_modes(tCgB, 0, 3),
)
# TMA load partition for SFA tensor
# ((atom_v, rest_v), STAGE)
# ((atom_v, rest_v), RestM, RestK, RestL)
tAsSFA, tAgSFA = cpasync.tma_partition(
tma_atom_sfa,
0,
cute.make_layout(1),
cute.group_modes(sSFA, 0, 3),
cute.group_modes(tCgSFA, 0, 3),
)
tAsSFA = cute.filter_zeros(tAsSFA)
tAgSFA = cute.filter_zeros(tAgSFA)
# TMA load partition for SFB tensor
# ((atom_v, rest_v), STAGE)
# ((atom_v, rest_v), RestN, RestK, RestL)
tBsSFB, tBgSFB = cpasync.tma_partition(
tma_atom_sfb,
0,
cute.make_layout(1),
cute.group_modes(sSFB, 0, 3),
cute.group_modes(tCgSFB, 0, 3),
)
tBsSFB = cute.filter_zeros(tBsSFB)
tBgSFB = cute.filter_zeros(tBgSFB)
#
# Partition shared/tensor memory tensor for TiledMMA_A/B/C
#
# (MMA, MMA_M, MMA_K, STAGE)
tCrA = tiled_mma.make_fragment_A(sA)
# (MMA, MMA_N, MMA_K, STAGE)
tCrB = tiled_mma.make_fragment_B(sB)
# (MMA, MMA_M, MMA_N)
acc_shape = tiled_mma.partition_shape_C(self.mma_tiler[:2])
# (MMA, MMA_M, MMA_N)
tCtAcc_fake = tiled_mma.make_fragment_C(acc_shape)
#
# Alloc tensor memory buffer
#
tmem_alloc_barrier = pipeline.NamedBarrier(
barrier_id=1,
num_threads=self.threads_per_cta,
)
tmem = utils.TmemAllocator(
storage.tmem_holding_buf.ptr,
barrier_for_retrieve=tmem_alloc_barrier,
)
tmem.allocate(self.num_tmem_alloc_cols)
tmem.wait_for_alloc()
acc_tmem_ptr = tmem.retrieve_ptr(cutlass.Float32)
tCtAcc = cute.make_tensor(acc_tmem_ptr, tCtAcc_fake.layout)
#
# Make SFA/SFB tmem tensor
#
# Get SFA tmem ptr
sfa_tmem_ptr = cute.recast_ptr(
acc_tmem_ptr + tcgen05.find_tmem_tensor_col_offset(tCtAcc),
dtype=sf_dtype,
)
# (MMA, MMA_M, MMA_K)
tCtSFA_layout = blockscaled_utils.make_tmem_layout_sfa(
tiled_mma,
self.mma_tiler,
sf_vec_size,
cute.slice_(sfa_smem_layout_staged, (None, None, None, 0)),
)
tCtSFA = cute.make_tensor(sfa_tmem_ptr, tCtSFA_layout)
# Get SFB tmem ptr
sfb_tmem_ptr = cute.recast_ptr(
acc_tmem_ptr
+ tcgen05.find_tmem_tensor_col_offset(tCtAcc)
+ tcgen05.find_tmem_tensor_col_offset(tCtSFA),
dtype=sf_dtype,
)
# (MMA, MMA_N, MMA_K)
tCtSFB_layout = blockscaled_utils.make_tmem_layout_sfb(
tiled_mma,
self.mma_tiler,
sf_vec_size,
cute.slice_(sfb_smem_layout_staged, (None, None, None, 0)),
)
tCtSFB = cute.make_tensor(sfb_tmem_ptr, tCtSFB_layout)
#
# Partition for S2T copy of SFA/SFB
#
# Make S2T CopyAtom
copy_atom_s2t = cute.make_copy_atom(
tcgen05.Cp4x32x128bOp(tcgen05.CtaGroup.ONE),
sf_dtype,
)
# (MMA, MMA_MN, MMA_K, STAGE)
tCsSFA_compact = cute.filter_zeros(sSFA)
# (MMA, MMA_MN, MMA_K)
tCtSFA_compact = cute.filter_zeros(tCtSFA)
tiled_copy_s2t_sfa = tcgen05.make_s2t_copy(copy_atom_s2t, tCtSFA_compact)
thr_copy_s2t_sfa = tiled_copy_s2t_sfa.get_slice(0)
# ((ATOM_V, REST_V), Rest_Tiler, MMA_MN, MMA_K, STAGE)
tCsSFA_compact_s2t_ = thr_copy_s2t_sfa.partition_S(tCsSFA_compact)
# ((ATOM_V, REST_V), Rest_Tiler, MMA_MN, MMA_K, STAGE)
tCsSFA_compact_s2t = tcgen05.get_s2t_smem_desc_tensor(
tiled_copy_s2t_sfa, tCsSFA_compact_s2t_
)
# ((ATOM_V, REST_V), Rest_Tiler, MMA_MN, MMA_K)
tCtSFA_compact_s2t = thr_copy_s2t_sfa.partition_D(tCtSFA_compact)
# (MMA, MMA_MN, MMA_K, STAGE)
tCsSFB_compact = cute.filter_zeros(sSFB)
# (MMA, MMA_MN, MMA_K)
tCtSFB_compact = cute.filter_zeros(tCtSFB)
tiled_copy_s2t_sfb = tcgen05.make_s2t_copy(copy_atom_s2t, tCtSFB_compact)
thr_copy_s2t_sfb = tiled_copy_s2t_sfb.get_slice(0)
# ((ATOM_V, REST_V), Rest_Tiler, MMA_MN, MMA_K, STAGE)
tCsSFB_compact_s2t_ = thr_copy_s2t_sfb.partition_S(tCsSFB_compact)
# ((ATOM_V, REST_V), Rest_Tiler, MMA_MN, MMA_K, STAGE)
tCsSFB_compact_s2t = tcgen05.get_s2t_smem_desc_tensor(
tiled_copy_s2t_sfb, tCsSFB_compact_s2t_
)
# ((ATOM_V, REST_V), Rest_Tiler, MMA_MN, MMA_K)
tCtSFB_compact_s2t = thr_copy_s2t_sfb.partition_D(tCtSFB_compact)
#
# Slice to per mma tile index
#
# ((atom_v, rest_v), RestK)
tAgA = tAgA[(None, mma_tile_coord_mnl[0], None, mma_tile_coord_mnl[2])]
# ((atom_v, rest_v), RestK)
tBgB = tBgB[(None, mma_tile_coord_mnl[1], None, mma_tile_coord_mnl[2])]
# ((atom_v, rest_v), RestK)
tAgSFA = tAgSFA[(None, mma_tile_coord_mnl[0], None, mma_tile_coord_mnl[2])]
# ((atom_v, rest_v), RestK)
tBgSFB = tBgSFB[(None, mma_tile_coord_mnl[1], None, mma_tile_coord_mnl[2])]
#
# Execute Data copy and Math computation in the k_tile loop
#
if warp_idx == 0:
# Wait for accumulator buffer empty
acc_empty = acc_producer.acquire_and_advance()
# Set ACCUMULATE field to False for the first k_tile iteration
tiled_mma.set(tcgen05.Field.ACCUMULATE, False)
# Execute k_tile loop
for k_tile in cutlass.range(
k_tile_cnt, prefetch_stages=self.num_ab_stage - 2
):
# Wait for AB buffer empty
ab_empty = ab_producer.acquire_and_advance()
# TMA load for A/B/SFA/SFB
cute.copy(
tma_atom_a,
tAgA[(None, ab_empty.count)],
tAsA[(None, ab_empty.index)],
tma_bar_ptr=ab_empty.barrier,
)
cute.copy(
tma_atom_b,
tBgB[(None, ab_empty.count)],
tBsB[(None, ab_empty.index)],
tma_bar_ptr=ab_empty.barrier,
)
cute.copy(
tma_atom_sfa,
tAgSFA[(None, ab_empty.count)],
tAsSFA[(None, ab_empty.index)],
tma_bar_ptr=ab_empty.barrier,
)
cute.copy(
tma_atom_sfb,
tBgSFB[(None, ab_empty.count)],
tBsSFB[(None, ab_empty.index)],
tma_bar_ptr=ab_empty.barrier,
)
# Wait for AB buffer full
ab_full = ab_consumer.wait_and_advance()
# Copy SFA/SFB to tmem
s2t_stage_coord = (None, None, None, None, ab_full.index)
tCsSFA_compact_s2t_staged = tCsSFA_compact_s2t[s2t_stage_coord]
tCsSFB_compact_s2t_staged = tCsSFB_compact_s2t[s2t_stage_coord]
cute.copy(
tiled_copy_s2t_sfa,
tCsSFA_compact_s2t_staged,
tCtSFA_compact_s2t,
)
cute.copy(
tiled_copy_s2t_sfb,
tCsSFB_compact_s2t_staged,
tCtSFB_compact_s2t,
)
# tCtAcc += tCrA * tCrSFA * tCrB * tCrSFB
num_kblocks = cute.size(tCrA, mode=[2])
for kblock_idx in cutlass.range(num_kblocks, unroll_full=True):
kblock_coord = (
None,
None,
kblock_idx,
ab_full.index,
)
# Set SFA/SFB tensor to tiled_mma
sf_kblock_coord = (None, None, kblock_idx)
tiled_mma.set(
tcgen05.Field.SFA,
tCtSFA[sf_kblock_coord].iterator,
)
tiled_mma.set(
tcgen05.Field.SFB,
tCtSFB[sf_kblock_coord].iterator,
)
cute.gemm(
tiled_mma,
tCtAcc,
tCrA[kblock_coord],
tCrB[kblock_coord],
tCtAcc,
)
# Enable accumulate on tCtAcc after first kblock
tiled_mma.set(tcgen05.Field.ACCUMULATE, True)
# Async arrive AB buffer empty
ab_full.release()
acc_empty.commit()
#
# Epilogue
# Partition for epilogue
#
op = tcgen05.Ld32x32bOp(tcgen05.Repetition.x128, tcgen05.Pack.NONE)
copy_atom_t2r = cute.make_copy_atom(op, cutlass.Float32)
tiled_copy_t2r = tcgen05.make_tmem_copy(copy_atom_t2r, tCtAcc)
thr_copy_t2r = tiled_copy_t2r.get_slice(tidx)
# (T2R_M, T2R_N, EPI_M, EPI_M)
tTR_tAcc = thr_copy_t2r.partition_S(tCtAcc)
# (T2R_M, T2R_N, EPI_M, EPI_N, RestM, RestN, RestL)
tTR_gC = thr_copy_t2r.partition_D(tCgC)
# (T2R_M, T2R_N, EPI_M, EPI_N
tTR_rAcc = cute.make_rmem_tensor(
tTR_gC[None, None, None, None, 0, 0, 0].shape, cutlass.Float32
)
# (T2R_M, T2R_N, EPI_M, EPI_N
tTR_rC = cute.make_rmem_tensor(
tTR_gC[None, None, None, None, 0, 0, 0].shape, c_dtype
)
# STG Atom
simt_atom = cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), c_dtype)
tTR_gC = tTR_gC[(None, None, None, None, *mma_tile_coord_mnl)]
# Release TMEM allocation lock
tmem.relinquish_alloc_permit()
# Wait for accumulator buffer full
acc_full = acc_consumer.wait_and_advance()
# Copy accumulator to register
cute.copy(tiled_copy_t2r, tTR_tAcc, tTR_rAcc)
acc_vec = epilogue_op(tTR_rAcc.load().to(c_dtype))
tTR_rC.store(acc_vec)
# Store C to global memory
cute.copy(simt_atom, tTR_rC, tTR_gC)
acc_full.release()
# Deallocate TMEM
cute.arch.barrier()
tmem.free(acc_tmem_ptr)
return
def run_nvfp4_gemm(
mnkl: Tuple[int, int, int, int],
tolerance: float,
do_benchmark: bool = False,
warmup_iterations: int = 10,
iterations: int = 100,
use_cold_l2: bool = True,
):
run(
gemm_class=Sm100BlockScaledDenseGemmKernel,
ab_dtype=ab_dtype,
sf_dtype=sf_dtype,
c_dtype=c_dtype,
sf_vec_size=sf_vec_size,
mma_tiler_mn=mma_tiler_mn,
cluster_shape_mnk=(1, 1, 1),
mnkl=mnkl,
tolerance=tolerance,
do_benchmark=do_benchmark,
warmup_iterations=warmup_iterations,
iterations=iterations,
use_cold_l2=use_cold_l2,
)
if __name__ == "__main__":
parser = create_parser()
args = parser.parse_args()
if len(args.mnkl) != 4:
parser.error("--mnkl must contain exactly 4 values")
m, n, k, _ = args.mnkl
if m % mma_tiler_mn[0] != 0:
parser.error("m must be multiples of mma_tiler_mn[0] (got m={})".format(m))
if n % mma_tiler_mn[1] != 0:
parser.error("n must be multiples of mma_tiler_mn[1] (got n={})".format(n))
if k % 256 != 0:
parser.error("k must be a multiple of 256 (got k={})".format(k))
run_nvfp4_gemm(
args.mnkl,
args.tolerance,
args.do_benchmark,
)
print("PASS")

View File

@@ -0,0 +1,934 @@
# Copyright (c) 2025 - 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# This is the second tutorial nvfp4 GEMM. It builds on the first tutorial by adding 2CTA MMA
# instructions with a 2x1 cluster.
import argparse
import os
import sys
from typing import Type, Tuple
import cuda.bindings.driver as cuda
import torch
import cutlass
import cutlass.cute as cute
import cutlass.utils as utils
import cutlass.pipeline as pipeline
from cutlass.cute.nvgpu import cpasync, tcgen05
import cutlass.torch as cutlass_torch
import cutlass.utils.blackwell_helpers as sm100_utils
import cutlass.utils.blockscaled_layout as blockscaled_utils
from cutlass.cute.runtime import from_dlpack, make_ptr
if __name__ == "__main__":
current_dir = os.path.dirname(os.path.abspath(__file__))
examples_dir = os.path.join(current_dir, "..", "..", "..", "..")
if examples_dir not in sys.path:
sys.path.insert(0, examples_dir)
from cute.blackwell.tutorial.tutorial_gemm.utils import create_parser, run
mma_tiler_mn = (256, 256)
mma_inst_shape_k = 64
ab_dtype = cutlass.Float4E2M1FN
sf_dtype = cutlass.Float8E4M3FN
c_dtype = cutlass.Float16
sf_vec_size = 16
cluster_shape_mnk = (2, 1, 1)
"""
The second tutorial further improves the performance of NVFP4 block-scaled batched GEMM
by adding 2CTA instructions and TMA multicast optimizations.
(1) The 2 CTA instructions could reduce the smem size requirement for B tensor,
increased num_ab_stage and improves the latency hiding capability.
For both 1CTA and 2CTA, the shared memory (smem) size per stage for the A, sfA, and sfB tensors is the same:
- For the A tensor, each stage requires 128 x 256 x sizeof(float4) = 16KB.
- For the sfA tensor, each stage requires 128 x (256 / 16) x sizeof(float8) = 2KB.
- For the sfB tensor, each stage requires 256 x (256 / 16) x sizeof(float8) = 4KB.
The situation is different for the B tensor:
- In the 1CTA case, each stage for the B tensor requires 256 x 256 x sizeof(float4) = 32KB.
- In the 2CTA case, only half this size is needed, i.e., 128 x 256 x sizeof(float4) = 16KB.
Therefore, the maximum number of AB stages is:
- For 1CTA: 227 // (16 + 32 + 2 + 4) = 4
- For 2CTA: 227 // (16 + 16 + 2 + 4) = 5
The latency hiding capability is:
- 1CTA: 512 * (4 - 1) = 1.5K cycles
- 2CTA: 512 * (5 - 1) = 2K cycles
(2) TMA multicast can help reduce L2 cache traffic.
Without TMA multicast, the L2 traffic per tile is typically 16KB + 32KB = 48KB (possibly less in practice, depending on hardware optimizations).
With TMA multicast in a cluster of shape (m, n), the L2 traffic per tile is reduced to 16KB / n + 32KB / m.
For example:
- In a 2x1 cluster: 16KB / 1 + 32KB / 2 = 24KB per tile
- In a 4x4 cluster: 16KB / 4 + 32KB / 4 = 12KB per tile
The first approach offers substantial capacity for hiding latency, whereas the second reduces the time required for data to become ready.
Both could be tried when the workload is latency-bound or limited by memory throughput.
To run this example:
.. code-block:: bash
python examples/blackwell/tutorial_gemm/nvfp4_gemm_1.py \
--mnkl 8192,8192,8192,1 --do_benchmark
Constraints for this example:
* The problem size of m, n and k must be divisible by the tile size m&n&k (256, 256, 256)
* The scaling factor vector size is 16.
* The A/B matrices have data contiguous on the k dimension.
* The C matrix has data contiguous on the n dimension.
* The A/B matrix data type is Float4E2M1FN.
* The SFA/SFB matrix data type is Float8E4M3FN.
"""
class Sm100BlockScaledDenseGemmKernel:
def __init__(self):
self.threads_per_cta = 128
self.smem_capacity = utils.get_smem_capacity_in_bytes("sm_100")
self.num_tmem_alloc_cols = 512
# set stages for ab_pipeline and acc_pipeline
self.num_acc_stage = 1
self.num_ab_stage = 5
@cute.jit
def __call__(
self,
a_ptr: cute.Pointer,
b_ptr: cute.Pointer,
sfa_ptr: cute.Pointer,
sfb_ptr: cute.Pointer,
c_ptr: cute.Pointer,
problem_size: tuple,
stream: cuda.CUstream,
epilogue_op: cutlass.Constexpr = lambda x: x,
):
# setup static attributes before smem/grid/tma computation
self.c_layout = utils.LayoutEnum.ROW_MAJOR
m, n, k, l = problem_size
self.use_2cta_instrs = False if mma_tiler_mn[0] == 128 else True
# Setup attributes that depend on gemm inputs
mma_inst_tile_k = 4
self.mma_tiler = (
mma_tiler_mn[0],
mma_tiler_mn[1],
mma_inst_shape_k * mma_inst_tile_k,
)
self.mma_inst_shape_sfb = (
mma_tiler_mn[0] // (2 if self.use_2cta_instrs else 1),
mma_tiler_mn[1],
mma_inst_shape_k,
)
self.mma_tiler_sfb = (
self.mma_inst_shape_sfb[0],
self.mma_inst_shape_sfb[1],
mma_inst_shape_k * mma_inst_tile_k,
)
a_tensor = cute.make_tensor(
a_ptr,
cute.make_layout(
(m, cute.assume(k, 32), l),
stride=(cute.assume(k, 32), 1, cute.assume(m * k, 32)),
),
)
b_tensor = cute.make_tensor(
b_ptr,
cute.make_layout(
(n, cute.assume(k, 32), l),
stride=(cute.assume(k, 32), 1, cute.assume(n * k, 32)),
),
)
# 256bit aligned. row_major
c_tensor = cute.make_tensor(
c_ptr,
cute.make_layout(
(cute.assume(m, 32), cute.assume(n, 16), l),
stride=(cute.assume(n, 16), 1, cute.assume(m * n, 512)),
),
)
# Setup sfa/sfb tensor by filling A/B tensor to scale factor atom layout
# ((Atom_M, Rest_M),(Atom_K, Rest_K),RestL)
sfa_layout = blockscaled_utils.tile_atom_to_shape_SF(
a_tensor.shape, sf_vec_size
)
sfa_tensor = cute.make_tensor(sfa_ptr, sfa_layout)
# ((Atom_N, Rest_N),(Atom_K, Rest_K),RestL)
sfb_layout = blockscaled_utils.tile_atom_to_shape_SF(
b_tensor.shape, sf_vec_size
)
sfb_tensor = cute.make_tensor(sfb_ptr, sfb_layout)
mma_op = tcgen05.MmaMXF4NVF4Op(
sf_dtype,
(*mma_tiler_mn, mma_inst_shape_k),
tcgen05.CtaGroup.ONE if not self.use_2cta_instrs else tcgen05.CtaGroup.TWO,
tcgen05.OperandSource.SMEM,
)
tiled_mma = cute.make_tiled_mma(mma_op)
# (CTA_Tile_Shape_M, Round_Up(MMA_Tile_Shape_N, 128), MMA_Inst_Shape_K)
# Note sfB don't support share among 2ctas
sfb_mma_op = tcgen05.MmaMXF4NVF4Op(
sf_dtype,
self.mma_inst_shape_sfb,
tcgen05.CtaGroup.ONE,
tcgen05.OperandSource.SMEM,
)
tiled_mma_sfb = cute.make_tiled_mma(sfb_mma_op)
self.cta_tile_shape_mnk = (
self.mma_tiler[0] // (2 if self.use_2cta_instrs else 1),
self.mma_tiler[1],
self.mma_tiler[2],
)
self.cta_tile_shape_mnk_sfb = (
self.mma_tiler_sfb[0] // (2 if self.use_2cta_instrs else 1),
self.mma_tiler_sfb[1],
self.mma_tiler_sfb[2],
)
self.cluster_layout_vmnk = cute.tiled_divide(
cute.make_layout(cluster_shape_mnk),
(tiled_mma.thr_id.shape,),
)
self.cluster_layout_sfb_vmnk = cute.tiled_divide(
cute.make_layout(cluster_shape_mnk),
(tiled_mma_sfb.thr_id.shape,),
)
# Compute number of multicast CTAs for A/B
self.num_mcast_ctas_a = cute.size(self.cluster_layout_vmnk.shape[2])
self.num_mcast_ctas_b = cute.size(self.cluster_layout_vmnk.shape[1])
self.num_mcast_ctas_sfb = cute.size(self.cluster_layout_sfb_vmnk.shape[1])
self.is_a_mcast = self.num_mcast_ctas_a > 1
self.is_b_mcast = self.num_mcast_ctas_b > 1
self.is_sfb_mcast = self.num_mcast_ctas_sfb > 1
# Compute A/B/SFA/SFB/C shared memory layout
self.a_smem_layout_staged = sm100_utils.make_smem_layout_a(
tiled_mma,
self.mma_tiler,
ab_dtype,
self.num_ab_stage,
)
self.b_smem_layout_staged = sm100_utils.make_smem_layout_b(
tiled_mma,
self.mma_tiler,
ab_dtype,
self.num_ab_stage,
)
self.sfa_smem_layout_staged = blockscaled_utils.make_smem_layout_sfa(
tiled_mma,
self.mma_tiler,
sf_vec_size,
self.num_ab_stage,
)
self.sfb_smem_layout_staged = blockscaled_utils.make_smem_layout_sfb(
tiled_mma,
self.mma_tiler,
sf_vec_size,
self.num_ab_stage,
)
atom_thr_size = cute.size(tiled_mma.thr_id.shape)
a_op = sm100_utils.cluster_shape_to_tma_atom_A(
cluster_shape_mnk[:2], tiled_mma.thr_id
)
# TMA load for A
a_smem_layout = cute.slice_(self.a_smem_layout_staged, (None, None, None, 0))
tma_atom_a, tma_tensor_a = cute.nvgpu.make_tiled_tma_atom_A(
a_op,
a_tensor,
a_smem_layout,
self.mma_tiler,
tiled_mma,
self.cluster_layout_vmnk.shape,
)
# TMA load for B
b_op = sm100_utils.cluster_shape_to_tma_atom_B(
cluster_shape_mnk[:2], tiled_mma.thr_id
)
b_smem_layout = cute.slice_(self.b_smem_layout_staged, (None, None, None, 0))
tma_atom_b, tma_tensor_b = cute.nvgpu.make_tiled_tma_atom_B(
b_op,
b_tensor,
b_smem_layout,
self.mma_tiler,
tiled_mma,
self.cluster_layout_vmnk.shape,
)
# TMA load for SFA
sfa_op = sm100_utils.cluster_shape_to_tma_atom_A(
cluster_shape_mnk[:2], tiled_mma.thr_id
)
sfa_smem_layout = cute.slice_(
self.sfa_smem_layout_staged, (None, None, None, 0)
)
tma_atom_sfa, tma_tensor_sfa = cute.nvgpu.make_tiled_tma_atom_A(
sfa_op,
sfa_tensor,
sfa_smem_layout,
self.mma_tiler,
tiled_mma,
self.cluster_layout_vmnk.shape,
internal_type=cutlass.Int16,
)
# TMA load for SFB
sfb_op = sm100_utils.cluster_shape_to_tma_atom_SFB(
cluster_shape_mnk[:2], tiled_mma.thr_id
)
sfb_smem_layout = cute.slice_(
self.sfb_smem_layout_staged, (None, None, None, 0)
)
tma_atom_sfb, tma_tensor_sfb = cute.nvgpu.make_tiled_tma_atom_B(
sfb_op,
sfb_tensor,
sfb_smem_layout,
self.mma_tiler_sfb,
tiled_mma_sfb,
self.cluster_layout_sfb_vmnk.shape,
internal_type=cutlass.Int16,
)
# Compute TMA load bytes
a_copy_size = cute.size_in_bytes(ab_dtype, a_smem_layout)
b_copy_size = cute.size_in_bytes(ab_dtype, b_smem_layout)
sfa_copy_size = cute.size_in_bytes(sf_dtype, sfa_smem_layout)
sfb_copy_size = cute.size_in_bytes(sf_dtype, sfb_smem_layout)
self.num_tma_load_bytes = (
a_copy_size + b_copy_size + sfa_copy_size + sfb_copy_size
) * atom_thr_size
# Compute grid size
grid = cute.round_up(
cute.ceil_div(
(c_tensor.layout.shape),
(self.cta_tile_shape_mnk[0], self.cta_tile_shape_mnk[1], 1),
),
cluster_shape_mnk,
)
# Launch the kernel
self.kernel(
tiled_mma,
tiled_mma_sfb,
tma_atom_a,
tma_tensor_a,
tma_atom_b,
tma_tensor_b,
tma_atom_sfa,
tma_tensor_sfa,
tma_atom_sfb,
tma_tensor_sfb,
c_tensor,
self.a_smem_layout_staged,
self.b_smem_layout_staged,
self.sfa_smem_layout_staged,
self.sfb_smem_layout_staged,
self.cluster_layout_vmnk,
self.cluster_layout_sfb_vmnk,
epilogue_op,
).launch(
grid=grid,
block=[self.threads_per_cta, 1, 1],
cluster=cluster_shape_mnk,
stream=stream,
)
return
# GPU device kernel
@cute.kernel
def kernel(
self,
tiled_mma: cute.TiledMma,
tiled_mma_sfb: cute.TiledMma,
tma_atom_a: cute.CopyAtom,
mA_mkl: cute.Tensor,
tma_atom_b: cute.CopyAtom,
mB_nkl: cute.Tensor,
tma_atom_sfa: cute.CopyAtom,
mSFA_mkl: cute.Tensor,
tma_atom_sfb: cute.CopyAtom,
mSFB_nkl: cute.Tensor,
mC_mnl: cute.Tensor,
a_smem_layout_staged: cute.ComposedLayout,
b_smem_layout_staged: cute.ComposedLayout,
sfa_smem_layout_staged: cute.Layout,
sfb_smem_layout_staged: cute.Layout,
cta_layout_vmnk: cute.Layout,
cta_layout_sfb_vmnk: cute.Layout,
epilogue_op: cutlass.Constexpr,
):
"""
GPU device kernel performing the batched GEMM computation.
"""
warp_idx = cute.arch.warp_idx()
warp_idx = cute.arch.make_warp_uniform(warp_idx)
tidx, _, _ = cute.arch.thread_idx()
#
# Setup cta/thread coordinates
#
# Coords inside cluster
bidx, bidy, bidz = cute.arch.block_idx()
cta_rank_in_cluster = cute.arch.block_idx_in_cluster()
cta_in_cluster_coord_vmnk = cta_layout_vmnk.get_flat_coord(cta_rank_in_cluster)
cta_in_cluster_coord_sfb_vmnk = cta_layout_sfb_vmnk.get_flat_coord(
cta_rank_in_cluster
)
# Coords outside cluster
mma_tile_coord_vmnk = (
bidx % cute.size(cta_layout_vmnk, mode=[0]),
bidx // cute.size(cta_layout_vmnk, mode=[0]),
bidy,
bidz,
)
mma_tile_coord_mnl = mma_tile_coord_vmnk[1:]
is_leader_cta = mma_tile_coord_vmnk[0] == 0
#
# Define shared storage for kernel
#
@cute.struct
class SharedStorage:
ab_mbar_ptr: cute.struct.MemRange[cutlass.Int64, self.num_ab_stage * 2]
acc_mbar_ptr: cute.struct.MemRange[cutlass.Int64, self.num_acc_stage * 2]
tmem_dealloc_mbar: cutlass.Int64
tmem_holding_buf: cutlass.Int32
smem = utils.SmemAllocator()
storage = smem.allocate(SharedStorage)
# (MMA, MMA_M, MMA_K, STAGE)
sA = smem.allocate_tensor(
element_type=ab_dtype,
layout=a_smem_layout_staged.outer,
byte_alignment=128,
swizzle=a_smem_layout_staged.inner,
)
# (MMA, MMA_N, MMA_K, STAGE)
sB = smem.allocate_tensor(
element_type=ab_dtype,
layout=b_smem_layout_staged.outer,
byte_alignment=128,
swizzle=b_smem_layout_staged.inner,
)
# (MMA, MMA_M, MMA_K, STAGE)
sSFA = smem.allocate_tensor(
element_type=sf_dtype,
layout=sfa_smem_layout_staged,
byte_alignment=128,
)
# (MMA, MMA_N, MMA_K, STAGE)
sSFB = smem.allocate_tensor(
element_type=sf_dtype,
layout=sfb_smem_layout_staged,
byte_alignment=128,
)
#
# Compute multicast mask for A/B/SFA/SFB buffer full
#
a_full_mcast_mask = None
b_full_mcast_mask = None
sfa_full_mcast_mask = None
sfb_full_mcast_mask = None
if cutlass.const_expr(
self.is_a_mcast or self.is_b_mcast or self.use_2cta_instrs
):
a_full_mcast_mask = cpasync.create_tma_multicast_mask(
cta_layout_vmnk, cta_in_cluster_coord_vmnk, mcast_mode=2
)
b_full_mcast_mask = cpasync.create_tma_multicast_mask(
cta_layout_vmnk, cta_in_cluster_coord_vmnk, mcast_mode=1
)
sfa_full_mcast_mask = cpasync.create_tma_multicast_mask(
cta_layout_vmnk, cta_in_cluster_coord_vmnk, mcast_mode=2
)
sfb_full_mcast_mask = cpasync.create_tma_multicast_mask(
cta_layout_sfb_vmnk, cta_in_cluster_coord_sfb_vmnk, mcast_mode=1
)
#
# Initialize mainloop ab_pipeline, acc_pipeline and their states
#
num_tma_producer = self.num_mcast_ctas_a + self.num_mcast_ctas_b - 1
ab_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread)
ab_pipeline_consumer_group = pipeline.CooperativeGroup(
pipeline.Agent.Thread, num_tma_producer
)
ab_producer, ab_consumer = pipeline.PipelineTmaUmma.create(
barrier_storage=storage.ab_mbar_ptr.data_ptr(),
num_stages=self.num_ab_stage,
producer_group=ab_pipeline_producer_group,
consumer_group=ab_pipeline_consumer_group,
tx_count=self.num_tma_load_bytes,
cta_layout_vmnk=cta_layout_vmnk,
).make_participants()
acc_producer, acc_consumer = pipeline.PipelineUmmaAsync.create(
barrier_storage=storage.acc_mbar_ptr.data_ptr(),
num_stages=self.num_acc_stage,
producer_group=ab_pipeline_producer_group,
consumer_group=pipeline.CooperativeGroup(
pipeline.Agent.Thread,
self.threads_per_cta * (2 if self.use_2cta_instrs else 1),
),
cta_layout_vmnk=cta_layout_vmnk,
).make_participants()
#
# Local_tile partition global tensors
#
# (bM, bK, RestM, RestK, RestL)
gA_mkl = cute.local_tile(
mA_mkl, cute.slice_(self.mma_tiler, (None, 0, None)), (None, None, None)
)
# (bN, bK, RestN, RestK, RestL)
gB_nkl = cute.local_tile(
mB_nkl, cute.slice_(self.mma_tiler, (0, None, None)), (None, None, None)
)
gSFA_mkl = cute.local_tile(
mSFA_mkl, cute.slice_(self.mma_tiler, (None, 0, None)), (None, None, None)
)
gSFB_nkl = cute.local_tile(
mSFB_nkl, cute.slice_(self.mma_tiler, (0, None, None)), (None, None, None)
)
# (bM, bN, RestM, RestN, RestL)
gC_mnl = cute.local_tile(
mC_mnl, cute.slice_(self.mma_tiler, (None, None, 0)), (None, None, None)
)
k_tile_cnt = cute.size(gA_mkl, mode=[3])
#
# Partition global tensor for TiledMMA_A/B/SFA/SFB/C
#
# (MMA, MMA_M, MMA_K, RestK)
thr_mma = tiled_mma.get_slice(mma_tile_coord_vmnk[0])
thr_mma_sfb = tiled_mma_sfb.get_slice(mma_tile_coord_vmnk[0])
# (MMA, MMA_M, MMA_K, RestM, RestK, RestL)
tCgA = thr_mma.partition_A(gA_mkl)
# (MMA, MMA_N, MMA_K, RestN, RestK, RestL)
tCgB = thr_mma.partition_B(gB_nkl)
# (MMA, MMA_M, MMA_K, RestM, RestK, RestL)
tCgSFA = thr_mma.partition_A(gSFA_mkl)
# (MMA, MMA_N, MMA_K, RestN, RestK, RestL)
# tCgSFB = thr_mma.partition_B(gSFB_nkl)
tCgSFB = thr_mma_sfb.partition_B(gSFB_nkl)
# (MMA, MMA_M, MMA_N, RestM, RestN, RestL)
tCgC = thr_mma.partition_C(gC_mnl)
#
# Partition global/shared tensor for TMA load A/B/SFA/SFB
#
# TMA load A partition_S/D
# ((atom_v, rest_v), STAGE)
# ((atom_v, rest_v), RestM, RestK, RestL)
tAsA, tAgA = cpasync.tma_partition(
tma_atom_a,
# 0,
# cute.make_layout(1),
cta_in_cluster_coord_vmnk[2],
cute.make_layout(cute.size(cta_layout_vmnk, mode=[2])),
cute.group_modes(sA, 0, 3),
cute.group_modes(tCgA, 0, 3),
)
# TMA load B partition_S/D
# ((atom_v, rest_v), STAGE)
# ((atom_v, rest_v), RestN, RestK, RestL)
tBsB, tBgB = cpasync.tma_partition(
tma_atom_b,
# 0,
# cute.make_layout(1),
cta_in_cluster_coord_vmnk[1],
cute.make_layout(cute.size(cta_layout_vmnk, mode=[1])),
cute.group_modes(sB, 0, 3),
cute.group_modes(tCgB, 0, 3),
)
# TMA load SFA partition_S/D
# ((atom_v, rest_v), STAGE)
# ((atom_v, rest_v), RestM, RestK, RestL)
tAsSFA, tAgSFA = cpasync.tma_partition(
tma_atom_sfa,
# 0,
# cute.make_layout(1),
cta_in_cluster_coord_vmnk[2],
cute.make_layout(cute.size(cta_layout_vmnk, mode=[2])),
cute.group_modes(sSFA, 0, 3),
cute.group_modes(tCgSFA, 0, 3),
)
tAsSFA = cute.filter_zeros(tAsSFA)
tAgSFA = cute.filter_zeros(tAgSFA)
# TMA load SFB partition_S/D
# ((atom_v, rest_v), STAGE)
# ((atom_v, rest_v), RestN, RestK, RestL)
sfb_cta_layout = cute.make_layout(
cute.slice_(cta_layout_sfb_vmnk, (0, None, 0, 0)).shape
)
tBsSFB, tBgSFB = cpasync.tma_partition(
tma_atom_sfb,
cta_in_cluster_coord_sfb_vmnk[1],
sfb_cta_layout,
cute.group_modes(sSFB, 0, 3),
cute.group_modes(tCgSFB, 0, 3),
)
tBsSFB = cute.filter_zeros(tBsSFB)
tBgSFB = cute.filter_zeros(tBgSFB)
#
# Partition shared/tensor memory tensor for TiledMMA_A/B/C
#
# (MMA, MMA_M, MMA_K, STAGE)
tCrA = tiled_mma.make_fragment_A(sA)
# (MMA, MMA_N, MMA_K, STAGE)
tCrB = tiled_mma.make_fragment_B(sB)
# (MMA, MMA_M, MMA_N)
acc_shape = tiled_mma.partition_shape_C(self.mma_tiler[:2])
# (MMA, MMA_M, MMA_N)
tCtAcc_fake = tiled_mma.make_fragment_C(acc_shape)
#
# Alloc tensor memory buffer
#
tmem_alloc_barrier = pipeline.NamedBarrier(
barrier_id=1,
num_threads=self.threads_per_cta,
)
tmem = utils.TmemAllocator(
storage.tmem_holding_buf.ptr,
barrier_for_retrieve=tmem_alloc_barrier,
is_two_cta=cute.size(cta_layout_vmnk, mode=[0]) > 1,
two_cta_tmem_dealloc_mbar_ptr=storage.tmem_dealloc_mbar.ptr,
)
tmem.allocate(self.num_tmem_alloc_cols)
tmem.wait_for_alloc()
acc_tmem_ptr = tmem.retrieve_ptr(cutlass.Float32)
tCtAcc = cute.make_tensor(acc_tmem_ptr, tCtAcc_fake.layout)
#
# Make SFA/SFB tmem tensor
#
# Get SFA tmem ptr
sfa_tmem_ptr = cute.recast_ptr(
acc_tmem_ptr + tcgen05.find_tmem_tensor_col_offset(tCtAcc),
dtype=sf_dtype,
)
# (MMA, MMA_M, MMA_K)
tCtSFA_layout = blockscaled_utils.make_tmem_layout_sfa(
tiled_mma,
self.mma_tiler,
sf_vec_size,
cute.slice_(sfa_smem_layout_staged, (None, None, None, 0)),
)
tCtSFA = cute.make_tensor(sfa_tmem_ptr, tCtSFA_layout)
# Get SFB tmem ptr
sfb_tmem_ptr = cute.recast_ptr(
acc_tmem_ptr
+ tcgen05.find_tmem_tensor_col_offset(tCtAcc)
+ tcgen05.find_tmem_tensor_col_offset(tCtSFA),
dtype=sf_dtype,
)
# (MMA, MMA_N, MMA_K)
tCtSFB_layout = blockscaled_utils.make_tmem_layout_sfb(
tiled_mma,
self.mma_tiler,
sf_vec_size,
cute.slice_(sfb_smem_layout_staged, (None, None, None, 0)),
)
tCtSFB = cute.make_tensor(sfb_tmem_ptr, tCtSFB_layout)
#
# Partition for S2T copy of SFA/SFB
#
# Make S2T CopyAtom
copy_atom_s2t = cute.make_copy_atom(
tcgen05.Cp4x32x128bOp(
tcgen05.CtaGroup.ONE
if not self.use_2cta_instrs
else tcgen05.CtaGroup.TWO
),
sf_dtype,
)
# (MMA, MMA_MN, MMA_K, STAGE)
tCsSFA_compact = cute.filter_zeros(sSFA)
# (MMA, MMA_MN, MMA_K)
tCtSFA_compact = cute.filter_zeros(tCtSFA)
tiled_copy_s2t_sfa = tcgen05.make_s2t_copy(copy_atom_s2t, tCtSFA_compact)
thr_copy_s2t_sfa = tiled_copy_s2t_sfa.get_slice(0)
# ((ATOM_V, REST_V), Rest_Tiler, MMA_MN, MMA_K, STAGE)
tCsSFA_compact_s2t_ = thr_copy_s2t_sfa.partition_S(tCsSFA_compact)
# ((ATOM_V, REST_V), Rest_Tiler, MMA_MN, MMA_K, STAGE)
tCsSFA_compact_s2t = tcgen05.get_s2t_smem_desc_tensor(
tiled_copy_s2t_sfa, tCsSFA_compact_s2t_
)
# ((ATOM_V, REST_V), Rest_Tiler, MMA_MN, MMA_K)
tCtSFA_compact_s2t = thr_copy_s2t_sfa.partition_D(tCtSFA_compact)
# (MMA, MMA_MN, MMA_K, STAGE)
tCsSFB_compact = cute.filter_zeros(sSFB)
# (MMA, MMA_MN, MMA_K)
tCtSFB_compact = cute.filter_zeros(tCtSFB)
tiled_copy_s2t_sfb = tcgen05.make_s2t_copy(copy_atom_s2t, tCtSFB_compact)
thr_copy_s2t_sfb = tiled_copy_s2t_sfb.get_slice(0)
# ((ATOM_V, REST_V), Rest_Tiler, MMA_MN, MMA_K, STAGE)
tCsSFB_compact_s2t_ = thr_copy_s2t_sfb.partition_S(tCsSFB_compact)
# ((ATOM_V, REST_V), Rest_Tiler, MMA_MN, MMA_K, STAGE)
tCsSFB_compact_s2t = tcgen05.get_s2t_smem_desc_tensor(
tiled_copy_s2t_sfb, tCsSFB_compact_s2t_
)
# ((ATOM_V, REST_V), Rest_Tiler, MMA_MN, MMA_K)
tCtSFB_compact_s2t = thr_copy_s2t_sfb.partition_D(tCtSFB_compact)
#
# Slice to per mma tile index
#
# ((atom_v, rest_v), RestK)
tAgA = tAgA[(None, mma_tile_coord_mnl[0], None, mma_tile_coord_mnl[2])]
# ((atom_v, rest_v), RestK)
tBgB = tBgB[(None, mma_tile_coord_mnl[1], None, mma_tile_coord_mnl[2])]
# ((atom_v, rest_v), RestK)
tAgSFA = tAgSFA[(None, mma_tile_coord_mnl[0], None, mma_tile_coord_mnl[2])]
# ((atom_v, rest_v), RestK)
tBgSFB = tBgSFB[(None, mma_tile_coord_mnl[1], None, mma_tile_coord_mnl[2])]
#
# Execute Data copy and Math computation in the k_tile loop
#
if warp_idx == 0:
# Wait for accumulator buffer empty
if is_leader_cta:
acc_producer.acquire_and_advance()
# Set ACCUMULATE field to False for the first k_tile iteration
tiled_mma.set(tcgen05.Field.ACCUMULATE, False)
# Execute k_tile loop
for k_tile in cutlass.range(
k_tile_cnt, prefetch_stages=self.num_ab_stage - 2
):
# Wait for AB buffer empty
ab_empty = ab_producer.acquire_and_advance()
# TMA load A/B/SFA/SFB
cute.copy(
tma_atom_a,
tAgA[(None, ab_empty.count)],
tAsA[(None, ab_empty.index)],
tma_bar_ptr=ab_empty.barrier,
mcast_mask=a_full_mcast_mask,
)
cute.copy(
tma_atom_b,
tBgB[(None, ab_empty.count)],
tBsB[(None, ab_empty.index)],
tma_bar_ptr=ab_empty.barrier,
mcast_mask=b_full_mcast_mask,
)
cute.copy(
tma_atom_sfa,
tAgSFA[(None, ab_empty.count)],
tAsSFA[(None, ab_empty.index)],
tma_bar_ptr=ab_empty.barrier,
mcast_mask=sfa_full_mcast_mask,
)
cute.copy(
tma_atom_sfb,
tBgSFB[(None, ab_empty.count)],
tBsSFB[(None, ab_empty.index)],
tma_bar_ptr=ab_empty.barrier,
mcast_mask=sfb_full_mcast_mask,
)
if is_leader_cta:
# Wait for AB buffer full
ab_full = ab_consumer.wait_and_advance()
# Copy SFA/SFB to tmem
s2t_stage_coord = (None, None, None, None, ab_full.index)
tCsSFA_compact_s2t_staged = tCsSFA_compact_s2t[s2t_stage_coord]
tCsSFB_compact_s2t_staged = tCsSFB_compact_s2t[s2t_stage_coord]
cute.copy(
tiled_copy_s2t_sfa,
tCsSFA_compact_s2t_staged,
tCtSFA_compact_s2t,
)
cute.copy(
tiled_copy_s2t_sfb,
tCsSFB_compact_s2t_staged,
tCtSFB_compact_s2t,
)
# tCtAcc += tCrA * tCrSFA * tCrB * tCrSFB
num_kblocks = cute.size(tCrA, mode=[2])
for kblock_idx in cutlass.range(num_kblocks, unroll_full=True):
kblock_coord = (
None,
None,
kblock_idx,
ab_full.index,
)
# Set SFA/SFB tensor to tiled_mma
sf_kblock_coord = (None, None, kblock_idx)
tiled_mma.set(
tcgen05.Field.SFA,
tCtSFA[sf_kblock_coord].iterator,
)
tiled_mma.set(
tcgen05.Field.SFB,
tCtSFB[sf_kblock_coord].iterator,
)
cute.gemm(
tiled_mma,
tCtAcc,
tCrA[kblock_coord],
tCrB[kblock_coord],
tCtAcc,
)
# Enable accumulate on tCtAcc after first kblock
tiled_mma.set(tcgen05.Field.ACCUMULATE, True)
# Async arrive AB buffer empty
ab_full.release()
if is_leader_cta:
acc_producer.commit()
#
# Epilogue
# Partition for epilogue
#
# x32 or x128 all is ok.
op = tcgen05.Ld32x32bOp(tcgen05.Repetition.x128, tcgen05.Pack.NONE)
copy_atom_t2r = cute.make_copy_atom(op, cutlass.Float32)
tiled_copy_t2r = tcgen05.make_tmem_copy(copy_atom_t2r, tCtAcc)
thr_copy_t2r = tiled_copy_t2r.get_slice(tidx)
# (T2R_M, T2R_N, EPI_M, EPI_M)
tTR_tAcc = thr_copy_t2r.partition_S(tCtAcc)
# (T2R_M, T2R_N, EPI_M, EPI_N, RestM, RestN, RestL)
tTR_gC = thr_copy_t2r.partition_D(tCgC)
# (T2R_M, T2R_N, EPI_M, EPI_N
tTR_rAcc = cute.make_rmem_tensor(
tTR_gC[None, None, None, None, 0, 0, 0].shape, cutlass.Float32
)
# (T2R_M, T2R_N, EPI_M, EPI_N
tTR_rC = cute.make_rmem_tensor(
tTR_gC[None, None, None, None, 0, 0, 0].shape, c_dtype
)
# STG Atom
simt_atom = cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), c_dtype)
tTR_gC = tTR_gC[(None, None, None, None, *mma_tile_coord_mnl)]
# Wait for accumulator buffer full
acc_full = acc_consumer.wait_and_advance()
# Copy accumulator to register
cute.copy(tiled_copy_t2r, tTR_tAcc, tTR_rAcc)
acc_vec = epilogue_op(tTR_rAcc.load().to(c_dtype))
tTR_rC.store(acc_vec)
# Store C to global memory
cute.copy(simt_atom, tTR_rC, tTR_gC)
acc_full.release()
# Ensure used buffers are properly synchronized before producer exit.
# This could avoid the invalid dsmem access due to early leading CTA exit.
if warp_idx == 0:
ab_producer.tail()
if is_leader_cta:
acc_producer.tail()
# Deallocate TMEM
cute.arch.barrier()
tmem.free(acc_tmem_ptr)
return
def run_nvfp4_gemm(
mnkl: Tuple[int, int, int, int],
tolerance: float,
warmup_iterations: int = 10,
iterations: int = 100,
use_cold_l2: bool = True,
do_benchmark: bool = False,
):
run(
gemm_class=Sm100BlockScaledDenseGemmKernel,
ab_dtype=ab_dtype,
sf_dtype=sf_dtype,
c_dtype=c_dtype,
sf_vec_size=sf_vec_size,
mma_tiler_mn=mma_tiler_mn,
cluster_shape_mnk=cluster_shape_mnk,
mnkl=mnkl,
tolerance=tolerance,
do_benchmark=do_benchmark,
warmup_iterations=warmup_iterations,
iterations=iterations,
use_cold_l2=use_cold_l2,
)
if __name__ == "__main__":
parser = create_parser()
args = parser.parse_args()
if len(args.mnkl) != 4:
parser.error("--mnkl must contain exactly 4 values")
m, n, k, _ = args.mnkl
if m % mma_tiler_mn[0] != 0:
parser.error("M must be multiples of mma_tiler_mn[0] (got m={})".format(m))
if n % mma_tiler_mn[1] != 0:
parser.error("N must be multiples of mma_tiler_mn[1] (got n={})".format(n))
if k % 256 != 0:
parser.error("k must be a multiple of 256 (got k={})".format(k))
run_nvfp4_gemm(
args.mnkl,
args.tolerance,
do_benchmark=args.do_benchmark,
)
print("PASS")

View File

@@ -0,0 +1,366 @@
# Copyright (c) 2025 - 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import argparse
from typing import Tuple
import torch
import cutlass
import cutlass.cute as cute
import cutlass.torch as cutlass_torch
from cutlass.cute.runtime import make_ptr
def parse_comma_separated_ints(s: str) -> Tuple[int, ...]:
try:
return tuple(int(x.strip()) for x in s.split(","))
except ValueError:
raise argparse.ArgumentTypeError(
"Invalid format. Expected comma-separated integers."
)
def create_parser():
parser = argparse.ArgumentParser(
description="Example of Sm100 Dense BlockScaled GEMM."
)
parser.add_argument(
"--mnkl",
type=parse_comma_separated_ints,
default=(8192, 8192, 8192, 8),
help="mnkl dimensions (comma-separated)",
)
parser.add_argument(
"--tolerance", type=float, default=1e-01, help="Tolerance for validation"
)
parser.add_argument(
"--do_benchmark", action="store_true", default=False, help="Do benchmark test"
)
return parser
def ceil_div(a, b):
return (a + b - 1) // b
# Helper function to create scale factor tensor SFA/SFB
# for 1x16 block scaled wise use case and follow the layout requirement
# defined in https://docs.nvidia.com/cuda/cublas/index.html?highlight=fp4#d-block-scaling-factors-layout
@cute.jit
def cvt_sf_MKL_to_M32x4xrm_K4xrk_L(
sf_ref_ptr: cute.Pointer,
sf_mma_ptr: cute.Pointer,
mn: int,
sf_k: int,
l: int,
mma_shape: tuple,
):
mma_permute_order = (3, 4, 1, 5, 2, 0)
permuted_shape = tuple(mma_shape[i] for i in mma_permute_order)
cute_layout = cute.make_ordered_layout(permuted_shape, order=(2, 1, 4, 0, 3, 5))
sf_ref_tensor = cute.make_tensor(
sf_ref_ptr, cute.make_layout((mn, sf_k, l), stride=(sf_k, 1, mn * sf_k))
)
sf_mma_tensor = cute.make_tensor(sf_mma_ptr, cute_layout)
sf_mma_tensor = cute.group_modes(sf_mma_tensor, 0, 3)
sf_mma_tensor = cute.group_modes(sf_mma_tensor, 1, 3)
for i in cutlass.range(cute.size(sf_ref_tensor)):
mkl_coord = sf_ref_tensor.layout.get_hier_coord(i)
sf_mma_tensor[mkl_coord] = sf_ref_tensor[mkl_coord]
pass
def to_blocked(input_matrix):
rows, cols = input_matrix.shape
# Please ensure rows and cols are multiples of 128 and 4 respectively
n_row_blocks = ceil_div(rows, 128)
n_col_blocks = ceil_div(cols, 4)
padded = input_matrix
blocks = padded.view(n_row_blocks, 128, n_col_blocks, 4).permute(0, 2, 1, 3)
rearranged = blocks.reshape(-1, 4, 32, 4).transpose(1, 2).reshape(-1, 32, 16)
return rearranged.flatten()
def run(
gemm_class,
ab_dtype,
sf_dtype,
c_dtype,
sf_vec_size,
mma_tiler_mn,
cluster_shape_mnk,
mnkl: Tuple[int, int, int, int],
tolerance: float,
warmup_iterations: int = 10,
iterations: int = 100,
use_cold_l2: bool = True,
do_benchmark: bool = False,
):
"""
Prepare A/B/SFA/SFB/C tensors, launch GPU kernel, and reference checking.
"""
print("=" * 60)
print("Launching Blackwell Dense BlockScaled GEMM Test")
print("-" * 60)
print(f"Input dimensions (m, n, k, l): {mnkl}")
print(f" m (rows): {mnkl[0]}")
print(f" n (cols): {mnkl[1]}")
print(f" k (inner): {mnkl[2]}")
print(f" l (batch): {mnkl[3]}")
print(f"Data Types & Precision:")
print(f" Input matrices (A, B): {ab_dtype}")
print(f" Scale factors (SFA, SFB): {sf_dtype}")
print(f" Output matrix (C): {c_dtype}")
print(f" Scale factor vector size: {sf_vec_size}")
print("Tile and cluster configuration:")
print(f" MMA tiler (M, N, K): {mma_tiler_mn}")
print(f" Cluster shape (M, N, K): {cluster_shape_mnk}")
print(f"Validation tolerance: {tolerance}")
print(f"Do benchmark: {do_benchmark}")
print("=" * 60)
# Unpack parameters
m, n, k, l = mnkl
if not torch.cuda.is_available():
raise RuntimeError("GPU is required to run this example!")
torch.manual_seed(1111)
# Create tensor A/B/C
a_ref = torch.randint(
0, 2, (l, m, k // 2), dtype=torch.uint8, device="cuda"
).permute(1, 2, 0)
b_ref = torch.randint(
0, 2, (l, n, k // 2), dtype=torch.uint8, device="cuda"
).permute(1, 2, 0)
# a_ref = torch.ones((l, m, k // 2), dtype=torch.uint8, device="cuda").permute(1, 2, 0)
# b_ref = torch.ones((l, n, k // 2), dtype=torch.uint8, device="cuda").permute(1, 2, 0)
a_ref_f4 = a_ref.view(torch.float4_e2m1fn_x2)
b_ref_f4 = b_ref.view(torch.float4_e2m1fn_x2)
c_tensor = torch.randn((l, m, n), dtype=torch.float16, device="cuda").permute(
1, 2, 0
)
# Create a torch tensor for scale factor tensor of A and B
def create_ref_scale_factor_tensor(l, mn, sf_k):
"""
Create the reference scale factor tensor on CPU.
Returns the reshaped/pruned tensor ready for ref computation and its original permuted form.
"""
ref_shape = (l, mn, sf_k)
ref_permute_order = (1, 2, 0)
ref_f8_random_int = torch.randint(1, 3, ref_shape, dtype=torch.int8)
ref_f8_torch_tensor_cpu = ref_f8_random_int.to(dtype=torch.float8_e4m3fn)
# permute to match ref_permute_order
ref_f8_torch_tensor_cpu_permuted = ref_f8_torch_tensor_cpu.permute(
*ref_permute_order
)
return ref_f8_torch_tensor_cpu_permuted
# Copy the reference scale factor tensor to the CUTE-format scale factor tensor
def create_cute_scale_factor_tensor(l, mn, sf_k, ref_f8_torch_tensor_cpu_permuted):
"""
Create the CUTE-format scale factor tensor on CUDA based on the reference tensor.
"""
atom_m = (32, 4)
atom_k = 4
mma_shape = (
l, # batch size
ceil_div(mn, atom_m[0] * atom_m[1]),
ceil_div(sf_k, atom_k),
atom_m[0],
atom_m[1],
atom_k,
)
mma_permute_order = (3, 4, 1, 5, 2, 0)
# Generate a random int8 tensor, then convert to float8_e4m3fn
rand_int_tensor = torch.randint(0, 2, mma_shape, dtype=torch.int8)
cute_f8_torch_tensor_cpu = rand_int_tensor.to(dtype=torch.float8_e4m3fn)
# Permute according to mma_permute_order
cute_f8_torch_tensor_cpu = cute_f8_torch_tensor_cpu.permute(*mma_permute_order)
# Call the helper function to do layout conversion
cvt_sf_MKL_to_M32x4xrm_K4xrk_L(
make_ptr(
cutlass.Float8E4M3FN,
ref_f8_torch_tensor_cpu_permuted.data_ptr(),
cute.AddressSpace.gmem,
assumed_align=32,
),
make_ptr(
cutlass.Float8E4M3FN,
cute_f8_torch_tensor_cpu.data_ptr(),
cute.AddressSpace.gmem,
assumed_align=32,
),
mn,
sf_k,
l,
mma_shape,
)
return cute_f8_torch_tensor_cpu.cuda()
sf_k = ceil_div(k, sf_vec_size)
sfa_ref = create_ref_scale_factor_tensor(l, m, sf_k)
sfb_ref = create_ref_scale_factor_tensor(l, n, sf_k)
# sfa_ref.fill_(1)
# sfb_ref.fill_(1)
sfa_tensor = create_cute_scale_factor_tensor(l, m, sf_k, sfa_ref)
sfb_tensor = create_cute_scale_factor_tensor(l, n, sf_k, sfb_ref)
# Configure gemm kernel
gemm = gemm_class()
# Initialize Stream
current_stream = cutlass_torch.default_stream()
a_ptr = make_ptr(
ab_dtype, a_ref_f4.data_ptr(), cute.AddressSpace.gmem, assumed_align=16
)
b_ptr = make_ptr(
ab_dtype, b_ref_f4.data_ptr(), cute.AddressSpace.gmem, assumed_align=16
)
c_ptr = make_ptr(
c_dtype, c_tensor.data_ptr(), cute.AddressSpace.gmem, assumed_align=32
)
sfa_ptr = make_ptr(
sf_dtype, sfa_tensor.data_ptr(), cute.AddressSpace.gmem, assumed_align=32
)
sfb_ptr = make_ptr(
sf_dtype, sfb_tensor.data_ptr(), cute.AddressSpace.gmem, assumed_align=32
)
# Compile gemm kernel
compiled_gemm = cute.compile(
gemm,
a_ptr,
b_ptr,
sfa_ptr,
sfb_ptr,
c_ptr,
(m, n, k, l),
current_stream,
)
# Launch GPU kernel
compiled_gemm(a_ptr, b_ptr, sfa_ptr, sfb_ptr, c_ptr, (m, n, k, l), current_stream)
# For batch l, do (m, k, l) @ (n, k, l).T along k for each batch.
# Result: (m, n, l)
# Allocate ref as (l, m, n) with n-contiguous layout, then permute to (m, n, l)
ref = torch.empty(
(l, m, n),
dtype=torch.float16,
device="cuda",
).permute(1, 2, 0)
for l_idx in range(l):
# Convert the scale factor tensor to blocked format
scale_a = to_blocked(sfa_ref[:, :, l_idx])
scale_b = to_blocked(sfb_ref[:, :, l_idx])
# (m, k) @ (n, k).T -> (m, n)
res = torch._scaled_mm(
a_ref_f4[:, :, l_idx],
b_ref_f4[:, :, l_idx].transpose(0, 1),
scale_a.cuda(),
scale_b.cuda(),
bias=None,
out_dtype=torch.float16,
)
ref[:, :, l_idx] = res
torch.testing.assert_close(c_tensor, ref, atol=tolerance, rtol=1e-02)
if do_benchmark:
def generate_tensors():
a_ptr = make_ptr(
ab_dtype, a_ref_f4.data_ptr(), cute.AddressSpace.gmem, assumed_align=16
)
b_ptr = make_ptr(
ab_dtype, b_ref_f4.data_ptr(), cute.AddressSpace.gmem, assumed_align=16
)
c_ptr = make_ptr(
c_dtype, c_tensor.data_ptr(), cute.AddressSpace.gmem, assumed_align=32
)
sfa_ptr = make_ptr(
sf_dtype,
sfa_tensor.data_ptr(),
cute.AddressSpace.gmem,
assumed_align=32,
)
sfb_ptr = make_ptr(
sf_dtype,
sfb_tensor.data_ptr(),
cute.AddressSpace.gmem,
assumed_align=32,
)
args = cute.testing.JitArguments(
a_ptr, b_ptr, sfa_ptr, sfb_ptr, c_ptr, (m, n, k, l), current_stream
)
args.add_to_scope([a_ref_f4, b_ref_f4, sfa_tensor, sfb_tensor, c_tensor])
return args
workspace_count = 1
if use_cold_l2:
one_workspace_bytes = (
a_ref_f4.numel() * a_ref_f4.element_size()
+ b_ref_f4.numel() * b_ref_f4.element_size()
+ sfa_tensor.numel() * sfa_tensor.element_size()
+ sfb_tensor.numel() * sfb_tensor.element_size()
+ c_tensor.numel() * c_tensor.element_size()
)
workspace_count = cute.testing.get_workspace_count(
one_workspace_bytes, warmup_iterations, iterations
)
# Return execution time in microseconds
time = cute.testing.benchmark(
compiled_gemm,
workspace_generator=generate_tensors,
workspace_count=workspace_count,
stream=current_stream,
warmup_iterations=warmup_iterations,
iterations=iterations,
)
print(f"Execution time: {time} us")
peta_flops = (4 * m * n * k * l) / (time * 1e-6) / 1e9 / 1000000
print(f"FLOPS: {peta_flops} PFLOPS")
bytes_transfer = (
2 * m * k / 2 * l * a_ref_f4.element_size()
+ 2 * n * k / 2 * l * b_ref_f4.element_size()
+ 2 * m * n * l * c_tensor.element_size()
+ 2 * m * sf_k * l * sfa_tensor.element_size()
+ 2 * n * sf_k * l * sfb_tensor.element_size()
)
print(f"Bytes: {bytes_transfer} Bytes")
bandwidth = bytes_transfer / time * 1e-3
print(f"BW: {bandwidth} GB/s")

View File

@@ -0,0 +1,385 @@
# CUTLASS Tutorial Examples for Blackwell TMA
## TMA V0: Understanding tma_partition - The Foundation of TMA Operations
This example demonstrates the fundamental building blocks of Tensor Memory Accelerator (TMA) operations on NVIDIA Blackwell (SM100) architecture using CuTe DSL. It focuses on understanding the `tma_partition` interface, which is essential for all TMA operations.
### Key Concepts
* **TMA Load (Global → Shared)**: Asynchronous bulk data transfer from Global Memory to Shared Memory using TMA hardware
* **TMA Store (Shared → Global)**: Asynchronous bulk data transfer from Shared Memory back to Global Memory
* **tma_partition**: Core interface that prepares tensors for TMA operations by partitioning them according to TMA atom layout requirements
* **group_modes**: Tensor mode grouping to define the TMA atom shape - crucial for proper tma_partition usage
* **mbarrier Synchronization**: Hardware barriers (`mbarrier_init`, `mbarrier_arrive`, `mbarrier_wait`) for synchronizing asynchronous TMA operations
### Kernel Architecture
The `Sm100SimpleCopyKernel` performs a simple tile-based copy operation to illustrate TMA fundamentals:
#### Configuration
* **tile_shape**: Fixed at (128, 128) - Tile dimensions (M, N)
* **cluster_shape_mn**: Fixed at (1, 1) - Single CTA execution (no cluster parallelism)
* **Shared Memory**: Single buffer sized to hold one tile (tile_m × tile_n elements)
* **Synchronization**: Single mbarrier for TMA load completion
#### Key Components
1. **TMA Descriptor Creation**: Creates TMA atoms (`tma_atom_src`, `tma_atom_dst`) that encapsulate TMA hardware instructions
2. **Shared Memory Layout**: Row-major layout `(tile_m, tile_n):(tile_n, 1)` for simplicity
3. **Barrier Management**: Single barrier coordinates TMA load completion before processing
### Execution Flow
1. **Initialization**:
* Allocate shared memory buffer for one tile
* Initialize mbarrier with `elect_one()` to ensure proper synchronization semantics
* Set barrier to expect TMA transaction bytes (`tile_m × tile_n × element_size`)
* Synchronize all threads after barrier initialization
2. **Tensor Preparation**:
```
Tile global tensors into (tile_m, tile_n) blocks
Apply group_modes to combine tile dimensions into Mode 0 (TMA atom)
Example: gSrc_tiled with shape (128, 128, 4, 2)
After group_modes(_, 0, 2): ((128, 128), 4, 2)
└───┬────┘ └─┬─┘
Mode 0 Rest modes
```
3. **TMA Partition**:
```python
tAsA, tAgA = tma_partition(
tma_atom_src, # TMA operation atom
cta_id=0, # CTA ID within cluster
cta_layout, # Cluster layout
group_modes(smem_tensor, 0, 2), # SMEM view (Mode 0 = atom)
group_modes(gSrc_tiled, 0, 2) # Global view (Mode 0 = atom)
)
# Returns:
# tAsA: SMEM view with TMA internal layout
# tAgA: Global view with shape ((TMA_Layout), grid_m, grid_n)
```
4. **Tile Selection**:
```python
# Select specific tile for this CTA
tAgA_cta = tAgA[(None, bidx, bidy)]
# None: keep entire TMA atom (Mode 0)
# bidx, bidy: index into grid dimensions
```
5. **TMA Load** (Global → Shared):
```python
cute.copy(tma_atom_src, tAgA_cta, tAsA, tma_bar_ptr=barrier_ptr)
# Arrive on barrier (producer signals completion)
# Wait on barrier (all threads wait for TMA completion)
```
6. **TMA Store** (Shared → Global):
```python
cute.copy(tma_atom_dst, tAsA, tBgB_cta)
# Synchronous store completes before kernel exit
```
### Understanding tma_partition
The `tma_partition` function is the key to TMA operations. The tutorial includes comprehensive inline diagrams showing:
* **Input Tensor Shapes**: How tensors are organized before partitioning
* **group_modes Effect**: How mode grouping creates the required atom structure
* **Partition Output**: The structure of partitioned tensors for TMA operations
* **Indexing Pattern**: How to select CTA-specific tiles from partitioned views
See lines ~155-255 in `tma_v0.py` for detailed visual explanations.
### Configuration Parameters
* `tile_shape`: Fixed at (128, 128) - Tile dimensions (M, N)
* `cluster_shape_mn`: Fixed at (1, 1) - Single CTA execution
* `threads_per_cta`: 32 - Single warp (all threads participate in barriers)
* `buffer_align_bytes`: 1024 - Shared memory alignment
### Usage
Run the copy kernel with custom matrix dimensions:
```bash
# Basic usage (default: 512×128 matrix)
python tma_v0.py
# Custom dimensions
python tma_v0.py --M 1024 --N 2048
# With custom benchmark iterations
python tma_v0.py --M 4096 --N 4096 --num_warmup 10 --num_iters 50
```
#### Example Code
```python
from tma_v0 import run_tma_copy
# Run copy on 1024×2048 matrix
run_tma_copy(M=1024, N=2048)
# Output: Performance metrics and verification result
```
### Performance Considerations
* **Single-stage operation**: No pipelining - simple sequential load → store pattern
* **Educational focus**: Designed for understanding TMA fundamentals, not peak performance
* **Barrier synchronization**: Demonstrates proper mbarrier usage patterns
* **Foundation for V1/V2**: Concepts learned here are essential for understanding multi-stage pipelines and warp specialization in V1 and V2
## TMA V1: Matrix Transpose with Producer-Consumer Pattern
This example demonstrates a TMA-based matrix transpose using producer-consumer synchronization with mbarriers on NVIDIA Blackwell (SM100) architecture.
### Key Concepts
* **Producer-Consumer Pattern**: Different warps coordinate through mbarrier synchronization
* **TMA Operations**: Asynchronous bulk data transfer between Global and Shared Memory
* **Shared Memory Swizzle**: Optimized layouts to avoid bank conflicts during transpose
* **Warp Specialization**: Dedicated warps for loading, transposing, and storing
* **mbarrier Synchronization**: Hardware barriers coordinate asynchronous operations
### Kernel Architecture
The `Sm100MatrixTransposeKernel` performs a tiled matrix transpose (M×N → N×M) with the following design:
#### Warp Roles
1. **TMA Load Warp** (Warp 4, Producer): Issues TMA load operations from Global Memory to Shared Memory buffer `sA`
2. **Transpose Warps** (Warps 0-3, 4 warps, Consumer/Producer):
* Wait for `sA` to be filled by TMA load (consumer of load_mbar)
* Transpose data from `sA` → Registers → `sB`
* Signal completion to TMA Store warp (producer for store_mbar)
3. **TMA Store Warp** (Warp 5, Consumer):
* Wait for `sB` to be ready (consumer of store_mbar)
* Issue TMA store operations from `sB` to Global Memory
#### Synchronization Barriers
The kernel uses two mbarrier instances for producer-consumer coordination:
1. **load_mbar_ptr**: Synchronizes TMA Load → Transpose Warps
* Producer: TMA Load Warp (arrives after TMA completes)
* Consumer: Transpose Warps (wait before reading `sA`)
* Expected arrivals: 1 (from TMA load warp)
* Expected transactions: `tile_m × tile_n × element_size` bytes
2. **store_mbar_ptr**: Synchronizes Transpose Warps → TMA Store
* Producer: Transpose Warps (each warp arrives after writing to `sB`)
* Consumer: TMA Store Warp (waits before issuing TMA store)
* Expected arrivals: 4 (one from each transpose warp)
#### Execution Flow
1. **Initialization**:
* All warps participate in barrier initialization (thread 0 initializes)
* Allocate two shared memory buffers: `sA` (row-major) and `sB` (column-major)
* Create TMA descriptors for source and transposed destination
* Initialize `load_mbar` with expected count of 1 and transaction bytes
* Initialize `store_mbar` with expected count of 4 (number of transpose warps)
2. **TMA Load Warp** (Producer for Load Pipeline):
```
partition source tensor by tile shape
issue TMA load: Global[block_tile] → sA
arrive on load_mbar to signal completion
```
3. **Transpose Warps** (Consumer for Load, Producer for Store):
```
wait on load_mbar for TMA load to complete
partition sA for reading (each thread handles subset)
copy data: sA → Registers
partition sB for writing
copy data: Registers → sB (transpose happens via layout)
fence to ensure smem writes are visible
synchronize with trans_sync_barrier
[elect one thread] arrive on store_mbar to signal completion
```
4. **TMA Store Warp** (Consumer for Store Pipeline):
```
wait on store_mbar for transpose to complete
partition destination tensor by tile shape
issue TMA store: sB → Global[block_tile]
```
### Key Features
* **Simple Producer-Consumer Model**: Clear separation of concerns with dedicated warps
* **Efficient Synchronization**: Hardware mbarriers minimize synchronization overhead
* **Memory Layout Optimization**: Swizzled layouts prevent bank conflicts
* **Transposition via Layouts**: Transpose is achieved through different memory layouts for `sA` and `sB`
### Configuration Parameters
* `tile_shape`: Fixed at (128, 128) - Tile dimensions (M, N)
* `cluster_shape_mn`: Fixed at (1, 1) - Single CTA execution
* Warp count: 6 warps (1 TMA Load + 4 Transpose + 1 TMA Store)
### Usage
Run the transpose kernel with custom matrix dimensions:
```bash
# Basic usage (128×128 matrix)
python tma_v1.py
# Custom dimensions
python tma_v1.py --M 1024 --N 2048
# With custom benchmark iterations
python tma_v1.py --M 4096 --N 4096 --num_warmup 10 --num_iters 50
```
#### Example Code
```python
from tma_v1 import run_transpose
# Run transpose on 1024×2048 matrix
run_transpose(M=1024, N=2048)
# Output: Performance metrics and verification result
```
### Performance Considerations
* **Single-stage pipeline**: Simpler than multi-stage but with potential for idle warps
* **Warp specialization**: Clear roles minimize synchronization complexity
* **Good for learning**: Demonstrates fundamental TMA and mbarrier concepts
## TMA V2: Transpose with Multi-Stage Pipeline
This example demonstrates a TMA implementation with multi-stage pipelining for efficient matrix transpose operations on NVIDIA Blackwell (SM100) architecture.
### Key Concepts
* **Multi-Stage Pipeline**: Multiple buffers enable overlapping TMA loads, computation (transpose), and TMA stores to hide memory latency
* **Pipeline Abstractions**: `PipelineTmaAsync` and `PipelineTmaStore` provide producer-consumer synchronization
* **Persistent Tile Scheduler**: Efficient work distribution across CTAs for dynamic load balancing
* **Shared Memory Swizzle**: Optimized shared memory layouts to avoid bank conflicts
* **Warp Specialization**: Different warps handle loading and transposing; first transpose warp also handles storing
### Kernel Architecture
The `Sm100MatrixTransposeKernelV2` performs a tiled matrix transpose (M×N → N×M) with the following design:
#### Warp Roles
1. **TMA Load Warp** (Producer): Loads tiles from Global Memory to Shared Memory buffer `sA` using TMA load operations
2. **Transpose Warps** (4 warps, Consumer/Producer):
* Wait for data in `sA` from load pipeline
* Transpose data from `sA` → Registers → `sB`
* Synchronize via named barrier to ensure all transpose warps complete
* First transpose warp (`trans_warp_id[0]`) issues TMA store operations from `sB` to Global Memory
#### Pipeline Stages
The kernel uses two separate pipelines for maximum parallelism:
1. **Load Pipeline**: `TMA Load Warp` (producer) → `Transpose Warps` (consumer)
* Multi-stage buffer `sA` (automatically computed based on available SMEM)
* Enables prefetching multiple tiles while processing current tile
2. **Store Pipeline**: `Transpose Warps` (producer) → `First Transpose Warp` (consumer, issues TMA store)
* Multi-stage buffer `sB` (automatically computed based on available SMEM)
* First transpose warp handles TMA store after all transpose warps finish writing to `sB`
#### Stage Calculation
The kernel automatically computes the optimal number of pipeline stages using `_compute_stages()`:
* Calculates bytes needed per stage for `sA` (tile_m × tile_n) and `sB` (tile_m × tile_n)
* Reserves space for pipeline barriers and metadata (~1KB)
* Divides remaining shared memory by bytes per stage
* Clamps to 2-8 stages (2 for double buffering minimum, 8 for diminishing returns)
#### Execution Flow
1. **Initialization**:
* Allocate multi-stage shared memory buffers (`sA_staged`, `sB_staged`)
* Create TMA descriptors for source and transposed destination
* Initialize load and store pipelines with barrier synchronization
* Use persistent tile scheduler to distribute work tiles across CTAs
2. **TMA Load Warp** (Producer for Load Pipeline):
```
for each tile assigned by scheduler:
acquire next available stage in load pipeline
issue TMA load: Global[tile] → sA[stage]
advance to next stage
```
3. **Transpose Warps** (Consumer for Load, Producer for Store):
```
for each tile assigned by scheduler:
wait for load pipeline to fill current stage
copy data: sA[load_stage] → Registers
release load pipeline stage
transpose and write: Registers → sB[store_stage]
fence to ensure smem writes are visible
synchronize all transpose warps with barrier
[first transpose warp only] issue TMA store: sB[stage] → Global[tile]
[first transpose warp only] commit to store pipeline
[first transpose warp only] acquire next store pipeline stage
synchronize all transpose warps with barrier
```
4. **Pipeline Teardown**:
* Producer/consumer tail operations ensure all in-flight operations complete
### Key Features
* **Automatic Stage Optimization**: Kernel calculates optimal number of stages based on tile size, data type, and available shared memory
* **Persistent Tile Scheduler**: Efficient work distribution across CTAs for dynamic load balancing
* **Memory Layout Optimization**: Uses appropriate swizzling for row-major input and column-major transposed output
* **Efficient Synchronization**: Named barriers for intra-CTA coordination, mbarriers for pipeline stages
### Configuration Parameters
* `tile_shape`: Fixed at (128, 128) - Tile dimensions (M, N)
* `cluster_shape_mn`: Fixed at (1, 1) - Single CTA execution
* Number of pipeline stages: Automatically computed based on available SMEM
### Usage
Run the transpose kernel with custom matrix dimensions:
```bash
# Basic usage (128×128 matrix)
python tma_v2.py
# Custom dimensions
python tma_v2.py --M 1024 --N 2048
python tma_v2.py --M 1024 --N 2048 --num_warmup 10 --num_iters 50
```
#### Example Code
```python
from tma_v2 import run_transpose
# Run transpose on 1024×2048 matrix
run_transpose(M=1024, N=2048)
# Output: "TransposeSuccess!" if verification passes
```
### Performance Considerations
* **Multi-stage pipelining** hides memory latency by overlapping loads, computation, and stores
* **Persistent scheduling** provides better load balancing for irregular matrix sizes
* **Warp specialization** maximizes throughput: TMA load warp handles all loads, transpose warps handle computation, and first transpose warp handles stores
## TMA V3: TMA With MMA (Tensor Cores)
TBD

View File

@@ -0,0 +1,409 @@
# Copyright (c) 2024 - 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import argparse
from typing import Type, Union
import cutlass
import cutlass.cute as cute
import cutlass.utils as utils
from cutlass.cute.nvgpu import cpasync
from cutlass.cute.runtime import from_dlpack
import torch
"""
TMA V0: Understanding tma_partition - The Foundation of TMA Operations
This tutorial demonstrates TMA (Tensor Memory Accelerator) operations through
a simple copy kernel. It focuses on understanding the fundamental tma_partition
interface, which is the key to all TMA operations.
What This Tutorial Covers:
1. TMA Load (Global Memory -> Shared Memory)
2. TMA Store (Shared Memory -> Global Memory)
3. Barrier synchronization for TMA (elect_one, mbarrier_init, mbarrier_arrive, mbarrier_wait)
4. Detailed explanation of tma_partition with visual diagrams
Key Learning Points:
- tma_partition: How it transforms tensors for TMA operations
- group_modes: Why and how to group tensor modes to define TMA atom shape
- Indexing: How to select specific tiles from partitioned tensors
- Data flow: Complete visualization from input tensors to TMA copy
Visual Diagrams:
See line ~155 for comprehensive diagrams showing:
- Input tensor shapes and transformations
- group_modes effect on tensor layouts
- tma_partition output structure
- Complete data flow from global/shared memory to TMA copy
- Indexing pattern for CTA-specific tiles
Example Usage:
```bash
python cutlass_ir/compiler/python/examples/cute/blackwell/tutorial/tutorial_tma/tma_v0.py
```
"""
class Sm100SimpleCopyKernel:
def __init__(self):
"""
Initializes the configuration for a Blackwell TMA copy kernel.
"""
self.tile_shape = (128, 128)
self.tile_m, self.tile_n = self.tile_shape
self.cluster_shape_mn = (1, 1)
self.threads_per_cta = 32
self.buffer_align_bytes = 1024
@cute.jit
def __call__(self, src: cute.Tensor, dst: cute.Tensor):
if cutlass.const_expr(src.element_type != dst.element_type):
raise TypeError("Source and destination element types must match")
self.dtype: Type[cutlass.Numeric] = src.element_type
# layout for each cta: (tile_m, tile_n):(tile_n, 1)
smem_layout = cute.make_layout(
(self.tile_m, self.tile_n), stride=(self.tile_n, 1)
)
@cute.struct
class SharedStorage:
barrier_storage: cute.struct.MemRange[cutlass.Int64, 1]
smem_data: cute.struct.Align[
cute.struct.MemRange[self.dtype, cute.cosize(smem_layout)],
self.buffer_align_bytes,
]
self.shared_storage = SharedStorage
self.num_tma_load_bytes = cute.size_in_bytes(self.dtype, smem_layout)
# cta_tiler: the per-CTA tile extents (M, N) used by TMA.
# Note: smem_layout may include swizzle or composed layout,
# so we use product_each(...) to take the product along each logical dimension and get
# the final (tile_m, tile_n) extents expected by TMA.
# In this simple example, smem_layout.shape == (tile_m, tile_n), so product_each(...) is
# just (tile_m, tile_n).
cta_tiler = cute.product_each(smem_layout.shape)
tma_atom_src, tma_tensor_src = cpasync.make_tiled_tma_atom(
cpasync.CopyBulkTensorTileG2SOp(), src, smem_layout, cta_tiler
)
tma_atom_dst, tma_tensor_dst = cpasync.make_tiled_tma_atom(
cpasync.CopyBulkTensorTileS2GOp(), dst, smem_layout, cta_tiler
)
# Grid shape is now (M/TileM, N/TileN)
grid_shape = cute.ceil_div((*src.layout.shape, 1), self.tile_shape)
self.kernel(
tma_atom_src, tma_tensor_src, tma_atom_dst, tma_tensor_dst, smem_layout
).launch(
grid=grid_shape,
block=(self.threads_per_cta, 1, 1),
cluster=(*self.cluster_shape_mn, 1),
)
@cute.kernel
def kernel(
self,
tma_atom_src: cute.CopyAtom,
tma_tensor_src: cute.Tensor,
tma_atom_dst: cute.CopyAtom,
tma_tensor_dst: cute.Tensor,
smem_layout: Union[cute.Layout, cute.ComposedLayout],
):
bidx, bidy, _ = cute.arch.block_idx()
# Allocate Shared Memory
smem = utils.SmemAllocator()
storage = smem.allocate(self.shared_storage)
# Initialize barrier for TMA synchronization
barrier_ptr = storage.barrier_storage.data_ptr()
# Initialize the barrier: elect_one ensures only one thread executes this
# Note: We must use elect_one() instead of "if tid == 0" because:
# - elect_one() provides proper synchronization semantics
# - It ensures all threads are aware that exactly one thread is executing
# - It prevents race conditions and provides memory ordering guarantees
with cute.arch.elect_one():
cute.arch.mbarrier_init(barrier_ptr, 1)
cute.arch.mbarrier_expect_tx(barrier_ptr, self.num_tma_load_bytes)
# Fence ensures init/expect_tx are visible before proceeding
cute.arch.mbarrier_init_fence()
cute.arch.barrier()
# Tile the (M, N) tensor: ((TileM, TileN), M/TileM, N/TileN)
gSrc_tiled = cute.local_tile(
tma_tensor_src, (self.tile_m, self.tile_n), (None, None)
)
gDst_tiled = cute.local_tile(
tma_tensor_dst, (self.tile_m, self.tile_n), (None, None)
)
smem_tensor = storage.smem_data.get_tensor(smem_layout)
# ======================================================================
# TMA Partition: Tensor Preparation for TMA Operations
# ======================================================================
#
# tma_partition prepares tensors for TMA copy by partitioning them
# according to the TMA atom's internal layout requirements.
#
# Signature:
# tma_partition(atom, cta_id, cta_layout, smem_tensor, gmem_tensor)
# -> (smem_view, gmem_view)
#
# Key Requirement: Mode 0 of both tensors must represent the TMA atom
#
# Example: M=512, N=128, TileM=128, TileN=64
#
# Input Tensors:
# gSrc_tiled: (128, 64, 4, 2) # 4 separate modes
# └──┬──┘ └──┬──┘
# Tile Grid
#
# smem_tensor: (128, 64) # 2 separate modes
# └──┬──┘
# Tile
#
# Apply group_modes(tensor, 0, 2) to group first 2 modes:
#
# group_modes(gSrc_tiled, 0, 2) => ((128, 64), 4, 2)
# └───┬───┘
# Mode 0 = Atom
#
# group_modes(smem_tensor, 0, 2) => ((128, 64),)
# └───┬───┘
# Mode 0 = Atom
#
# After tma_partition:
#
# tAsA: SMEM view with TMA internal layout
# Shape: ((TMA_Layout),)
# - TMA_Layout: Swizzled/banked layout for efficient SMEM access
#
# tAgA: Global view preserving rest modes
# Shape: ((TMA_Layout), 4, 2)
# └─────┬─────┘ └──┬──┘
# TMA atom Rest modes (grid)
#
# Usage Pattern:
# 1. Group modes to define atom: group_modes(tensor, 0, 2)
# 2. Call tma_partition: tAsA, tAgA = tma_partition(...)
# 3. Select tile for CTA: tAgA_cta = tAgA[(None, bidx, bidy)]
# - None: keep entire atom
# - bidx, bidy: index into rest modes
# 4. Issue TMA copy: cute.copy(atom, tAgA_cta, tAsA)
#
# Visual Data Flow:
#
# Global Memory (512x128) Shared Memory (128x64)
# ┌────────────────────┐ ┌──────────────┐
# │ ┌───┬───┐ │ │ │
# │ │0,0│0,1│ │ │ smem_tensor │
# │ ├───┼───┤ │ │ (128, 64) │
# │ │1,0│1,1│ 4x2 │ │ │
# │ ├───┼───┤ tiles │ └──────────────┘
# │ │2,0│2,1│ │ │
# │ ├───┼───┤ │ │ group_modes
# │ │3,0│3,1│ │ ↓
# │ └───┴───┘ │ ((128, 64),)
# └────────────────────┘ │
# │ │
# │ gSrc_tiled │
# │ (128, 64, 4, 2) │
# ↓ │
# group_modes(_, 0, 2) │
# ↓ │
# ((128, 64), 4, 2) │
# │ │
# └────────┬────────────────────────┘
# ↓
# tma_partition
# ↓
# ┌──────────────┴──────────────┐
# │ │
# tAgA tAsA
# ((TMA_Layout), 4, 2) ((TMA_Layout),)
# │
# ↓ tAgA[(None, bidx, bidy)]
# tAgA_cta
# ((TMA_Layout),)
#
# ======================================================================
# TMA Load partition
# Here we only use 1x1 cluster, so cta_id is 0 and cta_layout is (1).
# More details about how to set cta_coord and cta_layout can be found in the tma_v4.py
# Note: Smem and gemm should have the same size (atom element size) in the first rank
tAsA, tAgA = cute.nvgpu.cpasync.tma_partition(
tma_atom_src,
0,
cute.make_layout(1),
cute.group_modes(smem_tensor, 0, 2),
cute.group_modes(gSrc_tiled, 0, 2),
)
# TMA Store partition
# Same process as TMA Load, but for destination tensor
# Partitions gDst_tiled and smem_tensor according to TMA Store atom
_, tBgB = cute.nvgpu.cpasync.tma_partition(
tma_atom_dst,
0,
cute.make_layout(1),
cute.group_modes(smem_tensor, 0, 2),
cute.group_modes(gDst_tiled, 0, 2),
)
# Select specific tile for this CTA from partitioned global views
# Input: tAgA with shape ((TMA_Layout), 4, 2)
# Output: tAgA_cta with shape ((TMA_Layout),)
# The (None, bidx, bidy) indexing:
# - None: keeps the entire TMA atom layout (mode 0)
# - bidx: selects from rest mode 1 (M dimension grid)
# - bidy: selects from rest mode 2 (N dimension grid)
tAgA_cta = tAgA[(None, bidx, bidy)]
tBgB_cta = tBgB[(None, bidx, bidy)]
# ---------- TMA Load: Global -> Shared ----------
cute.copy(
tma_atom_src,
tAgA_cta, # Source (TMA Tensor View)
tAsA, # Dest (SMEM Tensor View)
tma_bar_ptr=barrier_ptr,
)
# Signal arrival on the barrier after TMA is issued
with cute.arch.elect_one():
cute.arch.mbarrier_arrive(barrier_ptr)
# Wait for TMA to complete
cute.arch.mbarrier_wait(barrier_ptr, 0)
# ---------- TMA Store: Shared -> Global ----------
cute.copy(
tma_atom_dst,
tAsA, # Source (SMEM Tensor View)
tBgB_cta, # Dest (Global Tensor View)
)
def run_tma_copy(M, N, num_warmup=5, num_iters=20):
"""
Run TMA copy kernel with performance measurement.
Args:
M: Matrix dimension M
N: Matrix dimension N
num_warmup: Number of warmup iterations
num_iters: Number of timing iterations
"""
# Create tensors with shape (M, N)
a = torch.randn((M, N), dtype=torch.float16, device="cuda")
b = torch.zeros((M, N), dtype=torch.float16, device="cuda")
# Notice: We declare N-dimension as the leading dimension should be divisible by 16
a_cute = (
from_dlpack(a, assumed_align=16)
.mark_layout_dynamic(leading_dim=1)
.mark_compact_shape_dynamic(mode=1, divisibility=16)
)
b_cute = (
from_dlpack(b, assumed_align=16)
.mark_layout_dynamic(leading_dim=1)
.mark_compact_shape_dynamic(mode=1, divisibility=16)
)
copy_kernel = Sm100SimpleCopyKernel()
compiled_kernel = cute.compile(copy_kernel, a_cute, b_cute)
# Warmup runs
for _ in range(num_warmup):
compiled_kernel(a_cute, b_cute)
torch.cuda.synchronize()
# Timed runs
start_event = torch.cuda.Event(enable_timing=True)
end_event = torch.cuda.Event(enable_timing=True)
start_event.record()
for _ in range(num_iters):
compiled_kernel(a_cute, b_cute)
end_event.record()
torch.cuda.synchronize()
# Calculate performance metrics
elapsed_time_ms = start_event.elapsed_time(end_event)
avg_time_ms = elapsed_time_ms / num_iters
# Calculate throughput
# For copy: read M*N elements + write M*N elements
bytes_per_element = a.element_size()
total_bytes = 2 * M * N * bytes_per_element # Read + Write
throughput_gb_s = (total_bytes / 1e9) / (avg_time_ms / 1000)
# Print performance metrics
print(f"Matrix size: {M}×{N}")
print(f"Tile shape: {copy_kernel.tile_shape}")
print(f"Average time: {avg_time_ms:.4f} ms")
print(f"Throughput: {throughput_gb_s:.2f} GB/s")
# Verify
if torch.allclose(a, b, atol=1e-3):
print("Verification: PASSED ✓")
else:
print("Verification: FAILED ✗")
diff = (a - b).abs()
print(f"Max diff: {diff.max()}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="TMA V0: Understanding tma_partition - The Foundation of TMA Operations"
)
parser.add_argument("--M", type=int, default=512, help="Matrix dimension M")
parser.add_argument("--N", type=int, default=128, help="Matrix dimension N")
parser.add_argument(
"--num_warmup", type=int, default=5, help="Number of warmup iterations"
)
parser.add_argument(
"--num_iters", type=int, default=20, help="Number of timing iterations"
)
args = parser.parse_args()
run_tma_copy(
M=args.M,
N=args.N,
num_warmup=args.num_warmup,
num_iters=args.num_iters,
)

View File

@@ -0,0 +1,462 @@
# Copyright (c) 2025 - 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import argparse
from typing import Tuple, Type
import cutlass
import cutlass.cute as cute
import cutlass.utils as utils
import cutlass.utils.blackwell_helpers as sm100_utils
from cutlass.cute.nvgpu import cpasync
from cutlass.cute.runtime import from_dlpack
import cutlass.pipeline as pipeline
import torch
"""
TMA Matrix Transpose with Producer-Consumer Pattern: TMA load -> S2R --> R2S -> TMA store
Warp Roles (Producer-Consumer Pattern):
- Producer: TMA Load Warp (Warp 4) - Loads from Global to Shared A
- Consumer: Transpose Warps (Warp 0-3) - Wait for load, then transpose sA -> sB
- Consumer: TMA Store Warp (Warp 5) - Wait for transpose, then store sB to Global
Synchronization:
1. load_mbar_ptr: TMA Load (producer) -> Transpose Warps (consumer)
2. store_mbar_ptr: Transpose Warps (producer) -> TMA Store (consumer)
This demonstrates how different warps can use shared memory barriers to coordinate
producer-consumer relationships.
"""
class Sm100MatrixTransposeKernelV1:
def __init__(self):
self.tile_shape = (128, 128)
self.tile_m, self.tile_n = self.tile_shape
self.cluster_shape_mn = (1, 1)
self.cluster_shape_mnk = (*self.cluster_shape_mn, 1)
# Set specialized warp ids based on tile_shape
self.num_trans_warps = 4 # Maximum number of transpose warps
self.trans_warp_id = tuple(range(self.num_trans_warps))
self.tma_load_warp_id = self.num_trans_warps
self.tma_store_warp_id = self.num_trans_warps + 1
self.threads_per_cta = 32 * len(
(self.tma_store_warp_id, self.tma_load_warp_id, *self.trans_warp_id)
)
self.num_trans_threads = 32 * len(self.trans_warp_id)
self.trans_tile = (self.tile_shape[0] // self.num_trans_warps, 8)
# Set barriers for producer-consumer sync
# Barrier 1: Trans warps sync (for internal coordination)
self.trans_sync_barrier = pipeline.NamedBarrier(
barrier_id=1,
num_threads=32 * len(self.trans_warp_id),
)
# Barrier 2: TMA Store warp waits after Trans warps finish
self.store_barrier = pipeline.NamedBarrier(
barrier_id=2,
num_threads=32, # Only TMA store warp
)
self.buffer_align_bytes = 1024
@cute.jit
def __call__(self, src: cute.Tensor, dst: cute.Tensor):
if cutlass.const_expr(src.element_type != dst.element_type):
raise TypeError("Source and destination element types must match")
self.dtype: Type[cutlass.Numeric] = src.element_type
# Create transposed view of dst for TMA descriptor
# dst is (N, M), we want to view it as (M, N) transposed
transed_dst = cute.make_tensor(
dst.iterator,
cute.make_layout(
(dst.shape[1], dst.shape[0]), stride=(dst.stride[1], dst.stride[0])
),
)
# row-major smem layout for sA (tile_m, tile_n)
smem_layout_sA = sm100_utils.make_smem_layout(
utils.LayoutEnum.from_tensor(src).mma_major_mode(),
(self.tile_m, self.tile_n),
self.dtype,
1,
)
# col-major smem layout for sB (tile_n, tile_m)
# sB should match the transposed destination layout
smem_layout_sB = sm100_utils.make_smem_layout(
utils.LayoutEnum.from_tensor(transed_dst).mma_major_mode(),
(self.tile_m, self.tile_n),
self.dtype,
1,
)
@cute.struct
class SharedStorage:
# Barrier for TMA Load: producer (TMA) -> consumer (Trans warps)
load_mbar_ptr: cute.struct.MemRange[cutlass.Int64, 1]
# Barrier for TMA Store: producer (Trans warps) -> consumer (TMA Store)
store_mbar_ptr: cute.struct.MemRange[cutlass.Int64, 1]
# Single shared memory buffer (sA and sB are different views of this)
sA: cute.struct.Align[
cute.struct.MemRange[self.dtype, cute.cosize(smem_layout_sA)], 128
]
sB: cute.struct.Align[
cute.struct.MemRange[self.dtype, cute.cosize(smem_layout_sB)], 128
]
self.shared_storage = SharedStorage
self.num_tma_load_bytes = cute.size_in_bytes(self.dtype, smem_layout_sA)
# TMA Atoms
# Use swizzled layout for TMA atom to handle swizzling during load
tma_atom_src, tma_tensor_src = cpasync.make_tiled_tma_atom(
cpasync.CopyBulkTensorTileG2SOp(),
src,
smem_layout_sA,
(self.tile_m, self.tile_n),
)
tma_atom_dst, tma_tensor_dst = cpasync.make_tiled_tma_atom(
cpasync.CopyBulkTensorTileS2GOp(),
transed_dst,
smem_layout_sB,
(self.tile_m, self.tile_n),
)
grid_shape = cute.ceil_div((*src.layout.shape, 1), self.tile_shape)
self.kernel(
tma_atom_src,
tma_tensor_src,
tma_atom_dst,
tma_tensor_dst,
smem_layout_sA,
smem_layout_sB,
).launch(
grid=grid_shape,
block=(self.threads_per_cta, 1, 1),
cluster=self.cluster_shape_mnk,
)
@cute.kernel
def kernel(
self,
tma_atom_load: cute.CopyAtom,
tma_tensor_src: cute.Tensor,
tma_atom_store: cute.CopyAtom,
tma_tensor_dst: cute.Tensor,
smem_layout_sA: cute.ComposedLayout,
smem_layout_sB: cute.ComposedLayout,
):
bidx, bidy, _ = cute.arch.block_idx()
tidx, _, _ = cute.arch.thread_idx()
warp_idx = cute.arch.warp_idx()
warp_idx = cute.arch.make_warp_uniform(warp_idx)
# Allocate Shared Memory
# We need two buffers for transpose:
# sA: Source Tile (swizzled)
# sB: Destination Tile (swizzled) for TMA Store
smem = utils.SmemAllocator()
storage = smem.allocate(self.shared_storage)
sA = storage.sA.get_tensor(smem_layout_sA.outer, swizzle=smem_layout_sA.inner)
# sA = cute.make_tensor(storage.sA.iterator, smem_layout_sA)
sB = storage.sB.get_tensor(smem_layout_sB.outer, swizzle=smem_layout_sB.inner)
self.num_tma_load_bytes = cute.size_in_bytes(self.dtype, smem_layout_sA)
load_mbar_ptr = storage.load_mbar_ptr.data_ptr()
store_mbar_ptr = storage.store_mbar_ptr.data_ptr()
# ------------------------------------------------------------------
# Initialize Barriers (all warps participate in initialization)
# ------------------------------------------------------------------
if tidx == 0:
# Barrier for TMA Load: expect 1 arrive (from TMA warp after TMA completes)
cute.arch.mbarrier_init(load_mbar_ptr, 1)
cute.arch.mbarrier_expect_tx(load_mbar_ptr, self.num_tma_load_bytes)
# Barrier for TMA Store: expect arrival from Trans warps
cute.arch.mbarrier_init(store_mbar_ptr, len(self.trans_warp_id))
cute.arch.mbarrier_init_fence()
# Sync all warps after barrier initialization
cute.arch.barrier()
# ------------------------------------------------------------------
# PRODUCER: TMA Load Warp (G -> sA)
# ------------------------------------------------------------------
if warp_idx == self.tma_load_warp_id:
# Issue TMA Load
# ((TileM, TileK), loopM, LoopK)
gA = cute.local_tile(tma_tensor_src, self.tile_shape, (None, None))
# ((TileM, TileK), loopM, LoopK)
tAsA, tAgA = cpasync.tma_partition(
tma_atom_load,
0,
cute.make_layout(1),
cute.group_modes(sA, 0, 2),
cute.group_modes(gA, 0, 2),
)
cute.copy(
tma_atom_load,
tAgA[(None, bidx, bidy)],
tAsA[(None, 0)],
tma_bar_ptr=load_mbar_ptr,
)
# Arrive on mbarrier to satisfy the init count of 1
with cute.arch.elect_one():
cute.arch.mbarrier_arrive(load_mbar_ptr)
# ------------------------------------------------------------------
# CONSUMER: Transpose Warps (sA -> Reg -> sB)
# ------------------------------------------------------------------
if warp_idx < self.tma_load_warp_id:
trans_tid = tidx % self.num_trans_threads
# Wait for TMA Load to complete (consumer wait on load_mbar)
cute.arch.mbarrier_wait(load_mbar_ptr, 0)
atom = cute.make_copy_atom(
cute.nvgpu.CopyUniversalOp(),
self.dtype,
num_bits_per_copy=self.dtype.width, # Copy one element at a time
)
copy_elems = 1
# Use SAME thread layout for both read and write
# Transpose happens through sB_transposed layout view
# TV layout notation: T = thread-id, V = value-lane within that thread.
# In this example `copy_elems = 1` and `thread_layout` has shape (T, V) = (num_trans_threads, 1),
# so V is always 0 (only one value-lane per thread).
#
# Linearization rule (row-major by strides):
# idx(Ti, Vj) = i + j * num_trans_threads
# Therefore here:
# idx(Ti, V0) = i
#
# Diagram (V is the column, T is the row; showing the first two threads):
#
# V0
# ┌──────┐
# T0 │ T0V0 │ -> idx 0
# T1 │ T1V0 │ -> idx 1
# ... │ ... │
# └──────┘
#
thread_layout = cute.make_layout(
(self.num_trans_threads, 1),
stride=(1, self.num_trans_threads),
)
value_layout = cute.make_layout((1, copy_elems))
# Build a "tiled copy" operator that defines the per-thread copy mapping (T,V) for this warp-group:
# - It is used twice below via `thr_copy.partition_S(...)` and `thr_copy.partition_D(...)` to
# create matching per-thread views of the source and destination tensors.
# - With the SAME (T,V) mapping, the actual transpose is achieved by changing the tensor view
# (`sA` vs `sB` / `sB_transposed`), not by changing which threads perform the copies.
tiled_copy = cute.make_tiled_copy_tv(atom, thread_layout, value_layout)
thr_copy = tiled_copy.get_slice(trans_tid)
# Partition sA (source) for reading
tCsA = thr_copy.partition_S(sA)
# When to use `tiled_copy.retile(...)`:
# - Use it when you allocate/build a register tensor yourself (or slice/reshape it) and its
# internal layout doesn't match the TV layout expected by `tiled_copy` for copy-in/out.
# Why we don't use it here:
# - `cute.make_fragment_like(tCsA)` creates an rmem fragment with the same per-thread shape/layout
# as `tCsA`, so it already matches `tiled_copy`'s view and can be copied into directly.
tCrA = cute.make_fragment_like(tCsA)
cute.copy(tiled_copy, tCsA, tCrA)
# Partition sB for writing
tCsB = thr_copy.partition_D(sB)
# Write from register to sB
cute.copy(tiled_copy, tCrA, tCsB)
# Fence and barrier to make sure shared memory store is visible to TMA store
cute.arch.fence_proxy(
"async.shared",
space="cta",
)
self.trans_sync_barrier.arrive_and_wait()
# Trans warps signal TMA Store warp: "sB is ready!"
with cute.arch.elect_one():
cute.arch.mbarrier_arrive(store_mbar_ptr)
# ------------------------------------------------------------------
# CONSUMER: TMA Store Warp (sB -> G)
# ------------------------------------------------------------------
if warp_idx == self.tma_store_warp_id:
# Wait for Trans warp to complete (consumer wait on store_mbar)
cute.arch.mbarrier_wait(store_mbar_ptr, 0)
gDst_cta = cute.local_tile(
tma_tensor_dst, (self.tile_m, self.tile_n), (None, None)
)
tBsB, tBgB = cpasync.tma_partition(
tma_atom_store,
0,
cute.make_layout(1),
cute.group_modes(sB, 0, 2),
cute.group_modes(gDst_cta, 0, 2),
)
cute.copy(tma_atom_store, tBsB[(None, 0)], tBgB[(None, bidx, bidy)])
def run_transpose(M, N, num_warmup=5, num_iters=20):
"""
Run TMA transpose kernel with performance measurement.
Args:
M: Matrix dimension M
N: Matrix dimension N
num_warmup: Number of warmup iterations
num_iters: Number of timing iterations
Performance Metrics:
- Throughput: Actual achieved bandwidth in GB/s
- Theoretical BW: Peak memory bandwidth (2048 B/clk × 4000 MHz = 8.192 TB/s)
- Bandwidth Efficiency: Percentage of theoretical peak achieved
"""
torch.manual_seed(1111)
# Input (M, N)
input_data = torch.randn((M, N), device="cuda", dtype=torch.float16)
# Output (N, M)
output_data = torch.zeros((N, M), device="cuda", dtype=torch.float16)
# CuTe Wrappers
tensor_src = (
from_dlpack(input_data, assumed_align=16)
.mark_layout_dynamic(leading_dim=1)
.mark_compact_shape_dynamic(mode=1, divisibility=16)
)
tensor_dst = (
from_dlpack(output_data, assumed_align=16)
.mark_layout_dynamic(leading_dim=1)
.mark_compact_shape_dynamic(mode=1, divisibility=16)
)
transpose_kernel = Sm100MatrixTransposeKernelV1()
print("Start kernel compilation...")
# Compile and Run
compiled_kernel = cute.compile(
transpose_kernel, tensor_src, tensor_dst, options="--generate-line-info"
)
print("Start kernel warmup...")
# Warmup runs
for _ in range(num_warmup):
compiled_kernel(tensor_src, tensor_dst)
torch.cuda.synchronize()
print("Kernel warmup completed.")
# Timed runs
start_event = torch.cuda.Event(enable_timing=True)
end_event = torch.cuda.Event(enable_timing=True)
start_event.record()
for _ in range(num_iters):
compiled_kernel(tensor_src, tensor_dst)
end_event.record()
torch.cuda.synchronize()
# Calculate performance metrics
elapsed_time_ms = start_event.elapsed_time(end_event)
avg_time_ms = elapsed_time_ms / num_iters
# Calculate throughput
# For transpose: read M*N elements + write M*N elements
bytes_per_element = input_data.element_size()
total_bytes = 2 * M * N * bytes_per_element # Read + Write
throughput_gb_s = (total_bytes / 1e9) / (avg_time_ms / 1000)
# Theoretical bandwidth limit
# Blackwell: 2048 B/clk at 4000 MHz
bytes_per_clk = 2048
freq_mhz = 4000
theoretical_bw_gb_s = bytes_per_clk * freq_mhz * 1e6 / 1e9 # Convert to GB/s
theoretical_bw_tb_s = theoretical_bw_gb_s / 1000 # Convert to TB/s
bandwidth_efficiency = (throughput_gb_s / theoretical_bw_gb_s) * 100 # Percentage
# Print performance metrics
print(f"Matrix size: {M}×{N}")
print(f"Tile shape: {transpose_kernel.tile_shape}")
print(f"Average time: {avg_time_ms:.4f} ms")
print(f"Throughput: {throughput_gb_s:.2f} GB/s")
print(
f"Theoretical BW: {theoretical_bw_tb_s:.2f} TB/s ({theoretical_bw_gb_s:.2f} GB/s)"
)
print(f"Bandwidth Efficiency: {bandwidth_efficiency:.2f}%")
# Verification
expected = input_data.t()
if torch.allclose(output_data, expected, atol=1e-2):
print("Verification: PASSED ✓")
else:
print("Verification: FAILED ✗")
print(f"Max diff: {(output_data - expected).abs().max()}")
if __name__ == "__main__":
def parse_comma_separated_ints(s: str) -> Tuple[int, ...]:
try:
return tuple(int(x.strip()) for x in s.split(","))
except ValueError:
raise argparse.ArgumentTypeError(
"Invalid format. Expected comma-separated integers."
)
parser = argparse.ArgumentParser(
description="TMA Matrix Transpose with Producer-Consumer Pattern"
)
parser.add_argument("--M", type=int, default=128, help="Matrix dimension M")
parser.add_argument("--N", type=int, default=128, help="Matrix dimension N")
parser.add_argument(
"--num_warmup", type=int, default=5, help="Number of warmup iterations"
)
parser.add_argument(
"--num_iters", type=int, default=20, help="Number of timing iterations"
)
args = parser.parse_args()
run_transpose(
args.M,
args.N,
num_warmup=args.num_warmup,
num_iters=args.num_iters,
)

View File

@@ -0,0 +1,648 @@
# Copyright (c) 2025 - 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import argparse
from typing import Tuple, Type, Union
import cutlass
import cutlass.cute as cute
import cutlass.utils as utils
import cutlass.utils.blackwell_helpers as sm100_utils
from cutlass.cute.nvgpu import cpasync
from cutlass.cute.runtime import from_dlpack
import cutlass.pipeline as pipeline
from cutlass.pipeline import pipeline_init_arrive, pipeline_init_wait
import torch
"""
TMA Matrix Transpose with Multi-Stage Pipeline(v2)
This version extends tma_v1.py with:
1. Multi-stage pipeline: Multiple buffers for pipelining TMA loads and stores
2. Pipeline abstraction: Using PipelineTmaAsync for proper producer-consumer coordination
3. Persistent tile scheduler: Efficient work distribution across CTAs
Key Improvements over v1:
- Multi-stage buffers enable overlapping TMA loads, computation, and TMA stores
- Pipeline objects provide cleaner synchronization semantics
Note: TMA multicast is NOT used because each CTA must process different input tiles.
Warp Roles:
- Producer: TMA Load Warp - Loads from Global to Shared memory (multi-stage, multi-tile)
- Consumer: Transpose Warps - Wait for load, transpose sA -> sB (multi-tile)
- Consumer: TMA Store Warp - Wait for transpose, store sB to Global (multi-stage, multi-tile)
Pipeline Stages:
1. Load Pipeline: TMA Load (producer) -> Transpose Warps (consumer)
2. Store Pipeline: Transpose Warps (producer) -> TMA Store (consumer)
"""
class Sm100MatrixTransposeKernelV2:
def __init__(
self,
):
"""
Initialize the TMA transpose kernel with multi-stage pipeline support.
Args:
tile_shape: Tile dimensions (M, N)
cluster_shape_mn: Cluster shape for parallel CTA execution (M, N)
Note:
- Each CTA processes different tiles independently
- Stage counts are automatically computed based on available shared memory
- Persistent scheduler distributes work across CTAs in the cluster
"""
self.tile_shape = (128, 128)
self.tile_m, self.tile_n = self.tile_shape
self.cluster_shape_mn = (1, 1)
self.cluster_shape_mnl = (*self.cluster_shape_mn, 1)
# Set specialized warp ids based on tile_shape
# For 128x128 tile, use 4 transpose warps (same as v1)
self.max_trans_warps = 4 # Maximum number of transpose warps
self.num_trans_warps = self.max_trans_warps # Use all transpose warps
self.trans_warp_id = tuple(range(self.num_trans_warps))
self.tma_load_warp_id = self.num_trans_warps
self.threads_per_cta = 32 * len((self.tma_load_warp_id, *self.trans_warp_id))
self.num_trans_threads = 32 * len(self.trans_warp_id)
# Set barriers for producer-consumer sync
# Barrier 1: Trans warps sync (for internal coordination)
self.trans_sync_barrier = pipeline.NamedBarrier(
barrier_id=1,
num_threads=32 * len(self.trans_warp_id),
)
self.buffer_align_bytes = 128
# Get shared memory capacity for stage computation
self.smem_capacity = utils.get_smem_capacity_in_bytes("sm_100")
@staticmethod
def _compute_stages(
tile_m: int,
tile_n: int,
dtype: Type[cutlass.Numeric],
smem_capacity: int,
) -> Tuple[int, int]:
"""
Compute the number of load and store stages based on shared memory capacity.
Strategy:
1. Calculate bytes per stage for load (sA) and store (sB) buffers
2. Reserve space for barriers and alignment
3. Divide remaining smem by bytes per stage to get max stages
4. Clamp to reasonable min/max values
Args:
tile_m: Tile dimension M
tile_n: Tile dimension N
dtype: Data type of the tensors
smem_capacity: Total shared memory capacity in bytes
Returns:
Tuple of (num_load_stages, num_store_stages)
"""
# Calculate bytes per tile (assuming row-major and col-major layouts)
bytes_per_element = dtype.width // 8
# For sA (load buffer): tile_m x tile_n elements
sA_bytes_per_stage = tile_m * tile_n * bytes_per_element
# For sB (store buffer): tile_m x tile_n elements (transposed)
sB_bytes_per_stage = tile_m * tile_n * bytes_per_element
# Reserve space for barriers and other metadata
# Each barrier: 8 bytes (Int64)
# Estimate: max 16 barriers (load + store stages * 2) + alignment
reserved_bytes = 1024 # Conservative estimate
# Available space for staging buffers
available_smem = smem_capacity - reserved_bytes
# Calculate max stages we can fit
# We need space for both load and store stages
total_bytes_per_stage_pair = sA_bytes_per_stage + sB_bytes_per_stage
# Max stages (same for load and store for simplicity)
max_stages = available_smem // total_bytes_per_stage_pair
# Clamp to reasonable values
# Min: 2 stages for basic double buffering
# Max: 8 stages (diminishing returns beyond this)
num_stages = max(2, min(max_stages, 8))
return num_stages, num_stages
@staticmethod
def _compute_grid(
c: cute.Tensor,
cta_tile_shape_mn: Tuple[int, int],
cluster_shape_mn: Tuple[int, int],
max_active_clusters: cutlass.Constexpr,
) -> Tuple[utils.PersistentTileSchedulerParams, Tuple[int, int, int]]:
"""Use persistent tile scheduler to compute the grid size for the output tensor C.
:param c: The output tensor C
:type c: cute.Tensor
:param cta_tile_shape_mn: The shape (M, N) of the CTA tile.
:type cta_tile_shape_mn: tuple[int, int]
:param cluster_shape_mn: Shape of each cluster in M, N dimensions.
:type cluster_shape_mn: tuple[int, int]
:param max_active_clusters: Maximum number of active clusters.
:type max_active_clusters: cutlass.Constexpr
:return: A tuple containing:
- tile_sched_params: Parameters for the persistent tile scheduler.
- grid: Grid shape for kernel launch.
:rtype: Tuple[utils.PersistentTileSchedulerParams, tuple[int, int, int]]
"""
c_shape = cute.slice_(cta_tile_shape_mn, (None, None))
gc = cute.zipped_divide(c, tiler=c_shape)
num_ctas_mn = gc[(0, (None, None))].shape
cluster_shape_mnl = (*cluster_shape_mn, 1)
num_ctas_mnl = (*num_ctas_mn, 1)
tile_sched_params = utils.PersistentTileSchedulerParams(
num_ctas_mnl, cluster_shape_mnl
)
grid = utils.StaticPersistentTileScheduler.get_grid_shape(
tile_sched_params, max_active_clusters
)
return tile_sched_params, grid
@cute.jit
def __call__(
self, src: cute.Tensor, dst: cute.Tensor, max_active_clusters: cutlass.Constexpr
):
if cutlass.const_expr(src.element_type != dst.element_type):
raise TypeError("Source and destination element types must match")
self.dtype: Type[cutlass.Numeric] = src.element_type
# Compute optimal stage counts based on tile size and dtype
self.num_load_stages, self.num_store_stages = self._compute_stages(
self.tile_m,
self.tile_n,
self.dtype,
self.smem_capacity,
)
# Create transposed view of dst for TMA descriptor
# dst is (N, M), we want to view it as (M, N) transposed
transed_dst = cute.make_tensor(
dst.iterator,
cute.make_layout(
(dst.shape[1], dst.shape[0]), stride=(dst.stride[1], dst.stride[0])
),
)
# Create multi-stage layouts for load and store buffers
# row-major smem layout for sA (tile_m, tile_n)
smem_layout_sA_staged = sm100_utils.make_smem_layout(
utils.LayoutEnum.from_tensor(src).mma_major_mode(),
(self.tile_m, self.tile_n),
self.dtype,
self.num_load_stages,
)
# col-major smem layout for sB (tile_n, tile_m)
# sB should match the transposed destination layout
smem_layout_sB_staged = sm100_utils.make_smem_layout(
utils.LayoutEnum.from_tensor(transed_dst).mma_major_mode(),
(self.tile_m, self.tile_n),
self.dtype,
self.num_store_stages,
)
@cute.struct
class SharedStorage:
# Pipeline barriers for multi-stage load
load_full_mbar_ptr: cute.struct.MemRange[
cutlass.Int64, self.num_load_stages
]
load_empty_mbar_ptr: cute.struct.MemRange[
cutlass.Int64, self.num_load_stages
]
# Pipeline barriers for multi-stage store
store_full_mbar_ptr: cute.struct.MemRange[
cutlass.Int64, self.num_store_stages
]
store_empty_mbar_ptr: cute.struct.MemRange[
cutlass.Int64, self.num_store_stages
]
# Multi-stage shared memory buffers
sA: cute.struct.Align[
cute.struct.MemRange[self.dtype, cute.cosize(smem_layout_sA_staged)],
self.buffer_align_bytes,
]
sB: cute.struct.Align[
cute.struct.MemRange[self.dtype, cute.cosize(smem_layout_sB_staged)],
self.buffer_align_bytes,
]
self.shared_storage = SharedStorage
a_smem_layout = cute.slice_(smem_layout_sA_staged, (None, None, 0))
self.num_tma_load_bytes = cute.size_in_bytes(self.dtype, a_smem_layout)
# TMA Atoms
# Each CTA loads its own tile independently
tma_load_op = cpasync.CopyBulkTensorTileG2SOp()
cluster_layout_vmnk = cute.tiled_divide(
cute.make_layout((*self.cluster_shape_mn, 1)), (1,)
)
tma_atom_src, tma_tensor_src = cpasync.make_tiled_tma_atom(
tma_load_op,
src,
smem_layout_sA_staged,
(self.tile_m, self.tile_n),
)
tma_atom_dst, tma_tensor_dst = cpasync.make_tiled_tma_atom(
cpasync.CopyBulkTensorTileS2GOp(),
transed_dst,
smem_layout_sB_staged,
(self.tile_m, self.tile_n),
)
tile_sched_params, grid_shape = self._compute_grid(
transed_dst,
(self.tile_m, self.tile_n),
self.cluster_shape_mn,
max_active_clusters,
)
self.kernel(
tma_atom_src,
tma_tensor_src,
tma_atom_dst,
tma_tensor_dst,
smem_layout_sA_staged,
smem_layout_sB_staged,
cluster_layout_vmnk,
tile_sched_params,
).launch(
grid=grid_shape,
block=(self.threads_per_cta, 1, 1),
cluster=self.cluster_shape_mnl,
)
@cute.kernel
def kernel(
self,
tma_atom_load: cute.CopyAtom,
tma_tensor_src: cute.Tensor,
tma_atom_store: cute.CopyAtom,
tma_tensor_dst: cute.Tensor,
smem_layout_sA_staged: Union[cute.Layout, cute.ComposedLayout],
smem_layout_sB_staged: Union[cute.Layout, cute.ComposedLayout],
cluster_layout_vmnk: cute.Layout,
tile_sched_params: utils.PersistentTileSchedulerParams,
):
tidx, _, _ = cute.arch.thread_idx()
warp_idx = cute.arch.warp_idx()
warp_idx = cute.arch.make_warp_uniform(warp_idx)
# ---------------- Shared mem & staged buffers ----------------
smem = utils.SmemAllocator()
storage = smem.allocate(self.shared_storage)
sA_staged = storage.sA.get_tensor(
smem_layout_sA_staged.outer, swizzle=smem_layout_sA_staged.inner
)
sB_staged = storage.sB.get_tensor(
smem_layout_sB_staged.outer, swizzle=smem_layout_sB_staged.inner
)
load_mbar_ptr = storage.load_full_mbar_ptr.data_ptr()
_store_mbar_ptr = storage.store_full_mbar_ptr.data_ptr()
load_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread, 1)
load_consumer_group = pipeline.CooperativeGroup(
pipeline.Agent.Thread, self.num_trans_warps
)
load_pipeline = pipeline.PipelineTmaAsync.create(
barrier_storage=load_mbar_ptr,
num_stages=self.num_load_stages,
producer_group=load_producer_group,
consumer_group=load_consumer_group,
tx_count=self.num_tma_load_bytes,
cta_layout_vmnk=cluster_layout_vmnk,
defer_sync=True,
)
store_producer_group = pipeline.CooperativeGroup(
pipeline.Agent.Thread, self.num_trans_threads
)
store_pipeline = pipeline.PipelineTmaStore.create(
num_stages=self.num_store_stages,
producer_group=store_producer_group,
)
# Critical: Initialize pipeline barriers across cluster
# This must happen after pipeline creation and before any producer/consumer work
pipeline_init_arrive(cluster_shape_mn=self.cluster_shape_mn, is_relaxed=True)
pipeline_init_wait(cluster_shape_mn=self.cluster_shape_mn)
gA = cute.local_tile(tma_tensor_src, self.tile_shape, (None, None))
_cta_layout = cute.make_layout(
cute.slice_(cluster_layout_vmnk, (0, None, None, 0)).shape
)
# ((TileM, TileK), loopM, LoopK)
tAsA, tAgA = cpasync.tma_partition(
tma_atom_load,
0,
cute.make_layout(1),
cute.group_modes(sA_staged, 0, 2),
cute.group_modes(gA, 0, 2),
)
gDst_cta = cute.local_tile(tma_tensor_dst, self.tile_shape, (None, None))
tBsB, tBgB = cpasync.tma_partition(
tma_atom_store,
0,
cute.make_layout(1),
cute.group_modes(sB_staged, 0, 2),
cute.group_modes(gDst_cta, 0, 2),
)
# ------------------------------------------------------------------
# PRODUCER: TMA Load Warp (G -> sA)
# ------------------------------------------------------------------
if warp_idx == self.tma_load_warp_id:
tile_sched = utils.StaticPersistentTileScheduler.create(
tile_sched_params, cute.arch.block_idx(), cute.arch.grid_dim()
)
work_tile = tile_sched.initial_work_tile_info()
load_producer_state = pipeline.make_pipeline_state(
pipeline.PipelineUserType.Producer, self.num_load_stages
)
while work_tile.is_valid_tile:
# Get tile coord from tile scheduler
cur_tile_coord = work_tile.tile_idx
tAgA_slice = tAgA[(None, cur_tile_coord[0], cur_tile_coord[1])]
load_pipeline.producer_acquire(load_producer_state)
cute.copy(
tma_atom_load,
tAgA_slice,
tAsA[(None, load_producer_state.index)],
tma_bar_ptr=load_pipeline.producer_get_barrier(load_producer_state),
)
load_producer_state.advance()
tile_sched.advance_to_next_work()
work_tile = tile_sched.get_current_work()
load_pipeline.producer_tail(load_producer_state)
# ------------------------------------------------------------------
# CONSUMER: Transpose Warps (sA -> Reg -> sB)
# ------------------------------------------------------------------
if warp_idx < self.tma_load_warp_id:
tile_sched = utils.StaticPersistentTileScheduler.create(
tile_sched_params, cute.arch.block_idx(), cute.arch.grid_dim()
)
work_tile = tile_sched.initial_work_tile_info()
trans_tid = tidx % self.num_trans_threads
load_consumer_state = pipeline.make_pipeline_state(
pipeline.PipelineUserType.Consumer, self.num_load_stages
)
while work_tile.is_valid_tile:
# Get tile coord from tile scheduler
cur_tile_coord = work_tile.tile_idx
# Wait for load pipeline to have data
load_pipeline.consumer_wait(load_consumer_state)
atom = cute.make_copy_atom(
cute.nvgpu.CopyUniversalOp(),
self.dtype,
num_bits_per_copy=self.dtype.width,
)
copy_elems = 1
thread_layout = cute.make_layout(
(self.num_trans_threads, 1),
stride=(1, self.num_trans_threads),
)
value_layout = cute.make_layout((1, copy_elems))
tiled_copy = cute.make_tiled_copy_tv(atom, thread_layout, value_layout)
thr_copy = tiled_copy.get_slice(trans_tid)
# sA -> Reg
tCsA = thr_copy.partition_S(sA_staged)
tCrA = cute.make_rmem_tensor(
tCsA[(None, None, None, 0)].shape, self.dtype
)
tCrA = tiled_copy.retile(tCrA)
cute.copy(
tiled_copy,
tCsA[(None, None, None, load_consumer_state.index)],
tCrA,
)
# release load pipeline
load_pipeline.consumer_release(load_consumer_state)
load_consumer_state.advance()
index = tile_sched.num_tiles_executed % self.num_store_stages
# Reg -> sB
tCsB = thr_copy.partition_D(sB_staged)
cute.copy(tiled_copy, tCrA, tCsB[(None, None, None, index)])
# Fence to ensure smem writes are visible
cute.arch.fence_proxy(
"async.shared",
space="cta",
)
self.trans_sync_barrier.arrive_and_wait()
if warp_idx == self.trans_warp_id[0]:
cute.copy(
tma_atom_store,
tBsB[(None, index)],
tBgB[(None, cur_tile_coord[0], cur_tile_coord[1])],
)
store_pipeline.producer_commit()
store_pipeline.producer_acquire()
self.trans_sync_barrier.arrive_and_wait()
tile_sched.advance_to_next_work()
work_tile = tile_sched.get_current_work()
self.trans_sync_barrier.arrive_and_wait()
store_pipeline.producer_tail()
def run_transpose(M, N, max_active_clusters=0, num_warmup=5, num_iters=20):
"""
Run TMA transpose kernel with automatic stage calculation and performance measurement.
Args:
M: Matrix dimension M
N: Matrix dimension N
max_active_clusters: Maximum number of active clusters (0 for auto)
num_warmup: Number of warmup iterations
num_iters: Number of timing iterations
Performance Metrics:
- Throughput: Actual achieved bandwidth in GB/s
- Theoretical BW: Peak memory bandwidth (2048 B/clk × 4000 MHz = 8.192 TB/s)
- Bandwidth Efficiency: Percentage of theoretical peak achieved
"""
torch.manual_seed(1111)
# Input (M, N)
input_data = torch.randn((M, N), device="cuda", dtype=torch.float16)
# Output (N, M)
output_data = torch.zeros((N, M), device="cuda", dtype=torch.float16)
# CuTe Wrappers
tensor_src = (
from_dlpack(input_data, assumed_align=16)
.mark_layout_dynamic(leading_dim=1)
.mark_compact_shape_dynamic(mode=1, divisibility=16)
)
tensor_dst = (
from_dlpack(output_data, assumed_align=16)
.mark_layout_dynamic(leading_dim=1)
.mark_compact_shape_dynamic(mode=1, divisibility=16)
)
transpose_kernel = Sm100MatrixTransposeKernelV2()
max_active_clusters = utils.HardwareInfo().get_max_active_clusters(1)
# Compile and Run
compiled_kernel = cute.compile(
transpose_kernel,
tensor_src,
tensor_dst,
max_active_clusters,
options="--generate-line-info",
)
# Warmup runs
for _ in range(num_warmup):
compiled_kernel(tensor_src, tensor_dst)
torch.cuda.synchronize()
# Timed runs
start_event = torch.cuda.Event(enable_timing=True)
end_event = torch.cuda.Event(enable_timing=True)
start_event.record()
for _ in range(num_iters):
compiled_kernel(tensor_src, tensor_dst)
end_event.record()
torch.cuda.synchronize()
# Calculate performance metrics
elapsed_time_ms = start_event.elapsed_time(end_event)
avg_time_ms = elapsed_time_ms / num_iters
# Calculate throughput
# For transpose: read M*N elements + write M*N elements
bytes_per_element = input_data.element_size()
total_bytes = 2 * M * N * bytes_per_element # Read + Write
throughput_gb_s = (total_bytes / 1e9) / (avg_time_ms / 1000)
# Theoretical bandwidth limit
# Blackwell: 2048 B/clk at 4000 MHz
bytes_per_clk = 2048
freq_mhz = 4000
theoretical_bw_gb_s = bytes_per_clk * freq_mhz * 1e6 / 1e9 # Convert to GB/s
theoretical_bw_tb_s = theoretical_bw_gb_s / 1000 # Convert to TB/s
bandwidth_efficiency = (throughput_gb_s / theoretical_bw_gb_s) * 100 # Percentage
# Print computed stage counts after compilation
print(f"Matrix size: {M}×{N}")
print(f"Tile shape: {transpose_kernel.tile_shape}")
print(
f"Computed stages: Load={transpose_kernel.num_load_stages}, Store={transpose_kernel.num_store_stages}"
)
print(f"Average time: {avg_time_ms:.4f} ms")
print(f"Throughput: {throughput_gb_s:.2f} GB/s")
print(
f"Theoretical BW: {theoretical_bw_tb_s:.2f} TB/s ({theoretical_bw_gb_s:.2f} GB/s)"
)
print(f"Bandwidth Efficiency: {bandwidth_efficiency:.2f}%")
# Verification
expected = input_data.t()
if torch.allclose(output_data, expected, atol=1e-2):
print("Verification: PASSED ✓")
else:
print("Verification: FAILED ✗")
print(f"Max diff: {(output_data - expected).abs().max()}")
if __name__ == "__main__":
def parse_comma_separated_ints(s: str) -> Tuple[int, ...]:
try:
return tuple(int(x.strip()) for x in s.split(","))
except ValueError:
raise argparse.ArgumentTypeError(
"Invalid format. Expected comma-separated integers."
)
parser = argparse.ArgumentParser(
description="TMA Matrix Transpose with Multi-Stage Pipeline and Cluster Support (v2)"
)
parser.add_argument("--M", type=int, default=128, help="Matrix dimension M")
parser.add_argument("--N", type=int, default=128, help="Matrix dimension N")
parser.add_argument(
"--num_warmup", type=int, default=5, help="Number of warmup iterations"
)
parser.add_argument(
"--num_iters", type=int, default=20, help="Number of timing iterations"
)
args = parser.parse_args()
run_transpose(
args.M,
args.N,
num_warmup=args.num_warmup,
num_iters=args.num_iters,
)