Skip to content

Commit f38b5d2

Browse files
committed
Add LOBPCG search-direction block to PPCG and use residual convergence
1 parent fd28954 commit f38b5d2

5 files changed

Lines changed: 186 additions & 43 deletions

File tree

source/source_hsolver/diago_ppcg.cpp

Lines changed: 160 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "source_base/kernels/math_kernel_op.h"
99
#include <cstdlib>
1010
#include <fstream>
11+
#include <limits>
1112
#include <numeric>
1213

1314
namespace hsolver {
@@ -518,24 +519,37 @@ namespace hsolver {
518519
//==============================================================================
519520

520521
// ---------------------------------------------------------------------------
521-
// Lock converged eigenpairs: bands whose eigenvalue stops changing between
522-
// successive Rayleigh-Ritz steps are considered converged. This matches the
523-
// convergence criterion used by CG and Davidson (eigenvalue change < ethr).
522+
// Lock converged eigenpairs: a band whose residual norm (H|psi> - eps*S|psi>)
523+
// is below the threshold is converged. This matches the CG/BPCG criterion and
524+
// detects both gradual and one-step convergence.
524525
// ---------------------------------------------------------------------------
525526
template <typename T, typename Device>
526527
void DiagoPPCG<T, Device>::lock_epairs(
527-
const Real* eigenvalue_prev,
528-
const Real* eigenvalue,
528+
const std::vector<T>& residual,
529529
const std::vector<double>& ethr_band,
530530
std::vector<int>& active_cols) const
531531
{
532532
active_cols.clear();
533533
active_cols.reserve(n_band_);
534+
std::vector<double> nrm2_all(n_band_, 0.0);
535+
#ifdef _OPENMP
536+
#pragma omp parallel for schedule(static) if (n_dim_ * n_band_ > ppcg_openmp_work_threshold)
537+
#endif
534538
for (int j = 0; j < n_band_; ++j)
535539
{
540+
double nrm2 = 0.0;
541+
for (int ig = 0; ig < n_dim_; ++ig)
542+
{
543+
nrm2 += double(std::norm(residual[idx(ig, j, ld_psi_)]));
544+
}
545+
nrm2_all[j] = nrm2;
546+
}
547+
reduce_pool_if_mpi_ready(nrm2_all.data(), n_band_);
548+
for (int j = 0; j < n_band_; ++j)
549+
{
550+
const Real rnrm = std::sqrt(std::max(Real(nrm2_all[j]), Real(0)));
536551
const Real thr = std::max(Real(ethr_band[j]), diag_thr_);
537-
const Real delta = std::abs(eigenvalue[j] - eigenvalue_prev[j]);
538-
if (delta > thr)
552+
if (rnrm > thr)
539553
{
540554
active_cols.push_back(j);
541555
}
@@ -549,10 +563,11 @@ template <typename T, typename Device>
549563
void DiagoPPCG<T, Device>::build_small_subspace(
550564
const T* psi,
551565
const std::vector<int>& cols,
566+
int nblk,
552567
SmallSubspace& subspace) const
553568
{
554569
const int l = int(cols.size());
555-
const int dim = 2 * l;
570+
const int dim = nblk * l;
556571
subspace.k.resize(dim * dim);
557572
subspace.m.resize(dim * dim);
558573
subspace.eval.resize(dim);
@@ -563,6 +578,12 @@ void DiagoPPCG<T, Device>::build_small_subspace(
563578
copy_cols(w_.data(), cols, subspace.w_l);
564579
copy_cols(sw_.data(), cols, subspace.sw_l);
565580
copy_cols(hw_.data(), cols, subspace.hw_l);
581+
if (nblk >= 3)
582+
{
583+
copy_cols(p_.data(), cols, subspace.p_l);
584+
copy_cols(sp_.data(), cols, subspace.sp_l);
585+
copy_cols(hp_.data(), cols, subspace.hp_l);
586+
}
566587

567588
// ---------------------------------------------------------------------------
568589
// Normalize w columns to unit S-norm for numerical stability.
@@ -617,6 +638,13 @@ void DiagoPPCG<T, Device>::build_small_subspace(
617638
subspace.sw_l,
618639
subspace.hw_l,
619640
l);
641+
if (nblk >= 3)
642+
{
643+
scale_to_unit_snorm(subspace.p_l,
644+
subspace.sp_l,
645+
subspace.hp_l,
646+
l);
647+
}
620648

621649
auto copy_block = [&](const std::vector<T>& src,
622650
const int col0,
@@ -657,6 +685,12 @@ void DiagoPPCG<T, Device>::build_small_subspace(
657685
copy_block(subspace.w_l, l, subspace.basis);
658686
copy_block(subspace.hw_l, l, subspace.hbasis);
659687
copy_block(subspace.sw_l, l, subspace.sbasis);
688+
if (nblk >= 3)
689+
{
690+
copy_block(subspace.p_l, 2 * l, subspace.basis);
691+
copy_block(subspace.hp_l, 2 * l, subspace.hbasis);
692+
copy_block(subspace.sp_l, 2 * l, subspace.sbasis);
693+
}
660694

661695
gram(subspace.basis.data(), subspace.hbasis.data(), dim, dim, subspace.k, dim);
662696
gram(subspace.basis.data(), subspace.sbasis.data(), dim, dim, subspace.m, dim);
@@ -721,30 +755,48 @@ void DiagoPPCG<T, Device>::update_one_block(
721755
T* psi,
722756
const std::vector<int>& cols,
723757
int l,
758+
int nblk,
724759
SmallSubspace& subspace)
725760
{
726-
const int dim = 2 * l;
761+
const int dim = nblk * l;
727762
const T* eigvec = subspace.k.data();
728763

729764
subspace.psi_new.assign(ld_psi_ * l, T(0));
730765
subspace.spsi_new.assign(ld_psi_ * l, T(0));
731766
subspace.hpsi_new.assign(ld_psi_ * l, T(0));
767+
subspace.p_new.assign(ld_psi_ * l, T(0));
768+
subspace.sp_new.assign(ld_psi_ * l, T(0));
769+
subspace.hp_new.assign(ld_psi_ * l, T(0));
732770

771+
// coeff_state: full Ritz-vector rows [psi, w, p] -> new iterate.
772+
// coeff_p: the [w, p] rows (psi rows zero) -> new search direction.
733773
subspace.coeff_state.resize(dim * l);
774+
subspace.coeff_p.resize(dim * l);
734775
#ifdef _OPENMP
735776
#pragma omp parallel for schedule(static) if (l * l > ppcg_openmp_work_threshold)
736777
#endif
737778
for (int j = 0; j < l; ++j)
738779
{
739780
for (int i = 0; i < l; ++i)
740781
{
741-
subspace.coeff_state[i + j * dim] = eigvec[i + j * dim];
742-
subspace.coeff_state[(l + i) + j * dim] = eigvec[(l + i) + j * dim];
782+
const T c_psi = eigvec[i + j * dim];
783+
const T c_w = eigvec[(l + i) + j * dim];
784+
subspace.coeff_state[i + j * dim] = c_psi;
785+
subspace.coeff_state[(l + i) + j * dim] = c_w;
786+
subspace.coeff_p[i + j * dim] = T(0);
787+
subspace.coeff_p[(l + i) + j * dim] = c_w;
788+
if (nblk >= 3)
789+
{
790+
const T c_p = eigvec[(2 * l + i) + j * dim];
791+
subspace.coeff_state[(2 * l + i) + j * dim] = c_p;
792+
subspace.coeff_p[(2 * l + i) + j * dim] = c_p;
793+
}
743794
}
744795
}
745796

746797
auto fill_basis = [&](const std::vector<T>& a,
747798
const std::vector<T>& b,
799+
const std::vector<T>& c,
748800
std::vector<T>& basis)
749801
{
750802
basis.resize(ld_psi_ * dim);
@@ -759,6 +811,12 @@ void DiagoPPCG<T, Device>::update_one_block(
759811
std::copy(b.begin() + j * ld_psi_,
760812
b.begin() + (j + 1) * ld_psi_,
761813
basis.begin() + (l + j) * ld_psi_);
814+
if (nblk >= 3)
815+
{
816+
std::copy(c.begin() + j * ld_psi_,
817+
c.begin() + (j + 1) * ld_psi_,
818+
basis.begin() + (2 * l + j) * ld_psi_);
819+
}
762820
}
763821
};
764822

@@ -783,17 +841,23 @@ void DiagoPPCG<T, Device>::update_one_block(
783841
ld_psi_);
784842
};
785843

786-
fill_basis(subspace.psi_l, subspace.w_l, subspace.basis);
787-
fill_basis(subspace.spsi_l, subspace.sw_l, subspace.sbasis);
788-
fill_basis(subspace.hpsi_l, subspace.hw_l, subspace.hbasis);
844+
fill_basis(subspace.psi_l, subspace.w_l, subspace.p_l, subspace.basis);
845+
fill_basis(subspace.spsi_l, subspace.sw_l, subspace.sp_l, subspace.sbasis);
846+
fill_basis(subspace.hpsi_l, subspace.hw_l, subspace.hp_l, subspace.hbasis);
789847

790848
combine(subspace.basis, subspace.coeff_state, subspace.psi_new);
791849
combine(subspace.sbasis, subspace.coeff_state, subspace.spsi_new);
792850
combine(subspace.hbasis, subspace.coeff_state, subspace.hpsi_new);
851+
combine(subspace.basis, subspace.coeff_p, subspace.p_new);
852+
combine(subspace.sbasis, subspace.coeff_p, subspace.sp_new);
853+
combine(subspace.hbasis, subspace.coeff_p, subspace.hp_new);
793854

794855
scatter_cols(psi, cols, subspace.psi_new);
795856
scatter_cols(spsi_.data(), cols, subspace.spsi_new);
796857
scatter_cols(hpsi_.data(), cols, subspace.hpsi_new);
858+
scatter_cols(p_.data(), cols, subspace.p_new);
859+
scatter_cols(sp_.data(), cols, subspace.sp_new);
860+
scatter_cols(hp_.data(), cols, subspace.hp_new);
797861
}
798862

799863
// ---------------------------------------------------------------------------
@@ -805,11 +869,6 @@ void DiagoPPCG<T, Device>::rayleigh_ritz(
805869
std::vector<int>& active_cols,
806870
const std::vector<double>& ethr_band)
807871
{
808-
// Remember the eigenvalues of the previous step; convergence is measured
809-
// as the eigenvalue change between successive Rayleigh-Ritz steps.
810-
eval_prev_.resize(n_band_);
811-
std::copy(eigenvalue, eigenvalue + n_band_, eval_prev_.begin());
812-
813872
gram(psi, hpsi_.data(), n_band_, n_band_, rr_hsub_, n_band_);
814873
gram(psi, spsi_.data(), n_band_, n_band_, rr_ssub_, n_band_);
815874

@@ -916,7 +975,7 @@ void DiagoPPCG<T, Device>::rayleigh_ritz(
916975
}
917976
}
918977

919-
lock_epairs(eval_prev_.data(), eigenvalue, ethr_band, active_cols);
978+
lock_epairs(w_, ethr_band, active_cols);
920979
}
921980

922981
} // namespace hsolver
@@ -954,6 +1013,9 @@ double DiagoPPCG<T, Device>::diag(const HPsiFunc& hpsi_func,
9541013
w_.assign(sz, T(0));
9551014
sw_.assign(sz, T(0));
9561015
hw_.assign(sz, T(0));
1016+
p_.assign(sz, T(0));
1017+
sp_.assign(sz, T(0));
1018+
hp_.assign(sz, T(0));
9571019
rr_psi_.resize(sz);
9581020
rr_spsi_.resize(sz);
9591021
rr_hpsi_.resize(sz);
@@ -1010,12 +1072,22 @@ double DiagoPPCG<T, Device>::diag(const HPsiFunc& hpsi_func,
10101072
std::vector<T> w_active;
10111073
std::vector<T> sw_active;
10121074
std::vector<T> hw_active;
1075+
std::vector<T> p_active;
1076+
std::vector<T> sp_active;
1077+
std::vector<T> hp_active;
10131078
w_active.reserve(sz);
10141079
sw_active.reserve(sz);
10151080
hw_active.reserve(sz);
1081+
p_active.reserve(sz);
1082+
sp_active.reserve(sz);
1083+
hp_active.reserve(sz);
10161084
std::vector<int> cols;
10171085
cols.reserve(std::min(sbsize_, ncol));
10181086
SmallSubspace subspace;
1087+
bool use_p = false; // previous search direction becomes available after
1088+
// the first block update.
1089+
Real prev_res = std::numeric_limits<Real>::max(); // restart watchdog
1090+
int stall_streak = 0; // consecutive residual rises
10191091

10201092
while (!active_cols.empty() && iter <= maxiter_)
10211093
{
@@ -1041,31 +1113,90 @@ double DiagoPPCG<T, Device>::diag(const HPsiFunc& hpsi_func,
10411113
scatter_cols(hw_.data(), active_cols, hw_active);
10421114
scatter_cols(sw_.data(), active_cols, sw_active);
10431115

1116+
// S-orthogonalize the previous search direction p against psi, then
1117+
// re-apply H/S. The full Rayleigh-Ritz rotation re-mixes psi columns
1118+
// every step, which would otherwise let p drift into psi's span and
1119+
// destabilize the [psi, w, p] block subspace.
1120+
if (use_p)
1121+
{
1122+
copy_cols(p_.data(), active_cols, p_active);
1123+
sp_active.assign(ld_psi_ * nact, T(0));
1124+
apply_s_current(p_active.data(), sp_active.data(), nact);
1125+
scatter_cols(sp_.data(), active_cols, sp_active);
1126+
project_against(psi_in, spsi_.data(), all_cols, p_, sp_, active_cols);
1127+
1128+
copy_cols(p_.data(), active_cols, p_active);
1129+
force_g0_real(p_active.data(), nact);
1130+
hp_active.assign(ld_psi_ * nact, T(0));
1131+
sp_active.assign(ld_psi_ * nact, T(0));
1132+
scatter_cols(p_.data(), active_cols, p_active);
1133+
apply_h(hpsi_func, p_active.data(), hp_active.data(), nact);
1134+
apply_s_current(p_active.data(), sp_active.data(), nact);
1135+
scatter_cols(hp_.data(), active_cols, hp_active);
1136+
scatter_cols(sp_.data(), active_cols, sp_active);
1137+
}
1138+
10441139
avg_iter += double(nact) / double(ncol);
10451140

1046-
// Use the stable 2-block [psi, w] projected subspace. The
1047-
// preconditioned residual w is normalized to unit S-norm before
1048-
// building the Gram matrix (see build_small_subspace), which
1049-
// keeps M well-conditioned even when residuals are small.
1141+
// LOBPCG-style block subspace. On the first sweep only [psi, w] is
1142+
// available; afterwards the previous search direction p is added as a
1143+
// third block, which restores the conjugate-gradient acceleration.
1144+
// The w/p blocks are normalized to unit S-norm before building the
1145+
// Gram matrix (see build_small_subspace), keeping M well-conditioned.
10501146

10511147
// Block subspace solve.
1148+
const int nblk = use_p ? 3 : 2;
10521149
for (int isb = 0; isb < nsb; ++isb)
10531150
{
10541151
const int i0 = isb * sbsize_;
10551152
const int l = std::min(sbsize_, nact - i0);
10561153
cols.assign(active_cols.begin() + i0,
10571154
active_cols.begin() + i0 + l);
10581155

1059-
build_small_subspace(psi_in, cols, subspace);
1060-
solve_small_generalized(2 * l, subspace);
1061-
update_one_block(psi_in, cols, l, subspace);
1156+
build_small_subspace(psi_in, cols, nblk, subspace);
1157+
solve_small_generalized(nblk * l, subspace);
1158+
update_one_block(psi_in, cols, l, nblk, subspace);
10621159
}
1160+
use_p = true;
10631161

10641162
// Rayleigh-Ritz after each block update keeps the global subspace
10651163
// synchronized with the updated active vectors. The block update
10661164
// can otherwise drift into an ill-conditioned basis before the next
10671165
// Ritz rotation.
10681166
rayleigh_ritz(psi_in, eigenvalue_in, active_cols, ethr_band);
1167+
// Restart the search direction if the residual keeps rising. With a
1168+
// poor preconditioner the LOBPCG recurrence can stagnate (or slowly
1169+
// diverge) instead of reducing the residual. Requiring several
1170+
// consecutive rises avoids resetting on a transient bump, and a reset
1171+
// falls back to a steepest-descent step to recover the low eigenpairs.
1172+
{
1173+
const Real cur_res = max_generalized_residual(hpsi_.data(),
1174+
spsi_.data(),
1175+
eigenvalue_in,
1176+
ld_psi_,
1177+
n_dim_,
1178+
ncol);
1179+
const Real rel_tol = std::max(Real(1e-12),
1180+
Real(1e2) * std::numeric_limits<Real>::epsilon());
1181+
const bool rising = cur_res > prev_res * (Real(1) + rel_tol);
1182+
if (rising)
1183+
{
1184+
++stall_streak;
1185+
}
1186+
else
1187+
{
1188+
stall_streak = 0;
1189+
}
1190+
if (stall_streak >= 3)
1191+
{
1192+
std::fill(p_.begin(), p_.end(), T(0));
1193+
std::fill(sp_.begin(), sp_.end(), T(0));
1194+
std::fill(hp_.begin(), hp_.end(), T(0));
1195+
use_p = false;
1196+
stall_streak = 0;
1197+
}
1198+
prev_res = cur_res;
1199+
}
10691200
// The Rayleigh-Ritz rotation already keeps hpsi_/spsi_ consistent
10701201
// with the rotated psi up to rounding; re-applying H/S exactly is
10711202
// only needed every rr_step_ iterations to reset the accumulated
@@ -1074,6 +1205,8 @@ double DiagoPPCG<T, Device>::diag(const HPsiFunc& hpsi_func,
10741205
{
10751206
apply_h(hpsi_func, psi_in, hpsi_.data(), ncol);
10761207
apply_s_current(psi_in, spsi_.data(), ncol);
1208+
apply_h(hpsi_func, p_.data(), hp_.data(), ncol);
1209+
apply_s_current(p_.data(), sp_.data(), ncol);
10771210
}
10781211
record_residual(iter, "rayleigh_ritz");
10791212

0 commit comments

Comments
 (0)