Architecture

This page follows arc42’s chapter ordering at a coarse grain. It describes SKaiNET as it is today; the deepest sections are the kernel SPI, the eager execution pipeline and the memory model, because that is where most of the architectural weight sits.

1. Introduction and goals

SKaiNET is a Kotlin Multiplatform ML framework whose primary target is on-device / edge inference and training in environments that already have JVM tooling: Android, JVM server, and Kotlin/Native iOS and linuxX64. The framework separates model authoring (a typed Kotlin DSL with compile-time tensor shape checks where possible) from execution (pluggable backends), so the same model can run eagerly during development and be lowered to MLIR StableHLO β†’ IREE for deployment.

Hard non-goals:

  • Become a full numerics library. SKaiNET targets the operators real models use, not the long tail in PyTorch / NumPy.

  • Run untrusted user code. Kernels are trusted code; security is about not corrupting memory, not about sandboxing.

Architecture diagram of the SKaiNET compiler pipeline

2. Constraints

Constraint Why

Kotlin Multiplatform with commonMain / per-target source sets

Same DSL must run on JVM, Android, iOS, macOS, linuxX64, JS, Wasm.

--enable-preview --add-modules jdk.incubator.vector on JVM 21+

FloatVector / ByteVector are still incubator on JDK 25 (JEP 508).

Maven Central publication via vanniktech.mavenPublish

All modules signed; coordinates sk.ainet.core:*.

Antora-based docs site under docs/modules/ROOT/

Source-controlled, follows DiΓ‘taxis quadrants for user-facing pages.

arc42 ordering for this page

Architectural reference, not a tutorial.

3. Context (system boundaries)

The framework’s outer surface:

  • DSL layer β€” Kotlin model DSL (nn { …​ }, tensor { …​ }) and imperative TensorOps / ExecutionContext API.

  • I/O layer β€” model loaders for GGUF, SafeTensors, ONNX in skainet-io-* modules. Reading is the common case, but skainet-io-gguf also writes: GGUFWriter/GgufExportFacade for arbitrary tensor export, and I2sAotConverter for the ternary AOT conversion in Ternary networks: getting started. The IREE-facing counterpart (GGUF β†’ .irpa) lives outside this repo, in the public SKaiNET-IREE-tools β€” a deliberate boundary (Β§9): IREE/StableHLO-target-specific conversion does not belong in core, which stays target-neutral.

  • Compile layer β€” RecordingExecution records ops to a tape, then lowers to StableHLO / IREE bytecode in skainet-compile-* modules.

  • Backend layer β€” BackendProvider dispatches TensorOps calls to a concrete implementation (CPU, XNNPACK, future GPU). Inside a backend, the kernel SPI picks the SIMD recipe for the host hardware.

4. Solution strategy

SKaiNET runs the same model graph through one of two execution strategies:

  • Eager execution β€” DirectCpuExecutionContext calls op implementations as the user invokes them. Used during development, testing, and on-device inference paths where AOT compilation is impractical (debug builds, dynamic graphs). This is the path the 0.21.0 SIMD work targets.

  • Recorded execution β€” RecordingExecution builds an op tape, which HloGenerator lowers to StableHLO MLIR. IREE compiles the MLIR to a portable bytecode for production deployment.

Both strategies share the same TensorOps surface, so a model written once runs in either mode without changes. Numerical parity between modes is part of the test contract.

In both modes the DSL describes computation only: memory residency, weight encodings, layouts and kernel choice are decided outside the model definition β€” per tensor at the load boundary (WeightForm), priced by the memory plan, and selected by format-keyed dispatch. The principle, the level responsibilities, and the evidence for the split are stated in The DSL is compute.

5. Building block view (static structure)

5.1 Module layout

Module path Role

skainet-lang/skainet-lang-core

DSL types, tensor abstractions, common ops, TensorOps / ExecutionContext interfaces. KMP, all targets.

skainet-lang/skainet-lang-models

Reference reusable models (Llama, Gemma, Qwen, Whisper) built on the DSL.

skainet-backends/skainet-backend-api

Neutral backend SPI β€” TensorOps, TensorDataFactory, kernel SPI (KernelProvider, Fp32MatmulKernel, Q4KMatmulKernel, KernelRegistry).

skainet-backends/skainet-backend-cpu

CPU implementation. Eager-execution DefaultCpuOpsBase (commonMain) + DefaultCpuOpsJvm (jvmMain) with SIMD kernels.

skainet-backends/skainet-backend-native-cpu

Native (FFM) kernel provider β€” JVM only, ART has no java.lang.foreign.

skainet-backends/skainet-backend-jni-cpu

Native (JNI) kernel provider for Android β€” same shared C kernels as skainet-backend-native-cpu, reached through JNI instead of FFM since FFM cannot run on ART. See Android NEON Kernels via JNI.

skainet-backends/skainet-backend-xnnpack

Optional XNNPACK CPU backend (FP32 matmul / conv2d / pooling) on linuxX64 / linuxArm64 / Android.

skainet-backends/benchmarks/jvm-cpu-jmh

JMH harness β€” MatmulBench, KernelMatmulBench, QuantizedMatmulBench, ElementwiseAdd1MBench, Reductions1MBench.

skainet-compile/*

Tape recording, StableHLO emission, IREE export.

skainet-io/skainet-io-gguf

The GGUF loader/writer: one loader configured by a single WeightForm (encoding Γ— byte order Γ— shape Γ— residency), resolved per tensor or passed explicitly; StreamingGgufParametersLoader for streaming reads; GGUFWriter/GgufExportFacade for export; I2sRepack/I2sAotConverter for ternary (I2_S) repacking, both load-time and ahead-of-time.

skainet-io/* (remaining)

SafeTensors and ONNX loaders, tokenizers, image I/O, the IREE parameter-archive (.irpa) writer (skainet-io-iree-params).

skainet-apps/skainet-plan

skainet plan <model.gguf> β€” prints a model’s memory plan from its header alone. See Plan a model’s memory before loading it.

skainet-backends/benchmarks/jvm-cpu-publish

Publishes benchmark records as JSON (schema-checked in CI), including generation metrics when a scenario runs a decode loop.

External: SKaiNET-IREE-tools

IREE/StableHLO-target-specific model conversion β€” starting with a standalone GGUF β†’ .irpa (IREE parameter archive) converter for ternary weights β€” lives in the public SKaiNET-IREE-tools repository, not in skainet-io-* (#1207, 2026-08). It is a deliberate duplication of the format logic (GGUF parsing, I2_S repack, the .irpa binary layout) rather than a dependency on SKaiNET core: core stays the authoritative implementation when the two disagree, and a second, target-specific converter repo can evolve (XLA, other IREE targets) without pulling backend-specific concerns into skainet-lang-core.

5.2 Kernel SPI

Introduced in 0.21.0 (PRs #554, #559, #562). The static structure:

androidMain (skainet-backend-jni-cpu)

jvmMain (skainet-backend-native-cpu)

jvmMain (skainet-backend-cpu)

commonMain (skainet-backend-cpu)

jvmMain (skainet-backend-api)

commonMain (skainet-backend-api)

implements

implements

implements

implements

ServiceLoader discovers

ServiceLoader discovers

ServiceLoader discovers

KernelProvider { name, priority, isAvailable(),
matmulFp32(), matmulQ4K(), ... }
KernelRegistry { register(), bestAvailable(), find() }
Fp32MatmulKernel.matmul(...) / Q4KMatmulKernel.matmul(...)

KernelServiceLoader.installAll()

ScalarKernelProvider β€” priority 0
ScalarMatmulKernel, every format, every target

PanamaVectorKernelProvider β€” priority 50
FP32 BF16 Q8_0 Q4_0 Q4_K Q6_K Q5_1 Q5_0 SIMD

NativeKernelProvider β€” priority 100, FFM/C
FP32 BF16 Q8_0 Q4_0 Q4_K Q5_K Q5_1 Q5_0

JniKernelProvider β€” priority 100, JNI/C
same C kernels as native-cpu; ART has no FFM
Q8_0 Q4_0 Q4_K Q5_K Q6_K Q5_1 Q5_0

Five live providers ship. The exact, machine-generated coverage of every weight format on every KMP target is at Kernel Γ— platform support matrix; for how the kernels are implemented see How SIMD Kernels Are Built (FP32), How Quantized SIMD Kernels Are Built (quantized), and Android NEON Kernels via JNI (Android JNI). Packed-quant matmul (Q4_K/Q6_K/Q5_1/Q5_0) also has a commonMain scalar kernel, so it runs on Kotlin/Native, JS and WASM β€” not only the JVM.

Native (FFM) provider β€” JVM only

NativeKernelProvider registers at priority 100 so that on JDK 21+ it wins KernelRegistry.bestAvailable() over the Panama Vector provider whenever the native library loads, and transparently falls back to Panama (priority 50) or scalar (priority 0) when it doesn’t β€” no code change above the registry. It uses FFM, not JNI (near-zero call overhead, no global lock), ships in the skainet-backend-native-cpu module with C kernels for FP32/BF16/Q8_0/Q4_0/Q4_K/Q5_K/Q5_1/Q5_0 (plus a zero-copy MemorySegment Q4_K path), and currently builds for the host architecture only (cross-arch builds and Maven classifier JARs are out of scope). Native FFM kernels for Q5_1/Q5_0 shipped in 0.39.1 (SKaiNET#708); Q6_K has no FFM kernel yet and still resolves to panama-vector on JVM β€” see Kernel Γ— platform support matrix. The kernel SPI this builds on shipped across 0.21.0 (PRs #554–#565); the in-process native-FFM groundwork landed in 0.22.0 (PR #571).

Native (JNI) provider β€” Android only

JniKernelProvider also registers at priority 100 β€” not because it competes with the FFM provider (they never coexist: FFM cannot load on ART, so a process runs one or the other, never both) but because it plays the same role on the platform where FFM isn’t an option. It calls the same shared C kernels as NativeKernelProvider through JNI instead of FFM, ships two .so tiers gated on a /proc/cpuinfo dot-product check (baseline armv8-a vs. armv8.2-a+dotprod, selected once at library-load time), and covers Q8_0/Q4_0/Q5_K/Q5_1/Q5_0/Q4_K/Q6_K β€” not yet dense FP32 (SKaiNET#920). Measured ~6.4Γ— decode speedup over scalar on a Pixel 8a. Shipped in 0.39.0; see Android NEON Kernels via JNI for the full mechanism, including why this does not contradict the FFM decision above.

5.3 View-based kernel dispatch (KernelDispatch)

5.2’s KernelProvider/KernelRegistry picks a SIMD recipe by dtype at install time. A second, later SPI (SKEEP-003 Β§5, 2026-08, #1189–#1193) dispatches a single matmul call by the exact shape of its operands β€” the one KernelDispatch.matmul(…​) flowchart in Β§6 already uses. This subsection is what that flowchart is built on:

registers / finds

KernelKey

op: String

operands: List<OperandKey>

placement: Placement

capabilities: Set<String>

OperandKey

format: Format

layout: LayoutClass

Β«enumerationΒ»

LayoutClass

CONTIGUOUS

STRIDED

BLOCKED_ROW_MAJOR

BLOCKED_INPUT_MAJOR

Β«interfaceΒ»

ViewKernel

key: KernelKey

name: String

+run(inputs, out, sink)

Β«marker interfaceΒ»

MappedCapableKernel

KernelDispatch

+register(kernel)

+find(key) : ViewKernel?

+matmul(a, b, out, scope, sink)

+mappedServableEncodings() : Set<TensorEncoding>

ReferenceMatmulKernel

decodes any format via TensorView.get()

A ViewKernel declares the exact KernelKey it serves instead of an is-ladder over TensorData subclasses β€” Β§993/Β§991 were dispatch bugs, not math bugs, and a declared key is compiler- and test-checkable where an is-ladder was not. LayoutClass names the two things a packed kernel actually varies on: BLOCKED_ROW_MAJOR (canonical file order) versus BLOCKED_INPUT_MAJOR (prepacked feed order) β€” the "block order" contract in Β§8. ReferenceMatmulKernel is registered for every key nothing else can serve; it is correct for any format because it reads through TensorView.get(), which decodes.

MappedCapableKernel is a marker on the two kernels that serve a BLOCKED_ROW_MAJOR weight straight from off-heap or mapped storage as well as from heap bytes β€” FfmRowMajorMatmulKernel (JVM/FFM, skainet-backend-native-cpu) and JniRowMajorMatmulKernel (Android/JNI, skainet-backend-jni-cpu), both introduced in #1189/#1192. Which encodings they actually cover is derived, not hand-declared: KernelDispatch.mappedServableEncodings() scans registered MappedCapableKernel`s for their `BLOCKED_ROW_MAJOR operand (#1193). This matters because StorageCapabilities.mappedServableEncodings (skainet-lang-core, Β§5.4) β€” the thing the memory planner uses to decide whether a tensor can be served from a file mapping at all β€” cannot depend on skainet-backend-api to ask KernelDispatch directly (Β§9: the dependency runs the other way, to avoid a cycle), so it stays a hand-kept constant, cross-checked against the derived set by KernelSupportMatrixTest.generate_and_gate_support_matrix(). A kernel gaining or losing the marker without updating that constant now fails CI instead of drifting silently β€” the same "declare it, don’t rediscover it" move as the KernelKey itself.

Every internal fallback β€” an operand’s storage the fast path can’t read, a strided activation β€” emits TraceEvent.KernelRun naming the kernel and the reason before falling back to ReferenceMatmulKernel, per the "every conversion is an event" principle in Β§8: a silent fallback to the ~1000Γ— slower reference path is exactly the kind of hidden cost that principle exists to surface.

5.4 Memory model

Four questions that a tensor library often answers with one type are answered by four here. Storage owns bytes; Format β€” a (DType, TensorEncoding) pair β€” says what they mean; Layout says where element (i, j) lives inside them; TensorView is the combination, and the only thing a kernel receives. Scope owns lifetime.

holds

faΓ§ade over

allocates and frees

Tensor

DSL handle

TensorData

user-facing read/write

TensorView

Shape + Format + Layout + Storage

+get(indices) : Float

+materialize(format, scope) : TensorView

Β«sealedΒ»

Storage

Heap | OffHeap | Mapped | Device

+slice(offset, length) : Storage

Format

DType + TensorEncoding

Layout

strides, offset, block geometry

Β«interfaceΒ»

Scope

Model | Forward | Ambient

Consequences that shape the rest of the system:

  • A quantized weight is Format(FP32, Q4_K) β€” a float tensor with a packed encoding, not a byte tensor. get() returns the decoded value, so one reference kernel serves every format.

  • Slicing, transposing and striding are Layout arithmetic over the same Storage; materialize() is the only copy point.

  • A ForwardScope is a pre-sized slab, bump-allocated and reset each step, which is what makes memory over a long generation a flat line rather than a staircase.

Full treatment: The memory model.

Deciding Storage before dispatch: AllocationResolver

Which Storage subtype backs a loaded weight is decided once, at load time, by AllocationResolver.resolve(…​) β€” not guessed at by the kernel that later reads it. Its inputs are a PlanTensor (the weight’s requested WeightForm: encoding Γ— byte order Γ— shape Γ— WeightResidency), the running PlatformStorage.current() capabilities, and PlannerProfile’s off-heap threshold (small tensors stay on the managed heap regardless of residency β€” off-heap has a fixed per-segment cost not worth paying under it). `AllocationResolver.servesFromMapping(weight) is the gate that decides whether a tensor can be served straight from a file mapping with zero bytes allocated: WeightResidency.MAPPED and the platform supports mapped files and the file’s bytes are already the target bytes (no encoding/byte-order conversion pending) and the encoding is in StorageCapabilities.mappedServableEncodings (Β§5.3). Get any one of those wrong and the plan silently falls back to heap staging β€” which is exactly the failure mode "plan versus reality" in Β§10 exists to catch.

5.5 Ternary / BitNet weights and AOT conversion

Ternary ({-1, 0, +1}) weights are SKaiNET’s smallest packed encoding (BITNET_B1_58: four codes per byte, one trailing FP32 scale β€” ~16Γ— smaller than FP32) and, until 0.51.0, its least storage-flexible one: they always heap-staged, because BitNetB158TensorData held a bare ByteArray rather than a Storage, and a GGUF’s I2_S bytes usually need repacking before they’re the BITNET_B1_58 byte order at all (#1198).

GROUP_128 / GROUP_64
(BitNet.cpp x86 / ARM)

SEQUENTIAL
NeoGPU convention

below PlannerProfile threshold

above threshold, mmap-eligible

rewrite GROUP_128/64 β†’ SEQUENTIAL once

GGUF I2_S tensor

which layout?

I2sRepack.toSequentialPayload
β€” a real copy, every load

already BITNET_B1_58 order
β€” zero-copy, #1203

BitNetB158TensorData
(Storage-backed, #1202)

Storage.Heap

Storage.Mapped
true zero-copy read

I2sAotConverter
(GGUF β†’ GGUF, ahead of time)

Two follow-on decisions landed with the 0.51.0 work, both recorded in Β§9:

  • Off-heap Storage, not a bare ByteArray (#1202) β€” BitNetB158TensorData gained a Storage-backed constructor and lazy heap snapshot, and the FFM/JNI ternary kernels (TernaryF32GemvKernel, BitNetGemvKernel, NativeKnTernaryF32Gemv, JniTernaryF32Gemv) were widened to read off-heap/mapped storage directly instead of silently falling back to the ~1000Γ— slower decoding reference for any non-heap operand.

  • A native kernel for the grouped BitNet.cpp layouts, proposed and explicitly not built (#1205, closed) β€” I2sAotConverter converts a GROUP_128/GROUP_64 file to SEQUENTIAL once, ahead of time, reaching the same zero-copy mmap path (#1203) as a native grouped-layout decoder would, for a fraction of the ongoing cost of maintaining a third SIMD decode variant. See Ternary networks: getting started for the user-facing walkthrough and #1205’s closing comment for what a grouped-layout kernel would need if a workload someday can’t tolerate an AOT step at all.

The fallback sidecar cache for repeated GROUP_128/GROUP_64 loads that don’t go through the AOT converter (#1198’s original proposal) remains open and unscheduled β€” see Β§11.

6. Runtime view β€” eager execution

A single op, from user code to bytes:

yes

no

yes

no

ctx.ops.matmul(a, b)

DefaultCpuOps* (TensorOps)

packed weight?

quantized fast path
SPI kernel by format

dense FP32?

fp32MatmulKernel
Panama / native / scalar

generic fallback
DefaultCpuOpsBase

output tensor

The kernel-facing half of the same call, for code that has already moved to views:

yes

no

yes

no

KernelDispatch.matmul(view_a, view_w, out)

normalize rank as views
[k] β†’ [1, k], [b, s, k] β†’ [b*s, k]

build KernelKey
(op, formats, layout classes, placement, capabilities)

kernel registered
for this key?

run it

insert a visible adapter
gather Β· requantize Β· relayout

kernel for the
adapted key?

ReferenceMatmulKernel
decodes any format

TraceEvent.KernelRun

Specifics worth calling out:

  • Lazy provider resolution. The kernel properties on the op set are by lazy; first access triggers KernelServiceLoader.installAll() when the registry is empty, then caches. Apps that pre-register providers via KernelRegistry.register(…​) bypass auto-discovery. Kotlin/Native has no ServiceLoader, so it registers manually with installNativeKernels().

  • Fall-through everywhere. Each routing decision returns null on a miss rather than throwing, so a new tensor type or SPI accessor is purely additive.

  • Rank is normalized once. A rank-1 decode step becomes [1, k] as a view before any kernel sees it, which is why a kernel written for rank 2 never meets a rank-1 tensor.

  • Adapters are visible. A gather, a dequantization, a requantization for a ternary kernel or a block relayout allocates in the caller’s scope and emits TraceEvent.AdapterInserted with its byte count. The costs that used to hide inside kernels are events.

A generation loop wraps this in phase spans β€” prefill, decode(step), sample, and nested module spans β€” from which TTFT, tokens per second, the per-module breakdown and the effective memory bandwidth are derived. See explanation/memory-model.adoc#_observability.

7. Deployment view

  • Maven Central β€” every module published as sk.ainet.core:<module>-<target>:<version>. KMP variants land per target (-jvm, -android, -iosarm64, -macosarm64, -linuxx64, -linuxarm64, -js, -wasm-js, *-wasm-wasi).

  • Single BOM β€” sk.ainet:skainet-bom provides a platform() import for downstream Gradle. Note the group is sk.ainet, not sk.ainet.core, so downstream BOMs (e.g. sk.ainet.transformers:skainet-transformers-bom) can import it under the standard umbrella group.

  • Releases β€” tags 0.X.Y on the release branch trigger .github/workflows/publish.yml β†’ ./gradlew publish on macOS-latest with JDK 25.

8. Cross-cutting concepts

  • Numerical parity testing. Every accelerated kernel has a parity test against a scalar reference within a documented tolerance (typically 1e-5 * k or 1e-4 relative). Examples: PanamaVectorMatmulKernelTest, PanamaVectorQ4KMatmulKernelTest, Q6KMatmulTest. The scalar reference is the contract; SIMD speed is a non-functional improvement that must not break the contract.

  • Lazy resource lifetimes. MemorySegmentTensorDataFactory uses Arena.ofAuto() for per-op outputs so output segments are GC-reclaimable. The earlier Arena.ofConfined() builds leaked ~tens of MB per matmul, blowing 32+ GiB of direct memory in inference loops over a 35-layer Gemma 4 forward pass. Fixed in PR #556.

  • Kill switches via system properties. The Vector API code path respects -Dskainet.cpu.vector.enabled=false so a deployment can opt out of incubator code without a recompile. Same pattern for BLAS (-Dskainet.cpu.blas.enabled=true).

  • Scoped memory. Weights and the KV cache live in a ModelScope; one step’s activations live in a pre-sized ForwardScope slab that is reset between steps. Allocation and free events carry the TensorId they back, so a plan can be compared with what a run actually did. See The memory model.

  • Every conversion is an event. A gather, a dequantization, a requantization or a block relayout allocates in the caller’s scope and emits AdapterInserted. A conversion that does not appear in the trace is a bug β€” this is what turned the "hidden allocations" class of issue into something a test can assert.

  • One block-order contract. Packed weights exist in exactly two block orders, carried on the Layout and declared in each kernel’s KernelKey. Mixing them yields plausible wrong numbers rather than a crash, so the contract is normative and published: Packed weight layout.

  • Observability is one stream. Phases, kernel runs, adapters, allocations, scope resets, counters and the memory plan go to a TraceSink; the debugger, the Perfetto/JFR/android.os.Trace exporters, the derived generation metrics and the benchmark JSON are all consumers of it.

9. Architecture decisions

Decision Date Rationale

Kernel SPI parallel to BackendProvider

2026-04 (PR #554)

Matmul / SDPA are model-agnostic; isolating them lets bench harnesses time the SIMD loop directly and lets a future native provider register without touching the op layer.

KernelProvider.matmulQ4K() accessor with default null

2026-04 (PR #562)

Backwards compat for existing providers (Scalar) without forcing every implementation to override. Same pattern will be used for Q6KMatmulKernel / Q4KMemSegMatmulKernel sibling SPIs.

ServiceLoader auto-discovery deferred until 2 providers exist

2026-04 (PR #559)

Single-provider auto-discovery would have been ceremony for nothing; once Panama landed alongside Scalar, the trigger condition was met.

FFM over JNI for the JVM native provider

roadmap M5

JNI’s per-call overhead and global lock are wrong for hot per-token kernels β€” where FFM is available. This did not generalize to "JNI is rejected everywhere": ART has no FFM at all, so skainet-backend-jni-cpu (0.39.0) uses JNI on Android specifically because there is no FFM alternative there. See Android NEON Kernels via JNI.

Antora docs (DiΓ‘taxis), not GitHub Wiki

2025

Source-controlled, branchable, ranked higher in search than wikis, ships with the repo.

Storage-first memory model (SKEEP-003)

2026-08

TensorData and TensorStorage were parallel layers with no answer to "who owns these bytes". Splitting ownership (Storage), meaning (Format), addressing (Layout) and lifetime (Scope) made mapped weights, scoped activations and honest memory plans expressible at all.

Kernels select on a declared KernelKey, not an is-ladder

2026-08

Every quantization bug was arriving as a dispatch bug. A kernel now declares the formats, layouts and capabilities it accepts, and the dispatcher inserts a visible adapter when an operand does not match.

Block order is part of the layout

2026-08

Two block orders for packed weights had been an unwritten contract, contradicting itself in seven places and shipping wrong numbers twice. It is now carried on Layout, declared in the key, and converted only by one engine-owned, idempotent function.

matmulWeightTransposed instead of transposing a packed weight

2026-08

Transposing block-quantized data is not representable β€” blocks quantize runs along the input dimension. What the engine called a packed transpose was a per-call layout copy that was not its own inverse; the primitive ggml and BLAS have takes the weight as [out, in] and converts once.

One WeightForm (encoding Γ— byte order Γ— shape Γ— residency) replaces quantPolicy/staging/weightOrientation; the three axes were removed in #1159 (0.49.0)

2026-08

The three flags asked the caller to resolve, per device, what the resolver can decide from file Γ— profile Γ— kernels; a single resolved value is priceable, traceable, and overridable.

quantPolicy Γ— staging as two axes of one loader (superseded above)

2026-08

"Streaming loader" and "mapped weights helper" were separate code paths that could disagree. What the values are and where the bytes live are independent questions.

Off-heap Storage for ternary weights instead of a bare ByteArray (#1202)

2026-08

BitNetB158TensorData heap-staged unconditionally, which put a 2B-parameter BitNet model’s ~0.6 GB of packed weight over Android’s default ART heap cap. Existing Storage.OffHeap/Mapped infrastructure already solved this for other encodings; ternary just hadn’t been wired to it.

Zero-copy mmap for SEQUENTIAL-layout I2_S tensors (#1203)

2026-08

The NeoGPU converter’s byte order already matches BITNET_B1_58; the loader was defensively copying it anyway. Validating without copying reaches the same AllocationResolver.servesFromMapping fast path every other packed format has.

KernelDispatch.mappedServableEncodings() derived, cross-checked against StorageCapabilities.MAPPED_SERVABLE_DEFAULT (#1193)

2026-08

Two independently hand-maintained lists of which encodings serve from mapped storage had nothing checking they agreed. skainet-lang-core still can’t depend on skainet-backend-api to derive the constant directly, so a guard test (KernelSupportMatrixTest) asserts the two match instead β€” drift now fails CI.

IREE-facing GGUF β†’ .irpa conversion lives in a separate SKaiNET-IREE-tools repo, not skainet-io-* (#1207)

2026-08

skainet-lang-core is already an architectural grey zone with StableHLO; adding XLA- or IREE-target-specific conversion on top would blur it further. A standalone converter (deliberately duplicating GGUF/I2_S format logic rather than depending on core) keeps core target-neutral and lets target-specific tooling evolve independently.

A native decode kernel for BitNet.cpp’s grouped I2_S layouts (GROUP_128/GROUP_64) considered and explicitly not built (#1205, closed)

2026-08

I2sAotConverter reaches the same zero-copy mmap path by converting a file once, ahead of time, for a fraction of the ongoing cost of maintaining a third SIMD decode variant across every backend. Revisit only if a workload genuinely can’t tolerate an AOT step.

10. Quality requirements

  • Performance. Panama FP32 matmul β‰₯1.5Γ— scalar (M5 metric β€” met, ~10Γ— at 1024Β² on Apple Silicon NEON). Native Q4_K matmul β‰₯2.5Γ— scalar dequant baseline (M5 metric β€” deferred to native FFM PR).

  • Numerical equivalence. Every SIMD kernel matches its scalar reference within FP-rounding tolerance (1e-5 * k for matmul, 1e-4 relative for quantized). Pinned by parity tests.

  • Multi-target buildability. ./gradlew allTests (the release gate) must pass on every KMP target β€” JVM, JS, Wasm, macosArm64, iosSimulatorArm64, linuxX64, linuxArm64, Android.

  • Flat memory during generation. A decode step allocates nothing in the forward scope after warm-up, and the resident set does not grow with the number of steps. Asserted on every commit against a synthetic decode harness, and measured on an ARMv8.2 Cortex-A55 reference board: zero major page faults across a run, identical RSS after 4 and 48 steps.

  • Plan versus reality. The header-derived memory plan matches what a run actually allocated within 10 %, checked in CI rather than by inspection.

  • Bit-identical decode across refactors. A golden gate pins the decoded bytes of every packed encoding and the output of every scalar kernel, on JVM and Kotlin/Native. Any change to a decoder, a kernel or a byte layout has to move a recorded digest deliberately.

11. Risks and technical debt

  • Vector API still incubator on JDK 25. JEP 508 keeps it that way through 2026; we depend on it heavily. If it breaks API in a future JDK, every JvmVectorKernels / JvmQuantizedVectorKernels file needs adjustment. Mitigation: thin wrappers, parity tests are a canary.

  • Whole-file mapping only. A file larger than 2 GB is refused rather than mapped in windows.

  • Ternary GGUFs still repack on every load unless converted ahead of time. I2sRepack transparently repacks BitNet.cpp’s grouped I2_S layouts (GROUP_128/GROUP_64) into BITNET_B1_58 order on each load β€” correct, but a real per-load cost for a repeatedly-loaded model. I2sAotConverter (Β§5.5) removes it for anyone who converts once; the automatic fallback sidecar cache for callers who don’t (#1198’s original proposal, tracked as a standalone follow-up, #1204) remains open and is intentionally low priority β€” it isn’t blocking, since the AOT path already covers the shipped-app case.

  • Whole-model plans only mapped-serve seven GGML formats plus dense FP32. StorageCapabilities.MAPPED_SERVABLE_DEFAULT β€” now cross-checked against kernel registrations (#1193, Β§5.3/Β§9) β€” still names a fixed set; a new packed encoding needs a MappedCapableKernel and an update to that constant before it can be served from a mapping. The guard test makes the two drift-safe, not automatic.

  • Hand-written NEON is a modest win over the compiler. On a Cortex-A55 the hand-written bitnet_gemv beats -O3 -ffast-math C by 1.08–1.24Γ—; the large win is being native at all. Worth knowing before investing in more intrinsics on that core.

  • Two reverted optimizations on develop history. MemSeg pool (commit 8642b322) and intra-op matmul parallelism (commit 9ed633b6) were both tried and reverted. Re-attempts need a different strategy than what was tried; the revert commits explain why.

12. Glossary (selected)

Term Meaning

FFM

Foreign Function & Memory API (JEP 442 et seq.). Java 22 stable, 21 preview. Replaces JNI for native interop.

FMA

Fused multiply-add (a Β· b + c in one instruction). Supported by every modern x86_64 (FMA3) and ARMv8 CPU.

ggml

The C library underpinning llama.cpp; defines the canonical Q4_K / Q6_K / Q8_0 block layouts SKaiNET uses for GGUF compatibility.

Block order

Which of the two orders a packed weight’s blocks are in β€” ROW_MAJOR (what a file holds) or INPUT_BLOCK_MAJOR (what the kernels read). Carried on Layout, declared in each KernelKey. See Packed weight layout.

Format

(DType, TensorEncoding) β€” what a tensor’s values mean and how they are stored. A Q4_K weight is Format(FP32, Q4_K).

Layout

Strides, offset and block geometry: where element (i, j) lives inside a Storage.

Prepack

Converting a packed weight’s blocks into the order its kernels read. A copy, done once at load, emitted as a visible adapter β€” not a transpose.

Scope

Who owns an allocation and when it is freed: Model (the session), Forward (one step, reset between steps), Ambient (the GC).

Storage

A byte range with an owner and a lifetime β€” Heap, OffHeap, Mapped or Device.

TensorView

Shape + Format + Layout + Storage; the only thing a kernel receives.

MemSeg

Short for java.lang.foreign.MemorySegment. Off-heap memory abstraction used for mmap’d weight buffers.

Panama

Codename for the JDK Vector API (jdk.incubator.vector) and FFM. Both originate from Project Panama.

SPI

Service provider interface β€” a public interface with multiple registered implementations, looked up at runtime. SKaiNET uses it for backends and now for kernels.

KernelKey / ViewKernel

The view-based kernel dispatch contract (Β§5.3): a ViewKernel declares the exact KernelKey (op, operand formats, LayoutClass`es, placement, capabilities) it serves; `KernelDispatch looks a key up instead of walking an is-ladder.

MappedCapableKernel

Marker interface (Β§5.3) on a ViewKernel that serves its BLOCKED_ROW_MAJOR weight from off-heap/mapped storage as well as heap bytes. KernelDispatch.mappedServableEncodings() derives coverage from kernels carrying this marker.

WeightForm / WeightResidency

A tensor’s requested encoding Γ— byte order Γ— shape Γ— residency, resolved once at load by AllocationResolver (Β§5.4) into a concrete Storage. WeightResidency.MAPPED requests β€” but does not guarantee β€” serving straight from a file mapping.

AOT conversion

Converting a model file once, ahead of time, into a byte order or layout its runtime path can serve with zero copies β€” e.g. I2sAotConverter repacking a BitNet.cpp GGUF’s grouped I2_S layout into BITNET_B1_58 order (Β§5.5), or SKaiNET-IREE-tools' GGUF β†’ .irpa converter for IREE deployment.