Swarm.migrate: shrink the working set instead of reclassifying every round - #609
Conversation
…round
The round loop re-offered the whole local array to points_in_domain on every
round, so the classification work per rank grew with the round count. Measured
on a fixed 47k-point global set through global_evaluate, points offered to
points_in_domain: 47k at np=1, 142k at np=2, 238k at np=4.
A point this rank has found to be in its domain stays in its domain — neither
the coordinates nor the mesh change during migration — so it does not need
retesting. The loop now remembers what it has claimed and classifies only the
rest.
The claimed set is keyed by COORDINATE rather than by index. dm.migrate does
not preserve the local ordering: measured, retained points are NOT left at the
front, so an index from the previous round names a different particle after the
move. Two particles sharing a coordinate share the answer, so a key collision
is harmless. Exact bit-pattern comparison, vectorised through a void row view
(15 ms against points_in_domain's 58 ms at 47k, so it pays whenever more than
about a quarter of the local array is already claimed).
Measured after, same probe:
np global_evaluate points offered to points_in_domain
1 0.208 -> 0.213 s 46 901 -> 46 901
2 0.326 -> 0.232 s 141 516 -> 47 558
4 0.443 -> 0.257 s 237 826 -> 50 025
42% off global_evaluate at np=4, and the growth with rank count is gone: the
classification now sees roughly the local set once whatever np is. The
nearest-centroid walk shrinks with it, since it is called from inside the
classification.
The call to points_in_domain is UNCONDITIONAL. It is collective — it reaches
get_max_radius() before any short-circuit precisely so a rank with nothing to
classify still joins the reduction (#405) — and an earlier draft of this
skipped it when a rank had no undecided points, which deadlocked at np=4 with
every rank inside the call.
Verified behaviour-preserving rather than merely green: the per-rank partition
fingerprint after migration is byte-identical to development at np=2 and np=4.
Full ./uw test 1556 passed, matching development.
test_0776 covers the loop, with a premise test that the fixture actually
migrates anything, and its own negative control: reintroducing the conditional
collective makes it time out at np=4 (rc=241), and the fix makes it pass.
Underworld development team with AI support from Claude Code
Adversarial reviewReviewed at 1. The claimed set grows without bound within a call, and is rebuilt on every 2. 3. The equivalence evidence covers np=2 and np=4 on one mesh. The partition 4. The premise test constrains the fixture, not the property. Checked and clean. The Underworld development team with AI support from Claude Code |
The np=4 parallel-test hang is pre-existing, not from this PRReporting this because an earlier run made it look otherwise. Running One earlier np=4 result on this branch should be discarded rather than trusted: What stands for this PR: full |
tests/parallel/test_0776_linear_rbf_proxy_parallel.py already exists, so the new file collided with it — the same defect as #600, where two subjects ended up sharing 1029. Caught while auditing which parallel files scripts/test.sh actually runs. Underworld development team with AI support from Claude Code
Addresses the dominant term in #551.
What was costing the time
Swarm.migrate's round loop re-offered the whole local array topoints_in_domainon every round, so the classification work per rank grew withthe round count. Measured through
global_evaluateon a fixed 47k-point globalset (2-D simplex,
cellSize=1/100, identical total work at every rank count):global_evaluatepoints_in_domainThe walk holds a roughly constant share. What grows is the classification, from
28% to 58% of the call, because a fixed 47k-point set is offered to it 238k
times at np=4.
The change
A point this rank has found to be in its domain stays in its domain — neither
the coordinates nor the mesh change during migration — so it does not need
retesting. The loop remembers what it has claimed and classifies only the rest.
Keyed by coordinate, not by index.
dm.migratedoes not preserve the localordering: measured, retained points are not left at the front, so an index from
the previous round names a different particle after the move. Two particles at
the same coordinate share the answer, so a key collision is harmless. Exact
bit-pattern comparison through a void row view — 15 ms against
points_in_domain's 58 ms at 47k, so it pays whenever more than about a quarterof the local array is already claimed, which it is from the second round on.
Measured after
global_evaluatepoints_in_domain42% off at np=4, and the growth with rank count is gone — the classification now
sees roughly the local set once whatever np is. np=1 is unchanged within noise
(one extra membership pass, no rounds to save). The walk shrinks with it, since
it is called from inside the classification.
This is the larger of the two faults named in #551's "amplifier" section. The
other — no cheap rejection inside the walk — attacks the smaller, flatter term
and is untouched here.
The collective
The
points_in_domaincall is unconditional, and that is load-bearing.It is collective — it reaches
get_max_radius()before any short-circuitprecisely so a rank with nothing to classify still joins the reduction (the #405
treatment, stated in its own source). Guarding it on whether this rank has
undecided points deadlocks at np=4, with every rank inside the call, as soon as
one rank runs out of them.
Verified
partition fingerprint after migration — the sorted coordinates each rank owns,
hashed — is byte-identical to
developmentat np=2 and np=4../uw test: 1556 passed, 32 skipped, 2 xfailed, matchingdevelopment.test_0776_migrate_working_set_mpi.pycovers the loop: the global particlecount is conserved, every point lands on a rank whose mesh contains it, and the
global multiset of coordinates is unchanged. It carries a premise test that the
fixture actually migrates anything —
populatealone leaves every particlealready owned, and the loop never runs.
test_0776time out at np=4 (rc=241); restoring the fix makes it pass. Thetest catches the defect it was written for.
Underworld development team with AI support from Claude Code