Skip to content

fix: swarm empty-rank save deadlock (MPIO dtype) and estimate_dt reshape crash - #680

Merged
lmoresi merged 3 commits into
developmentfrom
bugfix/swarm-empty-rank-save-reshape
Sep 4, 2026
Merged

fix: swarm empty-rank save deadlock (MPIO dtype) and estimate_dt reshape crash#680
lmoresi merged 3 commits into
developmentfrom
bugfix/swarm-empty-rank-save-reshape

Conversation

@bknight1

@bknight1 bknight1 commented Sep 3, 2026

Copy link
Copy Markdown
Member

Summary

Two fixes making passive-swarm operations safe when one or more MPI ranks hold zero particles.

  1. estimate_dt() empty-rank guard — zero-particle velocity data is reshaped to an explicit (0, dim) array instead of crashing on reshape(n, -1) with ValueError: cannot reshape array of size 0 into shape (0,newaxis).

  2. Empty-rank field-dtype preservation — fixes a parallel-HDF5 (MPIO) collective-close deadlock in SwarmVariable.save / write_timestep. An int swarm variable (e.g. add_variable("uid", dtype=int)) held int32 on a non-empty rank but float64 on an empty rank (the empty-rank unpack_raw_data_from_petsc fallback returned a bare float64 np.zeros((0, n))). save() passed the per-rank dtype to the collective create_dataset, so HDF5 metadata diverged and the collective close deadlocked. The field PETSc dtype is now stored (_petsc_dtype) and used for every zero-length fallback so dtypes agree across ranks.

Intended outcome

  • write_timestep / swarm save no longer hang with empty ranks (was reproducible 6/6 at np=4, now clean 6/6).
  • estimate_dt() no longer crashes on empty ranks.

The evaluate / global_evaluate / advection empty-rank deadlock was already fixed upstream by #611 / PR #656; coverage here guards that against regression.

Tests

New regression tests (both run with --with-mpi):

  • tests/parallel/test_0795_swarm_empty_rank_evaluate_save.py — evaluate + write_timestep on empty ranks. test_passive_swarm_save_empty_ranks reproduces the user's crust-only-tracer pattern and hung at np=4 before this change, now passes.
  • tests/parallel/test_0796_swarm_advection_empty_rank.py — advection on the default non-evalf path with empty ranks.

Result: 5 passed at np=2 and 5 passed at np=4 (4 from test_0795 + 1 from test_0796).

Underworld development team with AI support from Claude Code

On an MPI rank holding zero particles (e.g. crust-only tracers confined to a
subset of ranks), the velocity array entering the estimate_dt() max-speed
reduction is empty; reshape(n, -1) then raises 'ValueError: cannot reshape
array of size 0 into shape (0,newaxis)' because NumPy cannot infer the implied
dimension from zero elements. Guard the empty case explicitly so the rank
contributes zero to the global max and estimate_dt() returns cleanly.

The empty-rank collective point-location deadlock in advection's
global_evaluate is fixed upstream (issue #611 / PR #656) and covered by
tests/parallel/test_1076_global_evaluate_empty_rank.py, so no local workaround
is needed here.

Regression: test_0796 exercises the DEFAULT (non-evalf) advection path on empty
ranks, which also runs estimate_dt via order=2. test_0795 covers evaluate +
write_timestep on empty ranks.

Underworld development team with AI support from Claude Code
An int swarm variable (e.g. add_variable('uid', size=1, dtype=int)) holds
particles on some ranks but not others. On an empty rank (local_size == 0),
SwarmVariable.unpack_raw_data_from_petsc() returned np.zeros((0, n)) which is
float64, so an empty rank's data array had dtype float64 while a non-empty
rank's was the field's PETSc type (int32). In parallel HDF5 every rank must
create an identical dataset, but SwarmVariable.save() passed the per-rank
local_data.dtype to the collective create_dataset(); the divergent dtype left
the collective close(s) unsynchronised and the save deadlocked — the same
silent-hang class as issue #151.

Fix: store the field's PETSc dtype (_petsc_dtype) and use it for every
zero-length fallback array, so empty-rank data agrees with the field's true
type across all ranks. Regression covered by
tests/parallel/test_0795_swarm_empty_rank_evaluate_save.py::test_passive_swarm_save_empty_ranks,
which hung at np=4 before this change and now passes.

Underworld development team with AI support from Claude Code
@bknight1
bknight1 requested a review from lmoresi as a code owner September 3, 2026 07:43
Copilot AI lite review requested due to automatic review settings September 3, 2026 07:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new advection regression test can be flaky/incorrect due to add_particles_with_coordinates() migrating particles across ranks, and the new tests introduce disallowed data-access patterns (mesh.access/.data) per the style charter.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Fixes two empty-MPI-rank failure modes in swarm workflows (advection timestep estimation and parallel-HDF5 saving), and adds MPI regression tests to prevent deadlocks/crashes when some ranks hold zero particles.

Changes:

  • Preserve PETSc field dtype on empty ranks for SwarmVariable unpack/initialisation, preventing dtype divergence in collective parallel-HDF5 writes.
  • Guard Swarm.estimate_dt() against zero-length velocity arrays on empty ranks.
  • Add MPI regression tests covering evaluate/save and advection with empty ranks.
File summaries
File Description
src/underworld3/swarm.py Stores PETSc dtype on swarm variables for consistent empty-rank fallbacks; adds empty-rank handling in estimate_dt().
tests/parallel/test_0795_swarm_empty_rank_evaluate_save.py New MPI regression coverage for evaluate() + write_timestep() under empty-rank distributions.
tests/parallel/test_0796_swarm_advection_empty_rank.py New MPI regression coverage for swarm.advection(..., order=2) exercising global_evaluate and estimate_dt() on empty ranks.
Review details

Suppressed comments (2)

tests/parallel/test_0796_swarm_advection_empty_rank.py:47

  • New code should not use with mesh.access(...) (style charter forbids it); use the variable's .array property directly for data assignment.
    v = uw.discretisation.MeshVariable("v", mesh, mesh.dim, degree=1)
    with mesh.access(v):
        v.data[:, 0] = mesh.X.coords[:, 1]
        v.data[:, 1] = 0.0

tests/parallel/test_0795_swarm_empty_rank_evaluate_save.py:348

  • New code should not use with mesh.access(...) or the flat .data compatibility layer (see the style charter’s Data Access rules). Use the MeshVariable .array property directly for assignment, and read coordinates from mesh.X.coords.
    with mesh.access(T_mesh):
        T_mesh.data[:, 0] = 300.0 + 100.0 * T_mesh.coords[:, 0] * T_mesh.coords[:, 1]
  • Files reviewed: 3/3 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/underworld3/swarm.py
Comment on lines 5092 to +5101
if vel.ndim == 3:
vel = vel.reshape(vel.shape[0], -1)
# Guard against empty ranks: an array of size 0 cannot be
# reshaped with a `-1` axis (NumPy cannot infer the implied
# dimension from zero elements) — e.g. (0, 1, dim) -> (0, -1)
# raises ValueError. A zero-particle rank legitimately has no
# velocities and contributes 0 to the global max below.
if vel.size == 0:
vel = np.zeros((0, vel.shape[2]) if vel.ndim >= 3 else (0,))
else:
vel = vel.reshape(vel.shape[0], -1)
Comment on lines +51 to +63
# Particles only on rank 0 (collective call)
if uw.mpi.rank == 0:
coords = (np.random.rand(100, mesh.dim) * 0.8 + 0.1)
else:
coords = np.empty((0, mesh.dim))
swarm.add_particles_with_coordinates(coords)

uw.mpi.comm.barrier()

# Sanity: the empty-rank precondition must actually hold (all 100 points
# are added on rank 0 and no migration happens here, so the other ranks
# hold zero particles).
sizes = uw.mpi.comm.allgather(swarm.local_size)
import os
import numpy as np
import pytest
import sympy as sp
]


def test_advection_empty_rank_default(tmp_path_factory):
@lmoresi
lmoresi merged commit 37308de into development Sep 4, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants