Your first CUDA workflow with cudaverse

This guide takes you from an ordinary R matrix to PCA and nearest-neighbour results computed with CUDA. You stay in R, use familiar data structures, and do not need a deep-learning framework.

Before you start

Before opening R, make sure you have:

  1. Windows or Linux;
  2. a CUDA-capable NVIDIA GPU and working NVIDIA driver;
  3. a CUDA 12.x installation from NVIDIA; and
  4. the current cudaverse release.

Follow the GPU setup guide first if nvidia-smi does not show a GPU or the diagnostic call below reports missing libraries.

# install.packages("pak")
pak::pak("cudaverse/cudaverse@v0.4.1")

library(cudaverse)

check <- cuda_diagnostics()
check$summary
check$next_steps

# Stop here with a useful explanation if CUDA is not ready.
cuda_select_device("cuda")

If the last command succeeds, cudaverse is ready to use the GPU. If it fails, follow the printed next step; the requested CUDA task is never silently run somewhere else.

PCA and exact nearest neighbours on CUDA

This is the most useful first workflow. A numeric R matrix enters the package, PCA reduces it to 20 components, and exact kNN finds 15 neighbours per row.

set.seed(1)
x <- matrix(rnorm(10000 * 100), nrow = 10000, ncol = 100)

pca <- cuda_pca(
  x,
  n_components = 20,
  center = TRUE,
  scale. = FALSE,
  device = "cuda"
)

neighbors <- cuda_knn(
  pca$x,
  k = 15,
  metric = "euclidean",
  device = "cuda"
)

dim(pca$x)
dim(neighbors$index)
head(neighbors$index)
head(neighbors$distance)

The PCA scores can stay on the GPU while kNN uses them. You can confirm where the work ran:

cuda_provenance(pca)
cuda_provenance(neighbors)
cuda_memory_info("cuda")

GPU-resident matrix operations

Use a cudatensor when you want several matrix operations to reuse the same data on the GPU. to_cpu() brings the final result back as an ordinary R object.

x_gpu <- cuda_tensor(x, device = "cuda", dtype = "float32")

centered_gpu <- x_gpu - tensor_mean(x_gpu, dim = 1)
gram_gpu <- tensor_matmul(t(centered_gpu), centered_gpu)
column_totals_gpu <- tensor_sum(x_gpu, dim = 1)

tensor_device(gram_gpu)
gram <- to_cpu(gram_gpu)
column_totals <- to_cpu(column_totals_gpu)

Subsetting, transpose, arithmetic, matrix multiplication, and summaries follow familiar R conventions while supported results remain on the GPU.

Sparse normalization, PCA, and kNN

Sparse inputs begin as Matrix objects. The same workflow can normalize the matrix, run PCA, and find neighbours without making the user learn another API.

counts <- Matrix::rsparsematrix(10000, 100, density = 0.03)
counts@x <- abs(counts@x)

counts_gpu <- cuda_sparse(counts, device = "cuda")
normalized_gpu <- sparse_normalize(
  counts_gpu,
  margin = "rows",
  scale_factor = 10000,
  log1p = TRUE
)

sparse_pca <- cuda_pca(
  normalized_gpu,
  n_components = 20,
  device = "cuda"
)
sparse_neighbors <- cuda_knn(
  sparse_pca$x,
  k = 15,
  device = "cuda"
)

sparse_info(normalized_gpu)
cuda_provenance(sparse_neighbors)

Other CUDA tasks

The same strict device argument applies to decompositions, distances, and k-means:

svd_fit <- cuda_svd(x, nu = 20, nv = 20, device = "cuda")

distances <- cuda_distance(
  x[1:1000, ],
  x[1001:2000, ],
  batch_size = 256,
  device = "cuda"
)

clusters <- cuda_kmeans(
  pca$x,
  centers = 20,
  seed = 1,
  device = "cuda"
)

cuda_distance() returns a complete dense matrix, so its host output grows as nrow(x) * nrow(y). Prefer cuda_knn() when only the nearest neighbours are needed.

Where to go next