Skip to content

Share ordinary solver paths with Reactant - #4436

Draft
ChrisRackauckas-Claude wants to merge 5 commits into
SciML:masterfrom
ChrisRackauckas-Claude:agent/reactant-jit-solve
Draft

ChrisRackauckas-Claude wants to merge 5 commits into
SciML:masterfrom
ChrisRackauckas-Claude:agent/reactant-jit-solve

Conversation

@ChrisRackauckas-Claude

@ChrisRackauckas-Claude ChrisRackauckas-Claude commented Aug 30, 2026

Copy link
Copy Markdown
Member

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-dependent 100eps(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 isnan check 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:

PID: ArgumentError: only IController and PIController are supported inside Reactant compilation
Fixed-step endpoint clipping: 101 == 100 (failed in all four RHS/direction cases)
Test Summary:                    | Fail  Error  Total     Time
Review regressions without fixes |    4      1      5  4m51.7s

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 request abstol=1e-7, reltol=1e-5.

The exact initial-step test file was run with the previous Core implementation and then with the final implementation:

$ /home/crackauc/.juliaup/bin/julia +1.12.4 --project=.work/unfixed-env --compiled-modules=existing -e 'using Test; @testset "Initial-step regressions before fix" include("test/InterfaceI/ode_initdt_tests.jl")'
Initial-step regressions before fix | 37 passed, 2 failed, 39 total | 2m30.6s

$ /home/crackauc/.juliaup/bin/julia +1.12.4 --project=.work/env --compiled-modules=existing -e 'using Test; @testset "Initial-step regressions with fix" include("test/InterfaceI/ode_initdt_tests.jl")'
Initial-step regressions with fix | 39 passed, 39 total | 2m29.6s

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 to sqrt_fast as the boundary where the scalar NaN check stops detecting NaNs; this was a reduced historical-method check, not a full historical solver-environment bisect.

$ GROUP=Reactant /home/crackauc/.juliaup/bin/julia +1.12.4 --project=. -e 'using Pkg; Pkg.test(; julia_args=["--compiled-modules=existing"])'
Reactant Tests | 208 passed, 208 total | 13m24.1s
Testing OrdinaryDiffEq tests passed

$ GROUP=QA /home/crackauc/.juliaup/bin/julia +1.12.4 --project=. -e 'using Pkg; Pkg.test(; julia_args=["--compiled-modules=existing"])'
Quality Assurance Tests | 109 passed, 109 total | 1m41.5s
Testing OrdinaryDiffEq tests passed

$ GROUP=InterfaceI /home/crackauc/.juliaup/bin/julia +1.12.4 --project=. -e 'using Pkg; Pkg.test(; julia_args=["--compiled-modules=existing"])'
Tstops Tests | 62 passed, 62 total
Initdt Tests | 39 passed, 39 total
Controller Tests | 33 passed, 33 total
2259.164576 seconds
Testing OrdinaryDiffEq tests passed

$ /home/crackauc/.juliaup/bin/julia +1.12.4 --project=docs --compiled-modules=existing docs/make.jl
Documenter completed HTML rendering; exit code 0.

$ /home/crackauc/.juliaup/bin/julia +1.10 --project=../runic-1.10-env -e 'using Runic; exit(Runic.main(ARGS))' -- --check <review-changed Julia files>
Exit code 0.

$ typos <review-changed files>
Exit code 0; no findings.

$ git diff --check upstream/master
Exit code 0.

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 simultaenous in lib/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, and interp are 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 explicit dt. 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.

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member Author

Implementation/CI plan:

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
Comment on lines +1001 to +1004
if ReactantCore.within_compile()
isnothing(dt) && integrator.opts.adaptive && auto_dt_reset!(integrator)
return nothing
end

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment thread lib/OrdinaryDiffEqCore/src/solve.jl Outdated
Comment on lines +932 to +936
if ReactantCore.within_compile()
return integrator.opts.adaptive ?
_traced_adaptive_solve!(integrator, integrator.controller_cache) :
_traced_fixed_step_solve!(integrator)
end

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't reactant want a constant size output? That would require using saveat

Comment on lines +207 to +208
!adaptive && isnothing(dt) &&
throw(ArgumentError("dt is required for fixed-step solves inside Reactant compilation"))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this different? dt with tstops is the only other way, but tstops aren't supported, so this is an unnecessary check?

Comment on lines +205 to +206
isimplicit(alg) &&
throw(ArgumentError("implicit algorithms are not supported inside Reactant compilation"))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why?

ChrisRackauckas and others added 2 commits September 5, 2026 09:17
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
@ChrisRackauckas-Claude ChrisRackauckas-Claude changed the title Trace explicit ODE solves under Reactant Share ordinary solver paths with Reactant Sep 5, 2026
@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member Author

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 /home/crackauc/.codex/sessions/2026/09/05/rollout-2026-09-05T07-40-10-01a0715e-73c9-7e21-a18b-c5b0504a4bf3.jsonl.

if isinplace(integrator.sol.prob)
recursivecopy!(integrator.fsalfirst, integrator.fsallast)
fsalfirst, fsallast = if ReactantCore.within_compile()
get_fsalfirstlast(integrator.cache, integrator.u)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why would this be a function call only within compile?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +282 to +288
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is clamping ignored in reactant compile?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function modifies?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread lib/OrdinaryDiffEqCore/src/initdt.jl Outdated
# 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₁)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why would reactant compile not do a NaN check?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread lib/OrdinaryDiffEqCore/src/initdt.jl Outdated
end
return tdir * max(dtmin, min(100dt₀, dt₁, dtmax_tdir))
result_dt = tdir * max(dtmin, min(100dt₀, dt₁, dtmax_tdir))
if ReactantCore.within_compile()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What exactly is this doing?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread lib/OrdinaryDiffEqCore/src/reactant.jl Outdated
Comment on lines +3 to +5
# 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is piracy, should go to SciMLBase

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread lib/OrdinaryDiffEqCore/src/solve.jl Outdated
Comment on lines +789 to +791
if ReactantCore.within_compile() && adaptive &&
!(controller_cache isa Union{IControllerCache, PIControllerCache})
throw(ArgumentError("only IController and PIController are supported inside Reactant compilation"))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

ChrisRackauckas and others added 2 commits September 12, 2026 06:49
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants