Skip to content

feat[next]!: represent embedded execution by EmbeddedBackend objects - #2956

Open
egparedes wants to merge 10 commits into
otf-split-2-toolchain-namingfrom
otf-split-2b-embedded-backends
Open

egparedes wants to merge 10 commits into
otf-split-2-toolchain-namingfrom
otf-split-2b-embedded-backends

Conversation

@egparedes

@egparedes egparedes commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Stack: #2741 → #2956 → #2742 → #2743. Based on #2741's branch, so the diff shows only this PR.

Implements ADR 0030 (added here). Embedded execution used to be backend=None, with no allocator and no name, so every consumer special-cased it. It is now an EmbeddedBackend(Backend), a sibling of CompiledBackend, and the NumPy instance gtx.embedded_numpy is the default backend.

What changes

  • EmbeddedBackend runs the Python definition directly on fields. Its allocator is a validated array namespace (NumPy by default). Execution is the same for every instance; the allocator only decides where gt4py allocates buffers.
  • Instances in runners.embedded: embedded_numpy (also gtx.embedded_numpy), and embedded_cupy / embedded_jax, created on first access (an AttributeError names the missing package). Instances, not subclasses; a subclass with its own execution (e.g. jax.jit) is future work.
  • backend=None is a TypeError at creation, naming the replacement. This covers the decorators, from_function, with_backend and dataclasses.replace. No deprecation period. The iterator-level fendef(..., backend=None) keeps its meaning.
  • Dispatch: CompiledBackend compiles, EmbeddedBackend runs embedded, other Backends raise. .compile() on an embedded program raises a RuntimeError saying embedded programs are not compiled.
  • allocator=program works for embedded programs. Backend.__gt_allocator__ moves to the base class; allocator factories may yield an array namespace, and FieldConstructor unwraps factories before choosing the array-API or custom-layout path.
  • Metrics report embedded calls as prog<embedded_numpy> instead of prog<'embedded'>.
  • Deep copies: Backend.__deepcopy__ returns the backend itself and Program.__gt_allocator__ is no longer cached, so default programs stay deep-copyable (the NumPy module cannot be copied). Copies share their backend, including a DaCe external workspace.
  • Tests run on the real embedded backends (EmbeddedDummyBackend is gone). CI: the dace nox sessions deselect requires_jax, because they don't install JAX and the embedded-JAX test parameter now loads the real backend.

Breaking

Before After
backend=None, .with_backend(None) in ffront TypeError: omit backend or pass gtx.embedded_numpy
DEFAULT_BACKEND is None DEFAULT_BACKEND is gtx.embedded_numpy
backend is None to detect embedded isinstance(backend, EmbeddedBackend)
backend.allocator.device_type on any Backend only on CompiledBackend; an embedded allocator is an array namespace
__gt_allocator__ is always a field-buffer allocator it may be an array namespace
metrics key prog<'embedded'> prog<embedded_numpy>

icon4py (from a code search; to re-check against its main): drop backend=None in metric_fields.py; replace .with_backend(None) and BACKENDS["embedded"] = None; branch on isinstance(backend, EmbeddedBackend) before reading allocator.device_type in is_cpu_backend / is_gpu_backend; replace backend is None checks and the "embedded" if backend is None else backend.name pattern (the default's name is "embedded_numpy").

ADR number

This PR adds ADR 0030 (#2741 adds 0029). The draft connectivities-as-types stack (#2899, ...) claims the same numbers; whichever lands second renumbers.

Testing

Full suites run in CI. Locally: the internal and dace CPU nox sessions, pre-commit -a, typing exports and examples, before the latest merge from #2741, which was checked with targeted tests.

`FieldConstructor` now unwraps `__gt_allocator__` factories before choosing
between the array-API path and the custom-layout path, so a factory whose
allocator is an array namespace (e.g. NumPy) allocates like that namespace
instead of being pushed into the `TensorBuffer` path.

- `FieldBufferAllocatorFactoryProtocol.__gt_allocator__` may return an array
  namespace as well as a field buffer allocator.
- `is_field_allocator_factory_for(obj, device)` is true only for factories
  providing a field buffer allocator for that device type (it raised
  `AttributeError` for an array namespace).

Prepares embedded backends (ADR 0030), whose allocator is an array namespace.
- `Backend.__gt_allocator__` moves to the base class and returns the
  backend's allocator, unwrapping an allocator factory; `CompiledBackend`
  keeps its narrower override.
- `EmbeddedBackend(Backend)`, a sibling of `CompiledBackend`, runs the
  Python definition directly on fields; its allocator is an array namespace
  (validated, NumPy by default).
- New module `program_processors.runners.embedded` with `embedded_numpy`
  (also exported as `gtx.embedded_numpy`) and the lazily created
  `embedded_cupy` / `embedded_jax`, which raise `AttributeError` naming the
  missing optional package.
- `Backend.__deepcopy__` returns the backend itself (immutable configuration;
  the NumPy module it holds cannot be copied), and `Program.__gt_allocator__`
  is a plain property, so a program used as `allocator=` does not cache a
  module and stays deep-copyable. Copies share the backend.

Programs do not dispatch to `EmbeddedBackend` yet; that is the next step.
The dispatch helper treats an `EmbeddedBackend` like `None` (embedded
execution); `None` keeps working until it becomes an error.

- `Program.__gt_allocator__` returns the backend's allocator for every
  backend kind, so `gtx.zeros(..., allocator=program)` allocates with the
  array namespace of an embedded program.
- Embedded calls report metrics under `prog<backend.name>` (e.g.
  `prog<embedded_numpy>`); `None` still reports `prog<'embedded'>`.
- `.compile()` on a program or operator with an embedded backend says that
  embedded programs are not compiled.
- `EmbeddedDummyBackend` and `numpy_execution` / `cupy_execution` /
  `jax_numpy_execution` are deleted; the `EmbeddedIds` parameters load
  `runners.embedded.embedded_{numpy,cupy,jax}`.
- `cartesian_case` / `unstructured_case` always use the backend of
  `exec_alloc_descriptor`; `Case.backend` is a `Backend`, never `None`, and
  `Case.is_embedded` replaces the `backend is None` checks. The
  `*_no_backend` cases use `cases_utils.no_backend`.
- Tests that mapped the dummy to `None` pass the embedded backend directly.
…nd=None

- `decorator.DEFAULT_BACKEND` is `gtx.embedded_numpy`: programs and
  operators decorated without `backend=` still run embedded on NumPy.
- `backend=None` raises `TypeError` when a program or operator is created
  (decorators, `from_function`, `with_backend`, `dataclasses.replace`), and
  the message names the replacement. No deprecation aliases.
- The dispatch helper accepts `CompiledBackend` and `EmbeddedBackend` only;
  `backend` is typed `Backend` (no `None`) everywhere in the decorators.
- Embedded metrics keys always use the backend name.
- `iterator.runtime`: `fendef(..., backend=None)` keeps meaning
  iterator-level embedded execution; an `EmbeddedBackend` is rejected with a
  message pointing to it.

Tests: `backend=None` is rejected everywhere it can be passed, the default
is `embedded_numpy`, `with_backend(None)` is a mypy error, and the
remaining `None`-as-embedded uses are gone.
- New ADR 0030, listed under "Embedded Execution" in the ADR index; the
  plain-text "ADR 0030" mentions in ADR 0029 become links.
- Workshop exercises and slides use `gtx.embedded_numpy` instead of
  `backend = None`.
- `src/gt4py/next/AGENTS.md`: embedded execution is an `EmbeddedBackend`.
The dace sessions run with `--require-optional-deps` but do not install
the `jax` extra. Their selection includes the embedded JAX parameter of the
backend matrix (dace tests skip non-dace backends in the test body), and
that parameter now loads the real `embedded_jax` backend, which needs
`jax`. Exclude the marker, as the noxfile prescribes for unmet `requires_*`
markers.
egparedes added a commit that referenced this pull request Oct 29, 2026
Bring the reworked stack (#2741 backend structure, #2956 embedded backends,
#2742 observability on the new design) under the pipeline PR and adapt it:

- `CompiledBackend.compiler` / `.loader` and `Toolchain.frontend` are typed
  as `workflow.Step`; `Toolchain.translate` and `is_dace_backend` narrow to
  `CompilePipeline`; the dace SDFG conversion replaces the translation step
  inside `CompiledBackend.compiler`.
- The allocator-vs-compiler device-agreement check moves from
  `Toolchain.__post_init__` to `CompiledBackend.__post_init__` (after the
  abstract-base check); its tests and the pre-built-backend checks use
  `backend.compiler`.
- `roundtrip.gtir` keeps the default frontend (no private `Transforms`);
  the `Transforms.foast_to_itir` comment no longer names `roundtrip`.
- ADR 0029 edits of this PR (CachedStep without `.replace`/`.chain`,
  static-checking scope, "Cross-step agreement is checked on construction",
  phasing) are carried over into the rewritten, renamed ADR; the old file
  stays deleted. HackTheToolchain.md / WorkflowPatterns.md / AGENTS.md use
  the backend vocabulary.
@egparedes
egparedes added this pull request to stack #2957 October 29, 2026 07:48
…d-backends

# Conflicts:
#	docs/development/ADRs/next/0029-Backend-Structure-and-Pipeline-Simplification.md
…d-backends

# Conflicts:
#	src/gt4py/next/AGENTS.md
#	src/gt4py/next/backend.py

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant