Skip to content

In a server

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

Recomputing the whole layout on every request is unnecessary. Fit once, then place new points into the existing layout:

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 retains about 72% of a full fit's local overlap, and the layout stays stable across requests.
  • return_distances=True returns each point's distance to its nearest training neighbour, which serves as a fit score. A point 2 to 3× further out than the training mean is an extrapolation. A rising batch mean indicates that a refit is due.
  • to_npz and from_npz persist the model in a versioned numpy format. It survives releases, where a raw pickle would break on any dataclass change.
  • init=previous is an alternative: umap_project(window, 2, init=previous) reuses the previous coordinates so carried-over points start where they were, and new rows are placed by the caller. It refreshes a view without the fit/transform split.

A cached 5000×1024 model is about 20 MB, with training data stored as float32. Rolling windows and sparse input are not supported: densify sparse input first, and refit when the window slides.

Next