8000 Two bugfixes for periodic LNO Code by afarahva · Pull Request #228 · pyscf/pyscf-forge · GitHub
[go: up one dir, main page]

Skip to content

Two bugfixes for periodic LNO Code - #228

Merged
MatthewRHermes merged 6 commits into
pyscf:masterfrom
afarahva:lno_pr_1
Nov 2, 2026
Merged

MatthewRHermes merged 6 commits into
pyscf:masterfrom
afarahva:lno_pr_1

Conversation

@afarahva
@afarahva afarahva commented Oct 29, 2026 •
Copy link
Copy Markdown
Contributor

Summary


Two fixes to k-point LNO (pyscf/pbc/lno), plus related tests:

  1. q-block chunking of the outcore DF integrals in klno and klnoccsd, which was a TODO (# TODO: determine a chunks size). Without it, writing these integrals to disk moves many times more data than they contain.

  2. Canonical orthogonalization in k2s_scf, so that it works when the k-point SCF has removed near-linearly-dependent overlap eigenvectors.

Details


1. q-block chunking (klno / klnoccsd)

_KLNODFOUTCOREERIS_REAL stores the full-system ERIS (ovLR/ovLI, shape: nocc * nvir * Naux)) in unchunked HDF5 datasets. _init_mp_df_eris_real then fills them one q block at a time, as ovL[:,:,q-block].

Problem: An unchunked dataset, each (i,a) row has only a short contiguous piece in each q block. HDF5's sieve buffer therefore reads, modifies and writes back every piece. Truncated q blocks (naux_by_q[q] < naux) cost an extra strided pass, because their zero padding is written separately. This results in an enormous and unnecessary file i/o overhead, leading to a large slowdown for calculation which don't fit in memory.

The fix: In klno.py, ovLR/ovLI are chunked as (1, nvir, naux), i.e. one chunk per occupied orbital and q block. In klnoccsd.py, vvLR/vvLI are chunked one q block (naux columns) wide.
Each q block's zero padding [nauxq, naux) is now written together with its data, in the same aux-block loop, instead of in a separate strided write.
The resulting tensors are bit-identical to before. Readers that take occupied-orbital or row blocks see no extra reads.

2. Canonical orthogonalization in k2s_scf

Problem: k2s_scf re-diagonalizes the unfolded supercell Fock with a plain generalized eigh, which Cholesky-factorizes the supercell overlap. While the k-point SCF has removed near-linearly-dependent overlap eigenvectors, the supercell overlap has the same near-null space and is not removed by default. This leads factorization to sometimes fail with the error "leading minor ... of B is not positive definite". We hit this for LiF, NaF, LiCl and NaCl with cc-pVnZ basis sets.

Fix. k2s_scf now follows the same switches as the k-point SCF.

Tests


A series of fast tests (each taking <5 seconds on a single CPU on my local macbook) was implemented to specifically test that outcore LNO path matches incore path. If you want I can remove these extra tests, I just included them to ensure that the behavior of the code was unchanged (previous tests only went through incore path due to small system sizes used for tests).

test_lnoccsd.py::test_lno_iao_outcore: forces the outcore ovL for the LNO step with split occupied/aux blocks, and the DF-CCSD impurity solver. Checked against the existing incore water-dimer energies.

test_ulnoccsd.py::test_ulno_pm_outcore: forces the outcore ovL with split blocks. Checked against the existing ULNO reference.

test_klnoccsd.py::H4Chain::test_klnoccsd_t_outcore: H4 cell on a 2×1×1 mesh. The outcore ovL is checked alone and with the DF impurity CCSD forced (FORCE_DFKCC), against incore KLNO-CCSD(T) energies to 1e-8. Both runs split the aux blocks and zero-pad every q block, so the padded and blocked writes are covered.

🤖 Generated with help from Claude Code and edited and reviewed by me.

afarahva and others added 4 commits October 29, 2026 18:15
k2s_scf re-diagonalizes the unfolded supercell Fock with a plain generalized
eigh, which Cholesky-factorizes the supercell overlap. When the k-point SCF
has removed near-linearly-dependent overlap eigenvectors, the supercell
overlap carries the same near-null space, and the factorization can fail
("leading minor ... of B is not positive definite").
This was a TODO feature and also fixes a HDF5 issue.
Outcore ovL (and, for LNO and KLNO, the DF impurity CCSD integrals) with split aux/occ
blocks checked against incore energies; the KLNO test also zero pads every q block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rocksalt LiF in def2-SVPD on a 2x1x1 k-mesh has numerically singular k-point
and supercell overlaps. This test checks that k2s_scf works for such cases.
Comment thread pyscf/pbc/lno/tools.py Resolved
Comment thread pyscf/pbc/lno/klno.py
return np.asarray(self.ovLR[i0:i1]), np.asarray(self.ovLI[i0:i1])

def get_vir_blk(self, a0,a1, real_and_imag=False):
return np.asarray(self.ovLR[:,a0:a1], order='C'), np.asarray(self.ovLI[:,a0:a1], order='C')
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems the new chunking causes a slowdown here, since you only chunked along the occupied and never the virtual axis.

@afarahva afarahva Oct 31, 2026 •
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, but this the same layout the molecular LNO uses (chunks=(1, nvir, naux) in lno.py/ulno.py), I was just adding functionality to klno.py to bring it in line with what the molecular code already does.

The get_vir_blk function is only used by the optional particle DM builders, not the default '1h'. I’d rather fix both the molecular and k-point codes together in a follow-up if deemed necessary. The issue is just that how the chunk size and shape interacts with HDF5’s chunk cache and the aux-block write pattern is easy to get wrong in ways small tests won’t catch. I'd need to do some testing on production-size runs to feel confident about any changes.

@MatthewRHermes
Copy link
Copy Markdown
Collaborator

Before making the two inline comments above, I had Claude Code (Anthropic) review this PR. It ran the new tests, an end-to-end KLNO-CCSD(T) on your linearly dependent LiF cell, and some h5py benchmarks of the old and new chunking. Its unedited draft review is collapsed below in case the details are useful. I don't agree with all of it: where it differs from my inline comments, follow the inline comments. In particular, I'm not asking for support for PySCF < 2.13 or for any PySCF version logic in Forge.

Claude's draft review (unedited)

Thanks, both fixes look correct and useful. I checked them as follows:

  • The new tests pass. test_k2s_scf.py fails on master with LinAlgError: The leading minor of order 43 of B is not positive definite, so it is a real regression test.
  • I ran KLNO-CCSD(T) end to end on your LiF/def2-SVPD 2x1x1 cell (70 AOs, 62 MOs in the supercell). Incore and outcore energies agree to ~1e-14, so the downstream code handles nmo < nao.
  • An h5py microbenchmark of the old and new ovL write patterns (nocc=100, nvir=1000, naux=200, 4 q blocks) gave 14.6 s vs 1.4 s, and the gain held when the aux blocks were split.

One thing needs fixing, and I have a few suggestions.

Required: k2s_scf raises AttributeError on pyscf < 2.13 (tools.py)

mol_hf.remove_overlap_zero_eigenvalue was added in pyscf 2.13 (pyscf/pyscf#3191), in the same commit as _adjust_phase_. pyscf-forge doesn't pin a pyscf version, so on older pyscf every k2s_scf call, and so every KLNO run, fails. For the same reason the hasattr(mol_hf, '_adjust_phase_') guard never does anything. Please either guard the switch or require pyscf>=2.13.

Suggestion: reuse the core eigensolver

On pyscf >= 2.13, the supercell scf.RHF already has check_linear_dependency and eig(h, s, x=x), and that path returns the same non-square, phase-adjusted mo_coeff as the new block. If mf is built before the diagonalization, the block shrinks to:

if hasattr(mf, 'check_linear_dependency'):   # pyscf >= 2.13
    mo_energy, mo_coeff = mf.eig(fock, s1e, x=mf.check_linear_dependency(s1e))
else:
    mo_energy, mo_coeff = mol_hf.eig(fock, s1e)

This also fixes the issue above and removes the scipy.linalg import.

Suggestion: the (1, nvir, naux) chunks slow down virtual-block reads (klno.py)

Occupied-block readers are fine, but get_vir_blk reads ovLR[:, a0:a1], which now loads whole chunks. In the same benchmark, one sweep of get_vir_blk went from 0.15 s to 1.33 s with 100-wide virtual blocks (0.61 s with 250-wide blocks). make_lo_rdm1_vir_1p_real and make_lo_rdm1_occ_2p_real call it inside nested loops, so the slowdown repeats. Only the non-default '1p'/'2p' LNO types are affected. Chunks of (1, min(nvir, ~128), naux) would keep the writes chunk-aligned along aux and make virtual reads cheap. They would also keep chunks well below the HDF5 4 GB limit for large supercells.

Minor

  • The description says each q block "fills whole chunks". That is only exact when aux_blksize == naux; with split aux blocks each write fills part of a chunk. It is still much faster, as the numbers above show, so this is just wording. A timing from the real system that motivated the change would be a good addition.
  • H4Chain.test_klnoccsd_t_outcore puts the assertions inside the force_dfkcc loop, so a failure won't say which case broke. self.subTest(force_dfkcc=force_dfkcc) or two separate tests would fix that.
  • I'm happy to keep the molecular LNO/ULNO outcore tests. They cover a path that wasn't tested before.

@afarahva
afarahva commented Oct 31, 2026 •
Copy link
Copy Markdown
Contributor Author

Thanks for the helpful comments!

I agree with and made changes for the first (and made a note for myself to try and keep future bugfixes version agnostic).

The second was a good point, but I think for simplicity we should just keep the behavior between lno.py/ulno.py/klno.py consistent for now.

@MatthewRHermes
MatthewRHermes merged commit 1620ad6 into pyscf:master Nov 2, 2026
6 checks passed
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.

2 participants

0