Make simplification deterministic w.r.t. hash/objectid ordering#4675
Draft
baggepinnen wants to merge 1 commit into
Draft
Make simplification deterministic w.r.t. hash/objectid ordering#4675baggepinnen wants to merge 1 commit into
baggepinnen wants to merge 1 commit into
Conversation
Several pipeline stages ordered observable output by iterating Dict/Set
collections keyed by symbolic variables or by types. Type iteration order
follows objectid, which is not stable for types freshly created per compile
(e.g. FunctionWrapper/closure parameter types), so the parameter buffer layout
(and generated-function argument order) could differ run-to-run within a single
session - producing non-reproducible simplification/results. (Builtin-typed
params are stable, hence the bug is intermittent.)
- Add lib/ModelingToolkitBase/src/canonical_ordering.jl: a shared,
hash/objectid-independent `canonical_sort_key`/`canonical_name`/`canonical_sort`
keyed on variable names.
- index_cache.jl: order the nonnumeric/constant/discrete parameter buffers and
their members by `canonical_sort_key` of the parameters rather than by the
Dict{TypeT,Set}/Set{TypeT} iteration order.
- if_lifting.jl: store conditions in an OrderedDict and name the generated
condition variables deterministically (`ifelse_cond_N`) instead of `gensym`
(whose global counter was itself a source of non-determinism).
- sccnonlinearproblem.jl: order SCC cache buffers/types canonically in
`build_caches!`.
- Add test/nondeterminism_simplification.jl (compiles models repeatedly under
GC stress and asserts identical compiled structure) and register it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| canonical tie-break falls back to the original order among the colliding entries. | ||
| """ | ||
| function canonical_name(x::SymbolicT) | ||
| Moshi.Match.@match x begin |
Contributor
There was a problem hiding this comment.
[Runic] reported by reviewdog 🐶
Suggested change
| Moshi.Match.@match x begin | |
| return Moshi.Match.@match x begin |
Comment on lines
+51
to
+53
| BSImpl.Div(;) => :/ | ||
| BSImpl.ArrayOp(;) => Symbol("#arrayop") | ||
| BSImpl.Const(;) => Symbol("#const") |
Contributor
There was a problem hiding this comment.
[Runic] reported by reviewdog 🐶
Suggested change
| BSImpl.Div(;) => :/ | |
| BSImpl.ArrayOp(;) => Symbol("#arrayop") | |
| BSImpl.Const(;) => Symbol("#const") | |
| BSImpl.Div() => :/ | |
| BSImpl.ArrayOp() => Symbol("#arrayop") | |
| BSImpl.Const() => Symbol("#const") |
Comment on lines
+488
to
+489
| sort!(sorted_buffers; by = b -> canonical_sort_key(first(b[2])), | ||
| alg = Base.Sort.DEFAULT_STABLE) |
Contributor
There was a problem hiding this comment.
[Runic] reported by reviewdog 🐶
Suggested change
| sort!(sorted_buffers; by = b -> canonical_sort_key(first(b[2])), | |
| alg = Base.Sort.DEFAULT_STABLE) | |
| sort!( | |
| sorted_buffers; by = b -> canonical_sort_key(first(b[2])), | |
| alg = Base.Sort.DEFAULT_STABLE | |
| ) |
| all_cacheexprs = reduce(vcat, values(cacheexprs); init = SymbolicT[]) | ||
| sorted_cache_types = sort!(collect(keys(cachevars)); by = string) | ||
| all_cacheexprs = reduce( | ||
| vcat, (cacheexprs[T] for T in sorted_cache_types); init = SymbolicT[]) |
Contributor
There was a problem hiding this comment.
[Runic] reported by reviewdog 🐶
Suggested change
| vcat, (cacheexprs[T] for T in sorted_cache_types); init = SymbolicT[]) | |
| vcat, (cacheexprs[T] for T in sorted_cache_types); init = SymbolicT[] | |
| ) |
Comment on lines
+61
to
+62
| @variables x1(t)=0.1 x2(t)=0.2 x3(t)=0.3 x4(t)=0.4 | ||
| @parameters a=1.0 b=2.0 c=3.0 |
Contributor
There was a problem hiding this comment.
[Runic] reported by reviewdog 🐶
Suggested change
| @variables x1(t)=0.1 x2(t)=0.2 x3(t)=0.3 x4(t)=0.4 | |
| @parameters a=1.0 b=2.0 c=3.0 | |
| @variables x1(t) = 0.1 x2(t) = 0.2 x3(t) = 0.3 x4(t) = 0.4 | |
| @parameters a = 1.0 b = 2.0 c = 3.0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
mtkcompilecan produce different results run-to-run, even within a single Julia session (observed ~9/100 on metadata/array/connector-heavy models). The structural symbol hash is actually stable (SymbolicUtils deliberately avoidsobjectid), so the leak is elsewhere:Several pipeline stages order observable output by iterating
Dict/Setcollections keyed by types. Type iteration order followsobjectid/hash(::DataType), which is not stable for types freshly created per compile (e.g.FunctionWrapper{R,Tuple{…}}/ closure parameter types that multibody-style models route into nonnumeric parameter buffers). This permutes the parameter buffer layout and generated-function argument order — invisible in the equation list, which is why it's easy to miss. Builtin-typed parameters hash stably, which is why the symptom is intermittent.Reproduced deterministically with parameters carrying fresh struct symtypes: 176/200 distinct buffer layouts over 200 in-session compiles (with
GC.gc(true)between runs); 1/200 after this PR.A second, independent source:
IfLiftingstored its condition→variable map in a plainDictand named the generated condition variables withgensym(global counter) — both non-deterministic. That reproducer went 300/300 distinct → 1/300.Changes
lib/ModelingToolkitBase/src/canonical_ordering.jl(new): a shared, hash/objectid-independentcanonical_sort_key/canonical_name/canonical_sort, keyed on variable names (mirrors the approach already used in ModelingToolkitTearing).index_cache.jl: order the nonnumeric/constant/discrete parameter buffers (and the symbols within each) bycanonical_sort_keyof the parameters, not byDict{TypeT,Set}/Set{TypeT}iteration. This is the primary fix.if_lifting.jl:conditionsis now anOrderedDict, and condition variables get deterministicifelse_cond_Nnames instead ofgensym.sccnonlinearproblem.jl: order SCC cache buffers/types canonically inbuild_caches!.test/nondeterminism_simplification.jl(new, registered ingroup_interfaceii.jl): compiles representative models 150× withGC.gc(true)between runs and asserts identical compiled structure.Verification
solve+ symbolic parameter access correct after the buffer relayout.if_lifting.jlpasses;scc_nonlinear_problem.jlcore testsets pass.Not included (follow-up)
bounded_stringintearingstate.jl) keys on the printed form, whose Add/Mul operand order is in principle hash-dependent. It is currently stable (operand iteration verified stable), so this is latent and lives in a separate repo; suggested patch is to sort by canonical incidence instead:test/structural_transformation/tearing.jl).HashedRandominitial-guess seeding (seeded byhash(var)) left as-is.Draft pending a full
Pkg.testrun; please watch parameter-layout- and tearing-sensitive tests.🤖 Generated with Claude Code