Skip to content

Cover the type-distinct LU paths in the precompile workload - #117

Merged
ChrisRackauckas merged 1 commit into
JuliaLinearAlgebra:masterfrom
ChrisRackauckas-Claude:precompile-workload
Aug 9, 2026
Merged

Cover the type-distinct LU paths in the precompile workload#117
ChrisRackauckas merged 1 commit into
JuliaLinearAlgebra:masterfrom
ChrisRackauckas-Claude:precompile-workload

Conversation

@ChrisRackauckas-Claude

@ChrisRackauckas-Claude ChrisRackauckas-Claude commented Aug 9, 2026

Copy link
Copy Markdown

⚠️ Draft — please ignore until reviewed by @ChrisRackauckas.

The workload was lu!(rand(2, 2)). That is a better workload than it looks — inference is whole-body, so it reaches the recursive kernel, reckernel!, schur_complement! and the TriangularSolve legs even though a 2x2 never executes them. Measured: lu! on a 100x100 Matrix{Float64} already had a 1.2e-4 s first call on master.

What one call cannot reach is anything that changes a type. Float32, Val(false) pivoting, Val(true) threading, a matrix right-hand side through the NotIPIV backsolve, and the butterfly solve were each compiled from scratch on first use, costing up to 11.6 s.

Coverage, before and after

TTFX = in-process @elapsed of the first call in a fresh process, min over 5 processes, Julia 1.12.6, TriangularSolve 0.2.5 from the registry in both columns. Before = merge-base (identical to registered 0.2.29).

Entry point before F64 after F64 before F32 after F32
lu!(A), n = 8 (stdlib generic fallback) 0.00004 s 0.00006 s 8.579 s 0.00006 s
lu!(A), n = 20 (_generic_lufact!) 0.00006 s 0.00007 s 8.862 s 0.00008 s
lu!(A, Val(false)), n = 20 0.423 s 0.00012 s 7.845 s 0.00015 s
lu!(A), n = 100 (recursive kernel) 0.00012 s 0.00016 s 9.663 s 0.00018 s
lu!(A, Val(false)), n = 100 0.407 s 0.00022 s 8.960 s 0.00024 s
lu!(A, Val(true), Val(true)), n = 100 3.684 s 0.00027 s 11.614 s 0.00026 s
lu(A), n = 100 0.020 s 0.00019 s 7.503 s 0.00018 s
ldiv!(F::LU{..,NotIPIV}, b::Vector) 0.261 s 0.00005 s 0.323 s 0.00011 s
ldiv!(F::LU{..,NotIPIV}, B::Matrix) 3.105 s 0.00009 s 3.667 s 0.00013 s
ldiv!(F::LU pivoted, b::Vector) 0.0045 s 0.0052 s 0.074 s 0.00019 s
🦋solve!(🦋workspace(A, b), Val(false)) 2.090 s 0.00017 s 8.308 s 0.00015 s

The two ldiv! rows for NotIPIV are the legs that #116 routed onto TriangularSolve's native kernels; the matrix-RHS one was the largest uncovered Float64 gap in the package (3.1 s).

Overlap disclosure: both columns above pin TriangularSolve at the registered 0.2.5, which precompiles nothing. JuliaSIMD/TriangularSolve.jl#49 adds a workload there, and it closes most of those two ldiv! rows on its own — with RecursiveFactorization held at 0.2.29 and only TriangularSolve swapped, the Float64 rows go 0.261 s to 0.022 s and 3.105 s to 0.030 s. So if that PR lands first, the marginal gain from the ldiv! lines here is roughly 0.02-0.11 s, not 0.3-3.7 s. The lu! rows, which are the bulk of this PR, are unaffected by it.

ldiv! on a pivoted factorization is unchanged for Float64 (0.0045 vs 0.0052 s is run-to-run noise on an already-cached stdlib LAPACK path) and is the one row in the table whose Float64 entry this PR does not improve; it is here for the Float32 column.

Cost

Package precompile wall time, min over 3 alternating off/on rounds on a loaded 128-core box, so an upper bound on the delta.

workload precompile pkgimage @elapsed using RecursiveFactorization (min of 5)
lu!(rand(2,2)) (master) 12.4 s 7.25 MB 0.737 s
Float64 only, incl. butterfly 30.1 s 12.5 MB
this PR minus butterfly 54.0 s 19.1 MB
this PR 43.2 s 24.0 MB 1.128 s

(The "minus butterfly" row was measured under heavier load than the full row; read it only as "butterfly is roughly +7 s and +4.9 MB", which is repaid by its 2.1 s / 8.3 s TTFX.)

On a genuinely cold JULIA_DEPOT_PATH (39 dependencies, nothing cached):

$ JULIA_DEPOT_PATH=<fresh> julia +1.12 --project=<fresh> -e 'using Pkg; Pkg.develop(path=...); Pkg.instantiate(); Pkg.precompile()'
master:   11705.8 ms ✓ RecursiveFactorization — 39 dependencies successfully precompiled in 75 seconds
this PR:  38697.4 ms ✓ RecursiveFactorization — 39 dependencies successfully precompiled in 101 seconds

So: +31 s precompile, once per depot/version; +0.39 s load, per process; up to −11.6 s on the first factorization, per process.

Nothing was dropped as not paying for itself, but the honest downside is worth stating plainly: a user who only ever runs Float64, pivoted, serial lu! with vector right-hand sides was already fully covered on master, and for them this is a pure +0.39 s load-time regression. The judgment is that the covered set — Float32 anywhere, NoPivot, threading, matrix RHS, butterfly — is broad enough in the SciML stack (LinearSolve's defaultalg dispatches its RFLU band on eltype <: Union{Float32, Float64}, and ButterflyFactorization and the 32-bit mixed-precision algorithms are first-class) to be worth it. The lever if you disagree is for T in (Float64,), which is the 30.1 s row and roughly halves the load-time delta.

Verification

$ julia +1.12 --project=. -e 'using Pkg; Pkg.test()'
Test Summary:         | Pass  Total     Time
Test LU factorization | 3120   3120  7m27.2s
NoPivot lu! with a user-supplied ipiv leaves valid pivots |    6      6  3.0s
NotIPIV backsolves stay on TriangularSolve's native kernels |   38     38  0.0s
NotIPIV ldiv! correctness across small-to-large sizes |   24     24  0.1s
🦋            |   21     21  9.3s
Factorization panel solves stay on TriangularSolve |   33     33  0.3s
     Testing RecursiveFactorization tests passed

Same suite on the [compat] floor:

$ julia +1.10 --project=. -e 'using Pkg; Pkg.instantiate(); Pkg.test()'
Test LU factorization | 3120   3120  3m20.0s
NoPivot lu! with a user-supplied ipiv leaves valid pivots |    6      6  1.6s
NotIPIV backsolves stay on TriangularSolve's native kernels |   38     38  0.0s
NotIPIV ldiv! correctness across small-to-large sizes |   24     24  0.2s
🦋            |   21     21  2.1s
Factorization panel solves stay on TriangularSolve |   33     33  0.1s
     Testing RecursiveFactorization tests passed

Formatting: format_text(src, SciMLStyle()) is a fixed point on the changed file.

CI

20 of 21 checks green (Linux/macOS/Windows x Julia 1 and 1.x, plus codecov). The one red check is Invalidations / evaluate, which is pre-existing and unrelated: it has failed on every branch in this repo since 2025-12-15, inside SnoopCompile's report_invalidations on Julia 1.12, not on an invalidation regression. #115 proposes the fix (pin the workflow to 1.11) and was closed unmerged; that belongs in its own PR, not this one.

Not verified

  • All timings are from 1.12.6 only; 1.10 was verified for correctness, not for TTFX or precompile cost.
  • typos reports 6 pre-existing hits on siz in test/runtests.jl; untouched here, and this repo has no typos CI job.
  • Downstream effect on LinearSolve's own precompile time.
  • Note for CI budgeting: Pkg.test() precompiles again under --check-bounds=yes, so the +31 s is paid twice in a test run.

🤖 Generated with Claude Code

`lu!(rand(2, 2))` reaches only the Float64, pivoted, serial stack. Inference
is whole-body, so that one call does cover the recursive kernel and the
TriangularSolve legs it dispatches to; what it cannot reach is anything that
changes a type. Float32, `Val(false)` pivoting, `Val(true)` threading, a
matrix right-hand side through the NotIPIV backsolve, and the butterfly
solve were each recompiled from scratch on first use.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
@ChrisRackauckas
ChrisRackauckas marked this pull request as ready for review August 9, 2026 09:26
@ChrisRackauckas
ChrisRackauckas merged commit 22007f5 into JuliaLinearAlgebra:master Aug 9, 2026
20 of 21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants