diff --git a/lib/OrdinaryDiffEqBDF/src/OrdinaryDiffEqBDF.jl b/lib/OrdinaryDiffEqBDF/src/OrdinaryDiffEqBDF.jl index 3c5f198dcb9..d5be28f624e 100644 --- a/lib/OrdinaryDiffEqBDF/src/OrdinaryDiffEqBDF.jl +++ b/lib/OrdinaryDiffEqBDF/src/OrdinaryDiffEqBDF.jl @@ -26,7 +26,8 @@ import OrdinaryDiffEqCore: perform_step!, unwrap_alg, get_fsalfirstlast, generic_solver_docstring, _fixup_ad, _ode_interpolant, _ode_interpolant!, has_stiff_interpolation, _ode_addsteps!, DerivativeOrderNotPossibleError, set_discontinuity, - DIRK, COEFFICIENT_MULTISTEP, isnewton, set_new_W! + DIRK, COEFFICIENT_MULTISTEP, isnewton, set_new_W!, + find_algebraic_vars_eqs import SciMLBase: alg_order, isadaptive, _unwrap_val import DiffEqBase: calculate_residuals, calculate_residuals!, initialize! using OrdinaryDiffEqSDIRK: ESDIRKIMEXConstantCache, ESDIRKIMEXCache, diff --git a/lib/OrdinaryDiffEqBDF/src/bdf_caches.jl b/lib/OrdinaryDiffEqBDF/src/bdf_caches.jl index 3e58b3cfb49..e9984bf96c2 100644 --- a/lib/OrdinaryDiffEqBDF/src/bdf_caches.jl +++ b/lib/OrdinaryDiffEqBDF/src/bdf_caches.jl @@ -66,7 +66,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] ie_tab = ImplicitEulerESDIRKIMEXTableau( constvalue(uBottomEltypeNoUnits), constvalue(tTypeNoUnits) 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 d1b7cd834cf..05c6abb27f1 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, isdiag +using LinearAlgebra: opnorm, I, UniformScaling, diag, isdiag, Diagonal import PrecompileTools @@ -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 diff --git a/lib/OrdinaryDiffEqCore/src/misc_utils.jl b/lib/OrdinaryDiffEqCore/src/misc_utils.jl index d2f3ea15e54..bd750d7d449 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 88% rename from lib/OrdinaryDiffEqNonlinearSolve/test/sparse_algebraic_detection_tests.jl rename to lib/OrdinaryDiffEqCore/test/algebraic_vars_detection_tests.jl index bc7294fc278..dfbf333d585 100644 --- a/lib/OrdinaryDiffEqNonlinearSolve/test/sparse_algebraic_detection_tests.jl +++ b/lib/OrdinaryDiffEqCore/test/algebraic_vars_detection_tests.jl @@ -1,6 +1,7 @@ using Test using SparseArrays -using OrdinaryDiffEqNonlinearSolve: find_algebraic_vars_eqs +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 @@ -88,4 +89,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 diff --git a/lib/OrdinaryDiffEqCore/test/runtests.jl b/lib/OrdinaryDiffEqCore/test/runtests.jl index 46342deb64b..7aed7b54ebf 100644 --- a/lib/OrdinaryDiffEqCore/test/runtests.jl +++ b/lib/OrdinaryDiffEqCore/test/runtests.jl @@ -33,5 +33,6 @@ 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") @time @safetestset "Discontinuity Detection" include("disco_tests.jl") end diff --git a/lib/OrdinaryDiffEqDifferentiation/src/derivative_utils.jl b/lib/OrdinaryDiffEqDifferentiation/src/derivative_utils.jl index 54aaca1ce5c..09071aff2eb 100644 --- a/lib/OrdinaryDiffEqDifferentiation/src/derivative_utils.jl +++ b/lib/OrdinaryDiffEqDifferentiation/src/derivative_utils.jl @@ -184,7 +184,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 @@ -229,7 +229,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) @@ -573,6 +573,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/lib/OrdinaryDiffEqNonlinearSolve/src/OrdinaryDiffEqNonlinearSolve.jl b/lib/OrdinaryDiffEqNonlinearSolve/src/OrdinaryDiffEqNonlinearSolve.jl index a96bfd9b1a5..3a345be247a 100644 --- a/lib/OrdinaryDiffEqNonlinearSolve/src/OrdinaryDiffEqNonlinearSolve.jl +++ b/lib/OrdinaryDiffEqNonlinearSolve/src/OrdinaryDiffEqNonlinearSolve.jl @@ -45,7 +45,8 @@ using OrdinaryDiffEqCore: resize_nlsolver!, _initialize_dae!, Convergence, Divergence, NLStatus, MethodType, error_constant, - alg_extrapolates, resize_J_W!, alg_autodiff + alg_extrapolates, resize_J_W!, alg_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 62b0295bef5..6c30e2b9377 100644 --- a/lib/OrdinaryDiffEqNonlinearSolve/src/initialize_dae.jl +++ b/lib/OrdinaryDiffEqNonlinearSolve/src/initialize_dae.jl @@ -1,46 +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 - -# 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") diff --git a/lib/OrdinaryDiffEqRosenbrock/src/OrdinaryDiffEqRosenbrock.jl b/lib/OrdinaryDiffEqRosenbrock/src/OrdinaryDiffEqRosenbrock.jl index bd4ee9c8e08..de33264ec92 100644 --- a/lib/OrdinaryDiffEqRosenbrock/src/OrdinaryDiffEqRosenbrock.jl +++ b/lib/OrdinaryDiffEqRosenbrock/src/OrdinaryDiffEqRosenbrock.jl @@ -13,7 +13,8 @@ import OrdinaryDiffEqCore: alg_adaptive_order, isWmethod, isfsal, _unwrap_val, calculate_residuals, has_stiff_interpolation, ODEIntegrator, resize_non_user_cache!, _ode_addsteps!, full_cache, DerivativeOrderNotPossibleError, _fixup_ad, - LinearAliasSpecifier, copyat_or_push!, DifferentialVarsUndefined, resize_J_W! + LinearAliasSpecifier, copyat_or_push!, DifferentialVarsUndefined, resize_J_W!, + find_algebraic_vars_eqs using MuladdMacro: MuladdMacro, @muladd using FastBroadcast: FastBroadcast, @.. using RecursiveArrayTools: RecursiveArrayTools, recursivefill! diff --git a/lib/OrdinaryDiffEqRosenbrock/src/rosenbrock_caches.jl b/lib/OrdinaryDiffEqRosenbrock/src/rosenbrock_caches.jl index d78b1ce65e4..1ec05222240 100644 --- a/lib/OrdinaryDiffEqRosenbrock/src/rosenbrock_caches.jl +++ b/lib/OrdinaryDiffEqRosenbrock/src/rosenbrock_caches.jl @@ -250,7 +250,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₁, @@ -305,7 +305,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 1177690b937..23cb57342c1 100644 --- a/lib/OrdinaryDiffEqSDIRK/src/OrdinaryDiffEqSDIRK.jl +++ b/lib/OrdinaryDiffEqSDIRK/src/OrdinaryDiffEqSDIRK.jl @@ -17,7 +17,8 @@ using OrdinaryDiffEqCore: unwrap_alg, trivial_limiter!, generic_solver_docstring, _fixup_ad, current_extrapolant!, Predictor, - isnewton, get_W, set_new_W!, COEFFICIENT_MULTISTEP + isnewton, get_W, set_new_W!, COEFFICIENT_MULTISTEP, + find_algebraic_vars_eqs export Predictor using TruncatedStacktraces: @truncate_stacktrace using MuladdMacro: MuladdMacro, @muladd 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 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"} diff --git a/test/gpu/Project.toml b/test/gpu/Project.toml index 69357d3b085..7351d615d38 100644 --- a/test/gpu/Project.toml +++ b/test/gpu/Project.toml @@ -1,39 +1,67 @@ [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" +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" +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" [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"} +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"} [compat] Adapt = "4" CUDA = "6" +CUDSS = "0.7, 0.8" ComponentArrays = "0.15" -DiffEqBase = "7" -FastBroadcast = "1.3" FFTW = "1.8" +FastBroadcast = "1.3" FillArrays = "1" -OrdinaryDiffEq = "7" -OrdinaryDiffEqBDF = "2" -OrdinaryDiffEqNonlinearSolve = "2" -OrdinaryDiffEqRKIP = "2" -OrdinaryDiffEqRosenbrock = "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/dae_tests.jl b/test/gpu/dae_tests.jl new file mode 100644 index 00000000000..21fba7e85a4 --- /dev/null +++ b/test/gpu/dae_tests.jl @@ -0,0 +1,473 @@ +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). 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{true, FullSpecialize}( + 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 +# +# 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), # (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), # (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) + # ── Rosenbrock 5th order ── + (Rodas5, :suitable), + (Rodas5P, :suitable), + (Rodas5Pe, :suitable), # (CSC skipped: CPU MaxIters) + (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, :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 ───────────────────────────────────────────── + +# 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 (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, :cpu_skip (no CPU reference, non-gating), + # :gpu_mismatch, :gpu_failed, :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 + +# 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, 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() + + 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) + result.gpu_within_tol = + within_tol(sol_gpu, sol_cpu, gpu_match_tol(case.solver)) + end + catch e + result.gpu_error = e + end + + # Classify -------------------------------------------------------------- + result.status = classify(result) + return result +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.gpu_retcode !== nothing && !ok(r.gpu_retcode) && return :gpu_failed + 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[] + 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)) + end + 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) + +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 : + r.status === :cpu_skip ? :yellow : :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, :cpu_skip, :gpu_mismatch, :gpu_failed, :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 + return +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 + # :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 + +# res = debug_case(Rodas5P; jac = "CSR"); +# res = debug_case(Rosenbrock23; jac = "CSC");) 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 diff --git a/test/gpu/simple_dae.jl b/test/gpu/simple_dae.jl deleted file mode 100644 index 1b39a42cd1a..00000000000 --- a/test/gpu/simple_dae.jl +++ /dev/null @@ -1,67 +0,0 @@ -using OrdinaryDiffEqRosenbrock -using OrdinaryDiffEqNonlinearSolve -using CUDA -using LinearAlgebra -using Adapt -using SparseArrays -using Test - -#= -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 = [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) -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 - -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()) - -@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) - 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) - end -end 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