Repository navigation
par: every region at the widest width that pays, on a sub-team of that width - #109
Conversation
…t width (#91) The parallel gates chose between the token's whole degree and one thread, so on a wide team a region that pays at 8 or 16 ran serially. Every region now asks one rule for its width (`utils::cost::par_width`, `ParThreshold::region_width`: the widest width that pays, every width searched) and runs on a sub-team of that width (`utils::thread::with_width`, a one-child split with no worker dispatch). Capped splits search inside their cap. The per-step factorizations decide per step instead of latching `Par::Seq`; `Never` still makes the call sequential and `Fixed` keeps its rules. `par_pays`, `region_pays` and the Seq-only GEMM predicate are removed; `par-trace` records the chosen width and `utils::team::stats` counts the narrowed regions by width. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
evo regression gate, same procedure as #99. No regression beyond the noise floor except two of the smallest cells (+6 to +10%); one baseline pathology is gone. Setup. base
Machine state: the vendor row is the same binary in both runs; its two process sets differ by a median of 1.004 (max 1.069) per cell. What moved
OpenBLAS alongside (median of cand/vendor over the cells both arms share): If the two small-cell regressions matter, the place to look is the sub-team setup for regions below the old width; the rest of the arm is clean. |
Where the two small-cell regressions in the evo gate come from (04:33Z comment)I compared the gate decisions and team dispatches of the two cells on
|
| gate decisions | steps at width 8 | steps narrowed to 2-7 | serial steps | |
|---|---|---|---|---|
main 1ac38c6 |
43 | 42 | 0 | 85 (latched after the first refusal) |
| this PR | 127 | 42 | 19 (2:3, 3:3, 4:5, 5:5, 6:1, 7:2) | 66 |
The 19 extra parallel steps are what the rule says to take. Each one sits at the floor: the estimate is 1.8-2.9 us against 3 x D(w) of 1.8-2.8 us (D(w) = 0.60-0.94 us on the avx512 row). Main never asked about them because of the latch.
The mechanism's own costs, measured on primerose:
- Width search:
par_width_time, a cheap rejection then a descending scan, costs 20-55 ns. - Sub-team:
with_widthto a narrower width costs 31 ns. That is the one-child split, with no worker dispatch.
That gives at most 84 × ~40 ns + 19 × 31 ns ≈ 4 us against the measured 20-30 us. So the cost is not in the search or the split. It comes from the 19 floor-level steps losing on evo where the model says they gain about 1.2 us each.
The table's D(w) is the steady-state cost of an empty section. A full-pivoting step section also pays costs that the table does not contain:
- three allocations per step: the
bestvector, plus the item and slot vectors offor_each_child; - the reduction merge;
- the trailing block's columns moving between cores when the width changes from step to step.
At the floor, those dominate. The rule and its narrowing work as designed. Fixing this would mean changing the margin or the section count of this one region, which is host-constant tuning, so it is left as a residual. It is recorded in the PR body.
cholesky f64 128, team of 4
Main and this PR are identical:
- the same 2 gate decisions (the triangular join, width 2);
- the same dispatches:
run [4:2], 2 kernel GEMMs and 2 below the floor; - no region narrowed.
The llt path reaches none of the converted per-step or split regions at this size. The +5 us therefore does not come from a decision or a dispatch of this change. A layout or code-size effect is the remaining candidate; I have no mechanism to point at. It is also recorded as a residual.
Possible follow-ups (not in this PR)
- The 16-20 ns cheap rejection copies
DispatchCosts, throughdispatch_costs().min_cost(). A cached minimum would remove that. - A per-region fixed-overhead term in the cost model, so that floor-level steps of the per-step factorizations account for their allocations and merge.
Both are outside #91's scope. The broccoli high-thread results are still pending.
High-thread check on broccoli:
|
| cell | T | main | PR | main/PR | OpenBLAS |
|---|---|---|---|---|---|
| gemm 256x4096x256 | 1 | 21.11 | 21.06 | 1.002 | 20.59 |
| 8 | 3.417 | 3.410 | 1.002 | 4.371 | |
| 16 | 2.168 | 2.155 | 1.006 | 2.407 | |
| 32 | 1.632 | 1.648 | 0.990 | 1.862 | |
| 64 | 1.093 | 1.082 | 1.011 | 1.800 | |
| qr_factor 1024 | 1 | 72.49 | 73.23 | 0.990 | 139.08 |
| 8 | 18.42 | 18.34 | 1.004 | 91.64 | |
| 16 | 14.73 | 14.76 | 0.998 | 80.72 | |
| 32 | 13.04 | 13.23 | 0.986 | 80.71 | |
| 64 | 12.95 | 13.56 | 0.955 | 78.32 | |
| lu 1024 | 1 | 43.11 | 44.21 | 0.975 | 62.90 |
| 8 | 24.13 | 26.14 | 0.923 | 41.30 | |
| 16 | 24.33 | 24.53 | 0.992 | 40.42 | |
| 32 | 24.59 | 24.22 | 1.015 | 41.76 | |
| 64 | 25.52 | 38.64 | (0.660, unresolvable) | 54.01 | |
| householder_apply 4096x256 | 1 | 49.74 | 48.70 | 1.021 | skipped |
| 8 | 10.71 | 10.53 | 1.017 | skipped | |
| 16 | 6.165 | 6.386 | 0.965 | skipped | |
| 32 | 4.946 | 4.738 | 1.044 | skipped | |
| 64 | 3.942 | 3.846 | 1.025 | skipped |
Reading
- No cell improves, and nothing moves beyond this host's resolution except two small regressions
in the same direction:lu8T 24.13 -> 26.14 ms (+8%) andqr_factor64T 12.95 -> 13.56 (+4.5%,
and its 64T PR cell had a mode-hit process at 25.64).lu64T is not resolvable: the PR's two
processes were 38.64 and 47.28 ms against main's 25.52-42.51, i.e. both arms were in and out of the
mode there. - Everything else is 1.00 +/- 0.04, in both directions, with
gemmdead flat. - If the sub-team width selection is meant to help somewhere, these four cells are not where: the
shapes on this host are far enough above the floors that the region sizes do not change
(the earlier cost: read the team dispatch cost from threadteam 0.1.2; drop team_section_cost (threadteam-rs#6 stage 4) #104 run made the same point for the cost table itself). - The vendor column is the context for all of it: the fork is 1.7-6x faster per cell at every budget,
and OpenBLAS is flat from 16T on (qr_factor80.7 -> 78.3 ms,lu40.4 -> 54.0), so nothing in this
table is a scaling ceiling of the machine.
Before merging I would want the lu 8T pair reproduced on a quiet host (lu is the only cell that
moved in a direction, and 8T is where this host's noise is smallest, so it is the cheap one to check).
|
| arm | the five processes (ms) | median |
|---|---|---|
main 1ac38c6 |
25.24, 25.34, 25.52, 25.21, 25.49 | 25.34 |
PR 2bfbc90 |
25.61, 26.02, 25.97, 25.87, 63.63 (one #107-mode process) | 25.97 |
The +8 % does not reproduce; a 2.4 % difference does, and it is real. The two arms do not
overlap: main is 25.21-25.52 (1.2 % spread) and the PR is 25.61-26.02, so every one of the five sets
puts the PR above main by 1.4-3.1 %, median 0.976 (min-of-five 0.984). The earlier +8 % came from
comparing minima of three processes whose cells both contained mode-hit members; with five sets on a
single CCX pair the estimate is stable and three times smaller.
Not a blocker from this side — it is a 0.6 ms difference on a 25 ms call — but it is the one cell in
the #109 grid that separates reproducibly, so if the sub-team width selection has a cost anywhere in
these four families, lu at 8T is where to look for it.
|
| main | PR | |
|---|---|---|
| trailing updates at width 8 | 23 | 23 |
| trailing updates narrowed | 0 (serial) | 9: 5 at width 2, and one each at 3, 5, 6, 7 |
| trailing updates serial | 40 | 31 |
| swap stages | 8 at width 8, the rest serial | identical |
| triangular-solve job splits | 12, widths 2/4/8 | identical (12, jobs 2/4/8, region width 8) |
| GEMMs on the team kernel / below the floor | 52 / 398 | 60 / 406 |
sections (run) |
2:24 8:28 |
2:29 3:1 5:1 7:1 8:28 |
The only decision that differs is the trailing update at the levels where width 8 stops paying. Main ran those levels serially; this PR runs nine of them on a narrower sub-team. These are the shapes, and their widths, per call:
- rows 416, 352, 288, 224 at b = 32: widths 7, 5, 3, 2;
- rows 64 at b = 64: width 6;
- rows 1008, 976, 944, 912 at b = 16: width 2.
Their solves and products run on the sub-team too, which accounts for the 8 extra kernel GEMMs and 8 extra below-floor GEMMs.
What this can and cannot explain
All nine sit at or near the rule's floor, the same situation as the full_piv_lu 128 cell. Their total serial work is bounded by the largest of them, about 32 * (32*32/2 + 416*32) ≈ 0.44 M multiply-adds, around 40-50 us serially, and the rest are smaller. Even if every one of them lost its whole serial time, the loss would stay below about 0.2 ms per call.
That is the right size for a 1-2% effect. It is too small for +8% of an 8-thread lu 1024 call, unless that call is well under 3 ms. If the re-measurement reproduces +8%, the width choice above does not account for it, and I would look at what this decision table cannot see: placement or the per-process mode of #107. Otherwise it is these narrowed floor-level levels, and #111's per-section allocations, plus a fixed-cost term if one is still needed after #111, are the place to address them.
64 threads (for the qr_factor 1024 64T note): LU, for reference
- The trailing updates at width 64 are the same as main.
- The narrowed regions now reach widths 9-61 in the larger levels: the
narrowedhistogram has entries at 22, 24, 29, ..., 61. Main ran those levels at the full 64 or serially. - Kernel GEMMs: 92 against 73.
This is the intended effect of #91 at high width, so it is not by itself a regression signal. If qr_factor 1024 at 64T stays at +4.5% on re-measurement, I can produce the same table for qr_factor.
|
Merged. The broccoli and evo runs show no mechanism regression: the only cells that separate (lu 1024 8T, 2.4%; full_piv_lu 128 8T) are floor-level steps that now run narrowed, and the analysis above traces their loss to per-section costs the dispatch table does not model (allocation inside per-step sections, the reduction merge). Those are tracked in #111, after which the floor-level steps are re-measured. |
Closes #91. Part of #95.
Problem
Every parallel gate except the x86 GEMM kernel's chose between two outcomes: the token's whole degree, or one thread. On a 64-wide team (threadteam 0.1.2's Zen2 row, linear above 64), a region that pays at 8 or 16 therefore ran serially. The high-thread failure mode was "too serial".
Change
One rule, one helper.
utils::cost::par_width/par_width_timereturn the widest width that pays,pays_at(estimate, sections, dispatch_cost(w)), or 1. The search runs from the full degree down, so every width is considered and a falling row is safe.par_width_upto/par_width_time_uptosearch inside a cap.ParThreshold::region_widthadds the policy:Nevergives 1;Fixed(floor)gives the whole degree at or above the floor.kernel_width(x86 GEMM) is the same function.Narrowing.
utils::thread::with_width(par, w, f):w >= degreehands over the token itself;w < 2hands overPar::Seq;split(&[w]), led by the caller, with no worker dispatch.It lives in
utils::thread, so builds withoutthreadteamcompile. Each site wraps only its own region; the caller keeps its wholepar.Sites converted:
spicy_matmul's generic path, the sparse numeric product,gemm_tiled.upgrade_householder_factor's two reductions (cap: rows per participant).Per-step factorizations. Hessenberg, tridiagonal, unblocked bidiagonal, full-pivot LU, Bunch-Kaufman and col-pivot QR latched
par = Par::Seqfor the rest of the call; they now decide per step.Neverstill makes the whole call sequential, including at the public boundaries of col-pivot QR and bidiag.Behaviour changes, deliberate:
par), so the panel's own products choose their widths.par_pays,region_pays, and the GEMM route's full-width predicate, which was Seq-only after the team arm and so always 1.Instrumentation.
par-tracerecords the chosen width.utils::team::statsgainswidth, counting narrowed regions by width up to 256;runsaturates at 16.Verification (primerose,
-j 4, 4 cores, no timing)cargo test -p t4a-faer --lib: 527 passed.--doc: 63 passed.par-traceare clean.--no-default-features --features stdhas the same warnings asorigin/main.par_width_takes_the_widest_width_that_pays: a rising row, a falling row, and a cap inside a falling row.with_width_runs_on_a_sub_team_of_that_width: checks the participants of a section.the_families_narrow_their_regions_and_match_sequential: 14 regions on a team of 8 whose row makes 4 the widest paying width. Each region runs in its own counter window and must narrow to 4, run its own split or section on 4, never split wider than 4, and matchSeqwithin 1e-10.never_reaches_the_compact_factor_upgrades: fails without the boundary guards.Fixed/Neverandpar_widthtests.scripts/run-lapack-validation.py): hessenberg, schur, schur_reorder, balance, tridiagonal_evd, self_adjoint_evd, lu, cholesky, qr, triangular and lstsq pass. The remaining failures are the same onorigin/main31f059c:dchkbd_15_to_19andbidiagonal_family: NoConvergence on DCHKBD type 16 at n = 127 to 129, in all four dtypes. Reproduced on31f059c, so it predates this change: it started with perf(svd): blocked bidiagonalisation, dqds shift, padded work copy, caller-held worker group; values-only SVD at or below the vendor #52 (svd: DCHKBD type 16 returns NoConvergence at n = 127-129 since the dqds path (#52) #110).ddc55a0.ddc55a0, theirparalleltests timed out on four cores.Residuals (evo gate, 2026-10-29)
full_piv_lu f64 128at 8T, +6.6 to +10.4%. Main latched serial after 42 steps. This PR takes 19 more steps at widths 2-7, each exactly at the rule's floor (estimate 1.8-2.9 us against3 x D(w)).best, andfor_each_child's item and slot vectors) are a defect of the mechanism, tracked as par: no heap allocation inside per-step parallel sections (full_piv_lu best vector, for_each_child item/slot vectors) #111. A fixed-cost term in the cost model will be decided after that.cholesky f64 128at 4T, +9.5 to +10.5%. The gate decisions and dispatches are identical to main, and no region narrows, so the change's mechanism is not involved. Left as a residual (layout or code-size candidate).Not in this PR
(n-k-1)^2does not track theA02matvecs,k (n-k-1).rank_update, the SIMD GEMM loops, the schoolbook product, nativeK == 1.🤖 Generated with Claude Code