Add required changes for github pipeline. (#2648)

This commit is contained in:
Junkai-Wu
2025-09-17 22:22:45 -04:00
committed by GitHub
parent 7817e47154
commit 8825e8be4f
32 changed files with 343 additions and 1 deletions
@@ -0,0 +1,17 @@
# 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.
# Local module imports
from .dsl import *
from .runtime import *
from ._mlir_helpers import lru_cache_ir
from .env_manager import get_str_env_var, detect_gpu_arch
@@ -0,0 +1,27 @@
# 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 MLIR Dialect helper functions
"""
from . import arith
from .lru_cache_ir import lru_cache_ir
__all__ = ["arith", "lru_cache_ir"]
try:
from . import gpu
__all__.extend(["gpu"])
except ImportError:
pass
@@ -0,0 +1,691 @@
# 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 MLIR Arith Dialect helper functions
"""
import array
import numpy as np
from ..common import *
from ..._mlir import ir # type: ignore
from ..._mlir.extras import types as T # type: ignore
from ..._mlir.dialects import arith, nvgpu, math, builtin # type: ignore
from .lru_cache_ir import lru_cache_ir
# =============================================================================
# Arith Dialect Helper functions
# =============================================================================
def recast_type(src_type, res_elem_type) -> ir.Type:
if isinstance(src_type, T.VectorType):
if src_type.scalable:
res_type = T.vector(
*src_type.shape,
res_elem_type,
scalable=src_type.scalable,
scalable_dims=src_type.scalable_dims,
)
else:
res_type = T.vector(*src_type.shape, res_elem_type)
elif isinstance(src_type, T.RankedTensorType):
res_type = T.RankedTensorType.get(
element_type=res_elem_type, shape=src_type.shape, strides=src_type.strides
)
elif isinstance(src_type, T.UnrankedTensorType):
res_type = T.UnrankedTensorType.get(element_type=res_elem_type)
elif isinstance(src_type, T.MemRefType):
res_type = T.MemRefType.get(
element_type=res_elem_type, shape=src_type.shape, strides=src_type.strides
)
else:
res_type = res_elem_type
return res_type
def is_scalar(ty) -> bool:
return not isinstance(
ty, (T.VectorType, T.RankedTensorType, T.UnrankedTensorType, T.MemRefType)
)
def element_type(ty) -> ir.Type:
if not is_scalar(ty):
return ty.element_type
else:
return ty
def is_narrow_precision(ty) -> bool:
narrow_types = {
T.f8E8M0FNU(),
T.f8E4M3FN(),
T.f8E4M3(),
T.f8E5M2(),
T.f8E4M3B11FNUZ(),
T.f4E2M1FN(),
T.f6E3M2FN(),
T.f6E2M3FN(),
}
return ty in narrow_types
def is_float_type(ty) -> bool:
return (
arith._is_float_type(ty)
# TODO-upstream: prediction is not correct. Patch here and fix in upstream later
or is_narrow_precision(ty)
or ty in (T.bf16(), T.tf32())
)
def truncf_to_narrow(res_ty, src, loc, ip):
res_elem_ty = element_type(res_ty)
if res_elem_ty == T.f8E8M0FNU():
rnd = nvgpu.RoundingMode.RP
else:
rnd = nvgpu.RoundingMode.RN
return nvgpu.cvt_fptrunc(res_ty, src, rnd=rnd, loc=loc, ip=ip)
def extf_from_narrow(res_ty, src, loc, ip):
src_elem_ty = element_type(src.type)
# When source type is E8M0, temporary element type has to be bf16
tmp_elem_ty = T.bf16() if src_elem_ty == T.f8E8M0FNU() else T.f16()
tmp_ty = recast_type(src.type, tmp_elem_ty)
# narrow -> bf16/f16 -> target type
tmp = nvgpu.cvt_fpext(tmp_ty, src, loc=loc, ip=ip)
return arith.extf(res_ty, tmp, loc=loc, ip=ip)
def bitcast(src, res_elem_type, *, loc=None, ip=None):
res_type = recast_type(src.type, res_elem_type)
return arith.bitcast(res_type, src, loc=loc, ip=ip)
def cvtf(src, res_elem_type, *, loc=None, ip=None):
src_elem_type = element_type(src.type)
if res_elem_type == src_elem_type:
return src
res_type = recast_type(src.type, res_elem_type)
# Treat TF32 as F32 and use i32 as intermediate data
# TODO-upstream: update arith to support tf32 <-> f32 conversion
if src_elem_type == T.tf32():
# tf32 -> i32
tmp_type = recast_type(src.type, T.i32())
src = builtin.unrealized_conversion_cast([tmp_type], [src], loc=loc, ip=ip)
# i32 -> f32
src = bitcast(src, T.f32(), loc=loc, ip=ip)
# f32 -> X with `cvtf` recursively
return cvtf(src, res_elem_type, loc=loc, ip=ip)
if res_elem_type == T.tf32():
# X -> f32 with `cvtf`` recursively
tmp = cvtf(src, T.f32(), loc=loc, ip=ip)
# f32 -> i32
tmp = bitcast(tmp, T.i32(), loc=loc, ip=ip)
# i32 -> tf32
return builtin.unrealized_conversion_cast([res_type], [tmp], loc=loc, ip=ip)
if res_elem_type.width > src_elem_type.width:
if is_narrow_precision(src_elem_type):
return extf_from_narrow(res_type, src, loc, ip)
else:
return arith.extf(res_type, src, loc=loc, ip=ip)
else:
tmp_mlir_type = recast_type(src.type, T.f32())
# f16 -- extf -> f32 -- truncf -> bf16
# TODO-upstream: update arith to support bf16 <-> f16 conversion?
if (src_elem_type == T.f16() and res_elem_type == T.bf16()) or (
src_elem_type == T.bf16() and res_elem_type == T.f16()
):
tmp = arith.extf(tmp_mlir_type, src, loc=loc, ip=ip)
return arith.truncf(res_type, tmp, loc=loc, ip=ip)
# {f8, f6, f4} -> f16, f32, ...
elif is_narrow_precision(res_elem_type):
return truncf_to_narrow(res_type, src, loc, ip)
else:
return arith.truncf(res_type, src, loc=loc, ip=ip)
def fptoi(src, signed: Union[bool, None], res_elem_type, *, loc=None, ip=None):
res_type = recast_type(src.type, res_elem_type)
# TODO-upstream: update arith to support this kind of conversion
if element_type(src.type) in (T.tf32(), T.bf16()):
src = cvtf(src, T.f32(), loc=loc, ip=ip)
if signed:
return arith.fptosi(res_type, src, loc=loc, ip=ip)
else:
return arith.fptoui(res_type, src, loc=loc, ip=ip)
def itofp(src, signed: Union[bool, None], res_elem_type, *, loc=None, ip=None):
res_type = recast_type(src.type, res_elem_type)
orig_res_type = res_type
# TODO-upstream: update arith to support this kind of conversion
if res_elem_type in (T.tf32(), T.bf16()):
res_type = recast_type(src.type, T.f32())
if signed and element_type(src.type).width > 1:
res = arith.sitofp(res_type, src, loc=loc, ip=ip)
else:
res = arith.uitofp(res_type, src, loc=loc, ip=ip)
if orig_res_type == res_type:
return res
return cvtf(res, element_type(orig_res_type), loc=loc, ip=ip)
def int_to_int(a, dst_elem_type, *, loc=None, ip=None):
src_signed = a.signed
dst_signed = dst_elem_type.signed
src_width = element_type(a.type).width
dst_width = dst_elem_type.width
dst_mlir_type = recast_type(a.type, dst_elem_type.mlir_type)
if dst_width == src_width:
return a
elif src_signed != False and not dst_signed:
# Signed -> Unsigned
if dst_width > src_width:
return arith.extui(dst_mlir_type, a, loc=loc, ip=ip)
else:
return arith.trunci(dst_mlir_type, a, loc=loc, ip=ip)
elif src_signed == dst_signed:
# Same signedness
if dst_width > src_width:
if src_signed != False and src_width > 1:
return arith.extsi(dst_mlir_type, a, loc=loc, ip=ip)
else:
return arith.extui(dst_mlir_type, a, loc=loc, ip=ip)
else:
return arith.trunci(dst_mlir_type, a, loc=loc, ip=ip)
else:
# Unsigned -> Signed
if dst_width > src_width:
return arith.extui(dst_mlir_type, a, loc=loc, ip=ip)
else:
# For truncation from unsigned to signed, we need to handle overflow
# First truncate to the target width
trunc = arith.trunci(dst_mlir_type, a, loc=loc, ip=ip)
# Then reinterpret as signed
if dst_signed:
return arith.bitcast(dst_mlir_type, trunc, loc=loc, ip=ip)
return trunc
# =============================================================================
# Arith Ops Emitter Helpers
# - assuming type of lhs and rhs match each other
# - op name matches python module operator
# =============================================================================
def _cast(res_elem_ty, src, is_signed=None, *, loc=None, ip=None):
"""
This function provides simplified interface to upstream op builder
arith.truncf(T.vector(shape, new_type), src)
is simplified as because it's element-wise op which can't change shape
arith.truncf(new_type, src)
"""
if isinstance(src, ir.Value):
src_ty = src.type
else:
src_ty = type(src).mlir_type
src = src.ir_value()
src_elem_ty = element_type(src_ty)
if src_elem_ty == res_elem_ty:
return src
elif is_float_type(src_elem_ty) and is_float_type(res_elem_ty):
# float-to-float
return cvtf(src, res_elem_ty, loc=loc, ip=ip)
elif arith._is_integer_like_type(src_elem_ty) and arith._is_integer_like_type(
res_elem_ty
):
if src_elem_ty.width >= res_elem_ty.width:
cast_op = arith.trunci
else:
if is_signed:
cast_op = arith.extsi
else:
cast_op = arith.extui
res_ty = recast_type(src_ty, res_elem_ty)
return cast_op(res_ty, src, loc=loc, ip=ip)
elif is_float_type(src_elem_ty) and arith._is_integer_like_type(res_elem_ty):
return fptoi(src, is_signed, res_elem_ty, loc=loc, ip=ip)
elif arith._is_integer_like_type(src_elem_ty) and is_float_type(res_elem_ty):
return itofp(src, is_signed, res_elem_ty, loc=loc, ip=ip)
else:
raise DSLRuntimeError(
f"cast from {src_elem_ty} to {res_elem_ty} is not supported"
)
@lru_cache_ir()
def const(value, ty=None, *, loc=None, ip=None):
"""
Generates dynamic expression for constant values.
"""
from ..typing import Numeric, NumericMeta
from ..dsl import is_dynamic_expression, _numpy_type_to_mlir_type
if isinstance(value, Numeric):
value = value.value
# Early return
if is_dynamic_expression(value) and (
value.type.isinstance(value.type) or T.bool().isinstance(value.type)
):
return value
# Assume type
if ty is None:
if isinstance(value, float):
ty = T.f32()
elif isinstance(value, bool):
ty = T.bool()
elif isinstance(value, int):
ty = T.i32()
elif isinstance(value, np.ndarray):
ty = T.vector(*value.shape, _numpy_type_to_mlir_type(value.dtype))
value = array.array(value.dtype.kind, value.flatten().tolist())
else:
raise DSLNotImplemented(f"{type(value)} is not supported")
elif isinstance(ty, NumericMeta):
ty = ty.mlir_type
elif isinstance(ty, ir.Type):
if ir.RankedTensorType.isinstance(ty) or ir.VectorType.isinstance(ty):
elem_ty = ty.element_type
if isinstance(elem_ty, ir.IntegerType):
attr = ir.IntegerAttr.get(elem_ty, value)
else:
attr = ir.FloatAttr.get(elem_ty, value)
value = ir.DenseElementsAttr.get_splat(ty, attr)
elif arith._is_float_type(ty) and isinstance(value, (bool, int)):
value = float(value)
elif arith._is_integer_like_type(ty) and isinstance(value, float):
value = int(value)
else:
raise DSLNotImplemented(f"type {ty} is not supported")
return arith.constant(ty, value, loc=loc, ip=ip)
def _dispatch_to_rhs_r_op(op):
"""Decorator that dispatches to the right-hand-side's reverse operation.
If the other operand is not an ArithValue or is a subclass (more specific)
of ArithValue, this allows proper method resolution for binary operations.
"""
def wrapper(self, other, **kwargs):
if not isinstance(other, ArithValue):
if not isinstance(other, (int, float, bool)):
# allows to call other.__rmul__
return NotImplemented
return op(self, other, **kwargs)
return wrapper
def _binary_op(op):
"""
Decorator to check if the 'other' argument is an ArithValue.
If not, returns NotImplemented.
"""
def wrapper(self, other, **kwargs):
# When reach this point, `self` must be cast to base `ArithValue` type
if isinstance(other, (int, float, bool)):
other = const(other, self.type).with_signedness(self.signed)
# Call the original function
# If sub-class doesn't implement overloaded arithmetic, cast to base class
return op(self, other, **kwargs)
return wrapper
# Operator overloading
@ir.register_value_caster(ir.Float4E2M1FNType.static_typeid)
@ir.register_value_caster(ir.Float6E2M3FNType.static_typeid)
@ir.register_value_caster(ir.Float6E3M2FNType.static_typeid)
@ir.register_value_caster(ir.Float8E4M3FNType.static_typeid)
@ir.register_value_caster(ir.Float8E4M3B11FNUZType.static_typeid)
@ir.register_value_caster(ir.Float8E5M2Type.static_typeid)
@ir.register_value_caster(ir.Float8E4M3Type.static_typeid)
@ir.register_value_caster(ir.Float8E8M0FNUType.static_typeid)
@ir.register_value_caster(ir.BF16Type.static_typeid)
@ir.register_value_caster(ir.F16Type.static_typeid)
@ir.register_value_caster(ir.FloatTF32Type.static_typeid)
@ir.register_value_caster(ir.F32Type.static_typeid)
@ir.register_value_caster(ir.F64Type.static_typeid)
@ir.register_value_caster(ir.IntegerType.static_typeid)
@ir.register_value_caster(ir.VectorType.static_typeid)
@ir.register_value_caster(ir.RankedTensorType.static_typeid)
class ArithValue(ir.Value):
"""Overloads operators for MLIR's Arith dialects binary operations."""
def __init__(self, v, signed: Union[bool, None] = None):
if isinstance(v, int):
v = arith.constant(self.type, v)
super().__init__(v)
elem_ty = element_type(self.type)
self.is_float = arith._is_float_type(elem_ty)
# arith dialect consider `1` in `i1` as `-1`, treat it as unsigned for DSL
self.signed = signed and elem_ty.width > 1
def with_signedness(self, signed: Union[bool, None]):
return type(self)(self, signed)
def __neg__(self, *, loc=None, ip=None):
if self.type == T.bool():
raise TypeError(
"Negation, the operator `-` is not supported for boolean type"
)
if self.is_float:
return arith.negf(self, loc=loc, ip=ip)
else:
c0 = arith.constant(self.type, 0, loc=loc, ip=ip)
return arith.subi(c0, self, loc=loc, ip=ip)
@_binary_op
def __pow__(self, other, *, loc=None, ip=None) -> "ArithValue":
if self.is_float and other.is_float:
return math.powf(self, other, loc=loc, ip=ip)
elif self.is_float and not other.is_float:
return math.fpowi(self, other, loc=loc, ip=ip)
elif not self.is_float and other.is_float:
lhs = itofp(self, self.signed, T.f32(), loc=loc, ip=ip)
rhs = cvtf(other, T.f32(), loc=loc, ip=ip)
return math.powf(lhs, rhs, loc=loc, ip=ip)
elif not self.is_float and not other.is_float:
return math.ipowi(self, other, loc=loc, ip=ip)
else:
raise DSLNotImplemented(f"Unsupported '{self} ** {other}'")
@_binary_op
def __rpow__(self, other, *, loc=None, ip=None) -> "ArithValue":
return other.__pow__(self, loc=loc, ip=ip)
# arith operators
@_dispatch_to_rhs_r_op
@_binary_op
def __add__(self, other, *, loc=None, ip=None) -> "ArithValue":
if self.is_float:
return arith.addf(self, other, loc=loc, ip=ip)
else:
return arith.addi(self, other, loc=loc, ip=ip)
@_dispatch_to_rhs_r_op
@_binary_op
def __sub__(self, other, *, loc=None, ip=None) -> "ArithValue":
if self.is_float:
return arith.subf(self, other, loc=loc, ip=ip)
else:
return arith.subi(self, other, loc=loc, ip=ip)
@_dispatch_to_rhs_r_op
@_binary_op
def __mul__(self, other, *, loc=None, ip=None) -> "ArithValue":
if self.is_float:
return arith.mulf(self, other, loc=loc, ip=ip)
else:
return arith.muli(self, other, loc=loc, ip=ip)
@_dispatch_to_rhs_r_op
@_binary_op
def __truediv__(self, other, *, loc=None, ip=None) -> "ArithValue":
if self.is_float:
return arith.divf(self, other, loc=loc, ip=ip)
else:
lhs = itofp(self, self.signed, T.f32(), loc=loc, ip=ip)
rhs = itofp(other, other.signed, T.f32(), loc=loc, ip=ip)
return arith.divf(lhs, rhs, loc=loc, ip=ip)
@_dispatch_to_rhs_r_op
@_binary_op
def __floordiv__(self, other, *, loc=None, ip=None) -> "ArithValue":
if self.is_float:
q = arith.divf(self, other, loc=loc, ip=ip)
return math.floor(q, loc=loc, ip=ip)
elif self.signed != False:
return arith.floordivsi(self, other, loc=loc, ip=ip)
else:
return arith.divui(self, other, loc=loc, ip=ip)
@_dispatch_to_rhs_r_op
@_binary_op
def __mod__(self, other, *, loc=None, ip=None) -> "ArithValue":
if self.is_float:
return arith.remf(self, other, loc=loc, ip=ip)
elif self.signed != False:
return arith.remsi(self, other, loc=loc, ip=ip)
else:
return arith.remui(self, other, loc=loc, ip=ip)
@_binary_op
def __radd__(self, other, *, loc=None, ip=None) -> "ArithValue":
return other.__add__(self, loc=loc, ip=ip)
@_binary_op
def __rsub__(self, other, *, loc=None, ip=None) -> "ArithValue":
return other.__sub__(self, loc=loc, ip=ip)
@_binary_op
def __rmul__(self, other, *, loc=None, ip=None) -> "ArithValue":
return other.__mul__(self, loc=loc, ip=ip)
@_binary_op
def __rtruediv__(self, other, *, loc=None, ip=None) -> "ArithValue":
return other.__truediv__(self, loc=loc, ip=ip)
@_binary_op
def __rfloordiv__(self, other, *, loc=None, ip=None) -> "ArithValue":
return other.__floordiv__(self, loc=loc, ip=ip)
@_binary_op
def __rmod__(self, other, *, loc=None, ip=None) -> "ArithValue":
return other.__mod__(self, loc=loc, ip=ip)
# Comparison operators (comparison doesn't have right-hand-side variants)
@_dispatch_to_rhs_r_op
@_binary_op
def __lt__(self, other, *, loc=None, ip=None) -> "ArithValue":
if self.is_float:
return arith.cmpf(arith.CmpFPredicate.OLT, self, other, loc=loc, ip=ip)
elif self.signed != False:
return arith.cmpi(arith.CmpIPredicate.slt, self, other, loc=loc, ip=ip)
else:
return arith.cmpi(arith.CmpIPredicate.ult, self, other, loc=loc, ip=ip)
@_dispatch_to_rhs_r_op
@_binary_op
def __le__(self, other, *, loc=None, ip=None) -> "ArithValue":
if self.is_float:
return arith.cmpf(arith.CmpFPredicate.OLE, self, other, loc=loc, ip=ip)
elif self.signed != False:
return arith.cmpi(arith.CmpIPredicate.sle, self, other, loc=loc, ip=ip)
else:
return arith.cmpi(arith.CmpIPredicate.ule, self, other, loc=loc, ip=ip)
@_dispatch_to_rhs_r_op
@_binary_op
def __eq__(self, other, *, loc=None, ip=None) -> "ArithValue":
if self.is_float:
return arith.cmpf(arith.CmpFPredicate.OEQ, self, other, loc=loc, ip=ip)
else:
return arith.cmpi(arith.CmpIPredicate.eq, self, other, loc=loc, ip=ip)
@_dispatch_to_rhs_r_op
@_binary_op
def __ne__(self, other, *, loc=None, ip=None) -> "ArithValue":
if self.is_float:
# In Python, bool(float("nan")) is True, so use unordered comparison here
return arith.cmpf(arith.CmpFPredicate.UNE, self, other, loc=loc, ip=ip)
else:
return arith.cmpi(arith.CmpIPredicate.ne, self, other, loc=loc, ip=ip)
@_dispatch_to_rhs_r_op
@_binary_op
def __gt__(self, other, *, loc=None, ip=None) -> "ArithValue":
if self.is_float:
return arith.cmpf(arith.CmpFPredicate.OGT, self, other, loc=loc, ip=ip)
elif self.signed != False:
return arith.cmpi(arith.CmpIPredicate.sgt, self, other, loc=loc, ip=ip)
else:
return arith.cmpi(arith.CmpIPredicate.ugt, self, other, loc=loc, ip=ip)
@_dispatch_to_rhs_r_op
@_binary_op
def __ge__(self, other, *, loc=None, ip=None) -> "ArithValue":
if self.is_float:
return arith.cmpf(arith.CmpFPredicate.OGE, self, other, loc=loc, ip=ip)
elif self.signed != False:
return arith.cmpi(arith.CmpIPredicate.sge, self, other, loc=loc, ip=ip)
else:
return arith.cmpi(arith.CmpIPredicate.uge, self, other, loc=loc, ip=ip)
# Unary operators
def __invert__(self, *, loc=None, ip=None) -> "ArithValue":
return arith.xori(self, arith.constant(self.type, -1))
# Bitwise operations
@_dispatch_to_rhs_r_op
@_binary_op
def __and__(self, other, *, loc=None, ip=None) -> "ArithValue":
return arith.andi(self, other, loc=loc, ip=ip)
@_dispatch_to_rhs_r_op
@_binary_op
def __or__(self, other, *, loc=None, ip=None) -> "ArithValue":
return arith.ori(self, other, loc=loc, ip=ip)
@_dispatch_to_rhs_r_op
@_binary_op
def __xor__(self, other, *, loc=None, ip=None) -> "ArithValue":
return arith.xori(self, other, loc=loc, ip=ip)
@_dispatch_to_rhs_r_op
@_binary_op
def __rshift__(self, other, *, loc=None, ip=None) -> "ArithValue":
if self.signed != False:
return arith.shrsi(self, other, loc=loc, ip=ip)
else:
return arith.shrui(self, other, loc=loc, ip=ip)
@_dispatch_to_rhs_r_op
@_binary_op
def __lshift__(self, other, *, loc=None, ip=None) -> "ArithValue":
return arith.shli(self, other, loc=loc, ip=ip)
@_binary_op
def __rand__(self, other, *, loc=None, ip=None) -> "ArithValue":
return arith.andi(other, self, loc=loc, ip=ip)
@_binary_op
def __ror__(self, other, *, loc=None, ip=None) -> "ArithValue":
return arith.ori(other, self, loc=loc, ip=ip)
@_binary_op
def __rxor__(self, other, *, loc=None, ip=None) -> "ArithValue":
return arith.xori(other, self, loc=loc, ip=ip)
@_binary_op
def __rrshift__(self, other, *, loc=None, ip=None) -> "ArithValue":
return other.__rshift__(self, loc=loc, ip=ip)
@_binary_op
def __rlshift__(self, other, *, loc=None, ip=None) -> "ArithValue":
return other.__lshift__(self, loc=loc, ip=ip)
def __hash__(self):
return super().__hash__()
def __str__(self):
return "?"
def __repr__(self):
return self.__str__()
def _min(lhs, rhs, *, loc=None, ip=None):
"""
This function provides a unified interface for building arith min
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 min(lhs, rhs)
else:
lhs = arith.constant(rhs.type, lhs, loc=loc, ip=ip)
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):
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)
def _max(lhs, rhs, *, loc=None, ip=None):
"""
This function provides a unified interface for building arith max
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)
else:
lhs = arith.constant(rhs.type, lhs, loc=loc, ip=ip)
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):
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)
@@ -0,0 +1,64 @@
# 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 MLIR GPU Dialect helper functions
"""
from ..._mlir import ir
from ..._mlir.dialects import gpu, arith, scf
from ..._mlir.extras import types as T
from ..common import *
# =============================================================================
# GPU Dialect Helper functions
# =============================================================================
def create_async_token():
token_ty = gpu.AsyncTokenType.get()
token = gpu.wait(token_ty, [])
return token
def printf(fmt, *args, threadNumber=-1):
"""Generate gpu.printf OP predicated on threadNumber"""
type_formats = []
for arg in args:
ty_format = None
if ir.IndexType.isinstance(arg.type):
ty_format = "%llu"
if ir.IntegerType.isinstance(arg.type):
width = ir.IntegerType(arg.type).width
if width == 64:
ty_format = "%llu"
elif width == 32:
ty_format = "%d"
elif width == 1:
ty_format = "%i"
if ir.F32Type.isinstance(arg.type):
ty_format = "%f"
if ty_format is None:
raise DSLNotImplemented(arg.type)
type_formats.append(ty_format)
if threadNumber == -1:
gpu.printf(fmt.format(*type_formats) + "\n", args)
if threadNumber != -1:
tidx = gpu.thread_id(gpu.Dimension.x)
predicate = arith.cmpi(
arith.CmpIPredicate.eq, tidx, arith.constant(_T.index(), threadNumber)
)
if_op = scf.IfOp(predicate)
with ir.InsertionPoint(if_op.then_block):
gpu.printf(fmt.format(*type_formats) + "\n", args)
scf.yield_([])
@@ -0,0 +1,76 @@
# 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 @lru_cache_ir
It extends functools.lru_cache with IR Context awareness.
Example usage:
from cutlass import ir
from lru_cache_ir import lru_cache_ir
@lru_cache_ir(ir, maxsize=128, typed=False)
def make_layout(...):
...
"""
from functools import lru_cache, wraps
from ..._mlir import ir # type: ignore
def get_ir_context(func):
"""
Return the context for given func called under ir.
Currently the context includes MLIRContext and InsertionPoint.
"""
try:
if ir:
return (ir.Context.current, ir.InsertionPoint.current)
else:
return None
except ValueError:
return None
def lru_cache_ir(maxsize=128, typed=True):
"""
Applies an LRU cache to a given function, with awareness of IR context.
Usage is similar to functools.lru_cache while taking `ir` as required argument.
:param ir: The IR object from which to derive the context by `get_ir_context`
:param maxsize: Max cache size, same as functools.lru_cache
:param typed: Whether params are type-sensitive, default to True as IR is type-sensitive
"""
def decorator(func):
# Use functools.lru_cache with a custom wrapper to control the key generation
@lru_cache(maxsize=maxsize, typed=typed)
def cached_func(context, *args, **kwargs):
return func(*args, **kwargs)
@wraps(func)
def wrapper(*args, **kwargs):
try:
# Call the cached function with the context
return cached_func(get_ir_context(func), *args, **kwargs)
except (RuntimeError, TypeError):
return func(*args, **kwargs)
# Expose cache-related methods for introspection
wrapper.cache_clear = cached_func.cache_clear
wrapper.cache_info = cached_func.cache_info
return wrapper
return decorator
@@ -0,0 +1,34 @@
# 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 MLIR's OP helper functions
"""
import inspect
from functools import wraps
from ..._mlir import ir
def dsl_user_op(opFunc):
@wraps(opFunc)
def wrapper(*args, **kwargs):
loc = kwargs.pop("loc", None)
if loc is None:
frame = inspect.currentframe().f_back
file_loc = ir.Location.file(frame.f_code.co_filename, frame.f_lineno, 0)
loc = ir.Location.name(frame.f_code.co_name, childLoc=file_loc)
res_or_list = opFunc(*args, **kwargs, loc=loc)
return res_or_list
return wrapper
@@ -0,0 +1,581 @@
# 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 helper functions that are generated by the preprocessor.
The preprocessor read through python's ast and changes the input code.
"""
from typing import Callable, Iterator, Optional, overload
from typing_extensions import deprecated
import warnings
import inspect
from types import BuiltinFunctionType
from functools import lru_cache
from .utils.logger import log
from .common import *
from ._mlir_helpers.arith import ArithValue
class Executor:
"""
The Executor class handles dynamic and compile-time (constexpr) execution
of "for" loops and "if-else-elif" statements.
Methods:
set_functions: Assigns the functions for checking loop bounds and
conditional evaluation.
for_execute: Generates MLIR for OP
while_execute: Generates MLIR while OP
if_execute: generate MLIR if OP
"""
def __init__(self):
self._is_dynamic_expression = None
self._loop_execute_range_dynamic = None
self._if_dynamic = None
self._while_dynamic = None
self._compare_executor = None
self._any_executor = None
self._all_executor = None
self._builtin_redirector = None
def set_functions(
self,
*,
is_dynamic_expression: Callable,
loop_execute_range_dynamic: Callable,
if_dynamic: Callable,
while_dynamic: Callable,
compare_executor: Callable,
any_executor: Callable = None,
all_executor: Callable = None,
builtin_redirector: Callable = None,
):
self._is_dynamic_expression = is_dynamic_expression
self._loop_execute_range_dynamic = loop_execute_range_dynamic
self._if_dynamic = if_dynamic
self._while_dynamic = while_dynamic
self._compare_executor = compare_executor
self._any_executor = any_executor
self._all_executor = all_executor
self._builtin_redirector = builtin_redirector
@staticmethod
def convert_to_list(x):
"""This function is used to convert x to a list.
If x is None, return an empty list.
If x is not a list, return a list containing x.
Otherwise, return x itself.
"""
if x is None:
return []
if not isinstance(x, list):
return [x]
return x
@staticmethod
def converge_ret_val(res):
"""This function is used to converge res (the return value) of the function.
If res is None, return None.
If res is a list and has only one element, return the element.
Otherwise, return res itself.
"""
if res is None:
return res
elif isinstance(res, list) and len(res) == 1:
return res[0]
return res
def for_execute(
self,
func,
start,
stop,
step,
write_args=[],
full_write_args_count=0,
write_args_names=[],
unroll=-1,
unroll_full=False,
prefetch_stages=None,
):
assert (
self._loop_execute_range_dynamic
), "Functions must be set before execution."
log().debug("start [%s] stop [%s] step [%s]", start, stop, step)
return self._loop_execute_range_dynamic(
func,
start,
stop,
step,
write_args,
full_write_args_count,
write_args_names,
unroll,
unroll_full,
prefetch_stages,
)
def if_execute(
self,
pred,
then_block: Callable,
else_block: Optional[Callable] = None,
write_args=[],
full_write_args_count=0,
write_args_names=[],
):
assert self._if_dynamic, "Functions must be set before execution."
# MLIR generation
return self._if_dynamic(
pred,
then_block,
else_block,
write_args,
full_write_args_count,
write_args_names,
)
def while_execute(
self,
pred,
while_before_block: Callable,
while_after_block: Callable,
write_args=[],
full_write_args_count=0,
write_args_names=[],
):
assert self._while_dynamic, "Functions must be set before execution."
# MLIR generation
return self._while_dynamic(
while_before_block,
while_after_block,
write_args,
full_write_args_count,
write_args_names,
)
# =============================================================================
# Decorator
# =============================================================================
executor = Executor()
def loop_selector(
start,
stop,
step,
*,
write_args=[],
full_write_args_count=0,
write_args_names=[],
unroll=-1,
unroll_full=False,
prefetch_stages=None,
):
log().debug(
"start [%s] stop [%s] step [%s] write_args [%s] full_write_args_count [%s] write_args_names [%s] unroll [%s] unroll_full [%s] prefetch_stages [%s]",
start,
stop,
step,
write_args,
full_write_args_count,
write_args_names,
unroll,
unroll_full,
prefetch_stages,
)
from .typing import Integer, Numeric
def _maybe_upcast(value):
if isinstance(value, Integer):
value = value.ir_value()
return value
start = _maybe_upcast(start)
stop = _maybe_upcast(stop)
step = _maybe_upcast(step)
def ir_loop(func):
return executor.for_execute(
func,
start,
stop,
step,
write_args,
full_write_args_count,
write_args_names,
unroll,
unroll_full,
prefetch_stages,
)
return ir_loop
def if_selector(pred, write_args=[]):
log().debug("pred [%s] write_args [%s]", pred, write_args)
# Handle Numeric types here?
from .typing import Numeric
if isinstance(pred, Numeric):
pred = pred.value
def ir_loop(func):
return func(pred, *write_args)
return ir_loop
def while_selector(pred, write_args=[]):
def ir_while_loop(func):
return func(pred, *write_args)
return ir_while_loop
def while_executor(
pred,
while_before_block: Callable,
while_after_block: Callable,
write_args=[],
full_write_args_count=0,
write_args_names=[],
):
return executor.while_execute(
pred,
while_before_block,
while_after_block,
write_args,
full_write_args_count,
write_args_names,
)
def if_executor(
pred,
then_block: Callable,
else_block: Optional[Callable] = None,
write_args=[],
full_write_args_count=0,
write_args_names=[],
):
return executor.if_execute(
pred,
then_block,
else_block,
write_args,
full_write_args_count,
write_args_names,
)
# =============================================================================
# Range
# =============================================================================
class range:
"""
A range-like object for dynamic loop iteration in the DSL.
This class provides a range interface similar to Python's built-in range,
but is designed to be preprocessed into constructs for dynamic
loop execution.
The class supports both single-argument (stop) and three-argument
(start, stop, step) constructors with additional parameters for loop
optimization:
- unroll: Number of iterations to unroll (0 or 1 = no unrolling)
- unroll_full: Whether to fully unroll the loop
- prefetch_stages: Number of prefetch stages to generate
"""
@overload
def __new__(cls, stop, unroll=0, unroll_full=False, prefetch_stages=None):
pass
@overload
def __new__(
cls, start, stop, step, unroll=0, unroll_full=False, prefetch_stages=None
):
pass
def __new__(cls, *args, **kwargs):
raise DSLRuntimeError("dynamic range should be always preprocessed to IR")
def __iter__(self) -> Iterator[int]:
raise DSLRuntimeError("dynamic range should be always preprocessed to IR")
@deprecated(
"range_dynamic is deprecated and will be removed in the future, please remove it."
)
def range_dynamic(*args, **kwargs):
raise DSLRuntimeError("range_dynamic should be always preprocessed to IR")
def range_constexpr(*args):
raise DSLRuntimeError("range_constexpr should be preprocessed by preprocessor.")
# =============================================================================
# If expressions
# =============================================================================
def const_expr(expression):
"""
This function is used to check if the expression is a python value.
If the expression is a python value, return the boolean value of the expression.
If the expression is a dynamic expression, raise an error.
"""
from .typing import Numeric
failed = False
if isinstance(expression, Numeric):
if isinstance(expression.value, (int, float, bool)):
return expression.value
else:
failed = True
elif executor._is_dynamic_expression(expression):
failed = True
if failed:
raise DSLRuntimeError(
f"The function `const_expr({expression})` received a dynamic expression (non compile-time constant).",
context={
"If your expression depends on dynamic values": "Remove `const_expr()`",
},
)
return expression
@deprecated(
"dynamic_expr is deprecated and will be removed in the future, please remove it."
)
def dynamic_expr(expression):
return expression
# =============================================================================
# Assertion & casting
# =============================================================================
def assert_executor(test, msg=None):
from .typing import Numeric
fail = False
# Implicit convert dynamic expression to bool is not allowed
# So here explicitly do a None check
if test is not None and executor._is_dynamic_expression(test):
if isinstance(test, Numeric):
try:
test = test.to(bool)
except:
fail = True
else:
fail = True
if not fail:
assert test, msg
else:
raise DSLRuntimeError(
"Only constexpr (Python Value) is allowed here, but got non-constexpr (IR Values) expression.",
suggestion="Please replace with runtime assert.",
)
def bool_cast(value):
if executor._is_dynamic_expression(value):
raise DSLRuntimeError(
"Only constexpr (Python Value) is allowed here, but got non-constexpr (IR Values) expression.",
suggestion="Please explicitly convert to boolean with expressions like comparision.",
)
return bool(value)
def compare_executor(left, comparators, ops):
"""
Executes comparison operations with a left operand and a list of comparators.
Args:
left: The leftmost value in the comparison chain
comparators: A list of values to compare against
ops: A list of comparison operators to apply
Returns:
The result of the comparison chain
Raises:
AssertionError: If the executor function is not set before execution
"""
assert (
executor._compare_executor is not None
), "Function must be set before execution."
return executor._compare_executor(left, comparators, ops)
def any_executor(iterable):
"""Executes the 'any' operation on an iterable, handling both dynamic and static expressions.
:param iterable: An iterable to check if any elements evaluate to True
:type iterable: Iterable
:return: boolean of Python value or IR value
:rtype: bool or cutlass.Boolean
"""
if executor._any_executor and executor._is_dynamic_expression(iterable):
return executor._any_executor(iterable)
else:
return any(iterable)
def all_executor(iterable):
"""Executes the 'all' operation on an iterable, handling both dynamic and static expressions.
:param iterable: An iterable to check if all elements evaluate to True
:type iterable: Iterable
:return: boolean of Python value or IR value
:rtype: bool or cutlass.Boolean
"""
if executor._all_executor and executor._is_dynamic_expression(iterable):
return executor._all_executor(iterable)
else:
return all(iterable)
# =============================================================================
# Control flow checks
# =============================================================================
class DSLOptimizationWarning(Warning):
"""
This warning is used to warn the user about the optimization related issues in DSL.
"""
def __init__(self, message):
self.message = message
super().__init__()
def __str__(self):
return self.message
def range_value_check(*args):
"""
Ensure all `range_constexpr` bounds are compile-time constants (Python ints).
"""
try:
args = tuple(arg.__index__() for arg in args)
# Compute range size and warn if it's too large
start = 0
end = 0
step = 1
if len(args) == 1:
end = args[0]
elif len(args) == 2:
start = args[0]
end = args[1]
elif len(args) == 3:
start = args[0]
end = args[1]
step = args[2]
range_length = (abs(end - start) - 1) // abs(step) + 1
if range_length >= 64:
warnings.warn(
f"This static loop has {range_length} iterations, which may be very slow to compile, consider using `cutlass.range(..., unroll_full=True)` instead.",
category=DSLOptimizationWarning,
stacklevel=2,
)
return (start, end, step)
except:
raise DSLRuntimeError(
"`range_constexpr` requires constexpr (compile-time constant) for all arguments.",
suggestion="Use `range` instead of `range_constexpr`.",
)
def range_perf_warning(filename, lineno, *args):
has_dynamic_expr = False
for arg in args:
if executor._is_dynamic_expression(arg):
has_dynamic_expr = True
break
if not has_dynamic_expr:
warnings.warn_explicit(
(
"This loop is no longer unrolled and may cause performance regression. "
"Use `range(..., unroll_full=True)` for full unrolling, or switch to `range_constexpr` when bounds are compile-time constants."
),
category=DSLOptimizationWarning,
filename=filename,
lineno=lineno,
)
@lru_cache(maxsize=1)
def _get_self_module():
"""
This function is used to get the owning module of this function.
"""
return inspect.getmodule(_get_self_module)
def cf_symbol_check(symbol):
"""
Check if the symbol is control flow symbol from current module.
"""
failed = False
name = symbol.__name__
self_module = _get_self_module()
if inspect.ismodule(symbol):
name = "range"
if not self_module.__name__.startswith(symbol.__name__):
failed = True
else:
owning_module = inspect.getmodule(symbol)
if owning_module != self_module:
failed = True
if failed:
raise DSLRuntimeError(
f"Incorrect {symbol.__name__} is used.",
suggestion=f"Please avoid overriding `{symbol.__name__}` from DSL package.",
)
def redirect_builtin_function(fcn):
"""
This function is used to redirect built-in function call
to the function defined in DSL package.
"""
# Only redirect if it's a built-in
if isinstance(fcn, BuiltinFunctionType) and executor._builtin_redirector:
return executor._builtin_redirector(fcn)
return fcn
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,153 @@
# 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 cache load/dump helper functions
"""
import os
import uuid
import random
import tempfile
import pwd
import time
from pathlib import Path
import hashlib
from .utils.logger import log
from .jit_executor import JitExecutor
from .._mlir import ir
# =============================================================================
# Jit Cache Helper functions
# =============================================================================
def get_current_user():
# Try to get the user from the environment variable first
user = os.getenv("USER") or os.getenv("USERNAME")
if not user:
# Fallback for Unix-like systems
user = pwd.getpwuid(os.getuid()).pw_name
return user
try:
default_generated_ir_path = f"/tmp/{get_current_user()}/cutlass_python_cache/"
except Exception as e:
# If all else fails, provide a default fallback path
default_generated_ir_path = "/tmp/cutlass_python_cache/"
print(f"Could not determine user, using default path. Error: {e}")
def load_ir(file, asBytecode=False):
"""Load generated IR from a file."""
assert "mlir" in file
func_name = file.split(".mlir")[0].split("dsl_")[-1]
with ir.Context() as ctx:
with open(file, "rb" if asBytecode else "r") as f:
module = ir.Module.parse(f.read())
return func_name, module
def make_unique_filename(fpath: Path, new_ext: str = None) -> Path:
"""Generate a unique filename with an optional new extension."""
random_part = random.randint(0, 999999)
timestamp = time.time()
hash_input = f"{fpath}_{timestamp}_{random_part}".encode()
hash_code = hashlib.md5(hash_input).hexdigest()[:16] # Shorter hash for readability
stem_with_hash = f"{fpath.stem}_{hash_code}"
return fpath.with_name(stem_with_hash).with_suffix(new_ext or fpath.suffix)
def save_ir(
dsl_name: str,
module: object,
fname: str,
isTemp: bool = False,
asBytecode: bool = False,
) -> str:
"""Save generated IR to a file."""
initial_name = f"{dsl_name.lower()}_{fname}.mlir"
save_path = Path(tempfile.gettempdir() if isTemp else os.getcwd())
save_fname = save_path / initial_name
# Random ID to avoid any collisions
rnd_id = str(uuid.uuid4())
pid = os.getpid()
# use temp dir to be robust against program interruptions
temp_dir = os.path.join(save_path, f"tmp.pid_{pid}_{rnd_id}")
# If the process exits abnormally, may leave a temporary folder. Needs to be removed manually.
os.makedirs(temp_dir, exist_ok=False)
temp_fname = os.path.join(temp_dir, initial_name)
if asBytecode:
with open(temp_fname, "wb") as f:
module.operation.write_bytecode(f)
else:
with open(temp_fname, "w") as f:
print(module, file=f)
# os.replace is guaranteed to be atomic on POSIX systems if it succeeds
# so filepath cannot see a partial write
os.replace(temp_fname, save_fname)
os.removedirs(temp_dir)
log().debug("Generated IR saved into %s", save_fname)
return save_fname
def check_func_name(jit_cache, func_name):
if not func_name in jit_cache:
jit_cache[func_name] = JitExecutor(None, None, None, None, None, None)
return jit_cache
def load_cache_from_path(dsl_name, cache_limit, path=default_generated_ir_path):
"""Load cache from a directory path."""
if not os.path.exists(path):
return dict()
files = os.listdir(path)
jit_cache = dict()
try:
for idx, file in enumerate(files):
if idx >= int(cache_limit):
break
# identify dsl prefix
if not file.startswith(f"{dsl_name.lower()}"):
continue
if ".mlir" in file:
func_name, ir_module = load_ir(
os.path.join(path, file), asBytecode=True
)
jit_cache = check_func_name(jit_cache, func_name)
jit_cache[func_name].ir_module = ir_module
except Exception as e:
print(f"{dsl_name} failed with loading generated IR cache.", e)
jit_cache = dict()
return jit_cache
def dump_cache_to_path(
dsl_name, jit_cache, cache_limit, path=default_generated_ir_path
):
log().info("JIT cache : dumping [%s] items=[%s]", dsl_name, len(jit_cache))
os.makedirs(path, exist_ok=True)
original_path = os.getcwd()
try:
os.chdir(path)
for idx, [key, value] in enumerate(jit_cache.items()):
if idx >= int(cache_limit):
break
save_ir(dsl_name, value.ir_module, key, asBytecode=True)
except Exception as e:
print(f"{dsl_name} failed with caching generated IR", e)
finally:
os.chdir(original_path)
+268
View File
@@ -0,0 +1,268 @@
# 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 os
from typing import Any, Dict, Iterable, Optional, Union
"""
This module provides a Exception classes DSL class for any Dialect.
"""
# Add color codes at the top of the file after imports
class Colors:
"""ANSI color codes for error messages"""
RED = "\033[91m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
GREEN = "\033[92m"
BOLD = "\033[1m"
RESET = "\033[0m"
# =============================================================================
# DSL Exceptions
# =============================================================================
class DSLBaseError(Exception):
"""
Base exception for DSL-related errors.
Provides optional contextual metadata to aid in debugging.
"""
def __init__(
self,
message: str,
line: Optional[int] = None,
snippet: Optional[str] = None,
filename: Optional[str] = None,
error_code: Optional[Union[str, int]] = None,
context: Optional[Union[Dict[str, Any], str]] = None,
suggestion: Optional[str] = None,
cause: Optional[BaseException] = None,
) -> None:
self.message = message
self.line = line
self.filename = filename
self.snippet = snippet
self.error_code = error_code
self.context = context
self.suggestion = suggestion
self.cause = cause
super().__init__(self._format_message())
def _format_message(self):
"""
Formats the complete error message with available metadata.
Override this in subclasses if you want to change formatting logic.
"""
parts = [f"{self.__class__.__name__}: {self.message}"]
if self.error_code is not None:
parts.append(f"{Colors.BOLD}Error Code:{Colors.RESET} {self.error_code}\n")
if self.line is not None:
parts.append(f" Line: {self.line}")
if self.filename is not None:
parts.append(f" File: {self.filename}")
if self.snippet:
# Optionally truncate long snippets for readability
parts.append(f" Snippet: \n {self.snippet}")
if self.cause:
parts.append(f" Caused exception: {self.cause}")
if self.context:
if isinstance(self.context, dict):
parts.append(f"{Colors.BLUE}🔍 Additional Context:{Colors.RESET}\n")
for key, value in self.context.items():
parts.append(f" {key}: {value}")
else:
parts.append(
f"{Colors.BLUE}🔍 Additional Context:{Colors.RESET} {self.context}"
)
if self.suggestion:
parts.append(f"{Colors.GREEN}💡 Suggestions:{Colors.RESET}")
if isinstance(self.suggestion, (list, tuple)):
for suggestion in self.suggestion:
parts.append(f" {Colors.GREEN}{suggestion}{Colors.RESET}")
else:
parts.append(f" {self.suggestion}")
return "\n".join(parts)
class DSLRuntimeError(DSLBaseError):
"""
Raised when an error occurs during JIT-time code generation in the DSL.
"""
# Inherits all logic from DSLBaseError; override methods if you need
# specialized behavior or formatting for runtime errors.
pass
def _get_friendly_cuda_error_message(error_code, error_name):
# Avoid circular dependency
from .runtime.cuda import get_device_info
"""Get a user-friendly error message for common CUDA errors."""
# Strip the byte string markers if present
if isinstance(error_name, bytes):
error_name = error_name.decode("utf-8")
elif (
isinstance(error_name, str)
and error_name.startswith("b'")
and error_name.endswith("'")
):
error_name = error_name[2:-1]
# Add target architecture info
target_arch = os.getenv("CUTE_DSL_ARCH", "unknown")
error_messages = {
"CUDA_ERROR_INVALID_SOURCE": (
f"{Colors.RED}❌ Failed to load CUDA kernel - likely architecture mismatch.{Colors.RESET}\n\n"
),
"CUDA_ERROR_NO_BINARY_FOR_GPU": (
f"{Colors.RED}❌ CUDA kernel not compatible with your GPU.{Colors.RESET}\n\n"
),
"CUDA_ERROR_OUT_OF_MEMORY": (
f"{Colors.RED}💾 CUDA out of memory error.{Colors.RESET}\n\n"
),
"CUDA_ERROR_INVALID_DEVICE": (
f"{Colors.RED}❌ Invalid CUDA device.{Colors.RESET}\n\n"
),
"CUDA_ERROR_NOT_INITIALIZED": (
f"{Colors.RED}❌ CUDA context not initialized.{Colors.RESET}\n\n"
),
"CUDA_ERROR_INVALID_VALUE": (
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}"
),
}
error_suggestions = {
"CUDA_ERROR_INVALID_SOURCE": (
f"1. Ensure env CUTE_DSL_ARCH matches your GPU architecture",
f"2. Clear the compilation cache and regenerate the kernel",
f"3. Check CUDA toolkit installation",
),
"CUDA_ERROR_NO_BINARY_FOR_GPU": (
f"Set env CUTE_DSL_ARCH to match your GPU architecture",
),
"CUDA_ERROR_OUT_OF_MEMORY": (
f"1. Reduce batch size",
f"2. Reduce model size",
f"3. Free unused GPU memory",
),
"CUDA_ERROR_INVALID_DEVICE": (
f"1. Check if CUDA device is properly initialized",
f"2. Verify GPU is detected: nvidia-smi",
f"3. Check CUDA_VISIBLE_DEVICES environment variable",
),
"CUDA_ERROR_NOT_INITIALIZED": (
f"1. Check CUDA driver installation",
f"2. call `cuda.cuInit(0)` before any other CUDA operation",
f"3. Run nvidia-smi to confirm GPU status",
),
"CUDA_ERROR_INVALID_VALUE": (
f"1. Your GPU model",
f"2. SM ARCH setting",
f"3. Steps to reproduce",
),
}
message = error_messages.get(
error_name, f"{Colors.RED}Unknown CUDA error{Colors.RESET}"
)
# Add debug information
debug_info = f"\n- {Colors.BOLD}Error name: {error_name}\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"
)
try:
# Get GPU information using CUDA Python API
debug_info += f"\n{Colors.BLUE}📊 GPU Information:{Colors.RESET}\n"
gpu_info = get_device_info()
debug_info += gpu_info.pretty_str()
if target_arch and gpu_info.compatible_archs:
debug_info += f"\n{Colors.BOLD}Compatibility Check:{Colors.RESET}\n"
if target_arch not in gpu_info.compatible_archs:
debug_info += (
f"{Colors.RED}❌ Error: Target SM ARCH {target_arch} is not compatible\n"
f"💡 Please use one of SM ARCHs: "
f"{Colors.GREEN}{', '.join(gpu_info.compatible_archs or [])}{Colors.RESET}\n"
)
elif target_arch != gpu_info.sm_arch:
debug_info += (
f"{Colors.YELLOW}⚠️ Warning: Using compatible but non-optimal architecture\n"
f"• Current: {target_arch}\n"
f"• Recommended: {Colors.GREEN}{gpu_info.sm_arch}{Colors.RESET} (native)\n"
)
else:
debug_info += f"{Colors.GREEN}✓ Using optimal architecture: {gpu_info.sm_arch}{Colors.RESET}\n"
except Exception as e:
debug_info += (
f"\n{Colors.YELLOW}️ Could not retrieve GPU info: {str(e)}{Colors.RESET}"
)
return message, debug_info, error_suggestions.get(error_name, "")
class DSLCudaRuntimeError(DSLBaseError):
"""
Raised when an error occurs during CUDA runtime code generation in the DSL.
"""
# Inherits all logic from DSLRuntimeError; override methods if you need
# specialized behavior or formatting for runtime errors.
def __init__(self, error_code, error_name) -> None:
self._error_code = error_code
self._error_name = error_name
message, debug_info, suggestion = _get_friendly_cuda_error_message(
error_code, error_name
)
super().__init__(
message, error_code=error_code, context=debug_info, suggestion=suggestion
)
class DSLAstPreprocessorError(DSLBaseError):
"""
Raised when an error occurs during AST preprocessing or visiting in the DSL.
"""
# Same approach: You could override _format_message if you want
# to emphasize AST node details or anything specific to preprocessing.
pass
class DSLNotImplemented(DSLBaseError):
"""
Raised when a feature of the DSL is not implemented yet.
"""
# Useful for stubs in your DSL that you plan to implement in the future.
pass
+288
View File
@@ -0,0 +1,288 @@
# 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 a class that compiles generated IR using MLIR's PassManager
and executes it using MLIR's ExecutionEngine.
"""
from typing import Sequence, Optional, Tuple
import os
import sys
import inspect
import argparse
from .common import DSLRuntimeError
from .utils.logger import log
_SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__))
sys.path.append(_SCRIPT_PATH)
from .._mlir import ir
# =============================================================================
# Compiler Class
# =============================================================================
class CompilationError(RuntimeError):
"""Custom error class for compilation failures"""
# Add ANSI color codes
RED = "\033[91m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
GREEN = "\033[92m"
BOLD = "\033[1m"
RESET = "\033[0m"
def __init__(
self,
message: str,
nvvm_error: Optional[str] = None,
ir_context: Optional[str] = None,
cuda_toolkit: Optional[str] = None,
arch: Optional[str] = None,
):
self.nvvm_error = nvvm_error
self.ir_context = ir_context
self.cuda_toolkit = cuda_toolkit
self.arch = arch
# Call parent with formatted error to avoid showing class name
super().__init__("") # Empty string to avoid class name
# Store formatted error for str() representation
self._formatted_error = self._format_error()
def __str__(self) -> str:
"""Override string representation to avoid showing class name"""
return self._formatted_error
def __repr__(self) -> str:
"""Override repr representation to avoid showing class name"""
return self._formatted_error
def _format_error(self) -> str:
if not self.nvvm_error:
return str(self.args[0])
return f"""NVVM Compilation Error:
----------------------
{self.BLUE}⚙️ Current Settings:{self.RESET}
{self.BOLD}- CUDA Toolkit Path: {self.cuda_toolkit or "Not Set"}
- Target Architecture: {self.arch}{self.RESET}
IR Context (truncated):
{self.ir_context}
{self.YELLOW}💡 Possible Solutions:{self.RESET}
{self.GREEN}1. Check if CUDA_TOOLKIT_PATH is set correctly
2. Verify target architecture ({self.arch}) is supported by your CUDA toolkit
3. Make sure CUDA toolkit version matches the target architecture requirements{self.RESET}"""
class Compiler:
"""Compiler class for compiling and building MLIR modules."""
def __init__(self, passmanager, execution_engine):
self.passmanager = passmanager
self.execution_engine = execution_engine
def __call__(self, module):
"""Convenience application method."""
self.compile(module)
def _process_error(self, error_msg: str) -> Tuple[Optional[str], Optional[str]]:
"""Process error message to extract NVVM error and IR context"""
nvvm_error = None
ir_msg = ""
if "NVVM_ERROR" in error_msg:
# Extract the specific NVVM error
nvvm_error = (
error_msg.split("libNVVM extra log:")[1].strip()
if "libNVVM extra log:" in error_msg
else error_msg
)
# Extract IR context
if "see current operation:" in error_msg:
# Get the IR section
ir_section = error_msg.split("see current operation:")[1].strip()
# Remove duplicate IR section
ir_section = ir_section.split("error: unknown: Failed translating")[
0
].strip()
# Get first few lines and last few lines of the IR
ir_lines = ir_section.split("\n")
if len(ir_lines) > 10:
ir_msg = "\n".join(ir_lines[:5] + [" ..."] + ir_lines[-5:])
else:
ir_msg = ir_section
return nvvm_error, ir_msg
def compile(
self,
module,
pipeline: str,
cuda_toolkit: str = "",
arch: str = "",
enable_verifier=False,
):
"""Compiles the module by invoking the pipeline."""
try:
pm = self.passmanager.PassManager.parse(pipeline)
pm.enable_verifier(enable_verifier)
pm.run(module.operation)
except Exception as e:
error_msg = str(e)
nvvm_error, ir_msg = self._process_error(error_msg)
if nvvm_error:
raise CompilationError(
error_msg,
nvvm_error=nvvm_error,
ir_context=ir_msg,
cuda_toolkit=cuda_toolkit,
arch=arch,
) from e
raise e
def jit(self, module, opt_level: int = 2, shared_libs: Sequence[str] = ()):
"""Wraps the module in a JIT execution engine."""
return self.execution_engine.ExecutionEngine(
module, opt_level=opt_level, shared_libs=shared_libs
)
def compile_and_jit(
self,
module,
pipeline: str,
shared_libs: Sequence[str] = (),
opt_level: int = 2,
cuda_toolkit: str = "",
arch: str = "",
):
"""Compiles and jits the module."""
self.compile(
module,
pipeline,
cuda_toolkit,
arch,
)
return self.jit(module, opt_level, shared_libs)
class CompileOptions:
def __init__(self, options: str = ""):
"""
This class encapsulates all compilation options relevant to function compilation.
It provides a convenient way to manage and pass compilation options,
particularly for controlling compilation settings.
By centralizing these options, it ensures consistent and flexible configuration of
compilation parameters such as optimization level, debugging control, etc.
:param options: The options for the function. Will be parsed by argparse.
:type options: str
"""
if not isinstance(options, str):
raise DSLRuntimeError(
f"Invalid compilation `options`: {options}, it should be a string"
)
self._parser = argparse.ArgumentParser()
self._parser.add_argument("--opt-level", nargs="?", type=int, default=3)
self._parser.add_argument(
"--enable-device-assertions", action="store_true", default=False
)
self._parser.add_argument("--link-libraries", type=str, default="")
try:
self._options = self._parser.parse_args(options.split())
except SystemExit as e:
# catch argparse error and raise as DSLRuntimeError
raise DSLRuntimeError(
f"Invalid compile options: '{options}'. Please check the option values and format."
)
log().info("`cute.compile` CompileOptions: options=" + options)
def to_str(self):
"""
Generate a string representation of all compilation options
which will be used in pipeline options.
"""
option_strings = []
for key, value in vars(self._options).items():
hyphen_key = key.replace("_", "-")
if isinstance(value, bool):
formatted_value = "true" if value else "false"
else:
formatted_value = str(value)
option_strings.append(f"{hyphen_key}={formatted_value}")
return " ".join(option_strings)
def compile(func, *args, **kwargs):
"""
This function is used to compile a `cute.jit` decorated function.
It will process the compile options and input parameters, do explicit compilation and return the jit executor.
:param func: The function to compile. It can be a regular function, a method or a class instance.
:param args: The arguments to pass to the function.
:param kwargs: The keyword arguments to pass to the function. It can contain `options` like
`opt_level` to control the compilation flags.
:return: The jit executor.
:raises: DSLRuntimeError if the function is not decorated with `cute.jit` or is not callable.
"""
if func is None:
raise DSLRuntimeError("Function is not set or invalid.")
if not callable(func):
raise DSLRuntimeError("Object is not callable.")
kwargs["compile_only"] = True
kwargs["no_cache"] = True
if inspect.isfunction(func):
# regular function
pass
elif inspect.ismethod(func):
# if it's a method, add the instance to the first argument
args = [func.__self__] + list(args)
func = func.__func__
elif inspect.isclass(type(func)) and hasattr(func, "__call__"):
# If it's a class instance, get the class's __call__ method
args = [func] + list(args)
# Get the actual function from the class definition
func = func.__call__.__func__
else:
raise DSLRuntimeError(
"Invalid function type, only function, method and module are supported, but got",
func,
)
# If it's a wrapped function created by jit decorator, get the original function
if hasattr(func, "__wrapped__"):
func = func.__wrapped__
if not hasattr(func, "_dsl_object"):
raise DSLRuntimeError("Function is not decorated with jit decorator.")
# process compile options, extract the options and remove them from the kwargs
options = kwargs.pop("options", "")
func._dsl_object.compile_options = CompileOptions(options)
fcn_ptr = func._dsl_object._preprocess_and_execute(func)
return func._dsl_object._func(fcn_ptr, *args, **kwargs)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,320 @@
# 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 utilities for the environment variables setup.
It provides an EnvironmentVarManager, which reads environment variables for the DSL
and caches them for efficient access.
It also provides utilities to automatically setup a subset of environment variables
based on heuristics.
"""
import os
import sys
import shutil
import glob
from pathlib import Path
from functools import lru_cache
from typing import Any
from ..base_dsl.runtime.cuda import get_compute_capability_major_minor
from .utils.logger import log
IS_WINDOWS = sys.platform == "win32"
CLIB_EXT = ".dll" if IS_WINDOWS else ".so"
# =============================================================================
# Environment Variable Helpers
# =============================================================================
@lru_cache(maxsize=None)
def get_str_env_var(var_name, default_value=None):
value = os.getenv(var_name)
return value if value is not None else default_value
@lru_cache(maxsize=None)
def get_bool_env_var(var_name, default_value=False):
value = get_str_env_var(var_name)
if value is None:
return default_value
return value not in {"False", "0", ""}
@lru_cache(maxsize=None)
def get_int_env_var(var_name, default_value=0):
value = get_str_env_var(var_name)
return int(value) if value and value.isdigit() else default_value
@lru_cache(maxsize=None)
def has_env_var(var_name):
return os.getenv(var_name) is not None
def detect_gpu_arch(prefix):
"""
Attempts to detect the machine's GPU architecture.
Returns:
A string representing the GPU architecture (e.g. "70" for compute capability 7.0),
or a default value(e.g. "sm_100") if the GPU architecture cannot be determined.
"""
arch = (None, None)
try:
arch = get_compute_capability_major_minor()
except Exception as e:
log().info(f"Failed to get CUDA compute capability: {e}")
if arch == (None, None):
# default to sm_100
arch = (10, 0)
major, minor = arch
suffix = ""
if major >= 9:
suffix = "a"
return f"sm_{major}{minor}{suffix}"
def find_libs_in_ancestors(start, target_libs, lib_folder_guesses):
"""
Search ancestor directories for a candidate library folder containing all required libraries.
Starting from the given path, this function traverses up through each parent directory.
For every ancestor, it checks candidate subdirectories (specified by lib_folder_guesses)
for files that match the required library extension (CLIB_EXT). Library file names are
canonicalized by removing the "lib" prefix from their stem. If a candidate directory contains
all of the required libraries (as specified in target_libs), the function returns a list of
absolute paths to these library files.
Parameters:
start (str or Path): The starting directory from which to begin the search.
target_libs (iterable of str): A collection of required library names (without the "lib" prefix).
lib_folder_guesses (iterable of str): Relative paths from an ancestor directory that may contain the libraries.
Returns:
list[str] or None: A list of resolved paths to the required library files if found; otherwise, None.
"""
# Traverse through all parent directories of the resolved starting path.
for ancestor in Path(start).resolve().parents:
# Iterate over each candidate relative directory path.
for rel_path in lib_folder_guesses:
target_dir = ancestor / rel_path
# Skip if the candidate directory does not exist.
if not target_dir.is_dir():
continue
# Initialize a list to hold the resolved paths of matching library files.
libs_cand = []
# Create a set of the remaining libraries we need to find.
remaining_libs = set(target_libs)
# Iterate over all items in the candidate directory.
for p in target_dir.iterdir():
# Consider only files with the expected library extension.
if p.suffix == CLIB_EXT:
# Canonicalize the library name by removing the "lib" prefix.
lib_name = p.stem.removeprefix("lib")
# If this library is required, add its resolved path and mark it as found.
if lib_name in remaining_libs:
libs_cand.append(str(p.resolve()))
remaining_libs.remove(lib_name)
# If all required libraries have been found, return the list of library paths.
if len(remaining_libs) == 0:
return libs_cand
# Return None if no candidate directory contains all required libraries.
return None
def _find_cuda_home():
"""Find the CUDA installation path using a series of heuristic methods.
Methods below are checked in order, and the function returns on first match:
1. Checking the environment variables CUDA_HOME and CUDA_PATH.
2. Searching for the 'nvcc' compiler in the system PATH and deriving the path of cuda.
3. Scanning common installation directories based on the operating system.
- On Windows systems (when IS_WINDOWS is True), it searches in:
C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v*.*
- On Unix-like systems, it searches in:
/usr/local/cuda*
Returns:
Optional[str]: The absolute CUDA installation path if found; otherwise, None.
Note:
The variable IS_WINDOWS is defined in the module scope.
"""
# Guess #1
cuda_home = get_str_env_var("CUDA_HOME") or get_str_env_var("CUDA_PATH")
if cuda_home is None:
# Guess #2
nvcc_path = shutil.which("nvcc")
if nvcc_path is not None:
cuda_home = os.path.dirname(os.path.dirname(nvcc_path))
else:
# Guess #3
if IS_WINDOWS:
glob_pat = "C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v*.*"
else:
glob_pat = "/usr/local/cuda*"
cuda_homes = glob.glob(glob_pat)
if len(cuda_homes) == 0:
cuda_home = ""
else:
cuda_home = cuda_homes[0]
if not os.path.exists(cuda_home):
cuda_home = None
return cuda_home
def get_cuda_toolkit_path():
"""
Get cuda_toolkit_path. It returns get_str_env_var('CUDA_TOOLKIT_PATH') if
set. Otherwise, attempts to discover a valid CUDA toolkit location and
return. If not found, return None.
"""
# Check if the environment variable is already set, if so, return it immediately.
try:
cuda_toolkit_path_existing = get_str_env_var("CUDA_TOOLKIT_PATH")
if cuda_toolkit_path_existing:
return cuda_toolkit_path_existing
found_cuda_home = _find_cuda_home()
if found_cuda_home:
return found_cuda_home
except Exception as e:
log().info("default_env: exception on get_cuda_toolkit_path", e)
return None
def get_prefix_dsl_libs(prefix: str):
"""
Returns get_str_env_var('{prefix}_LIBS') if set.
Otherwise, attempts to discover libs based on heuristics and return
If not found, return None.
"""
# Check if the environment variable is already set, if so, return it immediately.
try:
prefix_libs_existing = get_str_env_var(f"{prefix}_LIBS")
if prefix_libs_existing:
return prefix_libs_existing
def get_libs_cand(start):
target_libs = {
"mlir_c_runner_utils",
"mlir_runner_utils",
"mlir_cuda_runtime",
}
lib_folder_guesses = [
"lib",
]
libs_cand = find_libs_in_ancestors(start, target_libs, lib_folder_guesses)
if libs_cand:
dsl_libs = ":".join(libs_cand)
return dsl_libs
return None
# find from install folder
dsl_libs = get_libs_cand(__file__)
if not dsl_libs:
# try to find from build folder structure
dsl_libs = get_libs_cand(Path(__file__).parent.parent.resolve())
return dsl_libs
except Exception as e:
log().info(f"default_env: exception on get_prefix_dsl_libs", e)
return None
class EnvironmentVarManager:
"""Manages environment variables for configuration options.
Printing options:
- [DSL_NAME]_LOG_TO_CONSOLE: Print logging to stderr (default: False)
- [DSL_NAME]_PRINT_AFTER_PREPROCESSOR: Print after preprocess (default: False)
- [DSL_NAME]_PRINT_IR: Print generated IR (default: False)
- [DSL_NAME]_FILTER_STACKTRACE: Filter internal stacktrace (default: True)
File options:
- [DSL_NAME]_KEEP_IR: Save generated IR in a file (default: False)
- [DSL_NAME]_LOG_TO_FILE: Store all logging into a file, excluding COMPILE_LOGS (default: False)
Other options:
- [DSL_NAME]_LOG_LEVEL: Logging level to set, for LOG_TO_CONSOLE or LOG_TO_FILE (default: 1).
- [DSL_NAME]_DRYRUN: Generates IR only (default: False)
- [DSL_NAME]_ARCH: GPU architecture (default: "sm_100")
- [DSL_NAME]_WARNINGS_AS_ERRORS: Enable warnings as error (default: False)
- [DSL_NAME]_WARNINGS_IGNORE: Ignore warnings (default: False)
- [DSL_NAME]_ENABLE_OPTIMIZATION_WARNINGS: Enable warnings of optimization warnings (default: False)
- [DSL_NAME]_JIT_TIME_PROFILING: Whether or not to profile the IR generation/compilation/execution time (default: False)
- [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]_NO_SOURCE_LOCATION: Generate source location (default: False)
"""
def __init__(self, prefix="DSL"):
self.prefix = prefix # change if needed
# Printing options
self.print_after_preprocessor = get_bool_env_var(
f"{prefix}_PRINT_AFTER_PREPROCESSOR", False
)
self.printIR = get_bool_env_var(f"{prefix}_PRINT_IR", False)
self.filterStacktrace = get_bool_env_var(f"{prefix}_FILTER_STACKTRACE", True)
# File options
self.keepIR = get_bool_env_var(f"{prefix}_KEEP_IR", False)
# Logging options
self.log_to_console = get_bool_env_var(f"{prefix}_LOG_TO_CONSOLE", False)
self.log_to_file = get_bool_env_var(f"{prefix}_LOG_TO_FILE", False)
if (
has_env_var(f"{prefix}_LOG_LEVEL")
and not self.log_to_console
and not self.log_to_file
):
log().warning(
f"Log level was set, but neither logging to file ({prefix}_LOG_TO_FILE) nor logging to console ({prefix}_LOG_TO_CONSOLE) is enabled!"
)
self.log_level = get_int_env_var(f"{prefix}_LOG_LEVEL", 1)
# Other options
self.dryrun = get_bool_env_var(f"{prefix}_DRYRUN", False)
self.arch = get_str_env_var(f"{prefix}_ARCH", detect_gpu_arch(prefix))
self.warnings_as_errors = get_bool_env_var(
f"{prefix}_WARNINGS_AS_ERRORS", False
)
self.warnings_ignore = get_bool_env_var(f"{prefix}_WARNINGS_IGNORE", False)
self.enable_optimization_warnings = get_bool_env_var(
f"{prefix}_ENABLE_OPTIMIZATION_WARNINGS", False
)
self.jitTimeProfiling = get_bool_env_var(f"{prefix}_JIT_TIME_PROFILING", False)
self.disable_file_caching = get_bool_env_var(
f"{prefix}_DISABLE_FILE_CACHING", False
)
self.file_caching_capacity = get_int_env_var(
f"{prefix}_FILE_CACHING_CAPACITY", 1000
)
self.generate_source_location = not get_bool_env_var(
f"{prefix}_NO_SOURCE_LOCATION", False
)
# set cuda
self.cuda_toolkit = get_cuda_toolkit_path()
# set mlir shared libraries
self.shared_libs = get_prefix_dsl_libs(prefix)
@@ -0,0 +1,357 @@
# 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
"""
import ctypes
import inspect
import io
from typing import get_origin
import numpy as np
# MLIR modules imports
from .._mlir import ir
# Local modules imports
from . import typing as t
from .common import DSLRuntimeError
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 CudaSingleModule:
def __init__(self, cuda_module, kernel_ptr):
self.cuda_module = cuda_module
self.kernel_ptr = kernel_ptr
class CudaModules:
def __init__(self, modules, args):
# list of CudaSingleModule
self.modules = modules
# extra kernel ptr arguments for launch
self.args = args
class JitExecutor:
def __init__(
self,
dsl,
engine,
capi_func,
ir_module,
args_spec,
function_name,
cuda_modules: CudaModules = None,
jit_time_profiling=False,
):
self.dsl = dsl
self.engine = engine
self.capi_func = capi_func
self.ir_module = ir_module
self.args_spec = args_spec
self.function_name = function_name
if args_spec is not None:
self.original_args_spec = args_spec
self.args_spec = self.filter_runtime_arg_spec(args_spec)
# cuda kernels
self.cuda_modules = cuda_modules
self.jit_time_profiling = jit_time_profiling
def filter_runtime_arg_spec(self, arg_spec: inspect.FullArgSpec):
runtime_args = []
runtime_annotations = {}
runtime_defaults = []
# Calculate the offset where defaults start in the original args
if arg_spec.defaults:
defaults_start_idx = len(arg_spec.args) - len(arg_spec.defaults)
else:
defaults_start_idx = len(arg_spec.args)
# Filter arguments and maintain their properties
for i, arg_name in enumerate(arg_spec.args):
arg_type = arg_spec.annotations.get(arg_name, None)
# Skip compile-time arguments
if is_arg_spec_constexpr(arg_type, arg_name, i, self.function_name):
continue
# Keep runtime arguments
runtime_args.append(arg_name)
if arg_name in arg_spec.annotations:
runtime_annotations[arg_name] = arg_type
# Keep corresponding default if it exists
if i >= defaults_start_idx:
default_idx = i - defaults_start_idx
runtime_defaults.append(arg_spec.defaults[default_idx])
# Filter kwonlyargs and their defaults
runtime_kwonlyargs = []
runtime_kwonlydefaults = {}
if arg_spec.kwonlyargs:
for kwarg in arg_spec.kwonlyargs:
arg_type = arg_spec.annotations.get(kwarg, None)
# Apply same filtering logic
if is_arg_spec_constexpr(arg_type, kwarg, i, self.function_name):
continue
runtime_kwonlyargs.append(kwarg)
if kwarg in arg_spec.annotations:
runtime_annotations[kwarg] = arg_type
if arg_spec.kwonlydefaults and kwarg in arg_spec.kwonlydefaults:
runtime_kwonlydefaults[kwarg] = arg_spec.kwonlydefaults[kwarg]
# Convert runtime_defaults to tuple if not empty (as expected by FullArgSpec)
runtime_defaults = tuple(runtime_defaults) if runtime_defaults else None
return inspect.FullArgSpec(
args=runtime_args,
varargs=arg_spec.varargs, # Keep original varargs
varkw=arg_spec.varkw, # Keep original varkw
defaults=runtime_defaults,
kwonlyargs=runtime_kwonlyargs,
kwonlydefaults=runtime_kwonlydefaults if runtime_kwonlydefaults else None,
annotations=runtime_annotations,
)
def __del__(self):
if self.cuda_modules:
cuda_modules = [module.cuda_module for module in self.cuda_modules.modules]
for module in set(cuda_modules):
cuda_helpers.unload_cubin_module(module)
def get_constexpr_args(self) -> list[dict[str, int | str]]:
"""
This function returns the constexpr args that have been pruned from the original function signature.
The return type is a list of dicts, each dict contains the argument index (argument_index) and argument name (argument_name).
:return: list of dicts, each dict contains the argument index (argument_index) and argument name (argument_name).
:rtype: list[dict[str, int | str]]
"""
if self.original_args_spec is None:
return list()
constexpr_args = list()
for i, arg_name in enumerate(self.original_args_spec.args):
if arg_name not in self.args_spec.args:
constexpr_args.append({"argument_index": i, "argument_name": arg_name})
if self.original_args_spec.kwonlyargs:
for kwarg in self.original_args_spec.kwonlyargs:
if kwarg not in self.args_spec.kwonlyargs:
constexpr_args.append(
{"argument_index": None, "argument_name": kwarg}
)
return constexpr_args
def generate_execution_args(self, args, kwargs, args_spec: inspect.FullArgSpec):
"""
This function is the prune version of `generate_mlir_function_types` which only generates execution args
to get rid of mlir context.
"""
# Process positional arguments with defaults
rectified_args = list(args)
if args_spec.defaults and len(args) < len(args_spec.args):
rectified_args.extend(args_spec.defaults[len(args) - len(args_spec.args) :])
for k, v in kwargs.items():
if k in args_spec.args:
idx = args_spec.args.index(k)
if idx < len(rectified_args):
rectified_args[idx] = v
else:
rectified_args.append(v)
# Process keyword arguments
rectified_kwargs = {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
):
rectified_kwargs.update(args_spec.kwonlydefaults)
# args/kwargs must match arg_specs
if len(rectified_args) != len(args_spec.args) or len(rectified_kwargs) != len(
args_spec.kwonlyargs
):
raise DSLRuntimeError(
"input args/kwargs length does not match runtime function signature!",
context={
"input args length": len(rectified_args),
"input kwargs length": len(rectified_kwargs),
"function signature args length": len(args_spec.args),
"function signature kwonlyargs length": len(args_spec.kwonlyargs),
},
)
exe_args = []
adapted_args = []
input_args = rectified_args + list(rectified_kwargs.values())
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
if hasattr(arg, "__c_pointers__"):
exe_args.extend(arg.__c_pointers__())
continue
arg_type = args_spec.annotations.get(arg_name, None)
# Implicit cast to NumericMeta
if isinstance(arg_type, t.NumericMeta):
arg = t.cast(arg, arg_type)
else:
# If not any known type, try registered adapter to do the conversion
adapter = JitArgAdapterRegistry.get_registered_adapter(type(arg))
if adapter:
arg = adapter(arg)
adapted_args.append(arg)
exe_args.extend(get_c_pointers(arg))
return exe_args, adapted_args
def __call__(self, *args, **kwargs):
exe_args, adapted_args = self.generate_execution_args(
args, kwargs, self.args_spec
)
self.run_compiled_program(exe_args)
# Assume each execution args has type `c_void_p` to reduce the overhead of `ctypes.cast`.
def get_invoke_packed_args(self, exe_args):
if self.cuda_modules:
exe_args += self.cuda_modules.args
packed_args = (ctypes.c_void_p * len(exe_args))()
for argNum in range(len(exe_args)):
packed_args[argNum] = exe_args[argNum]
return packed_args
def run_compiled_program(self, exe_args):
if self.jit_time_profiling:
profiler = timer(enable=True)
try:
packed_args = profiler(self.get_invoke_packed_args)(exe_args)
profiler(self.capi_func)(packed_args)
except Exception as e:
raise DSLRuntimeError(f"💥💥💥 Runtime Crash 💥💥💥", cause=e)
else:
try:
packed_args = self.get_invoke_packed_args(exe_args)
self.capi_func(packed_args)
except Exception as e:
raise DSLRuntimeError(f"💥💥💥 Runtime Crash 💥💥💥", cause=e)
def update_jit_cuda_modules(self, kernel_symbols):
# preload cuda module from compiled cubin in ir and store to jit_executor.kernels.
if len(kernel_symbols) > 0:
extra_args = []
module = self.ir_module
cuda_kernel_cache = dict()
cuda_driver_version = cuda_helpers.get_driver_version()
for sym in kernel_symbols:
if sym not in cuda_kernel_cache:
log().debug(f"Loading CUDA module for symbol: {sym}")
# load cuda module/get function pointer from module and cache
def walk_callback(sym, func_sym, cubin_data):
cubin_module = cuda_helpers.load_cubin_module_data(cubin_data)
kernel_ptr = cuda_helpers.get_kernel_function(
cubin_module, func_sym
)
# Enable non-portable cluster size for CUDA version 11.8 or higher.
if cuda_driver_version >= 11080:
cuda_helpers.set_kernel_attribute(
kernel_ptr,
cuda_helpers.cuda.CUfunction_attribute.CU_FUNC_ATTRIBUTE_NON_PORTABLE_CLUSTER_SIZE_ALLOWED,
1,
)
cuda_kernel_cache[sym] = CudaSingleModule(
cubin_module, kernel_ptr
)
self.walk_module_and_get_cubin_data(module, sym, walk_callback)
else:
log().debug(f"Symbol {sym} already in cache")
# check if kernel is empty.
if sym in cuda_kernel_cache:
extra_args.append(
ctypes.c_void_p(cuda_kernel_cache[sym].kernel_ptr.getPtr())
)
# store to the jit result if jit result is cached.
self.cuda_modules = CudaModules(cuda_kernel_cache.values(), extra_args)
return self
def _get_escaped_cubin_bytes(self, cubin_data):
"""This function escapes cubin data from mlir raw bytecode to executable binary bytes"""
def ishex(inp):
return (
inp in range(0x30, 0x3A)
or inp in range(0x61, 0x67)
or inp in range(0x41, 0x47)
)
converted = bytearray()
idx = 0
while idx < len(cubin_data):
# escape the original bytes
if cubin_data[idx] == 0x5C:
# if data of idx is b'\\'
if ishex(cubin_data[idx + 1]) and ishex(cubin_data[idx + 2]):
converted += bytearray.fromhex(
cubin_data[idx + 1 : idx + 3].decode()
)
idx += 3
elif cubin_data[idx + 1] == 0x5C:
converted.append(cubin_data[idx])
idx += 2
else:
# no escape, directly write
converted.append(cubin_data[idx])
idx += 1
return bytes(converted)
def walk_module_and_get_cubin_data(self, module, sym, callback):
"""This function is used to walk gpu binary op, extract the cubin inside, and process cubin data with callback."""
def walk_gpu_binary_op(op):
if op.name != "gpu.binary":
return ir.WalkResult.ADVANCE
s = io.BytesIO()
op.write_bytecode(s)
cubin_data = s.getvalue()
if sym.encode() not in cubin_data:
return ir.WalkResult.ADVANCE
if (
"kernels" != op.opview.sym_name.value
and sym != op.opview.sym_name.value
):
return ir.WalkResult.ADVANCE
# function symbol of kernel(gpu.launch_func) is equal to sym name in mlir
func_sym = sym
if sym == op.opview.sym_name.value and not sym.endswith("_kernel"):
func_sym = sym.rsplit("_", 1)[0]
cubin_data = cubin_data.split(b'bin = "')[1].split(b'">')[0]
cubin_data = self._get_escaped_cubin_bytes(cubin_data)
callback(sym, func_sym, cubin_data)
return ir.WalkResult.ADVANCE
module.operation.walk(walk_gpu_binary_op)
@@ -0,0 +1,25 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
"""
This module provides a runtime utility functions that are needed for
the DSL.
"""
from . import dlpack_types
from . import cuda
from . import jit_arg_adapters
__all__ = [
"dlpack_types",
"cuda",
"jit_arg_adapters",
]
@@ -0,0 +1,470 @@
# 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
"""
from functools import lru_cache
from dataclasses import dataclass
from typing import List, Optional
import numpy as np
import os
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
# =============================================================================
def _cudaGetErrorEnum(error):
if isinstance(error, cuda.CUresult):
err, name = cuda.cuGetErrorName(error)
return name if err == cuda.CUresult.CUDA_SUCCESS else "<unknown>"
elif isinstance(error, nvrtc.nvrtcResult):
return nvrtc.nvrtcGetErrorString(error)[1]
else:
raise DSLRuntimeError("Unknown error type: {}".format(error))
def _get_gpu_arch_info(major, minor):
"""Get GPU architecture information and compatibility details."""
gpu_arch_map = {
(7, 0): ("Volta", "sm_70", ["sm_70"]), # V100
(7, 5): ("Turing", "sm_75", ["sm_75"]), # RTX 20 Series, Quadro RTX
(8, 0): ("Ampere", "sm_80", ["sm_80"]), # A100
(8, 6): ("Ampere", "sm_86", ["sm_86", "sm_80"]), # RTX 30 Series
(8, 9): ("Ada", "sm_89", ["sm_89", "sm_86"]), # RTX 40 Series
(8, 7): ("Ampere", "sm_87", ["sm_87", "sm_86", "sm_80"]), # A10, A40
(9, 0): ("Hopper", "sm_90a", ["sm_90a"]), # H100
(10, 0): ("Blackwell", "sm_100a", ["sm_100a"]), # B200
}
return gpu_arch_map.get(
(major, minor), ("Unknown", f"sm_{major}{minor}", [f"sm_{major}{minor}"])
)
def get_compute_capability_major_minor(device_id: int = 0):
"""
Returns the compute capability of the CUDA device as a tuple of (major, minor).
For example: (8, 0) for Ampere, (9, 0) for Hopper, (10, 0) for Blackwell.
Returns None on failure.
"""
try:
checkCudaErrors(cuda.cuInit(0))
device = checkCudaErrors(cuda.cuDeviceGet(device_id))
major = checkCudaErrors(
cuda.cuDeviceGetAttribute(
cuda.CUdevice_attribute.CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR,
device,
)
)
minor = checkCudaErrors(
cuda.cuDeviceGetAttribute(
cuda.CUdevice_attribute.CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MINOR,
device,
)
)
return major, minor
except RuntimeError as e:
_log().info(f"Failed to get CUDA compute capability: {e}")
return None, None
@dataclass
class DeviceInfo:
"""Data class to store CUDA device information."""
device_count: int = 0
current_device: int = 0
device_name: Optional[str] = None
major_version: Optional[int] = None
minor_version: Optional[int] = None
arch_name: Optional[str] = None
sm_arch: Optional[str] = None
compatible_archs: Optional[List[str]] = None
memory_gb: Optional[float] = None
target_arch: Optional[str] = None
error_message: Optional[str] = None
initialization_failed: bool = False
def pretty_str(self) -> str:
"""
Convert DeviceInfo to a formatted string for display.
"""
info = ""
if self.initialization_failed:
return f"{Colors.BOLD}- CUDA initialization failed{Colors.RESET}"
if self.error_message:
return f"{Colors.BOLD}- Failed to get GPU info: {self.error_message}{Colors.RESET}"
if self.device_count > 0:
info += f"{Colors.BOLD}- CUDA devices available: {self.device_count} (current: {self.current_device})\n"
if self.major_version is not None and self.minor_version is not None:
info += f"- Architecture: {Colors.BLUE}{self.arch_name}{Colors.RESET} ({Colors.GREEN}{self.sm_arch}{Colors.RESET})\n"
info += f"- Compatible SM archs: {Colors.GREEN}{', '.join(self.compatible_archs or [])}{Colors.RESET}\n"
if self.memory_gb is not None:
info += f"- Total Memory: {Colors.BLUE}{self.memory_gb:.2f} GB{Colors.RESET}\n"
else:
info += f"- Compute capability: unknown\n"
info += f"- SM arch: unknown{Colors.RESET}\n"
else:
info += f"- No devices available\n"
return info
def get_device_info() -> DeviceInfo:
"""
Get detailed information about CUDA devices.
Returns a DeviceInfo dataclass with device information.
"""
device_info = DeviceInfo()
# Initialize CUDA if not already initialized
try:
result = cuda.cuInit(0)
if result[0].value: # Check for error
device_info.initialization_failed = True
return device_info
except:
pass
try:
# Get device count
result = cuda.cuDeviceGetCount()
device_info.device_count = result[1] if result[0].value == 0 else 0
if device_info.device_count > 0:
# Get current device
try:
result = cuda.cuCtxGetDevice()
if result[0].value == 0:
device_info.current_device = result[1]
except:
pass
# Get device name
try:
name_result = cuda.cuDeviceGetName(100, device_info.current_device)
if name_result[0].value == 0:
device_info.device_name = name_result[1]
except:
pass
# Get compute capability and architecture info
try:
major, minor = get_compute_capability_major_minor(
device_info.current_device
)
# Check if we successfully got the compute capability
if major is not None and minor is not None:
device_info.major_version = major
device_info.minor_version = minor
arch_name, sm_arch, compatible_archs = _get_gpu_arch_info(
device_info.major_version, device_info.minor_version
)
device_info.arch_name = arch_name
device_info.sm_arch = sm_arch
device_info.compatible_archs = compatible_archs
# Get memory info
try:
total_mem = cuda.cuDeviceGetAttribute(
cuda.CUdevice_attribute.CU_DEVICE_ATTRIBUTE_TOTAL_MEMORY,
device_info.current_device,
)
if total_mem[0].value == 0:
device_info.memory_gb = total_mem[1] / (
1024 * 1024 * 1024
) # Convert to GB
except:
pass
except Exception as e:
pass # Compute capability info will remain None
except Exception as e:
device_info.error_message = str(e)
return device_info
def checkCudaErrors(result):
"""Check CUDA errors and provide detailed error messages."""
if result[0].value:
error_code = result[0].value
error_name = _cudaGetErrorEnum(result[0])
raise DSLCudaRuntimeError(error_code, error_name)
if len(result) == 1:
return None
elif len(result) == 2:
return result[1]
else:
return result[1:]
# =============================================================================
# Driver Helpers
# =============================================================================
@lru_cache(maxsize=1)
def initialize_cuda_context(device_id: int = 0, flags: int = 0):
"""
Initializes the CUDA context for a specified device.
"""
# Initialize CUDA Driver API
_log().info(f"cuInit {flags}")
checkCudaErrors(cuda.cuInit(flags))
# Retrieve handle for device
_log().info(f"cuDeviceGet {device_id}")
cuDevice = checkCudaErrors(cuda.cuDeviceGet(device_id))
_log().info(f"{cuDevice} <-- cuDeviceGet")
# Create context
_log().info(f"cuCtxCreate {0} {cuDevice}")
context = checkCudaErrors(cuda.cuCtxCreate(0, cuDevice))
_log().info(f"{context} <-- cuCtxCreate")
return context
def load_cubin_module(cubin_file):
"""
Loads a CUBIN file and returns the module.
"""
# Load CUBIN file as binary data
_log().info(f"read cubin {cubin_file}")
with open(cubin_file, "rb") as f:
cubin_data = f.read()
# Load module data
_log().info(f"cuModuleLoadData {np.char.array(cubin_data).ctypes.data}")
module = checkCudaErrors(
cuda.cuModuleLoadData(np.char.array(cubin_data).ctypes.data)
)
return module
def unload_cubin_module(module):
"""
Unloads a CUBIN module.
"""
_log().info(f"cuModuleUnload {module}")
checkCudaErrors(cuda.cuModuleUnload(module))
def load_cubin_module_data(cubin_data):
"""
Loads a CUBIN from data and returns the module.
"""
# Load module data
_log().info(f"cuModuleLoadData {np.char.array(cubin_data).ctypes.data}")
module = checkCudaErrors(
cuda.cuModuleLoadData(np.char.array(cubin_data).ctypes.data)
)
return module
def get_kernel_function(module, kernel_name):
"""
Retrieves the kernel function from the module.
"""
_log().info(f"cuModuleGetFunction {module} {kernel_name}")
kernel = checkCudaErrors(
cuda.cuModuleGetFunction(module, bytes(kernel_name, "utf-8"))
)
_log().info(f"{kernel} <-- cuModuleGetFunction")
return kernel
def launch_kernel(kernel, grid_dims, block_dims, stream, smem_size, kernel_args=None):
"""
Launches the CUDA kernel.
"""
_log().info(
f"cuLaunchKernel {kernel} grid={grid_dims} blocks={block_dims} smem_size={smem_size} stream={stream} {kernel_args}"
)
checkCudaErrors(
cuda.cuLaunchKernel(
kernel,
grid_dims[0],
grid_dims[1],
grid_dims[2],
block_dims[0],
block_dims[1],
block_dims[2],
smem_size, # Shared memory size
stream,
kernel_args,
0, # Extra parameters
)
)
def stream_sync(stream):
"""
Synchronizes the CUDA stream.
"""
_log().info(f"cuStreamSynchronize {stream}")
checkCudaErrors(cuda.cuStreamSynchronize(stream))
def stream_create(id=0):
"""
Creates the CUDA stream.
"""
_log().info(f"cuStreamCreate {id}")
stream = checkCudaErrors(cuda.cuStreamCreate(id))
_log().info(f"{stream} <-- cuStreamCreate")
return stream
def stream_destroy(stream):
"""
Destroys the CUDA stream.
"""
_log().info(f"cuStreamDestroy {stream}")
checkCudaErrors(cuda.cuStreamDestroy(stream))
def context_destroy(context):
"""
Destroys the CUDA context.
"""
_log().info(f"cuCtxDestroy {context}")
checkCudaErrors(cuda.cuCtxDestroy(context))
def allocate(size_in_bytes: int, stream=None):
"""
Allocate device memory based on numpy host array size.
"""
_log().info("Allocate size_in_bytes=[%s] stream=[%s]", size_in_bytes, stream)
if stream is None:
device_memory = checkCudaErrors(cuda.cuMemAlloc(size_in_bytes))
else:
device_memory = checkCudaErrors(cuda.cuMemAllocAsync(size_in_bytes, stream))
_log().info("Allocated [%s]", device_memory)
return device_memory
def deallocate(device_pointer, stream=None):
"""
Deallocate the specified device memory pointer.
"""
_log().info(
"Deallocate device_pointer=[%s] stream=[%s]", hex(int(device_pointer)), stream
)
if stream is None:
checkCudaErrors(cuda.cuMemFree(device_pointer))
else:
checkCudaErrors(cuda.cuMemFreeAsync(device_pointer, stream))
def memcpy_h2d(host_pointer, device_pointer, size_in_bytes, stream=None):
"""
Copy data from host to device memory.
"""
_log().info(
"Copy host-to-device host_pointer[%s] device_ptr=[%s] size_in_bytes=[%s] stream=[%s]",
hex(host_pointer),
hex(int(device_pointer)),
size_in_bytes,
stream,
)
if stream is None:
checkCudaErrors(cuda.cuMemcpyHtoD(device_pointer, host_pointer, size_in_bytes))
else:
checkCudaErrors(
cuda.cuMemcpyHtoDAsync(device_pointer, host_pointer, size_in_bytes, stream)
)
def memcpy_d2h(host_pointer, device_pointer, size_in_bytes, stream=None):
"""
Copy data from device to host memory.
"""
_log().info(
"Copy device-host-to device_pointer=[%s] host_pointer[%s] size_in_bytes=[%s] stream=[%s]",
hex(int(device_pointer)),
hex(host_pointer),
size_in_bytes,
stream,
)
if stream is None:
checkCudaErrors(cuda.cuMemcpyDtoH(host_pointer, device_pointer, size_in_bytes))
else:
checkCudaErrors(
cuda.cuMemcpyDtoHAsync(host_pointer, device_pointer, size_in_bytes, stream)
)
def default_stream():
return cuda.CUstream(0)
def get_driver_version():
"""
Returns the CUDA driver version.
"""
return checkCudaErrors(cuda.cuDriverGetVersion())
def set_kernel_attribute(kernel, attribute, value):
"""
Sets a CUDA kernel attribute.
"""
return checkCudaErrors(cuda.cuFuncSetAttribute(kernel, attribute, value))
@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,121 @@
# 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 copy
from . import cuda as cuda_helpers
from .tensor_descriptor import *
from ..common import *
def allocate(tensor: TensorDescriptor, stream=None):
"""
Allocates GPU memory
"""
if tensor._check_is_managed_by_framework():
raise DSLRuntimeError(
"GPU tensors are managed by the framework and cannot be modified."
)
if not tensor.device_pointer is None:
raise DSLRuntimeError("Tensor is already allocated on the device.")
tensor.device_pointer = cuda_helpers.allocate(tensor.size_in_bytes, stream)
log().info("Allocate done tensor=[%s] dev_ptr=[%s]", tensor, tensor.device_pointer)
def deallocate(tensor: TensorDescriptor, stream=None):
"""
Deallocates GPU memory
"""
if tensor._check_is_managed_by_framework():
raise DSLRuntimeError(
"GPU tensors are managed by the framework and cannot be modified."
)
if tensor.device_pointer is None:
raise DSLRuntimeError("Tensor is not allocated on the device.")
log().info(
"Deallocating done tensor=[%s] dev_ptr=[%s]", tensor, tensor.device_pointer
)
cuda_helpers.deallocate(tensor.device_pointer, stream)
tensor.device_pointer = None
def copy_to_gpu(tensor: TensorDescriptor, do_allocate=True, stream=None):
"""
Copies data from host memory to the GPU memory.
If do_allocate is True, it first calls allocate
"""
log().info("copyin tensor=[%s] dev_ptr=[%s]", tensor, tensor.device_pointer)
if do_allocate:
allocate(tensor, stream)
cuda_helpers.memcpy_h2d(
tensor.data_ptr, tensor.device_pointer, tensor.size_in_bytes, stream
)
log().info("copyin done tensor=[%s] dev_ptr=[%s]", tensor, tensor.device_pointer)
return tensor
def copy_from_gpu(tensor: TensorDescriptor, do_deallocate=True, stream=None):
"""
Copies data from GPU memory back to the host.
If do_deallocate is True, it calls deallocate
"""
log().info("copyout tensor=[%s] dev_ptr=[%s]", tensor, tensor.device_pointer)
if tensor._check_is_managed_by_framework():
raise DSLRuntimeError(
"GPU tensors are managed by the framework and cannot be modified."
)
if tensor.device_pointer is None:
raise DSLRuntimeError("Tensor is not allocated on the device.")
cuda_helpers.memcpy_d2h(
tensor.data_ptr, tensor.device_pointer, tensor.size_in_bytes, stream
)
if do_deallocate:
deallocate(tensor, stream)
log().info("copyout done tensor=[%s] dev_ptr=[%s]", tensor, tensor.device_pointer)
def to_gpu(tensor, stream=None) -> TensorDescriptor:
"""
Copies the tensor to the GPU memory from Host memory
"""
if isinstance(tensor, TensorDescriptor):
new_tensor = copy.copy(tensor)
copy_to_gpu(new_tensor, stream=stream)
return new_tensor
if TensorDescriptor.can_transformed_to_dlpack(tensor):
new_tensor = TensorDescriptor(tensor)
copy_to_gpu(new_tensor, stream=stream)
return new_tensor
raise DSLRuntimeError("Unsupported type")
def from_gpu(tensor, stream=None) -> TensorDescriptor:
"""
Copies the tensor to the GPU memory from Host memory
"""
if isinstance(tensor, TensorDescriptor):
new_tensor = copy.copy(tensor)
copy_from_gpu(new_tensor, stream=stream)
return new_tensor
if TensorDescriptor.can_transformed_to_dlpack(tensor):
new_tensor = TensorDescriptor(tensor)
copy_from_gpu(new_tensor, stream=stream)
return new_tensor
raise DSLRuntimeError("Unsupported type")
@@ -0,0 +1,76 @@
# 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 helper structs for dlpack.
DLPack is an open standard for in-memory tensor structures, enabling
seamless sharing of tensors across different frameworks.
Learn more at: https://github.com/dmlc/dlpack
"""
import ctypes
import enum
class DLDeviceType(enum.IntEnum):
"""Enums for device types based on the DLPack specification."""
kDLCPU = 1
kDLGPU = 2
kDLCPUPinned = 3
class DLDataTypeCode:
"""Enums for data type codes based on the DLPack specification.
see https://github.com/dmlc/dlpack/blob/main/include/dlpack/dlpack.h
"""
kDLInt = 0
kDLUInt = 1
kDLFloat = 2
kDLOpaqueHandle = 3
kDLBfloat = 4
kDLComplex = 5
kDLBool = 6
class DLDevice(ctypes.Structure):
"""Structure representing the device information in DLPack."""
_fields_ = [
("device_type", ctypes.c_int), # kDLCPU, kDLGPU, etc.
("device_id", ctypes.c_int), # Device ID (e.g., GPU ID)
]
class DLDataType(ctypes.Structure):
"""Structure representing the data type in DLPack."""
_fields_ = [
("code", ctypes.c_uint8), # Data type code (e.g., kDLFloat)
("bits", ctypes.c_uint8), # Number of bits per value
("lanes", ctypes.c_uint16), # Number of lanes
]
class DLTensor(ctypes.Structure):
"""Structure representing the DLTensor in DLPack."""
_fields_ = [
("data", ctypes.c_void_p), # Pointer to tensor data
("device", DLDevice), # Device info
("ndim", ctypes.c_int), # Number of dimensions
("dtype", DLDataType), # Data type
("shape", ctypes.POINTER(ctypes.c_int64)), # Shape of tensor
("strides", ctypes.POINTER(ctypes.c_int64)), # Strides of tensor
("byte_offset", ctypes.c_uint64), # Byte offset to tensor data
]
@@ -0,0 +1,188 @@
# 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 runtime utilities for JIT argument conversion in DSL.
"""
from functools import wraps
from typing import get_origin
# Local modules imports
from ..common import DSLRuntimeError
from ..typing import (
Constexpr,
Int32,
Float32,
Boolean,
)
def is_arg_spec_constexpr(arg_spec, arg_name, arg_index, owning_func):
"""
Check if the argument spec is a constexpr.
"""
def _is_reserved_python_func_arg(arg_index, arg_name, func):
"""
Check if the argument is a reserved python function argument.
"""
if arg_index != 0:
return False
if arg_name == "self":
return True
is_classmethod = isinstance(func, classmethod) or (
hasattr(func, "__func__") and isinstance(func.__func__, classmethod)
)
return arg_name == "cls" and is_classmethod
return (
_is_reserved_python_func_arg(arg_index, arg_name, owning_func)
or (isinstance(arg_spec, type) and issubclass(arg_spec, Constexpr))
or (get_origin(arg_spec) is Constexpr)
)
def is_argument_constexpr(arg, arg_spec, arg_name, arg_index, owning_func):
"""
Check if the argument is a constexpr.
"""
def _is_type_argument(arg, arg_annotation):
"""
Check if the argument is a type argument like Type[X]
"""
return isinstance(arg, type) and (
arg_annotation is None or get_origin(arg_annotation) is type
)
return (
is_arg_spec_constexpr(arg_spec, arg_name, arg_index, owning_func)
or _is_type_argument(arg, arg_spec)
or arg is None
)
class JitArgAdapterRegistry:
"""
A registry to keep track of the JIT argument adapters.
An adapter is a callable that converts a Python type to a type with following protocols supported:
- JitArgument
- DynamicExpression
The converted type can then be further processed by DSL to generate arguments for JIT functions.
"""
# A dictionary with key=type and value=callable
jit_arg_adapter_registry = {}
@classmethod
def register_jit_arg_adapter(cls, *dargs, **dkwargs):
"""
Register a JIT argument adapter callable
This can be used as a decorator on any callable like:
@register_jit_arg_adapter(my_py_type)
def my_adapter_for_my_py_type(arg):
...
@register_jit_arg_adapter(my_py_type)
class MyAdapterForMyPythonType:
...
The adapters are registered per type. If a type is already registerd, an error will be raised.
"""
def decorator(*dargs, **dkwargs):
darg_python_ty = dargs[0]
@wraps(darg_python_ty)
def wrapper(*args, **kwargs):
if len(args) != 1 or not callable(args[0]):
raise DSLRuntimeError(
"a callable must be provided for registering JIT argument adapter"
)
adapter = args[0]
if darg_python_ty in cls.jit_arg_adapter_registry:
raise DSLRuntimeError(
f"JIT argument adapter for {darg_python_ty} is already registered!",
context={
"Registered adapter": cls.jit_arg_adapter_registry[
darg_python_ty
],
"Adapter to be registered": adapter,
},
)
cls.jit_arg_adapter_registry[darg_python_ty] = adapter
return adapter
return wrapper
if len(dargs) > 0:
return decorator(*dargs, **dkwargs)
else:
raise DSLRuntimeError(
"a Python type must be provided for registering JIT argument adapter"
)
@classmethod
def get_registered_adapter(cls, ty):
"""
Get the registered JIT argument adapter for the given type.
"""
return cls.jit_arg_adapter_registry.get(ty, None)
# =============================================================================
# JIT Argument Adapters
# =============================================================================
@JitArgAdapterRegistry.register_jit_arg_adapter(int)
@JitArgAdapterRegistry.register_jit_arg_adapter(float)
@JitArgAdapterRegistry.register_jit_arg_adapter(bool)
def _convert_python_scalar(arg):
"""
Convert a Python scalar to a DSL type.
"""
conversion_map = {
int: Int32,
float: Float32,
bool: Boolean,
}
return conversion_map.get(type(arg))(arg)
@JitArgAdapterRegistry.register_jit_arg_adapter(tuple)
@JitArgAdapterRegistry.register_jit_arg_adapter(list)
def _convert_python_sequence(arg):
"""
Go through each element in the sequence and convert it to a type that can be
further processed by DSL to generate the corresponding JIT argument(s).
"""
adapted_arg = []
for elem in arg:
adapter = JitArgAdapterRegistry.get_registered_adapter(type(elem))
if adapter is not None:
converted_elem = adapter(elem)
adapted_arg.append(converted_elem)
else:
# If no registered adapter is found, just return the original element
adapted_arg.append(elem)
assert len(adapted_arg) == len(arg)
return type(arg)(adapted_arg)
@@ -0,0 +1,201 @@
# 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.
# Helpers
import itertools, operator
import ctypes
from . import dlpack_types as _dpack
from .dlpack_runtime import (
dlpack_to_tensor_desc,
get_tensor_desc_data_ptr,
get_tensor_desc_is_in_device,
get_tensor_desc_element_type,
get_tensor_desc_shape,
get_tensor_desc_stride,
get_tensor_desc_element_size_in_bytes,
get_tensor_desc_ndim,
get_tensor_desc_dtype_code,
get_tensor_desc_dtype_bits,
get_tensor_desc_device_type,
get_tensor_desc_device_id,
)
from ..utils.logger import log
from ..common import *
from ..typing import (
Boolean,
Float8E5M2,
Int64,
Int32,
Int16,
Int8,
Uint64,
Uint32,
Uint16,
Uint8,
Float64,
Float32,
Float16,
BFloat16,
)
class TensorDescriptor:
def __init__(self, tensor):
"""Initialize with a tensor that supports the DLPack protocol.
Args:
tensor: Any tensor object that implements __dlpack__ and __dlpack_device__
"""
self.tensor = tensor
self._capsule = dlpack_to_tensor_desc(tensor)
self.data_ptr = get_tensor_desc_data_ptr(self._capsule)
self.device_type = get_tensor_desc_device_type(self._capsule)
self.device_type = _dpack.DLDeviceType(self.device_type)
if self.device_type == _dpack.DLDeviceType.kDLGPU:
self.device_pointer = self.data_ptr
elif self.device_type == _dpack.DLDeviceType.kDLCPU:
self.device_pointer = None
else:
raise DSLRuntimeError(
f"DLPack device type is not supported {self.dl_tensor.device.device_type}"
)
log().info("TensorDescriptor is created = [%s]", self)
@staticmethod
def can_transformed_to_dlpack(dl_tensor):
if not hasattr(dl_tensor, "__dlpack__") or not hasattr(
dl_tensor, "__dlpack_device__"
):
return False
return True
@property
def is_in_device(self):
"""Check if the tensor is stored on a device."""
return not self.device_pointer is None
@property
def device_id(self):
"""Return device id where tensor resides."""
if self.is_in_device:
return get_tensor_desc_device_id(self._capsule)
return -1
@property
def element_type(self):
"""Return the corresponding Python type based on DLPack dtype metadata."""
str_element_type = get_tensor_desc_element_type(self._capsule)
dtype_map = {
# bool is 8bit from numpy and torch
"Bool": Boolean,
"Int64": Int64,
"Int32": Int32,
"Int16": Int16,
"Int8": Int8,
"UInt64": Uint64,
"UInt32": Uint32,
"UInt16": Uint16,
"UInt8": Uint8,
"Float64": Float64,
"Float32": Float32,
"Float16": Float16,
"BFloat16": BFloat16,
"Float8E5M2": Float8E5M2,
}
if str_element_type not in dtype_map:
raise KeyError(
f"Unsupported element type in dlpack: '{str_element_type}'. Supported types are: {list(dtype_map.keys())}"
)
return dtype_map[str_element_type]
@property
def shape(self):
"""Return the shape of the tensor."""
return get_tensor_desc_shape(self._capsule)
@property
def rank(self):
"""Return the rank of the tensor."""
return get_tensor_desc_ndim(self._capsule)
@property
def strides(self):
"""Return the rank of the tensor."""
return get_tensor_desc_stride(self._capsule)
@property
def element_size_in_bytes(self):
"""Calculate the element size in bytes of the DLPack tensor."""
return get_tensor_desc_element_size_in_bytes(self._capsule)
@property
def size_in_bytes(self):
"""Calculate the total size in bytes of the DLPack tensor."""
# Calculate the number of elements using the shape
ndim = get_tensor_desc_ndim(self._capsule)
shape = get_tensor_desc_shape(self._capsule)
num_elements = 1
for i in range(ndim):
num_elements *= shape[i]
# Total bytes
total_bytes = self.element_size_in_bytes * num_elements
return total_bytes
def __str__(self):
"""Return a compact string representation of the device_tensor with a tensor prefix."""
# Extract shape
shape = "x".join(map(str, self.shape))
# Extract dtype
dtype_code = get_tensor_desc_dtype_code(self._capsule)
dtype_bits = get_tensor_desc_dtype_bits(self._capsule)
dtype = (
f"i{dtype_bits}"
if dtype_code == _dpack.DLDataTypeCode.kDLInt
else f"f{dtype_bits}"
)
# Extract device
device_type = "cpu" if not self.is_in_device else "gpu"
return f"tensor<{shape}x{dtype}>_{device_type}"
def _check_is_managed_by_framework(self):
"""
Ensure the tensor is not managed by the framework (e.g., GPU tensor).
Raises an exception if the tensor is framework-managed.
"""
return self.device_type == _dpack.DLDeviceType.kDLGPU
@staticmethod
def is_compatible(maybe_tensor_descriptor) -> bool:
"""Check if the object is a TensorDescriptor or can be converted to one."""
return isinstance(
maybe_tensor_descriptor, TensorDescriptor
) or TensorDescriptor.can_transformed_to_dlpack(maybe_tensor_descriptor)
def from_tensor(tensor) -> TensorDescriptor:
"""Create a TensorDescriptor from a tensor object."""
return TensorDescriptor(tensor)
def to_tensor(tensor_descriptor: TensorDescriptor):
"""Return tensor object from tensor descriptor."""
return tensor_descriptor.tensor
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,19 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from . import stacktrace
from . import logger
from . import timer
__all__ = [
"logger",
"timer",
"stacktrace",
]
@@ -0,0 +1,80 @@
# 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 logging helper functions
"""
import logging
logger = None
def log():
return logger
def setup_log(
name, log_to_console=False, log_to_file=False, log_file_path=None, log_level=1
):
"""Set up and configure a logger with console and/or file handlers.
:param name: Name of the logger to create
:type name: str
:param log_to_console: Whether to enable logging to console, defaults to False
:type log_to_console: bool, optional
:param log_to_file: Whether to enable logging to file, defaults to False
:type log_to_file: bool, optional
:param log_file_path: Path to the log file, required if log_to_file is True
:type log_file_path: str, optional
:param log_level: Logging level to set, defaults to 1
:type log_level: int, optional
:raises ValueError: If log_to_file is True but log_file_path is not provided
:return: Configured logger instance
:rtype: logging.Logger
"""
# Create a custom logger
global logger
logger = logging.getLogger(name)
if log_to_console or log_to_file:
logger.setLevel(log_level)
else:
logger.setLevel(logging.NOTSET)
# Clear existing handlers to prevent duplicate logs
if logger.hasHandlers():
logger.handlers.clear()
# Define formatter
formatter = logging.Formatter(
f"%(asctime)s - %(name)s - %(levelname)s - [%(funcName)s] - %(message)s"
)
# Add console handler if enabled
if log_to_console:
console_handler = logging.StreamHandler()
console_handler.setLevel(log_level)
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
# Add file handler if enabled
if log_to_file:
if not log_file_path:
raise ValueError("log_file_path must be provided when enable_file is True")
file_handler = logging.FileHandler(log_file_path)
file_handler.setLevel(log_level)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
return logger
logger = setup_log("generic")
@@ -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.
"""
This module provides stacktrace helper functions
"""
import os
import re
def walk_to_top_module(start_path):
"""
Walk up from the start_path to find the top-level Python module.
:param start_path: The path to start from.
:return: The path of the top-level module.
"""
current_path = start_path
while True:
# Check if we are at the root directory
if os.path.dirname(current_path) == current_path:
break
# Check for __init__.py
init_file_path = os.path.join(current_path, "__init__.py")
if os.path.isfile(init_file_path):
# If __init__.py exists, move up one level
current_path = os.path.dirname(current_path)
else:
# If no __init__.py, we are not in a module; stop
break
# If we reached the root without finding a module, return None
if os.path.dirname(current_path) == current_path and not os.path.isfile(
os.path.join(current_path, "__init__.py")
):
return None
# Return the path of the top-level module
return current_path
def _filter_internal_frames(traceback, internal_path):
"""
Filter out stack frames from the traceback that belong to the specified module path.
This function removes stack frames from the traceback whose file paths start with
the given prefix_path, effectively hiding internal implementation details from
the error traceback shown to users.
"""
iter_prev = None
iter_tb = traceback
while iter_tb is not None:
if os.path.abspath(iter_tb.tb_frame.f_code.co_filename).startswith(
internal_path
):
if iter_tb.tb_next:
if iter_prev:
iter_prev.tb_next = iter_tb.tb_next
else:
traceback = iter_tb.tb_next
else:
iter_prev = iter_tb
iter_tb = iter_tb.tb_next
return traceback
_generated_function_names = re.compile(
r"^(loop_body|while_region|while_before_block|while_after_block|if_region|then_block|else_block|elif_region)_\d+$"
)
def _filter_duplicated_frames(traceback):
"""
Filter out duplicated stack frames from the traceback.
The function filters out consecutive frames that are in the same file and have the same line number.
In a sequence of consecutive frames, the logic prefers to keep the non-generated frame or the last frame.
"""
iter_prev = None
iter_tb = traceback
while iter_tb is not None:
skip_current = False
skip_next = False
if iter_tb.tb_next:
current_filename = os.path.abspath(iter_tb.tb_frame.f_code.co_filename)
next_filename = os.path.abspath(iter_tb.tb_next.tb_frame.f_code.co_filename)
# if in the same file, check if the line number is the same
if current_filename == next_filename:
current_lineno = iter_tb.tb_lineno
next_lineno = iter_tb.tb_next.tb_lineno
if current_lineno == next_lineno:
# Same file and line number, check name, if current is generated, skip current, otherwise skip next
name = iter_tb.tb_frame.f_code.co_name
is_generated = bool(_generated_function_names.match(name))
if is_generated:
# Skip current
skip_current = True
else:
# Skip next if it's generated, otherwise keep both
next_name = iter_tb.tb_next.tb_frame.f_code.co_name
skip_next = bool(_generated_function_names.match(next_name))
if skip_current:
if iter_prev:
iter_prev.tb_next = iter_tb.tb_next
else:
traceback = iter_tb.tb_next
elif skip_next:
# if next is last frame, don't skip
if iter_tb.tb_next.tb_next:
iter_tb.tb_next = iter_tb.tb_next.tb_next
iter_prev = iter_tb
else:
iter_prev = iter_tb
iter_tb = iter_tb.tb_next
return traceback
def filter_stackframe(traceback, prefix_path):
"""
Filter out stack frames from the traceback that belong to the specified module path.
This function removes stack frames from the traceback whose file paths start with
the given prefix_path, effectively hiding internal implementation details from
the error traceback shown to users.
:param traceback: The traceback object to filter.
:param prefix_path: The path prefix to filter out from the traceback.
:return: The filtered traceback with internal frames removed.
"""
# Step 1: filter internal frames
traceback = _filter_internal_frames(traceback, prefix_path)
# Step 2: consolidate duplicated frames
return _filter_duplicated_frames(traceback)
def filter_exception(value, module_dir):
"""
Filter out internal implementation details from exception traceback.
This function recursively processes an exception and its cause chain,
removing stack frames that belong to the specified module directory.
This helps to present cleaner error messages to users by hiding
implementation details.
:param value: The exception object to filter.
:param module_dir: The module directory path to filter out from tracebacks.
:return: The filtered exception with internal frames removed.
"""
if hasattr(value, "__cause__") and value.__cause__:
filter_exception(value.__cause__, module_dir)
if hasattr(value, "__traceback__"):
filter_stackframe(value.__traceback__, module_dir)
@@ -0,0 +1,56 @@
# 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 a timing helper functions
"""
from functools import wraps
from .logger import log
# TODO: revisit this part when mlir timing manager is ready for pybind.
def timer(*dargs, **kwargs):
enable = kwargs.get("enable", True)
def decorator(func):
@wraps(func)
def func_wrapper(*args, **kwargs):
if not enable:
return func(*args, **kwargs)
from time import time
start = time()
result = func(*args, **kwargs)
end = time()
# Convert time from seconds to us
spend_us = (end - start) * 1e6
# Determine the function type and format the log message
if hasattr(func, "__name__"):
func_name = func.__name__
log_message = f"[JIT-TIMER] Function: {func_name} | Execution Time: {spend_us:.2f} µs"
elif "CFunctionType" in str(type(func)):
log_message = f"[JIT-TIMER] C API Function: {str(func)} | Execution Time: {spend_us:.2f} µs"
else:
log_message = f"[JIT-TIMER] Anonymous Function | Execution Time: {spend_us:.2f} µs"
log().info(log_message)
return result
return func_wrapper
if len(dargs) == 1 and callable(dargs[0]):
return decorator(dargs[0])
else:
return decorator
@@ -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.
from .cutlass import *
from ..base_dsl.ast_helpers import (
loop_selector,
if_selector,
if_executor,
while_selector,
while_executor,
range,
range_constexpr,
range_dynamic,
const_expr,
dynamic_expr,
assert_executor,
bool_cast,
compare_executor,
any_executor,
all_executor,
range_value_check,
range_perf_warning,
cf_symbol_check,
redirect_builtin_function,
)
from ..base_dsl import *
from ..base_dsl.dsl import extract_mlir_values, new_from_mlir_values
from ..base_dsl.typing import _binary_op_type_promote
from ..base_dsl._mlir_helpers.gpu import *
from ..base_dsl._mlir_helpers.op import dsl_user_op
from ..base_dsl.runtime import *
from ..base_dsl.runtime import cuda as cuda_helpers
from ..base_dsl.compiler import compile
from ..base_dsl.runtime.jit_arg_adapters import *
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,633 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
#
# Use of this software is governed by the terms and conditions of the
# NVIDIA End User License Agreement (EULA), available at:
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
#
# Any use, reproduction, disclosure, or distribution of this software
# and related documentation outside the scope permitted by the EULA
# is strictly prohibited.
from typing import List, Tuple
from types import NoneType
from cutlass._mlir import ir
from cutlass._mlir.dialects import scf, arith
from cutlass._mlir.extras import types as T
from collections.abc import Sequence
from ..base_dsl.dsl import is_dynamic_expression
from ..base_dsl.ast_helpers import *
from ..base_dsl.utils.logger import log
from ..base_dsl import typing as t
from ..base_dsl.typing import (
Int32,
Float32,
Boolean,
Numeric,
get_mlir_types,
as_numeric,
)
from . import cutlass as cutlass_dsl
from .tree_utils import PyTreeDef, check_tree_equal
# =============================================================================
# AST Helpers
# =============================================================================
class LoopUnroll(ir.Attribute):
def __init__(self, **kwargs):
valid_keys = set(["count", "full"])
def to_mlir_attr(val):
if isinstance(val, bool):
return "true" if val else "false"
elif isinstance(val, int):
return f"{val} : i32"
else:
raise DSLNotImplemented(f"{type(val)} is not supported")
cfg = {key: to_mlir_attr(kwargs[key]) for key in valid_keys if key in kwargs}
if kwargs.get("count", None) == 1:
cfg["disable"] = "true"
unroll = "<" + ", ".join(f"{key} = {value}" for key, value in cfg.items()) + ">"
super().__init__(
ir.Attribute.parse(f"#llvm.loop_annotation<unroll = {unroll}>")
)
class ScfGenerator:
"""
Encapsulates common scf dialect functionality: pack, unpack, and SCF execution.
"""
def __init__(self):
pass
@staticmethod
def _normalize_region_result_to_list(region_result: Any) -> List[Any]:
"""
Convert region_result to a list if it is not already a list
If region_result is a list, return it as is.
If region_result is None, return an empty list.
If region_result is not a list, return a list containing region_result as the only element.
"""
if region_result is None:
region_result_list = []
elif not isinstance(region_result, list):
region_result_list = [region_result]
else:
region_result_list = region_result
return region_result_list
@staticmethod
def _check_region_result(original_value, region_value, arg_name, op_type_name):
"""
Validate that a region result maintains the same type as the original value.
This method checks for type consistency between the original value passed to a dynamic
SCF operation (like for, if, while) and the value returned from the operation's region.
Args:
original_value: The value before entering the SCF operation region
region_value: The value returned from the SCF operation region
arg_name: Name of the argument being checked (for error reporting)
op_type_name: Type of SCF operation (e.g., 'for', 'if', 'while') for error reporting
Raises:
DSLRuntimeError: If the region value has a different type than the original value.
The error includes suggestions for using compile-time control flow instead.
Note:
This method performs relaxed type checking that allows inheritance relationships.
For example, a child class can be returned where a parent class was expected.
However, fundamental type changes (like None to non-None, different sequence types,
or different numeric types) are not allowed in dynamic SCF operations.
"""
def get_type_name(value):
if isinstance(value, NoneType):
return "None"
elif isinstance(value, Sequence):
return f"{type(value).__name__}<{len(value)}>"
else:
return type(value).__name__
# Check for type mismatches
type_mismatch = False
old_type_name = None
new_type_name = None
# Handle None type changes
if isinstance(original_value, NoneType) != isinstance(region_value, NoneType):
type_mismatch = True
old_type_name = get_type_name(original_value)
new_type_name = get_type_name(region_value)
# Handle sequence type/length changes
elif isinstance(original_value, Sequence) and isinstance(
region_value, Sequence
):
if type(original_value) != type(region_value) or len(original_value) != len(
region_value
):
type_mismatch = True
old_type_name = get_type_name(original_value)
new_type_name = get_type_name(region_value)
# Handle numeric type changes
elif isinstance(
original_value, (Numeric, ArithValue, ir.Value, int, float, bool)
) or isinstance(
region_value, (Numeric, ArithValue, ir.Value, int, float, bool)
):
try:
original_numeric = as_numeric(original_value)
region_numeric = as_numeric(region_value)
if original_numeric.dtype != region_numeric.dtype:
type_mismatch = True
old_type_name = original_numeric.dtype.__name__
new_type_name = region_numeric.dtype.__name__
except Exception:
pass
# Handle general type changes (relaxed for inheritance)
elif type(original_value) != type(region_value):
old_type = type(original_value)
new_type = type(region_value)
if not (issubclass(old_type, new_type) or issubclass(new_type, old_type)):
type_mismatch = True
old_type_name = old_type.__name__
new_type_name = new_type.__name__
if type_mismatch:
raise DSLRuntimeError(
f"`{arg_name}` is {old_type_name} prior to this `{op_type_name}`, "
f"and update to {new_type_name} inside of this `{op_type_name}` is not supported.",
suggestion=(
f"Please avoid changing type inside a dynamic `{op_type_name}`, "
f"or change to compile-time control flow by marking this `{op_type_name}` with "
f"`{'range_constexpr' if op_type_name == 'for' else 'const_expr'}`."
),
)
def scf_execute_dynamic(
self,
op_type_name: str,
mix_iter_args: List[Any],
full_write_args_count: int,
mix_iter_arg_names: List[str],
create_op_func: Callable[[List[ir.Value]], ir.Operation],
region_builders: List[
Callable[
[
"ir.Operation",
List["ir.Value"], # block_args
List["ir.Value"], # dyn_yield_ops
PyTreeDef,
List[Any],
int,
],
Any,
]
],
# block_term_op_builder[region_builder] = scf_op_builder
# e.g. scf.ConditionOp for while loop
block_term_op_builder: Dict[Callable, Callable] = {},
) -> Any:
# 1) Unpack
ir_values, pytree_def = cutlass_dsl.unpack_to_irvalue(
mix_iter_args, op_type_name, full_write_args_count
)
# 2) Create the SCF op
op = create_op_func(ir_values)
log().debug("Generated scf.%s \n[%s]", op_type_name, op)
# 3) Build the regions
for i, builder in enumerate(region_builders):
region = op.regions[i]
block = region.blocks[0]
with ir.InsertionPoint(block):
block_args = list(block.arguments)
region_result = builder(
op,
block_args,
ir_values,
pytree_def,
mix_iter_args,
full_write_args_count,
)
# Use custom terminator if provided for this builder, otherwise use default YieldOp
if builder in block_term_op_builder:
# Use the provided terminator generator
block_term_op_builder[builder](region_result, full_write_args_count)
else:
# Normalize region_result
region_result_list = ScfGenerator._normalize_region_result_to_list(
region_result
)
# For standard yield op, check result
for arg, result, name in zip(
mix_iter_args,
region_result_list,
mix_iter_arg_names,
):
ScfGenerator._check_region_result(
arg, result, name, op_type_name
)
# Default behavior - generate YieldOp
region_values, yield_pytree_def = cutlass_dsl.unpack_to_irvalue(
region_result_list, op_type_name, full_write_args_count
)
mismatch = check_tree_equal(pytree_def, yield_pytree_def)
if mismatch != -1:
# Get arg name
filterd_arg_names = (
cutlass_dsl.filter_readonly_frozen_dataclass_names(
mix_iter_args, mix_iter_arg_names, full_write_args_count
)
)
raise DSLRuntimeError(
f"`{filterd_arg_names[mismatch]}` is structured different after this `{op_type_name}`.",
suggestion=(
f"Please avoid changing type structure inside a dynamic `{op_type_name}`, "
f"or change to compile-time control flow by marking this `{op_type_name}` with "
f"`{'range_constexpr' if op_type_name == 'for' else 'const_expr'}`."
),
)
scf.YieldOp(region_values)
log().debug("Completed scf.%s \n[%s]", op_type_name, op)
# 4) Pack final results
final_results = cutlass_dsl.pack_from_irvalue(
op.results, pytree_def, mix_iter_args, full_write_args_count
)
# 5) Return in a nice pattern
if not final_results:
return
if len(final_results) == 1:
return final_results[0]
return final_results
def _attr_const_check(attr, expected_type, attr_name):
# Use strict type equality to prevent `bool` being accepted where `int` is required.
if is_dynamic_expression(attr) or type(attr) is not expected_type:
raise DSLRuntimeError(
f"loop attribute `{attr_name}` must be a Python value of type `{expected_type.__name__}`, got `{type(attr).__name__}`."
)
def _loop_execute_range_dynamic(
func: Callable,
start: Any,
stop: Any,
step: Any,
mix_iter_args: List[Any] = [],
full_write_args_count: int = 0,
mix_iter_arg_names: List[str] = [],
unroll: int = -1,
unroll_full: bool = False,
prefetch_stages: int = None,
):
"""
Example: build an scf.for with optional unroll, using our universal helper.
"""
scf_gen = ScfGenerator()
def create_for_op(dyn_yield_ops: List[ir.Value]):
for d in dyn_yield_ops:
if not isinstance(d, ir.Value):
raise DSLRuntimeError(
f"Invalid dyn_yield_ops: {dyn_yield_ops} \n\tExpected ir.Value, got {type(d)}"
)
# Convert Python ints or values to IR constants if needed
start_ = t.as_numeric(start)
stop_ = t.as_numeric(stop)
step_ = t.as_numeric(step)
assert start_ is not t.Int32, "Start is required for scf.for"
assert stop_ is not t.Int32, "Stop is required for scf.for"
assert step_ is not t.Int32, "Step is required for scf.for"
start_ = start_.ir_value()
stop_ = stop_.ir_value()
step_ = step_.ir_value()
# Attributes must be pure Python value, add a check
_attr_const_check(unroll, int, "unroll")
_attr_const_check(unroll_full, bool, "unroll_full")
# Possibly attach unroll attributes
unroll_attr = None
if unroll_full:
unroll_attr = LoopUnroll(full=True)
elif unroll != -1:
unroll_attr = LoopUnroll(count=unroll)
log().debug("Unroll attribute: %s", unroll_attr)
prefetch_stages_attr = None
if prefetch_stages is not None:
_attr_const_check(prefetch_stages, int, "prefetch_stages")
if prefetch_stages >= 0:
prefetch_stages_attr = ir.IntegerAttr.get(
ir.IntegerType.get_signless(32), prefetch_stages
)
else:
raise DSLRuntimeError(
f"loop attribute `prefetch_stages` must be non-negative, got `{prefetch_stages}`."
)
log().debug("prefetch_stages attribute: %s", prefetch_stages_attr)
log().debug(
"Creating scf.ForOp \n\t\tstart=%s: type : %s\n\t\tstop=%s: type : %s\n\t\tstep=%s: type : %s",
start_,
type(start_),
stop_,
type(stop_),
step_,
type(step_),
)
# Create scf.ForOp, passing iteration args if any
try:
if not dyn_yield_ops:
for_op = scf.ForOp(start_, stop_, step_)
else:
for_op = scf.ForOp(start_, stop_, step_, list(dyn_yield_ops))
except Exception as e:
yield_ops = "\n".join(
f"\t\t{i} => {d} : type : {type(d)}"
for i, d in enumerate(dyn_yield_ops)
)
raise DSLRuntimeError(
f"Failed to create scf.ForOp \n\t\tstart={start_}: type : {type(start_)}"
f"\n\t\tstop={stop_}: type : {type(stop_)}\n\t\tstep={step_}: type : {type(step_)}"
f", \n\tdyn_yield_ops:\n{yield_ops}"
) from e
if unroll_attr is not None:
for_op.attributes["loop_annotation"] = unroll_attr
if prefetch_stages_attr is not None:
for_op.attributes["cutlass.pipelining"] = prefetch_stages_attr
return for_op
def for_body_builder(
op,
block_args,
_,
pytree_def,
mix_iter_args,
full_write_args_count,
):
# scf.ForOp block_args are typically [induction_var, iter_args...]
# But MLIR also gives you op.induction_variable
iv = t.as_numeric(op.induction_variable)
log().debug(
"For body builder: %s block_args: %s full_write_args_count: %s",
iv,
block_args,
full_write_args_count,
)
# block_args[1:] are iteration variables
func_args = []
func_args.extend(
cutlass_dsl.pack_from_irvalue(
block_args[1:], pytree_def, mix_iter_args, full_write_args_count
)
)
if not func_args:
# No iteration arguments, or only the induction var
func(iv)
return [] # yield nothing
else:
updated_func_args = func(iv, *func_args)
return updated_func_args
# Now call the universal SCF executor with a single region builder
return scf_gen.scf_execute_dynamic(
op_type_name="for",
mix_iter_args=mix_iter_args,
full_write_args_count=full_write_args_count,
mix_iter_arg_names=mix_iter_arg_names,
create_op_func=create_for_op,
region_builders=[for_body_builder],
)
def _if_execute_dynamic(
pred: "ir.Value",
then_block: Callable,
else_block: Callable = None,
mix_yield_args: List[Any] = [],
full_write_args_count: int = 0,
mix_yield_arg_names: List[str] = [],
if_constexpr=None, # ignoring for brevity
):
"""
Build an scf.if with optional else, using our universal helper.
"""
scf_gen = ScfGenerator()
def create_if_op(dyn_yield_ops: List[ir.Value]):
# Assume final result types match the dynamic yields
result_types = [arg.type for arg in dyn_yield_ops]
pred_ = Boolean(pred)
try:
if_op = scf.IfOp(
pred_.ir_value(),
hasElse=(else_block is not None),
results_=result_types,
)
except Exception as e:
raise DSLRuntimeError(
f"Failed to create scf.IfOp \n\t\tpred={pred_}: type : {type(pred_)}"
) from e
return if_op
def then_builder(
if_op,
_,
dyn_yield_ops,
pytree_def,
mix_iter_args,
full_write_args_count,
):
flat_args = []
flat_args.extend(
cutlass_dsl.pack_from_irvalue(
dyn_yield_ops, pytree_def, mix_iter_args, full_write_args_count
)
)
return then_block(*flat_args)
region_builders = [then_builder]
if else_block is not None:
def else_builder(
if_op,
_,
dyn_yield_ops,
pytree_def,
mix_iter_args,
full_write_args_count,
):
flat_args = []
flat_args.extend(
cutlass_dsl.pack_from_irvalue(
dyn_yield_ops, pytree_def, mix_iter_args, full_write_args_count
)
)
return else_block(*flat_args)
region_builders.append(else_builder)
return scf_gen.scf_execute_dynamic(
op_type_name="if",
mix_iter_args=mix_yield_args,
full_write_args_count=full_write_args_count,
mix_iter_arg_names=mix_yield_arg_names,
create_op_func=create_if_op,
region_builders=region_builders,
)
def _while_execute_dynamic(
while_before_block: Callable,
while_after_block: Callable = None,
write_args=[],
full_write_args_count=0,
write_args_names=[],
):
"""
Create and return an SCF WhileOp for dynamic loops.
Generate the dynamic loop body using SCF WhileOp.
Args:
while_before_block: Function that returns (condition, updated_values)
while_after_block: Function that returns updated values
write_args: Values that are updated in the loop
See create_while_function in ast_preprocessor.py for details on the input structure.
"""
log().debug("_while_execute_dynamic")
while_op_type_name = "while"
scf_gen = ScfGenerator()
def create_while_op(dyn_yield_ops: List[ir.Value]):
# Create the while operation with the types from yield_args
result_types = [arg.type for arg in dyn_yield_ops]
try:
while_op = scf.WhileOp(result_types, dyn_yield_ops)
while_op.before.blocks.append(*result_types)
while_op.after.blocks.append(*result_types)
log().debug("[%s]", while_op)
return while_op
except Exception as e:
yield_ops = "\n".join(
f"\t\t{i} => {d} : type : {type(d)}"
for i, d in enumerate(dyn_yield_ops)
)
raise DSLRuntimeError(
f"Failed to create scf.WhileOp with yield_ops:\n{yield_ops}"
) from e
def before_block_builder(
op,
block_args,
_,
pytree_def,
mix_iter_args,
full_write_args_count,
):
# Build the before (condition) block
flat_args = []
flat_args.extend(
cutlass_dsl.pack_from_irvalue(
block_args, pytree_def, mix_iter_args, full_write_args_count
)
)
log().debug("before block args: %s", flat_args)
cond, before_results = while_before_block(*flat_args)
if not isinstance(before_results, (list, ir.OpResultList)):
before_results = [before_results]
log().debug("cond [%s]", cond)
log().debug(
"before_results [%s]",
before_results,
)
return cond, before_results
def before_block_terminator(cond_and_results, full_write_args_count):
# Generate a condition op instead of yield op
cond = cond_and_results[0]
before_result_list = ScfGenerator._normalize_region_result_to_list(
cond_and_results[1]
)
ir_cond = as_numeric(cond).ir_value()
ir_results_list, pytree_def = cutlass_dsl.unpack_to_irvalue(
before_result_list, while_op_type_name, full_write_args_count
)
log().debug(
"creating scf.ConditionOp with [%s], [%s]",
ir_cond,
ir_results_list,
)
scf.ConditionOp(ir_cond, ir_results_list)
def after_block_builder(
op,
block_args,
_,
pytree_def,
mix_iter_args,
full_write_args_count,
):
# Build the after (body) block
flat_args = []
flat_args.extend(
cutlass_dsl.pack_from_irvalue(
block_args, pytree_def, mix_iter_args, full_write_args_count
)
)
log().debug("after block args: %s", flat_args)
after_results = while_after_block(*flat_args)
if not isinstance(after_results, (list, ir.OpResultList)):
after_results = [after_results]
log().debug(
"after_results [%s]",
after_results,
)
return after_results
# Call the universal SCF executor with two region builders
return scf_gen.scf_execute_dynamic(
op_type_name=while_op_type_name,
mix_iter_args=write_args,
full_write_args_count=full_write_args_count,
mix_iter_arg_names=write_args_names,
create_op_func=create_while_op,
region_builders=[before_block_builder, after_block_builder],
block_term_op_builder={
before_block_builder: before_block_terminator
}, # Only customize the before block
)
@@ -0,0 +1,763 @@
# 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, Any, Iterable, Iterator, NamedTuple, Union, get_origin
import dataclasses
import itertools as it
from types import SimpleNamespace
from ..base_dsl.typing import as_numeric, Numeric, Constexpr
from ..base_dsl._mlir_helpers.arith import ArithValue
from ..base_dsl.common import DSLBaseError
from .._mlir import ir
# =============================================================================
# Tree Utils
# =============================================================================
class DSLTreeFlattenError(DSLBaseError):
"""Exception raised when tree flattening fails due to unsupported types."""
def __init__(self, msg: str, type_str: str):
super().__init__(msg)
self.type_str = type_str
def unzip2(pairs: Iterable[tuple[Any, Any]]) -> tuple[list[Any], list[Any]]:
"""Unzip a sequence of pairs into two lists."""
lst1, lst2 = [], []
for x1, x2 in pairs:
lst1.append(x1)
lst2.append(x2)
return lst1, lst2
def get_fully_qualified_class_name(x: Any) -> str:
"""
Get the fully qualified class name of an object.
Args:
x: Any object
Returns:
str: Fully qualified class name in format 'module.class_name'
Example:
>>> get_fully_qualified_class_name([1, 2, 3])
'builtins.list'
"""
return f"{x.__class__.__module__}.{x.__class__.__qualname__}"
def is_frozen_dataclass(obj_or_cls: Any) -> bool:
"""
Check if an object or class is a frozen dataclass.
Args:
obj_or_cls: Either a dataclass instance or class
Returns:
bool: True if the object/class is a dataclass declared with frozen=True,
False otherwise
Example:
>>> from dataclasses import dataclass
>>> @dataclass(frozen=True)
... class Point:
... x: int
... y: int
>>> is_frozen_dataclass(Point)
True
>>> is_frozen_dataclass(Point(1, 2))
True
"""
cls = obj_or_cls if isinstance(obj_or_cls, type) else obj_or_cls.__class__
return (
dataclasses.is_dataclass(cls)
and getattr(cls, "__dataclass_params__", None) is not None
and cls.__dataclass_params__.frozen
)
def is_dynamic_expression(x: Any) -> bool:
"""
Check if an object implements the DynamicExpression protocol.
Objects implementing this protocol must have both `__extract_mlir_values__`
and `__new_from_mlir_values__` methods.
Args:
x: Any object to check
Returns:
bool: True if the object implements the DynamicExpression protocol,
False otherwise
"""
return all(
hasattr(x, attr)
for attr in ("__extract_mlir_values__", "__new_from_mlir_values__")
)
def is_constexpr_field(field: dataclasses.Field) -> bool:
"""
Check if a field is a constexpr field.
"""
if field.type is Constexpr:
return True
elif get_origin(field.type) is Constexpr:
return True
return False
# =============================================================================
# PyTreeDef
# =============================================================================
class NodeType(NamedTuple):
"""
Represents a node in a pytree structure.
Attributes:
name: String representation of the node type
to_iterable: Function to convert node to iterable form
from_iterable: Function to reconstruct node from iterable form
"""
name: str
to_iterable: Callable
from_iterable: Callable
class PyTreeDef(NamedTuple):
"""
Represents the structure definition of a pytree.
Attributes:
node_type: The type of this node
node_metadata: SimpleNamespace metadata associated with this node
child_treedefs: Tuple of child tree definitions
"""
node_type: NodeType
node_metadata: SimpleNamespace
child_treedefs: tuple["PyTreeDef", ...]
@dataclasses.dataclass(frozen=True)
class Leaf:
"""
Represents a leaf node in a pytree structure.
Attributes:
is_numeric: Whether this leaf contains a `Numeric` value
is_none: Whether this leaf represents None
node_metadata: SimpleNamespace metadata associated with this leaf
ir_type_str: String representation of the IR type
"""
is_numeric: bool = False
is_none: bool = False
node_metadata: SimpleNamespace = None
ir_type_str: str = None
# =============================================================================
# Default to_iterable and from_iterable
# =============================================================================
def extract_dataclass_members(x: Any) -> tuple[list[str], list[Any]]:
"""
Extract non-method, non-function attributes from a dataclass instance.
Args:
x: A dataclass instance
Returns:
tuple: (field_names, field_values) lists
"""
fields = [field.name for field in dataclasses.fields(x)]
# If the dataclass has extra fields, raise an error
for k in x.__dict__.keys():
if k not in fields:
raise DSLTreeFlattenError(
f"`{x}` has extra field `{k}`",
type_str=get_fully_qualified_class_name(x),
)
if not fields:
return [], []
# record constexpr fields
members = []
constexpr_fields = []
for field in dataclasses.fields(x):
if is_constexpr_field(field):
constexpr_fields.append(field.name)
fields.remove(field.name)
v = getattr(x, field.name)
if is_dynamic_expression(v):
raise DSLTreeFlattenError(
f"`{x}` has dynamic expression field `{field.name}` with a Constexpr type annotation `{field.type}`",
type_str=get_fully_qualified_class_name(x),
)
else:
members.append(getattr(x, field.name))
return fields, members, constexpr_fields
def default_dataclass_to_iterable(x: Any) -> tuple[SimpleNamespace, list[Any]]:
"""
Convert a dataclass instance to iterable form for tree flattening.
Extracts all non-method, non-function attributes that don't start with '__'
and returns them along with metadata about the dataclass.
Args:
x: A dataclass instance
Returns:
tuple: (metadata, members) where metadata contains type info and field names,
and members is the list of attribute values
"""
fields, members, constexpr_fields = extract_dataclass_members(x)
metadata = SimpleNamespace(
type_str=get_fully_qualified_class_name(x),
fields=fields,
constexpr_fields=constexpr_fields,
original_obj=x,
)
return metadata, members
def set_dataclass_attributes(
instance: Any,
fields: list[str],
values: Iterable[Any],
constexpr_fields: list[str],
) -> Any:
"""
Set attributes on a dataclass instance.
Args:
instance: The dataclass instance
fields: List of field names
values: Iterable of field values
is_frozen: Whether the dataclass is frozen
Returns:
The instance with attributes set
"""
if not fields:
return instance
kwargs = dict(zip(fields, values))
for field in constexpr_fields:
kwargs[field] = getattr(instance, field)
return dataclasses.replace(instance, **kwargs)
def default_dataclass_from_iterable(
metadata: SimpleNamespace, children: Iterable[Any]
) -> Any:
"""
Reconstruct a dataclass instance from iterable form.
Handles both regular and frozen dataclasses appropriately.
Args:
metadata: Metadata containing type information and field names
children: Iterable of attribute values to reconstruct the instance
Returns:
The reconstructed dataclass instance
"""
instance = metadata.original_obj
new_instance = set_dataclass_attributes(
instance, metadata.fields, children, metadata.constexpr_fields
)
metadata.original_obj = new_instance
return new_instance
def dynamic_expression_to_iterable(x: Any) -> tuple[SimpleNamespace, list[Any]]:
"""
Convert a dynamic expression to iterable form.
Uses the object's `__extract_mlir_values__` method to extract MLIR values.
Args:
x: A dynamic expression object
Returns:
tuple: (metadata, mlir_values) where metadata marks this as a dynamic expression
and mlir_values are the extracted MLIR values
"""
return (
SimpleNamespace(is_dynamic_expression=1, original_obj=x),
x.__extract_mlir_values__(),
)
def dynamic_expression_from_iterable(
metadata: SimpleNamespace, children: Iterable[Any]
) -> Any:
"""
Reconstruct a dynamic expression from iterable form.
Uses the object's `__new_from_mlir_values__` method to reconstruct from MLIR values.
Args:
metadata: Metadata containing the original object
children: Iterable of MLIR values to reconstruct from
Returns:
The reconstructed dynamic expression object
"""
return metadata.original_obj.__new_from_mlir_values__(list(children))
def default_dict_to_iterable(x: Any) -> tuple[SimpleNamespace, list[Any]]:
"""
Convert a dict to iterable form.
"""
if isinstance(x, SimpleNamespace):
keys = list(x.__dict__.keys())
values = list(x.__dict__.values())
else:
keys = list(x.keys())
values = list(x.values())
return (
SimpleNamespace(
type_str=get_fully_qualified_class_name(x), original_obj=x, fields=keys
),
values,
)
def default_dict_from_iterable(
metadata: SimpleNamespace, children: Iterable[Any]
) -> Any:
"""
Reconstruct a dict from iterable form.
"""
instance = metadata.original_obj
fields = metadata.fields
is_simple_namespace = isinstance(instance, SimpleNamespace)
for k, v in zip(fields, children):
if is_simple_namespace:
setattr(instance, k, v)
else:
instance[k] = v
return instance
# =============================================================================
# Register pytree nodes
# =============================================================================
_node_types: dict[type, NodeType] = {}
def register_pytree_node(ty: type, to_iter: Callable, from_iter: Callable) -> NodeType:
"""
Register a new node type for pytree operations.
Args:
ty: The type to register
to_iter: Function to convert instances of this type to iterable form
from_iter: Function to reconstruct instances of this type from iterable form
Returns:
NodeType: The created NodeType instance
"""
nt = NodeType(str(ty), to_iter, from_iter)
_node_types[ty] = nt
return nt
def register_default_node_types() -> None:
"""Register default node types for pytree operations."""
default_registrations = [
(
tuple,
lambda t: (SimpleNamespace(length=len(t)), list(t)),
lambda _, xs: tuple(xs),
),
(
list,
lambda l: (SimpleNamespace(length=len(l)), list(l)),
lambda _, xs: list(xs),
),
(
dict,
default_dict_to_iterable,
default_dict_from_iterable,
),
(
SimpleNamespace,
default_dict_to_iterable,
default_dict_from_iterable,
),
]
for ty, to_iter, from_iter in default_registrations:
register_pytree_node(ty, to_iter, from_iter)
# Initialize default registrations
register_default_node_types()
# =============================================================================
# tree_flatten and tree_unflatten
# =============================================================================
"""
Behavior of tree_flatten and tree_unflatten, for example:
```python
a = (1, 2, 3)
b = MyClass(a=1, b =[1,2,3])
```
yields the following tree:
```python
tree_a = PyTreeDef(type = 'tuple',
metadata = {length = 3},
children = [
Leaf(type = int),
Leaf(type = int),
Leaf(type = int),
],
)
flattened_a = [1, 2, 3]
tree_b = PyTreeDef(type = 'MyClass',
metadata = {fields = ['a','b']},
children = [
PyTreeDef(type = `list`,
metadata = {length = 3},
children = [
Leaf(type=`int`),
Leaf(type=`int`),
Leaf(type=`int`),
],
),
Leaf(type=int),
],
)
flattened_b = [1, 1, 2, 3]
```
Passing the flattened values and PyTreeDef to tree_unflatten to reconstruct the original structure.
``` python
unflattened_a = tree_unflatten(tree_a, flattened_a)
unflattened_b = tree_unflatten(tree_b, flattened_b)
```
yields the following structure:
``` python
unflattened_a = (1, 2, 3)
unflattened_b = MyClass(a=1, b =[1,2,3])
```
unflattened_a should be structurally identical to a, and unflattened_b should be structurally identical to b.
"""
def tree_flatten(x: Any) -> tuple[list[Any], PyTreeDef]:
"""
Flatten a nested structure into a flat list of values and a tree definition.
This function recursively traverses nested data structures (trees) and
flattens them into a linear list of leaf values, while preserving the
structure information in a PyTreeDef.
Args:
x: The nested structure to flatten
Returns:
tuple: (flat_values, treedef) where flat_values is a list of leaf values
and treedef is the tree structure definition
Raises:
DSLTreeFlattenError: If the structure contains unsupported types
Example:
>>> tree_flatten([1, [2, 3], 4])
([1, 2, 3, 4], PyTreeDef(...))
"""
children_iter, treedef = _tree_flatten(x)
return list(children_iter), treedef
def get_registered_node_types_or_insert(x: Any) -> NodeType | None:
"""
Get the registered node type for an object, registering it if necessary.
This function checks if a type is already registered for pytree operations.
If not, it automatically registers the type based on its characteristics:
- Dynamic expressions get registered with dynamic expression handlers
- Dataclasses get registered with default dataclass handlers
Args:
x: The object to get or register a node type for
Returns:
NodeType or None: The registered node type, or None if the type
cannot be registered
"""
node_type = _node_types.get(type(x))
if node_type:
return node_type
elif is_dynamic_expression(x):
# If a class implements DynamicExpression protocol, register it before default dataclass one
return register_pytree_node(
type(x), dynamic_expression_to_iterable, dynamic_expression_from_iterable
)
elif dataclasses.is_dataclass(x):
return register_pytree_node(
type(x), default_dataclass_to_iterable, default_dataclass_from_iterable
)
else:
return None
def create_leaf_for_value(
x: Any,
is_numeric: bool = False,
is_none: bool = False,
node_metadata: SimpleNamespace = None,
ir_type_str: str = None,
) -> Leaf:
"""
Create a Leaf node for a given value.
Args:
x: The value to create a leaf for
is_numeric: Whether this is a numeric value
is_none: Whether this represents None
node_metadata: Optional metadata
ir_type_str: Optional IR type string
Returns:
Leaf: The created leaf node
"""
return Leaf(
is_numeric=is_numeric,
is_none=is_none,
node_metadata=node_metadata,
ir_type_str=ir_type_str or (str(x.type) if hasattr(x, "type") else None),
)
def _tree_flatten(x: Any) -> tuple[Iterable[Any], PyTreeDef | Leaf]:
"""
Internal function to flatten a tree structure.
This is the core implementation of tree flattening that handles different
types of objects including None, ArithValue, ir.Value, Numeric types,
and registered pytree node types.
Args:
x: The object to flatten
Returns:
tuple: (flattened_values, treedef) where flattened_values is an iterable
of leaf values and treedef is the tree structure
Raises:
DSLTreeFlattenError: If the object type is not supported
"""
match x:
case None:
return [], create_leaf_for_value(x, is_none=True)
case ArithValue() if is_dynamic_expression(x):
v = x.__extract_mlir_values__()
return v, create_leaf_for_value(
x,
node_metadata=SimpleNamespace(is_dynamic_expression=1, original_obj=x),
ir_type_str=str(v[0].type),
)
case ArithValue():
return [x], create_leaf_for_value(x, is_numeric=True)
case ir.Value():
return [x], create_leaf_for_value(x)
case Numeric():
v = x.__extract_mlir_values__()
return v, create_leaf_for_value(
x,
node_metadata=SimpleNamespace(is_dynamic_expression=1, original_obj=x),
ir_type_str=str(v[0].type),
)
case _:
node_type = get_registered_node_types_or_insert(x)
if node_type:
node_metadata, children = node_type.to_iterable(x)
children_flat, child_trees = unzip2(map(_tree_flatten, children))
flattened = it.chain.from_iterable(children_flat)
return flattened, PyTreeDef(
node_type, node_metadata, tuple(child_trees)
)
# Try to convert to numeric
try:
nval = as_numeric(x).ir_value()
return [nval], create_leaf_for_value(nval, is_numeric=True)
except Exception:
raise DSLTreeFlattenError(
"Flatten Error", get_fully_qualified_class_name(x)
)
def tree_unflatten(treedef: PyTreeDef, xs: list[Any]) -> Any:
"""
Reconstruct a nested structure from a flat list of values and tree definition.
This is the inverse operation of tree_flatten. It takes the flattened
values and the tree structure definition to reconstruct the original
nested structure.
Args:
treedef: The tree structure definition from tree_flatten
xs: List of flat values to reconstruct from
Returns:
The reconstructed nested structure
Example:
>>> flat_values, treedef = tree_flatten([1, [2, 3], 4])
>>> tree_unflatten(treedef, flat_values)
[1, [2, 3], 4]
"""
return _tree_unflatten(treedef, iter(xs))
def _tree_unflatten(treedef: PyTreeDef | Leaf, xs: Iterator[Any]) -> Any:
"""
Internal function to reconstruct a tree structure.
This is the core implementation of tree unflattening that handles
different types of tree definitions including Leaf nodes and PyTreeDef nodes.
Args:
treedef: The tree structure definition
xs: Iterator of flat values to reconstruct from
Returns:
The reconstructed object
"""
match treedef:
case Leaf(is_none=True):
return None
case Leaf(
node_metadata=metadata
) if metadata and metadata.is_dynamic_expression:
return metadata.original_obj.__new_from_mlir_values__([next(xs)])
case Leaf(is_numeric=True):
return as_numeric(next(xs))
case Leaf():
return next(xs)
case PyTreeDef():
children = (_tree_unflatten(t, xs) for t in treedef.child_treedefs)
return treedef.node_type.from_iterable(treedef.node_metadata, children)
def _check_tree_equal(lhs: Union[PyTreeDef, Leaf], rhs: Union[PyTreeDef, Leaf]) -> bool:
"""
Check if two tree definitions are structurally equal.
This is a helper function for check_tree_equal that recursively compares
tree structures.
Args:
lhs: Left tree definition (PyTreeDef or Leaf)
rhs: Right tree definition (PyTreeDef or Leaf)
Returns:
bool: True if the trees are structurally equal, False otherwise
"""
match (lhs, rhs):
case (Leaf(), Leaf()):
return lhs.is_none == rhs.is_none and lhs.ir_type_str == rhs.ir_type_str
case (PyTreeDef(), PyTreeDef()):
lhs_metadata = lhs.node_metadata
rhs_metadata = rhs.node_metadata
lhs_fields = getattr(lhs_metadata, "fields", [])
rhs_fields = getattr(rhs_metadata, "fields", [])
lhs_constexpr_fields = getattr(lhs_metadata, "constexpr_fields", [])
rhs_constexpr_fields = getattr(rhs_metadata, "constexpr_fields", [])
return (
lhs.node_type == rhs.node_type
and lhs_fields == rhs_fields
and lhs_constexpr_fields == rhs_constexpr_fields
and len(lhs.child_treedefs) == len(rhs.child_treedefs)
and all(map(_check_tree_equal, lhs.child_treedefs, rhs.child_treedefs))
)
case _:
return False
def check_tree_equal(lhs: PyTreeDef, rhs: PyTreeDef) -> int:
"""
Check if two tree definitions are equal and return the index of first difference.
This function compares two tree definitions and returns the index of the
first child that differs, or -1 if they are completely equal.
Args:
lhs: Left tree definition
rhs: Right tree definition
Returns:
int: Index of the first differing child, or -1 if trees are equal
Example:
>>> treedef1 = tree_flatten([1, [2, 3]])[1]
>>> treedef2 = tree_flatten([1, [2, 4]])[1]
>>> check_tree_equal(treedef1, treedef2)
1 # The second child differs
"""
assert len(lhs.child_treedefs) == len(rhs.child_treedefs)
def find_first_difference(
index_and_pair: tuple[int, tuple[PyTreeDef, PyTreeDef]]
) -> int:
index, (l, r) = index_and_pair
return index if not _check_tree_equal(l, r) else -1
differences = map(
find_first_difference, enumerate(zip(lhs.child_treedefs, rhs.child_treedefs))
)
return next((diff for diff in differences if diff != -1), -1)