Skip to content

Eliminate dense continuation step allocations on Julia 1.12 - #1061

Draft
ChrisRackauckas-Claude wants to merge 3 commits into
SciML:masterfrom
ChrisRackauckas-Claude:agent/fix-homotopy-step-allocations
Draft

Eliminate dense continuation step allocations on Julia 1.12#1061
ChrisRackauckas-Claude wants to merge 3 commits into
SciML:masterfrom
ChrisRackauckas-Claude:agent/fix-homotopy-step-allocations

Conversation

@ChrisRackauckas-Claude

@ChrisRackauckas-Claude ChrisRackauckas-Claude commented Jul 13, 2026

Copy link
Copy Markdown
Member

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:

  • keep a fixed-shape cached destination for dense AutoForwardDiff Jacobian evaluation
  • return the input directly when Utils.restructure receives the same object
  • run repeated sweep and arclength correctors directly to completion in their reused cache, without constructing a NonlinearSolution on every interior step
  • preserve ordinary solve! results and construct full solutions for anchors, final landings, and reported failures
  • add warmed allocation checks for the Jacobian destination, same-object restructure, and cache-only solve path
  • bump NonlinearSolveBase to 2.35.2

The 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:

  1. ForwardDiff reshapes its already-matrix Jacobian destination.
  2. ArrayInterface.restructure creates a wrapper when the target and source are already the same buffer.
  3. Each interior continuation corrector called ordinary 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: repeated LU{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-NamedTuple construction.

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.

  • exact root allocation regression on Julia 1.12.6: cache-only solve 0 B and returns the same cache; sweep 0.0 B/step; arclength 0.0 B/step
  • standalone NonlinearSolveBase Core: 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/14
  • final combined tree NonlinearSolveBase Core on Julia 1.12.6 and 1.10.11: passed
  • all 39 unchanged homotopy/arclength test files: passed, including allocation, Jacobian, failure, tolerance, effort, retention, and polyalgorithm paths
  • NonlinearSolveBase QA: 18/18
  • ForwardDiff cache reinit regression: 1/1
  • Runic 1.7.0 --check --diff .: passed over both final trees
  • git diff --check: passed

The 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.

Copy link
Copy Markdown
Member Author

Investigation scratchpad / completed plan:

  1. Reproduced clean master (579761dd2) on Julia 1.12.6: sweep 154.4 B/step, arclength 80.0 B/step.
  2. Checked the history boundary: Make AutoSpecialize infer through functor residuals; zero-alloc homotopy stepping #1046 (0343930c2) has the same remaining 154.4/80.0; its parent (6a7687f54) measures 250.4/400.0. Make AutoSpecialize infer through functor residuals; zero-alloc homotopy stepping #1046 removed the intended functor boxing but added the assertion that first exposes the remaining Julia 1.12 headers.
  3. Repeated Make AutoSpecialize infer through functor residuals; zero-alloc homotopy stepping #1046 with LinearSolve 4.3.0 and 5.0.0; both match, excluding the LinearSolve 5/Align downgrade floors with LinearSolve 4.3 #1059 update as the cause.
  4. Used Profile.Allocs to isolate 48 B/iteration in ForwardDiff's destination reshape and 32 B/iteration in plain-vector restructure.
  5. Implemented a local owned Jacobian adapter plus the plain-vector fast path, keeping cache.J a Matrix and relying only on owner/public APIs.
  6. Ran the existing allocation regression across LTS/current/prerelease, both supported LinearSolve lines, nested ForwardDiff checks, native Base and FirstOrder groups, and whole-repo Runic. Current/prerelease and LinearSolve 4.3/5.0 all measure 0.0/0.0 B per step.

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.

Copy link
Copy Markdown
Member Author

CI classification for the two current root failures:

  • Downgrade / Downgrade Tests - Core (job 86867008813)
  • tests / Core (julia lts, ubuntu-latest) (job 86867091501)

Both fail in the pre-existing 23-problem Broyden robustness set, with the same Generalized Rosenbrock | alg #4 residual failure (4.4 ≤ 0.001) tracked by #1057 and the same Brown almost linear | alg #4 unexpected pass tracked by #1056. Neither reaches or implicates the homotopy allocation regression changed by this PR. I have left #1061 unchanged.

Copy link
Copy Markdown
Member Author

Additional CI classification as the matrix completes:

  • Root Core on Julia 1 and Julia pre stops in the same clean-main Broyden robustness test before reaching this PR's allocation test. Julia 1 also reports the separately tracked Brown unexpected pass; Julia pre reports only the Generalized Rosenbrock residual failure.
  • lib/NonlinearSolveBase [QA] reports exactly the two independently audited clean-main findings: the JacobianCache{<:AbstractSciMLOperator}(::Nothing) ambiguity being handled in the stack above Raise SciMLOperators public API floors and resolve cache ambiguity #1060, and the non-public FunctionWrappersWrappers.SingleCacheStorage access whose public-constructor prerequisite is GPU Compatibility for 1D Arrays  #70.

The directly relevant NonlinearSolveBase Core lanes (LTS/current/pre) and its downgrade lane have passed on #1061. The branch remains unchanged.

Copy link
Copy Markdown
Member Author

Terminal CI audit for 7e96b21:

  • The changed homotopy allocation path is green in the owning sublibrary: NonlinearSolveBase Core passed on Julia LTS/current/prerelease and downgrade. NonlinearSolveHomotopyContinuation Core/QA/downgrade also passed. All downstream jobs, documentation, Runic, and spell checks passed.
  • All six red root Core lanes and the root downgrade lane stop earlier in the already-audited Broyden robustness cluster: generalized Rosenbrock residual 4.4 (focused fix Restore dense pinv fallback for inverse Jacobian init #1058) and, in the affected environments, the Brown unexpected pass (Brown bad-Broyden broken test is environment-sensitive #1056). They never implicate this PR's allocation assertions.
  • Base QA is the clean-main JacobianCache ambiguity plus non-public FunctionWrappersWrappers.SingleCacheStorage; Raise SciMLOperators public API floors and resolve cache ambiguity #1060 handles the ambiguity and FunctionWrappersWrappers.jl#70 is the owning-package public-API prerequisite.
  • CUDA has the exact clean-master pair of DimensionMismatch: array could not be broadcast to match destination errors at test/gpu/cuda_tests__item1.jl:34 and :44. Clean master evidence is job 86844917359; this PR's job is 86867091569. A separate clean-main reproduce/bisect/fix audit is queued.

The run is terminal: every failure is independent of the dense homotopy step-allocation change, and no test has been skipped, silenced, or loosened.

@ChrisRackauckas-Claude
ChrisRackauckas-Claude force-pushed the agent/fix-homotopy-step-allocations branch from 7e96b21 to 48d5e1f Compare July 13, 2026 20:16

Copy link
Copy Markdown
Member Author

Rebased the branch onto current master at 41681c5e4 after #1060 merged, then rechecked the final tree locally before updating the remote branch.

Final-tree validation:

  • Julia 1.12 NonlinearSolveBase native test suite passed, including linsolve_identity!! workspace 50/50, linear-solver routing 12/12, operator cache dispatch 1/1, and dense-LU allocation coverage 14/14.
  • Exact root regression: homotopy allocation regression | 11 Pass; measured sweep_bytes_per_step=0.0 and arclength_bytes_per_step=0.0.
  • Whole-repository Runic check passed.
  • git diff --check upstream/master...HEAD passed.

The rebased head is 48d5e1fddc0589b30cec268a4f4018aa5433b5ad; CI has restarted on that commit.

Copy link
Copy Markdown
Member Author

The two early red downgrade jobs on rebased head 48d5e1fdd are the same shared-action resolver failure already classified on #1063, not homotopy test failures:

  • Root downgrade fixes in-tree NonlinearSolveQuasiNewton 1.14.1, which requires SciMLOperators >= 1.24, but the generated downgrade manifest explicitly pins SciMLOperators = 1.22.0.
  • SimpleNonlinearSolve downgrade fixes in-tree NonlinearSolveBase 2.34.3, which also requires >= 1.24, against the same explicit 1.22.0 pin.

Both jobs stop at Unsatisfiable requirements before any test starts. The shared path-source handling is addressed by stacked julia-downgrade-compat drafts #54 and #55. Remaining checks are still running.

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member Author

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>
@ChrisRackauckas-Claude
ChrisRackauckas-Claude force-pushed the agent/fix-homotopy-step-allocations branch from 48d5e1f to ab05132 Compare July 16, 2026 18:01
@ChrisRackauckas-Claude ChrisRackauckas-Claude changed the title Eliminate dense homotopy step allocations on Julia 1.12 Eliminate dense continuation step allocations on Julia 1.12 Jul 16, 2026
@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member Author

Replaced the original adapter-based implementation with ab051325ba1084a597e6fe69ba38ef16bbdb5aa1, based directly on current upstream 60736b5b6.

The investigation separated the two allocation sites with Profile.Allocs, repeated endpoint measurements, a Julia 1.11.9 versus 1.12.0/1.12.5/1.12.6 microbenchmark, and a frozen-graph git bisect run. It then measured each candidate independently before combining them:

  • baseline: sweep 154.4 B/step, arclength 80.0 B/step
  • public full-view Jacobian destination only: 61.76 / 32.0 B/step
  • same-buffer restructure shortcut only: 92.64 / 48.0 B/step
  • combined: 0.0 / 0.0 B/step

This replacement removes the private _JacobianADBuffer type and extra cache field from the previous revision. It adds direct warmed zero-allocation tests, bumps NonlinearSolveBase to 2.35.2, and keeps every existing allocation threshold unchanged.

Publishing process: fetched and verified upstream and the fork at 60736b5b6; re-fetched the PR branch at exactly 48d5e1fddc0589b30cec268a4f4018aa5433b5ad; then replaced only that branch using the explicitly lease-guarded update. No other remote branch was modified.

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member Author

Terminal CI audit for head ab051325ba1084a597e6fe69ba38ef16bbdb5aa1:

None of the failing stacks enters either allocation fast path changed by this PR.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member Author

Apple ARM follow-up published at ffc0113af.

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 < 48 assertions.

The follow-up caches a fixed-shape Jacobian destination once. Its same-shape reshape returns the cached object, so Jacobian evaluation no longer constructs a wrapper on each call. The solver-facing cache.J remains the original Matrix; non-dense and non-ForwardDiff paths are unchanged.

Exact standalone validation on the new head:

  • NonlinearSolveBase Core passed, including allocation fast paths 6/6, workspace 50/50, routing 12/12, operator 1/1, dampening 12/12, and dense LU 14/14
  • the unchanged root allocation regression passed and measured sweep/arclength 0.0/0.0 B per step
  • Runic 1.7.0 and git diff --check passed

The remote branch was re-fetched at ab051325ba1084a597e6fe69ba38ef16bbdb5aa1 immediately before publication and advanced by a normal fast-forward. No force push was used. New Apple ARM CI is the cross-platform verification; I am not claiming a macOS pass from the Linux measurements.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>

Copy link
Copy Markdown
Member Author

Published the cache-only corrector follow-up at 3d568afa132c4f27035622def12de95c30dec40f by normal fast-forward from the audited remote head ffc0113af.

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 solve!, and failure originals still construct full solutions.

Exact standalone verification on this head:

  • cache-only solve: 0 B, same cache object, success retcode
  • sweep/arclength slopes: 0.0/0.0 B per step
  • NonlinearSolveBase Core: passed (allocation 6/6, workspace 50/50, routing 12/12, operator 1/1, dampening 12/12, dense LU 14/14)
  • Runic 1.7 and git diff --check: passed

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.

Copy link
Copy Markdown
Member Author

Corrected the Apple allocation attribution in the PR description after the combined-stack diagnostic evidence:

  • The standalone changes still verify the NonlinearSolve-owned Julia 1.12 x86_64 result: cache-only solve 0 B, sweep/arclength 0.0/0.0 B per step.
  • Apple ARM remained at 772.0/400.0 B per step after those changes; this disproved per-corrector solution construction as the remaining Apple allocator.
  • Stable-macOS profiling isolated the dominant remaining bytes to LinearSolve's AppleAccelerateLUFactorization wrapper/workspace construction.
  • LinearSolve #1099 now passes direct Apple-silicon allocation coverage on Julia 1.10, 1.12, and 1.13.
  • No downstream Apple success is claimed here until that dependency is merged/released and the continuation regression is rerun.

No source, test, threshold, or commit changed in this update.

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