@@ -199,6 +199,65 @@ wheel still loads on SSE2-only CPUs. Expected speedup at 1T: ~20–30 %, less
199199at 8T due to closer-to-bandwidth saturation. Estimated effort: half a day
200200plus 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.
2292994. **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