# 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](../00-intro/installation-and-make-setup.md)
by adding the **`cuda`** feature (you need the CUDA toolkit and an NVIDIA GPU):

```bash
"${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:

```python
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:

```text
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](eri-jk-strategies.md)),
so the rest of your workflow is unchanged:

```python
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)

```python
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}
:label: ex-gpu

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?
:::

:::{solution} ex-gpu
:class: dropdown

1. (a) qc-rs was **built without the `cuda` feature** (a default build has no GPU path), or (b) the build has
   `cuda` but no GPU is *visible* at run time (driver/`libcudart`/`CUDA_VISIBLE_DEVICES` issue). Rebuild with
   `--features …,cuda` and check the runtime GPU.
2. **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.
3. 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](eri-jk-strategies.md).
