Skip to content

Commit 6ba1eaf

Browse files
committed
Replace get_v3, get_odd_straight and gradient Fortran calls with numpy matrix products
Profiling the PdH 4x4x4 example showed these three SCHAModules routines spending their time in serial loops that are really matrix products: - get_v3: 13.8 s -> 0.9 s (averaged one chunk of configurations at a time to bound memory; the per-element error the Fortran routine computed was never returned to the callers and is skipped) - get_odd_straight: 22.9 s -> 1.4 s - get_gradient_supercell_new: 0.47 s -> 0.16 s at N = 1000 (its OpenMP pragmas are commented out upstream) The helpers reuse the Fortran get_emat, get_g and get_upsilon_matrix for the small input matrices. All outputs match the old implementation to 1e-14 on the PdH ensemble. The include_v4 and fast_grad branches are unchanged, and the replaced Fortran routines are left in place. The speedups do not depend on numpy's threaded BLAS. On a 120-atom test cell forced to a single thread (like a one-core cluster job): get_v3 102 s -> 2.6 s (the Fortran OpenMP also drops to one core), get_odd_straight 13.0 s -> 2.3 s, gradient 0.35 s -> 0.07 s. With the matching CellConstructor change the PdH hessian example goes from 224 s to 24 s (peak memory 1.5 -> 3.4 GB). Signed-off-by: Patrick Avery <patrick.avery@kitware.com>
1 parent 70c3060 commit 6ba1eaf

1 file changed

Lines changed: 97 additions & 15 deletions

File tree

Modules/Ensemble.py

Lines changed: 97 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,90 @@ def default(self, obj):
140140
return obj.tolist()
141141
return json.JSONEncoder.default(self, obj)
142142

143+
144+
def _get_v3_blas(a, er, transmode, amass, ityp_sc, f, u, rho):
145+
"""
146+
Same calculation as SCHAModules.get_v3 (with log_err = 'err_yesrho'),
147+
written with numpy matrix products instead of element-by-element loops.
148+
149+
The average over the configurations is done one chunk at a time to keep
150+
memory bounded for large ensembles. The per-element error that get_v3
151+
computed internally was never returned to the callers, so it is not
152+
computed here.
153+
"""
154+
n_random, nat_sc = u.shape[0], u.shape[1]
155+
n_modes = 3 * nat_sc
156+
157+
e = SCHAModules.get_emat(er, a, amass, ityp_sc, False, transmode)
158+
eprod = e.T @ e
159+
u2 = u.reshape(n_random, n_modes)
160+
f2 = f.reshape(n_random, n_modes)
161+
ur = u2 @ eprod
162+
163+
rf = (rho[:, None] * f2) / np.sum(rho)
164+
v3 = np.einsum("x,yz->xyz", rf.sum(axis=0), eprod)
165+
166+
# v3 -= sum_i rf[i,x] ur[i,y] ur[i,z], one chunk of configurations at a
167+
# time so the (chunk, n_modes^2) intermediate stays around ~256 MB
168+
chunk = max(1, (32 * 1024 * 1024) // (n_modes * n_modes))
169+
for i0 in range(0, n_random, chunk):
170+
i1 = min(i0 + chunk, n_random)
171+
outer = (ur[i0:i1, :, None] * ur[i0:i1, None, :]).reshape(i1 - i0, -1)
172+
v3 -= (rf[i0:i1].T @ outer).reshape(n_modes, n_modes, n_modes)
173+
174+
return v3
175+
176+
177+
def _get_odd_straight_blas(a, w, er, transmode, amass, ityp_sc, T, v3):
178+
"""
179+
Same calculation as SCHAModules.get_odd_straight, written with numpy
180+
matrix products: v3 is contracted with the polarization matrix on two
181+
indexes, then one final matrix product (with the g factor included)
182+
gives phi_odd.
183+
"""
184+
n_modes = len(a)
185+
l = SCHAModules.get_emat(er, a, amass, ityp_sc, True, transmode)
186+
g = SCHAModules.get_g(a, w, transmode, T)
187+
m = np.tensordot(np.tensordot(v3, l, axes=([1], [1])), l, axes=([1], [1]))
188+
m2 = m.reshape(n_modes, -1)
189+
return 0.5 * (m2 @ (m * g[None, :, :]).reshape(n_modes, -1).T)
190+
191+
192+
def _get_gradient_supercell_blas(rho, u_disp, eforces, w, pols, trans, T, mass, ityp):
193+
"""
194+
Same calculation as SCHAModules.get_gradient_supercell_new (with
195+
log_err = 'err_yesrho'), written with numpy matrix products. Returns
196+
the preconditioned gradient and its stochastic error.
197+
"""
198+
n_random, nat = u_disp.shape[0], u_disp.shape[1]
199+
n_modes = 3 * nat
200+
201+
ups = SCHAModules.get_upsilon_matrix(w, pols, trans, mass, ityp, T)
202+
u2 = u_disp.reshape(n_random, n_modes)
203+
f2 = eforces.reshape(n_random, n_modes)
204+
v = u2 @ ups
205+
206+
nc = float(n_random)
207+
av_rho = rho.sum() / nc
208+
s_rho = ((rho - av_rho) ** 2).sum() / (nc - 1)
209+
210+
rv = rho[:, None] * v
211+
av_f1 = (rv.T @ f2) / nc
212+
s_f = (((rho ** 2)[:, None] * v ** 2).T @ f2 ** 2 - nc * av_f1 ** 2) / (nc - 1)
213+
s_f_rho = ((rho[:, None] * rv).T @ f2 - nc * av_f1 * av_rho) / (nc - 1)
214+
215+
grad = av_f1 / av_rho
216+
with np.errstate(divide="ignore", invalid="ignore"):
217+
grad_err = np.abs(grad) / np.sqrt(nc) * np.sqrt(
218+
s_f / av_f1 ** 2 + s_rho / av_rho ** 2
219+
- 2 * s_f_rho / (av_rho * av_f1))
220+
221+
# get_gradient_supercell_new symmetrized the gradient (not the error)
222+
# the same way before returning
223+
grad = 0.5 * (grad + grad.T)
224+
return grad, grad_err
225+
226+
143227
class Ensemble:
144228
__debug_index__ = 0
145229

@@ -2759,15 +2843,13 @@ def get_preconditioned_gradient(self, subtract_sscha = True, return_error = Fals
27592843
nat, 3*nat, len(mass), preconditioned)
27602844
else:
27612845
if timer:
2762-
grad, grad_err = timer.execute_timed_function(SCHAModules.get_gradient_supercell_new,
2846+
grad, grad_err = timer.execute_timed_function(_get_gradient_supercell_blas,
27632847
self.rho, u_disp, eforces, w, pols, trans,
2764-
self.current_T, mass, ityp, log_err, self.N,
2765-
nat, 3*nat, len(mass),
2766-
override_name = "get_gradient_supercell_new")
2848+
self.current_T, mass, ityp,
2849+
override_name = "get_gradient_supercell_blas")
27672850
else:
2768-
grad, grad_err = SCHAModules.get_gradient_supercell_new(self.rho, u_disp, eforces, w, pols, trans,
2769-
self.current_T, mass, ityp, log_err, self.N,
2770-
nat, 3*nat, len(mass))
2851+
grad, grad_err = _get_gradient_supercell_blas(self.rho, u_disp, eforces, w, pols, trans,
2852+
self.current_T, mass, ityp)
27712853

27722854

27732855
# If we are at gamma, we can skip this part
@@ -3709,11 +3791,11 @@ def get_free_energy_hessian(self, include_v4 = False, get_full_hessian = True, v
37093791
if verbose:
37103792
print ("Going into d3")
37113793
if timer:
3712-
d3 = timer.execute_timed_function(SCHAModules.get_v3, a, new_pol, trans, amass, ityp,
3713-
f, u, self.rho, log_err, override_name="SCHAModules.get_v3")
3794+
d3 = timer.execute_timed_function(_get_v3_blas, a, new_pol, trans, amass, ityp,
3795+
f, u, self.rho, override_name="get_v3_blas")
37143796
else:
3715-
d3 = SCHAModules.get_v3(a, new_pol, trans, amass, ityp,
3716-
f, u, self.rho, log_err)
3797+
d3 = _get_v3_blas(a, new_pol, trans, amass, ityp,
3798+
f, u, self.rho)
37173799
if verbose:
37183800
print("Outside d3")
37193801

@@ -3777,11 +3859,11 @@ def get_free_energy_hessian(self, include_v4 = False, get_full_hessian = True, v
37773859
print (" ITYP = ", ityp)
37783860
print (" T = ", self.current_T)
37793861
if timer:
3780-
phi_sc_odd = timer.execute_timed_function(SCHAModules.get_odd_straight, a, w, new_pol, trans, amass, ityp,
3781-
self.current_T, d3)
3862+
phi_sc_odd = timer.execute_timed_function(_get_odd_straight_blas, a, w, new_pol, trans, amass, ityp,
3863+
self.current_T, d3, override_name="get_odd_straight_blas")
37823864
else:
3783-
phi_sc_odd = SCHAModules.get_odd_straight(a, w, new_pol, trans, amass, ityp,
3784-
self.current_T, d3)
3865+
phi_sc_odd = _get_odd_straight_blas(a, w, new_pol, trans, amass, ityp,
3866+
self.current_T, d3)
37853867

37863868
if verbose:
37873869
print ("Outside odd straight.")

0 commit comments

Comments
 (0)