Follow-up to #376 (fixed in #378). The root cause there — NDArray_With_Callback.__array_finalize__ hands the sync callbacks to every array derived by indexing, so callbacks written for the canonical storage fire on views and temporary copies — is more general than the two canonical callbacks that were fixed. The historical disable_inplace_operators=True blocking on swarm.points ("disabled for parallel safety", af308d8, Sept 2025) was a symptom-level mitigation of the same disease. Four remaining items, none blocking #378:
1. swarm.points / swarm_update_callback still has the disease, plus a worse one
- The callback calls collective
migrate() from inside a per-write callback — the exact pattern the swarm variable canonical callback was rewritten to avoid (SWARM-03 deferred migration) — and raises RuntimeError rank-locally on a size mismatch. Ranks that write unevenly diverge on collectives.
- The in-place blocking only covers the
i-operators; __setitem__ on a derived fancy-indexed copy still fires the callback.
- The documented example in
migration_control()'s own docstring raises today (verified):
with swarm.migration_control():
swarm.points[mask1] += delta1
# RuntimeError: In-place addition (+=) is disabled for parallel safety.
The disable_inplace_operators flag propagates to the fancy copy via __array_finalize__, so the advertised pattern is simply broken. .points is already deprecated in favour of swarm variables — either port it to the guarded-callback + deferred-migration pattern (after which the blocking flag can be dropped and the docstring becomes true), or accelerate its retirement.
2. Delayed-callback replay is rank-asymmetric by construction
delay_callback / uw.delay_callbacks_global queue (callback, array, context) per rank and replay them between blanket barriers at context exit. Queue lengths and contents are rank-dependent (each rank queues what it locally triggered) while the callbacks contain collectives — the barriers around the batch cannot fix mismatched collectives inside it. Per-item exceptions are swallowed with logger.warning (the same swallow that hid #376 for ten months). The robust shape already exists in the codebase: the swarm canonical callback's _pending_petsc_sync marks variables dirty by NAME and flushes collectively at context exit. The delay machinery should do the same — flush dirty variables canonically, not replay rank-local event queues.
3. The #376 guard lives in two closures, not the class
Any future add_callback user re-inherits the trap. Add e.g. add_canonical_callback() on NDArray_With_Callback that applies the base-chain identity guard (view → sync canonical; copy → skip, the parent __setitem__ does the real sync) once, centrally, so the safe pattern is the default. Migrate the three existing registration sites (mesh variable, swarm variable, swarm.points) onto it.
4. Adjacent, lower severity
- Silent-bypass writes:
np.add(x, 1, out=x), .fill(), np.copyto mutate without any callback → values land (the canonical wraps the vec memory) but ghost sync and the state increment don't happen — stale ghosts/caches rather than hangs.
- Every in-place op copies the entire array as
old_value for callbacks that never read it — pure memory/bandwidth waste on large variables.
Context and verification details: PR #378 review discussion; the guard semantics (identity in numpy's base chain, NOT np.may_share_memory, which is False for any zero-size array and re-creates the rank asymmetry on empty ranks) are documented in the fixed callbacks in discretisation_mesh_variables.py and swarm.py.
Underworld development team with AI support from Claude Code
Follow-up to #376 (fixed in #378). The root cause there —
NDArray_With_Callback.__array_finalize__hands the sync callbacks to every array derived by indexing, so callbacks written for the canonical storage fire on views and temporary copies — is more general than the two canonical callbacks that were fixed. The historicaldisable_inplace_operators=Trueblocking onswarm.points("disabled for parallel safety", af308d8, Sept 2025) was a symptom-level mitigation of the same disease. Four remaining items, none blocking #378:1.
swarm.points/swarm_update_callbackstill has the disease, plus a worse onemigrate()from inside a per-write callback — the exact pattern the swarm variable canonical callback was rewritten to avoid (SWARM-03 deferred migration) — and raisesRuntimeErrorrank-locally on a size mismatch. Ranks that write unevenly diverge on collectives.i-operators;__setitem__on a derived fancy-indexed copy still fires the callback.migration_control()'s own docstring raises today (verified):The
disable_inplace_operatorsflag propagates to the fancy copy via__array_finalize__, so the advertised pattern is simply broken..pointsis already deprecated in favour of swarm variables — either port it to the guarded-callback + deferred-migration pattern (after which the blocking flag can be dropped and the docstring becomes true), or accelerate its retirement.2. Delayed-callback replay is rank-asymmetric by construction
delay_callback/uw.delay_callbacks_globalqueue(callback, array, context)per rank and replay them between blanket barriers at context exit. Queue lengths and contents are rank-dependent (each rank queues what it locally triggered) while the callbacks contain collectives — the barriers around the batch cannot fix mismatched collectives inside it. Per-item exceptions are swallowed withlogger.warning(the same swallow that hid #376 for ten months). The robust shape already exists in the codebase: the swarm canonical callback's_pending_petsc_syncmarks variables dirty by NAME and flushes collectively at context exit. The delay machinery should do the same — flush dirty variables canonically, not replay rank-local event queues.3. The #376 guard lives in two closures, not the class
Any future
add_callbackuser re-inherits the trap. Add e.g.add_canonical_callback()onNDArray_With_Callbackthat applies the base-chain identity guard (view → sync canonical; copy → skip, the parent__setitem__does the real sync) once, centrally, so the safe pattern is the default. Migrate the three existing registration sites (mesh variable, swarm variable,swarm.points) onto it.4. Adjacent, lower severity
np.add(x, 1, out=x),.fill(),np.copytomutate without any callback → values land (the canonical wraps the vec memory) but ghost sync and the state increment don't happen — stale ghosts/caches rather than hangs.old_valuefor callbacks that never read it — pure memory/bandwidth waste on large variables.Context and verification details: PR #378 review discussion; the guard semantics (identity in numpy's base chain, NOT
np.may_share_memory, which is False for any zero-size array and re-creates the rank asymmetry on empty ranks) are documented in the fixed callbacks indiscretisation_mesh_variables.pyandswarm.py.Underworld development team with AI support from Claude Code