Skip to content

feat(kernels): ternary kernels on the wasm sidecar; direct x·Wᵀ kernel on the JVM - #1368

Merged
michalharakal merged 1 commit into
SKaiNET-developers:developfrom
michalharakal:feature/wasm-ternary-jvm-wt
Oct 20, 2026
Merged

michalharakal merged 1 commit into
SKaiNET-developers:developfrom
michalharakal:feature/wasm-ternary-jvm-wt

Conversation

@michalharakal

Copy link
Copy Markdown
Contributor

Summary

Third step of the JVM ↔ wasm compute parity series (after #1363 and #1364): the ternary kernels join the browser sidecar, and the JVM gets the direct x · Wᵀ kernel the sidecar has had since 0.61.0. Both directions of parity, both through the shared C tree.

Wasm: ternary kernels in the sidecar.

  • native/build-wasm.sh compiles skainet_ternary_f32.c and bitnet_gemv.c and exports skainet_ternary_f32_gemv, skainet_ternary_lmhead_stage1, skainet_bitnet_gemv_tq2_0.
  • The portable branch of skainet_ternary_f32.c (what MSVC and Emscripten build — the vendored NeoGPU file needs native pthreads and NEON) now holds the vendored kernel's 256-entry decode table as a compile-time constant (no init guard to race on), has a SIMD128 body for both the gemv and the fused four-plane lm_head (one LUT row is one f32x4 of decoded codes against four activations), and threads over output rows through the shared row pool. The scalar MSVC body computes exactly what it did before.
  • bitnet_gemv.c: a SIMD128 body for the TQ2_0 block (same unpack as the NEON one, i32x4.dot_i16x8_s on widened halves) and row threading over output rows on every tier; the NEON and scalar block bodies are unchanged.
  • WasmTernaryKernelPack (installed by WasmSimd.install()) registers three native faces — WasmTernaryF32Gemv, WasmTernaryLmhead, WasmBitNetGemv — through the same TernaryF32KernelPack / TernaryPlanesKernelPack / TernaryKernelPacks installs the JVM FFM and Android JNI packs use, so the dispatch keys and the kernel names (ternary_f32_gemv/wasm-simd128, …) are the same shape. Weights are uploaded once and stay resident; activation and output cross per call.

JVM: direct x · Wᵀ.

  • Fp32MatmulWtKernel and KernelProvider.matmulFp32Wt() (common), Fp32MemSegMatmulWtKernel and MemSegKernelProvider.matmulFp32WtMemSeg() (JVM) expose skainet_fp32_matmul_wt. NativeFp32MatmulWtKernel binds it with a heap face and a segment face; NativeKernelProvider carries both; KernelProvider.supports("matmul_wt", ["Float32", "Float32"]) reports it.
  • DefaultCpuOpsJvm.matmulWeightTransposed multiplies a dense [out, in] FP32 weight in place through the segment face: a segment-backed weight is read where it lies, a heap weight is made resident off-heap once (bounded cache keyed on the TensorData, replacing the transposed-copy cache for that case). No [k, n] transpose is materialised; the C kernel threads over output rows. Without the native library the cached-transpose path serves as before.
  • WasmKernelProvider.matmulFp32Wt() (WasmFp32MatmulWtKernel) exposes the same accessor on wasm; WasmCpuOps.matmulWeightTransposed routes through it.

Matrix, bench, docs.

  • Kernel support matrix: a fourth table, Kernel packs (view dispatch) — BitNet-b1.58 (FP32 activations), BitNet-planes, TQ2_0 (int8 activations) and Float32 x·Wᵀ direct per platform; the JVM cells are gated against the FFM provider and pack on the runner.
  • engine-fp32-linear in jvm-cpu-publish and wasm-bench: a Linear layer's x · Wᵀ (16 tokens, 768 → 3072) through ctx.ops.matmulWeightTransposed; --provider panama measures the cached-transpose path, --provider ffm the direct kernel.
  • eager-execution: ternary rows gain the wasm column, a x · Wᵀ direct row, the "Wasm ternary / BitNet" gap is removed.

Measured (Node, 12-core laptop; JVM on the same machine)

Scenario JVM wasm scalar wasm simd128-st wasm simd128-mt
engine-ternary-f32-gemv (2560×6912) 15.4 Gops (--provider ffm) skipped (no kernel) → runs 16.5 Gops 55.3 Gops
engine-fp32-linear (16×768 · (3072×768)ᵀ via ctx.ops.matmulWeightTransposed) panama (cached transpose + Vector GEMM) 5.0 GFLOPS → ffm direct 30.8 GFLOPS; scalar 1.1 2.6 7.8 30.0

The other engine-* scenarios are unchanged against #1364 within noise. On the JVM the direct kernel is 6× the cached-transpose path for this shape and allocates no transposed copy.

Verification

  • :skainet-backends:skainet-backend-wasm-simd:wasmJsNodeTest — new: ternary f32 gemv (two activation rows, 600 output rows so the mt sidecar splits them), planes lm_head and TQ2_0 bitnet gemv through KernelDispatch against the Kotlin reference kernels with the serving kernel name asserted; the direct x · Wᵀ provider kernel resident and windowed. Plus the existing eight tests.
  • :skainet-backends:skainet-backend-native-cpu:jvmTest — new NativeFp32MatmulWtKernelTest (heap face vs row-dot reference incl. the threaded regime, offsets, k == 0, segment face bit-identical to the heap face, provider accessors) and JvmDirectWeightTransposedTest (ops layer, heap and segment-backed weights, activation ranks 1–3, threaded rows, vs transpose-then-matmul); KernelSupportMatrixTest with the new table; the existing ternary FFM tests cover the vendored branch (unchanged).
  • C: the changed files compiled for wasm32 SIMD128 (emcc 4.0.14), the host portable branch (clang, scalar) and aarch64 NEON (bitnet_gemv.c, syntax check); CI macos-arm64 runs the NEON bitnet_gemv body with its row threading through the JNI-shaped C.
  • generateKernelMatrix, apiDump, scripts/pr-gate.sh --quick, full gate.

Next in the series: resident activations (Storage.WasmLinear) and sidecar elementwise/activation kernels, then fused softmax/norms/SDPA on both tiers, then the browser IO gaps.

…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.
@michalharakal
michalharakal merged commit 7fdb1ce into SKaiNET-developers:develop Oct 20, 2026
22 checks passed
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.

2 participants