88#include " source_base/kernels/math_kernel_op.h"
99#include < cstdlib>
1010#include < fstream>
11+ #include < limits>
1112#include < numeric>
1213
1314namespace 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// ---------------------------------------------------------------------------
525526template <typename T, typename Device>
526527void 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>
549563void 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