diff --git a/README.md b/README.md index 5c54b2e..8e6b511 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,41 @@ So the whole blocker is one operation: **submanifold 3×3×3 convolution**. - [ ] End-to-end inference - [ ] Fleet benchmark (m1max / m2max / m4pro / m1ultra / m3ultra) +## Fleet benchmark + +`SubMConv3d` at 128³ grid, 128 channels, ~10% occupancy (209,715 voxels) — the hot op +in the V-VAE. `conv_ms` is GPU work with a warm indice cache. + +| box | chip | GPU cores | conv_ms | Mvox/s | +|---|---|---|---|---| +| m3ultra | M3 Ultra | 80 | **13.8** | 15.25 | +| m2max | M2 Max | 38 | 27.3 | 7.69 | +| m1ultra | M1 Ultra | 64 | 39.7 | 5.28 | +| m1max | M1 Max | 32 | 41.4 | 5.06 | + +Getting there took two rounds, both of which the fleet data — not local profiling — +made visible: + +| version | m3ultra | note | +|---|---|---| +| per-offset loop | 81.6 ms | *slower than a 38-core M2 Max* | +| fused gather + matmul | 39.3 ms | 2.1× | +| cached device index | **13.8 ms** | 5.9× total | + +The first version was launch-latency bound: 2·K³ = 54 tiny dispatches per layer, none +large enough to occupy the GPU. The giveaway was throughput being *anti-correlated* with +core count — the 80-core Ultra lost to every smaller box, because its fused-die design +punishes small dispatches hardest. Fusing the taps into one `[N,K³·Cin]×[K³·Cin,Cout]` +matmul fixed the dispatch count; caching the prepared device index removed ~26 ms of +per-layer numpy bookkeeping that was hiding behind ~13 ms of real GPU work. + +Only after both did core count start predicting performance, which is the sign the op is +finally compute-bound. Note the M1 Ultra (64 cores) still barely beats the M1 Max (32) — +gather-heavy work scales poorly across UltraFusion. + +**Next target:** the indice map is now dominant (232–357 ms, CPU numpy) and is only +amortised because it is cached per coordinate set. It matters whenever coordinates change. + ## Correctness `spconv` cannot be installed here — that is the reason this port exists — so there is no