Release v4.0.0 (#2294)

This commit is contained in:
Kihiro Bando
2025-05-13 15:55:29 -04:00
committed by GitHub
parent ad7b2f5e84
commit f115c3f854
299 changed files with 51495 additions and 4413 deletions
+57
View File
@@ -0,0 +1,57 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from .cutlass_dsl import (
Constexpr,
as_numeric,
min,
max,
and_,
or_,
all_,
any_,
not_,
all_,
any_,
select_,
# Control-flow without AST pre-processor
if_generate,
for_generate,
LoopUnroll,
while_generate,
yield_out,
# Control-flow with AST pre-processor
range_constexpr,
range_dynamic,
const_expr,
dynamic_expr,
# Data types
dtype, # Provides conversions to types inheriting from NumericType
DSLRuntimeError,
JitArgAdapterRegistry,
# Construction utilities for user-defined classes
extract_mlir_values,
new_from_mlir_values,
)
from .cute.typing import *
# Utilities not belonging to CuTe
from . import utils as utils
# Used as internal symbol
from . import cutlass_dsl as _dsl
# Aliases
LaunchConfig = _dsl.BaseDSL.LaunchConfig
register_jit_arg_adapter = _dsl.JitArgAdapterRegistry.register_jit_arg_adapter
gpu = _dsl.cutlass_gpu
cuda = _dsl.cuda_helpers
+310
View File
@@ -0,0 +1,310 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
# Use the auto-generated enum AddressSpace
from cutlass._mlir.dialects.cute import AddressSpace
# Explicitly import types that might be directly used by other modules.
# This is a fix for using Sphinx to generate documentation
# Because Sphinx processes each module in isolation, it won't be able to rely
# on re-exported symbols via wildcard imports (from .typing import *) in the
# same way that Python does at runtime.
from .typing import (
Shape,
Stride,
IntTuple,
Coord,
Tile,
XTuple,
Tiler,
Layout,
Pointer,
Tensor,
)
# Import everything else
from .typing import *
from .core import (
assume,
is_integer,
is_int_tuple,
is_static,
size,
has_underscore,
slice_,
make_ptr,
make_layout,
recast_layout,
make_fragment_like,
depth,
rank,
flatten_to_tuple,
flatten,
unflatten,
product,
product_like,
shape,
size_in_bytes,
make_identity_layout,
make_ordered_layout,
make_composed_layout,
make_layout_tv,
make_swizzle,
recast_ptr,
make_tensor,
make_identity_tensor,
make_fragment,
recast_tensor,
get,
select,
front,
is_major,
find,
coalesce,
group_modes,
cosize,
dice,
product_each,
prepend,
append,
prepend_ones,
append_ones,
ceil_div,
slice_and_offset,
crd2idx,
domain_offset,
elem_less,
transform_leaf,
filter_zeros,
filter,
tile_to_shape,
shape_div,
composition,
complement,
right_inverse,
left_inverse,
max_common_layout,
max_common_vector,
logical_product,
zipped_product,
tiled_product,
flat_product,
raked_product,
blocked_product,
flat_divide,
logical_divide,
zipped_divide,
tiled_divide,
local_partition,
local_tile,
printf,
print_tensor,
# tiled mma/tiled copy
make_mma_atom,
make_tiled_mma,
make_copy_atom,
make_tiled_copy_tv,
make_tiled_copy,
make_tiled_copy_S,
make_tiled_copy_D,
make_tiled_copy_C_atom,
basic_copy,
basic_copy_if,
autovec_copy,
copy,
gemm,
# Wrapper classes
ComposedLayout,
Swizzle,
E,
Atom,
MmaAtom,
CopyAtom,
TiledCopy,
TiledMma,
TensorSSA,
ReductionOp,
full,
full_like,
empty_like,
ones_like,
zeros_like,
where,
any_,
all_,
# User defined struct
struct,
pretty_str,
make_layout_image_mask,
repeat_like,
round_up,
is_congruent,
is_weakly_congruent,
ScaledBasis,
get_divisibility,
Ratio,
)
from . import arch
from . import nvgpu
from . import testing
from . import runtime
# Export all math ops without "math."
from .math import *
# Used as internal symbol
from .. import cutlass_dsl as _dsl
# Aliases
jit = _dsl.CuTeDSL.jit
kernel = _dsl.CuTeDSL.kernel
register_jit_arg_adapter = _dsl.JitArgAdapterRegistry.register_jit_arg_adapter
compile = _dsl.compile
# Explicitly export all symbols for documentation generation
__all__ = [
# Core types
"AddressSpace",
"Tensor",
"Layout",
"ComposedLayout",
"Swizzle",
"E",
"Atom",
"MmaAtom",
"CopyAtom",
"TiledCopy",
"TiledMma",
"TensorSSA",
# Basic utility functions
"assume",
"is_integer",
"is_int_tuple",
"is_static",
"size",
"has_underscore",
"slice_",
"depth",
"rank",
"shape",
"printf",
"print_tensor",
"pretty_str",
# Layout functions
"make_layout",
"recast_layout",
"make_identity_layout",
"make_ordered_layout",
"make_composed_layout",
"make_layout_tv",
"make_layout_image_mask",
# Tensor functions
"make_ptr",
"make_tensor",
"make_identity_tensor",
"make_fragment",
"make_fragment_like",
"recast_ptr",
"recast_tensor",
# Tensor manipulation
"get",
"select",
"front",
"is_major",
"find",
"coalesce",
"group_modes",
"cosize",
"size_in_bytes",
# Tuple operations
"flatten_to_tuple",
"flatten",
"product",
"product_like",
"product_each",
"prepend",
"append",
"prepend_ones",
"append_ones",
# Math operations
"ceil_div",
"round_up",
# Layout operations
"slice_and_offset",
"crd2idx",
"domain_offset",
"elem_less",
"filter_zeros",
"filter",
"tile_to_shape",
"shape_div",
"dice",
# Layout algebra
"composition",
"complement",
"right_inverse",
"left_inverse",
"max_common_layout",
"max_common_vector",
"is_congruent",
"is_weakly_congruent",
# Product operations
"logical_product",
"zipped_product",
"tiled_product",
"flat_product",
"raked_product",
"blocked_product",
# Division operations
"flat_divide",
"logical_divide",
"zipped_divide",
"tiled_divide",
"local_partition",
"local_tile",
# MMA and Copy operations
"make_mma_atom",
"make_tiled_mma",
"make_copy_atom",
"make_tiled_copy_tv",
"make_tiled_copy",
"make_tiled_copy_C_atom",
"basic_copy",
"basic_copy_if",
"autovec_copy",
"copy",
"gemm",
# Tensor creation
"full",
"full_like",
"empty_like",
"ones_like",
"zeros_like",
"where",
"any_",
"all_",
"repeat_like",
"ScaledBasis",
# User defined struct
"struct",
# Modules
"arch",
"nvgpu",
"testing",
"runtime",
# Decorators and code generation
"jit",
"kernel",
"register_jit_arg_adapter",
"compile",
]
@@ -0,0 +1,98 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from .elect import *
from .mbar import *
from .nvvm_wrappers import *
from .smem import *
from .tmem import *
# __all__ is required here for documentation generation
__all__ = [
#
# elect.py
#
"make_warp_uniform",
"elect_one",
#
# mbar.py
#
"mbarrier_init_arrive_cnt",
"mbarrier_init_fence",
"mbarrier_init_tx_bytes",
"mbarrier_wait",
"mbarrier_try_wait",
"conditional_mbarrier_try_wait",
"mbarrier_arrive",
#
# nvvm_wrappers.py
#
"lane_idx",
"warp_idx",
"thread_idx",
"block_dim",
"block_idx",
"grid_dim",
"cluster_idx",
"cluster_dim",
"block_in_cluster_idx",
"block_in_cluster_dim",
"block_idx_in_cluster",
"shuffle_sync",
"shuffle_sync_up",
"shuffle_sync_down",
"shuffle_sync_bfly",
"barrier",
"sync_threads",
"sync_warp",
"fence_acq_rel_cta",
"fence_acq_rel_cluster",
"fence_acq_rel_gpu",
"fence_acq_rel_sys",
"cp_async_commit_group",
"cp_async_wait_group",
"cp_async_bulk_commit_group",
"cp_async_bulk_wait_group",
"cluster_wait",
"cluster_arrive",
"cluster_arrive_relaxed",
"fence_proxy",
"vote_ballot_sync",
"popc",
"fence_view_async_tmem_load",
"fence_view_async_tmem_store",
"warpgroup_reg_alloc",
"warpgroup_reg_dealloc",
"fma_packed_f32x2",
"mul_packed_f32x2",
"add_packed_f32x2",
"fmax",
"rcp_approx",
"exp2",
# Constants
"WARP_SIZE",
# Forward from auto-generated nvvm python
"ProxyKind",
"SharedSpace",
"RoundingModeKind",
#
# smem.py
#
"alloc_smem",
"get_dyn_smem",
#
# tmem.py
#
"retrieve_tmem_ptr",
"alloc_tmem",
"relinquish_tmem_alloc_permit",
"dealloc_tmem",
]
+75
View File
@@ -0,0 +1,75 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from cutlass.cutlass_dsl import CuTeDSL, T, dsl_user_op
import cutlass._mlir.dialects.cute_nvgpu as _cute_nvgpu_ir
from cutlass._mlir.dialects import nvvm, scf
from cutlass._mlir import ir
from ..typing import Int, Int32
from ...impl_utils import check_value_in
@dsl_user_op
def make_warp_uniform(value: Int, *, loc=None, ip=None) -> Int32:
"""
Creates a warp-uniform value from the given integer input.
:param value: The integer to make warp uniform.
:type value: Int
:return: The warp-uniform value equal to the input.
:rtype: Int32
"""
return Int32(
_cute_nvgpu_ir.arch_make_warp_uniform(
Int32(value).ir_value(loc=loc, ip=ip), loc=loc, ip=ip
)
)
class IfOpRegion:
"""
A context manager for if Op.
Automatically inserts `scf.yield([])` when exiting the context.
"""
def __init__(self, block, *, loc=None, ip=None):
self.block = block
self.insert_point = ir.InsertionPoint(self.block)
self.loc = loc
self.ip = ip
def __enter__(self):
self.insert_point.__enter__()
return self.block.arguments
def __exit__(self, exc_type, exc_value, traceback):
scf.yield_([], loc=self.loc, ip=self.ip)
self.insert_point.__exit__(exc_type, exc_value, traceback)
@dsl_user_op
def elect_one(*, loc=None, ip=None) -> IfOpRegion:
"""
Elects one thread within a warp.
.. code-block:: python
with elect_one():
# Only one thread in the warp executes the code in this context
pass
"""
arch = CuTeDSL._get_dsl().envar.arch
check_value_in(arch, ["sm_90", "sm_90a", "sm_100a"], "arch")
is_thread_leader = nvvm.elect_sync(T.bool())
if_op = scf.IfOp(is_thread_leader, loc=loc, ip=ip)
return IfOpRegion(if_op.then_block, loc=loc, ip=ip)
+208
View File
@@ -0,0 +1,208 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from cutlass.cutlass_dsl import CuTeDSL, T, if_generate, dsl_user_op
from cutlass._mlir.dialects import nvvm
from cutlass._mlir import ir
from ..typing import Pointer, Int, Boolean, Int32
from ...impl_utils import check_value_in
####################################################################################################
#
# Mbarrier management utilities
#
####################################################################################################
@dsl_user_op
def mbarrier_init_arrive_cnt(mbar_ptr: Pointer, cnt: Int, *, loc=None, ip=None) -> None:
"""
Initializes a mbarrier with the specified thread arrival count.
:param mbar_ptr: A pointer to the mbarrier in SMEM
:type mbar_ptr: Pointer
:param cnt: The arrival count of the mbarrier
:type cnt: Int
"""
nvvm.mbarrier_init_shared(
mbar_ptr.llvm_ptr, Int32(cnt).ir_value(loc=loc, ip=ip), loc=loc, ip=ip
)
@dsl_user_op
def mbarrier_init_fence(*, loc=None, ip=None) -> None:
"""
A fence operation that applies to the mbarrier initializations.
"""
arch = CuTeDSL._get_dsl().envar.arch
check_value_in(arch, ["sm_90", "sm_90a", "sm_100a"], "arch")
nvvm.fence_mbarrier_init(loc=loc, ip=ip)
@dsl_user_op
def mbarrier_init_tx_bytes(
mbar_ptr: Pointer, bytes: Int, peer_cta_rank_in_cluster=None, *, loc=None, ip=None
) -> None:
"""
Initializes a mbarrier with the specified number of transaction bytes.
:param mbar_ptr: A pointer to the mbarrier in SMEM
:type mbar_ptr: Pointer
:param bytes: The number of transaction bytes
:type bytes: Int
:param peer_cta_rank_in_cluster: An optional CTA rank in cluster. If provided, the pointer to
the mbarrier is converted to a remote address in the peer CTA's
SMEM.
"""
arch = CuTeDSL._get_dsl().envar.arch
check_value_in(arch, ["sm_90", "sm_90a", "sm_100a"], "arch")
mbar_llvm_ptr = mbar_ptr.llvm_ptr
if peer_cta_rank_in_cluster is not None:
mbar_llvm_ptr = nvvm.mapa_shared_cluster(
mbar_llvm_ptr.type,
mbar_llvm_ptr,
Int32(peer_cta_rank_in_cluster).ir_value(loc=loc, ip=ip),
loc=loc,
ip=ip,
)
space = nvvm.MBarrierSpaceKind.CLUSTER
else:
space = nvvm.MBarrierSpaceKind.CTA
nvvm.mbarrier_txn(
mbar_llvm_ptr,
Int32(bytes).ir_value(loc=loc, ip=ip),
kind=nvvm.MBarrierTxnKind.ARRIVE_EXPECT_TX,
space=space,
loc=loc,
ip=ip,
)
@dsl_user_op
def mbarrier_wait(mbar_ptr: Pointer, phase: Int, *, loc=None, ip=None) -> None:
"""
Waits on a mbarrier with a specified phase.
:param mbar_ptr: A pointer to the mbarrier in SMEM
:type mbar_ptr: Pointer
:param phase: The phase to wait for (either 0 or 1)
:type phase: Int
"""
arch = CuTeDSL._get_dsl().envar.arch
check_value_in(arch, ["sm_90", "sm_90a", "sm_100a"], "arch")
timeout_ns = 10000000
# This NVVM Op is a spin-loop wrapping the mbarrier.try_wait.parity.shared.b64 PTX
# The timeout in ns only applies to the latter and this call is truly blocking
nvvm.mbarrier_try_wait_parity_shared(
mbar_ptr.llvm_ptr,
Int32(phase).ir_value(loc=loc, ip=ip),
Int32(timeout_ns).ir_value(loc=loc, ip=ip),
loc=loc,
ip=ip,
)
@dsl_user_op
def mbarrier_try_wait(mbar_ptr: Pointer, phase: Int, *, loc=None, ip=None) -> Boolean:
"""
Attempts to wait on a mbarrier with a specified phase in a non-blocking fashion.
:param mbar_ptr: A pointer to the mbarrier in SMEM
:type mbar_ptr: Pointer
:param phase: The phase to wait for (either 0 or 1)
:type phase: Int
:return: A boolean value indicating whether the wait operation was successful
:rtype: Boolean
"""
arch = CuTeDSL._get_dsl().envar.arch
check_value_in(arch, ["sm_90", "sm_90a", "sm_100a"], "arch")
return Boolean(
nvvm.mbarrier_wait_parity(
T.bool(),
mbar_ptr.llvm_ptr,
Int32(phase).ir_value(loc=loc, ip=ip),
nvvm.MBarrierWaitKind.TRY,
loc=loc,
ip=ip,
)
)
@dsl_user_op
def conditional_mbarrier_try_wait(
cond, mbar_ptr: Pointer, phase: Int, *, loc=None, ip=None
) -> Boolean:
"""
Conditionally attempts to wait on a mbarrier with a specified phase in a non-blocking fashion.
:param cond: A boolean predicate
:param mbar_ptr: A pointer to the mbarrier in SMEM
:type mbar_ptr: Pointer
:param phase: The phase to wait for (either 0 or 1)
:type phase: Int
:return: A boolean value indicating whether the wait operation was successful
:rtype: Boolean
"""
arch = CuTeDSL._get_dsl().envar.arch
check_value_in(arch, ["sm_90", "sm_90a", "sm_100a"], "arch")
return if_generate(
cond,
lambda: mbarrier_try_wait(mbar_ptr, phase, loc=loc, ip=ip),
lambda: Boolean(True).ir_value(loc=loc, ip=ip),
None,
[Boolean],
)
@dsl_user_op
def mbarrier_arrive(
mbar_ptr: Pointer, peer_cta_rank_in_cluster: Int = None, *, loc=None, ip=None
) -> None:
"""
Arrives on an mbarrier.
:param mbar_ptr: A pointer to the mbarrier in SMEM
:type mbar_ptr: Pointer
:param peer_cta_rank_in_cluster: An optional CTA rank in cluster. If provided, the pointer to
the mbarrier is converted to a remote address in the peer CTA's
SMEM.
"""
mbar_llvm_ptr = mbar_ptr.llvm_ptr
if peer_cta_rank_in_cluster is not None:
arch = CuTeDSL._get_dsl().envar.arch
check_value_in(arch, ["sm_90", "sm_90a", "sm_100a"], "arch")
mbar_llvm_ptr = nvvm.mapa_shared_cluster(
mbar_llvm_ptr.type,
mbar_llvm_ptr,
Int32(peer_cta_rank_in_cluster).ir_value(loc=loc, ip=ip),
loc=loc,
ip=ip,
)
space = nvvm.MBarrierSpaceKind.CLUSTER
else:
space = nvvm.MBarrierSpaceKind.CTA
nvvm.mbarrier_txn(
mbar_llvm_ptr,
Int32(1).ir_value(loc=loc, ip=ip),
kind=nvvm.MBarrierTxnKind.ARRIVE,
space=space,
loc=loc,
ip=ip,
)
@@ -0,0 +1,547 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from functools import partial
from typing import Optional, Tuple, Union, Callable
from cutlass.cutlass_dsl import T, dsl_user_op
from cutlass._mlir import ir
from cutlass._mlir.dialects import llvm, nvvm, vector
# Forward nvvm enums
from cutlass._mlir.dialects.nvvm import (
ProxyKind,
SharedSpace,
Tcgen05WaitKind,
SetMaxRegisterAction,
RoundingModeKind,
)
from ..typing import Int, Boolean, Int32, Float32, Numeric, as_numeric
WARP_SIZE = 32
FULL_MASK = 0xFFFFFFFF
@dsl_user_op
def lane_idx(*, loc=None, ip=None) -> Int32:
"""
Returns the lane index of the current thread within the warp.
"""
return Int32(nvvm.read_ptx_sreg_laneid(T.i32(), loc=loc, ip=ip))
@dsl_user_op
def warp_idx(*, loc=None, ip=None) -> Int32:
"""
Returns the warp index within a CTA.
"""
warp_size = 32
tid_x = Int32(nvvm.read_ptx_sreg_tid_x(T.i32(), loc=loc, ip=ip))
tid_y = Int32(nvvm.read_ptx_sreg_tid_y(T.i32(), loc=loc, ip=ip))
tid_z = Int32(nvvm.read_ptx_sreg_tid_z(T.i32(), loc=loc, ip=ip))
ntid_x = Int32(nvvm.read_ptx_sreg_ntid_x(T.i32(), loc=loc, ip=ip))
ntid_y = Int32(nvvm.read_ptx_sreg_ntid_y(T.i32(), loc=loc, ip=ip))
tid = tid_x + tid_y * ntid_x + tid_z * ntid_x * ntid_y
return tid // warp_size
@dsl_user_op
def thread_idx(*, loc=None, ip=None) -> Tuple[Int32, Int32, Int32]:
"""
Returns the thread index within a CTA.
"""
return (
Int32(nvvm.read_ptx_sreg_tid_x(T.i32(), loc=loc, ip=ip)),
Int32(nvvm.read_ptx_sreg_tid_y(T.i32(), loc=loc, ip=ip)),
Int32(nvvm.read_ptx_sreg_tid_z(T.i32(), loc=loc, ip=ip)),
)
@dsl_user_op
def block_dim(*, loc=None, ip=None) -> Tuple[Int32, Int32, Int32]:
"""
Returns the number of threads in each dimension of the CTA.
"""
return (
Int32(nvvm.read_ptx_sreg_ntid_x(T.i32(), loc=loc, ip=ip)),
Int32(nvvm.read_ptx_sreg_ntid_y(T.i32(), loc=loc, ip=ip)),
Int32(nvvm.read_ptx_sreg_ntid_z(T.i32(), loc=loc, ip=ip)),
)
@dsl_user_op
def block_idx(*, loc=None, ip=None) -> Tuple[Int32, Int32, Int32]:
"""
Returns the CTA identifier within a grid.
"""
return (
Int32(nvvm.read_ptx_sreg_ctaid_x(T.i32(), loc=loc, ip=ip)),
Int32(nvvm.read_ptx_sreg_ctaid_y(T.i32(), loc=loc, ip=ip)),
Int32(nvvm.read_ptx_sreg_ctaid_z(T.i32(), loc=loc, ip=ip)),
)
@dsl_user_op
def grid_dim(*, loc=None, ip=None) -> Tuple[Int32, Int32, Int32]:
"""
Returns the number of CTAs in each dimension of the grid.
"""
return (
Int32(nvvm.read_ptx_sreg_nctaid_x(T.i32(), loc=loc, ip=ip)),
Int32(nvvm.read_ptx_sreg_nctaid_y(T.i32(), loc=loc, ip=ip)),
Int32(nvvm.read_ptx_sreg_nctaid_z(T.i32(), loc=loc, ip=ip)),
)
@dsl_user_op
def cluster_idx(*, loc=None, ip=None) -> Tuple[Int32, Int32, Int32]:
"""
Returns the cluster identifier within a grid.
"""
return (
Int32(nvvm.read_ptx_sreg_clusterid_x(T.i32(), loc=loc, ip=ip)),
Int32(nvvm.read_ptx_sreg_clusterid_y(T.i32(), loc=loc, ip=ip)),
Int32(nvvm.read_ptx_sreg_clusterid_z(T.i32(), loc=loc, ip=ip)),
)
@dsl_user_op
def cluster_dim(*, loc=None, ip=None) -> Tuple[Int32, Int32, Int32]:
"""
Returns the number of clusters in each dimension of the grid.
"""
return (
Int32(nvvm.read_ptx_sreg_nclusterid_x(T.i32(), loc=loc, ip=ip)),
Int32(nvvm.read_ptx_sreg_nclusterid_y(T.i32(), loc=loc, ip=ip)),
Int32(nvvm.read_ptx_sreg_nclusterid_z(T.i32(), loc=loc, ip=ip)),
)
@dsl_user_op
def block_in_cluster_idx(*, loc=None, ip=None) -> Tuple[Int32, Int32, Int32]:
"""
Returns the CTA index within a cluster across all dimensions.
"""
return (
Int32(nvvm.read_ptx_sreg_cluster_ctaid_x(T.i32(), loc=loc, ip=ip)),
Int32(nvvm.read_ptx_sreg_cluster_ctaid_y(T.i32(), loc=loc, ip=ip)),
Int32(nvvm.read_ptx_sreg_cluster_ctaid_z(T.i32(), loc=loc, ip=ip)),
)
@dsl_user_op
def block_in_cluster_dim(*, loc=None, ip=None) -> Tuple[Int32, Int32, Int32]:
"""
Returns the dimensions of the cluster.
"""
return (
Int32(nvvm.read_ptx_sreg_cluster_nctaid_x(T.i32(), loc=loc, ip=ip)),
Int32(nvvm.read_ptx_sreg_cluster_nctaid_y(T.i32(), loc=loc, ip=ip)),
Int32(nvvm.read_ptx_sreg_cluster_nctaid_z(T.i32(), loc=loc, ip=ip)),
)
@dsl_user_op
def block_idx_in_cluster(*, loc=None, ip=None) -> Int32:
"""
Returns the linearized identifier of the CTA within the cluster.
"""
return Int32(nvvm.read_ptx_sreg_cluster_ctarank(T.i32(), loc=loc, ip=ip))
@dsl_user_op
def shuffle_sync_op(
value: Numeric,
offset: Int,
mask: Int = FULL_MASK,
mask_and_clamp: Int = WARP_SIZE - 1,
kind: nvvm.ShflKind = nvvm.ShflKind.idx,
*,
loc=None,
ip=None,
) -> Numeric:
"""
Shuffles a value within the threads of a warp.
:param value: The value to shuffle
:type value: Numeric
:param mask: A mask describing the threads participating in this operation
:type mask: Int
:param offset: A source lane or a source lane offset depending on kind
:type offset: Int
:param mask_and_clamp: An integer containing two packed values specifying a mask for logically
splitting warps into sub-segments and an upper bound for clamping the
source lane index.
:type mask_and_clamp: Int
:param kind: The kind of shuffle, can be idx, up, down, or bfly
:type kind: ShflKind
:return: The shuffled value
:rtype: Numeric
"""
if not isinstance(value, Numeric):
value = as_numeric(value)
return type(value)(
nvvm.shfl_sync(
type(value).mlir_type,
Int32(mask).ir_value(loc=loc, ip=ip),
value.ir_value(loc=loc, ip=ip),
Int32(offset).ir_value(loc=loc, ip=ip),
Int32(mask_and_clamp).ir_value(loc=loc, ip=ip),
kind,
loc=loc,
ip=ip,
)
)
shuffle_sync = partial(shuffle_sync_op, kind=nvvm.ShflKind.idx)
shuffle_sync_up = partial(shuffle_sync_op, kind=nvvm.ShflKind.up)
shuffle_sync_down = partial(shuffle_sync_op, kind=nvvm.ShflKind.down)
shuffle_sync_bfly = partial(shuffle_sync_op, kind=nvvm.ShflKind.bfly)
@dsl_user_op
def barrier(*, barrier_id=None, number_of_threads=None, loc=None, ip=None) -> None:
"""
Creates a barrier, optionally named.
"""
if barrier_id is not None:
barrier_id = Int32(barrier_id).ir_value(loc=loc, ip=ip)
if number_of_threads is not None:
number_of_threads = Int32(number_of_threads).ir_value(loc=loc, ip=ip)
nvvm.barrier(
barrier_id=barrier_id, number_of_threads=number_of_threads, loc=loc, ip=ip
)
@dsl_user_op
def sync_threads(*, loc=None, ip=None) -> None:
"""
Synchronizes all threads within a CTA.
"""
nvvm.barrier(loc=loc, ip=ip)
@dsl_user_op
def sync_warp(mask: Int = FULL_MASK, *, loc=None, ip=None) -> None:
"""
Performs a warp-wide sync with an optional mask.
"""
nvvm.bar_warp_sync(Int32(mask).ir_value(loc=loc, ip=ip), loc=loc, ip=ip)
@dsl_user_op
def fence_acq_rel_cta(*, loc=None, ip=None) -> None:
"""
Fence operation with acquire-release semantics.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#parallel-synchronization-and-communication-instructions-membar>`__.
"""
nvvm.fence_acq_rel_cta(loc=loc, ip=ip)
@dsl_user_op
def fence_acq_rel_cluster(*, loc=None, ip=None) -> None:
"""
Fence operation with acquire-release semantics.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#parallel-synchronization-and-communication-instructions-membar>`__.
"""
nvvm.fence_acq_rel_cluster(loc=loc, ip=ip)
@dsl_user_op
def fence_acq_rel_gpu(*, loc=None, ip=None) -> None:
"""
Fence operation with acquire-release semantics.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#parallel-synchronization-and-communication-instructions-membar>`__.
"""
nvvm.fence_acq_rel_gpu(loc=loc, ip=ip)
@dsl_user_op
def fence_acq_rel_sys(*, loc=None, ip=None) -> None:
"""
Fence operation with acquire-release semantics.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#parallel-synchronization-and-communication-instructions-membar>`__.
"""
nvvm.fence_acq_rel_sys(loc=loc, ip=ip)
@dsl_user_op
def cp_async_commit_group(*, loc=None, ip=None) -> None:
"""
Commits all prior initiated but uncommitted cp.async instructions.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#data-movement-and-conversion-instructions-cp-async-commit-group>`__.
"""
nvvm.cp_async_commit_group(loc=loc, ip=ip)
@dsl_user_op
def cp_async_wait_group(n, *, loc=None, ip=None) -> None:
"""
Waits till only a specified numbers of cp.async groups are pending.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#data-movement-and-conversion-instructions-cp-async-wait-group-cp-async-wait-all>`__.
"""
nvvm.cp_async_wait_group(n, loc=loc, ip=ip)
@dsl_user_op
def cp_async_bulk_commit_group(*, loc=None, ip=None) -> None:
"""
Commits all prior initiated but uncommitted cp.async.bulk instructions.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#data-movement-and-conversion-instructions-cp-async-bulk-commit-group>`__.
"""
nvvm.cp_async_bulk_commit_group(loc=loc, ip=ip)
@dsl_user_op
def cp_async_bulk_wait_group(group, *, read=None, loc=None, ip=None) -> None:
"""
Waits till only a specified numbers of cp.async.bulk groups are pending.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#data-movement-and-conversion-instructions-cp-async-bulk-wait-group>`__.
"""
nvvm.cp_async_bulk_wait_group(group, read=read, loc=loc, ip=ip)
@dsl_user_op
def cluster_wait(*, loc=None, ip=None) -> None:
"""
A cluster-wide wait operation.
"""
nvvm.cluster_wait(loc=loc, ip=ip)
@dsl_user_op
def cluster_arrive(*, aligned=None, loc=None, ip=None) -> None:
"""
A cluster-wide arrive operation.
"""
nvvm.cluster_arrive(aligned=aligned, loc=loc, ip=ip)
@dsl_user_op
def cluster_arrive_relaxed(*, aligned=None, loc=None, ip=None) -> None:
"""
A cluster-wide arrive operation with relaxed semantics.
"""
nvvm.cluster_arrive_relaxed(aligned=aligned, loc=loc, ip=ip)
@dsl_user_op
def fence_proxy(
kind: ProxyKind,
*,
space: Optional[SharedSpace] = None,
use_intrinsic=None,
loc=None,
ip=None,
) -> None:
nvvm.fence_proxy(
kind=kind, space=space, use_intrinsic=use_intrinsic, loc=loc, ip=ip
)
@dsl_user_op
def vote_ballot_sync(
pred: Boolean, mask: Int = FULL_MASK, *, loc=None, ip=None
) -> Int32:
"""
Performs a ballot operation across the warp.
"""
return Int32(
nvvm.vote_ballot_sync(
T.i32(),
Int32(mask).ir_value(loc=loc, ip=ip),
Boolean(pred).ir_value(loc=loc, ip=ip),
loc=loc,
ip=ip,
)
)
@dsl_user_op
def popc(value: Numeric, *, loc=None, ip=None) -> Numeric:
"""
Performs a population count operation.
"""
if not isinstance(value, Numeric):
value = as_numeric(value)
return type(value)(llvm.intr_ctpop(value.ir_value(), loc=loc, ip=ip))
@dsl_user_op
def fence_view_async_tmem_op(
kind: Tcgen05WaitKind,
*,
loc=None,
ip=None,
) -> None:
"""
Perform a fence operation on the async TMEM load or store.
.. note::
This function is only available on sm_100a and above.
The fence is required to synchronize the TMEM load/store
and let the pipeline release or commit the buffer.
Take a mma2acc pipeline as an example of LOAD fence, the ACC tensor is from TMEM.
```
# Start to copy ACC from TMEM to register
cute.copy(tmem_load, tACC, rACC)
fence_view_async_tmem_load()
# After fence, we can ensure the TMEM buffer is consumed totally.
# Release the buffer to let the MMA know it can overwrite the buffer.
mma2accum_pipeline.consumer_release(curr_consumer_state)
```
Take a TS GEMM kernel as an example of STORE fence, the A tensor is from TMEM.
```
# Start to copy A from register to TMEM
cute.copy(tmem_store, rA, tA)
fence_view_async_tmem_store()
# After fence, we can ensure the TMEM buffer is ready.
# Commit the buffer to let the MMA know it can start to load A.
tmem_mma_pipeline.producer_commit(curr_producer_state)
```
:param kind: The kind of fence operation to perform including LOAD and STORE.
:type kind: Tcgen05WaitKind
"""
nvvm.tcgen05_wait(kind, loc=loc, ip=ip)
fence_view_async_tmem_load = partial(
fence_view_async_tmem_op, kind=Tcgen05WaitKind.LOAD
)
fence_view_async_tmem_store = partial(
fence_view_async_tmem_op, kind=Tcgen05WaitKind.STORE
)
@dsl_user_op
def warpgroup_reg_realloc_op(
reg_count: int,
kind: SetMaxRegisterAction,
*,
loc=None,
ip=None,
) -> None:
nvvm.setmaxregister(reg_count, kind, loc=loc, ip=ip)
warpgroup_reg_alloc = partial(
warpgroup_reg_realloc_op, kind=SetMaxRegisterAction.increase
)
warpgroup_reg_dealloc = partial(
warpgroup_reg_realloc_op, kind=SetMaxRegisterAction.decrease
)
@dsl_user_op
def calc_packed_f32x2_op(
src_a: Tuple[Float32, Float32],
src_b: Tuple[Float32, Float32],
src_c: Tuple[Float32, Float32] | None,
calc_func: Callable,
*,
rnd=RoundingModeKind.RZ,
ftz=True,
loc=None,
ip=None,
) -> Tuple[Float32, Float32]:
vec_type = ir.VectorType.get([2], Float32.mlir_type, loc=loc)
vec_src_a = vector.from_elements(
vec_type, tuple(as_numeric(a).ir_value() for a in src_a), loc=loc, ip=ip
)
vec_src_b = vector.from_elements(
vec_type, tuple(as_numeric(b).ir_value() for b in src_b), loc=loc, ip=ip
)
if src_c is not None:
vec_src_c = vector.from_elements(
vec_type, tuple(as_numeric(c).ir_value() for c in src_c), loc=loc, ip=ip
)
vec_res = calc_func(
vec_type, vec_src_a, vec_src_b, vec_src_c, rnd=rnd, ftz=ftz, loc=loc, ip=ip
)
else:
vec_res = calc_func(
vec_type, vec_src_a, vec_src_b, rnd=rnd, ftz=ftz, loc=loc, ip=ip
)
res0 = Float32(
vector.extract(
vec_res, dynamic_position=[], static_position=[0], loc=loc, ip=ip
)
)
res1 = Float32(
vector.extract(
vec_res, dynamic_position=[], static_position=[1], loc=loc, ip=ip
)
)
return res0, res1
fma_packed_f32x2 = partial(calc_packed_f32x2_op, calc_func=nvvm.fma_packed_f32x2)
mul_packed_f32x2 = partial(
calc_packed_f32x2_op, src_c=None, calc_func=nvvm.mul_packed_f32x2
)
add_packed_f32x2 = partial(
calc_packed_f32x2_op, src_c=None, calc_func=nvvm.add_packed_f32x2
)
@dsl_user_op
def fmax(
a: Union[float, Float32], b: Union[float, Float32], *, loc=None, ip=None
) -> Float32:
return Float32(
nvvm.fmax(
T.f32(),
Float32(a).ir_value(loc=loc, ip=ip),
Float32(b).ir_value(loc=loc, ip=ip),
loc=loc,
ip=ip,
)
)
@dsl_user_op
def rcp_approx(a: Union[float, Float32], *, loc=None, ip=None):
return Float32(
nvvm.rcp_approx_ftz_f(
T.f32(), Float32(a).ir_value(loc=loc, ip=ip), loc=loc, ip=ip
)
)
@dsl_user_op
def exp2(a: Union[float, Float32], *, loc=None, ip=None) -> Float32:
return Float32(
llvm.inline_asm(
T.f32(),
[Float32(a).ir_value(loc=loc, ip=ip)],
"ex2.approx.ftz.f32 $0, $1;",
"=f,f",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
)
+96
View File
@@ -0,0 +1,96 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from typing import Optional, Type
from cutlass.cutlass_dsl import T, dsl_user_op
import cutlass._mlir.dialects.cute as _cute_ir
import cutlass._mlir.dialects.cute_nvgpu as _cute_nvgpu_ir
from cutlass._mlir import ir
from ..typing import Pointer, Numeric, NumericMeta
@dsl_user_op
def alloc_smem(
element_type: Type[Numeric],
size_in_elems: int,
alignment: Optional[int] = None,
*,
loc=None,
ip=None,
) -> Pointer:
"""
Statically allocates SMEM.
:param element_type: The pointee type of the pointer.
:type element_type: Type[Numeric]
:param size_in_elems: The size of the allocation in terms of number of elements of the
pointee type
:type size_in_elems: int
:param alignment: An optional pointer alignment for the allocation
:type alignment: int
:return: A pointer to the start of the allocation
:rtype: Pointer
"""
if not isinstance(element_type, NumericMeta):
raise TypeError(
f"element_type must be a type of Numeric, but got {element_type}"
)
if alignment is None:
# Default alignment based on the element type's width
alignment = element_type.width // 8
ptr_ty = _cute_ir.PtrType.get(
element_type.mlir_type, _cute_ir.AddressSpace.smem, alignment
)
return _cute_nvgpu_ir.arch_alloc_smem(
ptr=ptr_ty,
input=ir.IntegerAttr.get(T.i32(), size_in_elems),
loc=loc,
ip=ip,
)
@dsl_user_op
def get_dyn_smem(
element_type: Type[Numeric],
alignment: Optional[int] = None,
*,
loc=None,
ip=None,
) -> Pointer:
"""
Retrieves a pointer to a dynamic SMEM allocation.
:param element_type: The pointee type of the pointer.
:type element_type: Type[Numeric]
:param alignment: An optional pointer alignment, the result pointer is offset appropriately
:type alignment: int
:return: A pointer to the start of the dynamic SMEM allocation with a correct
alignement
:rtype: Pointer
"""
if not isinstance(element_type, NumericMeta):
raise TypeError(
f"element_type must be a type of Numeric, but got {element_type}"
)
if alignment is None:
# Default alignment based on the element type's width
alignment = element_type.width // 8
ptr_ty = _cute_ir.PtrType.get(
element_type.mlir_type,
_cute_ir.AddressSpace.smem,
alignment,
)
return _cute_nvgpu_ir.arch_get_dyn_smem(ptr=ptr_ty, loc=loc, ip=ip)
+142
View File
@@ -0,0 +1,142 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from typing import Type
from cutlass.cutlass_dsl import dsl_user_op
import cutlass._mlir.dialects.cute as _cute_ir
import cutlass._mlir.dialects.cute_nvgpu as _cute_nvgpu_ir
from ..typing import Pointer, Int, Int32, Numeric, NumericMeta
SM100_TMEM_CAPACITY_COLUMNS = 512
SM100_TMEM_MIN_ALLOC_COLUMNS = 32
@dsl_user_op
def retrieve_tmem_ptr(
element_type: Type[Numeric],
alignment: int,
ptr_to_buffer_holding_addr: Pointer,
*,
loc=None,
ip=None,
) -> Pointer:
"""
Retrieves a pointer to TMEM with the provided element type and alignment.
:param element_type: The pointee type of the pointer.
:type element_type: Type[Numeric]
:param alignment: The alignment of the result pointer
:type alignment: int
:param ptr_to_buffer_holding_addr: A pointer to a SMEM buffer holding the TMEM address of the
start of the allocation allocation
:type ptr_to_buffer_holding_addr: Pointer
:return: A pointer to TMEM
:rtype: Pointer
"""
if not isinstance(element_type, NumericMeta):
raise TypeError(
f"element_type must be a type of Numeric, but got {element_type}"
)
res_ty = _cute_ir.PtrType.get(
element_type.mlir_type, _cute_ir.AddressSpace.tmem, alignment
)
return _cute_nvgpu_ir.arch_sm100_retrieve_tmem_ptr(
res_ty, ptr_to_buffer_holding_addr.value, loc=loc, ip=ip
)
@dsl_user_op
def alloc_tmem(
num_columns: Int,
smem_ptr_to_write_address: Pointer,
is_two_cta=None,
*,
loc=None,
ip=None,
) -> None:
"""
Allocates TMEM.
:param num_columns: The number of TMEM columns to allocate
:type num_columns: Int
:param smem_ptr_to_write_address: A pointer to a SMEM buffer where the TMEM address is written
to
:type smem_ptr_to_write_address: Pointer
:param is_two_cta: Optional boolean parameter for 2-CTA MMAs
"""
if isinstance(num_columns, int):
if (
num_columns < SM100_TMEM_MIN_ALLOC_COLUMNS
or num_columns > SM100_TMEM_CAPACITY_COLUMNS
or not (num_columns & (num_columns - 1) == 0)
):
raise ValueError(
f"num_columns must be between 32 and 512, and must be pow of 2, but got {num_columns}"
)
_cute_nvgpu_ir.arch_sm100_alloc_tmem(
Int32(num_columns).ir_value(loc=loc, ip=ip),
smem_ptr_to_write_address.value,
is_two_cta=is_two_cta,
loc=loc,
ip=ip,
)
@dsl_user_op
def relinquish_tmem_alloc_permit(is_two_cta=None, *, loc=None, ip=None) -> None:
"""
Relinquishes the right to allocate TMEM so that other CTAs potentially in a different grid can
allocate.
"""
_cute_nvgpu_ir.arch_sm100_relinquish_tmem_alloc_permit(
is_two_cta=is_two_cta, loc=loc, ip=ip
)
@dsl_user_op
def dealloc_tmem(
tmem_ptr: Pointer,
num_columns: Int,
is_two_cta=None,
*,
loc=None,
ip=None,
) -> None:
"""
Deallocates TMEM using the provided pointer and number of columns.
:param tmem_ptr: A pointer to the TMEM allocation to de-allocate
:type tmem_ptr: Pointer
:param num_columns: The number of columns in the TMEM allocation
:type num_columns: Int
:param is_two_cta: Optional boolean parameter for 2-CTA MMAs
"""
if isinstance(num_columns, int):
if (
num_columns < SM100_TMEM_MIN_ALLOC_COLUMNS
or num_columns > SM100_TMEM_CAPACITY_COLUMNS
or not (num_columns & (num_columns - 1) == 0)
):
raise ValueError(
f"num_columns must be between 32 and 512, and must be pow of 2, but got {num_columns}"
)
_cute_nvgpu_ir.arch_sm100_dealloc_tmem(
tmem_ptr.value,
Int32(num_columns).ir_value(loc=loc, ip=ip),
is_two_cta=is_two_cta,
loc=loc,
ip=ip,
)
File diff suppressed because it is too large Load Diff
+354
View File
@@ -0,0 +1,354 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from .core import TensorSSA
from cutlass._mlir.dialects import math, arith
def acos(a: TensorSSA) -> TensorSSA:
"""Compute element-wise arc cosine of the input tensor.
:param a: Input tensor
:type a: TensorSSA
:return: Tensor containing the arc cosine of each element in input tensor
:rtype: TensorSSA
Example:
.. code-block::
x = cute.make_fragment(layout) # Create tensor
y = x.load() # Load values
z = acos(y) # Compute arc cosine
"""
return TensorSSA(math.acos(a, fastmath=arith.FastMathFlags.none), a.shape, a.dtype)
def asin(a: TensorSSA) -> TensorSSA:
"""Compute element-wise arc sine of the input tensor.
:param a: Input tensor
:type a: TensorSSA
:return: Tensor containing the arc sine of each element in input tensor
:rtype: TensorSSA
Example:
.. code-block::
x = cute.make_fragment(layout) # Create tensor
y = x.load() # Load values
z = asin(y) # Compute arc sine
"""
return TensorSSA(math.asin(a, fastmath=arith.FastMathFlags.none), a.shape, a.dtype)
def atan(a: TensorSSA, fastmath: bool = False) -> TensorSSA:
"""Compute element-wise arc tangent of the input tensor.
:param a: Input tensor
:type a: TensorSSA
:param fastmath: Enable fast math optimizations, defaults to False
:type fastmath: bool, optional
:return: Tensor containing the arc tangent of each element in input tensor
:rtype: TensorSSA
Example:
.. code-block::
x = cute.make_fragment(layout) # Create tensor
y = x.load() # Load values
z = atan(y) # Compute arc tangent
"""
raise NotImplementedError("atan is not implemented")
return TensorSSA(math.atan(a, fastmath=arith.FastMathFlags.none), a.shape, a.dtype)
def atan2(a: TensorSSA, b: TensorSSA, fastmath: bool = False) -> TensorSSA:
"""Compute element-wise arc tangent of two tensors.
Computes atan2(a, b) element-wise. The function atan2(a, b) is the angle in radians
between the positive x-axis and the point given by the coordinates (b, a).
:param a: First input tensor (y-coordinates)
:type a: TensorSSA
:param b: Second input tensor (x-coordinates)
:type b: TensorSSA
:param fastmath: Enable fast math optimizations, defaults to False
:type fastmath: bool, optional
:return: Tensor containing the arc tangent of a/b element-wise
:rtype: TensorSSA
Example:
.. code-block::
y = cute.make_fragment(ptr1, layout).load() # y coordinates
x = cute.make_fragment(ptr2, layout).load() # x coordinates
theta = atan2(y, x) # Compute angles
"""
return TensorSSA(
math.atan2(a, b, fastmath=arith.FastMathFlags.none), a.shape, a.dtype
)
def cos(a: TensorSSA, fastmath: bool = False) -> TensorSSA:
"""Compute element-wise cosine of the input tensor.
:param a: Input tensor (in radians)
:type a: TensorSSA
:param fastmath: Enable fast math optimizations, defaults to False
:type fastmath: bool, optional
:return: Tensor containing the cosine of each element
:rtype: TensorSSA
Example:
.. code-block::
x = cute.make_fragment(layout) # Create tensor
y = x.load() # Load values
z = cos(y) # Compute cosine
"""
return TensorSSA(math.cos(a, fastmath=arith.FastMathFlags.none), a.shape, a.dtype)
def erf(a: TensorSSA, fastmath: bool = False) -> TensorSSA:
"""Compute element-wise error function of the input tensor.
The error function is defined as:
erf(x) = 2/π [0 to x] exp(-) dt
:param a: Input tensor
:type a: TensorSSA
:param fastmath: Enable fast math optimizations, defaults to False
:type fastmath: bool, optional
:return: Tensor containing the error function value for each element
:rtype: TensorSSA
Example:
.. code-block::
x = cute.make_fragment(layout) # Create tensor
y = x.load() # Load values
z = erf(y) # Compute error function
"""
return TensorSSA(math.erf(a, fastmath=arith.FastMathFlags.none), a.shape, a.dtype)
def exp2(a: TensorSSA, fastmath: bool = False) -> TensorSSA:
"""Compute element-wise base-2 exponential of the input tensor.
:param a: Input tensor
:type a: TensorSSA
:param fastmath: Enable fast math optimizations, defaults to False
:type fastmath: bool, optional
:return: Tensor containing 2 raised to the power of each element
:rtype: TensorSSA
Example:
.. code-block::
x = cute.make_fragment(layout) # Create tensor
y = x.load() # Load values
z = exp2(y) # Compute 2^x
"""
return TensorSSA(math.exp2(a, fastmath=arith.FastMathFlags.none), a.shape, a.dtype)
def log(a: TensorSSA, fastmath: bool = False) -> TensorSSA:
"""Compute element-wise natural logarithm of the input tensor.
:param a: Input tensor
:type a: TensorSSA
:param fastmath: Enable fast math optimizations, defaults to False
:type fastmath: bool, optional
:return: Tensor containing the natural logarithm of each element
:rtype: TensorSSA
Example:
.. code-block::
x = cute.make_fragment(layout) # Create tensor
y = x.load() # Load values
z = log(y) # Compute natural logarithm
"""
return TensorSSA(math.log(a, fastmath=arith.FastMathFlags.none), a.shape, a.dtype)
def log2(a: TensorSSA, fastmath: bool = False) -> TensorSSA:
"""Compute element-wise base-2 logarithm of the input tensor.
:param a: Input tensor
:type a: TensorSSA
:param fastmath: Enable fast math optimizations, defaults to False
:type fastmath: bool, optional
:return: Tensor containing the base-2 logarithm of each element
:rtype: TensorSSA
Example:
.. code-block::
x = cute.make_fragment(layout) # Create tensor
y = x.load() # Load values
z = log2(y) # Compute log base 2
"""
return TensorSSA(math.log2(a, fastmath=arith.FastMathFlags.none), a.shape, a.dtype)
def log10(a: TensorSSA, fastmath: bool = False) -> TensorSSA:
"""Compute element-wise base-10 logarithm of the input tensor.
:param a: Input tensor
:type a: TensorSSA
:param fastmath: Enable fast math optimizations, defaults to False
:type fastmath: bool, optional
:return: Tensor containing the base-10 logarithm of each element
:rtype: TensorSSA
Example:
.. code-block::
x = cute.make_fragment(layout) # Create tensor
y = x.load() # Load values
z = log10(y) # Compute log base 10
"""
return TensorSSA(math.log10(a, fastmath=arith.FastMathFlags.none), a.shape, a.dtype)
def rsqrt(a: TensorSSA, fastmath: bool = False) -> TensorSSA:
"""Compute element-wise reciprocal square root of the input tensor.
Computes 1/x element-wise.
:param a: Input tensor
:type a: TensorSSA
:param fastmath: Enable fast math optimizations, defaults to False
:type fastmath: bool, optional
:return: Tensor containing the reciprocal square root of each element
:rtype: TensorSSA
Example:
.. code-block::
x = cute.make_fragment(layout) # Create tensor
y = x.load() # Load values
z = rsqrt(y) # Compute 1/√x
"""
return TensorSSA(math.rsqrt(a, fastmath=arith.FastMathFlags.none), a.shape, a.dtype)
def sin(a: TensorSSA, fastmath: bool = False) -> TensorSSA:
"""Compute element-wise sine of the input tensor.
:param a: Input tensor (in radians)
:type a: TensorSSA
:param fastmath: Enable fast math optimizations, defaults to False
:type fastmath: bool, optional
:return: Tensor containing the sine of each element
:rtype: TensorSSA
Example:
.. code-block::
x = cute.make_fragment(layout) # Create tensor
y = x.load() # Load values
z = sin(y) # Compute sine
"""
return TensorSSA(math.sin(a, fastmath=arith.FastMathFlags.none), a.shape, a.dtype)
def sqrt(a: TensorSSA, fastmath: bool = False) -> TensorSSA:
"""Compute element-wise square root of the input tensor.
:param a: Input tensor
:type a: TensorSSA
:param fastmath: Enable fast math optimizations, defaults to False
:type fastmath: bool, optional
:return: Tensor containing the square root of each element
:rtype: TensorSSA
Example:
.. code-block::
x = cute.make_fragment(layout) # Create tensor
y = x.load() # Load values
z = sqrt(y) # Compute square root
"""
return TensorSSA(math.sqrt(a, fastmath=arith.FastMathFlags.none), a.shape, a.dtype)
def tan(a: TensorSSA) -> TensorSSA:
"""Compute element-wise tangent of the input tensor.
:param a: Input tensor (in radians)
:type a: TensorSSA
:return: Tensor containing the tangent of each element
:rtype: TensorSSA
Example:
.. code-block::
x = cute.make_fragment(layout) # Create tensor
y = x.load() # Load values
z = tan(y) # Compute tangent
"""
return TensorSSA(math.tan(a, fastmath=arith.FastMathFlags.none), a.shape, a.dtype)
def tanh(a: TensorSSA, fastmath: bool = False) -> TensorSSA:
"""Compute element-wise hyperbolic tangent of the input tensor.
:param a: Input tensor
:type a: TensorSSA
:param fastmath: Enable fast math optimizations, defaults to False
:type fastmath: bool, optional
:return: Tensor containing the hyperbolic tangent of each element
:rtype: TensorSSA
Example:
.. code-block::
x = cute.make_fragment(layout) # Create tensor
y = x.load() # Load values
z = tanh(y) # Compute hyperbolic tangent
"""
return TensorSSA(math.tanh(a, fastmath=arith.FastMathFlags.none), a.shape, a.dtype)
__all__ = [
"acos",
"asin",
"atan",
"atan2",
"cos",
"erf",
"exp2",
"log",
"log10",
"log2",
"rsqrt",
"sin",
"sqrt",
"tan",
"tanh",
]
@@ -0,0 +1,26 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from . import warp
from . import cpasync
from . import warpgroup
from . import tcgen05
from .common import *
from .helpers import *
# __all__ is required here for documentation generation
__all__ = [
"OpError",
"MmaUniversalOp",
"CopyUniversalOp",
]
+143
View File
@@ -0,0 +1,143 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from dataclasses import dataclass
from typing import Type, Optional
from cutlass.cutlass_dsl import DSLBaseError
import cutlass._mlir.dialects.cute as _cute_ir
import cutlass._mlir.dialects.cute_nvgpu as _cute_nvgpu_ir
from cutlass._mlir import ir
from .. import core
from ..typing import Float16, Float32, Float64, Numeric
class OpError(DSLBaseError):
"""
An exception class for Op construction errors.
"""
def __init__(
self, op: core.Op, message: str, suggestion: Optional[str] = None
) -> None:
if suggestion is None:
# Default suggestion
suggestion = "Check your Op construction code"
super().__init__(
message,
error_code=f"{op.__class__.__name__} error",
suggestion=suggestion,
)
####################################################################################################
#
# MMA Ops and Traits
#
####################################################################################################
@dataclass(frozen=True)
class MmaUniversalOp(core.MmaOp):
"""
The universal MMA Operation.
This Operation currently expects the A/B operands as well as the accumulator to share the same
data types.
:param abacc_dtype: The data type for the A/B operands and the accumulator
:type abacc_dtype: Type[Numeric]
"""
abacc_dtype: Type[Numeric]
def __post_init__(self) -> None:
if self.abacc_dtype not in [Float16, Float32, Float64]:
raise OpError(
self,
f"expects the 'abacc_dtype' Op parameter to be one of Float16, Float32, or Float64",
)
def __str__(self) -> str:
return (
"universal MMA Operation using FMA"
f"\n A/B/Accumulator data type = {self.abacc_dtype}"
)
def _make_trait(self, *, loc=None, ip=None, **kwargs) -> "MmaUniversalTrait":
shape_mnk_attr = ir.Attribute.parse(f'#cute.shape<"(1,1,1)">')
atom_ty = _cute_nvgpu_ir.UniversalFmaAtomType.get(
shape_mnk_attr,
self.abacc_dtype.mlir_type,
self.abacc_dtype.mlir_type,
self.abacc_dtype.mlir_type,
)
return MmaUniversalTrait(_cute_ir.atom(atom_ty, loc=loc, ip=ip))
class MmaUniversalTrait(core.Trait):
pass
####################################################################################################
#
# Copy Ops and Traits
#
####################################################################################################
@dataclass(frozen=True)
class CopyUniversalOp(core.CopyOp):
"""
The universal Copy Operation.
When creating a Copy Atom out of this operation, the expected usage pattern is
.. code-block:: python
op = cute.nvgpu.CopyUniversalOp()
atom = cute.make_copy_atom(op, tensor_dtype, num_bits_per_copy=64)
- ``tensor_dtype`` is the data type used to build the reference TV Layout (either the source \
or the destination TV Layout) in unit of tensor elements and is used for partitioning by \
``TiledCopy`` for example
- ``num_bits_per_copy`` is a kw argument specifying the number of bits to copy per Atom \
execution. This can be larger than the width of the above data type. When not provided, \
the compiler will do a best effort at auto-vectorizing.
"""
def __str__(self) -> str:
return "universal Copy Operation"
def _make_trait(
self,
copy_internal_type: Type[Numeric],
*,
loc=None,
ip=None,
**kwargs,
) -> "CopyUniversalTrait":
num_bits_per_copy = kwargs.get("num_bits_per_copy", 0)
if not isinstance(num_bits_per_copy, int) or (num_bits_per_copy < 0):
raise ValueError(
"expects a 'num_bits_per_copy' kw argument of type int that is non-negative "
f"when creating a copy Atom for {self.__class__.__name__}"
)
ty = _cute_nvgpu_ir.CopyAtomSIMTSyncCopyType.get(
copy_internal_type.mlir_type, num_bits_per_copy
)
return CopyUniversalTrait(_cute_ir.atom(ty, loc=loc, ip=ip))
class CopyUniversalTrait(core.Trait):
pass
@@ -0,0 +1,38 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from .copy import *
from .helpers import *
# __all__ is required here for documentation generation
__all__ = [
#
# copy.py
#
"LoadCacheMode",
"CopyG2SOp",
"CopyBulkTensorTileG2SOp",
"CopyBulkTensorTileG2SMulticastOp",
"CopyBulkTensorTileS2GOp",
#
# helpers.py
#
"make_tma_tile_atom",
"tma_partition",
"create_tma_multicast_mask",
"prefetch_descriptor",
"copy_tensormap",
"update_tma_descriptor",
"fence_tma_desc_acquire",
"cp_fence_tma_desc_release",
"fence_tma_desc_release",
]
@@ -0,0 +1,366 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
import enum
from dataclasses import dataclass
from typing import Optional, Type
from cutlass.cutlass_dsl import CuTeDSL, t
import cutlass._mlir.dialects.cute as _cute_ir
import cutlass._mlir.dialects.cute_nvgpu as _cute_nvgpu_ir
from cutlass._mlir import ir
from ...core import CopyOp, Trait
from ...typing import Int16, Pointer, Integer, Numeric
from ..common import OpError
from ..tcgen05.mma import CtaGroup
####################################################################################################
#
# Aynchronous copies
#
####################################################################################################
class LoadCacheMode(enum.Enum):
"""
An enumeration for the possible cache modes of a non-bulk ``cp.async`` instruction.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#cache-operators>`__.
"""
ALWAYS = _cute_nvgpu_ir.LoadCacheMode.always
GLOBAL = _cute_nvgpu_ir.LoadCacheMode.global_
STREAMING = _cute_nvgpu_ir.LoadCacheMode.streaming
LAST_USE = _cute_nvgpu_ir.LoadCacheMode.last_use
NONE = _cute_nvgpu_ir.LoadCacheMode.none
def __str__(self) -> str:
return f"{self.__class__.__name__}.{self.name}"
def __repr__(self) -> str:
return f"<{self.__class__.__name__}.{self.name}>"
def _to_ir(self) -> _cute_nvgpu_ir.LoadCacheMode:
return self.value
@dataclass(frozen=True)
class CopyG2SOp(CopyOp):
"""
Non-bulk asynchronous GMEM to SMEM Copy Operation.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#data-movement-and-conversion-instructions-non-bulk-copy>`__.
"""
cache_mode: LoadCacheMode = LoadCacheMode.ALWAYS
def __str__(self) -> str:
res = "cp.async GMEM -> SMEM copy Operation"
if self.cache_mode != LoadCacheMode.ALWAYS:
res += f"\n with cache mode = {self.cache_mode}"
return res
def _make_trait(
self,
copy_internal_type: Type[t.Numeric],
*,
loc=None,
ip=None,
**kwargs,
) -> "CopyG2STrait":
num_bits_per_copy = kwargs.get("num_bits_per_copy", None)
if not isinstance(num_bits_per_copy, int) or (num_bits_per_copy <= 0):
raise ValueError(
"expects a 'num_bits_per_copy' kw argument of type int that is positive "
f"when creating a copy Atom for {self.__class__.__name__}"
)
# Verify that the user provided enum values
if not isinstance(self.cache_mode, LoadCacheMode):
raise OpError(
self,
"expects the 'cache_mode' Op parameter to be a LoadCacheMode instance",
)
ty = _cute_nvgpu_ir.CopyAtomSIMTAsyncCopyType.get(
copy_internal_type.mlir_type, self.cache_mode._to_ir(), num_bits_per_copy
)
return CopyG2STrait(_cute_ir.atom(ty, loc=loc, ip=ip))
class CopyG2STrait(Trait):
pass
####################################################################################################
#
# Bulk tensor copies a.k.a TMA copies
#
####################################################################################################
TMA_MBAR_PTR_FIELD_NAME = "tma_bar"
TMA_MASK_FIELD_NAME = "mcast_mask"
TMA_DESC_PTR_FIELD_NAME = "tma_descriptor_ptr"
#
# TMA GMEM -> SMEM copies
#
@dataclass(frozen=True)
class CopyBulkTensorTileG2SOp(CopyOp):
"""
Bulk tensor asynchrnous GMEM to SMEM Copy Operation using the TMA unit.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#data-movement-and-conversion-instructions-cp-async-bulk-tensor>`__.
This Operation uses TMA in the ``.tile`` mode.
"""
cta_group: CtaGroup = CtaGroup.ONE
admissible_archs = ["sm_90", "sm_90a", "sm_100a"]
def __post_init__(self) -> None:
if not isinstance(self.cta_group, CtaGroup):
raise OpError(
self, "expects the 'cta_group' parameter to be a CtaGroup instance"
)
# Arch verification
arch = CuTeDSL._get_dsl().envar.arch
if arch not in self.admissible_archs:
raise OpError(
self,
f"expects arch to be one of {self.admissible_archs}, but got {arch}",
suggestion="Ensure env CUTE_DSL_ARCH matches your GPU architecture",
)
if (self.cta_group == CtaGroup.TWO) and arch[:5] == "sm_90":
raise OpError(
self,
f"CTA group of 2 is tcgen05-specific and is not and is not compatible with {arch}",
suggestion="Ensure env CUTE_DSL_ARCH matches your GPU architecture",
)
def __str__(self) -> str:
res = "cp.async GMEM -> SMEM bulk tensor copy Operation"
if self.cta_group == CtaGroup.TWO:
res += f"\n CTA group = 2"
return res
def _make_trait(
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
) -> "CopyBulkTensorTileG2SNonExecTrait":
raise NotImplementedError(
"Use cpasync.make_tma_tile_atom to obtain a copy Atom for TMA"
)
def _to_ir(self) -> _cute_nvgpu_ir.TiledTmaLoadEnum:
if self.cta_group == CtaGroup.ONE:
return _cute_nvgpu_ir.TiledTmaLoadEnum.sm_90
elif self.cta_group == CtaGroup.TWO:
return _cute_nvgpu_ir.TiledTmaLoadEnum.sm_100_2sm
else:
assert False, "unrecognized self.cta_group"
class CopyBulkTensorTileG2SNonExecTrait(Trait):
# We allow kw args to be dropped so that the user can write common code for non-multicast
# and multicast loads.
def unpack(
self,
*,
loc=None,
ip=None,
tma_bar_ptr: Optional[Pointer] = None,
tma_desc_ptr: Optional[Pointer] = None,
**kwargs,
):
"""
Custom implementation of unpack for non-executable TMAs.
The non-multicast TMA load requires a `tma_bar_ptr` keyword argument to be provided when
using `cute.copy`. Any other kw arguments will be ignored instead of triggering an error.
"""
if not isinstance(tma_bar_ptr, Pointer):
raise ValueError(
"expects a pointer to an mbarrier to be provided via the tma_bar_ptr kw argument"
)
exec_value = _cute_nvgpu_ir.atom_make_exec_tma(self.value, loc=loc, ip=ip)
attr_str = f"#cute_nvgpu.atom_copy_field_tmaload<{TMA_MBAR_PTR_FIELD_NAME}>"
attr = ir.Attribute.parse(attr_str)
exec_value = _cute_nvgpu_ir.atom_set_value(
exec_value, attr, tma_bar_ptr.value, loc=loc, ip=ip
)
if isinstance(tma_desc_ptr, Pointer):
attr_str = f"#cute_nvgpu.atom_copy_field_tmaload<{TMA_DESC_PTR_FIELD_NAME}>"
attr = ir.Attribute.parse(attr_str)
exec_value = _cute_nvgpu_ir.atom_set_value(
exec_value, attr, tma_desc_ptr.value, loc=loc, ip=ip
)
return exec_value
#
# TMA GMEM -> SMEM multicast copies
#
@dataclass(frozen=True)
class CopyBulkTensorTileG2SMulticastOp(CopyOp):
"""
Bulk tensor asynchrnous multicast GMEM to SMEM Copy Operation using the TMA unit.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#data-movement-and-conversion-instructions-cp-async-bulk-tensor>`__.
This Operation uses TMA in the ``.tile`` mode.
"""
cta_group: CtaGroup = CtaGroup.ONE
admissible_archs = ["sm_90", "sm_90a", "sm_100a"]
def __post_init__(self):
if not isinstance(self.cta_group, CtaGroup):
raise OpError(
self, "expects the 'cta_group' parameter to be a CtaGroup instance"
)
# Arch verification
arch = CuTeDSL._get_dsl().envar.arch
if arch not in self.admissible_archs:
raise OpError(
self,
f"expects arch to be one of {self.admissible_archs}, but got {arch}",
suggestion="Ensure env CUTE_DSL_ARCH matches your GPU architecture",
)
if (self.cta_group == CtaGroup.TWO) and arch[:5] == "sm_90":
raise OpError(
self,
f"CTA group of 2 is tcgen05-specific and is not and is not compatible with {arch}",
suggestion="Ensure env CUTE_DSL_ARCH matches your GPU architecture",
)
def __str__(self) -> str:
res = "cp.async GMEM -> SMEM bulk tensor multicast copy Operation"
if self.cta_group == CtaGroup.TWO:
res += f"\n CTA group = 2"
return res
def _make_trait(
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
) -> "CopyBulkTensorTileG2SMulticastNonExecTrait":
raise NotImplementedError(
"Use cpasync.make_tma_tile_atom to obtain a copy Atom for TMA"
)
def _to_ir(self) -> _cute_nvgpu_ir.TiledTmaLoadEnum:
if self.cta_group == CtaGroup.ONE:
return _cute_nvgpu_ir.TiledTmaLoadEnum.sm_90_multicast
elif self.cta_group == CtaGroup.TWO:
return _cute_nvgpu_ir.TiledTmaLoadEnum.sm_100_2sm_multicast
else:
assert False, "unrecognized self.cta_group"
class CopyBulkTensorTileG2SMulticastNonExecTrait(Trait):
def unpack(
self,
*,
loc=None,
ip=None,
tma_bar_ptr: Optional[Pointer] = None,
mcast_mask=None,
tma_desc_ptr=None,
):
"""
Custom implementation of unpack for non-executable TMAs.
The multicast TMA load requires a `tma_bar_ptr` and a `mcast_mask` keyword arguments to be
provided when using `cute.copy`.
"""
if not isinstance(tma_bar_ptr, Pointer):
raise ValueError(
"expects a pointer to an mbarrier to be provided via the tma_bar_ptr kw argument"
)
if not isinstance(mcast_mask, Integer):
raise ValueError(
"expects a multicast mask to be provided via the mcast_mask kw argument"
)
exec_value = _cute_nvgpu_ir.atom_make_exec_tma(self.value, loc=loc, ip=ip)
attr_str = f"#cute_nvgpu.atom_copy_field_tmaload<tma_bar>"
attr = ir.Attribute.parse(attr_str)
exec_value = _cute_nvgpu_ir.atom_set_value(
exec_value, attr, tma_bar_ptr.value, loc=loc, ip=ip
)
attr_str = f"#cute_nvgpu.atom_copy_field_tmaload<mcast_mask>"
attr = ir.Attribute.parse(attr_str)
exec_value = _cute_nvgpu_ir.atom_set_value(
exec_value, attr, Int16(mcast_mask).ir_value(loc=loc, ip=ip), loc=loc, ip=ip
)
if isinstance(tma_desc_ptr, Pointer):
attr_str = f"#cute_nvgpu.atom_copy_field_tmaload<{TMA_DESC_PTR_FIELD_NAME}>"
attr = ir.Attribute.parse(attr_str)
exec_value = _cute_nvgpu_ir.atom_set_value(
exec_value, attr, tma_desc_ptr.value, loc=loc, ip=ip
)
return exec_value
#
# TMA SMEM -> GMEM copies
#
@dataclass(frozen=True)
class CopyBulkTensorTileS2GOp(CopyOp):
"""
Bulk tensor asynchrnous SMEM to GMEM Copy Operation using the TMA unit.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#data-movement-and-conversion-instructions-cp-async-bulk-tensor>`__.
This Operation uses TMA in the ``.tile`` mode.
"""
admissible_archs = ["sm_90", "sm_90a", "sm_100a"]
def __post_init__(self):
# Arch verification
arch = CuTeDSL._get_dsl().envar.arch
if arch not in self.admissible_archs:
raise OpError(
self,
f"expects arch to be one of {self.admissible_archs}, but got {arch}",
suggestion="Ensure env CUTE_DSL_ARCH matches your GPU architecture",
)
def __str__(self) -> str:
return "cp.async SMEM -> GMEM bulk tensor copy Operation"
def _make_trait(
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
) -> "CopyBulkTensorTileS2GTrait":
raise NotImplementedError(
"Use cpasync.make_tma_tile_atom to obtain a copy Atom for TMA"
)
class CopyBulkTensorTileS2GTrait(Trait):
def unpack(self, *, loc=None, ip=None, tma_desc_ptr: Optional[Pointer] = None):
"""
Custom implementation of unpack for non-executable TMAs.
"""
exec_value = _cute_nvgpu_ir.atom_make_exec_tma(self.value, loc=loc, ip=ip)
if isinstance(tma_desc_ptr, Pointer):
attr_str = (
f"#cute_nvgpu.atom_copy_field_tmastore<{TMA_DESC_PTR_FIELD_NAME}>"
)
attr = ir.Attribute.parse(attr_str)
exec_value = _cute_nvgpu_ir.atom_set_value(
exec_value, attr, tma_desc_ptr.value, loc=loc, ip=ip
)
return exec_value
@@ -0,0 +1,327 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from typing import Optional, Tuple, Type, Union
from cutlass.cutlass_dsl import dsl_user_op
import cutlass._mlir.dialects.cute_nvgpu as _cute_nvgpu_ir
from cutlass._mlir.dialects import llvm
from ...typing import Coord, Layout, Tensor, Tiler, Pointer, Int16, Numeric, NumericMeta
from ... import core
from .copy import (
CopyBulkTensorTileG2SOp,
CopyBulkTensorTileG2SMulticastOp,
CopyBulkTensorTileS2GOp,
CopyBulkTensorTileG2SNonExecTrait,
CopyBulkTensorTileG2SMulticastNonExecTrait,
CopyBulkTensorTileS2GTrait,
)
@dsl_user_op
def make_tma_tile_atom(
op: Union[
CopyBulkTensorTileG2SOp,
CopyBulkTensorTileG2SMulticastOp,
CopyBulkTensorTileS2GOp,
],
gmem_tensor: Tensor,
smem_layout: Layout,
cta_tiler: Tiler,
num_multicast: int = 1,
*,
internal_type: Optional[Type[Numeric]] = None,
loc=None,
ip=None,
) -> Tuple[core.CopyAtom, Tensor]:
"""
Makes a TMA Copy Atom in the ``.tile`` mode to copy tiles of a GMEM tensor to/from and SMEM
buffer with the given Layout.
Given
- a GMEM tensor
- a SMEM layout
- a CTA-level Tiler
this function figures out the bulk tensor asynchronous copy instruction to use with the maximum
"TMA vector length" to copy tiles of the GMEM tensor to/from an SMEM buffer with the provided
layout and consistent with the provided Tiler.
This function returns two results:
1. the Copy Atom
2. the so-called TMA tensor used to map logical coordinates of the GMEM tensor to coordinates \
that the TMA unit can consume. TMA tensors have so-called basis stride elements so that the \
associated layout can output coordinates. Otherwise, TMA tensors can be partitioned \
similarly to any other CuTe tensors using the algebra.
:param op: The Copy Operation to construct an Atom for
:type op: Union[CopyBulkTensorTileG2SOp, CopyBulkTensorTileG2SMulticastOp, CopyBulkTensorTileS2GOp]
:param gmem_tensor: The GMEM tensor involved in the Copy
:type gmem_tensor: Tensor
:param smem_layout: The SMEM layout to construct the Copy Atom for
:type smem_layout: Layout
:param cta_tiler: The CTA Tiler to use
:type cta_tiler: Tiler
:param num_multicast: The multicast factor
:type num_multicast: int
:param internal_type: An optional parameter for the internal data type to use when the actual data type is not supported by the TMA unit
:type internal_type: Type[Numeric]
:return: A Copy Atom for this Operation and the associated TMA tensor
:rtype: Tuple[core.CopyAtom, Tensor]
"""
if internal_type is not None:
if not isinstance(internal_type, NumericMeta):
raise TypeError(f"internal_type must be a Numeric, but got {internal_type}")
internal_type = internal_type.mlir_type
cta_v_map = core.composition(
core.make_identity_layout(gmem_tensor.shape, loc=loc, ip=ip),
cta_tiler,
loc=loc,
ip=ip,
)
if isinstance(op, CopyBulkTensorTileG2SOp):
if num_multicast != 1:
raise ValueError(
f"expects num_multicast to be 1 for non multicast G2S copies, "
f"but got {num_multicast}"
)
res = _cute_nvgpu_ir.atom_make_non_exec_tiled_tma_load(
gmem_tensor.value,
smem_layout,
cta_v_map,
op._to_ir(),
num_multicast=num_multicast,
internal_type=internal_type,
loc=loc,
ip=ip,
)
return core.CopyAtom(op, CopyBulkTensorTileG2SNonExecTrait(res[0])), res[1]
elif isinstance(op, CopyBulkTensorTileG2SMulticastOp):
if num_multicast < 1:
raise ValueError(
f"expects num_multicast to be >= 1 for multicast G2S copies, "
f"but got {num_multicast}"
)
res = _cute_nvgpu_ir.atom_make_non_exec_tiled_tma_load(
gmem_tensor.value,
smem_layout,
cta_v_map,
op._to_ir(),
num_multicast=num_multicast,
internal_type=internal_type,
loc=loc,
ip=ip,
)
return (
core.CopyAtom(op, CopyBulkTensorTileG2SMulticastNonExecTrait(res[0])),
res[1],
)
elif isinstance(op, CopyBulkTensorTileS2GOp):
res = _cute_nvgpu_ir.atom_make_non_exec_tiled_tma_store(
gmem_tensor.value,
smem_layout,
cta_v_map,
internal_type=internal_type,
loc=loc,
ip=ip,
)
return core.CopyAtom(op, CopyBulkTensorTileS2GTrait(res[0])), res[1]
else:
raise ValueError(f"expects a bulk tensor (TMA) Copy Op, but got {op}")
@dsl_user_op
def tma_partition(
atom: core.CopyAtom,
cta_coord: Coord,
cta_layout: Layout,
smem_tensor: Tensor,
gmem_tensor: Tensor,
*,
loc=None,
ip=None,
) -> Tuple[Tensor, Tensor]:
"""
Tiles the GMEM and SMEM tensors for the provided TMA Copy Atom.
"""
cta_coord_val = core._pack_coord(cta_coord, loc=loc, ip=ip)
s, d = _cute_nvgpu_ir.atom_tma_partition(
atom._trait.value,
cta_coord=cta_coord_val,
cta_layout=cta_layout,
smem_tensor=smem_tensor.value,
gmem_tensor=gmem_tensor.value,
loc=loc,
ip=ip,
)
return s, d
@dsl_user_op
def create_tma_multicast_mask(
cta_layout_vmnk: Layout,
cta_coord_vmnk: Coord,
mcast_mode: int,
*,
loc=None,
ip=None,
) -> Int16:
"""
Computes a multicast mask for a TMA load Copy.
:param cta_layout_vmnk: The VMNK layout of the cluster
:type cta_layout_vmnk: Layout
:param cta_coord_vmnk: The VMNK coordinate of the current CTA
:type cta_coord_vmnk: Coord
:param mcast_mode: The tensor mode in which to multicast
:type mcast_mode: int
:return: The resulting mask
:rtype: Int16
"""
if core.rank(cta_layout_vmnk) != 4:
raise ValueError(
f"cta_layout_vmnk must be rank 4, but got {core.pretty_str(cta_layout_vmnk)}"
)
if core.rank(cta_coord_vmnk) != 4:
raise ValueError(
f"cta_coord_vmnk must be rank 4, but got {core.pretty_str(cta_coord_vmnk)}"
)
return core.make_layout_image_mask(
cta_layout_vmnk, cta_coord_vmnk, mcast_mode, loc=loc, ip=ip
)
@dsl_user_op
def prefetch_descriptor(tma_atom: core.CopyAtom, *, loc=None, ip=None) -> None:
"""
Prefetches the TMA descriptor associated with the TMA Atom.
"""
_cute_nvgpu_ir.prefetch_tma_desc(tma_atom._trait.value, loc=loc, ip=ip)
@dsl_user_op
def copy_tensormap(
tma_atom: core.CopyAtom, tensormap_ptr: Pointer, *, loc=None, ip=None
) -> None:
"""
Copies the tensormap held by a TMA Copy Atom to the memory location pointed to by the provided
pointer.
:param tma_atom: The TMA Copy Atom
:type tma_atom: CopyAtom
:param tensormap_ptr: The pointer to the memory location to copy the tensormap to
:type tensormap_ptr: Pointer
"""
_cute_nvgpu_ir.copy_tma_desc(
tma_atom._trait.value, tensormap_ptr.value, loc=loc, ip=ip
)
@dsl_user_op
def update_tma_descriptor(
tma_atom: core.CopyAtom,
gmem_tensor: Tensor,
tma_desc_ptr: Pointer,
*,
loc=None,
ip=None,
) -> None:
"""
Updates the TMA descriptor in the memory location pointed to by the provided pointer using
information from a TMA Copy Atom and the provided GMEM tensor.
Specifically, the following fields of the TMA descriptor will be updated:
1. the GMEM tensor base address
2. the GMEM tensor shape
3. the GMEM tensor stride
Other fields of the TMA descriptor are left unchanged.
:param tma_atom: The TMA Copy Atom
:type tma_atom: CopyAtom
:param gmem_tensor: The GMEM tensor
:type gmem_tensor: Tensor
:param tensormap_ptr: The pointer to the memory location of the descriptor to udpate
:type tensormap_ptr: Pointer
"""
_cute_nvgpu_ir.update_tma_desc(
tma_atom._trait.value, gmem_tensor.value, tma_desc_ptr.value, loc=loc, ip=ip
)
@dsl_user_op
def fence_tma_desc_acquire(
tma_desc_ptr: Pointer,
*,
loc=None,
ip=None,
) -> None:
"""
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#parallel-synchronization-and-communication-instructions-membar>`__.
"""
tma_desc_ptr_i64 = tma_desc_ptr.toint(loc=loc, ip=ip).ir_value()
llvm.inline_asm(
None,
[tma_desc_ptr_i64],
"fence.proxy.tensormap::generic.acquire.gpu [$0], 128;",
"l",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
@dsl_user_op
def cp_fence_tma_desc_release(
tma_desc_global_ptr: Pointer,
tma_desc_shared_ptr: Pointer,
*,
loc=None,
ip=None,
) -> None:
"""
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#parallel-synchronization-and-communication-instructions-tensormap-cp-fenceproxy>`__.
"""
tma_desc_global_ptr_i64 = tma_desc_global_ptr.toint(loc=loc, ip=ip).ir_value()
tma_desc_shared_ptr_i32 = tma_desc_shared_ptr.toint(loc=loc, ip=ip).ir_value()
llvm.inline_asm(
None,
[tma_desc_global_ptr_i64, tma_desc_shared_ptr_i32],
"tensormap.cp_fenceproxy.global.shared::cta.tensormap::generic.release.gpu.sync.aligned [$0], [$1], 128;",
"l,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
@dsl_user_op
def fence_tma_desc_release(*, loc=None, ip=None) -> None:
"""
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#parallel-synchronization-and-communication-instructions-membar>`__.
"""
llvm.inline_asm(
None,
[],
"fence.proxy.tensormap::generic.release.gpu;",
"",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
@@ -0,0 +1,159 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from typing import Optional, Tuple, Type, Union
from cutlass.cutlass_dsl import dsl_user_op
import cutlass._mlir.dialects.cute_nvgpu as _cute_nvgpu_ir
from .. import core
from ..typing import Shape, Layout, Tensor, Numeric, NumericMeta
from ...impl_utils import check_type_in
from .cpasync.copy import (
CopyBulkTensorTileG2SOp,
CopyBulkTensorTileG2SNonExecTrait,
CopyBulkTensorTileG2SMulticastOp,
CopyBulkTensorTileG2SMulticastNonExecTrait,
)
####################################################################################################
#
# TMA creation helpers for tcgen05 MMAs
#
####################################################################################################
@dsl_user_op
def make_tma_tile_atom_A(
op: Union[CopyBulkTensorTileG2SOp, CopyBulkTensorTileG2SMulticastOp],
gmem_tensor: Tensor,
smem_layout: Layout,
mma_tiler_mnk: Shape,
tiled_mma: core.TiledMma,
cluster_shape_vmnk: Shape,
*,
internal_type: Optional[Type[Numeric]] = None,
loc=None,
ip=None,
) -> Tuple[core.CopyAtom, Tensor]:
if internal_type is not None:
if not isinstance(internal_type, NumericMeta):
raise TypeError(f"internal_type must be a Numeric, but got {internal_type}")
internal_type = internal_type.mlir_type
check_type_in(
op,
[CopyBulkTensorTileG2SOp, CopyBulkTensorTileG2SMulticastOp],
"op",
"make_tma_tile_atom_A",
)
ident = core.make_identity_layout(gmem_tensor.shape, loc=loc, ip=ip)
mma_tiler_mk = (mma_tiler_mnk[0], *mma_tiler_mnk[2:])
g_tile = core.composition(ident, mma_tiler_mk, loc=loc, ip=ip)
cta_v_map = tiled_mma._thrfrg_A(g_tile)
cta_v_map = core.get(cta_v_map, mode=[1])
cta_v_map = core.dice(cta_v_map, (1, (1,) * core.rank(g_tile)))
if isinstance(op, CopyBulkTensorTileG2SOp):
num_multicast = 1
else:
assert isinstance(op, CopyBulkTensorTileG2SMulticastOp)
# multicast across the N-mode since those would share the same tile of A
num_multicast = core.size(cluster_shape_vmnk, mode=[2])
# res[0] = the IR Value for the non-executable atom instance
# res[1] = the IR Value for the associated TMA tensor
res = _cute_nvgpu_ir.atom_make_non_exec_tiled_tma_load(
gmem_tensor.value,
smem_layout,
cta_v_map,
op._to_ir(),
num_multicast=num_multicast,
internal_type=internal_type,
loc=loc,
ip=ip,
)
if isinstance(op, CopyBulkTensorTileG2SOp):
return core.CopyAtom(op, CopyBulkTensorTileG2SNonExecTrait(res[0])), res[1]
else:
assert isinstance(op, CopyBulkTensorTileG2SMulticastOp)
return (
core.CopyAtom(op, CopyBulkTensorTileG2SMulticastNonExecTrait(res[0])),
res[1],
)
@dsl_user_op
def make_tma_tile_atom_B(
op: Union[CopyBulkTensorTileG2SOp, CopyBulkTensorTileG2SMulticastOp],
gmem_tensor: Tensor,
smem_layout: Layout,
mma_tiler_mnk: Shape,
tiled_mma: core.TiledMma,
cluster_shape_vmnk: Shape,
*,
internal_type: Optional[Type[Numeric]] = None,
loc=None,
ip=None,
) -> Tuple[core.CopyAtom, Tensor]:
if internal_type is not None:
if not isinstance(internal_type, NumericMeta):
raise TypeError(f"internal_type must be a Numeric, but got {internal_type}")
internal_type = internal_type.mlir_type
check_type_in(
op,
[CopyBulkTensorTileG2SOp, CopyBulkTensorTileG2SMulticastOp],
"op",
"make_tma_tile_atom_B",
)
ident = core.make_identity_layout(gmem_tensor.shape, loc=loc, ip=ip)
mma_tiler_nk = (mma_tiler_mnk[1], *mma_tiler_mnk[2:])
g_tile = core.composition(ident, mma_tiler_nk, loc=loc, ip=ip)
cta_v_map = tiled_mma._thrfrg_B(g_tile)
cta_v_map = core.get(cta_v_map, mode=[1])
cta_v_map = core.dice(cta_v_map, (1, (1,) * core.rank(g_tile)))
if isinstance(op, CopyBulkTensorTileG2SOp):
num_multicast = 1
else:
assert isinstance(op, CopyBulkTensorTileG2SMulticastOp)
# multicast across the M-mode since those would share the same tile of B
num_multicast = core.size(cluster_shape_vmnk, mode=[1])
# res[0] = the IR Value for the non-executable atom instance
# res[1] = the IR Value for the associated TMA tensor
res = _cute_nvgpu_ir.atom_make_non_exec_tiled_tma_load(
gmem_tensor.value,
smem_layout,
cta_v_map,
op._to_ir(),
num_multicast=num_multicast,
internal_type=internal_type,
loc=loc,
ip=ip,
)
if isinstance(op, CopyBulkTensorTileG2SOp):
return core.CopyAtom(op, CopyBulkTensorTileG2SNonExecTrait(res[0])), res[1]
else:
assert isinstance(op, CopyBulkTensorTileG2SMulticastOp)
return (
core.CopyAtom(op, CopyBulkTensorTileG2SMulticastNonExecTrait(res[0])),
res[1],
)
__all__ = [
"make_tma_tile_atom_A",
"make_tma_tile_atom_B",
]
@@ -0,0 +1,57 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from .copy import *
from .mma import *
from .helpers import *
# __all__ is required here for documentation generation
__all__ = [
#
# copy.py
#
"Repetition",
"Pack",
"Unpack",
"Ld16x64bOp",
"Ld16x128bOp",
"Ld16x256bOp",
"Ld16x32bx2Op",
"Ld32x32bOp",
"St16x64bOp",
"St16x128bOp",
"St16x256bOp",
"St16x32bx2Op",
"St32x32bOp",
#
# mma.py
#
"OperandMajorMode",
"OperandSource",
"CtaGroup",
"Field",
"MmaTF32Op",
"MmaF16BF16Op",
"MmaI8Op",
"MmaFP8Op",
"SmemLayoutAtomKind",
#
# helpers.py
#
"make_smem_layout_atom",
"tile_to_mma_shape",
"commit",
"is_tmem_load",
"is_tmem_store",
"get_tmem_copy_properties",
"find_tmem_tensor_col_offset",
"make_tmem_copy",
]
@@ -0,0 +1,465 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
import enum
from dataclasses import dataclass
from typing import Type
from cutlass.cutlass_dsl import CuTeDSL
import cutlass._mlir.dialects.cute as _cute_ir
import cutlass._mlir.dialects.cute_nvgpu as _cute_nvgpu_ir
from cutlass._mlir import ir
from ..common import OpError
from ...core import CopyOp, Trait
from ...typing import Numeric
class Repetition(enum.Enum):
"""
An enumeration for the number of repetitions of a given TMEM copy within the instruction.
"""
x1 = 1
x2 = 2
x4 = 4
x8 = 8
x16 = 16
x32 = 32
x64 = 64
x128 = 128
def __str__(self) -> str:
return f"{self.__class__.__name__}.{self.name}"
def __repr__(self) -> str:
return f"<{self.__class__.__name__}.{self.name}>"
@classmethod
def _missing_(cls, value):
if isinstance(value, int):
if value == 1:
return Repetition.x1
elif value == 2:
return Repetition.x2
elif value == 8:
return Repetition.x8
elif value == 16:
return Repetition.x16
elif value == 32:
return Repetition.x32
elif value == 64:
return Repetition.x64
elif value == 128:
return Repetition.x128
class Pack(enum.Enum):
"""
An enumeration for the possible packing patterns for TMEM to RMEM copies.
"""
NONE = enum.auto()
PACK_16b_IN_32b = enum.auto()
def __str__(self) -> str:
return f"{self.__class__.__name__}.{self.name}"
def __repr__(self) -> str:
return f"<{self.__class__.__name__}.{self.name}>"
class Unpack(enum.Enum):
"""
An enumeration for the possible unpacking patterns for RMEM to TMEM copies.
"""
NONE = enum.auto()
UNPACK_32b_IN_16b = enum.auto()
def __str__(self) -> str:
return f"{self.__class__.__name__}.{self.name}"
def __repr__(self) -> str:
return f"<{self.__class__.__name__}.{self.name}>"
@dataclass(frozen=True)
class _LdBase(CopyOp):
repeat: Repetition = Repetition.x1
pack: Pack = Pack.NONE
admissible_archs = ["sm_100a"]
def __post_init__(self) -> None:
# Arch verification
arch = CuTeDSL._get_dsl().envar.arch
if arch not in self.admissible_archs:
raise OpError(
self,
f"expects arch to be one of {self.admissible_archs}, but got {arch}",
suggestion="Ensure env CUTE_DSL_ARCH matches your GPU architecture",
)
if not isinstance(self.repeat, Repetition):
raise OpError(
self,
"expects the 'repeat' Op parameter to be a tcgen05.Repetition instance",
)
if not isinstance(self.pack, Pack):
raise OpError(
self,
"expects the 'pack' Op parameter to be a tcgen05.Pack instance",
)
def __str__(self) -> str:
res = (
f"tcgen05 {self.__class__.__name__[:-2]} Copy Operation"
+ f"\n number of repetitions = {self.repeat.value}"
)
if self.pack == Pack.PACK_16b_IN_32b:
res += f"\n with 2x 16-bit to 32b packing"
return res
@dataclass(frozen=True)
class Ld16x64bOp(_LdBase):
"""
16x64b TMEM load Operation.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#tcgen05-instructions-tcgen05-ld>`__.
This Operation corresponds to the ``.16x64b`` qualifier.
"""
def _make_trait(
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
) -> "Ld16x64bTrait":
ty = _cute_nvgpu_ir.CopyAtomSM100TmemLoadType.get(
copy_internal_type.mlir_type,
16,
64,
self.repeat.value,
ir.UnitAttr.get() if self.pack == Pack.PACK_16b_IN_32b else None,
)
return Ld16x64bTrait(_cute_ir.atom(ty, loc=loc, ip=ip))
class Ld16x64bTrait(Trait):
pass
@dataclass(frozen=True)
class Ld16x128bOp(_LdBase):
"""
16x128b TMEM load Operation.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#tcgen05-instructions-tcgen05-ld>`__.
This Operation corresponds to the ``.16x128b`` qualifier.
"""
def __post_init__(self) -> None:
super().__post_init__()
if self.repeat == Repetition.x128:
raise OpError(
self,
"x128 repetition is not supported",
suggestion="choose one of x1, x2, x4, x8, x16, x32, x64",
)
def _make_trait(
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
) -> "Ld16x128bTrait":
ty = _cute_nvgpu_ir.CopyAtomSM100TmemLoadType.get(
copy_internal_type.mlir_type,
16,
128,
self.repeat.value,
ir.UnitAttr.get() if self.pack == Pack.PACK_16b_IN_32b else None,
)
return Ld16x128bTrait(_cute_ir.atom(ty, loc=loc, ip=ip))
class Ld16x128bTrait(Trait):
pass
@dataclass(frozen=True)
class Ld16x256bOp(_LdBase):
"""
16x256b TMEM load Operation.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#tcgen05-instructions-tcgen05-ld>`__.
This Operation corresponds to the ``.16x256b`` qualifier.
"""
def __post_init__(self) -> None:
super().__post_init__()
if self.repeat in (Repetition.x128, Repetition.x64):
raise OpError(
self,
"x64 and x128 repetition is not supported",
suggestion="choose one of x1, x2, x4, x8, x16, x32",
)
def _make_trait(
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
) -> "Ld16x256bTrait":
ty = _cute_nvgpu_ir.CopyAtomSM100TmemLoadType.get(
copy_internal_type.mlir_type,
16,
256,
self.repeat.value,
ir.UnitAttr.get() if self.pack == Pack.PACK_16b_IN_32b else None,
)
return Ld16x256bTrait(_cute_ir.atom(ty, loc=loc, ip=ip))
class Ld16x256bTrait(Trait):
pass
@dataclass(frozen=True)
class Ld16x32bx2Op(_LdBase):
"""
16x32bx2 TMEM load Operation.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#tcgen05-instructions-tcgen05-ld>`__.
This Operation corresponds to the ``.16x32bx2`` qualifier.
"""
def _make_trait(
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
) -> "Ld16x32bx2Trait":
ty = _cute_nvgpu_ir.CopyAtomSM100TmemLoadType.get(
copy_internal_type.mlir_type,
16,
32,
self.repeat.value,
ir.UnitAttr.get() if self.pack == Pack.PACK_16b_IN_32b else None,
)
return Ld16x32bx2Trait(_cute_ir.atom(ty, loc=loc, ip=ip))
class Ld16x32bx2Trait(Trait):
pass
@dataclass(frozen=True)
class Ld32x32bOp(_LdBase):
"""
32x32b TMEM load Operation.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#tcgen05-instructions-tcgen05-ld>`__.
This Operation corresponds to the ``.32x32`` qualifier.
"""
def _make_trait(
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
) -> "Ld32x32bTrait":
ty = _cute_nvgpu_ir.CopyAtomSM100TmemLoadType.get(
copy_internal_type.mlir_type,
32,
32,
self.repeat.value,
ir.UnitAttr.get() if self.pack == Pack.PACK_16b_IN_32b else None,
)
return Ld32x32bTrait(_cute_ir.atom(ty, loc=loc, ip=ip))
class Ld32x32bTrait(Trait):
pass
@dataclass(frozen=True)
class _StBase(CopyOp):
repeat: Repetition
unpack: Unpack = Unpack.NONE
admissible_archs = ["sm_100a"]
def __post_init__(self) -> None:
# Arch verification
arch = CuTeDSL._get_dsl().envar.arch
if arch not in self.admissible_archs:
raise OpError(
self,
f"expects arch to be one of {self.admissible_archs}, but got {arch}",
suggestion="Ensure env CUTE_DSL_ARCH matches your GPU architecture",
)
if not isinstance(self.repeat, Repetition):
raise OpError(
self,
"expects the 'repeat' Op parameter to be a tcgen05.Repetition instance",
)
if not isinstance(self.unpack, Unpack):
raise OpError(
self,
"expects the 'pack' Op parameter to be a tcgen05.Unpack instance",
)
def __str__(self) -> str:
res = (
f"tcgen05 {self.__class__.__name__[:-2]} Copy Operation"
+ f"\n number of repetitions = {self.repeat.value}"
)
if self.unpack == Unpack.UNPACK_32b_IN_16b:
res += f"\n with 32-bit to 2x 16b unpacking"
return res
@dataclass(frozen=True)
class St16x64bOp(_StBase):
"""
16x64b TMEM store Operation.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#tcgen05-instructions-tcgen05-st>`__.
This Operation corresponds to the ``.16x64`` qualifier.
"""
def _make_trait(
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
) -> "St16x64bTrait":
ty = _cute_nvgpu_ir.CopyAtomSM100TmemStoreType.get(
copy_internal_type.mlir_type,
16,
64,
self.repeat.value,
ir.UnitAttr.get() if self.unpack == Unpack.UNPACK_32b_IN_16b else None,
)
return St16x64bTrait(_cute_ir.atom(ty, loc=loc, ip=ip))
class St16x64bTrait(Trait):
pass
@dataclass(frozen=True)
class St16x128bOp(_StBase):
"""
16x128b TMEM store Operation.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#tcgen05-instructions-tcgen05-st>`__.
This Operation corresponds to the ``.16x128`` qualifier.
"""
def __post_init__(self) -> None:
super().__post_init__()
if self.repeat == Repetition.x128:
raise OpError(
self,
"x128 repetition is not supported",
suggestion="choose one of x1, x2, x4, x8, x16, x32, x64",
)
def _make_trait(
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
) -> "St16x128bTrait":
ty = _cute_nvgpu_ir.CopyAtomSM100TmemStoreType.get(
copy_internal_type.mlir_type,
16,
128,
self.repeat.value,
ir.UnitAttr.get() if self.unpack == Unpack.UNPACK_32b_IN_16b else None,
)
return St16x128bTrait(_cute_ir.atom(ty, loc=loc, ip=ip))
class St16x128bTrait(Trait):
pass
@dataclass(frozen=True)
class St16x256bOp(_StBase):
"""
16x256b TMEM store Operation.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#tcgen05-instructions-tcgen05-st>`__.
This Operation corresponds to the ``.16x256`` qualifier.
"""
def __post_init__(self) -> None:
super().__post_init__()
if self.repeat in (Repetition.x128, Repetition.x64):
raise OpError(
self,
"x64 and x128 repetition is not supported",
suggestion="choose one of x1, x2, x4, x8, x16, x32",
)
def _make_trait(
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
) -> "St16x256bTrait":
ty = _cute_nvgpu_ir.CopyAtomSM100TmemStoreType.get(
copy_internal_type.mlir_type,
16,
256,
self.repeat.value,
ir.UnitAttr.get() if self.unpack == Unpack.UNPACK_32b_IN_16b else None,
)
return St16x256bTrait(_cute_ir.atom(ty, loc=loc, ip=ip))
class St16x256bTrait(Trait):
pass
@dataclass(frozen=True)
class St16x32bx2Op(_StBase):
"""
16x32x2b TMEM store Operation.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#tcgen05-instructions-tcgen05-st>`__.
This Operation corresponds to the ``.16x32x2`` qualifier.
"""
def _make_trait(
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
) -> "St16x32bx2Trait":
ty = _cute_nvgpu_ir.CopyAtomSM100TmemStoreType.get(
copy_internal_type.mlir_type,
16,
32,
self.repeat.value,
ir.UnitAttr.get() if self.unpack == Unpack.UNPACK_32b_IN_16b else None,
)
return St16x32bx2Trait(_cute_ir.atom(ty, loc=loc, ip=ip))
class St16x32bx2Trait(Trait):
pass
@dataclass(frozen=True)
class St32x32bOp(_StBase):
"""
32x32b TMEM store Operation.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#tcgen05-instructions-tcgen05-st>`__.
This Operation corresponds to the ``.32x32`` qualifier.
"""
def _make_trait(
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
) -> "St32x32bTrait":
ty = _cute_nvgpu_ir.CopyAtomSM100TmemStoreType.get(
copy_internal_type.mlir_type,
32,
32,
self.repeat.value,
ir.UnitAttr.get() if self.unpack == Unpack.UNPACK_32b_IN_16b else None,
)
return St32x32bTrait(_cute_ir.atom(ty, loc=loc, ip=ip))
class St32x32bTrait(Trait):
pass
@@ -0,0 +1,301 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from typing import overload, Type, Tuple, Union
from cutlass.cutlass_dsl import dsl_user_op
import cutlass._mlir.dialects.cute as _cute_ir
import cutlass._mlir.dialects.cute_nvgpu as _cute_nvgpu_ir
from cutlass._mlir.dialects import nvvm
from ...typing import (
Shape,
IntTuple,
Layout,
Tensor,
Int,
Numeric,
NumericMeta,
Int16,
Int32,
)
from ... import core
from .mma import SmemLayoutAtomKind, CtaGroup
from .copy import (
Pack,
Unpack,
Ld16x64bOp,
Ld16x128bOp,
Ld16x256bOp,
Ld16x32bx2Op,
Ld32x32bOp,
St16x64bOp,
St16x128bOp,
St16x256bOp,
St16x32bx2Op,
St32x32bOp,
)
####################################################################################################
#
# Helper functions for MMA
#
####################################################################################################
@dsl_user_op
def make_smem_layout_atom(
kind: SmemLayoutAtomKind, element_type: Type[Numeric], *, loc=None, ip=None
) -> core.ComposedLayout:
"""
Makes a SMEM layout Atom.
This function creates a composed layout in unit of elements consistent with the requested layout
Atom kind and element data type.
:param kind: The kind of layout Atom
:type kind: SmemLayoutAtomKind
:param element_type: The element data type to construct the layout for
:type element_type: Type[Numeric]
:return: The SMEM layout atom
:rtype: core.ComposedLayout
"""
if not isinstance(element_type, NumericMeta):
raise TypeError(f"element_type must be a Numeric, but got {element_type}")
if kind in (SmemLayoutAtomKind.MN_INTER, SmemLayoutAtomKind.K_INTER):
num_contiguous_bits = 128
sw = core.make_swizzle(0, 4, 3)
elif kind in (SmemLayoutAtomKind.MN_SW32, SmemLayoutAtomKind.K_SW32):
num_contiguous_bits = 256
sw = core.make_swizzle(1, 4, 3)
elif kind in (SmemLayoutAtomKind.MN_SW64, SmemLayoutAtomKind.K_SW64):
num_contiguous_bits = 512
sw = core.make_swizzle(2, 4, 3)
elif kind in (SmemLayoutAtomKind.MN_SW128, SmemLayoutAtomKind.K_SW128):
num_contiguous_bits = 1024
sw = core.make_swizzle(3, 4, 3)
elif kind == SmemLayoutAtomKind.MN_SW128_32B:
num_contiguous_bits = 1024
sw = core.make_swizzle(2, 5, 2)
else:
raise ValueError("unrecognized SMEM layout atom kind")
num_contiguous_elems = num_contiguous_bits // element_type.width
if kind in (
SmemLayoutAtomKind.MN_INTER,
SmemLayoutAtomKind.MN_SW32,
SmemLayoutAtomKind.MN_SW64,
SmemLayoutAtomKind.MN_SW128,
SmemLayoutAtomKind.MN_SW128_32B,
):
# M/N-major layout
return core.make_composed_layout(
sw,
0,
core.make_layout(
(num_contiguous_elems, 8), stride=(1, num_contiguous_elems)
),
loc=loc,
ip=ip,
)
else:
# K-major layout
return core.make_composed_layout(
sw,
0,
core.make_layout(
(8, num_contiguous_elems), stride=(num_contiguous_elems, 1)
),
loc=loc,
ip=ip,
)
@overload
def tile_to_mma_shape(
atom: Layout, mma_tile_shape: Shape, order: IntTuple = None, *, loc=None, ip=None
) -> Layout: ...
@overload
def tile_to_mma_shape(
atom: core.ComposedLayout,
mma_tile_shape: Shape,
order: IntTuple = None,
*,
loc=None,
ip=None,
) -> core.ComposedLayout: ...
@dsl_user_op
def tile_to_mma_shape(
atom, mma_tile_shape: Shape, order: IntTuple = None, *, loc=None, ip=None
):
"""
Tiles a layout to an MMA shape.
"""
# Default order is colexicographical
if order is None:
order = tuple(range(core.rank(mma_tile_shape) - 1))
if core.rank(order) != core.rank(mma_tile_shape) - 1:
raise ValueError(
f"rank(order)={core.rank(order)} must be equal to "
f"rank(mma_tile_shape)-1={core.rank(mma_tile_shape)-1}"
)
order_val = core._pack_int_tuple(order, loc=loc, ip=ip)
mma_tile_shape_val = core._pack_shape(mma_tile_shape, loc=loc, ip=ip)
if not (
core.is_static(atom)
and core.is_static(mma_tile_shape_val)
and core.is_static(order_val)
):
raise ValueError("tile_to_mma_shape only supports static inputs")
res_ty = _cute_nvgpu_ir.tile_to_mma_shape(atom, mma_tile_shape_val, order_val)
return _cute_ir.static(res_ty, loc=loc, ip=ip)
@dsl_user_op
def commit(
mbar_ptr: core.Pointer,
mask=None,
cta_group: CtaGroup = CtaGroup.ONE,
*,
loc=None,
ip=None,
) -> None:
"""
Perform an arrive operation on a mbarrier upon completion of previous MMA operations.
:param mbar_ptr: A pointer to the mbarrier in SMEM
:type mbar_ptr: Pointer
:param mask: An optional multicast mask for the CTAs in the cluster to signal arrival to
:type mask: Int
"""
if cta_group == CtaGroup.ONE:
group = nvvm.Tcgen05GroupKind.CTA_1
else:
assert cta_group == CtaGroup.TWO
group = nvvm.Tcgen05GroupKind.CTA_2
mbar_ptr = mbar_ptr.llvm_ptr
if mask is not None:
mask = Int16(mask).ir_value(loc=loc, ip=ip)
nvvm.tcgen05_commit_arrive(
mbar_ptr, multicast_mask=mask, group=group, loc=loc, ip=ip
)
else:
nvvm.tcgen05_commit_arrive(mbar_ptr, group=group, loc=loc, ip=ip)
return
####################################################################################################
#
# Helper functions for Copies
#
####################################################################################################
def is_tmem_load(atom: core.CopyAtom) -> bool:
"""
Returns whether a CopyAtom instance is a TMEM load.
"""
return isinstance(
atom.op,
(
Ld16x64bOp,
Ld16x128bOp,
Ld16x256bOp,
Ld16x32bx2Op,
Ld32x32bOp,
),
)
def is_tmem_store(atom: core.CopyAtom) -> bool:
"""
Returns whether a CopyAtom instance is a TMEM store.
"""
return isinstance(
atom.op,
(
St16x64bOp,
St16x128bOp,
St16x256bOp,
St16x32bx2Op,
St32x32bOp,
),
)
def get_tmem_copy_properties(
atom: core.CopyAtom,
) -> Tuple[int, int, int, Union[Pack, Unpack]]:
"""
Returns the properties of a TMEM copy atom (number of data paths, bits, repetitions,
and whether packing/unpacking is used).
"""
if isinstance(atom.op, (Ld16x64bOp, St16x64bOp)):
num_dp, num_bits = 16, 64
elif isinstance(atom.op, (Ld16x128bOp, St16x128bOp)):
num_dp, num_bits = 16, 128
elif isinstance(atom.op, (Ld16x256bOp, St16x256bOp)):
num_dp, num_bits = 16, 256
elif isinstance(atom.op, (Ld16x32bx2Op, St16x32bx2Op)):
num_dp, num_bits = 16, 32
elif isinstance(atom.op, (Ld32x32bOp, St32x32bOp)):
num_dp, num_bits = 32, 32
else:
raise ValueError(f"expects 'atom' to be a TMEM copy, but got {atom}")
if is_tmem_load(atom):
return num_dp, num_bits, atom.op.repeat.value, atom.op.pack
else:
assert is_tmem_store(atom), "atom must be a TMEM store"
return num_dp, num_bits, atom.op.repeat.value, atom.op.unpack
@dsl_user_op
def find_tmem_tensor_col_offset(tmem_tensor: Tensor, *, loc=None, ip=None) -> Int:
"""
Computes the TMEM column offset given a TMEM tensor.
:param tmem_tensor: The TMEM tensor to use to compute the columns offset
:type tmem_tensor: Tensor
:return: The columns offset
:rtype: Int
"""
tmem_col_mask = 0x0000FFFF
offset = (
core.cosize(core.recast_tensor(tmem_tensor, Int32).layout, loc=loc, ip=ip)
& tmem_col_mask
)
if isinstance(offset, int):
return offset
return Int32(offset, loc=loc, ip=ip)
@dsl_user_op
def make_tmem_copy(
atom: core.CopyAtom, tmem_tensor: Tensor, *, loc=None, ip=None
) -> core.TiledCopy:
"""
Makes a Tiled Copy instance from a TMEM Copy Atom and a TMEM tensor.
"""
tiled_copy_val = _cute_nvgpu_ir.atom_make_tmem_copy(
atom._trait.value, tmem_tensor.value, loc=loc, ip=ip
)
new_trait = type(atom._trait)(tiled_copy_val)
return core.TiledCopy(atom.op, new_trait)
@@ -0,0 +1,603 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
import enum
from dataclasses import dataclass
from typing import Type
from cutlass.cutlass_dsl import CuTeDSL, T
import cutlass._mlir.dialects.cute as _cute_ir
import cutlass._mlir.dialects.cute_nvgpu as _cute_nvgpu_ir
from cutlass._mlir import ir
from ..common import OpError
from ...core import MmaOp, Trait, _pack_shape, rank, depth
from ...typing import (
Shape,
Float8E5M2,
Float8E4M3FN,
Float16,
BFloat16,
Float32,
TFloat32,
Boolean,
Int8,
Uint8,
Int32,
Numeric,
)
####################################################################################################
#
# MMA Ops and Traits
#
####################################################################################################
class OperandMajorMode(enum.Enum):
"""
An enumeration for the majorness of the input operands of the MMA.
"""
MN = _cute_ir.MajorMode.mn
K = _cute_ir.MajorMode.k
def __str__(self) -> str:
return f"{self.__class__.__name__}.{self.name}"
def __repr__(self) -> str:
return f"<{self.__class__.__name__}.{self.name}>"
@classmethod
def _missing_(cls, value):
if isinstance(value, str):
value = value.upper()
if value == "MN":
return OperandMajorMode.MN
elif value == "K":
return OperandMajorMode.K
def _to_ir(self) -> _cute_ir.MajorMode:
return self.value
class OperandSource(enum.Enum):
"""
An enumeration for the source memory location of the A input operand of the MMA.
"""
TMEM = _cute_ir.MmaFragKind.tmem
SMEM = _cute_ir.MmaFragKind.smem_desc
def __str__(self) -> str:
return f"{self.__class__.__name__}.{self.name}"
def __repr__(self) -> str:
return f"<{self.__class__.__name__}.{self.name}>"
def _to_ir(self) -> _cute_ir.MmaFragKind:
return self.value
class CtaGroup(enum.Enum):
"""
An enumeration for the ``cta_group`` qualifier of the MMA.
"""
ONE = 1
TWO = 2
def __str__(self) -> str:
return f"{self.__class__.__name__}.{self.name}"
def __repr__(self) -> str:
return f"<{self.__class__.__name__}.{self.name}>"
class Field(enum.Enum):
"""
An enumeration for the fields of the MMA Atom that can be modified at runtime.
"""
NEGATE_A = "neg_a"
NEGATE_B = "neg_b"
ACCUMULATE = "accum_c"
def __str__(self) -> str:
return f"{self.__class__.__name__}.{self.name}"
def __repr__(self) -> str:
return f"<{self.__class__.__name__}.{self.name}>"
def _to_ir_field_name(self) -> str:
return self.value
# Base class for all tcgen05 MMA Ops used to factor out some internal code
@dataclass(frozen=True)
class MmaOp(MmaOp):
a_dtype: Type[Numeric]
b_dtype: Type[Numeric]
acc_dtype: Type[Numeric]
shape_mnk: Shape
cta_group: CtaGroup
a_src: OperandSource
a_major_mode: OperandMajorMode
b_major_mode: OperandMajorMode
admissible_archs = ["sm_100a"]
def __post_init__(self) -> None:
# Verify arch
arch = CuTeDSL._get_dsl().envar.arch
if arch not in self.admissible_archs:
raise OpError(
self,
f"expects arch to be one of {self.admissible_archs}, but got {arch}",
suggestion="Ensure env CUTE_DSL_ARCH matches your GPU architecture",
)
# Verify that the user provided enum values
if not isinstance(self.cta_group, CtaGroup):
raise OpError(
self,
"expects the 'cta_group' Op parameter to be a tcgen05.CtaGroup instance",
)
if not isinstance(self.a_src, OperandSource):
raise OpError(
self,
"expects the 'a_src' Op parameter to be a tcgen05.OperandSource instance",
)
if not isinstance(self.a_major_mode, OperandMajorMode):
raise OpError(
self,
"expects the 'a_major_mode' Op parameter to be a tcgen05.OperandMajorMode instance",
)
if not isinstance(self.b_major_mode, OperandMajorMode):
raise OpError(
self,
"expects the 'b_major_mode' Op parameter to be a tcgen05.OperandMajorMode instance",
)
# Verify the instruction shape
if (rank(self.shape_mnk) not in [2, 3]) or (depth(self.shape_mnk) != 1):
raise OpError(
self,
f"expected a flat rank 2 or 3 tuple for the 'shape_mnk' Op parameter, "
f"but got {self.shape_mnk}",
)
m, n = self.shape_mnk[0], self.shape_mnk[1]
if self.cta_group == CtaGroup.ONE:
if m not in [64, 128]:
raise OpError(self, f"expects the M-mode to be 64 or 128, but got {m}")
if m == 64:
if (n < 8) or (n > 256) or (n % 8 != 0):
raise OpError(
self,
f"expects the N-mode to satisfy 8 <= N <= 256 and N % 8 == 0, but got {n}",
)
elif m == 128:
if (n < 16) or (n > 256) or (n % 16 != 0):
raise OpError(
self,
f"expects the N-mode to satisfy 8 <= N <= 256 and N % 16 == 0, but got {n}",
)
else:
if m not in [128, 256]:
raise OpError(self, f"expects the M-mode to be 128 or 256, but got {m}")
if (n < 32) or (n > 256) or (n % 32 != 0):
raise OpError(
self,
f"expects the N-mode to satisfy 32 <= N <= 256 and N % 32 == 0, but got {n}",
)
def __str__(self) -> str:
return (
self.__class__.descriptive_name # type: ignore
+ f"\n A data type = {self.a_dtype}"
+ f"\n B data type = {self.b_dtype}"
+ f"\n Accumulator data type = {self.acc_dtype}"
+ f"\n CTA group = {self.cta_group}"
+ f"\n A source location = {self.a_src}"
+ f"\n A major mode = {self.a_major_mode}"
+ f"\n B major mode = {self.b_major_mode}"
+ f"\n Instruction shape MNK = {self.shape_mnk}"
)
class MmaTrait(Trait):
admissible_fields = [Field.ACCUMULATE, Field.NEGATE_A, Field.NEGATE_B]
def set(self, field, value, *, loc=None, ip=None) -> None:
if field not in self.admissible_fields:
raise ValueError(
f"expects field to be one of {self.admissible_fields}, but got {field}"
)
field_name = f"#cute_nvgpu.atom_mma_field_sm100<{field._to_ir_field_name()}>"
attr = ir.Attribute.parse(field_name)
self.value = _cute_nvgpu_ir.atom_set_value(
self.value, attr, Boolean(value).ir_value(loc=loc, ip=ip), loc=loc, ip=ip
)
#
# TF32 MMA
#
@dataclass(frozen=True)
class MmaTF32Op(MmaOp):
"""
TF32 tcgen05 MMA Operation.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#tcgen05-mma-instructions-mma>`__.
This Operation corresponds to the ``.kind::tf32`` qualifier.
"""
descriptive_name = "tcgen05 TF32 MMA Operation"
def __init__(
self,
instruction_shape: Shape,
cta_group: CtaGroup,
a_src: OperandSource,
a_major_mode: OperandMajorMode,
b_major_mode: OperandMajorMode,
) -> None:
super().__init__(
TFloat32,
TFloat32,
Float32,
instruction_shape,
cta_group,
a_src,
a_major_mode,
b_major_mode,
)
self._verify()
def _verify(self) -> None:
# Verify the instruction shape
instruction_k = 8
if rank(self.shape_mnk) == 2:
object.__setattr__(self, "shape_mnk", (*self.shape_mnk, instruction_k))
if self.shape_mnk[2] != instruction_k:
raise OpError(
self,
f"expects the instruction extent in the K-mode to be {instruction_k}, "
f"but got {self.shape_mnk[2]}",
)
def _make_trait(self, *, loc=None, ip=None, **kwargs) -> "MmaTF32Trait":
shape_mnk = _pack_shape(self.shape_mnk, loc=loc, ip=ip)
ty = _cute_nvgpu_ir.MmaAtomSM100UMMAType.get(
shape_mnk.type.attribute,
self.cta_group.value,
self.a_major_mode._to_ir(),
self.b_major_mode._to_ir(),
self.a_dtype.mlir_type,
self.b_dtype.mlir_type,
self.acc_dtype.mlir_type,
self.a_src._to_ir(),
0,
)
return MmaTF32Trait(
_cute_nvgpu_ir.make_sm100_mma(
ty,
Boolean(False).ir_value(loc=loc, ip=ip),
Boolean(False).ir_value(loc=loc, ip=ip),
Boolean(False).ir_value(loc=loc, ip=ip),
loc=loc,
ip=ip,
)
)
class MmaTF32Trait(MmaTrait):
pass
#
# F16/BF16 MMA
#
@dataclass(frozen=True)
class MmaF16BF16Op(MmaOp):
"""
F16/BF16 tcgen05 MMA Operation.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#tcgen05-mma-instructions-mma>`__.
This Operation corresponds to the ``.kind::f16`` qualifier.
"""
descriptive_name = "tcgen05 F16/BF16 MMA Operation"
def __init__(
self,
ab_dtype: Type[Numeric],
acc_dtype: Type[Numeric],
instruction_shape: Shape,
cta_group: CtaGroup,
a_src: OperandSource,
a_major_mode: OperandMajorMode,
b_major_mode: OperandMajorMode,
) -> None:
super().__init__(
ab_dtype,
ab_dtype,
acc_dtype,
instruction_shape,
cta_group,
a_src,
a_major_mode,
b_major_mode,
)
self._verify()
def _verify(self) -> None:
# Input data type verification
if self.a_dtype not in [Float16, BFloat16]:
raise OpError(
self,
"expects the 'ab_dtype' Op parameter to be one of Float16 or BFloat16",
)
assert self.b_dtype == self.a_dtype, "a_dtype and b_dtype must be the same"
# Accumulator data type verification
if self.acc_dtype not in [Float16, Float32]:
raise OpError(
self,
"expects the 'acc_dtype' Op parameter to be one of Float16 or Float32",
)
# Instruction shape verification
instruction_k = 16
if rank(self.shape_mnk) == 2:
object.__setattr__(self, "shape_mnk", (*self.shape_mnk, instruction_k))
if self.shape_mnk[2] != instruction_k:
raise OpError(
self,
f"expects the instruction extent in the K-mode to be {instruction_k}, "
f"but got {self.shape_mnk[2]}",
)
def _make_trait(self, *, loc=None, ip=None, **kwargs) -> "MmaF16BF16Trait":
shape_mnk = _pack_shape(self.shape_mnk, loc=loc, ip=ip)
ty = _cute_nvgpu_ir.MmaAtomSM100UMMAType.get(
shape_mnk.type.attribute,
self.cta_group.value,
self.a_major_mode._to_ir(),
self.b_major_mode._to_ir(),
self.a_dtype.mlir_type,
self.b_dtype.mlir_type,
self.acc_dtype.mlir_type,
self.a_src._to_ir(),
0,
)
return MmaF16BF16Trait(
_cute_nvgpu_ir.make_sm100_mma(
ty,
Boolean(False).ir_value(loc=loc, ip=ip),
Boolean(False).ir_value(loc=loc, ip=ip),
Boolean(False).ir_value(loc=loc, ip=ip),
loc=loc,
ip=ip,
)
)
class MmaF16BF16Trait(MmaTrait):
pass
#
# I8 MMA
#
@dataclass(frozen=True)
class MmaI8Op(MmaOp):
"""
I8 tcgen05 MMA Operation.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#tcgen05-mma-instructions-mma>`__.
This Operation corresponds to the ``.kind::i8`` qualifier.
"""
descriptive_name = "tcgen05 I8 MMA Operation"
def __init__(
self,
ab_dtype: Type[Numeric],
instruction_shape: Shape,
cta_group: CtaGroup,
a_src: OperandSource,
a_major_mode: OperandMajorMode,
b_major_mode: OperandMajorMode,
) -> None:
super().__init__(
ab_dtype,
ab_dtype,
Int32,
instruction_shape,
cta_group,
a_src,
a_major_mode,
b_major_mode,
)
self._verify()
def _verify(self) -> None:
# Input data type verification
if self.a_dtype not in [Int8, Uint8]:
raise OpError(
self,
"expects the 'ab_dtype' Op parameter to be one of Int8 or Uint8",
)
assert self.b_dtype == self.a_dtype, "a_dtype and b_dtype must be the same"
# Instruction shape verification
instruction_k = 32
if rank(self.shape_mnk) == 2:
object.__setattr__(self, "shape_mnk", (*self.shape_mnk, instruction_k))
if self.shape_mnk[2] != instruction_k:
raise OpError(
self,
f"expects the instruction extent in the K-mode to be {instruction_k}, "
f"but got {self.shape_mnk[2]}",
)
def _make_trait(self, *, loc=None, ip=None, **kwargs) -> "MmaI8Trait":
shape_mnk = _pack_shape(self.shape_mnk, loc=loc, ip=ip)
ty = _cute_nvgpu_ir.MmaAtomSM100UMMAType.get(
shape_mnk.type.attribute,
self.cta_group.value,
self.a_major_mode._to_ir(),
self.b_major_mode._to_ir(),
(T.si8() if self.a_dtype.signed else T.ui8()),
(T.si8() if self.b_dtype.signed else T.ui8()),
T.si32(),
self.a_src._to_ir(),
0,
)
return MmaI8Trait(
_cute_nvgpu_ir.make_sm100_mma(
ty,
Boolean(False).ir_value(loc=loc, ip=ip),
Boolean(False).ir_value(loc=loc, ip=ip),
Boolean(False).ir_value(loc=loc, ip=ip),
loc=loc,
ip=ip,
)
)
class MmaI8Trait(MmaTrait):
pass
#
# F8F6F4 MMA
#
@dataclass(frozen=True)
class MmaFP8Op(MmaOp):
"""
F8 tcgen05 MMA Operation.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#tcgen05-mma-instructions-mma>`__.
"""
descriptive_name = "tcgen05 F8 MMA Operation"
def __init__(
self,
ab_dtype: Type[Numeric],
acc_dtype: Type[Numeric],
instruction_shape: Shape,
cta_group: CtaGroup,
a_src: OperandSource,
a_major_mode: OperandMajorMode,
b_major_mode: OperandMajorMode,
) -> None:
super().__init__(
ab_dtype,
ab_dtype,
acc_dtype,
instruction_shape,
cta_group,
a_src,
a_major_mode,
b_major_mode,
)
self._verify()
def _verify(self) -> None:
# Input data type verification
if self.a_dtype not in [Float8E5M2, Float8E4M3FN]:
raise OpError(
self,
"expects the 'ab_dtype' Op parameter to be one of Float8E5M2 or Float8E4M3FN",
)
assert self.b_dtype == self.a_dtype, "a_dtype and b_dtype must be the same"
# Accumulator data type verification
if self.acc_dtype not in [Float16, Float32]:
raise OpError(
self,
"expects the 'acc_dtype' Op parameter to be one of Float16 or Float32",
)
# Instruction shape verification
instruction_k = 32
if rank(self.shape_mnk) == 2:
object.__setattr__(self, "shape_mnk", (*self.shape_mnk, instruction_k))
if self.shape_mnk[2] != instruction_k:
raise OpError(
self,
f"expects the instruction extent in the K-mode to be {instruction_k}, "
f"but got {self.shape_mnk[2]}",
)
def _make_trait(self, *, loc=None, ip=None, **kwargs) -> "MmaFP8Trait":
shape_mnk = _pack_shape(self.shape_mnk, loc=loc, ip=ip)
ty = _cute_nvgpu_ir.MmaAtomSM100UMMAType.get(
shape_mnk.type.attribute,
self.cta_group.value,
self.a_major_mode._to_ir(),
self.b_major_mode._to_ir(),
self.a_dtype.mlir_type,
self.b_dtype.mlir_type,
self.acc_dtype.mlir_type,
self.a_src._to_ir(),
0,
)
return MmaFP8Trait(
_cute_nvgpu_ir.make_sm100_mma(
ty,
Boolean(False).ir_value(loc=loc, ip=ip),
Boolean(False).ir_value(loc=loc, ip=ip),
Boolean(False).ir_value(loc=loc, ip=ip),
loc=loc,
ip=ip,
)
)
class MmaFP8Trait(MmaTrait):
pass
####################################################################################################
#
# SMEM layout atoms
#
####################################################################################################
class SmemLayoutAtomKind(enum.Enum):
"""
Enum class for the kinds of SMEM layout atoms for SM100.
Given a swizzle kind, an SMEM layout atom is the compact layout of smallest size that can be
used to construct an SMEM layout using blocked product for operand A or B such that the
resulting layout is legal for both TMA and UMMA.
Note that there are other ways of creating legal layouts for operand A and B.
"""
MN_INTER = enum.auto()
MN_SW32 = enum.auto()
MN_SW64 = enum.auto()
MN_SW128 = enum.auto()
MN_SW128_32B = enum.auto()
K_INTER = enum.auto()
K_SW32 = enum.auto()
K_SW64 = enum.auto()
K_SW128 = enum.auto()
@@ -0,0 +1,25 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from .copy import *
from .mma import *
# __all__ is required here for documentation generation
__all__ = [
# mma.py
"MmaF16BF16Op",
# copy.py
"LdMatrix8x8x16bOp",
"LdMatrix16x16x8bOp",
"StMatrix8x8x16bOp",
"StMatrix16x8x8bOp",
]
@@ -0,0 +1,189 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from dataclasses import dataclass
from typing import Type
import cutlass._mlir.dialects.cute as _cute_ir
import cutlass._mlir.dialects.cute_nvgpu as _cute_nvgpu_ir
from cutlass._mlir import ir
from ..common import OpError
from ...core import CopyOp, Trait, _pack_shape
from ...typing import Numeric
@dataclass(frozen=True)
class BaseOp(CopyOp):
transpose: bool = False
num_matrices: int = 1
def __post_init__(self) -> None:
if not isinstance(self.transpose, bool):
raise OpError(
self,
"expects the 'transpose' Op parameter to be a bool instance",
)
def __str__(self) -> str:
res = (
f"{self.__class__.__name__[:-2]} Copy Operation"
+ f"\n number of matrices = {self.num_matrices}"
)
if self.transpose:
res += f"\n transposed"
return res
@dataclass(frozen=True)
class LdMatrix8x8x16bOp(BaseOp):
"""
8x8 ``ldmatrix`` Operation.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#warp-level-matrix-load-instruction-ldmatrix>`__.
This operation corresponds to the ``.m8n8`` qualifier.
"""
def __post_init__(self) -> None:
super().__post_init__()
if self.num_matrices not in [1, 2, 4]:
raise OpError(
self,
"expects the 'num_matrices' Op parameter to be one of [1,2,4]",
)
def _make_trait(
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
) -> "LdMatrix8x8x16bTrait":
mode = _pack_shape((8, 8), loc=loc, ip=ip)
ty = _cute_nvgpu_ir.CopyAtomLdsmType.get(
copy_internal_type.mlir_type,
mode.type.attribute,
_cute_nvgpu_ir.LdsmSzPattern.u16,
self.num_matrices,
ir.UnitAttr.get() if self.transpose else None,
)
return LdMatrix8x8x16bTrait(_cute_ir.atom(ty, loc=loc, ip=ip))
class LdMatrix8x8x16bTrait(Trait):
pass
@dataclass(frozen=True)
class LdMatrix16x16x8bOp(BaseOp):
"""
16x16 8-bit ``ldmatrix`` Operation.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#warp-level-matrix-load-instruction-ldmatrix>`__.
This operation corresponds to the ``.m16n16`` and the ``.b16`` qualifiers.
"""
def __init__(self, num_matrices: int) -> None:
super().__init__(transpose=True, num_matrices=num_matrices)
self._verify()
def _verify(self):
assert self.transpose, "transpose must be True"
if self.num_matrices not in [1, 2]:
raise OpError(
self,
"expects the 'num_matrices' Op parameter to be one of [1,2]",
)
def _make_trait(
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
) -> "LdMatrix16x16x8bTrait":
mode = _pack_shape((16, 16), loc=loc, ip=ip)
ty = _cute_nvgpu_ir.CopyAtomLdsmType.get(
copy_internal_type.mlir_type,
mode.type.attribute,
_cute_nvgpu_ir.LdsmSzPattern.u8,
self.num_matrices,
ir.UnitAttr.get(),
)
return LdMatrix16x16x8bTrait(_cute_ir.atom(ty, loc=loc, ip=ip))
class LdMatrix16x16x8bTrait(Trait):
pass
@dataclass(frozen=True)
class StMatrix8x8x16bOp(BaseOp):
"""
8x8 ``stmatrix`` Operation.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#warp-level-matrix-instructions-stmatrix>`__.
This operation corresponds to the ``m8n8`` qualifier.
"""
def __post_init__(self) -> None:
super().__post_init__()
if self.num_matrices not in [1, 2, 4]:
raise OpError(
self,
"expects the 'num_matrices' Op parameter to be one of [1,2,4]",
)
def _make_trait(
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
) -> "StMatrix8x8x16bTrait":
mode = _pack_shape((8, 8), loc=loc, ip=ip)
ty = _cute_nvgpu_ir.CopyAtomStsmType.get(
copy_internal_type.mlir_type,
mode.type.attribute,
self.num_matrices,
ir.UnitAttr.get() if self.transpose else None,
)
return StMatrix8x8x16bTrait(_cute_ir.atom(ty, loc=loc, ip=ip))
class StMatrix8x8x16bTrait(Trait):
pass
@dataclass(frozen=True)
class StMatrix16x8x8bOp(BaseOp):
"""
16x8 ``stmatrix`` Operation.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#warp-level-matrix-instructions-stmatrix>`__.
This operation corresponds to the ``m16n8`` qualifier.
"""
def __init__(self, num_matrices: int) -> None:
super().__init__(transpose=True, num_matrices=num_matrices)
self._verify()
def _verify(self):
if self.num_matrices not in [1, 2, 4]:
assert self.transpose, "transpose must be True"
raise OpError(
self,
"expects the 'num_matrices' Op parameter to be one of [1,2,4]",
)
def _make_trait(
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
) -> "StMatrix16x8x8bTrait":
mode = _pack_shape((16, 8), loc=loc, ip=ip)
ty = _cute_nvgpu_ir.CopyAtomStsmType.get(
copy_internal_type.mlir_type,
mode.type.attribute,
self.num_matrices,
ir.UnitAttr.get(),
)
return StMatrix16x8x8bTrait(_cute_ir.atom(ty, loc=loc, ip=ip))
class StMatrix16x8x8bTrait(Trait):
pass
@@ -0,0 +1,78 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from dataclasses import dataclass
from typing import Type
import cutlass._mlir.dialects.cute as _cute_ir
import cutlass._mlir.dialects.cute_nvgpu as _cute_nvgpu_ir
from ..common import OpError
from ...core import MmaOp, Trait, _pack_shape
from ...typing import Shape, Float16, BFloat16, Float32, Numeric
@dataclass(frozen=True)
class MmaF16BF16Op(MmaOp):
"""
F16/BF16 tcgen05 MMA Operation.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#warp-level-matrix-instructions-mma>`__.
This Operation covers the instructions using the ``.f16`` or ``.bf16`` qualifiers for the input operands.
"""
ab_dtype: Type[Numeric]
acc_dtype: Type[Numeric]
shape_mnk: Shape
def __post_init__(self) -> None:
if self.ab_dtype not in [Float16, BFloat16]:
raise OpError(
self,
"expects the 'ab_dtype' Op parameter to be one of Float16 or BFloat16",
)
if self.acc_dtype not in [Float16, Float32]:
raise OpError(
self,
"expects the 'acc_dtype' Op parameter to be one of Float16 or Float32",
)
if (self.ab_dtype == BFloat16) and (self.acc_dtype != Float32):
raise OpError(
self,
"expects the 'acc_dtype' Op parameter to be Float32 when 'ab_dtype' is BFloat16",
)
if self.shape_mnk not in [(16, 8, 8), (16, 8, 16)]:
raise OpError(
self,
"expects the 'shape_mnk' Op parameter to be one of (16,8,8) or (16,8,16)",
)
def _make_trait(self, *, loc=None, ip=None, **kwargs) -> "MmaF16BF16Trait":
shape_mnk = _pack_shape(self.shape_mnk, loc=loc, ip=ip)
ty = _cute_nvgpu_ir.MmaAtomSM80Type.get(
shape_mnk.type.attribute,
self.ab_dtype.mlir_type,
self.ab_dtype.mlir_type,
self.acc_dtype.mlir_type,
)
return MmaF16BF16Trait(_cute_ir.atom(ty, loc=loc, ip=ip))
def __str__(self) -> str:
return (
"warp-level F16/BF16 MMA Operation"
+ f"\n A/B data type = {self.ab_dtype}"
+ f"\n Accumulator data type = {self.acc_dtype}"
+ f"\n Instruction shape MNK = {self.shape_mnk}"
)
class MmaF16BF16Trait(Trait):
pass
@@ -0,0 +1,29 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from .mma import *
from .helpers import *
# __all__ is required here for documentation generation
__all__ = [
# mma.py
"OperandMajorMode",
"OperandSource",
"Field",
"MmaF16BF16Op",
"MmaF8Op",
"SmemLayoutAtomKind",
# helpers.py
"make_smem_layout_atom",
"fence",
"commit_group",
"wait_group",
]
@@ -0,0 +1,109 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from typing import Type
from cutlass.cutlass_dsl import dsl_user_op
from cutlass._mlir.dialects import nvvm
from ...typing import Numeric, NumericMeta
from ... import core
from .mma import SmemLayoutAtomKind
@dsl_user_op
def make_smem_layout_atom(
kind: SmemLayoutAtomKind, element_type: Type[Numeric], *, loc=None, ip=None
) -> core.ComposedLayout:
"""
Makes a SMEM layout Atom.
This function creates a composed layout in unit of elements consistent with the requested layout
Atom kind and element data type.
:param kind: The kind of layout Atom
:type kind: SmemLayoutAtomKind
:param element_type: The element data type to construct the layout for
:type element_type: Type[Numeric]
:return: The SMEM layout atom
:rtype: core.ComposedLayout
"""
if not isinstance(element_type, NumericMeta):
raise TypeError(f"element_type must be a Numeric, but got {element_type}")
if kind in (SmemLayoutAtomKind.MN_INTER, SmemLayoutAtomKind.K_INTER):
num_contiguous_bits = 128
sw = core.make_swizzle(0, 4, 3)
elif kind in (SmemLayoutAtomKind.MN_SW32, SmemLayoutAtomKind.K_SW32):
num_contiguous_bits = 256
sw = core.make_swizzle(1, 4, 3)
elif kind in (SmemLayoutAtomKind.MN_SW64, SmemLayoutAtomKind.K_SW64):
num_contiguous_bits = 512
sw = core.make_swizzle(2, 4, 3)
elif kind in (SmemLayoutAtomKind.MN_SW128, SmemLayoutAtomKind.K_SW128):
num_contiguous_bits = 1024
sw = core.make_swizzle(3, 4, 3)
else:
raise ValueError("unrecognized SMEM layout atom kind")
num_contiguous_elems = num_contiguous_bits // element_type.width
if kind in (
SmemLayoutAtomKind.MN_INTER,
SmemLayoutAtomKind.MN_SW32,
SmemLayoutAtomKind.MN_SW64,
SmemLayoutAtomKind.MN_SW128,
):
# M/N-major layout
return core.make_composed_layout(
sw,
0,
core.make_layout(
(num_contiguous_elems, 8), stride=(1, num_contiguous_elems)
),
loc=loc,
ip=ip,
)
else:
# K-major layout
return core.make_composed_layout(
sw,
0,
core.make_layout(
(8, num_contiguous_elems), stride=(num_contiguous_elems, 1)
),
loc=loc,
ip=ip,
)
@dsl_user_op
def fence(*, loc=None, ip=None) -> None:
"""
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#asynchronous-multiply-and-accumulate-instruction-wgmma-fence>`__.
"""
nvvm.wgmma_fence_aligned(loc=None, ip=None)
@dsl_user_op
def commit_group(*, loc=None, ip=None) -> None:
"""
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#asynchronous-warpgroup-level-matrix-instructions-wgmma-commit-group>`__.
"""
nvvm.wgmma_commit_group_sync_aligned(loc=loc, ip=ip)
@dsl_user_op
def wait_group(group, *, loc=None, ip=None) -> None:
"""
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#asynchronous-multiply-and-accumulate-instruction-wgmma-wait-group>`__.
"""
nvvm.wgmma_wait_group_sync_aligned(group, loc=loc, ip=ip)
@@ -0,0 +1,380 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
import enum
from dataclasses import dataclass
from typing import Type
from cutlass.cutlass_dsl import CuTeDSL
import cutlass._mlir.dialects.cute as _cute_ir
import cutlass._mlir.dialects.cute_nvgpu as _cute_nvgpu_ir
from cutlass._mlir import ir
from ..common import OpError
from ...core import MmaOp, Trait, _pack_shape, rank, depth
from ...typing import (
Shape,
Float16,
BFloat16,
Float32,
Boolean,
Float8E5M2,
Float8E4M3FN,
Numeric,
)
####################################################################################################
#
# MMA Ops and Traits
#
####################################################################################################
class OperandMajorMode(enum.Enum):
"""
An enumeration for the majorness of the input operands of the MMA.
"""
MN = _cute_ir.MajorMode.mn
K = _cute_ir.MajorMode.k
def __str__(self) -> str:
return f"{self.__class__.__name__}.{self.name}"
def __repr__(self) -> str:
return f"<{self.__class__.__name__}.{self.name}>"
@classmethod
def _missing_(cls, value):
if isinstance(value, str):
value = value.upper()
if value == "MN":
return OperandMajorMode.MN
elif value == "K":
return OperandMajorMode.K
def _to_ir(self) -> _cute_ir.MajorMode:
return self.value
class OperandSource(enum.Enum):
"""
An enumeration for the source memory location of the A input operand of the MMA.
"""
RMEM = _cute_ir.MmaFragKind.rmem
SMEM = _cute_ir.MmaFragKind.smem_desc
def __str__(self) -> str:
return f"{self.__class__.__name__}.{self.name}"
def __repr__(self) -> str:
return f"<{self.__class__.__name__}.{self.name}>"
def _to_ir(self) -> _cute_ir.MmaFragKind:
return self.value
class Field(enum.Enum):
"""
An enumeration for the fields of the MMA Atom that can be modified at runtime.
"""
ACCUMULATE = "accum_c"
def __str__(self) -> str:
return f"{self.__class__.__name__}.{self.name}"
def __repr__(self) -> str:
return f"<{self.__class__.__name__}.{self.name}>"
def _to_ir_field_name(self) -> str:
return self.value
@dataclass(frozen=True)
class MmaOp(MmaOp):
a_dtype: Type[Numeric]
b_dtype: Type[Numeric]
acc_dtype: Type[Numeric]
shape_mnk: Shape
a_src: OperandSource
a_major_mode: OperandMajorMode
b_major_mode: OperandMajorMode
admissible_archs = ["sm_90a"]
def __post_init__(self) -> None:
# Verify arch
arch = CuTeDSL._get_dsl().envar.arch
if arch not in self.admissible_archs:
raise OpError(
self,
f"expects arch to be one of {self.admissible_archs}, but got {arch}",
suggestion="Ensure env CUTE_DSL_ARCH matches your GPU architecture",
)
# Verify that the user provided enum values
if not isinstance(self.a_src, OperandSource):
raise OpError(
self,
"expects the 'a_src' Op parameter to be a warpgroup.OperandSource instance",
)
if not isinstance(self.a_major_mode, OperandMajorMode):
raise OpError(
self,
"expects the 'a_major_mode' Op parameter to be a warpgroup.OperandMajorMode instance",
)
if not isinstance(self.b_major_mode, OperandMajorMode):
raise OpError(
self,
"expects the 'b_major_mode' Op parameter to be a warpgroup.OperandMajorMode instance",
)
# Verify instruction shape
if (rank(self.shape_mnk) not in [2, 3]) or (depth(self.shape_mnk) != 1):
raise OpError(
self,
f"expected a flat rank 2 or 3 tuple for the 'shape_mnk' Op parameter, "
f"but got {self.shape_mnk}",
)
m, n = self.shape_mnk[0], self.shape_mnk[1]
if m != 64:
raise OpError(self, f"expects the M-mode to be 64, but got {m}")
if (n < 8) or (n > 256) or (n % 8 != 0):
raise OpError(
self,
f"expects the N-mode to satisfy 8 <= N <= 256 and N % 8 == 0. but got {n}",
)
def __str__(self) -> str:
return (
self.__class__.descriptive_name # type: ignore
+ f"\n A data type = {self.a_dtype}"
+ f"\n B data type = {self.b_dtype}"
+ f"\n Accumulator data type = {self.acc_dtype}"
+ f"\n A source location = {self.a_src}"
+ f"\n A major mode = {self.a_major_mode}"
+ f"\n B major mode = {self.b_major_mode}"
+ f"\n Instruction shape MNK = {self.shape_mnk}"
)
class MmaTrait(Trait):
admissible_fields = [Field.ACCUMULATE]
def set(self, field, value, *, loc=None, ip=None) -> None:
if field not in self.admissible_fields:
raise ValueError(
f"invalid field, must be {Field.ACCUMULATE}, but got {field}"
)
field_name = f"#cute_nvgpu.atom_mma_field_sm90<{field._to_ir_field_name()}>"
attr = ir.Attribute.parse(field_name)
self.value = _cute_nvgpu_ir.atom_set_value(
self.value, attr, Boolean(value).ir_value(loc=loc, ip=ip), loc=loc, ip=ip
)
@dataclass(frozen=True)
class MmaF16BF16Op(MmaOp):
"""
F16/BF16 warpgroup MMA Operation.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#asynchronous-multiply-and-accumulate-instruction-wgmma-mma-async>`__.
This Operation covers the instructions using the ``.f16`` or ``.bf16`` qualifiers for the input operands.
"""
descriptive_name = "warpgroup F16/BF16 MMA Operation"
def __init__(
self,
ab_dtype: Type[Numeric],
acc_dtype: Type[Numeric],
instruction_shape: Shape,
a_src: OperandSource,
a_major_mode: OperandMajorMode,
b_major_mode: OperandMajorMode,
) -> None:
super().__init__(
ab_dtype,
ab_dtype,
acc_dtype,
instruction_shape,
a_src,
a_major_mode,
b_major_mode,
)
self._verify()
def _verify(self) -> None:
# Input data type verification
if self.a_dtype not in [Float16, BFloat16]:
raise OpError(
self,
"expects the 'ab_dtype' Op parameter to be one of Float16 or BFloat16",
)
assert self.b_dtype == self.a_dtype, "a_dtype and b_dtype must be the same"
# Accumulator data type verification
if self.acc_dtype not in [Float16, Float32]:
raise OpError(
self,
"expects the 'acc_dtype' Op parameter to be one of Float16 or Float32",
)
if (self.a_dtype == BFloat16) and (self.acc_dtype != Float32):
raise OpError(
self,
"expects the 'acc_dtype' Op parameter to be Float32 when 'ab_dtype' is BFloat16",
)
# Verify the instruction shape
instruction_k = 16
if rank(self.shape_mnk) == 2:
object.__setattr__(self, "shape_mnk", (*self.shape_mnk, instruction_k))
if self.shape_mnk[2] != instruction_k:
raise OpError(
self,
f"expects the instruction extent in the K-mode to be {instruction_k}, "
f"but got {self.shape_mnk[2]}",
)
def _make_trait(self, *, loc=None, ip=None, **kwargs) -> "MmaF16BF16Trait":
shape_mnk = _pack_shape(self.shape_mnk, loc=loc, ip=ip)
ty = _cute_nvgpu_ir.MmaAtomSM90Type.get(
shape_mnk.type.attribute,
self.a_major_mode._to_ir(),
self.b_major_mode._to_ir(),
self.a_dtype.mlir_type,
self.b_dtype.mlir_type,
self.acc_dtype.mlir_type,
self.a_src._to_ir(),
)
return MmaF16BF16Trait(
_cute_nvgpu_ir.make_sm90_mma(
ty,
Boolean(False).ir_value(loc=loc, ip=ip),
loc=loc,
ip=ip,
)
)
class MmaF16BF16Trait(MmaTrait):
pass
@dataclass(frozen=True)
class MmaF8Op(MmaOp):
"""
F16/BF16 warpgroup MMA Operation.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#asynchronous-multiply-and-accumulate-instruction-wgmma-mma-async>`__.
This Operation covers the instructions using the ``.e4m3`` or ``.e5m2`` qualifiers for the input operands.
"""
descriptive_name = "warpgroup F8 MMA Operation"
def __init__(
self,
a_dtype: Type[Numeric],
b_dtype: Type[Numeric],
acc_dtype: Type[Numeric],
instruction_shape: Shape,
a_src: OperandSource,
a_major_mode: OperandMajorMode,
b_major_mode: OperandMajorMode,
) -> None:
super().__init__(
a_dtype,
b_dtype,
acc_dtype,
instruction_shape,
a_src,
a_major_mode,
b_major_mode,
)
self._verify()
def _verify(self):
# Input data type verification
if self.a_dtype not in [Float8E5M2, Float8E4M3FN]:
raise OpError(
self,
"expects the 'a_dtype' Op parameter to be one of Float8E5M2 or Float8E4M3FN",
)
if self.b_dtype not in [Float8E5M2, Float8E4M3FN]:
raise OpError(
self,
"expects the 'b_dtype' Op parameter to be one of Float8E5M2 or Float8E4M3FN",
)
# Accumulator data type verification
if self.acc_dtype != Float32:
raise OpError(
self,
"expects the 'acc_dtype' Op parameter to be Float32",
)
# Verify the instruction shape
instruction_k = 32
if rank(self.shape_mnk) == 2:
object.__setattr__(self, "shape_mnk", (*self.shape_mnk, instruction_k))
if self.shape_mnk[2] != instruction_k:
raise OpError(
self,
f"expects the instruction extent in the K-mode to be {instruction_k}, "
f"but got {self.shape_mnk[2]}",
)
def _make_trait(self, *, loc=None, ip=None, **kwargs) -> "MmaF8Trait":
shape_mnk = _pack_shape(self.shape_mnk, loc=loc, ip=ip)
ty = _cute_nvgpu_ir.MmaAtomSM90Type.get(
shape_mnk.type.attribute,
self.a_major_mode._to_ir(),
self.b_major_mode._to_ir(),
self.a_dtype.mlir_type,
self.b_dtype.mlir_type,
self.acc_dtype.mlir_type,
self.a_src._to_ir(),
)
return MmaF8Trait(
_cute_nvgpu_ir.make_sm90_mma(
ty, Boolean(False).ir_value(loc=loc, ip=ip), loc=loc, ip=ip
)
)
class MmaF8Trait(MmaTrait):
pass
####################################################################################################
#
# SMEM layout atoms
#
####################################################################################################
class SmemLayoutAtomKind(enum.Enum):
"""
Enum class for the kinds of SMEM layout atoms for SM90.
Given a swizzle kind, an SMEM layout atom is the compact layout of smallest size that can
be used to construct an SMEM layout using blocked product for operand A or B such that the
resulting layout is legal for both TMA and UMMA.
Note that there are other ways of creating legal layouts for operand A and B.
"""
MN_INTER = enum.auto()
MN_SW32 = enum.auto()
MN_SW64 = enum.auto()
MN_SW128 = enum.auto()
K_INTER = enum.auto()
K_SW32 = enum.auto()
K_SW64 = enum.auto()
K_SW128 = enum.auto()
+515
View File
@@ -0,0 +1,515 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
import ctypes
from functools import lru_cache
import itertools
import operator
from time import time
from typing import Union
# MLIR modules imports
from cutlass._mlir import ir
import cutlass._mlir.dialects.cute as _cute_ir
from cutlass.cutlass_dsl import TensorFormat, JitArgAdapterRegistry
# Local modules imports
from .typing import (
AddressSpace,
Tensor,
Type,
Pointer,
Boolean,
Numeric,
Float4E2M1FN,
Int64,
Int32,
Int16,
Int8,
Uint64,
Uint32,
Uint16,
Uint8,
Float64,
Float32,
Float16,
BFloat16,
Float8E5M2,
)
from .core import find, _Tensor as CoreTensor
class _Pointer(Pointer):
"""Runtime representation of a pointer that can inter-operate with various data structures,
including numpy arrays and device memory.
:param pointer: The pointer to the data
:type pointer: int or pointer-like object
:param dtype: Data type of the elements pointed to
:type dtype: Type
:param mem_space: Memory space where the pointer resides, defaults to generic
:type mem_space: _cute_ir.AddressSpace, optional
:param assumed_align: Assumed alignment of input pointer in bytes, defaults to None
:type assumed_align: int, optional
:ivar _pointer: The underlying pointer
:ivar _dtype: Data type of the elements
:ivar _addr_space: Memory space of the pointer
:ivar _assumed_align: Alignment of the pointer in bytes
:ivar _desc: C-type descriptor for the pointer
:ivar _c_pointer: C-compatible pointer representation
"""
def __init__(
self,
pointer,
dtype,
mem_space: _cute_ir.AddressSpace = _cute_ir.AddressSpace.generic,
assumed_align=None,
):
self._pointer = pointer
self._dtype = dtype
self._addr_space = mem_space
is_in_device = mem_space == _cute_ir.AddressSpace.gmem
if assumed_align is None:
if is_in_device:
self._assumed_align = 32
else:
self._assumed_align = dtype.width // 8
else:
self._assumed_align = assumed_align
class PtrDescriptor(ctypes.Structure):
"""A ctype descriptor for CuTe memref ptr"""
_fields_ = [("ptr", ctypes.c_void_p)]
def __str__(self):
return f"0x{self.ptr:016x}"
self._desc = PtrDescriptor(int(self._pointer))
self._c_pointer = ctypes.cast(ctypes.pointer(self._desc), ctypes.c_void_p)
assert (
self._desc.ptr % self._assumed_align == 0
), f"pointer must be {self._assumed_align} bytes aligned"
def size_in_bytes(self) -> int:
return ctypes.sizeof(self._desc)
def __get_mlir_types__(self):
return [self.mlir_type]
def __c_pointers__(self):
return [self._c_pointer]
def __new_from_mlir_values__(self, values):
assert len(values) == 1
return values[0]
# Move mlir Type out of __init__ to decouple with mlir Context
@property
def mlir_type(self) -> ir.Type:
return _cute_ir.PtrType.get(
self._dtype.mlir_type, self._addr_space, self._assumed_align
)
@property
def element_type(self) -> Type[Numeric]:
return self._dtype
@property
def memspace(self):
return self._addr_space
def verify(self, expected_py_type):
if expected_py_type is Pointer:
return True
elif isinstance(expected_py_type, ir.Value) and expected_py_type.ty is Pointer:
return True
return False
def __str__(self) -> str:
return f"Ptr<0x{self._desc.ptr:016x}@{self._addr_space}>"
def __repr__(self):
return self.__str__()
class _Tensor(Tensor):
def __init__(
self,
tensor,
assumed_align=None,
):
# If tensor is already a DLPack object, use it directly
if hasattr(tensor, "__dlpack_device__") and not hasattr(tensor, "__dlpack__"):
self._dlpack_data = tensor
else:
self._dlpack_data = tensor.__dlpack__()
self._dltensor_wrapper = None
self._assumed_align = assumed_align
self._is_dynamic = False
self._memref_desc = None
self._dtype = None
@property
def __class__(self) -> Type[Tensor]:
# Cheat to let `type(_Tensor())` to return cute.Tensor
return Tensor
@staticmethod
def lazily_load_dltensor(func):
"""Decorator to lazily load the DLTensorWrapper.
This decorator loads the DLTensorWrapper when needed,
avoiding overhead in the critical path of calling JIT functions.
"""
def wrapper(self, *args, **kwargs):
if self._dltensor_wrapper is None:
self._dltensor_wrapper = _cute_ir.DLTensorWrapper(self._dlpack_data)
return func(self, *args, **kwargs)
return wrapper
@lazily_load_dltensor
def mark_layout_dynamic(self, leading_dim: int | None = None):
"""Marks the tensor layout as dynamic based on the leading dimension.
:param leading_dim: The leading dimension of the layout, defaults to None
:type leading_dim: int, optional
When ``leading_dim`` is None, automatically deduces the leading dimension from the tensor layout.
The layout can be deduced only when exactly one dimension has a stride of 1. Raises an error
if the layout cannot be automatically deduced.
When ``leading_dim`` is explicitly specified, marks the layout as dynamic while setting the
stride at ``leading_dim`` to 1. Also validates that the specified ``leading_dim`` is consistent
with the existing layout by checking that the corresponding stride of that dimension is 1.
Limitation: only support flat layout for now. Will work on supporting nested layout in the future.
:return: The tensor with dynamic layout
:rtype: _Tensor
"""
self._dltensor_wrapper.mark_layout_dynamic(leading_dim)
return self
@lazily_load_dltensor
def mark_compact_shape_dynamic(
self,
mode: int,
stride_order: tuple[int, ...] | None = None,
divisibility: int = 1,
):
"""Marks the tensor shape as dynamic and propagates dynamic and divisibility information to the corresponding strides.
:param mode: The mode of the compact shape, defaults to 0
:type mode: int
:param stride_order: Consistent with `torch.Tensor.dim_order`. Defaults to None.
Indicates the order of the modes (dimensions) if the current layout were converted to row-major order.
It starts from the outermost to the innermost dimension.
:type stride_order: tuple[int, ...], optional
:param divisibility: The divisibility constraint for the compact shape, defaults to 1
:type divisibility: int, optional
:return: The tensor with dynamic compact shape
:rtype: _Tensor
If ``stride_order`` is not provided, the stride ordering will be automatically deduced from the layout.
Automatic deduction is only possible when exactly one dimension has a stride of 1 (compact layout).
An error is raised if automatic deduction fails.
If ``stride_order`` is explicitly specified, it does the consistency check with the layout.
For example:
- Layout: (4,2):(1,4) has stride_order: (1,0) indicates the innermost dimension is 0(`4:1`), the outermost dimension is 1(`2:4`)
- Layout: (5,3,2,4):(3,1,15,30) has stride_order: (3,2,0,1) indicates the innermost dimension is 1(`3:1`), the outermost dimension is 3(`4:30`).
Using `torch.Tensor.dim_order()` to get the stride order of the torch tensor.
.. code-block:: python
a = torch.empty(3, 4)
t = cute.runtime.from_dlpack(a)
t = t.mark_compact_shape_dynamic(mode=0, stride_order=a.dim_order())
"""
self._dltensor_wrapper.mark_compact_shape_dynamic(
mode, stride_order, divisibility
)
return self
@property
@lazily_load_dltensor
def element_type(self) -> Type[Numeric]:
if self._dtype is None:
self._dtype = self._dltensor_wrapper.dtype
return self._dtype
@element_type.setter
def element_type(self, new_type):
"""Set the element type of the tensor.
:warning: This API is added for narrow precision before we have a clean `recast_tensor` story.
:note: It is only used for the case that frameworks don't natively support narrow precision but we get tensor
from frameworks with storage type like uint8.
**Example**:
.. code-block:: python
# Create a tensor from a numpy array
import numpy as np
from cutlass.cute import from_dlpack
# Create a tensor with Float32 elements
a = np.zeros(shape, dtype=np.uint8)
tensor = from_dlpack(a)
# Change the element type to Float4E2M1FN even storage type is uint8
tensor.element_type = cutlass.Float4E2M1FN
src = from_dlpack(... data tensor ...)
# convert and initialize narrow precision tensor
cute.testing.convert(src, tensor)
"""
self._dtype = new_type
@property
@lazily_load_dltensor
def memspace(self):
return self._dltensor_wrapper.address_space
@property
@lazily_load_dltensor
def size_in_bytes(self) -> int:
return self._dltensor_wrapper.size_in_bytes()
@property
@lazily_load_dltensor
def mlir_type(self) -> ir.Type:
return self._dltensor_wrapper.get_type(
self.element_type.mlir_type, self._assumed_align
)
@lazily_load_dltensor
def __str__(self) -> str:
return f"Tensor<0x{self._dltensor_wrapper.str}>"
def __repr__(self):
return self.__str__()
def __setitem__(self, crd, value):
raise TypeError(f"runtime._Tensor is not indexable")
def __getitem__(self, crd):
raise TypeError(f"runtime._Tensor is not indexable")
@property
@lazily_load_dltensor
def iterator(self):
return _Pointer(
self._dltensor_wrapper.data_ptr,
self.element_type,
self.memspace,
self._assumed_align,
)
@property
def layout(self):
raise NotImplementedError(
f"layout property is not supported in runtime, support in future"
)
@property
@lazily_load_dltensor
def shape(self):
return self._dltensor_wrapper.shape
@property
@lazily_load_dltensor
def stride(self):
strides = self._dltensor_wrapper.stride
if strides is None:
strides = itertools.accumulate(
reversed(self.shape), func=operator.mul, initial=1
)
strides = tuple(reversed(list(strides)[:-1]))
return strides
@property
@lru_cache(maxsize=128, typed=True)
def leading_dim(self):
"""Get the leading dimension of this Tensor.
:return: The leading dimension index or indices
:rtype: int or tuple or None
The return value depends on the tensor's stride pattern:
* If a single leading dimension is found, returns an integer index
* If nested leading dimensions are found, returns a tuple of indices
* If no leading dimension is found, returns None
"""
return find(1, self.stride, exclude_when=(1, self.shape))
def fill(self, value: Numeric):
raise TypeError(f"fill function is not supported in runtime")
@property
@lazily_load_dltensor
def data_ptr(self):
return self._dltensor_wrapper.data_ptr
@lazily_load_dltensor
def __c_pointers__(self):
self._memref_desc = self._dltensor_wrapper.build_memref_desc(
self._assumed_align
)
return [_cute_ir.pycapsule_get_pointer(self._memref_desc)]
def __get_mlir_types__(self):
return [self.mlir_type]
def __new_from_mlir_values__(self, values):
assert len(values) == 1
assert isinstance(values[0], CoreTensor)
return CoreTensor(values[0].value, self._dtype)
def from_dlpack(
tensor_dlpack,
assumed_align=None,
) -> Tensor:
"""Convert from tensor object supporting __dlpack__() to a CuTe Tensor.
:param tensor_dlpack: Tensor object that supports the DLPack protocol
:type tensor_dlpack: object
:param assumed_align: Assumed alignment of the tensor (bytes), defaults to None,
if None, will use the element size bytes as the assumed alignment.
:type assumed_align: int, optional
:return: A CuTe Tensor object
:rtype: Tensor
Examples:
.. code-block:: python
import torch
from cutlass.cute.runtime import from_dlpack
x = torch.randn(100, 100)
y = from_dlpack(x)
y.shape
# (100, 100)
type(y)
# <class 'cutlass.cute.Tensor'>
"""
return _Tensor(
tensor_dlpack,
assumed_align=assumed_align,
)
def make_ptr(
dtype: Type[Numeric],
value: Union[int, ctypes._Pointer],
mem_space: AddressSpace = AddressSpace.generic,
assumed_align=None,
) -> Pointer:
"""Create a pointer from a memory address
:param dtype: Data type of the pointer elements
:type dtype: Type[Numeric]
:param value: Memory address as integer or ctypes pointer
:type value: Union[int, ctypes._Pointer]
:param mem_space: Memory address space, defaults to AddressSpace.generic
:type mem_space: AddressSpace, optional
:param align_bytes: Alignment in bytes, defaults to None
:type align_bytes: int, optional
:return: A pointer object
:rtype: Pointer
.. code-block:: python
import numpy as np
import ctypes
from cutlass import Float32
from cutlass.cute.runtime import make_ptr
# Create a numpy array
a = np.random.randn(16, 32).astype(np.float32)
# Get pointer address as integer
ptr_address = a.ctypes.data_as(ctypes.POINTER(ctypes.c_float))
# Create pointer from address
y = make_ptr(cutlass.Float32, ptr_address)
# Check properties
print(y.element_type)
print(type(y)) # <class 'cutlass.cute.Pointer'>
"""
# check if value is int or ctypes.POINTER
if isinstance(value, int):
address_value = value
elif isinstance(value, ctypes._Pointer):
# get address value
address_value = ctypes.cast(value, ctypes.c_void_p).value
assert address_value is not None, "Pointer address is None"
else:
raise TypeError(
f"Expect int or ctypes.POINTER for value but got {type(value)=}"
)
return _Pointer(address_value, dtype, mem_space, assumed_align=assumed_align)
class TensorAdapter:
"""
Convert a DLPack protocol supported tensor/array to a cute tensor.
"""
# Need reference these capsules to avoid being garbage collected
tensor_capsules = []
def __init__(self, arg):
self._arg = from_dlpack(arg).mark_layout_dynamic()
self.tensor_capsules.append(self._arg)
def __new_from_mlir_values__(self, values):
return self._arg.__new_from_mlir_values__(values)
def __c_pointers__(self):
return self._arg.__c_pointers__()
def __get_mlir_types__(self):
return self._arg.__get_mlir_types__()
# -------------------------------------------------------------------------
# Try to register_jit_arg_adapter for TensorAdapter
# -------------------------------------------------------------------------
try: # Register for numpy.ndarray
import numpy
JitArgAdapterRegistry.register_jit_arg_adapter(numpy.ndarray)(TensorAdapter)
except ImportError:
pass # silent attempt, suppress error
try: # Register for torch.Tensor
import torch
JitArgAdapterRegistry.register_jit_arg_adapter(torch.Tensor)(TensorAdapter)
except ImportError:
pass # silent attempt, suppress error
+285
View File
@@ -0,0 +1,285 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
import random
import numpy as np
import functools
import hashlib
from cutlass.cutlass_dsl import (
const,
T,
CuTeDSL,
BaseDSL,
t,
Constexpr,
detect_gpu_arch,
)
import cutlass._mlir.dialects.cute as _cute_ir
import cutlass._mlir.ir as ir
from cutlass._mlir.dialects import nvvm, cf, vector, builtin
from cutlass.cute import core
from cutlass.cute import nvgpu
from typing import Type
from inspect import isclass
def assert_(cond, msg=None):
if isinstance(cond, ir.Value):
if ir.VectorType.isinstance(cond.type):
assert (
cond.type.element_type == T.bool()
), f"only expects vector type with boolean elements, but got {cond.type}"
cond_val = vector.multi_reduction(
vector.CombiningKind.AND, cond, const(True), range(cond.type.rank)
)
else:
cond_val = cond
else:
cond_val = const(cond, t.Boolean)
cf.assert_(cond_val, msg if msg else "")
def _maybe_recast_tensor_from_f4(src: core.Tensor, tv_layout: core.Layout):
if src.element_type.width == 4:
tv_layout = core.recast_layout(8, 4, tv_layout)
src = core.recast_tensor(src, dtype=t.Int8)
return src, tv_layout
def _maybe_recast_to_f4(input: core.TensorSSA, dtype: Type[core.Numeric]):
"""Conditionally recasts the tensor to 4-bit type if the destination type is 4-bit.
:param input: The input tensor to recast.
:param dtype: The target numeric type to potentially recast to.
:raises TypeError: If dtype is not a subclass of Numeric.
:return: A new tensor recast to 4-bit if dtype is 4-bit, otherwise returns self unchanged.
"""
if not isclass(dtype) or not issubclass(dtype, core.Numeric):
raise TypeError(f"dst_ty must be a type of Numeric, but got {dtype}")
if dtype.width == 4:
recast_shape = core.recast_layout(4, 8, core.make_layout(input.shape)).shape
i4_vec = vector.bitcast(
T.vector(input.type.shape[0] * 2, T.i(4)), input.maybe_downcast()
)
res_vect = builtin.unrealized_conversion_cast(
[T.vector(i4_vec.type.shape[0], dtype.mlir_type)], [i4_vec]
)
return core.TensorSSA(res_vect, recast_shape, dtype)
return input
def _maybe_recast_from_f4(input: core.TensorSSA, src_dtype: Type[core.Numeric]):
"""Conditionally recasts the tensor from 4-bit type if the source type is 4-bit.
:param input: The input tensor to recast.
:param src_dtype: The source numeric type to potentially recast from.
:raises TypeError: If src_dtype is not a subclass of Numeric.
:return: A new tensor recast from 4-bit if src_dtype is 4-bit, otherwise returns self unchanged.
"""
if not isclass(src_dtype) or not issubclass(src_dtype, core.Numeric):
raise TypeError(f"src_ty must be a type of Numeric, but got {src_dtype}")
if src_dtype.width == 4:
recast_shape = core.recast_layout(8, 4, core.make_layout(input.shape)).shape
i4_vec = builtin.unrealized_conversion_cast(
[T.vector(input.type.shape[0], T.i(4))], [input.maybe_downcast()]
)
res_vect = vector.bitcast(T.vector(i4_vec.type.shape[0] // 2, T.i8()), i4_vec)
return core.TensorSSA(res_vect, recast_shape, core.Int8)
return input
@CuTeDSL.kernel
def _convert_kernel(
gSrc: core.Tensor,
gDst: core.Tensor,
cSrc: core.Tensor,
src_tv_layout: core.Layout,
dst_tv_layout: core.Layout,
src_shape: core.Shape,
src_ty,
dst_ty,
):
tidx = nvvm.read_ptx_sreg_tid_x(T.i32())
bidx = nvvm.read_ptx_sreg_ctaid_x(T.i32())
cta_coord = (None, bidx)
# logical idx -> address
ctaSrc = gSrc[cta_coord] # (...,TileV,...)
ctaDst = gDst[cta_coord] # (...,TileV,...)
ctaCSrc = cSrc[cta_coord] # (...,TileV,...)
# print(f"ctaSrc = {ctaSrc.type}")
# compose with CTA TV layout
# tid, vid -> address
tidfrgSrc = core.composition(ctaSrc, src_tv_layout) # (T,V)
tidfrgDst = core.composition(ctaDst, dst_tv_layout) # (T,V)
tidfrgCSrc = core.composition(ctaCSrc, src_tv_layout) # (T,V)
# print(f"tidfrgSrc = {tidfrgSrc.type}")
# slice for threads
thr_coord = (tidx, None)
thrSrc = tidfrgSrc[thr_coord] # (V)
thrDst = tidfrgDst[thr_coord] # (V)
thrCSrc = tidfrgCSrc[thr_coord] # (V)
# print(f"thrSrc = {thrSrc.type}")
# predicate
if core.elem_less(thrCSrc[0], src_shape):
# allocate fragments for gmem->rmem
frgSrc = core.make_fragment(
core.get(src_tv_layout, mode=[1]), gSrc.element_type
) # (V)
frgDst = core.make_fragment(
core.get(dst_tv_layout, mode=[1]), gDst.element_type
) # (V)
# print(f"frgSrc = {frgSrc.type}")
# Move data to reg address space
copy_atom_load = core.make_copy_atom(nvgpu.CopyUniversalOp(), gSrc.element_type)
core.copy(copy_atom_load, thrSrc, frgSrc)
vec_src = frgSrc.load()
vec_src = _maybe_recast_to_f4(vec_src, src_ty)
vec_dst = vec_src.to(dst_ty)
vec_dst = _maybe_recast_from_f4(vec_dst, dst_ty)
frgDst.store(vec_dst)
# Copy the results back to c
copy_atom_stg = core.make_copy_atom(nvgpu.CopyUniversalOp(), gDst.element_type)
core.copy(copy_atom_stg, frgDst, thrDst)
@CuTeDSL.jit(preprocess=False)
def _convert(
src: core.Tensor,
dst: core.Tensor,
leading_mode: Constexpr,
elem_per_copy: Constexpr,
):
# Step 1. figure proper tv_layout
src_ty = src.element_type
dst_ty = dst.element_type
tv_layout = core.make_layout((128, elem_per_copy), stride=(elem_per_copy, 1))
# Step 2. maybe recast from f4 tensor
src, src_tv_layout = _maybe_recast_tensor_from_f4(src, tv_layout)
dst, dst_tv_layout = _maybe_recast_tensor_from_f4(dst, tv_layout)
src_shape = src.shape
# predicate tensor
idA = core.make_identity_tensor(src.shape)
# Step 3. select a proper tiling pattern as (...,TileV, ...)
src_cta_tiler = [
1,
] * core.rank(src.layout)
src_cta_tiler[leading_mode] = core.size(src_tv_layout) # (...,TileV,...)
dst_cta_tiler = [
1,
] * core.rank(dst.layout)
dst_cta_tiler[leading_mode] = core.size(dst_tv_layout) # (...,TileV,...)
# Step 4. partition input and output tensor by cta tiler.
gS = core.zipped_divide(
src, tuple(src_cta_tiler)
) # ((...,TileV,...),(...,RestV,...))
cS = core.zipped_divide(
idA, tuple(src_cta_tiler)
) # ((...,TileV,...),(...,RestV,...))
gD = core.zipped_divide(
dst, tuple(dst_cta_tiler)
) # ((...,TileV,...),(...,RestV,...))
# print(f"{gS.type=}")
_convert_kernel(
gS,
gD,
cS,
src_tv_layout,
dst_tv_layout,
src_shape,
src_ty,
dst_ty,
).launch(
grid=[core.size(gS, mode=[1]), 1, 1],
block=[core.size(src_tv_layout, mode=[0]), 1, 1],
)
# Converts from src tensor to dst tensor, their logical shape are required to be the same.
# And when src or dst dtype is narrow precision(Float4E2M1FN/Float8E8M0FNU/Float8E4M3FN), the shape of
# their leading dimension should be 4(fp8)/8(fp4) element align. (nvgpu.cvt_fptrunc/cvt_fpext
# needs 32-bits aligned input/output)
def convert(src: core.Tensor, dst: core.Tensor):
assert len(src.shape) == len(
dst.shape
), "Shape of src and dst tensors should be the same rank."
# find leading mode
leading_mode = np.argmin([np.min(s) for s in src.stride])
elem_per_copy = 2
if src.element_type.width == 4 or dst.element_type.width == 4:
elem_per_copy = 8
elif src.element_type.width == 8 or dst.element_type.width == 8:
elem_per_copy = 4
assert (
src.shape[leading_mode] % elem_per_copy == 0
and dst.shape[leading_mode] % elem_per_copy == 0
)
_convert(src, dst, leading_mode, elem_per_copy)
#########################################
# Testing utilities
#########################################
def sample_pytest(rand_cfg=None):
"""
Decorator to randomly sample pytest parametrized tests.
rand_cfg: Tuple[int, float] - (random_seed, sample_ratio)
Sampling is disabled when:
- A specific test is selected (via -k or direct test path)
- Not running under pytest
"""
import functools
import os
import random
import pytest
import sys
seed, sample_ratio = rand_cfg
random.seed(seed)
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if rand_cfg is not None and "PYTEST_CURRENT_TEST" in os.environ:
# Check if test was explicitly selected like ::test_name[param1-param2-...]
if "-k" in sys.argv or any(".py::" in arg for arg in sys.argv):
# Test was explicitly selected, don't skip
return func(*args, **kwargs)
if random.uniform(0.0, 1.0) > sample_ratio:
pytest.skip(f"Randomly skipped (sampling ratio: {sample_ratio})")
return func(*args, **kwargs)
return wrapper
return decorator
+193
View File
@@ -0,0 +1,193 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from abc import ABC, abstractmethod
from typing import ForwardRef, Tuple, Union, Any, Type, List
from cutlass.base_dsl.typing import *
from cutlass._mlir import ir
import cutlass._mlir.extras.types as T
from cutlass._mlir.dialects.cute import AddressSpace
Int = Union[int, Integer]
ScaledBasis = ForwardRef("ScaledBasis")
IntTuple = Union[Int, Tuple["IntTuple", ...]]
Shape = Union[Int, Tuple["Shape", ...]]
Stride = Union[Int, ScaledBasis, Tuple["Stride", ...]]
Coord = Union[Int, None, Tuple["Coord", ...]]
class Layout(ir.Value):
def __init__(self, op_result):
super().__init__(op_result)
def __str__(self): ...
def get_hier_coord(self, idx) -> Coord:
"""Return the (hierarchical) ND logical coordinate corresponding to the linear index"""
...
@property
def shape(self, *, loc=None, ip=None) -> Shape: ...
@property
def stride(self, *, loc=None, ip=None) -> Stride: ...
Tile = Union[Int, None, Layout, Tuple["Tile", ...]]
# XTuple is super set of above types
XTuple = Union[IntTuple, Shape, Stride, Coord, Tile]
Tiler = Union[Shape, Layout, Tile]
class Pointer:
"""
Abstract base class for CuTe jit function and runtime _Pointer
"""
def __extract_mlir_values__(self):
# Doesn't matter just return a value
return [self]
class Tensor(ABC):
"""
Abstract base class for CuTe jit function and runtime _Tensor
A CuTe Tensor is iterator with layout
:Examples:
Create tensor from torch.tensor with Host Runtime:
.. code-block:: python
>>> import torch
>>> from cutlass.cute.runtime import from_dlpack
>>> mA = from_dlpack(torch.tensor([1, 3, 5], dtype=torch.int32))
>>> mA.shape
(3,)
>>> mA.stride
(1,)
>>> mA.layout
(3,):(1,)
Define JIT function:
.. code-block:: python
@cute.jit
def add(a: Tensor, b: Tensor, res: Tensor): ...
Call JIT function from python:
.. code-block:: python
>>> import torch
>>> a = torch.tensor([1, 3, 5], dtype=torch.int32)
>>> b = torch.tensor([2, 4, 6], dtype=torch.int32)
>>> c = torch.zeros([3], dtype=torch.int32)
>>> mA = from_dlpack(a)
>>> mB = from_dlpack(b)
>>> mC = from_dlpack(c)
>>> add(mA, mB, mC)
>>> c
tensor([3, 7, 11], dtype=torch.int32)
"""
def __str__(self): ...
@abstractmethod
def __getitem__(self, idx) -> Union["Tensor", ir.Value, IntTuple]: ...
@abstractmethod
def __setitem__(self, idx, value): ...
@property
@abstractmethod
def element_type(self) -> Union[Type[Numeric], Type[IntTuple]]: ...
@element_type.setter
def element_type(self, new_type): ...
@property
@abstractmethod
def memspace(self) -> AddressSpace: ...
@property
@abstractmethod
def iterator(self): ...
@property
def layout(self) -> Union[Layout, "ComposedLayout"]: ...
@property
def shape(self) -> Shape: ...
def load(self, *, loc=None, ip=None) -> "TensorSSA": ...
def store(self, data: "TensorSSA", *, loc=None, ip=None): ...
def mark_layout_dynamic(self, leading_dim: int|None = None) -> "Tensor": ...
def mark_compact_shape_dynamic(
self, mode: int, stride_order: tuple[int, ...]|None = None, divisibility: int = 1
) -> "Tensor": ...
@abstractmethod
def fill(self, value: Numeric) -> None: ...
__all__ = [
"Coord",
"Numeric",
"Integer",
"Boolean",
"Int8",
"Int16",
"Int32",
"Int64",
"Uint8",
"Uint16",
"Uint32",
"Uint64",
"Float",
"Float16",
"BFloat16",
"TFloat32",
"Float32",
"Float64",
"Float8E5M2",
"Float8E4M3FN",
"Float8E4M3B11FNUZ",
"Float8E4M3",
"Float8E8M0FNU",
"Float4E2M1FN",
"Float6E2M3FN",
"Float6E3M2FN",
"IntTuple",
"Layout",
"Pointer",
"Shape",
"Stride",
"Tensor",
"Tile",
"Tiler",
"XTuple",
]
+32
View File
@@ -0,0 +1,32 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
def check_value_in(
value, possible_values: list, value_description: str, prefix=""
) -> None:
if value not in possible_values:
err_msg = prefix
if err_msg != "":
err_msg += ": "
err_msg += f"invalid {value_description}, got {value}, must be one of {possible_values}"
raise ValueError(err_msg)
def check_type_in(ty, possible_types: list, type_description: str, prefix="") -> None:
if not isinstance(ty, type):
ty = type(ty)
if ty not in possible_types:
err_msg = prefix
if err_msg != "":
err_msg += ": "
err_msg += f"invalid type for {type_description}, got {ty}, must be one of {possible_types}"
raise TypeError(err_msg)
+169
View File
@@ -0,0 +1,169 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from dataclasses import dataclass
from enum import Enum
from typing import Optional, Type, Union
from cutlass.cute.typing import (
Numeric,
Boolean,
Float,
Integer,
TFloat32,
Float8E4M3B11FNUZ,
Float8E4M3FN,
Float8E5M2,
Float8E8M0FNU,
Float4E2M1FN,
Tensor,
)
from cutlass.cute.runtime import from_dlpack
import cutlass.cute as cute
import torch
def dtype(ty: Type[Numeric]):
"""
Return the corresponding torch.dtype per the given DSL type
"""
torch_dtype = getattr(torch, ty.__name__.lower(), None)
torch_type_map = {
Boolean: torch.bool,
# TFloat32 is just alias of float32
TFloat32: torch.float32,
Float8E5M2: torch.float8_e5m2,
Float8E4M3FN: torch.float8_e4m3fn,
Float8E4M3B11FNUZ: torch.float8_e4m3fnuz,
}
if torch_dtype is None:
torch_dtype = torch_type_map.get(ty)
if torch_dtype is None:
raise TypeError(f"{ty} is not supported by torch")
return torch_dtype
@dataclass
class ScalarInitConfig:
"""Configuration for scalar initialization"""
value: float = 0.0
@dataclass
class RandomInitConfig:
"""Configuration for random initialization"""
min_val: int = -2
max_val: int = 2
@dataclass
class GaussianInitConfig:
"""Configuration for Gaussian initialization"""
mean: float = 0.0
std: float = 1.0
scale: float = 1.0
class TensorInitType(Enum):
"""Enumeration of tensor initialization types"""
SKIP = "skip"
SCALAR = "scalar"
RANDOM = "random"
GAUSSIAN = "gaussian"
def create_and_permute_torch_tensor(
shape,
dtype: "torch.dtype",
permute_order=None,
init_type: TensorInitType = TensorInitType.RANDOM,
init_config: Optional[
Union[RandomInitConfig, ScalarInitConfig, GaussianInitConfig]
] = None,
) -> "torch.Tensor":
"""
Create a torch tensor with specified shape and dtype. Optionally permute it and initialize it with specified init type and config
"""
init_dtype = torch.int32 if init_type == TensorInitType.RANDOM else torch.float32
init_torch_tensor = torch.empty(*shape, dtype=init_dtype)
if init_type == TensorInitType.SKIP:
assert init_config is None
f32_torch_tensor = init_torch_tensor
elif init_type == TensorInitType.SCALAR:
if init_config is None:
init_config = ScalarInitConfig()
else:
if not isinstance(init_config, ScalarInitConfig):
raise ValueError("init_config must be ScalarInitConfig()")
f32_torch_tensor = init_torch_tensor.fill_(init_config.value)
elif init_type == TensorInitType.RANDOM:
if init_config is None:
init_config = RandomInitConfig()
else:
if not isinstance(init_config, RandomInitConfig):
raise ValueError("init_config must be RandomInitConfig()")
f32_torch_tensor = init_torch_tensor.random_(
init_config.min_val, init_config.max_val
).to(dtype=torch.float32)
elif init_type == TensorInitType.GAUSSIAN:
if init_config is None:
init_config = GaussianInitConfig()
else:
if not isinstance(init_config, GaussianInitConfig):
raise ValueError("init_config must be GaussianInitConfig()")
f32_torch_tensor = init_torch_tensor.normal_(init_config.mean, init_config.std)
f32_torch_tensor = f32_torch_tensor * (1 << init_config.scale)
else:
raise ValueError(f"Invalid init type: {init_type}")
if permute_order is not None:
f32_torch_tensor = f32_torch_tensor.permute(permute_order)
dtype_torch_tensor = f32_torch_tensor.to(dtype=dtype)
return dtype_torch_tensor
def convert_cute_tensor(
f32_torch_tensor: "torch.Tensor",
cute_tensor: Tensor,
dtype: Type[Numeric],
is_dynamic_layout: bool = True,
) -> Tensor:
"""
Change the value of the cute tensor to make its value converted from a fp32 torch tensor.
Used for fp8 types tensor creatation now.
"""
# if torch_tensor is on cpu, create a gpu copy
if f32_torch_tensor.device.type == "cpu":
f32_torch_tensor = f32_torch_tensor.cuda()
# Fp8 type need explicit type conversion
if dtype in {
Float8E5M2,
Float8E4M3FN,
Float8E8M0FNU,
Float4E2M1FN,
}:
fp32_cute_tensor = from_dlpack(f32_torch_tensor)
if is_dynamic_layout:
fp32_cute_tensor = fp32_cute_tensor.mark_layout_dynamic(
f32_torch_tensor.dim_order()[-1]
)
# Copy and convert from f32 cute tensor to dtype cute tensor
cute.testing.convert(fp32_cute_tensor, cute_tensor)
return cute_tensor
+9
View File
@@ -0,0 +1,9 @@
# Utilities
This folder contains various utilties for kernel authoring. Specifically, the implementation of the
followings can be considered experimental and subject to breaking changes:
- static persistent tile scheduler defined in [`static_persistent_tile_scheduler.py`](./static_persistent_tile_scheduler.py)
- pipeline abstractions defined in [`pipeline.py`](./pipeline.py)
- grouped GEMM utilties defined [`grouped_gemm_tile_scheduler_helper.py`](./grouped_gemm_tile_scheduler_helper.py)
and [`tensormap_manager.py`](./tensormap_manager.py)
+78
View File
@@ -0,0 +1,78 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from .static_persistent_tile_scheduler import (
WorkTileInfo,
PersistentTileSchedulerParams,
StaticPersistentTileScheduler,
)
from .pipeline import (
Agent,
CooperativeGroup,
PipelineUserType,
PipelineState,
make_pipeline_state,
PipelineAsync,
PipelineTmaAsync,
PipelineTmaUmma,
PipelineUmmaAsync,
PipelineTmaStore,
pipeline_init_wait,
)
from .hardware_info import (
HardwareInfo,
)
from .blackwell_helpers import (
compute_epilogue_tile_shape,
get_smem_store_op,
get_tmem_load_op,
get_num_tmem_alloc_cols,
make_smem_layout_a,
make_smem_layout_b,
make_smem_layout_epi,
make_trivial_tiled_mma,
)
from .hopper_helpers import (
sm90_get_smem_store_op,
)
from .grouped_gemm_tile_scheduler_helper import (
GroupSearchResult,
GroupedGemmGroupSearchState,
GroupedGemmTileSchedulerHelper,
create_initial_search_state,
)
from .tensormap_manager import (
TensorMapUpdateMode,
TensorMapManager,
)
from .smem_allocator import SmemAllocator
from .layout import LayoutEnum
__all__ = [
"WorkTileInfo",
"PersistentTileSchedulerParams",
"StaticPersistentTileScheduler",
"TensorMapUpdateMode",
"TensorMapManager",
"GroupSearchResult",
"GroupedGemmGroupSearchState",
"create_initial_search_state",
"GroupedGemmTileSchedulerHelper",
"HardwareInfo",
]
@@ -0,0 +1,26 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from enum import Enum
class SmemCapacity(Enum):
SM80_SMEM_CAPACITY_BYTES = (164 - 1) * 1024
SM86_SMEM_CAPACITY_BYTES = (100 - 1) * 1024
SM89_SMEM_CAPACITY_BYTES = (100 - 1) * 1024
# Dictionary to map compute capability to SMEM capacity
SMEM_CAPACITY = {
"sm80": SmemCapacity.SM80_SMEM_CAPACITY_BYTES.value,
"sm86": SmemCapacity.SM86_SMEM_CAPACITY_BYTES.value,
"sm89": SmemCapacity.SM89_SMEM_CAPACITY_BYTES.value,
}
@@ -0,0 +1,910 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from enum import Enum
from math import log2, ceil
from typing import List, Type, Union, Tuple
from cutlass.cutlass_dsl import (
Float16,
BFloat16,
TFloat32,
Float32,
Uint8,
Int8,
Float8E4M3FN,
Float8E5M2,
Numeric,
NumericMeta,
dsl_user_op,
)
import cutlass.cute as cute
from cutlass.cute.nvgpu.common import CopyUniversalOp
from cutlass.cute.nvgpu.warp import StMatrix8x8x16bOp, StMatrix16x8x8bOp
from cutlass.cute.nvgpu.tcgen05 import (
MmaF16BF16Op,
MmaTF32Op,
MmaI8Op,
MmaFP8Op,
OperandSource,
OperandMajorMode,
CtaGroup,
Ld16x64bOp,
Ld16x128bOp,
Ld16x256bOp,
Ld16x32bx2Op,
Ld32x32bOp,
Repetition,
Pack,
find_tmem_tensor_col_offset,
SmemLayoutAtomKind,
make_smem_layout_atom,
tile_to_mma_shape,
is_tmem_load,
get_tmem_copy_properties,
)
from cutlass.utils.layout import LayoutEnum
@dsl_user_op
def compute_epilogue_tile_shape(
cta_tile_shape: cute.Shape,
use_2cta_instrs: bool,
layout_d: LayoutEnum,
elem_ty_d: Type[Numeric],
*,
layout_c: LayoutEnum = None,
elem_ty_c: Union[Type[Numeric], None] = None,
loc=None,
ip=None,
) -> cute.Tile:
"""Attempts to compute a reasonable epilogue tile based on block tile shape or allows the user to provide one.
:param cta_tile_shape: A tuple or list representing the dimensions of the CTA tile, where
cta_tile_shape[0] corresponds to the height (M) and cta_tile_shape[1]
corresponds to the width (N) of the tile.
:type cta_tile_shape: cute.Shape
:param use_2cta_instrs: A flag indicating whether the configuration is for a 2SM setup.
:type use_2cta_instrs: bool
:param layout_d: The layout enum of the output tensor D.
:type layout_d: LayoutEnum
:param elem_ty_d: The element type of output tensor D.
:type elem_ty_d: Type[Numeric]
:param layout_c: The layout enum of the input tensor C. Defaults to None.
:type layout_c: LayoutEnum, optional
:param elem_ty_c: The element type for input tensor C. Defaults to None.
:type elem_ty_c: Union[Type[Numeric], None], optional
:return: Returns epilog tiler, which is used in subsequent epilog partitions.
:rtype: cute.Tile
:raises ValueError: If the computed tile cute.size does not meet minimum requirements based on CTA dimensions.
"""
def validate_type(ty, ty_name):
if not isinstance(ty, NumericMeta):
raise TypeError(f"{ty_name} must be Numeric, but got {ty}")
validate_type(elem_ty_d, "elem_ty_d")
if elem_ty_c is not None:
validate_type(elem_ty_c, "elem_ty_c")
cta_m, cta_n = cta_tile_shape[:2]
(warp_m, warp_n) = (2, 2) if (cta_m == 64 and use_2cta_instrs) else (4, 1)
disable_source = elem_ty_c == None
max_bits = (
elem_ty_d.width if disable_source else max(elem_ty_c.width, elem_ty_d.width)
)
dp_full = 32
tile_m = min(cta_m, dp_full * warp_m)
n_perf = 0
if disable_source:
if max_bits == 4:
compute_elts = 8192
else:
compute_elts = 4096
n_perf = compute_elts // tile_m
else:
if max_bits == 32:
n_perf = 16 if (cta_m > 64 and cta_n <= 128) else 32
elif max_bits == 16:
n_perf = 32 if cta_n <= 128 else 64
else:
n_perf = 64
d_is_m_major = layout_d.is_m_major_c()
c_is_m_major = True if layout_c is None else layout_c.is_m_major_c()
n_min_d = (
8 * warp_n
if d_is_m_major
else (128 * warp_n if elem_ty_d.width == 6 else 128 // elem_ty_d.width * warp_n)
)
n_min_c = (
8 * warp_n
if (c_is_m_major or disable_source)
else (128 * warp_n if elem_ty_c.width == 6 else 128 // elem_ty_c.width * warp_n)
)
tile_n = min(cta_n, max(n_perf, n_min_c, n_min_d))
if cta_n < n_min_c or cta_n < n_min_d:
raise ValueError(f"CTA tile too small: {cta_tile_shape=}")
# stride by tmem warp layout and return a by-mode tiler
tile_m_layout = cute.make_layout(tile_m, loc=loc, ip=ip)
tile_n_layout = cute.make_layout(
(tile_n // warp_n, warp_n), stride=(1, cta_n // warp_n), loc=loc, ip=ip
)
return (tile_m_layout, cute.coalesce(tile_n_layout, loc=loc, ip=ip))
@dsl_user_op
def get_smem_store_op(
layout_d: LayoutEnum,
elem_ty_d: Type[Numeric],
elem_ty_acc: Type[Numeric],
tiled_tmem_load: cute.TiledCopy,
*,
loc=None,
ip=None,
) -> cute.CopyAtom:
"""Selects the largest vectorized smem store atom available subject to
constraint of gmem layout and chosen TMEM_LOAD's thread-value ownership.
:param layout_d: The layout enum of the output tensor D.
:type layout_d: LayoutEnum
:param elem_ty_d: The element type for output tensor D.
:type elem_ty_d: Type[Numeric]
:param elem_ty_acc: The element type for accumulator.
:type elem_ty_acc: Type[Numeric]
:param tiled_tmem_load: An instance of TiledCopy that represents the tmem load operation.
:type tiled_tmem_load: cute.TiledCopy
:return: Either SmemStoreMatrix or SimtSyncCopy, based on the input parameters.
:rtype: cute.CopyAtom
"""
def validate_type(ty, ty_name):
if not isinstance(ty, NumericMeta):
raise TypeError(f"{ty_name} must be a Numeric, but got {ty}")
validate_type(elem_ty_d, "elem_ty_d")
validate_type(elem_ty_acc, "elem_ty_acc")
is_m_major = layout_d.is_m_major_c()
is_n_major = layout_d.is_n_major_c()
if not is_tmem_load(tiled_tmem_load):
return cute.make_copy_atom(CopyUniversalOp(), elem_ty_d, loc=loc, ip=ip)
num_dp, num_bits, num_rep, pack = get_tmem_copy_properties(tiled_tmem_load)
use_stmatrix_m8n8_4x = (
all(
[
elem_ty_acc.width == 32,
elem_ty_d.width == 32,
is_n_major,
num_dp == 16,
num_bits == 128,
num_rep in (2, 4, 8, 16, 32, 64),
pack == Pack.NONE,
]
)
or all(
[
elem_ty_acc.width == 32,
elem_ty_d.width == 16,
num_dp == 16,
num_bits == 256,
num_rep in (2, 4, 8, 16, 32),
pack == Pack.NONE,
]
)
or all(
[
elem_ty_acc.width == 16,
elem_ty_d.width == 16,
num_dp == 16,
num_bits == 128,
num_rep in (2, 4, 8, 16, 32, 64),
pack == Pack.PACK_16b_IN_32b,
]
)
)
use_stmatrix_m16n8_4x = all(
[
elem_ty_acc.width == 32,
elem_ty_d.width == 8,
is_m_major,
num_dp == 16,
num_bits == 256,
num_rep in (4, 8, 16, 32),
pack == Pack.NONE,
]
)
use_stmatrix_m8n8_2x = (
all(
[
elem_ty_acc.width == 32,
elem_ty_d.width == 32,
is_n_major,
num_dp == 16,
num_bits == 128,
num_rep == 1,
pack == Pack.NONE,
]
)
or all(
[
elem_ty_acc.width == 32,
elem_ty_d.width == 16,
num_dp == 16,
num_bits == 256,
num_rep == 1,
pack == Pack.NONE,
]
)
or all(
[
elem_ty_acc.width == 16,
elem_ty_d.width == 16,
num_dp == 16,
num_bits == 128,
num_rep == 1,
pack == Pack.PACK_16b_IN_32b,
]
)
)
use_stmatrix_m16n8_2x = all(
[
elem_ty_acc.width == 32,
elem_ty_d.width == 8,
is_m_major,
num_dp == 16,
num_bits == 256,
num_rep == 2,
pack == Pack.NONE,
]
)
use_stmatrix_m16n8_1x = all(
[
elem_ty_acc.width == 32,
elem_ty_d.width == 8,
is_m_major,
num_dp == 16,
num_bits == 256,
num_rep == 1,
pack == Pack.NONE,
]
)
if use_stmatrix_m8n8_4x:
op = StMatrix8x8x16bOp(is_m_major, 4)
return cute.make_copy_atom(op, elem_ty_d, loc=loc, ip=ip)
elif use_stmatrix_m8n8_2x:
op = StMatrix8x8x16bOp(is_m_major, 2)
return cute.make_copy_atom(op, elem_ty_d, loc=loc, ip=ip)
elif use_stmatrix_m16n8_4x:
op = StMatrix16x8x8bOp(4)
return cute.make_copy_atom(op, elem_ty_d, loc=loc, ip=ip)
elif use_stmatrix_m16n8_2x:
op = StMatrix16x8x8bOp(2)
return cute.make_copy_atom(op, elem_ty_d, loc=loc, ip=ip)
elif use_stmatrix_m16n8_1x:
op = StMatrix16x8x8bOp(1)
return cute.make_copy_atom(op, elem_ty_d, loc=loc, ip=ip)
else:
op = CopyUniversalOp()
return cute.make_copy_atom(op, elem_ty_d, loc=loc, ip=ip)
@dsl_user_op
def get_tmem_load_op(
cta_tile_shape: cute.Shape,
layout_d: LayoutEnum,
elem_ty_d: Type[Numeric],
elem_ty_acc: Type[Numeric],
epi_tile: cute.Tile,
use_2cta_instrs: bool,
*,
loc=None,
ip=None,
) -> cute.CopyAtom:
"""Finds a performant TMEM_LOAD copy op for the selected epilogue
tile (epi_tile), element types, and tcgen05.mma instruction used.
:param cta_tile_shape: A tuple or list representing the dimensions of the CTA tile.
:type cta_tile_shape: cute.Shape
:param layout_d: The layout enum of the output tensor D.
:type layout_d: LayoutEnum
:param elem_ty_d: The element type for output tensor D.
:type elem_ty_d: Type[Numeric]
:param elem_ty_acc: The element type for accumulation.
:type elem_ty_acc: Type[Numeric]
:param epi_tile: The epilogue tile configuration.
:type epi_tile: cute.Tile
:param use_2cta_instrs: A flag indicating whether the configuration is for 2 SMs.
:type use_2cta_instrs: bool
:return: An instance of Sm100TmemLoad with the computed configuration.
:rtype: cute.CopyAtom
:raises ValueError: If the function cannot handle the given combination of accumulation
and dimension types, or if it cannot determine the appropriate configuration based on
the input parameters.
"""
is_m_major = layout_d.is_m_major_c()
acc_bits = elem_ty_acc.width
d_bits = elem_ty_d.width
tmem_warp_shape_mn = (
(2, 2) if (cta_tile_shape[0] == 64 and use_2cta_instrs) else (4, 1)
)
epilog_tile_shape_mn = cute.product_each(
cute.shape(epi_tile, loc=loc, ip=ip), loc=loc, ip=ip
)
epilog_warp_tile_shape_mn = cute.shape_div(
epilog_tile_shape_mn, tmem_warp_shape_mn, loc=loc, ip=ip
)
num_dp = cute.size(epilog_warp_tile_shape_mn[0], loc=loc, ip=ip)
if num_dp not in {16, 32}:
raise ValueError("Cta tile and 2sm config does not generate correct num dp.")
num_col_bits = cute.size(epilog_warp_tile_shape_mn[1], loc=loc, ip=ip) * acc_bits
tmem_dp = 0
tmem_bit = 0
tmem_rep = 0
tmem_pack16b = False
if acc_bits == 32 and d_bits == 32:
if num_dp == 16:
if is_m_major:
tmem_dp = 16
tmem_bit = 256
else:
tmem_dp = 16
tmem_bit = 128
else:
tmem_dp = 32
tmem_bit = 32
elif acc_bits == 32 and d_bits == 16:
if num_dp == 16:
if is_m_major:
tmem_dp = 16
tmem_bit = 256
else:
tmem_dp = 16
tmem_bit = 256
else:
if is_m_major:
tmem_dp = 16
tmem_bit = 256
else:
tmem_dp = 32
tmem_bit = 32
elif acc_bits == 32 and d_bits == 8:
if num_dp == 16:
if is_m_major:
tmem_dp = 16
tmem_bit = 256
else:
tmem_dp = 16
tmem_bit = 32
else:
if is_m_major:
tmem_dp = 16
tmem_bit = 256
else:
tmem_dp = 32
tmem_bit = 32
elif acc_bits == 16 and d_bits == 16:
tmem_pack16b = True
if num_dp == 16:
if is_m_major:
tmem_dp = 16
tmem_bit = 128
else:
tmem_dp = 16
tmem_bit = 128
else:
if is_m_major:
tmem_dp = 16
tmem_bit = 128
else:
tmem_dp = 32
tmem_bit = 32
elif acc_bits == 32 and d_bits == 6:
if not num_dp == 32:
raise ValueError("Num dp must be 32.")
tmem_dp = 32
tmem_bit = 32
elif acc_bits == 32 and d_bits == 4:
if not num_dp == 32:
raise ValueError("Num dp must be 32.")
tmem_dp = 32
tmem_bit = 32
else:
raise ValueError(
f"Can not handle acc/d type combination: {elem_ty_acc=}, {elem_ty_d=}"
)
num_bit_div = tmem_bit
if tmem_dp == 16 and tmem_bit == 32:
num_bit_div = 64
if (num_col_bits % (num_bit_div * 128) == 0) and (
(tmem_dp == 16 and tmem_bit == 64)
or (tmem_dp == 16 and tmem_bit == 32)
or (tmem_dp == 32 and tmem_bit == 32)
):
tmem_rep = 128
elif (num_col_bits % (num_bit_div * 64) == 0) and (
(tmem_dp == 16 and tmem_bit == 128)
or (tmem_dp == 16 and tmem_bit == 64)
or (tmem_dp == 16 and tmem_bit == 32)
or (tmem_dp == 32 and tmem_bit == 32)
):
tmem_rep = 64
elif num_col_bits % (num_bit_div * 32) == 0:
tmem_rep = 32
elif num_col_bits % (num_bit_div * 16) == 0:
tmem_rep = 16
elif num_col_bits % (num_bit_div * 8) == 0:
tmem_rep = 8
elif num_col_bits % (num_bit_div * 4) == 0:
tmem_rep = 4
elif num_col_bits % (num_bit_div * 2) == 0:
tmem_rep = 2
elif num_col_bits % (num_bit_div * 1) == 0:
tmem_rep = 1
else:
raise ValueError("Can not pick tmem_rep based on cta tile shape and tmem atom.")
if tmem_dp == 16 and tmem_bit == 64:
op = Ld16x64bOp(
Repetition(tmem_rep), Pack.PACK_16b_IN_32b if tmem_pack16b else Pack.NONE
)
return cute.make_copy_atom(op, elem_ty_acc, loc=loc, ip=ip)
elif tmem_dp == 16 and tmem_bit == 128:
op = Ld16x128bOp(
Repetition(tmem_rep), Pack.PACK_16b_IN_32b if tmem_pack16b else Pack.NONE
)
return cute.make_copy_atom(op, elem_ty_acc, loc=loc, ip=ip)
elif tmem_dp == 16 and tmem_bit == 256:
op = Ld16x256bOp(
Repetition(tmem_rep), Pack.PACK_16b_IN_32b if tmem_pack16b else Pack.NONE
)
return cute.make_copy_atom(op, elem_ty_acc, loc=loc, ip=ip)
elif tmem_dp == 16 and tmem_bit == 32:
op = Ld16x32bx2Op(
Repetition(tmem_rep), Pack.PACK_16b_IN_32b if tmem_pack16b else Pack.NONE
)
return cute.make_copy_atom(op, elem_ty_acc, loc=loc, ip=ip)
elif tmem_dp == 32 and tmem_bit == 32:
op = Ld32x32bOp(
Repetition(tmem_rep), Pack.PACK_16b_IN_32b if tmem_pack16b else Pack.NONE
)
return cute.make_copy_atom(op, elem_ty_acc, loc=loc, ip=ip)
else:
raise ValueError()
def get_num_tmem_alloc_cols(
tmem_tensors: Union[cute.Tensor, List[cute.Tensor]], rounding=True
) -> int:
"""Get the total number of TMEM allocation columns for the given TMEM tensors.
:param tmem_tensors: The TMEM tensors to get the number of allocation columns for.
:type tmem_tensors: Union[cute.Tensor, List[cute.Tensor]]
:param rounding: Whether to round up the number of allocation columns to the nearest power of 2.
:type rounding: bool
:return: The total number of TMEM allocation columns.
:rtype: int
:raises ValueError: If the number of TMEM allocation columns exceeds the maximum capacity of 512 or is less than 32.
"""
# Turn tmem_tensors into a list
if isinstance(tmem_tensors, cute.Tensor):
tmem_tensors = [tmem_tensors]
# For each tensor in tmem_tensors, find the tmem_tensor_col_offset
num_tmem_alloc_cols_per_tensor = [
find_tmem_tensor_col_offset(t) for t in tmem_tensors
]
# Sum up the num_tmem_alloc_cols_per_tensor
num_tmem_alloc_cols = sum(num_tmem_alloc_cols_per_tensor)
# Round up num_tmem_cols_total to the nearest power of 2
if rounding:
num_tmem_alloc_cols = 1 << ceil(log2(num_tmem_alloc_cols))
# Validate the number of TMEM allocation columns
SM100_TMEM_CAPACITY_COLUMNS = 512
SM100_TMEM_MIN_ALLOC_COLUMNS = 32
if (
num_tmem_alloc_cols > SM100_TMEM_CAPACITY_COLUMNS
or num_tmem_alloc_cols < SM100_TMEM_MIN_ALLOC_COLUMNS
):
raise ValueError(
f"TMEM allocation columns {num_tmem_alloc_cols} exceeds the maximum capacity of {SM100_TMEM_CAPACITY_COLUMNS} or less than {SM100_TMEM_MIN_ALLOC_COLUMNS}"
)
return num_tmem_alloc_cols
def get_smem_layout_atom_ab(
major_mode: OperandMajorMode,
element_type: Type[Numeric],
smem_shape_mn_k: Tuple[int, int],
*,
loc=None,
ip=None,
) -> SmemLayoutAtomKind:
"""Simple heuristics to select the optimal SMEM layout atom based on the
majorness, the data type, and the major mode size.
:param major_mode: The major mode for the SMEM tensor is K major.
:type major_mode: OperandMajorMode
:param element_type: The element type for the SMEM tensor.
:type element_type: Type[Numeric]
:param smem_shape_mn_k: The shape of the SMEM tensor.
:type smem_shape_mn_k: Tuple[int, int]
:return: The SMEM layout atom kind
:rtype: SmemLayoutAtomKind
"""
is_k_major = major_mode == OperandMajorMode.K
major_mode_size = smem_shape_mn_k[1] if is_k_major else smem_shape_mn_k[0]
assert major_mode_size % 8 == 0
sw128_num_contiguous_bits = 1024
sw64_num_contiguous_bits = 512
sw32_num_contiguous_bits = 256
inter_num_contiguous_bits = 128
major_mode_size_bits = major_mode_size * element_type.width
assert major_mode_size_bits % inter_num_contiguous_bits == 0
if not is_k_major:
if (element_type.width == 32) and (
major_mode_size_bits % sw128_num_contiguous_bits == 0
):
return SmemLayoutAtomKind.MN_SW128_32B
if major_mode_size_bits % sw128_num_contiguous_bits == 0:
return SmemLayoutAtomKind.MN_SW128
if major_mode_size_bits % sw64_num_contiguous_bits == 0:
return SmemLayoutAtomKind.MN_SW64
if major_mode_size_bits % sw32_num_contiguous_bits == 0:
return SmemLayoutAtomKind.MN_SW32
return SmemLayoutAtomKind.MN_INTER
if major_mode_size_bits % sw128_num_contiguous_bits == 0:
return SmemLayoutAtomKind.K_SW128
if major_mode_size_bits % sw64_num_contiguous_bits == 0:
return SmemLayoutAtomKind.K_SW64
if major_mode_size_bits % sw32_num_contiguous_bits == 0:
return SmemLayoutAtomKind.K_SW32
return SmemLayoutAtomKind.K_INTER
@dsl_user_op
def make_smem_layout_a(
tiled_mma: cute.TiledMma,
mma_tiler_mnk: cute.Tile,
a_dtype: Type[Numeric],
num_stages: int,
*,
loc=None,
ip=None,
) -> Union[cute.Layout, cute.ComposedLayout]:
"""This function helps with:
1. Get the partitioned shape of the A tensor based on the tiled_mma & MMA tiler.
2. Select the heuristic SMEM layout atom based on the A tensor's majorness, the data type, and the major mode size.
3. cute.Tile the SMEM layout atom to the MMA tile shape.
4. Stage the SMEM layout based on the number of stages.
:param tiled_mma: The tiled MMA used to partition tensor A
:type tiled_mma: cute.TiledMma
:param mma_tiler_mnk: The MMA tile shape
:type mma_tiler_mnk: cute.cute.Tile
:param a_dtype: The element type for tensor A
:type a_dtype: Type[Numeric]
:param num_stages: The number of pipeline stages for tensor A
:type num_stages: int
:return: SMEM layout for tensor A
:rtype: Union[cute.Layout, cute.ComposedLayout]
"""
is_k_major = tiled_mma.op.a_major_mode == OperandMajorMode.K
a_smem_shape = tiled_mma.partition_shape_A(
cute.dice(mma_tiler_mnk, (1, None, 1), loc=loc, ip=ip)
)
a_smem_shape_mn_k = (
cute.size(a_smem_shape[0][0], loc=loc, ip=ip) * a_smem_shape[1],
cute.size(a_smem_shape[0][1], loc=loc, ip=ip) * a_smem_shape[2],
)
a_smem_layout_atom = make_smem_layout_atom(
get_smem_layout_atom_ab(
tiled_mma.op.a_major_mode,
a_dtype,
a_smem_shape_mn_k,
loc=loc,
ip=ip,
),
a_dtype,
loc=loc,
ip=ip,
)
a_smem_layout_staged = tile_to_mma_shape(
a_smem_layout_atom,
cute.append(a_smem_shape, num_stages, loc=loc, ip=ip),
order=((1, 0, 2) if not is_k_major else (0, 1, 2)),
loc=loc,
ip=ip,
)
return a_smem_layout_staged
@dsl_user_op
def make_smem_layout_b(
tiled_mma: cute.TiledMma,
mma_tiler_mnk: cute.Tile,
b_dtype: Type[Numeric],
num_stages: int,
*,
loc=None,
ip=None,
) -> Union[cute.Layout, cute.ComposedLayout]:
"""This function helps:
1. Get the partitioned shape of the B tensor based on the tiled_mma & MMA tiler.
2. Select the heuristic SMEM layout atom based on the B tensor's majorness, the data type, and the major mode size.
3. cute.Tile the SMEM layout atom to the MMA tile shape.
4. Stage the SMEM layout based on the number of stages.
:param tiled_mma: The tiled MMA which is used to partition the B tensor.
:type tiled_mma: cute.TiledMma
:param mma_tiler_mnk: The MMA tile shape.
:type mma_tiler_mnk: cute.cute.Tile
:param b_dtype: The element type for the B tensor.
:type b_dtype: Type[Numeric]
:param num_stages: The stage of the B tensor.
:type num_stages: int
:return: SMEM layout for the B tensor.
:rtype: Union[cute.Layout, cute.ComposedLayout]
"""
is_k_major = tiled_mma.op.b_major_mode == OperandMajorMode.K
b_smem_shape = tiled_mma.partition_shape_B(
cute.dice(mma_tiler_mnk, (None, 1, 1), loc=loc, ip=ip)
)
b_smem_shape_nk = (
cute.size(b_smem_shape[0][0], loc=loc, ip=ip) * b_smem_shape[1],
cute.size(b_smem_shape[0][1], loc=loc, ip=ip) * b_smem_shape[2],
)
b_smem_layout_atom = make_smem_layout_atom(
get_smem_layout_atom_ab(
tiled_mma.op.b_major_mode,
b_dtype,
b_smem_shape_nk,
loc=loc,
ip=ip,
),
b_dtype,
loc=loc,
ip=ip,
)
b_smem_layout_staged = tile_to_mma_shape(
b_smem_layout_atom,
cute.append(b_smem_shape, num_stages, loc=loc, ip=ip),
order=((1, 0, 2) if not is_k_major else (0, 1, 2)),
loc=loc,
ip=ip,
)
return b_smem_layout_staged
@dsl_user_op
def get_smem_layout_atom_epi(
layout: LayoutEnum,
element_type: Type[Numeric],
epi_tile: cute.Tile,
*,
loc=None,
ip=None,
) -> SmemLayoutAtomKind:
"""Simple heuristics to select the optimal SMEM layout atom for epilog tensors.
:param layout: The layout enum for the SMEM tensor.
:type layout: LayoutEnum
:param element_type: The element type for the SMEM tensor.
:type element_type: Type[Numeric]
:param epi_tile: The epilogue tile shape.
:type epi_tile: cute.Tile
:return: The SMEM layout atom kind
:rtype: SmemLayoutAtomKind
"""
# Get the max contiguous tile usable by TMA
tma_shape = tuple(
(
# assumes get<0>(epi_tile) is coalesced and unit stride
cute.coalesce(cute.right_inverse(x, loc=loc, ip=ip), loc=loc, ip=ip).shape
if isinstance(x, cute.Layout)
else x
)
for x in epi_tile
)
if layout.is_m_major_c():
# ColMajor C/D (M-major)
return get_smem_layout_atom_ab(
OperandMajorMode.MN, element_type, tma_shape, loc=loc, ip=ip
)
else:
# RowMajor C/D (N-major)
return get_smem_layout_atom_ab(
OperandMajorMode.K, element_type, tma_shape, loc=loc, ip=ip
)
@dsl_user_op
def make_smem_layout_epi(
epi_dtype: Type[Numeric],
epi_layout: LayoutEnum,
epi_tile: cute.Tile,
epi_stage: int,
*,
loc=None,
ip=None,
) -> Union[cute.Layout, cute.ComposedLayout]:
"""This function helps:
1. Select the heuristic SMEM layout atom based on the epilog tile shape,
the epilog tensor's majorness, and the element type.
2. cute.Tile the SMEM layout atom to the epilog tile shape.
3. Stage the SMEM layout based on the number of stages.
:param epi_dtype: The element type for the epilog tensor.
:type epi_dtype: Type[Numeric]
:param epi_layout: The layout enum for the epilog tensor.
:type epi_layout: LayoutEnum
:param epi_tile: The epilogue tile shape.
:type epi_tile: cute.cute.Tile
:param epi_stage: The stage of the epilog tensor.
:type epi_stage: int
:return: SMEM layout for epilog tensors (usually C & D which are processed in the epilog)
:rtype: Union[cute.Layout, cute.ComposedLayout]
"""
epilog_shape = cute.product_each(
cute.shape(epi_tile, loc=loc, ip=ip), loc=loc, ip=ip
)
c_smem_layout_atom = make_smem_layout_atom(
get_smem_layout_atom_epi(
epi_layout,
epi_dtype,
epi_tile,
loc=loc,
ip=ip,
),
epi_dtype,
loc=loc,
ip=ip,
)
epi_smem_layout_staged = cute.tile_to_shape(
c_smem_layout_atom,
cute.append(epilog_shape, epi_stage, loc=loc, ip=ip),
order=((1, 0, 2) if not epi_layout.is_n_major_c() else (0, 1, 2)),
loc=loc,
ip=ip,
)
return epi_smem_layout_staged
class SmemCapacity(Enum):
SM100_SMEM_CAPACITY_BYTES = (228 - 1) * 1024
SM120_SMEM_CAPACITY_BYTES = (100 - 1) * 1024
# Dictionary to map compute capability to SMEM capacity
SMEM_CAPACITY = {
"sm100": SmemCapacity.SM100_SMEM_CAPACITY_BYTES.value,
"sm120": SmemCapacity.SM120_SMEM_CAPACITY_BYTES.value,
}
@dsl_user_op
def make_trivial_tiled_mma(
ab_dtype: Type[Numeric],
a_leading_mode: OperandMajorMode,
b_leading_mode: OperandMajorMode,
acc_dtype: Type[Numeric],
cta_group: CtaGroup,
mma_tiler_mn: Tuple[int, int],
a_source: OperandSource = OperandSource.SMEM,
*,
loc=None,
ip=None,
) -> cute.TiledMma:
"""Make a tiled MMA atom with given data type, leading dimension, cta group and mma tile shape.
By default, the MMA atom is created with SMEM operand source for A.
:param ab_dtype: Data type of operands A and B.
:type ab_dtype: type[Numeric]
:param a_leading_mode: Leading dimension of operand A (1 for K, 0 for M/N).
:type a_leading_mode: tcgen05.OperandMajorMode
:param b_leading_mode: Leading dimension of operand B (1 for K, 0 for M/N).
:type b_leading_mode: tcgen05.OperandMajorMode
:param acc_dtype: Data type of the accumulator.
:type acc_dtype: type[Numeric]
:param cta_group: The CTA group to use.
:type cta_group: tcgen05.CtaGroup
:param mma_tiler_mn: The shape (M, N, K) of the MMA tiler.
:type mma_tiler_mn: Tuple[int, int]
:param a_source: The source of operand A (SMEM by default or TMEM).
:type a_source: OperandSource
:return: A tiled MMA atom.
:rtype: cute.TiledMma
:raises TypeError: If the data type is not supported.
"""
if ab_dtype in {Float16, BFloat16}:
mma_op = MmaF16BF16Op(
ab_dtype,
acc_dtype,
(*mma_tiler_mn, 16),
cta_group,
a_source,
a_leading_mode,
b_leading_mode,
)
elif ab_dtype in {TFloat32, Float32}:
mma_op = MmaTF32Op(
(*mma_tiler_mn, 8),
cta_group,
a_source,
a_leading_mode,
b_leading_mode,
)
elif ab_dtype in {
Uint8,
Int8,
}:
mma_op = MmaI8Op(
ab_dtype,
(*mma_tiler_mn, 32),
cta_group,
a_source,
a_leading_mode,
b_leading_mode,
)
elif ab_dtype in {Float8E4M3FN, Float8E5M2}:
mma_op = MmaFP8Op(
ab_dtype,
acc_dtype,
(*mma_tiler_mn, 32),
cta_group,
a_source,
a_leading_mode,
b_leading_mode,
)
else:
raise TypeError(f"unsupported ab_dtype, got {ab_dtype}")
return cute.make_tiled_mma(cute.make_mma_atom(mma_op))
@@ -0,0 +1,466 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from typing import List, Tuple
import cutlass.cute as cute
from cutlass.cutlass_dsl import Int32, extract_mlir_values, new_from_mlir_values
from cutlass._mlir import ir
from cutlass.utils.static_persistent_tile_scheduler import PersistentTileSchedulerParams
class GroupSearchResult:
"""
The result of the group search for grouped gemm.
:param group_idx: The result group index
:type group_idx: Int32
:param cta_tile_idx_m: CTA tile index along M dimension after rasterization
:type cta_tile_idx_m: Int32
:param cta_tile_idx_n: CTA tile index along N dimension after rasterization
:type cta_tile_idx_n: Int32
:param problem_shape_m: The M dimension of the gemm problem
:type problem_shape_m: Int32
:param problem_shape_n: The N dimension of the gemm problem
:type problem_shape_n: Int32
:param problem_shape_k: The K dimension of the gemm problem
:type problem_shape_k: Int32
:param cta_tile_count_k: Number of tiles along K dimension
:type cta_tile_count_k: Int32
"""
def __init__(
self,
group_idx: Int32,
cta_tile_idx_m: Int32,
cta_tile_idx_n: Int32,
problem_shape_m: Int32,
problem_shape_n: Int32,
problem_shape_k: Int32,
cta_tile_count_k: Int32,
) -> None:
self.group_idx = group_idx
self.cta_tile_idx_m = cta_tile_idx_m
self.cta_tile_idx_n = cta_tile_idx_n
self.problem_shape_m = problem_shape_m
self.problem_shape_n = problem_shape_n
self.problem_shape_k = problem_shape_k
self.cta_tile_count_k = cta_tile_count_k
def __extract_mlir_values__(self) -> List[ir.Value]:
values = extract_mlir_values(self.group_idx)
values.extend(extract_mlir_values(self.cta_tile_idx_m))
values.extend(extract_mlir_values(self.cta_tile_idx_n))
values.extend(extract_mlir_values(self.problem_shape_m))
values.extend(extract_mlir_values(self.problem_shape_n))
values.extend(extract_mlir_values(self.problem_shape_k))
values.extend(extract_mlir_values(self.cta_tile_count_k))
return values
def __new_from_mlir_values__(self, values: List[ir.Value]) -> "GroupSearchResult":
assert len(values) == 7
return GroupSearchResult(*tuple(values))
class GroupedGemmGroupSearchState:
"""
The state of group index search for grouped gemm.
The state will be initialized once and updated in every round of group index search.
:param start_group_idx: The group idx to start the search with
:type start_group_idx: Int32
:param tile_count_prev_group: Number of tiles before the matched group
:type tile_count_prev_group: Int32
:param tile_count_searched: Number of tiles we have searched. When the matched group is found,
it records the number of tiles including the matched group
:type tile_count_searched: Int32
"""
def __init__(
self,
start_group_idx: Int32,
tile_count_prev_group: Int32,
tile_count_searched: Int32,
) -> None:
self.start_group_idx = start_group_idx
self.tile_count_prev_group = tile_count_prev_group
self.tile_count_searched = tile_count_searched
def __extract_mlir_values__(self) -> List[ir.Value]:
values = extract_mlir_values(self.start_group_idx)
values.extend(extract_mlir_values(self.tile_count_prev_group))
values.extend(extract_mlir_values(self.tile_count_searched))
return values
def __new_from_mlir_values__(
self, values: List[ir.Value]
) -> "GroupedGemmGroupSearchState":
start_group_idx = new_from_mlir_values(self.start_group_idx, [values[0]])
tile_count_prev_group = new_from_mlir_values(
self.tile_count_prev_group, [values[1]]
)
tile_count_searched = new_from_mlir_values(
self.tile_count_searched, [values[2]]
)
return GroupedGemmGroupSearchState(
start_group_idx, tile_count_prev_group, tile_count_searched
)
def create_initial_search_state() -> GroupedGemmGroupSearchState:
"""
Create an initial search state for grouped gemm.
:return: A new search state with initial values
:rtype: GroupedGemmGroupSearchState
"""
return GroupedGemmGroupSearchState(
start_group_idx=Int32(0),
tile_count_prev_group=Int32(0),
tile_count_searched=Int32(0),
)
class GroupedGemmTileSchedulerHelper:
"""
A helper to translate the raw block index (x, y, z) from tile scheduler to real CTA tile index for grouped gemm.
:param group_count: Number of groups in current grouped gemm problem
:type group_count: int
:param tile_sched_params: Parameter used to create the tile scheduler this helper works with
:type tile_sched_params: PersistentTileSchedulerParams
:param cluster_tile_shape_mnk: The shape of cluster tile as (m, n, k)
:type cluster_tile_shape_mnk: tuple[int, int, int]
:param search_state: The initial search state
:type search_state: GroupedGemmGroupSearchState
"""
def __init__(
self,
group_count: int,
tile_sched_params: PersistentTileSchedulerParams,
cluster_tile_shape_mnk: tuple[int, int, int],
search_state: GroupedGemmGroupSearchState,
) -> None:
self.tile_sched_params = tile_sched_params
self.group_count = group_count
self.lane_idx = cute.arch.lane_idx()
self.cluster_tile_shape_mnk = cluster_tile_shape_mnk
self.search_state = search_state
def __extract_mlir_values__(self) -> List[ir.Value]:
values = extract_mlir_values(self.tile_sched_params)
values.extend(extract_mlir_values(self.search_state))
return values
def __new_from_mlir_values__(
self, values: List[ir.Value]
) -> "GroupedGemmTileSchedulerHelper":
tile_sched_params = new_from_mlir_values(self.tile_sched_params, values)
search_state = new_from_mlir_values(self.search_state, values[1:])
return GroupedGemmTileSchedulerHelper(
self.group_count,
tile_sched_params,
self.cluster_tile_shape_mnk,
search_state,
)
def delinearize_z(
self,
cta_tile_coord: tuple,
problem_shape_mnkl: cute.Tensor,
) -> GroupSearchResult:
"""
Delinearize the linear z index and return GroupSearchResult.
This function should be used by warps that need to know the CTA tile index on M and N dimensions.
:param cta_tile_coord: The raw CTA coordinate from tile scheduler
:type cta_tile_coord: tuple of Int32
:param problem_shape_mnkl: Tensor containing gemm problem size (M, N, K, L) for each group
:type problem_shape_mnkl: cute.Tensor
:return: The search result containing group index and tile coordinates
:rtype: GroupSearchResult
"""
# delinear the z coord
linear_idx = cta_tile_coord[2]
group_idx, problem_mnkl = self._group_search_and_load_problem_shape(
linear_idx,
problem_shape_mnkl,
self.search_state.start_group_idx,
self.search_state.tile_count_prev_group,
)
# linear index local to current group
cluster_tile_idx_in_current_group = (
linear_idx - self.search_state.tile_count_prev_group
)
cluster_count_m, cluster_count_n, cluster_count_k = cute.ceil_div(
(problem_mnkl[0], problem_mnkl[1], problem_mnkl[2]),
(
self.cluster_tile_shape_mnk[0],
self.cluster_tile_shape_mnk[1],
self.cluster_tile_shape_mnk[2],
),
)
# decompose to get indices on M and N
cta_tile_idx_m, cta_tile_idx_n = self._compute_cta_tile_coord(
cluster_tile_idx_in_current_group,
cta_tile_coord,
cluster_count_m,
cluster_count_n,
)
return GroupSearchResult(
group_idx,
cta_tile_idx_m,
cta_tile_idx_n,
problem_mnkl[0],
problem_mnkl[1],
problem_mnkl[2],
cluster_count_k,
)
def search_cluster_tile_count_k(
self,
cta_tile_coord: tuple,
problem_shape_mnkl: cute.Tensor,
) -> Tuple[Int32, Int32]:
"""
Search the matched group for given linear index and compute the number of tiles along K dimension for the matched group.
This function should be used by warps that are only interested in the number of tiles along K dimension.
:param cta_tile_coord: The raw CTA coordinate from tile scheduler
:type cta_tile_coord: tuple of Int32
:param problem_shape_mnkl: Tensor containing gemm problem size (M, N, K, L) for all groups
:type problem_shape_mnkl: cute.Tensor
:return: A tuple containing cluster count along K dimension and the group index
:rtype: Tuple[Int32, Int32]
"""
group_idx, problem_mnk = self._group_search_and_load_problem_shape(
cta_tile_coord[2],
problem_shape_mnkl,
self.search_state.start_group_idx,
self.search_state.tile_count_prev_group,
)
cluster_count_k = (
problem_mnk[2] + self.cluster_tile_shape_mnk[2] - 1
) // self.cluster_tile_shape_mnk[2]
return cluster_count_k, group_idx
@cute.jit
def _prefix_sum(self, value_per_thread: Int32) -> Int32:
"""
Perform prefix sum within a full warp.
:param value_per_thread: The value for this thread to contribute to the prefix sum
:type value_per_thread: Int32
:return: The prefix sum result for this thread
:rtype: Int32
"""
clamp_value = 0
idx = 1
sum_per_thread = value_per_thread
while idx < cute.arch.WARP_SIZE:
value = cute.arch.shuffle_sync_up(
sum_per_thread, idx, mask_and_clamp=clamp_value
)
if self.lane_idx >= idx:
sum_per_thread += value
idx = idx << 1
return sum_per_thread
def _get_problem_for_group(
self, problem_shape_mnkl: cute.Tensor, group_idx: Int32
) -> cute.Tensor:
"""
Load gemm problem (m,n,k,l) for the specified group from global memory to register.
:param problem_shape_mnkl: Tensor in global memory with layout (group_count, 4):(4, 1)
:type problem_shape_mnkl: cute.Tensor
:param group_idx: The index of the group to load
:type group_idx: Int32
:return: The problem shape tensor for the specified group
:rtype: cute.Tensor
"""
cur_problem_mnkl = cute.make_fragment(
cute.make_layout(4), problem_shape_mnkl.element_type
)
cute.autovec_copy(problem_shape_mnkl[(group_idx, None)], cur_problem_mnkl)
return cur_problem_mnkl
def _get_cluster_tile_count_mn(self, problem_shape: cute.Tensor) -> Int32:
"""
Compute total cluster count.
:param problem_shape: Tensor containing problem shape (m, n, k, l)
:type problem_shape: cute.Tensor
:return: The total cluster tile count for M and N dimensions
:rtype: Int32
"""
cur_ntile_m = (
problem_shape[0] + self.cluster_tile_shape_mnk[0] - 1
) // self.cluster_tile_shape_mnk[0]
cur_ntile_n = (
problem_shape[1] + self.cluster_tile_shape_mnk[1] - 1
) // self.cluster_tile_shape_mnk[1]
cur_ntile_mn = cur_ntile_m * cur_ntile_n
return cur_ntile_mn
def _compute_cta_tile_coord(
self,
cluster_tile_idx: Int32,
cta_tile_coord_in_cluster: tuple,
cluster_tile_count_m: Int32,
cluster_tile_count_n: Int32,
) -> tuple:
"""
Compute CTA tile indices along M and N dimensions based on the linear index within a group.
It uses the AlongM mode to decompose the linear index onto M and N dimensions.
:param cluster_tile_idx: The linear index within a group
:type cluster_tile_idx: Int32
:param cta_tile_coord_in_cluster: CTA indices along M and N dimensions within a cluster
:type cta_tile_coord_in_cluster: tuple of Int32
:param cluster_tile_count_m: The number of clusters along M dimension of the matched group
:type cluster_tile_count_m: Int32
:param cluster_tile_count_n: The number of clusters along N dimension of the matched group
:type cluster_tile_count_n: Int32
:return: A tuple containing CTA tile indices along M and N dimensions
:rtype: tuple of (Int32, Int32)
"""
cluster_layout_mn = cute.make_layout(
(cluster_tile_count_m, cluster_tile_count_n)
)
(mi, ni) = cluster_layout_mn.get_hier_coord(cluster_tile_idx)
cta_tile_idx_m = (
mi * self.tile_sched_params.cluster_shape_mn[0]
+ cta_tile_coord_in_cluster[0]
)
cta_tile_idx_n = (
ni * self.tile_sched_params.cluster_shape_mn[1]
+ cta_tile_coord_in_cluster[1]
)
return (cta_tile_idx_m, cta_tile_idx_n)
@cute.jit
def _group_search(
self,
linear_idx: Int32,
problem_shape_mnkl: cute.Tensor,
init_group_idx: Int32,
init_tile_count_searched: Int32,
) -> GroupedGemmGroupSearchState:
"""
Search which group the linear index belongs to.
:param linear_idx: The linear index to be decomposed
:type linear_idx: Int32
:param problem_shape_mnkl: Tensor containing gemm problem size (M, N, K, L) for all groups
:type problem_shape_mnkl: cute.Tensor
:param init_group_idx: The group idx to start the search with
:type init_group_idx: Int32
:param init_tile_count_searched: The number of tiles we have searched
:type init_tile_count_searched: Int32
:return: The updated search state
:rtype: GroupedGemmGroupSearchState
"""
c_0 = Int32(0).ir_value()
last_lane_idx = cute.arch.WARP_SIZE - 1
tile_count_searched = init_tile_count_searched
start_group_idx = init_group_idx
not_found = linear_idx >= tile_count_searched
tile_count_prev_group = self.search_state.tile_count_prev_group
while not_found:
# get group to search for current lane
cur_group_idx = start_group_idx + self.lane_idx
# check if the group to be checked is out of range
inside_group_bound = cur_group_idx < self.group_count
cur_ntile_mn = c_0
if inside_group_bound:
# get problem size of current group
cur_problem_mnkl = self._get_problem_for_group(
problem_shape_mnkl, cur_group_idx
)
cur_ntile_mn = self._get_cluster_tile_count_mn(cur_problem_mnkl)
# compute tile count from beginning to current group(included)
total_cluster_tile_count_ps_per_thread = self._prefix_sum(cur_ntile_mn)
cluster_tile_count_end_per_thread = (
total_cluster_tile_count_ps_per_thread + tile_count_searched
)
group_not_in_window = linear_idx >= cluster_tile_count_end_per_thread
hitted_group_idx_in_search_window = cute.arch.popc(
cute.arch.vote_ballot_sync(group_not_in_window)
)
not_found = hitted_group_idx_in_search_window == cute.arch.WARP_SIZE
start_group_idx = hitted_group_idx_in_search_window + start_group_idx
hit_the_1st_problem_in_search_window = (
hitted_group_idx_in_search_window == c_0
)
tile_count_prev_group = tile_count_searched
if hit_the_1st_problem_in_search_window == False:
tile_count_prev_group = cute.arch.shuffle_sync(
cluster_tile_count_end_per_thread,
hitted_group_idx_in_search_window - 1,
)
# If no matched group, then get new_cluster_tile_count_end from last lane
# Otherwise, get new_cluster_tile_count_end from the hitted group
lane_idx_for_cluster_tile_count_end = hitted_group_idx_in_search_window
if not_found:
lane_idx_for_cluster_tile_count_end = last_lane_idx
tile_count_searched = cute.arch.shuffle_sync(
cluster_tile_count_end_per_thread,
lane_idx_for_cluster_tile_count_end,
)
return GroupedGemmGroupSearchState(
start_group_idx,
tile_count_prev_group,
tile_count_searched,
)
def _group_search_and_load_problem_shape(
self,
linear_idx: Int32,
problem_shape_mnkl: cute.Tensor,
start_group_idx: Int32,
tile_count_searched: Int32,
) -> Tuple[Int32, cute.Tensor]:
"""
Perform group search and load problem shape for the matched group.
:param linear_idx: The linear index to be decomposed
:type linear_idx: Int32
:param problem_shape_mnkl: Tensor containing gemm problem size (M, N, K, L) for all groups
:type problem_shape_mnkl: cute.Tensor
:param start_group_idx: The group idx to start the search with
:type start_group_idx: Int32
:param tile_count_searched: The number of tiles we have searched
:type tile_count_searched: Int32
:return: A tuple containing the final group index and the problem shape tensor
:rtype: Tuple[Int32, cute.Tensor]
"""
self.search_state = self._group_search(
linear_idx,
problem_shape_mnkl,
start_group_idx,
tile_count_searched,
)
# get final group search state
final_group_idx = self.search_state.start_group_idx
# let's revisit if it's better to broadcast problem_shape_mnk in group_search
problem_mnkl = self._get_problem_for_group(problem_shape_mnkl, final_group_idx)
return final_group_idx, problem_mnkl
@@ -0,0 +1,174 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from cuda.bindings import driver, nvrtc
import cutlass.cute as cute
"""
This class is used to get the hardware info of given GPU device.
It provides methods to get the max active clusters for given cluster size.
Prerequisite:
- CUDA driver is initialized via `driver.cuInit` or other CUDA APIs.
- CUDA context is created via `driver.cuCtxCreate` or other CUDA APIs.
"""
class HardwareInfo:
"""
device_id: CUDA device ID to get the hardware info.
"""
def __init__(self, device_id: int = 0):
count = self._checkCudaErrors(driver.cuDeviceGetCount())
if device_id >= count:
raise ValueError(
f"Device ID {device_id} is out of range for device count {count}"
)
self.device_id = device_id
self.device = self._checkCudaErrors(driver.cuDeviceGet(device_id))
self.context = self._checkCudaErrors(driver.cuCtxGetCurrent())
self.driver_version = self._checkCudaErrors(driver.cuDriverGetVersion())
# Getting the max active clusters for a given cluster size
def get_max_active_clusters(self, cluster_size: int) -> int:
self._get_device_function()
if self._cuda_driver_version_lt(11, 8):
raise RuntimeError(
"CUDA Driver version < 11.8, cannot get _max_active_clusters"
)
if cluster_size <= 0 or cluster_size > 32:
raise ValueError(
f"Cluster size must be between 1 and 32, {cluster_size} is not supported"
)
max_shared_memory_per_block = self._checkCudaErrors(
driver.cuDeviceGetAttribute(
driver.CUdevice_attribute.CU_DEVICE_ATTRIBUTE_MAX_SHARED_MEMORY_PER_BLOCK_OPTIN,
self.device,
)
)
self._checkCudaErrors(
driver.cuFuncSetAttribute(
self.kernel,
driver.CUfunction_attribute.CU_FUNC_ATTRIBUTE_MAX_DYNAMIC_SHARED_SIZE_BYTES,
max_shared_memory_per_block,
)
)
max_dynamic_shared_memory = self._checkCudaErrors(
driver.cuOccupancyAvailableDynamicSMemPerBlock(
self.kernel, 1, 1 # numBlocks # blockSize
)
)
max_active_blocks = self._checkCudaErrors(
driver.cuOccupancyMaxActiveBlocksPerMultiprocessor(
self.kernel, 1, max_dynamic_shared_memory # blockSize,
)
)
# allow non-portable cluster size to support detection of non-portable cluster size
self._checkCudaErrors(
driver.cuFuncSetAttribute(
self.kernel,
driver.CUfunction_attribute.CU_FUNC_ATTRIBUTE_NON_PORTABLE_CLUSTER_SIZE_ALLOWED,
1,
)
)
# prepare launch configuration
launch_config = driver.CUlaunchConfig()
launch_config.blockDimX = 128
launch_config.blockDimY = 1
launch_config.blockDimZ = 1
launch_config.sharedMemBytes = max_dynamic_shared_memory
launch_config.numAttrs = 1
# max possible cluster size is 32
cluster_dims_attr = driver.CUlaunchAttribute()
cluster_dims_attr.id = (
driver.CUlaunchAttributeID.CU_LAUNCH_ATTRIBUTE_CLUSTER_DIMENSION
)
value = driver.CUlaunchAttributeValue()
value.clusterDim.x = cluster_size
value.clusterDim.y = 1
value.clusterDim.z = 1
cluster_dims_attr.value = value
launch_config.attrs = [cluster_dims_attr]
launch_config.gridDimX = cluster_size
launch_config.gridDimY = max_active_blocks
launch_config.gridDimZ = 1
num_clusters = self._checkCudaErrors(
driver.cuOccupancyMaxActiveClusters(self.kernel, launch_config)
)
return num_clusters
def get_l2_cache_size_in_bytes(self) -> int:
return self._checkCudaErrors(
driver.cuDeviceGetAttribute(
driver.CUdevice_attribute.CU_DEVICE_ATTRIBUTE_L2_CACHE_SIZE,
self.device,
)
)
def get_device_multiprocessor_count(self) -> int:
return self._checkCudaErrors(
driver.cuDeviceGetAttribute(
driver.CUdevice_attribute.CU_DEVICE_ATTRIBUTE_MULTIPROCESSOR_COUNT,
self.device,
)
)
def _checkCudaErrors(self, result) -> None:
if result[0].value:
raise RuntimeError(
"CUDA error code={}({})".format(
result[0].value, self._cudaGetErrorEnum(result[0])
)
)
# CUDA APIs always return the status as the first element of the result tuple
if len(result) == 1:
return None
elif len(result) == 2:
return result[1]
else:
return result[1:]
def _cudaGetErrorEnum(self, error) -> str:
if isinstance(error, driver.CUresult):
err, name = driver.cuGetErrorName(error)
return name if err == driver.CUresult.CUDA_SUCCESS else "<unknown>"
elif isinstance(error, nvrtc.nvrtcResult):
return nvrtc.nvrtcGetErrorString(error)[1]
else:
raise RuntimeError("Unknown error type: {}".format(error))
def _cuda_driver_version_ge(self, major: int, minor: int) -> bool:
return self.driver_version >= (major * 1000 + 10 * minor)
def _cuda_driver_version_lt(self, major: int, minor: int) -> bool:
return not self._cuda_driver_version_ge(major, minor)
@cute.kernel
def _empty_kernel(self):
return
@cute.jit
def _host_function(self):
self._empty_kernel().launch(
grid=[1, 1, 1],
block=[1, 1, 1],
)
# get a empty kernel to compute occupancy
def _get_device_function(self) -> None:
self.compiled_kernel = cute.compile(self._host_function)
self.module = next(iter(self.compiled_kernel.cuda_modules.modules)).cuda_module
self.kernel = next(iter(self.compiled_kernel.cuda_modules.modules)).kernel_ptr
@@ -0,0 +1,195 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from typing import Type, Tuple
from enum import Enum
from cutlass.utils.layout import LayoutEnum
from cutlass.cutlass_dsl import (
Float16,
BFloat16,
Float8E5M2,
Float8E4M3FN,
Numeric,
NumericMeta,
dsl_user_op,
)
import cutlass
import cutlass.cute as cute
from cutlass.cute.nvgpu.common import CopyUniversalOp
from cutlass.cute.nvgpu.warp import StMatrix8x8x16bOp
from cutlass.cute.nvgpu.warpgroup import (
MmaF16BF16Op,
MmaF8Op,
OperandMajorMode,
OperandSource,
)
@dsl_user_op
def sm90_get_smem_store_op(
layout_d: LayoutEnum,
elem_ty_d: Type[Numeric],
elem_ty_acc: Type[Numeric],
*,
loc=None,
ip=None,
) -> cute.CopyAtom:
"""
Selects the largest vectorized smem store atom available subject to constraint of gmem layout.
Parameters:
-----------
layout_d : LayoutEnum
The layout enum of the output tensor D.
elem_ty_d : Type[Numeric]
The element type for output tensor D.
elem_ty_acc : Type[Numeric]
The element type for accumulator.
Returns:
--------
Either SmemStoreMatrix or SimtSyncCopy, based on the input parameters.
"""
def validate_type(ty, ty_name):
if not isinstance(ty, NumericMeta):
raise TypeError(f"{ty_name} must be a Numeric, but got {ty}")
validate_type(elem_ty_d, "elem_ty_d")
validate_type(elem_ty_acc, "elem_ty_acc")
is_m_major = layout_d.is_m_major_c()
if elem_ty_d.width == 16:
return cute.make_copy_atom(
StMatrix8x8x16bOp(is_m_major, 4), elem_ty_d, loc=loc, ip=ip
)
else:
return cute.make_copy_atom(CopyUniversalOp(), elem_ty_d, loc=loc, ip=ip)
class SmemCapacity(Enum):
SM90_SMEM_CAPACITY_BYTES = (228 - 1) * 1024
# Dictionary to map compute capability to SMEM capacity
SMEM_CAPACITY = {
"sm90": SmemCapacity.SM90_SMEM_CAPACITY_BYTES.value,
}
def make_trivial_tiled_mma(
a_dtype: Type[Numeric],
b_dtype: Type[Numeric],
a_leading_mode: OperandMajorMode,
b_leading_mode: OperandMajorMode,
acc_dtype: Type[Numeric],
atom_layout_mnk: Tuple[int, int, int],
tiler_mn: Tuple[int, int],
) -> cute.TiledMma:
"""Make a tiled MMA atom with given data type, leading dimension, cta group and mma tile shape.
By default, the MMA atom is created with SMEM operand source for A.
:param a_dtype: Data type of operand A.
:type a_dtype: type[Numeric]
:param b_dtype: Data type of operand B.
:type b_dtype: type[Numeric]
:param a_leading_mode: Leading dimension of operand A (1 for K, 0 for M/N).
:type a_leading_mode: warpgroup.OperandMajorMode
:param b_leading_mode: Leading dimension of operand B (1 for K, 0 for M/N).
:type b_leading_mode: warpgroup.OperandMajorMode
:param acc_dtype: Data type of the accumulator.
:type acc_dtype: type[Numeric]
:param atom_layout_mnk: A integer tuple describing the tiling of Atom across threads.
:type atom_layout_mnk: Tuple[int, int, int]
:param tiler_mn: The shape (M, N) of the cta tiler.
:type tiler_mn: Tuple[int, int]
:return: A tiled MMA atom.
:rtype: cute.TiledMma
:raises TypeError: If the data type is not supported.
"""
if a_dtype in {Float16, BFloat16}:
if cutlass.const_expr(a_dtype != b_dtype):
raise TypeError(f"Type mismatch: {a_dtype} != {b_dtype}")
if cutlass.const_expr(a_dtype.width != b_dtype.width):
raise TypeError(f"Type width mismatch: {a_dtype.width} != {b_dtype.width}")
mma_op = MmaF16BF16Op(
a_dtype,
acc_dtype,
(*tiler_mn, 16),
OperandSource.SMEM,
a_leading_mode,
b_leading_mode,
)
elif a_dtype in {Float8E4M3FN, Float8E5M2} and b_dtype in {
Float8E4M3FN,
Float8E5M2,
}:
mma_op = MmaF8Op(
a_dtype,
b_dtype,
acc_dtype,
(*tiler_mn, 32),
OperandSource.SMEM,
a_leading_mode,
b_leading_mode,
)
else:
raise TypeError(f"unsupported a_dtype and b_dtype, got {a_dtype} and {b_dtype}")
return cute.make_tiled_mma(cute.make_mma_atom(mma_op), atom_layout_mnk)
def get_smem_layout_atom(
layout: LayoutEnum,
element_type: Type[Numeric],
major_mode_size: int,
*,
loc=None,
ip=None,
):
"""Select the optimal shared memory layout atom based on parameters.
:param layout: Layout enum of the tensor
:type layout: LayoutEnum
:param element_type: Data type of the elements
:type element_type: type[cutlass.Numeric]
:param major_mode_size: Size of the major mode dimension
:type major_mode_size: int
:return: Selected shared memory layout atom kind
:rtype: cute.nvgpu.warpgroup.SmemLayoutAtomKind
"""
assert major_mode_size % 8 == 0
sw128_num_contiguous_bits = 1024
sw64_num_contiguous_bits = 512
sw32_num_contiguous_bits = 256
major_mode_size_bits = major_mode_size * element_type.width
if layout.sm90_mma_major_mode() == OperandMajorMode.MN:
if major_mode_size_bits % sw128_num_contiguous_bits == 0:
return cute.nvgpu.warpgroup.SmemLayoutAtomKind.MN_SW128
if major_mode_size_bits % sw64_num_contiguous_bits == 0:
return cute.nvgpu.warpgroup.SmemLayoutAtomKind.MN_SW64
if major_mode_size_bits % sw32_num_contiguous_bits == 0:
return cute.nvgpu.warpgroup.SmemLayoutAtomKind.MN_SW32
return cute.nvgpu.warpgroup.SmemLayoutAtomKind.MN_INTER
if major_mode_size_bits % sw128_num_contiguous_bits == 0:
return cute.nvgpu.warpgroup.SmemLayoutAtomKind.K_SW128
if major_mode_size_bits % sw64_num_contiguous_bits == 0:
return cute.nvgpu.warpgroup.SmemLayoutAtomKind.K_SW64
if major_mode_size_bits % sw32_num_contiguous_bits == 0:
return cute.nvgpu.warpgroup.SmemLayoutAtomKind.K_SW32
return cute.nvgpu.warpgroup.SmemLayoutAtomKind.K_INTER
+68
View File
@@ -0,0 +1,68 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from enum import Enum
import cutlass.cute as cute
from cutlass.cute.nvgpu import warpgroup
from cutlass.cute.nvgpu import tcgen05
class LayoutEnum(Enum):
ROW_MAJOR = "row_major"
COL_MAJOR = "col_major"
def mma_major_mode(self):
return (
tcgen05.OperandMajorMode.K
if self == LayoutEnum.ROW_MAJOR
else tcgen05.OperandMajorMode.MN
)
def sm90_mma_major_mode(self):
return (
warpgroup.OperandMajorMode.K
if self == LayoutEnum.ROW_MAJOR
else warpgroup.OperandMajorMode.MN
)
def is_k_major_a(self):
return self == LayoutEnum.ROW_MAJOR
def is_m_major_a(self):
return self == LayoutEnum.COL_MAJOR
def is_k_major_b(self):
return self == LayoutEnum.COL_MAJOR
def is_n_major_b(self):
return self == LayoutEnum.ROW_MAJOR
def is_n_major_c(self):
return self == LayoutEnum.ROW_MAJOR
def is_m_major_c(self):
return self == LayoutEnum.COL_MAJOR
@staticmethod
def from_tensor(tensor: cute.Tensor) -> "LayoutEnum":
ret = None
if tensor.leading_dim == 1:
ret = LayoutEnum.ROW_MAJOR
elif tensor.leading_dim == 0:
ret = LayoutEnum.COL_MAJOR
else:
raise ValueError(f"Invalid leading dimension: {tensor.leading_dim}")
return ret
__all__ = ["LayoutEnum"]
+984
View File
@@ -0,0 +1,984 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
import enum
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional
from cutlass.cutlass_dsl import Boolean, Int32, Int64, T, if_generate, and_, or_
import cutlass._mlir.dialects.cute as _cute_ir
import cutlass.cute as cute
##############################################################################
# Agent class
##############################################################################
class Agent(enum.Enum):
"""
Agent indicates what is participating in the pipeline synchronization.
"""
# Arbitrary grouping of N threads
Thread = enum.auto()
# Same as AsyncThread, but includes all threads in the block
ThreadBlock = enum.auto()
# Same as AsyncThread, but includes all threads in the cluster
ThreadBlockCluster = enum.auto()
class CooperativeGroup:
"""
CooperativeGroup contains size and alignment restrictions for an Agent.
"""
def __init__(self, agent: Agent, size: int = 1, alignment: int = 1):
if agent is Agent.Thread:
assert size > 0
if size == 32:
assert (
size == alignment
), "Error: Alignment does not match number of threads in a warp."
elif size == 128:
assert (
size == alignment
), "Error: Alignment does not match number of threads in a warpgroup."
elif agent is Agent.ThreadBlock:
assert False, "Error: Not yet supported."
elif agent is Agent.ThreadBlockCluster:
assert False, "Error: Not yet supported."
else:
# Should never reach this state
size = 0
if size <= 0:
raise ValueError(
"Error: The number of threads in a CooperativeGroup must be more than 0."
)
# Size indicates how many threads are participating in this CooperativeGroup
self.size = size
# Agent indicates the type of thread group
self.agent = agent
class _PipelineOp(enum.Enum):
"""
PipelineOp assigns an operation to an agent corresponding to a specific hardware feature.
"""
# async-threads
AsyncThread = enum.auto()
# Blackwell (SM100a) MMA instruction
TCGen05Mma = enum.auto()
# Tensor Memory Accelerator load
TmaLoad = enum.auto()
# TMA Store consuming smem produced by AsyncThread
TmaStore = enum.auto()
def _get_pipeline_op(type_str):
return _PipelineOp(type_str)
##############################################################################
# SyncObjectArray class
##############################################################################
class SyncObjectArray(ABC):
"""
SyncObjectArray is an abstract base class for different types of hardware synchronizations (e.g. smem barriers, named barriers, fences)
"""
@abstractmethod
def wait(self):
pass
@abstractmethod
def arrive(self):
pass
@abstractmethod
def get_barrier(self):
pass
class MbarrierArray(SyncObjectArray):
"""
MbarrierArray implements an abstraction for an array of smem barriers.
"""
def __init__(
self,
barrier_storage: cute.Pointer,
num_stages: int,
agent: tuple[_PipelineOp, CooperativeGroup],
tx_count: int = 0,
):
self.barrier_storage = barrier_storage
self.tx_count = tx_count
self.num_stages = num_stages
self.op_type, self.cg = agent
self.arrive_count = self.cg.size
if self.num_stages <= 0:
raise ValueError("Error: Mbarrier stage count must be greater than 0.")
if self.arrive_count <= 0:
raise ValueError("Error: Mbarrier arrive count must be greater than 0.")
if self.op_type is _PipelineOp.TmaLoad and self.tx_count <= 0:
raise ValueError(
"Error: Mbarrier tx count must be greater than 0 for TMA ops."
)
# Using a tensor to store mbarrier i64 ptrs
self.mbarrier_array = cute.make_fragment(cute.make_layout(num_stages), Int64)
for i in range(num_stages):
self.mbarrier_array[i] = _cute_ir.ptrtoint(
T.i64(), (self.barrier_storage + i).value
)
# Mbarrier initialization in constructor
self.mbarrier_init()
# Mbarrier initialization
def mbarrier_init(self):
"""
Initializes an array of mbarriers using warp 0.
"""
def then_body():
for index in range(self.num_stages):
cute.arch.mbarrier_init_arrive_cnt(
_mbarrier_i64_to_ptr(self.mbarrier_array[index]), self.arrive_count
)
warp_idx = cute.arch.warp_idx()
warp_idx = cute.arch.make_warp_uniform(warp_idx)
if_generate(warp_idx == 0, then_body)
def arrive(self, index: int, dst: int):
"""
Select the arrive corresponding to this MbarrierArray's PipelineOp
:param index: Index of the mbarrier in the array to arrive on
:type index: int
:param dst: Destination parameter for selective arrival, which can be either a mask or destination cta rank. When None, both TCGen05Mma and AsyncThread will arrive on their local mbarrier.
- For TCGen05Mma, dst serves as a multicast mask (e.g., 0b1011 allows arrive signal to be multicast to CTAs in the cluster with rank = 0, 1, and 3).
- For AsyncThread, dst serves as a destination cta rank (e.g., 3 means threads will arrive on the mbarrier with rank = 3 in the cluster).
:type dst: int | None
"""
if self.op_type is _PipelineOp.AsyncThread:
self.arrive_mbarrier(index, dst)
elif self.op_type is _PipelineOp.TCGen05Mma:
self.arrive_tcgen05mma(index, dst)
elif self.op_type in [_PipelineOp.TmaLoad]:
self.arrive_and_expect_tx(index, self.tx_count)
else:
print(_get_pipeline_op(self.op_type))
assert False, "Error: MbarrierArray is not supported for this PipelineOp."
def arrive_mbarrier(self, index: int, dst_rank: int):
if dst_rank is None:
cute.arch.mbarrier_arrive(_mbarrier_i64_to_ptr(self.mbarrier_array[index]))
else:
cute.arch.mbarrier_arrive(
_mbarrier_i64_to_ptr(self.mbarrier_array[index]), dst_rank
)
def arrive_tcgen05mma(self, index: int, mask: int):
if mask is None:
with cute.arch.elect_one():
cute.nvgpu.tcgen05.commit(
_mbarrier_i64_to_ptr(self.mbarrier_array[index])
)
else:
with cute.arch.elect_one():
cute.nvgpu.tcgen05.commit(
_mbarrier_i64_to_ptr(self.mbarrier_array[index]),
mask,
cute.nvgpu.tcgen05.CtaGroup.TWO,
)
def arrive_and_expect_tx(self, index: int, tx_count: int):
with cute.arch.elect_one():
cute.arch.mbarrier_init_tx_bytes(
_mbarrier_i64_to_ptr(self.mbarrier_array[index]), tx_count
)
def try_wait(self, index: int, phase: int):
return cute.arch.mbarrier_try_wait(
_mbarrier_i64_to_ptr(self.mbarrier_array[index]), phase
)
def wait(self, index: int, phase: int):
cute.arch.mbarrier_wait(_mbarrier_i64_to_ptr(self.mbarrier_array[index]), phase)
def get_barrier(self, index: int) -> cute.Pointer:
return _mbarrier_i64_to_ptr(self.mbarrier_array[index])
class TmaStoreFence(SyncObjectArray):
"""
TmaStoreFence is used for a multi-stage epilogue buffer.
"""
def __init__(
self,
num_stages: int = 0,
):
if num_stages <= 0:
raise ValueError("Mbarrier stage count must be greater than 0.")
self.num_stages = num_stages
def arrive(self):
cute.arch.cp_async_bulk_commit_group()
def wait(self):
cute.arch.cp_async_bulk_wait_group(self.num_stages - 1, read=True)
# TmaStoreFence doesn't have mbarriers
def get_barrier(self):
assert (
False
), "Error: TmaStoreFence doesn't use mbarriers and cannot return a barrier."
def tail(self):
cute.arch.cp_async_bulk_wait_group(0, read=True)
##############################################################################
# PipelineState class
##############################################################################
class PipelineUserType(enum.Enum):
Producer = enum.auto()
Consumer = enum.auto()
class PipelineState:
"""
Pipeline state contains an index and phase bit corresponding to the current position in the circular buffer.
"""
def __init__(self, stages: int, count, index, phase):
self._stages = stages
self._count = count
self._index = index
self._phase = phase
def clone(self) -> "PipelineState":
return PipelineState(self.stages, self._count, self.index, self.phase)
@property
def index(self) -> Int32:
return self._index
@property
def count(self) -> Int32:
return self._count
@property
def stages(self) -> int:
return self._stages
@property
def phase(self) -> Int32:
return self._phase
def reset_count(self):
self._count = Int32(0)
def advance(self):
self._index += 1
self._count += 1
def then_body(index, phase):
new_index = Int32(0)
new_phase = phase ^ 1
return new_index, new_phase
def else_body(index, phase):
return index, phase
self._index, self._phase = if_generate(
self._index == self.stages,
then_body,
else_body,
[self.index, self.phase],
[Int32, Int32],
)
def reverse(self):
self._index -= 1
self._count -= 1
def then_body(index, phase):
new_index = Int32(self.stages - 1)
new_phase = phase ^ 1
return new_index, new_phase
def else_body(index, phase):
return index, phase
self._index, self._phase = if_generate(
self._index == -1,
then_body,
else_body,
[self.index, self.phase],
[Int32, Int32],
)
def __get_mlir_types__(self):
return [self._count.type, self._index.type, self._phase.type]
def __extract_mlir_values__(self):
count = self._count
index = self._index
phase = self._phase
return [count.ir_value(), index.ir_value(), phase.ir_value()]
# This can be overridden by derived classes
def __new_from_mlir_values__(self, values):
return PipelineState(
self.stages, Int32(values[0]), Int32(values[1]), Int32(values[2])
)
def make_pipeline_state(type: PipelineUserType, stages: int):
"""
Creates a pipeline state. Producers are assumed to start with an empty buffer and have a flipped phase bit of 1.
"""
if type is PipelineUserType.Producer:
return PipelineState(
stages,
Int32(0),
Int32(0),
Int32(1),
)
elif type is PipelineUserType.Consumer:
return PipelineState(
stages,
Int32(0),
Int32(0),
Int32(0),
)
else:
assert (
False
), "Error: invalid PipelineUserType specified for make_pipeline_state."
##############################################################################
# Pipeline classes
##############################################################################
@dataclass(frozen=True)
class PipelineAsync:
"""
PipelineAsync is a generic pipeline class where both the producer and consumer are
AsyncThreads. It also serves as a base class for specialized pipeline classes.
"""
sync_object_array_full: SyncObjectArray
sync_object_array_empty: SyncObjectArray
num_stages: Int32
producer_mask: Int32
consumer_mask: Int32
@staticmethod
def _make_sync_object_array(
barrier_storage: cute.Pointer,
num_stages: Int32,
agent: tuple[_PipelineOp, CooperativeGroup],
tx_count: int = 0,
) -> SyncObjectArray:
"""
Returns a SyncObjectArray corresponding to an agent's PipelineOp.
"""
if agent[0] in [
_PipelineOp.AsyncThread,
_PipelineOp.TmaLoad,
_PipelineOp.TCGen05Mma,
]:
return MbarrierArray(
barrier_storage=barrier_storage,
num_stages=num_stages,
agent=agent,
tx_count=tx_count,
)
elif agent[0] is _PipelineOp.TmaStore:
# Path taken for AsyncTmaStore
return TmaStoreFence(num_stages=num_stages)
else:
assert False, "Error: Invalid PipelineOp specified."
@staticmethod
def create(
barrier_storage: cute.Pointer,
num_stages: Int32,
producer_group: CooperativeGroup,
consumer_group: CooperativeGroup,
producer_mask: Int32 = None,
consumer_mask: Int32 = None,
):
"""
This helper function computes any necessary attributes and returns an instance of PipelineAsync.
:param barrier_storage: Pointer to the smem address for this pipeline's mbarriers
:type barrier_storage: cute.Pointer
:param num_stages: Number of buffer stages for this pipeline
:type num_stages: Int32
:param producer_group: CooperativeGroup for the producer agent
:type producer_group: CooperativeGroup
:param consumer_group: CooperativeGroup for the consumer agent
:type consumer_group: CooperativeGroup
:param producer_mask: Mask for signaling arrives for the producer agent
:type producer_mask: Int32 | None
:param consumer_mask: Mask for signaling arrives for the consumer agent
:type consumer_mask: Int32 | None
"""
producer_type = _PipelineOp.AsyncThread
consumer_type = _PipelineOp.AsyncThread
producer = (producer_type, producer_group)
consumer = (consumer_type, consumer_group)
sync_object_array_full = PipelineAsync._make_sync_object_array(
barrier_storage.align(min_align=8), num_stages, producer
)
sync_object_array_empty = PipelineAsync._make_sync_object_array(
barrier_storage.align(min_align=8) + num_stages, num_stages, consumer
)
pipeline_init_wait()
return PipelineAsync(
sync_object_array_full,
sync_object_array_empty,
num_stages,
producer_mask,
consumer_mask,
)
def producer_acquire(
self, state: PipelineState, try_acquire_token: Optional[Boolean] = None
):
if_generate(
try_acquire_token is None or try_acquire_token == 0,
lambda: self.sync_object_array_empty.wait(state.index, state.phase),
)
def producer_try_acquire(self, state: PipelineState):
return self.sync_object_array_empty.try_wait(state.index, state.phase)
def producer_commit(self, state: PipelineState):
self.sync_object_array_full.arrive(state.index, self.producer_mask)
def consumer_wait(
self, state: PipelineState, try_wait_token: Optional[Boolean] = None
):
if_generate(
try_wait_token is None or try_wait_token == 0,
lambda: self.sync_object_array_full.wait(state.index, state.phase),
)
def consumer_try_wait(self, state: PipelineState):
return self.sync_object_array_full.try_wait(state.index, state.phase)
def consumer_release(self, state: PipelineState):
self.sync_object_array_empty.arrive(state.index, self.consumer_mask)
def producer_get_barrier(self, state: PipelineState) -> cute.Pointer:
return self.sync_object_array_full.get_barrier(state.index)
def producer_tail(self, state: PipelineState):
"""
Make sure the last used buffer empty signal is visible to producer.
Producer tail is usually executed by producer before exit, to avoid dangling
mbarrier arrive signals after kernel exit.
:param state: The pipeline state that points to next useful buffer
:type state: PipelineState
"""
# Assume state contains that next useful buffer
# So we only need to advance to num_stages - 1 times to last used buffer
for i in range(self.num_stages - 1):
state.advance()
self.producer_acquire(state)
@dataclass(frozen=True)
class PipelineTmaAsync(PipelineAsync):
"""
PipelineTmaAsync is used for TMA producers and AsyncThread consumers (e.g. Hopper mainloops).
"""
is_signalling_thread: bool
@staticmethod
def init_empty_barrier_arrive_signal(cta_layout_vmnk: cute.Layout):
"""
Initialize the empty barrier arrive signal
This function returns the destination cta rank and a boolean indicating if the signalling thread is the same as the current thread
"""
# Logic to optimally schedule Empty Arrives
cluster_shape_mnk = cta_layout_vmnk.shape
tidx, _, _ = cute.arch.thread_idx()
cta_rank_in_cluster = cute.arch.make_warp_uniform(
cute.arch.block_idx_in_cluster()
)
is_signalling_thread = tidx < cute.size(cluster_shape_mnk)
dst_rank = tidx % cute.size(cluster_shape_mnk)
m = cluster_shape_mnk[0]
# Check if same row
is_same_row_l = dst_rank % m
is_same_row_r = cta_rank_in_cluster % m
is_same_row = is_same_row_l == is_same_row_r
# Check if same column
is_same_col_l = dst_rank // m
is_same_col_r = cta_rank_in_cluster // m
is_same_col = is_same_col_l == is_same_col_r
is_same_row_or_col = or_(is_same_row, is_same_col)
is_signalling_thread_final = and_(is_signalling_thread, is_same_row_or_col)
return dst_rank, is_signalling_thread_final
@staticmethod
def create(
barrier_storage: cute.Pointer,
num_stages: Int32,
producer_group: CooperativeGroup,
consumer_group: CooperativeGroup,
tx_count: int,
cta_layout_vmnk: Optional[cute.Layout] = None,
):
"""
This helper function computes any necessary attributes and returns an instance of PipelineTmaAsync.
:param barrier_storage: Pointer to the smem address for this pipeline's mbarriers
:type barrier_storage: cute.Pointer
:param num_stages: Number of buffer stages for this pipeline
:type num_stages: Int32
:param producer_group: CooperativeGroup for the producer agent
:type producer_group: CooperativeGroup
:param consumer_group: CooperativeGroup for the consumer agent
:type consumer_group: CooperativeGroup
:param tx_count: Number of bytes expected to be written to the transaction barrier for one stage
:type tx_count: int
:param cta_layout_vmnk: Layout of the cluster shape
:type cta_layout_vmnk: cute.Layout | None
"""
producer_type = _PipelineOp.TmaLoad
consumer_type = _PipelineOp.AsyncThread
producer = (producer_type, producer_group)
consumer = (consumer_type, consumer_group)
sync_object_array_full = PipelineAsync._make_sync_object_array(
barrier_storage.align(min_align=8), num_stages, producer, tx_count
)
sync_object_array_empty = PipelineAsync._make_sync_object_array(
barrier_storage.align(min_align=8) + num_stages, num_stages, consumer
)
dst_rank, is_signalling_thread = (
PipelineTmaAsync.init_empty_barrier_arrive_signal(cta_layout_vmnk)
)
if cta_layout_vmnk is None or cute.size(cta_layout_vmnk) == 1:
dst_rank = None
else:
dst_rank = dst_rank
is_signalling_thread = is_signalling_thread
producer_mask = None
pipeline_init_wait(cta_layout_vmnk)
return PipelineTmaAsync(
sync_object_array_full,
sync_object_array_empty,
num_stages,
producer_mask,
dst_rank,
is_signalling_thread,
)
def producer_acquire(
self, state: PipelineState, try_acquire_token: Optional[Boolean] = None
):
"""
TMA producer commit conditionally waits on buffer empty and sets the transaction barrier for leader threadblocks.
"""
if_generate(
try_acquire_token is None or try_acquire_token == 0,
lambda: self.sync_object_array_empty.wait(state.index, state.phase),
)
self.sync_object_array_full.arrive(state.index, self.producer_mask)
def producer_commit(self, state: PipelineState):
"""
TMA producer commit is a NOP. The transaction barrier signals the commit upon completion of the TMA.
"""
pass
def consumer_release(self, state: PipelineState):
"""
TMA consumer release conditionally signals the empty buffer to the producer.
"""
if_generate(
self.is_signalling_thread,
lambda: self.sync_object_array_empty.arrive(
state.index, self.consumer_mask
),
)
@dataclass(frozen=True)
class PipelineTmaUmma(PipelineAsync):
"""
PipelineTmaUmma is used for TMA producers and UMMA consumers (e.g. Blackwell mainloops).
"""
is_leader_cta: bool
@staticmethod
def _compute_mcast_arrival_mask(cta_layout_vmnk: cute.Layout):
"""
Computes a mask for signaling arrivals to multicasting threadblocks.
"""
cta_rank_in_cluster = cute.arch.make_warp_uniform(
cute.arch.block_idx_in_cluster()
)
cta_in_cluster_coord_vmnk = cta_layout_vmnk.get_flat_coord(cta_rank_in_cluster)
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
)
block_in_cluster_coord_vmnk_peer = (
cta_in_cluster_coord_vmnk[0] ^ 1,
*cta_in_cluster_coord_vmnk[1:],
)
tma_mcast_mask_a_peer = cute.nvgpu.cpasync.create_tma_multicast_mask(
cta_layout_vmnk, block_in_cluster_coord_vmnk_peer, mcast_mode=2
)
tma_mcast_mask_b_peer = cute.nvgpu.cpasync.create_tma_multicast_mask(
cta_layout_vmnk, block_in_cluster_coord_vmnk_peer, mcast_mode=1
)
return (
tma_mcast_mask_a
| tma_mcast_mask_b
| tma_mcast_mask_a_peer
| tma_mcast_mask_b_peer
)
@staticmethod
def _compute_is_leader_cta(cta_layout_vmnk: cute.Layout):
"""
Computes leader threadblocks for 2CTA kernels. For 1CTA, all threadblocks are leaders.
"""
bidx, bidy, _ = cute.arch.block_idx()
mma_coord_vmnk = (
bidx % cute.size(cta_layout_vmnk, mode=[0]),
bidx // cute.size(cta_layout_vmnk, mode=[0]),
bidy,
None,
)
return mma_coord_vmnk[0] == 0
@staticmethod
def create(
barrier_storage: cute.Pointer,
num_stages: Int32,
producer_group: CooperativeGroup,
consumer_group: CooperativeGroup,
tx_count: int,
cta_layout_vmnk: Optional[cute.Layout] = None,
):
"""
This helper function computes any necessary attributes and returns an instance of PipelineTmaUmma.
:param barrier_storage: Pointer to the smem address for this pipeline's mbarriers
:type barrier_storage: cute.Pointer
:param num_stages: Number of buffer stages for this pipeline
:type num_stages: Int32
:param producer_group: CooperativeGroup for the producer agent
:type producer_group: CooperativeGroup
:param consumer_group: CooperativeGroup for the consumer agent
:type consumer_group: CooperativeGroup
:param tx_count: Number of bytes expected to be written to the transaction barrier for one stage
:type tx_count: int
:param cta_layout_vmnk: Layout of the cluster shape
:type cta_layout_vmnk: cute.Layout | None
"""
producer_type = _PipelineOp.TmaLoad
consumer_type = _PipelineOp.TCGen05Mma
producer = (producer_type, producer_group)
consumer = (consumer_type, consumer_group)
sync_object_array_full = PipelineAsync._make_sync_object_array(
barrier_storage.align(min_align=8), num_stages, producer, tx_count
)
sync_object_array_empty = PipelineAsync._make_sync_object_array(
barrier_storage.align(min_align=8) + num_stages, num_stages, consumer
)
if cta_layout_vmnk is None or cute.size(cta_layout_vmnk) == 1:
# No mcast mask if not using clusters
producer_mask = None
# All threadblocks are leaders if not using clusters
is_leader_cta = True
else:
producer_mask = PipelineTmaUmma._compute_mcast_arrival_mask(cta_layout_vmnk)
is_leader_cta = PipelineTmaUmma._compute_is_leader_cta(cta_layout_vmnk)
consumer_mask = producer_mask
pipeline_init_wait(cta_layout_vmnk)
return PipelineTmaUmma(
sync_object_array_full,
sync_object_array_empty,
num_stages,
producer_mask,
consumer_mask,
is_leader_cta,
)
def producer_acquire(
self, state: PipelineState, try_acquire_token: Optional[Boolean] = None
):
"""
TMA producer commit conditionally waits on buffer empty and sets the transaction barrier for leader threadblocks.
"""
if_generate(
try_acquire_token is None or try_acquire_token == 0,
lambda: self.sync_object_array_empty.wait(state.index, state.phase),
)
if_generate(
self.is_leader_cta,
lambda: self.sync_object_array_full.arrive(state.index, self.producer_mask),
)
def producer_commit(self, state: PipelineState):
"""
TMA producer commit is a NOP. The transaction barrier signals the commit upon completion of the TMA.
"""
pass
@dataclass(frozen=True)
class PipelineUmmaAsync(PipelineAsync):
"""
PipelineTmaUmma is used for UMMA producers and AsyncThread consumers (e.g. Blackwell accumulator pipelines).
"""
@staticmethod
def _compute_tmem_sync_mask(cta_layout_vmnk: cute.Layout):
"""
Computes a mask to signal completion of tmem buffers for 2CTA kernels.
"""
cta_rank_in_cluster = cute.arch.make_warp_uniform(
cute.arch.block_idx_in_cluster()
)
cta_in_cluster_coord_vmnk = cta_layout_vmnk.get_flat_coord(cta_rank_in_cluster)
return cute.make_layout_image_mask(
cta_layout_vmnk, cta_in_cluster_coord_vmnk, mode=0
)
@staticmethod
def _compute_peer_cta_rank():
"""
Computes a mask to signal release of tmem buffers for 2CTA kernels.
"""
cta_rank_in_cluster = cute.arch.make_warp_uniform(
cute.arch.block_idx_in_cluster()
)
return cta_rank_in_cluster // 2 * 2
@staticmethod
def create(
barrier_storage: cute.Pointer,
num_stages: Int32,
producer_group: CooperativeGroup,
consumer_group: CooperativeGroup,
cta_layout_vmnk: Optional[cute.Layout] = None,
):
"""
This helper function computes any necessary attributes and returns an instance of PipelineUmmaAsync.
:param barrier_storage: Pointer to the smem address for this pipeline's mbarriers
:type barrier_storage: cute.Pointer
:param num_stages: Number of buffer stages for this pipeline
:type num_stages: Int32
:param producer_group: CooperativeGroup for the producer agent
:type producer_group: CooperativeGroup
:param consumer_group: CooperativeGroup for the consumer agent
:type consumer_group: CooperativeGroup
:param cta_layout_vmnk: Layout of the cluster shape
:type cta_layout_vmnk: cute.Layout | None
"""
producer_type = _PipelineOp.TCGen05Mma
consumer_type = _PipelineOp.AsyncThread
producer = (producer_type, producer_group)
consumer = (consumer_type, consumer_group)
sync_object_array_full = PipelineAsync._make_sync_object_array(
barrier_storage.align(min_align=8), num_stages, producer
)
sync_object_array_empty = PipelineAsync._make_sync_object_array(
barrier_storage.align(min_align=8) + num_stages, num_stages, consumer
)
if cta_layout_vmnk is None or cute.size(cta_layout_vmnk) == 1:
# Set mask to None if not using clusters (i.e. 1CTA kernels)
producer_mask = None
else:
producer_mask = PipelineUmmaAsync._compute_tmem_sync_mask(cta_layout_vmnk)
if cta_layout_vmnk is None or cute.size(cta_layout_vmnk, mode=[0]) == 1:
# Set mask to None if not using 2CTA intructions
consumer_mask = None
else:
consumer_mask = PipelineUmmaAsync._compute_peer_cta_rank()
pipeline_init_wait(cta_layout_vmnk)
return PipelineUmmaAsync(
sync_object_array_full,
sync_object_array_empty,
num_stages,
producer_mask,
consumer_mask,
)
def producer_tail(self, state: PipelineState):
"""
Make sure the last used buffer empty signal is visible to producer.
Producer tail is usually executed by producer before exit, to avoid dangling
mbarrier arrive signals after kernel exit.
:param state: The pipeline state that points to next useful buffer
:type state: PipelineState
"""
cta_rank_in_cluster = cute.arch.make_warp_uniform(
cute.arch.block_idx_in_cluster()
)
is_leader_cta = cta_rank_in_cluster % 2 == 0
def then_body():
# Assume state contains that next useful buffer
# So we only need to advance to num_stages - 1 times to last used buffer
for i in range(self.num_stages - 1):
state.advance()
self.producer_acquire(state)
if_generate(is_leader_cta, then_body)
@dataclass(frozen=True)
class PipelineTmaStore(PipelineAsync):
"""
PipelineTmaStore is used for synchronizing TMA stores in the epilogue. It does not use mbarriers.
"""
@staticmethod
def create(
num_stages: Int32,
producer_group: CooperativeGroup,
):
"""
This helper function computes any necessary attributes and returns an instance of PipelineTmaStore.
:param num_stages: Number of buffer stages for this pipeline
:type num_stages: Int32
:param producer_group: CooperativeGroup for the producer agent
:type producer_group: CooperativeGroup
"""
producer_type = _PipelineOp.TmaStore
producer = (producer_type, producer_group)
sync_object_array_full = PipelineAsync._make_sync_object_array(
None, num_stages, producer
)
return PipelineTmaStore(sync_object_array_full, None, num_stages, None, None)
def producer_acquire(self):
self.sync_object_array_full.wait()
def producer_commit(self):
self.sync_object_array_full.arrive()
def consumer_wait(self):
assert False, "Error: PipelineTmaStore does not have a consumer agent."
def consumer_release(self):
assert False, "Error: PipelineTmaStore does not have a consumer agent."
def producer_tail(self):
self.sync_object_array_full.tail()
##############################################################################
# Helper functions
##############################################################################
def pipeline_init_wait(cta_layout_vmnk: Optional[cute.Layout] = None):
"""
Fences the mbarrier init and syncs the threadblock or cluster
"""
cute.arch.mbarrier_init_fence()
if cta_layout_vmnk is None or cute.size(cta_layout_vmnk) == 1:
# If not using clusters, sync the threadblock
_sync(Agent.ThreadBlock)
else:
# If using clusters, sync the cluster
_sync(Agent.ThreadBlockCluster)
def _sync(group: Agent):
"""
Syncs all threads within an agent.
"""
if group is Agent.Thread:
assert False, "Error: Not supported."
elif group is Agent.ThreadBlock:
cute.arch.sync_threads()
elif group is Agent.ThreadBlockCluster:
cute.arch.cluster_arrive()
cute.arch.cluster_wait()
else:
assert (
False
), "Error: No explicit sync instruction exists. Please use barriers (named / mbarrier) instead."
def _mbarrier_i64_to_ptr(val: Int64) -> cute.Pointer:
"""
Converts a smem pointer of type Int64 to cute.Pointer with 8B alignment
"""
return cute.make_ptr(
Int64,
val.ir_value(),
mem_space=_cute_ir.AddressSpace.smem,
assumed_align=8,
)
@@ -0,0 +1,217 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from typing import Type, Union, overload
from cutlass.cutlass_dsl import Int8, Numeric, NumericMeta
import cutlass.cute as cute
from cutlass.cute.arch import get_dyn_smem
class SmemAllocator:
"""
A class for managing shared memory allocation on GPU.
This class manages a chunk of shared memory and provide APIs for sub-allocation
inside the chunk.
Attributes
----------
_base : cute.Pointer as i8 typed dynamic value
The current base address of the shared memory.
_allocated_bytes:
The bytes allocated in shared memory.
Methods
-------
allocate(num_bytes, alignment)
Allocates num_bytes in the shared memory with the given byte alignment.
allocate_value(value_ty, num_elems)
Allocates num_elems of value_ty values in the shared memory.
allocate_tensor(value_ty, layout, alignment)
Allocates a tensor in the shared memory with given layout and byte alignment.
Notes
-----
This class is responsible for managing the allocation of tensors in shared memory.
"""
def __init__(self):
"""
Initializes the SmemAllocator instance with dynamic smem base ptr,
which is i8 type and aligned to 1024.
"""
self._base = get_dyn_smem(Int8, alignment=1024)
self._allocated_bytes = 0
@overload
def allocate(self, size_or_type: int, byte_alignment: int): ...
@overload
def allocate(self, size_or_type: cute.struct, byte_alignment: int): ...
def allocate(self, size_or_type, byte_alignment: int = 1) -> int:
"""
Allocates a block of memory with the specified size and byte alignment.
This method adjusts the base cute.Pointer to ensure that the allocated memory
is aligned according to the specified byte alignment. It updates the internal
state to reflect the new base cute.Pointer and the total allocated bytes.
Parameters
----------
size_or_type : int or struct
The number of bytes to allocate or struct class.
byte_alignment : int
The byte alignment requirement for the allocation. Defaults to 1 (no alignment).
Returns
----------
A cute.Pointer to the start of the allocated memory block or struct instance.
Raises
----------
ValueError
If num_bytes is negative or if byte_alignmemt is less than 1.
"""
if isinstance(size_or_type, cute.struct):
alignment = max(byte_alignment, size_or_type.__alignof__())
base_ptr = self.allocate(size_or_type.__sizeof__(), alignment)
return size_or_type(base_ptr)
num_bytes = size_or_type
if num_bytes < 0:
raise ValueError("num_bytes must be non-negative")
if byte_alignment < 1:
raise ValueError("byte_alignment must be at least 1")
self._base = self._base.align(byte_alignment)
ptr = self._base
self._base += num_bytes
if self._allocated_bytes % byte_alignment != 0:
self._allocated_bytes += (
byte_alignment - self._allocated_bytes % byte_alignment
)
self._allocated_bytes += num_bytes
return ptr
def allocate_array(self, element_type: Type[Numeric], num_elems: int = 1):
"""
Allocates num_elems values of element_type in shared memory.
This method calls allocate() to return a byte ptr, pointing to start of shared
memory. Then calls cute.recast_ptr() to recast this byte cute.Pointer to element_type.
Parameters
----------
element_type : Type[Numeric]
The type of the values in the tensor.
num_elems : int, optional
The number of elements for each allocation. Defaults to 1.
Returns
----------
A value_type cute.Pointer to the start of the allocated memory block.
Raises
----------
ValueError
If num_elems is less than 1.
"""
if num_elems < 1:
raise ValueError("num_elems must be at least 1")
if not isinstance(element_type, NumericMeta):
raise TypeError(
f"value_ty must be a type of Numeric, but got {element_type}"
)
ptr = self.allocate(
element_type.width // 8 * num_elems, element_type.width // 8
)
return cute.recast_ptr(ptr, dtype=element_type)
def allocate_tensor(
self,
element_type: Type[Numeric],
layout: Union[int, cute.Layout, cute.ComposedLayout],
byte_alignment: int = 1,
swizzle: cute.Swizzle = None,
):
"""
Allocates a tensor in the shared memory with value type, layout and byte alignment.
Parameters
----------
element_type : Type[Numeric]
The type of the values in the tensor.
layout : int | DynamicInt | cute.Layout | cute.ComposedLayout
The layout of the tensor.
byte_alignment : int, optional
The byte alignment requirement for the allocation. Defaults to 1 (no alignment).
swizzle : cute.Swizzle
A swizzle for the iterator (for position-dependent swizzling).
Returns
-------
tensor : cute.Tensor
The allocated tensor with specified value type, layout and byte alignment.
Notes
-----
The base address is updated to point to the next available memory location.
"""
if not isinstance(element_type, NumericMeta):
raise TypeError(
f"value_ty must be a type of Numeric, but got {element_type}"
)
if (
isinstance(layout, cute.ComposedLayout)
and isinstance(layout.inner, cute.Swizzle)
) and (swizzle is not None):
raise TypeError(
f"iterator swizzle with swizzle layout is currently not supported"
)
if isinstance(layout, int):
layout = cute.make_layout(layout)
profile = layout(0)
if isinstance(profile, tuple):
raise TypeError(
f"cannot allocate a shared memory tensor with a non-integer iterator"
)
if not cute.is_static(layout.type):
raise NotImplementedError(f"dynamic layout is not supported: {layout.type}")
# At least align the allocation to the natural alignment given by the element type
if element_type.width // 8 > byte_alignment:
byte_alignment = element_type.width // 8
# Relevant only for sub-byte data types: verify that the entire allocation is byte-aligned
cosize_in_bits = cute.cosize(layout) * element_type.width
assert isinstance(cosize_in_bits, int)
if cosize_in_bits % 8 != 0:
raise ValueError("invalid allocation that is not byte-aligned")
num_bytes = cosize_in_bits // 8
ptr = self.allocate(num_bytes, byte_alignment)
ptr = cute.recast_ptr(ptr, swizzle, dtype=element_type)
res = cute.make_tensor(ptr, layout)
return res
@@ -0,0 +1,384 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from typing import Tuple
from cutlass.cutlass_dsl import (
Boolean,
Integer,
Int32,
min,
extract_mlir_values,
new_from_mlir_values,
dsl_user_op,
)
from cutlass._mlir import ir
import cutlass.cute as cute
##############################################################################
# Static persistent tile scheduler
##############################################################################
class WorkTileInfo:
"""A class to represent information about a work tile.
:ivar tile_idx: The index of the tile.
:type tile_idx: cute.Coord
:ivar is_valid_tile: Whether the tile is valid.
:type is_valid_tile: Boolean
"""
def __init__(self, tile_idx: cute.Coord, is_valid_tile: Boolean):
self._tile_idx = tile_idx
self._is_valid_tile = Boolean(is_valid_tile)
def __extract_mlir_values__(self) -> list[ir.Value]:
values = extract_mlir_values(self.tile_idx)
values.extend(extract_mlir_values(self.is_valid_tile))
return values
def __new_from_mlir_values__(self, values: list[ir.Value]) -> "WorkTileInfo":
assert len(values) == 4
new_tile_idx = new_from_mlir_values(self._tile_idx, values[:-1])
new_is_valid_tile = new_from_mlir_values(self._is_valid_tile, [values[-1]])
return WorkTileInfo(new_tile_idx, new_is_valid_tile)
@property
def is_valid_tile(self) -> Boolean:
"""Check latest tile returned by the scheduler is valid or not. Any scheduling
requests after all tasks completed will return an invalid tile.
:return: The validity of the tile.
:rtype: Boolean
"""
return self._is_valid_tile
@property
def tile_idx(self) -> cute.Coord:
"""
Get the index of the tile.
:return: The index of the tile.
:rtype: cute.Coord
"""
return self._tile_idx
class PersistentTileSchedulerParams:
"""A class to represent parameters for a persistent tile scheduler.
This class is designed to manage and compute the layout of clusters and tiles
in a batched gemm problem.
:ivar cluster_shape_mn: Shape of the cluster in (m, n) dimensions (K dimension cta count must be 1).
:type cluster_shape_mn: tuple
:ivar problem_layout_ncluster_mnl: Layout of the problem in terms of
number of clusters in (m, n, l) dimensions.
:type problem_layout_ncluster_mnl: cute.Layout
"""
def __init__(
self,
problem_shape_ntile_mnl: cute.Shape,
cluster_shape_mnk: cute.Shape,
*,
loc=None,
ip=None,
):
"""
Initializes the PersistentTileSchedulerParams with the given parameters.
:param problem_shape_ntile_mnl: The shape of the problem in terms of
number of CTA (Cooperative Thread Array) in (m, n, l) dimensions.
:type problem_shape_ntile_mnl: cute.Shape
:param cluster_shape_mnk: The shape of the cluster in (m, n) dimensions.
:type cluster_shape_mnk: cute.Shape
:raises ValueError: If cluster_shape_k is not 1.
"""
if cluster_shape_mnk[2] != 1:
raise ValueError(f"unsupported cluster_shape_k {cluster_shape_mnk[2]}")
self.problem_shape_ntile_mnl = problem_shape_ntile_mnl
# cluster_shape_mnk is kept for reconstruction
self._cluster_shape_mnk = cluster_shape_mnk
self.cluster_shape_mn = cluster_shape_mnk[:2]
self._loc = loc
# By default, we follow m major (col-major) raster order, so make a col-major layout
self.problem_layout_ncluster_mnl = cute.make_layout(
cute.ceil_div(
self.problem_shape_ntile_mnl, cluster_shape_mnk[:2], loc=loc, ip=ip
),
loc=loc,
ip=ip,
)
def __extract_mlir_values__(self):
values, self._values_pos = [], []
for obj in [self.problem_shape_ntile_mnl, self._cluster_shape_mnk]:
obj_values = extract_mlir_values(obj)
values += obj_values
self._values_pos.append(len(obj_values))
return values
def __new_from_mlir_values__(self, values):
obj_list = []
for obj, n_items in zip(
[self.problem_shape_ntile_mnl, self._cluster_shape_mnk], self._values_pos
):
obj_list.append(new_from_mlir_values(obj, values[:n_items]))
values = values[n_items:]
return PersistentTileSchedulerParams(*(tuple(obj_list)), loc=self._loc)
@dsl_user_op
def get_grid_shape(
self, max_active_clusters: Int32, *, loc=None, ip=None
) -> Tuple[Integer, Integer, Integer]:
"""
Computes the grid shape based on the maximum active clusters allowed.
:param max_active_clusters: The maximum number of active clusters that
can run in one wave.
:type max_active_clusters: Int32
:return: A tuple containing the grid shape in (m, n, persistent_clusters).
- m: self.cluster_shape_m.
- n: self.cluster_shape_n.
- persistent_clusters: Number of persistent clusters that can run.
"""
# Total ctas in problem size
num_ctas_mnl = tuple(
x * y
for x, y in zip(
self.problem_layout_ncluster_mnl.shape, self.cluster_shape_mn
)
) + (self.problem_layout_ncluster_mnl.shape[2],)
num_ctas_in_problem = cute.size(num_ctas_mnl, loc=loc, ip=ip)
num_ctas_per_cluster = cute.size(self.cluster_shape_mn, loc=loc, ip=ip)
# Total ctas that can run in one wave
num_ctas_per_wave = max_active_clusters * num_ctas_per_cluster
num_persistent_ctas = min(num_ctas_in_problem, num_ctas_per_wave)
num_persistent_clusters = num_persistent_ctas // num_ctas_per_cluster
return (*self.cluster_shape_mn, num_persistent_clusters)
class StaticPersistentTileScheduler:
"""A scheduler for static persistent tile execution in CUTLASS/CuTe kernels.
:ivar params: Tile schedule related params, including cluster shape and problem_layout_ncluster_mnl
:type params: PersistentTileSchedulerParams
:ivar num_persistent_clusters: Number of persistent clusters that can be launched
:type num_persistent_clusters: Int32
:ivar cta_id_in_cluster: ID of the CTA within its cluster
:type cta_id_in_cluster: cute.Coord
:ivar _num_tiles_executed: Counter for executed tiles
:type _num_tiles_executed: Int32
:ivar _current_work_linear_idx: Current cluster index
:type _current_work_linear_idx: Int32
"""
def __init__(
self,
params: PersistentTileSchedulerParams,
num_persistent_clusters: Int32,
current_work_linear_idx: Int32,
cta_id_in_cluster: cute.Coord,
num_tiles_executed: Int32,
):
"""
Initializes the StaticPersistentTileScheduler with the given parameters.
:param params: Tile schedule related params, including cluster shape and problem_layout_ncluster_mnl.
:type params: PersistentTileSchedulerParams
:param num_persistent_clusters: Number of persistent clusters that can be launched.
:type num_persistent_clusters: Int32
:param current_work_linear_idx: Current cluster index.
:type current_work_linear_idx: Int32
:param cta_id_in_cluster: ID of the CTA within its cluster.
:type cta_id_in_cluster: cute.Coord
:param num_tiles_executed: Counter for executed tiles.
:type num_tiles_executed: Int32
"""
self.params = params
self.num_persistent_clusters = num_persistent_clusters
self._current_work_linear_idx = current_work_linear_idx
self.cta_id_in_cluster = cta_id_in_cluster
self._num_tiles_executed = num_tiles_executed
def __extract_mlir_values__(self) -> list[ir.Value]:
values = extract_mlir_values(self.num_persistent_clusters)
values.extend(extract_mlir_values(self._current_work_linear_idx))
values.extend(extract_mlir_values(self.cta_id_in_cluster))
values.extend(extract_mlir_values(self._num_tiles_executed))
return values
def __new_from_mlir_values__(
self, values: list[ir.Value]
) -> "StaticPersistentTileScheduler":
assert len(values) == 6
new_num_persistent_clusters = new_from_mlir_values(
self.num_persistent_clusters, [values[0]]
)
new_current_work_linear_idx = new_from_mlir_values(
self._current_work_linear_idx, [values[1]]
)
new_cta_id_in_cluster = new_from_mlir_values(
self.cta_id_in_cluster, values[2:5]
)
new_num_tiles_executed = new_from_mlir_values(
self._num_tiles_executed, [values[5]]
)
return StaticPersistentTileScheduler(
self.params,
new_num_persistent_clusters,
new_current_work_linear_idx,
new_cta_id_in_cluster,
new_num_tiles_executed,
)
# called by host
@dsl_user_op
@staticmethod
def create(
params: PersistentTileSchedulerParams,
block_idx: Tuple[Integer, Integer, Integer],
grid_dim: Tuple[Integer, Integer, Integer],
*,
loc=None,
ip=None,
):
"""Initialize the static persistent tile scheduler.
:param params: Parameters for the persistent
tile scheduler.
:type params: PersistentTileSchedulerParams
:param block_idx: The 3d block index in the format (bidx, bidy, bidz).
:type block_idx: Tuple[Integer, Integer, Integer]
:param grid_dim: The 3d grid dimensions for kernel launch.
:type grid_dim: Tuple[Integer, Integer, Integer]
:return: A StaticPersistentTileScheduler object.
:rtype: StaticPersistentTileScheduler
"""
params = params
# Calculate the number of persistent clusters by dividing the total grid size
# by the number of CTAs per cluster
num_persistent_clusters = cute.size(grid_dim, loc=loc, ip=ip) // cute.size(
params.cluster_shape_mn, loc=loc, ip=ip
)
bidx, bidy, bidz = block_idx
# Initialize workload index equals to the cluster index in the grid
current_work_linear_idx = Int32(bidz)
# CTA id in the cluster
cta_id_in_cluster = (
Int32(bidx % params.cluster_shape_mn[0]),
Int32(bidy % params.cluster_shape_mn[1]),
Int32(0),
)
# Initialize number of tiles executed to zero
num_tiles_executed = Int32(0)
return StaticPersistentTileScheduler(
params,
num_persistent_clusters,
current_work_linear_idx,
cta_id_in_cluster,
num_tiles_executed,
)
# called by host
@staticmethod
def get_grid_shape(
params: PersistentTileSchedulerParams,
max_active_clusters: Int32,
*,
loc=None,
ip=None,
) -> Tuple[Integer, Integer, Integer]:
"""Calculates the grid shape to be launched on GPU using problem shape,
threadblock shape, and active cluster size.
:param params: Parameters for grid shape calculation.
:type params: PersistentTileSchedulerParams
:param max_active_clusters: Maximum active clusters allowed.
:type max_active_clusters: Int32
:return: The calculated 3d grid shape.
:rtype: Tuple[Integer, Integer, Integer]
"""
return params.get_grid_shape(max_active_clusters, loc=loc, ip=ip)
# private method
def _get_current_work_for_linear_idx(
self, current_work_linear_idx: Int32, *, loc=None, ip=None
) -> WorkTileInfo:
"""Compute current tile coord given current_work_linear_idx and cta_id_in_cluster.
:param current_work_linear_idx: The linear index of the current work.
:type current_work_linear_idx: Int32
:return: An object containing information about the current tile coordinates
and validity status.
:rtype: WorkTileInfo
"""
is_valid = current_work_linear_idx < cute.size(
self.params.problem_layout_ncluster_mnl, loc=loc, ip=ip
)
cur_cluster_coord = self.params.problem_layout_ncluster_mnl.get_hier_coord(
current_work_linear_idx, loc=loc, ip=ip
)
# cur_tile_coord is a tuple of i32 values
cur_tile_coord = tuple(
Int32(x) * Int32(z) + Int32(y)
for x, y, z in zip(
cur_cluster_coord,
self.cta_id_in_cluster,
(*self.params.cluster_shape_mn, Int32(1)),
)
)
return WorkTileInfo(cur_tile_coord, is_valid)
@dsl_user_op
def get_current_work(self, *, loc=None, ip=None) -> WorkTileInfo:
return self._get_current_work_for_linear_idx(
self._current_work_linear_idx, loc=loc, ip=ip
)
@dsl_user_op
def initial_work_tile_info(self, *, loc=None, ip=None) -> WorkTileInfo:
return self.get_current_work(loc=loc, ip=ip)
@dsl_user_op
def advance_to_next_work(self, *, advance_count: int = 1, loc=None, ip=None):
self._current_work_linear_idx += Int32(advance_count) * Int32(
self.num_persistent_clusters
)
self._num_tiles_executed += Int32(1)
@property
def num_tiles_executed(self) -> Int32:
return self._num_tiles_executed
@@ -0,0 +1,140 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from dataclasses import dataclass
from enum import Enum, auto
from typing import Tuple
from cutlass.cutlass_dsl import const_expr
import cutlass._mlir.dialects.cute as _cute_ir
import cutlass._mlir.dialects.cute_nvgpu as _cute_nvgpu_ir
import cutlass.cute as cute
class TensorMapUpdateMode(Enum):
"""
Enum class defining tensor map update modes.
Modes:
GMEM: Update tensormap in global memory
SMEM: Load tensormap from global memory to shared memory,
update it in shared memory, then store back to global memory
"""
GMEM = auto() # Update tensormap in global memory
SMEM = auto() # Update tensormap in shared memory
@dataclass(frozen=True)
class TensorMapManager:
"""
Manages TensorMap operations including initialization and updates.
Provides utilities to convert tensormap pointer to across different memory spaces.
"""
tensormap_update_mode: TensorMapUpdateMode
bytes_per_tensormap: int
# convert given cute.Pointer or cutlass.Int64 to a cute.Pointer to tensormap.
# address_space: the address space of the resulting tensormap pointer. It could be generic or gmem
def get_tensormap_ptr(
self,
ptr: cute.Pointer,
address_space=_cute_ir.AddressSpace.gmem,
) -> cute.Pointer:
if address_space not in [
_cute_ir.AddressSpace.gmem,
_cute_ir.AddressSpace.generic,
]:
raise ValueError(f"Invalid address space: {address_space} for tensormap")
gmem_ptr_i64 = ptr.toint().ir_value()
gmem_ptr_i64_align_ty = _cute_ir.ConstrainedIntType.get(
self.bytes_per_tensormap, gmem_ptr_i64.type.width
)
gmem_ptr_i64_align = _cute_ir.assume(gmem_ptr_i64_align_ty, gmem_ptr_i64)
gmem_ptr_ty = _cute_ir.PtrType.get(
_cute_nvgpu_ir.TmaDescriptorTiledType.get(),
address_space,
self.bytes_per_tensormap,
)
return _cute_ir.inttoptr(gmem_ptr_ty, gmem_ptr_i64_align)
# init tensormap pointed by dst_ptr with the one inside copy_atom.
# dst_ptr should be pointing to a global memory location or a smem location
# warp_id specifies which warp to perform the initialization
@cute.jit
def init_tensormap_from_atom(
self, copy_atom: cute.CopyAtom, dst_ptr: cute.Pointer, warp_id: int
) -> None:
warp_idx = cute.arch.warp_idx()
warp_idx = cute.arch.make_warp_uniform(warp_idx)
if warp_idx == warp_id:
with cute.arch.elect_one():
cute.nvgpu.cpasync.copy_tensormap(copy_atom, dst_ptr)
cute.arch.sync_warp()
return
# Perform a fence operation to ensure previous `init_tensormap_from_atom` calls have been completed
def fence_tensormap_initialization(
self,
) -> None:
if self.tensormap_update_mode == TensorMapUpdateMode.GMEM:
cute.arch.fence_acq_rel_cta()
return
# Perform a fence operation to ensure previous `update_tensormap` calls have been completed
def fence_tensormap_update(
self,
tensormap_ptr: cute.Pointer,
) -> None:
cute.nvgpu.cpasync.fence_tma_desc_acquire(tensormap_ptr)
return
@cute.jit
def update_tensormap(
self,
tensor_gmem: Tuple[cute.Tensor, ...],
tma_copy_atom: Tuple[cute.CopyAtom, ...],
tensormap_gmem_ptr: Tuple[cute.Pointer, ...],
warp_id: int,
tensormap_smem_ptr: Tuple[cute.Pointer, ...],
) -> None:
warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
# updates before touching tensormap in global memory
if warp_idx == warp_id:
if const_expr(self.tensormap_update_mode == TensorMapUpdateMode.SMEM):
for copy_atom, tensor, smem_ptr in zip(
tma_copy_atom, tensor_gmem, tensormap_smem_ptr
):
cute.nvgpu.cpasync.update_tma_descriptor(
copy_atom, tensor, smem_ptr
)
# wait until it's safe to update tensormap in global memory
with cute.arch.elect_one():
cute.arch.cp_async_bulk_commit_group()
cute.arch.cp_async_bulk_wait_group(0, read=True)
cute.arch.sync_warp()
# updates to tensormap in global memory
if const_expr(self.tensormap_update_mode == TensorMapUpdateMode.SMEM):
for gmem_ptr, smem_ptr in zip(tensormap_gmem_ptr, tensormap_smem_ptr):
cute.nvgpu.cpasync.cp_fence_tma_desc_release(gmem_ptr, smem_ptr)
else:
for copy_atom, tensor, gmem_ptr in zip(
tma_copy_atom, tensor_gmem, tensormap_gmem_ptr
):
cute.nvgpu.cpasync.update_tma_descriptor(
copy_atom, tensor, gmem_ptr
)
cute.arch.sync_warp()
cute.nvgpu.cpasync.fence_tma_desc_release()