Skip to content

perf(native-cpu): AVX2/FMA lane for the dense FP32 x·Wᵀ kernel on x86-64 - #1442

Merged
michalharakal merged 2 commits into
SKaiNET-developers:developfrom
michalharakal:feat/x86-avx2-fp32-dot
Oct 24, 2026
Merged

michalharakal merged 2 commits into
SKaiNET-developers:developfrom
michalharakal:feat/x86-avx2-fp32-dot

Conversation

@michalharakal

Copy link
Copy Markdown
Contributor

What

skainet_fp32_matmul_wt — the kernel behind the JVM's resident dense FP32 weights (SKEEP-007) — gains an x86-64 AVX2/FMA lane, selected at runtime. Every other target and every x86 host without AVX2 runs the previous code unchanged.

Why

The native library is built for baseline x86-64 (no -march, it has to run on any x86-64), so on x86 the kernel took its portable loop: SSE2 code, one accumulator, and the weight re-read once per activation row. The wasm and NEON builds have had SIMD bodies since SKEEP-007; x86 never did. In the LayaMoE encoder pass this kernel is two thirds of the time (#1441 describes the profile), and it is the FP32 product every JVM Linear resolves to once skainet-backend-native-cpu is on the classpath.

How

  • skainet_cpu_features.{h,c}: skainet_cpu_has_avx2_fma() — GCC/Clang __builtin_cpu_supports("avx2") && …("fma"), MSVC cpuid leaf 1 (FMA, AVX, OSXSAVE) + leaf 7 (AVX2) + xgetbv YMM state; cached; 0 elsewhere. Internal header, not part of the cinterop surface.
  • fp32_matmul_wt.c: skainet_fp32_wt_rows_avx2, compiled per function with __attribute__((target("avx2,fma"))) (MSVC emits VEX intrinsics without /arch), the same per-function dispatch the Apple arm64 dotprod kernels use. Tile: four weight rows × two activation rows, eight __m256 FMA chains fed by six loads per step, so the four weight rows stay in the core's cache while the activation rows stream past — the weight is read from memory once per call instead of once per activation row. Tails: 4 × 1 and single-row.
  • Every tile variant reduces an output the same way — the eight-lane chain over k & ~7, one fixed horizontal sum, the scalar tail in order — so an output's bits do not depend on which tile, thread or row slice computes it. The row pool's bit-identity promise holds, and so does the slice-independence that perf(backend-cpu): dense FP32 x·Wᵀ runs under the context schedule (SKEEP-005) #1441's schedule split relies on. The old header claim "bit-identical to transpose-then-GEMM" was already untrue for the wasm and NEON lanes; the header, the KDoc and the CHANGELOG now say what holds.
  • Dispatch is one cached probe per call; skainet_run_rows and the Kotlin binding are untouched. No Kotlin API change.

Measured

i7-9750H (6 cores, 12 threads), GCC 13.3, one native call per shape through the segment face (the C row pool is the only threading), median of five; baseline = the develop kernel in the same harness:

m n k develop this PR ×
63 1024 1024 29.6 GFLOP/s 131.7 GFLOP/s 4.5
63 2624 1024 33.4 183.9 5.5
63 1024 2624 28.7 135.3 4.7
183 1024 1024 30.2 162.3 5.4
1 1024 1024 (decode step) 28.5 71.7 2.5
63 256 1024 (one thread) 9.0 56.5 6.3

End to end, the LayaMoE example (SKaiNET-examples, ModernBERT-large encoder, FP32 lane, cli bench --index 0: three questions plus the router, 370 tokens) on the same host: 13.0 s per request on 0.62.1, 9.3 s with #1441 alone, 6.4 s with #1441 and this lane together (3 reps 6376/6477/6416 ms). Routing and every answer are the same; the largest change in any probability is 9.7e-07.

Tests

  • New NativeFp32MatmulWtX86LaneTest (skainet-backend-native-cpu jvmTest): 80 shapes over every tile edge (k ∉ 8ℤ, n ∉ 4ℤ, m ∈ {1, 2, 3, 63}) against a double-accumulated reference; a full call against two calls split at an unaligned column (and against row-at-a-time calls) bit for bit; the bench above behind -Dskainet.runBench=true. On a host without AVX2 the same tests exercise the portable lane.
  • Existing NativeFp32MatmulWtKernelTest, JvmDirectWeightTransposedTest, full :skainet-backend-native-cpu:jvmTest, :skainet-backend-cpu:jvmTest, both apiChecks green locally.
  • The MSVC probe cannot be run here; the native-cpu-multiarch windows-x86_64 leg is the gate for it.

CHANGELOG ## [Unreleased] → Changed.

skainet_fp32_matmul_wt had SIMD bodies for wasm and NEON only; the x86-64
library is built for baseline x86-64, so the kernel behind the JVM's
resident dense weights ran an SSE2 loop and re-read the weight once per
activation row. Hosts with AVX2 and FMA now select, through a cached cpuid
probe (skainet_cpu_has_avx2_fma), a per-function target("avx2,fma") lane:
eight-lane FMA chains over a tile of four weight rows by two activation
rows, the weight streamed from memory once per call.

Every tile variant reduces an output in the same order, so threaded,
sliced and single calls stay bit-identical to each other; the header and
KDoc now describe that instead of a bit-identity to transpose-then-GEMM
that the NEON and wasm lanes never had.

Measured single call on a 6-core host, m=63 n=1024 k=1024: 29.6 → 131.7
GFLOP/s; decode m=1: 28.5 → 71.7; one-thread n=256: 9.0 → 56.5.
@michalharakal
michalharakal merged commit ca64374 into SKaiNET-developers:develop Oct 24, 2026
17 checks passed
michalharakal added a commit that referenced this pull request Oct 24, 2026
…nd on AVX2

CHANGELOG [0.62.2] - 2026-10-24: the schedule-driven split of the dense FP32
x·Wᵀ product (#1441), the runtime-dispatched AVX2/FMA lane for the same kernel
on x86-64 (#1442), the CoroutineSchedule.forRange empty-range fix, the
added-token index in the text front-end (#1443), and the dependabot bumps. The
#1441 entry, dropped from Unreleased by a develop merge, is restored.

VERSION_NAME, CITATION, Antora version, README bom coordinate, What's New and
contributors, the Android tutorial coordinates; regenerated operator and
kernel-support matrices (version stamp).
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.

1 participant