Skip to content

fix[next]: decide concat_where pruning from the branches' domains - #76

Open
havogt wants to merge 4 commits into
mainfrom
next-prune-concat-where-from-branches
Open

havogt wants to merge 4 commits into
mainfrom
next-prune-concat-where-from-branches

Conversation

@havogt

@havogt havogt commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Written by Claude, not reviewed by @havogt.

Staged on the fork for discussion — an alternative to GridTools#2767, not a replacement proposal. It implements the design @tehrengruber suggested in review there: prune from the arguments rather than from the concat_where's own domain, since concat_where implicitly broadcasts the dimensions a branch lacks.

The finding that shaped it

Pruning from the branch domains alone cannot fix the never-selected-branch bug: the information is not present. Two expressions with byte-identical branch domains require opposite answers, differing only in node.annex.domain:

accessed domain a.annex.domain b.annex.domain correct
A V:[0,10[, K:[0,10[ V:[0,10[ V:[0,10[, K:[0,10[ prune to b
B V:[0,10[, K:[-5,10[ V:[0,10[ V:[0,10[, K:[0,10[ must not prune

a: Field[[Vertex]], b: Field[[Vertex, K]], condition K < 0 in both. infer_expr filters each branch's domain to the dimensions of the branch's own type, and for the branch lacking the concat dimension the one range that would be empty is exactly the one dropped.

So the design completes in one of three ways: feed the promotion the concat_where's domain (that is GridTools#2767), materialise the broadcast in the IR, or leave that bug unfixed. This branch does the second, which is what the review comment writes out as broadcast(a, (Vertex, K)).

The two commits

  1. b04eb80b8 — decide pruning from the branches' domains. Computes the concat_where's dimension set as cond ∪ tb ∪ fb from the branch annex domains, promotes the candidate branch's domain to it, intersects with the (complemented) condition, and prunes only if the kept branch spans all of them. Never reads node.annex.domain, never reads .type. Shares domain_utils.concat_where_branch_domain() with infer_domain._infer_concat_where, so the complement-before-promote ordering exists in one place.
  2. 23b02ad50 — a concat_where.broadcast_branches pass making the implicit broadcast explicit, wired into both pass managers after infer_domain_ops and before canonicalize_domain_argument. After it every branch spans the full dimensions, so the branch-domain test is sufficient. It runs type_inference.reinfer itself, which is legitimate at that point because it is before _infer_concat_where drops the type.

The split is deliberate: commit 2 can be dropped, in which case the never-selected-branch bug stays open.

Both fixes are needed, they are not alternatives

The dimension-dropping bug is a hard compile failure, not a pessimisation — inference.visit_SetAt's assert expr_type.dims == target_type.dims fires, reproduced from plain user code on gtfn. Under python -O the assert disappears and the mismatch reaches the backend. It requires statically known bounds, which is why the existing ffront tests miss it (cases produces runtime bounds, so nothing prunes) and is exactly icon4py's compilation mode.

The never-selected-branch bug is only a missed optimisation; the numbers are correct either way.

Cost, which is the least certain part

broadcast_branches runs type inference and rewrites IR for every concat_where with a lower-dimensional branch — a common icon4py shape. Measured: gtfn output byte-identical for a non-prunable case (fuse_as_fieldop absorbs the deref), the DaCe path produces exactly the as_fieldop(deref, D ∩ cond) its own lowering was constructing internally, and no test churn. But that is not evidence at scale, and it touches code currently being performance tuned.

Testing

iterator_tests 503 → 517. Every regression test verified to fail with its fix removed, keeping the module importable in each case. The ordering test asserts a value rather than pinning an AssertionError, so it also fails under python -O. Full integration_tests on roundtrip + numpy + gtfn: 2030 passed. ruff, tach, mypy clean.

Follow-up worth noting

With broadcast_branches in the pipeline, the promotion path in _translate_concat_where_branch() (gtir_to_sdfg_concat_where.py) becomes unreachable, since len(source_expr.type.dims) < len(output_type.dims) can no longer hold. Left alone here — out of scope, and translate_concat_where could still be handed unnormalised IR.

FlorianDeconinck and others added 4 commits August 10, 2026 12:12
## Description

We introduced the (strict) support for `IntEnum` in code and arguments
of stencils. The strategy is to replace them with proper integer as soon
as possible.

Guardrails exists for name clash and `IntEnum` derivation. 

Valid code looks like this

```python
@gtscript.enum
class LocalEnum(IntEnum):
    A = 42
    B = 1000

@GTScript.stencil
def enum(field: gtscript.Field[float], order: LocalEnum):  # type: ignore
    with computation(PARALLEL), interval(0, 1):
        if order > LocalEnum.A:
            field[0, 0, 0] = LocalEnum.B
```

TO CLEAN
- [ ] Register of Enum types should not leave in `gtscript`
- [x] ADR
- [x] Mixed precision

## Requirements

- [ ] All fixes and/or new features come with corresponding tests.
- [ ] Important design decisions have been documented in the appropriate
ADR inside the [docs/development/ADRs/](docs/development/ADRs/README.md)
folder.
## Description

Python 3.14 will remove support for a couple of (long) deprecated `ast`
types. Currently produces warnings like

```none
DeprecationWarning: ast.Ellipsis is deprecated and will be removed in Python 3.14; use ast.Constant instead
```

or

```none
DeprecationWarning: ast.Str is deprecated and will be removed in Python 3.14; use ast.Constant instead
```

This PR updates the (cartesian) gtscript frontend to not use those types
anymore and instead rely on things that are future proof, thus
eliminating the warning messages.

## Requirements

- [x] All fixes and/or new features come with corresponding tests.
  Covered by existing tests cases.
- [ ] Important design decisions have been documented in the appropriate
ADR inside the [docs/development/ADRs/](docs/development/ADRs/README.md)
folder.
  N/A
`prune_empty_concat_where` decided whether a branch is ever selected from the branch's
own domain, filtered to the dimensions of the condition, and replaced the `concat_where`
by the other branch without checking what that branch spans.

`concat_where(u⟨ Kᵥ: [-∞, 0[ ⟩, a, b)` with `a: Field[[Vertex, K]]` and `b: Field[[K]]`,
accessed on `Vertex: [0, 10[, K: [0, 10[`, was therefore rewritten to `b`, replacing a
two dimensional expression by a one dimensional one. Type inference then rejects the
program, or -- with assertions disabled -- the mismatch reaches the backend.

A `concat_where` spans the dimensions of its condition and of both branches, since a
branch is implicitly broadcast to the dimensions it does not have itself. Both branch
domains are promoted to those dimensions before intersecting with the region a branch is
selected on, and a branch may only replace the entire expression when it spans all of
them. Dimensions are read off the branch domains rather than the types, because
`infer_domain._infer_concat_where` rebuilds the node through `im.call`, which produces
an untyped `FunCall`.

The region a branch is selected on is what `_infer_concat_where` already computes. That
computation moves to `domain_utils.concat_where_branch_domain` and both callers use it,
so the requirement to take the complement before promoting -- `domain_complement` is
undefined on a range that is infinite on both sides -- exists in one place.
A branch of a `concat_where` is broadcast to the dimensions it does not have itself, so
`concat_where(u⟨ Kᵥ: [-∞, 0[ ⟩, a, b)` with `a: Field[[Vertex]]` and `b: Field[[Vertex, K]]`
is the same as `concat_where(u⟨ Kᵥ: [-∞, 0[ ⟩, broadcast(a, (Vertex, K)), b)`.

Leaving that broadcast implicit loses the region a branch is selected on: domain inference
restricts the domain of an expression to the dimensions of its type, so for `a` above the
selected region `Vertex: [0, 10[, K: [0, 0[` is recorded as `Vertex: [0, 10[` -- and it is
exactly the dropped range that is empty. A branch that is selected nowhere hence survived
`prune_empty_concat_where`, and a branch that is selected everywhere could not replace the
expression because it does not span all its dimensions.

`broadcast_branches` inserts the broadcast, which `remove_broadcast` turns into an
`as_fieldop(deref, ...)` right after pruning -- the same expression that the DaCe lowering
of `concat_where` builds for such a branch anyway. For the gtfn pipeline the additional
field operator is fused away again.
@tehrengruber

Copy link
Copy Markdown

Superseeded by GridTools#2795. Should cover all cases and more.

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.

4 participants