9b737b8GPU implementation of fmm3dbie's interior Helmholtz impedance solver, with a showcase room problemJeremy Magland 1# fmm3dbie on the GPU: a proof of concept
bb9c969Add the cymbal problem: a listener near a rigid cymbal in free spaceJeremy Magland 3fmm3dbie solves boundary integral equations on surfaces in three dimensions with high-order quadrature and the fast multipole method. On a CPU, most of its run time for the Helmholtz equation goes into computing the near-field quadrature corrections, and the rest into FMM-accelerated matrix-vector products inside GMRES. Here we present a GPU implementation of one fmm3dbie solver, the Helmholtz impedance problem (`helm_rpcomb_imp_solver`), in its interior and exterior modes. It uses the same integral equation and the same surface discretization, together with a showcase problem that runs on both codes so that their speed and accuracy can be compared directly.
9b737b8GPU implementation of fmm3dbie's interior Helmholtz impedance solver, with a showcase room problemJeremy Magland 4
5The showcase is a point source at a fixed frequency in a closed room with partially absorbing walls. The room is a slightly rounded cube, the superellipsoid |x|⁸ + |y|⁸ + |z|⁸ = 1. With time dependence e^{-iωt}, the pressure amplitude satisfies Δu + k²u = −δ(x − x₀) inside the room and ∂u/∂n + ikλu = 0 on the walls, with outward normal n and λ = −β. Here β > 0 is the normalized wall admittance, and smaller β gives more reflective walls.
7
bb9c969Add the cymbal problem: a listener near a rigid cymbal in free spaceJeremy Magland 9A second showcase, a listener near a rigid cymbal in free space, is described in [The cymbal problem](#the-cymbal-problem). It uses the exterior mode of the same solver, and both backends run it.
9b737b8GPU implementation of fmm3dbie's interior Helmholtz impedance solver, with a showcase room problemJeremy Magland 11A single precision port of the same solver to WebGPU, which runs in a browser without CUDA, is in [fmm3dbie-webgpu-poc](https://vault1.magland.org/proof-of-concept/fmm3dbie-webgpu-poc).
13## What is being compared
15Both backends solve for the correction v = u − G, where G is the free-space field of the source. The representation is v = S_k[σ] + iα D_k S_{i|k|}[σ], and the equation is fmm3dbie's discretized impedance equation. Both use the same 6m² quadrilateral patches (m × m on each face of the cube) of order p, with (p+1)² Gauss–Legendre nodes each, so the unknowns are the same.
17- `fmm3dbie` runs fmm3dbie itself through ctypes (`reference/`), with the quadrature and FMM tolerance `--eps` (default 1e-4).
18- `gpu` runs `fmm3dbie_gpu/`, a CuPy program with CUDA kernels compiled at run time. It does not use fmm3dbie.
20The GPU code differs from fmm3dbie in four ways, which also explain its speed and accuracy.
221. Self-patch quadrature. fmm3dbie uses precomputed generalized Gaussian rules. We use polar coordinates on the four triangles fanned from the target to the patch edges, with a sinh substitution along each edge. This handles targets close to an edge, needs no tables, and computes every (target, patch) pair in one thread block.
232. Near-patch quadrature. We use a quadtree on the reference square, refined by distance and by wavelength, with an 8 × 8 Gauss–Legendre rule per leaf. This is fmm3dbie's distance-based strategy rather than its adaptive one.
243. Far field. Patches that are not near a target are summed directly, O(N²), rather than with an FMM. Near patches are excluded from this sum, so it has no large cancelling terms and runs in single precision, with partial sums accumulated in double precision.
254. Geometry. For the rounded cube we evaluate the exact surface inside the integrals. fmm3dbie uses polynomial interpolants of the node positions and tangents. The interpolants are slightly inconsistent with each other, and the double-layer kernel amplifies this at nodes next to patch edges, by up to 0.6% of the local integral in our tests. With the exact geometry the GPU solution is more accurate than fmm3dbie's on the same nodes. `--interp-geometry` switches to the interpolated model for a like-for-like comparison.
27## Quick start
29The fmm3dbie backend needs gfortran, make and OpenBLAS; the GPU backend needs an NVIDIA GPU with a CUDA 12 driver. Both need Python 3.9 or later with NumPy, and the plots need Matplotlib.
31 scripts/build_fmm3dbie.sh # clones and builds ~/src/fmm3dbie (or set FMM3DBIE_LIB)
32 scripts/setup_gpu.sh # CuPy and CUDA libraries as pip wheels, if no CUDA toolkit
33 source scripts/gpuenv.sh
35 python3 run.py --backend gpu --case small --selftest
36 python3 run.py --backend fmm3dbie --case small --selftest
37 python3 compare.py
38 python3 plot.py results/small-gpu
bb9c969Add the cymbal problem: a listener near a rigid cymbal in free spaceJeremy Magland 40 python3 run.py --problem cymbal --backend gpu --case large --selftest --reciprocity
41 python3 run.py --problem cymbal --backend fmm3dbie --case large --selftest --reciprocity
42 python3 plot.py results/cymbal-large-gpu
9b737b8GPU implementation of fmm3dbie's interior Helmholtz impedance solver, with a showcase room problemJeremy Magland 44Each run writes `results/<case>-<backend>/summary.json`, with timings, iteration counts, errors and machine information, and `field.npz`. `compare.py` tabulates all runs and, for every case run with both backends, reports the speedup and the relative difference of the two wall fields. It also writes `results/comparison.md`. The runs do not have to be on the same machine: copy the result directories together before comparing. For a fair speedup, though, run fmm3dbie on the same machine as the GPU and with all of its cores (`OMP_NUM_THREADS`).
46The cases are
48| case | k | wall admittance β | room width | patches | order | points |
49|---|---|---|---|---|---|---|
50| small | 10 | 0.2 | 3.2 wavelengths | 96 | 6 | 4,704 |
51| medium | 10 | 0.2 | 3.2 wavelengths | 216 | 8 | 17,496 |
52| large | 40 | 0.05 | 12.7 wavelengths | 600 | 8 | 48,600 |
54and `--k`, `--beta`, `--m` and `--norder` override them. Run `python3 run.py --help` for the other options.
56## What each run reports
58- Timings. For the GPU, *setup* is the near quadrature for the operator and *solve* is GMRES. The one-time loading of the compiled CUDA kernels is reported separately and excluded. fmm3dbie computes its near quadrature inside the solver call, so its setup is zero and its solve includes it. Compare *setup + solve*.
59- Self-test (`--selftest`). This solves a second problem whose solution is known, the field of a source outside the room, and reports the relative error at 50 interior points and on the walls. This is the accuracy measure. Note that the GMRES tolerance (`--eps-gmres`, default 1e-6) bounds it from below.
60- Energy balance. The power radiated by the source, k/(4π) + Im v(x₀), must equal the power absorbed by the walls, −k Re(λ)∫|u|² dS. Their relative mismatch is a check that needs no exact solution.
62## Results
64Both backends were run with `run.py`, at the default settings, on the same workstation. That machine has an Intel Xeon w5-3435X (16 cores, 32 threads, all used by fmm3dbie) and an NVIDIA RTX 6000 Ada (48 GB). fmm3dbie was built at commit 94cb732 and run with eps = 1e-4. The GMRES tolerance was 1e-6 for both. The table is `results/comparison.md` as written by `compare.py`.
66| case | points | fmm3dbie solve | GPU setup + solve | speedup | GMRES iterations | self-test error, fmm3dbie / GPU | wall field difference |
67|---|---|---|---|---|---|---|---|
68| small | 4,704 | 4.7 s | 0.16 + 0.22 s | 12x | 24 | 1.5e-5 / 2.2e-6 | 3.8e-5 |
69| medium | 17,496 | 18.4 s | 0.48 + 0.60 s | 17x | 24 | 1.5e-6 / 1.1e-6 | 1.4e-6 |
70| large | 48,600 | 214 s | 1.6 + 9.2 s | 20x | 110 | 1.7e-5 / 1.0e-5 | 1.9e-5 |
72The two codes need the same number of GMRES iterations, which is consistent with their discretizing the same operator. Their wall fields agree to about the accuracy of each. The speedup grows with the problem size, because the fixed costs of the GPU code become less important. On the large case, 85% of the GPU time is GMRES. Most of that is presumably the O(N²) far-field sum, which is the part an FMM would reduce. The CUDA kernels are compiled or loaded once per process, taking about 2 s, and this is excluded from the GPU times. fmm3dbie's first solve in a process can be slower than its second: on the medium case the self-test solve took 81 s and the room solve 18 s. We report the room solve.
74At the default GMRES tolerance both codes' self-test errors are close to the tolerance. With a GMRES tolerance of 1e-10 the medium-case GPU error is 2.7e-7 with the single precision far field and 2.9e-10 with `--far64`. In an earlier run on a laptop, fmm3dbie reached 1.9e-8 there with eps 1e-7, at about four times the cost of eps 1e-4 (400 s against 92 s).
bb9c969Add the cymbal problem: a listener near a rigid cymbal in free spaceJeremy Magland 76## The cymbal problem
78The room problem puts the unknowns on a closed surface around the region of interest. A common acoustic question has the opposite structure: a small obstacle in free space, and a listener. Here we consider a listener near a cymbal, which we treat as stationary and sound-hard. We place a point source at the listener's position L and solve Δu + k²u = −δ(x − L) outside the cymbal, with ∂u/∂n = 0 on it and the radiation condition at infinity. By reciprocity, u(x) is also the pressure at the listener due to a unit source at x, so one solve gives the listener's response to a source anywhere around the cymbal. We evaluate u on a 3D grid in a bounding box (a 1 m cube around the cymbal) and on three planes through it. The box only selects where the field is evaluated: the integral representation satisfies the radiation condition, so, unlike a volume discretization, the problem needs no truncation or absorbing layer.
80fmm3dbie's integral equations need a closed surface. A zero-thickness cymbal would need an open-surface formulation, with hypersingular operators and a density that is singular at the edge, which fmm3dbie does not provide. We therefore model the cymbal as a thin closed body (`fmm3dbie_gpu/cymbal.py`). An axisymmetric midsurface with a raised bell and a shallow bow is offset by ±T/2 along its normal, where the thickness T decreases from 20 mm at the center to 10 mm at the rim, and a half-torus of diameter T closes the body at the rim. The cymbal is 40 cm across, so the rim thickness is 1/40 of the diameter; a real cymbal is closer to 1/400, and `--t-rim` sets it. The faces are meshed with an O-grid (an equiangular central square of at least 2 × 2 patches and rings of patches around it), graded toward the rim and refined across the bell. Every patch lies on one face or on the rim, so each is analytic, although the curvature jumps where the faces meet the rim. The rings are not conforming, which neither code requires. Node derivatives are computed by the complex step method, and at order 8 the surface reproduces the body's volume to 3e-9.
82The equation is the exterior mode of the same solver, `helm_rpcomb_imp_solver` with ifinout = 1 and λ = 0, and the GPU code needed no change to its operator for it. The GPU code uses the interpolated geometry for the cymbal, as fmm3dbie does, since no exact chart has been written for it in CUDA. The cases are in meters, with c = 343 m/s.
84| case | frequency | diameter | target patch size | order | patches | points |
85|---|---|---|---|---|---|---|
86| small | 1 kHz | 1.2 wavelengths | 6 cm | 6 | 388 | 19,012 |
87| medium | 3 kHz | 3.5 wavelengths | 4 cm | 8 | 444 | 35,964 |
88| large | 8 kHz | 9.3 wavelengths | 3 cm | 8 | 524 | 42,444 |
90`--freq`, `--h`, `--norder`, `--t-rim` and `--listener` override them. The listener is at (0.25, 0, 0.45) m by default, above the cymbal and outside its rim.
92There are three checks. The self-test uses about 5,000 point sources spread 3 mm apart over the midsurface out to r = 12 cm, with smoothly tapering strengths; their field is an exact solution outside. A single interior source would be at most 1 cm from the surface, and its data would vary on that scale, which the main problem's data does not. The energy balance compares the power emitted by the source, k/(4π) + Im v(L), with the power radiated to infinity, k∫|u∞|²dΩ, with the far-field pattern u∞ computed from the densities at the nodes. A rigid cymbal absorbs nothing, so the two must agree. `--reciprocity` adds a solve with the source at a point B below the cymbal and compares the scattered field at B due to the listener with the field at the listener due to B.
94Both backends were run on the same workstation as the room problem (Xeon w5-3435X with all 32 threads for fmm3dbie, eps = 1e-4; RTX 6000 Ada for the GPU), with GMRES tolerance 1e-6.
96| case | fmm3dbie solve | GPU setup + solve | speedup | GMRES iterations | self-test error, fmm3dbie / GPU | reciprocity, fmm3dbie / GPU | surface field difference |
97|---|---|---|---|---|---|---|---|
98| small | 27.0 s | 0.78 + 0.99 s | 15x | 25 | 4.3e-6 / 1.5e-5 | 2.0e-5 / 7.7e-5 | 6.6e-5 |
99| medium | 71.9 s | 1.31 + 1.89 s | 23x | 32 | 9.2e-7 / 1.1e-6 | 2.0e-6 / 5.2e-6 | 2.4e-6 |
100| large | 80.6 s | 1.50 + 1.82 s | 24x | 24 | 2.4e-5 / 4.6e-6 | 3.5e-6 / 7.0e-7 | 9.7e-6 |
102The energy mismatch was below 1.1e-6 for every run. The GMRES iteration counts are the same for both codes and do not grow with the frequency, which suggests that at this thickness the thin body does not degrade the conditioning. On the small case the GPU's discretization error is about three times fmm3dbie's. It does not depend on any of the GPU's quadrature parameters (near-field radius, quadtree refinement, rule orders, far-field oversampling) or on the far-field precision, and on a finer mesh (h = 4 cm, order 8, at 1 kHz) the two codes agree to 3e-6, so we take it to be a difference between two consistent discretizations rather than a quadrature error; we have not found its source. On the large case the GPU is the more accurate of the two. Note that the first version of the mesh had a single central patch at h = 6 cm, which left thin slivers of the first ring at the square's corners; with at least 2 × 2 central patches the small-case errors of both codes fell by a factor of 5 to 20.
104
106The figure shows the large case. The bottom row is the level relative to the free field: 0 dB where the cymbal has no effect, about +6 dB in the interference fringes above it, and a shadow of more than 15 dB below it.
108Several limitations are specific to this problem. The rim thickness is 1/40 of the diameter, ten times a real cymbal's; thinner bodies need more near-quadrature refinement, and we have not checked how the iteration counts behave. The rim is rounded, whereas a real cymbal's edge is closer to square. The cymbal is rigid, so its own vibration, which is how it makes sound, is not modeled.
9b737b8GPU implementation of fmm3dbie's interior Helmholtz impedance solver, with a showcase room problemJeremy Magland 110## Files
112- `run.py`, `compare.py`, `plot.py`: run one backend, compare the results, and plot a field.
bb9c969Add the cymbal problem: a listener near a rigid cymbal in free spaceJeremy Magland 113- `problems/room.py`, `problems/cymbal.py`: the two showcase problems, shared by both backends: cases, surfaces, sources, self-tests and checks. `problems/common.py` has the Green's function and other shared helpers.
9b737b8GPU implementation of fmm3dbie's interior Helmholtz impedance solver, with a showcase room problemJeremy Magland 114- `fmm3dbie_gpu/surface.py`: patch discretization and the rounded cube. It is pure NumPy.
bb9c969Add the cymbal problem: a listener near a rigid cymbal in free spaceJeremy Magland 115- `fmm3dbie_gpu/cymbal.py`: the cymbal's shape and its patch mesh, also pure NumPy. Both backends build the cymbal from it.
9b737b8GPU implementation of fmm3dbie's interior Helmholtz impedance solver, with a showcase room problemJeremy Magland 116- `fmm3dbie_gpu/cuda_ops.py`: the CUDA kernels: near pairs, quadtrees, near weights, far-field sum and near-field product.
117- `fmm3dbie_gpu/solver.py`: the operator, evaluation, and GMRES, following fmm3dbie's `zgmres_guru`.
118- `reference/`: the ctypes interface to fmm3dbie and the same rounded-cube surface built with fmm3dbie's routines. We use it because the Python bindings shipped with fmm3dbie need `numpy.distutils`, which NumPy 2 no longer has.
119- `tests/test_quadrature.py`: compares the GPU near weights with brute-force integration. For example, `python3 tests/test_quadrature.py 8 40 m=10 tp=45` runs at order 8 and k = 40.
120- `tests/geometry_test.py`: checks Green's identity at nodes next to patch edges with the interpolated, derivative-consistent and exact geometry models. This is the evidence for point 4 above.
122## Limitations
124The far field is a direct sum, so each product costs O(N²). At 48,600 points one product takes about 40 ms. Somewhere between 10⁵ and 2·10⁵ points a GPU FMM becomes necessary; FMM3D's Helmholtz scheme is the obvious model. The near weights would not change, since the FMM would only replace the dense sum.
126The exact geometry exists only for the rounded cube. General surfaces use the interpolated geometry, and with it fmm3dbie's sensitivity at nodes next to patch edges.
128The self-patch rule has been checked against brute-force integration for orders 6 and 8 and for k up to 40, but not for higher orders or more strongly curved patches.
130Only this one solver is ported. The two codes are compared on one family of problems, and the relative speed will differ for other geometries, orders and tolerances.