synchronised_array_update: collective dirty-variable flush replaces rank-local event replay (#379 item 2)#383
Conversation
Adversarial review — round 1One reviewer attacked the flush design with live np=2 repros (faulthandler-verified hang tracebacks). The creation-order id mechanism itself survived: no rank-conditional variable-construction path exists in src/ (every
Also recorded: the missing-id RuntimeError is nearly unreachable in practice (the orchestration model's registry pins every registered variable strongly — pre-existing), and counter misalignment would be undetectable in principle, resting on "construction always contains a world collective" — true today, worth a cheap debug assertion later. Test gaps to close with the fixes: exception path, Fixes follow on this branch. Underworld development team with AI support from Claude Code |
Round 1 — response (fixed; branch rebased onto the updated #381)All four findings addressed:
Also from the review: the near-unreachable missing-id error and the in-principle-undetectable counter misalignment are accepted as documented residual risk ("construction always contains a world collective" — true today, cheap debug assertion left for a follow-up), and the rank-uneven- 9/9 parallel tests at np=2 and np=4; quick gate 415 passed. Underworld development team with AI support from Claude Code |
…join the flush, live-canonical re-resolve (#383 review) Adversarial review round 1 (np=2 repros with faulthandler tracebacks) confirmed the creation-order id mechanism and found four holes, all closed here: 1. The exit runs the rank-agreement collective on BOTH paths: the allgather now carries an aborted flag, and any rank aborting makes every rank skip the flushes together. Previously the raising rank skipped the allgather while survivors ran it — a CAUGHT rank-local exception deadlocked (the old machinery replayed unconditionally, so this was a regression). All rank-local flushing now happens AFTER the agreement. 2. v.array = values (attribute assignment) routes through the view write path — the property setter packed collectively per write, bypassing the context and hanging rank-uneven assignments. Note: under active units this now enforces the same unit-aware-input requirement as view writes (previously plain arrays were packed silently). 3. Mesh coordinates join the collective flush: the coords callback registers through the canonical guard and the mesh carries a registration id, so mesh.X.coords writes inside the context defer to ONE rank-agreed deform at exit instead of replaying a rank-local collective deform per write (the legacy-replay invariant was violated by the repo's own most important legacy callback). 4. Swarm-variable flushes re-resolve the LIVE canonical through the owner (weakly held) instead of pinning the array captured at write time: migration inside the context invalidates and rebuilds the canonical, and a pinned array flushed stale pre-migration values. A migrate() issued inside the context forfeits unflushed prior writes but stays consistent (documented on the flush target). Four new parallel regression tests pin the previously-hanging shapes: caught rank-local exception, rank-uneven .array view write, rank-uneven v.array = assignment, and a coords write inside the context. 9/9 at np=2 and np=4; quick gate 415 passed. Underworld development team with AI support from Claude Code
97d4491 to
bcbdcad
Compare
1f42128 to
d2cf355
Compare
…to the central guard The inline guard deleted in the rebase onto development documented one genuinely unresolvable per-write corner (reshape/ravel of a non-contiguous derived view: copy on non-empty ranks, view on zero-size ranks). Preserve it on add_canonical_callback, pointing at the #383 per-variable flush as the real fix. Underworld development team with AI support from Claude Code
…join the flush, live-canonical re-resolve (#383 review) Adversarial review round 1 (np=2 repros with faulthandler tracebacks) confirmed the creation-order id mechanism and found four holes, all closed here: 1. The exit runs the rank-agreement collective on BOTH paths: the allgather now carries an aborted flag, and any rank aborting makes every rank skip the flushes together. Previously the raising rank skipped the allgather while survivors ran it — a CAUGHT rank-local exception deadlocked (the old machinery replayed unconditionally, so this was a regression). All rank-local flushing now happens AFTER the agreement. 2. v.array = values (attribute assignment) routes through the view write path — the property setter packed collectively per write, bypassing the context and hanging rank-uneven assignments. Note: under active units this now enforces the same unit-aware-input requirement as view writes (previously plain arrays were packed silently). 3. Mesh coordinates join the collective flush: the coords callback registers through the canonical guard and the mesh carries a registration id, so mesh.X.coords writes inside the context defer to ONE rank-agreed deform at exit instead of replaying a rank-local collective deform per write (the legacy-replay invariant was violated by the repo's own most important legacy callback). 4. Swarm-variable flushes re-resolve the LIVE canonical through the owner (weakly held) instead of pinning the array captured at write time: migration inside the context invalidates and rebuilds the canonical, and a pinned array flushed stale pre-migration values. A migrate() issued inside the context forfeits unflushed prior writes but stays consistent (documented on the flush target). Four new parallel regression tests pin the previously-hanging shapes: caught rank-local exception, rank-uneven .array view write, rank-uneven v.array = assignment, and a coords write inside the context. 9/9 at np=2 and np=4; quick gate 415 passed. Underworld development team with AI support from Claude Code
bcbdcad to
ce0beb3
Compare
ce0beb3 to
58091e5
Compare
…join the flush, live-canonical re-resolve (#383 review) Adversarial review round 1 (np=2 repros with faulthandler tracebacks) confirmed the creation-order id mechanism and found four holes, all closed here: 1. The exit runs the rank-agreement collective on BOTH paths: the allgather now carries an aborted flag, and any rank aborting makes every rank skip the flushes together. Previously the raising rank skipped the allgather while survivors ran it — a CAUGHT rank-local exception deadlocked (the old machinery replayed unconditionally, so this was a regression). All rank-local flushing now happens AFTER the agreement. 2. v.array = values (attribute assignment) routes through the view write path — the property setter packed collectively per write, bypassing the context and hanging rank-uneven assignments. Note: under active units this now enforces the same unit-aware-input requirement as view writes (previously plain arrays were packed silently). 3. Mesh coordinates join the collective flush: the coords callback registers through the canonical guard and the mesh carries a registration id, so mesh.X.coords writes inside the context defer to ONE rank-agreed deform at exit instead of replaying a rank-local collective deform per write (the legacy-replay invariant was violated by the repo's own most important legacy callback). 4. Swarm-variable flushes re-resolve the LIVE canonical through the owner (weakly held) instead of pinning the array captured at write time: migration inside the context invalidates and rebuilds the canonical, and a pinned array flushed stale pre-migration values. A migrate() issued inside the context forfeits unflushed prior writes but stays consistent (documented on the flush target). Four new parallel regression tests pin the previously-hanging shapes: caught rank-local exception, rank-uneven .array view write, rank-uneven v.array = assignment, and a coords write inside the context. 9/9 at np=2 and np=4; quick gate 415 passed. Underworld development team with AI support from Claude Code
…cal-phase success before collective flushes (#383 review round 2) Round 2 verified all round-1 fixes closed and found one blocking new defect introduced by the coords canonical registration: _deform_mesh re-wraps the coordinate array after a rebuild and copied the callback list verbatim — the canonical dispatch wrapper is bound (by weakref) to the OLD array's identity, so on the replacement it classified every write as a foreign copy and silently returned. A SECOND mesh.X.coords write did nothing: no deform, no version bump, no error. The re-wrap now re-homes canonical callbacks via their recorded original function (exactly the sync_data re-homing from #381 round 1). Regression test writes coordinates twice and checks the DM. Also hardened (round-2 note): rank-local flush phases (legacy replay, swarm packs) can raise rank-locally; when a collective flush follows, ranks now agree on local-phase success first, so no rank enters the per-variable collectives while another unwinds. Underworld development team with AI support from Claude Code
Round 2 — verification and one blocking find (fixed in 58091e5)The round-2 verifier confirmed all round-1 fixes closed (collective counts traced through every nesting/abort combination; setter and both view classes route through the canonical; flush targets re-resolve live). It found one blocking defect introduced by the coords canonical registration: Also hardened from a round-2 note: rank-local flush phases can raise rank-locally; when a collective flush follows, ranks now agree on local-phase success before entering the per-variable collectives, so no rank unwinds while others block. Residual (accepted, documented): a rank-local exception in the context body followed by other user collectives after the raising point is inherent to rank-local exceptions and outside this machinery's reach. Serial battery 21 passed; 9/9 parallel at np=2; gate re-running on the stack tip. Underworld development team with AI support from Claude Code |
…vely, not rank-local event replay (#379 item 2) The delay machinery queued (callback, array, change_info) per rank and replayed the queue between blanket barriers at context exit. Queue contents are rank-dependent while mesh-variable callbacks contain collectives (ghost sync), so rank-uneven writes ran mismatched collectives — a rank-0-only masked write hung every other rank. The barriers could not fix collectives inside the batch, and per-item exceptions were swallowed (the same swallow that hid #376). Now each write inside the context marks its CANONICAL variable dirty. At exit, mesh variables (collective packs) are flushed by cross-rank agreement: every variable carries a creation-order registration id (construction is SPMD-collective, so ids match by construction; NAMES cannot serve — temporary variables embed rank-local id() values), the union of dirty ids is allgathered unconditionally, and every rank flushes the union in id order. Ranks that wrote nothing pack unchanged values, keeping the per-variable collective matched. Swarm-variable packs are rank-local and flush without agreement; migration stays rank-agreed via the existing _needs_migration reduction. Exceptions propagate; if the context body raises, the flush is skipped entirely (ranks unwind asymmetrically — replaying collectives during unwinding was the hang vector; the written values remain in the local arrays). Two adjacent traps this exposed, fixed here because the flush's rank-uneven promise is broken without them: - The .array views (SimpleMeshArrayView / TensorMeshArrayView) called pack(sync=True) directly per write — a collective per setitem that bypassed the delay context entirely. They now route through the canonical array, so writes follow the standard path: immediate pack outside a context, one agreed flush inside. - First .data access performs collective vec setup, so a lazy first touch from rank-conditional code deadlocked. The canonical array is now created eagerly at variable construction, which is collective (same reasoning as the BUGFIX(#130) coordinate-cache pre-population). The two duplicate delay-context classes collapse into one; the legacy per-event queue survives only for untagged add_callback() users (rank-local, documented). test_0140 is rewritten from a print-script that could not fail into real tests pinning the new contract. Underworld development team with AI support from Claude Code
… suite (#379 item 2) Five np>=2 shapes that desynchronised the old replay: rank-uneven writes, opposite per-rank write order, helper-created variables written on one rank, mixed swarm+mesh updates, and a zero-size slice on one rank (the empty-rank classification case). Assertions use global reductions — after ghost sync each dof carries its owner's value, so rank-local expectations are wrong by construction. Verified: np=2 and np=4, 5 passed each. Underworld development team with AI support from Claude Code
…join the flush, live-canonical re-resolve (#383 review) Adversarial review round 1 (np=2 repros with faulthandler tracebacks) confirmed the creation-order id mechanism and found four holes, all closed here: 1. The exit runs the rank-agreement collective on BOTH paths: the allgather now carries an aborted flag, and any rank aborting makes every rank skip the flushes together. Previously the raising rank skipped the allgather while survivors ran it — a CAUGHT rank-local exception deadlocked (the old machinery replayed unconditionally, so this was a regression). All rank-local flushing now happens AFTER the agreement. 2. v.array = values (attribute assignment) routes through the view write path — the property setter packed collectively per write, bypassing the context and hanging rank-uneven assignments. Note: under active units this now enforces the same unit-aware-input requirement as view writes (previously plain arrays were packed silently). 3. Mesh coordinates join the collective flush: the coords callback registers through the canonical guard and the mesh carries a registration id, so mesh.X.coords writes inside the context defer to ONE rank-agreed deform at exit instead of replaying a rank-local collective deform per write (the legacy-replay invariant was violated by the repo's own most important legacy callback). 4. Swarm-variable flushes re-resolve the LIVE canonical through the owner (weakly held) instead of pinning the array captured at write time: migration inside the context invalidates and rebuilds the canonical, and a pinned array flushed stale pre-migration values. A migrate() issued inside the context forfeits unflushed prior writes but stays consistent (documented on the flush target). Four new parallel regression tests pin the previously-hanging shapes: caught rank-local exception, rank-uneven .array view write, rank-uneven v.array = assignment, and a coords write inside the context. 9/9 at np=2 and np=4; quick gate 415 passed. Underworld development team with AI support from Claude Code
…cal-phase success before collective flushes (#383 review round 2) Round 2 verified all round-1 fixes closed and found one blocking new defect introduced by the coords canonical registration: _deform_mesh re-wraps the coordinate array after a rebuild and copied the callback list verbatim — the canonical dispatch wrapper is bound (by weakref) to the OLD array's identity, so on the replacement it classified every write as a foreign copy and silently returned. A SECOND mesh.X.coords write did nothing: no deform, no version bump, no error. The re-wrap now re-homes canonical callbacks via their recorded original function (exactly the sync_data re-homing from #381 round 1). Regression test writes coordinates twice and checks the DM. Also hardened (round-2 note): rank-local flush phases (legacy replay, swarm packs) can raise rank-locally; when a collective flush follows, ranks now agree on local-phase success first, so no rank enters the per-variable collectives while another unwinds. Underworld development team with AI support from Claude Code
58091e5 to
de644a9
Compare
…mposed #382 bumps _mesh_version in _deform_mesh too The #382 round-2 fix adds a version bump inside _deform_mesh (so mesh.deform notifies swarms); the coords-callback path bumps again. The consumer contract is inequality — the regression tests now assert the same, instead of pinning per-write step counts that broke when the sibling branches composed on development. Underworld development team with AI support from Claude Code
Third of the #379 series, stacked on #381 (base is
bugfix/canonical-callback-guard; retarget todevelopmentonce #381 merges). Independent of #382.The problem
uw.synchronised_array_updatequeued every write event per rank and replayed the queue at context exit, between blanket barriers. The queue contents are rank-dependent (each rank queues what it locally wrote), while mesh-variable callbacks contain collective operations (the ghost synchronisation). So ranks that wrote unevenly replayed different collectives — a masked write on one rank hung every other rank — and the barriers around the batch could not fix collectives inside it. Per-item exceptions were swallowed into a logger warning, the same swallow that hid #376.The fix
Writes inside the context now mark their canonical variable dirty instead of queueing events. At exit:
id()values in their names.) The union of dirty ids is allgathered — unconditionally, empty sets included, so ranks stay matched — and every rank flushes the union in id order. Ranks that wrote nothing pack their unchanged values, which is what keeps the per-variable collective matched._needs_migrationreduction at solve entry.Two adjacent traps, fixed because the rank-uneven promise is broken without them
.arrayviews calledpack(sync=True)directly on every setitem — a collective per write that bypassed the delay context entirely (andmodel.py's persistent-variable transfer does exactly this inside the context). They now route through the canonical array, so.arraywrites follow the same path as.datawrites: immediate pack outside a context, one agreed flush inside..dataaccess performed collective vec setup lazily, so a first touch from rank-conditional code deadlocked. The canonical array is now created eagerly at variable construction (which is collective) — the same reasoning as the existing VE Stokes hangs on first solve at specific (np, mesh) combinations #130 coordinate-cache pre-population.Also: the two duplicate delay-context classes collapse into one;
swarm.access()/mesh.access()compatibility shims inherit the new exit semantics automatically.Verification
tests/test_0140_synchronised_updates.pyrewritten from a print-script that could not fail into real tests (level_1 / tier_a, written first): one flush per variable at exit, silence during the context, masked-write canonical marking, per-level nested flushing, exception-skips-flush, legacy untagged replay preserved.tests/parallel/test_0758_synchronised_update_collective_flush.py(level_2 / tier_a): rank-uneven writes, opposite per-rank write order, helper-created variables, mixed swarm+mesh, zero-size slice on one rank. 5 passed at np=2 and np=4. The rank-uneven case hangs on the old machinery.level_1 and tier_a: 411 passed, 1 pre-existing xfail. Swarm-write and mesh-deform MPI smokes at np=2: passed.Flagged, not fixed (out of scope):
_vector_stats/_tensor_statscreate variables named_temp_mag_{id(self)}— rank-asymmetric names (TODO in a follow-up issue); a latentnpNameError on the unit-aware tensor write path is marked with a TODO(BUG) at the site.Underworld development team with AI support from Claude Code