fix(distributed): close window-aliasing gap and O(N²) pass complexity in HOST collectives - #2246
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughChangesCollective validation and lowering
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant CollectiveOp
participant HostCollectiveLowering
participant CommunicationDomainScopes
CollectiveOp->>HostCollectiveLowering: collective buffers and signal
HostCollectiveLowering->>HostCollectiveLowering: validate signal, root, and window aliases
HostCollectiveLowering->>CommunicationDomainScopes: resolve communication scope
CommunicationDomainScopes-->>HostCollectiveLowering: materialized scope and buffer slots
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
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. Comment |
There was a problem hiding this comment.
Pull request overview
Improves HOST collective safety and pass scalability.
Changes:
- Adds generic window-alias rejection and broadcast root validation.
- Unifies most collective signal-shape checks.
- Replaces quadratic domain clustering lookup with a map.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
CMakeLists.txt |
Registers alias utility source. |
include/pypto/ir/transforms/utils/window_alias_utils.h |
Declares window alias validation. |
src/ir/transforms/utils/window_alias_utils.cpp |
Implements pairwise checks. |
src/ir/transforms/materialize_comm_domain_scopes_pass.cpp |
Adds indexed domain lookup. |
src/ir/transforms/lower_host_tensor_collectives_pass.cpp |
Applies alias and root checks. |
src/ir/op/distributed/collective.cpp |
Centralizes most signal validation. |
tests/ut/ir/transforms/test_materialize_comm_domain_scopes.py |
Tests three-domain ordering. |
tests/ut/ir/transforms/test_lower_host_tensor_collectives.py |
Tests alias and root rejection. |
tests/ut/ir/test_distributed_ops.py |
Tests signal-rank validation. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: daf98aa373
ℹ️ 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".
- Route pld.tensor.allreduce's public deducer (allreduce.cpp) through a shared signal-shape check too, closing the same public/builtin mismatch this PR fixed for the other 5 collectives (Copilot + Codex, duplicate). Split the shared helper in two: CheckSignalDistributedTensorShape (dtype + rank-1-or-2 only) for allreduce, since its ring-mode signal is a genuine [2*(NR-1), NR] matrix (second dim = NR, not 1) that the stricter CheckSignalDistributedTensor (dim1 must be 1) would have wrongly rejected — caught by the ring_allreduce test suite while fixing this. - Extend the 3-domain MaterializeCommDomainScopes regression test with a 4th allocation reusing an existing (non-zero-index) descriptor, so the map lookup's "found" branch is actually exercised, not just "append" (Copilot). - Add signal-aliasing tests for allgather/all_to_all (input-vs-signal, target-vs-signal), not just the pre-existing input-vs-target case (Copilot).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.
Suppressed comments (4)
src/ir/op/distributed/allreduce.cpp:105
- This still accepts a non-unit second dimension for the default
mode="mesh", not just for ring mode. The new “ring mode” unit test callsallreducewithoutmode="ring", andLowerTensorAllReduceRuleperforms no mesh signal-shape check, so[NR, 2]passes the public mesh path while the HOST builtin rejects it. Branch on themodekwarg here: useCheckSignalDistributedTensorfor mesh and the shape-only helper for ring, then make the non-unit test explicitly select ring and add a mesh rejection case.
CheckSignalDistributedTensorShape(As<DistributedTensorType>(args[1]->GetType()), "pld.tensor.allreduce");
src/ir/transforms/lower_host_tensor_collectives_pass.cpp:109
- This introduces a new public validation rule, but the public
broadcastdocstring still says only thatroot“Must be non-negative” (python/pypto/language/distributed/op/tensor_ops.py:688). Document that an explicit static comm domain also requiresroot < participating device count, while all-device domains cannot be upper-bound checked at compile time.
void CheckBroadcastRootInRange(const CallPtr& call, size_t participating_devices) {
auto root_value = call->GetKwarg<int>("root");
CHECK_SPAN(root_value >= 0 && static_cast<size_t>(root_value) < participating_devices, call->span_)
<< "pld.tensor.broadcast root (" << root_value << ") must be a valid rank in [0, "
<< participating_devices << ") for this explicit device subset";
src/ir/op/distributed/signal_utils.h:26
- This overview contradicts the helpers below: ring allreduce deliberately accepts
[2*(NR-1), NR], so not every collective accepts only the same two shapes and the public/builtin validators can differ by mode. Describe the mesh/single-column contract separately from allreduce’s ring exception.
* cross-rank barrier slot. All of them must accept the same two shapes
* (rank-1 [world_size] or rank-2 [world_size, 1]) — this lives in one place,
* shared across translation units (allreduce.cpp, collective.cpp), so the
* public op and its builtin can never disagree on what a signal is allowed
src/ir/op/distributed/collective.cpp:131
- The added rank-2 acceptance tests stop at these public deducers;
[NR, 1]already passed here before this PR because the old code checked only dtype. No test sends a rank-2 signal through HOST lowering to exercise the changed builtin validators—the exact late failure this PR intends to eliminate. Add HOST-lowering success tests with[NR, 1]for barrier, broadcast, and reduce-scatter.
CheckSignalDistributedTensor(As<DistributedTensorType>(args[0]->GetType()), "pld.tensor.barrier");
- Branch allreduce public signal validation on mode: mesh uses the strict CheckSignalDistributedTensor (dim1==1); ring keeps the shape-only helper. - Document broadcast root upper-bound check and clarify mesh vs ring signal contracts in signal_utils.h. - Add mesh rejection + mode="ring" tests; add HOST-lowering [NR,1] success paths for barrier/broadcast/reduce_scatter.
|
Addressed Copilot's second-pass suppressed findings in 5cefb5f:
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (4)
src/ir/op/distributed/allreduce.cpp:115
- Every value other than
"ring"is routed through the mesh validator. For example,mode="typo"with a ring-shaped[2, 2]signal now reportsshape[1] must be 1, so the intended invalid-mode diagnostic inLowerCompositeOpsis never reached. Validatemodebefore selecting its signal contract (including the no-signal form).
if (mode == "ring") {
CheckSignalDistributedTensorShape(signal_type, "pld.tensor.allreduce");
} else {
CheckSignalDistributedTensor(signal_type, "pld.tensor.allreduce");
src/ir/transforms/lower_host_tensor_collectives_pass.cpp:341
- The pass documentation is now stale:
docs/en/dev/passes/40-lower_host_tensor_collectives.mdstill documents onlystage/datadistinctness and signal capacity, omitting both the new pairwise-alias rejection and this explicit-subset root bound. Update the English ground-truth document and its Chinese mirror to match the new user-facing checks.
CheckStaticSignalCapacity(call, rule.signal_expr(call), scope->devices_.size());
if (rule.extra_static_check) rule.extra_static_check(call, scope->devices_.size());
tests/ut/ir/transforms/test_materialize_comm_domain_scopes.py:1167
- The test now creates four allocations after adding
buf_d, so thethree_allocsname no longer describes its setup. Rename it to reflect the four allocations and three descriptors/groups.
def test_three_allocs_different_descriptors_three_groups():
src/ir/op/distributed/signal_utils.h:75
- This changes the accepted public signal contract for
barrier,broadcast, andreduce_scatter, but their Python API docs still only describe an INT32 barrier tensor and do not state the enforced rank-1[NR]or rank-2[NR, 1]shapes. Document the new contract so users can understand and fix these newly surfaced validation errors.
inline void CheckSignalDistributedTensor(const DistributedTensorTypePtr& signal_type,
const std::string& op_name) {
CheckSignalDistributedTensorShape(signal_type, op_name);
if (signal_type->shape_.size() == 2) {
5cefb5f to
10ea2d2
Compare
…ST collectives - Generalize window-aliasing checks to all 6 HOST collectives via a shared CheckPairwiseDistinctWindows helper, reusing HostCollectiveRule::scope_buffers (which already enumerates every op's window operands). Previously only allgather/all_to_all checked input-vs-target; allreduce/barrier/broadcast/ reduce_scatter had no aliasing check at all, and none checked vs signal. - Fix an O(N^2) linear scan in MaterializeCommDomainScopes' domain-clustering phase (pass-complexity.md violation) by indexing pending domains with std::map<DeviceDescriptor, size_t> instead of scanning per allocation. - Unify signal rank/shape validation into one CheckSignalDistributedTensor helper (rank-1-or-2), used by every collective's public op and builtin deducer. Previously barrier/broadcast/reduce_scatter's public ops had no rank check at all while their builtins required rank-1 only, so a rank-2 signal (a pattern copied from allreduce/allgather) would type-check at the call site and fail 3 passes later with a confusing error. - Add a static-domain root-upper-bound check for broadcast (root >= 0 was checked, but never root < device_count), mirroring the same optional-callback pattern used for signal-capacity checks.
10ea2d2 to
e909cd4
Compare
A deep review of the distributed collectives stack (allreduce, barrier, broadcast, reduce_scatter, allgather, all_to_all) surfaced one real correctness gap and one real complexity-budget violation, both silent today. This PR fixes both, plus the consistency issue that caused the first one.
1. Window aliasing was unchecked for most HOST collectives — a real silent-corruption risk
Before this PR,
LowerHostTensorCollectives'sCheckDistinctInputTargetWindowswas wired into only 2 of 6 collectives (allgather,all_to_all), and even there it only checkedinputvstarget— never vssignal.allreduce,barrier,broadcast,reduce_scatterhad no aliasing check at all.Concretely: nothing stopped a user from writing
where
dataandsignalare two differentpld.window()views over the samealloc_window_buffer. That's a real cross-process race — the reduce's data write and the barrier's notify/wait control write land in the same memory — and it would compile cleanly and fail silently or corrupt data at runtime instead of raising a clear compile-time error.Fix:
HostCollectiveRule::scope_buffersalready enumerates every window operand for every op (it existed to resolve the enclosingCommDomainScopeStmt) — this PR reuses that same list to run a new, genericCheckPairwiseDistinctWindowshelper once, inLowerCollective, covering all 6 collectives uniformly. One shared 15-line helper instead of one-off per-op checks that get missed (this is exactly the bug class a recent all_to_all_v PR needed 3 rounds of review to close for just that one op — this generalizes the fix so it doesn't need to be rediscovered per-collective).2. O(N²) pass complexity — a direct violation of this repo's own
pass-complexity.mdMaterializeCommDomainScopes's domain-clustering phase did a linear scan of thependingdomains list per allocation to find a matching descriptor. For a program with many independently-scoped comm domains, this is O(allocations × domains) = O(N²).Fix:
DeviceDescriptoralready hasoperator<, so this PR adds astd::map<DeviceDescriptor, size_t>index alongsidepending, turning the lookup into O(log domains) — squarely within the project's own "ordered map/set lookups are fine" complexity allowance. No behavior change; a new 3-domain regression test pins the clustering order.3. Signal validation was inconsistent between the public op and its HOST builtin (root cause of gap #1's blind spots)
pld.tensor.barrier/broadcast/reduce_scatter's public deducers checked signal dtype only (no rank/shape check), while their builtin counterparts required rank-1 only — andallreduce/allgather/all_to_alleach separately inlined their own near-identical "rank-1-or-2" check. Net effect: a rank-2[NR, 1]signal (the pattern used by allreduce/allgather) would type-check fine for barrier/broadcast/reduce_scatter, then fail 3 passes later insideLowerHostTensorCollectiveswith a confusing, late error.Fix: one
CheckSignalDistributedTensorhelper (rank-1-or-2, INT32), called from all 12 signal-validation sites — every public op and every builtin. Removes ~90 lines of duplicated inline validation and makes this class of public/builtin mismatch structurally impossible going forward.4.
broadcast'srootwas never bounds-checked against the device countOnly
root >= 0was validated.root=100on a 4-rank job compiled cleanly and would fail at runtime as an out-of-bounds remote read. This PR adds aroot < participating_devicescheck on the explicit static-device-subset domain (mirroring the existing signal-capacity check's scope — the fully-dynamic "all device" domain is left as a documented limitation, same as the existing signal-capacity gap there, since there's no compile-time device count to check against in that case).What's intentionally not in this PR
Per a broader review (docs/discussion, not code), several other findings were evaluated and deliberately deferred — a
CollectiveOpBuilderabstraction (premature without an 8th collective), splittingcollective.cppinto per-op files (conflicts with the in-flight #2243), a runtime-assert IR primitive for the dynamic-domain gaps mentioned above (real gap, but new IR/runtime infra, out of scope here). Happy to open tracking issues for these if useful.Test plan
pypto3-hw-native-sys:simDocker image.clang-format --dry-run --Werrorandclang-tidyclean on all touched/new C++ files.ruff check/ruff format --checkclean on all touched Python test files.