@@ -90,6 +90,15 @@ table and the iteration-major loop (with its per-iteration alive scan) with:
9090 redundant there (committed endpoints already passed the in-bounds mask test,
9191 and the seed is an in-bounds integer voxel).
9292
93+ 10 . ** Runtime-dispatched FMA specialization** for the common default mode
94+ ` (RK2, convergence, restrict_to_mask) ` . On supported x86 CPUs the extension
95+ dispatches once, before thread fan-out, to a separately compiled tracer that
96+ uses scalar FMA contraction in the nested lerps. All other modes and CPUs use
97+ the portable kernel. Only ` src/cpp/flow/flow_density_fma.cxx ` receives the
98+ ISA flags (` -mavx -mfma ` for GCC/Clang, ` /arch:AVX2 ` for MSVC); the extension
99+ and binding layer remain baseline-safe. The build can disable this path with
100+ ` -C cmake.define.BIOIMAGE_FLOW_FMA_DISPATCH=OFF ` .
101+
93102Chosen defaults (see ` src/bioimage_cpp/flow/_flow.py ` ) are unchanged:
94103
95104``` python
@@ -200,9 +209,46 @@ threads runtime speedup vs 1T
200209Better than the pre-rewrite kernel's 3.57× at 8T; barrier and alive-scan removal
201210let it scale further before hitting the 4-physical-core / SMT ceiling.
202211
212+ ## Runtime FMA dispatch (2026-07-12)
213+
214+ The direct bilinear/trilinear sampler is a chain of scalar lerps. On baseline
215+ x86-64 those lerps compile as separate multiply/add operations; an FMA-enabled
216+ build contracts them and provides a further portable-at-runtime speedup without
217+ changing the flow layout or API.
218+
219+ Paired ` check_flow_density.py ` medians on the AMD EPYC 7513, from the same source
220+ revision and build environment:
221+
222+ | Fixture | Threads | Dispatch OFF | Dispatch ON | Improvement |
223+ | ---| ---:| ---:| ---:| ---:|
224+ | 3D | 1 | 1.4900 s | 1.3084 s | 12.2% |
225+ | 3D | 8 | 0.3035 s | 0.2553 s | 15.9% |
226+ | 2D | 1 | 0.2021 s | 0.1801 s | 10.9% |
227+
228+ The full registered 3D density remained byte-for-byte identical, both stored
229+ reference checks passed with unchanged metrics, and the complete test suite
230+ reported 1057 passed / 8 skipped. A further 400 randomized 2D/3D default-mode
231+ cases, including axis-of-length-one shapes and 1/4 threads, produced identical
232+ aggregate output digests with dispatch enabled and disabled.
233+
234+ Implementation details that matter:
235+
236+ - Runtime feature detection happens in the baseline-compiled caller before the
237+ specialized function is entered.
238+ - GCC/Clang require AVX+FMA; MSVC uses ` /arch:AVX2 ` and therefore also checks AVX2
239+ before dispatch.
240+ - Only the common selector 7 specialization is duplicated. Less common Euler,
241+ no-convergence, and unrestricted modes keep using the portable implementation.
242+ - The FMA translation unit uses a distinct ` CodegenVariant ` template argument.
243+ Without a distinct mangled name, linker COMDAT selection can silently retain
244+ the portable ` trace_all ` instantiation and discard the FMA implementation.
245+ - Automatic ` target_clones ` remains rejected: putting a target boundary around
246+ the driver or chunk inhibited the hot-loop inlining and regressed runtime.
247+
203248## Correctness
204249
205- - All 1067 tests in the package pass; the 17 ` tests/test_flow.py ` cases cover
250+ - The current suite reports 1057 passed / 8 skipped. The 17
251+ ` tests/test_flow.py ` cases cover
206252 2D/3D, Euler/RK2, mask on/off, convergence on/off, single-vs-multithread
207253 equality, non-contiguous inputs, degenerate ` n_iter ` , and invalid inputs.
208254- A differential harness ran 1056 randomized cases — axis-of-length-1 shapes,
@@ -251,6 +297,10 @@ Kept so these avenues are not blindly re-attempted.
251297- ** ` target_clones ` autovectorization SIMD** (earlier, rolled back) — the AVX2
252298 clone emitted only scalar FMA (gcc judged the 8-corner gather unprofitable) and
253299 regressed 8T from icache pressure of three inlined clones.
300+ - ** Current-sample channel prefetching** after interpolation offsets were known
301+ regressed the EPYC 3D fixture by about 5% at 1T and 2% at 8T. The warm-kernel
302+ counter run reported only about 1.45% L1-data load misses, so the extra
303+ prefetch instructions cost more than the avoided demand-load latency.
254304- ** Interleaved (channel-last) layout + hand-written AVX2 FMA** (earlier, rolled
255305 back, 2026-06-12) — codegen was confirmed optimal via ` objdump ` (8× packed
256306 ` vfmadd132ps ` , zero gathers) yet was no faster than scalar, and ` VS=4 ` padding
@@ -288,11 +338,12 @@ Lower priority than the landed work; all need a fixed-clock host with `perf`
288338 several kernel calls) to confirm where the remaining cycles go (front-end vs
289339 back-end memory vs back-end core) and to provide a low-noise environment for
290340 evaluating (1).
291- 3 . ** Cross-architecture confirmation** of the truncation gain magnitude on arm64
292- (macOS AppleClang, Linux aarch64) and Windows MSVC. The particle-major and
293- direct-interpolation gains are portable C++20; the truncation magnitude is the
294- most microarchitecture-dependent (it depends on how ` floor ` is lowered without
295- SSE4.1 / on the arm equivalent).
341+ 3 . ** Cross-architecture confirmation** on macOS x86-64 and Windows x86-64 for
342+ the FMA dispatch, plus the truncation gain magnitude on arm64 (macOS
343+ AppleClang and Linux aarch64). The particle-major and direct-interpolation
344+ gains are portable C++20; the x86 dispatch is compiler-specific and the
345+ truncation magnitude depends on how ` floor ` is lowered without SSE4.1 / on
346+ the arm equivalent.
296347
297348## Reproducing these measurements
298349
0 commit comments