Performance
pixi run bench produces the CPU figures,
make -f cuda/Makefile bench and bench-device
the CUDA figures, and make -f target/Makefile bench the
offload figures.
CPU
Apple M4, one thread, pixi run bench, gfortran 16, 2^24
elements, minimum of seven runs, GiB/s of output, the median of three
passes in one session. The baseline is the intrinsic
random_number, gfortran's xoshiro256**. It returns reals
only, so the integer rows scale a real64 draw, all 32 bits
for int32 and 53 of the 64 for int64. Its
normals are Box-Muller pairs over one array of uniforms, in the row's
precision.
| Tandem | random_number |
|
|---|---|---|
rng%fill, real64 array |
17.0 | 9.8 |
rng%fill, real32 array |
16.3 | 4.8 |
rng%fill, int32 array |
19.6 | 4.3 |
rng%fill, int64 array |
18.9 | 8.1 |
rng%next_real64() in a loop |
4.5 | 3.1 |
rng%fill_normal, real64 array
(ziggurat) |
7.6 | 0.96 |
rng%fill_normal, real32 array
(Box-Muller) |
5.5 | 0.55 |
The fills run at the speed of the C library, and clang for the C part
(FPM_CC=clang) gives the same figures. The scalar loop pays
a call into C per draw.
GPU
NVIDIA A100 40 GB (PCIe), CUDA 12.8,
make -f cuda/Makefile bench, 2^28 elements, minimum of 21
cudaEvent timings after a half-second warm-up. All GPU
figures on this page come from one window on 2026-10-06 with no other
process on the GPU, and a second pass agreed within 2 %. The cuRAND
column is Philox4x32-10 of cuRAND 10.3.9 in the same program, by the
same method. cuRAND has no 64-bit integer output for Philox, so the
int64 row's cuRAND figure is curandGenerate
writing the same bytes as 32-bit words, marked "nearest".
| GiB/s written | cuRAND Philox4x32-10 | cuRAND call | |
|---|---|---|---|
tandem_device_fill_real64 |
1392 | 806 | curandGenerateUniformDouble |
tandem_device_fill_real32 |
1381 | 1329 | curandGenerateUniform |
tandem_device_fill_int64 |
1393 | 1348 | curandGenerate, nearest |
tandem_device_fill_int32 |
1383 | 1336 | curandGenerate |
These are the rates of tandem.cuh's tile kernel, about
the card's memory bandwidth.
Draws inside a kernel,
make -f cuda/Makefile bench-device, nvfortran 25.3 with
-gpu=lto. Each thread walks one chunk's K blocks, and the
timed kernels store with a grid stride. The cuRAND rows are kernels with
the same thread count, each thread drawing from its own Philox4x32-10
subsequence of curand_device and storing with a grid
stride:
| GiB/s written | cuRAND Philox4x32-10 | cuRAND call | |
|---|---|---|---|
tandem_dev_next_real64 in a kernel |
1099 | 1411 | curand_uniform_double |
tandem_dev_next_int32 in a kernel |
635 | 1334 | curand |
The kernel module keeps its words as int32 bit patterns,
a draw from the cached block costs one compare, and link-time
optimization inlines the draws into the kernel. Before those changes the
rows read 244 and 174 GiB/s. They stay below cuRAND's: each Tandem draw
advances the 64-bit stream position, aligns it and checks its block,
about 40 instructions per 32-bit draw in nvfortran's code, while a
cuRAND draw takes the next word of its four-word buffer.
OpenMP target and do concurrent
Throughput on an NVIDIA A100 40 GB (PCIe), nvfortran 25.3, CUDA
driver 570, 2^28 elements held on the device, minimum of seven timings
after two warm-up fills, in the window above. The CUDA Fortran and
cuRAND columns are the rows of make -f cuda/Makefile bench
above. cuRAND has no bounded integers, so that row's cuRAND figure is
curandGenerate, marked "nearest":
| fill | CUDA Fortran | _target |
_stdpar |
cuRAND Philox4x32-10 |
|---|---|---|---|---|
real64 |
1392 GiB/s | 786 | 781 | 806 |
real32 |
1381 | 387 | 388 | 1329 |
int64 |
1393 | 1290 | 1279 | 1348, nearest |
int32 |
1383 | 481 | 1223 | 1336 |
bounded int32, bound 1000 |
not measured | 194 | 319 | 1336, nearest |
These run below the CUDA fills and cuRAND because each iteration
writes whole blocks at a stride of one row, the direct kernel's pattern:
do concurrent and OpenMP target cannot stage rows through
shared memory as the tile kernel of tandem.cuh does. The
int64 fills reach the direct kernel's rate. The others are
compute bound in nvfortran's code for the portable module, whose words
are int64 values: the _target
int64 fill ran at 780 GiB/s until a signed division per
element became a shift. LLVM flang with OpenMP offload to NVIDIA was not
run: batserv01 has no flang with an NVPTX offload runtime and the
conda-forge flang lacks its intrinsic modules.