Cover the type-distinct LU paths in the precompile workload - #117
Merged
ChrisRackauckas merged 1 commit intoAug 9, 2026
Merged
Conversation
`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
marked this pull request as ready for review
August 9, 2026 09:26
ChrisRackauckas
merged commit Aug 9, 2026
22007f5
into
JuliaLinearAlgebra:master
20 of 21 checks passed
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.
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 100x100Matrix{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 theNotIPIVbacksolve, 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
@elapsedof 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).lu!(A), n = 8 (stdlib generic fallback)lu!(A), n = 20 (_generic_lufact!)lu!(A, Val(false)), n = 20lu!(A), n = 100 (recursive kernel)lu!(A, Val(false)), n = 100lu!(A, Val(true), Val(true)), n = 100lu(A), n = 100ldiv!(F::LU{..,NotIPIV}, b::Vector)ldiv!(F::LU{..,NotIPIV}, B::Matrix)ldiv!(F::LU pivoted, b::Vector)🦋solve!(🦋workspace(A, b), Val(false))The two
ldiv!rows forNotIPIVare 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 theldiv!lines here is roughly 0.02-0.11 s, not 0.3-3.7 s. Thelu!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.
@elapsed using RecursiveFactorization(min of 5)lu!(rand(2,2))(master)(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):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'sdefaultalgdispatches its RFLU band oneltype <: Union{Float32, Float64}, andButterflyFactorizationand the 32-bit mixed-precision algorithms are first-class) to be worth it. The lever if you disagree isfor T in (Float64,), which is the 30.1 s row and roughly halves the load-time delta.Verification
Same suite on the
[compat]floor: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'sreport_invalidationson 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
typosreports 6 pre-existing hits onsizintest/runtests.jl; untouched here, and this repo has notyposCI job.Pkg.test()precompiles again under--check-bounds=yes, so the +31 s is paid twice in a test run.🤖 Generated with Claude Code