Copy
Source code for tensorplay.library
"""Custom operator registration and compiler integration.
Four public layers are provided:
1. :func:`custom_op` / the :class:`Library` class register opaque custom
operators with device-specific kernels, optional fake (meta) kernels,
an autograd formula (:meth:`CustomOpDef.register_autograd`), a vector-
map formula (:meth:`CustomOpDef.register_vmap`) and an autocast cast
rule (:meth:`CustomOpDef.register_autocast`).
2. :func:`triton_op` + :func:`wrap_triton` and :func:`tile_lang_op` +
:func:`wrap_tilelang` integrate user-written Triton / TileLang kernels:
such operators behave like any other custom operator in eager mode and
are captured as one opaque graph node by ``tensorplay.compile`` —
preserving the fusion boundary through compilation.
3. Registered operators compose with every compiler backend: during capture
a call whose arguments are symbolic records a single ``call_function``
node targeting the :class:`CustomOpDef`; backends that cannot lower it
treat the node as a barrier and fall back to the interpreter, which
dispatches to the registered kernel.
:func:`define`/:func:`impl`/:func:`impl_abstract`, :func:`infer_schema`,
:func:`get_kernel`, :meth:`CustomOpDef.set_kernel_enabled` and the
validation harness :func:`opcheck`.
``"namespace::name"`` string. Schemas are not modeled by a C++ dispatcher;
the optional ``schema=`` strings and :func:`infer_schema` output attach to
the operator for introspection, documentation and ``opcheck``.
``OpOverload``/``overload``/``deprecated``/``fallthrough_kernel``/
``NAMELESS_SCHEMA`` and ``get_ctx`` are absent, ``Library.fallback`` raises
``NotImplementedError``, and re-registering a kernel replaces the previous
one instead of raising (hot swaps for interactive sessions).
"""
from __future__ import annotations
import contextlib
import inspect
import threading
import typing
import warnings
from collections.abc import Callable, Sequence
from typing import Any, Iterable
import tensorplay
from .graph import (
GraphCaptureError,
capture_call as _capture_call,
capturing as _capturing,
_iter_proxies as _walk_proxies,
)
__all__ = [
"Library",
"CustomOpDef",
"custom_op",
"triton_op",
"tile_lang_op",
"wrap_triton",
"wrap_tilelang",
"register_kernel",
"register_fake",
"register_autograd",
"register_vmap",
"register_autocast",
"define",
"impl",
"impl_abstract",
"infer_schema",
"opcheck",
"get_kernel",
"get_op",
"has_op",
]
_LOCK = threading.RLock()
# "ns::op" -> CustomOpDef; namespaces that already own a DEF Library.
_OP_REGISTRY: dict[str, "CustomOpDef"] = {}
_DEFINED_LIBRARY_NAMESPACES: set[str] = set()
# Hot-path aliases resolved once at import (this module is imported last by
# tensorplay/__init__, so every attribute below already exists).
_is_grad_enabled = tensorplay.is_grad_enabled
# Profiler session gate for automatic op-span emission. Resolved lazily:
# the bridge function exists whenever the compiled extension ships it, and
# calling it is one atomic load. Module-level None until the first lookup
# keeps attribute access off the hot path.
_profiler_is_active_fn = None
_profiler_probe_done = False
# Span source tag: distinguishes custom-op spans from hand-written
# record_function annotations in exported traces.
_SPAN_SRC_CUSTOM_OP = "custom_op"
def _profiler_active() -> bool:
"""True while a profiling session records on this process.
One atomic load via the compiled bridge; the binding object is cached
after the first call. Falls back to False when the bridge is absent
(older extension), keeping the wrapper callable.
"""
global _profiler_is_active_fn, _profiler_probe_done
if not _profiler_probe_done:
_profiler_is_active_fn = getattr(
tensorplay._C, "_profiler_is_active", None
)
_profiler_probe_done = True
return _profiler_is_active_fn is not None and _profiler_is_active_fn()
# Live profiling sessions (nesting counted; the profiler start/stop hooks
# call _note_profiling_start/_note_profiling_stop). The eager paths read
# this module global: one dict-free global load per custom-op call, so an
# inactive session costs nothing and an active one emits an op span.
_profiling_sessions = 0
def _note_profiling_start() -> None:
global _profiling_sessions
_profiling_sessions += 1
def _note_profiling_stop() -> None:
global _profiling_sessions
if _profiling_sessions > 0:
_profiling_sessions -= 1
_COMPOSITE_KEYS = frozenset(
{"CompositeExplicitAutograd", "CompositeImplicitAutograd"}
)
_COMPOSITE_KEYS_LOWERED = frozenset(key.lower() for key in _COMPOSITE_KEYS)
_UNSET = object()
def _contains_proxy(*values: Any) -> bool:
for _proxy in _walk_proxies(values):
return True
return False
def _validate_name(name: str) -> tuple[str, str]:
if not isinstance(name, str):
raise TypeError(f"op name must be a str, got {type(name)!r}")
parts = name.split("::")
if len(parts) != 2 or not parts[0] or not parts[1]:
raise ValueError(
f'expected a qualified op name like "mylib::my_op", got {name!r}'
)
namespace, opname = parts
if not all(part.isidentifier() for part in (namespace, opname)):
raise ValueError(
f"namespace and op name must be identifiers, got {name!r}"
)
return namespace, opname
def _normalize_device_types(device_types: Any) -> list[str] | None:
"""Normalize ``device_types`` into registry keys.
``device_types`` is omitted); strings and iterables of strings map onto
per-device entries keyed by ``"cpu"``/``"cuda"``/...
"""
if device_types is None:
return None
items = [device_types] if isinstance(device_types, str) else list(device_types)
keys: list[str] = []
for item in items:
if not isinstance(item, str):
raise TypeError(
f"device types must be strings, got {item!r}; use None to "
"declare one device-agnostic implementation"
)
key = item.lower()
if key not in keys:
keys.append(key)
return keys
def _bridge_slot_key(device_types: Any) -> Any:
"""
``None``/empty iterables/composite spellings select the device-agnostic
slot (``None`` key); concrete devices lowercase into their own slot.
Used by the top-level registrations and ``Library.impl``.
"""
if device_types is None:
return None
if isinstance(device_types, str):
return None if device_type_is_composite(device_types) else device_types.lower()
keys = list(device_types)
if not keys:
return None
if len(keys) == 1:
return _bridge_slot_key(keys[0])
return [_bridge_slot_key(k) for k in keys]
def device_type_is_composite(device_type: str) -> bool:
return device_type in _COMPOSITE_KEYS
def _validate_mutates_args(mutates_args: Any) -> tuple[str, ...]:
if isinstance(mutates_args, str) or not isinstance(mutates_args, Iterable):
raise TypeError(
f"mutates_args must be an iterable of argument names, got "
f"{mutates_args!r}"
)
mutated = tuple(mutates_args)
if any(not isinstance(item, str) for item in mutated):
raise TypeError(
f"mutates_args entries must be strings, got {mutated!r}"
)
return mutated
def _validate_schema(schema: Any) -> str | None:
"""
TensorPlay keeps them verbatim for introspection/opcheck, so only the
qualified-name head is checked.
"""
if schema is None:
return None
if not isinstance(schema, str):
raise TypeError(f"schema must be a str or None, got {type(schema)!r}")
signature = schema.split("(", 1)[0].strip()
_validate_name(signature)
return schema
class CustomOpDef:
"""
Instances are callable. Calling with symbolic (tracer proxy) arguments
records one opaque graph node; calling with real tensors dispatches to
the kernel registered for the first tensor argument's device, wrapped
in the registered autograd formula when gradients are requested.
"""
def __init__(
self,
name: str,
*,
mutates_args: Sequence[str] = (),
device_types: Any = None,
schema: str | None = None,
is_triton_op: bool = False,
is_tile_lang_op: bool = False,
) -> None:
self._namespace, self._opname = _validate_name(name)
self._name = f"{self._namespace}::{self._opname}"
self._mutates_args = frozenset(_validate_mutates_args(mutates_args))
self._device_keys = _normalize_device_types(device_types)
self._schema = _validate_schema(schema)
self._is_triton_op = bool(is_triton_op)
self._is_tile_lang_op = bool(is_tile_lang_op)
# ``None`` key holds the device-agnostic kernel (device_types=None).
self._kernels: dict[str | None, Callable[..., Any]] = {}
self._disabled_kernels: set[str] = set()
# Memoized _kernel_for resolutions, keyed by device key. Every
# mutation of _kernels/_disabled_kernels clears this (registration
# and enable/disable are rare; the memo turns the per-call
# resolution into a single dict hit).
self._kernel_cache: dict[str | None, Callable[..., Any]] = {}
self._fake_fn: Callable[..., Any] | None = None
self._backward: Callable[..., Any] | None = None
self._setup_context: Callable[..., Any] | None = None
self._vmap_fn: Callable[..., Any] | None = None
self._autocast_rules: dict[str, Any] = {}
self._autograd_cls: type | None = None
def _install_default_kernel(self, fn: Callable[..., Any]) -> None:
"""Use ``fn`` as the initial kernel (the ``@custom_op`` body).
the advertised ``device_types`` — every device when omitted.
"""
if not callable(fn):
raise TypeError(f"operator body must be callable, got {type(fn)!r}")
if self._device_keys is None:
self._kernels[None] = fn
else:
for key in self._device_keys:
self._kernels[key] = fn
self._kernel_cache.clear()
self._mirror_native(self._device_keys, fn)
# -- introspection -----------------------------------------------------
@property
def name(self) -> str:
return self._name
@property
def namespace(self) -> str:
return self._namespace
@property
def opname(self) -> str:
return self._opname
@property
def mutates_args(self) -> frozenset[str]:
return self._mutates_args
@property
def is_triton_op(self) -> bool:
return self._is_triton_op
@property
def is_tile_lang_op(self) -> bool:
return self._is_tile_lang_op
@property
def schema(self) -> str | None:
return self._schema
# Readable node names once this object becomes a graph target
# (the graph target formatter resolves ``target.__name__`` first).
@property
def __name__(self) -> str: # type: ignore[override]
return self._name
def __repr__(self) -> str:
kind = (
"triton_op"
if self._is_triton_op
else "tile_lang_op"
if self._is_tile_lang_op
else "custom_op"
)
return f"<{kind} {self._name}>"
# -- registration API --------------------------------------------------
def register_kernel(
self, device_types: Any = None, fn: Callable[..., Any] | None = None, /
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
"""Register the implementation for one or more device types.
``device_types=None`` registers a single device-agnostic kernel;
otherwise pass a device string (``"cpu"``, ``"cuda"``, ...) or an
iterable of them. Usable directly
(``op.register_kernel("cpu", my_fn)``) or as a decorator
(``@op.register_kernel("cpu")``). Re-registering the same device
allows hot swaps for interactive sessions and tests).
CPU/CUDA kernels are additionally mirrored into the native p10
dispatcher under this operator's qualified name, so native code and
:meth:`run_native` can invoke them through the real dispatch path.
"""
keys = _normalize_device_types(device_types)
def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
if not callable(f):
raise TypeError(f"kernel must be callable, got {type(f)!r}")
with _LOCK:
if keys is None:
self._kernels[None] = f
else:
for key in keys:
self._kernels[key] = f
self._kernel_cache.clear()
self._mirror_native(keys, f)
return f
return decorator(fn) if fn is not None else decorator
def _mirror_native(
self, keys: list[str] | None, fn: Callable[..., Any]
) -> None:
"""Push a kernel into the native dispatcher bridge (best effort).
Tensor-in/tensor-out kernels on CPU/CUDA become callable from native
code via ``Dispatcher::findHandle("ns::op")``; other signatures keep
working through the Python dispatch path only.
"""
bridge = getattr(tensorplay._C, "_register_python_op_kernel", None)
if bridge is None:
return
slots = ["default"] if keys is None else [k for k in keys if k in ("cpu", "cuda")]
for slot in slots:
try:
bridge(self._name, slot, fn)
except Exception: # noqa: BLE001 - this is an optimization
return
def register_fake(self, fn: Callable[..., Any]) -> Callable[..., Any]:
"""Register a meta/fake kernel computing output metadata.
The fake kernel receives the same arguments but must not allocate
real tensor data; it returns tensors describing shape/dtype/device
(``tensorplay.empty_like`` style factories without data). It is
exercised by :func:`opcheck`'s ``test_faketensor``; capturing
compilers never execute either kernel version during tracing.
"""
if not callable(fn):
raise TypeError(f"fake kernel must be callable, got {type(fn)!r}")
self._fake_fn = fn
return fn
def register_autograd(
self,
backward: Callable[..., Any],
/,
*,
setup_context: Callable[..., Any] | None = None,
) -> None:
"""
``backward(ctx, *grad_outputs)`` receives the saved context. The
``setup_context`` callback may save tensors via
``ctx.save_for_backward``. When no
``setup_context`` is given the context stays empty, so backward must
derive its result purely from ``grad_outputs`` (or close over module
state).
"""
if not callable(backward):
raise TypeError(f"backward must be callable, got {type(backward)!r}")
if setup_context is not None and not callable(setup_context):
raise TypeError(
f"setup_context must be callable or None, got {setup_context!r}"
)
self._backward = backward
self._setup_context = setup_context
self._autograd_cls = self._build_autograd_class()
def register_vmap(self, fn: Callable[..., Any]) -> Callable[..., Any]:
"""
The engine does not batch yet; the registration is stored and surfaced
through :func:`get_kernel`-style introspection.
"""
if not callable(fn):
raise TypeError(f"vmap function must be callable, got {type(fn)!r}")
self._vmap_fn = fn
# Rebuild so the generated autograd class exposes the formula the
# same way a hand-written Function subclass would.
if self._backward is not None:
self._autograd_cls = self._build_autograd_class()
return fn
def register_autocast(
self, device_type: str, cast_inputs: Any
) -> None:
"""
When autocast is enabled for ``device_type``, floating-point tensor
arguments are cast to ``cast_inputs`` before any kernel runs — the
"""
if not isinstance(device_type, str):
raise TypeError(
f"device_type must be a str, got {type(device_type)!r}"
)
if not (
hasattr(cast_inputs, "is_floating_point")
and cast_inputs.is_floating_point
):
raise TypeError(
f"cast_inputs must be a floating-point dtype, got "
f"{cast_inputs!r}"
)
self._autocast_rules[device_type.lower()] = cast_inputs
@contextlib.contextmanager
def set_kernel_enabled(self, device_type: str, enabled: bool = True):
"""
context is active the concrete kernel for ``device_type`` is skipped
and dispatch falls back to the device-agnostic kernel (if any).
Disabling an already-disabled (or enabling an already-enabled)
kernel warns and is otherwise a no-op; the original state is always
restored on exit.
"""
if isinstance(device_type, str):
key = device_type.lower()
else:
key = device_type
originally_disabled = key in self._disabled_kernels
has_own_kernel = key in self._kernels
action = "enable" if enabled else "disable"
if not has_own_kernel and None not in self._kernels:
warnings.warn(
f"Attempted to {action} kernel for {key!r} but no kernel was "
"registered for this device type.",
stacklevel=2,
)
if not enabled:
if originally_disabled:
warnings.warn(
f"Attempted to disable kernel for {key!r} but it was "
"already disabled.",
stacklevel=2,
)
else:
self._disabled_kernels.add(key)
self._kernel_cache.clear()
else: # enable the kernel
if not originally_disabled:
warnings.warn(
f"Attempted to enable kernel for {key!r} but it was "
"already enabled.",
stacklevel=2,
)
else:
self._disabled_kernels.remove(key)
self._kernel_cache.clear()
try:
yield
finally:
# restore original state
if originally_disabled:
self._disabled_kernels.add(key)
else:
self._disabled_kernels.discard(key)
self._kernel_cache.clear()
def _build_autograd_class(self) -> type:
op_def = self
class _CustomOpAutograd(tensorplay.autograd.Function):
@staticmethod
def forward(*args: Any, **kwargs: Any) -> Any:
return op_def._run_kernel(args, kwargs)
@staticmethod
def setup_context(ctx: Any, inputs: tuple[Any, ...], output: Any) -> None:
if op_def._setup_context is not None:
op_def._setup_context(ctx, inputs, output)
@staticmethod
def backward(ctx: Any, *grad_outputs: Any) -> Any:
return op_def._backward(ctx, *grad_outputs)
if op_def._vmap_fn is not None:
_vmap_fn = op_def._vmap_fn
@staticmethod
def vmap(info: Any, in_dims: Any, *args: Any) -> Any:
return _vmap_fn(info, in_dims, *args)
_CustomOpAutograd.vmap = staticmethod(vmap) # type: ignore[method-assign]
_CustomOpAutograd.__qualname__ = f"_CustomOpAutograd[{self._name}]"
return _CustomOpAutograd
# -- dispatch ----------------------------------------------------------
def _kernel_for(
self, args: tuple[Any, ...], key: Any = _UNSET
) -> Callable[..., Any]:
# Lock-free read path: dict/set membership tests are atomic under
# the GIL and registration only ever swaps callables wholesale.
if key is _UNSET:
key = _first_device_key(args)
# Hot path: one dict hit for a previously resolved (key, state)
# combination. _disabled_kernels membership dominates the miss
# path only when a kernel was toggled; both mutation sites clear
# the memo.
cache = self._kernel_cache
fn = cache.get(key)
if fn is not None:
return fn
kernels = self._kernels
if key is not None:
fn = kernels.get(key)
if fn is not None and key not in self._disabled_kernels:
cache[key] = fn
return fn
# Disabled/shadowed concrete kernels fall back to the composite
fn = kernels.get(None)
if fn is not None:
if key is not None and key not in self._disabled_kernels:
cache[key] = fn
return fn
with _LOCK: # cold error path only
registered = sorted(str(item) for item in kernels)
where = f" for device {key!r}" if key is not None else ""
hint = f"; registered devices: {registered}" if registered else ""
raise NotImplementedError(
f"{self._name} has no kernel{where}{hint}. Register one via "
f"{self._name}.register_kernel(...)"
)
def _run_kernel(self, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
return self._kernel_for(args)(*args, **kwargs)
def run_native(
self, inputs: Sequence[Any], *, device_type: str | None = None
) -> list[tensorplay.Tensor]:
"""Invoke this operator through the native p10 dispatcher.
Exercises ``Dispatcher::findHandle`` plus the real kernel table —
the same path C++ code takes when calling the operator by name.
Requires a tensor-only signature (the canonical unboxed convention
is tensors-in/tensors-out); kernels that were never mirrored (e.g.
non-tensor arguments) raise ``NotImplementedError``.
"""
bridge = getattr(tensorplay._C, "_call_native_op", None)
if bridge is None:
raise RuntimeError(
"the native custom-op bridge is unavailable in this build"
)
return bridge(self._name, list(inputs), device_type)
def _eager_call(self, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
"""Dispatch real tensors with full eager semantics (no capture).
Shared by :meth:`__call__` and the native re-entry below so compiled
graphs keep device dispatch AND ``register_autograd`` behavior.
Hot path: one device-key scan, one dict lookup, one call. The
autocast rule table stays empty for the common un-registered case,
so its branch is fully skipped there.
"""
if self._autograd_cls is not None:
key = _first_device_key(args)
if self._autocast_rules:
rule = self._autocast_rules.get(key) if key is not None else None
if rule is not None and _autocast_enabled(key):
args = tuple(_cast_if_floating(v, rule) for v in args)
if kwargs:
kwargs = {
k: _cast_if_floating(v, rule) for k, v in kwargs.items()
}
if _is_grad_enabled():
if _profiling_sessions:
return self._profiled(
self._autograd_cls.apply, args, kwargs)
return self._autograd_cls.apply(*args, **kwargs)
return self._run_profiled(args, kwargs, key)
if self._autocast_rules:
key = _first_device_key(args)
rule = self._autocast_rules.get(key) if key is not None else None
if rule is not None and _autocast_enabled(key):
args = tuple(_cast_if_floating(v, rule) for v in args)
if kwargs:
kwargs = {
k: _cast_if_floating(v, rule) for k, v in kwargs.items()
}
return self._run_profiled(args, kwargs, key)
return self._run_profiled(args, kwargs, None)
# -- profiler span emission --------------------------------------------
def _profiled(self, call, args, kwargs):
"""Run ``call`` inside one user-annotation span (autograd path)."""
begin = tensorplay._C._profiler_user_begin
end = tensorplay._C._profiler_user_end
begin(self._name)
try:
return call(*args, **kwargs)
finally:
end()
def _run_profiled(
self, args: tuple[Any, ...], kwargs: dict[str, Any], key: Any
) -> Any:
"""Kernel invocation, emitting one op span when a session is live.
The session flag is a module-global load; outside profiling this is
the plain ``_kernel_for`` call with no extra work. Inner calls the
kernel itself fires (e.g. a composite body) nest under this span
like ordinary operator records do.
"""
if _profiling_sessions:
begin = tensorplay._C._profiler_user_begin
end = tensorplay._C._profiler_user_end
begin(self._name)
try:
return self._kernel_for(args, key)(*args, **kwargs)
finally:
end()
if key is None:
return self._kernel_for(args)(*args, **kwargs)
return self._kernel_for(args, key)(*args, **kwargs)
def __call__(self, *args: Any, **kwargs: Any) -> Any:
# A Proxy can only exist while a Tracer.trace() is live on this
# thread (the graph tracing depth), so the proxy walk is skipped
# entirely outside capture — identical to the generated functional
# wrappers' hot path in tensorplay/functional.py.
if _capturing():
captured = _capture_call(self, args, kwargs)
if captured is not None:
return captured
return self._eager_call(args, kwargs)
def _first_device_key(values: tuple[Any, ...]) -> str | None:
for value in values:
if isinstance(value, tensorplay.Tensor):
device = value.device
device_type = getattr(device, "type", None)
if isinstance(device_type, str):
return device_type
return "cuda" if device.is_cuda() else "cpu"
return None
def _autocast_enabled(device_key: str) -> bool:
fn = getattr(tensorplay, "is_autocast_enabled", None)
if fn is None:
return False
try:
return bool(fn(device_key))
except Exception: # noqa: BLE001 - autocast is best effort
return False
def _cast_if_floating(value: Any, dtype: Any) -> Any:
if isinstance(value, tensorplay.Tensor):
value_dtype = value.dtype
if value_dtype.is_floating_point and value_dtype != dtype:
return value.to(dtype)
return value
def get_op(op_name: str) -> CustomOpDef:
"""Return the previously defined operator named ``"ns::op"``."""
if not isinstance(op_name, str):
raise TypeError(f"op name must be a str, got {type(op_name)!r}")
with _LOCK:
op_def = _OP_REGISTRY.get(op_name)
if op_def is None:
raise RuntimeError(f"unknown operator {op_name!r}")
return op_def
def has_op(op_name: str) -> bool:
with _LOCK:
return op_name in _OP_REGISTRY
def _resolve_op(op: str | CustomOpDef) -> CustomOpDef:
if isinstance(op, CustomOpDef):
return op
if isinstance(op, str):
return get_op(op)
raise TypeError(
f"expected an operator name or CustomOpDef, got {type(op)!r}"
)
def _register_op_def(
op_def: CustomOpDef,
) -> CustomOpDef:
with _LOCK:
previous = _OP_REGISTRY.get(op_def.name)
if previous is not None:
raise RuntimeError(
f"operator {op_def.name!r} is already defined; use "
f"get_op({op_def.name!r}) or a fresh namespace"
)
_OP_REGISTRY[op_def.name] = op_def
return op_def
def _define_or_get(qualname: str, schema: str | None) -> CustomOpDef:
namespace, _opname = _validate_name(qualname)
del namespace
_validate_schema(schema)
with _LOCK:
op_def = _OP_REGISTRY.get(qualname)
if op_def is None:
op_def = _register_op_def(CustomOpDef(qualname))
elif schema is not None:
op_def._schema = _validate_schema(schema)
return op_def
[docs]
def custom_op(
name: str,
fn: Callable[..., Any] | None = None,
/,
*,
mutates_args: Sequence[str] = (),
device_types: Any = None,
schema: str | None = None,
) -> Callable[[Callable[..., Any]], CustomOpDef]:
"""
Example::
@tensorplay.library.custom_op("mylib::weighted_sum", mutates_args=())
def weighted_sum(x, weight):
return (x * weight).sum()
# Optional extra kernels per device:
@weighted_sum.register_kernel("cuda")
def _(x, weight): ...
``fn`` may also be passed positionally
(``custom_op("mylib::op", my_fn, mutates_args=())``), matching
operator's default kernel for the advertised ``device_types`` (every
device when omitted).
Args:
name: Qualified ``"namespace::name"`` identifier.
fn: Operator body; omit to use the return value as a decorator.
mutates_args: Names of arguments the kernel mutates in place.
Compile-time fusion treats these as barriers regardless of the
value; eager execution trusts the declaration.
device_types: Restriction advertised to users at definition time.
Kernels are selected per call from whatever was registered via
:meth:`CustomOpDef.register_kernel`.
schema: Optional schema string kept for introspection and
:func:`opcheck` (TensorPlay models no schema grammar).
Returns:
A decorator producing a callable :class:`CustomOpDef`.
"""
_validate_name(name)
_validate_mutates_args(mutates_args)
_normalize_device_types(device_types)
_validate_schema(schema)
def decorator(f: Callable[..., Any]) -> CustomOpDef:
op_def = CustomOpDef(
name,
mutates_args=mutates_args,
device_types=device_types,
schema=schema,
)
_register_op_def(op_def)
op_def._install_default_kernel(f)
return op_def
return decorator(fn) if fn is not None else decorator
[docs]
def triton_op(
name: str,
fn: Callable[..., Any] | None = None,
/,
*,
mutates_args: Sequence[str] = (),
device_types: Any = None,
schema: str | None = None,
) -> Callable[[Callable[..., Any]], CustomOpDef]:
"""
The registered kernel(s) must launch their Triton kernels through
:func:`wrap_triton` and only mutate arguments listed in
``mutates_args``. Under ``tensorplay.compile`` the whole operator is
captured as a single opaque node — the compiler never traces into the
Triton launches. ``device_types`` is a
"""
_validate_name(name)
_validate_mutates_args(mutates_args)
_normalize_device_types(device_types)
_validate_schema(schema)
def decorator(f: Callable[..., Any]) -> CustomOpDef:
op_def = CustomOpDef(
name,
mutates_args=mutates_args,
device_types=device_types,
schema=schema,
is_triton_op=True,
)
_register_op_def(op_def)
op_def._install_default_kernel(f)
return op_def
return decorator(fn) if fn is not None else decorator
def tile_lang_op(
name: str,
fn: Callable[..., Any] | None = None,
/,
*,
mutates_args: Sequence[str] = (),
device_types: Any = None,
schema: str | None = None,
) -> Callable[[Callable[..., Any]], CustomOpDef]:
"""Define a TileLang-backed operator (Triton contract, tile-lang kernels).
TileLang (https://github.com/tile-ai/tilelang) compiles ``@T.prim_func``
DSL programs into highly-tuned CUDA/Metal/CPU kernels; grid and thread
configuration live inside the prim func's ``with T.Kernel(...)`` block,
so unlike Triton there is no grid indexing at the launch site — a
compiled ``JITKernel`` is called directly with tensors.
The registered kernel body must launch its TileLang kernels through
:func:`wrap_tilelang`. Under ``tensorplay.compile`` the operator is
captured as a single opaque fusion-barrier node whose body never runs
during tracing, exactly like :func:`triton_op`.
"""
_validate_name(name)
_validate_mutates_args(mutates_args)
_normalize_device_types(device_types)
_validate_schema(schema)
def decorator(f: Callable[..., Any]) -> CustomOpDef:
op_def = CustomOpDef(
name,
mutates_args=mutates_args,
device_types=device_types,
schema=schema,
is_tile_lang_op=True,
)
_register_op_def(op_def)
op_def._install_default_kernel(f)
return op_def
return decorator(fn) if fn is not None else decorator
def _native_invoke(op_name: str, *tensors: Any) -> Any:
"""Re-entry point for compiled native graphs (Stax ``custom_op`` nodes).
The C++ executor installed by the bindings calls this with the
operator's qualified name and its tensor inputs; routing through the
:class:`CustomOpDef` keeps device dispatch and autograd identical to
eager execution instead of bypassing them with a raw kernel.
"""
return get_op(op_name)._eager_call(tuple(tensors), {})
class TritonKernelWrapper:
"""Grid-indexable passthrough around a ``@triton.jit`` kernel.
At eager time ``wrapped[grid](...)`` simply launches the kernel. If a
launch is ever captured symbolically (proxy arguments), it raises:
Triton launches are never part of the canonical graph — they live
inside :func:`triton_op` bodies, which the compiler captures as one
opaque node instead.
"""
__slots__ = ("kernel",)
def __init__(self, kernel: Any) -> None:
self.kernel = kernel
def __getitem__(self, grid: Any) -> Callable[..., Any]:
kernel = self.kernel
def launcher(*args: Any, **kwargs: Any) -> Any:
if _contains_proxy(grid, args, kwargs):
raise GraphCaptureError(
"a raw Triton launch cannot be captured inside "
"tensorplay.compile; define the launch inside "
"tensorplay.library.triton_op and call that operator "
"instead"
)
return kernel[grid](*args, **kwargs)
launcher.__name__ = getattr(kernel, "__name__", "triton_kernel")
return launcher
[docs]
def wrap_triton(kernel: Any) -> TritonKernelWrapper:
"""Mark a Triton kernel as launchable from within a ``triton_op``.
Accepts a ``triton.runtime.jit.JITFunction`` (the ``@triton.jit``
result) or any grid-indexable launcher; idempotent on wrappers.
"""
if isinstance(kernel, TritonKernelWrapper):
return kernel
is_jit_function = False
try:
from triton.runtime.jit import JITFunction
is_jit_function = isinstance(kernel, JITFunction)
except ImportError:
is_jit_function = False
if not is_jit_function and not hasattr(kernel, "__getitem__"):
raise TypeError(
"wrap_triton expects a @triton.jit kernel (JITFunction) or a "
f"grid-indexable launcher, got {type(kernel)!r}"
)
return TritonKernelWrapper(kernel)
class TileLangKernelWrapper:
"""Passthrough around a compiled TileLang kernel.
TileLang launch sites carry no grid (``with T.Kernel(...)`` lives in
the prim func), so unlike :class:`TritonKernelWrapper` this wraps a
directly-callable object: a ``tilelang.jit.kernel.JITKernel``, the
``JITImpl`` produced by ``@tilelang.jit`` (lazy mode: calling the
factory with shape/constexpr arguments yields the compiled kernel),
or any duck-typed adapter. Eager calls forward untouched; a symbolic
(proxy) launch raises, steering raw launches behind a
:func:`tile_lang_op` boundary.
"""
__slots__ = ("kernel",)
def __init__(self, kernel: Any) -> None:
self.kernel = kernel
def compile(self, *args: Any, **kwargs: Any) -> "TileLangKernelWrapper":
"""Bind a lazy-mode ``@tilelang.jit`` factory into a ready kernel."""
bound = self.kernel(*args, **kwargs)
return TileLangKernelWrapper(bound)
def __call__(self, *args: Any, **kwargs: Any) -> Any:
if _contains_proxy(args, kwargs):
raise GraphCaptureError(
"a raw TileLang launch cannot be captured inside "
"tensorplay.compile; define the launch inside "
"tensorplay.library.tile_lang_op and call that operator "
"instead"
)
return self.kernel(*args, **kwargs)
def _looks_like_tilelang_kernel(kernel: Any) -> bool:
"""Duck-typing for TileLang objects without importing tilelang.
Recognizes ``tilelang.jit.kernel.JITKernel`` (compiled, exposes
factory, exposes ``get_tir``/``out_idx``) and plain adapters that opt
in via a ``_tilelang_kernel`` marker attribute.
"""
if hasattr(kernel, "_tilelang_kernel"):
return bool(getattr(kernel, "_tilelang_kernel"))
return any(
hasattr(kernel, attr) for attr in ("adapter", "torch_function", "get_tir")
)
def wrap_tilelang(kernel: Any) -> TileLangKernelWrapper:
"""Mark a TileLang kernel as launchable from within a ``tile_lang_op``.
Accepts a compiled ``tilelang.jit.kernel.JITKernel``, the lazy-mode
``JITImpl`` factory returned by ``@tilelang.jit``, or any duck-typed
callables are rejected so typos surface early. Idempotent on
wrappers.
"""
if isinstance(kernel, TileLangKernelWrapper):
return kernel
recognized = False
try:
from tilelang.jit.kernel import JITKernel as _JITKernel
recognized = isinstance(kernel, _JITKernel)
except ImportError:
recognized = False
if not recognized:
try:
from tilelang.jit import JITImpl as _JITImpl
recognized = isinstance(kernel, _JITImpl)
except ImportError:
recognized = False
if not recognized:
recognized = _looks_like_tilelang_kernel(kernel)
if not recognized and callable(kernel):
raise TypeError(
"wrap_tilelang expects a tilelang JITKernel/JITImpl (see "
"https://github.com/tile-ai/tilelang) or a duck-typed adapter "
f"{type(kernel)!r}; plain callables are rejected — set "
f"'_tilelang_kernel = True' on custom launchers to opt in"
)
if not callable(kernel):
raise TypeError(
"wrap_tilelang expects a callable kernel, got "
f"{type(kernel)!r}"
)
return TileLangKernelWrapper(kernel)
[docs]
def register_kernel(
op: str | CustomOpDef,
device_types: Any = None,
func: Callable[..., Any] | None = None,
/,
*,
lib: Any = None,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
"""
Accepts a :class:`CustomOpDef` or a qualified operator name.
``device_types=None`` or an empty iterable means the device-agnostic
slot; composite spellings (``Composite…``) map there too. Usable
directly or as a decorator.
"""
del lib
op_def = _resolve_op(op)
key: Any = _bridge_slot_key(device_types)
return op_def.register_kernel(key) if func is None else op_def.register_kernel(key)(func)
[docs]
def register_fake(
op: str | CustomOpDef,
func: Callable[..., Any] | None = None,
/,
*,
lib: Any = None,
allow_override: bool = True,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
del lib, allow_override
op_def = _resolve_op(op)
return op_def.register_fake(func) if func is not None else op_def.register_fake
[docs]
def register_autograd(
op: str | CustomOpDef,
backward: Callable[..., Any],
/,
*,
setup_context: Callable[..., Any] | None = None,
lib: Any = None,
) -> None:
del lib
op_def = _resolve_op(op)
op_def.register_autograd(backward, setup_context=setup_context)
[docs]
def register_vmap(
op: str | CustomOpDef,
func: Callable[..., Any] | None = None,
/,
*,
lib: Any = None,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
del lib
op_def = _resolve_op(op)
return op_def.register_vmap(func) if func is not None else op_def.register_vmap
[docs]
def register_autocast(
op: str | CustomOpDef,
device_type: str,
cast_inputs: Any,
/,
*,
lib: Any = None,
) -> None:
del lib
_resolve_op(op).register_autocast(device_type, cast_inputs)
[docs]
def define(
qualname: str, schema: str | None = None, *, lib: Any = None, tags: Any = ()
) -> None:
"""
Creates the :class:`CustomOpDef` if absent (kernels are then attached
with :func:`impl` or ``Library("ns", "IMPL").impl``). ``tags`` is
Like ``Library.define``, a full ``"ns::op(Tensor) -> Tensor"`` string
may be pasted as ``qualname``.
"""
del lib, tags
if isinstance(qualname, str) and "(" in qualname:
if schema is None:
schema = qualname
qualname = qualname.split("(", 1)[0].strip()
_define_or_get(qualname, schema)
[docs]
def impl(
qualname: str,
types: Any,
func: Callable[..., Any] | None = None,
/,
*,
lib: Any = None,
) -> Callable[..., Any] | Callable[[Callable[..., Any]], Callable[..., Any]]:
"""
``types`` accepts concrete devices (``"CPU"``/``"CUDA"``) or composite
spellings (``CompositeExplicitAutograd`` → the device-agnostic slot).
"""
del lib
return register_kernel(qualname, types, func)
[docs]
def impl_abstract(
qualname: str,
func: Callable[..., Any] | None = None,
/,
*,
lib: Any = None,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
return register_fake(qualname, func, lib=lib)
[docs]
def get_kernel(
op: str | CustomOpDef, dispatch_key: str
) -> Callable[..., Any]:
"""
``dispatch_key`` accepts ``"cpu"``/``"cuda"``/``"default"`` and the
composite spellings. Raises ``LookupError`` when nothing usable is
registered (a disabled concrete kernel counts as absent, matching
:meth:`CustomOpDef.set_kernel_enabled` visibility).
"""
op_def = _resolve_op(op)
if not isinstance(dispatch_key, str):
raise TypeError(
f"dispatch_key must be a str, got {type(dispatch_key)!r}"
)
lowered = dispatch_key.lower()
key: Any = (
None
if lowered in _COMPOSITE_KEYS_LOWERED or lowered in ("default", "composite")
else lowered
)
# does NOT satisfy a concrete-device query (use "default" for that).
fn = op_def._kernels.get(key)
if fn is not None and (
key is None or key not in op_def._disabled_kernels
):
return fn
raise LookupError(
f"No kernel registered for {op_def.name} with dispatch key "
f"{dispatch_key!r}"
)
# ---------------------------------------------------------------------------
# infer_schema
# ---------------------------------------------------------------------------
_SCHEMA_PRIMITIVES: dict[Any, str] = {int: "SymInt", float: "float", bool: "bool", str: "str"}
def _annotation_to_schema_str(annotation: Any) -> str:
"""Best-effort translation of Python annotations to schema atoms.
Unannotated parameters default to ``Tensor`` (the overwhelming custom-op
case); unsupported annotations fall back to ``Tensor`` rather than
failing, since TensorPlay enforces no grammar.
"""
if annotation is None or annotation is type(None):
return "Tensor"
if annotation is tensorplay.Tensor or annotation == "Tensor":
return "Tensor"
if annotation in _SCHEMA_PRIMITIVES:
return _SCHEMA_PRIMITIVES[annotation]
origin = typing.get_origin(annotation)
if origin is typing.Union or (
hasattr(typing, "UnionType") and origin is typing.UnionType
):
args = [a for a in typing.get_args(annotation) if a is not type(None)]
if len(args) == 1:
return _annotation_to_schema_str(args[0]) + "?"
return "(" + ", ".join(_annotation_to_schema_str(a) for a in args) + ")"
if origin in (list, Sequence, tuple):
args = typing.get_args(annotation)
if origin is tuple and len(args) >= 2:
inner = ", ".join(_annotation_to_schema_str(a) for a in args)
return f"({inner})"
atom = _annotation_to_schema_str(args[0] if args else tensorplay.Tensor)
return f"{atom}[]"
text = str(annotation)
if "Tensor" in text:
return "Tensor"
return "Tensor"
[docs]
def infer_schema(
prototype_function: Callable[..., Any],
/,
*,
mutates_args: Sequence[str],
op_name: str | None = None,
tags: Any = (),
) -> str:
"""
Produces ``"ns::op(Tensor self, SymInt n, Tensor(a!) out) -> Tensor"``
alias-annotation ``(<type>(<letter>!))`` marker. Parameters without
annotations are treated as tensors; ``*args``/``**kwargs`` are skipped.
"""
del tags
mutated = frozenset(_validate_mutates_args(mutates_args))
if not callable(prototype_function):
raise TypeError(
f"prototype_function must be callable, got "
f"{type(prototype_function)!r}"
)
try:
hints = typing.get_type_hints(prototype_function)
except Exception: # noqa: BLE001 - unresolvable hints fall back to raw
hints = dict(getattr(prototype_function, "__annotations__", {}) or {})
parameters = inspect.signature(prototype_function).parameters
alias_letters = iter("abcdefghijklmnopqrstuvwxyz")
parts: list[str] = []
for pname, parameter in parameters.items():
if parameter.kind in (
inspect.Parameter.VAR_POSITIONAL,
inspect.Parameter.VAR_KEYWORD,
):
continue
atom = _annotation_to_schema_str(hints.get(pname))
if pname in mutated:
atom = f"{atom}({next(alias_letters)}!)"
parts.append(atom)
return_atom = _annotation_to_schema_str(hints.get("return"))
name = op_name if op_name is not None else getattr(
prototype_function, "__name__", "op"
)
_validate_name(name) if "::" in name else None
return f"{name}({', '.join(parts)}) -> {return_atom}"
# ---------------------------------------------------------------------------
# opcheck
# ---------------------------------------------------------------------------
_OPCHECK_DEFAULT_UTILS = (
"test_schema",
"test_autograd_registration",
"test_faketensor",
"test_aot_dispatch_dynamic",
)
def _flatten_tensors(value: Any) -> list[tensorplay.Tensor]:
if isinstance(value, tensorplay.Tensor):
return [value]
if isinstance(value, (list, tuple)):
flat: list[tensorplay.Tensor] = []
for item in value:
flat.extend(_flatten_tensors(item))
return flat
if isinstance(value, dict):
flat = []
for item in value.values():
flat.extend(_flatten_tensors(item))
return flat
return []
def _bind_named_tensors(
op_def: CustomOpDef, args: tuple[Any, ...], kwargs: dict[str, Any]
) -> dict[str, tensorplay.Tensor]:
"""Map argument names to tensor values via the kernel's signature."""
kernel = next(iter(op_def._kernels.values()), None)
named: dict[str, tensorplay.Tensor] = {}
if kernel is not None:
try:
sig = inspect.signature(kernel)
bound = sig.bind_partial(*args, **kwargs)
except TypeError:
bound = None
if bound is not None:
for name, value in bound.arguments.items():
if isinstance(value, tensorplay.Tensor):
named[name] = value
elif isinstance(value, (list, tuple)):
for item in value:
if isinstance(item, tensorplay.Tensor):
named[name] = item
break
return named
# Signature-less fallback: positional tensors get synthetic names.
for index, value in enumerate(args):
if isinstance(value, tensorplay.Tensor):
named[f"arg{index}"] = value
for name, value in kwargs.items():
if isinstance(value, tensorplay.Tensor):
named[name] = value
return named
def _tensors_equal(a: tensorplay.Tensor, b: tensorplay.Tensor) -> bool:
if a.shape != b.shape or a.dtype != b.dtype:
return False
try:
return bool(tensorplay.equal(a, b))
except Exception: # noqa: BLE001 - exotic dtypes: fall back to identity
return a.data_ptr() == b.data_ptr()
def _opcheck_test_schema(
op_def: CustomOpDef,
args: tuple[Any, ...],
kwargs: dict[str, Any],
) -> None:
named_before = _bind_named_tensors(op_def, args, kwargs)
snapshots = {n: t.clone() for n, t in named_before.items()}
output = op_def(*args, **kwargs)
# Undeclared mutation is the safety-critical direction: fusion treats
# undeclared side effects as barriers, so catch silent in-place edits.
for name, before in snapshots.items():
if name in op_def.mutates_args:
continue
after = named_before[name]
if not _tensors_equal(before, after):
raise AssertionError(
f"{op_def.name}: kernel mutated argument '{name}' which is "
"not declared in mutates_args; declare it or stop mutating"
)
# Outputs must be fresh allocations, never aliases of an input storage
input_ptrs = {t.data_ptr() for t in named_before.values()}
for tensor in _flatten_tensors(output):
if tensor.data_ptr() in input_ptrs:
raise AssertionError(
f"{op_def.name}: kernel returned a tensor aliasing an input; "
"custom ops must return fresh outputs (mutation goes through "
"mutates_args)"
)
def _opcheck_test_faketensor(
op_def: CustomOpDef,
args: tuple[Any, ...],
kwargs: dict[str, Any],
) -> None:
fake_fn = op_def._fake_fn
if fake_fn is None:
raise AssertionError(
f"{op_def.name}: no fake kernel registered; register one with "
"register_fake so compilers can propagate metadata"
)
real_out = _flatten_tensors(op_def(*args, **kwargs))
fake_out = _flatten_tensors(fake_fn(*args, **kwargs))
if len(real_out) != len(fake_out):
raise AssertionError(
f"{op_def.name}: fake kernel returned {len(fake_out)} tensor(s) "
f"but the real kernel returned {len(real_out)}"
)
for i, (real, fake) in enumerate(zip(real_out, fake_out)):
if tuple(real.shape) != tuple(fake.shape):
raise AssertionError(
f"{op_def.name}: output {i} shape mismatch "
f"(real {tuple(real.shape)} vs fake {tuple(fake.shape)})"
)
if real.dtype != fake.dtype:
raise AssertionError(
f"{op_def.name}: output {i} dtype mismatch "
f"(real {real.dtype} vs fake {fake.dtype})"
)
if str(real.device) != str(fake.device):
raise AssertionError(
f"{op_def.name}: output {i} device mismatch "
f"(real {real.device} vs fake {fake.device})"
)
def _opcheck_test_autograd_registration(
op_def: CustomOpDef,
args: tuple[Any, ...],
kwargs: dict[str, Any],
) -> None:
kernel = next(iter(op_def._kernels.values()), None)
if kernel is None:
return
try:
parameters = [
p
for p in inspect.signature(kernel).parameters.values()
if p.kind
not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
]
if len(parameters) < len(args):
raise TypeError
except (TypeError, ValueError):
return # cannot map inputs onto the kernel signature; skip probe
new_args = list(args)
new_kwargs = dict(kwargs)
leaves: list[tuple[tensorplay.Tensor, tensorplay.Tensor]] = []
for index, parameter in enumerate(parameters):
source = args[index] if index < len(args) else new_kwargs.get(parameter.name)
if (
isinstance(source, tensorplay.Tensor)
and source.dtype.is_floating_point
):
leaf = source.detach().clone().requires_grad_(True)
if index < len(args):
new_args[index] = leaf
else:
new_kwargs[parameter.name] = leaf
leaves.append((source, leaf))
if not leaves:
return # nothing differentiable to probe
outputs = op_def(*new_args, **new_kwargs)
flat_outputs = _flatten_tensors(outputs)
if not flat_outputs:
return
first = flat_outputs[0]
if not first.requires_grad:
raise AssertionError(
f"{op_def.name}: output does not require grad although a "
"floating input does; the kernel breaks the autograd graph"
)
first.backward(tensorplay.ones_like(first))
for original, leaf in leaves:
grad = leaf.grad
if grad is None:
raise AssertionError(
f"{op_def.name}: no gradient reached input; register an "
"autograd formula via register_autograd"
)
if tuple(grad.shape) != tuple(original.shape):
raise AssertionError(
f"{op_def.name}: gradient shape {tuple(grad.shape)} does not "
f"match input shape {tuple(original.shape)}"
)
def _make_fixed_arity_wrapper(
fn: Callable[..., Any], nargs: int, kwargs: dict[str, Any]
) -> Callable[..., Any]:
"""Build a fixed-arity ``f(a0, ..., aN)`` shim (the tracer rejects
varargs), forwarding into ``fn``."""
if nargs == 0:
return lambda: fn(**dict(kwargs))
if nargs == 1:
return lambda a0: fn(a0, **dict(kwargs))
if nargs == 2:
return lambda a0, a1: fn(a0, a1, **dict(kwargs))
if nargs == 3:
return lambda a0, a1, a2: fn(a0, a1, a2, **dict(kwargs))
if nargs == 4:
return lambda a0, a1, a2, a3: fn(a0, a1, a2, a3, **dict(kwargs))
if nargs == 5:
return lambda a0, a1, a2, a3, a4: fn(a0, a1, a2, a3, a4, **dict(kwargs))
if nargs == 6:
return lambda a0, a1, a2, a3, a4, a5: fn(
a0, a1, a2, a3, a4, a5, **dict(kwargs)
)
raise ValueError(
f"opcheck supports up to 6 sample tensor arguments, got {nargs}"
)
def _opcheck_test_aot_dispatch_dynamic(
op_def: CustomOpDef,
args: tuple[Any, ...],
kwargs: dict[str, Any],
) -> None:
from tensorplay.graph import Tracer
tensor_positions = [
i for i, v in enumerate(args) if isinstance(v, tensorplay.Tensor)
]
if not tensor_positions:
return # nothing symbolic could flow; capture adds no information
limit = min(len(tensor_positions), 6)
samples = [args[i].clone() for i in tensor_positions[:limit]]
wrapped = _make_fixed_arity_wrapper(op_def, len(samples), kwargs)
param_names = [f"a{i}" for i in range(len(samples))]
traced = Tracer().trace(
wrapped, sample_inputs=dict(zip(param_names, samples))
)
eager_out = _flatten_tensors(op_def(*samples, **kwargs))
compiled_out = _flatten_tensors(traced(*samples))
if len(eager_out) != len(compiled_out):
raise AssertionError(
f"{op_def.name}: captured graph returned {len(compiled_out)} "
f"tensor(s), eager returned {len(eager_out)}"
)
for i, (want, got) in enumerate(zip(eager_out, compiled_out)):
if not bool(tensorplay.allclose(want, got)):
raise AssertionError(
f"{op_def.name}: compiled output {i} diverges from eager"
)
_OPCHECK_TESTS = {
"test_schema": _opcheck_test_schema,
"test_faketensor": _opcheck_test_faketensor,
"test_autograd_registration": _opcheck_test_autograd_registration,
"test_aot_dispatch_dynamic": _opcheck_test_aot_dispatch_dynamic,
}
[docs]
def opcheck(
op: str | CustomOpDef,
args: tuple[Any, ...],
kwargs: dict[str, Any] | None = None,
*,
test_utils: str | Sequence[str] = _OPCHECK_DEFAULT_UTILS,
raise_exception: bool = True,
atol: float | None = None,
rtol: float | None = None,
) -> dict[str, str]:
"""
Runs each selected check and reports failures keyed by test name:
- ``test_schema``: undeclared inputs are left unmutated and no output
aliases an input storage (declared-mutation direction is trusted,
matching TensorPlay's declaration-driven fusion barriers).
- ``test_autograd_registration``: gradients reach every floating input
with matching shapes. TensorPlay composes Python kernels implicitly
(CompositeImplicitAutograd semantics), so a missing explicit formula
is legal — this check catches kernels that break the autograd graph
or drop gradients.
- ``test_faketensor``: the fake kernel reproduces the real outputs'
metadata.
- ``test_aot_dispatch_dynamic``: capture + execution reproduce the
Returns the failure mapping; empty means all checks passed.
"""
del atol, rtol # accepted for signature compatibility; comparisons are exact
op_def = _resolve_op(op)
kwargs = dict(kwargs or {})
if isinstance(test_utils, str):
selected = (
_OPCHECK_DEFAULT_UTILS if test_utils == "all" else (test_utils,)
)
else:
selected = tuple(test_utils)
unknown = [t for t in selected if t not in _OPCHECK_TESTS]
if unknown:
raise ValueError(
f"unknown opcheck test_utils {unknown}; expected a subset of "
f"{sorted(_OPCHECK_TESTS)} or 'all'"
)
failures: dict[str, str] = {}
for name in selected:
checker = _OPCHECK_TESTS[name]
try:
checker(op_def, args, kwargs)
except Exception as exc: # noqa: BLE001 - reported, not raised
failures[name] = f"{type(exc).__name__}: {exc}"
if failures and raise_exception:
rendered = "\n".join(f" {k}: {v}" for k, v in failures.items())
raise RuntimeError(
f"opcheck({op_def.name}) failed {len(failures)} check(s):\n"
f"{rendered}"
)
return failures
[docs]
class Library:
"""
one DEF library per process/namespace), ``"IMPL"`` adds kernels, and
``"FRAGMENT"`` extends an existing namespace from multiple locations.
"""
def __init__(self, namespace: str, kind: str = "DEF") -> None:
if not isinstance(namespace, str) or not namespace.isidentifier():
raise ValueError(
f"library namespace must be an identifier, got {namespace!r}"
)
if kind not in {"DEF", "IMPL", "FRAGMENT"}:
raise ValueError(f"unknown Library kind {kind!r}")
self.ns = namespace
self.kind = kind
self._op_names: list[str] = []
with _LOCK:
if kind == "DEF":
if namespace in _DEFINED_LIBRARY_NAMESPACES:
raise RuntimeError(
f"only a single DEF Library may exist for namespace "
f"{namespace!r}"
)
_DEFINED_LIBRARY_NAMESPACES.add(namespace)
def _define(self, opname: str) -> CustomOpDef:
full_name = f"{self.ns}::{opname}"
with _LOCK:
existing = _OP_REGISTRY.get(full_name)
if existing is not None:
if self.kind != "FRAGMENT" and existing.namespace == self.ns:
raise RuntimeError(
f"operator {full_name!r} already defined in namespace "
f"{self.ns!r}"
)
return existing
return _register_op_def(CustomOpDef(full_name))
[docs]
def define(
self, schema: str, *, alias_analysis: str = "", tags: Any = ()
) -> None:
"""Define an operator from a schema like ``"ns::add(Tensor, Tensor)"``.
Only the qualified name is meaningful (TensorPlay models no schema
schema strings can be pasted verbatim. ``alias_analysis`` and
``tags`` are accepted and ignored for call-site compatibility.
"""
del alias_analysis, tags
if not isinstance(schema, str) or "::" not in schema:
raise ValueError(
f'schema must look like "ns::op(...)", got {schema!r}'
)
signature = schema.split("(", 1)[0].strip()
namespace, opname = _validate_name(signature)
if namespace != self.ns:
raise ValueError(
f"schema namespace {namespace!r} does not match library "
f"namespace {self.ns!r}"
)
op_def = self._define(opname)
if op_def.name not in self._op_names:
self._op_names.append(op_def.name)
[docs]
def impl(
self,
op_name: str,
fn: Callable[..., Any] | None = None,
*,
device_type: str = "CompositeExplicitAutograd",
dispatch_key: str = "",
allow_override: bool = True,
) -> Callable[..., Any]:
"""
``device_type`` accepts composite spellings (``Composite…`` → the
device-agnostic slot) or concrete devices (``"CPU"``/``"CUDA"``);
(non-empty wins). May be used directly or as a decorator.
"""
if dispatch_key:
device_type = dispatch_key
if not isinstance(device_type, str):
raise TypeError(
f"device_type must be a str, got {type(device_type)!r}"
)
del allow_override
if device_type in _COMPOSITE_KEYS:
key: Any = None
else:
key = device_type.lower()
target = f"{self.ns}::{op_name}" if "::" not in op_name else op_name
op_def = _resolve_op(target)
if fn is not None and not callable(fn):
raise TypeError(f"kernel must be callable, got {type(fn)!r}")
def wrapper(func: Callable[..., Any]) -> Callable[..., Any]:
op_def.register_kernel(key)(func)
return func
return wrapper(fn) if fn is not None else wrapper
[docs]
def fallback(self, kind: str) -> None:
"""
TensorPlay's dispatcher has no per-key fallthrough table, so this
of silently mis-dispatching.
"""
raise NotImplementedError(
"Library.fallback is not supported: TensorPlay's dispatcher has "
"no per-dispatch-key fallthrough table (kind="
f"{kind!r})"
)
def __repr__(self) -> str:
return f"<Library ns={self.ns!r} kind={self.kind}>"Help improve this page
Found an error, an unclear step, or a missing example?
Was this page helpful?

