Refactor hsolver: DiagoIterAssist takes H|psi>/S|psi> functors - #7951
Open
Critsium-xy wants to merge 3 commits into
Open
Refactor hsolver: DiagoIterAssist takes H|psi>/S|psi> functors#7951Critsium-xy wants to merge 3 commits into
Critsium-xy wants to merge 3 commits into
Conversation
DiagoIterAssist was the last algorithm class in source_hsolver holding a
hamilt::Hamilt<T, Device>*. It used it for exactly two things --
`pHamilt->ops->hPsi(...)` and `pHamilt->sPsi(...)` -- so diag_subspace,
diag_subspace_init and cal_hs_subspace now take two functors instead:
HPsiFunc = void(T* psi_in, T* hpsi_out, int ld_psi, int current_nbasis, int nvec)
SPsiFunc = void(const T* psi_in, T* spsi_out, int nrow, int npw, int nbands)
DiagoCG, DiagoDavid, DiagoDavSubspace and DiagoBPCG have taken closures for
a while; this makes the last one consistent with them.
Both functors carry their dimensions explicitly rather than letting the
caller capture one fixed set, because DiagoIterAssist drives three
different wavefunction layouts: the caller's own psi in diag_subspace and
cal_hs_subspace, a one-band staging buffer in the GPU branch of
diag_subspace_init, and an nstart-band one in its CPU branch. Those
branches pass different values for sPsi's nrow/npw, and the CPU branch
carries a standing note that current_nbasis must be npw *without* npol or
Nonlocal::act's gemm goes wrong. Passing the dimensions through keeps every
caller's functor a plain forwarder and makes the substitution checkable by
inspection, which matters because the SOC and GPU paths are not covered by
the unit tests available to me.
The `pHamilt->ops == nullptr` early exit in diag_subspace_init becomes an
empty-functor check; the two callers that can hit it build the functor only
when ops is allocated.
source_hsolver -> source_hamilt includes: 6 files / 6 lines -> 5 files /
5 lines. The five that remain are the HSolver* facades, which legitimately
own a Hamilt pointer.
No behaviour change in any supported state. One failure mode changes shape:
psi_prepare.cpp builds one functor for both diag_subspace_init and
diag_subspace, and diag_subspace has never had a null-ops guard, so with a
null ops on that branch the old code dereferenced a null pointer and the new
one throws std::bad_function_call. Neither is a working state.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mohanchen
reviewed
Sep 12, 2026
Address review feedback on deepmodeling#7951: the H|psi>/S|psi> lambdas are now declared as DiagoIterAssist::HPsiFunc / SPsiFunc (and DiagoCG::SubspaceFunc for the CG subspace wrapper), and the Psi wrappers are declared with their concrete types, so the reader no longer has to infer them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShMLqMWfjYUnad69m5Lcgq
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reminder
AGENTS.mdanddocs/developers_guide/agent_governance.md.source/changes.Linked Issue
No issue. Third step of making
source_hsolvera self-contained numericalmodule, after #7920 and #7948. Behaviour-neutral.
There is a standing request for exactly this change in the tree:
source_lcao/module_lr/hsolver_lrtd.hpp:122readsThis PR does the
Hamilt*→hpsi_funcpart. Actually enabling the LR path isleft to whoever owns that module, since
HamiltLRneeds functors of its own.Unit Tests and/or Case Tests for my changes
Commands run (Linux, gcc, cmake 3.31 + ninja, 15 cores). Base commit
9cbcc0b55and this branch were built and tested in the same build tree.ENABLE_ELPA=ONis needed for full coverage of the module.Result summary
9cbcc0b55Failing sets are identical test-for-test (
diffof the sorted lists isempty); they are this sandbox's pre-existing failures.
The unit suite does not actually exercise these three functions, so that
result on its own is weak evidence. The CG unit tests pass
DiagoIterAssist<T>::need_subspace(which isfalse) intoDiagoCG, so theirsubspace_func— the one that callsdiag_subspace— is never invoked. Itherefore drove each of the three converted entry points through integration
cases instead, and checked the total energy against the committed
result.ref:result.ref01_PW/022_PW_CGdiag_subspace×1401_PW/036_PW_AFdiag_subspace×6010_others/01_NP_KP_spdiag_subspace_init×6401_PW/scf_deltaspin4cal_hs_subspace17_DS_DFTU/14_PW_DS_S4_XYZcal_hs_subspaceEvery case reproduces its reference to the precision the reference is stored at.
The call counts are read out of each run's own timing table, so the first three
rows are direct evidence that the converted code ran.
cal_hs_subspacehas notimer of its own, but it is what fills the
h_k/s_ksubspace matrices thatdiag_responce(×148 and ×156 in those two runs) then diagonalizes — a brokensubstitution there could not land within 1e-10 of the reference.
The last two rows matter most: they are
nspin 4, i.e.npol == 2, which isthe case the CPU branch's standing comment warns about and the one the unit
tests never reach.
Checks not run, with reason
base_device::DEVICE_GPU) branches ofdiag_subspace_initand ofcal_mw_from_lambda.cpp: no GPU runtime on the machine available to me. Theyare compiled by CI's CUDA job, and the substitution there is the same
forwarding shim as the CPU branches, with the dimensions passed through
unchanged (which is the reason the functors take them explicitly).
01_PW/BUG_SCF_DSPINwould have been annspin 4+cgcase, but it exitsnon-zero on the base commit as well — it is a known-broken case, as its name
says.
What's changed?
DiagoIterAssistwas the last algorithm class insource_hsolverstillholding a
hamilt::Hamilt<T, Device>*. It used it for exactly two things,pHamilt->ops->hPsi(...)andpHamilt->sPsi(...), so its threeHamiltonian-taking entry points —
diag_subspace,diag_subspace_initandcal_hs_subspace— now take two functors instead:DiagoCG,DiagoDavid,DiagoDavSubspaceandDiagoBPCGhave taken closuresfor a while, and
HSolverPW::hamiltSolvePsiKalready builds them; this bringsthe last one into line.
Why these functors carry their dimensions instead of capturing them. The
existing 4-argument
HPsiFuncinDiagoCGand friends works because thosesolvers drive one fixed wavefunction layout, so the caller can capture a single
cur_nbasis.DiagoIterAssistdrives three:hPsisPsi(nrow, npw, …)diag_subspace,cal_hs_subspacepsi(dmax, dmin, nstart)diag_subspace_init, GPU branch(dmin, dmin, 1)diag_subspace_init, CPU branchnstart-band staging buffer(psi_nc, psi_nc, nstart)sPsi'snpwgenuinely differs between the two branches of one function, so acaptured constant cannot reproduce it, and the CPU branch carries a standing
comment that
current_nbasismust be npw without npol orNonlocal::act'sgemm K stops matching
vkb's row count. Passing the dimensions through keepsevery caller's functor a plain forwarder and makes the substitution checkable by
reading it, which matters because the SOC and GPU paths are not covered by the
unit tests available to me.
Why the substitution is equivalent. Where the old code handed
Operator::hPsiapsi::Psiplus aRange, the functor rebuilds a one-kwrapper over the same pointer. That is the same thing because:
psi.get_pointer()returnspsi_current, whichfix_ksets topsi + ik * nbands * nbasis— exactly the pointerto_range(Range(1, current_k, 0, nstart-1))computes;Psi::get_npol()readsPARAM.inp.nspinglobally rather than a per-objectmember, so the wrapper reports the same
npol(and hence the samenbands * npolinsideOperator::hPsi) as the original object;get_nbasis()andget_current_nbas()are passed through explicitly, per thetable above.
The
ops == nullptrearly exit indiag_subspace_initbecomes anempty-functor check. The two callers that can reach it (
psi_prepare.cpp,hsolver_lcaopw.cpp) build the functor only whenopsis allocated andotherwise leave it default-constructed, so the warning and the
copy-psi-to-evc fallback fire under exactly the same condition as before.
One honest caveat about that:
psi_prepare.cppbuilds a single functor anduses it for both
diag_subspace_initanddiag_subspace, anddiag_subspacehas never had a null-
opsguard. So in the (unsupported, already-broken) statewhere
opsis null and that branch is taken, the failure changes shape — itused to dereference a null pointer, and now throws
std::bad_function_call.Neither is a working state; I did not want to invent a new guard here.
source_hsolverfiles includingsource_hamilt#include "source_hamilt/..."lines thereThe five that remain are
hsolver_lcao.h,hsolver_lcaopw.{h,cpp}andhsolver_pw.{h,cpp}— theHSolver*façades, which legitimately own aHamilt*because they run the k-loop. Moving those out ofsource_hsolverinto the physics modules they belong to is the next and last step, not this one.
Not in this PR:
DiagoIterAssist's five mutable statics (PW_DIAG_THR,PW_DIAG_NMAX,avg_iter,need_subspace,SCF_ITER). They are worthremoving — they are the AGENTS.md rule 2 pattern — but they create no
source_hamiltdependency, and only one algorithm actually reads one of them(
diago_bpcg.cpp:302readsSCF_ITER); the rest is parameter plumbing betweensetup_diago_params_*and theHSolverPWconstructor, which already hasmatching instance members. That is a separate, self-contained change and mixing
it in here would double the diff and the risk.
Governance Notes
Input_Item, no user-visible behavior, nooutput format is touched.
hamilt::class is modified —DiagoIterAssistsimply stops asking for one.PsiandOperatorare untouched. Callers insource_psiandsource_lcao/module_deltaspingain a forwarding functor each; the values thatreach
Operator::hPsiandHamilt::sPsiare unchanged.call sites are updated in this PR (AGENTS.md rule 5). The two pre-existing
defaults on
diag_subspace(n_band = 0,is_S_orthogonal = false) arekept as they were.
than behind a shared helper. A helper would have to live in
source_hamiltto avoid re-introducing the dependency this PR removes; that seemed like the
wrong thing to add here, and the natural moment to reconsider is after the
façades move.
🤖 Generated with Claude Code