Skip to content

Commit d318d55

Browse files
Add performance notes from flow optimization
1 parent 8463dcc commit d318d55

1 file changed

Lines changed: 84 additions & 14 deletions

File tree

development/flow/PERFORMANCE_NOTES.md

Lines changed: 84 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,65 @@ wheel still loads on SSE2-only CPUs. Expected speedup at 1T: ~20–30 %, less
199199
at 8T due to closer-to-bandwidth saturation. Estimated effort: half a day
200200
plus a careful microbenchmark of `vpgatherdd` cost on the target CPUs.
201201
202+
## Interleaved (channel-last) layout + hand-written AVX2 FMA (rolled back, 2026-06-12)
203+
204+
This implemented the two top "remaining" ideas below and measured them. **Net
205+
result: no win; small regression. Rolled back.** Documented here so the avenue
206+
is not re-attempted without new information.
207+
208+
The idea, in two composable stages:
209+
210+
1. **Channel-last (interleaved) flow buffer.** The input is channel-first
211+
(`channels[axis] = flow.data + axis*n_pixels`), so the `D` components at one
212+
position are gathers from regions `n_pixels` floats apart — in theory `D`
213+
distinct cache lines per corner. A one-time `O(N)` transpose into a
214+
`std::vector<float>` where a voxel's components sit at `[v*VS + axis]` makes
215+
each corner touch one line and yield all `D` components. `compute_corners`
216+
is unchanged (its offset is already the voxel flat index); only the sampler
217+
multiplies by the voxel stride `VS`.
218+
2. **Hand-written AVX2+FMA sampler for 3D** on that interleaved buffer, with
219+
`VS = 4` padding so each corner loads as one 128-bit vector. Lane-wise FMA
220+
accumulates the weighted `D`-vector across the 8 corners — **no gather**
221+
(the rejected approach above). Runtime-dispatched via
222+
`__builtin_cpu_supports`, per-function `__attribute__((target("avx2,fma")))`,
223+
scalar fallback for MSVC/arm64/non-AVX2. No CMake/global-arch-flag change.
224+
225+
The codegen was confirmed ideal via `objdump`: 8× `vfmadd132ps` each fusing a
226+
128-bit load (`(%rdi,%rax,1)`) with a `vbroadcastss` weight, zero gather
227+
instructions. So the SIMD path was real and optimal — and still no faster than
228+
scalar.
229+
230+
Measurements (3D fixture, warm, min of 8 runs, same session, same machine as
231+
the headline numbers):
232+
233+
```
234+
variant 1T min 8T min
235+
channel-first (baseline) 5.33 s 1.357 s
236+
interleaved VS=3 (no pad, scalar) 5.44 s 1.46 s
237+
interleaved VS=4 (scalar) 5.56 s 1.42 s
238+
interleaved VS=4 + AVX2 FMA 5.58 s 1.46 s
239+
```
240+
241+
Conclusions:
242+
243+
- **The loop is memory-latency bound, as the roofline diagnosis said.**
244+
Cutting the corner sum from 24 scalar mul-adds to 8 packed FMAs changes the
245+
ALU count, not the load latency, so it does nothing. The 8 corner loads are
246+
independent, so the out-of-order engine already overlaps them (high MLP);
247+
channel-first's three independent channel loads overlap too.
248+
- **Channel-first already has good locality.** A 64-byte line holds 16
249+
consecutive-x voxels of one channel; a trilinear cell's x-pair lives on one
250+
line. Interleaving with `VS=4` cuts that to 4 voxels/line — *worse* density —
251+
which is why VS=4 lost ~0.12 s/1T to VS=3. But even VS=3 (no waste, same
252+
144 MB footprint) did not beat channel-first.
253+
- **Padding `VS=4` also costs +33 % flow memory** (144 → 192 MB for the 3D
254+
fixture) for the AVX2 path's aligned 4-wide load.
255+
- The differences are partly inside this laptop's **±~8 % thermal-throttle
256+
noise** (baseline 1T alone ranged 5.33–5.85 s across the session, 8T
257+
1.357–1.59 s). That noise floor exceeds the expected gain of any remaining
258+
micro-optimization, so small wins cannot be validated on this host — they
259+
need a fixed-clock machine with `perf` counters.
260+
202261
## What was tried and rejected
203262
204263
- **Per-thread density scatter buffers** — scatter is <1 % of runtime,
@@ -212,25 +271,36 @@ plus a careful microbenchmark of `vpgatherdd` cost on the target CPUs.
212271
- **Half-precision (fp16) flow storage** — would halve memory traffic, but
213272
the kernel isn't bandwidth-bound (see diagnosis), and it's a breaking API
214273
change.
274+
- **Interleaved channel-last layout + hand-written AVX2 FMA** — implemented and
275+
measured 2026-06-12 (see the dedicated section above). No win, small
276+
regression, rolled back. The SIMD path was confirmed optimal in codegen yet
277+
did not beat scalar — the loop is load-latency bound, not ALU bound.
215278
216279
## What remains worth trying
217280
218-
In rough order of expected return on effort:
219-
220-
1. **Hand-written AVX2 (and SSE2-fallback) intrinsics for the corner sum**
221-
inside a runtime-dispatched specialization. Plausible 20–30 % at 1T. The
222-
plumbing scaffolding is documented above in the SIMD section.
223-
2. **SoA position layout** (`vector<float> pos_x, pos_y, pos_z` instead of
224-
`vector<array<float,3>>`). Lets the autovectorizer batch the clip + step
225-
+ convergence check across N particles. Pairs naturally with (1).
226-
3. **Reduce gather miss latency** via batch prefetching: prefetch the
227-
next-K particles' corner cache lines before computing the current one.
228-
Likely modest gain; needs profiling.
281+
In rough order of expected return on effort. **Note (2026-06-12):** items 1 and
282+
2 below were implemented and measured — they gave no speedup (see "Interleaved
283+
layout + AVX2 FMA" above). They are kept here only with that caveat; the live
284+
candidate is now (3), and (4) is the prerequisite for evaluating it.
285+
286+
1. ~~Hand-written AVX2 intrinsics for the corner sum~~ — **tried, no win.** The
287+
corner sum on an interleaved buffer compiled to 8 packed FMAs with no
288+
gathers, and was no faster than scalar: the loop waits on the 8 corner
289+
loads, not the arithmetic.
290+
2. ~~SoA position layout~~ — its only rationale was enabling the SIMD in (1);
291+
with (1) shown not to help, SoA's batched clip/step/convergence has nothing
292+
to pay for the extra coordinate gather/scatter. Not pursued separately.
293+
3. **Reduce load latency** via software prefetching: prefetch the next-K
294+
particles' corner cache lines before computing the current one. This is the
295+
one lever that targets the actual (latency) bottleneck, and it works on the
296+
existing channel-first layout — no relayout needed. Expected gain is modest
297+
and **cannot be validated on the current laptop** (its ±~8 % thermal noise
298+
floor swamps it); needs a fixed-clock host.
229299
4. **A real `perf stat`** run on a host where `linux-perf-tools` is
230300
installed, to confirm where cycles actually go (front-end, back-end
231-
memory, back-end core, retired-FMA-ratio). The streaming-probe upper
232-
bound is necessary but not sufficient — counter data would localise the
233-
bottleneck precisely.
301+
memory, back-end core, retired-FMA-ratio) and to provide a low-noise
302+
measurement environment in which (3) could actually be evaluated. The
303+
streaming-probe upper bound is necessary but not sufficient.
234304
235305
## Reproducing these measurements
236306

0 commit comments

Comments
 (0)