Skip to contents

What changed

The development version keeps selected features sparse while preparing native CUDA PCA. It transposes the sparse matrix and uses cudaverse’s existing sparse PCA interface. The dense PCA input is created on the GPU. Inputs whose dense representation is smaller than 1 MiB retain the existing dense host path to avoid sparse setup overhead. CPU and torch execution also retain dense host preparation.

PCA and sparse arithmetic remain float64. Function arguments, result objects, feature ordering, cell identifiers, duplicate-name handling, exact neighbours and explicit TF32 policy are unchanged. GPU PCA still needs dense workspace; this is not an out-of-core decomposition.

Measured workflows

The October 19, 2026 comparison used one NVIDIA A100-SXM4-40GB host, Linux, R 4.3.3, cudaverse 0.4.2, native CUDA, and OMP_NUM_THREADS=2 / OPENBLAS_NUM_THREADS=2. The host exposed 32 virtual CPUs on AMD EPYC 7713; R used the system reference BLAS/LAPACK. The baseline was the unchanged submitted cudacellr 0.4.0 archive; the candidate runtime was commit b1b2262. Each workflow had five warmups and ten synchronized timed runs in fresh processes for each variant. All cases used k = 15, batch_size = 256, and scaled PCA.

Input Cells Genes Selected genes / PCs Median seconds, baseline → candidate Peak host GiB, baseline → candidate
Historical small synthetic 1,000 50 40 / 30 0.032 → 0.030 0.429 → 0.427
Historical medium synthetic 10,000 100 80 / 30 0.176 → 0.166 0.459 → 0.456
Historical large synthetic 50,000 128 102 / 30 1.394 → 1.164 0.698 → 0.611
Sparse stress fixture 50,000 10,000 2,000 / 50 9.435 → 3.367 3.473 → 0.663
10x PBMC v3 11,769 33,538 2,000 / 50 5.465 → 4.567 2.096 → 1.570

The sparse stress fixture has 2,549,762 nonzeros; PBMC has 24,825,783. PBMC comes from 10x Genomics, 10k PBMCs from a Healthy Donor (v3 chemistry), licensed CC BY 4.0. The input manifest pins the download and derived inputs by SHA-256 and records synthetic seeds and all workflow parameters.

The host was available for shared use, without an exclusive GPU reservation. The 200 before/after compute-process observations in the accepted comparison reported no other compute process. These observations do not rule out activity between samples or other host contention. The table reports this run’s observations; it does not promise the same speedup on other hardware.

Peak host memory is the whole R process high-water mark, including runtime, input and returned results. Timings include the public workflow and a final CUDA synchronization, with garbage collection, serialization and diagnostic queries outside the timed call. The initial all-sparse candidate failed the small-input runtime gate; its evidence is retained. The 1 MiB dispatch threshold resolved that regression. The unchanged baseline measurements were reused for the final candidate on the same host, R/core versions and pinned inputs.

All five cases passed exact normalization, HVG, identifier and neighbour-index comparisons, PCA reconstruction/subspace tolerances, distance tolerances and native allocation cleanup. The large sparse fixture exceeded the required 25% host-memory reduction (80.9%), and every case passed the maximum 10% end-to-end slowdown gate. PBMC host memory fell 25.1%.

Where the time goes

Separate single-pass stage profiles locate work; they are not warmed medians and should not be added to the warmed workflow timings above.

Candidate stage, seconds Sparse stress fixture PBMC
Normalization 0.473 1.811
Variable-feature selection 0.071 0.785
PCA preparation 0.069 0.207
PCA including upload 2.527 2.022
Exact neighbours 0.856 0.097

The next profiling target is dense PCA workspace and decomposition for the large sparse case. For PBMC, CPU normalization and feature selection together also merit attention. These observations do not justify changing numerical algorithms or precision without a separate correctness study.

Transfers and GPU memory

Separate Nsight Systems 2022.4.2 traces measured CUDA memory operations over the complete profile process, including capability probes and result copies. Profiler runs are excluded from the timing gate. MB below means 1,000,000 bytes.

Trace Host → GPU MB, baseline → candidate GPU → host MB, both GPU → GPU MB, both
Sparse stress fixture 800.016 → 8.176 29.865 800.000
PBMC 188.320 → 9.930 7.691 188.304

The additional native sparse row-index upload is included in these traces. The benchmark JSON’s smaller CSR payload estimate excludes that extra copy and is not a measured transfer total. Native PCA retains GPU scores, and kNN provenance confirms reuse of those scores without another upload.

Peak allocations tracked by cudaverse were 4.646 GiB for the candidate sparse stress workflow and 1.192 GiB for PBMC. The corresponding baseline peaks were 4.638 and 1.183 GiB: reducing host memory does not reduce dense PCA’s GPU workspace. These are package-owned allocation peaks, excluding CUDA context, library-internal allocations and other applications. Allow additional headroom.

Sizing and batch guidance

The table defines tested configurations, not maximum supported sizes. In particular, 50,000 cells with 2,000 selected genes was validated at about 0.5% input density; a denser 50,000-cell matrix can need substantially more host memory. The full sparse input and normalized output still reside in host memory.

One float64 selected dense matrix needs approximately 8 * cells * n_hvg bytes, before decomposition workspace. At 50,000 cells and 2,000 selected genes that is 0.745 GiB per copy. Lower n_hvg when dense GPU memory is the constraint. Reducing n_components alone does not shrink that input matrix.

Exact kNN’s distance block needs about 8 * min(batch_size, cells) * cells bytes, plus scores, outputs and workspace. For 50,000 cells, batch sizes 128, 256 and 512 imply approximately 48.8, 97.7 and 195.3 MiB for that block. Start with the measured value of 256; reduce it to 128 if available memory is tight. The smaller batch’s throughput is not measured here. Batch size preserves exact neighbour definitions and original row-index tie breaking. It does not change quadratic search time or PCA memory.

Remove unused results and call gc() before checking available memory with cudaverse::cuda_memory_info("cuda"). Keep GPU scores with their PCA model when continuing directly into cuda_cell_neighbors().

Reproduce and inspect

The repository’s benchmark evidence contains the input manifest, all timing samples, stage profiles, numerical gate results and transfer summaries. Maintainer scripts under tools/ describe the protocol and preserve failed attempts. Public derivatives remove unrelated process identities; original reports, result matrices and raw CUDA traces are retained with their hashes in the publication/development evidence.

Ordinary package tests exercise CPU execution, unavailable-CUDA errors and the SeuratObject/SingleCellExperiment adapters. Separate native CUDA regressions cover sparse preparation, duplicate identifiers, deterministic ties, PCA reconstruction and device-resident score reuse. A skipped CUDA test in a CPU-only package check does not replace this hardware validation.