Eager execution: backends and kernels

The eager path is DirectCpuExecutionContext โ†’ DefaultCpuOps* โ†’ KernelRegistry โ†’ KernelProvider, distinct from the StableHLO/IREE export path. This page is the hand-authored overview; its companion kernel ร— platform matrix is generated from the registered providers and gated against drift.

This page covers the provider tier โ€” who can compute what, on which platform. The generic path selects differently, on a declared descriptor of the operands rather than on dtype and priority: see Kernel SPI and the selection algorithm for KernelDispatch, the ViewKernelPack SPI, and how a call site is matched to a kernel.

Legend: โœ… available ยท โŒ missing.

SKaiNET eager execution

CPU backend

Scalar floor

commonMain โ€” all KMP targets

FP32 BF16

Q8_0 Q4_0 Q4_K Q6_K Q5_K Q5_1 Q5_0

ternary TQ1_0 TQ2_0 BitNet

Panama Vector

JVM SIMD only โ€” jdk.incubator.vector

dense and packed quant

Native FFM

JVM only โ€” C kernels via CMake

Q4_K MemSeg zero-copy

direct xยทWแต€, no transposed copy

ternary LUT gemv and planes lm_head

Native JNI

Android only โ€” same C kernels via JNI

two .so tiers, cpuinfo-gated armv8-a vs armv8.2+dotprod

packed quant and bitnet_gemv

dense FP32 missing

Apple Accelerate

Native macOS iOS โ€” cinterop

dense FP32 and reductions

Native cinterop

Kotlin/Native โ€” linux and Apple

packed quant, runtime FEAT_DotProd dispatch

manual registration via installNativeKernels

Platforms

JVM โ€” scalar, Panama, Native-FFM

Android โ€” scalar, Native-JNI

Native linux โ€” scalar, Native-cinterop

Native apple โ€” scalar, Accelerate, Native-cinterop

SKEEP-007

Kernel ร— provider (matmul, FP32 activations)

Weight format Scalar (all targets) Panama (JVM) Native FFM (JVM) Native JNI (Android) Native cinterop (K/N) Wasm SIMD128 (wasmJs)

FP32

โœ…

โœ…

โœ…

โŒ

โŒ

โœ…

FP32 x ยท Wแต€ direct (weight [out, in], no transposed copy)

row-dot

โŒ (cached transpose)

โœ…

โŒ

โŒ

โœ…

BF16

โœ…

โœ…

โœ…

โŒ

โŒ

โœ…

FP16

โœ…

โœ…

โœ…

โŒ

โŒ

โœ…

Q8_0

โœ…

โœ…

โœ…

โœ…

โœ…

โœ…

Q4_0

โœ…

โœ…

โœ…

โœ…

โœ…

โœ…

Q4_K

โœ…

โœ…

โœ…

โœ…

โœ…

โœ…

Q6_K

โœ…

โœ…

โœ…

โœ…

โœ…

โœ…

Q5_K

โœ…

โœ…

โœ…

โœ…

โœ…

โœ…

Q5_1

โœ…

โœ…

โœ…

โœ…

โœ…

โœ…

Q5_0

โœ…

โœ…

โœ…

โœ…

โœ…

โœ…

TQ2_0 / BitNet b1.58 (int8 activations)

โœ…

โ€”

โ€”

โœ… NEON

โ€”

โœ… SIMD128

BitNet b1.58 (exact FP32 activations, LUT gemv)

โœ…

โ€”

โœ… FFM (vendored NeoGPU)

โœ… JNI

โœ… K/N cinterop

โœ… SIMD128 LUT

BITNET_PLANES (multi-plane ternary packing, fused lm_head)

โœ…

โ€”

โœ… FFM (vendored NeoGPU)

โœ… JNI

โœ… K/N cinterop

โœ… SIMD128 LUT

Q2_K / Q3_K / Q8_K / IQ4

โŒ (dequant to FP32 only)

โŒ

โŒ

โŒ

โŒ

โŒ

Resolution is by priority: Native (100 โ€” whichever of FFM/JNI/cinterop applies to the target) โ†’ Panama / Wasm SIMD128 (50) โ†’ Scalar (0). The best available provider that carries the kernel wins; otherwise it cascades down. At most one native tier is compiled into a given target, so the native columns are mutually exclusive per platform rather than stacked. The Wasm SIMD128 column is the sidecar pack (skainet-backend-wasm-simd), present only after WasmSimd.install(); every matmul kernel in it has an explicit SIMD128 body (the K-quant int8 dots on i32x4.dot_i16x8_s, a 4ร—8 register-blocked dense GEMM, eight-wide BF16/FP16 widening), and the dense kernels thread over the sidecar’s pthread pool like the packed ones when the page is cross-origin isolated. The ternary kernels in the sidecar are the portable mirror of the vendored NeoGPU kernel โ€” the same 256-entry decode table as a compile-time constant, read four codes at a time in SIMD128 โ€” registered under the same dispatch keys as the FFM and JNI ternary packs (WasmTernaryKernelPack).

The dense x ยท Wแต€ row is the product a Linear layer computes. The JVM and the wasm tier multiply the [out, in] weight in place through skainet_fp32_matmul_wt (KernelProvider.matmulFp32Wt; the JVM makes a heap weight resident off-heap once, the sidecar keeps it in its linear memory) and never materialise the transposed copy the Panama path caches per weight.

The fused row kernels โ€” softmax / log-softmax over the last dimension, layer norm and RMS norm (what LayerNormalization calls through the optional FusedNormOps hook), and scaled dot-product attention with its structural masks and grouped-query heads โ€” are one C pass each (f32_fused.c), served by the JVM FFM provider (softmaxFp32, normFp32, attentionFp32) and the wasm sidecar alike. Where neither is present the ops layer runs its common implementation: attention scalar per head and parallel over heads through the schedule, the normalisation as its decomposed elementwise chain. The decomposition is also what a recording or differentiating op set always sees, so the hook changes no recorded graph and no gradient.

Packed weights reach a kernel through the registry by declaring their block order; a canonical weight is relayouted once, not per call.

Platform ร— what runs

Target Providers available Notes

JVM

Scalar + Panama + Native-FFM

Full SIMD/native acceleration.

Android

Scalar + Native-JNI

Panama (jdk.incubator.vector) and FFM (java.lang.foreign) are JDK-only โ€” ART has neither, so Android’s native tier is JNI, not a degraded JVM. Includes the NEON bitnet_gemv pack.

Kotlin/Native โ€” linux x64/arm64

Scalar + Native-cinterop

Static archive embedded in the klib; manual installNativeKernels(), no ServiceLoader on K/N.

Kotlin/Native โ€” macOS/iOS

Scalar + Apple Accelerate + Native-cinterop

Accelerate covers dense FP32 and reductions; cinterop covers packed quant with runtime FEAT_DotProd dispatch, so one archive serves A12 through M-series.

JS / WASM (Js, Wasi)

Scalar; wasmJs + skainet-backend-wasm-simd

No SIMD in Kotlin code: packed weights are relaid once per weight into kernel feed order and served by the scalar packed kernels; dense [out, in] weights are multiplied in place without a transpose copy (SKEEP-007 Tier 0). On wasmJs, WasmSimd.install() adds the SIMD128 sidecar pack โ€” the engine’s C kernels compiled by Emscripten, with pthreads on cross-origin-isolated pages (SKEEP-007 Tier 1) โ€” and binds HOST_OFFHEAP to the sidecar’s linear memory, so matmul and convolution outputs, elementwise arithmetic, activations, reductions, softmax, the normalisations and attention stay resident there between kernels and cross back to the Kotlin heap only when read.

Packed-quant matmul works on every target: those formats have a commonMain scalar kernel and DefaultCpuOpsBase dispatches packed weights through the registry.

Gaps

  • Wasm beyond the kernel set โ€” what stays common Kotlin on wasmJs is memory movement (reshape, permute, concat, gather, unfold, pooling) and the integer and narrow-float element paths; every FP32 op a transformer or CNN forward pass spends its time in โ€” the matmuls, convolutions (im2col + GEMM), elementwise arithmetic, activations, reductions, softmax, the normalisations and attention โ€” runs in the sidecar with the activations resident in its linear memory.

  • Native JNI dense FP32/BF16 โ€” the Android JNI provider has no GEMM shim, so dense ops fall through to scalar there whichever .so tier loaded (SKaiNET#920).

  • Dense FP32/BF16 SIMD on Kotlin/Native linux โ€” still the scalar floor; Apple has Accelerate (SKaiNET#722, #910).

  • Other GGML quant formats (Q2_K, Q3_K, Q8_K, IQ4_NL/XS) โ€” loadable via dequantization to FP32, no packed matmul kernel on any provider.

  • Non-CPU eager backends (IREE, Metal, GPU) โ€” the KernelProvider SPI anticipates them, none are implemented for the eager path. The compiled path (DSL โ†’ StableHLO โ†’ IREE) does reach GPU on Android via Vulkan, but that is a separate pipeline.