Release v4.0.0 (#2294)
This commit is contained in:
@@ -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
|
||||
@@ -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
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
@@ -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)
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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
@@ -0,0 +1,19 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
|
||||
#
|
||||
# Use of this software is governed by the terms and conditions of the
|
||||
# NVIDIA End User License Agreement (EULA), available at:
|
||||
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
|
||||
#
|
||||
# Any use, reproduction, disclosure, or distribution of this software
|
||||
# and related documentation outside the scope permitted by the EULA
|
||||
# is strictly prohibited.
|
||||
|
||||
from . import stacktrace
|
||||
from . import logger
|
||||
from . import timer
|
||||
__all__ = [
|
||||
"logger",
|
||||
"timer",
|
||||
"stacktrace",
|
||||
]
|
||||
@@ -0,0 +1,80 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
|
||||
#
|
||||
# Use of this software is governed by the terms and conditions of the
|
||||
# NVIDIA End User License Agreement (EULA), available at:
|
||||
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
|
||||
#
|
||||
# Any use, reproduction, disclosure, or distribution of this software
|
||||
# and related documentation outside the scope permitted by the EULA
|
||||
# is strictly prohibited.
|
||||
|
||||
"""
|
||||
This module provides logging helper functions
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
logger = None
|
||||
|
||||
|
||||
def log():
|
||||
return logger
|
||||
|
||||
|
||||
def setup_log(
|
||||
name, log_to_console=False, log_to_file=False, log_file_path=None, log_level=1
|
||||
):
|
||||
"""Set up and configure a logger with console and/or file handlers.
|
||||
|
||||
:param name: Name of the logger to create
|
||||
:type name: str
|
||||
:param log_to_console: Whether to enable logging to console, defaults to False
|
||||
:type log_to_console: bool, optional
|
||||
:param log_to_file: Whether to enable logging to file, defaults to False
|
||||
:type log_to_file: bool, optional
|
||||
:param log_file_path: Path to the log file, required if log_to_file is True
|
||||
:type log_file_path: str, optional
|
||||
:param log_level: Logging level to set, defaults to 1
|
||||
:type log_level: int, optional
|
||||
:raises ValueError: If log_to_file is True but log_file_path is not provided
|
||||
:return: Configured logger instance
|
||||
:rtype: logging.Logger
|
||||
"""
|
||||
# Create a custom logger
|
||||
global logger
|
||||
logger = logging.getLogger(name)
|
||||
if log_to_console or log_to_file:
|
||||
logger.setLevel(log_level)
|
||||
else:
|
||||
logger.setLevel(logging.NOTSET)
|
||||
|
||||
# Clear existing handlers to prevent duplicate logs
|
||||
if logger.hasHandlers():
|
||||
logger.handlers.clear()
|
||||
|
||||
# Define formatter
|
||||
formatter = logging.Formatter(
|
||||
f"%(asctime)s - %(name)s - %(levelname)s - [%(funcName)s] - %(message)s"
|
||||
)
|
||||
|
||||
# Add console handler if enabled
|
||||
if log_to_console:
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(log_level)
|
||||
console_handler.setFormatter(formatter)
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
# Add file handler if enabled
|
||||
if log_to_file:
|
||||
if not log_file_path:
|
||||
raise ValueError("log_file_path must be provided when enable_file is True")
|
||||
file_handler = logging.FileHandler(log_file_path)
|
||||
file_handler.setLevel(log_level)
|
||||
file_handler.setFormatter(formatter)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
logger = setup_log("generic")
|
||||
@@ -0,0 +1,165 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
|
||||
#
|
||||
# Use of this software is governed by the terms and conditions of the
|
||||
# NVIDIA End User License Agreement (EULA), available at:
|
||||
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
|
||||
#
|
||||
# Any use, reproduction, disclosure, or distribution of this software
|
||||
# and related documentation outside the scope permitted by the EULA
|
||||
# is strictly prohibited.
|
||||
|
||||
"""
|
||||
This module provides stacktrace helper functions
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
def walk_to_top_module(start_path):
|
||||
"""
|
||||
Walk up from the start_path to find the top-level Python module.
|
||||
|
||||
:param start_path: The path to start from.
|
||||
:return: The path of the top-level module.
|
||||
"""
|
||||
current_path = start_path
|
||||
|
||||
while True:
|
||||
# Check if we are at the root directory
|
||||
if os.path.dirname(current_path) == current_path:
|
||||
break
|
||||
|
||||
# Check for __init__.py
|
||||
init_file_path = os.path.join(current_path, "__init__.py")
|
||||
if os.path.isfile(init_file_path):
|
||||
# If __init__.py exists, move up one level
|
||||
current_path = os.path.dirname(current_path)
|
||||
else:
|
||||
# If no __init__.py, we are not in a module; stop
|
||||
break
|
||||
|
||||
# If we reached the root without finding a module, return None
|
||||
if os.path.dirname(current_path) == current_path and not os.path.isfile(
|
||||
os.path.join(current_path, "__init__.py")
|
||||
):
|
||||
return None
|
||||
|
||||
# Return the path of the top-level module
|
||||
return current_path
|
||||
|
||||
|
||||
def _filter_internal_frames(traceback, internal_path):
|
||||
"""
|
||||
Filter out stack frames from the traceback that belong to the specified module path.
|
||||
|
||||
This function removes stack frames from the traceback whose file paths start with
|
||||
the given prefix_path, effectively hiding internal implementation details from
|
||||
the error traceback shown to users.
|
||||
"""
|
||||
iter_prev = None
|
||||
iter_tb = traceback
|
||||
while iter_tb is not None:
|
||||
if os.path.abspath(iter_tb.tb_frame.f_code.co_filename).startswith(
|
||||
internal_path
|
||||
):
|
||||
if iter_tb.tb_next:
|
||||
if iter_prev:
|
||||
iter_prev.tb_next = iter_tb.tb_next
|
||||
else:
|
||||
traceback = iter_tb.tb_next
|
||||
else:
|
||||
iter_prev = iter_tb
|
||||
iter_tb = iter_tb.tb_next
|
||||
return traceback
|
||||
|
||||
|
||||
_generated_function_names = re.compile(
|
||||
r"^(loop_body|while_region|while_before_block|while_after_block|if_region|then_block|else_block|elif_region)_\d+$"
|
||||
)
|
||||
|
||||
|
||||
def _filter_duplicated_frames(traceback):
|
||||
"""
|
||||
Filter out duplicated stack frames from the traceback.
|
||||
The function filters out consecutive frames that are in the same file and have the same line number.
|
||||
In a sequence of consecutive frames, the logic prefers to keep the non-generated frame or the last frame.
|
||||
"""
|
||||
iter_prev = None
|
||||
iter_tb = traceback
|
||||
while iter_tb is not None:
|
||||
skip_current = False
|
||||
skip_next = False
|
||||
if iter_tb.tb_next:
|
||||
current_filename = os.path.abspath(iter_tb.tb_frame.f_code.co_filename)
|
||||
next_filename = os.path.abspath(iter_tb.tb_next.tb_frame.f_code.co_filename)
|
||||
# if in the same file, check if the line number is the same
|
||||
if current_filename == next_filename:
|
||||
current_lineno = iter_tb.tb_lineno
|
||||
next_lineno = iter_tb.tb_next.tb_lineno
|
||||
if current_lineno == next_lineno:
|
||||
# Same file and line number, check name, if current is generated, skip current, otherwise skip next
|
||||
name = iter_tb.tb_frame.f_code.co_name
|
||||
is_generated = bool(_generated_function_names.match(name))
|
||||
if is_generated:
|
||||
# Skip current
|
||||
skip_current = True
|
||||
else:
|
||||
# Skip next if it's generated, otherwise keep both
|
||||
next_name = iter_tb.tb_next.tb_frame.f_code.co_name
|
||||
skip_next = bool(_generated_function_names.match(next_name))
|
||||
if skip_current:
|
||||
if iter_prev:
|
||||
iter_prev.tb_next = iter_tb.tb_next
|
||||
else:
|
||||
traceback = iter_tb.tb_next
|
||||
elif skip_next:
|
||||
# if next is last frame, don't skip
|
||||
if iter_tb.tb_next.tb_next:
|
||||
iter_tb.tb_next = iter_tb.tb_next.tb_next
|
||||
iter_prev = iter_tb
|
||||
else:
|
||||
iter_prev = iter_tb
|
||||
iter_tb = iter_tb.tb_next
|
||||
|
||||
return traceback
|
||||
|
||||
|
||||
def filter_stackframe(traceback, prefix_path):
|
||||
"""
|
||||
Filter out stack frames from the traceback that belong to the specified module path.
|
||||
|
||||
This function removes stack frames from the traceback whose file paths start with
|
||||
the given prefix_path, effectively hiding internal implementation details from
|
||||
the error traceback shown to users.
|
||||
|
||||
:param traceback: The traceback object to filter.
|
||||
:param prefix_path: The path prefix to filter out from the traceback.
|
||||
:return: The filtered traceback with internal frames removed.
|
||||
"""
|
||||
# Step 1: filter internal frames
|
||||
traceback = _filter_internal_frames(traceback, prefix_path)
|
||||
|
||||
# Step 2: consolidate duplicated frames
|
||||
return _filter_duplicated_frames(traceback)
|
||||
|
||||
|
||||
def filter_exception(value, module_dir):
|
||||
"""
|
||||
Filter out internal implementation details from exception traceback.
|
||||
|
||||
This function recursively processes an exception and its cause chain,
|
||||
removing stack frames that belong to the specified module directory.
|
||||
This helps to present cleaner error messages to users by hiding
|
||||
implementation details.
|
||||
|
||||
:param value: The exception object to filter.
|
||||
:param module_dir: The module directory path to filter out from tracebacks.
|
||||
:return: The filtered exception with internal frames removed.
|
||||
"""
|
||||
if hasattr(value, "__cause__") and value.__cause__:
|
||||
filter_exception(value.__cause__, module_dir)
|
||||
|
||||
if hasattr(value, "__traceback__"):
|
||||
filter_stackframe(value.__traceback__, module_dir)
|
||||
@@ -0,0 +1,56 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: LicenseRef-NvidiaProprietary
|
||||
#
|
||||
# Use of this software is governed by the terms and conditions of the
|
||||
# NVIDIA End User License Agreement (EULA), available at:
|
||||
# https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html
|
||||
#
|
||||
# Any use, reproduction, disclosure, or distribution of this software
|
||||
# and related documentation outside the scope permitted by the EULA
|
||||
# is strictly prohibited.
|
||||
|
||||
"""
|
||||
This module provides a timing helper functions
|
||||
"""
|
||||
from functools import wraps
|
||||
|
||||
from .logger import log
|
||||
|
||||
|
||||
# TODO: revisit this part when mlir timing manager is ready for pybind.
|
||||
def timer(*dargs, **kwargs):
|
||||
enable = kwargs.get("enable", True)
|
||||
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def func_wrapper(*args, **kwargs):
|
||||
if not enable:
|
||||
return func(*args, **kwargs)
|
||||
from time import time
|
||||
|
||||
start = time()
|
||||
result = func(*args, **kwargs)
|
||||
end = time()
|
||||
|
||||
# Convert time from seconds to us
|
||||
spend_us = (end - start) * 1e6
|
||||
|
||||
# Determine the function type and format the log message
|
||||
if hasattr(func, "__name__"):
|
||||
func_name = func.__name__
|
||||
log_message = f"[JIT-TIMER] Function: {func_name} | Execution Time: {spend_us:.2f} µs"
|
||||
elif "CFunctionType" in str(type(func)):
|
||||
log_message = f"[JIT-TIMER] C API Function: {str(func)} | Execution Time: {spend_us:.2f} µs"
|
||||
else:
|
||||
log_message = f"[JIT-TIMER] Anonymous Function | Execution Time: {spend_us:.2f} µs"
|
||||
|
||||
log().info(log_message)
|
||||
|
||||
return result
|
||||
|
||||
return func_wrapper
|
||||
|
||||
if len(dargs) == 1 and callable(dargs[0]):
|
||||
return decorator(dargs[0])
|
||||
else:
|
||||
return decorator
|
||||
Reference in New Issue
Block a user