Skip to content

Batch DF derivative work and reduce gradient workspace - #967

Open
zc62 wants to merge 4 commits into
pyscf:masterfrom
zc62:df-derivative-batching
Open

zc62 wants to merge 4 commits into
pyscf:masterfrom
zc62:df-derivative-batching

Conversation

@zc62

@zc62 zc62 commented Oct 15, 2026

Copy link
Copy Markdown

DF derivative drivers often evaluate one auxiliary shell group per launch, even when several groups fit in the allocated buffer. Pack consecutive groups into larger launches to improve GPU utilization. Each CUDA block continues to process a homogeneous angular-momentum/primitive group.

Changes

  • Optional numerical-stability commit: Apply the auxiliary metric through its factors in the TD-gradient helper. This reduces memory-dependent gradient differences from about 4e-12 to 2e-14 Eh/bohr in the test fixture. Alternatively, omit this commit and relax the repeated-density and summed-output consistency tolerances from 1e-12 to 1e-11 at these test lines:

assert abs(ejk.reshape(4, 3, mol.natm, 3) - ref).max() < 1e-12
ejk_sum = _jk_energies_per_atom(opt, dm, j_factor=j_factor,
k_factor=k_factor, sum_results=True)
assert abs(ejk.sum(axis=0) - ejk_sum).max() < 1e-12

  • Size gradient scratch for the largest integral tile and density-factor width, reducing Hermitian exchange workspace.
  • Pack auxiliary groups in gradient and Hessian derivative drivers (commit 3), and in the shared three-center integral and first-derivative evaluators (commit 4). Preserve function ordering and distribute RHF Hessian tasks across devices through the existing shared iterator.
  • Fix an existing buffer-sizing bug in tdscf.ris.get_Tpq: the integral evaluator writes a complete auxiliary tile before orbital contractions split it into smaller blocks. The original code sized the integral-output buffer for the smaller contraction block, causing a buffer-size assertion when a returned tile exceeded that capacity. Allocate integral scratch for the largest returned tile while retaining smaller contraction blocks. Larger packed tiles expose this bug more readily; a new low-memory test covers host and GPU result storage.

Performance

RTX 5070 Ti (16 GB, 70 SMs), Ryzen 7 9700X, 8 OpenMP/OpenBLAS threads; CUDA 13.0, CuPy 13.6.0 with the CuPy contraction backend. PBE0, def2-TZVP/def2-TZVP-JKFIT, level-3 grids, auxiliary-basis response enabled. Times cover complete gradient or Hessian kernel() calls using identical saved SCF/TD states: one warm-up followed by three synchronized serial measurements. The TDDFT gradient uses the first singlet root.

Full operation Initial Updated Speedup
Water10 ground-state gradient 1.756 s 1.153 s 1.52x
Water10 TDDFT gradient 13.870 s 13.255 s 1.05x
Water5 Hessian 9.773 s 9.201 s 1.06x

The two batching commits contribute separately:

Full operation Before commit 3 After commit 3 After commit 4
Water10 ground-state gradient 1.743 s 1.459 s 1.153 s
Water10 TDDFT gradient 13.926 s 13.647 s 13.255 s
Water5 Hessian 9.773 s 9.604 s 9.201 s

For water10, restricted Hermitian exchange scratch falls from 2,451 MB to 255 MB; the corresponding unrestricted allocation falls to 511 MB.

AI Usage Disclaimer

OpenAI Codex with GPT-6.1 assisted with implementation, benchmark execution and analysis, and drafting this PR.

zc62 added 4 commits October 15, 2026 16:38
Improve numerical stability of the density-pair gradient against
changes in memory-dependent contraction sizes. For density pairs
from test_jk_energy_per_atom_dm_pairs in
gpu4pyscf/df/tests/test_df_tdrhf_grad.py, substituting 16.2 MB and
2.49 MB as get_avail_mem returns produces a maximum K gradient
component discrepancy of 4e-12 Eh/bohr with the expanded metric.

_jk_energies_by_dm_factors forms G = C M^-1 C.T and applies G X to
assembled J vectors and K tensors. M is the contracted auxiliary
Coulomb matrix; C expands its basis into the integral representation.
Small eigenvalues of M produce large entries in G, so its output
dot products can subtract large terms to obtain small coefficients.

Compute t = A.T X, then A t, for each assembled array X, sharing A
across J and K.
Following gpu4pyscf.df.df._decompose_j2c, use A = C L^-T for
Cholesky M = L L.T, or A = C U lambda^-1/2 with retained eigenvectors
U and eigenvalues lambda. Preserve decomposition selection, fallback,
and the eigenvalue cutoff.

In this fixture, G entries reach about 20000, while A entries reach
about 70. One coefficient near -0.0065 sums
positive and negative contributions of about 50000 in G X. Along
the factorized path, a contributing t = A.T X entry combines signed
sums of about 200 to give an intermediate near -0.3; the final A t
combines signed sums of about 20 to give the coefficient near -0.0065.

The first product thus performs part of the cancellation before
forming the output coefficients. The final product reuses these
smaller t values across outputs. An error in one t value enters
each output with the corresponding weight from A; when those
contributions cancel in the gradient, their shared error cancels too.
Dense G X computes a separate large-term sum for each output, with
its own rounding residual. Both factorized products still round;
the gradient comparison below measures their combined effect.

For the same memory probes, the factorized application reduces the
maximum K gradient component discrepancy to 2e-14 Eh/bohr.
Multiplying the same stored G in extended precision also reduces
the discrepancy to 1e-14, isolating rounding in the dense product.

The improved consistency trades one dense metric multiplication
for two factor multiplications. With n expanded and m contracted
auxiliary functions, Cholesky application uses 2*m/n times the
leading arithmetic of G X; eigenvalue truncation replaces m with
the retained rank.
Memory-derived auxiliary sizes can exceed the evaluator's largest
tile. Cap integral and contraction scratch at that tile size in RHF,
UHF, and the TD summed-density and density-pair paths. The same cap
also bounds derivative batching.

Hermitian exchange scratch also reserves excess AO-sized capacity.
Size it by the density-factor width and AO count in RHF, UHF, and
summed-density TD, retaining both spin channels in UHF. Preserve
the transpose workspace required by non-Hermitian inputs.
RHF, UHF and TD-RHF derivative drivers filled one auxiliary group per
launch despite buffers sized for several groups, limiting grid
parallelism.

Pack consecutive groups within the existing auxiliary-function
capacity and pass the group count through the native driver's
existing second grid axis. This exposes more independent blocks per
packed launch while preserving group boundaries and function ordering.

To keep RHF Hessian packed work distributable across devices, target
the auxiliary-function count per device when limiting tile size,
with whole groups setting the minimum granularity. Assign packed
tiles through the existing shared iterator.
Int3c2eOpt.int3c2e_evaluator evaluates three-center Coulomb integrals.
Its aux_batch_size argument specifies a requested auxiliary-function
capacity, but setting it produced one shell group per call even when
several groups fit. This left buffer capacity and GPU parallelism
unused.

Use splits_by_blocksize to pack consecutive groups using that capacity.
Apply the same rule to _int3c2e_ip1_evaluator, which evaluates first
derivatives of the three-center integrals, and pass the packed group
count to its native driver. Preserve auxiliary-function ordering;
both evaluators return offsets describing their packed batches.

Larger tiles expose an existing buffer-sizing bug in tdscf.ris.get_Tpq.
It allocated the integral-output buffer using the smaller inner
orbital-contraction block size, causing buffer assertions under
limited memory. Fix this by allocating for the largest returned
integral tile, then contracting it in smaller orbital blocks.

Add test_get_Tpq_limited_memory in df/tests/test_df_tddft_ris.py.
It compares J/K orbital transforms under normal and constrained
memory, covering both host and GPU result storage, to guard the
full-tile buffer allocation.
@zc62
zc62 force-pushed the df-derivative-batching branch from c18624f to 4364bea Compare October 16, 2026 01:47
@zc62

zc62 commented Oct 16, 2026

Copy link
Copy Markdown
Author

Force-pushed to satisfy linter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant