# NumKong for JavaScript

> Mixed-precision math for every CPU and GPU, from JavaScript

It keeps the JS surface intentionally compact: dense distances, dot products, binary metrics, probability divergences, packed GEMM-like matrix multiplication, symmetric Gram matrices, dtype-tagged low-precision storage, typed views, and runtime capability inspection.

## Installation

The package targets Node `>= 22`.

```sh
npm install numkong
yarn add numkong
pnpm add numkong
bun add numkong
```

Installing picks the `@numkong/<platform>-<arch>` package with the prebuilt addon.
Where none matches or loads, the install compiles the addon through `cmake-js`, which needs CMake and a C compiler, and `node-gyp-build` loads it.
To build it by hand in a checkout, run `npm install --omit=optional`, so the published addon does not shadow yours, then `npm run prebuild` and `npm run build-js`.
Other native addons that link NumKong use CMake too: `add_subdirectory` or `FetchContent`, then `numkong::static`, as the [C README](https://www.unum.cloud/docs/numkong/c.md) shows.

## Quickstart

```ts
import { dot } from "numkong";

const a = new Float32Array([1, 2, 3]);
const b = new Float32Array([4, 5, 6]);
console.log(dot(a, b)); // 32
```

## Highlights

This SDK is deliberately smaller than Python or Rust.
Its job is to make the hot vector kernels easy to use from modern JavaScript runtimes.

__TypedArray-first API.__
Standard `Float32Array`, `Float64Array`, `Int8Array`, and `Uint8Array` work directly.
__DType tags for exotic storage.__
`f16`, `bf16`, fp8, and packed bits stay explicit.
__Owned and borrowed views.__
`Vector`, `VectorView`, and base tensor wrappers preserve dtype metadata.
__Portable runtime story.__
The same package can target native addons and WASM runtimes.
__No fake tensor-framework scope.__
This binding stays centered on the vector families it actually exports.

### Ecosystem Comparison

| Feature                      | NumKong                                                        | [mathjs][mathjs]                           | [tensorflow.js][tensorflow-js]                    |
| :--------------------------- | :------------------------------------------------------------- | :----------------------------------------- | :------------------------------------------------ |
| Operation families           | dots, distances, binary, probability, cast, packed, symmetric  | general arithmetic, matrix ops, statistics | matmul, elementwise, reductions                   |
| Precision                    | BFloat16 through sub-byte; automatic widening; Kahan summation | Float64 only; standard accuracy            | Float32 primarily; no sub-byte; standard accuracy |
| Runtime SIMD dispatch        | auto-selects best ISA per-thread across x86, ARM, RISC-V       | none; pure JS                              | fixed at build time via WASM SIMD or WebGL        |
| Packed matrix, GEMM-like     | `dotsPack` + `dotsPacked`; persistent packing; amortized       | `math.multiply` — no persistent packing    | `tf.matMul` — no persistent packing               |
| Symmetric kernels, SYRK-like | `dotsSymmetric`; upper triangle only; row-range partitioning   | no duplicate-pair skipping                 | no duplicate-pair skipping                        |
| WASM fallback                | yes — portable, runs in browser without native addon           | yes — pure JS, no native required          | yes — also WebGL/WebGPU                           |
| Bundle size                  | small                                                          | moderate                                   | large                                             |

[mathjs]: https://github.com/josdejong/mathjs
[tensorflow-js]: https://github.com/tensorflow/tfjs

## Scalar Types

JavaScript has no built-in `f16`, `bf16`, fp8, or packed-bit numeric model.
NumKong handles that with explicit dtype tags and wrapper arrays.

The supported low-precision types and their bit layouts are:

- `Float16Array`: 1 sign + 5 exponent + 10 mantissa bits, 2 bytes per element, range ±65504, supports Inf and NaN
- `BFloat16Array`: 1 sign + 8 exponent + 7 mantissa bits, 2 bytes per element, full `f32` dynamic range, supports Inf and NaN
- `E4M3Array`: 1 sign + 4 exponent + 3 mantissa bits, 1 byte per element, range ±448, no Inf, NaN is 0x7F only
- `E5M2Array`: 1 sign + 5 exponent + 2 mantissa bits, 1 byte per element, range ±57344, supports Inf and NaN
- `BinaryArray`: packed bits in bytes, 8 elements per byte

Use `.byteLength` for the exact payload size.

```ts
import { Float16Array, E4M3Array, DType, dot, angular } from "numkong";

const a16 = new Float16Array([1, 2, 3]);
const b16 = new Float16Array([4, 5, 6]);
console.log(dot(a16, b16, DType.F16));
console.log(a16.byteLength);

const a8 = new E4M3Array([1, 2, 3]);
const b8 = new E4M3Array([4, 5, 6]);
console.log(angular(a8, b8, DType.E4M3));
console.log(a8.byteLength);
```

If the underlying storage is a raw `Uint16Array` or `Uint8Array`, JS itself cannot know whether those bytes mean integers, `f16`, `bf16`, mini-floats, or packed bits.
That is exactly when the `DType` tag becomes mandatory.
`cast` converts many values between all supported types, naming both sides by their dtype string:

```ts
numkong.cast(f32Source, "f32", bf16Dest, "bf16");
numkong.cast(f32Source, "f32", e4m3Dest, "e4m3");
```

## Tensors and Views

The wrapper hierarchy exists to keep dtype and ownership explicit across addon and WASM boundaries.

- `TensorBase` carries `buffer`, `byteOffset`, and `dtype`
- `VectorBase` adds rank-1 semantics
- `VectorView` is a zero-copy borrowed wrapper over existing memory
- `Vector` owns its `ArrayBuffer`

```ts
import { VectorView, Vector, DType, dot } from "numkong";

const raw = new Uint16Array([0x3c00, 0x4000, 0x4200]); // f16 payload, not uint16 values
const view = VectorView.from(raw, DType.F16);

const owned = new Vector(3, DType.F32);
owned.toTypedArray().set(new Float32Array([4, 5, 6]));

console.log(dot(view, view));
console.log(dot(owned, owned));
console.log(owned.byteLength);
```

Use `VectorView` when the bytes already live somewhere else.
Use `Vector` when NumKong should own the storage.

Owned `Vector` and `Matrix` are fixed-capacity resizable.
`capacity` reports the allocated element ceiling.
`tryResize(...)` reshapes within capacity without moving the `ArrayBuffer`, so a `toTypedArray()` result stays valid.
`reserve(elements)` may allocate a new `ArrayBuffer` to grow, which __invalidates__ any `TypedArray` or `row()` handed out earlier.
`clear()` empties the length while keeping capacity.

## Dot Products

Dot products are separate from distances because dtype tagging and low-precision storage matter more here.

```ts
import { dot, inner } from "numkong";

const a = new Float32Array([1, 2, 3]);
const b = new Float32Array([4, 5, 6]);

console.log(dot(a, b));
console.log(inner(a, b)); // alias for ecosystem familiarity
```

For non-native numeric layouts, pass an explicit `DType` or wrap the storage in a typed NumKong view.

## Dense Distances

Dense distance entrypoints work directly on the standard numeric `TypedArray` types.

```ts
import { sqeuclidean, euclidean, angular } from "numkong";

const a = new Float32Array([1, 2, 3]);
const b = new Float32Array([4, 5, 6]);

console.log(sqeuclidean(a, b)); // equivalent shape to a manual sum((a[i] - b[i]) ** 2)
console.log(euclidean(a, b));
console.log(angular(a, b));
```

When the storage type is one of the standard JS typed arrays, dtype inference is automatic.

## Set Similarity

Binary metrics operate on packed storage rather than on generic boolean arrays.

```ts
import { toBinary, hamming, jaccard } from "numkong";

const a = toBinary(new Float32Array([1, -2, 3, -4, 5, -6, 7, -8]));
const b = toBinary(new Float32Array([1, 2, -3, -4, 5, 6, -7, -8]));

console.log(hamming(a, b));
console.log(jaccard(a, b));
```

This is a natural model for sign-quantized embeddings and semantic hashes.

## Probability Metrics

```ts
import { kullbackleibler, jensenshannon } from "numkong";

const p = new Float32Array([0.2, 0.3, 0.5]);
const q = new Float32Array([0.1, 0.3, 0.6]);

console.log(kullbackleibler(p, q));
console.log(jensenshannon(p, q));
```

These call the underlying SIMD divergence kernels directly.

## Runtime Dispatch

Capability detection is explicit, one `Device` at a time:

```ts
import { Capability, Device } from "numkong";

const cpu = Device.cpu();
console.log(cpu.capabilitiesEnabled()); // what dispatch runs within
console.log((cpu.capabilitiesEnabled() & Capability.haswell) !== 0n);

const metal = new Device("metal", 0); // throws without that GPU
console.log(metal.capabilitiesEnabled() & Capability.gpus);
```

`Device.cpu()` is the host CPU, and `new Device(kind, ordinal)` any device `Device.count(kind)` counts, with `kind` one of `"cpu"`, `"cuda"`, `"rocm"` or `"metal"`.
`capabilitiesEnabled()` is the one you usually want, and derives from two independent axes:

- `capabilitiesDetected()`: what the device, like this CPU or WASM host, can execute.
- `capabilitiesCompiled()`: what this build contains for devices of its kind, from the ISA probes at build time.
- `capabilitiesEnabled()`: what dispatch runs within, both axes at once, which the library settles as it loads.

`capabilitiesDetected()` describes the machine and says nothing about whether a kernel was compiled in, so a prebuild whose ISA probes failed still reports your CPU's full feature set while containing no SIMD kernels at all.
The enabled set always keeps the `serial` fallback, and the binding keeps no mask of its own: every kernel call passes `Capability.cpus`, which the library clamps to the enabled set.
`Capability` maps each lowercase capability name, like `haswell`, `neon`, `v128relaxed` or `ampere`, to its bit, and is built at load from the C library's own names; `cpus`, `devices` and `any` group them.
A WASM module has only the CPU, so `Device.count` throws for every GPU kind there.

The exact bitmask depends on whether you are running the native addon or a WASM runtime.

There is no `configure_thread` call in the JS binding.
Thread configuration is managed internally by the native addon or WASM runtime.

## Platform Notes

### Browser and WASM

The npm package includes pre-built WASM modules under `wasm/`, one per SIMD capability and address width: `numkong-wasm32-serial`, `numkong-wasm32-v128`, `numkong-wasm32-v128relaxed`, and `numkong-wasm64-v128relaxed`.
The `numkong/wasm` entry validates two tiny probe modules on import and picks the best wasm32 capability the engine accepts, so Chrome, Firefox, and Node get Relaxed SIMD, Safari and iOS WebKit get strict SIMD128, and an engine without SIMD gets `serial`.
Each module is its own package subpath, like `numkong/wasm32-v128`, so a bundler splits them and the page fetches only the one it runs.
A page without a bundler maps those subpaths itself:

```html
<script type="importmap">
  {
    "imports": {
      "numkong/wasm32-serial": "https://cdn.jsdelivr.net/npm/numkong@8/wasm/numkong-wasm32-serial.js",
      "numkong/wasm32-v128": "https://cdn.jsdelivr.net/npm/numkong@8/wasm/numkong-wasm32-v128.js",
      "numkong/wasm32-v128relaxed": "https://cdn.jsdelivr.net/npm/numkong@8/wasm/numkong-wasm32-v128relaxed.js"
    }
  }
</script>
<script type="module">
  import { dot, euclidean } from 'https://cdn.jsdelivr.net/npm/numkong@8/wasm/numkong.js';

  const query = new Float32Array([1.0, 2.0, 3.0]);
  const doc = new Float32Array([4.0, 5.0, 6.0]);
  console.log(dot(query, doc));          // 32 — SIMD-accelerated, client-side
  console.log(euclidean(query, doc));    // 5.196...
</script>
```

For self-hosted WASM, download the entries and the modules from a [GitHub Release](https://github.com/ashvardanian/NumKong/releases), serve them from one directory, and point the same import map at it.
Each module finds its `.wasm` binary relative to its own URL.

The opt-in `numkong/wasm-extended` entry loads `numkong-wasm64-v128relaxed`, with Memory64, threads and Relaxed SIMD, where the engine validates them and the page is cross-origin isolated, and throws otherwise.
Node runs it from version 24, whose Memory64 takes 64-bit sizes, while the package and the minimal entry keep Node 22 as the floor:

```javascript
const numkong = await import("numkong/wasm-extended").catch(() => import("numkong/wasm"));
```

Or import the subpath from a bundler or Node.js (without the native addon):

```javascript
import { dot } from "numkong/wasm";
```

To build the modules in a checkout, the `wasm32_emscripten` preset makes one per `NUMKONG_MARCH`, both into `build_wasm32_emscripten/`, where `npm run build-browser` bundles the loader beside them:

```sh
source ~/emsdk/emsdk_env.sh
cmake --preset wasm32_emscripten && cmake --build --preset wasm32_emscripten
cmake --preset wasm32_emscripten -D NUMKONG_MARCH=v128relaxed && cmake --build --preset wasm32_emscripten
npm run build-js && npm run build-browser
npm run test:wasm:emscripten
```

### Native Addon and WASM Runtimes

The top-level package is native-first.
It loads the compiled addon through `node-gyp-build`.

The repository also ships WASM wrappers.
Those are useful for portable or sandboxed environments.
They are not feature-complete replacements for the native SDKs.

Practical guidance:

- Use the native addon for lower host-call latency.
- Use the WASM path when portability matters more than absolute latency.
- Keep your expectations scoped to the vector and matrix-oriented API that this binding actually exports.
