Repository navigation
placement: opt-in member pinning, PAUSE-only reserved spin, oversubscribed park (#9) - #10
Merged
Merged
Conversation
…subscribed park (#9) Implements the decided design for #9 on top of the broccoli A/B/C measurement in tensor4all/faer#107 (pinning removes the 3x process mode: 0 of 20 slow with the participants pinned, 11 of 20 unpinned, 7 of 20 with one spare CPU). A. Opt-in placement. `PoolBuilder::pin_participants(bool)` (one private `PIN_PARTICIPANTS_DEFAULT: bool = false` switch), `Pool::pin_participants()` getter, `src/pin.rs`. At the start of a root team scope (`Exec::team_with` on the acquisition path and on the carried path of a root token minted by a `Team`) the leader reads its own mask S and runs one pin section through the section machinery (it replaces `acknowledge`): member k (participant k >= 1) pins itself to the k-th set CPU of S, wrapping modulo |S|. **The leader is never pinned** and its mask is never changed, not even for the scope: a vendor library builds its thread team from the thread that calls it, and a one-CPU caller silently confines the whole provider team (tenferro-rs PERFORMANCE_TIPS.md: 3-20x slower dgemm/LU), so S[0] is left to the leader without enforcing it; a pinned member must not build such a team directly inside a share (use `Exec::external`, which restores its mask first, or keep pinning off). Only the thread being moved changes its affinity, with `sched_setaffinity(0, ...)`. A member saves its previous mask in its own slot (UnsafeCell<cpu_set_t> + a thread-owned flag, SAFETY comment) the first time it is pinned and restores it itself in `member_loop` before it leaves `RESERVED`, before it serves Rayon while lent, and (`sched::unpin_worker`) when it is itself the thread that runs foreign work, e.g. the member that leads a `fork` child and calls `external`. Best effort everywhere; Linux only (accepted and inert elsewhere); `libc` is now also a Linux dependency (no libc type is public); stack `cpu_set_t` only, so the path allocates nothing. The pin section is tagged by a bool on `Section`; its closure captures the mask. B. Reserved spin without yield: `spin_wait` (one PAUSE per poll) replaces the shared `backoff` in the reserved `gen` poll and in `Section::join`/ `Section::barrier` when the scope is not oversubscribed. `backoff` stays for acquisition, the cold claim, `IDLE` and the leaving states. C. Oversubscription: a root scope whose participants exceed |S| (Linux affinity count, else `available_parallelism`) sets the members' park delay to zero for its reservation and resets `polls`/`spins` per idle episode (64 polls before the park check); `join`/`barrier` spin ~100 times then yield per poll. Root pressure and the park delay are decided on the thread that enters the scope — at `Exec::team_with` on both paths and, for a `Team`, also at `Team::exec`, so a token minted on one CPU set and entered on another is paced and placed by the entering thread's set. The flag is carried through `TeamAuth`, `TeamExec`, reborrow/limit/split/fork children and every `Section`. D. `src/park.rs`, the handshake and publication are untouched; the loom models pass unchanged. Tests: `tests/pinning.rs` (member placement on distinct CPUs, wrap-around on a 2-CPU caller mask, the leader's mask identical before, inside and after the scope for an acquisition and for a persistent-team root token and after a panic in a share, persistent members across two scopes, pinning off, restoration while lent by `external`, and a member-led `fork` child calling `external`), `tests/oversubscribed.rs` (a 2-CPU mask and a 1-CPU scope entry, both with a policy park delay far above the hang guard, so parking at all proves the scope's zero delay; nested split included), `src/tests/placement.rs` (flag propagation, entry-time pressure), and pinning-on allocation windows in `tests/alloc_free.rs` and `tests/team_alloc_free.rs`. `tests/idle_clock.rs` now checks the order of each member's own trace events instead of a wall-clock premise ("sections every 5 ms" breaks on a slow runner once members no longer yield): never a park less than the delay after the member's own last share, wake-up or start; every member parks once no section arrives; a following section wakes every parked member. Docs: `README.md`, `docs/design/placement.md` (new "Participant placement (opt-in)" and "How a reserved member and a joining leader wait" sections). Co-Authored-By: Claude Opus 5.0 <noreply@anthropic.com>
shinaoka
force-pushed
the
feat/9-placement
branch
from
October 29, 2026 04:51
fca30ad to
d8b4c5c
Compare
Member
Author
Broccoli re-check of the placement fix: 20 processes per arm, 16T on CPUs 48-63
Reading
Recommendation from this side: the pinning is a net win but it is arm 2 that makes the mode Cross-reference: the same numbers are in tensor4all/faer#107. |
This was referenced Oct 29, 2026
Merged
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.
Part of #9.
tensor4all/faer#107's A/B/C run on broccoli (03:36Z) identified the slow mode: two team threads stacked on one CPU.
This PR adds the pinning, opt-in, plus the waiting changes requested with it.
Changes
PoolBuilder::pin_participants(bool)/Pool::pin_participants(). The default is off. The privatePIN_PARTICIPANTS_DEFAULTinsrc/pool.rsis the one-line switch, waiting for the maintainer's decision.sched_getaffinity). Member k (k >= 1) pins itself to the k-th CPU of S, wrapping modulo |S|. Root scopes areExec::teamacquisitions and the root token minted by a persistentTeam, which is faer'swith_teampath.PERFORMANCE_TIPS.md: "Do not assign one CPU per worker", measured 3-20x slower dgemm/LU at 4T). S[0] is left to the leader without being enforced. Arm C of faer#107 pinned the leader too, so the broccoli re-check with this PR must confirm that member-only pinning removes the stacked mode.external(including as the leader of a fork child).Exec::external, or to keeping pinning off.cpu_set_tand no allocation. Other OSes accept the option and do nothing.join/barrier, spin with onespin_loop()per poll (PAUSE on x86, YIELD/ISB on aarch64).backoff(spin, thenyield_now) stays on acquisition and the idle paths. Members still park after the park delay through the unchangedParker.join/barrierspin about 100 times, thenyield_now.src/park.rs) and section publication; the loom models pass as they were.Tests
tests/pinning.rs:{S[k % 2]}and the leader keeps the full set);external.tests/oversubscribed.rs: members of an oversubscribed root scope park (nested split included), and the thread that enters the scope, not the mint, decides the park delay. Both use a 60 s park delay and wait for the parks, so reaching them proves the zero delay with no wall-clock bound.tests/idle_clock.rsis now order-based. It failed on macOS CI because its wall-clock premise ("a section every 5 ms") broke on a 3-core runner. For everyParkEnter, it checks that the time since the member's own lastSectionEnd/ParkExit/Enteris at least the delay; it then waits for every member to park after the last section, and checks that a section wakes them all.src/tests/placement.rs: the flag survives reborrows, splits and fork children; a fitting scope is not oversubscribed; the scope takes its pressure from the entering thread.tests/team_alloc_free.rs: mint, use and drop of a persistent-team token with pinning on make 0 allocations.tests/alloc_free.rs: anExec::teamscope allocates the same with pinning on as with pinning off. It already allocated its idVecbefore this change.Verification (primerose)
The AGENTS.md pre-push checklist passes:
cargo fmt --checkcargo clippy --all-targets, with--all-featuresand with--no-default-features, both-D warningscargo test: 171 passed (default), 185 (--all-features, three runs), 99 (--no-default-features)idle_clockandoversubscribedundertaskset -c 0-2: ten runs, all passingloom_park: 4 passedcargo deny check bansscripts/check-no-public-rayon.shcargo doccargo check --all-targets --target aarch64-apple-darwinIndependent pre- and post-implementation reviews (gpt-6.1-sol) were done. The pre-review's two BLOCKERs (pinning other threads by tid, and restoring through a lent job) led to the self-pinning design. The post-review's two IMPORTANT findings are fixed here: foreign work on a pinned fork-child leader, and oversubscription taken at mint instead of at scope entry. The leader is unpinned per the monitor's constraint (tenferro's vendor-team rule).
Acceptance in #9 that is still open:
No version bump: publishing is not part of this PR.
🤖 Generated with Claude Code