From 2b5cd2ce560d50414e6d44f0053f0669faee03ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Fri, 20 Feb 2026 11:55:48 +0100 Subject: [PATCH 01/20] =?UTF-8?q?add=20Diagonal=20overload=20f=C3=BCr=20ma?= =?UTF-8?q?ss=20matrix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/OrdinaryDiffEqNonlinearSolve/src/initialize_dae.jl | 5 +++++ .../test/sparse_algebraic_detection_tests.jl | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/lib/OrdinaryDiffEqNonlinearSolve/src/initialize_dae.jl b/lib/OrdinaryDiffEqNonlinearSolve/src/initialize_dae.jl index d1b8bf583c8..db5d603a8af 100644 --- a/lib/OrdinaryDiffEqNonlinearSolve/src/initialize_dae.jl +++ b/lib/OrdinaryDiffEqNonlinearSolve/src/initialize_dae.jl @@ -29,6 +29,11 @@ function find_algebraic_vars_eqs(M::SparseMatrixCSC) return algebraic_vars, algebraic_eqs end +function find_algebraic_vars_eqs(M::LinearAlgebra.Diagonal) + _idxs = map(iszero, LinearAlgebra.diag(M)) + return _idxs, _idxs +end + # Fallback for non-sparse matrices (original behavior) function find_algebraic_vars_eqs(M::AbstractMatrix) algebraic_vars = vec(all(iszero, M, dims = 1)) diff --git a/lib/OrdinaryDiffEqNonlinearSolve/test/sparse_algebraic_detection_tests.jl b/lib/OrdinaryDiffEqNonlinearSolve/test/sparse_algebraic_detection_tests.jl index bc7294fc278..df393108c79 100644 --- a/lib/OrdinaryDiffEqNonlinearSolve/test/sparse_algebraic_detection_tests.jl +++ b/lib/OrdinaryDiffEqNonlinearSolve/test/sparse_algebraic_detection_tests.jl @@ -88,4 +88,14 @@ using OrdinaryDiffEqNonlinearSolve: find_algebraic_vars_eqs @test vars == [false, true] @test eqs == [false, true] end + + # Test 4: Test Diagonal case + @testset "Test Diagonal cast" begin + M_diag = Diagonal([1.0, 0.0, 1.0, 1.0, 0.0]) + vars, eqs = find_algebraic_vars_eqs(M_diag) + @test vars == [false, true, false, false, true] + @test eqs == [false, true, false, false, true] + # compare to dense + @test find_algebraic_vars_eqs(M_diag) == find_algebraic_vars_eqs(collect(M_diag)) + end end From d845837b8152f64a48ff92580b45ffa03a2494db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Fri, 20 Feb 2026 19:07:57 +0100 Subject: [PATCH 02/20] fix dae gpu compat --- .../src/derivative_utils.jl | 10 ++++- test/gpu/simple_dae.jl | 40 ++++++++++++------- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/lib/OrdinaryDiffEqDifferentiation/src/derivative_utils.jl b/lib/OrdinaryDiffEqDifferentiation/src/derivative_utils.jl index 8528a0b7477..e297eb4dc50 100644 --- a/lib/OrdinaryDiffEqDifferentiation/src/derivative_utils.jl +++ b/lib/OrdinaryDiffEqDifferentiation/src/derivative_utils.jl @@ -183,7 +183,7 @@ function calc_tderivative!(integrator, cache, dtd1, repeat_step) tf.p = p alg = unwrap_alg(integrator, true) - autodiff_alg = ADTypes.dense_ad(gpu_safe_autodiff(alg_autodiff(alg), u)) + autodiff_alg = gpu_safe_autodiff(ADTypes.dense_ad(alg_autodiff(alg)), u) # Convert t to eltype(dT) if using ForwardDiff, to make FunctionWrappers work t = autodiff_alg isa AutoForwardDiff ? convert(eltype(dT), t) : t @@ -228,7 +228,7 @@ function calc_tderivative(integrator, cache) tf.u = uprev tf.p = p - autodiff_alg = ADTypes.dense_ad(gpu_safe_autodiff(alg_autodiff(alg), u)) + autodiff_alg = gpu_safe_autodiff(ADTypes.dense_ad(alg_autodiff(alg)), u) if alg_autodiff isa AutoFiniteDiff autodiff_alg = SciMLBase.@set autodiff_alg.dir = diffdir(integrator) @@ -572,6 +572,12 @@ function jacobian2W!( else @.. broadcast = false @view(W[idxs]) = muladd(λ, invdtgamma, @view(J[idxs])) end + elseif is_sparse(W) && !ArrayInterface.fast_scalar_indexing(nonzeros(W)) + # Sparse GPU arrays (e.g. CuSparseMatrixCSC/CSR) don't support broadcasting. + # ArrayInterface.fast_scalar_indexing is not specialized for AbstractGPUSparseArray, + # so we detect them by checking if the underlying nonzeros storage is a GPU array. + # we then fall back to allocating matrix arithmetic + copyto!(W, J - invdtgamma * mass_matrix) else @.. broadcast = false W = muladd(-mass_matrix, invdtgamma, J) end diff --git a/test/gpu/simple_dae.jl b/test/gpu/simple_dae.jl index 1b39a42cd1a..c1a346f0108 100644 --- a/test/gpu/simple_dae.jl +++ b/test/gpu/simple_dae.jl @@ -5,6 +5,7 @@ using LinearAlgebra using Adapt using SparseArrays using Test +using CUDSS #= du[1] = -u[1] @@ -38,30 +39,39 @@ tspan = (0.0, 5.0) prob = ODEProblem(odef, u0, tspan, p) sol = solve(prob, Rodas5P()) -# gpu version mass_matrix_d = adapt(CuArray, mass_matrix) - -# TODO: jac_prototype fails -# jac_prototype_d = adapt(CuArray, jac_prototype) -# jac_prototype_d = CUDA.CUSPARSE.CuSparseMatrixCSR(jac_prototype) -jac_prototype_d = nothing +jac_prototype_d_csc = CUDA.CUSPARSE.CuSparseMatrixCSC(jac_prototype) +jac_prototype_d_csr = CUDA.CUSPARSE.CuSparseMatrixCSR(jac_prototype) u0_d = adapt(CuArray, u0) p_d = adapt(CuArray, p) -odef_d = ODEFunction(dae!, mass_matrix = mass_matrix_d, jac_prototype = jac_prototype_d) -prob_d = ODEProblem(odef_d, u0_d, tspan, p_d) -sol_d = solve(prob_d, Rodas5P()) + +odef_d_dns = ODEFunction(dae!, mass_matrix = mass_matrix_d, jac_prototype = nothing) +odef_d_csc = ODEFunction(dae!, mass_matrix = mass_matrix_d, jac_prototype = jac_prototype_d_csc) +odef_d_csr = ODEFunction(dae!, mass_matrix = mass_matrix_d, jac_prototype = jac_prototype_d_csr) + +prob_d_dns = ODEProblem(odef_d_dns, u0_d, tspan, p_d) +prob_d_csc = ODEProblem(odef_d_csc, u0_d, tspan, p_d) +prob_d_csr = ODEProblem(odef_d_csr, u0_d, tspan, p_d) + +sol_d_dns = solve(prob_d_dns, Rodas5P()) # works +sol_d_csc = solve(prob_d_csc, Rodas5P()) # uses Krylov +sol_d_csr = solve(prob_d_csr, Rodas5P()) # uses CUDSS @testset "Test constraints in GPU sol" begin - for t in sol_d.t - u = Vector(sol_d(t)) - @test isapprox(u[1] + u[2], u[3]; atol = 1.0e-6) - @test isapprox(-u[1] + u[2], u[4]; atol = 1.0e-6) + for sol_d in [sol_d_dns, sol_d_csc, sol_d_csr] + for t in sol_d.t + u = Vector(sol_d(t)) + @test isapprox(u[1] + u[2], u[3]; atol = 1.0e-6) + @test isapprox(-u[1] + u[2], u[4]; atol = 1.0e-6) + end end end @testset "Compare GPU to CPU solution" begin - for t in tspan[begin]:0.1:tspan[end] - @test isapprox(Vector(sol_d(t)), sol(t); rtol = 1.0e-4) + for sol_d in [sol_d_dns, sol_d_csc, sol_d_csr] + for t in tspan[begin]:0.1:tspan[end] + @test Vector(sol_d(t)) ≈ sol(t) atol=1e-5 + end end end From a7d7d3663215b7593a9772721a0f920dd8dc61af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Mon, 23 Feb 2026 08:34:34 +0100 Subject: [PATCH 03/20] move find_algebraic_vars_eqs to OrdinayDiffEqCore --- .../ext/OrdinaryDiffEqCoreSparseArraysExt.jl | 26 +++++++++- .../src/OrdinaryDiffEqCore.jl | 2 +- lib/OrdinaryDiffEqCore/src/misc_utils.jl | 24 ++++++++++ .../test/algebraic_vars_detection_tests.jl} | 2 +- lib/OrdinaryDiffEqCore/test/runtests.jl | 1 + .../src/OrdinaryDiffEqNonlinearSolve.jl | 2 +- .../src/initialize_dae.jl | 48 ------------------- .../test/runtests.jl | 1 - 8 files changed, 53 insertions(+), 53 deletions(-) rename lib/{OrdinaryDiffEqNonlinearSolve/test/sparse_algebraic_detection_tests.jl => OrdinaryDiffEqCore/test/algebraic_vars_detection_tests.jl} (98%) diff --git a/lib/OrdinaryDiffEqCore/ext/OrdinaryDiffEqCoreSparseArraysExt.jl b/lib/OrdinaryDiffEqCore/ext/OrdinaryDiffEqCoreSparseArraysExt.jl index 6b57562971f..6a311b0792b 100644 --- a/lib/OrdinaryDiffEqCore/ext/OrdinaryDiffEqCoreSparseArraysExt.jl +++ b/lib/OrdinaryDiffEqCore/ext/OrdinaryDiffEqCoreSparseArraysExt.jl @@ -1,7 +1,7 @@ module OrdinaryDiffEqCoreSparseArraysExt using SparseArrays: SparseMatrixCSC -import OrdinaryDiffEqCore: _isdiag +import OrdinaryDiffEqCore: _isdiag, find_algebraic_vars_eqs # Efficient O(nnz) isdiag check for sparse matrices. # Standard isdiag is O(n²) which is prohibitively slow for large sparse matrices. @@ -22,4 +22,28 @@ function _isdiag(A::SparseMatrixCSC) return true end +""" + find_algebraic_vars_eqs(M::SparseMatrixCSC) + +O(nnz) detection of algebraic variables (zero columns) and equations (zero rows). +""" +function find_algebraic_vars_eqs(M::SparseMatrixCSC) + n_cols = size(M, 2) + n_rows = size(M, 1) + + algebraic_vars = fill(true, n_cols) + algebraic_eqs = fill(true, n_rows) + + @inbounds for j in 1:n_cols + for idx in M.colptr[j]:(M.colptr[j + 1] - 1) + if !iszero(M.nzval[idx]) + algebraic_vars[j] = false + algebraic_eqs[M.rowval[idx]] = false + end + end + end + + return algebraic_vars, algebraic_eqs +end + end diff --git a/lib/OrdinaryDiffEqCore/src/OrdinaryDiffEqCore.jl b/lib/OrdinaryDiffEqCore/src/OrdinaryDiffEqCore.jl index 2987bbc00c4..9f5e525d783 100644 --- a/lib/OrdinaryDiffEqCore/src/OrdinaryDiffEqCore.jl +++ b/lib/OrdinaryDiffEqCore/src/OrdinaryDiffEqCore.jl @@ -15,7 +15,7 @@ import Logging: @logmsg, LogLevel using MuladdMacro: @muladd -using LinearAlgebra: opnorm, I, UniformScaling, diag, rank, isdiag +using LinearAlgebra: opnorm, I, UniformScaling, diag, rank, isdiag, Diagonal import PrecompileTools diff --git a/lib/OrdinaryDiffEqCore/src/misc_utils.jl b/lib/OrdinaryDiffEqCore/src/misc_utils.jl index a130166a388..f73a8c6e6d6 100644 --- a/lib/OrdinaryDiffEqCore/src/misc_utils.jl +++ b/lib/OrdinaryDiffEqCore/src/misc_utils.jl @@ -156,6 +156,30 @@ end # Sparse specialization is provided in OrdinaryDiffEqCoreSparseArraysExt _isdiag(A::AbstractMatrix) = isdiag(A) +""" + find_algebraic_vars_eqs(M) + +Find algebraic variables (zero columns) and algebraic equations (zero rows) from mass matrix. +Returns `(algebraic_vars, algebraic_eqs)` as boolean arrays (true = algebraic). + +Works on CPU and GPU arrays. Sparse specialization (O(nnz)) is provided in +OrdinaryDiffEqCoreSparseArraysExt. +""" +function find_algebraic_vars_eqs(M::Diagonal) + _idxs = map(iszero, diag(M)) + return _idxs, _idxs +end + +function find_algebraic_vars_eqs(M::AbstractMatrix) + algebraic_vars = vec(all(iszero, M, dims=1)) + algebraic_eqs = vec(all(iszero, M, dims=2)) + return algebraic_vars, algebraic_eqs +end + +function find_algebraic_vars_eqs(M::AbstractSciMLOperator) + return find_algebraic_vars_eqs(convert(AbstractMatrix, M)) +end + isnewton(::Any) = false # Extract the chunk size integer from an ADType for use as a type parameter. diff --git a/lib/OrdinaryDiffEqNonlinearSolve/test/sparse_algebraic_detection_tests.jl b/lib/OrdinaryDiffEqCore/test/algebraic_vars_detection_tests.jl similarity index 98% rename from lib/OrdinaryDiffEqNonlinearSolve/test/sparse_algebraic_detection_tests.jl rename to lib/OrdinaryDiffEqCore/test/algebraic_vars_detection_tests.jl index df393108c79..b76922f7103 100644 --- a/lib/OrdinaryDiffEqNonlinearSolve/test/sparse_algebraic_detection_tests.jl +++ b/lib/OrdinaryDiffEqCore/test/algebraic_vars_detection_tests.jl @@ -1,6 +1,6 @@ using Test using SparseArrays -using OrdinaryDiffEqNonlinearSolve: find_algebraic_vars_eqs +using OrdinaryDiffEqCore: find_algebraic_vars_eqs @testset "Sparse Algebraic Detection Performance" begin # Test 1: Correctness - results should match between sparse and dense methods diff --git a/lib/OrdinaryDiffEqCore/test/runtests.jl b/lib/OrdinaryDiffEqCore/test/runtests.jl index 1f79736c7a8..056926af3e7 100644 --- a/lib/OrdinaryDiffEqCore/test/runtests.jl +++ b/lib/OrdinaryDiffEqCore/test/runtests.jl @@ -33,4 +33,5 @@ end # Functional tests if TEST_GROUP == "Core" || TEST_GROUP == "ALL" @time @safetestset "Sparse isdiag Performance" include("sparse_isdiag_tests.jl") + @time @safetestset "Algebraic Vars Detection" include("algebraic_vars_detection_tests.jl") end diff --git a/lib/OrdinaryDiffEqNonlinearSolve/src/OrdinaryDiffEqNonlinearSolve.jl b/lib/OrdinaryDiffEqNonlinearSolve/src/OrdinaryDiffEqNonlinearSolve.jl index 5900c60c7be..9e6b693dd1e 100644 --- a/lib/OrdinaryDiffEqNonlinearSolve/src/OrdinaryDiffEqNonlinearSolve.jl +++ b/lib/OrdinaryDiffEqNonlinearSolve/src/OrdinaryDiffEqNonlinearSolve.jl @@ -48,7 +48,7 @@ using OrdinaryDiffEqCore: resize_nlsolver!, _initialize_dae!, FastConvergence, Convergence, SlowConvergence, VerySlowConvergence, Divergence, NLStatus, MethodType, alg_order, error_constant, - alg_extrapolates, resize_J_W!, has_autodiff + alg_extrapolates, resize_J_W!, has_autodiff, find_algebraic_vars_eqs import OrdinaryDiffEqCore: _initialize_dae!, isnewton, get_W, isfirstcall, isfirststage, diff --git a/lib/OrdinaryDiffEqNonlinearSolve/src/initialize_dae.jl b/lib/OrdinaryDiffEqNonlinearSolve/src/initialize_dae.jl index db5d603a8af..2bf0c3e25e6 100644 --- a/lib/OrdinaryDiffEqNonlinearSolve/src/initialize_dae.jl +++ b/lib/OrdinaryDiffEqNonlinearSolve/src/initialize_dae.jl @@ -1,51 +1,3 @@ -# Efficient algebraic variable/equation detection for sparse mass matrices. -# O(nnz) instead of O(n²) for sparse matrices. -""" - find_algebraic_vars_eqs(M::SparseMatrixCSC) - -Find algebraic variables (zero columns) and algebraic equations (zero rows) from mass matrix. -Returns (algebraic_vars::Vector{Bool}, algebraic_eqs::Vector{Bool}). - -For sparse matrices, uses O(nnz) traversal of CSC structure instead of O(n²) iteration. -""" -function find_algebraic_vars_eqs(M::SparseMatrixCSC) - n_cols = size(M, 2) - n_rows = size(M, 1) - - # Initialize all as algebraic (true = zero column/row) - algebraic_vars = fill(true, n_cols) - algebraic_eqs = fill(true, n_rows) - - # Mark columns/rows with non-zero values as differential (false) - @inbounds for j in 1:n_cols - for idx in M.colptr[j]:(M.colptr[j + 1] - 1) - if !iszero(M.nzval[idx]) - algebraic_vars[j] = false - algebraic_eqs[M.rowval[idx]] = false - end - end - end - - return algebraic_vars, algebraic_eqs -end - -function find_algebraic_vars_eqs(M::LinearAlgebra.Diagonal) - _idxs = map(iszero, LinearAlgebra.diag(M)) - return _idxs, _idxs -end - -# Fallback for non-sparse matrices (original behavior) -function find_algebraic_vars_eqs(M::AbstractMatrix) - algebraic_vars = vec(all(iszero, M, dims = 1)) - algebraic_eqs = vec(all(iszero, M, dims = 2)) - return algebraic_vars, algebraic_eqs -end - -# Handle SciMLOperators (e.g., MatrixOperator) by converting to matrix -function find_algebraic_vars_eqs(M::AbstractSciMLOperator) - return find_algebraic_vars_eqs(convert(AbstractMatrix, M)) -end - # Optimized tolerance checking that avoids allocations @inline function check_dae_tolerance(integrator, err, abstol, t, ::Val{true}) if abstol isa Number diff --git a/lib/OrdinaryDiffEqNonlinearSolve/test/runtests.jl b/lib/OrdinaryDiffEqNonlinearSolve/test/runtests.jl index e770ad84d62..b55904c944a 100644 --- a/lib/OrdinaryDiffEqNonlinearSolve/test/runtests.jl +++ b/lib/OrdinaryDiffEqNonlinearSolve/test/runtests.jl @@ -26,7 +26,6 @@ end # Run functional tests if TEST_GROUP ∉ ("QA", "ModelingToolkit") @time @safetestset "Newton Tests" include("newton_tests.jl") - @time @safetestset "Sparse Algebraic Detection" include("sparse_algebraic_detection_tests.jl") @time @safetestset "Sparse DAE Initialization" include("sparse_dae_initialization_tests.jl") @time @safetestset "Linear Nonlinear Solver Tests" include("linear_nonlinear_tests.jl") @time @safetestset "Linear Solver Tests" include("linear_solver_tests.jl") From f1c069f97a18f3e2d46e15c413b090b10813820e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Fri, 24 Apr 2026 10:48:33 +0200 Subject: [PATCH 04/20] fix cache build for several solvers --- lib/OrdinaryDiffEqBDF/src/OrdinaryDiffEqBDF.jl | 2 +- lib/OrdinaryDiffEqBDF/src/bdf_caches.jl | 2 +- lib/OrdinaryDiffEqRosenbrock/src/OrdinaryDiffEqRosenbrock.jl | 2 +- lib/OrdinaryDiffEqRosenbrock/src/rosenbrock_caches.jl | 4 ++-- lib/OrdinaryDiffEqSDIRK/src/OrdinaryDiffEqSDIRK.jl | 3 ++- lib/OrdinaryDiffEqSDIRK/src/sdirk_caches.jl | 2 +- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/OrdinaryDiffEqBDF/src/OrdinaryDiffEqBDF.jl b/lib/OrdinaryDiffEqBDF/src/OrdinaryDiffEqBDF.jl index e825803cf50..72b3deb8260 100644 --- a/lib/OrdinaryDiffEqBDF/src/OrdinaryDiffEqBDF.jl +++ b/lib/OrdinaryDiffEqBDF/src/OrdinaryDiffEqBDF.jl @@ -22,7 +22,7 @@ import OrdinaryDiffEqCore: alg_order, calculate_residuals!, DAEAlgorithm, _unwrap_val, DummyController, get_fsalfirstlast, generic_solver_docstring, _ad_chunksize_int, _ad_fdtype, _fixup_ad, _ode_interpolant, _ode_interpolant!, has_stiff_interpolation, - _ode_addsteps!, DerivativeOrderNotPossibleError + _ode_addsteps!, DerivativeOrderNotPossibleError, find_algebraic_vars_eqs using OrdinaryDiffEqSDIRK: ImplicitEulerConstantCache, ImplicitEulerCache using TruncatedStacktraces: @truncate_stacktrace diff --git a/lib/OrdinaryDiffEqBDF/src/bdf_caches.jl b/lib/OrdinaryDiffEqBDF/src/bdf_caches.jl index 5ccef193a31..3326b8e10d4 100644 --- a/lib/OrdinaryDiffEqBDF/src/bdf_caches.jl +++ b/lib/OrdinaryDiffEqBDF/src/bdf_caches.jl @@ -62,7 +62,7 @@ function alg_cache( atmp = similar(u, uEltypeNoUnits) recursivefill!(atmp, false) algebraic_vars = f.mass_matrix === I ? nothing : - [all(iszero, x) for x in eachcol(f.mass_matrix)] + find_algebraic_vars_eqs(f.mass_matrix)[1] eulercache = ImplicitEulerCache( u, uprev, uprev2, fsalfirst, atmp, nlsolver, algebraic_vars, alg.step_limiter! diff --git a/lib/OrdinaryDiffEqRosenbrock/src/OrdinaryDiffEqRosenbrock.jl b/lib/OrdinaryDiffEqRosenbrock/src/OrdinaryDiffEqRosenbrock.jl index 3e7cc790c5b..cadee381c65 100644 --- a/lib/OrdinaryDiffEqRosenbrock/src/OrdinaryDiffEqRosenbrock.jl +++ b/lib/OrdinaryDiffEqRosenbrock/src/OrdinaryDiffEqRosenbrock.jl @@ -13,7 +13,7 @@ import OrdinaryDiffEqCore: alg_order, alg_adaptive_order, isWmethod, isfsal, _un calculate_residuals, has_stiff_interpolation, ODEIntegrator, resize_non_user_cache!, _ode_addsteps!, full_cache, DerivativeOrderNotPossibleError, _ad_chunksize_int, _ad_fdtype, _fixup_ad, - LinearAliasSpecifier, copyat_or_push! + LinearAliasSpecifier, copyat_or_push!, find_algebraic_vars_eqs using MuladdMacro, FastBroadcast, RecursiveArrayTools import MacroTools: namify using MacroTools: @capture diff --git a/lib/OrdinaryDiffEqRosenbrock/src/rosenbrock_caches.jl b/lib/OrdinaryDiffEqRosenbrock/src/rosenbrock_caches.jl index 32cbdc49a50..ab1bf8c06f4 100644 --- a/lib/OrdinaryDiffEqRosenbrock/src/rosenbrock_caches.jl +++ b/lib/OrdinaryDiffEqRosenbrock/src/rosenbrock_caches.jl @@ -228,7 +228,7 @@ function alg_cache( ) algebraic_vars = f.mass_matrix === I ? nothing : - [all(iszero, x) for x in eachcol(f.mass_matrix)] + find_algebraic_vars_eqs(f.mass_matrix)[1] return Rosenbrock23Cache( u, uprev, k₁, k₂, k₃, du1, du2, f₁, @@ -281,7 +281,7 @@ function alg_cache( ) algebraic_vars = f.mass_matrix === I ? nothing : - [all(iszero, x) for x in eachcol(f.mass_matrix)] + find_algebraic_vars_eqs(f.mass_matrix)[1] return Rosenbrock32Cache( u, uprev, k₁, k₂, k₃, du1, du2, f₁, fsalfirst, fsallast, dT, J, W, diff --git a/lib/OrdinaryDiffEqSDIRK/src/OrdinaryDiffEqSDIRK.jl b/lib/OrdinaryDiffEqSDIRK/src/OrdinaryDiffEqSDIRK.jl index 0ce00f4948b..c714cb40ce9 100644 --- a/lib/OrdinaryDiffEqSDIRK/src/OrdinaryDiffEqSDIRK.jl +++ b/lib/OrdinaryDiffEqSDIRK/src/OrdinaryDiffEqSDIRK.jl @@ -13,7 +13,8 @@ import OrdinaryDiffEqCore: alg_order, calculate_residuals!, trivial_limiter!, _ode_interpolant!, isesdirk, issplit, ssp_coefficient, get_fsalfirstlast, generic_solver_docstring, - _ad_chunksize_int, _ad_fdtype, _fixup_ad, current_extrapolant! + _ad_chunksize_int, _ad_fdtype, _fixup_ad, current_extrapolant!, + find_algebraic_vars_eqs using TruncatedStacktraces: @truncate_stacktrace using MuladdMacro, MacroTools, FastBroadcast, RecursiveArrayTools using SciMLBase: SplitFunction diff --git a/lib/OrdinaryDiffEqSDIRK/src/sdirk_caches.jl b/lib/OrdinaryDiffEqSDIRK/src/sdirk_caches.jl index 978120e3b86..70f63fd4708 100644 --- a/lib/OrdinaryDiffEqSDIRK/src/sdirk_caches.jl +++ b/lib/OrdinaryDiffEqSDIRK/src/sdirk_caches.jl @@ -35,7 +35,7 @@ function alg_cache( recursivefill!(atmp, false) algebraic_vars = f.mass_matrix === I ? nothing : - [all(iszero, x) for x in eachcol(f.mass_matrix)] + find_algebraic_vars_eqs(f.mass_matrix)[1] return ImplicitEulerCache( u, uprev, uprev2, fsalfirst, atmp, nlsolver, algebraic_vars, alg.step_limiter! From 9683f7314af8b04f4ea763916232ac2000d2c164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Mon, 23 Feb 2026 10:49:27 +0100 Subject: [PATCH 05/20] WIP test script --- test/gpu/simple_dae.jl | 206 ++++++++++++++++++++++++++++++++++------- 1 file changed, 175 insertions(+), 31 deletions(-) diff --git a/test/gpu/simple_dae.jl b/test/gpu/simple_dae.jl index c1a346f0108..0849fc604af 100644 --- a/test/gpu/simple_dae.jl +++ b/test/gpu/simple_dae.jl @@ -1,4 +1,7 @@ using OrdinaryDiffEqRosenbrock +using OrdinaryDiffEqSDIRK +using OrdinaryDiffEqBDF +using OrdinaryDiffEqFIRK using OrdinaryDiffEqNonlinearSolve using CUDA using LinearAlgebra @@ -6,6 +9,7 @@ using Adapt using SparseArrays using Test using CUDSS +using Printf #= du[1] = -u[1] @@ -25,53 +29,193 @@ p = [ -1 1 0 -1 ] -# mass_matrix = [1 0 0 0 -# 0 1 0 0 -# 0 0 0 0 -# 0 0 0 0] mass_matrix = Diagonal([1, 1, 0, 0]) jac_prototype = sparse(map(x -> iszero(x) ? 0.0 : 1.0, p)) u0 = [1.0, 1.0, 0.5, 0.5] # force init -odef = ODEFunction(dae!, mass_matrix = mass_matrix, jac_prototype = jac_prototype) - tspan = (0.0, 5.0) + +# CPU reference solution +odef = ODEFunction(dae!, mass_matrix = mass_matrix, jac_prototype = jac_prototype) prob = ODEProblem(odef, u0, tspan, p) sol = solve(prob, Rodas5P()) -mass_matrix_d = adapt(CuArray, mass_matrix) -jac_prototype_d_csc = CUDA.CUSPARSE.CuSparseMatrixCSC(jac_prototype) -jac_prototype_d_csr = CUDA.CUSPARSE.CuSparseMatrixCSR(jac_prototype) +# GPU data -- we use F64 for higher accuracy for comparision +u0_d = adapt(CuArray{Float64}, u0) +p_d = adapt(CuArray{Float64}, p) -u0_d = adapt(CuArray, u0) -p_d = adapt(CuArray, p) +# dense or spares mass matrix does not work yet! +mass_matrix_d = cu(mass_matrix) + +function test_gpu_dae(jac_prototype_d, solver) + sol_ref = solve(prob, solver) + odef_d = ODEFunction(dae!, mass_matrix = mass_matrix_d, jac_prototype = jac_prototype_d) + prob_d = ODEProblem(odef_d, u0_d, tspan, p_d) + sol_d = solve(prob_d, solver) + + for t in sol_d.t + u = Vector(sol_d(t)) + @test isapprox(u[1] + u[2], u[3]; atol = 1.0e-6) + @test isapprox(-u[1] + u[2], u[4]; atol = 1.0e-6) + end -odef_d_dns = ODEFunction(dae!, mass_matrix = mass_matrix_d, jac_prototype = nothing) -odef_d_csc = ODEFunction(dae!, mass_matrix = mass_matrix_d, jac_prototype = jac_prototype_d_csc) -odef_d_csr = ODEFunction(dae!, mass_matrix = mass_matrix_d, jac_prototype = jac_prototype_d_csr) + max_abserr = 0.0 + max_relerr = 0.0 + for t in tspan[begin]:0.1:tspan[end] + diff = abs.(Vector(sol_d(t)) - sol_ref(t)) + ref = abs.(sol_ref(t)) + max_abserr = max(max_abserr, maximum(diff)) + max_relerr = max(max_relerr, maximum(diff ./ max.(ref, eps()))) + end + cond = (max_abserr < 1e-5 || max_relerr < 1e-5) + cond || println("Solver $sol abstol=$max_abserr retlol=$max_relerr") + @test (max_abserr < 1e-5 || max_relerr < 1e-5) +end -prob_d_dns = ODEProblem(odef_d_dns, u0_d, tspan, p_d) -prob_d_csc = ODEProblem(odef_d_csc, u0_d, tspan, p_d) -prob_d_csr = ODEProblem(odef_d_csr, u0_d, tspan, p_d) +# Jacobian prototype options +jac_prots = [ + "none" => nothing, + "CSC" => CUDA.CUSPARSE.CuSparseMatrixCSC(jac_prototype), + "CSR" => CUDA.CUSPARSE.CuSparseMatrixCSR(jac_prototype), +] -sol_d_dns = solve(prob_d_dns, Rodas5P()) # works -sol_d_csc = solve(prob_d_csc, Rodas5P()) # uses Krylov -sol_d_csr = solve(prob_d_csr, Rodas5P()) # uses CUDSS +# Solver options +solvers = [ + # Rosenbrock (diagonal mass matrix only) + "Rosenbrock23" => Rosenbrock23(), # ✅ + "Rosenbrock32" => Rosenbrock32(), # 📉 poor fit for this DAE, 💥 CSC catastrophic + # Rosenbrock 2nd order + "ROS2" => ROS2(), # ✅ + "ROS2PR" => ROS2PR(), # ✅ + "ROS2S" => ROS2S(), # ✅ + # Rosenbrock 3rd order + "ROS3" => ROS3(), # ✅ + "ROS3PR" => ROS3PR(), # 📉 poor fit for this DAE + "ROS3PRL" => ROS3PRL(), # ⚠️ CSC accuracy loss + "ROS3PRL2" => ROS3PRL2(), # ⚠️ CSC accuracy loss + "ROS3P" => ROS3P(), # 📉 poor fit for this DAE + "Rodas3" => Rodas3(), # ⚠️ CSC accuracy loss + # "Rodas23W" => Rodas23W(), # 🚫 scalar indexing, requires large cahnges to `calculate_interpoldiff!` + # "Rodas3P" => Rodas3P(), # 🚫 scalar indexing + "Scholz4_7" => Scholz4_7(), # ✅ + # Rosenbrock 4th order + "ROS34PW1a" => ROS34PW1a(), # 📉 poor fit for this DAE + "ROS34PW1b" => ROS34PW1b(), # 📉 poor fit for this DAE + "ROS34PW2" => ROS34PW2(), # ⚠️ CSC accuracy loss + "ROS34PW3" => ROS34PW3(), # ✅ + "ROS34PRw" => ROS34PRw(), # ✅ + "RosShamp4" => RosShamp4(), # ✅ + "Veldd4" => Veldd4(), # ✅ + "Velds4" => Velds4(), # ✅ + "GRK4T" => GRK4T(), # ✅ + "GRK4A" => GRK4A(), # ✅ + "Ros4LStab" => Ros4LStab(), # ✅ + "Rodas4" => Rodas4(), # ✅ + "Rodas42" => Rodas42(), # ✅ + "Rodas4P" => Rodas4P(), # ✅ + "Rodas4P2" => Rodas4P2(), # ✅ + "ROK4a" => ROK4a(), # ✅ + # Rosenbrock 5th order + "Rodas5" => Rodas5(), # ✅ + "Rodas5P" => Rodas5P(), # ✅ + "Rodas5Pe" => Rodas5Pe(), # ✅ + "Rodas5Pr" => Rodas5Pr(), # ✅ + # Rosenbrock 6th order + "Rodas6P" => Rodas6P(), # ✅ + # SDIRK (don't include fixed time step which need explicit dt) + "ImplicitEuler" => ImplicitEuler(), # ✅ + "Trapezoid" => Trapezoid(), # ✅ + "SDIRK2" => SDIRK2(), # ✅ + "Cash4" => Cash4(), # ⚠️ CSC accuracy loss + "Hairer4" => Hairer4(), # ⚠️ CSC accuracy loss + "Hairer42" => Hairer42(), # ⚠️ CSC accuracy loss + # BDF + "ABDF2" => ABDF2(), # ⚠️ CSC accuracy loss (💥 catastrophic) + "QNDF1" => QNDF1(), # ✅ + "QNDF2" => QNDF2(), # ⚠️ CSC accuracy loss + # "QNDF" => QNDF(), # 🔧 DeviceMemory error in LinAlg + "QBDF1" => QBDF1(), # ✅ + "QBDF2" => QBDF2(), # ⚠️ accuracy loss (all jac_prots) + # "QBDF" => QBDF(), # 🔧 DeviceMemory error in LinAlg + # "FBDF" => FBDF(), # 🚫 scalar indexing -> needs extensive work on reinitFBDF! + # FIRK -> all need subtential changes to `perform_step!` for FIRK methods + # "RadauIIA3" => RadauIIA3(), # 🚫 scalar indexing, ComplexF64 sparse unsupported + # "RadauIIA5" => RadauIIA5(), # 🚫 scalar indexing, ComplexF64 sparse unsupported + # "RadauIIA9" => RadauIIA9(), # 🚫 scalar indexing, ComplexF64 sparse unsupported + # "AdaptiveRadau" => AdaptiveRadau(), # 🚫 scalar indexing, ComplexF64 sparse unsupported +] -@testset "Test constraints in GPU sol" begin - for sol_d in [sol_d_dns, sol_d_csc, sol_d_csr] - for t in sol_d.t - u = Vector(sol_d(t)) - @test isapprox(u[1] + u[2], u[3]; atol = 1.0e-6) - @test isapprox(-u[1] + u[2], u[4]; atol = 1.0e-6) - end +function _maxerrs(sol_a, sol_b) + max_abs = 0.0 + max_rel = 0.0 + for t in tspan[begin]:0.1:tspan[end] + a = isa(sol_a(t), CuArray) ? Vector(sol_a(t)) : Vector(sol_a(t)) + b = isa(sol_b(t), CuArray) ? Vector(sol_b(t)) : Vector(sol_b(t)) + diff = abs.(a - b) + ref = abs.(b) + max_abs = max(max_abs, maximum(diff)) + max_rel = max(max_rel, maximum(diff ./ max.(ref, eps()))) end + return max_abs, max_rel end -@testset "Compare GPU to CPU solution" begin - for sol_d in [sol_d_dns, sol_d_csc, sol_d_csr] - for t in tspan[begin]:0.1:tspan[end] - @test Vector(sol_d(t)) ≈ sol(t) atol=1e-5 +function _printval(val; threshold = 1e-3) + s = @sprintf("%.2e", val) + if val > threshold + printstyled(s; color = :red) + else + print(s) + end +end + +function debug_gpu_dae(jac_prototype_d, solver, name) + # CPU: this solver vs Rodas5P reference + sol_cpu = solve(prob, solver) + cpu_abs, cpu_rel = _maxerrs(sol_cpu, sol) + + # GPU solve + odef_d = ODEFunction(dae!, mass_matrix = mass_matrix_d, jac_prototype = jac_prototype_d) + prob_d = ODEProblem(odef_d, u0_d, tspan, p_d) + sol_d = solve(prob_d, solver) + + # GPU vs CPU same solver + gpu_abs, gpu_rel = _maxerrs(sol_d, sol_cpu) + + # Print row + print(rpad(name, 30)) + _printval(cpu_abs); print(" ") + _printval(cpu_rel); print(" ") + _printval(gpu_abs); print(" ") + _printval(gpu_rel) + println() +end + +using Printf + +println(rpad("", 30), "cpu_abs cpu_rel gpu_abs gpu_rel") +println("-"^72) + +for (sn, sv) in solvers, (jn, jp) in jac_prots + label = "$sn / $jn" + try + debug_gpu_dae(jp, sv, label) + catch e + printstyled(rpad(label, 30), "ERROR: ", sprint(showerror, e; context=:limit=>true), "\n"; color = :yellow) + end +end + +println("-"^72) + +#= Uncomment for actual testing +@testset "End-to-end GPU compat of mass matrix DAE solvers" begin + for (sn, sv) in solvers + @testset "GPU DAE: $sn" begin + for (jn, jp) in jac_prots + @testset "Jacboain prototype: $jn" begin + test_gpu_dae(jp, sv) + end + end end end end +=# From 7e01cc129c61ab1aa514e73b88dc6aba1acb7fa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Mon, 23 Feb 2026 11:30:31 +0100 Subject: [PATCH 06/20] improve test script --- test/gpu/simple_dae.jl | 329 +++++++++++++++++++++++------------------ 1 file changed, 185 insertions(+), 144 deletions(-) diff --git a/test/gpu/simple_dae.jl b/test/gpu/simple_dae.jl index 0849fc604af..d43c93b2731 100644 --- a/test/gpu/simple_dae.jl +++ b/test/gpu/simple_dae.jl @@ -35,122 +35,159 @@ jac_prototype = sparse(map(x -> iszero(x) ? 0.0 : 1.0, p)) u0 = [1.0, 1.0, 0.5, 0.5] # force init tspan = (0.0, 5.0) -# CPU reference solution +# CPU reference solution (Rodas5P) odef = ODEFunction(dae!, mass_matrix = mass_matrix, jac_prototype = jac_prototype) prob = ODEProblem(odef, u0, tspan, p) -sol = solve(prob, Rodas5P()) +sol_ref = solve(prob, Rodas5P()) -# GPU data -- we use F64 for higher accuracy for comparision +# GPU data -- we use F64 for higher accuracy for comparison u0_d = adapt(CuArray{Float64}, u0) p_d = adapt(CuArray{Float64}, p) -# dense or spares mass matrix does not work yet! +# dense or sparse mass matrix does not work yet! mass_matrix_d = cu(mass_matrix) -function test_gpu_dae(jac_prototype_d, solver) - sol_ref = solve(prob, solver) - odef_d = ODEFunction(dae!, mass_matrix = mass_matrix_d, jac_prototype = jac_prototype_d) - prob_d = ODEProblem(odef_d, u0_d, tspan, p_d) - sol_d = solve(prob_d, solver) - - for t in sol_d.t - u = Vector(sol_d(t)) - @test isapprox(u[1] + u[2], u[3]; atol = 1.0e-6) - @test isapprox(-u[1] + u[2], u[4]; atol = 1.0e-6) - end - - max_abserr = 0.0 - max_relerr = 0.0 - for t in tspan[begin]:0.1:tspan[end] - diff = abs.(Vector(sol_d(t)) - sol_ref(t)) - ref = abs.(sol_ref(t)) - max_abserr = max(max_abserr, maximum(diff)) - max_relerr = max(max_relerr, maximum(diff ./ max.(ref, eps()))) - end - cond = (max_abserr < 1e-5 || max_relerr < 1e-5) - cond || println("Solver $sol abstol=$max_abserr retlol=$max_relerr") - @test (max_abserr < 1e-5 || max_relerr < 1e-5) -end - -# Jacobian prototype options +# ── Jacobian prototype options ──────────────────────────────────────────────── jac_prots = [ "none" => nothing, "CSC" => CUDA.CUSPARSE.CuSparseMatrixCSC(jac_prototype), "CSR" => CUDA.CUSPARSE.CuSparseMatrixCSR(jac_prototype), ] -# Solver options +# ── Solver definitions ──────────────────────────────────────────────────────── +# +# Each entry: solver => (; tol overrides...) +# method_tol = (; atol, rtol) – cpu solver vs Rodas5P reference (some methods are poor fits) +# gpu_tol = (; atol, rtol) – gpu vs cpu (none jac_prot) +# csc_tol = (; atol, rtol) – gpu vs cpu (CSC jac_prot) +# csr_tol = (; atol, rtol) – gpu vs cpu (CSR jac_prot) +# Only specify fields that deviate from the defaults. + +const DEFAULT_METHOD_TOL = (; atol = 1e-4, rtol = 1e-4) # generous: we only care about GPU vs CPU match +const DEFAULT_GPU_TOL = (; atol = 1e-4, rtol = 1e-4) + solvers = [ - # Rosenbrock (diagonal mass matrix only) - "Rosenbrock23" => Rosenbrock23(), # ✅ - "Rosenbrock32" => Rosenbrock32(), # 📉 poor fit for this DAE, 💥 CSC catastrophic - # Rosenbrock 2nd order - "ROS2" => ROS2(), # ✅ - "ROS2PR" => ROS2PR(), # ✅ - "ROS2S" => ROS2S(), # ✅ - # Rosenbrock 3rd order - "ROS3" => ROS3(), # ✅ - "ROS3PR" => ROS3PR(), # 📉 poor fit for this DAE - "ROS3PRL" => ROS3PRL(), # ⚠️ CSC accuracy loss - "ROS3PRL2" => ROS3PRL2(), # ⚠️ CSC accuracy loss - "ROS3P" => ROS3P(), # 📉 poor fit for this DAE - "Rodas3" => Rodas3(), # ⚠️ CSC accuracy loss - # "Rodas23W" => Rodas23W(), # 🚫 scalar indexing, requires large cahnges to `calculate_interpoldiff!` - # "Rodas3P" => Rodas3P(), # 🚫 scalar indexing - "Scholz4_7" => Scholz4_7(), # ✅ - # Rosenbrock 4th order - "ROS34PW1a" => ROS34PW1a(), # 📉 poor fit for this DAE - "ROS34PW1b" => ROS34PW1b(), # 📉 poor fit for this DAE - "ROS34PW2" => ROS34PW2(), # ⚠️ CSC accuracy loss - "ROS34PW3" => ROS34PW3(), # ✅ - "ROS34PRw" => ROS34PRw(), # ✅ - "RosShamp4" => RosShamp4(), # ✅ - "Veldd4" => Veldd4(), # ✅ - "Velds4" => Velds4(), # ✅ - "GRK4T" => GRK4T(), # ✅ - "GRK4A" => GRK4A(), # ✅ - "Ros4LStab" => Ros4LStab(), # ✅ - "Rodas4" => Rodas4(), # ✅ - "Rodas42" => Rodas42(), # ✅ - "Rodas4P" => Rodas4P(), # ✅ - "Rodas4P2" => Rodas4P2(), # ✅ - "ROK4a" => ROK4a(), # ✅ - # Rosenbrock 5th order - "Rodas5" => Rodas5(), # ✅ - "Rodas5P" => Rodas5P(), # ✅ - "Rodas5Pe" => Rodas5Pe(), # ✅ - "Rodas5Pr" => Rodas5Pr(), # ✅ - # Rosenbrock 6th order - "Rodas6P" => Rodas6P(), # ✅ - # SDIRK (don't include fixed time step which need explicit dt) - "ImplicitEuler" => ImplicitEuler(), # ✅ - "Trapezoid" => Trapezoid(), # ✅ - "SDIRK2" => SDIRK2(), # ✅ - "Cash4" => Cash4(), # ⚠️ CSC accuracy loss - "Hairer4" => Hairer4(), # ⚠️ CSC accuracy loss - "Hairer42" => Hairer42(), # ⚠️ CSC accuracy loss - # BDF - "ABDF2" => ABDF2(), # ⚠️ CSC accuracy loss (💥 catastrophic) - "QNDF1" => QNDF1(), # ✅ - "QNDF2" => QNDF2(), # ⚠️ CSC accuracy loss - # "QNDF" => QNDF(), # 🔧 DeviceMemory error in LinAlg - "QBDF1" => QBDF1(), # ✅ - "QBDF2" => QBDF2(), # ⚠️ accuracy loss (all jac_prots) - # "QBDF" => QBDF(), # 🔧 DeviceMemory error in LinAlg - # "FBDF" => FBDF(), # 🚫 scalar indexing -> needs extensive work on reinitFBDF! - # FIRK -> all need subtential changes to `perform_step!` for FIRK methods - # "RadauIIA3" => RadauIIA3(), # 🚫 scalar indexing, ComplexF64 sparse unsupported - # "RadauIIA5" => RadauIIA5(), # 🚫 scalar indexing, ComplexF64 sparse unsupported - # "RadauIIA9" => RadauIIA9(), # 🚫 scalar indexing, ComplexF64 sparse unsupported - # "AdaptiveRadau" => AdaptiveRadau(), # 🚫 scalar indexing, ComplexF64 sparse unsupported + # ── Rosenbrock 2nd order ── + Rosenbrock23() => (; + csc_tol = (; atol = 2e-4, rtol = 5e-4), + ), + Rosenbrock32() => (; + csc_tol = (; atol = Inf, rtol = Inf), + ), + ROS2() => (;), + ROS2PR() => (; + csc_tol = (; atol = 3e-4, rtol = 4e-4), + ), + ROS2S() => (; + csc_tol = (; atol = 7e-4, rtol = 1.2e-3), + ), + # ── Rosenbrock 3rd order ── + ROS3() => (;), + ROS3PR() => (;), + ROS3PRL() => (; + csc_tol = (; atol = 2e-3, rtol = 4e-3), + ), + ROS3PRL2() => (; + csc_tol = (; atol = 3e-3, rtol = 4e-3), + ), + ROS3P() => (;), + Rodas3() => (; + csc_tol = (; atol = 2e-3, rtol = 3e-3), + ), + # Rodas23W() # 🚫 scalar indexing, requires large changes to `calculate_interpoldiff!` + # Rodas3P() # 🚫 scalar indexing + Scholz4_7() => (;), + # ── Rosenbrock 4th order ── + ROS34PW1a() => (;), + ROS34PW1b() => (;), + ROS34PW2() => (; + csc_tol = (; atol = 1.2e-3, rtol = 2.2e-3), + ), + ROS34PW3() => (;), + ROS34PRw() => (; + csc_tol = (; atol = 6e-4, rtol = 1e-3), + ), + RosShamp4() => (;), + Veldd4() => (;), + Velds4() => (;), + GRK4T() => (;), + GRK4A() => (;), + Ros4LStab() => (;), + Rodas4() => (;), + Rodas42() => (; + csc_tol = (; atol = 2e-5, rtol = 1.3e-4), + ), + Rodas4P() => (; + csc_tol = (; atol = 9e-6, rtol = 1.3e-4), + ), + Rodas4P2() => (;), + ROK4a() => (;), + # ── Rosenbrock 5th order ── + Rodas5() => (;), + Rodas5P() => (;), + Rodas5Pe() => (;), + Rodas5Pr() => (;), + # ── Rosenbrock 6th order ── + Rodas6P() => (;), + # ── SDIRK (don't include fixed time step which need explicit dt) ── + ImplicitEuler() => (; + gpu_tol = (; atol = 1e-4, rtol = 4e-4), + csc_tol = (; atol = 6e-5, rtol = 3e-4), + ), + Trapezoid() => (;), + SDIRK2() => (; + csc_tol = (; atol = 1.5e-4, rtol = 2.1e-4), + ), + Cash4() => (; + csc_tol = (; atol = 5e-3, rtol = 8e-3), + ), + Hairer4() => (; + csc_tol = (; atol = 8e-3, rtol = 6e-2), + ), + Hairer42() => (; + csc_tol = (; atol = 7e-3, rtol = 5e-2), + ), + # ── BDF ── + ABDF2() => (; + csc_tol = (; atol = Inf, rtol = Inf), # 💥 CSC catastrophic + ), + QNDF1() => (;), + QNDF2() => (; + csc_tol = (; atol = 4e-3, rtol = 5e-3), + ), + # QNDF() # 🔧 DeviceMemory error in LinAlg + QBDF1() => (;), + QBDF2() => (; + gpu_tol = (; atol = 2e-3, rtol = 2e-3), + csc_tol = (; atol = 3e-3, rtol = 6e-3), + csr_tol = (; atol = 2e-3, rtol = 2e-3), + ), + # QBDF() # 🔧 DeviceMemory error in LinAlg + # FBDF() # 🚫 scalar indexing -> needs extensive work on reinitFBDF! + # ── FIRK -> all need substantial changes to `perform_step!` for FIRK methods ── + # RadauIIA3() # 🚫 scalar indexing, ComplexF64 sparse unsupported + # RadauIIA5() # 🚫 scalar indexing, ComplexF64 sparse unsupported + # RadauIIA9() # 🚫 scalar indexing, ComplexF64 sparse unsupported + # AdaptiveRadau() # 🚫 scalar indexing, ComplexF64 sparse unsupported ] -function _maxerrs(sol_a, sol_b) +# ── Helpers ─────────────────────────────────────────────────────────────────── + +function _get_tol(overrides, jac_name) + _tol_keys = Dict("none" => :gpu_tol, "CSC" => :csc_tol, "CSR" => :csr_tol) + key = _tol_keys[jac_name] + hasproperty(overrides, key) && return getproperty(overrides, key) + # gpu_tol acts as default for all GPU combos + hasproperty(overrides, :gpu_tol) && return overrides.gpu_tol + return DEFAULT_GPU_TOL +end + +function maxerrs(sol_a, sol_b) max_abs = 0.0 max_rel = 0.0 for t in tspan[begin]:0.1:tspan[end] - a = isa(sol_a(t), CuArray) ? Vector(sol_a(t)) : Vector(sol_a(t)) - b = isa(sol_b(t), CuArray) ? Vector(sol_b(t)) : Vector(sol_b(t)) + a = Vector(sol_a(t)) + b = Vector(sol_b(t)) diff = abs.(a - b) ref = abs.(b) max_abs = max(max_abs, maximum(diff)) @@ -159,63 +196,67 @@ function _maxerrs(sol_a, sol_b) return max_abs, max_rel end -function _printval(val; threshold = 1e-3) - s = @sprintf("%.2e", val) - if val > threshold - printstyled(s; color = :red) - else - print(s) - end -end +function run_dae_tests() + results = Any[] -function debug_gpu_dae(jac_prototype_d, solver, name) - # CPU: this solver vs Rodas5P reference - sol_cpu = solve(prob, solver) - cpu_abs, cpu_rel = _maxerrs(sol_cpu, sol) - - # GPU solve - odef_d = ODEFunction(dae!, mass_matrix = mass_matrix_d, jac_prototype = jac_prototype_d) - prob_d = ODEProblem(odef_d, u0_d, tspan, p_d) - sol_d = solve(prob_d, solver) - - # GPU vs CPU same solver - gpu_abs, gpu_rel = _maxerrs(sol_d, sol_cpu) - - # Print row - print(rpad(name, 30)) - _printval(cpu_abs); print(" ") - _printval(cpu_rel); print(" ") - _printval(gpu_abs); print(" ") - _printval(gpu_rel) - println() -end + for (sv, overrides) in solvers, (jn, jp) in jac_prots + sn = string(nameof(typeof(sv))) + (; atol, rtol) = _get_tol(overrides, jn) -using Printf + # CPU: this solver vs reference + sol_cpu = solve(prob, sv) + cpu_abs, cpu_rel = maxerrs(sol_cpu, sol_ref) -println(rpad("", 30), "cpu_abs cpu_rel gpu_abs gpu_rel") -println("-"^72) + # GPU solve + odef_d = ODEFunction(dae!, mass_matrix = mass_matrix_d, jac_prototype = jp) + prob_d = ODEProblem(odef_d, u0_d, tspan, p_d) + sol_d = solve(prob_d, sv) -for (sn, sv) in solvers, (jn, jp) in jac_prots - label = "$sn / $jn" - try - debug_gpu_dae(jp, sv, label) - catch e - printstyled(rpad(label, 30), "ERROR: ", sprint(showerror, e; context=:limit=>true), "\n"; color = :yellow) + # GPU vs CPU (same solver) + gpu_abs, gpu_rel = maxerrs(sol_d, sol_cpu) + + passed = (gpu_abs < atol) && (gpu_rel < rtol) + push!(results, (; solver = sn, jac = jn, cpu_abs, cpu_rel, gpu_abs, gpu_rel, + tol_abs = atol, tol_rel = rtol, passed, error = "")) + @test passed end + + return results end -println("-"^72) +function show_results(results) + function _fmt(val; threshold = 1e-3) + isnan(val) && return " --- " + s = @sprintf("%.2e", val) + if val > threshold + return "\e[31m$s\e[0m" + end + return s + end + + println(rpad("Solver / Jac", 30), "cpu_abs cpu_rel gpu_abs gpu_rel status") + println("-"^85) -#= Uncomment for actual testing -@testset "End-to-end GPU compat of mass matrix DAE solvers" begin - for (sn, sv) in solvers - @testset "GPU DAE: $sn" begin - for (jn, jp) in jac_prots - @testset "Jacboain prototype: $jn" begin - test_gpu_dae(jp, sv) - end + for r in results + label = rpad("$(r.solver) / $(r.jac)", 30) + if r.error != "" + printstyled(label, "ERROR: ", r.error, "\n"; color = :yellow) + else + print(label) + print(_fmt(r.cpu_abs), " ", _fmt(r.cpu_rel), " ") + print(_fmt(r.gpu_abs), " ", _fmt(r.gpu_rel), " ") + if r.passed + printstyled("✓\n"; color = :green) + else + printstyled("✗\n"; color = :red) end end end + println("-"^85) end -=# + +@testset "GPU DAE solver compatibility" begin + results = run_dae_tests() +end + +show_results(results) From 9f569d5dc53d5fceb4650093b6cb70ec19721847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Mon, 23 Feb 2026 12:21:36 +0100 Subject: [PATCH 07/20] improve test script --- test/gpu/simple_dae.jl | 138 +++++++++++++++++++++-------------------- 1 file changed, 71 insertions(+), 67 deletions(-) diff --git a/test/gpu/simple_dae.jl b/test/gpu/simple_dae.jl index d43c93b2731..83b13c734f0 100644 --- a/test/gpu/simple_dae.jl +++ b/test/gpu/simple_dae.jl @@ -10,6 +10,8 @@ using SparseArrays using Test using CUDSS using Printf +using OrdinaryDiffEqNonlinearSolve.LinearSolve: KrylovJL_GMRES + #= du[1] = -u[1] @@ -39,6 +41,7 @@ tspan = (0.0, 5.0) odef = ODEFunction(dae!, mass_matrix = mass_matrix, jac_prototype = jac_prototype) prob = ODEProblem(odef, u0, tspan, p) sol_ref = solve(prob, Rodas5P()) +sol_ref_krylov = solve(prob, Rodas5P(linsolve=KrylovJL_GMRES())) # GPU data -- we use F64 for higher accuracy for comparison u0_d = adapt(CuArray{Float64}, u0) @@ -68,107 +71,102 @@ const DEFAULT_GPU_TOL = (; atol = 1e-4, rtol = 1e-4) solvers = [ # ── Rosenbrock 2nd order ── - Rosenbrock23() => (; + Rosenbrock23 => (; csc_tol = (; atol = 2e-4, rtol = 5e-4), ), - Rosenbrock32() => (; + Rosenbrock32 => (; csc_tol = (; atol = Inf, rtol = Inf), ), - ROS2() => (;), - ROS2PR() => (; + ROS2 => (;), + ROS2PR => (; csc_tol = (; atol = 3e-4, rtol = 4e-4), ), - ROS2S() => (; + ROS2S => (; csc_tol = (; atol = 7e-4, rtol = 1.2e-3), ), # ── Rosenbrock 3rd order ── - ROS3() => (;), - ROS3PR() => (;), - ROS3PRL() => (; + ROS3 => (;), + ROS3PR => (;), + ROS3PRL => (; csc_tol = (; atol = 2e-3, rtol = 4e-3), ), - ROS3PRL2() => (; + ROS3PRL2 => (; csc_tol = (; atol = 3e-3, rtol = 4e-3), ), - ROS3P() => (;), - Rodas3() => (; + ROS3P => (;), + Rodas3 => (; csc_tol = (; atol = 2e-3, rtol = 3e-3), ), - # Rodas23W() # 🚫 scalar indexing, requires large changes to `calculate_interpoldiff!` - # Rodas3P() # 🚫 scalar indexing - Scholz4_7() => (;), + # Rodas23W() # scalar indexing, requires large changes to `calculate_interpoldiff!` + # Rodas3P() # scalar indexing + Scholz4_7 => (;), # ── Rosenbrock 4th order ── - ROS34PW1a() => (;), - ROS34PW1b() => (;), - ROS34PW2() => (; + ROS34PW1a => (;), + ROS34PW1b => (;), + ROS34PW2 => (; csc_tol = (; atol = 1.2e-3, rtol = 2.2e-3), ), - ROS34PW3() => (;), - ROS34PRw() => (; + ROS34PW3 => (;), + ROS34PRw => (; csc_tol = (; atol = 6e-4, rtol = 1e-3), ), - RosShamp4() => (;), - Veldd4() => (;), - Velds4() => (;), - GRK4T() => (;), - GRK4A() => (;), - Ros4LStab() => (;), - Rodas4() => (;), - Rodas42() => (; - csc_tol = (; atol = 2e-5, rtol = 1.3e-4), - ), - Rodas4P() => (; - csc_tol = (; atol = 9e-6, rtol = 1.3e-4), - ), - Rodas4P2() => (;), - ROK4a() => (;), + RosShamp4 => (;), + Veldd4 => (;), + Velds4 => (;), + GRK4T => (;), + GRK4A => (;), + Ros4LStab => (;), + Rodas4 => (;), + Rodas42 => (;), + Rodas4P => (;), + Rodas4P2 => (;), + ROK4a => (;), # ── Rosenbrock 5th order ── - Rodas5() => (;), - Rodas5P() => (;), - Rodas5Pe() => (;), - Rodas5Pr() => (;), + Rodas5 => (;), + Rodas5P => (;), + Rodas5Pe => (;), + Rodas5Pr => (;), # ── Rosenbrock 6th order ── - Rodas6P() => (;), + Rodas6P => (;), # ── SDIRK (don't include fixed time step which need explicit dt) ── - ImplicitEuler() => (; - gpu_tol = (; atol = 1e-4, rtol = 4e-4), - csc_tol = (; atol = 6e-5, rtol = 3e-4), + ImplicitEuler => (;), + Trapezoid => (; + csc_tol = (; atol = 8e-3, rtol = 2.5e-2), ), - Trapezoid() => (;), - SDIRK2() => (; - csc_tol = (; atol = 1.5e-4, rtol = 2.1e-4), + SDIRK2 => (; + csc_tol = (; atol = 1.5e-4, rtol = 3.5e-4), ), - Cash4() => (; + Cash4 => (; csc_tol = (; atol = 5e-3, rtol = 8e-3), ), - Hairer4() => (; + Hairer4 => (; csc_tol = (; atol = 8e-3, rtol = 6e-2), ), - Hairer42() => (; + Hairer42 => (; csc_tol = (; atol = 7e-3, rtol = 5e-2), ), # ── BDF ── - ABDF2() => (; - csc_tol = (; atol = Inf, rtol = Inf), # 💥 CSC catastrophic + ABDF2 => (; + csc_tol = (; atol = 2e-4, rtol = 7e-4), ), - QNDF1() => (;), - QNDF2() => (; + QNDF1 => (;), + QNDF2 => (; csc_tol = (; atol = 4e-3, rtol = 5e-3), ), # QNDF() # 🔧 DeviceMemory error in LinAlg - QBDF1() => (;), - QBDF2() => (; + QBDF1 => (;), + QBDF2 => (; gpu_tol = (; atol = 2e-3, rtol = 2e-3), - csc_tol = (; atol = 3e-3, rtol = 6e-3), + csc_tol = (; atol = 4e-3, rtol = 7e-3), csr_tol = (; atol = 2e-3, rtol = 2e-3), ), - # QBDF() # 🔧 DeviceMemory error in LinAlg - # FBDF() # 🚫 scalar indexing -> needs extensive work on reinitFBDF! + # QBDF() # DeviceMemory error in LinAlg + # FBDF() # scalar indexing -> needs extensive work on reinitFBDF! # ── FIRK -> all need substantial changes to `perform_step!` for FIRK methods ── - # RadauIIA3() # 🚫 scalar indexing, ComplexF64 sparse unsupported - # RadauIIA5() # 🚫 scalar indexing, ComplexF64 sparse unsupported - # RadauIIA9() # 🚫 scalar indexing, ComplexF64 sparse unsupported - # AdaptiveRadau() # 🚫 scalar indexing, ComplexF64 sparse unsupported + # RadauIIA3() # scalar indexing, ComplexF64 sparse unsupported + # RadauIIA5() # scalar indexing, ComplexF64 sparse unsupported + # RadauIIA9() # scalar indexing, ComplexF64 sparse unsupported + # AdaptiveRadau() # scalar indexing, ComplexF64 sparse unsupported ] # ── Helpers ─────────────────────────────────────────────────────────────────── @@ -200,22 +198,28 @@ function run_dae_tests() results = Any[] for (sv, overrides) in solvers, (jn, jp) in jac_prots - sn = string(nameof(typeof(sv))) + println("Test $sv with prototype $jn") + sn = string(sv) (; atol, rtol) = _get_tol(overrides, jn) + # CSC will allways fall back to krylov so the ref solution should do to + krylov = (jn == "CSC") + _sol_ref = krylov ? sol_ref_krylov : sol_ref + # CPU: this solver vs reference - sol_cpu = solve(prob, sv) - cpu_abs, cpu_rel = maxerrs(sol_cpu, sol_ref) + cpu_alg = krylov ? sv(linsolve=KrylovJL_GMRES()) : sv() + sol_cpu = solve(prob, cpu_alg) + cpu_abs, cpu_rel = maxerrs(sol_cpu, _sol_ref) # GPU solve odef_d = ODEFunction(dae!, mass_matrix = mass_matrix_d, jac_prototype = jp) prob_d = ODEProblem(odef_d, u0_d, tspan, p_d) - sol_d = solve(prob_d, sv) + sol_d = solve(prob_d, sv()) # GPU vs CPU (same solver) gpu_abs, gpu_rel = maxerrs(sol_d, sol_cpu) - passed = (gpu_abs < atol) && (gpu_rel < rtol) + passed = (gpu_abs < atol) || (gpu_rel < rtol) push!(results, (; solver = sn, jac = jn, cpu_abs, cpu_rel, gpu_abs, gpu_rel, tol_abs = atol, tol_rel = rtol, passed, error = "")) @test passed @@ -256,7 +260,7 @@ function show_results(results) end @testset "GPU DAE solver compatibility" begin + global results results = run_dae_tests() end - show_results(results) From abd90043e04e233c15fd9aca9d7b9847ec4a14fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Mon, 23 Feb 2026 12:42:47 +0100 Subject: [PATCH 08/20] improve test script --- test/gpu/simple_dae.jl | 62 ++++++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/test/gpu/simple_dae.jl b/test/gpu/simple_dae.jl index 83b13c734f0..adf2c54d8e6 100644 --- a/test/gpu/simple_dae.jl +++ b/test/gpu/simple_dae.jl @@ -59,14 +59,15 @@ jac_prots = [ # ── Solver definitions ──────────────────────────────────────────────────────── # -# Each entry: solver => (; tol overrides...) -# method_tol = (; atol, rtol) – cpu solver vs Rodas5P reference (some methods are poor fits) -# gpu_tol = (; atol, rtol) – gpu vs cpu (none jac_prot) -# csc_tol = (; atol, rtol) – gpu vs cpu (CSC jac_prot) -# csr_tol = (; atol, rtol) – gpu vs cpu (CSR jac_prot) +# Each entry: SolverType => (; tol overrides...) +# method_tol = (; atol, rtol) – cpu solver vs Rodas5P ref (all jac); some methods are poor fits for DAEs +# method_csc_tol = (; atol, rtol) – cpu solver vs Rodas5P ref (CSC/Krylov path only) +# gpu_tol = (; atol, rtol) – gpu vs cpu (none/CSR fallback) +# csc_tol = (; atol, rtol) – gpu vs cpu (CSC jac_prot) +# csr_tol = (; atol, rtol) – gpu vs cpu (CSR jac_prot) # Only specify fields that deviate from the defaults. -const DEFAULT_METHOD_TOL = (; atol = 1e-4, rtol = 1e-4) # generous: we only care about GPU vs CPU match +const DEFAULT_METHOD_TOL = (; atol = 1e-2, rtol = 1e-2) const DEFAULT_GPU_TOL = (; atol = 1e-4, rtol = 1e-4) solvers = [ @@ -75,7 +76,8 @@ solvers = [ csc_tol = (; atol = 2e-4, rtol = 5e-4), ), Rosenbrock32 => (; - csc_tol = (; atol = Inf, rtol = Inf), + method_tol = (; atol = 2e-2, rtol = 1.5), + csc_tol = (; atol = Inf, rtol = Inf), ), ROS2 => (;), ROS2PR => (; @@ -86,14 +88,18 @@ solvers = [ ), # ── Rosenbrock 3rd order ── ROS3 => (;), - ROS3PR => (;), + ROS3PR => (; + method_tol = (; atol = 0.25, rtol = 15.0), + ), ROS3PRL => (; csc_tol = (; atol = 2e-3, rtol = 4e-3), ), ROS3PRL2 => (; csc_tol = (; atol = 3e-3, rtol = 4e-3), ), - ROS3P => (;), + ROS3P => (; + method_tol = (; atol = 0.25, rtol = 15.0), + ), Rodas3 => (; csc_tol = (; atol = 2e-3, rtol = 3e-3), ), @@ -101,8 +107,12 @@ solvers = [ # Rodas3P() # scalar indexing Scholz4_7 => (;), # ── Rosenbrock 4th order ── - ROS34PW1a => (;), - ROS34PW1b => (;), + ROS34PW1a => (; + method_tol = (; atol = 0.25, rtol = 5.0), + ), + ROS34PW1b => (; + method_tol = (; atol = 0.25, rtol = 5.0), + ), ROS34PW2 => (; csc_tol = (; atol = 1.2e-3, rtol = 2.2e-3), ), @@ -147,14 +157,17 @@ solvers = [ ), # ── BDF ── ABDF2 => (; - csc_tol = (; atol = 2e-4, rtol = 7e-4), + method_csc_tol = (; atol = Inf, rtol = Inf), # ABDF2 + Krylov diverges vs Rodas5P + Krylov + csc_tol = (; atol = 2e-4, rtol = 7e-4), ), QNDF1 => (;), QNDF2 => (; csc_tol = (; atol = 4e-3, rtol = 5e-3), ), # QNDF() # 🔧 DeviceMemory error in LinAlg - QBDF1 => (;), + QBDF1 => (; + method_tol = (; atol = 2e-2, rtol = 0.2), + ), QBDF2 => (; gpu_tol = (; atol = 2e-3, rtol = 2e-3), csc_tol = (; atol = 4e-3, rtol = 7e-3), @@ -171,6 +184,12 @@ solvers = [ # ── Helpers ─────────────────────────────────────────────────────────────────── +function _get_method_tol(overrides, jac_name) + jac_name == "CSC" && hasproperty(overrides, :method_csc_tol) && return overrides.method_csc_tol + hasproperty(overrides, :method_tol) && return overrides.method_tol + return DEFAULT_METHOD_TOL +end + function _get_tol(overrides, jac_name) _tol_keys = Dict("none" => :gpu_tol, "CSC" => :csc_tol, "CSR" => :csr_tol) key = _tol_keys[jac_name] @@ -200,7 +219,8 @@ function run_dae_tests() for (sv, overrides) in solvers, (jn, jp) in jac_prots println("Test $sv with prototype $jn") sn = string(sv) - (; atol, rtol) = _get_tol(overrides, jn) + gtol = _get_tol(overrides, jn) + mtol = _get_method_tol(overrides, jn) # CSC will allways fall back to krylov so the ref solution should do to krylov = (jn == "CSC") @@ -219,9 +239,11 @@ function run_dae_tests() # GPU vs CPU (same solver) gpu_abs, gpu_rel = maxerrs(sol_d, sol_cpu) - passed = (gpu_abs < atol) || (gpu_rel < rtol) + method_passed = (cpu_abs < mtol.atol) || (cpu_rel < mtol.rtol) + gpu_passed = (gpu_abs < gtol.atol) || (gpu_rel < gtol.rtol) + passed = method_passed && gpu_passed push!(results, (; solver = sn, jac = jn, cpu_abs, cpu_rel, gpu_abs, gpu_rel, - tol_abs = atol, tol_rel = rtol, passed, error = "")) + gtol, mtol, method_passed, gpu_passed, passed, error = "")) @test passed end @@ -247,12 +269,16 @@ function show_results(results) printstyled(label, "ERROR: ", r.error, "\n"; color = :yellow) else print(label) - print(_fmt(r.cpu_abs), " ", _fmt(r.cpu_rel), " ") + print(_fmt(r.cpu_abs, threshold=1e-2), " ", _fmt(r.cpu_rel, threshold=1e-2), " ") print(_fmt(r.gpu_abs), " ", _fmt(r.gpu_rel), " ") if r.passed printstyled("✓\n"; color = :green) + elseif !r.method_passed && !r.gpu_passed + printstyled("✗ method+gpu\n"; color = :red) + elseif !r.method_passed + printstyled("✗ method\n"; color = :red) else - printstyled("✗\n"; color = :red) + printstyled("✗ gpu\n"; color = :red) end end end From 962f71baa951a4eb747cfb2e27768c84ae43d819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Mon, 23 Feb 2026 12:49:18 +0100 Subject: [PATCH 09/20] adapt project.toml --- test/gpu/Project.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/gpu/Project.toml b/test/gpu/Project.toml index e77faa0429b..95f96621144 100644 --- a/test/gpu/Project.toml +++ b/test/gpu/Project.toml @@ -8,9 +8,11 @@ FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" FillArrays = "1a297f60-69ca-5386-bcde-b61e274b549b" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" OrdinaryDiffEqBDF = "6ad6398a-0878-4a85-9266-38940aa047c8" +OrdinaryDiffEqFIRK = "5960d6e9-dd7a-4743-88e7-cf307b64f125" OrdinaryDiffEqNonlinearSolve = "127b3ac7-2247-4354-8eb6-78cf4e7c58e8" OrdinaryDiffEqRKIP = "a4daff8c-1d43-4ff3-8eff-f78720aeecdc" OrdinaryDiffEqRosenbrock = "43230ef6-c299-4910-a778-202eb28ce4ce" +OrdinaryDiffEqSDIRK = "2d112036-d095-4a1e-ab9a-08536f3ecdbf" RecursiveArrayTools = "731186ca-8d62-57ce-b412-fbd966d074cd" SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" SciMLOperators = "c0aeaf25-5076-4817-a8d5-81caf7dfa961" From e4994317bcad2293d3222595d13f33b0751acb38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Mon, 23 Feb 2026 13:58:02 +0100 Subject: [PATCH 10/20] apply runic --- lib/OrdinaryDiffEqCore/src/misc_utils.jl | 4 +- test/gpu/simple_dae.jl | 114 ++++++++++++----------- 2 files changed, 61 insertions(+), 57 deletions(-) diff --git a/lib/OrdinaryDiffEqCore/src/misc_utils.jl b/lib/OrdinaryDiffEqCore/src/misc_utils.jl index f73a8c6e6d6..f1eb8254763 100644 --- a/lib/OrdinaryDiffEqCore/src/misc_utils.jl +++ b/lib/OrdinaryDiffEqCore/src/misc_utils.jl @@ -171,8 +171,8 @@ function find_algebraic_vars_eqs(M::Diagonal) end function find_algebraic_vars_eqs(M::AbstractMatrix) - algebraic_vars = vec(all(iszero, M, dims=1)) - algebraic_eqs = vec(all(iszero, M, dims=2)) + algebraic_vars = vec(all(iszero, M, dims = 1)) + algebraic_eqs = vec(all(iszero, M, dims = 2)) return algebraic_vars, algebraic_eqs end diff --git a/test/gpu/simple_dae.jl b/test/gpu/simple_dae.jl index adf2c54d8e6..44803183385 100644 --- a/test/gpu/simple_dae.jl +++ b/test/gpu/simple_dae.jl @@ -41,7 +41,7 @@ tspan = (0.0, 5.0) odef = ODEFunction(dae!, mass_matrix = mass_matrix, jac_prototype = jac_prototype) prob = ODEProblem(odef, u0, tspan, p) sol_ref = solve(prob, Rodas5P()) -sol_ref_krylov = solve(prob, Rodas5P(linsolve=KrylovJL_GMRES())) +sol_ref_krylov = solve(prob, Rodas5P(linsolve = KrylovJL_GMRES())) # GPU data -- we use F64 for higher accuracy for comparison u0_d = adapt(CuArray{Float64}, u0) @@ -67,41 +67,41 @@ jac_prots = [ # csr_tol = (; atol, rtol) – gpu vs cpu (CSR jac_prot) # Only specify fields that deviate from the defaults. -const DEFAULT_METHOD_TOL = (; atol = 1e-2, rtol = 1e-2) -const DEFAULT_GPU_TOL = (; atol = 1e-4, rtol = 1e-4) +const DEFAULT_METHOD_TOL = (; atol = 1.0e-2, rtol = 1.0e-2) +const DEFAULT_GPU_TOL = (; atol = 1.0e-4, rtol = 1.0e-4) solvers = [ # ── Rosenbrock 2nd order ── Rosenbrock23 => (; - csc_tol = (; atol = 2e-4, rtol = 5e-4), + csc_tol = (; atol = 2.0e-4, rtol = 5.0e-4), ), Rosenbrock32 => (; - method_tol = (; atol = 2e-2, rtol = 1.5), - csc_tol = (; atol = Inf, rtol = Inf), + method_tol = (; atol = 2.0e-2, rtol = 1.5), + csc_tol = (; atol = Inf, rtol = Inf), ), ROS2 => (;), ROS2PR => (; - csc_tol = (; atol = 3e-4, rtol = 4e-4), + csc_tol = (; atol = 3.0e-4, rtol = 4.0e-4), ), - ROS2S => (; - csc_tol = (; atol = 7e-4, rtol = 1.2e-3), + ROS2S => (; + csc_tol = (; atol = 7.0e-4, rtol = 1.2e-3), ), # ── Rosenbrock 3rd order ── - ROS3 => (;), - ROS3PR => (; + ROS3 => (;), + ROS3PR => (; method_tol = (; atol = 0.25, rtol = 15.0), ), - ROS3PRL => (; - csc_tol = (; atol = 2e-3, rtol = 4e-3), + ROS3PRL => (; + csc_tol = (; atol = 2.0e-3, rtol = 4.0e-3), ), ROS3PRL2 => (; - csc_tol = (; atol = 3e-3, rtol = 4e-3), + csc_tol = (; atol = 3.0e-3, rtol = 4.0e-3), ), - ROS3P => (; + ROS3P => (; method_tol = (; atol = 0.25, rtol = 15.0), ), - Rodas3 => (; - csc_tol = (; atol = 2e-3, rtol = 3e-3), + Rodas3 => (; + csc_tol = (; atol = 2.0e-3, rtol = 3.0e-3), ), # Rodas23W() # scalar indexing, requires large changes to `calculate_interpoldiff!` # Rodas3P() # scalar indexing @@ -113,65 +113,65 @@ solvers = [ ROS34PW1b => (; method_tol = (; atol = 0.25, rtol = 5.0), ), - ROS34PW2 => (; + ROS34PW2 => (; csc_tol = (; atol = 1.2e-3, rtol = 2.2e-3), ), - ROS34PW3 => (;), - ROS34PRw => (; - csc_tol = (; atol = 6e-4, rtol = 1e-3), + ROS34PW3 => (;), + ROS34PRw => (; + csc_tol = (; atol = 6.0e-4, rtol = 1.0e-3), ), RosShamp4 => (;), - Veldd4 => (;), - Velds4 => (;), - GRK4T => (;), - GRK4A => (;), + Veldd4 => (;), + Velds4 => (;), + GRK4T => (;), + GRK4A => (;), Ros4LStab => (;), - Rodas4 => (;), - Rodas42 => (;), - Rodas4P => (;), - Rodas4P2 => (;), - ROK4a => (;), + Rodas4 => (;), + Rodas42 => (;), + Rodas4P => (;), + Rodas4P2 => (;), + ROK4a => (;), # ── Rosenbrock 5th order ── - Rodas5 => (;), - Rodas5P => (;), + Rodas5 => (;), + Rodas5P => (;), Rodas5Pe => (;), Rodas5Pr => (;), # ── Rosenbrock 6th order ── - Rodas6P => (;), + Rodas6P => (;), # ── SDIRK (don't include fixed time step which need explicit dt) ── ImplicitEuler => (;), Trapezoid => (; - csc_tol = (; atol = 8e-3, rtol = 2.5e-2), + csc_tol = (; atol = 8.0e-3, rtol = 2.5e-2), ), - SDIRK2 => (; + SDIRK2 => (; csc_tol = (; atol = 1.5e-4, rtol = 3.5e-4), ), - Cash4 => (; - csc_tol = (; atol = 5e-3, rtol = 8e-3), + Cash4 => (; + csc_tol = (; atol = 5.0e-3, rtol = 8.0e-3), ), - Hairer4 => (; - csc_tol = (; atol = 8e-3, rtol = 6e-2), + Hairer4 => (; + csc_tol = (; atol = 8.0e-3, rtol = 6.0e-2), ), - Hairer42 => (; - csc_tol = (; atol = 7e-3, rtol = 5e-2), + Hairer42 => (; + csc_tol = (; atol = 7.0e-3, rtol = 5.0e-2), ), # ── BDF ── ABDF2 => (; method_csc_tol = (; atol = Inf, rtol = Inf), # ABDF2 + Krylov diverges vs Rodas5P + Krylov - csc_tol = (; atol = 2e-4, rtol = 7e-4), + csc_tol = (; atol = 2.0e-4, rtol = 7.0e-4), ), QNDF1 => (;), QNDF2 => (; - csc_tol = (; atol = 4e-3, rtol = 5e-3), + csc_tol = (; atol = 4.0e-3, rtol = 5.0e-3), ), # QNDF() # 🔧 DeviceMemory error in LinAlg QBDF1 => (; - method_tol = (; atol = 2e-2, rtol = 0.2), + method_tol = (; atol = 2.0e-2, rtol = 0.2), ), QBDF2 => (; - gpu_tol = (; atol = 2e-3, rtol = 2e-3), - csc_tol = (; atol = 4e-3, rtol = 7e-3), - csr_tol = (; atol = 2e-3, rtol = 2e-3), + gpu_tol = (; atol = 2.0e-3, rtol = 2.0e-3), + csc_tol = (; atol = 4.0e-3, rtol = 7.0e-3), + csr_tol = (; atol = 2.0e-3, rtol = 2.0e-3), ), # QBDF() # DeviceMemory error in LinAlg # FBDF() # scalar indexing -> needs extensive work on reinitFBDF! @@ -222,12 +222,12 @@ function run_dae_tests() gtol = _get_tol(overrides, jn) mtol = _get_method_tol(overrides, jn) - # CSC will allways fall back to krylov so the ref solution should do to + # CSC will always fall back to krylov so the ref solution should do to krylov = (jn == "CSC") _sol_ref = krylov ? sol_ref_krylov : sol_ref # CPU: this solver vs reference - cpu_alg = krylov ? sv(linsolve=KrylovJL_GMRES()) : sv() + cpu_alg = krylov ? sv(linsolve = KrylovJL_GMRES()) : sv() sol_cpu = solve(prob, cpu_alg) cpu_abs, cpu_rel = maxerrs(sol_cpu, _sol_ref) @@ -240,10 +240,14 @@ function run_dae_tests() gpu_abs, gpu_rel = maxerrs(sol_d, sol_cpu) method_passed = (cpu_abs < mtol.atol) || (cpu_rel < mtol.rtol) - gpu_passed = (gpu_abs < gtol.atol) || (gpu_rel < gtol.rtol) + gpu_passed = (gpu_abs < gtol.atol) || (gpu_rel < gtol.rtol) passed = method_passed && gpu_passed - push!(results, (; solver = sn, jac = jn, cpu_abs, cpu_rel, gpu_abs, gpu_rel, - gtol, mtol, method_passed, gpu_passed, passed, error = "")) + push!( + results, (; + solver = sn, jac = jn, cpu_abs, cpu_rel, gpu_abs, gpu_rel, + gtol, mtol, method_passed, gpu_passed, passed, error = "", + ) + ) @test passed end @@ -251,7 +255,7 @@ function run_dae_tests() end function show_results(results) - function _fmt(val; threshold = 1e-3) + function _fmt(val; threshold = 1.0e-3) isnan(val) && return " --- " s = @sprintf("%.2e", val) if val > threshold @@ -269,7 +273,7 @@ function show_results(results) printstyled(label, "ERROR: ", r.error, "\n"; color = :yellow) else print(label) - print(_fmt(r.cpu_abs, threshold=1e-2), " ", _fmt(r.cpu_rel, threshold=1e-2), " ") + print(_fmt(r.cpu_abs, threshold = 1.0e-2), " ", _fmt(r.cpu_rel, threshold = 1.0e-2), " ") print(_fmt(r.gpu_abs), " ", _fmt(r.gpu_rel), " ") if r.passed printstyled("✓\n"; color = :green) @@ -282,7 +286,7 @@ function show_results(results) end end end - println("-"^85) + return println("-"^85) end @testset "GPU DAE solver compatibility" begin From 46ea0495c19e3beb34ebb2bf38967f226e9349f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Mon, 23 Feb 2026 14:03:51 +0100 Subject: [PATCH 11/20] fix missing imports/packages --- .../test/algebraic_vars_detection_tests.jl | 1 + test/gpu/Project.toml | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/OrdinaryDiffEqCore/test/algebraic_vars_detection_tests.jl b/lib/OrdinaryDiffEqCore/test/algebraic_vars_detection_tests.jl index b76922f7103..dfbf333d585 100644 --- a/lib/OrdinaryDiffEqCore/test/algebraic_vars_detection_tests.jl +++ b/lib/OrdinaryDiffEqCore/test/algebraic_vars_detection_tests.jl @@ -1,6 +1,7 @@ using Test using SparseArrays using OrdinaryDiffEqCore: find_algebraic_vars_eqs +using LinearAlgebra @testset "Sparse Algebraic Detection Performance" begin # Test 1: Correctness - results should match between sparse and dense methods diff --git a/test/gpu/Project.toml b/test/gpu/Project.toml index 95f96621144..1f2d3b585d4 100644 --- a/test/gpu/Project.toml +++ b/test/gpu/Project.toml @@ -1,6 +1,7 @@ [deps] Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" +CUDSS = "45b445bb-4962-46a0-9369-b4df9d0f772e" ComponentArrays = "b0b7db55-cfe3-40fc-9ded-d10e2dbeff66" DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e" FastBroadcast = "7034ab61-46d4-4ed7-9d0f-46aef9175898" @@ -25,7 +26,8 @@ OrdinaryDiffEqRosenbrock = {path = "../../lib/OrdinaryDiffEqRosenbrock"} [compat] Adapt = "4" -CUDA = "4, 5" +CUDA = "5" +CUDSS = "0.6.7" ComponentArrays = "0.15" DiffEqBase = "7" FastBroadcast = "1.3" @@ -33,9 +35,11 @@ FFTW = "1.8" FillArrays = "1" OrdinaryDiffEq = "7" OrdinaryDiffEqBDF = "1" +OrdinaryDiffEqFIRK = "1" OrdinaryDiffEqNonlinearSolve = "1" OrdinaryDiffEqRKIP = "1" OrdinaryDiffEqRosenbrock = "1" +OrdinaryDiffEqSDIRK = "1" RecursiveArrayTools = "4" SciMLBase = "3" SciMLOperators = "1.3" From 24b90acd71637bec11552b7a82d9b2e7eb90b18f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Thu, 2 Jul 2026 14:22:39 +0200 Subject: [PATCH 12/20] WIP: dae_tests.jl harness + test/gpu env updates Preserve in-progress GPU DAE test harness before merging upstream/master. --- test/gpu/Project.toml | 12 +- test/gpu/dae_tests.jl | 420 +++++++++++++++++++++++++++++++++++++++++ test/gpu/simple_dae.jl | 14 +- 3 files changed, 438 insertions(+), 8 deletions(-) create mode 100644 test/gpu/dae_tests.jl diff --git a/test/gpu/Project.toml b/test/gpu/Project.toml index 1f2d3b585d4..ec8fa2c88ca 100644 --- a/test/gpu/Project.toml +++ b/test/gpu/Project.toml @@ -1,14 +1,18 @@ [deps] Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" +ArrayInterface = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9" CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" CUDSS = "45b445bb-4962-46a0-9369-b4df9d0f772e" ComponentArrays = "b0b7db55-cfe3-40fc-9ded-d10e2dbeff66" DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e" -FastBroadcast = "7034ab61-46d4-4ed7-9d0f-46aef9175898" FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" +FastBroadcast = "7034ab61-46d4-4ed7-9d0f-46aef9175898" FillArrays = "1a297f60-69ca-5386-bcde-b61e274b549b" +KrylovKit = "0b1a1467-8014-51b9-945f-bf0ae24f4b77" +LinearSolve = "7ed4a6bd-45f5-4d41-b270-4a48e9bafcae" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" OrdinaryDiffEqBDF = "6ad6398a-0878-4a85-9266-38940aa047c8" +OrdinaryDiffEqDifferentiation = "4302a76b-040a-498a-8c04-15b101fed76b" OrdinaryDiffEqFIRK = "5960d6e9-dd7a-4743-88e7-cf307b64f125" OrdinaryDiffEqNonlinearSolve = "127b3ac7-2247-4354-8eb6-78cf4e7c58e8" OrdinaryDiffEqRKIP = "a4daff8c-1d43-4ff3-8eff-f78720aeecdc" @@ -20,18 +24,20 @@ SciMLOperators = "c0aeaf25-5076-4817-a8d5-81caf7dfa961" [sources] OrdinaryDiffEqBDF = {path = "../../lib/OrdinaryDiffEqBDF"} +OrdinaryDiffEqFIRK = {path = "../../lib/OrdinaryDiffEqFIRK"} OrdinaryDiffEqNonlinearSolve = {path = "../../lib/OrdinaryDiffEqNonlinearSolve"} OrdinaryDiffEqRKIP = {path = "../../lib/OrdinaryDiffEqRKIP"} OrdinaryDiffEqRosenbrock = {path = "../../lib/OrdinaryDiffEqRosenbrock"} +OrdinaryDiffEqSDIRK = {path = "../../lib/OrdinaryDiffEqSDIRK"} [compat] Adapt = "4" -CUDA = "5" +CUDA = "6" CUDSS = "0.6.7" ComponentArrays = "0.15" DiffEqBase = "7" -FastBroadcast = "1.3" FFTW = "1.8" +FastBroadcast = "1.3" FillArrays = "1" OrdinaryDiffEq = "7" OrdinaryDiffEqBDF = "1" diff --git a/test/gpu/dae_tests.jl b/test/gpu/dae_tests.jl new file mode 100644 index 00000000000..42f0aaeaa7c --- /dev/null +++ b/test/gpu/dae_tests.jl @@ -0,0 +1,420 @@ +using OrdinaryDiffEqRosenbrock +using OrdinaryDiffEqSDIRK +using OrdinaryDiffEqBDF +using OrdinaryDiffEqFIRK +using OrdinaryDiffEqNonlinearSolve +using CUDA +using LinearAlgebra +using Adapt +using SparseArrays +using Test +using CUDSS +using Printf +using OrdinaryDiffEqNonlinearSolve.LinearSolve: KrylovJL_GMRES +using SciMLBase: ReturnCode, FullSpecialize + +#= +Test goal: exercise GPU code paths for stiff/DAE solvers with mass matrices. + +This is NOT a solver-quality test. We don't care whether Rosenbrock32 is a good +DAE solver; we care whether the GPU dispatch works and produces the same +result as the CPU dispatch for the same solver. + +Pass criterion: GPU solution matches CPU solution (same solver) within tolerance. +CPU-vs-reference is recorded for the table but does not gate pass/fail. + +The test matrix is (solver × jac_prototype × mass_matrix × linsolve). Each axis +is a simple vector at the top of the file — comment lines in/out to narrow scope. + +Note on FullSpecialize: ODEFunctions are built with `FullSpecialize` to avoid a +FunctionWrappers bug where the wrapper's compiled signature is too narrow for +some solver caches, producing `llvmcall requires the compiler` errors. With +FullSpecialize the function is specialized on concrete types directly and +FunctionWrappers is bypassed. + + du[1] = -u[1] + du[2] = -0.5*u[2] + 0 = u[1] + u[2] - u[3] + 0 = -u[1] + u[2] - u[4] +=# + +# ── Problem definition ─────────────────────────────────────────────────────── + +function dae!(du, u, p, t) + return mul!(du, p, u) +end + +P = [ + -1 0 0 0 + 1 -0.5 0 0 + 1 1 -1 0 + -1 1 0 -1 +] + +MASS_MATRIX = Diagonal([1, 1, 0, 0]) +JAC_PROTOTYPE = sparse(map(x -> iszero(x) ? 0.0 : 1.0, P)) +U0 = [1.0, 1.0, 0.5, 0.5] +TSPAN = (0.0, 5.0) + +INITALG = BrownFullBasicInit() + +# Shared solver kwargs. `maxiters` caps runaway integrations so a stuck +# solver fails cleanly rather than hangs indefinitely. +SOLVE_KWARGS = (; maxiters = 10_000) + +# Helper: build an ODEFunction with FullSpecialize (CPU or GPU). +make_odef(; mass_matrix, jac_prototype) = + ODEFunction(dae!; mass_matrix = mass_matrix, jac_prototype = jac_prototype) + +# ── CPU reference ──────────────────────────────────────────────────────────── + +ODEF_CPU = make_odef(; mass_matrix = MASS_MATRIX, jac_prototype = JAC_PROTOTYPE) +PROB_CPU = ODEProblem(ODEF_CPU, U0, TSPAN, P; initializealg = INITALG) +SOL_REF = solve(PROB_CPU, Rodas5P(); SOLVE_KWARGS...) +SOL_REF_KRYLOV = solve(PROB_CPU, Rodas5P(linsolve = KrylovJL_GMRES()); SOLVE_KWARGS...) + +# ── GPU problem data ───────────────────────────────────────────────────────── + +U0_D = adapt(CuArray{Float64}, U0) +P_D = adapt(CuArray{Float64}, P) +MASS_MATRIX_D_DIAG = cu(MASS_MATRIX) + +# ── Test matrix axes ───────────────────────────────────────────────────────── +# Comment lines to narrow scope during debugging. + +# Each entry: (name, jac_prototype_for_gpu, needs_krylov_on_cpu) +JAC_VARIANTS = [ + ("none", nothing, false), + ("CSC", CUDA.CUSPARSE.CuSparseMatrixCSC(JAC_PROTOTYPE), true), + ("CSR", CUDA.CUSPARSE.CuSparseMatrixCSR(JAC_PROTOTYPE), false), +] + +# Each entry: (name, gpu_mass_matrix) +MASS_VARIANTS = [ + ("diag_cu", MASS_MATRIX_D_DIAG), + # ("dense_cu", CuArray(Matrix(MASS_MATRIX))), # not supported yet + # ("sparse_cu", CUDA.CUSPARSE.CuSparseMatrixCSC(sparse(MASS_MATRIX))), # not supported yet +] + +# ── Solvers ────────────────────────────────────────────────────────────────── +# Classification reflects suitability for mass-matrix DAEs, purely informational +# for the report. Does NOT affect pass/fail — we only gate on GPU-vs-CPU match. +# +# :suitable — designed or known-good for index-1 mass-matrix DAEs +# :marginal — may lose order / show artifacts but typically runs +# :unsuitable — included only for GPU code-path coverage + +SOLVERS = [ + # ── Rosenbrock 2nd order ── + # (Rosenbrock23, :marginal), + # (Rosenbrock32, :unsuitable), # low-accuracy, used for coverage + (ROS2, :marginal), + (ROS2PR, :suitable), + (ROS2S, :suitable), + # ── Rosenbrock 3rd order ── + # (ROS3, :marginal), + (ROS3PR, :suitable), + (ROS3PRL, :suitable), + (ROS3PRL2, :suitable), + (ROS3P, :suitable), + (Rodas3, :suitable), + # Rodas23W — scalar indexing, needs `calculate_interpoldiff!` rework + # Rodas3P — scalar indexing + (Scholz4_7, :suitable), + # ── Rosenbrock 4th order ── + #= + (ROS34PW1a, :suitable), + (ROS34PW1b, :suitable), + (ROS34PW2, :suitable), + (ROS34PW3, :suitable), + (ROS34PRw, :suitable), + # (RosShamp4, :marginal), # classical Rosenbrock, not DAE-derived + # (Veldd4, :marginal), # d + (Velds4, :marginal), + # (GRK4T, :marginal), # does not work with CSC/Krylov + (GRK4A, :marginal), + (Ros4LStab, :marginal), + (Rodas4, :suitable), + (Rodas42, :suitable), + (Rodas4P, :suitable), + (Rodas4P2, :suitable), + # (ROK4a, :suitable), + # ── Rosenbrock 5th order ── + (Rodas5, :suitable), + (Rodas5P, :suitable), + # (Rodas5Pe, :suitable), + (Rodas5Pr, :suitable), + # ── Rosenbrock 6th order ── + (Rodas6P, :suitable), + # ── SDIRK ── + (ImplicitEuler, :marginal), + (Trapezoid, :unsuitable), # oscillates on algebraic states + (SDIRK2, :suitable), + (Cash4, :suitable), + (Hairer4, :suitable), + (Hairer42, :suitable), + # ── BDF ── + (ABDF2, :suitable), + (QNDF1, :marginal), + (QNDF2, :suitable), + # QNDF — DeviceMemory error in LinAlg + # (QBDF1, :marginal), + # (QBDF2, :suitable), + # QBDF — DeviceMemory error in LinAlg + # FBDF — scalar indexing, needs reinitFBDF! rework + # ── FIRK (all need `perform_step!` rework) ── + # RadauIIA3, RadauIIA5, RadauIIA9, AdaptiveRadau + # — scalar indexing, ComplexF64 sparse unsupported + =# +] + +# ── Pass-criterion configuration ───────────────────────────────────────────── + +# GPU must match CPU within this tolerance. Single knob. +GPU_MATCH_TOL = (atol = 1.0e-3, rtol = 1.0e-3) + +# Known-exception list for solvers that genuinely can't meet GPU_MATCH_TOL +# (e.g. intrinsically low accuracy, non-L-stable artifacts). Entries here +# should be rare and each should be justified in a comment. +LOOSE_GPU_MATCH_TOL = Dict{Type, NamedTuple}( + # Rosenbrock32 => (atol = Inf, rtol = Inf), # low-accuracy method + # Trapezoid => (atol = 1e-2, rtol = 3e-2), # oscillation amplitude drift +) + +gpu_match_tol(solver) = get(LOOSE_GPU_MATCH_TOL, solver, GPU_MATCH_TOL) + +# ── Types ──────────────────────────────────────────────────────────────────── + +struct TestCase + solver::Any + solver_class::Symbol + jac_name::String + jac_prototype::Any + needs_krylov_cpu::Bool + mass_name::String + mass_matrix::Any +end + +Base.show(io::IO, c::TestCase) = print(io, + "$(nameof(c.solver)) [jac=$(c.jac_name), mass=$(c.mass_name)]") + +mutable struct TestResult + case::TestCase + cpu_abs::Float64 # vs reference (informational) + cpu_rel::Float64 + gpu_abs::Float64 # vs CPU same-solver (pass/fail) + gpu_rel::Float64 + cpu_retcode::Union{Nothing, ReturnCode.T} + gpu_retcode::Union{Nothing, ReturnCode.T} + cpu_error::Union{Nothing, Exception} + gpu_error::Union{Nothing, Exception} + status::Symbol # :pass, :gpu_mismatch, :cpu_failed, :gpu_failed, + # :cpu_error, :gpu_error +end + +# ── Helpers ────────────────────────────────────────────────────────────────── + +function build_cases(; solvers = SOLVERS, + jac_variants = JAC_VARIANTS, + mass_variants = MASS_VARIANTS) + cases = TestCase[] + for (sv, cls) in solvers, + (jn, jp, needs_krylov) in jac_variants, + (mn, mm) in mass_variants + push!(cases, TestCase(sv, cls, jn, jp, needs_krylov, mn, mm)) + end + return cases +end + +function maxerrs(sol_a, sol_b) + max_abs = 0.0 + max_rel = 0.0 + for t in TSPAN[1]:0.1:TSPAN[2] + a = Vector(sol_a(t)) + b = Vector(sol_b(t)) + diff = abs.(a .- b) + ref = abs.(b) + max_abs = max(max_abs, maximum(diff)) + max_rel = max(max_rel, maximum(diff ./ max.(ref, eps()))) + end + return max_abs, max_rel +end + +within_tol(abs_err, rel_err, tol) = + abs_err < tol.atol || rel_err < tol.rtol + +ok(retcode) = retcode === ReturnCode.Success || retcode === ReturnCode.Default + +# ── Run a single case ──────────────────────────────────────────────────────── + +function run_case(case::TestCase) + result = TestResult(case, NaN, NaN, NaN, NaN, + nothing, nothing, nothing, nothing, :pending) + + # CPU run --------------------------------------------------------------- + ref = case.needs_krylov_cpu ? SOL_REF_KRYLOV : SOL_REF + cpu_alg = case.needs_krylov_cpu ? + case.solver(linsolve = KrylovJL_GMRES()) : + case.solver() + + sol_cpu = nothing + try + sol_cpu = solve(PROB_CPU, cpu_alg; SOLVE_KWARGS...) + result.cpu_retcode = sol_cpu.retcode + if ok(sol_cpu.retcode) + result.cpu_abs, result.cpu_rel = maxerrs(sol_cpu, ref) + end + catch e + result.cpu_error = e + end + + # GPU run --------------------------------------------------------------- + sol_gpu = nothing + try + odef_d = make_odef(; + mass_matrix = case.mass_matrix, + jac_prototype = case.jac_prototype) + prob_d = ODEProblem(odef_d, U0_D, TSPAN, P_D; initializealg = INITALG) + sol_gpu = solve(prob_d, cpu_alg; SOLVE_KWARGS...) + result.gpu_retcode = sol_gpu.retcode + if ok(sol_gpu.retcode) && sol_cpu !== nothing && ok(sol_cpu.retcode) + result.gpu_abs, result.gpu_rel = maxerrs(sol_gpu, sol_cpu) + end + catch e + result.gpu_error = e + end + + # Classify -------------------------------------------------------------- + result.status = classify(result) + return result +end + +function classify(r::TestResult) + r.gpu_error !== nothing && return :gpu_error + r.cpu_error !== nothing && return :cpu_error + r.gpu_retcode !== nothing && !ok(r.gpu_retcode) && return :gpu_failed + r.cpu_retcode !== nothing && !ok(r.cpu_retcode) && return :cpu_failed + within_tol(r.gpu_abs, r.gpu_rel, gpu_match_tol(r.case.solver)) && return :pass + return :gpu_mismatch +end + +# ── Run & report ───────────────────────────────────────────────────────────── + +function run_all(cases = build_cases(); verbose = true) + results = TestResult[] + Threads.@threads for (i, case) in collect(enumerate(cases)) + # for (i, case) in collect(enumerate(cases)) + verbose && @printf("[%3d/%3d] %s ... \n", i, length(cases), case) + r = run_case(case) + push!(results, r) + # verbose && println(status_glyph(r.status)) + end + return results +end + +status_glyph(s) = s === :pass ? "✓" : + s === :gpu_mismatch ? "✗ gpu mismatch" : + s === :cpu_failed ? "✗ cpu retcode" : + s === :gpu_failed ? "✗ gpu retcode" : + s === :cpu_error ? "✗ cpu error" : + s === :gpu_error ? "✗ gpu error" : + string(s) + +function show_results(results; threshold = 1.0e-3) + function _fmt(val) + isnan(val) && return " --- " + s = @sprintf("%.2e", val) + return val > threshold ? "\e[31m$s\e[0m" : s + end + + _rc(rc) = rc === nothing ? "---" : string(rc) + + label_w = 42 + println(rpad("Solver / jac / mass", label_w), + "class cpu_abs cpu_rel gpu_abs gpu_rel cpu_rc / gpu_rc status") + println("-"^140) + + for r in results + c = r.case + label = rpad("$(nameof(c.solver)) / $(c.jac_name) / $(c.mass_name)", label_w) + cls = rpad(string(c.solver_class), 11) + print(label, cls) + print(_fmt(r.cpu_abs), " ", _fmt(r.cpu_rel), " ") + print(_fmt(r.gpu_abs), " ", _fmt(r.gpu_rel), " ") + print(rpad("$(_rc(r.cpu_retcode)) / $(_rc(r.gpu_retcode))", 24)) + color = r.status === :pass ? :green : :red + printstyled(status_glyph(r.status), "\n"; color) + end + println("-"^140) + + # Summary + counts = Dict{Symbol,Int}() + for r in results + counts[r.status] = get(counts, r.status, 0) + 1 + end + println("\nSummary:") + for s in (:pass, :gpu_mismatch, :cpu_failed, :gpu_failed, :cpu_error, :gpu_error) + c = get(counts, s, 0) + c > 0 && println(" $(rpad(string(s), 15)) $c") + end + return nothing +end + +function show_errors(results) + for r in results + r.gpu_error === nothing && r.cpu_error === nothing && continue + println("── $(r.case) ──") + r.cpu_error !== nothing && (println("CPU error:"); showerror(stdout, r.cpu_error); println()) + r.gpu_error !== nothing && (println("GPU error:"); showerror(stdout, r.gpu_error); println()) + println() + end +end + +# ── Convenience for interactive debugging ──────────────────────────────────── + +""" + debug_case(solver; jac="none", mass="diag_cu") + +Run a single case interactively. Throws instead of catching so you can inspect +the stack trace. +""" +function debug_case(solver::Type; jac = "none", mass = "diag_cu") + jv = only(filter(x -> x[1] == jac, JAC_VARIANTS)) + mv = only(filter(x -> x[1] == mass, MASS_VARIANTS)) + cls = something(SOLVERS[findfirst(s -> s[1] == solver, SOLVERS)], (solver, :unknown))[2] + case = TestCase(solver, cls, jv[1], jv[2], jv[3], mv[1], mv[2]) + + ref = case.needs_krylov_cpu ? SOL_REF_KRYLOV : SOL_REF + cpu_alg = case.needs_krylov_cpu ? + case.solver(linsolve = KrylovJL_GMRES()) : + case.solver() + + @info "CPU solve" + sol_cpu = solve(PROB_CPU, cpu_alg; SOLVE_KWARGS...) + @info "CPU retcode" sol_cpu.retcode + @info "GPU solve" + odef_d = make_odef(; + mass_matrix = case.mass_matrix, + jac_prototype = case.jac_prototype) + prob_d = ODEProblem(odef_d, U0_D, TSPAN, P_D; initializealg = INITALG) + sol_gpu = solve(prob_d, case.solver(); SOLVE_KWARGS...) + @info "GPU retcode" sol_gpu.retcode + + cpu_abs, cpu_rel = maxerrs(sol_cpu, ref) + gpu_abs, gpu_rel = maxerrs(sol_gpu, sol_cpu) + @info "results" cpu_abs cpu_rel gpu_abs gpu_rel + return (; sol_cpu, sol_gpu, cpu_abs, cpu_rel, gpu_abs, gpu_rel) +end + +# ── Entry point ────────────────────────────────────────────────────────────── + +@testset "GPU DAE solver compatibility" begin + global results = run_all() + show_results(results) + @testset "$(r.case)" for r in results + @test r.status === :pass + end +end + +# res = debug_case(Rodas5P; jac = "CSR"); +# res = debug_case(Rosenbrock23; jac = "CSC");) diff --git a/test/gpu/simple_dae.jl b/test/gpu/simple_dae.jl index 44803183385..9e2bf22fa33 100644 --- a/test/gpu/simple_dae.jl +++ b/test/gpu/simple_dae.jl @@ -39,7 +39,8 @@ tspan = (0.0, 5.0) # CPU reference solution (Rodas5P) odef = ODEFunction(dae!, mass_matrix = mass_matrix, jac_prototype = jac_prototype) -prob = ODEProblem(odef, u0, tspan, p) +initializealg = BrownFullBasicInit() +prob = ODEProblem(odef, u0, tspan, p; initializealg) sol_ref = solve(prob, Rodas5P()) sol_ref_krylov = solve(prob, Rodas5P(linsolve = KrylovJL_GMRES())) @@ -53,8 +54,8 @@ mass_matrix_d = cu(mass_matrix) # ── Jacobian prototype options ──────────────────────────────────────────────── jac_prots = [ "none" => nothing, - "CSC" => CUDA.CUSPARSE.CuSparseMatrixCSC(jac_prototype), - "CSR" => CUDA.CUSPARSE.CuSparseMatrixCSR(jac_prototype), + # "CSC" => CUDA.CUSPARSE.CuSparseMatrixCSC(jac_prototype), + # "CSR" => CUDA.CUSPARSE.CuSparseMatrixCSR(jac_prototype), ] # ── Solver definitions ──────────────────────────────────────────────────────── @@ -228,12 +229,15 @@ function run_dae_tests() # CPU: this solver vs reference cpu_alg = krylov ? sv(linsolve = KrylovJL_GMRES()) : sv() + println(" CPU solve...") sol_cpu = solve(prob, cpu_alg) cpu_abs, cpu_rel = maxerrs(sol_cpu, _sol_ref) # GPU solve odef_d = ODEFunction(dae!, mass_matrix = mass_matrix_d, jac_prototype = jp) - prob_d = ODEProblem(odef_d, u0_d, tspan, p_d) + initializealg = BrownFullBasicInit() + prob_d = ODEProblem(odef_d, u0_d, tspan, p_d; initializealg) + println(" GPU solve... ") sol_d = solve(prob_d, sv()) # GPU vs CPU (same solver) @@ -292,5 +296,5 @@ end @testset "GPU DAE solver compatibility" begin global results results = run_dae_tests() + show_results(results) end -show_results(results) From 39d568be4bea2fa62ea4ca8acf1105a099c66006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Thu, 2 Jul 2026 15:41:41 +0200 Subject: [PATCH 13/20] Fix ImplicitEuler GPU: use GPU-safe find_algebraic_vars_eqs After the upstream SDIRK rewrite unified ImplicitEuler into the _PureSDIRKAlg alg_cache, algebraic_vars was computed via `[all(iszero, x) for x in eachcol(f.mass_matrix)]`, which triggers scalar indexing on GPU arrays and errors. Replace with the GPU-safe, Diagonal-aware find_algebraic_vars_eqs (behavior-preserving on CPU). Co-Authored-By: Claude Opus 4.8 --- lib/OrdinaryDiffEqSDIRK/src/OrdinaryDiffEqSDIRK.jl | 3 ++- lib/OrdinaryDiffEqSDIRK/src/sdirk_caches.jl | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/OrdinaryDiffEqSDIRK/src/OrdinaryDiffEqSDIRK.jl b/lib/OrdinaryDiffEqSDIRK/src/OrdinaryDiffEqSDIRK.jl index 51fe7334e99..a85e6fae6b9 100644 --- a/lib/OrdinaryDiffEqSDIRK/src/OrdinaryDiffEqSDIRK.jl +++ b/lib/OrdinaryDiffEqSDIRK/src/OrdinaryDiffEqSDIRK.jl @@ -13,7 +13,8 @@ import OrdinaryDiffEqCore: alg_order, calculate_residuals!, trivial_limiter!, _ode_interpolant!, isesdirk, issplit, ssp_coefficient, get_fsalfirstlast, generic_solver_docstring, - _ad_chunksize_int, _ad_fdtype, _fixup_ad, current_extrapolant!, Predictor + _ad_chunksize_int, _ad_fdtype, _fixup_ad, current_extrapolant!, Predictor, + find_algebraic_vars_eqs export Predictor using TruncatedStacktraces: @truncate_stacktrace using MuladdMacro, MacroTools, FastBroadcast, RecursiveArrayTools diff --git a/lib/OrdinaryDiffEqSDIRK/src/sdirk_caches.jl b/lib/OrdinaryDiffEqSDIRK/src/sdirk_caches.jl index 3b3de2945f5..1f5317913c5 100644 --- a/lib/OrdinaryDiffEqSDIRK/src/sdirk_caches.jl +++ b/lib/OrdinaryDiffEqSDIRK/src/sdirk_caches.jl @@ -170,7 +170,9 @@ function alg_cache( atmp = similar(u, uEltypeNoUnits) recursivefill!(atmp, false) algebraic_vars = if (alg isa ImplicitEuler) && f.mass_matrix !== I - [all(iszero, x) for x in eachcol(f.mass_matrix)] + # find_algebraic_vars_eqs is GPU-safe (broadcast-based, Diagonal-aware), + # unlike `eachcol` which triggers scalar indexing on GPU arrays. + find_algebraic_vars_eqs(f.mass_matrix)[1] else nothing end From 3a35194fb29eb24fa84298540bea5e80789727fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Thu, 2 Jul 2026 16:15:31 +0200 Subject: [PATCH 14/20] test/gpu: registry CUDA6 stack + [sources] for in-repo libs - CUDSS compat 0.6.7 -> 0.7,0.8 (CUDSS >=0.7 targets modular CUDA 6.2; the whole external stack now resolves clean from the registry: CUDA 6.2, CUDSS 0.7, LinearSolve 3.87 with the modular cuSPARSE fix) - list all in-repo OrdinaryDiffEq* libs + DiffEqBase under [deps]+[sources] so the env instantiates from paths without manual Pkg.develop - drop redundant test/gpu/simple_dae.jl (superseded by dae_tests.jl) Co-Authored-By: Claude Opus 4.8 --- test/gpu/Project.toml | 17 ++- test/gpu/simple_dae.jl | 300 ----------------------------------------- 2 files changed, 16 insertions(+), 301 deletions(-) delete mode 100644 test/gpu/simple_dae.jl diff --git a/test/gpu/Project.toml b/test/gpu/Project.toml index b5a984d290d..11d6e8fb467 100644 --- a/test/gpu/Project.toml +++ b/test/gpu/Project.toml @@ -12,28 +12,43 @@ KrylovKit = "0b1a1467-8014-51b9-945f-bf0ae24f4b77" LinearSolve = "7ed4a6bd-45f5-4d41-b270-4a48e9bafcae" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" OrdinaryDiffEqBDF = "6ad6398a-0878-4a85-9266-38940aa047c8" +OrdinaryDiffEqCore = "bbf590c4-e513-4bbe-9b18-05decba2e5d8" +OrdinaryDiffEqDefault = "50262376-6c5a-4cf5-baba-aaf4f84d72d7" OrdinaryDiffEqDifferentiation = "4302a76b-040a-498a-8c04-15b101fed76b" +OrdinaryDiffEqExplicitTableaus = "3278f1b1-0f5c-4cde-98e0-ba5eb00db955" OrdinaryDiffEqFIRK = "5960d6e9-dd7a-4743-88e7-cf307b64f125" OrdinaryDiffEqNonlinearSolve = "127b3ac7-2247-4354-8eb6-78cf4e7c58e8" OrdinaryDiffEqRKIP = "a4daff8c-1d43-4ff3-8eff-f78720aeecdc" OrdinaryDiffEqRosenbrock = "43230ef6-c299-4910-a778-202eb28ce4ce" +OrdinaryDiffEqRosenbrockTableaus = "b4bd8bb3-f80f-41d2-9b21-73a655b304b9" OrdinaryDiffEqSDIRK = "2d112036-d095-4a1e-ab9a-08536f3ecdbf" +OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a" +OrdinaryDiffEqVerner = "79d7bb75-1356-48c1-b8c0-6832512096c2" RecursiveArrayTools = "731186ca-8d62-57ce-b412-fbd966d074cd" SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" SciMLOperators = "c0aeaf25-5076-4817-a8d5-81caf7dfa961" [sources] +DiffEqBase = {path = "../../lib/DiffEqBase"} +OrdinaryDiffEq = {path = "../.."} OrdinaryDiffEqBDF = {path = "../../lib/OrdinaryDiffEqBDF"} +OrdinaryDiffEqCore = {path = "../../lib/OrdinaryDiffEqCore"} +OrdinaryDiffEqDefault = {path = "../../lib/OrdinaryDiffEqDefault"} +OrdinaryDiffEqDifferentiation = {path = "../../lib/OrdinaryDiffEqDifferentiation"} +OrdinaryDiffEqExplicitTableaus = {path = "../../lib/OrdinaryDiffEqExplicitTableaus"} OrdinaryDiffEqFIRK = {path = "../../lib/OrdinaryDiffEqFIRK"} OrdinaryDiffEqNonlinearSolve = {path = "../../lib/OrdinaryDiffEqNonlinearSolve"} OrdinaryDiffEqRKIP = {path = "../../lib/OrdinaryDiffEqRKIP"} OrdinaryDiffEqRosenbrock = {path = "../../lib/OrdinaryDiffEqRosenbrock"} +OrdinaryDiffEqRosenbrockTableaus = {path = "../../lib/OrdinaryDiffEqRosenbrockTableaus"} OrdinaryDiffEqSDIRK = {path = "../../lib/OrdinaryDiffEqSDIRK"} +OrdinaryDiffEqTsit5 = {path = "../../lib/OrdinaryDiffEqTsit5"} +OrdinaryDiffEqVerner = {path = "../../lib/OrdinaryDiffEqVerner"} [compat] Adapt = "4" CUDA = "6" -CUDSS = "0.6.7" +CUDSS = "0.7, 0.8" ComponentArrays = "0.15" DiffEqBase = "7" FFTW = "1.8" diff --git a/test/gpu/simple_dae.jl b/test/gpu/simple_dae.jl deleted file mode 100644 index 9e2bf22fa33..00000000000 --- a/test/gpu/simple_dae.jl +++ /dev/null @@ -1,300 +0,0 @@ -using OrdinaryDiffEqRosenbrock -using OrdinaryDiffEqSDIRK -using OrdinaryDiffEqBDF -using OrdinaryDiffEqFIRK -using OrdinaryDiffEqNonlinearSolve -using CUDA -using LinearAlgebra -using Adapt -using SparseArrays -using Test -using CUDSS -using Printf -using OrdinaryDiffEqNonlinearSolve.LinearSolve: KrylovJL_GMRES - - -#= -du[1] = -u[1] -du[2] = -0.5*u[2] - 0 = u[1] + u[2] - u[3] - 0 = -u[1] + u[2] - u[4] -=# - -function dae!(du, u, p, t) - return mul!(du, p, u) -end - -p = [ - -1 0 0 0 - 1 -0.5 0 0 - 1 1 -1 0 - -1 1 0 -1 -] - -mass_matrix = Diagonal([1, 1, 0, 0]) -jac_prototype = sparse(map(x -> iszero(x) ? 0.0 : 1.0, p)) - -u0 = [1.0, 1.0, 0.5, 0.5] # force init -tspan = (0.0, 5.0) - -# CPU reference solution (Rodas5P) -odef = ODEFunction(dae!, mass_matrix = mass_matrix, jac_prototype = jac_prototype) -initializealg = BrownFullBasicInit() -prob = ODEProblem(odef, u0, tspan, p; initializealg) -sol_ref = solve(prob, Rodas5P()) -sol_ref_krylov = solve(prob, Rodas5P(linsolve = KrylovJL_GMRES())) - -# GPU data -- we use F64 for higher accuracy for comparison -u0_d = adapt(CuArray{Float64}, u0) -p_d = adapt(CuArray{Float64}, p) - -# dense or sparse mass matrix does not work yet! -mass_matrix_d = cu(mass_matrix) - -# ── Jacobian prototype options ──────────────────────────────────────────────── -jac_prots = [ - "none" => nothing, - # "CSC" => CUDA.CUSPARSE.CuSparseMatrixCSC(jac_prototype), - # "CSR" => CUDA.CUSPARSE.CuSparseMatrixCSR(jac_prototype), -] - -# ── Solver definitions ──────────────────────────────────────────────────────── -# -# Each entry: SolverType => (; tol overrides...) -# method_tol = (; atol, rtol) – cpu solver vs Rodas5P ref (all jac); some methods are poor fits for DAEs -# method_csc_tol = (; atol, rtol) – cpu solver vs Rodas5P ref (CSC/Krylov path only) -# gpu_tol = (; atol, rtol) – gpu vs cpu (none/CSR fallback) -# csc_tol = (; atol, rtol) – gpu vs cpu (CSC jac_prot) -# csr_tol = (; atol, rtol) – gpu vs cpu (CSR jac_prot) -# Only specify fields that deviate from the defaults. - -const DEFAULT_METHOD_TOL = (; atol = 1.0e-2, rtol = 1.0e-2) -const DEFAULT_GPU_TOL = (; atol = 1.0e-4, rtol = 1.0e-4) - -solvers = [ - # ── Rosenbrock 2nd order ── - Rosenbrock23 => (; - csc_tol = (; atol = 2.0e-4, rtol = 5.0e-4), - ), - Rosenbrock32 => (; - method_tol = (; atol = 2.0e-2, rtol = 1.5), - csc_tol = (; atol = Inf, rtol = Inf), - ), - ROS2 => (;), - ROS2PR => (; - csc_tol = (; atol = 3.0e-4, rtol = 4.0e-4), - ), - ROS2S => (; - csc_tol = (; atol = 7.0e-4, rtol = 1.2e-3), - ), - # ── Rosenbrock 3rd order ── - ROS3 => (;), - ROS3PR => (; - method_tol = (; atol = 0.25, rtol = 15.0), - ), - ROS3PRL => (; - csc_tol = (; atol = 2.0e-3, rtol = 4.0e-3), - ), - ROS3PRL2 => (; - csc_tol = (; atol = 3.0e-3, rtol = 4.0e-3), - ), - ROS3P => (; - method_tol = (; atol = 0.25, rtol = 15.0), - ), - Rodas3 => (; - csc_tol = (; atol = 2.0e-3, rtol = 3.0e-3), - ), - # Rodas23W() # scalar indexing, requires large changes to `calculate_interpoldiff!` - # Rodas3P() # scalar indexing - Scholz4_7 => (;), - # ── Rosenbrock 4th order ── - ROS34PW1a => (; - method_tol = (; atol = 0.25, rtol = 5.0), - ), - ROS34PW1b => (; - method_tol = (; atol = 0.25, rtol = 5.0), - ), - ROS34PW2 => (; - csc_tol = (; atol = 1.2e-3, rtol = 2.2e-3), - ), - ROS34PW3 => (;), - ROS34PRw => (; - csc_tol = (; atol = 6.0e-4, rtol = 1.0e-3), - ), - RosShamp4 => (;), - Veldd4 => (;), - Velds4 => (;), - GRK4T => (;), - GRK4A => (;), - Ros4LStab => (;), - Rodas4 => (;), - Rodas42 => (;), - Rodas4P => (;), - Rodas4P2 => (;), - ROK4a => (;), - # ── Rosenbrock 5th order ── - Rodas5 => (;), - Rodas5P => (;), - Rodas5Pe => (;), - Rodas5Pr => (;), - # ── Rosenbrock 6th order ── - Rodas6P => (;), - # ── SDIRK (don't include fixed time step which need explicit dt) ── - ImplicitEuler => (;), - Trapezoid => (; - csc_tol = (; atol = 8.0e-3, rtol = 2.5e-2), - ), - SDIRK2 => (; - csc_tol = (; atol = 1.5e-4, rtol = 3.5e-4), - ), - Cash4 => (; - csc_tol = (; atol = 5.0e-3, rtol = 8.0e-3), - ), - Hairer4 => (; - csc_tol = (; atol = 8.0e-3, rtol = 6.0e-2), - ), - Hairer42 => (; - csc_tol = (; atol = 7.0e-3, rtol = 5.0e-2), - ), - # ── BDF ── - ABDF2 => (; - method_csc_tol = (; atol = Inf, rtol = Inf), # ABDF2 + Krylov diverges vs Rodas5P + Krylov - csc_tol = (; atol = 2.0e-4, rtol = 7.0e-4), - ), - QNDF1 => (;), - QNDF2 => (; - csc_tol = (; atol = 4.0e-3, rtol = 5.0e-3), - ), - # QNDF() # 🔧 DeviceMemory error in LinAlg - QBDF1 => (; - method_tol = (; atol = 2.0e-2, rtol = 0.2), - ), - QBDF2 => (; - gpu_tol = (; atol = 2.0e-3, rtol = 2.0e-3), - csc_tol = (; atol = 4.0e-3, rtol = 7.0e-3), - csr_tol = (; atol = 2.0e-3, rtol = 2.0e-3), - ), - # QBDF() # DeviceMemory error in LinAlg - # FBDF() # scalar indexing -> needs extensive work on reinitFBDF! - # ── FIRK -> all need substantial changes to `perform_step!` for FIRK methods ── - # RadauIIA3() # scalar indexing, ComplexF64 sparse unsupported - # RadauIIA5() # scalar indexing, ComplexF64 sparse unsupported - # RadauIIA9() # scalar indexing, ComplexF64 sparse unsupported - # AdaptiveRadau() # scalar indexing, ComplexF64 sparse unsupported -] - -# ── Helpers ─────────────────────────────────────────────────────────────────── - -function _get_method_tol(overrides, jac_name) - jac_name == "CSC" && hasproperty(overrides, :method_csc_tol) && return overrides.method_csc_tol - hasproperty(overrides, :method_tol) && return overrides.method_tol - return DEFAULT_METHOD_TOL -end - -function _get_tol(overrides, jac_name) - _tol_keys = Dict("none" => :gpu_tol, "CSC" => :csc_tol, "CSR" => :csr_tol) - key = _tol_keys[jac_name] - hasproperty(overrides, key) && return getproperty(overrides, key) - # gpu_tol acts as default for all GPU combos - hasproperty(overrides, :gpu_tol) && return overrides.gpu_tol - return DEFAULT_GPU_TOL -end - -function maxerrs(sol_a, sol_b) - max_abs = 0.0 - max_rel = 0.0 - for t in tspan[begin]:0.1:tspan[end] - a = Vector(sol_a(t)) - b = Vector(sol_b(t)) - diff = abs.(a - b) - ref = abs.(b) - max_abs = max(max_abs, maximum(diff)) - max_rel = max(max_rel, maximum(diff ./ max.(ref, eps()))) - end - return max_abs, max_rel -end - -function run_dae_tests() - results = Any[] - - for (sv, overrides) in solvers, (jn, jp) in jac_prots - println("Test $sv with prototype $jn") - sn = string(sv) - gtol = _get_tol(overrides, jn) - mtol = _get_method_tol(overrides, jn) - - # CSC will always fall back to krylov so the ref solution should do to - krylov = (jn == "CSC") - _sol_ref = krylov ? sol_ref_krylov : sol_ref - - # CPU: this solver vs reference - cpu_alg = krylov ? sv(linsolve = KrylovJL_GMRES()) : sv() - println(" CPU solve...") - sol_cpu = solve(prob, cpu_alg) - cpu_abs, cpu_rel = maxerrs(sol_cpu, _sol_ref) - - # GPU solve - odef_d = ODEFunction(dae!, mass_matrix = mass_matrix_d, jac_prototype = jp) - initializealg = BrownFullBasicInit() - prob_d = ODEProblem(odef_d, u0_d, tspan, p_d; initializealg) - println(" GPU solve... ") - sol_d = solve(prob_d, sv()) - - # GPU vs CPU (same solver) - gpu_abs, gpu_rel = maxerrs(sol_d, sol_cpu) - - method_passed = (cpu_abs < mtol.atol) || (cpu_rel < mtol.rtol) - gpu_passed = (gpu_abs < gtol.atol) || (gpu_rel < gtol.rtol) - passed = method_passed && gpu_passed - push!( - results, (; - solver = sn, jac = jn, cpu_abs, cpu_rel, gpu_abs, gpu_rel, - gtol, mtol, method_passed, gpu_passed, passed, error = "", - ) - ) - @test passed - end - - return results -end - -function show_results(results) - function _fmt(val; threshold = 1.0e-3) - isnan(val) && return " --- " - s = @sprintf("%.2e", val) - if val > threshold - return "\e[31m$s\e[0m" - end - return s - end - - println(rpad("Solver / Jac", 30), "cpu_abs cpu_rel gpu_abs gpu_rel status") - println("-"^85) - - for r in results - label = rpad("$(r.solver) / $(r.jac)", 30) - if r.error != "" - printstyled(label, "ERROR: ", r.error, "\n"; color = :yellow) - else - print(label) - print(_fmt(r.cpu_abs, threshold = 1.0e-2), " ", _fmt(r.cpu_rel, threshold = 1.0e-2), " ") - print(_fmt(r.gpu_abs), " ", _fmt(r.gpu_rel), " ") - if r.passed - printstyled("✓\n"; color = :green) - elseif !r.method_passed && !r.gpu_passed - printstyled("✗ method+gpu\n"; color = :red) - elseif !r.method_passed - printstyled("✗ method\n"; color = :red) - else - printstyled("✗ gpu\n"; color = :red) - end - end - end - return println("-"^85) -end - -@testset "GPU DAE solver compatibility" begin - global results - results = run_dae_tests() - show_results(results) -end From e3e643439e0a4f33ad9b359e6a34afecd3a45e57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Fri, 3 Jul 2026 13:52:22 +0200 Subject: [PATCH 15/20] enable all passing solvers --- test/gpu/dae_tests.jl | 118 ++++++++++++++++++++++++++++-------------- 1 file changed, 78 insertions(+), 40 deletions(-) diff --git a/test/gpu/dae_tests.jl b/test/gpu/dae_tests.jl index 42f0aaeaa7c..1014157e192 100644 --- a/test/gpu/dae_tests.jl +++ b/test/gpu/dae_tests.jl @@ -62,9 +62,12 @@ INITALG = BrownFullBasicInit() # solver fails cleanly rather than hangs indefinitely. SOLVE_KWARGS = (; maxiters = 10_000) -# Helper: build an ODEFunction with FullSpecialize (CPU or GPU). +# Helper: build an ODEFunction with FullSpecialize (CPU or GPU). The problem is +# in-place (`dae!` mutates `du`), so iip = true. FullSpecialize bypasses the +# FunctionWrappers path described in the header note. make_odef(; mass_matrix, jac_prototype) = - ODEFunction(dae!; mass_matrix = mass_matrix, jac_prototype = jac_prototype) + ODEFunction{true, FullSpecialize}(dae!; + mass_matrix = mass_matrix, jac_prototype = jac_prototype) # ── CPU reference ──────────────────────────────────────────────────────────── @@ -103,46 +106,51 @@ MASS_VARIANTS = [ # :suitable — designed or known-good for index-1 mass-matrix DAEs # :marginal — may lose order / show artifacts but typically runs # :unsuitable — included only for GPU code-path coverage +# +# Several solvers pass on the `none`/`CSR` variants but their `CSC` variant uses +# a Krylov CPU reference that fails to converge (Unstable/MaxIters) *on the CPU +# too* — the GPU reproduces the same non-convergence, so that variant is skipped +# (no CPU reference to compare against), not counted as a GPU failure. See +# `classify`. These are marked "(CSC skipped: CPU non-convergent)" below. SOLVERS = [ # ── Rosenbrock 2nd order ── - # (Rosenbrock23, :marginal), - # (Rosenbrock32, :unsuitable), # low-accuracy, used for coverage + (Rosenbrock23, :marginal), # (CSC skipped: CPU Unstable) + # (Rosenbrock32, :unsuitable), # Unstable on every variant incl. CPU `none` — no reference anywhere (ROS2, :marginal), (ROS2PR, :suitable), (ROS2S, :suitable), # ── Rosenbrock 3rd order ── - # (ROS3, :marginal), + (ROS3, :marginal), # (CSC skipped: CPU MaxIters) (ROS3PR, :suitable), (ROS3PRL, :suitable), (ROS3PRL2, :suitable), (ROS3P, :suitable), (Rodas3, :suitable), - # Rodas23W — scalar indexing, needs `calculate_interpoldiff!` rework - # Rodas3P — scalar indexing + (Rodas23W, :suitable), # works on all variants (earlier scalar-indexing issue resolved) + (Rodas3P, :suitable), # works on all variants (earlier scalar-indexing issue resolved) (Scholz4_7, :suitable), # ── Rosenbrock 4th order ── - #= (ROS34PW1a, :suitable), (ROS34PW1b, :suitable), (ROS34PW2, :suitable), (ROS34PW3, :suitable), (ROS34PRw, :suitable), - # (RosShamp4, :marginal), # classical Rosenbrock, not DAE-derived - # (Veldd4, :marginal), # d + (RosShamp4, :marginal), # (CSC skipped: CPU MaxIters) + (Veldd4, :marginal), # (CSC skipped: CPU MaxIters) (Velds4, :marginal), - # (GRK4T, :marginal), # does not work with CSC/Krylov + (GRK4T, :marginal), # (CSC skipped: CPU/Krylov MaxIters) (GRK4A, :marginal), (Ros4LStab, :marginal), (Rodas4, :suitable), (Rodas42, :suitable), (Rodas4P, :suitable), (Rodas4P2, :suitable), - # (ROK4a, :suitable), + (ROK4a, :suitable), # (CSC skipped: CPU MaxIters) # ── Rosenbrock 5th order ── (Rodas5, :suitable), (Rodas5P, :suitable), - # (Rodas5Pe, :suitable), + (Rodas5Pe, :suitable), # (CSC skipped: CPU MaxIters) (Rodas5Pr, :suitable), # ── Rosenbrock 6th order ── (Rodas6P, :suitable), @@ -157,15 +165,16 @@ SOLVERS = [ (ABDF2, :suitable), (QNDF1, :marginal), (QNDF2, :suitable), - # QNDF — DeviceMemory error in LinAlg - # (QBDF1, :marginal), - # (QBDF2, :suitable), - # QBDF — DeviceMemory error in LinAlg - # FBDF — scalar indexing, needs reinitFBDF! rework - # ── FIRK (all need `perform_step!` rework) ── - # RadauIIA3, RadauIIA5, RadauIIA9, AdaptiveRadau - # — scalar indexing, ComplexF64 sparse unsupported - =# + (QNDF, :suitable), # works on all variants (earlier DeviceMemory issue resolved) + (QBDF1, :marginal), # works on all variants + (QBDF2, :suitable), # works on all variants + (QBDF, :suitable), # works on all variants (earlier DeviceMemory issue resolved) + (FBDF, :suitable), # works on all variants (earlier scalar-indexing issue resolved) + # ── FIRK ── still GPU-incompatible on every variant: + # RadauIIA3/5/9, AdaptiveRadau + # `none`: "Scalar indexing is disallowed" inside perform_step! + # `CSR` : setindex! not defined for CuSparseMatrixCSR{ComplexF64} (complex W) + # `CSC` : non-concrete / ComplexF64 Jacobian not supported by RadauIIA ] # ── Pass-criterion configuration ───────────────────────────────────────────── @@ -202,14 +211,15 @@ mutable struct TestResult case::TestCase cpu_abs::Float64 # vs reference (informational) cpu_rel::Float64 - gpu_abs::Float64 # vs CPU same-solver (pass/fail) + gpu_abs::Float64 # vs CPU same-solver (informational; see gpu_within_tol) gpu_rel::Float64 + gpu_within_tol::Bool # combined atol/rtol gate vs CPU same-solver (pass/fail) cpu_retcode::Union{Nothing, ReturnCode.T} gpu_retcode::Union{Nothing, ReturnCode.T} cpu_error::Union{Nothing, Exception} gpu_error::Union{Nothing, Exception} - status::Symbol # :pass, :gpu_mismatch, :cpu_failed, :gpu_failed, - # :cpu_error, :gpu_error + status::Symbol # :pass, :cpu_skip (no CPU reference, non-gating), + # :gpu_mismatch, :gpu_failed, :gpu_error end # ── Helpers ────────────────────────────────────────────────────────────────── @@ -240,15 +250,26 @@ function maxerrs(sol_a, sol_b) return max_abs, max_rel end -within_tol(abs_err, rel_err, tol) = - abs_err < tol.atol || rel_err < tol.rtol +# Gate: GPU must match CPU within a combined atol/rtol at every sampled point, +# `|a - b| <= atol + rtol*|b|`. Unlike a separate abs-OR-rel test this cannot be +# passed by a tiny absolute error masking a large relative one (or vice versa), +# and unlike abs-AND-rel it stays robust where a state passes through zero (atol +# dominates near 0, rtol for large states). +function within_tol(sol_a, sol_b, tol) + for t in TSPAN[1]:0.1:TSPAN[2] + a = Vector(sol_a(t)) + b = Vector(sol_b(t)) + all(abs.(a .- b) .<= tol.atol .+ tol.rtol .* abs.(b)) || return false + end + return true +end ok(retcode) = retcode === ReturnCode.Success || retcode === ReturnCode.Default # ── Run a single case ──────────────────────────────────────────────────────── function run_case(case::TestCase) - result = TestResult(case, NaN, NaN, NaN, NaN, + result = TestResult(case, NaN, NaN, NaN, NaN, false, nothing, nothing, nothing, nothing, :pending) # CPU run --------------------------------------------------------------- @@ -279,6 +300,8 @@ function run_case(case::TestCase) result.gpu_retcode = sol_gpu.retcode if ok(sol_gpu.retcode) && sol_cpu !== nothing && ok(sol_cpu.retcode) result.gpu_abs, result.gpu_rel = maxerrs(sol_gpu, sol_cpu) + result.gpu_within_tol = + within_tol(sol_gpu, sol_cpu, gpu_match_tol(case.solver)) end catch e result.gpu_error = e @@ -290,33 +313,41 @@ function run_case(case::TestCase) end function classify(r::TestResult) + # The CPU solve is the reference. If it can't produce one — it errored, or + # returned a non-success retcode (Unstable/MaxIters) because the solver is + # intrinsically unsuitable for this problem — there is nothing to compare + # the GPU against, so skip (non-gating). Crucially this is checked *before* + # the GPU retcode: a GPU that faithfully reproduces the CPU's Unstable/ + # MaxIters result is matching, not a GPU incompatibility. + (r.cpu_error !== nothing || + (r.cpu_retcode !== nothing && !ok(r.cpu_retcode))) && return :cpu_skip + # CPU produced a reference; the GPU must now reproduce it without erroring. r.gpu_error !== nothing && return :gpu_error - r.cpu_error !== nothing && return :cpu_error r.gpu_retcode !== nothing && !ok(r.gpu_retcode) && return :gpu_failed - r.cpu_retcode !== nothing && !ok(r.cpu_retcode) && return :cpu_failed - within_tol(r.gpu_abs, r.gpu_rel, gpu_match_tol(r.case.solver)) && return :pass + r.gpu_within_tol && return :pass return :gpu_mismatch end # ── Run & report ───────────────────────────────────────────────────────────── +# Run serially. GPU solves share a single CUDA context/default stream, so +# concurrent execution (e.g. `Threads.@threads`) races on device state and +# `push!` into a shared Vector is not thread-safe — keep this a plain loop. function run_all(cases = build_cases(); verbose = true) results = TestResult[] - Threads.@threads for (i, case) in collect(enumerate(cases)) - # for (i, case) in collect(enumerate(cases)) - verbose && @printf("[%3d/%3d] %s ... \n", i, length(cases), case) + for (i, case) in enumerate(cases) + verbose && @printf("[%3d/%3d] %s ... ", i, length(cases), case) r = run_case(case) push!(results, r) - # verbose && println(status_glyph(r.status)) + verbose && println(status_glyph(r.status)) end return results end status_glyph(s) = s === :pass ? "✓" : + s === :cpu_skip ? "− skipped (cpu has no reference)" : s === :gpu_mismatch ? "✗ gpu mismatch" : - s === :cpu_failed ? "✗ cpu retcode" : s === :gpu_failed ? "✗ gpu retcode" : - s === :cpu_error ? "✗ cpu error" : s === :gpu_error ? "✗ gpu error" : string(s) @@ -342,7 +373,8 @@ function show_results(results; threshold = 1.0e-3) print(_fmt(r.cpu_abs), " ", _fmt(r.cpu_rel), " ") print(_fmt(r.gpu_abs), " ", _fmt(r.gpu_rel), " ") print(rpad("$(_rc(r.cpu_retcode)) / $(_rc(r.gpu_retcode))", 24)) - color = r.status === :pass ? :green : :red + color = r.status === :pass ? :green : + r.status === :cpu_skip ? :yellow : :red printstyled(status_glyph(r.status), "\n"; color) end println("-"^140) @@ -353,7 +385,7 @@ function show_results(results; threshold = 1.0e-3) counts[r.status] = get(counts, r.status, 0) + 1 end println("\nSummary:") - for s in (:pass, :gpu_mismatch, :cpu_failed, :gpu_failed, :cpu_error, :gpu_error) + for s in (:pass, :cpu_skip, :gpu_mismatch, :gpu_failed, :gpu_error) c = get(counts, s, 0) c > 0 && println(" $(rpad(string(s), 15)) $c") end @@ -412,7 +444,13 @@ end global results = run_all() show_results(results) @testset "$(r.case)" for r in results - @test r.status === :pass + # :cpu_skip = the CPU couldn't solve this case, so there is no reference + # to compare against — record as skipped, not pass/fail. + if r.status === :cpu_skip + @test_skip r.status === :pass + else + @test r.status === :pass + end end end From a1655fc89669ff52657343065bae401410cb262d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Sat, 4 Jul 2026 00:25:52 +0200 Subject: [PATCH 16/20] add outer GPU testset otherwise the first failing GPU test file will prevent others from beeing run --- test/runtests.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 3890024fa2e..409743daeb4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -206,8 +206,10 @@ function gpu_group() is_APPVEYOR && return activate_gpu_env() gpudir = joinpath(@__DIR__, "gpu") - for f in sort(filter(f -> endswith(f, ".jl"), readdir(gpudir))) - @time @eval @safetestset $("GPU: " * f) include($(joinpath(gpudir, f))) + @testset "GPU Tests" begin + for f in sort(filter(f -> endswith(f, ".jl"), readdir(gpudir))) + @time @eval @safetestset $("GPU: " * f) include($(joinpath(gpudir, f))) + end end return nothing end From 1d43c4f6e41e45953d1c42208b3f56a7b2ca5ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Sat, 4 Jul 2026 09:32:45 +0200 Subject: [PATCH 17/20] fix GPU test env testfiles dependet on more broad exports from OrdinaryDiffEq, imports now explicit --- test/gpu/Project.toml | 17 +++++++++-------- test/gpu/autoswitch.jl | 1 + test/gpu/linear_exp.jl | 2 ++ test/gpu/linear_lsrk.jl | 1 + test/gpu/reaction_diffusion_stiff.jl | 2 ++ 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/test/gpu/Project.toml b/test/gpu/Project.toml index 11d6e8fb467..7351d615d38 100644 --- a/test/gpu/Project.toml +++ b/test/gpu/Project.toml @@ -17,13 +17,18 @@ OrdinaryDiffEqDefault = "50262376-6c5a-4cf5-baba-aaf4f84d72d7" OrdinaryDiffEqDifferentiation = "4302a76b-040a-498a-8c04-15b101fed76b" OrdinaryDiffEqExplicitTableaus = "3278f1b1-0f5c-4cde-98e0-ba5eb00db955" OrdinaryDiffEqFIRK = "5960d6e9-dd7a-4743-88e7-cf307b64f125" +OrdinaryDiffEqLinear = "521117fe-8c41-49f8-b3b6-30780b3f0fb5" +OrdinaryDiffEqLowOrderRK = "1344f307-1e59-4825-a18e-ace9aa3fa4c6" +OrdinaryDiffEqLowStorageRK = "b0944070-b475-4768-8dec-fb6eb410534d" OrdinaryDiffEqNonlinearSolve = "127b3ac7-2247-4354-8eb6-78cf4e7c58e8" OrdinaryDiffEqRKIP = "a4daff8c-1d43-4ff3-8eff-f78720aeecdc" OrdinaryDiffEqRosenbrock = "43230ef6-c299-4910-a778-202eb28ce4ce" OrdinaryDiffEqRosenbrockTableaus = "b4bd8bb3-f80f-41d2-9b21-73a655b304b9" OrdinaryDiffEqSDIRK = "2d112036-d095-4a1e-ab9a-08536f3ecdbf" +OrdinaryDiffEqStabilizedRK = "358294b1-0aab-51c3-aafe-ad5ab194a2ad" OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a" OrdinaryDiffEqVerner = "79d7bb75-1356-48c1-b8c0-6832512096c2" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" RecursiveArrayTools = "731186ca-8d62-57ce-b412-fbd966d074cd" SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" SciMLOperators = "c0aeaf25-5076-4817-a8d5-81caf7dfa961" @@ -37,11 +42,15 @@ OrdinaryDiffEqDefault = {path = "../../lib/OrdinaryDiffEqDefault"} OrdinaryDiffEqDifferentiation = {path = "../../lib/OrdinaryDiffEqDifferentiation"} OrdinaryDiffEqExplicitTableaus = {path = "../../lib/OrdinaryDiffEqExplicitTableaus"} OrdinaryDiffEqFIRK = {path = "../../lib/OrdinaryDiffEqFIRK"} +OrdinaryDiffEqLinear = {path = "../../lib/OrdinaryDiffEqLinear"} +OrdinaryDiffEqLowOrderRK = {path = "../../lib/OrdinaryDiffEqLowOrderRK"} +OrdinaryDiffEqLowStorageRK = {path = "../../lib/OrdinaryDiffEqLowStorageRK"} OrdinaryDiffEqNonlinearSolve = {path = "../../lib/OrdinaryDiffEqNonlinearSolve"} OrdinaryDiffEqRKIP = {path = "../../lib/OrdinaryDiffEqRKIP"} OrdinaryDiffEqRosenbrock = {path = "../../lib/OrdinaryDiffEqRosenbrock"} OrdinaryDiffEqRosenbrockTableaus = {path = "../../lib/OrdinaryDiffEqRosenbrockTableaus"} OrdinaryDiffEqSDIRK = {path = "../../lib/OrdinaryDiffEqSDIRK"} +OrdinaryDiffEqStabilizedRK = {path = "../../lib/OrdinaryDiffEqStabilizedRK"} OrdinaryDiffEqTsit5 = {path = "../../lib/OrdinaryDiffEqTsit5"} OrdinaryDiffEqVerner = {path = "../../lib/OrdinaryDiffEqVerner"} @@ -50,17 +59,9 @@ Adapt = "4" CUDA = "6" CUDSS = "0.7, 0.8" ComponentArrays = "0.15" -DiffEqBase = "7" FFTW = "1.8" FastBroadcast = "1.3" FillArrays = "1" -OrdinaryDiffEq = "7" -OrdinaryDiffEqBDF = "2" -OrdinaryDiffEqFIRK = "2" -OrdinaryDiffEqNonlinearSolve = "2" -OrdinaryDiffEqRKIP = "2" -OrdinaryDiffEqRosenbrock = "2" -OrdinaryDiffEqSDIRK = "2" RecursiveArrayTools = "4" SciMLBase = "3" SciMLOperators = "1.3" diff --git a/test/gpu/autoswitch.jl b/test/gpu/autoswitch.jl index a7670d199c2..39a0a16b6f1 100644 --- a/test/gpu/autoswitch.jl +++ b/test/gpu/autoswitch.jl @@ -1,4 +1,5 @@ using OrdinaryDiffEq, CUDA, Test +using OrdinaryDiffEqLowOrderRK: AutoDP5 CUDA.allowscalar(false) # https://github.com/SciML/OrdinaryDiffEq.jl/issues/1614 diff --git a/test/gpu/linear_exp.jl b/test/gpu/linear_exp.jl index cd22a8f41e7..5f17e2e18c7 100644 --- a/test/gpu/linear_exp.jl +++ b/test/gpu/linear_exp.jl @@ -3,6 +3,8 @@ using SparseArrays using CUDA using CUDA.CUSPARSE using OrdinaryDiffEq +using OrdinaryDiffEqLinear: LinearExponential +using SciMLOperators: MatrixOperator # Linear exponential solvers A = MatrixOperator([2.0 -1.0; -1.0 2.0]) diff --git a/test/gpu/linear_lsrk.jl b/test/gpu/linear_lsrk.jl index 039912c6bcd..7f55032a56d 100644 --- a/test/gpu/linear_lsrk.jl +++ b/test/gpu/linear_lsrk.jl @@ -1,4 +1,5 @@ using OrdinaryDiffEq, CUDA, Test +using OrdinaryDiffEqLowStorageRK: ORK256, CarpenterKennedy2N54, SHLDDRK64, DGLDDRK73_C CUDA.allowscalar(false) N = 256 # Define the initial condition as normal arrays diff --git a/test/gpu/reaction_diffusion_stiff.jl b/test/gpu/reaction_diffusion_stiff.jl index 175508e8bc2..80dff06c0ec 100644 --- a/test/gpu/reaction_diffusion_stiff.jl +++ b/test/gpu/reaction_diffusion_stiff.jl @@ -1,4 +1,6 @@ using OrdinaryDiffEq, RecursiveArrayTools, LinearAlgebra, Test +using OrdinaryDiffEqLowOrderRK: BS3 +using OrdinaryDiffEqStabilizedRK: ROCK2 # Define the constants for the PDE const α₂ = 1.0 From ad0f24096df1f9075673a2c4729ad1fe6705ce28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Sat, 4 Jul 2026 11:51:13 +0200 Subject: [PATCH 18/20] runic format --- test/gpu/dae_tests.jl | 173 +++++++++++++++++++++++------------------- 1 file changed, 94 insertions(+), 79 deletions(-) diff --git a/test/gpu/dae_tests.jl b/test/gpu/dae_tests.jl index 1014157e192..21fba7e85a4 100644 --- a/test/gpu/dae_tests.jl +++ b/test/gpu/dae_tests.jl @@ -66,8 +66,10 @@ SOLVE_KWARGS = (; maxiters = 10_000) # in-place (`dae!` mutates `du`), so iip = true. FullSpecialize bypasses the # FunctionWrappers path described in the header note. make_odef(; mass_matrix, jac_prototype) = - ODEFunction{true, FullSpecialize}(dae!; - mass_matrix = mass_matrix, jac_prototype = jac_prototype) + ODEFunction{true, FullSpecialize}( + dae!; + mass_matrix = mass_matrix, jac_prototype = jac_prototype +) # ── CPU reference ──────────────────────────────────────────────────────────── @@ -88,8 +90,8 @@ MASS_MATRIX_D_DIAG = cu(MASS_MATRIX) # Each entry: (name, jac_prototype_for_gpu, needs_krylov_on_cpu) JAC_VARIANTS = [ ("none", nothing, false), - ("CSC", CUDA.CUSPARSE.CuSparseMatrixCSC(JAC_PROTOTYPE), true), - ("CSR", CUDA.CUSPARSE.CuSparseMatrixCSR(JAC_PROTOTYPE), false), + ("CSC", CUDA.CUSPARSE.CuSparseMatrixCSC(JAC_PROTOTYPE), true), + ("CSR", CUDA.CUSPARSE.CuSparseMatrixCSR(JAC_PROTOTYPE), false), ] # Each entry: (name, gpu_mass_matrix) @@ -115,61 +117,61 @@ MASS_VARIANTS = [ SOLVERS = [ # ── Rosenbrock 2nd order ── - (Rosenbrock23, :marginal), # (CSC skipped: CPU Unstable) + (Rosenbrock23, :marginal), # (CSC skipped: CPU Unstable) # (Rosenbrock32, :unsuitable), # Unstable on every variant incl. CPU `none` — no reference anywhere - (ROS2, :marginal), - (ROS2PR, :suitable), - (ROS2S, :suitable), + (ROS2, :marginal), + (ROS2PR, :suitable), + (ROS2S, :suitable), # ── Rosenbrock 3rd order ── - (ROS3, :marginal), # (CSC skipped: CPU MaxIters) - (ROS3PR, :suitable), - (ROS3PRL, :suitable), - (ROS3PRL2, :suitable), - (ROS3P, :suitable), - (Rodas3, :suitable), - (Rodas23W, :suitable), # works on all variants (earlier scalar-indexing issue resolved) - (Rodas3P, :suitable), # works on all variants (earlier scalar-indexing issue resolved) - (Scholz4_7, :suitable), + (ROS3, :marginal), # (CSC skipped: CPU MaxIters) + (ROS3PR, :suitable), + (ROS3PRL, :suitable), + (ROS3PRL2, :suitable), + (ROS3P, :suitable), + (Rodas3, :suitable), + (Rodas23W, :suitable), # works on all variants (earlier scalar-indexing issue resolved) + (Rodas3P, :suitable), # works on all variants (earlier scalar-indexing issue resolved) + (Scholz4_7, :suitable), # ── Rosenbrock 4th order ── - (ROS34PW1a, :suitable), - (ROS34PW1b, :suitable), - (ROS34PW2, :suitable), - (ROS34PW3, :suitable), - (ROS34PRw, :suitable), - (RosShamp4, :marginal), # (CSC skipped: CPU MaxIters) - (Veldd4, :marginal), # (CSC skipped: CPU MaxIters) - (Velds4, :marginal), - (GRK4T, :marginal), # (CSC skipped: CPU/Krylov MaxIters) - (GRK4A, :marginal), - (Ros4LStab, :marginal), - (Rodas4, :suitable), - (Rodas42, :suitable), - (Rodas4P, :suitable), - (Rodas4P2, :suitable), - (ROK4a, :suitable), # (CSC skipped: CPU MaxIters) + (ROS34PW1a, :suitable), + (ROS34PW1b, :suitable), + (ROS34PW2, :suitable), + (ROS34PW3, :suitable), + (ROS34PRw, :suitable), + (RosShamp4, :marginal), # (CSC skipped: CPU MaxIters) + (Veldd4, :marginal), # (CSC skipped: CPU MaxIters) + (Velds4, :marginal), + (GRK4T, :marginal), # (CSC skipped: CPU/Krylov MaxIters) + (GRK4A, :marginal), + (Ros4LStab, :marginal), + (Rodas4, :suitable), + (Rodas42, :suitable), + (Rodas4P, :suitable), + (Rodas4P2, :suitable), + (ROK4a, :suitable), # (CSC skipped: CPU MaxIters) # ── Rosenbrock 5th order ── - (Rodas5, :suitable), - (Rodas5P, :suitable), - (Rodas5Pe, :suitable), # (CSC skipped: CPU MaxIters) - (Rodas5Pr, :suitable), + (Rodas5, :suitable), + (Rodas5P, :suitable), + (Rodas5Pe, :suitable), # (CSC skipped: CPU MaxIters) + (Rodas5Pr, :suitable), # ── Rosenbrock 6th order ── - (Rodas6P, :suitable), + (Rodas6P, :suitable), # ── SDIRK ── (ImplicitEuler, :marginal), - (Trapezoid, :unsuitable), # oscillates on algebraic states - (SDIRK2, :suitable), - (Cash4, :suitable), - (Hairer4, :suitable), - (Hairer42, :suitable), + (Trapezoid, :unsuitable), # oscillates on algebraic states + (SDIRK2, :suitable), + (Cash4, :suitable), + (Hairer4, :suitable), + (Hairer42, :suitable), # ── BDF ── - (ABDF2, :suitable), - (QNDF1, :marginal), - (QNDF2, :suitable), - (QNDF, :suitable), # works on all variants (earlier DeviceMemory issue resolved) - (QBDF1, :marginal), # works on all variants - (QBDF2, :suitable), # works on all variants - (QBDF, :suitable), # works on all variants (earlier DeviceMemory issue resolved) - (FBDF, :suitable), # works on all variants (earlier scalar-indexing issue resolved) + (ABDF2, :suitable), + (QNDF1, :marginal), + (QNDF2, :suitable), + (QNDF, :suitable), # works on all variants (earlier DeviceMemory issue resolved) + (QBDF1, :marginal), # works on all variants + (QBDF2, :suitable), # works on all variants + (QBDF, :suitable), # works on all variants (earlier DeviceMemory issue resolved) + (FBDF, :suitable), # works on all variants (earlier scalar-indexing issue resolved) # ── FIRK ── still GPU-incompatible on every variant: # RadauIIA3/5/9, AdaptiveRadau # `none`: "Scalar indexing is disallowed" inside perform_step! @@ -204,8 +206,10 @@ struct TestCase mass_matrix::Any end -Base.show(io::IO, c::TestCase) = print(io, - "$(nameof(c.solver)) [jac=$(c.jac_name), mass=$(c.mass_name)]") +Base.show(io::IO, c::TestCase) = print( + io, + "$(nameof(c.solver)) [jac=$(c.jac_name), mass=$(c.mass_name)]" +) mutable struct TestResult case::TestCase @@ -219,18 +223,20 @@ mutable struct TestResult cpu_error::Union{Nothing, Exception} gpu_error::Union{Nothing, Exception} status::Symbol # :pass, :cpu_skip (no CPU reference, non-gating), - # :gpu_mismatch, :gpu_failed, :gpu_error + # :gpu_mismatch, :gpu_failed, :gpu_error end # ── Helpers ────────────────────────────────────────────────────────────────── -function build_cases(; solvers = SOLVERS, - jac_variants = JAC_VARIANTS, - mass_variants = MASS_VARIANTS) +function build_cases(; + solvers = SOLVERS, + jac_variants = JAC_VARIANTS, + mass_variants = MASS_VARIANTS + ) cases = TestCase[] for (sv, cls) in solvers, - (jn, jp, needs_krylov) in jac_variants, - (mn, mm) in mass_variants + (jn, jp, needs_krylov) in jac_variants, + (mn, mm) in mass_variants push!(cases, TestCase(sv, cls, jn, jp, needs_krylov, mn, mm)) end return cases @@ -269,14 +275,16 @@ ok(retcode) = retcode === ReturnCode.Success || retcode === ReturnCode.Default # ── Run a single case ──────────────────────────────────────────────────────── function run_case(case::TestCase) - result = TestResult(case, NaN, NaN, NaN, NaN, false, - nothing, nothing, nothing, nothing, :pending) + result = TestResult( + case, NaN, NaN, NaN, NaN, false, + nothing, nothing, nothing, nothing, :pending + ) # CPU run --------------------------------------------------------------- ref = case.needs_krylov_cpu ? SOL_REF_KRYLOV : SOL_REF cpu_alg = case.needs_krylov_cpu ? - case.solver(linsolve = KrylovJL_GMRES()) : - case.solver() + case.solver(linsolve = KrylovJL_GMRES()) : + case.solver() sol_cpu = nothing try @@ -294,7 +302,8 @@ function run_case(case::TestCase) try odef_d = make_odef(; mass_matrix = case.mass_matrix, - jac_prototype = case.jac_prototype) + jac_prototype = case.jac_prototype + ) prob_d = ODEProblem(odef_d, U0_D, TSPAN, P_D; initializealg = INITALG) sol_gpu = solve(prob_d, cpu_alg; SOLVE_KWARGS...) result.gpu_retcode = sol_gpu.retcode @@ -319,8 +328,10 @@ function classify(r::TestResult) # the GPU against, so skip (non-gating). Crucially this is checked *before* # the GPU retcode: a GPU that faithfully reproduces the CPU's Unstable/ # MaxIters result is matching, not a GPU incompatibility. - (r.cpu_error !== nothing || - (r.cpu_retcode !== nothing && !ok(r.cpu_retcode))) && return :cpu_skip + ( + r.cpu_error !== nothing || + (r.cpu_retcode !== nothing && !ok(r.cpu_retcode)) + ) && return :cpu_skip # CPU produced a reference; the GPU must now reproduce it without erroring. r.gpu_error !== nothing && return :gpu_error r.gpu_retcode !== nothing && !ok(r.gpu_retcode) && return :gpu_failed @@ -344,12 +355,12 @@ function run_all(cases = build_cases(); verbose = true) return results end -status_glyph(s) = s === :pass ? "✓" : - s === :cpu_skip ? "− skipped (cpu has no reference)" : - s === :gpu_mismatch ? "✗ gpu mismatch" : - s === :gpu_failed ? "✗ gpu retcode" : - s === :gpu_error ? "✗ gpu error" : - string(s) +status_glyph(s) = s === :pass ? "✓" : + s === :cpu_skip ? "− skipped (cpu has no reference)" : + s === :gpu_mismatch ? "✗ gpu mismatch" : + s === :gpu_failed ? "✗ gpu retcode" : + s === :gpu_error ? "✗ gpu error" : + string(s) function show_results(results; threshold = 1.0e-3) function _fmt(val) @@ -361,8 +372,10 @@ function show_results(results; threshold = 1.0e-3) _rc(rc) = rc === nothing ? "---" : string(rc) label_w = 42 - println(rpad("Solver / jac / mass", label_w), - "class cpu_abs cpu_rel gpu_abs gpu_rel cpu_rc / gpu_rc status") + println( + rpad("Solver / jac / mass", label_w), + "class cpu_abs cpu_rel gpu_abs gpu_rel cpu_rc / gpu_rc status" + ) println("-"^140) for r in results @@ -374,13 +387,13 @@ function show_results(results; threshold = 1.0e-3) print(_fmt(r.gpu_abs), " ", _fmt(r.gpu_rel), " ") print(rpad("$(_rc(r.cpu_retcode)) / $(_rc(r.gpu_retcode))", 24)) color = r.status === :pass ? :green : - r.status === :cpu_skip ? :yellow : :red + r.status === :cpu_skip ? :yellow : :red printstyled(status_glyph(r.status), "\n"; color) end println("-"^140) # Summary - counts = Dict{Symbol,Int}() + counts = Dict{Symbol, Int}() for r in results counts[r.status] = get(counts, r.status, 0) + 1 end @@ -400,6 +413,7 @@ function show_errors(results) r.gpu_error !== nothing && (println("GPU error:"); showerror(stdout, r.gpu_error); println()) println() end + return end # ── Convenience for interactive debugging ──────────────────────────────────── @@ -418,8 +432,8 @@ function debug_case(solver::Type; jac = "none", mass = "diag_cu") ref = case.needs_krylov_cpu ? SOL_REF_KRYLOV : SOL_REF cpu_alg = case.needs_krylov_cpu ? - case.solver(linsolve = KrylovJL_GMRES()) : - case.solver() + case.solver(linsolve = KrylovJL_GMRES()) : + case.solver() @info "CPU solve" sol_cpu = solve(PROB_CPU, cpu_alg; SOLVE_KWARGS...) @@ -427,7 +441,8 @@ function debug_case(solver::Type; jac = "none", mass = "diag_cu") @info "GPU solve" odef_d = make_odef(; mass_matrix = case.mass_matrix, - jac_prototype = case.jac_prototype) + jac_prototype = case.jac_prototype + ) prob_d = ODEProblem(odef_d, U0_D, TSPAN, P_D; initializealg = INITALG) sol_gpu = solve(prob_d, case.solver(); SOLVE_KWARGS...) @info "GPU retcode" sol_gpu.retcode From 8c09d60290fe1af54cb4b5e81754c6c886d99f4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Sat, 4 Jul 2026 12:22:49 +0200 Subject: [PATCH 19/20] use on local version of sublibs rather than registry version --- test/ODEInterfaceRegression/Project.toml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/ODEInterfaceRegression/Project.toml b/test/ODEInterfaceRegression/Project.toml index 6c4afe2e4e5..2de4f467b84 100644 --- a/test/ODEInterfaceRegression/Project.toml +++ b/test/ODEInterfaceRegression/Project.toml @@ -2,10 +2,16 @@ ODEInterface = "54ca160b-1b9f-5127-a996-1867f4bc2a2c" ODEInterfaceDiffEq = "09606e27-ecf5-54fc-bb29-004bd9f985bf" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" +OrdinaryDiffEqBDF = "6ad6398a-0878-4a85-9266-38940aa047c8" OrdinaryDiffEqCore = "bbf590c4-e513-4bbe-9b18-05decba2e5d8" +OrdinaryDiffEqDifferentiation = "4302a76b-040a-498a-8c04-15b101fed76b" OrdinaryDiffEqExplicitRK = "9286f039-9fbf-40e8-bf65-aa933bdc4db0" OrdinaryDiffEqHighOrderRK = "d28bc4f8-55e1-4f49-af69-84c1a99f0f58" OrdinaryDiffEqLowOrderRK = "1344f307-1e59-4825-a18e-ace9aa3fa4c6" +OrdinaryDiffEqNonlinearSolve = "127b3ac7-2247-4354-8eb6-78cf4e7c58e8" +OrdinaryDiffEqRosenbrock = "43230ef6-c299-4910-a778-202eb28ce4ce" +OrdinaryDiffEqRosenbrockTableaus = "b4bd8bb3-f80f-41d2-9b21-73a655b304b9" +OrdinaryDiffEqSDIRK = "2d112036-d095-4a1e-ab9a-08536f3ecdbf" SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" SciMLTesting = "09d9d899-5365-40a9-917a-5f67fddea283" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" @@ -23,7 +29,13 @@ SciMLTesting = "1" [sources] OrdinaryDiffEq = {path = "../../"} +OrdinaryDiffEqBDF = {path = "../../lib/OrdinaryDiffEqBDF"} OrdinaryDiffEqCore = {path = "../../lib/OrdinaryDiffEqCore"} +OrdinaryDiffEqDifferentiation = {path = "../../lib/OrdinaryDiffEqDifferentiation"} OrdinaryDiffEqExplicitRK = {path = "../../lib/OrdinaryDiffEqExplicitRK"} OrdinaryDiffEqHighOrderRK = {path = "../../lib/OrdinaryDiffEqHighOrderRK"} OrdinaryDiffEqLowOrderRK = {path = "../../lib/OrdinaryDiffEqLowOrderRK"} +OrdinaryDiffEqNonlinearSolve = {path = "../../lib/OrdinaryDiffEqNonlinearSolve"} +OrdinaryDiffEqRosenbrock = {path = "../../lib/OrdinaryDiffEqRosenbrock"} +OrdinaryDiffEqRosenbrockTableaus = {path = "../../lib/OrdinaryDiffEqRosenbrockTableaus"} +OrdinaryDiffEqSDIRK = {path = "../../lib/OrdinaryDiffEqSDIRK"} From d30b2174799bd540dfb070a747e7e34aa15bedf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Mon, 6 Jul 2026 15:01:21 +0200 Subject: [PATCH 20/20] fix missing import of AbstractSciMLOperator --- lib/OrdinaryDiffEqCore/src/OrdinaryDiffEqCore.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/OrdinaryDiffEqCore/src/OrdinaryDiffEqCore.jl b/lib/OrdinaryDiffEqCore/src/OrdinaryDiffEqCore.jl index e34d305d5e1..05c6abb27f1 100644 --- a/lib/OrdinaryDiffEqCore/src/OrdinaryDiffEqCore.jl +++ b/lib/OrdinaryDiffEqCore/src/OrdinaryDiffEqCore.jl @@ -37,7 +37,7 @@ import DiffEqBase: ODE_DEFAULT_NORM, ODE_DEFAULT_UNSTABLE_CHECK, DEVerbosity, _process_verbose_param -import SciMLOperators: MatrixOperator, FunctionOperator, +import SciMLOperators: AbstractSciMLOperator, MatrixOperator, FunctionOperator, update_coefficients, update_coefficients!, isconstant