Whether a GPU is worth it depends on the computation, the size of the data and the hardware at both ends. This vignette reports what we measured, so you can judge for your own work, including the cases where the GPU loses.
The timings below were recorded by
bench/run-benchmarks.R in the package’s source repository,
not computed when this vignette was built (the machines that build R
packages have no GPU).
| GPU, driver | NVIDIA RTX A5500, 580.76.05 |
| CPU | Intel(R) Xeon(R) Gold 6336Y CPU @ 2.40GHz |
| CPU threads | 8 |
| BLAS | libopenblasp-r0.3.34.so |
| R | R version 4.5.3 (2026-03-11) |
| gputools | 2.0.0.9000 (commit 9e37078) |
| date | 2026-10-15 |
The CPU side is base R as most people run it: a multi-threaded BLAS
on 8 cores, which is what %*%, lm,
qr and solve use. dist,
hclust and cor do not use the BLAS and run on
one core.
Each time is the median of three runs after a warm-up run, in seconds. Data is in R’s memory at the start and the result back in R’s memory at the end, so the GPU times include copying both ways.
| Task | Size | base R | GPU single | speed-up | GPU double | speed-up |
|---|---|---|---|---|---|---|
| gpuMatMult vs %*% | 2000 x 2000 | 0.032 | 0.053 | 0.6x | ||
| gpuMatMult vs %*% | 4000 x 4000 | 0.220 | 0.337 | 0.7x | ||
| gpuCor pearson vs cor | 2000 obs x 1000 vars | 0.909 | 0.018 | 50.5x | 0.030 | 30.3x |
| gpuCor kendall vs cor | 1000 obs x 100 vars | 61.663 | 0.023 | 2681.0x | 0.256 | 240.9x |
| gpuDist vs dist | 5000 points x 50 dims | 2.433 | 0.358 | 6.8x | 0.405 | 6.0x |
| gpuHclust vs hclust | 5000 points | 0.748 | 0.735 | 1.0x | 1.015 | 0.7x |
| gpuDistClust vs hclust(dist()) | 5000 points x 50 dims | 3.158 | 0.547 | 5.8x | 0.814 | 3.9x |
| gpuGranger vs lm.fit loop | 500 times x 50 series, lag 2 | 0.415 | 0.099 | 4.2x | 0.112 | 3.7x |
| gpuLm vs lm | 100000 obs x 50 predictors | 0.140 | 0.125 | 1.1x | 0.142 | 1.0x |
| gpuQr vs qr | 4000 x 1000 | 1.256 | 0.880 | 1.4x | 1.030 | 1.2x |
| gpuSolve vs solve | 2000 x 2000 | 0.096 | 0.040 | 2.4x | 0.106 | 0.9x |
gpuMatMult only computes in double precision, so it has
no single column.
#> Warning in xinch(0.1): x log scale: xinch() is nonsense
Where the GPU wins. Computations that are large,
regular and many times over the same data: Kendall’s correlation (base
R’s is a direct O(n^2) loop per pair of variables), Pearson’s and
Spearman’s (one matrix product of standardised columns on the GPU),
distance matrices, Granger tests over many pairs of series, the combined
gpuDistClust, which keeps the distance matrix on the GPU
for the clustering, and solving and decomposing large matrices
(gpuSolve, gpuPrcomp).
Where it does not.
gpuMatMult, gpuLm: a modern multi-threaded
BLAS is already fast at these, and the GPU pays for copying the data
across and back. The GPU here is also a workstation card with slow
double precision.gpuHclust alone: the clustering is a sequence of n
small dependent steps, which leaves the GPU mostly idle; it pays off
only together with the distance computation
(gpuDistClust).Single or double precision. Consumer and workstation GPUs run double precision many times slower than single; data-centre GPUs do not. For statistics whose result is a correlation, a distance or a test statistic, single precision usually loses nothing that matters; for least squares on badly conditioned data, use double.
Every GPU call has a fixed cost: copying the data to the card and back, starting the computation, and the first time a function is used in a session, compiling its GPU code. On small data that fixed cost is most of the time, and base R is faster. Each function has a size above which the GPU wins. The table gives, for each, the smallest size tested at which it beat base R in single precision, and at which it was at least 5 times faster; the chart shows the whole range.
| Function | Size measured as | Tested | Faster than R from | 5 times faster from |
|---|---|---|---|---|
| gpuCor | variables (200 rows) | 10 to 3,000 | 30 | 300 |
| gpuCor spearman | variables (200 rows) | 10 to 3,000 | 10 | 30 |
| gpuCor kendall | variables (200 rows) | 5 to 300 | 5 | 5 |
| gpuDist | points (20 dimensions) | 50 to 3,000 | 50 | not reached |
| gpuHclust | points | 50 to 3,000 | 1,000 | not reached |
| gpuDistClust | points (20 dimensions) | 50 to 3,000 | 300 | not reached |
| gpuGranger | series (300 times, lag 2) | 5 to 100 | 5 | not reached |
| gpuLm | rows (20 predictors) | 1,000 to 1,000,000 | not reached | not reached |
| gpuGlm | rows (20 predictors) | 1,000 to 100,000 | not reached | not reached |
| gpuQr | columns (2 x as many rows) | 50 to 2,000 | 1,000 | not reached |
| gpuSolve | n (n x n system) | 50 to 3,000 | 300 | not reached |
| gpuPrcomp | variables (1,000 rows) | 20 to 1,000 | 100 | not reached |
| gpuMatMult | n (n x n by n x n) | 100 to 3,000 | not reached | not reached |
Filled points are single precision, open points double.
cor()
on the same two columns. Correlating 1,000 pairs of columns one
gpuCor call at a time took 0.065 s, about 4 times as long
as the same loop with cor() (0.015 s). One
gpuCor call on all 1,000 columns at once, computing every
one of the million pairs, took 0.015 s. Put the variables in the columns
of one matrix rather than looping.For thousands of variables or more, the whole correlation matrix is
the problem: 50,000 variables have 1,250,000,000 pairs, and their matrix
takes 20 GB. Usually only the strong pairs are wanted.
gpuCorPairs() returns those, every pair at or above a
threshold or each variable’s strongest partners, computing the matrix on
the GPU a block at a time and never holding it whole.
The table times finding every pair with |r| >= 0.8 among variables
of 100 observations (random data with a few hundred planted correlated
groups): base R’s cor() and then which(),
gpuCor() and then which(), and
gpuCorPairs(); and, last, gpuCorPairs()
finding each variable’s 10 strongest partners. Times are in seconds; “–”
was not run, the matrix being too big to wait for.
| Variables | Pairs found | cor + which | gpuCor + which | gpuCorPairs | top 10 each |
|---|---|---|---|---|---|
| 2,000 | 109 | 0.25 | 0.22 | 0.01 | 0.01 |
| 5,000 | 246 | 1.57 | 0.98 | 0.01 | 0.02 |
| 10,000 | 534 | 6.45 | 3.46 | 0.02 | 0.05 |
| 20,000 | 945 | – | 15.60 | 0.03 | 0.19 |
| 50,000 | 2,510 | – | – | 0.09 | 1.07 |
Most of the time cor() and gpuCor() take
here goes on the matrix itself, building it and searching it in R, which
gpuCorPairs() never does. Use it whenever you want pairs,
not the matrix.
gpuAssoc() tests every column of one matrix against
every column of another by linear regression with covariates: the scan
of an eQTL study, SNPs against genes. It computes what Matrix eQTL’s
linear model does (Shabalin 2012), so the comparison is with Matrix eQTL
itself, version 2.4, running on the multi-threaded OpenBLAS of the
machine above. Each scan has 500 samples and 5 covariates and reports p
<= 1e-5; both programs reported the same associations wherever both
ran.
| SNPs | Genes | Tests | Found | Matrix eQTL | gpuAssoc single | gpuAssoc double |
|---|---|---|---|---|---|---|
| 2e+03 | 5,000 | 10,000,000 | 147 | 0.35 | 0.05 | 0.09 |
| 1e+04 | 5,000 | 50,000,000 | 540 | 0.74 | 0.07 | 0.20 |
| 1e+04 | 20,000 | 200,000,000 | 2,194 | 2.31 | 0.14 | 0.58 |
| 5e+04 | 20,000 | 1,000,000,000 | 10,286 | 11.01 | 0.37 | – |
| 2e+05 | 20,000 | 4,000,000,000 | 40,351 | – | 1.20 | – |
Times are in seconds; “–” was not run (at four billion tests Matrix eQTL ran out of a 48 GB job’s memory). At 1,000,000,000 tests the GPU was 30 times as fast. A genome-wide scan, millions of SNPs, is a loop over chromosomes or blocks of SNPs, each call large enough to keep the GPU busy.
gpuKmeans() runs Lloyd’s algorithm, step for step as
kmeans(algorithm = "Lloyd") does. Base R’s default,
Hartigan-Wong, is also timed, since it is what most people run: it does
more work per pass but needs fewer passes, and usually ends at a lower
total within-cluster sum of squares from the same start. All runs start
from the same centres (50 dimensions, at most 20 iterations); the last
column is the GPU’s sum of squares relative to Hartigan-Wong’s. Times
are in seconds; “–” was not run, base R taking many minutes.
| Points | Clusters | kmeans Lloyd | kmeans Hartigan-Wong | gpuKmeans single | gpuKmeans double | sum of squares vs H-W |
|---|---|---|---|---|---|---|
| 1,000 | 10 | 0.00 | 0.00 | 0.008 | 0.002 | 0.995 |
| 10,000 | 10 | 0.11 | 0.04 | 0.020 | 0.028 | 1.000 |
| 10,000 | 100 | 0.65 | 0.17 | 0.015 | 0.028 | 1.091 |
| 100,000 | 10 | 1.16 | 0.58 | 0.206 | 0.247 | 1.001 |
| 100,000 | 100 | 7.96 | 2.08 | 0.208 | 0.359 | 1.067 |
| 100,000 | 1,000 | 45.74 | 14.50 | 0.245 | 0.803 | 1.038 |
| 1,000,000 | 100 | 80.25 | 12.72 | 2.111 | 3.492 | 0.999 |
| 1,000,000 | 1,000 | – | – | 2.781 | 11.986 | – |
The GPU pays off from about ten thousand points and a hundred
clusters, and most with many clusters. Where Lloyd’s algorithm ends at a
worse sum of squares, a few random starts (nstart) cost
little on the GPU and usually close the gap.
A permutation test repeats the whole computation for every shuffle of the samples, so a thousand permutations cost a thousand times the test: the case the GPU is best at. Both functions here also give Westfall and Young’s maxT adjustment for testing everything at once, which needs every permutation’s results together.
gpuCorPerm(), 1000 permutations of 50 samples, every
column of x against every column of y, against the same test in R: a
loop of cor() calls, and the faster loop of BLAS products
of standardised columns. Times in seconds; “–” was not run.
| x and y variables | Pairs | cor loop | BLAS loop | gpuCorPerm single | gpuCorPerm double |
|---|---|---|---|---|---|
| 50 | 2,500 | 0.25 | 0.06 | 0.048 | 0.071 |
| 200 | 40,000 | 2.71 | 0.73 | 0.046 | 0.072 |
| 1,000 | 1,000,000 | 69.88 | 20.39 | 0.125 | 0.485 |
| 3,000 | 9,000,000 | – | 200.20 | 0.929 | 3.484 |
gpuRowTtest(), Welch’s t test of every gene between two
groups of samples with 1000 permutations, against the same test in
vectorised R (each permutation’s t for every gene from
rowMeans and rowSums). The t tests themselves,
without permutations, run on the CPU and take the time in the last
column.
| Genes | Samples | R loop | gpuRowTtest single | gpuRowTtest double | no permutations |
|---|---|---|---|---|---|
| 1,000 | 20 | 0.28 | 0.009 | 0.008 | 0.001 |
| 20,000 | 20 | 4.75 | 0.025 | 0.037 | 0.011 |
| 20,000 | 100 | 23.41 | 0.063 | 0.101 | 0.032 |
| 50,000 | 100 | 56.81 | 0.157 | 0.262 | 0.090 |