Repository navigation
feat(wasm): resident activations in the sidecar; elementwise, activation and reduction kernels - #1369
Merged
michalharakal merged 3 commits intoOct 20, 2026
Conversation
…l on the JVM Wasm: the sidecar exports skainet_ternary_f32_gemv, skainet_ternary_lmhead_stage1 and skainet_bitnet_gemv_tq2_0. The portable branch of skainet_ternary_f32.c (MSVC, Emscripten) holds the vendored decode table as a compile-time constant with a SIMD128 body and threads over output rows through the shared row pool; bitnet_gemv.c gains a SIMD128 block body and the same row threading on every tier. WasmTernaryKernelPack (installed by WasmSimd.install) registers the three native faces under the dispatch keys the JVM FFM and Android JNI ternary packs serve. JVM: Fp32MatmulWtKernel / KernelProvider.matmulFp32Wt() and the JVM-only Fp32MemSegMatmulWtKernel / MemSegKernelProvider.matmulFp32WtMemSeg() expose skainet_fp32_matmul_wt; NativeFp32MatmulWtKernel binds it with a heap and a segment face. DefaultCpuOpsJvm.matmulWeightTransposed multiplies a dense [out, in] FP32 weight in place over a resident off-heap weight instead of transposing and caching a copy. WasmKernelProvider exposes the same accessor. Kernel support matrix: a fourth table, "Kernel packs (view dispatch)". Bench: engine-fp32-linear on both harnesses. Docs: eager-execution ternary rows and x·Wᵀ row; the wasm ternary gap is closed.
…ion and reduction kernels PlatformStorage.offHeapAllocator (Kotlin/Wasm) binds MemoryDomain.HOST_OFFHEAP to a backend's memory. WasmSimd.install() binds it to the SIMD128 sidecar's linear memory: WasmLinearStorage (Storage.OffHeap: pointer, bulk copies, slices, close frees) and WasmLinearTensorData (FloatArrayTensorData whose heap mirror is downloaded once on first access), so a ForwardScope frees sidecar activations at reset() and StorageCapabilities reports off-heap support in a browser. Ambient outputs are freed through a FinalizationRegistry; WasmSimd.residentBudgetBytes bounds what stays resident. WasmCpuOps keeps matmul / matmulWeightTransposed outputs resident and runs add/subtract/multiply/ divide, relu/sigmoid/silu/gelu/erf/tanh and sum/mean in the sidecar on resident operands (transcendentals on heap operands too); two heap operands keep the common path. toResident() uploads an input once. New shared C kernels in f32_ops.c (skainet_f32_binary/unary/reduce_all/reduce_axis; SIMD128 and NEON bodies, row-pool chunks), compiled into the native library and the sidecar. wasm-bench gains the laya.chain case and resident-operand variants of the add and sum scenarios.
…on A/B; chain line reports the budget
This was referenced Oct 20, 2026
aharakal
approved these changes
Oct 20, 2026
michalharakal
added a commit
that referenced
this pull request
Oct 21, 2026
CHANGELOG [0.62.0] - 2026-10-21 with the parity headline over #1363, #1364, #1368, #1369, #1426, #1427, #1428 and #1430, plus entries for the operator coverage matrix (#1370), the BitNet decode allocation and threading fixes (#1359, #1360, #1361, #1362), the ClassificationSupport extraction (#1429) and the toolchain bumps (Kotlin 2.4.21, kotest 6.2.6, Gradle 9.8.1). VERSION_NAME, CITATION, Antora version, README bom coordinate, What's New and contributors, Android tutorial coordinates; regenerated operator and kernel-support matrices; SKEEP-007 rollout step 3 marked shipped.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fourth step of the JVM ↔ wasm compute parity series (after #1363, #1364, #1368): activations stay resident in the SIMD128 sidecar between kernels, and the ops between two matmuls — elementwise arithmetic, the activations,
sum/mean— run there too. This is the "resident activations" step SKEEP-007 left open.Why. Every matmul on the wasm tier already ran in the sidecar, but its output was copied back to the Kotlin heap, and everything until the next matmul (bias add, GELU, gated multiply, residual add, the reductions of a norm) ran as scalar Kotlin on one thread. Kotlin/Wasm has no SIMD and no shared-memory threads, and copying an activation into the sidecar per op costs about what the scalar loop does — so the only way to use the sidecar for those ops is to not copy at all.
Storage.
PlatformStorage.offHeapAllocator(Kotlin/Wasm,skainet-lang-core) is a hook that bindsMemoryDomain.HOST_OFFHEAPto a backend's memory, the same pattern as the ops override; lang-core cannot depend on the wasm module.WasmSimd.install()binds it to the sidecar's linear memory:WasmLinearStorage : Storage.OffHeap(pointer, bulkcopyInto/copyFrom, slices,close()frees) andWasmLinearTensorData : FloatArrayTensorData(a resident tensor; its heap mirror is downloaded once, on the first access tobuffer, an element,copyToFloatArray()orview, so every common fast path keeps working on it unchanged).StorageCapabilities.current()reports off-heap support in a browser while installed; aForwardScopeorModelScopeasking forHOST_OFFHEAPgets sidecar memory that itsreset()/close()frees. Ambient outputs are registered with aFinalizationRegistryand freed when their tensor is collected;WasmSimd.residentBudgetBytes(768 MB by default) bounds what can be pinned — above it the kernels still run in the sidecar and the result is read back to the heap.Ops.
WasmCpuOpskeeps the outputs ofmatmul/matmulWeightTransposedresident, and runsadd/subtract/multiply/divide(same shape, scalar and last-dim bias broadcasts),relu/sigmoid/silu/gelu(both forms)/erf/tanhandsum/mean(all dims or one) in the sidecar when an operand is resident; the transcendental activations go there for heap operands too (the loop costs more than the upload). Two heap operands of an elementwise op stay on the common lane by design, so a notebook-stylea + bis unchanged.WasmCpuOps.toResident()uploads an input once for callers that want the chain to start resident.Kernels. New shared C file
f32_ops.c:skainet_f32_binary(op code, per-operand index mode),skainet_f32_unary(op code),skainet_f32_reduce_all,skainet_f32_reduce_axis, with SIMD128 and NEON bodies for the arithmetic, relu and the reductions, scalar libm for the transcendentals (same formulas as the Kotlin reference ops), and row-pool threading over output chunks. Compiled into every native build as well (the JVM keeps its Vector API paths; a binding is a few lines when wanted).Bench.
wasm-benchgainslaya.chain(x · Wᵀ, GELU, multiply, bias add, sum, end to end at Laya's MLP shape) andoperands=residentvariants ofengine-elementwise-addandengine-reductions-sum; the heap-operand lines stay as the like-for-like comparison with the JVM.Measured (Node, 12-core laptop; medians, residency on unless noted)
laya.chain(150×768 · (2304×768)ᵀ, GELU, multiply, bias add, sum — end to end)laya.chainwithSKAINET_WASM_RESIDENT=0(sidecar kernels, every output read back)engine-elementwise-add, 1M, resident operandsengine-elementwise-add, 1M, heap operands (unchanged path)engine-reductions-sum, 1M, resident operandengine-reductions-sum, 1M, heap operand (unchanged path)The chain is the number this step is about: the matmul alone is about 62 / 17 ms on the two sidecar tiers, so the four ops after it went from roughly 160 ms of scalar Kotlin to a few milliseconds. The A/B row shows the split: most of that is the kernels running in the sidecar at all; keeping the intermediates resident takes the rest. The other
engine-*scenarios are unchanged against #1368 within noise (the packed and ternary mt figures were verified back to back with and without residency).Verification
:skainet-backends:skainet-backend-wasm-simd:wasmJsNodeTest— new: an activation chain (matmul → gelu → multiply → bias add → relu/sigmoid/silu/tanh/erf/gelu-exact, subtract/divide with scalar and bias broadcasts, sum/mean over all and one axis) stays resident and matches the scalar lane; routing (heap operands common, transcendentals and matmul outputs resident,toResident, element/view/common-op reads through the mirror); scoped outputs freed atForwardScope.reset()with aStorageClosedExceptionafterwards, the budget fallback, and the storage round trip (zeroed allocation,copyFrom/copyInto, slice, close,StorageCapabilitieswhile installed/uninstalled). Plus the existing eight tests.f32_ops.ccompiled for wasm32 SIMD128 (emcc 4.0.14), the host portable branch and aarch64 NEON (syntax check); the JVM native library builds it (quick gate).scripts/pr-gate.sh --quick, full gate (lang-core and backend wasmJs/js/wasi test legs included).Next in the series: fused softmax, normalisations and banded SDPA kernels on both tiers (wasm + JVM FFM), then the browser IO gaps.