Skip to content

feat(data): skainet-data-source on Kotlin/JS and Kotlin/Wasm - #1430

Merged
michalharakal merged 7 commits into
SKaiNET-developers:developfrom
michalharakal:feature/wasm-data-source
Oct 20, 2026
Merged

michalharakal merged 7 commits into
SKaiNET-developers:developfrom
michalharakal:feature/wasm-data-source

Conversation

@michalharakal

Copy link
Copy Markdown
Contributor

Summary

Eighth and last step of the JVM ↔ wasm parity series (after #1363, #1364, #1368, #1369, #1426, #1427, #1428): skainet-data-source was the one IO module without a web target, so a browser build that wanted a Hugging Face file had to hand-roll fetch, the token header and the checksum. The module now builds for Kotlin/JS and Kotlin/Wasm beside the JVM.

WebDataSourceResolver (webMain, shared by js and wasmJs) — the browser's DataSourceResolver: the same URIs (https://, hf://, hf+https://, file:), DataSourceRequest, cache policies, checksum verification and token precedence as JvmDataSourceResolver, over the browser's fetch through the Ktor JS engine (KtorRemoteDataSourceFetcher, the same class name and contract as the JVM's CIO fetcher). What a tab imposes is explicit:

  • No file system. Remote artifacts are cached in memory for the life of the resolver (a second resolve of the same URI fetches nothing); a resolved artifact has no localPath. A file: URI resolves only to bytes the page registered under that path — a file picker's or a drop's — and fails with DataSourceException otherwise. An application that wants a persistent cache passes its own DataSourceArtifactStore (Cache API, IndexedDB, OPFS); the engine takes no position on that policy.
  • No environment. The Hugging Face token comes from the request or the resolver; there is no HF_TOKEN.
  • Body read whole. Ktor's channel-to-kotlinx.io bridge is JVM-only, and the store buffers artifacts anyway.

InMemoryDataSourceArtifactStore (common) — the store behind the browser resolver and the natural one for tests: cachedKeys, cachedBytes, clear(), evict(key), registered local artifacts.

Sha256DataSourceChecksum (common) — SHA-256 in Kotlin (FIPS 180-4, incremental, hex() once), because crypto.subtle is asynchronous and cannot serve the resolver's incremental DataSourceHash. Byte-identical to MessageDigest; usable on every target.

Docs — the data-sources tutorial gains In a browser (what changes, CORS, the persistent-cache hook) and names the web artifacts; io-readers.adoc's per-target table gains the remote-artifact row; CHANGELOG.

No native target yet: Ktor's Darwin engine is in the catalog but unused and Curl is not, which is a separate decision.

Verification

  • jvmTest (52): new Sha256DataSourceChecksumTest (standard vectors including the 56-byte two-block message, chunked updates of every size around a block, hex() idempotent and finishing), InMemoryDataSourceArtifactStoreTest (cache / refresh / bypass / offline, checksum pass, fail and re-verify on a hit, registered-only file:, token scope, evict), Sha256DataSourceChecksumJvmTest against MessageDigest for every length 0–130 and 255 / 256 / 1023 / 4096 / 65 537 / 1 000 003 bytes fed in uneven pieces.
  • wasmJsBrowserTest and jsBrowserTest (47 each, Chromium under Karma): the common suites plus WebDataSourceResolverTest — the Ktor JS engine really fetches the Karma server's page (cache hit on the second resolve, bypass), a 404 propagates and Offline without a cache is a DataSourceException, checksum and token through the resolver, file: resolves registered bytes only.
  • apiDump, scripts/pr-gate.sh --quick, full gate (verifyNpmPins included: the Ktor JS engine added no npm package to either lockfile).

…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.
…sm sidecar and the JVM FFM provider

New shared C kernels in f32_fused.c — skainet_f32_softmax_rows, skainet_f32_layer_norm,
skainet_f32_rms_norm and skainet_f32_sdpa (grouped-query heads, causal, sliding window with
rightContext, per-batch key lengths, dense additive mask with trailing-aligned broadcast) —
with SIMD128 and NEON bodies and row-pool threading over query rows. SPI faces
Fp32SoftmaxKernel / Fp32NormKernel / Fp32AttentionKernel behind KernelProvider.softmaxFp32() /
normFp32() / attentionFp32() (supports("softmax" | "layer_norm" | "rms_norm" | "sdpa",
["Float32"])), served by NativeKernelProvider through FFM and by WasmKernelProvider.

DefaultCpuOpsJvm routes softmax/logSoftmax over the last dimension and scaledDotProductAttention
to the native kernels when the FFM provider is loaded; WasmCpuOps runs them in the sidecar with
resident outputs. The common SDPA's validation moves into protected helpers both overrides share.
FusedNormOps (lang-core) is an optional interface beside TensorOps that LayerNormalization
consults for FP32 input; only the sidecar and the JVM FFM op sets implement it, so recording and
differentiating op sets keep the decomposition.

Also fixes a latent loader failure: GCC with -ffast-math vectorised the expf/tanhf/erf loops of
f32_ops.c into libmvec calls the shared library is not linked against, which terminated the
process at the first JVM call into such a kernel; the transcendentals now go through opaque
scalar wrappers and libskainet_kernels.so has no libmvec reference.

Bench: engine-softmax, engine-layer-norm, engine-sdpa on both harnesses. Kernel matrix: three
fused rows. Docs, CHANGELOG, apiDump.
…worker-side blocking reads, sharded SafeTensors from Blobs, PlannerProfile.BROWSER, browser MemoryProbe

JsBlobRandomAccessSource (Kotlin/Wasm) addresses a Blob with Long positions and size
(Blob.slice takes 53-bit numbers; the Int path truncated a shard over 2 GB), moves bytes
into Kotlin through 4 MB stages in wasm linear memory instead of one interop call per byte
(219 ms -> 36 ms per 8 MB), and serves blocking reads beyond the preloaded window from the
Blob itself inside a Web Worker through FileReaderSync (supportsSyncReads, openSync,
hasFileReaderSync()); on the main thread such a read throws with the explanation. The
Kotlin/JS variant gets Long addressing and a copy-free ByteArray view.

ShardedSafeTensorsParametersLoader(indexJson, shardSource) and
StreamingShardedSafeTensorsReader.openFromIndexContent / openFromParsedIndex take the
index as text and a suspending provider that opens each shard by file name, so a sharded
checkpoint loads from Blobs or fetched bytes where there is no file system; withPolicy for
the new form. Both forms now open every shard once and close what was opened on failure.

PlannerProfile.BROWSER (Budget.RESERVE_BROWSER, BROWSER_AVAILABLE_BYTES): 256 MB reserved,
dense weights quantized to Q8_0 at load, KV auto-quantized past 80 %, nothing mapped;
forDevice() returns it on JS and wasmJs (expect/actual isWebHost); skainet-plan --profile browser.

MemoryProbe on Kotlin/Wasm sums the JS heap in use (performance.memory), the module's linear
memory and the installed OffHeapAllocator's heldBytes() (new default-null member; the SIMD128
sidecar reports resident weights + activations); Kotlin/JS reports the JS heap. Faults stay null.

Docs: io-readers gains the browser section and a per-target table naming each remaining
difference with its reason; planner and memory-model pages updated. Tests: Blob source under
Karma incl. the worker path (FileReaderSync polyfilled by a sync XHR on a Blob URL), sharded
checkpoint from Blobs end to end, common reader provider tests, JVM path-vs-provider parity,
browser profile rules. M2AcceptanceTest no longer couples "RSS known" to "faults known".
conv1d/conv2d/conv3d on dense FP32 were the last FP32 ops a forward pass
spends time in that still ran as the common per-element loop on wasmJs
(im2col + the Vector API on the JVM). New shared C kernel
skainet_f32_conv (f32_conv.c, every native build): one kernel for all
three ranks over three spatial dims; per batch and group the receptive
fields are gathered patch-major into caller scratch, threaded over
patches, and multiplied against the weight rows by the existing
skainet_fp32_matmul_wt SIMD128/NEON dot kernel; groups (depthwise
included), stride, padding, dilation and a [c_out] bias are geometry.

WasmCpuOps overrides the three convolutions: the weight resident after
its first call, a heap input staged, a resident one read in place, the
output resident, so conv -> relu chains never leave linear memory;
shapes the common validation rejects stay on the common path. Sidecar
rebuilt (one new export).

Kernel support matrix: a "Float32 conv1d / conv2d / conv3d (im2col +
GEMM)" row in the Kernel packs table (panama-vector on the JVM,
wasm-simd128 on JS/WASM). engine-conv2d joins jvm-cpu-publish and
wasm-bench with identical shapes. Docs: eager-execution gap bullet
rewritten, browser-lane status, benchmarks table, CHANGELOG.

Tests: WasmSimdParityTest gains the conv parity test (eleven cases incl.
depthwise, grouped, dilated, asymmetric padding, the 4-D bias form and
conv3d, a resident chain, weight resident once) against DefaultCpuOps.
Measured (1x64x56x56, 64 filters 3x3): wasm scalar 0.04 GFLOPS ->
simd128 st 6.8 / mt 22.0; JVM Vector API path 10.7.
The data-source module was JVM-only (Ktor CIO + the file system), so a
browser build that wanted a Hugging Face file hand-rolled fetch, the token
header and the checksum. It now builds for js and wasmJs beside the JVM.

WebDataSourceResolver (webMain, shared by both web targets): the same
URIs, DataSourceRequest, cache policies, checksum verification and token
precedence as JvmDataSourceResolver, over the browser's fetch through the
Ktor JS engine (KtorRemoteDataSourceFetcher, same name and contract as the
CIO fetcher). What a tab imposes is explicit: remote artifacts are cached
in memory for the life of the resolver (no localPath), a file: URI
resolves only to bytes the page registered under that path, the token
comes from the request or the resolver, the body is read whole (Ktor's
channel-to-kotlinx.io bridge is JVM-only). A persistent cache is an
application-supplied DataSourceArtifactStore.

Common: InMemoryDataSourceArtifactStore (the browser store and the one for
tests) and Sha256DataSourceChecksum, a SHA-256 in Kotlin for targets
without a synchronous platform digest, byte-identical to MessageDigest.

Docs: tutorial "In a browser" section and the web artifacts, io-readers
per-target row, CHANGELOG. Tests: common SHA-256 vectors and chunking,
in-memory store through the resolver on jvm/js/wasmJs, JVM comparison
against MessageDigest for lengths around every block boundary, and the web
resolver fetching from the Karma server under Chromium on both web targets.
@michalharakal
michalharakal merged commit ee5a994 into SKaiNET-developers:develop Oct 20, 2026
23 checks passed
@michalharakal
michalharakal deleted the feature/wasm-data-source branch October 20, 2026 14:44
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant