Portable mixed-precision math, linear-algebra, & retrieval library with 2'000+ SIMD kernels for x86, Arm, RISC-V, LoongArch, Power, & WebAssembly, leveraging rare algebraic transforms with both 1D & 2D registers like AMX & SME, covering 15+ numeric types from 4-bit integers & 6-bit floats to 128-bit complex numbers, validated against 118-bit extended-precision baselines with saturation, casting, & rounding edge-case coverage, in a 5-100x smaller binary than other BLAS-like alternatives, co-designed with Tensor abstractions in C++, Python, Rust, JavaScript, GoLang, & Swift.

Most libraries return dot products in the same type as the input — Float16 × Float16 → Float16, Int8 × Int8 → Int8.
This leads to quiet overflow: a 2048-dimensional i8 dot product can reach ±10 million, but i8 maxes out at 127.
NumKong promotes to wider accumulators — Float16 → Float32, BFloat16 → Float32, Int8 → Int32, Float32 → Float64 — so results stay in range.
| Input | NumPy + OpenBLAS | PyTorch + MKL | JAX | NumKong |
|---|---|---|---|---|
| ░░░░░░░░░░░░░░ | ░░░░░░░░░░░░░░ | ░░░░░░░░░░░░░░ | ░░░░░░░░░░░░░░ | |
f64 |
2.0 gso/s, 1e-15 err | 0.6 gso/s, 1e-15 err | 0.4 gso/s, 1e-14 err | 5.8 gso/s, 1e-16 err |
f32 |
1.5 gso/s, 2e-6 err | 0.6 gso/s, 2e-6 err | 0.4 gso/s, 5e-6 err | 7.1 gso/s, 2e-7 err |
bf16 |
— | 0.5 gso/s, 1.9% err | 0.5 gso/s, 1.9% err | 9.7 gso/s, 1.8% err |
f16 |
0.2 gso/s, 0.25% err | 0.5 gso/s, 0.25% err | 0.4 gso/s, 0.25% err | 11.5 gso/s, 0.24% err |
e5m2 |
— | 0.7 gso/s, 4.6% err | 0.5 gso/s, 4.6% err | 7.1 gso/s, 0% err |
i8 |
1.1 gso/s, overflow | 0.5 gso/s, overflow | 0.5 gso/s, overflow | 14.8 gso/s, 0% err |
Single 2048-d dot product on Intel Sapphire Rapids, single-threaded. Each cell shows gso/s, mean relative error vs higher-precision reference. gso/s = Giga Scalar Operations per Second — a more suitable name than GFLOP/s when counting both integer and floating-point work. NumPy 2.4, PyTorch 2.10, JAX 0.9.
A fair objection: PyTorch and JAX are designed for throughput, not single-call latency. They lower execution graphs through XLA or vendored BLAS libraries like Intel MKL and Nvidia cuBLAS. So here's the same comparison on a throughput-oriented workload — matrix multiplication:
| Input | NumPy + OpenBLAS | PyTorch + MKL | JAX | NumKong |
|---|---|---|---|---|
| ░░░░░░░░░░░░░░ | ░░░░░░░░░░░░░░ | ░░░░░░░░░░░░░░ | ░░░░░░░░░░░░░░ | |
f64 |
65.5 gso/s, 1e-15 err | 68.2 gso/s, 1e-15 err | ~14.3 gso/s, 1e-15 err | 8.6 gso/s, 1e-16 err |
f32 |
140 gso/s, 9e-7 err | 145 gso/s, 1e-6 err | ~60.5 gso/s, 1e-6 err | 37.7 gso/s, 4e-7 err |
bf16 |
— | 851 gso/s, 1.8% err | ~25.8 gso/s, 3.4% err | 458 gso/s, 3.6% err |
f16 |
0.3 gso/s, 0.25% err | 140 gso/s, 0.37% err | ~26.1 gso/s, 0.35% err | 103 gso/s, 0.26% err |
e5m2 |
— | 0.4 gso/s, 4.6% err | ~26.4 gso/s, 4.6% err | 398 gso/s, 0% err |
i8 |
0.4 gso/s, overflow | 50.0 gso/s, overflow | ~0.0 gso/s, overflow | 1279 gso/s, 0% err |
Matrix multiplication (2048 × 2048) × (2048 × 2048) on Intel Sapphire Rapids, single-threaded. gso/s = Giga Scalar Operations per Second, same format. NumPy 2.4, PyTorch 2.10, JAX 0.9, same versions.
For f64, compensated "Dot2" summation reduces error by 10–50× compared to naive Float64 accumulation, depending on vector length.
For f32, widening to Float64 gives 5–10× lower error.
The library ships as a relatively small binary:
| Package | Size | Parallelism & Memory | Available For |
|---|---|---|---|
| PyTorch + MKL | 705 MB | Vector & Tile SIMD, OpenMP Threads, Hidden Allocs | Python, C++, Java |
| JAX + jaxlib | 357 MB | Vector SIMD, XLA Threads, Hidden Allocs | Python |
| NumPy + OpenBLAS | 30 MB | Vector SIMD, Built-in Threads, Hidden Allocs | Python |
| mathjs | 9 MB | No SIMD, No Threads, Many Allocs | JS |
| NumKong | 5 MB | Vector & Tile SIMD, Your Threads, Your Allocs | 7 languages |
Every kernel is validated against 118-bit extended-precision baselines with per-type ULP budgets across log-normal, uniform, and Cauchy input distributions. Tests check triangle inequality, Cauchy-Schwarz bounds, NaN propagation, overflow detection, and probability-simplex constraints for each ISA variant. Results are cross-validated against OpenBLAS, Intel MKL, and Apple Accelerate. A broader throughput comparison is maintained in NumWars.
| Language | Install | Compatible with | Guide |
|---|---|---|---|
| C / C++ | CMake, headers, & prebuilt | Linux, macOS, Windows, Android | include/README.md |
| Python | pip install |
Linux, macOS, Windows | python/README.md |
| Rust | cargo add |
Linux, macOS, Windows | rust/README.md |
| JS | npm install & import |
Node.js, Bun, Deno & browsers | javascript/README.md |
| Swift | Swift Package Manager | macOS, iOS, tvOS, watchOS | swift/README.md |
| Go | go get |
Linux, macOS, Windows via cGo | golang/README.md |
NumKong covers 17 numeric types — from 6-bit floats to 64-bit complex numbers — across dozens of operations and 30+ SIMD backends, with hardware-aware defaults: Arm prioritizes f16, x86 prioritizes bf16.
| Operation | C 99 & C++ 23 | Python | Rust | JavaScript | Swift | GoLang |
|---|---|---|---|---|---|---|
| Vector Ops | ||||||
| Dot Product | ● | ● | ● | ● | ● | ● |
| Spatial Metric | ● | ● | ● | ● | ● | ● |
| Set Similarity | ● | ● | ● | ● | ● | ● |
| Geospatial | ● | ● | ● | · | ● | ● |
| Mesh Alignment | ● | ● | ● | · | · | · |
| Sparse Products | ● | ● | ● | · | · | · |
| Probability Divergences | ● | ● | ● | ● | · | ● |
| Curved Spaces | ● | ● | ● | · | · | · |
| Many-to-Many Vector Ops | ||||||
| "Dots" Products | ● | ● | ● | ● | ● | ● |
| "Spatials" Metrics | ● | ● | ● | ● | ● | ● |
| "Sets" Similarities | ● | ● | ● | · | ● | ● |
| MaxSim Scoring | ● | ● | ● | · | ● | ● |
| Scalar Ops | ||||||
| Cast | ● | ● | ● | ● | · | · |
| Reduce | ● | ● | ● | · | · | · |
| Each | ● | ● | ● | · | · | · |
| Trigonometry | ● | ● | ● | · | · | · |
The rest of this document unpacks the functionality and the logic behind the design decisions.
Most "optimized SIMD code" is a 2–4x unrolled data-parallel for-loop over f32 arrays with a serial scalar tail for the last few elements:
float boring_dot_product_f32(float const *a, float const *b, size_t n) {
__m256 sum0 = _mm256_setzero_ps(), sum1 = _mm256_setzero_ps();
size_t i = 0;
for (; i + 16 <= n; i += 16) {
sum0 = _mm256_fmadd_ps(_mm256_loadu_ps(a + i), _mm256_loadu_ps(b + i), sum0);
sum1 = _mm256_fmadd_ps(_mm256_loadu_ps(a + i + 8), _mm256_loadu_ps(b + i + 8), sum1);
}
float result = _mm256_reduce_add_ps(_mm256_add_ps(sum0, sum1));
for (; i < n; i++) result += a[i] * b[i]; // serial tail
return result;
}
This kind of unrolling has been a common request for NumKong, but the library avoids it by design.
Modern CPUs already "unroll" in hardware.
Out-of-order engines with reorder buffers of 320–630 entries (Zen 4: 320, Golden Cove: 512, Apple Firestorm: ~630) can keep a dozen of loop iterations in-flight simultaneously.
The physical register file is much larger than the ISA-visible architectural registers — Skylake has ~180 physical integer registers behind 16 architectural GPRs, and ~168 physical vector registers behind 32 architectural ZMMs.
The register renaming unit maps the same zmm0 in iteration N and iteration N+1 to different physical registers, extracting cross-iteration parallelism automatically — exactly the benefit that source-level unrolling was historically supposed to provide.
Unrolling works against NumKong's goals.
Every unrolled copy is a distinct instruction in the binary.
With 1,500+ kernel endpoints across 30+ backends, even 2x unrolling would inflate the .text section by megabytes — directly impacting install size for Python wheels, NPM packages, and Rust crates.
Larger loop bodies also increase instruction-cache and micro-op-cache pressure; Agner Fog also recommends:
"avoid loop unrolling where possible in order to economize the use of the micro-op cache".
A loop that spills out of the uop cache falls back to the slower legacy decoder, making the "optimized" version slower than the compact original. For a header-only library, unrolling also compounds compilation time: register allocation is NP-hard (reducible to graph coloring), and unrolling multiplies the number of simultaneously live ranges the allocator must consider, increasing compile time super-linearly across every translation unit that includes the headers.
Serial tails are a correctness hazard.
The leftover elements after the last full SIMD chunk run through a scalar loop that silently drops FMA fusion, compensated accumulation, and saturating arithmetic — producing results with different numerical properties than the SIMD body.
NumKong often uses masked loads instead (_mm512_maskz_loadu_ps on AVX-512, predicated svld1_f32 on SVE), processing every element through the same arithmetic path regardless of alignment.
It's not exactly orthogonal to loop-unrolling, but makes a different kernel layout more comp
$ claude mcp add NumKong \
-- python -m otcore.mcp_server <graph>