Skip to content

IBM particle-laden: force exchange ships ~96% zeros; a ghost-particle exchange would be ~3000x smaller #1703

Description

@sbryngelson

Static read of master while inspecting the multi-rank IBM for a large particle-laden weak-scaling study. The immersed-boundary force/torque exchange moves a great deal more data than the problem requires, and a standard ghost-particle exchange looks like it would replace it at a tiny fraction of the cost. Writing up the arithmetic so the assumptions can be checked. This is static analysis, not profiling — please push back where the model is wrong.

Reference configuration throughout: 7500 spherical IBs per rank, particle diameter 10 cells, ib_neighborhood_radius = 1 (the default), $300^3$ cells per rank, 3D, TVD-RK3, 8 ranks/node. wp is compile-time selectable in m_precision_select.f90; numbers below assume double, so halve them for a single-precision build.

1. Every pass sends the whole neighborhood array

s_communicate_ib_forces in src/simulation/m_ibm.fpp packs all tracked IBs on each pass:

do i = 1, num_ibs
    send_ids(i) = patch_ib(i)%gbl_patch_id
    send_ft(1:3,i) = forces(i,:)
    send_ft(4:6,i) = torques(i,:)
end do

num_ibs is the neighborhood count, not the owned count — m_start_up.fpp filters with f_neighborhood_ranks_own_location, so at R=1 it spans a 3x3x3 block of subdomains: 27 x 7500 = 202,500 entries, giving a 10.5 MB message. There are 4R passes per dimension (2R accumulate + 2R back-propagate), 12 per RK substep, and s_propagate_immersed_boundaries(s) runs per substep:

one message 10.5 MB
per rank per RK substep 126 MB
per rank per timestep (RK3) 379 MB
per node per timestep 3.0 GB
entries with a nonzero local contribution ~7,500 of 202,500 (3.7%)

A rank only has contributions for IBs overlapping its own subdomain. About 96% of the payload is zeros, re-sent 36 times per timestep. This is comparable to or larger than the fluid halo exchange for the same configuration, though that comparison depends on sys_size and buff_size so I am not putting a precise ratio on it.

2. A ghost-particle exchange would be dramatically smaller

The force on a particle only needs summing over the ranks whose subdomains it actually intersects — at most 2 per direction for a 10-cell particle in a 300-cell subdomain. It does not need to reach every rank in the neighborhood.

With centroids uniform, a particle is wholly interior with probability $(1 - 10/300)^3 = 0.903$. So per rank:

  • ~6,775 of 7,500 particles need no communication at all
  • ~725 straddle a boundary; expected neighbour-sends ~775 (crossing k dimensions implies $2^k - 1$ neighbours)
approach volume per substep rounds
current sweep 126 MB 12
straddlers to all 26 neighbours (conservative) 0.98 MB 1
straddlers to only the ranks they intersect 40 KB 1

That is ~130x for the lazy version and ~3100x for the targeted one, in a single MPI_Neighbor_alltoallv instead of 12 sequential rounds. Per timestep: 379 MB becomes ~121 KB.

This is the ghost/halo particle pattern standard in parallel DEM and MD. The pieces already exist in the tree — f_local_rank_owns_location is used exactly this way in m_collisions.fpp to make one rank responsible for each collision pair.

This also reframes the O(R) argument. The sweep is O(R) in message count versus O(R^d) for reaching every neighbourhood rank directly, which is its motivation. But reaching every neighbourhood rank is not required; only the ranks a given particle intersects are. Once the exchange is sparse, the quantity being minimised is no longer the binding cost.

3. Host round-trip on every pass

forces and torques are local automatic arrays (m_ibm.fpp:981), not in a persistent device region, and the unpack loop carries

$:GPU_PARALLEL_LOOP(..., copy='[forces, torques, recv_forces_snap, recv_torques_snap]')

so four arrays of num_ibs * 3 reals move host<->device on every pass: roughly 39 MB x 12 passes x 3 substeps ~ 1.4 GB per rank per timestep. Cray MPICH supports GPU-aware MPI (MPICH_GPU_SUPPORT_ENABLED=1); device pointers could go straight to MPI_SENDRECV. Several copy= clauses also look stronger than needed and want copyin/copyout.

4. Rank placement is not topology-aware

MPI_CART_CREATE in src/common/m_mpi_common.fpp passes reorder = .false., so the Cartesian grid maps row-major onto MPI_COMM_WORLD. Neighbour separation at 8 ranks/node:

ranks z-neighbour y-neighbour x-neighbour
4096 (16^3) same node 2 nodes 32 nodes
32768 (32^3) same node 4 nodes 128 nodes
74088 (42^3) same node 5.25 nodes 220 nodes

At scale the x-direction sweep crosses ~220 nodes of a dragonfly on each of its passes. This grows monotonically with rank count, which is the right shape for degradation that only shows up at large node counts.

5. Ownership handoff is latency-bound, not bandwidth-bound

Worth separating from the above, because it is often assumed to be the expensive part. s_handoff_ib_ownership broadcasts only newly-owned patches, so for particles that move slowly relative to a subdomain the payload is nearly empty. What it does cost is 26 neighbour rounds per substep, so ~78 latency exposures per timestep, which interacts badly with item 4.

It also does allocate(send_buf(buf_size), recv_bufs(buf_size, max_nbrs)) on every call, with buf_size sized for num_local_ibs_max and 26 receive buffers. Hoisting that allocation looks free.

6. Global reduction each step under cfl_adap_dt

m_time_steppers.fpp calls s_mpi_allreduce_min(dt_local, dt) in s_compute_dt, dispatched every step under cfl_adap_dt (only at t_step == 0 under cfl_const_dt). At tens of thousands of ranks that is a global min-reduction and a hard synchronisation point per step, which also exposes load imbalance.

Suggested order

  1. Sparse ghost-particle exchange (item 2) — the large one, and it subsumes much of the rest.
  2. GPU-aware MPI (item 3) — no algorithm change.
  3. Topology-aware rank mapping (item 4) — reorder = .true. or MPICH_RANK_REORDER_METHOD.
  4. Fuse the IB exchange into the existing fluid halo exchange — same neighbours, same cadence, removes a set of separate latency rounds.
  5. Persistent neighbour collectives — the pattern is static between ownership changes, so MPI_Neighbor_alltoallv_init can amortise setup.
  6. Hoist the handoff allocations (item 5), and relax the dt reduction (item 6) where CFL margin allows.

Each is independently measurable.

Related observations

  • The back-propagation loop uses min(2*ib_neighborhood_radius, num_procs_x - 1), the same count as accumulation... each rank appears to end with a window offset by R. Withdrawn — this was wrong, see the correction below. The 2R count is correct and exactly minimal; I had missed that updates are gated per particle by s_get_neighborhood_idx, which prevents a particle's data from propagating outside its own neighbourhood. Nothing in items 1-6 depends on this.
  • s_detect_ib_collisions (the ghost-point detector) is commented out in src/simulation/m_collisions.fpp in favour of s_detect_ib_collisions_n2. At neighbourhood-scale num_ibs the quadratic path is very expensive for any collisional run. (This one stands.)

Related: #1536 also concerns ib_neighborhood_radius. See also #1704 for a lubrication correction proposal.


Edited after filing: added the ghost-particle comparison in item 2, which changes the recommended order; separated the ownership handoff (item 5) as latency- rather than bandwidth-bound; added the precision caveat; removed a specific fluid-halo ratio I could not pin down; and struck the back-propagation observation, which was incorrect.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions