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