Repository navigation
Interpolate to tstops by step length and handle sub-dtmin stop spacing - #561
Draft
ChrisRackauckas-Claude wants to merge 17 commits into
Draft
ChrisRackauckas-Claude wants to merge 17 commits into
ChrisRackauckas-Claude wants to merge 17 commits into
Conversation
Keep the step's own state at a tstop only when the accepted step length is exactly the distance to that stop; timestamps within one ulp say nothing about how far past the stop the step integrated (8 time units at t = 2^26 in Float32). Otherwise interpolate to the stop as before. Never clamp the next step to a stop closer than the minimum step size, and leave stops within the snap threshold of `tf` to the endpoint snap. Adjacent representable stops are then reached by the following step's tstop branch instead of forcing `dt < dtmin`. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Agent-Harness: Claude Code 2.1.280 Agent-Model: claude-opus-5-5[1m] Agent-Session: https://claude.ai/code/session_01SJfy3ez7u4CqvAerKrT5np
Shorten each trial step so it ends exactly on the next pending tstop (when that stop is at least dtmin away) instead of overshooting and replacing the step's end state with an interpolated one. The step's dense output then stays intact for saveat points it covers, and the state at the stop is the step's own result. Honor every pending stop before `tf`, including stops within dtmin of it, and allow a step shorter than dtmin only as the forced final interval to `tf`. Previously such stops were left to the endpoint snap, which skipped their callbacks (including terminating ones). Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Agent-Harness: Claude Code 2.1.280 Agent-Model: claude-opus-5-5[1m] Agent-Session: https://claude.ai/code/session_01SJfy3ez7u4CqvAerKrT5np
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Agent-Harness: Claude Code 2.1.280 Agent-Model: claude-opus-5-5[1m] Agent-Session: https://claude.ai/code/session_01SJfy3ez7u4CqvAerKrT5np
A step shorter than dtmin was allowed whenever less than dtmin remained to `tf`. When error control rejected that final interval and shrank the step, the shorter accepted step was then snapped to `tf`, reporting success with only part of the interval integrated. Exempt only a step covering exactly the remaining interval; a rejected sub-dtmin final step fails with `dt<dtmin`, as before. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Agent-Harness: Claude Code 2.1.280 Agent-Model: claude-opus-5-5[1m] Agent-Session: https://claude.ai/code/session_01SJfy3ez7u4CqvAerKrT5np
Replace the absolute 1e-14 endpoint snap with exact landing. Each trial step is bounded by the next landing target (the next pending tstop at least dtmin away, otherwise `tf`) and becomes exactly the remaining distance when it would reach the target or leave less than 1% of a step. The accept branch sets `t = tf` only for a step of exactly `tf - t`, so completion always means the whole interval was integrated; only the rounding of `t + (tf - t)` is absorbed. A stop lies inside an accepted step only when `stop - t <= dt` (no absolute tolerance). The dtmin check also rejects steps too small to change `t`, and exempts only a step that is exactly the remaining interval. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Agent-Harness: Claude Code 2.1.280 Agent-Model: claude-opus-5-5[1m] Agent-Session: https://claude.ai/code/session_01SJfy3ez7u4CqvAerKrT5np
A Float64 `tstops` array for a Float32 problem made the bounded step Float64, and Kvaerno3/Kvaerno5 then failed to build their nonlinear solver. Convert the pending stop to the time type before using it. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Agent-Harness: Claude Code 2.1.280 Agent-Model: claude-opus-5-5[1m] Agent-Session: https://claude.ai/code/session_01SJfy3ez7u4CqvAerKrT5np
A stop closer than dtmin to the current time was not a landing target, so the step covering it landed there by evaluating its dense output near Θ = 1, which is inaccurate for the Float32 Verner interpolants (Vern9 was off by 4e-4 on an exactly integrable problem). Make every pending stop after `t` a landing target and exempt a step that lands exactly on its target from dtmin. A step shortened only to land on a stop keeps the controller's earlier proposal for the next step, and `dtnew` is no longer clamped after acceptance (the next trial step is bounded anyway), so a one-ulp landing step does not collapse the step size. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Agent-Harness: Claude Code 2.1.280 Agent-Model: claude-opus-5-5[1m] Agent-Session: https://claude.ai/code/session_01SJfy3ez7u4CqvAerKrT5np
Cramer's rule in the 2x2/3x3 `linear_solve` forms products of matrix entries without scaling. A Rosenbrock W = I/(γ dt) - J with a tiny landing step has entries near 1/dt, so the determinant overflowed and Rodas4/Rodas5P silently returned a zero (or NaN) increment. When the determinant or the solution is not finite, solve the system divided by its largest entry instead; the solution is unchanged and well-scaled systems keep their exact arithmetic. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Agent-Harness: Claude Code 2.1.280 Agent-Model: claude-opus-5-5[1m] Agent-Session: https://claude.ai/code/session_01SJfy3ez7u4CqvAerKrT5np
Landing on a stop with an integrated step of length near floatmin overflows the `C/dt` terms in the Rosenbrock stage formulas, and the NaN error estimate was accepted. Below `_landing_floor = 1024 / floatmax(T)` (|C| ≤ 166 in Rodas5P), a stop is reached by interpolating the covering step as before, and a final interval shorter than the floor fails `dtmin` instead of running the step. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Agent-Harness: Claude Code 2.1.280 Agent-Model: claude-opus-5-5[1m] Agent-Session: https://claude.ai/code/session_01SJfy3ez7u4CqvAerKrT5np
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Agent-Harness: Claude Code 2.1.280 Agent-Model: claude-opus-5-5[1m] Agent-Session: https://claude.ai/code/session_01SJfy3ez7u4CqvAerKrT5np # Conflicts: # src/ensemblegpukernel/integrators/integrator_utils.jl
A step whose state or scaled error is not finite is never accepted. A step shortened to land on a stop retries with the controller's covering step and lands on the stop by interpolation; any other step is rejected and shrunk. The check is on the norm's inputs because `ODE_DEFAULT_NORM` uses fast-math, under which `isfinite` on its result is folded to true. This replaces the fixed landing floor, which a scaled mass matrix could still overflow. `savevalues!` returns the accepted state (or the previous one) for saveat times at the ends of a step; the dense output, which is not exact at Θ = 0 or 1 for every method, is used only strictly inside the step. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Agent-Harness: Claude Code 2.1.280 Agent-Model: claude-opus-5-5[1m] Agent-Session: https://claude.ai/code/session_01SJfy3ez7u4CqvAerKrT5np
`continue` in the error-control loop produced SPIR-V that fails validation on OpenCL (a block emitted before its dominator). Fold the retry into the rejection branch instead: a non-finite step sets `EEst = Inf`, and a rejected landing step retries with the controller's covering step rather than a shrunken one. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Agent-Harness: Claude Code 2.1.280 Agent-Model: claude-opus-5-5[1m] Agent-Session: https://claude.ai/code/session_01SJfy3ez7u4CqvAerKrT5np
The retry after an overflowing landing step uses the controller's covering step, which can reach past `tf`; the step was then accepted past `tf` with the wrong time and state. Such a step now lands on `tf` by interpolation, so endpoint callbacks still run. A state obtained by interpolating to a stop or to `tf` that is not finite fails explicitly instead of returning success. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Agent-Harness: Claude Code 2.1.280 Agent-Model: claude-opus-5-5[1m] Agent-Session: https://claude.ai/code/session_01SJfy3ez7u4CqvAerKrT5np
Fold the covering retry's landing on `tf` into the stop-landing branch so each adaptive stepper keeps a single dense-output evaluation in the accept branch, which keeps the kernels' code size (and Julia 1.12 compile time) where it was. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Agent-Harness: Claude Code 2.1.280 Agent-Model: claude-opus-5-5[1m] Agent-Session: https://claude.ai/code/session_01SJfy3ez7u4CqvAerKrT5np
A covering retry that could reach past `tf` made the dense-output call in the accept branch reachable for every problem, including those without tstops, where it is otherwise dead code. Compiling the Verner interpolants for ForwardDiff duals inline in the step loop took 236 s (Vern7) and 1177 s (Vern9) on Julia 1.12, against ~20 s and ~50 s on master. The covering retry now applies only to a landing on a stop before `tf`, so it never reaches past `tf`. A final interval whose step is not finite fails explicitly (`dt<dtmin`) instead of being interpolated. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Agent-Harness: Claude Code 2.1.280 Agent-Model: claude-opus-5-5[1m] Agent-Session: https://claude.ai/code/session_01SJfy3ez7u4CqvAerKrT5np
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Agent-Harness: Claude Code 2.1.280 Agent-Model: claude-opus-5-5[1m] Agent-Session: https://claude.ai/code/session_01SJfy3ez7u4CqvAerKrT5np
`2^55` is an Int, which overflows to 0 on 32-bit Julia, so the saveat testsets' `eps(t0) == T(8)` assertion failed on the x86 CPU job. `T(2)^55` and `T(2)^26` give bit-identical times on 64-bit. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Agent-Harness: Devin CLI (edit); Claude Code 2.1.280 (verification and commit) Agent-Model: swe-2; claude-opus-5-5[1m] Agent-Session: local Devin job ~/sandbox/swe2-queue/work/difeqgpu-561-x86-int-overflow-r2 on amdci2; https://claude.ai/code/session_01SJfy3ez7u4CqvAerKrT5np Claude-Session: https://claude.ai/code/session_01SJfy3ez7u4CqvAerKrT5np
ChrisRackauckas-Claude
pushed a commit
to ChrisRackauckas-Claude/DiffEqGPU.jl
that referenced
this pull request
Oct 3, 2026
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SJfy3ez7u4CqvAerKrT5np
This branch has not been deployed
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.
Ignore this draft until reviewed by @ChrisRackauckas.
What changed and why
#556 (merged as
d6be47f) made adaptive steps land on pending tstops. Its review and six rounds of review of this PR found silent partial integrations, each coming from absolute thresholds: the 1e-14 endpoint snap, the 1e-14dtmin, and the100epststop tolerance, interacting with step control and tstops. This PR replaces those thresholds with one invariant, in all nine adaptiveEnsembleGPUKernelsteppers:Implementation, through shared
@inlinehelpers inintegrator_utils.jl:_bounded_stepchooses every trial step. It never goes past the next landing target, which is the next pending tstop aftert, otherwisetf. It becomes exactly the remaining distance when the proposal reaches the target or would leave less than 1% of a step. Steps therefore end on stops and ontfby construction. The state at a stop is the step's own result, and its dense output stays intact forsaveat.stop - t <= dt, no absolute tolerance) is landed on; otherwiset = tfifdt == tf - t, elset += dt.dtmincheck errors when the step is below 1e-14 or too small to changet. The only exemption is a step of exactly the distance to its landing target (a stop ortf). So a rejected or otherwise shortened step belowdtminfails withdt<dtmininstead of being snapped over.tis a landing target. A step that lands exactly on its target (a stop ortf) is exempt fromdtmin, so close stops, including adjacent stops one ulp apart, are reached by integrated steps. A step shortened only to land on a stop keeps the controller's earlier proposal for the next step, anddtnewis not clamped after acceptance (the next trial step is bounded anyway), so a one-ulp landing step does not collapse the step size. Every stop beforetfis honored and its callbacks run, including terminating ones.The Float32 endpoint stall from #544 cannot recur: the final step is exactly
tf - t, and a step that does not advanceterrors instead of looping.Bounding the step by
tfalso stops a first step from overshootingtf. The reproducer of #535 now gives[0.99004984, 0.3680994](exact[0.99004984, 0.36787945], test rtol 1e-2), where master gives[0.98403025, 0.20528531]. That overlaps #537.Converts each pending stop to the integrator's time type before using it. A Float64
tstopsarray on a Float32 problem otherwise made the bounded step Float64, and Kvaerno3/Kvaerno5 threwMethodErrorbuilding their nonlinear solver (round-7 review; commit51c325d).Solves badly scaled small linear systems by equilibration. The 2×2/3×3
linear_solveuses Cramer's rule without scaling. For a Rosenbrock W = I/(γ·dt) − J with a tiny landing step (e.g.dt = 1e-20), the determinant overflowed, and Rodas4/Rodas5P silently returned a zero (or NaN) increment (round-9 review). When the determinant or the solution is not finite,linear_solvenow solves the system divided by its largest entry. The solution is unchanged in exact arithmetic, and well-scaled systems keep their exact unscaled arithmetic (results are bitwise identical to before). This also covers Kvaerno3/5's Newton solves, which use the samelinear_solve.Never accepts a non-finite step. If a trial step's state or scaled error is not finite, it is not accepted. A step shortened to land on a stop retries with the controller's covering step and lands on the stop by interpolation, as master does. For tiny landing steps, the Rosenbrock
C/dtterms andW = J - M/(γ dt)can overflow at any fixed threshold (a scaled mass matrix defeats one), so this guard replaces a fixed landing floor. Any other non-finite step is rejected and shrunk, and ends indt<dtminif it stays non-finite. The check is on the norm's inputs, through a non-short-circuitmapreduce(isfinite, &, …):ODE_DEFAULT_NORMuses fast-math, under whichisfiniteon its result folds totrueandNaN > 1would accept the step. The retry lives in the rejection branch rather than as acontinue, becausecontinuein the error-control loop produced SPIR-V that fails OpenCL validation.Saves step endpoints exactly.
savevalues!returns the accepted state (or the previous one) forsaveattimes equal to the step's end (or start). The dense output, which is not exact at Θ = 0 or 1 for every method (Vern9 Float32: 4.7e-4 at Θ = 1), is used only strictly inside a step.Never accepts a step past
tf. The covering retry applies only to a landing on a stop beforetf, so it never reaches pasttf. A final interval whose step is not finite is rejected and shrunk until it fails with an explicitdt<dtmin, rather than being interpolated. A state obtained by interpolating to a covered stop that is not finite fails with"non-finite state at tstop".Regressions on master, public reproducers
5e8faeet0 = 2^26,tf = t0 + 64,dt = 32,tstops = [t0 + 24], Tsit5 (expect 64)[0.8125, nextfloat(0.8125)], callback +10 at each (expect 21.25)dt<dtminprevfloat(2.0), callback +10 thenterminate!(expect 11.25 att = prevfloat(2))dt<dtmint = 1.9999999999999998[prevfloat(2)],[2 - 1.5e-14, 2 - 5e-15],[prevfloat(2), 2]with callbacks, all 9 algorithmsdt<dtminfor all 27t0 = 2^26(Float32) /2^55(Float64),tf = t0 + 32,tstops = [t0 + 24],saveat = [t0, t0 + 8](expect 8)The fourth and fifth rows are the round-4 review's reproducers (
endpoint_stops.jl,coarse_saveat_types.jl). On this PR's previous head0d0f775they gave 45 pass / 27 fail (near-tfcallbacks silently dropped, terminating callback skipped: final1.25att = 2.0) and 6.0–6.75 instead of 8 for Rodas4, Rodas5P, Kvaerno3 and Kvaerno5.Tests
test/gpu_kernel_de/adaptive_endpoint.jl(CPU group), all nine adaptive algorithms, 451 assertions:Termination:
step!with an iteration cap, Float32 and Float64, over 200tfvalues × λ ∈ {1, 10, 100} × reltol ∈ {1e-3, 1e-4, 1e-6} foru' = (-u₁, -λu₂). Every configuration must reacht == tf.Snap:
u' = (1, 1)fromt0 = 2.094426f7totf = 9.4428696f7withdt0 = tf - t0, wheret0 + (tf - t0)rounds one ulp (8) belowtf. Each algorithm must reachtfin one step. For the six methods that integrateu' = 1exactly, the final state must equalu0 + (tf - t0).One tstop before the endpoint (
step!):t0 = 0.027657658f0,tf = 1,dt0 = prevfloat(tf - t0),tstops = [0.5]. The stop is visited,dtnew > 0whenevert < tf, andt == tfat the end.One tstop, public
solve:tspan = (0.75f0, 1f0),dt = prevfloat(0.25f0),tstops = [0.875]. Finalt == 1, final state≈ 1.25(rtol 1e-6).Multiple tstops (
step!): same span,tstops = [0.8125, 0.875, 0.9375]. Each stop is visited exactly once and in order, times are sorted, andt == tfat the end.Callback at an intermediate tstop, public
solve: the reproducer from the round-10 review, aDiscreteCallbackadding 10 att == 0.875withmerge_callbacks = true. Finalt == 1and final state≈ 11.25.Tstop inside a coarse step, public
solve: Float32,u' = (1, 1),u0 = 0,t0 = 2^26,tf = t0 + 32,dt = 32,tstops = [t0 + 24]. One ulp is 8 here, and the initial step of 32 would cross the stop. The final state must be≈ 32. (tf = t0 + 32keeps every step a whole number of ulps, so the test does not depend on Adaptive kernel steppers: t += dt rounding decouples time from integrated length at large t #567.)Adjacent tstops with callbacks, public
solve: Float64,tspan = (0.75, 2),dt = 0.0625,tstops = [0.8125, nextfloat(0.8125)], and aDiscreteCallbackadding 10 at each stop. The final state must be≈ 21.25, i.e. both callbacks fire.Tstops at the endpoint (callbacks, public
solve): stops[2],[prevfloat(2)],[2 - 1.5e-14, 2 - 5e-15]and[prevfloat(2), 2]ontspan = (0.75, 2), with a callback adding 10 at each stop. The test checks that every callback fires andt == 2at the end.Terminating callback one ulp before
tf: the solve must stop atprevfloat(2)with the callback's state.saveatinside a step that reaches a tstop: Float32t0 = 2^26and Float64t0 = 2^55(one ulp = 8),tstops = [t0 + 24],saveat = [t0, t0 + 8]. The sample att0 + 8must be 8.Rejected sub-
dtminfinal interval:u' = -1e15 u,tspan = (0, 4e-15),dt = 4e-15. Error control rejects the single full-interval step. The solve must either fail withdt<dtminor returnexp(-4)(rtol 1e-5), never a different value with success.Stiff dynamics switched on just before
tf: a callback at the stop1 - 4e-15setsu₁ = 1e15inu₂' = -u₁u₂, with the same pass condition againstexp(-1e15 (tf - stop)).Property grid: for each algorithm, every combination of reltol ∈ {1e-6, 1e-9}, interval length L ∈ {1e-13, 1e-3, 1}, stiffness λL ∈ {0.1, 1, 4} for
u' = -λu, initialdt∈ {0.5, 0.999, 1}·L, and stops ∈ {none, L/4, 0.99L, prevfloat(L), L − 5e-15}. Each solve must either fail withdt<dtminor end within 2000·reltol (relative) ofexp(-λL). On this head every method's error stays below 900·reltol (Rosenbrock23, second order, is the largest); a skipped remainder produces errors of 1e-4 to 0.3. The round-6 reproducer (u' = -1e12u, L = 1e-13,dt = 5e-14, stop 2.5e-14, Rodas5P/Kvaerno5) is one of these cases.Float64 tstops on a Float32 problem:
tspan = (0.75f0, 1f0),dt = 0.125f0,tstops = [0.875]; finalt == 1and state≈ 1.25. It errors for Kvaerno3/Kvaerno5 onceeed40(310 pass / 2 errors) and passes on51c325d(312/312).Stop closer than
dtmin, publicsolve: Float32dt = 1f-14, stopprevfloat(dt),u' = (1f14, 0), terminating callback at the stop. The state must be within 64 eps(Float32) of the BigFloat value0.9999999015. On51c325dVern9 returns1.0004045(fails); this head passes for all nine.Tiny landing step: Float32
u' = (r, r)forr∈ {1f14, 1f20},dt = 1f-14, stop1f-20, terminating callback; the state must be within 64 eps(Float32) of the BigFloat value. On1b90199Rodas4/Rodas5P return0(1f14) or fail (1f20); this head passes for all nine.Stop near
floatmin: Float32 stop5f-38and Float64 stop1e-307,u' = (1e14, 1e14),dt = 1e-14, terminating callback. The time must equal the stop, the state must be finite, and it must be within 128 eps of the BigFloat value (Vern7 is checked for finiteness only, because its dense output is GPUVern7 Float32 dense output is ~60 ulps off at mid-step for a constant RHS (visible at tstops) #554).Overflowing landing step with a mass matrix:
M = 1024 I, stop1024/floatmax(T), Rosenbrock23/Rodas4/Rodas5P, Float32 and Float64. The state must be within 128 eps of the BigFloat value. On0d2241fRodas4/Rodas5P returnSuccesswithNaN.saveatat step endpoints: all nine methods, Float32 and Float64,u' = (1, 1),dt = 0.5, terminating stop0.125,saveat = [0, 0.0625, 0.125]. The sample at 0 must be exactly 0 and the sample at the stop within 128 eps of0.125. On0d2241fVern9 saves0.12505849(Float32).Overflowing final landing step: the round-12
final_retry.jlcases (Rosenbrock23/Rodas4/Rodas5P; a tiny interval with an identity mass matrix, and a1e30/1e300scaled mass matrix with and without an endpoint callback). NaN right-hand side attf:nan_at_tf.jl, all nine methods, both precisions. Each case accepts exactly two outcomes: an accurate, finite state at exactlytf, or a non-Success return or explicit error. A successful solve at another time, or with a non-finite state, fails. Onfa8c70f, 30 of these checks fail, because the solve returnedSuccesspasttf.The single-tstop and intermediate-callback tests now check the exact state for all nine algorithms. Earlier versions exempted Vern7 because stopping went through its dense output (#554). Steps now end on stops, so Vern7 and Vern9 are exact there (0 ulps; previously 62 and −3).
Before/after, Julia 1.12.7,
include-ing the test file:5e8faeedt<dtminin 63–81 of 135 cases per method0d0f775tfstops, terminating callback, stiffsaveat)6a351d7dtminfinal step snapped: 0.759 instead of 0.0183)25d5a75ceeed4051c325ddtminstop1b901991fde4870d2241f(e8992bf+ merge of master4f726d5)fa8c70ftf(all nine methods, 18)2ead3c4a2e999d/8da09d3(this head;8da09d3changes only a comment)Verification
Review probes (Julia 1.12;
final_retry.jlandnan_at_tf.jlona2e999d, the others on2ead3c4, which differs only in the past-tflanding):extreme.jl(252 cases)mass_floor.jlfinal_retry.jltfwith the expected value (Rosenbrock23); 12 fail explicitly withdt<dtmin(Rodas4/Rodas5P, whose final step overflows)nan_at_tf.jldt<dtmin; none returnsSuccessretry_callback.jlprobes.jl(every round-10/11 probe)fa8c70f; the 43 are the interior dense-output samples discussed belowThe interior-sample failures are the dense-output accuracy of these methods, not the landing logic. On master with no tstops at all, the same interpolants give the same errors at the same Θ: Vern9 Float32 2.1e-5 at Θ = 0.5 vs 2.4e-7 at Θ = 0.125, Rodas5P Float64 3.3e-14 vs 1.8e-14, and Vern7 Float32 6e-5 at either Θ. Landing on a stop moves an interior sample to a different Θ of a shorter step, so the observed error changes. See #554; Vern9 and Rodas5P belong in its scope too.
Runic
--check,typos, andgit diff --checkexit 0 on the changed files.Comparison with master on the round-12 and round-11 reproducers (Julia 1.12)
final_retry.jl(no stop beforetf; the final interval itself overflows for Rodas):4f726d5tiny, Float32 / Float64Success, u = 1.0 (expected 5e-24 / 1e-293)Success, exact valuescaled,scaled_callback(both precisions)Success,NaNSuccess, exact value (callback applied)tiny(both precisions)Success, u ≈ 1.0 (expected 5e-24 / 1e-293)dt<dtminscaled,scaled_callback(both precisions)Success,NaNdt<dtminMaster returns
Successwith a wrong state in all 18 cases. This head is exact in 6 and fails explicitly in 12, and never returnsSuccesswith a wrong state.rodas_extreme.jl(stop at5f-38/1e-307beforetf, terminating callback): identical on both. Rodas4 gives5.0000008f-24/9.999999999999994e-294, and Rodas5P gives5.0000035f-24/1.0000000000000074e-293; all 4 pass. A landing on a stop beforetfkeeps the covering retry, so it lands on the stop by interpolation exactly as master does.Compile time
An earlier revision (
2ead3c4) let a covering retry reach pasttfand land there by interpolation. That made the dense-output call in the accept branch reachable for every problem, including those without tstops, where it is otherwise dead code. On Julia 1.12, compiling the Verner interpolants for ForwardDiff duals inline in the step loop made the CPU group's ForwardDiff test set take 33 to 36 minutes. This head restricts the covering retry to stops, which restores the compile time; the cost is the explicit failure for an overflowing final interval described above.First adaptive solve, CPU backend, Julia 1.12, Lorenz with 6-partial
ForwardDiff.Dualstate and parameters (as intest/gpu_kernel_de/forward_diff.jl):4f726d5fa8c70f2ead3c4a2e999d/8da09d3(this head)On
2ead3c4, the remaining methods took 5 to 16 s each. Making the non-finite error path@noinlinedid not help (221 s for Vern7). A@noinlineinterpolation would pass the mutable integrator to a non-inlined call, which heap-allocates it in device code, and that fails on OpenCL.Full
GROUP=CPUrun, ForwardDiff test set on Julia 1.12: 33 min 29 s on2ead3c4; 3 min 06 s ona2e999d(8da09d3changes only a comment).Risk assessment
dtmincheck, tstop landing and non-finite-step rejection in all nine adaptiveEnsembleGPUKernelsteppers;saveatat step endpoints; the overflow fallback in the internal 2×2/3×3linear_solve. No public API change. Results change for adaptive solves withtstops(steps land on stops) and at rounding level for the final step of every adaptive solve.2ead3c4, fixed by this head (see Compile time); round 14 is pending.Related work and known CI noise
|t|(Float32t0 = 2^26, Float64t0 = 2^55), a rejected final step whoset + dtrounds totfcan still complete without integrating the whole interval.t,t += dtrounding decouples time from the integrated length, with or without tstops (Tsit5dt = 12att = 2^26gives 60 instead of 64, before Fix Float32 endpoint stall in adaptive GPU kernel steppers #544 and on master). It is not addressed here. In the review'st0 + 64survey with a stop att0 + 24, this branch gives 64 for six algorithms. Vern9, Rodas4 and Rodas5P land at 60.3, 65.6 and 63.8 because of Adaptive kernel steppers: t += dt rounding decouples time from integrated length at large t #567; master gives 72 for most algorithms, and gave 61.7/64.0/66.0 for those three before Respect pending tstops in the adaptive next-step clamp #556.dtminstop clusters, a pre-existing issue reproduced before Fix Float32 endpoint stall in adaptive GPU kernel steppers #544, is resolved by landing on every stop with an integrated step: the round-9 review measured its tiny/clustered stiffsaveatmatrix at 27/27 on1b90199(not re-measured on this head, whose only further change is the overflow fallback inlinear_solve).ContinuousCallbackjust before a tstop suppresses that stop'sDiscreteCallback; pre-existing.test/enzyme/cuda_records.jl:9failure and the downgrade-lane failure are pre-existing on master and unrelated; they are tracked in CUDA CI: test/enzyme/cuda_records.jl fails on released CUDACore (Enzyme undef-shadow fill! for non-numeric eltypes) #546 and Raise ModelingToolkitBase floor to 1.72 (fixes downgrade CPU lane) #548.Not verified
GROUP=QAwas not rerun for this revision. It passed here for the previous head (LTS 25/25, 1.12 27/27). The round-4 reviewer could not reproduce that: LTS failed Aqua's persistent-tasks load deadline (as on the parent), and 1.12 hit a hostEMFILEin JET'sFolderMonitor. So QA should be treated as unverified for this PR.🤖 Generated with Claude Code (model: claude-opus-5-5[1m])
https://claude.ai/code/session_01SJfy3ez7u4CqvAerKrT5np