Repository navigation
perf: cooperative pivot search in the device LDLᵀ kernels (#75) - #87
Conversation
Pass 1 (λ, r, column maximum), pass 2 (σ, column r) and the fallback scan are workgroup reductions with first-maximum tie-breaking, so the pivot sequence equals the reference. The pivot is taken from the values before the interchange, keeping five barriers per step. The reference fallback prunes columns that cannot win and exits the threshold test early (same result). Regime-A LDLᵀ reserves 1024 B of local memory for the reduction slots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A lane past the remaining rows (passes 1 and 2) or columns (pass 3) holds the neutral partial, so the stage-1 and final combines stop there; small KKT fronts no longer pay the full 128 → 16 → 1 fold per step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cqp7TUSyCEdjPtLDEftJf5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cqp7TUSyCEdjPtLDEftJf5
| # lanes of pass `P` at column k that can hold a partial result (the others hold the neutral one): one per row | ||
| # below k (pass 1, with k the column the step advances to), per row from k (pass 2), per column from k (pass 3) | ||
| @inline _lt_nused(ctl, k, ::Val{P}) where {P} = |
There was a problem hiding this comment.
nit (non-blocking): for pass 1 the lanes that can hold data are the rows k+1:f, i.e. f - k lanes (lane li covers row k + li), but _lt_nused returns f - k + 1 for P == 1 as for pass 2 (rows k:f). Harmless, since _lt_pass1! writes the neutral values (0, 0, 0) into every lane ≤ NL, so the extra lane folds as a no-op; the correctness of the fold never depends on the bound. If you want the comment and the bound to agree, use Int(ctl[_ST_F]) - k for pass 1.
There was a problem hiding this comment.
VERDICT: APPROVE (head f47a2d6)
Perf PR for issue #75, step 1 of experiment 1 (T25 owner note, item 1); not a TNN task, so there is no TASKS.md Report block and the PR body serves as the report.
Checked
- Scope: only the cooperative pivot search (
src/numeric/ldlt.jl), the matching early-exit threshold test and_best_1x1pruning in the reference, the regime-A LDLᵀ local-memory reserve (SUBTREE_LOCAL_RESERVE_LDLT,subtree_local_reserve,build_schedule(…; reserve),subtree_capacity(…, reserve), both analysis callers), and the PERFORMANCE.md tables. PLAN.md, TASKS.md and Manifest.toml untouched; no public name, parameter or phase string changed. The reserve is used consistently: the LDLᵀ launch usesLB - SUBTREE_LOCAL_RESERVE_LDLT, the schedule capacity for"S"/"H"subtracts the same 1024 B; Cholesky keeps 256 B. The 944 B figure in the docstring matches ctl (22 × 8) + pv (8 × 16) + red (48 × 8) + redi (32 × 8). - Correctness of the search, pass by pass against
_choose_pivot: pass 1 (λ, r = first maximum over rows k+1:w, column max over k+1:f), pass 2 (σ over k:w but r; column-r max over k:f but r, k; column-k max over k:f but k, r, so_accept_1x1(F, r, k)=max(mr, |F[k, r]|)and_accept_2x2gets m1 = mk, m2 = mr), pass 3 (_best_1x1with the first-maximum tie-break, thepivot_type = "D"largest-diagonal fallback with the NaN-at-k case). Per-lane strict>plus the fold rule (greater value, or equal value and smaller index; index 0 = none) reproduces the serial first maximum;maxpropagates NaN as_colmaxdid;_threshold_ok’s early exit is equivalent because the prefix max is monotone andu ≥ 0._lt_select!reads the 2×2 block (F[k,k], F[r,k], F[r,r]) andpsign[perm[piv[c0+c-1]]]before the interchange, which equals the post-swap values the old_lt_pivot!read;_lt_swap!writes the new diagonal/zero from the work item that owns row k (and k+1), after its own swap writes, so no cross-lane race. End-of-step pass 1 reads column K+STEP while_lt_scale!writes columns K..K+STEP−1 only. Phase branches readctl[_LT_PHASE]after a barrier (uniform), so the nested@synchronizeare safe; the reductions have a fixed order (deterministic). - Conventions: 1-D workgroups,
@localmemsized byVal/constants, no warp intrinsics, no atomics, no allocation or host sync in the kernels, genericT/INT(red::R,redi::IT), no@allowscalar. - Tests: none weakened;
check_schedulepasses the structure’s reserve as the change requires. The existing equality suite (piv, pivot_kind, D, L, stats vsref_ldlt!on BACKENDS × ELTYPES, three regime mixes,"D"/"N"pivot types, perturbation) exercises all three passes on both kernels. The PR body’s counts (104914 pass / 2 broken, the T16@test_broken) are plausible; CPU and CUDA CI are still pending on this head and are handled by the pipeline. - Follow-up: #86 exists, open, labelled
found-by-agent(not yettriaged, which will hold the chain after merge, as designed).
Blocking findings: none.
Non-blocking: one inline nit on _lt_nused (pass 1 bound is one lane over; harmless because every lane holds a neutral value).
#90) * CI: parallel test runner, CPU job on kkt, LDLᵀ compiled only when used The CI jobs went from ~21 to ~52 min with the regime-B/C LDLᵀ kernels (#87-#89). Almost all of it is compilation: on the CPU backend the KA kernels compile with their caller, and the solver's numeric phase called factorize!, which branches on the structure at run time and so compiled the LDLᵀ path (all its kernels) for every Cholesky solver as well. test_api (SPD/HPD only, 4 T x 2 INT) went from 658 s to 2416 s. * src/solver.jl: the numeric phase dispatches on the structure behind an inference barrier that takes the mutable solver handle only (no boxing, so the numeric-phase allocation budgets hold); factorize_cholesky! is the Cholesky path of factorize!, which keeps its static branch. First solver factorization per (T, INT) on the CPU backend: 42-84 s -> 20-36 s. * test/runtests.jl: ParallelTestRunner (as CUDA.jl and oneAPI.jl); every test_*.jl runs in its own module on a worker pool with the shared helpers as init_code. The seed 666 goes with each test file since the runner reseeds with 1 after init_code. SDS_TEST_ONLY/SDS_TEST_SKIP still work. * ci.yml: the CPU-only job runs on the self-hosted kkt machine (same check name, it is required by the ruleset); PTR_NUM_JOBS caps the workers (8 CPU, 4 GPU) since the four kkt runners share the machine. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QSF5MG6xxpF2D1TCFkb3UZ * Tests: run the long test files once per element type A worker's cold compilation sets the wall time, and it grows with the element types a test file covers. The long files (SPLIT_FILES in test/runtests.jl) now run as four parts, test_api[Float32] etc., each in a worker of its own: ELTYPES/REAL_ELTYPES/COMPLEX_ELTYPES are the part's subset (test/utils.jl, set through Main.SDS_TEST_PART before the helpers are included), and the testsets that do not loop over the element types run in the Float64 part only (RUN_SHARED). Test counts are unchanged (CPU 62938, CUDA 69499, 1 broken). Owner's machine: CPU suite 13m52s -> 9m30s (16 workers), CUDA 16m23s -> 10m31s (8 workers); test_numeric_ldlt's slowest part 226 s against 814 s unsplit. ci.yml: 32 CPU workers and 12 CUDA workers on kkt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QSF5MG6xxpF2D1TCFkb3UZ --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Refs #75
Task: #75, experiment 1 of PERFORMANCE.md, step 1 of 3 — parallel pivot search in the device LDLᵀ/LDLᴴ kernels (T25 owner note, item 1).
What was built:
src/numeric/ldlt.jl: the pivot search offront_ldlt_kernel!andsubtree_ldlt_kernel!(was_lt_chooseon work item 1) is a cooperative search: pass 1 (λ, r and the column maximum of column k), pass 2 (σ and the column-r maxima of Bunch–Kaufman) and pass 3 (the_best_1x1fallback, one lane per candidate column) are workgroup reductions in@localmem(1-D, no warp intrinsics, two-stage fold 128 → 16 → 1, only over lanes that can hold data) with the reference's tie-breaking (first maximum), so every decision equals the serial one. Work item 1 takes the pivot from the values before the interchange and the interchange phase writes the new diagonal, which keeps five barriers per step; pass 1 of the next step runs in the scale phase.src/reference/ldlt.jl:_best_1x1skips columns that cannot beat the best so far, and the threshold test exits at the first violation (_threshold_ok;u ≥ 0is finite, sod ≥ u·max⇔d ≥ u·|x|for every entry, NaN included). Same results, the equality tests prove it.src/symbolic/schedule.jl:SUBTREE_LOCAL_RESERVE_LDLT = 1024andsubtree_local_reserve(structure): the regime-A LDLᵀ kernel keeps 1024 B (was 256) of its local memory for the control words and reduction slots;build_schedule(…; reserve)andsubtree_capacity(…, reserve), passed by the analysis for"S"/"H". Cholesky schedules are unchanged.PERFORMANCE.md: tracked-issues table, "Experiments 1–2 results" with the step-1 table.Tests:
julia --project=. -e 'using Pkg; Pkg.test()'with CUDA in the local test environment (RTX 4080, not committed): 104914 pass / 0 fail / 2 broken (the T16@test_broken, once per backend) before the last commit (lane-bounded folds); after ittest_numeric_ldlt,test_symbolic_schedule,test_api,test_ported,test_refinement,test_ubatch,test_solveagain on CPU and CUDA: 15020 + 25371 pass, 0 fail, 2 broken. Rebased on T18 (#83, no overlap).test/test_symbolic_schedule.jl:check_scheduleuses the structure's reserve (required by the change, no assertion loosened). No allocation change (factorize!LDLᵀ 64 B on the CPU backend).Measurements (RTX 4080; refactorization best of 5 warm runs,
bench/profile_phases.jl;*:bench/compare.jlmedian where the profile skips the matrix):nperturbedidentical tomainon all nine.kkt_matrix(Float64, 3000, 1000, 1e-8), default analysis (the T15 number): CUDA 33.9 s → 8.3 s; KA CPU backend 7.0 s → 7.0 s againstref_ldlt!2.6 s (work items of a workgroup run one after another there, so a parallel search cannot help).Deviations from PLAN.md / TASKS.md: the regime-A LDLᵀ local-memory reserve is an analysis change for
"S"/"H"(1024 B instead of 256 B per budget class), needed for the reduction slots in the 48 KiB class. The smallest KKT dumps (case14, w ≤ 16) are 3–17% slower (reduction barriers); apache2/lap3d_40 within ±5% (one workgroup per huge front, step 3).Follow-up issues opened: #86 (found-by-agent, pre-existing on
main): the device LDLᵀ differs fromref_ldlt!on the K2 dumps in pivot sequence andnperturbed(rounding with max|L| 1e14–1e16, #71); inertia equal.🤖 Generated with Claude Code
https://claude.ai/code/session_01Cqp7TUSyCEdjPtLDEftJf5