Skip to content

Fix ExplicitTaylor fsallast aliasing to state buffer - #3975

Open
singhharsh1708 wants to merge 1 commit into
SciML:masterfrom
singhharsh1708:fix-explicittaylor-fsallast-aliasing
Open

Fix ExplicitTaylor fsallast aliasing to state buffer#3975
singhharsh1708 wants to merge 1 commit into
SciML:masterfrom
singhharsh1708:fix-explicittaylor-fsallast-aliasing

Conversation

@singhharsh1708

@singhharsh1708 singhharsh1708 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

get_fsalfirstlast for ExplicitTaylorCache and ExplicitTaylorAdaptiveOrderCache returned (cache.u, cache.u), aliasing the FSAL buffer to the state buffer itself (cache.u === integrator.u).

ExplicitTaylor does not override isfsal, so it inherits isfsal(alg) == true and the generic auto-dt heuristic (ode_determine_initdt) takes its FSAL branch, writing an extra f evaluation into integrator.fsallast. With the aliasing, that call is literally f(u, u, p, t), which corrupts u mid-evaluation for any in-place RHS that reads from u after writing to du — a normal, valid pattern.

Effects on current master:

  • integrator.u is silently wrong immediately after init(...), before a single step is taken. With u0 = [1.0, 2.0] and an RHS that writes du then reads u, integrator.u comes back as [-16.0, 16.0].
  • The initial-step estimate sees a NaN, emits a spurious "First function call produced NaNs" warning, and falls back to tdir * dtmin. On prob_ode_pleiades at abstol = reltol = 1e-10 the first step size is 1.0e-323, and the controller spends hundreds of steps climbing out.

Measured on prob_ode_pleiades with ExplicitTaylor(order = Val(6)):

setting before after
default tolerances Success, 386 steps + NaN warning Success, 59 steps
abstol = reltol = 1e-10 Success, 905 steps Success, 587 steps

Solutions agree to about six significant figures on this chaotic problem.

Fix: return independent buffers of the du type rather than the state, matching the core default get_fsalfirstlast(cache::OrdinaryDiffEqConstantCache, u) = (zero(u), zero(u)). These were the only two caches in the repo with this aliasing; the corresponding ConstantCaches already used the core default.

Known follow-up, not fixed here

ExplicitTaylorAdaptiveOrder() had the same aliasing. Before this fix its auto-dt collapsed to dtmin and the solve died loudly with retcode = MaxIters after 1,000,001 steps. After the fix that failure is gone, but a separate pre-existing bug in its per-step order/error controller becomes visible: on Pleiades it returns retcode = Success after 3 steps with sol.u[end] bit-identical to u0, i.e. the state never moved. That is a silent wrong answer where there used to be a loud failure. It is filed as #3976 rather than fixed here, and it is worth weighing that trade-off when reviewing.

Tests

fsallast must not alias the state buffer asserts directly that integrator.fsallast !== integrator.u and that init(...) leaves u unmodified, for both affected caches, using a two-line RHS that reads u after writing du. It fails four times on master and passes here, in about two seconds.

Auto-dt on Pleiades does not collapse to dtmin keeps the end-to-end check with a bound that actually discriminates (length(sol.t) < 700; master takes 905 steps, this branch 587).

Existing OrdinaryDiffEqTaylorSeries Core group passes unchanged. Runic --check clean on both changed files.

AI Disclosure

Claude assisted with this work.

get_fsalfirstlast returned (cache.u, cache.u) for ExplicitTaylorCache and
ExplicitTaylorAdaptiveOrderCache, aliasing the FSAL buffer to the state
buffer itself. ExplicitTaylor does not override isfsal, so the auto-dt
heuristic takes its FSAL branch and writes an extra f evaluation into
fsallast; with the aliasing that call is f(u, u, p, t), which corrupts the
state mid-evaluation for any in-place RHS that reads u after writing du.

The corruption lands before the first step: integrator.u is already wrong
after init, and the resulting NaN in the initial-step estimate collapses
dt0 to dtmin, so the controller burns hundreds of steps recovering. On
Pleiades at 1e-10 that is 905 steps versus 587 after the fix.

Return independent buffers of the du type instead, matching the core
OrdinaryDiffEqConstantCache default.
@singhharsh1708
singhharsh1708 force-pushed the fix-explicittaylor-fsallast-aliasing branch from eb143a7 to 651ad20 Compare August 1, 2026 22:10
Comment on lines +84 to +91
# FSAL currently not used. `cache.u` must NOT be reused here: the generic
# auto-dt heuristic (`ode_determine_initdt`) writes an extra `f` evaluation
# into whichever buffer `fsallast` points to, so aliasing it to the state
# itself makes that call `f(u, u, p, t)` and can corrupt `u` mid-evaluation
# for RHS functions that read from `u` after writing to `du`, which corrupts the
# state before the first step. Use independent buffers of the `du` type, matching
# the core `OrdinaryDiffEqConstantCache` default.
get_fsalfirstlast(cache::ExplicitTaylorCache, u) = (zero(u), zero(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.

where is this actually called? This function shouldn't allocate.

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