Skip to content

Ternary f32 LUT gemv — vendored NeoGPU kernel: exact FP32×b1.58 matmul for baseline-NEON ARM (Pi-4/A72) #1136

Description

@michalharakal

Goal

Vendor NeoGPU's ternary LUT NEON kernel (anjaustin/neogpu, MIT) into SKaiNET as an exact FP32-activation × BitNet-b1.58 matmul path for baseline-NEON ARM. Agreed with upstream in anjaustin/neogpu#1.

Why, versus our existing sdot-based bitnet_gemv (#1041):

  • f32 activations → exact results. No I8-absmax requantization step ([S2.8] P6: F32→I8 absmax requant adapter (Forward scope, traced) + reference bitnet_gemv #1040), no ~1.5% quantization error. Weight decode is exact, so logits match the FP32 reference bit-for-bit (modulo summation order).
  • Baseline NEON only (armv8-a+simd). No FEAT_DotProd required — this is the fast path for Cortex-A72 / Raspberry Pi 4-class targets where the current NEON kernel falls back to vmull_s8.
  • Upstream measured (Pi-4, 4 threads, full-vocab lm_head): 96 ms / 6.78 GOPS LUT vs 51 ms / 3.20 GOPS int8 (with quant error) vs 175 ms / 0.93 GOPS F16 NEON.

The kernel is src/hs_ml_ternary_neon.c in NeoGPU: 338 lines, fully standalone (only stdint/string/pthread/arm_neon), NEON + exact scalar fallback branches, 256-entry × 4-float LUT (4 KB, L1-resident), vld1q_f32(LUT[w[b]]) + vfmaq_f32 inner loop.

Design decisions (verified against both codebases)

  1. No new TensorEncoding. NeoGPU's sequential 2-bit payload is byte-identical to our BITNET_B1_58 payload: LUT ((b >> (lane*2)) & 3) − 1TernaryCodec.encodeBitNet (code shl ((i%4)*2), code = value+1). Same element order, same bias.
  2. Key on BITNET_B1_58 (in-band trailing LE FP32 scale, TernaryCodec.bitNetScale), not TernaryPacked (out-of-band scale, unrecoverable from a TensorView). The C kernel sees only the payload; Kotlin applies the scale to the output.
  3. Dispatch priority is free. KernelDispatch.matmul checks the exact key first; registering under matmul(FP32 dense contiguous × FP32/BITNET_B1_58 blocked_row_major) wins over the requantize→bitnet_gemv path with zero dispatcher changes. Pack absent → today's behavior, unchanged.
  4. Vendor verbatim (REUSE .license sidecar, SPDX-License-Identifier: MIT, SPDX-FileCopyrightText: 2024 NeoGPU 贡献者) + a thin house-style adapter skainet_ternary_f32.c. NeoGPU's pthread threading (4 threads over N≥512) stays — it is the measured configuration. LUT-init race closed via pthread_once in the adapter. MSVC has no pthreads → vendored file compiles only on non-MSVC; adapter carries a portable scalar fallback.
  5. -march pin. Non-Apple aarch64 kernel builds use -march=armv8.2-a+fp16+dotprod; the vendored + adapter files get pinned to -march=armv8-a (set_source_files_properties) — the whole point is dotprod-less hardware.
  6. Code-3 handling. Byte code 3 decodes to +2.0 in both NeoGPU's LUT and our decodeBitNet — parity-consistent. It is rejected at load-time repack (I2_S import), never in the kernel.
  7. I2_S layout trap. Stock BitNet.cpp GGUF I2_S (type 36) uses a 64-weight group layout (code = (block[j%16] >> (6 − 2*(j/16))) & 3); NeoGPU's own converter emits the sequential layout. Import repacks group→sequential once at load (WeightForm / EncodingRequest.RequantizeTo), so both file flavors feed the same kernel.

Architecture proof points (this effort is also a validation exercise)

Beyond the kernel itself, this port is a deliberate stress test of SKaiNET's claim to be modular and scalable. Success means each of the following lands as a plug-in — encoding + codec + kernel pack — with zero changes to the DSL NN builders, the DAG/tape machinery, KernelDispatch, TensorOps, or any consumer code:

  1. 新建 SIMD kernel → faster on real HW in eager mode. A third-party (vendored, MIT) NEON kernel reaches eager execution (DIRECT mode) purely via exact-key registration in KernelDispatch — no compilation step, no fusion pass, no runtime special-casing. Measured win expected on dotprod-less aarch64 (Pi-4/A72), where the current path falls back to vmull_s8.
  2. 新建 quant type → memory saving. I2_S import kept packed as BITNET_B1_58 = 0.25 bytes/weight (+scale) instead of 4 bytes when FP32-widened (today's [S2.7] P6: ternary encodings TQ1_0, TQ2_0, BITNET_B1_58 with block spec + activation hint; reference decoder and parity fixtures generated from the descriptor #1033 behavior) — ~16× weight memory on every ternary tensor, gigabytes at BitNet-2B scale.
  3. Novel research format → just another encoding. BITNET_PLANES (multi-plane trit residual, [ternary-f32] Phase 6: BITNET_PLANES encoding (multi-plane trit + FP16 row scale) + codec + lmhead-stage1 kernel pack #1150) shows a format invented outside SKaiNET slots in as encoding + codec + pack, and consumers (SKaiNET-transformers) select it declaratively via WeightForm — the network definition stays a stock dense layer.
  4. Optionality / graceful degradation. Every pack is an optional artifact: absent → dispatch falls back to the existing requantize/reference path, bit-identical to today, no crash. install(null) warns and registers nothing (contract pinned by tests).
  5. Third-party code, first-class licensing. Verbatim MIT vendoring with REUSE sidecars — external innovation is absorbed without forking or rewriting it into house code.

Phase 5's benchmark scenario + a loaded-weight-memory measurement are the evidence artifacts for 1 and 2; the parity/dispatch test suites are the evidence for 3 and 4.

Sub-issues

关注-up candidates (file when reached): thread-count control / house threadpool for the proj kernel (deviates from verbatim vendoring), baseline--march variant of the full FFM kernels lib for Pi-4, TernaryPacked dispatch once views can carry an out-of-band scale. (The NeoGPU "used by" PR and upstream comment moved into #1166.)

Verification lanes

  • :skainet-backends:skainet-backend-native-cpu:jvmTest — FFM + scalar/NEON goldens (incl. all 256 byte values)
  • :skainet-backends:skainet-backend-native-cpu:linuxArm64Test -PcrossArm64=true — NEON under qemu-aarch64
  • JniKernelParityTest on arm64 device/emulator
  • ./gradlew generateKernelMatrix refresh
  • End-to-end: microsoft/bitnet-b1.58-2B-4T GGUF (group) + NeoGPU-converted GGUF (sequential) vs FP32-widened baseline

Related

Metadata

Metadata

Assignees

No one assigned

    标签

    enhancement新建 feature or request

    Type

    No type

    项目

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions