Files
OneCAD-Simulation/docs/lbm-gpu-performance.md
T
Christian Svensson 60fd0998f5 lbm-gpu: stream in place with the AA pattern
The collide and stream passes each moved the whole population field
through two buffers (304 bytes per cell update). The AA pattern (Bailey
et al. 2009; Wittmann et al. 2013) keeps one buffer: even steps collide
each cell in place with its directions swapped, odd steps read a cell's
incoming populations at its neighbours and write the outgoing ones where
the next step reads them, so each thread writes the addresses it read
and streaming happens inside the collision (152 bytes). A cell's
coordinates are computed once and neighbours wrap with a compare and an
add. The per-link wall rules gather their sources before a scatter
writes them, so no link overwrites another's post-collision value, and
the momentum exchange is taken in the gather of the step whose forces
are read back.

Options::streaming picks the layout; Auto, what a solve uses, streams
in place, which measured faster on every device tried: 1.5x on an RTX
4090, 1.85x on the GB10 and 1.45x on an M4 Max at periodic 128^3.
Options::profile times each kernel with timestamp queries for the
throughput test, which now runs both layouts. The verification tests
run in both layouts, with two new ones comparing fields, mass, forces
and heat with the CPU after odd and even step counts across submission
boundaries.

Part of #562.
2026-10-17 10:45:09 -04:00

2.9 KiB
Raw Blame History

GPU lattice Boltzmann: streaming layout and performance

The momentum populations stream in one of two layouts, chosen per flow with fieldcraft_lbm_gpu::Options::streaming:

  • In place (the AA pattern: Bailey, Myre, Walsh, Lilja and Saar, "Accelerating lattice Boltzmann fluid flow simulations using graphics processors", ICPP 2009; Wittmann, Zeiser, Hager and Wellein, Comput. Math. Appl. 65 (2013) 924–935, §6). One population buffer. Even steps read population q of cell x at (q, x) and write the collided one at (opposite(q), x); odd steps read it at (opposite(q), x − c_q) and write it at (q, x + c_q). In both, a cell writes exactly the addresses it read, so cells never race, and streaming happens inside the collision: 152 population bytes per cell update.
  • Two buffers (A–B). Collide in place, then pull into the other buffer: 304 bytes per update.

The wall, reflection and open-face rules are the same per-link rules in both. In place, a gather pass reads every link's post-collision source into a scratch value before a scatter pass writes, so no link overwrites another's source; the momentum exchange is taken in the gather of the step whose forces are read back (a submission's last). The temperature lattice is two-buffer in both.

Streaming::Auto, what a solve uses, streams in place: it was faster on every device measured (the ignored throughput test, million cell updates per second, means of three runs; the 4090 row is from the first version of this layout, before its addressing was made cheaper):

device case in place two buffers
RTX 4090, Vulkan periodic 128³ 3897 2578
RTX 4090, Vulkan sphere 128³ 3572 2441
GB10 (unified memory), Vulkan periodic 128³ 1224 656
GB10 (unified memory), Vulkan sphere 128³ 1146 630
M4 Max, Metal periodic 128³ 1364 937
M4 Max, Metal sphere 128³ 1256 872

On the 4090 and the GB10 the two-buffer step is bound by memory bandwidth (~0.8 TB/s and ~0.2 of the GB10's 0.27), so halving the bytes pays directly; on the M4 Max it gains once the in-place addressing is cheap (a cell's coordinates once, neighbours wrapped by compare and add). With 128³ cells in place also needs one 152 MiB population buffer less. 64³ fits in cache, where the two layouts are closer and vary more between runs.

Measuring

cargo test --release -p fieldcraft-lbm-gpu --test throughput -- --ignored --nocapture

runs each case in both layouts on the device FIELDCRAFT_GPU picks, marks the one Auto chooses and prints how long creating the pipelines took (on Metal this reads near zero: the driver compiles lazily or from its cache, so the cost lands in the first dispatches instead). With FIELDCRAFT_LBM_GPU_PROFILE=1 it also prints each kernel's GPU time (Options::profile, timestamp queries; the throughput is then not representative, as every dispatch gets its own pass).