diff --git a/PATCHSET.md b/PATCHSET.md index abb5bf84..4f6a4aed 100644 --- a/PATCHSET.md +++ b/PATCHSET.md @@ -36,6 +36,7 @@ upstream `faer`. | Trailing-update size floor in the blocked partial-pivoting LU | `lu_in_place_recursion` halves its panel every level and ran, per level, a panel solve and a trailing product at the caller's `par` whatever their size. At `512 x 512` that is ~60 rayon regions, most of them a few thousand flops. Both products now take `Par::Seq` while `A11.nrows() * A11.ncols()` is not above `params.par_threshold` (`128 * 128`), the same field the swap stage already reads; the recursion still forwards the caller's `par`, so each level decides for its own trailing update. The floor is a sweep over 256x256, 512x512 and 1024x1024, not a single cell. Parent -> child, best of 4, min over 14 interleaved reps, f64, 4 threads (CPUs 0-3): 128x128 0.0980 -> 0.0582 (1.68x), 192x192 0.1597 -> 0.1120 (1.43x), 256x256 0.3108 -> 0.2550 (1.22x), 384x384 0.6046 -> 0.4806 (1.26x), 512x512 1.2799 -> 1.1673 (1.10x), 1024x1024 5.2400 -> 5.1188 (1.02x); 512x512 at 1T unchanged (1.8818 -> 1.8724). The vendor gap is not closed: the sequential leaf kernel and the level-0 products remain, and the swap stage's own region was deliberately left alone. Sixteen cores to eight cores of the campaign's page are not reproduced; four physical cores only. | KEEP | tlinalg-rs#46 | Own code: no vendor source is used or adapted. | | Tall compact-WY trailing update scaling | The blocked compact QR applies its panel through `apply_block_householder_transpose_on_the_left_in_place_with_conj`, whose generic path partitions the target's columns with `n_tasks = min(parallelism_degree(par), K, 4)` and `rayon(par / n_tasks)` inside each. On a tall DRAM-bound panel (4096x16 basis, 4096x240 target, eight physical Zen 5c cores sharing 8 MiB of L3, 16.8 MB target) the update is 8.72 ms at `Par::Seq` and 2.45 ms at `Par::rayon(8)` = 3.56x, where OpenBLAS reaches 5.5x on the same cores for the same factorization; splitting the columns by hand saturates at 3.87x, so the ceiling is the kernel, not the decomposition. Candidates: partition rows for the `C -= V tmp` half, which is row-parallel and has the long dimension (rows >> columns, while the column partition caps at 4), and the DRAM-bound path of the inner matmuls. | TODO | tlinalg-rs#46 | Reproducer and the phase split: tlinalg-benchmark `docs/worklog.md`, 2026-10-12. Drives no code change in tlinalg until the kernel moves. | | EVD and SVD factorisation floor | The question `2026-10-14-bidiag-parallel-hint.md` left open: both `tridiag_in_place` and `bidiag_in_place` latch to `Par::Seq` for the rest of the factorisation once the trailing area drops below `192 * 256`, which at 128x128 closes the whole call, so `eigh` and `svd_values` are flat from 1T to 8T there while MKL threads that size. Forcing the gate open (threshold `0`) is 1.5-4x worse at every shape measured -- `tridiag_in_place` f64, four threads, CPUs 0-3, best of 10, min over 6 interleaved reps, ms open / shipped / fully sequential: 96 0.2662 / 0.0682 / 0.0677, 128 0.3890 / 0.1266 / 0.1264, 160 0.5368 / 0.2221 / 0.2213, 192 0.6927 / 0.3393 / 0.3418, 256 1.0405 / 0.7027 / 0.6973; `bidiag_in_place` 128 0.6024 / 0.3098 / 0.3115, 192 1.3596 / 0.8145 / 0.8169, 256 3.0776 / 2.3277 / 2.1858. At 512/1024 the shipped floor is the best swept value and the fully sequential call is 1.5-1.7x worse, so the open window pays there. Lower floors (8192/16384/32768) buy nothing anywhere. A floor above the first column's area wins 5.6% on `256 x 256` bidiagonalisation and is noise at five other shapes, so it was not taken. No code change. | DROPPED | — | Isolated driver on the two workers, f64, square n in 96/128/160/192/256/512/1024, CPUs 0-3 (four cores), pool = threads, `RAYON_NUM_THREADS=8` repeated for 128/256 and identical to within 1%. Numbers and the untaken lead: `docs/worklogs/2026-10-14-evd-svd-floor-sweep.md`. | +| Tall panel per-call region cost | `tlinalg-rs#46`'s open item: the level-2 panel of a tall compact-WY QR is a chain of reflectors, the fork's rank-one arm already threads each one, and each threading is its own rayon region with the pool parked in between. Measured the panel and the region: the unblocked `4096 x 16` QR (16 reflectors, each a `(4096 - j) x (16 - j)` rank-one update) is 0.1957 / 0.1788 / 0.1731 / 0.1968 ms at 1 / 2 / 4 / 8 workers on four cores (CPUs 0-3, best of 20, min over 10 reps) -- 1.13x, already spent at two threads, worse at eight. Region cost, 300 repetitions of one threaded `64 x 64 x 64` product, four-thread pool, best of 3, min over 5 interleaved reps, us per repetition: sequential 6.44, threaded back to back 8.55 (+2.11), threaded after one sequential product 10.27 (+3.83). Fifteen reflectors is 32-57 us of the panel's 173 us. Reusing the region is the `Thread policy` row, not a gate. Also recorded: the unblocked `m x 16` QR is 34% slower threaded at `m = 1024` and 17% at `2048`, and pays only from `3072`, while the rank-one arm's floor is active from `m ~ 546`; the floor is load-bearing where it was measured, so it was not moved. No code change. | DROPPED | tlinalg-rs#46 | Numbers: `docs/worklogs/2026-10-14-tall-panel-region-cost.md`. The campaign's `c64` provider cell (13.4 ms, 5 ms panel) is not reproduced; these are `f64` through faer's own API. Four physical cores. | | Typed uninitialized overwrite destination | A `MatUninitMut`-style destination that never reads the prior output, accepted by `matmul_with_conj`. | TODO | strided-rs#198 | strided-rs currently returns a Faer-only `Unsupported`. | | Native SVD / eigh direct output | At nbatch=1, stop writing U and the eigenvectors into an intermediate `Mat` and copying them out; write into the caller's output region instead. | TODO | tlinalg-rs#25 | Preserve output shape, initialization, and failure cleanup. | | Borrowed reflector columns | Stop copying reflectors out of the compact QR buffer into `basis` scratch; borrow them through a column split. | TODO | tlinalg-rs#17 | Numerics, the tau convention, and the restored diagonal beta stay identical. | diff --git a/docs/worklogs/2026-10-14-tall-panel-region-cost.md b/docs/worklogs/2026-10-14-tall-panel-region-cost.md new file mode 100644 index 00000000..c30833ad --- /dev/null +++ b/docs/worklogs/2026-10-14-tall-panel-region-cost.md @@ -0,0 +1,74 @@ +# The tall panel's per-region cost + +Ledger row: `Tall panel per-call region cost`. Consumer: `tlinalg-rs#46`'s open item, +the compact-WY cell at `4096 x 256` `b1 c64` eight threads, whose serial part is said to +be the level-2 panel. + +## Decisions + +- **No code change. The panel's parallelism is bounded by its own dispatch, and the + dispatch is a work-model question, not a gate.** The level-2 panel of a tall blocked QR + is a chain of reflectors, each of which depends on the previous one: batching them into + a compact WY factor is what the *blocked* level already does one level up, and the panel + is what is left after that. What is left to parallelise is inside one reflector -- the + fork's rank-one arm already falls through to the generic, column-split path above + `8192` elements (`fa22a69`) -- and the cost of *asking* for that split is a fresh rayon + region per reflector, with the pool parked in between because + `make_householder_in_place` and the column norm are serial. Reusing one region across + the chain, or batching the reflector applications, needs a caller-owned work model + rather than `Par::rayon` per call. That is the ledger's `Thread policy` row + (`tenferro-rs#2000`, `strided-rs#6`), and this record says so with the measurements + rather than half-building it. +- **The panel does not reach the threads, and it is not four threads that stops it.** The + unblocked `4096 x 16` QR -- 16 reflectors, each a `(4096 - j) x (16 - j)` rank-one + update, which is the level-2 panel shape -- measures 0.1957 ms at one thread, 0.1788 at + two, 0.1731 at four and 0.1968 at eight. The whole gain is 1.13x, and it is already + three quarters spent at two threads. The eight-thread column is *worse* than one + thread. So the premise that the panel "stops scaling at four threads" is optimistic: + on these four cores it barely starts. +- **The per-call region cost, measured three ways.** Same driver, `f64`, four-thread pool + on CPUs 0-3, 300 repetitions of one `64 x 64 x 64` product, best of three calls, + smallest over five interleaved repetitions, µs per repetition: + - the product sequential: 6.44 + - the product threaded, back to back with the next: 8.55 -- the region costs 2.11 µs + *more* than the sequential call for the same work, and buys nothing, because `64^3` + is below any useful split + - the product threaded, separated from the next region by a second (sequential) + product: 16.71 for the pair, so 10.27 µs for the threaded half, against 6.44 for the + sequential one -- a serial gap of one product turns 2.11 µs of overhead into 3.83 µs. + + Fifteen reflectors per panel is therefore 32-57 µs of region dispatch, against the + 173 µs the panel takes. It is a fifth to a third of the panel and it is not the whole + story: even with no dispatch at all the panel would not scale far, because each + reflector is one `4096 x 15` pass over 0.5 MB and the four cores share one memory + system. +- **Recorded, not taken: the rank-one arm's `8192`-element floor is below the crossover + for this shape class.** The same unblocked `m x 16` QR at one and four threads (ms, + four-thread pool, CPUs 0-3): `1024` 0.0535 -> 0.0719 (34% *slower* threaded), `2048` + 0.1004 -> 0.1171 (17% slower), `3072` 0.1483 -> 0.1423 (4% faster), `4096` 0.1958 -> + 0.1733 (11% faster). The rank-one arm is active from `8192` elements, i.e. `m` about + 546 at this width, so it is threading throughout the losing range. Whether the loss is + the region or the arm's column split is not separated here, and the arm's floor is + load-bearing where it was measured (`fa22a69`, the ledger's threaded-rank-one row), so + it is left alone. + +## Verification conclusions and constraints + +- Establishes: inside this fork the level-2 panel's problem is not a threshold. The + threads are asked for on every reflector and the answer is 1.13x; the cost being paid + for that is 2.1-3.8 µs per region, measured on a product whose arithmetic is small + enough that the region cannot pay for itself. +- Context: the blocked `4096 x 256` `f64` QR through faer's own API is 12.9014 ms at one + thread and 8.2981 ms at four (1.55x), so the panel above is 1.3% of that call. The + campaign's `4096 x 256 b1 c64` cell is `c64` through tlinalg's provider and is not + reproduced here; its 13.4 ms and its 5 ms panel are not this record's numbers, and the + phase shares are not comparable across the two. +- Limits: **four physical cores.** Everything is pinned to CPUs 0-3; the campaign holds + 4-11 and their siblings. The eight-thread columns are eight workers on four cores, + which is why the panel's `8` row is worse than its `1` row, and why the per-region cost + measured here is a four-core cost. A wider pool can only reduce the wake-up share. +- Limits: the region-cost probe uses `linalg::matmul` on a `64 x 64 x 64` product, not a + reflector application, so it measures rayon's fork-and-join and wake-up inside faer's + dispatch and not the arm's own bookkeeping. The panel's own dispatch also allocates its + column partition, which the probe does not. +- No test and no gate: no code changed.