Skip to content

Commit a264c13

Browse files
maki49claude
andcommitted
Perf: speed up the LR kernel-to-potential integrands by ~45%
`PotHxcLR::cal_v_eff` dominates an LR run: 74% of the total for 07_LiF/pbe (949 s of 1289 s over 541 calls). Roughly half of that was FFT; the rest was serial element-wise work and allocator traffic. Five changes, no algorithmic change: 1. OpenMP on the element-wise loops. Everything these loops call (`grad_rho`, `grad_dot`, the Hartree kernel) was already parallel; the integrands here were the only part still running on one thread. 2. Raw pointers instead of `.at()` (43 sites). The bounds check is a branch per access, ~10 per grid point, and it blocks vectorization. The outer-vector lookups (`drho_gs.at(0)`) are hoisted out of the loops, where they keep their bounds check for free. 3. A scratch pool instead of per-call temporaries. `drho`, `gdot_terms` and `vxc_tmp` were allocated and value-initialized on every call and then overwritten before being read -- ~450 MB of pointless memset per call at a 200^3 grid, plus the page faults. `grad_dot` assigns rather than accumulates, so even `vxc_tmp`'s zero-fill was dead. The pool is shared by all instances (a run holds three `PotHxcLR`) so it does not multiply. 4. `add_v_hartree` replaces `H_Hartree_pw::v_hartree`, which (a) re-did the forward FFT of rho^X that the GGA branch needs anyway -- 1 of the 10 FFTs per call was pure duplication, (b) reduced a Hartree "energy" of the transition density through `Parallel_Reduce::reduce_pool` every call, a collective nobody reads that also clobbers the global `H_Hartree_pw::hartree_energy`, and (c) returned a `matrix` by value, to which `v_eff += 2 * (...)` added a second full-size temporary. 5. The nspin=2 singlet/triplet combinations `v2rho2_uu -+ v2rho2_ud` and `2*vsigma_uu -+ vsigma_ud` are pre-contracted once per potential instead of being rebuilt at every grid point of every call, which also replaces two strided reads with one contiguous one. Costs 8-16 B/point, and only for closed-shell nspin=2. `PotGradXCLR::cal_v_eff` (the g^xc branch feeding the Z-vector RHS) gets 1-3 of the same treatment, for -20% to -28%. The shared pool matters more there: a `PotGradXCLR` is constructed inside the loop over excited states, so per-object buffers would never be reused at all. Measured (single node, 16 threads), analytic gradients unchanged: 01_Si/lda 93 s -> 51 s (-45%) cal_v_eff 66 -> 45 ms/call 01_Si/pbe 229 s -> 125 s (-46%) cal_v_eff 356 -> 268 ms/call 02_Li2/lda 165 s -> 86 s (-48%) cal_v_eff 559 -> 278 ms/call 02_Li2/pbe 530 s -> 253 s (-52%) cal_v_eff 2651 -> 1553 ms/call (The total also benefits from solving the Z-vector equation once instead of three times, a separate fix; the per-call figures above isolate this commit.) Excitation energies are identical to every printed digit; the nspin=1 cases agree to 1e-14, and of 60 force components per nspin=2 case, 2-3 differ by exactly one unit in the last printed digit (the Hartree prefactor is now associated differently). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QMV7xFZ69hhc3HnVBHbD96
1 parent 073ab9b commit a264c13

4 files changed

Lines changed: 335 additions & 184 deletions

File tree

source/source_lcao/module_lr/Grad/xc/pot_grad_xc.cpp

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,24 @@
88
#include <set>
99
namespace LR
1010
{
11+
using Vec3 = ModuleBase::Vector3<double>;
12+
PotGradXCLR::Scratch& PotGradXCLR::scratch()
13+
{
14+
static Scratch sc; // see the comment on `Scratch` in the header
15+
return sc;
16+
}
17+
18+
void PotGradXCLR::Scratch::alloc(const int nrxx, const bool gga)
19+
{
20+
// resize() on an already-large vector is a no-op, so only the first call allocates.
21+
if (static_cast<int>(this->vtmp.size()) < nrxx) { this->vtmp.resize(nrxx); }
22+
if (gga)
23+
{
24+
if (static_cast<int>(this->gdot.size()) < nrxx) { this->gdot.resize(nrxx); }
25+
if (static_cast<int>(this->drho1.size()) < nrxx) { this->drho1.resize(nrxx); }
26+
}
27+
}
28+
1129
// constructor for exchange-correlation kernel
1230
PotGradXCLR::PotGradXCLR(const KernelXC& xc_kernel, const ModulePW::PW_Basis& rho_basis, const UnitCell& ucell,
1331
const int& nrxx, const bool triplet)
@@ -41,46 +59,66 @@ namespace LR
4159

4260
if (func_type == 1) // LDA: only the $g^{\rho\rho\rho}$ term survives
4361
{
62+
const double* const a_s2 = g.a_s2.data();
63+
const double* const r1 = rho[0];
64+
double* const v = v_eff.c;
65+
#ifdef _OPENMP
66+
#pragma omp parallel for schedule(static)
67+
#endif
4468
for (int ir = 0;ir < nrxx_;++ir)
4569
{
46-
v_eff(0, ir) += ModuleBase::e2 * g.a_s2.at(ir) * rho[0][ir] * rho[0][ir];
70+
v[ir] += ModuleBase::e2 * a_s2[ir] * r1[ir] * r1[ir];
4771
}
4872
}
4973
else if (func_type == 2 || func_type == 4) // GGA or HYB_GGA
5074
{
51-
std::vector<ModuleBase::Vector3<double>> drho1(nrxx_); // transition density gradient
52-
LR_Util::grad(rho[0], drho1.data(), this->rho_basis_, this->tpiba_);
75+
scratch().alloc(nrxx_, /*gga=*/true);
76+
Vec3* const drho1 = scratch().drho1.data(); // transition density gradient
77+
LR_Util::grad(rho[0], drho1, this->rho_basis_, this->tpiba_);
5378

54-
std::vector<double> v_tmp(nrxx_, 0.0);
79+
double* const v_tmp = scratch().vtmp.data();
80+
Vec3* const gdot_terms = scratch().gdot.data();
81+
const Vec3* const dgs = kxc.drho_gs.at(0).data();
82+
const double* const r1 = rho[0];
83+
const double* const e_s2 = g.e_s2.data(); const double* const e_st = g.e_st.data();
84+
const double* const e_t2 = g.e_t2.data(); const double* const e_q = g.e_q.data();
85+
const double* const c_s = g.c_s.data(); const double* const c_t = g.c_t.data();
86+
const double* const a_s2 = g.a_s2.data(); const double* const a_st = g.a_st.data();
87+
const double* const a_t2 = g.a_t2.data(); const double* const a_q = g.a_q.data();
5588

5689
// 1. the vector under the divergence, accumulated negated so that `grad_dot` yields
5790
// $-\nabla\cdot\boldsymbol{E}$. The four $e$ coefficients share the same
5891
// $\nabla\rho^{gs}$ direction (see `KernelXC::GxcCoef`), so it is pulled out of
5992
// their sum -- exact, and it keeps this bandwidth-bound loop reading 4 doubles per
6093
// point instead of 12.
61-
std::vector<ModuleBase::Vector3<double>> gdot_terms(nrxx_);
94+
#ifdef _OPENMP
95+
#pragma omp parallel for schedule(static)
96+
#endif
6297
for (int ir = 0;ir < nrxx_;++ir)
6398
{
64-
const ModuleBase::Vector3<double>& drho = kxc.drho_gs.at(0).at(ir); // $\nabla\rho$
65-
const double s = rho[0][ir]; // $\rho^1$
66-
const double t = drho * drho1.at(ir); // $\nabla\rho\cdot\nabla\rho^1$
67-
const double q = drho1.at(ir) * drho1.at(ir); // $\nabla\rho^1\cdot\nabla\rho^1$
68-
const double e = g.e_s2.at(ir) * (s * s) + g.e_st.at(ir) * (s * t)
69-
+ g.e_t2.at(ir) * (t * t) + g.e_q.at(ir) * q;
70-
gdot_terms[ir] = -(drho * e + drho1.at(ir) * (g.c_s.at(ir) * s + g.c_t.at(ir) * t));
99+
const Vec3& drho = dgs[ir]; // $\nabla\rho$
100+
const double s = r1[ir]; // $\rho^1$
101+
const double t = drho * drho1[ir]; // $\nabla\rho\cdot\nabla\rho^1$
102+
const double q = drho1[ir] * drho1[ir]; // $\nabla\rho^1\cdot\nabla\rho^1$
103+
const double e = e_s2[ir] * (s * s) + e_st[ir] * (s * t)
104+
+ e_t2[ir] * (t * t) + e_q[ir] * q;
105+
gdot_terms[ir] = -(drho * e + drho1[ir] * (c_s[ir] * s + c_t[ir] * t));
71106
}
72-
XC_Functional::grad_dot(gdot_terms.data(), v_tmp.data(), &this->rho_basis_, this->tpiba_);
107+
XC_Functional::grad_dot(gdot_terms, v_tmp, &this->rho_basis_, this->tpiba_);
73108

74109
// 2. the local terms $A$
110+
#ifdef _OPENMP
111+
#pragma omp parallel for schedule(static)
112+
#endif
75113
for (int ir = 0;ir < nrxx_;++ir)
76114
{
77-
const double s = rho[0][ir];
78-
const double t = kxc.drho_gs.at(0).at(ir) * drho1.at(ir);
79-
const double q = drho1.at(ir) * drho1.at(ir);
80-
v_tmp[ir] += g.a_s2.at(ir) * (s * s) + g.a_st.at(ir) * (s * t)
81-
+ g.a_t2.at(ir) * (t * t) + g.a_q.at(ir) * q;
115+
const double s = r1[ir];
116+
const double t = dgs[ir] * drho1[ir];
117+
const double q = drho1[ir] * drho1[ir];
118+
v_tmp[ir] += a_s2[ir] * (s * s) + a_st[ir] * (s * t)
119+
+ a_t2[ir] * (t * t) + a_q[ir] * q;
82120
}
83-
BlasConnector::axpy(nrxx_, ModuleBase::e2, v_tmp.data(), 1, v_eff.c, 1);
121+
BlasConnector::axpy(nrxx_, ModuleBase::e2, v_tmp, 1, v_eff.c, 1);
84122
}
85123
else
86124
{
@@ -90,5 +128,4 @@ namespace LR
90128

91129
ModuleBase::timer::end("PotGradXCLR", "cal_v_eff");
92130
}
93-
94131
}

source/source_lcao/module_lr/Grad/xc/pot_grad_xc.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@ namespace LR
1919
/// kernel components from PotHxcLR
2020
const KernelXC& xc_kernel_components_;
2121
const bool triplet_ = false;
22+
23+
private:
24+
/// Scratch, shared by every `PotGradXCLR` and grown on demand. These used to be
25+
/// allocated (and value-initialized) on every call. Safe to share because
26+
/// `cal_v_eff` is only ever entered from a single thread (all the OpenMP is inside).
27+
struct Scratch
28+
{
29+
std::vector<ModuleBase::Vector3<double>> drho1; ///< $\nabla\rho^1$
30+
std::vector<ModuleBase::Vector3<double>> gdot; ///< integrand of the divergence
31+
std::vector<double> vtmp; ///< local part $A$
32+
void alloc(const int nrxx, const bool gga);
33+
};
34+
static Scratch& scratch();
2235
};
2336

2437
}

0 commit comments

Comments
 (0)