Skip to content

Performance: GPU

Each figure is measured with the released 0.86 xcc, on an Apple MacBook Pro with an M4 Max (Metal), a Linux machine with an AMD Ryzen 9 9955HX and its integrated Radeon (Vulkan), and a Windows machine with an RTX 5080 and an RTX 3090 (CUDA and Vulkan). How the CPU code compares with other compilers is on Performance: CPU; GPUs on other machines are on Performance: networked.

A par block runs a loop’s iterations at once, across every CPU thread, on the GPU, or across several devices at once: the block is cut into tiles handed to every GPU the machine has, and to the CPU, together. The GPU is reached through:

  • Metal on Apple silicon;
  • CUDA on an NVIDIA GPU under Windows;
  • Vulkan on Linux, Windows and Android;
  • WebGPU in a browser.

Six programs in benchmark/par measure it:

  • mandelbrot: a 2048×2048 escape-time image, at most 256 iterations a pixel;
  • perlin: 2048×2048 improved noise, four octaves;
  • nbody: the force on each of 8192 bodies from all the others;
  • saxpy: y = a·x + y over 16 million integers, with a sum;
  • md5, md5_mask: the hashcat MD5 workload, hashing 64 million candidates through the full 64-round compression and summing the digests.

Each runs its block eight times and reports the best run. The single-device modes are one thread (XC_PAR=cpu XC_PAR_THREADS=1), all threads (XC_PAR=cpu) and GPU (XC_PAR=gpu). A block can also run on every GPU (XC_PAR=gpus) or on every device (XC_PAR=all), and XC_PAR_WORKERS=… names an exact set (cuda, vulkan, metal, cpu). auto, the default, times the sets the machine can form and keeps the fastest; it remembers the choice, per program and per machine, so later runs do not measure again. Every mode’s checksum must agree. On Windows GPU is CUDA and Vulkan the same work through Vulkan (XC_PAR_GPU=vulkan).

Apple MacBook Pro, M4 Max (12 performance and 4 efficiency cores), its GPU through Metal (ms; best of eight runs)

benchmarkone threadall threadsGPUall devicesautoGPU vs all threadsGPU’s first run
mandelbrot14411.92.0734 µs878 µs6.0× faster33.8
md511891024.24.84.124× faster26.7
md5_mask92477.43.53.63.522× faster38.3
nbody19.62.01.22.01.11.75× faster32.8
perlin78.86.51.11.11.25.7× faster24.0
saxpy1.3499 µs1.63.1502 µs3.3× slower28.6

AMD Ryzen 9 9955HX (16 cores, 32 threads), Linux, its integrated Radeon 610M (2 compute units, 128 shader lanes) through Vulkan (ms; best of eight runs)

benchmarkone threadall threadsGPUall devicesautoGPU vs all threadsGPU’s first run
mandelbrot57.93.39.79.82.73.0× slower28.7
md539416.610811316.06.5× slower135
md5_mask31413.588.187.713.26.5× slower116
nbody6.0506 µs3.43.6503 µs6.8× slower23.0
perlin1148.75.75.85.71.52× faster32.6
saxpy3.32.99.63.12.93.3× slower35.6

AMD Ryzen 9 5950X (16 cores, 32 threads), Windows, an NVIDIA RTX 5080 through CUDA and an RTX 3090 through Vulkan (ms; best of eight runs)

benchmarkone threadall threadsGPUall GPUsVulkanall devicesautoGPU vs all threadsGPU’s first runVulkan’s first run
mandelbrot25910.8143 µs161 µs269 µs160 µs143 µs75× faster95.8144
md583044.0875 µs707 µs1.6711 µs829 µs50× faster96.7144
md5_mask66936.1727 µs667 µs1.3670 µs729 µs50× faster95.0142
nbody1445.3400 µs417 µs548 µs415 µs398 µs13.3× faster95.6138
perlin18112.5440 µs444 µs1.0326 µs427 µs28× faster82.0142
saxpy5.14.15.45.430.13.54.01.33× slower114173

Chrome on the Apple M4 Max: wasm32 (one thread) and WebGPU (from 0.73) (ms; best of eight runs)

benchmarkone threadall threadsGPUautoGPU vs one threadGPU’s first run
mandelbrot366–1.91.6192× faster10.4
md53655–4.64.6795× faster26.3
md5_mask2882–3.73.7779× faster21.9
nbody52.8–1.31.441× faster5.9
perlin91.3–2.42.438× faster8.8
saxpy1.4–21.11.515.1× slower58.0

These blocks are sized so that a single GPU stays busy for tens of milliseconds — long enough that the tile queue’s fixed per-tile cost is amortised and spreading the block across a second GPU cuts the wall-clock time. The benchmarks above each finish on one modern card in around a millisecond, too little for a second card to help; these are the opposite case. There are no one thread, all threads or all devices columns: at this size a single CPU thread takes minutes per run, and auto would never put a saturating GPU block on the CPU, so only the GPU paths are measured. On a one-GPU machine all GPUs is the same device as GPU and is dropped; the point is the machine with two.

These figures are from the development compiler for 0.8.1, not released 0.8: they need two fixes landing in that release — a narrow integer now converts to float in a CUDA (PTX) kernel instead of dropping the kernel to a Vulkan fallback, and the tile queue keeps a card’s second back end in reach so a block can spread across both NVIDIA cards. On released 0.8 this block stays on one card. The two cards are not a matched pair — an RTX 5080 and an older RTX 3090 — so the gain is not 2×; read it from the slower card, which is the fairer baseline: the 3090 alone against the two together. auto still keeps this block on the single faster card for now — the per-tile cost during its short probe hides the second card’s gain — so the two-card figure is the all GPUs column (XC_PAR=gpus); teaching auto to choose it is the next step.

Apple MacBook Pro, M4 Max (12 performance and 4 efficiency cores), its GPU through Metal (ms; best of eight runs)

benchmarkGPUautoGPU’s first run
mandel_deep44.044.188.1

AMD Ryzen 9 5950X (16 cores, 32 threads), Windows, an NVIDIA RTX 5080 through CUDA and an RTX 3090 through Vulkan (ms; best of eight runs)

benchmarkGPUall GPUsVulkanautoGPU’s first run
mandel_deep19.311.434.511.3115

What the tables show:

  • A discrete GPU wins by a wide margin on the programs that do real work per element — md5 hashes 64 million candidates in about a millisecond on one discrete card against hundreds on the CPU — and auto finds that and uses it.
  • saxpy stays on the CPU. It does one multiply-add for every eight bytes it moves, so copying to and from the GPU outweighs the arithmetic, and auto keeps it on the CPU everywhere.
  • The tile queue scales a block across GPUs, where that pays. saxpy spreads across both cards as the sum of their PCIe links — one GPU 16 ms, both 7.9 — though it is memory-bound, so the CPU still wins and auto keeps it there. md5 at 64 million candidates takes 0.9 ms on one card and 0.7 on both: a block this short gains little from a second card, since the queue’s cost per tile is paid twice, and auto chooses between them by measuring. A second GPU pays most on a block big enough to need its compute and not bound by the transfer to it.
  • Mixed graphics APIs. The scheduler does not require one API: a block can run one card through CUDA beside another through Vulkan, so an NVIDIA and an AMD card could share a block the same way.
  • The Ryzen’s integrated Radeon has 128 shader lanes against the CPU’s 32 threads. It just beats them on mandelbrot, perlin and md5 and loses on nbody and saxpy, where auto keeps the CPU.
  • Vulkan against CUDA: the RTX 3090 through Vulkan against the RTX 5080 through CUDA, two cards, so this compares the APIs only roughly: Vulkan is slower on mandel_deep (34.5 ms against 19.3) and mandelbrot (269 µs ms against 143 µs) and md5 (1.6 ms against 875 µs) and md5_mask (1.3 ms against 727 µs) and nbody (548 µs ms against 400 µs) and perlin (1.0 ms against 440 µs) and saxpy (30.1 ms against 5.4).
  • The first run carries one-off costs (building the kernel, and on NVIDIA creating the driver context and compiling the PTX or SPIR-V), so it is shown apart: a few tens of milliseconds on Metal and the integrated GPU, and on the 3090 82 to 115 ms through CUDA and 138 to 198 ms through Vulkan. auto pays it once, while it measures.

--par-auto turns a loop whose iterations are independent into a par block. Four of the programs above, written again as plain loops with no par in them, are built as written and with --par-auto, and each mode’s checksum must agree. The last column is the hand-written par version’s auto.

Apple M4 Max, Metal (ms; best of eight runs)

benchmarkas written--par-auto, all threads--par-auto, GPU--par-auto, autohand-written par, auto
mandelbrot14412.02.01.9878 µs
nbody39.72.01.11.21.1
perlin79.46.51.31.21.2
saxpy1.4502 µs2.4497 µs502 µs

AMD Ryzen 9 9955HX, its integrated Radeon through Vulkan (ms; best of eight runs)

benchmarkas written--par-auto, all threads--par-auto, GPU--par-auto, autohand-written par, auto
mandelbrot57.92.49.72.42.7
nbody95.0555 µs3.4464 µs503 µs
perlin1128.75.75.75.7
saxpy3.32.89.62.82.9

Windows, RTX 5080 through CUDA (ms; best of eight runs)

benchmarkas written--par-auto, all threads--par-auto, GPU--par-auto, autohand-written par, auto
mandelbrot25810.8143 µs142 µs143 µs
nbody1195.3397 µs397 µs398 µs
perlin16911.8432 µs437 µs427 µs
saxpy4.94.25.44.04.0
python3 benchmark/par/run.py # the par tables
python3 benchmark/par/run.py --autopar # the --par-auto tables
python3 benchmark/page.py