
Open-source deep learning framework simplifies creation of modern AI applications, adhering to GitFlow for branching and Semantic Versioning for release management.
Click the diagram for the full architecture reference, or read the short ARCHITECTURE.md.
SKaiNET is a Kotlin Multiplatform AI framework. New here? Choose the path that matches what you want to try first.
| Goal | Start here | Time |
|---|---|---|
| Run tensor operations | Quickstart (below) | 2–5 min |
| Build and train a neural net | Hello Neural Net (below) | 5 min |
| Run a local GGUF model | SKaiNET Transformers starter | 5 min after model setup |
| Export a secure MCU bundle | Minerva getting started | 10 min without firmware flashing |
Working in Java? SKaiNET ships first-class Java support — see the Java getting-started guide.
[!NOTE] Looking for LLM inference? Llama, Qwen, Gemma, Apertus, BERT embeddings and GGUF chat models live in SKaiNET-transformers — this repository is the engine underneath it (tensors, NN DSL, compiler, CPU/native backends, GGUF/SafeTensors IO). Depend on the
sk.ainet.transformersartifacts, pinned together by the transformers BOM.
Use the version shown in this README as the source of truth for first-run snippets. If another page shows a different version, please open an issue or PR.
Add the core dependencies (Gradle Kotlin DSL):
dependencies {
// Recommended: import the umbrella BOM and drop versions on the engine modules.
implementation(platform("sk.ainet:skainet-bom:0.40.1"))
implementation("sk.ainet.core:skainet-lang-core")
implementation("sk.ainet.core:skainet-backend-cpu")
}val model = nn {
input(28 * 28)
dense(out = 128)
relu()
dense(out = 10)
}val a = tensor(shape(2, 2)) { float(1f, 2f, 3f, 4f) }
val b = tensor(shape(2, 2)) { float(5f, 6f, 7f, 8f) }
val c = a matMul b
val d = c.relu()// Recommended: streaming reader — memory-efficient, supports quantized types
val source = JvmRandomAccessSource.open("model.gguf")
StreamingGGUFReader.open(source).use { reader ->
println("Tensors: ${reader.tensorCount}")
// Load specific tensor on demand (no whole-file loading)
val bytes = reader.loadTensor("token_embd.weight")
// Or get a TensorStorage descriptor with encoding/placement metadata
val storage = reader.loadTensorStorage("token_embd.weight")
}More examples: SKaiNET-examples | SKaiNET-notebook
SKaiNET is a modular ecosystem. While this repository contains the core engine, specialized high-level libraries are maintained in standalone repositories:
| Project | Description |
|---|---|
| SKaiNET-transformers | Pre-built transformer architectures and layers |
| SKaiNET-examples | Sample projects and integration demos |
| Goal | Start here |
|---|---|
| Examples and sample projects | SKaiNET-examples |
| Interactive notebooks | SKaiNET-notebook |
| Eager backends & kernels (what runs where) | Backends & kernels mindmap |
| Design proposals and long-lived API decisions | SKEEP proposals |
Small fixes can go straight through the normal contribution flow described in CONTRIBUTING.md and GITFLOW.adoc.
Use a SKEEP when a change affects public APIs, DSL syntax, tensor semantics,
compiler/runtime integration, storage behavior, compatibility policy, or other
decisions that need a durable design record. SKEEP files live under
docs/modules/skeep/pages/ and use three-digit numbering, starting with
001.
SKaiNET ships an official Phoronix-Test-Suite-compatible benchmark
program for the compute engine. See the
methodology and replay docs,
the release manifest, and the
CI workflow. Smoke runs fire
on every PR via ubuntu-latest; full publishable runs fire on a
self-hosted Linux x86 runner on release.
Quick local replay:
./gradlew :skainet-backends:benchmarks:jvm-cpu-publish:shadowJar
./scripts/run_engine_smoke.shSKaiNET is built around one path: a model is defined once in the Kotlin DSL, then either compiled or executed eagerly — without rewriting it.
nn { } / dag { }).ComputeGraph.ComputeGraph through one of several
sibling code-generation backends, each emitting code for a different target
from the same graph:
HloGenerator) → IREE-compilable, for native / edge /
accelerator targets and the wider MLIR ecosystem.StableHLO/MLIR is therefore one code-generation backend among siblings — the IREE/native path next to the C99/Arduino and Minerva MCU paths — not a separate pipeline.
flowchart LR
DSL["Model — Kotlin DSL"] --> Graph["Tape / DAG (ComputeGraph)"]
Graph --> Eager["Eager backend (JVM, …)"]
Graph -->|code generation| HLO["StableHLO / MLIR"]
Graph -->|code generation| C99["Arduino / C99"]
Graph -->|code generation| Minerva["Minerva"]
HLO --> Native["IREE → native / edge / accelerator"]
C99 --> MCU["Microcontroller"]
Minerva --> SecMCU["Secure-MCU bundle"]The same DSL model feeds every path: eager execution for development and JVM deployment, and the code-generation backends — StableHLO/MLIR (→ IREE), Arduino/C99, and Minerva — as sibling alternatives for native, edge, and secure-MCU targets.
SKaiNET now includes a Minerva export backend for secure MCU deployment. It is a sibling to StableHLO and Arduino/C99 export: it starts from a supported ComputeGraph, lowers static MLPs to a Minerva compiler input, invokes libminerva when configured, and packages generated weights, host fixtures, firmware skeletons, and a fingerprinted manifest.json.
Start here:
Runnable examples:
./gradlew :skainet-compile:skainet-compile-minerva:runMinervaSecureMcuExamples
./gradlew :skainet-compile:skainet-compile-minerva:runMinervaSecureMcuExamples \
-Pminerva.example=sensor-classifiersafe-lowbit, balanced, experimental-max. See TurboQuantUsage for integration guide.nn { input(); dense(); relu(); dense() }
dag { } for ResNet, YOLO-style architecturesfile://, https://, hf+https://, and hf://...
.jsonl, .ndjson)val raw = JvmDataSourceResolver().rawDataset {
from("hf://datasets/org/repo@main/train.jsonl")
format(DataFormat.JSON_LINES)
cachePolicy(CachePolicy.Use)
}
val withoutLabel = dataPipeline<RawDataset>()
.stage(
dataTransformer(
name = "drop-label",
outputSchema = { schema -> DataSchema(schema.columns - "label") }
) { dataset ->
val columns = dataset.schema.columns - "label"
dataset.copy(
schema = DataSchema(columns),
rows = dataset.rows.map { row ->
RawDataRow(row.values.filterKeys { key -> key in columns })
}
)
}
)
.execute(raw)HloGenerator
transpose() was silently wrong, not crashing. ops.matmul(x, ops.transpose(W)) on a packed-quantized weight (Q4_0/Q5_0/Q5_1/Q8_0/Q4_K/Q5_K/Q6_K) with more than one quant block per row produced silently incorrect output — sometimes all-zero — across the scalar, Panama-vector, and native (FFM/JNI) kernel tiers, with no exception raised. transpose() now performs a real block-grid byte permutation instead of a shape-only relabel; a misaligned packed tensor now throws instead of silently truncating. Closes #968. Upgrading is strongly recommended for anyone calling ops.transpose() on packed-quantized weights.DEQUANTIZE_TO_FP32 no longer over-allocates. A 1.1B Q4_K_M GGUF transiently needed >12 GB heap against a ~4.4 GB dense-FP32 floor. Three compounding allocation sources in the loader and K-quant kernels are fixed, bringing peak live allocation to ~1.05x of the dense FP32 size.NATIVE_OPTIMIZED.skainet-backend-native-cpu now publishes iosArm64, iosSimulatorArm64, and macosArm64 Kotlin/Native targets with embedded kernel archives; a single Apple arm64 archive dispatches FEAT_DotProd at runtime, so one build serves A12 through M-series.FloatArray buffer, benefiting every non-JVM target — Android, Kotlin/Native, JS/Wasm. DirectCpuExecutionContext.ops is also cached instead of rebuilt per access.skainet-backend-jni-cpu module: the hand-tuned ARM matmul kernels reach Android through a JNI bridge (ART has no java.lang.foreign, so the FFM provider can never run there). Two .so tiers are built from the same sources and selected at load time from /proc/cpuinfo — a baseline armv8-a build that runs on every 64-bit core, and an armv8.2-a+dotprod build for the vdotq_s32 Q4_K/Q6_K paths — so a single artifact is safe from Cortex-A53 up. Measured on a Pixel 8a: ~24 tok/s SmolLM2-135M Q8_0 decode versus ~3.8 scalar (6.4x), clearing the on-device usability bar. The provider auto-registers via ServiceLoader; an app just adds the AAR.createRandomAccessSource returned null on Android, forcing every model load through a full-file heap read that exhausted the ART heap on real devices. It now streams via positional FileChannel reads across skainet-io-gguf / -safetensors / -onnx.skainet-backend-native-cpu (-linuxx64 / -linuxarm64, and the path future Apple targets will use) link with no manual setup. A NEON body was also added for the Q4_0 matmul kernel.FileBacked/Aliased transfer path; and a rank-safe default copyToFloatArray.Dim vocabulary makes "dynamic extent" explicit instead of an overloaded -1, and the StableHLO emitter renders it as an MLIR ?. One compiled vmfb now serves every autoregressive decode step with a growing cache, instead of one fixed cache length. Verified end-to-end: the full FunctionGemma with_past decode graph and the Moonshine v2 decoder (dynamic self and cross caches) self-compile from the DSL to a CPU vmfb — graphs that could not be compiled before. Static graphs are emitted byte-for-byte unchanged.KEEP_NATIVE, two bytes per element at rest instead of widening to FP32, and reach format-specific matmul kernels still packed. Narrow floats are a storage width only: kernels widen to f32 lanes and accumulate in f32.VoidTensorOps propagates shapes through a ShapeOnlyTensorData that allocates no backing buffer, so a dynamic extent flows through a whole decode trace instead of throwing on a negative-size allocation.Lstm layer — single-layer, batch-first LSTM built from existing primitives only, with torch.nn.LSTM-compatible gate order and a caller-owned LstmState + step() API.Dropout, mutable optimizer lr plus linearWarmupCosineDecay, bias-less and open Linear.scaledDotProductAttention at its default scale multiplied every score by zero on the CPU backend; it now resolves to 1/sqrt(headDim) as documented.CrossEntropyLoss no longer detaches the tape, and softmax/logSoftmax/variance backward now work for rank ≥ 3.See CHANGELOG.md for details and the full release history.
We love contributions! Whether it's a new operator, documentation, or a bug fix:
Browse the full codebase documentation on DeepWiki.
transpose() block-grid correctness fix, all three kernel tiers (#968, #969)DEQUANTIZE_TO_FP32 over-allocation fix (#782), native Q5_0/Q5_1 packed matmul kernels (#708), Apple arm64 runtime FEAT_DotProd dispatch (#958), Apple iOS/macOS Kotlin/Native kernel targets (#959)createRandomAccessSource streaming loads (#922), cinterop klib archive embedding (#942), Q4_0 NEON kernel (#939), GGUF loader fail-fast (#919), tensor-storage correctness fixes (#927, #928, #929, #930, #931), AAR release publishing (#947)Lstm layer (#824), Dropout masking (#867), LR schedules (#866), optional/open Linear (#870, #875), SDPA scale fix (#880), autograd fixes (#877), argMax DAG spec (#878), tokenizer + gather fixes (#879), Android native IO targets (#836, #842, #845)MIT — see LICENCE.
Click the diagram for the full architecture reference, or read the short ARCHITECTURE.md.
SKaiNET is a Kotlin Multiplatform AI framework. New here? Choose the path that matches what you want to try first.
| Goal | Start here | Time |
|---|---|---|
| Run tensor operations | Quickstart (below) | 2–5 min |
| Build and train a neural net | Hello Neural Net (below) | 5 min |
| Run a local GGUF model | SKaiNET Transformers starter | 5 min after model setup |
| Export a secure MCU bundle | Minerva getting started | 10 min without firmware flashing |
Working in Java? SKaiNET ships first-class Java support — see the Java getting-started guide.
[!NOTE] Looking for LLM inference? Llama, Qwen, Gemma, Apertus, BERT embeddings and GGUF chat models live in SKaiNET-transformers — this repository is the engine underneath it (tensors, NN DSL, compiler, CPU/native backends, GGUF/SafeTensors IO). Depend on the
sk.ainet.transformersartifacts, pinned together by the transformers BOM.
Use the version shown in this README as the source of truth for first-run snippets. If another page shows a different version, please open an issue or PR.
Add the core dependencies (Gradle Kotlin DSL):
dependencies {
// Recommended: import the umbrella BOM and drop versions on the engine modules.
implementation(platform("sk.ainet:skainet-bom:0.40.1"))
implementation("sk.ainet.core:skainet-lang-core")
implementation("sk.ainet.core:skainet-backend-cpu")
}val model = nn {
input(28 * 28)
dense(out = 128)
relu()
dense(out = 10)
}val a = tensor(shape(2, 2)) { float(1f, 2f, 3f, 4f) }
val b = tensor(shape(2, 2)) { float(5f, 6f, 7f, 8f) }
val c = a matMul b
val d = c.relu()// Recommended: streaming reader — memory-efficient, supports quantized types
val source = JvmRandomAccessSource.open("model.gguf")
StreamingGGUFReader.open(source).use { reader ->
println("Tensors: ${reader.tensorCount}")
// Load specific tensor on demand (no whole-file loading)
val bytes = reader.loadTensor("token_embd.weight")
// Or get a TensorStorage descriptor with encoding/placement metadata
val storage = reader.loadTensorStorage("token_embd.weight")
}More examples: SKaiNET-examples | SKaiNET-notebook
SKaiNET is a modular ecosystem. While this repository contains the core engine, specialized high-level libraries are maintained in standalone repositories:
| Project | Description |
|---|---|
| SKaiNET-transformers | Pre-built transformer architectures and layers |
| SKaiNET-examples | Sample projects and integration demos |
| Goal | Start here |
|---|---|
| Examples and sample projects | SKaiNET-examples |
| Interactive notebooks | SKaiNET-notebook |
| Eager backends & kernels (what runs where) | Backends & kernels mindmap |
| Design proposals and long-lived API decisions | SKEEP proposals |
Small fixes can go straight through the normal contribution flow described in CONTRIBUTING.md and GITFLOW.adoc.
Use a SKEEP when a change affects public APIs, DSL syntax, tensor semantics,
compiler/runtime integration, storage behavior, compatibility policy, or other
decisions that need a durable design record. SKEEP files live under
docs/modules/skeep/pages/ and use three-digit numbering, starting with
001.
SKaiNET ships an official Phoronix-Test-Suite-compatible benchmark
program for the compute engine. See the
methodology and replay docs,
the release manifest, and the
CI workflow. Smoke runs fire
on every PR via ubuntu-latest; full publishable runs fire on a
self-hosted Linux x86 runner on release.
Quick local replay:
./gradlew :skainet-backends:benchmarks:jvm-cpu-publish:shadowJar
./scripts/run_engine_smoke.shSKaiNET is built around one path: a model is defined once in the Kotlin DSL, then either compiled or executed eagerly — without rewriting it.
nn { } / dag { }).ComputeGraph.ComputeGraph through one of several
sibling code-generation backends, each emitting code for a different target
from the same graph:
HloGenerator) → IREE-compilable, for native / edge /
accelerator targets and the wider MLIR ecosystem.StableHLO/MLIR is therefore one code-generation backend among siblings — the IREE/native path next to the C99/Arduino and Minerva MCU paths — not a separate pipeline.
flowchart LR
DSL["Model — Kotlin DSL"] --> Graph["Tape / DAG (ComputeGraph)"]
Graph --> Eager["Eager backend (JVM, …)"]
Graph -->|code generation| HLO["StableHLO / MLIR"]
Graph -->|code generation| C99["Arduino / C99"]
Graph -->|code generation| Minerva["Minerva"]
HLO --> Native["IREE → native / edge / accelerator"]
C99 --> MCU["Microcontroller"]
Minerva --> SecMCU["Secure-MCU bundle"]The same DSL model feeds every path: eager execution for development and JVM deployment, and the code-generation backends — StableHLO/MLIR (→ IREE), Arduino/C99, and Minerva — as sibling alternatives for native, edge, and secure-MCU targets.
SKaiNET now includes a Minerva export backend for secure MCU deployment. It is a sibling to StableHLO and Arduino/C99 export: it starts from a supported ComputeGraph, lowers static MLPs to a Minerva compiler input, invokes libminerva when configured, and packages generated weights, host fixtures, firmware skeletons, and a fingerprinted manifest.json.
Start here:
Runnable examples:
./gradlew :skainet-compile:skainet-compile-minerva:runMinervaSecureMcuExamples
./gradlew :skainet-compile:skainet-compile-minerva:runMinervaSecureMcuExamples \
-Pminerva.example=sensor-classifiersafe-lowbit, balanced, experimental-max. See TurboQuantUsage for integration guide.nn { input(); dense(); relu(); dense() }
dag { } for ResNet, YOLO-style architecturesfile://, https://, hf+https://, and hf://...
.jsonl, .ndjson)val raw = JvmDataSourceResolver().rawDataset {
from("hf://datasets/org/repo@main/train.jsonl")
format(DataFormat.JSON_LINES)
cachePolicy(CachePolicy.Use)
}
val withoutLabel = dataPipeline<RawDataset>()
.stage(
dataTransformer(
name = "drop-label",
outputSchema = { schema -> DataSchema(schema.columns - "label") }
) { dataset ->
val columns = dataset.schema.columns - "label"
dataset.copy(
schema = DataSchema(columns),
rows = dataset.rows.map { row ->
RawDataRow(row.values.filterKeys { key -> key in columns })
}
)
}
)
.execute(raw)HloGenerator
transpose() was silently wrong, not crashing. ops.matmul(x, ops.transpose(W)) on a packed-quantized weight (Q4_0/Q5_0/Q5_1/Q8_0/Q4_K/Q5_K/Q6_K) with more than one quant block per row produced silently incorrect output — sometimes all-zero — across the scalar, Panama-vector, and native (FFM/JNI) kernel tiers, with no exception raised. transpose() now performs a real block-grid byte permutation instead of a shape-only relabel; a misaligned packed tensor now throws instead of silently truncating. Closes #968. Upgrading is strongly recommended for anyone calling ops.transpose() on packed-quantized weights.DEQUANTIZE_TO_FP32 no longer over-allocates. A 1.1B Q4_K_M GGUF transiently needed >12 GB heap against a ~4.4 GB dense-FP32 floor. Three compounding allocation sources in the loader and K-quant kernels are fixed, bringing peak live allocation to ~1.05x of the dense FP32 size.NATIVE_OPTIMIZED.skainet-backend-native-cpu now publishes iosArm64, iosSimulatorArm64, and macosArm64 Kotlin/Native targets with embedded kernel archives; a single Apple arm64 archive dispatches FEAT_DotProd at runtime, so one build serves A12 through M-series.FloatArray buffer, benefiting every non-JVM target — Android, Kotlin/Native, JS/Wasm. DirectCpuExecutionContext.ops is also cached instead of rebuilt per access.skainet-backend-jni-cpu module: the hand-tuned ARM matmul kernels reach Android through a JNI bridge (ART has no java.lang.foreign, so the FFM provider can never run there). Two .so tiers are built from the same sources and selected at load time from /proc/cpuinfo — a baseline armv8-a build that runs on every 64-bit core, and an armv8.2-a+dotprod build for the vdotq_s32 Q4_K/Q6_K paths — so a single artifact is safe from Cortex-A53 up. Measured on a Pixel 8a: ~24 tok/s SmolLM2-135M Q8_0 decode versus ~3.8 scalar (6.4x), clearing the on-device usability bar. The provider auto-registers via ServiceLoader; an app just adds the AAR.createRandomAccessSource returned null on Android, forcing every model load through a full-file heap read that exhausted the ART heap on real devices. It now streams via positional FileChannel reads across skainet-io-gguf / -safetensors / -onnx.skainet-backend-native-cpu (-linuxx64 / -linuxarm64, and the path future Apple targets will use) link with no manual setup. A NEON body was also added for the Q4_0 matmul kernel.FileBacked/Aliased transfer path; and a rank-safe default copyToFloatArray.Dim vocabulary makes "dynamic extent" explicit instead of an overloaded -1, and the StableHLO emitter renders it as an MLIR ?. One compiled vmfb now serves every autoregressive decode step with a growing cache, instead of one fixed cache length. Verified end-to-end: the full FunctionGemma with_past decode graph and the Moonshine v2 decoder (dynamic self and cross caches) self-compile from the DSL to a CPU vmfb — graphs that could not be compiled before. Static graphs are emitted byte-for-byte unchanged.KEEP_NATIVE, two bytes per element at rest instead of widening to FP32, and reach format-specific matmul kernels still packed. Narrow floats are a storage width only: kernels widen to f32 lanes and accumulate in f32.VoidTensorOps propagates shapes through a ShapeOnlyTensorData that allocates no backing buffer, so a dynamic extent flows through a whole decode trace instead of throwing on a negative-size allocation.Lstm layer — single-layer, batch-first LSTM built from existing primitives only, with torch.nn.LSTM-compatible gate order and a caller-owned LstmState + step() API.Dropout, mutable optimizer lr plus linearWarmupCosineDecay, bias-less and open Linear.scaledDotProductAttention at its default scale multiplied every score by zero on the CPU backend; it now resolves to 1/sqrt(headDim) as documented.CrossEntropyLoss no longer detaches the tape, and softmax/logSoftmax/variance backward now work for rank ≥ 3.See CHANGELOG.md for details and the full release history.
We love contributions! Whether it's a new operator, documentation, or a bug fix:
Browse the full codebase documentation on DeepWiki.
transpose() block-grid correctness fix, all three kernel tiers (#968, #969)DEQUANTIZE_TO_FP32 over-allocation fix (#782), native Q5_0/Q5_1 packed matmul kernels (#708), Apple arm64 runtime FEAT_DotProd dispatch (#958), Apple iOS/macOS Kotlin/Native kernel targets (#959)createRandomAccessSource streaming loads (#922), cinterop klib archive embedding (#942), Q4_0 NEON kernel (#939), GGUF loader fail-fast (#919), tensor-storage correctness fixes (#927, #928, #929, #930, #931), AAR release publishing (#947)Lstm layer (#824), Dropout masking (#867), LR schedules (#866), optional/open Linear (#870, #875), SDPA scale fix (#880), autograd fixes (#877), argMax DAG spec (#878), tokenizer + gather fixes (#879), Android native IO targets (#836, #842, #845)MIT — see LICENCE.