Repository navigation
sens: read a row where the run stands when CVODES returns tout from a step that does not span it (#1301) - #1343
Conversation
… step that does not span it (lanl#1301) After the sensitivity RHS refused a state (GH lanl#395's recoverable return), CVODES could return tout from a CV_NORMAL call whose last step does not span it: its CVodeGetDky refused ("Illegal value for t"), which CVode ignores, so `y` was left as the call before had it and the row took that state at the time asked for. The per-row CVodeGetSens then failed too, and the row's sensitivities were left at zero, with no error: 24 rows of 101 on MODEL2001200002 (Mpekris 2017). Its second message, a solver "at t = 2.6e-312", is SUNDIALS 7.2.1's CVodeGetSensDky1 printing MSGCV_BAD_T with no arguments, not a stale handle. Where tout lies outside the last step, the row is read at tn, as a sensitivity run records its rows, and counted in solver_stats["n_rows_outside_last_step"]. The per-row read is CVodeGetSensDky at the row's own time (CVodeGetSens reads at the time CVODES last returned, which there is the one asked for), and a row whose sensitivities cannot be read raises instead of reading 0. The same model on m_T8, which failed with CV_ILL_INPUT at t = 91 and finished only on the GH lanl#176 FD retry, runs on its own Jacobian.
…tep does not span (lanl#1301) The rows the issue saw come from a SUNDIALS defect, not from the output loop. In cvStep's staggered sensitivity corrector CVODES evaluates f at the converged state, and a recoverable failure there goes back round the step loop with neither cvRestore nor a smaller step. The next attempt predicts again from the advanced tn, so the step covers 2h and its first h is crossed by the uncorrected predictor, which neither the error test nor the root search sees. CV_NORMAL then returned tout from a last step that does not span it. Reading those rows where the run stood left everything else as wrong as before: on MPEKRIS the state was 9.0e-3 off a tight plain run and the sensitivities 5.2e-3 / 1.4e-2 off central differences. On a 10,001-point grid, 2,870 rows shared a time and stood up to 0.094 past the time asked for. The bug is still in SUNDIALS main. - third_party/sundials/carry_cvodes_staggered_restore.cmake applies the fix as the FetchContent PATCH_COMMAND: the failure goes through cvHandleNFlag as RHSFUNC_RECVR, like every other recoverable failure in cvStep. The configure fails if the fetched source lacks the marker, and VENDOR.json lists the carry, which vendor_sundials.py now keeps. BNGSIM_SUNDIALS_STAGGERED_RESTORE=0 takes the upstream path, for an A/B only. On MPEKRIS: state 3.4e-8, sensitivities 3.1e-6 / 2.3e-6, every row on the grid, 1,577 steps instead of 8,283. - A CV_NORMAL return whose last step does not span tout is refused instead of being read where the run stands. The check now also covers the CV_TOO_MUCH_WORK retry's return, and it mirrors CVodeGetDky's own window (the hu < 0 fuzz was inverted). The helper moved out of retry_while_advancing's doc comment. A system SUNDIALS build warns at configure time. - n_rows_outside_last_step is dropped: a returned run would always read 0. - The per-row CVodeGetSensDky read and its refusal stay. - The tests compare against independent references (a tight plain run, central differences) and the grid itself, and use the hatch to show that the model still reaches the defect. All 5 fail on main and on the PR as submitted.
…the marker (lanl#1301) cvStep resets ncf to 0 just above the refused f call on every attempt, so counted there a refusal that a smaller step does not cure never reached maxncf: on MODEL2001140002 the step shrank to h ~ 1e-16 inside one CVode call and the run ignored its own timeout (as it does on main, through the upstream path). ncfS is kept across a step's attempts. The run now fails with the GH lanl#54 collapsed-step message, and the wall-clock budget ends the FD retry. The marker is now "r2": the patch step is idempotent on the marker, so a build tree holding the first revision is refused at configure with the instruction to delete its _deps/sundials-* directories. Swept over the 1,323 rr_parity ODE models with three parameters each: three reach the upstream path (MODEL2001200002, MODEL2001160002, MODEL2001140002).
IDAS has the same pattern, reported as llnl/sundials#1074.
…n filter (lanl#1301) From an independent review of the final head. - A row recorded before the first step (a start time named twice: sample_times=[2, 2, 3]) read CVODES's interpolation where it has no step size yet, which divides 0 by 0: the per-row read refused the nan, and main, reading at another time, recorded dA/dA0 = 0 instead of 1 with no error. With no step taken in the run and no restart, the row now records the seed row 0 recorded. After a restart the read is left to CVODES, which refuses the nan as before. - The finite-difference test picked columns at 1e-8 of the largest, where a column the parameter does not reach (Treg for sigma_nk) is the difference's own noise, within a factor of 10 of the cut and moving with libm. The cut is 1e-6 now; the PR as submitted still fails it (5.2e-3). - The refusal also compares t_ret with t_target, the retry's tout. All 6 tests fail on the PR as submitted and pass here; the suite passes (17,753 passed, 167 skipped).
|
Thanks for the trace: "a last step from 44.0099" was the thread that led to the cause. I pushed review commits on top of a merge of main (7eab654, 7649533, e9e1903, 8e334e5). They replace the row move, because the root cause is in SUNDIALS, not in the output loop. The causeIn What the move leftReading a row where the run stands fixed the zeros, but the values stayed wrong. Against independent references (a plain run at rtol 1e-12, and central differences of plain runs):
On a 10,001-point grid the move put 2,870 rows on repeated times, up to 0.094 past the time each asked for: once the run stood past an output, the next outputs were recorded where it stood. The "3e-3 from t ≈ 41" in the body is this defect, not #1298. The spline check compared each row with neighbours that were just as wrong. The review commits
Blast radius. A probe build counted the upstream path over all 1,323
Suites: python/tests + parity_checks/tests, 17,753 passed, 167 skipped, 0 failed; C++ ctest 6/6 from a fresh fetch, which ran the patch step on a clean download. |
Fixes #1301.
What was wrong
The issue's two messages are one defect, and the second is not a stale handle.
1. CVODES returned an output time from a step that does not span it. After the sensitivity RHS refuses a state (GH #395's recoverable return), a
CV_NORMALcall can returntoutfrom a last step that starts after it. Traced on the issue's MPEKRIS fixture, at row 44: one call from tn = 43.984 took 3 steps, ended at 44.0311 with a last step from 44.0099, and returnedflag 0, t_ret = 44. CVode then interpolates totoutwith(void) CVodeGetDky(...), ignoring its result. So the refusal ("Illegal value for t") leftyas the call before had it, and the row took that state, at the time asked for.2. The row's sensitivities were left at zero. The per-row
CVodeGetSensinterpolates at CVODES's last return time, which is that sametout, so it failed too. The output loop had noelse, so 24 rows of 101 came back with all-zero sensitivities, and no error. On the plain-run comparison, those are rows 44, 47, 49–52, 56, … 98.3. The "solver at t = 2.6e-312" is a SUNDIALS message bug. In SUNDIALS 7.2.1,
CVodeGetSensDky1callscvProcessError(…, MSGCV_BAD_T), a format with three%gfields, with no arguments.CVodeGetDkypassest, tn - hu, tn. The denormals are whatever was in the registers.Either recovery switch removes it (
BNGSIM_SENS_NONFINITE_RECOVER=0,BNGSIM_SENS_CLAMP_NUMERIC_ZERO=0: 0 bad reads instead of 48), which confirms the trigger.The fix (
cvode_simulator.cpp)CV_NORMALreturn oft_ret == toutin a sensitivity run, check whethertoutis inside the last step[tn − hu, tn], with the fuzzCVodeGetDkyitself allows (inside_last_step).toutis outside, take the state attnitself, which is always readable, and sett_ret = tn. A sensitivity run already records each row at the time it stands at ("A run with sensitivity columns keeps the time it stands at"), so the row is labelled honestly. Each such row is counted insolver_stats["n_rows_outside_last_step"].CVodeGetSensDky(t_ret)instead ofCVodeGetSens, which reads at CVODES's stale return time.CVODES still prints its own
CVodeGetDkymessage from insideCVode, once per such row. bngsim cannot stop it attempting the interpolation, only stop trusting it. TheCVodeGetSensDky1messages are gone.Verified
MPEKRIS (
sensitivity_params=["sigma_nk", "g_reg"], rtol 1e-9):n_rows_outside_last_stepCVode)The rows are right, not just non-zero. Each moved row's deviation from a spline through its neighbours is, for sensitivities, 7.2e-4 against 2.0e-6 median and 1.4e-3 max elsewhere; for the state, 1.8e-4 against a 1.4e-3 max. That is within the run's own range, where main's were zeros. By construction a moved row's state is CVODES's solution at
tn; on main it was the previous call's state at 43.98, labelled 44.MODEL2001200002 itself:
m_T8, rtol 1e-6: on main it fails withCV_ILL_INPUTat t = 91 and finishes only on the GH Four Linux-only test failures, found by #169's first whole-suite run: the GH #176 FD fallback dies at a second crossing, and a steady-state fixture is exactly singular under reference LAPACK #176 finite-difference retry. Here it runs on its own Jacobian, with no retry and 2 rows read where it stood.mu, k, k_th: the 28 such rows are now read. All its sensitivities are zero, correctly: none of the three appears in any rate law.Not this PR's: the sensitivity run's state differs from a plain run by about 3e-3 from t ≈ 41 on, including rows with no bad read. That is the clamp-retry trajectory itself, #1298's domain, which #1300 addresses.
Tests (
python/tests/test_sens_rows_outside_last_step.py)On MPEKRIS, copied from #1300's test:
n_rows_outside_last_step > 0). sens: evaluate the sensitivity RHS at the value RHS's clamped state (#1298) #1300 stops this model reaching it, so that would make the tests below pass for nothing; this test fails instead and asks for another model;On main: 3 fail, 1 passes (the in-order times).
Mutations, each rebuilt and caught:
CVodeGetSens: the same.Suites:
python/testsandparity_checks/testsgive 17,326 passed, 172 skipped, 1 xfailed, every event and sensitivity test included. clang-format 17.0.6 and ruff 0.14.0 are clean. The core stub gains the newSolverStatsfield.Not run: a sanitizer build. The issue's memory-safety question is answered by reading SUNDIALS's source: point 3 above. No second
cvode_memis involved.