diff --git a/CHANGELOG.md b/CHANGELOG.md index 74efe019..6ac786a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,16 +2,15 @@ ## [Unreleased] -### Fixed - -- **`DTypeConstraintResolutionPass` no longer disconnects the nodes it resolves** (`skainet-compile-opt`, #1495): the pass marked a policy-carrying node by removing and re-adding it, which dropped every edge connected to that node, so a `dag { … dtypePolicy(…) }` graph left the pass with those nodes unwired. It now rebuilds the graph with the edges intact, and reports no change on an already resolved graph, so a multi-iteration pipeline converges. -- **A same-dtype `PrecisionBoundary` leaves no cast in a trace** (`skainet-lang-core`, SKEEP-008 phase A2, #1433): `stage("body", dtype = FP32)` inside an FP32 network called `convert`, which returns its input on the CPU backend but is still recorded by the tracing wrapper, so the exported module carried two `stablehlo.convert` `f32 -> f32`. The boundary now passes a tensor of the target dtype through without calling `convert`. -- **`concat` backward with inputs of different sizes** (`skainet-compile-dag`, #1490): the rule split the upstream gradient by one input's size and picked chunk `offset / size`, which only lines up when every input has the same extent along the concat axis; other shapes got another input's slice, or `null`, without an error. Each input now receives its own `narrow` of the upstream at the offset the forward gave it, for any sizes and a negative `dim`. -- **`squeeze` backward with no axis** (`skainet-compile-dag`, #1490): `squeeze(x)` removes every size-1 axis, but the backward unsqueezed dim 0 alone, so `x: [1, 3, 1]` got a `[1, 3]` gradient. The gradient is now the upstream reshaped to the input's shape. +## [0.64.0] - 2026-11-11 -### Removed - -- **`dense()`, `stage()` and the mixed-precision scaffold** (`skainet-lang-core`, SKEEP-008 phase A4, #1433): the generic overloads of `NeuralNetworkDsl`, deprecated at `ERROR` level in 0.63.0, are gone; use `dense(outputDimension, dtype = FP16) { }` / `stage(id, dtype = FP16) { }`. `MixedPrecisionModule`, `InternalMixedPrecisionModule` and `MixedPrecisionTensorOps`, deprecated in 0.63.0 and never implemented, are removed, together with `MixedPrecisionException`, which only they could throw. A third-party `NeuralNetworkDsl` implementation drops its three overrides. +Headline: **every differentiable op has a backward rule, and mixed precision is finished: it trains, exports without +redundant casts and is checked by the compiler** (see *Added*). `scaledDotProductAttention` gets its adjoint, so +attention layers train and the ops matrix Backward column reads 60/60. `TensorOps.convert` is differentiable, so the +layers behind `dense(64, dtype = BF16) { }` / `stage("head", dtype = FP16) { }` train too; `CastFoldingPass` folds +the casts between adjacent boundaries that change no value, and a boundary's dtype travels with the traced graph as a +`DTypePolicy.Prefer` that `DTypeConstraintResolutionPass` checks. SKEEP-008 is implemented. The generic +`dense()` / `stage()` overloads and the `MixedPrecision*` scaffold, deprecated in 0.63.0, are removed. ### Added @@ -21,7 +20,22 @@ - **Training through a mixed-precision boundary** (`skainet-lang-core`, `skainet-compile-dag`, SKEEP-008 phase A3, #1433): `TensorOps.convert` is a `@Diff` op. Its backward rule treats the cast as the identity and casts the upstream gradient back to the source dtype, so a gradient entering an FP16 / BF16 region is rounded to that format and the layers behind `dense(dtype = …)` / `stage(id, dtype = …)` train; a cast from or to an integer dtype has no gradient. `MixedPrecisionBackwardTest` checks the rule, a finite-difference gradient through FP16 boundaries, parity with the all-FP32 network, and an SGD run. The inference-only notes are gone; the ops matrix Backward column reads 60/60. - **SKEEP-008 acceptance tests** (`skainet-lang-core`, `skainet-backend-cpu`, #1433): `repackTo(Dense(4))` is checked bit-exact for Q5_0, Q5_1, Q5_K, Q6_K and `BITNET_B1_58` too, and one FP32 network with both a BF16 `dense` and an FP16 `stage` runs against the FP32 reference. - **Mixed-precision export test** (`skainet-compile-hlo`, SKEEP-008 phase A2, #1433): `MixedPrecisionExportTest` traces an FP32 network with one BF16 `dense` and one FP16 `stage` and pins the result: one `convert` pair per boundary in the graph, inner layers typed in the narrow dtype, and `stablehlo.convert` `f32 -> bf16`, `bf16 -> f32`, `f32 -> f16`, `f16 -> f32` in the exported module. -- **`scaledDotProductAttention` backward** (`skainet-compile-dag`, #1384): the attention adjoint (dQ, dK, dV and the dense mask's gradient) replaces the stub that returned `null`, so attention layers train. It honours `scale`, `causal`, sliding `window` / `rightContext`, `keyLengths` and the dense mask, recomputes the probabilities instead of storing them, and reduces grouped-query heads into their shared K/V. Masked scores get exactly zero gradient. `SdpaBackwardTest` checks every option against finite differences and three cases against PyTorch autograd; `stubbedBackwardOpNames` is empty and the coverage matrix's Backward column is complete. +- **`scaledDotProductAttention` backward** (`skainet-compile-dag`, #1384): the attention adjoint (dQ, dK, dV and the dense mask's gradient) replaces the stub that returned `null`, so attention layers train. It honours `scale`, `causal`, sliding `window` / `rightContext`, `keyLengths` and the dense mask, recomputes the probabilities instead of storing them, and reduces grouped-query heads into their shared K/V. Masked scores get exactly zero gradient. `SdpaBackwardTest` checks every option against finite differences and three cases against PyTorch autograd; `stubbedBackwardOpNames` is empty and the coverage matrix's Backward column is complete. `TransformerBlockBackwardTest` checks all 204 parameter entries of a pre-norm Transformer block (embeddings, RMSNorm, two-head causal attention, gated GELU feed-forward) against central finite differences. + +### Changed + +- **Dependencies**: Gradle wrapper 9.9.0 (#1488), Dokka Gradle plugin 2.3.0 (#1486), json-schema-validator 3.0.9 (#1487), `actions/setup-java` 6.2.0 (#1485), CodeQL upload-sarif 4.39.3 (#1484). + +### Removed + +- **`dense()`, `stage()` and the mixed-precision scaffold** (`skainet-lang-core`, SKEEP-008 phase A4, #1433): the generic overloads of `NeuralNetworkDsl`, deprecated at `ERROR` level in 0.63.0, are gone; use `dense(outputDimension, dtype = FP16) { }` / `stage(id, dtype = FP16) { }`. `MixedPrecisionModule`, `InternalMixedPrecisionModule` and `MixedPrecisionTensorOps`, deprecated in 0.63.0 and never implemented, are removed, together with `MixedPrecisionException`, which only they could throw. A third-party `NeuralNetworkDsl` implementation drops its three overrides. + +### Fixed + +- **`DTypeConstraintResolutionPass` no longer disconnects the nodes it resolves** (`skainet-compile-opt`, #1495): the pass marked a policy-carrying node by removing and re-adding it, which dropped every edge connected to that node, so a `dag { … dtypePolicy(…) }` graph left the pass with those nodes unwired. It now rebuilds the graph with the edges intact, and reports no change on an already resolved graph, so a multi-iteration pipeline converges. +- **A same-dtype `PrecisionBoundary` leaves no cast in a trace** (`skainet-lang-core`, SKEEP-008 phase A2, #1433): `stage("body", dtype = FP32)` inside an FP32 network called `convert`, which returns its input on the CPU backend but is still recorded by the tracing wrapper, so the exported module carried two `stablehlo.convert` `f32 -> f32`. The boundary now passes a tensor of the target dtype through without calling `convert`. +- **`concat` backward with inputs of different sizes** (`skainet-compile-dag`, #1490): the rule split the upstream gradient by one input's size and picked chunk `offset / size`, which only lines up when every input has the same extent along the concat axis; other shapes got another input's slice, or `null`, without an error. Each input now receives its own `narrow` of the upstream at the offset the forward gave it, for any sizes and a negative `dim`. +- **`squeeze` backward with no axis** (`skainet-compile-dag`, #1490): `squeeze(x)` removes every size-1 axis, but the backward unsqueezed dim 0 alone, so `x: [1, 3, 1]` got a `[1, 3]` gradient. The gradient is now the upstream reshaped to the input's shape. ## [0.63.0] - 2026-11-06 diff --git a/CITATION.cff b/CITATION.cff index 53a5f9bb..f316b0f5 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -12,8 +12,8 @@ authors: orcid: https://orcid.org/0009-0006-7981-4075 - name: SKaiNET-developers contributors website: https://github.com/SKaiNET-developers -version: "0.63.0" -date-released: "2026-11-06" +version: "0.64.0" +date-released: "2026-11-11" license: MIT repository-code: https://github.com/SKaiNET-developers/SKaiNET url: https://skainet-developers.github.io/SKaiNET/ diff --git a/README.md b/README.md index 2947ba90..7c9272ea 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Add the core dependencies (Gradle Kotlin DSL): ```kotlin dependencies { // Recommended: import the umbrella BOM and drop versions on the engine modules. - implementation(platform("sk.ainet:skainet-bom:0.63.0")) + implementation(platform("sk.ainet:skainet-bom:0.64.0")) implementation("sk.ainet.core:skainet-lang-core") implementation("sk.ainet.core:skainet-backend-cpu") @@ -308,32 +308,28 @@ val withoutLabel = dataPipeline() --- -## What's New in 0.63.0 - -Block-quantized weights reach an NPU through StableHLO, and the engine gains mixed precision, explicit re-encoding, -`convTranspose2d` and a working ONNX import path. - -- **Q8_0 / Q4_0 weights to an NPU** — `PackedConstantHandling.KEEP_PACKED` keeps a packed weight in the traced - graph, `BlockQuantMatmulFusionPass` folds every `x · Wᵀ` over one, and the StableHLO export emits the - dequantize-then-`batch_matmul` chain with `i8` / `i4` codes and `bf16` scales inline. Torq 2.3.0 - (`--iree-input-type=stablehlo-torq`) compiles it to its `mixed_quant_matmul` kernel: the whole FunctionGemma 270M - decode step runs on an SL2610 NPU in one dispatch at 162 ms per step, with the same next token as the f32 - reference. Weights from the GGUF loader (off-heap) export the same as heap ones. -- **StableHLO and TOSA passes** — the `STABLE_HLO` phase of a target optimizer rewrites the emitted module, and - `DivideByBroadcastPass`, `RankTwoWeightMatmulPass` and `TosaFinish` prepare a module for StableHLO's own TOSA - legalization (Moonshine's streaming encoder runs on an NPU through `tosa-torq` in one dispatch). `Int4` maps to - `i4`. -- **Mixed precision per layer** — `dense(64, dtype = BF16) { }` and `stage("head", dtype = FP16) { }` build a layer - in another dtype behind a `PrecisionBoundary` that casts in and out (inference only). The generic - `dense()` / `stage()` overloads, which could only throw, are deprecated at `ERROR` level. -- **Re-encoding storages** — `repackTo(Dense(4))` decodes every packed encoding with a dequantizer bit-exactly, and - `requantizeTo` is the explicit lossy direction. -- **New ops and layers** — `convTranspose2d` with backward and StableHLO lowering, the `GroupNormalization` forward - pass, `sliceCopy {}`, and precision, recall and F1 metrics with binary, macro and micro averaging. -- **ONNX** — `LeakyRelu`, `Silu`, `Clip`, `BatchNormalization`, `Pad` and `ConvTranspose` import as executable ops, - and `OnnxModelReader.open` reads metadata up front and each tensor on demand. -- **Fixes** — the JVM tokenizer's `Split` regex follows Unicode character classes, and `randn` never returns - `Inf` / `NaN`. +## What's New in 0.64.0 + +Every differentiable op has a backward rule, and mixed precision is finished: it trains, exports without redundant +casts and is checked by the compiler. + +- **Attention trains** — `scaledDotProductAttention` has its adjoint (dQ, dK, dV and the dense mask's gradient) for + every option: `scale`, `causal`, sliding windows, key lengths, grouped-query heads. It is checked against finite + differences and PyTorch autograd, and a whole Transformer block is checked parameter by parameter. The ops + matrix Backward column reads 60/60. +- **Training through mixed precision** — `TensorOps.convert` is differentiable, so the layers behind + `dense(64, dtype = BF16) { }` and `stage("head", dtype = FP16) { }` train. The cast passes the gradient straight + through and rounds it to the region's format; integer casts have no gradient. +- **Fewer casts in the export** — `CastFoldingPass`, part of the default optimization pipelines, folds casts that + change no value: the `bf16 -> f32 -> f16` between two adjacent boundaries exports as one `stablehlo.convert`, and + a same-dtype boundary exports none. A narrowing round trip is never folded. +- **A layer's dtype is checked** — ops traced inside a boundary carry `DTypePolicy.Prefer(dtype)`, and + `DTypeConstraintResolutionPass` reports a region that a later pass retyped. `ComputeGraphExecutor` runs + `convert` nodes, so a traced mixed-precision graph executes. +- **Removed** — the generic `dense()` / `stage()` overloads and the `MixedPrecision*` scaffold, deprecated in + 0.63.0; use the `dtype =` argument. +- **Fixes** — `concat` backward with inputs of different sizes, `squeeze` backward with no axis, and + `DTypeConstraintResolutionPass` no longer drops the edges of the nodes it resolves. See [CHANGELOG.md](CHANGELOG.md) for full release notes, including every prior release. @@ -359,6 +355,13 @@ We love contributions! Whether it's a new operator, documentation, or a bug fix: Browse the full codebase documentation on [DeepWiki](https://deepwiki.com/SKaiNET-developers/SKaiNET). +### Contributors (0.64.0) + +- **Michal Harakal** ([@michalharakal](https://github.com/michalharakal)) — the `scaledDotProductAttention` backward + with its finite-difference and Transformer-block tests, the `concat` / `squeeze` backward fixes, the rest of + SKEEP-008 (differentiable `convert`, export tests, `CastFoldingPass`, the checked boundary dtype, the scaffold + removal), and the release + ### Contributors (0.63.0) - **Michal Harakal** ([@michalharakal](https://github.com/michalharakal)) — the block-quant StableHLO export and its @@ -511,7 +514,7 @@ from it). Ready-to-paste BibTeX: author = {Harakal, Michal and {SKaiNET-developers contributors}}, title = {{SKaiNET}: A {Kotlin} Multiplatform {AI} Framework}, year = {2026}, - note = {Version 0.63.0. MIT License}, + note = {Version 0.64.0. MIT License}, howpublished = {\url{https://github.com/SKaiNET-developers/SKaiNET}} } ``` diff --git a/docs/antora.yml b/docs/antora.yml index 7bfb5cd7..262e2868 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -15,7 +15,7 @@ asciidoc: framework_name: SKaiNET # Current SKaiNET release — bump once per release; referenced as # {skainet_version} in dependency snippets (blocks need subs="attributes+"). - skainet_version: 0.63.0 + skainet_version: 0.64.0 ksp_version: 2.2.21-2.0.5 dokka_version: 2.1.0 asciidoctorj_version: 3.0.0 diff --git a/docs/modules/ROOT/pages/reference/kernel-support-matrix.adoc b/docs/modules/ROOT/pages/reference/kernel-support-matrix.adoc index 21b81e76..c4be7446 100644 --- a/docs/modules/ROOT/pages/reference/kernel-support-matrix.adoc +++ b/docs/modules/ROOT/pages/reference/kernel-support-matrix.adoc @@ -1,7 +1,7 @@ = Kernel × platform support matrix :description: Which compute-kernel provider serves each weight format on each KMP target. -Generated from `kernel-support.json` (version `0.63.0`) by `KernelSupportMatrixTest` — registry introspection of the registered `KernelProvider` implementations. Do not edit by hand; run `./gradlew generateKernelMatrix` to refresh. +Generated from `kernel-support.json` (version `0.64.0`) by `KernelSupportMatrixTest` — registry introspection of the registered `KernelProvider` implementations. Do not edit by hand; run `./gradlew generateKernelMatrix` to refresh. Each cell is the best (highest-priority) provider that serves `Float32 × format` `matmul` on that platform: *native-ffm* / *native-jni* / *native-cinterop* (100) → *panama-vector* / *wasm-simd128* (50) → *scalar* (0). An empty cell (`—`) means no provider carries a kernel there (the format is dequant-to-FP32 only). The JS/WASM column assumes `WasmSimd.install()` has run; without the sidecar it is the scalar floor. diff --git a/docs/modules/ROOT/pages/reference/operators/generated/index.adoc b/docs/modules/ROOT/pages/reference/operators/generated/index.adoc index f107d66a..8fa0a923 100644 --- a/docs/modules/ROOT/pages/reference/operators/generated/index.adoc +++ b/docs/modules/ROOT/pages/reference/operators/generated/index.adoc @@ -1,6 +1,6 @@ = AI-NET Operators Reference -Generated from version `0.63.0` on 2026-11-01 +Generated from version `0.64.0` on 2026-11-11 == Operators by Modality diff --git a/docs/modules/ROOT/pages/reference/ops-status-matrix.adoc b/docs/modules/ROOT/pages/reference/ops-status-matrix.adoc index 8ee5a9aa..3a1b97b5 100644 --- a/docs/modules/ROOT/pages/reference/ops-status-matrix.adoc +++ b/docs/modules/ROOT/pages/reference/ops-status-matrix.adoc @@ -1,7 +1,7 @@ = Operator Coverage Matrix :description: Cross-backend status for every operator function in SKaiNET. -Generated from `operators.json` version `0.63.0` on 2026-11-01. +Generated from `operators.json` version `0.64.0` on 2026-11-11. Rows are `Operator.function` pairs. A gap is marked in source with `@NotImplemented(reason = …, issue = …)` (or `@InProgress`) and shows up here as ❌ (or 🚧) linked to its tracking issue; the PR that closes the gap removes the annotation. See xref:contributing/issue-taxonomy.adoc[Issue taxonomy] to pick one up. diff --git a/docs/modules/ROOT/pages/tutorials/android-classifier-getting-started.adoc b/docs/modules/ROOT/pages/tutorials/android-classifier-getting-started.adoc index 83dca382..675bca6d 100644 --- a/docs/modules/ROOT/pages/tutorials/android-classifier-getting-started.adoc +++ b/docs/modules/ROOT/pages/tutorials/android-classifier-getting-started.adoc @@ -19,12 +19,12 @@ An Android project with Kotlin. Add the SKaiNET modules: [source,kotlin] ---- dependencies { - implementation("sk.ainet.core:skainet-lang-core:0.63.0") // tensors, DSL, training - implementation("sk.ainet.core:skainet-backend-cpu:0.63.0") // CPU ops - implementation("sk.ainet.core:skainet-compile-dag:0.63.0") // autograd (training context) - implementation("sk.ainet.core:skainet-data-api:0.63.0") // Dataset / DataBatch - implementation("sk.ainet.core:skainet-data-simple:0.63.0") // embedded Iris - runtimeOnly("sk.ainet.core:skainet-backend-jni-cpu:0.63.0") // NEON kernels (see below) + implementation("sk.ainet.core:skainet-lang-core:0.64.0") // tensors, DSL, training + implementation("sk.ainet.core:skainet-backend-cpu:0.64.0") // CPU ops + implementation("sk.ainet.core:skainet-compile-dag:0.64.0") // autograd (training context) + implementation("sk.ainet.core:skainet-data-api:0.64.0") // Dataset / DataBatch + implementation("sk.ainet.core:skainet-data-simple:0.64.0") // embedded Iris + runtimeOnly("sk.ainet.core:skainet-backend-jni-cpu:0.64.0") // NEON kernels (see below) } ---- diff --git a/docs/modules/skeep/pages/008-precision-and-encoding-conversion.adoc b/docs/modules/skeep/pages/008-precision-and-encoding-conversion.adoc index 2640b29d..f48f8da2 100644 --- a/docs/modules/skeep/pages/008-precision-and-encoding-conversion.adoc +++ b/docs/modules/skeep/pages/008-precision-and-encoding-conversion.adoc @@ -15,9 +15,9 @@ Origin: the stub inventory behind the operator coverage matrix (https://github.c | B1 | Lossless `repackTo`, lossy `requantizeTo` | https://github.com/SKaiNET-developers/SKaiNET/pull/1470[#1470], 0.63.0 | A1 | `PrecisionBoundary`, `dense(dtype = …)`, `stage(id, dtype = …)`, deprecations | https://github.com/SKaiNET-developers/SKaiNET/pull/1469[#1469], 0.63.0 -| A2 | Trace and StableHLO export pinned by `MixedPrecisionExportTest`; a same-dtype boundary no longer records a cast | https://github.com/SKaiNET-developers/SKaiNET/pull/1492[#1492] -| A3 | `convert` is `@Diff`; gradient check and training through a boundary in `MixedPrecisionBackwardTest` | https://github.com/SKaiNET-developers/SKaiNET/pull/1492[#1492] -| A4 | Generic `dense()` / `stage()` overloads and the `MixedPrecision*` scaffold removed | https://github.com/SKaiNET-developers/SKaiNET/pull/1492[#1492] +| A2 | Trace and StableHLO export pinned by `MixedPrecisionExportTest`; a same-dtype boundary no longer records a cast | https://github.com/SKaiNET-developers/SKaiNET/pull/1492[#1492], 0.64.0 +| A3 | `convert` is `@Diff`; gradient check and training through a boundary in `MixedPrecisionBackwardTest` | https://github.com/SKaiNET-developers/SKaiNET/pull/1492[#1492], 0.64.0 +| A4 | Generic `dense()` / `stage()` overloads and the `MixedPrecision*` scaffold removed | https://github.com/SKaiNET-developers/SKaiNET/pull/1492[#1492], 0.64.0 |=== The sections below are the proposal as accepted. Where the implementation differs, the text diff --git a/gradle.properties b/gradle.properties index b7a0217e..f2d9793c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ GROUP=sk.ainet.core -VERSION_NAME=0.63.0 +VERSION_NAME=0.64.0 POM_DESCRIPTION=SKaiNET POM_URL=https://github.com/SKaiNET-developers/skainet/