forked from ccf-ai-infra/TileOPs-Metax
321 lines
10 KiB
Python
321 lines
10 KiB
Python
"""Binary arithmetic elementwise ops with broadcasting."""
|
|
|
|
from math import prod
|
|
from typing import Dict, Optional
|
|
|
|
import torch
|
|
|
|
from tileops.kernels.elementwise import (
|
|
AddFwdKernel,
|
|
DivFwdKernel,
|
|
DivTruncFwdKernel,
|
|
FloorDivideFwdKernel,
|
|
LerpFwdKernel,
|
|
LerpTensorFwdKernel,
|
|
MaximumFwdKernel,
|
|
MinimumFwdKernel,
|
|
MulFwdKernel,
|
|
PowFwdKernel,
|
|
RemainderFwdKernel,
|
|
SubFwdKernel,
|
|
)
|
|
from tileops.kernels.kernel_base import Kernel
|
|
|
|
from ..op_base import Op
|
|
from ._base import (
|
|
_OP_REGISTRY,
|
|
BinaryOp,
|
|
_AlphaScaledBinaryOp,
|
|
coalesce_broadcast_dims,
|
|
)
|
|
|
|
|
|
class AddFwdOp(_AlphaScaledBinaryOp):
|
|
"""Element-wise addition with broadcast: y = input + alpha * other.
|
|
|
|
Conforms to ``torch.add(input, other, *, alpha=1)``. ``alpha`` is baked
|
|
into the kernel at construction time, so non-default ``alpha`` runs
|
|
through the same fast kernel as the default.
|
|
"""
|
|
|
|
_op_name = "add"
|
|
kernel_cls = AddFwdKernel
|
|
|
|
|
|
class SubFwdOp(_AlphaScaledBinaryOp):
|
|
"""Element-wise subtraction with broadcast: y = input - alpha * other.
|
|
|
|
Conforms to ``torch.sub(input, other, *, alpha=1)``. ``alpha`` is baked
|
|
into the kernel at construction time, so non-default ``alpha`` runs
|
|
through the same fast kernel as the default.
|
|
"""
|
|
|
|
_op_name = "sub"
|
|
kernel_cls = SubFwdKernel
|
|
|
|
|
|
class MulFwdOp(BinaryOp):
|
|
"""Element-wise multiplication with broadcast: y = input * other."""
|
|
|
|
_op_name = "mul"
|
|
kernel_cls = MulFwdKernel
|
|
|
|
|
|
_DIV_KERNEL_BY_ROUNDING_MODE = {
|
|
None: DivFwdKernel,
|
|
"trunc": DivTruncFwdKernel,
|
|
"floor": FloorDivideFwdKernel,
|
|
}
|
|
|
|
|
|
class DivFwdOp(BinaryOp):
|
|
"""Element-wise division with broadcast: y = input / other.
|
|
|
|
Conforms to ``torch.div(input, other, *, rounding_mode=None)``.
|
|
``rounding_mode`` accepts ``None`` (true division), ``"trunc"``
|
|
(truncation toward zero), or ``"floor"`` (floor division); each
|
|
value selects a dedicated kernel specialization. The leading ``*``
|
|
makes ``rounding_mode`` and the existing ``kernel_map`` / ``tune``
|
|
parameters keyword-only; only the positional triplet
|
|
``(a_shape, b_shape, dtype)`` is shared with ``BinaryOp``.
|
|
"""
|
|
|
|
_op_name = "div"
|
|
kernel_cls = DivFwdKernel
|
|
|
|
def __init__(
|
|
self,
|
|
a_shape: tuple,
|
|
b_shape: tuple,
|
|
dtype: torch.dtype,
|
|
*,
|
|
rounding_mode: Optional[str] = None,
|
|
kernel_map: Optional[Dict[str, Kernel]] = None,
|
|
tune: bool = False,
|
|
):
|
|
if rounding_mode not in _DIV_KERNEL_BY_ROUNDING_MODE:
|
|
raise ValueError(
|
|
f"DivFwdOp received rounding_mode={rounding_mode!r}; "
|
|
"manifest allows None, 'trunc', or 'floor'"
|
|
)
|
|
self.rounding_mode = rounding_mode
|
|
# ``self.kernel_cls`` becomes an instance attribute that shadows the
|
|
# class attribute so ``BinaryOp.default_kernel_map`` (and the
|
|
# SUPPORTED_DTYPES check in ``BinaryOp.__init__``) pick the variant
|
|
# matching ``rounding_mode``.
|
|
self.kernel_cls = _DIV_KERNEL_BY_ROUNDING_MODE[rounding_mode]
|
|
super().__init__(
|
|
a_shape, b_shape, dtype, kernel_map=kernel_map, tune=tune,
|
|
)
|
|
|
|
|
|
class RemainderFwdOp(BinaryOp):
|
|
"""Element-wise remainder with broadcast: y = a % b."""
|
|
|
|
_op_name = "remainder"
|
|
kernel_cls = RemainderFwdKernel
|
|
|
|
|
|
class PowFwdOp(BinaryOp):
|
|
"""Element-wise power with broadcast: y = input ** exponent.
|
|
|
|
Conforms to ``torch.pow(input, exponent)``: the second operand carries
|
|
the manifest-declared name ``exponent`` rather than the generic
|
|
``other`` so the L1 signature check matches the manifest.
|
|
"""
|
|
|
|
_op_name = "pow"
|
|
kernel_cls = PowFwdKernel
|
|
_other_name = "exponent"
|
|
|
|
|
|
class FloorDivideFwdOp(BinaryOp):
|
|
"""Element-wise floor division with broadcast: y = floor(a / b)."""
|
|
|
|
_op_name = "floor_divide"
|
|
kernel_cls = FloorDivideFwdKernel
|
|
|
|
|
|
class LerpFwdOp(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).
|
|
kernel_map: Optional kernel dispatch override.
|
|
tune: Whether to autotune.
|
|
"""
|
|
|
|
_op_name = "lerp"
|
|
kernel_cls = LerpFwdKernel
|
|
_other_name = "end"
|
|
|
|
def __init__(
|
|
self,
|
|
a_shape: tuple,
|
|
b_shape: tuple,
|
|
dtype: torch.dtype,
|
|
weight: float = 0.5,
|
|
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._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, tune=tune,
|
|
weight=weight,
|
|
)
|
|
# Register in global registry for torch.compile dispatch
|
|
self._instance_key = id(self)
|
|
_OP_REGISTRY[self._instance_key] = self
|
|
|
|
|
|
class MaximumFwdOp(BinaryOp):
|
|
"""Element-wise maximum with broadcast: y = max(a, b)."""
|
|
|
|
_op_name = "maximum"
|
|
kernel_cls = MaximumFwdKernel
|
|
|
|
|
|
class MinimumFwdOp(BinaryOp):
|
|
"""Element-wise minimum with broadcast: y = min(a, b)."""
|
|
|
|
_op_name = "minimum"
|
|
kernel_cls = MinimumFwdKernel
|
|
|
|
|
|
class LerpTensorFwdOp(Op):
|
|
"""Tensor-weight lerp: out = input + weight * (end - input).
|
|
|
|
Conforms to the Tensor-weight overload of ``torch.lerp`` —
|
|
``torch.lerp(input, end, weight: Tensor)`` where ``weight`` is a
|
|
Tensor that broadcasts together with ``input`` and ``end`` to the
|
|
output shape. The Op layer expands the three inputs to the broadcast
|
|
shape and dispatches the flat ``LerpTensorFwdKernel`` on
|
|
``N_total = product(broadcast_shape)`` elements. The scalar-weight
|
|
overload is handled separately by ``LerpFwdOp``.
|
|
|
|
Args:
|
|
input: Shape of the start tensor.
|
|
end: Shape of the end tensor.
|
|
weight: Shape of the per-element weight tensor.
|
|
dtype: Torch dtype for all three operands.
|
|
"""
|
|
|
|
_op_name = "lerp_tensor"
|
|
_wrapped = None
|
|
|
|
# Manifest declares all three operands as ``float16 | bfloat16 | float32``;
|
|
# fp8 dtypes are rejected at the op-layer signature so the impl matches
|
|
# the manifest contract (the kernel also rejects fp8 independently).
|
|
_SUPPORTED_DTYPES = (torch.float16, torch.bfloat16, torch.float32)
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
input: tuple,
|
|
end: tuple,
|
|
weight: tuple,
|
|
dtype: torch.dtype,
|
|
kernel_map: Optional[Dict[str, Kernel]] = None,
|
|
tune: bool = False,
|
|
):
|
|
if dtype not in self._SUPPORTED_DTYPES:
|
|
names = ", ".join(str(dt) for dt in self._SUPPORTED_DTYPES)
|
|
raise ValueError(
|
|
f"LerpTensorFwdOp does not support dtype {dtype}. "
|
|
f"Supported: [{names}]"
|
|
)
|
|
self.input_shape = tuple(input)
|
|
self.end_shape = tuple(end)
|
|
self.weight_shape = tuple(weight)
|
|
self.dtype = dtype
|
|
self.out_shape = tuple(
|
|
torch.broadcast_shapes(
|
|
self.input_shape, self.end_shape, self.weight_shape,
|
|
)
|
|
)
|
|
self.N_total = prod(self.out_shape) if self.out_shape else 1
|
|
self.dispatch_kernel(kernel_map)
|
|
self.kernel = self.kernel_map[self._op_name](
|
|
self.N_total, dtype, tune=tune,
|
|
)
|
|
self._instance_key = id(self)
|
|
_OP_REGISTRY[self._instance_key] = self
|
|
|
|
@property
|
|
def default_kernel_map(self) -> Dict[str, Kernel]:
|
|
return {"lerp_tensor": LerpTensorFwdKernel}
|
|
|
|
@staticmethod
|
|
def _expand_flat(t: torch.Tensor, target_shape: tuple) -> torch.Tensor:
|
|
"""Expand ``t`` to ``target_shape`` and return a contiguous flat view."""
|
|
if tuple(t.shape) != tuple(target_shape):
|
|
t = t.expand(target_shape)
|
|
return t.contiguous().view(-1)
|
|
|
|
def _eager_forward(
|
|
self,
|
|
input: torch.Tensor,
|
|
end: torch.Tensor,
|
|
weight: torch.Tensor,
|
|
) -> torch.Tensor:
|
|
out_shape = self.out_shape if self.out_shape else (1,)
|
|
a_flat = self._expand_flat(input, out_shape)
|
|
b_flat = self._expand_flat(end, out_shape)
|
|
w_flat = self._expand_flat(weight, out_shape)
|
|
result = self.kernel(a_flat, b_flat, w_flat)
|
|
return result.view(self.out_shape if self.out_shape else ())
|
|
|
|
def forward(
|
|
self,
|
|
input: torch.Tensor,
|
|
end: torch.Tensor,
|
|
weight: torch.Tensor,
|
|
) -> torch.Tensor:
|
|
if not (input.is_cuda and end.is_cuda and weight.is_cuda):
|
|
raise ValueError("Inputs must be CUDA tensors")
|
|
for name, t, expected in [
|
|
("input", input, self.input_shape),
|
|
("end", end, self.end_shape),
|
|
("weight", weight, self.weight_shape),
|
|
]:
|
|
if t.dtype != self.dtype:
|
|
raise ValueError(
|
|
f"Expected {name}.dtype {self.dtype}, got {t.dtype}"
|
|
)
|
|
if tuple(t.shape) != expected:
|
|
raise ValueError(
|
|
f"Expected {name}.shape {expected}, got {tuple(t.shape)}"
|
|
)
|
|
wrapped = type(self)._wrapped
|
|
if wrapped is not None:
|
|
return wrapped(input, end, weight, self._instance_key)
|
|
return self._eager_forward(input, end, weight)
|