perf(mpi): ♻️ lease alltoallv staging buffers instead of allocating per call - #233
perf(mpi): ♻️ lease alltoallv staging buffers instead of allocating per call#233diagonal-hamiltonian wants to merge 1 commit into
Conversation
…er call begin_alltoallv resized a fresh send buffer every call and copied every per-rank vector into it, and wait_into allocated each per-source block; both sit inside the exchange timer. Fitting exchange = A + B*bytes across two rungs with the collective count held constant isolates a fixed component A = 9.38 partition-seconds, 21.4% of a leg that is itself 68.2% of layer time, and per-call heap traffic is what that component is made of. The buffers now come from a per-thread free list. Ownership is the partition master, which is the per-partition scope: each partition runs its whole gate sequence on one pinned master, so capacity carries gate to gate and every buffer stays on the NUMA node whose thread first grew it -- the same reasoning as Scan.h's nz. The list is typed on the element type, so the query leg can never be handed a bundle belonging to the response leg. What makes reuse safe against Comm.h's "unmodified until the verb's second barrier" contract is the handle's lifetime, not a caller obligation: PendingAlltoallv is now move-only and completes its MPI request in the destructor before the bundle returns to the pool. Recycling under a live Ialltoallv is exactly the silent-wrong-answer failure this class of change invites, so it is closed structurally. Three reuse tests, all in the shape that actually fails -- sizes going large, small, large, so a logical size left at the high-water mark or a counts tail never rewritten shows up. Verified by mutation: dropping the recv_counts clear fails 12 cases, appending instead of assigning in wait_into fails 1. Not covered: recycling under a live MPI_Ialltoallv, since ShmComm never sets a request -- that path needs the multi-rank equivalence run. Assisted-by: ClaudeCode:claude-opus-5
|
Docs preview: https://pr-233.monoprop-docs.pages.dev |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## pr/layer-profile #233 +/- ##
=================================================
Coverage 97.70% 97.70%
=================================================
Files 14 14
Lines 742 742
Branches 98 98
=================================================
Hits 725 725
Misses 12 12
Partials 5 5
Flags with carried forward coverage won't be shown. Click here to find out more. |
|
|
🤖 AI text below 🤖 Closing this stack to rebuild it around a single-path design. Not abandoned — the work returns as fresh PRs once it is ready. Why. The stack accumulated runtime knobs as a way to A/B competing paths, and an A/B knob is not a shipping knob. Against
What the measurements decided. Several paths were being kept behind default-off knobs on the strength of stale or never-run comparisons:
What returns. The sparse query record together with the positions-based resolve (this PR's successor), the self-resolve simplification, and the layer profiler. Roughly 1,800 lines are deleted rather than added, and every PR in the new stack is a pure addition — the intermediate record format is never introduced, so nothing is added and then removed. Reopening as new PRs once the local work is verified. |



🤖 AI text below 🤖
Summary
begin_alltoallvresized a fresh send buffer every call and copied every per-rank vector into it, andwait_intoallocated each per-source block. Both sit inside the exchange timer, once per gate per partition.Fitting
exchange = A + B·bytesacross two problem sizes with the collective count held constant isolates a fixed component A = 9.38 partition-seconds — 21.4% of a leg that is itself 68.2% of layer time. Per-call heap traffic is what that component is made of.Changes
The buffers now come from a per-thread free list. Ownership is the partition master, which is the per-partition scope: each partition runs its whole gate sequence on one pinned master, so capacity carries gate to gate and every buffer stays on the NUMA node whose thread first grew it. The list is typed on the element type, so the query leg can never be handed a bundle belonging to the response leg.
What makes reuse safe is the handle's lifetime, not a caller obligation.
Comm.hrequires a buffer be unmodified until the verb's second barrier.PendingAlltoallvis now move-only and completes its MPI request in the destructor, before the bundle returns to the pool. Recycling a buffer under a liveIalltoallvis exactly the silent-wrong-answer failure this class of change invites, so it is closed structurally rather than by convention.Tests
Three reuse tests, all in the shape that actually fails — sizes going large, small, large, so a logical size left at the high-water mark or a counts tail never rewritten shows up. Verified by mutation: dropping the
recv_countsclear fails 12 cases; appending instead of assigning inwait_intofails 1.Not covered: recycling under a live
MPI_Ialltoallv, sinceShmCommnever sets a request. That path needs the multi-rank equivalence run, which is green at 1/2/4 ranks here but is not a targeted test of it.Checklist
docs/,CONTRIBUTING.md) if neededCHANGELOG/ release notes updated if applicable — release notes are generated from the PR titleAI/LLM disclosure