Skip to content

InlineDynamicShifts: let-bound producer shared between dynamically shifted consumers is not inlined #2839

Description

@tehrengruber

InlineDynamicShifts does not inline a let-bound producer shared between two dynamically shifted consumers, so it reaches infer_domain as an UNKNOWN domain and is emitted as an as_fieldop without a domain.

Reproducer

@gtx.field_operator
def testee(a: IKFloatField, b: IKFloatField, off: IKField) -> IKFloatField:
    x = b * 3.0
    p = (a + x) * 2.0
    q = (b + x) * 4.0
    return p(as_offset(Koff, off)) + q(as_offset(Koff, off))

Fails on roundtrip.gtir with TypeError: as_fieldop() missing 1 required positional argument: 'domain', and on dace_cpu / dace_cpu_noopt with the assert len(fun_node.args) == 2 at gtir_to_sdfg_primitives.py:239. Under PYTHONOPTIMIZE — which ICON's generated setting script exports — that assert is stripped and the next line surfaces it as ValueError: not enough values to unpack (expected 2, got 1). Passes on embedded and on roundtrip.default.

Cause

The IR reaching the pass binds the shared subexpression b * 3.0 in a let and uses it inside both shifted consumers:

(λ(xᐞ0) → as_fieldop(+)(
     as_fieldop(λ(a, xᐞ0, off) → …⟪Koff, ·off⟫(a) + …⟪Koff, ·off⟫(xᐞ0)…)(a, xᐞ0, off),
     as_fieldop(λ(b, xᐞ0, off) → …⟪Koff, ·off⟫(b) + …⟪Koff, ·off⟫(xᐞ0)…)(b, xᐞ0, off)))(
   as_fieldop(×)(b, 3.0))

Two things have to line up and neither does:

  1. The let-inlining pre-pass in visit_FunCall only fires when the let body is itself the dynamically shifted as_fieldop. Here the body is the + node one level up, so _dynamic_shift_args(let_body) is all-False and xᐞ0 is never marked for inlining.
  2. Because xᐞ0 stays a SymRef, the fusion loop skips it — fuse_args starts with not isinstance(inp, itir.SymRef) — and stops at if not any(fuse_args): break with arguments a, xᐞ0, off.

infer_domain then marks xᐞ0 UNKNOWN and emits its producer without a domain.

Why the obvious fix is not acceptable

Searching the whole let body for dynamically shifted as_fieldops (node.fun.expr.pre_walk_values()) and revisiting after inlining does make the reproducer pass, and the existing suites stay green. It is not viable on cost grounds: visit_FunCall runs at every let node, and _dynamic_shift_args calls trace_shifts.trace_stencil at every FunCall it walks past, so each let re-traverses its entire body with a stencil trace per node. On the deep let chains that CSE produces for real stencils that is far too expensive.

A workable fix within the current IR likely needs the reverse direction — collecting, in one pass, which symbols are consumed in a dynamically shifted argument position, and consulting that when the let node is reached — rather than re-walking the body per let.

More fundamentally, this is a symptom of the IR being a tree in which sharing is encoded as a let binding and a reference is just a name. The pass has a SymRef in hand and needs to know what it denotes, and the only ways to answer that are to carry a symbol table, to re-walk, or to inline the binding away — which is what the pre-pass does, and why it has to guess in advance which bindings will matter. In a graph representation, where a reference is an edge to the defining node, the question is answered by following the edge: the fusion loop could look through xᐞ0 directly, see the as_fieldop behind it, and fuse without any inlining, any pre-pass, or any traversal of the surrounding body. Shared subexpressions would then be shared structurally instead of by name, so the "is this argument a SymRef?" test — the thing that makes the loop skip exactly the shared case — would not need to exist at all.

Notes

AI Disclaimer: This issue was written with the help of AI tools. I read the description and found it to be decent.


Reported by @havogt in #2829 (comment).

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    gt4py.nextIssues concerning the new version with support for non-cartesian grids.triage: bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions