Copy
Source code for tensorplay._stax.stax
"""The TensorPlay native graph compiler backend.
This is a compiler backend, not a second public compiler frontend:
``tensorplay.compile`` owns
capture, guards, specialization, and graph-break policy; this module owns
lowering and executable generation for the canonical graph.
small lazy adapter. Native Stax code is loaded only when the backend is
actually selected, keeping import-time overhead out of the frontend.
"""
from __future__ import annotations
import operator
import numbers
import re
from typing import Any
from ..graph.passes import POINTWISE_FUSED_OP_NAMES
from ..graph import GraphModule, Node
from ..library import CustomOpDef as _CustomOpDef
def _nodes(value: Any):
if isinstance(value, Node):
yield value
elif isinstance(value, (tuple, list)):
for item in value:
yield from _nodes(item)
elif isinstance(value, dict):
for item in value.values():
yield from _nodes(item)
elif isinstance(value, slice):
yield from _nodes(value.start)
yield from _nodes(value.stop)
yield from _nodes(value.step)
def _native_value_leaves(value: Any, values: dict[Node, Any]) -> list[Any]:
if isinstance(value, Node):
native = values.get(value)
if isinstance(native, tuple):
return list(native)
return [] if native is None else [native]
if isinstance(value, tuple | list):
result: list[Any] = []
for item in value:
result.extend(_native_value_leaves(item, values))
return result
if isinstance(value, dict):
result = []
for item in value.values():
result.extend(_native_value_leaves(item, values))
return result
if isinstance(value, slice):
result = []
for item in (value.start, value.stop, value.step):
result.extend(_native_value_leaves(item, values))
return result
return []
def _native_output_spec(value: Any, values: dict[Node, Any]) -> Any:
if isinstance(value, Node):
native = values.get(value)
if isinstance(native, tuple):
custom = value.meta.get("custom")
template = custom.get("nested_output_template") if isinstance(custom, dict) else None
if template is None:
raise RuntimeError("native multi-output node has no output template")
return ("nested", template)
if native is None:
raise RuntimeError(f"native graph has no value for output {value.name!r}")
return ("leaf",)
if isinstance(value, tuple):
return ("tuple", tuple(_native_output_spec(item, values) for item in value))
if isinstance(value, list):
return ("list", tuple(_native_output_spec(item, values) for item in value))
if isinstance(value, dict):
return (
"dict",
tuple((key, _native_output_spec(item, values)) for key, item in value.items()),
)
raise RuntimeError("native graph outputs must be tensor values")
def _consume_template(template: Any, outputs: list[Any], index: int) -> tuple[Any, int]:
kind = template[0]
if kind in {"leaf", "tensor"}:
if index >= len(outputs):
raise RuntimeError("native graph produced too few outputs")
return outputs[index], index + 1
if kind == "nested":
return _consume_template(template[1], outputs, index)
if kind == "tuple":
result = []
for item in template[1]:
value, index = _consume_template(item, outputs, index)
result.append(value)
return tuple(result), index
if kind == "list":
result = []
for item in template[1]:
value, index = _consume_template(item, outputs, index)
result.append(value)
return result, index
if kind == "dict":
result = {}
for key, item in template[1]:
value, index = _consume_template(item, outputs, index)
result[key] = value
return result, index
raise RuntimeError(f"unknown native output template {kind!r}")
def _target_name(target: Any) -> str:
if target is operator.add:
return "add"
if target is operator.sub:
return "sub"
if target is operator.mul:
return "mul"
if target is operator.truediv:
return "div"
if target is operator.pow:
return "pow"
if target is operator.matmul:
return "matmul"
if target is operator.neg:
return "neg"
if target is operator.pos:
return "pos"
return getattr(target, "__name__", str(target))
_NATIVE_OPS = {
"add",
"sub",
"mul",
"div",
"pow",
"matmul",
"t",
"linear",
"neg",
"pos",
"abs",
"sin",
"cos",
"exp",
"log",
"sigmoid",
"sqrt",
"square",
"tanh",
"relu",
"mm",
# Tensor kernels used by the ResNet inference graph. These are kept in
# the native graph instead of falling back to the generated Python
# executor; the latter still calls every functional wrapper through the
# interpreter and is not a compiled path in any meaningful sense.
"conv2d",
"add_relu",
"batch_norm",
"max_pool2d",
"adaptive_avg_pool2d",
"flatten",
}
# The fused-op name set is shared by the graph pass and this lowering.
_CPU_FUSED_OPS = POINTWISE_FUSED_OP_NAMES
# Opcodes of the CPU fused interpreter (p10 StaxPointwiseKernels switch).
# This is a strict subset of _CPU_FUSED_OPS: graphs whose programs contain
# Triton-only opcodes fall back per lowering instead of reaching the CPU
# program runner.
_CPU_FUSED_OPCODES = {
"add": 1,
"sub": 2,
"mul": 3,
"div": 4,
"pow": 5,
"neg": 6,
"pos": 7,
"abs": 8,
"sin": 9,
"cos": 10,
"exp": 11,
"log": 12,
"sigmoid": 13,
"sqrt": 14,
"square": 15,
"tanh": 16,
"relu": 17,
"relu_grad": 18,
"abs_grad": 19,
}
# Triton-only opcode extension. Emitted programs keep the shared triple
# format; the extra opcodes never reach the CPU interpreter because the CPU
# program builder runs against _CPU_FUSED_OPCODES.
#
# ``where`` is ternary, so it is encoded as a two-instruction pair:
# ``where`` carries (cond, a) and the immediately following ``where_rest``
# carries (cond, b). The code generator pairs them by adjacency; ``where``
# itself emits no source. ``cast`` stores the target float-dtype id in its
# rhs operand slot (it has a single value operand).
_TRITON_EXTRA_OPCODES = {
"lt": 20,
"le": 21,
"gt": 22,
"ge": 23,
"eq": 24,
"ne": 25,
"where": 26,
"where_rest": 27,
"minimum": 28,
"maximum": 29,
"clamp_min": 30,
"clamp_max": 31,
"rsqrt": 32,
"exp2": 33,
"erf": 34,
"cast": 35,
}
_TRITON_OPCODES = dict(_CPU_FUSED_OPCODES, **_TRITON_EXTRA_OPCODES)
# Cast targets accepted by the fused program, keyed by ``str(dtype)``.
# Non-float casts (bool/int) stay uncompiled for now: the program's value
# space is float and the store path types outputs off the sample dtype.
_CAST_DTYPE_IDS = {
"tensorplay.float16": 1,
"tensorplay.bfloat16": 2,
"tensorplay.float32": 3,
"tensorplay.float64": 4,
}
# Backward-compatibility alias: the autograd gate keeps covering exactly the
# ops whose elementwise VJP rules exist (the CPU interpreter's surface minus
# pow). Triton-only opcodes stay out, so training graphs using them fall
# back instead of producing a wrong gradient.
_CPU_FUSED_AUTOGRAD_OPS = frozenset(_CPU_FUSED_OPCODES) - {"pow"}
def _is_scalar(value: Any) -> bool:
return isinstance(value, (bool, int, float))
def _set_scalar_attr(native_node: Any, value: Any, position: int) -> None:
if isinstance(value, bool) or isinstance(value, int):
native_node.set_int_attr("scalar_value", int(value))
elif isinstance(value, numbers.Real):
native_node.set_float_attr("scalar_value", float(value))
else:
raise TypeError(f"unsupported Stax scalar constant: {type(value)!r}")
native_node.set_int_attr("scalar_position", position)
def _int_list(value: Any) -> list[int] | None:
"""Return a constant integer list accepted by a native Stax node."""
if not isinstance(value, (tuple, list)):
return None
if any(isinstance(item, bool) or not isinstance(item, int) for item in value):
return None
return [int(item) for item in value]
def _set_int_list_attr(native_node: Any, key: str, value: Any) -> bool:
values = _int_list(value)
if values is None:
return False
native_node.set_ints_attr(key, values)
return True
def _normalize_pointwise_grad_output(grad_output: Any, reference: Any) -> Any:
"""Match the output shape expected by a fused elementwise backward.
TensorPlay's current reduction backward may hand a scalar tangent to a
custom Function for ``output.sum().backward()``. The compiled backward
contract supplies the expanded tangent, so normalize that boundary here
before entering either the p10 or Triton backward kernel.
"""
if (
grad_output.numel() == 1 and reference.numel() != 1
) or not grad_output.is_contiguous():
import tensorplay
return tensorplay.ones_like(reference, requires_grad=False) * grad_output
return grad_output
def _attach_fast_call(lowering: Any, exec_fn: Any = None) -> None:
"""Install the C steady-state trampoline for a compiled lowering.
``exec_fn`` selects the steady-state execution entry; the default is the
native ``Graph.execute`` bound method. Lowerings with a direct kernel
entry pass it here so the trampoline skips the graph walk.
"""
import tensorplay
installer = tensorplay._C._stax.install_call_trampoline
tail = [
lowering.graph_module._get_attr(target)
for target in lowering.attribute_targets
]
tail.extend(lowering.constant_values)
lowering._fast_call = installer(
lowering,
exec_fn if exec_fn is not None else lowering.graph.execute,
tail,
tensorplay.Tensor,
len(lowering.placeholders),
int(getattr(lowering, "_output_count", 1)),
getattr(lowering, "_gradient_plan", None) is not None,
int(getattr(lowering, "_native_direct", 0) or 0),
)
def _metadata_fingerprint(value: Any) -> Any:
"""Metadata snapshot for tensors outside the version-counter contract.
Only reached when ``_version`` is unavailable and the tensor is not an
inference tensor; every component is normalized so the snapshot stays
comparable across calls.
"""
shape = getattr(value, "shape", ())
if callable(shape):
shape = shape()
try:
shape = tuple(int(item) for item in shape)
except (TypeError, ValueError):
shape = repr(shape)
stride = getattr(value, "stride", ())
if callable(stride):
stride = stride()
try:
stride = tuple(int(item) for item in stride)
except (TypeError, ValueError):
stride = repr(stride)
dtype = getattr(value, "dtype", None)
if callable(dtype):
dtype = dtype()
device = getattr(value, "device", None)
if callable(device):
device = device()
return ("metadata", shape, stride, dtype, device)
class _NativeLowering:
def __init__(
self,
graph_module: GraphModule,
graph: Any,
attribute_targets: list[str],
constant_values: list[Any] | None = None,
output_count: int = 1,
native_values: dict[Node, Any] | None = None,
output_spec: Any = None,
public_output_count: int | None = None,
) -> None:
self.graph_module = graph_module
self.graph = graph
self.placeholders = graph_module.graph.placeholders
self.attribute_targets = attribute_targets
self.constant_values = list(constant_values or [])
self._output_count = output_count
self._public_output_count = (
output_count if public_output_count is None else public_output_count
)
self._output_spec = output_spec
self.native_values = dict(native_values or {})
self._tensorplay_codegen = "stax-native"
# (id, _version) memo of the last resolved input vector; attributes
# and constants appended by _bind_inputs are process-stable.
self._bind_fp: Any = None
self._last_bound_inputs: list[Any] | None = None
_attach_fast_call(self)
@staticmethod
def _input_route_fingerprint(args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
import tensorplay
def fp(value: Any) -> Any:
if isinstance(value, tensorplay.Tensor):
try:
version = value._version
except RuntimeError:
# Inference tensors carry no version counter and are
# immutable, so the identity alone keys the entry: no
# metadata snapshot, no divert on the next call.
if getattr(value, "is_inference", lambda: False)():
return ("t", id(value), None)
version = _metadata_fingerprint(value)
return (
"t",
id(value),
version,
)
return ("o", id(value))
items = [fp(item) for item in args]
items.extend((k, fp(v)) for k, v in sorted(kwargs.items()))
return tuple(items)
def _bind_inputs_fresh(self, *args: Any, **kwargs: Any) -> list[Any]:
bound = self.graph_module.signature.bind_partial(*args, **kwargs)
bound.apply_defaults()
inputs = [
bound.arguments[node.target if isinstance(node.target, str) else node.name]
for node in self.placeholders
]
inputs.extend(
self.graph_module._get_attr(target) for target in self.attribute_targets
)
inputs.extend(self.constant_values)
return inputs
def _bind_inputs(self, *args: Any, **kwargs: Any) -> list[Any]:
# Attribute targets and constants are process-stable; user inputs are
# covered by the (id, _version) fingerprint, so an unchanged call
# reuses the previously resolved input list without signature binding.
fp = self._input_route_fingerprint(args, kwargs)
if fp == self._bind_fp:
return list(self._last_bound_inputs)
inputs = self._bind_inputs_fresh(*args, **kwargs)
self._bind_fp = fp
self._last_bound_inputs = inputs
return inputs
def __call__(self, *args: Any, **kwargs: Any) -> Any:
inputs = self._bind_inputs(*args, **kwargs)
outputs = self.graph.execute(inputs)
public_outputs = outputs[: self._public_output_count]
if self._output_spec is not None:
value, consumed = _consume_template(self._output_spec, public_outputs, 0)
if consumed != len(public_outputs):
raise RuntimeError("native graph produced an unexpected output count")
return value
if len(public_outputs) == 1:
return public_outputs[0]
return tuple(public_outputs)
class _CpuFusedPointwiseLowering(_NativeLowering):
"""Executable wrapper for Stax's vectorized CPU expression kernel."""
def __init__(
self,
graph_module: GraphModule,
graph: Any,
attribute_targets: list[str],
expected_shape: tuple[int, ...],
expected_dtype: Any,
expected_device: Any,
gradient_plan: tuple[list[int], list[float], tuple[int, ...]] | None = None,
strict_native: bool = False,
native_runner: Any = None,
native_direct: int = 0,
expected_layouts: tuple[tuple[tuple[int, ...], tuple[int, ...]], ...]
| None = None,
) -> None:
super().__init__(graph_module, graph, attribute_targets)
self._expected_shape = expected_shape
self._expected_dtype = expected_dtype
self._expected_device = expected_device
# Per-input (shape, strides) pinned at lowering time; set only for
# broadcast/strided specializations whose generated addressing is
# valid for exactly these layouts.
self._expected_layouts = expected_layouts
self._gradient_plan = gradient_plan
self._strict_native = strict_native
self._tensorplay_codegen = "stax-fused-cpu"
self._autograd_function: Any | None = None
# Runtime-generated C kernel for the native route: straight-line
# compiler-scheduled code replacing the program interpreter when the
# system compiler is available. None keeps the graph-execute path.
self._native_runner = native_runner
# Address of the kernel's pointer-level entry (0 = absent); the C
# steady-state trampoline reads it for the direct launch path.
self._native_direct = int(native_direct) if native_direct else 0
# Route memo (id, _version, requires_grad) per input: eligibility and
# autograd routing are pure functions of these, so steady-state calls
# skip the per-input shape/dtype/device/contiguity probes entirely.
# In-place mutation bumps _version; fresh tensors have fresh ids.
self._route_fp: tuple[Any, ...] | None = None
self._route: str | None = None
_attach_fast_call(self, exec_fn=self._native_runner)
if gradient_plan is not None:
from ..autograd import Function
lowering = self
class _FusedPointwiseAutograd(Function):
@staticmethod
def forward(ctx: Any, *forward_inputs: Any) -> Any:
ctx.save_for_backward(*forward_inputs)
return lowering._execute_inputs(list(forward_inputs))
@staticmethod
def backward(ctx: Any, *grad_outputs: Any) -> tuple[Any, ...]:
grad_output = grad_outputs[0] if grad_outputs else None
if grad_output is None:
return (None,) * len(ctx.saved_tensors)
gradients = lowering._execute_backward(
ctx.saved_tensors,
grad_output,
)
return gradients
self._autograd_function = _FusedPointwiseAutograd
@staticmethod
def _eligible_inputs(
inputs: list[Any],
expected_shape: tuple[int, ...],
expected_dtype: Any,
expected_device: Any,
expected_layouts: tuple[tuple[tuple[int, ...], tuple[int, ...]], ...]
| None = None,
) -> bool:
try:
import tensorplay
tensor_type = tensorplay.Tensor
except (AttributeError, ImportError):
return False
if expected_layouts is not None:
if len(inputs) != len(expected_layouts):
return False
# Broadcast/strided specializations pin each input's exact
# (shape, strides): the generated addressing was proven for
# that layout, so any deviation must re-lower.
# The expected device is captured from the region's own sample
# inputs, so comparing against it is what pins the device.
return bool(inputs) and all(
isinstance(value, tensor_type)
and value.dtype == expected_dtype
and value.device == expected_device
and tuple(int(item) for item in value.shape) == layout[0]
and tuple(int(item) for item in value.stride()) == layout[1]
for value, layout in zip(inputs, expected_layouts)
)
return bool(inputs) and all(
isinstance(value, tensor_type)
and value.dtype == expected_dtype
and value.device == expected_device
and tuple(int(item) for item in value.shape) == expected_shape
and value.is_contiguous()
for value in inputs
)
def _execute_inputs(self, inputs: list[Any]) -> Any:
if self._native_runner is not None:
return self._native_runner(inputs)
outputs = self.graph.execute(inputs)
if len(outputs) != 1:
return tuple(outputs)
return outputs[0]
def _execute_backward(
self,
inputs: tuple[Any, ...],
grad_output: Any,
) -> tuple[Any, ...]:
gradients = []
if self._gradient_plan is None:
raise RuntimeError("Stax fused pointwise backward plan is missing")
import tensorplay
grad_output = _normalize_pointwise_grad_output(grad_output, inputs[0])
program, constants, output_refs = self._gradient_plan
gradients = tensorplay._C._stax.execute_fused_pointwise_multi(
[*inputs, grad_output],
program,
constants,
output_refs,
)
return tuple(gradients)
@staticmethod
def _input_route_fingerprint(value: Any) -> Any:
import tensorplay
if isinstance(value, tensorplay.Tensor):
try:
version = value._version
except RuntimeError:
# Inference tensors are immutable: the identity alone keys
# the entry, no metadata snapshot needed.
if getattr(value, "is_inference", lambda: False)():
return ("t", id(value), None)
version = _metadata_fingerprint(value)
return (
"t",
id(value),
version,
bool(getattr(value, "requires_grad", False)),
)
return ("o", id(value))
def _resolve_route(self, inputs: list[Any]) -> str:
if not self._eligible_inputs(
inputs,
self._expected_shape,
self._expected_dtype,
self._expected_device,
getattr(self, "_expected_layouts", None),
):
return "fallback"
if self._gradient_plan is not None and any(
value.requires_grad for value in inputs
):
return "autograd"
return "native"
def __call__(self, *args: Any, **kwargs: Any) -> Any:
if not kwargs and len(args) == len(self.placeholders):
inputs = list(args)
else:
bound = self.graph_module.signature.bind_partial(*args, **kwargs)
bound.apply_defaults()
inputs = [
bound.arguments[node.target if isinstance(node.target, str) else node.name]
for node in self.placeholders
]
fp = tuple(self._input_route_fingerprint(value) for value in inputs)
if fp != self._route_fp:
self._route = self._resolve_route(inputs)
self._route_fp = fp
route = self._route
if route == "fallback":
raise RuntimeError(
"Stax fused CPU lowering received inputs outside its "
"compiled specialization"
)
if route == "autograd":
if self._autograd_function is None:
raise RuntimeError("Stax fused pointwise autograd function is missing")
return self._autograd_function.apply(*inputs)
return self._execute_inputs(inputs)
_ARITHMETIC_OPS = frozenset({"add", "sub", "mul", "div", "pow"})
_COMPARISON_OPS = frozenset({"lt", "le", "gt", "ge", "eq", "ne"})
_ORDER_OPS = frozenset({"minimum", "maximum", "clamp_min", "clamp_max"})
_UNARY_OPS = frozenset(
{
"neg",
"pos",
"abs",
"sin",
"cos",
"exp",
"log",
"sigmoid",
"sqrt",
"square",
"tanh",
"relu",
"rsqrt",
"exp2",
"erf",
}
)
_CAST_METHOD_DTYPES = {
"float": "tensorplay.float32",
"half": "tensorplay.float16",
"double": "tensorplay.float64",
}
def _build_pointwise_program(
graph_module: GraphModule,
*,
skip_node: Node | None = None,
output_override: Node | None = None,
allow_empty: bool = False,
opcodes: dict[str, int] | None = None,
nodes: list[Node] | None = None,
extra_refs: dict[Node, int] | None = None,
input_slots: int | None = None,
constants: list[float] | None = None,
) -> tuple[list[Node], list[int], list[float], list[tuple[str, int, int, int]], int] | None:
"""Encode one canonical pointwise graph as Stax's postfix program.
``skip_node`` excludes one node from the program (used by the Triton
reduction-epilogue path, which folds a full-reduction ``sum`` tail into
the kernel instead of lowering it as an op), with ``output_override``
naming the program's result node.
``nodes`` narrows the walk to one ordered slice of the region, with
``extra_refs`` naming values the slice may read but does not compute and
``input_slots`` widening the reference space those extra names live in.
A caller passing ``constants`` accumulates into it, so several slices of
one region share a single constant pool and a single reference space.
``opcodes`` selects the target opcode table: the CPU fused interpreter
supports the base table only, while the Triton code generator accepts
the extended surface (comparisons, ``where``, order relations, casts).
Values are typed numerically — comparisons yield booleans, everything
else yields floats, and the program output must be a float value (the
store path types outputs off the sample dtype).
"""
table = _TRITON_OPCODES if opcodes is None else opcodes
external_nodes = list(graph_module.graph.placeholders)
base = len(external_nodes) if input_slots is None else int(input_slots)
refs: dict[Node, int] = {
node: index for index, node in enumerate(external_nodes)
}
refs.update(extra_refs or {})
ref_types: dict[int, str] = {index: "num" for index in range(base)}
program: list[int] = []
if constants is None:
constants = []
instructions: list[tuple[str, int, int, int]] = []
temp_count = 0
def constant_ref(value: Any) -> int:
if not _is_scalar(value):
raise TypeError("Stax CPU pointwise constants must be scalar")
constants.append(float(value))
return -len(constants)
def value_ref(value: Any) -> int:
if isinstance(value, Node):
if value not in refs:
raise ValueError("pointwise graph references an unavailable value")
return refs[value]
return constant_ref(value)
def value_type(ref: int) -> str:
return ref_types.get(ref, "num")
def emit(op_name: str, lhs: int, rhs: int = -1) -> int | None:
nonlocal temp_count
code = table.get(op_name)
if code is None:
return None
program.extend((code, lhs, rhs))
result = base + temp_count
temp_count += 1
instructions.append((op_name, lhs, rhs, result))
return result
for node in graph_module.graph.nodes if nodes is None else nodes:
if node is skip_node:
continue
if node.op in {"placeholder", "output"}:
continue
if node.op not in {"call_function", "call_method"}:
return None
op_name = _target_name(node.target)
if op_name not in _CPU_FUSED_OPS:
return None
kwargs = node.kwargs or {}
result_type = "num"
if op_name in {"add", "sub"} and (
len(node.args) == 3
or ("alpha" in (node.kwargs or {}) and len(node.args) == 2)
):
if len(node.args) == 3 and "alpha" not in kwargs:
lhs, rhs, alpha = node.args
else:
if len(node.args) != 2:
return None
lhs, rhs = node.args
alpha = kwargs.get("alpha", 1)
if not _is_scalar(alpha):
return None
lhs_ref = value_ref(lhs)
rhs_ref = value_ref(rhs)
if alpha != 1:
scaled = emit("mul", rhs_ref, constant_ref(alpha))
if scaled is None:
return None
ref_types[scaled] = "num"
rhs_ref = scaled
node_ref = emit(op_name, lhs_ref, rhs_ref)
if node_ref is None:
return None
refs[node] = node_ref
ref_types[node_ref] = "num"
continue
if op_name in _ARITHMETIC_OPS or op_name in _COMPARISON_OPS or (
op_name in _ORDER_OPS
):
if kwargs or len(node.args) != 2:
return None
node_ref = emit(
op_name, value_ref(node.args[0]), value_ref(node.args[1])
)
if node_ref is None:
return None
if op_name in _COMPARISON_OPS:
result_type = "bool"
refs[node] = node_ref
ref_types[node_ref] = result_type
continue
if op_name in _UNARY_OPS:
if kwargs or len(node.args) != 1:
return None
node_ref = emit(op_name, value_ref(node.args[0]))
if node_ref is None:
return None
refs[node] = node_ref
ref_types[node_ref] = "num"
continue
if op_name == "where":
if kwargs or len(node.args) != 3:
return None
cond_ref = value_ref(node.args[0])
a_ref = value_ref(node.args[1])
b_ref = value_ref(node.args[2])
# v1 contract: a boolean condition selects between float values.
# Numeric conditions and boolean branches stay uncompiled.
if value_type(cond_ref) != "bool":
return None
if value_type(a_ref) != "num" or value_type(b_ref) != "num":
return None
then_ref = emit("where", cond_ref, a_ref)
if then_ref is None:
return None
ref_types[then_ref] = "num"
node_ref = emit("where_rest", cond_ref, b_ref)
if node_ref is None:
return None
refs[node] = node_ref
ref_types[node_ref] = "num"
continue
if op_name in _CAST_METHOD_DTYPES or op_name == "to":
if op_name == "to":
if set(kwargs) - {"dtype"} or len(node.args) > 2:
return None
dtype_value = (
node.args[1] if len(node.args) > 1 else kwargs.get("dtype")
)
if dtype_value is None:
return None
dtype_key = str(dtype_value)
else:
if kwargs or len(node.args) != 1:
return None
dtype_key = _CAST_METHOD_DTYPES[op_name]
dtype_id = _CAST_DTYPE_IDS.get(dtype_key)
if dtype_id is None:
return None
node_ref = emit("cast", value_ref(node.args[0]), dtype_id)
if node_ref is None:
return None
refs[node] = node_ref
ref_types[node_ref] = "num"
continue
return None
output_values = (
[output_override]
if output_override is not None
else [
value
for output in graph_module.graph.outputs
for value in _nodes(output.args)
]
)
if (not program and not allow_empty) or len(output_values) != 1 or (
output_values[0] not in refs
):
return None
if value_type(refs[output_values[0]]) != "num":
# A boolean program output would need a typed store path.
return None
return external_nodes, program, constants, instructions, refs[output_values[0]]
def _broadcast_shape(shapes: tuple[tuple[int, ...], ...]) -> tuple[int, ...] | None:
"""Broadcast several shapes to one result shape (``None`` on mismatch)."""
rank = max((len(s) for s in shapes), default=0)
result: list[int] = []
for dim in range(rank):
extent = 1
for shape in shapes:
idx = dim - (rank - len(shape))
d = shape[idx] if idx >= 0 else 1
if d != 1:
if extent != 1 and extent != d:
return None
extent = d
result.append(extent)
return tuple(result)
def _lower_cpu_fused_pointwise(
graph_module: GraphModule,
example_inputs: list[Any],
*,
strict_native: bool = False,
dynamic: bool = False,
) -> _CpuFusedPointwiseLowering | None:
"""Build one CPU expression program for a pointwise graph.
The specialization requires matching contiguous float32 CPU tensors. For
grad-enabled pointwise graphs, Stax also emits a vectorized reverse-mode
program and attaches it through TensorPlay's Function contract. General
broadcasting, views, and unsupported derivatives stay on the native p10
path.
Two program surfaces are attempted in order: the base opcode table,
which every execution route (compiled kernel, program interpreter,
fused backward) can run, and the extended surface (comparisons,
``where``, order relations, casts), which only the runtime-generated C
kernel can execute — extended programs therefore require a successful
native build and a grad-free graph.
"""
if dynamic:
return None
try:
import tensorplay
native_module = getattr(tensorplay._C, "_stax", None)
tensor_type = tensorplay.Tensor
except (AttributeError, ImportError):
return None
if native_module is None or not hasattr(native_module.Graph, "execute"):
return None
if not example_inputs or any(not isinstance(value, tensor_type) for value in example_inputs):
return None
first = example_inputs[0]
if (
not first.device.is_cpu()
or first.dtype != tensorplay.float32
or not first.is_contiguous()
):
return None
if any(
value.device != first.device or value.dtype != first.dtype
for value in example_inputs[1:]
):
return None
# Broadcast/strided acceptance: inputs may differ in shape or layout as
# long as the emitter can prove every address it generates contiguous
# within a vector. Everything else keeps the generic fallback.
input_shapes = tuple(
tuple(int(item) for item in value.shape) for value in example_inputs
)
input_strides = tuple(
tuple(int(item) for item in value.stride()) for value in example_inputs
)
output_shape = input_shapes[0]
if _broadcast_shape(input_shapes) != output_shape:
return None
try:
from .codegen.cpp import analyze_input_modes
input_modes = analyze_input_modes(
input_shapes, input_strides, output_shape, lane_count=16
)
except (TypeError, ValueError):
return None
if input_modes is None:
return None
# Legacy surface (every input flat) keeps the program-interpreter
# fallback; anything else requires the compiled kernel, whose generated
# addressing is only valid for these exact layouts, and a grad-free
# graph (the fused backward program assumes flat inputs).
layouts_only = any(mode != "flat" for mode, _ in input_modes)
if layouts_only and any(
value.requires_grad for value in example_inputs
):
return None
extended = False
try:
pointwise = _build_pointwise_program(
graph_module, opcodes=_CPU_FUSED_OPCODES
)
if pointwise is None:
pointwise = _build_pointwise_program(
graph_module, opcodes=_TRITON_OPCODES
)
extended = pointwise is not None
if extended and any(
value.requires_grad for value in example_inputs
):
return None
except (TypeError, ValueError, RuntimeError):
return None
if pointwise is None:
return None
external_nodes, program, constants, instructions, output_ref = pointwise
if len(external_nodes) != len(example_inputs):
return None
native_runner: Any = None
native_direct = 0
try:
from .codegen.cpp import build_cpu_native_kernel
built = build_cpu_native_kernel(
instructions,
constants,
len(external_nodes),
output_ref,
shape=first.shape,
device=first.device,
input_shapes=input_shapes,
input_strides=input_strides,
)
if isinstance(built, tuple):
native_runner, native_direct = built
else:
native_runner = built
except Exception:
native_runner = None
native_direct = 0
if extended and native_runner is None:
return None
# Broadcast/strided layouts have no interpreter-compatible program: the
# compiled kernel is the only route that can address them.
if layouts_only and native_runner is None:
return None
try:
graph = native_module.Graph()
native_values: dict[Node, Any] = {
node: graph.add_input() for node in external_nodes
}
output_values = [
value for output in graph_module.graph.outputs for value in _nodes(output.args)
]
if len(output_values) != 1:
return None
fused = graph.create_node("fused_pointwise", output_values[0].name)
for node in external_nodes:
fused.add_input(native_values[node])
fused.set_int_attr("input_count", len(external_nodes))
fused.set_ints_attr("program", program)
fused.set_floats_attr("constants", constants)
graph.register_output(fused.add_output())
except (TypeError, ValueError, RuntimeError):
return None
gradient_plan: tuple[list[int], list[float], tuple[int, ...]] | None = None
if not extended and not layouts_only and any(
value.requires_grad for value in example_inputs
):
if any(op_name not in _CPU_FUSED_AUTOGRAD_OPS for op_name, *_ in instructions):
return None
try:
gradient_plan = _build_fused_gradient_graphs(
len(external_nodes),
instructions,
program,
constants,
len(program) // 3,
output_ref,
)
except (TypeError, ValueError, RuntimeError):
return None
if gradient_plan is None:
return None
return _CpuFusedPointwiseLowering(
graph_module,
graph,
[],
tuple(int(item) for item in first.shape),
first.dtype,
first.device,
gradient_plan,
strict_native,
native_runner,
native_direct,
expected_layouts=(
tuple(zip(input_shapes, input_strides)) if layouts_only else None
),
)
# Reduction spellings the fused CPU reduction path recognizes. ``max``/``min``
# are accepted only in their whole-tensor form: with a dimension they return a
# value/index pair, which is a different lowering contract.
_REDUCTION_METHODS = frozenset(
{"sum", "mean", "prod", "max", "min", "amax", "amin"}
)
_DIMLESS_ONLY_REDUCTIONS = frozenset({"max", "min"})
_DIM_ONLY_REDUCTIONS = frozenset({"amax", "amin"})
def _reduction_dtype_ok(value: Any) -> bool:
"""Whether a reduction's ``dtype`` argument keeps the float32 contract."""
if value is None:
return True
import tensorplay
if value is tensorplay.float32:
return True
# The captured call carries the sentinel that means "keep the input
# dtype"; the route already pinned float32 inputs.
return str(value).rsplit(".", 1)[-1].lower() == "undefined"
def _parse_reduction(node: Node, rank: int) -> Any:
"""Read one reduction node into a :class:`ReduceSpec`, or ``None``.
Positional and keyword spellings both resolve here: the trailing
positional arguments of a reduction are ``dim``, ``keepdim``, ``dtype``
in that order.
"""
from .codegen.cpp_reduction import ReduceSpec
if node.op not in {"call_function", "call_method"}:
return None
name = _target_name(node.target)
if name not in _REDUCTION_METHODS:
return None
args = list(node.args)
if not args or not isinstance(args[0], Node):
return None
rest = args[1:]
kwargs = dict(node.kwargs or {})
if len(rest) > 3:
return None
dim: Any = None
keepdim: Any = False
if len(rest) >= 1:
dim = rest[0]
if len(rest) >= 2:
keepdim = rest[1]
if len(rest) >= 3 and not _reduction_dtype_ok(rest[2]):
return None
if "dim" in kwargs:
if dim is not None:
return None
dim = kwargs.pop("dim")
if "keepdim" in kwargs:
if len(rest) >= 2:
return None
keepdim = kwargs.pop("keepdim")
if "dtype" in kwargs and not _reduction_dtype_ok(kwargs.pop("dtype")):
return None
if kwargs:
return None
if not isinstance(keepdim, bool):
return None
if dim is None:
if name in _DIM_ONLY_REDUCTIONS:
return None
return ReduceSpec(name, tuple(range(rank)), False)
if name in _DIMLESS_ONLY_REDUCTIONS:
return None
if isinstance(dim, bool):
return None
if isinstance(dim, int):
dims: tuple[int, ...] = (int(dim),)
elif isinstance(dim, (tuple, list)) and dim and all(
isinstance(item, int) and not isinstance(item, bool) for item in dim
):
dims = tuple(int(item) for item in dim)
else:
return None
return ReduceSpec(name, dims, keepdim)
class _CpuFusedReductionLowering:
"""Executable wrapper for Stax's fused CPU reduction kernel.
The kernel owns the whole region: it evaluates the pointwise expression
and the reduction in one pass, allocates its own output, and returns the
wrapped tensor, so the steady-state call never builds an intermediate.
"""
def __init__(
self,
graph_module: GraphModule,
expected_dtype: Any,
expected_device: Any,
expected_layouts: tuple[tuple[tuple[int, ...], tuple[int, ...]], ...],
native_runner: Any,
native_direct: int,
out_shape: tuple[int, ...],
strict_native: bool = False,
) -> None:
self.graph_module = graph_module
# No interpreter-executable graph backs this region: the compiled
# kernel is the only route, and the route check below guarantees the
# inputs it was specialized for.
self.graph = None
self.placeholders = graph_module.graph.placeholders
self.attribute_targets: list[str] = []
self.constant_values: list[Any] = []
self.native_values: dict[Node, Any] = {}
self._output_count = 1
self._public_output_count = 1
self._output_spec = None
self._tensorplay_codegen = "stax-fused-cpu-reduce"
self._expected_dtype = expected_dtype
self._expected_device = expected_device
self._expected_layouts = expected_layouts
self._out_shape = out_shape
self._strict_native = strict_native
self._native_runner = native_runner
self._native_direct = int(native_direct) if native_direct else 0
self._route_fp: tuple[Any, ...] | None = None
self._route: str | None = None
_attach_fast_call(self, exec_fn=native_runner)
def _resolve_route(self, inputs: list[Any]) -> str:
if not _CpuFusedPointwiseLowering._eligible_inputs(
inputs,
(),
self._expected_dtype,
self._expected_device,
self._expected_layouts,
):
return "fallback"
if any(getattr(value, "requires_grad", False) for value in inputs):
# Reverse mode over a fused reduction is not part of this
# lowering's contract; a grad-carrying call re-lowers.
return "fallback"
return "native"
def __call__(self, *args: Any, **kwargs: Any) -> Any:
if not kwargs and len(args) == len(self.placeholders):
inputs = list(args)
else:
bound = self.graph_module.signature.bind_partial(*args, **kwargs)
bound.apply_defaults()
inputs = [
bound.arguments[
node.target if isinstance(node.target, str) else node.name
]
for node in self.placeholders
]
fp = tuple(
_CpuFusedPointwiseLowering._input_route_fingerprint(value)
for value in inputs
)
if fp != self._route_fp:
self._route = self._resolve_route(inputs)
self._route_fp = fp
if self._route == "fallback":
raise RuntimeError(
"Stax fused CPU reduction received inputs outside its "
"compiled specialization"
)
return self._native_runner(inputs)
def _lower_cpu_fused_reduction(
graph_module: GraphModule,
example_inputs: list[Any],
*,
strict_native: bool = False,
dynamic: bool = False,
) -> _CpuFusedReductionLowering | None:
"""Build one fused CPU kernel for a pointwise region ending in a reduction.
The region's single output must be a reduction whose operand is used
nowhere else, so folding it into the reduction loop cannot change what any
other node observes. Everything upstream of the reduction is encoded as
the same expression program the pointwise path uses.
"""
if dynamic:
return None
try:
import tensorplay
tensor_type = tensorplay.Tensor
except (AttributeError, ImportError):
return None
if not example_inputs or any(
not isinstance(value, tensor_type) for value in example_inputs
):
return None
first = example_inputs[0]
if (
not first.device.is_cpu()
or first.dtype != tensorplay.float32
or not first.is_contiguous()
):
return None
if any(
value.device != first.device or value.dtype != first.dtype
for value in example_inputs[1:]
):
return None
if any(value.requires_grad for value in example_inputs):
return None
input_shapes = tuple(
tuple(int(item) for item in value.shape) for value in example_inputs
)
input_strides = tuple(
tuple(int(item) for item in value.stride()) for value in example_inputs
)
in_shape = _broadcast_shape(input_shapes)
if in_shape is None or not in_shape:
return None
output_values = [
value
for output in graph_module.graph.outputs
for value in _nodes(output.args)
]
if len(output_values) != 1 or not isinstance(output_values[0], Node):
return None
reduce_node = output_values[0]
spec = _parse_reduction(reduce_node, len(in_shape))
if spec is None:
return None
source = reduce_node.args[0]
if not isinstance(source, Node) or len(source.users) != 1:
return None
try:
pointwise = _build_pointwise_program(
graph_module,
skip_node=reduce_node,
output_override=source,
allow_empty=True,
opcodes=_TRITON_OPCODES,
)
except (TypeError, ValueError, RuntimeError):
return None
if pointwise is None:
return None
external_nodes, _program, constants, instructions, output_ref = pointwise
if len(external_nodes) != len(example_inputs):
return None
try:
from .codegen.cpp_reduction import build_cpu_reduction_kernel
built = build_cpu_reduction_kernel(
instructions,
constants,
len(external_nodes),
output_ref,
spec,
in_shape=in_shape,
device=first.device,
input_shapes=input_shapes,
input_strides=input_strides,
)
except Exception:
built = None
if built is None:
return None
runner, direct, out_shape = built
return _CpuFusedReductionLowering(
graph_module,
first.dtype,
first.device,
tuple(zip(input_shapes, input_strides)),
runner,
direct,
out_shape,
strict_native,
)
def _elem_dependencies(
target: Node, kinds: dict[Node, str], stop: set[Node] | None = None
) -> list[Node]:
"""Order the elementwise nodes one value depends on, producers first.
Placeholders, row values, and anything in ``stop`` are read through
references rather than recomputed, so they end the walk. A node reached
from two stages and not staged appears in both of their orders: that
stage re-evaluates it while the row is still in cache, which is cheaper
than the memory traffic of materializing it.
"""
if target.op == "placeholder" or (stop is not None and target in stop):
return []
order: list[Node] = []
seen: set[Node] = set()
stack: list[tuple[Node, bool]] = [(target, False)]
while stack:
node, expanded = stack.pop()
if expanded:
order.append(node)
continue
if node in seen:
continue
seen.add(node)
stack.append((node, True))
operands = list(_nodes(node.args)) + list(_nodes(node.kwargs or {}))
for operand in operands:
if stop is not None and operand in stop:
continue
if kinds.get(operand) == "elem" and operand.op != "placeholder":
if operand not in seen:
stack.append((operand, False))
return order
def _plan_row_fusion(
graph_module: GraphModule,
in_shape: tuple[int, ...],
input_shapes: tuple[tuple[int, ...], ...],
*,
stage: bool = True,
) -> Any:
"""Split a region into row stages, or return ``None``.
A node is *elementwise* when it carries one value per input element and
*row-valued* when it carries one value per row: reductions over the
trailing axis turn the former into the latter, and every later stage may
read row values as broadcasts. The region qualifies when at least one
such reduction exists and every node lands in one of the two classes.
"""
from .codegen.cpp_rowfusion import ROW_OPS, RowFusion, RowStep, _ROW_UNARY
rank = len(in_shape)
if rank < 2 or any(int(extent) <= 0 for extent in in_shape):
return None
extent = int(in_shape[-1])
rows = 1
for size in in_shape[:-1]:
rows *= int(size)
placeholders = list(graph_module.graph.placeholders)
if not placeholders or len(placeholders) != len(input_shapes):
return None
# Every input has to span the reduced axis: a value that is constant along
# it would be row-valued, and the classification below assumes it is not.
for shape in input_shapes:
if len(shape) > rank:
return None
aligned = (1,) * (rank - len(shape)) + tuple(int(size) for size in shape)
if aligned[-1] != extent:
return None
output_values = [
value
for output in graph_module.graph.outputs
for value in _nodes(output.args)
]
if len(output_values) != 1 or not isinstance(output_values[0], Node):
return None
target = output_values[0]
if target.op == "placeholder":
return None
kinds: dict[Node, str] = {node: "elem" for node in placeholders}
row_shapes: dict[Node, tuple[int, ...]] = {}
slots: dict[Node, int] = {}
staged: list[tuple[str, Node, Any]] = []
for node in graph_module.graph.nodes:
if node.op in {"placeholder", "output"}:
continue
if node.op not in {"call_function", "call_method"}:
return None
spec = _parse_reduction(node, rank)
if spec is not None:
normalized = spec.normalized(rank)
if normalized is None or normalized.dims != (rank - 1,):
return None
source = node.args[0]
if not isinstance(source, Node) or kinds.get(source) != "elem":
return None
kinds[node] = "row"
slots[node] = len(slots)
row_shapes[node] = (
tuple(in_shape[:-1]) + (1,)
if normalized.keepdim
else tuple(in_shape[:-1])
)
staged.append(("reduce", node, normalized))
continue
operands = list(_nodes(node.args)) + list(_nodes(node.kwargs or {}))
if any(operand not in kinds for operand in operands):
return None
if not operands or any(kinds[operand] == "elem" for operand in operands):
kinds[node] = "elem"
continue
name = _target_name(node.target)
if name not in ROW_OPS or (node.kwargs or {}):
return None
arity = 1 if name in _ROW_UNARY else 2
if len(node.args) != arity:
return None
if any(
not isinstance(arg, Node) and not _is_scalar(arg)
for arg in node.args
):
return None
shapes = {
row_shapes[arg] for arg in node.args if isinstance(arg, Node)
}
if len(shapes) != 1:
return None
kinds[node] = "row"
slots[node] = len(slots)
row_shapes[node] = next(iter(shapes))
staged.append(("rowop", node, name))
if not slots or not any(entry[0] == "reduce" for entry in staged):
return None
# A row value an elementwise stage reads has to broadcast along the
# reduced axis, which is what the kept trailing dimension expresses.
keep = tuple(in_shape[:-1]) + (1,)
for node in slots:
if any(kinds.get(user) == "elem" for user in node.users):
if row_shapes[node] != keep:
return None
input_count = len(placeholders)
extra_refs = {node: input_count + slot for node, slot in slots.items()}
# A value a reduction pass computes and a later pass needs again is worth
# keeping: the pass already holds it in a register, so staging it costs
# one store and saves the later pass the whole expression behind it.
reduce_sources = [
node.args[0] for kind, node, _payload in staged if kind == "reduce"
]
later: list[set[Node]] = []
seen_later: set[Node] = set()
for source in reversed(reduce_sources[1:]):
seen_later |= set(_elem_dependencies(source, kinds))
later.append(set(seen_later))
later.reverse()
if kinds[target] == "elem":
output_deps = set(_elem_dependencies(target, kinds))
else:
output_deps = set()
stages: dict[Node, int] = {}
for index, source in enumerate(reduce_sources) if stage else ():
if source.op == "placeholder" or source in stages:
continue
reused = output_deps | (later[index] if index < len(later) else set())
if source in reused:
stages[source] = len(stages)
total_inputs = input_count + len(slots) + len(stages)
stage_refs = {
node: input_count + len(slots) + slot for node, slot in stages.items()
}
constants: list[float] = []
def elem_program(node: Node, available: dict[Node, int]) -> Any:
try:
return _build_pointwise_program(
graph_module,
output_override=node,
allow_empty=True,
opcodes=_TRITON_OPCODES,
nodes=_elem_dependencies(node, kinds, stop=set(available)),
extra_refs={**extra_refs, **available},
input_slots=total_inputs,
constants=constants,
)
except (TypeError, ValueError, RuntimeError):
return None
def row_operand(value: Any) -> int | None:
if isinstance(value, Node):
return extra_refs.get(value)
if not _is_scalar(value):
return None
constants.append(float(value))
return -len(constants)
steps: list[Any] = []
available: dict[Node, int] = {}
for entry_kind, node, payload in staged:
if entry_kind == "reduce":
source = node.args[0]
built = elem_program(source, available)
if built is None:
return None
instructions, output_ref = built[3], built[4]
stage = stage_refs.get(source)
steps.append(
RowStep(
kind="reduce",
slot=slots[node],
op=payload.op,
instructions=tuple(instructions),
output_ref=output_ref,
stage=-1 if stage is None else stage - input_count - len(slots),
)
)
if stage is not None:
available[source] = stage
continue
lhs = row_operand(node.args[0])
rhs = -1 if payload in _ROW_UNARY else row_operand(node.args[1])
if lhs is None or rhs is None:
return None
steps.append(
RowStep(kind="rowop", slot=slots[node], op=payload, lhs=lhs, rhs=rhs)
)
if kinds[target] == "elem":
built = elem_program(target, available)
if built is None:
return None
out_instructions, out_ref = built[3], built[4]
if not out_instructions:
return None
output_kind = "elem"
out_shape = tuple(int(size) for size in in_shape)
else:
output_kind = "row"
out_instructions = []
out_ref = extra_refs[target]
out_shape = row_shapes[target]
return RowFusion(
input_count=input_count,
row_slots=len(slots),
stage_slots=len(stages),
constants=tuple(constants),
steps=tuple(steps),
output_kind=output_kind,
out_instructions=tuple(out_instructions),
out_ref=out_ref,
reduce_extent=extent,
rows=rows,
in_shape=tuple(int(size) for size in in_shape),
out_shape=tuple(int(size) for size in out_shape),
)
class _CpuRowFusionLowering(_CpuFusedReductionLowering):
"""Executable wrapper for a row-staged CPU kernel.
The route contract is the reduction kernel's: one compiled specialization
that owns the whole region, allocates its output, and declines anything
outside the layouts it was built for.
"""
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self._tensorplay_codegen = "stax-fused-cpu-rowfuse"
def __call__(self, *args: Any, **kwargs: Any) -> Any:
try:
return super().__call__(*args, **kwargs)
except RuntimeError as error:
if "fused CPU reduction" in str(error):
raise RuntimeError(
"Stax row-staged CPU kernel received inputs outside its "
"compiled specialization"
) from None
raise
def _expand_row_normalizations(graph_module: GraphModule) -> GraphModule | None:
"""Rewrite the softmax family into primitives on a copy of the region.
The composites this expands are single fused kernels of their own, so the
expansion is only worth having when it is fused back into one kernel.
Working on a copy is what makes that conditional: a region the row-staged
planner then declines keeps the operators -- and the kernels -- it had.
"""
from ..graph.passes import DecomposeRowNormalizations, row_normalization_names
known = row_normalization_names()
present = False
for node in graph_module.graph.nodes:
if node.op == "call_method":
name = node.target if isinstance(node.target, str) else None
elif node.op == "call_function":
name = getattr(node.target, "__name__", None)
else:
continue
if name in known:
present = True
break
if not present:
return None
try:
clone = _copy_region_graph(graph_module.graph)
expanded = GraphModule(
graph_module.root, clone, graph_module.signature
)
result = DecomposeRowNormalizations()(expanded)
except Exception:
return None
return result.graph_module if result.modified else None
def _copy_region_graph(graph: Any) -> Any:
"""Duplicate a graph's nodes without duplicating what they carry.
Node metadata holds the traced tensor values, so a deep copy of the
region would clone every intermediate; node-level copies keep those
references shared, which is all a rewrite needs.
"""
from ..graph import Graph, map_arg
clone = Graph()
mapping: dict[Node, Node] = {}
for node in graph.nodes:
if node.op == "output":
continue
mapping[node] = clone.node_copy(node, lambda value: mapping[value])
for node in graph.nodes:
if node.op == "output":
clone.output(
map_arg(node.args[0], lambda value: mapping[value]), node.type
)
return clone
def _row_fusion_plan(
graph_module: GraphModule,
example_inputs: list[Any],
on_device: str,
) -> Any:
"""Guard a region and plan it as row stages; ``None`` when it is not one.
The plan itself carries no device: the same stages compile to a CPU loop
nest or to one CUDA program per row, so both lowerings share this front
end and differ only in the generator they hand the plan to.
"""
try:
import tensorplay
tensor_type = tensorplay.Tensor
except (AttributeError, ImportError):
return None
if not example_inputs or any(
not isinstance(value, tensor_type) for value in example_inputs
):
return None
first = example_inputs[0]
on = first.device.is_cuda() if on_device == "cuda" else first.device.is_cpu()
if not on or first.dtype != tensorplay.float32 or not first.is_contiguous():
return None
if any(
value.device != first.device
or value.dtype != first.dtype
or not value.is_contiguous()
for value in example_inputs[1:]
):
return None
if any(value.requires_grad for value in example_inputs):
return None
input_shapes = tuple(
tuple(int(item) for item in value.shape) for value in example_inputs
)
input_strides = tuple(
tuple(int(item) for item in value.stride()) for value in example_inputs
)
in_shape = _broadcast_shape(input_shapes)
if in_shape is None or not in_shape:
return None
# Keeping a value alive across a reduction is a win where the buffer
# sits in cache and a loss where it sits in a register file: one device
# stages, the other recomputes.
stage = on_device != "cuda"
module = graph_module
fusion = _plan_row_fusion(module, in_shape, input_shapes, stage=stage)
if fusion is None:
expanded = _expand_row_normalizations(graph_module)
if expanded is None:
return None
fusion = _plan_row_fusion(
expanded, in_shape, input_shapes, stage=stage
)
if fusion is None:
return None
module = expanded
return module, fusion, input_shapes, input_strides, first
class _CudaRowFusionLowering(_CpuFusedReductionLowering):
"""Executable wrapper for a row-staged CUDA kernel.
One program per output row, the row resident for the whole region: the
inputs are read once no matter how many reductions the region contains.
"""
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self._tensorplay_codegen = "stax-fused-cuda-rowfuse"
def __call__(self, *args: Any, **kwargs: Any) -> Any:
try:
return super().__call__(*args, **kwargs)
except RuntimeError as error:
if "fused CPU reduction" in str(error):
raise RuntimeError(
"Stax row-staged CUDA kernel received inputs outside its "
"compiled specialization"
) from None
raise
def _lower_cuda_row_fusion(
graph_module: GraphModule,
example_inputs: list[Any],
*,
strict_native: bool = False,
dynamic: bool = False,
) -> Any:
"""Build one CUDA kernel for a region with reductions in the middle.
Splitting such a region at every reduction gives one kernel per stage,
each streaming the input again and writing an intermediate the next one
reads back. Keeping the row resident across the stages removes all of
that traffic and leaves one launch.
"""
if dynamic:
return None
planned = _row_fusion_plan(graph_module, example_inputs, "cuda")
if planned is None:
return None
module, fusion, input_shapes, input_strides, first = planned
try:
from .codegen.triton_rowfusion import build_cuda_row_fusion_kernel
launch = build_cuda_row_fusion_kernel(
fusion,
input_shapes=input_shapes,
input_strides=input_strides,
)
except Exception:
launch = None
if launch is None:
return None
return _CudaRowFusionLowering(
module,
first.dtype,
first.device,
tuple(zip(input_shapes, input_strides)),
launch,
0,
fusion.out_shape,
strict_native,
)
def _lower_cpu_row_fusion(
graph_module: GraphModule,
example_inputs: list[Any],
*,
strict_native: bool = False,
dynamic: bool = False,
) -> Any:
"""Build one CPU kernel for a region with reductions in the middle.
The reduction results feed elementwise work over the same axis they were
reduced along, so the region cannot be expressed as a pointwise program
or as a pointwise program ending in a reduction. Staging it per row keeps
every intermediate in cache instead of writing it out and reading it back.
"""
if dynamic:
return None
planned = _row_fusion_plan(graph_module, example_inputs, "cpu")
if planned is None:
return None
module, fusion, input_shapes, input_strides, first = planned
try:
from .codegen.cpp_rowfusion import build_cpu_row_fusion_kernel
built = build_cpu_row_fusion_kernel(
fusion,
device=first.device,
input_shapes=input_shapes,
input_strides=input_strides,
)
except Exception:
built = None
if built is None:
return None
runner, direct = built
return _CpuRowFusionLowering(
module,
first.dtype,
first.device,
tuple(zip(input_shapes, input_strides)),
runner,
direct,
fusion.out_shape,
strict_native,
)
# ---------------------------------------------------------------------------
# Mixed regions: generated kernels between operators that run as they are
def _traced_value(graph_module: GraphModule, node: Node) -> Any:
"""The tensor a node carried when the region was captured, or ``None``."""
if node.op == "get_attr":
try:
return graph_module._get_attr(node.target)
except (AttributeError, KeyError, RuntimeError):
return None
return node.meta.get("val")
def _tensor_layout(value: Any) -> tuple[tuple[int, ...], tuple[int, ...]] | None:
try:
shape = tuple(int(item) for item in value.shape)
stride = tuple(int(item) for item in value.stride())
except (AttributeError, TypeError, ValueError):
return None
return shape, stride
def _segment_externals(nodes: tuple[Node, ...]) -> list[Node]:
"""Values a segment reads from outside itself, in first-use order."""
inside = set(nodes)
externals: list[Node] = []
seen: set[Node] = set()
for node in nodes:
operands = list(_nodes(node.args)) + list(_nodes(node.kwargs or {}))
for operand in operands:
if operand in inside or operand in seen:
continue
seen.add(operand)
externals.append(operand)
return externals
def _build_segment_kernel(
graph_module: GraphModule,
segment: Any,
device: Any,
) -> tuple[list[Node], Any] | None:
"""Compile one fusible segment; return its inputs and callable runner."""
externals = _segment_externals(segment.nodes)
if not externals or len(externals) > 16:
return None
layouts = []
for node in externals:
layout = _tensor_layout(_traced_value(graph_module, node))
if layout is None:
return None
layouts.append(layout)
input_shapes = tuple(shape for shape, _stride in layouts)
input_strides = tuple(stride for _shape, stride in layouts)
refs = {node: index for index, node in enumerate(externals)}
reduce_node = segment.tail if segment.kind == "pw+red" else None
body = [node for node in segment.nodes if node is not reduce_node]
source = segment.producer if reduce_node is not None else segment.nodes[-1]
if source is None:
return None
try:
program = _build_pointwise_program(
graph_module,
output_override=source,
allow_empty=reduce_node is not None,
opcodes=_TRITON_OPCODES,
nodes=body,
extra_refs=refs,
input_slots=len(externals),
)
except (TypeError, ValueError, RuntimeError):
return None
if program is None:
return None
_external, _encoded, constants, instructions, output_ref = program
if reduce_node is None:
layout = _tensor_layout(_traced_value(graph_module, source))
if layout is None:
return None
out_shape = layout[0]
try:
from .codegen.cpp import build_cpu_native_kernel
built = build_cpu_native_kernel(
instructions,
constants,
len(externals),
output_ref,
shape=out_shape,
device=device,
input_shapes=input_shapes,
input_strides=input_strides,
)
except Exception:
return None
if built is None:
return None
runner = built[0] if isinstance(built, tuple) else built
return externals, runner
layout = _tensor_layout(_traced_value(graph_module, source))
if layout is None:
return None
try:
from .codegen.cpp_reduction import build_cpu_reduction_kernel
built = build_cpu_reduction_kernel(
instructions,
constants,
len(externals),
output_ref,
segment.reduction,
in_shape=layout[0],
device=device,
input_shapes=input_shapes,
input_strides=input_strides,
)
except Exception:
return None
if built is None:
return None
return externals, built[0]
def _kernel_step(runner: Any, sources: tuple[int, ...]):
"""Close over one generated kernel and the slots holding its inputs."""
def run(values: list[Any]) -> Any:
return runner([values[slot] for slot in sources])
return run
class _CpuSegmentedLowering:
"""Runs a mixed region: generated kernels between untouched operators.
A region that mixes fusible work with operators the generators do not
cover used to lose the compiled route entirely. Here the fusible runs
become one kernel each and the rest of the region runs exactly as it
was captured, so a pointwise chain between two matrix products costs a
single pass over its data instead of one pass per operator.
"""
def __init__(
self,
graph_module: GraphModule,
steps: list[tuple],
slot_count: int,
constants: dict[int, Any],
output_slot: int,
expected_dtype: Any,
expected_device: Any,
expected_layouts: tuple[tuple[tuple[int, ...], tuple[int, ...]], ...],
strict_native: bool = False,
) -> None:
self.graph_module = graph_module
self.graph = None
self.placeholders = graph_module.graph.placeholders
self.attribute_targets: list[str] = []
self.constant_values: list[Any] = []
self.native_values: dict[Node, Any] = {}
self._output_count = 1
self._public_output_count = 1
self._output_spec = None
self._tensorplay_codegen = "stax-fused-cpu-segments"
self._steps = steps
self._template: list[Any] = [None] * slot_count
for slot, value in constants.items():
self._template[slot] = value
self._output_slot = output_slot
self._expected_dtype = expected_dtype
self._expected_device = expected_device
self._expected_layouts = expected_layouts
self._strict_native = strict_native
self._route_fp: tuple[Any, ...] | None = None
self._route: str | None = None
_attach_fast_call(self, exec_fn=self._execute)
def _execute(self, inputs: list[Any]) -> Any:
# The captured constants never move, so the value table starts as a
# copy of a template that already holds them.
values = self._template.copy()
values[: len(inputs)] = inputs
for step, target, release in self._steps:
values[target] = step(values)
# Dropping an intermediate as soon as its last reader has run
# hands the buffer straight back to the allocator, so the next
# operator writes into memory that is still warm.
for slot in release:
values[slot] = None
return values[self._output_slot]
def _resolve_route(self, inputs: list[Any]) -> str:
if not _CpuFusedPointwiseLowering._eligible_inputs(
inputs,
(),
self._expected_dtype,
self._expected_device,
self._expected_layouts,
):
return "fallback"
if any(getattr(value, "requires_grad", False) for value in inputs):
return "fallback"
return "native"
def __call__(self, *args: Any, **kwargs: Any) -> Any:
if not kwargs and len(args) == len(self.placeholders):
inputs = list(args)
else:
bound = self.graph_module.signature.bind_partial(*args, **kwargs)
bound.apply_defaults()
inputs = [
bound.arguments[
node.target if isinstance(node.target, str) else node.name
]
for node in self.placeholders
]
fp = tuple(
_CpuFusedPointwiseLowering._input_route_fingerprint(value)
for value in inputs
)
if fp != self._route_fp:
self._route = self._resolve_route(inputs)
self._route_fp = fp
if self._route == "fallback":
raise RuntimeError(
"Stax segmented CPU region received inputs outside its "
"compiled specialization"
)
return self._execute(inputs)
def _lower_cpu_segmented(
graph_module: GraphModule,
example_inputs: list[Any],
*,
strict_native: bool = False,
dynamic: bool = False,
) -> _CpuSegmentedLowering | None:
"""Compile the fusible runs of a region the whole-region paths declined.
The scheduler partitions the region; every pointwise run and every run
ending in a reduction becomes one generated kernel, and each remaining
operator stays a single call between them. A store-time epilogue is the
one schedule this path cannot wire yet, and a region containing one keeps
its existing route rather than losing the fusion the epilogue expresses.
"""
if dynamic:
return None
try:
import tensorplay
tensor_type = tensorplay.Tensor
except (AttributeError, ImportError):
return None
if not example_inputs or any(
not isinstance(value, tensor_type) for value in example_inputs
):
return None
first = example_inputs[0]
if not first.device.is_cpu() or first.dtype != tensorplay.float32:
return None
if any(
value.device != first.device or value.dtype != first.dtype
for value in example_inputs[1:]
):
return None
if any(value.requires_grad for value in example_inputs):
return None
if len(example_inputs) != len(graph_module.graph.placeholders):
return None
output_values = [
value
for output in graph_module.graph.outputs
for value in _nodes(output.args)
]
if len(output_values) != 1 or not isinstance(output_values[0], Node):
return None
final = output_values[0]
def is_pointwise(node: Node) -> bool:
if node.op not in {"call_function", "call_method"}:
return False
return _target_name(node.target) in _CPU_FUSED_OPS
def classify_reduction(node: Node) -> Any:
if node.op not in {"call_function", "call_method"}:
return None
if not node.args or not isinstance(node.args[0], Node):
return None
layout = _tensor_layout(_traced_value(graph_module, node.args[0]))
if layout is None:
return None
return _parse_reduction(node, len(layout[0]))
from .scheduler import segment_graph
segments = segment_graph(
graph_module,
is_pointwise=is_pointwise,
classify_reduction=classify_reduction,
)
if segments is None:
return None
if any(segment.epilogue for segment in segments):
return None
compiled_kinds = {"pw", "pw+red"}
if not any(segment.kind in compiled_kinds for segment in segments):
return None
if not any(segment.kind == "extern" for segment in segments):
# A region that is fusible end to end belongs to the whole-region
# paths; reaching here means they declined it for another reason.
return None
slots: dict[Node, int] = {
node: index for index, node in enumerate(graph_module.graph.placeholders)
}
constants: dict[int, Any] = {}
steps: list[tuple] = []
next_slot = len(slots)
def slot_for(node: Node) -> int | None:
if node in slots:
return slots[node]
if node.op != "get_attr":
return None
value = _traced_value(graph_module, node)
if not isinstance(value, tensor_type):
return None
nonlocal next_slot
slots[node] = next_slot
constants[next_slot] = value
next_slot += 1
return slots[node]
def extern_step(node: Node):
"""Close over one operator's call, resolving its operands by slot.
The plan is built once: an argument is either a slot to read or a
value to pass through, so the steady-state call walks a flat list
instead of rebuilding the captured argument structure.
"""
target = node.target
op = node.op
kwargs_template = dict(node.kwargs or {})
table = slots
simple = all(
not isinstance(item, (list, tuple, dict, slice))
for item in (*node.args, *kwargs_template.values())
)
if not simple:
from ..graph import map_arg
def run_general(values: list[Any]) -> Any:
resolve = lambda item: values[table[item]] # noqa: E731
args = map_arg(node.args, resolve)
kwargs = map_arg(kwargs_template, resolve)
if op == "call_function":
return target(*args, **kwargs)
return getattr(args[0], target)(*args[1:], **kwargs)
return run_general
plan = tuple(
(table[item], None) if isinstance(item, Node) else (-1, item)
for item in node.args
)
keys = tuple(kwargs_template)
key_plan = tuple(
(table[item], None) if isinstance(item, Node) else (-1, item)
for item in kwargs_template.values()
)
if not keys and op == "call_function":
def run_positional(values: list[Any]) -> Any:
return target(
*[
values[slot] if value is None else value
for slot, value in plan
]
)
return run_positional
def run(values: list[Any]) -> Any:
args = [values[slot] if value is None else value for slot, value in plan]
kwargs = {
key: (values[slot] if value is None else value)
for key, (slot, value) in zip(keys, key_plan)
}
if op == "call_function":
return target(*args, **kwargs)
return getattr(args[0], target)(*args[1:], **kwargs)
return run
def add_extern(node: Node) -> bool:
nonlocal next_slot
if node.op == "get_attr":
return slot_for(node) is not None
if node.op not in {"call_function", "call_method"}:
return False
operands = list(_nodes(node.args)) + list(_nodes(node.kwargs or {}))
for operand in operands:
if slot_for(operand) is None:
return False
sources = tuple(slots[operand] for operand in operands)
slots[node] = next_slot
next_slot += 1
steps.append((extern_step(node), slots[node], sources))
return True
compiled_count = 0
for segment in segments:
if segment.kind != "extern":
built = _build_segment_kernel(graph_module, segment, first.device)
if built is not None:
externals, runner = built
sources = []
for node in externals:
source = slot_for(node)
if source is None:
return None
sources.append(source)
export = segment.export_node
if export is None:
return None
slots[export] = next_slot
next_slot += 1
steps.append(
(
_kernel_step(runner, tuple(sources)),
slots[export],
tuple(sources),
)
)
compiled_count += 1
continue
# One run the generators cannot express does not cost the region
# its other kernels: those operators run individually instead.
for node in segment.nodes:
if not add_extern(node):
return None
if compiled_count == 0:
return None
if final not in slots:
return None
# Liveness: a value is dropped right after the step that reads it last,
# so long regions do not hold every intermediate alive to the end.
last_use: dict[int, int] = {}
for index, (_step, _target, sources) in enumerate(steps):
for slot in sources:
last_use[slot] = index
output_slot = slots[final]
plan = [
(
step,
target,
tuple(
slot
for slot, index in last_use.items()
if index == position and slot != output_slot
),
)
for position, (step, target, _sources) in enumerate(steps)
]
input_shapes = tuple(
tuple(int(item) for item in value.shape) for value in example_inputs
)
input_strides = tuple(
tuple(int(item) for item in value.stride()) for value in example_inputs
)
return _CpuSegmentedLowering(
graph_module,
plan,
next_slot,
constants,
output_slot,
first.dtype,
first.device,
tuple(zip(input_shapes, input_strides)),
strict_native,
)
def _build_fused_gradient_graphs(
input_count: int,
instructions: list[tuple[str, int, int, int]],
forward_program: list[int],
forward_constants: list[float],
forward_temp_count: int,
output_ref: int,
) -> tuple[list[int], list[float], tuple[int, ...]] | None:
"""Create one shared fused reverse-mode program for all inputs.
The forward intermediates are emitted once. Each input derivative then
extends that same program and records one final temporary, allowing the
native kernel to evaluate all gradients in one vector loop.
"""
def remap_forward_ref(ref: int) -> int:
if ref >= input_count:
return ref + 1 # reserve the final external input for grad_output
return ref
remapped_forward_program: list[int] = []
for offset in range(0, len(forward_program), 3):
remapped_forward_program.extend(
(
forward_program[offset],
remap_forward_ref(forward_program[offset + 1]),
remap_forward_ref(forward_program[offset + 2]),
)
)
program = list(remapped_forward_program)
constants = list(forward_constants)
temp_count = forward_temp_count
zero_ref = -(len(constants) + 1)
constants.append(0.0)
one_ref = -(len(constants) + 1)
constants.append(1.0)
two_ref = -(len(constants) + 1)
constants.append(2.0)
grad_output_ref = input_count
output_refs: list[int] = []
def emit(op_name: str, lhs: int, rhs: int = -1) -> int:
nonlocal temp_count
if op_name not in _CPU_FUSED_OPCODES:
raise ValueError(f"unsupported fused derivative op: {op_name}")
program.extend((_CPU_FUSED_OPCODES[op_name], lhs, rhs))
result = input_count + 1 + temp_count
temp_count += 1
return result
def is_zero(ref: int) -> bool:
return ref == zero_ref
def is_one(ref: int) -> bool:
return ref == one_ref
def add_ref(lhs: int, rhs: int) -> int:
if is_zero(lhs):
return rhs
if is_zero(rhs):
return lhs
return emit("add", lhs, rhs)
def sub_ref(lhs: int, rhs: int) -> int:
if is_zero(rhs):
return lhs
return emit("sub", lhs, rhs)
def mul_ref(lhs: int, rhs: int) -> int:
if is_zero(lhs) or is_zero(rhs):
return zero_ref
if is_one(lhs):
return rhs
if is_one(rhs):
return lhs
return emit("mul", lhs, rhs)
def neg_ref(ref: int) -> int:
if is_zero(ref):
return ref
return emit("neg", ref)
def div_ref(lhs: int, rhs: int) -> int:
if is_zero(lhs):
return zero_ref
if is_one(rhs):
return lhs
return emit("div", lhs, rhs)
adjoints: dict[int, int] = {
remap_forward_ref(output_ref): grad_output_ref,
}
def add_adjoint(ref: int, contribution: int) -> None:
if ref < 0 or is_zero(contribution):
return
adjoints[ref] = add_ref(adjoints.get(ref, zero_ref), contribution)
for op_name, lhs, rhs, result in reversed(instructions):
lhs = remap_forward_ref(lhs)
rhs = remap_forward_ref(rhs)
result = remap_forward_ref(result)
grad = adjoints.get(result, zero_ref)
if is_zero(grad):
continue
if op_name == "add":
add_adjoint(lhs, grad)
add_adjoint(rhs, grad)
elif op_name == "sub":
add_adjoint(lhs, grad)
add_adjoint(rhs, neg_ref(grad))
elif op_name == "mul":
add_adjoint(lhs, mul_ref(grad, rhs))
add_adjoint(rhs, mul_ref(grad, lhs))
elif op_name == "div":
add_adjoint(lhs, div_ref(grad, rhs))
denominator = mul_ref(rhs, rhs)
add_adjoint(rhs, neg_ref(div_ref(mul_ref(grad, lhs), denominator)))
elif op_name == "neg":
add_adjoint(lhs, neg_ref(grad))
elif op_name == "pos":
add_adjoint(lhs, grad)
elif op_name == "abs":
add_adjoint(lhs, mul_ref(grad, emit("abs_grad", lhs)))
elif op_name == "sin":
add_adjoint(lhs, mul_ref(grad, emit("cos", lhs)))
elif op_name == "cos":
add_adjoint(lhs, mul_ref(grad, neg_ref(emit("sin", lhs))))
elif op_name == "exp":
add_adjoint(lhs, mul_ref(grad, result))
elif op_name == "log":
add_adjoint(lhs, div_ref(grad, lhs))
elif op_name == "sigmoid":
local = mul_ref(result, sub_ref(one_ref, result))
add_adjoint(lhs, mul_ref(grad, local))
elif op_name == "sqrt":
add_adjoint(lhs, div_ref(grad, mul_ref(two_ref, result)))
elif op_name == "square":
add_adjoint(lhs, mul_ref(grad, mul_ref(two_ref, lhs)))
elif op_name == "tanh":
local = sub_ref(one_ref, mul_ref(result, result))
add_adjoint(lhs, mul_ref(grad, local))
elif op_name == "relu":
add_adjoint(lhs, mul_ref(grad, emit("relu_grad", lhs)))
else:
return None
# Make every output a temporary. This also handles a disconnected input
# (constant zero) and an input that receives grad_output directly.
for input_ref in range(input_count):
output_refs.append(emit("pos", adjoints.get(input_ref, zero_ref)))
# Remove forward values that are not needed by any local derivative. For
# example, d(sin(x))/dx uses cos(x), not the forward sin(x) result; this
# follows the derivative graph rather than blindly replaying all of
# the forward graph.
total_input_count = input_count + 1
instruction_count = len(program) // 3
live = [False] * instruction_count
pending = list(output_refs)
while pending:
ref = pending.pop()
if ref < total_input_count:
continue
instruction = ref - total_input_count
if instruction < 0 or instruction >= instruction_count or live[instruction]:
continue
live[instruction] = True
offset = instruction * 3
pending.extend((program[offset + 1], program[offset + 2]))
compact_refs: dict[int, int] = {}
next_instruction = 0
for instruction, is_live in enumerate(live):
if is_live:
compact_refs[total_input_count + instruction] = (
total_input_count + next_instruction
)
next_instruction += 1
compact_program: list[int] = []
for instruction, is_live in enumerate(live):
if not is_live:
continue
offset = instruction * 3
compact_program.extend(
(
program[offset],
compact_refs.get(program[offset + 1], program[offset + 1]),
compact_refs.get(program[offset + 2], program[offset + 2]),
)
)
output_refs = [compact_refs[ref] for ref in output_refs]
program = compact_program
if len(program) // 3 > 64:
return None
return program, constants, tuple(output_refs)
def _fold_eval_conv_batch_norm(
graph_module: GraphModule,
example_inputs: list[Any],
) -> dict[Node, tuple[Node, Any, Any]]:
"""Precompute inference BatchNorm parameters for Conv2d users.
ResNet inference contains the stable pattern ``conv2d -> batch_norm``.
Folding the running-statistics transform into the convolution removes one
full feature-map kernel and its intermediate write. The optimization is
intentionally restricted to eval-mode BatchNorm with a single Conv2d
user, so training graphs and branched tensors retain the ordinary native
operators.
The returned tensors are compile-time constants owned by the native
lowering. TensorPlay's public compile contract currently has no
parameter-version guard, therefore this pass is only enabled for the
inference lowering path; callers that mutate parameters must recompile.
"""
# Do not fold a graph that is being differentiated with respect to its
# runtime inputs. The folded parameters are inference constants, while
# eval-mode autograd still needs the original parameter edges.
if any(getattr(value, "requires_grad", False) for value in example_inputs):
return {}
try:
import tensorplay
tensor_type = tensorplay.Tensor
except (AttributeError, ImportError):
return {}
# Folding changes parameter dataflow, so it is valid only for the
# no-grad inference specialization that the benchmark requests. A
# grad-enabled compile must retain the ordinary Conv/BN autograd edges.
if tensorplay.is_grad_enabled():
return {}
def tensor_attr(value: Any) -> Any | None:
if not isinstance(value, Node) or value.op != "get_attr":
return None
attribute = graph_module._get_attr(value.target)
return attribute if isinstance(attribute, tensor_type) else None
folded: dict[Node, tuple[Node, Any, Any]] = {}
for batch_norm in graph_module.graph.nodes:
if (
batch_norm.op != "call_function"
or _target_name(batch_norm.target) != "batch_norm"
or len(batch_norm.args) != 8
or batch_norm.kwargs
or batch_norm.args[5] is not False
):
continue
conv = batch_norm.args[0]
if (
not isinstance(conv, Node)
or conv.op != "call_function"
or _target_name(conv.target) != "conv2d"
or len(conv.args) != 7
or conv.kwargs
or conv.users != {batch_norm}
):
continue
running_mean = tensor_attr(batch_norm.args[1])
running_var = tensor_attr(batch_norm.args[2])
bn_weight = tensor_attr(batch_norm.args[3])
bn_bias = tensor_attr(batch_norm.args[4])
conv_weight = tensor_attr(conv.args[1])
conv_bias = tensor_attr(conv.args[2])
eps = batch_norm.args[7]
if (
running_mean is None
or running_var is None
or conv_weight is None
or not isinstance(eps, numbers.Real)
or conv.args[2] is not None and conv_bias is None
or batch_norm.args[3] is not None and bn_weight is None
or batch_norm.args[4] is not None and bn_bias is None
):
continue
try:
with tensorplay.no_grad():
running_mean = running_mean.detach()
running_var = running_var.detach()
conv_weight = conv_weight.detach()
weight_coeff = tensorplay.rsqrt(running_var + float(eps))
scale = (
bn_weight.detach()
if bn_weight is not None
else tensorplay.ones_like(running_var)
) * weight_coeff
folded_weight = conv_weight * scale.reshape((-1, 1, 1, 1))
base_bias = (
conv_bias.detach()
if conv_bias is not None
else tensorplay.zeros_like(running_mean)
)
folded_bias = (
(base_bias - running_mean) * scale
+ (
bn_bias.detach()
if bn_bias is not None
else tensorplay.zeros_like(running_mean)
)
)
except (AttributeError, RuntimeError, TypeError, ValueError):
# Keep the ordinary native Conv+BN path if a backend dtype or
# device cannot materialize the folded constants.
continue
folded[conv] = (batch_norm, folded_weight, folded_bias)
return folded
_NATIVE_OP_SUPPORT: dict[str, bool] = {}
def _native_runs_linear() -> bool:
"""Whether the loaded runtime can execute a fused ``linear`` node.
The lowering and the runtime library are built separately, and a tree can
hold one newer than the other, so a node this module knows how to emit is
not necessarily one the runtime knows how to run. Probed once per
process with a one-node graph; a runtime that cannot run it keeps the
transpose-product-add form, which every runtime can.
"""
known = _NATIVE_OP_SUPPORT.get("linear")
if known is not None:
return known
supported = False
try:
import tensorplay
native = tensorplay._C._stax
graph = native.Graph()
node = graph.create_node("linear", "probe")
node.add_input(graph.add_input())
node.add_input(graph.add_input())
graph.register_output(node.add_output())
graph.execute([tensorplay.zeros((1, 1)), tensorplay.zeros((1, 1))])
supported = True
except Exception: # noqa: BLE001 - any failure means "emit the long form"
supported = False
_NATIVE_OP_SUPPORT["linear"] = supported
return supported
def _lower_native(
graph_module: GraphModule,
example_inputs: list[Any],
*,
use_fusion: bool = True,
extra_output_nodes: list[Node] | None = None,
) -> _NativeLowering | None:
"""Lower the canonical graph into the native Stax IR when possible."""
try:
import tensorplay
native_module = getattr(tensorplay._C, "_stax", None)
except (AttributeError, ImportError):
native_module = None
if native_module is None:
return None
if not hasattr(native_module.Graph, "execute"):
return None
try:
import tensorplay
tensor_type = tensorplay.Tensor
except (AttributeError, ImportError):
return None
if len(example_inputs) != len(graph_module.graph.placeholders):
return None
if any(not isinstance(value, tensor_type) for value in example_inputs):
# Stax's native ABI is Tensor-only. The generated GraphModule remains
# the correct compiled path for scalar/keyword placeholders.
return None
graph = native_module.Graph()
values: dict[Node, Any] = {}
attribute_targets: list[str] = []
constant_values: list[Any] = []
folded_convs = _fold_eval_conv_batch_norm(graph_module, example_inputs)
# convolution inputs, weights, and outputs. Its generated wrapper uses
# ``empty_strided`` tensors with the channels-last strides and the current
# rather than assuming NCHW. Materialize the same weight layout once for
# folded constants; runtime activations are converted by a native graph
# node immediately before the first convolution that consumes them.
use_channels_last = bool(
example_inputs
and example_inputs[0].device.is_cuda()
and not tensorplay.is_grad_enabled()
)
if use_channels_last and folded_convs:
try:
folded_with_layout: dict[Node, tuple[Node, Any, Any]] = {}
for conv, (batch_norm, weight, bias) in folded_convs.items():
# NCHW logical shape, NHWC physical storage, then reinterpret
# as NCHW. This layout is represented by generated weight
# strides, e.g. [K*C*R*S, 1, S*C, C].
physical_weight = weight.permute((0, 2, 3, 1)).clone()
channels_last_weight = physical_weight.permute((0, 3, 1, 2))
folded_with_layout[conv] = (
batch_norm,
channels_last_weight,
bias,
)
folded_convs = folded_with_layout
except (AttributeError, RuntimeError, TypeError, ValueError):
# Keep the already validated folding path if this backend cannot
# materialize the specialized layout for a particular dtype.
use_channels_last = False
folded_native_inputs: dict[Node, tuple[Any, Any]] = {}
folded_batch_norm_to_conv = {
batch_norm: conv for conv, (batch_norm, _, _) in folded_convs.items()
}
# belong in the native ABI. Inference Conv+BN folding leaves the original
# parameter nodes in the captured graph, but the native graph consumes only the folded
# weight/bias constants. Passing those dead tensors through Python and
# C++ on every invocation is pure call-boundary overhead.
live_attribute_nodes: set[Node] = set()
visited_nodes: set[Node] = set()
def visit_native_dependency(value: Any) -> None:
if isinstance(value, Node):
if value in visited_nodes:
return
visited_nodes.add(value)
if value.op == "get_attr":
live_attribute_nodes.add(value)
return
if value in folded_convs or value in folded_batch_norm_to_conv:
visit_native_dependency(value.args[0])
return
for argument in value.args:
visit_native_dependency(argument)
for argument in value.kwargs.values():
visit_native_dependency(argument)
return
if isinstance(value, (tuple, list)):
for item in value:
visit_native_dependency(item)
elif isinstance(value, dict):
for item in value.values():
visit_native_dependency(item)
elif isinstance(value, slice):
visit_native_dependency(value.start)
visit_native_dependency(value.stop)
visit_native_dependency(value.step)
for output in graph_module.graph.outputs:
visit_native_dependency(output.args)
for extra_node in extra_output_nodes or []:
visit_native_dependency(extra_node)
fused_relu_convs: dict[Node, Node] = {}
fused_add_relus: dict[Node, Node] = {}
fused_relu_nodes: set[Node] = set()
layout_values: dict[Node, bool] = {}
channels_last_values: dict[Node, Any] = {}
peel_conv_bias = bool(example_inputs and example_inputs[0].device.is_cuda())
# This is the same producer/sole-consumer legality check used by
# training graph because add_relu has a generated autograd formula; the
# Conv->ReLU and Conv+BN folding paths remain inference-only.
if use_fusion:
for relu in graph_module.graph.nodes:
if (
relu.op != "call_function"
or _target_name(relu.target) != "relu"
or len(relu.args) != 1
or relu.kwargs not in ({}, {"inplace": False}, {"inplace": True})
):
continue
source = relu.args[0]
if (
isinstance(source, Node)
and source.op == "call_function"
and _target_name(source.target) == "add"
and source.users == {relu}
):
if len(source.args) == 2:
lhs, rhs = source.args
alpha = 1
elif len(source.args) == 3:
lhs, rhs, alpha = source.args
else:
continue
if (
alpha == 1
and isinstance(lhs, Node)
and isinstance(rhs, Node)
):
fused_add_relus[source] = relu
continue
if tensorplay.is_grad_enabled():
continue
conv = folded_batch_norm_to_conv.get(source)
if conv is None:
conv = source
if not (
isinstance(conv, Node)
and conv.op == "call_function"
and _target_name(conv.target) == "conv2d"
):
continue
if source.users != {relu} or conv.users != ({source} if source is not conv else {relu}):
continue
fused_relu_convs[conv] = relu
for node in graph_module.graph.placeholders:
values[node] = graph.add_input()
layout_values[node] = False
def channels_last_value(node: Any) -> Any | None:
"""Return the native value in the preferred 4-D layout."""
if not isinstance(node, Node) or node not in values:
return None
if not use_channels_last:
return values[node]
if layout_values.get(node, False):
return values[node]
cached = channels_last_values.get(node)
if cached is not None:
return cached
reorder = graph.create_node("channels_last", f"{node.name}_channels_last")
reorder.add_input(values[node])
converted = reorder.add_output()
channels_last_values[node] = converted
return converted
# Register all live module attributes before synthetic folded weights so
# Graph::execute's input order is independent of where get_attr nodes are
# placed in the Python graph.
for node in graph_module.graph.nodes:
if node.op != "get_attr" or node not in live_attribute_nodes:
continue
attribute = graph_module._get_attr(node.target)
if not isinstance(attribute, tensor_type):
return None
values[node] = graph.add_input()
attribute_targets.append(node.target)
for node in graph_module.graph.nodes:
folded = folded_convs.get(node)
if folded is None:
continue
_, folded_weight, folded_bias = folded
native_weight = graph.add_input()
native_bias = graph.add_input()
folded_native_inputs[node] = (native_weight, native_bias)
constant_values.extend((folded_weight, folded_bias))
for node in graph_module.graph.nodes:
if node.op in {"placeholder", "output", "get_attr"}:
continue
if node.op not in {"call_function", "call_method"}:
return None
if (
node.op == "call_function"
and node.target is operator.getitem
and len(node.args) == 2
and isinstance(node.args[0], Node)
and isinstance(node.args[1], int)
and isinstance(values.get(node.args[0]), tuple)
):
source_values = values[node.args[0]]
index = node.args[1]
if index < 0:
index += len(source_values)
if index < 0 or index >= len(source_values):
return None
values[node] = source_values[index]
layout_values[node] = False
continue
op_name = _target_name(node.target)
# User-defined operators enter the native dispatcher bridge. Their
# output arity is carried by the graph metadata because the native
# value table reserves one value per result.
if isinstance(node.target, _CustomOpDef):
if node.kwargs or not node.args:
return None
native_node = graph.create_node("custom_op", node.name)
native_node.set_str_attr("op_name", node.target.name)
for argument in node.args:
resolved = values.get(argument) if isinstance(argument, Node) else None
if resolved is None or isinstance(resolved, tuple):
return None
native_node.add_input(resolved)
custom = node.meta.get("custom")
output_count = 1
if isinstance(custom, dict):
output_count = int(custom.get("nested_output_count", 1))
if output_count < 1:
return None
output_values = tuple(native_node.add_output() for _ in range(output_count))
values[node] = output_values[0] if output_count == 1 else output_values
continue
# ``ReLU(inplace=True)`` is an aliasing detail of the captured graph.
# The native kernel accepts that schema explicitly; other keyword
# combinations are rejected by this lowering.
if node.kwargs:
if (
op_name != "relu"
or node.kwargs not in ({"inplace": False}, {"inplace": True})
):
return None
if op_name not in _NATIVE_OPS:
return None
if op_name == "relu" and node in fused_relu_nodes:
source = node.args[0]
if source not in values:
return None
# The producer was lowered with a fused Conv+ReLU primitive; the
# ReLU value is an alias of that already-activated output.
values[node] = values[source]
layout_values[node] = layout_values.get(source, False)
continue
if op_name == "relu" and node in fused_add_relus.values():
source = node.args[0]
if not isinstance(source, Node) or source not in values:
return None
# The residual add is lowered as add_relu below, so the ReLU
# node observes the already-activated output without another
# native launch.
values[node] = values[source]
layout_values[node] = layout_values.get(source, False)
continue
def node_value(value: Any) -> Any | None:
if not isinstance(value, Node) or value not in values:
return None
return values[value]
def add_tensor_input(native_node: Any, value: Any) -> bool:
resolved = node_value(value)
if resolved is None or isinstance(resolved, tuple):
return False
native_node.add_input(resolved)
return True
if op_name == "conv2d":
if len(node.args) != 7:
return None
input_node, weight_node, bias_node, stride, padding, dilation, groups = node.args
fused_relu = fused_relu_convs.get(node)
use_conv_relu = fused_relu is not None and not peel_conv_bias
native_node = graph.create_node(
"conv2d_relu" if use_conv_relu else "conv2d",
node.name,
)
conv_input = channels_last_value(input_node)
if conv_input is None:
return None
native_node.add_input(conv_input)
folded_inputs = folded_native_inputs.get(node)
bias_input = None
bias_tensor = None
if folded_inputs is not None:
native_node.add_input(folded_inputs[0])
bias_input = folded_inputs[1]
folded_spec = folded_convs.get(node)
bias_tensor = folded_spec[2] if folded_spec is not None else None
else:
conv_weight = channels_last_value(weight_node)
if conv_weight is None:
return None
native_node.add_input(conv_weight)
if bias_node is None:
native_node.set_int_attr("has_bias", 0)
else:
bias_input = node_value(bias_node)
if bias_input is None:
return None
try:
bias_tensor = graph_module._get_attr(bias_node.target)
except (AttributeError, TypeError):
return None
if bias_input is None:
native_node.set_int_attr("has_bias", 0)
elif not peel_conv_bias or use_conv_relu:
native_node.add_input(bias_input)
native_node.set_int_attr("has_bias", 1)
else:
# the cuDNN call because cuDNN is slower with it. Keep the
# bias as a broadcast pointwise input after the convolution.
native_node.set_int_attr("has_bias", 0)
if not all(
_set_int_list_attr(native_node, key, value)
for key, value in (
("stride", stride),
("padding", padding),
("dilation", dilation),
)
):
return None
if isinstance(groups, bool) or not isinstance(groups, int):
return None
native_node.set_int_attr("groups", int(groups))
conv_value = native_node.add_output()
values[node] = conv_value
layout_values[node] = use_channels_last
if peel_conv_bias and bias_input is not None and not use_conv_relu:
if bias_tensor is None or not hasattr(bias_tensor, "shape"):
return None
bias_shape = tuple(int(item) for item in bias_tensor.shape)
if len(bias_shape) != 1:
return None
bias_view = graph.create_node("reshape", f"{node.name}_bias_view")
bias_view.add_input(bias_input)
bias_view.set_ints_attr("shape", [1, bias_shape[0], 1, 1])
bias_value = bias_view.add_output()
add_node = graph.create_node(
"add_relu" if fused_relu is not None else "add",
f"{node.name}_bias_add",
)
add_node.add_input(conv_value)
add_node.add_input(bias_value)
values[node] = add_node.add_output()
layout_values[node] = use_channels_last and fused_relu is not None
if fused_relu is not None:
fused_relu_nodes.add(fused_relu)
elif use_conv_relu and fused_relu is not None:
fused_relu_nodes.add(fused_relu)
continue
if op_name == "batch_norm":
if len(node.args) != 8:
return None
folded_batch_norm = next(
(
batch_norm
for batch_norm, _, _ in folded_convs.values()
if batch_norm is node
),
None,
)
if folded_batch_norm is not None:
values[node] = values[node.args[0]]
layout_values[node] = layout_values.get(node.args[0], False)
continue
input_node, running_mean, running_var, weight, bias, training, momentum, eps = node.args
native_node = graph.create_node("batch_norm", node.name)
if not add_tensor_input(native_node, input_node):
return None
optional_inputs = (
("has_running_mean", running_mean),
("has_running_var", running_var),
("has_weight", weight),
("has_bias", bias),
)
for attr_name, optional_node in optional_inputs:
if optional_node is None:
native_node.set_int_attr(attr_name, 0)
continue
if not add_tensor_input(native_node, optional_node):
return None
native_node.set_int_attr(attr_name, 1)
if not isinstance(training, bool) or not isinstance(momentum, numbers.Real) or not isinstance(eps, numbers.Real):
return None
native_node.set_int_attr("training", int(training))
native_node.set_float_attr("momentum", float(momentum))
native_node.set_float_attr("eps", float(eps))
values[node] = native_node.add_output()
layout_values[node] = False
continue
if op_name == "max_pool2d":
if len(node.args) != 7:
return None
input_node, kernel_size, stride, padding, dilation, ceil_mode, return_indices = node.args
if return_indices is not False or not isinstance(ceil_mode, bool):
return None
native_node = graph.create_node("max_pool2d", node.name)
if not add_tensor_input(native_node, input_node):
return None
for key, value in (
("kernel_size", kernel_size),
("stride", stride),
("padding", padding),
("dilation", dilation),
):
if not _set_int_list_attr(native_node, key, value):
return None
native_node.set_int_attr("ceil_mode", int(ceil_mode))
values[node] = native_node.add_output()
# The cuDNN tensor descriptor and output follow the input layout;
# a later convolution can therefore consume the max-pool result
# without an NCHW round-trip.
layout_values[node] = use_channels_last and layout_values.get(
input_node, False
)
continue
if op_name == "adaptive_avg_pool2d":
if len(node.args) != 2 or not _set_int_list_attr(
native_node := graph.create_node("adaptive_avg_pool2d", node.name),
"output_size",
node.args[1],
):
return None
if not add_tensor_input(native_node, node.args[0]):
return None
values[node] = native_node.add_output()
layout_values[node] = False
continue
if op_name == "flatten":
if len(node.args) != 2 or node.args[1] != 1 or node.kwargs:
return None
native_node = graph.create_node("flatten", node.name)
if not add_tensor_input(native_node, node.args[0]):
return None
native_node.set_int_attr("start_dim", 1)
native_node.set_int_attr("end_dim", -1)
values[node] = native_node.add_output()
layout_values[node] = False
continue
if op_name == "add" and node in fused_add_relus:
if len(node.args) == 2:
input_node, other_node = node.args
alpha = 1
elif len(node.args) == 3:
input_node, other_node, alpha = node.args
else:
return None
if (
alpha != 1
or not isinstance(input_node, Node)
or not isinstance(other_node, Node)
or input_node not in values
or other_node not in values
):
return None
fused = graph.create_node("add_relu", node.name)
fused.add_input(values[input_node])
fused.add_input(values[other_node])
values[node] = fused.add_output()
layout_values[node] = use_channels_last and (
layout_values.get(input_node, False)
or layout_values.get(other_node, False)
)
continue
# ``alpha`` argument, so their graph node has
# ``(input, other, alpha)`` even when alpha is the default 1, and
# keyword-only spellings record alpha in the node kwargs. Lower
# that contract to the native pointwise IR instead of falling back
# to a Python method call. Non-unit alpha becomes a scalar multiply
# and can be consumed by Stax's mul-add fusion pass.
if op_name in {"add", "sub"} and (
len(node.args) == 3
or (len(node.args) == 2 and "alpha" in (node.kwargs or {}))
):
if len(node.args) == 3 and not node.kwargs:
input_node, other_node, alpha = node.args
else:
input_node, other_node = node.args
alpha = node.kwargs.get("alpha", 1)
if not isinstance(input_node, Node) or not _is_scalar(alpha):
return None
if input_node not in values:
return None
if isinstance(other_node, Node) and other_node not in values:
return None
if alpha == 1:
binary = graph.create_node(op_name, node.name)
binary.add_input(values[input_node])
if isinstance(other_node, Node):
binary.add_input(values[other_node])
elif _is_scalar(other_node):
_set_scalar_attr(binary, other_node, 1)
else:
return None
values[node] = binary.add_output()
layout_values[node] = False
continue
if isinstance(other_node, Node):
scale = graph.create_node("mul", f"{node.name}_alpha")
scale.add_input(values[other_node])
_set_scalar_attr(scale, alpha, 1)
scaled_other = scale.add_output()
binary = graph.create_node(op_name, node.name)
binary.add_input(values[input_node])
binary.add_input(scaled_other)
values[node] = binary.add_output()
layout_values[node] = False
continue
if _is_scalar(other_node):
binary = graph.create_node(op_name, node.name)
binary.add_input(values[input_node])
_set_scalar_attr(binary, other_node * alpha, 1)
values[node] = binary.add_output()
layout_values[node] = False
continue
return None
if op_name == "linear":
if len(node.args) not in {2, 3} or any(
not isinstance(arg, Node) and arg is not None for arg in node.args
):
return None
input_node, weight_node = node.args[:2]
bias_node = node.args[2] if len(node.args) == 3 else None
if not isinstance(input_node, Node) or not isinstance(weight_node, Node):
return None
if bias_node is not None and not isinstance(bias_node, Node):
return None
if any(
value_node not in values
for value_node in (input_node, weight_node, bias_node)
if value_node is not None
):
return None
# One node, not a transpose plus a product plus an addition: the
# bias belongs in the product's epilogue, and adding it back
# separately costs a whole pass over the output.
if _native_runs_linear():
fused = graph.create_node("linear", node.name)
fused.add_input(values[input_node])
fused.add_input(values[weight_node])
if bias_node is not None:
fused.add_input(values[bias_node])
values[node] = fused.add_output()
layout_values[node] = False
continue
transpose = graph.create_node("t", f"{node.name}_weight_t")
transpose.add_input(values[weight_node])
transposed_weight = transpose.add_output()
matmul = graph.create_node("matmul", f"{node.name}_matmul")
matmul.add_input(values[input_node])
matmul.add_input(transposed_weight)
result = matmul.add_output()
if bias_node is not None:
add = graph.create_node("add", f"{node.name}_bias")
add.add_input(result)
add.add_input(values[bias_node])
result = add.add_output()
values[node] = result
layout_values[node] = False
continue
input_nodes: list[Node] = []
scalar_args: list[tuple[int, Any]] = []
for position, arg in enumerate(node.args):
if isinstance(arg, Node):
input_nodes.append(arg)
elif _is_scalar(arg):
scalar_args.append((position, arg))
else:
return None
if len(scalar_args) > 1:
return None
if op_name in {
"neg",
"pos",
"abs",
"sin",
"cos",
"exp",
"log",
"sigmoid",
"sqrt",
"square",
"tanh",
"relu",
}:
if len(node.args) != 1 or len(input_nodes) != 1:
return None
elif len(input_nodes) not in {1, 2} or len(node.args) not in {1, 2}:
return None
if any(input_node not in values for input_node in input_nodes):
return None
if op_name == "mm":
if len(node.args) != 2 or len(input_nodes) != 2:
return None
native_node = graph.create_node("mm", node.name)
native_node.add_input(values[input_nodes[0]])
native_node.add_input(values[input_nodes[1]])
values[node] = native_node.add_output()
layout_values[node] = False
continue
native_node = graph.create_node(op_name, node.name)
for input_node in input_nodes:
native_node.add_input(values[input_node])
if scalar_args:
_set_scalar_attr(native_node, scalar_args[0][1], scalar_args[0][0])
if op_name == "relu":
# Preserve the functional schema. The executor may call relu_
# only when the captured call explicitly requested mutation.
native_node.set_int_attr(
"inplace", int(node.kwargs.get("inplace", False))
)
values[node] = native_node.add_output()
layout_values[node] = False
try:
output_arg = graph_module.graph.output_node.args[0]
output_values = _native_value_leaves(output_arg, values)
output_spec = _native_output_spec(output_arg, values)
except (IndexError, KeyError, RuntimeError, TypeError):
return None
if not output_values or any(value is None for value in output_values):
return None
for output_value in output_values:
graph.register_output(output_value)
public_output_count = len(output_values)
registered_extra_outputs = 0
for extra_node in extra_output_nodes or []:
if extra_node not in values:
return None
extra_values = values[extra_node]
if isinstance(extra_values, tuple):
if not extra_values or any(value is None for value in extra_values):
return None
for extra_value in extra_values:
graph.register_output(extra_value)
registered_extra_outputs += len(extra_values)
else:
if extra_values is None:
return None
graph.register_output(extra_values)
registered_extra_outputs += 1
if use_fusion and not registered_extra_outputs:
graph.fuse()
return _NativeLowering(
graph_module,
graph,
attribute_targets,
constant_values,
output_count=public_output_count + registered_extra_outputs,
native_values=values,
output_spec=output_spec,
public_output_count=public_output_count,
)
class _AotShape(tuple):
"""Tensor metadata that supports both ``shape`` and ``shape()`` schemas."""
def __new__(cls, value: Any):
return super().__new__(cls, (int(item) for item in value))
def __call__(self) -> tuple[int, ...]:
return tuple(self)
class _AotNativeSymbol:
"""A symbolic Tensor value used while materializing an AOT backward graph."""
__slots__ = ("builder", "value", "shape")
def __init__(self, builder: "_AotNativeGraphBuilder", value: Any, shape: Any):
self.builder = builder
self.value = value
self.shape = _AotShape(shape)
def _binary(self, op_name: str, other: Any) -> "_AotNativeSymbol":
return self.builder.binary(op_name, self, other)
def __add__(self, other: Any) -> "_AotNativeSymbol":
return self._binary("add", other)
def __radd__(self, other: Any) -> "_AotNativeSymbol":
return self.builder.binary("add", other, self)
def __sub__(self, other: Any) -> "_AotNativeSymbol":
return self._binary("sub", other)
def __rsub__(self, other: Any) -> "_AotNativeSymbol":
return self.builder.binary("sub", other, self)
def __mul__(self, other: Any) -> "_AotNativeSymbol":
return self._binary("mul", other)
def __rmul__(self, other: Any) -> "_AotNativeSymbol":
return self.builder.binary("mul", other, self)
def __truediv__(self, other: Any) -> "_AotNativeSymbol":
return self._binary("div", other)
def __rtruediv__(self, other: Any) -> "_AotNativeSymbol":
return self.builder.binary("div", other, self)
def __neg__(self) -> "_AotNativeSymbol":
return self.builder.unary("neg", self)
def __pos__(self) -> "_AotNativeSymbol":
return self.builder.unary("pos", self)
def t(self) -> "_AotNativeSymbol":
return self.builder.unary("t", self, shape=self.shape[::-1])
def mm(self, other: Any) -> "_AotNativeSymbol":
return self.builder.binary("mm", self, other)
def matmul(self, other: Any) -> "_AotNativeSymbol":
return self.builder.binary("matmul", self, other)
def reshape(self, shape: Any) -> "_AotNativeSymbol":
return self.builder.reshape(self, shape)
def view(self, shape: Any) -> "_AotNativeSymbol":
return self.builder.reshape(self, shape)
def expand(self, shape: Any) -> "_AotNativeSymbol":
raise NotImplementedError("AOT native expand lowering is not implemented")
def sum(self, dim: Any = None, keepdim: bool = False) -> "_AotNativeSymbol":
return self.builder.sum(self, dim, keepdim)
def numel(self) -> int:
result = 1
for item in self.shape:
result *= item
return result
class _AotNativeTuple:
__slots__ = ("values",)
def __init__(self, values: tuple[_AotNativeSymbol, ...]):
self.values = values
class _AotNativeGraphBuilder:
"""Small native-IR builder used by the source-derived reverse pass."""
def __init__(self, native_module: Any):
self.native_module = native_module
self.graph = native_module.Graph()
@staticmethod
def _shape(value: Any) -> tuple[int, ...]:
return tuple(int(item) for item in getattr(value, "shape", ()))
@staticmethod
def _symbol(value: Any) -> _AotNativeSymbol | None:
return value if isinstance(value, _AotNativeSymbol) else None
def input(self, example_value: Any) -> _AotNativeSymbol:
return _AotNativeSymbol(self, self.graph.add_input(), self._shape(example_value))
def _add_inputs(self, native_node: Any, args: tuple[Any, ...]) -> list[_AotNativeSymbol]:
symbols: list[_AotNativeSymbol] = []
for value in args:
if not isinstance(value, _AotNativeSymbol):
raise TypeError("AOT native op received a non-Tensor argument")
native_node.add_input(value.value)
symbols.append(value)
return symbols
@staticmethod
def _broadcast_shape(lhs: tuple[int, ...], rhs: tuple[int, ...]) -> tuple[int, ...]:
result: list[int] = []
for left, right in zip(reversed(lhs), reversed(rhs)):
if left != right and left != 1 and right != 1:
raise ValueError(f"incompatible AOT shapes: {lhs} and {rhs}")
result.append(max(left, right))
longer = lhs if len(lhs) >= len(rhs) else rhs
result.extend(reversed(longer[: abs(len(lhs) - len(rhs))]))
return tuple(reversed(result))
def binary(self, op_name: str, lhs: Any, rhs: Any) -> _AotNativeSymbol:
lhs_symbol = self._symbol(lhs)
rhs_symbol = self._symbol(rhs)
if lhs_symbol is None and rhs_symbol is None:
if op_name == "add":
return lhs + rhs
if op_name == "sub":
return lhs - rhs
if op_name == "mul":
return lhs * rhs
if op_name == "div":
return lhs / rhs
raise NotImplementedError(f"AOT scalar operation is unsupported: {op_name}")
native_node = self.graph.create_node(op_name, f"aot_{op_name}_{len(self.graph.nodes)}")
shape = lhs_symbol.shape if lhs_symbol is not None else rhs_symbol.shape
if lhs_symbol is not None and rhs_symbol is not None:
native_node.add_input(lhs_symbol.value)
native_node.add_input(rhs_symbol.value)
shape = self._broadcast_shape(lhs_symbol.shape, rhs_symbol.shape)
else:
symbol = lhs_symbol if lhs_symbol is not None else rhs_symbol
scalar = rhs if lhs_symbol is not None else lhs
native_node.add_input(symbol.value)
_set_scalar_attr(native_node, scalar, 1 if lhs_symbol is not None else 0)
return _AotNativeSymbol(self, native_node.add_output(), shape)
def unary(
self,
op_name: str,
value: _AotNativeSymbol,
*,
shape: tuple[int, ...] | None = None,
) -> _AotNativeSymbol:
native_node = self.graph.create_node(op_name, f"aot_{op_name}_{len(self.graph.nodes)}")
native_node.add_input(value.value)
return _AotNativeSymbol(self, native_node.add_output(), shape or value.shape)
def helper(
self,
op_name: str,
args: tuple[_AotNativeSymbol, ...],
*,
attrs: dict[str, Any] | None = None,
shape: tuple[int, ...] | None = None,
outputs: int = 1,
) -> _AotNativeSymbol | _AotNativeTuple:
native_node = self.graph.create_node(op_name, f"aot_{op_name}_{len(self.graph.nodes)}")
symbols = self._add_inputs(native_node, args)
del symbols
for key, value in (attrs or {}).items():
if isinstance(value, bool) or isinstance(value, int):
native_node.set_int_attr(key, int(value))
elif isinstance(value, numbers.Real):
native_node.set_float_attr(key, float(value))
elif isinstance(value, (tuple, list)) and all(
isinstance(item, int) and not isinstance(item, bool) for item in value
):
native_node.set_ints_attr(key, [int(item) for item in value])
else:
raise TypeError(f"unsupported AOT native attribute: {key}={value!r}")
if outputs == 1:
return _AotNativeSymbol(self, native_node.add_output(), shape or args[0].shape)
return _AotNativeTuple(
tuple(
_AotNativeSymbol(self, native_node.add_output(), shape or args[0].shape)
for _ in range(outputs)
)
)
def reshape(self, value: _AotNativeSymbol, shape: Any) -> _AotNativeSymbol:
normalized = tuple(int(item) for item in shape)
return self.helper("reshape", (value,), attrs={"shape": normalized}, shape=normalized) # type: ignore[return-value]
def sum(
self, value: _AotNativeSymbol, dim: Any = None, keepdim: bool = False
) -> _AotNativeSymbol:
if dim is None:
return self.helper("sum", (value,), shape=()) # type: ignore[return-value]
dims = tuple(int(item) for item in (dim if isinstance(dim, (tuple, list)) else (dim,)))
normalized_dims = tuple(item if item >= 0 else item + len(value.shape) for item in dims)
shape = list(value.shape)
if keepdim:
for item in normalized_dims:
shape[item] = 1
else:
for item in sorted(normalized_dims, reverse=True):
shape.pop(item)
return self.helper(
"sum",
(value,),
attrs={"dim": normalized_dims, "keepdim": bool(keepdim)},
shape=tuple(shape),
) # type: ignore[return-value]
def _aot_derivative_specs() -> dict[str, tuple[Any, dict[str, str]]]:
"""Read the local derivative schema used by TensorPlay code generation."""
from pathlib import Path
from tools.codegen.model import parse_derivatives_yaml, parse_schema
yaml_path = Path(__file__).resolve().parents[2] / "config" / "derivatives.yaml"
result: dict[str, tuple[Any, dict[str, str]]] = {}
for definition in parse_derivatives_yaml(str(yaml_path)):
parsed = parse_schema(definition["name"])
formulas = {
key: value for key, value in definition.items() if key != "name"
}
result[parsed.func_name] = (parsed, formulas)
return result
def _aot_formula_python(formula: str, tensor_params: set[str]) -> str:
"""Compile one derivatives.yaml formula into a Python expression.
Shares the codegen expression AST (tokenizer + parser); the emitter
renders against the runtime formula env -- builder callables like
add/mul/t/sum plus get_tuple -- instead of the C++ text the generated
autograd nodes need.
"""
from tools.codegen.gen_autograd import (
BinOp, BoolLit, Braced, Call, Method, Neg, Num, StrLit, Var,
TENSOR_METHODS, parse_expr,
)
symbols = set(tensor_params) | {"grad", "grad_output", "result"}
def is_tensor(expr: Any) -> bool:
if isinstance(expr, Var):
return expr.name in symbols
if isinstance(expr, Neg):
return looks_tensor(expr.value)
if isinstance(expr, Method):
return expr.name.rstrip("_") in TENSOR_METHODS
if isinstance(expr, Call):
leaf = expr.callee.split("::")[-1].split("<")[0]
return leaf not in ("Scalar",)
if isinstance(expr, BinOp):
return is_tensor(expr.left) or looks_tensor(expr.right)
return False
def looks_tensor(expr: Any) -> bool:
return is_tensor(expr) or isinstance(expr, BinOp)
def emit(expr: Any) -> str:
if isinstance(expr, Num):
return expr.text
if isinstance(expr, BoolLit):
return "True" if expr.text == "true" else "False"
if isinstance(expr, StrLit):
return expr.text
if isinstance(expr, Var):
return expr.name
if isinstance(expr, Neg):
inner = emit(expr.value)
return f"neg({inner})" if looks_tensor(expr.value) else f"-{inner}"
if isinstance(expr, Braced):
# Python target: a braced list renders as a tuple (builder.sum
# dims, reshape shapes), matching _aot_default_value.
items = [emit(item) for item in expr.items]
if len(items) == 1:
return f"({items[0]},)"
return f"({', '.join(items)})"
if isinstance(expr, Call):
args = ", ".join(emit(a) for a in expr.args)
get = re.fullmatch(r"std::get<(\d+)>", expr.callee)
if get:
return f"get_tuple({get.group(1)}, {args})"
callee = expr.callee.split("::")[-1]
return f"{callee}({args})"
if isinstance(expr, Method):
recv = emit(expr.receiver)
args = ", ".join(emit(a) for a in expr.args)
name = expr.name
base = name[:-1] if name.endswith("_") and name[:-1] in TENSOR_METHODS else name
if base in TENSOR_METHODS:
return f"{TENSOR_METHODS[base]}({recv}, {args})" if args \
else f"{TENSOR_METHODS[base]}({recv})"
return f"{recv}.{name}({args})" if args else f"{recv}.{name}()"
if isinstance(expr, BinOp):
left = emit(expr.left)
right = emit(expr.right)
left_tensor = is_tensor(expr.left)
right_tensor = looks_tensor(expr.right)
if expr.op in "+-" and left_tensor:
return f"{'add' if expr.op == '+' else 'sub'}({left}, {right})"
if expr.op == "*" and left_tensor:
return f"mul({left}, {right})"
if expr.op == "/" and left_tensor:
return f"div({left}, {right})"
if expr.op == "*" and right_tensor:
return f"mul({right}, {left})"
if expr.op == "-" and right_tensor:
return f"neg(sub({right}, {left}))"
return f"({left} {expr.op} {right})"
raise NotImplementedError(f"AOT formula node is unsupported: {expr!r}")
return emit(parse_expr(formula))
def _build_aot_formula_env(
builder: _AotNativeGraphBuilder,
*,
batch_norm_cache: dict[tuple[int, ...], _AotNativeTuple],
) -> dict[str, Any]:
def binary(name: str):
return lambda lhs, rhs: builder.binary(name, lhs, rhs)
def unary(name: str):
return lambda value: builder.unary(name, value)
def get_tuple(index: int, value: _AotNativeTuple):
return value.values[int(index)]
def batch_norm_backward(*args: Any):
key = tuple(id(item) if isinstance(item, _AotNativeSymbol) else hash(repr(item)) for item in args)
cached = batch_norm_cache.get(key)
if cached is not None:
return cached
grad, input_value, weight, running_mean, running_var, training, eps = args
tensor_args = (grad, input_value)
attrs = {
"has_weight": weight is not None,
"has_running_mean": running_mean is not None,
"has_running_var": running_var is not None,
"training": bool(training),
"eps": float(eps),
}
optional = tuple(item for item in (weight, running_mean, running_var) if item is not None)
value = builder.helper(
"batch_norm_backward",
tensor_args + optional,
attrs=attrs,
shape=input_value.shape,
outputs=3,
)
assert isinstance(value, _AotNativeTuple)
batch_norm_cache[key] = value
return value
def conv_grad(name: str):
def invoke(grad, input_value, weight, stride, padding, dilation, groups):
return builder.helper(
name,
(grad, input_value, weight),
attrs={
"stride": tuple(stride),
"padding": tuple(padding),
"dilation": tuple(dilation),
"groups": int(groups),
},
shape=(
input_value.shape
if name.endswith("input")
else weight.shape
if name.endswith("weight")
else (grad.shape[1],)
),
)
return invoke
def max_pool_backward(grad, input_value, kernel_size, stride, padding, dilation, ceil_mode):
return builder.helper(
"max_pool2d_backward",
(grad, input_value),
attrs={
"kernel_size": tuple(kernel_size),
"stride": tuple(stride),
"padding": tuple(padding),
"dilation": tuple(dilation),
"ceil_mode": bool(ceil_mode),
},
shape=input_value.shape,
)
def adaptive_avg_pool_backward(grad, input_value):
return builder.helper(
"adaptive_avg_pool2d_backward", (grad, input_value), shape=input_value.shape
)
def threshold_backward(grad, output, threshold):
return builder.helper(
"threshold_backward",
(grad, output),
attrs={"threshold": threshold},
shape=grad.shape,
)
return {
"add": binary("add"),
"sub": binary("sub"),
"mul": binary("mul"),
"div": binary("div"),
"matmul": binary("matmul"),
"mm": binary("mm"),
"neg": unary("neg"),
"pos": unary("pos"),
"t": unary("t"),
"reshape": builder.reshape,
"sum": builder.sum,
"get_tuple": get_tuple,
"batch_norm_backward": batch_norm_backward,
"conv2d_grad_input": conv_grad("conv2d_grad_input"),
"conv2d_grad_weight": conv_grad("conv2d_grad_weight"),
"conv2d_grad_bias": conv_grad("conv2d_grad_bias"),
"max_pool2d_backward": max_pool_backward,
"adaptive_avg_pool2d_backward": adaptive_avg_pool_backward,
"threshold_backward": threshold_backward,
}
def _aot_schema_for(
specs: dict[str, tuple[Any, dict[str, str]]], op_name: str
) -> tuple[Any, dict[str, str]] | None:
candidates = [op_name]
if op_name in {"add", "sub", "mul", "div"}:
candidates.insert(0, f"{op_name}.Tensor")
for candidate in candidates:
if candidate in specs:
return specs[candidate]
return None
def _aot_default_value(value: Any) -> Any:
if value is None:
return None
if value == "true":
return True
if value == "false":
return False
if value == "{}":
return ()
if isinstance(value, str) and value.startswith("{") and value.endswith("}"):
return tuple(int(item.strip()) for item in value[1:-1].split(",") if item.strip())
try:
return int(value)
except (TypeError, ValueError):
try:
return float(value)
except (TypeError, ValueError):
return value
def _aot_add_adjoint(
builder: _AotNativeGraphBuilder,
adjoints: dict[Node, _AotNativeSymbol],
target: Any,
contribution: Any,
) -> bool:
if not isinstance(target, Node) or not isinstance(contribution, _AotNativeSymbol):
return contribution is None
previous = adjoints.get(target)
adjoints[target] = contribution if previous is None else builder.binary(
"add", previous, contribution
)
return True
def _build_aot_backward(
graph_module: GraphModule,
native_module: Any,
forward_lowering: _NativeLowering,
saved_nodes: list[Node],
runtime_values: dict[Node, Any],
runtime_inputs: list[Any],
public_node: Node,
) -> tuple[Any, list[int]] | None:
try:
specs = _aot_derivative_specs()
except (ImportError, ModuleNotFoundError, OSError):
# Derivative tooling/config unavailable: AOT lowering is optional;
# the caller falls back to the non-AOT native path.
return None
builder = _AotNativeGraphBuilder(native_module)
external_nodes = list(graph_module.graph.placeholders) + [
node for node in graph_module.graph.nodes if node.op == "get_attr"
]
external_symbols: list[_AotNativeSymbol] = []
forward_symbols: dict[Node, _AotNativeSymbol] = {}
for index, node in enumerate(external_nodes):
symbol = builder.input(runtime_inputs[index])
external_symbols.append(symbol)
forward_symbols[node] = symbol
saved_symbols: list[_AotNativeSymbol] = []
for node in saved_nodes:
actual = runtime_values.get(node)
if actual is None:
return None
symbol = builder.input(actual)
saved_symbols.append(symbol)
forward_symbols[node] = symbol
tangent = builder.input(runtime_values[public_node])
adjoints: dict[Node, _AotNativeSymbol] = {public_node: tangent}
batch_norm_cache: dict[tuple[int, ...], _AotNativeTuple] = {}
formula_env = _build_aot_formula_env(builder, batch_norm_cache=batch_norm_cache)
for node in reversed(graph_module.graph.nodes):
if node.op in {"placeholder", "get_attr", "output"}:
continue
grad = adjoints.get(node)
if grad is None:
continue
op_name = _target_name(node.target)
if op_name == "linear":
if len(node.args) not in {2, 3} or not all(
isinstance(item, Node) for item in node.args[:2]
):
return None
input_node, weight_node = node.args[:2]
bias_node = node.args[2] if len(node.args) == 3 else None
if bias_node is not None and not isinstance(bias_node, Node):
return None
input_value = forward_symbols[input_node]
weight_value = forward_symbols[weight_node]
weight_t = builder.unary("t", weight_value, shape=weight_value.shape[::-1])
input_grad = builder.helper(
"matmul_backward_self", (grad, input_value, weight_t), shape=input_value.shape
)
weight_t_grad = builder.helper(
"matmul_backward_other", (grad, input_value, weight_t), shape=weight_t.shape
)
if not _aot_add_adjoint(builder, adjoints, input_node, input_grad):
return None
if not _aot_add_adjoint(
builder, adjoints, weight_node, builder.unary("t", weight_t_grad, shape=weight_value.shape)
):
return None
if bias_node is not None:
dims = tuple(range(max(0, len(grad.shape) - 1)))
bias_grad = builder.sum(grad, dims, False) if dims else grad
if not _aot_add_adjoint(builder, adjoints, bias_node, bias_grad):
return None
continue
if op_name == "flatten":
if not node.args or not isinstance(node.args[0], Node):
return None
source_value = runtime_values.get(node.args[0])
if source_value is None:
return None
# the input storage. Keep that metadata-only dependency out of
# the saved-tensor list so the rebuilt graph can omit the pooled
# activation while still producing the exact reshape backward.
contribution = builder.reshape(grad, tuple(source_value.shape))
if not _aot_add_adjoint(builder, adjoints, node.args[0], contribution):
return None
continue
schema = _aot_schema_for(specs, op_name)
if schema is None:
return None
parsed, formulas = schema
if node.op == "call_method":
arg_values = (node.args[0],) if node.args else ()
elif op_name == "batch_norm":
names = (
"input", "running_mean", "running_var", "weight", "bias",
"training", "momentum", "eps",
)
arg_values = tuple(zip(names, node.args))
else:
arg_values = tuple(zip((arg.name for arg in parsed.args), node.args))
if op_name == "batch_norm":
context = {name: value for name, value in arg_values}
else:
context = dict(arg_values)
for arg in parsed.args:
if arg.name not in context and arg.default is not None:
context[arg.name] = _aot_default_value(arg.default)
context["grad"] = grad
# others only need metadata or their inputs (e.g. adaptive average
# pooling). A pruned saved-tensor set must not require a symbol for
# a result that the selected formula never reads.
context["result"] = forward_symbols.get(node)
tensor_params = {
name for name, value in context.items() if isinstance(value, Node)
}
env = dict(formula_env)
env.update(
{
name: forward_symbols.get(value) if isinstance(value, Node) else value
for name, value in context.items()
}
)
try:
for arg_name, formula in formulas.items():
target = context.get(arg_name)
if not isinstance(target, Node):
continue
translated = _aot_formula_python(formula, tensor_params)
contribution = eval(translated, {"__builtins__": {}}, env)
if not _aot_add_adjoint(builder, adjoints, target, contribution):
return None
except (KeyError, NameError, NotImplementedError, TypeError, ValueError, RuntimeError):
return None
grad_positions: list[int] = []
for index, node in enumerate(external_nodes):
actual = runtime_inputs[index]
if not getattr(actual, "requires_grad", False):
continue
contribution = adjoints.get(node)
if contribution is None:
continue
builder.graph.register_output(contribution.value)
grad_positions.append(index)
if not grad_positions:
return None
needed_saved_nodes = [
node
for node, symbol in zip(saved_nodes, saved_symbols)
if getattr(symbol.value, "use_count", 0) != 0
]
return builder.graph, grad_positions, needed_saved_nodes
class _AotNativeLowering:
def __init__(
self,
graph_module: GraphModule,
forward_graph: Any,
backward_graph: Any,
attribute_targets: list[str],
grad_positions: list[int],
) -> None:
self.graph_module = graph_module
self.forward_graph = forward_graph
self.backward_graph = backward_graph
self.placeholders = graph_module.graph.placeholders
self.attribute_targets = attribute_targets
self.grad_positions = list(grad_positions)
self.input_count = len(self.placeholders) + len(self.attribute_targets)
self._tensorplay_codegen = "stax-aot-native"
self._tensorplay_backward_codegen = "stax-aot-native"
lowering = self
from ..autograd import Function
class _AotAutogradFunction(Function):
@staticmethod
def forward(ctx: Any, *inputs: Any) -> Any:
outputs = lowering.forward_graph.execute(list(inputs))
ctx.save_for_backward(*inputs, *outputs[1:])
return outputs[0]
@staticmethod
def backward(ctx: Any, *grad_outputs: Any) -> tuple[Any, ...]:
grad_output = grad_outputs[0] if grad_outputs else None
if grad_output is None:
return (None,) * lowering.input_count
saved = list(ctx.saved_tensors)
outputs = lowering.backward_graph.execute([*saved, grad_output])
by_position = dict(zip(lowering.grad_positions, outputs))
return tuple(by_position.get(index) for index in range(lowering.input_count))
self._autograd_function = _AotAutogradFunction
def _bind_inputs(self, *args: Any, **kwargs: Any) -> list[Any]:
bound = self.graph_module.signature.bind_partial(*args, **kwargs)
bound.apply_defaults()
inputs = [
bound.arguments[node.target if isinstance(node.target, str) else node.name]
for node in self.placeholders
]
inputs.extend(self.graph_module._get_attr(target) for target in self.attribute_targets)
return inputs
def __call__(self, *args: Any, **kwargs: Any) -> Any:
inputs = self._bind_inputs(*args, **kwargs)
import tensorplay
if not tensorplay.is_grad_enabled() or not any(
getattr(value, "requires_grad", False) for value in inputs
):
return self.forward_graph.execute(inputs)[0]
return self._autograd_function.apply(*inputs)
def _lower_aot_native(
graph_module: GraphModule,
example_inputs: list[Any],
) -> _AotNativeLowering | None:
"""Build separate native forward/backward graphs at the AOT boundary."""
try:
import tensorplay
native_module = getattr(tensorplay._C, "_stax", None)
tensor_type = tensorplay.Tensor
except (AttributeError, ImportError):
return None
if native_module is None or not hasattr(native_module.Graph, "execute"):
return None
if len(example_inputs) != len(graph_module.graph.placeholders):
return None
if any(not isinstance(value, tensor_type) for value in example_inputs):
return None
if not tensorplay.is_grad_enabled():
return None
external_nodes = list(graph_module.graph.placeholders) + [
node for node in graph_module.graph.nodes if node.op == "get_attr"
]
attribute_targets = [node.target for node in external_nodes if node.op == "get_attr"]
runtime_inputs = list(example_inputs)
runtime_inputs.extend(graph_module._get_attr(target) for target in attribute_targets)
if not any(getattr(value, "requires_grad", False) for value in runtime_inputs):
return None
saved_nodes = [
node
for node in graph_module.graph.nodes
if node.op in {"call_function", "call_method"}
]
output_values = [
value for output in graph_module.graph.outputs for value in _nodes(output.args)
]
if len(output_values) != 1:
return None
public_node = output_values[0]
forward_lowering = _lower_native(
graph_module,
example_inputs,
use_fusion=False,
extra_output_nodes=saved_nodes,
)
if forward_lowering is None:
return None
if len(runtime_inputs) != len(forward_lowering.graph.inputs):
return None
# Training BatchNorm updates running buffers during forward. A compiler
# trace must not perform that update a second time; restore non-gradient
# capture path separates tracing state from the user execution state.
snapshots: list[tuple[Any, Any]] = []
seen_attributes: set[int] = set()
try:
for target in attribute_targets:
value = graph_module._get_attr(target)
if (
isinstance(value, tensor_type)
and not getattr(value, "requires_grad", False)
and id(value) not in seen_attributes
):
snapshots.append((value, value.detach().clone()))
seen_attributes.add(id(value))
with tensorplay.no_grad():
forward_outputs = forward_lowering.graph.execute(runtime_inputs)
except (AttributeError, RuntimeError, TypeError, ValueError):
return None
finally:
if snapshots:
with tensorplay.no_grad():
for value, snapshot in snapshots:
value.copy_(snapshot)
if len(forward_outputs) != 1 + len(saved_nodes):
return None
runtime_values: dict[Node, Any] = {public_node: forward_outputs[0]}
for index, node in enumerate(saved_nodes, start=1):
runtime_values[node] = forward_outputs[index]
built = _build_aot_backward(
graph_module,
native_module,
forward_lowering,
saved_nodes,
runtime_values,
runtime_inputs,
public_node,
)
if built is None:
return None
_, grad_positions, needed_saved_nodes = built
# The first graph is a shape/materialization graph. Rebuild the forward
# graph with only the values that the source-derived backward graph reads,
# intermediate until backward.
forward_lowering = _lower_native(
graph_module,
example_inputs,
use_fusion=False,
extra_output_nodes=needed_saved_nodes,
)
if forward_lowering is None:
return None
rebuilt = _build_aot_backward(
graph_module,
native_module,
forward_lowering,
needed_saved_nodes,
runtime_values,
runtime_inputs,
public_node,
)
if rebuilt is None:
return None
backward_graph, grad_positions, rebuilt_saved_nodes = rebuilt
if rebuilt_saved_nodes != needed_saved_nodes:
return None
return _AotNativeLowering(
graph_module,
forward_lowering.graph,
backward_graph,
attribute_targets,
grad_positions,
)
[docs]
def stax(
graph_module: GraphModule,
example_inputs: list[Any],
*,
mode: str | None = None,
options: dict[str, Any] | None = None,
name: str | None = None,
dynamic: bool | None = None,
strict_native: bool = False,
**kwargs: Any,
):
"""Compile one canonical graph and return an executable callable.
``example_inputs`` and backend options are part of the same contract as
metadata in the frontend and uses the native graph when its lowering
contract is satisfied. ``strict_native`` makes a failed lowering a hard
compiler error, so a benchmark can never report the Python GraphModule
executor as compiled performance.
"""
del name, kwargs
if mode not in {None, "default", "reduce-overhead", "max-autotune", "max-autotune-no-cudagraphs"}:
raise RuntimeError(f"unknown Stax optimization mode: {mode!r}")
if options is not None:
if not isinstance(options, dict):
raise TypeError(f"options must be a dict, got {type(options)!r}")
unknown = set(options).difference(
{"stax.native", "stax.fusion", "stax.triton"}
)
if unknown:
raise RuntimeError(
f"Unexpected Stax optimization option(s): {sorted(unknown)!r}"
)
if any(not isinstance(value, bool) for value in options.values()):
raise RuntimeError("Stax optimization options must be bool values")
use_native = options is None or options.get("stax.native", True)
use_fusion = options is None or options.get("stax.fusion", True)
use_triton = options is None or options.get("stax.triton", True)
if use_native and use_fusion:
fused_cpu_graph = _lower_cpu_fused_pointwise(
graph_module,
example_inputs,
strict_native=strict_native,
dynamic=bool(dynamic is True),
)
if fused_cpu_graph is not None:
graph_module._stax_native_graph = fused_cpu_graph.graph
return fused_cpu_graph
# A region whose tail is a reduction folds the whole expression into
# the reduction loop: one pass over the input, no intermediate.
fused_cpu_reduction = _lower_cpu_fused_reduction(
graph_module,
example_inputs,
strict_native=strict_native,
dynamic=bool(dynamic is True),
)
if fused_cpu_reduction is not None:
graph_module._stax_codegen = "stax-fused-cpu-reduce"
return fused_cpu_reduction
# A region whose reductions sit in the middle stages per row: each
# reduction folds the row to one value that the following work reads
# as a broadcast, so the region still reads its inputs once.
row_fused_cpu = _lower_cpu_row_fusion(
graph_module,
example_inputs,
strict_native=strict_native,
dynamic=bool(dynamic is True),
)
if row_fused_cpu is not None:
graph_module._stax_codegen = "stax-fused-cpu-rowfuse"
return row_fused_cpu
if use_native and use_triton:
# Keep Triton optional and lazy. Importing tensorplay on a CPU-only
# machine must not import Triton or its compiler toolchain.
try:
first = example_inputs[0]
is_cuda = first.device.is_cuda()
except (AttributeError, IndexError):
is_cuda = False
if is_cuda and use_fusion:
# A region whose reductions sit in the middle keeps its row
# resident across every stage; splitting it at each reduction
# would stream the input once per stage instead.
row_fused_cuda = _lower_cuda_row_fusion(
graph_module,
example_inputs,
strict_native=strict_native,
dynamic=bool(dynamic is True),
)
if row_fused_cuda is not None:
graph_module._stax_codegen = "stax-fused-cuda-rowfuse"
return row_fused_cuda
if is_cuda:
from .codegen.triton import (
compile_graph_module as compile_triton_graph,
)
triton_graph = compile_triton_graph(
graph_module,
example_inputs,
mode=mode,
strict_native=strict_native,
)
if triton_graph is not None:
graph_module._stax_codegen = "triton"
return triton_graph
if use_native and getattr(graph_module.root, "training", False):
aot_graph = _lower_aot_native(graph_module, example_inputs)
if aot_graph is not None:
graph_module._stax_native_graph = aot_graph.forward_graph
return aot_graph
if strict_native and any(
getattr(graph_module._get_attr(node.target), "requires_grad", False)
for node in graph_module.graph.nodes
if node.op == "get_attr"
):
raise RuntimeError(
"AOT backward graph for the captured training region"
)
native_graph = (
_lower_native(graph_module, example_inputs, use_fusion=use_fusion)
if use_native
else None
)
if native_graph is not None:
graph_module._stax_native_graph = native_graph.graph
return native_graph
if use_native and use_fusion:
# Nothing claimed the region whole. Its fusible runs are still worth
# compiling: each becomes one kernel, and the operators between them
# run as captured instead of the region losing every compiled route.
segmented = _lower_cpu_segmented(
graph_module,
example_inputs,
strict_native=strict_native,
dynamic=bool(dynamic is True),
)
if segmented is not None:
graph_module._stax_codegen = "stax-fused-cpu-segments"
return segmented
if strict_native:
raise RuntimeError(
"strict_native Stax lowering failed: captured graph has no native executable"
)
# No native executable exists for this graph (scalar placeholders,
# factory-only regions, unsupported surface). Fall back to the
# generated Python executor so the region still runs with captured
# semantics instead of failing to compile.
return graph_module.recompile()Help improve this page
Found an error, an unclear step, or a missing example?
Was this page helpful?

