latest (dev)
Copy
Latest development documentation · Updated 2026-10-15
Source code for tensorplay.compiler.backends.stax.backend
"""Compiler entry for the Stax backend.
The layers below hold the work: :mod:`loops` for the IR every device is
lowered to, :mod:`graph_lowering` for the captured-graph walk, and
and re-exports their names, so ``backend`` stays the single import
surface for the code generators and the tests.
"""
from __future__ import annotations
import inspect
import os
import sys
from typing import Any
from ....graph import GraphModule
# Option patch each compile ``mode`` selects, keyed by the backend option
# namespace. ``default`` leaves every knob at its built-in value;
# ``reduce-overhead`` replays the artifact through CUDA graphs;
# ``max-autotune(-no-cudagraphs)`` additionally selects the exhaustive
# search tier: the widened pointwise candidate table plus coordinate-descent
# refinement of every benchmark winner.
_MODE_OPTIONS: dict[str, dict[str, bool]] = {
"default": {},
"reduce-overhead": {
"stax.cudagraphs": True,
},
"max-autotune-no-cudagraphs": {
"stax.max_autotune": True,
"stax.coordinate_descent_tuning": True,
},
"max-autotune": {
"stax.max_autotune": True,
"stax.cudagraphs": True,
"stax.coordinate_descent_tuning": True,
},
}
# Every backend option key accepted by ``stax`` (and therefore by explicit
# ``options`` dicts and mode patches alike).
_STAX_OPTIONS = (
"stax.fusion",
"stax.cuda_codegen",
"stax.triton",
"stax.cudagraphs",
"stax.max_autotune",
"stax.coordinate_descent_tuning",
)
def _is_scalar(value: Any) -> bool:
"""Whether a captured graph argument is a plain Python number.
A graph walk has to tell a constant apart from a produced value before it
can emit one: a constant becomes a literal inside the kernel body, while a
produced value becomes a load. Booleans count as numbers here because a
captured ``True``/``False`` is written into the body as 1.0/0.0, and
complex is excluded because a kernel body has no complex literal form.
"""
return isinstance(value, (bool, int, float))
def list_mode_options(mode: str | None = None) -> dict[str, Any]:
"""Return the optimization options each compile ``mode`` selects.
With ``mode`` set, returns that mode's option patch; with ``mode``
unset, returns the full mode-to-options mapping. Unknown modes raise.
The patch feeds the same validation path as explicit backend options,
so options supplied by a caller override the mode patch per key.
"""
try:
return dict(_MODE_OPTIONS[mode]) if mode else dict(_MODE_OPTIONS)
except KeyError as exc:
raise RuntimeError(
f"Unrecognized mode={mode}, should be one of: "
f"{', '.join(_MODE_OPTIONS)}"
) from exc
[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, *_MODE_OPTIONS):
raise RuntimeError(f"unknown Stax optimization mode: {mode!r}")
# A mode's option patch is applied first; explicit options overlay it
# per key (a mode selects defaults, an explicit option wins).
resolved = dict(_MODE_OPTIONS[mode or "default"])
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_OPTIONS)
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")
resolved.update(options)
use_fusion = resolved.get("stax.fusion", True)
use_cuda_codegen = resolved.get("stax.cuda_codegen", False)
use_triton = resolved.get("stax.triton", True)
max_autotune = resolved.get("stax.max_autotune", False)
coordinate_descent_tuning = resolved.get(
"stax.coordinate_descent_tuning", False
)
cudagraphs_requested = resolved.get("stax.cudagraphs", False)
compiled = _lower_stax_region(
graph_module,
example_inputs,
use_fusion=use_fusion,
use_cuda_codegen=use_cuda_codegen,
use_triton=use_triton,
max_autotune=max_autotune,
coordinate_descent_tuning=coordinate_descent_tuning,
dynamic=dynamic,
strict_native=strict_native,
)
if not cudagraphs_requested or isinstance(compiled, GraphModule):
return compiled
from ..cudagraphs import cudagraph_wrap
wrapped, reason = cudagraph_wrap(
compiled, graph_module, example_inputs, dynamic=dynamic
)
if reason is not None:
graph_module._stax_cudagraph_skip_reason = reason
return compiled
return wrapped
# Operator tracing resolves composites and overloads before lowering. Symbolic
# inference also accepts canonical calls so size expressions remain graph values.
stax.lowers_operator_graphs = True
stax.supports_symbolic_shapes = True
def _trace_decompositions():
from .op_lowerings import trace_decompositions
return trace_decompositions()
# An operation with no lowering, template or library call of its own is traced
# as the parts it decomposes into, which are lowered, instead of reaching the
# region as a call the framework makes.
stax.trace_decompositions = _trace_decompositions
def _publish_codegen(compiled, tag: str, *, backward: bool) -> None:
"""Report on the artifact itself which route produced it.
One place, so every route's answer is visible the same way, and a caller
can tell a compiled region from an interpreted one without knowing which
route claimed it.
"""
try:
compiled._tensorplay_codegen = tag # type: ignore[attr-defined]
compiled._tensorplay_backward_codegen = tag if backward else None # type: ignore[attr-defined]
except AttributeError: # noqa: BLE001 - an artifact that cannot carry it
pass
def _lower_stax_region(
graph_module: GraphModule,
example_inputs: list[Any],
*,
use_fusion: bool,
use_cuda_codegen: bool,
use_triton: bool,
max_autotune: bool,
coordinate_descent_tuning: bool,
dynamic: bool | None,
strict_native: bool,
):
"""Lower 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.
"""
strict = bool(strict_native)
dynamic_shapes = bool(dynamic is True)
is_backward = bool(graph_module.meta.get("aot_backward", False))
training = is_backward or getattr(graph_module.root, "training", False) or any(
getattr(value, "requires_grad", False) for value in example_inputs
)
try:
on_cuda = example_inputs[0].device.is_cuda()
except (AttributeError, IndexError):
on_cuda = False
if strict and training 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")
# The region is lowered, scheduled and printed, and what that produces is
# what the caller calls. A region that cannot be lowered is a region the
# framework runs itself, which is what falling back is for.
from .graph_lowering import GraphLowering
if any(
getattr(node.target, "__name__", None) == "auto_functionalized"
for node in graph_module.graph.nodes
if node.op == "call_function"
):
# A mutating operator with no functional form reaches this backend as
# its functional stand-in; it is lowered as the copies and the
# operator writing into them.
from .graph_passes.post_grad import decompose_auto_functionalized
decompose_auto_functionalized(graph_module.graph)
graph_module.recompile()
from tensorplay.graph.experimental.symbolic_shapes import ShapeEnv
graph = GraphLowering(
graph_module,
example_inputs,
shape_env=ShapeEnv(duck_shape=not is_backward) if dynamic_shapes else None,
cpp_wrapper=use_cuda_codegen,
aot_mode=training,
extern_node_serializer=None,
is_inference=not training,
is_backward=is_backward,
)
# Everything from here on is being done about this region, and anything
# asked along the way -- what a kernel reads, how big an extent is, what a
# buffer is written into -- is answered by the region it belongs to. So
# the region is the one that is current for all of it.
from .loops import set_graph
from .loops import set_fake_mode
from tensorplay._higher_order_ops._hop_base import FakeTensorMode
from .loops import Debug
from .virtualized import V as VirtualMachine
from tensorplay._C import (
_enter_lowered_graph_scope as _enter_lowered_graph_scope,
_exit_lowered_graph_scope as _exit_lowered_graph_scope,
)
# Layouts and timings asked of operators while planning are asked under
# the conditions the written-out code runs them in.
_enter_lowered_graph_scope()
try:
# A recorder of what was emitted is current for the whole of the
# compilation, because both the schedule and the region's own picture
# are asked of it by name while they are being decided, and neither can
# say where to write unless something has already said that it wants to
# be written at all. The fake mode is entered as well as installed:
# fallback operators dispatch through the Python layer only while a
# mode sits on the dispatch stack, and a stack with no mode makes the
# generated Python-dispatch kernel pop nothing on entry.
fake_mode = FakeTensorMode(allow_non_fake_inputs=True)
with (
set_fake_mode(fake_mode),
VirtualMachine.set_fake_mode(fake_mode),
FakeTensorMode(allow_non_fake_inputs=True),
set_graph(graph),
VirtualMachine.set_debug_handler(Debug()),
):
graph.run(*example_inputs)
compiled_module = graph.compile_to_module()
except NotImplementedError as exc:
# A region whose printed form this compiler does not cover has no built
# artifact, and an artifact that cannot be entered is worse than none:
# the failure would arrive at the first call rather than here. So a
# caller that did not ask for a built form gets the region run as it was
# written, and a caller that did asks for the failure instead.
if os.environ.get("TP_STAX_LOWER_DEBUG"):
print(
f"[stax-lower] uncovered: {type(exc).__name__}: {exc}",
file=sys.stderr,
)
if strict:
raise RuntimeError(
"strict_native Stax lowering failed: captured graph has no built form"
) from exc
return graph_module.recompile()
except Exception as exc:
if os.environ.get("TP_STAX_LOWER_DEBUG"):
import traceback as _tb
print(
f"[stax-lower] unbuilt: {type(exc).__name__}: {exc}",
file=sys.stderr,
)
_tb.print_exc(limit=30, file=sys.stderr)
if strict:
raise RuntimeError(
"strict_native Stax lowering failed: captured graph has no built form"
) from exc
return graph_module.recompile()
finally:
_exit_lowered_graph_scope()
# The generated entry point takes the region's arguments as one sequence,
# because that is how the written-out code receives them, and it hands back
# a sequence of results for the same reason. Everything that calls a
# compiled region passes ordinary arguments and expects an ordinary result,
# so the two conventions are met here rather than making every caller know
# which one this one happens to use -- and a region that produced one result
# produces that result, rather than a sequence holding it.
module_call = compiled_module.call
input_guard = None
if dynamic_shapes:
from .shape_guards import build_input_guard
input_guard = build_input_guard(graph)
single_output = bool(getattr(graph, "single_output", False))
# The generated entry point takes the region's arguments as one sequence,
# in the order the region's placeholders stand. The artifact is reached
# through the same signature the region was captured under, so keyword
# and default arguments are bound back to their parameter names and
# re-ordered into that sequence using the captured signature.
signature = graph_module.signature
# A call that hands every captured argument by position, in the order
# the placeholders stand, needs no binding: that is how the autograd
# wrapper and every compiled caller call a region.
placeholder_names = [
node.target if isinstance(node.target, str) else node.name
for node in graph_module.graph.placeholders
]
positional = signature is not None and [
name for name, parameter in signature.parameters.items()
if parameter.kind in (
inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD
)
] == placeholder_names and len(signature.parameters) == len(placeholder_names)
num_placeholders = len(placeholder_names)
def ordered_inputs(args, kwargs):
if signature is None or (
positional and not kwargs and len(args) == num_placeholders
):
ordered = list(args)
else:
bound = signature.bind_partial(*args, **kwargs)
bound.apply_defaults()
ordered = []
for node in graph_module.graph.placeholders:
parameter_name = (
node.target if isinstance(node.target, str) else node.name
)
if parameter_name in bound.arguments:
ordered.append(bound.arguments[parameter_name])
elif node.name in bound.arguments:
ordered.append(bound.arguments[node.name])
else:
raise TypeError(
f"compiled region is missing a value for "
f"captured argument {parameter_name!r}"
)
return ordered
def compiled(*args, **kwargs):
ordered = ordered_inputs(args, kwargs)
if input_guard is not None and not input_guard(ordered):
raise RuntimeError("compiled shape constraints do not hold for these inputs")
# A list rather than a tuple, because the written-out code empties what
# it is given once it has taken it -- which is how a caller that holds
# the same values does not keep them alive for the call.
#
# The call runs inside a lowered-graph scope: the written-out code
# reads every result with the strides recorded when it was planned,
# so the operators it hands work to take their operands as they lie
# instead of repacking them into an order of their own choosing.
_enter_lowered_graph_scope()
try:
result = module_call(ordered)
finally:
_exit_lowered_graph_scope()
if single_output and isinstance(result, tuple) and len(result) == 1:
return result[0]
return result
def boxed_call(args):
if input_guard is not None and not input_guard(args):
raise RuntimeError("compiled shape constraints do not hold for these inputs")
_enter_lowered_graph_scope()
try:
result = module_call(args)
finally:
_exit_lowered_graph_scope()
if single_output and isinstance(result, tuple) and len(result) == 1:
return result[0]
return result
compiled._tensorplay_boxed_call = boxed_call # type: ignore[attr-defined]
compiled._tensorplay_module_call = module_call # type: ignore[attr-defined]
if input_guard is not None:
compiled._tensorplay_input_guard = input_guard
compiled._tensorplay_guard = lambda *args, **kwargs: input_guard(ordered_inputs(args, kwargs))
# The report is on what the caller actually receives, which is this and not
# the module behind it -- a caller asking which route produced its callable
# would otherwise be told nothing, since the module is not what it holds.
_publish_codegen(compiled, "triton" if on_cuda else "stax-cpu", backward=training)
return compiled
#: Everything the layers own, re-exported under one name so the
#: code generators, the lazy ``_stax`` proxy, and the tests keep
#: importing from ``backend``.
__all__ = [
"list_mode_options",
"stax",
]Help improve this page
Found an error, an unclear step, or a missing example?
Was this page helpful?

