Skip to content

draw_markers: send the markers to their processes also without sorting boxes - #765

Closed
spossann wants to merge 1 commit into
develfrom
draw-markers-mpi-sort
Closed

spossann wants to merge 1 commit into
develfrom
draw-markers-mpi-sort

Conversation

@spossann

Copy link
Copy Markdown
Member

Solves the following issue(s):

Closes #764. Needed by #734, whose end-to-end GPU tests run without sorting boxes and crash on 2 ranks without this fix.

Core changes:

  • Particles.draw_markers always MPI-sorts the markers on more than one process. Before, it called mpi_sort_markers() only when sorting boxes were set up. Without boxes, markers drawn on the whole domain (sobol_standard, sobol_antithetic, pseudo_random with spatial="disc", external, restart) stayed on the wrong process until the first push. Every setup step before that saw them: the initial Poisson accumulation of VlasovAmpereOneSpecies and LinearVlasovAmpereOneSpecies wrote outside the local stencil arrays (segfault on 2 ranks), and Particles5D evaluated the magnetic moment at off-process markers. Sorting into boxes (sort, do_sort) is unchanged, and on one process nothing changes.
  • New MPI test test_draw_without_boxes_on_rank_domain (pic/tests/test_draw_parallel.py, marked mpi_pic): checks that, without sorting boxes, every marker is on its process right after drawing, for pseudo_random, sobol_standard and sobol_antithetic, with sort=True and sort=False. Without the fix it fails on 2 ranks (250 markers off-process per rank).

Behaviour change: a strong load imbalance after drawing (e.g. spatial="disc", or an uneven external load) now aborts at draw time (mpi_comm.Abort() in the marker exchange) instead of at the first push.

Tests run locally (Linux, Open MPI 4.1.6):

  • test_draw_parallel.py and test_sorting.py pass on 1, 2, 3 and 4 ranks (69 passed each);
  • -m mpi_pic pic/tests on 2 ranks: 41 passed;
  • test_sph.py on 2 ranks: 449 passed; test_tesselation.py, test_sorting.py and test_load_external.py on 2 ranks: 270 passed;
  • verification tests of VlasovAmpereOneSpecies and LinearVlasovAmpereOneSpecies on 2 ranks: 3 passed;
  • the end-to-end GPU tests of [gpu-1] End-to-end GPU test of VlasovAmpereOneSpecies on 1 and 2 ranks #734 with this fix patched in, on 2 ranks: NumPy 3 passed and 2 skipped; fake CuPy 4 passed and 1 skipped.

Documentation changes:

The draw_markers docstring describes when the markers are sorted.

🤖 Generated with Claude Code

…g boxes (#764)

On more than one process, draw_markers MPI-sorted the markers only if sorting
boxes were set up. Without boxes, the markers drawn on the whole domain stayed
on the wrong process until the first push, and accumulations before it (e.g.
the initial Poisson solve of VlasovAmpereOneSpecies) wrote outside the local
stencil arrays (segfault on 2 ranks). Now draw_markers always MPI-sorts on
several processes; sorting into boxes is unchanged. On one process the
behaviour is unchanged.

New MPI test test_draw_without_boxes_on_rank_domain (pseudo_random, sobol_standard,
sobol_antithetic; sort=True/False): fails on 2 ranks without the fix.

Closes #764.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
spossann added a commit that referenced this pull request Oct 13, 2026
The end-to-end tests run on 2 ranks without sorting boxes and need the
markers MPI-sorted after drawing (#764).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
max-models added a commit that referenced this pull request Oct 13, 2026
#734)

**Solves the following issue(s):**

Part of #689, tracked in #650. This PR covers the items of #689 that are
left for `VlasovAmpereOneSpecies`: running on 1 and 2 ranks, no
host/device transfers in the time loop, and the diagnostics it needs. It
does not close #689, because these runs use the CPU emulation; the first
run on a real GPU is #687.

**Stack:** this is **gpu-1** of a new stack. Merge it after #722
(`cuda-namespaces`), #716, #717, #719 and #765 (MPI sort of the markers
after drawing, needed for the 2-rank runs). It merges those branches in,
so until they are merged, this diff also shows their commits. **gpu-2**
(the benchmark script) is based on this branch.

**Core changes:**

- **New test `models/tests/test_gpu_VlasovAmpereOneSpecies.py`.** A
small Landau-damping run on NumPy and on CuPy: 8 cells, 320 Sobol
markers, 3 steps, full-f weights, and an `e1_v1` binning plot so that
the diagnostics run on the device too. It compares the energies, the
`e_field` and `phi` coefficients, and the markers by ID (tolerances
explained in `compare_runs`), and checks the transfer budget of the time
loop.
- **Shared helpers in `models/tests/gpu_e2e.py`.** The transfer budget,
the instrumentation, gathering markers across ranks and the device
context, moved out of #719's test.
`test_gpu_LinearVlasovAmpereOneSpecies.py` now uses them.
- **The GPU tests also run without a GPU.** On cunumpy's fake CuPy
(`CUNUMPY_FAKE_CUPY=1`), every CUDA launch is emulated on the CPU and
`CudaKernel.compile` is skipped. `Simulation.compile_cuda_kernels` still
checks that every kernel of the time loop has a CUDA version. Without a
GPU and without the fake CuPy, `test_fake_cupy_matches_numpy` runs the
comparison in a serial child process.
- **Transfer budget of `model.integrate`.** Only scalar downloads are
allowed: the residual norm of each Krylov iteration, which feectools
copies for its convergence test (feectools#98 keeps everything else of
the solve on the device). No arrays, no host-to-device copies, no host
kernels.
- **Bug fix (every backend): `VlasovAmpereCoupling` updates the ghost
regions of the mid-point field before the push.** The pusher kernel
reads `_e_sum._data` including its ghost regions, and `_e_sum` is built
with `+=` from the solver output, which does not sync them. Stale ghost
regions would give a wrong field near process (and periodic) boundaries.
- **`Particles`.** Checking whether MPI is CUDA-aware now happens when
the marker array is allocated (in the setup), not on the first marker
exchange. Before, that check was the only array transfer inside the time
loop on 2 ranks.

**Results** (fake CuPy, every CUDA launch emulated on the CPU; macOS;
**not run on a GPU**):

| test | 1 rank | 2 ranks |
|---|---|---|
| `test_gpu_VlasovAmpereOneSpecies.py` (NumPy reference + CuPy vs NumPy
+ transfer budget) | passed | passed |
|
`test_gpu_LinearVlasovAmpereOneSpecies.py::test_cupy_matches_numpy_without_transfers`
| passed | not run |

Transfers in the time loop of the CuPy run:
- `integrate`: one 8-byte residual norm per pcg iteration.
- Diagnostics: one 24-byte copy of the three scalars per update.
- Output: one copy per saved dataset.

**Diagnostics:** no new CUDA kernels are needed for this model. The
scalars use `xp` reductions and feectools' device inner products, and
binning uses `xp.histogramdd`.

**Found along the way (not changed here):** now #766 (Poisson
stabilization) and #767 (energy diagnostic with the control variate).
- The initial Poisson solve with the default `stab_eps=1e-14` puts
(Monte-Carlo net charge) / 1e-14 into the constant mode of `phi`. That
makes `phi` about 1e14, so E = -grad(phi) loses about 5% to cancellation
on either backend. The verification tests use this default. The new test
uses `stab_eps=1e-6`, as #719 does.
- With `control_variate=True`, `kinetic_energy` at t=0 is computed with
the full-f weights and afterwards with the delta-f weights, so
`total_energy` jumps after the first step. With full-f weights the total
is conserved from step to step to 1e-9, but the t=0 value still differs
by about 2e-4 (not investigated).

**Review fixes** (pushed by @spossann, see the review):
- the e2e tests run in CI: unit shard-2 (serial and 2 ranks) and on the
fake CuPy on 1 and 2 ranks (`test-PR-unit-mpi.yml`); merged #765,
without which they segfault on 2 ranks (#764);
- `MassMatrixPreconditioner._core_diagonal` returns a diagonal operator
again (with the wrapped band entries);
- `test_preconditioner_cupy.py` uses cunumpy's `emulated_launches` and
`run_fake_cupy_child`, and its GPU test emulates its launches on the
fake CuPy;
- `FFTSolver.solve` asserts host input.

**Documentation changes:**

`CUDA_STRATEGY.md`: the end-to-end entry now covers both models, the CPU
emulation and the transfer budget.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Stefan Possanner <stefan.possanner@ipp.mpg.de>
@spossann

Copy link
Copy Markdown
Member Author

Has been solved in another PR of stack #762

@spossann spossann closed this Oct 14, 2026
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.

draw_markers does not MPI-sort the markers without sorting boxes (off-rank markers in setup accumulations, segfault on 2 ranks)

1 participant