Skip to content

[compiler] carry Python containers through dynamic if/for/while/ifexp#874

Open
xudoyuan wants to merge 20 commits into
mainfrom
xudoyuan/dyn-cf-list-carry
Open

[compiler] carry Python containers through dynamic if/for/while/ifexp#874
xudoyuan wants to merge 20 commits into
mainfrom
xudoyuan/dyn-cf-list-carry

Conversation

@xudoyuan

Copy link
Copy Markdown
Collaborator

Dynamic scf.if / scf.for / scf.while / ifexp previously required every carried variable to be a single ir.Value, so a reassigned list/tuple/dict local raised "state variable '...' is list, not an MLIR Value".

Route the four dispatchers through an explode/assemble step that reuses the existing DSL<->ir.Value protocol (protocol.extract/construct): each carried value (possibly a nested container) is exploded to per-element iter_args/ results and assembled back from the pre-region value used as the structural template on exit. Scalars stay the degenerate single-slot case so existing behaviour is unchanged; a branch/body that changes a container's shape or an element's dtype now fails with a clear error.

Adds MLIR-level unit tests and device system tests for list/tuple/dict/nested carry across if/for/while/ifexp.

Motivation

Technical Details

Test Plan

Test Result

Submission Checklist

@xudoyuan
xudoyuan force-pushed the xudoyuan/dyn-cf-list-carry branch 2 times, most recently from cea9d05 to e6a56ab Compare July 24, 2026 09:07
@xudoyuan
xudoyuan marked this pull request as ready for review July 24, 2026 09:11
xudoyuan and others added 18 commits July 24, 2026 16:29
Dynamic scf.if / scf.for / scf.while / ifexp previously required every
carried variable to be a single ir.Value, so a reassigned list/tuple/dict
local raised "state variable '...' is list, not an MLIR Value".

Route the four dispatchers through an explode/assemble step that reuses the
existing DSL<->ir.Value protocol (protocol.extract/construct): each carried
value (possibly a nested container) is exploded to per-element iter_args/
results and assembled back from the pre-region value used as the structural
template on exit. Scalars stay the degenerate single-slot case so existing
behaviour is unchanged; a branch/body that changes a container's shape or an
element's dtype now fails with a clear error.

Adds MLIR-level unit tests and device system tests for list/tuple/dict/nested
carry across if/for/while/ifexp.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The device/launch test belongs with the other end-to-end tests under
tests/system (renamed to the *_e2e.py convention); the MLIR-level unit test
stays under tests/unit. Aligns the header with the existing e2e tests
(direct torch import + skip).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Rename the test class to *E2E, reword the module docstring, and use the
direct `import torch` + skip style shared by the other tests/system e2e files.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Replace the bespoke explode/assemble recursion in the dynamic control-flow
dispatchers with the existing protocol.extract_to_ir_values /
construct_from_ir_values, so there is a single source of truth for the
DSL-value <-> flat ir.Value round trip. Removes the duplicate
_explode_carried / _assemble_carried / _carried_slot_count helpers (and the
now-dead _unwrap_mlir_values / _pack_dispatch_results); keeps only the three
shared unpack/pack/verify helpers reused by if/for/while/ifexp.

protocol.py: add dict handling to get_ir_types / extract_to_ir_values /
construct_from_ir_values (parallel to the SimpleNamespace branch); make the
construct list/tuple branch dispatch on the exemplar so a top-level container
works while staying backward compatible with the type-sequence call; add a
bare-scalar coercion fallback to extract so python scalars still promote.

Net -96 lines in ast_rewriter.py. unit + struct + dispatch + device e2e green.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Add unit cases carrying pure dict, tuple and SimpleNamespace through a dynamic
scf.if, plus mixed nesting (tuple-of-dict, dict-of-list, SimpleNamespace of
list+tuple+scalar, and a 3-deep list[dict{tuple}, list]). Also carry a dict
through scf.for and a tuple-of-dict through scf.while. A small _carry_through_if
helper removes the repeated MLIR scaffolding for the if cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…_values

extract_to_ir_values now raises for anything it cannot flatten, matching its
original contract; the promotion of a bare python scalar to an MLIR constant
lives in _explode_states, where the pre-refactor control-flow code handled it.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Normalize a carried python scalar to an MLIR constant with a direct type check
before flattening, mirroring how the pre-refactor control flow ran every value
through as_ir_value. Drops the try/except-on-TypeError fallback so scalar
coercion is a deliberate control-flow step rather than an error path.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Carry a dict through if, a tuple through for, a SimpleNamespace through while,
and a tuple-holding-a-dict through if, each in a real kernel launched on device,
matching the container variety already covered at the MLIR-unit level.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The whole tuple of pre-region values is the structure template, so unpack it in
one extract_to_ir_values / get_ir_types call and rebuild it in one
construct_from_ir_values call. Replaces the per-name _explode_states /
_assemble_states / _explode_branch_outputs (with hand-maintained counts and
per-name exemplars) with three thin helpers: _unpack_states, _pack_states, and
_unpack_branch_outputs. Structure verification collapses to a single flat-type
equality check; its message unifies the former shape/dtype errors, so the
matching tests are updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…tched var

- Condense the module comment and the three helper docstrings to what they do.
- Drop the outer list() at _pack_states call sites; the helper already wraps slots.
- _unpack_branch_outputs now checks each carried variable against its entry value
  per name, so the error names the offending variable and shows its entry vs exit
  types instead of one flattened whole-region comparison.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
scf.for infers its result types from iter_args, so the flat types from
_unpack_states are unused there; unpack them into _ instead of a named var.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Two defects in carrying state through dynamic control flow:

1. A bare python scalar entry (acc = 0) was promoted to a raw ir.Value, which
   construct_from_ir_values cannot rebuild (no __construct_from_ir_values__), so
   packing threw 'Cannot construct DSL value for ArithValue'. Promote scalars to
   a DSL numeric via as_dsl_value(as_ir_value(v)) instead, so the exemplar stays
   reconstructable. This restores the acc = 0 / acc = 0.0 accumulator pattern.

2. Branch-output verification compared only the flattened leaf ir.Types, so a
   branch reordering dict/namespace keys yielded values into the wrong slots with
   no diagnostic (silent miscompile). Compare the full pytree structure
   (container kind + keys/length + order + leaf dtypes) via _tree_structure.

Adds unit guards (bare-scalar for-accumulator, dict key-reorder error) and an
e2e bare-scalar accumulator; error-message match strings updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
A dict's key order is not semantic ({'a':1,'b':2} == {'b':2,'a':1}), so a branch
listing keys in a different order than the entry should carry correctly, not
error. Replace the structure-equality check with _flatten_like, which flattens a
branch output guided by the entry template: dict/namespace are aligned by key
(order-insensitive), list/tuple by position. Reordered keys now land in the
right slots; genuine mismatches (different key set, length, or dtype) still
raise a clear per-variable error.

Guards: unit tests for key reorder (accepted) and key-set mismatch (errors);
e2e for a reordered else branch keeping a=100, b=200.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Covers acc = 0.0 alongside the existing acc = 0 guard; both go through the same
scalar-promotion path but the review asked for 0/0.0 explicitly.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…evel

_promote_scalars only mapped the top-level carried states, so a bare literal
nested in a container (e.g. d = {'count': 0, 'vec': [0, x]}) survived unpromoted
and crashed in extract_to_ir_values ('Cannot extract IR values from 0'). Make
promotion recursive (_promote walks dict/namespace/list/tuple), matching the
leaf-level promotion _flatten_like already does for branch outputs. Adds a
nested bare-scalar carry guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Structure {"a": i32, "b": [i32, (i32, i32)], "c": {"d": i32, "e": [i32]}}
(6 leaves, dict/list/tuple mutually nested) carried through dynamic for, if, and
while, with every leaf rewritten inside the region, launched on device and
checked against expected values.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
fx.buffer_ops (attribute on the flydsl.expr package) is gone on current main;
import the submodule directly (from flydsl.expr import buffer_ops) like the other
system tests do. The old form only worked against a stale local build.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
PR #880 moved buffer_ops to kernels/common and dropped the flydsl.expr alias, so
neither fx.buffer_ops nor 'from flydsl.expr import buffer_ops' resolves on main.
Write outputs with plain tensor subscripting (Out[i] = val), matching the other
system tests (e.g. test_while_e2e) and dropping the buffer_ops dependency.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@xudoyuan
xudoyuan force-pushed the xudoyuan/dyn-cf-list-carry branch from 9d49c82 to 70b0af7 Compare July 24, 2026 16:31
@xudoyuan
xudoyuan requested review from coderfeli and sjfeng1999 July 24, 2026 17:35
xudoyuan and others added 2 commits July 24, 2026 17:39
_promote is already recursive, so the _promote_scalars wrapper (its only caller
being _unpack_states) is just [_promote(v) for v in values]; inline it.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Kernels may carry a raw ir.Value (e.g. an ArithValue) as control-flow state, not
a DSL numeric -- flash-attn's lazy_rescale_o does. _promote only handled bare
python scalars, so the raw ir.Value stayed in the exemplar and construct threw
'Cannot construct DSL value for ArithValue'. Promote raw ir.Values via
as_dsl_value too, restoring the old dispatcher's behavior. Adds a regression test
carrying a raw ir.Value through scf.if.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
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