Release v4.0.0 (#2294)

This commit is contained in:
Kihiro Bando
2025-05-13 15:55:29 -04:00
committed by GitHub
parent ad7b2f5e84
commit f115c3f854
299 changed files with 51495 additions and 4413 deletions
+17
View File
@@ -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 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 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:
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:
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:
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:
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:
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:
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.const(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:
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 super().__str__().replace(ir.Value.__name__, ArithValue.__name__)
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:
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:
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
+584
View File
@@ -0,0 +1,584 @@
# 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 .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_dynamic: Generates MLIR for OP
for_constexpr: Executes a for loop at JIT compile-time
for_execute: Decides whether to execute the loop at compile-time or generate MLIR for OP based on the provided bounds.
if_dynamic: Generates MLIR if OP
if_constexpr: Executes a if at JIT compile-time by python interpreter
if_execute: Decides whether to execute the if statement at compile-time or generate MLIR if OP based on the predicate.
"""
def __init__(self):
self._is_dynamic_expression = None
self._loop_execute_range_dynamic = None
self._if_dynamic = None
self._while_dynamic = None
def set_functions(
self,
is_dynamic_expression: Callable,
loop_execute_range_dynamic: Callable,
if_dynamic: Callable,
while_dynamic: Callable,
):
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
@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_dynamic(
self,
func: Callable,
start,
stop,
step,
used_args: list,
iter_args: list,
iter_arg_names: list,
unroll=bool,
unroll_full=int,
):
log().info("start [%s] stop [%s] step [%s]", start, stop, step)
return self._loop_execute_range_dynamic(
func,
start,
stop,
step,
used_args,
iter_args,
iter_arg_names,
unroll,
unroll_full,
)
@staticmethod
def for_constexpr(
func: Callable,
start: int,
stop: int,
step: int,
used_args: list,
iter_args: list,
):
log().info("start [%s] stop [%s] step [%s]", start, stop, step)
loop_results = iter_args
log().debug("iter_args [%s]", iter_args)
for i in range(start, stop, step):
log().debug("i [%s] iter_args [%s]", i, iter_args)
loop_results = func(i, *used_args, *loop_results)
log().debug("loop_results [%s]", loop_results)
if loop_results is None:
loop_results = []
if not isinstance(loop_results, list):
loop_results = [loop_results]
log().debug("done loop_results [%s]", loop_results)
return Executor.converge_ret_val(loop_results)
def for_execute(
self,
func,
start,
stop,
step,
used_args=[],
iter_args=[],
iter_arg_names=[],
unroll=-1,
unroll_full=False,
is_range_constexpr=None,
):
assert (
self._loop_execute_range_dynamic and self._is_dynamic_expression
), "Functions must be set before execution."
log().debug("start [%s] stop [%s] step [%s]", start, stop, step)
any_dynamic_expression = (
self._is_dynamic_expression(start)
or self._is_dynamic_expression(stop)
or self._is_dynamic_expression(step)
)
if is_range_constexpr is None:
if not any_dynamic_expression:
return self.for_constexpr(func, start, stop, step, used_args, iter_args)
else:
return self.for_dynamic(
func,
start,
stop,
step,
used_args,
iter_args,
iter_arg_names,
unroll,
unroll_full,
)
# Ensure bounds are compile-time constants for constexpr execution
if is_range_constexpr:
if any_dynamic_expression:
raise DSLRuntimeError(
"Loop bounds must be constexpr (compile-time constants)"
)
return self.for_constexpr(func, start, stop, step, used_args, iter_args)
# MLIR generation
return self.for_dynamic(
func,
start,
stop,
step,
used_args,
iter_args,
iter_arg_names,
unroll,
unroll_full,
)
def if_dynamic(
self,
pred,
then_block: Callable,
else_block: Optional[Callable] = None,
used_args=[],
yield_args=[],
yield_arg_names=[],
):
return self._if_dynamic(
pred, then_block, else_block, used_args, yield_args, yield_arg_names
)
@staticmethod
def if_constexpr(
pred,
then_block: Callable,
else_block: Optional[Callable] = None,
used_args=[],
yield_args=[],
):
if pred:
log().debug(" running then block [%s]", yield_args)
res = then_block(*used_args, *yield_args)
log().debug("result [%s]", res)
return Executor.converge_ret_val(res)
elif else_block is not None:
log().debug("running else [%s]", yield_args)
res = else_block(*used_args, *yield_args)
log().debug("result [%s]", res)
return Executor.converge_ret_val(res)
def if_execute(
self,
pred,
then_block: Callable,
else_block: Optional[Callable] = None,
used_args=[],
yield_args=[],
yield_arg_names=[],
if_constexpr=None,
):
assert (
self._if_dynamic and self._is_dynamic_expression
), "Functions must be set before execution."
is_if_constexpr = not self._is_dynamic_expression(pred)
if if_constexpr is None:
if is_if_constexpr:
return self.if_constexpr(
pred, then_block, else_block, used_args, yield_args
)
else:
return self.if_dynamic(
pred, then_block, else_block, used_args, yield_args, yield_arg_names
)
# Ensure bounds are compile-time constants for constexpr execution
if if_constexpr:
if not is_if_constexpr:
raise DSLRuntimeError(
"If predicate must be constexpr (compile-time constants)"
)
return self.if_constexpr(
pred, then_block, else_block, used_args, yield_args
)
# MLIR generation
return self.if_dynamic(
pred, then_block, else_block, used_args, yield_args, yield_arg_names
)
def while_dynamic(
self,
while_before_block: Callable,
while_after_block: Callable,
used_args=[],
yield_args=[],
yield_arg_names=[],
):
return self._while_dynamic(
while_before_block,
while_after_block,
used_args,
yield_args,
yield_arg_names,
)
@staticmethod
def while_constexpr(
while_before_block,
while_after_block,
used_args=[],
yield_args=[],
):
log().debug(
"while_constexpr begin %s", while_before_block.__qualname__
)
cond, loop_results = while_before_block(*used_args, *yield_args)
while cond:
loop_results = Executor.convert_to_list(loop_results)
log().debug(
"calling while_after [%s], [%s]",
used_args,
loop_results,
)
loop_results = while_after_block(*used_args, *loop_results)
log().debug(
"while after [%s]", loop_results
)
loop_results = Executor.convert_to_list(loop_results)
log().debug(
"calling while_before [%s], [%s]",
used_args,
loop_results,
)
cond, loop_results = while_before_block(*used_args, *loop_results)
log().debug(
"while_before cond, results [%s], [%s]",
cond,
loop_results,
)
log().debug(
"while_constexpr results %s", loop_results
)
return Executor.converge_ret_val(loop_results)
def while_execute(
self,
pred,
while_before_block: Callable,
while_after_block: Callable,
used_args=[],
yield_args=[],
yield_arg_names=[],
while_constexpr=None,
):
assert (
self._while_dynamic and self._is_dynamic_expression
), "Functions must be set before execution."
is_while_constexpr = not self._is_dynamic_expression(pred)
# Ensure bounds are compile-time constants for constexpr execution
if while_constexpr:
if not is_while_constexpr:
raise DSLRuntimeError(
"While predicate must be constexpr (compile-time constants)"
)
return self.while_constexpr(
while_before_block, while_after_block, used_args, yield_args
)
# MLIR generation
return self.while_dynamic(
while_before_block,
while_after_block,
used_args,
yield_args,
yield_arg_names,
)
# =============================================================================
# Decorator
# =============================================================================
executor = Executor()
def loop_selector(
start,
stop,
step,
used_args=[],
iter_args=[],
iter_arg_names=[],
unroll=-1,
unroll_full=False,
constexpr=None,
):
log().info(
"start [%s] stop [%s] step [%s] used_args [%s] iter_args [%s] unroll [%s] unroll_full [%s] constexpr [%s]",
start,
stop,
step,
used_args,
iter_args,
unroll,
unroll_full,
constexpr,
)
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,
used_args,
iter_args,
iter_arg_names,
unroll,
unroll_full,
constexpr,
)
return ir_loop
def if_selector(pred, used_args=[], yield_args=[]):
log().info("pred [%s] used_args [%s] yield_args [%s]", pred, used_args, yield_args)
# Handle Numeric types here?
from .typing import Numeric
if isinstance(pred, Numeric):
pred = pred.value
def ir_loop(func):
return func(pred, *used_args, *yield_args)
return ir_loop
def while_selector(pred, used_args=[], yield_args=[]):
def ir_while_loop(func):
return func(pred, *used_args, *yield_args)
return ir_while_loop
def while_executor(
pred,
while_before_block: Callable,
while_after_block: Callable,
used_args=[],
yield_args=[],
yield_arg_names=[],
constexpr=None,
):
return executor.while_execute(
pred,
while_before_block,
while_after_block,
used_args,
yield_args,
yield_arg_names,
constexpr,
)
def if_executor(
pred,
then_block: Callable,
else_block: Optional[Callable] = None,
used_args=[],
yield_args=[],
yield_arg_names=[],
constexpr=None,
):
return executor.if_execute(
pred, then_block, else_block, used_args, yield_args, yield_arg_names, constexpr
)
# =============================================================================
# Range
# =============================================================================
class range_dynamic:
@overload
def __new__(cls, stop, unroll=0, unroll_full=False):
pass
@overload
def __new__(cls, start, stop, step, unroll=0, unroll_full=False):
pass
def __new__(cls, *args, **kwargs):
raise DSLRuntimeError("range_dynamic should be always preprocessed to IR")
class range_constexpr:
def __init__(self, *args):
if len(args) == 1:
self.start = 0
self.stop = args[0]
self.step = 1
elif len(args) == 2:
self.start, self.stop = args
self.step = 1
elif len(args) == 3:
self.start, self.stop, self.step = args
else:
raise DSLRuntimeError(
"range_constexpr supports up to 3 arguments (start, stop, step)"
)
# Ensure the arguments are compile-time constants (if required)
for arg_name, arg_value in [
("step", self.step),
("start", self.start),
("stop", self.stop),
]:
if executor._is_dynamic_expression(arg_value):
raise DSLRuntimeError(
f"`range_constexpr` requires `constexpr` (non-IR Values) for all arguments, "
f"but `{arg_name}` is not. If the arguments are dynamic, use `range`; the DSL "
f"will handle them during runtime. ",
suggestion="Use `range` instead of `range_constexpr`.",
)
def __iter__(self) -> Iterator[int]:
current = self.start
while current < self.stop:
yield current
current += self.step
# =============================================================================
# If expressions
# =============================================================================
def const_expr(expression):
if executor._is_dynamic_expression(expression):
raise DSLRuntimeError(
f"The function `const_expr({expression})` received a dynamic expression (non compile-time constant).",
context={
"const_expr": "Accepts only constexpr (compile-time constant)",
"If your expression depends on dynamic values": "Avoid marking it as `const_expr()`",
"If the expression could be either dynamic or constexpr": "Omit explicit `const_expr()` marker; the DSL will infer the correct handling automatically",
},
)
return expression
def dynamic_expr(expression):
raise DSLRuntimeError("dynamic_expr should be always preprocessed to IR")
# =============================================================================
# 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)
File diff suppressed because it is too large Load Diff
+154
View File
@@ -0,0 +1,154 @@
# 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))
if not os.path.exists(path):
os.makedirs(path)
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
+221
View File
@@ -0,0 +1,221 @@
# 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
from .common import DSLRuntimeError
_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)
def compile(func, *args, **kwargs):
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.")
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
+303
View File
@@ -0,0 +1,303 @@
# 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
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 and minor >= 0:
suffix = "a"
elif minor != 0:
# e.g sm_86, belong with sm_80 family
minor = 0
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]_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.log_to_console = get_bool_env_var(f"{prefix}_LOG_TO_CONSOLE", False)
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)
self.log_to_file = get_bool_env_var(f"{prefix}_LOG_TO_FILE", False)
# Other options
self.log_level = get_int_env_var(f"{prefix}_LOG_LEVEL", 1)
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.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)
+301
View File
@@ -0,0 +1,301 @@
# 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 io
import inspect
import ctypes
import numpy as np
from typing import get_origin
# Local modules imports
from .utils.timer import timer
from .utils.logger import log
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 . import typing as t
# MLIR modules imports
from .._mlir import ir
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.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 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.
"""
# args/kwargs must match arg_specs
# No canonicalization of args/kwargs to avoid extra latency
if len(args) != len(args_spec.args) or len(kwargs) != len(args_spec.kwonlyargs):
raise DSLRuntimeError(
"input args/kwargs length does not match runtime function signature!",
context={
"input args length": len(args),
"input kwargs length": len(kwargs),
"function signature args length": len(args_spec.args),
"function signature kwonlyargs length": len(args_spec.kwonlyargs),
},
)
exe_args = []
input_args = [*args, *kwargs.values()]
input_arg_names = [*args_spec.args, *args_spec.kwonlyargs]
for i, arg in enumerate(input_args):
arg_type = args_spec.annotations.get(input_arg_names[i], None)
# Implicit cast to NumericMeta
if isinstance(arg_type, t.NumericMeta):
arg = t.cast(arg, arg_type)
# If not any known type, try registered adapter to do the conversion
adapter = JitArgAdapterRegistry.get_registered_adapter(type(arg))
adapted_arg = adapter(arg) if adapter else arg
exe_args.extend(get_c_pointers(adapted_arg))
return exe_args
def __call__(self, *args, **kwargs):
exe_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,29 @@
# 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 device_tensor
from . import dlpack_types
from . import cuda
from . import tensor_descriptor
from . import jit_arg_adapters
__all__ = [
"device_tensor",
"dlpack_types",
"cuda",
"tensor_descriptor",
"jit_arg_adapters",
]
+470
View File
@@ -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=0, 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 = ctypes.cast(self._arg.getPtr(), ctypes.c_void_p)
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
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
def is_tensor_descriptor(maybe_tensor_descriptor) -> bool:
"""Check if the object is a TensorDescriptor."""
return isinstance(
maybe_tensor_descriptor, TensorDescriptor
) or TensorDescriptor.can_transformed_to_dlpack(maybe_tensor_descriptor)
File diff suppressed because it is too large Load Diff
+19
View File
@@ -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",
]
+80
View File
@@ -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")
+165
View File
@@ -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)
+56
View File
@@ -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