Repository navigation
Make adaptive time updates exact at large |t| (fixes #567; stacked on #561) - #569
Draft
ChrisRackauckas-Claude wants to merge 21 commits into
Draft
ChrisRackauckas-Claude wants to merge 21 commits into
ChrisRackauckas-Claude wants to merge 21 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
ChrisRackauckas-Claude
force-pushed
the
fix/representable-time-steps
branch
from
October 2, 2026 02:36
6c0a04a to
5b3f63c
Compare
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
ChrisRackauckas-Claude
force-pushed
the
fix/representable-time-steps
branch
from
October 2, 2026 04:49
5b3f63c to
ad82e36
Compare
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
ChrisRackauckas-Claude
force-pushed
the
fix/representable-time-steps
branch
from
October 2, 2026 22:55
ad82e36 to
b6d091a
Compare
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
ChrisRackauckas-Claude
force-pushed
the
fix/representable-time-steps
branch
from
October 3, 2026 03:46
b6d091a to
a66dee9
Compare
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
ChrisRackauckas-Claude
force-pushed
the
fix/representable-time-steps
branch
from
October 3, 2026 08:22
a66dee9 to
9c7047d
Compare
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
`t += dt` rounds when `t` is large relative to `dt`, so time advanced by a different amount than the step integrated, and a step shortened by error control could round onto `tf` and complete the solve over un-integrated dynamics (SciML#567). Before the first trial step and after every rejection, round the step down to one whose end time is exactly representable; a step of exactly the distance to the landing target is kept. The time advance then always equals the integrated length, and a step with no representable end time fails the dtmin check. 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
ChrisRackauckas-Claude
force-pushed
the
fix/representable-time-steps
branch
from
October 3, 2026 10:55
9c7047d to
cb6aef9
Compare
`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
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
`apply_callback!` moved `integrator.u` to the event state and then saved the pending `saveat` points with the step's dense output. The Rosenbrock (Rodas4, Rodas5P) and Hermite (Kvaerno3, Kvaerno5) interpolants use `integrator.u` as the step's end state, so every `saveat` point between `tprev` and the event was interpolated against the wrong endpoint. Save those points first, while the step's state is intact. The defect predates this branch. Master's clamp of `dtnew` to the distance to `tf` usually kept steps short enough that no `saveat` point shared a step with an event; 1b90199 removed the clamp, so the step after the bounce at t = 9 in the stiff ContinuousCallback test reaches tf, contains the bounce at t = 15, and the save at t = 9.1 was off by 0.71 (GPURodas4, CUDA). 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 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 Agent-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.
Stacked on #561: it uses #561's step helpers in
integrator_utils.jl. The branch is #561's head8da09d3(which already merges current master4f726d5), then one commit,cb6aef9Make every adaptive time update exact, which fixes #567. The mixed-type tstops fix that this PR first carried is now part of #561 (51c325d).What changed and why
In all nine adaptive
EnsembleGPUKernelsteppers,t += dtrounds when|t|is large relative todt. Time then advances by a different amount than the step integrated (Tsit5 withdt = 12att = 2^26ends at 60 instead of 64). And a step that error control shortened could round ontotf, completing the solve with part of the interval un-integrated (round-7 review of #561). Before the first trial step and after every rejection, the shared helper_representable_stepnow roundsdtdown to the largest step whose end timet + dtis exactly representable. A step of exactly the distance to the landing target (tfor a stop) is kept as is. The time advance therefore always equals the integrated length, and a shortened step can never round up ontotf. If no representable end time exists beforet + dt, i.e. time precision cannot resolve the step, the step becomes 0 and fails the existingdtmincheck with an explicitdt<dtmin. Rounding only ever goes down, so a rejected step is never re-enlarged and the error-control loop cannot cycle.Reproducers
The round-7 review of #561, Vern7, Float32
t0 = 2^26,tf = nextfloat(t0)(tf - t0 = 8),u' = (1/8, -u₂/8),dt = 8, exact endpoint[1, exp(-1)]:bacef85/ #561ceeed40Success,[0.72025377, 0.48662883](accepteddt = 5.76203, storedt = tf)ERROR: dt<dtmin. Error control rejects the one representable step and no shorter step has a representable end timeIts step-level grid (
invariant_grid.jl, 281 stiffness values per algorithm,t0 = 2^26Float32 and2^55Float64, 8-unit final interval) finds a counterexample (t == tfafter a step shorter thantf - t0) for all 18 algorithm/precision pairs on master and on #561, and none on this branch.#567's reproducer (no tstops, Float32
t0 = 2^26,tf = t0 + 64, exact 64):bacef85The
t0 + 64survey with a stop att0 + 24now gives 64 for all nine algorithms (Rodas5P 64.0001, Kvaerno3 63.999996). On #561 Vern9, Rodas4 and Rodas5P gave 60.3, 65.6 and 63.8.Tests
Added to
test/gpu_kernel_de/adaptive_endpoint.jl(CPU group), across all nine adaptive algorithms:t == tfcovered the whole interval. The public reproducer must either fail withdt<dtminor return the exact endpoint.u' = 1over 64 units att0 = 2^26/2^55withdt∈ {12, 20}. The final state must be≈ 64.Before/after, Julia 1.12.7,
include-ing the test file (384 assertions):bacef85ceeed4051c325d)cb6aef9Verification
Runic
--check,typos, andgit diff --checkexit 0 on the changed files.Merged from #561: saveat before a continuous event (e1000a2)
e1000a2merges #561's8adb69a. That commit fixes the CUDA "Callbacks" failure attest/gpu_kernel_de/stiff_ode/gpu_ode_continuous_callbacks.jl:185(GPURodas4,0.71378124 < 7e-4): https://github.com/SciML/DiffEqGPU.jl/actions/runs/37130654908/job/111224921829.apply_callback!movedintegrator.uto the event state before savingsaveatpoints. The Rodas4/Rodas5P/Kvaerno interpolants useintegrator.uas the step's end state, so points before the event were interpolated against the wrong endpoint. #561's1b90199(nodtnewclamp after acceptance) let the step after a bounce run totfpast the next bounce, which exposed it. Cause, bisect and failing-before/passing-after evidence are in #561's body. The only conflict was intest/gpu_kernel_de/adaptive_endpoint.jl, where both branches append testsets; both were kept. On the merge, the endpoint file runs 577/577 (Julia 1.12, CPU). Runic--checkandtyposexit 0.Risk assessment
EnsembleGPUKernelsteppers: every trial step is rounded down to one with an exactly representable end time, before the error-control loop and on rejection. No public API change. Results change at rounding level for all adaptive solves, and visibly at large|t|, which is the intended fix.8adb69a, whiche1000a2merges here, and confirmed the diagnosis by running it. The independent review is pending: Codex is over its usage pace, and Devin and Grok are exhausted. CUDA CI on the new head is also pending.Known CI noise
test/enzyme/cuda_records.jl:9and downgrade-lane failures are pre-existing on master, 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
e1000a2, only the endpoint test file was rerun locally (host overloaded). The full CPU, OpenCL and Enzyme groups are left to CI.GROUP=QA.Closes #567 (once #561 is merged).
🤖 Generated with Claude Code (model: claude-opus-5-5[1m])
https://claude.ai/code/session_01SJfy3ez7u4CqvAerKrT5np