Replace the vector-RHS BLAS deferral with a rank-4 pure-Julia sweep (never-BLAS vectors) - #48
Merged
ChrisRackauckas merged 2 commits intoAug 8, 2026
Conversation
Vector ldiv! previously ran the matrix kernels' M == 1 scalar remainder up to n = 128 and deferred to LinearAlgebra (BLAS trsv) above. Both lose to a plain column-oriented substitution sweep that LLVM vectorizes: outer-unrolled rank-4, it beats the old kernel path by 1.35-3.4x and trsv by 1.14-7.4x at every measured size (n = 4..2000, both triangles, unit/non-unit, Float32/64, AVX2), so the vector path is now never-BLAS with no swap logic. Inner-product forms cover row-contiguous strided parents. Vector methods now also throw DimensionMismatch for all four wrappers (the Lower ones previously read out of bounds on mismatched sizes). Matrix paths are unchanged; a naive per-column sweep lost to the blocked SIMD kernels in 17 of 18 tiny-size cells, so no matrix swapping. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #48 +/- ##
==========================================
+ Coverage 94.69% 95.22% +0.52%
==========================================
Files 1 1
Lines 849 942 +93
==========================================
+ Hits 804 897 +93
Misses 45 45 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
ChrisRackauckas
marked this pull request as ready for review
August 8, 2026 20:30
ChrisRackauckas-Claude
pushed a commit
to ChrisRackauckas-Claude/RecursiveFactorization.jl
that referenced
this pull request
Aug 8, 2026
…r kernels TriangularSolve 0.2.5 (JuliaSIMD/TriangularSolve.jl#48) replaced its vector-entry BLAS deferral (native <= 128, trsv above) with a rank-4 pure-Julia sweep: never-BLAS and faster than trsv/getrs! at every size (measured 0.33-0.79x of getrs! and ~2x faster than the n-by-1-reshape workaround this branch previously used, 1 thread, Zen 2; allocation-free). Drop the reshape helper, call the vector entry directly, and raise the TriangularSolve compat floor to 0.2.5 so the vector legs can never silently defer to BLAS on older TriangularSolve. The dispatch audit now asserts the vector signatures resolve to the native vector kernel methods. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UGVN6qeNL2jGCtaYg1386X
ChrisRackauckas-Claude
pushed a commit
to ChrisRackauckas-Claude/LinearSolve.jl
that referenced
this pull request
Aug 8, 2026
…nels TriangularSolve 0.2.5 (JuliaSIMD/TriangularSolve.jl#48) made the vector ldiv! entry never-BLAS at every size and faster than getrs!/trsv (measured 0.33-0.79x of getrs!, 1 thread, Zen 2; allocation-free), so _rf_ldiv!'s vector method calls it directly instead of presenting the vector as an n-by-1 reshape onto the matrix kernels (~2x slower). Raise the TriangularSolve compat floor to 0.2.5 — below it the vector entry silently deferred large vectors to BLAS trsv, which the routing policy forbids. Supersedes the ext-local naive back-solve approach of SciML#1169. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UGVN6qeNL2jGCtaYg1386X
This was referenced Aug 8, 2026
ChrisRackauckas
added a commit
to SciML/LinearSolve.jl
that referenced
this pull request
Aug 9, 2026
…rash, factorization-cell audit (#1171) * RFLU backsolves: TriangularSolve-only routing, fix pivot=Val(false) crash The extension's backsolve consumed fact.ipiv even for pivot = Val(false), where RecursiveFactorization returns the caller-supplied ipiv unwritten (undefined memory): every RFLUFactorization(pivot = Val(false)) and RF32MixedLUFactorization(pivot = Val(false)) solve segfaulted in dlaswp (vector RHS, via LAPACK.getrs!) or threw BoundsError (matrix RHS, via _ipiv_rows!). _rf_ldiv! now takes the pivot flag from the algorithm type and never touches fact.ipiv when pivoting is off. Routing policy: wherever TriangularSolve has a native kernel (Float32/Float64, strided), both triangular legs run on TriangularSolve — never on a BLAS kernel. Vector right-hand sides and single-column matrices, which previously took LAPACK.getrs!, are presented to TriangularSolve as n-by-1 view-reshapes (allocation-free, preserving the zero-allocation re-solve contract); the n x 1 early-return in the matrix path is gone. Complex and non-strided types keep the stdlib path (TriangularSolve has no kernels there), with NoPivot solved through the triangular legs so no path reads unwritten pivots. RF32MixedLUFactorization shares _rf_ldiv! instead of plain ldiv!. Adds _ts_native_backsolve, a which()-based enforcement helper the test suite uses to fail if TriangularSolve dispatch ever lands these argument types on its LinearAlgebra catch-all again. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UGVN6qeNL2jGCtaYg1386X * Test RFLU backsolve routing: both pivots, all RHS shapes, dispatch audit Correctness for {vector, matrix, n-by-1} x {pivoted, NoPivot} x n in {8, 40, 300} (300 spans TriangularSolve's vector-entry cutoff) plus ComplexF64, and a which()-based dispatch audit asserting the extension's TriangularSolve-routing methods are selected and both triangular legs — including the n-by-1 view-reshape type the vector path actually passes — resolve to native TriangularSolve kernels, never its LinearAlgebra catch-all. The NoPivot cases crashed (segfault/BoundsError) before the extension fix. Re-enables the previously commented-out RF32MixedLUFactorization(pivot = Val(false)) test. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UGVN6qeNL2jGCtaYg1386X * Pin the RFLU factorization cells: ext-gated defaults, complex stays opt-in RFLU's factorization is RecursiveFactorization.lu!, dispatch-audited as BLAS-free for Float32/Float64 in RF's own suite; the complex panel solves are the one LAPACK cell (TriangularSolve has no complex kernels). Assert the two LinearSolve-side facts that keep that cell contained: the ext enables userecursivefactorization, and defaultalg never routes complex matrices to RFLU — so LAPACK panels stay reachable only by explicitly requesting the algorithm. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UGVN6qeNL2jGCtaYg1386X * RFLU vector backsolves: use TriangularSolve 0.2.5's native vector kernels TriangularSolve 0.2.5 (JuliaSIMD/TriangularSolve.jl#48) made the vector ldiv! entry never-BLAS at every size and faster than getrs!/trsv (measured 0.33-0.79x of getrs!, 1 thread, Zen 2; allocation-free), so _rf_ldiv!'s vector method calls it directly instead of presenting the vector as an n-by-1 reshape onto the matrix kernels (~2x slower). Raise the TriangularSolve compat floor to 0.2.5 — below it the vector entry silently deferred large vectors to BLAS trsv, which the routing policy forbids. Supersedes the ext-local naive back-solve approach of #1169. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UGVN6qeNL2jGCtaYg1386X --------- Co-authored-by: ChrisRackauckas-Claude <accounts@chrisrackauckas.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed and why
Vector right-hand sides now run a pure-Julia rank-4 column sweep (
_naive_vsolve_fwd!/_naive_vsolve_bwd!, plus inner-product forms for row-contiguous strided parents) instead of being routed into the matrix kernels belown = 128and deferred toLinearAlgebra/BLAStrsvabove it. TheVECTOR_RHS_CUTOFF = 128deferral is deleted: the vector path is now never-BLAS at every size, for all four wrapper types, Float32/Float64, 2- and 3-arg,Val(true/false). Matrix paths are untouched. Version bumped to 0.2.5 (ldiv!vector behavior above n = 128 changes from BLAS-backed to native; results differ only in floating-point rounding).The swap logic this PR was scoped to add ("naive vs existing kernels, pick per regime") turned out to be unnecessary: after benchmarking, the naive kernel wins every measured cell against both the existing kernels and BLAS, so per the "data decides" instruction the swap was deleted and all vectors route to the naive sweep.
Decision table
ldiv!→ naive sweep; no swap, no BLASabs(stride(A,1)) <= abs(stride(A,2))Kernel-form findings that set the final shape (AMD EPYC 7502, AVX2,
tasksetpinned, 1 BLAS thread, interleaved min-times):@simd ivdepon the inner axpy beats plain@inbounds(measurable at Float32) and beats an explicit@turboinner loop (e.g. F64 n=256 forward: 6.0 vs 8.9 µs).x) beats rank-2 by 10–35% and rank-1 by ~1.5–2x. It also eliminated a bimodal per-process code-placement slowdown that plagued the rank-1 form (F32 n=64 forward: 830 ns fast-mode vs 1300–1600 ns slow-mode across fresh processes, data-placement ruled out by within-process reallocation tests; rank-4: 526 ns in every process). Rank-8 not attempted (≤ ~10% theoretical headroom, 32-cycle dependency chain).x[j]store discipline is direction-specific and measured: elide forward (storing cost 1.7x at F64 n=16), store backward (eliding cost 1.7x at F32 n=64).Benchmarks
Median over 3 fresh pinned processes (
taskset -c 5,julia -t1 -O3,BLAS.set_num_threads(1)), interleaved min-of-31-passes per candidate, ns per solve.new= this PR'sTriangularSolve.ldiv!;ts_cur= the pre-PR sub-cutoff path (matrix drivers viadiv_dispatch!/div_dispatch_L!withstatic(1), i.e. what ran at n ≤ 128) extended to all n;ts_ub= same drivers with large-N blocking skipped;trsv=LinearAlgebra.ldiv!(OpenBLAS). Pre-PR public behavior wasts_curfor n ≤ 128 andtrsvabove.Float64
Float32
newwins all 80 cells against both comparators (min best-old/new = 1.35, min trsv/new = 1.14).Matrix-path regression spot-check (untouched code, must be unchanged)
Upper-
ldiv!, nrhs = 8, Float64, ns per solve, main → branch: n=64: 1500 → 1510; n=256: 20430 → 20010; n=1000: 290169 → 290179. Within noise, as expected — the diff does not touch matrix kernels.Tiny matrix RHS (naive rank-4 per column vs existing kernels, ns, 2 replicated processes)
Existing kernels win 17 of 18 cells (up to 6.6x); naive wins only (F32, 4, 2) by 30 ns. Matrix swapping rejected; matrix paths untouched.
Behavioral notes for review
Matrix(U) \ breferences atrtol = sqrt(eps(T)) * n.Val(thread)argument is now ignored for vector RHS (previously it only toggled internal driver configuration; threading never engaged at M = 1). BothVal(true)andVal(false)run the same single-threaded sweep; both are tested.ldiv!(Lower*/UnitLower*, b)with mismatched sizes previously did not throw below the cutoff (div_dispatch!has no size checks; it silently read out of bounds). All four wrappers now throwDimensionMismatchin both arities, matchingdiv_dispatch_L!and LinearAlgebra semantics. Tests added.muladdsweeps contract to FMA; diagonal division uses plain/(no fastmath approximations).Verification
All on this machine (AMD EPYC 7502, AVX2, Linux), at commit 30d74ad (the branch tip adds only a README sentence on top of it).
Full suite, Julia 1.12.4 —
julia --project=. -e 'using Pkg; Pkg.test()':Full suite, Julia 1.10.11 — temp env,
Pkg.develop(path=...); Pkg.test("TriangularSolve"):Both runs include the Aqua QA + ambiguity testsets. New tests: vector solves at n ∈ {1, 2, 5, 8, 16, 33, 64, 127, 128, 129, 200, 500, 1201} × 4 wrappers × F32/F64 ×
Val(true/false)× {2-arg, 3-arg, 3-arg-aliased}, packedlu!-factor vector solves at n ∈ {8..512}, non-contiguous strided views (step-2 parent and vector, 2- and 3-arg), direct tests of the inner-product kernels, vectorDimensionMismatchthrows for both triangles and arities, BigFloat andBidiagonal-parent LinearAlgebra fallbacks, and warm-in-loop zero-allocation assertions for 2- and 3-arg vector solves. Beyond the suite: an exhaustive n = 1:40 sweep (every rank-4 remainder case) × 4 wrappers × F32/F64 × both arities passed, plus n ∈ {127, 128, 129, 500, 1000, 2003}.typosover the diff: clean (one false positive on a git hash). JuliaFormatter v1 with the repo's.JuliaFormatter.tomlwas run; its churn on untouched code was reverted so the diff stays confined to the vector path (formatting-only sweep can be a separate PR). Added-line comment share: 17/254 = 6.7%.Not verified: AVX-512 and aarch64/Apple Silicon (all crossover measurements are AVX2-EPYC; the all-naive rule has no tunable constants to mis-transfer, but win margins there are unmeasured), MKL as BLAS backend, Windows/macOS, multi-threaded BLAS comparisons (deliberately pinned to 1 thread), and downstream packages (RecursiveFactorization.jl, LinearSolve.jl) against this branch.
Downstream
🤖 Generated with Claude Code