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:
|
what runs on the GPU |
|---|---|
|
conventional 4-center J/K on the GPU |
|
density-fitted (RI) J/K, device-resident factor + cuBLAS |
|
semi-direct RI-JK (recompute the 3-center each cycle on the GPU) |
|
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
qc.GPU_ENABLEDisFalseon a machine that definitely has an NVIDIA GPU. Give two possible reasons.You have a small molecule and a big GPU, and
4c-cudais slower than the CPU. Is that a bug? Explain.You need a GPU-accelerated RI-MP2 correlation energy. What does qc-rs support today, and what is your practical option?
Solution to Exercise 19
(a) qc-rs was built without the
cudafeature (a default build has no GPU path), or (b) the build hascudabut no GPU is visible at run time (driver/libcudart/CUDA_VISIBLE_DEVICESissue). Rebuild with--features …,cudaand check the runtime GPU.Not a bug. For a small molecule the cost of shipping data to the GPU outweighs its throughput advantage; GPUs win on large systems. Use the CPU for small jobs.
GPU RI-JK (
ri-cuda) works, but GPU RI-MP2 is not implemented. Run the SCF on the GPU if you like, but compute the MP2 correlation on the CPU (lct(method="mp2")), which is the supported path.
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.