Packed-panel CQ4 weight format: NEON + SME2 kernels#709
Conversation
E2E validation on Gemma 4 E2B IT (M4 Pro)Converted Protocol: ~1.2k-token prompt, 32 decode tokens, 6 alternating cycles, cycle 1 discarded. Both bundles produce coherent on-task completions.
Per-cycle decode spread is <0.5 tok/s on both sides. The legacy baseline matches its previously documented numbers, and the panel bundle carries no load-time transformation — the gains come purely from the file-borne layout. Note: converting Gemma requires the new |
Generalization: the panel format (NEON path) on two more architecturesSame protocol as the Gemma table (~1k-token prompt, 32 decode tokens, 3 alternating cycles, cycle 1 dropped, coherent output checked); legacy = the existing production interleaved bundles, panel = fresh converts on this branch; both on this PR's engine.
Decode gains of +18–40% across three very different architectures; panel conv projections on the LFM2 hybrid work out of the box. One transpile-workflow note surfaced by this: the chunked-prefill component's chunk length is captured from the shape-capture prompt/ids, so short capture prompts produce small chunks (a 45-token chunk bundle prefilled ~40% slower than a 128-token one purely from per-chunk dispatch overhead). Worth pinning |
On-device A/B (Android, PR #698 harness, 512+32 spec)Same six bundles as the host tables, run on real phones via Samsung SM-S942U1 (big-core SoC) — round means:
Pixel 10a (Tensor G4):
Decode parity on the 1.7–2B models on Samsung is expected: phone decode sits at the DRAM roofline and both formats stream the same 4-bit bytes — the format's decode wins show where compute/dispatch headroom exists (small models, and all models on M-class). ⚠ The one regression (Pixel gemma decode) is not the weight format: on-device |
Pixel/Gemma decode regression: root-caused and fixed (54f2749)Follow-up to the Android tables — the one regressing cell (Pixel 10a gemma decode, −35%) decomposed into three independent causes, none of them the weight format's bytes:
Also ruled out with on-device experiments: thermal (order-reversed reruns), the orth lm_head driver and panel lm_head file (byte-swapped into the legacy bundle under its bound name: 10.0–10.5 tok/s, no cost), scheduling (big-cores-only pin), and the safe-product-scale path (trace shows Post-fix numbers (unfused graphs):
|
e9561b8 to
f714712
Compare
f714712 to
3a2661f
Compare
7a76c3e to
083dafe
Compare
The convert step hard-coded torch.float16 for HF model loading, which corrupts models whose weights exceed fp16 range (Gemma 4). bf16 loads them safely; quantization math is unchanged. Default stays float16. Signed-off-by: Noah Cylich <noahcylich@gmail.com>
The fused node costs ~15 ms/decode-step on Tensor G4 (profile-diffed on device) while gaining ~2% on M4. Its gate keys on ".cq" in the weight path, which the packed-panel suffix is about to satisfy; flipping the default keeps panel bundles on the plain matmul path. Re-enable by constructing FusionConfig(enable_dense_mlp_tq_fused=True). Signed-off-by: Noah Cylich <noahcylich@gmail.com>
Introduce a kernel-ready "packed panel" on-disk layout for CQ4 weights that both NEON (this change) and future SME paths consume zero-copy from the mmap, with no runtime cache and no second resident weight copy. Format - Data region holds panels [SB64][num_groups][gs/4][128B]; each 128B kg-block is 256 nibbles (low-first) = the int8 SDOT panel [4 vec][16 ch][4 K]. - Scales region adds folded fp32 panel norms (matmul rescale) alongside the legacy fp16 norms (embedding row decode), plus the transposed rotation for orthogonal lm_heads (virtual 128-wide groups written directly). - New header flag FLAG_PACKED_PANELS (1<<5) and a unique `.cq4p.weights` filename suffix; new bundles ship only that file. Enabled on all aarch64. Kernels (cactus-kernels) - Panel SDOT GEMV (M=1) and a fused panel NEON GEMM (M>1) over the file layout, plus the orthogonal lm_head GEMV with the file's transposed rotation. Mobile thread budget (ceil(SB64 / CACTUS_GEMV_SB_PER_THREAD), default 8). Batched orthogonal embedding row decode reads panels directly. - cactus_quant_build_panels is the byte-exact reference encoder (test/fixture). Loader (cactus-graph) - io.cpp parses the flag and points packed_panels / norm_panels / rotation_t straight into the mmap; the cache/registry/madvise machinery is removed. Panel files expose no legacy packed-indices region. Transpiler (python/cactus) - cq.write_cq_tensor emits panels for CQ4 matmul weights and orthogonal lm_heads/embeddings (chunked for large vocabs), byte-identical to the engine reference encoder. qdq learns to dequantize panels for export round-trips. Tests - Panel GEMV/GEMM/orth/embed vs fp32 oracle; panel file loaded through the graph matches the direct kernel; transpiler panels are byte-identical to the C reference encoder (ctypes); panel qdq round-trips the legacy dequant. Signed-off-by: Noah Cylich <noahcylich@gmail.com>
Signed-off-by: Noah Cylich <noahcylich@gmail.com>
Stacks the SME2 layer on the panel file format: the LUTI4/SMOPA leaves read the mmap'd panels directly (the panel bytes ARE the streaming layout), so there is no cache builder and no second weight copy. Non-SME2 silicon and non-512-bit SVL fall back to the NEON panel path bit-for-bit. Kernels - matmul_sme2_gemv.cpp: LUTI4/ZT0 nibble-expanding GEMV + fused GEMM leaves emitting raw int32 partials (fp32 rescale stays in NEON, numerics identical to the NEON path). Compiled at -O1: clang mis-compiles the vg1x4 ZA dots at -O2+ into encodings that SIGILL on M4. - matmul_sme2.cpp: SMOPA/USMOPA leaves incl. prefill-attention QK/AV passes and the streaming-SVL probe; compiled with armv8.2-a+...+sme2 (armv9-a implies non-streaming SVE2 Apple Silicon lacks). - attention_hybrid.cpp: SME prefill attention over the cached int8 KV segment (flat-scale QK requantize at pre-pack, u8 USMOPA AV with folded V scales). Dispatch - SME workers replace NEON workers inside the existing thread budget (flat k = min(2, nt-1), the measured power/speed frontier; CACTUS_SME_GEMV_WORKERS overrides). The fused SMOPA GEMM takes all M>1 panel matmuls. - CACTUS_QUANT_BACKEND / cactus_quant_set_backend: 0 auto, 1 force NEON, 2 force the streaming leaves (test coverage). - cpu_has_sme2() gates on streaming svcntb() == 64: every layout is pinned to 512-bit SVL; other SME2 implementations fall back to NEON for correctness. Tests - [sme2] variants of every panel test (GEMV/GEMM incl. M/N tails, orthogonal lm_head, interleaved fixtures, CQ1-4) through the real dispatch, plus a NEON<->SME agreement gate on identical fixtures and the SME prefill attention differential. Measured (M4 Pro, Gemma 4 E2B IT panel bundle, ~1k-token prompt, 32 tokens, 6 alternating cycles vs the NEON panel engine): prefill +17.6% (300->353 tok/s), TTFT -14.9% (3278->2788 ms), decode +2.9%, decode energy/token -17.9% (358->294 mJ). Kernel-level: fused SMOPA GEMM 1.5-3.1x the NEON panel GEMM; GEMV with SME workers up to 2.1x on K-heavy shapes. Signed-off-by: Noah Cylich <noahcylich@gmail.com>
083dafe to
22df501
Compare
The packed-panel weight format and the kernels over it — NEON and SME2. Stacked on #717 (the fused GEMV dispatch); retarget to main after it merges. Besides the format and SME2 work this PR carries two small pipeline fixes its bundles depend on (see "Pipeline fixes"); all numbers below are measured against #717's engine on the same bundles/devices, so they isolate exactly what this PR adds.
Packed-panel format
A kernel-ready layout for CQ4 weights, emitted into the
.cactweight files by the transpiler and consumed zero-copy from the mmap. No runtime cache, no second resident weight copy — the file is the kernel's operand stream.[SB64][num_groups][gs/4][128B]; each 128B kg-block is 256 nibbles (low-first) = the int8 SDOT panel[4 vec][16 ch][4 K]. Channels past N zero-padded.FLAG_PACKED_PANELS(1<<5) + unique.cq4p.weightssuffix; new bundles ship only that file, so pre-panel engines fail loudly.vzips in the NEON unpack (9 vs 7 ops/32 weights): free on wide-issue cores, visible only compute-bound on Cortex-X4-class (see phone numbers).Kernels
NEON: panel SDOT GEMV; fused panel GEMM (M>1) consuming file bytes directly — the legacy GEMM re-expands packed nibbles into a 4×-larger int8 buffer per call, panels never do; orthogonal lm_head GEMV via the file's transposed rotation; batched orthogonal embedding row decode.
SME2 (compile probe + runtime
cpu_has_sme2()with streaming-SVL==64 gate): LUTI4/ZT0 GEMV leaf, fused SMOPA GEMM for all M>1 panel matmuls, SME prefill attention. SME workers replace NEON workers inside the same thread budget (flat k=min(2,nt−1), the measured speed/power frontier). All SME leaves emit raw int32 partials; the fp32 rescale stays in NEON — SME numerics are identical to NEON (locked by an agreement test). Non-SME2 silicon runs the NEON panel path bit-for-bit. SME TUs build at-march=armv8.2-a+…+sme2with the vg1x4 GEMV TU at-O1(documented clang miscompile); knowledge base incactus-kernels/docs/sme/.Tests
Panel GEMV/GEMM/orth/embed vs FP32 oracle with
[sme2]variants of every test (backend control), M/N tails included; NEON↔SME agreement (identical int32 partials); SME attention differentials vs hybrid NEON + double-precision oracle; panel file loaded through the real graph matches direct kernels at M=1/M>1; transpiler panels byte-identical to the C reference encoder (ctypes); panel qdq round-trips legacy dequant at 0.0 diff.Measured — M4 Pro
Kernel (Gemma shapes, GF; baseline = #717 fused-dispatch legacy):
E2E (~1k prompt + 32 decode, alternating cycles, coherent; baseline = same engine, legacy bundle):
Beyond throughput: decode energy 358 → 294 mJ/token (−18%, 2 SME workers replace ~10 cores); chunk-aligned prompts reach 977 tok/s prefill / 1.10 s TTFT (1024-token prompt — arbitrary-length prompts pay a per-token tail that caps blended prefill, independent of this PR); cold start carries zero cache build vs the prior runtime-cache SME design (3.3 s vs 5.9–7.4 s first-message TTFT, ~1.2 GB less RAM).
Measured — on-device (#698 harness, 512+32 spec; NEON panel path, SME gates self-disable — portability validated)
(Legacy baselines are #717's fused dispatch — a deliberately hard bar: #717 already captured the dispatch wins on the old format.) The split is clean: panels win prefill on phones (+11–32%) and everything on M-class; the legacy format with #717's dispatch keeps the phone decode crown (the 2-op NEON unpack tax is visible on compute-bound Cortex cores, and phone decode at the DRAM roofline gains nothing from the format). Both formats remain fully supported by one engine — bundle choice per deployment, with sliding-window-safe behavior unchanged.
Pipeline fixes carried by this PR
--torch-dtypefor convert (bf16-safe model loading). Convert hard-coded fp16 HF loading; Gemma weights exceed fp16 range and were silently corrupted (the historical source of garbled Gemma bundles). Panel bundles for Gemma are built with--torch-dtype bfloat16; quantization math unchanged..cq4p.weightssuffix would satisfy, silently enabling the regression on panel bundles. Re-enable programmatically viaFusionConfig(enable_dense_mlp_tq_fused=True).Loader / transpiler
io.cpppointspacked_panels/norm_panels/rotation_tstraight into the mmap; cache/registry/madvise machinery removed by construction.write_cq_tensoremits panels for CQ4 matmuls and orthogonal lm_heads/embeddings (chunked for large vocabs), byte-identical to the engine reference encoder;qdqdequantizes panels for export round-trips.