fastEmbedR provides native implementations of two widely
used nonlinear dimension-reduction workflows:
The same R interface selects native CPU, Apple Metal, or NVIDIA CUDA code. One-call functions include nearest-neighbor search, initialization, graph or affinity construction, and optimization. KNN-input functions let an existing neighbor graph be reused without repeating search. Explicit GPU requests either run on the requested GPU backend or fail with an informative error; they are never silently reported as GPU work after falling back to CPU.
Every backend-capable function accepts backend = "cpu",
"metal", or "cuda". An explicit function
argument is the clearest choice for a reproducible script:
fit_cpu <- fastEmbedR::umap(
x,
backend = "cpu",
n.cores = 4,
seed = 4
)
tsne_cpu <- fastEmbedR::tsne(
x,
perplexity = 10,
backend = "cpu",
n.cores = 4,
seed = 4
)
fit_metal <- fastEmbedR::umap(x, backend = "metal", seed = 4)
fit_cuda <- fastEmbedR::umap(x, backend = "cuda", seed = 4)n.cores is a positive integer controlling CPU parallel
work, including the native nearest-neighbor search and the CPU graph,
affinity, and optimization stages that support parallel execution. For
UMAP, the optimizer currently uses at most four workers and records the
requested and effective values. When n.cores is omitted,
KNN uses one worker, UMAP selects a size-aware default from one to four
workers, and t-SNE uses its thread option, which defaults to four
workers. Metal and CUDA execute on their respective accelerators and
ignore n.cores; the argument does not control GPU
concurrency.
For an interactive session, set a default once and omit
backend from later calls:
old_backend <- getOption("fastEmbedR.backend")
fastEmbedR::fastEmbedR_backend("metal")
fastEmbedR::fastEmbedR_backend() # reports "metal"
u <- fastEmbedR::umap(x) # uses Metal
t <- fastEmbedR::tsne(x, perplexity = 10)
u.cpu <- fastEmbedR::umap( # explicit arguments take precedence
x,
backend = "cpu",
n.cores = 4
)
options(fastEmbedR.backend = old_backend)The equivalent base-R setting is
options(fastEmbedR.backend = "cpu"), "metal",
or "cuda". For a batch job, set
FASTEMBEDR_BACKEND before starting R, for example
FASTEMBEDR_BACKEND=cuda Rscript analysis.R. Resolution
precedence is:
backend function argument;options(fastEmbedR.backend = ...) or
fastEmbedR_backend();FASTEMBEDR_BACKEND environment variable;Before requesting an accelerator, inspect the installed build and runtime device support:
fastEmbedR::fastEmbedR_capabilities()[
, c("backend", "available", "device", "knn_engine", "unavailable_reason")
]The backend requested by the user and the backend that actually ran
are also recorded in returned objects, for example
fit_cpu$parameters[c("backend", "nn_backend", "n.cores")].
A requested Metal or CUDA backend must be available and execute
natively; otherwise the function stops with an informative error instead
of silently using CPU.
The public workflow is intentionally small:
| Goal | Function |
|---|---|
| Precompute native non-self neighbors | precompute_knn() |
| Run UMAP from a data matrix | umap() |
| Run interpolation-based t-SNE from a data matrix | tsne() |
| Reuse a KNN graph | umap_knn() or tsne_knn() |
| Compute reusable PCA scores or t-SNE initialization | pca() |
| Approximate a very large run with landmarks | Landmark functions |
| Place new observations in a fixed t-SNE reference | transform_tsne() |
| Quantify embedding quality | evaluate_embedding() |
fastEmbedR_api() is the normative inventory for the
installed release. It separates these canonical workflows from advanced
prepared-state interfaces, diagnostics, secondary graph utilities, and
the compatibility dispatcher, and reports accepted/returned classes,
backend support, residency, related methods, and lifecycle status.
api <- fastEmbedR::fastEmbedR_api()
api[api$tier == "canonical", c("function", "purpose", "return_class")]
#> function purpose
#> 1 fastEmbedR_backend Get or set the session backend.
#> 2 fastEmbedR_capabilities Report compiled and runtime backend capabilities.
#> 3 fastEmbedR_api Return this public API inventory.
#> 4 pca Backend-native randomized PCA/TSVD.
#> 5 precompute_knn Compute reusable non-self nearest neighbors.
#> 6 tsne Run the complete native t-SNE workflow.
#> 7 tsne_knn Run t-SNE from reusable neighbors.
#> 8 umap Run the opinionated fixed-policy UMAP workflow.
#> 9 umap_knn Run fixed-policy UMAP from reusable neighbors.
#> 10 select_landmarks Select a reusable reference subset.
#> 11 fit_landmark_model Fit a UMAP or t-SNE reference embedding.
#> 12 project_landmark_model Project original or new rows into a fixed reference.
#> return_class
#> 1 character scalar
#> 2 data.frame
#> 3 data.frame
#> 4 fastEmbedR_pca
#> 5 fastEmbedR_knn
#> 6 fastEmbedR_embedding
#> 7 numeric or float32 layout matrix
#> 8 fastEmbedR_embedding
#> 9 numeric or float32 layout matrix
#> 10 fastEmbedR_landmark_selection
#> 11 fastEmbedR_landmark_model
#> 12 fastEmbedR_embeddingBecause other R packages export generic names such as
umap() and pca(), use
fastEmbedR::umap() and fastEmbedR::pca() in
scripts, package code, and published analyses. fastEmbedR follows
ordinary R search-path rules and does not silently resolve masking
conflicts.
We use the four numeric Iris measurements for a small, reproducible
example. Iris is supplied by R’s separate datasets package
and is not bundled with fastEmbedR. The matrix is scaled once outside
the embedding calls, so standardize = FALSE prevents
repeated preprocessing.
library(fastEmbedR)
iris_data <- datasets::iris
x <- scale(as.matrix(iris_data[, 1:4]))
labels <- iris_data$Species
dim(x)
#> [1] 150 4UMAP uses a neighborhood size, whereas t-SNE uses perplexity. For this small dataset we use 15 neighbors and perplexity 10. Missing t-SNE optimization settings are chosen by the package’s native auto-configuration policy. Its default initialization is a small-scale randomized-PCA layout.
fit_umap <- umap(
x,
n_neighbors = 15,
graph_mode = "fuzzy",
standardize = FALSE,
n.cores = 2,
seed = 4
)
fit_tsne <- tsne(
x,
perplexity = 10,
standardize = FALSE,
n.cores = 2,
seed = 4
)
c(
UMAP_seconds = fit_umap$metrics$elapsed,
tsne_seconds = fit_tsne$metrics$elapsed
)
#> UMAP_seconds tsne_seconds
#> 0.227 0.131Both functions return a fastEmbedR_embedding object. The
full coordinates are in $layout; $metrics,
$timings, and $parameters preserve enough
context to audit a run.
old_par <- par(mfrow = c(1, 2), mar = c(1, 1, 2.5, 1), bg = "white")
plot_embedding(fit_tsne$layout, labels, "t-SNE")
plot_embedding(fit_umap$layout, labels, "UMAP (fuzzy graph)")Iris one-call CPU embeddings.
The backend recorded in the result is the backend that actually ran:
fit_umap$parameters[c("backend", "nn_backend", "n.cores", "graph_mode")]
#> $backend
#> [1] "cpu"
#>
#> $nn_backend
#> [1] "cpu"
#>
#> $n.cores
#> [1] 2
#>
#> $graph_mode
#> [1] "fuzzy"
fit_tsne$parameters[c(
"backend", "nn_backend", "n.cores", "perplexity",
"affinity_support", "affinity_support_k"
)]
#> $backend
#> [1] "cpu"
#>
#> $nn_backend
#> [1] "cpu"
#>
#> $n.cores
#> [1] 2
#>
#> $perplexity
#> [1] 10
#>
#> $affinity_support
#> [1] "standard"
#>
#> $affinity_support_k
#> [1] 30The Biobase package supplies
sample.ExpressionSet, a compact expression dataset with 26
biological samples, 500 measured features, and Case/Control annotations.
It provides an executable Bioconductor example without requiring a
network download or bundling another copy of the data in fastEmbedR.
Samples must occupy matrix rows, so the assay matrix is transposed
before analysis.
if (requireNamespace("Biobase", quietly = TRUE)) {
data_env <- new.env(parent = baseenv())
utils::data(
"sample.ExpressionSet",
package = "Biobase",
envir = data_env
)
expression_set <- data_env$sample.ExpressionSet
expression_x <- t(Biobase::exprs(expression_set))
expression_x <- scale(expression_x, center = TRUE, scale = FALSE)
expression_labels <- Biobase::pData(expression_set)$type
expression_pca <- fastEmbedR::pca(
expression_x,
ncomp = 2,
backend = "cpu",
n.cores = 2,
seed = 4
)
expression_tsne <- fastEmbedR::tsne(
expression_x,
perplexity = 5,
backend = "cpu",
n.cores = 2,
early_exaggeration_iter = 10,
n_iter = 25,
seed = 4
)
expression_umap <- fastEmbedR::umap(
expression_x,
n_neighbors = 10,
graph_mode = "fuzzy",
backend = "cpu",
n.cores = 2,
seed = 4
)
c(
samples = nrow(expression_x),
features = ncol(expression_x),
pca_components = ncol(expression_pca$scores)
)
table(expression_labels)
old_par <- par(mfrow = c(1, 2), mar = c(1, 1, 2.5, 1), bg = "white")
plot_embedding(expression_tsne$layout, expression_labels, "t-SNE")
plot_embedding(
expression_umap$layout,
expression_labels,
"UMAP (fuzzy graph)"
)
par(old_par)
}The two embedding functions deliberately expose different levels of
control. t-SNE exposes perplexity/support, initialization, iteration
counts, early and normal exaggeration, learning rate, momentum,
clipping, and exact-versus-FFT repulsion. Set
auto_config = FALSE and provide explicit values when
automatic iteration or stopping choices are not appropriate.
UMAP is an opinionated high-throughput implementation, not a
general-purpose hyperparameter interface. Users select
n_neighbors, metric, graph mode, preprocessing, backend,
seed, and CPU thread count. The release policy selects the remaining
values and records them in fit$parameters: 500 epochs below
10,000 observations and 200 for larger ordinary profiles (at least 300
for high-variability profiles), five negative samples, spread 1,
repulsion strength 1, min_dist 0.01 or 0.1 under the
wide-shell rule, and learning rate 1 or 1.25 under the same rule.
umap_init() makes package-native initialization
reusable, but umap() does not accept arbitrary initial
coordinates. The nearest-neighbor router targets recall 0.99 and
intentionally hides index tuning; supply an external KNN object when
search policy must be controlled independently. Fixed seeds do not
ensure bitwise Metal or CUDA identity because atomic update order can
differ.
These settings affect cluster compactness, convergence effort, and
the balance between attractive and repulsive updates. Fixing them keeps
one tested policy aligned across CPU, Metal, and CUDA; it does not imply
that the selected values are universally optimal. Analyses requiring
min_dist, spread, epoch, learning-rate, repulsion,
negative-sample, or optimizer-mode sweeps should use a general-purpose
implementation such as uwot or umap-learn.
Float32 input, reusable KNN, prepared graphs, compact t-SNE support, and
landmarking remain explicit memory/speed controls; compact support and
landmarking must be described as approximations.
The two methods answer related but different questions.
| Property | UMAP | interpolation-based t-SNE |
|---|---|---|
| Model | Weighted or binary KNN graph | Sparse affinities |
| Main scale parameter | n_neighbors |
perplexity |
| Initialization | Graph-native initialization | Randomized PCA by default |
| Repulsion | Sampled negative edges | Exact or FFT-grid negative gradient |
| New data | Landmark projection | Fixed-reference transform |
UMAP emphasizes local neighborhoods with some graph-level organization. t-SNE emphasizes local probability neighborhoods and cluster organization.
Distances and empty space in either two-dimensional plot should not be treated as calibrated biological effect sizes. Compare methods with neighborhood and label-aware metrics rather than visual appearance alone.
Graph clustering is a secondary downstream utility; the primary package contribution is native UMAP and t-SNE embedding.
knn_graph() builds a compact undirected graph from raw
data, an embedding, or precomputed neighbors. It uses the existing KNN
route selected by backend; there is no second
nearest-neighbor method menu inside the graph API.
graph <- knn_graph(fit_tsne, k = 20, weight = "snn", n.cores = 2)
communities <- graph_cluster(graph, method = "leiden", seed = 4)
table(communities$membership)
#>
#> 1 2 3 4 5 6
#> 33 17 26 23 20 31Louvain and Leiden have native CPU, CUDA, and Metal backends and use
sparse graph storage. Select an accelerator explicitly with
graph_cluster(..., backend = "cuda") or
"metal"; an unavailable backend fails rather than falling
back to CPU. walktrap is the true Pons-Latapy random-walk
agglomeration method and remains CPU-only because its exact transition
matrix is intentionally limited to moderate graphs. Neither
igraph, cuGraph, nor an external clustering routine is
called at run time.
pca() exposes the backend-native truncated PCA used for
initialization. It deliberately omits an SVD-method menu: randomized SVD
is the supported decomposition family. For backend = "cpu",
n.cores sets the CPU BLAS/OpenMP thread limit for the call.
tsne_init = FALSE is the default; request it when the
t-SNE-ready layout is needed. irlba is not used by the
package implementation and appears only as an external benchmark
reference.
pca_fit <- pca(
x,
ncomp = 2,
center = TRUE,
scale = FALSE,
backend = "cpu",
n.cores = 2,
seed = 4,
tsne_init = TRUE
)
c(
score_rows = nrow(pca_fit$scores),
score_columns = ncol(pca_fit$scores),
init_max_sd = max(apply(as.matrix(pca_fit$tsne_init), 2, stats::sd))
)
#> score_rows score_columns init_max_sd
#> 1.5e+02 2.0e+00 1.0e-04The ordinary scores remain in pca_fit$scores. The added
pca_fit$tsne_init is centered and rescaled so the largest
component standard deviation is 1e-4, the small
initialization scale expected by t-SNE optimizers. CPU fits record the
requested and observable thread counts in n.cores_requested
and n.cores_effective. A single-threaded BLAS may still
report one effective thread. The initialization can be reused across
methods, seeds, or backends:
layout_tsne_fixed_init <- tsne(
x,
perplexity = 10,
Y_init = pca_fit$tsne_init,
standardize = FALSE,
n.cores = 2,
seed = 4
)CPU PCA uses the package-native float32 blocked RSVD path. Metal uses
a native float32 block-subspace TSVD with Metal Performance Shaders
matrix products. CUDA uses native RAPIDS RAFT TSVD when that support is
compiled. No backend calls Python or reticulate. Float32
CUDA input is passed to RAFT without an intermediate R double matrix,
and float32 scores and loadings are preserved.
The KNN-input API is useful when comparing embeddings, graph modes,
or seeds. precompute_knn() exposes the same
backend-specific native search selected by the one-call embedding
functions, without exposing a second algorithm menu.
knn <- precompute_knn(
x,
k = 30,
metric = "euclidean",
backend = "cpu",
n.cores = 2
)
knn
#> <fastEmbedR_knn>
#> observations: 150
#> neighbors: 30 (non-self)
#> metric: euclidean
#> backend: cpu
#> engine: native_cpu_hnsw
#> residency: host
#> elapsed: 0.008 sprepare_umap_knn() builds graph state and
epochs_per_sample once. prepare_tsne_knn()
validates, strips self-neighbors when needed, and stores the selected
affinity support. Its default uses three candidate neighbors per unit of
perplexity; affinity_support = "compact" explicitly selects
the older one-per-perplexity approximation. Prepared objects are useful
for repeated runs.
prepared_umap <- prepare_umap_knn(
knn,
graph_mode = "fuzzy",
n.cores = 2
)
prepared_tsne <- prepare_tsne_knn(knn, perplexity = 10)
layout_umap_reused <- umap_knn(
prepared_umap,
n.cores = 2,
seed = 4
)
layout_tsne_reused <- tsne_knn(
prepared_tsne,
Y_init = pca_fit$tsne_init,
n.cores = 2,
seed = 4
)
cbind(UMAP = dim(layout_umap_reused), TSNE = dim(layout_tsne_reused))
#> UMAP TSNE
#> [1,] 150 150
#> [2,] 2 2umap_init() separates graph construction and sparse
initialization from layout optimization. The returned object retains the
exact prepared graph and initial coordinates, so umap_knn()
can reuse both without rebuilding either stage. This is useful for
repeated seeds, backend agreement tests, and optimizer-only timing.
umap_initialization <- umap_init(
prepared_umap,
backend = "cpu",
graph_mode = "fuzzy",
n.cores = 2,
seed = 4
)
layout_from_initialization <- umap_knn(
umap_initialization,
backend = "cpu",
n.cores = 2,
seed = 4
)
umap_initialization$timings
#> stage elapsed_sec
#> 1 knn 0.000
#> 2 graph 0.000
#> 3 initialization 0.004
#> 4 total 0.004The initialization object can also be passed to a Metal or CUDA optimizer. That controlled comparison holds the graph and starting coordinates fixed; ordinary one-call GPU embeddings continue to use their resident backend path.
The graph mode is an explicit scientific choice, not a hidden speed setting.
graph_mode = "fuzzy" is the default and uses locally
scaled fuzzy membership strengths from the standard UMAP graph
construction.graph_mode = "binary" symmetrizes unit-weight neighbor
edges and then uses the same UMAP optimizer. It is an explicit
sensitivity mode and can produce stronger separation on some
datasets.layout_binary <- umap_knn(
knn,
graph_mode = "binary",
n.cores = 2,
seed = 4
)
layout_fuzzy <- umap_knn(
knn,
graph_mode = "fuzzy",
n.cores = 2,
seed = 4
)old_par <- par(mfrow = c(1, 2), mar = c(1, 1, 2.5, 1), bg = "white")
plot_embedding(layout_binary, labels, "Binary graph")
plot_embedding(layout_fuzzy, labels, "Fuzzy graph")UMAP graph modes use the same optimizer.
Record the chosen mode whenever embeddings are reported or compared.
All native embedding buffers use float32 and integer indices. A
standard R numeric matrix is converted at the native boundary. Supplying
a float::float32 matrix avoids retaining an unnecessary
double-precision copy in R, and the returned layout remains float32.
if (requireNamespace("float", quietly = TRUE)) {
x_float <- float::fl(x)
fit_float <- umap(
x_float,
n_neighbors = 15,
graph_mode = "fuzzy",
standardize = FALSE,
n.cores = 2,
seed = 4
)
c(
input_class = class(x_float)[1],
layout_class = class(fit_float$layout)[1]
)
}
#> input_class layout_class
#> "float32" "float32"For large datasets:
keep_knn = FALSE unless neighbor matrices are
needed later;transform_tsne() places query observations into an
existing reference embedding without moving the reference points. It
builds query-to-reference affinities, initializes each query near its
reference neighbors, and optimizes only the query coordinates. Reference
and query data must have undergone the same preprocessing.
For this executable example, every fifth Iris observation is treated
as new. We compute its exact query-to-reference KNN directly. The same
API accepts any plain host list with query-to-reference
indices and distances.
query_rows <- seq(5L, nrow(x), by = 5L)
reference_rows <- setdiff(seq_len(nrow(x)), query_rows)
reference_data <- x[reference_rows, , drop = FALSE]
query_data <- x[query_rows, , drop = FALSE]
reference_fit <- tsne(
reference_data,
perplexity = 10,
standardize = FALSE,
n.cores = 2,
seed = 4
)
exact_query_knn <- function(reference, query, k) {
squared_distances <- outer(
rowSums(query^2), rowSums(reference^2), "+"
) - 2 * tcrossprod(query, reference)
squared_distances[squared_distances < 0] <- 0
indices <- t(apply(squared_distances, 1L, order))[
, seq_len(k),
drop = FALSE
]
distances <- matrix(
sqrt(squared_distances[
cbind(rep(seq_len(nrow(query)), each = k), as.vector(t(indices)))
]),
nrow = nrow(query),
byrow = TRUE
)
list(indices = indices, distances = distances)
}
query_knn <- exact_query_knn(reference_data, query_data, k = 15)
query_layout <- transform_tsne(
reference_fit,
knn = query_knn,
perplexity = 5,
n_iter = 100,
n.cores = 2,
backend = "cpu",
seed = 4
)reference_labels <- factor(labels[reference_rows], levels = levels(labels))
query_labels <- factor(labels[query_rows], levels = levels(labels))
par(bg = "white")
plot(
reference_fit$layout,
col = embedding_colors[as.integer(reference_labels)],
pch = 19,
cex = 0.65,
asp = 1,
axes = FALSE,
xlab = "",
ylab = "",
main = "Fixed-reference transform"
)
points(
query_layout,
col = embedding_colors[as.integer(query_labels)],
pch = 17,
cex = 1.05
)
legend(
"bottomright",
legend = c("Reference", "New"),
pch = c(19, 17),
bty = "n",
cex = 0.8
)
box(col = "#B8B8B8")Transform into a fixed t-SNE reference.
For Metal or CUDA, request the same backend explicitly. If the native fixed-reference path is unavailable, the call fails rather than relabeling a CPU transform as GPU work.
Landmarking embeds a representative subset and places the remaining
rows in that reference. It is never activated silently by
umap() or tsne(). The final layout still has
one row per input observation. Reference and projection searches use
fastEmbedR’s native KNN routes.
The staged API keeps selection, reference fitting, and projection separate. This is useful when the same landmark reference will be reused:
landmark_selection <- select_landmarks(
x,
landmarks = 0.5,
seed = 4,
n.cores = 2
)
umap_reference <- fit_landmark_model(
x,
landmark_selection,
method = "umap",
n_neighbors = 15,
graph_mode = "fuzzy",
backend = "cpu",
n.cores = 2,
seed = 4
)
umap_projected <- project_landmark_model(
umap_reference,
x,
refinement_epochs = 50,
n.cores = 2
)fit_landmark_model() invokes the ordinary production
umap() or tsne() implementation on the
reference rows. Consequently, UMAP graph_mode, optimizer
settings, seed, and backend are explicit and consistent with a full-data
fit. project_landmark_model() computes only the
query-to-reference neighbors. It does not recompute neighbors among
landmarks or among query observations. Passing a matrix of new
observations instead of x projects those observations into
the same fixed reference embedding.
The one-call functions below perform the same workflow when the intermediate selection and model do not need to be retained.
fit_landmark_umap <- landmark_umap(
x,
landmarks = 0.5,
n_neighbors = 15,
graph_mode = "fuzzy",
standardize = FALSE,
n.cores = 2,
seed = 4
)
fit_landmark_tsne <- landmark_tsne(
x,
landmarks = 0.5,
perplexity = 10,
standardize = FALSE,
n.cores = 2,
seed = 4
)
rbind(
UMAP = fit_landmark_umap$metrics[
1, c(
"elapsed", "reference_embedding_elapsed",
"landmark_projection_knn_elapsed", "landmark_refinement_elapsed"
)
],
TSNE = setNames(
fit_landmark_tsne$metrics[
1, c(
"elapsed", "reference_embedding_elapsed",
"landmark_projection_knn_elapsed", "transform_elapsed"
)
],
c(
"elapsed", "reference_embedding_elapsed",
"landmark_projection_knn_elapsed", "landmark_refinement_elapsed"
)
)
)
#> elapsed reference_embedding_elapsed landmark_projection_knn_elapsed
#> UMAP 0.352 0.344 0.004
#> TSNE 0.115 0.095 0.004
#> landmark_refinement_elapsed
#> UMAP 0.003
#> TSNE 0.016old_par <- par(mfrow = c(1, 2), mar = c(1, 1, 2.5, 1), bg = "white")
plot_embedding(fit_landmark_tsne$layout, labels, "Landmark t-SNE")
plot_embedding(fit_landmark_umap$layout, labels, "Landmark UMAP")Iris with 50% landmarks.
CPU uses native HNSW for query-to-reference search. Metal uses native exact search for small references and recall-tuned IVF-Flat for larger references, followed by fixed-reference transforms. CUDA uses native exact or IVF-Flat query search and keeps KNN, layouts, and refinement buffers resident on the GPU. The result records landmark count, fraction, projection backend, and separate reference, query-KNN, and transform timings so a landmark approximation cannot be mistaken for a full embedding.
evaluate_embedding() reports local, global, and
label-aware metrics. For large datasets it uses deterministic subsamples
rather than constructing a complete all-pairs distance matrix. Here all
Iris observations are small enough to evaluate directly. Supplying
reference_nn also avoids repeating the high-dimensional
neighbor search.
quality_umap <- evaluate_embedding(
x,
fit_umap$layout,
labels = labels,
k = 10,
primary_k = 10,
reference_nn = knn,
sample_size_for_local_metrics = nrow(x),
sample_size_for_global_metrics = nrow(x),
seed = 4,
method = "UMAP",
backend = "cpu",
dataset = "iris",
n.cores = 2
)
quality_tsne <- evaluate_embedding(
x,
fit_tsne$layout,
labels = labels,
k = 10,
primary_k = 10,
reference_nn = knn,
sample_size_for_local_metrics = nrow(x),
sample_size_for_global_metrics = nrow(x),
seed = 4,
method = "t-SNE",
backend = "cpu",
dataset = "iris",
n.cores = 2
)
quality <- rbind(quality_umap, quality_tsne)
quality[, c(
"method", "trustworthiness", "knn_preservation_10",
"distance_spearman", "silhouette", "label_knn_accuracy"
)]
#> method trustworthiness knn_preservation_10 distance_spearman silhouette
#> 1 UMAP 0.9769765 0.7220000 0.9038878 0.5498250
#> 2 t-SNE 0.9862305 0.7773333 0.9043164 0.4516718
#> label_knn_accuracy
#> 1 0.9733333
#> 2 0.9333333No single score is sufficient. Trustworthiness and neighbor preservation measure local structure; distance correlation is a coarse global diagnostic; silhouette and KNN label accuracy are meaningful only when labels are valid external annotations rather than tuning targets.
The backend argument has exactly three choices.
| Backend | One-call KNN | Embedding implementation |
|---|---|---|
"cpu" |
HNSW | Multithreaded C++ |
"metal" |
Exact or IVF-Flat | Metal kernels |
"cuda" |
Exact or IVF-Flat | CUDA kernels |
CPU t-SNE uses a fast Fourier transform (FFT) grid. Metal uses native Objective-C++ kernels. CUDA uses FAISS GPU or RAPIDS cuVS search and keeps embedding buffers resident on the device.
GPU use is explicit:
fit_metal <- tsne(x, perplexity = 10, backend = "metal", seed = 4)
fit_cuda <- umap(x, n_neighbors = 15, backend = "cuda", seed = 4)If the requested backend was not compiled or is unavailable at runtime, these calls stop. CPU fallback is not relabeled as Metal or CUDA. Because parallel reductions and atomic updates can be ordered differently, fixed seeds provide controlled stochastic initialization and sampling but do not promise bitwise identity between devices or thread counts.
The package owns the matrix and KNN-input interfaces, native
nearest-neighbor routes, PCA initialization, graph or affinity
construction, low-dimensional optimization, landmark transformation,
graph construction, community detection, quality metrics, and returned
metadata. A plain host KNN object can be supplied as
indices and distances, while a CUDA KNN result
produced by precompute_knn() can remain device-resident
through the CUDA embedding path. No external R nearest-neighbor package
is called by the production workflow.
The fuzzy path follows the graph construction and cross-entropy
optimization defined by McInnes, Healy, and Melville.
fastEmbedR changes the execution boundary rather than the
UMAP objective: graph weights, coordinates, schedules, and optimizer
state use compact float32 buffers, and dense graph intermediates are
avoided. CPU parallelizes row preparation and sampled-edge updates;
Metal retains graph and optimizer buffers in unified memory; CUDA
constructs and optimizes the graph from device-resident KNN arrays.
graph_mode = "fuzzy" is the standard default. The
explicit "binary" mode assigns unit weight to the symmetric
KNN union and is an adjacency-only sensitivity analysis for settings
where neighbor identity is more reliable than distance calibration. It
is not standard UMAP and is not guaranteed to be faster because every
retained edge receives the maximum sampling frequency.
The affinity definition and Kullback–Leibler objective follow van der
Maaten and Hinton. Interpolation-based repulsion follows FIt-SNE, while
the automatic two-phase optimization policy is informed by opt-SNE and
openTSNE. The default affinity support uses
ceiling(3 * perplexity) non-self candidates; the explicit
compact approximation uses ceiling(perplexity).
CPU uses a retained FFT-grid workspace, Metal uses package-native
scatter, transform, gather, attraction, and optimizer kernels, and CUDA
combines native kernels with cuFFT and persistent device buffers. PCA
initialization is created by pca(..., tsne_init = TRUE) or
internally by the one-call workflow. The fixed-reference transform
follows the published reference-space embedding procedure and keeps the
fitted reference coordinates unchanged.
knn_graph() builds weighted sparse graphs using
shared-neighbor, inverse- distance, or binary edges.
graph_cluster() provides native Louvain, Leiden, and exact
CPU Walktrap implementations. Louvain and Leiden have CPU, Metal, and
CUDA routes; Walktrap remains CPU-only because its exact transition
representation is quadratic in the number of vertices. The
implementations are tested against igraph on matched
weighted graphs, but igraph is not a runtime
dependency.
Landmark workflows separate selection, reference embedding, projection KNN, local correction, and optional fixed-reference refinement. They are explicit approximations: timings and returned parameters identify landmark and projection stages rather than presenting them as ordinary full-data embeddings.
An explicit accelerator request must execute on that backend or fail.
Returned objects record the resolved embedding backend, nearest-neighbor
route, precision, parameters, and stage timings. Backend availability
and diagnostic reasons are reported by
fastEmbedR_capabilities().
The package is distributed under the MIT license. Adapted source and
linked library boundaries, upstream commits, SPDX identifiers, and
retained notices are recorded in inst/NOTICE,
inst/COPYRIGHTS,
inst/THIRD_PARTY_DEPENDENCIES.json, and the repository’s
source-provenance documentation. Software used only for validation
remains outside the production call graph.
Before publishing an embedding analysis:
n_neighbors or
perplexity, and UMAP graph mode;sessionInfo() and the result’s
$parameters and $timings.sessionInfo()
#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 26.04.1 LTS
#>
#> Matrix products: default
#> BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.32.so; LAPACK version 3.12.0
#>
#> locale:
#> [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C LC_TIME=en_US.UTF-8
#> [4] LC_COLLATE=en_US.UTF-8 LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
#> [7] LC_PAPER=en_US.UTF-8 LC_NAME=C LC_ADDRESS=C
#> [10] LC_TELEPHONE=C LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
#>
#> time zone: Etc/UTC
#> tzcode source: system (glibc)
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] fastEmbedR_0.99.17 rmarkdown_2.32
#>
#> loaded via a namespace (and not attached):
#> [1] cli_3.6.6 knitr_1.52 rlang_1.3.0 xfun_0.61
#> [5] otel_0.2.0 generics_0.1.4 jsonlite_2.0.0 buildtools_1.0.0
#> [9] htmltools_0.5.9 maketools_1.3.2 sys_3.4.3 sass_0.4.10
#> [13] Biobase_2.73.2 evaluate_1.0.5 jquerylib_0.1.4 fastmap_1.2.0
#> [17] yaml_2.3.12 lifecycle_1.0.5 RhpcBLASctl_0.23-42 compiler_4.6.1
#> [21] Rcpp_1.1.2 float_0.3-3 digest_0.6.39 R6_2.6.1
#> [25] parallel_4.6.1 bslib_0.12.0 withr_3.0.3 tools_4.6.1
#> [29] BiocGenerics_0.59.12 cachem_1.1.0