Skip to content

Commit 0642805

Browse files
Implement optimized kernel dispatch for flow computation
1 parent 5b4bcdc commit 0642805

4 files changed

Lines changed: 240 additions & 8 deletions

File tree

CMakeLists.txt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,36 @@ target_compile_options(_core PRIVATE
3636
$<$<NOT:$<CONFIG:Debug>>:-O3>
3737
)
3838

39+
option(
40+
BIOIMAGE_FLOW_FMA_DISPATCH
41+
"Build an x86 FMA-specialized flow tracer with runtime CPU dispatch"
42+
ON
43+
)
44+
if(BIOIMAGE_FLOW_FMA_DISPATCH)
45+
string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" _bioimage_system_processor)
46+
if(_bioimage_system_processor MATCHES "^(x86_64|amd64|i[3-6]86)$")
47+
if(MSVC)
48+
target_sources(_core PRIVATE src/cpp/flow/flow_density_fma.cxx)
49+
set_property(
50+
SOURCE src/cpp/flow/flow_density_fma.cxx
51+
APPEND PROPERTY COMPILE_OPTIONS /arch:AVX2
52+
)
53+
target_compile_definitions(
54+
_core PRIVATE
55+
BIOIMAGE_FLOW_FMA_DISPATCH=1
56+
BIOIMAGE_FLOW_FMA_REQUIRES_AVX2=1
57+
)
58+
elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
59+
target_sources(_core PRIVATE src/cpp/flow/flow_density_fma.cxx)
60+
set_property(
61+
SOURCE src/cpp/flow/flow_density_fma.cxx
62+
APPEND PROPERTY COMPILE_OPTIONS -mavx -mfma
63+
)
64+
target_compile_definitions(_core PRIVATE BIOIMAGE_FLOW_FMA_DISPATCH=1)
65+
endif()
66+
endif()
67+
endif()
68+
3969
option(BIOIMAGE_PROFILE "Enable per-phase profiling instrumentation (development only)" OFF)
4070
if(BIOIMAGE_PROFILE)
4171
target_compile_definitions(_core PRIVATE BIOIMAGE_PROFILE)

development/flow/PERFORMANCE_NOTES.md

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
93102
Chosen defaults (see `src/bioimage_cpp/flow/_flow.py`) are unchanged:
94103

95104
```python
@@ -200,9 +209,46 @@ threads runtime speedup vs 1T
200209
Better than the pre-rewrite kernel's 3.57× at 8T; barrier and alive-scan removal
201210
let 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

include/bioimage_cpp/flow/flow_density.hxx

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
#include <cstdint>
1111
#include <vector>
1212

13+
#if defined(BIOIMAGE_FLOW_FMA_DISPATCH) && defined(_MSC_VER)
14+
#include <immintrin.h>
15+
#include <intrin.h>
16+
#endif
17+
1318
namespace bioimage_cpp::flow {
1419
namespace detail {
1520

@@ -233,7 +238,12 @@ inline void trace_particle(
233238
}
234239
}
235240

236-
template <std::size_t D, bool UseRK2, bool CheckConvergence, bool RestrictToMask>
241+
// CodegenVariant gives separately compiled ISA variants a distinct linker
242+
// identity. Without it, COMDAT selection may replace an FMA instantiation with
243+
// the portable instantiation that has the otherwise-identical template name.
244+
template <
245+
std::size_t D, bool UseRK2, bool CheckConvergence, bool RestrictToMask,
246+
bool CodegenVariant = false>
237247
void trace_all(
238248
std::vector<std::array<float, D>> &positions,
239249
const std::array<const float *, D> &channels,
@@ -261,6 +271,90 @@ void trace_all(
261271
);
262272
}
263273

274+
#if defined(BIOIMAGE_FLOW_FMA_DISPATCH)
275+
276+
inline bool runtime_fma_supported() noexcept {
277+
static const bool supported = []() noexcept {
278+
#if defined(_MSC_VER)
279+
int registers[4]{};
280+
__cpuid(registers, 1);
281+
constexpr int fma_bit = 1 << 12;
282+
constexpr int osxsave_bit = 1 << 27;
283+
constexpr int avx_bit = 1 << 28;
284+
if ((registers[2] & (fma_bit | osxsave_bit | avx_bit)) !=
285+
(fma_bit | osxsave_bit | avx_bit)) {
286+
return false;
287+
}
288+
if ((_xgetbv(0) & 0x6) != 0x6) {
289+
return false;
290+
}
291+
#if defined(BIOIMAGE_FLOW_FMA_REQUIRES_AVX2)
292+
__cpuidex(registers, 7, 0);
293+
constexpr int avx2_bit = 1 << 5;
294+
if ((registers[1] & avx2_bit) == 0) {
295+
return false;
296+
}
297+
#endif
298+
return true;
299+
#elif defined(__GNUC__) || defined(__clang__)
300+
return __builtin_cpu_supports("avx") && __builtin_cpu_supports("fma");
301+
#else
302+
return false;
303+
#endif
304+
}();
305+
return supported;
306+
}
307+
308+
void trace_all_fma_2d(
309+
std::vector<std::array<float, 2>> &positions,
310+
const std::array<const float *, 2> &channels,
311+
const GridLayout<2> &grid,
312+
const std::uint8_t *mask,
313+
std::size_t n_threads,
314+
std::size_t n_iter,
315+
float dt,
316+
float tol
317+
);
318+
319+
void trace_all_fma_3d(
320+
std::vector<std::array<float, 3>> &positions,
321+
const std::array<const float *, 3> &channels,
322+
const GridLayout<3> &grid,
323+
const std::uint8_t *mask,
324+
std::size_t n_threads,
325+
std::size_t n_iter,
326+
float dt,
327+
float tol
328+
);
329+
330+
template <std::size_t D>
331+
bool try_trace_all_fma(
332+
std::vector<std::array<float, D>> &positions,
333+
const std::array<const float *, D> &channels,
334+
const GridLayout<D> &grid,
335+
const std::uint8_t *mask,
336+
const std::size_t n_threads,
337+
const std::size_t n_iter,
338+
const float dt,
339+
const float tol
340+
) {
341+
if (!runtime_fma_supported()) {
342+
return false;
343+
}
344+
if constexpr (D == 2) {
345+
trace_all_fma_2d(
346+
positions, channels, grid, mask, n_threads, n_iter, dt, tol
347+
);
348+
} else {
349+
trace_all_fma_3d(
350+
positions, channels, grid, mask, n_threads, n_iter, dt, tol
351+
);
352+
}
353+
return true;
354+
}
355+
356+
#endif
357+
264358
} // namespace detail
265359

266360
// Preconditions (validated in the binding layer):
@@ -346,7 +440,25 @@ void compute_flow_density(
346440
BIOIMAGE_FLOW_TRACE(4, true, false, false)
347441
BIOIMAGE_FLOW_TRACE(5, true, false, true)
348442
BIOIMAGE_FLOW_TRACE(6, true, true, false)
349-
BIOIMAGE_FLOW_TRACE(7, true, true, true)
443+
case 7:
444+
#if defined(BIOIMAGE_FLOW_FMA_DISPATCH)
445+
if (detail::try_trace_all_fma<D>(
446+
positions,
447+
channels,
448+
grid,
449+
fg_mask.data,
450+
n_threads,
451+
n_iter,
452+
dt,
453+
tol
454+
)) {
455+
break;
456+
}
457+
#endif
458+
detail::trace_all<D, true, true, true>(
459+
positions, channels, grid, fg_mask.data, n_threads, n_iter, dt, tol
460+
);
461+
break;
350462
}
351463
#undef BIOIMAGE_FLOW_TRACE
352464
}

src/cpp/flow/flow_density_fma.cxx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#include "bioimage_cpp/flow/flow_density.hxx"
2+
3+
#if !defined(BIOIMAGE_FLOW_FMA_DISPATCH)
4+
#error "flow_density_fma.cxx must only be built with BIOIMAGE_FLOW_FMA_DISPATCH"
5+
#endif
6+
7+
namespace bioimage_cpp::flow::detail {
8+
9+
void trace_all_fma_2d(
10+
std::vector<std::array<float, 2>> &positions,
11+
const std::array<const float *, 2> &channels,
12+
const GridLayout<2> &grid,
13+
const std::uint8_t *mask,
14+
const std::size_t n_threads,
15+
const std::size_t n_iter,
16+
const float dt,
17+
const float tol
18+
) {
19+
trace_all<2, true, true, true, true>(
20+
positions, channels, grid, mask, n_threads, n_iter, dt, tol
21+
);
22+
}
23+
24+
void trace_all_fma_3d(
25+
std::vector<std::array<float, 3>> &positions,
26+
const std::array<const float *, 3> &channels,
27+
const GridLayout<3> &grid,
28+
const std::uint8_t *mask,
29+
const std::size_t n_threads,
30+
const std::size_t n_iter,
31+
const float dt,
32+
const float tol
33+
) {
34+
trace_all<3, true, true, true, true>(
35+
positions, channels, grid, mask, n_threads, n_iter, dt, tol
36+
);
37+
}
38+
39+
} // namespace bioimage_cpp::flow::detail

0 commit comments

Comments
 (0)