Stop convergence studies retaining the trajectories they are not measuring - #4060
Conversation
|
Overlaps with #4056 — a parallel session reached #4037 independently. Only one of these should merge; I have laid out the differences at #4056 (comment). Short version: #4056 reduces each ensemble after The two are not complementary — they solve the same problem at different points in the pipeline. Whichever is preferred, the other should be closed. |
50a2364 to
a3e7866
Compare
Downstream impact auditThe break is narrow: only the Monte-Carlo (SDE/RODE) method of Who indexes
Every downstream SDE caller already skips the reduction. The two
Registry reverse dependencies declaring DiffEqDevTools as a runtime dep: Caveat. GitHub code search only reaches public, indexed repositories, so private code is out of scope, and So the practical blast radius is zero known users. That does not make the change non-breaking — the |
…uring `test_convergence` kept one full solution object per trajectory per step size. A `RODESolution` carries the solver cache, the noise process, the problem and its interpolation, none of which a convergence study needs — it wants the per-trajectory errors and the endpoints the weak error is formed from. At the trajectory counts the weak-convergence tests use, that gap is the difference between passing and being OOM-killed: `SROCKC2WeakConvergence`, `IIPWeakConvergence` and `OOPWeakConvergence` all died at 15.6 GiB in a 16 GiB cgroup with no test output, which is the "runner lost communication with the server" signature reported in SciML#4036. Trajectories are no longer retained by default. Three things compose: - The `EnsembleProblem` is built with an `output_func` that reduces each trajectory to a `ConvergenceTrajectory` as it is solved, so the full solutions never coexist and the peak is bounded, not just the retention. `ConvergenceTrajectory` stores one-tuples and a `NamedTuple` rather than one-element `Vector`s and the solver's error `Dict`; indexing and key lookup are unchanged, but the `Dict` alone dominated everything else retained once a study runs to millions of trajectories. - `calculate_ensemble_errors` moves inside the solve loop, so each step size's ensemble is summarised and released before the next is solved rather than all of them being held at once. - `_drop_trajectories` strips the summarised ensemble to one representative, since the reduced objects are dead weight once the errors are computed. `retain_solutions = true` restores the old behaviour. The reduction is skipped, and the solutions retained, where it cannot apply: a caller-supplied `EnsembleProblem` owns its own `output_func`, `expected_value` forms the weak error from the trajectory values directly, and `weak_timeseries_errors` and `weak_dense_errors` need the whole timeseries. Drop `uEltype` from the `ConvergenceSimulation` constructor — it was assigned and never used, and was the only thing requiring `solutions[i].u[j]` to be a full solution. Measured on the weak files, 16 GiB cgroup, JULIA_NUM_THREADS=2: weak_srockc2.jl OOM 15.61 GiB @3m29s -> exit 0, 23m41s, 2.81 GiB oop_weak.jl OOM 15.62 GiB @23m50s -> exit 0, 13m08s, 1.32 GiB additive_weak.jl (same group) -> exit 0, 3m22s, 1.04 GiB Order estimates are bit-identical with and without the reduction. With no group needing more than 2.81 GiB, remove the `high-memory` runner pin from all eight groups that carried it. The five StochasticDiffEqWeak groups needed no change — they already reduce through their own `output_func` and peak at 0.71-2.09 GiB, so the label was simply wrong for them. Shipped as a minor bump, 3.1.5 -> 3.2.0. A search of public Julia code found no consumer of the trajectories this stops retaining: `sim.solutions` appears four times in total, two of which are this package's own source and one an archived pre-split mirror, and the only real use is PL1WM.jl in this repo. That one now passes `retain_solutions = true` explicitly at the four call sites whose sims are compared trajectory-by-trajectory, rather than relying on the caller-supplied-`EnsembleProblem` skip. The two downstream SDE callers (DiffEqGPU.jl, SciMLBenchmarks) pass their own `EnsembleProblem` and are unaffected, and `analyticless_test_convergence` is untouched. The reduce-by-default design and `_drop_trajectories` are taken from SciML#4056, which is closed in favour of this. Fixes SciML#4037 Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwLXp5WY1uiqPun7qhwxeU
a3e7866 to
d51fc98
Compare
Downstream is already ready — no companion PRs requiredFollowing up on the audit above, now checking the compat side as well as the code side. Every consumer's bound already admits 3.2.0, because Julia reads
And on the code side, both SDE callers do not merely avoid the change — they already reduce their own trajectories, which is the same thing this PR now does by default: # SciML/DiffEqGPU.jl, test/gpu_kernel_de/gpu_sde_convergence.jl
ensemble_prob = EnsembleProblem(prob; output_func = (sol, ctx) -> (sol.u[end], false))
# SciML/SciMLBenchmarks, script/NonStiffSDE/HighOrderWeakSDEWorkPrecision.jl
ensemble_prob = EnsembleProblem(prob;
output_func = (sol, ctx) -> (h2(sol.u[end][1]), false), prob_func = prob_func)Both then read only So the only edit anywhere in the ecosystem is the one already in this PR: |
Please ignore until reviewed by @ChrisRackauckas.
Fixes #4037. With this, no test group needs a
high-memoryrunner and the label is removed everywhere, which unblocks the eight jobs that were pinned to a label no runner in the fleet carries (#4030).DiffEqDevTools 3.1.5 → 3.2.0. Supersedes #4056, which is closed; its reduce-by-default design and its
drop_trajectoriesstep are both taken up here.The problem
test_convergencekeeps one full solution object per trajectory per step size:A
RODESolutioncarries the solver cache, the noise process, the problem and its interpolation. The convergence study needs none of that — only the per-trajectory errors and the endpoints the weak error is formed from. At the trajectory counts the weak-convergence tests use, that gap is the difference between passing and being OOM-killed:masterin a 16 GiB cgroupSROCKC2WeakConvergencereceived signal: KILLIIPWeakConvergencereceived signal: KILLOOPWeakConvergencereceived signal: KILLAll three die with no test output, which is the "The self-hosted runner lost communication with the server" / no-uploaded-log signature reported in #4036.
(This is what remains after #4050 removed the 36 kB per-solve SROCK tableau. That fix cut the per-trajectory cost 10–14×, and I re-measured these three groups on post-#4050 master to confirm it was not enough on its own — they still SIGKILL at the same calls.)
The change
Trajectories are no longer retained by default. Three things compose:
EnsembleProblemis built with anoutput_functhat turns each trajectory into aConvergenceTrajectory— its errors and endpoint values — as it is solved, so the full solutions never coexist and the peak is bounded, not just the retention. This is what Summarise the Monte-Carlo test_convergence ensembles by default (breaking) #4056 identified as the piece it was missing.calculate_ensemble_errorsmoves inside the solve loop, so each step size's ensemble is reduced and released before the next is solved rather than all of them being held at once._drop_trajectories(from Summarise the Monte-Carlo test_convergence ensembles by default (breaking) #4056) keeps one representative trajectory, since the reduced objects are dead weight after the statistics are computed.ConvergenceTrajectorystores one-tuples and aNamedTuplerather than one-elementVectors and the solver's errorDict.[end]and key lookup work identically, but aDictalone costs several hundred bytes, which dominated everything else once the study runs to millions of trajectories — using it dropped the reduced size a further ~4×.retain_solutions = truerestores the old behaviour. The reduction is skipped where it cannot apply, retaining the solutions rather than failing: a caller-suppliedEnsembleProblemowns its ownoutput_func,expected_valueforms the weak error from the trajectory values directly, andweak_timeseries_errors/weak_dense_errorsneed the whole timeseries.Also removes
uEltype = eltype(solutions[1].u[1])in theConvergenceSimulationconstructor — it was assigned and never used, and it was the only thing forcingsolutions[i].u[j]to be a full solution.Why this is a minor bump and not a major one
ConvergenceSimulation.solutionsis documented as holding all the solutions, andBase.getindex(sim, i, I...)forwards into it, sosim[i, j]forj > 1on the Monte-Carlo method now reaches past the representative. Strictly that is a change to a documented field, and the field documentation is updated to match.It ships as 3.2.0 because the audit below found no consumer of the trajectories anywhere — so rather than push a major bump and its compat churn through 40
Project.tomlfiles for a contract nobody relies on, the one real user is updated directly.Scope, audited rather than assumed:
test_convergencedo not take the keyword and are untouched — includingsim[i].u[2]insymplectic_convergence.jl, which is aDynamicalODEProblem.PL1WM.jl, comparing two algorithms path by path. It would have been covered anyway by the caller-supplied-EnsembleProblemskip, but this PR makes the requirement explicit:retain_solutions = trueat the four call sites whose sims feed those comparisons, so the intent is at the call site rather than implicit. Verified by running the file — all four comparisons pass.test_convergence(SciML/DiffEqGPU.jl,SciML/SciMLBenchmarks) pass their ownEnsembleProblem, which retains solutions, and read only𝒪est. Unaffected, no change needed.analyticless_test_convergenceis untouched, which covers the remaining external users found.Effect
Retained bytes, 20 000 trajectories × 5 step sizes, with the order estimates asserted equal:
save_solutions = truefalseSROCKC2oopSROCK2iipEMoopSimplifiedEMiipThe order estimates are bit-identical — same seed, same trajectories, only the retained representation differs.
Test-side changes
No test call sites change — the reduction is the default. The
high-memorypin is removed from all eight groups that carried it.The five
StochasticDiffEqWeakgroups (WeakConvergence2–6) needed no code change — they already reduce through their ownoutput_funcand were measured at 0.71–2.09 GiB peak, so the label was simply wrong for them. Measured at CI's exact configuration (16 GiB cgroup,JULIA_NUM_THREADS=2, each group's own timeout):WeakConvergence3WeakConvergence2WeakConvergence5WeakConvergence6WeakConvergence4Note the four that time out do so while using under a gigabyte — they are over-specified Monte-Carlo studies (
WeakConvergence4alone asks for 2.0e9 trajectory-solves;WeakConvergence3, which passes, asks for 1.7e8). This PR does not address that; it removes a label they never needed. Cutting those trajectory counts is a separate change with its own statistical-power tradeoff, and it should be made by someone willing to re-tune the order tolerances.Tests
New
lib/DiffEqDevTools/test/retain_solutions_tests.jl(groupCore) asserting that𝒪est,errors,weak_errors,error_meansand the per-trajectory error vectors are unchanged by the reduction; that the trajectory count actually drops and the retained representative keeps the endpoints; that the reduced form is ≥20× smaller; and that each case where the reduction cannot apply retains the solutions instead — including the reducing-EnsembleProblem+expected_valueshape thatPL1WM.jldepends on. 35 assertions, all passing.Verification
Each file run under a 16 GiB cgroup (
systemd-run --scope -p MemoryMax=16G -p MemorySwapMax=0) atJULIA_NUM_THREADS=2, matching the 8 vCPU / 16 GB pool and the self-hostednum-threads: autodefault:masterweak_srockc2.jloop_weak.jladditive_weak.jloop_weak)iip_weak.jlNew regression test:
statistics are unchanged18/18,trajectories are dropped by default11/11,reduction is skipped where it cannot apply6/6.iip_weak.jlreaches a pre-existing failureWith the memory fixed,
iip_weak.jlgets all the way to theSRIstudy at line 186 for the first time, and that assertion fails:This is not caused by this PR. Two checks:
retain_solutions = true, same seed, at 1 and 2 threads — gives bit-identical order estimates in all six combinations. The reduction does not perturb the trajectories.0.5533865655544976) on a re-run.It was simply unreachable before: the file OOM-died at the
SROCK2call on line 71, 115 lines earlier. For context on the estimator, over 12 independent seeds:< 0.5so the method's weak order really is ≈2 and the in-file RNG state is a ~3σ draw. Fixing it means either raising that study's trajectory count (variance scales as
1/sqrt(n)) or re-examining the tolerance — a judgement about someone else's test that I have deliberately kept out of this PR rather than quietly tuning a number to get green. Filed separately.Note on
Pkg.test()Pkg.test()cannot currently resolve anywhere in the monorepo —lib/ImplicitDiscreteSolve/Project.tomlrequiresNonlinearSolveBase = "2.40"but only 2.38.0 is registered (#4054, bisected to #4042). That is unrelated to this PR and predates it; it is also what the redDocumentationjob on master is failing on. The runs above therefore go through a pre-resolved environment rather thanPkg.test(). Once #4054 is fixed the groups should be re-run through the normal harness.