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 # 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) ## [4.3.1](https://github.com/NVIDIA/cutlass/releases/tag/v4.3.1) (2025-11-26)
### CuTe DSL ### CuTe DSL
+5 -2
View File
@@ -1,9 +1,9 @@
![ALT](./media/images/gemm-hierarchy-with-epilogue-no-labels.png "Complete CUDA GEMM decomposition") ![ALT](./media/images/gemm-hierarchy-with-epilogue-no-labels.png "Complete CUDA GEMM decomposition")
# Overview # 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) 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 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 l2 cache evict priority for tma related ops. Users could do fine-grain l2 cache control.
- Added Blackwell SM103 support. - Added Blackwell SM103 support.
- Multiple dependent DSOs in the wheel have been merged into one single DSO. - 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: * Debuggability improvements:
- Supported source location tracking for DSL APIs (Allow tools like ``nsight`` profiling to correlate perf metrics with Python source code) - 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) - 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 an issue with mark_compact_shape_dynamic
- Fixed device reset issue with tvm-ffi - Fixed device reset issue with tvm-ffi
- Fixed tvm-ffi export compiled function - 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++ ## CUTLASS C++
* Further enhance Blackwell SM100 Attention kernels in [example 77](https://github.com/NVIDIA/cutlass/tree/main/examples/77_blackwell_fmha/). * 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.occupancy = 1
self.threads_per_cta = 128 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): def _setup_attributes(self):
"""Set up configurations that are dependent on GEMM inputs """Set up configurations that are dependent on GEMM inputs
+1 -1
View File
@@ -36,7 +36,7 @@
#define CUTLASS_MAJOR 4 #define CUTLASS_MAJOR 4
#define CUTLASS_MINOR 3 #define CUTLASS_MINOR 3
#define CUTLASS_PATCH 1 #define CUTLASS_PATCH 2
#ifdef CUTLASS_VERSIONS_GENERATED #ifdef CUTLASS_VERSIONS_GENERATED
#include "cutlass/version_extended.h" #include "cutlass/version_extended.h"
@@ -4,8 +4,7 @@
Compile with TVM FFI Compile with TVM FFI
==================== ====================
Apache TVM FFI is an open ABI and FFI for machine learning systems. More information can be found in 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/>`_.
the `official documentation <https://tvm.apache.org/ffi/>`_.
To install TVM FFI, you can run the following command: 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 # optional package for improved torch tensor calling performance
pip install torch-c-dlpack-ext 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 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).
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| 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. 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``. 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.
Please note that this setting will apply to all JIT compilations within the environment.
Minimizing Host Overhead 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 - **Declare shape constraints using fake tensors** and reuse the compiled function
throughout your execution. throughout your execution.
- **Pass PyTorch tensors directly** to the compiled function to avoid explicit DLPack conversion. - **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, - **Rely on compiled argument validation** instead of Python-side attribute validation,
as TVM FFI functions perform fast compiled checks. 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. 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. 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 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 ``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 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:
``enable_tvm_ffi=True`` option or the environment variable ``CUTE_DSL_ENABLE_TVM_FFI=1``. For example:
.. code-block:: python .. code-block:: python
@@ -246,11 +210,11 @@ The following example demonstrates this approach; the function accepts ``torch.c
Using Environment Stream Using Environment Stream
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~
The second option is to rely on the environment-stream flag. 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 Pass ``use_tvm_ffi_env_stream=True`` to ``make_fake_stream`` to mark the stream argument as an
environment stream so it no longer has to be provided explicitly. environment stream, which means it no longer needs to be provided explicitly.
TVM FFI will reuse its environment stream, synchronizing it with ``torch.cuda.current_stream()`` TVM FFI will automatically use its environment stream (i.e., the current PyTorch stream)
before each call. The example below shows this flow: as the stream argument. The example below demonstrates this flow:
.. code-block:: python .. 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() example_add_one_with_tuple()
Supported types 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]``) * - Tuple of types (e.g. ``Tuple[cute.Tensor, cute.Tensor, cutlass.Int32]``)
- Python tuple of corresponding call-time types. - Python tuple of corresponding call-time types.
Error handling Error handling
-------------- --------------
@@ -389,7 +351,7 @@ example error cases that can be checked:
except ValueError as e: except ValueError as e:
# Mismatched b.shape[0] on argument #1 when calling: # Mismatched b.shape[0] on argument #1 when calling:
# `add_one(a: Tensor([n0], float32), b: Tensor([n0], float32))`, # `add_one(a: Tensor([n0], float32), b: Tensor([n0], float32))`,
# symbolic constraint violated # expected to match a.shape[0]
print(f"ValueError: {e}") print(f"ValueError: {e}")
try: 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 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. 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: The following environment variables control file caching:
.. code-block:: bash .. code-block:: bash
@@ -140,6 +143,9 @@ The following environment variables control file caching:
# Maximum number of cache files allowed, defaults to 1000. # Maximum number of cache files allowed, defaults to 1000.
export CUTE_DSL_FILE_CACHING_CAPACITY=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 Limitations
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~
@@ -22,6 +22,7 @@ import time
from pathlib import Path from pathlib import Path
import hashlib import hashlib
from functools import lru_cache from functools import lru_cache
import tempfile
from .utils.logger import log from .utils.logger import log
from .jit_executor import JitCompiledFunction from .jit_executor import JitCompiledFunction
@@ -46,15 +47,23 @@ def get_current_user():
# default_generated_ir_path is the path to the cache directory. # default_generated_ir_path is the path to the cache directory.
# It is set to /tmp/{user}/cutlass_python_cache/ by default. # If `CUTE_DSL_CACHE_DIR` is set, it is used as the cache directory.
# If the user is not found, the default path is used or /tmp/cutlass_python_cache/ is used. # Otherwise, it is set to a directory controled by TMPDIR defaulting
try: # to /tmp/${USER}/cutlass_python_cache.
default_generated_ir_path = f"/tmp/{get_current_user()}/cutlass_python_cache/" if not (default_generated_ir_path := os.getenv("CUTE_DSL_CACHE_DIR", None)):
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}")
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) @lru_cache(maxsize=1)
def get_default_file_dump_root(): def get_default_file_dump_root():
@@ -223,6 +232,8 @@ def dump_cache_to_path(
:type bytecode_writer: callable, optional :type bytecode_writer: callable, optional
""" """
log().info("JIT cache : dumping [%s] items=[%s]", dsl_name, len(jit_cache)) 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) os.makedirs(path, exist_ok=True)
try: try:
for idx, [key, value] in enumerate(jit_cache.items()): 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) atexit.register(restore_excepthook, origin_excepthook)
def dump_cache(self): def dump_cache(self, path=None):
if not self.envar.disable_file_caching: if not self.envar.disable_file_caching:
dump_cache_to_path( 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) @lru_cache(maxsize=1)
@@ -296,6 +296,7 @@ class EnvironmentVarManager(LogEnvironmentManager):
- [DSL_NAME]_FILTER_STACKTRACE: Filter internal stacktrace (default: True) - [DSL_NAME]_FILTER_STACKTRACE: Filter internal stacktrace (default: True)
File options: File options:
- [DSL_NAME]_DUMP_DIR: Directory to dump the generated files (default: current working directory) - [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_IR: Save generated IR in a file (default: False)
- [DSL_NAME]_KEEP_PTX: Save generated PTX 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) - [DSL_NAME]_KEEP_CUBIN: Save generated CUBIN in a file (default: False)
@@ -333,6 +334,7 @@ class EnvironmentVarManager(LogEnvironmentManager):
# File options # File options
self.keep_ir = get_bool_env_var(f"{prefix}_KEEP_IR", False) 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 # Other options
self.dryrun = get_bool_env_var(f"{prefix}_DRYRUN", False) self.dryrun = get_bool_env_var(f"{prefix}_DRYRUN", False)
self.arch = get_str_env_var(f"{prefix}_ARCH", detect_gpu_arch(prefix)) 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"], dtype: Union[str, "tvm_ffi.dtype"],
*, *,
device_type: Optional[str] = None, device_type: Optional[str] = None,
device_id: Optional[Var] = None,
strides: Optional[Sequence[Var]] = None, strides: Optional[Sequence[Var]] = None,
map_tensor_dtype_f4x2_to_f4: bool = False, map_tensor_dtype_f4x2_to_f4: bool = False,
data_alignment: Optional[int] = None, data_alignment: Optional[int] = None,
@@ -229,7 +230,10 @@ class Tensor(Param):
example_device = tvm_ffi.device(device_type, 0) example_device = tvm_ffi.device(device_type, 0)
self.dlpack_device_type = example_device.dlpack_device_type() self.dlpack_device_type = example_device.dlpack_device_type()
self.device_type_name = example_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 self.map_tensor_dtype_f4x2_to_f4 = map_tensor_dtype_f4x2_to_f4
@@ -818,6 +818,7 @@ class TVMFFIFunctionBuilder(TVMFFIBuilder):
_fn_call_context: str _fn_call_context: str
matched_var_binding: dict[spec.Var, ir.Value] matched_var_binding: dict[spec.Var, ir.Value]
matched_var_source: 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: def __init__(self, module: ir.Module) -> None:
super().__init__() super().__init__()
@@ -826,6 +827,7 @@ class TVMFFIFunctionBuilder(TVMFFIBuilder):
self._fn_call_context: str = "" self._fn_call_context: str = ""
self.matched_var_binding = {} self.matched_var_binding = {}
self.matched_var_source = {} self.matched_var_source = {}
self.matched_var_arg_field_name = {}
def find_or_declare_extern_func( def find_or_declare_extern_func(
self, name: str, params: Sequence[ir.Type], ret: ir.Type self, name: str, params: Sequence[ir.Type], ret: ir.Type
@@ -897,6 +899,7 @@ class TVMFFIFunctionBuilder(TVMFFIBuilder):
*arg_context.get(), *arg_context.get(),
self._fn_call_context, self._fn_call_context,
], ],
arg_context.get_field_name(""),
) )
def decode_param_float( def decode_param_float(
@@ -1000,6 +1003,7 @@ class TVMFFIFunctionBuilder(TVMFFIBuilder):
result = result_block.arguments[0] result = result_block.arguments[0]
self.matched_var_binding[param] = result self.matched_var_binding[param] = result
self.matched_var_source[param] = v_float64 self.matched_var_source[param] = v_float64
self.matched_var_arg_field_name[param] = arg_context.get_field_name("")
return result_block return result_block
@@ -1054,6 +1058,7 @@ class TVMFFIFunctionBuilder(TVMFFIBuilder):
# For opaque handles, we store the pointer directly # For opaque handles, we store the pointer directly
self.matched_var_binding[param] = v_ptr self.matched_var_binding[param] = v_ptr
self.matched_var_source[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 return current_block
@@ -1191,8 +1196,10 @@ class TVMFFIFunctionBuilder(TVMFFIBuilder):
var: Union[spec.Var, int], var: Union[spec.Var, int],
value: ir.Value, value: ir.Value,
error_msg_context: list[str], error_msg_context: list[str],
arg_field_name: str,
*, *,
skip_check_predicate: Optional[ir.Value] = None, skip_check_predicate: Optional[ir.Value] = None,
skip_cast_and_check: bool = False,
) -> ir.Block: ) -> ir.Block:
"""Set or check the matched var binding.""" """Set or check the matched var binding."""
error_kind = "ValueError" error_kind = "ValueError"
@@ -1202,33 +1209,48 @@ class TVMFFIFunctionBuilder(TVMFFIBuilder):
if isinstance(var, spec.Var): if isinstance(var, spec.Var):
# if var contains llvm_value and is not populated, populate it # if var contains llvm_value and is not populated, populate it
if var not in self.matched_var_binding: if var not in self.matched_var_binding:
current_block = self.check_int_value_dtype_bound( if not skip_cast_and_check:
current_block, value, var.dtype, error_msg_context 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
) )
# 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 return current_block
# otherwise, it appears more than once, we need to check if the value matches # otherwise, it appears more than once, we need to check if the value matches
expected_value = self.matched_var_source[var] expected_value = self.matched_var_source[var]
prev_arg_field_name = self.matched_var_arg_field_name[var]
error_msg_mismatch = [ error_msg_mismatch = [
error_prefix_mismatch, error_prefix_mismatch,
*error_msg_context, *error_msg_context,
", symbolic constraint violated" f", expected to match {prev_arg_field_name}",
] ]
else: else:
assert isinstance(var, int) assert isinstance(var, int)
with ir.InsertionPoint(current_block): 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_msg_mismatch = [
error_prefix_mismatch, error_prefix_mismatch,
*error_msg_context, *error_msg_context,
@@ -1261,6 +1283,7 @@ class TVMFFIFunctionBuilder(TVMFFIBuilder):
) -> ir.Block: ) -> ir.Block:
"""Load the shape value from the argument or match the shape value from the parameter.""" """Load the shape value from the argument or match the shape value from the parameter."""
field_name = arg_context.get_field_name(field_suffix) field_name = arg_context.get_field_name(field_suffix)
arg_field_name = f"{field_name}[{shape_index}]"
error_msg = [ error_msg = [
field_name, field_name,
f"[{shape_index}] ", f"[{shape_index}] ",
@@ -1268,7 +1291,8 @@ class TVMFFIFunctionBuilder(TVMFFIBuilder):
self._fn_call_context, self._fn_call_context,
] ]
return self.set_or_check_matched_var_binding( 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( def decode_param_shape_from_ffi_array(
@@ -1553,8 +1577,22 @@ class TVMFFIFunctionBuilder(TVMFFIBuilder):
# store the matched values, these do not need constraint checks # store the matched values, these do not need constraint checks
self.matched_var_binding[param.data] = data self.matched_var_binding[param.data] = data
self.matched_var_source[param.data] = param.data self.matched_var_source[param.data] = param.data
self.matched_var_binding[param.device_id] = device_id self.matched_var_arg_field_name[param.data] = arg_context.get_field_name(".data")
self.matched_var_source[param.device_id] = param.device_id
# 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 # check ndim
expected_ndim = len(param.shape) expected_ndim = len(param.shape)
# Break error message into reusable parts for better string deduplication # 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.""" """Decode the stream parameter at the given index."""
# stream is decoded as opaque handle # stream is decoded as opaque handle
return self.decode_param_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( def decode_param_data_pointer(
@@ -1873,6 +1912,7 @@ class TVMFFIFunctionBuilder(TVMFFIBuilder):
) )
self.matched_var_binding[param.var] = env_stream self.matched_var_binding[param.var] = env_stream
self.matched_var_source[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 return current_block
@@ -115,7 +115,9 @@ class ConverterContext:
def __init__(self): def __init__(self):
self.num_dyn_shape_vars = 0 self.num_dyn_shape_vars = 0
self.num_dyn_stride_vars = 0 self.num_dyn_stride_vars = 0
self.num_device_id_vars = 0
self.sym_int_id_mapping = {} self.sym_int_id_mapping = {}
self.vdevice_to_device_id_mapping = {}
def alloc_shape_name(self) -> str: def alloc_shape_name(self) -> str:
"""Allocate a new dynamic shape variable name.""" """Allocate a new dynamic shape variable name."""
@@ -143,6 +145,25 @@ class ConverterContext:
self.sym_int_id_mapping[sym_int_id] = var self.sym_int_id_mapping[sym_int_id] = var
return 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( def _convert_single_arg(
arg, arg,
@@ -209,17 +230,28 @@ def _convert_single_arg(
if hasattr(arg, "_tvm_ffi_tensor"): if hasattr(arg, "_tvm_ffi_tensor"):
tvm_ffi_tensor = arg._tvm_ffi_tensor tvm_ffi_tensor = arg._tvm_ffi_tensor
dtype = tvm_ffi_tensor.dtype 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( tvm_ffi_cute_tensor = spec.Tensor(
arg_name, arg_name,
shapes, shapes,
arg._tvm_ffi_tensor.dtype, arg._tvm_ffi_tensor.dtype,
strides=strides, strides=strides,
data_alignment=arg._assumed_align, data_alignment=arg._assumed_align,
device_type=tvm_ffi_tensor.device.type device_type=device_type,
device_id=device_id
) )
else: else:
# for FakeTensor, strictly follow the shape and stride from the cute tensor # for FakeTensor, strictly follow the shape and stride from the cute tensor
device_type = "cuda" if _is_gpu_memspace(arg.memspace) else "cpu" 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( tvm_ffi_cute_tensor = spec.Tensor(
arg_name, arg_name,
shapes, shapes,
@@ -227,6 +259,7 @@ def _convert_single_arg(
strides=strides, strides=strides,
data_alignment=arg._assumed_align, data_alignment=arg._assumed_align,
device_type=device_type, device_type=device_type,
device_id=device_id
) )
if arg.element_type == Float4E2M1FN: if arg.element_type == Float4E2M1FN:
tvm_ffi_cute_tensor = spec.create_map_tensor_dtype_f4x2_to_f4_spec( 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. when the dimension is dynamic.
:type use_32bit_stride: bool, optional :type use_32bit_stride: bool, optional
""" """
def __init__(self, dtype, shape, *, stride, memspace=None, assumed_align=None): 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. :param shape: Shape of the tensor.
:type shape: tuple[int, ...] :type shape: tuple[int, ...]
:param stride_order: Order in which strides (memory layout) are assigned to the tensor dimensions. :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). If None, the default layout is col-major. Otherwise, it should be a permutation of the dimension indices.
Otherwise, it should be a permutation order of the dimension indices.
:type stride_order: tuple[int, ...], optional :type stride_order: tuple[int, ...], optional
:param memspace: Memory space where the fake tensor resides. Optional. :param memspace: Memory space where the fake tensor resides. Optional.
:type memspace: str, optional :type memspace: str, optional
@@ -55,6 +55,8 @@ class CudaDialectJitModule:
for library in self.cuda_library: for library in self.cuda_library:
cuda_runtime.cudaLibraryUnload(library) cuda_runtime.cudaLibraryUnload(library)
self.cuda_library.clear() self.cuda_library.clear()
except Exception as e:
pass
finally: finally:
self._unloaded = True 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 import cutlass.cute as cute
from cutlass.cute.arch import get_dyn_smem, get_dyn_smem_size 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 = { SMEM_CAPACITY_MAP = {
"sm_120": (100 - 1) * 1024, "sm_120": (100 - 1) * 1024,
"sm_103": (228 - 1) * 1024,
"sm_100": (228 - 1) * 1024, "sm_100": (228 - 1) * 1024,
"sm_90": (228 - 1) * 1024, "sm_90": (228 - 1) * 1024,
"sm_80": (164 - 1) * 1024, "sm_80": (164 - 1) * 1024,
@@ -71,7 +73,7 @@ class SmemAllocator:
""" """
@staticmethod @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. """Get the shared memory capacity in bytes for a given compute capability.
Returns the maximum shared memory capacity in bytes available for the specified Returns the maximum shared memory capacity in bytes available for the specified
@@ -83,6 +85,9 @@ class SmemAllocator:
:rtype: int :rtype: int
:raises ValueError: If the compute capability is not supported :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: if compute_capability not in SMEM_CAPACITY_MAP:
raise ValueError(f"Unsupported compute capability: {compute_capability}") raise ValueError(f"Unsupported compute capability: {compute_capability}")
return SMEM_CAPACITY_MAP[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._base = get_dyn_smem(Int8, alignment=1024, loc=loc, ip=ip)
self._allocated_bytes = 0 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 @overload
def allocate( def allocate(
+1 -1
View File
@@ -133,7 +133,7 @@ def get_option_registry():
this._option_registry = OptionRegistry(device_cc()) this._option_registry = OptionRegistry(device_cc())
return this._option_registry 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.backend import create_memory_pool
from cutlass_cppgen.emit.pytorch import pytorch from cutlass_cppgen.emit.pytorch import pytorch
+1 -1
View File
@@ -51,7 +51,7 @@ setup_pycute.perform_setup()
setup( setup(
name='cutlass_cppgen', name='cutlass_cppgen',
version='4.3.1', version='4.3.2',
description='CUTLASS Pythonic Interface', description='CUTLASS Pythonic Interface',
package_dir={'': '.'}, package_dir={'': '.'},
packages=[ packages=[
+1 -1
View File
@@ -36,7 +36,7 @@ from setuptools import setup
def perform_setup(): def perform_setup():
setup( setup(
name='pycute', name='pycute',
version='4.3.1', version='4.3.2',
description='Python implementation of CuTe', description='Python implementation of CuTe',
packages=['pycute'], packages=['pycute'],
) )