From 64b842a803a37756cee42d32630b4e1526827c04 Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Thu, 8 Oct 2026 15:52:57 +0200 Subject: [PATCH 1/2] Update skill content from SKaiNET ~0.20.0 to 0.57.0 Every API claim re-verified against the SKaiNET 0.57.0 source tree. Both plugins bumped to 0.2.0. Consumer skills: - Toolchain: published JVM jars are Java 21 bytecode (JDK 21+ required); Android compilations stay at JVM 11 - dag { } moved to its own artifact skainet-lang-dag; TokenizerFactory moved to sk.ainet.io.tokenizer (ships in skainet-io-core) - Artifact tables rebuilt for 0.57.0 (new published modules: lang-dag, compile-c/-json/-minerva/-opt, data-source, io-iree-params, backend-native-cpu, backend-jni-cpu; test-groundtruth is not published) - GGUFModelReader is a deprecated fail-fast stub: examples rewritten to StreamingGgufParametersLoader; added ShardedSafeTensorsParametersLoader, tensorFilter, and narrow-float load policies - Schedule API (0.54.0): ctx.schedule / withSchedule / CoroutineSchedule covered in inference and Android threading guidance - TurboQuant re-anchored to its real KV-cache surface (KvCacheStore, TurboQuantPresets); dropped unsupported weight-compression advice - DSL: full 14-dtype set incl. BF16, GQA-native scaledDotProductAttention, dag schedule hints and dtype policies, complete transform list, fixed non-compiling unsqueeze example; ExperimentalMemoryApi opt-in removed - Fixed invented APIs: ForwardHooks.onLayerOutput, module.setParameter, JvmFileRandomAccessSource/BytesRandomAccessSource replaced with the real interfaces - Android: jni-cpu NEON backend with ServiceLoader auto-discovery, mapped GGUF weights (AndroidGguf, WeightResidency.MAPPED), minSdk 24 Contributor skills: - Test policy: kotlin.test in commonTest AND jvmTest; Kotest survives only in the skainet-apps CLI property tests - New-module workflow: sk.ainet.multiplatform convention plugin, per-module gradle.properties POM coordinates, BOM membership automatic via sk.ainet.transformers.bom-coverage - JDK 21 floor set centrally; catalog reference now treats libs.versions.toml as source of truth (0.57.0 snapshot included) - SKEEP/DARC contribution process rules added Eval fixtures aligned with the corrected policies. Co-Authored-By: Claude Fable 5 --- .claude-plugin/marketplace.json | 4 +- .../.claude-plugin/plugin.json | 2 +- .../skainet-android-integration/SKILL.md | 48 +++++--- .../references/asset-loading.md | 17 ++- .../references/lifecycle-and-threading.md | 15 ++- .../skills/skainet-consumer-setup/SKILL.md | 41 ++++--- .../skainet-consumer-setup/evals/evals.json | 2 +- .../references/artifact-picker.md | 26 +++- .../references/bom-coordinates.md | 38 ++++-- .../skills/skainet-data-dsl/SKILL.md | 25 ++-- .../references/tensor-builders.md | 33 ++++- .../references/transform-ops.md | 66 +++++++++- .../skills/skainet-inference/SKILL.md | 55 +++++++-- .../references/execution-context.md | 61 ++++++--- .../references/forward-pass.md | 15 +-- .../references/turboquant.md | 74 +++++++---- .../skills/skainet-java-consumer/SKILL.md | 50 ++++++-- .../skainet-java-consumer/evals/evals.json | 2 +- .../references/java-entry-points.md | 41 ++++--- .../references/maven-deps.md | 11 +- .../skills/skainet-model-loading/SKILL.md | 116 +++++++++++------- .../references/format-picker.md | 12 +- .../references/loaders.md | 102 ++++++++++----- .../skills/skainet-nn-dsl/SKILL.md | 37 ++++-- .../skainet-nn-dsl/references/dag-nodes.md | 45 ++++--- .../skainet-nn-dsl/references/layers.md | 67 ++++++---- .../references/sequential-vs-dag.md | 4 +- .../.claude-plugin/plugin.json | 2 +- skainet-contributor-skills/skills/INDEX.md | 4 +- .../skills/gradle-multimodule/SKILL.md | 64 +++++++--- .../gradle-multimodule/evals/evals.json | 26 ++-- .../references/catalog-aliases.md | 65 ++++++---- .../references/convention-plugins.md | 57 ++++++--- .../skills/kmp/SKILL.md | 45 +++++-- .../skills/kmp/evals/evals.json | 2 +- .../skills/kotlin/SKILL.md | 8 +- .../skills/skainet-java-interop/SKILL.md | 16 +-- .../skills/skainet-testing/SKILL.md | 26 ++-- .../skills/skainet-testing/evals/evals.json | 11 +- 39 files changed, 926 insertions(+), 409 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index cde3948..2ae5476 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -8,7 +8,7 @@ "name": "skainet-contributor-skills", "source": "./skainet-contributor-skills", "description": "Skills for contributing TO SKaiNET. Activate when editing files inside the SKaiNET repository: Kotlin idioms, multi-module Gradle + version catalog, KMP source-sets, Java-interop facade convention, in-repo test strategy.", - "version": "0.1.0", + "version": "0.2.0", "tags": [ "skainet", "contributor", @@ -21,7 +21,7 @@ "name": "skainet-consumer-skills", "source": "./skainet-consumer-skills", "description": "Skills for USING SKaiNET in your own app. Activate when adding SKaiNET to a consumer Gradle project, building tensors and networks via the public DSL, loading models, running inference, integrating on Android, or consuming from pure Java.", - "version": "0.1.0", + "version": "0.2.0", "tags": [ "skainet", "consumer", diff --git a/skainet-consumer-skills/.claude-plugin/plugin.json b/skainet-consumer-skills/.claude-plugin/plugin.json index be54a91..6abc7ec 100644 --- a/skainet-consumer-skills/.claude-plugin/plugin.json +++ b/skainet-consumer-skills/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "skainet-consumer-skills", - "version": "0.1.0", + "version": "0.2.0", "description": "Skills for using SKaiNET as a library in your own Kotlin Multiplatform / JVM / Android / Java app. Covers BOM-based dependency setup, the data and neural-network DSLs, model loading (GGUF / ONNX / SafeTensors), running inference, Android integration, and pure-Java consumers calling the sk.ainet.java facade. Contributor-side skills (working inside the SKaiNET repo itself) live in the sibling skainet-contributor-skills plugin.", "author": { "name": "SKaiNET-developers", diff --git a/skainet-consumer-skills/skills/skainet-android-integration/SKILL.md b/skainet-consumer-skills/skills/skainet-android-integration/SKILL.md index 761589f..8957220 100644 --- a/skainet-consumer-skills/skills/skainet-android-integration/SKILL.md +++ b/skainet-consumer-skills/skills/skainet-android-integration/SKILL.md @@ -26,18 +26,20 @@ Android-specific concerns for SKaiNET consumers: how to load model files from `a ## Hard rules 1. **`minSdk` = 24.** SKaiNET's Android target is built for `minSdk = 24`, `compileSdk = 36`. Lower is unsupported; higher is fine. -2. **Ship ARM64 at minimum; ARM32 only if you must.** The CPU backend has NEON paths for both, but ARM64 is the primary target. Do not ship x86 / x86_64 unless you specifically support emulator-only builds — production devices are ARM. Configure `ndk.abiFilters` accordingly to keep APK size down. -3. **Never call `forward(...)` on the main thread.** Use `viewModelScope.launch { withContext(Dispatchers.Default) { ... } }` from a `ViewModel`, or `lifecycleScope.launch { ... }` from an `Activity`/`Fragment`. The CPU backend can take hundreds of ms to seconds; the main thread has a 16 ms budget. -4. **Open assets with `context.assets.open(name)`. Don't decode the path as a `File` — there is no path inside the APK.** Use a `RandomAccessSource` factory that wraps the `InputStream` if the loader needs random access (some loaders do; SafeTensors and GGUF need random access — read the asset to a cache file in `context.cacheDir` and load from there if random access is required). -5. **Hold one `ExecutionContext` and one `Module` on the `Application` object** (or in a DI scope spanning the app lifetime). They're heavyweight; per-Activity construction is wasteful. -6. **React to `onTrimMemory(level)`.** When `level >= TRIM_MEMORY_BACKGROUND`, drop the `Module` (and any cached tensors); rebuild on next access. Holding multi-GB models across a backgrounded app is a sure path to LMK kills. -7. **Cache the model file once.** If the asset is a multi-GB GGUF that must be unpacked from the APK, copy it to `context.filesDir` (or `context.cacheDir`) on first launch and load from there subsequently. Loading from `assets/` repeatedly extracts on every cold start. +2. **Ship ARM64 at minimum; ARM32 only if you must.** ARM64 is the primary target. Do not ship x86 / x86_64 unless you specifically support emulator-only builds — production devices are ARM. Configure `ndk.abiFilters` accordingly to keep APK size down. +3. **Add `skainet-backend-jni-cpu` for real speed.** ART has no `java.lang.foreign`, so the JVM's FFM kernel provider can never run on Android; without the JNI AAR, inference runs on the scalar reference floor. The AAR ships NDK-built kernel `.so`s (`arm64-v8a` + `x86_64`, 16 KB-page-aligned for Android 15+; two NEON tiers selected at load time from `/proc/cpuinfo`) and a `JniKernelProvider` that the Android ops factory discovers via `ServiceLoader` automatically — no bootstrap call. Consumer R8 rules keeping the ServiceLoader entry are shipped in the AAR. +4. **Never call `forward(...)` on the main thread.** Use `viewModelScope.launch { withContext(Dispatchers.Default) { ... } }` from a `ViewModel`, or `lifecycleScope.launch { ... }` from an `Activity`/`Fragment`. The CPU backend can take hundreds of ms to seconds; the main thread has a 16 ms budget. +5. **Open assets with `context.assets.open(name)`. Don't decode the path as a `File` — there is no path inside the APK.** SafeTensors and GGUF loaders need random access: copy the asset to a real file in `context.cacheDir` / `context.filesDir` once, then open it with `openRandomAccessSource(path)` or `AndroidGguf.loader(path)`. +6. **Prefer mapped weights for GGUF.** The ART heap is hard-capped (256 MB, 512 MB with `largeHeap`) no matter the device RAM. `AndroidGguf.loader(path)` defaults to `WeightForm(residency = WeightResidency.MAPPED)`: weights are served from file-backed pages the OS pages in on demand and evicts under pressure — they never count against the heap cap. Check fit *before* loading with `AndroidGguf.plan(path, ctx)` + `AndroidGguf.deviceMemory(context)`. +7. **Hold one `ExecutionContext` and one `Module` on the `Application` object** (or in a DI scope spanning the app lifetime). They're heavyweight; per-Activity construction is wasteful. +8. **React to `onTrimMemory(level)`.** When `level >= TRIM_MEMORY_BACKGROUND`, drop the `Module` (and any cached tensors / KV cache); rebuild on next access. Mapped weight pages are evictable by the OS on their own, but heap-staged tensors are not. +9. **Cache the model file once.** If the asset is a multi-GB GGUF that must be unpacked from the APK, copy it to `context.filesDir` (or `context.cacheDir`) on first launch and load from there subsequently. Loading from `assets/` repeatedly extracts on every cold start — and mmap needs a real file anyway. ## Workflow -1. Add the SKaiNET BOM + `skainet-lang-core` + `skainet-backend-cpu` (see `skainet-consumer-setup`). For loaders, add `skainet-io-core` + the format-specific artifact. +1. Add the SKaiNET BOM + `skainet-lang-core` + `skainet-backend-cpu` + `skainet-backend-jni-cpu` (NEON kernels; see `skainet-consumer-setup`). For loaders, add `skainet-io-core` + the format-specific artifact. 2. Pick where to host the `ExecutionContext` and `Module` — usually a singleton tied to `Application` lifetime, or a Hilt/Koin app-scope binding. -3. Load the model file (asset, downloaded cache, or remote): see Canonical examples. +3. Load the model file (asset, downloaded cache, or remote): see Canonical examples. For GGUF, run the header-only fit check first (`AndroidGguf.plan` / `fitOn`). 4. Wire the forward pass through a coroutine on `Dispatchers.Default`. 5. Handle `onTrimMemory` to release the `Module` under pressure. 6. Test on a real ARM64 device — emulators don't surface ABI / NEON issues. @@ -74,6 +76,9 @@ class SkainetApp : Application() { **Loading a GGUF from assets via cacheDir (random-access loaders need a real file):** ```kotlin +import sk.ainet.io.gguf.AndroidGguf +import sk.ainet.lang.types.FP32 + private suspend fun copyAssetIfNeeded(context: Context, name: String): java.io.File = withContext(Dispatchers.IO) { val target = java.io.File(context.cacheDir, name) @@ -85,12 +90,18 @@ private suspend fun copyAssetIfNeeded(context: Context, name: String): java.io.F target } -private suspend fun loadGGUF(context: Context): GGUFModelReader = withContext(Dispatchers.IO) { +private suspend fun loadGguf(context: Context, ctx: ExecutionContext) = withContext(Dispatchers.IO) { val file = copyAssetIfNeeded(context, "model-q4.gguf") - GGUFModelReader(/* RandomAccessSource factory wrapping `file` */) + // Mapped residency by default: tensor payloads come from mmap'd pages, not the ART heap. + val loader = AndroidGguf.loader(file.absolutePath) + loader.load(ctx, FP32::class) { name, tensor -> + // bind 'name' -> 'tensor' into your Module's parameters + } } ``` +`AndroidGguf` (in `skainet-io-gguf`, androidMain) also offers the pre-load fit check: `AndroidGguf.plan(path, ctx)` prices the model from the GGUF header alone (a few KB of reads), and `AndroidGguf.deviceMemory(context)` reports RAM + the ART heap cap — combine with `fitOn` to refuse a model *before* the OOM kill. + **Inference in a ViewModel:** ```kotlin @@ -120,14 +131,20 @@ class ClassifyViewModel( **Asset-resident SafeTensors via cache-first pattern (same idea as GGUF):** ```kotlin +import sk.ainet.io.openRandomAccessSource +import sk.ainet.io.weights.WeightMapper +import sk.ainet.io.weights.WeightTensor + suspend fun loadSafeTensors(context: Context, ctx: ExecutionContext, module: Module) { val file = copyAssetIfNeeded(context, "weights.safetensors") val loader = SafeTensorsParametersLoader( - sourceProvider = { JvmFileRandomAccessSource(file) } + sourceProvider = { openRandomAccessSource(file.absolutePath)!! } ) + val loaded = mutableListOf>() loader.load(ctx, FP32::class) { name, tensor -> - module.setParameter(name, tensor) + loaded += WeightTensor(name, tensor.shape.dimensions.toList(), tensor) } + WeightMapper.applyWeights(module, loaded) // name-based binding into the Module tree } ``` @@ -151,6 +168,7 @@ dependencies { implementation(platform(libs.skainet.bom)) implementation(libs.skainet.lang.core) implementation(libs.skainet.backend.cpu) + implementation(libs.skainet.backend.jni.cpu) // AAR: NEON kernels via JNI; ServiceLoader auto-discovery implementation(libs.skainet.io.core) implementation(libs.skainet.io.gguf) } @@ -182,12 +200,12 @@ val ctx = app.executionContext() ```kotlin // WRONG — assets opened as a File path (path doesn't exist inside the APK) -val reader = GGUFModelReader(JvmFileRandomAccessSource(File("file:///android_asset/model.gguf"))) +val loader = AndroidGguf.loader("file:///android_asset/model.gguf") ``` ```kotlin -// RIGHT — copy to cacheDir, load from there +// RIGHT — copy to cacheDir, load from there (mapped weights need a real file) val file = copyAssetIfNeeded(context, "model.gguf") -val reader = GGUFModelReader(/* source factory wrapping `file` */) +val loader = AndroidGguf.loader(file.absolutePath) ``` ```kotlin diff --git a/skainet-consumer-skills/skills/skainet-android-integration/references/asset-loading.md b/skainet-consumer-skills/skills/skainet-android-integration/references/asset-loading.md index 27da684..56d8211 100644 --- a/skainet-consumer-skills/skills/skainet-android-integration/references/asset-loading.md +++ b/skainet-consumer-skills/skills/skainet-android-integration/references/asset-loading.md @@ -28,15 +28,30 @@ private suspend fun copyAssetIfNeeded(context: Context, assetName: String): File Use `cacheDir` if the model can be re-downloaded; `filesDir` if losing it would brick the feature. +Once the file exists on disk, open it by path: + +```kotlin +// GGUF — mapped weights by default (page cache, not ART heap): +val loader = AndroidGguf.loader(file.absolutePath) // sk.ainet.io.gguf (androidMain) + +// SafeTensors — the cross-platform source factory: +val stLoader = SafeTensorsParametersLoader( + sourceProvider = { openRandomAccessSource(file.absolutePath)!! } // sk.ainet.io +) +``` + +On Android, `openRandomAccessSource` is backed by `AndroidRandomAccessSource` (positional `FileChannel` reads — thread-safe, API 1+), so streaming loaders work without heap-loading the whole file. + ## ONNX is sequential — `assets.open(...).asSource()` works ONNX is a single-pass parse. The `OnnxLoader.fromModelSource { ... }` lambda can return a `Source` backed directly by an asset stream: ```kotlin import kotlinx.io.asSource +import kotlinx.io.buffered val loader = OnnxLoader.fromModelSource { - context.assets.open("model.onnx").asSource() + context.assets.open("model.onnx").asSource().buffered() } ``` diff --git a/skainet-consumer-skills/skills/skainet-android-integration/references/lifecycle-and-threading.md b/skainet-consumer-skills/skills/skainet-android-integration/references/lifecycle-and-threading.md index 3f3344e..1499204 100644 --- a/skainet-consumer-skills/skills/skainet-android-integration/references/lifecycle-and-threading.md +++ b/skainet-consumer-skills/skills/skainet-android-integration/references/lifecycle-and-threading.md @@ -28,6 +28,15 @@ The dispatcher contract: `forward(...)` is CPU-bound; never use `Dispatchers.Main` or `Dispatchers.IO` for it. +## Intra-op threading: the `Schedule`, and the JNI kernel pool + +Two separate layers (0.54.0+): + +- **`ctx.schedule`** (`sk.ainet.context.schedule.Schedule`) says how an op's independent work maps onto tasks. On Android the platform default is `Schedule.Sequential` — one task, the caller's thread. (The JVM's `CoroutineSchedule` lives in `skainet-backend-cpu` *jvmMain* and is not part of the Android compilation.) A schedule never changes results. +- **The native kernels thread themselves.** With `skainet-backend-jni-cpu`, the packed-quant matmul kernels run on their own native worker pool (engaged at `outputDim >= 512`, spin-then-park — the llama.cpp trick), independent of the Kotlin dispatcher you call `forward` from. Results are bit-identical to single-threaded. So one `forward` call on `Dispatchers.Default` already uses multiple cores for the heavy matmuls. + +Practical consequence: don't fan one request out over many coroutines to "parallelise" a single forward pass — run one forward per request on `Dispatchers.Default` and let the kernels thread. + ## Memory pressure — `onTrimMemory(level)` Override on `Application`: @@ -55,6 +64,8 @@ override fun onTrimMemory(level: Int) { After dropping, set the holder reference to `null`. The next `model()` call rebuilds — make sure the rebuild path is fast (cached weight file vs. re-download). +Note on mapped GGUF weights (`AndroidGguf.loader`, default `WeightResidency.MAPPED`): tensor payloads live in file-backed page cache, which the OS evicts under pressure on its own — they never count against the ART heap cap. `onTrimMemory` still matters for everything heap-staged: the KV cache, activations, dense/repacked tensors, and your own caches. + ## ABI filters ```kotlin @@ -70,11 +81,13 @@ What to ship: - **`arm64-v8a` + `armeabi-v7a`** — adds ~30% to native lib size; covers older / cheaper devices. - **`x86_64`** — emulator only (Android Studio runs x86 emulators). Don't ship in the production split. +The `skainet-backend-jni-cpu` AAR builds `.so`s for `arm64-v8a` and `x86_64`, 16 KB-page-aligned (required on Android 15+ devices with 16 KB pages). On arm64, two tiers are built from the same sources and selected at load time from `/proc/cpuinfo`: baseline `armv8-a` NEON (runs on every arm64 core) and `armv8.2-a+fp16+dotprod` (faster q4k/q6k paths, gated so Cortex-A53-class cores don't SIGILL). On 32-bit ARM devices the scalar reference kernels run; benchmark on arm64 hardware. + Use App Bundles (`AAB`) — Play Store auto-splits per-device, so you can list multiple ABIs without paying the size cost on every device. ## Locking (multi-coroutine forward) -`DirectCpuExecutionContext` is thread-safe for read-only inference. If two coroutines might enter `forward(...)` simultaneously, that's fine — they share the context, not the model state. +Sharing one `DirectCpuExecutionContext` across coroutines doing inference is the normal pattern; since 0.54.0 the kernel registries are safe for concurrent reads. If two coroutines might enter `forward(...)` simultaneously, that's fine — they share the context, not the model state. If you're updating the `Module` (loading new weights, swapping), guard with a `Mutex`: diff --git a/skainet-consumer-skills/skills/skainet-consumer-setup/SKILL.md b/skainet-consumer-skills/skills/skainet-consumer-setup/SKILL.md index b8ff9b6..81355b2 100644 --- a/skainet-consumer-skills/skills/skainet-consumer-setup/SKILL.md +++ b/skainet-consumer-skills/skills/skainet-consumer-setup/SKILL.md @@ -26,13 +26,13 @@ Add SKaiNET to a Gradle project that does NOT live inside the SKaiNET repo. Cent 1. **Use the BOM.** Add `implementation(platform("sk.ainet:skainet-bom:"))` (or the catalog-accessor equivalent) and depend on `sk.ainet.core:skainet-*` artifacts WITHOUT specifying versions. Hard-pinning per-artifact versions silently lets transitives drift. 2. **The BOM coordinate is `sk.ainet:skainet-bom`. The library coordinates are `sk.ainet.core:skainet-`.** They look like a typo; they're not. The BOM module overrides its group from the project default; libraries publish under `sk.ainet.core`. -3. **At minimum, every consumer needs `skainet-lang-core` + one backend.** `sk.ainet.core:skainet-lang-core` provides the DSL and types; `sk.ainet.core:skainet-backend-cpu` provides the default CPU runtime. Without the backend, `DirectCpuExecutionContext.create()` won't resolve. +3. **At minimum, every consumer needs `skainet-lang-core` + one backend.** `sk.ainet.core:skainet-lang-core` provides the DSL and types; `sk.ainet.core:skainet-backend-cpu` provides the default CPU runtime. Without the backend, `DirectCpuExecutionContext.create()` won't resolve. The `dag { }` graph DSL lives in its own artifact, `sk.ainet.core:skainet-lang-dag` — `sequential { }` stays in `skainet-lang-core`. 4. **Version catalog over inline coordinates.** A consumer project that already uses `gradle/libs.versions.toml` MUST register SKaiNET there too. Mixing styles inside one project is forbidden. -5. **JVM target ≥ 11.** SKaiNET's published `.jar` is compiled for JVM 11. Configuring the consumer below 11 fails at link time. +5. **JVM target ≥ 21 on the JVM.** SKaiNET's published JVM `.jar`s are compiled to Java 21 bytecode (the root build sets `jvmTarget = JVM_21` / `--release 21` for all JVM targets). A JVM consumer below 21 fails at link time. The Android compilations of the KMP modules target JVM 11, so Android consumers only need `compileOptions` at 11+. ## Workflow -1. **Pick the version.** Use the latest stable release from . SNAPSHOT (`0.20.0-SNAPSHOT`) only if the consumer explicitly opts in to the snapshot repo. +1. **Pick the version.** Use the latest stable release from (`0.57.0` at the time of writing). A `-SNAPSHOT` version only if the consumer explicitly opts in to the snapshot repo. 2. **Add the BOM** to `libs.versions.toml` and to the consuming module(s). 3. **Pick the artifact set** from the picker below (4-7). 4. **Add the snapshot repo** (only if using a `-SNAPSHOT` version) — `maven("https://central.sonatype.com/repository/maven-snapshots/")`. @@ -43,16 +43,21 @@ Add SKaiNET to a Gradle project that does NOT live inside the SKaiNET repo. Cent | You want to … | Add (in addition to `skainet-lang-core` + `skainet-backend-cpu`) | |---|---| | Define networks and tensors only (no model files yet) | nothing else | +| Build `dag { }` graph programs | `skainet-lang-dag` | | Load a GGUF model | `skainet-io-core` + `skainet-io-gguf` | | Load an ONNX model | `skainet-io-core` + `skainet-io-onnx` | | Load SafeTensors weights | `skainet-io-core` + `skainet-io-safetensors` | | Decode images for input pipelines | `skainet-io-image` | +| Pull raw data from `file://` / `https://` / `hf://` URIs (`rawDataset { }`, `dataPipeline()`) | `skainet-data-source` | | Use built-in datasets (MNIST, etc.) | `skainet-data-simple` | | Build preprocessing pipelines (`pipeline().rescale().normalize()`) | `skainet-data-transform` | -| Compile a DAG to StableHLO / C99 | `skainet-compile-core` + `skainet-compile-hlo` (StableHLO) or `skainet-compile-c` | +| Compile a DAG to StableHLO | `skainet-compile-core` + `skainet-compile-hlo` | +| Generate C99 code for embedded targets | `skainet-compile-core` + `skainet-compile-c` | +| Export weights as IREE Parameter Archives (`.irpa`) | `skainet-io-iree-params` | +| Hand-tuned native CPU kernels on the JVM (FFM) | `skainet-backend-native-cpu` (JVM-only — `jvmMain`, never `commonMain`) | +| Hand-tuned native CPU kernels on Android (JNI AAR) | `skainet-backend-jni-cpu` | | Run a higher-level pipeline framework | `skainet-pipeline` | | Use the YOLO model topology helpers | `skainet-model-yolo` | -| Test with the in-repo assertion library (rare for consumers) | `skainet-test-groundtruth` | The full BOM-managed artifact list lives in [`references/bom-coordinates.md`](references/bom-coordinates.md). @@ -63,7 +68,7 @@ The full BOM-managed artifact list lives in [`references/bom-coordinates.md`](re ```toml # gradle/libs.versions.toml (in your consumer project) [versions] -skainet = "0.20.0-SNAPSHOT" # or the latest stable +skainet = "0.57.0" # latest stable at the time of writing [libraries] skainet-bom = { module = "sk.ainet:skainet-bom", version.ref = "skainet" } @@ -88,7 +93,7 @@ dependencies { ```kotlin dependencies { - implementation(platform("sk.ainet:skainet-bom:0.20.0-SNAPSHOT")) + implementation(platform("sk.ainet:skainet-bom:0.57.0")) implementation("sk.ainet.core:skainet-lang-core") implementation("sk.ainet.core:skainet-backend-cpu") implementation("sk.ainet.core:skainet-io-gguf") @@ -124,8 +129,8 @@ kotlin { implementation(libs.skainet.lang.core) } jvmMain.dependencies { - // Backend is JVM-only by default; for KMP you can use it - // wherever the JVM (or Android) target compiles. + // skainet-backend-cpu is KMP (jvm, android, ios/macos, linux, + // js/wasm, androidNative) — add it to every target you run on. implementation(libs.skainet.backend.cpu) } androidMain.dependencies { @@ -148,23 +153,23 @@ kotlin { ```kotlin // WRONG — pinning per-artifact versions, no BOM -implementation("sk.ainet.core:skainet-lang-core:0.20.0-SNAPSHOT") -implementation("sk.ainet.core:skainet-io-gguf:0.19.0") // drift waiting to happen +implementation("sk.ainet.core:skainet-lang-core:0.57.0") +implementation("sk.ainet.core:skainet-io-gguf:0.53.0") // drift waiting to happen ``` ```kotlin // RIGHT — BOM manages all SKaiNET versions in one place -implementation(platform("sk.ainet:skainet-bom:0.20.0-SNAPSHOT")) +implementation(platform("sk.ainet:skainet-bom:0.57.0")) implementation("sk.ainet.core:skainet-lang-core") implementation("sk.ainet.core:skainet-io-gguf") ``` ```kotlin // WRONG — wrong group for the BOM -implementation(platform("sk.ainet.core:skainet-bom:0.20.0-SNAPSHOT")) +implementation(platform("sk.ainet.core:skainet-bom:0.57.0")) ``` ```kotlin // RIGHT — BOM lives at sk.ainet, libraries at sk.ainet.core -implementation(platform("sk.ainet:skainet-bom:0.20.0-SNAPSHOT")) +implementation(platform("sk.ainet:skainet-bom:0.57.0")) implementation("sk.ainet.core:skainet-lang-core") ``` @@ -186,12 +191,12 @@ dependencies { ``` ```kotlin -// WRONG — JVM target below 11 -kotlin { jvmToolchain(8) } +// WRONG — JVM toolchain below 21 (published JVM jars are Java 21 bytecode) +kotlin { jvmToolchain(17) } ``` ```kotlin -// RIGHT — JVM 11 minimum -kotlin { jvmToolchain(11) } // 17 or 21 also fine +// RIGHT — JVM 21 minimum for JVM targets +kotlin { jvmToolchain(21) } // newer also fine ``` ## References diff --git a/skainet-consumer-skills/skills/skainet-consumer-setup/evals/evals.json b/skainet-consumer-skills/skills/skainet-consumer-setup/evals/evals.json index 84db8fd..9700bd2 100644 --- a/skainet-consumer-skills/skills/skainet-consumer-setup/evals/evals.json +++ b/skainet-consumer-skills/skills/skainet-consumer-setup/evals/evals.json @@ -45,7 +45,7 @@ }, { "id": "snapshot-repo-when-snapshot-version", - "prompt": "I want the latest 0.20.0-SNAPSHOT. Configure my repositories.", + "prompt": "I want the latest SKaiNET SNAPSHOT build. Configure my repositories.", "must_contain": [ "central.sonatype.com/repository/maven-snapshots", "includeGroupByRegex" diff --git a/skainet-consumer-skills/skills/skainet-consumer-setup/references/artifact-picker.md b/skainet-consumer-skills/skills/skainet-consumer-setup/references/artifact-picker.md index 0c3463c..40bd689 100644 --- a/skainet-consumer-skills/skills/skainet-consumer-setup/references/artifact-picker.md +++ b/skainet-consumer-skills/skills/skainet-consumer-setup/references/artifact-picker.md @@ -8,6 +8,8 @@ Decision tree for "I need to do X, what should I add?". Always implies `sk.ainet You don't need anything beyond `skainet-lang-core` + `skainet-backend-cpu`. Construct the model in code, optionally load weights from a file (next sections), call `model.forward(x, ctx)`. +If the model is a `dag { }` graph program instead of a `sequential` stack, add `skainet-lang-dag` (the graph DSL lives there, not in `skainet-lang-core`). + ### "Load a Hugging Face model in SafeTensors format" ``` @@ -58,7 +60,13 @@ Likely also wants TurboQuant for KV-cache compression — that's already in `ska + skainet-data-transform # preprocessing ``` -The training context comes from the same `DirectCpuExecutionContext` with `phase = Phase.TRAIN`. +The training context comes from the same `DirectCpuExecutionContext` with `phase = Phase.TRAIN` (`DirectCpuExecutionContext.create(Phase.TRAIN)`). + +### "Load training data from files, URLs, or the Hugging Face Hub" + +``` ++ skainet-data-source # rawDataset { from("hf://owner/repo/data.csv") }, CSV/TSV/JSON/JSONL parsers, caching +``` ## Compilation / export scenarios @@ -73,10 +81,10 @@ The training context comes from the same `DirectCpuExecutionContext` with `phase ``` + skainet-compile-core -+ skainet-compile-c # check coordinates - currently part of contributor build, may need direct module dep ++ skainet-compile-c ``` -(C99 codegen is currently shipped as a SKaiNET-internal module; for now consumers building C99 may need to clone and `composite-build` until/if it's promoted.) +`sk.ainet.core:skainet-compile-c` is published and BOM-managed. Related export targets: `skainet-compile-json` (JSON graph export), `skainet-compile-minerva` (Minerva secure-MCU export), `skainet-io-iree-params` (`IrpaWriter` for IREE `.irpa` parameter archives). ## Multi-target scenarios @@ -88,9 +96,15 @@ Common code should depend ONLY on `skainet-lang-core` (and any data/transform mo - `iosArm64Main`, `iosSimulatorArm64Main` → `skainet-backend-cpu` (Native target ships) - `wasmJsMain`, `wasmWasiMain` → `skainet-backend-cpu` +### "I want the hand-tuned native CPU kernels" + +- JVM (desktop/server): add `skainet-backend-native-cpu` to `jvmMain.dependencies` — it reaches the C/NEON kernels through the Java FFM API and auto-registers via ServiceLoader. It is JVM-only on that path; adding it to `commonMain` breaks the Native/JS/Wasm targets. +- Android: add `skainet-backend-jni-cpu` (AAR) — ART has no FFM, so Android goes through JNI. +- Since 0.52.0 the kernel packs self-install on first use (`KernelDispatch.ensureInstalled()`); no startup install call is needed. + ### "I'm Java-only (Spring Boot, Android Java, …)" -See [`../skainet-java-consumer/SKILL.md`](../skainet-java-consumer/SKILL.md). The artifact set is a JVM-only subset; the consumer doesn't depend on `skainet-data-simple` (which has Android-specific cinterop) unless explicitly needed. +See [`../skainet-java-consumer/SKILL.md`](../skainet-java-consumer/SKILL.md). The artifact set is a JVM-only subset of the same coordinates. ## "Why isn't X resolving?" @@ -98,6 +112,8 @@ See [`../skainet-java-consumer/SKILL.md`](../skainet-java-consumer/SKILL.md). Th |---|---| | `Unresolved reference: DirectCpuExecutionContext` | Missing `skainet-backend-cpu`. | | `Unresolved reference: GGUFModelReader` | Missing `skainet-io-gguf`. | -| `Could not find sk.ainet:skainet-bom:0.20.0-SNAPSHOT` | Snapshot version without the snapshot repo configured. | +| `Unresolved reference: dag` | Missing `skainet-lang-dag` (the `dag { }` DSL is not in `skainet-lang-core`). | +| `Could not find sk.ainet:skainet-bom:-SNAPSHOT` | Snapshot version without the snapshot repo configured. | +| `UnsupportedClassVersionError` / "class file version 65.0" | Running on a JVM older than 21 — published JVM jars are Java 21 bytecode. | | Resolver picks an old version of `kotlinx-coroutines` | Add the BOM (`platform(...)`) — without it the consumer's other libraries can drag in mismatched transitives. | | Build passes but native targets fail at link | Backend not added to that target's source set; KMP consumer needs the backend in every target it actually runs on. | diff --git a/skainet-consumer-skills/skills/skainet-consumer-setup/references/bom-coordinates.md b/skainet-consumer-skills/skills/skainet-consumer-setup/references/bom-coordinates.md index d90ac8d..7a36ea6 100644 --- a/skainet-consumer-skills/skills/skainet-consumer-setup/references/bom-coordinates.md +++ b/skainet-consumer-skills/skills/skainet-consumer-setup/references/bom-coordinates.md @@ -1,6 +1,6 @@ # BOM coordinates -Authoritative source: `SKaiNET/skainet-bom/build.gradle.kts`. Every artifact below is constrained by the BOM, so consumers depend on the coordinate WITHOUT a version once the BOM is added. +Authoritative source: `SKaiNET/skainet-bom/build.gradle.kts` + the `sk.ainet.transformers.bom-coverage` convention plugin. The BOM's constraints are auto-populated: every sibling subproject that applies the publish plugin is added as an `api` constraint. Consumers depend on the coordinates WITHOUT a version once the BOM is added. ## BOM @@ -16,45 +16,58 @@ All under group `sk.ainet.core` (one exception is the BOM itself at `sk.ainet`). | Coordinate | Provides | |---|---| -| `sk.ainet.core:skainet-lang-core` | Tensor type system, `Shape`, dtypes (`FP32`, `FP16`, `Int8`, …), the `tensor { }` and `sequential { }` DSLs, `Module`, `ExecutionContext` interface, the Java facade objects (`SKaiNET`, `TensorJavaOps`). **Required for every consumer.** | +| `sk.ainet.core:skainet-lang-core` | Tensor type system, `Shape`, dtypes (`FP32`, `FP16`, `Int8`, …), the `tensor { }` and `sequential { }` DSLs, `Module`, `ExecutionContext` interface, the Java facade (`TensorJavaOps`, `SequentialModelBuilder`, `Optimizers`, `Losses`, `TrainingLoop`). **Required for every consumer.** | +| `sk.ainet.core:skainet-lang-dag` | The `dag { }` graph DSL (`sk.ainet.lang.dag.GraphDsl`), tracing to `GraphProgram`, schedule / dtype-policy DSLs. | +| `sk.ainet.core:skainet-lang-models` | `Model` / `ModelCard` / `ModelLoader` abstractions over architecture + weights + loading state. | +| `sk.ainet.core:skainet-lang-ksp-annotations` | Annotations consumed by the KSP processor; `skainet-lang-core` already exposes it via `api`, so consumers rarely add it directly. | +| `sk.ainet.core:skainet-lang-ksp-processor` | The KSP processor itself (build-time, `ksp(...)` configuration). | ### Backends | Coordinate | Provides | |---|---| -| `sk.ainet.core:skainet-backend-api` | `Backend` SPI; rarely depended on directly. | -| `sk.ainet.core:skainet-backend-cpu` | `DirectCpuExecutionContext.create()`, CPU op implementations, NEON / Java fallback auto-selection. **At least one backend is required.** | +| `sk.ainet.core:skainet-backend-api` | `Backend` SPI and kernel-provider contracts; rarely depended on directly. | +| `sk.ainet.core:skainet-backend-cpu` | `DirectCpuExecutionContext.create()`, CPU op implementations. **At least one backend is required.** | +| `sk.ainet.core:skainet-backend-native-cpu` | Hand-tuned C/NEON kernels reached via the Java FFM API on the JVM (plus published Kotlin/Native klibs). JVM side is JVM-only — add to `jvmMain.dependencies`, never `commonMain`. Auto-registers via ServiceLoader. | +| `sk.ainet.core:skainet-backend-jni-cpu` | Android JNI bridge (AAR) to the same native kernels — ART has no FFM, so Android goes through JNI. | ### IO (model loaders) | Coordinate | Provides | |---|---| -| `sk.ainet.core:skainet-io-core` | Common loader infrastructure (`RandomAccessSource`, `ModelReader`, `ParametersLoader`, progress callbacks). Required by every other `skainet-io-*` artifact. | +| `sk.ainet.core:skainet-io-core` | Common loader infrastructure (`RandomAccessSource`, `ModelReader`, `ParametersLoader`, progress callbacks) plus `TokenizerFactory` (`sk.ainet.io.tokenizer`). Required by every other `skainet-io-*` artifact. | | `sk.ainet.core:skainet-io-gguf` | `GGUFModelReader` for GGUF files (LLM weights, tokenizer metadata, KV-cache tensors). | -| `sk.ainet.core:skainet-io-safetensors` | `SafeTensorsParametersLoader` for HuggingFace SafeTensors. | -| `sk.ainet.core:skainet-io-onnx` | `OnnxLoader` for ONNX files via pbandk. | -| `sk.ainet.core:skainet-io-image` | Image decoders for input tensors (PNG/JPEG → `Tensor`). | +| `sk.ainet.core:skainet-io-safetensors` | `SafeTensorsParametersLoader` for HuggingFace SafeTensors (incl. sharded checkpoints). | +| `sk.ainet.core:skainet-io-onnx` | `OnnxLoader` for ONNX files via pbandk. | +| `sk.ainet.core:skainet-io-image` | Image decoders for input tensors (PNG/JPEG → tensors). | +| `sk.ainet.core:skainet-io-iree-params` | `IrpaWriter` — writes IREE Parameter Archive (`.irpa`) files for IREE deployment. | ### Data | Coordinate | Provides | |---|---| -| `sk.ainet.core:skainet-data-api` | `Dataset` / `DataBatch` abstractions. | +| `sk.ainet.core:skainet-data-api` | `Dataset` / `DataBatch` abstractions (shuffle, split, filter views). | | `sk.ainet.core:skainet-data-simple` | Built-in toy datasets (MNIST, CIFAR-style helpers). | +| `sk.ainet.core:skainet-data-source` | URI-backed data sources (`file://`, `https://`, `hf://owner/repo/path`), `rawDataset { }` + `dataPipeline()` DSLs, CSV/TSV/JSON/JSONL parsers, caching via `CachePolicy`. | | `sk.ainet.core:skainet-data-transform` | `pipeline<...>().rescale().normalize().clamp()...` preprocessing chain. | ### Compilation | Coordinate | Provides | |---|---| -| `sk.ainet.core:skainet-compile-core` | `ComputeGraph` lowering, the bridge between `dag { }` and runtime / codegen. | +| `sk.ainet.core:skainet-compile-core` | `ComputeGraph` lowering, the bridge between graph programs and runtime / codegen. | +| `sk.ainet.core:skainet-compile-dag` | DAG compilation: `ComputeGraph` construction from traces, autograd / graph inversion. | +| `sk.ainet.core:skainet-compile-opt` | Per-phase, per-target compile optimization passes (`TargetOptimizer`, `TargetOptimizers` registry, output pruning). | | `sk.ainet.core:skainet-compile-hlo` | `StableHloConverterFactory` — emits StableHLO MLIR for IREE / XLA. | +| `sk.ainet.core:skainet-compile-c` | C99 code generation for embedded targets. | +| `sk.ainet.core:skainet-compile-json` | JSON export of compiled graphs. | +| `sk.ainet.core:skainet-compile-minerva` | Minerva secure-MCU export. | ### Pipeline | Coordinate | Provides | |---|---| -| `sk.ainet.core:skainet-pipeline` | Higher-level pipeline framework over `Transform` chains. | +| `sk.ainet.core:skainet-pipeline` | Higher-level pipeline framework (`Pipeline`, `PipelineBuilder`, node/edge graph). | ### Models @@ -64,7 +77,8 @@ All under group `sk.ainet.core` (one exception is the BOM itself at `sk.ainet`). ## Not BOM-managed -The contributor-side test infrastructure (`skainet-test-groundtruth`, `skainet-test-java`) is internal-facing and not part of the consumer BOM. +- The in-repo test infrastructure (`skainet-test-groundtruth`, `skainet-test-java`) does not apply the publish plugin — it is not published and not in the BOM. +- `skainet-data-media` exists in the repo but is not published either (no publish plugin as of 0.57.0). ## Snapshot repo diff --git a/skainet-consumer-skills/skills/skainet-data-dsl/SKILL.md b/skainet-consumer-skills/skills/skainet-data-dsl/SKILL.md index 276dd25..29842ef 100644 --- a/skainet-consumer-skills/skills/skainet-data-dsl/SKILL.md +++ b/skainet-consumer-skills/skills/skainet-data-dsl/SKILL.md @@ -13,7 +13,7 @@ Building blocks for tensor data: creation, initialisation, slicing, and transfor - Constructing a tensor literal with a known shape and fill (zeros, ones, fill, random, fromArray). - Slicing or viewing an existing tensor. - Building a preprocessing pipeline (rescale → normalize → unsqueeze → reshape). -- Choosing a dtype tag (`FP32`, `FP16`, `Int8`, `Int32`, `Int4`, `Ternary`). +- Choosing a dtype tag (`FP32`, `FP16`, `BF16`, `Int8`, `Int32`, `Int4`, `Ternary`, …). ## When NOT to use @@ -35,7 +35,7 @@ val t = tensor(ctx, FP32::class) { } } } -// from: SKaiNET/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/dsl/TensorDSL.kt:17-25 +// from: SKaiNET/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/dsl/TensorDSL.kt:18-25 ``` ```kotlin @@ -47,10 +47,10 @@ val t = data(ctx) { } } } -// from: SKaiNET/skainet-lang/skainet-lang-core/src/commonTest/kotlin/sk/ainet/readme/ReadmeSnippetsTest.kt:18-32 +// from: SKaiNET/skainet-lang/skainet-lang-core/src/commonTest/kotlin/sk/ainet/readme/ReadmeSnippetsTest.kt:20-29 ``` -Use form (b) when you're already inside a phase-aware execution context (training vs eval) and want phase-tagged tensors. Use form (a) for plain inference / tests / examples. +Use form (b) when you're already inside a phase-aware execution context (training vs eval) and want phase-tagged tensors. Use form (a) for plain inference / tests / examples. `data(...)` defaults its context to `DefaultDataExecutionContext()` when you omit the argument, and has an untyped overload plus `createDataMap(ctx) { tensor(name) { ... } }` that returns every *named* tensor built in the block as a `Map>` (`ContextDsl.kt:19-53`). Inside a `data { }` block you also get `scalar(v)`, `vector(length) { ... }` and `matrix(rows, cols) { ... }` conveniences (`DataContextDsl.kt`). ### Initialisation strategies inside `shape(...) { ... }` @@ -86,14 +86,14 @@ The number of `segment { }` blocks MUST equal the rank of the tensor — `valida ```kotlin val preprocess = pipeline>() .rescale(ctx, scale = 255f) - .normalize(ctx, mean = imagenetMean, std = imagenetStd, channelAxis = -1) - .unsqueeze(0) // add batch dim at position 0 + .normalize(ctx, mean = ImageNet.mean, std = ImageNet.std, channelAxis = -1) + .unsqueeze(ctx, 0) // add batch dim at position 0 — every step takes ctx val batch = preprocess(rawImageTensor) -// from: SKaiNET/skainet-data/skainet-data-transform/src/commonMain/kotlin/sk/ainet/data/transform/TensorTransformDsl.kt:18-50 +// from: SKaiNET/skainet-data/skainet-data-transform/src/commonMain/kotlin/sk/ainet/data/transform/TensorTransformDsl.kt:34-134 ``` -Available transform extensions: `rescale`, `normalize`, `scaleAndShift`, `clamp`, `reshape` (more to follow — file is the source of truth). +Available transform extensions: `rescale`, `normalize`, `scaleAndShift`, `clamp`, `reshape`, `flatten`, `unsqueeze`, `squeeze`. Built-in normalization presets: `ImageNet`, `CIFAR10Norm`, `MNISTNorm` (each exposes `mean` / `std` FloatArrays). A `transforms(ctx) { ... }` scope drops the repeated `ctx` argument — inside it every extension is called without the context (`TensorTransformDsl.kt:197-287`). ### Dtype tags @@ -101,12 +101,17 @@ Available transform extensions: `rescale`, `normalize`, `scaleAndShift`, `clamp` |---|---|---| | `FP32` | `Float` | default; training, inference, ground truth | | `FP16` | `Float` (promoted) | half precision inference | +| `BF16` | `Float` (promoted) | bfloat16 weights/inference (NPU-friendly) | | `Int32` | `Int` | indices, labels | | `Int8` | `Byte` | quantised inference | | `Int4` | `Byte` (promoted) | aggressive quantisation | -| `Ternary` | `Byte` | -1/0/+1 weights | +| `Ternary` | `Byte` | -1/0/+1 weights (BitNet) | -`tensor(...)` — the value-type parameter follows the table above. `tensor(...)` will not type-check. +`DType` is a sealed interface with exactly fourteen `object` members — the table above plus `FP64` (`Double`), `Int16` (`Short`), `Int64` (`Long`) and `UInt8`/`UInt16`/`UInt32`/`UInt64` (`DType.entries`, from `sk.ainet.lang.types.DType`). `tensor(...)` — the value-type parameter follows the table above. `tensor(...)` will not type-check. + +### Memory API — no opt-in needed anymore + +The low-level memory/storage API under `sk.ainet.lang.memory` (`Storage`, `Scope`, `Format`, `Layout`, `TensorView`, …) no longer requires `@OptIn(ExperimentalMemoryApi::class)` — the annotation was deleted in 0.54.0. If existing code carries that opt-in, remove it; the symbol no longer exists. ## Workflow diff --git a/skainet-consumer-skills/skills/skainet-data-dsl/references/tensor-builders.md b/skainet-consumer-skills/skills/skainet-data-dsl/references/tensor-builders.md index fe45ba1..603dc86 100644 --- a/skainet-consumer-skills/skills/skainet-data-dsl/references/tensor-builders.md +++ b/skainet-consumer-skills/skills/skainet-data-dsl/references/tensor-builders.md @@ -10,10 +10,26 @@ public fun tensor( dtype: KClass, content: TensorDefineDsl.() -> Tensor ): Tensor -// from: TensorDSL.kt:17-25 +// from: TensorDSL.kt:18-25 ``` -`data(ctx) { tensor { shape(...) { ... } } }` is the phase-aware variant defined in `sk.ainet.context` (separate import: `sk.ainet.context.data`). +`data(ctx) { tensor { shape(...) { ... } } }` is the phase-aware variant defined in `sk.ainet.context` (separate import: `sk.ainet.context.data`). Its `executionContext` parameter defaults to `DefaultDataExecutionContext()`. Related entry points in the same file (`ContextDsl.kt:19-53`): + +```kotlin +public fun data(executionContext: ExecutionContext = DefaultDataExecutionContext(), + content: DataContextDsl.(executionContext: ExecutionContext) -> Unit) // untyped block form + +public inline fun data( + executionContext: ExecutionContext = DefaultDataExecutionContext(), + noinline content: TypedDataContextDsl.(executionContext: ExecutionContext) -> Tensor +): Tensor + +public fun createDataMap(executionContext: ExecutionContext = DefaultDataExecutionContext(), + content: DataContextDsl.(executionContext: ExecutionContext) -> Unit +): Map> // every tensor in the block must use the named tensor(name) { } overload +``` + +Inside a `data { }` block, `DataContextDsl` (in `sk.ainet.lang.tensor.dsl.DataContextDsl`) additionally offers `scalar(value)`, `vector(length) { ... }`, `matrix(rows, columns) { ... }` and a named `tensor(name) { ... }` overload — all with reified-dtype convenience wrappers. ## `TensorDefineDsl` @@ -78,7 +94,7 @@ TensorBuilder(FP32::class) .shape(2, 3) .ones() // returns TensorInitializer .build(ctx) // materialises to Tensor -// from: TensorDSL.kt:145-203 +// from: TensorDSL.kt:145-267 ``` Staged form is a substitute for the DSL when you can't run the lambda eagerly (KSP code generation). For everyday code, prefer the DSL form. @@ -113,13 +129,18 @@ public class TensorSliceBuilder { ## Choosing a dtype + value-type -The pair `` must be consistent. From the canonical types in `sk.ainet.lang.types`: +The pair `` must be consistent. `DType` is a sealed interface with fourteen `object` members (`sk.ainet.lang.types.DType.entries`); the value-type mapping follows `DType.kotlinClass()` in `sk.ainet.lang.types.DTypeExtensions.kt:5-20`: | `T` | `V` | |---|---| | `FP32` | `Float` | +| `FP64` | `Double` | | `FP16` | `Float` | -| `Int32` | `Int` | +| `BF16` | `Float` | | `Int8` | `Byte` | +| `Int16` | `Short` | +| `Int32` | `Int` | +| `Int64` | `Long` | | `Int4` | `Byte` | -| `Ternary` | `Byte` | +| `UInt8` / `UInt16` / `UInt32` / `UInt64` | `UByte` / `UShort` / `UInt` / `ULong` | +| `Ternary` | `Byte` (as used by `Tensor` call sites) | diff --git a/skainet-consumer-skills/skills/skainet-data-dsl/references/transform-ops.md b/skainet-consumer-skills/skills/skainet-data-dsl/references/transform-ops.md index 5740446..23b5b3a 100644 --- a/skainet-consumer-skills/skills/skainet-data-dsl/references/transform-ops.md +++ b/skainet-consumer-skills/skills/skainet-data-dsl/references/transform-ops.md @@ -10,12 +10,14 @@ A pipeline is a chain of `Transform` values composed by `then`. The DSL val preprocess = pipeline>() .rescale(ctx, scale = 255f) .normalize(ctx, mean = mean, std = std, channelAxis = -1) - .unsqueeze(0) + .unsqueeze(ctx, 0) val batch = preprocess(raw) -// from: SKaiNET/skainet-data/skainet-data-transform/src/commonMain/kotlin/sk/ainet/data/transform/TensorTransformDsl.kt:18-23 +// pipeline(): SKaiNET/skainet-data/skainet-data-transform/src/commonMain/kotlin/sk/ainet/data/transform/Transform.kt:168 ``` +`pipeline()` returns an `Identity` transform to chain from; `then` is the infix composer on `Transform` (`Transform.kt:92`). + ## Available extensions ```kotlin @@ -32,7 +34,7 @@ public fun Transform>.normalize( std: FloatArray, channelAxis: Int = -1 ): Transform> -// Channel-wise (`output - mean) / std` along `channelAxis`. Default channelAxis = -1 (last). +// Channel-wise `(output - mean) / std` along `channelAxis`. Default channelAxis = -1 (last). // from: TensorTransformDsl.kt:34-39 public fun Transform>.scaleAndShift( @@ -50,13 +52,67 @@ public fun Transform>.clamp( ): Transform> // Restrict values to [min, max]. // from: TensorTransformDsl.kt:72-76 + +public fun Transform>.reshape( + ctx: ExecutionContext, + shape: Shape +): Transform> +public fun Transform>.reshape( + ctx: ExecutionContext, + vararg dims: Int +): Transform> +// from: TensorTransformDsl.kt:84-102 + +public fun Transform>.flatten( + ctx: ExecutionContext, + startDim: Int = 0, + endDim: Int = -1 +): Transform> +// from: TensorTransformDsl.kt:107-112 + +public fun Transform>.unsqueeze( + ctx: ExecutionContext, + dim: Int +): Transform> +// Adds a size-1 dimension at `dim`. NOTE: takes the ExecutionContext like every other extension. +// from: TensorTransformDsl.kt:119-123 + +public fun Transform>.squeeze( + ctx: ExecutionContext, + dim: Int? = null +): Transform> +// Removes the size-1 dimension at `dim`, or all size-1 dimensions when null. +// from: TensorTransformDsl.kt:130-134 +``` + +That is the complete extension list as of 0.57.0. + +## Normalization presets + +`TensorTransformDsl.kt:150-178` ships three preset objects, each with `mean` and `std` FloatArrays (RGB order): + +```kotlin +pipeline.normalize(ctx, ImageNet.mean, ImageNet.std) // 0.485/0.456/0.406, 0.229/0.224/0.225 +pipeline.normalize(ctx, CIFAR10Norm.mean, CIFAR10Norm.std) +pipeline.normalize(ctx, MNISTNorm.mean, MNISTNorm.std) // grayscale, single channel ``` -`reshape`, `unsqueeze` and friends follow the same pattern — see the source file for the up-to-date list. +## Context-scoped form — `transforms(ctx) { ... }` + +`TransformScope` re-declares every extension above without the `ctx` parameter; `transforms(ctx) { ... }` opens the scope (`TensorTransformDsl.kt:197-287`): + +```kotlin +val preprocessing = transforms(ctx) { + pipeline>() + .rescale(255f) + .normalize(ImageNet.mean, ImageNet.std) + .unsqueeze(0) +} +``` ## Where the actual transform classes live -The functions above produce instances of `Normalize`, `Rescale`, `ScaleAndShift`, `Clamp`, etc. — concrete classes in the same package. The DSL extensions are the call site; the classes are the implementation. +The functions above produce instances of `Normalize`, `Rescale`, `ScaleAndShift`, `Clamp`, `Reshape`, `Flatten`, `Unsqueeze`, `Squeeze` — concrete classes in `TensorTransforms.kt` in the same package. The DSL extensions are the call site; the classes are the implementation. ## When to NOT use a pipeline diff --git a/skainet-consumer-skills/skills/skainet-inference/SKILL.md b/skainet-consumer-skills/skills/skainet-inference/SKILL.md index 5c7c5cf..271618a 100644 --- a/skainet-consumer-skills/skills/skainet-inference/SKILL.md +++ b/skainet-consumer-skills/skills/skainet-inference/SKILL.md @@ -6,16 +6,16 @@ version: 0.1.0 # skainet-inference -Running a SKaiNET model: `ExecutionContext` lifecycle, the forward pass, eval/train phase toggling, and TurboQuant for KV-cache or weight compression. The four steps that connect "I have a model" to "I have predictions." +Running a SKaiNET model: `ExecutionContext` lifecycle, the forward pass, eval/train phase toggling, the `Schedule` API for intra-op threading, and TurboQuant for KV-cache compression. The four steps that connect "I have a model" to "I have predictions." ## When to use - Running a forward pass. - Picking between `DirectCpuExecutionContext.create()` and `DefaultNeuralNetworkExecutionContext()`. - Switching a model between eval and train phases. -- Configuring TurboQuant on tensors (KV cache, weights). +- Configuring TurboQuant for the KV cache. - Batching multiple inputs through one `forward` call. -- Threading concerns: which thread should host the forward pass? +- Threading concerns: which thread hosts the forward pass, and how many cores one op uses (`ctx.schedule`, `ctx.withSchedule`, `Schedule.Sequential`). ## When NOT to use @@ -31,8 +31,9 @@ Running a SKaiNET model: `ExecutionContext` lifecycle, the forward pass, eval/tr 2. **`Phase.EVAL` is the default for inference.** `DirectCpuExecutionContext.create()` returns `EVAL`; do not pass `Phase.TRAIN` "for safety" — dropout / batchnorm change behaviour. 3. **`forward(x, ctx)` is synchronous and CPU-bound.** Run it on a worker thread / dispatcher (`Dispatchers.Default` for compute, `Dispatchers.IO` for IO-mixed). Never on the Android main thread. 4. **Inputs MUST have the shape the model declares.** A model with `input(28 * 28)` accepts `[batch, 784]`; with `input(intArrayOf(1, 28, 28))` accepts `[batch, 1, 28, 28]`. Reshape at the source, not by silently letting the framework error in the middle of a layer. -5. **TurboQuant is opt-in.** Apply it only to specific tensors (KV cache, weights) — don't blanket-encode everything. The quantisation rounds and the choice of bits (2/3/4/8) is a quality/size trade-off the consumer must make consciously. -6. **Don't read intermediate activations through reflection.** Use `ForwardHooks` (passed via `_hooks` to the `ExecutionContext` constructor) when you need to observe layer outputs. +5. **Intra-op parallelism is the context's `Schedule`, not yours.** Since 0.54.0 every `ExecutionContext` carries a `sk.ainet.context.schedule.Schedule` that says how many tasks an op's independent work spreads over. The JVM default is `CoroutineSchedule.hardware()` (one task per core); every other target defaults to `Schedule.Sequential`. A schedule never changes results — scheduled runs are bit-identical to sequential ones. Want single-threaded kernels (e.g. you already saturate cores with concurrent requests)? Use `ctx.withSchedule(Schedule.Sequential) { ... }` or construct `DirectCpuExecutionContext(schedule = Schedule.Sequential)`. +6. **TurboQuant is opt-in.** Apply it to the KV cache via `KvCacheStore.turboQuant(...)` / `TensorEncoding.TurboQuantPolar` — don't blanket-encode everything. The quantisation rounds and the choice of bits (2/3/4/8) is a quality/size trade-off the consumer must make consciously. +7. **Don't read intermediate activations through reflection.** Use `ForwardHooks` (passed via `_hooks` to the `ExecutionContext` constructor) when you need to observe module inputs/outputs. ## Workflow @@ -67,13 +68,13 @@ val x = tensor(ctx, FP32::class) { val y = model.forward(x, ctx) // y.shape == Shape(1, 10) -// from: SKaiNET/skainet-lang/skainet-lang-core/src/commonTest/kotlin/sk/ainet/readme/ReadmeSnippetsTest.kt:36-56 +// adapted from: SKaiNET/skainet-lang/skainet-lang-core/src/commonTest/kotlin/sk/ainet/readme/ReadmeSnippetsTest.kt ``` **Real-world consumer pattern (image-in / tensor-out, with progress):** ```kotlin -val ctx: ExecutionContext = modelInstance.executionContext // DirectCpuExecutionContext.create() +val ctx: ExecutionContext = modelInstance.executionContext // a DirectCpuExecutionContext val module = modelInstance.model.create(ctx) val inputTensor = imageLoader.imageToTensor(image, ctx) @@ -126,14 +127,42 @@ suspend fun classify(image: Tensor, model: Module, ctx `Dispatchers.Default` is the right pool for CPU-bound work; `Dispatchers.IO` is sized for IO-blocking calls. Do NOT call `forward` from `Dispatchers.Main` (Android UI / UI-thread executors). +**Controlling intra-op threading — the `Schedule` API (0.54.0+):** + +```kotlin +import sk.ainet.context.DirectCpuExecutionContext +import sk.ainet.context.schedule.Schedule +import sk.ainet.context.withSchedule +import sk.ainet.exec.schedule.CoroutineSchedule // JVM only (skainet-backend-cpu jvmMain) + +val ctx = DirectCpuExecutionContext() // JVM default: CoroutineSchedule.hardware() +println(ctx.schedule.name) // e.g. "coroutines(12)"; Android/native/JS: "sequential" + +// Force single-threaded kernels for one block of work: +val out = ctx.withSchedule(Schedule.Sequential) { seq -> + model.forward(x, seq) +} + +// Or pin a worker count (JVM): +val two = ctx.withSchedule(CoroutineSchedule(parallelism = 2)) { par -> model.forward(x, par) } +// Results are bit-identical across schedules — only the thread mapping changes. +// from: SKaiNET/docs/modules/ROOT/examples/kotlin/sk/ainet/docs/samples/ScheduleDemo.kt +``` + **TurboQuant for a KV cache:** ```kotlin -import sk.ainet.lang.tensor.encoding.TensorEncoding +import sk.ainet.lang.tensor.storage.KvCacheStore +import sk.ainet.lang.tensor.storage.TensorEncoding + +// One-line preset factory ("safe-lowbit", "balanced", "experimental-max"): +val cache = KvCacheStore.turboQuant( + preset = "balanced", numLayers = 32, numHeads = 32, headDim = 128, maxSeqLen = 4096 +) +// Or per-tensor encodings for a custom KvCacheConfig: val keyEncoding = TensorEncoding.TurboQuantPolar(bitsPerElement = 4, blockSize = 128) val valueEncoding = TensorEncoding.TurboQuantPolar(bitsPerElement = 4, blockSize = 128) -// Plug encodings into your KV-cache implementation. ``` ```kotlin @@ -141,8 +170,8 @@ import sk.ainet.lang.tensor.ops.turboquant.TurboQuantCodec import sk.ainet.lang.tensor.ops.turboquant.TurboQuantConfig val config = TurboQuantConfig.polarPlusQjl(bits = 4, residualBits = 1, seed = 42) -val encoded = TurboQuantCodec.encode(rawFloats, config) -val decoded = TurboQuantCodec.decode(encoded) +val encoded = TurboQuantCodec.encode(rawFloats, config) // TurboQuantBlock +val decoded = TurboQuantCodec.decode(encoded) // FloatArray // from: SKaiNET/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/ops/turboquant/TurboQuantCodec.kt ``` @@ -211,6 +240,6 @@ val outs = model.forward(batched, ctx) // single forward call ## References -- [`references/execution-context.md`](references/execution-context.md) — `ExecutionContext` factories, `Phase`, hooks, stats, and the lifetime contract. +- [`references/execution-context.md`](references/execution-context.md) — `ExecutionContext` factories, `Phase`, the `Schedule` API, hooks, stats, and the lifetime contract. - [`references/forward-pass.md`](references/forward-pass.md) — `Module.forward(x, ctx)`, batching, output shape rules, when to convert to primitive arrays. -- [`references/turboquant.md`](references/turboquant.md) — `TurboQuantPolar`, `TurboQuantPolarQjl`, `TurboQuantConfig`, `TurboQuantCodec` with the bit/quality trade-off. +- [`references/turboquant.md`](references/turboquant.md) — `TurboQuantPolar`, `TurboQuantPolarQjl`, `TurboQuantConfig`, `TurboQuantCodec`, KV-cache stores and presets, with the bit/quality trade-off. diff --git a/skainet-consumer-skills/skills/skainet-inference/references/execution-context.md b/skainet-consumer-skills/skills/skainet-inference/references/execution-context.md index bd6d4df..169b987 100644 --- a/skainet-consumer-skills/skills/skainet-inference/references/execution-context.md +++ b/skainet-consumer-skills/skills/skainet-inference/references/execution-context.md @@ -1,15 +1,16 @@ # ExecutionContext -Authoritative source: `SKaiNET/skainet-backends/skainet-backend-cpu/src/commonMain/kotlin/sk/ainet/context/DirectCpuExecutionContext.kt` and `SKaiNET/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/context/Phase.kt`. +Authoritative source: `SKaiNET/skainet-backends/skainet-backend-cpu/src/commonMain/kotlin/sk/ainet/context/DirectCpuExecutionContext.kt`, `SKaiNET/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/context/ExecutionContext.kt`, `.../context/Phase.kt`, and `.../context/schedule/Schedule.kt`. ## What it is `ExecutionContext` is the runtime backend SKaiNET operations dispatch through. It owns: - The tensor data factory (`tensorDataFactory`) — produces backing storage for new tensors. -- The execution phase (`phase: Phase.EVAL` or `Phase.TRAIN`). -- Optional forward hooks (`_hooks: ForwardHooks?`) — observe layer outputs. -- Execution stats (`executionStats: ExecutionStats`) — profiling counters. +- The execution phase (`phase: Phase.EVAL` or `Phase.TRAIN`), with the convenience flag `inTraining`. +- The schedule (`schedule: Schedule`) — how an op's independent work maps onto cores (0.54.0+). +- Optional forward hooks (`hooks: ForwardHooks?`) — observe module forward calls. +- Execution stats (`executionStats: ExecutionStats`) and an observer registry (`observers`). Every operation (tensor construction, matmul, conv2d, …) takes the context (or reads it from a tensor's `ops` field). Different contexts → different backends. @@ -18,12 +19,13 @@ Every operation (tensor construction, matmul, conv2d, …) takes the context (or ### CPU (the default) ```kotlin -public class DirectCpuExecutionContext( +public class DirectCpuExecutionContext @JvmOverloads constructor( override val executionStats: ExecutionStats = ExecutionStats(), override val phase: Phase = Phase.EVAL, private val _hooks: ForwardHooks? = null, - override val tensorDataFactory: TensorDataFactory = DenseTensorDataFactory() + override val tensorDataFactory: TensorDataFactory = DenseTensorDataFactory(), ) : ExecutionContext { + // Secondary constructor adds `schedule: Schedule` as a fifth parameter. public companion object { @JvmStatic @JvmOverloads @@ -34,9 +36,9 @@ public class DirectCpuExecutionContext( `DirectCpuExecutionContext.create()` is the standard consumer entry point. `@JvmStatic` + `@JvmOverloads` make it idiomatic from Java too: `SKaiNET.context()` ultimately resolves here. -The CPU backend auto-selects NEON / SSE / scalar paths based on platform — consumers don't pick. +The CPU backend auto-selects its kernel tier per platform (Panama/FFM vector kernels on the JVM, JNI NEON on Android when `skainet-backend-jni-cpu` is on the classpath, scalar reference as the floor) — consumers don't pick. Since 0.52.0 kernel providers self-install on first use (`KernelDispatch.ensureInstalled()` via `ServiceLoader` on JVM/Android); no bootstrap call needed. -### Phase-aware (NN-flavoured) +### Shape-only (NN-flavoured) ```kotlin public class DefaultNeuralNetworkExecutionContext( @@ -44,9 +46,7 @@ public class DefaultNeuralNetworkExecutionContext( ) : NeuralNetworkExecutionContext ``` -A simpler context used in some examples and tests. It defers to `DirectCpuExecutionContext` underneath for CPU ops but exposes the `NeuralNetworkExecutionContext` interface that the `sequential { }` builder uses. - -For most consumer code, prefer `DirectCpuExecutionContext.create()` directly. +Lives in `sk.ainet.lang.nn`. Its `ops` is `VoidTensorOps` — shape propagation without real arithmetic — so it is for building/describing models and shape-checking, not for producing real numbers. For actual inference, use `DirectCpuExecutionContext.create()`. ## `Phase` @@ -56,7 +56,8 @@ public enum class Phase { EVAL } -public val ExecutionContext.inTraining: Boolean get() = phase == Phase.TRAIN +// On ExecutionContext itself: +public val inTraining: Boolean get() = phase == Phase.TRAIN ``` Layer behaviour that depends on phase: @@ -67,29 +68,51 @@ Layer behaviour that depends on phase: Default to `EVAL` everywhere except inside a training loop. +## `Schedule` — intra-op threading (0.54.0+) + +`sk.ainet.context.schedule.Schedule` is the Halide-style split between *what* an op computes and *how many tasks* compute it. A schedule never changes a result: scheduled runs are bit-identical to sequential ones. + +```kotlin +public interface Schedule { + public val parallelism: Int // 1 means sequential + public val name: String // "sequential", "coroutines(8)", … + public fun forRange(n: Int, grain: Int = 1, body: (start: Int, end: Int) -> Unit) + public object Sequential : Schedule // the default everywhere: one task, caller's thread +} +``` + +- `ctx.schedule` — the context's current schedule. +- `ctx.withSchedule(schedule)` — a sibling context whose ops run under that schedule. There is also a block form: `ctx.withSchedule(s) { scheduled -> model.forward(x, scheduled) }` (extension in `sk.ainet.context`). Create inputs through the scheduled context so tensor-bound ops carry the schedule. +- Platform defaults: **JVM** → `CoroutineSchedule.hardware()` (one task per core on `Dispatchers.Default`); **Android, Kotlin/Native, JS/Wasm** → `Schedule.Sequential`. +- `sk.ainet.exec.schedule.CoroutineSchedule` (JVM-only, in `skainet-backend-cpu`): `CoroutineSchedule(parallelism = N)`, `CoroutineSchedule.hardware()`, and `CoroutineSchedule.dedicated(N)` for an owned daemon pool (`AutoCloseable`). +- A context that cannot honour a requested schedule emits `TraceEvent.ScheduleDowngraded` — unhonoured requests are visible in the trace, never silent. + +When to touch it: force `Schedule.Sequential` when your server already runs many concurrent forward calls (avoid oversubscription), or when you need deterministic single-threaded profiling. Leave the default alone otherwise. + ## Lifetime - One context per inference session is typical. -- Contexts are heavyweight (factory + stats + caches) — don't construct one per `forward(...)` call. -- Contexts are NOT explicitly disposed; let GC reclaim them when the session ends. -- A `DirectCpuExecutionContext` is thread-safe to share across multiple coroutines doing forward passes (CPU ops are stateless apart from stats). +- Contexts are heavyweight (factory + cached ops + stats) — don't construct one per `forward(...)` call. The `ops` instance is built lazily once and cached. +- Contexts are NOT explicitly disposed; let GC reclaim them when the session ends. (A `CoroutineSchedule.dedicated(...)` pool is the exception — close it.) +- Sharing one `DirectCpuExecutionContext` across coroutines doing forward passes is the normal pattern; the kernel registries are safe for concurrent reads (0.54.0). ## Hooks ```kotlin public interface ForwardHooks { - public fun onLayerOutput(layerId: String, output: Tensor<*, *>) + public fun onForwardBegin(module: ModuleNode, input: Any) + public fun onForwardEnd(module: ModuleNode, input: Any, output: Any) } ``` -Pass an implementation to the constructor (`_hooks` parameter) to observe layer outputs by id. Useful for: +Lives in `sk.ainet.lang.nn.hooks`. Pass an implementation to the constructor (`_hooks` parameter); the context exposes it as `hooks`. `Module.forward` wraps every module call in `onForwardBegin`/`onForwardEnd`. Useful for: - Logging activations for debugging. - Capturing intermediate tensors for visualisation. -- Building telemetry pipelines. +- Building telemetry pipelines (a `TapeRecorder` in the same package records a tape this way). Setting hooks doesn't change `forward` semantics — it just emits observations. ## ExecutionStats -`ExecutionStats` accumulates counters (op count, byte counts, …) across a context's lifetime. Read fields directly to surface them in benchmarks. Default: `ExecutionStats()` collects nothing (safe to leave alone). +`ExecutionStats` accumulates counters across a context's lifetime. Read fields directly to surface them in benchmarks. Default: `ExecutionStats()` collects nothing (safe to leave alone). For richer tracing there is also `ctx.traceSink` (default `NoopTraceSink`) and the `observers` registry (`registerObserver` / `unregisterObserver`). diff --git a/skainet-consumer-skills/skills/skainet-inference/references/forward-pass.md b/skainet-consumer-skills/skills/skainet-inference/references/forward-pass.md index 0e79a5e..dc64dd0 100644 --- a/skainet-consumer-skills/skills/skainet-inference/references/forward-pass.md +++ b/skainet-consumer-skills/skills/skainet-inference/references/forward-pass.md @@ -68,27 +68,28 @@ Forward is synchronous and CPU-bound: | Android — UI handler | `withContext(Dispatchers.Default) { ... }` from a `lifecycleScope.launch { }`. | | KMP Native (iOS) | a worker thread / dispatcher; never the main runloop. | -The CPU backend itself doesn't multi-thread within one `forward` call (the platform's NEON / SSE intrinsics process one batch element at a time). For higher throughput, batch larger or run multiple `forward` calls concurrently. +Whether one `forward` call multi-threads internally is the context's `Schedule` (0.54.0+): on the JVM the default is `CoroutineSchedule.hardware()`, so scheduled ops (scaled-dot-product attention first) spread their independent `(batch, head)` units across cores; on Android, Kotlin/Native and JS/Wasm the default is `Schedule.Sequential`. Results are bit-identical either way. For many concurrent `forward` calls on a server, consider `ctx.withSchedule(Schedule.Sequential) { ... }` to avoid oversubscribing the pool — see `execution-context.md`. ## Concurrency safety -`DirectCpuExecutionContext` is safe to share across coroutines doing `forward` calls — the per-tensor `ops` field is stateless. The execution stats accumulator is the only shared state, and it's tolerant of concurrent updates. +Sharing one `DirectCpuExecutionContext` across coroutines doing `forward` calls is the normal pattern; since 0.54.0 the kernel registries (`KernelDispatch`, `KernelRegistry`) are safe for concurrent reads. -A `Module` is also safe to share — its parameters are immutable in `EVAL`. In `TRAIN`, only one coroutine should run forward+backward at a time per `Module`. +A `Module` is also safe to share for inference — its parameters are not mutated in `EVAL`. In `TRAIN`, only one coroutine should run forward+backward at a time per `Module`. ## Observing intermediate layers -Don't reach for reflection; use `ForwardHooks`: +Don't reach for reflection; use `ForwardHooks` (`sk.ainet.lang.nn.hooks`): ```kotlin val ctx = DirectCpuExecutionContext( phase = Phase.EVAL, _hooks = object : ForwardHooks { - override fun onLayerOutput(layerId: String, output: Tensor<*, *>) { - println("$layerId: ${output.shape}") + override fun onForwardBegin(module: ModuleNode, input: Any) {} + override fun onForwardEnd(module: ModuleNode, input: Any, output: Any) { + println("${module.name}: ${(output as? Tensor<*, *>)?.shape}") } } ) ``` -Layer ids come from the optional `id = "..."` argument on each layer call (`dense(128, id = "fc1")`); auto-generated if you didn't set them. +`Module.forward` invokes the hooks around every module in the tree; `module.name` / `module.path` identify the layer. Ids come from the optional `id = "..."` argument on each layer call (`dense(128, id = "fc1")`); auto-generated if you didn't set them. diff --git a/skainet-consumer-skills/skills/skainet-inference/references/turboquant.md b/skainet-consumer-skills/skills/skainet-inference/references/turboquant.md index 7143d6e..222301b 100644 --- a/skainet-consumer-skills/skills/skainet-inference/references/turboquant.md +++ b/skainet-consumer-skills/skills/skainet-inference/references/turboquant.md @@ -1,43 +1,75 @@ # TurboQuant -Authoritative source: `SKaiNET/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/ops/turboquant/TurboQuantCodec.kt` and surrounding files in the same package. +Authoritative sources: `SKaiNET/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/ops/turboquant/TurboQuantCodec.kt` (plus `TurboQuantPresets.kt`, `TurboQuantUsage.kt` in the same package) and `.../sk/ainet/lang/tensor/storage/TensorEncoding.kt`, `.../storage/KvCacheStore.kt`. ## What it is -A polar-coordinates quantisation scheme tuned for KV cache and weight compression. Two flavours: +Runtime KV-cache compression using rotation-based quantisation (after the TurboQuant paper): compress K/V projections on write, decompress on read. No model retraining or weight re-quantisation — any model benefits immediately. Two flavours: -- **Polar (default)** — uniform polar quantisation. Configurable bit width (2, 3, 4, 8) per element; configurable block size (typically 128). -- **Polar + QJL** — polar with a residual correction (`residualBits`, typically 1) for higher quality at marginal cost. +- **Polar (default)** — rotation + scalar quantisation + bit-packing. Bits per element 2, 3, 4 or 8; block size a power of 2 (typically 64 or 128). +- **Polar + QJL** — adds a QJL residual stage (`residualBits`, 1–4, typically 1). Closest to the paper; better inner-product accuracy at extra storage cost. ## Encodings +In `sk.ainet.lang.tensor.storage.TensorEncoding` (sealed hierarchy alongside `Q4_K`, `Q8_0`, …): + ```kotlin public data class TurboQuantPolar( - val bitsPerElement: Int = 4, - val blockSize: Int = 128 + val bitsPerElement: Int = 4, // must be 2, 3, 4 or 8 + val blockSize: Int = 128 // positive power of 2 ) : TensorEncoding { - override val name: String // "TurboQuant-Polar-4b" + override val name: String // "TurboQuant-Polar-4b" } public data class TurboQuantPolarQjl( val bitsPerElement: Int = 4, - val residualBits: Int = 1, + val residualBits: Int = 1, // 1..4 val blockSize: Int = 128 ) : TensorEncoding { - override val name: String // "TurboQuant-PolarQjl-4b+1r" + override val name: String // "TurboQuant-PolarQjl-4b+1r" } ``` -Plug into a `TensorSpec` (or directly into KV-cache storage) to apply at the storage layer. +Plug them into a `KvCacheConfig` (`keyEncoding` / `valueEncoding`) — the KV-cache store applies them at the storage layer. + +## KV-cache stores and presets + +```kotlin +import sk.ainet.lang.tensor.storage.KvCacheStore + +// Named preset: "safe-lowbit", "balanced", "experimental-max" +val cache = KvCacheStore.turboQuant( + preset = "balanced", numLayers = 32, numHeads = 32, headDim = 128, maxSeqLen = 4096 +) + +// Custom bit budgets (asymmetric K/V is the point): +val cache2 = KvCacheStore.turboQuant( + numLayers = 32, numHeads = 32, headDim = 128, maxSeqLen = 4096, + keyBits = 8, valueBits = 4, useQjl = false +) + +// Uncompressed FP32 baseline for quality comparison: +val dense = KvCacheStore.dense(numLayers = 32, numHeads = 32, headDim = 128, maxSeqLen = 4096) +``` + +Presets (`TurboQuantPresets`) encode the observation that key precision is more quality-sensitive than value precision: + +| Preset | Keys | Values | +|---|---|---| +| `safe-lowbit` | Q8_0 | TurboQuant-4 | +| `balanced` | TurboQuant-4 | TurboQuant-4 | +| `experimental-max` | TurboQuant-3 | TurboQuant-3 | + +The backing implementations are `TurboQuantKvCacheStore` / `DefaultKvCacheStore` (`appendToken(layer, key, value)`, `memoryReport()`). See `TurboQuantUsage.kt` in the source for the full integration guide. ## Codec ```kotlin public data class TurboQuantConfig( val bits: Int = 4, - val seed: Int = 0, val useQjl: Boolean = false, - val residualBits: Int = 1 + val residualBits: Int = 1, + val seed: Int = 0 ) { public companion object { public fun polarOnly(bits: Int = 4, seed: Int = 0): TurboQuantConfig @@ -48,30 +80,30 @@ public data class TurboQuantConfig( public object TurboQuantCodec { public fun encode(input: FloatArray, config: TurboQuantConfig): TurboQuantBlock public fun decode(block: TurboQuantBlock): FloatArray + public fun encodedSize(elementCount: Int, config: TurboQuantConfig): Int } ``` -Use `TurboQuantCodec` directly when you want raw FloatArray ↔ block transitions outside the tensor system. For tensor-level encoding, attach a `TurboQuantPolar` / `TurboQuantPolarQjl` to a `TensorSpec` and let the storage layer handle it. +Use `TurboQuantCodec` directly when you want raw FloatArray ↔ block transitions outside the tensor system (`TurboQuantBlock` carries `packedCodes`, `scales`, `seed`, `bits`, `elementCount`, optional `residual`; `sizeInBytes` reports the footprint). For KV-cache use, prefer the `KvCacheStore` factories above. On the JVM, the hot paths are SIMD-accelerated (`JvmTurboQuantKernels`, Java Vector API). ## Picking bits | Bits | Compression | Quality | |---|---|---| | 2 | ≈16× | Aggressive — measurable accuracy loss; use only with QJL residual and on tensors you've evaluated. | -| 3 | ≈10× | Aggressive; QJL recommended. | +| 3 | ≈10× | Aggressive (`experimental-max`); QJL recommended. | | 4 | ≈8× | Sweet spot for KV cache. Low quality loss, large memory savings. | -| 8 | ≈4× | Near-lossless on most workloads. Default for sensitive tensors (e.g. attention weights). | +| 8 | ≈4× | Near-lossless on most workloads. Default for keys in `safe-lowbit` (as Q8_0). | -Block size 128 is the default and rarely needs changing. Larger blocks compress better but degrade quality on heterogeneous tensors; smaller blocks are inverse. +Block size 128 is the default and rarely needs changing (must be a power of 2). Larger blocks compress better but degrade quality on heterogeneous tensors; smaller blocks are inverse. ## Where consumers apply it -- **KV cache in transformer inference**: encode keys + values to 4-bit polar — typical ~8× memory reduction with minimal accuracy impact. -- **Weights of large layers** that are read-only at inference: encode at load time; trade load time + storage size for memory footprint at runtime. -- **Activation cache in beam search / iterative decoding**: encode intermediate states. +- **KV cache in transformer inference** (the designed use case): 4-bit polar keys+values — typical ~8× memory reduction with minimal accuracy impact. +- **Activation cache in beam search / iterative decoding**: encode intermediate states via the codec. -Don't encode inputs or outputs that the consumer needs to read in raw FP32 form — encoding rounds, and you'll see drift. +Don't encode inputs or outputs that the consumer needs to read in raw FP32 form — encoding rounds, and you'll see drift. For *weight* compression, prefer the GGUF block formats (Q4_K, Q8_0, …) that the loaders preserve as packed storage — TurboQuant is aimed at the runtime KV cache. ## Determinism -`seed` controls QJL's randomised residual basis. Same seed → same encoding result for the same input. Use a fixed seed in tests; in production it's typically fine to leave the default 0. +`seed` controls the randomised rotation basis (stored per block). Same seed → same encoding result for the same input. Use a fixed seed in tests; in production it's typically fine to leave the default 0. diff --git a/skainet-consumer-skills/skills/skainet-java-consumer/SKILL.md b/skainet-consumer-skills/skills/skainet-java-consumer/SKILL.md index 9e780fb..00acf10 100644 --- a/skainet-consumer-skills/skills/skainet-java-consumer/SKILL.md +++ b/skainet-consumer-skills/skills/skainet-java-consumer/SKILL.md @@ -13,8 +13,9 @@ Calling SKaiNET from a pure-Java app: which Maven artifacts to depend on, the en - The consumer project's source is `.java`, not `.kt`, and they want to use SKaiNET. - Maven (not Gradle) configuration for SKaiNET dependencies. - Calling `SKaiNET.context()`, `SKaiNET.tensor(...)`, `SKaiNET.zeros(...)`, `TensorJavaOps.add(...)`, etc. from Java. -- Using `StableHloConverterFactory.createBasic()` / `createExtended()` for HLO export. +- Using `StableHloConverterFactory.createBasic()` / `createExtended()` / `createFast()` for HLO export. - Using `TokenizerFactory.fromGguf(metadata)` to build a tokenizer from a GGUF file. +- Building and training simple models from Java via `SequentialModelBuilder`, `Optimizers`, `Losses`, `TrainingLoop`. - Reading the Java JUnit 5 tests in `skainet-test-java` as canonical usage examples. ## When NOT to use @@ -29,10 +30,10 @@ Calling SKaiNET from a pure-Java app: which Maven artifacts to depend on, the en - `sk.ainet:skainet-bom:` (BOM) - `sk.ainet.core:skainet-lang-core` (DSL types + `SKaiNET` + `TensorJavaOps`) - `sk.ainet.core:skainet-backend-cpu` (`DirectCpuExecutionContext.create()`) - - Optional loaders: `skainet-io-core` + `skainet-io-{gguf|onnx|safetensors}` + - Optional loaders: `skainet-io-core` + `skainet-io-{gguf|onnx|safetensors}` (`TokenizerFactory` lives in `skainet-io-core`) - Optional HLO: `skainet-compile-core` + `skainet-compile-hlo` The `skainet-test-java` consumer module's `build.gradle.kts` is the canonical reference for the dependency set: `SKaiNET/skainet-test/skainet-test-java/build.gradle.kts:9-21`. -2. **JVM target ≥ 11.** Java 21 is recommended (matches SKaiNET's own JDK toolchain). The HLO test harness uses preview features and the Vector API (`--enable-preview --add-modules jdk.incubator.vector`); only enable those flags if you actually use Vector / preview APIs. +2. **JDK 21+.** SKaiNET's published JVM jars are Java 21 bytecode (the build compiles with `--release 21` / `jvmTarget = JVM_21`), so the consumer's toolchain and runtime must be 21 or newer. The HLO test harness uses preview features and the Vector API (`--enable-preview --add-modules jdk.incubator.vector`); only enable those flags if you actually use Vector / preview APIs. 3. **Always go through `SKaiNET` and `TensorJavaOps` — never import internal `sk.ainet.lang.*` or `sk.ainet.context.*` packages from Java unless absolutely necessary.** The Java entry points are the supported surface. 4. **Tensors crossing into Java are `Tensor`** (the Java view of `Tensor<*, *>`). Don't try to declare `Tensor` in Java — the Kotlin `*` projection collapses to `?` and any further generic bounds are awkward. 5. **DType is `DType.fp32()`, `DType.int32()`, `DType.int8()`, etc.** — never `KClass`. The `SKaiNET` factory takes a `DType` instance and resolves to a `KClass` internally. @@ -56,7 +57,7 @@ Calling SKaiNET from a pure-Java app: which Maven artifacts to depend on, the en sk.ainet skainet-bom - 0.20.0-SNAPSHOT + 0.57.0 pom import @@ -88,7 +89,7 @@ Calling SKaiNET from a pure-Java app: which Maven artifacts to depend on, the en ```kotlin dependencies { - implementation(platform("sk.ainet:skainet-bom:0.20.0-SNAPSHOT")) + implementation(platform("sk.ainet:skainet-bom:0.57.0")) implementation("sk.ainet.core:skainet-lang-core") implementation("sk.ainet.core:skainet-backend-cpu") } @@ -162,9 +163,9 @@ StableHloConverter extended = StableHloConverterFactory.createExtended(); **Tokenizer from GGUF metadata:** ```java -import sk.ainet.tokenizer.Tokenizer; -import sk.ainet.tokenizer.TokenizerFactory; -import sk.ainet.tokenizer.UnsupportedTokenizerException; +import sk.ainet.io.tokenizer.Tokenizer; +import sk.ainet.io.tokenizer.TokenizerFactory; +import sk.ainet.io.tokenizer.UnsupportedTokenizerException; try { Tokenizer tok = TokenizerFactory.fromGguf(ggufMetadataMap); @@ -172,9 +173,32 @@ try { } catch (UnsupportedTokenizerException e) { // GGUF didn't carry a recognised tokenizer; fall back to a manual one } +// package sk.ainet.io.tokenizer, shipped in skainet-io-core // from: SKaiNET/skainet-test/skainet-test-java/src/test/java/sk/ainet/java/ReleaseApiJavaTest.java ``` +**Build + train a small model entirely from Java (no Kotlin module needed):** + +```java +import sk.ainet.java.SequentialModelBuilder; + +Module model = new SequentialModelBuilder(ctx) + .input(4) + .dense(8) + .relu() + .dense(2) + .build(); + +TrainingLoop loop = TrainingLoop.builder() + .model(model) + .loss(Losses.mse()) + .optimizer(Optimizers.adam()) // lr defaults to 0.001 + .context(ctx) + .build(); +float lastLoss = loop.step(x, y); // or loop.train(dataSupplier, epochs) +// from: SKaiNET/skainet-test/skainet-test-java/src/test/java/sk/ainet/java/ModelBuilderTest.java +``` + **Spring Boot bean wiring (typical):** ```java @@ -201,7 +225,7 @@ public class Classifier { } ``` -For models, consumers typically build the `Module` in a small Kotlin module (the DSL is awkward from Java) and expose the `Module` to the Java service layer. Mixed-language consumer projects are the norm. +For simple feed-forward models, `SequentialModelBuilder` (package `sk.ainet.java`) covers the whole flow from Java. For anything the builder doesn't cover (conv nets, DAGs, custom modules), consumers build the `Module` in a small Kotlin module (the receiver-typed DSL is awkward from Java) and expose the `Module` to the Java service layer. Mixed-language consumer projects are the norm. ## Related skills @@ -252,15 +276,15 @@ public CompletableFuture> loadTensorAsync(String name) { ``` ```java -// WRONG — JVM 8 toolchain -java { toolchain { languageVersion = JavaLanguageVersion.of(8) } } +// WRONG — toolchain below 21 (SKaiNET jars are Java 21 bytecode) +java { toolchain { languageVersion = JavaLanguageVersion.of(17) } } ``` ```java -// RIGHT — JVM 11+ +// RIGHT — JDK 21+ java { toolchain { languageVersion = JavaLanguageVersion.of(21) } } ``` ## References - [`references/maven-deps.md`](references/maven-deps.md) — Maven `` blocks, BOM import scope, JDK toolchain notes. -- [`references/java-entry-points.md`](references/java-entry-points.md) — every Java-friendly entry point currently shipped (`SKaiNET`, `TensorJavaOps`, `StableHloConverterFactory`, `TokenizerFactory`, `TensorSpecs`). +- [`references/java-entry-points.md`](references/java-entry-points.md) — every Java-friendly entry point currently shipped (`SKaiNET`, `TensorJavaOps`, `SequentialModelBuilder`, `Optimizers`, `Losses`, `TrainingLoop`, `StableHloConverterFactory`, `TokenizerFactory`, `TensorSpecs`). diff --git a/skainet-consumer-skills/skills/skainet-java-consumer/evals/evals.json b/skainet-consumer-skills/skills/skainet-java-consumer/evals/evals.json index 8af8766..38e0f6e 100644 --- a/skainet-consumer-skills/skills/skainet-java-consumer/evals/evals.json +++ b/skainet-consumer-skills/skills/skainet-java-consumer/evals/evals.json @@ -14,7 +14,7 @@ "skainet-backend-cpu" ], "must_not_contain": [ - "0.20.0-SNAPSHOT\n compile", + "0.57.0\n compile", "sk.ainet\n skainet-lang-core" ], "must_not_contain_reason": "BOM-managed artifacts must omit ; libraries are sk.ainet.core, not sk.ainet." diff --git a/skainet-consumer-skills/skills/skainet-java-consumer/references/java-entry-points.md b/skainet-consumer-skills/skills/skainet-java-consumer/references/java-entry-points.md index 86bc115..253b0a6 100644 --- a/skainet-consumer-skills/skills/skainet-java-consumer/references/java-entry-points.md +++ b/skainet-consumer-skills/skills/skainet-java-consumer/references/java-entry-points.md @@ -4,11 +4,10 @@ The supported Java surface lives in `package sk.ainet.java` and a few matching f ## `SKaiNET` (factory + context) -`@file:JvmName("SKaiNET")` in `SKaiNET/skainet-backends/skainet-backend-cpu/src/jvmMain/kotlin/sk/ainet/java/SKaiNET.kt`. +`object SKaiNET` (`@file:JvmName("SKaiNET")`) in `SKaiNET/skainet-backends/skainet-backend-cpu/src/jvmMain/kotlin/sk/ainet/java/SKaiNET.kt`. ```java -ExecutionContext ctx = SKaiNET.context(); -ExecutionContext train = SKaiNET.context(); // EVAL by default +ExecutionContext ctx = SKaiNET.context(); // CPU context, EVAL phase Tensor t = SKaiNET.tensor(ctx, new int[]{2, 3}, DType.fp32(), new float[]{...}); Tensor ti = SKaiNET.tensorFromInts(ctx, new int[]{2}, DType.int32(), new int[]{1, 2}); @@ -20,7 +19,7 @@ Tensor f = SKaiNET.full(ctx, new int[]{2, 2}, DType.fp32(), 0.5f); Tensor r = SKaiNET.randn(ctx, new int[]{2, 2}); ``` -All members are `@JvmStatic` + `@JvmOverloads` — Java sees them as plain static methods with sensible default-omitting overloads. +All members are `@JvmStatic`; `zeros` / `ones` / `randn` are also `@JvmOverloads` (dtype defaults to FP32). For a training-phase context, call `DirectCpuExecutionContext.create(Phase.TRAIN)` directly — it is `@JvmStatic @JvmOverloads` too (defaults to `Phase.EVAL`). ## `TensorJavaOps` (operations facade) @@ -44,46 +43,58 @@ Other: `tril(a, k)` (default 0). All return `Tensor`. +## Model building + training facade (`sk.ainet.java`, in `skainet-lang-core`) + +All four live in `skainet-lang-core`'s `src/jvmMain/kotlin/sk/ainet/java/`: + +- **`SequentialModelBuilder`** — fluent builder for feed-forward stacks: `new SequentialModelBuilder(ctx)` (optional second `DType` arg, defaults FP32), then `.input(n)`, `.dense(n)`, `.relu()`, `.sigmoid()`, `.silu()`, `.gelu()`, `.softmax(dim)` (default -1), `.flatten(startDim, endDim)` (defaults 1, -1), `.build()` → `Module`. +- **`Optimizers`** — `@JvmStatic @JvmOverloads` factories: `adam(lr, beta1, beta2, epsilon, weightDecay)` (defaults 0.001/0.9/0.999/1e-8/0.0), `adamw(...)` (weightDecay defaults 0.01, decoupled), `sgd(lr, momentum, weightDecay)`. +- **`Losses`** — `crossEntropy(dim)`, `categoricalCrossEntropy(dim)`, `sparseCategoricalCrossEntropy(dim)`, `mse()`, `mae()`, `binaryCrossEntropy(epsilon)`, `bceWithLogits()`, `huber(delta)`, `hinge(margin)`, `squaredHinge(margin)`, `poisson(logInput, epsilon)`. +- **`TrainingLoop`** — `TrainingLoop.builder().model(m).loss(l).optimizer(o).context(ctx).build()`; then `step(x, y)` → float loss, `train(Supplier>>, epochs)` → `TrainingResult`, or `trainAsync(...)` → `CompletableFuture` (virtual threads). + ## `StableHloConverterFactory` -`SKaiNET/skainet-compile/skainet-compile-hlo/src/jvmMain/kotlin/...` exposes: +`object StableHloConverterFactory` in `SKaiNET/skainet-compile/skainet-compile-hlo/src/commonMain/kotlin/sk/ainet/compile/hlo/StableHloConverterFactory.kt` (common code now, no longer a jvmMain-only file): ```java StableHloConverter basic = StableHloConverterFactory.createBasic(); StableHloConverter extended = StableHloConverterFactory.createExtended(); +StableHloConverter fast = StableHloConverterFactory.createFast(); ``` -Pass a `ComputeGraph` (the lowered form of `dag { }`) to either; the converter emits StableHLO MLIR text. +All three are `@JvmStatic @JvmOverloads`; the no-arg forms shown above work from Java, and optional parameters (`ConstantMaterializationPolicy`, `ConversionErrorPolicy`, …) can be passed positionally. Pass a `ComputeGraph` (the lowered form of `dag { }`) to a converter; it emits StableHLO MLIR text. ## `TokenizerFactory` +Package **`sk.ainet.io.tokenizer`** (moved from `sk.ainet.tokenizer`), shipped in `skainet-io-core`: + ```java -import sk.ainet.tokenizer.Tokenizer; -import sk.ainet.tokenizer.TokenizerFactory; -import sk.ainet.tokenizer.UnsupportedTokenizerException; +import sk.ainet.io.tokenizer.Tokenizer; +import sk.ainet.io.tokenizer.TokenizerFactory; +import sk.ainet.io.tokenizer.UnsupportedTokenizerException; try { - Tokenizer tok = TokenizerFactory.fromGguf(ggufMetadataMap); - // also: TokenizerFactory.fromTokenizerJson(...) + Tokenizer tok = TokenizerFactory.fromGguf(ggufFieldsMap); // Map + // also: TokenizerFactory.fromTokenizerJson(jsonString) } catch (UnsupportedTokenizerException e) { /* ... */ } ``` -`fromGguf` reads tokenizer-related keys out of GGUF metadata and constructs a tokenizer if the format is recognised. +Dispatch is per-architecture, not per file format: `fromGguf` inspects `tokenizer.ggml.model`, `fromTokenizerJson` inspects `model.type`. Byte-level BPE (Qwen, GPT-2, Mistral-Nemo) and SentencePiece (LLaMA, Gemma, TinyLlama) are supported; WordPiece/BERT throws `UnsupportedTokenizerException`. ## `TensorSpecs` (encoding helper) -`@file:JvmName("TensorSpecs")` exposes: +`@file:JvmName("TensorSpecs")` on `skainet-lang-core`'s `sk/ainet/lang/tensor/ops/TensorSpecEncoding.kt` exposes: ```java TensorEncoding enc = TensorSpecs.getTensorEncoding(spec); TensorSpec annotated = TensorSpecs.withTensorEncoding(spec, TensorEncoding.Q8_0.INSTANCE); ``` -Used when you're building a `TensorSpec` for HLO export and need to tag the encoding (Q8_0, TurboQuantPolar, etc.). +Used when you're building a `TensorSpec` for HLO export and need to tag the encoding (Q8_0, etc.). `withTensorEncoding` returns a copy; `getTensorEncoding` returns `null` on an un-annotated spec. ## What's NOT supported from Java -- The DSL builders (`tensor { }`, `sequential { }`, `dag { }`) — receiver-typed lambdas don't translate. Build models in a Kotlin module and expose the resulting `Module` / `GraphProgram` to Java. +- The DSL builders (`tensor { }`, `sequential { }`, `dag { }`) — receiver-typed lambdas don't translate. Simple feed-forward stacks can be built with `SequentialModelBuilder`; for everything else build models in a Kotlin module and expose the resulting `Module` / `GraphProgram` to Java. - Suspend functions in `skainet-io-*`. Wrap with a Kotlin shim that returns `CompletableFuture`. - Internal `*.ops` field on `Tensor<*, *>` — its generics collapse to `?` in Java; use `TensorJavaOps`. - `KClass` parameters anywhere — use `DType` instances instead. diff --git a/skainet-consumer-skills/skills/skainet-java-consumer/references/maven-deps.md b/skainet-consumer-skills/skills/skainet-java-consumer/references/maven-deps.md index fc4f084..ee2ef96 100644 --- a/skainet-consumer-skills/skills/skainet-java-consumer/references/maven-deps.md +++ b/skainet-consumer-skills/skills/skainet-java-consumer/references/maven-deps.md @@ -10,7 +10,7 @@ Authoritative source: `SKaiNET/skainet-test/skainet-test-java/build.gradle.kts:9 sk.ainet skainet-bom - 0.20.0-SNAPSHOT + 0.57.0 pom import @@ -43,7 +43,8 @@ Both are required for any usable Java consumer: ## Optional dependencies (per use case) ```xml - + sk.ainet.core skainet-io-core @@ -54,7 +55,7 @@ Both are required for any usable Java consumer: - + sk.ainet.core skainet-compile-core @@ -73,7 +74,7 @@ Both are required for any usable Java consumer: ## JDK toolchain -SKaiNET targets JVM 11. Recommended for Java consumers: JDK 21 (matches SKaiNET's own toolchain in `skainet-test-java/build.gradle.kts`). +SKaiNET's published JVM jars are Java 21 bytecode — the root build enforces JDK 21+ and compiles with `--release 21` / `jvmTarget = JVM_21`. Java consumers therefore need JDK 21 or newer (`skainet-test-java/build.gradle.kts` uses `jvmToolchain(21)`). Maven (`pom.xml`): @@ -127,5 +128,5 @@ Skip if using a stable release version. - **Wrong group on the BOM**: it's `sk.ainet`, not `sk.ainet.core`. Hard rule from `skainet-consumer-setup`. - **Hard-pinning library versions**: defeats the BOM. Once `import` is set up, omit `` on every `sk.ainet.core:*` artifact. -- **JDK 8 toolchain**: SKaiNET's bytecode targets 11. JDK 8 won't link. +- **Toolchain below 21**: SKaiNET's JVM bytecode targets 21. JDK 17 or older won't link (`UnsupportedClassVersionError`). - **Missing the snapshot repo for `-SNAPSHOT` versions**: Maven won't synthesize one; declare it explicitly. diff --git a/skainet-consumer-skills/skills/skainet-model-loading/SKILL.md b/skainet-consumer-skills/skills/skainet-model-loading/SKILL.md index cc57727..f0a79b8 100644 --- a/skainet-consumer-skills/skills/skainet-model-loading/SKILL.md +++ b/skainet-consumer-skills/skills/skainet-model-loading/SKILL.md @@ -1,23 +1,24 @@ --- name: skainet-model-loading -description: Use when loading a pre-trained model into a SKaiNET-based app — GGUF (`GGUFModelReader`), ONNX (`OnnxLoader`), SafeTensors (`SafeTensorsParametersLoader`), or JSON. Trigger tokens include `GGUFModelReader`, `OnnxLoader.fromModelSource`, `SafeTensorsParametersLoader`, `loadGGUF`, `loadOnnx`, `.safetensors`, `.gguf`, `.onnx`, `ParametersLoader`. Do NOT fire on `model.forward(x, ctx)` (that's `skainet-inference`), defining model architecture in code (`skainet-nn-dsl`), or constructing tensors from raw arrays (`skainet-data-dsl`). +description: Use when loading a pre-trained model into a SKaiNET-based app — GGUF (`StreamingGgufParametersLoader`), ONNX (`OnnxLoader`), SafeTensors (`SafeTensorsParametersLoader`, `ShardedSafeTensorsParametersLoader`), or JSON. Trigger tokens include `StreamingGgufParametersLoader`, `GGUFModelReader`, `OnnxLoader.fromModelSource`, `SafeTensorsParametersLoader`, `ShardedSafeTensorsParametersLoader`, `loadGGUF`, `loadOnnx`, `.safetensors`, `.gguf`, `.onnx`, `ParametersLoader`. Do NOT fire on `model.forward(x, ctx)` (that's `skainet-inference`), defining model architecture in code (`skainet-nn-dsl`), or constructing tensors from raw arrays (`skainet-data-dsl`). version: 0.1.0 --- # skainet-model-loading -Loading pre-trained models in the four formats SKaiNET supports: GGUF (LLM weights, llama.cpp ecosystem), ONNX (cross-framework graph + weights), SafeTensors (HuggingFace weights), and the project's own JSON serialisation. Each format has a dedicated loader in `skainet-io-*`. +Loading pre-trained models in the four formats SKaiNET supports: GGUF (LLM weights, llama.cpp ecosystem), ONNX (cross-framework graph + weights), SafeTensors (HuggingFace weights, single-file or sharded), and the project's own JSON serialisation. Each format has a dedicated loader in `skainet-io-*`. ## When to use - Loading a `.gguf` / `.onnx` / `.safetensors` / `.json` file from disk, classpath, or assets. +- Loading a sharded HuggingFace checkpoint (`model.safetensors.index.json` + `model-NNNNN-of-NNNNN.safetensors`). - Picking which format to use for a new model file. - Streaming weights into a `Module` built with `sequential` or `dag`. - Reading the metadata / tokenizer config out of a GGUF file. ## When NOT to use -- The dependency is missing (`Unresolved reference: GGUFModelReader`) — that's `skainet-consumer-setup`. +- The dependency is missing (`Unresolved reference: StreamingGgufParametersLoader`) — that's `skainet-consumer-setup`. - The model is defined inline with the DSL and weights are random / hand-set — that's `skainet-nn-dsl` + `skainet-data-dsl`. - The forward pass after loading — that's `skainet-inference`. - Loading from Android `AssetManager` specifically — coordinate with `skainet-android-integration`. @@ -25,64 +26,72 @@ Loading pre-trained models in the four formats SKaiNET supports: GGUF (LLM weigh ## Hard rules 1. **One format = one loader artifact.** Don't add `skainet-io-onnx` if the consumer is only loading `.gguf` files; it brings pbandk and the protobuf runtime. Use the artifact picker in `skainet-consumer-setup`. -2. **Loaders are coroutine-suspending. Call them from a coroutine.** `OnnxLoader.load()`, `GGUFModelReader.loadTensor(name)`, `SafeTensorsParametersLoader.load(...)` are all `suspend`. Wrap from non-coroutine code with `runBlocking { }` only at the top level (CLI / test). In an Android `ViewModel`, use `viewModelScope.launch { }`. -3. **Loaders take a source factory, not a path string.** The signature is `() -> RandomAccessSource` (or `suspend () -> Source`). Open the file inside the lambda so the loader controls lifetime; never call `.use { }` outside. -4. **The tensor produced by a loader is `Tensor` with a known dtype (FP32 unless requested otherwise).** Pick the dtype with the loader's typed `load(ctx, dtype, …)` overload — don't load to FP32 then `cast()` to a smaller dtype unless quantisation is intentional. -5. **Pass an `onProgress` callback** for files >50 MB — the consumer-facing wrapper expects user feedback during long loads. +2. **Loaders are coroutine-suspending. Call them from a coroutine.** `OnnxLoader.load()`, `StreamingGgufParametersLoader.load(...)`, `SafeTensorsParametersLoader.load(...)` are all `suspend`. Wrap from non-coroutine code with `runBlocking { }` only at the top level (CLI / test). In an Android `ViewModel`, use `viewModelScope.launch { }`. +3. **Loaders take a source factory, not a path string.** The signature is `() -> RandomAccessSource` (or `suspend () -> Source` for ONNX). Open the file inside the lambda — `sk.ainet.io.openRandomAccessSource(path)` is the one cross-platform factory — so the loader controls lifetime; never call `.use { }` outside. (Exception: `ShardedSafeTensorsParametersLoader` takes the index-file *path*, because it must resolve sibling shard files itself.) +4. **Don't dequantise by accident.** The typed `load(ctx, dtype, …)` call picks the target dtype; the GGUF streaming loader preserves quantised source types (Q4_K, Q8_0, …) as packed tensor data instead of widening them. Don't load to FP32 then `cast()` to a smaller dtype unless quantisation is intentional. +5. **Pass an `onProgress` callback** for files >50 MB — the consumer-facing wrapper expects user feedback during long loads. `current`/`total` are tensor counts; `message` is the tensor name. +6. **`GGUFModelReader` is a deprecated fail-fast stub** (since the dtype-policy rework): its `loadTensor` throws. Use `StreamingGgufParametersLoader` — the legacy facade exists only so old code keeps compiling. ## Workflow 1. Pick the format. If converting between formats, use `skainet-compile-*` (separate skill, separate concern). 2. Check the matching artifact is on the classpath (see `skainet-consumer-setup`). 3. Construct the loader with a source factory and (optional) progress callback. -4. Inside a coroutine, call `load(...)` — bind into your model's `Module` parameters or hold the `ModelReader` for streaming. +4. Inside a coroutine, call `load(...)` and bind each delivered tensor into your model's `Module` parameters (the `WeightMapper` helpers in `sk.ainet.io.weights` do name-based mapping). 5. Test with a small known-good file before wiring into production code paths. ## Format picker | Format | Loader | When to pick | |---|---|---| -| GGUF | `GGUFModelReader` | LLM checkpoints from llama.cpp / ollama; tokenizer metadata embedded; quantised weights (Q4_0, Q8_0, …). | -| SafeTensors | `SafeTensorsParametersLoader` | HuggingFace models; transformer weights; safe (no pickle). | +| GGUF | `StreamingGgufParametersLoader` | LLM checkpoints from llama.cpp / ollama; tokenizer metadata embedded; quantised weights (Q4_K, Q8_0, …) preserved as packed storage. | +| SafeTensors (single file) | `SafeTensorsParametersLoader` | HuggingFace models; transformer weights; safe (no pickle). | +| SafeTensors (sharded) | `ShardedSafeTensorsParametersLoader` | Multi-file HF checkpoints with `model.safetensors.index.json`. | | ONNX | `OnnxLoader` | Cross-framework models (PyTorch / TensorFlow exports); graph + weights together. | | JSON | `skainet-compile-json` | SKaiNET's own portable serialisation; for round-tripping models built with the DSL. | ## Canonical examples -**GGUF — `GGUFModelReader`:** +**GGUF — `StreamingGgufParametersLoader`:** ```kotlin -import sk.ainet.io.gguf.GGUFModelReader +import sk.ainet.io.gguf.StreamingGgufParametersLoader +import sk.ainet.io.openRandomAccessSource import sk.ainet.context.DirectCpuExecutionContext +import sk.ainet.lang.types.FP32 import kotlinx.coroutines.runBlocking val ctx = DirectCpuExecutionContext.create() -val reader = GGUFModelReader(/* source factory pointing at file */) +val loader = StreamingGgufParametersLoader( + sourceProvider = { openRandomAccessSource("model-q4.gguf")!! }, + onProgress = { current, total, name -> println("[$current/$total] $name") } +) runBlocking { - val metadata = reader.metadata // Map: arch, vocab, etc. - val tensorInfos = reader.tensors // Map: shape + dtype per tensor - val qProj = reader.loadTensor("model.layers.0.self_attn.q_proj.weight") - // bind qProj into your model's parameters + loader.load(ctx, FP32::class) { name, tensor -> + // F32/I32 arrive dense; Q4_K / Q8_0 / … arrive as packed block storage + // bind 'name' -> 'tensor' into your Module's parameters + } } -reader.close() -// from: SKaiNET/skainet-io/skainet-io-gguf/src/commonMain/kotlin/sk/ainet/io/gguf/GGUFModelReader.kt (public surface) +// from: SKaiNET/skainet-io/skainet-io-gguf/src/commonMain/kotlin/sk/ainet/io/gguf/StreamingGgufParametersLoader.kt (public surface) ``` -GGUF is streaming-friendly — `loadTensor(name)` reads one tensor at a time, suitable for large LLM weights that don't fit in memory at once. +The streaming loader parses only the header up front and loads tensor data on demand — suitable for large LLM weights. A file containing tensors outside the supported set fails fast, before any tensor is delivered. Optional constructor knobs: `weightForm` / `weightFormFor` (encoding × byte order × shape × residency, e.g. `WeightForm(residency = WeightResidency.MAPPED)` to serve weights from memory-mapped pages), `keepF16Native` / `keepBf16Native`. For metadata/tokenizer access, use `StreamingGGUFReader.open(source).fields` (e.g. `"general.architecture"`) and `TokenizerFactory.fromGguf(fields)`. **SafeTensors — `SafeTensorsParametersLoader`:** ```kotlin import sk.ainet.io.safetensors.SafeTensorsParametersLoader +import sk.ainet.io.openRandomAccessSource import sk.ainet.lang.types.FP32 import kotlinx.coroutines.runBlocking val ctx = DirectCpuExecutionContext.create() val loader = SafeTensorsParametersLoader( - sourceProvider = { createRandomAccessSource("model.safetensors") }, + sourceProvider = { openRandomAccessSource("model.safetensors")!! }, onProgress = { current, total, name -> println("[$current/$total] $name") - } + }, + tensorFilter = { info -> !info.name.endsWith(".rotary_emb.inv_freq") } // optional (0.54.0+) ) runBlocking { loader.load(ctx, FP32::class) { name, tensor -> @@ -92,7 +101,25 @@ runBlocking { // from: SKaiNET/skainet-io/skainet-io-safetensors/src/commonMain/kotlin/sk/ainet/io/safetensors/SafeTensorsParametersLoader.kt (public surface) ``` -SafeTensors is callback-driven — the loader hands you each tensor as it's parsed; you bind it where it belongs. +SafeTensors is callback-driven — the loader hands you each tensor as it's parsed; you bind it where it belongs. `tensorFilter` skips tensors without reading them (name allowlists, size guards, dtypes the requested target can't accept) — filtered tensors don't count toward progress. BF16/F16 tensors dequantise to FP32 by default; `bf16Policy` / `fp16Policy = NarrowFloatLoadPolicy.KEEP_NATIVE` keeps the on-disk layout. + +**Sharded SafeTensors — `ShardedSafeTensorsParametersLoader` (0.53.0+):** + +```kotlin +import sk.ainet.io.safetensors.ShardedSafeTensorsParametersLoader + +val loader = ShardedSafeTensorsParametersLoader( + indexPath = "/models/gemma/model.safetensors.index.json", + onProgress = { c, t, name -> println("[$c/$t] $name") }, + tensorFilter = { info -> info.name.startsWith("model.") } // optional +) +runBlocking { + loader.load(ctx, FP32::class) { name, tensor -> /* bind */ } +} +// from: SKaiNET/skainet-io/skainet-io-safetensors/src/commonMain/kotlin/sk/ainet/io/safetensors/ShardedSafeTensorsParametersLoader.kt +``` + +Takes the `model.safetensors.index.json` path and resolves the `model-NNNNN-of-NNNNN.safetensors` shards itself; tensors are delivered in name-sorted order regardless of shard. Unsupported-dtype failures are raised in one exception *before* any tensor is delivered (a 40-shard load should not die on tensor 900). Path-based shard resolution means it needs a file system (JVM, Android, most native targets — not JS/Wasm). **ONNX — `OnnxLoader.fromModelSource`:** @@ -100,10 +127,11 @@ SafeTensors is callback-driven — the loader hands you each tensor as it's pars import sk.ainet.io.onnx.OnnxLoader import kotlinx.coroutines.runBlocking import kotlinx.io.asSource +import kotlinx.io.buffered runBlocking { val loader = OnnxLoader.fromModelSource { - java.io.File("model.onnx").inputStream().asSource() + java.io.File("model.onnx").inputStream().asSource().buffered() } val loaded = loader.load() val proto = loaded.proto // ModelProto from pbandk @@ -118,18 +146,20 @@ ONNX is a graph format — the loader hands you the protobuf representation. Low **Source factories — common idioms:** ```kotlin -// File on disk (JVM/desktop) -val src: () -> RandomAccessSource = { JvmFileRandomAccessSource(java.io.File("path")) } +import sk.ainet.io.openRandomAccessSource -// Classpath resource (server-side) -val src: () -> RandomAccessSource = { - val bytes = MyClass::class.java.getResourceAsStream("/model.gguf")!!.readBytes() - BytesRandomAccessSource(bytes) -} +// File on disk — one expect/actual for every platform (skainet-io-core). +// Returns null where the platform has no file system (JS, Wasm) or the file can't open. +val src: () -> RandomAccessSource = { openRandomAccessSource("/models/model.gguf")!! } + +// Classpath resource (server-side): copy to a temp file first, then open by path — +// the random-access loaders need to seek, which a classpath stream can't. // Android assets — see ../skainet-android-integration/SKILL.md ``` +(`createRandomAccessSource` in `sk.ainet.io.gguf` is a deprecated delegate to `openRandomAccessSource`.) + ## Related skills - Adding the right `skainet-io-*` artifact — [`../skainet-consumer-setup/SKILL.md`](../skainet-consumer-setup/SKILL.md). @@ -152,35 +182,39 @@ viewModelScope.launch { val loaded = loader.load() } // Android ViewModel ```kotlin // WRONG — opening the file outside the loader's source factory -val source = JvmFileRandomAccessSource(File("model.safetensors")) +val source = openRandomAccessSource("model.safetensors")!! val loader = SafeTensorsParametersLoader(sourceProvider = { source }) // Now multiple loads share one already-opened source — no, the loader expects to control lifetime. ``` ```kotlin // RIGHT — open inside the lambda val loader = SafeTensorsParametersLoader( - sourceProvider = { JvmFileRandomAccessSource(File("model.safetensors")) } + sourceProvider = { openRandomAccessSource("model.safetensors")!! } ) ``` ```kotlin -// WRONG — load to FP32 then cast to FP16 +// WRONG — load to FP32 then cast back down val tensor = loader.load(...) // FP32 val small = tensor.cast() // wastes the FP32 alloc ``` ```kotlin -// RIGHT — request FP16 directly from the typed loader overload -loader.load(ctx, FP16::class) { name, tensor -> ... } +// RIGHT — tell the loader to keep the narrow on-disk layout +val loader = StreamingGgufParametersLoader(sourceProvider, keepF16Native = true) // GGUF +val stl = SafeTensorsParametersLoader( + sourceProvider = srcFactory, + bf16Policy = NarrowFloatLoadPolicy.KEEP_NATIVE, // SafeTensors BF16 +) ``` ```kotlin -// WRONG — silently loading a multi-GB GGUF without progress feedback -val reader = GGUFModelReader(sourceProvider) -runBlocking { val w = reader.loadTensor("...") } // 30 second freeze +// WRONG — the deprecated legacy facade; loadTensor throws at runtime +val reader = GGUFModelReader() +runBlocking { val w = reader.loadTensor("...") } // error("GGUFModelReader is a legacy facade…") ``` ```kotlin -// RIGHT — use loaders that accept onProgress, surface progress to the UI / log -val loader = SafeTensorsParametersLoader(sourceProvider, onProgress = { c, t, n -> ui.update(c, t) }) +// RIGHT — the streaming loader, with progress surfaced to the UI / log +val loader = StreamingGgufParametersLoader(sourceProvider, onProgress = { c, t, n -> ui.update(c, t) }) ``` ## References diff --git a/skainet-consumer-skills/skills/skainet-model-loading/references/format-picker.md b/skainet-consumer-skills/skills/skainet-model-loading/references/format-picker.md index 0d4e702..1188c12 100644 --- a/skainet-consumer-skills/skills/skainet-model-loading/references/format-picker.md +++ b/skainet-consumer-skills/skills/skainet-model-loading/references/format-picker.md @@ -15,11 +15,14 @@ | Format | Loader | Artifact | |---|---|---| -| GGUF | `GGUFModelReader` | `sk.ainet.core:skainet-io-gguf` | -| SafeTensors | `SafeTensorsParametersLoader` | `sk.ainet.core:skainet-io-safetensors` | +| GGUF | `StreamingGgufParametersLoader` | `sk.ainet.core:skainet-io-gguf` | +| SafeTensors (single file) | `SafeTensorsParametersLoader` | `sk.ainet.core:skainet-io-safetensors` | +| SafeTensors (`model.safetensors.index.json` + shards) | `ShardedSafeTensorsParametersLoader` | `sk.ainet.core:skainet-io-safetensors` | | ONNX | `OnnxLoader.fromModelSource` | `sk.ainet.core:skainet-io-onnx` | | SKaiNET JSON | (tooling in `skainet-compile-json`) | `sk.ainet.core:skainet-compile-json` | +(`GGUFModelReader` is a deprecated fail-fast stub — see `loaders.md`.) + ## "I have a model in format X, which should I convert to?" | Source | Converter | Target | @@ -33,7 +36,7 @@ SKaiNET itself does not ship Python conversion tooling; conversion lives in the ## When format choice matters at runtime -- **GGUF**: best for LLMs because weights can be quantised in-format (Q4_0, Q8_0) and the loader streams. Use when memory is tight. +- **GGUF**: best for LLMs because weights can be quantised in-format (Q4_K, Q8_0, …) and the loader streams, preserving the packed encodings for the quantised matmul kernels. With `WeightForm(residency = WeightResidency.MAPPED)` weights are served straight from memory-mapped file pages. Use when memory is tight. - **SafeTensors**: best for general transformer weights — fast load, safe (no pickle), good HuggingFace ecosystem support. Pair with a SKaiNET-DSL model definition. - **ONNX**: best when you have a trained graph from another framework and want SKaiNET to consume it whole. Higher coupling to `skainet-compile-*` because the graph needs to lower into a runnable `Module`. - **JSON**: best for round-tripping models you built with the SKaiNET DSL — diff-friendly, version-controllable. @@ -42,4 +45,5 @@ SKaiNET itself does not ship Python conversion tooling; conversion lives in the - Treating SafeTensors as a "model" — it's weights only. You still need a SKaiNET `Module` (built with `sequential` or `dag`) to bind the weights into. - Using `OnnxLoader` to extract weights from an ONNX file when SafeTensors would be cleaner — ONNX loading parses the whole graph; if you only need weights, ask whether SafeTensors is available. -- Loading GGUF in-memory all at once via a `ByteArray` source — defeats GGUF's streaming model. Use a `RandomAccessSource` factory that mmaps the file. +- Loading GGUF in-memory all at once via a `ByteArray` source — defeats GGUF's streaming model. Use `openRandomAccessSource(path)` so the loader reads positionally (and can serve mapped weights). +- Hand-loading each shard of a sharded HF checkpoint with the single-file loader — `ShardedSafeTensorsParametersLoader` takes the index path, resolves shards, and fail-fasts on unsupported dtypes before delivering anything. diff --git a/skainet-consumer-skills/skills/skainet-model-loading/references/loaders.md b/skainet-consumer-skills/skills/skainet-model-loading/references/loaders.md index 179eae1..c6f4ab2 100644 --- a/skainet-consumer-skills/skills/skainet-model-loading/references/loaders.md +++ b/skainet-consumer-skills/skills/skainet-model-loading/references/loaders.md @@ -1,44 +1,88 @@ # Loader API reference Authoritative sources: -- `SKaiNET/skainet-io/skainet-io-gguf/src/commonMain/kotlin/sk/ainet/io/gguf/GGUFModelReader.kt` -- `SKaiNET/skainet-io/skainet-io-safetensors/src/commonMain/kotlin/sk/ainet/io/safetensors/SafeTensorsParametersLoader.kt` +- `SKaiNET/skainet-io/skainet-io-gguf/src/commonMain/kotlin/sk/ainet/io/gguf/StreamingGgufParametersLoader.kt` and `StreamingGGUFReader.kt` +- `SKaiNET/skainet-io/skainet-io-safetensors/src/commonMain/kotlin/sk/ainet/io/safetensors/SafeTensorsParametersLoader.kt` and `ShardedSafeTensorsParametersLoader.kt` - `SKaiNET/skainet-io/skainet-io-onnx/src/commonMain/kotlin/sk/ainet/io/onnx/OnnxLoader.kt` If signatures here disagree with the source, the source wins. -## `GGUFModelReader` +## GGUF — `StreamingGgufParametersLoader` ```kotlin -public class GGUFModelReader : ModelReader { - public override val metadata: Map - public override val tensors: Map - public override suspend fun loadTensor(name: String): TensorData<*, *> - public override fun close() +public class StreamingGgufParametersLoader( + private val sourceProvider: () -> RandomAccessSource, + private val onProgress: (current: Long, total: Long, message: String?) -> Unit = { _, _, _ -> }, + private val keepF16Native: Boolean = false, + private val keepBf16Native: Boolean = false, + private val weightForm: WeightForm? = null, // uniform form; null = as-stored, on-heap + private val weightFormFor: ((tensorName: String) -> WeightForm?)? = null, // per-tensor override, wins over weightForm + private val traceSink: TraceSink = NoopTraceSink, // reports load-time re-encodings + private val i2sLayout: I2sGgufLayout = I2sGgufLayout.GROUP_128, // BitNet I2_S bit order +) : ParametersLoader { + public suspend fun load( + ctx: ExecutionContext, dtype: KClass, + onTensorLoaded: (String, Tensor) -> Unit + ) } ``` -Construct with a source provider (the exact constructor signature varies by version; check the source). Stream tensors one at a time via `loadTensor(name)`. Use `metadata` for `gguf.architecture`, vocab, special tokens, and `tensors` for per-tensor shape/dtype info. +- F32 / I32 tensors arrive dense; quantised tensors (Q4_K, Q8_0, Q4_0, Q5_0, Q5_1, Q5_K, Q6_K, …) arrive as packed block `TensorData` with their source encoding preserved. +- Fail-fast contract (#919): a file containing tensor types outside the supported set throws before any tensor is delivered, naming the offenders. +- `WeightForm(encoding, order, shape, residency)` is the one memory-intent knob: `WeightForm(residency = WeightResidency.MAPPED)` serves weights from file-backed pages (the Android default via `AndroidGguf.loader`); `EncodingRequest.DequantizeTo(FP32)` forces dense FP32. +- Companion `withPolicy(...)` builds the loader from a generalised `DTypePolicy` (e.g. `Require(FP16)` → `keepF16Native`). + +### Metadata and tokenizer + +```kotlin +val reader = StreamingGGUFReader.open(openRandomAccessSource(path)!!) +reader.fields["general.architecture"] // LinkedHashMap of GGUF metadata +reader.tensors // List — shape/dtype/offset, no payload +val tokenizer = TokenizerFactory.fromGguf(reader.fields) // sk.ainet.io.tokenizer (skainet-io-core) +``` + +Memory use is proportional to metadata size (~1 MB), not file size. -The `TokenizerFactory.fromGguf(metadata)` Java helper (in `skainet-compile-hlo`) builds a tokenizer from the GGUF metadata map. +### Legacy: `GGUFModelReader` -## `SafeTensorsParametersLoader` +`@Deprecated` fail-fast stub — `metadata`/`tensors` are empty and `loadTensor(name)` throws. It exists only so old dependants compile; migrate to `StreamingGgufParametersLoader`. + +## SafeTensors — `SafeTensorsParametersLoader` ```kotlin public class SafeTensorsParametersLoader( private val sourceProvider: () -> RandomAccessSource, - private val onProgress: (current: Long, total: Long, message: String?) -> Unit = { _, _, _ -> } + private val onProgress: (current: Long, total: Long, message: String?) -> Unit = { _, _, _ -> }, + private val bf16Policy: Bf16LoadPolicy = NarrowFloatLoadPolicy.DEQUANT_TO_FP32, + private val fp16Policy: NarrowFloatLoadPolicy = NarrowFloatLoadPolicy.DEQUANT_TO_FP32, + private val tensorFilter: ((StreamingSafeTensorInfo) -> Boolean)? = null, // 0.54.0+ ) : ParametersLoader { - - public override suspend fun load( - ctx: ExecutionContext, - dtype: KClass, - onTensorLoaded: (name: String, tensor: Tensor) -> Unit + public suspend fun load( + ctx: ExecutionContext, dtype: KClass, + onTensorLoaded: (String, Tensor) -> Unit ) } ``` -Callback-style: `onTensorLoaded` fires once per tensor, in file order. Progress: `current` and `total` are byte counts; `message` is the tensor name being parsed. +Callback-style: `onTensorLoaded` fires once per tensor, in file order. Progress: `current` and `total` are **tensor counts** (filtered tensors excluded); `message` is the tensor name. Dtype conversions: F32/F64 → FP32, I32/I64 → Int32, I8/U8 → Int8, F16/BF16 → FP32 by default or kept native via the policies (`KEEP_NATIVE` routes matmuls to the vectorised BF16 kernel — confirm dispatch support before flipping). A companion `withPolicy(sourceProvider, policy, ...)` accepts a `DTypePolicy` instead. + +## Sharded SafeTensors — `ShardedSafeTensorsParametersLoader` (0.53.0+) + +```kotlin +public class ShardedSafeTensorsParametersLoader( + private val indexPath: String, // path to model.safetensors.index.json + private val onProgress: (current: Long, total: Long, message: String?) -> Unit = { _, _, _ -> }, + private val bf16Policy: Bf16LoadPolicy = NarrowFloatLoadPolicy.DEQUANT_TO_FP32, + private val fp16Policy: NarrowFloatLoadPolicy = NarrowFloatLoadPolicy.DEQUANT_TO_FP32, + private val allowPartial: Boolean = false, // true tolerates missing shards + private val tensorFilter: ((ShardedTensorInfo) -> Boolean)? = null, +) : ParametersLoader +``` + +- Same materialisation as the single-file loader (shared `SafeTensorsMaterializer`), so both produce identical tensors for identical bytes. +- Tensors are delivered in name-sorted order regardless of which shard holds them; every shard stays open until the load completes. +- Fail-fast pre-scan: all tensors whose dtype can't materialise into the requested `DType` are reported in one `IllegalArgumentException` *before* any tensor is delivered. +- Resolves shards by path (`openRandomAccessSource`), so it needs a file system — not available on JS/Wasm. ## `OnnxLoader` @@ -61,30 +105,32 @@ public class OnnxLoader( } } -public class OnnxLoadedModel( - public val proto: M, - public val rawBytes: ByteArray +public data class OnnxLoadedModel( + val proto: M, + val rawBytes: ByteArray ) ``` `fromModelSource` is the common entry point — produces an `OnnxLoader` which decodes the standard `onnx.ModelProto` from pbandk. Use the generic `fromSource` if your `.onnx` file is a different protobuf message. -`Source` is `kotlinx-io-core`'s read-only stream type; convert from JVM `InputStream` via `inputStream.asSource()`, from `kotlin-io` `Buffer` via direct construction. +`Source` is `kotlinx-io-core`'s read-only stream type; convert from a JVM `InputStream` via `inputStream.asSource().buffered()`. Note: `skainet-io-onnx` keeps kotlinx-io as an `implementation` dependency, so a consumer building the `Source` itself must declare `kotlinx-io-core` too ("Cannot access class kotlinx.io.Source" is that missing dependency; only `skainet-data-source` api-exports kotlinx-io, since 0.57.0). + +To push initializer weights into a DSL-built module, `OnnxWeightLoader.applyWeights(module, tensors)` (in `skainet-io-onnx`) delegates to the format-agnostic `WeightMapper` in `sk.ainet.io.weights`. ## `ModelReader` and `ParametersLoader` interfaces -Both live in `skainet-io-core`. Adding a new format means implementing one or both: +Both live in `skainet-io-core` (`sk.ainet.io`). Adding a new format means implementing one: -- `ModelReader` — graph + weights + metadata; suitable for formats like ONNX (full graph) or GGUF (weights + metadata). -- `ParametersLoader` — weights only; suitable for formats like SafeTensors that ship raw tensors without a graph. +- `ParametersLoader` — push-style, context-bound: `suspend fun load(ctx, dtype, onTensorLoaded)`. All three production loaders (GGUF streaming, SafeTensors single + sharded) implement it. +- `ModelReader` — pull-style (`metadata`, `tensors`, `loadTensor(name)`); legacy surface, kept for compatibility. -`OnnxLoader` is currently structured as a concrete class rather than `ModelReader` because of the protobuf-message generic. +`OnnxLoader` is a concrete class rather than either interface because of the protobuf-message generic. ## Source types | Type | Where from | |---|---| -| `RandomAccessSource` | `skainet-io-core`. Random-access byte source — required by SafeTensors and GGUF (need to seek). | +| `RandomAccessSource` | `sk.ainet.io` (`skainet-io-core`). Positional-read byte source — required by SafeTensors and GGUF (need to seek). | | `kotlinx.io.Source` | `kotlinx-io-core`. Sequential read-only stream — sufficient for ONNX (parse once). | -`RandomAccessSourceFactory` is `expect`/`actual` per platform (`commonMain` declares; `jvmMain` / `androidMain` / native targets implement). Consumers normally don't construct these directly — pass a path or a stream and let the platform pick. +`openRandomAccessSource(filePath)` in `sk.ainet.io` is the one `expect`/`actual` factory for every platform (#1037): `JvmRandomAccessSource` on the JVM, `AndroidRandomAccessSource` (positional `FileChannel` reads) on Android, `pread(2)`-based sources on native; returns `null` on platforms without a file system (JS, Wasm) so callers can fall back to sequential whole-file loading. The per-format `createRandomAccessSource` functions are deprecated delegates to it. diff --git a/skainet-consumer-skills/skills/skainet-nn-dsl/SKILL.md b/skainet-consumer-skills/skills/skainet-nn-dsl/SKILL.md index 94f342a..3cae840 100644 --- a/skainet-consumer-skills/skills/skainet-nn-dsl/SKILL.md +++ b/skainet-consumer-skills/skills/skainet-nn-dsl/SKILL.md @@ -52,7 +52,7 @@ val cnn = sequential { } ``` -Layer signatures (from `NetworkBuilder.kt:104-400+`): +Layer signatures (from `NetworkBuilder.kt:113-467`): ```kotlin input(inputSize: Int, id: String = "", requiresGrad: Boolean = false) @@ -61,18 +61,24 @@ dense(outputDimension: Int, id: String = "", content: DENSE.() -> Unit = { flatten(id: String = "", content: FLATTEN.() -> Unit = {}) activation(id: String = "", activation: (Tensor) -> Tensor) softmax(dim: Int = -1, id: String = "") +gru(hiddenSize: Int, id: String = "", content: GRU.() -> Unit = {}) conv1d(outChannels: Int, kernelSize: Int, stride: Int = 1, padding: Int = 0, dilation: Int = 1, groups: Int = 1, bias: Boolean = true, id: String = "", content: CONV1D.() -> Unit = {}) conv2d(outChannels: Int, kernelSize: Pair, stride: Pair = 1 to 1, padding: Pair = 0 to 0, dilation: Pair = 1 to 1, groups: Int = 1, bias: Boolean = true, id: String = "", content: CONV2D.() -> Unit = {}) conv3d(outChannels: Int, kernelSize: Triple, ...) maxPool2d(kernelSize: Pair, stride: Pair = kernelSize, padding: Pair = 0 to 0, id: String = "") -avgPool2d(kernelSize: Pair, ...) +avgPool2d(kernelSize: Pair, stride: Pair = kernelSize, padding: Pair = 0 to 0, countIncludePad: Boolean = true, id: String = "") upsample2d(scale: Pair = 2 to 2, mode: UpsampleMode = UpsampleMode.Nearest, alignCorners: Boolean = false, id: String = "") batchNorm(numFeatures: Int, eps: Double = 1e-5, momentum: Double = 0.1, affine: Boolean = true, id: String = "") groupNorm(numGroups: Int, numChannels: Int, eps: Double = 1e-5, affine: Boolean = true, id: String = "") layerNorm(normalizedShape: IntArray, eps: Double = 1e-5, elementwiseAffine: Boolean = true, id: String = "") +sequential(content: NeuralNetworkDsl.() -> Unit) // nested grouping block +stage(id: String, content: NeuralNetworkDsl.() -> Unit) +stage(id: String, content: NeuralNetworkDsl.() -> Unit) // precision-scoped stage ``` -Each spatial layer has a "content-block-only" overload (`conv2d(id) { outChannels = ...; kernelSize(5); stride(1); padding(2) }`). Use it when configuration is verbose enough to warrant block syntax. +`conv2d`, `maxPool2d` and `upsample2d` have a "content-block-only" overload (`conv2d(id) { outChannels = ...; kernelSize(5); stride(1); padding(2) }`) — `conv1d`, `conv3d` and `avgPool2d` do not. Use it when configuration is verbose enough to warrant block syntax. + +LLM/transformer layer builders (`embedding`, `rmsNorm`, `multiHeadAttention`, `swiGluFFN`, `residual`, `xielu`) are NOT in this DSL — they moved to the SKaiNET-transformers repository's llm-core module (`NetworkBuilder.kt:422-423` notes the move). The fused attention *op* stayed in the engine: `ops.scaledDotProductAttention(query, key, value, mask = null, scale = 0f, causal = false)` (`TensorOps.kt:389-397`) with native grouped-query attention — K/V come as `[batch, nKVHeads, kvLen, headDim]`, `nKVHeads` must divide the query's `nHeads`, and K/V are never tiled up to the query head count. ### Activation as a separate layer vs function @@ -94,7 +100,7 @@ val program = dag { val y = relu(mm) output(y) } -// from: SKaiNET/skainet-lang/skainet-lang-dag/src/commonMain/kotlin/sk/ainet/lang/dag/GraphDsl.kt:48-65 +// from: SKaiNET/skainet-lang/skainet-lang-dag/src/commonMain/kotlin/sk/ainet/lang/dag/GraphDsl.kt:43-70 ``` DAG node helpers (from `GraphDsl.kt`): @@ -105,12 +111,29 @@ parameter(name: String, spec: TensorSpec): GraphValue parameter(name: String, builder: SymbolicTensorBuilder.() -> TensorSpec): GraphValue constant(name: String, spec: TensorSpec): GraphValue constant(name: String, builder: SymbolicTensorBuilder.() -> TensorSpec): GraphValue -op(operation: Operation, inputs: List>, id: String = "", attributes: Map): List> +op(operation: Operation, inputs: List>, id: String = "", attributes: Map = emptyMap()): List> output(vararg values: GraphValue<*>) ``` The DAG builder is **definition-only** — no tensors are allocated. The returned `GraphProgram` is consumed by `skainet-compile-dag` to produce a `ComputeGraph`. +### Schedule and dtype hints on DAG nodes + +Two annotation DSLs ride a node's `attributes` without changing what the graph computes: + +```kotlin +dag { + schedule(parallel("heads")) { // every op recorded inside carries the hint + op(sdpa, listOf(q, k, v)) + } + op(matmul, listOf(x, w), schedule = parallel("rows", parallelism = 8)) // single op + op(matmul, listOf(x, w), dtypePolicy = DTypePolicy.Require(BF16)) // dtype constraint +} +// from: SKaiNET/skainet-lang/skainet-lang-dag/src/commonMain/kotlin/sk/ainet/lang/dag/ScheduleDsl.kt + DtypePolicyDsl.kt +``` + +`parallel(vararg dims, parallelism = null)` builds a `ScheduleHint` (`sk.ainet.context.schedule.ScheduleHint`). `ScheduleAnnotationPass` (skainet-compile-opt) validates hints per op, and the StableHLO export emits them as the `skainet.schedule` module attribute. Dtype policies feed `DTypeConstraintResolutionPass`. + ### Reusable sub-graphs — `dagModule` ```kotlin @@ -128,7 +151,7 @@ val program = dag { val out = module(residualBlock, listOf(x)) output(out[0]) } -// from: SKaiNET/skainet-lang/skainet-lang-dag/src/commonMain/kotlin/sk/ainet/lang/dag/GraphDsl.kt:222-257 +// from: SKaiNET/skainet-lang/skainet-lang-dag/src/commonMain/kotlin/sk/ainet/lang/dag/GraphDsl.kt:419-457 ``` ## Decision rule — sequential or DAG? @@ -137,7 +160,7 @@ val program = dag { |---|---| | MLP, CNN with linear stack of layers, simple RNN | `sequential { }` | | ResNet (skip connections), U-Net, multi-input, multi-output | `dag { }` | -| You need symbolic compilation to C / HLO / ONNX | `dag { }` (compiler operates on `GraphProgram`) | +| You need symbolic compilation to C / StableHLO / JSON / Minerva | `dag { }` (compiler operates on `GraphProgram`) | | You're in a unit test and want `model.forward(x, ctx)` directly | `sequential` | | You'd like to reuse a sub-graph in multiple places | `dag` + `dagModule { }` | diff --git a/skainet-consumer-skills/skills/skainet-nn-dsl/references/dag-nodes.md b/skainet-consumer-skills/skills/skainet-nn-dsl/references/dag-nodes.md index ab71020..1482aa4 100644 --- a/skainet-consumer-skills/skills/skainet-nn-dsl/references/dag-nodes.md +++ b/skainet-consumer-skills/skills/skainet-nn-dsl/references/dag-nodes.md @@ -6,7 +6,7 @@ Authoritative source: `SKaiNET/skainet-lang/skainet-lang-dag/src/commonMain/kotl ```kotlin public fun dag(block: DagBuilder.() -> Unit): GraphProgram -// from: GraphDsl.kt:61-65 +// from: GraphDsl.kt:63-67 ``` The DAG builder is **definition-only**: no tensors are allocated. The returned `GraphProgram` is what `skainet-compile-dag` lowers into an executable `ComputeGraph`. @@ -19,10 +19,10 @@ public data class GraphValue( public val outputIndex: Int, public val spec: TensorSpec ) -// from: GraphDsl.kt:14-19 +// from: GraphDsl.kt:17-21 ``` -Every value flowing through the graph is one of these. `spec` carries shape + dtype metadata (for shape inference and ONNX export). +Every value flowing through the graph is one of these. `spec` carries shape + dtype metadata used by shape inference and the compile/export passes. ## Inputs / parameters / constants @@ -31,23 +31,23 @@ public fun input( name: String, spec: TensorSpec = TensorSpec(name = name, shape = null, dtype = "unknown") ): GraphValue -// from: GraphDsl.kt:127-136 +// from: GraphDsl.kt:152 public fun parameter(name: String, spec: TensorSpec): GraphValue -// from: GraphDsl.kt:141-150 +// from: GraphDsl.kt:166 public inline fun parameter( name: String, noinline builder: SymbolicTensorBuilder.() -> TensorSpec ): GraphValue -// from: GraphDsl.kt:174-182 +// from: GraphDsl.kt:372-379 public fun constant(name: String, spec: TensorSpec): GraphValue public inline fun constant( name: String, noinline builder: SymbolicTensorBuilder.() -> TensorSpec ): GraphValue -// from: GraphDsl.kt:155-195 +// from: GraphDsl.kt:180, 385-392 ``` The ` { shape(...) { ... } }` form is the same shape-DSL family as the data DSL — see `skainet-data-dsl/references/tensor-builders.md`. @@ -61,16 +61,32 @@ public fun op( id: String = "", attributes: Map = emptyMap() ): List> -// from: GraphDsl.kt:200-206 +// from: GraphDsl.kt:398-404 ``` Use this to wire any `Operation` instance — useful when the DSL doesn't yet have a sugar function for an op you need. +Two annotated `op(...)` overloads attach compile-lane hints to the node's attributes: + +```kotlin +// Schedule hint (SKEEP-005 compile lane) — ScheduleDsl.kt +public fun DagBuilder.op(operation: Operation, inputs: List>, + schedule: ScheduleHint, id: String = "", extraAttributes: Map = emptyMap()): List> +public fun DagBuilder.schedule(hint: ScheduleHint, block: DagBuilder.() -> Unit) // ambient hint for every op in block +public fun parallel(vararg dims: String, parallelism: Int? = null): ScheduleHint // e.g. parallel("heads"), parallel("rows", parallelism = 8) + +// Dtype policy (W6 of the hybrid adaptive DSL RFC) — DtypePolicyDsl.kt +public fun DagBuilder.op(operation: Operation, inputs: List>, + dtypePolicy: DTypePolicy, id: String = "", extraAttributes: Map = emptyMap()): List> +``` + +Hints never change what the graph computes. `ScheduleAnnotationPass` (skainet-compile-opt) validates schedule hints per op and the StableHLO export emits them as the `skainet.schedule` module attribute; dtype policies feed `DTypeConstraintResolutionPass`. `DagBuilder.withAttributes(attributes, block)` (`GraphDsl.kt:86-93`) is the generic ambient-attribute mechanism both are built on. + ## Outputs ```kotlin public fun output(vararg values: GraphValue<*>) -// from: GraphDsl.kt:211-214 +// from: GraphDsl.kt:409-411 ``` If `output(...)` is never called, the last node's outputs are used as the program's outputs. Be explicit anyway — drift in node order will silently change which value is "the output." @@ -85,20 +101,20 @@ public abstract class DagModule { public fun DagBuilder.module(module: DagModule, inputs: List>): List> public fun dagModule(block: DagBuilder.(List>) -> List>): DagModule -// from: GraphDsl.kt:222-257 +// from: GraphDsl.kt:419-454 ``` `dagModule { inputs -> ... }` is the easy way to define a reusable block. It's instantiated with `module(myBlock, listOf(...))` inside any `dag { ... }`. ## Op-name sugar -The DSL has helpers like `matmul(a, b)`, `relu(x)`, `add(a, b)` declared as DSL extensions on `DagBuilder` (in the same file family). Each calls `recordNode(...)` with the appropriate `Operation` and propagates inferred output specs. +Helpers like `matmul(a, b)`, `relu(x)`, `add(a, b)` are KSP-generated extensions on `DagBuilder`: `GraphDslOps.kt` declares `@GenerateGraphDsl public interface GraphDslOps : TensorOps`, and the processor in `skainet-lang-ksp-processor` (`GraphDslGenerator.kt`) emits one DSL extension per `TensorOps` operation. Real usage: `skainet-compile/skainet-compile-dag/src/commonTest/kotlin/sk/ainet/compile/graph/GraphProgramCompilerTest.kt:19-20` (`val mm = matmul(x, w); val y = relu(mm)` inside `dag { }`). -If a helper you want isn't yet present, fall back to `op(operation, inputs)`. Don't add helpers without also adding shape inference for the matching `Operation`. +If a helper you want isn't present, fall back to `op(operation, inputs)` with the `Operation` instance (e.g. `op(MatmulOperation(), listOf(x, w))`). ## Shape inference -`recordNode` calls `operation.inferOutputs(inputSpecs)` — implemented by each `Operation`. If inference fails (or returns empty), the DSL falls back to propagating dtype/shape from the first input. Treat fallback as a code smell — the operation should know its output shape. +`recordNode` calls `ensureOutputSpecs`, which first consults the DSL-side `inferDagOutputSpecs(operation, inputs, nodeId)` hook, then `operation.inferOutputs(inputSpecs)` — implemented by each `Operation`. If inference fails (or returns empty), the DSL falls back to propagating dtype/shape from the first input (`GraphDsl.kt:98-122`). Treat fallback as a code smell — the operation should know its output shape. ## When to use op-only `op(...)` vs `dagModule` @@ -113,6 +129,7 @@ If a helper you want isn't yet present, fall back to `op(operation, inputs)`. Do - `skainet-compile:skainet-compile-hlo` — emits StableHLO MLIR. - `skainet-compile:skainet-compile-c` — generates C99/Arduino code. - `skainet-compile:skainet-compile-json` — JSON serialisation for tooling. -- `skainet-compile:skainet-compile-opt` — graph optimization passes. +- `skainet-compile:skainet-compile-opt` — graph optimization passes (incl. `ScheduleAnnotationPass`, `DTypeConstraintResolutionPass`). +- `skainet-compile:skainet-compile-minerva` — secure-MCU project-bundle export. The DSL author rarely calls these directly; the test or app entry point does. diff --git a/skainet-consumer-skills/skills/skainet-nn-dsl/references/layers.md b/skainet-consumer-skills/skills/skainet-nn-dsl/references/layers.md index 390d5c0..5058b56 100644 --- a/skainet-consumer-skills/skills/skainet-nn-dsl/references/layers.md +++ b/skainet-consumer-skills/skills/skainet-nn-dsl/references/layers.md @@ -8,13 +8,13 @@ Authoritative source: `SKaiNET/skainet-lang/skainet-lang-core/src/commonMain/kot public inline fun sequential( content: NeuralNetworkDsl.() -> Unit ): Module -// from: NetworkBuilder.kt:62-67 +// from: NetworkBuilder.kt:63-68 public inline fun sequential( executionContext: ExecutionContext, content: NeuralNetworkDsl.() -> Unit ): Module -// from: NetworkBuilder.kt:72-78 +// from: NetworkBuilder.kt:73-79 ``` The two-arg overload wires the execution context (and via it the tensor factory + ops) at construction time. The single-arg overload uses `DefaultNeuralNetworkExecutionContext()`. @@ -24,7 +24,7 @@ The two-arg overload wires the execution context (and via it the tensor factory ```kotlin fun input(inputSize: Int, id: String = "", requiresGrad: Boolean = false) fun input(inputShape: IntArray, id: String = "", requiresGrad: Boolean = false) // per-sample shape, batch excluded -// from: NetworkBuilder.kt:112-128 +// from: NetworkBuilder.kt:113-129 ``` Use `IntArray` form when downstream layers are spatial (`conv2d`, `maxPool2d`) and a later `flatten()` needs to know the unrolled feature count. @@ -37,16 +37,25 @@ fun dense(id: String = "", content: DENSE.() -> Unit = {}) fun dense(outputDimension: Int, id: String = "", content: DENSE.() -> Unit = {}): Module fun dense(id: String = "", content: DENSE.() -> Unit = {}): Module -// from: NetworkBuilder.kt:146-181 +// from: NetworkBuilder.kt:147-199 ``` The `` overload allows mixed precision — one dense layer at FP16 inside an FP32 network, etc. +## Recurrent + +```kotlin +fun gru(hiddenSize: Int, id: String = "", content: GRU.() -> Unit = {}) +// from: NetworkBuilder.kt:166 +``` + +The GRU layer infers its input size from the preceding layer's output dimension. The `GRU` scope exposes only `trainable` (`NetworkBuilder.kt:614-616`). + ## Reshape / Flatten ```kotlin fun flatten(id: String = "", content: FLATTEN.() -> Unit = {}) -// from: NetworkBuilder.kt:137 +// from: NetworkBuilder.kt:138 ``` ## Convolutions @@ -63,7 +72,7 @@ fun conv1d( id: String = "", content: CONV1D.() -> Unit = {} ) -// from: NetworkBuilder.kt:356-366 +// from: NetworkBuilder.kt:368-378 (no block-only overload) fun conv2d( outChannels: Int, @@ -77,7 +86,7 @@ fun conv2d( content: CONV2D.() -> Unit = {} ) fun conv2d(id: String = "", content: CONV2D.() -> Unit) // block-only form -// from: NetworkBuilder.kt:264-289 +// from: NetworkBuilder.kt:276-308 fun conv3d( outChannels: Int, @@ -90,7 +99,7 @@ fun conv3d( id: String = "", content: CONV3D.() -> Unit = {} ) -// from: NetworkBuilder.kt:381-391 +// from: NetworkBuilder.kt:393-403 (no block-only overload) ``` ## Pooling @@ -103,7 +112,7 @@ fun maxPool2d( id: String = "" ) fun maxPool2d(id: String = "", content: MAXPOOL2D.() -> Unit) // block-only form -// from: NetworkBuilder.kt:299-318 +// from: NetworkBuilder.kt:311-337 fun avgPool2d( kernelSize: Pair, @@ -112,7 +121,7 @@ fun avgPool2d( countIncludePad: Boolean = true, id: String = "" ) -// signature continues from NetworkBuilder.kt:393+; check source for full args +// from: NetworkBuilder.kt:414-420 (no block-only overload) ``` ## Upsampling @@ -125,7 +134,7 @@ fun upsample2d( id: String = "" ) fun upsample2d(id: String = "", content: UPSAMPLE2D.() -> Unit) -// from: NetworkBuilder.kt:328-341 +// from: NetworkBuilder.kt:340-355; UpsampleMode is `Nearest` or `Bilinear` (sk.ainet.lang.tensor.ops.UpsampleMode) ``` ## Normalization @@ -151,7 +160,7 @@ fun layerNorm( elementwiseAffine: Boolean = true, id: String = "" ) -// from: NetworkBuilder.kt:209-249 +// from: NetworkBuilder.kt:221-263 ``` ## Activations @@ -159,19 +168,35 @@ fun layerNorm( ```kotlin fun activation(id: String = "", activation: (Tensor) -> Tensor) fun softmax(dim: Int = -1, id: String = "") -// from: NetworkBuilder.kt:189-197 +// from: NetworkBuilder.kt:201-209 +``` + +`activation { it.relu() }`, `activation { it.gelu() }`, `activation { it.sigmoid() }`, `activation { it.silu() }` are the typical forms (extensions in `sk.ainet.lang.tensor.TensorExtensions.kt`); you can also call any `Tensor` extension you've defined. + +## Grouping — nested `sequential`, `stage` + +```kotlin +fun sequential(content: NeuralNetworkDsl.() -> Unit) // organizational grouping +fun stage(id: String, content: NeuralNetworkDsl.() -> Unit) // named stage/block +fun stage(id: String, content: NeuralNetworkDsl.() -> Unit): Module +// from: NetworkBuilder.kt:430-466 ``` -`activation { it.relu() }`, `activation { it.gelu() }`, `activation { it.sigmoid() }`, `activation { it.silu() }` are the typical forms; you can also call any `Tensor` extension you've defined. +The `` overload scopes a different precision to every layer inside the stage (mixed-precision blocks); the returned module handles the conversion. + +## Not in this DSL: LLM / transformer layers + +`embedding`, `rmsNorm`, `multiHeadAttention`, `swiGluFFN`, `residual` and `xielu` moved to the SKaiNET-transformers repository's llm-core module (`NetworkBuilder.kt:422-423`). The fused `scaledDotProductAttention` op (with native grouped-query attention) remains in the engine on `TensorOps` (`TensorOps.kt:389-397`). ## Layer scope blocks (`DENSE`, `CONV2D`, `MAXPOOL2D`, …) -Layer-scope content blocks expose configuration setters and weight initialisers. The exact members vary by layer; common ones: +Layer-scope content blocks expose configuration setters and weight initialisers (`NetworkBuilder.kt:460-596`). The exact members vary by layer; common ones: -- `inChannels = ...` (CONV*) -- `outChannels = ...` (CONV*, when using block-only form) -- `kernelSize(5)` (sets all dims to 5) -- `stride(2)` / `padding(0)` -- `weights { shape -> ... }` and `bias { shape -> ... }` for custom initialisation +- `inChannels = ...` / `outChannels = ...` (CONV*) +- `kernelSize(5)` (Int helper, sets all dims to 5) — CONV2D/CONV3D/MAXPOOL2D/AVGPOOL2D +- `stride(2)` / `padding(0)` (same Int helpers) +- `trainable = ...` (DENSE, CONV*, GRU) +- `weights { shape -> ... }` and `bias { shape -> ... }` for custom initialisation (any layer implementing `WandBTensorValueContext` — DENSE, CONV1D/2D/3D); the blocks run in `WeightsScope`/`BiasScope`, which extend the data DSL's `TensorCreationScope` (so `randn`, `uniform`, `zeros`, … work) +- DENSE additionally has `activation = ...` and `units = ...`; FLATTEN has `startDim` / `endDim` -Look at the layer's source file in `sk.ainet.lang.nn` for the full member list. +Look at the interface declarations in `NetworkBuilder.kt` for the full member list per layer. diff --git a/skainet-consumer-skills/skills/skainet-nn-dsl/references/sequential-vs-dag.md b/skainet-consumer-skills/skills/skainet-nn-dsl/references/sequential-vs-dag.md index 22a77a0..c552148 100644 --- a/skainet-consumer-skills/skills/skainet-nn-dsl/references/sequential-vs-dag.md +++ b/skainet-consumer-skills/skills/skainet-nn-dsl/references/sequential-vs-dag.md @@ -9,8 +9,9 @@ | Multiple inputs (image + metadata) | `dag { }` | | Multiple outputs (multi-task heads) | `dag { }` | | To call `.forward(x, ctx)` directly | `sequential` | -| To compile to C / HLO / ONNX | `dag` (compiler operates on `GraphProgram`) | +| To compile to C / StableHLO / JSON / Minerva (MCU) | `dag` (compiler operates on `GraphProgram`) | | To reuse a sub-graph | `dag` + `dagModule { }` | +| To stamp schedule hints (`schedule(parallel("heads")) { }`) or dtype policies on nodes | `dag` | ## Why these two builders, not one @@ -28,6 +29,7 @@ Triggers to migrate: - You start hacking around the linear-stack constraint (e.g. computing branch outputs outside the builder and zipping them back in). - You need shape information per node for compilation / quantisation / export. - You want to share a sub-graph between two models. +- You need per-node compile hints — schedule annotations (`schedule(parallel("heads")) { ... }`) or dtype policies (`op(..., dtypePolicy = ...)`) only exist on `dag { }` nodes. How to migrate: 1. Identify the input(s) and turn each into a `dag { val x = input<...>("x", spec) ... }`. diff --git a/skainet-contributor-skills/.claude-plugin/plugin.json b/skainet-contributor-skills/.claude-plugin/plugin.json index 05d014a..82de087 100644 --- a/skainet-contributor-skills/.claude-plugin/plugin.json +++ b/skainet-contributor-skills/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "skainet-contributor-skills", - "version": "0.1.0", + "version": "0.2.0", "description": "Skills for contributing TO the SKaiNET framework — Kotlin idioms enforced by the project, the multi-module Gradle layout with version catalog, KMP source-set rules, the Java-interop facade convention, and the common in-repo test strategy. Use when editing files inside the SKaiNET repo. Consumer-side skills (using SKaiNET in your own app) live in the sibling skainet-consumer-skills plugin.", "author": { "name": "SKaiNET-developers", diff --git a/skainet-contributor-skills/skills/INDEX.md b/skainet-contributor-skills/skills/INDEX.md index fd95bac..e116f96 100644 --- a/skainet-contributor-skills/skills/INDEX.md +++ b/skainet-contributor-skills/skills/INDEX.md @@ -1,6 +1,6 @@ # SKaiNET contributor skills — Index -Skills that fire ONLY when the agent is editing files **inside the SKaiNET repository** (`/home/miso/projects/SKills/SKaiNET/` or wherever the repo is cloned). Each skill enforces in-repo conventions: package layout, version-catalog discipline, KMP source-set placement, the Java-facade convention, and the in-repo test policy. +Skills that fire ONLY when the agent is editing files **inside the SKaiNET repository** (wherever the repo is cloned). Each skill enforces in-repo conventions: package layout, version-catalog discipline, KMP source-set placement, the Java-facade convention, and the in-repo test policy. If the user is writing application code that *consumes* SKaiNET as a library, none of these should fire — the sibling **skainet-consumer-skills** plugin covers that audience. The two plugins are designed to be installed together; the descriptions disambiguate via path heuristics so only one fires per prompt. @@ -22,7 +22,7 @@ gradle-multimodule → kmp → kotlin | [gradle-multimodule](gradle-multimodule/SKILL.md) | edits `SKaiNET/build.gradle.kts`, `SKaiNET/settings.gradle.kts`, `SKaiNET/gradle/libs.versions.toml`, `SKaiNET/build-logic/`, or adds a new `skainet-*` module within SKaiNET. | | [kmp](kmp/SKILL.md) | configures KMP targets or moves code between source-sets in a `SKaiNET/skainet-*/` module. | | [skainet-java-interop](skainet-java-interop/SKILL.md) | designs the Java-facing facade INSIDE SKaiNET — files under `SKaiNET/*/jvmMain/kotlin/sk/ainet/java/`. | -| [skainet-testing](skainet-testing/SKILL.md) | writes or edits tests INSIDE SKaiNET — Kotest specs in `jvmTest/`, `kotlin.test` in `commonTest/`, `TensorAssertions` + `ToleranceConfig`, JUnit 5 mirrors in `skainet-test-java`. | +| [skainet-testing](skainet-testing/SKILL.md) | writes or edits tests INSIDE SKaiNET — `kotlin.test` in `commonTest/` and `jvmTest/` (Kotest only in the `skainet-apps` CLI suites), `TensorAssertions` + `ToleranceConfig`, JUnit 5 mirrors in `skainet-test-java`. | ## Public DSL skills (consumer plugin) diff --git a/skainet-contributor-skills/skills/gradle-multimodule/SKILL.md b/skainet-contributor-skills/skills/gradle-multimodule/SKILL.md index 5ff0a0b..9017f05 100644 --- a/skainet-contributor-skills/skills/gradle-multimodule/SKILL.md +++ b/skainet-contributor-skills/skills/gradle-multimodule/SKILL.md @@ -25,26 +25,44 @@ Rules for editing the SKaiNET multi-module Gradle build. The project is a Gradle ## Hard rules 1. **Version-catalog-only.** Every dependency, plugin, and version reference in any `build.gradle.kts` MUST go through `libs.versions.`, `libs.`, or `libs.plugins.`. Hard-coded version strings (`"1.10.2"`, `version = "2.3.21"`) MUST NOT appear in module build scripts. -2. **Plugins via `alias(libs.plugins.)`.** Direct `id("...") version "..."` is reserved for two cases only: (a) the `sk.ainet.documentation` convention plugin (no version because it's local), (b) plugins in `build-logic/build.gradle.kts` itself. -3. **New module = three edits in one change.** When adding a `skainet-foo` module: (a) create `skainet-foo/build.gradle.kts`, (b) add `include("skainet-foo")` (or the nested form) to `settings.gradle.kts`, (c) register it in `skainet-bom/build.gradle.kts` if it ships as a published artifact. +2. **Plugins via `alias(libs.plugins.)`.** Direct `id("...")` without a version is the pattern for the in-tree convention plugins (`sk.ainet.dokka`, `sk.ainet.multiplatform`, `sk.ainet.documentation`, `sk.ainet.npm-pins`, `sk.ainet.maven-pins`, `sk.ainet.transformers.bom-coverage`) — local plugins have no version to track. `id("...") version "..."` with a literal version is reserved for `build-logic/convention/build.gradle.kts` itself. +3. **New published module = four edits in one change.** When adding a `skainet-foo` module: (a) create `skainet-foo/build.gradle.kts`, (b) add `include("skainet-foo")` (or the nested form) to `settings.gradle.kts`, (c) add the module's own `gradle.properties` with `POM_ARTIFACT_ID` and `POM_NAME` — without it the publication has no POM name and never reaches Maven Central (this bit `skainet-compile-minerva` once), (d) apply `alias(libs.plugins.vanniktech.mavenPublish)`. BOM membership is automatic: the `sk.ainet.transformers.bom-coverage` plugin on `skainet-bom` adds every sibling that applies the publish plugin as an `api` constraint; to keep a published module OUT of the BOM, add its path to `bomCoverage.excludePublished` in `skainet-bom/build.gradle.kts`. 4. **Dokka comes from the `sk.ainet.dokka` convention plugin.** Modules apply it via `id("sk.ainet.dokka")`, not by configuring Dokka directly. Convention plugins live in `build-logic/convention/src/main/kotlin/`. 5. **`binary-compatibility-validator` is applied on every published module.** Don't suppress it locally; if the API dump changes, regenerate via `./gradlew :module:apiDump` and commit the diff (see `kotlin` skill, api-stability reference). -6. **JVM target = 11 across the project.** Set via `compilerOptions { jvmTarget.set(JvmTarget.JVM_11) }` inside the `android { }` block and inherited elsewhere through KMP defaults. Do not set higher. +6. **JDK 21+, Java 21 bytecode on JVM, JVM 11 on Android.** The root `build.gradle.kts` requires JDK 21+, sets `JvmTarget.JVM_21` on every Kotlin JVM target and `options.release = 21` for Java sources — do not re-declare that per module. Android compilations are the exception: they target `JvmTarget.JVM_11` (set in the `android { }` block, or via `skainet { androidJvmTarget }` under the `sk.ainet.multiplatform` plugin; the `skainet-io` family explicitly ships `JVM_1_8`). 7. **No transitive accidents.** Use `api(...)` only when the dependency's types appear in a module's public Kotlin signatures. Default to `implementation(...)`. KSP-generated source goes through `add("kspCommonMainMetadata", project(":skainet-...:...-ksp-processor"))`. ## Workflow — adding a new module 1. Decide the area: pick the right top-level group (`skainet-lang`, `skainet-data`, `skainet-io`, `skainet-backends`, `skainet-compile`, `skainet-models`, `skainet-pipeline`, `skainet-apps`, `skainet-test`). -2. Create the directory and a minimal `build.gradle.kts` that mirrors the closest sibling. Apply the same plugin set (`kotlinMultiplatform`, `vanniktech.mavenPublish`, `binary.compatibility.validator`, `sk.ainet.dokka`, optionally `ksp`, `kotlinx-benchmark`). +2. Create the directory and a minimal `build.gradle.kts` that mirrors the closest sibling. Prefer the convention-plugin set (`id("sk.ainet.multiplatform")`, `androidMultiplatformLibrary` for Android, `vanniktech.mavenPublish`, `binary.compatibility.validator`, `sk.ainet.dokka`, optionally `ksp`, `kotlinx-benchmark`) plus a `skainet { namespace = "..." }` block. 3. Add `include("skainet-:")` to `settings.gradle.kts` in the matching `// ====== ` section. -4. Add the catalog entry if the module exposes a new external dependency (rare). -5. If the module is published, register it in `skainet-bom/build.gradle.kts`. +4. Add the module's `gradle.properties` with `POM_ARTIFACT_ID` / `POM_NAME` (published modules), and `skainet.targets=` only when diverging from the default target set. +5. Add the catalog entry if the module exposes a new external dependency (rare). The BOM picks the module up automatically once `vanniktech.mavenPublish` is applied. 6. Run `./gradlew :skainet-::assemble` to validate the wiring. 7. If a public API was introduced, run `./gradlew :skainet-::apiDump` and commit the dump. ## Canonical examples -**Module `build.gradle.kts` — KMP library with KSP and Dokka:** +**Module `build.gradle.kts` — convention-plugin form (preferred for new modules):** + +```kotlin +plugins { + id("sk.ainet.multiplatform") + alias(libs.plugins.androidMultiplatformLibrary) + alias(libs.plugins.vanniktech.mavenPublish) + alias(libs.plugins.binary.compatibility.validator) + id("sk.ainet.dokka") +} + +// Targets, explicitApi() and kotlin-test in commonTest come from sk.ainet.multiplatform. +skainet { + namespace = "sk.ainet.pipeline" +} +// from: SKaiNET/skainet-pipeline/build.gradle.kts:1-15 +``` + +**Module `build.gradle.kts` — hand-rolled KMP library with KSP and Dokka (older modules):** ```kotlin plugins { @@ -76,19 +94,19 @@ kotlin { } } } -// from: SKaiNET/skainet-lang/skainet-lang-core/build.gradle.kts:4-69 +// from: SKaiNET/skainet-lang/skainet-lang-core/build.gradle.kts:4-82 ``` **Catalog entries — every coordinate routed through `libs.versions.toml`:** ```toml [versions] -kotlin = "2.3.21" -ksp = "2.3.6" -dokka = "2.1.0" -kotest = "6.1.11" -kover = "0.9.8" -binaryCompatibilityValidator = "0.18.1" +kotlin = "2.4.20" +ksp = "2.3.10" +dokka = "2.2.0" +kotest = "6.2.5" +kover = "0.9.9" +binaryCompatibilityValidator = "0.18.2" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } @@ -99,7 +117,10 @@ kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } skainet-docs = { id = "sk.ainet.documentation" } -// from: SKaiNET/gradle/libs.versions.toml:1-95 +skainet-multiplatform = { id = "sk.ainet.multiplatform" } +skainet-npmPins = { id = "sk.ainet.npm-pins" } +// excerpt — values drift; SKaiNET/gradle/libs.versions.toml is the source of truth +// (it also holds the npm security pins forced into the kotlin-js-store lockfiles) ``` **`settings.gradle.kts` — module registration, grouped by area:** @@ -116,6 +137,7 @@ include("skainet-lang:skainet-lang-dag") // ====== DATA include("skainet-data:skainet-data-api") +include("skainet-data:skainet-data-source") include("skainet-data:skainet-data-transform") include("skainet-data:skainet-data-simple") include("skainet-data:skainet-data-media") @@ -123,7 +145,8 @@ include("skainet-data:skainet-data-media") // ====== TEST include("skainet-test:skainet-test-groundtruth") include("skainet-test:skainet-test-java") -// from: SKaiNET/settings.gradle.kts:21-73 +// from: SKaiNET/settings.gradle.kts:21-78 (abridged — the full file also includes the +// COMPILE, BACKENDS, BENCHMARKS, PIPELINE, IO, models, BOM, APPS and DOCS sections) ``` ## Related skills @@ -136,7 +159,7 @@ include("skainet-test:skainet-test-java") ```kotlin // WRONG — hard-coded coordinate / version -implementation("io.kotest:kotest-runner-junit5:6.1.11") +implementation("io.kotest:kotest-runner-junit5:6.2.5") ``` ```kotlin // RIGHT — catalog reference @@ -145,7 +168,7 @@ implementation(libs.kotest.runner.junit5) ```kotlin // WRONG — applying Dokka by id -plugins { id("org.jetbrains.dokka") version "2.1.0" } +plugins { id("org.jetbrains.dokka") version "2.2.0" } ``` ```kotlin // RIGHT — apply the convention plugin (it configures Dokka) @@ -157,11 +180,12 @@ plugins { id("sk.ainet.dokka") } // (skainet-io/skainet-io-newformat/build.gradle.kts created, but settings.gradle.kts unchanged) ``` ```kotlin -// RIGHT — also edit settings.gradle.kts AND skainet-bom in the same change +// RIGHT — also edit settings.gradle.kts AND add the module's gradle.properties +// (POM_ARTIFACT_ID / POM_NAME) in the same change; the BOM picks it up automatically include("skainet-io:skainet-io-newformat") // in settings.gradle.kts ``` ## References - [`references/catalog-aliases.md`](references/catalog-aliases.md) — version, library, and plugin aliases pulled from `libs.versions.toml`. -- [`references/convention-plugins.md`](references/convention-plugins.md) — what `sk.ainet.documentation` / `sk.ainet.dokka` does and how to add a new convention plugin. +- [`references/convention-plugins.md`](references/convention-plugins.md) — the full `sk.ainet.*` convention-plugin set (`multiplatform`, `dokka`, `documentation`, `npm-pins`, `maven-pins`, `bom-coverage`) and how to add a new one. diff --git a/skainet-contributor-skills/skills/gradle-multimodule/evals/evals.json b/skainet-contributor-skills/skills/gradle-multimodule/evals/evals.json index c8b57e3..3b999ab 100644 --- a/skainet-contributor-skills/skills/gradle-multimodule/evals/evals.json +++ b/skainet-contributor-skills/skills/gradle-multimodule/evals/evals.json @@ -7,12 +7,13 @@ "prompt": "Add a new published module `skainet-io:skainet-io-newformat` that depends on `skainet-io:skainet-io-core` and uses kotlinx-io for stream handling. Provide the full set of edits.", "must_contain": [ "include(\"skainet-io:skainet-io-newformat\")", - "alias(libs.plugins.kotlinMultiplatform)", + "id(\"sk.ainet.multiplatform\")", "alias(libs.plugins.vanniktech.mavenPublish)", "alias(libs.plugins.binary.compatibility.validator)", "id(\"sk.ainet.dokka\")", "implementation(libs.kotlinx.io.core)", - "api(project(\":skainet-io:skainet-io-core\"))" + "api(project(\":skainet-io:skainet-io-core\"))", + "POM_ARTIFACT_ID" ], "must_not_contain": [ "version = \"3.4.3\"", @@ -20,32 +21,35 @@ "implementation(\"org.jetbrains.kotlinx:kotlinx-io-core:0.9.0\")", "id(\"org.jetbrains.dokka\") version" ], - "must_not_contain_reason": "Hard rule 1: every coordinate routes through the catalog; hard rule 4: Dokka is configured by sk.ainet.dokka, not applied directly." + "must_not_contain_reason": "Hard rule 1: every coordinate routes through the catalog; hard rule 4: Dokka is configured by sk.ainet.dokka, not applied directly. New modules get targets/explicitApi/kotlin-test from the sk.ainet.multiplatform convention plugin and need a per-module gradle.properties with POM_ARTIFACT_ID/POM_NAME." }, { "id": "add-test-deps-via-catalog", - "prompt": "Add Kotest as a JVM-test dependency to `skainet-data:skainet-data-transform`.", + "prompt": "Add Kotest property-testing dependencies to `skainet-apps:skainet-grayscale-cli`.", "must_contain": [ - "jvmTest.dependencies", - "implementation(libs.kotest.runner.junit5)", - "implementation(libs.kotest.assertions.core)" + "testImplementation(libs.kotest.runner.junit5)", + "testImplementation(libs.kotest.assertions.core)", + "testImplementation(libs.kotest.property)" ], "must_not_contain": [ "io.kotest:kotest-runner-junit5:", "version.ref" ], - "must_not_contain_reason": "Catalog entries already encode versions; module scripts only refer to aliases." + "must_not_contain_reason": "Catalog entries already encode versions; module scripts only refer to aliases. Kotest lives only in the skainet-apps CLI tests — library modules use kotlin.test." }, { "id": "register-published-module-in-bom", "prompt": "I just added skainet-io:skainet-io-newformat. What else changes for it to be a properly published artifact?", "must_contain": [ - "skainet-bom", + "POM_ARTIFACT_ID", "binary-compatibility-validator", "apiDump", "vanniktech" ], - "must_not_contain_reason": null + "must_not_contain": [ + "skainet-bom/build.gradle.kts" + ], + "must_not_contain_reason": "BOM membership is automatic: the sk.ainet.transformers.bom-coverage convention plugin constrains every module that applies the publish plugin — hand-editing skainet-bom is the pre-0.57.0 workflow." }, { "id": "wrong-jvm-target-rejected", @@ -56,7 +60,7 @@ "must_not_contain": [ "JvmTarget.JVM_17" ], - "must_not_contain_reason": "Hard rule 6: JVM target is fixed at 11 across the project." + "must_not_contain_reason": "Android compilations of KMP modules stay at JVM 11 (JVM targets build Java 21 bytecode, set centrally in the root build); per-module deviation only via skainet { androidJvmTarget }." } ] } diff --git a/skainet-contributor-skills/skills/gradle-multimodule/references/catalog-aliases.md b/skainet-contributor-skills/skills/gradle-multimodule/references/catalog-aliases.md index 05f2d6d..ffa36fc 100644 --- a/skainet-contributor-skills/skills/gradle-multimodule/references/catalog-aliases.md +++ b/skainet-contributor-skills/skills/gradle-multimodule/references/catalog-aliases.md @@ -1,32 +1,39 @@ # Version catalog aliases -Authoritative source: `SKaiNET/gradle/libs.versions.toml`. If anything below disagrees, the source wins — update this file. +Authoritative source: `SKaiNET/gradle/libs.versions.toml`. **Always read the toml for the +current pin** — the values drift every release; the alias names below are the durable part. +Snapshot values noted here were verified against 0.57.0 (Kotlin 2.4.20, AGP 9.4.1, +Ktor 3.6.0, kotest 6.2.5, KSP 2.3.10, Dokka 2.2.0, kotlinx-benchmark 0.5.0, +binary-compatibility-validator 0.18.2, kover 0.9.9). ## Versions -| Alias | Value | Used for | -|---|---|---| -| `kotlin` | 2.3.21 | Kotlin compiler, kotlin-test | -| `ksp` | 2.3.6 | KSP processor + API | -| `dokka` | 2.1.0 | API docs | -| `agp` | 9.2.0 | Android multiplatform library plugin | -| `android-minSdk` | 24 | Android `minSdk` | -| `android-compileSdk` | 36 | Android `compileSdk` | -| `kotlinxCoroutines` | 1.10.2 | Coroutines runtime + test | -| `kotlinxSerializationJson` | 1.11.0 | JSON serialization | -| `kotlinxIo` | 0.9.0 | kotlinx-io-core | -| `kotlinxBenchmark` | 0.4.16 | JVM benchmarks | -| `kotlinxCli` | 0.3.6 | CLI argument parsing (apps) | -| `kotlinpoet` | 2.3.0 | KSP code generation | -| `kctfork` | 0.12.1 | kotlin-compile-testing (KSP tests) | -| `ktorClientCore` | 3.4.3 | Ktor client (model loading from URL) | -| `kotest` | 6.1.11 | Kotest spec runner + assertions | -| `junit` | 4.13.2 | (legacy) JUnit 4 | -| `junitJupiter` | 6.0.3 | JUnit 5 (skainet-test-java) | -| `kover` | 0.9.8 | Coverage | -| `binaryCompatibilityValidator` | 0.18.1 | API surface tracking | -| `pbandk` | 0.16.0 | Protobuf for ONNX | -| `logbackClassic` | 1.5.32 | JVM logging | +| Alias | Used for | +|---|---| +| `kotlin` | Kotlin compiler, kotlin-test, KGP (2.4.20 as of 0.57.0) | +| `ksp` | KSP processor + API | +| `dokka` | API docs (Dokka 2.x) | +| `agp` | Android Gradle plugin / android multiplatform library plugin (9.4.1 as of 0.57.0) | +| `android-minSdk` / `android-compileSdk` | Android `minSdk` (24) / `compileSdk` (36) | +| `android-ndk` | Pinned NDK for `skainet-backend-jni-cpu`'s externalNativeBuild (r28+: 16 KB-page-aligned .so's) | +| `kotlinxCoroutines` | Coroutines runtime + test | +| `kotlinxSerializationJson` | JSON serialization | +| `kotlinxIo` | kotlinx-io-core | +| `kotlinxBenchmark` | JVM benchmarks | +| `kotlinxCli` | CLI argument parsing (apps) | +| `kotlinBrowser` | kotlinx-browser (wasm/js) | +| `kotlinpoet` | KSP code generation | +| `kctfork` | kotlin-compile-testing (KSP tests) | +| `ktorClientCore` / `ktorClientPlugins` | Ktor client (model loading from URL) | +| `kotest` | Kotest runner + assertions + property (used in `skainet-apps` test suites) | +| `junit` | (legacy) JUnit 4 | +| `junitJupiter` | JUnit 5 (skainet-test-java) | +| `kover` | Coverage | +| `binaryCompatibilityValidator` | API surface tracking | +| `pbandk` | Protobuf for ONNX | +| `logbackClassic` | JVM logging | +| `jacksonDatabind`, `jsonSchemaValidator`, `jsonSchemaValidatorVersion` | JSON schema validation (build-logic / docs pipeline) | +| `npm-*` | npm security pins forced into the `kotlin-js-store` lockfiles via the `sk.ainet.npm-pins` root plugin (`skainet { npmPins { pin(...) } }`); never edit `kotlin-js-store/**/yarn.lock` by hand | ## Libraries (most-used aliases) @@ -42,13 +49,16 @@ Authoritative source: `SKaiNET/gradle/libs.versions.toml`. If anything below dis | `libs.kotlinpoet` | `com.squareup:kotlinpoet` | | `libs.kotlinpoet.ksp` | `com.squareup:kotlinpoet-ksp` | | `libs.ksp.api` | `com.google.devtools.ksp:symbol-processing-api` | +| `libs.kotlin.compile.testing` / `.ksp` | `dev.zacsweers.kctfork:core` / `:ksp` | | `libs.kotest.runner.junit5` | `io.kotest:kotest-runner-junit5` | | `libs.kotest.assertions.core` | `io.kotest:kotest-assertions-core` | | `libs.kotest.property` | `io.kotest:kotest-property` | | `libs.junit.jupiter` | `org.junit.jupiter:junit-jupiter` | -| `libs.ktor.client.core` | `io.ktor:ktor-client-core` | +| `libs.junit.platform.launcher` | `org.junit.platform:junit-platform-launcher` | +| `libs.ktor.client.core` (+ `.cio`, `.darwin`, `.js`, `.android`, …) | `io.ktor:ktor-client-*` | | `libs.pbandk.runtime` | `pro.streem.pbandk:pbandk-runtime` | | `libs.logback.classic` | `ch.qos.logback:logback-classic` | +| `libs.kotlin.gradlePlugin` / `libs.android.gradlePlugin` | KGP / AGP types — `compileOnly` in build-logic only | ## Plugins @@ -65,6 +75,9 @@ Authoritative source: `SKaiNET/gradle/libs.versions.toml`. If anything below dis | `libs.plugins.ksp` | `com.google.devtools.ksp` | | `libs.plugins.dokka` | `org.jetbrains.dokka` | | `libs.plugins.skainet-docs` | `sk.ainet.documentation` (local convention plugin) | +| `libs.plugins.skainet-multiplatform` | `sk.ainet.multiplatform` (local convention plugin) | +| `libs.plugins.skainet-npmPins` | `sk.ainet.npm-pins` (local, root project only) | +| `libs.plugins.skainet-mavenPins` | `sk.ainet.maven-pins` (local, root project only) | | `libs.plugins.kotlinx-benchmark` | `org.jetbrains.kotlinx.benchmark` | | `libs.plugins.shadow` | `com.gradleup.shadow` | | `libs.plugins.asciidoctorJvm` | `org.asciidoctor.jvm.convert` | @@ -74,5 +87,5 @@ Authoritative source: `SKaiNET/gradle/libs.versions.toml`. If anything below dis 1. Add a `versions` line if the dependency has a version not yet pinned. 2. Add a `libraries` entry referencing the version: `module = ":", version.ref = ""`. -3. For plugins: add a `plugins` entry. Plugins applied to `build-logic` itself need to be re-declared in `build-logic/settings.gradle.kts` because the composite-build has its own catalog access. +3. For plugins: add a `plugins` entry. `build-logic` shares the same toml (its `settings.gradle.kts` loads it via `from(files("../gradle/libs.versions.toml"))`); only the plugins applied to `build-logic/convention/build.gradle.kts` itself (`kotlin("jvm")`, `kotlin("plugin.serialization")`) carry inline versions there. 4. Reference from a module's `build.gradle.kts` via `libs.foo.bar` (dashes → dots). Do not paste the coordinate. diff --git a/skainet-contributor-skills/skills/gradle-multimodule/references/convention-plugins.md b/skainet-contributor-skills/skills/gradle-multimodule/references/convention-plugins.md index 0e29d89..566f3c8 100644 --- a/skainet-contributor-skills/skills/gradle-multimodule/references/convention-plugins.md +++ b/skainet-contributor-skills/skills/gradle-multimodule/references/convention-plugins.md @@ -1,40 +1,60 @@ # Convention plugins (`build-logic/convention`) -`build-logic` is included via `includeBuild("build-logic")` in `SKaiNET/settings.gradle.kts`. It exposes Gradle convention plugins that every module can apply by id. Convention plugins are the project's mechanism for sharing build configuration without copy-paste. +`build-logic` is included via `includeBuild("build-logic")` in `SKaiNET/settings.gradle.kts`. It exposes Gradle convention plugins that every module can apply by id. Convention plugins are the project's mechanism for sharing build configuration without copy-paste. Registration lives in `build-logic/convention/build.gradle.kts` (`gradlePlugin { plugins { register(...) } }`); `sk.ainet.dokka` is the exception — it is a precompiled script plugin (`build-logic/convention/src/main/kotlin/sk.ainet.dokka.gradle.kts`). ## Currently published -### `sk.ainet.documentation` (a.k.a. `sk.ainet.dokka`) +### `sk.ainet.multiplatform` (`SkainetMultiplatformPlugin`) -Configures Dokka 2.x consistently across modules: +SKaiNET's standard KMP module setup — replaces the hand-copied target list, `android { }` block, `explicitApi()`, `kotlin-test` wiring and Karma hardening: -- Hides native and cinterop source sets from generated documentation. -- Restricts visibility to `public` declarations only. -- Suppresses inherited members (otherwise every `public class` would document every method on `Any`). +- Creates the default target set (`jvm`, `js`, `wasmJs`, `wasmWasi`, `apple`, `linux`); a module diverges via the `skainet.targets` property in its own `gradle.properties` (`androidNative` opt-in, `none` = declare targets yourself). +- Contributes the `skainet { }` extension: `namespace` (required with the AGP KMP plugin), `androidJvmTarget` (default `JVM_11`; `skainet-io` family uses `JVM_1_8`), `explicitApi` (default true), `expectActualClasses`, `kotlinTestInCommonTest` (default true). +- Fills in `compileSdk`/`minSdk` from the catalog for the Android target. +- Fails js/wasmJs modules loudly when the root project doesn't apply `sk.ainet.npm-pins`. -Module application: ```kotlin plugins { - id("sk.ainet.dokka") + id("sk.ainet.multiplatform") + alias(libs.plugins.androidMultiplatformLibrary) // opt into Android } +skainet { namespace = "sk.ainet.pipeline" } ``` -A module SHOULD apply this plugin if it has a public API. Apps under `skainet-apps/` and the test infrastructure (`skainet-test-*`) typically don't. +### `sk.ainet.dokka` (precompiled script plugin) + +Configures Dokka 2.x consistently across modules: + +- Restricts visibility to `public` declarations, suppresses inherited members and generated files. +- Suppresses native source sets that use cinterop, and the `androidMain` source set of modules with a shared `src/jvmAndroidMain` directory (the shared API is documented once, via jvm). +- Wires source links to GitHub. + +A module SHOULD apply `id("sk.ainet.dokka")` if it has a public API. Apps under `skainet-apps/` and the test infrastructure (`skainet-test-*`) typically don't. Remember to also add the module to the root `dokka(project(...))` aggregation block. + +### `sk.ainet.documentation` (`DocumentationPlugin`) + +Applied on the root project (`alias(libs.plugins.skainet.docs)`). Registers the docs pipeline tasks: `generateDocs` (operator docs from KSP-emitted `operators.json` into Antora), `generateKernelMatrix` (kernel × platform support matrix from `kernel-support.json`), and `validateOperatorSchema`. Configured through the root `documentation { }` extension. + +### `sk.ainet.transformers.bom-coverage` (`BomCoveragePlugin`) + +Applied by `skainet-bom`. Populates the BOM's constraints automatically: every sibling subproject that applies `com.vanniktech.maven.publish` becomes an `api` constraint, sorted by Gradle path. Opt a published module out via `bomCoverage.excludePublished`. + +### `sk.ainet.npm-pins` (`NpmPinsPlugin`) and `sk.ainet.maven-pins` (`MavenPinsPlugin`) -The plugin is registered in `build-logic/convention/build.gradle.kts` with `implementationClass = "DocumentationPlugin"`. +Root-project-only security plugins. `npm-pins` forces catalog-declared npm versions into both `kotlin-js-store` yarn lockfiles via Yarn resolutions (`skainet { npmPins { pin("ws", libs.versions.npm.ws) } }`, optionally scoped with `NpmPinTarget.JS`) and adds `verifyNpmPins`; regenerate lockfiles with `./gradlew kotlinUpgradeYarnLock kotlinWasmUpgradeYarnLock`, never by hand. `maven-pins` does the same for transitive Maven coordinates through `resolutionStrategy`, verified by `verifyMavenPins` on `check`. ## Adding a new convention plugin When the same Gradle configuration block is being copied into 3+ modules, lift it into a convention plugin. -1. Create `build-logic/convention/src/main/kotlin/sk/ainet/buildlogic/Plugin.kt` with a class implementing `org.gradle.api.Plugin`. +1. Create `build-logic/convention/src/main/kotlin/sk/ainet/buildlogic//Plugin.kt` with a class implementing `org.gradle.api.Plugin` (or a `sk.ainet..gradle.kts` precompiled script for simple cases). 2. Register it in `build-logic/convention/build.gradle.kts`: ```kotlin gradlePlugin { plugins { - register("skainet") { + register("SKaiNet") { id = "sk.ainet." - implementationClass = "sk.ainet.buildlogic.Plugin" + implementationClass = "sk.ainet.buildlogic..Plugin" } } } @@ -46,15 +66,12 @@ When the same Gradle configuration block is being copied into 3+ modules, lift i ``` 4. Apply from a module: ```kotlin - plugins { alias(libs.plugins.skainet-) } + plugins { alias(libs.plugins.skainet.) } ``` or `id("sk.ainet.")` if you don't want to go through the catalog (acceptable for in-tree convention plugins because there's no version to track). -## Why convention plugins, not subproject `subprojects { }` blocks +Note: build-logic needs KGP/AGP types? Add them as `compileOnly` (`libs.kotlin.gradlePlugin` / `libs.android.gradlePlugin`), never `implementation` — a runtime copy would load a second KGP/AGP in a different classloader and break `getByType(...)` with ClassCastExceptions. -- Convention plugins are opt-in per module. Modules choose what they need. -- They surface in IDE/Gradle introspection as actual plugins, with their own KDoc and lifecycle. -- They version-control as Kotlin code, not Groovy DSL strings. -- They can be unit-tested with `kotlin-compile-testing` if needed. +## Convention plugins vs root `subprojects { }` -`subprojects { }` in the root `build.gradle.kts` is forbidden in this project — every cross-cutting concern goes through a convention plugin or a catalog alias. +Cross-cutting *opt-in* concerns go through convention plugins: modules choose what they need, the configuration is versioned Kotlin code, and it surfaces in IDE/Gradle introspection as real plugins. The root `build.gradle.kts` keeps a small `subprojects { }` block for the truly unconditional project-wide settings (JDK 21 floor, `JvmTarget.JVM_21` on JVM targets, Java `--release 21`, test heap, kover) — don't grow it; anything a module could reasonably opt out of belongs in a convention plugin or a catalog alias. diff --git a/skainet-contributor-skills/skills/kmp/SKILL.md b/skainet-contributor-skills/skills/kmp/SKILL.md index 5a38bc5..e93790c 100644 --- a/skainet-contributor-skills/skills/kmp/SKILL.md +++ b/skainet-contributor-skills/skills/kmp/SKILL.md @@ -24,7 +24,7 @@ Rules for configuring Kotlin Multiplatform targets and source-set hierarchies in ## Hard rules -1. **Canonical target list for a published library** (mirror `skainet-lang-core`'s set unless there's a documented reason not to): +1. **New modules get their targets from the `sk.ainet.multiplatform` convention plugin, not a hand-written list.** Apply `id("sk.ainet.multiplatform")` (plus `alias(libs.plugins.androidMultiplatformLibrary)` to opt into Android) and declare `skainet { namespace = "sk.ainet." }`. The default target set is `jvm`, `js { browser() }`, `wasmJs { browser() }`, `wasmWasi { nodejs() }`, `apple` (`iosArm64`, `iosSimulatorArm64`, `macosArm64`) and `linux` (`linuxX64`, `linuxArm64`); a module that differs sets the `skainet.targets` Gradle property in its own `gradle.properties` (comma-separated list; `androidNative` adds `androidNativeArm32`/`androidNativeArm64` for vendor-native backends; `none` means the module declares every target itself). The plugin also applies `explicitApi()` and wires `kotlin-test` into `commonTest` (both switchable in `skainet { }`). Older modules such as `skainet-lang-core` still write the same canonical list by hand: - `jvm()` - `android { namespace = "sk.ainet."; compileSdk = libs.versions.android.compileSdk.get().toInt(); minSdk = libs.versions.android.minSdk.get().toInt(); compilerOptions { jvmTarget.set(JvmTarget.JVM_11) } }` - `iosArm64()`, `iosSimulatorArm64()` @@ -35,26 +35,46 @@ Rules for configuring Kotlin Multiplatform targets and source-set hierarchies in - `@OptIn(ExperimentalWasmDsl::class) wasmJs { browser() }`, `@OptIn(ExperimentalWasmDsl::class) wasmWasi { nodejs() }` 2. **`expect` lives in `commonMain`. `actual` lives in the narrowest source set that needs the platform API** — `jvmMain` for `java.io`, `iosArm64Main` for an iOS-specific call, etc. 3. **`commonMain` is the default home for Kotlin code.** Move a file out only when it imports a platform API that `commonMain` cannot resolve. -4. **`commonTest` uses `kotlin.test`. `jvmTest` may add Kotest.** Kotest's runner is JVM-only — never import it from a `commonTest` source set. +4. **`kotlin.test` everywhere — `commonTest` AND `jvmTest` in library modules.** Kotest's runner is JVM-only, so never import it from a `commonTest` source set; in practice the library modules' `jvmTest` sets use `kotlin.test` too, and Kotest (runner + assertions + property) appears only in the `skainet-apps/skainet-grayscale-cli` JVM test suite. 5. **Source-set dependencies use the per-set DSL** (`commonMain.dependencies { }`, `jvmMain.dependencies { }`), or the `sourceSets { commonMain { dependencies { ... } } }` form. Don't add dependencies to the bare `dependencies { }` block at the top level of a KMP module — that's a JVM-only Gradle pattern. 6. **`api(...)` only when the dependency's types appear in the public Kotlin signatures of the consuming source set.** Otherwise `implementation(...)`. The KSP annotations module is a deliberate `api(...)` because KSP-generated code references those annotations from `commonMain`. -7. **KSP-generated sources for `commonMain` are added explicitly** to `commonMain.kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin")`. The `tasks.configureEach { … dependsOn("kspCommonMainKotlinMetadata") }` block at the bottom of the build script is required — copy it verbatim from `skainet-lang-core/build.gradle.kts:72-77` when KSP is involved. -8. **Don't introduce target-specific intermediate source sets without a reason.** Use the default hierarchy template (`iosMain` aggregating `iosArm64Main` + `iosSimulatorArm64Main`; `nativeMain` aggregating all native targets) only when at least two siblings actually share code. +7. **KSP-generated sources for `commonMain` are added explicitly** to `commonMain.kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin")`. The `tasks.configureEach { … dependsOn("kspCommonMainKotlinMetadata") }` block at the bottom of the build script is required — copy it verbatim from `skainet-lang-core/build.gradle.kts:85-90` when KSP is involved. +8. **Don't introduce target-specific intermediate source sets without a reason.** Use the default hierarchy template (`iosMain` aggregating `iosArm64Main` + `iosSimulatorArm64Main`; `nativeMain` aggregating all native targets) only when at least two siblings actually share code. For code shared between the JVM and Android compilations only (e.g. `java.nio` mmap storage), the project pattern is a plain shared source *directory* — `src/jvmAndroidMain/kotlin` added via `kotlin.srcDir(...)` to both `jvmMain` and `androidMain` — not an intermediate source set, so each compilation keeps its full platform classpath (see `skainet-lang-core/build.gradle.kts:71-76`, #921). ## Workflow -1. Open the closest sibling module's `build.gradle.kts` and copy its target list — divergence from the canonical set MUST be justified in the change description. +1. For a new module, apply `id("sk.ainet.multiplatform")` and only declare `skainet.targets` in the module's `gradle.properties` if it must diverge from the default set (divergence MUST be justified in the change description). When editing an older hand-rolled module, keep its existing target list in sync with the canonical set. 2. Decide the source set for the new code: - Pure Kotlin, no platform API → `commonMain`. - Platform API needed → narrowest source set (`jvmMain`, `iosArm64Main`, `wasmJsMain`). - Cross-platform behaviour with platform-specific implementation → `expect` in `commonMain`, `actual` in each platform source set that the module targets. 3. Wire dependencies in the matching source-set block (`commonMain.dependencies { }`, etc.). -4. Add tests in the matching test source set (`commonTest` for cross-target, `jvmTest` for JVM-only tooling like Kotest). +4. Add tests in the matching test source set (`commonTest` for cross-target, `jvmTest` for JVM-only fixtures) — `kotlin.test` in both. 5. Run `./gradlew :module:assemble` to validate every target compiles. If a Native target fails, the source set probably leaked a JVM API — move it to `jvmMain`. ## Canonical examples -**Full target list with explicit-API mode:** +**Convention-plugin module (preferred for new modules):** + +```kotlin +plugins { + id("sk.ainet.multiplatform") + alias(libs.plugins.androidMultiplatformLibrary) // opt into Android + alias(libs.plugins.vanniktech.mavenPublish) + alias(libs.plugins.binary.compatibility.validator) + id("sk.ainet.dokka") +} + +// Default target set (jvm, js, wasmJs, wasmWasi, apple, linux + Android from the +// AGP plugin above), explicitApi() and kotlin-test in commonTest all come from +// sk.ainet.multiplatform. +skainet { + namespace = "sk.ainet.pipeline" +} +// from: SKaiNET/skainet-pipeline/build.gradle.kts:1-15 +``` + +**Full hand-rolled target list with explicit-API mode (older modules):** ```kotlin kotlin { @@ -116,7 +136,8 @@ sourceSets { implementation(libs.kotlin.test) } } -// from: SKaiNET/skainet-lang/skainet-lang-core/build.gradle.kts:52-68 +// from: SKaiNET/skainet-lang/skainet-lang-core/build.gradle.kts:52-81 (abridged — the +// full block also adds the shared src/jvmAndroidMain directory to jvmMain and androidMain) ``` **KSP wiring required when `commonMain` consumes generated code:** @@ -132,7 +153,7 @@ tasks.configureEach { dependencies { add("kspCommonMainMetadata", project(":skainet-lang:skainet-lang-ksp-processor")) } -// from: SKaiNET/skainet-lang/skainet-lang-core/build.gradle.kts:72-83 +// from: SKaiNET/skainet-lang/skainet-lang-core/build.gradle.kts:85-96 ``` ## Related skills @@ -175,11 +196,9 @@ public actual fun loadModelFile(path: String): ByteArray = java.io.File(path).re import io.kotest.core.spec.style.StringSpec // Kotest runner is JVM-only ``` ```kotlin -// RIGHT — Kotest in jvmTest, kotlin.test in commonTest -// commonTest/.../FooTest.kt +// RIGHT — kotlin.test in commonTest and jvmTest alike +// commonTest/.../FooTest.kt (and jvmTest, in library modules) import kotlin.test.Test -// jvmTest/.../FooSpec.kt -import io.kotest.core.spec.style.StringSpec ``` ## References diff --git a/skainet-contributor-skills/skills/kmp/evals/evals.json b/skainet-contributor-skills/skills/kmp/evals/evals.json index bcd61ec..e480d2e 100644 --- a/skainet-contributor-skills/skills/kmp/evals/evals.json +++ b/skainet-contributor-skills/skills/kmp/evals/evals.json @@ -41,7 +41,7 @@ "io.kotest.core", "kotest-runner-junit5" ], - "must_not_contain_reason": "Hard rule 4: Kotest is JVM-only and will not compile against native targets from commonTest." + "must_not_contain_reason": "Hard rule 4: Kotest will not compile against native targets from commonTest — and as of 0.57.0 library modules do not use Kotest at all (kotlin.test in commonTest and jvmTest; Kotest only in the skainet-apps CLI tests)." }, { "id": "ksp-wiring-when-needed", diff --git a/skainet-contributor-skills/skills/kotlin/SKILL.md b/skainet-contributor-skills/skills/kotlin/SKILL.md index 54e1cda..db2b999 100644 --- a/skainet-contributor-skills/skills/kotlin/SKILL.md +++ b/skainet-contributor-skills/skills/kotlin/SKILL.md @@ -23,7 +23,7 @@ Idiomatic Kotlin coding rules for production code in SKaiNET. Covers package lay ## Hard rules -1. **`explicitApi()` is on for every `kotlin { }` block in this project.** Every public top-level declaration MUST carry an explicit visibility modifier (`public`, `internal`, `private`). Do not write `fun foo()` at top level — write `public fun foo()` or `internal fun foo()`. +1. **Explicit-API mode is on for every library module in this project.** Older modules call `explicitApi()` in their `kotlin { }` block; modules on the `sk.ainet.multiplatform` convention plugin get it from the plugin (its `skainet { explicitApi }` flag defaults to `true`). Either way, every public top-level declaration MUST carry an explicit visibility modifier (`public`, `internal`, `private`). Do not write `fun foo()` at top level — write `public fun foo()` or `internal fun foo()`. 2. **Package layout is `sk.ainet.[.]`.** New files MUST live under `src//kotlin/sk/ainet/...`. Do not introduce new top-level packages. 3. **No Java-style getters.** Expose Kotlin properties (`val`, `var`) — never `getFoo()` / `setFoo()` on a Kotlin class. Java consumers reach Kotlin through the dedicated facades in `sk/ainet/java/` (covered by `skainet-java-interop`). 4. **`value class` for any wrapper around a primitive that has semantic meaning** (e.g. tensor IDs, layer names, axis indices). Not for things that need equality on multiple fields — those are `data class`. @@ -32,6 +32,7 @@ Idiomatic Kotlin coding rules for production code in SKaiNET. Covers package lay 7. **Coroutines are structured.** Suspend functions belong on a `CoroutineScope` provided by the caller; never launch into `GlobalScope`. Hot streams use `Flow`; cold one-shot APIs use suspend functions. 8. **Public-API additions are gated by binary-compatibility-validator.** When the build fails because `*.api` changed, regenerate the dump (`./gradlew apiDump`) and own the change in the same commit — don't suppress the check. 9. **`@PublishedApi internal` is the only way to expose internals to inline functions.** Don't widen visibility just to satisfy `inline fun`. +10. **Changes to public API shape, DSL syntax/semantics, or tensor dtype/shape/storage/execution behavior need an SKEEP.** SKEEP (SKaiNET Evolution and Enhancement Process) is the project's KEEP-style design-record process: a proposal under `docs/modules/skeep/pages/NNN-short-title.adoc` plus a tracking issue, written before or alongside the implementation. See `CONTRIBUTING.md` for the triggers and procedure; additive features behind an existing interface are DARC features, not SKEEPs. ## Workflow @@ -71,14 +72,15 @@ public fun tensor( // from: SKaiNET/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/dsl/TensorDSL.kt:17-25 ``` -**Module-level KMP plugins (so explicit-API is enforced):** +**Module-level explicit-API enforcement (hand-rolled KMP modules):** ```kotlin kotlin { explicitApi() // ... targets ... } -// from: SKaiNET/skainet-lang/skainet-lang-core/build.gradle.kts:14-16 +// from: SKaiNET/skainet-lang/skainet-lang-core/build.gradle.kts:14-15 +// Modules using id("sk.ainet.multiplatform") get explicitApi() from the plugin instead. ``` ## Related skills diff --git a/skainet-contributor-skills/skills/skainet-java-interop/SKILL.md b/skainet-contributor-skills/skills/skainet-java-interop/SKILL.md index 86c9e86..6ac814c 100644 --- a/skainet-contributor-skills/skills/skainet-java-interop/SKILL.md +++ b/skainet-contributor-skills/skills/skainet-java-interop/SKILL.md @@ -24,9 +24,9 @@ Rules for making SKaiNET features usable from Java without surfacing Kotlin-spec ## Hard rules -1. **`@file:JvmName("...")` on every Java-callable file.** The file's first non-blank line MUST be `@file:JvmName("...")` followed by `package sk.ainet.java`. The JvmName matches what Java users will type as the class identifier (`SKaiNET`, `TensorJavaOps`, `JavaOps`). -2. **Container shape is `object`, never top-level functions.** A Java-callable surface MUST be `public object Name { ... }`. Never expose top-level `public fun foo(...)` to Java callers — Java sees them as static methods on a synthetic `Kt` class. -3. **Every member carries `@JvmStatic`.** Without it, Java users have to type `SKaiNET.INSTANCE.context()` instead of `SKaiNET.context()`. This rule applies to every public function and every property accessor on the Java-facing object. +1. **`@file:JvmName("...")` on every static-facade file.** For an `object`-shaped facade, the file's first non-blank line MUST be `@file:JvmName("...")` followed by `package sk.ainet.java`. The JvmName matches what Java users will type as the class identifier (`SKaiNET`, `TensorJavaOps`, `Losses`, `Optimizers`, `JavaOps`). A stateful builder that IS a plain `public class` whose Kotlin name is already the Java identifier (`SequentialModelBuilder`, `TrainingLoop`) needs no `@file:JvmName` — but its constructor takes `@JvmOverloads` when it has defaults. +2. **Stateless surface shape is `object`, never top-level functions.** A static Java-callable surface MUST be `public object Name { ... }`. Never expose top-level `public fun foo(...)` to Java callers — Java sees them as static methods on a synthetic `Kt` class. Stateful fluent builders are plain classes with chainable methods returning `this`. +3. **Every `object` member carries `@JvmStatic`.** Without it, Java users have to type `SKaiNET.INSTANCE.context()` instead of `SKaiNET.context()`. This rule applies to every public function and every property accessor on the Java-facing object. 4. **Default arguments require `@JvmOverloads`.** Java doesn't see Kotlin defaults; `@JvmOverloads` synthesises overloads with progressively fewer parameters from right to left. Apply it to every `@JvmStatic` function that has a default value. 5. **Forbidden in Java-facing signatures**: - `value class` types (Java sees the boxed form, defeats the purpose) @@ -43,11 +43,11 @@ Rules for making SKaiNET features usable from Java without surfacing Kotlin-spec ## Workflow -1. Find the closest sibling under `skainet-*/src/jvmMain/kotlin/sk/ainet/java/`. Mirror its `@file:JvmName`, package declaration, KDoc-with-Java-example header, and member shape. +1. Find the closest sibling under `skainet-*/src/jvmMain/kotlin/sk/ainet/java/`. The existing surface (as of 0.57.0): `SKaiNET` entry point in `skainet-backends/skainet-backend-cpu`, and `TensorJavaOps`, `Losses`, `Optimizers`, `SequentialModelBuilder`, `TrainingLoop` in `skainet-lang/skainet-lang-core`. Mirror the sibling's `@file:JvmName`, package declaration, KDoc-with-Java-example header, and member shape. 2. Define the new `object` (or extend an existing one). 3. For each member: `@JvmStatic` is mandatory; `@JvmOverloads` if any parameter has a default; primitive arrays for bulk data; `Tensor<*, *>` for tensors crossing the boundary. 4. Implement the member by casting the wildcard tensor to the internal generic form (`Tensor`) and delegating to the Kotlin API. -5. Write a JUnit 5 test under `skainet-test-java/src/test/java/sk/ainet/java/JavaOpsTest.java` proving the surface compiles and runs from Java. +5. Write a JUnit 5 test under `skainet-test-java/src/test/java/sk/ainet/java/JavaOpsTest.java` proving the surface compiles and runs from Java (existing mirrors: `SKaiNETTest`, `TensorJavaOpsTest`, `ModelBuilderTest`, `ReleaseApiJavaTest`). 6. Self-verify with the checklist below before reporting done. ## Self-verification checklist @@ -56,8 +56,8 @@ Before declaring the change complete: - [ ] File starts with `@file:JvmName("...")` (line 1). - [ ] File is in `package sk.ainet.java`. -- [ ] Container is a `public object`. -- [ ] Every public member has `@JvmStatic`. +- [ ] Container is a `public object` (or a plain `public class` if it is a stateful fluent builder). +- [ ] Every public member of an `object` facade has `@JvmStatic`. - [ ] Every member with a default has `@JvmOverloads`. - [ ] No `value class`, no `Result`, no `Sequence`, no receiver-typed lambdas in any signature. - [ ] Tensors cross the boundary as `Tensor<*, *>`. @@ -102,7 +102,7 @@ public object SKaiNET { return ctx.zeros(Shape(*shape), kclass) } } -// from: SKaiNET/skainet-backends/skainet-backend-cpu/src/jvmMain/kotlin/sk/ainet/java/SKaiNET.kt:1-102 +// from: SKaiNET/skainet-backends/skainet-backend-cpu/src/jvmMain/kotlin/sk/ainet/java/SKaiNET.kt:1-136 ``` **Ops facade — every member `@JvmStatic`, defaults via `@JvmOverloads`:** diff --git a/skainet-contributor-skills/skills/skainet-testing/SKILL.md b/skainet-contributor-skills/skills/skainet-testing/SKILL.md index 25d0d52..651ef82 100644 --- a/skainet-contributor-skills/skills/skainet-testing/SKILL.md +++ b/skainet-contributor-skills/skills/skainet-testing/SKILL.md @@ -1,12 +1,12 @@ --- name: skainet-testing -description: Use ONLY when writing or editing unit tests INSIDE the SKaiNET repository — tests under `SKaiNET/skainet-*/src/commonTest/`, `*/jvmTest/`, or `SKaiNET/skainet-test/skainet-test-java/`. Enforces the in-repo test policy: Kotest spec runner on JVM, `kotlin.test` in commonTest, `TensorAssertions.assertTensorClose` with explicit `ToleranceConfig.{STRICT/STANDARD/RELAXED/GRADIENT}`, Java JUnit 5 in `skainet-test-java`. Trigger tokens include `assertTensorClose`, `assertArrayClose`, `ToleranceConfig`, `GroundTruthTensor`. Do NOT fire on a CONSUMER project writing its own tests against SKaiNET as a dependency — those tests don't have access to `skainet-test-groundtruth` and can use any framework they like. +description: Use ONLY when writing or editing unit tests INSIDE the SKaiNET repository — tests under `SKaiNET/skainet-*/src/commonTest/`, `*/jvmTest/`, or `SKaiNET/skainet-test/skainet-test-java/`. Enforces the in-repo test policy: `kotlin.test` in commonTest and jvmTest (Kotest only in `skainet-apps` JVM suites), `TensorAssertions.assertTensorClose` with explicit `ToleranceConfig.{STRICT/STANDARD/RELAXED/GRADIENT}`, Java JUnit 5 in `skainet-test-java`. Trigger tokens include `assertTensorClose`, `assertArrayClose`, `ToleranceConfig`, `GroundTruthTensor`. Do NOT fire on a CONSUMER project writing its own tests against SKaiNET as a dependency — those tests don't have access to `skainet-test-groundtruth` and can use any framework they like. version: 0.1.0 --- # skainet-testing -Common unit-test strategy across every SKaiNET module: tolerance-aware tensor comparisons, Kotest as the JVM/common runner, JUnit 5 for the Java mirror, fixed source-set placement. +Common unit-test strategy across every SKaiNET module: tolerance-aware tensor comparisons, `kotlin.test` as the runner in commonTest and jvmTest, JUnit 5 for the Java mirror, fixed source-set placement. Kotest (6.2.5) survives only in the `skainet-apps` JVM test suites (property-based CLI tests). ## When to use @@ -26,27 +26,28 @@ Common unit-test strategy across every SKaiNET module: tolerance-aware tensor co 2. Every comparison MUST pass an explicit `atol` from `ToleranceConfig` (or call `ToleranceConfig.forOperation(name)`). Bare numeric literals like `1e-5f` MUST NOT appear at the call site. 3. Tolerance picker, no exceptions: `STRICT` (1e-6) for `add` / `subtract` / `multiply` / `divide` / `relu` / `flatten` / `reshape`; `STANDARD` (1e-5) for `matmul` / `conv1d` / `conv2d` / `conv3d` / `sum` / `mean` / `variance`; `RELAXED` (1e-4) for `sigmoid` / `gelu` / `silu` / `softmax` / `logSoftmax` / `leakyRelu`; `GRADIENT` (1e-4) for any backward / autograd assertion; `VERY_RELAXED` (1e-3) only when the operation has documented numerical instability. 4. Kotlin tests live in `commonTest/` whenever they don't reference JVM-only APIs; JVM-only tests live in `jvmTest/`. Java tests live in `skainet-test/skainet-test-java/src/test/java/sk/ainet/java/`. -5. Kotest is the spec runner for JVM-targeted Kotlin tests. Pure `commonTest` (multiplatform) uses `kotlin.test` because Kotest's runner is JVM-only — do not import Kotest from `commonTest` source sets. +5. `kotlin.test` is the runner for library-module Kotlin tests — in `commonTest` AND `jvmTest`. Kotest's runner is JVM-only and is used only in the `skainet-apps/skainet-grayscale-cli` JVM test suite (Kotest runner + assertions + property, via the catalog aliases); do not import Kotest from `commonTest` source sets, and do not introduce it into a library module's `jvmTest` without precedent in that module. 6. A new test that compares a SKaiNET tensor against expected values MUST express the expected as a `GroundTruthTensor` (or a `FloatArray` + `Shape`) and call `TensorAssertions.assertTensorClose(...)` / `assertArrayClose(...)`. Never call `tensor.getData().copyToFloatArray()` and then `assertEquals` element by element. ## Workflow 1. Decide the source set: - Pure logic (no JVM-only API)? → `commonTest/` with `kotlin.test`. - - JVM-only fixtures, file IO, or you want Kotest spec syntax? → `jvmTest/` with Kotest. + - JVM-only fixtures or file IO? → `jvmTest/`, still with `kotlin.test`. - Mirroring a Java consumer surface? → `skainet-test-java/src/test/java/...` with JUnit 5. + - Property-based CLI tests in `skainet-apps`? → `src/test/` with Kotest. 2. Pick the tolerance from rule 3 above by looking at the operation under test, not by trial-and-error. 3. Build the expected value once (literal `FloatArray` + `Shape`, or load a `GroundTruthTensor` fixture). 4. Call `TensorAssertions.assertTensorClose(expected, actual, atol = ToleranceConfig.STANDARD)` — pass `rtol` only if the comparison is dominated by relative error (rare; default `1e-5f` is fine for most cases). -5. For shape-only assertions, use `TensorAssertions.assertShapeEquals(expected, actual)`. For Kotest infix style, prefer `actual shouldBeCloseTo expected`. +5. For shape-only assertions, use `TensorAssertions.assertShapeEquals(expected, actual)`. For infix style (defined in `skainet-test-groundtruth` itself, works with any runner), prefer `actual shouldBeCloseTo expected`. 6. Self-verify before reporting done: every numeric literal in the test that isn't a value being asserted on is gone (no `1e-5f`, `0.001f`, etc. as tolerance arguments — they live in `ToleranceConfig`). ## Canonical examples -**Kotest StringSpec on JVM (preferred for new tests in JVM-only modules):** +**`kotlin.test` in `jvmTest` (JVM-only fixtures, e.g. against the eager CPU backend):** ```kotlin -import io.kotest.core.spec.style.StringSpec +import kotlin.test.Test import sk.ainet.context.DirectCpuExecutionContext import sk.ainet.lang.tensor.Shape import sk.ainet.lang.tensor.dsl.tensor @@ -55,8 +56,9 @@ import sk.ainet.test.groundtruth.GroundTruthTensor import sk.ainet.test.groundtruth.TensorAssertions import sk.ainet.test.groundtruth.ToleranceConfig -class MatmulSpec : StringSpec({ - "2x2 matmul matches expected within STANDARD tolerance" { +class MatmulTest { + @Test + fun `2x2 matmul matches expected within STANDARD tolerance`() { val ctx = DirectCpuExecutionContext.create() val a = tensor(ctx, FP32::class) { tensor { shape(2, 2) { from(1f, 2f, 3f, 4f) } } @@ -76,7 +78,7 @@ class MatmulSpec : StringSpec({ atol = ToleranceConfig.STANDARD ) } -}) +} ``` **Multiplatform `commonTest` (no Kotest — `kotlin.test` only):** @@ -134,7 +136,7 @@ The Java mirror uses raw `assertArrayEquals` with a numeric epsilon because Java ## Related skills -- Test wiring (`testImplementation` lines, Kotest plugin) — see [`../gradle-multimodule/SKILL.md`](../gradle-multimodule/SKILL.md). +- Test wiring (`testImplementation` lines, catalog aliases) — see [`../gradle-multimodule/SKILL.md`](../gradle-multimodule/SKILL.md). - Source-set rules for `commonTest` vs `jvmTest` placement — see [`../kmp/SKILL.md`](../kmp/SKILL.md). - Java facade convention being tested — see [`../skainet-java-interop/SKILL.md`](../skainet-java-interop/SKILL.md). - Building the tensors used as inputs in tests — see the `skainet-data-dsl` skill (in the sibling consumer plugin). @@ -155,7 +157,7 @@ TensorAssertions.assertArrayClose(expected, actualData, expShape, actShape, atol import io.kotest.core.spec.style.StringSpec // commonTest cannot run Kotest ``` ```kotlin -// RIGHT — kotlin.test for commonTest, Kotest only in jvmTest / src/test +// RIGHT — kotlin.test for commonTest and jvmTest; Kotest only in skainet-apps src/test import kotlin.test.Test ``` diff --git a/skainet-contributor-skills/skills/skainet-testing/evals/evals.json b/skainet-contributor-skills/skills/skainet-testing/evals/evals.json index 1f628d7..825474b 100644 --- a/skainet-contributor-skills/skills/skainet-testing/evals/evals.json +++ b/skainet-contributor-skills/skills/skainet-testing/evals/evals.json @@ -3,23 +3,24 @@ "skill": "skainet-testing", "scenarios": [ { - "id": "matmul-kotest-standard-tolerance", - "prompt": "Add a Kotest StringSpec test under skainet-lang-core/src/jvmTest/kotlin asserting that 2x3 @ 3x2 matmul of FP32 tensors equals the expected 2x2 result within STANDARD tolerance.", + "id": "matmul-kotlin-test-standard-tolerance", + "prompt": "Add a kotlin.test test under skainet-lang-core/src/jvmTest/kotlin asserting that 2x3 @ 3x2 matmul of FP32 tensors equals the expected 2x2 result within STANDARD tolerance.", "must_contain": [ - "import io.kotest.core.spec.style.StringSpec", + "import kotlin.test.Test", "import sk.ainet.test.groundtruth.TensorAssertions", "import sk.ainet.test.groundtruth.ToleranceConfig", "TensorAssertions.assertTensorClose", "ToleranceConfig.STANDARD" ], "must_not_contain": [ + "io.kotest", "kotlin.test.assertEquals", "1e-5f", "1e-6f", "ToleranceConfig.STRICT", "ToleranceConfig.RELAXED" ], - "must_not_contain_reason": "matmul accumulates kernel products and lives in the STANDARD bucket. STRICT is too tight; literal epsilons bypass the tolerance policy." + "must_not_contain_reason": "kotlin.test is the runner in jvmTest too — Kotest only remains in the skainet-apps CLI property tests. matmul accumulates kernel products and lives in the STANDARD bucket; literal epsilons bypass the tolerance policy." }, { "id": "softmax-relaxed-tolerance", @@ -34,7 +35,7 @@ "ToleranceConfig.STANDARD", "ToleranceConfig.STRICT" ], - "must_not_contain_reason": "Kotest cannot run in commonTest; softmax is a transcendental and must use RELAXED." + "must_not_contain_reason": "Library modules use kotlin.test everywhere (commonTest and jvmTest); softmax is a transcendental and must use RELAXED." }, { "id": "java-mirror-existing-pattern", From e149825abaa37ab310b61648776781881a726fde Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Thu, 8 Oct 2026 16:19:41 +0200 Subject: [PATCH 2/2] Fix citation-check failures: line ranges + None-safe sort Four citations were written without line ranges, which crashed verify-citations.py while sorting (None vs str). Give them real spans verified against SKaiNET main, and make the tool's sort None-safe so a rangeless citation reports instead of crashing. Co-Authored-By: Claude Fable 5 --- .../skills/skainet-model-loading/SKILL.md | 6 +++--- skainet-consumer-skills/skills/skainet-nn-dsl/SKILL.md | 3 ++- tools/verify-citations.py | 5 ++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/skainet-consumer-skills/skills/skainet-model-loading/SKILL.md b/skainet-consumer-skills/skills/skainet-model-loading/SKILL.md index f0a79b8..405171f 100644 --- a/skainet-consumer-skills/skills/skainet-model-loading/SKILL.md +++ b/skainet-consumer-skills/skills/skainet-model-loading/SKILL.md @@ -72,7 +72,7 @@ runBlocking { // bind 'name' -> 'tensor' into your Module's parameters } } -// from: SKaiNET/skainet-io/skainet-io-gguf/src/commonMain/kotlin/sk/ainet/io/gguf/StreamingGgufParametersLoader.kt (public surface) +// from: SKaiNET/skainet-io/skainet-io-gguf/src/commonMain/kotlin/sk/ainet/io/gguf/StreamingGgufParametersLoader.kt:59-115 (public surface) ``` The streaming loader parses only the header up front and loads tensor data on demand — suitable for large LLM weights. A file containing tensors outside the supported set fails fast, before any tensor is delivered. Optional constructor knobs: `weightForm` / `weightFormFor` (encoding × byte order × shape × residency, e.g. `WeightForm(residency = WeightResidency.MAPPED)` to serve weights from memory-mapped pages), `keepF16Native` / `keepBf16Native`. For metadata/tokenizer access, use `StreamingGGUFReader.open(source).fields` (e.g. `"general.architecture"`) and `TokenizerFactory.fromGguf(fields)`. @@ -98,7 +98,7 @@ runBlocking { // bind 'name' -> 'tensor' into your Module's parameter map } } -// from: SKaiNET/skainet-io/skainet-io-safetensors/src/commonMain/kotlin/sk/ainet/io/safetensors/SafeTensorsParametersLoader.kt (public surface) +// from: SKaiNET/skainet-io/skainet-io-safetensors/src/commonMain/kotlin/sk/ainet/io/safetensors/SafeTensorsParametersLoader.kt:49-136 (public surface) ``` SafeTensors is callback-driven — the loader hands you each tensor as it's parsed; you bind it where it belongs. `tensorFilter` skips tensors without reading them (name allowlists, size guards, dtypes the requested target can't accept) — filtered tensors don't count toward progress. BF16/F16 tensors dequantise to FP32 by default; `bf16Policy` / `fp16Policy = NarrowFloatLoadPolicy.KEEP_NATIVE` keeps the on-disk layout. @@ -138,7 +138,7 @@ runBlocking { val rawBytes = loaded.rawBytes // for re-serialisation // walk proto.graph.nodes / proto.graph.initializers } -// from: SKaiNET/skainet-io/skainet-io-onnx/src/commonMain/kotlin/sk/ainet/io/onnx/OnnxLoader.kt (public surface) +// from: SKaiNET/skainet-io/skainet-io-onnx/src/commonMain/kotlin/sk/ainet/io/onnx/OnnxLoader.kt:1-57 (public surface) ``` ONNX is a graph format — the loader hands you the protobuf representation. Lowering ONNX into a runnable SKaiNET `Module` is the next step (consumer apps typically use `skainet-compile-*` for that). diff --git a/skainet-consumer-skills/skills/skainet-nn-dsl/SKILL.md b/skainet-consumer-skills/skills/skainet-nn-dsl/SKILL.md index 3cae840..9e7e488 100644 --- a/skainet-consumer-skills/skills/skainet-nn-dsl/SKILL.md +++ b/skainet-consumer-skills/skills/skainet-nn-dsl/SKILL.md @@ -129,7 +129,8 @@ dag { op(matmul, listOf(x, w), schedule = parallel("rows", parallelism = 8)) // single op op(matmul, listOf(x, w), dtypePolicy = DTypePolicy.Require(BF16)) // dtype constraint } -// from: SKaiNET/skainet-lang/skainet-lang-dag/src/commonMain/kotlin/sk/ainet/lang/dag/ScheduleDsl.kt + DtypePolicyDsl.kt +// from: SKaiNET/skainet-lang/skainet-lang-dag/src/commonMain/kotlin/sk/ainet/lang/dag/ScheduleDsl.kt:25-49 +// from: SKaiNET/skainet-lang/skainet-lang-dag/src/commonMain/kotlin/sk/ainet/lang/dag/DtypePolicyDsl.kt:1-64 ``` `parallel(vararg dims, parallelism = null)` builds a `ScheduleHint` (`sk.ainet.context.schedule.ScheduleHint`). `ScheduleAnnotationPass` (skainet-compile-opt) validates hints per op, and the StableHLO export emits them as the `skainet.schedule` module attribute. Dtype policies feed `DTypeConstraintResolutionPass`. diff --git a/tools/verify-citations.py b/tools/verify-citations.py index 4f8659a..94272ab 100755 --- a/tools/verify-citations.py +++ b/tools/verify-citations.py @@ -37,7 +37,10 @@ def collect_citations(repo_root: Path) -> list[tuple[str, str | None, str | None text = path.read_text(encoding="utf-8") for m in CITE_RE.finditer(text): cites.add((m.group(1), m.group(2), m.group(3), str(path))) - return [(p, s, e, Path(src)) for (p, s, e, src) in sorted(cites)] + return [ + (p, s, e, Path(src)) + for (p, s, e, src) in sorted(cites, key=lambda c: (c[0], c[1] or "", c[2] or "", c[3])) + ] def main() -> int: