Repository navigation
Wrap PDE solutions with GPU states without scalar indexing - #716
Merged
Merged
Conversation
PDETimeSeriesSolution and PDENoTimeSolution extracted each grid point with sol[i, :] / sol.u[i] and evaluated observed functions on the raw states. With CuArray states that scalar-indexes device memory and throws under CUDA.allowscalar(false), so GPU users had to pass wrap = Val(false). Copy non-Array states to the host once (host_solution) and run all extraction and observed evaluation on that copy. Solutions whose states are already Arrays are used as is, so the CPU path is unchanged. Adds an opt-in GPU test group (test/GPU, own environment, not in "All" or test_groups.toml) covering a GPU-solved ODEProblem, and DAEProblem, NonlinearProblem and array-observed solutions with CuArray states, each compared against the host wrap of the same states. Agent-Harness: Claude Code 2.1.296 Agent-Model: claude-opus-5-5 Agent-Session: 644ed0ff-c8c9-4cc3-b4c6-f326231a4030 Agent-Review: unreviewed Co-authored-by: Claude Code <agent@noreply.juliahub.com>
Member
Author
|
1 parameters also belong on gpu |
xtalax
marked this pull request as ready for review
October 19, 2026 20:27
xtalax
added a commit
that referenced
this pull request
Oct 20, 2026
Resolves the conflict with #716 in the solution wrappers the same way as the tested integration branch (integration/mol-2026-10, 2952823). Agent-Harness: Claude Code 2.1.304 Agent-Model: claude-opus-5-5 Agent-Session: 644ed0ff-c8c9-4cc3-b4c6-f326231a4030 Agent-Review: unreviewed Co-authored-by: Claude Code <agent@noreply.juliahub.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Status: ready for review
GROUP=GPUpasses 17/17 andSol_Interface26/26 (Julia 1.12.6, RTX 4080 SUPER).host_solutioncopies the states and leavesprob.pwhere it is. Moving device-resident parameters to the host is left out until a real case needs it (see "Not done").GPUgroup stays opt-in and local-only (GROUP=GPU, not in"All"ortest_groups.toml), because CI has no GPU runner.Problem
After a GPU solve, the default
wrap = Val(true)builds aPDETimeSeriesSolution. That constructor scalar-indexes theCuArraystates, so it throwsScalar indexing is disallowedunderCUDA.allowscalar(false). Until now the only workaround wassolve(...; wrap = Val(false)).Root cause
PDETimeSeriesSolution(sol, ::MOLMetadata)insrc/interface/solution/timedep.jlbuilds each grid point's time series from the raw solution states, and it does so in three ways, all of which touch device memory:sol[i, :]does a per-elementVectorOfArraygetindex on every saved state.mtkcompileeliminated:SciMLBase.observed(sol, sym, :)runs the generated observed function on each state.array_observed_solutionevaluates the observed function onstate_values(sol), then doesgetindex.(values, (I,))per element.PDENoTimeSolutionintimeindep.jlhas the same pattern, usingsol.u[i]andSciMLBase.observed(sol, sym).Fix
A new
host_solution(sol)insolution_utils.jlcopies non-Arraystates to the host once, usingSciMLBase.build_solution(sol.prob, sol.alg, sol.t, Array.(sol.u); retcode, calculate_error = false). TheNonlinearSolutionmethod works the same way withArray(sol.u). Both constructors run all extraction and observed evaluation on that host copy. They still store the originalsolasoriginal_soland take every other field from it. The rebuilt solution keepsprob, so observed functions, parameters and the symbolic container are the same objects as before.When the states are already
Arrays,host_solutionreturnssolitself. The CPU path does exactly what it did before and makes no copy. A new assertion inSol_Interfacepins that (host_solution(sol.original_sol) === sol.original_sol).Tests
New opt-in GPU group
The GPU tests live in
test/GPU, with their ownProject.toml(CUDA, NonlinearSolve, and so on), wired intoruntests.jlthe same way asNeuralNets. The group is not part of"All"and is not listed intest_groups.toml, so CI never runs it. Run it withGROUP=GPU. If CUDA is not functional, it records a@test_skip.The test file sets
CUDA.allowscalar(false)for the whole file. It uses a 1D heat equation with 41 grid points, above the 16-element scalarfill_arr!threshold. Each case compares the GPU wrap with the host wrap of the same states, using exact equality (==):ODEProblemfrommtkcompileTsit5onCuArrayu0, run underCUDA.@allowscalarDAEProblemfrom defaultdiscretizeCuArraywithbuild_solutionNonlinearProblem(1D Poisson)umapped toCuArrayPDENoTimeSolution, unknowns and observedw = basis * reduced)CuArraystatesarray_observed_solutionThe real GPU solve needs scalar indexing for two reasons:
AutoDespecializefunction wrapper and MTK initialization convertu0back to aVector. To keep the states on the device, the test builds anODEFunction{true, FullSpecialize}from the compiled function and solves withinitializealg = NoInit().So only the wrapping runs under
allowscalar(false). A GPU solve of theDAEProblemfails inside the solver with a GPU kernel compilation error, which is unrelated to this change. That is why the DAE case maps CPU states to the device instead.Against unpatched
master, all four cases error: three withScalar indexing is disallowed, and the array-observed case with aMatrix * CuArrayBLASMethodError. With the patch, all 17 assertions pass.Test group results (local, Julia 1.12.6)
Sol_InterfaceComponents@test_brokeninutils_test.jl)DAEStationaryGPU(RTX 4080 SUPER, CUDA.jl 6.4.2)Runic --check .exits 0.Not done
host_solutioncopies the states but notprob.p. MOL'sMTKParametersare host arrays, and MTK has noAdaptrule for them. If someone builds a problem with device-resident tunables, the observed evaluation would still scalar-index them. I left that alone rather than guess at a conversion with no real case to test it against.wrap_sol. The fix is entirely in MOL's constructors.Note: the DAE case uses mapped states because a GPU DAE solve currently fails inside the solver. That failure comes from ModelingToolkit's scalarized
dureads in the DAE residual, which produce hostarray_literals inside GPU broadcasts. It is fixed by the draft SciML/ModelingToolkit.jl#5242, not by MethodOfLines.🤖 Posted by an AI agent · harness: Claude Code 2.1.296 · model: claude-opus-5-5
Review: unreviewed
Session: 644ed0ff-c8c9-4cc3-b4c6-f326231a4030