v4.3 tag release update. (#2789)
This commit is contained in:
@@ -689,7 +689,21 @@ def _min(lhs, rhs, *, loc=None, ip=None):
|
||||
if not is_dynamic_expression(rhs):
|
||||
rhs = arith.constant(lhs.type, rhs, loc=loc, ip=ip)
|
||||
|
||||
if arith._is_integer_like_type(lhs.type):
|
||||
# Handle vector types
|
||||
if isinstance(lhs.type, T.VectorType):
|
||||
elem_type = lhs.type.element_type
|
||||
if arith._is_integer_like_type(elem_type):
|
||||
assert hasattr(
|
||||
lhs, "signed"
|
||||
), "Should have attribute `signed`, must be a bug"
|
||||
if lhs.signed != False:
|
||||
return arith.minsi(lhs, rhs, loc=loc, ip=ip)
|
||||
else:
|
||||
return arith.minui(lhs, rhs, loc=loc, ip=ip)
|
||||
else:
|
||||
return arith.minimumf(lhs, rhs, loc=loc, ip=ip)
|
||||
elif arith._is_integer_like_type(lhs.type):
|
||||
assert hasattr(lhs, "signed"), "Should have attribute `signed`, must be a bug"
|
||||
if lhs.signed != False:
|
||||
return arith.minsi(lhs, rhs, loc=loc, ip=ip)
|
||||
else:
|
||||
@@ -705,7 +719,6 @@ def _max(lhs, rhs, *, loc=None, ip=None):
|
||||
Assuming the operands have the same type
|
||||
"""
|
||||
from ..dsl import is_dynamic_expression
|
||||
|
||||
if not is_dynamic_expression(lhs):
|
||||
if not is_dynamic_expression(rhs):
|
||||
return max(lhs, rhs)
|
||||
@@ -714,8 +727,21 @@ def _max(lhs, rhs, *, loc=None, ip=None):
|
||||
else:
|
||||
if not is_dynamic_expression(rhs):
|
||||
rhs = arith.constant(lhs.type, rhs, loc=loc, ip=ip)
|
||||
|
||||
if arith._is_integer_like_type(lhs.type):
|
||||
# Handle vector types
|
||||
if isinstance(lhs.type, T.VectorType):
|
||||
elem_type = lhs.type.element_type
|
||||
if isinstance(elem_type, ir.IntegerType):
|
||||
assert hasattr(
|
||||
lhs, "signed"
|
||||
), "Should have attribute `signed`, must be a bug"
|
||||
if lhs.signed != False:
|
||||
return arith.maxsi(lhs, rhs, loc=loc, ip=ip)
|
||||
else:
|
||||
return arith.maxui(lhs, rhs, loc=loc, ip=ip)
|
||||
else:
|
||||
return arith.maximumf(lhs, rhs, loc=loc, ip=ip)
|
||||
elif arith._is_integer_like_type(lhs.type):
|
||||
assert hasattr(lhs, "signed"), "Should have attribute `signed`, must be a bug"
|
||||
if lhs.signed != False:
|
||||
return arith.maxsi(lhs, rhs, loc=loc, ip=ip)
|
||||
else:
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
from enum import Enum
|
||||
import re
|
||||
from typing import Callable, List
|
||||
from typing import Callable, List, Tuple
|
||||
|
||||
|
||||
class Arch(Enum):
|
||||
@@ -49,16 +49,47 @@ class Arch(Enum):
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
"""Support creating Arch enum from (major, minor, suffix) tuple"""
|
||||
if not isinstance(value, tuple):
|
||||
raise ValueError(f"invalid arguments for Arch: {value}")
|
||||
major, minor, suffix = None, None, None
|
||||
if len(value) == 2:
|
||||
if isinstance(value, tuple) and len(value) == 2:
|
||||
# Support creating Arch enum from (major, minor) tuple
|
||||
# Arch(major, minor) is equivalent to Arch(major, minor, "")
|
||||
major, minor, suffix = *value, ""
|
||||
return cls((major, minor, suffix))
|
||||
else:
|
||||
raise ValueError(f"invalid arguments for Arch: {value}")
|
||||
|
||||
|
||||
return cls(major, minor, suffix)
|
||||
# attributes to get arch list of specific families
|
||||
@classmethod
|
||||
def AmpereArchs(cls) -> Tuple["Arch"]:
|
||||
return (Arch.sm_80, Arch.sm_86, Arch.sm_87)
|
||||
|
||||
@classmethod
|
||||
def AdaArchs(cls) -> Tuple["Arch"]:
|
||||
return (Arch.sm_89,)
|
||||
|
||||
@classmethod
|
||||
def HopperArchs(cls) -> Tuple["Arch"]:
|
||||
return (Arch.sm_90, Arch.sm_90a)
|
||||
|
||||
@classmethod
|
||||
def BlackwellArchs(cls) -> Tuple["Arch"]:
|
||||
return (
|
||||
Arch.sm_100,
|
||||
Arch.sm_100a,
|
||||
Arch.sm_100f,
|
||||
Arch.sm_101,
|
||||
Arch.sm_101a,
|
||||
Arch.sm_101f,
|
||||
Arch.sm_110,
|
||||
Arch.sm_110a,
|
||||
Arch.sm_110f,
|
||||
Arch.sm_120,
|
||||
Arch.sm_120a,
|
||||
Arch.sm_120f,
|
||||
Arch.sm_121,
|
||||
Arch.sm_121a,
|
||||
Arch.sm_121f,
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
@@ -526,6 +526,10 @@ class DSLPreprocessor(ast.NodeTransformer):
|
||||
self.generic_visit(node)
|
||||
self.loop_nest_level -= 1
|
||||
|
||||
def visit_FunctionDef(self, node):
|
||||
# Stop at nested function def
|
||||
return
|
||||
|
||||
checker = EarlyExitChecker(kind)
|
||||
checker.generic_visit(tree)
|
||||
if not checker.has_early_exit:
|
||||
@@ -1325,6 +1329,7 @@ class DSLPreprocessor(ast.NodeTransformer):
|
||||
node,
|
||||
)
|
||||
elif func.id in ["min", "max"]:
|
||||
self.import_top_module = True
|
||||
return ast.copy_location(
|
||||
ast.Call(
|
||||
func=self._create_module_attribute(
|
||||
@@ -1334,7 +1339,7 @@ class DSLPreprocessor(ast.NodeTransformer):
|
||||
lineno=node.lineno,
|
||||
col_offset=node.col_offset,
|
||||
),
|
||||
args=[node.args[0], node.args[1]],
|
||||
args=node.args,
|
||||
keywords=[],
|
||||
),
|
||||
node,
|
||||
|
||||
@@ -134,7 +134,7 @@ def _get_friendly_cuda_error_message(error_code, error_name):
|
||||
# Add target architecture info
|
||||
target_arch = os.getenv("CUTE_DSL_ARCH", "unknown")
|
||||
|
||||
error_messages = {
|
||||
additional_info = {
|
||||
"CUDA_ERROR_INVALID_SOURCE": (
|
||||
f"{Colors.RED}❌ Failed to load CUDA kernel - likely architecture mismatch.{Colors.RESET}\n\n"
|
||||
),
|
||||
@@ -157,6 +157,9 @@ def _get_friendly_cuda_error_message(error_code, error_name):
|
||||
f"{Colors.RED}⚠️ Invalid parameter passed to CUDA operation.{Colors.RESET}\n\n"
|
||||
f"{Colors.YELLOW}This is likely a bug - please report it with:{Colors.RESET}"
|
||||
),
|
||||
"CUDA_ERROR_INVALID_CLUSTER_SIZE": (
|
||||
f"{Colors.RED}❌ Invalid cluster size.{Colors.RESET}\n\n"
|
||||
),
|
||||
}
|
||||
|
||||
error_suggestions = {
|
||||
@@ -194,12 +197,12 @@ def _get_friendly_cuda_error_message(error_code, error_name):
|
||||
),
|
||||
}
|
||||
|
||||
message = error_messages.get(
|
||||
error_name, f"{Colors.RED}Unknown CUDA error{Colors.RESET}"
|
||||
)
|
||||
message = f"{error_name} (error code: {error_code}) \n" \
|
||||
f"{additional_info.get(error_name, '')} \n\n{Colors.RESET}"
|
||||
|
||||
# Add debug information
|
||||
debug_info = f"\n- {Colors.BOLD}Error name: {error_name}\n"
|
||||
debug_info += f"- Error code: {error_code}\n"
|
||||
debug_info += f"- CUDA_TOOLKIT_PATH: {os.getenv('CUDA_TOOLKIT_PATH', 'not set')}\n"
|
||||
debug_info += (
|
||||
f"- Target SM ARCH: {os.getenv('CUTE_DSL_ARCH', 'not set')}{Colors.RESET}\n"
|
||||
|
||||
@@ -15,7 +15,7 @@ and executes it using MLIR's ExecutionEngine.
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Optional, Tuple
|
||||
from typing import Sequence, Optional, Tuple, Callable
|
||||
import os
|
||||
import sys
|
||||
import inspect
|
||||
@@ -99,6 +99,8 @@ class Compiler:
|
||||
self.execution_engine = execution_engine
|
||||
# Flag to track if CUDA dependencies have been checked once in this process
|
||||
self._cuda_dependencies_checked = False
|
||||
# Post-compile hook to run on Module
|
||||
self._post_compile_hook: Optional[Callable[[ir.Module], None]] = None
|
||||
|
||||
def _process_error(self, error_msg: str) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""Process error message to extract NVVM error and IR context"""
|
||||
@@ -158,11 +160,13 @@ class Compiler:
|
||||
) from e
|
||||
raise e
|
||||
|
||||
if self._post_compile_hook:
|
||||
self._post_compile_hook(module)
|
||||
|
||||
def jit(self, module, opt_level: int = 2, shared_libs: Sequence[str] = ()):
|
||||
"""Wraps the module in a JIT execution engine."""
|
||||
# Check CUDA driver and GPU dependencies before JIT execution (once per process)
|
||||
self._check_cuda_dependencies_once(shared_libs)
|
||||
|
||||
# If pre-checks passed, attempt to create ExecutionEngine
|
||||
# Any failures at this point are likely non-CUDA related
|
||||
return self.execution_engine.ExecutionEngine(
|
||||
@@ -216,6 +220,23 @@ class Compiler:
|
||||
)
|
||||
|
||||
|
||||
class PostCompileHookContext:
|
||||
"""Context manager for post-compile hook for a compiler."""
|
||||
|
||||
def __init__(self, compiler: Compiler, hook: Callable[[ir.Module], None]):
|
||||
self.compiler = compiler
|
||||
self.hook = hook
|
||||
self.prev_post_compile_hook = None
|
||||
|
||||
def __enter__(self):
|
||||
self.prev_post_compile_hook = self.compiler._post_compile_hook
|
||||
self.compiler._post_compile_hook = self.hook
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.compiler._post_compile_hook = self.prev_post_compile_hook
|
||||
|
||||
|
||||
class CompileOption:
|
||||
"""
|
||||
Base class for compile options.
|
||||
@@ -276,6 +297,11 @@ class BooleanBasedFileDumpOption(CompileOption):
|
||||
return ""
|
||||
|
||||
|
||||
class EmptyCompileOption(CompileOption):
|
||||
def serialize(self):
|
||||
return ""
|
||||
|
||||
|
||||
class OptLevel(CompileOption):
|
||||
option_name = "opt-level"
|
||||
|
||||
@@ -329,6 +355,10 @@ class GPUArch(StringCompileOption):
|
||||
self._value = value
|
||||
|
||||
|
||||
class EnableTVMFFI(EmptyCompileOption):
|
||||
option_name = "enable-tvm-ffi"
|
||||
|
||||
|
||||
class CompileOptions:
|
||||
"""
|
||||
This class encapsulates compilation options to configure the JIT compilation.
|
||||
@@ -349,6 +379,7 @@ class CompileOptions:
|
||||
KeepPTX: KeepPTX(False),
|
||||
GPUArch: GPUArch(""),
|
||||
LinkLibraries: LinkLibraries(""),
|
||||
EnableTVMFFI: EnableTVMFFI(False),
|
||||
}
|
||||
|
||||
if options is not None:
|
||||
@@ -376,15 +407,28 @@ class CompileOptions:
|
||||
self.options[EnableAssertions].value = True
|
||||
if envar.lineinfo:
|
||||
self.options[GenerateLineInfo].value = True
|
||||
if envar.enable_tvm_ffi:
|
||||
self.options[EnableTVMFFI].value = True
|
||||
|
||||
# Update the dump path if the option is set
|
||||
arch = (
|
||||
envar.arch
|
||||
if self.options[GPUArch].value == ""
|
||||
else self.options[GPUArch].value
|
||||
)
|
||||
if self.options[KeepPTX].value:
|
||||
self.options[KeepPTX].dump_path = os.path.join(
|
||||
envar.dump_dir, f"{function_name}.ptx"
|
||||
envar.dump_dir, f"{function_name}"
|
||||
)
|
||||
self.options[KeepPTX].full_ptx_path = os.path.join(
|
||||
envar.dump_dir, f"{function_name}.{arch}.ptx"
|
||||
)
|
||||
if self.options[KeepCUBIN].value:
|
||||
self.options[KeepCUBIN].dump_path = os.path.join(
|
||||
envar.dump_dir, f"{function_name}.cubin"
|
||||
envar.dump_dir, f"{function_name}"
|
||||
)
|
||||
self.options[KeepCUBIN].full_cubin_path = os.path.join(
|
||||
envar.dump_dir, f"{function_name}.{arch}.cubin"
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -399,12 +443,38 @@ class CompileOptions:
|
||||
def dump_ptx_path(self) -> str | None:
|
||||
return self.options[KeepPTX].dump_path if self.options[KeepPTX].value else None
|
||||
|
||||
@property
|
||||
def full_ptx_path(self) -> str | None:
|
||||
return (
|
||||
self.options[KeepPTX].full_ptx_path if self.options[KeepPTX].value else None
|
||||
)
|
||||
|
||||
@property
|
||||
def dump_cubin_path(self) -> str | None:
|
||||
return (
|
||||
self.options[KeepCUBIN].dump_path if self.options[KeepCUBIN].value else None
|
||||
)
|
||||
|
||||
@property
|
||||
def full_cubin_path(self) -> str | None:
|
||||
return (
|
||||
self.options[KeepCUBIN].full_cubin_path
|
||||
if self.options[KeepCUBIN].value
|
||||
else None
|
||||
)
|
||||
|
||||
@property
|
||||
def enable_tvm_ffi(self) -> bool:
|
||||
ret = self.options[EnableTVMFFI].value
|
||||
if ret:
|
||||
try:
|
||||
import tvm_ffi
|
||||
except ModuleNotFoundError:
|
||||
raise DSLRuntimeError(
|
||||
"TVM FFI is not installed, please install it via `pip install apache-tvm-ffi`"
|
||||
)
|
||||
return ret
|
||||
|
||||
def to_str(self) -> str:
|
||||
"""
|
||||
Generate a string representation of all compilation options
|
||||
@@ -433,6 +503,7 @@ def _parse_compile_options_from_str(options: str) -> CompileOptions:
|
||||
"keep_cubin": KeepCUBIN,
|
||||
"keep_ptx": KeepPTX,
|
||||
"gpu_arch": GPUArch,
|
||||
"enable_tvm_ffi": EnableTVMFFI,
|
||||
}
|
||||
return mapping[option_str]
|
||||
|
||||
@@ -448,6 +519,7 @@ def _parse_compile_options_from_str(options: str) -> CompileOptions:
|
||||
parser.add_argument("--keep-ptx", action="store_true", default=False)
|
||||
parser.add_argument("--ptxas-options", type=str, default="")
|
||||
parser.add_argument("--gpu-arch", type=str, default="")
|
||||
parser.add_argument("--enable-tvm-ffi", action="store_true", default=False)
|
||||
compile_options = CompileOptions()
|
||||
try:
|
||||
# Use shlex to properly handle options with spaces
|
||||
@@ -553,6 +625,9 @@ class CompileCallable:
|
||||
|
||||
# process compile options, extract the options and remove them from the kwargs
|
||||
options = kwargs.pop("options", None)
|
||||
if isinstance(options, str) and len(options) == 0:
|
||||
options = None
|
||||
|
||||
if options is not None and isinstance(options, str):
|
||||
compile_options = _parse_compile_options_from_str(options)
|
||||
else:
|
||||
|
||||
@@ -27,6 +27,7 @@ import re
|
||||
import inspect
|
||||
import argparse
|
||||
import hashlib
|
||||
import weakref
|
||||
from functools import lru_cache, wraps
|
||||
from collections import namedtuple, OrderedDict
|
||||
from abc import ABC, abstractmethod
|
||||
@@ -52,7 +53,7 @@ from .runtime.jit_arg_adapters import is_argument_constexpr, JitArgAdapterRegist
|
||||
|
||||
from .ast_preprocessor import DSLPreprocessor
|
||||
from .common import *
|
||||
from .typing import get_c_pointers, get_mlir_types, Integer
|
||||
from .typing import get_c_pointers, get_mlir_types, Integer, arg_compatible_with_tvm_ffi
|
||||
from .arch import Arch
|
||||
|
||||
# =============================================================================
|
||||
@@ -345,6 +346,7 @@ class BaseDSL:
|
||||
|
||||
if preprocess:
|
||||
self.preprocessor = DSLPreprocessor(dsl_package_name)
|
||||
|
||||
log().info(f"Initializing {name} DSL")
|
||||
log().debug(f"Logger initialized for {self.name}")
|
||||
|
||||
@@ -756,6 +758,7 @@ class BaseDSL:
|
||||
args_spec: inspect.FullArgSpec,
|
||||
*,
|
||||
is_host=True,
|
||||
compile_only=False,
|
||||
):
|
||||
"""Generate JIT function arguments."""
|
||||
|
||||
@@ -806,7 +809,14 @@ class BaseDSL:
|
||||
jit_adapted_args.append(arg)
|
||||
|
||||
if is_host:
|
||||
jit_exec_arg.extend(get_c_pointers(arg))
|
||||
if self.envar.enable_tvm_ffi:
|
||||
if not arg_compatible_with_tvm_ffi(arg):
|
||||
raise DSLRuntimeError(
|
||||
f"Argument #{i + 1} ({arg_name}) is not a TVM FFI argument."
|
||||
)
|
||||
jit_exec_arg.extend([arg])
|
||||
else:
|
||||
jit_exec_arg.extend(get_c_pointers(arg))
|
||||
jit_arg_type.extend(get_mlir_types(arg))
|
||||
else:
|
||||
dyn_vals = extract_mlir_values(arg)
|
||||
@@ -814,7 +824,10 @@ class BaseDSL:
|
||||
jit_arg_type.extend([v.type for v in dyn_vals])
|
||||
|
||||
if not jit_arg_type or not jit_exec_arg:
|
||||
if (is_host and hasattr(arg, "__c_pointers__")) or (
|
||||
# when it is compile only, we don't have to prepare the executable arguments.
|
||||
if (
|
||||
is_host and (compile_only or hasattr(arg, "__c_pointers__"))
|
||||
) or (
|
||||
not is_host
|
||||
and hasattr(arg, "__extract_mlir_values__")
|
||||
and hasattr(arg, "__new_from_mlir_values__")
|
||||
@@ -845,20 +858,32 @@ class BaseDSL:
|
||||
return jit_exec_args, jit_arg_types, jit_arg_attrs, jit_adapted_args
|
||||
|
||||
def generate_mlir_function_types(
|
||||
self, func, function_name, input_args, kwargs, args_spec: inspect.FullArgSpec
|
||||
self,
|
||||
func,
|
||||
function_name,
|
||||
input_args,
|
||||
kwargs,
|
||||
args_spec: inspect.FullArgSpec,
|
||||
compile_only=False,
|
||||
):
|
||||
"""Convert input arguments to MLIR function signature also convert numpy arrays to memref."""
|
||||
|
||||
exe_args, types, attrs, adapted_args = self._generate_jit_func_args(
|
||||
func, function_name, input_args, kwargs, args_spec, is_host=True
|
||||
func,
|
||||
function_name,
|
||||
input_args,
|
||||
kwargs,
|
||||
args_spec,
|
||||
is_host=True,
|
||||
compile_only=compile_only,
|
||||
)
|
||||
|
||||
log().debug("Execution Arguments: %s", ", ".join(map(str, exe_args)))
|
||||
log().debug("Types: %s", ", ".join(map(str, types)))
|
||||
|
||||
assert len(exe_args) == len(types), (
|
||||
"expects the same number of arguments and function parameters"
|
||||
)
|
||||
assert (
|
||||
compile_only or self.envar.enable_tvm_ffi or len(exe_args) == len(types)
|
||||
), "expects the same number of arguments and function parameters"
|
||||
|
||||
return exe_args, types, adapted_args
|
||||
|
||||
@@ -867,6 +892,7 @@ class BaseDSL:
|
||||
cluster: list = None
|
||||
grid: list = field(default_factory=lambda: [1, 1, 1])
|
||||
block: list = field(default_factory=lambda: [1, 1, 1])
|
||||
max_number_threads: list = field(default_factory=lambda: [0, 0, 0])
|
||||
smem: int = None
|
||||
async_deps: list = field(default_factory=list)
|
||||
has_cluster: bool = False
|
||||
@@ -906,6 +932,13 @@ class BaseDSL:
|
||||
elif len(self.cluster) != 3:
|
||||
raise DSLRuntimeError(f"Expect 3d cluster!")
|
||||
|
||||
def has_max_number_threads(self):
|
||||
"""Check if max_number_threads is given by user"""
|
||||
return all(
|
||||
value == 0 if not is_dynamic_expression(value) else False
|
||||
for value in self.max_number_threads
|
||||
)
|
||||
|
||||
def diagnostic(self):
|
||||
"""Check command line parameters and enables diagnostic"""
|
||||
# Check command line arguments "-diagnostic"
|
||||
@@ -1029,7 +1062,6 @@ class BaseDSL:
|
||||
pipeline = re.sub(r"{.+}", opt_str, pipeline)
|
||||
else:
|
||||
pipeline = pipeline.rstrip(")") + f"{{{opt_str}}})"
|
||||
log().debug(f"Using pipeline = {pipeline}")
|
||||
return pipeline
|
||||
|
||||
def get_shared_libs(self) -> list:
|
||||
@@ -1101,6 +1133,18 @@ class BaseDSL:
|
||||
|
||||
return module
|
||||
|
||||
def get_return_types(self) -> List[ir.Type]:
|
||||
"""
|
||||
Get the return types of the host function.
|
||||
"""
|
||||
return []
|
||||
|
||||
def generate_default_return_values(self, ip=None) -> List[ir.Value]:
|
||||
"""
|
||||
Generate the default return values of the host function.
|
||||
"""
|
||||
return []
|
||||
|
||||
def generate_original_ir(
|
||||
self,
|
||||
ir,
|
||||
@@ -1115,27 +1159,35 @@ class BaseDSL:
|
||||
frame=None,
|
||||
):
|
||||
def build_ir_module():
|
||||
module = ir.Module.create(loc=self.get_location(frame))
|
||||
loc = self.get_location(frame)
|
||||
module = ir.Module.create(loc=loc)
|
||||
unit_attr = ir.UnitAttr.get()
|
||||
module.operation.attributes["gpu.container_module"] = unit_attr
|
||||
|
||||
with ir.InsertionPoint(module.body):
|
||||
# Always generate gpu module. It's canonicalized by the compiler when it's not used.
|
||||
self._build_gpu_module(gpu_module_attrs, loc=self.get_location(frame))
|
||||
self._build_gpu_module(gpu_module_attrs, loc=loc)
|
||||
|
||||
ret_types = self.get_return_types()
|
||||
fop = func.FuncOp(
|
||||
function_name, (func_types, []), loc=self.get_location(frame)
|
||||
function_name, (func_types, ret_types), loc=loc
|
||||
)
|
||||
fop.attributes["llvm.emit_c_interface"] = ir.UnitAttr.get()
|
||||
log().debug("Generated Function OP [%s]", fop)
|
||||
with ir.InsertionPoint(fop.add_entry_block()):
|
||||
# Attach per-argument source locations if supported by the FuncOp binding.
|
||||
arg_locs = [loc for _ in range(len(func_types))]
|
||||
entry_block = fop.add_entry_block(arg_locs=arg_locs)
|
||||
with ir.InsertionPoint(entry_block):
|
||||
ir_args, ir_kwargs = self.generate_execution_arguments(
|
||||
args, kwargs, fop, args_spec
|
||||
)
|
||||
# Call user function body
|
||||
try:
|
||||
result = funcBody(*ir_args, **ir_kwargs)
|
||||
func.ReturnOp([], loc=self.get_location(frame))
|
||||
default_ret_values = self.generate_default_return_values(
|
||||
ir.InsertionPoint.current
|
||||
)
|
||||
func.ReturnOp(default_ret_values, loc=loc)
|
||||
except NameError as name_error:
|
||||
raise DSLRuntimeError(
|
||||
f"💥💥💥 Error during runtime code generation for function `{funcBody.__name__}` 💥💥💥",
|
||||
@@ -1165,6 +1217,10 @@ class BaseDSL:
|
||||
args_spec,
|
||||
no_cache,
|
||||
func_type=JitCompiledFunction,
|
||||
*,
|
||||
dynamic_args=None,
|
||||
dynamic_kwargs=None,
|
||||
original_function_name=None,
|
||||
):
|
||||
# If `gpu-arch` is set by compile_options, use it. Otherwise, use the arch from the environment variable.
|
||||
compile_gpu_arch = (
|
||||
@@ -1178,6 +1234,7 @@ class BaseDSL:
|
||||
pipeline = self.preprocess_pipeline(
|
||||
self._get_pipeline(pipeline), compile_gpu_arch
|
||||
)
|
||||
log().debug(f"Using pipeline = {pipeline}")
|
||||
shared_libs = self.get_shared_libs()
|
||||
profiler = timer(enable=self.envar.jit_time_profiling)
|
||||
if (
|
||||
@@ -1212,7 +1269,7 @@ class BaseDSL:
|
||||
)
|
||||
capi_func = profiler(engine.lookup)(function_name) if engine else None
|
||||
|
||||
fn = JitCompiledFunction(
|
||||
fn = func_type(
|
||||
module,
|
||||
engine,
|
||||
capi_func,
|
||||
@@ -1221,12 +1278,15 @@ class BaseDSL:
|
||||
self.kernel_info,
|
||||
jit_time_profiling=self.envar.jit_time_profiling,
|
||||
jit_function_artifacts=JitFunctionArtifacts(
|
||||
PTX=self.compile_options.dump_ptx_path,
|
||||
CUBIN=self.compile_options.dump_cubin_path,
|
||||
PTX=self.compile_options.full_ptx_path,
|
||||
CUBIN=self.compile_options.full_cubin_path,
|
||||
MLIR=(self.dump_mlir_path if self.envar.keep_ir else None),
|
||||
),
|
||||
)
|
||||
|
||||
# set dynamic arguments if the jit_function is a JitCompiledFunction for AOT generation.
|
||||
fn.set_dynamic_args(dynamic_args, dynamic_kwargs)
|
||||
|
||||
if not no_cache:
|
||||
# module stored in cache is compiled.
|
||||
self.jit_cache[module_hash] = fn
|
||||
@@ -1243,6 +1303,34 @@ class BaseDSL:
|
||||
# reset the compile options after the compilation is done.
|
||||
self.compile_options = CompileOptions()
|
||||
|
||||
def extract_dynamic_args(self, funcBody, args, kwargs, args_spec):
|
||||
"""This function is used to extract the original dynamic arguments for AOT C header generation.
|
||||
The dynamic argument is the argument which is not marked as `Constexpr` in the function signature.
|
||||
"""
|
||||
dynamic_args = []
|
||||
dynamic_kwargs = OrderedDict()
|
||||
for i, arg in enumerate(args):
|
||||
if not is_argument_constexpr(
|
||||
arg,
|
||||
args_spec.annotations.get(args_spec.args[i], None),
|
||||
args_spec.args[i],
|
||||
i,
|
||||
funcBody,
|
||||
):
|
||||
try:
|
||||
dynamic_args.append(weakref.proxy(arg))
|
||||
except TypeError:
|
||||
# If arg cannot be weakly referenced (e.g., int, float)
|
||||
dynamic_args.append(arg)
|
||||
for i, (k, v) in enumerate(kwargs.items()):
|
||||
if not is_argument_constexpr(v, args_spec.kwonlyargs[i], k, i, funcBody):
|
||||
try:
|
||||
dynamic_kwargs[k] = weakref.proxy(v)
|
||||
except TypeError:
|
||||
# If v cannot be weakly referenced (e.g., int, float)
|
||||
dynamic_kwargs[k] = v
|
||||
return dynamic_args, dynamic_kwargs
|
||||
|
||||
def generate_mlir(
|
||||
self,
|
||||
funcBody,
|
||||
@@ -1258,49 +1346,63 @@ class BaseDSL:
|
||||
frame=None,
|
||||
):
|
||||
"""Generate MLIR module and compile iself.T_provider."""
|
||||
with ir.Context(), ir.Location.unknown():
|
||||
# Convert input arguments to MLIR arguments
|
||||
exe_args, func_types, adapted_args = self.generate_mlir_function_types(
|
||||
funcBody, function_name, args, kwargs, args_spec
|
||||
)
|
||||
|
||||
# Generate original ir module and its hash value.
|
||||
module, module_hash, result = self.generate_original_ir(
|
||||
ir,
|
||||
func,
|
||||
funcBody,
|
||||
kwargs,
|
||||
function_name,
|
||||
func_types,
|
||||
gpu_module_attrs,
|
||||
args,
|
||||
args_spec,
|
||||
frame=frame,
|
||||
)
|
||||
|
||||
# dryrun is used to only generate IR
|
||||
if self.envar.dryrun:
|
||||
return result
|
||||
|
||||
if (
|
||||
no_cache
|
||||
or module_hash not in self.jit_cache
|
||||
or self.jit_cache[module_hash].capi_func is None
|
||||
):
|
||||
# no cache or cache miss, do ir generation/compilation/jit engine
|
||||
jit_function = self.compile_and_cache(
|
||||
module, module_hash, function_name, pipeline, args_spec, no_cache
|
||||
with ir.Context(), self.get_location(frame):
|
||||
try:
|
||||
# Convert input arguments to MLIR arguments
|
||||
exe_args, func_types, adapted_args = self.generate_mlir_function_types(
|
||||
funcBody, function_name, args, kwargs, args_spec, compile_only
|
||||
)
|
||||
else:
|
||||
# cache hit
|
||||
log().info(
|
||||
"JIT cache hit IN-MEMORY function=[%s] module_hash=[%s]",
|
||||
dynamic_args, dynamic_kwargs = self.extract_dynamic_args(
|
||||
funcBody, args, kwargs, args_spec
|
||||
)
|
||||
original_function_name = funcBody.__name__
|
||||
|
||||
# Generate original ir module and its hash value.
|
||||
module, module_hash, result = self.generate_original_ir(
|
||||
ir,
|
||||
func,
|
||||
funcBody,
|
||||
kwargs,
|
||||
function_name,
|
||||
module_hash,
|
||||
func_types,
|
||||
gpu_module_attrs,
|
||||
args,
|
||||
args_spec,
|
||||
frame=frame,
|
||||
)
|
||||
jit_function = self.jit_cache[module_hash]
|
||||
|
||||
self.post_compilation_cleanup()
|
||||
# dryrun is used to only generate IR
|
||||
if self.envar.dryrun:
|
||||
return result
|
||||
|
||||
if (
|
||||
no_cache
|
||||
or module_hash not in self.jit_cache
|
||||
or self.jit_cache[module_hash].capi_func is None
|
||||
):
|
||||
# no cache or cache miss, do ir generation/compilation/jit engine
|
||||
jit_function = self.compile_and_cache(
|
||||
module,
|
||||
module_hash,
|
||||
function_name,
|
||||
pipeline,
|
||||
args_spec,
|
||||
no_cache,
|
||||
dynamic_args=dynamic_args,
|
||||
dynamic_kwargs=dynamic_kwargs,
|
||||
original_function_name=original_function_name,
|
||||
)
|
||||
else:
|
||||
# cache hit
|
||||
log().info(
|
||||
"JIT cache hit IN-MEMORY function=[%s] module_hash=[%s]",
|
||||
function_name,
|
||||
module_hash,
|
||||
)
|
||||
jit_function = self.jit_cache[module_hash]
|
||||
|
||||
finally:
|
||||
self.post_compilation_cleanup()
|
||||
|
||||
# If compile_only is set, bypass execution return the jit_executor directly
|
||||
if compile_only:
|
||||
@@ -1569,13 +1671,13 @@ class BaseDSL:
|
||||
"""
|
||||
ret = None
|
||||
|
||||
with ir.Context(), ir.Location.unknown():
|
||||
with ir.Context(), self.get_location():
|
||||
loc = self.get_location()
|
||||
module = ir.Module.create(loc=loc)
|
||||
unit_attr = ir.UnitAttr.get()
|
||||
module.operation.attributes["gpu.container_module"] = unit_attr
|
||||
with ir.InsertionPoint(module.body):
|
||||
self._build_gpu_module({})
|
||||
self._build_gpu_module({}, loc=loc)
|
||||
ret, kernel_name = kernel_generator()
|
||||
log().debug(
|
||||
f"Kernel generator returned: ret={ret}, kernel_name={kernel_name}"
|
||||
|
||||
@@ -245,8 +245,15 @@ def get_prefix_dsl_libs(prefix: str):
|
||||
]
|
||||
|
||||
libs_cand = find_libs_in_ancestors(start, target_libs, lib_folder_guesses)
|
||||
|
||||
optional_libs_cand = find_libs_in_ancestors(
|
||||
start, {"cuda_dialect_runtime"}, lib_folder_guesses
|
||||
)
|
||||
|
||||
if libs_cand:
|
||||
dsl_libs = ":".join(libs_cand)
|
||||
if optional_libs_cand:
|
||||
dsl_libs += ":" + ":".join(optional_libs_cand)
|
||||
return dsl_libs
|
||||
|
||||
return None
|
||||
@@ -312,6 +319,7 @@ class EnvironmentVarManager(LogEnvironmentManager):
|
||||
- [DSL_NAME]_DISABLE_FILE_CACHING: Disable file caching (default: False)
|
||||
- [DSL_NAME]_FILE_CACHING_CAPACITY: Limits the number of the cache save/load files (default: 1000)
|
||||
- [DSL_NAME]_LIBS: Path to dependent shared libraries (default: None)
|
||||
- [DSL_NAME]_ENABLE_TVM_FFI: Enable TVM-FFI or not (default: False)
|
||||
"""
|
||||
|
||||
def __init__(self, prefix="DSL"):
|
||||
@@ -358,3 +366,5 @@ class EnvironmentVarManager(LogEnvironmentManager):
|
||||
|
||||
# whether to enable assert in host and device code
|
||||
self.enable_assertions = get_bool_env_var(f"{prefix}_ENABLE_ASSERTIONS", False)
|
||||
|
||||
self.enable_tvm_ffi = get_bool_env_var(f"{prefix}_ENABLE_TVM_FFI", False)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# 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 .c_header_generator import CHeaderGenerator
|
||||
from .export import get_export_module, dump_to_object, export_to_c
|
||||
|
||||
__all__ = [
|
||||
"CHeaderGenerator",
|
||||
"get_export_module",
|
||||
"dump_to_object",
|
||||
"export_to_c",
|
||||
]
|
||||
@@ -0,0 +1,383 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
|
||||
#
|
||||
# Use of this software is governed by the terms and conditions of the
|
||||
# NVIDIA End User License Agreement (EULA), available at:
|
||||
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
|
||||
#
|
||||
# Any use, reproduction, disclosure, or distribution of this software
|
||||
# and related documentation outside the scope permitted by the EULA
|
||||
# is strictly prohibited.
|
||||
|
||||
from ..typing import (
|
||||
Numeric,
|
||||
NumericMeta,
|
||||
Int8,
|
||||
Int16,
|
||||
Int32,
|
||||
Int64,
|
||||
Uint8,
|
||||
Uint16,
|
||||
Uint32,
|
||||
Uint64,
|
||||
Boolean,
|
||||
Float16,
|
||||
Float32,
|
||||
Float64,
|
||||
TFloat32,
|
||||
)
|
||||
from ..dsl import is_dynamic_expression
|
||||
from ..common import DSLRuntimeError
|
||||
from ..jit_executor import ExecutionArgs
|
||||
from ..._mlir import ir
|
||||
|
||||
from typing import Type, List, Any, Dict
|
||||
from inspect import isclass
|
||||
import cuda.bindings.driver as cuda
|
||||
|
||||
# =============================================================================
|
||||
# C Header Generator for c/cpp AOT support
|
||||
# =============================================================================
|
||||
cubin_suffix = "cubin"
|
||||
|
||||
|
||||
class CHeaderGenerator:
|
||||
"""This class provides a Export C Header Generator for c/cpp AOT support."""
|
||||
|
||||
includes = """
|
||||
#pragma once
|
||||
|
||||
#include <cuda.h>
|
||||
#include <cuda_fp16.h>
|
||||
#include <stdio.h>
|
||||
|
||||
"""
|
||||
cuda_error_check = r"""_CUDA_ERROR_CHECK(err) { \
|
||||
if ((err) != CUDA_SUCCESS) { \
|
||||
const char *pErrStr = NULL; \
|
||||
if (cuGetErrorString(err, &pErrStr) != CUDA_SUCCESS) { \
|
||||
printf("Error: cuGetErrorString failed\n"); \
|
||||
exit(err); \
|
||||
} \
|
||||
const char *pErrName = NULL; \
|
||||
if (cuGetErrorName(err, &pErrName) != CUDA_SUCCESS) { \
|
||||
printf("Error: cuGetErrorName failed\n"); \
|
||||
exit(err); \
|
||||
} \
|
||||
printf("Got Cuda Error: %s %s\n", pErrName, pErrStr); \
|
||||
exit(err); \
|
||||
} \
|
||||
}
|
||||
"""
|
||||
|
||||
numeric_to_c_type = {
|
||||
Int8: "int8_t ",
|
||||
Int16: "int16_t ",
|
||||
Int32: "int32_t ",
|
||||
Int64: "int64_t ",
|
||||
Uint8: "uint8_t ",
|
||||
Uint16: "uint16_t ",
|
||||
Uint32: "uint32_t ",
|
||||
Uint64: "uint64_t ",
|
||||
Boolean: "uint8_t ",
|
||||
Float32: "float ",
|
||||
TFloat32: "float ",
|
||||
Float64: "double ",
|
||||
Float16: "__half_raw ",
|
||||
}
|
||||
|
||||
def _count_dynamic_expression(self, arg):
|
||||
"""
|
||||
Count the number of dynamic values in the argument.
|
||||
"""
|
||||
if isinstance(arg, (list, tuple)):
|
||||
return sum(self._count_dynamic_expression(x) for x in arg)
|
||||
elif is_dynamic_expression(arg):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
def _generate_numeric_argument(self, arg_name: str, arg_type: Type[Numeric]):
|
||||
"""
|
||||
Generate the argument of the wrapper function.
|
||||
"""
|
||||
if arg_type in self.numeric_to_c_type:
|
||||
return self.numeric_to_c_type[arg_type] + arg_name
|
||||
raise DSLRuntimeError(
|
||||
f"Unsupported argument type for c function argument generation: {arg_type}"
|
||||
)
|
||||
|
||||
def _generate_check_cuda(self, dsl_name: str):
|
||||
check_cuda = (
|
||||
f"""
|
||||
// Macro to check for cuda errors.
|
||||
#ifndef {dsl_name}_CUDA_ERROR_CHECK
|
||||
#define {dsl_name}"""
|
||||
+ self.cuda_error_check
|
||||
+ f"""
|
||||
#endif
|
||||
"""
|
||||
)
|
||||
|
||||
return check_cuda
|
||||
|
||||
def _generate_kernel_metadata(
|
||||
self, symbol_prefix: str, kernel_info: Dict[str, List], dsl_name: str
|
||||
):
|
||||
"""
|
||||
Generate the kernel metadata for the compiled function.
|
||||
"""
|
||||
function_declarations = []
|
||||
function_loads = []
|
||||
function_set_attributes = []
|
||||
function_non_portable_cluster_size_allowed = []
|
||||
for sym, attrs in kernel_info.items():
|
||||
function_declaration = f"CUfunction {sym};"
|
||||
function_load = f'{dsl_name}_CUDA_ERROR_CHECK(cuModuleGetFunction(&metadata->{sym}, metadata->module, "{sym}"));'
|
||||
function_declarations.append(function_declaration)
|
||||
function_loads.append(function_load)
|
||||
for attr in attrs:
|
||||
function_set_attributes.append(
|
||||
f"{dsl_name}_CUDA_ERROR_CHECK(cuFuncSetAttribute(metadata->{sym}, {attr.name}, {attr.value}));"
|
||||
)
|
||||
function_non_portable_cluster_size_allowed.append(
|
||||
f"{dsl_name}_CUDA_ERROR_CHECK(cuFuncSetAttribute(metadata->{sym}, CU_FUNC_ATTRIBUTE_NON_PORTABLE_CLUSTER_SIZE_ALLOWED, 1));"
|
||||
)
|
||||
function_declarations_str = "\n ".join(function_declarations)
|
||||
kernel_metadata_struct = f"""
|
||||
typedef struct {{
|
||||
CUmodule module;
|
||||
{function_declarations_str}
|
||||
}} {symbol_prefix}_Kernel_Metadata_t;
|
||||
"""
|
||||
function_loads_str = "\n ".join(function_loads)
|
||||
function_set_attributes_str = "\n ".join(function_set_attributes)
|
||||
function_non_portable_cluster_size_allowed_str = "\n ".join(
|
||||
function_non_portable_cluster_size_allowed
|
||||
)
|
||||
kernel_metadata_load = f"""
|
||||
static inline void {symbol_prefix}_Kernel_Metadata_Load({symbol_prefix}_Kernel_Metadata_t *metadata) {{
|
||||
{dsl_name}_CUDA_ERROR_CHECK(cuModuleLoadData(&metadata->module, {symbol_prefix}_{cubin_suffix}));
|
||||
{function_loads_str}
|
||||
{function_set_attributes_str}
|
||||
int driver_version;
|
||||
{dsl_name}_CUDA_ERROR_CHECK(cuDriverGetVersion(&driver_version));
|
||||
if (driver_version >= 11080) {{
|
||||
{function_non_portable_cluster_size_allowed_str}
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
kernel_metadata_unload = f"""
|
||||
static inline void {symbol_prefix}_Kernel_Metadata_Unload({symbol_prefix}_Kernel_Metadata_t *metadata) {{
|
||||
{dsl_name}_CUDA_ERROR_CHECK(cuModuleUnload(metadata->module));
|
||||
}}
|
||||
"""
|
||||
|
||||
return kernel_metadata_struct + kernel_metadata_load + kernel_metadata_unload
|
||||
|
||||
def _generate_arguments(
|
||||
self,
|
||||
symbol_prefix: str,
|
||||
args_spec: ExecutionArgs,
|
||||
args: List[Any],
|
||||
kwargs: Dict[str, Any],
|
||||
):
|
||||
"""
|
||||
Generate the arguments of the wrapper function.
|
||||
"""
|
||||
arguments = []
|
||||
packed_args = []
|
||||
declarations = []
|
||||
# traverse the runtime args_spec and generate the arguments
|
||||
rectified_args = args_spec.get_rectified_args(args, kwargs)
|
||||
input_arg_names = args_spec.args_spec.args + args_spec.args_spec.kwonlyargs
|
||||
for arg_name, arg in zip(input_arg_names, rectified_args):
|
||||
arg_type = args_spec.args_spec.annotations.get(arg_name, None)
|
||||
|
||||
# process optional argument
|
||||
if arg is None:
|
||||
continue
|
||||
|
||||
# Generate basic numeric types
|
||||
if isinstance(arg_type, NumericMeta):
|
||||
arguments.append(self._generate_numeric_argument(arg_name, arg_type))
|
||||
packed_args.append("&" + arg_name)
|
||||
elif isclass(arg_type) and issubclass(arg_type, cuda.CUstream):
|
||||
arguments.append("CUstream " + arg_name)
|
||||
packed_args.append("&" + arg_name)
|
||||
else:
|
||||
raise DSLRuntimeError(
|
||||
f"Unsupported argument for c function argument generation: {arg} with type {arg_type}"
|
||||
)
|
||||
|
||||
return arguments, packed_args, declarations
|
||||
|
||||
def _generate_wrapper_function(
|
||||
self,
|
||||
symbol_prefix: str,
|
||||
args_spec: ExecutionArgs,
|
||||
function_name: str,
|
||||
kernel_info: Dict[str, List],
|
||||
dynamic_args: list,
|
||||
dynamic_kwargs: dict,
|
||||
):
|
||||
"""
|
||||
Generate the wrapper function for the compiled function which is provided to users as the entry point.
|
||||
It uses the `symbol_prefix` as the function name for identification. The host/device symbols are hidden under the bytecode.
|
||||
"""
|
||||
# 1. Get the name of the function wrapper
|
||||
wrapper_function_name = f"{symbol_prefix}_wrapper"
|
||||
capi_function_name = f"_mlir_{symbol_prefix}__mlir_ciface_{function_name}"
|
||||
# 2. Generate the signature of the wrapper function
|
||||
arguments, packed_args, declarations = self._generate_arguments(
|
||||
symbol_prefix, args_spec, dynamic_args, dynamic_kwargs
|
||||
)
|
||||
declarations = "\n".join(declarations)
|
||||
|
||||
# 3. Generate the wrapper function
|
||||
kernel_symbols = tuple(kernel_info.keys())
|
||||
kernel_symbols_str = ", ".join([f"&metadata->{sym}" for sym in kernel_symbols])
|
||||
function = (
|
||||
declarations
|
||||
+ f"""
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
#endif
|
||||
void {capi_function_name}(void **args, int32_t num_args);
|
||||
|
||||
static inline void {wrapper_function_name}({symbol_prefix}_Kernel_Metadata_t *metadata, {", ".join(arguments)}) {{
|
||||
void *args[{len(packed_args) + len(kernel_symbols)}] = {{
|
||||
{", ".join(packed_args)}, {kernel_symbols_str}
|
||||
}};
|
||||
{capi_function_name}(args, {len(packed_args) + len(kernel_symbols)});
|
||||
}}
|
||||
"""
|
||||
)
|
||||
return function
|
||||
|
||||
def _generate_binary_declaration(self, symbol_prefix: str):
|
||||
"""
|
||||
Generate the binary of the compiled function.
|
||||
"""
|
||||
|
||||
varname = symbol_prefix + "_" + cubin_suffix
|
||||
binary = f"""
|
||||
extern const unsigned char {varname}[];
|
||||
"""
|
||||
return binary
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
symbol_prefix: str,
|
||||
export_module: ir.Module,
|
||||
args_spec: ExecutionArgs,
|
||||
function_name: str,
|
||||
kernel_info: Dict[str, List],
|
||||
dynamic_args: list,
|
||||
dynamic_kwargs: dict,
|
||||
dsl_name: str,
|
||||
) -> str:
|
||||
if len(kernel_info) > 0:
|
||||
check_cuda = self._generate_check_cuda(dsl_name)
|
||||
kernel_metadata = self._generate_kernel_metadata(
|
||||
symbol_prefix, kernel_info, dsl_name
|
||||
)
|
||||
binary = self._generate_binary_declaration(symbol_prefix)
|
||||
else:
|
||||
check_cuda = ""
|
||||
kernel_metadata = ""
|
||||
binary = ""
|
||||
function = self._generate_wrapper_function(
|
||||
symbol_prefix,
|
||||
args_spec,
|
||||
function_name,
|
||||
kernel_info,
|
||||
dynamic_args,
|
||||
dynamic_kwargs,
|
||||
)
|
||||
header = self.includes + check_cuda + binary + kernel_metadata + function
|
||||
return header
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Below is the example output of a complete header file.
|
||||
# =============================================================================
|
||||
# #pragma once
|
||||
|
||||
# #include <cuda.h>
|
||||
# #include <cuda_fp16.h>
|
||||
# #include <stdio.h>
|
||||
|
||||
|
||||
# // Macro to check for cuda errors.
|
||||
# #ifndef CUTE_DSL_CUDA_ERROR_CHECK
|
||||
# #define CUTE_DSL_CUDA_ERROR_CHECK(err) { \
|
||||
# if ((err) != CUDA_SUCCESS) { \
|
||||
# const char *pErrStr = NULL; \
|
||||
# if (cuGetErrorString(err, &pErrStr) != CUDA_SUCCESS) { \
|
||||
# printf("Error: cuGetErrorString failed\n"); \
|
||||
# exit(err); \
|
||||
# } \
|
||||
# const char *pErrName = NULL; \
|
||||
# if (cuGetErrorName(err, &pErrName) != CUDA_SUCCESS) { \
|
||||
# printf("Error: cuGetErrorName failed\n"); \
|
||||
# exit(err); \
|
||||
# } \
|
||||
# printf("Got Cuda Error: %s %s\n", pErrName, pErrStr); \
|
||||
# exit(err); \
|
||||
# } \
|
||||
# }
|
||||
# #endif
|
||||
|
||||
# extern const unsigned char gemm_cubin[];
|
||||
|
||||
# typedef struct {
|
||||
# CUmodule module;
|
||||
# CUfunction kernel_cutlass_dummy_gemm_tensorptrf32generico1_tensorptrf32generico1_tensorptrf32generico1_0;
|
||||
# } gemm_Kernel_Metadata_t;
|
||||
|
||||
# static inline void gemm_Kernel_Metadata_Load(gemm_Kernel_Metadata_t *metadata) {
|
||||
# CUTE_DSL_CUDA_ERROR_CHECK(cuModuleLoadData(&metadata->module, gemm_cubin));
|
||||
# CUTE_DSL_CUDA_ERROR_CHECK(cuModuleGetFunction(&metadata->kernel_cutlass_dummy_gemm_tensorptrf32generico1_tensorptrf32generico1_tensorptrf32generico1_0, metadata->module, "kernel_cutlass_dummy_gemm_tensorptrf32generico1_tensorptrf32generico1_tensorptrf32generico1_0"));
|
||||
|
||||
# int driver_version;
|
||||
# CUTE_DSL_CUDA_ERROR_CHECK(cuDriverGetVersion(&driver_version));
|
||||
# if (driver_version >= 11080) {
|
||||
# CUTE_DSL_CUDA_ERROR_CHECK(cuFuncSetAttribute(metadata->kernel_cutlass_dummy_gemm_tensorptrf32generico1_tensorptrf32generico1_tensorptrf32generico1_0, CU_FUNC_ATTRIBUTE_NON_PORTABLE_CLUSTER_SIZE_ALLOWED, 1));
|
||||
# }
|
||||
# }
|
||||
|
||||
# static inline void gemm_Kernel_Metadata_Unload(gemm_Kernel_Metadata_t *metadata) {
|
||||
# CUTE_DSL_CUDA_ERROR_CHECK(cuModuleUnload(metadata->module));
|
||||
# }
|
||||
|
||||
# typedef struct {
|
||||
# void *data;
|
||||
# int32_t dynamic_shapes[2];
|
||||
# int64_t dynamic_strides[1];
|
||||
# } gemm_Tensor_a_t;
|
||||
|
||||
|
||||
# typedef struct {
|
||||
# void *data;
|
||||
# int32_t dynamic_shapes[2];
|
||||
# int64_t dynamic_strides[1];
|
||||
# } gemm_Tensor_b_t;
|
||||
|
||||
|
||||
# typedef struct {
|
||||
# void *data;
|
||||
# int32_t dynamic_shapes[2];
|
||||
# int64_t dynamic_strides[1];
|
||||
# } gemm_Tensor_c_t;
|
||||
|
||||
# #ifdef __cplusplus
|
||||
# extern "C"
|
||||
# #endif
|
||||
# void _mlir_gemm__mlir_ciface_cutlass_host_func_Tensorgenericoi641_Tensorgenericoi641_Tensorgenericoi641(void **args, int32_t num_args);
|
||||
|
||||
# static inline void gemm_wrapper(gemm_Kernel_Metadata_t *metadata, gemm_Tensor_a_t *a, gemm_Tensor_b_t *b, gemm_Tensor_c_t *c) {
|
||||
# void *args[4] = {
|
||||
# a, b, c, &metadata->kernel_cutlass_dummy_gemm_tensorptrf32generico1_tensorptrf32generico1_tensorptrf32generico1_0
|
||||
# };
|
||||
# _mlir_gemm__mlir_ciface_cutlass_host_func_Tensorgenericoi641_Tensorgenericoi641_Tensorgenericoi641(args, 4);
|
||||
# }
|
||||
@@ -0,0 +1,199 @@
|
||||
# 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 io
|
||||
import os
|
||||
import array
|
||||
import tempfile
|
||||
|
||||
from ..common import DSLRuntimeError
|
||||
from ..jit_executor import JitCompiledFunction, get_escaped_cubin_bytes
|
||||
from ...base_dsl.dsl import BaseDSL
|
||||
from ...base_dsl.typing import Int32, Int64, Float32, Float64
|
||||
from ..._mlir import ir
|
||||
from ..._mlir.dialects import llvm
|
||||
|
||||
from .c_header_generator import CHeaderGenerator
|
||||
|
||||
from typing import Union
|
||||
|
||||
cubin_suffix = "cubin"
|
||||
|
||||
def get_export_module(ir_module: ir.Module, symbol_prefix: str):
|
||||
"""Get the export module which is cloned from the original compiled ir module, and add the prefix
|
||||
to avoid the symbol conflict.
|
||||
|
||||
@param ir_module: The original compiled ir module. Comes from the JitCompiledFunction.ir_module.
|
||||
@param symbol_prefix: The prefix name of the function. This is the unique identifier name of the function to avoid symbol conflict in the generated object file.
|
||||
@return: The export module of the function.
|
||||
"""
|
||||
# Add prefix for symbol names to avoid conflict with other functions
|
||||
defined_symbols = set()
|
||||
|
||||
def walk_llvm_func_op(op):
|
||||
# not a declaration
|
||||
if (
|
||||
op.name == "llvm.func"
|
||||
and len(op.opview.operation.regions) > 0
|
||||
and len(op.opview.operation.regions[0].blocks) > 0
|
||||
):
|
||||
defined_symbols.add(op.attributes["sym_name"].value)
|
||||
op.attributes["sym_name"] = ir.StringAttr.get(
|
||||
symbol_prefix + "_" + op.attributes["sym_name"].value
|
||||
)
|
||||
return ir.WalkResult.ADVANCE
|
||||
|
||||
def walk_llvm_call_op(op):
|
||||
if op.name == "llvm.call" and op.attributes["callee"].value in defined_symbols:
|
||||
op.attributes["callee"] = ir.FlatSymbolRefAttr.get(
|
||||
symbol_prefix + "_" + op.attributes["callee"].value
|
||||
)
|
||||
return ir.WalkResult.ADVANCE
|
||||
|
||||
with ir.Context():
|
||||
export_module = ir.Module.parse(str(ir_module))
|
||||
export_module.operation.walk(walk_llvm_func_op)
|
||||
export_module.operation.walk(walk_llvm_call_op)
|
||||
return export_module
|
||||
|
||||
|
||||
|
||||
def dump_to_object(
|
||||
prefix_name: str,
|
||||
export_module: ir.Module,
|
||||
jit_function: Union[JitCompiledFunction, "CudaDialectJitCompiledFunction"],
|
||||
dsl: BaseDSL,
|
||||
use_gpu_dialect: bool,
|
||||
) -> bytes:
|
||||
"""Dump the compiled ir function to a bytes object with ELF format. The bytes object contains the host
|
||||
launch entry function and cubin inside.
|
||||
|
||||
@param prefix_name: The prefix name of the function. This is the unique identifier name of the function to avoid symbol conflict in the generated object file.
|
||||
@param export_module: The export module of the function. This is the module that contains the function to be exported.
|
||||
@param jit_function: The jit-compiled function. To provided other metadata for the object file.
|
||||
@param dsl: The dsl object. This is the dsl object to get the compiler provider and shared libs.
|
||||
@return: The bytes object of the function.
|
||||
"""
|
||||
if use_gpu_dialect:
|
||||
cubin_data = None
|
||||
|
||||
def strip_gpu_binary_op(op):
|
||||
if op.name == "gpu.binary":
|
||||
s = io.BytesIO()
|
||||
op.operation.write_bytecode(s)
|
||||
nonlocal cubin_data
|
||||
cubin_data = s.getvalue()
|
||||
cubin_data = cubin_data.split(b'bin = "')[1].split(b'">')[0]
|
||||
cubin_data = get_escaped_cubin_bytes(cubin_data)
|
||||
op.erase()
|
||||
return ir.WalkResult.ADVANCE
|
||||
return ir.WalkResult.ADVANCE
|
||||
|
||||
# Strip gpu related to avoid the object file generating builtin module load/unload functions
|
||||
export_module.operation.walk(strip_gpu_binary_op)
|
||||
|
||||
cubin_array = array.array("b", cubin_data)
|
||||
with (
|
||||
export_module.context,
|
||||
ir.Location.unknown(),
|
||||
ir.InsertionPoint(export_module.body),
|
||||
):
|
||||
new_binary_global_op = llvm.GlobalOp(
|
||||
sym_name="_".join([prefix_name, cubin_suffix]),
|
||||
global_type=ir.Type.parse(f"!llvm.array<{len(cubin_array)} x i8>"),
|
||||
linkage=ir.Attribute.parse("#llvm.linkage<external>"),
|
||||
value=ir.DenseIntElementsAttr.get(cubin_array),
|
||||
constant=True,
|
||||
)
|
||||
if "gpu.container_module" in export_module.operation.attributes:
|
||||
del export_module.operation.attributes["gpu.container_module"]
|
||||
# Generate the object file
|
||||
export_engine = dsl.compiler_provider.jit(
|
||||
export_module, shared_libs=dsl.get_shared_libs()
|
||||
)
|
||||
# This lookup is necessary to make sure the compilation is done.
|
||||
entry_func = export_engine.raw_lookup(
|
||||
"_".join([prefix_name, jit_function.function_name])
|
||||
)
|
||||
if not entry_func:
|
||||
raise DSLRuntimeError(
|
||||
f"Execution engine cannot find the entry function {prefix_name}_{jit_function.function_name}"
|
||||
)
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile() as tmp_object_file:
|
||||
export_engine.dump_to_object_file(tmp_object_file.name)
|
||||
with open(tmp_object_file.name, "rb") as f:
|
||||
ret = f.read()
|
||||
return ret
|
||||
except Exception as e:
|
||||
raise DSLRuntimeError(f"Error dumping object file: {e}") from e
|
||||
|
||||
|
||||
def export_to_c(
|
||||
jit_function: Union[JitCompiledFunction, "CudaDialectJitCompiledFunction"],
|
||||
file_path: str,
|
||||
file_name: str,
|
||||
dsl: BaseDSL,
|
||||
c_header_generator: CHeaderGenerator,
|
||||
use_gpu_dialect: bool,
|
||||
):
|
||||
"""Exports the jit-compiled function to a C compatible files(header/library).
|
||||
This is used for c/cpp AOT support.
|
||||
The `file_name` will be used as the symbol prefix of the generated functions, it is guaranteed by
|
||||
the caller that the generated functions are unique. And the function will always overwrite the existing file.
|
||||
|
||||
|
||||
The c header file is generated with following components:
|
||||
1. The host launch entry function. And the structure definitions of the arguments.
|
||||
2. The device metadata load/unload functions.
|
||||
3. The cubin data array and len.
|
||||
|
||||
The library contains the binary of the underlying host launch entry function.
|
||||
|
||||
@param jit_function: The jit-compiled function from `cute.compile`.
|
||||
@param file_path: The path to the directory where the header and object files will be saved.
|
||||
@param file_name: The name of the function. This is the unique identifier name of the function to avoid symbol conflict in the generated object file.
|
||||
@param dsl: The dsl object. This is the dsl object to get the compiler provider and shared libs.
|
||||
@param c_header_generator: The c header generator. This is the c header generator to generate the c header file.
|
||||
"""
|
||||
export_module = get_export_module(jit_function.ir_module, file_name)
|
||||
# Generate the c header file
|
||||
header_file_content = c_header_generator(
|
||||
file_name,
|
||||
export_module,
|
||||
jit_function.args_spec,
|
||||
jit_function.function_name,
|
||||
jit_function.kernel_info,
|
||||
jit_function.dynamic_args,
|
||||
jit_function.dynamic_kwargs,
|
||||
dsl.name,
|
||||
)
|
||||
try:
|
||||
with open(os.path.join(file_path, file_name + ".h"), "w") as f:
|
||||
f.write(header_file_content)
|
||||
except Exception as e:
|
||||
raise DSLRuntimeError(f"Error writing header file: {e}") from e
|
||||
|
||||
# Generate the object file
|
||||
object_file_content = dump_to_object(
|
||||
file_name,
|
||||
export_module,
|
||||
jit_function,
|
||||
dsl,
|
||||
use_gpu_dialect,
|
||||
)
|
||||
try:
|
||||
with open(os.path.join(file_path, file_name + ".o"), "wb") as f:
|
||||
f.write(object_file_content)
|
||||
except Exception as e:
|
||||
raise DSLRuntimeError(f"Error writing object file: {e}") from e
|
||||
|
||||
|
||||
@@ -28,14 +28,13 @@ from .._mlir import ir
|
||||
|
||||
# Local modules imports
|
||||
from . import typing as t
|
||||
from .common import DSLRuntimeError
|
||||
from .common import DSLRuntimeError, DSLCudaRuntimeError
|
||||
from .runtime import cuda as cuda_helpers
|
||||
from .runtime.jit_arg_adapters import JitArgAdapterRegistry, is_arg_spec_constexpr
|
||||
from .typing import get_c_pointers
|
||||
from .utils.logger import log
|
||||
from .utils.timer import timer
|
||||
|
||||
|
||||
class CudaModuleAndKernel:
|
||||
"""A loaded CUDA kernel and its metadata."""
|
||||
|
||||
@@ -143,13 +142,11 @@ class ExecutionArgs:
|
||||
self.args_spec = self.filter_runtime_arg_spec(spec)
|
||||
self.original_args_spec = spec
|
||||
|
||||
def generate_execution_args(self, args, kwargs):
|
||||
def get_rectified_args(self, args, kwargs):
|
||||
"""
|
||||
This function is the prune version of `generate_mlir_function_types` which only generates execution args
|
||||
to get rid of mlir context.
|
||||
This function is used to rectify the args and kwargs to a final runtime argument list according to the args_spec.
|
||||
"""
|
||||
args_spec = self.args_spec
|
||||
|
||||
# Process positional arguments with defaults
|
||||
rectified_args = list(args)
|
||||
if args_spec.defaults and len(args) < len(args_spec.args):
|
||||
@@ -163,7 +160,9 @@ class ExecutionArgs:
|
||||
rectified_args.append(v)
|
||||
|
||||
# Process keyword arguments
|
||||
rectified_kwargs = {k: v for k, v in kwargs.items() if k not in args_spec.args}
|
||||
rectified_kwargs = collections.OrderedDict(
|
||||
(k, v) for k, v in kwargs.items() if k not in args_spec.args
|
||||
)
|
||||
if args_spec.kwonlydefaults and len(rectified_kwargs) < len(
|
||||
args_spec.kwonlyargs
|
||||
):
|
||||
@@ -182,10 +181,18 @@ class ExecutionArgs:
|
||||
"function signature kwonlyargs length": len(args_spec.kwonlyargs),
|
||||
},
|
||||
)
|
||||
return rectified_args + list(rectified_kwargs.values())
|
||||
|
||||
def generate_execution_args(self, args, kwargs):
|
||||
"""
|
||||
This function is the prune version of `generate_mlir_function_types` which only generates execution args
|
||||
to get rid of mlir context.
|
||||
"""
|
||||
args_spec = self.args_spec
|
||||
|
||||
exe_args = []
|
||||
adapted_args = []
|
||||
input_args = rectified_args + list(rectified_kwargs.values())
|
||||
input_args = self.get_rectified_args(args, kwargs)
|
||||
input_arg_names = args_spec.args + args_spec.kwonlyargs
|
||||
for arg, arg_name in zip(input_args, input_arg_names):
|
||||
# short-cut for args already converted
|
||||
@@ -243,7 +250,7 @@ class ExecutionArgs:
|
||||
runtime_kwonlydefaults = {}
|
||||
|
||||
if arg_spec.kwonlyargs:
|
||||
for kwarg in arg_spec.kwonlyargs:
|
||||
for i, kwarg in enumerate(arg_spec.kwonlyargs):
|
||||
arg_type = arg_spec.annotations.get(kwarg, None)
|
||||
|
||||
# Apply same filtering logic
|
||||
@@ -366,6 +373,8 @@ class JitModule:
|
||||
for m in set([m.cuda_module for m in self.cuda_modules]):
|
||||
cuda_helpers.unload_library(m)
|
||||
self.cuda_modules.clear()
|
||||
except Exception as e:
|
||||
pass
|
||||
finally:
|
||||
self._unloaded = True
|
||||
|
||||
@@ -382,8 +391,8 @@ class JitExecutor:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
jit_module: JitModule,
|
||||
exec_context: JitExecuteContext,
|
||||
jit_module: Union[JitModule, "CudaDialectJitModule"],
|
||||
exec_context: Optional[JitExecuteContext],
|
||||
jit_time_profiling: bool,
|
||||
):
|
||||
# JitExecutor will keep JitCompiledFunction alive so that the underlying
|
||||
@@ -393,12 +402,25 @@ class JitExecutor:
|
||||
self.exec_context = exec_context
|
||||
self.profiler = timer(enable=jit_time_profiling)
|
||||
|
||||
# Get the cuda result type from the capi function.
|
||||
# This is only set to i32 if CudaDialectJitModule is used.
|
||||
cuda_result_type = self.jit_module.capi_func.restype
|
||||
self.cuda_result = cuda_result_type() if cuda_result_type is not None else None
|
||||
|
||||
# Assume each execution args has type `c_void_p` to reduce the overhead of `ctypes.cast`.
|
||||
def _get_invoke_packed_args(self, exe_args):
|
||||
exe_args += self.exec_context.kernel_functions_ptrs
|
||||
# If expecting a cuda result, add a pointer to exe_args
|
||||
if self.cuda_result is not None:
|
||||
exe_args.append(ctypes.addressof(self.cuda_result))
|
||||
if self.exec_context is not None:
|
||||
exe_args += self.exec_context.kernel_functions_ptrs
|
||||
packed_args = (ctypes.c_void_p * len(exe_args))()
|
||||
for argNum in range(len(exe_args)):
|
||||
packed_args[argNum] = exe_args[argNum]
|
||||
arg = exe_args[argNum]
|
||||
if isinstance(arg, ctypes.c_void_p):
|
||||
packed_args[argNum] = arg
|
||||
else:
|
||||
packed_args[argNum] = ctypes.c_void_p(arg).value
|
||||
return packed_args
|
||||
|
||||
def generate_execution_args(self, *args, **kwargs):
|
||||
@@ -408,6 +430,17 @@ class JitExecutor:
|
||||
try:
|
||||
packed_args = self.profiler(self._get_invoke_packed_args)(exe_args)
|
||||
self.profiler(self.jit_module.capi_func)(packed_args)
|
||||
if self.cuda_result is not None:
|
||||
if self.cuda_result.value != 0:
|
||||
error_code = self.cuda_result.value
|
||||
error_name = cuda_helpers._cudaGetErrorEnum(
|
||||
cuda_helpers.cuda.CUresult(error_code)
|
||||
)
|
||||
raise DSLCudaRuntimeError(error_code, error_name)
|
||||
return self.cuda_result.value
|
||||
return None
|
||||
except DSLCudaRuntimeError as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise DSLRuntimeError(f"💥💥💥 Runtime Crash 💥💥💥", cause=e)
|
||||
|
||||
@@ -458,6 +491,8 @@ class JitCompiledFunction:
|
||||
kernel_info,
|
||||
jit_time_profiling,
|
||||
jit_function_artifacts,
|
||||
prefix=None,
|
||||
load_from_binary=False,
|
||||
):
|
||||
self.ir_module = ir_module
|
||||
self.engine = engine
|
||||
@@ -473,6 +508,8 @@ class JitCompiledFunction:
|
||||
or jit_function_artifacts is None
|
||||
)
|
||||
self.artifacts = jit_function_artifacts
|
||||
self.prefix = prefix
|
||||
self.load_from_binary = load_from_binary
|
||||
|
||||
# This runtime state is stored here so that we can preserve the module
|
||||
# in the compiler cache. Callers can extend the lifetime of the module
|
||||
@@ -496,6 +533,35 @@ class JitCompiledFunction:
|
||||
"""Returns the MLIR code of the JIT-compiled function."""
|
||||
return self.artifacts.MLIR if self.artifacts is not None else None
|
||||
|
||||
def _deserializer(self):
|
||||
"""Load the cuda module from the binary execution engine. This function will be injected as the
|
||||
JitCompiledFunction method which will be called by the jit executor to load the cuda module by AOT flow.
|
||||
@param self: The JitCompiledFunction object. This is the JitCompiledFunction object to load the cuda module.
|
||||
@param name: The name of the function. This is the unique identifier name of the function to avoid symbol conflict in the generated object file.
|
||||
@param execution_engine: The binary execution engine. This is the execution engine to load the cuda module.
|
||||
@param kernel_info: The kernel info. This is the kernel info to load the cuda module.
|
||||
@return: The list of cuda modules.
|
||||
"""
|
||||
cubin_suffix = "cubin"
|
||||
if self.prefix is None:
|
||||
raise DSLRuntimeError("prefix is required to be set for binary loading")
|
||||
cubin_data = self.engine.lookup("_".join([self.prefix, cubin_suffix]))
|
||||
if not cubin_data:
|
||||
raise RuntimeError(
|
||||
"Unknown function " + "_".join([self.prefix, cubin_suffix])
|
||||
)
|
||||
cubin_module = cuda_helpers.load_library_data(cubin_data)
|
||||
# load cuda module/get function pointer from module and cache
|
||||
kernel_modules = collections.OrderedDict()
|
||||
for sym, attrs in self.kernel_info.items():
|
||||
kernel = cuda_helpers.get_library_kernel(cubin_module, sym)
|
||||
if cuda_helpers.get_driver_version() >= 11080:
|
||||
attrs[
|
||||
cuda_helpers.cuda.CUfunction_attribute.CU_FUNC_ATTRIBUTE_NON_PORTABLE_CLUSTER_SIZE_ALLOWED
|
||||
] = 1
|
||||
kernel_modules[sym] = CudaModuleAndKernel(sym, cubin_module, kernel, attrs)
|
||||
return list(kernel_modules.values())
|
||||
|
||||
def to(self, device=None) -> JitExecutor:
|
||||
"""Returns an executable function bound to the given device.
|
||||
|
||||
@@ -510,9 +576,10 @@ class JitCompiledFunction:
|
||||
with self._executor_lock:
|
||||
# We need to ensure that the modules are loaded if not already
|
||||
if self.jit_module is None:
|
||||
cuda_modules = load_kernels_from_ir_module(
|
||||
self.ir_module, self.kernel_info
|
||||
)
|
||||
if self.ir_module is not None:
|
||||
cuda_modules = load_kernels_from_ir_module(
|
||||
self.ir_module, self.kernel_info
|
||||
)
|
||||
self.jit_module = JitModule(
|
||||
self.engine, self.capi_func, self.args_spec, cuda_modules
|
||||
)
|
||||
@@ -522,6 +589,11 @@ class JitCompiledFunction:
|
||||
context = self.jit_module.get_device_execute_context(device)
|
||||
return JitExecutor(self.jit_module, context, self.jit_time_profiling)
|
||||
|
||||
def set_dynamic_args(self, dynamic_args, dynamic_kwargs):
|
||||
"""Sets the dynamic argument information required for export to c code generation."""
|
||||
self.dynamic_args = dynamic_args
|
||||
self.dynamic_kwargs = dynamic_kwargs
|
||||
|
||||
def generate_execution_args(self, *args, **kwargs):
|
||||
return self.args_spec.generate_execution_args(args, kwargs)
|
||||
|
||||
|
||||
@@ -23,15 +23,9 @@ import ctypes
|
||||
import cuda.bindings.driver as cuda
|
||||
import cuda.bindings.nvrtc as nvrtc
|
||||
|
||||
# MLIR imports
|
||||
from ..._mlir import ir
|
||||
from ..._mlir.dialects import gpu
|
||||
|
||||
# Local module imports
|
||||
from ..utils.logger import log as _log
|
||||
from ..common import *
|
||||
from .jit_arg_adapters import JitArgAdapterRegistry
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Utils
|
||||
@@ -504,18 +498,18 @@ def load_library_data(cubin_data):
|
||||
"""
|
||||
Loads a CUBIN from data and returns the library.
|
||||
:param cubin_data: The binary data of the CUBIN.
|
||||
:type cubin_data: bytes
|
||||
:type cubin_data: bytes or ctypes.c_void_p
|
||||
:return: The library.
|
||||
:rtype: cuda.CUlibrary
|
||||
:raise DSLRuntimeError: If the CUDA operation fails.
|
||||
"""
|
||||
# Load module data
|
||||
_log().info(f"cuLibraryLoadData {np.char.array(cubin_data).ctypes.data}")
|
||||
if isinstance(cubin_data, bytes):
|
||||
cubin_data = np.char.array(cubin_data).ctypes.data
|
||||
_log().info(f"cuLibraryLoadData {cubin_data}")
|
||||
|
||||
library = checkCudaErrors(
|
||||
cuda.cuLibraryLoadData(
|
||||
np.char.array(cubin_data).ctypes.data, None, None, 0, None, None, 0
|
||||
)
|
||||
cuda.cuLibraryLoadData(cubin_data, None, None, 0, None, None, 0)
|
||||
)
|
||||
return library
|
||||
|
||||
@@ -803,24 +797,3 @@ def get_device_attribute(attribute, device_id: int = 0):
|
||||
"""
|
||||
device = checkCudaErrors(cuda.cuDeviceGet(device_id))
|
||||
return checkCudaErrors(cuda.cuDeviceGetAttribute(attribute, device))
|
||||
|
||||
|
||||
@JitArgAdapterRegistry.register_jit_arg_adapter(cuda.CUstream)
|
||||
class StreamAdapter:
|
||||
"""
|
||||
Convert a CUDA stream to a stream representation for JIT arg generation.
|
||||
"""
|
||||
|
||||
def __init__(self, arg):
|
||||
self._arg = arg
|
||||
self._c_pointer = self._arg.getPtr()
|
||||
|
||||
def __new_from_mlir_values__(self, values):
|
||||
assert len(values) == 1
|
||||
return values[0]
|
||||
|
||||
def __c_pointers__(self):
|
||||
return [self._c_pointer]
|
||||
|
||||
def __get_mlir_types__(self):
|
||||
return [gpu.AsyncTokenType.get()]
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
This module provides CUDA Python helper functions
|
||||
"""
|
||||
|
||||
import cuda.bindings.driver as cuda
|
||||
|
||||
# MLIR imports
|
||||
from ..._mlir import ir
|
||||
from ..._mlir.dialects import gpu
|
||||
|
||||
from .jit_arg_adapters import JitArgAdapterRegistry
|
||||
|
||||
|
||||
@JitArgAdapterRegistry.register_jit_arg_adapter(cuda.CUstream)
|
||||
class StreamAdapter:
|
||||
"""
|
||||
Convert a CUDA stream to a stream representation for JIT arg generation.
|
||||
"""
|
||||
|
||||
def __init__(self, arg):
|
||||
self._arg = arg
|
||||
self._c_pointer = self._arg.getPtr()
|
||||
|
||||
def __new_from_mlir_values__(self, values):
|
||||
assert len(values) == 1
|
||||
return values[0]
|
||||
|
||||
def __c_pointers__(self):
|
||||
return [self._c_pointer]
|
||||
|
||||
def __get_mlir_types__(self):
|
||||
return [gpu.AsyncTokenType.get()]
|
||||
@@ -0,0 +1,84 @@
|
||||
# TVM FFI DSL Bridge
|
||||
|
||||
This folder implements a binding generator that
|
||||
can be used by DSL compilers to generate functions in tvm-ffi ABI.
|
||||
|
||||
- Interface specification: we provide a spec namespace that allows
|
||||
developers to define the interface of the function, with possibly shape and type constraints.
|
||||
- Constraint checking: we will generate MLIR functions that exposes the tvm-ffi ABI convention
|
||||
checks the overall constraints and reads all related variable fields.
|
||||
- User defined calling convention: user can provide a `CallProvider` instance
|
||||
that takes `CallContext` and emit a call to internal DSL function.
|
||||
|
||||
## Basic Example: Mixed Parameters
|
||||
|
||||
```python
|
||||
|
||||
from cutlass.base_dsl.tvm_ffi_builder import (
|
||||
spec, attach_ffi_func, ExecutionEngine, NopProvider
|
||||
)
|
||||
from cutlass._mlir import ir
|
||||
import tvm_ffi
|
||||
import numpy as np
|
||||
|
||||
# Define parameters: int + tensor with symbolic shape
|
||||
n = spec.Var("n", "int32")
|
||||
params = [
|
||||
spec.Var("batch_size", "int32"), # Integer parameter
|
||||
spec.Tensor("data", [n, 128], "float32"), # Tensor with symbolic first dimension
|
||||
]
|
||||
|
||||
# Generate function
|
||||
# Function signature: process_data(batch_size: int32, data: Tensor([n, 128], float32))
|
||||
with ir.Context(), ir.Location.unknown():
|
||||
module = ir.Module.create()
|
||||
attach_ffi_func(module, "process_data", params, NopProvider())
|
||||
|
||||
# Execute
|
||||
engine = ExecutionEngine(module, opt_level=2, shared_libs=[])
|
||||
func = tvm_ffi.Function.__from_mlir_packed_safe_call__(
|
||||
engine.raw_lookup("__tvm_ffi_process_data")
|
||||
)
|
||||
|
||||
# Call function
|
||||
data = tvm_ffi.from_dlpack(np.zeros((10, 128), dtype=np.float32))
|
||||
func(42, data) # batch_size=42, data shape=[10, 128]
|
||||
```
|
||||
|
||||
## Matrix Multiplication Example
|
||||
|
||||
```python
|
||||
# Define matrix multiplication with automatic constraint validation
|
||||
n = spec.Var("n", "int32")
|
||||
m = spec.Var("m", "int32")
|
||||
k = spec.Var("k", "int32")
|
||||
|
||||
with spec.DefaultConfig(device="cpu"):
|
||||
params = [
|
||||
spec.Tensor("A", [n, k], "float32"), # A: n×k
|
||||
spec.Tensor("B", [k, m], "float32"), # B: k×m
|
||||
spec.Tensor("C", [n, m], "float32"), # C: n×m
|
||||
]
|
||||
|
||||
# Generate function
|
||||
# Function signature: matmul(A: Tensor([n, k], float32), B: Tensor([k, m], float32), C: Tensor([n, m], float32))
|
||||
with ir.Context(), ir.Location.unknown():
|
||||
module = ir.Module.create()
|
||||
attach_ffi_func(module, "matmul", params, NopProvider())
|
||||
|
||||
# Execute
|
||||
engine = ExecutionEngine(module, opt_level=2, shared_libs=[])
|
||||
func = tvm_ffi.Function.__from_mlir_packed_safe_call__(
|
||||
engine.raw_lookup("__tvm_ffi_matmul")
|
||||
)
|
||||
|
||||
# Valid call: 2×3 × 3×4 = 2×4
|
||||
A = tvm_ffi.from_dlpack(np.zeros((2, 3), dtype=np.float32))
|
||||
B = tvm_ffi.from_dlpack(np.zeros((3, 4), dtype=np.float32))
|
||||
C = tvm_ffi.from_dlpack(np.zeros((2, 4), dtype=np.float32))
|
||||
func(A, B, C) # ✅ Valid dimensions
|
||||
|
||||
# Invalid call: dimension mismatch
|
||||
A_wrong = tvm_ffi.from_dlpack(np.zeros((2, 4), dtype=np.float32)) # Wrong: 2×4
|
||||
func(A_wrong, B, C) # ❌ Error: A[1]=4 != B[0]=3
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
|
||||
#
|
||||
# Use of this software is governed by the terms and conditions of the
|
||||
# NVIDIA End User License Agreement (EULA), available at:
|
||||
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
|
||||
#
|
||||
# Any use, reproduction, disclosure, or distribution of this software
|
||||
# and related documentation outside the scope permitted by the EULA
|
||||
# is strictly prohibited.
|
||||
|
||||
"""Helper tool to build TVM-FFI functions using MLIR."""
|
||||
|
||||
from .call_provider import DynamicParamPackCallProvider, NopCallProvider
|
||||
from .spec import Param, Var
|
||||
from .tvm_ffi_builder import (
|
||||
CallContext,
|
||||
CallProvider,
|
||||
attach_ffi_func,
|
||||
rename_tvm_ffi_function,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"CallContext",
|
||||
"CallProvider",
|
||||
"DynamicParamPackCallProvider",
|
||||
"NopCallProvider",
|
||||
"Param",
|
||||
"Var",
|
||||
"attach_ffi_func",
|
||||
"rename_tvm_ffi_function",
|
||||
]
|
||||
@@ -0,0 +1,245 @@
|
||||
# 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.
|
||||
|
||||
"""Call provider that implements a specific calling convention."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Union
|
||||
|
||||
from . import spec
|
||||
from ..._mlir import ir
|
||||
from ..._mlir.dialects import llvm
|
||||
from .tvm_ffi_builder import CallContext, CallProvider, TVMFFIBuilder
|
||||
|
||||
|
||||
class NopCallProvider(CallProvider):
|
||||
"""No-op call provider for testing purposes."""
|
||||
|
||||
def __call__(self, current_block: ir.Block, context: CallContext) -> ir.Block:
|
||||
"""No-op call provider that just returns the current block."""
|
||||
return current_block
|
||||
|
||||
|
||||
class DynamicParamPackCallProvider(CallProvider, TVMFFIBuilder):
|
||||
"""Packs dynamic arguments to a struct then calls the function.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
void call(Tensor0 t0, Tensor1 t1) {
|
||||
// packed arguments
|
||||
void** packed_args[] = {&t0, &t1};
|
||||
// call target
|
||||
target_func(packed_args);
|
||||
}
|
||||
|
||||
Parameters
|
||||
----------
|
||||
target_func: str
|
||||
The name of the target function.
|
||||
|
||||
include_num_args: bool
|
||||
Whether to include the number of arguments in the packed arguments.
|
||||
|
||||
struct_call: bool
|
||||
Whether to use the struct call convention.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, target_func: str, include_num_args: bool = False, struct_call: bool = False,
|
||||
) -> None:
|
||||
import tvm_ffi
|
||||
|
||||
TVMFFIBuilder.__init__(self)
|
||||
self.target_func = target_func
|
||||
self.include_num_args = include_num_args
|
||||
self.struct_call = struct_call
|
||||
self.float4x2_dtype = tvm_ffi.dtype("float4_e2m1fnx2")
|
||||
|
||||
def get_callee_struct_for_param_tensor(
|
||||
self,
|
||||
param: spec.Tensor,
|
||||
current_block: ir.Block,
|
||||
data: ir.Value,
|
||||
shape: list[ir.Value],
|
||||
strides: list[ir.Value],
|
||||
flatten_struct: ir.Type,
|
||||
) -> ir.Type:
|
||||
"""Routine used to override tensor passsing struct conention."""
|
||||
return flatten_struct
|
||||
|
||||
def pack_param_tensor(
|
||||
self, current_block: ir.Block, context: CallContext, param: spec.Tensor
|
||||
) -> tuple[ir.Type, ir.Value]:
|
||||
"""Pack a tensor parameter to a struct."""
|
||||
map_shape_value = lambda _, value: value
|
||||
map_stride_value = lambda _, value: value
|
||||
|
||||
if param.map_tensor_dtype_f4x2_to_f4 and param.dtype == self.float4x2_dtype:
|
||||
# specially handle f4x2 to f4 tensor conversion
|
||||
# we multiply the stride by 2 for all dimensions except the one with stride=1
|
||||
# we also multiply the shape by 2 for the specific dimension with stride=1
|
||||
def find_stride_one_index() -> int:
|
||||
if param.strides is None:
|
||||
return len(param.shape) - 1
|
||||
for i, stride in enumerate(param.strides):
|
||||
if isinstance(stride, int) and stride == 1:
|
||||
return i
|
||||
raise ValueError("stride=1 index not found, needed for f4 tensor")
|
||||
stride_one_index = find_stride_one_index()
|
||||
|
||||
def map_shape_for_tensor_dtype_f4x2_to_f4(index: int, value: ir.Value) -> ir.Value:
|
||||
if index == stride_one_index:
|
||||
with ir.InsertionPoint(current_block):
|
||||
return self.mul(value, self.integer_constant(value.type, 2))
|
||||
return value
|
||||
|
||||
def map_stride_for_tensor_dtype_f4x2_to_f4(index, value: ir.Value) -> ir.Value:
|
||||
if index != stride_one_index:
|
||||
with ir.InsertionPoint(current_block):
|
||||
return self.mul(value, self.integer_constant(value.type, 2))
|
||||
return value
|
||||
|
||||
map_shape_value = map_shape_for_tensor_dtype_f4x2_to_f4
|
||||
map_stride_value = map_stride_for_tensor_dtype_f4x2_to_f4
|
||||
|
||||
data = context.matched_var_binding[param.data]
|
||||
shape = []
|
||||
strides = []
|
||||
# append all vars in shape
|
||||
for index, dim in enumerate(param.shape):
|
||||
if isinstance(dim, spec.Var):
|
||||
shape.append(map_shape_value(index, context.matched_var_binding[dim]))
|
||||
# append all vars in strides
|
||||
if param.strides is not None:
|
||||
for index, dim in enumerate(param.strides):
|
||||
if isinstance(dim, spec.Var):
|
||||
strides.append(map_stride_value(index, context.matched_var_binding[dim]))
|
||||
flatten_struct, alloca = self.pack_values_to_alloca(
|
||||
current_block, context.entry_block, [data, *shape, *strides]
|
||||
)
|
||||
callee_struct = self.get_callee_struct_for_param_tensor(
|
||||
param, current_block, data, shape, strides, flatten_struct
|
||||
)
|
||||
|
||||
return callee_struct, alloca
|
||||
|
||||
def pack_param_var(
|
||||
self, current_block: ir.Block, context: CallContext, param: spec.Var
|
||||
) -> tuple[ir.Type, ir.Value]:
|
||||
"""Pack a var parameter to a struct."""
|
||||
value: ir.Value = context.matched_var_binding[param]
|
||||
_, alloca = self.pack_values_to_alloca(
|
||||
current_block, context.entry_block, [value]
|
||||
)
|
||||
return (value.type, alloca)
|
||||
|
||||
def pack_param_shape(
|
||||
self, current_block: ir.Block, context: CallContext, param: spec.Shape
|
||||
) -> tuple[ir.Type, ir.Value]:
|
||||
"""Pack a shape parameter to a struct."""
|
||||
dynamic_args: list[ir.Value] = []
|
||||
for dim in param.shape:
|
||||
if isinstance(dim, spec.Var):
|
||||
dynamic_args.append(context.matched_var_binding[dim])
|
||||
return self.pack_values_to_alloca(
|
||||
current_block, context.entry_block, dynamic_args
|
||||
)
|
||||
|
||||
def pack_params(
|
||||
self, current_block: ir.Block, context: CallContext
|
||||
) -> list[tuple[ir.Type, ir.Value]]:
|
||||
"""Pack a parameter to a struct."""
|
||||
packed_params = []
|
||||
for param in context.params:
|
||||
if isinstance(param, spec.Tensor):
|
||||
packed_params.append(
|
||||
self.pack_param_tensor(current_block, context, param)
|
||||
)
|
||||
elif isinstance(param, spec.Var):
|
||||
packed_params.append(self.pack_param_var(current_block, context, param))
|
||||
elif isinstance(param, spec.Shape):
|
||||
packed_params.append(
|
||||
self.pack_param_shape(current_block, context, param)
|
||||
)
|
||||
elif isinstance(param, (spec.Stream, spec.EnvStream)):
|
||||
packed_params.append(
|
||||
self.pack_param_var(current_block, context, param.var)
|
||||
)
|
||||
elif isinstance(param, spec.DataPointer):
|
||||
packed_params.append(
|
||||
self.pack_param_var(current_block, context, param.var)
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(f"Unsupported parameter type: {type(param)}")
|
||||
return packed_params
|
||||
|
||||
def generate_llvm_call(
|
||||
self,
|
||||
current_block: ir.Block,
|
||||
call_operands: list[ir.Value],
|
||||
context: CallContext,
|
||||
) -> ir.Block:
|
||||
"""Generate the LLVM call operation."""
|
||||
with ir.InsertionPoint(current_block):
|
||||
llvm.call(
|
||||
result=None,
|
||||
callee=self.target_func,
|
||||
callee_operands=call_operands,
|
||||
op_bundle_sizes=[],
|
||||
op_bundle_operands=[],
|
||||
)
|
||||
return current_block
|
||||
|
||||
def load_to_call_operands(
|
||||
self,
|
||||
struct_type: Union[ir.Type, tuple[ir.Type]],
|
||||
alloca: Union[ir.Value, tuple[ir.Value]],
|
||||
) -> list[ir.Value]:
|
||||
"""Load the packed parameters to the call operands."""
|
||||
assert (isinstance(struct_type, ir.Type) and isinstance(alloca, ir.Value)) or (
|
||||
isinstance(struct_type, tuple) and isinstance(alloca, tuple)
|
||||
)
|
||||
if isinstance(struct_type, tuple):
|
||||
return [
|
||||
llvm.load(struct_type[i], alloca[i]) for i in range(len(struct_type))
|
||||
]
|
||||
return [llvm.load(struct_type, alloca)]
|
||||
|
||||
def __call__(self, current_block: ir.Block, context: CallContext) -> ir.Block:
|
||||
"""Alloca call provider that uses dynamic param pack call convention."""
|
||||
packed_params = self.pack_params(current_block, context)
|
||||
|
||||
if self.struct_call:
|
||||
# load back arguments as structs from alloca
|
||||
call_operands = []
|
||||
with ir.InsertionPoint(current_block):
|
||||
for struct_type, alloca in packed_params:
|
||||
call_operands += self.load_to_call_operands(struct_type, alloca)
|
||||
else:
|
||||
# pack the values to an alloca that we can pass as void**
|
||||
all_values = []
|
||||
for _, value in packed_params:
|
||||
if isinstance(value, tuple):
|
||||
all_values.extend(value)
|
||||
else:
|
||||
all_values.append(value)
|
||||
_, packed_args_value = self.pack_values_to_alloca(
|
||||
current_block, context.entry_block, all_values
|
||||
)
|
||||
|
||||
call_operands = [packed_args_value]
|
||||
if self.include_num_args:
|
||||
with ir.InsertionPoint(current_block):
|
||||
num_args = self.i32(len(all_values))
|
||||
call_operands.append(num_args)
|
||||
|
||||
current_block = self.generate_llvm_call(current_block, call_operands, context)
|
||||
return current_block
|
||||
@@ -0,0 +1,468 @@
|
||||
# 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.
|
||||
|
||||
"""MLIR type builder and basic operations for LLVM dialect."""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Optional
|
||||
|
||||
from ..._mlir import ir
|
||||
from ..._mlir.dialects import llvm
|
||||
|
||||
|
||||
class MLIRTypeBuilder:
|
||||
"""Builder for MLIR types and basic operations."""
|
||||
|
||||
BRANCH_WEIGHTS_LIKELY = (2000, 1)
|
||||
BRANCH_WEIGHTS_UNLIKELY = (1, 2000)
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the MLIR type builder."""
|
||||
self.i32_type = ir.IntegerType.get_signless(32)
|
||||
self.ui32_type = ir.IntegerType.get_unsigned(32)
|
||||
self.i64_type = ir.IntegerType.get_signless(64)
|
||||
self.i16_type = ir.IntegerType.get_signless(16)
|
||||
self.i8_type = ir.IntegerType.get_signless(8)
|
||||
self.i1_type = ir.IntegerType.get_signless(1)
|
||||
self.f32_type = ir.Type.parse("f32")
|
||||
self.f64_type = ir.Type.parse("f64")
|
||||
self.ptr_type = llvm.PointerType.get()
|
||||
self.gpu_ptr_type = llvm.PointerType.get(address_space=1)
|
||||
# did not find a programmatic way to get the void type
|
||||
self.void_type = ir.Type.parse("!llvm.void")
|
||||
|
||||
def ptr_type_with_address_space(
|
||||
self, address_space: Optional[int] = None
|
||||
) -> ir.Type:
|
||||
"""Get the pointer type with the given address space."""
|
||||
if address_space is None or address_space == 0:
|
||||
return self.ptr_type
|
||||
return llvm.PointerType.get(address_space=address_space)
|
||||
|
||||
def as_attr(self, tp: ir.Type) -> ir.TypeAttr:
|
||||
"""Convert the type to a type attribute."""
|
||||
return ir.TypeAttr.get(tp)
|
||||
|
||||
def int_type(self, bits: int) -> ir.Type:
|
||||
"""Get the `i<bits>` type."""
|
||||
return ir.IntegerType.get_signless(bits)
|
||||
|
||||
def uint_type(self, bits: int) -> ir.Type:
|
||||
"""Get the `ui<bits>` type."""
|
||||
return ir.IntegerType.get_unsigned(bits)
|
||||
|
||||
def struct_type(
|
||||
self,
|
||||
*,
|
||||
name: Optional[str] = None,
|
||||
fields: Sequence[ir.Type] = (),
|
||||
packed: bool = False,
|
||||
) -> ir.Type:
|
||||
"""Get or create a struct type.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : Optional[str]
|
||||
The name of the struct type. If not provided, a `literal` struct type is created,
|
||||
which is identified by its fields only.
|
||||
fields : Sequence[ir.Type]
|
||||
The fields of the struct type.
|
||||
packed : bool
|
||||
Whether to create a packed struct type. If `True`, there is no padding between fields.
|
||||
Otherwise, there might be padding
|
||||
between fields to ensure alignment.
|
||||
|
||||
See Also
|
||||
--------
|
||||
https://mlir.llvm.org/docs/Dialects/LLVM/#structure-types
|
||||
|
||||
"""
|
||||
if name is None:
|
||||
return llvm.StructType.get_literal(fields, packed=packed)
|
||||
else:
|
||||
return llvm.StructType.new_identified(name, fields, packed=packed)
|
||||
|
||||
def identified_struct_type(self, name: str) -> ir.Type:
|
||||
"""Get a previously created identified struct type by its name."""
|
||||
return llvm.StructType.get_identified(name)
|
||||
|
||||
def func_type(self, *, params: Sequence[ir.Type] = (), ret: ir.Type) -> ir.Type:
|
||||
"""Get a function type.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
params : Sequence[ir.Type]
|
||||
The parameters of the function type.
|
||||
ret : ir.Type
|
||||
The return type of the function type.
|
||||
|
||||
See Also
|
||||
--------
|
||||
https://mlir.llvm.org/docs/Dialects/LLVM/#function-types
|
||||
|
||||
"""
|
||||
return ir.Type.parse(
|
||||
"!llvm.func<{} ({})>".format(str(ret), ", ".join(map(str, params)))
|
||||
) # did not find a programmatic way to get the function type
|
||||
|
||||
def global_dtor_entry_type(self) -> ir.Type:
|
||||
"""Get the type of the global destructor entry.
|
||||
|
||||
See Also:
|
||||
- https://llvm.org/docs/LangRef.html#the-llvm-global-dtors-global-variable
|
||||
- https://mlir.llvm.org/docs/Dialects/LLVM/#llvmmlirglobal_dtors-llvmglobaldtorsop
|
||||
"""
|
||||
return self.struct_type(fields=[self.i32_type, self.ptr_type, self.ptr_type])
|
||||
|
||||
|
||||
class MLIRBuilder(MLIRTypeBuilder):
|
||||
"""A builder for MLIR related types and operations.
|
||||
|
||||
Convention:
|
||||
all statement-generation methods expect we are inside a insertion point of the current_block.
|
||||
If the statement terminates the current block, it will assign the new block to the current_block
|
||||
but not set the insersion point.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the MLIR builder."""
|
||||
super().__init__()
|
||||
self.module: Optional[ir.Module] = None
|
||||
self.const_str_table: dict[str, ir.Value] = {}
|
||||
self.get_element_extra_kwargs: dict[str, Any] = {}
|
||||
|
||||
# create constants
|
||||
def integer_constant(self, tp: ir.Type, value: int) -> ir.Value:
|
||||
"""Create an integer constant with the given type and value."""
|
||||
return llvm.ConstantOp(tp, ir.IntegerAttr.get(tp, value)).res
|
||||
|
||||
def i32(self, value: int) -> ir.Value:
|
||||
"""Create an i32 constant with the given value."""
|
||||
return self.integer_constant(self.i32_type, value)
|
||||
|
||||
def ui32(self, value: int) -> ir.Value:
|
||||
"""Create a ui32 constant with the given value."""
|
||||
return self.integer_constant(self.ui32_type, value)
|
||||
|
||||
def i1(self, value: int) -> ir.Value:
|
||||
"""Create an i1 constant with the given value."""
|
||||
return self.integer_constant(self.i1_type, value)
|
||||
|
||||
def i8(self, value: int) -> ir.Value:
|
||||
"""Create an i8 constant with the given value."""
|
||||
return self.integer_constant(self.i8_type, value)
|
||||
|
||||
def i16(self, value: int) -> ir.Value:
|
||||
"""Create an i16 constant with the given value."""
|
||||
return self.integer_constant(self.i16_type, value)
|
||||
|
||||
def i64(self, value: int) -> ir.Value:
|
||||
"""Create an i64 constant with the given value."""
|
||||
return self.integer_constant(self.i64_type, value)
|
||||
|
||||
def mul(self, lhs: ir.Value, rhs: ir.Value) -> ir.Value:
|
||||
"""Create a multiplication operation between two values."""
|
||||
return llvm.mul(lhs, rhs, overflow_flags=llvm.IntegerOverflowFlags.none)
|
||||
|
||||
# expressions
|
||||
def not_equal(self, lhs: ir.Value, rhs: ir.Value) -> ir.Value:
|
||||
"""Create a not-equal comparison between two values."""
|
||||
return llvm.icmp(llvm.ICmpPredicate.ne, lhs, rhs)
|
||||
|
||||
def equal(self, lhs: ir.Value, rhs: ir.Value) -> ir.Value:
|
||||
"""Create an equal comparison between two values."""
|
||||
return llvm.icmp(llvm.ICmpPredicate.eq, lhs, rhs)
|
||||
|
||||
def or_(self, lhs: ir.Value, rhs: ir.Value) -> ir.Value:
|
||||
"""Create a logical OR operation between two values."""
|
||||
return llvm.or_(lhs, rhs)
|
||||
|
||||
def and_(self, lhs: ir.Value, rhs: ir.Value) -> ir.Value:
|
||||
"""Create a logical AND operation between two values."""
|
||||
return llvm.and_(lhs, rhs)
|
||||
|
||||
def not_(self, value: ir.Value) -> ir.Value:
|
||||
"""Create a logical NOT operation."""
|
||||
# Ensure we're working with i1 type for boolean operations
|
||||
if value.type != self.i1_type:
|
||||
value = llvm.trunc(res=self.i1_type, arg=value)
|
||||
return llvm.xor(value, self.i1(1))
|
||||
|
||||
def i64_divisible_const(self, value: ir.Value, align_const: int) -> ir.Value:
|
||||
"""Check if i64 value is divisible by align_const.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : ir.Value
|
||||
The i64 value to check.
|
||||
align_const : int
|
||||
The alignment constant to check divisibility against.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ir.Value
|
||||
A boolean value (i1) indicating if value is divisible by align_const.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Uses fast path (bitwise AND) when align_const is a power of two,
|
||||
otherwise uses modulo operation.
|
||||
"""
|
||||
# Check if align_const is a power of two
|
||||
is_power_of_two = (align_const > 0) and (align_const & (align_const - 1)) == 0
|
||||
|
||||
if is_power_of_two:
|
||||
# Fast path: use bitwise AND
|
||||
# value is divisible by align_const iff (value & (align_const - 1)) == 0
|
||||
mask = self.i64(align_const - 1)
|
||||
masked = llvm.and_(value, mask)
|
||||
return self.equal(masked, self.i64(0))
|
||||
else:
|
||||
# Slow path: use modulo operation
|
||||
align_val = self.i64(align_const)
|
||||
remainder = llvm.urem(value, align_val)
|
||||
return self.equal(remainder, self.i64(0))
|
||||
|
||||
def br(self, target_block: ir.Block, *, args: Optional[list[ir.Value]] = None) -> None:
|
||||
"""Create an unconditional branch.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
target_block : ir.Block
|
||||
The target block to branch to.
|
||||
args : list[ir.Value], optional
|
||||
The values to pass as arguments to the target block. If None,
|
||||
no arguments are passed. The target block must have the same
|
||||
number of arguments as values in this list.
|
||||
|
||||
"""
|
||||
if args is None:
|
||||
llvm.br(dest=target_block, dest_operands=[])
|
||||
else:
|
||||
llvm.br(dest_operands=args, dest=target_block)
|
||||
|
||||
def address_of(self, name: str, tp: ir.Type) -> ir.Value:
|
||||
"""Get the address of a global symbol."""
|
||||
return llvm.AddressOfOp(tp, name).result
|
||||
|
||||
def getelementptr(
|
||||
self, ptr: ir.Value, constant_indices: Sequence[int], elem_type: ir.Type
|
||||
) -> ir.Value:
|
||||
"""Create a getelementptr operation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ptr : ir.Value
|
||||
The pointer to the element.
|
||||
indices : Sequence[ir.Value]
|
||||
The indices to the element.
|
||||
elem_type : ir.Type
|
||||
The type of the element.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ir.Value
|
||||
The resulting element pointer.
|
||||
"""
|
||||
try:
|
||||
return llvm.getelementptr(
|
||||
self.ptr_type,
|
||||
ptr,
|
||||
[],
|
||||
raw_constant_indices=ir.DenseI32ArrayAttr.get(constant_indices),
|
||||
elem_type=elem_type,
|
||||
**self.get_element_extra_kwargs,
|
||||
)
|
||||
except Exception:
|
||||
# compatibility with different LLVM versions
|
||||
self.get_element_extra_kwargs = {"no_wrap_flags": []}
|
||||
return llvm.getelementptr(
|
||||
self.ptr_type,
|
||||
ptr,
|
||||
[],
|
||||
raw_constant_indices=ir.DenseI32ArrayAttr.get(constant_indices),
|
||||
elem_type=elem_type,
|
||||
**self.get_element_extra_kwargs,
|
||||
)
|
||||
|
||||
def return_(self, ret: Optional[ir.Value] = None) -> None:
|
||||
"""Create a return statement."""
|
||||
llvm.return_(arg=ret)
|
||||
|
||||
def cond_br(
|
||||
self,
|
||||
cond: ir.Value,
|
||||
true_block: ir.Block,
|
||||
false_block: ir.Block,
|
||||
branch_weights=None
|
||||
) -> None:
|
||||
"""Create a conditional branch.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cond : ir.Value
|
||||
The condition value (i1 type).
|
||||
true_block : ir.Block
|
||||
The block to branch to if condition is true.
|
||||
false_block : ir.Block
|
||||
The block to branch to if condition is false.
|
||||
branch_weights : Optional[tuple[int, int]]
|
||||
Optional branch weights [true_weight, false_weight] for optimization hints.
|
||||
Higher values indicate higher probability. For example, (99, 1) indicates
|
||||
the true branch is much more likely than the false branch.
|
||||
"""
|
||||
if branch_weights is not None:
|
||||
# Branch weights should be a tuple/list of two integers [true_weight, false_weight]
|
||||
if len(branch_weights) != 2:
|
||||
raise ValueError("branch_weights must have exactly 2 elements")
|
||||
llvm.cond_br(
|
||||
cond,
|
||||
true_dest_operands=[],
|
||||
false_dest_operands=[],
|
||||
true_dest=true_block,
|
||||
false_dest=false_block,
|
||||
branch_weights=ir.DenseI32ArrayAttr.get(list(branch_weights))
|
||||
)
|
||||
else:
|
||||
llvm.cond_br(
|
||||
cond,
|
||||
true_dest_operands=[],
|
||||
false_dest_operands=[],
|
||||
true_dest=true_block,
|
||||
false_dest=false_block,
|
||||
)
|
||||
|
||||
def define_global_string(self, content: str) -> str:
|
||||
"""Define a global string symbol with the given content using standard MLIR APIs."""
|
||||
if content in self.const_str_table:
|
||||
return self.const_str_table[content]
|
||||
symbol = f"__tvm_ffi__str_{len(self.const_str_table)}"
|
||||
|
||||
# The string_attr.value gives us the original string, but we need to escape it for
|
||||
# MLIR parsing. Let's use a simple approach: escape quotes and backslashes
|
||||
escaped_content = content.replace("\\", "\\\\").replace('"', '\\"')
|
||||
|
||||
# Parse the MLIR string with proper escaping using the standard API
|
||||
module_body = self.module.body # type: ignore[union-attr]
|
||||
with ir.InsertionPoint(module_body):
|
||||
parsed_op = ir.Operation.parse(
|
||||
f'llvm.mlir.global private constant @{symbol}("{escaped_content}\\00")'
|
||||
)
|
||||
module_body.append(parsed_op)
|
||||
self.const_str_table[content] = symbol
|
||||
return symbol
|
||||
|
||||
# function
|
||||
def function(
|
||||
self,
|
||||
name: str,
|
||||
params_type: Sequence[ir.Type],
|
||||
ret_type: ir.Type,
|
||||
internal: bool = False,
|
||||
) -> tuple[list[ir.Value], ir.Block]:
|
||||
"""Create a function with the given signature."""
|
||||
func_op = llvm.func(
|
||||
name,
|
||||
function_type=self.as_attr(
|
||||
self.func_type(ret=ret_type, params=params_type)
|
||||
),
|
||||
)
|
||||
if internal:
|
||||
func_op.attributes["llvm.linkage"] = ir.StringAttr.get("private")
|
||||
else:
|
||||
func_op.attributes["llvm.emit_c_interface"] = ir.UnitAttr.get()
|
||||
|
||||
params = []
|
||||
func_body: Any = func_op.body
|
||||
if func_body is not None:
|
||||
entry_block = ir.Block.create_at_start(func_body)
|
||||
else:
|
||||
raise RuntimeError("Function body is None")
|
||||
for param_type in params_type:
|
||||
params.append(entry_block.add_argument(param_type, ir.Location.unknown()))
|
||||
return params, entry_block
|
||||
|
||||
def declare_extern_func(
|
||||
self, name: str, params: Sequence[ir.Type], ret: ir.Type
|
||||
) -> None:
|
||||
"""Define extern function."""
|
||||
func_type = self.func_type(params=params, ret=ret)
|
||||
func_op = llvm.func(
|
||||
name,
|
||||
function_type=self.as_attr(func_type),
|
||||
)
|
||||
func_op.attributes["llvm.linkage"] = ir.StringAttr.get("external")
|
||||
|
||||
def pack_values_to_alloca(
|
||||
self,
|
||||
current_block: ir.Block,
|
||||
entry_block: ir.Block,
|
||||
values: Sequence[ir.Value],
|
||||
) -> tuple[ir.Type, ir.Value]:
|
||||
"""Pack values to an alloca that lays out in the order of the values.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
current_block : ir.Block
|
||||
The current block.
|
||||
entry_block : ir.Block
|
||||
The entry block to create an alloca for the struct.
|
||||
values : Sequence[ir.Value]
|
||||
The values to pack.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[ir.Type, ir.Value]
|
||||
The struct type and the alloca.
|
||||
"""
|
||||
with ir.InsertionPoint(entry_block.operations[0]):
|
||||
# declare the struct type
|
||||
struct_type = self.struct_type(fields=[value.type for value in values])
|
||||
alloca = llvm.alloca(
|
||||
res=self.ptr_type,
|
||||
elem_type=struct_type,
|
||||
array_size=self.i32(1),
|
||||
)
|
||||
|
||||
with ir.InsertionPoint(current_block):
|
||||
for index, value in enumerate(values):
|
||||
# Get pointer to the field at the given index
|
||||
field_ptr = self.getelementptr(
|
||||
alloca,
|
||||
[0, index],
|
||||
struct_type,
|
||||
)
|
||||
# Store the value into the field
|
||||
llvm.store(value, field_ptr)
|
||||
return (struct_type, alloca)
|
||||
|
||||
def find_operations_in_module(
|
||||
self, module: ir.Module, name: str
|
||||
) -> list[ir.Operation]:
|
||||
"""Find operations in the module by the operation name."""
|
||||
operations = []
|
||||
for op in module.body: # type: ignore[union-attr]
|
||||
if op.name == name:
|
||||
operations.append(op)
|
||||
return operations
|
||||
|
||||
def find_func_in_module(
|
||||
self, module: ir.Module, name: str
|
||||
) -> Optional[ir.Operation]:
|
||||
"""Find a function in the module."""
|
||||
for op in module.body: # type: ignore[union-attr]
|
||||
if op.name == "llvm.func":
|
||||
# Get the function name from the sym_name attribute
|
||||
if "sym_name" in op.attributes:
|
||||
func_name = str(op.attributes["sym_name"]).strip('"')
|
||||
if func_name == name:
|
||||
return op
|
||||
return None
|
||||
@@ -0,0 +1,431 @@
|
||||
# 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.
|
||||
|
||||
"""Kernel specification classes for TVM-FFI function parameters."""
|
||||
from abc import ABC
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Optional, Union
|
||||
|
||||
try:
|
||||
import tvm_ffi
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
|
||||
class DefaultConfig:
|
||||
"""Default configuration with context manager support."""
|
||||
|
||||
_current: Optional["DefaultConfig"] = None
|
||||
_old_current: Optional["DefaultConfig"] = None
|
||||
device_type: str
|
||||
|
||||
def __init__(self, *, device_type: Optional[str] = None) -> None:
|
||||
"""Initialize a default configuration.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
device_type : str, optional
|
||||
The device type (e.g., "cpu", "cuda", "metal"). If None, copies from current config.
|
||||
"""
|
||||
if device_type is None:
|
||||
device_type = DefaultConfig.current().device_type # type: ignore[union-attr]
|
||||
self.device_type = device_type
|
||||
|
||||
def __enter__(self) -> "DefaultConfig":
|
||||
"""Enter the context manager."""
|
||||
self._old_current = DefaultConfig._current
|
||||
DefaultConfig._current = self
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: Optional[type],
|
||||
exc_val: Optional[BaseException],
|
||||
exc_tb: Optional[object],
|
||||
) -> None:
|
||||
"""Exit the context manager."""
|
||||
DefaultConfig._current = self._old_current
|
||||
|
||||
@classmethod
|
||||
def current(cls) -> Optional["DefaultConfig"]:
|
||||
"""Get the current default configuration.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[DefaultConfig]
|
||||
The current default configuration.
|
||||
"""
|
||||
return cls._current
|
||||
|
||||
@classmethod
|
||||
def _set_init_default_config(cls) -> None:
|
||||
"""Set the initial default configuration."""
|
||||
current = cls.__new__(cls)
|
||||
current.device_type = "cuda"
|
||||
cls._current = current
|
||||
|
||||
|
||||
# Initialize the default config to cuda
|
||||
DefaultConfig._set_init_default_config()
|
||||
|
||||
|
||||
class Param(ABC):
|
||||
"""Base class for all parameters."""
|
||||
|
||||
|
||||
class Var(Param):
|
||||
"""variables: pointer, integer, floating-point, boolean, etc.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The parameter name.
|
||||
dtype : str | tvm_ffi.dtype
|
||||
The data type of the parameter.
|
||||
divisibility: Optional[int]
|
||||
The divisibility of the parameter, by default None.
|
||||
|
||||
"""
|
||||
|
||||
name: str
|
||||
dtype: "tvm_ffi.dtype"
|
||||
divisibility: Optional[int]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
dtype: Union[str, "tvm_ffi.dtype"],
|
||||
*,
|
||||
divisibility: Optional[int] = None,
|
||||
) -> None:
|
||||
"""Initialize a Var parameter.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The parameter name.
|
||||
dtype : str | tvm_ffi.dtype
|
||||
The data type of the parameter.
|
||||
|
||||
"""
|
||||
self.name = name
|
||||
self.dtype = tvm_ffi.dtype(dtype)
|
||||
self.divisibility = divisibility
|
||||
|
||||
|
||||
class Shape(Param):
|
||||
"""Shape parameter.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The parameter name.
|
||||
shape: list[int | Var]
|
||||
The shape of the parameter.
|
||||
|
||||
"""
|
||||
|
||||
name: str
|
||||
shape: list[Union[int, Var]]
|
||||
|
||||
def __init__(self, name: str, shape: list[Union[int, Var]]) -> None:
|
||||
"""Initialize a Shape parameter.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The parameter name.
|
||||
shape : list[int | Var]
|
||||
The shape of the parameter.
|
||||
|
||||
"""
|
||||
self.name = name
|
||||
self.shape = shape
|
||||
|
||||
|
||||
class Tensor(Param):
|
||||
"""Tensor parameter.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The parameter name.
|
||||
dtype : str | tvm_ffi.dtype
|
||||
The data type of the parameter.
|
||||
shape : Sequence[int | Var]
|
||||
The shape of the parameter.
|
||||
device_type : int
|
||||
The device type of the parameter.
|
||||
device_id : Var
|
||||
The device id of the parameter.
|
||||
strides : Optional[Sequence[Var]], optional
|
||||
The strides of the parameter, by default None.
|
||||
"""
|
||||
|
||||
name: str
|
||||
shape: list[Union[int, Var]]
|
||||
dtype: "tvm_ffi.dtype"
|
||||
strides: Optional[list[Var]]
|
||||
dlpack_device_type: int
|
||||
device_id: Var
|
||||
map_tensor_dtype_f4x2_to_f4: bool
|
||||
data_alignment: Optional[int]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
shape: Sequence[Union[int, Var]],
|
||||
dtype: Union[str, "tvm_ffi.dtype"],
|
||||
*,
|
||||
device_type: Optional[str] = None,
|
||||
strides: Optional[Sequence[Var]] = None,
|
||||
map_tensor_dtype_f4x2_to_f4: bool = False,
|
||||
data_alignment: Optional[int] = None,
|
||||
) -> None:
|
||||
"""Initialize a Tensor parameter.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The parameter name.
|
||||
device_type : str
|
||||
The device type of the parameter.
|
||||
shape : Sequence[int | Var]
|
||||
The shape of the parameter.
|
||||
dtype : str | tvm_ffi.dtype
|
||||
The data type of the parameter.
|
||||
strides : Optional[Sequence[Var]], optional
|
||||
The strides of the parameter, by default None.
|
||||
map_tensor_dtype_f4x2_to_f4: bool
|
||||
Whether to map tensor dtype float4x2 to float4 for internal use.
|
||||
data_alignment: Optional[int], optional
|
||||
The data alignment of the parameter, by default None.
|
||||
"""
|
||||
self.name = name
|
||||
self.data = Var(name + ".data", tvm_ffi.dtype("handle"))
|
||||
self.shape: list[Union[int, Var]] = list(shape)
|
||||
self.dtype = tvm_ffi.dtype(dtype)
|
||||
self.strides: Optional[list[Var]] = list(strides) if strides is not None else None
|
||||
self.data_alignment = data_alignment
|
||||
|
||||
# Use default device type if none specified
|
||||
if device_type is None:
|
||||
device_type = DefaultConfig.current().device_type # type: ignore[union-attr]
|
||||
|
||||
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"))
|
||||
self.map_tensor_dtype_f4x2_to_f4 = map_tensor_dtype_f4x2_to_f4
|
||||
|
||||
|
||||
class Stream(Param):
|
||||
"""Stream parameter."""
|
||||
|
||||
name: str
|
||||
var: Var
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
"""Initialize a Stream parameter.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The parameter name.
|
||||
"""
|
||||
self.name = name
|
||||
self.var = Var(name, tvm_ffi.dtype("handle"))
|
||||
|
||||
|
||||
class EnvStream(Param):
|
||||
"""EnvStream parameter.
|
||||
|
||||
Note
|
||||
----
|
||||
This parameter spec indicates that we expect
|
||||
to call the function with TVMFFIEnvGetStream to get the stream
|
||||
and it is not part of the FFI function signature.
|
||||
"""
|
||||
|
||||
name: str
|
||||
var: Var
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
"""Initialize a EnvStream parameter.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The parameter name.
|
||||
"""
|
||||
self.name = name
|
||||
self.var = Var(name, tvm_ffi.dtype("handle"))
|
||||
|
||||
|
||||
class DataPointer(Param):
|
||||
"""Data pointer parameter.
|
||||
|
||||
The main difference between DataPointer and Var with handle type
|
||||
is that DataPointer can contain address space information.
|
||||
We also allow passing in int as data pointer, so it can
|
||||
conveniently support torch.Tensor.data_ptr() as input.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The parameter name.
|
||||
"""
|
||||
|
||||
name: str
|
||||
var: Var
|
||||
address_space: Optional[int]
|
||||
|
||||
def __init__(self, name: str, address_space: Optional[int] = None) -> None:
|
||||
"""Initialize a DataPointer parameter.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The parameter name.
|
||||
"""
|
||||
self.name = name
|
||||
self.var = Var(name, tvm_ffi.dtype("handle"))
|
||||
self.address_space = address_space
|
||||
|
||||
|
||||
def signature(name: str, params: list[Param]) -> str:
|
||||
"""Generate a function signature string from name and parameters.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The function name.
|
||||
params : list[Param]
|
||||
List of parameter objects (Var or Tensor).
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The formatted function signature.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If an unknown parameter type is encountered.
|
||||
|
||||
"""
|
||||
param_strs = []
|
||||
|
||||
for param in params:
|
||||
if isinstance(param, Var):
|
||||
param_str = f"{param.name}: {param.dtype}"
|
||||
elif isinstance(param, Tensor):
|
||||
# Format tensor shape
|
||||
shape_strs = []
|
||||
for dim in param.shape:
|
||||
if isinstance(dim, Var):
|
||||
shape_strs.append(dim.name)
|
||||
else:
|
||||
shape_strs.append(str(dim))
|
||||
shape_str = "[" + ", ".join(shape_strs) + "]"
|
||||
param_str = f"{param.name}: Tensor({shape_str}, {param.dtype})"
|
||||
elif isinstance(param, Shape):
|
||||
# Format shape parameter
|
||||
shape_strs = []
|
||||
for dim in param.shape:
|
||||
if isinstance(dim, Var):
|
||||
shape_strs.append(dim.name)
|
||||
else:
|
||||
shape_strs.append(str(dim))
|
||||
shape_str = "[" + ", ".join(shape_strs) + "]"
|
||||
param_str = f"{param.name}: Shape({shape_str})"
|
||||
elif isinstance(param, Stream):
|
||||
param_str = f"{param.name}: Stream"
|
||||
elif isinstance(param, DataPointer):
|
||||
param_str = f"{param.name}: DataPointer"
|
||||
elif isinstance(param, EnvStream):
|
||||
# env stream is not part of the FFI function signature
|
||||
# continue to skip append
|
||||
continue
|
||||
else:
|
||||
raise TypeError(f"Unsupported parameter type: {type(param)}")
|
||||
|
||||
param_strs.append(param_str)
|
||||
|
||||
return f"{name}({', '.join(param_strs)})"
|
||||
|
||||
|
||||
def create_map_tensor_dtype_f4x2_to_f4_spec(f4_tensor_spec: Tensor) -> Tensor:
|
||||
"""
|
||||
Create a new Tensor spec that can be translated to f4 via f4x2->f4 conversion.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
f4_tensor_spec : Tensor
|
||||
The original Tensor spec that declares a f4 tensor.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tensor
|
||||
The new Tensor spec that can be translated to f4 via f4x2->f4 conversion.
|
||||
"""
|
||||
if f4_tensor_spec.dtype != tvm_ffi.dtype("float4_e2m1fn"):
|
||||
raise ValueError("f4_tensor_spec must be a float4 tensor")
|
||||
|
||||
def find_stride_one_index() -> int:
|
||||
if f4_tensor_spec.strides is None:
|
||||
return len(f4_tensor_spec.shape) - 1
|
||||
for i, stride in enumerate(f4_tensor_spec.strides):
|
||||
if isinstance(stride, int) and stride == 1:
|
||||
return i
|
||||
raise ValueError("Cannot find dimension with stride=1")
|
||||
stride_one_index = find_stride_one_index()
|
||||
|
||||
def divisibility_divide_by_2(value: Var) -> Optional[int]:
|
||||
if value.divisibility is not None:
|
||||
if value.divisibility % 2 != 0:
|
||||
raise ValueError(f"Dimension with stride=1 must be divisible by 2")
|
||||
return value.divisibility // 2
|
||||
return None
|
||||
|
||||
def map_shape(index: int, value: Union[int, Var]) -> Union[int, Var]:
|
||||
if index == stride_one_index:
|
||||
if isinstance(value, int):
|
||||
if value % 2 != 0:
|
||||
raise ValueError(f"Dimension {index} with stride=1 must be even")
|
||||
return value // 2
|
||||
# create a new var with the same name and dtype
|
||||
return Var(value.name, value.dtype, divisibility=divisibility_divide_by_2(value))
|
||||
return value
|
||||
|
||||
def map_stride(index: int, value: Union[int, Var]) -> Union[int, Var]:
|
||||
if index != stride_one_index:
|
||||
if isinstance(value, int):
|
||||
if value % 2 != 0:
|
||||
raise ValueError(f"Dimension {index} with stride != 1 must be even")
|
||||
return value // 2
|
||||
# create a new var with the same name and dtype
|
||||
return Var(value.name, value.dtype, divisibility=divisibility_divide_by_2(value))
|
||||
return value
|
||||
|
||||
new_shape = [map_shape(i, x) for i, x in enumerate(f4_tensor_spec.shape)]
|
||||
if f4_tensor_spec.strides is not None:
|
||||
new_strides = [map_stride(i, x) for i, x in enumerate(f4_tensor_spec.strides)]
|
||||
else:
|
||||
new_strides = None
|
||||
|
||||
return Tensor(
|
||||
f4_tensor_spec.name,
|
||||
new_shape,
|
||||
dtype=tvm_ffi.dtype("float4_e2m1fnx2"),
|
||||
strides=new_strides,
|
||||
map_tensor_dtype_f4x2_to_f4=True,
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -239,6 +239,19 @@ def get_c_pointers(obj):
|
||||
return []
|
||||
|
||||
|
||||
def arg_compatible_with_tvm_ffi(arg):
|
||||
"""
|
||||
Given the `arg`, check if it is a compatible argument for TVM FFI
|
||||
"""
|
||||
import tvm_ffi
|
||||
|
||||
return (
|
||||
hasattr(arg, "__tvm_ffi_object__")
|
||||
or isinstance(arg, (int, float, bool))
|
||||
or isinstance(arg, tvm_ffi.Shape)
|
||||
)
|
||||
|
||||
|
||||
def get_mlir_types(obj):
|
||||
"""
|
||||
Given the `obj`, recursively go through it to extract all contained MLIR types
|
||||
@@ -1416,6 +1429,9 @@ class Integer(Numeric, metaclass=IntegerMeta, mlir_type=T.i32, is_abstract=True)
|
||||
def __rxor__(self, other, *, loc=None, ip=None):
|
||||
return self.__xor__(other, loc=loc, ip=ip)
|
||||
|
||||
def __tvm_ffi_int__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class Float(Numeric, metaclass=FloatMeta, mlir_type=T.f32, is_abstract=True):
|
||||
"""A class representing floating-point values.
|
||||
@@ -1496,6 +1512,9 @@ class Float(Numeric, metaclass=FloatMeta, mlir_type=T.f32, is_abstract=True):
|
||||
else:
|
||||
raise DSLRuntimeError(f"{x} to Float conversion is not supported")
|
||||
|
||||
def __tvm_ffi_float__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class Boolean(Integer, metaclass=IntegerMeta, width=1, signed=True, mlir_type=T.bool):
|
||||
"""Boolean type representation in the DSL.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
# is strictly prohibited.
|
||||
|
||||
# Use the auto-generated enum AddressSpace
|
||||
from cutlass._mlir.dialects.cute import AddressSpace
|
||||
from cutlass._mlir.dialects.cute import AddressSpace, CacheEvictionPriority
|
||||
|
||||
# Explicitly import types that might be directly used by other modules.
|
||||
# This is a fix for using Sphinx to generate documentation
|
||||
@@ -29,6 +29,7 @@ from .typing import (
|
||||
ComposedLayout,
|
||||
Pointer,
|
||||
Tensor,
|
||||
SymInt,
|
||||
)
|
||||
|
||||
# Import everything else
|
||||
@@ -114,6 +115,9 @@ from .core import (
|
||||
ScaledBasis,
|
||||
get_divisibility,
|
||||
Ratio,
|
||||
# FastDivmod operations
|
||||
FastDivmodDivisor,
|
||||
fast_divmod_create_divisor,
|
||||
)
|
||||
|
||||
from .tuple import (
|
||||
@@ -175,6 +179,8 @@ from .atom import (
|
||||
from .algorithm import gemm, copy, basic_copy, basic_copy_if, autovec_copy, prefetch
|
||||
|
||||
from . import arch
|
||||
|
||||
from . import export
|
||||
from . import nvgpu
|
||||
from . import testing
|
||||
from . import runtime
|
||||
@@ -200,10 +206,16 @@ KeepPTX = _dsl.KeepPTX
|
||||
GPUArch = _dsl.GPUArch
|
||||
LinkLibraries = _dsl.LinkLibraries
|
||||
|
||||
# attach the TVM FFI ABI interface postprocessor to the DSL
|
||||
from . import _tvm_ffi_args_spec_converter
|
||||
|
||||
_tvm_ffi_args_spec_converter.attach_args_spec_converter()
|
||||
|
||||
# Explicitly export all symbols for documentation generation
|
||||
__all__ = [
|
||||
# Core types
|
||||
"AddressSpace",
|
||||
"CacheEvictionPriority",
|
||||
"Tensor",
|
||||
"Layout",
|
||||
"ComposedLayout",
|
||||
@@ -219,6 +231,7 @@ __all__ = [
|
||||
"ThrCopy",
|
||||
"TensorSSA",
|
||||
"ReductionOp",
|
||||
"SymInt",
|
||||
# Basic utility functions
|
||||
"assume",
|
||||
"is_integer",
|
||||
@@ -344,8 +357,12 @@ __all__ = [
|
||||
"repeat_like",
|
||||
# User defined struct
|
||||
"struct",
|
||||
# FastDivmod operations
|
||||
"FastDivmodDivisor",
|
||||
"fast_divmod_create_divisor",
|
||||
# Modules
|
||||
"arch",
|
||||
"export",
|
||||
"nvgpu",
|
||||
"testing",
|
||||
"runtime",
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
# 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.base_dsl.tvm_ffi_builder import spec
|
||||
from cutlass.base_dsl.jit_executor import ExecutionArgs
|
||||
from cutlass.base_dsl.common import DSLRuntimeError
|
||||
from cutlass.cutlass_dsl import is_cute_algebra_type
|
||||
from cutlass._mlir.dialects import cute as _cute_ir
|
||||
from .runtime import _FakeStream
|
||||
from .typing import Tensor, Pointer, SymInt
|
||||
from .typing import (
|
||||
Numeric,
|
||||
Boolean,
|
||||
Int4,
|
||||
Int8,
|
||||
Uint8,
|
||||
Int16,
|
||||
Uint16,
|
||||
Int32,
|
||||
Uint32,
|
||||
Int64,
|
||||
Uint64,
|
||||
Float16,
|
||||
BFloat16,
|
||||
Float32,
|
||||
TFloat32,
|
||||
Float64,
|
||||
Float6E2M3FN,
|
||||
Float6E3M2FN,
|
||||
Float8E5M2,
|
||||
Float8E4M3FN,
|
||||
Float8E8M0FNU,
|
||||
Float4E2M1FN,
|
||||
)
|
||||
import cuda.bindings.driver as cuda
|
||||
|
||||
from typing import List, Dict, Any, Optional
|
||||
import inspect
|
||||
|
||||
NumericToTVMFFIDtype = {
|
||||
Boolean: "bool",
|
||||
Int4: "int4",
|
||||
Int8: "int8",
|
||||
Uint8: "uint8",
|
||||
Int16: "int16",
|
||||
Uint16: "uint16",
|
||||
Int32: "int32",
|
||||
Uint32: "uint32",
|
||||
Int64: "int64",
|
||||
Uint64: "uint64",
|
||||
Float16: "float16",
|
||||
BFloat16: "bfloat16",
|
||||
Float32: "float32",
|
||||
TFloat32: "float32",
|
||||
Float64: "float64",
|
||||
Float8E5M2: "float8_e5m2",
|
||||
Float8E4M3FN: "float8_e4m3fn",
|
||||
Float8E8M0FNU: "float8_e8m0fnu",
|
||||
Float4E2M1FN: "float4_e2m1fn",
|
||||
Float6E2M3FN: "float6_e2m3fn",
|
||||
Float6E3M2FN: "float6_e3m2fn",
|
||||
}
|
||||
|
||||
AcceptableNumericTypesForScalar = [
|
||||
Boolean,
|
||||
Int8,
|
||||
Uint8,
|
||||
Int16,
|
||||
Uint16,
|
||||
Int32,
|
||||
Uint32,
|
||||
Int64,
|
||||
Uint64,
|
||||
Float32,
|
||||
Float64,
|
||||
]
|
||||
|
||||
|
||||
def _get_llvm_address_space_from_memspace(
|
||||
memspace: _cute_ir.AddressSpace,
|
||||
) -> Optional[int]:
|
||||
if memspace == _cute_ir.AddressSpace.gmem:
|
||||
return 1
|
||||
return None
|
||||
|
||||
|
||||
class SymIntId:
|
||||
def __init__(self, sym_int: SymInt):
|
||||
self.sym_int = sym_int
|
||||
|
||||
def __hash__(self):
|
||||
return id(self.sym_int)
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
return self.sym_int is other.sym_int
|
||||
|
||||
|
||||
def _tvm_ffi_args_spec_converter(
|
||||
function_name: str,
|
||||
args_spec: inspect.FullArgSpec,
|
||||
dynamic_args: List[Any],
|
||||
dynamic_kwargs: Dict[str, Any],
|
||||
):
|
||||
"""Convert cute algebra args to tvm ffi spec params.
|
||||
|
||||
This function converts the cute arguments specs to tvm ffi spec params.
|
||||
"""
|
||||
exec_args = ExecutionArgs(args_spec, function_name)
|
||||
rectified_args = exec_args.get_rectified_args(dynamic_args, dynamic_kwargs)
|
||||
arg_names = exec_args.args_spec.args + exec_args.args_spec.kwonlyargs
|
||||
|
||||
params = []
|
||||
num_dyn_shape_vars = 0
|
||||
num_dyn_stride_vars = 0
|
||||
sym_int_id_mapping = {}
|
||||
|
||||
|
||||
def alloc_shape_name():
|
||||
nonlocal num_dyn_shape_vars
|
||||
name = f"n{num_dyn_shape_vars}"
|
||||
num_dyn_shape_vars += 1
|
||||
return name
|
||||
|
||||
def alloc_stride_name():
|
||||
nonlocal num_dyn_stride_vars
|
||||
name = f"s{num_dyn_stride_vars}"
|
||||
num_dyn_stride_vars += 1
|
||||
return name
|
||||
|
||||
def alloc_or_reuse_symint_var(value, name_alloc_func):
|
||||
nonlocal sym_int_id_mapping
|
||||
sym_int_id = SymIntId(value)
|
||||
if sym_int_id in sym_int_id_mapping:
|
||||
return sym_int_id_mapping[sym_int_id]
|
||||
name = name_alloc_func()
|
||||
if value.width == 32:
|
||||
dtype = NumericToTVMFFIDtype[Int32]
|
||||
else:
|
||||
dtype = NumericToTVMFFIDtype[Int64]
|
||||
var = spec.Var(name, dtype, divisibility=value.divisibility)
|
||||
sym_int_id_mapping[sym_int_id] = var
|
||||
return var
|
||||
|
||||
for arg, arg_name in zip(rectified_args, arg_names):
|
||||
arg_type = args_spec.annotations.get(arg_name, None)
|
||||
if isinstance(arg, Numeric) and arg.dtype in AcceptableNumericTypesForScalar:
|
||||
params.append(spec.Var(arg_name, NumericToTVMFFIDtype[arg.dtype]))
|
||||
elif is_cute_algebra_type(arg_type):
|
||||
shape = []
|
||||
for i in range(len(arg)):
|
||||
if isinstance(arg[i], int):
|
||||
shape.append(arg[i])
|
||||
elif isinstance(arg[i], SymInt):
|
||||
shape.append(alloc_or_reuse_symint_var(arg[i], alloc_shape_name))
|
||||
else:
|
||||
shape.append(spec.Var(alloc_shape_name(), NumericToTVMFFIDtype[arg[i].dtype]))
|
||||
params.append(spec.Shape(arg_name, shape))
|
||||
elif isinstance(arg, Tensor):
|
||||
shapes = []
|
||||
for i, dyn_mask in enumerate(arg.dynamic_shapes_mask):
|
||||
if not dyn_mask:
|
||||
shapes.append(arg.shape[i])
|
||||
elif isinstance(arg.shape[i], SymInt):
|
||||
shapes.append(alloc_or_reuse_symint_var(arg.shape[i], alloc_shape_name))
|
||||
else:
|
||||
shapes.append(spec.Var(alloc_shape_name(), NumericToTVMFFIDtype[Int32]))
|
||||
strides = []
|
||||
|
||||
for i, dyn_mask in enumerate(arg.dynamic_strides_mask):
|
||||
if not dyn_mask:
|
||||
strides.append(arg.stride[i])
|
||||
elif isinstance(arg.stride[i], SymInt):
|
||||
strides.append(alloc_or_reuse_symint_var(arg.stride[i], alloc_stride_name))
|
||||
else:
|
||||
if hasattr(arg, "_use_32bit_stride") and arg._use_32bit_stride:
|
||||
dtype = NumericToTVMFFIDtype[Int32]
|
||||
else:
|
||||
dtype = NumericToTVMFFIDtype[Int64]
|
||||
strides.append(spec.Var(alloc_stride_name(), dtype))
|
||||
|
||||
tvm_ffi_cute_tensor = spec.Tensor(
|
||||
arg_name,
|
||||
shapes,
|
||||
NumericToTVMFFIDtype[arg.element_type],
|
||||
strides=strides,
|
||||
data_alignment=arg._assumed_align,
|
||||
)
|
||||
if arg.element_type == Float4E2M1FN:
|
||||
tvm_ffi_cute_tensor = spec.create_map_tensor_dtype_f4x2_to_f4_spec(
|
||||
tvm_ffi_cute_tensor
|
||||
)
|
||||
params.append(tvm_ffi_cute_tensor)
|
||||
elif isinstance(arg, Pointer):
|
||||
address_space = None
|
||||
if hasattr(arg, "memspace"):
|
||||
address_space = _get_llvm_address_space_from_memspace(arg.memspace)
|
||||
params.append(spec.DataPointer(arg_name, address_space=address_space))
|
||||
elif isinstance(arg, _FakeStream):
|
||||
if arg.use_tvm_ffi_env_stream:
|
||||
params.append(spec.EnvStream(arg_name))
|
||||
else:
|
||||
params.append(spec.Stream(arg_name))
|
||||
elif isinstance(arg, cuda.CUstream):
|
||||
params.append(spec.Stream(arg_name))
|
||||
else:
|
||||
raise DSLRuntimeError(f"Unsupported argument type: {type(arg)}")
|
||||
# The following code can obtain signature of the function
|
||||
# that maybe useful for future debugging and usecases.
|
||||
# signature = spec.signature(function_name, params)
|
||||
return params
|
||||
|
||||
|
||||
def attach_args_spec_converter():
|
||||
"""Attach TVM FFI ABI interface postprocessor to the DSL."""
|
||||
from .. import cutlass_dsl as _dsl
|
||||
|
||||
_dsl.CuTeDSL._get_dsl()._tvm_ffi_args_spec_converter = _tvm_ffi_args_spec_converter
|
||||
@@ -221,8 +221,8 @@ def autovec_copy(src: Tensor, dst: Tensor, *, loc=None, ip=None) -> None:
|
||||
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)
|
||||
# Finally, we put a cap at 256b
|
||||
num_bits_per_copy = math.gcd(upper_bound, 256)
|
||||
|
||||
if (num_common_elements > 1) and (num_bits_per_copy % 8 == 0):
|
||||
num_common_elements = num_bits_per_copy // src.element_type.width
|
||||
@@ -355,7 +355,7 @@ def copy(
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
cute.copy(tma_atom, src, dst, tma_bar_ptr=mbar_ptr, mcast_mask=mask)
|
||||
cute.copy(tma_atom, src, dst, tma_bar_ptr=mbar_ptr, mcast_mask=mask, cache_policy=policy)
|
||||
|
||||
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
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
# is strictly prohibited.
|
||||
|
||||
|
||||
from cutlass.cutlass_dsl import dsl_user_op
|
||||
from cutlass.base_dsl.arch import Arch
|
||||
from cutlass.base_dsl.common import DSLRuntimeError
|
||||
from cutlass.cutlass_dsl import CuTeDSL, dsl_user_op
|
||||
from cutlass._mlir import ir
|
||||
from cutlass._mlir.dialects import builtin, arith, llvm, vector
|
||||
|
||||
@@ -34,15 +36,15 @@ from ..typing import (
|
||||
Int8,
|
||||
Int32,
|
||||
Float16,
|
||||
Float32,
|
||||
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.
|
||||
Fast conversion from int8 to bfloat16. It converts a vector of int8 to a vector of bfloat16.
|
||||
|
||||
:param vec_i8: The input vector of int8.
|
||||
:type vec_i8: 1D vector of int8
|
||||
@@ -51,6 +53,9 @@ def cvt_i8_bf16_intrinsic(vec_i8, length, *, loc=None, ip=None):
|
||||
:return: The output 1D vector of bfloat16 with the same length as the input vector.
|
||||
:rtype: 1D vector of bfloat16
|
||||
"""
|
||||
arch = CuTeDSL._get_dsl().get_arch_enum()
|
||||
if not arch in cvt_i8_bf16_intrinsic.supported_archs:
|
||||
raise DSLRuntimeError(f"cvt_i8_bf16_intrinsic is not supported on {arch}")
|
||||
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)
|
||||
@@ -116,7 +121,7 @@ def cvt_i8_bf16_intrinsic(vec_i8, length, *, loc=None, ip=None):
|
||||
@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.
|
||||
Fast conversion from int4 to bfloat16. It converts a vector of int4 to a vector of bfloat16.
|
||||
|
||||
:param vec_i4: The input vector of int4.
|
||||
:type vec_i4: 1D vector of int4
|
||||
@@ -125,6 +130,9 @@ def cvt_i4_bf16_intrinsic(vec_i4, length, *, loc=None, ip=None):
|
||||
:return: The output 1D vector of bfloat16 with the same length as the input vector.
|
||||
:rtype: 1D vector of bfloat16
|
||||
"""
|
||||
arch = CuTeDSL._get_dsl().get_arch_enum()
|
||||
if not arch in cvt_i4_bf16_intrinsic.supported_archs:
|
||||
raise DSLRuntimeError(f"cvt_i4_bf16_intrinsic is not supported on {arch}")
|
||||
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)
|
||||
@@ -261,3 +269,18 @@ def cvt_f4e2m1_f16_intrinsic(vec_f4e2m1, length, *, loc=None, ip=None):
|
||||
ip=ip,
|
||||
)
|
||||
return vec_dst
|
||||
|
||||
|
||||
# Expose supported architectures via the intrinsic symbol
|
||||
cvt_i8_bf16_intrinsic.supported_archs = (
|
||||
*Arch.AmpereArchs(),
|
||||
*Arch.AdaArchs(),
|
||||
*Arch.HopperArchs(),
|
||||
*Arch.BlackwellArchs(),
|
||||
)
|
||||
cvt_i4_bf16_intrinsic.supported_archs = (
|
||||
Arch.sm_100a,
|
||||
Arch.sm_110a,
|
||||
Arch.sm_120a,
|
||||
Arch.sm_121a,
|
||||
)
|
||||
|
||||
@@ -10,14 +10,19 @@
|
||||
# is strictly prohibited.
|
||||
|
||||
from functools import partial
|
||||
from typing import Optional, Tuple, Union, Callable
|
||||
from typing import Optional, Tuple, Union, Callable, TYPE_CHECKING
|
||||
from typing_extensions import deprecated
|
||||
|
||||
from cutlass.cutlass_dsl import T, dsl_user_op
|
||||
from cutlass.cutlass_dsl import T, dsl_user_op, cutlass_arith
|
||||
|
||||
import cutlass.cutlass_dsl as cutlass_dsl
|
||||
|
||||
from cutlass._mlir import ir
|
||||
from cutlass._mlir.dialects import arith, llvm, nvvm, vector
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cutlass.tensor import TensorSSA
|
||||
|
||||
# Forward nvvm enums
|
||||
from cutlass._mlir.dialects.nvvm import (
|
||||
ProxyKind,
|
||||
@@ -27,6 +32,8 @@ from cutlass._mlir.dialects.nvvm import (
|
||||
RoundingModeKind,
|
||||
)
|
||||
|
||||
from ..core import size
|
||||
|
||||
from ..typing import (
|
||||
Int,
|
||||
Boolean,
|
||||
@@ -184,7 +191,7 @@ def block_idx_in_cluster(*, loc=None, ip=None) -> Int32:
|
||||
|
||||
@dsl_user_op
|
||||
def shuffle_sync_op(
|
||||
value: Numeric,
|
||||
value: Union[Numeric, "TensorSSA"],
|
||||
offset: Int,
|
||||
mask: Int = FULL_MASK,
|
||||
mask_and_clamp: Int = WARP_SIZE - 1,
|
||||
@@ -192,12 +199,12 @@ def shuffle_sync_op(
|
||||
*,
|
||||
loc=None,
|
||||
ip=None,
|
||||
) -> Numeric:
|
||||
) -> Union[Numeric, "TensorSSA"]:
|
||||
"""
|
||||
Shuffles a value within the threads of a warp.
|
||||
|
||||
:param value: The value to shuffle
|
||||
:type value: Numeric
|
||||
:type value: Numeric or TensorSSA
|
||||
:param mask: A mask describing the threads participating in this operation
|
||||
:type mask: Int
|
||||
:param offset: A source lane or a source lane offset depending on kind
|
||||
@@ -211,12 +218,37 @@ def shuffle_sync_op(
|
||||
:return: The shuffled value
|
||||
:rtype: Numeric
|
||||
"""
|
||||
from ..tensor import TensorSSA
|
||||
|
||||
if isinstance(value, TensorSSA):
|
||||
bit_width = value.dtype.width * size(value.shape)
|
||||
if bit_width == 32:
|
||||
i32_val = llvm.bitcast(
|
||||
T.i32(), value.ir_value(loc=loc, ip=ip), loc=loc, ip=ip
|
||||
)
|
||||
i32_res = nvvm.shfl_sync(
|
||||
T.i32(),
|
||||
Int32(mask).ir_value(loc=loc, ip=ip),
|
||||
i32_val,
|
||||
Int32(offset).ir_value(loc=loc, ip=ip),
|
||||
Int32(mask_and_clamp).ir_value(loc=loc, ip=ip),
|
||||
kind,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
result_vec = llvm.bitcast(value.type, i32_res, loc=loc, ip=ip)
|
||||
return TensorSSA(result_vec, value.shape, value.dtype)
|
||||
else:
|
||||
raise ValueError(f"shuffle_sync only supports 32 bit, but got {value.type}")
|
||||
|
||||
if not isinstance(value, Numeric):
|
||||
value = as_numeric(value)
|
||||
|
||||
if value.width > 64:
|
||||
raise ValueError("shuffle_sync only supports values up to 64 bits")
|
||||
|
||||
orig_type = type(value)
|
||||
|
||||
if value.width < 32:
|
||||
if value.dtype.is_float:
|
||||
value = value.to(Float32)
|
||||
@@ -331,6 +363,7 @@ def warp_reduction(
|
||||
:rtype: cutlass.Numeric
|
||||
"""
|
||||
offset = threads_in_group // 2
|
||||
|
||||
while offset > 0:
|
||||
val = op(
|
||||
val,
|
||||
@@ -343,7 +376,8 @@ def warp_reduction(
|
||||
|
||||
|
||||
warp_reduction_max = partial(
|
||||
warp_reduction, op=lambda x, y: fmax(x, y) if isinstance(x, Float32) else max(x, y)
|
||||
warp_reduction,
|
||||
op=lambda x, y: fmax(x, y) if isinstance(x, Float32) else cutlass_dsl.max(x, y),
|
||||
)
|
||||
warp_reduction_sum = partial(warp_reduction, op=lambda x, y: x + y)
|
||||
|
||||
@@ -796,11 +830,11 @@ add_packed_f32x2 = partial(
|
||||
calc_packed_f32x2_op, src_c=None, calc_func=nvvm.add_packed_f32x2
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def fmax(
|
||||
a: Union[float, Float32], b: Union[float, Float32], *, loc=None, ip=None
|
||||
) -> Float32:
|
||||
|
||||
return Float32(
|
||||
nvvm.fmax(
|
||||
T.f32(),
|
||||
@@ -811,7 +845,6 @@ def fmax(
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def rcp_approx(a: Union[float, Float32], *, loc=None, ip=None):
|
||||
return Float32(
|
||||
|
||||
@@ -99,6 +99,9 @@ class Trait(ABC):
|
||||
def unpack(self, *, loc=None, ip=None, **kwargs) -> ir.Value:
|
||||
return self.value
|
||||
|
||||
def with_(self, *, loc=None, ip=None, **kwargs) -> "Trait":
|
||||
return self.__class__(self.unpack(loc=loc, ip=ip, **kwargs))
|
||||
|
||||
|
||||
def make_atom(ty, values=None, *, loc=None, ip=None):
|
||||
"""
|
||||
@@ -185,6 +188,21 @@ class Atom(ABC):
|
||||
"""
|
||||
return self._trait.get(field, loc=loc, ip=ip)
|
||||
|
||||
def with_(self, *, loc=None, ip=None, **kwargs) -> "Atom":
|
||||
"""
|
||||
Returns a new Atom with the new Operation and Trait with the given runtime state. The runtime state
|
||||
is provided as keyword arguments and it is Atom-specific.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
tiled_copy = cute.make_tiled_copy(tma_copy_op)
|
||||
new_tiled_copy = tiled_copy.with_(tma_bar_ptr=tma_bar_ptr, cache_policy=cute.CacheEvictionPriority.EVICT_LAST)
|
||||
|
||||
The ``with_`` method provides a way to the user to modify such runtime state or create an executable Atom
|
||||
(e.g. an Executable TMA Load Atom).
|
||||
"""
|
||||
return self.__class__(self.op, self._trait.with_(loc=loc, ip=ip, **kwargs))
|
||||
|
||||
def _unpack(self, *, loc=None, ip=None, **kwargs) -> ir.Value:
|
||||
return self._trait.unpack(loc=loc, ip=ip, **kwargs)
|
||||
|
||||
@@ -215,23 +233,28 @@ class MmaAtom(Atom):
|
||||
#
|
||||
|
||||
@property
|
||||
def thr_id(self) -> Layout:
|
||||
return static(self._trait.value.type.thr_id)
|
||||
@dsl_user_op
|
||||
def thr_id(self, *, loc=None, ip=None) -> Layout:
|
||||
return static(self._trait.value.type.thr_id, loc=loc, ip=ip)
|
||||
|
||||
@property
|
||||
def shape_mnk(self) -> Shape:
|
||||
return _unpack_x_tuple(self._trait.value.type.shape_mnk)
|
||||
@dsl_user_op
|
||||
def shape_mnk(self, *, loc=None, ip=None) -> Shape:
|
||||
return _unpack_x_tuple(self._trait.value.type.shape_mnk, loc=loc, ip=ip)
|
||||
|
||||
@property
|
||||
def tv_layout_A(self) -> Layout:
|
||||
return static(self._trait.value.type.layout_a_tv)
|
||||
@dsl_user_op
|
||||
def tv_layout_A(self, *, loc=None, ip=None) -> Layout:
|
||||
return static(self._trait.value.type.layout_a_tv, loc=loc, ip=ip)
|
||||
|
||||
@property
|
||||
def tv_layout_B(self) -> Layout:
|
||||
return static(self._trait.value.type.layout_b_tv)
|
||||
@dsl_user_op
|
||||
def tv_layout_B(self, *, loc=None, ip=None) -> Layout:
|
||||
return static(self._trait.value.type.layout_b_tv, loc=loc, ip=ip)
|
||||
|
||||
@property
|
||||
def tv_layout_C(self) -> Layout:
|
||||
@dsl_user_op
|
||||
def tv_layout_C(self, *, loc=None, ip=None) -> Layout:
|
||||
return static(self._trait.value.type.layout_c_tv)
|
||||
|
||||
#
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
# is strictly prohibited.
|
||||
|
||||
from functools import partial, reduce
|
||||
import inspect
|
||||
from inspect import isclass
|
||||
from typing import Any, Dict, List, Optional, Tuple, Type, Union, overload
|
||||
|
||||
@@ -304,9 +305,10 @@ class IntValue(cutlass_arith.ArithValue):
|
||||
def __str__(self):
|
||||
if self.divisibility == 1:
|
||||
return "?"
|
||||
else:
|
||||
elif self.type.width == 32:
|
||||
return f"?{{div={self.divisibility}}}"
|
||||
|
||||
else:
|
||||
return f"?{{i{self.type.width} div={self.divisibility}}}"
|
||||
def __repr__(self):
|
||||
parent_name = cutlass_arith.ArithValue.__name__
|
||||
return super().__str__().replace(parent_name, IntValue.__name__)
|
||||
@@ -913,8 +915,8 @@ class _Layout(Layout):
|
||||
|
||||
layout = make_layout((4, 8), stride=(8, 1))
|
||||
|
||||
# map linear index back to coordinate: 5 -> (1, 1)
|
||||
coord = get_hier_coord(5, layout)
|
||||
# map linear index back to coordinate: 9 -> (1, 1)
|
||||
coord = layout.get_hier_coord(9)
|
||||
"""
|
||||
idx_val = Int32(idx).ir_value(loc=loc, ip=ip)
|
||||
crd = _cute_ir.get_hier_coord(idx_val, self, loc=loc, ip=ip)
|
||||
@@ -3116,12 +3118,6 @@ def make_ptr(
|
||||
if isinstance(value, ir.Value) and llvm.PointerType.isinstance(value.type):
|
||||
value = llvm.ptrtoint(T.i64(), value)
|
||||
|
||||
if not isinstance(mem_space, AddressSpace):
|
||||
raise TypeError(f"expects mem_space to be an AddressSpace, but got {mem_space}")
|
||||
|
||||
if isinstance(value, ir.Value) and llvm.PointerType.isinstance(value.type):
|
||||
value = llvm.ptrtoint(T.i64(), value)
|
||||
|
||||
if not is_integer(value):
|
||||
raise TypeError(f"expects integer value, but got {type(value)}")
|
||||
value = Int32(value) if mem_space == AddressSpace.tmem else Int64(value)
|
||||
@@ -3611,9 +3607,9 @@ def make_layout_image_mask(
|
||||
sliced_lay, offset = slice_and_offset(slicer, lay, loc=loc, ip=ip)
|
||||
# Given that we replace only one mode with _, the rank of the slice should be 1
|
||||
assert rank(sliced_lay) == 1
|
||||
|
||||
if not is_static(sliced_lay):
|
||||
raise ValueError("make_layout_image_mask requires the layout to be static")
|
||||
assert is_static(
|
||||
sliced_lay
|
||||
), "make_layout_image_mask requires the layout to be static"
|
||||
|
||||
# Create the mask of the image
|
||||
mcast_mask = Int16(0)
|
||||
@@ -4125,3 +4121,166 @@ class ThrMma(_ThrMma):
|
||||
@deprecated("cute.core.ThrCopy is deprecated, use cute.ThrCopy instead")
|
||||
class ThrCopy(_ThrCopy):
|
||||
pass
|
||||
|
||||
|
||||
#
|
||||
# FastDivmod operations for optimized division and modulus
|
||||
#
|
||||
class FastDivmodDivisor:
|
||||
"""
|
||||
First-class FastDivmod divisor with operator overloading support.
|
||||
|
||||
This class wraps a FastDivmod divisor and enables natural Python operator syntax:
|
||||
quotient, remainder = divmod(dividend, divisor)
|
||||
quotient = dividend // divisor
|
||||
remainder = dividend % divisor
|
||||
|
||||
:ivar _divisor: The FastDivmod divisor MLIR value
|
||||
"""
|
||||
|
||||
@dsl_user_op
|
||||
def __init__(
|
||||
self,
|
||||
divisor: Integer,
|
||||
is_power_of_2: bool = None,
|
||||
*,
|
||||
loc=None,
|
||||
ip=None,
|
||||
):
|
||||
"""
|
||||
Create a FastDivmod divisor for optimized division operations.
|
||||
|
||||
:param divisor: The divisor value (should be runtime-dynamic value)
|
||||
:param is_power_of_2: Whether divisor is known to be a power of 2.
|
||||
Defaults to False.
|
||||
"""
|
||||
# Convert divisor to ir.Value for MLIR operation
|
||||
if isinstance(divisor, ir.Value):
|
||||
divisor_val = divisor
|
||||
else:
|
||||
divisor_val = Int32(divisor).ir_value()
|
||||
|
||||
# Use user-provided flag or default to False
|
||||
# Power-of-2 optimization should be handled by compiler passes at IR level
|
||||
if is_power_of_2 is None:
|
||||
is_power_of_2 = False
|
||||
|
||||
# Create FastDivmod divisor
|
||||
fast_divmod_divisor_type = _cute_ir.FastDivmodDivisorType.get(32, is_power_of_2)
|
||||
self._divisor = _cute_ir.fast_divmod_create_divisor(
|
||||
fast_divmod_divisor_type, divisor_val, loc=loc, ip=ip
|
||||
)
|
||||
|
||||
@dsl_user_op
|
||||
def __rdivmod__(
|
||||
self, dividend: Integer, *, loc=None, ip=None
|
||||
) -> Tuple[Integer, Integer]:
|
||||
"""
|
||||
Overload for: divmod(dividend, self)
|
||||
Returns (quotient, remainder).
|
||||
|
||||
:param dividend: The dividend value
|
||||
:param loc: Source location for MLIR
|
||||
:param ip: Insertion point for MLIR
|
||||
:return: Tuple of (quotient, remainder)
|
||||
"""
|
||||
# Convert dividend to ir.Value for MLIR operation
|
||||
if isinstance(dividend, ir.Value):
|
||||
dividend_val = dividend
|
||||
else:
|
||||
dividend_val = Int32(dividend).ir_value()
|
||||
|
||||
quotient_type = dividend_val.type
|
||||
remainder_type = dividend_val.type
|
||||
|
||||
results = _cute_ir.fast_divmod_compute(
|
||||
quotient_type,
|
||||
remainder_type,
|
||||
dividend_val,
|
||||
self._divisor,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
return (IntValue(results[0]), IntValue(results[1]))
|
||||
|
||||
@dsl_user_op
|
||||
def __rfloordiv__(self, dividend: Integer, *, loc=None, ip=None) -> Integer:
|
||||
"""
|
||||
Overload for: dividend // self
|
||||
Returns quotient only.
|
||||
|
||||
:param dividend: The dividend value
|
||||
:param loc: Source location for MLIR
|
||||
:param ip: Insertion point for MLIR
|
||||
:return: The quotient
|
||||
"""
|
||||
quotient, _ = self.__rdivmod__(dividend, loc=loc, ip=ip)
|
||||
return quotient
|
||||
|
||||
@dsl_user_op
|
||||
def __rmod__(self, dividend: Integer, *, loc=None, ip=None) -> Integer:
|
||||
"""
|
||||
Overload for: dividend % self
|
||||
Returns remainder only.
|
||||
|
||||
:param dividend: The dividend value
|
||||
:param loc: Source location for MLIR
|
||||
:param ip: Insertion point for MLIR
|
||||
:return: The remainder
|
||||
"""
|
||||
_, remainder = self.__rdivmod__(dividend, loc=loc, ip=ip)
|
||||
return remainder
|
||||
|
||||
def __extract_mlir_values__(self):
|
||||
"""Extract MLIR values for Host->Device transfer."""
|
||||
return [self._divisor]
|
||||
|
||||
def __new_from_mlir_values__(self, values):
|
||||
"""Reconstruct FastDivmodDivisor from MLIR values."""
|
||||
new_obj = object.__new__(FastDivmodDivisor)
|
||||
new_obj._divisor = values[0]
|
||||
return new_obj
|
||||
def __repr__(self):
|
||||
return f"FastDivmodDivisor({self._divisor.type})"
|
||||
|
||||
|
||||
# Set explicit signature for Sphinx documentation to avoid issues with @dsl_user_op decorator
|
||||
FastDivmodDivisor.__init__.__signature__ = inspect.Signature(
|
||||
[
|
||||
inspect.Parameter("self", inspect.Parameter.POSITIONAL_OR_KEYWORD),
|
||||
inspect.Parameter(
|
||||
"divisor",
|
||||
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
||||
annotation=Integer,
|
||||
),
|
||||
inspect.Parameter(
|
||||
"is_power_of_2",
|
||||
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
||||
default=None,
|
||||
annotation=bool,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def fast_divmod_create_divisor(
|
||||
divisor: Integer, *, loc=None, ip=None
|
||||
) -> FastDivmodDivisor:
|
||||
"""Create a FastDivmod divisor for optimized division operations.
|
||||
|
||||
This function creates a FastDivmod divisor that precomputes auxiliary values
|
||||
to enable fast division and modulus operations without using division instructions.
|
||||
|
||||
The returned FastDivmodDivisor object supports natural Python operator syntax:
|
||||
divisor = fast_divmod_create_divisor(batch_size)
|
||||
quotient, remainder = divmod(linear_idx, divisor)
|
||||
quotient = linear_idx // divisor
|
||||
remainder = linear_idx % divisor
|
||||
|
||||
:param divisor: The divisor value (should be runtime-dynamic value)
|
||||
:type divisor: Integer
|
||||
:return: FastDivmodDivisor object with operator overloading support
|
||||
:rtype: FastDivmodDivisor
|
||||
"""
|
||||
return FastDivmodDivisor(divisor, loc=loc, ip=ip)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
|
||||
#
|
||||
# Use of this software is governed by the terms and conditions of the
|
||||
# NVIDIA End User License Agreement (EULA), available at:
|
||||
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
|
||||
#
|
||||
# Any use, reproduction, disclosure, or distribution of this software
|
||||
# and related documentation outside the scope permitted by the EULA
|
||||
# is strictly prohibited.
|
||||
|
||||
from .c_header_generator import CuteCHeaderGenerator
|
||||
|
||||
from ...base_dsl.export import (
|
||||
get_export_module,
|
||||
dump_to_object as _dump_to_object,
|
||||
export_to_c as _export_to_c,
|
||||
)
|
||||
from ...cutlass_dsl import CuTeDSL
|
||||
from functools import partial as _partial
|
||||
from ...cutlass_dsl.cuda_jit_executor import CudaDialectJitCompiledFunction
|
||||
|
||||
dump_to_object = _partial(
|
||||
_dump_to_object,
|
||||
dsl=CuTeDSL._get_dsl(),
|
||||
)
|
||||
export_to_c = _partial(
|
||||
_export_to_c,
|
||||
dsl=CuTeDSL._get_dsl(),
|
||||
c_header_generator=CuteCHeaderGenerator(),
|
||||
use_gpu_dialect=False,
|
||||
)
|
||||
__all__ = [
|
||||
"CuteCHeaderGenerator",
|
||||
"get_export_module",
|
||||
"dump_to_object",
|
||||
"export_to_c",
|
||||
]
|
||||
@@ -0,0 +1,241 @@
|
||||
# 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.cute.typing import NumericMeta, Integer
|
||||
from cutlass.base_dsl.export import CHeaderGenerator
|
||||
from cutlass.base_dsl.dsl import is_dynamic_expression
|
||||
from cutlass.base_dsl.common import DSLRuntimeError
|
||||
from cutlass.base_dsl.jit_executor import ExecutionArgs
|
||||
from cutlass.cutlass_dsl.cutlass import is_cute_algebra_type
|
||||
from ..runtime import Tensor, Pointer
|
||||
|
||||
from typing import List, Any, Dict
|
||||
from inspect import isclass
|
||||
import cuda.bindings.driver as cuda
|
||||
|
||||
# =============================================================================
|
||||
# Cute DSL C Header Generator for c/cpp AOT support
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class CuteCHeaderGenerator(CHeaderGenerator):
|
||||
"""This class provides a Export C Header Generator for cute c/cpp AOT support."""
|
||||
|
||||
def _get_cute_algebra_type(self, arg_type: Any, arg: Any) -> str:
|
||||
"""Judge if the dynamic elements of the cute algebra type are same(Int32 or Int64).
|
||||
If so, generate the corresponding C type. Otherwise, refuse to generate the argument
|
||||
in C header and raise an error.
|
||||
"""
|
||||
if not isinstance(arg, (list, tuple, Integer)):
|
||||
raise DSLRuntimeError(
|
||||
f"Unsupported argument for c function argument generation: {arg} with type {arg_type}"
|
||||
)
|
||||
if isinstance(arg, Integer):
|
||||
return self.numeric_to_c_type[arg.dtype]
|
||||
dyn_type = None
|
||||
for elem in arg:
|
||||
if not isinstance(elem, (Integer, int)):
|
||||
raise DSLRuntimeError(
|
||||
f"Unsupported argument for c function argument generation: {arg} with type {arg_type}"
|
||||
)
|
||||
if isinstance(elem, int):
|
||||
continue
|
||||
if dyn_type is not None:
|
||||
if elem.dtype != dyn_type:
|
||||
raise DSLRuntimeError(
|
||||
f"Expects all dynamic elements of the cute algebra type to be of the same type, but got {elem.dtype} and {dyn_type}"
|
||||
)
|
||||
else:
|
||||
dyn_type = elem.dtype
|
||||
if dyn_type is None:
|
||||
return "int32_t "
|
||||
return self.numeric_to_c_type[dyn_type]
|
||||
|
||||
def _generate_binary_declaration(self, symbol_prefix: str):
|
||||
"""
|
||||
Generate the binary of the compiled function.
|
||||
"""
|
||||
return ""
|
||||
|
||||
def _generate_kernel_metadata(
|
||||
self, symbol_prefix: str, kernel_info: Dict[str, List], dsl_name: str
|
||||
):
|
||||
"""
|
||||
Generate the kernel metadata for the compiled function.
|
||||
"""
|
||||
kernel_metadata_struct = f"""
|
||||
typedef struct {{
|
||||
CUlibrary module;
|
||||
}} {symbol_prefix}_Kernel_Metadata_t;
|
||||
"""
|
||||
kernel_metadata_load = f"""
|
||||
#ifdef __cplusplus
|
||||
extern "C" {{
|
||||
#endif
|
||||
void _mlir_{symbol_prefix}_cuda_init(void **);
|
||||
void _mlir_{symbol_prefix}_cuda_load(void **);
|
||||
static inline void {symbol_prefix}_Kernel_Metadata_Load({symbol_prefix}_Kernel_Metadata_t *metadata) {{
|
||||
CUlibrary *libraryPtr = &(metadata->module);
|
||||
int32_t ret;
|
||||
struct {{
|
||||
CUlibrary **libraryPtr;
|
||||
int32_t *ret;
|
||||
}} initArgs = {{&libraryPtr, &ret}};
|
||||
_mlir_{symbol_prefix}_cuda_init((void **)(&initArgs));
|
||||
{dsl_name}_CUDA_ERROR_CHECK((CUresult)(ret));
|
||||
struct {{
|
||||
CUlibrary *library;
|
||||
int32_t *ret;
|
||||
}} loadArgs = {{libraryPtr, &ret}};
|
||||
_mlir_{symbol_prefix}_cuda_load((void **)(&loadArgs));
|
||||
{dsl_name}_CUDA_ERROR_CHECK((CUresult)(ret));
|
||||
CUdevice device;
|
||||
{dsl_name}_CUDA_ERROR_CHECK(cuCtxGetDevice(&device));
|
||||
int max_shared_memory_per_block_optin;
|
||||
{dsl_name}_CUDA_ERROR_CHECK(cuDeviceGetAttribute(&max_shared_memory_per_block_optin, CU_DEVICE_ATTRIBUTE_MAX_SHARED_MEMORY_PER_BLOCK_OPTIN, device));
|
||||
unsigned int num_kernels;
|
||||
{dsl_name}_CUDA_ERROR_CHECK(cuLibraryGetKernelCount(&num_kernels, metadata->module));
|
||||
CUkernel *kernels = (CUkernel *)malloc(num_kernels * sizeof(CUkernel));
|
||||
{dsl_name}_CUDA_ERROR_CHECK(cuLibraryEnumerateKernels(kernels, num_kernels, metadata->module));
|
||||
for (unsigned int i = 0; i < num_kernels; i++) {{
|
||||
{dsl_name}_CUDA_ERROR_CHECK(cuKernelSetAttribute(CU_FUNC_ATTRIBUTE_MAX_DYNAMIC_SHARED_SIZE_BYTES, max_shared_memory_per_block_optin, kernels[i], device));
|
||||
}}
|
||||
free(kernels);
|
||||
}}
|
||||
"""
|
||||
kernel_metadata_unload = f"""
|
||||
static inline void {symbol_prefix}_Kernel_Metadata_Unload({symbol_prefix}_Kernel_Metadata_t *metadata) {{
|
||||
{dsl_name}_CUDA_ERROR_CHECK(cuLibraryUnload(metadata->module));
|
||||
}}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}}
|
||||
#endif
|
||||
"""
|
||||
|
||||
return kernel_metadata_struct + kernel_metadata_load + kernel_metadata_unload
|
||||
|
||||
def _generate_arguments(
|
||||
self,
|
||||
symbol_prefix: str,
|
||||
args_spec: ExecutionArgs,
|
||||
args: List[Any],
|
||||
kwargs: Dict[str, Any],
|
||||
):
|
||||
"""
|
||||
Generate the arguments of the wrapper function.
|
||||
"""
|
||||
arguments = []
|
||||
packed_args = []
|
||||
declarations = []
|
||||
# traverse the runtime args_spec and generate the arguments
|
||||
rectified_args = args_spec.get_rectified_args(args, kwargs)
|
||||
input_arg_names = args_spec.args_spec.args + args_spec.args_spec.kwonlyargs
|
||||
for arg_name, arg in zip(input_arg_names, rectified_args):
|
||||
arg_type = args_spec.args_spec.annotations.get(arg_name, None)
|
||||
|
||||
# process optional argument
|
||||
if arg is None:
|
||||
continue
|
||||
|
||||
if isinstance(arg, Pointer):
|
||||
arguments.append(f"void *{arg_name}")
|
||||
packed_args.append("&" + arg_name)
|
||||
elif isinstance(arg, Tensor):
|
||||
dynamic_shapes = (
|
||||
f"\n int32_t dynamic_shapes[{sum(arg.dynamic_shapes_mask)}];"
|
||||
if sum(arg.dynamic_shapes_mask) > 0
|
||||
else ""
|
||||
)
|
||||
stride_type = "int32_t" if arg._use_32bit_stride else "int64_t"
|
||||
dynamic_strides = (
|
||||
f"\n {stride_type} dynamic_strides[{sum(arg.dynamic_strides_mask)}];"
|
||||
if sum(arg.dynamic_strides_mask) > 0
|
||||
else ""
|
||||
)
|
||||
declarations.append(
|
||||
f"""
|
||||
typedef struct {{
|
||||
void *data;{dynamic_shapes}{dynamic_strides}
|
||||
}} {symbol_prefix}_Tensor_{arg_name}_t;
|
||||
"""
|
||||
)
|
||||
arguments.append(f"{symbol_prefix}_Tensor_{arg_name}_t *{arg_name}")
|
||||
packed_args.append(arg_name)
|
||||
# Generate basic numeric types
|
||||
elif isinstance(arg_type, NumericMeta):
|
||||
arguments.append(self._generate_numeric_argument(arg_name, arg_type))
|
||||
packed_args.append("&" + arg_name)
|
||||
elif is_cute_algebra_type(arg_type):
|
||||
c_type = self._get_cute_algebra_type(arg_type, arg)
|
||||
arguments.append(f"{c_type}*{arg_name}")
|
||||
for i in range(self._count_dynamic_expression(arg)):
|
||||
packed_args.append("&" + arg_name + "[" + str(i) + "]")
|
||||
elif isclass(arg_type) and issubclass(arg_type, cuda.CUstream):
|
||||
arguments.append("CUstream " + arg_name)
|
||||
packed_args.append("&" + arg_name)
|
||||
else:
|
||||
raise DSLRuntimeError(
|
||||
f"Unsupported argument for c function argument generation: {arg} with type {arg_type}"
|
||||
)
|
||||
|
||||
return arguments, packed_args, declarations
|
||||
|
||||
def _generate_wrapper_function(
|
||||
self,
|
||||
symbol_prefix: str,
|
||||
args_spec: ExecutionArgs,
|
||||
function_name: str,
|
||||
kernel_info: Dict[str, List],
|
||||
dynamic_args: list,
|
||||
dynamic_kwargs: dict,
|
||||
):
|
||||
"""
|
||||
Generate the wrapper function for the compiled function which is provided to users as the entry point.
|
||||
It uses the `symbol_prefix` as the function name for identification. The host/device symbols are hidden under the bytecode.
|
||||
"""
|
||||
# 1. Get the name of the function wrapper
|
||||
wrapper_function_name = f"{symbol_prefix}_wrapper"
|
||||
capi_function_name = f"_mlir_{symbol_prefix}__mlir_ciface_{function_name}"
|
||||
# 2. Generate the signature of the wrapper function
|
||||
arguments, packed_args, declarations = self._generate_arguments(
|
||||
symbol_prefix, args_spec, dynamic_args, dynamic_kwargs
|
||||
)
|
||||
# 3. Get the return type of the wrapper function.
|
||||
# Note that this requires the return type to be properly annotated in python.
|
||||
return_type = args_spec.args_spec.annotations.get("return", None)
|
||||
if return_type is None:
|
||||
return_type = "void"
|
||||
else:
|
||||
return_type = self.numeric_to_c_type[return_type][:-1]
|
||||
declarations = "\n".join(declarations)
|
||||
|
||||
# 4. Generate the wrapper function
|
||||
function = (
|
||||
declarations
|
||||
+ f"""
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
#endif
|
||||
void {capi_function_name}(void **args, int32_t num_args);
|
||||
|
||||
static inline {return_type} {wrapper_function_name}({symbol_prefix}_Kernel_Metadata_t *metadata, {", ".join(arguments)}) {{
|
||||
{return_type} ret;
|
||||
void *args[{len(packed_args) + 1}] = {{
|
||||
{", ".join(packed_args)},
|
||||
&ret
|
||||
}};
|
||||
{capi_function_name}(args, {len(packed_args) + 1});
|
||||
return ret;
|
||||
}}
|
||||
"""
|
||||
)
|
||||
return function
|
||||
@@ -22,7 +22,7 @@ from cutlass._mlir import ir
|
||||
|
||||
from ...atom import CopyOp, Trait
|
||||
from ...tensor import ReductionOp
|
||||
from ...typing import Int16, Pointer, Integer, Numeric
|
||||
from ...typing import Int16, Int64, Pointer, Integer, Numeric
|
||||
from ..common import OpError
|
||||
from ..tcgen05.mma import CtaGroup
|
||||
|
||||
@@ -113,6 +113,7 @@ TMA_MBAR_PTR_FIELD_NAME = "tma_bar"
|
||||
TMA_MCAST_MASK_FIELD_NAME = "mcast_mask"
|
||||
TMA_DESC_PTR_FIELD_NAME = "tma_descriptor_ptr"
|
||||
TMA_BYTE_MASK_FIELD_NAME = "byte_mask"
|
||||
TMA_CACHE_POLICY_FIELD_NAME = "cache_policy"
|
||||
|
||||
|
||||
class TmaCopyOp(CopyOp):
|
||||
@@ -184,6 +185,10 @@ class CopyBulkTensorTileG2SOp(TmaCopyOp):
|
||||
class CopyBulkTensorTileG2SNonExecTrait(Trait):
|
||||
# We allow kw args to be dropped so that the user can write common code for non-multicast
|
||||
# and multicast loads.
|
||||
|
||||
def with_(self, *, loc=None, ip=None, **kwargs) -> "CopyBulkTensorTileG2STrait":
|
||||
return CopyBulkTensorTileG2STrait(self.unpack(loc=loc, ip=ip, **kwargs))
|
||||
|
||||
def unpack(
|
||||
self,
|
||||
*,
|
||||
@@ -191,13 +196,15 @@ class CopyBulkTensorTileG2SNonExecTrait(Trait):
|
||||
ip=None,
|
||||
tma_bar_ptr: Optional[Pointer] = None,
|
||||
tma_desc_ptr: Optional[Pointer] = None,
|
||||
cache_policy: Optional[Int64] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Custom implementation of unpack for non-executable TMAs.
|
||||
|
||||
The non-multicast TMA load requires a `tma_bar_ptr` keyword argument to be provided when
|
||||
using `cute.copy`. Any other kw arguments will be ignored instead of triggering an error.
|
||||
using `cute.copy`. `cache_policy` keyword argument to be provided to set the l2 cache eviction priority.
|
||||
Any other kw arguments will be ignored instead of triggering an error.
|
||||
"""
|
||||
if not isinstance(tma_bar_ptr, Pointer):
|
||||
raise ValueError(
|
||||
@@ -215,9 +222,25 @@ class CopyBulkTensorTileG2SNonExecTrait(Trait):
|
||||
exec_value = _cute_nvgpu_ir.atom_set_value(
|
||||
exec_value, attr, tma_desc_ptr.value, loc=loc, ip=ip
|
||||
)
|
||||
if cache_policy is not None:
|
||||
if not isinstance(cache_policy, Int64):
|
||||
raise ValueError(
|
||||
"expects `Int64` value to be provided via the cache_policy kw argument"
|
||||
)
|
||||
|
||||
attr_str = (
|
||||
f"#cute_nvgpu.atom_copy_field_tmaload<{TMA_CACHE_POLICY_FIELD_NAME}>"
|
||||
)
|
||||
attr = ir.Attribute.parse(attr_str)
|
||||
exec_value = _cute_nvgpu_ir.atom_set_value(
|
||||
exec_value, attr, cache_policy.value, loc=loc, ip=ip
|
||||
)
|
||||
return exec_value
|
||||
|
||||
|
||||
class CopyBulkTensorTileG2STrait(Trait):
|
||||
pass
|
||||
|
||||
#
|
||||
# TMA GMEM -> SMEM multicast copies
|
||||
#
|
||||
@@ -277,6 +300,13 @@ class CopyBulkTensorTileG2SMulticastOp(TmaCopyOp):
|
||||
|
||||
|
||||
class CopyBulkTensorTileG2SMulticastNonExecTrait(Trait):
|
||||
def with_(
|
||||
self, *, loc=None, ip=None, **kwargs
|
||||
) -> "CopyBulkTensorTileG2SMulticastTrait":
|
||||
return CopyBulkTensorTileG2SMulticastTrait(
|
||||
self.unpack(loc=loc, ip=ip, **kwargs)
|
||||
)
|
||||
|
||||
def unpack(
|
||||
self,
|
||||
*,
|
||||
@@ -285,12 +315,14 @@ class CopyBulkTensorTileG2SMulticastNonExecTrait(Trait):
|
||||
tma_bar_ptr: Optional[Pointer] = None,
|
||||
mcast_mask=None,
|
||||
tma_desc_ptr=None,
|
||||
cache_policy: Optional[Int64] = None,
|
||||
):
|
||||
"""
|
||||
Custom implementation of unpack for non-executable TMAs.
|
||||
|
||||
The multicast TMA load requires a `tma_bar_ptr` and a `mcast_mask` keyword arguments to be
|
||||
provided when using `cute.copy`.
|
||||
provided when using `cute.copy`. `cache_policy` keyword argument to be provided to set the
|
||||
l2 cache eviction priority.
|
||||
"""
|
||||
if not isinstance(tma_bar_ptr, Pointer):
|
||||
raise ValueError(
|
||||
@@ -312,6 +344,19 @@ class CopyBulkTensorTileG2SMulticastNonExecTrait(Trait):
|
||||
exec_value = _cute_nvgpu_ir.atom_set_value(
|
||||
exec_value, attr, tma_desc_ptr.value, loc=loc, ip=ip
|
||||
)
|
||||
if cache_policy is not None:
|
||||
if not isinstance(cache_policy, Int64):
|
||||
raise ValueError(
|
||||
"expects `Int64` value to be provided via the cache_policy kw argument"
|
||||
)
|
||||
|
||||
attr_str = (
|
||||
f"#cute_nvgpu.atom_copy_field_tmaload<{TMA_CACHE_POLICY_FIELD_NAME}>"
|
||||
)
|
||||
attr = ir.Attribute.parse(attr_str)
|
||||
exec_value = _cute_nvgpu_ir.atom_set_value(
|
||||
exec_value, attr, cache_policy.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>"
|
||||
@@ -321,6 +366,9 @@ class CopyBulkTensorTileG2SMulticastNonExecTrait(Trait):
|
||||
)
|
||||
return exec_value
|
||||
|
||||
class CopyBulkTensorTileG2SMulticastTrait(Trait):
|
||||
pass
|
||||
|
||||
|
||||
#
|
||||
# TMA SMEM -> GMEM copies
|
||||
@@ -351,14 +399,24 @@ class CopyBulkTensorTileS2GOp(TmaCopyOp):
|
||||
|
||||
def _make_trait(
|
||||
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
|
||||
) -> "CopyBulkTensorTileS2GTrait":
|
||||
) -> "CopyBulkTensorTileS2GNonExecTrait":
|
||||
raise NotImplementedError(
|
||||
"Use cpasync.make_tiled_tma_atom to obtain a copy Atom for TMA"
|
||||
)
|
||||
|
||||
|
||||
class CopyBulkTensorTileS2GTrait(Trait):
|
||||
def unpack(self, *, loc=None, ip=None, tma_desc_ptr: Optional[Pointer] = None):
|
||||
class CopyBulkTensorTileS2GNonExecTrait(Trait):
|
||||
def with_(self, *, loc=None, ip=None, **kwargs) -> "CopyBulkTensorTileS2GTrait":
|
||||
return CopyBulkTensorTileS2GTrait(self.unpack(loc=loc, ip=ip, **kwargs))
|
||||
|
||||
def unpack(
|
||||
self,
|
||||
*,
|
||||
loc=None,
|
||||
ip=None,
|
||||
tma_desc_ptr: Optional[Pointer] = None,
|
||||
cache_policy: Optional[Int64] = None,
|
||||
):
|
||||
"""
|
||||
Custom implementation of unpack for non-executable TMAs.
|
||||
"""
|
||||
@@ -371,9 +429,26 @@ class CopyBulkTensorTileS2GTrait(Trait):
|
||||
exec_value = _cute_nvgpu_ir.atom_set_value(
|
||||
exec_value, attr, tma_desc_ptr.value, loc=loc, ip=ip
|
||||
)
|
||||
if cache_policy is not None:
|
||||
if not isinstance(cache_policy, Int64):
|
||||
raise ValueError(
|
||||
"expects `Int64` value to be provided via the cache_policy kw argument"
|
||||
)
|
||||
|
||||
attr_str = (
|
||||
f"#cute_nvgpu.atom_copy_field_tmastore<{TMA_CACHE_POLICY_FIELD_NAME}>"
|
||||
)
|
||||
attr = ir.Attribute.parse(attr_str)
|
||||
exec_value = _cute_nvgpu_ir.atom_set_value(
|
||||
exec_value, attr, cache_policy.value, loc=loc, ip=ip
|
||||
)
|
||||
return exec_value
|
||||
|
||||
|
||||
class CopyBulkTensorTileS2GTrait(Trait):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CopyReduceBulkTensorTileS2GOp(TmaCopyOp):
|
||||
"""
|
||||
@@ -400,7 +475,7 @@ class CopyReduceBulkTensorTileS2GOp(TmaCopyOp):
|
||||
|
||||
def _make_trait(
|
||||
self, copy_internal_type: Type[Numeric], *, loc=None, ip=None, **kwargs
|
||||
) -> "CopyReduceBulkTensorTileS2GTrait":
|
||||
) -> "CopyReduceBulkTensorTileS2GNonExecTrait":
|
||||
raise NotImplementedError(
|
||||
"Use cpasync.make_tiled_tma_atom to obtain a copy Atom for TMA"
|
||||
)
|
||||
@@ -426,8 +501,20 @@ class CopyReduceBulkTensorTileS2GOp(TmaCopyOp):
|
||||
assert False, "unrecognized self.reduction_kind"
|
||||
|
||||
|
||||
class CopyReduceBulkTensorTileS2GTrait(Trait):
|
||||
def unpack(self, *, loc=None, ip=None, tma_desc_ptr: Optional[Pointer] = None):
|
||||
class CopyReduceBulkTensorTileS2GNonExecTrait(Trait):
|
||||
def with_(
|
||||
self, *, loc=None, ip=None, **kwargs
|
||||
) -> "CopyReduceBulkTensorTileS2GTrait":
|
||||
return CopyReduceBulkTensorTileS2GTrait(self.unpack(loc=loc, ip=ip, **kwargs))
|
||||
|
||||
def unpack(
|
||||
self,
|
||||
*,
|
||||
loc=None,
|
||||
ip=None,
|
||||
tma_desc_ptr: Optional[Pointer] = None,
|
||||
cache_policy: Optional[Int64] = None,
|
||||
):
|
||||
"""
|
||||
Custom implementation of unpack for non-executable TMAs.
|
||||
"""
|
||||
@@ -440,9 +527,25 @@ class CopyReduceBulkTensorTileS2GTrait(Trait):
|
||||
exec_value = _cute_nvgpu_ir.atom_set_value(
|
||||
exec_value, attr, tma_desc_ptr.value, loc=loc, ip=ip
|
||||
)
|
||||
if cache_policy is not None:
|
||||
if not isinstance(cache_policy, Int64):
|
||||
raise ValueError(
|
||||
"expects `Int64` value to be provided via the cache_policy kw argument"
|
||||
)
|
||||
attr_str = (
|
||||
f"#cute_nvgpu.atom_copy_field_tmareduce<{TMA_CACHE_POLICY_FIELD_NAME}>"
|
||||
)
|
||||
attr = ir.Attribute.parse(attr_str)
|
||||
exec_value = _cute_nvgpu_ir.atom_set_value(
|
||||
exec_value, attr, cache_policy.value, loc=loc, ip=ip
|
||||
)
|
||||
return exec_value
|
||||
|
||||
|
||||
class CopyReduceBulkTensorTileS2GTrait(Trait):
|
||||
pass
|
||||
|
||||
|
||||
#
|
||||
# Bulk GMEM -> SMEM copies
|
||||
#
|
||||
@@ -494,13 +597,15 @@ class CopyBulkG2STrait(Trait):
|
||||
loc=None,
|
||||
ip=None,
|
||||
mbar_ptr: Optional[Pointer] = None,
|
||||
cache_policy: Optional[Int64] = 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.
|
||||
using `cute.copy`. `cache_policy` keyword argument to be provided to set the l2 cache eviction priority.
|
||||
Any other kw arguments will be ignored instead of triggering an error.
|
||||
"""
|
||||
if not isinstance(mbar_ptr, Pointer):
|
||||
raise ValueError(
|
||||
@@ -511,6 +616,18 @@ class CopyBulkG2STrait(Trait):
|
||||
val = _cute_nvgpu_ir.atom_set_value(
|
||||
self.value, attr, mbar_ptr.value, loc=loc, ip=ip
|
||||
)
|
||||
if cache_policy is not None:
|
||||
if not isinstance(cache_policy, Int64):
|
||||
raise ValueError(
|
||||
"expects `Int64` value to be provided via the cache_policy kw argument"
|
||||
)
|
||||
attr_str = (
|
||||
f"#cute_nvgpu.atom_copy_field_bulkg2s<{TMA_CACHE_POLICY_FIELD_NAME}>"
|
||||
)
|
||||
attr = ir.Attribute.parse(attr_str)
|
||||
val = _cute_nvgpu_ir.atom_set_value(
|
||||
val, attr, cache_policy.value, loc=loc, ip=ip
|
||||
)
|
||||
return val
|
||||
|
||||
|
||||
@@ -566,6 +683,7 @@ class CopyBulkG2SMulticastTrait(Trait):
|
||||
ip=None,
|
||||
mbar_ptr: Optional[Pointer] = None,
|
||||
mcast_mask: Optional[Integer] = None,
|
||||
cache_policy: Optional[Int64] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
@@ -592,6 +710,18 @@ class CopyBulkG2SMulticastTrait(Trait):
|
||||
val = _cute_nvgpu_ir.atom_set_value(
|
||||
val, attr, Int16(mcast_mask).ir_value(loc=loc, ip=ip), loc=loc, ip=ip
|
||||
)
|
||||
if cache_policy is not None:
|
||||
if not isinstance(cache_policy, Int64):
|
||||
raise ValueError(
|
||||
"expects `Int64` value to be provided via the cache_policy kw argument"
|
||||
)
|
||||
attr_str = (
|
||||
f"#cute_nvgpu.atom_copy_field_bulkg2s<{TMA_CACHE_POLICY_FIELD_NAME}>"
|
||||
)
|
||||
attr = ir.Attribute.parse(attr_str)
|
||||
val = _cute_nvgpu_ir.atom_set_value(
|
||||
val, attr, cache_policy.value, loc=loc, ip=ip
|
||||
)
|
||||
return val
|
||||
|
||||
|
||||
|
||||
@@ -35,8 +35,8 @@ from .copy import (
|
||||
CopyReduceBulkTensorTileS2GOp,
|
||||
CopyBulkTensorTileG2SNonExecTrait,
|
||||
CopyBulkTensorTileG2SMulticastNonExecTrait,
|
||||
CopyBulkTensorTileS2GTrait,
|
||||
CopyReduceBulkTensorTileS2GTrait,
|
||||
CopyBulkTensorTileS2GNonExecTrait,
|
||||
CopyReduceBulkTensorTileS2GNonExecTrait,
|
||||
)
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ def make_tiled_tma_atom(
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
return atom.CopyAtom(op, CopyBulkTensorTileS2GTrait(res[0])), res[1]
|
||||
return atom.CopyAtom(op, CopyBulkTensorTileS2GNonExecTrait(res[0])), res[1]
|
||||
elif isinstance(op, CopyReduceBulkTensorTileS2GOp):
|
||||
res = _cute_nvgpu_ir.atom_make_non_exec_tiled_tma_reduce(
|
||||
gmem_tensor.value,
|
||||
@@ -167,7 +167,10 @@ def make_tiled_tma_atom(
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
return atom.CopyAtom(op, CopyReduceBulkTensorTileS2GTrait(res[0])), res[1]
|
||||
return (
|
||||
atom.CopyAtom(op, CopyReduceBulkTensorTileS2GNonExecTrait(res[0])),
|
||||
res[1],
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"expects a bulk tensor (TMA) Copy Op, but got {op}")
|
||||
|
||||
|
||||
@@ -285,7 +285,9 @@ def find_tmem_tensor_col_offset(tmem_tensor: Tensor, *, loc=None, ip=None) -> In
|
||||
"""
|
||||
tmem_col_mask = 0x0000FFFF
|
||||
offset = (
|
||||
core.cosize(recast_tensor(tmem_tensor, Int32).layout, loc=loc, ip=ip)
|
||||
core.cosize(
|
||||
recast_tensor(tmem_tensor, Int32, loc=loc, ip=ip).layout, loc=loc, ip=ip
|
||||
)
|
||||
& tmem_col_mask
|
||||
)
|
||||
if isinstance(offset, int):
|
||||
|
||||
@@ -10,19 +10,24 @@
|
||||
# is strictly prohibited.
|
||||
|
||||
import ctypes
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from functools import lru_cache
|
||||
import itertools
|
||||
import operator
|
||||
from typing import Union, Optional
|
||||
from typing import Union, Optional, Type, List
|
||||
|
||||
# MLIR modules imports
|
||||
from cutlass._mlir import ir
|
||||
import cutlass._mlir.dialects.cute as _cute_ir
|
||||
import cutlass._mlir.dialects.cuda as _cuda_dialect
|
||||
|
||||
from cutlass.cutlass_dsl import JitArgAdapterRegistry, DSLRuntimeError
|
||||
from cutlass.cutlass_dsl import JitArgAdapterRegistry, CuTeDSL as _CuTeDSL
|
||||
from cutlass.base_dsl.common import DSLRuntimeError
|
||||
|
||||
# Local modules imports
|
||||
from .typing import AddressSpace, Tensor, Type, Pointer, Numeric
|
||||
from .typing import AddressSpace, Layout, Tensor, Pointer, Numeric, SymInt
|
||||
from . import core
|
||||
from .tensor import _Tensor as CoreTensor
|
||||
|
||||
@@ -76,6 +81,9 @@ class _Pointer(Pointer):
|
||||
def __get_mlir_types__(self):
|
||||
return [self.mlir_type]
|
||||
|
||||
def __tvm_ffi_opaque_ptr__(self):
|
||||
return self._pointer
|
||||
|
||||
def __c_pointers__(self):
|
||||
if self._c_pointer is None:
|
||||
self._desc = ctypes.c_void_p(int(self._pointer))
|
||||
@@ -104,14 +112,6 @@ class _Pointer(Pointer):
|
||||
def align(self, min_align: int, *, loc=None, ip=None) -> Pointer:
|
||||
raise NotImplementedError("align is not supported in runtime")
|
||||
|
||||
def verify(self, expected_py_type):
|
||||
if expected_py_type is Pointer:
|
||||
return True
|
||||
elif isinstance(expected_py_type, ir.Value) and expected_py_type.ty is Pointer:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Ptr<0x{int(self._pointer):016x}@{self._addr_space}>"
|
||||
|
||||
@@ -120,12 +120,30 @@ class _Pointer(Pointer):
|
||||
|
||||
|
||||
class _Tensor(Tensor):
|
||||
def __init__(self, tensor, assumed_align=None, use_32bit_stride=False):
|
||||
def __init__(
|
||||
self,
|
||||
tensor,
|
||||
assumed_align=None,
|
||||
use_32bit_stride=False,
|
||||
*,
|
||||
enable_tvm_ffi=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
|
||||
self._dlpack_data = tensor.__dlpack_device__()
|
||||
else:
|
||||
self._dlpack_data = tensor.__dlpack__()
|
||||
try:
|
||||
# we expect no stream sync. Because torch has different default behavior
|
||||
# for stream parameter on different version.
|
||||
# we need to explicitly pass -1 to achieve no sync effects.
|
||||
self._dlpack_data = tensor.__dlpack__(stream=-1)
|
||||
except Exception:
|
||||
self._dlpack_data = tensor.__dlpack__()
|
||||
if enable_tvm_ffi:
|
||||
import tvm_ffi
|
||||
|
||||
self._tvm_ffi_tensor = tvm_ffi.from_dlpack(tensor)
|
||||
self._dlpack_data = self._tvm_ffi_tensor.__dlpack__()
|
||||
self._dltensor_wrapper = None
|
||||
self._assumed_align = assumed_align
|
||||
self._is_dynamic = False
|
||||
@@ -217,7 +235,6 @@ class _Tensor(Tensor):
|
||||
mode, stride_order, divisibility
|
||||
)
|
||||
return self
|
||||
|
||||
@property
|
||||
@lazily_load_dltensor
|
||||
def element_type(self) -> Type[Numeric]:
|
||||
@@ -342,6 +359,18 @@ class _Tensor(Tensor):
|
||||
def data_ptr(self):
|
||||
return self._dltensor_wrapper.data_ptr
|
||||
|
||||
@property
|
||||
@lazily_load_dltensor
|
||||
def dynamic_shapes_mask(self):
|
||||
"""Get the mask of dynamic shapes in the tensor."""
|
||||
return self._dltensor_wrapper.get_dynamic_shapes_mask()
|
||||
|
||||
@property
|
||||
@lazily_load_dltensor
|
||||
def dynamic_strides_mask(self):
|
||||
"""Get the mask of dynamic strides in the tensor."""
|
||||
return self._dltensor_wrapper.get_dynamic_strides_mask()
|
||||
|
||||
@lazily_load_dltensor
|
||||
def __c_pointers__(self):
|
||||
self._memref_desc = self._dltensor_wrapper.build_memref_desc(
|
||||
@@ -357,11 +386,336 @@ class _Tensor(Tensor):
|
||||
assert isinstance(values[0], CoreTensor)
|
||||
return CoreTensor(values[0].value, self._dtype)
|
||||
|
||||
def __tvm_ffi_object__(self):
|
||||
return self._tvm_ffi_tensor
|
||||
|
||||
|
||||
def _get_cute_type_str(inp):
|
||||
def _convert_dyn_elem(e):
|
||||
return f"?{{i{e.width} div={e.divisibility}}}"
|
||||
|
||||
elems = [_convert_dyn_elem(e) if isinstance(e, SymInt) else str(e) for e in inp]
|
||||
return "(" + ",".join(elems) + ")"
|
||||
|
||||
|
||||
class _FakeCompactTensor(Tensor):
|
||||
def __init__(
|
||||
self,
|
||||
dtype,
|
||||
shape,
|
||||
stride_order,
|
||||
memspace=None,
|
||||
assumed_align=None,
|
||||
use_32bit_stride=False,
|
||||
):
|
||||
self._dtype = dtype
|
||||
self._shape = shape
|
||||
self._stride_order = stride_order or tuple(range(len(shape)))
|
||||
self._memspace = memspace or AddressSpace.gmem
|
||||
self._assumed_align = assumed_align or -(-dtype.width // 8)
|
||||
self._use_32bit_stride = use_32bit_stride
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"FakeTensorOrdered<{self._dtype}, {self._shape}, {self._stride_order}>"
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
@property
|
||||
def mlir_type(self) -> ir.Type:
|
||||
shape_ty = ir.Type.parse(
|
||||
'!cute.shape<"' + _get_cute_type_str(self._shape) + '">'
|
||||
)
|
||||
layout_ty = _cute_ir.LayoutType.get_ordered(
|
||||
shape_ty, self._stride_order, self._use_32bit_stride
|
||||
)
|
||||
self._stride = layout_ty.stride
|
||||
ptr_ty = _cute_ir.PtrType.get(
|
||||
self._dtype.mlir_type, self._memspace, self._assumed_align
|
||||
)
|
||||
return _cute_ir.MemRefType.get(ptr_ty, layout_ty)
|
||||
|
||||
def __get_mlir_types__(self):
|
||||
return [self.mlir_type]
|
||||
|
||||
def __new_from_mlir_values__(self, values):
|
||||
assert len(values) == 1
|
||||
assert isinstance(values[0], CoreTensor)
|
||||
return CoreTensor(values[0].value, self._dtype)
|
||||
|
||||
def __setitem__(self, crd, value):
|
||||
raise DSLRuntimeError("runtime._FakeCompactTensor is not indexable")
|
||||
|
||||
def __getitem__(self, crd):
|
||||
raise DSLRuntimeError("runtime._FakeCompactTensor is not indexable")
|
||||
|
||||
@property
|
||||
def element_type(self) -> Type[Numeric]:
|
||||
return self._dtype
|
||||
|
||||
@property
|
||||
def memspace(self):
|
||||
return self._memspace
|
||||
|
||||
@property
|
||||
def iterator(self):
|
||||
raise DSLRuntimeError("runtime._FakeTensor has dummy iterator")
|
||||
|
||||
@property
|
||||
def shape(self):
|
||||
return self._shape
|
||||
|
||||
@property
|
||||
def stride(self):
|
||||
return self._stride
|
||||
|
||||
@property
|
||||
def dynamic_shapes_mask(self):
|
||||
return tuple(1 if isinstance(e, SymInt) else 0 for e in self._shape)
|
||||
|
||||
@property
|
||||
def dynamic_strides_mask(self):
|
||||
return tuple(1 if isinstance(e, SymInt) else 0 for e in self._stride)
|
||||
|
||||
def fill(self, value: Numeric):
|
||||
raise DSLRuntimeError("runtime._FakeCompactTensor is not writable")
|
||||
|
||||
|
||||
class _FakeTensor(Tensor):
|
||||
"""Fake Tensor implementation as a placeholder.
|
||||
It mimics the interface of Tensor, but does not hold real data or allow indexing.
|
||||
Used for compilation or testing situations where only shape/type/layout information is needed.
|
||||
All attempts to access or mutate data will raise errors.
|
||||
"""
|
||||
|
||||
"""
|
||||
Create a fake tensor with the given shape, type, and layout.
|
||||
|
||||
:param dtype: Data type of the tensor elements
|
||||
:type dtype: Type[Numeric]
|
||||
:param shape: Shape of the tensor, consists of int (static) or SymInt (dynamic)
|
||||
:type shape: tuple[int, ...]
|
||||
:param stride: Stride of the tensor, defaults to None, consists of int (static) or SymInt (dynamic)
|
||||
:type stride: tuple[int, ...], optional
|
||||
:param assumed_align: Assumed alignment of the tensor (bytes), defaults to None. If None, uses 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 sizes (cosize(layout) <= Int32_max) for better performance. This is only applied
|
||||
when the dimension is dynamic.
|
||||
:type use_32bit_stride: bool, optional
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, dtype, shape, *, stride, memspace=None, assumed_align=None):
|
||||
self._dtype = dtype
|
||||
self._shape = shape
|
||||
self._stride = stride
|
||||
self._memspace = memspace or AddressSpace.generic
|
||||
self._assumed_align = assumed_align
|
||||
if assumed_align is None:
|
||||
# use the bytes width of the element dtype. The alignment is at least one byte align.
|
||||
self._assumed_align = (self._dtype.width + 7) // 8
|
||||
|
||||
if not isinstance(shape, (tuple, list)):
|
||||
raise ValueError(f"Expected tuple or list but got {type(shape)}")
|
||||
|
||||
if not all(isinstance(s, (int, SymInt)) for s in self._shape):
|
||||
raise ValueError("All shape elements must be int or SymInt")
|
||||
|
||||
if stride is not None and not all(
|
||||
isinstance(s, (int, SymInt)) for s in self._stride
|
||||
):
|
||||
raise ValueError("All stride elements must be int or SymInt")
|
||||
@property
|
||||
def mlir_type(self) -> ir.Type:
|
||||
shape_str = _get_cute_type_str(self._shape)
|
||||
stride_str = _get_cute_type_str(self._stride)
|
||||
layout_ty = ir.Type.parse(f'!cute.layout<"{shape_str}:{stride_str}">')
|
||||
ptr_ty = _cute_ir.PtrType.get(
|
||||
self._dtype.mlir_type, self._memspace, self._assumed_align
|
||||
)
|
||||
return _cute_ir.MemRefType.get(ptr_ty, layout_ty)
|
||||
|
||||
def __get_mlir_types__(self):
|
||||
return [self.mlir_type]
|
||||
|
||||
def __new_from_mlir_values__(self, values):
|
||||
assert len(values) == 1
|
||||
assert isinstance(values[0], CoreTensor)
|
||||
return CoreTensor(values[0].value, self._dtype)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"FakeTensor<{self._dtype}, {self._shape}, {self._stride}>"
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def __setitem__(self, crd, value):
|
||||
raise DSLRuntimeError("runtime._FakeTensor is not indexable")
|
||||
|
||||
def __getitem__(self, crd):
|
||||
raise DSLRuntimeError("runtime._FakeTensor is not indexable")
|
||||
|
||||
@property
|
||||
def element_type(self) -> Type[Numeric]:
|
||||
return self._dtype
|
||||
|
||||
@property
|
||||
def memspace(self):
|
||||
return self._memspace
|
||||
|
||||
@property
|
||||
def iterator(self):
|
||||
raise DSLRuntimeError("runtime._FakeTensor has dummy iterator")
|
||||
|
||||
@property
|
||||
def shape(self):
|
||||
return self._shape
|
||||
|
||||
@property
|
||||
def stride(self):
|
||||
return self._stride
|
||||
|
||||
@property
|
||||
def dynamic_shapes_mask(self):
|
||||
return tuple(1 if isinstance(e, SymInt) else 0 for e in self._shape)
|
||||
|
||||
@property
|
||||
def dynamic_strides_mask(self):
|
||||
return tuple(1 if isinstance(e, SymInt) else 0 for e in self._stride)
|
||||
|
||||
def fill(self, value: Numeric):
|
||||
raise DSLRuntimeError("runtime._FakeTensor is not writable")
|
||||
|
||||
|
||||
def make_fake_compact_tensor(
|
||||
dtype,
|
||||
shape,
|
||||
*,
|
||||
stride_order=None,
|
||||
memspace=None,
|
||||
assumed_align=None,
|
||||
use_32bit_stride=False,
|
||||
):
|
||||
"""
|
||||
Create a fake tensor with the specified shape, element type, and a compact memory layout.
|
||||
|
||||
:param dtype: Data type of the tensor elements.
|
||||
:type dtype: Type[Numeric]
|
||||
: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 row-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
|
||||
:param assumed_align: Assumed byte alignment for the tensor data. If None, the default alignment is used.
|
||||
:type assumed_align: int, optional
|
||||
:param use_32bit_stride: Whether to use 32-bit stride for dynamic dimensions. If True and the total size of the
|
||||
layout (cosize(layout)) fits within int32, then dynamic strides will use 32-bit integers for improved performance.
|
||||
Only applies when dimensions are dynamic. Defaults to False.
|
||||
:type use_32bit_stride: bool, optional
|
||||
:return: An instance of a fake tensor with the given properties and compact layout.
|
||||
:rtype: _FakeCompactTensor
|
||||
|
||||
**Examples:**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@cute.jit
|
||||
def foo(x: cute.Tensor):
|
||||
...
|
||||
|
||||
x = make_fake_compact_tensor(
|
||||
cutlass.Float32, (100, cute.sym_int32(divisibility=8)), stride_order=(1, 0)
|
||||
)
|
||||
|
||||
# Compiled function will take a tensor with the type:
|
||||
# tensor<ptr<f32, generic> o (100,?{div=8}):(?{i32 div=8},1)>
|
||||
compiled_foo = cute.compile(foo, x)
|
||||
"""
|
||||
|
||||
return _FakeCompactTensor(
|
||||
dtype,
|
||||
shape,
|
||||
stride_order=stride_order,
|
||||
memspace=memspace,
|
||||
assumed_align=assumed_align,
|
||||
use_32bit_stride=use_32bit_stride,
|
||||
)
|
||||
|
||||
def make_fake_tensor(dtype, shape, stride, *, memspace=None, assumed_align=None):
|
||||
"""
|
||||
Create a fake tensor with the specified element type, shape, and stride.
|
||||
|
||||
:param dtype: Data type of the tensor elements.
|
||||
:type dtype: Type[Numeric]
|
||||
:param shape: Shape of the tensor.
|
||||
:type shape: tuple[int, ...]
|
||||
:param stride: Stride of the tensor.
|
||||
:type stride: tuple[int, ...]
|
||||
:param assumed_align: Assumed byte alignment for the tensor data. If None, the default alignment is used. Defaults to None.
|
||||
:type assumed_align: int, optional
|
||||
:return: An instance of a fake tensor with the given properties.
|
||||
:rtype: _FakeTensor
|
||||
"""
|
||||
return _FakeTensor(
|
||||
dtype, shape, stride=stride, memspace=memspace, assumed_align=assumed_align
|
||||
)
|
||||
|
||||
|
||||
class _FakeStream:
|
||||
"""A fake stream that can be used as a placeholder for a stream in compilation.
|
||||
|
||||
When use_tvm_ffi_env_stream is True and the function is compiled with TVM-FFI,
|
||||
the argument will be skipped from the function signature and we pass in
|
||||
this value through the environment stream obtained from caller context
|
||||
(e.g. torch.cuda.current_stream()).
|
||||
"""
|
||||
|
||||
use_tvm_ffi_env_stream: bool
|
||||
|
||||
def __init__(self, *, use_tvm_ffi_env_stream: bool = False):
|
||||
self.use_tvm_ffi_env_stream = use_tvm_ffi_env_stream
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"FakeStream"
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def __new_from_mlir_values__(self, values):
|
||||
assert len(values) == 1
|
||||
return values[0]
|
||||
|
||||
def __c_pointers__(self):
|
||||
return [0]
|
||||
|
||||
def __get_mlir_types__(self):
|
||||
return [_cuda_dialect.StreamType.get()]
|
||||
|
||||
|
||||
def make_fake_stream(*, use_tvm_ffi_env_stream: bool = False):
|
||||
"""Create a fake stream that can be used as a placeholder for a stream in compilation.
|
||||
|
||||
When use_tvm_ffi_env_stream is True and the function is compiled with TVM-FFI,
|
||||
the argument will be skipped from the function signature and we pass in
|
||||
this value through the environment stream obtained from caller context
|
||||
(e.g. torch.cuda.current_stream()). This can speedup the calling process
|
||||
since we no longer need to do stream query in python.
|
||||
|
||||
:param use_tvm_ffi_env_stream: Whether to skip this parameter use environment stream instead.
|
||||
:type use_tvm_ffi_env_stream: bool
|
||||
"""
|
||||
return _FakeStream(use_tvm_ffi_env_stream=use_tvm_ffi_env_stream)
|
||||
|
||||
|
||||
def from_dlpack(
|
||||
tensor_dlpack,
|
||||
assumed_align=None,
|
||||
use_32bit_stride=False,
|
||||
*,
|
||||
enable_tvm_ffi=False,
|
||||
) -> Tensor:
|
||||
"""Convert from tensor object supporting __dlpack__() to a CuTe Tensor.
|
||||
|
||||
@@ -374,25 +728,33 @@ def from_dlpack(
|
||||
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
|
||||
:param enable_tvm_ffi: Whether to enable TVM-FFI, defaults to False. When True, the tensor will be converted to
|
||||
a TVM-FFI function compatible tensor.
|
||||
:type enable_tvm_ffi: bool, optional
|
||||
:return: A CuTe Tensor object
|
||||
:rtype: Tensor
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
**Examples:**
|
||||
|
||||
import torch
|
||||
from cutlass.cute.runtime import from_dlpack
|
||||
x = torch.randn(100, 100)
|
||||
y = from_dlpack(x)
|
||||
y.shape
|
||||
# (100, 100)
|
||||
type(y)
|
||||
# <class 'cutlass.cute.Tensor'>
|
||||
.. code-block:: python
|
||||
|
||||
import torch
|
||||
from cutlass.cute.runtime import from_dlpack
|
||||
x = torch.randn(100, 100)
|
||||
y = from_dlpack(x)
|
||||
y.shape
|
||||
# (100, 100)
|
||||
type(y)
|
||||
# <class 'cutlass.cute.Tensor'>
|
||||
"""
|
||||
# If the environment variable `CUTE_DSL_ENABLE_TVM_FFI` is set to True, the tensor will be converted to
|
||||
# a TVM-FFI function compatible tensor.
|
||||
enable_tvm_ffi = enable_tvm_ffi or _CuTeDSL._get_dsl().envar.enable_tvm_ffi
|
||||
return _Tensor(
|
||||
tensor_dlpack,
|
||||
assumed_align=assumed_align,
|
||||
use_32bit_stride=use_32bit_stride,
|
||||
enable_tvm_ffi=enable_tvm_ffi,
|
||||
)
|
||||
|
||||
|
||||
@@ -451,6 +813,23 @@ def make_ptr(
|
||||
return _Pointer(address_value, dtype, mem_space, assumed_align=assumed_align)
|
||||
|
||||
|
||||
def nullptr(
|
||||
dtype: Type[Numeric],
|
||||
mem_space: AddressSpace = AddressSpace.generic,
|
||||
assumed_align=None,
|
||||
) -> Pointer:
|
||||
"""Create a null pointer which is useful for compilation
|
||||
|
||||
:param dtype: Data type of the pointer elements
|
||||
:type dtype: Type[Numeric]
|
||||
:param mem_space: Memory address space, defaults to AddressSpace.generic
|
||||
:type mem_space: AddressSpace, optional
|
||||
:return: A null pointer object
|
||||
:rtype: Pointer
|
||||
"""
|
||||
return make_ptr(dtype, 0, mem_space, assumed_align=assumed_align)
|
||||
|
||||
|
||||
class TensorAdapter:
|
||||
"""
|
||||
Convert a DLPack protocol supported tensor/array to a cute tensor.
|
||||
@@ -469,6 +848,74 @@ class TensorAdapter:
|
||||
return self._arg.__get_mlir_types__()
|
||||
|
||||
|
||||
def find_runtime_libraries(*, enable_tvm_ffi: bool = True) -> List[str]:
|
||||
"""
|
||||
Find the runtime libraries that needs to be available for loading modules.
|
||||
|
||||
:param enable_tvm_ffi: Whether to enable TVM-FFI.
|
||||
:type enable_tvm_ffi: bool, optional
|
||||
:return: A list of runtime libraries that needs to be available for loading modules.
|
||||
:rtype: list
|
||||
"""
|
||||
|
||||
def _get_cuda_dialect_runtime_path():
|
||||
libs = os.environ.get("CUTE_DSL_LIBS")
|
||||
if libs:
|
||||
sep = ";" if sys.platform.startswith("win32") else ":"
|
||||
for path in libs.split(sep):
|
||||
if path.endswith("libcuda_dialect_runtime.so"):
|
||||
return path
|
||||
try:
|
||||
# find package library from wheel package
|
||||
pkg_base = Path(__file__).resolve().parent.parent
|
||||
lib_path = pkg_base / "lib" / "libcuda_dialect_runtime.so"
|
||||
if lib_path.is_file():
|
||||
return str(lib_path)
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
libs = []
|
||||
cuda_dialect_runtime_path = _get_cuda_dialect_runtime_path()
|
||||
if cuda_dialect_runtime_path:
|
||||
libs.append(cuda_dialect_runtime_path)
|
||||
|
||||
if enable_tvm_ffi:
|
||||
import tvm_ffi
|
||||
|
||||
libs.append(tvm_ffi.libinfo.find_libtvm_ffi())
|
||||
|
||||
return libs
|
||||
|
||||
# cache to load runtime libraries so they can be found by the DSO loader
|
||||
_LOAD_MODULE_LIBS_CACHE = []
|
||||
|
||||
|
||||
def load_module(file_path: str, *, enable_tvm_ffi: bool = True):
|
||||
"""Load a module from a file path. Today only support TVM-FFI module.
|
||||
|
||||
:param file_path: The path to the module file
|
||||
:type file_path: str
|
||||
:param enable_tvm_ffi: Whether to enable TVM-FFI, defaults to True. When True, the module will be loaded as a TVM-FFI module.
|
||||
:type enable_tvm_ffi: bool, optional
|
||||
:return: A module object
|
||||
:rtype: module
|
||||
"""
|
||||
if len(_LOAD_MODULE_LIBS_CACHE) == 0:
|
||||
# ensure the runtime libraries are loaded so they can be found by the DSO loader
|
||||
# no need to load tvm_ffi library here since it will be loaded by tvm_ffi package.
|
||||
for path in find_runtime_libraries(enable_tvm_ffi=False):
|
||||
if Path(path).exists():
|
||||
_LOAD_MODULE_LIBS_CACHE.append(ctypes.CDLL(path))
|
||||
|
||||
if enable_tvm_ffi:
|
||||
import tvm_ffi
|
||||
|
||||
return tvm_ffi.load_module(file_path)
|
||||
else:
|
||||
raise DSLRuntimeError(
|
||||
"Unimplemented, please load the module with enable_tvm_ffi=True."
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Try to register_jit_arg_adapter for TensorAdapter
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -479,7 +926,6 @@ try: # Register for numpy.ndarray
|
||||
JitArgAdapterRegistry.register_jit_arg_adapter(numpy.ndarray)(TensorAdapter)
|
||||
except ImportError:
|
||||
pass # silent attempt, suppress error
|
||||
|
||||
try: # Register for torch.Tensor
|
||||
import torch
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from cutlass.cutlass_dsl import (
|
||||
T,
|
||||
cutlass_arith,
|
||||
_binary_op_type_promote,
|
||||
CuTeDSL,
|
||||
)
|
||||
from cutlass._mlir import ir
|
||||
import cutlass._mlir.dialects.cute as _cute_ir
|
||||
@@ -33,6 +34,7 @@ from .core import (
|
||||
_pack_coord,
|
||||
_pack_shape,
|
||||
_ComposedLayout,
|
||||
_ComposedLayoutWithInnerFunc,
|
||||
is_static,
|
||||
is_weakly_congruent,
|
||||
rank,
|
||||
@@ -250,7 +252,7 @@ class _Tensor(Tensor):
|
||||
val = tensor[0] # Error: sub-byte scalar dereference not supported
|
||||
"""
|
||||
if has_underscore(crd):
|
||||
return slice_(self.value, crd, loc=loc, ip=ip)
|
||||
return slice_(self, crd, loc=loc, ip=ip)
|
||||
elif isinstance(self.type, _cute_ir.CoordTensorType):
|
||||
res = _cute_ir.get_iter(
|
||||
slice_(self, crd, loc=loc, ip=ip).value, loc=loc, ip=ip
|
||||
@@ -448,7 +450,7 @@ class _Tensor(Tensor):
|
||||
|
||||
self._check_can_load_store()
|
||||
|
||||
res_vect = _cute_ir.memref_load_vec(self.value, row_major=True, loc=loc, ip=ip)
|
||||
res_vect = _cute_ir.memref_load_vec(self.value, loc=loc, ip=ip)
|
||||
if self.element_type is Boolean:
|
||||
assert res_vect.type.element_type == T.i8(), (
|
||||
f"Boolean tensor must be stored as i8 in memory, but got {res_vect.type.element_type}"
|
||||
@@ -501,9 +503,7 @@ class _Tensor(Tensor):
|
||||
# Implicit upcast to wider type
|
||||
new_data = self._cvt_to_dest(data, loc=loc, ip=ip)
|
||||
|
||||
return _cute_ir.memref_store_vec(
|
||||
new_data, self.value, row_major=True, loc=loc, ip=ip
|
||||
)
|
||||
return _cute_ir.memref_store_vec(new_data, self.value, loc=loc, ip=ip)
|
||||
|
||||
@dsl_user_op
|
||||
def fill(self, value: Numeric, *, loc=None, ip=None) -> None:
|
||||
@@ -555,6 +555,11 @@ class _Tensor(Tensor):
|
||||
):
|
||||
raise ValueError(f"{self} doesn't support load and store")
|
||||
|
||||
if self.type.is_swizzled:
|
||||
raise NotImplementedError(
|
||||
f"load & store swizzled memory is not supported yet: {self}"
|
||||
)
|
||||
|
||||
def _check_can_dereference(self):
|
||||
# Check for sub-byte types and raise error if needed
|
||||
if self.element_type.width % 8 != 0 and self.element_type is not Boolean:
|
||||
@@ -579,11 +584,16 @@ def make_tensor(
|
||||
evaluates coordinates by applying the layout mapping and dereferencing the engine
|
||||
at the resulting offset.
|
||||
|
||||
:param iterator: Engine component (pointer, iterator, or counting iterator) that provides
|
||||
data access capabilities
|
||||
:type iterator: Union[Pointer, IntTuple]
|
||||
:param iterator: Engine component that provides data access capabilities. Can be:
|
||||
- A pointer (Pointer type)
|
||||
- An integer or integer tuple for coordinate tensors
|
||||
- A shared memory descriptor (SmemDescType)
|
||||
:type iterator: Union[Pointer, IntTuple, ir.Value]
|
||||
:param layout: Layout component that defines the mapping from logical coordinates to
|
||||
physical offsets
|
||||
physical offsets. Can be:
|
||||
- A shape tuple that will be converted to a layout
|
||||
- A Layout object
|
||||
- A ComposedLayout object (must be a normal layout)
|
||||
:type layout: Union[Shape, Layout, ComposedLayout]
|
||||
:param loc: Source location for MLIR operation tracking, defaults to None
|
||||
:type loc: Optional[Location]
|
||||
@@ -592,30 +602,40 @@ def make_tensor(
|
||||
:return: A tensor object representing the composition E ∘ L
|
||||
:rtype: Tensor
|
||||
|
||||
:raises ValueError: If iterator type is not supported
|
||||
:raises TypeError: If iterator type is not a supported type
|
||||
:raises ValueError: If layout is a composed layout with customized inner functions
|
||||
|
||||
**Examples:**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Create a tensor with row-major layout
|
||||
# Create a tensor with row-major layout from a pointer
|
||||
ptr = make_ptr(Float32, base_ptr, AddressSpace.gmem)
|
||||
layout = make_layout((64, 128), stride=(128, 1))
|
||||
tensor = make_tensor(ptr, layout)
|
||||
|
||||
# Create a tensor with hierarchical layout
|
||||
# Create a tensor with hierarchical layout in shared memory
|
||||
smem_ptr = make_ptr(Float16, base_ptr, AddressSpace.smem)
|
||||
layout = make_layout(((128, 8), (1, 4, 1)), stride=((32, 1), (0, 8, 4096)))
|
||||
tensor = make_tensor(smem_ptr, layout)
|
||||
|
||||
# Create a coord tensor
|
||||
# Create a coordinate tensor
|
||||
layout = make_layout(2, stride=16 * E(0))
|
||||
tensor = make_tensor(5, layout)
|
||||
tensor = make_tensor(5, layout) # coordinate tensor with iterator starting at 5
|
||||
|
||||
Notes:
|
||||
- The engine (iterator) must support random access operations
|
||||
- Common engine types include raw pointers, arrays, and random-access iterators
|
||||
- The layout defines both the shape (logical dimensions) and stride (physical mapping)
|
||||
- Supports both direct coordinate evaluation T(c) and partial evaluation (slicing)
|
||||
- ComposedLayouts must be "normal" layouts (no inner functions)
|
||||
- For coordinate tensors, the iterator is converted to a counting sequence
|
||||
"""
|
||||
if isinstance(layout, _ComposedLayoutWithInnerFunc):
|
||||
raise ValueError(
|
||||
"CuTe DSL tensor does not support composed layouts with inner functions: {layout}"
|
||||
)
|
||||
|
||||
if not isinstance(layout, (Layout, ComposedLayout)):
|
||||
layout = make_layout(layout, loc=loc, ip=ip)
|
||||
# Automatic decay to normal layout
|
||||
@@ -964,6 +984,65 @@ def print_tensor(
|
||||
_cute_ir.print_view(tensor.value, verbose=verbose, is_signed=signed, loc=loc, ip=ip)
|
||||
|
||||
|
||||
def _get_row_and_col_map(col_maj_shape_1d: tuple, is_row_to_col: bool):
|
||||
"""
|
||||
Create an index mapping mask for converting between row-major and column-major vector ordering.
|
||||
"""
|
||||
|
||||
# create row-major layout with compact row-major stride
|
||||
if col_maj_shape_1d is None:
|
||||
raise ValueError("vector shape for row/col map cannot be None")
|
||||
if isinstance(col_maj_shape_1d, tuple):
|
||||
row_maj_shape_1d = tuple(reversed(flatten(col_maj_shape_1d)))
|
||||
else:
|
||||
# Single dimension
|
||||
row_maj_shape_1d = col_maj_shape_1d
|
||||
|
||||
flat_shape = flatten(row_maj_shape_1d)
|
||||
if isinstance(flat_shape, tuple):
|
||||
strides = []
|
||||
current_stride = 1
|
||||
flat_shape = tuple(reversed(flat_shape))
|
||||
for dim in flat_shape:
|
||||
strides.append(current_stride)
|
||||
current_stride *= dim
|
||||
row_maj_stride = tuple(reversed(strides))
|
||||
else:
|
||||
# Single dimension
|
||||
row_maj_stride = 1
|
||||
|
||||
row_maj_lay_1d = make_layout(row_maj_shape_1d, stride=row_maj_stride)
|
||||
|
||||
# get idx map
|
||||
shape_size = size(row_maj_shape_1d)
|
||||
mask = [0] * shape_size
|
||||
|
||||
for col_index in range(shape_size):
|
||||
row_index = crd2idx(col_index, row_maj_lay_1d)
|
||||
if is_row_to_col:
|
||||
mask[row_index] = col_index
|
||||
else:
|
||||
mask[col_index] = row_index
|
||||
|
||||
return mask
|
||||
|
||||
|
||||
def _row2col(vec: ir.Value, *, shape, loc=None, ip=None) -> ir.Value:
|
||||
"""
|
||||
Convert a vector or tensor from row-major order to column-major order.
|
||||
"""
|
||||
row_and_col_map = _get_row_and_col_map(shape, is_row_to_col=True)
|
||||
return vector.shuffle(vec, vec, row_and_col_map, loc=loc, ip=ip)
|
||||
|
||||
|
||||
def _col2row(vec: ir.Value, *, shape, loc=None, ip=None) -> ir.Value:
|
||||
"""
|
||||
Convert a vector or tensor from column-major order to row-major order.
|
||||
"""
|
||||
row_and_col_map = _get_row_and_col_map(shape, is_row_to_col=False)
|
||||
return vector.shuffle(vec, vec, row_and_col_map, loc=loc, ip=ip)
|
||||
|
||||
|
||||
def _infer_broadcast_shape(*shapes: Shape) -> Shape:
|
||||
"""
|
||||
Infer the broadcasted shape from multiple input shapes according to broadcasting rules.
|
||||
@@ -1129,6 +1208,32 @@ class TensorSSA(cutlass_arith.ArithValue):
|
||||
|
||||
return res
|
||||
|
||||
@dsl_user_op
|
||||
def apply_op(self, op, other, flip=False, *, loc=None, ip=None) -> "TensorSSA":
|
||||
"""
|
||||
Apply a binary operation to this tensor and another operand.
|
||||
|
||||
This is a public interface to the internal _apply_op method, providing
|
||||
a stable API for external users who need to apply custom operations.
|
||||
|
||||
Args:
|
||||
op: The operation function (e.g., operator.add, operator.mul, etc.)
|
||||
other: The other operand (TensorSSA, ArithValue, or scalar)
|
||||
flip: Whether to flip the operands (for right-hand operations)
|
||||
loc: MLIR location (optional)
|
||||
ip: MLIR insertion point (optional)
|
||||
|
||||
Returns:
|
||||
TensorSSA: The result of the operation
|
||||
|
||||
Example:
|
||||
>>> tensor1 = cute.Tensor(...)
|
||||
>>> tensor2 = cute.Tensor(...)
|
||||
>>> result = tensor1.apply_op(operator.add, tensor2)
|
||||
>>> # Equivalent to: tensor1 + tensor2
|
||||
"""
|
||||
return self._apply_op(op, other, flip=flip, loc=loc, ip=ip)
|
||||
|
||||
@dsl_user_op
|
||||
def broadcast_to(self, target_shape: Shape, *, loc=None, ip=None) -> "TensorSSA":
|
||||
"""
|
||||
@@ -1147,20 +1252,21 @@ class TensorSSA(cutlass_arith.ArithValue):
|
||||
|
||||
transform_leaf(_check_broadcast, shape, target_shape)
|
||||
|
||||
# convert TensorSSA col-major vec to row-m to be compatible with mlir vector ops
|
||||
row_major_vec = _col2row(self, shape=shape, loc=loc, ip=ip)
|
||||
# reshape to flatten N-D vector
|
||||
flat_shp = flatten_to_tuple(shape)
|
||||
temp_ty = ir.VectorType.get(list(flat_shp), self.dtype.mlir_type)
|
||||
temp_vect = vector.shape_cast(temp_ty, self, loc=loc, ip=ip)
|
||||
temp_vect = vector.shape_cast(temp_ty, row_major_vec, loc=loc, ip=ip)
|
||||
|
||||
# broadcast to result N-D vector
|
||||
flat_tgt_shp = flatten_to_tuple(target_shape)
|
||||
temp_tgt_ty = ir.VectorType.get(list(flat_tgt_shp), self.dtype.mlir_type)
|
||||
temp_tgt_vect = vector.broadcast(temp_tgt_ty, temp_vect, loc=loc, ip=ip)
|
||||
|
||||
res_1d_ty = ir.VectorType.get([size(target_shape)], self.dtype.mlir_type) # type: ignore
|
||||
res_1d_vect = vector.shape_cast(res_1d_ty, temp_tgt_vect, loc=loc, ip=ip)
|
||||
|
||||
return TensorSSA(res_1d_vect, target_shape, self.dtype)
|
||||
return self._build_result(
|
||||
temp_tgt_vect, target_shape, row_major=True, loc=loc, ip=ip
|
||||
)
|
||||
|
||||
@dsl_user_op
|
||||
def __pow__(self, other, *, loc=None, ip=None) -> "TensorSSA":
|
||||
@@ -1499,15 +1605,43 @@ class TensorSSA(cutlass_arith.ArithValue):
|
||||
assert isinstance(flat_crd, tuple) and is_static(flat_crd)
|
||||
return flat_shp, flat_crd
|
||||
|
||||
def _build_result(self, res_vect, res_shp, *, loc=None, ip=None):
|
||||
def _build_result(self, res_vect, res_shp, *, row_major=False, loc=None, ip=None):
|
||||
if isinstance(res_shp, ir.Value):
|
||||
raise ValueError(f"Expected static shape, but got {self._shape}")
|
||||
|
||||
# cast back to 1D vector
|
||||
res_1d_ty = ir.VectorType.get([size(res_shp)], self.type.element_type)
|
||||
res_1d_vect = vector.shape_cast(res_1d_ty, res_vect, loc=loc, ip=ip)
|
||||
|
||||
if row_major:
|
||||
res_1d_vect = _row2col(res_1d_vect, shape=res_shp, loc=loc, ip=ip)
|
||||
|
||||
return TensorSSA(res_1d_vect, res_shp, self.dtype)
|
||||
|
||||
@dsl_user_op
|
||||
def reshape(self, shape: Shape, *, loc=None, ip=None) -> "TensorSSA":
|
||||
"""Reshape the tensor to a new shape.
|
||||
|
||||
:param shape: The new shape to reshape to.
|
||||
:type shape: Shape
|
||||
:return: A new tensor with the same elements but with the new shape.
|
||||
:rtype: TensorSSA
|
||||
:raises NotImplementedError: If dynamic size is not supported
|
||||
:raises ValueError: If the new shape is not compatible with the current shape
|
||||
"""
|
||||
|
||||
cur_size = size(self.shape, loc=loc, ip=ip)
|
||||
shp_size = size(shape, loc=loc, ip=ip)
|
||||
|
||||
if type(shp_size) is not int:
|
||||
raise NotImplementedError(f"dynamic shape is not supported: {shape}")
|
||||
if cur_size != shp_size:
|
||||
raise ValueError(
|
||||
f"expected reshaped size to be the same: {self.shape} -> {shape}"
|
||||
)
|
||||
|
||||
return TensorSSA(self, shape, self.dtype)
|
||||
|
||||
@dsl_user_op
|
||||
def __getitem__(
|
||||
self, crd: Coord, *, loc=None, ip=None
|
||||
@@ -1562,9 +1696,11 @@ class TensorSSA(cutlass_arith.ArithValue):
|
||||
|
||||
flat_shp, flat_crd = self._flatten_shape_and_coord(crd, loc=loc, ip=ip)
|
||||
|
||||
# convert TensorSSA col-major vec to row-m to be compatible with mlir vector ops
|
||||
row_major_vec = _col2row(self, shape=self._shape, loc=loc, ip=ip)
|
||||
multi_dim_ty = ir.VectorType.get(list(flat_shp), self.type.element_type)
|
||||
# vector<NxTy> -> vector<N_0xN_1x...xTy>
|
||||
tmp_vect = vector.shape_cast(multi_dim_ty, self, loc=loc, ip=ip)
|
||||
tmp_vect = vector.shape_cast(multi_dim_ty, row_major_vec, loc=loc, ip=ip)
|
||||
|
||||
# Slice and keep dims matching `_` or None
|
||||
res_shp = slice_(self._shape, crd, loc=loc, ip=ip)
|
||||
@@ -1593,7 +1729,7 @@ class TensorSSA(cutlass_arith.ArithValue):
|
||||
|
||||
# Slice and keep dims matching `_` or None
|
||||
res_shp = slice_(self._shape, crd, loc=loc, ip=ip)
|
||||
return self._build_result(res_vect, res_shp, loc=loc, ip=ip)
|
||||
return self._build_result(res_vect, res_shp, row_major=True, loc=loc, ip=ip)
|
||||
|
||||
@dsl_user_op
|
||||
def to(self, dtype: Type[Numeric], *, loc=None, ip=None):
|
||||
@@ -1634,11 +1770,15 @@ class TensorSSA(cutlass_arith.ArithValue):
|
||||
src, dtype.signed, dtype.mlir_type, loc=loc, ip=ip
|
||||
)
|
||||
elif issubclass(src_dtype, Integer) and dtype.is_float:
|
||||
# fast conversion path for supported combinations
|
||||
# check if there is a fast conversion path for given data types and arch
|
||||
fast_cvt_func = None
|
||||
if src_dtype in (Int8, Uint8) and dtype == BFloat16:
|
||||
res_vect = cvt_i8_bf16_intrinsic(src, size(self.shape), loc=loc, ip=ip)
|
||||
fast_cvt_func = cvt_i8_bf16_intrinsic
|
||||
elif src_dtype == Int4 and dtype == BFloat16:
|
||||
res_vect = cvt_i4_bf16_intrinsic(src, size(self.shape), loc=loc, ip=ip)
|
||||
fast_cvt_func = cvt_i4_bf16_intrinsic
|
||||
arch = CuTeDSL._get_dsl().get_arch_enum()
|
||||
if fast_cvt_func is not None and arch in fast_cvt_func.supported_archs:
|
||||
res_vect = fast_cvt_func(src, size(self.shape), loc=loc, ip=ip)
|
||||
else:
|
||||
res_vect = cutlass_arith.itofp(
|
||||
src, src_dtype.signed, dtype.mlir_type, loc=loc, ip=ip
|
||||
@@ -1748,8 +1888,10 @@ class TensorSSA(cutlass_arith.ArithValue):
|
||||
assert depth(flat_shp) == 1 and depth(flat_prof) == 1
|
||||
assert rank(flat_shp) == rank(flat_prof)
|
||||
|
||||
# convert TensorSSA col-major vec to row-m to be compatible with mlir vector ops
|
||||
row_major_vec = _col2row(self, shape=self._shape, loc=loc, ip=ip)
|
||||
temp_ty = ir.VectorType.get(list(flat_shp), elem_type.mlir_type)
|
||||
temp_vect = vector.shape_cast(temp_ty, self, loc=loc, ip=ip)
|
||||
temp_vect = vector.shape_cast(temp_ty, row_major_vec, loc=loc, ip=ip)
|
||||
|
||||
red_dims = [i for i, x in enumerate(flat_prof) if x is not None]
|
||||
|
||||
@@ -1763,7 +1905,7 @@ class TensorSSA(cutlass_arith.ArithValue):
|
||||
|
||||
# Slice and keep dims matching `_` or None
|
||||
res_shp = slice_(self.shape, reduction_profile, loc=loc, ip=ip)
|
||||
return self._build_result(res_vect, res_shp, loc=loc, ip=ip)
|
||||
return self._build_result(res_vect, res_shp, row_major=True, loc=loc, ip=ip)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
|
||||
@@ -1057,7 +1057,7 @@ def tune(
|
||||
except NotImplementedError as e:
|
||||
logger.info(f" Encountered unimplemented error, abort execution: {e}")
|
||||
raise e
|
||||
except (ValueError, TypeError) as e:
|
||||
except (ValueError, TypeError, CantImplementError) as e:
|
||||
logger.info(f" Configuration parameter skipping: {e}")
|
||||
continue
|
||||
except Exception as e:
|
||||
@@ -1073,3 +1073,17 @@ def tune(
|
||||
logger.info(f"Best configuration: {best_config}, execution time: {min_time} us")
|
||||
logger.info(f"Total tuning time: {tuning_time} s")
|
||||
return best_config
|
||||
|
||||
|
||||
class CantImplementError(Exception):
|
||||
"""Exception raised when a function is not implemented."""
|
||||
|
||||
def __init__(self, message=None):
|
||||
self.message = message or "The current config is invalid/unsupported"
|
||||
super().__init__(self.message)
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
|
||||
def __repr__(self):
|
||||
return self.message
|
||||
|
||||
@@ -10,17 +10,79 @@
|
||||
# is strictly prohibited.
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import ForwardRef, Tuple, Union, Any, Type, List, Optional
|
||||
import ctypes
|
||||
from typing import ForwardRef, Tuple, Union, Any, Type, List, Optional, Literal
|
||||
from functools import lru_cache
|
||||
|
||||
from cutlass.base_dsl.typing import *
|
||||
|
||||
from cutlass._mlir import ir
|
||||
from cutlass._mlir.dialects.cute import AddressSpace, ConstrainedIntType
|
||||
from cutlass.base_dsl.typing import JitArgument
|
||||
|
||||
|
||||
Int = Union[int, Integer]
|
||||
|
||||
|
||||
class SymInt:
|
||||
def __init__(self, width: Literal[32, 64] = 32, *, divisibility=1):
|
||||
if width not in [32, 64]:
|
||||
raise ValueError(f"Unsupported width: {width}")
|
||||
self._width = width
|
||||
self._divisibility = divisibility
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self._width
|
||||
|
||||
@property
|
||||
def divisibility(self):
|
||||
return self._divisibility
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"?{{i{self._width} div={self._divisibility}}}"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.__str__()
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if not isinstance(other, SymInt):
|
||||
return False
|
||||
return all(
|
||||
[self._width == other._width, self._divisibility == other._divisibility]
|
||||
)
|
||||
|
||||
def __c_pointers__(self):
|
||||
return [ctypes.c_void_p(0).value]
|
||||
|
||||
def __get_mlir_types__(self) -> List[ir.Type]:
|
||||
res_ty = ir.Type.parse(
|
||||
f'!cute.int_tuple<"?{{i{self.width} div={self.divisibility}}}">'
|
||||
)
|
||||
return [res_ty]
|
||||
|
||||
def __new_from_mlir_values__(self, values) -> "SymInt":
|
||||
from .core import IntValue
|
||||
|
||||
if self.width == 32:
|
||||
return Int32(IntValue(values[0]))
|
||||
elif self.width == 64:
|
||||
return Int64(IntValue(values[0]))
|
||||
else:
|
||||
assert False, f"Unsupported width: {self.width}"
|
||||
return self
|
||||
def sym_int(width: Literal[32, 64] = 32, *, divisibility=1) -> SymInt:
|
||||
return SymInt(width, divisibility=divisibility)
|
||||
|
||||
|
||||
def sym_int32(divisibility=1) -> SymInt:
|
||||
return sym_int(32, divisibility=divisibility)
|
||||
|
||||
|
||||
def sym_int64(divisibility=1) -> SymInt:
|
||||
return sym_int(64, divisibility=divisibility)
|
||||
|
||||
|
||||
ScaledBasis = ForwardRef("ScaledBasis")
|
||||
|
||||
IntTuple = Union[Int, Tuple["IntTuple", ...]]
|
||||
@@ -288,7 +350,10 @@ def is_int_tuple(a) -> bool:
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Coord",
|
||||
"SymInt",
|
||||
"sym_int",
|
||||
"sym_int32",
|
||||
"sym_int64",
|
||||
"Numeric",
|
||||
"Integer",
|
||||
"Boolean",
|
||||
@@ -317,13 +382,14 @@ __all__ = [
|
||||
"Float6E3M2FN",
|
||||
"IntTuple",
|
||||
"Layout",
|
||||
"Pointer",
|
||||
"Coord",
|
||||
"Shape",
|
||||
"Stride",
|
||||
"Tensor",
|
||||
"Tile",
|
||||
"Tiler",
|
||||
"XTuple",
|
||||
"is_integer",
|
||||
"is_int_tuple",
|
||||
"Pointer",
|
||||
"Tensor",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
This module provides jit executor related classes for CUTLASS.
|
||||
"""
|
||||
import ctypes
|
||||
import functools
|
||||
import weakref
|
||||
import threading
|
||||
|
||||
import cuda.bindings.runtime as cuda_runtime
|
||||
import cuda.bindings.driver as cuda_driver
|
||||
|
||||
# Local modules imports
|
||||
from ..base_dsl.jit_executor import JitExecutor, ExecutionArgs, JitFunctionArtifacts
|
||||
from ..base_dsl.utils.logger import log
|
||||
from ..base_dsl.common import DSLCudaRuntimeError, DSLRuntimeError
|
||||
from ..base_dsl.typing import Int32
|
||||
|
||||
class CudaDialectJitModule:
|
||||
"""Holds the execution engine and cuda libraries."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
engine,
|
||||
capi_func,
|
||||
args_spec: ExecutionArgs,
|
||||
cuda_library: list["cuda_runtime.cudaLibrary_t"],
|
||||
):
|
||||
self.engine = engine
|
||||
self.capi_func = capi_func
|
||||
self.args_spec = args_spec
|
||||
self.cuda_library = cuda_library
|
||||
self._unloaded = False
|
||||
|
||||
def is_unloaded(self):
|
||||
return self._unloaded
|
||||
|
||||
def unload(self):
|
||||
try:
|
||||
for library in self.cuda_library:
|
||||
cuda_runtime.cudaLibraryUnload(library)
|
||||
self.cuda_library.clear()
|
||||
finally:
|
||||
self._unloaded = True
|
||||
|
||||
def __del__(self):
|
||||
self.unload()
|
||||
|
||||
|
||||
class CudaDialectJitCompiledFunction:
|
||||
"""Holds a compiled function and its module."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ir_module,
|
||||
engine,
|
||||
capi_func,
|
||||
args_spec,
|
||||
function_name,
|
||||
kernel_info,
|
||||
jit_time_profiling,
|
||||
jit_function_artifacts,
|
||||
prefix=None,
|
||||
load_from_binary=False,
|
||||
):
|
||||
self.ir_module = ir_module
|
||||
self.engine = engine
|
||||
self.capi_func = capi_func
|
||||
self.function_name = function_name
|
||||
self.kernel_info = kernel_info
|
||||
if args_spec is not None:
|
||||
self.args_spec = ExecutionArgs(args_spec, self.function_name)
|
||||
self.jit_time_profiling = jit_time_profiling
|
||||
self.prefix = prefix
|
||||
assert (
|
||||
isinstance(jit_function_artifacts, JitFunctionArtifacts)
|
||||
or jit_function_artifacts is None
|
||||
)
|
||||
self.artifacts = jit_function_artifacts
|
||||
self.load_from_binary = load_from_binary
|
||||
|
||||
# Set cuda result return type.
|
||||
# When execution engine/capi function is None, do not set the return type.
|
||||
if self.capi_func:
|
||||
self.capi_func.restype = ctypes.c_int32
|
||||
if self.args_spec:
|
||||
self.args_spec.args_spec.annotations["return"] = Int32
|
||||
|
||||
# This runtime state is stored here so that we can preserve the module
|
||||
# in the compiler cache. Callers can extend the lifetime of the module
|
||||
# by creating and retaining the executor.
|
||||
self.jit_module = None
|
||||
self._executor_lock = threading.RLock()
|
||||
self._default_executor = None
|
||||
|
||||
@property
|
||||
def __ptx__(self):
|
||||
"""Returns the PTX code of the JIT-compiled function."""
|
||||
return self.artifacts.PTX if self.artifacts is not None else None
|
||||
|
||||
@property
|
||||
def __cubin__(self):
|
||||
"""Returns the CUBIN data of the JIT-compiled function."""
|
||||
return self.artifacts.CUBIN if self.artifacts is not None else None
|
||||
|
||||
@property
|
||||
def __mlir__(self):
|
||||
"""Returns the MLIR code of the JIT-compiled function."""
|
||||
return self.artifacts.MLIR if self.artifacts is not None else None
|
||||
|
||||
@functools.cached_property
|
||||
def num_devices(self):
|
||||
"""Returns the number of CUDA devices available."""
|
||||
dev_err, devs = cuda_runtime.cudaGetDeviceCount()
|
||||
if dev_err != cuda_runtime.cudaError_t.cudaSuccess:
|
||||
raise DSLCudaRuntimeError(dev_err, cuda_runtime.cudaGetErrorName(dev_err))
|
||||
return devs
|
||||
|
||||
def _deserializer(self):
|
||||
"""Load the cuda library from the binary execution engine.
|
||||
@return: The list of cuda kernels.
|
||||
"""
|
||||
library = ctypes.c_void_p()
|
||||
pointer_to_library = ctypes.pointer(library)
|
||||
pointer_to_pointer_to_library = ctypes.pointer(pointer_to_library)
|
||||
err = ctypes.c_int32(0)
|
||||
pointer_to_err = ctypes.pointer(err)
|
||||
|
||||
# cuda init takes in a pointer to a cudaLibrary_t and returns
|
||||
# a i32 cudaError_t. It initialized (lazy loads) our cudaLibrary_t
|
||||
cuda_init = self.engine.lookup(f"_mlir_{self.prefix}_cuda_init")
|
||||
if cuda_init is None:
|
||||
raise DSLRuntimeError("cuda_init not found")
|
||||
cuda_init = ctypes.CFUNCTYPE(None, ctypes.c_void_p)(cuda_init)
|
||||
# cuda load takes in a pointer to a cudaLibrary_t and returns
|
||||
# a i32 cudaError_t. It loads the functions from the cuda library,
|
||||
# sets function attributes, and returns an error if encountered.
|
||||
cuda_load = self.engine.lookup(f"_mlir_{self.prefix}_cuda_load")
|
||||
if cuda_load is None:
|
||||
raise DSLRuntimeError("cuda_load not found")
|
||||
cuda_load = ctypes.CFUNCTYPE(None, ctypes.c_void_p)(cuda_load)
|
||||
|
||||
cuda_init_args = [pointer_to_pointer_to_library, pointer_to_err]
|
||||
packed_args = (ctypes.c_void_p * len(cuda_init_args))()
|
||||
for i in range(len(cuda_init_args)):
|
||||
packed_args[i] = ctypes.cast(cuda_init_args[i], ctypes.c_void_p)
|
||||
cuda_init(packed_args)
|
||||
|
||||
if err.value != 0:
|
||||
error_code = err.value
|
||||
error_name = cuda_runtime.cudaGetErrorName(
|
||||
cuda_runtime.cudaError_t(error_code)
|
||||
)
|
||||
raise DSLCudaRuntimeError(error_code, error_name)
|
||||
|
||||
cuda_load_args = [pointer_to_library, pointer_to_err]
|
||||
packed_args = (ctypes.c_void_p * len(cuda_load_args))()
|
||||
for i in range(len(cuda_load_args)):
|
||||
packed_args[i] = ctypes.cast(cuda_load_args[i], ctypes.c_void_p)
|
||||
cuda_load(packed_args)
|
||||
|
||||
if err.value != 0:
|
||||
error_code = err.value
|
||||
error_name = cuda_runtime.cudaGetErrorName(
|
||||
cuda_runtime.cudaError_t(error_code)
|
||||
)
|
||||
raise DSLCudaRuntimeError(error_code, error_name)
|
||||
|
||||
return [cuda_runtime.cudaLibrary_t(library.value)]
|
||||
|
||||
def _get_cuda_init_and_load(self):
|
||||
"""Returns the cuda init and load functions from the engine."""
|
||||
# cuda init takes in a pointer to a cudaLibrary_t and returns
|
||||
# a i32 cudaError_t. It initialized (lazy loads) our cudaLibrary_t
|
||||
|
||||
cuda_init = None
|
||||
|
||||
# cuda load for device takes in a pointer to a cudaLibrary_t and a device index and returns
|
||||
# a i32 cudaError_t. It resolves each kernel from the cuda library,
|
||||
# and applies device scoped attributes for the given device id, and returns an error if encountered.
|
||||
cuda_load_to_device = None
|
||||
|
||||
# When load_from_binary is true, the symbols are prefixed by _mlir_<prefix>_ and are looked
|
||||
# up from the JIT engine. Otherwise we look for the unprefixed forms. Looking up cuda_init
|
||||
# and cuda_load_to_device from the engine which are defined in CudaToLLVM.
|
||||
if self.load_from_binary:
|
||||
if self.prefix is None:
|
||||
raise DSLRuntimeError("prefix is required to be set for binary loading")
|
||||
cuda_init = self.engine.lookup(f"_mlir_{self.prefix}_cuda_init")
|
||||
if cuda_init is None:
|
||||
raise DSLRuntimeError(f"cuda_init not found for prefix {self.prefix}")
|
||||
cuda_load_to_device = self.engine.lookup(
|
||||
f"_mlir_{self.prefix}_cuda_load_to_device"
|
||||
)
|
||||
if cuda_load_to_device is None:
|
||||
raise DSLRuntimeError(
|
||||
f"cuda_load_to_device not found for prefix {self.prefix}"
|
||||
)
|
||||
else:
|
||||
cuda_init = self.engine.raw_lookup("cuda_init")
|
||||
if cuda_init is None:
|
||||
raise DSLRuntimeError("cuda_init not found")
|
||||
cuda_load_to_device = self.engine.raw_lookup("cuda_load_to_device")
|
||||
if cuda_load_to_device is None:
|
||||
raise DSLRuntimeError("cuda_load_to_device not found")
|
||||
cuda_init = ctypes.CFUNCTYPE(ctypes.c_int32, ctypes.c_void_p)(cuda_init)
|
||||
cuda_load_to_device = ctypes.CFUNCTYPE(ctypes.c_int32, ctypes.c_void_p)(
|
||||
cuda_load_to_device
|
||||
)
|
||||
|
||||
return cuda_init, cuda_load_to_device
|
||||
|
||||
def _load_cuda_library(self):
|
||||
"""Loads the CUDA library from the engine."""
|
||||
|
||||
cuda_init, cuda_load_to_device = self._get_cuda_init_and_load()
|
||||
|
||||
library = ctypes.c_void_p()
|
||||
pointer_to_library = ctypes.pointer(library)
|
||||
pointer_to_pointer_to_library = ctypes.pointer(pointer_to_library)
|
||||
err = ctypes.c_int32(0)
|
||||
pointer_to_err = ctypes.pointer(err)
|
||||
|
||||
cuda_init_args = [pointer_to_pointer_to_library, pointer_to_err]
|
||||
packed_args = (ctypes.c_void_p * len(cuda_init_args))()
|
||||
for i in range(len(cuda_init_args)):
|
||||
packed_args[i] = ctypes.cast(cuda_init_args[i], ctypes.c_void_p)
|
||||
cuda_init(packed_args)
|
||||
|
||||
if err.value != 0:
|
||||
error_code = err.value
|
||||
error_name = cuda_runtime.cudaGetErrorName(
|
||||
cuda_runtime.cudaError_t(error_code)
|
||||
)
|
||||
raise DSLCudaRuntimeError(error_code, error_name)
|
||||
|
||||
device_id = ctypes.c_int32(0)
|
||||
pointer_to_device_id = ctypes.pointer(device_id)
|
||||
|
||||
cuda_load_args = [pointer_to_library, pointer_to_device_id, pointer_to_err]
|
||||
packed_args = (ctypes.c_void_p * len(cuda_load_args))()
|
||||
for i, arg in enumerate(cuda_load_args):
|
||||
packed_args[i] = ctypes.cast(arg, ctypes.c_void_p)
|
||||
|
||||
for dev in range(self.num_devices):
|
||||
device_id.value = dev
|
||||
cuda_load_to_device(packed_args)
|
||||
if err.value != 0:
|
||||
raise DSLCudaRuntimeError(
|
||||
err.value,
|
||||
cuda_runtime.cudaGetErrorName(cuda_runtime.cudaError_t(err.value)),
|
||||
)
|
||||
|
||||
if err.value != 0:
|
||||
error_code = err.value
|
||||
error_name = cuda_runtime.cudaGetErrorName(
|
||||
cuda_runtime.cudaError_t(error_code)
|
||||
)
|
||||
raise DSLCudaRuntimeError(error_code, error_name)
|
||||
|
||||
return [cuda_runtime.cudaLibrary_t(library.value)]
|
||||
|
||||
def to(self, device=None) -> JitExecutor:
|
||||
"""Returns an executable function bound to the given device.
|
||||
|
||||
For multi-device execution this method can be called for each device where
|
||||
the kernel will run.
|
||||
|
||||
Since CudaJitCompiledFunction uses CUDA libraries, which are context free,
|
||||
binding to a device is not necessary and the device is ignored. Device is
|
||||
kept in for compatibility with the JitCompiledFunction.
|
||||
|
||||
:param device: Specifies the device for the executor. If None the current device is used.
|
||||
:type device: Optional[Union[int, CUdevice]]
|
||||
:return: A callable executor function.
|
||||
:rtype: JitExecutor
|
||||
"""
|
||||
with self._executor_lock:
|
||||
# We need to ensure that the modules are loaded if not already
|
||||
if self.jit_module is None or self.jit_module.is_unloaded():
|
||||
cuda_library = self._load_cuda_library()
|
||||
self.jit_module = CudaDialectJitModule(
|
||||
self.engine,
|
||||
self.capi_func,
|
||||
self.args_spec,
|
||||
cuda_library,
|
||||
)
|
||||
|
||||
return JitExecutor(self.jit_module, None, self.jit_time_profiling)
|
||||
|
||||
def generate_execution_args(self, *args, **kwargs):
|
||||
return self.args_spec.generate_execution_args(args, kwargs)
|
||||
|
||||
def set_dynamic_args(self, dynamic_args, dynamic_kwargs):
|
||||
"""Sets the dynamic argument information required for export to c code generation."""
|
||||
self.dynamic_args = dynamic_args
|
||||
self.dynamic_kwargs = dynamic_kwargs
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Executes the jit-compiled function under the currently active CUDA context.
|
||||
|
||||
Calling this method multiple devices is not allowed and will result in unexpected
|
||||
CUDA errors. If you need to call the kernel on multiple devices use `to`
|
||||
to return a per-device function.
|
||||
"""
|
||||
exe_args, adapted_args = self.generate_execution_args(*args, **kwargs)
|
||||
return self.run_compiled_program(exe_args)
|
||||
|
||||
def run_compiled_program(self, exe_args):
|
||||
"""Executes the jit-compiled function under the currently active CUDA context.
|
||||
|
||||
Calling this method multiple devices is not allowed and will result in unexpected
|
||||
CUDA errors. If you need to call the kernel on multiple devices use `to`
|
||||
to return a per-device function.
|
||||
"""
|
||||
with self._executor_lock:
|
||||
if self._default_executor is None:
|
||||
log().debug("Creating default executor.")
|
||||
# We use a weak reference here so that this instance does not keep this
|
||||
# object alive as it hold a reference to self.
|
||||
proxy_self = weakref.proxy(self)
|
||||
self._default_executor = proxy_self.to(None)
|
||||
return self._default_executor.run_compiled_program(exe_args)
|
||||
@@ -0,0 +1,44 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
This module provides CUDA Python helper functions
|
||||
"""
|
||||
|
||||
import cuda.bindings.driver as cuda_driver
|
||||
|
||||
# MLIR imports
|
||||
from .._mlir import ir
|
||||
from .._mlir.dialects import cuda
|
||||
|
||||
# Local module imports
|
||||
from ..base_dsl.runtime.jit_arg_adapters import JitArgAdapterRegistry
|
||||
|
||||
|
||||
@JitArgAdapterRegistry.register_jit_arg_adapter(cuda_driver.CUstream)
|
||||
class CudaDialectStreamAdapter:
|
||||
"""
|
||||
Convert a CUDA stream to a stream representation for JIT arg generation.
|
||||
"""
|
||||
|
||||
def __init__(self, arg):
|
||||
self._arg = arg
|
||||
self._c_pointer = self._arg.getPtr()
|
||||
|
||||
def __new_from_mlir_values__(self, values):
|
||||
assert len(values) == 1
|
||||
return values[0]
|
||||
|
||||
def __c_pointers__(self):
|
||||
return [self._c_pointer]
|
||||
|
||||
def __get_mlir_types__(self):
|
||||
return [cuda.StreamType.get()]
|
||||
@@ -43,11 +43,21 @@ from ..base_dsl.typing import *
|
||||
from ..base_dsl.typing import DynamicExpression, get_mlir_types
|
||||
from ..base_dsl.runtime.jit_arg_adapters import is_arg_spec_constexpr
|
||||
from ..base_dsl.runtime import cuda as cuda_helpers
|
||||
from .cuda_stream_adapter import CudaDialectStreamAdapter
|
||||
|
||||
from .cuda_jit_executor import CudaDialectJitCompiledFunction
|
||||
|
||||
# MLIR Imports
|
||||
from cutlass._mlir import ir, execution_engine, passmanager
|
||||
from cutlass._mlir.dialects import arith, func, gpu, scf, cute, gpu as cutlass_gpu
|
||||
from cutlass._mlir.dialects import (
|
||||
arith,
|
||||
func,
|
||||
gpu,
|
||||
scf,
|
||||
cute,
|
||||
gpu as cutlass_gpu,
|
||||
cuda as cuda_dialect,
|
||||
)
|
||||
from cutlass._mlir.dialects._ods_common import (
|
||||
get_op_result_or_op_results as _get_op_result_or_op_results,
|
||||
)
|
||||
@@ -220,6 +230,10 @@ class CutlassBaseDSL(BaseDSL):
|
||||
preprocess=preprocess,
|
||||
)
|
||||
self._smem_usage_tracker: tuple = None
|
||||
# extra function to convert cute arguments to tvm ffi spec params
|
||||
# this needs to be reverse registered because the arg convention
|
||||
# depends on the runtime type of the DSL arguments
|
||||
self._tvm_ffi_args_spec_converter = None
|
||||
|
||||
# this method is not useful for cutlass_dsl, so we only provide a dummy implementation.
|
||||
def _is_tensor_descriptor(self, maybe_tensor_descriptor) -> bool:
|
||||
@@ -241,7 +255,7 @@ class CutlassBaseDSL(BaseDSL):
|
||||
|
||||
def _get_pipeline(self, pipeline):
|
||||
pipeline = super()._get_pipeline(pipeline)
|
||||
if pipeline == None:
|
||||
if pipeline is None:
|
||||
# cubin format is required to be cubin as we launch cuda module at python level.
|
||||
return (
|
||||
"builtin.module(cute-to-nvvm{cubin-format=bin "
|
||||
@@ -253,7 +267,10 @@ class CutlassBaseDSL(BaseDSL):
|
||||
|
||||
def preprocess_pipeline(self, pipeline, arch) -> str:
|
||||
pipeline = super().preprocess_pipeline(pipeline, arch)
|
||||
pipeline = pipeline.rstrip(")") + ",external-kernel-for-gpu-launch)"
|
||||
pipeline = (
|
||||
pipeline.rstrip("})")
|
||||
+ " enable-cuda-dialect=true cuda-dialect-external-module=true})"
|
||||
)
|
||||
return pipeline
|
||||
|
||||
def _enter_gpu_module(self):
|
||||
@@ -265,13 +282,35 @@ class CutlassBaseDSL(BaseDSL):
|
||||
)
|
||||
|
||||
ret = {}
|
||||
# generate launch bound attr from LaunchConfig
|
||||
max_threads = ", ".join(map(str, config.block))
|
||||
ret["nvvm.reqntid"] = ir.Attribute.parse(f"array<i32 : {max_threads}>")
|
||||
# min_blocks_per_mp is optional for kernel
|
||||
min_blocks = config.min_blocks_per_mp
|
||||
if min_blocks > 0:
|
||||
ret["nvvm.minctasm"] = ir.Attribute.parse(f"{min_blocks} : i32")
|
||||
if config.has_max_number_threads():
|
||||
block_str = ", ".join(map(str, config.block))
|
||||
has_dynamic = any(is_dynamic_expression(dim) for dim in config.block)
|
||||
if not has_dynamic:
|
||||
# Case 1. Auto-generate reqntid from block size
|
||||
ret["nvvm.reqntid"] = ir.Attribute.parse(f"array<i32 : {block_str}>")
|
||||
else:
|
||||
# Case 2. Don't auto-generate reqntid from block size
|
||||
self.print_warning_once(
|
||||
f"Dynamic variable in block size {block_str}, cannot auto-generate `nvvm.reqntid`"
|
||||
)
|
||||
else:
|
||||
# Case 3. Use user-defined value for maxntid
|
||||
max_ntid_str = ", ".join(map(str, config.max_number_threads))
|
||||
for value in config.max_number_threads:
|
||||
if is_dynamic_expression(value):
|
||||
raise DSLRuntimeError(
|
||||
f"User defined max number threads `{max_ntid_str}` contains dynamic expression, cannot generate `nvvm.maxntid`",
|
||||
suggestion="Consider using `Constexpr` annotation or Python constant",
|
||||
)
|
||||
# Use user-defined maxntid
|
||||
ret["nvvm.maxntid"] = ir.Attribute.parse(f"array<i32 : {max_ntid_str}>")
|
||||
|
||||
# Add optional minimum blocks per multiprocessor
|
||||
if config.min_blocks_per_mp > 0:
|
||||
ret["nvvm.minctasm"] = ir.Attribute.parse(
|
||||
f"{config.min_blocks_per_mp} : i32"
|
||||
)
|
||||
|
||||
return ret
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
@@ -313,6 +352,116 @@ class CutlassBaseDSL(BaseDSL):
|
||||
|
||||
return version_hash
|
||||
|
||||
def get_return_types(self) -> List[ir.Type]:
|
||||
"""
|
||||
Get the return types of the function.
|
||||
With cuda dialect, the return type is i32 to indicate cuda result.
|
||||
"""
|
||||
i32_ty = ir.IntegerType.get_signless(32)
|
||||
return [i32_ty]
|
||||
|
||||
def generate_default_return_values(self, ip=None) -> List[ir.Value]:
|
||||
"""
|
||||
Generate the default return values of the function.
|
||||
With cuda dialect, the default return value is 0 to indicate success.
|
||||
"""
|
||||
if ip is None:
|
||||
raise DSLRuntimeError("ip is required to generate default return values")
|
||||
return_types = self.get_return_types()
|
||||
if len(return_types) != 1:
|
||||
raise DSLRuntimeError(
|
||||
f"Expected exactly one return type, but got {len(return_types)}"
|
||||
)
|
||||
return_type = return_types[0]
|
||||
with ip:
|
||||
zero = arith.ConstantOp(return_type, 0).result
|
||||
return [zero]
|
||||
|
||||
def compile_and_cache(
|
||||
self,
|
||||
module,
|
||||
module_hash,
|
||||
function_name,
|
||||
pipeline,
|
||||
args_spec,
|
||||
no_cache,
|
||||
*,
|
||||
dynamic_args=None,
|
||||
dynamic_kwargs=None,
|
||||
original_function_name=None,
|
||||
):
|
||||
"""
|
||||
Compile the module and cache the result.
|
||||
|
||||
:param module: The MLIR module to compile.
|
||||
:param module_hash: The hash of the MLIR module.
|
||||
:param function_name: The name of the function to compile.
|
||||
:param pipeline: The pipeline to use for compilation.
|
||||
:param args_spec: The args spec to use for compilation.
|
||||
:param no_cache: Whether to cache the result.
|
||||
:param dynamic_args: The dynamic arguments to use for compilation.
|
||||
:param dynamic_kwargs: The dynamic keyword arguments to use for compilation.
|
||||
:param original_function_name: The name of the original function without mangling.
|
||||
:return: The compiled function.
|
||||
"""
|
||||
if self.compile_options.enable_tvm_ffi:
|
||||
# TVM FFI post compile logic
|
||||
# attach extra ABI function to the MLIR module
|
||||
from .tvm_ffi_provider import (
|
||||
TVMFFIJitCompiledFunction,
|
||||
TVMFFICuteCallProvider,
|
||||
)
|
||||
from cutlass.base_dsl.tvm_ffi_builder import attach_ffi_func
|
||||
|
||||
assert self._tvm_ffi_args_spec_converter is not None
|
||||
tvm_ffi_spec_params = self._tvm_ffi_args_spec_converter(
|
||||
function_name, args_spec, dynamic_args, dynamic_kwargs
|
||||
)
|
||||
tvm_ffi_provider = TVMFFICuteCallProvider(function_name)
|
||||
|
||||
# ensure we run the postprocessor hook after the compiler has run its passes
|
||||
def post_compile_hook(module: ir.Module):
|
||||
with module.context, module.operation.location:
|
||||
# attach the tvm ffi function to the mlir module
|
||||
attach_ffi_func(
|
||||
module,
|
||||
function_name,
|
||||
tvm_ffi_spec_params,
|
||||
tvm_ffi_provider,
|
||||
fn_display_name=original_function_name,
|
||||
)
|
||||
module.operation.verify()
|
||||
|
||||
# ensure the compiler can run post-compile hook after its passes
|
||||
# the context will restore the previous post-compile hook after it exits
|
||||
with compiler.PostCompileHookContext(
|
||||
self.compiler_provider, post_compile_hook
|
||||
):
|
||||
return super().compile_and_cache(
|
||||
module,
|
||||
module_hash,
|
||||
function_name,
|
||||
pipeline,
|
||||
args_spec,
|
||||
no_cache,
|
||||
TVMFFIJitCompiledFunction,
|
||||
dynamic_args=dynamic_args,
|
||||
dynamic_kwargs=dynamic_kwargs,
|
||||
)
|
||||
|
||||
return super().compile_and_cache(
|
||||
module,
|
||||
module_hash,
|
||||
function_name,
|
||||
pipeline,
|
||||
args_spec,
|
||||
no_cache,
|
||||
CudaDialectJitCompiledFunction,
|
||||
dynamic_args=dynamic_args,
|
||||
dynamic_kwargs=dynamic_kwargs,
|
||||
original_function_name=original_function_name,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def track_smem_allocator(allocator, callback):
|
||||
"""
|
||||
@@ -343,6 +492,103 @@ class CutlassBaseDSL(BaseDSL):
|
||||
allocator, callback = self._smem_usage_tracker
|
||||
return callback(allocator)
|
||||
|
||||
@staticmethod
|
||||
def cuda_launch_func(
|
||||
stream: Union[list, tuple],
|
||||
kernel,
|
||||
grid_size_x,
|
||||
grid_size_y,
|
||||
grid_size_z,
|
||||
block_size_x,
|
||||
block_size_y,
|
||||
block_size_z,
|
||||
kernel_operands,
|
||||
*,
|
||||
cluster_size_x=None,
|
||||
cluster_size_y=None,
|
||||
cluster_size_z=None,
|
||||
dynamic_shared_memory_size=None,
|
||||
use_pdl=False,
|
||||
loc=None,
|
||||
ip=None,
|
||||
):
|
||||
|
||||
# Max number of attributes in the launch config is set to 2 for PDL and cluster size
|
||||
max_num_attributes = 2
|
||||
launch_config_type = cuda_dialect.LaunchConfigType.get(max_num_attributes)
|
||||
|
||||
if len(stream) == 0:
|
||||
stream = cuda_dialect.CastOp(
|
||||
src=Int64(0).ir_value(),
|
||||
dst=cuda_dialect.StreamType.get(),
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
else:
|
||||
stream = stream[0]
|
||||
|
||||
dynamic_shared_memory_size = Int64(dynamic_shared_memory_size).ir_value()
|
||||
block_size_x = Int32(block_size_x).ir_value()
|
||||
block_size_y = Int32(block_size_y).ir_value()
|
||||
block_size_z = Int32(block_size_z).ir_value()
|
||||
grid_size_x = Int32(grid_size_x).ir_value()
|
||||
grid_size_y = Int32(grid_size_y).ir_value()
|
||||
grid_size_z = Int32(grid_size_z).ir_value()
|
||||
|
||||
cfg = cuda_dialect.launch_cfg_create(
|
||||
# Launch config type
|
||||
launch_config_type,
|
||||
# Max num of attributes the launch config can hold
|
||||
# set to 2 for PDL and cluster size
|
||||
ir.IntegerAttr.get(ir.IntegerType.get_signless(32), max_num_attributes),
|
||||
block_size_x,
|
||||
block_size_y,
|
||||
block_size_z,
|
||||
dynamic_shared_memory_size,
|
||||
grid_size_x,
|
||||
grid_size_y,
|
||||
grid_size_z,
|
||||
stream,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
if use_pdl:
|
||||
cuda_dialect.launch_cfg_programmatic_stream_serialization_allowed(
|
||||
cfg, Int32(use_pdl).ir_value(), loc=loc, ip=ip
|
||||
)
|
||||
|
||||
if cluster_size_x is not None:
|
||||
if cluster_size_y is None:
|
||||
cluster_size_y = 1
|
||||
if cluster_size_z is None:
|
||||
cluster_size_z = 1
|
||||
cluster_size_x = Int32(cluster_size_x).ir_value(loc=loc, ip=ip)
|
||||
cluster_size_y = Int32(cluster_size_y).ir_value(loc=loc, ip=ip)
|
||||
cluster_size_z = Int32(cluster_size_z).ir_value(loc=loc, ip=ip)
|
||||
cuda_dialect.launch_cfg_cluster_dim(
|
||||
cfg, cluster_size_x, cluster_size_y, cluster_size_z, loc=loc, ip=ip
|
||||
)
|
||||
|
||||
op = cuda_dialect.launch_ex(
|
||||
cuda_dialect.ResultType.get(),
|
||||
kernel,
|
||||
cfg,
|
||||
kernel_operands,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
res_i32 = cuda_dialect.CastOp(
|
||||
src=op,
|
||||
dst=cuda_dialect.IntegerType.get_signless(32),
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
cuda_dialect.return_if_error([res_i32], loc=loc, ip=ip)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def gpu_launch_func(
|
||||
async_token,
|
||||
@@ -396,20 +642,32 @@ class CutlassBaseDSL(BaseDSL):
|
||||
|
||||
def generate_func_op(self, arg_types, arg_attrs, kernel_name, loc=None):
|
||||
super().generate_func_op(arg_types, arg_attrs, kernel_name)
|
||||
self.func_op = func.FuncOp(
|
||||
self.func_op = cuda_dialect.KernelOp(
|
||||
kernel_name, ir.FunctionType.get(arg_types, []), loc=loc
|
||||
)
|
||||
self.arg_types = arg_types
|
||||
self.func_op.attributes["cu_attrs"] = ir.DictAttr.get(
|
||||
{
|
||||
str(
|
||||
cuda_dialect.CUFunctionAttribute.non_portable_cluster_size_allowed
|
||||
): ir.IntegerAttr.get(ir.IntegerType.get_signless(32), 1),
|
||||
str(
|
||||
cuda_dialect.CUFunctionAttribute.max_dynamic_shared_size_bytes
|
||||
): cuda_dialect.DevMaxSharedMemoryOptinAttr.get(),
|
||||
}
|
||||
)
|
||||
if arg_attrs is not None:
|
||||
log().debug(arg_attrs)
|
||||
self.func_op.arg_attrs = arg_attrs
|
||||
return self.func_op
|
||||
|
||||
def generate_func_ret_op(self):
|
||||
return func.ReturnOp([])
|
||||
def generate_func_ret_op(self, loc=None, ip=None):
|
||||
return cuda_dialect.ReturnOp([], loc=loc, ip=ip)
|
||||
|
||||
def get_func_body_start(self):
|
||||
assert self.func_op is not None, "Invalid func_op is not expected!"
|
||||
return self.func_op.add_entry_block()
|
||||
arg_locs = [self.func_op.operation.location for _ in self.arg_types]
|
||||
return self.func_op.add_entry_block(arg_locs=arg_locs)
|
||||
|
||||
def generate_launch_op(self, *args, **kwargs):
|
||||
# Extract args and do validation
|
||||
@@ -430,12 +688,6 @@ class CutlassBaseDSL(BaseDSL):
|
||||
|
||||
cfg = requiredArgs.config
|
||||
|
||||
# Apply to grid, block, and cluster if present
|
||||
cfg.grid = [to_index(size) for size in cfg.grid]
|
||||
cfg.block = [to_index(size) for size in cfg.block]
|
||||
if cfg.has_cluster:
|
||||
cfg.cluster = [to_index(size) for size in cfg.cluster]
|
||||
|
||||
smem_usage = self.dsl._get_smem_usage()
|
||||
if any(not isinstance(x, int) for x in [cfg.smem, smem_usage]):
|
||||
pass # cannot compare dynamic value inside kernel to launch op in py
|
||||
@@ -449,12 +701,11 @@ class CutlassBaseDSL(BaseDSL):
|
||||
)
|
||||
cfg.smem = const(cfg.smem)
|
||||
|
||||
async_deps = cfg.async_deps
|
||||
if not isinstance(cfg.async_deps, (list, tuple)):
|
||||
cfg.async_deps = [cfg.async_deps]
|
||||
is_async = len(cfg.async_deps) > 0
|
||||
token = CutlassBaseDSL.gpu_launch_func(
|
||||
gpu.AsyncTokenType.get() if is_async else None,
|
||||
cfg.async_deps,
|
||||
async_deps = [cfg.async_deps]
|
||||
CutlassBaseDSL.cuda_launch_func(
|
||||
async_deps,
|
||||
kernelSym,
|
||||
*cfg.grid,
|
||||
*cfg.block,
|
||||
@@ -469,7 +720,7 @@ class CutlassBaseDSL(BaseDSL):
|
||||
use_pdl=cfg.use_pdl,
|
||||
loc=loc,
|
||||
)
|
||||
return token if is_async else None
|
||||
return None
|
||||
|
||||
return KernelLauncher(
|
||||
self,
|
||||
@@ -526,6 +777,10 @@ class CutlassBaseDSL(BaseDSL):
|
||||
# skip generic types such as List[int], Tuple[int, int], etc. for performance consideration?
|
||||
pass
|
||||
|
||||
elif getattr(arg_annotation, "__name__", None) == "CUstream" and arg.__class__.__name__ == "_FakeStream":
|
||||
# allow FakeStream to be passed as CUDA stream
|
||||
pass
|
||||
|
||||
elif isinstance(arg_annotation, type):
|
||||
# Handle simple type annotations
|
||||
if not isinstance(arg, arg_annotation) and arg is not None:
|
||||
@@ -564,7 +819,23 @@ class CutlassBaseDSL(BaseDSL):
|
||||
# Handle dynamic types
|
||||
jit_arg_type.extend([v.type for v in dyn_vals])
|
||||
jit_arg_attr.extend([default_attr] * len(dyn_vals))
|
||||
jit_exec_arg.extend(get_c_pointers(arg) if is_host else dyn_vals)
|
||||
if is_host and self.envar.enable_tvm_ffi:
|
||||
import tvm_ffi
|
||||
|
||||
jit_exec_arg.extend(
|
||||
[
|
||||
tvm_ffi.Shape(
|
||||
[
|
||||
v.value if isinstance(v, Numeric) else v
|
||||
for v in arg
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
else:
|
||||
jit_exec_arg.extend(
|
||||
get_c_pointers(arg) if is_host else dyn_vals
|
||||
)
|
||||
else:
|
||||
jit_exec_arg = jit_arg_type = jit_arg_attr = None
|
||||
elif not hasattr(arg, "__extract_mlir_values__") and not hasattr(
|
||||
@@ -879,7 +1150,6 @@ def to_index(value):
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def _validate_iter_args_structure(iter_args, ir_values):
|
||||
"""
|
||||
Validates that iter_args structure contains the same number of atomic values
|
||||
@@ -938,6 +1208,15 @@ def _minmax(op, *args, loc=None, ip=None):
|
||||
emitter = getattr(cutlass_arith, f"_{op.__name__}")
|
||||
if not (is_dynamic_expression(res) or is_dynamic_expression(x)):
|
||||
res = emitter(op(res), op(x))
|
||||
elif (
|
||||
hasattr(res, "type")
|
||||
and hasattr(x, "type")
|
||||
and isinstance(res.type, T.VectorType)
|
||||
and isinstance(x.type, T.VectorType)
|
||||
):
|
||||
lhs = op(res, loc=loc, ip=ip)
|
||||
rhs = op(x, loc=loc, ip=ip)
|
||||
return lhs.apply_op(emitter, rhs, loc=loc, ip=ip)
|
||||
else:
|
||||
lhs = as_numeric(op(res, loc=loc, ip=ip))
|
||||
rhs = as_numeric(op(x, loc=loc, ip=ip))
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
# 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.base_dsl.tvm_ffi_builder import (
|
||||
DynamicParamPackCallProvider,
|
||||
CallContext,
|
||||
rename_tvm_ffi_function,
|
||||
spec,
|
||||
)
|
||||
from cutlass._mlir import ir
|
||||
from cutlass._mlir.dialects import llvm
|
||||
from cutlass._mlir._mlir_libs import _execution_engine_extra
|
||||
from cutlass.cutlass_dsl.cuda_jit_executor import CudaDialectJitCompiledFunction
|
||||
from cutlass.base_dsl.common import DSLRuntimeError
|
||||
import tvm_ffi
|
||||
|
||||
|
||||
class TVMFFICuteCallProvider(DynamicParamPackCallProvider):
|
||||
"""Cute call provider that uses cute call convention."""
|
||||
|
||||
def __init__(self, target_func: str):
|
||||
super().__init__(target_func, struct_call=True)
|
||||
self.cuda_global_state_symbol = f"__{target_func}_cuda_state"
|
||||
|
||||
def get_callee_struct_for_param_tensor(
|
||||
self,
|
||||
param: spec.Tensor,
|
||||
current_block: ir.Block,
|
||||
data: ir.Value,
|
||||
shape: list[ir.Value],
|
||||
strides: list[ir.Value],
|
||||
flatten_struct: ir.Type,
|
||||
) -> ir.Type:
|
||||
"""Routine used to override the tensor passing struct convention"""
|
||||
with ir.InsertionPoint(current_block):
|
||||
data_type = self.gpu_ptr_type
|
||||
strides_type = (
|
||||
self.struct_type(fields=[x.type for x in strides])
|
||||
if len(strides) != 1
|
||||
else strides[0].type
|
||||
)
|
||||
shape_type = (
|
||||
self.struct_type(fields=[x.type for x in shape])
|
||||
if len(shape) != 1
|
||||
else shape[0].type
|
||||
)
|
||||
shape_stride_tuple_type = self.struct_type(
|
||||
fields=[shape_type, strides_type]
|
||||
)
|
||||
tensor_type = self.struct_type(fields=[data_type, shape_stride_tuple_type])
|
||||
return tensor_type
|
||||
|
||||
def pack_param_shape(
|
||||
self, current_block: ir.Block, context: CallContext, param: spec.Shape
|
||||
) -> tuple[tuple[ir.Type], tuple[ir.Value]]:
|
||||
"""Pack a shape parameter to a struct."""
|
||||
allocas: list[ir.Value] = []
|
||||
arg_types: list[ir.Type] = []
|
||||
for dim in param.shape:
|
||||
if isinstance(dim, spec.Var):
|
||||
allocas.append(
|
||||
self.pack_values_to_alloca(
|
||||
current_block,
|
||||
context.entry_block,
|
||||
[context.matched_var_binding[dim]],
|
||||
)[1]
|
||||
)
|
||||
arg_types.append(context.matched_var_binding[dim].type)
|
||||
return tuple(arg_types), tuple(allocas)
|
||||
|
||||
def declare_extern_funcs(self, current_block: ir.Block, context: CallContext):
|
||||
"""Append the error handling function to the current block."""
|
||||
with ir.InsertionPoint(context.module.body):
|
||||
self.declare_extern_func(
|
||||
"cuda_dialect_get_error_name",
|
||||
[self.i32_type],
|
||||
self.ptr_type,
|
||||
)
|
||||
self.declare_extern_func(
|
||||
"_cudaSetDevice",
|
||||
[self.i32_type],
|
||||
self.i32_type,
|
||||
)
|
||||
self.declare_extern_func(
|
||||
"cuda_dialect_init_library_once",
|
||||
[self.ptr_type, self.ptr_type, self.ptr_type, self.ptr_type],
|
||||
self.i32_type,
|
||||
)
|
||||
self.declare_extern_func(
|
||||
"cuda_dialect_unload_library_once",
|
||||
[self.ptr_type],
|
||||
self.void_type,
|
||||
)
|
||||
return current_block
|
||||
|
||||
def insert_lazy_init_cuda(self, current_block: ir.Block, context: CallContext):
|
||||
"""Insert the lazy init cuda function."""
|
||||
# create global private static that is initialized to nullptr
|
||||
with ir.InsertionPoint(context.module.body):
|
||||
parsed_op = ir.Operation.parse(
|
||||
f"llvm.mlir.global private @{self.cuda_global_state_symbol}(0 : i64) : i64"
|
||||
)
|
||||
context.module.body.append(parsed_op)
|
||||
|
||||
|
||||
with ir.InsertionPoint(current_block):
|
||||
cuda_global_state_ptr = self.address_of(
|
||||
self.cuda_global_state_symbol, self.ptr_type
|
||||
)
|
||||
cuda_init_ptr = self.address_of("cuda_init", self.ptr_type)
|
||||
cuda_load_ptr = self.address_of("cuda_load", self.ptr_type)
|
||||
set_error_ptr = self.address_of(
|
||||
"TVMFFIErrorSetRaisedFromCStr", self.ptr_type
|
||||
)
|
||||
# Call the callback function with the loaded ptr value
|
||||
init_result = llvm.call(
|
||||
result=self.i32_type, # function returns i32
|
||||
callee="cuda_dialect_init_library_once",
|
||||
callee_operands=[
|
||||
cuda_global_state_ptr,
|
||||
cuda_init_ptr,
|
||||
cuda_load_ptr,
|
||||
set_error_ptr,
|
||||
],
|
||||
op_bundle_sizes=[],
|
||||
op_bundle_operands=[],
|
||||
)
|
||||
# Create blocks for conditional branching
|
||||
error_block = current_block.create_after()
|
||||
success_block = error_block.create_after()
|
||||
# Check if initialization failed (non-zero return code)
|
||||
llvm.cond_br(
|
||||
self.equal(init_result, self.i32(0)),
|
||||
true_dest_operands=[],
|
||||
false_dest_operands=[],
|
||||
true_dest=success_block,
|
||||
false_dest=error_block,
|
||||
)
|
||||
# Error block: return the error code
|
||||
# error is already set by cuda_dialect_init_library_once
|
||||
with ir.InsertionPoint(error_block):
|
||||
llvm.return_(arg=self.i32(-1))
|
||||
|
||||
# Continue with success block
|
||||
return success_block
|
||||
|
||||
def append_unload_to_global_dtors(
|
||||
self, current_block: ir.Block, context: CallContext
|
||||
) -> ir.Block:
|
||||
"""Append the cuda_dialect_unload_library_once function to the global destructor list."""
|
||||
unload_func_symbol = "cuda_dialect_unload_library_once"
|
||||
# define a private function to call the extern function, we need this wrapper function
|
||||
# since llvm.mlir.global_dtors require the dtor defined in the module
|
||||
unload_func_wrapper_symbol = f"__dtor_{unload_func_symbol}"
|
||||
with ir.InsertionPoint(context.module.body):
|
||||
params, entry_block = self.function(
|
||||
name=unload_func_wrapper_symbol,
|
||||
params_type=[],
|
||||
ret_type=self.void_type,
|
||||
internal=True,
|
||||
)
|
||||
with ir.InsertionPoint(entry_block):
|
||||
llvm.call(
|
||||
result=None,
|
||||
callee=unload_func_symbol,
|
||||
callee_operands=[
|
||||
self.address_of(self.cuda_global_state_symbol, self.ptr_type)
|
||||
],
|
||||
op_bundle_sizes=[],
|
||||
op_bundle_operands=[],
|
||||
)
|
||||
llvm.return_()
|
||||
|
||||
# find or create the global destructors
|
||||
global_dtors_list: list[ir.Operation] = self.find_operations_in_module(
|
||||
context.module, "llvm.mlir.global_dtors"
|
||||
)
|
||||
if len(global_dtors_list) == 0:
|
||||
# create the global destructors
|
||||
with ir.InsertionPoint(context.module.body):
|
||||
global_dtors = llvm.mlir_global_dtors(
|
||||
dtors=[],
|
||||
priorities=[],
|
||||
)
|
||||
else:
|
||||
# use the existing global destructors
|
||||
global_dtors = global_dtors_list[0]
|
||||
|
||||
# append the unload function to the global destructors
|
||||
global_dtors.attributes["dtors"] += [
|
||||
ir.FlatSymbolRefAttr.get(unload_func_wrapper_symbol)
|
||||
]
|
||||
global_dtors.attributes["priorities"] += [
|
||||
ir.IntegerAttr.get(self.i32_type, 65535)
|
||||
] # the default priority
|
||||
|
||||
return current_block
|
||||
|
||||
def check_cuda_error(
|
||||
self, code: ir.Value, current_block: ir.Block, context: CallContext
|
||||
):
|
||||
"""Check if the CUDA error is raised and return the error string if so."""
|
||||
with ir.InsertionPoint(current_block):
|
||||
# check if the call is successful
|
||||
error_block = current_block.create_after()
|
||||
success_block = error_block.create_after()
|
||||
# Check if call is successful (non-zero return code)
|
||||
self.cond_br(
|
||||
cond=self.equal(code, self.i32(0)),
|
||||
true_block=success_block,
|
||||
false_block=error_block,
|
||||
branch_weights=self.BRANCH_WEIGHTS_LIKELY,
|
||||
)
|
||||
|
||||
# Error block: raise error and return
|
||||
with ir.InsertionPoint(error_block):
|
||||
error_str = llvm.call(
|
||||
result=self.ptr_type,
|
||||
callee="cuda_dialect_get_error_name",
|
||||
callee_operands=[code],
|
||||
op_bundle_sizes=[],
|
||||
op_bundle_operands=[],
|
||||
)
|
||||
# Raise error and return -1
|
||||
context.builder.raise_error_and_return(
|
||||
error_kind="RuntimeError",
|
||||
error_message_parts=["CUDA Error: ", error_str],
|
||||
)
|
||||
return success_block
|
||||
|
||||
def generate_llvm_call(
|
||||
self,
|
||||
current_block: ir.Block,
|
||||
call_operands: list[ir.Value],
|
||||
context: CallContext,
|
||||
) -> ir.Block:
|
||||
"""Generate the LLVM call operation and check if the call is successful."""
|
||||
with ir.InsertionPoint(current_block):
|
||||
result = llvm.call(
|
||||
result=self.i32_type,
|
||||
callee=self.target_func,
|
||||
callee_operands=call_operands,
|
||||
op_bundle_sizes=[],
|
||||
op_bundle_operands=[],
|
||||
)
|
||||
return self.check_cuda_error(result, current_block, context)
|
||||
|
||||
def insert_set_cuda_device(self, current_block: ir.Block, context: CallContext):
|
||||
"""Call the _cudaSetDevice function if we can find device id from tensor parameters."""
|
||||
|
||||
def find_cuda_device_index_from_params():
|
||||
for param in context.params:
|
||||
if (
|
||||
isinstance(param, spec.Tensor)
|
||||
and param.dlpack_device_type != tvm_ffi.DLDeviceType.kDLCPU
|
||||
):
|
||||
return context.matched_var_binding[param.device_id]
|
||||
return None
|
||||
|
||||
cuda_device_index = find_cuda_device_index_from_params()
|
||||
|
||||
if cuda_device_index is None:
|
||||
return current_block
|
||||
|
||||
with ir.InsertionPoint(current_block):
|
||||
result = llvm.call(
|
||||
result=self.i32_type,
|
||||
callee="_cudaSetDevice",
|
||||
callee_operands=[cuda_device_index],
|
||||
op_bundle_sizes=[],
|
||||
op_bundle_operands=[],
|
||||
)
|
||||
|
||||
return self.check_cuda_error(result, current_block, context)
|
||||
|
||||
def __call__(self, current_block: ir.Block, context: CallContext) -> ir.Block:
|
||||
current_block = self.declare_extern_funcs(current_block, context)
|
||||
current_block = self.insert_lazy_init_cuda(current_block, context)
|
||||
current_block = self.append_unload_to_global_dtors(current_block, context)
|
||||
current_block = self.insert_set_cuda_device(current_block, context)
|
||||
current_block = super().__call__(current_block, context)
|
||||
return current_block
|
||||
|
||||
|
||||
class TVMFFIJitCompiledFunction(tvm_ffi.Function, CudaDialectJitCompiledFunction):
|
||||
"""TVM FFI Function that contains metadata of the compiled function and interface to the FFI layer.
|
||||
|
||||
This function should not be directly used after
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# initialize the tvm_ffi.Function from the current execution engine
|
||||
self._init_ffi_function()
|
||||
|
||||
# use direct call to the tvm_ffi.Function.__call__
|
||||
# to avoid most of python overhead
|
||||
__call__ = tvm_ffi.Function.__call__
|
||||
|
||||
def _init_ffi_function(self):
|
||||
"""Initialize the tvm_ffi.Function from the current execution engine.
|
||||
|
||||
This function must be called at once during compilation time.
|
||||
The reason why it is not called during init is because the original
|
||||
flow may already created an execution engine and the function is not
|
||||
guaranteed to be initialized at that time.
|
||||
"""
|
||||
if self.__chandle__() != 0:
|
||||
raise DSLRuntimeError("TVM FFI function is already initialized")
|
||||
# get the MLIR function pointer from the execution engine
|
||||
tvm_ffi_function_ptr = self.engine.raw_lookup("__tvm_ffi_" + self.function_name)
|
||||
tvm_ffi_function = tvm_ffi.Function.__from_mlir_packed_safe_call__(
|
||||
tvm_ffi_function_ptr
|
||||
)
|
||||
# move the handle from the tvm_ffi.Function to the current instance
|
||||
self.__move_handle_from__(tvm_ffi_function)
|
||||
|
||||
def to(self, device=None):
|
||||
"""TVM FFI function itself is already support all devices."""
|
||||
return self
|
||||
|
||||
def run_compiled_program(self, exe_args: list[ir.Value]):
|
||||
"""Run the compiled program. This override is needed for implicit compile and execution."""
|
||||
return self.__call__(*exe_args)
|
||||
|
||||
def export_to_c(self, object_file_path: str, function_name: str = None):
|
||||
"""Export the TVM FFI function to an object file.
|
||||
|
||||
:param object_file_path: The path to the object file.
|
||||
:param function_name: The name of the function to export.
|
||||
"""
|
||||
if function_name is not None and function_name != self.function_name:
|
||||
mod = self.ir_module
|
||||
rename_tvm_ffi_function(mod, self.function_name, function_name)
|
||||
else:
|
||||
mod = self.ir_module
|
||||
|
||||
_execution_engine_extra.dump_object_file_pic(
|
||||
mod, object_file_path, "__tvm_ffi_" + function_name, 2
|
||||
)
|
||||
@@ -20,7 +20,9 @@ from .helpers import (
|
||||
PipelineUserType,
|
||||
PipelineState,
|
||||
make_pipeline_state,
|
||||
pipeline_init_arrive,
|
||||
pipeline_init_wait,
|
||||
agent_sync,
|
||||
arrive,
|
||||
arrive_unaligned,
|
||||
wait,
|
||||
@@ -68,7 +70,9 @@ __all__ = [
|
||||
"PipelineProducer",
|
||||
"PipelineConsumer",
|
||||
"make_pipeline_state",
|
||||
"pipeline_init_arrive",
|
||||
"pipeline_init_wait",
|
||||
"agent_sync",
|
||||
"arrive",
|
||||
"arrive_unaligned",
|
||||
"wait",
|
||||
|
||||
@@ -16,7 +16,14 @@ from typing import Optional, Union
|
||||
import warnings
|
||||
|
||||
import cutlass.cute as cute
|
||||
from cutlass.cutlass_dsl import Boolean, Int32, Int64, if_generate
|
||||
from cutlass.cutlass_dsl import (
|
||||
Boolean,
|
||||
Int32,
|
||||
Int64,
|
||||
if_generate,
|
||||
dsl_user_op,
|
||||
dsl_user_op,
|
||||
)
|
||||
from cutlass._mlir.dialects import llvm
|
||||
import cutlass._mlir.dialects.cute as _cute_ir
|
||||
|
||||
@@ -39,6 +46,11 @@ class Agent(enum.Enum):
|
||||
ThreadBlockCluster = enum.auto()
|
||||
|
||||
|
||||
##############################################################################
|
||||
# CooperativeGroup class
|
||||
##############################################################################
|
||||
|
||||
|
||||
class CooperativeGroup:
|
||||
"""
|
||||
CooperativeGroup contains size and alignment restrictions for an Agent.
|
||||
@@ -74,6 +86,11 @@ class CooperativeGroup:
|
||||
self.agent = agent
|
||||
|
||||
|
||||
##############################################################################
|
||||
# PipelineOp class
|
||||
##############################################################################
|
||||
|
||||
|
||||
class PipelineOp(enum.Enum):
|
||||
"""
|
||||
PipelineOp assigns an operation to an agent corresponding to a specific hardware feature.
|
||||
@@ -134,6 +151,11 @@ class SyncObject(ABC):
|
||||
pass
|
||||
|
||||
|
||||
##############################################################################
|
||||
# MbarrierArray class
|
||||
##############################################################################
|
||||
|
||||
|
||||
class MbarrierArray(SyncObject):
|
||||
"""
|
||||
MbarrierArray implements an abstraction for an array of smem barriers.
|
||||
@@ -185,25 +207,33 @@ class MbarrierArray(SyncObject):
|
||||
return new_mbarrier_array
|
||||
|
||||
# Mbarrier initialization
|
||||
def mbarrier_init(self) -> None:
|
||||
def mbarrier_init(self, *, loc=None, ip=None) -> None:
|
||||
"""
|
||||
Initializes an array of mbarriers using warp 0.
|
||||
"""
|
||||
|
||||
def then_body():
|
||||
for index in range(self.num_stages):
|
||||
cute.arch.mbarrier_init(self.get_barrier(index), self.arrive_count)
|
||||
cute.arch.mbarrier_init(
|
||||
self.get_barrier(index, loc=loc, ip=ip),
|
||||
self.arrive_count,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
warp_idx = cute.arch.warp_idx()
|
||||
warp_idx = cute.arch.make_warp_uniform(warp_idx)
|
||||
warp_idx = cute.arch.warp_idx(loc=loc, ip=ip)
|
||||
warp_idx = cute.arch.make_warp_uniform(warp_idx, loc=loc, ip=ip)
|
||||
|
||||
if_generate(warp_idx == 0, then_body)
|
||||
if_generate(warp_idx == 0, then_body, loc=loc, ip=ip)
|
||||
|
||||
def arrive(
|
||||
self,
|
||||
index: int,
|
||||
dst: int,
|
||||
cta_group: Optional[cute.nvgpu.tcgen05.CtaGroup] = None,
|
||||
*,
|
||||
loc=None,
|
||||
ip=None,
|
||||
) -> None:
|
||||
"""Select the arrive corresponding to this MbarrierArray's PipelineOp.
|
||||
|
||||
@@ -220,49 +250,75 @@ class MbarrierArray(SyncObject):
|
||||
:type cta_group: ``cute.nvgpu.tcgen05.CtaGroup``, optional
|
||||
"""
|
||||
if self.op_type is PipelineOp.AsyncThread:
|
||||
self.arrive_mbarrier(index, dst)
|
||||
self.arrive_mbarrier(index, dst, loc=loc, ip=ip)
|
||||
elif self.op_type is PipelineOp.TCGen05Mma:
|
||||
assert cta_group is not None, (
|
||||
"Error: CTA group must be provided for TCGen05Mma."
|
||||
)
|
||||
self.arrive_tcgen05mma(index, dst, cta_group)
|
||||
self.arrive_tcgen05mma(index, dst, cta_group, loc=loc, ip=ip)
|
||||
elif self.op_type in [PipelineOp.TmaLoad]:
|
||||
self.arrive_and_expect_tx(index, self.tx_count)
|
||||
elif self.op_type is PipelineOp.AsyncLoad:
|
||||
self.arrive_cp_async_mbarrier(index)
|
||||
self.arrive_cp_async_mbarrier(index, loc=loc, ip=ip)
|
||||
else:
|
||||
assert False, (
|
||||
f"Error: MbarrierArray is not supported for PipelineOp: {_get_pipeline_op(self.op_type)}."
|
||||
)
|
||||
|
||||
def arrive_mbarrier(self, index: int, dst_rank: Optional[int] = None) -> None:
|
||||
def arrive_mbarrier(
|
||||
self, index: int, dst_rank: Optional[int] = None, *, loc=None, ip=None
|
||||
) -> None:
|
||||
if dst_rank is None:
|
||||
cute.arch.mbarrier_arrive(self.get_barrier(index))
|
||||
cute.arch.mbarrier_arrive(
|
||||
self.get_barrier(index, loc=loc, ip=ip), loc=loc, ip=ip
|
||||
)
|
||||
else:
|
||||
cute.arch.mbarrier_arrive(self.get_barrier(index), dst_rank)
|
||||
cute.arch.mbarrier_arrive(
|
||||
self.get_barrier(index, loc=loc, ip=ip), dst_rank, loc=loc, ip=ip
|
||||
)
|
||||
|
||||
def arrive_cp_async_mbarrier(self, index: int):
|
||||
cute.arch.cp_async_mbarrier_arrive_noinc(self.get_barrier(index))
|
||||
def arrive_cp_async_mbarrier(self, index: int, *, loc=None, ip=None):
|
||||
cute.arch.cp_async_mbarrier_arrive_noinc(
|
||||
self.get_barrier(index, loc=loc, ip=ip), loc=loc, ip=ip
|
||||
)
|
||||
|
||||
def arrive_tcgen05mma(
|
||||
self, index: int, mask: Optional[int], cta_group: cute.nvgpu.tcgen05.CtaGroup
|
||||
self,
|
||||
index: int,
|
||||
mask: Optional[int],
|
||||
cta_group: cute.nvgpu.tcgen05.CtaGroup,
|
||||
*,
|
||||
loc=None,
|
||||
ip=None,
|
||||
) -> None:
|
||||
if mask is None:
|
||||
with cute.arch.elect_one():
|
||||
cute.nvgpu.tcgen05.commit(self.get_barrier(index))
|
||||
with cute.arch.elect_one(loc=loc, ip=ip):
|
||||
cute.nvgpu.tcgen05.commit(
|
||||
self.get_barrier(index, loc=loc, ip=ip), loc=loc, ip=ip
|
||||
)
|
||||
else:
|
||||
with cute.arch.elect_one():
|
||||
cute.nvgpu.tcgen05.commit(self.get_barrier(index), mask, cta_group)
|
||||
with cute.arch.elect_one(loc=loc, ip=ip):
|
||||
cute.nvgpu.tcgen05.commit(
|
||||
self.get_barrier(index, loc=loc, ip=ip),
|
||||
mask,
|
||||
cta_group,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
def arrive_and_expect_tx(self, index: int, tx_count: int) -> None:
|
||||
with cute.arch.elect_one():
|
||||
cute.arch.mbarrier_arrive_and_expect_tx(self.get_barrier(index), tx_count)
|
||||
|
||||
def try_wait(self, index: int, phase: int) -> Boolean:
|
||||
return cute.arch.mbarrier_try_wait(self.get_barrier(index), phase)
|
||||
def try_wait(self, index: int, phase: int, *, loc=None, ip=None) -> Boolean:
|
||||
return cute.arch.mbarrier_try_wait(
|
||||
self.get_barrier(index, loc=loc, ip=ip), phase, loc=loc, ip=ip
|
||||
)
|
||||
|
||||
def wait(self, index: int, phase: int) -> None:
|
||||
cute.arch.mbarrier_wait(self.get_barrier(index), phase)
|
||||
def wait(self, index: int, phase: int, *, loc=None, ip=None) -> None:
|
||||
cute.arch.mbarrier_wait(
|
||||
self.get_barrier(index, loc=loc, ip=ip), phase, loc=loc, ip=ip
|
||||
)
|
||||
|
||||
def arrive_and_wait(
|
||||
self,
|
||||
@@ -270,14 +326,17 @@ class MbarrierArray(SyncObject):
|
||||
phase: int,
|
||||
dst: int,
|
||||
cta_group: Optional[cute.nvgpu.tcgen05.CtaGroup] = None,
|
||||
*,
|
||||
loc=None,
|
||||
ip=None,
|
||||
) -> None:
|
||||
arrive(index, dst, cta_group)
|
||||
wait(index, phase)
|
||||
arrive(index, dst, cta_group, loc=loc, ip=ip)
|
||||
wait(index, phase, loc=loc, ip=ip)
|
||||
|
||||
def arrive_and_drop(self) -> None:
|
||||
def arrive_and_drop(self, *, loc=None, ip=None) -> None:
|
||||
raise NotImplementedError("Error: Not yet supported.")
|
||||
|
||||
def get_barrier(self, index: int) -> cute.Pointer:
|
||||
def get_barrier(self, index: int, *, loc=None, ip=None) -> cute.Pointer:
|
||||
return self.mbarrier_base + index
|
||||
|
||||
def max(self) -> int:
|
||||
@@ -294,6 +353,11 @@ class MbarrierArray(SyncObject):
|
||||
)
|
||||
|
||||
|
||||
##############################################################################
|
||||
# NamedBarrier class
|
||||
##############################################################################
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NamedBarrier(SyncObject):
|
||||
"""
|
||||
@@ -314,16 +378,21 @@ class NamedBarrier(SyncObject):
|
||||
"NamedBarrier ID 0 is by other driver APIs (i.e. sync_threads()) and should not be used."
|
||||
)
|
||||
|
||||
def arrive(self) -> None:
|
||||
@dsl_user_op
|
||||
def arrive(self, *, loc=None, ip=None) -> None:
|
||||
"""
|
||||
The aligned flavor of arrive is used when all threads in the CTA will execute the
|
||||
same instruction. See PTX documentation.
|
||||
"""
|
||||
cute.arch.barrier_arrive(
|
||||
barrier_id=self.barrier_id, number_of_threads=self.num_threads
|
||||
barrier_id=self.barrier_id,
|
||||
number_of_threads=self.num_threads,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
def arrive_unaligned(self) -> None:
|
||||
@dsl_user_op
|
||||
def arrive_unaligned(self, *, loc=None, ip=None) -> None:
|
||||
"""
|
||||
The unaligned flavor of arrive can be used with an arbitrary number of threads in the CTA.
|
||||
"""
|
||||
@@ -337,7 +406,8 @@ class NamedBarrier(SyncObject):
|
||||
asm_dialect=llvm.AsmDialect.AD_ATT,
|
||||
)
|
||||
|
||||
def wait(self) -> None:
|
||||
@dsl_user_op
|
||||
def wait(self, *, loc=None, ip=None) -> None:
|
||||
"""
|
||||
NamedBarriers do not have a standalone wait like mbarriers, only an arrive_and_wait.
|
||||
If synchronizing two warps in a producer/consumer pairing, the arrive count would be
|
||||
@@ -348,7 +418,7 @@ class NamedBarrier(SyncObject):
|
||||
warnings.warn(
|
||||
"NamedBarrier wait also arrives on the barrier. Routing call to NamedBarrier.arrive_and_wait()."
|
||||
)
|
||||
self.arrive_and_wait()
|
||||
self.arrive_and_wait(loc=loc, ip=ip)
|
||||
|
||||
def wait_unaligned(self) -> None:
|
||||
llvm.inline_asm(
|
||||
@@ -361,18 +431,22 @@ class NamedBarrier(SyncObject):
|
||||
asm_dialect=llvm.AsmDialect.AD_ATT,
|
||||
)
|
||||
|
||||
def arrive_and_wait(self) -> None:
|
||||
@dsl_user_op
|
||||
def arrive_and_wait(self, *, loc=None, ip=None) -> None:
|
||||
cute.arch.barrier(
|
||||
barrier_id=self.barrier_id, number_of_threads=self.num_threads
|
||||
barrier_id=self.barrier_id,
|
||||
number_of_threads=self.num_threads,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
def arrive_and_drop(self) -> None:
|
||||
def arrive_and_drop(self, *, loc=None, ip=None) -> None:
|
||||
raise NotImplementedError("Error: Not supported.")
|
||||
|
||||
def sync(self) -> None:
|
||||
def sync(self, *, loc=None, ip=None) -> None:
|
||||
self.arrive_and_wait()
|
||||
|
||||
def get_barrier(self) -> int:
|
||||
def get_barrier(self, *, loc=None, ip=None) -> int:
|
||||
return self.barrier_id
|
||||
|
||||
def max(self) -> int:
|
||||
@@ -380,6 +454,11 @@ class NamedBarrier(SyncObject):
|
||||
return 4095
|
||||
|
||||
|
||||
##############################################################################
|
||||
# TmaStoreFence class
|
||||
##############################################################################
|
||||
|
||||
|
||||
class TmaStoreFence(SyncObject):
|
||||
"""
|
||||
TmaStoreFence is used for a multi-stage epilogue buffer.
|
||||
@@ -391,21 +470,27 @@ class TmaStoreFence(SyncObject):
|
||||
|
||||
self.num_stages = num_stages
|
||||
|
||||
def arrive(self) -> None:
|
||||
cute.arch.cp_async_bulk_commit_group()
|
||||
@dsl_user_op
|
||||
def arrive(self, *, loc=None, ip=None) -> None:
|
||||
cute.arch.cp_async_bulk_commit_group(loc=loc, ip=ip)
|
||||
|
||||
def wait(self) -> None:
|
||||
cute.arch.cp_async_bulk_wait_group(self.num_stages - 1, read=True)
|
||||
@dsl_user_op
|
||||
def wait(self, *, loc=None, ip=None) -> None:
|
||||
cute.arch.cp_async_bulk_wait_group(
|
||||
self.num_stages - 1, read=True, loc=loc, ip=ip
|
||||
)
|
||||
|
||||
def arrive_and_wait(self) -> None:
|
||||
self.arrive()
|
||||
self.wait()
|
||||
@dsl_user_op
|
||||
def arrive_and_wait(self, *, loc=None, ip=None) -> None:
|
||||
self.arrive(loc=loc, ip=ip)
|
||||
self.wait(loc=loc, ip=ip)
|
||||
|
||||
def arrive_and_drop(self) -> None:
|
||||
@dsl_user_op
|
||||
def arrive_and_drop(self, *, loc=None, ip=None) -> None:
|
||||
raise NotImplementedError("Error: Not supported.")
|
||||
|
||||
# TmaStoreFence doesn't have mbarriers
|
||||
def get_barrier(self) -> None:
|
||||
def get_barrier(self, *, loc=None, ip=None) -> None:
|
||||
assert False, (
|
||||
"Error: TmaStoreFence doesn't use mbarriers and cannot return a barrier."
|
||||
)
|
||||
@@ -413,12 +498,13 @@ class TmaStoreFence(SyncObject):
|
||||
def max(self) -> None:
|
||||
raise NotImplementedError("Error: Not supported.")
|
||||
|
||||
def tail(self) -> None:
|
||||
cute.arch.cp_async_bulk_wait_group(0, read=True)
|
||||
@dsl_user_op
|
||||
def tail(self, *, loc=None, ip=None) -> None:
|
||||
cute.arch.cp_async_bulk_wait_group(0, read=True, loc=loc, ip=ip)
|
||||
|
||||
|
||||
##############################################################################
|
||||
# PipelineState class
|
||||
# PipelineUserType class
|
||||
##############################################################################
|
||||
|
||||
|
||||
@@ -427,6 +513,11 @@ class PipelineUserType(enum.Enum):
|
||||
Consumer = enum.auto()
|
||||
|
||||
|
||||
##############################################################################
|
||||
# PipelineState class
|
||||
##############################################################################
|
||||
|
||||
|
||||
class PipelineState:
|
||||
"""
|
||||
Pipeline state contains an index and phase bit corresponding to the current position in the circular buffer.
|
||||
@@ -460,12 +551,13 @@ class PipelineState:
|
||||
def reset_count(self):
|
||||
self._count = Int32(0)
|
||||
|
||||
def advance(self):
|
||||
@dsl_user_op
|
||||
def advance(self, *, loc=None, ip=None):
|
||||
self._index += 1
|
||||
self._count += 1
|
||||
|
||||
def then_body(index, phase):
|
||||
new_index = Int32(0)
|
||||
new_index = Int32(0, loc=loc, ip=ip)
|
||||
new_phase = phase ^ 1
|
||||
return new_index, new_phase
|
||||
|
||||
@@ -478,14 +570,17 @@ class PipelineState:
|
||||
else_body,
|
||||
[self.index, self.phase],
|
||||
[Int32, Int32],
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
def reverse(self):
|
||||
@dsl_user_op
|
||||
def reverse(self, *, loc=None, ip=None):
|
||||
self._index -= 1
|
||||
self._count -= 1
|
||||
|
||||
def then_body(index, phase):
|
||||
new_index = Int32(self.stages - 1)
|
||||
new_index = Int32(self.stages - 1, loc=loc, ip=ip)
|
||||
new_phase = phase ^ 1
|
||||
return new_index, new_phase
|
||||
|
||||
@@ -498,6 +593,8 @@ class PipelineState:
|
||||
else_body,
|
||||
[self.index, self.phase],
|
||||
[Int32, Int32],
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
def __get_mlir_types__(self):
|
||||
@@ -516,23 +613,23 @@ class PipelineState:
|
||||
)
|
||||
|
||||
|
||||
def make_pipeline_state(type: PipelineUserType, stages: int):
|
||||
def make_pipeline_state(type: PipelineUserType, stages: int, *, loc=None, ip=None):
|
||||
"""
|
||||
Creates a pipeline state. Producers are assumed to start with an empty buffer and have a flipped phase bit of 1.
|
||||
"""
|
||||
if type is PipelineUserType.Producer:
|
||||
return PipelineState(
|
||||
stages,
|
||||
Int32(0),
|
||||
Int32(0),
|
||||
Int32(1),
|
||||
Int32(0, loc=loc, ip=ip),
|
||||
Int32(0, loc=loc, ip=ip),
|
||||
Int32(1, loc=loc, ip=ip),
|
||||
)
|
||||
elif type is PipelineUserType.Consumer:
|
||||
return PipelineState(
|
||||
stages,
|
||||
Int32(0),
|
||||
Int32(0),
|
||||
Int32(0),
|
||||
Int32(0, loc=loc, ip=ip),
|
||||
Int32(0, loc=loc, ip=ip),
|
||||
Int32(0, loc=loc, ip=ip),
|
||||
)
|
||||
else:
|
||||
assert False, (
|
||||
@@ -545,31 +642,65 @@ def make_pipeline_state(type: PipelineUserType, stages: int):
|
||||
##############################################################################
|
||||
|
||||
|
||||
def pipeline_init_wait(cta_layout_vmnk: Optional[cute.Layout] = None):
|
||||
def pipeline_init_arrive(
|
||||
cluster_shape_mn: Optional[cute.Layout] = None,
|
||||
is_relaxed: bool = False,
|
||||
*,
|
||||
loc=None,
|
||||
ip=None,
|
||||
):
|
||||
"""
|
||||
Fences the mbarrier init and syncs the threadblock or cluster
|
||||
Fences the mbarrier_init and sends an arrive if using clusters.
|
||||
"""
|
||||
cute.arch.mbarrier_init_fence()
|
||||
|
||||
if cta_layout_vmnk is None or cute.size(cta_layout_vmnk) == 1:
|
||||
# If using clusters, send nonblocking arrives. Otherwise, do nothing
|
||||
# because sync_threads() doesn't have a nonblocking arrive.
|
||||
|
||||
cute.arch.mbarrier_init_fence(loc=loc, ip=ip)
|
||||
|
||||
if cluster_shape_mn is not None and cute.size(cluster_shape_mn, loc=loc, ip=ip) > 1:
|
||||
if is_relaxed:
|
||||
# Fences memory operations issued before the arrive
|
||||
cute.arch.cluster_arrive_relaxed(loc=loc, ip=ip)
|
||||
else:
|
||||
# Skips the memory barrier
|
||||
cute.arch.cluster_arrive(loc=loc, ip=ip)
|
||||
|
||||
|
||||
def pipeline_init_wait(
|
||||
cluster_shape_mn: Optional[cute.Layout] = None, *, loc=None, ip=None
|
||||
):
|
||||
"""
|
||||
Syncs the threadblock or cluster
|
||||
"""
|
||||
|
||||
if cluster_shape_mn is None or cute.size(cluster_shape_mn, loc=loc, ip=ip) == 1:
|
||||
# If not using clusters, sync the threadblock
|
||||
_sync(Agent.ThreadBlock)
|
||||
agent_sync(Agent.ThreadBlock, loc=loc, ip=ip)
|
||||
else:
|
||||
# If using clusters, sync the cluster
|
||||
_sync(Agent.ThreadBlockCluster)
|
||||
# If using clusters, wait on the cluster
|
||||
cute.arch.cluster_wait(loc=loc, ip=ip)
|
||||
|
||||
|
||||
def _sync(group: Agent):
|
||||
def _sync(group: Agent, is_relaxed: bool = False, *, loc=None, ip=None):
|
||||
warnings.warn("_sync is deprecated. Please use agent_sync instead.")
|
||||
agent_sync(group, is_relaxed, loc=loc, ip=ip)
|
||||
|
||||
|
||||
def agent_sync(group: Agent, is_relaxed: bool = False, *, loc=None, ip=None):
|
||||
"""
|
||||
Syncs all threads within an agent.
|
||||
"""
|
||||
if group is Agent.Thread:
|
||||
raise NotImplementedError("Error: Not supported.")
|
||||
elif group is Agent.ThreadBlock:
|
||||
cute.arch.sync_threads()
|
||||
cute.arch.sync_threads(loc=loc, ip=ip)
|
||||
elif group is Agent.ThreadBlockCluster:
|
||||
cute.arch.cluster_arrive()
|
||||
cute.arch.cluster_wait()
|
||||
if is_relaxed:
|
||||
cute.arch.cluster_arrive_relaxed(loc=loc, ip=ip)
|
||||
else:
|
||||
cute.arch.cluster_arrive(loc=loc, ip=ip)
|
||||
cute.arch.cluster_wait(loc=loc, ip=ip)
|
||||
else:
|
||||
assert False, (
|
||||
"Error: No explicit sync instruction exists. Please use barriers (named / mbarrier) instead."
|
||||
@@ -589,15 +720,17 @@ def _mbarrier_i64_to_ptr(val: Int64) -> cute.Pointer:
|
||||
|
||||
|
||||
# NamedBarrier free functions
|
||||
def arrive(barrier_id: int, num_threads: int):
|
||||
def arrive(barrier_id: int, num_threads: int, *, loc=None, ip=None):
|
||||
"""
|
||||
The aligned flavor of arrive is used when all threads in the CTA will execute the
|
||||
same instruction. See PTX documentation.
|
||||
"""
|
||||
cute.arch.barrier_arrive(barrier_id=barrier_id, number_of_threads=num_threads)
|
||||
cute.arch.barrier_arrive(
|
||||
barrier_id=barrier_id, number_of_threads=num_threads, loc=loc, ip=ip
|
||||
)
|
||||
|
||||
|
||||
def arrive_unaligned(barrier_id: int, num_threads: int):
|
||||
def arrive_unaligned(barrier_id: int, num_threads: int, *, loc=None, ip=None):
|
||||
"""
|
||||
The unaligned flavor of arrive can be used with an arbitrary number of threads in the CTA.
|
||||
"""
|
||||
@@ -623,10 +756,10 @@ def wait(barrier_id: int, num_threads: int):
|
||||
warnings.warn(
|
||||
"NamedBarrier wait also arrives on the barrier. Routing call to NamedBarrier.arrive_and_wait()."
|
||||
)
|
||||
arrive_and_wait()
|
||||
arrive_and_wait(loc=loc, ip=ip)
|
||||
|
||||
|
||||
def wait_unaligned(barrier_id: int, num_threads: int):
|
||||
def wait_unaligned(barrier_id: int, num_threads: int, *, loc=None, ip=None):
|
||||
warnings.warn(
|
||||
"NamedBarrier wait also arrives on the barrier. Routing call to NamedBarrier.arrive_and_wait()."
|
||||
)
|
||||
@@ -641,9 +774,11 @@ def wait_unaligned(barrier_id: int, num_threads: int):
|
||||
)
|
||||
|
||||
|
||||
def arrive_and_wait(barrier_id: int, num_threads: int):
|
||||
cute.arch.barrier(barrier_id=barrier_id, number_of_threads=num_threads)
|
||||
def arrive_and_wait(barrier_id: int, num_threads: int, *, loc=None, ip=None):
|
||||
cute.arch.barrier(
|
||||
barrier_id=barrier_id, number_of_threads=num_threads, loc=loc, ip=ip
|
||||
)
|
||||
|
||||
|
||||
def sync(barrier_id: int = 0):
|
||||
cute.arch.barrier(barrier_id=barrier_id)
|
||||
def sync(barrier_id: int = 0, *, loc=None, ip=None):
|
||||
cute.arch.barrier(barrier_id=barrier_id, loc=loc, ip=ip)
|
||||
|
||||
@@ -17,11 +17,12 @@ import cutlass.cute as cute
|
||||
from cutlass.cutlass_dsl import Boolean, if_generate
|
||||
|
||||
from cutlass.pipeline import (
|
||||
Agent,
|
||||
CooperativeGroup,
|
||||
PipelineOp,
|
||||
PipelineState,
|
||||
pipeline_init_wait,
|
||||
PipelineAsync,
|
||||
agent_sync,
|
||||
)
|
||||
|
||||
##############################################################################
|
||||
@@ -106,23 +107,27 @@ class PipelineTmaUmma(PipelineAsync):
|
||||
barrier_storage: cute.Pointer = None,
|
||||
cta_layout_vmnk: Optional[cute.Layout] = None,
|
||||
mcast_mode_mn: tuple[int, int] = (1, 1),
|
||||
defer_sync: bool = False,
|
||||
):
|
||||
"""
|
||||
This helper function computes any necessary attributes and returns an instance of PipelineTmaUmma.
|
||||
:param barrier_storage: Pointer to the smem address for this pipeline's mbarriers
|
||||
:type barrier_storage: cute.Pointer
|
||||
"""Creates and initializes a new PipelineTmaUmma instance.
|
||||
|
||||
:param num_stages: Number of buffer stages for this pipeline
|
||||
:type num_stages: Int32
|
||||
:param producer_group: `CooperativeGroup` for the producer agent
|
||||
:type num_stages: int
|
||||
:param producer_group: CooperativeGroup for the producer agent
|
||||
:type producer_group: CooperativeGroup
|
||||
:param consumer_group: `CooperativeGroup` for the consumer agent
|
||||
:param consumer_group: CooperativeGroup for the consumer agent
|
||||
:type consumer_group: CooperativeGroup
|
||||
:param tx_count: Number of bytes expected to be written to the transaction barrier for one stage
|
||||
:type tx_count: int
|
||||
:param barrier_storage: Pointer to the shared memory address for this pipeline's mbarriers
|
||||
:type barrier_storage: cute.Pointer, optional
|
||||
:param cta_layout_vmnk: Layout of the cluster shape
|
||||
:type cta_layout_vmnk: cute.Layout | None
|
||||
:param mcast_mode_mn: Tuple of two integers, specifying whether mcast is enabled for the m and n modes. At least one of the two integers must be 1.
|
||||
:type mcast_mode_mn: tuple[int, int]
|
||||
:type cta_layout_vmnk: cute.Layout, optional
|
||||
:param mcast_mode_mn: Tuple specifying multicast modes for m and n dimensions (each 0 or 1)
|
||||
:type mcast_mode_mn: tuple[int, int], optional
|
||||
:raises ValueError: If barrier_storage is not a cute.Pointer instance
|
||||
:return: A new PipelineTmaUmma instance configured with the provided parameters
|
||||
:rtype: PipelineTmaUmma
|
||||
"""
|
||||
if not isinstance(barrier_storage, cute.Pointer):
|
||||
raise ValueError(
|
||||
@@ -161,7 +166,12 @@ class PipelineTmaUmma(PipelineAsync):
|
||||
|
||||
consumer_mask = producer_mask
|
||||
|
||||
pipeline_init_wait(cta_layout_vmnk)
|
||||
if not defer_sync:
|
||||
cute.arch.mbarrier_init_fence()
|
||||
if cta_layout_vmnk is None or cute.size(cta_layout_vmnk) == 1:
|
||||
agent_sync(Agent.ThreadBlock)
|
||||
else:
|
||||
agent_sync(Agent.ThreadBlockCluster, is_relaxed=True)
|
||||
|
||||
return PipelineTmaUmma(
|
||||
sync_object_full,
|
||||
@@ -262,19 +272,23 @@ class PipelineAsyncUmma(PipelineAsync):
|
||||
consumer_group: CooperativeGroup,
|
||||
barrier_storage: cute.Pointer = None,
|
||||
cta_layout_vmnk: Optional[cute.Layout] = None,
|
||||
defer_sync: bool = False,
|
||||
):
|
||||
"""
|
||||
This helper function computes any necessary attributes and returns an instance of PipelineAsyncUmma.
|
||||
:param barrier_storage: Pointer to the smem address for this pipeline's mbarriers
|
||||
:type barrier_storage: cute.Pointer
|
||||
"""Creates and initializes a new PipelineAsyncUmma instance.
|
||||
|
||||
:param num_stages: Number of buffer stages for this pipeline
|
||||
:type num_stages: Int32
|
||||
:param producer_group: `CooperativeGroup` for the producer agent
|
||||
:type num_stages: int
|
||||
:param producer_group: CooperativeGroup for the producer agent
|
||||
:type producer_group: CooperativeGroup
|
||||
:param consumer_group: `CooperativeGroup` for the consumer agent
|
||||
:param consumer_group: CooperativeGroup for the consumer agent
|
||||
:type consumer_group: CooperativeGroup
|
||||
:param barrier_storage: Pointer to the shared memory address for this pipeline's mbarriers
|
||||
:type barrier_storage: cute.Pointer, optional
|
||||
:param cta_layout_vmnk: Layout of the cluster shape
|
||||
:type cta_layout_vmnk: cute.Layout | None
|
||||
:type cta_layout_vmnk: cute.Layout, optional
|
||||
:raises ValueError: If barrier_storage is not a cute.Pointer instance
|
||||
:return: A new PipelineAsyncUmma instance configured with the provided parameters
|
||||
:rtype: PipelineAsyncUmma
|
||||
"""
|
||||
if not isinstance(barrier_storage, cute.Pointer):
|
||||
raise ValueError(
|
||||
@@ -315,7 +329,12 @@ class PipelineAsyncUmma(PipelineAsync):
|
||||
# consumer needs to get the mask to signal
|
||||
consumer_mask = PipelineAsyncUmma._compute_peer_cta_mask(cta_layout_vmnk)
|
||||
|
||||
pipeline_init_wait(cta_layout_vmnk)
|
||||
if not defer_sync:
|
||||
cute.arch.mbarrier_init_fence()
|
||||
if cta_layout_vmnk is None or cute.size(cta_layout_vmnk) == 1:
|
||||
agent_sync(Agent.ThreadBlock)
|
||||
else:
|
||||
agent_sync(Agent.ThreadBlockCluster, is_relaxed=True)
|
||||
|
||||
return PipelineAsyncUmma(
|
||||
sync_object_full,
|
||||
@@ -372,19 +391,23 @@ class PipelineUmmaAsync(PipelineAsync):
|
||||
consumer_group: CooperativeGroup,
|
||||
barrier_storage: cute.Pointer = None,
|
||||
cta_layout_vmnk: Optional[cute.Layout] = None,
|
||||
defer_sync: bool = False,
|
||||
):
|
||||
"""
|
||||
This helper function computes any necessary attributes and returns an instance of PipelineUmmaAsync.
|
||||
:param barrier_storage: Pointer to the smem address for this pipeline's mbarriers
|
||||
:type barrier_storage: cute.Pointer
|
||||
"""Creates an instance of PipelineUmmaAsync with computed attributes.
|
||||
|
||||
:param num_stages: Number of buffer stages for this pipeline
|
||||
:type num_stages: Int32
|
||||
:param producer_group: `CooperativeGroup` for the producer agent
|
||||
:type num_stages: int
|
||||
:param producer_group: ``CooperativeGroup`` for the producer agent
|
||||
:type producer_group: CooperativeGroup
|
||||
:param consumer_group: `CooperativeGroup` for the consumer agent
|
||||
:param consumer_group: ``CooperativeGroup`` for the consumer agent
|
||||
:type consumer_group: CooperativeGroup
|
||||
:param barrier_storage: Pointer to the shared memory address for this pipeline's mbarriers
|
||||
:type barrier_storage: cute.Pointer, optional
|
||||
:param cta_layout_vmnk: Layout of the cluster shape
|
||||
:type cta_layout_vmnk: cute.Layout | None
|
||||
:type cta_layout_vmnk: cute.Layout, optional
|
||||
:raises ValueError: If barrier_storage is not a cute.Pointer instance
|
||||
:return: New instance of ``PipelineUmmaAsync``
|
||||
:rtype: PipelineUmmaAsync
|
||||
"""
|
||||
if not isinstance(barrier_storage, cute.Pointer):
|
||||
raise ValueError(
|
||||
@@ -411,7 +434,7 @@ class PipelineUmmaAsync(PipelineAsync):
|
||||
producer_mask = PipelineUmmaAsync._compute_tmem_sync_mask(cta_layout_vmnk)
|
||||
|
||||
if cta_layout_vmnk is None or cute.size(cta_layout_vmnk, mode=[0]) == 1:
|
||||
# Set mask to None if not using 2CTA intructions
|
||||
# Set mask to None if not using 2CTA instructions
|
||||
consumer_mask = None
|
||||
else:
|
||||
consumer_mask = PipelineUmmaAsync._compute_peer_cta_rank()
|
||||
@@ -422,7 +445,12 @@ class PipelineUmmaAsync(PipelineAsync):
|
||||
else cute.nvgpu.tcgen05.CtaGroup.TWO
|
||||
)
|
||||
|
||||
pipeline_init_wait(cta_layout_vmnk)
|
||||
if not defer_sync:
|
||||
cute.arch.mbarrier_init_fence()
|
||||
if cta_layout_vmnk is None or cute.size(cta_layout_vmnk) == 1:
|
||||
agent_sync(Agent.ThreadBlock)
|
||||
else:
|
||||
agent_sync(Agent.ThreadBlockCluster, is_relaxed=True)
|
||||
|
||||
return PipelineUmmaAsync(
|
||||
sync_object_full,
|
||||
|
||||
@@ -13,8 +13,10 @@ from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
import cutlass.cute as cute
|
||||
from cutlass.cutlass_dsl import Boolean, Int32, if_generate
|
||||
from cutlass.cutlass_dsl import Boolean, Int32, if_generate, dsl_user_op
|
||||
|
||||
from cutlass.pipeline import (
|
||||
Agent,
|
||||
CooperativeGroup,
|
||||
MbarrierArray,
|
||||
PipelineOp,
|
||||
@@ -23,7 +25,7 @@ from cutlass.pipeline import (
|
||||
SyncObject,
|
||||
TmaStoreFence,
|
||||
make_pipeline_state,
|
||||
pipeline_init_wait,
|
||||
agent_sync,
|
||||
)
|
||||
|
||||
##############################################################################
|
||||
@@ -157,6 +159,7 @@ class PipelineAsync:
|
||||
barrier_storage: cute.Pointer = None,
|
||||
producer_mask: Int32 = None,
|
||||
consumer_mask: Int32 = None,
|
||||
defer_sync: bool = False,
|
||||
):
|
||||
"""Creates and initializes a new PipelineAsync instance.
|
||||
|
||||
@@ -167,17 +170,17 @@ class PipelineAsync:
|
||||
:type barrier_storage: cute.Pointer
|
||||
:param num_stages: Number of buffer stages for this pipeline
|
||||
:type num_stages: int
|
||||
:param producer_group: `CooperativeGroup` for the producer agent
|
||||
:param producer_group: ``CooperativeGroup`` for the producer agent
|
||||
:type producer_group: CooperativeGroup
|
||||
:param consumer_group: `CooperativeGroup` for the consumer agent
|
||||
:param consumer_group: ``CooperativeGroup`` for the consumer agent
|
||||
:type consumer_group: CooperativeGroup
|
||||
:param producer_mask: Mask for signaling arrives for the producer agent, defaults to ``None``
|
||||
:param producer_mask: Mask for signaling arrives for the producer agent
|
||||
:type producer_mask: Int32, optional
|
||||
:param consumer_mask: Mask for signaling arrives for the consumer agent, defaults to ``None``
|
||||
:param consumer_mask: Mask for signaling arrives for the consumer agent
|
||||
:type consumer_mask: Int32, optional
|
||||
:return: A new PipelineAsync instance
|
||||
:rtype: PipelineAsync
|
||||
:raises ValueError: If barrier_storage is not a cute.Pointer instance
|
||||
:return: A new ``PipelineAsync`` instance
|
||||
:rtype: PipelineAsync
|
||||
"""
|
||||
if not isinstance(barrier_storage, cute.Pointer):
|
||||
raise ValueError(
|
||||
@@ -197,7 +200,9 @@ class PipelineAsync:
|
||||
barrier_storage.align(min_align=8) + num_stages, num_stages, consumer
|
||||
)
|
||||
|
||||
pipeline_init_wait()
|
||||
if not defer_sync:
|
||||
cute.arch.mbarrier_init_fence()
|
||||
agent_sync(Agent.ThreadBlock)
|
||||
|
||||
return PipelineAsync(
|
||||
sync_object_full,
|
||||
@@ -280,21 +285,24 @@ class PipelineCpAsync(PipelineAsync):
|
||||
consumer_group: CooperativeGroup,
|
||||
producer_mask: Int32 = None,
|
||||
consumer_mask: Int32 = None,
|
||||
defer_sync: bool = False,
|
||||
):
|
||||
"""
|
||||
This helper function computes any necessary attributes and returns an instance of PipelineAsync.
|
||||
:param barrier_storage: Pointer to the smem address for this pipeline's mbarriers
|
||||
"""Helper function that computes necessary attributes and returns a ``PipelineCpAsync`` instance.
|
||||
|
||||
:param barrier_storage: Pointer to the shared memory address for this pipeline's mbarriers
|
||||
:type barrier_storage: cute.Pointer
|
||||
:param num_stages: Number of buffer stages for this pipeline
|
||||
:type num_stages: Int32
|
||||
:param producer_group: CooperativeGroup for the producer agent
|
||||
:param producer_group: ``CooperativeGroup`` for the producer agent
|
||||
:type producer_group: CooperativeGroup
|
||||
:param consumer_group: CooperativeGroup for the consumer agent
|
||||
:param consumer_group: ``CooperativeGroup`` for the consumer agent
|
||||
:type consumer_group: CooperativeGroup
|
||||
:param producer_mask: Mask for signaling arrives for the producer agent
|
||||
:type producer_mask: Int32 | None
|
||||
:param consumer_mask: Mask for signaling arrives for the consumer agent
|
||||
:type consumer_mask: Int32 | None
|
||||
:param producer_mask: Mask for signaling arrives for the producer agent, defaults to None
|
||||
:type producer_mask: Int32, optional
|
||||
:param consumer_mask: Mask for signaling arrives for the consumer agent, defaults to None
|
||||
:type consumer_mask: Int32, optional
|
||||
:return: A new ``PipelineCpAsync`` instance configured with the provided parameters
|
||||
:rtype: PipelineCpAsync
|
||||
"""
|
||||
producer_type = PipelineOp.AsyncLoad
|
||||
consumer_type = PipelineOp.AsyncThread
|
||||
@@ -309,7 +317,9 @@ class PipelineCpAsync(PipelineAsync):
|
||||
barrier_storage.align(min_align=8) + num_stages, num_stages, consumer
|
||||
)
|
||||
|
||||
pipeline_init_wait()
|
||||
if not defer_sync:
|
||||
cute.arch.mbarrier_init_fence()
|
||||
agent_sync(Agent.ThreadBlock)
|
||||
|
||||
return PipelineCpAsync(
|
||||
sync_object_array_full,
|
||||
@@ -331,11 +341,22 @@ class PipelineTmaAsync(PipelineAsync):
|
||||
@staticmethod
|
||||
@cute.jit
|
||||
def init_empty_barrier_arrive_signal(
|
||||
cta_layout_vmnk: cute.Layout, tidx: Int32, mcast_mode_mn: tuple[int, int]
|
||||
cta_layout_vmnk: cute.Layout, tidx: Int32, mcast_mode_mn: tuple[int, int] = (1, 1)
|
||||
):
|
||||
"""
|
||||
Initialize the empty barrier arrive signal
|
||||
This function returns the destination cta rank and a boolean indicating if the signalling thread is the same as the current thread
|
||||
"""Initialize the empty barrier arrive signal.
|
||||
|
||||
This function determines which threads should signal empty barrier arrives based on the cluster layout
|
||||
and multicast modes. It returns the destination CTA rank and whether the current thread should signal.
|
||||
|
||||
:param cta_layout_vmnk: Layout describing the cluster shape and CTA arrangement
|
||||
:type cta_layout_vmnk: cute.Layout
|
||||
:param tidx: Thread index within the warp
|
||||
:type tidx: Int32
|
||||
:param mcast_mode_mn: Tuple specifying multicast modes for m and n dimensions (each 0 or 1), defaults to (1,1)
|
||||
:type mcast_mode_mn: tuple[int, int]
|
||||
:raises ``AssertionError``: If both multicast modes are disabled (0,0)
|
||||
:return: Tuple containing destination CTA rank and boolean indicating if current thread signals
|
||||
:rtype: tuple[Int32, Boolean]
|
||||
"""
|
||||
# Logic to optimally schedule Empty Arrives
|
||||
cluster_shape_vmnk = cta_layout_vmnk.shape
|
||||
@@ -384,25 +405,29 @@ class PipelineTmaAsync(PipelineAsync):
|
||||
cta_layout_vmnk: Optional[cute.Layout] = None,
|
||||
tidx: Optional[Int32] = None,
|
||||
mcast_mode_mn: tuple[int, int] = (1, 1),
|
||||
defer_sync: bool = False,
|
||||
):
|
||||
"""
|
||||
This helper function computes any necessary attributes and returns an instance of PipelineTmaAsync.
|
||||
:param barrier_storage: Pointer to the smem address for this pipeline's mbarriers
|
||||
:type barrier_storage: cute.Pointer
|
||||
"""Create a new ``PipelineTmaAsync`` instance.
|
||||
|
||||
:param num_stages: Number of buffer stages for this pipeline
|
||||
:type num_stages: Int32
|
||||
:param producer_group: `CooperativeGroup` for the producer agent
|
||||
:type num_stages: int
|
||||
:param producer_group: ``CooperativeGroup`` for the producer agent
|
||||
:type producer_group: CooperativeGroup
|
||||
:param consumer_group: `CooperativeGroup` for the consumer agent
|
||||
:param consumer_group: ``CooperativeGroup`` for the consumer agent
|
||||
:type consumer_group: CooperativeGroup
|
||||
:param tx_count: Number of bytes expected to be written to the transaction barrier for one stage
|
||||
:type tx_count: int
|
||||
:param cta_layout_vmnk: Layout of the cluster shape
|
||||
:type cta_layout_vmnk: cute.Layout | None
|
||||
:param tidx: thread index to consumer async threads
|
||||
:type tidx: Int32 | None
|
||||
:param mcast_mode_mn: Tuple of two integers, specifying whether mcast is enabled for the m and n modes. At least one of the two integers must be 1.
|
||||
:type mcast_mode_mn: tuple[int, int]
|
||||
:param barrier_storage: Pointer to the shared memory address for this pipeline's mbarriers, defaults to None
|
||||
:type barrier_storage: cute.Pointer, optional
|
||||
:param cta_layout_vmnk: Layout of the cluster shape, defaults to None
|
||||
:type cta_layout_vmnk: cute.Layout, optional
|
||||
:param tidx: Thread index to consumer async threads, defaults to None
|
||||
:type tidx: Int32, optional
|
||||
:param mcast_mode_mn: Tuple specifying multicast modes for m and n dimensions (each 0 or 1), defaults to (1,1)
|
||||
:type mcast_mode_mn: tuple[int, int], optional
|
||||
:raises ValueError: If barrier_storage is not a cute.Pointer instance
|
||||
:return: New ``PipelineTmaAsync`` instance
|
||||
:rtype: PipelineTmaAsync
|
||||
"""
|
||||
if not isinstance(barrier_storage, cute.Pointer):
|
||||
raise ValueError(
|
||||
@@ -438,7 +463,12 @@ class PipelineTmaAsync(PipelineAsync):
|
||||
|
||||
producer_mask = None
|
||||
|
||||
pipeline_init_wait(cta_layout_vmnk)
|
||||
if not defer_sync:
|
||||
cute.arch.mbarrier_init_fence()
|
||||
if cta_layout_vmnk is None or cute.size(cta_layout_vmnk) == 1:
|
||||
agent_sync(Agent.ThreadBlock)
|
||||
else:
|
||||
agent_sync(Agent.ThreadBlockCluster, is_relaxed=True)
|
||||
|
||||
return PipelineTmaAsync(
|
||||
sync_object_full,
|
||||
@@ -449,17 +479,25 @@ class PipelineTmaAsync(PipelineAsync):
|
||||
is_signalling_thread,
|
||||
)
|
||||
|
||||
@dsl_user_op
|
||||
def producer_acquire(
|
||||
self, state: PipelineState, try_acquire_token: Optional[Boolean] = None
|
||||
self,
|
||||
state: PipelineState,
|
||||
try_acquire_token: Optional[Boolean] = None,
|
||||
*,
|
||||
loc=None,
|
||||
ip=None,
|
||||
):
|
||||
"""
|
||||
TMA producer commit conditionally waits on buffer empty and sets the transaction barrier.
|
||||
"""
|
||||
if_generate(
|
||||
try_acquire_token is None or try_acquire_token == 0,
|
||||
lambda: self.sync_object_empty.wait(state.index, state.phase),
|
||||
lambda: self.sync_object_empty.wait(
|
||||
state.index, state.phase, loc=loc, ip=ip
|
||||
),
|
||||
)
|
||||
self.sync_object_full.arrive(state.index, self.producer_mask)
|
||||
self.sync_object_full.arrive(state.index, self.producer_mask, loc=loc, ip=ip)
|
||||
|
||||
def producer_commit(self, state: PipelineState):
|
||||
"""
|
||||
@@ -467,13 +505,16 @@ class PipelineTmaAsync(PipelineAsync):
|
||||
"""
|
||||
pass
|
||||
|
||||
def consumer_release(self, state: PipelineState):
|
||||
@dsl_user_op
|
||||
def consumer_release(self, state: PipelineState, *, loc=None, ip=None):
|
||||
"""
|
||||
TMA consumer release conditionally signals the empty buffer to the producer.
|
||||
"""
|
||||
if_generate(
|
||||
self.is_signalling_thread,
|
||||
lambda: self.sync_object_empty.arrive(state.index, self.consumer_mask),
|
||||
lambda: self.sync_object_empty.arrive(
|
||||
state.index, self.consumer_mask, loc=loc, ip=ip
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -498,23 +539,29 @@ class PipelineTmaMultiConsumersAsync(PipelineAsync):
|
||||
tx_count: int,
|
||||
barrier_storage: cute.Pointer = None,
|
||||
cta_layout_vmnk: Optional[cute.Layout] = None,
|
||||
defer_sync: bool = False,
|
||||
):
|
||||
"""
|
||||
This helper function computes any necessary attributes and returns an instance of PipelineTmaMultiConsumersAsync.
|
||||
:param barrier_storage: Pointer to the smem address for this pipeline's mbarriers
|
||||
"""Creates an instance of PipelineTmaMultiConsumersAsync with computed attributes.
|
||||
|
||||
:param barrier_storage: Pointer to the shared memory address for this pipeline's mbarriers
|
||||
:type barrier_storage: cute.Pointer
|
||||
:param num_stages: Number of buffer stages for this pipeline
|
||||
:type num_stages: Int32
|
||||
:param producer_group: `CooperativeGroup` for the producer agent
|
||||
:type num_stages: int
|
||||
:param producer_group: ``CooperativeGroup`` for the producer agent
|
||||
:type producer_group: CooperativeGroup
|
||||
:param consumer_group_umma: `CooperativeGroup` for the UMMA consumer agent
|
||||
:param consumer_group_umma: ``CooperativeGroup`` for the UMMA consumer agent
|
||||
:type consumer_group_umma: CooperativeGroup
|
||||
:param consumer_group_async: `CooperativeGroup` for the AsyncThread consumer agent
|
||||
:param consumer_group_async: ``CooperativeGroup`` for the AsyncThread consumer agent
|
||||
:type consumer_group_async: CooperativeGroup
|
||||
:param tx_count: Number of bytes expected to be written to the transaction barrier for one stage
|
||||
:type tx_count: int
|
||||
:param cta_layout_vmnk: Layout of the cluster shape
|
||||
:type cta_layout_vmnk: cute.Layout | None
|
||||
:param cta_layout_vmnk: Layout of the cluster shape, defaults to None
|
||||
:type cta_layout_vmnk: Optional[cute.Layout]
|
||||
:raises ValueError: If ``barrier_storage`` is not a ``cute.Pointer`` instance
|
||||
:raises ValueError: If ``UMMA`` and ``AsyncThread`` consumer groups are not the same agent
|
||||
:raises ValueError: If ``cta_layout_vmnk`` size is not 1
|
||||
:return: New instance of ``PipelineTmaMultiConsumersAsync``
|
||||
:rtype: PipelineTmaMultiConsumersAsync
|
||||
"""
|
||||
if not isinstance(barrier_storage, cute.Pointer):
|
||||
raise ValueError(
|
||||
@@ -560,7 +607,7 @@ class PipelineTmaMultiConsumersAsync(PipelineAsync):
|
||||
# No mcast mask if not using clusters
|
||||
producer_mask = None
|
||||
consumer_mask = None
|
||||
# All threadblocks are leaders if not using clusters
|
||||
# All thread-blocks are leaders if not using clusters
|
||||
is_leader_cta = True
|
||||
cta_group = (
|
||||
cute.nvgpu.tcgen05.CtaGroup.ONE
|
||||
@@ -568,7 +615,11 @@ class PipelineTmaMultiConsumersAsync(PipelineAsync):
|
||||
else cute.nvgpu.tcgen05.CtaGroup.TWO
|
||||
)
|
||||
|
||||
pipeline_init_wait(cta_layout_vmnk)
|
||||
if not defer_sync:
|
||||
if cta_layout_vmnk is None or cute.size(cta_layout_vmnk) == 1:
|
||||
agent_sync(Agent.ThreadBlock)
|
||||
else:
|
||||
agent_sync(Agent.ThreadBlockCluster, is_relaxed=True)
|
||||
|
||||
return PipelineTmaMultiConsumersAsync(
|
||||
sync_object_full,
|
||||
@@ -626,14 +677,15 @@ class PipelineTmaStore(PipelineAsync):
|
||||
num_stages: int,
|
||||
producer_group: CooperativeGroup,
|
||||
):
|
||||
"""
|
||||
This helper function computes any necessary attributes and returns an instance of PipelineTmaStore.
|
||||
:param num_stages: Number of buffer stages for this pipeline
|
||||
:type num_stages: Int32
|
||||
:param producer_group: `CooperativeGroup` for the producer agent
|
||||
:type producer_group: CooperativeGroup
|
||||
"""
|
||||
"""This helper function computes any necessary attributes and returns an instance of ``PipelineTmaStore``.
|
||||
|
||||
:param num_stages: Number of buffer stages for this pipeline
|
||||
:type num_stages: int
|
||||
:param producer_group: ``CooperativeGroup`` for the producer agent
|
||||
:type producer_group: CooperativeGroup
|
||||
:return: A new ``PipelineTmaStore`` instance
|
||||
:rtype: PipelineTmaStore
|
||||
"""
|
||||
producer_type = PipelineOp.TmaStore
|
||||
|
||||
producer = (producer_type, producer_group)
|
||||
@@ -642,20 +694,25 @@ class PipelineTmaStore(PipelineAsync):
|
||||
|
||||
return PipelineTmaStore(sync_object_full, None, num_stages, None, None)
|
||||
|
||||
def producer_acquire(self):
|
||||
self.sync_object_full.wait()
|
||||
@dsl_user_op
|
||||
def producer_acquire(self, *, loc=None, ip=None):
|
||||
self.sync_object_full.wait(loc=loc, ip=ip)
|
||||
|
||||
def producer_commit(self):
|
||||
self.sync_object_full.arrive()
|
||||
@dsl_user_op
|
||||
def producer_commit(self, *, loc=None, ip=None):
|
||||
self.sync_object_full.arrive(loc=loc, ip=ip)
|
||||
|
||||
def consumer_wait(self):
|
||||
@dsl_user_op
|
||||
def consumer_wait(self, *, loc=None, ip=None):
|
||||
assert False, "Error: PipelineTmaStore does not have a consumer agent."
|
||||
|
||||
def consumer_release(self):
|
||||
@dsl_user_op
|
||||
def consumer_release(self, *, loc=None, ip=None):
|
||||
assert False, "Error: PipelineTmaStore does not have a consumer agent."
|
||||
|
||||
def producer_tail(self):
|
||||
self.sync_object_full.tail()
|
||||
@dsl_user_op
|
||||
def producer_tail(self, *, loc=None, ip=None):
|
||||
self.sync_object_full.tail(loc=loc, ip=ip)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -706,6 +763,7 @@ class PipelineOrder:
|
||||
length: int,
|
||||
group_id: int,
|
||||
producer_group: CooperativeGroup,
|
||||
defer_sync: bool = False,
|
||||
):
|
||||
if not isinstance(barrier_storage, cute.Pointer):
|
||||
raise ValueError(
|
||||
@@ -722,7 +780,9 @@ class PipelineOrder:
|
||||
barrier_storage.align(min_align=8), num_stages, producer
|
||||
)
|
||||
|
||||
pipeline_init_wait()
|
||||
if not defer_sync:
|
||||
cute.arch.mbarrier_init_fence()
|
||||
agent_sync(Agent.ThreadBlock)
|
||||
|
||||
return PipelineOrder(
|
||||
sync_object_full,
|
||||
@@ -740,16 +800,20 @@ class PipelineOrder:
|
||||
def get_barrier_for_current_stage_idx(self, group_id):
|
||||
return self.state.index * self.length + group_id
|
||||
|
||||
def arrive(self):
|
||||
@dsl_user_op
|
||||
def arrive(self, *, loc=None, ip=None):
|
||||
signalling_id = (self.group_id + 1) % self.length
|
||||
idx = self.get_barrier_for_current_stage_idx(signalling_id)
|
||||
cute.arch.mbarrier_arrive(self.sync_object_full.get_barrier(idx))
|
||||
self.state.advance()
|
||||
cute.arch.mbarrier_arrive(
|
||||
self.sync_object_full.get_barrier(idx), loc=loc, ip=ip
|
||||
)
|
||||
self.state.advance(loc=loc, ip=ip)
|
||||
|
||||
def wait(self):
|
||||
@dsl_user_op
|
||||
def wait(self, *, loc=None, ip=None):
|
||||
idx = self.get_barrier_for_current_stage_idx(self.group_id)
|
||||
cute.arch.mbarrier_wait(
|
||||
self.sync_object_full.get_barrier(idx), self.state.phase
|
||||
self.sync_object_full.get_barrier(idx), self.state.phase, loc=loc, ip=ip
|
||||
)
|
||||
|
||||
|
||||
@@ -881,6 +945,10 @@ class PipelineProducer:
|
||||
self.__state = state
|
||||
self.__group = group
|
||||
|
||||
def clone(self):
|
||||
"""Create a new Producer instance with the same state."""
|
||||
return PipelineProducer(self.__pipeline, self.__state.clone(), self.__group)
|
||||
|
||||
def reset(self):
|
||||
"""Reset the count of how many handles this producer has committed."""
|
||||
self.__state.reset_count()
|
||||
@@ -1053,6 +1121,10 @@ class PipelineConsumer:
|
||||
self.__group = group
|
||||
self.__state = state
|
||||
|
||||
def clone(self):
|
||||
"""Create a new Consumer instance with the same state."""
|
||||
return PipelineConsumer(self.__pipeline, self.__state.clone(), self.__group)
|
||||
|
||||
def reset(self):
|
||||
"""Reset the count of how many handles this consumer has consumed."""
|
||||
self.__state.reset_count()
|
||||
|
||||
@@ -47,6 +47,11 @@ def dtype(ty: Type[Numeric]):
|
||||
Float8E4M3FN: torch.float8_e4m3fn,
|
||||
Float8E4M3B11FNUZ: torch.float8_e4m3fnuz,
|
||||
}
|
||||
|
||||
# float8_e8m0fnu is introduced in latest version of torch
|
||||
if hasattr(torch, "float8_e8m0fnu"):
|
||||
torch_type_map[Float8E8M0FNU] = torch.float8_e8m0fnu
|
||||
|
||||
if torch_dtype is None:
|
||||
torch_dtype = torch_type_map.get(ty)
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from .static_persistent_tile_scheduler import (
|
||||
WorkTileInfo,
|
||||
PersistentTileSchedulerParams,
|
||||
StaticPersistentTileScheduler,
|
||||
StaticPersistentRuntimeTileScheduler,
|
||||
)
|
||||
|
||||
from .hardware_info import (
|
||||
@@ -65,21 +66,27 @@ from .tmem_allocator import TmemAllocator
|
||||
|
||||
from .layout import LayoutEnum
|
||||
|
||||
from .distributed_helpers import (
|
||||
spin_lock_wait,
|
||||
spin_lock_multimem_arrive,
|
||||
multimem_ld_reduce_8xf16,
|
||||
multimem_ld_reduce_4xf32,
|
||||
multimem_ld_reduce_8xbf16,
|
||||
multimem_ld_reduce_16xe4m3,
|
||||
multimem_ld_reduce_16xe5m2,
|
||||
multimem_st_4xb32,
|
||||
sm_wise_inter_gpu_multimem_barrier,
|
||||
from . import distributed
|
||||
|
||||
from .mixed_input_helpers import (
|
||||
TransformMode,
|
||||
scale_tma_partition,
|
||||
transform_partition,
|
||||
scale_partition,
|
||||
get_gmem_layout_scale,
|
||||
get_smem_layout_scale,
|
||||
compute_smem_layout,
|
||||
get_transform_a_source,
|
||||
get_tma_atom_kind,
|
||||
get_copy_atom_a_transform,
|
||||
is_valid_scale_granularity,
|
||||
get_divisibility,
|
||||
)
|
||||
|
||||
from . import hopper_helpers as sm90
|
||||
from . import blackwell_helpers as sm100
|
||||
|
||||
from .print_latex import print_latex, print_latex_tv
|
||||
|
||||
__all__ = [
|
||||
"get_smem_capacity_in_bytes",
|
||||
@@ -89,6 +96,7 @@ __all__ = [
|
||||
"WorkTileInfo",
|
||||
"PersistentTileSchedulerParams",
|
||||
"StaticPersistentTileScheduler",
|
||||
"StaticPersistentRuntimeTileScheduler",
|
||||
"TensorMapUpdateMode",
|
||||
"TensorMapManager",
|
||||
"GroupSearchResult",
|
||||
@@ -96,6 +104,18 @@ __all__ = [
|
||||
"create_initial_search_state",
|
||||
"GroupedGemmTileSchedulerHelper",
|
||||
"HardwareInfo",
|
||||
"TransformMode",
|
||||
"scale_tma_partition",
|
||||
"transform_partition",
|
||||
"scale_partition",
|
||||
"get_gmem_layout_scale",
|
||||
"get_smem_layout_scale",
|
||||
"compute_smem_layout",
|
||||
"get_transform_a_source",
|
||||
"get_tma_atom_kind",
|
||||
"get_copy_atom_a_transform",
|
||||
"is_valid_scale_granularity",
|
||||
"get_divisibility",
|
||||
"compute_epilogue_tile_shape",
|
||||
"get_smem_store_op",
|
||||
"get_tmem_load_op",
|
||||
@@ -107,4 +127,7 @@ __all__ = [
|
||||
"make_blockscaled_trivial_tiled_mma",
|
||||
"sm90",
|
||||
"sm100",
|
||||
"print_latex",
|
||||
"print_latex_tv",
|
||||
"distributed",
|
||||
]
|
||||
|
||||
@@ -689,7 +689,7 @@ def make_smem_layout_a(
|
||||
|
||||
is_k_major = tiled_mma.op.a_major_mode == OperandMajorMode.K
|
||||
a_smem_shape = tiled_mma.partition_shape_A(
|
||||
cute.dice(mma_tiler_mnk, (1, None, 1), loc=loc, ip=ip)
|
||||
cute.dice(mma_tiler_mnk, (1, None, 1), loc=loc, ip=ip), loc=loc, ip=ip
|
||||
)
|
||||
a_smem_shape_mn_k = (
|
||||
cute.size(a_smem_shape[0][0], loc=loc, ip=ip) * a_smem_shape[1],
|
||||
@@ -741,7 +741,7 @@ def make_smem_layout_b(
|
||||
|
||||
is_k_major = tiled_mma.op.b_major_mode == OperandMajorMode.K
|
||||
b_smem_shape = tiled_mma.partition_shape_B(
|
||||
cute.dice(mma_tiler_mnk, (None, 1, 1), loc=loc, ip=ip)
|
||||
cute.dice(mma_tiler_mnk, (None, 1, 1), loc=loc, ip=ip), loc=loc, ip=ip
|
||||
)
|
||||
b_smem_shape_nk = (
|
||||
cute.size(b_smem_shape[0][0], loc=loc, ip=ip) * b_smem_shape[1],
|
||||
@@ -939,7 +939,9 @@ def make_trivial_tiled_mma(
|
||||
else:
|
||||
raise TypeError(f"unsupported ab_dtype, got {ab_dtype}")
|
||||
|
||||
return cute.make_tiled_mma(cute.make_mma_atom(mma_op), loc=loc, ip=ip)
|
||||
return cute.make_tiled_mma(
|
||||
cute.make_mma_atom(mma_op, loc=loc, ip=ip), loc=loc, ip=ip
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
@@ -1009,7 +1011,9 @@ def make_blockscaled_trivial_tiled_mma(
|
||||
else:
|
||||
raise TypeError(f"unsupported ab_dtype, got {ab_dtype}")
|
||||
|
||||
return cute.make_tiled_mma(cute.make_mma_atom(mma_op), loc=loc, ip=ip)
|
||||
return cute.make_tiled_mma(
|
||||
cute.make_mma_atom(mma_op, loc=loc, ip=ip), loc=loc, ip=ip
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
# 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.cute.typing import Pointer, Int32
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
# misc
|
||||
"atomicAdd",
|
||||
"ld_bypass",
|
||||
# Message Passing Lock & Unlock
|
||||
"multimem_red_add1",
|
||||
"red_add1",
|
||||
"spin_lock_atom_cas_relaxed_wait",
|
||||
# Load & Store
|
||||
"multimem_ld_reduce_8xf16",
|
||||
"multimem_ld_reduce_4xf32",
|
||||
"multimem_ld_reduce_8xbf16",
|
||||
"multimem_ld_reduce_16xe4m3",
|
||||
"multimem_ld_reduce_16xe5m2",
|
||||
"multimem_st_4xb32",
|
||||
]
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
########################################################
|
||||
# Message Passing Lock & Unlock
|
||||
########################################################
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def multimem_red_release_gpu_add1(
|
||||
lock_ptr: Pointer,
|
||||
loc=None,
|
||||
ip=None,
|
||||
) -> None:
|
||||
llvm.inline_asm(
|
||||
None,
|
||||
[lock_ptr.toint().ir_value(loc=loc, ip=ip)],
|
||||
"multimem.red.release.gpu.global.add.u32 [$0], 1;",
|
||||
"l",
|
||||
has_side_effects=True,
|
||||
asm_dialect=0,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def multimem_red_release_sys_add1(
|
||||
lock_ptr: Pointer,
|
||||
loc=None,
|
||||
ip=None,
|
||||
) -> None:
|
||||
llvm.inline_asm(
|
||||
None,
|
||||
[lock_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_relaxed_gpu_add1(
|
||||
lock_ptr: Pointer,
|
||||
loc=None,
|
||||
ip=None,
|
||||
) -> None:
|
||||
llvm.inline_asm(
|
||||
None,
|
||||
[lock_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,
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def multimem_red_relaxed_sys_add1(
|
||||
lock_ptr: Pointer,
|
||||
loc=None,
|
||||
ip=None,
|
||||
) -> None:
|
||||
llvm.inline_asm(
|
||||
None,
|
||||
[lock_ptr.toint().ir_value(loc=loc, ip=ip)],
|
||||
"multimem.red.relaxed.sys.global.add.u32 [$0], 1;",
|
||||
"l",
|
||||
has_side_effects=True,
|
||||
asm_dialect=0,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def multimem_red_add1(
|
||||
lock_ptr: Pointer,
|
||||
*,
|
||||
order: str,
|
||||
scope: str,
|
||||
loc=None,
|
||||
ip=None,
|
||||
) -> None:
|
||||
"""
|
||||
add 1 to multicast ptr
|
||||
"""
|
||||
if scope == "gpu":
|
||||
if order == "release":
|
||||
multimem_red_release_gpu_add1(lock_ptr=lock_ptr, loc=loc, ip=ip)
|
||||
elif order == "relaxed":
|
||||
multimem_red_relaxed_gpu_add1(lock_ptr=lock_ptr, loc=loc, ip=ip)
|
||||
elif scope == "sys":
|
||||
if order == "release":
|
||||
multimem_red_release_sys_add1(lock_ptr=lock_ptr, loc=loc, ip=ip)
|
||||
elif order == "relaxed":
|
||||
multimem_red_relaxed_sys_add1(lock_ptr=lock_ptr, loc=loc, ip=ip)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def red_release_gpu_add1(
|
||||
lock_ptr: Pointer,
|
||||
loc=None,
|
||||
ip=None,
|
||||
) -> None:
|
||||
llvm.inline_asm(
|
||||
None,
|
||||
[lock_ptr.toint().ir_value(loc=loc, ip=ip)],
|
||||
"red.release.gpu.global.add.u32 [$0], 1;",
|
||||
"l",
|
||||
has_side_effects=True,
|
||||
asm_dialect=0,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def red_release_sys_add1(
|
||||
lock_ptr: Pointer,
|
||||
loc=None,
|
||||
ip=None,
|
||||
) -> None:
|
||||
llvm.inline_asm(
|
||||
None,
|
||||
[lock_ptr.toint().ir_value(loc=loc, ip=ip)],
|
||||
"red.release.sys.global.add.u32 [$0], 1;",
|
||||
"l",
|
||||
has_side_effects=True,
|
||||
asm_dialect=0,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def red_relaxed_gpu_add1(
|
||||
lock_ptr: Pointer,
|
||||
loc=None,
|
||||
ip=None,
|
||||
) -> None:
|
||||
llvm.inline_asm(
|
||||
None,
|
||||
[lock_ptr.toint().ir_value(loc=loc, ip=ip)],
|
||||
"red.relaxed.gpu.global.add.u32 [$0], 1;",
|
||||
"l",
|
||||
has_side_effects=True,
|
||||
asm_dialect=0,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def red_relaxed_sys_add1(
|
||||
lock_ptr: Pointer,
|
||||
loc=None,
|
||||
ip=None,
|
||||
) -> None:
|
||||
llvm.inline_asm(
|
||||
None,
|
||||
[lock_ptr.toint().ir_value(loc=loc, ip=ip)],
|
||||
"red.relaxed.sys.global.add.u32 [$0], 1;",
|
||||
"l",
|
||||
has_side_effects=True,
|
||||
asm_dialect=0,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def red_add1(
|
||||
lock_ptr: Pointer,
|
||||
*,
|
||||
order: str,
|
||||
scope: str,
|
||||
loc=None,
|
||||
ip=None,
|
||||
) -> None:
|
||||
"""
|
||||
add 1 to multicast ptr
|
||||
"""
|
||||
if scope == "gpu":
|
||||
if order == "release":
|
||||
red_release_gpu_add1(lock_ptr=lock_ptr, loc=loc, ip=ip)
|
||||
elif order == "relaxed":
|
||||
red_relaxed_gpu_add1(lock_ptr=lock_ptr, loc=loc, ip=ip)
|
||||
elif scope == "sys":
|
||||
if order == "release":
|
||||
red_release_sys_add1(lock_ptr=lock_ptr, loc=loc, ip=ip)
|
||||
elif order == "relaxed":
|
||||
red_relaxed_sys_add1(lock_ptr=lock_ptr, loc=loc, ip=ip)
|
||||
|
||||
|
||||
@cute.jit
|
||||
def spin_lock_atom_cas_relaxed_wait(
|
||||
lock_ptr: Pointer,
|
||||
*,
|
||||
expected_val: Int32,
|
||||
reset_val: Int32,
|
||||
scope: str,
|
||||
loc=None,
|
||||
ip=None,
|
||||
) -> None:
|
||||
"""
|
||||
wait on a spin lock until the expected count is reached. Reset flag to reset_val if the expected count is reached.
|
||||
"""
|
||||
if scope == "gpu":
|
||||
result = 0
|
||||
while result != expected_val:
|
||||
result = nvvm.atomicrmw(
|
||||
T.i32(),
|
||||
AtomicOpKind.CAS,
|
||||
lock_ptr.llvm_ptr,
|
||||
Int32(reset_val).ir_value(loc=loc, ip=ip),
|
||||
b=Int32(expected_val).ir_value(loc=loc, ip=ip),
|
||||
mem_order=MemOrderKind.RELAXED,
|
||||
syncscope=MemScopeKind.GPU,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
elif scope == "sys":
|
||||
result = 0
|
||||
while result != expected_val:
|
||||
result = nvvm.atomicrmw(
|
||||
T.i32(),
|
||||
AtomicOpKind.CAS,
|
||||
lock_ptr.llvm_ptr,
|
||||
Int32(reset_val).ir_value(loc=loc, ip=ip),
|
||||
b=Int32(expected_val).ir_value(loc=loc, ip=ip),
|
||||
mem_order=MemOrderKind.RELAXED,
|
||||
syncscope=MemScopeKind.SYS,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
|
||||
########################################################
|
||||
# Multimem Load & Store
|
||||
########################################################
|
||||
|
||||
|
||||
@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,
|
||||
)
|
||||
@@ -171,8 +171,18 @@ class GroupedGemmTileSchedulerHelper:
|
||||
def __new_from_mlir_values__(
|
||||
self, values: List[ir.Value]
|
||||
) -> "GroupedGemmTileSchedulerHelper":
|
||||
tile_sched_params = new_from_mlir_values(self.tile_sched_params, values)
|
||||
search_state = new_from_mlir_values(self.search_state, values[1:])
|
||||
# Reconstruct tile_sched_params and determine how many values it consumed.
|
||||
# NOTE: tile_sched_params may contain FastDivmod divisors (when swizzle_size == 1),
|
||||
# which adds extra MLIR values.
|
||||
params_values = extract_mlir_values(self.tile_sched_params)
|
||||
n_params_values = len(params_values)
|
||||
tile_sched_params = new_from_mlir_values(
|
||||
self.tile_sched_params, values[:n_params_values]
|
||||
)
|
||||
|
||||
# Reconstruct search_state from remaining values
|
||||
search_state = new_from_mlir_values(self.search_state, values[n_params_values:])
|
||||
|
||||
return GroupedGemmTileSchedulerHelper(
|
||||
self.group_count,
|
||||
tile_sched_params,
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
# and related documentation outside the scope permitted by the EULA
|
||||
# is strictly prohibited.
|
||||
|
||||
from cuda.bindings import driver, nvrtc
|
||||
from cuda.bindings import driver, nvrtc, runtime
|
||||
from cutlass.cutlass_dsl.cuda_jit_executor import CudaDialectJitModule
|
||||
from cutlass.base_dsl.common import DSLRuntimeError
|
||||
|
||||
import cutlass.cute as cute
|
||||
|
||||
@@ -51,7 +53,7 @@ class HardwareInfo:
|
||||
f"Cluster size must be between 1 and 32, {cluster_size} is not supported"
|
||||
)
|
||||
|
||||
device_fn = self._get_device_function(self.device)
|
||||
self._get_device_function(self.device)
|
||||
|
||||
max_shared_memory_per_block = self._checkCudaErrors(
|
||||
driver.cuDeviceGetAttribute(
|
||||
@@ -173,7 +175,14 @@ class HardwareInfo:
|
||||
)
|
||||
|
||||
# get a empty kernel to compute occupancy
|
||||
def _get_device_function(self, device) -> None:
|
||||
def _get_device_function(self, device) -> driver.CUfunction:
|
||||
self.compiled_kernel = cute.compile(self._host_function).to(device)
|
||||
self.kernel = self.compiled_kernel.exec_context.kernel_functions[0]
|
||||
self.module = self.compiled_kernel.exec_context.module.cuda_modules[0]
|
||||
assert isinstance(self.compiled_kernel.jit_module, CudaDialectJitModule)
|
||||
err, kernels = runtime.cudaLibraryEnumerateKernels(
|
||||
1, self.compiled_kernel.jit_module.cuda_library[0]
|
||||
)
|
||||
if err is not runtime.cudaError_t.cudaSuccess:
|
||||
raise DSLRuntimeError(f"Failed to enumerate kernels: {err}")
|
||||
self.kernel = kernels[0]
|
||||
self.kernel = self._checkCudaErrors(driver.cuKernelGetFunction(self.kernel))
|
||||
return self.kernel
|
||||
|
||||
@@ -0,0 +1,507 @@
|
||||
# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
|
||||
# 1. Redistributions of source code must retain the above copyright notice, this
|
||||
# list of conditions and the following disclaimer.
|
||||
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum, auto
|
||||
from math import log2
|
||||
from typing import Optional, Union
|
||||
|
||||
import cutlass
|
||||
import cutlass.cute as cute
|
||||
import cutlass.utils.blackwell_helpers as sm100_utils
|
||||
from cutlass.cute.nvgpu import cpasync, tcgen05
|
||||
from cutlass.cute.runtime import from_dlpack
|
||||
|
||||
"""
|
||||
This file contain common utilities for mixed-input GEMM.
|
||||
"""
|
||||
|
||||
|
||||
class TransformMode(Enum):
|
||||
"""
|
||||
An enumeration for the possible transform modes of a mixed-input GEMM.
|
||||
"""
|
||||
|
||||
ConvertOnly = auto()
|
||||
ConvertScale = auto()
|
||||
|
||||
|
||||
def scale_tma_partition(
|
||||
tCsS: cute.Tensor,
|
||||
tCgS: cute.Tensor,
|
||||
tma_atom_s: cute.CopyAtom,
|
||||
block_in_cluster_coord_vmnk: cute.Coord,
|
||||
scale_cta_layout: cute.Layout,
|
||||
) -> tuple[cute.Tensor, cute.Tensor]:
|
||||
"""
|
||||
Perform TMA partition for scale tensor.
|
||||
This method partitions the gobal memory and shared memory buffer for scale tensor for TMA load.
|
||||
:param tCsS: Input scale shared memory tensor
|
||||
:type tCsS: cute.Tensor
|
||||
:param tCgS: Input scale global memory tensor
|
||||
:type tCgS: cute.Tensor
|
||||
:param tma_atom_s: TMA copy atom for scale tensor
|
||||
:type tma_atom_s: cute.CopyAtom
|
||||
:param block_in_cluster_coord_vmnk: CTA coord in the cluster
|
||||
:type block_in_cluster_coord_vmnk: cute.Coord
|
||||
:param scale_cta_layout: Layout of CTA from the view of the scale tensor
|
||||
:type scale_cta_layout: cute.Layout
|
||||
:return: A tuple containing (tSsS, tSgS) where:
|
||||
|
||||
* tSsS: Partitioned scale tensor in shared memory
|
||||
* tSgS: Partitioned scale tensor in global memory
|
||||
|
||||
:rtype: tuple[cute.Tensor, cute.Tensor]
|
||||
"""
|
||||
tSsS, tSgS = cpasync.tma_partition(
|
||||
tma_atom_s,
|
||||
block_in_cluster_coord_vmnk[2],
|
||||
scale_cta_layout,
|
||||
cute.group_modes(tCsS, 0, 3),
|
||||
cute.group_modes(tCgS, 0, 3),
|
||||
)
|
||||
# Add rest_v mode
|
||||
# ((atom_v, rest_v), STAGE)
|
||||
# ((atom_v, rest_v), loopM, loopK, loopL)
|
||||
tSsS = cute.make_tensor(
|
||||
tSsS.iterator,
|
||||
cute.make_layout(
|
||||
((tSsS.layout.shape[0], 1), *tSsS.layout.shape[1:]),
|
||||
stride=(
|
||||
(tSsS.layout.stride[0], 0),
|
||||
*tSsS.layout.stride[1:],
|
||||
),
|
||||
),
|
||||
)
|
||||
tSgS = cute.make_tensor(
|
||||
tSgS.iterator,
|
||||
cute.make_layout(
|
||||
((tSgS.layout.shape[0], 1), *tSgS.layout.shape[1:]),
|
||||
stride=(
|
||||
(tSgS.layout.stride[0], 0),
|
||||
*tSgS.layout.stride[1:],
|
||||
),
|
||||
),
|
||||
)
|
||||
return tSsS, tSgS
|
||||
|
||||
|
||||
def transform_partition(
|
||||
transform_a_source: tcgen05.OperandSource,
|
||||
scale_mode: TransformMode,
|
||||
copy_atom_a_input: cute.CopyAtom,
|
||||
copy_atom_a_transform: cute.CopyAtom,
|
||||
sA_input: cute.Tensor,
|
||||
A_transform: cute.Tensor,
|
||||
transform_local_tidx: cutlass.Int32,
|
||||
) -> tuple[cute.TiledCopy, cute.TiledCopy, cute.Tensor, cute.Tensor]:
|
||||
"""
|
||||
Partition tensors for transform input and output.
|
||||
This method sets up the copy atoms and partitions the shared/tensor memory
|
||||
for the transformation of tensor A.
|
||||
:param transform_a_source: Where the transformed tensor A is stored (TMEM or SMEM)
|
||||
:type transform_a_source: tcgen05.OperandSource
|
||||
:param scale_mode: The transform mode (ConvertOnly or ConvertScale)
|
||||
:type scale_mode: TransformMode
|
||||
:param copy_atom_a_input: Copy atom for loading A from shared memory
|
||||
:type copy_atom_a_input: cute.CopyAtom
|
||||
:param copy_atom_a_transform: Copy atom for storing transformed A
|
||||
:type copy_atom_a_transform: cute.CopyAtom
|
||||
:param sA_input: Input tensor A in shared memory
|
||||
:type sA_input: cute.Tensor
|
||||
:param A_transform: Transformed tensor A in tensor or shared memory
|
||||
:type A_transform: cute.Tensor
|
||||
:param transform_local_tidx: Local thread index for transformation warps
|
||||
:type transform_local_tidx: cutlass.Int32
|
||||
:return: A tuple containing (src_copy_a, dst_copy_a, tAsA_input, tA_transform) where:
|
||||
|
||||
* src_copy_a: Tiled copy for source tensor
|
||||
* dst_copy_a: Tiled copy for destination tensor
|
||||
* tAsA_input: Partitioned input tensor A
|
||||
* tA_transform: Partitioned transformed tensor A
|
||||
|
||||
:rtype: tuple[cute.TiledCopy, cute.TiledCopy, cute.Tensor, cute.Tensor]
|
||||
"""
|
||||
if cutlass.const_expr(transform_a_source == tcgen05.OperandSource.TMEM):
|
||||
if cutlass.const_expr(
|
||||
cute.size(A_transform, mode=[0, 0]) == 128
|
||||
and cute.size(sA_input, mode=[0, 0]) == 64
|
||||
):
|
||||
tensor_input = cute.make_tensor(
|
||||
sA_input.iterator,
|
||||
cute.logical_product(
|
||||
sA_input.layout,
|
||||
((cute.make_layout(2, stride=0), None), None, None, None),
|
||||
),
|
||||
)
|
||||
else:
|
||||
tensor_input = sA_input
|
||||
reg2tmem_tiled_copy = tcgen05.make_tmem_copy(
|
||||
copy_atom_a_transform, A_transform[(None, None, None, 0)]
|
||||
)
|
||||
thr_reg2tmem_tiled_copy = reg2tmem_tiled_copy.get_slice(transform_local_tidx)
|
||||
partitioned_tensor_input = thr_reg2tmem_tiled_copy.partition_S(tensor_input)
|
||||
partitioned_tensor_transform = thr_reg2tmem_tiled_copy.partition_D(A_transform)
|
||||
src_copy_a = (
|
||||
cute.make_tiled_copy_S(copy_atom_a_input, reg2tmem_tiled_copy)
|
||||
if scale_mode is TransformMode.ConvertScale
|
||||
else None
|
||||
)
|
||||
dst_copy_a = reg2tmem_tiled_copy
|
||||
tAsA_input = partitioned_tensor_input
|
||||
tA_transform = partitioned_tensor_transform
|
||||
elif cutlass.const_expr(transform_a_source == tcgen05.OperandSource.SMEM):
|
||||
# Construct tiled_copy satisfying 8 contiguous elts per copy atom
|
||||
reg2smem_tiled_copy = cute.make_cotiled_copy(
|
||||
copy_atom_a_transform,
|
||||
cute.make_layout((128, 8), stride=(8, 1)),
|
||||
A_transform[(None, None, None, 0)].layout,
|
||||
)
|
||||
thr_reg2smem_tiled_copy = reg2smem_tiled_copy.get_slice(transform_local_tidx)
|
||||
partitioned_tensor_input = thr_reg2smem_tiled_copy.partition_S(sA_input)
|
||||
partitioned_tensor_transform = thr_reg2smem_tiled_copy.partition_D(A_transform)
|
||||
src_copy_a = (
|
||||
cute.make_tiled_copy_S(copy_atom_a_input, reg2smem_tiled_copy)
|
||||
if scale_mode is TransformMode.ConvertScale
|
||||
else None
|
||||
)
|
||||
# auto-vec copy is enough for copy from register to shared memory here
|
||||
dst_copy_a = None
|
||||
tAsA_input = partitioned_tensor_input
|
||||
tA_transform = partitioned_tensor_transform
|
||||
return src_copy_a, dst_copy_a, tAsA_input, tA_transform
|
||||
|
||||
|
||||
def scale_partition(
|
||||
src_copy_a: cute.TiledCopy,
|
||||
tCsS: cute.Tensor,
|
||||
transform_local_tidx: cutlass.Int32,
|
||||
mma_dtype: type[cutlass.Numeric],
|
||||
) -> tuple[cute.TiledCopy, cute.Tensor, cute.Tensor, cute.Tensor]:
|
||||
"""
|
||||
Partition the scale tensor for transformation.
|
||||
This method prepares the copy atom and partitions the shared memory for the scale tensor.
|
||||
:param src_copy_a: Tiled copy for the source tensor
|
||||
:type src_copy_a: cute.TiledCopy
|
||||
:param tCsS: Scale tensor in shared memory
|
||||
:type tCsS: cute.Tensor
|
||||
:param transform_local_tidx: Local thread index for transformation warps
|
||||
:type transform_local_tidx: cutlass.Int32
|
||||
:param mma_dtype: Data type for the MMA operation
|
||||
:type mma_dtype: type[cutlass.Numeric]
|
||||
:return: A tuple containing (smem_thr_copy_S, tSsS_trans, tSrS_copy, tSrS) where:
|
||||
|
||||
* smem_thr_copy_S: Tiled copy for the scale tensor
|
||||
* tSsS_trans: Partitioned scale tensor for transformation
|
||||
* tSrS_copy: Register fragment for the scale tensor
|
||||
* tSrS: View of scale tensor used for transformation computation
|
||||
|
||||
:rtype: tuple[cute.TiledCopy, cute.Tensor, cute.Tensor, cute.Tensor]
|
||||
"""
|
||||
smem_thr_copy_S = None
|
||||
tSsS_trans = None
|
||||
tSrS = None
|
||||
# Partition scale tensor
|
||||
smem_thr_copy_S = src_copy_a.get_slice(transform_local_tidx)
|
||||
tSsS_trans = smem_thr_copy_S.partition_S(tCsS)
|
||||
# Construct register fragment for scale tensor
|
||||
tSsS_layout_per_stage = tSsS_trans[(None, None, None, None, 0)].layout
|
||||
# tSrS for copy
|
||||
tSrS_copy = cute.make_rmem_tensor(
|
||||
cute.filter_zeros(tSsS_layout_per_stage).shape, mma_dtype
|
||||
)
|
||||
# tSrS view for transformation computation
|
||||
tSrS = cute.make_tensor(
|
||||
tSrS_copy.iterator,
|
||||
cute.make_layout(tSsS_layout_per_stage.shape, stride=tSrS_copy.layout.stride),
|
||||
)
|
||||
return smem_thr_copy_S, tSsS_trans, tSrS_copy, tSrS
|
||||
|
||||
|
||||
def get_gmem_layout_scale(
|
||||
scale_shape_mkl: tuple[int, int, int],
|
||||
scale_granularity_m: int,
|
||||
scale_granularity_k: int,
|
||||
scale_major_mode: tcgen05.OperandMajorMode,
|
||||
) -> cute.Layout:
|
||||
"""
|
||||
Get the layout of the scale tensor in global memory.
|
||||
:param scale_shape_mkl: The shape of the scale tensor (M, K, L).
|
||||
:type scale_shape_mkl: tuple[int, int, int]
|
||||
:return: The layout of the scale tensor in global memory.
|
||||
:rtype: cute.Layout
|
||||
"""
|
||||
m, k, l = scale_shape_mkl
|
||||
shape_scale = (
|
||||
(scale_granularity_m, cute.ceil_div(m, scale_granularity_m)),
|
||||
(scale_granularity_k, cute.ceil_div(k, scale_granularity_k)),
|
||||
)
|
||||
if cutlass.const_expr(scale_major_mode == tcgen05.OperandMajorMode.MN):
|
||||
layout_mk = cute.make_layout(
|
||||
shape_scale,
|
||||
stride=(
|
||||
(0, 1),
|
||||
(0, cute.size(shape_scale[0][1])),
|
||||
),
|
||||
)
|
||||
else:
|
||||
layout_mk = cute.make_layout(
|
||||
shape_scale,
|
||||
stride=(
|
||||
(0, cute.size(shape_scale[1][1])),
|
||||
(0, 1),
|
||||
),
|
||||
)
|
||||
return cute.make_layout(
|
||||
(*layout_mk.shape, l),
|
||||
stride=(*layout_mk.stride, cute.cosize(layout_mk)),
|
||||
)
|
||||
|
||||
|
||||
def get_smem_layout_scale(
|
||||
mma_tiler: tuple[int, int, int],
|
||||
use_2cta_instrs: bool,
|
||||
scale_granularity_m: int,
|
||||
scale_granularity_k: int,
|
||||
scale_major_mode: tcgen05.OperandMajorMode,
|
||||
a_scale_dtype: type[cutlass.Numeric],
|
||||
num_scale_load2trans_stage: int,
|
||||
) -> tuple[tuple[int, int], cute.ComposedLayout, cute.ComposedLayout]:
|
||||
"""
|
||||
Get the layout of the scale tensor in shared memory.
|
||||
:return: A tuple containing (scale_tile_shape, smem_layout_scale_per_stage, smem_layout_scale) where:
|
||||
|
||||
* scale_tile_shape: The tile shape
|
||||
* smem_layout_scale_per_stage: Shared memory layout for scale tensor per stage
|
||||
* smem_layout_scale: Shared memory layout for scale tensor
|
||||
|
||||
:rtype: tuple[tuple[int, int], cute.ComposedLayout, cute.ComposedLayout]
|
||||
"""
|
||||
scale_tile_shape = (
|
||||
(cute.size(mma_tiler[0]) // 2 if use_2cta_instrs else cute.size(mma_tiler[0])),
|
||||
cute.size(mma_tiler[2]),
|
||||
)
|
||||
size_mn = scale_tile_shape[0]
|
||||
size_k = scale_tile_shape[1]
|
||||
smem_size_mn = scale_granularity_m if scale_granularity_m < size_mn else size_mn
|
||||
smem_size_k = scale_granularity_k if scale_granularity_k < size_k else size_k
|
||||
div_mn = cute.ceil_div(size_mn, smem_size_mn)
|
||||
div_k = cute.ceil_div(size_k, smem_size_k)
|
||||
smem_atom_shape = (
|
||||
(smem_size_mn, div_mn),
|
||||
(smem_size_k, div_k),
|
||||
)
|
||||
if cutlass.const_expr(scale_major_mode == tcgen05.OperandMajorMode.MN):
|
||||
outer_layout = cute.make_layout(
|
||||
smem_atom_shape,
|
||||
stride=(
|
||||
(0, 1),
|
||||
(0, div_mn),
|
||||
),
|
||||
)
|
||||
else:
|
||||
outer_layout = cute.make_layout(
|
||||
smem_atom_shape,
|
||||
stride=(
|
||||
(0, div_k),
|
||||
(0, 1),
|
||||
),
|
||||
)
|
||||
# Apply a trivial swizzle to make it a composed layout, which could be used to construct TMA atom
|
||||
smem_layout_scale_per_stage = cute.make_composed_layout(
|
||||
cute.make_swizzle(0, 4, 3), 0, outer_layout
|
||||
)
|
||||
assert cute.rank(smem_layout_scale_per_stage) == 2, "Scale layout must be rank 2"
|
||||
assert (
|
||||
cute.size(mma_tiler[0]) % cute.size(smem_layout_scale_per_stage.outer[0]) == 0
|
||||
), "smem_layout_scale_per_stage must equal the tile shape."
|
||||
assert (
|
||||
cute.size(mma_tiler[2]) % cute.size(smem_layout_scale_per_stage.outer[1]) == 0
|
||||
), "smem_layout_scale_per_stage must evenly divide tile k shape."
|
||||
# Shared memory buffer for scale must be at least 128B to satisfy TMA requirement
|
||||
|
||||
assert cute.size_in_bytes(a_scale_dtype, smem_layout_scale_per_stage) >= 128, (
|
||||
"smem size for scale must be at least 128B"
|
||||
)
|
||||
# Scale layout in smem with multiple stages
|
||||
smem_layout_scale = cute.append(
|
||||
smem_layout_scale_per_stage,
|
||||
cute.make_layout(
|
||||
(num_scale_load2trans_stage),
|
||||
stride=(cute.cosize(smem_layout_scale_per_stage.outer)),
|
||||
),
|
||||
)
|
||||
return scale_tile_shape, smem_layout_scale_per_stage, smem_layout_scale
|
||||
|
||||
|
||||
def compute_smem_layout(
|
||||
tiled_mma: cute.TiledMma,
|
||||
mma_tiler_mnk: tuple[int, int, int],
|
||||
a_dtype: type[cutlass.Numeric],
|
||||
b_dtype: type[cutlass.Numeric],
|
||||
load2trans_stage_count: int,
|
||||
trans2mma_stage_count: int,
|
||||
) -> tuple[
|
||||
cute.ComposedLayout,
|
||||
cute.ComposedLayout,
|
||||
cute.ComposedLayout,
|
||||
]:
|
||||
"""
|
||||
Compute shared memory layouts for tensor A, transformed A and tensor B.
|
||||
:param tiled_mma: The tiled MMA object defining the core computation.
|
||||
:type tiled_mma: cute.TiledMma
|
||||
:param mma_tiler_mnk: The shape (M, N, K) of the MMA tiler.
|
||||
:type mma_tiler_mnk: tuple[int, int, int]
|
||||
:param a_dtype: Data type of operand A.
|
||||
:type a_dtype: type[cutlass.Numeric]
|
||||
:param b_dtype: Data type of operand B.
|
||||
:type b_dtype: type[cutlass.Numeric]
|
||||
:param load2trans_stage_count: Number of stages for load-to-transform pipeline.
|
||||
:type load2trans_stage_count: int
|
||||
:param trans2mma_stage_count: Number of stages for transform-to-MMA pipeline.
|
||||
:type trans2mma_stage_count: int
|
||||
:return: A tuple containing (smem_layout_a, smem_layout_a_transform, smem_layout_b) where:
|
||||
|
||||
* smem_layout_a: Shared memory layout for tensor A
|
||||
* smem_layout_a_transform: Shared memory layout for transformed tensor A
|
||||
* smem_layout_b: Shared memory layout for tensor B
|
||||
|
||||
:rtype: tuple[cute.ComposedLayout, cute.ComposedLayout, cute.ComposedLayout]
|
||||
"""
|
||||
smem_layout_a = sm100_utils.make_smem_layout_a(
|
||||
tiled_mma,
|
||||
mma_tiler_mnk,
|
||||
a_dtype,
|
||||
load2trans_stage_count,
|
||||
)
|
||||
smem_layout_a_transform = sm100_utils.make_smem_layout_a(
|
||||
tiled_mma,
|
||||
mma_tiler_mnk,
|
||||
tiled_mma.op.a_dtype,
|
||||
trans2mma_stage_count,
|
||||
)
|
||||
smem_layout_b = sm100_utils.make_smem_layout_b(
|
||||
tiled_mma,
|
||||
mma_tiler_mnk,
|
||||
b_dtype,
|
||||
load2trans_stage_count,
|
||||
)
|
||||
return (smem_layout_a, smem_layout_a_transform, smem_layout_b)
|
||||
|
||||
|
||||
def get_transform_a_source(
|
||||
a_major_mode: tcgen05.OperandMajorMode,
|
||||
) -> tcgen05.OperandSource:
|
||||
"""
|
||||
Determine the operand source for transformed A tensor based on the operand major mode.
|
||||
"""
|
||||
if cutlass.const_expr(a_major_mode == tcgen05.OperandMajorMode.K):
|
||||
return tcgen05.OperandSource.TMEM
|
||||
else:
|
||||
return tcgen05.OperandSource.SMEM
|
||||
|
||||
|
||||
def get_tma_atom_kind(
|
||||
mcast: cutlass.Boolean,
|
||||
use_2cta_instrs: bool,
|
||||
is_b: bool,
|
||||
) -> Union[cpasync.CopyBulkTensorTileG2SMulticastOp, cpasync.CopyBulkTensorTileG2SOp]:
|
||||
"""
|
||||
Get the TMA atom kind based on 1) whether it's a multicast operation,
|
||||
2) whether 2CTA tcgen05.mma instruction is enabled, and
|
||||
3) whether it's a B tensor
|
||||
"""
|
||||
# Not using .2CTA instructions for tensor A as the consumer is threads on different CTAs
|
||||
cta_group = (
|
||||
tcgen05.CtaGroup.TWO if (use_2cta_instrs and is_b) else tcgen05.CtaGroup.ONE
|
||||
)
|
||||
if cutlass.const_expr(mcast):
|
||||
return cpasync.CopyBulkTensorTileG2SMulticastOp(cta_group)
|
||||
return cpasync.CopyBulkTensorTileG2SOp(cta_group)
|
||||
|
||||
|
||||
def get_copy_atom_a_transform(
|
||||
mma_dtype: type[cutlass.Numeric],
|
||||
use_2cta_instrs: bool,
|
||||
transform_a_source: tcgen05.OperandSource,
|
||||
a_smem_shape: cute.Shape,
|
||||
a_dtype: type[cutlass.Numeric],
|
||||
) -> cute.CopyAtom:
|
||||
"""
|
||||
Determine the copy atom for transformed A tensor based on the operand source and tile size.
|
||||
"""
|
||||
if cutlass.const_expr(transform_a_source == tcgen05.OperandSource.TMEM):
|
||||
if cutlass.const_expr(
|
||||
cute.size(a_smem_shape[0][0]) == 64 and (not use_2cta_instrs)
|
||||
):
|
||||
copy_op_r2t = tcgen05.St16x256bOp(
|
||||
tcgen05.Repetition(1), tcgen05.Unpack.NONE
|
||||
)
|
||||
else:
|
||||
copy_op_r2t = tcgen05.St32x32bOp(tcgen05.Repetition(8), tcgen05.Unpack.NONE)
|
||||
return cute.make_copy_atom(copy_op_r2t, mma_dtype)
|
||||
else:
|
||||
return cute.make_copy_atom(
|
||||
cute.nvgpu.CopyUniversalOp(), a_dtype, num_bits_per_copy=32
|
||||
)
|
||||
|
||||
|
||||
def is_valid_scale_granularity(
|
||||
scale_granularity_m: int,
|
||||
scale_granularity_k: int,
|
||||
a_dtype: type[cutlass.Numeric],
|
||||
k: int,
|
||||
mma_tiler_k: int,
|
||||
) -> bool:
|
||||
"""
|
||||
Check if the scale granularity settings are valid for the given data type and problem size.
|
||||
"""
|
||||
if a_dtype.width == 8:
|
||||
# No scale tensor for 8bit data type A
|
||||
if not (scale_granularity_m == 0 and scale_granularity_k == 0):
|
||||
return False
|
||||
elif a_dtype.width == 4:
|
||||
if scale_granularity_m != 1 or (
|
||||
scale_granularity_k == 0
|
||||
or k % scale_granularity_k != 0
|
||||
or scale_granularity_k % mma_tiler_k != 0
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def get_divisibility(contiguous_dim_size: int, upper_bound: int = 128) -> int:
|
||||
"""
|
||||
Calculate the largest power of 2 divisibility factor for memory alignment.
|
||||
"""
|
||||
# Check the largest power of 2 factor of contiguous_dim_size
|
||||
for i in range(int(log2(contiguous_dim_size)), 0, -1):
|
||||
if contiguous_dim_size % (2**i) == 0:
|
||||
return min(2**i, upper_bound)
|
||||
return 1
|
||||
@@ -0,0 +1,165 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
|
||||
#
|
||||
# Use of this software is governed by the terms and conditions of the
|
||||
# NVIDIA End User License Agreement (EULA), available at:
|
||||
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
|
||||
#
|
||||
# Any use, reproduction, disclosure, or distribution of this software
|
||||
# and related documentation outside the scope permitted by the EULA
|
||||
# is strictly prohibited.
|
||||
|
||||
from typing import Callable, Union
|
||||
|
||||
from ..cute import (
|
||||
Layout,
|
||||
ComposedLayout,
|
||||
append,
|
||||
is_static,
|
||||
make_layout,
|
||||
size,
|
||||
product_each,
|
||||
rank,
|
||||
)
|
||||
from ..cute.typing import IntTuple
|
||||
|
||||
__all__ = ["print_latex", "print_latex_tv"]
|
||||
|
||||
|
||||
def tikz_color_bwx8(idx: int):
|
||||
color_map = [
|
||||
"black!00",
|
||||
"black!40",
|
||||
"black!20",
|
||||
"black!60",
|
||||
"black!10",
|
||||
"black!50",
|
||||
"black!30",
|
||||
"black!70",
|
||||
]
|
||||
return color_map[idx % 8]
|
||||
|
||||
|
||||
def tikz_color_white(idx: int):
|
||||
return "white"
|
||||
|
||||
|
||||
def tikz_color_tv(tid: int, vid: int):
|
||||
color_map = [
|
||||
"{rgb,255:red,175;green,175;blue,255}",
|
||||
"{rgb,255:red,175;green,255;blue,175}",
|
||||
"{rgb,255:red,255;green,255;blue,175}",
|
||||
"{rgb,255:red,255;green,175;blue,175}",
|
||||
"{rgb,255:red,210;green,210;blue,255}",
|
||||
"{rgb,255:red,210;green,255;blue,210}",
|
||||
"{rgb,255:red,255;green,255;blue,210}",
|
||||
"{rgb,255:red,255;green,210;blue,210}",
|
||||
]
|
||||
return color_map[tid % 8]
|
||||
|
||||
|
||||
def print_latex(x: Union[Layout, ComposedLayout], *, color: Callable = tikz_color_bwx8):
|
||||
"""
|
||||
Prints a layout.
|
||||
:param x: A layout
|
||||
:type x: Union[Layout, ComposedLayout]
|
||||
:param color: A function that returns TiKZ colors
|
||||
:type color: Callable
|
||||
"""
|
||||
|
||||
if not is_static(x):
|
||||
raise ValueError("Requires static input")
|
||||
if rank(x) > 2:
|
||||
raise ValueError("Requires rank <= 2 to print")
|
||||
|
||||
if rank(x) == 1:
|
||||
layout = append(x, make_layout(1, stride=0))
|
||||
else:
|
||||
layout = x
|
||||
|
||||
print("%% Layout: {}", layout)
|
||||
print("\\documentclass[convert]{standalone}")
|
||||
print("\\usepackage{tikz}")
|
||||
print("\\begin{document}")
|
||||
print(
|
||||
"\\begin{tikzpicture}[x={(0cm,-1cm)},y={(1cm,0cm)},every node/.style={minimum size=1cm, outer sep=0pt}]"
|
||||
)
|
||||
|
||||
M, N = product_each(x.shape)
|
||||
|
||||
for m in range(M):
|
||||
for n in range(N):
|
||||
idx = layout((m, n))
|
||||
print("\\node[fill=")
|
||||
print(color(idx))
|
||||
print("] at (%d,%d) {%d};\n" % (m, n, idx))
|
||||
print(
|
||||
"\\draw[color=black,thick,shift={(-0.5,-0.5)}] (0,0) grid (%d,%d);\n\n" % (M, N)
|
||||
)
|
||||
for m in range(M):
|
||||
print("\\node at (%d,%d) {\\Large{\\texttt{%d}}};\n" % (m, -1, m))
|
||||
for n in range(N):
|
||||
print("\\node at (%d,%d) {\\Large{\\texttt{%d}}};\n" % (-1, n, n))
|
||||
|
||||
## Footer
|
||||
print("\\end{tikzpicture}")
|
||||
print("\\end{document}")
|
||||
|
||||
|
||||
def print_latex_tv(
|
||||
layout_tv: Union[Layout, ComposedLayout],
|
||||
tile_mn: Union[IntTuple, Layout],
|
||||
*,
|
||||
color: Callable = tikz_color_tv,
|
||||
):
|
||||
"""
|
||||
Prints a tv layout for a tile M N. Everything must be static.
|
||||
:param layout_tv: A static thread value layout
|
||||
:type layout_tv: Union[Layout, ComposedLayout]
|
||||
:param tile_mn: A static M N tile
|
||||
:type tile_mn: Union[IntTuple, Layout]
|
||||
:param color: A function that returns TiKZ colors
|
||||
:type color: Callable
|
||||
"""
|
||||
if not is_static(layout_tv) or not is_static(tile_mn):
|
||||
raise ValueError("Layout tv and tile_mn must be static")
|
||||
if rank(layout_tv) != 2:
|
||||
raise ValueError("Require layout_tv to be rank 2")
|
||||
|
||||
print("%% Layout TV: {}", layout_tv)
|
||||
print("\\documentclass[convert]{standalone}")
|
||||
print("\\usepackage{tikz}")
|
||||
print("\\begin{document}")
|
||||
print(
|
||||
"\\begin{tikzpicture}[x={(0cm,-1cm)},y={(1cm,0cm)},every node/.style={minimum size=1cm, outer sep=0pt}]\n"
|
||||
)
|
||||
|
||||
if not isinstance(tile_mn, Layout):
|
||||
tile_mn = make_layout(tile_mn)
|
||||
|
||||
M, N = product_each(tile_mn.shape)
|
||||
filled = [[False for n in range(N)] for m in range(M)]
|
||||
|
||||
for tid in range(size(layout_tv, mode=[0])):
|
||||
for vid in range(size(layout_tv, mode=[1])):
|
||||
idx = layout_tv((tid, vid))
|
||||
m = (idx // tile_mn.stride[0]) % tile_mn.shape[0]
|
||||
n = (idx // tile_mn.stride[1]) % tile_mn.shape[1]
|
||||
if not filled[m][n]:
|
||||
filled[m][n] = True
|
||||
print(
|
||||
"\\node[fill=%s] at (%d,%d) {\\shortstack{T%d \\\\ V%d}};\n"
|
||||
% (color(tid, vid), m, n, tid, vid)
|
||||
)
|
||||
|
||||
print(
|
||||
"\\draw[color=black,thick,shift={(-0.5,-0.5)}] (0,0) grid (%d,%d);\n\n" % (M, N)
|
||||
)
|
||||
for m in range(M):
|
||||
print("\\node at (%d,%d) {\\Large{\\texttt{%d}}};\n" % (m, -1, m))
|
||||
for n in range(N):
|
||||
print("\\node at (%d,%d) {\\Large{\\texttt{%d}}};\n" % (-1, n, n))
|
||||
|
||||
## Footer
|
||||
print("\\end{tikzpicture}")
|
||||
print("\\end{document}")
|
||||
@@ -10,6 +10,7 @@
|
||||
# is strictly prohibited.
|
||||
|
||||
from typing import Optional, Type, Union, overload
|
||||
import inspect
|
||||
|
||||
import cutlass.cute as cute
|
||||
from cutlass.cute.arch import get_dyn_smem, get_dyn_smem_size
|
||||
@@ -285,4 +286,11 @@ class SmemAllocator:
|
||||
return cute.make_tensor(ptr, layout, loc=loc, ip=ip)
|
||||
|
||||
|
||||
# Set explicit signature for Sphinx documentation to avoid issues with @dsl_user_op decorator
|
||||
SmemAllocator.__init__.__signature__ = inspect.Signature(
|
||||
[
|
||||
inspect.Parameter("self", inspect.Parameter.POSITIONAL_OR_KEYWORD),
|
||||
]
|
||||
)
|
||||
|
||||
get_smem_capacity_in_bytes = SmemAllocator.capacity_in_bytes
|
||||
|
||||
@@ -19,6 +19,7 @@ from cutlass.cutlass_dsl import (
|
||||
extract_mlir_values,
|
||||
new_from_mlir_values,
|
||||
dsl_user_op,
|
||||
const_expr,
|
||||
)
|
||||
from cutlass._mlir import ir
|
||||
import cutlass.cute as cute
|
||||
@@ -135,6 +136,7 @@ class PersistentTileSchedulerParams:
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
# Apply swizzle if swizzle_size > 1
|
||||
if swizzle_size > 1:
|
||||
problem_shape_ncluster_mnl = cute.round_up(
|
||||
self.problem_layout_ncluster_mnl.shape,
|
||||
@@ -172,6 +174,35 @@ class PersistentTileSchedulerParams:
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
# Create FastDivmod divisors (only when swizzle_size == 1 for correctness)
|
||||
# FastDivmod assumes simple col-major layout, incompatible with swizzled layouts
|
||||
if swizzle_size == 1:
|
||||
problem_layout_size = cute.size(
|
||||
self.problem_layout_ncluster_mnl, loc=loc, ip=ip
|
||||
)
|
||||
cluster_count_m = self.problem_layout_ncluster_mnl.shape[0]
|
||||
cluster_count_n = self.problem_layout_ncluster_mnl.shape[1]
|
||||
|
||||
# batch_fdd: Used to map linear_idx to work_unit_id (handles persistent scheduling)
|
||||
self.batch_fdd = cute.fast_divmod_create_divisor(
|
||||
problem_layout_size, loc=loc, ip=ip
|
||||
)
|
||||
|
||||
# cluster_shape_m_fdd: Used to decode work_unit_id to cluster coordinates
|
||||
self.cluster_shape_m_fdd = cute.fast_divmod_create_divisor(
|
||||
cluster_count_m, loc=loc, ip=ip
|
||||
)
|
||||
|
||||
# cluster_shape_n_fdd: Used for the second level decomposition
|
||||
self.cluster_shape_n_fdd = cute.fast_divmod_create_divisor(
|
||||
cluster_count_n, loc=loc, ip=ip
|
||||
)
|
||||
else:
|
||||
# FastDivmod not applicable with swizzling, set to None
|
||||
self.batch_fdd = None
|
||||
self.cluster_shape_m_fdd = None
|
||||
self.cluster_shape_n_fdd = None
|
||||
|
||||
def __extract_mlir_values__(self):
|
||||
values, self._values_pos = [], []
|
||||
for obj in [
|
||||
@@ -183,10 +214,38 @@ class PersistentTileSchedulerParams:
|
||||
obj_values = extract_mlir_values(obj)
|
||||
values += obj_values
|
||||
self._values_pos.append(len(obj_values))
|
||||
|
||||
# Add FastDivmod divisors to MLIR values for Host->Device transfer
|
||||
# Only add non-None values to avoid MLIR type errors
|
||||
fastdivmod_values = []
|
||||
fastdivmod_indices = [] # Track which FastDivmod objects are present
|
||||
|
||||
for i, (fdd_name, fdd_obj) in enumerate(
|
||||
[
|
||||
("batch_fdd", self.batch_fdd),
|
||||
("cluster_shape_m_fdd", self.cluster_shape_m_fdd),
|
||||
("cluster_shape_n_fdd", self.cluster_shape_n_fdd),
|
||||
]
|
||||
):
|
||||
if fdd_obj is not None:
|
||||
# Extract MLIR values from FastDivmodDivisor objects
|
||||
fdd_values = extract_mlir_values(fdd_obj)
|
||||
fastdivmod_values.extend(fdd_values)
|
||||
fastdivmod_indices.append(i)
|
||||
|
||||
values += fastdivmod_values
|
||||
self._values_pos.append(
|
||||
len(fastdivmod_indices)
|
||||
) # Store count of FastDivmod objects, not values
|
||||
self._fastdivmod_indices = fastdivmod_indices # Store for reconstruction
|
||||
|
||||
return values
|
||||
|
||||
def __new_from_mlir_values__(self, values):
|
||||
obj_list = []
|
||||
values_copy = list(values) # Make a copy to avoid modifying original
|
||||
|
||||
# Reconstruct original objects from MLIR values
|
||||
for obj, n_items in zip(
|
||||
[
|
||||
self.problem_shape_ntile_mnl,
|
||||
@@ -194,11 +253,32 @@ class PersistentTileSchedulerParams:
|
||||
self.swizzle_size,
|
||||
self._raster_along_m,
|
||||
],
|
||||
self._values_pos,
|
||||
self._values_pos[:-1], # Exclude FastDivmod count
|
||||
):
|
||||
obj_list.append(new_from_mlir_values(obj, values[:n_items]))
|
||||
values = values[n_items:]
|
||||
return PersistentTileSchedulerParams(*(tuple(obj_list)), loc=self._loc)
|
||||
obj_list.append(new_from_mlir_values(obj, values_copy[:n_items]))
|
||||
values_copy = values_copy[n_items:]
|
||||
|
||||
# Create new params object by calling __init__ with reconstructed values
|
||||
# This properly recreates layouts and other derived attributes in the device context
|
||||
new_params = PersistentTileSchedulerParams(*(tuple(obj_list)), loc=self._loc)
|
||||
|
||||
# Restore FastDivmod divisors from remaining values
|
||||
fdd_names = ["batch_fdd", "cluster_shape_m_fdd", "cluster_shape_n_fdd"]
|
||||
|
||||
if hasattr(self, "_fastdivmod_indices") and len(self._fastdivmod_indices) > 0:
|
||||
# Override the FastDivmod divisors created by __init__ with reconstructed ones
|
||||
for j, original_index in enumerate(self._fastdivmod_indices):
|
||||
fdd_name = fdd_names[original_index]
|
||||
# Get the original FastDivmodDivisor object
|
||||
original_fdd = getattr(self, fdd_name)
|
||||
if original_fdd is not None and j < len(values_copy):
|
||||
# Each FastDivmodDivisor has 1 MLIR value
|
||||
reconstructed_fdd = new_from_mlir_values(
|
||||
original_fdd, [values_copy[j]]
|
||||
)
|
||||
setattr(new_params, fdd_name, reconstructed_fdd)
|
||||
|
||||
return new_params
|
||||
|
||||
@dsl_user_op
|
||||
def get_grid_shape(
|
||||
@@ -285,12 +365,16 @@ class StaticPersistentTileScheduler:
|
||||
values.extend(extract_mlir_values(self._current_work_linear_idx))
|
||||
values.extend(extract_mlir_values(self.cta_id_in_cluster))
|
||||
values.extend(extract_mlir_values(self._num_tiles_executed))
|
||||
|
||||
# CRITICAL: Also extract FastDivmod divisors from params
|
||||
values.extend(extract_mlir_values(self.params))
|
||||
|
||||
return values
|
||||
|
||||
def __new_from_mlir_values__(
|
||||
self, values: list[ir.Value]
|
||||
) -> "StaticPersistentTileScheduler":
|
||||
assert len(values) == 6
|
||||
assert len(values) >= 6
|
||||
new_num_persistent_clusters = new_from_mlir_values(
|
||||
self.num_persistent_clusters, [values[0]]
|
||||
)
|
||||
@@ -303,8 +387,13 @@ class StaticPersistentTileScheduler:
|
||||
new_num_tiles_executed = new_from_mlir_values(
|
||||
self._num_tiles_executed, [values[5]]
|
||||
)
|
||||
|
||||
# Reconstruct params with FastDivmod divisors
|
||||
params_values = values[6:] # Remaining values are from params
|
||||
new_params = new_from_mlir_values(self.params, params_values)
|
||||
|
||||
return StaticPersistentTileScheduler(
|
||||
self.params,
|
||||
new_params, # Use reconstructed params with FastDivmod divisors
|
||||
new_num_persistent_clusters,
|
||||
new_current_work_linear_idx,
|
||||
new_cta_id_in_cluster,
|
||||
@@ -403,11 +492,14 @@ class StaticPersistentTileScheduler:
|
||||
self.params.problem_layout_ncluster_mnl, loc=loc, ip=ip
|
||||
)
|
||||
|
||||
# Choose coordinate calculation method based on swizzle configuration
|
||||
if self.params.swizzle_size == 1:
|
||||
cur_cluster_coord = self.params.problem_layout_ncluster_mnl.get_hier_coord(
|
||||
# Use FastDivmod optimization for non-swizzled layouts
|
||||
cur_cluster_coord = self._get_cluster_work_idx_with_fastdivmod(
|
||||
current_work_linear_idx, loc=loc, ip=ip
|
||||
)
|
||||
else:
|
||||
# Use get_flat_coord for swizzled layouts (FastDivmod doesn't support them)
|
||||
cur_cluster_coord = self.params.problem_layout_ncluster_mnl.get_flat_coord(
|
||||
current_work_linear_idx, loc=loc, ip=ip
|
||||
)
|
||||
@@ -424,6 +516,40 @@ class StaticPersistentTileScheduler:
|
||||
|
||||
return WorkTileInfo(cur_tile_coord, is_valid)
|
||||
|
||||
def _get_cluster_work_idx_with_fastdivmod(
|
||||
self, current_work_linear_idx: Int32, *, loc=None, ip=None
|
||||
) -> Tuple[Int32, Int32, Int32]:
|
||||
"""
|
||||
FastDivmod optimized CLUSTER coordinate calculation.
|
||||
|
||||
CRITICAL: This should mimic problem_layout_ncluster_mnl.get_hier_coord()
|
||||
which returns CLUSTER coordinates, not tile coordinates!
|
||||
|
||||
:param current_work_linear_idx: Linear index in the work space
|
||||
:type current_work_linear_idx: Int32
|
||||
:return: Cluster coordinates (m, n, l) or None if FastDivmod not available
|
||||
:rtype: Tuple[Int32, Int32, Int32] or None
|
||||
"""
|
||||
|
||||
# Step 1: Handle persistent scheduling - map linear_idx to work_unit_id
|
||||
work_iteration, work_unit_id = divmod(
|
||||
current_work_linear_idx, self.params.batch_fdd
|
||||
)
|
||||
|
||||
# Step 2: Decode work_unit_id using FastDivmod objects
|
||||
# The layout structure is: problem_layout_ncluster_mnl has shape (cluster_count_m, cluster_count_n, batch_count)
|
||||
# work_unit_id needs to be decomposed into (batch_l, cluster_n, cluster_m) in little-endian order
|
||||
|
||||
# First, get cluster_m using cluster_shape_m_fdd
|
||||
cluster_n_batch, cluster_m = divmod(
|
||||
work_unit_id, self.params.cluster_shape_m_fdd
|
||||
)
|
||||
|
||||
# Then decode cluster_n_batch to get cluster_n and batch_l using FastDivmod
|
||||
batch_l, cluster_n = divmod(cluster_n_batch, self.params.cluster_shape_n_fdd)
|
||||
|
||||
return (cluster_m, cluster_n, batch_l)
|
||||
|
||||
@dsl_user_op
|
||||
def get_current_work(self, *, loc=None, ip=None) -> WorkTileInfo:
|
||||
return self._get_current_work_for_linear_idx(
|
||||
@@ -446,3 +572,176 @@ class StaticPersistentTileScheduler:
|
||||
return self._num_tiles_executed
|
||||
|
||||
|
||||
class StaticPersistentRuntimeTileScheduler(StaticPersistentTileScheduler):
|
||||
"""A scheduler for static persistent runtime tile execution in CUTLASS/CuTe kernels.
|
||||
This scheduler will always launch all the SMs and the scheduler will generate the real tile info for each SM.
|
||||
|
||||
:ivar params: Tile schedule related params, including cluster shape and problem_layout_ncluster_mnl
|
||||
:type params: PersistentTileSchedulerParams
|
||||
:ivar num_persistent_clusters: Number of persistent clusters that can be launched
|
||||
:type num_persistent_clusters: Int32
|
||||
:ivar cta_id_in_cluster: ID of the CTA within its cluster
|
||||
:type cta_id_in_cluster: cute.Coord
|
||||
:ivar _num_tiles_executed: Counter for executed tiles
|
||||
:type _num_tiles_executed: Int32
|
||||
:ivar _current_work_linear_idx: Current cluster index
|
||||
:type _current_work_linear_idx: Int32
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
params: PersistentTileSchedulerParams,
|
||||
num_persistent_clusters: Int32,
|
||||
current_work_linear_idx: Int32,
|
||||
cta_id_in_cluster: cute.Coord,
|
||||
num_tiles_executed: Int32,
|
||||
inner_mode: int = 1,
|
||||
):
|
||||
"""
|
||||
Initializes the StaticPersistentTileScheduler with the given parameters.
|
||||
|
||||
:param params: Tile schedule related params, including cluster shape and problem_layout_ncluster_mnl.
|
||||
:type params: PersistentTileSchedulerParams
|
||||
:param num_persistent_clusters: Number of persistent clusters that can be launched.
|
||||
:type num_persistent_clusters: Int32
|
||||
:param current_work_linear_idx: Current cluster index.
|
||||
:type current_work_linear_idx: Int32
|
||||
:param cta_id_in_cluster: ID of the CTA within its cluster.
|
||||
:type cta_id_in_cluster: cute.Coord
|
||||
:param num_tiles_executed: Counter for executed tiles.
|
||||
:type num_tiles_executed: Int32
|
||||
:param inner_mode: The inner mode along which the linear index will be decomposed first.
|
||||
:type inner_mode: int
|
||||
"""
|
||||
super().__init__(
|
||||
params,
|
||||
num_persistent_clusters,
|
||||
current_work_linear_idx,
|
||||
cta_id_in_cluster,
|
||||
num_tiles_executed,
|
||||
)
|
||||
if inner_mode not in [0, 1]:
|
||||
raise ValueError(
|
||||
f"inner_mode must be 0(for M mode) or 1(for N mode), but got {inner_mode}"
|
||||
)
|
||||
self.inner_mode = inner_mode
|
||||
|
||||
def __new_from_mlir_values__(
|
||||
self, values: list[ir.Value]
|
||||
) -> "StaticPersistentRuntimeTileScheduler":
|
||||
assert len(values) >= 6
|
||||
new_num_persistent_clusters = new_from_mlir_values(
|
||||
self.num_persistent_clusters, [values[0]]
|
||||
)
|
||||
new_current_work_linear_idx = new_from_mlir_values(
|
||||
self._current_work_linear_idx, [values[1]]
|
||||
)
|
||||
new_cta_id_in_cluster = new_from_mlir_values(
|
||||
self.cta_id_in_cluster, values[2:5]
|
||||
)
|
||||
new_num_tiles_executed = new_from_mlir_values(
|
||||
self._num_tiles_executed, [values[5]]
|
||||
)
|
||||
|
||||
# Reconstruct params with FastDivmod divisors (same as parent class)
|
||||
params_values = values[6:] # Remaining values are from params
|
||||
new_params = new_from_mlir_values(self.params, params_values)
|
||||
|
||||
return StaticPersistentRuntimeTileScheduler(
|
||||
new_params, # Use reconstructed params with FastDivmod divisors
|
||||
new_num_persistent_clusters,
|
||||
new_current_work_linear_idx,
|
||||
new_cta_id_in_cluster,
|
||||
new_num_tiles_executed,
|
||||
self.inner_mode,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@dsl_user_op
|
||||
def create(
|
||||
params: PersistentTileSchedulerParams,
|
||||
block_idx: Tuple[Integer, Integer, Integer],
|
||||
grid_dim: Tuple[Integer, Integer, Integer],
|
||||
inner_mode: int = 1,
|
||||
*,
|
||||
loc=None,
|
||||
ip=None,
|
||||
):
|
||||
"""Initialize the static persistent tile scheduler.
|
||||
|
||||
:param params: Parameters for the persistent
|
||||
tile scheduler.
|
||||
:type params: PersistentTileSchedulerParams
|
||||
:param block_idx: The 3d block index in the format (bidx, bidy, bidz).
|
||||
:type block_idx: Tuple[Integer, Integer, Integer]
|
||||
:param grid_dim: The 3d grid dimensions for kernel launch.
|
||||
:type grid_dim: Tuple[Integer, Integer, Integer]
|
||||
:param inner_mode: The inner mode along which the linear index will be decomposed first.
|
||||
:type inner_mode: int
|
||||
|
||||
:return: A StaticPersistentRuntimeTileScheduler object.
|
||||
:rtype: StaticPersistentRuntimeTileScheduler
|
||||
"""
|
||||
|
||||
# Calculate the number of persistent clusters by dividing the total grid size
|
||||
# by the number of CTAs per cluster
|
||||
num_persistent_clusters = cute.size(grid_dim, loc=loc, ip=ip) // cute.size(
|
||||
params.cluster_shape_mn, loc=loc, ip=ip
|
||||
)
|
||||
|
||||
bidx, bidy, bidz = block_idx
|
||||
|
||||
# Initialize workload index equals to the cluster index in the grid
|
||||
current_work_linear_idx = Int32(bidz)
|
||||
|
||||
# CTA id in the cluster
|
||||
cta_id_in_cluster = (
|
||||
Int32(bidx % params.cluster_shape_mn[0]),
|
||||
Int32(bidy % params.cluster_shape_mn[1]),
|
||||
Int32(0),
|
||||
)
|
||||
# Initialize number of tiles executed to zero
|
||||
num_tiles_executed = Int32(0)
|
||||
return StaticPersistentRuntimeTileScheduler(
|
||||
params,
|
||||
num_persistent_clusters,
|
||||
current_work_linear_idx,
|
||||
cta_id_in_cluster,
|
||||
num_tiles_executed,
|
||||
inner_mode,
|
||||
)
|
||||
|
||||
# private method
|
||||
def _get_current_work_for_linear_idx(
|
||||
self, current_work_linear_idx: Int32, *, loc=None, ip=None
|
||||
) -> WorkTileInfo:
|
||||
"""Compute current tile coord given current_work_linear_idx and cta_id_in_cluster.
|
||||
|
||||
:param current_work_linear_idx: The linear index of the current work.
|
||||
:type current_work_linear_idx: Int32
|
||||
|
||||
:return: An object containing information about the current tile coordinates
|
||||
and validity status.
|
||||
:rtype: WorkTileInfo
|
||||
"""
|
||||
ntile_shape = self.params.problem_layout_ncluster_mnl.shape
|
||||
int_max = 2147483647
|
||||
if const_expr(self.inner_mode == 1):
|
||||
ntile_layout = cute.make_layout(
|
||||
(int_max, ntile_shape[1]), stride=(ntile_shape[1], 1)
|
||||
)
|
||||
else:
|
||||
ntile_layout = cute.make_layout(
|
||||
(ntile_shape[0], int_max), stride=(1, ntile_shape[0])
|
||||
)
|
||||
cluster_tile_coord_mn = ntile_layout.get_hier_coord(current_work_linear_idx)
|
||||
cur_tile_coord = (
|
||||
cluster_tile_coord_mn[0],
|
||||
cluster_tile_coord_mn[1],
|
||||
Int32(0),
|
||||
)
|
||||
|
||||
# it is determined by kernel implementation
|
||||
is_valid = True
|
||||
|
||||
return WorkTileInfo(cur_tile_coord, is_valid)
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
# is strictly prohibited.
|
||||
|
||||
from typing import Optional, Type
|
||||
import inspect
|
||||
|
||||
from cutlass import const_expr
|
||||
from cutlass.cutlass_dsl import (
|
||||
@@ -17,6 +18,7 @@ from cutlass.cutlass_dsl import (
|
||||
Float32,
|
||||
extract_mlir_values,
|
||||
new_from_mlir_values,
|
||||
dsl_user_op,
|
||||
)
|
||||
import cutlass.pipeline as pipeline
|
||||
import cutlass.cute as cute
|
||||
@@ -43,22 +45,27 @@ class TmemAllocator:
|
||||
:type _two_cta_tmem_dealloc_mbar_ptr: cute.Pointer
|
||||
"""
|
||||
|
||||
@dsl_user_op
|
||||
@cute.jit
|
||||
def _init_dealloc_mbarrier(self):
|
||||
def _init_dealloc_mbarrier(self, *, loc=None, ip=None):
|
||||
assert self._two_cta_tmem_dealloc_mbar_ptr is not None, (
|
||||
"two_cta_tmem_dealloc_mbar_ptr is required for two cta"
|
||||
)
|
||||
warp_idx = cute.arch.warp_idx()
|
||||
warp_idx = cute.arch.make_warp_uniform(warp_idx)
|
||||
warp_idx = cute.arch.warp_idx(loc=loc, ip=ip)
|
||||
warp_idx = cute.arch.make_warp_uniform(warp_idx, loc=loc, ip=ip)
|
||||
_is_allocator_warp = warp_idx == self._allocator_warp_id
|
||||
if _is_allocator_warp:
|
||||
num_tmem_dealloc_threads = 32
|
||||
with cute.arch.elect_one():
|
||||
with cute.arch.elect_one(loc=loc, ip=ip):
|
||||
cute.arch.mbarrier_init(
|
||||
self._two_cta_tmem_dealloc_mbar_ptr, num_tmem_dealloc_threads
|
||||
self._two_cta_tmem_dealloc_mbar_ptr,
|
||||
num_tmem_dealloc_threads,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
cute.arch.mbarrier_init_fence()
|
||||
|
||||
@dsl_user_op
|
||||
def __init__(
|
||||
self,
|
||||
alloc_result_dst_smem_ptr: cute.Pointer,
|
||||
@@ -67,11 +74,40 @@ class TmemAllocator:
|
||||
is_two_cta: bool = False,
|
||||
num_allocated_columns: int = 0,
|
||||
two_cta_tmem_dealloc_mbar_ptr: Optional[cute.Pointer] = None,
|
||||
*,
|
||||
loc=None,
|
||||
ip=None,
|
||||
):
|
||||
"""Initialize the TmemAllocator instance.
|
||||
"""
|
||||
Initialize a TmemAllocator instance for managing tensor memory on Blackwell GPUs.
|
||||
|
||||
Sets up the allocator state by initializing smem pointer that holds the base address of allocated tensor memory, allocator warp id, whether it is for two cta, number of allocated columns, and barrier for retrieving tensor memory ptr.
|
||||
Meanwhile, it also initializes the mbarrier pointer for two cta deallocation case.
|
||||
This initializer sets up the allocator's state, including the shared memory (smem) pointer
|
||||
holding the base address of the allocated tensor memory, barrier synchronization for
|
||||
retrieving the tensor memory pointer, allocator warp ID, whether the allocator is being used
|
||||
for a 2-SM configuration, number of allocated columns in tensor
|
||||
memory, and the optional mbarrier pointer for deallocation in the 2-SM case.
|
||||
|
||||
If `is_two_cta` is set to True, this will initialize the mbarrier pointer required for tensor
|
||||
memory deallocation across two CTAs.
|
||||
|
||||
:param alloc_result_dst_smem_ptr: The shared memory pointer that holds the base address of allocated tensor memory.
|
||||
:type alloc_result_dst_smem_ptr: cute.Pointer
|
||||
:param barrier_for_retrieve: The named barrier for retrieving the tensor memory pointer.
|
||||
:type barrier_for_retrieve: pipeline.NamedBarrier
|
||||
:param allocator_warp_id: The warp ID of the allocator warp, defaults to 0.
|
||||
:type allocator_warp_id: int, optional
|
||||
:param is_two_cta: Whether the allocator should coordinate two CTAs, defaults to False.
|
||||
:type is_two_cta: bool, optional
|
||||
:param num_allocated_columns: The number of columns allocated in tensor memory, defaults to 0.
|
||||
:type num_allocated_columns: int, optional
|
||||
:param two_cta_tmem_dealloc_mbar_ptr: The mbarrier pointer required for two-CTA tensor memory deallocation, optional.
|
||||
:type two_cta_tmem_dealloc_mbar_ptr: cute.Pointer, optional
|
||||
:param loc: Optional codegen location for debugging and error reporting.
|
||||
:type loc: Any, optional
|
||||
:param ip: Optional insertion point for codegen.
|
||||
:type ip: Any, optional
|
||||
|
||||
:raises AssertionError: If two_cta_tmem_dealloc_mbar_ptr is None while is_two_cta is True.
|
||||
"""
|
||||
# TODO: automatically maintain a smem address
|
||||
self._alloc_result_dst_smem_ptr = alloc_result_dst_smem_ptr
|
||||
@@ -83,7 +119,7 @@ class TmemAllocator:
|
||||
|
||||
# Init tmem dealloc mbarrier if two cta
|
||||
if const_expr(self._is_two_cta):
|
||||
self._init_dealloc_mbarrier()
|
||||
self._init_dealloc_mbarrier(loc=loc, ip=ip)
|
||||
|
||||
def __extract_mlir_values__(self) -> list[ir.Value]:
|
||||
values = extract_mlir_values(self._alloc_result_dst_smem_ptr)
|
||||
@@ -137,8 +173,9 @@ class TmemAllocator:
|
||||
return False
|
||||
return True
|
||||
|
||||
@dsl_user_op
|
||||
@cute.jit
|
||||
def allocate(self, num_columns: int):
|
||||
def allocate(self, num_columns: int, *, loc=None, ip=None):
|
||||
"""Allocate a block of tensor memory.
|
||||
|
||||
This method allocates a block of tensor memory from allocator warp and returns a handle to retrieve
|
||||
@@ -152,29 +189,34 @@ class TmemAllocator:
|
||||
"total allocated columns must be less than or equal to 512"
|
||||
)
|
||||
|
||||
warp_idx = cute.arch.warp_idx()
|
||||
warp_idx = cute.arch.make_warp_uniform(warp_idx)
|
||||
warp_idx = cute.arch.warp_idx(loc=loc, ip=ip)
|
||||
warp_idx = cute.arch.make_warp_uniform(warp_idx, loc=loc, ip=ip)
|
||||
_is_allocator_warp = warp_idx == self._allocator_warp_id
|
||||
if _is_allocator_warp:
|
||||
cute.arch.alloc_tmem(
|
||||
num_columns,
|
||||
self._alloc_result_dst_smem_ptr,
|
||||
is_two_cta=self._is_two_cta,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
self._num_allocated_columns += num_columns
|
||||
|
||||
@cute.jit
|
||||
def wait_for_alloc(self):
|
||||
@dsl_user_op
|
||||
def wait_for_alloc(self, *, loc=None, ip=None):
|
||||
"""Wait for the allocator warp to finish allocation.
|
||||
|
||||
This method is used to synchronize the allocator warp with the other warps before retrieving tmem ptr.
|
||||
"""
|
||||
self._barrier_for_retrieve.arrive_and_wait()
|
||||
self._barrier_for_retrieve.arrive_and_wait(loc=loc, ip=ip)
|
||||
|
||||
@cute.jit
|
||||
@dsl_user_op
|
||||
def retrieve_ptr(
|
||||
self,
|
||||
dtype: Type[Numeric] = Float32,
|
||||
*,
|
||||
loc=None,
|
||||
ip=None,
|
||||
) -> cute.Pointer:
|
||||
"""Retrieve the pointer to the allocated tensor memory.
|
||||
|
||||
@@ -185,30 +227,36 @@ class TmemAllocator:
|
||||
dtype,
|
||||
alignment=16,
|
||||
ptr_to_buffer_holding_addr=self._alloc_result_dst_smem_ptr,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
@dsl_user_op
|
||||
@cute.jit
|
||||
def relinquish_alloc_permit(self):
|
||||
def relinquish_alloc_permit(self, *, loc=None, ip=None):
|
||||
"""Relinquish the tensor memory allocation permit.
|
||||
|
||||
This method relinquishes the tensor memory allocation permit for the allocator warp, promising
|
||||
the allocator warp will not allocate any more tensor memory.
|
||||
"""
|
||||
warp_idx = cute.arch.warp_idx()
|
||||
warp_idx = cute.arch.make_warp_uniform(warp_idx)
|
||||
warp_idx = cute.arch.warp_idx(loc=loc, ip=ip)
|
||||
warp_idx = cute.arch.make_warp_uniform(warp_idx, loc=loc, ip=ip)
|
||||
_is_allocator_warp = warp_idx == self._allocator_warp_id
|
||||
if _is_allocator_warp:
|
||||
cute.arch.relinquish_tmem_alloc_permit(is_two_cta=self._is_two_cta)
|
||||
cute.arch.relinquish_tmem_alloc_permit(
|
||||
is_two_cta=self._is_two_cta, loc=loc, ip=ip
|
||||
)
|
||||
|
||||
@dsl_user_op
|
||||
@cute.jit
|
||||
def free(self, tmem_ptr: cute.Pointer, num_columns: int = 0):
|
||||
def free(self, tmem_ptr: cute.Pointer, num_columns: int = 0, *, loc=None, ip=None):
|
||||
"""Deallocate the tensor memory.
|
||||
|
||||
This method sync on mbarrier (for two cta use case) and deallocates the tensor memory from the allocator warp.
|
||||
User can optionally specify the number of columns to deallocate. If not specified, all allocated columns will be deallocated.
|
||||
"""
|
||||
warp_idx = cute.arch.warp_idx()
|
||||
warp_idx = cute.arch.make_warp_uniform(warp_idx)
|
||||
warp_idx = cute.arch.warp_idx(loc=loc, ip=ip)
|
||||
warp_idx = cute.arch.make_warp_uniform(warp_idx, loc=loc, ip=ip)
|
||||
_is_allocator_warp = warp_idx == self._allocator_warp_id
|
||||
|
||||
assert num_columns <= self._num_allocated_columns, (
|
||||
@@ -223,15 +271,67 @@ class TmemAllocator:
|
||||
self._num_allocated_columns -= num_deallocate_columns
|
||||
if _is_allocator_warp:
|
||||
if const_expr(self._is_two_cta):
|
||||
bid_in_cluster = cute.arch.block_idx_in_cluster(loc=loc, ip=ip)
|
||||
_cta_rank_in_cluster = cute.arch.make_warp_uniform(
|
||||
cute.arch.block_idx_in_cluster()
|
||||
bid_in_cluster, loc=loc, ip=ip
|
||||
)
|
||||
# Arrive and wait for dealloc signal from peer cta
|
||||
cute.arch.mbarrier_arrive(
|
||||
self._two_cta_tmem_dealloc_mbar_ptr, _cta_rank_in_cluster ^ 1
|
||||
self._two_cta_tmem_dealloc_mbar_ptr,
|
||||
_cta_rank_in_cluster ^ 1,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
cute.arch.mbarrier_wait(
|
||||
self._two_cta_tmem_dealloc_mbar_ptr, 0, loc=loc, ip=ip
|
||||
)
|
||||
cute.arch.mbarrier_wait(self._two_cta_tmem_dealloc_mbar_ptr, 0)
|
||||
# Deallocate tmem
|
||||
cute.arch.dealloc_tmem(
|
||||
tmem_ptr, num_deallocate_columns, is_two_cta=self._is_two_cta
|
||||
tmem_ptr,
|
||||
num_deallocate_columns,
|
||||
is_two_cta=self._is_two_cta,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
|
||||
# Set explicit signature for Sphinx documentation to avoid issues with @dsl_user_op decorator
|
||||
TmemAllocator.__init__.__signature__ = inspect.Signature(
|
||||
[
|
||||
inspect.Parameter("self", inspect.Parameter.POSITIONAL_OR_KEYWORD),
|
||||
inspect.Parameter(
|
||||
"alloc_result_dst_smem_ptr",
|
||||
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
||||
annotation=cute.Pointer,
|
||||
),
|
||||
inspect.Parameter(
|
||||
"barrier_for_retrieve",
|
||||
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
||||
annotation=pipeline.NamedBarrier,
|
||||
),
|
||||
inspect.Parameter(
|
||||
"allocator_warp_id",
|
||||
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
||||
default=0,
|
||||
annotation=int,
|
||||
),
|
||||
inspect.Parameter(
|
||||
"is_two_cta",
|
||||
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
||||
default=False,
|
||||
annotation=bool,
|
||||
),
|
||||
inspect.Parameter(
|
||||
"num_allocated_columns",
|
||||
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
||||
default=0,
|
||||
annotation=int,
|
||||
),
|
||||
inspect.Parameter(
|
||||
"two_cta_tmem_dealloc_mbar_ptr",
|
||||
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
||||
default=None,
|
||||
annotation=Optional[cute.Pointer],
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user