umap-web
v0.2.0
Published
Standalone TypeScript implementation of UMAP for browsers and Node.js — memory-efficient CPU backend and WebGPU backend for large datasets. Functional parity with umap-learn.
Maintainers
Readme
umap-web
A standalone, production-grade TypeScript implementation of UMAP for browsers and
Node.js — functionally at parity with Python's umap-learn (pinned 0.5.9.post2),
with a memory-disciplined CPU backend (typed arrays + worker pool) and a WebGPU
backend for large datasets. Zero runtime dependencies.
Built for privacy-preserving, in-browser dimensionality reduction: your data never leaves the device. Everything — kNN construction, fuzzy graph, spectral init, SGD layout — runs locally, from a static page if you like.
Quickstart (Node)
import { UMAP } from 'umap-web';
const umap = new UMAP({ nNeighbors: 15, minDist: 0.1, seed: 42 });
const embedding = await umap.fitTransform({
data: myFloat32Array, // row-major
rows: nSamples,
cols: nFeatures,
});
// embedding: { data: Float32Array, rows, cols: 2 }
const more = await umap.transform(newPoints); // project held-out rows
const blob = umap.serialize(); // persist the fitted model
const restored = UMAP.deserialize(blob); // ... and reload it elsewherenumber[][] inputs and CSR sparse inputs ({ data, indices, indptr, rows, cols })
work too. Supervision: await umap.fit(X, labels) (Int32Array; -1 = unlabeled) or a
continuous target with targetMetric: 'l2'.
Quickstart (browser)
import { UMAP } from 'umap-web';
// CPU workers (no special headers needed — works on GitHub Pages):
const umap = new UMAP({ concurrency: 'auto', onEpoch: ({ embedding }) => draw(embedding) });
// WebGPU (Chrome/Edge on capable hardware):
const gpu = new UMAP({ backend: 'webgpu' });
const embedding = await gpu.fitTransform(X);
console.log(gpu.backendInfo); // { knn: 'webgpu', layout: 'webgpu' }backend: 'auto' (default) picks WebGPU when an adapter exists and the configuration
is GPU-supported, else CPU workers, else single-thread. Every run reports which backend
actually executed each stage via umap.backendInfo.
- Workers without COOP/COEP: SharedArrayBuffer is used when available
(
crossOriginIsolatedor Node); otherwise a first-class transfer-fallback path runs (that is what executes on GitHub Pages). - Node GPU (device injection): pass a
GPUDevice(e.g. from a Dawn binding such as thewebgpunpm package) via thedeviceoption; the backend uses it instead ofnavigator.gpu. Non-Chromium GPU stacks are best-effort.
Workers & bundlers
The CPU worker ships as a single self-contained ESM file (umap-web/worker,
31.5 KB raw / 8.2 KB gzip — zero bytes added to your main bundle) and is spawned with
the inline new Worker(new URL('./worker.js', import.meta.url), { type: 'module' })
pattern that bundlers detect statically:
webpack 5, Vite (build), Rollup: zero config — the worker is emitted as an asset automatically.
Vite (dev server): esbuild's dependency pre-bundling breaks the pattern. Either add
optimizeDeps: { exclude: ['umap-web'] }tovite.config, or pass the URL explicitly:import workerUrl from 'umap-web/worker?worker&url'; const umap = new UMAP({ concurrency: 'auto', workerUrl });esbuild / anything that doesn't rewrite worker URLs: copy
node_modules/umap-web/dist/worker.jsnext to your bundle (it has no imports, so a plain copy works), or pointworkerUrlat wherever you host it.CDN / import maps: browsers refuse cross-origin workers, so umap-web automatically re-fetches the worker and spawns it from a
blob:URL. With a CSP, same-origin needsworker-src 'self'; the CDN path needsworker-src blob:.Node:
worker_threadsare resolved from the installed package automatically;workerUrlaccepts afile://URL if you relocate the file.
If no worker can be spawned (bad URL, CSP, exotic bundling), fit never hangs: the
pool gives up after a 10 s init timeout, a warning explains what failed and points
here, and the run completes single-threaded — check umap.backendInfo.layout. An
explicit workerUrl is trusted as-is: if it fails, umap-web warns and goes
single-threaded rather than guessing other locations.
Parity philosophy
"Looks right" is not a spec. This library is validated against umap-learn 0.5.9.post2
by a two-layer oracle harness:
- Deterministic stage parity — every deterministic pipeline stage (metrics,
smooth_knn_dist, membership strengths, fuzzy set operations, supervised intersections,make_epochs_per_sample, spectral eigen-invariants, the transform stage) is compared against artifacts generated by the pinned Python package on the same inputs (tolerances: atol 2e-4 / rtol 1e-3 for float32-derived quantities). - Statistical embedding parity — final embeddings are compared against bands generated from 8-seed Python runs (trustworthiness, continuity, kNN-label-accuracy, Spearman of pairwise-distance ranks, Procrustes disparity). The bar: a umap-web embedding must be indistinguishable from another umap-learn run. 14 parity cells (PARITY.md) cover defaults, cosine/sparse inputs, supervision, densMAP, haversine output, transform, inverseTransform, multi-component spectral placement, and AlignedUMAP — on CPU and WebGPU backends.
Where the pinned wheel's executed behavior deviates from its source text (e.g. numba
fastmath folding in smooth_knn_dist), umap-web matches the executed oracle;
every such case is documented in DECISIONS.md.
GPU support matrix
| Feature | WebGPU | Notes |
|---------|--------|-------|
| kNN metrics | euclidean, sqeuclidean, cosine, correlation, manhattan, hamming, jaccard | others fall back to CPU with a logged notice |
| k (nNeighbors) | ≤ 32 | larger k falls back to CPU |
| Layout | euclidean output | fixed-point atomicAdd SGD (scale 2^20) |
| densMAP / non-euclidean output / supervision graph ops | CPU | layout falls back per-stage |
| transform / inverseTransform | CPU | GPU-fitted models build the search structure lazily on first transform() |
All kernels run under default WebGPU limits (≤ 8 storage buffers per stage, 16 KB workgroup memory) and request elevated buffer limits when available; candidate streaming keeps every dispatch watchdog-safe and no n×n buffer ever exists.
GPU failures are typed, never silent: buffer sizes are validated against the
granted device limits before allocation, kernels run under error scopes, and device
loss is tracked — all surfacing as UmapBackendError. Under backend: 'auto' a GPU
failure falls back to the CPU for that stage (seeded results stay reproducible; the
rng is restored to its pre-stage state); with explicit backend: 'webgpu' the typed
error propagates. Device acquisition is capped at 5 s so a stalled driver cannot hang
fit().
Performance (Apple M5 Pro, Chromium/Metal — see BENCHMARKS.md)
| Workload | Backend | Time | |----------|---------|------| | digits 1,797×64 | CPU single-thread | 4.5 s | | MNIST 70,000×784 | CPU workers | 24 s | | MNIST 70,000×784 | WebGPU | 7.8 s | | transform 10k onto 60k model | CPU | 5.3 s | | synthetic 500,000×128 | WebGPU | 78 s | | synthetic 1,000,000×256 | WebGPU | 5.5 min |
API surface
UMAP—fit,fitTransform,transform(+'graph'mode),inverseTransform,update,serialize/UMAP.deserialize,embedding,graph,densities,backendInfo,phaseTimings. Full option set mirrorsumap-learn(camelCase).AlignedUMAP— aligned embeddings over dataset slices withrelations,alignmentRegularisation,alignmentWindowSize;fit,fitTransform,update.umap-web/core— the low-level functional layer (nearestNeighbors*,smoothKnnDist,computeMembershipStrengths,fuzzySimplicialSet,makeEpochsPerSample,findABParams,initTransform/initGraphTransforminternals, spectral/LOBPCG, metrics, CSR helpers, PCG32).umap-web/webgpu— GPU device management + kernels for advanced integration.
Typed errors: UmapValidationError (bad input incl. NaN/Infinity — never silently
filtered), UmapBackendError (worker/GPU failures), both extending UmapError; plus
AbortError via standard AbortSignal:
const controller = new AbortController();
umap.fit(X, null, { signal: controller.signal }); // controller.abort() stops mid-epochfit, fitTransform, transform, inverseTransform, update, and
AlignedUMAP.fit/fitTransform/update all accept { signal }; layout aborts
per-epoch, GPU kNN per chunk, other phases at stage boundaries. An aborted or failed
fit releases its workers/GPU device and restores the previously fitted model (a
first fit stays unfitted) — the estimator is never left half-fitted.
Serialization (serialize/UMAP.deserialize) is a single versioned binary blob,
little-endian on every platform. deserialize treats the blob as untrusted:
every section is bounds- and type-checked and cross-array invariants are validated,
so a corrupt or crafted blob throws UmapValidationError instead of producing
garbage.
Determinism (§ reproducibility): with seed set and concurrency: 1, two runs are
byte-identical (PCG32 with published test vectors; Math.random is banned in the
source tree). Parallel CPU and WebGPU paths are documented non-deterministic;
deterministic: true forces the reproducible path.
Demo
apps/demo — a static Vite app (GitHub Pages-ready, base: '/umap-web/') with a
WebGL2 point renderer, dataset picker (digits / mnist10k / your own CSV — parsed
locally), live per-epoch animation, backend selector with actual-backend indicator,
phase-timing HUD, and CSV export.
Roadmap (deliberate non-goals today)
ParametricUMAP(needs a neural-network framework)umap.plot(the demo app plays this role)- scikit-learn API compatibility / estimator plumbing
- WASM backend (the
Backendinterface is designed so one can slot in) - GPU NN-descent (brute-force GPU kNN is fast through ~1M×256; see BENCHMARKS.md)
License
BSD-3-Clause. Reimplements the algorithms of umap-learn and pynndescent (see NOTICE).
