Skip to content

dsv4: NEON arm for FP4 expert matmul (fixes #1696 Apple Silicon prefill) - #1730

Merged
JustVugg merged 4 commits into
JustVugg:devfrom
mfethe1:perf/dsv4-fp4-neon-arm
Sep 24, 2026
Merged

JustVugg merged 4 commits into
JustVugg:devfrom
mfethe1:perf/dsv4-fp4-neon-arm

Conversation

@mfethe1

@mfethe1 mfethe1 commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

What

NEON arms for both FP4 expert kernels, so arm64 stops falling through to the scalar #else arms — the root cause of #1696 (Apple Silicon prefill at decode speed).

Batch kernel coli_fp4_matmul_batch_rows16_order (prefill): ports the AVX2 arm's algorithm to NEON — vqtbl1q_u8 nibble-LUT decode (doubled e2m1 ints, ×0.5f un-double), vtrnq/vcombine 4×4 transposes, strict per-element (x*w)*scale then add (separate vmul/vmul/vadd — no FMA, no reassociation; the rows16 rounding contract must be preserved).

Matvec kernel coli_fp4_matvec_rows16_order (decode; also used by batch at S==1): same decode/transpose/accumulate structure, 4 float32x4_t accumulators held in registers for the whole 16-row tile.

Numbers (M-series, gcc-15 -O3, bit-exact vs scalar arm)

shape scalar NEON speedup
128×4096×2048 (w1/w3) 8.5–9.0 GFLOP/s 192–199 GFLOP/s ~22×
128×2048×4096 (w2) 12.2 GFLOP/s 202 GFLOP/s ~17×
32×4096×2048 16.6–19.6 GFLOP/s 146–171 GFLOP/s ~9×
128×128×128 (tiny fixture) 20.6–25 GFLOP/s 57–60 GFLOP/s ~2.8×
S=1 matvec 4096×2048 (decode) 21.8 GFLOP/s 35.0 GFLOP/s 1.6×
S=1 matvec 2048×4096 (decode) 19.9 GFLOP/s 38.4 GFLOP/s 1.9×

Validation

  • Bit-exact identical to the scalar arms (0/262144 floats differ at the headline shapes; 0/2048 at S=1) — mandatory, since the AVX2↔scalar arms are already bit-exact and the design note at deepseek_v4.c:~16450 requires every rows16 ISA to round per-row identically.
  • make check: 1373 tests OK (skipped=128) on the patched tree.
  • 0 new compiler warnings vs dev baseline (22 pre-existing, identical sets).
  • Microbench harness used for the A/B: c/tools/bench_fp4_matmul.{c,sh} (PR tools: FP4 expert matmul microbench (SIMD vs scalar arm, ref #1696) #1727).

Reproduce

cd c && tools/bench_fp4_matmul.sh 128 4096 2048 7

arm64: builds the NEON arm and reports; x86-64: builds AVX2 arm vs -mno-avx2 scalar. No model files, torch, or GPU needed — CPU-only, ~30 s.

Fixes #1696.

mfethe1 and others added 3 commits September 24, 2026 07:17
bench_fp4_matmul builds the COLI_V4_UNIT_NATIVE_QUANT unit twice — default
flags (SIMD arm active) and with EXTRA_CFLAGS=-mno-avx2 (scalar arm) —
verifies the two arms agree bit-exactly on seeded data, and times both at
the real DeepSeek-V4-Flash expert shapes (w1/w3 [4096→2048], w2 [2048→4096]).

Motivation (JustVugg#1696): on x86-64 (Zen 2, gcc 15, -march=x86-64-v3 vs -mno-avx2)
the AVX2 batch arm measures 40–75× faster than the scalar arm at those
shapes (~141–159 vs ~1.9–3.8 GFLOP/s), with bit-exact identical outputs.
This harness reproduces that A/B anywhere and gives arm64 a baseline for
the (currently missing) NEON arm.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Port the AVX2 arm's algorithm to NEON so arm64 stops running the scalar
#else arm (Apple Silicon prefill ~1.3-1.75 tok/s, issue JustVugg#1696):
- vqtbl1q_u8 nibble LUT decode of doubled e2m1 ints, x0.5f un-double
- 4x4 float transposes (vtrnq_f32 + vcombine_f32) turning rows column-major
- strict (x*w)*scale rounding with separate mul/mul/add, no FMA fusion —
  bit-exact identical to the scalar and AVX2 arms (0/262144 floats differ)
- one x broadcast serves all 4 columns of a group; 4 independent add chains

M-series (gcc-15 -O3): 197-211 GFLOP/s at real expert shapes vs scalar
~9-17 GFLOP/s — 18-22x kernel speedup. Repo make check: 1373 tests OK,
0 new warnings vs dev.
…g#1696)

Same decode/transpose/accumulate structure as the batch arm; S=1 keeps
4 float32x4 accumulators in registers for the whole 16-row tile. Bit-exact
vs the scalar arm (0/2048 differ at I=4096/O=2048), 1.6-1.9x faster than
scalar at S=1 (memory-bound at 4 flop/byte), make check green (1373 tests).
…RM runner

The bench compared the SIMD and scalar arms only on x86 (-mno-avx2); on
arm64 it built one arm and compared nothing. There the scalar build is
-march=armv8-a+nosimd, which undefines __ARM_NEON, and a mismatch now exits
non-zero. The ARM job runs it at a batch shape and at S=1, so the NEON arms
this PR adds are checked on real arm64 on every change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants