Skip to content

Avoid RSwM3 reject_step! temporaries on the in-place path - #341

Merged
ChrisRackauckas merged 2 commits into
SciML:masterfrom
ChrisRackauckas-Claude:reject-step-inplace
Oct 16, 2026
Merged

ChrisRackauckas merged 2 commits into
SciML:masterfrom
ChrisRackauckas-Claude:reject-step-inplace

Conversation

@ChrisRackauckas-Claude

@ChrisRackauckas-Claude ChrisRackauckas-Claude commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

Summary

On the RSwM3 nonempty-S₂ rejection path, in-place reject_step! was building L₂ - W.dWtilde / L₃ - W.dZtilde temporaries before copyat_or_push!, which showed up as per-step allocations in warmed adaptive SDE solves (SRIW1 ~44.5 B/step). The in-place branch now mutates the just-popped S₂ slot (@.. L₂ -= W.dWtilde, and likewise for L₃ when Z is present) and then copies that entry into S₁. That is safe because pop! only decrements the stack cursor, copyat_or_push! copies into S₁ before S₂ reuses the slot, and the empty-S₂ / NoiseGrid / NoiseFunction paths never enter this branch.

Regression / numbers

Allocation regression (test/RSwM_correctness_test.jl, warmed nonempty-S₂ reject_step!, length-4 W+Z):

  • Unfixed: total == 19200 → @test total == 0 FAIL (before.txt)
  • Fixed: total == 0 → PASS (after.txt)

@benchmark minima (JULIA_NUM_THREADS=1 taskset -c 0, reject-only with setup=accept_step!, length-4 W+Z):

min time memory allocs
master 240 ns 192 B 4
this PR 170 ns 0 B 0

SDE-level warmed adaptive iip (from the C2 audit patch on the same machine family; 35200 accepts):

alg before after
SRIW1 44.5 B/step 0.0 B/step
LambaEM 1.6 B/step 0.0 B/step
RKMil 1.0 B/step 0.0 B/step
SOSRI 8.1 B/step 0.2 B/step

Bitwise identical to baseline over 100 seeds for LambaEM / RKMil / RKMilCommute / SOSRI / SRIW1 (audit logs).

Test-group tails

Core (Pkg.test, default All/Core):

Test Summary:               | Pass  Total  Time
Core/wiener_backend_test.jl |    3      3  0.2s
     Testing DiffEqNoiseProcess tests passed

(Core/RSwM_correctness_test.jl: 1282718 pass, including the new allocation test.)

QA (GROUP=QA):

Test Summary: | Pass  Total     Time
QA/qa.jl      |   21     21  2m09.7s
     Testing DiffEqNoiseProcess tests passed

Also: Runic + typos on the touched files; Julia 1.10.12 allocation regression OK in a scratch env.

Not verified

  • Full SDE re-benchmark of SRIW1/SOSRI/etc. against StochasticDiffEq tip on this checkout (cited SDE B/step numbers are from the prior C2 audit patch on amdci2).
  • Out-of-place path still allocates the L₂ - W.dWtilde temporaries (intentional; not changed).
  • Empty-S₂ / RSwM1 / RSwM2 rejection branches still form W.dW - W.dWtilde with out-of-place arithmetic even when isinplace(W).
  • GPU / non-Array noise prototypes.

Reviewer pushback

  • Confirm the aliasing story: mutating the popped S₂ buffer is only correct because copyat_or_push!(S₁, …) runs before S₂ reclaims that slot with dWtilde.
  • Whether the empty-S₂ in-place branch should get the same treatment in a follow-up.
  • Whether @test total == 0 is too brittle under GC / CI noise (it was exact on amdci2 for 100 warmed rejects).

Please ignore until reviewed by @ChrisRackauckas.

Risk assessment

Independent review: pending

🤖 Generated with Cursor Agent 2026.10.01-14929f9 (model: unknown (Cursor auto)), transcript /home/crackauc/sandbox/goals/performance/jobs/sde/pr-dnp-reject/log.txt on amdci2; orchestrated by Claude Code (claude-opus-5-5[1m]) https://claude.ai/code/session_01LPHREnnonfLg1VcE1EJovv

Made with Cursor

Independent review: Devin Fusion (fusion-claude-opus-5-5-high-sidekick-swe-2-medium) rated it low, verdict MERGE: #341 (comment)

Risk assessment

  • Risk: low
  • Blast radius: In-place NoiseProcess RSwM3 rejection with a nonempty secondary stack. Changes allocation behavior while preserving the bridge calculation and stored increments; no public API, dependency, or out-of-place behavior change.
  • Evidence: Reviewed the diff, complete rejection path, stack construction, and ResettableStacks pop/copy semantics. All 19 reported checks pass. Read Core CI logs: RSwM_correctness_test.jl passes 1,282,718 assertions on stable, LTS, and x86; QA and downstream checks also pass. No local Julia tests or independent performance measurements were run.
  • Independent review: codex-cli gpt-6.1-sol rated it low, high confidence: the popped buffer is consumed before mutation and copied to independent storage before its stack slot is reused, with correctness and allocation coverage in CI.
  • Merge: auto-merge candidate — narrowly scoped internal allocation optimization with green correctness, QA, documentation, downgrade, and downstream checks.

Independent risk assessment by codex-cli (gpt-6.1-sol), reviewed head 5a6017422c; dispatched by the fleet master, Claude Code (claude-opus-5-5), https://claude.ai/code/session_01VZjwf7Ux9YtTzwHGHMqnsq

ChrisRackauckas and others added 2 commits October 8, 2026 23:39
Form the S₁ stack entry by mutating the just-popped S₂ slot in place so
warmed adaptive SDE steps no longer allocate L₂ - dWtilde / L₃ - dZtilde
arrays on every nonempty-S₂ rejection.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Cursor Agent <noreply@cursor.com>
Agent-Harness: Cursor Agent 2026.10.01-14929f9
Agent-Model: unknown (Cursor auto)
Agent-Session: local session, transcript /home/crackauc/sandbox/goals/performance/jobs/sde/pr-dnp-reject/log.txt on amdci2
Measure reject_step! through a local function so @allocated sees a
compiled call site under SciMLTesting's @safetestset on Julia 1.10.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Cursor Agent <noreply@cursor.com>
Agent-Harness: Cursor Agent CLI
Agent-Model: auto
@ChrisRackauckas

Copy link
Copy Markdown
Member

🤖 Automated comment from an AI agent running as @ChrisRackauckas — not written or reviewed by Chris.

Independent review (Devin CLI 3000.11.3, model fusion-claude-opus-5-5-high-sidekick-swe-2-medium): MERGE, risk low.

Full review

VERDICT: MERGE
RISK: low

Blocking findings

None.

Non-blocking findings

  1. The SDE-level B/step table in the PR body comes from a different patch. The body says the SRIW1/LambaEM/RKMil/SOSRI "before/after" numbers come from "the C2 audit patch on amdci2". I measured with this PR alone (confirmed by running, scratch/sdealloc.jl, StochasticDiffEq 7.2.0, iip diagonal 4-dim, RSwM3, 20 warmed solves per algorithm). Reverted-src vs PR:

    • LambaEM goes from 274.74 to 270.37 B/accept over 28,402 rejects, which works out to about 192 B per reject.
    • RKMil goes from 255.44 to 255.04 B/accept.
    • SRIW1 and SOSRI had 0 rejects on that problem, so nothing changed.

    The ~192 B/reject saving matches the body's microbenchmark (192 B, 4 allocs → 0). The "0.0 B/step after" column is not something this PR delivers alone, since other per-step allocations remain. The body does say where the table came from, so this is a wording issue, not a correctness one. It would be clearer to drop the table or label it as coming from the combined audit patch.

  2. Attribution trailers are incomplete (checked from git metadata).

    • Commit 5a60174 has Agent-Harness: Cursor Agent CLI with no version, Agent-Model: auto, and no Agent-Session trailer.
    • The PR body reports the model as "unknown (Cursor auto)".

    CLAUDE.md asks for the exact model ID and a session reference. This is cosmetic for the code, but it is a rule miss.

  3. Comment density in the test is at the limit. 3 of the 28 added lines (~11%) are a comment explaining why @allocated goes through a local function. The comment is justified: it explains a 1.10 world-age artifact. No change needed.

  4. @test total == 0 brittleness (raised by the author). It was exactly 0 on 1.10.x (lts) and 1.11.9 here, and CI passed on lts, 1, x86 and pre. I don't see a reason to loosen it.

Correctness reasoning (from reading, then confirmed by the probes below)

  • L₂/L₃ are added into W.dWtmp/W.dZtmp at lines 436–438, and they are read as Wh by the bridge at lines 455–457. Both happen before the new in-place @.. L₂ -= W.dWtilde at line 470. So the later W.dW += W.dWtilde - W.dWtmp (line 566) still sees the original chunk.
  • ResettableStacks.copyat_or_push! (checked in v1.1.1 and v1.4.0) either safecopys on growth or .=-copies into an existing slot. S₁/S₂ are created as ResettableStack{iip} with the same iip as the process (src/types.jl:209–223, 301–303). So S₁ never aliases the popped S₂ slot, and the later copyat_or_push!(W.S₂, (dtM, W.dWtilde, …)) overwrites that slot only after S₁ has copied it.
  • If cutLength ≤ discard_length, the slot is left alone. If dtM ≤ discard_length, the mutated slot stays inactive (index > cur) and is only ever overwritten by copy.
  • The same pop-mutate-copyat_or_push! pattern already exists in setup_next_step! at src/noise_interfaces/noise_process_interface.jl:191–211. The change is idiomatic for this file.
  • The out-of-place path, the Z === nothing path (guarded by W.Z !== nothing &&) and the empty-S₂ path keep their existing behaviour.

What I ran

All runs were local on macOS aarch64, with scratch environments that Pkg.develop either the PR checkout or a copy of the PR with src/noise_interfaces/noise_process_interface.jl restored from origin/master ("rev"). The rev tree's src/ is byte-identical to master's (diff -r clean).

  • New test fails before the fix and passes after (confirmed by running). I extracted the new testset into scratch/newtest2.jl.
    • Julia 1.11.9, rev: Evaluated: 19200 == 0, FAIL. PR: 101/101 pass.
    • Julia lts (1.10), rev: Evaluated: 19200 == 0, FAIL. PR: 101/101 pass.
  • Bitwise differential vs master at the noise-process level (confirmed by running, scratch/diffprobe.jl).
    • Setup: 100 seeds × {RSwM1, RSwM2, RSwM3} × {Z, no Z} × n ∈ {1, 4} × discard_length ∈ {1e-15, 1e-3}, with 400 random accept/reject ops each. After every op I recorded dt, dW, dZ, the full active contents of S₁ and S₂, and the final t/W/Z.
    • Result: rev vs PR gave 0 mismatches out of 2400 configurations (434,736 time points). Different seeds do give different paths, so the comparison is not trivially equal.
    • Branch coverage: in an instrumented copy (scratch/pr_instr, counter inserted before the new if isinplace(W) at line 469), the changed branch ran 19,496 times during this probe.
  • Bitwise differential at the SDE level (confirmed by running, scratch/sdeprobe.jl).
    • Setup: StochasticDiffEq 7.2.0; SRIW1/SOSRI/LambaEM/RKMil × 100 seeds; iip 4-dim diagonal; RSwM3; seed = seed passed to solve.
    • Result: rev vs PR gave 0 of 400 mismatches on sol.t, sol.u, naccept and nreject (1,980 rejects in total). rev vs rev re-run also gave 0 mismatches, so the comparison is deterministic.
    • An earlier run without seed= mismatched 400/400 only because solve reseeds the noise. That was a mistake in my harness, not in the PR.
  • Allocations per accept at the SDE level, rev vs PR (scratch/sdealloc.jl): numbers are in non-blocking finding 1.
  • Existing tests on the PR, Julia 1.11: test/RSwM_correctness_test.jl passed 1,282,718/1,282,718 and test/RSwM3_test.jl passed. The author's count is 1,282,718, which matches.
  • Formatting and spelling: typos on the 2 changed files is clean. julia +1.12 --project=@runic -m Runic --check --diff on both files is clean.
  • CI: gh pr checks 341 shows all green: Core on lts/1/x86/pre, QA, docs, downgrade, and OrdinaryDiffEq and SciMLSensitivity downstream.
  • Cleanup: no Julia processes from this review are still running.

What I did not verify

  • The author's @benchmark timings (240 ns → 170 ns), and the amdci2 SDE B/step table (only reproduced in part, see non-blocking finding 1).
  • GROUP=QA locally (I relied on the green CI QA job), and the full Core suite locally (I ran only the RSwM test files).
  • GPU and other non-Array in-place noise prototypes. The new code uses @.., as the surrounding in-place code already does.
  • There is no linked issue or tracking issue, so I did no sibling-work interface check.

🤖 Posted by an AI agent — harness: Devin CLI 3000.11.3 (review), Claude Code 2.1.285 (posting) · model: fusion-claude-opus-5-5-high-sidekick-swe-2-medium
Conversation: local Claude session 3c311569-dda6-4687-bc9f-65febb212c33 (fleet master)

@ChrisRackauckas
ChrisRackauckas marked this pull request as ready for review October 16, 2026 04:24
@ChrisRackauckas
ChrisRackauckas merged commit 2fc3074 into SciML:master Oct 16, 2026
19 checks passed
ChrisRackauckas referenced this pull request Oct 17, 2026
Bump version to 5.36.5
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Harness: Claude Code 2.1.301
Agent-Model: claude-opus-5-5
Claude-Session: https://claude.ai/code/session_01VZjwf7Ux9YtTzwHGHMqnsq
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