Repository navigation
par: no heap allocation inside a parallel region - #126
Conversation
|
evo regression check, narrowed as asked (same procedure as #99). No regression at 8T; the cell #109 moved is unchanged against main. One mixed pair at 4T is the only thing outside +-5%. Setup. base
Machine state: the vendor row (same binary in both runs) differs by a median of 1.002 (4T) / 1.005 (8T) between its two process sets, max 1.033.
The one mixed pair, 4T Everything else: OpenBLAS alongside (median cand/vendor): |
`for_each_child` collected every item into a `Vec` and then a slot `Vec`; the full-pivoting LU and Bunch-Kaufman steps built a pivot-candidate vector per step; `join_sized` boxed its closures. At the floor, where #91 now runs narrowed per-step regions, these were a large share of a section. The producer is now split on the leader into the blocks and each block's leader iterates its piece (an inline array up to 64 blocks of small producers, a recursive halving otherwise); the pivot candidates come from the call's scratch (`lu_in_place_scratch`, `cholesky_in_place_scratch` grow by `par.degree()` entries); `join_sized` takes two typed slots. A counting- allocator test asserts zero allocations per call on teams of 4, 8 and 70. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
a96737b to
a4fe512
Compare
The
|
| arm | the process medians (ms) | median |
|---|---|---|
origin/main 7e5e9e3 |
25.26, 24.74, 25.58, 25.54, 25.72 | 25.54 |
PR a96737b |
25.49, 26.25, 26.30, 25.98, 25.94 | 25.98 |
ratio 0.983 (0.978 over the four clean sets — the fifth PR process was guard-rejected at 7.7 % busy
by two other agents' processes on CPUs 50/53; its own value, 25.94, is in line, so the conclusion does
not depend on it).
Reading
- The difference is still there and is now inside the overlap: main spans 24.74-25.72, the PR
spans 25.49-26.30, so 1.7 % nominal with both ranges touching. - Most of the apparent improvement since par: every region at the widest width that pays, on a sub-team of that width #109 is
origin/maindrifting, not this PR. Against
1ac38c6the pair was main 25.34 / PR 25.97 (+2.4 %); against7e5e9e3it is 25.54 / 25.98
(+1.7 %). The PR's own level did not move (25.97 -> 25.98); main moved up 0.8 %. - So on this cell par: no heap allocation inside a parallel region #126 is neutral, and the 2.4 % should be carried as an open question about
lu's
C-order row-major adaptation rather than attributed to par: every region at the widest width that pays, on a sub-team of that width #109 or closed by par: no heap allocation inside a parallel region #126. The per-step regions
this PR fixes (for_each_child's collection, the full-pivoting pivot candidates) are not what
dominateslu1024 at 8T; the PR body's own evidence isfull_piv_lu 128at 8T, a different cell.
If you want this settled rather than carried: a 4T/8T pair of lu 1024 C with 10 interleaved sets on
a quiet host, and a full_piv_lu 128 8T pair (the PR's own regression cell) would show whether #126's
win exists where it was claimed and whether this cell is a real 1-2 % cost.
Closes #111. Part of #95.
Problem
Every parallel region on the team route allocated on the heap:
utils::team::for_each_child(behind everyfor_eachandfor_each_with_par) collected all the iterator's items into aVec, one per column forpar_col_iter_mut, and then a slotVec.nthreads-long vector of pivot candidates per step.join_sizedboxed its two closures.These allocations were suspected of causing #109's floor-level loss (
full_piv_lu f64 128at 8T on evo, +6.6 to +10.4%). The evo re-measurement of this PR shows they were not: the cell is at 1.0025 ofmainwith them gone, and nothing else regressed. The change stands on its own as a cleanup of the mechanism; the monitor ruled the allocations a mechanism defect.The remaining candidates for #109's floor-level loss are recorded, not chased here: the fixed per-section costs of the 19 steps that narrow, namely the reduction merge and the trailing block's columns moving between cores as the width changes.
Change
for_each_childsplits the producer on the leader into the blocks (split_at, relative to the remainder, at thepar_split_indicesboundaries). Each block's leader takes its piece and iterates it there, soP::IntoIternever crosses threads. Everything runs inside the producer callback, so borrowing producers stay valid.Option<P>array (at most 16 KiB) and are dealt in one fork.lu_in_place_scratchandcholesky_in_place_scratchgrow bypar.degree()entries of(usize, usize, Real); in BK's they are in anorwith the blocked workspace. The buffer is passed down and reused by every step.join_sizedtakes two typed slots instead ofBox<dyn FnOnce>.split_at, such as rayon'smap_with, still does.Verification (primerose,
-j 4, 4 cores, rebased on0c806f1)faer/tests/no_step_alloc.rsis its own binary, with a process-wide counting#[global_allocator], gated onstdandthreadteam.ParThreshold::Fixed(0), with a warm-up first.join_sizedwith capturing closures. Each has 0 allocations in the counted call and dispatched a parallel section.origin/mainthe same test counts 190 allocations in one full-pivoting LU 96 call at width 4.for_each_child_deals_every_item_once: flat and recursive paths, a producer over the inline-size limit, counts that do not divide each other, exact item ownership, child widths within the even shares.for_each_child_unwinds_and_keeps_the_team.cargo test --lib534 passed before the rebases onto0c806f1and7e5e9e3(theutils,luandcholeskysubsets pass after it);--doc63 passed.cargo checkpasses for--no-default-features,--no-default-features --features std, andpar-trace.Performance (monitor, evo, 2026-10-29 07:54Z)
No regression.
full_piv_lu f64 128at 8T is at 1.0025 ofmain, so #109's +7 to 10% on that cell remains. Whether to chase it, and whether the cost model needs a per-region fixed-cost term, is decided later.🤖 Generated with Claude Code