Skip to content

feat(ir): dispatch-predicate scope form for pl.at - #2151

Open
luohuan19 wants to merge 1 commit into
hw-native-sys:mainfrom
luohuan19:issue-2066-at-dispatch-predicate
Open

feat(ir): dispatch-predicate scope form for pl.at#2151
luohuan19 wants to merge 1 commit into
hw-native-sys:mainfrom
luohuan19:issue-2066-at-dispatch-predicate

Conversation

@luohuan19

Copy link
Copy Markdown
Contributor

Phase 4 of hw-native-sys/pypto#2066 — dispatch-predicate phased rollout. Phase 1 (hw-native-sys/pypto#2067) shipped the call form, Phase 3 (hw-native-sys/pypto#2092) the pl.spmd scope form. This is the pl.at cut Phase 3 explicitly deferred. Pure frontend on the current runtime — no submodule bump.

What this does

Extends predicate= to with pl.at(level=pl.Level.CORE_GROUP, ...):

with pl.at(level=pl.Level.CORE_GROUP) as g_tid:                  # producer of rc
    rc = pl.store(pl.load(rc, [0, 0], [128, 128]), [0, 0], rc)

with pl.at(level=pl.Level.CORE_GROUP,                            # producer is a dep
           deps=[g_tid], predicate=(rc[0, 0] > 0)) as tid:
    out = pl.store(pl.load(x, [0, 0], [128, 128]), [0, 0], out)

Same expression, validation, lowering and runtime ABI as the two existing forms. The predicate rides on InCoreScopeStmt::attrs_ from parse until OutlineIncoreScopes moves it onto Submit::predicate_.

Why it really was "parser + dsl_api only"

Phase 3's prediction held. Every scope-attr walker — IRVisitor::VisitScopeAttrs, IRMutator::MutateScopeAttrs, ConvertToSSA::SubstScopeAttrs, DCE FindLiveRoots, structural_hash, NoNestedCall::ShouldVisitScopeAttr — and ScopeOutliner itself are already scope-kind agnostic. pl.at InCore scopes are outlined at pass 8, a strict subset of the passes the pl.spmd predicate (pass 9) already survives, so there is no new pass exposure. The only C++ additions are the printer hookup and two backstop asserts.

Placement restrictions

A predicate only has a runtime carrier where the scope becomes an independently submitted L0 task. Two placements would silently drop it, so both are rejected at parse time:

Placement Why it would be lost
level != CORE_GROUP Builds a HierarchyScopeStmt. OutlineHierarchyScopes only rewrites scopes inside Opaque functions, so inside an @pl.function(type=Orchestration) the scope survives the entire Default pipeline un-outlined and never produces a Submit (verified empirically).
nested in pl.cluster() / pl.spmd() / another pl.at The inner dispatch is folded into the enclosing Group / Spmd wrapper (or the enclosing kernel body); BuildSpmdCallDispatchPlan / BuildAivOnlyGroupDispatchPlan / BuildMixedGroupDispatchPlan all pass the outer wrapper call to EmitPredicateHint.

Mirrors _reject_spmd_submit_only_kwargs_in_cluster. For hand-built / deserialized IR both are re-asserted in C++ — ScopeOutliner guards the Hierarchy case, and a new AssertNoInnerDispatchPredicate in OutlineClusterScopes walks each outlined Group / Spmd body for a Submit::predicate_ (same pattern as the existing UnwrapNestedSpmd guard).

Scope-producer tracking

_record_scope_producer (added for pl.spmd in Phase 3) now also runs on with pl.at(...) as tid:, so the Phase-2 producer∈deps= contract genuinely applies to tensors a pl.at body writes. Unlike pl.spmd, pl.at accepts deps= on every form, so the remediation hint always offers deps=[tid] instead of redirecting to the as tid capture form (which would be wrong advice here).

Outliner bug found and fixed (also affects the merged pl.spmd form)

ScopeOutliner read kAttrPredicate raw, bypassing its own store_target_renames_ substitution — a target-kind scope skips the base MutateScopeAttrs walk, since VisitScopeKind goes straight to OutlineScope.

A pl.at body typically writes its tensor with pl.store, so after SSA the attr names a scope-local post-store alias. Outlining exports that store target under a fresh call-site Var and drops the alias, leaving the emitted Submit referencing a Var with no definition in the parent function:

ret__tmp_v0_1 = pl.submit(main_incore_1, ..., predicate=(0 < pl.cast(pl.tensor.read(rc__ssa_v1__FREE_VAR, [1, 2]), pl.INDEX)))
#                                                                   ^^^^^^^^^^^^^^^^^^^^^ dangling

UseAfterDefCheck catches it, so it fails loudly rather than miscompiling — but it made the feature unusable for the most natural pl.at spelling. The operand is now resolved through the same rename map the synthesised call's args use, i.e. to the value current as of this scope.

This was latent for pl.spmd too: a pl.spmd predicate over a tensor written by an earlier sibling pl.at scope hits the identical path. The pl.spmd STs did not surface it because their producer scopes bind via a call result, not a store.

Testing

  • Parser (test_submit_predicate.py, +12 tests) — attr landing, canonical attr order, print → reparse assert_structural_equal, no-as tid form, the level rejection, all three nesting rejections, shared shape validation, the scope-producer contract (accepts with deps=, rejects without), and the hint wording.
  • Transforms (test_submit_passes.py) — SSA versions the operand inside the InCore scope attr; test_outlining_rebinds_a_store_produced_predicate_operand pins the fix above (it reproduces the __FREE_VAR output without it); DCE keeps a Var used only in the predicate index.
  • Codegen (test_predicate_codegen.py, +9 tests) — the L0TaskPredicate block with &ext_rc, operator/operand-order mapping, set_predicate before rt_submit_*, exactly one predicated task. Plus a windowize=True case: windowize is pl.at-only, making this the only DSL path that reaches the kAttrPredicate entry in WindowExternalization's view-synthesised key filter, which until now was written defensively and unreachable from the frontend. Verified the rewrite genuinely fires (the emitted args switch to ext_x.view(...)).
  • On-board a2a3 STtests/st/runtime/scheduling/test_predicated_dispatch.py gains a pl.at variant with the identical task chain and expected values. 6/6 pass across all three authoring forms (gate=0 skips, gate=1 dispatches; the consumer unlocks in both cases, so inline-retire still settles fanout).
  • Regression — full UT suite 7808 passed, 2 skipped.

Docs

docs/en/dev/language/00-python_syntax.md + docs/zh-cn/... — the stale "It is not accepted on pl.at(...)" note is replaced with the surface, a pl.at subsection covering the two placement rules, and the deps=-on-every-form difference vs pl.spmd. The pl.at ... as tid table row is updated to match how pl.spmd's row documents the kwarg.

Refs #2066

Refs hw-native-sys#2066

Extends `predicate=` from `pl.submit` / `pl.spmd_submit` (Phase 1, hw-native-sys#2067) and
the `with pl.spmd(...)` scope form (Phase 3, hw-native-sys#2092) to `pl.at`:

    with pl.at(level=pl.Level.CORE_GROUP) as g_tid:
        rc = pl.store(pl.load(rc, [0, 0], [128, 128]), [0, 0], rc)

    with pl.at(level=pl.Level.CORE_GROUP,
               deps=[g_tid], predicate=(rc[0, 0] > 0)) as tid:
        out = pl.store(pl.load(x, [0, 0], [128, 128]), [0, 0], out)

Same expression, validation, lowering and runtime ABI as the existing forms —
the predicate rides on `InCoreScopeStmt::attrs_` until `OutlineIncoreScopes`
moves it onto `Submit::predicate_`. The scope-attr walkers (visitor, mutator,
ConvertToSSA, DCE, structural_hash, NoNestedCall) were already scope-kind
agnostic, so this is parser + dsl_api + printer, as the tracking issue predicted.

Placement is restricted to where the scope actually becomes an independently
submitted L0 task, since anywhere else the predicate has no runtime carrier and
would be silently dropped:

  * `level=pl.Level.CORE_GROUP` only — a Hierarchy `pl.at` inside an
    Orchestration function survives the whole Default pipeline un-outlined
    (`OutlineHierarchyScopes` is Opaque-only) and never produces a `Submit`.
  * not nested inside `pl.cluster()` / `pl.spmd()` / another `pl.at` — the
    inner dispatch is folded into the enclosing Group / Spmd wrapper, and
    codegen emits `set_predicate` from the outer call only.

Both are rejected at parse time, with `ScopeOutliner` and a new
`AssertNoInnerDispatchPredicate` in `OutlineClusterScopes` re-asserting them for
hand-built / deserialized IR (mirroring the `UnwrapNestedSpmd` guard).

Scope-producer tracking (`_record_scope_producer`, added for `pl.spmd` in
Phase 3) now also runs on `with pl.at(...) as tid:`, so the producer-in-`deps=`
contract genuinely applies. Unlike `pl.spmd`, `pl.at` accepts `deps=` on every
form, so the remediation hint always offers it.

Outliner fix: `ScopeOutliner` read the predicate attr raw, bypassing its own
`store_target_renames_` substitution. A `pl.at` body typically writes its tensor
with `pl.store`, so the attr names a scope-local post-store alias that outlining
drops — the emitted `Submit` then referenced a dangling Var (UseAfterDefCheck).
The operand is now resolved exactly as the synthesised call's args are. This
also closes the same latent hole for a `pl.spmd` predicate over a tensor written
by an earlier sibling `pl.at` scope.

Tests: parser (attr landing, canonical order, print round-trip, no-`as tid`
form, level + three nesting rejections, shared shape validation, scope-producer
contract, hint wording), transforms (SSA versioning, outliner rebinding, DCE
liveness), codegen (`set_predicate` block, operator/operand-order, emission
order, and a `windowize=True` case — the only DSL path that reaches the
predicate entry in WindowExternalization's view-synthesised key filter).
On-board a2a3 ST gains a `pl.at` variant of the predicated-dispatch chain:
6/6 pass across all three authoring forms. Full UT suite 7791 passed, 2 skipped.

Docs: `docs/en` + `docs/zh-cn` python_syntax — the stale "not accepted on
pl.at" note is replaced with the surface, its two placement rules, and the
`deps=` difference vs `pl.spmd`.
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds predicate= support to pl.at(level=CORE_GROUP, ...), including parser validation, dependency tracking, SSA-aware outlining, Python printing, IR safeguards, documentation, runtime tests, and code-generation coverage.

Changes

CORE_GROUP pl.at predicate support

Layer / File(s) Summary
Frontend predicate contract
python/pypto/language/dsl_api.py, python/pypto/language/parser/ast_parser.py, tests/ut/language/parser/test_submit_predicate.py, docs/{en,zh-cn}/dev/language/00-python_syntax.md
Adds the predicate argument, validates CORE_GROUP placement and nesting, tracks producing scopes for deps=, and documents the updated predicate forms.
SSA-aware outlining and validation
include/pypto/ir/transforms/utils/scope_outline_utils.h, src/ir/transforms/outline_cluster_scopes_pass.cpp, src/ir/transforms/python_printer.cpp
Rewrites predicate operands during outlining, rejects predicates that would be dropped by wrapper scopes, and prints scope predicates.
IR preservation tests
tests/ut/ir/transforms/test_submit_passes.py
Covers SSA conversion, store-produced operand rebinding, and DCE retention of predicate producers.
Runtime and codegen coverage
tests/st/runtime/scheduling/test_predicated_dispatch.py, tests/ut/codegen/test_predicate_codegen.py
Tests predicated pl.at execution, predicate emission and ordering, operator/index mapping, and window externalization.‌

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

  • hw-native-sys/pypto#2066 — Covers the frontend rollout for pl.at predicate support and its propagation through parsing, outlining, and code generation.
  • hw-native-sys/pypto#2059 — Tracks the dispatch-predicate frontend and related parser, IR, and code-generation work.

Possibly related PRs

  • hw-native-sys/pypto#2067 — Adds the preceding pl.submit and pl.spmd_submit predicate frontend and shared printer plumbing.
  • hw-native-sys/pypto#2092 — Modifies shared scope predicate extraction, SSA rewriting, and transfer to Submit.predicate_.

Sequence Diagram(s)

sequenceDiagram
  participant User as Python DSL
  participant Parser as AST parser
  participant Scope as InCoreScopeStmt
  participant Outliner as ScopeOutliner
  participant Submit as Submit
  participant Codegen as Runtime codegen
  User->>Parser: define pl.at(predicate=...)
  Parser->>Scope: validate and attach predicate
  Scope->>Outliner: provide scope predicate
  Outliner->>Submit: rewrite operands and transfer predicate
  Submit->>Codegen: emit set_predicate before submit
Loading

Poem

A rabbit hops through scopes of code,
With predicates lightening the load.
SSA paths neatly realign,
Submit follows every sign.
“CORE_GROUP!” the bunny sings—
And guarded tasks grow useful wings.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately names the main change: adding dispatch-predicate support for pl.at.
Description check ✅ Passed The description matches the changeset and clearly explains the new pl.at predicate support, restrictions, fixes, and tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

for (auto& func : all_outlined_functions) {

P2 Badge Check pre-existing wrapper functions for inner predicates

When the input program already contains a FunctionType::Group or FunctionType::Spmd function with a predicated inner Submit, the main pass loop places that function in new_functions and continues, while this guard iterates only all_outlined_functions. Such parser-authored, hand-built, or deserialized wrappers therefore bypass AssertNoInnerDispatchPredicate, and the outer wrapper dispatch can silently ignore the inner predicate; run the guard over both existing and newly outlined wrapper functions.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +4928 to +4932
for kind, owner in (
(ir.ScopeKind.Cluster, "pl.cluster()"),
(ir.ScopeKind.Spmd, "pl.spmd()"),
(ir.ScopeKind.InCore, "another pl.at(level=pl.Level.CORE_GROUP)"),
):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject predicates in every non-dispatching context

When a predicated CORE_GROUP pl.at is nested in a non-CORE_GROUP pl.at, the scope stack contains ScopeKind.Hierarchy, which this list never checks; similarly, a top-level predicated pl.at inside an explicitly declared Group or Spmd function is accepted even though OutlineIncoreScopes skips those function types. In both cases the scope does not become an independently dispatched L0 task and its predicate can be dropped, despite the updated documentation prohibiting nesting inside another pl.at; include the hierarchy and enclosing-function contexts in the placement gate.

AGENTS.md reference: AGENTS.md:L38-L39

Useful? React with 👍 / 👎.

// ``pl.at(level != CORE_GROUP, predicate=...)``; guard here for hand-built /
// deserialized IR, mirroring the UnwrapNestedSpmd guard in
// OutlineClusterScopes.
INTERNAL_CHECK_SPAN(!scope_predicate || !As<HierarchyScopeStmt>(op), op->span_)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate hierarchy predicates before skipping orchestration functions

For hand-built or deserialized IR containing a predicated HierarchyScopeStmt in an Orchestration function, OutlineHierarchyScopes skips the function because it only processes Opaque functions, so this OutlineScope check is never reached. The unsupported scope therefore survives the pipeline with its predicate instead of triggering the promised backstop, allowing codegen to silently lose the dispatch condition; perform this validation before the function-type skip or in a program-wide verifier.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/ut/ir/transforms/test_submit_passes.py`:
- Around line 530-534: Strengthen the assertions after printing `main` to
inspect the outlined `Submit` nodes and verify that the predicated submit’s
operand is the call-site `rc` result exported by the preceding gate submit. Keep
the existing predicate and `__FREE_VAR` checks, but distinguish the gate
submit’s exported result from the pre-store/input `rc`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8f060f2f-c6de-495f-9586-b2c0f4abede6

📥 Commits

Reviewing files that changed from the base of the PR and between ef6ce7c and 81efc4c.

📒 Files selected for processing (11)
  • docs/en/dev/language/00-python_syntax.md
  • docs/zh-cn/dev/language/00-python_syntax.md
  • include/pypto/ir/transforms/utils/scope_outline_utils.h
  • python/pypto/language/dsl_api.py
  • python/pypto/language/parser/ast_parser.py
  • src/ir/transforms/outline_cluster_scopes_pass.cpp
  • src/ir/transforms/python_printer.cpp
  • tests/st/runtime/scheduling/test_predicated_dispatch.py
  • tests/ut/codegen/test_predicate_codegen.py
  • tests/ut/ir/transforms/test_submit_passes.py
  • tests/ut/language/parser/test_submit_predicate.py

Comment on lines +530 to +534
main = program.get_function("main")
assert main is not None
printed = ir.python_print(main)
assert "predicate=(" in printed, printed
assert "__FREE_VAR" not in printed, f"predicate references a dangling Var:\n{printed}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the predicate binds to the gate submit’s exported rc result.

These checks also pass if outlining binds the predicate to the pre-store/input rc: it is defined and prints without __FREE_VAR, but dispatch observes the wrong value. Inspect the outlined Submit nodes and assert the predicated submit’s operand is the call-site rc result exported by the preceding gate submit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/ut/ir/transforms/test_submit_passes.py` around lines 530 - 534,
Strengthen the assertions after printing `main` to inspect the outlined `Submit`
nodes and verify that the predicated submit’s operand is the call-site `rc`
result exported by the preceding gate submit. Keep the existing predicate and
`__FREE_VAR` checks, but distinguish the gate submit’s exported result from the
pre-store/input `rc`.

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

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant