Skip to content

feat(backend): NEON body for the Q4_0 matmul kernel (#920) - #939

Merged
michalharakal merged 3 commits into
developfrom
feature/q4_0-neon-920
Aug 10, 2026
Merged

michalharakal merged 3 commits into
developfrom
feature/q4_0-neon-920

Conversation

@michalharakal

Copy link
Copy Markdown
Contributor

First implementation slice of #920 (native kernels for mobile): skainet_q4_0_matmul was the only priority quant format without a SIMD path — scalar C only, while q8_0/q4k/q5k/q6k all had NEON bodies.

The kernel

Split-layout nibble unpack (vand/vshr — low nibbles decode elements 0..15, high nibbles 16..31), re-centre by 8 in the signed int8 domain, widen to f32 and FMA against the activation lanes — the same widen+vfmaq_f32 structure as the q8_0 body (activations are FP32; an int8 dotprod would need int8 activations). Plain NEON, no dotprod/i8mm requirement — the path runs on every AArch64 core including armv8.0 (Cortex-A53), which matters for the Android runtime-dispatch story in the #920 plan.

The loop is also restructured to the block-outer/row-inner order q8_0/q4k already use (sequential 18-byte-strided weight reads for the in-order-core case). Per output row, blocks still accumulate in ascending order — numerically identical to the previous row-outer form.

Test-coverage gap closed

The jvmTest side had a Q4_0 FFM parity test, but the Kotlin/Native lane (qemu/board) had no Q4_0 coverage at all — a NEON regression there would have been invisible. New NativeKnQ4_0MatmulKernelParityTest mirrors the Q8_0 one (4 shapes up to 4096×64, full-range random nibbles exercising both lanes and the re-centring).

Verified

  • jvmTest + linuxX64Test green (host archives — scalar/auto-vectorized path, loop-restructure regression)
  • linuxArm64Test -PcrossArm64=true under qemu-aarch64 with the K/N-bundled aarch64 gcc 8.3 (-march=armv8.2-a+fp16+dotprod): 27/27 green, including the 4 new Q4_0 parity cases
  • objdump of the cross-built libskainet_kernels.a shows fmla vector FMAs inside skainet_q4_0_matmul — the NEON body executed under qemu, not the scalar fallback (same verification standard as the AARCH64-VERIFIED banner)
  • skainet_simd.h banner updated to reflect the new coverage

We can run the same parity binary on physical aarch64 hardware (as done for the original NEON verification) on request.

Refs #920

skainet_q4_0_matmul was the only priority quant format without a SIMD
path — scalar C only, while q8_0/q4k/q5k/q6k all had NEON bodies. Q4_0
is named alongside Q8_0/Q4_K as a priority mobile format in the #920
plan, so it needs a NEON story before ARM ships as a perf target.

The body unpacks the split-layout nibbles (low nibbles decode elements
0..15, high nibbles 16..31) with vand/vshr, re-centres by 8 in the
signed int8 domain, widens to f32 and FMAs against the activation
lanes — the same widen+vfmaq_f32 structure as the q8_0 body, because
activations are FP32. Plain NEON only, no dotprod/i8mm requirement, so
the path runs on every AArch64 core including armv8.0 (Cortex-A53).

The loop is restructured to the block-outer/row-inner order q8_0 and
q4k already use: the weight is block-major, so consecutive rows within
a block are 18 bytes apart and weight reads become sequential. Per
output row the blocks still accumulate in ascending order — results
are numerically identical to the previous row-outer form.

Also adds the missing Kotlin/Native Q4_0 parity test: the jvmTest side
had one, but the aarch64 (qemu/board) lane had no Q4_0 coverage at all,
so a NEON regression there would have been invisible.

Verified:
- jvmTest + linuxX64Test green (host archives, scalar/auto-vec path)
- linuxArm64Test under qemu-aarch64 with the K/N-bundled aarch64 gcc
  8.3 (-march=armv8.2-a+fp16+dotprod): 27/27 tests green, including the
  4 new Q4_0 parity cases
- objdump of the cross-built archive shows fmla vector FMAs inside
  skainet_q4_0_matmul — the NEON body executed, not the scalar fallback

Refs #920
Resolve the [Unreleased] CHANGELOG conflict with the merged #922/#919/#930
entries: keep the Added section for the NEON kernel plus all merged Fixed
bullets.
@michalharakal
michalharakal requested a review from aharakal August 10, 2026 13:03
@michalharakal
michalharakal merged commit 6891817 into develop Aug 10, 2026
12 of 13 checks passed
@michalharakal
michalharakal deleted the feature/q4_0-neon-920 branch August 10, 2026 13:04
michalharakal added a commit to MacOS/SKaiNET that referenced this pull request Aug 11, 2026
Bump VERSION_NAME 0.38.0 -> 0.39.0 and update all version-carrying docs:
- CHANGELOG: consolidate [Unreleased] under [0.39.0] with a headline summary
  (includes the SKaiNET-developers#947 AAR-publishing CI entry, now merged to develop).
- README: BOM snippet -> 0.39.0, What's New in 0.39.0, Contributors (0.39.0).
- docs/antora.yml: skainet_version attribute 0.38.0 -> 0.39.0 (used in the
  docs' dependency snippets).
- kernel-support-matrix.adoc: regenerated via generateKernelMatrix — the
  Android column now shows native-jni for Q8_0/Q4_0/Q4_K/Q5_K/Q6_K (the new
  JNI backend), replacing panama-vector; version stamp -> 0.39.0.

0.39.0 headline: on-device AI on Android becomes real — the
skainet-backend-jni-cpu JNI NEON backend (~24 tok/s SmolLM2-135M Q8_0 on a
Pixel 8a vs ~3.8 scalar), plus Android streaming GGUF loads (SKaiNET-developers#922), linkable
K/N kernel klibs (SKaiNET-developers#942), the Q4_0 NEON kernel (SKaiNET-developers#939), GGUF loader fail-fast
(SKaiNET-developers#919), and a tensor-storage correctness pass (SKaiNET-developers#927-SKaiNET-developers#931).

Local prep only — not pushed/tagged. Cut off develop after SKaiNET-developers#947 merged, so
the branch already carries the AAR-publishing workflow + pinned NDK.
michalharakal added a commit to MacOS/SKaiNET that referenced this pull request Aug 11, 2026
…ative, JNI (SKaiNET-developers#708)

0.39.0 shipped packed GGUF loading for Q5_0/Q5_1 plus the scalar and Panama
kernels, but the priority-100 native tier carried no Q5_x kernels: the JVM
cascaded to Panama, and Kotlin/Native and Android ran the formats on the
priority-0 scalar floor.

Mirrors the Q4_0 pattern (SKaiNET-developers#920/SKaiNET-developers#939):

- q5_0_matmul.c / q5_1_matmul.c: block-outer/row-inner C kernels with a
  plain-NEON body (no dotprod/i8mm — runs on every AArch64 core). The qh
  high-bit plane expands via a per-lane vtstq_u8 bit test onto the split
  nibbles; dequant folds algebraically (Q5_0: d*(dot-16*Σx), Q5_1:
  d*dot+m*Σx) so the per-block input sum hoists out of the output-row loop
  and the codes widen unsigned through skainet_neon_u8x16_to_f32x4x4.
- FFM wrappers NativeQ5_0/Q5_1MatmulKernel + NativeKernelProvider wiring.
- Kotlin/Native NativeKnQ5_0/Q5_1MatmulKernel + NativeKnKernelProvider
  wiring (cinterop bindings regenerate from skainet_kernels.h).
- JNI bridge: q50Matmul/q51Matmul shims + JniKernels externals +
  JniKernelProvider accessors, sources added to both CMake lists.
- Parity tests vs the scalar references on all three lanes (jvmTest,
  nativeTest, androidTest) + KernelSupportMatrixTest tier update.

Verified: jvmTest 8/8+8/8 (FFM vs scalar), linuxX64Test 4/4+4/4 (cinterop
vs scalar), JNI arm64-v8a cross-build exports both symbols with fmla/cmtst
in the disassembly (NEON body compiled in). The qemu-aarch64 NEON parity
lane (-PcrossArm64=true) runs in CI as for Q4_0.

Unblocks the packed Q5_1 converter path for functiongemma-270m "Q5_K_M"
checkpoints under NATIVE_OPTIMIZED — SKaiNET-transformers#170.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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