Repository navigation
Conversation
40c134b to
63713c3
Compare
63713c3 to
5dfb78f
Compare
5dfb78f to
ef9785b
Compare
ef9785b to
70b9425
Compare
70b9425 to
ee68592
Compare
49edc24 to
a78e837
Compare
a78e837 to
f743ebd
Compare
f743ebd to
4fced1e
Compare
4fced1e to
41da37c
Compare
41da37c to
247de77
Compare
247de77 to
0e18073
Compare
c19045d to
7f33e8a
Compare
aaeaf90 to
007dac0
Compare
007dac0 to
3cd88c1
Compare
…loading Adopt the vocabulary ADR 0017 already defines: the root object that carries a program from DSL source to a runnable callable is the *toolchain*, not the *backend* (which is one of its two halves). Renamed: - `next.backend.Backend` -> `next.backend.Toolchain` - `Toolchain.transforms` -> `Toolchain.frontend` - `Toolchain.executor` -> `Toolchain.backend` - `otf.workflow.ConcreteArtifact` -> `otf.workflow.ProgramWithArgs` - `ProgramWithArgs.data` -> `ProgramWithArgs.definition` - `otf.stages.CompilableProgramDef` -> `otf.stages.CompilableProgram` - `gtx.typing.Backend` -> `gtx.typing.Toolchain` - `ffront.foast_to_past.ItirShim.definition` -> `ItirShim.operator_def`, freeing the name for the renamed `ProgramWithArgs` field it now wraps Beyond the renames, this restructures how artifacts are loaded. `Backend.load_artifact()` was an overridable method, so a toolchain needing backend-specific runtime data in the loaded program subclassed the root object (ADR 0027's dace external-workspace path did exactly that). It becomes `Toolchain.loading`, a plain step field defaulting to `artifacts.load_artifact`, so customization is composition-time like the other three fields. The `DaCeBackend` subclass is therefore deleted and `run_dace_*` are plain `Toolchain` instances carrying a `DaCeLoadingStep`; the public path to the workspace moves from `backend.external_workspace` to `backend.loading.external_workspace`. This also keeps subclassing off the load path, which matters because per ADR 0024 a toolchain that customizes `compile` is opaque to the process-pool runner. These are hard renames: no deprecation aliases are kept, so downstream has to move in this release rather than drift on a compatibility layer. An alias would have been partial anyway -- constructing with the old keyword names cannot be aliased on a dataclass without hand-writing `__init__` -- and a half-working compatibility layer is harder to act on than one loud break. Also resolves the `TODO(tehrengruber)` in `backend.py` asking for exactly this rename, and records the decision in a new ADR 0029, which partially supersedes ADR 0011 and refines ADR 0017. Programs compile and run exactly as before. Fingerprints embed qualified class and field names, so the `ProgramWithArgs` rename rotates the two persistent translation-cache keys (gtfn, dace); this is the second such rotation in the stack, after the module move that created `otf.workflow.ConcreteArtifact`. Each is a cold rebuild rather than a stale hit because the key itself changes -- entries written under the old name are simply never looked up. On top of that, `BUILD_CACHE_VERSION_ID` (the gt4py version) already rotates these keys at every release, so the cost is nothing extra for anyone crossing a release boundary. The two rotations collapse into one if the stack lands in a single release.
3cd88c1 to
9cf30a3
Compare
havogt
left a comment
There was a problem hiding this comment.
I stopped here, because if we want to do this change, then many renames are actually missing. I think we should discuss if we really want to rename the user facing "Backend" to "Toolchain". It's a quite an established concept and from the user perspective both the frontend transformation and the backend transforms are actually the backend.
| @@ -129,11 +129,11 @@ def compile(self, program, compile_time_args): | |||
| return self._wrapped.compile(program, compile_time_args=compile_time_args) | |||
|
|
|||
| backend = _WrapperBackend( | |||
There was a problem hiding this comment.
shouldn't backend be renamed to toolchain here?
|
|
||
|
|
||
| class NoBackend(next_backend.Backend): | ||
| class NoBackend(next_backend.Toolchain): |
There was a problem hiding this comment.
| class NoBackend(next_backend.Toolchain): | |
| class NoToolchain(next_backend.Toolchain): |
| cls, | ||
| mesh_descriptor: MeshDescriptor, | ||
| backend: Optional[next_backend.Backend] = None, | ||
| backend: Optional[next_backend.Toolchain] = None, |
There was a problem hiding this comment.
| backend: Optional[next_backend.Toolchain] = None, | |
| toolchain: Optional[next_backend.Toolchain] = None, |
etc
| backend: workflow.Workflow[stages.CompilableProgram, artifacts.CompilationArtifact] | ||
| allocator: next_allocators.FieldBufferAllocatorProtocol[core_defs.DeviceTypeT] | ||
| transforms: workflow.Workflow[stages.ConcreteProgramDef, stages.CompilableProgramDef] | ||
| frontend: workflow.Workflow[stages.ConcreteProgramDef, stages.CompilableProgram] |
There was a problem hiding this comment.
We agree, what we call "Backend" should:
- not contain frontend transformations
- should contain the allocator
… an internal Toolchain Rework the root object after review: the user-facing object keeps the name `Backend` (`gtx.typing.Backend` again), but it no longer carries the frontend transforms. - `backend.Backend` is an abstract base with `name` and `allocator` (instantiating it raises `TypeError`). - `backend.CompiledBackend(Backend)` adds `compiler` (formerly `Toolchain.backend` / `Backend.executor`) and `loader` (formerly `Toolchain.loading`), and provides `__gt_allocator__`. - `backend.Toolchain` becomes internal: the global frontend (`DEFAULT_TRANSFORMS`) composed with a `CompiledBackend`. The compiled-programs pool holds one, `FieldOperatorFromFoast` and the iterator runtime build one for their one-shot compiles. - Programs and operators dispatch on the backend kind in one helper; objects that are not backends are rejected when they are created. - The frontend is global, so `Program`/`FieldOperator` always lower with `DEFAULT_TRANSFORMS`; `roundtrip.gtir` drops its own, identical `Transforms`. - `compile` is no longer a backend method, so the opaque "customized compile" path of `make_compilation_task` is deleted. Tests: the compilation guards in `test_compiled_program.py` swapped `program.backend` after the pool was created and therefore guarded nothing; they now replace the pool's toolchain (`_forbid_compilation`), with a test proving the guard raises. Test doubles wrap `compiler` instead of overriding `compile`.
… frontend roundtrip.gtir now uses the global default frontend; its former private Transforms used the same factories with the same caching flags and only owned separate in-memory caches. The test compares both lowerings of a program and of a field operator.
Its only producer was the opaque 'customized compile' path of make_compilation_task, deleted with the backend restructuring. Tasks still fall back to the calling thread when their executor cannot be pickled; a new test covers that for a backend with a closure compiler.
…y DaCe backends by type - DaCeLoadingStep -> DaCeLoader (the external workspace stays reachable as backend.loader.external_workspace); both are exported from runners.dace. - New is_dace_backend(backend): a CompiledBackend whose compiler is the standard compile pipeline with a DaCeTranslator translation step (looking through CachedStep). Program.__sdfg__ uses it instead of checking that the backend name contains 'dace', so renamed DaCe backends convert too; the orchestration and vertical-advection tests use it as well.
The builders return a `CompiledBackend` and `Toolchain` is now internal, so the public names follow the backend vocabulary: - `gtfn.make_gtfn_toolchain` -> `gtfn.make_gtfn_backend` - `dace.make_dace_toolchain` -> `dace.make_dace_backend` - `backend.ToolchainConfig` -> `backend.CompiledBackendConfig` The flat-keyword `make_dace_backend(gpu, auto_optimize=..., ...)`, deprecated in the previous release, is removed; its name now belongs to the config-based builder. Calls in the removed shape (`make_dace_backend(gpu=...)` or `make_dace_backend(True)`) fail at the call with a `TypeError` that explains the new signature and where each old argument goes. The `name_postfix` docstrings now say what the generated name encodes instead of claiming the name "must stay unique".
…fication ADR 0029 (introduced by this PR, not yet on main) is rewritten for the reworked design: the user-facing Backend stays, as an abstract base with a CompiledBackend kind; the frontend is global and composed with a compiled backend by an internal Toolchain; dispatch on the backend kind; no overridable compile; Backend.name is a label; builder/config renames. The 'Pipeline, not combinators', 'Stage observability' and 'Phasing' sections of the later stack PRs are kept in the new vocabulary. The file is renamed accordingly. Also: notes in ADRs 0011, 0017, 0024, 0027 and 0028; HackTheToolchain.md says the frontend is not a user customization point; WorkflowPatterns.md and the next AGENTS.md module map use the new names.
|
Thanks for stopping here; you were right on both counts. A complete rename would have had to reach every The reworked PR (same branch, new commits; the description has the details):
Your inline comments:
One coordination point: your #2953 overlaps this PR in 7 files ( |
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.
…_sdfg__ `__sdfg__` lowers through the shared frontend and then overwrote the cached `past_to_itir` result in place (transformed definition, offset provider types instead of tables). Every later compilation of the same program reused the corrupted stage, so running it on another backend failed (an assertion in domain inference, or `'NeighborConnectivityType' object has no attribute 'asnumpy'` when offloading the compilation). Build a new stage instead. The bug exists on main for the backends sharing the default frontend; the previous commits extended it to `roundtrip.gtir`, which now shares it too. Found in an adversarial review of #2741.
Present the backend structure first on its own terms (concepts, the route from definition to callable, customization, pipelines, observability) and move the transition from the old structure (name mapping, removed mechanisms, no-alias policy, cache-key rotation, phasing, earlier ADRs) into a later section. No decision changes. Also corrects claims that did not match the code: compiling in the calling thread is not only the unpicklable-compiler fallback (serial mode), the cache version salt does not change on every source commit, `__sdfg__` no longer mutates the cached stage, and DaCe names encode whether auto-optimization is on.
egparedes
left a comment
There was a problem hiding this comment.
I have several questions and comments
| def load_artifact(artifact: CompilationArtifact) -> ExecutableProgram: | ||
| """ | ||
| Load a compilation artifact into a directly-callable program. | ||
|
|
||
| This is the default `CompiledBackend.loader`. Backends that need to inject | ||
| backend-specific runtime data into the loaded program supply their own | ||
| loader instead of overriding a method. | ||
|
|
||
| Args: | ||
| artifact: The artifact produced by the backend's compiler. | ||
|
|
||
| Returns: | ||
| The loaded, directly-callable program. | ||
| """ | ||
| return artifact.load() | ||
|
|
There was a problem hiding this comment.
Why is this needed?
There was a problem hiding this comment.
It's the default value of CompiledBackend.loader: a dataclass default has to be a callable, and a named module-level function stays picklable, readable in reprs and documented. DaCeLoader also calls it after injecting the workspace. The alternative, an optional loader with a fallback to artifact.load(), would put a None check in every caller.
|
|
||
| CompiledProgramsKey: TypeAlias = Annotated[compiled_program.CompiledProgramsKey, _ONLY_FOR_TYPING] | ||
|
|
||
| Backend: TypeAlias = Annotated[backend.Backend, _ONLY_FOR_TYPING] |
There was a problem hiding this comment.
Is this alias still needed or can we just use the actual Backend, since it's an abstract class?
There was a problem hiding this comment.
I'd keep it: every name in gtx.typing is an Annotated[..., "only for typing"] alias, including concrete classes like Program and FieldOperator. The alias marks the name as meant for annotations, which doesn't change with Backend being abstract.
| """ | ||
| return artifact.load() | ||
| @dataclasses.dataclass(frozen=True) | ||
| class CompiledBackend(Backend): |
There was a problem hiding this comment.
Shouldn't it be Generic on the Device Type?
There was a problem hiding this comment.
|
|
||
| @dataclasses.dataclass(frozen=True) | ||
| class ToolchainConfig: | ||
| class Toolchain: |
There was a problem hiding this comment.
Shouldn't it be Generic on the Device Type?
There was a problem hiding this comment.
Yes, done in e9f4113 (see the comment on CompiledBackend).
|
|
||
| backend: CompiledBackend | ||
| frontend: workflow.Workflow[stages.ConcreteProgramDef, stages.CompilableProgram] = ( | ||
| DEFAULT_TRANSFORMS |
There was a problem hiding this comment.
Maybe we should rename DEFAULT_TRANSFORMS to something more meaningful
There was a problem hiding this comment.
Done in e9f4113: it's now DEFAULT_FRONTEND, since it is the single global frontend, not a backend's default transforms. ADR 0029's name table lists the rename.
| def _compiled_backend_or_none( | ||
| backend: next_backend.Backend | None, | ||
| ) -> next_backend.CompiledBackend | None: | ||
| """ | ||
| Dispatch on the kind of `backend`. | ||
|
|
||
| Returns: | ||
| The compiled backend to run on, or `None` for embedded execution. | ||
|
|
||
| Raises: | ||
| TypeError: If `backend` is a `Backend` of a kind that cannot run programs. | ||
| """ | ||
| match backend: | ||
| case next_backend.CompiledBackend(): | ||
| return backend | ||
| case None: | ||
| return None | ||
| case _: | ||
| raise TypeError( | ||
| f"Backend '{backend.name}' of type '{type(backend).__name__}' is not supported:" | ||
| " expected a 'CompiledBackend', or None for embedded execution." | ||
| ) |
There was a problem hiding this comment.
Wouldn't it be simpler to just call isinstance(... , CompiledBackend)?
There was a problem hiding this comment.
The helper also rejects unsupported kinds. With a bare isinstance(..., CompiledBackend), everything else would silently run embedded, including a Backend subclass that is neither kind (e.g. the NoBackend test double, which must fail loudly). #2956 extends it to three outcomes (compiled, embedded, error), and keeping that in one place avoids repeating it in Program, FieldOperator and the scan operator.
| @@ -263,8 +297,9 @@ def __gt_type__(self) -> ts_ffront.ProgramType: | |||
|
|
|||
| # TODO(ricoh): linting should become optional, up to the backend. | |||
There was a problem hiding this comment.
Is this comment still accurate and relevant?
There was a problem hiding this comment.
Not anymore: "up to the backend" no longer fits, since backends don't own the frontend. Reworded to "linting should become optional" in e9f4113.
| @property | ||
| def _frontend_transforms(self) -> next_backend.Transforms: | ||
| if self.backend is None: | ||
| return next_backend.DEFAULT_TRANSFORMS | ||
| # TODO(tehrengruber): This class relies heavily on `self.backend.transforms` being | ||
| # a `next_backend.Transforms`, but the backend type annotation does not reflect that. | ||
| assert isinstance(self.backend.transforms, next_backend.Transforms) | ||
| return self.backend.transforms | ||
| # The frontend is global: it is not part of any backend (ADR 0029). | ||
| return next_backend.DEFAULT_TRANSFORMS |
There was a problem hiding this comment.
Is this property still really needed or useful? Who's using it?
There was a problem hiding this comment.
No, it only returned the global frontend. Removed in e9f4113; Program, FieldOperator and the DaCe __sdfg__ use DEFAULT_FRONTEND directly.
| @property | ||
| def _frontend_transforms(self) -> next_backend.Transforms: | ||
| if self.backend is None: | ||
| return next_backend.DEFAULT_TRANSFORMS | ||
| # TODO(tehrengruber): This class relies heavily on `self.backend.transforms` being | ||
| # a `next_backend.Transforms`, but the backend type annotation does not reflect that. | ||
| assert isinstance(self.backend.transforms, next_backend.Transforms) | ||
| return self.backend.transforms | ||
| # The frontend is global: it is not part of any backend (ADR 0029). | ||
| return next_backend.DEFAULT_TRANSFORMS |
There was a problem hiding this comment.
Same question as in Program: is this still needed? why?
- `CompiledBackend` and `Toolchain` are generic on the device type again, as `Backend` was on main, so the allocator keeps its device type. - Rename `DEFAULT_TRANSFORMS` to `DEFAULT_FRONTEND`: it is the global frontend, not a backend's default transforms. - Drop the `_frontend_transforms` properties of `Program` and `FieldOperator`; they only returned the global frontend. - Reword the linting TODO: backends no longer own the frontend.
| executor: workflow.Workflow[stages.CompilableProgramDef, artifacts.CompilationArtifact] | ||
| allocator: next_allocators.FieldBufferAllocatorProtocol[core_defs.DeviceTypeT] | ||
| transforms: workflow.Workflow[stages.ConcreteProgramDef, stages.CompilableProgramDef] | ||
| allocator: constructors.Allocator |
There was a problem hiding this comment.
Why not __gt_allocator__ in the abstract interface?
| > [!NOTE] | ||
| > The frontend (`gtx.backend.Transforms`, the definition transformations shown in the next two | ||
| > sections) is global and not a user customization point: a backend does not carry a frontend | ||
| > (ADR 0029). A modified `Transforms` can only be used through the internal | ||
| > `gtx.backend.Toolchain`, e.g. in gt4py's own tests. What a backend does is customized through | ||
| > its `compiler` and `loader`, see "Alternative Workflow" below. | ||
|
|
There was a problem hiding this comment.
| > [!NOTE] | |
| > The frontend (`gtx.backend.Transforms`, the definition transformations shown in the next two | |
| > sections) is global and not a user customization point: a backend does not carry a frontend | |
| > (ADR 0029). A modified `Transforms` can only be used through the internal | |
| > `gtx.backend.Toolchain`, e.g. in gt4py's own tests. What a backend does is customized through | |
| > its `compiler` and `loader`, see "Alternative Workflow" below. |
| @property | ||
| def _frontend_transforms(self) -> next_backend.Transforms: | ||
| if self.backend is None: | ||
| return next_backend.DEFAULT_TRANSFORMS | ||
| # TODO(tehrengruber): This class relies heavily on `self.backend.transforms` being | ||
| # a `next_backend.Transforms`, but the backend type annotation does not reflect that. | ||
| assert isinstance(self.backend.transforms, next_backend.Transforms) | ||
| return self.backend.transforms | ||
| # The frontend is global: it is not part of any backend (ADR 0029). | ||
| return next_backend.DEFAULT_TRANSFORMS |
There was a problem hiding this comment.
I'd make it a private field of the dataclass which is default initialized to DEFAULT_TRANSFORMS.
| # TODO(tehrengruber): We can not use transforms from `self.backend` since this can be | ||
| # a different backend than the one of the program that calls this field operator. Just use | ||
| # the hard-coded lowering until this is cleaned up. | ||
| # The lowering does not depend on the backend: the frontend is global (ADR 0029). |
There was a problem hiding this comment.
| # The lowering does not depend on the backend: the frontend is global (ADR 0029). |
| def load_artifact(self, artifact: artifacts.CompilationArtifact) -> artifacts.ExecutableProgram: | ||
| program = super().load_artifact(artifact) | ||
| def __call__(self, artifact: artifacts.CompilationArtifact) -> artifacts.ExecutableProgram: | ||
| program = artifacts.load_artifact(artifact) |
There was a problem hiding this comment.
| program = artifacts.load_artifact(artifact) | |
| program = artifact.load() |
| compiler: workflow.Workflow[stages.CompilableProgram, artifacts.CompilationArtifact] | ||
| # A plain `Callable`, not `workflow.Workflow`: the protocol names its | ||
| # parameter `inp`, which would force every loader to use that name. | ||
| loader: Callable[[artifacts.CompilationArtifact], artifacts.ExecutableProgram] = ( |
There was a problem hiding this comment.
The custom loader was introduced with the dace external workspace in #2719. It's to my knowledge the only stateful aspect of any backend, wondering if we should provide the workspace at execution time instead of binding it to the program via backend. That approach would also have downsides, because the workspace is clearly coupled to a dace backend (unless we generalize it).
Stack: #2741 → #2956 → #2742 → #2743.
Reworked after review. The first version renamed the user-facing
BackendtoToolchain. Users think of "backend" as the whole execution choice, frontend included, and a complete rename would have had to reach everybackend=. This version keepsBackendas the public concept and moves the split one level down: what users pick is a backend (compiler + allocator), and the frontend, which is the same for every backend, is no longer part of it. ADR 0029 records the design.What changes
Backendis an abstract base withnameandallocator; instantiating it directly raisesTypeError.gtx.typing.Backend,backend=,with_backendandProgram.backendkeep their names.CompiledBackend(Backend)addscompiler(formerlyexecutor) andloader(formerly the overridableload_artifact()). Embedded execution is stillbackend=Nonehere;EmbeddedBackendobjects come in feat[next]!: represent embedded execution by EmbeddedBackend objects #2956.Transforms, nowDEFAULT_FRONTEND; the one exception,roundtrip.gtir, built an identical copy and now uses it too (a test pins that both lower identically). An internal, unexportedToolchaincomposes the frontend with aCompiledBackendwhere compilation happens: in the compiled-programs pool,FieldOperatorFromFoastand the iterator runtime.decorator.pyfor programs, field operators and scan operators:CompiledBackendcompiles,Noneruns embedded, otherBackends raiseTypeError. Non-backends are rejected when the program or operator is created.compile. The opaque "customized compile" path of the process-pool runner (ADR 0024) andCompilationTask.no_offload_reasonare removed; test doubles wrapcompilerinstead. An unpicklable compiler still compiles in the calling thread, with a warning.Backend.nameis a documented label for metrics and logs: expected to be unique, not enforced, never used as identity.DaCeBackendsubclass is replaced by aDaCeLoader(backend.loader.external_workspace).__sdfg__recognizes DaCe backends by type (is_dace_backend), not by name.__sdfg__no longer mutates the cached frontend result. It overwrote the cached lowering in place, so a later compilation of the same program on another backend failed. The bug exists onmainfor the backends sharing the default frontend; this PR would have extended it toroundtrip.gtir. Found in an adversarial review; covered by a regression test.make_gtfn_backend,make_dace_backend,CompiledBackendConfig. The flat-keywordmake_dace_backend(gpu=...), deprecated in the last release, is removed and its name reused; old-style calls fail with aTypeErrorexplaining the new signature.Also fixed: the "no recompilation" guards in
test_compiled_program.pyswappedprogram.backendafter the pool existed, so they guarded nothing. They now replace the pool's toolchain, and a test proves the guard fires.Breaking (no aliases)
main(v1.2.3)Backend(name, executor, allocator, transforms)CompiledBackend(name=, allocator=, compiler=, loader=);Backendis abstractbackend.executor/backend.transformsbackend.compiler/ removed (global frontend)load_artifact/compiledataclasses.replace(backend, loader=... / compiler=...)DaCeBackend,backend.external_workspaceDaCeLoader,backend.loader.external_workspacemake_gtfn_toolchain,make_dace_toolchain,ToolchainConfigmake_gtfn_backend,make_dace_backend,CompiledBackendConfigmake_dace_backend(gpu=..., auto_optimize=..., use_metrics=...)make_dace_backend(DaCeConfig(gpu=..., auto_optimize=...), translation=functools.partial(make_dace_translator, use_metrics=...))ConcreteArtifact/.data,CompilableProgramDefProgramWithArgs/.definition,CompilableProgrambackend.DEFAULT_TRANSFORMSbackend.DEFAULT_FRONTENDcompile_variant_hook(..., backend: Backend)backend: CompiledBackendCode that only passes backend instances around (
gtx.gtfn_cpu,run_dace_cpu, ...) is unaffected. icon4py needs two changes inmodel_backends.py(the builder calls).Cache keys
The
ProgramWithArgsrename rotates the persistent translation-cache keys (gtfn, dace): a cold rebuild, never a stale hit. The backend restructuring rotates nothing:Program.backendis excluded from fingerprints.Merge order
#2953 overlaps this PR in 7 files and reshapes
CompilationTask; we should agree which lands first.Testing
Full suites run in CI. The rework passed the
internalanddaceCPU nox sessions,pre-commit -a, typing exports and examples locally; later commits were checked with targeted tests.