Share ordinary solver paths with Reactant - #4436
ChrisRackauckas-Claude wants to merge 5 commits into
Conversation
|
Implementation/CI plan:
|
5e7c058 to
fcef277
Compare
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Codex <noreply@openai.com> Agent-Harness: Codex CLI 0.151.0 Agent-Model: unknown Agent-Session: local session ID 01a04f92-0d12-7990-926c-f3e5a23f3a31
fcef277 to
9f78a3c
Compare
| if ReactantCore.within_compile() | ||
| isnothing(dt) && integrator.opts.adaptive && auto_dt_reset!(integrator) | ||
| return nothing | ||
| end |
There was a problem hiding this comment.
This isn't doing the tdir flip? Why not do this the other way around and just if not in a compile, do the error messages?
| if ReactantCore.within_compile() | ||
| return integrator.opts.adaptive ? | ||
| _traced_adaptive_solve!(integrator, integrator.controller_cache) : | ||
| _traced_fixed_step_solve!(integrator) | ||
| end |
There was a problem hiding this comment.
Why is this treated as separate code instead of integrating it?
| throw(ArgumentError("implicit algorithms are not supported inside Reactant compilation")) | ||
| !adaptive && isnothing(dt) && | ||
| throw(ArgumentError("dt is required for fixed-step solves inside Reactant compilation")) | ||
| isempty(saveat) || throw(ArgumentError("saveat is not supported inside Reactant compilation")) |
There was a problem hiding this comment.
doesn't reactant want a constant size output? That would require using saveat
| !adaptive && isnothing(dt) && | ||
| throw(ArgumentError("dt is required for fixed-step solves inside Reactant compilation")) |
There was a problem hiding this comment.
why is this different? dt with tstops is the only other way, but tstops aren't supported, so this is an unnecessary check?
| isimplicit(alg) && | ||
| throw(ArgumentError("implicit algorithms are not supported inside Reactant compilation")) |
Route compiled solves through the ordinary step loop, initial-step estimator, FSAL update, and I/PI controllers. Promote loop-carried state for tracing and retain narrow adapters for static output and endpoint handling. Cover custom controllers, zero dynamics, in-place fixed steps, and rejected first steps. Merge current master to resolve the test-group configuration and retain upstream fixes. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Codex <noreply@openai.com> Agent-Harness: Codex CLI 0.153.4 Agent-Model: gpt-6-astra Agent-Session: local transcript /home/crackauc/.codex/sessions/2026/09/05/rollout-2026-09-05T07-40-10-01a0715e-73c9-7e21-a18b-c5b0504a4bf3.jsonl
Keep the ordinary integrator's active FSAL aliases and select cache buffers at the tracing boundary. Composite cache initialization buffers need not belong to the active solver. The existing composite tests fail 2 of 16 checks without this correction and pass all 16 with it. Reactant passes 178/178 and QA passes 109/109. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Codex <noreply@openai.com> Agent-Harness: Codex CLI 0.153.4 Agent-Model: gpt-6-astra Agent-Session: local transcript /home/crackauc/.codex/sessions/2026/09/05/rollout-2026-09-05T07-40-10-01a0715e-73c9-7e21-a18b-c5b0504a4bf3.jsonl
|
Reworked the implementation to use the ordinary solver loop, initial-step estimator, controllers, acceptance/rejection, and FSAL update. Removed the separate traced solver and initial-step implementations. The remaining compilation checks handle traced state, buffer aliases, endpoint stops, and output/error boundaries. Local verification: Reactant 178/178; QA 109/109; full InterfaceI passed in 2367.4 seconds; targeted composite tests 16/16; docs build, Runic, and typos passed. The new IController regression errors on the original implementation and passes with this revision; the updated body includes the code and output. Commits: The PR remains a draft and should be ignored until reviewed by @ChrisRackauckas. CI is running. 🤖 Generated with Codex CLI 0.153.4 (model: gpt-6-astra). Session: local transcript |
| if isinplace(integrator.sol.prob) | ||
| recursivecopy!(integrator.fsalfirst, integrator.fsallast) | ||
| fsalfirst, fsallast = if ReactantCore.within_compile() | ||
| get_fsalfirstlast(integrator.cache, integrator.u) |
There was a problem hiding this comment.
why would this be a function call only within compile?
There was a problem hiding this comment.
Both execution modes now use the same FSAL-buffer accessor and copy. Composite/default/constant caches retain their active integrator buffers because their initialization accessor does not identify the current runtime buffers.
Updated in abb04b3. Clean local Reactant suite: 208/208; QA: 109/109.
| if ReactantCore.within_compile() | ||
| dt = integrator.opts.adaptive ? integrator.dt : integrator.dtcache | ||
| integrator.dt = integrator.tdir * min( | ||
| abs(dt), abs(first_tstop(integrator) - integrator.tdir * integrator.t) | ||
| ) | ||
| return nothing | ||
| end |
There was a problem hiding this comment.
why is clamping ignored in reactant compile?
There was a problem hiding this comment.
Removed the compilation-only timestep-clipping implementation. Both modes now use the normal stop flags, clipping, roundoff tolerance, and restoration of the proposed timestep.
Updated in abb04b3. Clean local Reactant suite: 208/208; QA: 109/109.
| integrator.accept_step = ( | ||
| !integrator.isout && | ||
| !integrator.isout & | ||
| accept_step_controller( |
There was a problem hiding this comment.
The built-in acceptance predicates only inspect controller state, but eager evaluation was unnecessary. The shared path now evaluates the predicate only when the candidate is in-domain, preserving short-circuit behavior for custom controllers as well.
Updated in abb04b3. Clean local Reactant suite: 208/208; QA: 109/109.
| # because it also checks if partials are NaN | ||
| # https://discourse.julialang.org/t/incorporating-forcing-functions-in-the-ode-model/70133/26 | ||
| if isnan(d₁) | ||
| if !ReactantCore.within_compile() && isnan(d₁) |
There was a problem hiding this comment.
why would reactant compile not do a NaN check?
There was a problem hiding this comment.
The NaN checks and fallback are now part of the common initial-step control flow, including compiled solves. Added native/compiled fallback parity tests for both RHS forms. The IIP path also checks the derivative directly because the fast-math norm can hide NaNs from a later scalar check; Float32/Float64 native regressions cover that fallback.
Updated in abb04b3. Clean local Reactant suite: 208/208; QA: 109/109.
| end | ||
| return tdir * max(dtmin, min(100dt₀, dt₁, dtmax_tdir)) | ||
| result_dt = tdir * max(dtmin, min(100dt₀, dt₁, dtmax_tdir)) | ||
| if ReactantCore.within_compile() |
There was a problem hiding this comment.
What exactly is this doing?
There was a problem hiding this comment.
That deferred correction was compensating for skipped early decisions during tracing. Removed it: NaN and constant-derivative decisions now occur in the shared initial-step path.
Updated in abb04b3. Clean local Reactant suite: 208/208; QA: 109/109.
| # Function wrappers hide types from Reactant and provide no compile-time reuse inside `@jit`. | ||
| SciMLBase.specialization(::ODEFunction{iip, SciMLBase.AutoSpecialize}) where {iip} = | ||
| ReactantCore.within_compile() ? SciMLBase.FullSpecialize : SciMLBase.AutoSpecialize |
There was a problem hiding this comment.
This is piracy, should go to SciMLBase
There was a problem hiding this comment.
Moved specialization dispatch to a ReactantCore extension owned by SciMLBase, with instance/type and outside-compilation tests: SciML/SciMLBase.jl#1564. OrdinaryDiffEq no longer defines those methods.
Updated in abb04b3. Clean local Reactant suite: 208/208; QA: 109/109.
| if ReactantCore.within_compile() && adaptive && | ||
| !(controller_cache isa Union{IControllerCache, PIControllerCache}) | ||
| throw(ArgumentError("only IController and PIController are supported inside Reactant compilation")) |
There was a problem hiding this comment.
Removed the I/PI whitelist. PID now uses the common controller implementation with a traceable error history, and is covered by the compiled solve tests.
Updated in abb04b3. Clean local Reactant suite: 208/208; QA: 109/109.
Use shared FSAL access, stop clipping, controller acceptance, and initial-step control flow. Support PID tuple history and keep specialization in SciMLBase. Cover compiled clipping, PID and NaN parity plus native controller reinit and NaN initial-step regressions. Merge current upstream solver changes. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Codex <noreply@openai.com> Agent-Harness: Codex CLI 0.153.4 Agent-Model: gpt-6-astra Agent-Session: local transcript /home/crackauc/.codex/sessions/2026/09/05/rollout-2026-09-05T07-40-10-01a0715e-73c9-7e21-a18b-c5b0504a4bf3.jsonl
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Codex <noreply@openai.com> Agent-Harness: Codex CLI 0.153.4 Agent-Model: gpt-6-astra Agent-Session: local transcript /home/crackauc/.codex/sessions/2026/09/05/rollout-2026-09-05T07-40-10-01a0715e-73c9-7e21-a18b-c5b0504a4bf3.jsonl
Important
Ignore this PR until it has been reviewed by @ChrisRackauckas.
What changed and why
Explicit ODE solves inside Reactant use the ordinary solver loop, step acceptance/rejection, FSAL updates, initial-step estimator, and I/PI/PID controllers. The numerical paths share traceable control flow; backend checks remain at tracing/output boundaries and host-only diagnostics. SciMLBase specialization methods now live in the owning package's extension: SciML/SciMLBase.jl#1564.
Both execution modes use the same FSAL-buffer accessor and timestep-stop flags, clipping, and proposed-step restoration. Rejected terminal steps no longer exit the solve early. Endpoint roundoff detection uses a shared relative guard,
100eps(time_type) * max(abs(t), abs(tstop)), instead of the old value-dependent100eps(scale). PID history is a three-element tuple in the ordinary cache, avoiding scalar indexing of traced arrays. Controller acceptance preserves short-circuit evaluation for custom controllers.Initial-step NaN and constant-derivative decisions execute in the common path. A direct derivative NaN check also fixes a native failure: the fast-math default norm can make a subsequent scalar
isnancheck return false. The norm check remains to catch NaN AD partials. Generic estimator helpers and locally scoped temporaries keep SIMD and logging implementation details out of traced branch outputs.Verification
The regression uses the same dependency versions before and after (Reactant 0.2.285, ReactantCore 0.1.22). Reinstating the previous compilation-only clipping path and I/PI whitelist produces:
The revised official Reactant suite covers these assertions, native/compiled NaN fallback parity, reusable compiled executables, zero dynamics, rejected first steps, and analytic accuracy. The analytic tolerance remains
rtol=5e-4; I/PID cases requestabstol=1e-7, reltol=1e-5.The exact initial-step test file was run with the previous Core implementation and then with the final implementation:
The native NaN regression also fails on clean OrdinaryDiffEq master for both Float32 and Float64 (
dt = NaN) and passes with the direct guard (dt = nextfloat(dtmin)). Independent historical reduction identifies the norm's switch tosqrt_fastas the boundary where the scalar NaN check stops detecting NaNs; this was a reduced historical-method check, not a full historical solver-environment bisect.The full InterfaceI and clean Reactant runs above both use the final solver source, including the direct NaN guard and shared branch scoping. The minimal native fix was also independently checked against the existing initial-step test file on master, which passed.
Julia jobs used
TMPDIR="$PWD/.work/tmp"and local package environments. Tests were not skipped or weakened.CI status
The branch's spell check fails on the unchanged master typo
simultaenousinlib/DiffEqBase/test/modelingtoolkit/events.jl. A separate mechanical cleanup already covers it: #4524. Runic also fails on pre-existing trailing whitespace in the BDF regression test, covered by the same cleanup. The review-changed files pass both checks locally and pass the CI Runic inspection. The unchanged Dependabot workflow also reports a startup failure without creating jobs; solver jobs are still running or queued. CI is not green.Scope and release preparation
Compiled output remains endpoint-only:
prob,stats, andinterpare stripped. Intermediate/partial saving, callbacks, user stops/discontinuities, implicit algorithms, progress, custom domain/instability checks, and step limiters remain unsupported. Fixed-step solves require an explicitdt. Tests cover Tsit5 and Vern7, including in-place Tsit5.The draft still depends on open SciMLBase return-code/specialization and Reactant traced-enum prerequisites. The Reactant test group installs those branches. The temporary version/compat setup needs release preparation before merge.
Reactant and ReactantCore use MIT licenses, matching OrdinaryDiffEq. This revision adds no dependencies or public names. Not verified locally:
GROUP=Everything, GPU jobs, downstream suites, performance benchmarks, and Julia LTS/pre-release matrices.Links
🤖 Generated with Codex CLI 0.153.4 (model: gpt-6-astra). Session: local transcript
/home/crackauc/.codex/sessions/2026/09/05/rollout-2026-09-05T07-40-10-01a0715e-73c9-7e21-a18b-c5b0504a4bf3.jsonl.