Eliminate dense continuation step allocations on Julia 1.12 - #1061
ChrisRackauckas-Claude wants to merge 3 commits into
Conversation
|
Investigation scratchpad / completed plan:
The #1057 prerelease Core job independently corroborates the original 154.4/80.0 signature. That branch and the separate clean-master Broyden failures were left untouched. |
|
CI classification for the two current root failures:
Both fail in the pre-existing 23-problem Broyden robustness set, with the same |
|
Additional CI classification as the matrix completes:
The directly relevant NonlinearSolveBase Core lanes (LTS/current/pre) and its downgrade lane have passed on #1061. The branch remains unchanged. |
|
Terminal CI audit for
The run is terminal: every failure is independent of the dense homotopy step-allocation change, and no test has been skipped, silenced, or loosened. |
7e96b21 to
48d5e1f
Compare
|
Rebased the branch onto current Final-tree validation:
The rebased head is |
|
The two early red downgrade jobs on rebased head
Both jobs stop at |
|
Post-rebase functional CI classification from the exact logs:
The allocation assertions are no longer the failing point on this branch, and no unrelated public-API allowlist or Broyden test is changed here. |
Avoid temporary reshape wrappers for dense AutoForwardDiff Jacobians and same-buffer restructure calls, with direct warmed allocation regressions. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
48d5e1f to
ab05132
Compare
|
Replaced the original adapter-based implementation with The investigation separated the two allocation sites with
This replacement removes the private Publishing process: fetched and verified upstream and the fork at |
|
Terminal CI audit for head
None of the failing stacks enters either allocation fast path changed by this PR. |
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
|
Apple ARM follow-up published at The previous full-view destination removed the ForwardDiff reshape allocation on local x86_64, but the combined stack showed that it still depended on compiler scalar replacement: macOS current measured 957.28/496.0 B per sweep/arclength step, and macOS prerelease measured 772.0/400.0 B, against the unchanged The follow-up caches a fixed-shape Jacobian destination once. Its same-shape Exact standalone validation on the new head:
The remote branch was re-fetched at |
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
|
Published the cache-only corrector follow-up at Why this follow-up is needed: the combined Apple prerelease job still measured exactly 772.0 B/step for sweep and 400.0 B/step for arclength after the cached Jacobian destination change. Profiling isolated the remaining per-step construction of complete nonlinear solution objects. Interior correctors now finish directly in the reused cache and read state/residual/retcode/stats from it; anchors, final landings, ordinary Exact standalone verification on this head:
The combined-tree validation additionally passed all 39 continuation files, Base QA 18/18, Julia 1.10 Base Core, and the ForwardDiff cache reinit regression. Fresh Apple CI is still required; I am not claiming the cross-architecture result from Linux measurements. |
|
Corrected the Apple allocation attribution in the PR description after the combined-stack diagnostic evidence:
No source, test, threshold, or commit changed in this update. |
Superseded — rebase onto current
|
| This PR's commit | Superseded by |
|---|---|
ab051325b — FixedShapeJacobianDestination / J_destination, restructure same-object fast path |
81450a1c8 |
ffc0113af — cached ForwardDiff Jacobian destinations |
81450a1c8 |
3d568afa1 — _solve_without_solution! / _solve_result_* |
81450a1c8 |
Confirmed with git log -S on each distinctive symbol (FixedShapeJacobianDestination, J_destination, y === x ? x : ArrayInterface.restructure, _solve_without_solution!) — every one first appears in 81450a1c8 and nowhere else.
Master is strictly ahead, not merely equivalent
Resolving the conflicts to master's side loses nothing, because master's version of each is a superset:
_solve_without_solution!now guards onapplicable(InternalAPI.step!, cache) && hasfield(…, :termination_cache) && hasfield(…, :trace)and returns a materialized solution when aBoundedWrapperis present, rather than unconditionally returning the cache. This PR's version would have returned a bare cache for bounded problems._has_bounded_wrapper(cache)replaces this PR's inlinehasfield(typeof(cache.prob), :f) && hasfield(typeof(cache.prob.f), :f) && cache.prob.f.f isa BoundedWrapperchain.homotopy_sweep.jlgained_homotopy_corrector!, contraction-rejection handling (_strict_contraction_rejection,_rejected_step_size,_accepted_step_size) and_store_sweep_state!on top of the_solve_result_*calls introduced here.solve.jlgained_run_cache_to_completion!with astep_observerhook and a publicsolve_cache!entry point.
No test assertion was lost
$ diff <(git show <pr-head>:lib/NonlinearSolveBase/test/allocation_fastpaths.jl) \
<(git show origin/master:lib/NonlinearSolveBase/test/allocation_fastpaths.jl)
# (identical)
$ comm -23 <(git show <pr-head>:test/Core/homotopy_alloc_tests__item1.jl | grep -o '@test.*' | sort -u) \
<(git show origin/master:test/Core/homotopy_alloc_tests__item1.jl | grep -o '@test.*' | sort -u)
# (empty — every assertion in this PR exists on master)
Master additionally carries a cached_sweep_allocations regression that this PR did not have.
The goal verifies on current master
The blocking dependency named in the PR description — SciML/LinearSolve.jl#1099, "Reuse direct BLAS LU workspaces during refactorization" — merged 2026-07-23 and ships in LinearSolve v5.6.0. Reran both allocation suites against unmodified master with that dependency resolved:
$ julia +1.12 --project=. -e 'include("test/Core/homotopy_alloc_tests__item1.jl")'
LinearSolve version: 5.6.0
Julia: 1.12.6 x86_64-linux-gnu
Test Summary: | Pass Total Time
homotopy_alloc_tests__item1 | 17 17 14.8s
$ julia +1.12 --project=. -e 'include("lib/NonlinearSolveBase/test/allocation_fastpaths.jl")'
Test Summary: | Pass Total Time
allocation_fastpaths | 6 6 2.8s
That covers the cache-only _solve_without_solution! path (0 B, returns the same cache), the fixed-shape Jacobian destination (0 B, reshape is identity), the same-object restructure fast path (0 B), the cached-sweep regression (0 B), and the end-to-end per-step slopes for both HomotopySweep and ArcLengthContinuation (< 48 B/step on the nbig = 50 problem).
Not verified
- Apple silicon. The open cross-platform question in the PR description (772.0/400.0 B per sweep/arclength step on ARM) is untested here — this run is x86_64 Linux only. LinearSolve#1099 has since merged, so it is worth re-measuring on ARM, but that is independent of this PR and needs Apple hardware.
- Full
Coregroup. Only the two allocation files above were run, not the whole suite.master's own CI is green apart fromDowngrade / CoreandOrdinaryDiffEq.jl/Interface, both unrelated to this work.
The red checks visible on this PR are from its stale July base and do not reflect master.
Recommend closing as superseded — no code change is needed. Leaving open for @ChrisRackauckas to confirm.
Ignore this PR until reviewed by @ChrisRackauckas.
Summary
Remove the NonlinearSolve-owned dense continuation step allocations observed on Julia 1.12 x86_64 without weakening the existing allocation regression:
AutoForwardDiffJacobian evaluationUtils.restructurereceives the same objectNonlinearSolutionon every interior stepsolve!results and construct full solutions for anchors, final landings, and reported failuresNonlinearSolveBaseto 2.35.2The new solver helpers are private. No public API, threshold, tolerance, skip, broken-test designation, or failure behavior changes.
Root cause and platform boundary
The standalone NonlinearSolve changes remove three independently observed sources:
ArrayInterface.restructurecreates a wrapper when the target and source are already the same buffer.solve!(cache), constructing a complete solution although the driver only needs cache state, residual, retcode, and stats.Removing those sources produces the verified zero-allocation Julia 1.12 x86_64 result below. It does not, by itself, establish the Apple result.
Fresh combined-stack Apple ARM runs after all three changes still measured exactly 772.0/400.0 B per sweep/arclength step on prerelease Julia. A subsequent stable-macOS allocation profile measured 957.28/496.0 B per step and isolated the dominant remaining allocator upstream in LinearSolve's
AppleAccelerateLUFactorization: repeatedLU{Float64, Matrix, Vector{Int32}}construction contributed 25,984 bytes / 812 allocations in sweep and 11,648 bytes / 364 allocations in arclength, alongside tuple,Ref, range, and keyword-NamedTupleconstruction.Focused draft LinearSolve #1099 caches the factor, pivot, and LAPACK-status workspaces. Its direct Apple-silicon tests pass on Julia 1.10, 1.12, and 1.13 without changing the allocation ceiling. This standalone PR still makes no downstream cross-platform allocation claim until the LinearSolve PR is merged/released and the continuation regression is rerun against that dependency.
Validation
Standalone head:
3d568afa132c4f27035622def12de95c30dec40f.0B and returns the same cache; sweep0.0B/step; arclength0.0B/stepNonlinearSolveBaseCore: passed, including allocation fast paths 6/6, workspace 50/50, routing 12/12, operator dispatch 1/1, dampening 12/12, and dense-LU allocations 14/14NonlinearSolveBaseCore on Julia 1.12.6 and 1.10.11: passedNonlinearSolveBaseQA: 18/18--check --diff .: passed over both final treesgit diff --check: passedThe canonical root Core run still stops earlier at the separately audited clean-base Brown/Broyden unexpected-pass failure; no test was skipped or altered to continue past it.