Headline: every ExecutionContext gets a Schedule — and the CI run that exercised it found a
real deadlock. sk.ainet.context.schedule.Schedule splits what an op computes from how its
independent chunks spread across cores: scaledDotProductAttention is the first scheduled op,
parallelChunks no longer hides a runBlocking(Dispatchers.Default) island, and a JVM
CoroutineSchedule spreads chunks across a shared pool. Turning it on deadlocked
skainet-backend-cpu:jvmTest on CI's 4-vCPU runner for three of the last four test (jvm) runs —
not the OOM a first pass assumed, but a coroutineScope waiting on children the pool had no thread
left to run. A region is now a shared chunk queue instead: whatever the pool is doing, the caller
can always finish its own region alone. Also in this release: SafeTensorsParametersLoader's
tensorFilter reaches parity with the sharded loader, and the sk.ainet.lang.memory API drops its
ExperimentalMemoryApi opt-in gate now that SKEEP-003's M0–M2 have shipped.
- Schedules — the compute-level algorithm/schedule split (SKEEP-005)
(#1259 registries not safe for concurrent reads,
#1260
parallelChunksrunBlockingisland): a dependency-freesk.ainet.context.schedule.Scheduleon everyExecutionContext(ctx.schedule,ctx.withSchedule(s) { … },ScheduledExecutionContext), a JVMCoroutineSchedulebuilt on structured concurrency (caller runs the first chunk, nested regions inline,dedicated()pool), and the first scheduled op —scaledDotProductAttentionruns its(batch, head)units on the context's schedule, bit-identical to the sequential loop.parallelChunksno longer hides arunBlocking(Dispatchers.Default)island: it delegates to the ops' schedule, soDirectCpuExecutionContext(schedule = Schedule.Sequential)makes every kernel single-threaded and the JVM default (CoroutineSchedule.hardware()) spreads them across cores. Unhonoured requests are visible asTraceEvent.ScheduleDowngraded; regions asTraceEvent.ScheduleRegion. Compile lane:dag { schedule(parallel("heads")) { … } }stamps aScheduleHintthatScheduleAnnotationPassvalidates per op andStableHloConverteremits as theskainet.schedulemodule attribute besideskainet.tensor_layouts. Registries (KernelDispatch,KernelRegistry) are now safe for concurrent reads. Found on the way and reported, not fixed here: #1261 (Panama matmul's first call differs by an ULP until the JIT intrinsifiesreduceLanes). Docs: SKEEP-005, "Algorithm and schedule", "Schedule getting started" (executable sample). tensorFilteron the single-fileSafeTensorsParametersLoader(#1256): parity with the sharded loader — an optional predicate over the tensor headers; filtered-out tensors are neither read nor delivered and do not count toward progress. Lets a family load selectively from a checkpoint that also carries tensors the requested dtype cannot accept (int64 index tables, custom quantized payloads) instead of failing on the first unmapped one. Threaded throughwithPolicy.
ExperimentalMemoryApiopt-in gate removed (SKEEP-003): thesk.ainet.lang.memoryAPI (Storage,Scope,Format,Layout,TensorView,WeightForm,WeightByteOrder, …) no longer requires@OptIn(ExperimentalMemoryApi::class)to use. The annotation's own message — "usable, but may change until milestone M1 is complete" — was stale: M0/M1/M2 all shipped complete in 0.49.0.ExperimentalMemoryApiis deleted along with every@OptIn/@ExperimentalMemoryApiannotation referencing it.
CoroutineScheduledeadlock when a region is entered from its own pool (#1262, #1264, #1266):CoroutineSchedule.forRangeran a region asrunBlocking { coroutineScope { launch(Dispatchers.Default) … } }, and acoroutineScopewaits for every child — including ones the pool never got a thread for. Once everyDispatchers.Defaultworker was itself inside a region (routine on a 4-vCPU CI runner; never reproduced on a many-core laptop), nobody was left to run the children and the JVM parked forever — the intermittenttest (jvm)timeout that #1264's "force fully serial" fix mistook for an OOM hang. A region is now a shared chunk queue:tasks - 1helpers dispatch to the pool, the caller runs chunk 0 and then drains the queue itself, and waits only for chunks a thread has already claimed — a caller can always finish its own region alone, whatever the pool is doing. Contract unchanged (first failure wins, nested regions run inline, writes happen-before return); public API unchanged.
Headline: the export pipeline emits billion-parameter models. Tracing a 4.5B-parameter Gemma 3n E2B through the DSL → tape → StableHLO path could not finish on a 48 GB host (#1247): shape-only tracing materialized real zero buffers, constant extraction copied every weight while the originals stayed live, and when the converter did fail it said so in MLIR comments and exited 0 — so the first broken node cascaded through a thousand more and still produced a "module". Each of those is closed, and the last one turned out to hide a fourth: the tied embedding is exactly one byte larger than a JVM array can hold, which no amount of widening could fix — external constants now travel as the aliased float array they already are. The same repro that OOMed a 46 GB heap now exports the full model in under a minute with zero failure comments, and the sharded SafeTensors loader the transformers families kept re-implementing lives in the engine.
ShardedSafeTensorsParametersLoader— the sharded-index SafeTensors loader (#1246, #1252): aParametersLoaderovermodel.safetensors.index.json, ridingStreamingShardedSafeTensorsReader.openFromIndexwith the single-file loader's BF16/FP16 policies (withPolicyparity), a fail-fast dtype pre-scan before any tensor is delivered, and atensorFilterhook so size guards and name allowlists stay family-side while every dtype decision stays in the engine. The dtype dispatch and dequant helpers moved into a shared internalSafeTensorsMaterializer, so both loaders materialize identically (parity-tested). Downstream: SKaiNET-transformers' hand-rolled per-family SafeTensors loaders can collapse onto it.BufferHandle.Floats— array-free path for ≥2 GiB constants (#1247): external FP32 constants now ride the aliasedFloatArrayend-to-end (graph →ExternalParameterRef→.irpa), never serializing to a singleByteArray— the gemma3n tied embedding (262144x2048 FP32 =Int.MAX_VALUE+ 1 bytes) structurally cannot exist as one byte buffer.IrpaWriterstreams the values little-endian in 64 MiB chunks;DefaultBufferResolverreads through a chunked byte view. Constant element counts fold inLong, and an oversized single-buffer serialization now throwsConstantTooLargeExceptionwith the remediation instead of theNegativeArraySizeExceptionthat was previously mistaken for a registry miss.
- Void tracing allocates nothing
(#1247,
#1249):
VoidTensorOpsrecorded every shape-propagation op into a real dense zero buffer, allocated twice and retained by the trace — ~9 GB of zeros across a 30-layer Gemma 3n E2B trace. Static-shape void results are now lazy placeholders (readers still see zeros; unread ops allocate nothing),ShapeOnlyTensorDatais public for consumers that hand-roll shape-only data, andmatmulWeightTransposedno longer constructs the transposed intermediate. A 4096×4096 traced op stack tracks 0 bytes. - Graph constants alias live weights; packed params fail loudly
(#1247):
TraceToGraphBuilderno longer copies every frozen float weight into the graph — the constant'sinitial_valuealiases the live buffer (read-only contract), halving weight residency during export. BF16/FP16 dense weights widen to one FP32 copy. A frozen parameter with packed storage (Q4_K, Q8_0, ternary, …) now throwsPackedConstantExceptioninstead of silently becoming a function argument and producing an unservable module;PackedConstantHandling.DEQUANTIZE(threaded throughtoComputeGraph) opts into dense FP32 extraction instead. - StableHLO conversion fails loudly by default
(#1247): converter failures were
MLIR comments — a graph whose first node failed to lower could cascade through every downstream
node and still "succeed" with an empty
returnand exit 0.StableHloConverterand everyStableHloConverterFactoryentry point now take aConversionErrorPolicy(defaultSTRICT): an unconvertible node throwsHloConversionException, and an operand whose producer was never converted throwsMissingOperandExceptioninstead of being silently dropped and shifting later operands into earlier positions.ConversionErrorPolicy.LENIENTrestores the historical comment-and-continue behavior for callers that inspect partially-converted modules.
clamplowers to StableHLO (#1247): the tracedclamp(x, minVal, maxVal)op had no converter at all — the first gap the strict gemma3n export surfaced once failures stopped being comments. Lowers tostablehlo.clampwith splat bounds.indexSelectis now routable in the StableHLO gather converter (#1247): the KSP tracing wrapper emitsindexSelect, but the registry only knewindex_select, so traced index-select nodes (e.g. Gemma per-layer embeddings) could not lower. Both spellings route to the gather lowering.StableHloConverterFactory.createBasicregistersNeuralNetOperationsConverter(#1247): parity withcreateExtended— a traced model with conv/pool/norm nodes no longer fails to lower via the basic factory, with registration order preserving existing op-name precedence.
Headline: the engine stops silently running on the scalar floor. A downstream Gemma 4 port
was generating garbage at roughly 0.04 tok/s, and the investigation
(#1220) found the cause split across
both repositories — but the engine's share of it was one theme repeated: a fast path that exists,
is compiled in, and never gets used, with nothing saying so. KernelDispatch was never populated
in production at all, so every matmul fell back to the decoding reference kernel; dense FP32
weights in mapped or off-heap storage missed the kernel that serves them and dequantized instead;
and the fallback itself was routed to a no-op trace sink, which is why a ~1000x degradation could
sit in a release undetected. Those are closed, and the dispatcher now installs itself on first use
rather than trusting every entry point to remember — ternary/BitNet packs included,
so the discovery set covers every format the backends ship kernels for. Alongside that, Android's native targets now
run the whole dependency chain, not just its first two modules.
ViewKernelPackSPI and self-healing dispatch (#1220, #1221):KernelDispatchpopulates itself on first use viaensureInstalled(), backed by a newViewKernelPackservice interface withinstallPlatformKernelPacks()actuals per platform — ServiceLoader-based on JVM and Android, explicit on Kotlin/Native, which has no ServiceLoader. Applications no longer have to call an install routine at startup and no longer silently lose every kernel when they forget. The two backends ship discovery metadata:FfmRowMajorKernelPackFactory(skainet-backend-native-cpu) andJniMappedKernelPackFactory(skainet-backend-jni-cpu).androidNativeArm32/androidNativeArm64across the downstream chain (#1239):skainet-io-gguf,skainet-lang-dag,skainet-compile-dag,skainet-compile-opt,skainet-backend-api,skainet-backend-cpu, plusskainet-lang-modelsandskainet-compile-jsonto close the target set over test compilations. Onlyskainet-io-coreand friends had these targets before, so a consumer building for an Android device could not resolve the rest of what it needed.skainet-io-core's 64-bit split source set has no counterpart here: none of these modules has posix-typed code, so arm32'sInt-widthssize_t/size_tdoes not reach them.- Mapped-serving encodings derived from kernel registrations
(#1193,
#1215):
KernelDispatch.mappedServableEncodings()reports which encodings aMappedCapableKernelactually serves right now, replacing a hand-kept list that could drift from the registry it described. gemma4in the model registries (#1221):TokenizerFactoryaccepts the architecture andModelArchitecture.ggufIdMapmaps"gemma4"toGEMMA, so a Gemma 4 GGUF loads through the engine's own routes instead of throwing.- Dense FP32 GEMV path in the Panama kernel
(#1221):
PanamaVectorMatmulKernelgainsgemvRows()for the m ≤ 8 shapes a decode step actually issues — 16.7x at m=1 over the general blocked path, which was written for prefill-sized work.
-
Ternary kernel packs join the self-healing dispatch SPI (#1240, #1241): the
ServiceLoaderservice files now listFfmTernaryKernelPackFactory(JVM jar) andJniTernaryKernelPackFactory(Android AAR), soKernelDispatch.ensureInstalled()wires the exact FP32×BITNET_B1_58LUT gemv and the fusedBITNET_PLANESlm_head with no bootstrap call — without this, a consumer loading ternary weights silently got the int8-requantize or decoding-reference path (~120× slower per the #1141 bench) unless it calledNativeTernaryF32GemvKernel.install()/NativeTernaryLmheadKernel.install()explicitly: the exact failure mode this release's self-healing dispatch exists to eliminate, closed for the ternary formats in the same release. Kotlin/Native still installs explicitly (noServiceLoaderthere); the ternary tutorial's install table says which targets are automatic. -
Dense FP32 weights in mapped or off-heap storage fell back to dequantization (#1218): the kernel that serves them only recognised
Heap, so a memory-mapped model — the whole point of mapped staging — took the slow path. Now served from any storage kind. -
The reference-kernel fallback was invisible (#1221):
DefaultCpuOpshardcodedNoopTraceSinkat both dispatch sites, so falling back to the decoding reference kernel emitted nothing.KernelDispatchgains adefaultSinkand warns once, loudly, the first time it happens. -
SpecialTokenSplitterlost word boundaries when decoding token by token (#1221): it did not overridedecodeToken, so streaming consumers of any SentencePiece GGUF with special tokens saw spaces disappear from the output. Also fixestoken_typeparsing, which discardedUInt-typed GGUF metadata and could silently drop a model's special tokens. -
ar/ranlibselection for the aarch64 cross build on macOS hosts (#1209): the build picked the host's Mach-O tools for an ELF target, producing archives the linker rejected.
- Small-shape FP32 matmul (#1221):
a direct-loop path under
SMALL_FP32_MATMUL_WORKskips blocking overhead that costs more than it saves at decode sizes, andtransposedDenseWeight()caches the transpose instead of rebuilding it per call. Measured end to end on a downstream Gemma 4 port: ~2.3x on both decode and prefill.
- Kernel-selection explanation page, covering the two registries and how a weight reaches a kernel (#1221).
- Architecture reference gains its missing building blocks — kernel dispatch, ternary, AOT conversion (#1216).
- DARC/SKEEP onboarding, issue taxonomy and an
F1Scoreworked example for contributors (#1238). GITFLOW.adocreconciled with themainbranch reset, documenting the release sequence actually used from 0.51.0 onward (#1213).- Why the
GROUP_128/GROUP_64native decode kernel was closed (#1205, #1214).
github/codeql-action/upload-sarif4.37.8 → 4.37.9 (#1236).
Headline: ternary/BitNet weights join the memory-mapped weight story 0.50.0 started for every
other quant format. 0.50.0 shipped mapped staging for every GGML block format but left ternary
(BITNET_B1_58) heap-staging, flagged then as needing the work tracked in
#1198. That work is done: off-heap
storage removes the Android ART heap-cap OOM risk a repacked ternary weight used to carry, and a
SEQUENTIAL-layout (NeoGPU-converted) GGUF now gets a true zero-copy mmap load with no repack at
all. A new AOT converter lets a build that owns its model pipeline pay that repack cost once,
offline, instead of on every load — its IREE-facing counterpart lives in a new home,
SKaiNET-IREE-tools, on the
architectural grounds that compiled-target-specific conversion belongs beside the compiler
toolchain it targets, not inside core (see #1207).
- Off-heap storage for packed ternary/quantized weights
(#1202,
#1206):
StoragegainscopyInto/copyFrombulk byte primitives implemented for every concrete storage kind (Heap,SegmentStorage,MappedFileStorage,DirectBufferStorage,MappedBufferStorage,NativeMallocStorage,NativeMappedStorage).PackedBlockStoragegains apackedStorageproperty (default: wrapspackedDatainStorage.Heap, so every existing quantized format is unaffected);packedViewreads from it instead of always re-wrapping the raw array.BitNetB158TensorDatagains aStorage-backed constructor/fromStorage()factory — the per-element accessors lazily snapshot off-heap storage only if actually touched, so inference through the GEMV kernels never materializes a heap copy.StreamingGgufParametersLoaderallocates off-heap for repacked I2_S payloads at or abovePlannerProfile.OFF_HEAP_THRESHOLD(256 KB) instead of a permanentByteArray. - Zero-copy native gemv for off-heap ternary weights
(#1202,
#1206):
TernaryF32GemvNativegainsgemvPackedStorage(), letting the JVM/FFM face hand aSegmentStorage'sMemorySegmentstraight to the native downcall — the weight is never copied, not even once, where the previousgemvPackedpath re-copied the entire weight matrix into a fresh arena on every row of every call, independent of storage kind.NativeTernaryF32ViewKernelnow dispatches on the weight's storage kind instead of only acceptingStorage.Heap, so an off-heap ternary weight keeps the fast NEON/FFM path instead of silently falling back to the slow reference kernel. - Zero-copy mmap for
SEQUENTIAL-layout I2_S tensors (#1203, #1208):I2sRepack.toSequentialPayloadno longer copies aSEQUENTIALbuffer that's already exactly the target payload (the common NeoGPU-converted case). More significantly, the loader's mmap-eligibility branch — previously keyed on a hardcoded encoding whitelist that didn't includeI2_S— now maps aSEQUENTIALI2_S tensor directly off the file whenever its trailing bytes are provably the real scale (no companion<name>_scaletensor overriding them), giving it the same zero-copy path Q4_K/Q8_0/etc. already had.GROUP_128/GROUP_64payloads and companion-scoredSEQUENTIALfiles correctly keep repacking. I2sAotConverter: AOT GGUF → GGUF conversion for I2_S (#1207, #1210): reads an arbitrary GGUF, repacks I2_S tensors intoSEQUENTIAL+trailer order ahead of time, drops the now-redundant companion scale tensor, and passes every other tensor and all KV metadata through unchanged — the converted file always takes the new zero-copy mmap path above, with no on-device cost at all.GgufTensorEntry(the writer's tensor-entry type) gains arawBytespassthrough mode to support this without going through the element-indexedTensorFlattenpath.- GGUF I2_S →
.irpaconversion (IREE-facing counterpart, in SKaiNET-IREE-tools#1, not this repo): a standalone Python tool producing an IREE parameter archive directly from a GGUF's ternary tensors, foriree-compile --iree-opt-import-parameters=.
- Scoped dense-FP32 activations silently fell out of the quantized matmul chooser
(#1211):
chooseQuantizedMatmul2Daccepted onlyFloatArrayTensorData/MemorySegmentBackedDataactivations; aScopedExecutionContextforward's slab-backedStorageFloatTensorDatafell through tomatmulGeneric, whose per-elementget()on aQ8MemorySegmentTensorDataweight returns the raw quantization byte, not the value — silently wrong logits (the #993 class of bug). Any dense activation (encoding == null) is now accepted via its own offset-awarecopyToFloatArray().
Headline: model size on Android is now a page-cache question, not a heap question — and decode is 2.5× faster. Quantized weights are served straight from the memory-mapped GGUF file (zero copies, zero relayout), the packed matmul kernels thread across cores, and a 1.0 GB Qwen2.5-1.5B Q4_K_M — which OOM'd at load under the 256 MB ART cap in 0.49.0 — loads in ~0.4 s with 566 KB of weight heap and decodes at 61–66 ms/step with zero steady-state page faults (Pixel 8a, measured by the M2-A5 harness). The same file-order kernels serve heap-staged weights too, closing a measured 48,771 ms/step silent-fallback trap for mixed-quant models — and fallbacks are never silent again.
- Packed-tensor mapped staging (#1189,
#1190): under
WeightForm(residency = MAPPED)every GGML block format (Q4_K, Q6_K, Q5_K, Q8_0, Q4_0, Q5_0, Q5_1 — #1192) is served from file-backed pages:BufferPackedTensorDataoverMappedBufferStorage/DirectBufferStorage, row-major (_rm) C kernels that read canonical GGUF file order (no prepack, no relayout copy), reached via JNI direct-buffer entries on Android and FFMMemorySegment.ofBufferon the JVM (#1191). Ternary (BITNET_B1_58) still heap-stages — its load-time repack needs the sidecar cache tracked in #1198. - Threaded packed matmuls (#1195,
#1196): a spin-then-park worker pool with
guided row-grains, engaged at
outputDim ≥ 512. The design is measurement-driven and the failed variants are documented in the PR: per-callpthread_createcost 994 ms/step against deep-idle cores, and every sleeping pool lost to one pegged big core because sub-millisecond bursts never build scheduler utilization — the ~1 ms spin before parking (the same trick llama.cpp uses) is what unlocks big cores at full clocks. 153 → 61 ms/step on the 1.5B; results are bit-identical to single-threaded (disjoint row ranges, unchanged per-row accumulation order). - Storage-polymorphic row-major dispatch, and no silent fallbacks
(#1193,
#1197): one kernel per format serves
BLOCKED_ROW_MAJORweights from mapped, direct or heap storage — an un-prepacked heap canonical weight used to fall to the decoding reference kernel silently (measured: 48,771 ms/step on SmolLM2-135M, whose non-256-multiple dims made llama.cpp's quantizer emit mostly Q8_0; now 33 ms/step, the fastest configuration measured).ViewKernelgained a sink-awarerunoverload and every packed bridge announces a reference fallback as aKernelRuntrace event with the reason; the M2-A5 harness prints the count. - The plan tells the truth about mapped weights
(#1190):
MemoryPlan.budgetedBytescharges mapped-servable weights against device RAM/page cache instead of the heap budget (fits, suggestions andPlannerProfile's KV auto-quantization follow), rendered as its ownmapped (page cache, evictable — not heap)line.AllocationResolver.servesFromMappingis the one predicate the resolver, the plan and the loader share, gated byStorageCapabilities.mappedServableEncodings, so they cannot tell different stories;planInputtakes theWeightFormthe load will use. - Kernel-support matrix: mapped serving section — the generated matrix now shows, per platform,
which formats serve from a mapping (all seven on Android
native-jni-directand JVMffm-rowmajor; empty cells are the documented gaps). - "The DSL is compute" (#1194):
the architecture principle behind all of the above as a teachable explanation page — network
definitions describe computation only; memory intent lives in
WeightFormat the load boundary, priced by the plan, honored-or-visibly-rejected by resolvers, with runtime dispatch holding no memory policy.
- Cross-order kernel results (row-major vs feed-order) are numerically equivalent, not bit-exact:
under
-O3 -ffast-mathcompilers may contract float accumulation differently per loop shape (measured 2 ULP on clang/arm64, more under MSVC auto-vectorization). Integer-dot formats (Q4_K, Q6_K) currently match exactly, but only threaded-vs-single-threaded identity is contractual. The parity suites encode this. - The M2-A5 measurement harness gained
residency=heap|mappedand reports mapped vs heap weight bytes and the packed-kernel fallback count.
- The vendored NeoGPU ternary NEON kernel
(
skainet-backends/skainet-backend-native-cpu/native/src/vendor/neogpu/hs_ml_ternary_neon.c, © 2024 NeoGPU Contributors, MIT, byte-identical to upstream anjaustin/neogpu @0846b24) ships unchanged in this release; REUSE metadata andMETA-INF/THIRD-PARTY-NOTICES.mdin the published artifacts carry the attribution (#1166).
Headline: the SKEEP-003 memory & storage architecture, complete — from accepted proposal to shipped system.
One storage model (Storage / Scope / Format / Layout / TensorView), resolver-owned decisions
(what form a weight takes, where its bytes live — never decided by the model author), scope-recycled eager
execution (flat-memory decode), and a compile lane that carries what the runtime decides into the exported
MLIR and .irpa. The version jump (0.40.1 → 0.49.0, ~100 merged PRs) is deliberate: this is the release
downstream repositories (SKaiNET-transformers, the IREE conformity pipeline) should build on, and it
removes every façade the architecture replaced. See Breaking changes below for the migration map.
- The three legacy loader axes are gone (#1159):
QuantPolicy,StagingPolicyandWeightOrientationare deleted. The loader takes oneWeightForm(encoding, order, shape, residency)(uniformweightFormor per-tensorweightFormFor); migration:DEQUANTIZE_TO_FP32→EncodingRequest.DequantizeTo(FP32),StagingPolicy.MAPPED→WeightForm(residency = WeightResidency.MAPPED),WeightOrientation.OUT_IN→WeightShapeOrientation.OUT_IN.AndroidGguf.loadertakes aWeightForm(default mapped residency). - The dead placement machinery is gone (#1142):
@Place/@Weights(declared, retained, read by nothing),sk.ainet.lang.tensor.storage.MemoryPlanner,StorageSpec, andPlacement.residency/Residency. Lifetime isScopeKind; weight staging isWeightForm.WeightResidency; placement is decided byAllocationResolver(see Added).Placementitself stays (KV-cache stores carry it);@KvCache/@KvCacheBypassmoved toKvCacheAnnotations.kt. - The
skainet.tensor_encodingsmodule attribute is gone (#1179): replaced by the machine-readableskainet.tensor_layouts({kind, block_elems, block_bytes, bits, block_order}); nothing outside this repository read the old names dictionary. LogicalDTypeis deprecated end to end in favour ofDType(#1014); removal at the next major.
- The memory model (SKEEP-003 M0 "know before you load" / M1 "flat decode" / M2 "1.58-bit on a 2 GB board")
(#1001,
#1002,
#1003; slices #1004–#1042):
Storage(Heap/OffHeap/Mapped, ownership + liveness — a use-after-free is a loudStorageClosedException),Scope(ModelScope/ForwardScopeslab withreset()),Format(dtype, encoding),Layout(strides/offset/block geometry),TensorViewwithprepack()as the visible relayout andmaterialize()as the single copy point;TraceSinkevents (allocations, scope resets, adapter insertions);MemoryPlan/MemoryPlansheader-only planning with budgets, suggestions and a plan-vs-actual check; theskainet-planCLI;PlannerProfile(MOBILE_2GBwith automatic KV quantization — andstrict, so a missing kernel refuses instead of silently costing several times the weight, #1128); Android mmap loading + device fit checks; KV cache preallocated in model scope with declared formats; kernel dispatch on declared formats (KernelKey, registry-backed capabilities); the decode harness and M2 acceptance runs. - Weight forms — one resolved decision instead of three caller flags
(#1109 arc, #1114–#1120):
WeightForm(encoding × byte order × shape × residency) resolved byWeightFormResolverfrom what the file holds × the profile × what the backend's kernels can feed; the loader honours it, the plan prices it (a resolved dequantization shows in the table, not at the OOM), conversions are traced, and packed weights can load directly in kernel-feed order (#1120). - Placement is resolver-owned (#1133 →
#1142–#1144):
AllocationResolver.resolve(weight, profile, platform)decides memory domain and scope (mapping requires: the form asks, the platform can, the bytes are the file's bytes);AllocationResolver.explain()renders every decision with its reason pre-load;ResolvedGgufwires plan → load with the documented user-wins precedence (per-tensorweightFormFor> uniformweightForm> resolver). - Scope-recycled eager execution (#1135 →
#1145/#1146/#1173):
ExecutionContext.memoryScopeis consulted by tensor creation and op outputs (TensorDataFactory.adoptFloatArray,ScopedTensorDataFactory), soctx.forwardScope(slabFloats) { … }gives steady-state decode that allocates zero new slab bytes per step; the FP32 fast paths and the JVM Panama vector kernels are offset-aware, so slab-backed tensors keep SIMD speed. - Model-footprint analysis for GGUF, safetensors and ONNX
(#1169): header-only
planInputfor all three formats (ONNXexternal_datasidecars priced correctly — multi-GB models no longer report ~0 bytes; sizesLong-safe), andPlannerProfile.EDGEfor embedded devices where the budget is the usable RAM. "Will it fit in ~2.1 GB?" is answered in seconds, without reading a tensor payload. - The compile lane carries what the runtime decides
(#1147 → #1178/#1179/#1180):
TensorRefcarries tensor identity (TraceSession.identify, registered fromtrainableParameters()), the encoding object (block size intact) and packed block order across the trace→graph boundary; the emitted module header declares structural facts per tensor (skainet.tensor_layouts);ExternalParameterRefdeclares block order to the.irpaconsumer;HloGenerator.generate(target = …)runs the optimizer pipeline on the production path withLayoutAssignmentPass(rank-2 packed weights get kernel-feed order; the tape's carried facts are never overridden), and theResolvedComputeGraphseams surface exactly the decisions made. - BitNet / ternary compute track (#1033, #1040/#1041, #1136–#1141, #1150):
ternary encodings (
TQ1_0/TQ2_0,BITNET_B1_58,BITNET_PLANESmulti-plane packing), i2s GGUF import, the vendored NeoGPU ternary f32 NEON kernel (MIT, verbatim) exposed through FFM, JNI and Kotlin/Native including a fused lm_head kernel, requant adapters, NEON BitNet packing, and ternary benchmarks + getting-started docs. - Iris dataset provider (#1044, #1101, contributed by @AjithGoveas): the embedded 150-row dataset used by the new Android classifier tutorial.
- Sliding-window KV SDPA (#1036) and KV formats declared by the store (#1077).
- Packed block order end to end: feed-order bytes in a type claiming canonical order decoded to
plausible garbage (#1124,
#1126);
TensorDatanow declares itsBlockOrderand every reader agrees on the same bytes. - Packed ternary weights reach dispatch through the Wᵀ marker — ANY packed block storage routes through the marker path (#1136, #1181).
- M2 acceptance page-fault flake (#1107); safetensors dtype mapper no longer prints a WARNING into stdout mid-parse (#1169).
apiCheckhas its own named PR leg (test (api-compatibility)) instead of hiding inside golden-parity (#1176), after a stale dump reacheddevelopunnoticed (#1174); branch protection ondevelopnow requires the fullbuild-jobaggregator, admins included.- Android per-target API dumps dropped (jvm + klib only, #1111).
- The memory model as built:
explanation/memory-model.adoc,explanation/packed-weight-layout.adoc(#1106 — design drafts underdocs/design/retired in favour of Antora pages),explanation/virtual-tensors.adoc(the ML Drift-style split, with diagrams), and SKEEP-003a (skeep/003a-placement-and-planning-resolution.adoc) recording the P7/P8 resolutions. - Tutorials whose code cannot rot: the Android classifier getting-started and the ternary
getting-started, with snippets compiled and executed in CI by
skainet-docs-samples(the Iris training loop asserts held-out accuracy ≥ 0.80). - SKEEP-003 accepted (#932) with its
thirteen design decisions; how-to: plan a model's memory before loading it, including the
embedded-device (
edge) verdict.
Headline: correctness hotfix — silently wrong output, not a crash. DefaultCpuOps.transpose() for packed quantized weights (Q4_0/Q5_0/Q5_1/Q8_0/Q4_K/Q5_K/Q6_K) performed a shape-only relabel instead of a real block-grid byte permutation whenever a row spanned more than one quant block (blocksPerInputDim > 1 — true of virtually every real model). ops.matmul(x, ops.transpose(W)) fed the packed-quant kernels bytes in the wrong order across all three kernel tiers — scalar, Panama-vector, and native (FFM/JNI) — silently producing wrong numbers, sometimes all-zero output, with no exception raised. Upgrading is strongly recommended for anyone using packed-quantized weights with ops.transpose().
- Packed-quant
transpose()silently corrupted matmul output (#968, #969) —DefaultCpuOps.transpose()swapped the tensor's shape metadata without physically reordering the underlyingpackedDatabytes, on the assumption that the packed-quant matmul kernels index those bytes block-major regardless of layout. That assumption only holds when there is a single quant block per row (blocksPerInputDim == 1); for any wider row the canonical (row-major) and kernel-native block orderings are literal transposes of the(outputDim, blocksPerInputDim)block grid and do not coincide, so a freshly-loaded weight run throughops.transpose()fed the scalar, Panama-vector, and native (FFM/JNI) kernel tiers alike bytes in the wrong order — for all seven packed formats (Q4_0/Q5_0/Q5_1/Q8_0/Q4_K/Q5_K/Q6_K).transpose()now performs a realO(bytes)block-grid permutation (transposePackedBlocks); a misaligned packed tensor (inputDimnot a multiple of the format's block size) now throwsIllegalArgumentExceptioninstead of silently truncating a partial trailing block.DefaultCpuOpsJvm's separate, independently-buggy shape-swap-only interception forQ4_KTensorDatais removed, falling through to the shared corrected implementation. Caught by a new ground-truth regression test (NativeLazyTransposeGroundTruthReproTest) that dequants each packed format's canonical and kernel-native byte layouts independently and checks both the classic (transpose+ matmul) and pre-transposed paths against that ground truth, per format — the kind of test the original "same bytes, new shape" optimization lacked.
Headline: big models fit on real devices, and SKaiNET reaches iOS/macOS
natively. Off-heap/mmap tensor storage lets Android load models beyond the
hard ART heap cap by paging weight bytes from mapped files instead of the
managed heap, and a compounding GGUF dequantization bug that transiently
needed >12 GB heap for a 1.1B Q4_K_M model is fixed down to a ~1.05x-of-dense
floor. Q5_0/Q5_1 packed matmul reaches the native tier (FFM, Kotlin/Native,
JNI) for the first time, and skainet-backend-native-cpu now publishes
iOS/macOS Kotlin/Native targets whose single Apple arm64 archive dispatches
FEAT_DotProd at runtime — one build serves A12 through M-series.
- Off-heap / mmap tensor storage on Android
(#921, SKEEP-002/SKEEP-003
slice): the java.nio memory-mapped storage that existed jvmMain-only is now shared
source between the JVM and Android compilations (
FileChannel.mapis API 1 — no JNI):MmapFloatTensorData/MmapTensorSource(skainet-lang-core),JvmMappedMemoryChunk,MappedRandomAccessSourceand theBufferHandle.FileBackedresolverJvmFileBackedResolver(skainet-io-core). NewMappedGgufWeights(skainet-io-gguf, JVM+Android) opens a GGUF once, maps it read-only, and serves dense F32 tensors as zero-heap mapped views, any tensor as aFileBackedTensorStoragedescriptor, and packed payloads as heap bytes for the existing kernels. Weight bytes live in file-backed pages the OS pages in/out — outside the hard ART heap cap that limited practical model size on Android. Verified host-side (no device required): a 640 MB dense model loads and reads through mapped views with 1.4 MB of managed-heap allocation (0.0022x of the dense size; per-thread allocation counters), and the Android compilation is exercised by newandroidHostTestsuites (96 MB payload, ~240 KB used-heap growth). Files over 2 GB are rejected fast (single-region mapping); windowed mapping is a follow-up under SKEEP-003's IO pipeline improvement. - Native Q5_0 / Q5_1 packed matmul kernels (FFM, Kotlin/Native, JNI). 0.39.0 shipped
packed GGUF loading for Q5_0/Q5_1 plus scalar + Panama kernels, but the native tier had
no Q5_x kernels — on the JVM the registry cascaded to Panama (50), and on Kotlin/Native and
Android the formats ran on the priority-0 scalar floor. New
skainet_q5_0_matmul/skainet_q5_1_matmulC kernels (plain NEON, no dotprod/i8mm requirement — runs on every AArch64 core) expand theqhhigh-bit plane with a per-lanevtstq_u8bit test and fold the dequant algebraically (d*(dot - 16*Σx)for Q5_0,d*dot + m*Σxfor Q5_1) so the per-block input sum hoists out of the output-row loop. Wired into all three consumers: the FFMNativeKernelProvider(JVM), the cinteropNativeKnKernelProvider(Kotlin/Native), and the Android JNI bridge (JniKernels.q50Matmul/q51Matmul+JniKernelProvider), each with parity tests against the scalar references. Unblocks the packed Q5_1 path forfunctiongemma-270m"Q5_K_M" checkpoints (whose attention/FFN weights are Q5_1) underNATIVE_OPTIMIZED— see SKaiNET-transformers#170. (#708) skainet-backend-native-cpupublishes Apple Kotlin/Native targets —iosArm64,iosSimulatorArm64, andmacosArm64klibs with the Mach-O kernel static archive embedded via cinterop, exactly like the Linux pair (#959, iOS kernel track of #920). Archives are built by new Apple CMake lanes on a macOS host (platform SDKs, no-march— the #958 runtime FEAT_DotProd dispatch serves A12 through M-series from one device archive) or injected in CI via-PskainetKernelsIosArm64Dir/-PskainetKernelsIosSimulatorArm64Dir/-PskainetKernelsMacosArm64Dir. The shared nativeTest parity suites now also run asmacosArm64Test(native) andiosSimulatorArm64Test(simulator) on the macos-14 PR lane. On non-macOS hosts the Apple task family is disabled (ubuntu CI unaffected). Registration on K/N remains manual viainstallNativeKernels()— Apple consumers call it once at startup, same as Linux.
- GGUF
DEQUANTIZE_TO_FP32no longer over-allocates (#782): loading a 1.1B Q4_K_M transiently needed >12 GB heap against a ~4.4 GB dense-FP32 floor. Three compounding causes, all inskainet-io-gguf: (1) the legacyGGUFReadereagerly materialized every tensor payload as a boxedList<Any>at parse time — measured at 41x the payload size in allocations (~26 GB for a 637 MB file); payloads are now constant-space lazy views that decode elements on access (sameList<Any>API, same contents). (2) every dense tensor paid a full-size defensivecopyOfin the tensor factory on top of the dequant intermediate —StreamingGgufParametersLoadernow wraps its loader-owned arrays zero-copy (ctx.wrapFloatArray). (3) the K-quant kernels allocated per-blockcopyOfRangescratch — they now index the source buffer directly, so a full-tensor dequant allocates exactly the destinationFloatArray.StreamingGgufParametersLoaderalso gains an optionalquantPolicyparameter:DEQUANTIZE_TO_FP32streams each quantized tensor block-by-block straight into its destination array (peak transient per tensor = the packed source bytes; measured: eager FP32 load of a synthetic multi-tensor model allocates 1.38x the FP32 total vs 2.1-2.3x for the historical copy chain, with peak live ≈ 1.05x). The default (NATIVE_OPTIMIZED) keeps the loader's historical packed-block behavior bit-for-bit; a parity test pins the dequant path to the packed accessors bit-exactly across all seven supported quant formats.
- Apple arm64 runtime FEAT_DotProd dispatch for the Q4_K/Q6_K C kernels
(
skainet-backend-native-cpu, #958, part of the iOS kernel track of #920). Apple builds now compile at the SDK-default arm64 baseline — a Kotlin/Native klib embeds exactly one static archive, and Apple A12 (iPhone XS/XR, still iOS-supported) lacks FEAT_DotProd while A13+/M-series have it — with the dotprod hot bodies compiled twice (baseline +target("dotprod")-attributed) and selected once per matmul via a cachedsysctlbyname("hw.optional.arm.FEAT_DotProd")probe. Non-Apple builds keep the compile-time-marchguard as the only mechanism; Linux codegen is unchanged (qemu parity green,sdotverified in the cross archive). The existing macOS FFM dylib moves from TU-level dotprod to baseline+dispatch — runtime-equivalent on every Apple Silicon Mac. iOS builds are static-only (SKAINET_STATIC_ONLY, auto-on forCMAKE_SYSTEM_NAME=iOS).
- Releases embed the Apple kernel archives (#959):
publish.yml's macos leg builds the three Mach-O static archives (with an
nm/objdumpsdotassertion guarding the #958 dispatch body against clang's silent unknown-feature ignore), uploads them fail-loud, and the publish job verifies and injects them via the-PskainetKernels{IosArm64,IosSimulatorArm64,MacosArm64}Dirproperties — the same verified-artifact-or-fail contract as the Linux ELF archives.
- Kernel support matrix gains the
native-cinteroptier (Native·linux + Native·apple, the 7 packed-quant formats) — the Native·linux column was under-reported asscalarbefore; both native columns now reflectNativeKnKernelProvider(#959). The eager-backends mindmap and theinstallNativeKernels()KDoc document the manual-registration contract and the Apple A12 dispatch fallback.
Headline: eager overhead off the JVM is gone. The eager CPU ops gain
primitive FP32 fast paths, removing the per-element allocation/boxing overhead
that dominated on-device LLM decode (83% of end-to-end time on a Pixel 8a even
with NEON matmul), and DirectCpuExecutionContext.ops is cached instead of
rebuilt per access. The README now points LLM users to SKaiNET-transformers.
- README points LLM users to SKaiNET-transformers
(#923): a callout under
"Start in 5 minutes" says plainly that LLM inference lives in the
SKaiNET-transformers repository — this repo is the engine underneath — and names
the
sk.ainet.transformersartifacts and BOM to depend on.
- Primitive FP32 fast paths for the eager CPU ops (
skainet-backend-cpu, #949). The generic paths inDefaultCpuOpspaid, per element: twoIntArrayallocations for broadcast index mapping, a vararg-spread boxeddata.get, a KClasswhen (dtype)comparison, and a boxed lambda round-trip. On ART that overhead dominated LLM decode — 83% of end-to-end SmolLM2-135M decode on a Pixel 8a was non-matmul overhead even with the NEON backend doing every matmul. The hot ops now run flat primitive loops over the denseFloatArraybuffer (falling back to the generic path for other dtypes/layouts): binary/scalar arithmetic (incl. last-dim bias broadcast, mirroring the JVM vector path's coverage), the activation family (relu/sigmoid/silu/gelu/…), unary math (sqrt/exp/log/sin/cos/tanh/pow/…), softmax and logSoftmax along the last dim (also removing an O(n²)-per-slice max/denominator recompute), sum/mean reductions, concat (block copy), and reshape/flatten (buffer copy). Benefits every non-JVM target — Android, Kotlin/Native, JS/Wasm; the JVM ops class keeps its Panama/FFM specializations on top. DirectCpuExecutionContext.opsis cached. The getter previously constructed a fresh ops instance on every access, re-running per-instance lazy kernel resolution in the eager hot loop (#949).
Headline: on-device AI on Android becomes real. A JNI NEON kernel backend
(skainet-backend-jni-cpu) brings hand-tuned ARM matmul to Android — where the
FFM provider can never run — measured at ~24 tok/s SmolLM2-135M Q8_0 decode on a
Pixel 8a versus ~3.8 scalar (6.4x), clearing the on-device usability bar. The
release also hardens the GGUF load path on Android (streaming instead of
full-file heap loads), makes published Kotlin/Native kernel klibs linkable, and
lands a batch of tensor-storage correctness fixes.
- Release workflow publishes the
skainet-backend-jni-cpuAAR../gradlew publishnow includes the Android JNI kernel module, whose AAR carries NDK-cross-built.sos; the publish job gains Android SDK + a pinned NDK setup so the native build succeeds on the release runner (previously the release would ship no JNI artifact, or fail). The NDK version is pinned ingradle/libs.versions.toml(android-ndk) and referenced by the module'sndkVersionand the workflow, so local and CI builds are reproducible. (#946)
- NEON body for the Q4_0 matmul kernel.
skainet_q4_0_matmulwas the only priority quant format without a SIMD path (scalar C only, while q8_0/q4k/q5k/q6k had NEON). It now unpacks the split-layout nibbles withvand/vshr, re-centres in the signed int8 domain, and widens to f32 FMA lanes — plain NEON with no dotprod/i8mm requirement, so it runs on every AArch64 core, and the same block-outer/row-inner loop order as q8_0 (sequential weight reads; per-row accumulation order unchanged). Verified: 27/27 kernel tests green under qemu-aarch64 (cross-built-march=armv8.2-a+fp16+dotprod, K/N-bundled gcc 8.3),fmlaconfirmed in the archive's disassembly; a new Kotlin/Native Q4_0 parity test closes the gap where the aarch64 lane had no Q4_0 coverage at all. Part of the mobile-kernels effort (#920). skainet-backend-jni-cpu: Android JNI bridge for the native NEON kernels. ART has nojava.lang.foreign, so the priority-100 FFM provider can never run on Android — until now Android inference ran on the priority-0 scalar floor. The new AAR ships the same C kernel sources via NDK/CMake with thin JNI shims (GetPrimitiveArrayCritical, zero-copy pins) and a priority-100JniKernelProviderdiscovered viaServiceLoader(which ART supports; the Android ops factory now installs discovered providers exactly like the JVM does). Two.sotiers are built from the same sources and selected at load time from/proc/cpuinfo: baselinearmv8-a(NEON, runs on every arm64 core — 0 dot-product instructions, verified by disassembly) andarmv8.2-a+fp16+dotprod(enables thevdotq_s32q4k/q6k paths — would SIGILL on Cortex-A53-class cores, hence the gate). Q8_0/Q4_0/Q4_K/Q5_K/Q6_K bridged; 16 KB-page-aligned.sos (Android 15+); consumer R8 rules keep the ServiceLoader entry in release builds. On-device parity tests included (androidTest, vs the scalar references). Part of the mobile-kernels effort (#920).
- GGUF tensors report their real encodings and sizes in
TensorStorage.StreamingGGUFReadermapped only Q4_K/Q8_0 to dedicatedTensorEncodings; Q4_0, Q5_0, Q5_1, Q5_K and Q6_K — all of which have encoding objects and CPU kernels — fell through toOpaque(name, 0), whose zero byte count short-circuitsTensorStorage.physicalBytesand silently corrupted every memory report and compression ratio. All seven quant formats now map to their encodings, and genuinely unknown types carry the tensor's real byte count inOpaque. (#928) - Memory-copy diagnostics attribute copies to their source.
MemoryTracker.recordCopydiscarded thesourceNameevery instrumented call site passes; reports now carry a per-source breakdown (copiesBySource: Map<String, CopySourceStat>, included in the report's text form, sorted by volume).ActiveMemoryTracker.currentis now@Volatilewith an honest thread-safety contract in its docs; making the tracker per-execution-context instead of a process-wide hook is part of the SKEEP-003 storage-model discussion. (#931) TensorStorageFactoryownership labels are now truthful.borrowFloatArrayre-encoded the floats into a private byte copy and labeled itBorroweddespite its "(zero-copy)" doc — it is now deprecated (delegating tofromFloatArray) and honestly returnsOwned;fromTensorData's doc claimed "borrowed (not copied)" while its dense branches copy — the contract is now documented per branch (packed Q4_K/Q8_0 genuinely borrow zero-copy, dense arrays convert to owned bytes) and pinned by ownership + mutation-visibility tests. (#927)TensorStoragetransfer API can materialize its own placements.copyMaterialize()threw forAliasedhandles (now resolved directly, producing an independent owned copy of the slice) and forFileBacked— meaningcopyToHost()could not bringMMAP_WEIGHTSstorage to the heap, the one transfer the storage layer was designed around. Both methods gain aBufferResolveroverload that reads file-backed regions through a configured resolver;DeviceResidentremains unsupported with an error that says why. (#929)- Published Kotlin/Native klibs for
skainet-backend-native-cpunow carry their machine code. The static kernel archive was attached via project-locallinkerOpts, which does not travel with a published klib — downstream K/N consumers of the-linuxx64/-linuxarm64artifacts could not link (unresolvedskainet_*symbols). The archive is now EMBEDDED into the cinterop klib (-staticLibrary/-libraryPath), conditional on a correct-architecture ELF archive being available; bindings-only builds (e.g. macOS dev hosts) warn loudly instead of staying silent. The release workflow's ubuntu leg now also cross-builds and uploads both linux static archives, and the macOS publish job injects them, so released klibs embed real code. In-repo K/N test binaries link purely from the embedded klib — the consumer-link scenario is what the test suite now exercises. (#941) TensorData.copyToFloatArray()default implementation works for rank >= 2. It used to iterate a single flat index into the varargget, tripping every implementation's one-index-per-dimension arity check — a latent trap for any implementation that didn't override it. The default now unravels flat positions into per-dimension indices (row-major); a contract test exercises the default at ranks 1–3. (#930)- Streaming GGUF loads fail fast on unsupported tensor types instead of silently skipping them.
StreamingGgufParametersLoaderused to emit aSKIPprogress string for any tensor type outside itswhenand deliver a model with silently missing weights — the failure then surfaced far away in the forward pass (the load-time half of the Q4_1 report in #654). An eager pre-scan of the tensor directory now throwsIllegalArgumentExceptionbefore any tensor is delivered, naming every offending tensor, its type (including raw values for unknown types), and the supported set; the per-tensorelseis a hard error guarding against drift from the newSUPPORTED_TENSOR_TYPEScompanion set. Behavior change: files that previously "loaded" with skipped tensors now fail at load — the legacyGgufParametersLoaderalready behaved this way. Q4_0 / Q5_0 / Q5_1 — which had packedTensorDataand matmul kernels but were missing from the loader — now load as packed blocks instead of being skipped. Closes #919. - Random file access on Android: streaming model loads instead of full-file heap loads.
createRandomAccessSourceunconditionally returnednullon Android inskainet-io-gguf,skainet-io-safetensorsandskainet-io-onnx, forcing every load through the legacy materialise-the-whole-file path — on a real device a 138 MiB GGUF then OOMs the ART heap (capped at 256/512 MB) before tensors are even built. A newAndroidRandomAccessSourceinskainet-io-core(androidMain, positionalFileChannelreads — thread-safe, API 1+) now backs all three actuals, makingStreamingGGUFReader/streaming loaders reachable on Android; this also un-breaksTokenizerFactory.fromGguf, which needsStreamingGGUFReader.fields. Android host-side unit tests (withHostTest {}, a first in the repo) cover the read contract including concurrent positional reads. Closes #922.
-
Narrow-float (BF16 + FP16) weights kept packed. A shared
NarrowFloatCodeclayer (Bf16Codec/Fp16Codec) plusNarrowFloatDenseTensorData/Fp16DenseTensorDatalet SafeTensors F16 and GGUF F16/BF16 weights loadKEEP_NATIVE— two bytes per element at rest instead of widening to FP32 at load.DefaultCpuOpsJvmdispatches to format-specific matmul kernels by codec, so the packed weight reaches the kernel rather than a widened copy. Narrow floats are a storage width only: kernels widen to f32 lanes, accumulate in f32, and narrow on store. -
Native (FFM) FP16 matmul kernel.
skainet_fp16_matmuljoins the existing BF16 kernel inskainet-backend-native-cpu, wired throughNativeKernelProvider.matmulFp16(). Until now the provider carriedmatmulBf16but no FP16 counterpart, so BF16 resolved to the native kernel at priority 100 while FP16 silently cascaded to the JVM Panama kernel at 50 — which read as a slow kernel and was a missing one.KernelProvider.supportsgains the matching"Float16"arm, absent while every other matmul dtype was present. -
First-class dynamic dimensions (
Dim). A newsk.ainet.lang.tensor.Dimvocabulary makes "dynamic extent" explicit instead of an overloaded-1:Dim.DYNAMICis a reserved sentinel (Int.MIN_VALUE) distinct from reshape's-1= infer, with the dynamic-aware shape arithmetic (concat,compatible,render,isDynamic/isStatic) centralized in one place rather than scatteredextent < 0guards.ShapegainshasDynamic(),isDynamic(axis),dynamicAxes, and itsvolumenow throws on a dynamic shape (an unknown extent has no materializable element count) instead of returning a corrupt product. The slice/range DSL is dynamic-aware too:all()over a dynamic axis is a valid symbolic full-axis, and reshape passes a dynamic target through unchanged. Theskainet-compile-hloemitter now shares this one sentinel (TypeMapper.DYNAMIC_DIMaliasesDim.DYNAMIC), so tracer and emitter agree by construction. -
Dynamic-shape-safe StableHLO emission (streaming KV-cache decode). The
skainet-compile-hloemitter now renders a-1tensor extent as an MLIR?(dynamic) dimension and emits op forms thatiree-compileaccepts under a dynamic dim, so a single compiled vmfb serves every autoregressive decode step (growing KV cache) instead of one fixed cache length. A newTypeMapper.DYNAMIC_DIM = -1marker plus aList<Int>.hasDynamic()predicate gate the dynamic paths, so every static graph is emitted byte-for-byte unchanged. Verified: one dynamic SDPA vmfb runs at key lengths 3 and 17, and the full FunctionGemmawith_pastdecode graph (real weights, dynamic1x{nKV}x?x256cache) self-compiles from the DSL to a CPU vmfb — a graph that could not be compiled before. -
Dynamic-shape-safe trace finalization.
TraceToGraphBuilder.extractFloatArrayno longer probesTensor.volumefor non-dense data, so a dynamic-shaped graph input (e.g. a?KV-cache tensor) is left as an input instead of tripping the (correctly) throwingvolumeon an unknown extent. This lets a decode graph with dynamic caches finalize to aComputeGraphand compile — verified with the real Moonshine v2with_pastdecoder (dynamic self and cross caches,1x8x?x40) self-compiling from the DSL to a vmfb. -
Allocation-free shape-only tracing (
VoidTensorOps). The trace-time op set now propagates shapes through aShapeOnlyTensorDatathat carries aShapebut allocates no backing buffer, so a dynamic (-1) extent flows through a whole decode trace instead of throwingNegativeArraySizeExceptionwhen a real buffer of negative size is allocated. This is what lets a real KV-cache seq dim be traced as dynamic end-to-end (rather than via a sentinel-dimension + post-emit text substitution).
- SDPA scale folded into Q.
AttentionOperationsConverternow applies the attention scale as a scalar constant multiplied into Q before the QKdot_general((q·s)@kᵀ ≡ scores·s, exact), instead of a dense splat constant sized to the full scores shape. This drops a scores-sized constant from every attention graph and, crucially, avoids an invalid dynamic-shape splat when the key/cache dim is?. - Softmax broadcasts are dynamic-shape-safe. When the softmax/scores shape carries a dynamic dim,
AttentionOperationsConverterandActivationOperationsConverterbroadcast the reduced max/sum withstablehlo.dynamic_broadcast_in_dim(runtime shape operand built viaget_dimension_size+concatenate) instead of a staticbroadcast_in_dim. Static graphs keep the explicitbroadcast_in_dimunchanged. - Identity reshapes/slices are elided.
ShapeOperationsConverterreturns the operand SSA value with no emitted op when a reshape's input and result types are identical, or aslice/narrowcovers the full extent of every axis (the KV-cache "cache-as-output-sink" and full-cache head-expansion patterns). Besides being a no-op, this is the only valid lowering on a dynamic axis — a staticstablehlo.slicecannot express a full-extent bound on a?dim (its limit would be the-1extent, e.g.0:-1:1). - Concatenate propagates dynamic extents. Both the trace-time shape inference
(
VoidTensorOps.calculateConcatShape) and the emitter (ShapeOperationsConverterconcat) now keep the concatenated axis dynamic when any operand's extent there is dynamic, instead of numerically summing it (which turned a growing cache? ++ 1into a bogus static0). - Narrow-float weights transpose for free.
NarrowFloatInputMajorTensorDatastores a rank-2 narrow weight input-major, so the[out, in]→[in, out]transpose thatLinear.onForwardperforms on every call is a zero-copy reinterpretation of the same buffer. Projections are stored[out, in]but the narrow matmul dispatch needs[in, out], and transposing a row-major narrow tensor previously walked it elementwise through boxedget()and widened to FP32 — 4.4 s for a 4096x11008 projection, per weight, per token, which madeKEEP_NATIVEslower than not using it. A row-major narrow buffer deliberately still takes the generic path: swapping its shape would silently yield a different matrix rather than the transpose. - Native narrow-float kernels read the weight once per matmul. Both the BF16 and FP16 kernels tile
the
jdimension atm > 1and widen each weight row once per tile, instead of walking the whole weight matrix once per row of the input. Accumulation into any output element stayspascending, so results are bit-identical to the previous formulation. Atm == 1both keep the straight pass — there is nothing to amortize and tiling costs ~15% there. Fp16Codec.decodeis straight-line. The subnormal arm no longer renormalizes in a data-dependent loop; binary16 subnormals aremant * 2⁻²⁴with both factors exact in FP32, so one multiply lets the hardware renormalize. A NaN now decodes quiet, matchingencode(which already never emitted a signaling binary16 NaN) and the hardware conversion the JVM kernel uses. This changes 1022 patterns — the signaling NaNs — and nothing else; without it the JVM and every other target would disagree on them.
- FP16 matmul was 2–18x slower than the FP32 SGEMM it replaces. The cause was dispatch, not
arithmetic:
NativeKernelProviderhad nomatmulFp16(), so FP16 fell through to the JVM kernel while BF16 ran natively. Head to head the two JVM kernels are within ~15% of each other. FP16 is now 1.5–1.7x faster than FP32. - CI and supply-chain hardening. Least-privilege permissions on the build workflow, the docs MathJax
npm install pinned by version, and a
logback-classicbump.
Measured on an i7-9750H (AVX2), OpenJDK 21, median ms per call, 4096x11008 projection:
| batch 1 | batch 16 | |
|---|---|---|
| FP32 SGEMM | 108.7 | 271.1 |
| BF16 | 57.4 | 143.5 |
| FP16 | 71.2 | 159.3 |
Both narrow formats now beat the FP32 SGEMM — BF16 by 1.8–1.9x, FP16 by 1.5–1.7x. Note these kernels
are compute-bound on the FMA chain at batch 16, not bandwidth-bound: cutting weight traffic 16x bought
only 9–19%, so the next win is a blocked microkernel or bfdot/bfmmla on ARMv8.6-A+, not more layout
work.
Lstmlayer. Single-layer, batch-first LSTM mirroringGru's unroll-at-trace-time design and built from existing primitives only (matmul/narrow/sigmoid/tanh/multiply) — no newTensorOpsop, and it traces to StableHLO with no dedicated converter. Gate orderi,f,g,oand dual biases matchtorch.nn.LSTM. AddsLstmState(h, c)with an explicit caller-ownedstep(xt, state, ctx)API — required by transducer prediction networks (RNN-T/TDT) and exactly the shape that lowers to a fixed-shape single-step StableHLO graph — plus aninitialState(batch, ctx, dtype)helper. (PR #824)- Learning-rate schedules and mutable optimizer
lr.AdamOptimizerandSgdOptimizerexposelras a publicvar, so training loops can adjust it between steps without recreating the optimizer and losing the moment estimates. A statelessLrSchedulefun interface plus alinearWarmupCosineDecayfactory cover the GPT-style pretraining recipe: linear ramp frominitialLrtopeakLrover the warmup steps, then cosine decay tominLr. (PR #866, issue #865) - BREAKING (Java callers): optional bias in
Linear.initBiasis now nullable with anulldefault, creating a bias-less projection (y = x W^T) — the equivalent of PyTorch'snn.Linear(bias=False), needed for GPT-styleqkv_bias=falseprojections and weight-tied output heads. A bias-less layer registers only its weight parameter, so parameter counts and checkpoints match architectures defined without bias. Adds abiasOrNull()helper next to the throwingbias()accessor. The@JvmOverloadsoverload set changes: the 4-arg(in, out, weights, bias)convenience overload is replaced by(in, out, weights), so Java code binding the 4-arg overload fails to compile (and breaks binary compatibility) until it moves to the full constructor. Kotlin callers bind the full constructor and are unaffected. (PR #870) Linearisopen. Adapter-style layers (LoRA and similar) can subclass it and augmentonForwardorparamswhile reusing the base projection. No behavior change — the override points were already open viaModule. (PR #875)androidNativetargets for the IO modules.skainet-io-coregainsandroidNativeArm64andandroidNativeArm32(via a 64-bit-onlynative64Mainsource set), andskainet-io-safetensorsgains theandroidNativetarget set; the latter also drops unusedcompile-core/compile-dagdependencies. (PRs #836, #842, #845)- OpenSSF Scorecard workflow and badge. (PR #814)
Dropoutnow actually drops. It was an identity placeholder in both phases; it now applies inverted dropout under a training-phase context — each element is zeroed with probabilitypand survivors are scaled by1/(1-p), so the expected activation is preserved and inference needs no rescaling. The mask is a constant tensor combined with an element-wise multiply, so gradients flow to surviving inputs without a dedicated autograd rule; an injectableRandomenables reproducible masks. Models that relied onDropoutbeing a no-op will see different (correct) training-time activations. (PR #867, issue #861)- Toolchain and dependency bumps. Kotlin 2.4.10 (JVM plugin and
kotlinx.serializationplugin aligned), AGP 9.3.1, Shadow 9.6.1, Kover 0.9.9, JUnit Jupiter 6.1.2, and Logback 1.6.0. No public API or behavior changes. (PRs #818, #819, #820, #826, #829, #840, #856, #873, #874, #811) - Supply-chain hardening of CI and the docs image. All GitHub Actions across every workflow are
now pinned to commit hashes, and the documentation
Dockerfilepins its base image by digest and its npm packages (Antora CLI, site-generator, lunr-extension, mermaid-cli, asciidoctor-kroki) to exact versions from the npm registry, making documentation builds reproducible. (PRs #816, #821, #827, #830, #831, #832, #834, #837, #838, #846, #848, #868) - CI
allTestssplit into parallel per-target jobs, ending the recurring out-of-memory flakes on the combined job. (PR #854) - Docs deployment publishes on release, and fork PRs no longer fail on the comment step. (PR #850)
scaledDotProductAttentionsilently discarded the attention pattern at its default scale. The op'sscaleparameter defaults to0f, documented as meaning1/sqrt(headDim), but the CPU backend applied it literally — every score was multiplied by zero, so the softmax collapsed to a uniform average. The CPU kernel now resolvesscale == 0fto1/sqrt(headDim). Any model calling SDPA without an explicit scale was producing attention-free output. (PR #880, issue #860)- Three autograd bugs that silently froze or crashed training. (PR #877, issues #862, #863, #864)
CrossEntropyLoss: the index-target path built its result host-side viatensorDataFactory+fromData, detaching the tape so no gradient reached the predictions, and the soft-target path only recorded when targets lived on the recording context. Both now compute the NLL with differentiable ops dispatched through the predictions' ops (a constant one-hot for the index path), keeping the tape connected.softmax/logSoftmaxbackward: a negativedim(e.g.softmax(dim = -1)) was passed straight tobroadcastToInput, which reserves-1as a no-unsqueeze sentinel, so the reduced axis was never re-expanded and backward crashed for rank ≥ 3. The dim is now normalized.varianceBackward: the reduced mean and upstream kept the reduced shape, so the subtract/multiply could not broadcast for rank ≥ 2, andNused the total volume instead of the axis size. Both are now expanded over the reduced axis andNis the axis size.
argMaxtraced through thedag {}builder emitted invalid StableHLO.GraphDsl.inferDagOutputSpecshad noargMaxcase, so it echoed operand-0's shape and dtype; the converter then faithfully emitted an integer literal for anf32tensor (rejected byiree-compile) and a reduce whose result kept the reduced dim. The builder now infers a reduced shape with anInt32index dtype, matching theVoidTensorOpspath that was already correct — which is why only the rawdag {}path broke. (PR #878, issue #876)- Legacy
tokenizer.jsonfiles withoutmodel.typefailed to load. Files such asopenai-community/gpt2omit the field;TokenizerFactory.fromTokenizerJsonnow infers the type from structure — amergeslist is unique to BPE — and routes them toQwenByteLevelBpeTokenizerinstead of throwing. (PR #879, issue #858) - CPU
gatherthrew on multi-dimensional indices. An[N, L]index tensor needs one coordinate per dimension for a flatdata[i]access; indices are now read in row-major order via the contiguous buffer, falling back to unravel. (PR #879, issue #859)
- REUSE / SPDX license-compliance setup. Added
REUSE.tomland theLICENSES/directory, areuse-complianceCI workflow (running onmainanddevelop), and a REUSE status badge in the README, making the repository machine-verifiable against the REUSE specification. (PRs #806, #807, #808)
- Kotlin 2.4.0 toolchain. The build now targets Kotlin 2.4.0 (up from 2.3.21), with the supporting toolchain aligned to match: KSP 2.3.10 and Dokka 2.2.0 (Dokka 2.1.0 is incompatible with the Kotlin 2.4.0 Gradle plugin). No public API or behavior changes — this is a compiler and build-toolchain upgrade. (PRs #658, #659, #660)
skainet-datamodule POM coordinates and names aligned with module names. Data-module POMartifactIds and display names now match their Gradle module names, removing the previous mismatches. (PR #793)
ComputeGraphExecutorreplayedpermuteas a plain transpose, dropping the recorded axes. The builtin dispatch groupedpermutewithtranspose/transpose2d, so a tracedpermute(t, axes)executed as a last-two-dims swap — only coincidentally correct for rank-2. Any rank-3 permutation (e.g. multi-head attention's heads/sequence swap[1, 0, 2]in a full-sequence encoder or prefill trace) silently produced the wrong layout; single-token decode paths never hit it, which is why generation workloads didn't surface the bug.permutenow replays with its recordedaxes(the sameList<Int>conventionpermuteBackwardand the StableHLO converter already parse), falling back to the legacy transpose behavior for older traces without axes. Surfaced by the BERT DSL-path encoder work in SKaiNET-transformers, which carries a temporaryLLMFusedOpHandlersoverride to be removed once this fix ships.
-
argMax(tensor, dim)tensor op. Index of the maximum value along a dimension, with ties resolved to the lowest index (numpy/greedy); the reduced dimension is removed (no keepdim). Non-differentiable by design (index selection). LikescaledDotProductAttention, it stays a single op and is lowered at the StableHLO stage rather than needing a new primitive:ArgMaxOperationsConvertercomposesiota+ reduce-maximum+broadcast_in_dim+compare EQselect+ reduce-minimum(the samestablehloprimitives the causal-mask code already emits) — so no variadic reducer region and no new DSL ops. The eager CPU kernel is a scalar reduction-to-index. Indices arei32in the compiled StableHLO; the eager path materializes them as index-valued floats in the input dtype so the result is a portableTensor<T, V>(an i32 payload inside a float tensor is unreadable on Kotlin/Native + Wasm). Unblocks folding an LLM'slogits → token-idsargmax tail into the DSL trace instead of a post-hoc MLIR rewrite. (PR #800)
-
BREAKING (coordinates):
skainet-data-simplenow publishes under its module name. The artifactId changes from the mismatchedsk.ainet.core:skainet-data-basictosk.ainet.core:skainet-data-simple; update your dependency declarations when upgrading past 0.34.0 (BOM consumers only need the new artifactId).skainet-data-transformalso gets a distinct POM name ("skainet data transforms" — it previously duplicated the datasets module's name); its coordinates are unchanged.
- New module
sk.ainet.core:skainet-data-source— URI-backed data sources. One declarative way to get raw data into SKaiNET fromfile://,https://, and Hugging Face (hf://owner/repo/pathandhf+https://…) URIs:DataSourcecontracts +DataSourceUriParser, aDefaultDataSourceResolver(JVM:JvmDataSourceResolverwith artifact materialization/caching viaCachePolicy), streaming of source artifacts with kotlinx-io, and parameterizable Hugging Face auth (token provider — no hard-coded credentials). (PRs #784, #785) - Raw dataset parsers + suspendable data pipeline DSL.
DataFormatParserimplementations for CSV, TSV, JSON arrays/objects, and JSON Lines (.jsonl/.ndjson) produce schema-carryingRawDatasets;rawDataset { from(…); format(…); cachePolicy(…) }builds a dataset straight from a source URI, anddataPipeline<T>()chains named, schema-awaredataTransformerstages as a suspend pipeline. See the new data sources getting-started tutorial. (PR #785) - Dataset operation views and richer batches (
skainet-data-api).Datasetgains deterministic seededshuffle(seed),split(ratio, seed, stratified)(label-stratified splitting),filter/ index-based views, optionalinputShape/outputShapemetadata, and batch/epochFlows.DataBatchnow carries sampleindicesand ametadatamap, and supportsslice(range)over the leading batch dimension. All additions have defaults — existingDatasetimplementations keep compiling; the bundled MNIST / Fashion-MNIST / CIFAR-10 loaders support indexed (non-contiguous) batches, are routed through the source layer, and unsupported platform targets now fail with a clearDatasetLoaderUnsupportedTargetException. (PR #785) - bf16-native DSL → StableHLO export path. A model authored in bf16 in the NN DSL now exports
StableHLO whose weights reach the matmuls as bf16 (required by NPUs that reject fp32 weights). Adds
DtypeForwardPropagationPass(unifies the graph's float dtype end-to-end, coercing float sources whentargetFloatDtypeis set), a width-matched-infbit pattern for softmax/attention max-reduce identities, validstablehlo.convertfunction-form emission, and BF16 support inDenseTensorDataFactory.zeros/placeholder. Verified: a DSL-authored bf16 Moonshine encoder traces to all-bf16 StableHLO and compiles to an aarch64 llvm-cpu vmfb. (PRs #788, #791) - Pluggable per-phase, per-target compile optimization (
skainet-compile-opt). The seam that keeps hardware knowledge out of the agnostic compiler core:CompilePhase { TAPE, DAG, STABLE_HLO },TargetOptimizer(per-phase pass provider), theTargetOptimizersregistry, anddagPipelineFor(target, corePasses). The StableHLO emitter additionally accepts an optional target id andOpGranularityPolicy(+FusedOpAllowList) so per-target fused-vs-decomposed emission decisions are possible; everything defaults to the previous behavior — emission is byte-identical when unused. (PR #791) KernelProfilediagnostics (skainet-backend-cpu). Always-on accumulating profiler over the threeDefaultCpuOps.matmuldispatch paths (quant-NEON / fp32-scalar / generic), read viaKernelProfile.report()— used to localize on-device decode cost.- Contributor Covenant 3.0 Code of Conduct, with GitHub private vulnerability reporting as the contact channel. (PR #790)
- Native CPU K-quant matmul: 2.07× Q4_K on Cortex-A55. Two levers, validated against the Panama
reference and on-board: (1) block-outer / output-row-inner loop order so block-major weight bytes are
read strictly sequentially (the dominant win on in-order cores — the old order made every weight read
a cold cache miss), applied to Q4_K, Q5_K, Q6_K, and Q8_0; (2) ggml-style fused Q8 activation
quantization + int8 dot path (
vdotq_s32on dotprod targets, scalar fallback otherwise) for Q4_K and Q6_K. TinyLlama Q4_K_M on-board decode improved 1.50× end-to-end. Note: the fused int8 path is deliberately lossy (activation quantization, ~1–3% worst-case on uniform-random fixtures); parity tests gate on aggregateRMS(error)/RMS(signal) < 0.03instead of bit-exactness. (PR #787) - NEON kernels verified on real aarch64. Shared
nativeTestsuite now runs the matmul parity tests on linuxX64 and linuxArm64 (cross-built binary under the bundled qemu-aarch64, overridable to a real board); all fp32/q4k/q5k/q6k/q8_0 kernels pass on QEMU and on a physical Cortex-A55, with objdump confirming genuine SIMD (udot/sdot/fmla), not the scalar fallback. (PR #786)
- LayerNorm normalization computed in f32 regardless of model dtype. A bf16 variance (sum of many
bf16 squares) loses enough precision that
sqrt(var + eps)can produce NaN, and some accelerator backends miscompile the low-precision decomposed reduce. Mean/variance/std/divide are now upcast to f32 (standard PyTorch/JAX practice); the gamma/beta affine stays in the model dtype. No-op for f32 models. (PR #791) - Rank-0 tensor types emit as
tensor<elem>. The StableHLO type mapper unconditionally inserted thexshape separator, so scalars produced the malformedtensor<xbf16>thatiree-compilerejects. (PR #791) - Green
./gradlew buildon macOS hosts. Refreshed lagging binary-compatibility API dumps (additive only) and gated the linuxX64/linuxArm64 Kotlin/Native test-link/run tasks off non-Linux hosts (the CMake host build emits Mach-O objects that cannot cross-link into a Linux ELF); klib compilation and publishing untouched, the native NEON parity suite still runs on Linux CI / QEMU / hardware. (PR #789)
- Antora docs image consolidated to the offline
markup-antorabuild. One image shared across the SKaiNET docs projects: offline Mermaid rendering to inline SVG at build time (no Kroki, no network), content-hash diagram caching, rootless-safe, with a build-time Mermaid smoke test. (PR #781) - Data source URIs and the data loader APIs documented, including a new
data-sources-getting-startedtutorial; README reworked to frame StableHLO/MLIR as one of several sibling code-generation backends (next to Arduino/C99 and Minerva) lowering the sameComputeGraph. (PR #776)
- Gradle wrapper 9.6.0 → 9.6.1, logback-classic 1.5.36 → 1.5.37, JUnit Jupiter 6.1.0 → 6.1.1, kotlinx-io-core 0.9.0 → 0.9.1. (PRs #777–#780)
- GRU layer (
sk.ainet.lang.nn.Gru). SKaiNET's first recurrent layer (issue #217): single-layer, unidirectional, batch-first[B, S, D] -> [B, S, H], PyTorch gate order (reset, update, new). Built by composing existing primitives (matmul/add/sigmoid/tanh/narrow/concat) unrolled over the static sequence length at trace time — StableHLO has no loop construct, so any recurrence must unroll. It runs eagerly, is trainable through the standard tape, and exports to StableHLO with no dedicated converter. Also adds agru(hiddenSize) { … }network-DSL builder. (PR #772) upsample2dBilinear + StableHLO export. Adds the Bilinear forward (PyTorch coord map, 4-neighbour blend) and its autodiff backward, and a traceable StableHLO lowering for both Nearest and Bilinear (scale is static at trace time, so everything lowers to fixed reshape/broadcast/dot_general— no runtime index math, nocustom_call). Unblocks export of resize/FPN-style paths. (PR #771)- Seven newly-differentiable ops.
cos,sin,tril,gather,indexSelect,unfold,convTranspose1dnow carry@Diffand have backward rules (with finite-difference parity tests): trig for RoPE,gatherfor embedding lookup,trilfor causal masks, the rest structural. (PR #774) - KSP-generated autodiff-coverage guard. The tracing-wrapper processor now emits
DifferentiableTensorOpsRules.ruleNames(the authoritative@Diffop set); a unit test asserts the execution tape's dispatch covers it, so a differentiable op can no longer ship with a backward rule that is never wired.operators.jsonnow recordsisDifferentiable(+ optionaldiffRuleName), schema-validated. (PR #774)
- Silent gradient drop for
elu,leakyRelu,permute. These were@Diffand had correct backward formulas, but had no arm in the execution tape's trace dispatch, so their gradients fell through tonulland were silently discarded. Now wired (and guarded by the coverage test above);permuteBackwardalso fixed to decode itsaxesattribute as the tracedList<Int>. (PR #774) layerNorm/rmsNorm/batchNormlower to realstablehlo.reduce. The norm converters previously emitted non-compilablereduce_mean/reduce_variancecustom_calls (export-only); they now decompose to realstablehlo.reduce, so all three compile and run on stock IREE (llvm-cpu). (PR #769)
- BREAKING:
TensorOps.sin,TensorOps.cos,TensorOps.convTranspose1dare now abstract. They previously had defaultthrow NotImplementedError(...)bodies; they are abstract so the tracing wrapper records them (and they become differentiable/exportable). Any type implementingTensorOpsdirectly must now override them — both bundled backends (DefaultCpuOpsBase,VoidTensorOps) already do. (PR #774)
- Streaming detokenization preserves word-boundary spaces (
Tokenizer.decodeToken). A generation loop that decodes one token at a time (decode(tokenId)) ran words together ("the process"→"theprocess"): the single-token path delegated to the sequence-levelSentencePieceTokenizer.decode(IntArray), whoseaddSpacePrefixleading-space strip is only correct once per sequence. AddsTokenizer.decodeToken(id)(default =decode(intArrayOf(id))) and aSentencePieceTokenizeroverride that decodes a single token without the leading strip (llama.cpptoken_to_piecesemantics), plus adecode(ids, stripLeadingSpace)overload. Every streaming consumer now reconstructs spacing correctly.
- Graph-output pruning for export (
ComputeGraph.prunedToOutputs). A traced decoder surfaces every leaf tensor (e.g. per-layer intermediates) as a graph output, so a StableHLO/IREE export returns the logits plus dozens of dangling tensors — extrafuncreturns and dead op subgraphs. AddsOutputDesignatedGraph(skainet-compile-dag) to override the output set by node id, andComputeGraph.prunedToOutputs(outputNodeIds)(skainet-compile-opt) which designates those outputs and runsDeadCodeEliminationPassso only the nodes feeding them survive. Exporters can now keep just the logits. AddsGraphPruningTest(commonTest).
- SDPA causal mask uses a large finite fill (
-1e30) instead of-inf. The attention HLO converter's causal path emitteddense<0xFF800000>(-inf) for the masked-fill select; it now emits-1.000000e+30, matchingMultiHeadAttention.buildSlidingCausalMaskand avoiding a-infsplat in the IR (numerically equivalent after softmax). (AttentionOperationsConverter)
ExecutionContext.isRecording. A default-falseisRecordingon the lang-coreExecutionContextinterface, overridden byGraphExecutionContext(currentTape?.isRecording). Lets modules with an eager fast-path that bypassesops.*(e.g. RoPE's raw-array INTERLEAVED rotation) detect tracing and emit a graph-traceablectx.ops.*path — so they export to StableHLO while keeping the fast path for eager inference — without depending on the compile-dag module. Backward-compatible (existingExecutionContextimplementations inherit the default). (PR #757)
- Docs: Antora docs version-currency (dependency snippets → current release) and broken-link
fixes across all pages (
.mdlink:→xref:, deadtensorflow.org/xla→openxla.org, deadrfc.md/bench-prd.mdreferences,ainet-sk→SKaiNET-developers). (PR #758) - Dependency:
ch.qos.logback:logback-classic1.5.34 → 1.5.35. (PR #756)
- GroupNorm now compiles on stock IREE. The 0.32.0 GroupNorm converter lowered mean/variance
with
stablehlo.custom_call @reduce_mean/@reduce_variance(mirroring LayerNorm), whichiree-compilecannot lower — so agroupNormmodule exported but failed to compile. It now emits realstablehlo.reduce(add region) +divide, computing variance asE[x²] − E[x]²(population, ddof=0), exactly like thesum/mean/varianceconverters. Verified end-to-end through theskainet-iree-conformanceharness:iree-compile+iree-run-module+ numpy validate → PASS (max_abs_err = 1.2e-7).GroupNormConverterTestasserts real reductions and nocustom_call. (PR #754)
- GroupNorm StableHLO converter.
NeuralNetOperationsConverternow lowersgroupNorm(and thegroupNormalization/GroupNormalization/group_normaliases) to realstablehlo.*ops instead of falling through to the "operation not supported" path. The lowering mirrors the LayerNorm/RMSNorm decomposition: reshape(N, C, *spatial)to(N, G, M), per-groupmean/variance, normalize, reshape back, and apply the optional per-channelscale/offset. AddsGroupNormConverterTest(commonTest). (PR #752) - SKEEP proposals docs module. (PR #750)
- Quantization-process explanation doc (weights, activations, calibration). (PR #747)
- Dependency bumps:
com.vanniktech.maven.publish→ 0.37.0 (PR #748),com.networknt:json-schema-validator→ 3.0.5 (PR #749), kotest → 6.2.1 (PR #744), Gradle wrapper → 9.6.0 (PR #745),actions/checkout→ 7 (PR #743).
RowDequantSource+ops.gatherrow-dequant path. AddsRowDequantSource(aTensorDatamarker,dequantRow(rowIdx): FloatArray) toskainet-lang-core, and teachesDefaultCpuOps.gatherto use it: when the gathered table implementsRowDequantSource, only the rows actually touched are dequantised (each unique row once, cached) instead of the generic element path — which callsget()(unsupported on such tensors) and would otherwise force a full FP32 materialise of the table. The table declares logical dtypeFP32, sogatherreturns FP32 with no typing change. This lets a packed/oversized embedding (a Q-quantisedtoken_embd) stay packed and be looked up viaops.gatherdirectly — generalising the per-row-dequant trick out of the model layer. AddsGatherRowDequantTest(commonTest). (PR #741)
ops.transposenow lazily handles every packed matmul dtype. The CPU backend's 2-D transpose rewraps the packed bytes with a flipped shape (a metadata-only "lazy transpose") for the K-series (Q4_K/Q5_K/Q6_K) and Q5_0/Q5_1, but Q8_0 and Q4_0 fell through to the generic FP32 path, which cast the Byte-backed buffer to Float and threwClassCastException. Added theQ8_0TensorDataandQ4_0TensorDatacases so a packed Q8_0/Q4_0 matmul weight (e.g. a model's tied Q8_0lm_head) surviveslinearProject'smatmul(x, ops.transpose(W))and dispatches to its packed kernel instead of crashing —ops.transposenow covers the fullchooseQuantizedMatmulHeapdispatch set (Q4_K/Q5_K/Q6_K/Q5_0/Q5_1/Q8_0/Q4_0). Addstranspose_preserves_every_packed_quant_type(commonTest, jvm + linuxX64) as a regression guard. (PR #736, #737)
- Bumped
com.networknt:json-schema-validatorto 3.0.4. (PR #733)
- First-class Q5_K packed matmul. New
TensorEncoding.Q5_K,Q5_KTensorData/Q5_KBlockTensorData(256-element / 176-byte super-blocks with theqh5th-bit plane), and aQ5KMatmulKernelSPI. Implementations: scalar reference (commonMain → Kotlin/Native, JS, Wasm), JVM Panama Vector, and native-C (FFM). Wired intoDefaultCpuOpspacked-quant matmul dispatch + lazy transpose, registered viaKernelRegistry, and added to the GGUFStreamingGgufParametersLoader(Q5_K + Q6_K packed branches). Q5_K weights stay packed and dequantize inside the matmul, matching the existing Q4_K/Q6_K path. (PR #734) - ARM NEON kernels for the native CPU backend. Hand-written NEON paths for
fp32,q8_0,q4k, andq5kmatmul (sharedskainet_simd.h), behind#if __ARM_NEONso x86 keeps its-O3 -ffast-mathauto-vectorized scalar path. The native CMake build adds an aarch64 branch (-march=armv8.2-a+fp16+dotprod; no+i8mm— Cortex-A55 lacks it) and an opt-in-PcrossArm64cross-compile with a toolchain file. (PR #734) - Kotlin/Native consumption of the C kernels via cinterop.
skainet-backend-native-cpunow builds a static archive (libskainet_kernels.a) alongside the shared lib and addslinuxX64+linuxArm64targets with a cinterop.def, sharednativeMainNativeKn*MatmulKernelwrappers, and aNativeKnKernelProvider(+installNativeKernels()). On-device Kotlin/Native binaries can now reach the same hand-tuned C/NEON kernels the JVM uses via FFM. (PR #734)
sk.ainet.core:skainet-compile-minervais now published to Maven Central. The new Minerva export module (shipped in 0.29.0) applies the publish plugin and was auto-included in the BOM, but it lacked the per-modulegradle.properties(POM_ARTIFACT_ID/POM_NAME) that every other published module carries, so its publication had no POM name and never made it to Maven Central. Added the module'sgradle.properties; the artifact now publishes alongside the rest of the engine.
- Minerva secure-MCU export module. A new export pipeline that takes a SKaiNET model all the way to a secure microcontroller project bundle, built up in phases:
- Shared graph-export contracts. A backend-agnostic export contract layer (
feat(export)), with a StableHLO graph-export adapter that exposes the existing HLO path through those contracts. (#697, #698, #702) - Module API scaffold. The Minerva export module's public API surface. (#700)
- Phase-one graph-compatibility validation. Validates that a graph is exportable before any lowering work begins. (#701)
- IR lowering + npz compiler input. Compatible graphs lower to Minerva IR (#704) and emit an
.npzcompiler input (#706). - Compiler packager + host verification. A packager flow that drives libminerva packaging, plus a host-verification flow and runtime-verification profile that prove the exported bundle on the host before it ships. (#706, #712, #714, #716, #717, #721, #725)
- Manifest fingerprinting. Generated manifest artifacts are fingerprinted so bundle contents are tamper-evident. (#724)
- Runnable sample + examples + docs. A runnable sample task and runner, secure MCU export examples, an ONNX export workflow, getting-started / explanation pages, and model-source guidance. (#707, #712, #719, #725)
- Shared graph-export contracts. A backend-agnostic export contract layer (
- Packed-quant matmul kernels with Kotlin/Native parity. Q5_0, Q5_1, Q4_K, and Q6_K gain matmul support across the provider stack:
- commonMain scalar kernels + SPI for Q5_1/Q5_0/Q4_K/Q6_K, giving Kotlin/Native parity with the JVM path. (#710, #711)
- Packed-quant matmul dispatch in
DefaultCpuOpsBaseso the packed-quant path is selected on Native, not just JVM. (#709, #711) - Panama Vector (JVM SIMD) kernels for Q5_1/Q5_0 (#709) and Q6_K, with Q6_K routed via the
KernelRegistry. (#715, #720) - Packed Q5_1/Q5_0 matmul kernels + lazy transpose on the CPU backend. (#709)
- Kernel × platform support matrix is auto-generated and CI-gated. The kernel/platform support matrix is now rendered through the build-logic → Antora pipeline (the same pipeline as the ops docs) from a CI-gated source, so it can't drift from the code. (#716, #724)
- Refreshed the architecture kernel-provider section (native FFM ships; link matrix). (#726)
- Eager-execution backends & kernels mindmap, refreshed after Panama Q6_K. (#720, #723)
- Hardened CI browser tests against launch flakiness under
allTests(Karma) and raised the Mocha timeout 10s → 60s for the micrograd demo. (#703, #705)
- Bumped
io.github.optimumcode:json-schema-validatorto 0.5.5. (#713)
- DAG-DSL StableHLO export now compiles end-to-end with IREE for the full conformance suite (7/7 models, 27/27 ops). Shape-changing ops declared a result/return type inferred from operand-0 instead of the op's real output, so
iree-compilerejected the modules ("inferred shape … is incompatible with return type").DagBuilder.inferDagOutputSpecsnow computes the correct output spec for:reshape/view— reads the target shape from the op'snewShapeparameter (aShape, which the converter'sas? List<Int>had missed). (#673, PR #674)matmul/dot/mm/bmm—(…, M, K) @ (…, K, N) → (…, M, N)instead of echoing operand-0. (#673, PR #674)concatenate— the corrected summed-axis extent propagates to the consumers and thefunc.funcreturn type, not just the op line. (#673, PR #674)conv1d— windowed(N, Cout, Lout);conv2dalready inferred viaConv2dOperation, butconv1dwas aGenericOperation. (#675, PR #676)gather—table[:axis] ⊕ indices.shape ⊕ table[axis+1:]. (#675, PR #676)maxpool2d/avgpool2d— windowed(N, C, Hout, Wout). (#675, PR #676)flatten— collapses[startDim..endDim]while preserving the leading batch dim (it was collapsing everything to rank-1, breaking the dense layer in mnist-cnn). (#675, PR #676)
reduce_windowis emitted in IREE's parseable generic region form. Pooling previously used the pretty… applies <op> over window …form, which IREE rejects ("has no custom assembly form"). Now emits"stablehlo.reduce_window"(…) ({ ^bb0(…): … })with full NCHW-rank window attributes; average pooling's divisor is splatted to the output type (was a scalar-vs-tensor mismatch). (#675, PR #676)MlirValidatorunderstands region block arguments. It now registers^bb0(%a, %b)block-argument SSA definitions and every%x =result on a line, so single-line region ops (e.g.reduce_window) validate. (PR #676)
- Regenerated the JVM binary-compatibility baselines (
apiDump) to match the public API exposed since 0.27.0 (AttentionOperationsConverter, multi-output port helpers,KClassdtype constructors).
reshapewhose target shape lives only in an operation parameter now lowers.ShapeOperationsConverter.convertReshapepreviously depended on a declared outputTensorSpec; a graph that carried the target only in the op'soutputShape/shape/newShapeparameter produced an empty/untyped module. It now reads the parameter and synthesizes a typedstablehlo.reshaperesult. (Issue #666, PR #670)- Multi-input
concatenatesums the operands' extents on the concatenated axis.convertConcatechoed operand-0's extent into the result type, so e.g. concatenating1×1×8×8 + 1×4×8×8 + 1×1×8×8on dim 1 emittedtensor<1x1x8x8xf32>instead oftensor<1x6x8x8xf32>. The result type now sums the axis across all operands. (Issue #667, PR #670) - StableHLO DSL export for constants and reductions. DAG constants are inlined (baked) into the emitted module rather than externalized as function arguments, and a reduction drops the reduced dimension. (Issue #663, PR #664)
HloGeneratorforward-pass tracing records ops. The sample input is bound to the tape execution context and external inputs are synthesized (toComputeGraph(synthesizeExternalInputs = true, …)), so tracing aModelemits real StableHLO ops instead of a structure-only module. (Issue #668)
- Non-JVM image runtime support. Image and data-transform modules are scoped to their supported KMP targets, with a non-JVM image runtime implementation so the image/data-transform APIs build honestly across targets. (PR #671)
- Full gemma3 network lowers to StableHLO with zero gaps. A batch of new core converters closes every remaining op gap on the Kotlin DSL → MLIR StableHLO path, so a complete gemma3 graph traces and lowers end-to-end (verified by
GemmaTraceTestover the composite build: 140 nodes → 255 lines, 0 unsupported, 0 arity errors):scaledDotProductAttentionconverter (AttentionOperationsConverter) — lowers the atomic SDPA op to the standard StableHLO subgraph:scores = Q·Kᵀ(dot_general, contracthead_dim),* scale(arg or1/sqrt(head_dim)), numerically stable softmax over key length,out = attn·V. Batched[..,S,D]with all leading dims as batching dims. SDPA is a coreTensorOpsop, so its converter lives in core.- Causal mask for SDPA — when the node's
causalattr is set, emits an additive-infmask before softmax (iota/compare GE/select) so each query attends only to keys at or before it. Validated EXACT against a NumPy causal reference and accepted byiree-compile. - Explicit SDPA mask operand —
AttentionOperationsConverternow consumesoperands[3](the additive mask), broadcasting it trailing-aligned to the scores shape before softmax. Fixes gemma sliding-window layers (causal=false+ explicit causal+window mask) that previously exported unmasked and attended to future tokens. (PR #661) permute+narrowconverters —permuteregistered as an arbitrary-axis alias routed to the existing transpose path;narrowslicing support.- Multi-output converter support +
splitconverter — per-(nodeId, outputPort)SSA naming inConversionContext, operand resolution that walks incoming edges bydestinationInputIndexand resolves each by the edge'ssourceOutputIndex, andsplit/chunklowering to Nstablehlo.sliceops each registered on its own output port (lowers the RoPEsplitgap).
- Boxing-free
FloatArrayweight externalization for.irpabaking.finalize()now stores resolved weights as the primitiveFloatArrayinstead of.toList()(boxing a real LLM weight — e.g. a 262153×640 embedding → ~2.7 GBList<Float>— OOMed the trace).ConstantOperationsConverterexternalizesFloatArraydirectly (floatArrayToLittleEndianBytes+tryMaterializeExternalFloats, inlining small/InlineAlwaystensors viaasList()), andIrpaWriterwrites byte ranges in one shot. With this, the real Gemma-270M function bakes: 1 func arg (tokens) + 360 weights externalized toutil.global #flow.parameter.named. - DSL prescribes element dtype for placeholder weights. The DSL can now specify the element dtype for placeholder weights during tracing.
- Numerical validation harness for SDPA lowering. Dumps a small
scaledDotProductAttentionStableHLO graph;iree-compile+iree-run-moduleoutput matches a NumPy reference exactly to 5 decimals, confirming the attention converter is numerically correct, not just structurally valid.
- IREE-valid StableHLO syntax — full gemma3 compiles to
vmfb. Aligned converter emission to whatiree-compile's StableHLO parser accepts (verified by compiling the full gemma3 graph end-to-end):gatheruses the generic MLIR form,slice/narrow/splituse the canonical bracket form via a sharedsliceLine()helper,concatenateemits the full functional type, and batch matmul derives batch dims asmin(lhsRank,rhsRank)-2(fixes 3D-activation @ 2D-weight Linear projections). Result: SKaiNET gemma3 DSL → StableHLO →iree-compile(llvm-cpu; +neon aarch64) →vmfbfor both host x64 and aarch64 targets. VoidTensorOps.gatheroutput shape for multi-dim indices. The void/tracing gather collapsed the gathered axis toindices.shape[0], so a[vocab,emb]table with[batch,seq]indices traced to[batch,emb]instead of[batch,seq,emb](breaking the embedding's downstream reshape during weight-free tracing). Now replaces the axis with the full indices shape, matchingDefaultCpuOps.gather, unblocking tracing of full transformer (gemma3) graphs.
- Q4_0 promoted to a first-class quantized format. The older GGML 4-bit format (18 bytes / 32 elements) was previously a JVM/MemSegment-only, GGUF-only side-path; it is now wired across the full provider stack mirroring Q8_0 / Q4_K:
- commonMain heap
Q4_0TensorData/Q4_0BlockTensorData(+TensorEncoding.Q4_0) so any loader can produce it and non-JVM targets can use it. Q4_0MatmulKernelSPI +KernelProvider.matmulQ4_0(), with scalar (commonMain), Panama Vector (JVM SIMD), and native FFM (skainet_q4_0_matmul) implementations selected viaKernelRegistry.bestAvailable()(native → Panama → scalar).DefaultCpuOpsJvm.chooseQuantizedMatmulgains anis Q4_0TensorDatabranch.Q4_0Quantizer(FP32 → Q4_0) — the produce side was missing, so dense weights from any source (SafeTensors / JSON / in-memory) can now be quantized to canonical ggml Q4_0 without going through GGUF.- All Q4_0 paths use the canonical ggml split nibble layout (low nibbles → elements 0..15, high → 16..31,
(code - 8) * d). (PRs #648, #649, #650, #651)
- commonMain heap
tanhas a first-classTensorOpsactivation primitive. Promotestanhfrom a defaultNotImplementedErrorstub to a fully wired@Diff @ActivationDslprimitive, eliminating the2*sigmoid(2x)-1polyfill that downstream consumers were forced to re-derive. Wires the standard six-layer pattern:TensorOpsinterface,TanhOperationclass,Tensor.tanh()extension, CPU backend, recording decorator, and autograd backward (1 - output^2). A Karpathy micrograddemo.ipynbport lands as an end-to-end training test (moons dataset,[2,16,16,1]tanh-MLP, MSE + SGD, held-out accuracy) exercising the primitive through the full DSL + training stack. (Issue #630, PR #631)- CPU tensor
convertop. Implements the dtype-conversion operation on the CPU backend. (PR #636)
- Recording decorator uses the current recording-stop API internally. Aligns the internal call site with the current recording API surface. (PR #640)
- Q4_0 MemSegment matmul layout. The pre-existing JVM MemSegment Q4_0 kernel (
JvmQuantizedVectorKernels.dotQ4_0BlockMemSeg) andQ4MemorySegmentTensorDataused an interleaved nibble layout that did not match real GGUF Q4_0 weights (a latent correctness bug; the path was unverified and had no in-repo callers). Reconciled to the canonical split layout so it now agrees with the heap type, the SPI kernels, andDequantOps.dequantQ4_0FromBytes. (PR #649)
- Ignored common tests made portable across KMP targets via the shared
@Ignoreannotation, so the same skip applies consistently on every platform. (PR #638) - Ignored-test clarity and BatchNorm coverage. Clarifies why each ignored test is ignored and enables previously-disabled BatchNorm coverage. (PR #634)
- Gradle build-hygiene warnings fixed. (PR #633)
- Feature-PR workflow trigger scope narrowed to avoid duplicate workflow runs on feature PRs. (PR #645)
- Resynced binary-compatibility-validator baselines to current source (the committed
.apidumps had drifted). (PR #647)
- BF16 matmul end-to-end. New
Bf16TensorData+Bf16DenseTensorData(PR #610), opt-in SafeTensors loader policy to keep BF16 native through the loader chain (PR #612), andDefaultCpuOpsJvmBF16 dispatch wired againstBf16TensorData(PR #614). The matmul kernel itself ships asBf16MatmulKernelwith scalar, Panama, and native implementations registered with theKernelRegistrySPI (PR #605). - Q8_0 matmul end-to-end.
Q8_0MatmulKernelwith scalar, Panama, and native implementations (PR #606);DefaultCpuOpsJvm.chooseQuantizedMatmulnow resolves through theKernelRegistrySPI so the best-available Q8_0 kernel wins automatically (PR #608). - Autograd completeness for
pow,log, and conv/pool/upsample/split. Newpow/powScalarops plus aPowSpecializationPass(Tier A of #617),log/log2/log10ops (Tier B), backward formulas filled in forpow/logwith the matching dispatch arms (Tier C partial), and the remaining conv / pool / upsample / split backward formulas (Tier C). End-to-end CNN training-step test (Tier D) pins the contract. (PR #618) - Hybrid adaptive DSL with optional dtype constraints — RFC implementation.
DTypeConstraintResolutionPassregistered in the pipeline (W7), adtypePolicy(...)extension onDagBuilder(W6),StreamingGgufParametersLoader.withPolicy(DTypePolicy)(W0c), a typedResolvedComputeGraphview (W8), and atoStableHlo(ResolvedComputeGraph)overload (W9). (Issue #615, PR #616) @DarcValidatedannotation for operator documentation. Newsk.ainet.lang.ops.DarcValidatedannotation inskainet-lang-ksp-annotations; the KSPOperatorDocProcessorthreads its values throughoperators.json, and the doc generator renders a✅ / ⚠ / ✖badge above each function's signature plus aValidatedcolumn on the operator coverage matrix.matmulis annotated as the worked example. The AntoraContributingsection gains a dedicateddarc-workflow.adocpage that defines the workflow and the criteria for setting the annotation;.github/ISSUE_TEMPLATE/darc_feature_request.mdwas aligned (DEFINE→DOCUMENT,CONTRIBUTE→CODE). (Issue #627, PR #628)- SentencePiece special-token splitter. New
SpecialTokenSplitterdecorator inskainet-io-coreplus fixes for SentencePiece HF JSON gaps that previously misrouted control tokens. (PR #595)
operator-doc-schema-v1.jsonwidened to match what the processor actually emits. Added thecompositemodality, theinheritedbackend status,version: "unknown"(mirroring the existingcommithandling), and thedescriptionnote type. The optionalvalidated/validatedBy/validatedOn/validatedCommit/referencesCheckedfields were added in support of@DarcValidated.validateOperatorSchemagoes from0/4 validto4/4 valid. (PR #628)
getInputNodesordering. Now ordered bydestinationInputIndexso DAG consumers see operands in the right slot. (Issue #620, PR #622)
- DARC workflow contributor page (
contributing/darc-workflow.adoc) — authoritative in-repo definition of Document / Assess / Research / Code, with the operator-doc specialisation. The Antora docs site is now declared the authoritative source over the GitHub wiki. (PR #628) - SIMD kernels deep-dive + arc42 architecture page. How FP32 and quantized Panama Vector kernels are built; how to read the matmul benchmark; the priority-100 native FFM provider plan. (PR #623)
- Nav split: Using SKaiNET vs Contributing. The left nav now separates the consumer-facing Tutorials / How-to / Reference / Explanation tree from the maintainer-facing Contributing tree, and Java-specific pages are annotated. (PR #619)
- Canonical five-minute start path. New tutorial path that gets a reader from zero to a working SKaiNET call in five minutes; deliberately Kotlin-first to match the framework's primary surface. (PR #624, plus follow-up "keep the start path Kotlin-first")
- Architecture goal in the README. Single paragraph stating what SKaiNET is optimising for, so readers landing on the README know the framework's centre of gravity. (PR #625)
native-ffm-plan.adocremoved from the published docs. The page read as a PRD/issue, not user-facing reference; content is preserved as an untracked draft for promotion to a real issue. The nine incoming xrefs across six pages were surgically rewritten. (PR #628)
- Bump
androidGradlePluginto 9.2.1 (PR #597). - Bump
org.jetbrains.kotlinx:kotlinx-benchmark-runtimeto 0.4.17 (PR #599). - Bump
org.jetbrains.kotlinx:kotlinx-coroutines-coreto 1.11.0 (PR #600). - Bump Gradle wrapper to 9.5.1 (PR #601).
- Bump
io.ktor:ktor-client-coreto 3.5.0 (PR #602). - Bump
org.junit.jupiter:junit-jupiterto 6.1.0 (PR #621).
TensorDataFactory.placeholder(shape, dtype)— returns aTensorDatawhose underlying primitive array materializes lazily on first read, instead of allocating aFloatArray(shape.volume)eagerly. The default interface implementation falls back tozeros, preserving behavior for any custom factory;DenseTensorDataFactoryoverrides withLazyZeroFloatArrayTensorData/LazyZeroIntArrayTensorData.ExecutionContext.placeholder(...)exposes the same path at theTensorlevel. (PR #588)PosixPreadRandomAccessSourcefor Kotlin/Native — new public class inskainet-io-core'snativeMainsource set wrapping POSIXpread(2).preadis positional and atomic, so concurrent reads from different positions are safe without locking. Companionopen(path)returnsnullon open/stat failure to match the JVMJvmRandomAccessSource.open(...)behaviour, letting callers cleanly fall back to the legacy sequential reader if needed. CoversmacosArm64,linuxX64,linuxArm64,iosArm64,iosSimulatorArm64— every target in the defaultnativeMainsource set on this module. 11nativeTestcases pin the contract (size, partial reads, offset/length variants, EOF/argument validation, idempotent close, missing-file null return). (PR #591)
- Kotlin/Native consumers couldn't load GGUFs larger than ~2 GiB —
sk.ainet.io.gguf.createRandomAccessSource(filePath)on the native target was a placeholderactual fun … = null, forcing every K/N caller (StreamingGGUFReader.open(...)via the gguf-specific factory, every*NetworkLoader.fromGguf(...)path,LlamaWeightLoader) to fall through to the legacy reader, which slurps the entire file into a singleByteArray. Kotlin arrays cap atInt.MAX_VALUEbytes (~2 GiB), so any GGUF over ~1.9 GiB threwIllegalStateException: Can't create an array of size 2147483648. Practical impact: macOS / Linux / iOS native builds couldn't open Q8 models above ~1B parameters or Q4 models above ~3B — the JVM target had no such cap becauseJvmRandomAccessSourcewas already implemented. Theskainet-io-gguffactory's native actual now delegates to the newPosixPreadRandomAccessSource(see Added above) and returns the samenullsentinel on open/stat failure, so existing fall-back code paths remain valid. Verified on macOS arm64 againstQwen3-1.7B-Q8_0.gguf(~1.8 GiB), which previously OOMed at construction time. (Issue #589, PR #591) - DSL eagerly allocated zero tensors for every Linear / Conv1d / Conv2d, OOMing real-model loaders —
NetworkBuilder.kt'screateLinear,DenseImpl,Conv1dImpl, andConv2dImplpaths calledtensorDataFactory.zeros<T, V>(shape, kClass)eagerly to satisfy each module's constructor whenever the user had not provided initial weights or bias. Downstream loaders always build the network first and only then substitute weights viaWeightMapper.applyWeights, so the eager zeros were always immediately discarded — but they determined the JVM's peak heap footprint. Forunsloth/Apertus-8B-Instruct-2509-GGUF(Q4_K_S, 4.7 GB on disk) that was ~27 GB of FP32 zeros allocated and thrown away. Switched every eager-init call site to the newplaceholder(...)API; the lazy fires only if a caller actually reads the tensor, which never happens on the substitution path becauseparameter.value =swaps the entireTensor. Verified against the real Apertus-8B Q4_K_S GGUF:ApertusNetworkLoader.fromGguf().load<FP32, Float>(ctx)now succeeds in 12 GB heap (previously OOMed at 12 GB), constructs all 35 top-level modules in 13 s. Same fix benefits Gemma / Llama / Qwen / Voxtral DSL paths transparently. (Issue #587, PR #588)
skainet-bompublished at the wrong Maven coordinates — the umbrella BOM was being emitted assk.ainet.core:skainet-bombecause the engine-wideGROUP=sk.ainet.corefrom the rootgradle.propertiesclobbered the per-modulegroup = "sk.ainet"override picked up byvanniktech.maven.publish. Downstream BOMs (e.g.sk.ainet.transformers:skainet-transformers-bom) import this with<groupId>sk.ainet</groupId>, so they were unresolvable from a freshmavenCentral()-only project. Fix uses vanniktech's explicitmavenPublishing { coordinates("sk.ainet", "skainet-bom", VERSION_NAME) }so the BOM publishes atsk.ainet:skainet-bom:0.22.2.validate-published-poms.shextended to assert the BOM landed at the expected path so the regression cannot ship again. (Issue #584)GgufModelMetadatasilently droppedUInt/ULongnumeric fields — modern GGUFs (recent llama.cpp converters) store dimensions and counts asuint32, which the reader preserves as KotlinUInt. Kotlin's unsigned types do not extendkotlin.Number, so the previous private(value as? Number)?.toInt()helper returnednullfor everyUInt/ULongfield. Result:contextLength,embeddingLength,layerCount,headCount,vocabSize(fallback),bosTokenId, andeosTokenIdall came backnullon real-world GGUFs and downstream loaders fell back to defaults (e.g.blockCount=0→ zero-layer transformer). New public fileGgufFieldAccessors.ktexposesMap<String, Any?>extensions (getInt/getLong/getString/getIntList/getStringList) covering every signed and unsigned integer type the reader can emit, plus the matching primitive arrays for the list variant.GgufModelMetadata.from()now routes through these public accessors; the buggy private helpers are deleted. NewGgufModelMetadataUnsignedTestpins the contract. Non-breaking — only adds public API and fixes existing methods to return correct values. (Issue #585)
StreamingShardedSafeTensorsReader.loadTensorStorageMapped— by-name and by-ShardedTensorInfooverloads that mirror the existing single-fileStreamingSafeTensorsReader.loadTensorStorageMapped(tensor, filePath). Both return aTensorStoragewhoseBufferHandle.FileBackedreferences the resolved shard file's tensor byte range, enabling zero-copy / memory-mapped reads of tensors that exceed the 2 GB JVMByteArraylimit. The new methods delegate internally to the per-shard reader; callers don't need to know which physical shard contains a given tensor. Unblocks downstream consumers (e.g. SKaiNET-transformers' Gemma 4 PLE token-embedding table at ~4.7 GB BF16 on E2B) that previously rolled their ownFileChannel.map. (PR #582)
This release closes milestone M5 of the JVM inference performance roadmap with a priority-100 native kernel provider that wraps a bundled C shared library via Java's Foreign Function & Memory API. Plugs into the existing KernelProvider SPI so KernelRegistry.bestAvailable() automatically routes Q4_K and FP32 matmul through native when the lib loads, falling back cleanly to the priority-50 Panama Vector kernels otherwise.
skainet-backend-native-cpumodule — new JVM-only KMP module wrapping a CMake-built shared library (libskainet_kernels.{so,dylib,dll}). Bundled into the JAR resources atnative/<os>-<arch>/, extracted at runtime to a process-scoped temp dir, loaded viaSystem.load, and accessed viaLinker.nativeLinker().downcallHandle(...). ServiceLoader auto-registersNativeKernelProviderFactoryviaMETA-INF/services/sk.ainet.backend.api.kernel.KernelProvider. (PR #571)- Native Q4_K matmul — single-source scalar C kernel (
-O3 -ffast-math -funroll-loops); the inner 32-iteration loop auto-vectorizes cleanly intovfmadd231ps(AVX2) /fmla(NEON). MirrorsPanamaVectorQ4KMatmulKernelbyte-for-byte on the canonical ggml super-block layout (256 elements / 144 bytes, FP16 d/dMin, 12-byteget_scale_min_k4packed sub-scales, 128 bytes of strided 4-bit codes, lazy-dminaccumulation). Microbench (Linux x86_64, JDK 21.0.10): 5.87× / 4.71× / 4.17× faster than Panama Vector at 1024² / 2048² / 4096² Q4_K matmul shapes — single-threaded native beating Panama'sparallelChunksmulti-threaded path on every measured shape. Numerical parity vs Panama within1e-4relative tolerance. (PR #572) Q4KMemSegMatmulKernelSPI sibling + zero-copy native variant — JVM-only sibling kernel interface inskainet-backend-api/jvmMaintaking weights asMemorySegmentinstead ofByteArray, plus a JVM-onlyMemSegKernelProviderprovider interface that providers can implement alongsideKernelProviderfor the smart-cast lookup pattern at the call site. Reuses the same C symbol as the heap-input kernel — the bytes just don't round-trip through the JVM heap. +20% wall-clock at 4096² vs the heap-copy path (9 MB weight transfer eliminated); noise-level at smaller shapes. Bit-identical output to the heap variant. (PR #573)- Cross-arch CI matrix — new
.github/workflows/native-cpu-multiarch.ymlbuilds and tests the native module onubuntu-latest,macos-14(Apple Silicon), andwindows-latestfor every push/PR that touches the native module. Catches portability regressions (linker, alignment, compiler-specific syntax) at PR time rather than after release. C portability tightened:SKAINET_RESTRICTmacro maps to__restrict__on GCC/Clang and__restricton MSVC; CMake grows an MSVC compile-flag branch (/O2 /fp:fast /W3) alongside the existing GCC/Clang one. Linux ARM64 was attempted but Kotlin/Native plugin 2.3.21 doesn't supportlinux aarch64as a HOST target ("Unknown host target") — left out for now. (PRs #574, #577) - Native FP32 SGEMM — row-major
C(m,n) = A(m,k) * B(k,n)with stride support, i-p-j outer-product order so the innerc[j] += a*b[j]loop streams two contiguous arrays and auto-vectorizes into FMA. Wired into the existingmatmulFp32()SPI accessor. Microbench at 256³ / 512³ / 1024³: 1.77× / 1.58× / 1.55× faster thanPanamaVectorMatmulKernel. The narrower margin vs Q4_K reflects Panama's already-polished FP32 path (tile-blocking + B-pack +parallelChunks); native still wins on every measured shape. Numerical parity within1e-5 * krelative tolerance. (PR #575) - Multi-arch fat JAR publishing —
.github/workflows/publish.ymlextended to a two-phase flow: a matrixbuild-nativejob buildslibskainet_kernelson each supported host (linux-x86_64, macos-arm64, windows-x86_64), and thepublishjob downloads all three artifacts, stages them into the native module's resources tree, and publishes with every supported arch bundled. Consumers on any of the three arches get a working native path out of the box — no manual side-loading.
skainet-backend-native-cpuregistered in BOM —skainet-bomnow constrains the new module alongsideskainet-backend-apiandskainet-backend-cpu. Consumers depending on the BOM get a constrained version without a separate pin. (PR #576)- Publishing config wired —
vanniktech.mavenPublishplugin + per-modulegradle.properties(POM_ARTIFACT_ID + POM_NAME) on the new module. Composite-build consumers (e.g. SKaiNET-transformers viaincludeBuild) substitute the published coordinates with the local project ref through the same path every other SKaiNET module uses. (PR #576)
NativeKernelProviderconsumption kdoc — covers two gotchas downstream consumers hit on first wiring: (1) the module is JVM-only (FFM has no Native/JS/Wasm equivalents) so KMP consumers must add the dep tojvmMain.dependencies, nevercommonMain; (2)com.gradleup.shadow:9.4.xmergeServiceFiles()silently drops theNativeKernelProviderFactoryentry when bothskainet-backend-cpuandskainet-backend-native-cpuare on a shadow JAR's classpath — workaround pointer to thekllama-clidoLastfix in SKaiNET-transformers PR #88. (PR #579)docs/.../perf/native-ffm-plan.adoc— design baseline for the native FFM provider (recovered from the 0.21.0-cycle PRD that was dropped from the repo root and rehomed as asciidoc). Documents module layout, FFM binding pattern, staged delivery, success metrics, and risks.
- Linux ARM64 native lib is not in the published JAR. Kotlin/Native plugin 2.3.21 doesn't support
linux aarch64as a HOST target on the runners GitHub provides, so the cross-arch CI matrix excludes it. Linux ARM64 consumers (Raspberry Pi, AWS Graviton) cleanly fall back to the priority-50 Panama Vector provider — no functional regression, just no native speedup. Re-add when either the Kotlin/Native plugin gains the host or a self-hosted ARM64 runner is wired in. - Shadow-jar consumers using
com.gradleup.shadow:9.4.xstill need adoLastworkaround to merge theMETA-INF/services/sk.ainet.backend.api.kernel.KernelProviderentries — see SKaiNET-transformers PR #88'skllama-cli/skainet-clifix for the canonical implementation. Spring Boot apps consuming via Maven (BOOT-INF/lib/) are unaffected.
This release lands the JVM Vector half of milestone M5 from the JVM inference performance roadmap — a pluggable kernel SPI parallel to BackendProvider, plus a Panama Vector provider that matches or beats the prior production path on every shape we measure. A native (FFM) priority-100 provider closing the milestone metric is deferred.
KernelProviderSPI —skainet-backend-apinow exposes aKernelProviderinterface withname,priority,isAvailable(), and per-kernel accessors (matmulFp32(),matmulQ4K()).KernelRegistrydoes priority-orderedbestAvailable()lookup; a JVM-onlyKernelServiceLoader.installAll()auto-discovers providers viaMETA-INF/services/sk.ainet.backend.api.kernel.KernelProvider. Manualregister(...)still works for tests and non-JVM platforms. (PRs #554, #559)Fp32MatmulKernel+PanamaVectorMatmulKernel— JDK Vector API implementation usingFloatVector.SPECIES_PREFERRED+fma+reduceLanes, cache-blocked with 8×8×128 tiles.KernelMatmulBenchmeasures 8.61× / 8.62× / 10.83× speedup over scalar at 256/512/1024 (JDK 21.0.10, M-series macOS). Within JMH noise of — and often slightly faster than — the priorJvmVectorKernels.matmulFloatBlockedproduction path, so routing introduced no regression. (PRs #557, #558, #560)- Production matmul routes through
KernelRegistry—DefaultCpuOpsJvm.matmulnow resolves the FP32 kernel viaKernelRegistry.bestAvailable()instead of callingJvmVectorKernels.matmulFloat*directly. ProductionMatmulBenchnumbers post-routing match pre-routing within JMH noise. (PR #561) Q4KMatmulKernelSPI + SIMD-fused Panama implementation — Sibling kernel interface inskainet-backend-api/commonMain,KernelProvider.matmulQ4K()accessor (default-nullfor backwards compat).PanamaVectorQ4KMatmulKernelfuses Q4_K dequant inline with the FMA accumulator: a singleByteVectorload feeds both lo and hi sub-block accumulators per qs slab via AND/LSHR nibble extract →castShape(B2F)→ FMA, with the lazy-dmincorrection (acc += scale·codeSum − offset·inputSumonce per sub-block).QuantizedMatmulBenchmeasures 0.07/0.15/0.46 ms at 1024×1024 / 4096×1024 / 4096×4096 (≈30/55/73 GFLOPS — same throughput regime as the FP32 SIMD kernel, meaning fused dequant adds essentially zero cost on top of the FMA).DefaultCpuOpsJvm.chooseQuantizedMatmul'sQ4_KTensorDatabranch routes through the SPI with a fall-through to the legacy kernel when no provider resolves. (PR #562)- Q4_K MemSeg SIMD — Same fused-pipeline algorithm applied inline to
JvmQuantizedVectorKernels.matmulF32Q4_KMemSeg(the path mmap'd weights take).ByteVector.fromMemorySegmentinstead ofByteVector.fromArray— no heap copy. (PR #563) - Q6_K SIMD dequant —
dequantQ6_KBlockreplaces its scalar 32-iteration loop with aByteVector-based ql + qh extraction pipeline: perfloatStep-wide chunk ofl, loads ql + qh slices, assemblesq1..q4 = (ql nibble) | ((qh slice) << 4) − 32per lane, multiplies by per-sub-blockd·scale, stores to four 32-element regions of the scratch FloatArray. (PR #564) - Q4_0 partial SIMD —
dotQ4_0BlockMemSegtwo-stage pattern: scalar byte-pair unpack into a caller-supplied scratch FloatArray (16 byte loads, two nibbles each — half the byte traffic) followed by aFloatVectorFMA reduction. Closes the last fully-scalar quantized kernel; every quantized format inJvmQuantizedVectorKernels(Q4_0, Q4_K, Q4_K MemSeg, Q6_K, Q8_0) is now SIMD'd to some degree. (PR #565)
ScratchPoolSPI — Runtime workspace allocation for transient tensor scratch buffers. Per-runtime size-classed slabs, scoped acquire/release. Closes the framework-side primitive for milestone M1 of the JVM perf roadmap. (PR #550)TensorOps.permute(axes)— Arbitrary-axis permutation (generalizes the existingtransposeto N-D). (PR #552)
- Q4_K / Q5_K canonical ggml layout + FP32 MemSeg arena leak —
Q4_KTensorDataand Q5_K dequant now apply the canonical ggml layout (super-block scale + per-sub-block scaleIdx/minIdx viaget_scale_min_k4mixing, strided 4-bit codes layout).MemorySegmentTensorDataFactoryusesArena.ofAuto()for per-op outputs so the matmul / transpose output segments are GC-reclaimable; the priorofConfined()builds leaked tens of MB per matmul, which over a 35-layer Gemma 4 forward pass exhausted the JVM direct-memory cap. Liveness-based freeing of intermediate tensors inComputeGraphExecutor. (PR #556)
- Q6_K Native Matmul: New
Q6_KTensorData/Q6_KBlockTensorDatainskainet-lang-corestores 210-byte ggml Q6_K blocks verbatim (128ql+ 64qh+ 16 scales + 2 f16d), row-major by default, with adequantizeBlockpath matching theDequantOpsreference line-for-line.DefaultCpuOpsJvm.chooseQuantizedMatmuldispatches to a newJvmQuantizedVectorKernels.matmulQ6_KVecSIMD kernel (Kotlin Vector API, samefloatSpeciesas the Q4_K / Q8_0 kernels) using a dequant-one-block-to-scratch-then-SIMD-dot pattern. NewTensorEncoding.Q6_Kvariant. Unblocks running Gemma 4 E2B Q4_K_M (and any mostly-Q4_K + Q6_K checkpoint) through the DSL path without a ~12 GB FP32 dequant blow-up at load. - Q4_K Lazy Shape-Swap Transpose:
DefaultCpuOpsJvm.transpose(Q4_KTensorData)now returns a newQ4_KBlockTensorDatawrapping the same packed byte array with swapped shape — mirroring the existing Q4/Q8 MemorySegment lazy-transpose path.matmulQ4_KVec's input-block-major layout produces correct values under the swapped shape without any physical data reordering, solinearProject(x, W)can runmatmul(x, transpose(Q4_K_W))without round-tripping through FP32. Validated at the DSL level byGemmaDslQ4KTestin the transformers repo (Δ logits = 4.29e-6 vs the FP32 baseline). - Q6_K Lazy Transpose: Same shape-swap specialization extended to
Q6_KTensorData, enabling the same DSL path for Q6_K weights. - Lazy-Transpose Invariant Tests: New
QuantizedMemSegMatmulTestcases pin the two load-bearing properties of the Q4_K and Q6_K transpose specializations — (1) shape is swapped; (2)packedDatais the SAME byte-array reference, not a copy — so the path cannot silently regress to the generic element-wise transpose (which wouldClassCastExceptionon packed nibbles).
- SDPA Recording + StableHLO Emission:
scaledDotProductAttentionis now recorded byRecordingExecution(was silently delegating without recording, likeconv1dbefore #532) and lowered to StableHLO byNeuralNetOperationsConverter. The decomposition isdot_general(Q, K.T)(batching dims[0,1], contracting dims[3]×[3]) → scale → optional mask → softmax (max-subtract-exp-sum-div) →dot_general(weights, V)(contracting dims[3]×[2]). NewScaledDotProductAttentionOperationinTensorOperationswith output-shape inference (output shape = query shape). NewSdpaHloExportTestverifies tape → graph → MLIR withdot_general;TapeAttentionPermuteBugTestpins a regression around raw array permute producing zero constants.ShapeOperationsConverter.concatenateinput-type annotation fix. (#543)
- SDPA Q/K/V Shape Validation:
scaledDotProductAttentionpreviously required only rank-4 inputs, so a mismatch inhead_dim(e.g. Q=512 vs K=256, as seen in real Gemma 4 E2B where mixed-head-dim layers share a KV cache) surfaced as anArrayIndexOutOfBoundsExceptionburied 2000+ lines deep in the dot-product loop. Addedrequire()preconditions on matching batch, head count, Q/K head_dim, Q/V head_dim, and K/VseqKV, each with a message naming the offending dimensions. NewSDPAShapeValidationTest(5 cases,commonTest) pins the contract.
- Kotlin: 2.3.20 → 2.3.21 (including JVM toolchain and
plugin.serialization). - Android Gradle Plugin: 9.1.1 → 9.2.0.
io.ktor:ktor-client-core: 3.4.2 → 3.4.3.
- Broken POM for
skainet-backend-cpu: The 0.19.0 POM forsk.ainet.core:skainet-backend-cpu-*declared a runtime dependency onsk.ainet:skainet-backend-api-jvm:unspecified— wrong group coordinate and no valid version, becauseskainet-backend-apiwas not configured to publish and the rootallprojects { group = "sk.ainet" }disagreed with theGROUP=sk.ainet.coreused by vanniktech's maven publish plugin. Consumers pulling 0.19.0 hit unresolved-dependency errors. Fixed by:- Applying
vanniktech.mavenPublishand settingPOM_ARTIFACT_ID=skainet-backend-apionskainet-backend-apiso it is actually published alongside the BOM entry that already referenced it. - Aligning
allprojects { group = "sk.ainet.core" }with theGROUPproperty and pinningversionfromVERSION_NAMEsoproject(...)coordinates in generated POMs are consistent.
- Applying
- CI guard: New
verify-published-pomsjob publishes to the local Maven repository and fails the build if any generated.pomcontains<version>unspecified</version>or references a project-local group outsidesk.ainet.core, preventing a regression of this class of coordinate bug.
- Qwen / GPT-2 Byte-Level BPE Tokenizer:
QwenByteLevelBpeTokenizerimplements the full GPT-2-style pipeline — byte-to-unicode mapping, GPT-2 pretokenization regex, merge-rank BPE, and atomic special-token splitting. Builds from either GGUF metadata (fromGgufFields) or a HuggingFacetokenizer.json(fromTokenizerJson). Verified against Qwen2.5-0.5B reference token IDs from HuggingFacetransformers. (#463) - LLaMA / SentencePiece Tokenizer:
SentencePieceTokenizerimplements the llama.cpp SPM pipeline — whitespace escape (▁), code-point symbol split, score-priority BPE (the SPM rule, opposite of the merge-rank rule used for GPT-2 BPE), and<0xNN>byte fallback for unknown characters. Builds from GGUF (tokenizer.ggml.model == "llama") and HuggingFacetokenizer.json(model.type == "Unigram"). Verified against TinyLlama-1.1B reference token IDs from HuggingFacetransformers. (#464) TokenizerFactorywith Per-Architecture Dispatch: Tokenizer selection is now per-architecture, not per file format.TokenizerFactory.fromGguf(fields)and.fromTokenizerJson(json)inspecttokenizer.ggml.model/model.typeand dispatch to the right implementation — Qwen/GPT-2 → byte-level BPE, LLaMA/Gemma/TinyLlama → SentencePiece — regardless of whether weights come from GGUF or SafeTensors. (#463)TokenizerInterface: Common surface implemented byTekkenTokenizer,QwenByteLevelBpeTokenizer, andSentencePieceTokenizer(encode,decode,vocabSize,bosTokenId,eosTokenId).- GGUF Tokenizer Metadata:
GgufModelMetadatanow exposestokenizerModel,tokenizerTokens,tokenizerMerges,tokenizerTokenTypes,bosTokenId, andeosTokenIdso callers can build a tokenizer without re-parsing the raw field map.
- Whisper Encoder E2E: Whisper encoder now compiles end-to-end via SKaiNET → StableHLO → IREE.
- Real StableHLO Lowerings:
softmax,layerNorm, andrmsnormnow lower to real StableHLO ops (reductions,broadcast_in_dim, standard ops) instead ofcustom_callstubs. (#467, #479, #480) - New Op Converters:
gather/embedding, andconcat/slice/castStableHLO converters. (#483, #489) - Activation Alias:
silu/SiLUregistered as an alias forswishinActivationOperationsConverter. (#484) ConstantMaterializationPolicy: Seam for externalizing large weight tensors out of the StableHLO module (enables.irpaexternalization). (#524)- Splat Constant Folding: Uniform-value tensor constants collapsed to
dense<v>splat instead of fully materialized arrays. (#522) - SSA Value Type Tracking: Tracks SSA value types so
reshapeemits the operand's declared type, producing valid MLIR. (#521) - Tensor Encoding in Output:
tensor_encodingcomments in StableHLO output and a top-levelskainet.tensor_encodingsmodule attribute. (#473, #477)
skainet-io-iree-paramsModule: New module withIrpaWriterfor writing IREE Parameter Archive (.irpa) files. AcceptsFileBackedhandles via mmap on JVM / Android for zero-copy weight export. (#523, #525, #528, #529)
skainet-backend-apiModule: New module cleanly separating backend contracts; CPU backend now depends on it. (#468)TensorEncodingMetadata: Accessor forTensorSpec.metadataand propagation throughTraceToGraphBuilder.finalize, keeping quantization encoding visible end-to-end. (#469)
- Annotated
StableHloConverterFactoryandTokenizerFactoryfor idiomatic Java call sites. (#400) - Renamed
TensorSpecEncoding.ktclass for Java callers. (#400) - Added
skainet-backend-apito the BOM. (#400) - New
ReleaseApiJavaTestcovering the 0.19.0 Java surface. (#400)
- Antora + Diátaxis: Migrated docs to Antora with Divio / Diátaxis layout (tutorials, how-tos, reference, explanation). (#494)
skainet-docs-uiv1.1.1: Adopted the new theme with Diátaxis card-grid landing page. (#501)- Operator Coverage Matrix: Emit cross-backend Operator Coverage Matrix generated from
TensorOpssurface scan. (#494, #511) - Ops Docs: KDoc
@paramextraction, real version stamps, LaTeX rendering, fixed partials, and dropped void backend. (#511, #513) - Dokka API Bundle: Wired into the Antora site build. (#494)
- Local Mermaid: Drop kroki, render Mermaid locally via
mmdc. (#496)
androidNativeArm32: Added across core modules. (#503)
- Byte-Level BPE Broken for Qwen/GPT-2 Models: Previously there was no GPT-2-style byte-level BPE tokenizer in the repo, and
GgufModelMetadataignoredtokenizer.ggml.mergesentirely — so any Qwen / GPT-2 / Mistral-Nemo model encoded text into garbage tokens (byte-level chars instead of merged vocab IDs), blocking chat mode and tool calling. The newQwenByteLevelBpeTokenizer+TokenizerFactorydispatch fix the issue for both GGUF and SafeTensors sources. (#463) - No SentencePiece Path for LLaMA-Family GGUF Models:
TokenizerFactorypreviously threwUnsupportedTokenizerExceptionfortokenizer.ggml.model == "llama", leaving LLaMA / TinyLlama / Gemma / Mistral-v0.1 GGUFs untokenizable. The newSentencePieceTokenizercloses that gap. (#464) - GGUF UInt Fields Silently Dropped: GGUF UINT32 fields (e.g.
tokenizer.ggml.bos_token_id) arrive fromStreamingGGUFReaderaskotlin.UInt, which is a value class — not a subclass ofkotlin.Number— so a plainas? Numbercast was returning null. The newtoIntFlexiblehelper handles every signed and unsigned numeric type GGUF can produce, restoring the BOS/EOS/UNK ids on the tokenizer builders. - Graph Conv Output Shape Inference:
conv1d/conv2d/conv3doperations in graph inference previously produced placeholder output shapes, breaking downstream shape-dependent passes. Graph ops now compute real output shapes. (#536, #537) - Conv1d/Conv3d Not Recorded:
conv1dandconv3dwere not routed through the recording decorator, so they disappeared from traced computation graphs. (#532, #533) - Static Conv1d HLO Shape Crash: Conv1d StableHLO lowering crashed when trace attributes were missing; now falls back to
TensorRefshape / dtype. (#530, #531) - Flatten Hardcoded to MNIST Shape:
NetworkBuilder.flatten()returned a hardcodedlastDimension = 1568(the MNIST CNN value); any other architecture — e.g. a 64-channel CNN over 32×32 inputs — crashed withArrayIndexOutOfBoundsExceptionin the followingdense()layer. The DSL now tracks per-sample shape through a newinput(IntArray)overload,conv1d/conv2d/conv3d,maxPool2d,avgPool2d, andupsample2d, reusing theConvShapeUtilsarithmetic introduced in #537;flatten()reads the tracked shape and honorsstartDim/endDim, andConv*layers can auto-inferinChannelsfrom the declared input. (#535, #538) - StableHLO
transpose/dot_generalMLIR Emission: Fixed malformed MLIR produced bystablehlo.transposeandstablehlo.dot_generalthat blocked IREE compilation. (#520) - WasmJS / JS / Native Compile: Replaced JVM-only
putIfAbsentwith a common-stdlib idiom. (#485) - Antora Container:
HOME=/tmpso Chromium crashpad can launch during Mermaid rendering in CI. (#534) bundleDokkaIntoSiteCI Permission Failure: Fixed docs pipeline permission error. (#496)- Pandoc Artifacts in Docs: Stripped pandoc anchors and demoted heading levels in migrated pages. (#496)
compile-hloDependencies: Dropped vestigialskainet-backend-cpudependency fromcompile-hlojvmMain. (#472)- Moved-LLM Docs: Replaced relocated LLM pages with redirect stubs pointing at the standalone repo. (#499)
- Maven Group / Version Refs: Bumped stale version references and fixed Maven group coordinates. (#499)
- Stale
TURBOQUANT_ISSUES.mdtracker at the repo root. (#490)
- agp: 9.1.0 → 9.1.1.
- com.networknt:json-schema-validator: 3.0.1 → 3.0.2.
- org.jetbrains.kotlinx:kotlinx-serialization-json: bumped to 1.11.0.
- actions/checkout: 4 → 6.
- actions/upload-pages-artifact: 3 → 5.
- actions/cache: 4 → 5.
- actions/setup-java: 4 → 5.
- actions/deploy-pages: 4 → 5.
- actions/github-script: 8 → 9.
- docker/build-push-action: 5 → 7.
- docker/setup-buildx-action: 3 → 4.
- TurboQuant KV-Cache Compression: Runtime KV-cache compression for LLM inference using rotation-based quantization (Google Research TurboQuant paper). Supports PolarOnly and PolarPlusQjl variants with 2/3/4/8-bit encoding.
TurboQuantCodec: End-to-end encode/decode pipeline (random rotation, scalar quantization, QJL residual, bit-packing).TurboQuantKvCacheStore: Compressed KV cache with per-head TurboQuant blocks and asymmetric K/V policies.TurboQuantPresets: Named presets —safe-lowbit(Q8_0-K + TQ4-V),balanced(TQ4/TQ4),experimental-max(TQ3/TQ3).KvCacheStore.turboQuant("balanced", ...): One-line factory for skainet-transformers integration.CompressedKvAttention: SDPA bridge with FULL_TILE and RAW_STORAGE dequant strategies.@KvCacheand@KvCacheBypassDSL annotations for declarative KV cache configuration.KvCacheAnnotationResolver: Resolve annotations to cache instances.TurboQuantUsage: Documented integration guide with compilable examples.
- Memory Architecture Hardening: First-class storage and placement abstractions for zero-copy, quantization-preserving tensor management.
TensorStorage: Runtime descriptor replacing ad-hoc array passing (logical type, physical encoding, buffer ownership, placement).TensorEncoding: Sealed hierarchy —Dense,Q4_K,Q8_0,TernaryPacked,TurboQuantPolar,TurboQuantPolarQjl,Opaque.BufferHandle: Five ownership modes —Owned,Borrowed,Aliased,FileBacked,DeviceResident.Placement: Device/memory-domain intent with fallback policies (CPU_HEAP,MMAP_WEIGHTS,GPU_PREFERRED).LogicalDType: Semantic numeric types separate from physical encoding.PackedBlockStorage: Unified contract for all packed quantized formats.MemoryPlanner,MemoryTracker,ActiveMemoryTracker: Placement resolution and copy diagnostics.
- KV-Cache Subsystem:
KvCacheStoreinterface with append-by-token writes, layer/head addressing, eviction, andDefaultKvCacheStore(dense FP32 baseline). - Quantization-Preserving Loaders:
StreamingGGUFReaderandStreamingSafeTensorsReaderproduceTensorStoragewithFileBackedorBorrowedhandles (no forced densification).StorageAwareSafeTensorsLoader: Zero-copy file-backed SafeTensors loading.- Completed
Quants.ktport:byteShapeToQuantShape,quantByteSize,isBlockQuantized,validateQuantizedBytes.
- Tekken Tokenizer: Mistral Tekken (tiktoken-based BPE) tokenizer support.
- CPU SIMD TurboQuant Kernels:
JvmTurboQuantKernelswith Java Vector API acceleration for abs-max, quantize, dequantize, and Walsh-Hadamard butterfly. - JMH Benchmarks: TurboQuant encode/decode throughput, bit-packing, rotation, and KV cache append/read benchmarks (
TurboQuantBenchmarks.kt). - Storage Benchmarks: Dequantization throughput (Q4_K, Q8_0, Ternary), buffer accessor, and TensorData bridge benchmarks (
StorageBenchmarks.kt). - New Ops:
sin,cos,tanh,convTranspose1d. - New Layers:
TransposedConv1d,Snakeactivation,LayerScale.
- Streaming GGUF as Default:
StreamingGGUFReaderis now the recommended GGUF loading path (memory-efficient, supports quantized types). - DSL Annotations: Extended
PlacementAnnotations.ktwith@KvCache(preset=...)and@KvCacheBypassfor TurboQuant configuration.
- Int Overflow for Large Tensors: Fixed
StreamingTensorInfo.nBytesandStreamingSafeTensorInfo.sizeInBytesfromInttoLong, preventing silent overflow for tensors > 2 GB. Fixes loading of Gemma 4 E4B and future large models. (#452) - Legacy GGUFReader Overflow Guard: Added explicit overflow check with actionable error message for tensors > 2 GB in the legacy eager loader.
- io.github.kotest:kotest: 6.1.9 → 6.1.11.
- com.squareup:kotlinpoet: 2.2.0 → 2.3.0.
- Core Engine Focus: Refactored the repository to focus on the core
ComputeGraphframework, compiler, and backends. - Standalone Ecosystem: Extracted high-level LLM and transformer implementations to dedicated repositories (SKaiNET-LLM and SKaiNET-transformers).
- LLM-as-DSL: High-level DSL for defining and running LLM architectures within the core
ComputeGraphframework. - ComputeGraphExecutor: New optimized executor with support for fusion passes and trace-to-DAG bridging.
- SDPA & Gather: Implementation of Scaled Dot-Product Attention (SDPA) and
gather/indexSelectops across backends. - EmbeddingAdapter: Streamlined embedding layer integration for transformer models.
- Optimized LLM execution: Integrated fusion passes for faster inference on supported backends.
- Improved Tensor API: Refined
Tensorinterface and updatedComputeGraphExecutorfor better type safety and performance. - Dependency Cleanups: Removed stale references to LLM and transformer code already moved to the standalone
skainet-transformersrepository.
- Embedding Padding: Fixed
paddingIdxhandling in embedding layers. - Concatenation: Resolved rank-specific issues in tensor concatenation (rank > 1).
- Compilation: Fixed various build and compilation errors after module migrations.
- Deduplicated LLM infrastructure: unified
KvCache,softmax,RoPE, andsamplinglogic across modules for improved maintainability. - Updated skainet-bom: Refactored the Bill of Materials (BOM) to use local
project()references for better build consistency.
- LLM Module Extraction: Extracted and moved core LLM modules to the standalone SKaiNET-LLM repository to reduce core codebase footprint.
- Transformer Code Cleanup: Removed redundant code that has been moved to the SKaiNET-transformers repository.
- Dependency Graph: Resolved inverted dependency issues in the LLM infrastructure.
- System Prompt Support (Java): Added
systemPromptsupport toKLlamaJavaandKLlamaSessionfor prepending system instructions to conversations. - Model Module Extraction: Extracted model-specific code into dedicated
skainet-modelsmodules for better separation of concerns and maintainability. - Enhanced Smoke Tests: Refactored
smoke-test.shto support multiple runners via JSON configuration and improved LLM loading verification.
- Whisper HLO Generation: Fixed StableHLO MLIR generation for Whisper models.
- Compilation: Fixed various Kotlin/JVM compilation errors.
- First-Class Java 21+ Support: Complete Java API surface with
SKaiNETentry point,TensorJavaOps, builder-pattern model definition (SequentialModelBuilder),KLlamaJava/KBertJavafacades,JavaAgentLoopfor tool-calling agents, andTrainingLoopbuilder. - Maven BOM: New
sk.ainet:skainet-bomartifact for one-line version management across all modules. - Java Documentation: Added Getting Started, LLM Inference, and Model Training guides.
- Java 25 Performance Documentation: Added documentation for JVM CPU backend performance advantages.
- WasmWasi Target: Added
wasmWasitarget support across all KMP modules. - StableHLO MLIR Streaming API: New
HloGeneratorpublic API with generic Model + Tensor interface and streaming MLIR output. - ReductionOperationsConverter: Added support for reduction operations in StableHLO export.
- JVM Performance (Jlama Techniques): MemorySegment-based tensors, SIMD GEMM kernels, paged KV cache, batch attention for prompt prefill, fused QKV projections, and cached quantized weights.
- Native RandomAccessSource: POSIX
pread()-based source for memory-efficient GGUF parsing. - MemorySegment Weight Conversion: New
NATIVE_OPTIMIZEDquant policy andMemSegWeightConverterpipeline with Arena lifecycle management. - Lazy Transpose: Added lazy transpose for Q4/Q8 MemorySegment tensors and MemSeg FP32 transpose.
- Java CLI App: New Java-based KLlama CLI application.
- Android KMP Plugin Migration: Migrated Android subprojects to
androidMultiplatformLibraryplugin for AGP 9 compatibility. - Refactored Model Loading: Extracted shared dequantization, registry, tensor naming, and decoder runtime into reusable components.
- JDK Requirement Relaxed: Allow JDK >= 21 instead of requiring exactly JDK 21.
- Gradle Upgrade: Updated to Gradle 9.3.1.
- Kotlin Upgrade: Bumped Kotlin from 2.2.21 to 2.3.10.
- Kotlin Compile Testing: Replaced abandoned
kotlin-compile-testingwithkctforkfor Kotlin 2.3.0 compatibility.
- StableHLO MLIR Export: Fixed MLIR export to produce valid IREE-compilable output.
- OOM in Dequantization Benchmark: Fixed out-of-memory in
DEQUANTIZE_TO_FP32E2E benchmark test. - Quantized MatMul: Fixed block offset calculation in quantized matrix multiplication.
- CI Stability: Fixed AAPT2 daemon crashes and improved Android build stability.
- Documentation CI: Fixed workflow permissions for PR comments.
- Deprecated API Usage: Fixed
createTempDir()deprecation in data-simple integration tests.
- com.gradleup.shadow: 9.3.1 → 9.3.2.
- com.fasterxml.jackson.core:jackson-databind: 2.21.0 → 2.21.1.
- ch.qos.logback:logback-classic: 1.5.27 → 1.5.32.
- io.github.kotest:kotest: 6.1.3 → 6.1.4.
- org.jetbrains.kotlinx:kotlinx-io-core: 0.8.2 → 0.9.0.
- com.vanniktech.maven.publish: → 0.36.0.
- org.jetbrains.kotlinx.kover: → 0.9.7.
- actions/setup-node: 4 → 6.
- actions/upload-artifact: 6 → 7.
- actions/download-artifact: 7 → 8.
- junit-platform-launcher added for CI test execution.
Thank you to the following contributors for their work on this release:
- Dhia Chemingui (@dhiaspaner) — Android KMP plugin migration (#385, #386)
- Tool Calling: Added support for tool calling in KLlama, including a new
skainet-kllama-agentmodule. - Gemma 3n Support: New
skainet-kgemmamodule for Google's Gemma 3n E2B multimodal models. - Extended SafeTensors Support: Added SafeTensors weight loading support for both KLlama CLI and Gemma models.
- HuggingFace Tokenizer: Initial support for HuggingFace-style tokenizers in Gemma models.
- Named Arguments: Refactored various internal APIs to use named arguments for better optional parameter support.
- System Prompt Handling: Improved system prompt formatting and handling in agentic workflows.
- BERT Support: Full support for BERT-based models with
SafeTensorsweight loading. - kbert-cli: New CLI tool for running BERT inference, supporting text encoding and cosine similarity computation.
- WordPiece Tokenizer: Implementation of WordPiece tokenizer for BERT models.
- TinyFoA Support: Implemented missing operators (
abs,sign,clamp,lt,ge,narrow,pad2d,unfold) to support TinyFoA (AAAI 2025) training pipeline for memory-efficient on-device learning. - Multi-platform KLlama: Added macOS target support for the KLlama runtime.
- Custom Backends Documentation: Added detailed guide and examples for injecting custom backends into KLlama.
- Improved robustness of TinyFoA operations with comprehensive unit tests.
- Benchmarking DSL: New
BenchmarkDslandBenchmarkRunnerfor measuring model performance and latency. - Execution Observers: Added
ExecutionObserverAPI withLatencyExecutionObserverandMemorySnapshotObserverfor profiling. - New Layers: Added
RMSNormalizationlayer support. - KLlama Enhancements: Improved weight loading and initial support for GPU-accelerated attention (experimental).
- Refactored
ExecutionContextto support execution observers and better phase management. - Updated KLlama runtime with improved ingestion and benchmarking utilities.
- Generative AI Section: New README section with simple code for GGUF text generation.
- Tokenizer Strategies: Automatic detection of tokenizer type (SentencePiece, BPE, WordPiece) from GGUF metadata.
- Improved Token Decoding: Support for multi-byte UTF-8 character decoding from byte tokens.
- Llama Runtime: Rewritten
matmulNoBiasfor better performance and support for row-major weights. - GGUF Loading: Improved dequantization for Q2_K, Q4_K, Q5_K, and Q6_K formats matching llama.cpp logic.
- GGUF Storage Order: Fixed critical bug with column-major storage in GGUF files by implementing proper transposition during loading.
- Llama Attention: Fixed missing attention output projection (wo) in the runtime.
- Tokenizer: Fixed BOS token handling and multi-byte character reconstruction.
- SafeTensors Support: Initial implementation of
skainet-io-safetensorsfor reading SafeTensors format. - Generalized I/O & Weight Mapping:
- New
WeightMapperandWeightLoaderAPIs for unified model parameter loading across formats. LoadingProgressAPI for tracking model loading state.GgufModelMetadataandOnnxModelMetadatafor better inspection of model files.
- New
- JVM Performance: Enhanced
DefaultCpuOpsJvmwithJvmVectorKernelsfor SIMD-accelerated tensor operations using the Java Vector API. - Llama Enhancements:
- Added
GGUFTokenizerfor better text processing. - Improved
LlamaIngestionand ingestion pipelines.
- Added
- Improved GGUF/ONNX Loading: Robust weight loading and metadata parsing for GGUF and ONNX models.
- Streamlined CLI: Removed unfinished CLI samples and reorganized
skainet-tensor-tools. - Documentation Cleanup: Removed outdated technical docs and consolidated architecture information.
- Improved robustness of GGUF and ONNX streaming readers.
- Fixed various issues in WASM/JS weight parsing.
- Updated version to 0.8.3.
- KLlama (Llama 2 port): Initial version ported from
llama2-kmp, supporting GGUF models. - GGUF Enhancements:
- Support for
mmapfor zero-copy GGUF tensor loading. - Embedded tokenizer support in GGUF.
- New quantization formats:
Q8_0,Q4_K, and BitNet/Ternary support (TQ1_0,TQ2_0). - Improved loading and bug fixes for quantization and mapping.
- Added
int64support for GGUF. - Improved GGUF metadata loading.
- Support for
- Streaming Support: Added streaming support for GGUF and ONNX models.
- Advanced Operations:
- New activations:
LeakyReLU,ELU. - New pooling:
AvgPool2d. - New convolutions:
Conv1d,Conv3d.
- New activations:
- Optimizers & Training:
- Added
AdamandAdamWoptimizers. - Comprehensive loss function library.
- New
Metricinterface withAccuracyimplementation. - KSP-based DSL generator for Network activations.
- Added
- Data & Datasets:
- Support for
CIFAR-10andFashion-MNISTdatasets. - New
Data Transform APIandImage Transform DSL.
- Support for
- Testing & Documentation:
skainet-test-groundtruthmodule for validation against PyTorch.- Integration tests for quantized inference and
KvCache. - Shadow JAR support for JVM fat JAR builds.
- New documentation for testing architecture with Mermaid diagrams.
- WASM/JS: Initial version of a simple WASM/JS sample.
- Simplified model support to GGUF-only (removed legacy Karpathy
.binformat support). - Improved KLlama loading and robustness.
- Updated roadmap with Phase 1 completion and multi-backend storage abstraction plans.
- Improved I/O system and overall robustness.
- Fixed various bugs in quantization and memory mapping.
- Resolved compilation errors and failing tests in CIFAR-10 support.
- Fixed KSP and TracingWrapperProcessor tests to match updated log messages.
- Fixed GGUF metadata loading issues.
- Initial release of 0.8.x series.
- Sine Approximation CLI (
skainet-sine-approx-cli) as a new example application for training models. TapeRecordingStrategyto handle different recording behaviors for prediction and backpropagation.- Comprehensive E2E tests for training sine wave approximations.
- New documentation:
autograd-basic.mdexplaining the autograd engine.
- Refined
Linear,Flatten,Inputmodules andreluactivation to better support gradient tracking and context propagation. - Improved
DefaultExecutionTapeandDefaultGraphExecutionContextfor more robust computation tracing. - Optimized internal
OpSinkandTraceSessionhandling.
- Infinite loop error during backpropagation tracing by implementing specialized tape recording strategies.
- Context mismatch errors in backpropagation tracing.
- Broken testing in the sinus sample application.
- Initial Autograd engine (
DefaultGradientTape) for automatic differentiation and reverse-mode gradients. - Optimizer API with
SgdOptimizerimplementation for training neural networks. - Loss functions module including
MSELossandCrossEntropyLosswith configurable reductions (MEAN, SUM, NONE). - Training DSL and helper utilities for building training loops (
trainStep,evaluateLoss). - Improved Graph DSL with better context propagation and support for recording computation traces.
- Updated dependency versions and refined internal execution context APIs to support gradient tracking.
- Refactored
skainet-compile-dagto support autograd and graph inversion.
- StableHLO implementation and E2E CLI app for compiling models to CUDA via IREE.
ArduinoCodegenfor exporting models to standalone C99 code with static memory allocation, optimized for Arduino.- KSP-based generation of
TracingOpsfor automated recording pipeline updates. - Initial implementation of
skainet-compile-hlofor high-level optimization.
- Improved CUDA backend strategy and IREE integration.
- Optimized long-running property tests for C code generation.
- Refactored
TracingTensorOpsto use execution context for code generation.
- Common I/O abstraction with
ModelReaderandTensorInfoinskainet-io-corefor unified model loading. - Efficient memory handling with non-copying
sliceviews inMemoryChunk. - Unified
skainet-tensor-toolsCLI combining ONNX and GGUF utilities. OnnxStatsClitool for analyzing ONNX model parameters and structure.
- Migrated project to
SKaiNET-developersorganization; updated repository URLs and deployment configurations. - Standardized artifact naming in documentation (e.g.,
SKaiNET-lang-core). - Improved
GGUFReaderwith better alignment parsing and tensor data handling. - Optimized test infrastructure: increased heap size to 8GB for large model tests and added
ReadmeSnippetsTestfor documentation verification.
- Legacy standalone applications and tools:
skainet-KGPChat,skainet-mnist, and separate ONNX/GGUF tool modules.
- ONNX import module (
skainet-io-onnx) with pbandk-generated proto surface, loader utilities, and importer that maps ONNX graphs into SKaiNET compute graphs, plus doc and tests. - CLI tooling:
skainet-onnx-toolsto export ONNX initializers to JSON andskainet-onnx-detectCLI to run YOLO detections from ONNX weights. - YOLOv8 model upgrades: depth/width scaling, decoupled heads with DFL projection, class-name parsing, and detection helpers to align with ONNX exports.
- Image IO module now published with explicit API surface for bitmap <-> tensor conversions across platforms.
- BatchNorm now reshapes stats for broadcasting and exercises JVM/native tests; CPU backend implements
sqrtto support it.
- Added pbandk runtime 0.16.0 for ONNX protobuf decoding.
- Recording/tracing pipeline for tensor ops (RecordingExecution/TracingTensorOps) and compute-graph DAG under
sk.ainet.lang.graph, including tape-to-graph conversion and GraphViz export helpers/tests. - JSON export proof of concept via new
skainet-compile-jsonmodule with serialization models,exportJsonCLI, and tiny graph golden fixtures. - Multiplatform image IO module to convert platform bitmaps <-> tensors and RGB byte arrays; includes macOS implementation fixes.
- Dedicated YOLOv8 model module (
skainet-models:skainet-model-yolo) with graph assembly, config/pre/post-processing, and missing upsample/concat ops required by the model. - NN DSL additions: multi-input
Functionalwrapper, newUpsample2d/Softmax helpers, scalar DSL builder plus tensor/number operator overloads, and extra tensor view/pprint utilities.
- Graph DSL relocated into the lang namespace with refreshed default execution tape/graph context wiring; removed unused integration module scaffolding.
- Removed committed MNIST training assets; rely on download at runtime.
- Added scalar arithmetic support across backends and void ops to match new operator overloads.
- Corrected unsqueeze view handling and data DSL dtype reuse; stabilized tracing/JSON/tape tests.
- Fixed macOS image conversion path and cleaned duplicate files in the new IO/image pipeline.
- io.ktor client 3.3.3 (from 3.3.2).
- logback-classic 1.5.21 (from 1.5.20).
- Kolmogorov–Arnold Network (KAN/AKN) module and DSL support, including public factory and aliases for direct construction. Introduces
Akn/AknConfigandcreateAknmirroring DSL defaults. - Example KAN models and graphs (e.g., Sine function examples and pretrained variant) with tests and Graphviz export.
- Additional NN DSL conveniences around initialization scopes (weights/basis/bias) and activation hooks used by KAN.
- Minor API refinements in lang/nn DSL to better align with execution context usage for new KAN modules.
- Stabilized integration tests for KAN modules and examples.
- Minor initialization performance tweaks for new modules.
- Updated docs and samples to include KAN usage and references.
- Initial support for model code sharing API (model definition, execution, loading). Implements #196, related to #169.
- Batch Normalization layer. Implements #193.
- Forward hooks and simple tape recording for NN. Implements #190, related to #104.
- Common traversal base for modules, with tests; Embedding implementation with dual value types; switched EEmbeddings to DualModule implementation.
- Dropout (initial implementation) and phase support (training/eval) in execution context so modules can behave differently by phase. Related to #5.
trilop (initial version).- MaxPool op with DSL support; Conv2D DSL support.
- Data API: initial version including MNIST data loader; JSON loading support (renamed loader classes from CSV to JSON) with tests. Implements #180, #181; related to #176, #179.
- GGUF model loading implementation (initial import and working version). Implements #178, #182; related to #176, #177.
- MatMul support in backends.
- Nested data blocks support in DSL (data block returns a tensor); contexts for creating and collecting tensors (returning last or all created tensors).
- JVM Ops using the Java Vector API (initial implementation) and SIMD Vector API acceleration.
- JMH benchmarks (JVM module) and additional benchmarks.
- Sample showing general tensor calculations (e.g., image color transformations).
- NN DSL refactored to use
ExecutionContext; addedExecutionContextparameter toforwardfunctions. - Models and data APIs improved; unified tensor value creation in DSL; moved tensor creation context for safer vector/matrix/tensor creation.
- Default CPU compute used for JS target.
- JS and WASM Kotlin targets aligned for library packaging.
- Gradle updated to 9.0.0; Android target namespaces fixed.
- Crash in schema validation task; added Kotlin compiler plugin configuration for expect/actual.
- Activation not applied in Dense layer (fixed).
- JVM target issues; fixed failing JVM tests; added regression tests; stabilized platform matching test (temporarily ignored) and additional general test fixes.
- Miscellaneous build-signing validation added to avoid CI failures.
- SIMD/Java Vector API acceleration for JVM backend operations.
- com.vanniktech.maven.publish: 0.34.0 → 0.35.0.
- io.ktor (android, cio, content-negotiation, core, darwin, js, logging): 3.3.1 → 3.3.2.
- com.fasterxml.jackson.core:jackson-databind: 2.15.2 → 2.20.0 → 2.20.1.
- GitHub Actions: use Java 22.
- Bump actions/checkout from v4 to v5.
- Add Gradle local caches to .gitignore.
- Preparations for 0.2.0 release and ability to build local Maven version of the upcoming release.
- Added hint/reference on normalization layer paper. Related to #192.
- Initial public release of SKaiNET 0.1.0.