Skip to content

Choose the SCC block container from the specialization level, and precompile a multi-block SCC initialization - #5202

Draft
ChrisRackauckas wants to merge 2 commits into
masterfrom
scc-container-by-specialization
Draft

ChrisRackauckas wants to merge 2 commits into
masterfrom
scc-container-by-specialization

Conversation

@ChrisRackauckas

Copy link
Copy Markdown
Member

🚧 UNREVIEWED — awaiting review by @ChrisRackauckas. Opened by an AI agent running as @ChrisRackauckas; Chris has not reviewed this change.
Harness: Claude Code 2.1.280 · Model: claude-opus-5-5[1m]
Conversation: https://claude.ai/code/session_01Wt79fZeNaZxiwQ3LagMYXH

Please ignore until reviewed by @ChrisRackauckas.

Depends on an SCCNonlinearSolve release. At AutoDespecialize, small homotopy initialization problems now take SCCNonlinearSolve's vector path, which threw MethodError: no method matching probvec(::HomotopyProblem). Fixed in SciML/NonlinearSolve.jl#1321 and released by SciML/NonlinearSolve.jl#1323. Until 1.15.4 is registered, test/homotopy_initialization_scc.jl will error on CI here. The SCCNonlinearSolve floor (currently "1.13") must be raised to "1.15.4" before merging. I left it unchanged so CI can resolve now.

What changed and why

SCCNonlinearProblem now picks its block container from the specialization level: AutoDespecialize always stores the blocks in a vector, FullSpecialize always in a tuple, and AutoSpecialize keeps the existing heuristic (tuple for ≤ 5 blocks). Previously every level used the heuristic, so an initialization problem with 2–5 blocks had a type specific to its block count and the kind of each block, and no precompile workload could cover a user's model. Initialization problems built by ODEProblem are AutoDespecialize by default, so they now share one type across models.

The DAE precompile workload also never reached this path: precompile_dae_problem has a single algebraic block, which builds a plain NonlinearProblem initialization (its docstring said SCC; corrected). This adds precompile_scc_dae_problem — a nonlinear block followed by a linear block — built in the package workload and solved in the FBDF, Rodas5P and default-algorithm extension workloads.

Verification

Julia 1.12.6, aarch64-linux (Jetson AGX Orin), --startup-file=no.

New tests fail without the container change and pass with it. test/scc_nonlinear_problem.jl, the two added testsets, run with src/problems/sccnonlinearproblem.jl reverted to master:

SCC block container follows the specialization level:  Test Failed
  Expression: prob.probs isa container          (x2)
Test Summary: ... | Pass 16  Fail 2  Total 18

container actually produced on master, by level:
  n=2  FullSpecialize -> Tuple    AutoSpecialize -> Tuple    AutoDespecialize -> Tuple
  n=7  FullSpecialize -> Vector   AutoSpecialize -> Vector   AutoDespecialize -> Vector

Initialization `SCCNonlinearProblem` type does not depend on the block count:  Test Failed
  Expression: allequal(typeof.(initprobs))
  Expression: typeof(workload) === typeof(initprobs[1])
Test Summary: ... | Pass 7  Fail 2  Total 9

With the change: 18/18 and 9/9 pass.

First solve of a user model in a fresh session, this checkout at master versus master + this PR, everything else identical, pkgimages built before each timed run. Chain DAE with array parameters, FBDF:

model master this PR
DAE, 3 init blocks, no kwargs 5.58 s 4.17 s
DAE, 6 init blocks, no kwargs 6.09 s 3.96 s
DAE, 6 init blocks, abstol/reltol 6.47 s 5.87 s
ODE, no init blocks (control) 1.13 s 1.22 s

The 6-block case improves too although it was already a vector on master, because nothing precompiled the SCC path before. The row with tolerances barely moves: the workloads solve without keyword arguments, and passing abstol/reltol re-specializes __init's keyword body — a separate, finite axis for a follow-up.

Runtime cost of the vector container, the same blocks stored as a tuple versus a vector, warm solve(prob, NewtonRaphson()), 10th percentile of 2000 (medians were noisier; a test suite was running alongside):

blocks tuple vector
2 66.98 µs 78.15 µs
3 115.36 µs 127.55 µs
5 200.90 µs 219.84 µs

About 10–20 µs per initialization solve, once per solve. FullSpecialize keeps the tuple for anyone for whom that matters.

Suite (this branch, with SCCNonlinearSolve from SciML/NonlinearSolve.jl#1321 dev'd):

  • GROUP=InterfaceII: Pass 1367, Broken 7 — Testing ModelingToolkit tests passed.
  • GROUP=Initialization: Pass 922, Broken 12 — Testing ModelingToolkit tests passed. Against registered SCCNonlinearSolve 1.15.3 the same group has one error, the probvec(::HomotopyProblem) above.
  • GROUP=QA: Pass 39, Fail 1. The failure is No unapproved public reexports for [:ap_var, :canonical_constraints, :constraint_residual], none of which this PR touches. Master's own CI fails identically at e167d97: https://github.com/SciML/ModelingToolkit.jl/actions/runs/35838708180/job/107109419276
  • Docs (docs/make.jl): builds through doctests, @docs expansion and cross-references. The only error is linkcheck on https://docs.sciml.ai/PDEBase/stable/interface/ in docs/src/API/PDESystem.md (not touched here), which returns HTTP 403 from this machine with curl too.

Runic: no changes on the touched files. typos: clean on added lines.

Not verified

  • x86-64: all numbers are from one aarch64 machine.
  • Test groups other than InterfaceII and Initialization; no downstream packages.
  • NoSpecialize and FunctionWrapperSpecialize take the vector path here (anything that is not FullSpecialize or the AutoSpecialize heuristic). That was not specified; push back if either should keep the heuristic.

🤖 Posted by an AI agent — harness: Claude Code 2.1.280 · model: claude-opus-5-5[1m]
Conversation: https://claude.ai/code/session_01Wt79fZeNaZxiwQ3LagMYXH

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wt79fZeNaZxiwQ3LagMYXH

ChrisRackauckas and others added 2 commits September 23, 2026 17:51
AutoDespecialize always stores the blocks of an SCCNonlinearProblem in a vector,
FullSpecialize always in a tuple, and AutoSpecialize keeps the tuple-for-at-most-5-blocks
heuristic. A tuple makes the problem type depend on the block count and on each block's
kind, so initialization problems (AutoDespecialize by default) could not share compiled
code across models.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agent-Harness: Claude Code 2.1.280
Agent-Model: claude-opus-5-5[1m]
Agent-Session: https://claude.ai/code/session_01Wt79fZeNaZxiwQ3LagMYXH
Claude-Session: https://claude.ai/code/session_01Wt79fZeNaZxiwQ3LagMYXH
precompile_dae_problem has a single algebraic block, so its initialization is a plain
NonlinearProblem and the SCC path was never precompiled (its docstring said SCC; fixed).
precompile_scc_dae_problem builds a nonlinear block followed by a linear block and is
solved in the FBDF, Rodas5P and default-algorithm workloads. Tests pin the container per
specialization level and that initialization problems with 2, 3 and 7 blocks share the
workload's type.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agent-Harness: Claude Code 2.1.280
Agent-Model: claude-opus-5-5[1m]
Agent-Session: https://claude.ai/code/session_01Wt79fZeNaZxiwQ3LagMYXH
Claude-Session: https://claude.ai/code/session_01Wt79fZeNaZxiwQ3LagMYXH
@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Results (Julia vlts)

Time benchmarks
master 6538da2... master / 6538da2...
ODEProblem 5.62 ± 0.26 ms 7.07 ± 1.9 ms 0.796 ± 0.21
init 0.116 ± 0.038 ms 0.13 ± 0.04 ms 0.891 ± 0.4
large_parameter_init/ODEProblem 16.7 ± 1.3 ms 20.1 ± 2.9 ms 0.828 ± 0.13
large_parameter_init/init 0.107 ± 0.016 ms 0.132 ± 0.043 ms 0.815 ± 0.29
mtkcompile 9.71 ± 0.51 ms 11.8 ± 2.7 ms 0.823 ± 0.19
sparse_analytical_jacobian/ODEProblem 27.9 ± 1.1 ms 0.0359 ± 0.0043 s 0.778 ± 0.098
sparse_analytical_jacobian/f_iip 0.17 ± 0.01 μs 0.17 ± 0 μs 1 ± 0.059
sparse_analytical_jacobian/f_oop 2.37 ± 0.023 ms 2.45 ± 0.074 ms 0.968 ± 0.031
time_to_load 7.91 ± 0.24 s 8.45 ± 0.59 s 0.936 ± 0.071
Memory benchmarks
master 6538da2... master / 6538da2...
ODEProblem 0.0375 M allocs: 1.98 MB 0.0375 M allocs: 1.98 MB 1
init 0.508 k allocs: 0.0846 MB 0.508 k allocs: 0.0846 MB 1
large_parameter_init/ODEProblem 0.161 M allocs: 6.06 MB 0.161 M allocs: 6.06 MB 1
large_parameter_init/init 0.64 k allocs: 0.179 MB 0.64 k allocs: 0.179 MB 1
mtkcompile 0.0594 M allocs: 3.36 MB 0.0594 M allocs: 3.36 MB 1
sparse_analytical_jacobian/ODEProblem 0.177 M allocs: 8.75 MB 0.177 M allocs: 8.76 MB 1
sparse_analytical_jacobian/f_iip 0 allocs: 0 B 0 allocs: 0 B
sparse_analytical_jacobian/f_oop 0.867 k allocs: 0.0532 MB 0.867 k allocs: 0.0532 MB 1
time_to_load 0.153 k allocs: 14.6 kB 0.153 k allocs: 14.6 kB 1

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Results (Julia v1)

Time benchmarks
master 6538da2... master / 6538da2...
ODEProblem 5.47 ± 0.58 ms 4.8 ± 0.17 ms 1.14 ± 0.13
init 0.071 ± 0.024 ms 0.0677 ± 0.025 ms 1.05 ± 0.52
large_parameter_init/ODEProblem 16.4 ± 2.4 ms 13.3 ± 2.1 ms 1.24 ± 0.27
large_parameter_init/init 0.0738 ± 0.011 ms 0.0644 ± 0.026 ms 1.15 ± 0.49
mtkcompile 10.4 ± 1.8 ms 8.81 ± 0.64 ms 1.18 ± 0.22
sparse_analytical_jacobian/ODEProblem 29 ± 3.2 ms 27.8 ± 6.1 ms 1.04 ± 0.26
sparse_analytical_jacobian/f_iip 1.89 ± 0.56 μs 1.89 ± 0.84 μs 0.999 ± 0.53
sparse_analytical_jacobian/f_oop 0.253 ± 0.025 ms 0.266 ± 0.014 ms 0.948 ± 0.11
time_to_load 5.73 ± 0.044 s 5.11 ± 0.025 s 1.12 ± 0.01
Memory benchmarks
master 6538da2... master / 6538da2...
ODEProblem 0.0377 M allocs: 1.71 MB 0.0377 M allocs: 1.71 MB 0.999
init 0.474 k allocs: 0.0602 MB 0.474 k allocs: 0.0602 MB 1
large_parameter_init/ODEProblem 0.157 M allocs: 5.39 MB 0.157 M allocs: 5.39 MB 0.999
large_parameter_init/init 0.756 k allocs: 0.166 MB 0.756 k allocs: 0.166 MB 1
mtkcompile 0.0558 M allocs: 2.62 MB 0.0558 M allocs: 2.62 MB 1
sparse_analytical_jacobian/ODEProblem 0.178 M allocs: 7.36 MB 0.178 M allocs: 7.36 MB 1
sparse_analytical_jacobian/f_iip 0 allocs: 0 B 0 allocs: 0 B
sparse_analytical_jacobian/f_oop 1.08 k allocs: 0.0661 MB 1.08 k allocs: 0.0661 MB 1
time_to_load 0.202 k allocs: 12.2 kB 0.202 k allocs: 12.2 kB 1

ChrisRackauckas-Claude pushed a commit to ChrisRackauckas-Claude/ModelingToolkit.jl that referenced this pull request Sep 30, 2026
Address independent review of SciML#5221: leave the Homotopy SCC container
policy to SciML#5202, return InitializeprobParameterMap only under
AutoDespecialize, retarget type-equality to DAE sizes (6, 9), and cover
non-tunable array-parameter indices in CopyParamsByTemplate.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Cursor Agent <noreply@cursor.com>
Agent-Harness: Cursor Agent CLI
Agent-Model: auto
Agent-Session: local session, transcript at /home/crackauc/sandbox/agent-jobs/mtk-ttfx/jobs/initdata/log.txt on amdci2.julia.csail.mit.edu

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

bot-generated Filed by an AI agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant