From cb75d2900b3e3900e90766b68ad12c9d827e3377 Mon Sep 17 00:00:00 2001 From: Hannes Vogt Date: Wed, 5 Aug 2026 17:55:06 +0200 Subject: [PATCH 1/3] idea[havogt]: add ambient execution-time binding as a closure-variable stretch goal Records the ContextVar variant next to the read-only-field stretch goal it extends: a globally declared value bound at program-execution time, so mesh properties (Cartesian scalars, unstructured connectivities and weights) need not be threaded through signatures. Late binding is what lets one set of operators run against a second mesh in a process. --- content/index.md | 2 +- .../havogt/closure-variable-resolution.md | 53 ++++++++++++++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/content/index.md b/content/index.md index 13d9474..552e520 100644 --- a/content/index.md +++ b/content/index.md @@ -29,7 +29,7 @@ Work-in-progress proposals, organized by contributor. - [[personal/havogt/dtype-generic-fields|Dtype-generic fields in gt4py.next]] — keywords: type-system, generics, dtype, frontend, foast, past, monomorphization, field-operators, type-checking - [[personal/havogt/dimension-generic-fields|Generic dimensions and statically typed staggering]] — keywords: type-system, generics, dimensions, staggering, type-checking, mypy, frontend, foast, monomorphization, unstructured - [[personal/havogt/dependent-local-dimensions|Dependent local dimensions and connectivity chains]] — keywords: type-system, dimensions, unstructured, connectivities, local-dimensions, reduction, neighbor-sum, type-checking -- [[personal/havogt/closure-variable-resolution|Closure variable resolution in gt4py.next]] — keywords: frontend, foast, past, closure-variables, name-resolution, constants, builtins, aliasing, gtir, lowering +- [[personal/havogt/closure-variable-resolution|Closure variable resolution in gt4py.next]] — keywords: frontend, foast, past, closure-variables, name-resolution, constants, builtins, aliasing, gtir, lowering, contextvar, ambient, fields, mesh, static-args - [[personal/havogt/scan-redesign|Redesign of the vertical scan in gt4py.next]] — keywords: scan, vertical, reduction, boundary-conditions, windows, k-caches, fusion, embedded, jax, frontend - [[personal/havogt/mesh-and-first-class-halos|A mesh concept with first-class halos]] — keywords: mesh, halos, unstructured, connectivities, offset-provider, domain-inference, distributed, halo-exchange, prior-art - [[personal/havogt/field-data-protocol|A FieldData protocol for gt4py.next embedded fields]] — keywords: fields, domain, data, protocol, embedded, function-fields, boundary-conditions, materialization, lazy, concat_where, origin, prior-art diff --git a/content/personal/havogt/closure-variable-resolution.md b/content/personal/havogt/closure-variable-resolution.md index 8debe15..fa79969 100644 --- a/content/personal/havogt/closure-variable-resolution.md +++ b/content/personal/havogt/closure-variable-resolution.md @@ -1,7 +1,7 @@ --- title: Closure variable resolution in gt4py.next author: havogt -tags: [frontend, foast, past, closure-variables, name-resolution, constants, builtins, aliasing, gtir, lowering] +tags: [frontend, foast, past, closure-variables, name-resolution, constants, builtins, aliasing, gtir, lowering, contextvar, ambient, fields, mesh, static-args] created: 2026-06-12 status: draft --- @@ -194,6 +194,57 @@ could bake the buffer in as a true constant, layered on `StaticArg`-style descriptors (ADR 0021 — Argument Descriptors). This fits A (the canonicalization pass introduces the hidden parameter) and becomes more natural under B. +### Stretch goal: ambient values bound at execution time + +The stretch goal above binds at *decoration* time. The variant worth recording +binds later: a user declares a value once, globally, and any field operator +reaches it without it appearing in a signature — a `ContextVar` filled at +program-execution time (JIT time for compiled backends). + +The motivating case is mesh properties. They are scalars on a Cartesian grid +(`dx`, `dy`) and fields on an unstructured one (connectivities plus their +weights); both are fixed for the lifetime of the application, and both must +today be carried explicitly — as extra operator parameters, or as the +`offset_provider` threaded down the call chain. Every operator between the +caller and the one that actually needs a weight has to name it. + +Most of the machinery exists, so this is more unification than invention: + +- **`ContextVar`s are already how gt4py carries such values.** + `next/embedded/context.py` holds `_offset_provider` and + `_closure_column_range`, with an `update()` context manager and + `get_offset_provider()` — internal and embedded-only today. +- **The scalar case already works**, but only through a namespace object: + `ConstantPythonNamespaceObject = eve_utils.FrozenNamespace | enum.EnumMeta` + (`type_translation.py`), folded in `closure_var_folding.py`, fingerprint-aware + in `fingerprinting.py`. +- **The field case is the stretch goal above**: hidden parameter via + `__gt_implicit_args__()`, and *bind the binding, not the buffer*, so only the + type/domain descriptor enters the compiled cache key. + +What late binding adds over the decoration-time variant is that the same +operators can run against a *different* mesh in one process; the decoration-time +form fixes the value before any operator is defined. + +Open questions: what "static for the lifetime of the application" means +operationally (per process, per `ContextVar` scope, or per compiled program — +and what happens on rebinding: recompile, reject, or re-bind an argument +descriptor); which part of an ambient field enters the fingerprint; how an +ambient declaration is typed, and whether an operator's type should reflect its +ambient dependencies at all; and the standard objection to dynamic scoping — +errors when nothing is bound must be good, and there must be a way to see what +an operator depends on ambiently. + +The ceiling this aims at is a mesh concept built on top, where connectivities +and weights are properties of an ambient mesh rather than arguments — see +[[personal/havogt/mesh-and-first-class-halos|A mesh concept with first-class halos]]. +It would also be a plausible substrate for +[[personal/egparedes/discretization-independent-fd-syntax|A discretization-independent +surface syntax]], whose mesh-invariant surface needs weights and connectivity to +come from a declared mesh property rather than from operator arguments (that +proposal is a higher-level surface layered on gt4py's core concepts, not a +replacement for them). + ### Source material - Proposed ADR and **partial prototype of strategy A** on gt4py branch From 618b529fe04b21868b025bd27c6c53628788e620 Mon Sep 17 00:00:00 2001 From: Hannes Vogt Date: Wed, 5 Aug 2026 22:16:47 +0200 Subject: [PATCH 2/3] idea[havogt]: fold the ambient prototype's findings into the stretch goal Records what the gt4py branch established: a declaration carries its own type so no type-system change is needed; the reference becomes a synthesised program parameter added once at definition; and the only difference between the two forms is whether that parameter is static. Adds the measured variant counts and the three things the design turned out not to need. --- .../havogt/closure-variable-resolution.md | 118 +++++++++++++----- ...ed_dimensions.cpython-312-pytest-9.1.1.pyc | Bin 0 -> 37178 bytes .../typed_dimensions.cpython-312.pyc | Bin 0 -> 22616 bytes 3 files changed, 87 insertions(+), 31 deletions(-) create mode 100644 content/personal/havogt/dimension-generic-fields/__pycache__/test_typed_dimensions.cpython-312-pytest-9.1.1.pyc create mode 100644 content/personal/havogt/dimension-generic-fields/__pycache__/typed_dimensions.cpython-312.pyc diff --git a/content/personal/havogt/closure-variable-resolution.md b/content/personal/havogt/closure-variable-resolution.md index fa79969..f11a5e6 100644 --- a/content/personal/havogt/closure-variable-resolution.md +++ b/content/personal/havogt/closure-variable-resolution.md @@ -201,39 +201,95 @@ binds later: a user declares a value once, globally, and any field operator reaches it without it appearing in a signature — a `ContextVar` filled at program-execution time (JIT time for compiled backends). +> **Prototyped**: gt4py branch +> [`ambient-offset-provider`](https://github.com/havogt/gt4py/pull/71) (fork PR). +> Everything below marked *measured* comes from there; everything marked *open* +> is still open. + The motivating case is mesh properties. They are scalars on a Cartesian grid (`dx`, `dy`) and fields on an unstructured one (connectivities plus their -weights); both are fixed for the lifetime of the application, and both must -today be carried explicitly — as extra operator parameters, or as the -`offset_provider` threaded down the call chain. Every operator between the -caller and the one that actually needs a weight has to name it. - -Most of the machinery exists, so this is more unification than invention: - -- **`ContextVar`s are already how gt4py carries such values.** - `next/embedded/context.py` holds `_offset_provider` and - `_closure_column_range`, with an `update()` context manager and - `get_offset_provider()` — internal and embedded-only today. -- **The scalar case already works**, but only through a namespace object: - `ConstantPythonNamespaceObject = eve_utils.FrozenNamespace | enum.EnumMeta` - (`type_translation.py`), folded in `closure_var_folding.py`, fingerprint-aware - in `fingerprinting.py`. -- **The field case is the stretch goal above**: hidden parameter via - `__gt_implicit_args__()`, and *bind the binding, not the buffer*, so only the - type/domain descriptor enters the compiled cache key. - -What late binding adds over the decoration-time variant is that the same -operators can run against a *different* mesh in one process; the decoration-time -form fixes the value before any operator is defined. - -Open questions: what "static for the lifetime of the application" means -operationally (per process, per `ContextVar` scope, or per compiled program — -and what happens on rebinding: recompile, reject, or re-bind an argument -descriptor); which part of an ambient field enters the fingerprint; how an -ambient declaration is typed, and whether an operator's type should reflect its -ambient dependencies at all; and the standard objection to dynamic scoping — -errors when nothing is bound must be good, and there must be a way to see what -an operator depends on ambiently. +weights); both must today be carried explicitly — as extra operator parameters, +or as the `offset_provider` threaded down the call chain. Every operator between +the caller and the one that actually needs a weight has to name it. + +#### Surface + +```python +mesh = gtx.Namespace("mesh") # connectivities +dx = gtx.Static[float] # a value, folded into the generated code +nu = gtx.Extern[float] # a value, passed at runtime + + +@gtx.field_operator +def delta_x(f: IJField) -> IJField: + return (1.0 / dx) * (f(I + 1) - f) # never a parameter + + +prog(f, out, bind={mesh: my_mesh, dx: 0.5}) # or: with gtx.bind(dx, 0.5): ... +``` + +`bind=` is sugar over the `ContextVar`, scoped to one call, so the two spellings +compose rather than compete. + +#### How it works + +- **A declaration carries its type.** `Static[float]` implements `__gt_type__`, + and `type_translation.from_value` already dispatches on that — so an operator + referring to an ambient value type-checks when it is *defined*, with nothing + bound. **No type-system change was needed.** An untyped placeholder fails with + `DSLTypeError: Unexpected object ...`, and the failure is temporal, not + spatial: it happens in the same file, so putting the declaration in another + module changes nothing. +- **The reference becomes a synthesised program parameter**, added once in + `func_to_past`. From there it travels the ordinary path: type checking, + lowering, `static_params` and the compiled-program key all treat it as an + argument, and only the value is supplied per call. +- **The two forms differ in one place only** — whether that parameter is listed + as static. `Extern[T]` stays a runtime argument; `Static[T]` is a static + argument, so the *existing* fold in `past_to_itir` bakes it in and the + *existing* `StaticArg` key specialises on it. *Measured* on gtfn and dace with + two values: `Static` compiles 2 variants, `Extern` 1, both correct. +- **Connectivities identify by content, not by `id`.** `gtx.freeze(conn)` caches + a content hash once; `hash_offset_provider_items_by_id` prefers it. *Measured*: + 3 compiled variants drop to 2 when two structurally identical meshes stop + being keyed apart by object identity — that function's own docstring warns it + "could generate different hashes for two offset providers that are + semantically equal". + +Three things it turned out **not** to need, each of which was assumed at some +point in the design: a new FOAST specialization step (the existing static-argument +fold suffices); threading the parameter into operator signatures and call sites +(a free symbol in a lowered operator resolves against the *program's* parameters, +and both gtfn and dace codegen fine that way); and a per-operator inspection pass +(`transform_utils._get_closure_vars_recursively` already collects declarations +transitively through nested operators). + +#### Binding time + +"Static for the lifetime of the application" is the wrong unit; the useful one is +**static for a jitted program**. That makes the two forms a declaration-site +choice of what identifies a value in the jit key: `Static[T]` puts the *value* +there, `Extern[T]` only its type. Which also dissolves an apparent conflict with +[[personal/havogt/jax-connectivities/jax-connectivities|the JAX connectivities proposal]] — a +connectivity wants descriptor-identity (one program per mesh *class*), a scalar +wants value-identity, and nothing forces one policy on both. + +#### Open + +- **Canonical identifiers.** Synthesised parameters currently take the closure + variable's *local* name, so two operators naming the same declaration + differently, or two modules that both use `dx`, collide. Declarations should be + named explicitly (`Static[float]("dx")`) with a FOAST rename pass; a generated + counter will not do, since the name lands in the compiled signature and would + shift with import order. +- **Ambient fields** (`mesh.edge_length`) are not implemented, but now look like + an `Extern[Field[...]]` — a synthesised parameter with no folding, i.e. the + read-only-field stretch goal above with a later binding time. +- **Immutability.** `freeze(readonly=True)` would make the cached content hash + trustworthy, but the gtfn bindings are generated with mutable `ndarray` + parameters and reject a read-only array outright, so it is off by default. +- **Debuggability**, unchanged: errors when nothing is bound must be good, and + there should be a way to see what an operator depends on ambiently. The ceiling this aims at is a mesh concept built on top, where connectivities and weights are properties of an ambient mesh rather than arguments — see diff --git a/content/personal/havogt/dimension-generic-fields/__pycache__/test_typed_dimensions.cpython-312-pytest-9.1.1.pyc b/content/personal/havogt/dimension-generic-fields/__pycache__/test_typed_dimensions.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bdf5d5a54b4f77ff130af26067e1f194881136ef GIT binary patch literal 37178 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 Z&g% Date: Wed, 5 Aug 2026 22:46:40 +0200 Subject: [PATCH 3/3] idea[havogt]: unify ambient binding on declarations, and stage the transition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records that connectivities and values now bind by one rule — the declaration is the key — which retires the name-harvesting 'Namespace' and its collision check. Splits the transition: moving the binding surface is done and touches nothing below it; making connectivities ordinary ambient values is a separate question, since the offset provider's type drives domain inference and connectivity typing. --- .../havogt/closure-variable-resolution.md | 53 ++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/content/personal/havogt/closure-variable-resolution.md b/content/personal/havogt/closure-variable-resolution.md index f11a5e6..0b7245a 100644 --- a/content/personal/havogt/closure-variable-resolution.md +++ b/content/personal/havogt/closure-variable-resolution.md @@ -214,8 +214,10 @@ the caller and the one that actually needs a weight has to name it. #### Surface +One rule for everything ambient: **the declaration is the key**. + ```python -mesh = gtx.Namespace("mesh") # connectivities +V2E = gtx.FieldOffset("V2E", source=Edge, target=(Vertex, V2EDim)) dx = gtx.Static[float] # a value, folded into the generated code nu = gtx.Extern[float] # a value, passed at runtime @@ -225,9 +227,30 @@ def delta_x(f: IJField) -> IJField: return (1.0 / dx) * (f(I + 1) - f) # never a parameter -prog(f, out, bind={mesh: my_mesh, dx: 0.5}) # or: with gtx.bind(dx, 0.5): ... +prog(f, out, bind={V2E: connectivity, dx: 0.5}) # or: with gtx.bind(dx, 0.5): ... ``` +A `FieldOffset` is *already* a declaration — it names the offset and fixes its +source and target — so it binds exactly like a value, and a program called +without an `offset_provider` assembles one from the bound offsets. A container +may declare what it supplies, with the class attribute holding the declaration +and the instance attribute the value: + +```python +class Mesh: + V2E = V2E # this mesh supplies the V2E connectivity + dx = physics.dx + + +prog(f, out, bind=Mesh(...)) +``` + +Binding by declaration *identity* rather than by attribute name is what lets a +container supply the very offset an operator refers to, rather than one that +merely shares its name — and it means a container attribute need not be named +after the declaration at all (*measured*: an attribute called `spacing` resolves +`physics.dx` correctly, across modules). + `bind=` is sugar over the `ContextVar`, scoped to one call, so the two spellings compose rather than compete. @@ -264,6 +287,32 @@ and both gtfn and dace codegen fine that way); and a per-operator inspection pas (`transform_utils._get_closure_vars_recursively` already collects declarations transitively through nested operators). +#### Transition: retiring the second binding rule + +The prototype first grew *two* binding mechanisms with different identity rules: +connectivities were harvested from a bound object by attribute **name** (a +`Namespace`), values were keyed by declaration **identity**. Same surface, two +rules — and the name-based half needed a collision check, because two namespaces +could each offer an offset called `V2E`. + +Unifying on declarations removes `Namespace`, the attribute harvesting and that +collision check outright, and it is worth doing in two steps: + +1. **Move only the binding surface.** Offsets bind by declaration; the + `offset_provider` is still assembled from the bound ones and passed exactly as + today, so the frontend, the IR and the backends are untouched. *This step is + implemented.* +2. **Then consider making connectivities ordinary ambient values**, i.e. an + `Extern[Connectivity]` that becomes a synthesised parameter like any other, + at which point `offset_provider` stops being a separate concept. This is not + obviously right and should not be assumed: `offset_provider` carries things + the parameter path does not model today (its *type* drives domain inference + and connectivity typing, and `arguments.py` still notes the temporary pass + needs the runtime object). Worth a separate look rather than a follow-through. + +The staging matters because step 1 is a pure surface change with no risk to the +toolchain, while step 2 touches how connectivities are typed and inferred. + #### Binding time "Static for the lifetime of the application" is the wrong unit; the useful one is