Make SparseArrays a regular dependency of OrdinaryDiffEqCore.jl#3031
Make SparseArrays a regular dependency of OrdinaryDiffEqCore.jl#3031JoshuaLampert wants to merge 2 commits intoSciML:masterfrom
Conversation
| # Efficient O(nnz) isdiag check for sparse matrices. | ||
| # Standard isdiag is O(n²) which is prohibitively slow for large sparse matrices. | ||
| """ | ||
| _isdiag(A::SparseMatrixCSC) | ||
|
|
||
| Check if a sparse matrix is diagonal in O(nnz) time by traversing the CSC structure directly. | ||
| Returns `true` if all non-zero elements are on the diagonal. | ||
| """ | ||
| function _isdiag(A::SparseArrays.SparseMatrixCSC) | ||
| m, n = size(A) | ||
| m != n && return false | ||
| @inbounds for j in 1:n | ||
| for k in A.colptr[j]:(A.colptr[j + 1] - 1) | ||
| A.rowval[k] != j && return false | ||
| end | ||
| end | ||
| return true | ||
| end | ||
|
|
There was a problem hiding this comment.
Has this been upstreamed to SparseArrays?
There was a problem hiding this comment.
Yes, I agree. So should this PR just revert #3011?
There was a problem hiding this comment.
no. We should open this upstream, but keep this optimization in the meantime.
There was a problem hiding this comment.
This is breaking CI in at least 5-10 our repos. This is pretty urgent for us.
There was a problem hiding this comment.
Isn't the breaking part that this used to be in an extension? Shouldn't bringing it into OrdinaryDiffEqCore fix your issue?
There was a problem hiding this comment.
Yes, if we can merge this PR as is that would also be fine as a hot fix. I thought you wanted to keep the status quo until this is upstreamed to SparseArrays.jl.
There was a problem hiding this comment.
sorry for the confusion. I meant keeping the status quo of implimenting _isdiag ourselves, but changing where it lives to Core.
There was a problem hiding this comment.
Yes, in other words: merging this PR because changing where _isdiag lives is exactly what this PR does. That's fine with me. Or do you mean something else than this PR currently does?
SparseArrays is not part of the default system image anymore since Julia 1.10. This actually motivated a push towards SparseArrays extensions, e.g. JuliaArrays/FillArrays.jl#285 or JuliaStats/Distances.jl#251 or JuliaDiff/ChainRulesCore.jl#638 |
Ok, I see. Thanks for the clarification. But in this case this code should simply live in SparseArrays.jl. So I would propose to merge this PR as a hotfix to fix CI of Downstream packages using Julia v1.10 and then open an issue/a PR in SparseArrays.jl to include it there. Once that PR is merged and released the function can be removed here again. We need a fix for this as soon as possible because there are multiple PRs, which wait for this. Some I know of:
Even here the corresponding CI job gives an error, e.g., https://github.com/SciML/OrdinaryDiffEq.jl/actions/runs/21580707995/job/62177277916?pr=3024#step:5:1011, which for some reason doesn't let the job fail. |
|
Surely, the code should be moved to SparseArrays. I haven't followed the bug reports closely, but to me it seems that the current problem might just be the known bug in Julia 1.10 with cycle detection that was fixed in subsequent Julia releases? |
|
Yes, that might be the case. But if OrdinaryDiffEq.jl officially supports julia v1.10, it should not give an error (or even a warning) on julia v1.10 when simply doing |
|
The only possible concern would be whether this just moves the problem downstream. How does this change affect downstream packages on Julia 1.10, that depend on OrdinaryDiffEqCore and have a SparseArrays extension? Will 1.10 start reporting a circular dependency for them? |
|
That could be the case. I don't know of any package, which has an extension for SparseArrays.jl and depends on OrdinaryDiffEqCore.jl to test it though. |
|
Maybe OrdinaryDiffEqDifferentiation? |
|
I tested it and I think the following tells us that OrdinaryDiffEqDifferentiation.jl is also fine with this PR: lampert@nb1759:~/.julia/dev/OrdinaryDiffEq$ rm -r ~/.julia/compiled/v1.10/OrdinaryDiffEqCore/ ~/.julia/compiled/v1.10/OrdinaryDiffEqDifferentiation/ ~/.julia/compiled/v1.10/OrdinaryDiffEqDifferentiationSparseArraysExt/ ~/.julia/compiled/v1.10/EnzymeCore
lampert@nb1759:~/.julia/dev/OrdinaryDiffEq$ git checkout fix-circular-dep-v1.10
Switched to branch 'fix-circular-dep-v1.10'
Your branch is up to date with 'origin/fix-circular-dep-v1.10'.
lampert@nb1759:~/.julia/dev/OrdinaryDiffEq$ julia +1.10 --project=run
_
_ _ _(_)_ | Documentation: https://docs.julialang.org
(_) | (_) (_) |
_ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help.
| | | | | | |/ _` | |
| | |_| | | | (_| | | Version 1.10.10 (2025-06-27)
_/ |\__'_|_|_|\__'_| | Official https://julialang.org/ release
|__/ |
(run) pkg> dev OrdinaryDiffEqCore
Path `/home/lampert/.julia/dev/OrdinaryDiffEq` exists and looks like the correct repo. Using existing path.
Resolving package versions...
Updating `~/.julia/dev/OrdinaryDiffEq/run/Project.toml`
[bbf590c4] + OrdinaryDiffEqCore v3.4.1 `~/.julia/dev/OrdinaryDiffEq/lib/OrdinaryDiffEqCore`
Updating `~/.julia/dev/OrdinaryDiffEq/run/Manifest-v1.10.toml`
[bbf590c4] ~ OrdinaryDiffEqCore v3.4.0 `~/.julia/dev/OrdinaryDiffEq/lib/OrdinaryDiffEqCore` ⇒ v3.4.1 `~/.julia/dev/OrdinaryDiffEq/lib/OrdinaryDiffEqCore`
(run) pkg> dev OrdinaryDiffEqDifferentiation
Path `/home/lampert/.julia/dev/OrdinaryDiffEq` exists and looks like the correct repo. Using existing path.
Resolving package versions...
Updating `~/.julia/dev/OrdinaryDiffEq/run/Project.toml`
[4302a76b] + OrdinaryDiffEqDifferentiation v2.0.0 `~/.julia/dev/OrdinaryDiffEq/lib/OrdinaryDiffEqDifferentiation`
No Changes to `~/.julia/dev/OrdinaryDiffEq/run/Manifest-v1.10.toml`
(run) pkg> add EnzymeCore
Resolving package versions...
Updating `~/.julia/dev/OrdinaryDiffEq/run/Project.toml`
[f151be2c] + EnzymeCore v0.8.18
No Changes to `~/.julia/dev/OrdinaryDiffEq/run/Manifest-v1.10.toml`
Precompiling packages finished.
35 dependencies successfully precompiled in 85 seconds. 141 already precompiled.
(run) pkg> rm EnzymeCore
Updating `~/.julia/dev/OrdinaryDiffEq/run/Project.toml`
[f151be2c] - EnzymeCore v0.8.18
No Changes to `~/.julia/dev/OrdinaryDiffEq/run/Manifest-v1.10.toml`
(run) pkg> rm OrdinaryDiffEqDifferentiation
Updating `~/.julia/dev/OrdinaryDiffEq/run/Project.toml`
[4302a76b] - OrdinaryDiffEqDifferentiation v2.0.0 `~/.julia/dev/OrdinaryDiffEq/lib/OrdinaryDiffEqDifferentiation`
No Changes to `~/.julia/dev/OrdinaryDiffEq/run/Manifest-v1.10.toml`
julia>
lampert@nb1759:~/.julia/dev/OrdinaryDiffEq$ rm -r ~/.julia/compiled/v1.10/OrdinaryDiffEqCore/ ~/.julia/compiled/v1.10/OrdinaryDiffEqDifferentiation/ ~/.julia/compiled/v1.10/OrdinaryDiffEqDifferentiationSparseArraysExt/ ~/.julia/compiled/v1.10/EnzymeCore
lampert@nb1759:~/.julia/dev/OrdinaryDiffEq$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
lampert@nb1759:~/.julia/dev/OrdinaryDiffEq$ julia +1.10 --project=run
_
_ _ _(_)_ | Documentation: https://docs.julialang.org
(_) | (_) (_) |
_ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help.
| | | | | | |/ _` | |
| | |_| | | | (_| | | Version 1.10.10 (2025-06-27)
_/ |\__'_|_|_|\__'_| | Official https://julialang.org/ release
|__/ |
(run) pkg> dev OrdinaryDiffEqDifferentiation
Path `/home/lampert/.julia/dev/OrdinaryDiffEq` exists and looks like the correct repo. Using existing path.
Resolving package versions...
Updating `~/.julia/dev/OrdinaryDiffEq/run/Project.toml`
[bbf590c4] ~ OrdinaryDiffEqCore v3.4.1 `~/.julia/dev/OrdinaryDiffEq/lib/OrdinaryDiffEqCore` ⇒ v3.4.0 `~/.julia/dev/OrdinaryDiffEq/lib/OrdinaryDiffEqCore`
[4302a76b] + OrdinaryDiffEqDifferentiation v2.0.0 `~/.julia/dev/OrdinaryDiffEq/lib/OrdinaryDiffEqDifferentiation`
Updating `~/.julia/dev/OrdinaryDiffEq/run/Manifest-v1.10.toml`
[bbf590c4] ~ OrdinaryDiffEqCore v3.4.1 `~/.julia/dev/OrdinaryDiffEq/lib/OrdinaryDiffEqCore` ⇒ v3.4.0 `~/.julia/dev/OrdinaryDiffEq/lib/OrdinaryDiffEqCore`
(run) pkg> add EnzymeCore
Resolving package versions...
Updating `~/.julia/dev/OrdinaryDiffEq/run/Project.toml`
[f151be2c] + EnzymeCore v0.8.18
No Changes to `~/.julia/dev/OrdinaryDiffEq/run/Manifest-v1.10.toml`
┌ Warning: Circular dependency detected.
│ Precompilation will be skipped for dependencies in this cycle:
│ ┌ OrdinaryDiffEqCore → OrdinaryDiffEqCoreEnzymeCoreExt
│ └─ OrdinaryDiffEqCore → OrdinaryDiffEqCoreSparseArraysExt
│ Precompilation will also be skipped for the following, which depend on the above cycle:
│ OrdinaryDiffEqDifferentiation → OrdinaryDiffEqDifferentiationSparseArraysExt
│ OrdinaryDiffEqLowStorageRK
│ OrdinaryDiffEqLowOrderRK
│ OrdinaryDiffEqDifferentiation
│ OrdinaryDiffEqNonlinearSolve
│ OrdinaryDiffEqSDIRK
└ @ Pkg.API.Precompilation ~/.julia/juliaup/julia-1.10.10+0.x64.linux.gnu/share/julia/stdlib/v1.10/Pkg/src/precompilation.jl:583
Precompiling packages finished.
28 dependencies successfully precompiled in 75 seconds. 141 already precompiled. 8 skipped due to circular dependency.Please tell me if I should test something else. |
|
I was curious, and did a On Julia 1.10: (jl_KPTG1m) pkg> why EnzymeCore
OrdinaryDiffEqCore → DiffEqBase → BracketingNonlinearSolve → NonlinearSolveBase → EnzymeCore
(jl_KPTG1m) pkg> why SparseArrays
OrdinaryDiffEqCore → DiffEqBase → BracketingNonlinearSolve → NonlinearSolveBase → SciMLBase → Statistics → SparseArrays
OrdinaryDiffEqCore → DiffEqBase → BracketingNonlinearSolve → NonlinearSolveBase → SciMLJacobianOperators → SciMLBase → Statistics → SparseArrays
OrdinaryDiffEqCore → DiffEqBase → BracketingNonlinearSolve → SciMLBase → Statistics → SparseArrays
OrdinaryDiffEqCore → DiffEqBase → SciMLBase → Statistics → SparseArrays
OrdinaryDiffEqCore → SciMLBase → Statistics → SparseArraysOn Julia 1.12: (jl_IlqkUf) pkg> why EnzymeCore
OrdinaryDiffEqCore → DiffEqBase → BracketingNonlinearSolve → NonlinearSolveBase → EnzymeCore
(jl_IlqkUf) pkg> why SparseArrays
ERROR: The following package names could not be resolved:
* SparseArrays (not found in project or manifest)On Julia 1.10, apparently SparseArrays is only pulled in via Statistics but the dependency of Statistics on SparseArrays was apparently removed in more recent Julia versions. NonlinearSolveBase depends on EnzymeCore due to https://github.com/SciML/NonlinearSolve.jl/blob/ed96792bf00e5d42c7a01ea7da676d78ea37489a/lib/NonlinearSolveBase/src/autodiff.jl#L7 and https://github.com/SciML/NonlinearSolve.jl/blob/ed96792bf00e5d42c7a01ea7da676d78ea37489a/lib/NonlinearSolveBase/src/autodiff.jl#L17. It seems slightly unfortunate that both a dependency on EnzymeCore and a weak dependency on Enzyme are needed... I wonder if ADTypes could support specification of modes without having to depend on EnzymeCore (using |
|
As long as this issue is fixed soon I'm fine with any fix. We only need it as soon as possible to be able to properly continue our work in the downstream packages. |
|
My take-away is that - regardless of the Julia version - as long as NonlinearSolveBase has a strong dependency on EnzymeCore, it doesn't make sense to make EnzymeCore a weak dependency of OrdinaryDiffEqCore. Maybe making EnzymeCore a strong dependency in OrdinaryDiffEqCore would be sufficient to break the "cycle" that Julia 1.10 (incorrectly IMO) complains about. |
Sounds good. I created #3032 for that. |
|
The answer isn't to do that... just solve it at its core (pun intended) and remove the EnzymeCore from NonlinearSolveBase. |
|
If we can do that within the next couple of hours fine (but I will not create a PR for that). Otherwise I would prefer a hotfix, which can be improved later. |
|
I don't think a Julia v1.10 hotfix release will come that quickly... but that doesn't mean we should put bugs in other places. |
|
I don't mean a Julia v1.10 hotfix release, but a hotfix release within the SciML framework. |
|
The cleanest thing would be to just add |
|
Great, but my answer from above is still valid:
|
|
That would be literally just as fast to do. |
|
Sounds great! I'm looking forward to a corresponding PR. |
|
Just waiting for @wsmoses to be up and should be done in like one chat. |
|
I couldn't get in touch with @wsmoses so doing the hotfix for now. |
|
Thanks! |
Checklist
contributor guidelines, in particular the SciML Style Guide and
COLPRAC.
Additional context
This fixes #3025. See also #3011 (comment). SparseArrays.jl is a stdlib. So I think having it as a regular dependency should be fine.