perf(mpi): remove the serial O(R·S²) protocol floor from HybridComm - #166
Draft
diagonal-hamiltonian wants to merge 3 commits into
Draft
perf(mpi): remove the serial O(R·S²) protocol floor from HybridComm#166diagonal-hamiltonian wants to merge 3 commits into
diagonal-hamiltonian wants to merge 3 commits into
Conversation
|
Docs preview: https://pr-166.monoprop-docs.pages.dev |
diagonal-hamiltonian
force-pushed
the
perf/multinode-comm-scaling
branch
from
July 29, 2026 08:29
ca71897 to
42de519
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #166 +/- ##
==========================================
- Coverage 84.81% 83.36% -1.46%
==========================================
Files 59 60 +1
Lines 4202 4333 +131
Branches 1497 1552 +55
==========================================
+ Hits 3564 3612 +48
- Misses 248 310 +62
- Partials 390 411 +21
Flags with carried forward coverage won't be shown. Click here to find out more. |
diagonal-hamiltonian
force-pushed
the
perf/multinode-comm-scaling
branch
from
July 29, 2026 08:40
42de519 to
55ca2f3
Compare
…ives monoprop_COMM_PROFILE=1 accumulates, per partition and per cache-line-isolated slot, where a collective's wall time goes: table_p0 (fills one partition runs alone) vs table_par (fills every partition runs on its own slice) vs table_move (payload memcpy) vs mpi vs barrier wait, plus the barrier's L3-domain group count. Off by default and never allocated then, so the hot path pays one null check per instrumented region. The splits are the ones that decide protocol questions. table_p0 vs table_par separates a serial protocol from a parallel one, and attributing barrier wait per partition is what makes the asymmetry visible -- the master's own wait stays small precisely when the master is the bottleneck. table_par vs table_move separates bookkeeping, which a better protocol shrinks, from data movement, which it cannot. Assisted-by: ClaudeCode:claude-opus-5
A partitioned multi-node run was protocol-bound, not network-bound: every collective rebuilt O(R*S^2) integer offset tables and partition 0 filled all of them alone while the other S-1 partitions spun at a barrier. Measured with monoprop_COMM_PROFILE=1 (2 nodes, 512-site Hubbard, 4 layers, R=2, S=112), partition 0's serial fill was 22.33 s of a 26.74 s run, while MPI itself was 1.58 s -- the wire was never the problem. Four changes, wall 26.74 -> 4.46 s (6.0x), expectation value bit-identical at every step and at every rank x partition split: - Parallel prefix. The offset of block (rank b, dest t, source u) splits into a per-(b,t) base needing global knowledge, which is only O(R*S) and stays on partition 0, and a scan over u that partition t owns outright. The count matrix is relaid source-partition-major so each partition writes a contiguous run; dest-major would put S partitions on every cache line and trade a serial fill for pure false sharing. - alltoallv_reverse. The answer leg travels the query exchange's legs backwards, so its geometry is the query round's, and rebuilding it costs 3*R*S^2 entries and a barrier for nothing. It is a ratio and not an identity: the query leg carries Sink::kStride elements per record and the answer leg one, so every reused offset and per-rank count is divided by the stride (exact -- every forward count is a multiple of it). Reusing them undivided would still deliver correct data while staging and transmitting kStride times the bytes. - Empty-block veto. Each partition publishes a cache-line-padded bitmask of the ranks it sends anything to; the sizing phase then runs source-partition-outer, so a partition that sends nothing costs one load instead of R strided probes into its count array. At early layers nearly every block is empty. - Two-level barrier. PartitionBarrier fans in within an L3 domain and then across domains, so both the arrival fetch_add and the release-store invalidation cost O(S/G) coherence transactions inside one L3 slice instead of O(S) across the socket interconnect. Domains are derived from the partition cpusets rather than from the placement logic, so the two cannot drift apart, and a rank spanning one domain keeps the flat barrier (a root barrier of one is pure overhead). Tests: the pre-existing hybrid_comm cases all sent the same count to every destination per source, so a transposed (b,t,u) index passed unnoticed -- two asymmetric-count cases close that. partition_barrier_tests.cpp tests the two-level path directly, because engaging it through a real comm needs pinning and >=2 L3 domains, which no test host can be relied on to have. Both additions are mutation-verified: an inconsistent receiver index and a skipped root barrier each fail the new cases while the old ones pass. Assisted-by: ClaudeCode:claude-opus-5
… barrier Both are new behaviour from the HybridComm floor work: a runtime knob belongs in the environment-variable table, and the barrier's L3-domain grouping is a performance property a reader tuning partition placement needs to know about. Assisted-by: ClaudeCode:claude-opus-5
diagonal-hamiltonian
force-pushed
the
perf/multinode-comm-scaling
branch
from
July 29, 2026 08:54
55ca2f3 to
69a7932
Compare
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



What
A partitioned multi-node run was protocol-bound, not network-bound. Every
HybridCommcollective rebuilt
O(R·S²)integer offset tables, and partition 0 filled all of themalone while the other
S−1partitions spun at a barrier. Four changes remove that serialphase; the fifth commit adds the opt-in instrumentation that found it.
Measured with
monoprop_COMM_PROFILE=1(2 nodes, 512-site Hubbard, 4 layers,R=2, S=112),partition 0's serial fill was 22.33 s of a 26.74 s run while MPI itself was 1.58 s — the
wire was never the problem.
(b,t)base needing global knowledge (onlyO(R·S), stays on partition 0) and a scan overuthat partitiontowns outright. The count matrix is relaid source-partition-major so each partition writes a contiguous run — dest-major would trade a serial fill for pure false sharing.HybridComm.halltoallv_reverse. The answer leg travels the query exchange's legs backwards, so its geometry is the query round's; rebuilding it cost3·R·S²entries and a barrier for nothing. A ratio, not an identity — the query leg carriesSink::kStrideelements per record and the answer leg one, so every reused offset and count is divided by the stride (exact: every forward count is a multiple of it).HybridComm.h,MPICompat.h,Engine.hRstrided probes. At early layers nearly every block is empty.HybridComm.hPartitionBarrierfans in within an L3 domain then across domains, so the arrivalfetch_addand the release store costO(S/G)coherence transactions inside one L3 slice instead ofO(S)across the socket interconnect.PartitionBarrier.h,CpuTopology.h,PartitionGroup.hCorrectness gate
These changes move no floating-point value — only integer offsets and barrier topology —
so the acceptance criterion is a bit-identical expectation value, not a tolerance. That
held at every step of the ladder and at every rank × partition split.
ctest, plus the comm suite undermpiexec -n 3and-n 4(SandRinteractin the new index arithmetic, so world 2 alone is not enough).
hybrid_commcase sent the samecount to every destination per source, which cannot distinguish the
(rank, dest partition, source partition)index order from its transpose — exactly theindex change 1 makes. Added two asymmetric-count cases, mutation-verified: an inconsistent
receiver index makes the old cases pass and both new ones fail. The two-level barrier gets
6 direct cases (uneven/non-contiguous/singleton domains, poison, reset), because its
grouped path needs pinning and ≥2 L3 domains to engage through a real
ShmComm, which notest host is guaranteed to provide.
A/B
Leonardo DCGP, 2 nodes,
--exclusive, one allocation for both sides run interleaved (aper-allocation slowdown would otherwise land on one side and read as an effect). Two venvs built
by the same script from detached worktrees at
origin/mainand this branch, sobenches/isidentical and only
src/differs. Driver: the in-repo Hubbard model (60 sites / 120 qubits) viabenches/_builders, barriered per layer, expectation value compared asrepr— the gate isbit-identity, not a tolerance.
c1 — 20 layers,
atol1e-4, 1,063,245 terms (small operator: cost set by the per-gate sync count)c2 — 29 layers,
atol1e-6, 260,928,282 terms (large operator: real work dominates)Term counts are identical everywhere, and the expectation value is bit-identical between the two
sides at every layout. The two layouts differ from each other in the last bit — the partition
count sets the reduction order — which is pre-existing behaviour these changes do not alter.
What this actually buys, stated plainly:
main, putting one partition per physicalcore in a single rank — the natural configuration, and the default — costs 4.5× (c1) or
1.8× (c2) against splitting the same 112 cores into 4 ranks. With this PR those layouts land
within 1.36× and 1.00×. That is the
O(R·S²)term draining out, and it is the result to read.Layer 1 at
1×112is 9–10× cheaper in both configs — it holds a tiny operator, so it measuresthe floor almost directly. Once the operator is 261 M terms and the layout is already
4×28,the floor is not what you are paying for and the change is a wash.
4×28has layer 1 going 0.553 → 0.633 s (+14 %, singlereplicate). Expected in kind — fix 1 adds two barriers per verb, and at
S=28 there is littleserial fill to reclaim. It is why fixes 2–4 are load-bearing rather than optional.
O(R·S²); this A/B is the smallestinteresting case. On the 8-node / 1024-qubit problem this work was written for, the same four
changes took the 29-layer run from 541 → 192 s at
1×112and 163 → 146 s at4×28, with layer1 going 22.7 → 2.4 s. Those numbers come from a separate harness that is not in this PR, so
treat them as context, not as this diff's verification.
A/B binaries built from
ca71897; the pushed tip differs from it only by clang-formatwhitespace in four files.
Notes for the reviewer
mpiis now the largest bucket, ~65k collectives at 25–100 µs, i.e. call latency, notbandwidth. Nothing in the table or barrier layer touches it; the only remaining lever is
fewer collectives per gate (batching commuting gates), which is deliberately out of scope
here — it touches the evolution scan/fold.
Change 4 engages only where a rank spans more than one L3 domain, which the profile's
barrier_groups=field reports rather than leaving to be inferred from timing.monoprop_COMM_PROFILEis off by default and allocates nothing then; the hot path pays onenull check per instrumented region.
Static analysis. Sonar's reliability gate caught one real defect, now fixed: both new
transport destructors call
CommProfile::dump(), whosestd::printcan throw, and a throwingdestructor running during unwinding terminates the process.
dump()is nownoexceptandswallows internally — a diagnostic print must never take the job down. Three families of
finding are left deliberately unaddressed, and I'd rather say so than quietly churn the diff:
the
memory_orderrule wantsseq_cston the barrier's atomics, which would undo fix 4 (andfires on the pre-existing flat barrier on
maintoo); thevoid *payload parameters and the[&]capture inalltoallv_reversemirror the sibling verbs in the same file, where divergingwould be worse than complying; and the
HybridCommfield/method counts are pre-existing classsize that this PR is not the place to refactor.
One stateful contract is introduced:
alltoallv_reversemust directly follow thealltoallv_resolvewhose layout it reverses. Guarded by a generation counter that throwsrather than silently reading stale tables, and documented on the verb.
🤖 Generated with Claude Code