Skip to content

Cut substitute_v allocations without changing integrand point types - #383

Merged
ChrisRackauckas merged 2 commits into
SciML:masterfrom
ChrisRackauckas-Claude:int-substitute-v-svector
Oct 10, 2026
Merged

ChrisRackauckas merged 2 commits into
SciML:masterfrom
ChrisRackauckas-Claude:int-substitute-v-svector

Conversation

@ChrisRackauckas-Claude

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

Copy link
Copy Markdown
Member

Summary

For Vector domains, HCubature evaluates at immutable SVector points while substitute_v historically mapped with the Vector bounds first, allocating a Vector of intermediates on every evaluation. This PR maps via the evaluation point when it is immutable, then copies once into similar(lb) so the integrand still receives the domain container type (Vector for Vector domains, SVector for SVector domains)—preserving the deliberate design from ded0128.

This is a non-breaking rework of the earlier draft that returned SVector points for Vector domains.

What a reviewer should push back on

  • Residual allocations: one similar(lb) per evaluation remains (3-D Vector ~19k allocs vs ~66 for the breaking approach). A type-stable reused buffer was tried via Ref{Any} and regressed badly; a typed buffer reuse may still be possible.
  • Whether ArrayInterface.ismutable is the right gate for “domain container kind” (e.g. unusual AbstractVector subtypes).

Verification

Regression (domain container type + x .^ 2 Vector output)

  • Before (breaking HEAD): 2 pass / 1 fail / 1 error (x isa Vector failed; x .^ 2 TypeError). See job before.txt.
  • After: 7 pass. See after.txt.
  • Julia 1.10.12: 7 pass.

Benchmarks (JULIA_NUM_THREADS=1 taskset -c 23, @benchmark minima)

case master this PR
2D Vector 118.59 µs / 4076 80.8 µs / 2070
3D Vector 1006.58 µs / 38742 660.26 µs / 19404
2D inf Vector 6564.84 µs / 242492 4525.1 µs / 121282
3D inf Vector 649107 µs / 17882780 395293 µs / 8941430
3D SVector 275.12 µs / 11 275.1 µs / 11

Reviewer probes (orch/reviews/pr-383/round1/)

  • substv_probe.jl: 221 rows, 0 type/error diffs vs master (11 algorithms × domains × oop/iip/batch).
  • substv_break.jl: matches master (Vector-typed integrands, setindex!, push!, x .^ 2 → Vector{Float64}; HAdaptive iip still unsupported as on master).

Test-group tails

GROUP=Core (Julia 1.12.4):

Core/alt_transformation_tests.jl |   19     19  18.9s
Core/gaussian_quadrature_tests.jl |   37     37  5.5s
Core/hadaptiveintegration_tests.jl |    9      9  11.2s
Core/inf_integral_tests.jl |  753    753  3m38.5s
Core/interface_tests.jl |  871    871  9m01.1s
Core/precompile_workload.jl |    1      1  1.1s
Core/quadrule_tests.jl |    4      4  2.4s
Core/sampled_tests.jl |   30     30  9.1s
Core/verbosity_tests.jl |   25     25  1.2s
Testing Integrals tests passed

GROUP=QA:

QA/qa.jl |  27     27  2m20.0s
Testing Integrals tests passed

Runic + typos clean on changed files.

What I did NOT verify

  • AD / ADBatch CI groups
  • Downstream packages beyond the reviewer probes
  • A production-quality type-stable buffer reuse

Please ignore until reviewed by @ChrisRackauckas.

Risk assessment

  • Risk: low — evaluation-point types match master; only allocation path changes.
  • Blast radius: Tuple-domain algorithms via ChangeOfVariables(transformation_if_inf, …); benefit concentrated on HCubatureJL + Vector domains.
  • Evidence: probe parity with master; Core/QA; before/after benches.
  • Independent review: pending
  • Merge: needs review

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

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

ChrisRackauckas and others added 2 commits October 9, 2026 19:05
Leading Base.map with Vector domain bounds forced a Vector of transformed
points on every HCubatureJL evaluation; ordering the map by the SVector
point removes those allocations and matches what HCubature passes directly.

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/integrals/int-substv/log.txt on amdci2
Map via immutable evaluation points (HCubature SVectors) then copy once
into similar(lb) so Vector domains still pass Vector to the integrand,
matching the ded0128 container-type contract with ~half the allocations.

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/integrals/int-substv/log.txt on amdci2
@ChrisRackauckas-Claude ChrisRackauckas-Claude changed the title Make substitute_v follow the SVector evaluation-point container type Cut substitute_v allocations without changing integrand point types Oct 10, 2026
@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 new tests don't check the change this PR makes (confirmed by running). The added testset (test/inf_integral_tests.jl:259-285) passes unchanged on origin/master: I ran it against a master export and got 7/7 pass. So it only guards against the breaking approach in the earlier commit 4b60407 (SVector points for Vector domains), which never reached master. Nothing in the testset checks the allocation reduction, which is the point of the PR. A small @allocated bound would cover it, e.g. Integrals.substitute_v(t2ujac, SVector, Vector, Vector) allocating at most one array; I measured 112 bytes vs 224 on master. @test jac isa Real is also a weak assertion. The expected values are analytic (1/3, a unit-mass Gaussian), not copied from output, and the tolerance comes from the file's abstol = 1e-3, so the tests themselves are sound.
  2. Behaviour change for Tuple evaluation points with MVector/SizedVector domains (confirmed by running, direct call only). substitute_v(t2ujac, (0.25, 0.5), MVector(...), MVector(...)) returns Vector{Float64} on master and MVector{2,Float64} on the PR; SizedVector domains behave the same way. No built-in algorithm in src/ or ext/ passes Tuple points. HCubature, HAdaptiveIntegration, Cubature, Cuba and VEGAS all produced identical point types in my solve probe. The only way to reach this is a user-supplied multi-dimensional QuadratureRule whose nodes are tuples. The new result arguably fits the PR's stated "domain container type" goal better, but it contradicts the PR body's claim that point types are identical to master in every case.
  3. Branch history. The branch has two commits, and the first (4b60407) contains the breaking design that the second undoes. Squash-merge so the intermediate breaking state doesn't land in master.
  4. Minor: the inline comment at src/infinity_handling.jl:13 is fine in content. The (vi, l, u) -> v2ujac(vi, l, u) lambda could just be v2ujac, since the arguments are already in the right order.

What I ran

All runs used Julia 1.11.9, with scratch environments that Pkg.develop the PR head or a git archive origin/master export.

  • Type and value parity probe (scratch/probe.jl), master vs PR, output diffed:

    • Direct substitute_v calls for every combination of:
      • 13 domain types: Vector, Vector with Inf, SVector, MVector, SizedVector, view, Float32, BigFloat, Int, Vector{Real}, Unitful, Vector{Dual}, range;
      • 7 point types: SVector, SVector{Float32}, SVector{Dual}, MVector, Vector, Tuple, range;
      • 2 transformations: t2ujac and t2ujac_tan.
    • Out-of-place and in-place solve with HCubatureJL, CubatureJLh, CubatureJLp, CubaCuhre, HAdaptiveIntegrationJL and VEGAS over 8 domain types, recording the type of x that the integrand sees.
    • Result: of 274 lines, the only differences are finding 2 (Tuple points with MVector/SizedVector domains) and VEGAS Monte-Carlo noise. All deterministic solve values are bit-identical and all integrand point types are identical.
  • Benchmarks (scratch/bench.jl): exp(-|x|²), HCubatureJL, tolerance 1e-8, @benchmark minimum allocation counts, JULIA_NUM_THREADS=1. The results reproduce the PR's claim that allocations are halved:

    case master PR
    2D Vector 3252 1654
    3D Vector 38734 19396
    2D Vector, infinite bounds 229154 114608
    3D SVector 8 8
    2D MVector 815 815
    • sol.u is identical between master and PR. The infinite-domain case gives π, which is correct.
    • return_types of substitute_v is concretely inferred: Tuple{Vector{Float64},Float64}.
  • ForwardDiff (scratch/fd.jl): gradients with respect to p (finite, doubly infinite and semi-infinite Vector domains) and with respect to ub are bit-identical on master and PR. -π/p² at p=1.5 gives −1.3963, which matches.

  • AD paths: Zygote (@non_differentiable) and Mooncake (@zero_derivative) both treat substitute_v as non-differentiable, so the similar/copyto! mutation cannot reach a reverse-mode trace. I confirmed this by reading the code, not by running it.

  • Tests: the full test/inf_integral_tests.jl on the PR head passes with no failures or errors (≈128 s). The new testset alone also passes on master (finding 1).

  • Formatting and spelling: Runic --check on both changed files exits 0. typos on the changed files reports nothing.

  • API and dependencies: ArrayInterface.ismutable is public in ArrayInterface 7.30.2 and ArrayInterface is already a dependency. The PR adds no new dependency. The repo has no AGENTS.md or CLAUDE.md, and the PR links no issue or tracking issue, so there is no sibling work to check against.

What I did not verify

  • The full GROUP=Core and GROUP=QA runs, the AD/ADBatch groups, and Julia 1.10. I ran only inf_integral_tests.jl plus the probes above.
  • The author's own probe files (orch/reviews/pr-383/round1/*), which aren't in the repo. I replaced them with my own probe.
  • The exact timings in the PR's table. That integrand and machine are unknown; I reproduced the allocation halving, not the microsecond numbers.
  • GPU or OffsetArray domains, which master may not support either.

🤖 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 10, 2026 17:15
@ChrisRackauckas
ChrisRackauckas merged commit e02a9f9 into SciML:master Oct 10, 2026
15 checks passed
ChrisRackauckas referenced this pull request Oct 14, 2026
Agent-Harness: Devin CLI 3000.11.3
Agent-Model: Fusion (Claude Opus 5.5 Medium + SWE-2 Medium)
Agent-Session: local transcript ~/.local/share/devin/cli/transcripts/flash-hour.json

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
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