Add native left-upper ldiv! and right-lower rdiv! kernels - #41
Merged
ChrisRackauckas merged 4 commits intoAug 8, 2026
Conversation
Implements the backward-substitution dual of the existing right-upper rdiv! family: native right-lower division C = A / L (column blocks solved last-to-first), from which the left-upper solve is obtained for free via U \ B = (B' / U')', mirroring how the left-lower ldiv! reuses the right-upper kernels. Column-major pointers use the rdiv_L!/solve_AL kernels; transposed (row-major) pointers dispatch to the _ldiv_U! family, so ldiv!(::UpperTriangular, B) hits SIMD kernels rather than the LinearAlgebra fallback. All loads and stores use ascending positive-stride indices; only block offsets descend. Supports packed LU parents (reads only the wrapper-indicated triangle), Unit variants, 2- and 3-arg forms, and the Val thread flag, with the same blocking and Polyester threading structure as the existing paths. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
New public methods (upper ldiv!, lower rdiv!) are purely additive, so this stays within the 0.2 compat range downstream packages already declare. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Two fixes from self-review of the new backward-substitution family: - The 16 new ldiv!/rdiv! methods now require StridedMatrix storage (both the triangular parent and the right-hand side), so non-strided AbstractMatrix inputs (sparse, structured wrappers) keep hitting the LinearAlgebra catch-all that served them before, instead of failing in stridedpointer_preserve. - div_dispatch_L! throws DimensionMismatch on shape mismatches instead of reading out of bounds of the triangular parent. The pre-existing forward-substitution dispatch has the same latent gap; left untouched here to avoid changing existing behavior, flagged for follow-up. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Author
|
The
The action would need to unpin SnoopCompile (4.x handles binding roots) or the workflow could run the snippet directly; leaving that infra call to you rather than bundling a workflow edit into this PR. The actual test jobs (Julia 1 and nightly, 1 and 3 threads) are green. |
Member
|
Since this is just a mirror kernel, merging as it's already close to optimal. I'll get all downstream, but there will be an optimization coming. |
ChrisRackauckas
marked this pull request as ready for review
August 8, 2026 07:32
This was referenced Aug 8, 2026
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.
Add native left-upper
ldiv!and right-lowerrdiv!kernelsImplements the two missing side/triangle combinations:
ldiv!(::UpperTriangular, A)(A ← U \ A) andrdiv!(A, ::LowerTriangular)(A ← A / L), plus theirUnitvariants and 3-arg forms. Previouslyldiv!(UpperTriangular(F), B)silently fell through the catch-all toLinearAlgebra.ldiv!, which is the multi-RHS RFLU backsolve gap behind SciML/LinearSolve.jl#1146 (worked around by anO(n²)-materializing exchange-matrix path in SciML/LinearSolve.jl#1152).Design
The library is formulated in terms of right-division with forward substitution over column blocks (
C = A / U, upper-triangularU), with left-lowerldiv!obtained for free viaL \ A = (A' / L')'dispatching to row-major-specialized kernels (_ldiv_L!). This PR adds the exact dual: native right-lower divisionC = A / Lwith backward substitution (column blocks solved last-to-first), from which left-upperldiv!is obtained for free viaU \ B = (B' / U')'. Right-lower was chosen as the native family because it mirrors the existing code structure one-to-one — every new function is the backward twin of an existing one:solve_AUsolve_ALBdivU_small_kern[_u]!BdivL_small_kern[_u]!rdiv_solve_W[_u]!rdiv_solve_W[_u]_L!rdiv_U!(column-major)rdiv_L!(column-major)rdiv_U!(2,2-stride) →_ldiv_L!(ldiv_solve_W*,ldiv_remainder!)rdiv_L!(2,2-stride) →_ldiv_U!(uldiv_solve_W*,uldiv_remainder!)nmuladd!,rdiv_block_N!,rdiv_block_MandN!,RDivBlockMandNv2,multithread_rdiv!,div_dispatch!nmuladd_L!,rdiv_block_N_L!,rdiv_block_MandN_L!,RDivBlockMandNv2L,multithread_rdiv_L!,div_dispatch_L!Key points:
J-materialization. Only block offsets descend; every SIMD load/store is a contiguous ascending index range. Then-column remainder (N % W) lives at the trailing columns[N-Nr, N)and is solved first (it depends on nothing); fullW/W*UFblocks then walk down with their reduction running over the already-solved trailing columns[n+W, N)viaSafeCloseOpen(n + W, N).solve_ALreads the diagonal block's lower triangle only), soUpperTriangular(F)from a packed LU whose lower triangle holdsLworks.solve_ALissolve_AUwith the intra-block loop reversed; the transposed family reusestranspose_vecunrolland the sameUnrollload/store patterns, unroll factors, masking, andPolyester.batchthreading (m-partition) as the forward code.Npath (N > block_size) mirrorsrdiv_block_N!: solve the rightmost column block, then for each block leftward do an@turbonmuladd_L!update against all solved trailing columns before its triangular solve.Float32/Float64still falls back toLinearAlgebra(tested withBigFloat).Verification
All run locally on Julia 1.12.4 and 1.10.11, AMD EPYC 7502 (Zen 2, AVX2, 2×32 cores), OpenBLAS with 1 thread for comparisons.
Full test suite (existing tests + new upper-
ldiv!/lower-rdiv!cases in the same NaN-box style, packed-LU factors up to n=512, dispatch checks, allocation checks):plus Aqua and
detect_ambiguitiesclean on both.Failing-before / passing-after (the correctness tests alone would pass on master via the
LinearAlgebrafallback, so the discriminating test asserts dispatch reaches a native method — the issue's first success criterion):On unmodified master:
With this PR:
Extended sweep (beyond the test suite): n ∈ 1..33, 47..49, 63..65, 100, 127..129, 200, 255..257, 400; nrhs ∈ {1,2,3,4,5,7,8,11,16,31,32}; Float32/Float64; unit/non-unit;
Val(true)/Val(false); 2-arg/3-arg; packedlu!parents to n=512; threaded partition path with M up to 2003: 0 failures. Steady-state allocations: 0 (serial and threaded, including onRecursiveFactorization.lu!factors).Benchmarks
Solve-only, Float64,
BLAS.set_num_threads(1),@belapsedwithsetup=(X = copy(B)), pinned to 8 cores.J-matemulates the LinearSolve.jl#1152 workaround (materialize reversedUas lower-triangular + reversed lower solve).Median BLAS/TS-serial = 2.29× for n ∈ [64, 500], nrhs ∈ {4, 8} (acceptance bar was ≥1.3×); median vs the J-materialize path 3.17×, with zero extra allocation.
(full grid in the bench script; all cells 1.58–3.63× vs BLAS)
No regression on existing kernels (main vs this branch, serial, μs; differences are within run-to-run noise, and the diff is purely additive):
* re-ran 3×: main 6647–6763 μs vs branch 6727–6936 μs, overlapping ranges.
What was not verified
@nifremainder cases) are exercised here, but wider-vector unroll counts (e.g. W=8 Float64, UF from a 32-register file) only via CI.versioninfo(): Julia 1.12.4, LLVM 18.1.7 znver2; also tested on 1.10.11.Reviewer notes
W*UF-unrolled transposed kerneluldiv_solve_W_u!generates its intra-block backward loop manually (descending sub-block order with cross-sub-block updates) rather than viaBase.Cartesian.@nexprs; the forwardldiv_solve_W_u!could use@nexprsbecause its dependencies run ascending.rdiv_solve_W[_u]_L!anduldiv_solve_W[_u]!take an extranendargument relative to their forward twins: backward blocks reduce over the trailing solved columns[n+W, N), so the kernels need the total column count, where forward kernels reduce over[0, n).StridedMatrixstorage (parent and RHS), so non-stridedAbstractMatrixinputs (sparse, structured wrappers) keep hitting theLinearAlgebracatch-all that served them before, anddiv_dispatch_L!throwsDimensionMismatchon shape mismatches instead of reading out of bounds. Both came out of an adversarial self-review; the pre-existing forward-substitution family has the same two latent hazards on its own signatures (bareAbstractMatrixdispatch, no shape validation) — left untouched here to avoid changing existing behavior, but worth the same treatment as a follow-up if you agree.compat = "0.2"users'ldiv!(::UpperTriangular, ·)calls will switch from the fallback's single-threaded BLAS numerics to these (default-threaded) kernels on upgrade. If you prefer that behavior change to gate on 0.3.0 instead, say so and I'll adjust.MWE for the LinearSolve integration (run, output shown):