forked from ccf-ai-infra/TileOPs-Metax
1864 lines
56 KiB
Python
1864 lines
56 KiB
Python
"""Elementwise op templates and broadcast utility.
|
|
|
|
Three Op template base classes:
|
|
- UnaryOp: wraps UnaryKernel with reshape/flatten
|
|
- BinaryOp: wraps BinaryKernel with broadcast coalescing
|
|
- FusedGatedOp: wraps FusedGatedKernel with (M, 2N) layout
|
|
|
|
torch.compile support:
|
|
- All 55 concrete ops are registered via @torch.library.custom_op at module load time
|
|
- Three factory functions (_register_unary_custom_op, _register_binary_custom_op,
|
|
_register_fused_gated_custom_op) register every op; instances are looked up at
|
|
runtime via _OP_REGISTRY keyed by id(instance)
|
|
|
|
Utility:
|
|
- coalesce_broadcast_dims: reduces N-dim broadcast to minimal effective dims
|
|
"""
|
|
|
|
import math
|
|
import weakref
|
|
from math import prod
|
|
from typing import Dict, List, Optional
|
|
|
|
import torch
|
|
|
|
from tileops.kernels.elementwise import (
|
|
AbsKernel,
|
|
AddKernel,
|
|
AlibiKernel,
|
|
BitwiseAndKernel,
|
|
BitwiseNotKernel,
|
|
BitwiseOrKernel,
|
|
BitwiseXorKernel,
|
|
CeilKernel,
|
|
ClampKernel,
|
|
CosKernel,
|
|
DivKernel,
|
|
EluKernel,
|
|
EqKernel,
|
|
ErfKernel,
|
|
ExpKernel,
|
|
Expm1Kernel,
|
|
FloorDivideKernel,
|
|
FloorKernel,
|
|
GeKernel,
|
|
GeluAndMulKernel,
|
|
GeluKernel,
|
|
GeluTanhAndMulKernel,
|
|
GtKernel,
|
|
HardsigmoidKernel,
|
|
HardswishKernel,
|
|
HardtanhKernel,
|
|
IsfiniteKernel,
|
|
IsinfKernel,
|
|
IsnanKernel,
|
|
LeakyReluKernel,
|
|
LeKernel,
|
|
LerpKernel,
|
|
Log1pKernel,
|
|
LogicalAndKernel,
|
|
LogicalNotKernel,
|
|
LogicalOrKernel,
|
|
LogKernel,
|
|
LtKernel,
|
|
MaskedFillKernel,
|
|
MaximumKernel,
|
|
MinimumKernel,
|
|
MishKernel,
|
|
MulKernel,
|
|
NanToNumKernel,
|
|
NegKernel,
|
|
NeKernel,
|
|
PowKernel,
|
|
PreluKernel,
|
|
ReciprocalKernel,
|
|
ReluKernel,
|
|
RemainderKernel,
|
|
RoundKernel,
|
|
RsqrtKernel,
|
|
SeluKernel,
|
|
SigmoidKernel,
|
|
SignKernel,
|
|
SiluAndMulKernel,
|
|
SiluKernel,
|
|
SinKernel,
|
|
SinusoidalKernel,
|
|
SoftplusKernel,
|
|
SqrtKernel,
|
|
SubKernel,
|
|
TanhKernel,
|
|
TruncKernel,
|
|
WhereKernel,
|
|
)
|
|
from tileops.kernels.kernel import Kernel
|
|
|
|
from .op import Op
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# torch.compile registration factories
|
|
#
|
|
# Each factory creates a @torch.library.custom_op + register_fake pair.
|
|
# Instances register themselves in _OP_REGISTRY keyed by integer id.
|
|
# The custom_op receives this key and looks up the instance to call the
|
|
# pre-built tilelang kernel. The key is a plain int so dynamo can trace
|
|
# through forward() without hitting unsupported Python side-effects.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_OP_REGISTRY: weakref.WeakValueDictionary = weakref.WeakValueDictionary()
|
|
|
|
_FP8_NONSAT_OUTPUT_DTYPES = {
|
|
torch.float8_e5m2: torch.float16,
|
|
}
|
|
|
|
|
|
def _effective_scalar_kernel_dtype(dtype: torch.dtype) -> torch.dtype:
|
|
"""Return the dtype used when scalar literals are materialized in kernels."""
|
|
return _FP8_NONSAT_OUTPUT_DTYPES.get(dtype, dtype)
|
|
|
|
|
|
def _validate_scalar_param_repr(
|
|
param_name: str, value: float, dtype: torch.dtype, op_name: str,
|
|
) -> None:
|
|
"""Reject scalar params that cannot be represented in the kernel dtype."""
|
|
if not isinstance(value, (int, float)):
|
|
raise TypeError(f"{op_name} expected scalar {param_name} to be int/float, got {type(value)}")
|
|
|
|
kernel_dtype = _effective_scalar_kernel_dtype(dtype)
|
|
finfo = torch.finfo(kernel_dtype)
|
|
value_f64 = float(value)
|
|
if math.isnan(value_f64):
|
|
return
|
|
if math.isinf(value_f64):
|
|
raise ValueError(
|
|
f"{op_name} received {param_name}={value!r}, but {param_name} must be finite and "
|
|
f"representable in effective kernel dtype {kernel_dtype}"
|
|
)
|
|
if not (finfo.min <= value_f64 <= finfo.max):
|
|
raise ValueError(
|
|
f"{op_name} received {param_name}={value!r}, which is not representable in "
|
|
f"effective kernel dtype {kernel_dtype} (valid finite range: "
|
|
f"[{finfo.min}, {finfo.max}])"
|
|
)
|
|
|
|
|
|
def _register_unary_custom_op(op_cls, output_dtype_override=None):
|
|
"""Register a unary elementwise op for torch.compile.
|
|
|
|
Args:
|
|
op_cls: The Op subclass to register (must have ``_op_name``).
|
|
output_dtype_override: If set, the output dtype (e.g. torch.bool for predicates).
|
|
"""
|
|
op_name = op_cls._op_name
|
|
|
|
@torch.library.custom_op(f"top::elementwise_unary_{op_name}", mutates_args=())
|
|
def _wrapped(x: torch.Tensor, instance_key: int) -> torch.Tensor:
|
|
instance = _OP_REGISTRY[instance_key]
|
|
return instance._eager_forward(x)
|
|
|
|
@_wrapped.register_fake
|
|
def _(x: torch.Tensor, instance_key: int) -> torch.Tensor:
|
|
out_dtype = output_dtype_override if output_dtype_override is not None else x.dtype
|
|
return torch.empty_like(x, dtype=out_dtype)
|
|
|
|
op_cls._wrapped = _wrapped
|
|
|
|
|
|
def _register_binary_custom_op(op_cls, output_bool: bool = False):
|
|
"""Register a binary elementwise op for torch.compile.
|
|
|
|
Args:
|
|
op_cls: The Op subclass to register.
|
|
output_bool: If True, output dtype is torch.bool (for comparison/logical ops).
|
|
"""
|
|
op_name = op_cls._op_name
|
|
|
|
@torch.library.custom_op(f"top::elementwise_binary_{op_name}", mutates_args=())
|
|
def _wrapped(
|
|
a: torch.Tensor,
|
|
b: torch.Tensor,
|
|
out_shape: List[int],
|
|
instance_key: int,
|
|
) -> torch.Tensor:
|
|
instance = _OP_REGISTRY[instance_key]
|
|
return instance._eager_forward(a, b)
|
|
|
|
@_wrapped.register_fake
|
|
def _(
|
|
a: torch.Tensor,
|
|
b: torch.Tensor,
|
|
out_shape: List[int],
|
|
instance_key: int,
|
|
) -> torch.Tensor:
|
|
out_dtype = torch.bool if output_bool else a.dtype
|
|
return a.new_empty(out_shape, dtype=out_dtype)
|
|
|
|
op_cls._wrapped = _wrapped
|
|
|
|
|
|
def _register_prelu_custom_op(op_cls):
|
|
"""Register a PReLU-style op (x, weight -> y) for torch.compile."""
|
|
op_name = op_cls._op_name
|
|
|
|
@torch.library.custom_op(f"top::elementwise_{op_name}", mutates_args=())
|
|
def _wrapped(
|
|
x: torch.Tensor,
|
|
weight: torch.Tensor,
|
|
instance_key: int,
|
|
) -> torch.Tensor:
|
|
instance = _OP_REGISTRY[instance_key]
|
|
return instance._eager_forward(x, weight)
|
|
|
|
@_wrapped.register_fake
|
|
def _(
|
|
x: torch.Tensor,
|
|
weight: torch.Tensor,
|
|
instance_key: int,
|
|
) -> torch.Tensor:
|
|
return torch.empty_like(x)
|
|
|
|
op_cls._wrapped = _wrapped
|
|
|
|
|
|
def _register_where_custom_op(op_cls):
|
|
"""Register a where-style op (cond, x, y -> out) for torch.compile."""
|
|
op_name = op_cls._op_name
|
|
|
|
@torch.library.custom_op(f"top::elementwise_{op_name}", mutates_args=())
|
|
def _wrapped(
|
|
cond: torch.Tensor,
|
|
x: torch.Tensor,
|
|
y: torch.Tensor,
|
|
instance_key: int,
|
|
) -> torch.Tensor:
|
|
instance = _OP_REGISTRY[instance_key]
|
|
return instance._eager_forward(cond, x, y)
|
|
|
|
@_wrapped.register_fake
|
|
def _(
|
|
cond: torch.Tensor,
|
|
x: torch.Tensor,
|
|
y: torch.Tensor,
|
|
instance_key: int,
|
|
) -> torch.Tensor:
|
|
return torch.empty_like(x)
|
|
|
|
op_cls._wrapped = _wrapped
|
|
|
|
|
|
def _register_masked_fill_custom_op(op_cls):
|
|
"""Register a masked-fill-style op (x, mask -> y) for torch.compile."""
|
|
op_name = op_cls._op_name
|
|
|
|
@torch.library.custom_op(f"top::elementwise_{op_name}", mutates_args=())
|
|
def _wrapped(
|
|
x: torch.Tensor,
|
|
mask: torch.Tensor,
|
|
instance_key: int,
|
|
) -> torch.Tensor:
|
|
instance = _OP_REGISTRY[instance_key]
|
|
return instance._eager_forward(x, mask)
|
|
|
|
@_wrapped.register_fake
|
|
def _(
|
|
x: torch.Tensor,
|
|
mask: torch.Tensor,
|
|
instance_key: int,
|
|
) -> torch.Tensor:
|
|
return torch.empty_like(x)
|
|
|
|
op_cls._wrapped = _wrapped
|
|
|
|
|
|
def _register_generative_custom_op(op_cls, out_shape_fn):
|
|
"""Register a generative op (no tensor input -> out) for torch.compile.
|
|
|
|
A scalar ``device_carrier`` tensor is passed so that ``register_fake``
|
|
can derive the correct device and dtype from a real tensor reference,
|
|
which is required by the torch.compile tracing infrastructure.
|
|
|
|
Args:
|
|
op_cls: The Op subclass to register.
|
|
out_shape_fn: Callable(carrier, num_a, num_b) -> Tensor returning
|
|
the output metadata so register_fake can produce the right shape.
|
|
"""
|
|
op_name = op_cls._op_name
|
|
|
|
@torch.library.custom_op(f"top::elementwise_{op_name}", mutates_args=())
|
|
def _wrapped(
|
|
device_carrier: torch.Tensor,
|
|
num_a: int,
|
|
num_b: int,
|
|
instance_key: int,
|
|
) -> torch.Tensor:
|
|
instance = _OP_REGISTRY[instance_key]
|
|
return instance._eager_forward()
|
|
|
|
@_wrapped.register_fake
|
|
def _(
|
|
device_carrier: torch.Tensor,
|
|
num_a: int,
|
|
num_b: int,
|
|
instance_key: int,
|
|
) -> torch.Tensor:
|
|
return out_shape_fn(device_carrier, num_a, num_b)
|
|
|
|
op_cls._wrapped = _wrapped
|
|
|
|
|
|
def _register_fused_gated_custom_op(op_cls):
|
|
"""Register a fused gated elementwise op for torch.compile.
|
|
|
|
Args:
|
|
op_cls: The Op subclass to register.
|
|
"""
|
|
op_name = op_cls._op_name
|
|
|
|
@torch.library.custom_op(f"top::elementwise_fused_gated_{op_name}", mutates_args=())
|
|
def _wrapped(
|
|
x: torch.Tensor,
|
|
M: int,
|
|
N: int,
|
|
instance_key: int,
|
|
) -> torch.Tensor:
|
|
instance = _OP_REGISTRY[instance_key]
|
|
return instance._eager_forward(x)
|
|
|
|
@_wrapped.register_fake
|
|
def _(
|
|
x: torch.Tensor,
|
|
M: int,
|
|
N: int,
|
|
instance_key: int,
|
|
) -> torch.Tensor:
|
|
return x.new_empty((M, N), dtype=x.dtype)
|
|
|
|
op_cls._wrapped = _wrapped
|
|
|
|
|
|
__all__ = [
|
|
"coalesce_broadcast_dims",
|
|
"UnaryOp",
|
|
"BinaryOp",
|
|
"FusedGatedOp",
|
|
# Unary
|
|
"ReluOp",
|
|
# Binary arithmetic
|
|
"AddOp",
|
|
"SubOp",
|
|
"MulOp",
|
|
"DivOp",
|
|
"RemainderOp",
|
|
"PowOp",
|
|
"FloorDivideOp",
|
|
"LerpOp",
|
|
"MaximumOp",
|
|
"MinimumOp",
|
|
# Comparison (output bool)
|
|
"EqOp",
|
|
"NeOp",
|
|
"GtOp",
|
|
"LtOp",
|
|
"GeOp",
|
|
"LeOp",
|
|
# Logical (output bool)
|
|
"LogicalAndOp",
|
|
"LogicalOrOp",
|
|
# Bitwise
|
|
"BitwiseAndOp",
|
|
"BitwiseOrOp",
|
|
"BitwiseXorOp",
|
|
# Fused gated
|
|
"SiluAndMulOp",
|
|
"GeluAndMulOp",
|
|
"GeluTanhAndMulOp",
|
|
# --- math (17) ---
|
|
"AbsOp",
|
|
"CeilOp",
|
|
"CosOp",
|
|
"ErfOp",
|
|
"ExpOp",
|
|
"Expm1Op",
|
|
"FloorOp",
|
|
"Log1pOp",
|
|
"LogOp",
|
|
"NegOp",
|
|
"ReciprocalOp",
|
|
"RoundOp",
|
|
"RsqrtOp",
|
|
"SignOp",
|
|
"SinOp",
|
|
"SqrtOp",
|
|
"TruncOp",
|
|
# --- activations (8) ---
|
|
"GeluOp",
|
|
"HardsigmoidOp",
|
|
"HardswishOp",
|
|
"MishOp",
|
|
"SeluOp",
|
|
"SigmoidOp",
|
|
"SiluOp",
|
|
"TanhOp",
|
|
# --- logical (1) ---
|
|
"LogicalNotOp",
|
|
# --- bitwise (1) ---
|
|
"BitwiseNotOp",
|
|
# --- special predicates (3) ---
|
|
"IsfiniteOp",
|
|
"IsinfOp",
|
|
"IsnanOp",
|
|
# --- independent (custom-signature, 11) ---
|
|
"LeakyReluOp",
|
|
"EluOp",
|
|
"HardtanhOp",
|
|
"SoftplusOp",
|
|
"PreluOp",
|
|
"WhereOp",
|
|
"ClampOp",
|
|
"MaskedFillOp",
|
|
"NanToNumOp",
|
|
"AlibiOp",
|
|
"SinusoidalOp",
|
|
]
|
|
|
|
|
|
def coalesce_broadcast_dims(a_shape, b_shape):
|
|
"""Coalesce N-dim broadcast into minimal effective dimensions.
|
|
|
|
Merges adjacent dimensions that have the same broadcast behaviour
|
|
(both real or both broadcast) to minimise the number of divmod
|
|
operations inside the kernel loop.
|
|
|
|
Args:
|
|
a_shape: Shape tuple of input a.
|
|
b_shape: Shape tuple of input b.
|
|
|
|
Returns:
|
|
Tuple of (out_shape, coalesced_shape, a_strides, b_strides) where
|
|
strides use 0 for broadcast dimensions.
|
|
"""
|
|
# Normalise scalar (0-dim) inputs to 1-dim with size 1
|
|
if len(a_shape) == 0:
|
|
a_shape = (1,)
|
|
if len(b_shape) == 0:
|
|
b_shape = (1,)
|
|
|
|
out_shape = torch.broadcast_shapes(a_shape, b_shape)
|
|
ndim = len(out_shape)
|
|
a_pad = (1,) * (ndim - len(a_shape)) + tuple(a_shape)
|
|
b_pad = (1,) * (ndim - len(b_shape)) + tuple(b_shape)
|
|
|
|
def _make_strides(padded_shape):
|
|
strides = [1] * ndim
|
|
for i in range(ndim - 2, -1, -1):
|
|
strides[i] = strides[i + 1] * padded_shape[i + 1]
|
|
# Only zero strides for genuinely broadcast dims (size-1 expanded to >1)
|
|
return [
|
|
0 if padded_shape[i] == 1 and out_shape[i] > 1 else strides[i]
|
|
for i in range(ndim)
|
|
]
|
|
|
|
a_raw = _make_strides(a_pad)
|
|
b_raw = _make_strides(b_pad)
|
|
|
|
# Coalesce adjacent dims with compatible broadcast patterns
|
|
groups = [(out_shape[0], a_raw[0], b_raw[0])]
|
|
for i in range(1, ndim):
|
|
prev_out, prev_as, prev_bs = groups[-1]
|
|
a_can = (a_raw[i] == 0 and prev_as == 0) or (
|
|
a_raw[i] != 0 and prev_as == a_raw[i] * out_shape[i]
|
|
)
|
|
b_can = (b_raw[i] == 0 and prev_bs == 0) or (
|
|
b_raw[i] != 0 and prev_bs == b_raw[i] * out_shape[i]
|
|
)
|
|
if a_can and b_can:
|
|
groups[-1] = (prev_out * out_shape[i], a_raw[i], b_raw[i])
|
|
else:
|
|
groups.append((out_shape[i], a_raw[i], b_raw[i]))
|
|
|
|
# Remove trivial size-1 groups (unless all trivial)
|
|
groups = [g for g in groups if g[0] > 1] or [(1, 0, 0)]
|
|
coalesced_shape = tuple(g[0] for g in groups)
|
|
a_strides = tuple(g[1] for g in groups)
|
|
b_strides = tuple(g[2] for g in groups)
|
|
return out_shape, coalesced_shape, a_strides, b_strides
|
|
|
|
|
|
def _apply_fp8_post_cast(result: torch.Tensor, kernel) -> torch.Tensor:
|
|
"""Apply fp8 output cast if the kernel requires it.
|
|
|
|
For e5m2 dtypes the kernel produces fp16 output to preserve Inf/NaN;
|
|
this helper performs the final non-saturating cast via PyTorch.
|
|
"""
|
|
fp8_out = getattr(kernel, "_fp8_output_dtype", None)
|
|
if fp8_out is not None:
|
|
return result.to(fp8_out)
|
|
return result
|
|
|
|
|
|
class UnaryOp(Op):
|
|
"""Template base class for unary elementwise ops.
|
|
|
|
Subclass must set ``kernel_cls`` and ``_op_name``.
|
|
Subclass should also set ``_wrapped`` via ``_register_unary_custom_op``
|
|
to enable torch.compile support.
|
|
|
|
Args:
|
|
N_total: Total number of elements (flattened).
|
|
dtype: Torch dtype.
|
|
strategy: Kernel strategy override.
|
|
kernel_map: Optional kernel dispatch override.
|
|
tune: Whether to autotune.
|
|
"""
|
|
|
|
kernel_cls: type
|
|
_op_name: str
|
|
_wrapped = None # Set by _register_unary_custom_op at class definition
|
|
|
|
def __init__(
|
|
self,
|
|
N_total: int,
|
|
dtype: torch.dtype,
|
|
strategy: Optional[str] = None,
|
|
kernel_map: Optional[Dict[str, Kernel]] = None,
|
|
tune: bool = False,
|
|
):
|
|
self.N_total = N_total
|
|
self.dtype = dtype
|
|
self.strategy = strategy
|
|
self.dispatch_kernel(kernel_map)
|
|
self.kernel = self.kernel_map[self._op_name](
|
|
N_total, dtype, strategy=strategy, tune=tune,
|
|
)
|
|
# Use _fp8_output_dtype (the final dtype after Op-layer post-cast)
|
|
# rather than kernel.output_dtype (which is fp16 for e5m2).
|
|
fp8_out = getattr(self.kernel, "_fp8_output_dtype", None)
|
|
self.output_dtype = fp8_out or getattr(self.kernel, "output_dtype", dtype)
|
|
# Register in global registry for torch.compile dispatch
|
|
self._instance_key = id(self)
|
|
_OP_REGISTRY[self._instance_key] = self
|
|
|
|
@property
|
|
def default_kernel_map(self) -> Dict[str, Kernel]:
|
|
return {self._op_name: self.kernel_cls}
|
|
|
|
@property
|
|
def total_memory(self) -> float:
|
|
"""Read x + write y."""
|
|
return self.N_total * (self.dtype.itemsize + self.output_dtype.itemsize)
|
|
|
|
def _eager_forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
"""Direct kernel call for use inside custom_op implementation."""
|
|
orig_shape = x.shape
|
|
x = x.contiguous().reshape(-1)
|
|
result = self.kernel(x).reshape(orig_shape)
|
|
# For e5m2: kernel produces fp16 to preserve Inf/NaN;
|
|
# cast to e5m2 here using PyTorch's non-saturating conversion.
|
|
return _apply_fp8_post_cast(result, self.kernel)
|
|
|
|
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
if not x.is_cuda:
|
|
raise ValueError("Input must be a CUDA tensor")
|
|
if x.dtype != self.dtype:
|
|
raise ValueError(f"Expected x.dtype {self.dtype}, got {x.dtype}")
|
|
if x.numel() != self.N_total:
|
|
raise ValueError(
|
|
f"Expected {self.N_total} elements, got {x.numel()}"
|
|
)
|
|
wrapped = type(self)._wrapped
|
|
if wrapped is not None:
|
|
return wrapped(x, self._instance_key)
|
|
return self._eager_forward(x)
|
|
|
|
|
|
class BinaryOp(Op):
|
|
"""Template base class for binary elementwise ops with broadcast.
|
|
|
|
Subclass must set ``kernel_cls`` and ``_op_name``.
|
|
Subclass should also set ``_wrapped`` via ``_register_binary_custom_op``
|
|
to enable torch.compile support.
|
|
|
|
Args:
|
|
a_shape: Shape of input a.
|
|
b_shape: Shape of input b.
|
|
dtype: Torch dtype.
|
|
strategy: Kernel strategy override.
|
|
kernel_map: Optional kernel dispatch override.
|
|
tune: Whether to autotune.
|
|
"""
|
|
|
|
kernel_cls: type
|
|
_op_name: str
|
|
_wrapped = None # Set by _register_binary_custom_op at class definition
|
|
|
|
def __init__(
|
|
self,
|
|
a_shape: tuple,
|
|
b_shape: tuple,
|
|
dtype: torch.dtype,
|
|
strategy: Optional[str] = None,
|
|
kernel_map: Optional[Dict[str, Kernel]] = None,
|
|
tune: bool = False,
|
|
):
|
|
supported = self.kernel_cls.SUPPORTED_DTYPES
|
|
if supported is not None and dtype not in supported:
|
|
names = ", ".join(str(dt) for dt in supported)
|
|
raise ValueError(
|
|
f"{self._op_name} does not support dtype {dtype}. "
|
|
f"Supported: [{names}]"
|
|
)
|
|
self.dtype = dtype
|
|
self.a_shape = tuple(a_shape)
|
|
self.b_shape = tuple(b_shape)
|
|
self.strategy = strategy
|
|
out_shape, coalesced_shape, a_strides, b_strides = coalesce_broadcast_dims(
|
|
a_shape, b_shape,
|
|
)
|
|
self.out_shape = out_shape
|
|
self._out_shape_list = list(out_shape) # cached for custom_op hot path
|
|
self.N_total = prod(out_shape)
|
|
self.a_numel = prod(a_shape)
|
|
self.b_numel = prod(b_shape)
|
|
self.dispatch_kernel(kernel_map)
|
|
self.kernel = self.kernel_map[self._op_name](
|
|
self.N_total, dtype, coalesced_shape, a_strides, b_strides,
|
|
self.a_numel, self.b_numel, strategy=strategy, tune=tune,
|
|
)
|
|
# Register in global registry for torch.compile dispatch
|
|
self._instance_key = id(self)
|
|
_OP_REGISTRY[self._instance_key] = self
|
|
|
|
@property
|
|
def default_kernel_map(self) -> Dict[str, Kernel]:
|
|
return {self._op_name: self.kernel_cls}
|
|
|
|
@property
|
|
def total_memory(self) -> float:
|
|
"""Read a + read b + write y."""
|
|
in_elem = self.dtype.itemsize
|
|
fp8_out = getattr(self.kernel, "_fp8_output_dtype", None)
|
|
out_elem = fp8_out.itemsize if fp8_out is not None else in_elem
|
|
return (self.a_numel + self.b_numel) * in_elem + self.N_total * out_elem
|
|
|
|
def _eager_forward(self, a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
|
|
"""Direct kernel call for use inside custom_op implementation."""
|
|
result = self.kernel(
|
|
a.contiguous().view(-1), b.contiguous().view(-1),
|
|
).reshape(self.out_shape)
|
|
return _apply_fp8_post_cast(result, self.kernel)
|
|
|
|
def forward(self, a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
|
|
if not a.is_cuda or not b.is_cuda:
|
|
raise ValueError("Inputs must be CUDA tensors")
|
|
if a.dtype != self.dtype:
|
|
raise ValueError(f"Expected a.dtype {self.dtype}, got {a.dtype}")
|
|
if b.dtype != self.dtype:
|
|
raise ValueError(f"Expected b.dtype {self.dtype}, got {b.dtype}")
|
|
if a.numel() != self.a_numel:
|
|
raise ValueError(
|
|
f"Expected a to have {self.a_numel} elements, got {a.numel()}"
|
|
)
|
|
if b.numel() != self.b_numel:
|
|
raise ValueError(
|
|
f"Expected b to have {self.b_numel} elements, got {b.numel()}"
|
|
)
|
|
wrapped = type(self)._wrapped
|
|
if wrapped is not None:
|
|
return wrapped(a, b, self._out_shape_list, self._instance_key)
|
|
return self._eager_forward(a, b)
|
|
|
|
|
|
class FusedGatedOp(Op):
|
|
"""Template base class for fused gated elementwise ops.
|
|
|
|
Input: x of shape (M, 2*N). gate = x[:, :N], value = x[:, N:].
|
|
Output: y = activation(gate) * value, shape (M, N).
|
|
|
|
Subclass must set ``kernel_cls`` and ``_op_name``.
|
|
Subclass should also set ``_wrapped`` via ``_register_fused_gated_custom_op``
|
|
to enable torch.compile support.
|
|
|
|
Args:
|
|
M: Number of rows.
|
|
N: Half column dim (output width).
|
|
dtype: Torch dtype.
|
|
strategy: Kernel strategy override.
|
|
kernel_map: Optional kernel dispatch override.
|
|
tune: Whether to autotune.
|
|
"""
|
|
|
|
kernel_cls: type
|
|
_op_name: str
|
|
_wrapped = None # Set by _register_fused_gated_custom_op at class definition
|
|
|
|
def __init__(
|
|
self,
|
|
M: int,
|
|
N: int,
|
|
dtype: torch.dtype,
|
|
strategy: Optional[str] = None,
|
|
kernel_map: Optional[Dict[str, Kernel]] = None,
|
|
tune: bool = False,
|
|
):
|
|
supported = self.kernel_cls.SUPPORTED_DTYPES
|
|
if supported is not None and dtype not in supported:
|
|
names = ", ".join(str(dt) for dt in supported)
|
|
raise ValueError(
|
|
f"{self._op_name} does not support dtype {dtype}. "
|
|
f"Supported: [{names}]"
|
|
)
|
|
self.M = M
|
|
self.N = N
|
|
self.dtype = dtype
|
|
self.strategy = strategy
|
|
self.dispatch_kernel(kernel_map)
|
|
self.kernel = self.kernel_map[self._op_name](
|
|
M, N, dtype, strategy=strategy, tune=tune,
|
|
)
|
|
# Register in global registry for torch.compile dispatch
|
|
self._instance_key = id(self)
|
|
_OP_REGISTRY[self._instance_key] = self
|
|
|
|
@property
|
|
def default_kernel_map(self) -> Dict[str, Kernel]:
|
|
return {self._op_name: self.kernel_cls}
|
|
|
|
@property
|
|
def total_memory(self) -> float:
|
|
"""Read x (M*2N) + write y (M*N)."""
|
|
in_elem = self.dtype.itemsize
|
|
fp8_out = getattr(self.kernel, "_fp8_output_dtype", None)
|
|
out_elem = fp8_out.itemsize if fp8_out is not None else in_elem
|
|
return self.M * 2 * self.N * in_elem + self.M * self.N * out_elem
|
|
|
|
def _eager_forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
"""Direct kernel call for use inside custom_op implementation."""
|
|
x = x.contiguous()
|
|
result = self.kernel(x)
|
|
return _apply_fp8_post_cast(result, self.kernel)
|
|
|
|
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
if not x.is_cuda:
|
|
raise ValueError("Input must be a CUDA tensor")
|
|
if x.dtype != self.dtype:
|
|
raise ValueError(f"Expected x.dtype {self.dtype}, got {x.dtype}")
|
|
if x.shape != (self.M, 2 * self.N):
|
|
raise ValueError(
|
|
f"Expected shape ({self.M}, {2 * self.N}), got {tuple(x.shape)}"
|
|
)
|
|
wrapped = type(self)._wrapped
|
|
if wrapped is not None:
|
|
return wrapped(x, self.M, self.N, self._instance_key)
|
|
return self._eager_forward(x)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Concrete op subclasses
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class ReluOp(UnaryOp):
|
|
"""ReLU activation: y = max(x, 0)."""
|
|
|
|
_op_name = "relu"
|
|
kernel_cls = ReluKernel
|
|
|
|
|
|
class AddOp(BinaryOp):
|
|
"""Element-wise addition with broadcast: y = a + b."""
|
|
|
|
_op_name = "add"
|
|
kernel_cls = AddKernel
|
|
|
|
|
|
class SubOp(BinaryOp):
|
|
"""Element-wise subtraction with broadcast: y = a - b."""
|
|
|
|
_op_name = "sub"
|
|
kernel_cls = SubKernel
|
|
|
|
|
|
class MulOp(BinaryOp):
|
|
"""Element-wise multiplication with broadcast: y = a * b."""
|
|
|
|
_op_name = "mul"
|
|
kernel_cls = MulKernel
|
|
|
|
|
|
class DivOp(BinaryOp):
|
|
"""Element-wise division with broadcast: y = a / b."""
|
|
|
|
_op_name = "div"
|
|
kernel_cls = DivKernel
|
|
|
|
|
|
class RemainderOp(BinaryOp):
|
|
"""Element-wise remainder with broadcast: y = a % b."""
|
|
|
|
_op_name = "remainder"
|
|
kernel_cls = RemainderKernel
|
|
|
|
|
|
class PowOp(BinaryOp):
|
|
"""Element-wise power with broadcast: y = a ** b."""
|
|
|
|
_op_name = "pow"
|
|
kernel_cls = PowKernel
|
|
|
|
|
|
class FloorDivideOp(BinaryOp):
|
|
"""Element-wise floor division with broadcast: y = floor(a / b)."""
|
|
|
|
_op_name = "floor_divide"
|
|
kernel_cls = FloorDivideKernel
|
|
|
|
|
|
class LerpOp(BinaryOp):
|
|
"""Element-wise lerp with broadcast: y = a + weight * (b - a).
|
|
|
|
Unlike ``torch.lerp(a, b, weight)`` where weight is a runtime parameter,
|
|
here weight is a **construction-time constant** baked into the compiled
|
|
kernel. This enables compile-time folding but means a new Op instance is
|
|
needed for each distinct weight value.
|
|
|
|
Args:
|
|
a_shape: Shape of input a.
|
|
b_shape: Shape of input b.
|
|
dtype: Torch dtype.
|
|
weight: Scalar interpolation weight, fixed at construction (default 0.5).
|
|
strategy: Kernel strategy override.
|
|
kernel_map: Optional kernel dispatch override.
|
|
tune: Whether to autotune.
|
|
"""
|
|
|
|
_op_name = "lerp"
|
|
kernel_cls = LerpKernel
|
|
|
|
def __init__(
|
|
self,
|
|
a_shape: tuple,
|
|
b_shape: tuple,
|
|
dtype: torch.dtype,
|
|
weight: float = 0.5,
|
|
strategy: Optional[str] = None,
|
|
kernel_map: Optional[Dict[str, Kernel]] = None,
|
|
tune: bool = False,
|
|
):
|
|
supported = self.kernel_cls.SUPPORTED_DTYPES
|
|
if supported is not None and dtype not in supported:
|
|
names = ", ".join(str(dt) for dt in supported)
|
|
raise ValueError(
|
|
f"{self._op_name} does not support dtype {dtype}. "
|
|
f"Supported: [{names}]"
|
|
)
|
|
self.dtype = dtype
|
|
self.a_shape = tuple(a_shape)
|
|
self.b_shape = tuple(b_shape)
|
|
self.strategy = strategy
|
|
self._weight = weight
|
|
out_shape, coalesced_shape, a_strides, b_strides = coalesce_broadcast_dims(
|
|
a_shape, b_shape,
|
|
)
|
|
self.out_shape = out_shape
|
|
self._out_shape_list = list(out_shape) # cached for custom_op hot path
|
|
self.N_total = prod(out_shape)
|
|
self.a_numel = prod(a_shape)
|
|
self.b_numel = prod(b_shape)
|
|
self.dispatch_kernel(kernel_map)
|
|
self.kernel = self.kernel_map[self._op_name](
|
|
self.N_total, dtype, coalesced_shape, a_strides, b_strides,
|
|
self.a_numel, self.b_numel, strategy=strategy, tune=tune,
|
|
weight=weight,
|
|
)
|
|
# Register in global registry for torch.compile dispatch
|
|
self._instance_key = id(self)
|
|
_OP_REGISTRY[self._instance_key] = self
|
|
|
|
|
|
class MaximumOp(BinaryOp):
|
|
"""Element-wise maximum with broadcast: y = max(a, b)."""
|
|
|
|
_op_name = "maximum"
|
|
kernel_cls = MaximumKernel
|
|
|
|
|
|
class MinimumOp(BinaryOp):
|
|
"""Element-wise minimum with broadcast: y = min(a, b)."""
|
|
|
|
_op_name = "minimum"
|
|
kernel_cls = MinimumKernel
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Comparison op subclasses (output bool)
|
|
# ---------------------------------------------------------------------------
|
|
#
|
|
# Kernels produce int8 (1/0) because TileLang cannot vectorize bool.
|
|
# The Op forward() casts to torch.bool after the kernel call.
|
|
|
|
|
|
class _BoolOutputBinaryOp(BinaryOp):
|
|
"""Mixin that casts kernel int8 output to torch.bool.
|
|
|
|
_eager_forward casts the int8 kernel output to bool. In the torch.compile
|
|
path, register_fake already declares torch.bool as the output dtype, and
|
|
the actual execution goes through _eager_forward which handles the cast.
|
|
No forward override is needed because BinaryOp.forward delegates to
|
|
_eager_forward (eager) or _wrapped (compile, where register_fake is correct).
|
|
"""
|
|
|
|
def _eager_forward(self, a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
|
|
result = super()._eager_forward(a, b)
|
|
return result.to(torch.bool)
|
|
|
|
|
|
class EqOp(_BoolOutputBinaryOp):
|
|
"""Element-wise equality with broadcast: y = (a == b)."""
|
|
|
|
_op_name = "eq"
|
|
kernel_cls = EqKernel
|
|
|
|
|
|
class NeOp(_BoolOutputBinaryOp):
|
|
"""Element-wise not-equal with broadcast: y = (a != b)."""
|
|
|
|
_op_name = "ne"
|
|
kernel_cls = NeKernel
|
|
|
|
|
|
class GtOp(_BoolOutputBinaryOp):
|
|
"""Element-wise greater-than with broadcast: y = (a > b)."""
|
|
|
|
_op_name = "gt"
|
|
kernel_cls = GtKernel
|
|
|
|
|
|
class LtOp(_BoolOutputBinaryOp):
|
|
"""Element-wise less-than with broadcast: y = (a < b)."""
|
|
|
|
_op_name = "lt"
|
|
kernel_cls = LtKernel
|
|
|
|
|
|
class GeOp(_BoolOutputBinaryOp):
|
|
"""Element-wise greater-equal with broadcast: y = (a >= b)."""
|
|
|
|
_op_name = "ge"
|
|
kernel_cls = GeKernel
|
|
|
|
|
|
class LeOp(_BoolOutputBinaryOp):
|
|
"""Element-wise less-equal with broadcast: y = (a <= b)."""
|
|
|
|
_op_name = "le"
|
|
kernel_cls = LeKernel
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Logical op subclasses (output bool)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class LogicalAndOp(_BoolOutputBinaryOp):
|
|
"""Element-wise logical AND with broadcast using non-zero truthiness."""
|
|
|
|
_op_name = "logical_and"
|
|
kernel_cls = LogicalAndKernel
|
|
|
|
|
|
class LogicalOrOp(_BoolOutputBinaryOp):
|
|
"""Element-wise logical OR with broadcast using non-zero truthiness."""
|
|
|
|
_op_name = "logical_or"
|
|
kernel_cls = LogicalOrKernel
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Bitwise op subclasses
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class BitwiseAndOp(BinaryOp):
|
|
"""Element-wise bitwise AND with broadcast: y = a & b."""
|
|
|
|
_op_name = "bitwise_and"
|
|
kernel_cls = BitwiseAndKernel
|
|
|
|
|
|
class BitwiseOrOp(BinaryOp):
|
|
"""Element-wise bitwise OR with broadcast: y = a | b."""
|
|
|
|
_op_name = "bitwise_or"
|
|
kernel_cls = BitwiseOrKernel
|
|
|
|
|
|
class BitwiseXorOp(BinaryOp):
|
|
"""Element-wise bitwise XOR with broadcast: y = a ^ b."""
|
|
|
|
_op_name = "bitwise_xor"
|
|
kernel_cls = BitwiseXorKernel
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fused gated op subclasses
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class SiluAndMulOp(FusedGatedOp):
|
|
"""SiLU-and-Mul: y = silu(gate) * value."""
|
|
|
|
_op_name = "silu_and_mul"
|
|
kernel_cls = SiluAndMulKernel
|
|
|
|
|
|
class GeluAndMulOp(FusedGatedOp):
|
|
"""GELU-and-Mul: y = gelu(gate) * value (exact GELU)."""
|
|
|
|
_op_name = "gelu_and_mul"
|
|
kernel_cls = GeluAndMulKernel
|
|
|
|
|
|
class GeluTanhAndMulOp(FusedGatedOp):
|
|
"""GELU-Tanh-and-Mul: y = gelu_tanh(gate) * value (tanh approximation)."""
|
|
|
|
_op_name = "gelu_tanh_and_mul"
|
|
kernel_cls = GeluTanhAndMulKernel
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Unary math ops (17)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class ExpOp(UnaryOp):
|
|
"""Element-wise exp(x)."""
|
|
|
|
_op_name = "exp"
|
|
kernel_cls = ExpKernel
|
|
|
|
|
|
class LogOp(UnaryOp):
|
|
"""Element-wise log(x)."""
|
|
|
|
_op_name = "log"
|
|
kernel_cls = LogKernel
|
|
|
|
|
|
class SqrtOp(UnaryOp):
|
|
"""Element-wise sqrt(x)."""
|
|
|
|
_op_name = "sqrt"
|
|
kernel_cls = SqrtKernel
|
|
|
|
|
|
class RsqrtOp(UnaryOp):
|
|
"""Element-wise 1/sqrt(x)."""
|
|
|
|
_op_name = "rsqrt"
|
|
kernel_cls = RsqrtKernel
|
|
|
|
|
|
class AbsOp(UnaryOp):
|
|
"""Element-wise |x|."""
|
|
|
|
_op_name = "abs"
|
|
kernel_cls = AbsKernel
|
|
|
|
|
|
class NegOp(UnaryOp):
|
|
"""Element-wise -x."""
|
|
|
|
_op_name = "neg"
|
|
kernel_cls = NegKernel
|
|
|
|
|
|
class ReciprocalOp(UnaryOp):
|
|
"""Element-wise 1/x."""
|
|
|
|
_op_name = "reciprocal"
|
|
kernel_cls = ReciprocalKernel
|
|
|
|
|
|
class SignOp(UnaryOp):
|
|
"""Element-wise sign(x): -1, 0, or +1."""
|
|
|
|
_op_name = "sign"
|
|
kernel_cls = SignKernel
|
|
|
|
|
|
class SinOp(UnaryOp):
|
|
"""Element-wise sin(x)."""
|
|
|
|
_op_name = "sin"
|
|
kernel_cls = SinKernel
|
|
|
|
|
|
class CosOp(UnaryOp):
|
|
"""Element-wise cos(x)."""
|
|
|
|
_op_name = "cos"
|
|
kernel_cls = CosKernel
|
|
|
|
|
|
class FloorOp(UnaryOp):
|
|
"""Element-wise floor(x)."""
|
|
|
|
_op_name = "floor"
|
|
kernel_cls = FloorKernel
|
|
|
|
|
|
class CeilOp(UnaryOp):
|
|
"""Element-wise ceil(x)."""
|
|
|
|
_op_name = "ceil"
|
|
kernel_cls = CeilKernel
|
|
|
|
|
|
class RoundOp(UnaryOp):
|
|
"""Element-wise round(x)."""
|
|
|
|
_op_name = "round"
|
|
kernel_cls = RoundKernel
|
|
|
|
|
|
class TruncOp(UnaryOp):
|
|
"""Element-wise trunc(x)."""
|
|
|
|
_op_name = "trunc"
|
|
kernel_cls = TruncKernel
|
|
|
|
|
|
class ErfOp(UnaryOp):
|
|
"""Element-wise erf(x)."""
|
|
|
|
_op_name = "erf"
|
|
kernel_cls = ErfKernel
|
|
|
|
|
|
class Log1pOp(UnaryOp):
|
|
"""Element-wise log(1 + x)."""
|
|
|
|
_op_name = "log1p"
|
|
kernel_cls = Log1pKernel
|
|
|
|
|
|
class Expm1Op(UnaryOp):
|
|
"""Element-wise exp(x) - 1."""
|
|
|
|
_op_name = "expm1"
|
|
kernel_cls = Expm1Kernel
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Activation ops (8)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class GeluOp(UnaryOp):
|
|
"""Element-wise GELU using the standard erf formulation."""
|
|
|
|
_op_name = "gelu"
|
|
kernel_cls = GeluKernel
|
|
|
|
|
|
class SiluOp(UnaryOp):
|
|
"""Element-wise SiLU (Swish): x * sigmoid(x)."""
|
|
|
|
_op_name = "silu"
|
|
kernel_cls = SiluKernel
|
|
|
|
|
|
class SigmoidOp(UnaryOp):
|
|
"""Element-wise sigmoid(x)."""
|
|
|
|
_op_name = "sigmoid"
|
|
kernel_cls = SigmoidKernel
|
|
|
|
|
|
class TanhOp(UnaryOp):
|
|
"""Element-wise tanh(x)."""
|
|
|
|
_op_name = "tanh"
|
|
kernel_cls = TanhKernel
|
|
|
|
|
|
class HardswishOp(UnaryOp):
|
|
"""Element-wise HardSwish: x * clamp(x + 3, 0, 6) / 6."""
|
|
|
|
_op_name = "hardswish"
|
|
kernel_cls = HardswishKernel
|
|
|
|
|
|
class HardsigmoidOp(UnaryOp):
|
|
"""Element-wise HardSigmoid: clamp(x + 3, 0, 6) / 6."""
|
|
|
|
_op_name = "hardsigmoid"
|
|
kernel_cls = HardsigmoidKernel
|
|
|
|
|
|
class MishOp(UnaryOp):
|
|
"""Element-wise Mish: x * tanh(softplus(x))."""
|
|
|
|
_op_name = "mish"
|
|
kernel_cls = MishKernel
|
|
|
|
|
|
class SeluOp(UnaryOp):
|
|
"""Element-wise SELU."""
|
|
|
|
_op_name = "selu"
|
|
kernel_cls = SeluKernel
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Logical op (1)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class LogicalNotOp(UnaryOp):
|
|
"""Element-wise logical NOT with bool output."""
|
|
|
|
_op_name = "logical_not"
|
|
kernel_cls = LogicalNotKernel
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Bitwise op (1)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class BitwiseNotOp(UnaryOp):
|
|
"""Element-wise bitwise NOT (~x) for bool/integer inputs."""
|
|
|
|
_op_name = "bitwise_not"
|
|
kernel_cls = BitwiseNotKernel
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Special predicate ops (3)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class IsnanOp(UnaryOp):
|
|
"""Element-wise isnan with bool output."""
|
|
|
|
_op_name = "isnan"
|
|
kernel_cls = IsnanKernel
|
|
|
|
|
|
class IsinfOp(UnaryOp):
|
|
"""Element-wise isinf with bool output."""
|
|
|
|
_op_name = "isinf"
|
|
kernel_cls = IsinfKernel
|
|
|
|
|
|
class IsfiniteOp(UnaryOp):
|
|
"""Element-wise isfinite with bool output."""
|
|
|
|
_op_name = "isfinite"
|
|
kernel_cls = IsfiniteKernel
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Independent (custom-signature) op classes (11)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class LeakyReluOp(Op):
|
|
"""Leaky ReLU: y = x if x > 0 else negative_slope * x.
|
|
|
|
Args:
|
|
N_total: Total number of elements (flattened).
|
|
dtype: Torch dtype.
|
|
negative_slope: Slope for negative inputs (default 0.01).
|
|
"""
|
|
|
|
_op_name = "leaky_relu"
|
|
_wrapped = None
|
|
|
|
def __init__(self, N_total: int, dtype: torch.dtype, negative_slope: float = 0.01):
|
|
_validate_scalar_param_repr("negative_slope", negative_slope, dtype, self._op_name)
|
|
self.N_total = N_total
|
|
self.dtype = dtype
|
|
self.negative_slope = negative_slope
|
|
self.kernel = LeakyReluKernel(N_total, dtype, negative_slope=negative_slope)
|
|
self._instance_key = id(self)
|
|
_OP_REGISTRY[self._instance_key] = self
|
|
|
|
@property
|
|
def default_kernel_map(self):
|
|
return {"leaky_relu": LeakyReluKernel}
|
|
|
|
def _eager_forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
orig_shape = x.shape
|
|
result = self.kernel(x.contiguous().reshape(-1)).reshape(orig_shape)
|
|
return _apply_fp8_post_cast(result, self.kernel)
|
|
|
|
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
if not x.is_cuda:
|
|
raise ValueError("Input must be a CUDA tensor")
|
|
if x.dtype != self.dtype:
|
|
raise ValueError(f"Expected x.dtype {self.dtype}, got {x.dtype}")
|
|
if x.numel() != self.N_total:
|
|
raise ValueError(f"Expected {self.N_total} elements, got {x.numel()}")
|
|
wrapped = type(self)._wrapped
|
|
if wrapped is not None:
|
|
return wrapped(x, self._instance_key)
|
|
return self._eager_forward(x)
|
|
|
|
|
|
class EluOp(Op):
|
|
"""ELU: y = x if x > 0 else alpha * (exp(x) - 1).
|
|
|
|
Args:
|
|
N_total: Total number of elements (flattened).
|
|
dtype: Torch dtype.
|
|
alpha: Scale for the negative part (default 1.0).
|
|
"""
|
|
|
|
_op_name = "elu"
|
|
_wrapped = None
|
|
|
|
def __init__(self, N_total: int, dtype: torch.dtype, alpha: float = 1.0):
|
|
_validate_scalar_param_repr("alpha", alpha, dtype, self._op_name)
|
|
self.N_total = N_total
|
|
self.dtype = dtype
|
|
self.alpha = alpha
|
|
self.kernel = EluKernel(N_total, dtype, alpha=alpha)
|
|
self._instance_key = id(self)
|
|
_OP_REGISTRY[self._instance_key] = self
|
|
|
|
@property
|
|
def default_kernel_map(self):
|
|
return {"elu": EluKernel}
|
|
|
|
def _eager_forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
orig_shape = x.shape
|
|
result = self.kernel(x.contiguous().reshape(-1)).reshape(orig_shape)
|
|
return _apply_fp8_post_cast(result, self.kernel)
|
|
|
|
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
if not x.is_cuda:
|
|
raise ValueError("Input must be a CUDA tensor")
|
|
if x.dtype != self.dtype:
|
|
raise ValueError(f"Expected x.dtype {self.dtype}, got {x.dtype}")
|
|
if x.numel() != self.N_total:
|
|
raise ValueError(f"Expected {self.N_total} elements, got {x.numel()}")
|
|
wrapped = type(self)._wrapped
|
|
if wrapped is not None:
|
|
return wrapped(x, self._instance_key)
|
|
return self._eager_forward(x)
|
|
|
|
|
|
class HardtanhOp(Op):
|
|
"""Hardtanh: y = clamp(x, min_val, max_val).
|
|
|
|
Args:
|
|
N_total: Total number of elements (flattened).
|
|
dtype: Torch dtype.
|
|
min_val: Lower bound (default -1.0).
|
|
max_val: Upper bound (default 1.0).
|
|
"""
|
|
|
|
_op_name = "hardtanh"
|
|
_wrapped = None
|
|
|
|
def __init__(self, N_total: int, dtype: torch.dtype,
|
|
min_val: float = -1.0, max_val: float = 1.0):
|
|
_validate_scalar_param_repr("min_val", min_val, dtype, self._op_name)
|
|
_validate_scalar_param_repr("max_val", max_val, dtype, self._op_name)
|
|
self.N_total = N_total
|
|
self.dtype = dtype
|
|
self.min_val = min_val
|
|
self.max_val = max_val
|
|
self.kernel = HardtanhKernel(N_total, dtype, min_val=min_val, max_val=max_val)
|
|
self._instance_key = id(self)
|
|
_OP_REGISTRY[self._instance_key] = self
|
|
|
|
@property
|
|
def default_kernel_map(self):
|
|
return {"hardtanh": HardtanhKernel}
|
|
|
|
def _eager_forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
orig_shape = x.shape
|
|
result = self.kernel(x.contiguous().reshape(-1)).reshape(orig_shape)
|
|
return _apply_fp8_post_cast(result, self.kernel)
|
|
|
|
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
if not x.is_cuda:
|
|
raise ValueError("Input must be a CUDA tensor")
|
|
if x.dtype != self.dtype:
|
|
raise ValueError(f"Expected x.dtype {self.dtype}, got {x.dtype}")
|
|
if x.numel() != self.N_total:
|
|
raise ValueError(f"Expected {self.N_total} elements, got {x.numel()}")
|
|
wrapped = type(self)._wrapped
|
|
if wrapped is not None:
|
|
return wrapped(x, self._instance_key)
|
|
return self._eager_forward(x)
|
|
|
|
|
|
class SoftplusOp(Op):
|
|
"""Softplus: y = log(1 + exp(x*beta))/beta if x*beta <= threshold else x.
|
|
|
|
Args:
|
|
N_total: Total number of elements (flattened).
|
|
dtype: Torch dtype.
|
|
beta: Scaling factor (default 1.0).
|
|
threshold: Linear regime threshold (default 20.0).
|
|
"""
|
|
|
|
_op_name = "softplus"
|
|
_wrapped = None
|
|
|
|
def __init__(self, N_total: int, dtype: torch.dtype,
|
|
beta: float = 1.0, threshold: float = 20.0):
|
|
_validate_scalar_param_repr("beta", beta, dtype, self._op_name)
|
|
_validate_scalar_param_repr("threshold", threshold, dtype, self._op_name)
|
|
self.N_total = N_total
|
|
self.dtype = dtype
|
|
self.beta = beta
|
|
self.threshold = threshold
|
|
self.kernel = SoftplusKernel(N_total, dtype, beta=beta, threshold=threshold)
|
|
self._instance_key = id(self)
|
|
_OP_REGISTRY[self._instance_key] = self
|
|
|
|
@property
|
|
def default_kernel_map(self):
|
|
return {"softplus": SoftplusKernel}
|
|
|
|
def _eager_forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
orig_shape = x.shape
|
|
result = self.kernel(x.contiguous().reshape(-1)).reshape(orig_shape)
|
|
return _apply_fp8_post_cast(result, self.kernel)
|
|
|
|
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
if not x.is_cuda:
|
|
raise ValueError("Input must be a CUDA tensor")
|
|
if x.dtype != self.dtype:
|
|
raise ValueError(f"Expected x.dtype {self.dtype}, got {x.dtype}")
|
|
if x.numel() != self.N_total:
|
|
raise ValueError(f"Expected {self.N_total} elements, got {x.numel()}")
|
|
wrapped = type(self)._wrapped
|
|
if wrapped is not None:
|
|
return wrapped(x, self._instance_key)
|
|
return self._eager_forward(x)
|
|
|
|
|
|
class PreluOp(Op):
|
|
"""PReLU: y = x if x > 0 else weight[channel] * x.
|
|
|
|
Channel dimension follows PyTorch convention: dimension 1 for inputs
|
|
with ndim >= 2, dimension 0 for 1-D inputs.
|
|
|
|
Args:
|
|
shape: Shape of the input tensor (must have a channel dimension).
|
|
dtype: Torch dtype.
|
|
num_channels: Number of channels (weight length).
|
|
"""
|
|
|
|
_op_name = "prelu"
|
|
_wrapped = None
|
|
|
|
def __init__(self, shape: tuple, dtype: torch.dtype, num_channels: int):
|
|
self.shape = shape
|
|
self.dtype = dtype
|
|
self.num_channels = num_channels
|
|
N_total = prod(shape)
|
|
self.N_total = N_total
|
|
# PyTorch PReLU: channel dim is 1 for ndim>=2, else 0
|
|
inner_size = (prod(shape[2:]) if len(shape) > 2 else 1) if len(shape) >= 2 else 1
|
|
self.inner_size = inner_size
|
|
self.kernel = PreluKernel(N_total, num_channels, inner_size, dtype)
|
|
self._instance_key = id(self)
|
|
_OP_REGISTRY[self._instance_key] = self
|
|
|
|
@property
|
|
def default_kernel_map(self):
|
|
return {"prelu": PreluKernel}
|
|
|
|
def _eager_forward(self, x: torch.Tensor, weight: torch.Tensor) -> torch.Tensor:
|
|
orig_shape = x.shape
|
|
result = self.kernel(
|
|
x.contiguous().reshape(-1), weight.contiguous().reshape(-1),
|
|
).reshape(orig_shape)
|
|
return _apply_fp8_post_cast(result, self.kernel)
|
|
|
|
def forward(self, x: torch.Tensor, weight: torch.Tensor) -> torch.Tensor:
|
|
if not x.is_cuda:
|
|
raise ValueError("Input must be a CUDA tensor")
|
|
if x.dtype != self.dtype:
|
|
raise ValueError(f"Expected x.dtype {self.dtype}, got {x.dtype}")
|
|
if x.numel() != self.N_total:
|
|
raise ValueError(f"Expected {self.N_total} elements, got {x.numel()}")
|
|
wrapped = type(self)._wrapped
|
|
if wrapped is not None:
|
|
return wrapped(x, weight, self._instance_key)
|
|
return self._eager_forward(x, weight)
|
|
|
|
|
|
class WhereOp(Op):
|
|
"""Where: out = cond ? x : y.
|
|
|
|
Args:
|
|
N_total: Total number of elements (flattened).
|
|
dtype: Torch dtype for x and y.
|
|
"""
|
|
|
|
_op_name = "where"
|
|
_wrapped = None
|
|
|
|
def __init__(self, N_total: int, dtype: torch.dtype):
|
|
self.N_total = N_total
|
|
self.dtype = dtype
|
|
self.kernel = WhereKernel(N_total, dtype)
|
|
self._instance_key = id(self)
|
|
_OP_REGISTRY[self._instance_key] = self
|
|
|
|
@property
|
|
def default_kernel_map(self):
|
|
return {"where": WhereKernel}
|
|
|
|
def _eager_forward(
|
|
self, cond: torch.Tensor, x: torch.Tensor, y: torch.Tensor,
|
|
) -> torch.Tensor:
|
|
orig_shape = x.shape
|
|
# Fast path: cast to bool if needed, then flatten + pack to uint8
|
|
# for vectorized T.copy in the kernel.
|
|
cond_flat = (cond if cond.dtype == torch.bool else cond.bool()).contiguous().view(-1)
|
|
x_flat = x.contiguous().view(-1)
|
|
y_flat = y.contiguous().view(-1)
|
|
return self.kernel(cond_flat.view(torch.uint8), x_flat, y_flat).view(orig_shape)
|
|
|
|
def forward(
|
|
self, cond: torch.Tensor, x: torch.Tensor, y: torch.Tensor,
|
|
) -> torch.Tensor:
|
|
if not x.is_cuda:
|
|
raise ValueError("Input must be a CUDA tensor")
|
|
if x.dtype != self.dtype:
|
|
raise ValueError(f"Expected x.dtype {self.dtype}, got {x.dtype}")
|
|
if x.numel() != self.N_total:
|
|
raise ValueError(f"Expected {self.N_total} elements, got {x.numel()}")
|
|
wrapped = type(self)._wrapped
|
|
if wrapped is not None:
|
|
return wrapped(cond, x, y, self._instance_key)
|
|
return self._eager_forward(cond, x, y)
|
|
|
|
|
|
class ClampOp(Op):
|
|
"""Clamp: y = clamp(x, min, max) with optional bounds.
|
|
|
|
Args:
|
|
N_total: Total number of elements (flattened).
|
|
dtype: Torch dtype.
|
|
min_val: Lower bound (None = no lower bound).
|
|
max_val: Upper bound (None = no upper bound).
|
|
"""
|
|
|
|
_op_name = "clamp"
|
|
_wrapped = None
|
|
|
|
def __init__(self, N_total: int, dtype: torch.dtype,
|
|
min_val: Optional[float] = None, max_val: Optional[float] = None):
|
|
if min_val is not None:
|
|
_validate_scalar_param_repr("min_val", min_val, dtype, self._op_name)
|
|
if max_val is not None:
|
|
_validate_scalar_param_repr("max_val", max_val, dtype, self._op_name)
|
|
self.N_total = N_total
|
|
self.dtype = dtype
|
|
self.min_val = min_val
|
|
self.max_val = max_val
|
|
self.kernel = ClampKernel(N_total, dtype, min_val=min_val, max_val=max_val)
|
|
self._instance_key = id(self)
|
|
_OP_REGISTRY[self._instance_key] = self
|
|
|
|
@property
|
|
def default_kernel_map(self):
|
|
return {"clamp": ClampKernel}
|
|
|
|
def _eager_forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
orig_shape = x.shape
|
|
result = self.kernel(x.contiguous().reshape(-1)).reshape(orig_shape)
|
|
return _apply_fp8_post_cast(result, self.kernel)
|
|
|
|
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
if not x.is_cuda:
|
|
raise ValueError("Input must be a CUDA tensor")
|
|
if x.dtype != self.dtype:
|
|
raise ValueError(f"Expected x.dtype {self.dtype}, got {x.dtype}")
|
|
if x.numel() != self.N_total:
|
|
raise ValueError(f"Expected {self.N_total} elements, got {x.numel()}")
|
|
wrapped = type(self)._wrapped
|
|
if wrapped is not None:
|
|
return wrapped(x, self._instance_key)
|
|
return self._eager_forward(x)
|
|
|
|
|
|
class MaskedFillOp(Op):
|
|
"""MaskedFill: out = mask ? fill_value : x.
|
|
|
|
Args:
|
|
N_total: Total number of elements (flattened).
|
|
dtype: Torch dtype.
|
|
fill_value: Scalar value to fill where mask is True.
|
|
"""
|
|
|
|
_op_name = "masked_fill"
|
|
_wrapped = None
|
|
|
|
def __init__(self, N_total: int, dtype: torch.dtype, fill_value: float):
|
|
_validate_scalar_param_repr("fill_value", fill_value, dtype, self._op_name)
|
|
self.N_total = N_total
|
|
self.dtype = dtype
|
|
self.fill_value = fill_value
|
|
self.kernel = MaskedFillKernel(N_total, dtype, fill_value)
|
|
self._instance_key = id(self)
|
|
_OP_REGISTRY[self._instance_key] = self
|
|
|
|
@property
|
|
def default_kernel_map(self):
|
|
return {"masked_fill": MaskedFillKernel}
|
|
|
|
def _eager_forward(self, x: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
|
orig_shape = x.shape
|
|
# Fast path: cast to bool if needed, then flatten + pack to uint8
|
|
# for vectorized T.copy in the kernel.
|
|
mask_flat = (mask if mask.dtype == torch.bool else mask.bool()).contiguous().view(-1)
|
|
x_flat = x.contiguous().view(-1)
|
|
result = self.kernel(x_flat, mask_flat.view(torch.uint8)).view(orig_shape)
|
|
return _apply_fp8_post_cast(result, self.kernel)
|
|
|
|
def forward(self, x: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
|
if not x.is_cuda:
|
|
raise ValueError("Input must be a CUDA tensor")
|
|
if x.dtype != self.dtype:
|
|
raise ValueError(f"Expected x.dtype {self.dtype}, got {x.dtype}")
|
|
if x.numel() != self.N_total:
|
|
raise ValueError(f"Expected {self.N_total} elements, got {x.numel()}")
|
|
if not mask.is_cuda:
|
|
raise ValueError("Mask must be a CUDA tensor")
|
|
if mask.dtype != torch.bool:
|
|
raise ValueError(f"Expected mask.dtype torch.bool, got {mask.dtype}")
|
|
if mask.numel() != self.N_total:
|
|
raise ValueError(
|
|
f"Expected mask with {self.N_total} elements, got {mask.numel()}"
|
|
)
|
|
wrapped = type(self)._wrapped
|
|
if wrapped is not None:
|
|
return wrapped(x, mask, self._instance_key)
|
|
return self._eager_forward(x, mask)
|
|
|
|
|
|
class NanToNumOp(Op):
|
|
"""NanToNum: replace NaN, +Inf, -Inf with specified values.
|
|
|
|
Args:
|
|
N_total: Total number of elements (flattened).
|
|
dtype: Torch dtype.
|
|
nan_val: Replacement for NaN (default 0.0).
|
|
posinf_val: Replacement for +Inf (default 1e4).
|
|
neginf_val: Replacement for -Inf (default -1e4).
|
|
"""
|
|
|
|
_op_name = "nan_to_num"
|
|
_wrapped = None
|
|
|
|
def __init__(self, N_total: int, dtype: torch.dtype,
|
|
nan_val: float = 0.0, posinf_val: float = 1e4, neginf_val: float = -1e4):
|
|
_validate_scalar_param_repr("nan_val", nan_val, dtype, self._op_name)
|
|
_validate_scalar_param_repr("posinf_val", posinf_val, dtype, self._op_name)
|
|
_validate_scalar_param_repr("neginf_val", neginf_val, dtype, self._op_name)
|
|
self.N_total = N_total
|
|
self.dtype = dtype
|
|
self.nan_val = nan_val
|
|
self.posinf_val = posinf_val
|
|
self.neginf_val = neginf_val
|
|
self.kernel = NanToNumKernel(
|
|
N_total, dtype, nan_val=nan_val, posinf_val=posinf_val, neginf_val=neginf_val,
|
|
)
|
|
self._instance_key = id(self)
|
|
_OP_REGISTRY[self._instance_key] = self
|
|
|
|
@property
|
|
def default_kernel_map(self):
|
|
return {"nan_to_num": NanToNumKernel}
|
|
|
|
def _eager_forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
orig_shape = x.shape
|
|
result = self.kernel(x.contiguous().reshape(-1)).reshape(orig_shape)
|
|
return _apply_fp8_post_cast(result, self.kernel)
|
|
|
|
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
if not x.is_cuda:
|
|
raise ValueError("Input must be a CUDA tensor")
|
|
if x.dtype != self.dtype:
|
|
raise ValueError(f"Expected x.dtype {self.dtype}, got {x.dtype}")
|
|
if x.numel() != self.N_total:
|
|
raise ValueError(f"Expected {self.N_total} elements, got {x.numel()}")
|
|
wrapped = type(self)._wrapped
|
|
if wrapped is not None:
|
|
return wrapped(x, self._instance_key)
|
|
return self._eager_forward(x)
|
|
|
|
|
|
class AlibiOp(Op):
|
|
"""ALiBi position encoding: bias[h, i, j] = -slope_h * |i - j|.
|
|
|
|
Generates the full (num_heads, seq_len, seq_len) bias tensor.
|
|
|
|
Args:
|
|
seq_len: Sequence length.
|
|
num_heads: Number of attention heads.
|
|
dtype: Torch dtype.
|
|
"""
|
|
|
|
_op_name = "alibi"
|
|
_wrapped = None
|
|
|
|
def __init__(self, seq_len: int, num_heads: int, dtype: torch.dtype):
|
|
self.seq_len = seq_len
|
|
self.num_heads = num_heads
|
|
self.dtype = dtype
|
|
self.kernel = AlibiKernel(seq_len, num_heads, dtype)
|
|
# Scalar tensor used as device/dtype carrier for torch.compile tracing
|
|
self._device_carrier = torch.empty((), dtype=dtype, device="cuda")
|
|
self._instance_key = id(self)
|
|
_OP_REGISTRY[self._instance_key] = self
|
|
|
|
@property
|
|
def default_kernel_map(self):
|
|
return {"alibi": AlibiKernel}
|
|
|
|
def _eager_forward(self) -> torch.Tensor:
|
|
out = self.kernel()
|
|
result = out.reshape(self.num_heads, self.seq_len, self.seq_len)
|
|
return _apply_fp8_post_cast(result, self.kernel)
|
|
|
|
def forward(self) -> torch.Tensor:
|
|
wrapped = type(self)._wrapped
|
|
if wrapped is not None:
|
|
return wrapped(
|
|
self._device_carrier,
|
|
self.num_heads, self.seq_len,
|
|
self._instance_key,
|
|
)
|
|
return self._eager_forward()
|
|
|
|
|
|
class SinusoidalOp(Op):
|
|
"""Sinusoidal positional encoding from "Attention Is All You Need".
|
|
|
|
Generates the full (seq_len, d_model) encoding tensor.
|
|
|
|
Args:
|
|
seq_len: Sequence length.
|
|
d_model: Model dimension.
|
|
dtype: Torch dtype.
|
|
"""
|
|
|
|
_op_name = "sinusoidal"
|
|
_wrapped = None
|
|
|
|
def __init__(self, seq_len: int, d_model: int, dtype: torch.dtype):
|
|
self.seq_len = seq_len
|
|
self.d_model = d_model
|
|
self.dtype = dtype
|
|
self.kernel = SinusoidalKernel(seq_len, d_model, dtype)
|
|
# Scalar tensor used as device/dtype carrier for torch.compile tracing
|
|
self._device_carrier = torch.empty((), dtype=dtype, device="cuda")
|
|
self._instance_key = id(self)
|
|
_OP_REGISTRY[self._instance_key] = self
|
|
|
|
@property
|
|
def default_kernel_map(self):
|
|
return {"sinusoidal": SinusoidalKernel}
|
|
|
|
def _eager_forward(self) -> torch.Tensor:
|
|
out = self.kernel()
|
|
result = out.reshape(self.seq_len, self.d_model)
|
|
return _apply_fp8_post_cast(result, self.kernel)
|
|
|
|
def forward(self) -> torch.Tensor:
|
|
wrapped = type(self)._wrapped
|
|
if wrapped is not None:
|
|
return wrapped(
|
|
self._device_carrier,
|
|
self.seq_len, self.d_model,
|
|
self._instance_key,
|
|
)
|
|
return self._eager_forward()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# torch.compile registration for all 55 concrete ops
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# --- Unary ops: float-preserving output (1 + 17 + 8 + 1 = 27 ops) ---
|
|
for _cls in [
|
|
ReluOp,
|
|
# math (17)
|
|
ExpOp, LogOp, SqrtOp, RsqrtOp, AbsOp, NegOp, ReciprocalOp, SignOp,
|
|
SinOp, CosOp, FloorOp, CeilOp, RoundOp, TruncOp, ErfOp, Log1pOp, Expm1Op,
|
|
# activations (8)
|
|
GeluOp, SiluOp, SigmoidOp, TanhOp, HardswishOp, HardsigmoidOp, MishOp, SeluOp,
|
|
# bitwise (1) -- output same dtype as input
|
|
BitwiseNotOp,
|
|
]:
|
|
_register_unary_custom_op(_cls)
|
|
|
|
# --- Unary ops: bool output (4 ops) ---
|
|
for _cls in [LogicalNotOp, IsnanOp, IsinfOp, IsfiniteOp]:
|
|
_register_unary_custom_op(_cls, output_dtype_override=torch.bool)
|
|
|
|
# --- Binary ops: same-dtype output (10 + 3 = 13 ops) ---
|
|
for _cls in [
|
|
# arithmetic (10)
|
|
AddOp, SubOp, MulOp, DivOp, RemainderOp, PowOp, FloorDivideOp,
|
|
LerpOp, MaximumOp, MinimumOp,
|
|
# bitwise (3)
|
|
BitwiseAndOp, BitwiseOrOp, BitwiseXorOp,
|
|
]:
|
|
_register_binary_custom_op(_cls)
|
|
|
|
# --- Binary ops: bool output (comparison 6 + logical 2 = 8 ops) ---
|
|
for _cls in [
|
|
EqOp, NeOp, GtOp, LtOp, GeOp, LeOp,
|
|
LogicalAndOp, LogicalOrOp,
|
|
]:
|
|
_register_binary_custom_op(_cls, output_bool=True)
|
|
|
|
# --- Fused gated ops (3 ops) ---
|
|
for _cls in [SiluAndMulOp, GeluAndMulOp, GeluTanhAndMulOp]:
|
|
_register_fused_gated_custom_op(_cls)
|
|
|
|
# --- Independent unary-like ops (6 ops: x -> y with baked params) ---
|
|
for _cls in [
|
|
LeakyReluOp, EluOp, HardtanhOp, SoftplusOp, ClampOp, NanToNumOp,
|
|
]:
|
|
_register_unary_custom_op(_cls)
|
|
|
|
# --- PReLU op (1 op: x, weight -> y) ---
|
|
_register_prelu_custom_op(PreluOp)
|
|
|
|
# --- Where op (1 op: cond, x, y -> out) ---
|
|
_register_where_custom_op(WhereOp)
|
|
|
|
# --- MaskedFill op (1 op: x, mask -> y) ---
|
|
_register_masked_fill_custom_op(MaskedFillOp)
|
|
|
|
# --- Generative ops (2 ops: no tensor input -> out) ---
|
|
_register_generative_custom_op(
|
|
AlibiOp,
|
|
out_shape_fn=lambda carrier, num_heads, seq_len: carrier.new_empty(
|
|
(num_heads, seq_len, seq_len),
|
|
),
|
|
)
|
|
_register_generative_custom_op(
|
|
SinusoidalOp,
|
|
out_shape_fn=lambda carrier, seq_len, d_model: carrier.new_empty(
|
|
(seq_len, d_model),
|
|
),
|
|
)
|
|
|
|
# Clean up loop variable
|
|
del _cls
|