Repository navigation
fix(core): a step that changes the layout of the state is refused (MADD-ANO-220) - #254
Merged
Merged
Conversation
…t stored GraphManager.step, run and run_adaptive stored a stepped state whose leaf had another shape than the one it replaced (a list given for a scalar constant broadcasts the leaf at the first step), so the state stopped being the one initial_state(), reset_state() and a checkpoint describe (MADD-ANO-220, since 0.1.0). The stepped state is now compared with the state it replaces once per trace of the step, host-side (keys, shapes, kinds of dtype), and a difference is a ValueError naming the node and the leaf with nothing stored. The FMU sidecar, which runs the compiled step on a state of its own, makes the same comparison. The compile-count tests' retracing fixture grew its state to provoke a retrace; it now starts from a weakly typed scalar, which retraces once and keeps the layout. [skip ci] Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UkCde7g23gTziUjYAvnKD
…ds for MADD-ANO-220 Tests of the comparison on the FMU sidecar and bridge, on both REST step routes for a node the add-node door never saw, and on a graph with a sharded node. MADD-ANO-220 is resolved in the registry; the changelog, the release notes' line, the node authoring guide and claim REST-058 say what a step now refuses. [skip ci] Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UkCde7g23gTziUjYAvnKD
A state leaf that is a PRNG key has an extended dtype, which is not a NumPy dtype: the comparison raised on it, so the step guard refused every step of a graph that carries a key. A key is now compared as itself (a key of the same implementation). The REST add-node door, which refused such a node through that internal error, refuses it by name: a key has no JSON form, so no reply could carry the state. [skip ci] Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UkCde7g23gTziUjYAvnKD
…ep-guard Both sides kept in the changelog; the release notes take the base's Known-anomalies lines with MADD-ANO-220's line as resolved; the SOUP table regenerated. [skip ci] Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UkCde7g23gTziUjYAvnKD
… is refused in a graph The unstructured wrapper's test stepped such a node with gm.step(), which stored a state with a field initial_state() did not build. The step guard refuses that. The test keeps what it protected -- the wrapper is handed its own output back -- on the wrapper's own jitted update, and pins the graph's refusal; the sharding guide says a node in a graph gives its integral an initial value. [skip ci] Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UkCde7g23gTziUjYAvnKD
The work-in-progress pushes of this branch skipped CI; this one does not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UkCde7g23gTziUjYAvnKD
NicholasEhsanRoy
marked this pull request as ready for review
October 7, 2026 02:15
…type `jax.dtypes.issubdtype` is not in the module's exports, which the type checker reports (two errors in core, whose ceiling is zero). `jax.numpy.issubdtype` is the exported name for the same test and takes an extended dtype on every jax version the suite runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UkCde7g23gTziUjYAvnKD
NicholasEhsanRoy
added a commit
that referenced
this pull request
Oct 7, 2026
NicholasEhsanRoy
added a commit
that referenced
this pull request
Oct 7, 2026
NicholasEhsanRoy
added a commit
that referenced
this pull request
Oct 7, 2026
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.
Resolves MADD-ANO-220:
GraphManager.step,runandrun_adaptivestored a stepped state whose layout differed from the state it replaced (shipped 0.1.0 to 0.3.1, silent). They now raiseValueErrornaming the node and the leaf, and store nothing.Decision needed before merge (PR left as draft for it)
A node whose
updatereturns a field itsinitial_state()does not build is now refused at its first step. The guard compares keys as well as shapes (as the saved patch did), so a state that gains a field is a layout change. One existing test pinned the opposite as working:tests/cloud/multigpu/test_unstructured_layout_contract.py::test_a_node_without_an_initial_integral_steps_in_a_graph(verification of MADD-ANO-040), where a node emits a domain integral thatinitial_state()lacks andgm.step()stored it.docs/developer_guide/sharding_topology.mddescribed that as "a graph's second step". No stock node and no shipped example does this (domain_integral_fieldsis used by none of them; the sweeps below found no other graph).What this branch does: the test keeps what it protected (the wrapper is handed its own output back, three jitted
updatecalls, same integral) and now also asserts the graph's refusal; the doc says a node in a graph gives its integral an initial value; the declared form was already tested in graphs and scans (test_a_node_declaring_its_integral_runs_in_a_graph_like_the_unsharded_node).The alternative, if emitting an undeclared field through
step()is to stay supported: do not reportappears only after the updateinGraphManager._refuse_layout_drift(a few lines), and revert that test and doc sentence. The cost: such a state still does not scan, and its checkpoint has a field the reset graph lacks, which is the defect class of 220.What the saved patch needed
p4_34_step_guard.patchapplied cleanly withgit apply --3way. Four things were changed after reading and probing it; each has a regression test and a mutant:add_external_input("a", "drive"), two good steps, thendrive=jnp.ones(3)). The comparison is now due whenever(compile generation, trace count)differs from the last one compared. Cost on every other step: one tuple comparison.params=of another shape was already refused by the params shape check.dtypes=False). An integer leaf returned as a float was stored the same way, is refused by the scans, and its checkpoint is refused after a reset ("holds 0.5 (float32), which this graph's int32 cannot hold"): same class.run_adaptivecompares the first step it keeps, before its callback, observers and thedt_minwarning. The patch compared at the end of the run, after every callback andEVENT_STEPobserver had been handed the reshaped state._leaf_layoutraised on an extended dtype (key<fry>), so the guard refused every step of a graph that carries a key (found by the sweep:test_step_program_digests.py,chain-into-ring/choice-0). A key is now compared as a kind of its own (same implementation).POST /graph/nodeshad been refusing such a node through that internal error; it now refuses it by name (a key has no JSON form, so no state reply could carry it; a graph with a key handed to the server in process answers 500 on/sim/stepon the base too, which is a separate, existing matter).The layout rule
Keys (a missing or a new leaf), shapes, and the kind of dtype (boolean, integer, float, complex, or a PRNG key of one implementation). Width is not compared: under x64 a stock node returns float64 for a float32 state and a width check would refuse them all. A width change is still stored silently by
step(pinned:test_a_dtype_of_another_width_is_not_what_the_comparison_refuses); that is the x64 carry matter of MADD-ANO-017 and 190, not this entry.Host-side only: shapes and dtypes are read from the returned state (tracers under a transform included); nothing is traced, no node's
updateis called.Entry points
step,run_store_stepped_state, once per tracetest_the_step_entry_points_keep_the_shape_of_every_state_leaf[step, run]run_adaptive[run_adaptive]test_a_later_step_that_is_traced_again_is_compared_againtest_the_step_of_a_graph_compiled_again_is_compared_againjax.gradtest_a_step_inside_a_transform_is_compared_on_the_traced_statetest_a_graph_with_a_coupling_group_is_held_to_its_layout_tootests/cloud/multigpu/test_a_sharded_step_keeps_the_state_layout.py/sim/step,/sim/rungm.step/gm.run; 400, nothing storedtest_a_node_the_door_never_saw_is_refused_where_the_graph_stepsgm.step(they rungm._compiled_stepon their own state): own comparison inFmuSidecar._advancedtests/fmi/test_a_sidecar_step_keeps_the_state_layout.pyrun_scan,run_scan_with_history,run_adaptive_scanTypeError(carry)test_the_scan_entry_points_refuse_a_step_that_changes_a_leafs_shapeThe sidecar was a sibling site the entry did not name:
build_model_descriptionaccepted the broadcasting ball andsidecar.stepstoredpositionas(2,). It compares once per trace for a graph's current compiled step, and at every step for any other callable or for the step of a graph compiled again since.On refusal the graph holds the same state object as before (
gm._state is held; the clock and step count of a multi-rate or coupled graph live in it), no observer is told of a step, no callback runs, and the refusal repeats until the node is fixed. After an external input of the wrong shape is refused, stepping on with the right one matches a fresh graph to the bits.How the REST door relates:
_dry_run_nodestays. It refuses before a node is added, traces abstractly, and names the parameter to blame. The graph's guard is the backstop for every graph that did not pass that door (built in process, loaded, edited in process).The compile-count fixture
AvalChangesOncegrewxfrom(1,)to(2,). It now startsxas a Python float: the first step is traced against a weakly typed float32 scalar and returns a strongly typed one, so the second step retraces once and the count settles at 2. Shape and kind never change; the state saves, reloads and scans. Measured:trace_count == 2,compile_counts(gm).retrace_count == 2, and the child-process gate tests built on the fixture pass unchanged.Graphs the guard refused
Sweeps with a plugin that logs every refusal, swallowed or not (jax 0.11.0, 3 cores, per-push lane):
tests/core(four chunks, 8,365 passed),tests/nodes(877),tests/test_examples_smoke.py(273),tests/fmi(838),tests/adaptive,tests/verificationandtests/cloud/multigpuwithout therun_podfiles (1,344 together), and thetests/property,tests/surrogates,tests/viz,tests/usdfiles that never name/cloud(1,412). Thetests/apifiles that never name/cloudran in one long process that was killed at the 10 GB cap 73% of the way through a combined run; nothing was refused before that, and CI is the check for the rest.AvalChangesOnce(test_compile_counts.py)chain-into-ring/choice-0(test_step_program_digests.py)_Source(8)underShardedUnstructuredNodeinitial_state()lacksNo stock node and no shipped example was refused.
Programs, counts, bit identity
scripts/capture_step_programs.py --check, fresh process each: 24 graphs unchanged on jax 0.10.2, 0.11.0 and 0.11.2 (before and after merging the base).scripts/compile_counts.py --checkon jax 0.10.2: counts match, 6 workloads.test_the_comparison_is_made_once_per_trace_and_calls_no_update: nine steps, one trace, oneupdatecall, one comparison.Mutants: 17 seeded, 17 caught
Comparison skipped; stamp without the compile generation (the "never reset on recompile" fault); stamp without the trace count; refused but stored; a refusal that uses the comparison up; kind of dtype not compared;
run_adaptivenot compared;run_adaptivecompared only after its callbacks; compared at every step; sidecar: skipped, trusting a non-graph step, ignoring a later compile, comparing at every graph step, refusal uses the comparison up; a key not a kind of its own; any two keys one kind; the REST door taking a keyed state.Code paths newly enabled or changed
### Changed): a step that changes a leaf's shape or kind of dtype, or the state's fields, raises where it was stored;FmuSidecar.stepand a bridgesteplikewise (error reply, nothing committed).Sibling sites checked
GraphManager: every_store_statecaller (step,run,run_adaptive: guarded;run_scan,run_scan_with_history,run_adaptive_scan: loud already;run_sweepstores nothing).profiler.compile_countsandsim_profilestep throughgm.step.FmuSidecar.stepandFmuTcpBridge(all advancing goes through_advanced): guarded here.examples/basics/bouncing_ball.pycalls_compiled_stepon a local state and stores nothing in the graph.Not run locally
The full suite, the slow lane beyond the files that cite the changed symbols (
test_compile_counts.py,test_bridge_master_dt_is_the_graph_step.py,test_differential_fmu.py: their slow tests ran once, all passed), and the test files that send requests to/cloud. Pyright 1.1.414 on the two changed source files: 0 errors (local numpy, not CI's).One observation, not acted on: a graph with a sharded node beside a plain one traces its step twice (the plain node's leaves come back placed on the mesh), so it compiles twice per run.
🤖 Generated with Claude Code
https://claude.ai/code/session_013UkCde7g23gTziUjYAvnKD