Summary
After changing the geometry of mf.mol in place (mol.set_geom_(...)) and
calling mf.reset(), a density-fitted calculation in gpu4pyscf rebuilds
_cderi with the auxiliary basis still sitting at the old coordinates.
The SCF then converges normally and returns a silently wrong energy — for a
1e-3 Bohr displacement of one atom in water/def2-SVP we get an error of
~2e-2 Ha, with no warning or error raised.
The same sequence is handled correctly by CPU PySCF, so code that follows the
documented scanner/reset pattern gives correct results on CPU and corrupted
results on GPU. We hit this in production: our finite-difference Hessians
(displace → reset() → kernel() loop) were entirely corrupted, producing
non-physical frequencies in the 10^4–10^5 cm⁻¹ range before we tracked it down.
Minimal reproduction
from pyscf import gto, dft
WATER = "O 0 0 0; H 0.9572 0 0; H -0.2400 0.9266 0"
def make_mol(displaced=False):
mol = gto.M(atom=WATER, basis="def2-svp", unit="Angstrom", verbose=0)
if displaced:
c = mol.atom_coords(unit="Bohr").copy()
c[0, 0] += 1e-3
mol.set_geom_(c, unit="Bohr")
mol.build(dump_input=False)
return mol
# Reference: fresh mf at the displaced geometry
e_fresh = dft.RKS(make_mol(True), xc="wb97x").density_fit().to_gpu().kernel()
# Reuse: kernel at equilibrium, displace in place, reset, kernel again
mf = dft.RKS(make_mol(), xc="wb97x").density_fit().to_gpu()
mf.kernel()
c = mf.mol.atom_coords(unit="Bohr").copy()
c[0, 0] += 1e-3
mf.mol.set_geom_(c, unit="Bohr")
mf.mol.build(dump_input=False)
mf.reset()
e_reused = mf.kernel()
print(f"fresh = {float(e_fresh):.10f}")
print(f"reused = {float(e_reused):.10f}")
print(f"|diff| = {abs(float(e_reused) - float(e_fresh)):.2e} Ha")
# stale auxiliary basis: max deviation equals the displacement
import numpy as np
aux = mf.with_df.auxmol.atom_coords(unit="Bohr")
print(f"auxmol max deviation from current mol geometry: "
f"{np.abs(aux - mf.mol.atom_coords(unit='Bohr')).max():.2e} Bohr")
Observed (gpu4pyscf 1.7.4, cupy 14.1.1, pyscf 2.13.1, RTX 3070, CUDA 12.4/WSL2):
fresh = -76.3496849152
reused = -76.3700807832
|diff| = 2.03e-02 Ha
auxmol max deviation from current mol geometry: 1.00e-03 Bohr
Expected: |diff| at SCF-convergence level (the same script with
.density_fit() removed, or run on CPU PySCF, gives ~1e-11 Ha).
Controls we ran (same displace+reset+kernel sequence):
| variant |
result |
| GPU DF + SMD |
wrong (2.04e-2 Ha) |
| GPU DF only |
wrong (2.03e-2 Ha) |
| GPU SMD only (no DF) |
correct |
| GPU bare RKS |
correct |
| CPU DF (+/- SMD) |
correct |
So the issue is specific to the density-fitting path on GPU; solvent models
are unaffected on their own.
Root cause
CPU PySCF df.DF.build regenerates the auxiliary Mole unconditionally
from the current self.mol on every build
(pyscf/df/df.py,
auxmol = self.auxmol = addons.make_auxmol(self.mol, self.auxbasis)),
so a stale auxmol can never survive a rebuild.
gpu4pyscf df.DF.build only regenerates it when it is missing:
if auxmol is None:
self.auxmol = auxmol = addons.make_auxmol(mol, self.auxbasis)
and DF.reset(mol=None) only clears auxmol when a new mol object is
passed:
def reset(self, mol=None):
if mol is not None:
self.mol = mol
self.auxmol = None
...
self._cderi = None
...
mf.reset() (no argument) therefore keeps the old-geometry auxmol (and on
current master also intopt/j_engine), while _cderi is rebuilt against
it — producing wrong 3c2e integrals for the new geometry with no diagnostic.
Suggested fix
Either
- clear
self.auxmol (and intopt/j_engine) unconditionally in
DF.reset(), or
- rebuild
auxmol from self.mol unconditionally in DF.build(), matching
CPU PySCF semantics.
If keeping the cache is intentional for scanner performance, a geometry
consistency check between mol and auxmol at build time would at least
turn silent corruption into a loud error.
Environment
Minimal reproduction above was run on:
- gpu4pyscf 1.7.4 (code path unchanged on current master as of 2026-07-23)
- pyscf 2.13.1, cupy 14.1.1
- NVIDIA GeForce RTX 3070, CUDA 12.4, WSL2 (Linux 6.6)
The original production corruption (finite-difference Hessians) was also
observed on RTX 4070 Ti and A100 SXM systems with CUDA 13.x, so the issue is
not specific to a particular GPU architecture or CUDA major version.
Possibly related
#552 reported numerical vs analytical Hessian disagreement with DF on GPU.
If that workflow reused a live mf across displacements (in-place
set_geom_ + reset()), the stale-auxmol behavior described here could
explain it.
Summary
After changing the geometry of
mf.molin place (mol.set_geom_(...)) andcalling
mf.reset(), a density-fitted calculation in gpu4pyscf rebuilds_cderiwith the auxiliary basis still sitting at the old coordinates.The SCF then converges normally and returns a silently wrong energy — for a
1e-3 Bohr displacement of one atom in water/def2-SVP we get an error of
~2e-2 Ha, with no warning or error raised.
The same sequence is handled correctly by CPU PySCF, so code that follows the
documented scanner/reset pattern gives correct results on CPU and corrupted
results on GPU. We hit this in production: our finite-difference Hessians
(displace →
reset()→kernel()loop) were entirely corrupted, producingnon-physical frequencies in the 10^4–10^5 cm⁻¹ range before we tracked it down.
Minimal reproduction
Observed (gpu4pyscf 1.7.4, cupy 14.1.1, pyscf 2.13.1, RTX 3070, CUDA 12.4/WSL2):
Expected:
|diff|at SCF-convergence level (the same script with.density_fit()removed, or run on CPU PySCF, gives ~1e-11 Ha).Controls we ran (same displace+reset+kernel sequence):
So the issue is specific to the density-fitting path on GPU; solvent models
are unaffected on their own.
Root cause
CPU PySCF
df.DF.buildregenerates the auxiliary Mole unconditionallyfrom the current
self.molon every build(pyscf/df/df.py,
auxmol = self.auxmol = addons.make_auxmol(self.mol, self.auxbasis)),so a stale
auxmolcan never survive a rebuild.gpu4pyscf
df.DF.buildonly regenerates it when it is missing:and
DF.reset(mol=None)only clearsauxmolwhen a newmolobject ispassed:
mf.reset()(no argument) therefore keeps the old-geometryauxmol(and oncurrent master also
intopt/j_engine), while_cderiis rebuilt againstit — producing wrong 3c2e integrals for the new geometry with no diagnostic.
Suggested fix
Either
self.auxmol(andintopt/j_engine) unconditionally inDF.reset(), orauxmolfromself.molunconditionally inDF.build(), matchingCPU PySCF semantics.
If keeping the cache is intentional for scanner performance, a geometry
consistency check between
molandauxmolat build time would at leastturn silent corruption into a loud error.
Environment
Minimal reproduction above was run on:
The original production corruption (finite-difference Hessians) was also
observed on RTX 4070 Ti and A100 SXM systems with CUDA 13.x, so the issue is
not specific to a particular GPU architecture or CUDA major version.
Possibly related
#552 reported numerical vs analytical Hessian disagreement with DF on GPU.
If that workflow reused a live mf across displacements (in-place
set_geom_+reset()), the stale-auxmolbehavior described here couldexplain it.