Status: current · RTX 4090 (sm_89), CUDA 13.2 · measured 2026-09-10
Native versus vendor timings for potrf, getrf, geqrf, orgqr and getrs at n = 4..512, all four types. The routing windows in tuned/ for these ops and the gates in docs/design/small-n-factorization-plan.md rest on these numbers.
Ratios are vendor_ms / native_ms. > 1 means native wins. Each row carries a resolved_route read back per arm from the coverage instrument in a separate process. pin_parsed = 1 means the pin parsed, not that it ran. The CSVs use route-era spellings (vendor:auto, native:<tier>); current coverage records vendor, cta and blocked.
61 rows in 60 cells are flagged bad=1 (relative sd >= 10% over 7 interleaved reps). They stay in the CSVs and are excluded from every ratio here: 52 orgqr (see orgqr), 5 getrs, 2 getrf and 1 geqrf, all at n <= 8 where a call takes 20-60 us.
Three more cells are excluded by hand: orgqr cdouble 1024x128 b8192 (48 GB), geqrf cdouble 1024x128 b8192 (32 GB) and orgqr cdouble 512x64 b16384 (24 GB). They oversubscribe the card and measure UVM paging, and their rel_sd (0.001-0.008) passes the variance gate.
Three batches per order: half, nominal and double the SAT_LADDER value of docs/perf/lu.md. The tables quote the top batch of each ladder. A ~ marks a cell whose time moved more than 5% between the nominal and top batch. Cells read ratio (native route).
Warning: a ~ cell is not saturated. Its number is the best reading available, not a converged one. ~ is common here; see Saturation.
| n (batch) | float | cfloat | double | cdouble |
|---|---|---|---|---|
| 4 (b32768) | 1.94 (cta) | 1.14~ (cta) | 0.86 (cta) | 0.97 (cta) |
| 8 (b32768) | 2.10 (cta) | 1.21~ (cta) | 1.07 (cta) | 1.36 (cta) |
| 16 (b32768) | 1.64 (cta) | 0.66~ (cta) | 0.63 (cta) | 0.72 (cta) |
| 17 (b32768) | 2.70 (cta) | 1.47~ (cta) | 0.94 (cta) | 1.53 (cta) |
| 24 (b32768) | 2.07~ (cta) | 1.38~ (cta) | 0.96 (cta) | 1.43 (cta) |
| 32 (b16384) | 1.30~ (cta) | 0.81 (cta) | 0.81 (cta) | 1.01 (cta) |
| 33 (b16384) | 1.84~ (cta) | 1.30~ (cta) | 1.08 (cta) | 1.55 (cta) |
| 48 (b16384) | 1.08~ (cta) | 0.79~ (cta) | 0.76~ (cta) | 0.77 (cta) |
| 64 (b16384) | 0.95~ (cta) | 0.52~ (cta) | 0.54 (cta) | 0.59 (cta) |
| 65 (b8192) | 1.21~ (cta) | 0.72~ (cta) | 1.06 (cta) | 1.29 (cta) |
| 96 (b8192) | 0.63~ (cta) | 0.45~ (cta) | 0.52 (cta) | 0.51 (blocked) |
| 128 (b8192) | 0.43~ (cta) | 0.53~ (blocked) | 0.46 (blocked) | 0.58 (blocked) |
| 256 (b4096) | 0.85~ (blocked) | 0.92~ (blocked) | 0.83 (blocked) | 0.82 (blocked) |
| 512 (b1024) | 1.19~ (blocked) | 1.08~ (blocked) | 0.99 (blocked) | 0.90 (blocked) |
| n (batch) | float | cfloat | double | cdouble |
|---|---|---|---|---|
| 4 (b32768) | 0.21~ (cta) | dropped | 0.07~ (cta) | 0.06~ (cta) |
| 32 (b16384) | 0.55 (cta) | 0.55~ (cta) | 0.29~ (cta) | 0.41 (cta) |
| 33 (b16384) | 1.13 (cta) | 1.06 (cta) | 0.20 (blocked) | 0.50 (cta) |
| 64 (b16384) | 1.63 (cta) | 0.82 (cta) | 0.31 (blocked) | 0.43 (cta) |
| 65 (b8192) | 0.83 (cta) | 0.62 (cta) | 0.18~ (blocked) | 0.32 (cta) |
| 128 (b8192) | 0.72 (cta) | 0.46~ (blocked) | 0.28~ (blocked) | 0.40 (blocked) |
| 192 (b4096) | 0.91~ (blocked) | 0.79~ (blocked) | 0.52~ (blocked) | 0.50 (blocked) |
| 256 (b4096) | 1.19~ (blocked) | 1.06 (blocked) | 0.68~ (blocked) | 0.57 (blocked) |
| 512 (b1024) | 2.17~ (blocked) | 1.60~ (blocked) | 0.75~ (blocked) | 0.74 (blocked) |
Full grid: benchmarks/results/factor_baseline_getrf_<type>.csv.
| shape (batch) | float | cfloat | double | cdouble |
|---|---|---|---|---|
| 8 (b32768) | 0.08~ (cta) | dropped | 0.02~ (cta) | 0.04~ (cta) |
| 32 (b16384) | 0.73~ (cta) | 0.62~ (cta) | 0.21 (cta) | 0.33 (cta) |
| 33 (b16384) | 0.76~ (cta) | 0.69~ (cta) | 0.22 (cta) | 0.34 (cta) |
| 48 (b16384) | 1.02~ (cta) | 1.74~ (cta) | 0.32~ (cta) | 0.45 (cta) |
| 64 (b16384) | 1.71~ (cta) | 2.72 (cta) | 0.58 (blocked) | 0.52 (cta) |
| 65 (b8192) | 2.46~ (cta) | 2.31 (cta) | 0.66~ (blocked) | 0.57 (cta) |
| 96 (b8192) | 2.69~ (cta) | 2.28 (cta) | 1.16~ (blocked) | 0.49 (blocked) |
| 128 (b8192) | 3.64~ (blocked) | 3.79 (blocked) | 1.76~ (blocked) | 0.68 (blocked) |
| 192 (b4096) | 5.38~ (blocked) | 5.71~ (blocked) | 2.47 (blocked) | 1.06 (blocked) |
| 512 (b1024) | 13.38~ (blocked) | 10.52~ (blocked) | 4.96~ (blocked) | 1.89~ (blocked) |
| 128x32 (b16384) | 2.23~ (cta) | 3.79~ (cta) | 0.68~ (cta) | 0.68 (cta) |
| 512x32 (b16384) | 2.68 (cta) | 3.39~ (blocked) | 1.58 (blocked) | 2.16 (blocked) |
| 512x64 (b16384) | 3.18 (blocked) | 4.10~ (blocked) | 2.37 (blocked) | 1.73 (blocked) |
1024x128 (b8192) is excluded: cdouble reads 1.691 there and is paging (2.956 at b4096 and 2.130 at b2048 are kept). Full grid: benchmarks/results/factor_baseline_geqrf_<type>.csv.
The vendor arm is a per-item cusolverDnXorgqr loop on an out-of-order sub-queue (the batch > 1 branch of orgqr_vendor in src/backends/cublas.cc), not a batched routine.
Note: every orgqr ratio means "beats the per-item loop". None means "beats cuSOLVER".
| shape (batch) | float | cfloat | double | cdouble |
|---|---|---|---|---|
| 16 (b32768) | 209.57 | 172.40 | 97.72 | 42.25 |
| 33 (b16384) | 60.53 | 33.93 | 59.45~ | 20.27 |
| 64 (b16384) | 27.10~ | 15.09~ | 27.09 | 11.31 |
| 128 (b8192) | 9.47 | 5.29 | 12.27 | 6.57~ |
| 256 (b4096) | 5.30 | 4.04 | 8.46 | 4.75 |
| 512 (b1024) | 3.64 | 2.54 | 4.61 | 2.77 |
| 512x64 (b16384) | 2.71 | 1.95 | 4.93 | excluded (2.71 at b8192) |
| 1024x128 (b8192) | 1.73 | 1.63 | 2.91 | excluded (2.72 at b4096) |
Full grid, including the *dropped* small-n cells: benchmarks/results/factor_baseline_orgqr_<type>.csv.
Two right-hand-side widths: nrhs <= 2 (route-era clause A, every type) and nrhs = 4 (clause B, float only). Both carried live traffic when this grid was measured.
Today getrs chooses on sm_89 from tuned/getrs.sm_89.txt (transcribed at 424a45bc, untimed). cta ranks first for n >= 32 with nrhs <= 2 (every type) and nrhs 3-4 (float). vendor ranks first everywhere else: n=31 nrhs=1 is vendor-first, n=32 nrhs=1 is cta-first, and n=32 nrhs=3 is cta-first for float and vendor-first for the other three types.
| n (batch) | float | cfloat | double | cdouble |
|---|---|---|---|---|
| nrhs = 1 | ||||
| 4 (b32768) | 0.71~ | 0.59~ | 0.52~ | 0.23~ |
| 8 (b32768) | 1.12 | 0.69~ | 0.98~ | 0.41~ |
| 16 (b32768) | 1.04~ | 3.76 | 0.99 | 2.00 |
| 17 (b32768) | 0.76~ | 1.38 | 0.92 | 2.04 |
| 32 (b16384) | 2.29 | 1.40~ | 3.86 | 2.17 |
| 64 (b16384) | 1.65~ | 1.25 | 3.95~ | 2.66 |
| 512 (b1024) | 1.73~ | 1.35~ | 1.73~ | 1.60~ |
| nrhs = 4 | ||||
| 4 (b32768) | 0.45~ | 0.35~ | 0.12~ | 0.07~ |
| 32 (b16384) | 1.30 | 1.03~ | 0.85 | 0.58 |
| 512 (b1024) | 1.53~ | 1.27~ | 2.75~ | 2.14 |
Bold marks a cell inside a shipped clause that loses. Every bold cell is at n <= 24, below the floor. Full grid: benchmarks/results/factor_baseline_getrs_<type>.csv.
The floor is 32. This table reads each order at the worst rung of its batch ladder, the statistic that shows whether a clause has a loss anywhere inside it (nrhs = 1):
| T | n=4 | n=8 | n=16 | n=17 | n=24 | n=32 | n=48 |
|---|---|---|---|---|---|---|---|
| float | 0.71 | 1.12 | 1.04 | 0.76 | 0.95 | 2.29 | 1.94 |
| cfloat | 0.59 | 0.69 | 3.76 | 1.38 | 1.27 | 1.40 | 1.60 |
| double | 0.52 | 0.98 | 0.99 | 0.87 | 3.66 | 3.77 | 3.74 |
| cdouble | 0.23 | 0.41 | 2.00 | 2.02 | 1.93 | 2.13 | 2.46 |
Mechanism. The fused kernel gives one work-group to a matrix whose solve is a few dozen flops, so the work-group is the cost; cublas?getrsBatched has no such floor. Full write-up: getrs order floor evidence in docs/perf/lu.md.
The SAT_LADDER inherited from docs/perf/lu.md does not saturate these ops. Of 442 cells with a clean nominal and top rung, 201 (45%) moved more than 5% on the last doubling. The cause is not statistical. At potrf float n >= 96 the native arm is linear in batch and the vendor arm is superlinear (n = 128: native x1.99 per doubling, cuSOLVER x2.24-2.35; n = 512: x2.08-2.10 against x2.32-2.36), so the ratio has no fixed point inside the ladder.
Extensions past the ladder, one process per cell:
| cell | ladder | extension | verdict |
|---|---|---|---|
| potrf float 128 | 0.322 / 0.363 / 0.429 | 0.457 (b16384), 0.466 (b32768) | saturated at b32768, +2.0% on the last doubling |
| potrf float 256 | 0.620 / 0.687 / 0.851 | 0.916 (b8192) | still +7.6%, not saturated |
| potrf float 512 | 0.948 / 1.045 / 1.187 | 1.330 (b2048), 1.503 (b4096) | still +13%, not saturated, rising |
| geqrf float 128 | 3.175 / 3.322 / 3.643 | 3.618 (b16384) | saturated at b16384, -0.7% |
| geqrf float 512 | 26.279 / 17.364 / 13.376 | 10.98 (b2048) | still -18%, not saturated, falling |
Quote potrf float 512 as ">= 1.19 and rising" and geqrf float 512 as "<= 13.4 and falling", not as point estimates. Neither converges inside 24 GB.
Two small-order effects also affect ~ cells: the potrf CTA arm steps superlinearly when the batch's working set crosses the 72 MB L2 (float n = 32 at b16384 reads x2.51 against x1.77 one rung below), and below n ~ 16 both arms are launch-bound.
Of 66 cells with a per-cell number in docs/perf/{potrf,lu,qr}.md, 43 reproduce within 15% and 23 do not. Every disagreement is at the prior's own batch. The vendor arm reproduces every vendor time the record states to three significant figures, so no disagreement is the vendor moving.
| op | type | n | batch | prior | now | delta | explanation |
|---|---|---|---|---|---|---|---|
| geqrf | float | 128 | 4096 | 2.01 | 3.32 | +65% | tier. The prior is the CTA time; the shipped default is Blocked |
| geqrf | float | 256 | 2048 | 5.62 | 7.39 | +32% | unexplained (blocked driver) |
| orgqr | float | 32 | 8192 | 123.2 | 52.9 | -57% | vendor per-item loop ~2.3x cheaper per item |
| potrf | float | 8 | 8192* | 2.75 | 2.08 | -24% | see The potrf n = 8 finding |
* The prior's batch (4096) is not on this ladder; the nearest rung (8192) is used.
The other geqrf rows (+22% to +77%, n = 128..512) are unexplained; the orgqr cdouble rows at 256 and 512 move the other way (+37%, +62%).
geqrf n = 128 is a tier mismatch, and it is exact. Pinned: cta 15.419 ms, blocked 9.349 ms, vendor 31.024 ms (float). docs/perf/qr.md records "30.9 -> 15.4 ms (2.01x)"; its native number is the CTA time, while its own tier table gives the default at that cell as blocked, 9.97 ms. The float form=sq n=128 row of tuned/geqrf.sm_89.txt ranks tiny (which cannot run at this order), then blocked, then cta, then vendor.
The other geqrf cells (+22% to +77%) are not explained. Ruled out: the tier, the vendor arm (it reproduces exactly), and the trailing GEMM route (forcing BATCHLAS_GEMM_ROUTE=vendor moves the native arm by 0.1% at n = 128 / 256 / 512). Left: the blocked driver itself, which runs 1.31x faster at n = 256 and 1.43x faster at n = 512 than order.csv records. Not isolated further.
orgqr's vendor arm halved at n <= 256 and did not move at n = 512. That is the signature of a per-item launch cost: about 54 us per item at n = 32, against 486 us at n = 512, where each item's own kernel hides it. Driver 595.84 is a candidate, but this run cannot test it.
docs/perf/potrf.md records 2.75x at float n = 8, batch 4096, from the CTA-vs-cuSOLVER table in experiments/wp4_potrf/README.md §4. This grid reads 2.08-2.18x. The number does not reproduce on any instrument, card or pin.
| instrument | card | vendor ms | native ms | ratio |
|---|---|---|---|---|
| archived realpotrf.cpp, BATCHLAS_POTRF_ROUTE pinned | GPU 1 | 0.0284 | 0.0130 | 2.18 |
| factor_bench, interleaved | GPU 1 | 0.0255 | 0.0121 | 2.10 |
| factor_bench, the grid, b8192 / 16384 / 32768 | GPU 1 | 0.0395 / 0.0588 / 0.1024 | 0.0190 / 0.0284 / 0.0488 | 2.08 / 2.07 / 2.10 |
Ruled out:
The cell is dispatch-bound: at n = 8, batch 4096, more than 80% of both arms is per-call launch latency (DRAM roof 2.2 us, native 13 us, vendor 28 us).
No raw data backs the prior. experiments/wp4_potrf/ at the evidence tag has no CTA-vs-cuSOLVER grid. The n = 8 and n = 16 rows of that table should be replaced by the numbers on this page; the rest reproduces.
The harness warms up on a time budget (WARM_S per arm) and discards the warm-up reps. A cold first run (SYCL JIT plus cold clocks) fabricated a 3.7x result in this repository.
The warm-up runs in the same arm order as the timed loop. With a per-arm warm-up, the first timed rep of arm 0 came in 2.2x slow every time (0.0567 ms against a steady 0.0261 at potrf float n = 8) and tripped the rel_sd gate on a stable cell.
--arms= splits on commas exactly and rejects an empty list. (It once matched by substring, so --arms=vendor silently dropped the native arm.) Each arm name is its own route pin, recorded in the CSV pin_parsed column, and the residual and pivot gate runs per arm.
An arm whose pin cannot run the shape throws std::invalid_argument before anything is timed. For example, BATCHLAS_GETRF_ROUTE=tiny at cdouble n = 32 throws (src/ops/getrf/getrf.cc:48).
| what | path |
|---|---|
| the 20 grids, every row including the discarded ones | benchmarks/results/factor_baseline_<op>_<type>.csv |
| the harness, ladder driver and GPU guard | benchmarks/factor_bench.cc, benchmarks/run_factor_grid.sh, benchmarks/gpu_guard.sh |
| the archived potrf n = 8 instrument | git show perf-evidence/vendor-independence:experiments/wp4_potrf/phase2_ab/realpotrf.cpp |
| the prior tables this page compares against | docs/perf/potrf.md §"CTA kernel measured against cuSOLVER", docs/perf/qr.md §"geqrf order and batch grid" and §"orgqr grid", docs/perf/lu.md §"Negative results" 4 |
The saturation extensions, tier pins and GEMM-route probe are not in the CSVs; their times are quoted inline.