v4.3.2 update. (#2839)

This commit is contained in:
Junkai-Wu
2025-12-04 10:14:32 -05:00
committed by GitHub
parent 52ae719eda
commit bc680c7f67
19 changed files with 169 additions and 301 deletions
+8
View File
@@ -2,6 +2,14 @@
# CUTLASS 4.x
## [4.3.2](https://github.com/NVIDIA/cutlass/releases/tag/v4.3.2) (2025-12-05)
* New features
- New env var `CUTE_DSL_CACHE_DIR` to specify the path for dumping caches
* Bug fixing and improvements
- Fixed an issue of CUDA JitExecutor when unloading kernels
- Fixed an issue of allocating max smem when there's statically allocated smem
## [4.3.1](https://github.com/NVIDIA/cutlass/releases/tag/v4.3.1) (2025-11-26)
### CuTe DSL
+5 -2
View File
@@ -1,9 +1,9 @@
![ALT](./media/images/gemm-hierarchy-with-epilogue-no-labels.png "Complete CUDA GEMM decomposition")
# Overview
# CUTLASS 4.3.1
# CUTLASS 4.3.2
_CUTLASS 4.3.1 - Nov 2025_
_CUTLASS 4.3.2 - Dec 2025_
CUTLASS is a collection of abstractions for implementing high-performance matrix-matrix multiplication (GEMM)
and related computations at all levels and scales within CUDA. It incorporates strategies for
@@ -53,6 +53,7 @@ To get started quickly - please refer :
- Added l2 cache evict priority for tma related ops. Users could do fine-grain l2 cache control.
- Added Blackwell SM103 support.
- Multiple dependent DSOs in the wheel have been merged into one single DSO.
- New env var `CUTE_DSL_CACHE_DIR` to specify the path for dumping caches.
* Debuggability improvements:
- Supported source location tracking for DSL APIs (Allow tools like ``nsight`` profiling to correlate perf metrics with Python source code)
- Supported dumping PTX and CUBIN code: [Hello World Example](https://github.com/NVIDIA/cutlass/blob/main/examples/python/CuTeDSL/notebooks/hello_world.ipynb)
@@ -99,6 +100,8 @@ To get started quickly - please refer :
- Fixed an issue with mark_compact_shape_dynamic
- Fixed device reset issue with tvm-ffi
- Fixed tvm-ffi export compiled function
- Fixed an issue of CUDA JitExecutor when unloading kernels
- Fixed an issue of allocating max smem when there's statically allocated smem
## CUTLASS C++
* Further enhance Blackwell SM100 Attention kernels in [example 77](https://github.com/NVIDIA/cutlass/tree/main/examples/77_blackwell_fmha/).
@@ -215,7 +215,7 @@ class DenseGemmKernel:
self.occupancy = 1
self.threads_per_cta = 128
self.smem_capacity = utils.get_smem_capacity_in_bytes("sm_100")
self.smem_capacity = utils.get_smem_capacity_in_bytes()
def _setup_attributes(self):
"""Set up configurations that are dependent on GEMM inputs
+1 -1
View File
@@ -36,7 +36,7 @@
#define CUTLASS_MAJOR 4
#define CUTLASS_MINOR 3
#define CUTLASS_PATCH 1
#define CUTLASS_PATCH 2
#ifdef CUTLASS_VERSIONS_GENERATED
#include "cutlass/version_extended.h"
@@ -4,8 +4,7 @@
Compile with TVM FFI
====================
Apache TVM FFI is an open ABI and FFI for machine learning systems. More information can be found in
the `official documentation <https://tvm.apache.org/ffi/>`_.
Apache TVM FFI is an open ABI and FFI for machine learning systems. More information can be found in the `official documentation <https://tvm.apache.org/ffi/>`_.
To install TVM FFI, you can run the following command:
@@ -15,9 +14,7 @@ To install TVM FFI, you can run the following command:
# optional package for improved torch tensor calling performance
pip install torch-c-dlpack-ext
In |DSL|, TVM FFI can be enabled as an option for JIT-compiled functions. Using TVM FFI can lead to faster
JIT function invocation and provides better interoperability with machine learning frameworks
(e.g., directly take ``torch.Tensor`` as arguments).
In |DSL|, TVM FFI can be enabled as an option for JIT-compiled functions. Using TVM FFI can lead to faster JIT function invocation and provides better interoperability with machine learning frameworks (e.g., directly take ``torch.Tensor`` as arguments).
Enable Apache TVM FFI in |DSL|
@@ -43,8 +40,7 @@ There are two ways to enable TVM FFI in |DSL|:
Note that the object returned by ``cute.compile`` is a Python function specific to TVM FFI.
2. Alternatively, you can enable TVM FFI globally by setting the environment variable ``CUTE_DSL_ENABLE_TVM_FFI=1``.
Please note that this setting will apply to all JIT compilations within the environment.
2. Alternatively, you can enable TVM FFI globally by setting the environment variable ``CUTE_DSL_ENABLE_TVM_FFI=1``. Please note that this setting will apply to all JIT compilations within the environment.
Minimizing Host Overhead
@@ -59,7 +55,7 @@ To maximize performance benefits, we recommend setting up your workflow as follo
- **Declare shape constraints using fake tensors** and reuse the compiled function
throughout your execution.
- **Pass PyTorch tensors directly** to the compiled function to avoid explicit DLPack conversion.
- **Use the environment stream flag** to implicitly synchronize with the current PyTorch stream.
- **Use the environment stream flag** to implicitly pass the current PyTorch stream.
- **Rely on compiled argument validation** instead of Python-side attribute validation,
as TVM FFI functions perform fast compiled checks.
@@ -118,37 +114,6 @@ The fake tensor is a placeholder that mimics the interface of a real tensor but
It is used in compilation or testing scenarios where only shape/type/layout information is needed.
All attempts to access or mutate data will raise errors.
Interoperability with `from_dlpack`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The Fake Tensor flow supports more flexible constraints on Tensor arguments than the `from_dlpack` flow.
When fake tensor is used, it's recommended to use TVM FFI backend as it supports more flexible constraints on
Tensor arguments than the `from_dlpack` flow.
For instance, fake tensor can specify per-mode static shape or constraints on shape and strides which is not supported by
`from_dlpack`. It's expected that JIT function compiled with fake tensor may have different ABI with tensor converted
with `from_dlpack`.
.. code-block:: python
import cutlass.cute as cute
import torch
n = cute.sym_int()
# Dynamic Shape
fake_a = cute.runtime.make_fake_compact_tensor(cute.Float32, (n,))
# Compile without tvm-ffi
compiled_fn = cute.compile(foo, fake_a)
# Wrong, in compatible ABI
compiled_fn(from_dlpack(a))
In order to avoid mismatched ABI, it's recommended to use TVM FFI when fake tensor is used for compilation.
Note on Stride Order
~~~~~~~~~~~~~~~~~~~~
@@ -164,8 +129,7 @@ stride via the ``stride`` argument in the ``make_fake_tensor`` API.
``cute.Tensor`` adapter for TVM FFI
-----------------------------------
To adapt the ``cute.Tensor`` to the TVM FFI function, you can use the ``cute.runtime.from_dlpack`` function with the
``enable_tvm_ffi=True`` option or the environment variable ``CUTE_DSL_ENABLE_TVM_FFI=1``. For example:
To adapt the ``cute.Tensor`` to the TVM FFI function, you can use the ``cute.runtime.from_dlpack`` function with the ``enable_tvm_ffi=True`` option or the environment variable ``CUTE_DSL_ENABLE_TVM_FFI=1``. For example:
.. code-block:: python
@@ -246,11 +210,11 @@ The following example demonstrates this approach; the function accepts ``torch.c
Using Environment Stream
~~~~~~~~~~~~~~~~~~~~~~~~
The second option is to rely on the environment-stream flag.
Pass ``use_tvm_ffi_env_stream=True`` to ``make_fake_stream`` to mark the argument as an
environment stream so it no longer has to be provided explicitly.
TVM FFI will reuse its environment stream, synchronizing it with ``torch.cuda.current_stream()``
before each call. The example below shows this flow:
The second option is to rely on the environment stream flag.
Pass ``use_tvm_ffi_env_stream=True`` to ``make_fake_stream`` to mark the stream argument as an
environment stream, which means it no longer needs to be provided explicitly.
TVM FFI will automatically use its environment stream (i.e., the current PyTorch stream)
as the stream argument. The example below demonstrates this flow:
.. code-block:: python
@@ -324,7 +288,6 @@ composed of the types that are supported by TVM FFI. The example below shows how
example_add_one_with_tuple()
Supported types
---------------
@@ -353,7 +316,6 @@ The TVM FFI function supports the following |DSL|-specific types as arguments:
* - Tuple of types (e.g. ``Tuple[cute.Tensor, cute.Tensor, cutlass.Int32]``)
- Python tuple of corresponding call-time types.
Error handling
--------------
@@ -389,7 +351,7 @@ example error cases that can be checked:
except ValueError as e:
# Mismatched b.shape[0] on argument #1 when calling:
# `add_one(a: Tensor([n0], float32), b: Tensor([n0], float32))`,
# symbolic constraint violated
# expected to match a.shape[0]
print(f"ValueError: {e}")
try:
@@ -130,6 +130,9 @@ After serialization, compiled MLIR bytecode is stored in file.
The cache directory is ``/tmp/{current_user}/cutlass_python_cache``.
The cache loads from files into memory during |DSL| initialization and saves back to files when the process exits.
Note that for efficiency, the default cache directory is located in a temporary folder. However, this location is not persistent, it may be cleared by the system (for example, during a reboot or disk space cleanup).
If you wish to preserve the cache across sessions, set the ``CUTE_DSL_CACHE_DIR`` environment variable to point to a persistent directory.
The following environment variables control file caching:
.. code-block:: bash
@@ -140,6 +143,9 @@ The following environment variables control file caching:
# Maximum number of cache files allowed, defaults to 1000.
export CUTE_DSL_FILE_CACHING_CAPACITY=1000
# Cache directory, defaults to /tmp/{current_user}/cutlass_python_cache.
export CUTE_DSL_CACHE_DIR=/home/user/local_cutlass_python_cache/dense_gemm_cache/
Limitations
~~~~~~~~~~~~~~~~~~~~~
@@ -22,6 +22,7 @@ import time
from pathlib import Path
import hashlib
from functools import lru_cache
import tempfile
from .utils.logger import log
from .jit_executor import JitCompiledFunction
@@ -46,15 +47,23 @@ def get_current_user():
# default_generated_ir_path is the path to the cache directory.
# It is set to /tmp/{user}/cutlass_python_cache/ by default.
# If the user is not found, the default path is used or /tmp/cutlass_python_cache/ is used.
try:
default_generated_ir_path = f"/tmp/{get_current_user()}/cutlass_python_cache/"
except Exception as e:
# If all else fails, provide a default fallback path
default_generated_ir_path = "/tmp/cutlass_python_cache/"
print(f"Could not determine user, using default path. Error: {e}")
# If `CUTE_DSL_CACHE_DIR` is set, it is used as the cache directory.
# Otherwise, it is set to a directory controled by TMPDIR defaulting
# to /tmp/${USER}/cutlass_python_cache.
if not (default_generated_ir_path := os.getenv("CUTE_DSL_CACHE_DIR", None)):
tmp_dir = Path(os.environ.get("TMPDIR", tempfile.gettempdir()))
def get_reusable_temp_dir(name):
path = tmp_dir / f"{get_current_user()}/{name}"
path.mkdir(parents=True, exist_ok=True)
return str(path)
try:
default_generated_ir_path = get_reusable_temp_dir("cutlass_python_cache")
except Exception as e:
default_generated_ir_path = str(tmp_dir / "cutlass_python_cache")
print(f"Could not determine user, using default path. Error: {e}")
@lru_cache(maxsize=1)
def get_default_file_dump_root():
@@ -223,6 +232,8 @@ def dump_cache_to_path(
:type bytecode_writer: callable, optional
"""
log().info("JIT cache : dumping [%s] items=[%s]", dsl_name, len(jit_cache))
if not path:
path = default_generated_ir_path
os.makedirs(path, exist_ok=True)
try:
for idx, [key, value] in enumerate(jit_cache.items()):
+2 -2
View File
@@ -372,10 +372,10 @@ class BaseDSL:
atexit.register(restore_excepthook, origin_excepthook)
def dump_cache(self):
def dump_cache(self, path=None):
if not self.envar.disable_file_caching:
dump_cache_to_path(
self.name, self.jit_cache, self.envar.file_caching_capacity
self.name, self.jit_cache, self.envar.file_caching_capacity, path=path
)
@lru_cache(maxsize=1)
@@ -296,6 +296,7 @@ class EnvironmentVarManager(LogEnvironmentManager):
- [DSL_NAME]_FILTER_STACKTRACE: Filter internal stacktrace (default: True)
File options:
- [DSL_NAME]_DUMP_DIR: Directory to dump the generated files (default: current working directory)
- [DSL_NAME]_CACHE_DIR: Cache directory (default: /tmp/{dsl_name}_python_cache_{tmpfile_suffix})
- [DSL_NAME]_KEEP_IR: Save generated IR in a file (default: False)
- [DSL_NAME]_KEEP_PTX: Save generated PTX in a file (default: False)
- [DSL_NAME]_KEEP_CUBIN: Save generated CUBIN in a file (default: False)
@@ -333,6 +334,7 @@ class EnvironmentVarManager(LogEnvironmentManager):
# File options
self.keep_ir = get_bool_env_var(f"{prefix}_KEEP_IR", False)
self.cache_dir = get_str_env_var(f"{prefix}_CACHE_DIR", None)
# Other options
self.dryrun = get_bool_env_var(f"{prefix}_DRYRUN", False)
self.arch = get_str_env_var(f"{prefix}_ARCH", detect_gpu_arch(prefix))
@@ -192,6 +192,7 @@ class Tensor(Param):
dtype: Union[str, "tvm_ffi.dtype"],
*,
device_type: Optional[str] = None,
device_id: Optional[Var] = None,
strides: Optional[Sequence[Var]] = None,
map_tensor_dtype_f4x2_to_f4: bool = False,
data_alignment: Optional[int] = None,
@@ -229,7 +230,10 @@ class Tensor(Param):
example_device = tvm_ffi.device(device_type, 0)
self.dlpack_device_type = example_device.dlpack_device_type()
self.device_type_name = example_device.type
self.device_id = Var(name + ".device_id", tvm_ffi.dtype("int32"))
if device_id is None:
self.device_id = Var(name + ".device.index", tvm_ffi.dtype("int32"))
else:
self.device_id = device_id
self.map_tensor_dtype_f4x2_to_f4 = map_tensor_dtype_f4x2_to_f4
@@ -818,6 +818,7 @@ class TVMFFIFunctionBuilder(TVMFFIBuilder):
_fn_call_context: str
matched_var_binding: dict[spec.Var, ir.Value]
matched_var_source: dict[spec.Var, ir.Value]
matched_var_arg_field_name: dict[spec.Var, str]
def __init__(self, module: ir.Module) -> None:
super().__init__()
@@ -826,6 +827,7 @@ class TVMFFIFunctionBuilder(TVMFFIBuilder):
self._fn_call_context: str = ""
self.matched_var_binding = {}
self.matched_var_source = {}
self.matched_var_arg_field_name = {}
def find_or_declare_extern_func(
self, name: str, params: Sequence[ir.Type], ret: ir.Type
@@ -897,6 +899,7 @@ class TVMFFIFunctionBuilder(TVMFFIBuilder):
*arg_context.get(),
self._fn_call_context,
],
arg_context.get_field_name(""),
)
def decode_param_float(
@@ -1000,6 +1003,7 @@ class TVMFFIFunctionBuilder(TVMFFIBuilder):
result = result_block.arguments[0]
self.matched_var_binding[param] = result
self.matched_var_source[param] = v_float64
self.matched_var_arg_field_name[param] = arg_context.get_field_name("")
return result_block
@@ -1054,6 +1058,7 @@ class TVMFFIFunctionBuilder(TVMFFIBuilder):
# For opaque handles, we store the pointer directly
self.matched_var_binding[param] = v_ptr
self.matched_var_source[param] = v_ptr
self.matched_var_arg_field_name[param] = arg_context.get_field_name("")
return current_block
@@ -1191,8 +1196,10 @@ class TVMFFIFunctionBuilder(TVMFFIBuilder):
var: Union[spec.Var, int],
value: ir.Value,
error_msg_context: list[str],
arg_field_name: str,
*,
skip_check_predicate: Optional[ir.Value] = None,
skip_cast_and_check: bool = False,
) -> ir.Block:
"""Set or check the matched var binding."""
error_kind = "ValueError"
@@ -1202,33 +1209,48 @@ class TVMFFIFunctionBuilder(TVMFFIBuilder):
if isinstance(var, spec.Var):
# if var contains llvm_value and is not populated, populate it
if var not in self.matched_var_binding:
current_block = self.check_int_value_dtype_bound(
current_block, value, var.dtype, error_msg_context
)
# check divisibility if specified
if var.divisibility is not None:
current_block = self.check_int_value_divisibility(
current_block, value, var.divisibility, error_msg_context,
skip_check_predicate=skip_check_predicate,
)
# store the source value with parameter info
with ir.InsertionPoint(current_block):
self.matched_var_source[var] = value
self.matched_var_binding[var] = self.downcast_i64_to_lower_bits(
value, var.dtype
if not skip_cast_and_check:
current_block = self.check_int_value_dtype_bound(
current_block, value, var.dtype, error_msg_context
)
# check divisibility if specified
if var.divisibility is not None:
current_block = self.check_int_value_divisibility(
current_block, value, var.divisibility, error_msg_context,
skip_check_predicate=skip_check_predicate,
)
# store the source value with parameter info
with ir.InsertionPoint(current_block):
target_value = self.downcast_i64_to_lower_bits(
value, var.dtype
)
else:
target_value = value
# store the source value
self.matched_var_source[var] = value
# store the target value (casted to target dtype aleady)
self.matched_var_binding[var] = target_value
# store arg_field_name
self.matched_var_arg_field_name[var] = arg_field_name
return current_block
# otherwise, it appears more than once, we need to check if the value matches
expected_value = self.matched_var_source[var]
prev_arg_field_name = self.matched_var_arg_field_name[var]
error_msg_mismatch = [
error_prefix_mismatch,
*error_msg_context,
", symbolic constraint violated"
f", expected to match {prev_arg_field_name}",
]
else:
assert isinstance(var, int)
with ir.InsertionPoint(current_block):
expected_value = self.i64(var)
if not skip_cast_and_check:
expected_value = self.i64(var)
else:
expected_value = self.downcast_i64_to_lower_bits(
self.i64(var), var.dtype
)
error_msg_mismatch = [
error_prefix_mismatch,
*error_msg_context,
@@ -1261,6 +1283,7 @@ class TVMFFIFunctionBuilder(TVMFFIBuilder):
) -> ir.Block:
"""Load the shape value from the argument or match the shape value from the parameter."""
field_name = arg_context.get_field_name(field_suffix)
arg_field_name = f"{field_name}[{shape_index}]"
error_msg = [
field_name,
f"[{shape_index}] ",
@@ -1268,7 +1291,8 @@ class TVMFFIFunctionBuilder(TVMFFIBuilder):
self._fn_call_context,
]
return self.set_or_check_matched_var_binding(
current_block, var, value, error_msg, skip_check_predicate=skip_check_predicate
current_block, var, value, error_msg, arg_field_name,
skip_check_predicate=skip_check_predicate
)
def decode_param_shape_from_ffi_array(
@@ -1553,8 +1577,22 @@ class TVMFFIFunctionBuilder(TVMFFIBuilder):
# store the matched values, these do not need constraint checks
self.matched_var_binding[param.data] = data
self.matched_var_source[param.data] = param.data
self.matched_var_binding[param.device_id] = device_id
self.matched_var_source[param.device_id] = param.device_id
self.matched_var_arg_field_name[param.data] = arg_context.get_field_name(".data")
# check device_id constraint if user specifies a device_id variable
current_block = self.set_or_check_matched_var_binding(
current_block,
param.device_id,
device_id,
[
"device index ",
*arg_context.get(),
self._fn_call_context,
],
arg_context.get_field_name(".device.index"),
skip_cast_and_check=True,
)
# check ndim
expected_ndim = len(param.shape)
# Break error message into reusable parts for better string deduplication
@@ -1683,7 +1721,8 @@ class TVMFFIFunctionBuilder(TVMFFIBuilder):
"""Decode the stream parameter at the given index."""
# stream is decoded as opaque handle
return self.decode_param_opaque_handle(
current_block, param.var, args, arg_index, arg_context
current_block, param.var, args, arg_index, arg_context,
allow_int_as_ptr=True
)
def decode_param_data_pointer(
@@ -1873,6 +1912,7 @@ class TVMFFIFunctionBuilder(TVMFFIBuilder):
)
self.matched_var_binding[param.var] = env_stream
self.matched_var_source[param.var] = env_stream
self.matched_var_arg_field_name[param.var] = param.name
return current_block
@@ -115,7 +115,9 @@ class ConverterContext:
def __init__(self):
self.num_dyn_shape_vars = 0
self.num_dyn_stride_vars = 0
self.num_device_id_vars = 0
self.sym_int_id_mapping = {}
self.vdevice_to_device_id_mapping = {}
def alloc_shape_name(self) -> str:
"""Allocate a new dynamic shape variable name."""
@@ -143,6 +145,25 @@ class ConverterContext:
self.sym_int_id_mapping[sym_int_id] = var
return var
def alloc_or_reuse_device_id(self, device_type: str, vdevice_id: int) -> Optional[spec.Var]:
"""Allocate or reuse a device_id variable for a given virtual device.
This function returns None for CPU tensors.
"""
# Don't allocate device_id for CPU tensors
if device_type == "cpu":
return None
vdevice_key = (device_type, vdevice_id)
if vdevice_key in self.vdevice_to_device_id_mapping:
return self.vdevice_to_device_id_mapping[vdevice_key]
name = f"device_id{self.num_device_id_vars}"
self.num_device_id_vars += 1
device_id_var = spec.Var(name, "int32")
self.vdevice_to_device_id_mapping[vdevice_key] = device_id_var
return device_id_var
def _convert_single_arg(
arg,
@@ -209,17 +230,28 @@ def _convert_single_arg(
if hasattr(arg, "_tvm_ffi_tensor"):
tvm_ffi_tensor = arg._tvm_ffi_tensor
dtype = tvm_ffi_tensor.dtype
device_type = tvm_ffi_tensor.device.type
# Allocate device_id (returns None for CPU tensors)
vdevice_id = tvm_ffi_tensor.device.index
device_id = ctx.alloc_or_reuse_device_id(device_type, vdevice_id)
tvm_ffi_cute_tensor = spec.Tensor(
arg_name,
shapes,
arg._tvm_ffi_tensor.dtype,
strides=strides,
data_alignment=arg._assumed_align,
device_type=tvm_ffi_tensor.device.type
device_type=device_type,
device_id=device_id
)
else:
# for FakeTensor, strictly follow the shape and stride from the cute tensor
device_type = "cuda" if _is_gpu_memspace(arg.memspace) else "cpu"
# Allocate device_id (returns None for CPU tensors)
vdevice_id = 0 # For now, use vdevice_id = 0 for all GPU tensors
device_id = ctx.alloc_or_reuse_device_id(device_type, vdevice_id)
tvm_ffi_cute_tensor = spec.Tensor(
arg_name,
shapes,
@@ -227,6 +259,7 @@ def _convert_single_arg(
strides=strides,
data_alignment=arg._assumed_align,
device_type=device_type,
device_id=device_id
)
if arg.element_type == Float4E2M1FN:
tvm_ffi_cute_tensor = spec.create_map_tensor_dtype_f4x2_to_f4_spec(
+2 -2
View File
@@ -515,6 +515,7 @@ class _FakeTensor(Tensor):
when the dimension is dynamic.
:type use_32bit_stride: bool, optional
"""
def __init__(self, dtype, shape, *, stride, memspace=None, assumed_align=None):
@@ -617,8 +618,7 @@ def make_fake_compact_tensor(
:param shape: Shape of the tensor.
:type shape: tuple[int, ...]
:param stride_order: Order in which strides (memory layout) are assigned to the tensor dimensions.
If None, the default layout is left-to-right order (known as column-major order for flatten layout).
Otherwise, it should be a permutation order of the dimension indices.
If None, the default layout is col-major. Otherwise, it should be a permutation of the dimension indices.
:type stride_order: tuple[int, ...], optional
:param memspace: Memory space where the fake tensor resides. Optional.
:type memspace: str, optional
@@ -55,6 +55,8 @@ class CudaDialectJitModule:
for library in self.cuda_library:
cuda_runtime.cudaLibraryUnload(library)
self.cuda_library.clear()
except Exception as e:
pass
finally:
self._unloaded = True
@@ -1,208 +0,0 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from functools import partial
from typing import Tuple
import cutlass.cute as cute
from cutlass.cutlass_dsl import T, dsl_user_op
from cutlass._mlir import ir
from cutlass._mlir.dialects import llvm, nvvm
from cutlass._mlir.dialects.nvvm import (
MemOrderKind,
MemScopeKind,
AtomicOpKind,
)
from cutlass.cute.typing import Pointer, Int32
@dsl_user_op
def atomicAdd(dst_ptr: Pointer, val: Int32, loc=None, ip=None) -> Int32:
return nvvm.atomicrmw(
T.i32(),
AtomicOpKind.ADD,
dst_ptr.llvm_ptr,
val.ir_value(loc=loc, ip=ip),
mem_order=MemOrderKind.RELAXED,
syncscope=MemScopeKind.SYS,
loc=loc,
ip=ip,
)
@cute.jit
def ld_bypass(input_tensor: cute.Tensor):
fragment = cute.make_rmem_tensor(input_tensor.layout, input_tensor.element_type)
copy_atom_load = cute.make_copy_atom(
cute.nvgpu.CopyUniversalOp(),
input_tensor.element_type,
memory_order=cute.nvgpu.common.MemoryOrder.VOLATILE,
memory_scope=cute.nvgpu.common.MemoryScope.SYS,
)
cute.copy_atom_call(copy_atom_load, input_tensor, fragment)
vals = fragment.load()
return vals
@cute.jit
def spin_lock_wait(
lock_ptr: Pointer,
expect_count: Int32,
mem_order: str = "relaxed",
mem_scope: str = "gpu",
loc=None,
ip=None,
) -> None:
"""
wait on a spin lock until the expected count is reached.
"""
res = 0
while res != expect_count:
res = nvvm.atomicrmw(
T.i32(),
AtomicOpKind.CAS,
lock_ptr.llvm_ptr,
Int32(0).ir_value(loc=loc, ip=ip),
b=Int32(expect_count).ir_value(loc=loc, ip=ip),
mem_order=(
MemOrderKind.ACQUIRE if mem_order == "acquire" else MemOrderKind.RELAXED
),
syncscope=MemScopeKind.GPU if mem_scope == "gpu" else MemScopeKind.SYS,
)
@dsl_user_op
def multimem_red_add_sys_release(mc_ptr: Pointer, loc=None, ip=None) -> None:
"""
add 1 to the multimem address
"""
llvm.inline_asm(
None,
[mc_ptr.toint().ir_value(loc=loc, ip=ip)],
"multimem.red.release.sys.global.add.u32 [$0], 1;",
"l",
has_side_effects=True,
asm_dialect=0,
loc=loc,
ip=ip,
)
@dsl_user_op
def multimem_red_add_gpu_relaxed(mc_ptr: Pointer, loc=None, ip=None) -> None:
"""
add 1 to the multimem address
"""
llvm.inline_asm(
None,
[mc_ptr.toint().ir_value(loc=loc, ip=ip)],
"multimem.red.relaxed.gpu.global.add.u32 [$0], 1;",
"l",
has_side_effects=True,
asm_dialect=0,
loc=loc,
ip=ip,
)
def spin_lock_multimem_arrive(lock_ptr: Pointer, loc=None, ip=None) -> None:
"""
arrive a spin lock when the lock_ptr is a multimem address.
"""
multimem_red_add_gpu_relaxed(lock_ptr, loc=loc, ip=ip)
def sm_wise_inter_gpu_multimem_barrier(
barrier: Pointer, barrier_mc: Pointer, num_ranks, loc=None, ip=None
) -> None:
"""
barrier for inter-gpu sm-wise
"""
bidx, bidy, bidz = cute.arch.block_idx()
bdimx, bdimy, _ = cute.arch.grid_dim()
pid = bidx + bidy * bdimx + bidz * bdimx * bdimy
multimem_red_add_sys_release(barrier_mc + pid, loc=loc, ip=ip)
cute.arch.fence_proxy(cute.arch.ProxyKind.alias)
spin_lock_wait(
barrier + pid, num_ranks, mem_order="acquire", mem_scope="sys", loc=loc, ip=ip
)
@dsl_user_op
def multimem_ld_reduce_base(
mc_ptr: Pointer,
*,
ptx_string: str = "",
loc=None,
ip=None,
) -> Tuple[Int32, Int32, Int32, Int32]:
# ld reduce 8xf16 elts
mc_ptr_int = mc_ptr.toint(loc=loc, ip=ip).ir_value(loc=loc, ip=ip)
return_struct = llvm.inline_asm(
ir.Type.parse("!llvm.struct<(i32,i32,i32,i32)>"),
[mc_ptr_int],
ptx_string,
"=r,=r,=r,=r,l",
has_side_effects=True,
asm_dialect=0,
loc=loc,
ip=ip,
)
return_regs = [llvm.extractvalue(T.i32(), return_struct, [i]) for i in range(4)]
return return_regs[0], return_regs[1], return_regs[2], return_regs[3]
multimem_ld_reduce_8xf16 = partial(
multimem_ld_reduce_base,
ptx_string="multimem.ld_reduce.sys.relaxed.global.add.acc::f32.v4.f16x2 {$0, $1, $2, $3}, [$4];",
)
multimem_ld_reduce_4xf32 = partial(
multimem_ld_reduce_base,
ptx_string="multimem.ld_reduce.sys.relaxed.global.add.v4.f32 {$0, $1, $2, $3}, [$4];",
)
multimem_ld_reduce_8xbf16 = partial(
multimem_ld_reduce_base,
ptx_string="multimem.ld_reduce.sys.relaxed.global.add.acc::f32.v4.bf16x2 {$0, $1, $2, $3}, [$4];",
)
multimem_ld_reduce_16xe4m3 = partial(
multimem_ld_reduce_base,
ptx_string="multimem.ld_reduce.sys.relaxed.global.add.acc::f16.v4.e4m3x4 {$0, $1, $2, $3}, [$4];",
)
multimem_ld_reduce_16xe5m2 = partial(
multimem_ld_reduce_base,
ptx_string="multimem.ld_reduce.sys.relaxed.global.add.acc::f16.v4.e5m2x4 {$0, $1, $2, $3}, [$4];",
)
@dsl_user_op
def multimem_st_4xb32(
mc_ptr: Pointer,
x: Int32,
y: Int32,
z: Int32,
w: Int32,
*,
loc=None,
ip=None,
) -> None:
# st 4x32 bits of data
mc_ptr_int = mc_ptr.toint(loc=loc, ip=ip).ir_value(loc=loc, ip=ip)
llvm.inline_asm(
T.i32(),
[mc_ptr_int, x, y, z, w],
"multimem.st.sys.relaxed.global.v4.f32 [$1], {$2, $3, $4, $5};",
"=r,l,r,r,r,r",
has_side_effects=True,
asm_dialect=0,
loc=loc,
ip=ip,
)
@@ -14,10 +14,12 @@ import inspect
import cutlass.cute as cute
from cutlass.cute.arch import get_dyn_smem, get_dyn_smem_size
from cutlass.cutlass_dsl import CutlassBaseDSL, Int8, Numeric, NumericMeta, dsl_user_op
from cutlass.cutlass_dsl import CuTeDSL, Int8, Numeric, NumericMeta, dsl_user_op
SMEM_CAPACITY_MAP = {
"sm_120": (100 - 1) * 1024,
"sm_103": (228 - 1) * 1024,
"sm_100": (228 - 1) * 1024,
"sm_90": (228 - 1) * 1024,
"sm_80": (164 - 1) * 1024,
@@ -71,7 +73,7 @@ class SmemAllocator:
"""
@staticmethod
def capacity_in_bytes(compute_capability: str) -> int:
def capacity_in_bytes(compute_capability: Optional[str] = None) -> int:
"""Get the shared memory capacity in bytes for a given compute capability.
Returns the maximum shared memory capacity in bytes available for the specified
@@ -83,6 +85,9 @@ class SmemAllocator:
:rtype: int
:raises ValueError: If the compute capability is not supported
"""
if compute_capability is None:
arch = CuTeDSL._get_dsl().get_arch_enum()
compute_capability = f"sm_{arch.major}{arch.minor}"
if compute_capability not in SMEM_CAPACITY_MAP:
raise ValueError(f"Unsupported compute capability: {compute_capability}")
return SMEM_CAPACITY_MAP[compute_capability]
@@ -101,7 +106,7 @@ class SmemAllocator:
"""
self._base = get_dyn_smem(Int8, alignment=1024, loc=loc, ip=ip)
self._allocated_bytes = 0
CutlassBaseDSL.track_smem_allocator(self, lambda cls: cls._allocated_bytes)
CuTeDSL.track_smem_allocator(self, lambda cls: cls._allocated_bytes)
@overload
def allocate(
+1 -1
View File
@@ -133,7 +133,7 @@ def get_option_registry():
this._option_registry = OptionRegistry(device_cc())
return this._option_registry
this.__version__ = '4.3.1'
this.__version__ = '4.3.2'
from cutlass_cppgen.backend import create_memory_pool
from cutlass_cppgen.emit.pytorch import pytorch
+1 -1
View File
@@ -51,7 +51,7 @@ setup_pycute.perform_setup()
setup(
name='cutlass_cppgen',
version='4.3.1',
version='4.3.2',
description='CUTLASS Pythonic Interface',
package_dir={'': '.'},
packages=[
+1 -1
View File
@@ -36,7 +36,7 @@ from setuptools import setup
def perform_setup():
setup(
name='pycute',
version='4.3.1',
version='4.3.2',
description='Python implementation of CuTe',
packages=['pycute'],
)