Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
5aa01e2
symbolic: device construction of the pattern and assembly maps
sshin23 Oct 9, 2026
f4df2e4
docs: plain backticks for the undocumented _group_amap cross-reference
sshin23 Oct 9, 2026
77f935f
Numeric: division-free right-looking Cholesky in the regime-A and reg…
sshin23 Oct 10, 2026
81a725b
Numeric: enumerate the packed triangle in the trailing updates
sshin23 Oct 10, 2026
f91facd
bench/e2e: fresh-machine script for the MadNLP + SDS workflow
sshin23 Oct 10, 2026
8c1dd1f
bench/e2e: fresh-machine script covers AMD (vendor autodetection, AMD…
sshin23 Oct 10, 2026
1820b75
bench/e2e: fresh-machine script is explicitly detachable (stdin close…
sshin23 Oct 10, 2026
bf9ba89
bench/e2e: one-file Julia fresh-machine script
sshin23 Oct 10, 2026
2586ff0
bench/e2e: fresh_machine.jl covers AMD (vendor autodetection, backend…
sshin23 Oct 10, 2026
23223d8
bench/e2e: fresh_machine.jl develops SDS from an editable checkout (~…
sshin23 Oct 10, 2026
3ef916f
bench/e2e: fresh_machine.jl runs only the SDSProtoSolver workflow
sshin23 Oct 10, 2026
347c2da
bench/e2e: fresh_machine.jl ends in a plain madnlp(m) call
sshin23 Oct 10, 2026
b0b19f2
bench/e2e: drop the redundant madnlp options (MadNLP defaults them un…
sshin23 Oct 10, 2026
1cdbe09
fix: reapply the kernel optimizations on current main without reverti…
sshin23 Oct 10, 2026
4d7e19e
bench/e2e: fresh_machine.jl loads CUDSS again (MadNLPGPU's CUDA exten…
sshin23 Oct 10, 2026
a5be931
bench/e2e: fresh_machine.jl updates an existing dev checkout to the b…
sshin23 Oct 10, 2026
c972c8d
Ordering: expose METIS nseps as the nd_nseps config parameter
sshin23 Oct 9, 2026
c1015f9
Ordering: nd_seed config parameter; e2e wrapper exposes custom reorde…
sshin23 Oct 10, 2026
0ee4687
test: the T22 chooser testset passes all four ND_PROVIDER arguments
sshin23 Oct 10, 2026
13ea37b
bench/e2e: on-disk permutation cache in the MadNLP wrapper
sshin23 Oct 10, 2026
10f209e
bench/e2e: vendor detection runs the GPU tool (nvidia-smi on PATH wit…
sshin23 Oct 10, 2026
9223dfd
bench/e2e: NVIDIA detection also requires a listed device, not just a…
sshin23 Oct 10, 2026
038e40b
Merge branch 'perf/device-symbolic-maps' of https://github.com/exanau…
sshin23 Oct 10, 2026
ecabd8a
bench: full comparison record on a Quadro GV100
sshin23 Oct 9, 2026
1cedf8e
fix: the fused kernel's completion test used the atomic's NEW value
sshin23 Oct 10, 2026
aee012c
Merge custom-reordering: fused-kernel completion fix (left == 0, true…
sshin23 Oct 10, 2026
719dcd9
bench/e2e: wide-front roles in SDSProto + per-device wide strategy in…
sshin23 Oct 10, 2026
5332ed2
bench/e2e: the backend's root module (CUDACore under the CUDA 6 split…
sshin23 Oct 10, 2026
91c9bf4
bench/e2e: GPU detection reads /dev/nvidia*//dev/kfd first; NVML can …
sshin23 Oct 10, 2026
653c31c
Merge custom-reordering: wide-front roles + per-device wide dispatch,…
sshin23 Oct 10, 2026
35afa6b
bench/e2e: all tuning knobs in one runtime SDSTuning bundle; per-arch…
sshin23 Oct 10, 2026
b82e2a4
fix: the solve kernels' inline spin loops keep their literal backoff …
sshin23 Oct 10, 2026
655205d
Merge custom-reordering: per-arch tuning campaign (SDSTuning bundle, …
sshin23 Oct 10, 2026
5304f70
bench: performance document for the October 2026 cross-platform campaign
sshin23 Oct 10, 2026
52d3d3a
bench/e2e: the fused solver is THE solver
sshin23 Oct 10, 2026
d6e6c76
bench/e2e: tolerate concurrent dev-checkout updates (shared NFS home)…
sshin23 Oct 10, 2026
b7d827a
Merge custom-reordering: campaign write-up, H200 champion, fused solv…
sshin23 Oct 10, 2026
85f25c0
fused kernel: no-pivot LDL^T mode (Val-dispatched; stock src untouched)
sshin23 Oct 11, 2026
a62ac0e
rename the fused-kernel module: SDSProto -> SDSFused
sshin23 Oct 11, 2026
192690d
fused LDL: host-wide hybrid path (vendor trsm/gemm + 64-block panel k…
sshin23 Oct 11, 2026
b899547
fused LDL: potrf-derived host-wide fronts (the minimal-difference path)
sshin23 Oct 11, 2026
58b18f8
CPU fast path prototype: task-parallel refactorization with per-front…
sshin23 Oct 11, 2026
5d4ad4a
SDSCPU: fix single-thread deadlock (workspace pool needs nthreads()+1…
sshin23 Oct 11, 2026
fd378ef
SDSCPU: single-thread specialization (the ExaModels nothing-backend s…
sshin23 Oct 11, 2026
c7eee03
bench: MA27 reference via libcoinhsl (the libHSL 2025 build stubs the…
sshin23 Oct 11, 2026
c5ed5b8
bench: MA57 reference via libcoinhsl; MA27 on the augmented k2 system
sshin23 Oct 11, 2026
a78b0f5
gfx906: fused LDL validated (1.05x of Cholesky); champion config conf…
sshin23 Oct 11, 2026
ff5db17
CPU fast path: amalgamation 96 default for the CPU test (-17% at 24 t…
sshin23 Oct 12, 2026
918326d
bench: performance write-up updated through the LDL, CPU and AMD work…
sshin23 Oct 12, 2026
c70a705
ordering: import the GPU-hybrid METIS NodeND prototype
sshin23 Oct 12, 2026
b3bd565
ordering: device node-separator refinement kernels (GpuRefine.jl), of…
sshin23 Oct 12, 2026
9a7b7d4
MadNLP interface as a package extension; the fused pipeline moves int…
sshin23 Oct 12, 2026
104e383
Merge remote-tracking branch 'origin/consolidated-prs' into ldl-fused
sshin23 Oct 12, 2026
68614e3
ordering: level-batched block coarsening (GpuBatch.jl), validated on …
sshin23 Oct 12, 2026
9082389
Merge remote consolidated-prs
sshin23 Oct 12, 2026
0cf1b7c
Cleanup of the consolidation branch (PR #126)
claude Oct 12, 2026
7606173
fused: rename the tile count in emit_split; it shadowed the element t…
sshin23 Oct 12, 2026
2b719a3
bench: the fresh-machine one-liner tests the PR branch again, not main
sshin23 Oct 12, 2026
f0f14fa
tuning: default METIS nseps 4 -> 1; fast first-touch reordering
sshin23 Oct 12, 2026
ca58cc5
bench/e2e: fresh_machine.jl reports the timing split and a warm secon…
sshin23 Oct 12, 2026
63534b8
bench/e2e: fresh_machine.sh builds the @sds project environment; work…
sshin23 Oct 12, 2026
0eabf72
bench/e2e: fresh_machine.sh ends in a REPL on a terminal; fix silent …
sshin23 Oct 12, 2026
8b02065
MadNLP ext: no-pivot LDL^T mode (sds_factorization = :ldl) with true …
sshin23 Oct 12, 2026
49801a1
bench/e2e: SDS_PERM_CACHE=0 turns the permutation cache off in workfl…
sshin23 Oct 12, 2026
5f5468f
MadNLP ext: remove the permutation cache entirely
sshin23 Oct 12, 2026
8f8d067
MadNLP ext: expose SDSSolver the standard way (declared global set by…
sshin23 Oct 12, 2026
95e8806
CI: Aqua validates the extension-assigned exports with MadNLP loaded
sshin23 Oct 12, 2026
8e66366
fused: review fixes — LDL failure detection, completion race, NaN piv…
sshin23 Oct 12, 2026
25fdc4b
bench: the LDL gates gate (nonzero exit, stock-tied residual, matchin…
sshin23 Oct 12, 2026
3941d31
symbolic/options: review fixes — device-route validation, Int32 gates…
sshin23 Oct 12, 2026
cf665d0
MadNLP ext: no-pivot LDL disables 2x2 pivot-pair detection — the prod…
sshin23 Oct 12, 2026
e86447d
bench: flip the fresh-machine refs to main for the merge
sshin23 Oct 12, 2026
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
5 changes: 4 additions & 1 deletion .github/workflows/Aqua.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@ jobs:
with:
version: '1'
- name: Aqua.jl
run: julia --color=yes -e 'using Pkg; Pkg.add("Aqua"); Pkg.develop(path="."); using Aqua, SparseDirectSolver; Aqua.test_all(SparseDirectSolver, ambiguities=false)'
# MadNLP is loaded so the SparseDirectSolverMadNLPExt extension assigns the
# exported SDSSolver/SDSSolverOptions globals: the undefined-exports check
# then validates them for real instead of flagging the declared-only state
run: julia --color=yes -e 'using Pkg; Pkg.add(["Aqua", "MadNLP"]); Pkg.develop(path="."); using Aqua, MadNLP, SparseDirectSolver; Aqua.test_all(SparseDirectSolver, ambiguities=false)'
6 changes: 4 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ GPUArrays = "0c68f7d7-f131-5f86-a1c3-88cf8149b2d7"
GPUArraysCore = "46192b85-c4d5-4398-a991-12ede77f4527"
KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Metis = "2679e427-3c69-5b7f-982b-ece356f1e94b"
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"

[weakdeps]
AMDGPU = "21141c5a-9bdb-4563-92ae-f87d6854732e"
CUDACore = "bd0ed864-bdfe-4181-a5ed-ce625a5fdea2"
Krylov = "ba0b0d4f-ebba-5204-a429-3ac8c609bfb7"
Metis = "2679e427-3c69-5b7f-982b-ece356f1e94b"
MadNLP = "2621e9c9-9eb4-46b1-8089-e8c72242dfb6"
cuBLAS = "182d3088-87b7-4494-8cad-fc6afaa545bc"
cuSOLVER = "887afef0-6a32-4de5-add4-7827692ba8fc"
cuSPARSE = "b26da814-b3bc-49ef-b0ee-c816305aa060"
Expand All @@ -27,7 +28,7 @@ cuSPARSE = "b26da814-b3bc-49ef-b0ee-c816305aa060"
SparseDirectSolverAMDGPUExt = "AMDGPU"
SparseDirectSolverCUDAExt = ["CUDACore", "cuBLAS", "cuSOLVER", "cuSPARSE"]
SparseDirectSolverKrylovExt = "Krylov"
SparseDirectSolverMetisExt = "Metis"
SparseDirectSolverMadNLPExt = "MadNLP"

[compat]
AMD = "0.5"
Expand All @@ -41,6 +42,7 @@ GPUArraysCore = "0.2"
KernelAbstractions = "0.9"
Krylov = "0.10"
LinearAlgebra = "1.13"
MadNLP = "0.8, 0.9, 0.10"
Metis = "1"
SparseArrays = "1.13"
cuBLAS = "6"
Expand Down
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ oneAPI, Metal and the KernelAbstractions CPU backend.
**Status: v0.1 under construction.** Working today, on the CPU backend, on
CUDA and on AMDGPU (ROCm):

* host symbolic analysis: AMD or nested dissection (METIS through the Metis
extension) ordering, elimination tree, supernodes with GPU-tuned
* host symbolic analysis: AMD or nested dissection (METIS) ordering,
elimination tree, supernodes with GPU-tuned
amalgamation, static factor layout and device assembly maps; for symmetric
indefinite matrices the analysis pairs structurally zero pivots with a
partner (`pivot_pairs`, MA57-style compressed-graph ordering) so that the
Expand Down Expand Up @@ -108,8 +108,7 @@ Pkg.add(url = "https://github.com/exanauts/SparseDirectSolver.jl")
Loading CUDA.jl enables the CUDA extension (`CuSparseMatrixCSR`/`CuSparseMatrixCSC`
constructors, cuBLAS/cuSOLVER dense kernels); loading AMDGPU.jl enables the
AMDGPU extension (`ROCSparseMatrixCSR`/`ROCSparseMatrixCSC` constructors,
rocBLAS/rocSOLVER dense kernels; ROCm must be found by AMDGPU.jl). Loading Metis.jl enables nested
dissection ordering; without it the ordering is AMD.
rocBLAS/rocSOLVER dense kernels; ROCm must be found by AMDGPU.jl).

## Usage

Expand Down
107 changes: 107 additions & 0 deletions bench/ARCH-TUNING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Per-architecture tuning of the fused factorization

How the October 2026 campaign tuned the fused refactorization prototype per GPU
architecture: the knob inventory, the measurement discipline, the step-by-step
procedure for a new device, the mechanisms behind each knob, and the measured
champions. Workload throughout: the 78k-bus ACOPF condensed KKT
(`pglib_opf_case78484_epigrids`, n = 674562, nnz(L) ≈ 14.3M), tuned ordering
(METIS nseps = 4, seed = 3).

## Ground rules (these produced every number below)

1. **Gate every configuration.** A trial is valid only with `info == 0`,
elementwise factor delta ~1e-9 against the stock factorization, and the
residual matching stock. An `info == 0` check alone is insufficient: one
race produced a silently wrong factor (Δ ≈ 1e-5) with clean info.
2. **Interleave with the baseline.** Shared boxes drift by 2× within an hour;
A/B/A/B in one process is the only defensible comparison. Three pairs
minimum; treat sub-2% differences as noise.
3. **Correctness before speed on a new chip.** Run 20 gated refactorizations
(`bench/e2e/ldl_fused_race.jl`) before believing any timing. This is what caught
the sm_86/89/Blackwell/gfx906 completion-signal race that sm_70/80/90 hide.
4. **Occupancy arithmetic before submitting sweeps.** Most structural knobs are
decided by `blocks/SM = min(smem_per_SM ÷ smem_per_block,
threads_per_SM ÷ workgroup)`; compute it first and you can predict the sign
of the result (every prediction below held).

## Knob inventory

| knob | where | kind | mechanism |
|---|---|---|---|
| wide-front strategy (`wk`/`mseg`) | plan builder | runtime | host vendor calls vs in-kernel blocked wides + single merged launch; trades ~100 host launches against in-kernel spin residency |
| subtree budget (`subtree_budgets`) | solver option | runtime | regime-A local-memory class; smaller class = more subtree workgroups resident per SM/CU |
| subtree parallelism / amalgamation | solver options | runtime | subtree formation and supernode merging; saturated on every tested arch except amal 32→48 (small win on big-SM parts) |
| ordering (`nd_nseps`, `nd_seed`, `user_perm`) | solver/wrapper | runtime | fill/flops; arch-independent; every instance recomputes (no caching); pin with `user_perm` |
| panel width (`pw`) | plan + kernel `Val` | runtime per-plan | fronts ≤ pw factor in-kernel; packed panel = pw(pw+1)/2 · 8B of shared memory **for every block of the kernel** (one kernel = one shared size) |
| tile size (`tl`) | plan + kernel `Val` | runtime per-plan | syrk tile; shared halves = 2·32·tl · 8B; accumulators = tl²/WGF registers per thread |
| workgroup size (`SDSTuning.workgroup`) | tuning bundle | runtime (>= 128, multiple of 32: the tile roles' register accumulators) | waves per workgroup; latency hiding on wave64 AMD; occupancy on 1536-thread parts |
| spin backoff cap (`SDSTuning.spin_cap`) | tuning bundle | runtime | dependency-counter wake-up latency vs polling traffic |
| CUDA graph replay (`graph`) | run_fused kwarg | runtime | whole refactorization captured once, replayed as one launch |

## Procedure for a new architecture

1. **Correctness:** `bench/e2e/ldl_fused_race.jl` (20 gated refactorizations).
Any failure: stop, bisect kernel-vs-src-vs-config before timing anything.
2. **Phase profile:** `profile_refact()` — sync-inflated but reveals the split
(resets / subtrees / mega kernel / host wides / stats). The dominant phase
picks the first lever: host-wides share ≥50% ⇒ try merged wides first.
3. **Wide strategy:** `mseg_ab.jl`. Decision so far follows one attribute:
`MAX_THREADS_PER_MULTIPROCESSOR < 2048` (sm_86/89, Blackwell workstation)
or AMD ⇒ merged wins big (1.6–2.1×); 2048-thread parts (sm_70/80/90) keep
host wides (merged costs ~1.5 ms there).
4. **Knob sweep:** `tune_platform.jl` (budgets, sp, amal, cw, rows, flip-check
of the wide strategy), baseline-interleaved. Expect budgets to be the only
mover: pick the class whose size ≈ `LDS_per_SM ÷ desired_workgroups`.
5. **Structural (only where shared memory allows):** `pw_ab.jl`, `tl_ab.jl`.
Run the occupancy arithmetic first; on ≤100 KB/SM parts pw=96/tl=64 lose by
construction, on 228 KB (Hopper) they are free.
6. **Bundle knobs:** `SDSTuning.workgroup` ∈ {128, 256} and `SDSTuning.spin_cap`
∈ {128, 16} (runtime, per plan; workgroup has a hard 128 floor). WGF=256 was −20% on wave64 AMD, flat-to-pending on
NVIDIA; the spin cap matters only on residency-thin parts.
7. **Graphs:** `graph_ab.jl` — small (~1%) and free; keep it on everywhere.
8. **Stack and re-validate:** combine the winners, three gated interleaved
pairs (`combo_ab.jl`), then 20 gated refactorizations of the final config.

## Mechanisms, compressed

- **2048- vs 1536-thread SMs is the single most predictive attribute.** It
decides the wide strategy, the subtree budget, and whether the completion
race (pre-fix) manifested at all.
- **One fused kernel means one shared-memory size.** Any panel/tile growth
taxes *all* roles' occupancy; it only pays where SM shared memory grew
faster than the panel (Hopper).
- **Launch overhead doesn't scale with silicon.** The faster the chip, the
larger the share of host launches and dependency-hop latency; hence merged
wides and graphs matter more on newer parts, flops-side knobs less.
- **Wave64 wants bigger workgroups.** 128 threads = 2 waves on AMD leaves too
little in-flight work per workgroup; 256 = 4 waves was −20%.

## Measured champions (fused refactorization, gated, interleaved)

| device | arch | threads/SM | config | SDS | cuDSS |
|---|---|---|---|---|---|
| Quadro GV100 | sm_70 | 2048 | host wides, bud 16K, graph | **24.1 ms** | 24.3 ms |
| A100-80GB | sm_80 | 2048 | host wides, bud 16K | 19–21 ms | 17.3–18.7 ms |
| H200 | sm_90 | 2048 | pending the structural matrix | 13.6 ms so far | 11.1 ms |
| L40S | sm_89 | 1536 | merged, bud 8K, graph | **15.2 ms** | 12.2 ms |
| RTX Pro 6000 | Blackwell | 1536 | merged, bud 8K | **15.8 ms** | 13.5 ms |
| RTX 3080 | sm_86 | 1536 | merged | 32.4 ms (10 GB: kernel-level only) | — |
| Radeon VII | gfx906 | wave64 | merged, bud 8K, WGF 256, cap 16 | **40.0 ms** | — |

All of this is bundled: `SparseDirectSolver.Fused.SDSTuning` holds every knob as a runtime
field, `default_tuning(backend)` is the single per-architecture defaults table,
and `madnlp(m; ..., sds_tuning = SDSTuning(; workgroup = 256))` overrides it.
The portable kernel takes workgroup size and spin cap per plan — nothing is
compile-time or environment-dependent in the wrapper path.

## Scripts

The campaign's one-off sweep drivers (`race_repro.jl`, `fact_lib.jl`'s
`profile_refact`, `mseg_ab.jl`, `pw_ab.jl`, `tl_ab.jl`, `graph_ab.jl`,
`combo_ab.jl`, `tune_platform.jl`, `h200_matrix.jl`, `e2e/amd_tune.jl`) were
not shipped; the committed equivalents are `e2e/ldl_fused_race.jl` (gated
correctness), `e2e/ldl_fused_test.jl` (fused vs stock + same-strategy ratio)
and `e2e/amd_sweep.jl` (knob sweep). Read the procedure above with those
substitutions; CUDA-graph replay (step 7's `graph_ab.jl`) retired with the
consolidation cleanup (no graph kwarg at HEAD). All gates take the
matrix from `bench/data/` and the tuned permutation from `bench/perm_cand3.bin`.
Loading
Loading