11
2- ! This subroutine calculates the L mat needed to get the average of the
3- ! third order derivatives. It is formed by four polarization vectors
4- ! times the mass^1/2 divided by the normal length.
2+ ! This subroutine computes the odd (third order) SCHA correction to the
3+ ! free energy Hessian, including the fourth order term v4:
4+ !
5+ ! phi_sc_odd = v3^T . Lambda . (I - v4.Lambda)^-1 . v3
6+ !
7+ ! (R. Bianco et al., PRB 96, 014111 (2017), Eq. 27), with nl = n_mode^2.
8+ !
9+ ! MEMORY-OPTIMIZED "KRON" VERSION: the nl x nl Lambda matrix is NEVER
10+ ! materialized. The old path called get_cmat, which allocated TWO extra
11+ ! nl x nl temporaries (mat_e, mat_et) plus the nl x nl output cmat and did
12+ ! an O(N^6) dgemm; together with v4 that was a 4-array (4 "d4 units") peak.
13+ ! Instead we exploit the exact factorized structure that get_cmat builds
14+ ! (see get_cmat.f90, loops at its lines ~78-90):
15+ !
16+ ! mat_e (ka,ja) = e(nu,x) * e(mu,y)
17+ ! mat_et(ja,ka) = mat_e(ka,ja) * mat_w(mu,nu) * 0.5
18+ ! Lambda = mat_e . mat_et
19+ !
20+ ! with ka = (x-1)*N + y (y fast) and ja = (mu-1)*N + nu (nu fast), i.e.
21+ !
22+ ! Lambda(ka,ka') = sum_{mu,nu} e(nu,x) e(mu,y) D(mu,nu) e(nu,x') e(mu,y')
23+ !
24+ ! where e(N,N) comes from get_emat, D(mu,nu) = mat_w(mu,nu)/2 with mat_w
25+ ! from get_g (both small, O(N^2)). Applying Lambda therefore reduces to
26+ ! contractions with the SMALL matrix e (each an O(N^5) dgemm) plus a
27+ ! diagonal scaling over the mode pair (O(N^4)). The only O(N^6) operation
28+ ! left is the inversion of (I - v4.Lambda).
29+ !
30+ ! EXACT REPRODUCTION OF THE ORIGINAL PRODUCT (index bookkeeping).
31+ ! The original code built maux = I - v42 . Lambda with the reordered copy
32+ ! v42(ja,ka) = v4(w,z,x,y), ja = (w-1)*N + z (z fast),
33+ ! ka = (x-1)*N + y (y fast).
34+ ! Expanding, the matrix subtracted from the identity is, as a 4-tensor,
35+ !
36+ ! P(w,z,x,y) = sum_{x',y',mu,nu} v4(w,z,x',y') e(nu,x') e(mu,y')
37+ ! * D(mu,nu) * e(nu,x) e(mu,y)
38+ !
39+ ! placed at maux( (w-1)*N + z , (x-1)*N + y ). NO permutation symmetry of
40+ ! v4 is assumed anywhere: this is an identity in the indices, valid for
41+ ! arbitrary v4 (verified numerically against v1.5 with a NON-symmetrized
42+ ! random v4 as well; the old optional flag use_v4_symmetry is gone since
43+ ! the v42 copy it avoided no longer exists).
44+ !
45+ ! P is evaluated with partial (Kronecker-factor) contractions that
46+ ! ping-pong between exactly TWO nl x nl buffers: maux and v4 ITSELF used
47+ ! as scratch. Layouts below are column-major, leftmost index fastest:
48+ !
49+ ! A: B(w,z,x',mu) = sum_y' v4(w,z,x',y') e(mu,y') v4 -> maux
50+ ! one dgemm: (N^3 x N) . (N x N)^T, O(N^5)
51+ ! B: T(w,z,nu,mu) = sum_x' B(w,z,x',mu) e(nu,x') maux-> v4
52+ ! N slice dgemms over mu: (N^2 x N) . (N x N)^T, O(N^5)
53+ ! C: T(w,z,nu,mu) *= D(mu,nu) v4 in place, O(N^4)
54+ ! D: C(w,z,nu,y) = sum_mu T(w,z,nu,mu) e(mu,y) v4 -> maux
55+ ! one dgemm: (N^3 x N) . (N x N), O(N^5)
56+ ! E: P(w,z,x,y) = sum_nu C(w,z,nu,y) e(nu,x) maux-> v4
57+ ! N slice dgemms over y: (N^2 x N) . (N x N), O(N^5)
58+ ! F: maux((w-1)N+z,(x-1)N+y) = delta - P(w,z,x,y) v4 -> maux, O(N^4)
59+ ! explicit permuted copy: the natural buffer layout of P has (w fast,
60+ ! z slow) in the row pair and (x fast, y slow) in the column pair,
61+ ! while the original v42.Lambda uses (z fast, w slow) rows and
62+ ! (y fast, x slow) columns, so BOTH index pairs are swapped here.
63+ !
64+ ! After F: LU inversion in place in maux (dgetrf/dgetri, unchanged), then
65+ ! tmp = maux . v3^T (nl x ns, O(N^5))
66+ ! cf = Lambda . tmp via the same e/D/e^T factorization
67+ ! applied to skinny matrices, using two
68+ ! O(N^3) buffers (N x N x ns)
69+ ! phi_sc_odd = v32 . cf (as before)
70+ !
71+ ! !!! WARNING: v4 IS INTENT(INOUT) AND ITS CONTENT IS DESTROYED !!!
72+ ! v4 is deliberately used as one of the two nl x nl scratch buffers (its
73+ ! original content is consumed by step A before step B overwrites it).
74+ ! This is safe for the only caller, Ensemble.get_free_energy_hessian
75+ ! (Modules/Ensemble.py): it creates d4 with SCHAModules.get_v4 (hence
76+ ! F-contiguous, so f2py's intent(inout) wraps it in place without a copy),
77+ ! optionally symmetrizes it in place, calls this routine, and never uses
78+ ! d4 again. Any NEW caller must pass a throw-away, F-contiguous v4.
79+ !
80+ ! Peak large-memory budget: exactly 2 nl x nl arrays alive (v4 + maux),
81+ ! plus O(N^3) skinny buffers (v32, tmp, cf, b1, b2) and O(N^2)/O(N) work
82+ ! arrays. For N = 192 (Au 4x4x4): 2 * 10.87 GB = 21.7 GB, vs ~43.5 GB
83+ ! inside the old get_cmat call (v4 + mat_e + mat_et + cmat).
584
685subroutine get_odd_straight_with_v4 ( a , wr , er , transmode , amass , ityp_sc , T , v3 , v4 , phi_sc_odd , &
7- n_mode , nat_sc , ntyp , use_v4_symmetry )
86+ n_mode , nat_sc , ntyp )
887
988 implicit none
1089
@@ -15,163 +94,175 @@ subroutine get_odd_straight_with_v4 ( a, wr, er, transmode, amass, ityp_sc, T, v
1594 integer , dimension (nat_sc), intent (in ) :: ityp_sc
1695 double precision , intent (in ) :: T
1796 double precision , dimension (n_mode,n_mode,n_mode), intent (in ) :: v3
18- double precision , dimension (n_mode,n_mode,n_mode, n_mode), intent (in ) :: v4
97+ ! v4 is used as scratch and DESTROYED on exit -- see warning above.
98+ double precision , dimension (n_mode,n_mode,n_mode,n_mode), intent (inout ) :: v4
1999 double precision , dimension (n_mode, n_mode), intent (out ) :: phi_sc_odd
20- ! Optional: if present and .true., assume v4 is permutation-symmetric
21- ! (standard case, after ApplySymmetryToTensor4 / use_symmetries=True) and
22- ! skip building the explicit reordered copy v42 -- see note below.
23- logical , intent (in ), optional :: use_v4_symmetry
24-
25100
26101 integer :: nat_sc, n_mode, nl, ns, ntyp
27- double precision , dimension (:,:), allocatable :: l, g, phi_aux, v1, v2, v32
28- double precision :: lsum
29- double precision , dimension (:), allocatable :: laux1, lres1, veclong
30- double precision , dimension (:), allocatable :: laux2, lres2
31102
32- double precision , dimension (:,:), allocatable :: lamat, v42, maux
103+ ! The ONLY allocated nl x nl array (v4, the other big buffer, is the
104+ ! caller's own array).
105+ double precision , dimension (:,:), allocatable :: maux
106+
107+ ! Small O(N^2) factors of Lambda.
108+ double precision , dimension (:,:), allocatable :: e ! from get_emat
109+ double precision , dimension (:,:), allocatable :: dmat ! D = mat_w/2, from get_g
110+
111+ ! Skinny O(N^3) buffers.
112+ double precision , dimension (:,:), allocatable :: v32 ! ns x nl
113+ double precision , dimension (:,:), allocatable :: tmp, cf ! nl x ns
114+ double precision , dimension (:,:,:), allocatable :: b1, b2 ! ns x ns x ns
115+
33116 double precision , dimension (:), allocatable :: work
34117 integer , dimension (:), allocatable :: ipiv
35118 integer :: info
36119
37- double precision , dimension (:,:), allocatable :: cf
38- double precision , dimension (:,:), allocatable :: tmp
39-
40- integer :: mu, nu, alpha
41- integer :: ka, ja
42- integer :: i, j, x, y, z, w
43-
44- real :: t1, t2
120+ integer :: mu, nu
121+ integer :: ka
122+ integer :: i, s, x, y, z, w
45123
46124 logical , parameter :: debug = .true.
47- logical :: sym
48-
49- ! Get integers
50125
51126 if (debug) then
52- print * , " === DEBUG ODD STRAIGHT ==="
127+ print * , " === DEBUG ODD STRAIGHT (kron, Lambda never materialized) ==="
53128 print * , " N_MODE:" , n_mode
54129 print * , " NTYP:" , ntyp
55130 print * , " NAT_SC:" , nat_sc
56131 call flush()
57132 end if
58133
59- ! nat_sc = size(er(:,1,1))
60- ! n_mode = 3*nat_sc
61-
62134 ns = n_mode
63135 nl = n_mode* n_mode
64136
65- sym = .false.
66- if (present (use_v4_symmetry)) sym = use_v4_symmetry
67-
68- ! Allocate stuff
69-
70- allocate (lamat(nl,nl))
71137 allocate (maux(nl,nl))
138+ allocate (e(ns,ns))
139+ allocate (dmat(ns,ns))
140+ allocate (v32(ns,nl))
141+ allocate (tmp(nl,ns))
142+ allocate (cf(nl,ns))
143+ allocate (b1(ns,ns,ns))
144+ allocate (b2(ns,ns,ns))
72145 allocate (ipiv(nl))
73146 allocate (work(nl))
74- allocate (v32(n_mode,n_mode* n_mode))
75-
76- allocate (cf(nl,ns))
77- allocate (tmp(nl,ns))
78-
79- ! v42 (explicit reordered copy of v4) is only needed when we cannot rely on
80- ! the permutation symmetry of v4. It is one full nl x nl array.
81- if (.not. sym) allocate (v42(nl,nl))
82147
83- ! Get lambda matrix
84-
85- call get_cmat ( a, wr, er, transmode, amass, ityp_sc, T, .true. , lamat,n_mode, nat_sc, ntyp )
86-
87- ! print *, "AFTER CMAT"
88- ! call flush()
89-
90- ! Write third and fourth order force constants as rank 2
148+ ! Small factors of Lambda: exactly what get_cmat used internally.
149+ ! (v3_log = .true., as in the old get_cmat call from this routine.)
150+ call get_emat ( er, a, amass, ityp_sc, .true. , transmode, e, n_mode, nat_sc, ntyp)
151+ call get_g (a, wr, transmode, T, dmat, n_mode)
152+ dmat = 0.5d0 * dmat ! D(mu,nu) = mat_w(mu,nu)/2 (the 0.5 of mat_et)
91153
154+ ! Third order force constants as rank 2, v32(:,ka) with ka=(x-1)*N+y.
92155 ka = 0
93-
94- do x = 1 , n_mode
95- do y = 1 , n_mode
156+ do x = 1 , ns
157+ do y = 1 , ns
96158 ka = ka + 1
97159 v32(:,ka) = v3(:,x,y)
98- if (.not. sym) then
99- ja = 0
100- do w = 1 , n_mode
101- do z = 1 , n_mode
102- ja = ja + 1
103- v42(ja,ka) = v4(w,z,x,y)
104- end do
105- end do
106- end if
107160 end do
108161 end do
109162
110- ! Prepare identity matrix directly in maux (was: iden then maux = iden;
111- ! bitwise identical, avoids one nl x nl temporary)
112-
113- maux = 0.0d0
114-
115- do x = 1 , nl
116- maux(x,x) = 1.0d0
163+ ! ---------------------------------------------------------------------
164+ ! Build maux = I - v42.Lambda without forming Lambda (steps A-F above).
165+ ! ---------------------------------------------------------------------
166+
167+ ! A: B(w,z,x',mu) = sum_y' v4(w,z,x',y') e(mu,y') [v4 -> maux]
168+ ! v4 viewed as (N^3 x N) with columns y'; op(B)=e^T has entry
169+ ! (y',mu) = e(mu,y'). Fills maux completely (N^3 * N = nl*nl).
170+ call dgemm(' N' ,' T' , nl* ns, ns, ns, 1.0d0 , v4(1 ,1 ,1 ,1 ), nl* ns, &
171+ e(1 ,1 ), ns, 0.0d0 , maux(1 ,1 ), nl* ns)
172+
173+ ! B: T(w,z,nu,mu) = sum_x' B(w,z,x',mu) e(nu,x') [maux -> v4]
174+ ! For each mu, the slice B(:,:,:,mu) is the (N^2 x N) block starting
175+ ! at maux(1,(mu-1)*ns+1) (linear offset (mu-1)*N^3), columns x';
176+ ! result panel written at v4(:,:,:,mu) with layout (w,z,nu).
177+ ! From here on the original content of v4 is destroyed.
178+ do mu = 1 , ns
179+ call dgemm(' N' ,' T' , nl, ns, ns, 1.0d0 , maux(1 ,(mu-1 )* ns+1 ), nl, &
180+ e(1 ,1 ), ns, 0.0d0 , v4(1 ,1 ,1 ,mu), nl)
117181 end do
118182
119- ! Calculate ** iden - v4 lamat ** matrix
120-
121- ! print *, "BEFORE I - V4Lambda"
122- ! call flush()
123-
124- if (.not. sym) then
125- call dgemm(' N' ,' N' ,nl,nl,nl,- 1.0d0 ,v42,nl,lamat,nl,1.0d0 ,maux,nl)
126- else
127- ! v4 fully permutation-symmetric => the reordered copy v42(ja,ka)=v4(w,z,x,y)
128- ! equals the plain column-major reshape of v4 into an (nl,nl) matrix, so we
129- ! can feed v4 directly to dgemm with leading dimension nl and skip v42.
130- ! (Not valid with use_symmetries=False -> guarded by use_v4_symmetry.)
131- call dgemm(' N' ,' N' ,nl,nl,nl,- 1.0d0 ,v4,nl,lamat,nl,1.0d0 ,maux,nl)
132- end if
133-
134- if (allocated (v42)) deallocate (v42)
183+ ! C: T(w,z,nu,mu) *= D(mu,nu) [v4 in place]
184+ ! NOTE the argument order: 4th dim of T is mu, 3rd is nu.
185+ do mu = 1 , ns
186+ do nu = 1 , ns
187+ v4(:,:,nu,mu) = v4(:,:,nu,mu) * dmat(mu,nu)
188+ end do
189+ end do
135190
136- ! Invert ** iden - lamat v4 **
191+ ! D: C(w,z,nu,y) = sum_mu T(w,z,nu,mu) e(mu,y) [v4 -> maux]
192+ call dgemm(' N' ,' N' , nl* ns, ns, ns, 1.0d0 , v4(1 ,1 ,1 ,1 ), nl* ns, &
193+ e(1 ,1 ), ns, 0.0d0 , maux(1 ,1 ), nl* ns)
137194
138- ! print *, "BEFORE (I - V4Lambda)^-1"
139- ! call flush()
195+ ! E: P(w,z,x,y) = sum_nu C(w,z,nu,y) e(nu,x) [maux -> v4]
196+ do y = 1 , ns
197+ call dgemm(' N' ,' N' , nl, ns, ns, 1.0d0 , maux(1 ,(y-1 )* ns+1 ), nl, &
198+ e(1 ,1 ), ns, 0.0d0 , v4(1 ,1 ,1 ,y), nl)
199+ end do
140200
201+ ! F: maux = I - P with BOTH index pairs swapped to the original
202+ ! v42.Lambda convention: rows (z fast, w slow), cols (y fast, x slow).
203+ do y = 1 , ns
204+ do x = 1 , ns
205+ ka = (x-1 )* ns + y
206+ do z = 1 , ns
207+ do w = 1 , ns
208+ maux((w-1 )* ns + z, ka) = - v4(w,z,x,y)
209+ end do
210+ end do
211+ end do
212+ end do
213+ do i = 1 , nl
214+ maux(i,i) = maux(i,i) + 1.0d0
215+ end do
141216
217+ ! Invert ** iden - v4 lamat ** in place (unchanged from v1.5).
142218 call dgetrf ( nl, nl, maux, nl, ipiv, info )
143219 call dgetri ( nl, maux, nl, ipiv, work, nl, info )
144220
145- ! Take product between lamat and the inverted matrix, contracted with v3.
146- !
147- ! REASSOCIATION (numerically equivalent to ~1e-15 relative, NOT bitwise):
148- ! the original code formed the nl x nl product v42 := lamat * maux and then
149- ! cf := v42 * v3^T. We instead evaluate cf = lamat * (maux * v3^T) using an
150- ! nl x ns buffer tmp, which is mathematically identical by associativity
151- ! ( Lambda * M^-1 * v3^T = Lambda * (M^-1 * v3^T) ) and reduces both the flop
152- ! count (O(N^6) -> O(N^5)) and the memory (no nl x nl buffer needed).
153-
154- ! tmp = (I - v4 lamat)^-1 * v3^T (nl x ns)
155- call dgemm(' N' ,' T' ,nl,ns,nl,1.0d0 ,maux,nl,&
156- v32,ns,0.0d0 ,tmp,nl)
157-
158- ! cf = lamat * tmp = lamat (I - v4 lamat)^-1 v3^T (nl x ns)
159- call dgemm(' N' ,' N' ,nl,ns,nl,1.0d0 ,lamat,nl,&
160- tmp,nl,0.0d0 ,cf,nl)
161-
162- ! Now get:
163- ! v3 * ( 1 - lamat*v4)^-1 lamat * v3
221+ ! tmp = (I - v4 lamat)^-1 . v3^T (nl x ns)
222+ call dgemm(' N' ,' T' , nl, ns, nl, 1.0d0 , maux(1 ,1 ), nl, &
223+ v32(1 ,1 ), ns, 0.0d0 , tmp(1 ,1 ), nl)
224+
225+ ! ---------------------------------------------------------------------
226+ ! cf = Lambda . tmp, again without forming Lambda. With tmp's row index
227+ ! ka' = (x'-1)*N + y' (y' fast) read as tmp3(y',x',s):
228+ ! u(mu,nu,s) = sum_{x',y'} e(mu,y') e(nu,x') tmp3(y',x',s)
229+ ! cf3(y,x,s) = sum_{mu,nu} e(mu,y) e(nu,x) D(mu,nu) u(mu,nu,s)
230+ ! All buffers are O(N^3).
231+ ! ---------------------------------------------------------------------
232+
233+ ! C1: b1(mu,x',s) = sum_y' e(mu,y') tmp3(y',x',s)
234+ ! tmp reshaped as (N x N*ns), one dgemm.
235+ call dgemm(' N' ,' N' , ns, nl, ns, 1.0d0 , e(1 ,1 ), ns, &
236+ tmp(1 ,1 ), ns, 0.0d0 , b1(1 ,1 ,1 ), ns)
237+
238+ ! C2: b2(mu,nu,s) = sum_x' b1(mu,x',s) e(nu,x')
239+ do s = 1 , ns
240+ call dgemm(' N' ,' T' , ns, ns, ns, 1.0d0 , b1(1 ,1 ,s), ns, &
241+ e(1 ,1 ), ns, 0.0d0 , b2(1 ,1 ,s), ns)
242+ end do
164243
165- ! print *, "BEFORE V3 Lambda(I - V4Lambda)^-1 V3"
166- ! call flush()
167- call dgemm( ' N ' , ' N ' ,ns,ns,nl, 1.0d0 ,v32,ns,&
168- cf,nl, 0.0d0 ,phi_sc_odd,ns)
244+ ! C3: b2(mu,nu,s) *= D(mu,nu)
245+ do s = 1 , ns
246+ b2(:,:,s) = b2(:,:,s) * dmat(:,:)
247+ end do
169248
249+ ! C4: b1(y,nu,s) = sum_mu e(mu,y) b2(mu,nu,s) (e^T . b2_s)
250+ do s = 1 , ns
251+ call dgemm(' T' ,' N' , ns, ns, ns, 1.0d0 , e(1 ,1 ), ns, &
252+ b2(1 ,1 ,s), ns, 0.0d0 , b1(1 ,1 ,s), ns)
253+ end do
170254
171- ! call get_odd_from_cmat_fu2 (v42, v32, phi_sc_odd)
255+ ! C5: cf3(y,x,s) = sum_nu b1(y,nu,s) e(nu,x); column s of cf viewed as
256+ ! an (N x N) block (y fast, x slow), matching ka = (x-1)*N + y.
257+ do s = 1 , ns
258+ call dgemm(' N' ,' N' , ns, ns, ns, 1.0d0 , b1(1 ,1 ,s), ns, &
259+ e(1 ,1 ), ns, 0.0d0 , cf(1 ,s), ns)
260+ end do
172261
173- ! Deallocate stuff
262+ ! phi_sc_odd = v3 . lamat (I - v4 lamat)^-1 . v3
263+ call dgemm(' N' ,' N' , ns, ns, nl, 1.0d0 , v32(1 ,1 ), ns, &
264+ cf(1 ,1 ), nl, 0.0d0 , phi_sc_odd(1 ,1 ), ns)
174265
175- deallocate (lamat,v32,maux,ipiv,work , cf, tmp )
266+ deallocate (maux, e, dmat, v32, tmp , cf, b1, b2, ipiv, work )
176267
177268end subroutine get_odd_straight_with_v4
0 commit comments