SupernodalLU: fix block factorization on the direct-BLAS backends - #1119
Conversation
`snlu` on a Mac was a `MethodError` for any matrix with a supernode at or
above `dense_threshold`:
MethodError: no method matching _lu_from_cacheval(::AppleAccelerateLUCache{...})
`_lu_from_cacheval` pulled the block factorization out of the dense cache's
cacheval, handling `LU` and `Tuple`. The direct-BLAS backends store neither:
Apple Accelerate, BLIS, and OpenBLAS each keep a mutable `*LUCache` holding
the factored matrix, the getrf pivots, and `info`, which
`_custom_cache_factorization` wraps in an `LU` only on demand. On a Mac the
*default* dense solver resolves to Apple Accelerate, so the default path fell
straight into the hole.
`_cache_lu!` reads exactly `.factors`/`.ipiv`, with the same LAPACK
semantics those caches use, so the cacheval is handed back as-is rather than
allocating a throwaway `LU` per block; anything exposing neither field now
gets a real error message instead of a `MethodError`.
`test/Core/supernodal_lu.jl` missed this because it loads
RecursiveFactorization, which makes the default resolve to RFLU (a `Tuple`
cacheval) everywhere. The new testset names the platform's direct-BLAS
backends explicitly, over both a caching-heavy and a caching-sparse
`dense_threshold`, and covers refactorization through the same caches.
Also version-gates two assertions in test/qa/allocations.jl. Above
`PANEL_BLAS_CUTOFF` the sweeps hand off to LinearAlgebra's `ldiv!`/`mul!`,
whose wrappers are not statically allocation-free on every release: 1.10 and
1.13 leave a `generic_trimatdiv!` dynamic dispatch plus boxed
`MulAddMul`/`SubArray` values that AllocCheck reports from stdlib frames, and
1.11 allocates them for real on the `SubArray`-matrix path the multi-RHS
sweep takes. Runtime allocation is 0 on 1.10, 1.12, and 1.13, so each
assertion is pinned to the versions that can carry it rather than dropped.
Verified on macOS aarch64: qa/allocations.jl and Core/supernodal_lu.jl pass
on 1.10, 1.11, and 1.12; runtime zero-allocation confirmed on 1.13. The new
testset fails with the original `MethodError` without the numeric.jl change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Ran this on Linux x86_64, which closes the caveat at the bottom of the description. Your version-driven explanation is right; it is not platform-driven. Same two matrices as the testset,
18 findings on 1.10 Linux is the same count you measured on 1.10 macOS aarch64, and 1.12 is clean on both. So the stdlib wrapper story holds and there is nothing Apple- or ARM-specific in it. Two follow-ups, one of which I think matters more than the version pinning: 1. The group/version pairing is the actual root cause of the [QA] versions = ["lts", "1"]
[AppleAccelerate] versions = ["lts", "1", "pre"]
Since 2. Every other cell in your table is a static-proof failure with runtime 0. That one is an actual allocation per multi-RHS solve, and The |
`test/runtests.jl` includes `qa/allocations.jl` from two groups, and they do not
cover the same Julia versions:
[QA] versions = ["lts", "1"]
[AppleAccelerate] versions = ["lts", "1", "pre"]
So AllocCheck ran on `pre` through a group that was never meant to carry it.
The file mixes two things with different portability. The direct-BLAS
refactorization proofs hold on every release - that contract is why the
AppleAccelerate group exists. The SupernodalLU sweep proof does not: above
`PANEL_BLAS_CUTOFF` the sweeps hand off to LinearAlgebra's `ldiv!`/`mul!`,
whose wrappers leave a `generic_trimatdiv!` dynamic dispatch plus boxed
`MulAddMul`/`SubArray` values that AllocCheck reports on 1.10 and 1.13. Only
the second one belongs behind QA's version set, so only it moves.
Measured, same two matrices as the testset:
Julia 1.10.11 x86_64-Linux 18 static findings runtime 0 B / 0 B
Julia 1.12.6 x86_64-Linux 0 static findings runtime 0 B / 0 B
18 findings on 1.10 x86_64-Linux is the same count SciML#1119 measured on 1.10
aarch64-Darwin, and 1.12 is clean on both, so this is the stdlib and the Julia
version - not the platform, and not this package's code. Worth noting the
coverage is narrower than the toml suggests: only one QA job materialises
(`QA (julia 1)`), so the static proof runs on 1.12 alone.
The static proof keeps a version guard, since that is a limit of the prover.
`RUNTIME_MULTIRHS_ZERO` is dropped: it gated a *runtime* allocation assertion,
and on 1.11 that assertion fails because the multi-RHS path really does
allocate (6144 B, per SciML#1119's table). Skipping it hides a regression rather
than a prover limitation, so it should fail there until the allocation is
fixed.
Verified: `qa/allocations.jl` completes on Julia 1.10, the version that was
failing; `GROUP=QA` and `GROUP=Core` both pass on 1.12, which is what CI runs.
Stacked on SciML#1119 - it carries the `_lu_from_cacheval` fix this needs.
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017rr42T1vFRRT8D7DMAmJzf
`test/runtests.jl` includes `qa/allocations.jl` from two groups, and they do not
cover the same Julia versions:
[QA] versions = ["lts", "1"]
[AppleAccelerate] versions = ["lts", "1", "pre"]
So AllocCheck ran on `pre` through a group that was never meant to carry it.
The file mixes two things with different portability. The direct-BLAS
refactorization proofs hold on every release - that contract is why the
AppleAccelerate group exists. The SupernodalLU sweep proof does not: above
`PANEL_BLAS_CUTOFF` the sweeps hand off to LinearAlgebra's `ldiv!`/`mul!`,
whose wrappers leave a `generic_trimatdiv!` dynamic dispatch plus boxed
`MulAddMul`/`SubArray` values that AllocCheck reports on 1.10 and 1.13. Only
the second one belongs behind QA's version set, so only it moves.
Measured, same two matrices as the testset:
Julia 1.10.11 x86_64-Linux 18 static findings runtime 0 B / 0 B
Julia 1.12.6 x86_64-Linux 0 static findings runtime 0 B / 0 B
18 findings on 1.10 x86_64-Linux is the same count #1119 measured on 1.10
aarch64-Darwin, and 1.12 is clean on both, so this is the stdlib and the Julia
version - not the platform, and not this package's code. Worth noting the
coverage is narrower than the toml suggests: only one QA job materialises
(`QA (julia 1)`), so the static proof runs on 1.12 alone.
The static proof keeps a version guard, since that is a limit of the prover.
`RUNTIME_MULTIRHS_ZERO` is dropped: it gated a *runtime* allocation assertion,
and on 1.11 that assertion fails because the multi-RHS path really does
allocate (6144 B, per #1119's table). Skipping it hides a regression rather
than a prover limitation, so it should fail there until the allocation is
fixed.
Verified: `qa/allocations.jl` completes on Julia 1.10, the version that was
failing; `GROUP=QA` and `GROUP=Core` both pass on 1.12, which is what CI runs.
Stacked on #1119 - it carries the `_lu_from_cacheval` fix this needs.
Claude-Session: https://claude.ai/code/session_017rr42T1vFRRT8D7DMAmJzf
Co-authored-by: ChrisRackauckas-Claude <accounts@chrisrackauckas.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Fixes both macOS CI failures from #1114. They have separate causes.
1.
snluis aMethodErroron any Mac (the real bug)_lu_from_cachevalpulls the block factorization back out of the dense block cache's cacheval, handlingLUandTuple. The direct-BLAS backends store neither: Apple Accelerate, BLIS, and OpenBLAS each keep a mutable*LUCacheholding the factored matrix, the getrf pivots, andinfo, which_custom_cache_factorizationwraps in anLUonly on demand. On a Mac the default dense solver resolves toAppleAccelerateLUFactorization, so the default path fell straight into the hole.Despite surfacing in the allocation test, this is not a test bug — plain
snlu(A)on any Mac fails for any matrix with a supernode at or abovedense_threshold:_cache_lu!reads exactly.factors/.ipiv, with the same LAPACK semantics those caches use, so the cacheval is now handed back as-is rather than allocating a throwawayLUper block. Anything exposing neither field gets a real error message instead of aMethodError.Why the existing tests missed it:
test/Core/supernodal_lu.jldoesusing RecursiveFactorization, which makes the default dense solver resolve to RFLU (aTuplecacheval) on every platform.test/qa/allocations.jlis the only placesnluruns without it — which is why a Mac-only bug surfaced in an allocation test.The new testset names the platform's direct-BLAS backends explicitly, over a caching-heavy and a caching-sparse
dense_threshold, and covers refactorization through the same caches. It fails with the originalMethodErrorwithout thenumeric.jlchange.2.
@check_allocsfindings on lts and preDifferent cause, and not macOS-specific. Every finding is in LinearAlgebra stdlib frames, none in SupernodalLU:
ldiv!(UpperTriangular(...))→ ageneric_trimatdiv!dynamic dispatch plus boxedSubArrays (triangular.jl:752), andmul!→ boxedMulAddMulingemv!(matmul.jl:446). Measured on macOS aarch64:@check_allocs@allocatedThe sweeps are genuinely allocation-free at runtime on every version in CI; only the static proof is version-fragile, and only above
PANEL_BLAS_CUTOFFwhere the sweeps hand off to the stdlib. Mac is simply the only runner exercising anything but 1.12 — the Linux QA group runsjulia 1only, which is why this never appeared there.Each assertion is pinned to the versions that can carry it rather than weakened everywhere. What the assertions exist to protect — that the sweeps carry no growth branch — is unchanged on every version.
Testing
macOS aarch64.
test/qa/allocations.jlandtest/Core/supernodal_lu.jlpass on 1.10, 1.11, and 1.12; runtime zero-allocation confirmed separately on 1.13 (AllocCheck itself does not compile on my local 1.13.0-beta1, unrelated LLVM issue). Accelerate-routed blocks match the LU path to ~1e-15 acrossdense_threshold4/16/64, including refactorization. Runic reports no formatting changes.One caveat I could not close locally: I have no Linux box here, so the claim that #2 is version-driven rather than platform-driven rests on the stack traces containing no platform-specific frames and on 1.12 being clean on this same Mac.
🤖 Generated with Claude Code