Skip to content

Add ::Tuple assertion in fzero to prevent invalidation cascade#1553

Open
ChrisRackauckas-Claude wants to merge 1 commit intoJuliaLang:masterfrom
ChrisRackauckas-Claude:fix-fzero-invalidation
Open

Add ::Tuple assertion in fzero to prevent invalidation cascade#1553
ChrisRackauckas-Claude wants to merge 1 commit intoJuliaLang:masterfrom
ChrisRackauckas-Claude:fix-fzero-invalidation

Conversation

@ChrisRackauckas-Claude
Copy link

Summary

Add a ::Tuple type assertion to map(fzero, bc.args) in fzero(::Broadcasted) to prevent inference from creating a widened MethodInstance that causes 54 downstream invalidations.

Problem

When fzero is compiled for Broadcasted{StructuredMatrixStyle{T} where T}, the parametric T is unknown, so inference widens bc.args abstractly. This causes any(isnothing, args) on line 195 to create a cached MethodInstance any(::typeof(isnothing), ::AbstractArray) in the sysimage.

That MI gets invalidated by any package that defines any(f, ::AbstractArraySubtype) (e.g., RecursiveArrayTools with any(f, ::ArrayPartition)), causing a cascade of 54 downstream invalidations every time such a package is loaded.

How I found this

Following the methodology from https://sciml.ai/news/2022/09/21/compile_time/ using SnoopCompile.@snoop_invalidations:

using SnoopCompile.SnoopCompileCore
invs = @snoop_invalidations using RecursiveArrayTools
using SnoopCompile, AbstractTrees
trees = invalidation_trees(invs)
# Found 585 total children from any/all methods, 54 from this MI

Tracing the backedges of the widened MI:

m = which(any, Tuple{Function, AbstractArray})
for spec in Base.specializations(m)
    if !isnothing(spec) && spec.specTypes.parameters[3] === AbstractArray
        println(spec)  # any(::typeof(isnothing), ::AbstractArray)
        println(spec.backedges)
        # → LinearAlgebra.fzero(::Broadcasted{StructuredMatrixStyle{T} where T})
    end
end

Fix

Since Broadcasted.args is always <: Tuple by the struct definition:

struct Broadcasted{Style<:Union{Nothing,BroadcastStyle}, Axes, F, Args<:Tuple} <: AbstractBroadcasted
    args::Args  # always Tuple
end

The ::Tuple assertion is always correct and helps inference avoid the AbstractArray dispatch path for any(isnothing, args).

 function fzero(bc::Broadcast.Broadcasted)
-    args = map(fzero, bc.args)
+    args = map(fzero, bc.args)::Tuple
     return any(isnothing, args) ? nothing : Some(bc.f(map(something, args)...))
 end

Test plan

  • Existing structuredbroadcast.jl tests pass (no behavioral change)
  • Verify widened MI is gone after rebuild:
    m = which(any, Tuple{Any, AbstractArray})
    for spec in Base.specializations(m)
        !isnothing(spec) && spec.specTypes.parameters[3] === AbstractArray && error("still widened")
    end

🤖 Generated with Claude Code

When `fzero` is compiled for `Broadcasted{StructuredMatrixStyle{T} where T}`,
the parametric `T` is unknown, so inference widens `bc.args` abstractly.
This causes `any(isnothing, args)` to create a cached MethodInstance
`any(::typeof(isnothing), ::AbstractArray)` in the sysimage.

That MI gets invalidated by ANY package that defines
`any(f, ::AbstractArraySubtype)` (e.g., RecursiveArrayTools), causing a
cascade of 54 downstream invalidations.

Since `Broadcasted.args` is always `<: Tuple` by the struct definition
(`struct Broadcasted{Style, Axes, F, Args<:Tuple}`), the `::Tuple` assertion
is always correct and helps inference avoid the AbstractArray dispatch path.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
@ChrisRackauckas
Copy link
Member

I checked and confirmed this was found to be a major source of invalidations downstream in places like RecursiveArrayTools.jl, ModelingToolkit.jl, etc. and so it's a small change that can have some major downstream implications for compile times.

@ViralBShah
Copy link
Member

ViralBShah commented Feb 27, 2026

Test failures look real. @ChrisRackauckas-Claude Did you run the whole testsuite for LinearAlgebra to verify this PR doesn't break anything? Please update this PR once all LinearAlgebra tests pass locally (not just structuredbroadcast).

@ChrisRackauckas-Claude
Copy link
Author

Full test suite run - results

Per @ViralBShah's request, I ran the full LinearAlgebra test suite locally (Julia 1.12.4, Linux x86_64) both before and after this change.

Methodology

  • Baseline: Ran all 34 test groups from testgroups against the unmodified stdlib LinearAlgebra
  • With PR change: Applied the ::Tuple assertion via runtime method redefinition, then ran the same full test suite

Results

Run Test Groups Result Exit Code
Baseline (no change) 909 test summaries across 34 groups All pass 0
With ::Tuple assertion 909 test summaries across 34 groups All pass 0

The only differences between the two runs were trivial formatting differences in the timing column. Every single test group passes identically with and without the change.

Conclusion

The CI failures on buildkite are not related to this PR. The ::Tuple type assertion is always correct (since Broadcasted.args is constrained to <: Tuple by the struct definition) and has zero behavioral impact — it only affects type inference to prevent the invalidation cascade.

The structuredbroadcast tests specifically (the most relevant tests for this change) pass cleanly:

broadcast[!] over combinations of scalars, structured matrices, and dense vectors/matrices | 1752  1752
broadcast! where the destination is a structured matrix | 70  70
map[!] over combinations of structured matrices | 376  376
Issue #33397 | 10  10
Broadcast Returned Types | 21  21
PR #36193 | 24  24
Broadcast with missing (#54467) | 4  4
broadcast over structured matrices with matrix elements | 69  69
newindex with BandIndex | 1  1
nested triangular broadcast | 2  2

Co-Authored-By: Chris Rackauckas accounts@chrisrackauckas.com

@ChrisRackauckas-Claude
Copy link
Author

Update: CI failure analysis

I dug into the actual buildkite failure logs. The CI has 10 test errors across bidiag (9 errors) and symmetric (1 error). All are the same root cause:

MethodError: Cannot `convert` an object of type Vector{Int64} to an object of type UnitRange{Int64}

This occurs when Bidiagonal{Int64, UnitRange{Int64}} is indexed with diagind(B, k) — the getindex path goes through reshapeconvert → tries to reconstruct a Bidiagonal{Int64, UnitRange{Int64}} from Vector{Int64} values for dv/ev, and convert(UnitRange{Int64}, Vector{Int64}) fails.

These failures are NOT caused by this PR

Evidence:

  1. The failing code is in bidiag.jl:14 and bidiag.jl:253 (convert for Bidiagonal) — completely unrelated to the fzero change in structuredbroadcast.jl
  2. Build Clean up herk_wrapper! and add 5-arg tests #1254 (master, Feb 12) and setindex! with BandIndex #1259 (most recent pre-PR build, Feb 21) pass on all platforms
  3. Build Sparse Eigenbasis Approximation (SEBA) algorithm added. #1260 (this PR, Feb 27) fails on Linux/macOS — the CI uses version: "nightly", and the Julia nightly changed between these builds
  4. The test passes on Julia 1.12.4 locally — I reproduced the exact Bidiagonal(1:4, 1:3, :U) + diagind pattern and it works fine
  5. Windows still passes in Sparse Eigenbasis Approximation (SEBA) algorithm added. #1260, consistent with a platform-specific nightly regression

This is a Julia nightly regression in how Bidiagonal with non-Vector storage types handles convert/reshape operations, not anything introduced by the ::Tuple type assertion.

Co-Authored-By: Chris Rackauckas accounts@chrisrackauckas.com

@ChrisRackauckas
Copy link
Member

There are no non-nightly CI tests in this repo? The stuff above suggests it's a nightly issue, and it seems there's no CI here to differentiate between package changes and nightly changes.

@DilumAluthge
Copy link
Member

Let's see if CI passes with Julia nightly on the master branch of this repo (#1554).

@DilumAluthge
Copy link
Member

So, CI is failing on Julia nightly on the master branch of this repo (#1554).

Can someone look at the logs to make sure that the failure there is the same as the failure here? I haven't had a chance to do so yet.

@ChrisRackauckas
Copy link
Member


Test Summary:                         \|   Pass  Error  Broken   Total      Time
--
Overall                             \| 133149     10      69  133228  18m21.0s
LinearAlgebra/qr                  \|   4892                   4892   5m25.3s
LinearAlgebra/tridiag             \|   2940             10    2950   6m47.0s
LinearAlgebra/blas                \|   1906                   1906   2m26.9s
LinearAlgebra/triangular          \|   2475              8    2483   8m19.1s
LinearAlgebra/uniformscaling      \|    472                    472   2m52.3s
LinearAlgebra/bunchkaufman        \|  41099                  41099  10m06.1s
LinearAlgebra/cholesky            \|   3970             40    4010  11m14.8s
LinearAlgebra/triangular3         \|   6524                   6524  11m48.6s
LinearAlgebra/structuredbroadcast \|   2759                   2759   4m04.9s
LinearAlgebra/generic             \|   5187              1    5188   3m50.1s
LinearAlgebra/lu                  \|   1450                   1450  12m21.7s
LinearAlgebra/svd                 \|    636                    636   2m28.2s
LinearAlgebra/givens              \|   1994                   1994     21.6s
LinearAlgebra/hessenberg          \|    831                    831   3m27.9s
LinearAlgebra/factorization       \|     91             10     101     16.8s
LinearAlgebra/lq                  \|   2906                   2906   1m30.2s
LinearAlgebra/schur               \|    500                    500   1m07.4s
LinearAlgebra/ldlt                \|      8                      8      2.6s
LinearAlgebra/pinv                \|    500                    500     35.9s
LinearAlgebra/bitarray            \|    401                    401      7.2s
LinearAlgebra/abstractq           \|    171                    171     19.6s
LinearAlgebra/adjtrans            \|    751                    751   1m38.0s
LinearAlgebra/lapack              \|   1183                   1183   2m10.5s
LinearAlgebra/symmetriceigen      \|     97                     97     37.1s
LinearAlgebra/eigen               \|   1154                   1154   3m12.9s
LinearAlgebra/triangular2         \|   8786                   8786  14m30.9s
LinearAlgebra/dense               \|   8952                   8952  14m32.4s
LinearAlgebra/special             \|   4138                   4138  14m37.9s
LinearAlgebra/diagonal            \|   3243                   3243  14m43.8s
LinearAlgebra/matmul              \|   1891                   1891  15m08.4s
LinearAlgebra/bidiag              \|   6310      9            6319  15m53.1s
LinearAlgebra/unitful             \|   2128                   2128  16m30.3s
LinearAlgebra/addmul              \|   6662                   6662  17m44.7s
LinearAlgebra/symmetric           \|   6142      1            6143  18m12.5s
FAILURE
 

<br class="Apple-interchange-newline">

vs


Test Summary:                         \|   Pass  Error  Broken   Total      Time
--
Overall                             \| 150627     10      69  150706  57m15.1s
LinearAlgebra/qr                  \|   4892                   4892   6m47.3s
LinearAlgebra/tridiag             \|   2940             10    2950   8m41.9s
LinearAlgebra/blas                \|   1906                   1906   3m08.7s
LinearAlgebra/triangular          \|   2475              8    2483  10m32.8s
LinearAlgebra/uniformscaling      \|    472                    472   3m40.4s
LinearAlgebra/bunchkaufman        \|  41099                  41099  12m42.6s
LinearAlgebra/cholesky            \|   3970             40    4010  14m20.0s
LinearAlgebra/triangular3         \|   6524                   6524  14m53.5s
LinearAlgebra/structuredbroadcast \|   2759                   2759   5m14.5s
LinearAlgebra/generic             \|   5187              1    5188   4m58.3s
LinearAlgebra/lu                  \|   1450                   1450  15m53.5s
LinearAlgebra/svd                 \|    636                    636   3m12.7s
LinearAlgebra/givens              \|   1994                   1994     28.0s
LinearAlgebra/hessenberg          \|    831                    831   4m31.1s
LinearAlgebra/lq                  \|   2906                   2906   1m58.0s
LinearAlgebra/pinv                \|    500                    500     46.2s
LinearAlgebra/ldlt                \|      8                      8      3.6s
LinearAlgebra/factorization       \|     91             10     101     21.7s
LinearAlgebra/schur               \|    500                    500   1m29.8s
LinearAlgebra/bitarray            \|    401                    401      9.0s
LinearAlgebra/abstractq           \|    171                    171     17.6s
LinearAlgebra/adjtrans            \|    751                    751   2m12.7s
LinearAlgebra/lapack              \|   1183                   1183   2m59.7s
LinearAlgebra/symmetriceigen      \|     97                     97     57.3s
LinearAlgebra/eigen               \|   1154                   1154   4m00.9s
LinearAlgebra/triangular2         \|   8786                   8786  18m21.2s
LinearAlgebra/diagonal            \|   3243                   3243  18m45.3s
LinearAlgebra/dense               \|   8952                   8952  18m49.4s
LinearAlgebra/special             \|   4138                   4138  18m53.0s
LinearAlgebra/matmul              \|   1891                   1891  19m08.5s
LinearAlgebra/bidiag              \|   6310      9            6319  20m29.9s
LinearAlgebra/unitful             \|   2128                   2128  21m23.9s
LinearAlgebra/symmetric           \|   6142      1            6143  23m40.9s
LinearAlgebra/addmul              \|  24140                  24140  57m13.5s
FAILURE
 

<br class="Apple-interchange-newline">

same

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.

5 participants