Skip to main content

fastumap

A UMAP and clustering library built for one thing: importing it costs milliseconds, not seconds — nothing compiles when you import it. Mostly NumPy and SciPy, with precompiled native kernels (Rust, built into the wheel) for speed at scale.

cold import added to image
import umap (umap-learn) ~21 s ~172 MB
import fastumap ~12 ms 0 MB

umap-learn's kernels are compiled by numba with LLVM the first time the package is imported — seconds on a laptop, minutes on a small CPU-throttled container. NumPy and SciPy are equally compiled, but they ship their machine code precompiled in the wheel, so they load immediately. fastumap reimplements the UMAP algorithm on top of them: the same mathematics, with nothing left to compile at import time.

Installation

pip install fastumap                 # one wheel — native kernels included
pip install fastumap[ann]            # adds an approximate kNN backend for large inputs (see below)

Usage

from fastumap import umap_project, spectral_project

xy  = umap_project(x, 2)                     # (n, 2)
xyz = umap_project(x, 3)                     # (n, 3)
red = umap_project(x, 10)                    # ~10-D for clustering (not just 2/3)
cos = umap_project(x, 2, metric="cosine")    # text / CLS embeddings
dm  = umap_project(x, 2, densmap=True)       # densMAP: keep dense/sparse regions distinct
  • metric="cosine" is recommended for encoder embeddings; Euclidean distance on unnormalised vectors is dominated by magnitude rather than the direction that carries meaning.
  • pca_dim=100 pre-reduces very wide inputs (e.g. 1024-dimensional embeddings) before the nearest-neighbour search. Neighbour overlap is preserved to within ~0.01. Off by default.

umap_project also accepts n_neighbors, min_dist, spread, n_epochs, negative_sample_rate, random_state, and chunk_count.

Supervised projection

Pass categorical labels as y to let the classes inform the layout — same-label points attract, so structure that is separable in the input space stays separable in 2-D instead of interleaving:

xy = umap_project(x, 2, y=labels)                     # labels: one int per point, -1 = unlabelled
xy = umap_project(x, 2, y=labels, target_weight=0.9)  # lean harder on the labels

target_weight ∈ [0, 1] (default 0.5) trades geometry against labels: 0.0 barely weakens inter-class edges, 1.0 cuts them entirely. -1 labels count as unlabelled (semi-supervised), and the default y=None is ordinary unsupervised UMAP, unchanged.

This ports umap-learn's categorical intersection. The UMAP paper only sketches the idea (arXiv:1802.03426, §7 Future Work), so umap-learn is the reference — see its supervised docs.

Clustering

cluster groups points directly — implemented ourselves (numpy and scipy plus the native k-means kernel), with no external clustering library. It does not compute a 2-D layout first: that layout is an iterative SGD and takes minutes at a million points, and clustering doesn't need it.

from fastumap import cluster

labels = cluster(embeddings, method="kmeans", n_clusters=20)     # k-means++ / Lloyd
labels = cluster(embeddings, method="spectral", n_clusters=20)   # non-convex clusters
labels = cluster(embeddings, method="dbscan")                    # density: finds k, noise = -1
labels = cluster(embeddings, method="hdbscan")                   # density, no eps to pick

hdbscan is the hierarchical version of dbscan: no eps at all, so clusters of different densities come out together (the root of the hierarchy is never a cluster, so a single-group dataset comes back all -1). kmeans and spectral take n_clusters and metric ("euclidean" / "cosine") and are deterministic given random_state. k-means is our own k-means++ seeding and Lloyd iterations with a blocked assignment step, so the n×k distance matrix is never materialised — it holds to the low-memory constraint at scale. spectral reuses fastumap's own normalised-eigenvector machinery, then k-means those. For a picture, call umap_project separately (usually on a sample) — clustering and visualisation are different jobs.

More knobs:

  • return_centroids=True returns a ClusterResult(labels, centroids, inertia) instead of just labels — the numbers you need to compare runs or pick k.
  • choose_k(x, k_min=2, k_max=10) picks n_clusters for you, by the inertia elbow. A heuristic: it lands on the true k give or take one when the clusters are clear.
  • assign_clusters(x_new, centroids) labels new points against already-fitted centroids — the predict half, no re-clustering.

For large inputs fastumap's native k-means kernel — compiled into the wheel (matrixmultiply SIMD GEMM + rayon over row-blocks) — replaces the inner loop: it clusters 1,000,000 × 128 into 64 groups in ~10 s on a 16-core machine (measured on SIFT1M), versus minutes for the numpy path, and engages automatically above 50k points.

Above 200k points it also switches to mini-batch k-means, which samples batch_size rows per iteration instead of sweeping all of them, so clustering is no longer something you need a big machine for. Measured on SIFT1M inside a docker --cpus=2 container: 5.2 s against full Lloyd's 25.3 s, at 1.010× the inertia — 4.9× faster for a 1% worse objective.

labels = cluster(x, method="kmeans", n_clusters=64)   # mini-batch above 200k rows
labels, cent = kmeans(x, 64, batch_size=0)            # force full Lloyd at any size
labels, cent = kmeans(x, 64, batch_size=5000)         # force mini-batch, your batch

The switch is on n alone, never on the core count, so the same input and seed give the same labels on every machine. One caveat worth knowing: on perfectly separated clusters mini-batch can leave two centroids inside one group (measured 4.8× worse inertia on synthetic blobs 8σ apart), because its running-mean update cannot walk a centroid back across an empty gap. Real embeddings overlap and do not trigger this — MNIST lands at 1.013× Lloyd — but batch_size=0 is there if your data is unusually clean.

Quality relative to umap-learn

fastumap stays within 0.01 neighbour overlap of umap-learn. Measured on MNIST (784-dimensional, seed 42). These numbers are generated, not pasted: run make bench-report to regenerate them into a dated report under docs/bench/, which records the machine, the load average and every package version they were measured on.

n method wall time overlap@15 global corr
5000 fastumap 22 s 0.341 0.320
5000 umap-learn 79 s 0.344 0.329
5000 pca 87 s 0.058 0.523
10000 fastumap 46 s 0.271 0.361
10000 umap-learn 50 s 0.268 0.387
10000 pca 120 s 0.038 0.487
20000 fastumap 57 s 0.207 0.358
20000 umap-learn 69 s 0.204 0.368
20000 pca 237 s 0.024 0.504

Read the quality columns, not the wall times: overlap@15 and global corr are deterministic, while the times above came from a loaded machine (see the report) and are inflated for every method. PCA is the control — it keeps global structure best and local structure worst, which is the trade-off UMAP exists to fix.

umap-learn's times reuse the numba compilation from its first fit; a fresh process pays roughly 20 s of compilation on every invocation, which fastumap never pays. Above ~50k points fastumap's exact O(n²) neighbour search becomes the bottleneck — pip install fastumap[ann] addresses that (see below). Raising chunk_count (default 1) recovers most of the remaining local-overlap gap at proportional cost and stays deterministic.

overlap@15 is the fraction of each point's 15 input-space neighbours retained after projection (read against a random baseline). global corr is the Spearman correlation of all pairwise distances, before versus after.

Speed per call

Cold start is fastumap's win. Per-call latency is the trade-off.

  • The native kernel ships by default, so per-call speed is close to umap-learn on large batches (it runs umap-learn's true in-place SGD, in Rust).
  • The numpy fallback (only an unbuilt source checkout uses it) is the slow one: ~2× slower at 1024 dims, n=5000 (~40 s vs ~20 s). A vectorised numpy SGD can't match numba's compiled loop.

So: pick fastumap when import and cold-start cost dominate. On raw per-call latency for big, high-dimensional batches, it's roughly even with umap-learn on the kernel path.

BLAS thread cap under a CPU quota

Some containers cap CPU (docker --cpus, Kubernetes/EKS limits, EC2 cgroups) but still report the host's full core count. BLAS then starts too many threads for the quota and thrashes under load.

fastumap caps the BLAS pool to the quota automatically — no configuration. Measured under docker --cpus=0.5, 16-core host, 4 workers: per-call time drops ~70 s → ~43 s (≈1.6×). Where no quota is visible, nothing is capped.

AWS Fargate is a no-op here: it meters CPU outside the cgroup, so the quota is invisible. But Fargate also reports a low core count, so BLAS doesn't oversubscribe there anyway.

Server usage

umap_project is thread-safe — it holds no module-level mutable state and seeds a fresh RNG per call — so it may be called from a worker thread (await asyncio.to_thread(umap_project, x, 2)).

Don't recompute the whole layout on every request. Fit once, then place new points into it:

from fastumap import fit, transform
from fastumap.projection import UMAPModel

model = fit(window, 2)                                 # cache it
xy, fit_distance = transform(model, pts, return_distances=True)   # coords + per-point fit

model.to_npz("layout.npz")                             # persist across restarts (versioned)
model = UMAPModel.from_npz("layout.npz")
  • transform places new points without a refit. It keeps ~72% of a full fit's local overlap, and the layout stays stable across requests.
  • return_distances=True gives each point's distance to its nearest training neighbour — a fit score. A point 2–3× further out than the training mean is an extrapolation. A rising batch mean means: time to refit.
  • to_npz / from_npz persists the model in a versioned numpy format. It survives releases; a raw pickle would break on any dataclass change.
  • init=previous is the other option: umap_project(window, 2, init=previous) reuses the old coordinates so carried-over points start where they were (you place any new rows). Use it to refresh a view without the fit/transform split.

A cached 5000×1024 model is about 20 MB (training data stored as float32). Not supported: rolling windows and sparse input — densify sparse input first, and refit when the window slides.

The native accelerator

fastumap ships a compiled Rust extension (fastumap._accel) inside the wheel — a reimplementation of the SGD layout kernel and the k-means kernel. The prebuilt wheels (abi3, Python 3.11+; Linux x86_64 and aarch64/Graviton, musl for Alpine, and Windows x86_64) carry it, so it is used automatically. macOS has no prebuilt wheel yet and builds from the sdist. On a platform without a prebuilt wheel, pip builds from the sdist, which needs a Rust toolchain. The NumPy path remains as a runtime fallback — if the extension is ever not present (for instance an unbuilt source checkout), fastumap still works, just slower. The SGD kernel is roughly 1.7–1.9× faster with slightly higher overlap.

Because it performs umap-learn's true in-place walk rather than the fallback's per-epoch approximation, it produces a different — higher-quality — layout for the same seed. Query the active path with:

import fastumap
fastumap.accelerator_active()   # True if the native kernel is present (the normal case)

Upgrading from fastumap-accel? It no longer exists as a separate package — its kernels are built into fastumap itself (0.2.1+). Just pip install -U fastumap and drop any fastumap-accel dependency; nothing else changes.

Large inputs (approximate kNN)

The exact neighbour search is O(n²) and dominates runtime above ~20k points. pip install fastumap[ann] adds an approximate backend — faiss HNSW, which ships prebuilt wheels (Linux x86_64/aarch64, macOS, Windows), so it installs without a compiler.

xy = umap_project(x, 2, knn="auto")   # default: exact for small n, approximate above 16k
xy = umap_project(x, 2, knn="approx") # force approximate (needs fastumap[ann])
xy = umap_project(x, 2, knn="exact")  # force the exact brute force

"auto" (the default) only switches to approximate when the extra is installed and n ≥ 16384, so small inputs stay bit-identical. It's deterministic and keeps ≥0.86 neighbour recall against exact. Measured on the neighbour search alone (256-dim):

n exact approx speedup recall@15
20000 71 s 29 s 2.4× 0.91
30000 142 s 50 s 2.8× 0.86

fastumap.ann_available() reports whether the backend is installed.

For a given input and seed, output is bit-identical across processes and machines in the same environment. Two things change it across different environments:

  • The native kernel vs the numpy fallback — the wheels carry the kernel and use it by default; an unbuilt checkout falls back to numpy and differs.
  • fastumap[ann] — the approximate neighbour graph above 16k points.

Treat both as part of the environment. accelerator_active() and ann_available() report which paths a run used, so a stored projection can record how it was made.

Guarantees

Each is enforced by a test:

  • Nothing compiles at import — no numba/llvmlite JIT. The base is NumPy, SciPy and the pure-Python threadpoolctl; the native kernels ship precompiled in the wheel, so they import in milliseconds too.
  • Import under 200 ms, deterministic (bit-identical) output, and thread-safe.
  • Bounded memory — the full n×n distance matrix is never materialised (blocked kNN); under 200 MB at 5000×1024.
  • Any output dimension — 2-D/3-D for pictures, ~10-D for clustering; fully type-checked under pyright strict.

Development

make check        # lint, type-check, and tests
make tox          # the suite across Python 3.11, 3.12, and 3.13
make bench-report # regenerate the quality + speed tables into a dated docs/bench/ report
make fargate      # import and fit timing under one CPU cap (docker --cpus=0.5 --memory=2g)
make constrained  # fit AND cluster timing under 0.5, 1 and 2 vCPU, into a dated report

License and attribution

fastumap is an independent reimplementation of the UMAP algorithm (McInnes, Healy & Melville, arXiv:1802.03426). It is not affiliated with or endorsed by the UMAP authors and is not a drop-in replacement; the public API is intentionally small. MIT licensed.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fastumap-0.2.18.tar.gz (53.3 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

fastumap-0.2.18-cp311-abi3-win_amd64.whl (266.4 kB view details)

Uploaded CPython 3.11+Windows x86-64

fastumap-0.2.18-cp311-abi3-musllinux_1_2_x86_64.whl (363.0 kB view details)

Uploaded CPython 3.11+musllinux: musl 1.2+ x86-64

fastumap-0.2.18-cp311-abi3-musllinux_1_2_aarch64.whl (331.3 kB view details)

Uploaded CPython 3.11+musllinux: musl 1.2+ ARM64

fastumap-0.2.18-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (364.2 kB view details)

Uploaded CPython 3.11+manylinux: glibc 2.17+ x86-64

fastumap-0.2.18-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (332.5 kB view details)

Uploaded CPython 3.11+manylinux: glibc 2.17+ ARM64

File details

Details for the file fastumap-0.2.18.tar.gz.

File metadata

  • Download URL: fastumap-0.2.18.tar.gz
  • Upload date:
  • Size: 53.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for fastumap-0.2.18.tar.gz
Algorithm Hash digest
SHA256 849582d937c246559472565c79191c3791d8f296494529c139cecef92937668e
MD5 0a15c881f759e25ca284f55fda714448
BLAKE2b-256 0699c65ff0af041709ffa0dab86d143be5efc9ed18c66a508f88003a2cf41e61

See more details on using hashes here.

File details

Details for the file fastumap-0.2.18-cp311-abi3-win_amd64.whl.

File metadata

  • Download URL: fastumap-0.2.18-cp311-abi3-win_amd64.whl
  • Upload date:
  • Size: 266.4 kB
  • Tags: CPython 3.11+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.2

File hashes

Hashes for fastumap-0.2.18-cp311-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 c81e89ee2b0dcd7dbaca9fe51dfdfff084230691cd82c1511b5814cf10c53841
MD5 38dd53d0df431d56a9b6b63867cb747f
BLAKE2b-256 3652296d2b47057e61267f7c32547b71d522fd1f5f0fc0575fe0eaf36676dec5

See more details on using hashes here.

File details

Details for the file fastumap-0.2.18-cp311-abi3-musllinux_1_2_x86_64.whl.

File metadata

  • Download URL: fastumap-0.2.18-cp311-abi3-musllinux_1_2_x86_64.whl
  • Upload date:
  • Size: 363.0 kB
  • Tags: CPython 3.11+, musllinux: musl 1.2+ x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for fastumap-0.2.18-cp311-abi3-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 cafe70fb318490368015f5e2a621e26e0a463dfccaea29154e8b4aaaface67fc
MD5 813c7b37e68f1627baa2b2e1a4a6c568
BLAKE2b-256 02756922d178bdf2aecc5a1cddc629b6ca48fc4df992805b49ed93319eb7267c

See more details on using hashes here.

File details

Details for the file fastumap-0.2.18-cp311-abi3-musllinux_1_2_aarch64.whl.

File metadata

  • Download URL: fastumap-0.2.18-cp311-abi3-musllinux_1_2_aarch64.whl
  • Upload date:
  • Size: 331.3 kB
  • Tags: CPython 3.11+, musllinux: musl 1.2+ ARM64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for fastumap-0.2.18-cp311-abi3-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 46eba9373d1fb71e47781e162dcc4740cdd382b8b9a08cbce4bd0d03e325e6bb
MD5 0933ad4e7af8159d58271c3607dce4a1
BLAKE2b-256 3e7aded447494133b27026c7f29fc55a4ec2742d20e00be06cdc70018a94ecab

See more details on using hashes here.

File details

Details for the file fastumap-0.2.18-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

  • Download URL: fastumap-0.2.18-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
  • Upload date:
  • Size: 364.2 kB
  • Tags: CPython 3.11+, manylinux: glibc 2.17+ x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for fastumap-0.2.18-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 90fb02e2ae3a0f7da16b5f8fddb1758e51f19eaede55149cbbeeab23f50e9ccc
MD5 b6ffa1be43d71c483e34397b7258f678
BLAKE2b-256 926b2d47ccf801b11a7dca91f93776d6693624066820b96b0e97beda105dbc51

See more details on using hashes here.

File details

Details for the file fastumap-0.2.18-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

  • Download URL: fastumap-0.2.18-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
  • Upload date:
  • Size: 332.5 kB
  • Tags: CPython 3.11+, manylinux: glibc 2.17+ ARM64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for fastumap-0.2.18-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 4cdd5f2c81ad86e405487499a85d7bbe261fffda6c4ffcf471d04caf2911114d
MD5 4b31dc2a7158bd9d1022ae547875368d
BLAKE2b-256 693de2325e63c5ad3e6caa5e220ad9684d8e7e54ad01008e8ade37c45e292b16

See more details on using hashes here.

Release history Release notifications | RSS feed

0.2.29

1 file

0.2.28

1 file

0.2.27

6 files

0.2.26

1 file

0.2.20

6 files

0.2.19

6 files

This release

0.2.18 This release

6 files

0.2.17

6 files

0.2.16

5 files

0.2.15

5 files

0.2.14

5 files

0.2.13

5 files

0.2.12

5 files

0.2.11

5 files

0.2.10

5 files

0.2.9

5 files

0.2.8

5 files

0.2.7

5 files

0.2.6

3 files

0.2.5

3 files

0.2.4

3 files

0.2.3

3 files

0.2.2

3 files

0.2.1

3 files

0.2.0

2 files

0.1.26

2 files

0.1.25

2 files

0.1.24

2 files

0.1.23

2 files

0.1.22

2 files

0.1.21

2 files

0.1.20

2 files

0.1.19

2 files

0.1.18

2 files

0.1.16

2 files

0.1.15

2 files

0.1.14

2 files

0.1.13

2 files

0.1.12

2 files

0.1.11

2 files

0.1.10

2 files

0.1.9

2 files

0.1.8

2 files

0.1.7

2 files

0.1.6

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page