Skip to content

fix(ir): make composite collective barrier signals reusable - #2175

Open
georgebisbas wants to merge 2 commits into
hw-native-sys:mainfrom
georgebisbas:fix/2156-reusable-collective-barrier-signals
Open

fix(ir): make composite collective barrier signals reusable#2175
georgebisbas wants to merge 2 commits into
hw-native-sys:mainfrom
georgebisbas:fix/2156-reusable-collective-barrier-signals

Conversation

@georgebisbas

Copy link
Copy Markdown
Contributor

Closes #2156.

The composite collective lowerings shared a Set(1) + Wait(>= 1) completion barrier. That leaves every signal cell at 1 after a call returns, so a later invocation reusing the same signal could satisfy its waits from stale state before peers finished the new data transfers — returning mixed or partial results. This replaces it with one generation protocol shared by the whole pld.tensor.* family, so signals are safely reusable.

The protocol

for peer != my_rank:  pld.system.notify(signal, peer, <my cell>, 1, op=AtomicAdd)
for src  != my_rank:  pld.system.wait  (signal, <src cell>, generation, cmp=Ge)

AtomicAdd turns each cell into a monotonically increasing count of how many barriers the rank owning that row has entered, so the n-th barrier issued against a signal waits for >= n.

Latent bug fixed on the way

reduce_scatter mixed Set (ready barrier) and AtomicAdd (post-reduce barrier) on the same cells. Every collective now notifies with AtomicAdd only.

New compile-time rejections (previously silent corruption)

  • Any collective inside a dynamic for/while loop (all six, not just allreduce)
  • Signal shared between per-cell mesh and per-row ring protocols
  • Reuse after a mesh allreduce over a symbolic extent

Changes

  • src/ir/transforms/lower_composite_ops_pass.cppSignalGenerationTable + LoweringBuilder::EmitBarrier helpers; all six collectives converted
  • tests/ut/ir/transforms/test_lower_composite_ops.py — generation sequencing, AtomicAdd/Ge, rebind chaining, pl.unroll escape hatch, rejections (80 passed)
  • docs/en/dev/distributed_ops.md + docs/zh-cn/dev/distributed_ops.md — notify/wait docs restructured

Test plan

  • UT: 80 passed in test_lower_composite_ops.py, 2622 wider UT regression
  • ST: test_l3_tensor_collective_signal_reuse.py — back-to-back all_to_all, allgather, reduce_scatter green on sim (P=2)
  • Existing collective STs regression (sim): 17 passed, 17 skipped (4-rank)
  • Lints: ruff, clang-format, pyright, headers, EN-only, EN↔ZH parity
  • NPU verification pending (sim only)

The composite collective lowerings shared a `Set(1)` + `Wait(>= 1)`
completion barrier. That leaves every signal cell at `1` after a call
returns, so a later invocation reusing the same signal could satisfy its
waits from stale state before peers finished the new data transfers,
returning mixed or partial results.

Replace it with one generation protocol shared by the whole
`pld.tensor.*` family: each barrier does `AtomicAdd(1)` into every peer's
cell, then `Wait(>= generation)`, where `generation` counts how many
barriers have been issued against that signal. Cells become a monotonic
per-rank barrier count, so the n-th barrier waits for `>= n` and a
signal is safely reusable across back-to-back calls — including chains
that mix collectives.

Generations are assigned at lowering time by `SignalGenerationTable`,
keyed on the signal's allocation identity so the rebind idiom
`sig = pld.tensor.barrier(sig)` keeps counting on one counter instead of
restarting. Collectives barrier through `LoweringBuilder::EmitBarrier`,
so a new collective inherits the protocol rather than rolling its own.

This also removes a latent hazard in `reduce_scatter`, which mixed
`Set` and `AtomicAdd` on the same cells — a set can clobber an
already-advanced counter.

Cases the compile-time scheme cannot make safe are now rejected with an
actionable error instead of silently degrading:

- any collective inside a dynamic `for` / `while` loop (previously only
  `allreduce` was rejected; the other five were silently broken)
- one signal shared between the per-cell mesh and per-row ring protocols
- reuse after a mesh `allreduce` over a symbolic extent, whose per-chunk
  barrier count is not statically known

Tests: protocol assertions (generation sequence, AtomicAdd/Ge, rebind
chaining, rejections) in `test_lower_composite_ops.py`, plus a
back-to-back distributed ST for `all_to_all` / `allgather` /
`reduce_scatter` on a shared signal.

Closes hw-native-sys#2156
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 53e1f9a4-2f62-4a26-aa59-5ce5dd8f9ae5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The composite lowering pass now assigns reusable compile-time barrier generations across tensor collectives. Lowerings, DSL and developer documentation, unit tests, and distributed tests were updated for AtomicAdd/Ge synchronization, signal aliasing, reuse restrictions, and loop validation.

Changes

Collective barrier signal reuse

Layer / File(s) Summary
Generation tracking and validation
src/ir/transforms/lower_composite_ops_pass.cpp
Adds signal generation tracking, alias preservation, shared nested-builder state, generation reservation, and generalized collective loop checks.
Collective barrier rewiring
src/ir/transforms/lower_composite_ops_pass.cpp
Routes all tensor collective barriers through reusable generation helpers, including mesh chunks and ring rows.
Protocol and signal-reuse tests
tests/ut/ir/transforms/test_lower_composite_ops.py, tests/st/distributed/collectives/test_l3_tensor_collective_signal_reuse.py
Tests generation progression, AtomicAdd/Ge lowering, rejection cases, loop behavior, and repeated collective results.
Barrier protocol documentation
docs/en/dev/..., docs/zh-cn/dev/..., python/pypto/language/distributed/op/tensor_ops.py
Documents reusable signals, generation assignment, collective-specific accounting, and signal-lifetime constraints.

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

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant CollectiveLowering
  participant SignalGenerationTable
  participant DistributedSignal
  CollectiveLowering->>SignalGenerationTable: allocate next generation
  CollectiveLowering->>DistributedSignal: AtomicAdd notification
  DistributedSignal-->>CollectiveLowering: Ge wait reaches generation
  CollectiveLowering->>SignalGenerationTable: reserve later generations
Loading

Poem

A rabbit hops where signals gleam,
AtomicAdd guards every stream.
Generations rise, stale waits flee,
Collectives share a safe decree.
“Ge!” cries the bunny, pleased and bright.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: reusable composite collective barrier signals.
Description check ✅ Passed The description matches the implemented reusable generation-based barrier protocol and its tests/docs.
Linked Issues check ✅ Passed The PR implements a shared reusable barrier protocol, adds back-to-back regression coverage, and updates docs as required by #2156.
Out of Scope Changes check ✅ Passed The changes stay within the collective-barrier protocol work, with related docs, tests, and lowering updates only.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

Here are some automated review suggestions for this pull request.

Reviewed commit: e6219ce9ea

ℹ️ 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 +1837 to +1839
if (!IsTensorCollective(call)) return;
CHECK_SPAN(repeating_scope_depth_ == 0, call->span_)
<< "pld.tensor.allreduce is not supported inside a for/while loop. "
"The signal protocol is single-use and cannot reuse a signal across dynamic invocations.";
<< call->op_->name_

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 collectives under conditional control flow

When a pld.tensor.* collective appears inside an if/else, the mutator still lowers both branches while using a single compile-time generation counter, but only one branch executes at runtime. For example, if flag: sig = pld.tensor.barrier(sig) else: sig = pld.tensor.barrier(sig) assigns generation 1 to the then branch and generation 2 to the else branch, so taking the else branch deadlocks waiting for a counter that was incremented only once; a later barrier after the if can also wait on stale state because the IfStmt result is not tied to the executed branch's generation. The new check only forbids for/while, so conditional collectives remain silently miscompiled; either reject them or model generation as control-flow-dependent.

AGENTS.md reference: AGENTS.md:L36-L39

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in the latest push. The repeating_scope_depth_ counter has been renamed to scope_depth_ and now also increments for IfStmtPtr visitors, so collectives inside for/while/if/else are all rejected. The error message has been updated accordingly: "is not supported inside a for/while/if/else scope."

Additionally, LoweringBuilder now tracks a nested_ flag and guards EmitBarrier() with INTERNAL_CHECK_SPAN(!nested_) — inner builders created by EmitFor, EmitIf, and EmitIfExpr are all marked nested so they cannot emit barriers directly.

@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

🧹 Nitpick comments (1)
src/ir/transforms/lower_composite_ops_pass.cpp (1)

444-457: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider enforcing the "straight-line only" contract for EmitBarrier.

The restriction is documented but unchecked; calling it from an EmitFor/EmitIf body would reserve one generation for a barrier that executes N times, producing a hang or a silently degraded barrier. A bool nested_ flag set on builders constructed by the control-flow emitters plus an INTERNAL_CHECK_SPAN(!nested_, span) here would make the misuse a build-time failure rather than a runtime deadlock.

🤖 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 `@src/ir/transforms/lower_composite_ops_pass.cpp` around lines 444 - 457,
Enforce EmitBarrier’s straight-line-only contract by tracking whether the
builder is nested in control flow: set a nested_ flag on builders created by
EmitFor/EmitIf, then add INTERNAL_CHECK_SPAN(!nested_, span) at the start of
EmitBarrier. Ensure top-level builders remain allowed while nested calls fail
during construction.
🤖 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 `@src/ir/transforms/lower_composite_ops_pass.cpp`:
- Around line 1831-1845: Extend CheckCollectiveLoopUse to reject tensor
collectives when either loop or conditional scope depth is nonzero. Track IfStmt
branch entry and exit with a conditional-scope depth counter in the mutator, and
include that counter in the existing CHECK_SPAN condition while preserving the
current loop diagnostic behavior.

---

Nitpick comments:
In `@src/ir/transforms/lower_composite_ops_pass.cpp`:
- Around line 444-457: Enforce EmitBarrier’s straight-line-only contract by
tracking whether the builder is nested in control flow: set a nested_ flag on
builders created by EmitFor/EmitIf, then add INTERNAL_CHECK_SPAN(!nested_, span)
at the start of EmitBarrier. Ensure top-level builders remain allowed while
nested calls fail during construction.
🪄 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: 84dbabc7-7cca-4f18-bb23-fdf980647950

📥 Commits

Reviewing files that changed from the base of the PR and between 00cacc2 and e6219ce.

📒 Files selected for processing (10)
  • docs/en/dev/distributed_ops.md
  • docs/en/dev/passes/12-lower_composite_ops.md
  • docs/en/dev/passes/38-synthesize_allreduce_signals.md
  • docs/zh-cn/dev/distributed_ops.md
  • docs/zh-cn/dev/passes/12-lower_composite_ops.md
  • docs/zh-cn/dev/passes/38-synthesize_allreduce_signals.md
  • python/pypto/language/distributed/op/tensor_ops.py
  • src/ir/transforms/lower_composite_ops_pass.cpp
  • tests/st/distributed/collectives/test_l3_tensor_collective_signal_reuse.py
  • tests/ut/ir/transforms/test_lower_composite_ops.py

Comment thread src/ir/transforms/lower_composite_ops_pass.cpp
@georgebisbas
georgebisbas force-pushed the fix/2156-reusable-collective-barrier-signals branch from e6219ce to e85c587 Compare July 28, 2026 09:13

@georgebisbas georgebisbas left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

All AI review feedback addressed in the latest push:

  1. Conditional scope tracking: Renamed repeating_scope_depth_ to scope_depth_, added VisitStmt_(const IfStmtPtr&) override. CheckCollectiveLoopUse now rejects collectives inside for/while/if/else.

  2. Nested barrier guard: Added nested_ flag to LoweringBuilder, nested constructor, and INTERNAL_CHECK_SPAN(!nested_) at the top of EmitBarrier(). All inner builders from EmitFor/EmitIf/EmitIfExpr are now marked nested.

  3. Clang-tidy fix: Removed unused #include "pypto/ir/program.h"

…ys#2156

Move pld.system.notify/wait docs before Signal lifetime for readability.
Add test_unrolled_loop_collective_gets_fresh_generations to verify the
pl.unroll escape hatch for the loop rejection added in the protocol.
Route operator names through ir.get_op() so typos raise at import.
@georgebisbas
georgebisbas force-pushed the fix/2156-reusable-collective-barrier-signals branch from e85c587 to 289046e Compare July 28, 2026 09:41
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.

Make composite collective barrier signals reusable

1 participant