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.
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 |
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 ( |
Kotlin/Native โ linux x64/arm64 |
Scalar + Native-cinterop |
Static archive embedded in the klib; manual |
Kotlin/Native โ macOS/iOS |
Scalar + Apple Accelerate + Native-cinterop |
Accelerate covers dense FP32 and reductions; cinterop covers packed quant with runtime |
JS / WASM (Js, Wasi) |
Scalar; wasmJs + |
No SIMD in Kotlin code: packed weights are relaid once per weight into kernel feed order and served by the scalar packed kernels; dense |
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
.sotier 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
KernelProviderSPI 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.