v4.3 update. (#2709)

* v4.3 update.

* Update the cute_dsl_api changelog's doc link

* Update version to 4.3.0

* Update the example link

* Update doc to encourage user to install DSL from requirements.txt

---------

Co-authored-by: Larry Wu <larwu@nvidia.com>
This commit is contained in:
Junkai-Wu
2025-10-22 02:26:30 +08:00
committed by GitHub
parent e6e2cc29f5
commit b1d6e2c9b3
244 changed files with 59272 additions and 10455 deletions

View File

@@ -26,6 +26,7 @@ from .typing import (
XTuple,
Tiler,
Layout,
ComposedLayout,
Pointer,
Tensor,
)
@@ -35,47 +36,37 @@ from .typing import *
from .core import (
assume,
is_integer,
is_int_tuple,
is_static,
size,
static,
get_leaves,
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_layout_like,
make_composed_layout,
make_layout_tv,
make_swizzle,
make_sparse_elem,
recast_ptr,
make_tensor,
make_identity_tensor,
make_fragment,
recast_tensor,
get,
select,
front,
is_major,
leading_dim,
find,
find_if,
coalesce,
group_modes,
cosize,
dice,
product_each,
prepend,
append,
prepend_ones,
@@ -83,9 +74,7 @@ from .core import (
ceil_div,
slice_and_offset,
crd2idx,
domain_offset,
elem_less,
transform_leaf,
idx2crd,
filter_zeros,
filter,
tile_to_shape,
@@ -109,7 +98,65 @@ from .core import (
local_partition,
local_tile,
printf,
# Wrapper classes
Swizzle,
E,
# User defined struct
struct,
pretty_str,
make_layout_image_mask,
repeat,
repeat_as_tuple,
repeat_like,
round_up,
is_congruent,
is_weakly_congruent,
ScaledBasis,
get_divisibility,
Ratio,
)
from .tuple import (
transform_leaf,
find_if,
find,
flatten_to_tuple,
unflatten,
product,
product_like,
product_each,
elem_less,
)
from .tensor import (
TensorSSA,
ReductionOp,
make_tensor,
make_identity_tensor,
make_fragment,
make_fragment_like,
make_rmem_tensor_like,
make_rmem_tensor,
recast_tensor,
domain_offset,
print_tensor,
full,
full_like,
empty_like,
ones_like,
zeros_like,
where,
any_,
all_,
)
from .atom import (
Atom,
MmaAtom,
CopyAtom,
TiledCopy,
TiledMma,
ThrMma,
ThrCopy,
make_atom,
# tiled mma/tiled copy
make_mma_atom,
make_tiled_mma,
@@ -122,48 +169,16 @@ from .core import (
make_tiled_copy_B,
make_tiled_copy_C,
make_tiled_copy_C_atom,
basic_copy,
basic_copy_if,
autovec_copy,
copy,
make_cotiled_copy,
copy_atom_call,
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 .algorithm import gemm, copy, basic_copy, basic_copy_if, autovec_copy, prefetch
from . import arch
from . import nvgpu
from . import testing
from . import runtime
from . import math
# Export all math ops without "math."
from .math import *
@@ -175,7 +190,15 @@ from .. import cutlass_dsl as _dsl
jit = _dsl.CuTeDSL.jit
kernel = _dsl.CuTeDSL.kernel
register_jit_arg_adapter = _dsl.JitArgAdapterRegistry.register_jit_arg_adapter
compile = _dsl.compile
compile = _dsl.CompileCallable()
OptLevel = _dsl.OptLevel
PtxasOptions = _dsl.PtxasOptions
EnableAssertions = _dsl.EnableAssertions
GenerateLineInfo = _dsl.GenerateLineInfo
KeepCUBIN = _dsl.KeepCUBIN
KeepPTX = _dsl.KeepPTX
GPUArch = _dsl.GPUArch
LinkLibraries = _dsl.LinkLibraries
# Explicitly export all symbols for documentation generation
__all__ = [
@@ -186,22 +209,22 @@ __all__ = [
"ComposedLayout",
"Swizzle",
"E",
"ScaledBasis",
"Atom",
"MmaAtom",
"CopyAtom",
"TiledCopy",
"TiledMma",
"ThrMma",
"ThrCopy",
"TensorSSA",
"ReductionOp",
# Basic utility functions
"assume",
"is_integer",
"is_int_tuple",
"is_static",
"size",
"has_underscore",
"slice_",
"depth",
"rank",
"shape",
"printf",
"print_tensor",
@@ -211,6 +234,7 @@ __all__ = [
"recast_layout",
"make_identity_layout",
"make_ordered_layout",
"make_layout_like",
"make_composed_layout",
"make_layout_tv",
"make_layout_image_mask",
@@ -220,6 +244,8 @@ __all__ = [
"make_identity_tensor",
"make_fragment",
"make_fragment_like",
"make_rmem_tensor",
"make_rmem_tensor_like",
"recast_ptr",
"recast_tensor",
# Tensor manipulation
@@ -230,6 +256,7 @@ __all__ = [
"leading_dim",
"find",
"find_if",
"transform_leaf",
"coalesce",
"group_modes",
"cosize",
@@ -237,6 +264,7 @@ __all__ = [
# Tuple operations
"flatten_to_tuple",
"flatten",
"unflatten",
"product",
"product_like",
"product_each",
@@ -244,6 +272,7 @@ __all__ = [
"append",
"prepend_ones",
"append_ones",
"elem_less",
# Math operations
"ceil_div",
"round_up",
@@ -251,7 +280,6 @@ __all__ = [
"slice_and_offset",
"crd2idx",
"domain_offset",
"elem_less",
"filter_zeros",
"filter",
"tile_to_shape",
@@ -280,18 +308,27 @@ __all__ = [
"tiled_divide",
"local_partition",
"local_tile",
# MMA and Copy operations
# MMA and Copy atom operations
"make_atom",
"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_A",
"make_tiled_copy_B",
"make_tiled_copy_C",
"make_tiled_copy_C_atom",
"make_cotiled_copy",
"copy_atom_call",
# Algorithm operations
"basic_copy",
"basic_copy_if",
"autovec_copy",
"copy",
"copy_atom_call",
"prefetch",
"gemm",
# Tensor creation
"full",
@@ -302,8 +339,9 @@ __all__ = [
"where",
"any_",
"all_",
"repeat_as_tuple",
"repeat",
"repeat_like",
"ScaledBasis",
# User defined struct
"struct",
# Modules
@@ -311,6 +349,8 @@ __all__ = [
"nvgpu",
"testing",
"runtime",
# Math utils
*math.__all__,
# Decorators and code generation
"jit",
"kernel",

View File

@@ -0,0 +1,438 @@
# 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 math
from typing import Optional, Dict, Any, List, Tuple
from cutlass._mlir import ir
from cutlass.cutlass_dsl import for_generate, yield_out, if_generate, dsl_user_op
import cutlass._mlir.dialects.cute as _cute_ir
import cutlass._mlir.dialects.cute_nvgpu as _cute_nvgpu_ir
from .typing import Tensor, Int64, Int16, AddressSpace
from .core import (
rank,
is_static,
size,
make_layout,
make_ptr,
max_common_layout,
logical_divide,
append_ones,
group_modes,
)
from .atom import MmaAtom, CopyAtom, make_atom
@dsl_user_op
def gemm(
atom: MmaAtom,
d: Tensor,
a: Tensor,
b: Tensor,
c: Tensor,
*,
loc=None,
ip=None,
**kwargs,
) -> None:
"""The GEMM algorithm.
Computes ``D <- A * B + C`` where ``C`` and ``D`` can alias. Note that some MMA Atoms (e.g.
warpgroup-wide or tcgen05 MMAs) require manually setting an "accumulate" boolean field.
All tensors must be partitioned according to the provided MMA Atom.
For MMA Atoms that require single-threaded execution, the gemm op automatically handles thread
election internally. Manual thread selection is not required in such cases.
Following dispatch rules are supported:
- Dispatch [1]: (V) x (V) => (V) => (V,1,1) x (V,1,1) => (V,1,1)
- Dispatch [2]: (M) x (N) => (M,N) => (1,M,1) x (1,N,1) => (1,M,N)
- Dispatch [3]: (M,K) x (N,K) => (M,N) => (1,M,K) x (1,N,K) => (1,M,N)
- Dispatch [4]: (V,M) x (V,N) => (V,M,N) => (V,M,1) x (V,N,1) => (V,M,N)
- Dispatch [5]: (V,M,K) x (V,N,K) => (V,M,N)
:param atom: MMA atom
:type atom: MmaAtom
:param d: Destination tensor
:type d: Tensor
:param a: First source tensor
:type a: Tensor
:param b: Second source tensor
:type b: Tensor
:param c: Third source tensor
:type c: Tensor
:param loc: Source location for MLIR, defaults to None
:type loc: Optional[Location], optional
:param ip: Insertion point for MLIR, defaults to None
:type ip: Optional[InsertionPoint], optional
:param kwargs: Additional keyword arguments
:type kwargs: dict
:return: None
:rtype: None
"""
a_rank = rank(a.shape)
b_rank = rank(b.shape)
c_rank = rank(c.shape)
d_rank = rank(d.shape)
if a_rank != b_rank:
raise ValueError("`a` and `b` must have the same rank")
if c_rank != d_rank:
raise ValueError("`c` and `d` must have the same rank")
if a_rank == 1:
if c_rank > 2:
raise ValueError("`c` must have rank <= 2 when `a` has rank 1")
elif a_rank == 2:
if c_rank not in (2, 3):
raise ValueError("`c` must have rank 2 or 3 when `a` has rank 2")
elif a_rank == 3:
if c_rank != 3:
raise ValueError("`c` must have rank 3 when `a` has rank 3")
value = atom._unpack(loc=loc, ip=ip, **kwargs)
return _cute_ir.gemm(value, d.value, a.value, b.value, c.value, loc=loc, ip=ip)
@dsl_user_op
def basic_copy(src: Tensor, dst: Tensor, *, loc=None, ip=None) -> None:
"""Performs a basic element-wise copy.
This functions **assumes** the following pre-conditions:
1. `size(src) == size(dst)`
When the `src` and `dst` shapes are static, the pre-conditions are actually verified and the
element-wise loop is fully unrolled.
:param src: Source tensor
:type src: Tensor
:param dst: Destination tensor
:type dst: Tensor
:param loc: Source location for MLIR, defaults to None
:type loc: Optional[Location], optional
:param ip: Insertion point, defaults to None
:type ip: Optional[InsertionPoint], optional
"""
if is_static(src.shape) and is_static(dst.shape):
simt_copy_ty = _cute_nvgpu_ir.CopyAtomSIMTSyncCopyType.get(
src.element_type.mlir_type, src.element_type.width
)
simt_copy = make_atom(simt_copy_ty, loc=loc, ip=ip)
return _cute_ir.copy(simt_copy, src.value, dst.value, loc=loc, ip=ip)
s = size(dst, loc=loc, ip=ip)
# Always generate an scf.for Op when one of the tensors is dynamic
for i in for_generate(0, s, loc=loc, ip=ip):
dst[i] = src[i]
yield_out()
@dsl_user_op
def basic_copy_if(pred: Tensor, src: Tensor, dst: Tensor, *, loc=None, ip=None) -> None:
"""Performs a basic predicated element-wise copy.
This functions **assumes** the following pre-conditions:
1. `size(src) == size(dst)`
2. `size(src) == size(pred)`
When all shapes are static, the pre-conditions are actually verified and the element-wise loop
is fully unrolled.
"""
if src.element_type.width != dst.element_type.width:
raise NotImplementedError(
"basic_copy_if currently only supports equal source and destination "
"element type bit width"
)
if is_static(src.shape) and is_static(dst.shape) and is_static(pred.shape):
return _basic_copy_if_static(pred, src, dst, loc=loc, ip=ip)
s = size(dst, loc=loc, ip=ip)
# Always generate an scf.for Op when one of the tensors is dynamic
for i in for_generate(0, s, loc=loc, ip=ip):
if_generate(pred[i], lambda: dst.__setitem__(i, src[i]), loc=loc, ip=ip) # type: ignore
yield_out()
# Version of basic_copy_if when src and dst have static shapes
# - verify size(src) == size(dst) == size(prd)
# - fully unroll the loop for now
def _basic_copy_if_static(
pred: Tensor, src: Tensor, dst: Tensor, *, loc=None, ip=None
) -> None:
assert is_static(src.shape) and is_static(dst.shape) and is_static(pred.shape)
if size(src, loc=loc, ip=ip) != size(dst, loc=loc, ip=ip):
raise ValueError(
"basic_copy expects the size of source, destination, and predicate tensors to match"
)
# Fully unrolled loop in the static case for now
for i in range(size(dst, loc=loc, ip=ip)):
if_generate(pred[i], lambda: dst.__setitem__(i, src[i]), loc=loc, ip=ip) # type: ignore
@dsl_user_op
def autovec_copy(src: Tensor, dst: Tensor, *, loc=None, ip=None) -> None:
"""
Auto-vectorization SIMT copy policy.
Given a source and destination tensors that are statically shaped, this policy figures out the
largest safe vector width that the copy instruction can take and performs the copy.
"""
if src.element_type.width != dst.element_type.width:
raise NotImplementedError(
"autovec_copy currently only supports equal source and destination "
"element type bit width"
)
# We are going to dispatch to copy-with-atom which requires shapes to be static
if not is_static(src.shape) or not is_static(dst.shape):
raise ValueError(
"autovec_copy expects source and destination tensors to be statically shaped"
)
vec_layout = max_common_layout(src, dst, loc=loc, ip=ip)
num_common_elements = size(vec_layout, loc=loc, ip=ip)
# Next we construct an upper-bound on the number bits that can be vectorized by considering
# - the maximum alignment of the layouts
# - the maximum alignment of the pointers
upper_bound = math.gcd(src.layout.max_alignment, dst.layout.max_alignment)
upper_bound = math.gcd(upper_bound, num_common_elements)
upper_bound *= src.element_type.width
# For our instructions, the alignment of the pointer is an upper bound to the vector width
# max_alignment, as opposed to alignment, takes into account possible address swizzling
upper_bound = math.gcd(upper_bound, src.iterator.max_alignment * 8)
upper_bound = math.gcd(upper_bound, dst.iterator.max_alignment * 8)
# Finally, we put a cap at 128b
num_bits_per_copy = math.gcd(upper_bound, 128)
if (num_common_elements > 1) and (num_bits_per_copy % 8 == 0):
num_common_elements = num_bits_per_copy // src.element_type.width
# 2 step logical divides ensuring that the divides are valid at every step
vec_src = logical_divide(src, vec_layout, loc=loc, ip=ip)
vec_dst = logical_divide(dst, vec_layout, loc=loc, ip=ip)
tiled_src = logical_divide(
vec_src, make_layout(num_common_elements, loc=loc, ip=ip), loc=loc, ip=ip
)
tiled_dst = logical_divide(
vec_dst, make_layout(num_common_elements, loc=loc, ip=ip), loc=loc, ip=ip
)
# Dispatch to copy with atom
simt_type = _cute_nvgpu_ir.CopyAtomSIMTSyncCopyType.get(
src.element_type.mlir_type, num_bits_per_copy
)
simt_copy = make_atom(simt_type, loc=loc, ip=ip)
return _cute_ir.copy(
simt_copy, tiled_src.value, tiled_dst.value, loc=loc, ip=ip
)
# Failed to vectorize, use a basic copy
basic_copy(src, dst, loc=loc, ip=ip)
def _parse_auto_multicast_args(
kwargs: Dict[str, Any],
) -> List[Tuple[str, ir.Attribute]]:
"""
Parse multicast-related kwargs and return a list of (attr_name, attr) pairs.
This function consumes the following key from kwargs if present:
- 'auto_multicast': dict
dict: { 'multicast_layout': str, 'use_2cta': bool }
Returns:
List of (attr_name, ir.Attribute) pairs to be attached to the op.
Recognized attributes:
- ('multicast_layout', #cute.layout<...>) when a layout string is provided
- ('use_2cta', unit) when use_2cta is True
"""
attr_pairs: List[Tuple[str, ir.Attribute]] = []
# Pop known keys to avoid leaking to trait unpack
auto_multicast = kwargs.pop("auto_multicast", None)
use_2cta: bool = False
layout_str: Optional[str] = None
if auto_multicast is not None:
if not isinstance(auto_multicast, dict):
raise TypeError(
"auto_multicast must be a dict with keys 'multicast_layout' and optional 'use_2cta'"
)
layout_str = auto_multicast.get("multicast_layout", None)
use_2cta = bool(auto_multicast.get("use_2cta", False))
if layout_str is not None:
if not isinstance(layout_str, str):
raise TypeError(
"multicast_layout must be a string representing a CuTe layout, e.g. '(4,2):(1,0)'"
)
attr_pairs.append(
(
"multicast_layout",
ir.Attribute.parse(f'#cute.layout<"{layout_str}">'),
)
)
if use_2cta:
attr_pairs.append(("use_2cta", ir.UnitAttr.get()))
return attr_pairs
@dsl_user_op
def copy(
atom: CopyAtom,
src: Tensor,
dst: Tensor,
*,
pred: Optional[Tensor] = None,
loc=None,
ip=None,
**kwargs,
) -> None:
"""Facilitates data transfer between two tensors conforming to layout profile ``(V, Rest...)``.
:param atom: Copy atom specifying the transfer operation
:type atom: CopyAtom
:param src: Source tensor with layout profile ``(V, Rest...)``
:type src: Tensor
:param dst: Destination tensor with layout profile ``(V, Rest...)``
:type dst: Tensor
:param pred: Optional predication tensor for conditional transfers, defaults to None
:type pred: Optional[Tensor], optional
:param loc: Source location information, defaults to None
:type loc: Any, optional
:param ip: Insertion point, defaults to None
:type ip: Any, optional
:param kwargs: Additional copy atom specific arguments
:type kwargs: Dict[str, Any]
:raises TypeError: If source and destination element type bit widths differ
:raises ValueError: If source and destination ranks differ
:raises ValueError: If source and destination mode-1 sizes differ
:raises NotImplementedError: If ``V-mode`` rank exceeds 2
:return: None
:rtype: None
The ``V-mode`` represents either:
- A singular mode directly consumable by the provided Copy Atom
- A composite mode requiring recursive decomposition, structured as ``(V, Rest...)``,
and src/dst layout like ``((V, Rest...), Rest...)``
The algorithm recursively processes the ``V-mode``, decomposing it until reaching the minimum granularity
compatible with the provided Copy Atom's requirements.
Source and destination tensors must be partitioned in accordance with the Copy Atom specifications.
Post-partitioning, both tensors will exhibit a ``(V, Rest...)`` layout profile.
**Precondition:** The size of mode 1 must be equal for both source and destination tensors:
``size(src, mode=[1]) == size(dst, mode=[1])``
**Examples**:
TMA copy operation with multicast functionality:
.. code-block:: python
cute.copy(tma_atom, src, dst, tma_bar_ptr=mbar_ptr, mcast_mask=mask)
Optional predication is supported through an additional tensor parameter. For partitioned tensors with
logical profile ``((ATOM_V,ATOM_REST),REST,...)``, the predication tensor must maintain profile
compatibility with ``(ATOM_REST,REST,...)``.
For Copy Atoms requiring single-threaded execution, thread election is managed automatically by the
copy operation. External thread selection mechanisms are not necessary.
.. note::
- Certain Atoms may require additional operation-specific keyword arguments.
- Current implementation limits ``V-mode`` rank to 2 or less. Support for higher ranks is planned
for future releases.
"""
if isinstance(src.type, _cute_ir.MemRefType) and isinstance(
dst.type, _cute_ir.MemRefType
):
if src.element_type.width != dst.element_type.width:
raise TypeError(
"`copy` currently only supports equal source and destination "
"element type bit width"
)
if rank(src) != rank(dst):
raise ValueError(
"Expected source and destination tensors to have the same rank, "
f"but got {rank(src)} and {rank(dst)}"
)
# Canonicalize to at least rank-2 tensors
src = group_modes(append_ones(src, up_to_rank=2), 1)
dst = group_modes(append_ones(dst, up_to_rank=2), 1)
if pred is not None:
pred = group_modes(append_ones(pred, up_to_rank=2), 1)
if is_static(src.shape[1]) and is_static(dst.shape[1]):
if size(src, mode=[1]) != size(dst, mode=[1]):
raise ValueError(
"Expected source and destination tensors to have the same size in mode-1, "
f"but got {size(src, mode=[1])} and {size(dst, mode=[1])}"
)
multicast_attr_pairs = _parse_auto_multicast_args(kwargs)
value = atom._unpack(loc=loc, ip=ip, **kwargs)
if isinstance(pred, Tensor):
pred = pred.value
op = _cute_ir.copy(value, src.value, dst.value, pred=pred, loc=loc, ip=ip)
for name, attr in multicast_attr_pairs:
op.attributes[name] = attr
return op
@dsl_user_op
def prefetch(atom: CopyAtom, src: Tensor, *, loc=None, ip=None) -> None:
"""
The Prefetch algorithm.
The "prefetch" expects source tensors to be partitioned according to the provided Copy Atom.
Prefetch is used for loading tensors from global memory to L2.
Prefetch accepts Copy Atom but not all are allowed. Currently, only supports TMA prefetch.
.. code-block:: python
cute.prefetch(tma_prefetch, src)
For Copy Atoms that require single-threaded execution, the copy op automatically handles thread
election internally. Manual thread selection is not required in such cases.
"""
dummy_tma_bar_ptr = make_ptr(Int64, 0, AddressSpace.smem, loc=loc, ip=ip)
dummy_mcast_mask = Int16(0)
value = atom._unpack(
loc=loc, ip=ip, tma_bar_ptr=dummy_tma_bar_ptr, mcast_mask=dummy_mcast_mask
)
return _cute_ir.prefetch(value, src.value, loc=loc, ip=ip)

View File

@@ -11,9 +11,11 @@
from .elect import *
from .mbar import *
from .numeric_conversion import *
from .nvvm_wrappers import *
from .smem import *
from .tmem import *
from .numeric_conversion import *
# __all__ is required here for documentation generation
__all__ = [
@@ -44,6 +46,7 @@ __all__ = [
"grid_dim",
"cluster_idx",
"cluster_dim",
"cluster_size",
"block_in_cluster_idx",
"block_in_cluster_dim",
"block_idx_in_cluster",
@@ -66,9 +69,12 @@ __all__ = [
"cluster_wait",
"cluster_arrive",
"cluster_arrive_relaxed",
"fence_proxy",
"vote_ballot_sync",
"vote_any_sync",
"vote_all_sync",
"vote_uni_sync",
"popc",
"fence_proxy",
"fence_view_async_tmem_load",
"fence_view_async_tmem_store",
"warpgroup_reg_alloc",
@@ -98,4 +104,15 @@ __all__ = [
"alloc_tmem",
"relinquish_tmem_alloc_permit",
"dealloc_tmem",
#
# numeric_conversion.py
#
"prmt",
"cvt_i8_bf16_intrinsic",
"cvt_i4_bf16_intrinsic",
"cvt_f4e2m1_f16_intrinsic",
"cvt_i8x4_to_f32x4",
"cvt_i8x2_to_f32x2",
"cvt_i8_bf16",
"cvt_f32x2_bf16x2",
]

View File

@@ -9,6 +9,7 @@
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from cutlass.base_dsl.arch import Arch
from cutlass.cutlass_dsl import CuTeDSL, T, dsl_user_op
import cutlass._mlir.dialects.cute_nvgpu as _cute_nvgpu_ir
@@ -16,17 +17,17 @@ 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.
Provides a compiler hint indicating that the specified value is invariant across all threads in the warp,
which may enable performance optimizations.
:param value: The integer to make warp uniform.
:param value: The integer value to be marked as warp-uniform.
:type value: Int
:return: The warp-uniform value equal to the input.
:return: The input value, marked as warp-uniform.
:rtype: Int32
"""
return Int32(
@@ -68,17 +69,7 @@ def elect_one(*, loc=None, ip=None) -> IfOpRegion:
# 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",
"sm_100f",
],
"arch",
)
CuTeDSL._get_dsl().check_arch(lambda arch: arch >= Arch.sm_90)
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)

View File

@@ -10,14 +10,12 @@
# is strictly prohibited.
from typing import Optional
from cutlass.base_dsl.arch import Arch
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
from ..typing import Pointer, Int, Boolean, Int32, AddressSpace
####################################################################################################
#
@@ -46,17 +44,7 @@ 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",
"sm_100f",
],
"arch",
)
CuTeDSL._get_dsl().check_arch(lambda arch: arch >= Arch.sm_90)
nvvm.fence_mbarrier_init(loc=loc, ip=ip)
@@ -75,17 +63,7 @@ def mbarrier_arrive_and_expect_tx(
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",
"sm_100f",
],
"arch",
)
CuTeDSL._get_dsl().check_arch(lambda arch: arch >= Arch.sm_90)
mbar_llvm_ptr = mbar_ptr.llvm_ptr
if peer_cta_rank_in_cluster is not None:
@@ -125,17 +103,7 @@ def mbarrier_expect_tx(
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",
"sm_100f",
],
"arch",
)
CuTeDSL._get_dsl().check_arch(lambda arch: arch >= Arch.sm_90)
mbar_llvm_ptr = mbar_ptr.llvm_ptr
if peer_cta_rank_in_cluster is not None:
@@ -170,17 +138,7 @@ def mbarrier_wait(mbar_ptr: Pointer, phase: Int, *, loc=None, ip=None) -> None:
: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",
"sm_100f",
],
"arch",
)
CuTeDSL._get_dsl().check_arch(lambda arch: arch >= Arch.sm_90)
timeout_ns = 10000000
# This NVVM Op is a spin-loop wrapping the mbarrier.try_wait.parity.shared.b64 PTX
@@ -206,17 +164,7 @@ def mbarrier_try_wait(mbar_ptr: Pointer, phase: Int, *, loc=None, ip=None) -> Bo
: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",
"sm_100f",
],
"arch",
)
CuTeDSL._get_dsl().check_arch(lambda arch: arch >= Arch.sm_90)
return Boolean(
nvvm.mbarrier_wait_parity(
@@ -245,23 +193,15 @@ def mbarrier_conditional_try_wait(
: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",
"sm_100f",
],
"arch",
)
CuTeDSL._get_dsl().check_arch(lambda arch: arch >= Arch.sm_90)
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],
loc=loc,
ip=ip,
)
@@ -284,17 +224,7 @@ def mbarrier_arrive(
"""
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",
"sm_100f",
],
"arch",
)
CuTeDSL._get_dsl().check_arch(lambda arch: arch >= Arch.sm_90)
mbar_llvm_ptr = nvvm.mapa_shared_cluster(
mbar_llvm_ptr.type,
@@ -328,17 +258,7 @@ def cp_async_mbarrier_arrive_noinc(mbar_ptr: Pointer, *, loc=None, ip=None) -> N
:param mbar_ptr: A pointer to the mbarrier in SMEM
:type mbar_ptr: Pointer
"""
arch = CuTeDSL._get_dsl().envar.arch
check_value_in(
arch,
[
"sm_90",
"sm_90a",
"sm_100a",
"sm_100f",
],
"arch",
)
CuTeDSL._get_dsl().check_arch(lambda arch: arch >= Arch.sm_90)
mbar_llvm_ptr = mbar_ptr.llvm_ptr
nvvm.cp_async_mbarrier_arrive_shared(

View File

@@ -0,0 +1,263 @@
# 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 dsl_user_op
from cutlass._mlir import ir
from cutlass._mlir.dialects import builtin, arith, llvm, vector
from .nvvm_wrappers import (
cvt_i8_bf16,
cvt_f32x2_bf16x2,
cvt_i8x4_to_f32x4,
cvt_i8x2_to_f32x2,
cvt_i4x8_to_bf16x8,
cvt_i4x4_to_bf16x4,
cvt_i4x2_to_bf16x2,
cvt_i4_bf16,
cvt_f4e2m1x8_to_f16x8,
cvt_f4e2m1x4_to_f16x4,
cvt_f4e2m1x2_to_f16x2,
cvt_f4e2m1_f16,
)
from ..typing import (
Int4,
Int8,
Int32,
Float16,
BFloat16,
Float32,
)
@dsl_user_op
def cvt_i8_bf16_intrinsic(vec_i8, length, *, loc=None, ip=None):
"""
Convert a vector of int8 to a vector of bfloat16.
:param vec_i8: The input vector of int8.
:type vec_i8: 1D vector of int8
:param length: The length of the input vector.
:type length: int
:return: The output 1D vector of bfloat16 with the same length as the input vector.
:rtype: 1D vector of bfloat16
"""
src_pos = 0
vec_i8x4_type = ir.VectorType.get([4], Int8.mlir_type, loc=loc)
vec_i8x2_type = ir.VectorType.get([2], Int8.mlir_type, loc=loc)
vec_f32x2_type = ir.VectorType.get([2], Float32.mlir_type, loc=loc)
vec_dst_type = ir.VectorType.get([length], BFloat16.mlir_type, loc=loc)
vec_dst = llvm.mlir_zero(vec_dst_type, loc=loc, ip=ip)
# try to use vectorized version
if length >= 4:
num_vec4 = length // 4
for _ in range(num_vec4):
vec_i8x4 = vector.extract_strided_slice(
vec_i8x4_type, vec_i8, [src_pos], [4], [1], loc=loc, ip=ip
)
vec_f32x4 = cvt_i8x4_to_f32x4(vec_i8x4, loc=loc, ip=ip)
vec_f32x2_lo = vector.extract_strided_slice(
vec_f32x2_type, vec_f32x4, [0], [2], [1], loc=loc, ip=ip
)
vec_f32x2_hi = vector.extract_strided_slice(
vec_f32x2_type, vec_f32x4, [2], [2], [1], loc=loc, ip=ip
)
vec_bf16x2_lo = cvt_f32x2_bf16x2(vec_f32x2_lo, loc=loc, ip=ip)
vec_bf16x2_hi = cvt_f32x2_bf16x2(vec_f32x2_hi, loc=loc, ip=ip)
vec_dst = vector.insert_strided_slice(
vec_bf16x2_lo, vec_dst, [src_pos], [1], loc=loc, ip=ip
)
vec_dst = vector.insert_strided_slice(
vec_bf16x2_hi, vec_dst, [src_pos + 2], [1], loc=loc, ip=ip
)
src_pos += 4
length -= 4
if length >= 2:
vec_i8x2 = vector.extract_strided_slice(
vec_i8x2_type, vec_i8, [src_pos], [2], [1], loc=loc, ip=ip
)
vec_f32x2 = cvt_i8x2_to_f32x2(vec_i8x2, loc=loc, ip=ip)
vec_bf16x2 = cvt_f32x2_bf16x2(vec_f32x2, loc=loc, ip=ip)
vec_dst = vector.insert_strided_slice(
vec_bf16x2, vec_dst, [src_pos], [1], loc=loc, ip=ip
)
src_pos += 2
length -= 2
if length >= 1:
val_bf16 = cvt_i8_bf16(
vector.extractelement(
vec_i8,
position=arith.constant(Int32.mlir_type, src_pos),
loc=loc,
ip=ip,
),
loc=loc,
ip=ip,
)
vec_dst = vector.insertelement(
val_bf16,
vec_dst,
position=arith.constant(Int32.mlir_type, src_pos),
loc=loc,
ip=ip,
)
return vec_dst
@dsl_user_op
def cvt_i4_bf16_intrinsic(vec_i4, length, *, loc=None, ip=None):
"""
Convert a vector of int4 to a vector of bfloat16.
:param vec_i4: The input vector of int4.
:type vec_i4: 1D vector of int4
:param length: The length of the input vector.
:type length: int
:return: The output 1D vector of bfloat16 with the same length as the input vector.
:rtype: 1D vector of bfloat16
"""
src_pos = 0
vec_i4x8_type = ir.VectorType.get([8], Int4.mlir_type, loc=loc)
vec_i4x4_type = ir.VectorType.get([4], Int4.mlir_type, loc=loc)
vec_i4x2_type = ir.VectorType.get([2], Int4.mlir_type, loc=loc)
vec_dst_type = ir.VectorType.get([length], BFloat16.mlir_type, loc=loc)
vec_dst = llvm.mlir_zero(vec_dst_type, loc=loc, ip=ip)
# try to use vectorized version
if length >= 8:
num_vec8 = length // 8
for _ in range(num_vec8):
vec_i4x8 = vector.extract_strided_slice(
vec_i4x8_type, vec_i4, [src_pos], [8], [1], loc=loc, ip=ip
)
vec_bf16x8 = cvt_i4x8_to_bf16x8(vec_i4x8, loc=loc, ip=ip)
vec_dst = vector.insert_strided_slice(
vec_bf16x8, vec_dst, [src_pos], [1], loc=loc, ip=ip
)
src_pos += 8
length -= 8
if length >= 4:
vec_i4x4 = vector.extract_strided_slice(
vec_i4x4_type, vec_i4, [src_pos], [4], [1], loc=loc, ip=ip
)
vec_bf16x4 = cvt_i4x4_to_bf16x4(vec_i4x4, loc=loc, ip=ip)
vec_dst = vector.insert_strided_slice(
vec_bf16x4, vec_dst, [src_pos], [1], loc=loc, ip=ip
)
src_pos += 4
length -= 4
if length >= 2:
vec_i4x2 = vector.extract_strided_slice(
vec_i4x2_type, vec_i4, [src_pos], [2], [1], loc=loc, ip=ip
)
vec_bf16x2 = cvt_i4x2_to_bf16x2(vec_i4x2, loc=loc, ip=ip)
vec_dst = vector.insert_strided_slice(
vec_bf16x2, vec_dst, [src_pos], [1], loc=loc, ip=ip
)
src_pos += 2
length -= 2
if length >= 1:
val_bf16 = cvt_i4_bf16(
vector.extractelement(
vec_i4,
position=arith.constant(Int32.mlir_type, src_pos),
loc=loc,
ip=ip,
),
loc=loc,
ip=ip,
)
vec_dst = vector.insertelement(
val_bf16,
vec_dst,
position=arith.constant(Int32.mlir_type, src_pos),
loc=loc,
ip=ip,
)
return vec_dst
@dsl_user_op
def cvt_f4e2m1_f16_intrinsic(vec_f4e2m1, length, *, loc=None, ip=None):
"""
Convert a vector of float4e2m1 to a vector of float16.
:param vec_f4e2m1: The input vector of float4e2m1.
:type vec_f4e2m1: 1D vector of float4e2m1
:param length: The length of the input vector.
:type length: int
:return: The output 1D vector of float16 with the same length as the input vector.
:rtype: 1D vector of float16
"""
src_pos = 0
vec_src_i4 = builtin.unrealized_conversion_cast(
[ir.VectorType.get([length], Int4.mlir_type, loc=loc)],
[vec_f4e2m1],
loc=loc,
ip=ip,
)
vec_i4x8_type = ir.VectorType.get([8], Int4.mlir_type, loc=loc)
vec_i4x4_type = ir.VectorType.get([4], Int4.mlir_type, loc=loc)
vec_i4x2_type = ir.VectorType.get([2], Int4.mlir_type, loc=loc)
vec_dst_type = ir.VectorType.get([length], Float16.mlir_type, loc=loc)
vec_dst = llvm.mlir_zero(vec_dst_type, loc=loc, ip=ip)
# try to use vectorized version
if length >= 8:
num_vec8 = length // 8
for _ in range(num_vec8):
vec_f4e2m1x8 = vector.extract_strided_slice(
vec_i4x8_type, vec_src_i4, [src_pos], [8], [1], loc=loc, ip=ip
)
vec_f16x8 = cvt_f4e2m1x8_to_f16x8(vec_f4e2m1x8, loc=loc, ip=ip)
vec_dst = vector.insert_strided_slice(
vec_f16x8, vec_dst, [src_pos], [1], loc=loc, ip=ip
)
src_pos += 8
length -= 8
if length >= 4:
vec_f4e2m1x4 = vector.extract_strided_slice(
vec_i4x4_type, vec_src_i4, [src_pos], [4], [1], loc=loc, ip=ip
)
vec_f16x4 = cvt_f4e2m1x4_to_f16x4(vec_f4e2m1x4, loc=loc, ip=ip)
vec_dst = vector.insert_strided_slice(
vec_f16x4, vec_dst, [src_pos], [1], loc=loc, ip=ip
)
src_pos += 4
length -= 4
if length >= 2:
vec_f4e2m1x2 = vector.extract_strided_slice(
vec_i4x2_type, vec_src_i4, [src_pos], [2], [1], loc=loc, ip=ip
)
vec_f16x2 = cvt_f4e2m1x2_to_f16x2(vec_f4e2m1x2, loc=loc, ip=ip)
vec_dst = vector.insert_strided_slice(
vec_f16x2, vec_dst, [src_pos], [1], loc=loc, ip=ip
)
src_pos += 2
length -= 2
if length >= 1:
val_f16 = cvt_f4e2m1_f16(
vector.extractelement(
vec_src_i4,
position=arith.constant(Int32.mlir_type, src_pos),
loc=loc,
ip=ip,
),
loc=loc,
ip=ip,
)
vec_dst = vector.insertelement(
val_f16,
vec_dst,
position=arith.constant(Int32.mlir_type, src_pos),
loc=loc,
ip=ip,
)
return vec_dst

View File

@@ -16,7 +16,7 @@ from typing_extensions import deprecated
from cutlass.cutlass_dsl import T, dsl_user_op
from cutlass._mlir import ir
from cutlass._mlir.dialects import llvm, nvvm, vector
from cutlass._mlir.dialects import arith, llvm, nvvm, vector
# Forward nvvm enums
from cutlass._mlir.dialects.nvvm import (
@@ -30,11 +30,13 @@ from cutlass._mlir.dialects.nvvm import (
from ..typing import (
Int,
Boolean,
Int8,
Int16,
Uint16,
Int32,
Uint32,
Int64,
Float16,
Float32,
BFloat16,
Numeric,
@@ -164,6 +166,14 @@ def block_in_cluster_dim(*, loc=None, ip=None) -> Tuple[Int32, Int32, Int32]:
)
@dsl_user_op
def cluster_size(*, loc=None, ip=None) -> Int32:
"""
Returns the number of CTA within the cluster.
"""
return Int32(nvvm.read_ptx_sreg_cluster_nctarank(T.i32(), loc=loc, ip=ip))
@dsl_user_op
def block_idx_in_cluster(*, loc=None, ip=None) -> Int32:
"""
@@ -295,12 +305,49 @@ def shuffle_sync_op(
shlf_res = llvm.bitcast(orig_type.mlir_type, shlf_res, loc=loc, ip=ip)
return orig_type(shlf_res)
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 warp_reduction(
val: Numeric, op: Callable, *, threads_in_group: int = 32, loc=None, ip=None
) -> Numeric:
"""warp reduction of a Numeric value(e.g.Float32) by shuffle_sync_bfly, accepts custom binary operator.
The threads_in_group is the number of threads reduction group in a warp.
E.g. 32 means the whole warp reduced in one group. 8 means the warp is divided into 4 thread groups, each group has 8 threads in reduction.
:param val: register value
:type val: cutlass.Numeric
:param op: binary operator
:type op: Callable
:param threads_in_group: the number of threads reduction group in a warp
:type threads_in_group: int
:return: reduced value
:rtype: cutlass.Numeric
"""
offset = threads_in_group // 2
while offset > 0:
val = op(
val,
shuffle_sync_bfly(
val, offset=offset, mask=-1, mask_and_clamp=31, loc=loc, ip=ip
),
)
offset = offset // 2
return val
warp_reduction_max = partial(
warp_reduction, op=lambda x, y: fmax(x, y) if isinstance(x, Float32) else max(x, y)
)
warp_reduction_sum = partial(warp_reduction, op=lambda x, y: x + y)
@dsl_user_op
def barrier(*, barrier_id=None, number_of_threads=None, loc=None, ip=None) -> None:
"""
@@ -473,8 +520,19 @@ def fence_proxy(
def vote_ballot_sync(
pred: Boolean, mask: Int = FULL_MASK, *, loc=None, ip=None
) -> Int32:
"""
Performs a ballot operation across the warp.
"""Performs a ballot operation across the warp.
It copies the predicate from each thread in mask into the corresponding bit position of
destination register d, where the bit position corresponds to the thread's lane id.
:param pred: The predicate value for the current thread
:type pred: Boolean
:param mask: A 32-bit integer mask specifying which threads participate, defaults to all threads (0xFFFFFFFF)
:type mask: Int, optional
:return: A 32-bit integer where each bit represents a thread's predicate value
:rtype: Int32
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#parallel-synchronization-and-communication-instructions-vote-sync>`__.
"""
return Int32(
nvvm.vote_ballot_sync(
@@ -487,6 +545,97 @@ def vote_ballot_sync(
)
@dsl_user_op
def vote_sync_op(
pred: Boolean, kind: str, mask: Int = FULL_MASK, *, loc=None, ip=None
) -> Union[Int32, Boolean]:
return_type = Boolean
return_type_str = "pred"
return return_type(
llvm.inline_asm(
T.bool(),
[
Boolean(pred).ir_value(loc=loc, ip=ip),
Int32(mask).ir_value(loc=loc, ip=ip),
],
f"""{{\n\t
.reg .pred ps;\n\t
.reg .pred pd;\n\t
setp.ne.b32 ps, $1, 0;\n\t
vote.sync.{kind}.{return_type_str} pd, ps, $2;\n\t
selp.b32 $0, 1, 0, pd;\n\t
}}""",
"=r,r,i",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
)
@dsl_user_op
def vote_any_sync(
pred: Boolean, mask: Int = FULL_MASK, *, loc=None, ip=None
) -> Boolean:
"""True if source predicate is True for any non-exited threads in mask. Negate the source
predicate to compute .not_all.
:param pred: The predicate value for the current thread
:type pred: Boolean
:param mask: A 32-bit integer mask specifying which threads participate, defaults to all
threads (0xFFFFFFFF)
:type mask: Int, optional
:return: A boolean value indicating if the source predicate is True for all non-exited
threads in mask
:rtype: Boolean
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#parallel-synchronization-and-communication-instructions-vote-sync>`__.
"""
return vote_sync_op(pred, "any", mask, loc=loc, ip=ip)
@dsl_user_op
def vote_all_sync(
pred: Boolean, mask: Int = FULL_MASK, *, loc=None, ip=None
) -> Boolean:
"""True if source predicate is True for all non-exited threads in mask. Negate the source
predicate to compute .none.
:param pred: The predicate value for the current thread
:type pred: Boolean
:param mask: A 32-bit integer mask specifying which threads participate, defaults to all
threads (0xFFFFFFFF)
:type mask: Int, optional
:return: A boolean value indicating if the source predicate is True for all non-exited
threads in mask
:rtype: Boolean
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#parallel-synchronization-and-communication-instructions-vote-sync>`__.
"""
return vote_sync_op(pred, "all", mask, loc=loc, ip=ip)
@dsl_user_op
def vote_uni_sync(
pred: Boolean, mask: Int = FULL_MASK, *, loc=None, ip=None
) -> Boolean:
"""True f source predicate has the same value in all non-exited threads in mask. Negating
the source predicate also computes .uni
:param pred: The predicate value for the current thread
:type pred: Boolean
:param mask: A 32-bit integer mask specifying which threads participate, defaults to all
threads (0xFFFFFFFF)
:type mask: Int, optional
:return: A boolean value indicating if the source predicate is True for all non-exited
threads in mask
:rtype: Boolean
"""
return vote_sync_op(pred, "uni", mask, loc=loc, ip=ip)
@dsl_user_op
def popc(value: Numeric, *, loc=None, ip=None) -> Numeric:
"""
@@ -494,7 +643,7 @@ def popc(value: Numeric, *, loc=None, ip=None) -> Numeric:
"""
if not isinstance(value, Numeric):
value = as_numeric(value)
return type(value)(llvm.intr_ctpop(value.ir_value(), loc=loc, ip=ip))
return type(value)(llvm.intr_ctpop(value.ir_value(loc=loc, ip=ip), loc=loc, ip=ip))
@dsl_user_op
@@ -546,6 +695,27 @@ fence_view_async_tmem_store = partial(
)
@dsl_user_op
def fence_view_async_shared(
*,
loc=None,
ip=None,
) -> None:
"""
Perform a fence operation on the async shared memory load or store.
.. note::
This function is only available on sm_90 or higher.
The fence is required to synchronize the shared memory load/store
and let the pipeline release or commit the buffer.
This function is usually used for async execution unit (like TMA, UMMA) after the load/store operations.
"""
nvvm.fence_proxy(
nvvm.ProxyKind.async_shared, space=nvvm.SharedSpace.shared_cta, loc=loc, ip=ip
)
@dsl_user_op
def warpgroup_reg_realloc_op(
reg_count: int,
@@ -569,7 +739,7 @@ warpgroup_reg_dealloc = partial(
def calc_packed_f32x2_op(
src_a: Tuple[Float32, Float32],
src_b: Tuple[Float32, Float32],
src_c: Tuple[Float32, Float32] | None,
src_c: Optional[Tuple[Float32, Float32]],
calc_func: Callable,
*,
rnd=RoundingModeKind.RZ,
@@ -579,14 +749,23 @@ def calc_packed_f32x2_op(
) -> 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_type,
tuple(as_numeric(a).ir_value(loc=loc, ip=ip) 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
vec_type,
tuple(as_numeric(b).ir_value(loc=loc, ip=ip) 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_type,
tuple(as_numeric(c).ir_value(loc=loc, ip=ip) 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
@@ -660,6 +839,418 @@ def exp2(a: Union[float, Float32], *, loc=None, ip=None) -> Float32:
)
# Convert 1 int8 value to 1 bfloat16 value
@dsl_user_op
def cvt_i8_bf16(src_i8, *, loc=None, ip=None):
src_i16 = llvm.zext(Int16.mlir_type, src_i8, loc=loc, ip=ip)
val_i16 = llvm.inline_asm(
Uint16.mlir_type,
[
src_i16,
],
"""{\n\t
.reg .b16 r;\n\t
.reg .b8 s;\n\t
mov.b16 {s,_}, $1;\n\t
cvt.rn.bf16.s8 r, s;\n\t
mov.b16 $0, r;\n\t
}""",
"=h,h",
)
val_bf16 = llvm.bitcast(BFloat16.mlir_type, val_i16, loc=loc, ip=ip)
return val_bf16
# Convert vector of 2 float values to vector of 2 bfloat16 values with satfinite rounding
@dsl_user_op
def cvt_f32x2_bf16x2(src_vec2, *, loc=None, ip=None):
src0 = vector.extractelement(
src_vec2, position=arith.constant(Int32.mlir_type, 0, loc=loc, ip=ip)
)
src1 = vector.extractelement(
src_vec2, position=arith.constant(Int32.mlir_type, 1, loc=loc, ip=ip)
)
rst = llvm.inline_asm(
T.i32(),
[
Float32(src1).ir_value(loc=loc, ip=ip),
Float32(src0).ir_value(loc=loc, ip=ip),
],
"cvt.rn.satfinite.bf16x2.f32 $0, $1, $2;",
"=r,f,f",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
vec_type = ir.VectorType.get([2], BFloat16.mlir_type, loc=loc)
vec_bf16x2 = llvm.bitcast(vec_type, rst, loc=loc, ip=ip)
return vec_bf16x2
# Convert 1 float32 value to 1 bfloat16 value
@dsl_user_op
def cvt_f32_bf16(src_f32, *, loc=None, ip=None):
bf16_val = llvm.inline_asm(
BFloat16.mlir_type,
[
src_f32,
],
"cvt.rn.bf16.f32 $0, $1;",
"=h,f",
)
return bf16_val
# Convert vector of 4 int8 values to vector of 4 float32 values
@dsl_user_op
def cvt_i8x4_to_f32x4(src_vec4, *, loc=None, ip=None):
zero = arith.constant(Int32.mlir_type, 0, loc=loc, ip=ip)
mask4 = (
arith.constant(Int32.mlir_type, 0x00000001, loc=loc, ip=ip),
arith.constant(Int32.mlir_type, 0x00000100, loc=loc, ip=ip),
arith.constant(Int32.mlir_type, 0x00010000, loc=loc, ip=ip),
arith.constant(Int32.mlir_type, 0x01000000, loc=loc, ip=ip),
)
src_i32 = llvm.bitcast(Int32.mlir_type, src_vec4, loc=loc, ip=ip)
rst0 = llvm.inline_asm(
Int32.mlir_type,
[
src_i32,
mask4[0],
zero,
],
"dp4a.s32.s32 $0, $1, $2, $3;",
"=r,r,r,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
rst1 = llvm.inline_asm(
Int32.mlir_type,
[
src_i32,
mask4[1],
zero,
],
"dp4a.s32.s32 $0, $1, $2, $3;",
"=r,r,r,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
rst2 = llvm.inline_asm(
Int32.mlir_type,
[
src_i32,
mask4[2],
zero,
],
"dp4a.s32.s32 $0, $1, $2, $3;",
"=r,r,r,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
rst3 = llvm.inline_asm(
Int32.mlir_type,
[
src_i32,
mask4[3],
zero,
],
"dp4a.s32.s32 $0, $1, $2, $3;",
"=r,r,r,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
res0 = llvm.inline_asm(
Float32.mlir_type,
[
rst0,
],
"cvt.rn.f32.s32 $0, $1;",
"=f,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
res1 = llvm.inline_asm(
Float32.mlir_type,
[
rst1,
],
"cvt.rn.f32.s32 $0, $1;",
"=f,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
res2 = llvm.inline_asm(
Float32.mlir_type,
[
rst2,
],
"cvt.rn.f32.s32 $0, $1;",
"=f,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
res3 = llvm.inline_asm(
Float32.mlir_type,
[
rst3,
],
"cvt.rn.f32.s32 $0, $1;",
"=f,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
vec_f32x4_type = ir.VectorType.get([4], Float32.mlir_type, loc=loc)
vec_f32x4 = vector.from_elements(
vec_f32x4_type, [res0, res1, res2, res3], loc=loc, ip=ip
)
return vec_f32x4
# Convert vector of 2 int8 values to vector of 2 float32 values
@dsl_user_op
def cvt_i8x2_to_f32x2(src_vec2, *, loc=None, ip=None):
zero = arith.constant(Int32.mlir_type, 0, loc=loc, ip=ip)
mask2 = (
arith.constant(Int32.mlir_type, 0x00000001, loc=loc, ip=ip),
arith.constant(Int32.mlir_type, 0x00000100, loc=loc, ip=ip),
)
src_i16 = llvm.bitcast(Int16.mlir_type, src_vec2, loc=loc, ip=ip)
src_i32_pad16b = llvm.zext(Int32.mlir_type, src_i16, loc=loc, ip=ip)
rst0 = llvm.inline_asm(
Int32.mlir_type,
[
src_i32_pad16b,
mask2[0],
zero,
],
"dp4a.s32.s32 $0, $1, $2, $3;",
"=r,r,r,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
rst1 = llvm.inline_asm(
Int32.mlir_type,
[
src_i32_pad16b,
mask2[1],
zero,
],
"dp4a.s32.s32 $0, $1, $2, $3;",
"=r,r,r,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
res0 = llvm.inline_asm(
Float32.mlir_type,
[
rst0,
],
"cvt.rn.f32.s32 $0, $1;",
"=f,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
res1 = llvm.inline_asm(
Float32.mlir_type,
[
rst1,
],
"cvt.rn.f32.s32 $0, $1;",
"=f,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
vec_f32x2_type = ir.VectorType.get([2], Float32.mlir_type, loc=loc)
vec_f32x2 = vector.from_elements(vec_f32x2_type, [res0, res1], loc=loc, ip=ip)
return vec_f32x2
# Permute bytes from register pair.
@dsl_user_op
def prmt(src, src_reg_shifted, prmt_indices, *, loc=None, ip=None):
return llvm.inline_asm(
T.i32(),
[
Int32(src).ir_value(loc=loc, ip=ip),
Int32(src_reg_shifted).ir_value(loc=loc, ip=ip),
Int32(prmt_indices).ir_value(loc=loc, ip=ip),
],
"prmt.b32 $0, $1, $2, $3;",
"=r,r,r,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
# Convert 1 int4 value to 1 bfloat16 value
@dsl_user_op
def cvt_i4_bf16(src_i4, *, loc=None, ip=None):
# i4 -> i32 -> f32 -> bf
src_i32 = llvm.zext(Int32.mlir_type, src_i4, loc=loc, ip=ip)
src_f32 = llvm.sitofp(Float32.mlir_type, src_i32, loc=loc, ip=ip)
bf16_val = cvt_f32_bf16(src_f32, loc=loc, ip=ip)
return bf16_val
# Convert multiple int4 values to bfloat16 values.
# The number of elements to be converted must be be even as specified by num_elts.
# Int4 values are packed into int32 values with upper bits filled with 0 if there are less than 4 int4 values.
# Results bfloat16 values are also packed into int32 values.
@dsl_user_op
def cvt_i4_to_bf16_impl(src_i32, num_elts, *, loc=None, ip=None):
c4 = arith.constant(Int32.mlir_type, 4, loc=loc, ip=ip)
src_shr4 = llvm.lshr(src_i32, c4, loc=loc, ip=ip)
xor_mask0 = arith.constant(Int32.mlir_type, 0x08080808, loc=loc, ip=ip)
and_mask = arith.constant(Int32.mlir_type, 0x0F0F0F0F, loc=loc, ip=ip)
imm_lut = arith.constant(Int32.mlir_type, 0x0000006A, loc=loc, ip=ip)
src_i32 = llvm.inline_asm(
Int32.mlir_type,
[
src_i32,
and_mask,
xor_mask0,
imm_lut,
],
"lop3.b32 $0, $1, $2, $3, $4;",
"=r,r,n,n,n",
)
xor_mask1 = arith.constant(Int32.mlir_type, 0x88080808, loc=loc, ip=ip)
src_shr4 = llvm.inline_asm(
Int32.mlir_type,
[
src_shr4,
and_mask,
xor_mask1,
imm_lut,
],
"lop3.b32 $0, $1, $2, $3, $4;",
"=r,r,n,n,n",
)
prmt_indices = [
arith.constant(Int32.mlir_type, imme, loc=loc, ip=ip)
for imme in [
0x0000F4F0,
0x0000F5F1,
0x0000F6F2,
0x0000F7F3,
]
]
num_i32_elts = num_elts // 2
rsts = []
for i in range(num_i32_elts):
rst = llvm.inline_asm(
Int32.mlir_type,
[
src_i32,
src_shr4,
prmt_indices[i],
],
"prmt.b32 $0, $1, $2, $3;",
"=r,r,r,r",
)
rsts.append(rst)
mask_clear_top_bit = arith.constant(Int32.mlir_type, 0xFF7FFFFF, loc=loc, ip=ip)
rsts[-1] = llvm.inline_asm(
Int32.mlir_type,
[
rsts[-1],
mask_clear_top_bit,
],
"and.b32 $0, $1, $2;",
"=r,r,r",
)
mul = arith.constant(Int32.mlir_type, 0x83808380, loc=loc, ip=ip)
bias = arith.constant(Int32.mlir_type, 0xC308C308, loc=loc, ip=ip)
for i in range(num_i32_elts):
rsts[i] = llvm.inline_asm(
Int32.mlir_type,
[
rsts[i],
mul,
bias,
],
"fma.rn.bf16x2 $0, $1, $2, $3;",
"=r,r,r,r",
)
# pack rsts into a vector
vec_type = ir.VectorType.get([num_i32_elts], Int32.mlir_type, loc=loc)
vec_rsts = vector.from_elements(vec_type, rsts, loc=loc, ip=ip)
return vec_rsts
# Convert 2 int4 values to 2 bfloat16 values
@dsl_user_op
def cvt_i4x2_to_bf16x2(src_vec2, *, loc=None, ip=None):
# pack 2 int4 into 1 int32 value and fill upper bits with 0
src_i8 = llvm.bitcast(Int8.mlir_type, src_vec2, loc=loc, ip=ip)
src_i32 = llvm.zext(Int32.mlir_type, src_i8, loc=loc, ip=ip)
rst_i32 = cvt_i4_to_bf16_impl(src_i32, 2, loc=loc, ip=ip)
vec_bf16x2_type = ir.VectorType.get([2], BFloat16.mlir_type, loc=loc)
vec_bf16x2 = llvm.bitcast(vec_bf16x2_type, rst_i32, loc=loc, ip=ip)
return vec_bf16x2
# Convert 4 int4 values to 4 bfloat16 values
@dsl_user_op
def cvt_i4x4_to_bf16x4(src_vec4, *, loc=None, ip=None):
# pack 4 int4 into 1 int32 value and fill upper bits with 0
src_i16 = llvm.bitcast(Int16.mlir_type, src_vec4, loc=loc, ip=ip)
src_i32 = llvm.zext(Int32.mlir_type, src_i16, loc=loc, ip=ip)
rst_i32 = cvt_i4_to_bf16_impl(src_i32, 4, loc=loc, ip=ip)
vec_bf16x4_type = ir.VectorType.get([4], BFloat16.mlir_type, loc=loc)
vec_bf16x4 = llvm.bitcast(vec_bf16x4_type, rst_i32, loc=loc, ip=ip)
return vec_bf16x4
# Convert 8 int4 values to 8 bfloat16 values
@dsl_user_op
def cvt_i4x8_to_bf16x8(src_vec8, *, loc=None, ip=None):
# pack 8 int4 into 1 int32 value and fill upper bits with 0
src_i32 = llvm.bitcast(Int32.mlir_type, src_vec8, loc=loc, ip=ip)
rst_i32 = cvt_i4_to_bf16_impl(src_i32, 8, loc=loc, ip=ip)
vec_bf16x8_type = ir.VectorType.get([8], BFloat16.mlir_type, loc=loc)
vec_bf16x8 = llvm.bitcast(vec_bf16x8_type, rst_i32, loc=loc, ip=ip)
return vec_bf16x8
@dsl_user_op
def log2_of_pow2_int(a: Int32, *, loc=None, ip=None) -> Int32:
tmp = llvm.inline_asm(
Int32.mlir_type,
[a.ir_value(loc=loc, ip=ip)],
"brev.b32 $0, $1;",
"=r,r",
has_side_effects=False,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
return Int32(
llvm.inline_asm(
Int32.mlir_type,
[tmp],
"bfind.shiftamt.u32 $0, $1;",
"=r,r",
has_side_effects=False,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
)
@dsl_user_op
@deprecated(
"cute.arch.exp is deprecated, use cute.math.exp with `fastmath=True` instead"
@@ -679,3 +1270,96 @@ def exp_packed_f32x2(
LOG2_E = Float32(1.4426950408889634)
b = mul_packed_f32x2(a, (LOG2_E, LOG2_E), loc=loc, ip=ip)
return exp2(b[0], loc=loc, ip=ip), exp2(b[1], loc=loc, ip=ip)
@dsl_user_op
def cvt_f4e2m1_f16(src, *, loc=None, ip=None):
# 0 padding for upper 4 bits
zero = arith.constant(src.type, 0, loc=loc, ip=ip)
vec2 = vector.from_elements(
ir.VectorType.get([2], src.type, loc=loc), [src, zero], loc=loc, ip=ip
)
rst_vec2 = cvt_f4e2m1x2_to_f16x2(vec2, loc=loc, ip=ip)
# only the 1st element is valid
rst = vector.extract(
rst_vec2, dynamic_position=[], static_position=[0], loc=loc, ip=ip
)
return rst
# Convert 2 float4e2m1 values to 2 float16 values
@dsl_user_op
def cvt_f4e2m1x2_to_f16x2(src_vec2, *, loc=None, ip=None):
# pack 2 float4e2m1 into 1 int8 value and fill upper bits with 0
src_i8 = llvm.bitcast(Int8.mlir_type, src_vec2, loc=loc, ip=ip)
src_i16 = llvm.zext(Int16.mlir_type, src_i8, loc=loc, ip=ip)
rst_i32 = llvm.inline_asm(
Int32.mlir_type,
[src_i16],
"""{\n\t
.reg .b8 b;\n\t
mov.b16 {b,_}, $1;\n\t
cvt.rn.f16x2.e2m1x2 $0, b;\n\t
}""",
"=r,h",
)
vec_f16x2_type = ir.VectorType.get([2], Float16.mlir_type, loc=loc)
vec_f16x2 = llvm.bitcast(vec_f16x2_type, rst_i32, loc=loc, ip=ip)
return vec_f16x2
# Convert 4 float4e2m1 values to 4 float16 values
@dsl_user_op
def cvt_f4e2m1x4_to_f16x4(src_vec4, *, loc=None, ip=None):
# pack 4 float4e2m1 into 1 int16 value
src_i16 = llvm.bitcast(Int16.mlir_type, src_vec4, loc=loc, ip=ip)
rst_i32x2 = llvm.inline_asm(
llvm.StructType.get_literal([T.i32(), T.i32()]),
[src_i16],
"""{\n\t
.reg .b8 b0, b1;\n\t
mov.b16 {b0, b1}, $2;\n\t
cvt.rn.f16x2.e2m1x2 $0, b0;\n\t
cvt.rn.f16x2.e2m1x2 $1, b1;\n\t
}""",
"=r,=r,h",
)
res0 = llvm.extractvalue(T.i32(), rst_i32x2, [0])
res1 = llvm.extractvalue(T.i32(), rst_i32x2, [1])
vec_f32x2_type = ir.VectorType.get([2], Int32.mlir_type, loc=loc)
vec_f32x2 = vector.from_elements(vec_f32x2_type, [res0, res1], loc=loc, ip=ip)
vec_f16x4_type = ir.VectorType.get([4], Float16.mlir_type, loc=loc)
vec_f16x4 = llvm.bitcast(vec_f16x4_type, vec_f32x2, loc=loc, ip=ip)
return vec_f16x4
# Convert 8 float4e2m1 values to 8 float16 values
@dsl_user_op
def cvt_f4e2m1x8_to_f16x8(src_vec8, *, loc=None, ip=None):
# pack 8 float4e2m1 into 1 int32 value and fill upper bits with 0
src_i32 = llvm.bitcast(Int32.mlir_type, src_vec8, loc=loc, ip=ip)
rst_i32x4 = llvm.inline_asm(
llvm.StructType.get_literal([T.i32(), T.i32(), T.i32(), T.i32()]),
[src_i32],
"""{\n\t
.reg .b8 b0, b1, b2, b3;\n\t
mov.b32 {b0, b1, b2, b3}, $4;\n\t
cvt.rn.f16x2.e2m1x2 $0, b0;\n\t
cvt.rn.f16x2.e2m1x2 $1, b1;\n\t
cvt.rn.f16x2.e2m1x2 $2, b2;\n\t
cvt.rn.f16x2.e2m1x2 $3, b3;\n\t
}""",
"=r,=r,=r,=r,r",
)
res0 = llvm.extractvalue(T.i32(), rst_i32x4, [0])
res1 = llvm.extractvalue(T.i32(), rst_i32x4, [1])
res2 = llvm.extractvalue(T.i32(), rst_i32x4, [2])
res3 = llvm.extractvalue(T.i32(), rst_i32x4, [3])
vec_f32x4_type = ir.VectorType.get([4], Int32.mlir_type, loc=loc)
vec_f32x4 = vector.from_elements(
vec_f32x4_type, [res0, res1, res2, res3], loc=loc, ip=ip
)
vec_f16x8_type = ir.VectorType.get([8], Float16.mlir_type, loc=loc)
vec_f16x8 = llvm.bitcast(vec_f16x8_type, vec_f32x4, loc=loc, ip=ip)
return vec_f16x8

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -9,8 +9,11 @@
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from .core import TensorSSA
from typing import Callable, Union
from .typing import Numeric
from .tensor import TensorSSA
from cutlass._mlir.dialects import math, arith
from typing import Callable, Union
@@ -62,7 +65,7 @@ def acos(
.. code-block::
x = cute.make_fragment(layout) # Create tensor
x = cute.make_rmem_tensor(layout) # Create tensor
y = x.load() # Load values
z = acos(y) # Compute arc cosine
"""
@@ -85,7 +88,7 @@ def asin(
.. code-block::
x = cute.make_fragment(layout) # Create tensor
x = cute.make_rmem_tensor(layout) # Create tensor
y = x.load() # Load values
z = asin(y) # Compute arc sine
"""
@@ -108,11 +111,10 @@ def atan(
.. code-block::
x = cute.make_fragment(layout) # Create tensor
x = cute.make_rmem_tensor(layout) # Create tensor
y = x.load() # Load values
z = atan(y) # Compute arc tangent
"""
raise NotImplementedError("atan is not implemented")
return _math_op(math.atan, fastmath, a)
@@ -137,8 +139,8 @@ def atan2(
.. code-block::
y = cute.make_fragment(ptr1, layout).load() # y coordinates
x = cute.make_fragment(ptr2, layout).load() # x coordinates
y = cute.make_rmem_tensor(ptr1, layout).load() # y coordinates
x = cute.make_rmem_tensor(ptr2, layout).load() # x coordinates
theta = atan2(y, x) # Compute angles
"""
return _math_op(math.atan2, fastmath, a, b)
@@ -160,7 +162,7 @@ def cos(
.. code-block::
x = cute.make_fragment(layout) # Create tensor
x = cute.make_rmem_tensor(layout) # Create tensor
y = x.load() # Load values
z = cos(y) # Compute cosine
"""
@@ -186,7 +188,7 @@ def erf(
.. code-block::
x = cute.make_fragment(layout) # Create tensor
x = cute.make_rmem_tensor(layout) # Create tensor
y = x.load() # Load values
z = erf(y) # Compute error function
"""
@@ -209,7 +211,7 @@ def exp(
.. code-block::
x = cute.make_fragment(layout) # Create tensor
x = cute.make_rmem_tensor(layout) # Create tensor
y = x.load() # Load values
z = exp(y) # Compute exponential
"""
@@ -232,7 +234,7 @@ def exp2(
.. code-block::
x = cute.make_fragment(layout) # Create tensor
x = cute.make_rmem_tensor(layout) # Create tensor
y = x.load() # Load values
z = exp2(y) # Compute 2^x
"""
@@ -255,7 +257,7 @@ def log(
.. code-block::
x = cute.make_fragment(layout) # Create tensor
x = cute.make_rmem_tensor(layout) # Create tensor
y = x.load() # Load values
z = log(y) # Compute natural logarithm
"""
@@ -278,7 +280,7 @@ def log2(
.. code-block::
x = cute.make_fragment(layout) # Create tensor
x = cute.make_rmem_tensor(layout) # Create tensor
y = x.load() # Load values
z = log2(y) # Compute log base 2
"""
@@ -301,7 +303,7 @@ def log10(
.. code-block::
x = cute.make_fragment(layout) # Create tensor
x = cute.make_rmem_tensor(layout) # Create tensor
y = x.load() # Load values
z = log10(y) # Compute log base 10
"""
@@ -326,7 +328,7 @@ def rsqrt(
.. code-block::
x = cute.make_fragment(layout) # Create tensor
x = cute.make_rmem_tensor(layout) # Create tensor
y = x.load() # Load values
z = rsqrt(y) # Compute 1/√x
"""
@@ -349,7 +351,7 @@ def sin(
.. code-block::
x = cute.make_fragment(layout) # Create tensor
x = cute.make_rmem_tensor(layout) # Create tensor
y = x.load() # Load values
z = sin(y) # Compute sine
"""
@@ -372,7 +374,7 @@ def sqrt(
.. code-block::
x = cute.make_fragment(layout) # Create tensor
x = cute.make_rmem_tensor(layout) # Create tensor
y = x.load() # Load values
z = sqrt(y) # Compute square root
"""
@@ -395,7 +397,7 @@ def tan(
.. code-block::
x = cute.make_fragment(layout) # Create tensor
x = cute.make_rmem_tensor(layout) # Create tensor
y = x.load() # Load values
z = tan(y) # Compute tangent
"""
@@ -418,7 +420,7 @@ def tanh(
.. code-block::
x = cute.make_fragment(layout) # Create tensor
x = cute.make_rmem_tensor(layout) # Create tensor
y = x.load() # Load values
z = tanh(y) # Compute hyperbolic tangent
"""

View File

@@ -18,8 +18,9 @@ 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 .. import atom
from ..typing import Float16, Float32, Float64, Numeric
from cutlass import cute
class OpError(DSLBaseError):
@@ -28,7 +29,7 @@ class OpError(DSLBaseError):
"""
def __init__(
self, op: core.Op, message: str, suggestion: Optional[str] = None
self, op: atom.Op, message: str, suggestion: Optional[str] = None
) -> None:
if suggestion is None:
# Default suggestion
@@ -48,7 +49,7 @@ class OpError(DSLBaseError):
@dataclass(frozen=True)
class MmaUniversalOp(core.MmaOp):
class MmaUniversalOp(atom.MmaOp):
"""
The universal MMA Operation.
@@ -65,7 +66,7 @@ class MmaUniversalOp(core.MmaOp):
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",
"expects the 'abacc_dtype' Op parameter to be one of Float16, Float32, or Float64",
)
def __str__(self) -> str:
@@ -75,14 +76,14 @@ class MmaUniversalOp(core.MmaOp):
)
def _make_trait(self, *, loc=None, ip=None, **kwargs) -> "MmaUniversalTrait":
shape_mnk_attr = ir.Attribute.parse(f'#cute.shape<"(1,1,1)">')
shape_mnk_attr = ir.Attribute.parse('#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))
return MmaUniversalTrait(cute.make_atom(atom_ty, loc=loc, ip=ip))
def _verify_fragment_A(self, input, *, loc=None, ip=None):
pass
@@ -90,7 +91,8 @@ class MmaUniversalOp(core.MmaOp):
def _verify_fragment_B(self, input, *, loc=None, ip=None):
pass
class MmaUniversalTrait(core.Trait):
class MmaUniversalTrait(atom.Trait):
pass
@@ -137,8 +139,9 @@ class MemoryScope(enum.Enum):
def _to_ir(self) -> _cute_ir.MemScopeKind:
return self.value
@dataclass(frozen=True)
class CopyUniversalOp(core.CopyOp):
class CopyUniversalOp(atom.CopyOp):
"""
The universal Copy Operation.
@@ -182,8 +185,8 @@ class CopyUniversalOp(core.CopyOp):
memory_order._to_ir(),
memory_scope._to_ir(),
)
return CopyUniversalTrait(_cute_ir.atom(ty, loc=loc, ip=ip))
return CopyUniversalTrait(cute.make_atom(ty, loc=loc, ip=ip))
class CopyUniversalTrait(core.Trait):
class CopyUniversalTrait(atom.Trait):
pass

View File

@@ -36,4 +36,5 @@ __all__ = [
"fence_tma_desc_acquire",
"cp_fence_tma_desc_release",
"fence_tma_desc_release",
"group_bulk_copy_modes",
]

View File

@@ -13,13 +13,15 @@ import enum
from dataclasses import dataclass
from typing import Optional, Type
from cutlass.cutlass_dsl import CuTeDSL, t
from cutlass import cute
from cutlass.base_dsl.arch import Arch
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 ...core import CopyOp, Trait, ReductionOp
from ...atom import CopyOp, Trait
from ...tensor import ReductionOp
from ...typing import Int16, Pointer, Integer, Numeric
from ..common import OpError
from ..tcgen05.mma import CtaGroup
@@ -73,19 +75,13 @@ class CopyG2SOp(CopyOp):
def _make_trait(
self,
copy_internal_type: Type[t.Numeric],
copy_internal_type: Type[Numeric],
*,
loc=None,
ip=None,
**kwargs,
) -> "CopyG2STrait":
num_bits_per_copy = kwargs.get("num_bits_per_copy", None)
# 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",
)
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 "
@@ -100,7 +96,7 @@ class CopyG2SOp(CopyOp):
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))
return CopyG2STrait(cute.make_atom(ty, loc=loc, ip=ip))
class CopyG2STrait(Trait):
@@ -114,8 +110,18 @@ class CopyG2STrait(Trait):
####################################################################################################
TMA_MBAR_PTR_FIELD_NAME = "tma_bar"
TMA_MASK_FIELD_NAME = "mcast_mask"
TMA_MCAST_MASK_FIELD_NAME = "mcast_mask"
TMA_DESC_PTR_FIELD_NAME = "tma_descriptor_ptr"
TMA_BYTE_MASK_FIELD_NAME = "byte_mask"
class TmaCopyOp(CopyOp):
"""
Base class for all TMA copy operations.
"""
pass
#
# TMA GMEM -> SMEM copies
@@ -123,7 +129,7 @@ TMA_DESC_PTR_FIELD_NAME = "tma_descriptor_ptr"
@dataclass(frozen=True)
class CopyBulkTensorTileG2SOp(CopyOp):
class CopyBulkTensorTileG2SOp(TmaCopyOp):
"""
Bulk tensor asynchrnous GMEM to SMEM Copy Operation using the TMA unit.
@@ -133,27 +139,20 @@ class CopyBulkTensorTileG2SOp(CopyOp):
cta_group: CtaGroup = CtaGroup.ONE
admissible_archs = [
"sm_90",
"sm_90a",
"sm_100a",
"sm_100f",
]
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:
arch: Arch = CuTeDSL._get_dsl().get_arch_enum()
if not arch >= Arch.sm_90:
raise OpError(
self,
f"expects arch to be one of {self.admissible_archs}, but got {arch}",
f"expects arch to be at least {Arch.sm_90.name}, but got {arch.name}",
suggestion="Ensure env CUTE_DSL_ARCH matches your GPU architecture",
)
if (self.cta_group == CtaGroup.TWO) and arch[:5] == "sm_90":
if (self.cta_group == CtaGroup.TWO) and arch.major == Arch.sm_90.major:
raise OpError(
self,
f"CTA group of 2 is tcgen05-specific and is not and is not compatible with {arch}",
@@ -163,7 +162,7 @@ class CopyBulkTensorTileG2SOp(CopyOp):
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"
res += "\n CTA group = 2"
return res
def _make_trait(
@@ -225,7 +224,7 @@ class CopyBulkTensorTileG2SNonExecTrait(Trait):
@dataclass(frozen=True)
class CopyBulkTensorTileG2SMulticastOp(CopyOp):
class CopyBulkTensorTileG2SMulticastOp(TmaCopyOp):
"""
Bulk tensor asynchrnous multicast GMEM to SMEM Copy Operation using the TMA unit.
@@ -235,27 +234,20 @@ class CopyBulkTensorTileG2SMulticastOp(CopyOp):
cta_group: CtaGroup = CtaGroup.ONE
admissible_archs = [
"sm_90",
"sm_90a",
"sm_100a",
"sm_100f",
]
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:
arch = CuTeDSL._get_dsl().get_arch_enum()
if not arch >= Arch.sm_90:
raise OpError(
self,
f"expects arch to be one of {self.admissible_archs}, but got {arch}",
f"expects arch to be at least {Arch.sm_90.name}, but got {arch.name}",
suggestion="Ensure env CUTE_DSL_ARCH matches your GPU architecture",
)
if (self.cta_group == CtaGroup.TWO) and arch[:5] == "sm_90":
if (self.cta_group == CtaGroup.TWO) and arch.major == Arch.sm_90.major:
raise OpError(
self,
f"CTA group of 2 is tcgen05-specific and is not and is not compatible with {arch}",
@@ -265,7 +257,7 @@ class CopyBulkTensorTileG2SMulticastOp(CopyOp):
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"
res += "\n CTA group = 2"
return res
def _make_trait(
@@ -309,12 +301,7 @@ class CopyBulkTensorTileG2SMulticastNonExecTrait(Trait):
"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_str = "#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
@@ -325,6 +312,13 @@ class CopyBulkTensorTileG2SMulticastNonExecTrait(Trait):
exec_value = _cute_nvgpu_ir.atom_set_value(
exec_value, attr, tma_desc_ptr.value, loc=loc, ip=ip
)
# Set the tma_bar_ptr at last to ensure that the atom creation and setting
# operations above can be moved outside the loop
attr_str = "#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
)
return exec_value
@@ -334,7 +328,7 @@ class CopyBulkTensorTileG2SMulticastNonExecTrait(Trait):
@dataclass(frozen=True)
class CopyBulkTensorTileS2GOp(CopyOp):
class CopyBulkTensorTileS2GOp(TmaCopyOp):
"""
Bulk tensor asynchronous SMEM to GMEM Copy Operation using the TMA unit.
@@ -342,20 +336,13 @@ class CopyBulkTensorTileS2GOp(CopyOp):
This Operation uses TMA in the ``.tile`` mode.
"""
admissible_archs = [
"sm_90",
"sm_90a",
"sm_100a",
"sm_100f",
]
def __post_init__(self):
# Arch verification
arch = CuTeDSL._get_dsl().envar.arch
if arch not in self.admissible_archs:
arch = CuTeDSL._get_dsl().get_arch_enum()
if not arch >= Arch.sm_90:
raise OpError(
self,
f"expects arch to be one of {self.admissible_archs}, but got {arch}",
f"expects arch to be at least {Arch.sm_90.name}, but got {arch.name}",
suggestion="Ensure env CUTE_DSL_ARCH matches your GPU architecture",
)
@@ -386,8 +373,9 @@ class CopyBulkTensorTileS2GTrait(Trait):
)
return exec_value
@dataclass(frozen=True)
class CopyReduceBulkTensorTileS2GOp(CopyOp):
class CopyReduceBulkTensorTileS2GOp(TmaCopyOp):
"""
Bulk tensor asynchronous SMEM to GMEM Reduction Operation using the TMA unit.
@@ -397,20 +385,13 @@ class CopyReduceBulkTensorTileS2GOp(CopyOp):
reduction_kind: ReductionOp = ReductionOp.ADD
admissible_archs = [
"sm_90",
"sm_90a",
"sm_100a",
"sm_100f",
]
def __post__init__(self):
# Arch verification
arch = CuTeDSL.__get_dsl().envar.arch
if arch not in self.admissible_archs:
arch = CuTeDSL._get_dsl().get_arch_enum()
if not arch >= Arch.sm_90:
raise OpError(
self,
f"expects arch to be one of {self.admissible_archs}, but got {arch}",
f"expects arch to be at least {Arch.sm_90.name}, but got {arch.name}",
suggestion="Ensure env CUTE_DSL_ARCH matches your GPU architecture",
)
@@ -461,11 +442,200 @@ class CopyReduceBulkTensorTileS2GTrait(Trait):
)
return exec_value
__all__ = [
"LoadCacheMode",
"CopyG2SOp",
"CopyBulkTensorTileG2SOp",
"CopyBulkTensorTileG2SMulticastOp",
"CopyBulkTensorTileS2GOp",
"CopyReduceBulkTensorTileS2GOp",
]
#
# Bulk GMEM -> SMEM copies
#
@dataclass(frozen=True)
class CopyBulkG2SOp(CopyOp):
"""
Bulk copy asynchrnous GMEM to SMEM Copy Operation.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#data-movement-and-conversion-instructions-cp-async-bulk>`__.
"""
def __post_init__(self) -> None:
# Arch verification
arch: Arch = CuTeDSL._get_dsl().get_arch_enum()
if not arch >= Arch.sm_90:
raise OpError(
self,
f"expects arch to be at least {Arch.sm_90.name}, but got {arch.name}",
suggestion="Ensure env CUTE_DSL_ARCH matches your GPU architecture",
)
def __str__(self) -> str:
res = "cp.async GMEM -> SMEM bulk copy Operation"
return res
def _make_trait(
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
) -> "CopyBulkG2STrait":
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 positive "
f"when creating a copy Atom for {self.__class__.__name__}"
)
ty = _cute_nvgpu_ir.CopyAtomBulkCopyG2SType.get(
copy_internal_type.mlir_type, num_bits_per_copy, False
)
return CopyBulkG2STrait(cute.make_atom(ty, loc=loc, ip=ip))
class CopyBulkG2STrait(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,
mbar_ptr: Optional[Pointer] = None,
**kwargs,
):
"""
Custom implementation of unpack for bulk copy load.
The non-multicast bulk load requires a `mbar_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(mbar_ptr, Pointer):
raise ValueError(
"expects a pointer to an mbarrier to be provided via the mbar_ptr kw argument"
)
attr_str = f"#cute_nvgpu.atom_copy_field_bulkg2s<{TMA_MBAR_PTR_FIELD_NAME}>"
attr = ir.Attribute.parse(attr_str)
val = _cute_nvgpu_ir.atom_set_value(
self.value, attr, mbar_ptr.value, loc=loc, ip=ip
)
return val
#
# Bulk GMEM -> SMEM Multicast copies
#
@dataclass(frozen=True)
class CopyBulkG2SMulticastOp(CopyOp):
"""
Bulk multicast copy asynchrnous GMEM to SMEM Copy Operation.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#data-movement-and-conversion-instructions-cp-async-bulk>`__.
"""
def __post_init__(self) -> None:
# Arch verification
arch: Arch = CuTeDSL._get_dsl().get_arch_enum()
if not arch >= Arch.sm_90:
raise OpError(
self,
f"expects arch to be at least {Arch.sm_90.name}, but got {arch.name}",
suggestion="Ensure env CUTE_DSL_ARCH matches your GPU architecture",
)
def __str__(self) -> str:
res = "cp.async GMEM -> SMEM multicast bulk copy Operation"
return res
def _make_trait(
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
) -> "CopyBulkG2SMulticastTrait":
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 positive "
f"when creating a copy Atom for {self.__class__.__name__}"
)
ty = _cute_nvgpu_ir.CopyAtomBulkCopyG2SType.get(
copy_internal_type.mlir_type, num_bits_per_copy, True
)
return CopyBulkG2SMulticastTrait(cute.make_atom(ty, loc=loc, ip=ip))
class CopyBulkG2SMulticastTrait(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,
mbar_ptr: Optional[Pointer] = None,
mcast_mask: Optional[Integer] = None,
**kwargs,
):
"""
Custom implementation of unpack for bulk copy load.
The non-multicast bulk load requires a `mbar_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(mbar_ptr, Pointer):
raise ValueError(
"expects a pointer to an mbarrier to be provided via the mbar_ptr kw argument"
)
if not isinstance(mcast_mask, Integer):
raise ValueError(
"expects a multicast mask to be provided via the mcast_mask kw argument"
)
attr_str = f"#cute_nvgpu.atom_copy_field_bulkg2s<{TMA_MBAR_PTR_FIELD_NAME}>"
attr = ir.Attribute.parse(attr_str)
val = _cute_nvgpu_ir.atom_set_value(
self.value, attr, mbar_ptr.value, loc=loc, ip=ip
)
attr_str = f"#cute_nvgpu.atom_copy_field_bulkg2s<{TMA_MCAST_MASK_FIELD_NAME}>"
attr = ir.Attribute.parse(attr_str)
val = _cute_nvgpu_ir.atom_set_value(
val, attr, Int16(mcast_mask).ir_value(loc=loc, ip=ip), loc=loc, ip=ip
)
return val
#
# Bulk SMEM -> GMEM copies
#
@dataclass(frozen=True)
class CopyBulkS2GOp(CopyOp):
"""
Bulk copy asynchrnous SMEM to GMEM Copy Operation.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#data-movement-and-conversion-instructions-cp-async-bulk>`__.
"""
def __post_init__(self) -> None:
# Arch verification
arch: Arch = CuTeDSL._get_dsl().get_arch_enum()
if not arch >= Arch.sm_90:
raise OpError(
self,
f"expects arch to be at least {Arch.sm_90.name}, but got {arch.name}",
suggestion="Ensure env CUTE_DSL_ARCH matches your GPU architecture",
)
def __str__(self) -> str:
res = "cp.async SMEM -> GMEM bulk copy Operation"
return res
def _make_trait(
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
) -> "CopyBulkS2GTrait":
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 positive "
f"when creating a copy Atom for {self.__class__.__name__}"
)
ty = _cute_nvgpu_ir.CopyAtomBulkCopyS2GType.get(
copy_internal_type.mlir_type, num_bits_per_copy, False
)
return CopyBulkS2GTrait(cute.make_atom(ty, loc=loc, ip=ip))
class CopyBulkS2GTrait(Trait):
pass

View File

@@ -16,8 +16,18 @@ 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 ...typing import (
Coord,
Layout,
ComposedLayout,
Tensor,
Tiler,
Pointer,
Int16,
Numeric,
NumericMeta,
)
from ... import core, atom
from .copy import (
CopyBulkTensorTileG2SOp,
CopyBulkTensorTileG2SMulticastOp,
@@ -39,14 +49,14 @@ def make_tiled_tma_atom(
CopyReduceBulkTensorTileS2GOp,
],
gmem_tensor: Tensor,
smem_layout: Union[Layout, core.ComposedLayout],
smem_layout: Union[Layout, ComposedLayout],
cta_tiler: Tiler,
num_multicast: int = 1,
*,
internal_type: Optional[Type[Numeric]] = None,
loc=None,
ip=None,
) -> Tuple[core.CopyAtom, Tensor]:
) -> Tuple[atom.CopyAtom, Tensor]:
"""
Makes a TMA Copy Atom in the ``.tile`` mode to copy tiles of a GMEM tensor to/from SMEM
buffer with the given Layout.
@@ -74,7 +84,7 @@ def make_tiled_tma_atom(
: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: Union[Layout, core.ComposedLayout]
:type smem_layout: Union[Layout, ComposedLayout]
:param cta_tiler: The CTA Tiler to use
:type cta_tiler: Tiler
:param num_multicast: The multicast factor
@@ -82,7 +92,7 @@ def make_tiled_tma_atom(
: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]
:rtype: Tuple[atom.CopyAtom, Tensor]
"""
if internal_type is not None:
@@ -97,6 +107,9 @@ def make_tiled_tma_atom(
ip=ip,
)
if isinstance(smem_layout, core._ComposedLayout):
smem_layout = smem_layout.value
if isinstance(op, CopyBulkTensorTileG2SOp):
if num_multicast != 1:
raise ValueError(
@@ -113,7 +126,7 @@ def make_tiled_tma_atom(
loc=loc,
ip=ip,
)
return core.CopyAtom(op, CopyBulkTensorTileG2SNonExecTrait(res[0])), res[1]
return atom.CopyAtom(op, CopyBulkTensorTileG2SNonExecTrait(res[0])), res[1]
elif isinstance(op, CopyBulkTensorTileG2SMulticastOp):
if num_multicast < 1:
raise ValueError(
@@ -131,7 +144,7 @@ def make_tiled_tma_atom(
ip=ip,
)
return (
core.CopyAtom(op, CopyBulkTensorTileG2SMulticastNonExecTrait(res[0])),
atom.CopyAtom(op, CopyBulkTensorTileG2SMulticastNonExecTrait(res[0])),
res[1],
)
elif isinstance(op, CopyBulkTensorTileS2GOp):
@@ -143,7 +156,7 @@ def make_tiled_tma_atom(
loc=loc,
ip=ip,
)
return core.CopyAtom(op, CopyBulkTensorTileS2GTrait(res[0])), res[1]
return atom.CopyAtom(op, CopyBulkTensorTileS2GTrait(res[0])), res[1]
elif isinstance(op, CopyReduceBulkTensorTileS2GOp):
res = _cute_nvgpu_ir.atom_make_non_exec_tiled_tma_reduce(
gmem_tensor.value,
@@ -154,14 +167,14 @@ def make_tiled_tma_atom(
loc=loc,
ip=ip,
)
return core.CopyAtom(op, CopyReduceBulkTensorTileS2GTrait(res[0])), res[1]
return atom.CopyAtom(op, CopyReduceBulkTensorTileS2GTrait(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,
atom: atom.CopyAtom,
cta_coord: Coord,
cta_layout: Layout,
smem_tensor: Tensor,
@@ -221,7 +234,7 @@ def create_tma_multicast_mask(
@dsl_user_op
def prefetch_descriptor(tma_atom: core.CopyAtom, *, loc=None, ip=None) -> None:
def prefetch_descriptor(tma_atom: atom.CopyAtom, *, loc=None, ip=None) -> None:
"""
Prefetches the TMA descriptor associated with the TMA Atom.
"""
@@ -230,7 +243,7 @@ def prefetch_descriptor(tma_atom: core.CopyAtom, *, loc=None, ip=None) -> None:
@dsl_user_op
def copy_tensormap(
tma_atom: core.CopyAtom, tensormap_ptr: Pointer, *, loc=None, ip=None
tma_atom: atom.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
@@ -248,7 +261,7 @@ def copy_tensormap(
@dsl_user_op
def update_tma_descriptor(
tma_atom: core.CopyAtom,
tma_atom: atom.CopyAtom,
gmem_tensor: Tensor,
tma_desc_ptr: Pointer,
*,
@@ -289,7 +302,7 @@ def fence_tma_desc_acquire(
"""
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()
tma_desc_ptr_i64 = tma_desc_ptr.toint(loc=loc, ip=ip).ir_value(loc=loc, ip=ip)
llvm.inline_asm(
None,
[tma_desc_ptr_i64],
@@ -312,8 +325,12 @@ def cp_fence_tma_desc_release(
"""
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()
tma_desc_global_ptr_i64 = tma_desc_global_ptr.toint(loc=loc, ip=ip).ir_value(
loc=loc, ip=ip
)
tma_desc_shared_ptr_i32 = tma_desc_shared_ptr.toint(loc=loc, ip=ip).ir_value(
loc=loc, ip=ip
)
llvm.inline_asm(
None,
[tma_desc_global_ptr_i64, tma_desc_shared_ptr_i32],
@@ -339,3 +356,13 @@ def fence_tma_desc_release(*, loc=None, ip=None) -> None:
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
@dsl_user_op
def group_bulk_copy_modes(src: Tensor, dst: Tensor, loc=None, ip=None) -> Tuple:
"""
Copy async bulk need group mode 0, acquiring whole tensor for bulk copy
"""
mSrc = core.group_modes(src, 0, core.rank(src))
mDst = core.group_modes(dst, 0, core.rank(dst))
return (mSrc, mDst)

View File

@@ -15,8 +15,8 @@ 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 .. import core, atom
from ..typing import Shape, Layout, ComposedLayout, Tensor, Numeric, NumericMeta
from ...impl_utils import check_type_in
from .cpasync.copy import (
CopyBulkTensorTileG2SOp,
@@ -37,15 +37,15 @@ from .cpasync.copy import (
def make_tiled_tma_atom_A(
op: Union[CopyBulkTensorTileG2SOp, CopyBulkTensorTileG2SMulticastOp],
gmem_tensor: Tensor,
smem_layout: Union[Layout, core.ComposedLayout],
smem_layout: Union[Layout, ComposedLayout],
mma_tiler_mnk: Shape,
tiled_mma: core.TiledMma,
cluster_shape_vmnk: Shape,
tiled_mma: atom.TiledMma,
cluster_shape_vmnk: Union[Shape, None] = None,
*,
internal_type: Optional[Type[Numeric]] = None,
loc=None,
ip=None,
) -> Tuple[core.CopyAtom, Tensor]:
) -> Tuple[atom.CopyAtom, Tensor]:
"""
Makes a TMA Copy atom mapping to ``.tile`` mode for ``cp.async.bulk.tensor`` PTX operation
accounting for the MK projections of the TiledMMA for A tensor loads.
@@ -76,18 +76,18 @@ def make_tiled_tma_atom_A(
:param gmem_tensor: The GMEM tensor to be loaded by this copy atom
:type gmem_tensor: Tensor
:param smem_layout: Shared memory layout to load the tensor into (PDSL)
:type smem_layout: Union[Layout, core.ComposedLayout]
:type smem_layout: Union[Layout, ComposedLayout]
:param mma_tiler_mnk: The MMA Tiler shape (TILE_M, TILE_N, TILE_K) in MNK dimensions
:type mma_tiler_mnk: Shape
:param tiled_mma: The TiledMMA that will consume the load as operands
:type tiled_mma: core.TiledMma
:type tiled_mma: atom.TiledMma
:param cluster_shape_vmnk: The Cluster-level shape in VMNK dimensions
:type cluster_shape_vmnk: Shape
:param internal_type: An optional parameter for the internal data type to when element
type does not match the copy type
:type internal_type: Type[Numeric]
:return: A copy atom for this operation and the associated TMA coord tensor
:rtype: Tuple[core.CopyAtom, Tensor]
:rtype: Tuple[atom.CopyAtom, Tensor]
"""
@@ -114,8 +114,15 @@ def make_tiled_tma_atom_A(
else:
assert isinstance(op, CopyBulkTensorTileG2SMulticastOp)
# multicast across the N-mode since those would share the same tile of A
if cluster_shape_vmnk is None:
raise ValueError(
"cluster_shape_vmnk must be provided for multicast A tensor loads"
)
num_multicast = core.size(cluster_shape_vmnk, mode=[2])
if isinstance(smem_layout, core._ComposedLayout):
smem_layout = smem_layout.value
# 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(
@@ -129,11 +136,11 @@ def make_tiled_tma_atom_A(
ip=ip,
)
if isinstance(op, CopyBulkTensorTileG2SOp):
return core.CopyAtom(op, CopyBulkTensorTileG2SNonExecTrait(res[0])), res[1]
return atom.CopyAtom(op, CopyBulkTensorTileG2SNonExecTrait(res[0])), res[1]
else:
assert isinstance(op, CopyBulkTensorTileG2SMulticastOp)
return (
core.CopyAtom(op, CopyBulkTensorTileG2SMulticastNonExecTrait(res[0])),
atom.CopyAtom(op, CopyBulkTensorTileG2SMulticastNonExecTrait(res[0])),
res[1],
)
@@ -142,15 +149,15 @@ def make_tiled_tma_atom_A(
def make_tiled_tma_atom_B(
op: Union[CopyBulkTensorTileG2SOp, CopyBulkTensorTileG2SMulticastOp],
gmem_tensor: Tensor,
smem_layout: Union[Layout, core.ComposedLayout],
smem_layout: Union[Layout, ComposedLayout],
mma_tiler_mnk: Shape,
tiled_mma: core.TiledMma,
cluster_shape_vmnk: Shape,
tiled_mma: atom.TiledMma,
cluster_shape_vmnk: Union[Shape, None] = None,
*,
internal_type: Optional[Type[Numeric]] = None,
loc=None,
ip=None,
) -> Tuple[core.CopyAtom, Tensor]:
) -> Tuple[atom.CopyAtom, Tensor]:
"""
Makes a TMA Copy atom mapping to ``.tile`` mode for ``cp.async.bulk.tensor`` PTX operation
accounting for the NK projections of the TiledMMA for B tensor loads.
@@ -181,7 +188,7 @@ def make_tiled_tma_atom_B(
:param gmem_tensor: The GMEM tensor to be loaded by this copy atom
:type gmem_tensor: Tensor
:param smem_layout: Shared memory layout to load the tensor into (PDSL)
:type smem_layout: Union[Layout, core.ComposedLayout]
:type smem_layout: Union[Layout, ComposedLayout]
:param mma_tiler_mnk: The MMA Tiler shape (TILE_M, TILE_N, TILE_K) in MNK dimensions
:type mma_tiler_mnk: Shape
:param tiled_mma: The TiledMMA that will consume the load as operands
@@ -192,7 +199,7 @@ def make_tiled_tma_atom_B(
type does not match the copy type
:type internal_type: Type[Numeric]
:return: A Copy Atom for this Operation and the associated TMA tensor
:rtype: Tuple[core.CopyAtom, Tensor]
:rtype: Tuple[atom.CopyAtom, Tensor]
"""
@@ -219,8 +226,15 @@ def make_tiled_tma_atom_B(
else:
assert isinstance(op, CopyBulkTensorTileG2SMulticastOp)
# multicast across the M-mode since those would share the same tile of B
if cluster_shape_vmnk is None:
raise ValueError(
"cluster_shape_vmnk must be provided for multicast B tensor loads"
)
num_multicast = core.size(cluster_shape_vmnk, mode=[1])
if isinstance(smem_layout, core._ComposedLayout):
smem_layout = smem_layout.value
# 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(
@@ -234,11 +248,11 @@ def make_tiled_tma_atom_B(
ip=ip,
)
if isinstance(op, CopyBulkTensorTileG2SOp):
return core.CopyAtom(op, CopyBulkTensorTileG2SNonExecTrait(res[0])), res[1]
return atom.CopyAtom(op, CopyBulkTensorTileG2SNonExecTrait(res[0])), res[1]
else:
assert isinstance(op, CopyBulkTensorTileG2SMulticastOp)
return (
core.CopyAtom(op, CopyBulkTensorTileG2SMulticastNonExecTrait(res[0])),
atom.CopyAtom(op, CopyBulkTensorTileG2SMulticastNonExecTrait(res[0])),
res[1],
)

View File

@@ -40,6 +40,7 @@ __all__ = [
"Field",
"MmaTF32Op",
"MmaF16BF16Op",
"MmaF16BF16SparseOp",
"MmaI8Op",
"MmaFP8Op",
"MmaMXF8Op",

View File

@@ -13,14 +13,15 @@ import enum
from dataclasses import dataclass
from typing import Type
from cutlass import cute
from cutlass.base_dsl.arch import Arch
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 ...atom import CopyOp, Trait
from ...typing import Numeric
from .mma import CtaGroup
@@ -46,24 +47,6 @@ class Repetition(enum.Enum):
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):
"""
@@ -97,17 +80,40 @@ class Unpack(enum.Enum):
@dataclass(frozen=True)
class _LdBase(CopyOp):
"""
Base class for TMEM load operations in the tcgen05 instruction set.
This abstract base class provides common functionality and validation for tensor memory (TMEM)
load operations. It defines the fundamental parameters and architecture constraints that apply
to all load operation variants.
:param repeat: Number of repetitions for the load operation, defaults to Repetition.x1
:type repeat: Repetition, optional
:param pack: Packing pattern for TMEM to RMEM copies, defaults to Pack.NONE
:type pack: Pack, optional
:raises OpError: If the current architecture is not supported or if invalid parameters are provided
"""
repeat: Repetition = Repetition.x1
pack: Pack = Pack.NONE
admissible_archs = [
"sm_100a",
"sm_100f",
]
admissible_archs = Arch.filter(
lambda arch: arch.is_family_of(Arch.sm_100f) or arch.is_family_of(Arch.sm_110f)
)
def __post_init__(self) -> None:
"""
Post-initialization validation for TMEM load operations.
Performs comprehensive validation of operation parameters and architecture compatibility.
This method is automatically called after object creation to ensure all constraints are met.
:raises OpError: If architecture is not supported
:raises OpError: If repeat parameter is not a Repetition instance
:raises OpError: If pack parameter is not a Pack instance
"""
# Arch verification
arch = CuTeDSL._get_dsl().envar.arch
arch = CuTeDSL._get_dsl().get_arch_enum()
if arch not in self.admissible_archs:
raise OpError(
self,
@@ -127,12 +133,21 @@ class _LdBase(CopyOp):
)
def __str__(self) -> str:
"""
Generate a human-readable string representation of the load operation.
Creates a formatted description showing the operation type, repetition count,
and any special packing configuration.
:return: Multi-line string describing the operation configuration
:rtype: 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"
res += "\n with 2x 16-bit to 32b packing"
return res
@@ -148,6 +163,24 @@ class Ld16x64bOp(_LdBase):
def _make_trait(
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
) -> "Ld16x64bTrait":
"""
Create a trait object for the 16x64b TMEM load operation.
Constructs an MLIR-based trait that encapsulates the specific parameters and
characteristics of this load operation. The trait is used by the compiler
infrastructure to generate the appropriate low-level code.
:param copy_internal_type: The data type for the copy operation
:type copy_internal_type: Type[Numeric]
:param loc: MLIR location information for debugging, defaults to None
:type loc: optional
:param ip: MLIR insertion point for code generation, defaults to None
:type ip: optional
:param kwargs: Additional keyword arguments passed to the trait constructor
:type kwargs: dict
:return: A trait object that represents this specific load operation
:rtype: Ld16x64bTrait
"""
ty = _cute_nvgpu_ir.CopyAtomSM100TmemLoadType.get(
copy_internal_type.mlir_type,
16,
@@ -155,7 +188,7 @@ class Ld16x64bOp(_LdBase):
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))
return Ld16x64bTrait(cute.make_atom(ty, loc=loc, ip=ip))
class Ld16x64bTrait(Trait):
@@ -172,6 +205,15 @@ class Ld16x128bOp(_LdBase):
"""
def __post_init__(self) -> None:
"""
Additional validation specific to 16x128b load operations.
Extends the base class validation with operation-specific constraints.
The 16x128b operation has limitations on the maximum repetition count due to
hardware register and bandwidth constraints.
:raises OpError: If x128 repetition is specified
"""
super().__post_init__()
if self.repeat == Repetition.x128:
raise OpError(
@@ -183,6 +225,20 @@ class Ld16x128bOp(_LdBase):
def _make_trait(
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
) -> "Ld16x128bTrait":
"""
Create a trait object for the 16x128b TMEM load operation.
:param copy_internal_type: The data type for the copy operation
:type copy_internal_type: Type[Numeric]
:param loc: MLIR location information for debugging, defaults to None
:type loc: optional
:param ip: MLIR insertion point for code generation, defaults to None
:type ip: optional
:param kwargs: Additional keyword arguments
:type kwargs: dict
:return: A trait object for this load operation
:rtype: Ld16x128bTrait
"""
ty = _cute_nvgpu_ir.CopyAtomSM100TmemLoadType.get(
copy_internal_type.mlir_type,
16,
@@ -190,7 +246,7 @@ class Ld16x128bOp(_LdBase):
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))
return Ld16x128bTrait(cute.make_atom(ty, loc=loc, ip=ip))
class Ld16x128bTrait(Trait):
@@ -207,6 +263,15 @@ class Ld16x256bOp(_LdBase):
"""
def __post_init__(self) -> None:
"""
Additional validation specific to 16x256b load operations.
Extends the base class validation with operation-specific constraints.
The 16x256b operation has more restrictive limitations on repetition count due to
the larger data size per operation requiring more hardware resources.
:raises OpError: If x64 or x128 repetition is specified
"""
super().__post_init__()
if self.repeat in (Repetition.x128, Repetition.x64):
raise OpError(
@@ -218,6 +283,20 @@ class Ld16x256bOp(_LdBase):
def _make_trait(
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
) -> "Ld16x256bTrait":
"""
Create a trait object for the 16x256b TMEM load operation.
:param copy_internal_type: The data type for the copy operation
:type copy_internal_type: Type[Numeric]
:param loc: MLIR location information for debugging, defaults to None
:type loc: optional
:param ip: MLIR insertion point for code generation, defaults to None
:type ip: optional
:param kwargs: Additional keyword arguments
:type kwargs: dict
:return: A trait object for this load operation
:rtype: Ld16x256bTrait
"""
ty = _cute_nvgpu_ir.CopyAtomSM100TmemLoadType.get(
copy_internal_type.mlir_type,
16,
@@ -225,7 +304,7 @@ class Ld16x256bOp(_LdBase):
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))
return Ld16x256bTrait(cute.make_atom(ty, loc=loc, ip=ip))
class Ld16x256bTrait(Trait):
@@ -244,6 +323,20 @@ class Ld16x32bx2Op(_LdBase):
def _make_trait(
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
) -> "Ld16x32bx2Trait":
"""
Create a trait object for the 16x32bx2 TMEM load operation.
:param copy_internal_type: The data type for the copy operation
:type copy_internal_type: Type[Numeric]
:param loc: MLIR location information for debugging, defaults to None
:type loc: optional
:param ip: MLIR insertion point for code generation, defaults to None
:type ip: optional
:param kwargs: Additional keyword arguments
:type kwargs: dict
:return: A trait object for this load operation
:rtype: Ld16x32bx2Trait
"""
ty = _cute_nvgpu_ir.CopyAtomSM100TmemLoadType.get(
copy_internal_type.mlir_type,
16,
@@ -251,7 +344,7 @@ class Ld16x32bx2Op(_LdBase):
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))
return Ld16x32bx2Trait(cute.make_atom(ty, loc=loc, ip=ip))
class Ld16x32bx2Trait(Trait):
@@ -270,6 +363,20 @@ class Ld32x32bOp(_LdBase):
def _make_trait(
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
) -> "Ld32x32bTrait":
"""
Create a trait object for the 32x32b TMEM load operation.
:param copy_internal_type: The data type for the copy operation
:type copy_internal_type: Type[Numeric]
:param loc: MLIR location information for debugging, defaults to None
:type loc: optional
:param ip: MLIR insertion point for code generation, defaults to None
:type ip: optional
:param kwargs: Additional keyword arguments
:type kwargs: dict
:return: A trait object for this load operation
:rtype: Ld32x32bTrait
"""
ty = _cute_nvgpu_ir.CopyAtomSM100TmemLoadType.get(
copy_internal_type.mlir_type,
32,
@@ -277,7 +384,7 @@ class Ld32x32bOp(_LdBase):
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))
return Ld32x32bTrait(cute.make_atom(ty, loc=loc, ip=ip))
class Ld32x32bTrait(Trait):
@@ -286,17 +393,30 @@ class Ld32x32bTrait(Trait):
@dataclass(frozen=True)
class _StBase(CopyOp):
"""
Base class for TMEM store operations in the tcgen05 instruction set.
This abstract base class provides common functionality and validation for tensor memory (TMEM)
store operations. It defines the fundamental parameters and architecture constraints that apply
to all store operation variants.
:param repeat: Number of repetitions for the store operation (required parameter)
:type repeat: Repetition
:param unpack: Unpacking pattern for RMEM to TMEM copies, defaults to Unpack.NONE
:type unpack: Unpack, optional
:raises OpError: If the current architecture is not supported or if invalid parameters are provided
"""
repeat: Repetition
unpack: Unpack = Unpack.NONE
admissible_archs = [
"sm_100a",
"sm_100f",
]
admissible_archs = Arch.filter(
lambda arch: arch.is_family_of(Arch.sm_100f) or arch.is_family_of(Arch.sm_110f)
)
def __post_init__(self) -> None:
# Arch verification
arch = CuTeDSL._get_dsl().envar.arch
arch = CuTeDSL._get_dsl().get_arch_enum()
if arch not in self.admissible_archs:
raise OpError(
self,
@@ -312,7 +432,7 @@ class _StBase(CopyOp):
if not isinstance(self.unpack, Unpack):
raise OpError(
self,
"expects the 'pack' Op parameter to be a tcgen05.Unpack instance",
"expects the 'unpack' Op parameter to be a tcgen05.Unpack instance",
)
def __str__(self) -> str:
@@ -321,7 +441,7 @@ class _StBase(CopyOp):
+ 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"
res += "\n with 32-bit to 2x 16b unpacking"
return res
@@ -337,6 +457,20 @@ class St16x64bOp(_StBase):
def _make_trait(
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
) -> "St16x64bTrait":
"""
Create a trait object for the 16x64b TMEM store operation.
:param copy_internal_type: The data type for the copy operation
:type copy_internal_type: Type[Numeric]
:param loc: MLIR location information for debugging, defaults to None
:type loc: optional
:param ip: MLIR insertion point for code generation, defaults to None
:type ip: optional
:param kwargs: Additional keyword arguments
:type kwargs: dict
:return: A trait object for this store operation
:rtype: St16x64bTrait
"""
ty = _cute_nvgpu_ir.CopyAtomSM100TmemStoreType.get(
copy_internal_type.mlir_type,
16,
@@ -344,7 +478,7 @@ class St16x64bOp(_StBase):
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))
return St16x64bTrait(cute.make_atom(ty, loc=loc, ip=ip))
class St16x64bTrait(Trait):
@@ -379,7 +513,7 @@ class St16x128bOp(_StBase):
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))
return St16x128bTrait(cute.make_atom(ty, loc=loc, ip=ip))
class St16x128bTrait(Trait):
@@ -414,7 +548,7 @@ class St16x256bOp(_StBase):
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))
return St16x256bTrait(cute.make_atom(ty, loc=loc, ip=ip))
class St16x256bTrait(Trait):
@@ -440,7 +574,7 @@ class St16x32bx2Op(_StBase):
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))
return St16x32bx2Trait(cute.make_atom(ty, loc=loc, ip=ip))
class St16x32bx2Trait(Trait):
@@ -466,7 +600,7 @@ class St32x32bOp(_StBase):
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))
return St32x32bTrait(cute.make_atom(ty, loc=loc, ip=ip))
class St32x32bTrait(Trait):
@@ -475,20 +609,27 @@ class St32x32bTrait(Trait):
@dataclass(frozen=True)
class _S2TCopyBase(CopyOp):
cta_group: CtaGroup
"""
Base class for SMEM to TMEM copy operations in the tcgen05 instruction set.
admissible_archs = [
"sm_100a",
"sm_100f",
]
This abstract base class provides common functionality and validation for shared memory (SMEM)
to tensor memory (TMEM) copy operations. These operations are used for high-throughput data
movement between different memory hierarchies in modern GPU architectures.
:param cta_group: Cooperative Thread Array (CTA) group configuration
:type cta_group: CtaGroup
:raises OpError: If the current architecture is not SM100f family or if invalid parameters are provided
"""
cta_group: CtaGroup
def __post_init__(self) -> None:
# Arch verification
arch = CuTeDSL._get_dsl().envar.arch
if arch not in self.admissible_archs:
arch = CuTeDSL._get_dsl().get_arch_enum()
if not arch.is_family_of(Arch.sm_100f):
raise OpError(
self,
f"expects arch to be one of {self.admissible_archs}, but got {arch}",
f"expects arch to be one of {Arch.filter(lambda arch: arch.is_family_of(Arch.sm_100f))}, but got {arch}",
suggestion="Ensure env CUTE_DSL_ARCH matches your GPU architecture",
)
# Verify that the user provided enum values
@@ -519,6 +660,20 @@ class Cp128x256bOp(_S2TCopyBase):
def _make_trait(
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
) -> "Cp128x256bTrait":
"""
Create a trait object for the 128x256b SMEM to TMEM copy operation.
:param copy_internal_type: The data type for the copy operation
:type copy_internal_type: Type[Numeric]
:param loc: MLIR location information for debugging, defaults to None
:type loc: optional
:param ip: MLIR insertion point for code generation, defaults to None
:type ip: optional
:param kwargs: Additional keyword arguments
:type kwargs: dict
:return: A trait object for this S2T copy operation
:rtype: Cp128x256bTrait
"""
ty = _cute_nvgpu_ir.CopyAtomSM100CopyS2TType.get(
copy_internal_type.mlir_type,
128,
@@ -526,7 +681,7 @@ class Cp128x256bOp(_S2TCopyBase):
self.cta_group.value,
_cute_nvgpu_ir.CopyS2TBroadcast.none,
)
return Cp128x256bTrait(_cute_ir.atom(ty, loc=loc, ip=ip))
return Cp128x256bTrait(cute.make_atom(ty, loc=loc, ip=ip))
class Cp128x256bTrait(Trait):
@@ -552,7 +707,7 @@ class Cp128x128bOp(_S2TCopyBase):
self.cta_group.value,
_cute_nvgpu_ir.CopyS2TBroadcast.none,
)
return Cp128x128bTrait(_cute_ir.atom(ty, loc=loc, ip=ip))
return Cp128x128bTrait(cute.make_atom(ty, loc=loc, ip=ip))
class Cp128x128bTrait(Trait):
@@ -578,7 +733,7 @@ class Cp4x256bOp(_S2TCopyBase):
self.cta_group.value,
_cute_nvgpu_ir.CopyS2TBroadcast.none,
)
return Cp4x256bTrait(_cute_ir.atom(ty, loc=loc, ip=ip))
return Cp4x256bTrait(cute.make_atom(ty, loc=loc, ip=ip))
class Cp4x256bTrait(Trait):
@@ -604,7 +759,7 @@ class Cp4x32x128bOp(_S2TCopyBase):
self.cta_group.value,
_cute_nvgpu_ir.CopyS2TBroadcast.x4,
)
return Cp4x32x128bTrait(_cute_ir.atom(ty, loc=loc, ip=ip))
return Cp4x32x128bTrait(cute.make_atom(ty, loc=loc, ip=ip))
class Cp4x32x128bTrait(Trait):
@@ -630,7 +785,7 @@ class Cp2x64x128b0213Op(_S2TCopyBase):
self.cta_group.value,
_cute_nvgpu_ir.CopyS2TBroadcast.lw_0213,
)
return Cp2x64x128b0213Trait(_cute_ir.atom(ty, loc=loc, ip=ip))
return Cp2x64x128b0213Trait(cute.make_atom(ty, loc=loc, ip=ip))
class Cp2x64x128b0213Trait(Trait):
@@ -656,7 +811,7 @@ class Cp2x64x128b0123Op(_S2TCopyBase):
self.cta_group.value,
_cute_nvgpu_ir.CopyS2TBroadcast.lw_0123,
)
return Cp2x64x128b0123Trait(_cute_ir.atom(ty, loc=loc, ip=ip))
return Cp2x64x128b0123Trait(cute.make_atom(ty, loc=loc, ip=ip))
class Cp2x64x128b0123Trait(Trait):

View File

@@ -13,7 +13,6 @@ 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
@@ -21,6 +20,7 @@ from ...typing import (
Shape,
IntTuple,
Layout,
ComposedLayout,
Tensor,
Int,
Numeric,
@@ -29,6 +29,8 @@ from ...typing import (
Int32,
)
from ... import core
from ...tensor import recast_tensor
from ...atom import CopyAtom, TiledCopy
from .mma import SmemLayoutAtomKind, CtaGroup
from .copy import (
Pack,
@@ -56,7 +58,7 @@ from .copy import (
@dsl_user_op
def make_smem_layout_atom(
kind: SmemLayoutAtomKind, element_type: Type[Numeric], *, loc=None, ip=None
) -> core.ComposedLayout:
) -> ComposedLayout:
"""
Makes a SMEM layout Atom.
@@ -68,7 +70,7 @@ def make_smem_layout_atom(
: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
:rtype: ComposedLayout
"""
if not isinstance(element_type, NumericMeta):
raise TypeError(f"element_type must be a Numeric, but got {element_type}")
@@ -130,13 +132,13 @@ def tile_to_mma_shape(
@overload
def tile_to_mma_shape(
atom: core.ComposedLayout,
atom: ComposedLayout,
mma_tile_shape: Shape,
order: IntTuple = None,
*,
loc=None,
ip=None,
) -> core.ComposedLayout: ...
) -> ComposedLayout: ...
@dsl_user_op
@@ -152,7 +154,7 @@ def tile_to_mma_shape(
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}"
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)
@@ -164,8 +166,12 @@ def tile_to_mma_shape(
):
raise ValueError("tile_to_mma_shape only supports static inputs")
if isinstance(atom, core._ComposedLayout):
atom = atom.value
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)
res_val = core.static(res_ty, loc=loc, ip=ip)
return core.coalesce(res_val, target_profile=mma_tile_shape, loc=loc, ip=ip)
@dsl_user_op
@@ -209,7 +215,7 @@ def commit(
####################################################################################################
def is_tmem_load(atom: core.CopyAtom) -> bool:
def is_tmem_load(atom: CopyAtom) -> bool:
"""
Returns whether a CopyAtom instance is a TMEM load.
"""
@@ -225,7 +231,7 @@ def is_tmem_load(atom: core.CopyAtom) -> bool:
)
def is_tmem_store(atom: core.CopyAtom) -> bool:
def is_tmem_store(atom: CopyAtom) -> bool:
"""
Returns whether a CopyAtom instance is a TMEM store.
"""
@@ -242,7 +248,7 @@ def is_tmem_store(atom: core.CopyAtom) -> bool:
def get_tmem_copy_properties(
atom: core.CopyAtom,
atom: CopyAtom,
) -> Tuple[int, int, int, Union[Pack, Unpack]]:
"""
Returns the properties of a TMEM copy atom (number of data paths, bits, repetitions,
@@ -279,7 +285,7 @@ def find_tmem_tensor_col_offset(tmem_tensor: Tensor, *, loc=None, ip=None) -> In
"""
tmem_col_mask = 0x0000FFFF
offset = (
core.cosize(core.recast_tensor(tmem_tensor, Int32).layout, loc=loc, ip=ip)
core.cosize(recast_tensor(tmem_tensor, Int32).layout, loc=loc, ip=ip)
& tmem_col_mask
)
if isinstance(offset, int):
@@ -289,8 +295,8 @@ def find_tmem_tensor_col_offset(tmem_tensor: Tensor, *, loc=None, ip=None) -> In
@dsl_user_op
def make_tmem_copy(
atom: core.CopyAtom, tmem_tensor: Tensor, *, loc=None, ip=None
) -> core.TiledCopy:
atom: CopyAtom, tmem_tensor: Tensor, *, loc=None, ip=None
) -> TiledCopy:
"""
Makes a Tiled Copy instance from a TMEM Copy Atom and a TMEM tensor.
"""
@@ -298,13 +304,13 @@ def 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)
return TiledCopy(atom.op, new_trait)
@dsl_user_op
def make_s2t_copy(
atom: core.CopyAtom, tmem_tensor: Tensor, *, loc=None, ip=None
) -> core.TiledCopy:
atom: CopyAtom, tmem_tensor: Tensor, *, loc=None, ip=None
) -> TiledCopy:
"""
Makes a Tiled Copy instance from a TMEM Copy Atom and a TMEM tensor.
"""
@@ -312,12 +318,12 @@ def make_s2t_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)
return TiledCopy(atom.op, new_trait)
@dsl_user_op
def get_s2t_smem_desc_tensor(
atom: core.CopyAtom, smem_tensor: Tensor, *, loc=None, ip=None
atom: CopyAtom, smem_tensor: Tensor, *, loc=None, ip=None
) -> Tensor:
"""
Returns the SMEM descriptor tensor from a S2T copy atom and a SMEM tensor.

View File

@@ -11,8 +11,10 @@
import enum
from dataclasses import dataclass
from typing import Type
from typing import Type, Any
from cutlass import cute
from cutlass.base_dsl.arch import Arch
from cutlass.cutlass_dsl import CuTeDSL, T
import cutlass._mlir.dialects.cute as _cute_ir
@@ -20,8 +22,9 @@ import cutlass._mlir.dialects.cute_nvgpu as _cute_nvgpu_ir
from cutlass._mlir import ir
from ..common import OpError
from ... import core
from ...core import Trait, _pack_shape, rank, depth, _Tensor
from ... import core, atom
from ...core import _pack_shape, rank, depth
from ...tensor import _Tensor
from ...typing import (
Shape,
Float4E2M1FN,
@@ -40,6 +43,9 @@ from ...typing import (
AddressSpace,
Pointer,
)
from ...atom import Trait
from ..warp.mma import SparseMetadataFormat
####################################################################################################
@@ -49,6 +55,14 @@ from ...typing import (
####################################################################################################
class Tcgen05MmaOp(atom.MmaOp):
"""
Base class for all tcgen05 MMA operations.
"""
pass
class OperandMajorMode(enum.Enum):
"""
An enumeration for the majorness of the input operands of the MMA.
@@ -108,6 +122,7 @@ class CtaGroup(enum.Enum):
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.
@@ -131,7 +146,7 @@ class Field(enum.Enum):
# Base class for all tcgen05 MMA Ops with syntax `tcgen05.mma.cta_group.kind` used to factor out some internal code
@dataclass(frozen=True)
class MmaOp(core.MmaOp):
class MmaOp(Tcgen05MmaOp):
a_dtype: Type[Numeric]
b_dtype: Type[Numeric]
acc_dtype: Type[Numeric]
@@ -141,14 +156,13 @@ class MmaOp(core.MmaOp):
a_major_mode: OperandMajorMode
b_major_mode: OperandMajorMode
admissible_archs = [
"sm_100a",
"sm_100f",
]
admissible_archs = Arch.filter(
lambda arch: arch.is_family_of(Arch.sm_100f) or arch.is_family_of(Arch.sm_110f)
)
def __post_init__(self) -> None:
# Verify arch
arch = CuTeDSL._get_dsl().envar.arch
arch = CuTeDSL._get_dsl().get_arch_enum()
if arch not in self.admissible_archs:
raise OpError(
self,
@@ -194,18 +208,18 @@ class MmaOp(core.MmaOp):
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):
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 % 16 == 0, but got {n}",
f"expects the N-mode to satisfy 8 <= N <= 256 and N % 8 == 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):
if (n < 16) or (n > 256) or (n % 16 != 0):
raise OpError(
self,
f"expects the N-mode to satisfy 32 <= N <= 256 and N % 32 == 0, but got {n}",
f"expects the N-mode to satisfy 16 <= N <= 256 and N % 16 == 0, but got {n}",
)
def __str__(self) -> str:
@@ -246,7 +260,7 @@ class MmaOp(core.MmaOp):
return True
class MmaTrait(Trait):
class MmaTraits(Trait):
admissible_fields = [Field.ACCUMULATE, Field.NEGATE_A, Field.NEGATE_B]
def set(self, field, value, *, loc=None, ip=None) -> None:
@@ -260,10 +274,21 @@ class MmaTrait(Trait):
self.value, attr, Boolean(value).ir_value(loc=loc, ip=ip), loc=loc, ip=ip
)
def get(self, field, *, loc=None, ip=None) -> Any:
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)
return _cute_nvgpu_ir.atom_get_value(
Boolean.mlir_type, self.value, attr, loc=loc, ip=ip
)
# Base class for all tcgen05 BlockScaled MMA Ops with syntax `tcgen05.mma.cta_group.kind.block_scale` used to factor out some internal code
@dataclass(frozen=True)
class BlockScaledMmaOp(core.MmaOp):
class BlockScaledMmaOp(Tcgen05MmaOp):
a_dtype: Type[Numeric]
b_dtype: Type[Numeric]
acc_dtype: Float32
@@ -276,12 +301,12 @@ class BlockScaledMmaOp(core.MmaOp):
b_major_mode: OperandMajorMode
admissible_archs = [
"sm_100a",
Arch.sm_100a,
]
def __post_init__(self) -> None:
# Verify arch
arch = CuTeDSL._get_dsl().envar.arch
arch = CuTeDSL._get_dsl().get_arch_enum()
if arch not in self.admissible_archs:
raise OpError(
self,
@@ -409,6 +434,170 @@ class BlockScaledMmaTraits(Trait):
self.value, attr, value, loc=loc, ip=ip
)
def get(self, field, *, loc=None, ip=None) -> Any:
if field not in [Field.ACCUMULATE, Field.NEGATE_A, Field.NEGATE_B]:
raise ValueError(f"the get method for {field} is not supported")
field_name = f"#cute_nvgpu.atom_mma_field_sm100_block_scaled<{field._to_ir_field_name()}>"
attr = ir.Attribute.parse(field_name)
return _cute_nvgpu_ir.atom_get_value(
Boolean.mlir_type, self.value, attr, loc=loc, ip=ip
)
# Base class for all tcgen05 Sparse MMA Ops with syntax `tcgen05.mma.cta_group.kind.sparse` used to factor out some internal code
@dataclass(frozen=True)
class SparseMmaOp(Tcgen05MmaOp):
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
sparse_metadata_format: SparseMetadataFormat
admissible_archs = Arch.filter(
lambda arch: arch.is_family_of(Arch.sm_100f) or arch.is_family_of(Arch.sm_110f)
)
def __post_init__(self) -> None:
# Verify arch
arch = CuTeDSL._get_dsl().get_arch_enum()
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",
)
if not isinstance(self.sparse_metadata_format, SparseMetadataFormat):
raise OpError(
self,
"expects the 'sparse_metadata_format' Op parameter to be a tcgen05.SparseMetadataFormat 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]
# For sparse MMA, the shape validation follows the same rules as dense MMA
# but the K dimension is typically doubled in the derived classes
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 16 <= 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}"
+ f"\n Sparse metadata format = {self.sparse_metadata_format}"
)
def _verify_fragment_A(self, input: _Tensor, *, loc=None, ip=None):
if input.memspace == AddressSpace.smem and isinstance(
input.layout.type, _cute_ir.ComposedLayoutType
):
raise OpError(
self,
f"Expected affine layout for {self._make_trait()}'s operand A, "
f"but got composed layout instead: {input.layout}"
f"\nPlease use recast_ptr(ptr, {input.layout.inner}, element_type) operation to move swizzle to the ptr",
)
return True
def _verify_fragment_B(self, input: _Tensor, *, loc=None, ip=None):
if input.memspace == AddressSpace.smem and isinstance(
input.layout.type, _cute_ir.ComposedLayoutType
):
raise OpError(
self,
f"Expected affine layout for {self._make_trait()}'s operand B, "
f"but got composed layout instead: {input.layout}"
f"\nPlease use recast_ptr(ptr, {input.layout.inner}, element_type) operation to move swizzle to the ptr",
)
return True
class SparseMmaTraits(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_sparse<{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
)
def get(self, field, *, loc=None, ip=None) -> Any:
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_sparse<{field._to_ir_field_name()}>"
)
attr = ir.Attribute.parse(field_name)
return _cute_nvgpu_ir.atom_get_value(
Boolean.mlir_type, self.value, attr, loc=loc, ip=ip
)
#
# TF32 MMA
@@ -472,18 +661,20 @@ class MmaTF32Op(MmaOp):
0,
)
return MmaTF32Trait(
_cute_nvgpu_ir.make_sm100_mma(
cute.make_atom(
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),
(
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):
class MmaTF32Trait(MmaTraits):
pass
@@ -564,18 +755,123 @@ class MmaF16BF16Op(MmaOp):
0,
)
return MmaF16BF16Trait(
_cute_nvgpu_ir.make_sm100_mma(
cute.make_atom(
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),
(
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):
class MmaF16BF16Trait(MmaTraits):
pass
@dataclass(frozen=True)
class MmaF16BF16SparseOp(SparseMmaOp):
"""
F16/BF16 tcgen05 Sparse MMA Operation.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#tcgen05-mma-instructions-mma-sp>`__.
This Operation corresponds to the ``.kind::f16`` qualifier with sparse support.
"""
descriptive_name = "tcgen05 F16/BF16 Sparse 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,
sparse_metadata_format: SparseMetadataFormat,
) -> None:
super().__init__(
ab_dtype,
ab_dtype,
acc_dtype,
instruction_shape,
cta_group,
a_src,
a_major_mode,
b_major_mode,
sparse_metadata_format,
)
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 = 32 # For sparse, K is doubled compared to dense F16/BF16
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) -> "MmaF16BF16SparseTrait":
shape_mnk = _pack_shape(self.shape_mnk, loc=loc, ip=ip)
ty = _cute_nvgpu_ir.MmaAtomSM100UMMASparseType.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,
T.ui8(),
self.sparse_metadata_format._to_ir(),
self.a_src._to_ir(),
0, # cScaleExp
)
def get_e_ptr():
ptr_type = _cute_ir.PtrType.get(T.ui8(), _cute_ir.AddressSpace.tmem, 8)
address_value = Int32(0).ir_value(loc=loc, ip=ip)
aligned_ty = _cute_ir.ConstrainedIntType.get(8, 32)
aligned_intptr = _cute_ir.assume(aligned_ty, address_value, loc=loc, ip=ip)
ui8_tmem_ptr = _cute_ir.inttoptr(ptr_type, aligned_intptr, loc=loc, ip=ip)
return ui8_tmem_ptr
return MmaF16BF16SparseTrait(
cute.make_atom(
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),
get_e_ptr().value,
),
loc=loc,
ip=ip,
)
)
class MmaF16BF16SparseTrait(SparseMmaTraits):
pass
@@ -649,18 +945,20 @@ class MmaI8Op(MmaOp):
0,
)
return MmaI8Trait(
_cute_nvgpu_ir.make_sm100_mma(
cute.make_atom(
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),
(
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):
class MmaI8Trait(MmaTraits):
pass
@@ -689,7 +987,6 @@ class MmaFP8Op(MmaOp):
a_major_mode: OperandMajorMode,
b_major_mode: OperandMajorMode,
) -> None:
super().__init__(
ab_dtype,
ab_dtype,
@@ -741,18 +1038,20 @@ class MmaFP8Op(MmaOp):
0,
)
return MmaFP8Trait(
_cute_nvgpu_ir.make_sm100_mma(
cute.make_atom(
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),
(
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):
class MmaFP8Trait(MmaTraits):
pass
@@ -829,13 +1128,19 @@ class MmaMXF8Op(BlockScaledMmaOp):
self.sf_vec_size,
)
return MmaMXF8Trait(
_cute_nvgpu_ir.make_sm100_mma_bs(
cute.make_atom(
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),
core.make_ptr(self.sf_dtype, 0, _cute_ir.AddressSpace.tmem).value,
core.make_ptr(self.sf_dtype, 0, _cute_ir.AddressSpace.tmem).value,
(
Boolean(False).ir_value(loc=loc, ip=ip),
Boolean(False).ir_value(loc=loc, ip=ip),
Boolean(False).ir_value(loc=loc, ip=ip),
core.make_ptr(
self.sf_dtype, 0, _cute_ir.AddressSpace.tmem, loc=loc, ip=ip
).value,
core.make_ptr(
self.sf_dtype, 0, _cute_ir.AddressSpace.tmem, loc=loc, ip=ip
).value,
),
loc=loc,
ip=ip,
)
@@ -909,13 +1214,19 @@ class MmaMXF4Op(BlockScaledMmaOp):
self.sf_vec_size,
)
return MmaMXF4Trait(
_cute_nvgpu_ir.make_sm100_mma_bs(
cute.make_atom(
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),
core.make_ptr(self.sf_dtype, 0, _cute_ir.AddressSpace.tmem).value,
core.make_ptr(self.sf_dtype, 0, _cute_ir.AddressSpace.tmem).value,
(
Boolean(False).ir_value(loc=loc, ip=ip),
Boolean(False).ir_value(loc=loc, ip=ip),
Boolean(False).ir_value(loc=loc, ip=ip),
core.make_ptr(
self.sf_dtype, 0, _cute_ir.AddressSpace.tmem, loc=loc, ip=ip
).value,
core.make_ptr(
self.sf_dtype, 0, _cute_ir.AddressSpace.tmem, loc=loc, ip=ip
).value,
),
loc=loc,
ip=ip,
)
@@ -996,13 +1307,19 @@ class MmaMXF4NVF4Op(BlockScaledMmaOp):
self.sf_vec_size,
)
return MmaMXF4NVF4Trait(
_cute_nvgpu_ir.make_sm100_mma_bs(
cute.make_atom(
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),
core.make_ptr(self.sf_dtype, 0, _cute_ir.AddressSpace.tmem).value,
core.make_ptr(self.sf_dtype, 0, _cute_ir.AddressSpace.tmem).value,
(
Boolean(False).ir_value(loc=loc, ip=ip),
Boolean(False).ir_value(loc=loc, ip=ip),
Boolean(False).ir_value(loc=loc, ip=ip),
core.make_ptr(
self.sf_dtype, 0, _cute_ir.AddressSpace.tmem, loc=loc, ip=ip
).value,
core.make_ptr(
self.sf_dtype, 0, _cute_ir.AddressSpace.tmem, loc=loc, ip=ip
).value,
),
loc=loc,
ip=ip,
)
@@ -1012,6 +1329,7 @@ class MmaMXF4NVF4Op(BlockScaledMmaOp):
class MmaMXF4NVF4Trait(BlockScaledMmaTraits):
pass
####################################################################################################
#
# SMEM layout atoms

View File

@@ -12,13 +12,14 @@
from dataclasses import dataclass
from typing import Type
import cutlass._mlir.dialects.cute as _cute_ir
from cutlass import cute
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 ...core import _pack_shape
from ...typing import Numeric
from ...atom import CopyOp, Trait
@dataclass(frozen=True)
@@ -39,7 +40,7 @@ class BaseOp(CopyOp):
+ f"\n number of matrices = {self.num_matrices}"
)
if self.transpose:
res += f"\n transposed"
res += "\n transposed"
return res
@@ -71,7 +72,7 @@ class LdMatrix8x8x16bOp(BaseOp):
self.num_matrices,
ir.UnitAttr.get() if self.transpose else None,
)
return LdMatrix8x8x16bTrait(_cute_ir.atom(ty, loc=loc, ip=ip))
return LdMatrix8x8x16bTrait(cute.make_atom(ty, loc=loc, ip=ip))
class LdMatrix8x8x16bTrait(Trait):
@@ -110,7 +111,7 @@ class LdMatrix16x16x8bOp(BaseOp):
self.num_matrices,
ir.UnitAttr.get(),
)
return LdMatrix16x16x8bTrait(_cute_ir.atom(ty, loc=loc, ip=ip))
return LdMatrix16x16x8bTrait(cute.make_atom(ty, loc=loc, ip=ip))
class LdMatrix16x16x8bTrait(Trait):
@@ -144,7 +145,7 @@ class StMatrix8x8x16bOp(BaseOp):
self.num_matrices,
ir.UnitAttr.get() if self.transpose else None,
)
return StMatrix8x8x16bTrait(_cute_ir.atom(ty, loc=loc, ip=ip))
return StMatrix8x8x16bTrait(cute.make_atom(ty, loc=loc, ip=ip))
class StMatrix8x8x16bTrait(Trait):
@@ -182,7 +183,7 @@ class StMatrix16x8x8bOp(BaseOp):
self.num_matrices,
ir.UnitAttr.get(),
)
return StMatrix16x8x8bTrait(_cute_ir.atom(ty, loc=loc, ip=ip))
return StMatrix16x8x8bTrait(cute.make_atom(ty, loc=loc, ip=ip))
class StMatrix16x8x8bTrait(Trait):

View File

@@ -12,16 +12,36 @@
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
import enum
from cutlass import cute
from ..common import OpError
from ...core import MmaOp, Trait, _pack_shape, _Tensor
from ...typing import Shape, Float16, BFloat16, Float32, Numeric, AddressSpace
from ...typing import Shape, Float16, BFloat16, Float32, Numeric
from ...core import _pack_shape
from ...tensor import _Tensor
from ...atom import MmaOp, Trait
import cutlass._mlir.dialects.cute_nvgpu as _cute_nvgpu_ir
from cutlass._mlir.dialects.cute_nvgpu import SparseMetadataFormat
####################################################################################################
#
# MMA Ops and Traits
#
####################################################################################################
class WarpMmaOp(MmaOp):
"""
Base class for all warp-level MMA operations.
"""
pass
@dataclass(frozen=True)
class MmaF16BF16Op(MmaOp):
class MmaF16BF16Op(WarpMmaOp):
"""
F16/BF16 tcgen05 MMA Operation.
@@ -63,7 +83,7 @@ class MmaF16BF16Op(MmaOp):
self.ab_dtype.mlir_type,
self.acc_dtype.mlir_type,
)
return MmaF16BF16Trait(_cute_ir.atom(ty, loc=loc, ip=ip))
return MmaF16BF16Trait(cute.make_atom(ty, loc=loc, ip=ip))
def __str__(self) -> str:
return (
@@ -79,5 +99,84 @@ class MmaF16BF16Op(MmaOp):
def _verify_fragment_B(self, input: _Tensor, *, loc=None, ip=None):
pass
class MmaF16BF16Trait(Trait):
pass
class SparseMetadataFormat(enum.Enum):
"""
An enumeration for the sparse metadata format of the MMA.
"""
TID = SparseMetadataFormat.tid
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.SparseMetadataFormat:
return self.value
@dataclass(frozen=True)
class MmaF16BF16SparseOp(WarpMmaOp):
ab_dtype: Type[Numeric]
acc_dtype: Type[Numeric]
shape_mnk: Shape
sparse_metadata_format: SparseMetadataFormat
def __post_init__(self) -> None:
# verify field after initialization
if not isinstance(self.sparse_metadata_format, SparseMetadataFormat):
raise OpError(
self,
"expects the 'sparse_metadata_format' Op parameter to be a SparseMetadataFormat instance",
)
# verify the instruction shape
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, 16), (16, 8, 32)]:
raise OpError(
self,
"expects the 'shape_mnk' Op parameter to be one of (16,8,16) or (16,8,32)",
)
def _make_trait(self, *, loc=None, ip=None, **kwargs) -> "MmaF16BF16SparseTrait":
shape_mnk = _pack_shape(self.shape_mnk, loc=loc, ip=ip)
ty = _cute_nvgpu_ir.MmaAtomSM80SparseType.get(
shape_mnk.type.attribute,
self.ab_dtype.mlir_type,
self.ab_dtype.mlir_type,
self.acc_dtype.mlir_type,
self.sparse_metadata_format._to_ir(),
)
return MmaF16BF16SparseTrait(cute.make_atom(ty, loc=loc, ip=ip))
def __str__(self) -> str:
return (
"warp-level F16/BF16 Sparse 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}"
+ f"\n Sparse metadata format = {self.sparse_metadata_format}"
)
class MmaF16BF16SparseTrait(Trait):
pass

View File

@@ -15,7 +15,7 @@ from cutlass.cutlass_dsl import dsl_user_op
from cutlass._mlir.dialects import nvvm
from ...typing import Numeric, NumericMeta
from ...typing import Numeric, NumericMeta, ComposedLayout
from ... import core
from .mma import SmemLayoutAtomKind
@@ -23,7 +23,7 @@ from .mma import SmemLayoutAtomKind
@dsl_user_op
def make_smem_layout_atom(
kind: SmemLayoutAtomKind, element_type: Type[Numeric], *, loc=None, ip=None
) -> core.ComposedLayout:
) -> ComposedLayout:
"""
Makes a SMEM layout Atom.
@@ -35,7 +35,7 @@ def make_smem_layout_atom(
: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
:rtype: ComposedLayout
"""
if not isinstance(element_type, NumericMeta):
raise TypeError(f"element_type must be a Numeric, but got {element_type}")

View File

@@ -11,16 +11,19 @@
import enum
from dataclasses import dataclass
from typing import Type
from typing import Type, Any
from cutlass.cutlass_dsl import CuTeDSL
from cutlass import cute
from cutlass.base_dsl.arch import Arch
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, _Tensor
from ...core import _pack_shape, rank, depth
from ...tensor import _Tensor
from ...typing import (
Shape,
Float16,
@@ -29,9 +32,13 @@ from ...typing import (
Boolean,
Float8E5M2,
Float8E4M3FN,
Int32,
Int8,
Uint8,
Numeric,
AddressSpace,
)
from ...atom import MmaOp, Trait
####################################################################################################
@@ -41,6 +48,14 @@ from ...typing import (
####################################################################################################
class WarpGroupMmaOp(MmaOp):
"""
Base class for all warpgroup-level MMA operations.
"""
pass
class OperandMajorMode(enum.Enum):
"""
An enumeration for the majorness of the input operands of the MMA.
@@ -104,7 +119,7 @@ class Field(enum.Enum):
@dataclass(frozen=True)
class MmaOp(MmaOp):
class MmaOp(WarpGroupMmaOp):
a_dtype: Type[Numeric]
b_dtype: Type[Numeric]
acc_dtype: Type[Numeric]
@@ -113,15 +128,13 @@ class MmaOp(MmaOp):
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:
arch = CuTeDSL._get_dsl().get_arch_enum()
if not arch == Arch.sm_90a:
raise OpError(
self,
f"expects arch to be one of {self.admissible_archs}, but got {arch}",
f"expects arch to be {Arch.sm_90a}, but got {arch}",
suggestion="Ensure env CUTE_DSL_ARCH matches your GPU architecture",
)
# Verify that the user provided enum values
@@ -193,7 +206,7 @@ class MmaOp(MmaOp):
return True
class MmaTrait(Trait):
class MmaTraits(Trait):
admissible_fields = [Field.ACCUMULATE]
def set(self, field, value, *, loc=None, ip=None) -> None:
@@ -207,13 +220,24 @@ class MmaTrait(Trait):
self.value, attr, Boolean(value).ir_value(loc=loc, ip=ip), loc=loc, ip=ip
)
def get(self, field, *, loc=None, ip=None) -> Any:
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)
return _cute_nvgpu_ir.atom_get_value(
Boolean.mlir_type, self.value, attr, 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>`__.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#asynchronous-warpgroup-level-matrix-instructions-wgmma-mma>`__.
This Operation covers the instructions using the ``.f16`` or ``.bf16`` qualifiers for the input operands.
"""
@@ -281,16 +305,16 @@ class MmaF16BF16Op(MmaOp):
self.a_src._to_ir(),
)
return MmaF16BF16Trait(
_cute_nvgpu_ir.make_sm90_mma(
cute.make_atom(
ty,
Boolean(False).ir_value(loc=loc, ip=ip),
(Boolean(False).ir_value(loc=loc, ip=ip),),
loc=loc,
ip=ip,
)
)
class MmaF16BF16Trait(MmaTrait):
class MmaF16BF16Trait(MmaTraits):
pass
@@ -299,7 +323,7 @@ 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>`__.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#asynchronous-warpgroup-level-matrix-instructions-wgmma-mma>`__.
This Operation covers the instructions using the ``.e4m3`` or ``.e5m2`` qualifiers for the input operands.
"""
@@ -367,13 +391,111 @@ class MmaF8Op(MmaOp):
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
cute.make_atom(
ty,
(Boolean(False).ir_value(loc=loc, ip=ip),),
loc=loc,
ip=ip,
)
)
class MmaF8Trait(MmaTrait):
class MmaF8Trait(MmaTraits):
pass
@dataclass(frozen=True)
class MmaI8Op(MmaOp):
"""
I8 warpgroup MMA Operation.
See the `PTX documentation <https://docs.nvidia.com/cuda/parallel-thread-execution/#asynchronous-warpgroup-level-matrix-instructions-wgmma-mma>`__.
This Operation covers the instructions using the ``.s8`` or ``.u8`` qualifiers for the input operands.
"""
descriptive_name = "warpgroup I8 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 [Int8, Uint8]:
raise OpError(
self,
"expects the 'a_dtype' Op parameter to be one of Int8 or Uint8",
)
if self.b_dtype not in [Int8, Uint8]:
raise OpError(
self,
"expects the 'b_dtype' Op parameter to be one of Int8 or Uint8",
)
# Accumulator data type verification
if self.acc_dtype != Int32:
raise OpError(
self,
"expects the 'acc_dtype' Op parameter must be Int32",
)
# 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]}",
)
n = self.shape_mnk[1]
if not (n >= 8 and n <= 256 and (n == 8 or n == 24 or n % 16 == 0)):
raise OpError(
self,
"expects the N-mode to satisfy N=8*i where i={1,2,3,4} ",
f"or N=16*i where i={{3,4,...,15,16}}. But got {n}",
)
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.MmaAtomSM90Type.get(
shape_mnk.type.attribute,
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()),
self.acc_dtype.mlir_type,
self.a_src._to_ir(),
)
return MmaI8Trait(
cute.make_atom(
ty,
(Boolean(False).ir_value(loc=loc, ip=ip),),
loc=loc,
ip=ip,
)
)
class MmaI8Trait(MmaTraits):
pass

View File

@@ -13,41 +13,18 @@ import ctypes
from functools import lru_cache
import itertools
import operator
from time import time
from typing import Union
from typing import Union, Optional
# MLIR modules imports
from cutlass._mlir import ir
import cutlass._mlir.dialects.cute as _cute_ir
from cutlass.base_dsl.dsl import is_dynamic_expression
from cutlass.cutlass_dsl import JitArgAdapterRegistry
from cutlass.cutlass_dsl import JitArgAdapterRegistry, DSLRuntimeError
# 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 .typing import AddressSpace, Tensor, Type, Pointer, Numeric
from . import core
from .core import _Tensor as CoreTensor
from .tensor import _Tensor as CoreTensor
class _Pointer(Pointer):
@@ -88,9 +65,9 @@ class _Pointer(Pointer):
self._assumed_align = assumed_align
self._c_pointer = None
assert (
int(self._pointer) % self._assumed_align == 0
), f"pointer must be {self._assumed_align} bytes aligned"
assert int(self._pointer) % self._assumed_align == 0, (
f"pointer must be {self._assumed_align} bytes aligned"
)
def size_in_bytes(self) -> int:
self._desc = ctypes.c_void_p(int(self._pointer))
@@ -109,9 +86,6 @@ class _Pointer(Pointer):
assert len(values) == 1
return values[0]
def __extract_mlir_values__(self):
return [self._c_pointer]
# Move mlir Type out of __init__ to decouple with mlir Context
@property
def mlir_type(self) -> ir.Type:
@@ -146,11 +120,7 @@ class _Pointer(Pointer):
class _Tensor(Tensor):
def __init__(
self,
tensor,
assumed_align=None,
):
def __init__(self, tensor, assumed_align=None, use_32bit_stride=False):
# If tensor is already a DLPack object, use it directly
if hasattr(tensor, "__dlpack_device__") and not hasattr(tensor, "__dlpack__"):
self._dlpack_data = tensor
@@ -161,13 +131,13 @@ class _Tensor(Tensor):
self._is_dynamic = False
self._memref_desc = None
self._dtype = None
self._use_32bit_stride = use_32bit_stride
@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.
@@ -177,13 +147,15 @@ class _Tensor(Tensor):
def wrapper(self, *args, **kwargs):
if self._dltensor_wrapper is None:
self._dltensor_wrapper = _cute_ir.DLTensorWrapper(self._dlpack_data)
self._dltensor_wrapper = _cute_ir.DLTensorWrapper(
self._dlpack_data, self._use_32bit_stride
)
return func(self, *args, **kwargs)
return wrapper
@lazily_load_dltensor
def mark_layout_dynamic(self, leading_dim: int | None = None):
def mark_layout_dynamic(self, leading_dim: Optional[int] = None):
"""Marks the tensor layout as dynamic based on the leading dimension.
:param leading_dim: The leading dimension of the layout, defaults to None
@@ -209,7 +181,7 @@ class _Tensor(Tensor):
def mark_compact_shape_dynamic(
self,
mode: int,
stride_order: tuple[int, ...] | None = None,
stride_order: Optional[tuple[int, ...]] = None,
divisibility: int = 1,
):
"""Marks the tensor shape as dynamic and propagates dynamic and divisibility information to the corresponding strides.
@@ -308,10 +280,10 @@ class _Tensor(Tensor):
return self.__str__()
def __setitem__(self, crd, value):
raise TypeError(f"runtime._Tensor is not indexable")
raise TypeError("runtime._Tensor is not indexable")
def __getitem__(self, crd):
raise TypeError(f"runtime._Tensor is not indexable")
raise TypeError("runtime._Tensor is not indexable")
@property
@lazily_load_dltensor
@@ -326,7 +298,7 @@ class _Tensor(Tensor):
@property
def layout(self):
raise NotImplementedError(
f"layout property is not supported in runtime, support in future"
"layout property is not supported in runtime, support in future"
)
@property
@@ -363,7 +335,7 @@ class _Tensor(Tensor):
return core.leading_dim(self.shape, self.stride)
def fill(self, value: Numeric):
raise TypeError(f"fill function is not supported in runtime")
raise TypeError("fill function is not supported in runtime")
@property
@lazily_load_dltensor
@@ -389,6 +361,7 @@ class _Tensor(Tensor):
def from_dlpack(
tensor_dlpack,
assumed_align=None,
use_32bit_stride=False,
) -> Tensor:
"""Convert from tensor object supporting __dlpack__() to a CuTe Tensor.
@@ -397,6 +370,10 @@ def from_dlpack(
: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
:param use_32bit_stride: Whether to use 32-bit stride, defaults to False. When True, the dynamic
stride bitwidth will be set to 32 for small problem size (cosize(layout) <= Int32_max) for better performance.
This is only applied when the dimension is dynamic.
:type use_32bit_stride: bool, optional
:return: A CuTe Tensor object
:rtype: Tensor
@@ -415,6 +392,7 @@ def from_dlpack(
return _Tensor(
tensor_dlpack,
assumed_align=assumed_align,
use_32bit_stride=use_32bit_stride,
)

File diff suppressed because it is too large Load Diff

View File

@@ -13,37 +13,37 @@ import functools
import inspect
import logging
import os
from enum import Enum
from inspect import isclass
from itertools import product
from time import time
from typing import Any, Callable, Dict, List, Optional, Type, Union
from typing import Type, Union, Callable, Optional, Dict, List, Any
import cuda.bindings.driver as cuda_driver
import cuda.bindings.runtime as cuda_runtime
import numpy as np
import cutlass._mlir.ir as ir
import cutlass.base_dsl.jit_executor
from cutlass.cutlass_dsl import Constexpr, CuTeDSL, T, dsl_user_op
from .typing import Numeric, Int8, Boolean
import cutlass.cute as cute
from cutlass.cute import nvgpu
from cutlass._mlir.dialects import builtin, cf, nvvm, vector
from cutlass.cute import core, nvgpu
from cutlass.cutlass_dsl import Constexpr, CuTeDSL, T, t, dsl_user_op
@dsl_user_op
def assert_(cond, msg=None, *, loc=None, ip=None):
cf.assert_(t.Boolean(cond).ir_value(), msg if msg else "", loc=loc, ip=ip)
cf.assert_(Boolean(cond).ir_value(), msg if msg else "", loc=loc, ip=ip)
def _maybe_recast_tensor_from_f4(src: core.Tensor, tv_layout: core.Layout):
def _maybe_recast_tensor_from_f4(src: cute.Tensor, tv_layout: cute.Layout):
if src.element_type.width == 4:
tv_layout = core.recast_layout(8, 4, tv_layout)
src = core.recast_tensor(src, dtype=t.Int8)
tv_layout = cute.recast_layout(8, 4, tv_layout)
src = cute.recast_tensor(src, dtype=Int8)
return src, tv_layout
def _maybe_recast_to_f4(input: core.TensorSSA, dtype: Type[core.Numeric]):
def _maybe_recast_to_f4(input: cute.TensorSSA, dtype: Type[Numeric]):
"""Conditionally recasts the tensor to 4-bit type if the destination type is 4-bit.
:param input: The input tensor to recast.
@@ -51,22 +51,22 @@ def _maybe_recast_to_f4(input: core.TensorSSA, dtype: Type[core.Numeric]):
: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):
if not inspect.isclass(dtype) or not issubclass(dtype, 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
recast_shape = cute.recast_layout(4, 8, cute.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 cute.TensorSSA(res_vect, recast_shape, dtype)
return input
def _maybe_recast_from_f4(input: core.TensorSSA, src_dtype: Type[core.Numeric]):
def _maybe_recast_from_f4(input: cute.TensorSSA, src_dtype: Type[Numeric]):
"""Conditionally recasts the tensor from 4-bit type if the source type is 4-bit.
:param input: The input tensor to recast.
@@ -74,27 +74,27 @@ def _maybe_recast_from_f4(input: core.TensorSSA, src_dtype: Type[core.Numeric]):
: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):
if not inspect.isclass(src_dtype) or not issubclass(src_dtype, 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
recast_shape = cute.recast_layout(8, 4, cute.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 cute.TensorSSA(res_vect, recast_shape, 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,
gSrc: cute.Tensor,
gDst: cute.Tensor,
cSrc: cute.Tensor,
src_tv_layout: cute.Layout,
dst_tv_layout: cute.Layout,
src_shape: cute.Shape,
src_ty,
dst_ty,
):
@@ -110,9 +110,9 @@ def _convert_kernel(
# 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)
tidfrgSrc = cute.composition(ctaSrc, src_tv_layout) # (T,V)
tidfrgDst = cute.composition(ctaDst, dst_tv_layout) # (T,V)
tidfrgCSrc = cute.composition(ctaCSrc, src_tv_layout) # (T,V)
# print(f"tidfrgSrc = {tidfrgSrc.type}")
# slice for threads
@@ -123,19 +123,19 @@ def _convert_kernel(
# print(f"thrSrc = {thrSrc.type}")
# predicate
if core.elem_less(thrCSrc[0], src_shape):
if cute.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
frgSrc = cute.make_rmem_tensor(
cute.get(src_tv_layout, mode=[1]), gSrc.element_type
) # (V)
frgDst = core.make_fragment(
core.get(dst_tv_layout, mode=[1]), gDst.element_type
frgDst = cute.make_rmem_tensor(
cute.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)
copy_atom_load = cute.make_copy_atom(nvgpu.CopyUniversalOp(), gSrc.element_type)
cute.copy(copy_atom_load, thrSrc, frgSrc)
vec_src = frgSrc.load()
vec_src = _maybe_recast_to_f4(vec_src, src_ty)
@@ -144,49 +144,48 @@ def _convert_kernel(
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)
copy_atom_stg = cute.make_copy_atom(nvgpu.CopyUniversalOp(), gDst.element_type)
cute.copy(copy_atom_stg, frgDst, thrDst)
@CuTeDSL.jit(preprocess=False)
def _convert(
src: core.Tensor,
dst: core.Tensor,
src: cute.Tensor,
dst: cute.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))
tv_layout = cute.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)
idA = cute.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,...)
] * cute.rank(src.layout)
src_cta_tiler[leading_mode] = cute.size(src_tv_layout) # (...,TileV,...)
dst_cta_tiler = [
1,
] * core.rank(dst.layout)
dst_cta_tiler[leading_mode] = core.size(dst_tv_layout) # (...,TileV,...)
] * cute.rank(dst.layout)
dst_cta_tiler[leading_mode] = cute.size(dst_tv_layout) # (...,TileV,...)
# Step 4. partition input and output tensor by cta tiler.
gS = core.zipped_divide(
gS = cute.zipped_divide(
src, tuple(src_cta_tiler)
) # ((...,TileV,...),(...,RestV,...))
cS = core.zipped_divide(
cS = cute.zipped_divide(
idA, tuple(src_cta_tiler)
) # ((...,TileV,...),(...,RestV,...))
gD = core.zipped_divide(
gD = cute.zipped_divide(
dst, tuple(dst_cta_tiler)
) # ((...,TileV,...),(...,RestV,...))
# print(f"{gS.type=}")
@@ -201,8 +200,8 @@ def _convert(
src_ty,
dst_ty,
).launch(
grid=[core.size(gS, mode=[1]), 1, 1],
block=[core.size(src_tv_layout, mode=[0]), 1, 1],
grid=[cute.size(gS, mode=[1]), 1, 1],
block=[cute.size(src_tv_layout, mode=[0]), 1, 1],
)
@@ -210,10 +209,10 @@ def _convert(
# 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."
def convert(src: cute.Tensor, dst: cute.Tensor):
assert len(src.shape) == len(dst.shape), (
"Shape of src and dst tensors should be the same rank."
)
# find leading mode
leading_mode = [
idx
@@ -329,9 +328,9 @@ def _does_kernel_use_stream(
:rtype: bool
"""
assert int(stream) != int(
cuda_driver.CUstream_flags.CU_STREAM_DEFAULT
), "Stream must be a non-default stream"
assert int(stream) != int(cuda_driver.CUstream_flags.CU_STREAM_DEFAULT), (
"Stream must be a non-default stream"
)
err = cuda_runtime.cudaStreamBeginCapture(
stream, cuda_runtime.cudaStreamCaptureMode.cudaStreamCaptureModeThreadLocal
@@ -474,8 +473,13 @@ def benchmark(
elapsed_time = float("nan")
if use_cuda_graphs:
# Check if the callable is a JitExecutor
if not isinstance(callable, cutlass.base_dsl.jit_executor.JitExecutor):
# Check if the callable is a JitCompiledFunction or JitExecutor
# These are functions that can be called to launch kernels
compiled_types = (
cutlass.base_dsl.jit_executor.JitCompiledFunction,
cutlass.base_dsl.jit_executor.JitExecutor,
)
if not isinstance(callable, compiled_types):
raise TypeError("Function must be precompiled to be used with CUDA Graphs")
# Check if the stream is a non-default stream
@@ -502,7 +506,7 @@ def benchmark(
# Assertion is >= since we may launch multiple kernels in one host function
if num_nodes < warmup_iterations:
raise ValueError(
f"CUDA stream passed to benchmark does not match the stream the kernel was launched in"
"CUDA stream passed to benchmark does not match the stream the kernel was launched in"
)
# Capture profiling graph
@@ -549,7 +553,6 @@ def benchmark(
_cuda_success(err, "Error on destroying graph")
else:
if int(stream) != int(
cuda_driver.CUstream_flags.CU_STREAM_DEFAULT
) and not _does_kernel_use_stream(
@@ -599,12 +602,474 @@ def get_workspace_count(
:rtype: int
"""
num_l2_cache_bytes = cutlass.utils.HardwareInfo().get_l2_cache_size_in_bytes()
return max(
1,
min(
warmup_iterations + iterations, # Don't create more workspaces than needed
(num_l2_cache_bytes + one_workspace_bytes - 1)
// one_workspace_bytes, # Ceiling division
),
)
num_workspaces = (num_l2_cache_bytes * 3) // one_workspace_bytes + 1
num_iters = warmup_iterations + iterations
return num_iters if num_iters < num_workspaces else num_workspaces
#########################################
# Autotuning/Tuning utilities
#########################################
def _benchmark_for_autotune(
callable: Callable,
*args,
warmup_iterations: int,
iterations: int,
use_cold_l2: bool,
print_verbose: bool,
current_stream: Optional[cuda_driver.CUstream] = None,
**kwargs,
) -> float:
"""Benchmarks a callable function with the specified parameters.
This function differs from the benchmark function in that it is used for autotuning. In this case we
do not loop through workspaces to keep the L2 cache cold. Instead we rely on writing to an L2 cache sized address to keep the L2 cache cold.
The primary reason for doing this is that we do not have information on how to generate the workspaces for the kernel when autotuning.
We also do not have information on how much memory the workspaces take up.
This benchmarking is done as a close approximation of the actual runtime of the kernel in an E2E system,
where we may have clock throttling, a warm cache, or other factors that could affect the runtime of the kernel.
:param callable: The function to benchmark
:type callable: Callable
:param args: Arguments to pass to the callable function
:param warmup_iterations: Number of warmup iterations, defaults to 10
:type warmup_iterations: int, optional
:param iterations: Number of benchmark iterations, defaults to 100
:type iterations: int, optional
:param use_cold_l2: Whether to clear L2 cache between runs, defaults to True
:type use_cold_l2: bool, optional
:param print_verbose: Whether to print verbose output, defaults to False
:type print_verbose: bool, optional
:param current_stream: Stream to benchmark in, defaults to CUDA stream default
:type current_stream: CUstream, None
:param kwargs: Additional keyword arguments to pass to the callable function
:return: The benchmark time in microseconds
:rtype: float
"""
if current_stream is None:
current_stream = cuda_driver.CUstream(
cuda_driver.CUstream_flags.CU_STREAM_DEFAULT
)
if int(current_stream) != int(
cuda_driver.CUstream(cuda_driver.CUstream_flags.CU_STREAM_DEFAULT)
) and not _does_kernel_use_stream(callable, current_stream, *args, **kwargs):
raise ValueError(f"Incorrect stream passed to kernel: {current_stream}")
if use_cold_l2:
from cutlass.utils import HardwareInfo
# use memset to clear L2 cache
hardware_info = HardwareInfo()
num_l2_cache_bytes = hardware_info.get_l2_cache_size_in_bytes()
err, cache_ptr = cuda_driver.cuMemAlloc(int(num_l2_cache_bytes))
_cuda_success(err, "Error on allocating memory")
# Create CUDA events for timing
err, start_event = cuda_driver.cuEventCreate(
cuda_driver.CUevent_flags.CU_EVENT_DEFAULT
)
_cuda_success(err, "Error on creating event")
err, end_event = cuda_driver.cuEventCreate(
cuda_driver.CUevent_flags.CU_EVENT_DEFAULT
)
_cuda_success(err, "Error on creating event")
try:
# warmup
for _ in range(warmup_iterations):
callable(*args, **kwargs)
time = 0
execution_time_ms = []
for _ in range(iterations):
if use_cold_l2:
# clear L2 cache by memset to zero for every run
err = cuda_driver.cuMemsetD32Async(
cache_ptr, 0, int(num_l2_cache_bytes // 4), current_stream
)
_cuda_success(err, "Error on memset")
err = cuda_driver.cuEventRecord(start_event, current_stream)
_cuda_success(err, "Error on recording event")
callable(*args, **kwargs)
err = cuda_driver.cuEventRecord(end_event, current_stream)
_cuda_success(err, "Error on recording event")
err = cuda_driver.cuEventSynchronize(end_event)
_cuda_success(err, "Error on synchronizing event")
err, elapsed_time = cuda_driver.cuEventElapsedTime(start_event, end_event)
_cuda_success(err, "Error on querying event")
execution_time_ms.append(elapsed_time)
# unit: us
time_us = sum(execution_time_ms) / len(execution_time_ms)
except Exception as e:
print(f"This config execution error: {e}")
time_us = float("inf")
if print_verbose:
print(f"Execution time: {time_us:.4f} us")
if use_cold_l2:
err = cuda_driver.cuMemFree(cache_ptr)
_cuda_success(err, "Error on freeing memory")
err = cuda_driver.cuEventDestroy(start_event)
_cuda_success(err, "Error on destroying event")
err = cuda_driver.cuEventDestroy(end_event)
_cuda_success(err, "Error on destroying event")
return time_us
class autotune_jit:
"""Auto-tuning tool supporting both dictionary and parameterized decorator styles.
The autotune_jit class can be used as a decorator or a function.
When used as a decorator, it will automatically tune the function based on the parameters.
When used as a function, it will return a decorator that can be used to decorate a function.
For example:
.. code-block:: python
@autotune_jit(params_dict={'param1': [1, 2, 3], 'param2': [4, 5, 6]}, update_on_change=['param3'])
@cute.jit
def user_function(param1=1, param2=2, param3=3):
# contents of the function
pass
The function will be automatically tuned over all combinations of param1 and param2 whenever param3 changes .
For non-specified parameters, the default value in user_function will be used (e.g., `param3` in `user_function`).
.. code-block:: python
user_function(a, b, c) # Autotunes code
user_function(a, b, c) # This call pulls the best kernel from cache
Known Limitations:
- Only supports functions that are decorated with cute.jit
- If the function which is decorated with cute.jit is call method of a class, and the class has internal state that
is used as constexpr arguments in the function, the autotuner will not be able to find the best configuration.
Note: The autotuner has the same semantics as cute.compile. If the function is compiled, but global variables are changed,
the autotuner will not recompile the kernel.
"""
logger = None
@classmethod
def _initialize_logger(cls):
"""Ensure the logger is initialized"""
if cls.logger is None:
cls.logger = logging.getLogger(__name__ + "_Autotune")
if not cls.logger.handlers:
handler = logging.StreamHandler()
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
handler.setFormatter(formatter)
cls.logger.addHandler(handler)
if (
os.environ.get("CUTE_DSL_LOG_AUTOTUNE") is not None
and os.environ.get("CUTE_DSL_LOG_AUTOTUNE") != "0"
):
cls.logger.setLevel(logging.INFO)
@classmethod
def _create_tuning_wrapper(
cls, func, warmup_iterations, iterations, autotune_update_params
):
"""Create a wrapper function that performs auto-tuning
Args:
func: Original function
Returns:
Decorated wrapper function
"""
# Initialize autotune parameters
if not hasattr(func, "_autotune_params"):
func._original_func = func
func._autotune_params = {}
func._autotune_update_params = autotune_update_params
func._best_kernel = dict()
func._best_config = dict()
# Create wrapper function for auto-tuning
@functools.wraps(func)
def tuning_wrapper(*args, **kwargs):
parameters = inspect.signature(func._original_func).parameters.keys()
tuning_key = list()
for param_name in func._autotune_update_params:
if param_name in kwargs.keys():
tuning_key.append(kwargs[param_name])
else:
index = list(parameters).index(param_name)
if index < len(args):
tuning_key.append(args[index])
tuning_key = tuple(tuning_key)
if tuning_key in func._best_kernel.keys():
cls.logger.info(
f"Using cached best configuration: {func._best_config[tuning_key]}"
)
return func._best_kernel[tuning_key](*args, **kwargs)
# Get all parameter configurations
params_dict = func._autotune_params
keys = list(params_dict.keys())
values = list(params_dict.values())
min_time = float("inf")
best_kernel = None
# Record start time
start = time()
# Iterate through all possible configuration combinations
for config_values in product(*values):
# Build current configuration
current_config = dict(zip(keys, config_values))
cls.logger.info(f"Tuning configuration: {current_config}")
try:
# Call the original function, using current configuration to replace default parameters
# For example, if current_config contains "cluster_shape_mn": (2, 1)
# It will override func's default parameter value
merged_kwargs = {**kwargs, **current_config}
compiled_func = cute.compile(
func._original_func, *args, **merged_kwargs
)
# Detect which constexpr arguments we need to remove from args and merged_kwargs
# This is done because after compiling our function signature will change, removing all constexpr arguments.
indexes_to_remove = list()
for arg in compiled_func.args_spec.get_constexpr_args():
if arg["argument_name"] in merged_kwargs:
del merged_kwargs[arg["argument_name"]]
elif arg["argument_index"] is not None:
indexes_to_remove.append(arg["argument_index"])
if arg["argument_name"] not in func._autotune_update_params:
# Handle the case where the programmer avoided autotuning over constexpr values, and
# recompile in that case
func._autotune_update_params.append(
arg["argument_name"]
)
# Remove constexpr arguments from args
args_no_constexpr = list(args)
for index in sorted(indexes_to_remove, reverse=True):
del args_no_constexpr[index]
# Benchmark the compiled function
cur_time = _benchmark_for_autotune(
compiled_func,
*args_no_constexpr,
warmup_iterations=warmup_iterations,
iterations=iterations,
use_cold_l2=True,
print_verbose=False,
**merged_kwargs,
)
cls.logger.info(f" Execution time: {cur_time} us")
# Update best results
if cur_time < min_time:
min_time = cur_time
best_kernel = compiled_func
best_config = current_config
except NotImplementedError as e:
cls.logger.info(
f" Encountered unimplemented error, abort execution: {e}"
)
raise e
except (ValueError, TypeError) as e:
cls.logger.info(f" Configuration parameter skipping: {e}")
raise e
continue
except Exception as e:
cls.logger.info(f" Execution error skipping: {e}")
raise e
continue
end = time()
tuning_time = end - start
if best_kernel is None:
raise ValueError("No best kernel found")
cls.logger.info(
f"Best configuration: {best_config}, execution time: {min_time} us"
)
cls.logger.info(f"Total tuning time: {tuning_time} s")
func._best_kernel[tuning_key] = best_kernel
func._best_config[tuning_key] = best_config
return best_kernel(*args, **kwargs)
# Append autotune wrapper to not conflict with the jit kernel names
tuning_wrapper.__name__ = func.__name__ + "_autotune_wrapper"
tuning_wrapper.__qualname__ = func.__qualname__ + "_autotune_wrapper"
return tuning_wrapper
return func # If already has a wrapper, return the original function
def __init__(
self,
params_dict: Dict[str, List[Any]] = None,
update_on_change: List[str] = None,
warmup_iterations=10,
iterations=100,
):
"""Initialize the autotune_jit decorator.
:param params_dict: Dictionary containing parameter names and their possible values
:type params_dict: Dict[str, List[Any]], optional
:param update_on_change: Whether to retune when the parameters changes, defaults to None
:type update_on_change: bool, optional
:param warmup_iterations: Number of warmup iterations, defaults to 100
:type warmup_iterations: int, optional
:param iterations: Number of benchmark iterations, defaults to 100
:type iterations: int, optional
"""
# Initialize logger
self._initialize_logger()
# Save parameter dictionary
self.params_dict = params_dict or {}
self.update_on_change = update_on_change or list()
# Save iterations
self.warmup_iterations = warmup_iterations
self.iterations = iterations
def __call__(self, func):
"""Called when class instance is used as a decorator.
:param func: Function to be decorated
:type func: Callable
:return: Decorated function
:rtype: Callable
"""
# Create wrapper function
decorated_func = self._create_tuning_wrapper(
func, self.warmup_iterations, self.iterations, self.update_on_change
)
# Use the wrapper if it exists, otherwise use the original function
result_func = (
decorated_func if hasattr(decorated_func, "_autotune_params") else func
)
# Add parameters from the dictionary to the function's autotune parameters
for param_name, param_values in self.params_dict.items():
result_func._autotune_params[param_name] = param_values
return result_func
def tune(
func: Callable[[Any], Callable[[], Any]],
params_dict: Dict[str, List[Any]] = None,
kernel_arguments: JitArguments = JitArguments(),
warmup_iterations=10,
iterations=100,
stream: Optional[cuda_driver.CUstream] = None,
) -> Dict[str, Any]:
"""Tuning tool to suport arbitrary functions. The user must provide a function that returns a callable, which
takes no arguments to be tuned over.
Best practice is to return a jit function that is compiled with cute.compile for optimal performance.
For example:
.. code-block:: python
def user_function(param1=1, param2=2, param3=3) -> Callable[[], Any]:
# contents of the function
return lambda : compiled_func(param1, param2, param3)
config = tune(user_function, params_dict={'param1': [1, 2, 3], 'param2': [4, 5, 6]}, update_on_change=['param3'])
:param func: Function to be tuned, note that errors raised in the function will be ignored and the next configuration will be tried.
:type func: Callable[[Any], Callable[[], Any]]
:param params_dict: Dictionary containing parameter names and their possible values
:type params_dict: Dict[str, List[Any]], optional
:param kernel_arguments: Kernel arguments to launch callable with, defaults to JitArguments()
:type kernel_arguments: JitArguments, optional
:param warmup_iterations: Number of warmup iterations, defaults to 10
:type warmup_iterations: int, optional
:param iterations: Number of benchmark iterations, defaults to 100
:type iterations: int, optional
:param stream: Stream kernel is launched in, defaults to CUDA stream default
:type stream: CUstream, None
:return: Best configuration
:rtype: Dict[str, Any]
"""
logger = logging.getLogger(__name__ + "_Autotune")
if not logger.handlers:
handler = logging.StreamHandler()
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
handler.setFormatter(formatter)
logger.addHandler(handler)
if (
os.environ.get("CUTE_DSL_LOG_AUTOTUNE") is not None
and os.environ.get("CUTE_DSL_LOG_AUTOTUNE") != "0"
):
logger.setLevel(logging.INFO)
if stream is None:
stream = cuda_driver.CUstream(cuda_driver.CUstream_flags.CU_STREAM_DEFAULT)
# Get all parameter configurations
keys = list(params_dict.keys())
values = list(params_dict.values())
min_time = float("inf")
best_config = None
# Record start time
start = time()
# Iterate through all possible configuration combinations
for config_values in product(*values):
# Build current configuration
current_config = dict(zip(keys, config_values))
logger.info(f"Tuning configuration: {current_config}")
try:
merged_kwargs = {**kernel_arguments.kwargs, **current_config}
compiled_func = func(*kernel_arguments.args, **merged_kwargs)
# Benchmark the compiled function
cur_time = _benchmark_for_autotune(
compiled_func,
warmup_iterations=warmup_iterations,
iterations=iterations,
use_cold_l2=True,
print_verbose=False,
current_stream=stream,
)
logger.info(f" Execution time: {cur_time} us")
# Update best results
if cur_time < min_time:
min_time = cur_time
best_config = current_config
except NotImplementedError as e:
logger.info(f" Encountered unimplemented error, abort execution: {e}")
raise e
except (ValueError, TypeError) as e:
logger.info(f" Configuration parameter skipping: {e}")
continue
except Exception as e:
logger.info(f" Execution error skipping: {e}")
continue
end = time()
tuning_time = end - start
if best_config is None:
raise ValueError("No best kernel found")
logger.info(f"Best configuration: {best_config}, execution time: {min_time} us")
logger.info(f"Total tuning time: {tuning_time} s")
return best_config

View File

@@ -0,0 +1,331 @@
# 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 inspect import signature
from itertools import chain
from typing import Any, Callable, Union, Tuple, List, Iterable
from cutlass.cutlass_dsl import is_dynamic_expression, dsl_user_op
from cutlass._mlir import ir
import cutlass._mlir.dialects.cute as _cute_ir
from .typing import XTuple, IntTuple, Shape, Coord, Boolean, is_integer
def wrap(x) -> Tuple[Any, ...]:
"""
Wraps the input into a tuple if not a tuple.
"""
if isinstance(x, tuple):
return x
return (x,)
def flatten_to_tuple(a: XTuple) -> Tuple[Any, ...]:
"""Flattens a potentially nested tuple structure into a flat tuple.
This function recursively traverses the input structure and flattens it into
a single-level tuple, preserving the order of elements.
:param a: The structure to flatten
:type a: Union[IntTuple, Coord, Shape, Stride]
:return: A flattened tuple containing all elements from the input
:rtype: tuple
**Examples:**
.. code-block:: python
flatten_to_tuple((1, 2, 3)) # Returns (1, 2, 3)
flatten_to_tuple(((1, 2), 3)) # Returns (1, 2, 3)
flatten_to_tuple((1, (2, (3,)))) # Returns (1, 2, 3)
"""
if not isinstance(a, tuple):
return wrap(a)
else:
return tuple(chain.from_iterable(tuple(flatten_to_tuple(x) for x in a)))
def unflatten(
sequence: Union[Tuple[Any, ...], List[Any], Iterable[Any]], profile: XTuple
) -> XTuple:
"""Unflatten a flat tuple into a nested tuple structure according to a profile.
This function transforms a flat sequence of elements into a nested tuple structure
that matches the structure defined by the profile parameter. It traverses the profile
structure and populates it with elements from the sequence.
sequence must be long enough to fill the profile. Raises RuntimeError if it is not.
:param sequence: A flat sequence of elements to be restructured
:type sequence: Union[Tuple[Any, ...], List[Any], Iterable[Any]]
:param profile: A nested tuple structure that defines the shape of the output
:type profile: XTuple
:return: A nested tuple with the same structure as profile but containing elements from sequence
:rtype: XTuple
**Examples:**
.. code-block:: python
unflatten([1, 2, 3, 4], ((0, 0), (0, 0))) # Returns ((1, 2), (3, 4))
"""
def _make_generator():
for element in sequence:
yield element
xs = _make_generator()
return transform_leaf(lambda _: next(xs), profile)
@dsl_user_op
def product(a: Union[IntTuple, Shape], *, loc=None, ip=None):
# Local import to avoid circular dependency
from .core import _pack_int_tuple, _unpack_x_tuple
"""Return product of the given IntTuple or Shape.
Computes the product of all elements in the input tuple or shape.
Returns static value if type is static otherwise dynamic value.
:param a: The input tuple or shape
:type a: IntTuple or Shape
:param loc: Source location for MLIR, defaults to None
:type loc: optional
:param ip: Insertion point, defaults to None
:type ip: optional
:return: Static product of IntTuple or Shape if static, otherwise a Value
:rtype: int or Value
:raises TypeError: If input is not an IntTuple or Shape
"""
if is_integer(a):
return a
if isinstance(a, tuple):
a_val = _pack_int_tuple(a, loc=loc, ip=ip)
res = _cute_ir.tuple_product(a_val, loc=loc, ip=ip)
return _unpack_x_tuple(res, loc=loc, ip=ip)
else:
raise TypeError(f"expects IntTuple or Shape, but got {type(a)}")
@dsl_user_op
def product_like(a: IntTuple, target_profile: XTuple, *, loc=None, ip=None) -> IntTuple:
"""Return product of the given IntTuple or Shape at leaves of `target_profile`.
This function computes products according to the structure defined by target_profile.
:param a: The input tuple or shape
:type a: IntTuple or Shape
:param target_profile: The profile that guides how products are computed
:type target_profile: XTuple
:param loc: Source location for MLIR, defaults to None
:type loc: optional
:param ip: Insertion point, defaults to None
:type ip: optional
:return: The resulting tuple with products computed according to target_profile
:rtype: IntTuple or Shape
:raises TypeError: If inputs have incompatible types
:raises ValueError: If inputs have incompatible shapes
"""
# Perform product at leaf of `target_profile`
if not isinstance(target_profile, tuple):
return product(a, loc=loc, ip=ip)
if not isinstance(a, tuple):
raise TypeError(f"expects `a` tuple but got {a}")
if len(a) != len(target_profile):
raise ValueError("expects `a` and `guide` have the same rank")
return tuple(product_like(x, g, loc=loc, ip=ip) for x, g in zip(a, target_profile))
@dsl_user_op
def product_each(a: IntTuple, *, loc=None, ip=None) -> IntTuple:
from .core import _pack_int_tuple, _unpack_x_tuple
"""Compute products for each component of the input.
Returns a rank(a) tuple result such that ``get(result, mode=[i]) == product(get(a, mode=[i]))``
:param a: The input IntTuple or Shape
:type a: IntTuple or Shape
:param loc: Source location for MLIR, defaults to None
:type loc: optional
:param ip: Insertion point, defaults to None
:type ip: optional
:return: A tuple containing products for each component
:rtype: tuple
:raises TypeError: If input is not an IntTuple or Shape
"""
if is_integer(a):
return a
if not isinstance(a, tuple):
raise TypeError(f"expects IntTuple or Shape, but got {type(a)}")
if a == ():
return 1
a_val = _pack_int_tuple(a, loc=loc, ip=ip)
res = _cute_ir.tuple_product_each(a_val, loc=loc, ip=ip)
return _unpack_x_tuple(res, loc=loc, ip=ip)
def find_if(
t: Union[tuple, ir.Value, int],
pred_fn: Callable[[int, Tuple[int, ...]], bool],
*,
loc=None,
ip=None,
) -> Union[int, Tuple[int, ...], None]:
from .core import rank, get
"""Find the first position in t where pred_fn(val, pos) returns True.
:param t: The search space
:type t: Union[tuple, ir.Value, int]
:param pred_fn: A callable object (lambda, function, etc.) that predicates the value and position in t.
It takes the current leaf value and position, returns True if the value or position is satisfied.
:type pred_fn: Callable[[int, Tuple[int, ...]], bool]
:return: Index if found at top level, tuple of indices showing nested position, or None if not found
:rtype: Union[int, Tuple[int, ...], None]
**Examples:**
.. code-block:: python
# Find the first position of x in t
t = (3, 4)
find_if(t, pred_fn=lambda val, pos: val == x)
.. code-block:: python
# find the leading dimension
shape = (3, 4)
stride = (4, 1)
# Find value 1 in stride where the corresponding shape is not 1
def pred_fn(val, pos):
mode = [pos] if isinstance(pos, int) else list(pos)
return val == 1 and get(shape, mode) != 1
find_if(stride, pred_fn=pred_fn)
"""
def _find_if_impl(curr, pos, *, loc=None, ip=None):
if isinstance(curr, tuple):
# Recursively search nested tuple
for i in range(rank(curr)):
sub_curr = get(curr, mode=[i], loc=loc, ip=ip)
sub_pos = (pos, i) if isinstance(pos, int) else pos + (i,)
res_pos = _find_if_impl(sub_curr, sub_pos, loc=loc, ip=ip)
if res_pos is not None:
return res_pos
else:
# For leaf values, check if it matches x
if pred_fn(curr, pos):
return pos
return None
def _check_pred_fn():
if not callable(pred_fn):
raise TypeError(f"pred_fn must be callable, but got {type(pred_fn)}")
sig = signature(pred_fn)
if len(sig.parameters) != 2:
raise ValueError(
f"pred_fn must have two parameters (value, pos), but got {len(sig.parameters)}"
)
_check_pred_fn()
for i in range(rank(t)):
curr = get(t, mode=[i], loc=loc, ip=ip)
res_pos = _find_if_impl(curr, i, loc=loc, ip=ip)
if res_pos is not None:
return res_pos
return None
@dsl_user_op
def find(
t: Union[tuple, ir.Value, int], x: int, *, loc=None, ip=None
) -> Union[int, Tuple[int, ...], None]:
"""Find the first position of a value ``x`` in a hierarchical structure ``t``.
Searches for the first occurrence of x in t, optionally excluding positions
where a comparison value matches. The search can traverse nested structures
and returns either a single index or a tuple of indices for nested positions.
:param t: The search space
:type t: Union[tuple, ir.Value, int]
:param x: The static integer x to search for
:type x: int
:return: Index if found at top level, tuple of indices showing nested position, or None if not found
:rtype: Union[int, Tuple[int, ...], None]
"""
if not isinstance(x, int):
raise TypeError(f"find() requires a static x to search for, but got {x}")
def pred_fn(val, pos):
# Skip dynamic values which can't be compared
return not is_dynamic_expression(val) and val == x
return find_if(t, pred_fn=pred_fn, loc=loc, ip=ip)
def transform_leaf(f, *args):
"""
Apply a function to the leaf nodes of nested tuple structures.
This function traverses nested tuple structures in parallel and applies the function f
to corresponding leaf nodes. All input tuples must have the same nested structure.
:param f: Function to apply to leaf nodes
:type f: Callable
:param args: One or more nested tuple structures with matching profiles
:return: A new nested tuple with the same structure as the inputs, but with leaf values transformed by f
:raises TypeError: If the input tuples have different nested structures
**Example:**
.. code-block:: python
>>> transform_leaf(lambda x: x + 1, (1, 2))
(2, 3)
>>> transform_leaf(lambda x, y: x + y, (1, 2), (3, 4))
(4, 6)
>>> transform_leaf(lambda x: x * 2, ((1, 2), (3, 4)))
((2, 4), (6, 8))
"""
if all(isinstance(t, tuple) for t in args):
return tuple(transform_leaf(f, *_args) for _args in zip(*args))
elif all(not isinstance(t, tuple) for t in args):
return f(*args)
else:
raise TypeError(f"profile of input tuples doesn't match: {args}")
@dsl_user_op
def elem_less(
lhs: Union[Shape, IntTuple, Coord],
rhs: Union[Shape, IntTuple, Coord],
*,
loc=None,
ip=None,
) -> Boolean:
from .core import _pack_coord
# Coord is super set of IntTuple and Shape
lhs_val = _pack_coord(lhs, loc=loc, ip=ip)
rhs_val = _pack_coord(rhs, loc=loc, ip=ip)
return Boolean(_cute_ir.elem_less(lhs_val, rhs_val, loc=loc, ip=ip))

View File

@@ -10,13 +10,12 @@
# is strictly prohibited.
from abc import ABC, abstractmethod
from typing import ForwardRef, Tuple, Union, Any, Type, List
from typing import ForwardRef, Tuple, Union, Any, Type, List, Optional
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
from cutlass._mlir.dialects.cute import AddressSpace, ConstrainedIntType
Int = Union[int, Integer]
@@ -24,7 +23,6 @@ Int = Union[int, Integer]
ScaledBasis = ForwardRef("ScaledBasis")
IntTuple = Union[Int, Tuple["IntTuple", ...]]
Shape = Union[Int, Tuple["Shape", ...]]
Stride = Union[Int, ScaledBasis, Tuple["Stride", ...]]
@@ -35,7 +33,7 @@ class Layout(ir.Value):
def __init__(self, op_result):
super().__init__(op_result)
def __str__(self): ...
def __str__(self) -> str: ...
def get_hier_coord(self, idx) -> Coord:
"""Return the (hierarchical) ND logical coordinate corresponding to the linear index"""
@@ -48,12 +46,95 @@ class Layout(ir.Value):
def stride(self, *, loc=None, ip=None) -> Stride: ...
class ComposedLayout(ABC):
r"""ComposedLayout represents the functional composition of layouts in CuTe.
**Formally:**
.. math::
R(c) := (inner \circ offset \circ outer)(c) := inner(offset + outer(c))
where:
- inner: The inner layout or swizzle that is applied last
- offset: An integer tuple representing a coordinate offset
- outer: The outer layout that is applied first
This composition allows for complex transformations of coordinates and indices,
enabling operations like tiling, partitioning, and reshaping of data.
:ivar inner: The inner layout or swizzle component
:ivar offset: The coordinate offset applied between inner and outer layouts
:ivar outer: The outer layout component
:ivar max_alignment: The maximum alignment of the composed layout
**Examples:**
.. code-block:: python
# Create a composed layout with inner layout, offset, and outer layout
# inner layout: (4, 8):(1, 4)
inner_layout = make_layout((4, 8))
offset = (0, 0)
# outer layout: (2, 2):(1@0, 1@1)
outer_layout = make_layout((2, 2), stride=(1 * E(0), 1 * E(1)))
# composed layout: (inner o offset o outer)
composed = make_composed_layout(inner_layout, offset, outer_layout)
# Accessing components of the composed layout
inner = composed.inner
offset = composed.offset
outer = composed.outer
# map coordinate (0, 1) to linear index
# - outer(0, 1) = (0, 1)
# - offset + outer(0, 1) = (0, 1)
# - inner(0, 1) = 0 * 1 + 1 * 4 = 4
idx = crd2idx((0, 1), composed)
# Composition is used in many tiling operations
# For example, in logical_product, raked_product, and blocked_product
"""
@property
@abstractmethod
def type(self) -> ir.Type: ...
@property
@abstractmethod
def is_normal(self) -> bool: ...
@property
@abstractmethod
def inner(self, *, loc=None, ip=None): ...
@property
@abstractmethod
def offset(self, *, loc=None, ip=None) -> IntTuple: ...
@property
@abstractmethod
def outer(self, *, loc=None, ip=None) -> Layout: ...
@property
@abstractmethod
def shape(self, *, loc=None, ip=None): ...
@abstractmethod
def __call__(self, coord: Coord, loc=None, ip=None) -> IntTuple: ...
Tile = Union[Int, None, Layout, Tuple["Tile", ...]]
Tiler = Union[Shape, Layout, Tile]
# XTuple is super set of above types
XTuple = Union[IntTuple, Shape, Stride, Coord, Tile]
Tiler = Union[Shape, Layout, Tile]
XTuple = Union[Any, Tuple["XTuple", ...]]
class Pointer(ABC):
@@ -70,6 +151,8 @@ class Pointer(ABC):
def align(self, min_align: int) -> "Pointer": ...
def __add__(self, other: int, *, loc=None, ip=None) -> "Pointer": ...
def __get_mlir_types__(self) -> List[ir.Type]: ...
def __extract_mlir_values__(self) -> List[ir.Value]: ...
@@ -78,51 +161,69 @@ class Pointer(ABC):
class Tensor(ABC):
"""
Abstract base class for CuTe jit function and runtime _Tensor
r"""Abstract base class for Tensor representations in CuTe DSL.
A CuTe Tensor is iterator with layout
A CuTe Tensor is iterator with layout. A tensor evaluates the layout by mapping a
coordinate to the codomain, offsets the iterator accordingly, and dereferences
the result to obtain the tensor's value.
:Examples:
**Formally:**
.. math::
T(c) = (E \circ L)(c) = *(E + L(c))
where
- :math:`E` is the iterator/engine
- :math:`L` is the layout
**Notes:**
- The tensor supports both direct element access via coordinates and slicing operations
- Load/store operations are only supported for specific memory spaces (rmem, smem, gmem, generic)
- For composed layouts, stride information is not directly accessible
- Dynamic layouts do not support vector load/store operations
**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,)
import torch
from cutlass.cute.runtime import from_dlpack
mA = from_dlpack(torch.tensor([1, 3, 5], dtype=torch.int32))
print(mA.shape) # (3,)
print(mA.stride) # (1,)
print(mA.layout) # (3,):(1,)
Define JIT function:
.. code-block:: python
@cute.jit
def add(a: Tensor, b: Tensor, res: Tensor): ...
def add(a: Tensor, b: Tensor, res: Tensor):
res.store(a.load() + b.load())
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)
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)
print(c) # tensor([3, 7, 11], dtype=torch.int32)
"""
def __str__(self): ...
@abstractmethod
def __str__(self) -> str: ...
@abstractmethod
def __getitem__(self, idx) -> Union["Tensor", ir.Value, IntTuple]: ...
@@ -143,7 +244,7 @@ class Tensor(ABC):
@property
@abstractmethod
def iterator(self): ...
def iterator(self) -> Union[Pointer, IntTuple]: ...
@property
def layout(self) -> Union[Layout, "ComposedLayout"]: ...
@@ -151,16 +252,19 @@ class Tensor(ABC):
@property
def shape(self) -> Shape: ...
@property
def stride(self) -> Stride: ...
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_layout_dynamic(self, leading_dim: Optional[int] = None) -> "Tensor": ...
def mark_compact_shape_dynamic(
self,
mode: int,
stride_order: tuple[int, ...] | None = None,
stride_order: Optional[tuple[int, ...]] = None,
divisibility: int = 1,
) -> "Tensor": ...
@@ -168,11 +272,27 @@ class Tensor(ABC):
def fill(self, value: Numeric) -> None: ...
def is_integer(a) -> bool:
"""Check if an object is static integer or dynamic integer"""
return isinstance(a, (int, Integer)) or (
isinstance(a, ir.Value)
and isinstance(a.type, (ir.IntegerType, ConstrainedIntType))
)
def is_int_tuple(a) -> bool:
if isinstance(a, tuple):
return all([is_int_tuple(x) for x in a])
else:
return is_integer(a)
__all__ = [
"Coord",
"Numeric",
"Integer",
"Boolean",
"Int4",
"Int8",
"Int16",
"Int32",
@@ -204,4 +324,6 @@ __all__ = [
"Tile",
"Tiler",
"XTuple",
"is_integer",
"is_int_tuple",
]