Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions PATCHSET.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>`-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. |
Expand Down
74 changes: 74 additions & 0 deletions docs/worklogs/2026-10-14-tall-panel-region-cost.md
Original file line number Diff line number Diff line change
@@ -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.
Loading