GPU computing with CUDA#

The third road to speed is the GPU (graphics processing unit). For the right kind of work — the same operation applied to huge arrays — a single GPU can outrun many CPU cores. qc-rs has an optional NVIDIA-GPU path for the integral and Fock build. This chapter explains what a GPU is, how to build qc-rs with GPU support, and what runs on it today.

What a GPU is, and when it helps#

A CPU has a few powerful cores optimized for general, branchy work. A GPU has thousands of small cores optimized for doing the same arithmetic on many data elements at once (massive data parallelism). That is a perfect match for quantum chemistry’s densest inner loops — the two-electron integrals and the matrix multiplies of the J/K build — which apply the same formula across enormous arrays.

CUDA is NVIDIA’s programming platform for their GPUs; qc-rs’s GPU path is built on it (via vendored, GPU-optimized integral kernels). It helps most on large systems where the GPU’s throughput dominates the cost of shipping data to the card; a tiny molecule may be slower on the GPU than on a CPU because of that overhead.

The cuda build (opt-in; default OFF)#

The GPU path is off by default — a normal qc-rs build has no CUDA dependency at all, links nothing from NVIDIA, and reports qc.GPU_ENABLED == False. You opt in at build time by adding the cuda feature (you need the CUDA toolkit and an NVIDIA GPU):

"${UV_PROJECT}/.venv/bin/maturin" develop --no-default-features \
  --features intel-mkl-system,xc-bundled,pcm,python-mpi-direct,hdf5,cuda

Then the sentinel flips:

import qc
qc.GPU_ENABLED       # True on a cuda build with a visible GPU; False otherwise

If you request a GPU strategy on a non-cuda build, qc-rs fails with a clear message rather than silently running on the CPU:

jk: eri="4c-cuda" requires qc-rs built with the `cuda` feature and a CUDA GPU; rebuild with …

Warning

The cuda build is dev-only / not relocatable The CUDA extension links vendored .so files by an rpath into the build tree and needs libcudart at run time, so cargo clean, a moved install, or a copied wheel will break it. Treat a cuda build as local to the machine that built it.

Using the GPU: eri="4c-cuda" and friends#

The GPU is selected through the integral strategy (ints(eri=...), the next chapter), so the rest of your workflow is unchanged:

import qc
m = qc.chk.new(atom="...", ao="cc-pvdz")
m = qc.ints(m, eri="4c-cuda").scf(ref="r").run()      # RHF with J/K on the GPU
print(qc.GPU_ENABLED, m.scf.energy)

The GPU strategies:

eri=

what runs on the GPU

4c-cuda

conventional 4-center J/K on the GPU

ri-cuda

density-fitted (RI) J/K, device-resident factor + cuBLAS

ri-recomp-cuda

semi-direct RI-JK (recompute the 3-center each cycle on the GPU)

ri-ram-cuda

host-resident RI factor streamed to cuBLAS J/K on the GPU

A GPU SCF also gets GPU KS-DFT (the exchange–correlation build runs on the card, eri="4c-cuda" with an xc=) and GPU PCM solvation for free (pcm={..., device="auto"} uses the GPU when the SCF already does).

What works today — and what doesn’t yet#

The GPU path is correctness-complete for its core, matching the CPU/PySCF energies:

  • J/K — RHF / UHF / ROHF, spherical and Cartesian, up to g functions (l ≤ 4), with screening and the range-separated-hybrid long-range term.

  • KS-DFT XC — RKS / UKS / ROKS across LDA / GGA / meta-GGA and hybrids (libxc still runs on the CPU).

  • RI-JK (ri-cuda) — density-fitted J/K, optimized (it beats the reference gpu4pyscf on a 32-water flagship), including range-separated hybrids.

  • PCM — the GPU reaction-field coupling.

Not yet on the GPU: RI-MP2 (the GPU RI path is J/K only), a lower-precision (fp32/mixed) fast path, angular momentum ≥ 5 (h functions), and KS-DFT gradients. For those, use the CPU path. The GPU work is correctness-complete rather than fully performance-tuned, so treat it as a fast J/K/XC engine, not a universal accelerator.

Worked example (conceptual — needs a cuda build + GPU)#

import qc
# On a machine WITHOUT a cuda build, this raises a clear ValueError (verified):
m = qc.chk.new(atom="O 0 0 0.117; H 0 0.757 -0.469; H 0 -0.757 -0.469",
               ao="cc-pvdz", unit="angstrom")
try:
    m.ints(eri="4c-cuda").scf(ref="r").run()
except ValueError as e:
    print("GPU not available:", str(e)[:60])   # ... requires the `cuda` feature and a CUDA GPU

Exercise 19

  1. qc.GPU_ENABLED is False on a machine that definitely has an NVIDIA GPU. Give two possible reasons.

  2. You have a small molecule and a big GPU, and 4c-cuda is slower than the CPU. Is that a bug? Explain.

  3. You need a GPU-accelerated RI-MP2 correlation energy. What does qc-rs support today, and what is your practical option?

You have now met all three parallelism levels. The final chapter ties them together through the one axis that touches all of them — the ERI / J-K strategy.