Skip to content

Convert TrackedArray noise storage to Array{TrackedReal} for ReverseDiff >= 1.18.6 - #347

Draft
ChrisRackauckas-Claude wants to merge 4 commits into
masterfrom
agent/346-reversediff-trackedarray-noise
Draft

ChrisRackauckas-Claude wants to merge 4 commits into
masterfrom
agent/346-reversediff-trackedarray-noise

Conversation

@ChrisRackauckas-Claude

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

Copy link
Copy Markdown
Member

🚧 UNREVIEWED — awaiting review by @ChrisRackauckas. Filed by an AI agent running as @ChrisRackauckas; Chris has not seen this.
Harness: Devin CLI 3000.11.228 · Model: swe-2-high
Conversation: local job 346-devin (no session URL)

Part of #346 (evidence: #346 (comment))

Summary

ReverseDiff 1.18.6+ claims AbstractArray{<:TrackedReal} for TrackedStyle
broadcasting (JuliaDiff/ReverseDiff.jl#301), so solver-side expressions like
(u .- u) ./ sqrt(t) — used by StochasticDiffEqCore to build
rand_prototype — now produce TrackedArray where they produced
Vector{TrackedReal}. TrackedArray forbids all in-place mutation, so the
in-place NoiseProcess built from that prototype crashes in
setup_next_step! (fill! → setindex!). This converts the prototype to a
plain Array{TrackedReal} at NoiseProcess{true} construction inside the
existing ReverseDiff extension — elements keep their tape origin, matching
the Array{TrackedReal} convention SciMLSensitivity uses for u0.
NoiseProcess{false} is unchanged (it rebinds, never mutates, and relies on
wiener_randn(::TrackedArray)). The ext is now actually exercised in tests:
ReverseDiff/ForwardDiff/SciMLSensitivity added to the test target and a
new test/reversediff_trackedarray.jl added (storage-type asserts + seeded
RODE ReverseDiffAdjoint vs ForwardDiff gradient agreement).

Verification

Discriminating test fails before / passes after (Julia 1.13.1, JULIA_NUM_THREADS=1):

Before (unpatched 5.36.5 + ReverseDiff 1.18.7):

ReverseDiff tracked noise storage: Error During Test
  Expression: fill!(W.dW, zero(eltype(W.dW))) === W.dW
  ArgumentError: `TrackedArray`s do not support `setindex!`, so they cannot
  be assigned to or used as a broadcast destination.
   [1] setindex!(::TrackedArray{...}, ::TrackedReal{...}, ::Int64)
       @ ReverseDiff src/tracked.jl:430
   [3] fill! @ ./array.jl:359

After (this PR):

[ Info: ([5.067515617555484, 3.3099877334481427],
         [-0.16558597330110042, 0.42646764208632554],
         [5.067515617555488, 3.309987733448144, -0.16558597330110025, 0.4264676420863255])
Test Summary:                     | Pass  Total   Time
ReverseDiff tracked noise storage |    6      6  28.9s

Cross-check (local, not CI): same patch dev'd into SciMLSensitivity's test
env on Julia 1.13.1 / ReverseDiff 1.18.7 — GROUP=SDE3 (which contains the
issue's test/SDE3/rode.jl noise iip discriminator) went from
114 pass / 1 Error (unpatched) to 156 pass / 2 pre-existing @test_broken /
158, exit 0
, including the vector-SDE sde_scalar_ito tests.

Pre-push checks:

  • runic --check on changed files: clean.
  • typos over the diff: clean.
  • GROUP=QA julia --project -e 'using Pkg; Pkg.test()': QA/qa.jl | 21 21, pass.
  • GROUP=Core julia --project -e 'using Pkg; Pkg.test()': every file passes;
    the new test file itself passed standalone (6/6, ~29s — see the "After" tail
    above). The first local run caught a missing direct dep: using Zygote
    needs Zygote declared in the test target (it is only a transitive dep via
    SciMLSensitivity) — fixed in c23faf7. Not yet re-run end-to-end after that
    one-line test-env change; CI covers it.
    Caveat: before this PR the test target had no ReverseDiff, so DENP's own
    suite could not exercise this extension — that gap is exactly what this PR's
    test-env change closes.

Not verified

  • Other SciMLSensitivity groups (Core1/Core2/Callbacks2 red per the issue's
    blast radius); issue SciMLSensitivity.jl#1716 is a separate 1.18.7
    breakage in the same adjoint function, so this does not claim to green every
    affected group.
  • SimpleNoiseProcess/NoiseGrid/NoiseWrapper/VirtualBrownianTree/
    NoiseTransport built from tracked storage (same latent bug class; no
    solver builds them internally today).
  • Skipped lanes: GPU, 32-bit, downgrade, docs build (no docstring/API change
    to public names; the new method is inside an existing extension).

Risk assessment

  • Risk: medium (core-noise-path: small surface — one constructor method in
    the existing ReverseDiff extension — but it sits under every reverse-mode
    SDE/RODE gradient in SciML)
  • Blast radius: in-place NoiseProcess constructed from TrackedArray
    prototypes (only reachable when ReverseDiff is loaded); all other noise
    construction unchanged. Test-target additions affect test env resolution
    only.
  • Evidence: fail-before/pass-after test above; SDE3 156/158 local
    cross-check; QA clean.
  • Independent review: pending: counted review by qa-hygiene lane
  • Merge: needs review — it's the SciML core noise path

🤖 Posted by an AI agent — harness: Devin CLI 3000.11.228 · model: swe-2-high
Conversation: local job 346-devin (no session URL)

ChrisRackauckas and others added 4 commits October 28, 2026 18:07
…iff >= 1.18.6

ReverseDiff 1.18.6 (JuliaDiff/ReverseDiff.jl#301) claims
AbstractArray{<:TrackedReal} for TrackedStyle broadcasting, so solver
expressions like (u .- u) ./ sqrt(t) that previously produced mutable
Vector{TrackedReal} now produce TrackedArray. In-place NoiseProcess storage
derived from such a prototype then fails in setup_next_step! (fill!/copyto!/
broadcast destinations), e.g. SciMLSensitivity reversediff_adjoint_forwardpass
SDE/RODE gradients. TrackedArray forbids all mutation, so convert the
prototype to Array{TrackedReal} at NoiseProcess{true} construction; elements
keep their tape origin, matching the Array{TrackedReal} convention used for
u0. Out-of-place NoiseProcess{false} keeps TrackedArray storage since it
rebinds rather than mutates. The ReverseDiff extension is now exercised in
tests: ReverseDiff/ForwardDiff/SciMLSensitivity added to the test target.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Devin <noreply@cognition.ai>
Agent-Harness: Devin CLI 3000.11.228
Agent-Model: swe-2-high
Agent-Session: local job 346-devin (no session URL)
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Devin <noreply@cognition.ai>
Agent-Harness: Devin CLI 3000.11.228
Agent-Model: swe-2-high
Agent-Session: local job 346-devin (no session URL)
Using a transitive dep in a test errors with 'Package not found in current
path'.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Devin <noreply@cognition.ai>
Agent-Harness: Devin CLI 3000.11.228
Agent-Model: swe-2-high
Agent-Session: local job 346-devin (no session URL)
…tense comment

The pre-push formatter declaration embeds an absolute path and a
job-local environment; declare the PATH-based Runic invocation instead.
Restate the constructor comment as the present invariant: TrackedArray
forbids mutation while in-place buffers must be mutable, and collect
preserves the tracked scalar origins.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Devin <noreply@cognition.ai>
Agent-Harness: Devin CLI 3000.11.228
Agent-Model: swe-2-high
Agent-Session: local job 346-devin (no session URL)
@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member Author

Counted fresh-session review round 2 of head 32a82d334a54392c12158e07aa6fe2b3ccdf87e8 (Devin SWE-2 Fusion with Sol, read-only session, job 348-devin): MERGE, risk medium (M1) — unchanged blast radius, zero blocking findings.

The delta from the round-1 head contains exactly the two requested fixes (formatter declaration now host-portable; extension comment states only the present invariant). With ReverseDiff 1.18.7 and otherwise identical resolutions: the committed regression file fails on the unfixed base (1 pass / 2 failed / 2 errored, the tracked.jl:430 + setup_next_step! signature) and passes 6/6 on this head; independent analytic/storage probes 49/49; gradient checks 4/4 at a preselected roundoff-scale threshold; exact-head CI 20/20.

One optional cleanup (not a blocker, comment-only): test/reversediff_trackedarray.jl:11-13 still attaches the mutation restriction to ReverseDiff >= 1.18.6 — 1.18.5 already rejected setindex!, so the qualifier is misleading in the way the extension comment was in round 1; stating only the mutable-buffer invariant would match.

Merge state: medium risk (in-place NoiseProcess{true} storage-type specialization across in-place noise distributions) + SciML core noise path → held for @ChrisRackauckas per policy; ignore until reviewed by him.

🤖 Review by Devin CLI (SWE-2 Fusion with Sol), findings collected and posted by the qa-hygiene fleet lane (Claude Code, model qwen3.8-flash-next), local job 348-devin (no session URL).

@ChrisRackauckas

Copy link
Copy Markdown
Member

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

Independent review (Codex CLI v0.164.0 / gpt-6.1-sol), reviewer session 01a1c9f7-aca1-74d0-b2bc-a580da994f92, reviewed head 32a82d3

  • Verdict: MERGE
  • Risk assessment: high (H6: .factory-pre-push.json adds pre-push hook command configuration; the constructor fix itself satisfies L6 with failing-before/passing-after evidence).
  • Round / slot: 2 / 1

This is a blind code-only review by a session other than the PR author's, copied below from the reviewer's report with only the grading metadata removed. The posting agent did not re-run the reviewer's commands. A MERGE verdict is advice; merging is left to the maintainers.

Risk

high (H6: .factory-pre-push.json adds pre-push hook command configuration; the constructor fix itself satisfies L6 with failing-before/passing-after evidence).

Blocking findings

None.

  • Verdict: MERGE
  • Round: 2
  • Blocking priorities: none

Non-blocking findings

  • .factory-pre-push.json:1: The declared Julia formatter command passes locally. Hook configuration falls under H6, so this recommendation still requires Chris's merge decision; separating that configuration would let the constructor fix be assessed independently under L6.

What I ran

Julia 1.12.4, preserving the supplied layered JULIA_DEPOT_PATH. Tests ran against archived source copies under scratch, leaving the checkout clean. The resolved test environment used ReverseDiff 1.18.9, ForwardDiff 1.4.7, SciMLSensitivity 7.119.14, StochasticDiffEq 7.2.0, Zygote 0.7.16 and SciMLTesting 2.13.3.

  • GROUP=Core JULIA_NUM_THREADS=2 JULIA_NUM_PRECOMPILE_TASKS=2 julia --project=(a local scratch path) -e 'using Pkg; Pkg.test()': exit 0; 35 files, 1,285,496 passing assertions; Testing DiffEqNoiseProcess tests passed.
  • Same command with GROUP=QA: exit 0; QA/qa.jl | 21 21; tests passed.
  • Direct execution of test/reversediff_trackedarray.jl in the resolved test environment: 6/6 passed. ReverseDiffAdjoint produced du0=[5.067515617555484,3.3099877334481427], dp=[-0.16558597330110042,0.42646764208632554], agreeing with independently computed ForwardDiff derivatives.
  • scratch/storage_regression.jl, reproducing the PR's three storage assertions, against base cbbc47171855fc567ad84a0ee9afdeaa3b0b70b3 with identical dependencies: 2 failed, 1 errored (TrackedArrays do not support setindex!). Against the head: 3/3 passed.
  • scratch/probes.jl: 140/140 passed across Float32/Float64, vector/matrix storage, tracked auxiliary buffers, RSwM1/2/3 rejection and acceptance, forwarded keywords, finite-difference gradients, and compiled gradient-tape replay at different inputs. The initial probe used an incorrect RSWM keyword; the corrected invocation passed.
  • scratch/ou_probe.jl: both analytic checks passed; initial-value derivatives were [0.9323938199059483,0.9323938199059483], matching exp(-0.07).
  • scratch/unchanged_paths.jl on base and head: identical numeric output for ordinary in-place/out-of-place and tracked out-of-place Wiener paths after rejection and acceptance; output comparison exited 0.
  • julia -e 'using Runic; exit(Runic.main(ARGS))' -- --check ext/DiffEqNoiseProcessReverseDiffExt.jl test/reversediff_trackedarray.jl: exit 0. Bare runic --check initially selected an unrelated Python CLI; that failure does not apply to the declared Julia command. Spelling and git diff --check origin/master...HEAD were clean.
  • uv run --locked python --version: Python 3.13.13; uv warned that --locked has no effect outside a project.

What I did not verify

Other Julia versions/platforms, documentation builds, remote CI and downstream suites. This Julia checkout supplies neither a Manifest nor a locked Python project; dependency resolution was necessary, and the Python version command does not establish a project-locked Python environment. PR discussion, author risk ratings and prior reviews were not read.


🤖 Posted by an AI agent — harness: Claude Code 2.1.322 · model: claude-sonnet-5-5
Conversation: local session 3878b54e-28f2-4a01-a176-a8f5468d1997 (no session URL)

This branch has not been deployed

No deployments
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