Skip to content

Commit 560b47f

Browse files
Merge pull request #30 from AsymmetryChou/rgf_acc
Refactor RGF kernel for memory optimization and cleanup
2 parents b6663bd + 7c1cc18 commit 560b47f

4 files changed

Lines changed: 229 additions & 32 deletions

File tree

dpnegf/negf/device_property.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def set_leadLR(self, lead_L, lead_R):
135135

136136

137137
def cal_green_function(self, energy, kpoint, eta_device=0., block_tridiagonal=True, Vbias=None,
138-
HS_inmem:bool=True, need_lesser:bool=False, need_greater:bool=False, need_gr_lc:bool=True):
138+
HS_inmem:bool=True, need_lesser:bool=False, need_greater:bool=False, need_gr_lc:bool=False):
139139
''' computes the Green's function for a given energy and k-point in device.
140140
141141
the tags used here to identify different Green's functions follows the NEGF theory
@@ -273,11 +273,16 @@ def cal_green_function(self, energy, kpoint, eta_device=0., block_tridiagonal=Tr
273273
else:
274274
s_in = 0
275275

276+
# gr_left is only consumed inside the lesser/greater forward pass of the
277+
# kernel. If neither is active, the per-block list would sit on the GPU
278+
# unread; ask the kernel to drop it so its slots are freed mid-sweep.
279+
keep_gr_left = bool(need_lesser or need_greater)
276280
ans = recursive_gf(energy, hl=self.hl, hd=self.hd, hu=self.hu,
277281
sd=self.sd, su=self.su, sl=self.sl,
278282
left_se=seL, right_se=seR, seP=None, s_in=s_in,
279283
s_out=None, eta=eta_device, E_ref=self.E_ref,
280-
need_lesser=need_lesser, need_greater=need_greater, need_gr_lc=need_gr_lc)
284+
need_lesser=need_lesser, need_greater=need_greater,
285+
need_gr_lc=need_gr_lc, keep_gr_left=keep_gr_left)
281286
# green shape [[g_trans, grd, grl,...],[g_trans, ...]]
282287

283288
for t in range(len(tags)):
@@ -290,6 +295,16 @@ def cal_green_function(self, energy, kpoint, eta_device=0., block_tridiagonal=Tr
290295

291296
# self.green = update_temp_file(update_fn=fn, file_path=GFpath, ee=ee, tags=tags, info="Computing Green's Function")
292297

298+
def release_greenfuncs(self):
299+
'''Drop the Green's-function dict so the underlying rgf_device storage
300+
can be freed before the next energy chunk. H/S blocks are kept resident
301+
(they are k,V-dependent, not energy-dependent). The runner is
302+
responsible for restoring scalar lead.se references before calling
303+
this, so any batched [B,n,n] copies become collectable too.'''
304+
self.greenfuncs = 0
305+
if isinstance(self.rgf_device, torch.device) and self.rgf_device.type == "cuda":
306+
torch.cuda.empty_cache()
307+
293308
def _cal_current_(self, espacing):
294309
'''calculate the current based on the voltage difference
295310
@@ -312,14 +327,6 @@ def _cal_current_(self, espacing):
312327
xl = min(v_L, v_R)-4*self.kBT
313328
xu = max(v_L, v_R)+4*self.kBT
314329

315-
def fcn(e):
316-
self.cal_green_function()
317-
318-
cc = leggauss(fcn=self._cal_tc_)
319-
320-
int_grid, int_weight = gauss_xw(xl=xl, xu=xu, n=int((xu-xl)/espacing))
321-
322-
323330
self.__CURRENT__ = simpson(y=(self.lead_L.fermi_dirac(self.ee+self.E_ref)
324331
- self.lead_R.fermi_dirac(self.ee+self.E_ref)) * self.tc, x=self.ee)
325332

dpnegf/negf/recursive_green_cal.py

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
def recursive_gf_cal(energy, mat_l_list, mat_d_list, mat_u_list,
55
sd, su, sl, s_in=0, s_out=0, eta=1e-5,
66
need_lesser=False, need_greater=False, need_gr_lc=False,
7-
stacked=False):
7+
stacked=False, keep_gr_left=True):
88
"""The recursive Green's function algorithm is taken from
99
M. P. Anantram, M. S. Lundstrom and D. E. Nikonov, Proceedings of the IEEE, 96, 1511 - 1550 (2008)
1010
DOI: 10.1109/JPROC.2008.927355
@@ -92,10 +92,13 @@ def recursive_gf_cal(energy, mat_l_list, mat_d_list, mat_u_list,
9292
-mat_d[q + 1] - mat_l[q] @ gr_left[q] @ mat_u[q],
9393
eye_bnn,
9494
)
95+
# mat_d is dead after the forward sweep — backward sweep only reads mat_l/mat_u.
96+
del mat_d
9597

9698
grl = [None] * (num_of_matrices - 1)
9799
gru = [None] * (num_of_matrices - 1)
98-
grd = [g.clone() for g in gr_left]
100+
grd = [None] * num_of_matrices
101+
grd[-1] = gr_left[-1].clone()
99102
g_trans = gr_left[-1].clone()
100103
gr_lc = [g_trans] if need_gr_lc else None
101104
for q in range(num_of_matrices - 2, -1, -1):
@@ -106,8 +109,13 @@ def recursive_gf_cal(energy, mat_l_list, mat_d_list, mat_u_list,
106109
g_trans = gU @ g_trans
107110
if need_gr_lc:
108111
gr_lc.append(g_trans)
112+
del gU
109113
if need_gr_lc:
110114
gr_lc.reverse()
115+
if not need_lesser and not need_greater:
116+
# Stacked path: mat_l/mat_u are 4-D and can't be slice-freed mid-loop;
117+
# they are dead now if no lesser/greater pass will read them.
118+
del mat_l, mat_u
111119

112120
gnd = gnl = gnu = gin_left = None
113121
if need_lesser:
@@ -120,7 +128,8 @@ def recursive_gf_cal(energy, mat_l_list, mat_d_list, mat_u_list,
120128

121129
gnl = [None] * (num_of_matrices - 1)
122130
gnu = [None] * (num_of_matrices - 1)
123-
gnd = [g.clone() for g in gin_left]
131+
gnd = [None] * num_of_matrices
132+
gnd[-1] = gin_left[-1].clone()
124133
for q in range(num_of_matrices - 2, -1, -1):
125134
gLmH = mat_l[q] @ gr_left[q].mH # hoisted: used twice
126135
gnl[q] = grd[q + 1] @ mat_l[q] @ gin_left[q] + gnd[q + 1] @ gLmH # (B10)
@@ -140,7 +149,8 @@ def recursive_gf_cal(energy, mat_l_list, mat_d_list, mat_u_list,
140149

141150
gpl = [None] * (num_of_matrices - 1)
142151
gpu = [None] * (num_of_matrices - 1)
143-
gpd = [g.clone() for g in gip_left]
152+
gpd = [None] * num_of_matrices
153+
gpd[-1] = gip_left[-1].clone()
144154
for q in range(num_of_matrices - 2, -1, -1):
145155
lcgc = mat_l[q].conj() @ gr_left[q].conj() # hoisted: used twice
146156
gpl[q] = grd[q + 1] @ mat_l[q] @ gip_left[q] + gpd[q + 1] @ lcgc
@@ -150,6 +160,8 @@ def recursive_gf_cal(energy, mat_l_list, mat_d_list, mat_u_list,
150160
(gru[q] @ mat_l[q] @ gip_left[q])
151161
gpu[q] = gpl[q].mH
152162

163+
if not keep_gr_left:
164+
gr_left = None
153165
return _pack_ans(g_trans, gr_lc, grd, grl, gru, gr_left,
154166
gnd, gnl, gnu, gin_left,
155167
gpd, gpl, gpu, gip_left,
@@ -161,7 +173,9 @@ def recursive_gf_cal(energy, mat_l_list, mat_d_list, mat_u_list,
161173
e_bcast = (energy + 1j * eta).view(-1, 1, 1)
162174

163175
for jj in range(len(mat_d_list)):
164-
mat_d_list[jj] = mat_d_list[jj] - e_bcast * sd[jj]
176+
# In-place: mat_d_list is a fresh tensor (wrapper's `* 1.` copy on D),
177+
# so we can fuse the energy shift without the e_bcast*sd transient.
178+
mat_d_list[jj].addcmul_(sd[jj], e_bcast, value=-1)
165179
for jj in range(len(mat_l_list)):
166180
mat_l_list[jj] = mat_l_list[jj] - e_bcast * sl[jj]
167181
for jj in range(len(mat_u_list)):
@@ -183,18 +197,29 @@ def _batched_eye(n):
183197
# ------------------ retarded Green's function ----------------------
184198
gr_left = [None] * num_of_matrices
185199
gr_left[0] = tLA.solve(-mat_d_list[0], _batched_eye(mat_shapes[0][-1]))
200+
mat_d_list[0] = None # consumed; free immediately
186201

187202
for q in range(num_of_matrices - 1): # (B2)
188203
gr_left[q + 1] = tLA.solve(
189204
-mat_d_list[q + 1] - mat_l_list[q] @ gr_left[q] @ mat_u_list[q],
190205
_batched_eye(mat_shapes[q + 1][-1]),
191206
)
207+
mat_d_list[q + 1] = None # consumed; backward sweep only reads mat_l/mat_u.
192208

193209
grl = [None] * (num_of_matrices - 1)
194210
gru = [None] * (num_of_matrices - 1)
195-
grd = [i.clone() for i in gr_left]
211+
grd = [None] * num_of_matrices
212+
grd[-1] = gr_left[-1].clone()
196213
g_trans = gr_left[-1].clone()
197214
gr_lc = [g_trans] if need_gr_lc else None
215+
# Slots that go dead at the end of iteration q:
216+
# - mat_l_list[q], mat_u_list[q]: only re-read by the lesser/greater branches.
217+
# - gr_left[q]: dead unless the lesser/greater branch will consume it OR
218+
# the caller asked us to keep the list intact.
219+
# Nulling per slot lets the caching allocator coalesce its free list inside the
220+
# loop instead of holding a long fragmented tail until the sweep ends.
221+
drop_lu = not need_lesser and not need_greater
222+
drop_gl = drop_lu and not keep_gr_left
198223
for q in range(num_of_matrices - 2, -1, -1):
199224
gU = gr_left[q] @ mat_u_list[q] # hoisted
200225
grl[q] = grd[q + 1] @ mat_l_list[q] @ gr_left[q] # (B5)
@@ -203,6 +228,12 @@ def _batched_eye(n):
203228
g_trans = gU @ g_trans
204229
if need_gr_lc:
205230
gr_lc.append(g_trans)
231+
del gU
232+
if drop_lu:
233+
mat_l_list[q] = None
234+
mat_u_list[q] = None
235+
if drop_gl:
236+
gr_left[q] = None
206237
if need_gr_lc:
207238
gr_lc.reverse()
208239

@@ -219,7 +250,8 @@ def _batched_eye(n):
219250

220251
gnl = [None] * (num_of_matrices - 1)
221252
gnu = [None] * (num_of_matrices - 1)
222-
gnd = [i.clone() for i in gin_left]
253+
gnd = [None] * num_of_matrices
254+
gnd[-1] = gin_left[-1].clone()
223255

224256
for q in range(num_of_matrices - 2, -1, -1):
225257
gLmH = mat_l_list[q] @ gr_left[q].mH # hoisted
@@ -243,7 +275,8 @@ def _batched_eye(n):
243275

244276
gpl = [None] * (num_of_matrices - 1)
245277
gpu = [None] * (num_of_matrices - 1)
246-
gpd = [i.clone() for i in gip_left]
278+
gpd = [None] * num_of_matrices
279+
gpd[-1] = gip_left[-1].clone()
247280

248281
for q in range(num_of_matrices - 2, -1, -1):
249282
lcgc = mat_l_list[q].conj() @ gr_left[q].conj() # hoisted
@@ -255,6 +288,8 @@ def _batched_eye(n):
255288
(gru[q] @ mat_l_list[q] @ gip_left[q])
256289
gpu[q] = gpl[q].mH
257290

291+
if not keep_gr_left:
292+
gr_left = None
258293
return _pack_ans(g_trans, gr_lc, grd, grl, gru, gr_left,
259294
gnd, gnl, gnu, gin_left,
260295
gpd, gpl, gpu, gip_left,
@@ -287,7 +322,8 @@ def _pack_ans(g_trans, gr_lc, grd, grl, gru, gr_left,
287322

288323

289324
def recursive_gf(energy, hl, hd, hu, sd, su, sl, left_se, right_se, seP=None, E_ref=0.0, s_in=0, s_out=0,
290-
eta=1e-5, need_lesser=False, need_greater=False, need_gr_lc=False):
325+
eta=1e-5, need_lesser=False, need_greater=False, need_gr_lc=False,
326+
keep_gr_left=True):
291327

292328
"""The recursive Green's function algorithm is taken from
293329
M. P. Anantram, M. S. Lundstrom and D. E. Nikonov, Proceedings of the IEEE, 96, 1511 - 1550 (2008)
@@ -364,8 +400,10 @@ def _to_batch(t):
364400
return t
365401

366402
temp_mat_d_list = [_to_batch(hd[i]) * 1. for i in range(len(hd))]
367-
temp_mat_l_list = [_to_batch(hl[i]) * 1. for i in range(len(hl))]
368-
temp_mat_u_list = [_to_batch(hu[i]) * 1. for i in range(len(hu))]
403+
# L and U are only subtracted out-of-place inside the kernel; the expanded
404+
# view is fine, and skipping the copy saves K x B x n^2 per list.
405+
temp_mat_l_list = [_to_batch(hl[i]) for i in range(len(hl))]
406+
temp_mat_u_list = [_to_batch(hu[i]) for i in range(len(hu))]
369407
sd_b = [_to_batch(sd[i]) for i in range(len(sd))]
370408
sl_b = [_to_batch(sl[i]) for i in range(len(sl))]
371409
su_b = [_to_batch(su[i]) for i in range(len(su))]
@@ -421,15 +459,17 @@ def _to_batch(t):
421459
need_lesser=need_lesser,
422460
need_greater=need_greater,
423461
need_gr_lc=need_gr_lc,
424-
stacked=True)
462+
stacked=True,
463+
keep_gr_left=keep_gr_left)
425464
else:
426465
ans = recursive_gf_cal(shift_energy, temp_mat_l_list, temp_mat_d_list, temp_mat_u_list,
427466
sd_b, su_b, sl_b,
428467
s_in=s_in_b, s_out=s_out_b, eta=eta,
429468
need_lesser=need_lesser,
430469
need_greater=need_greater,
431470
need_gr_lc=need_gr_lc,
432-
stacked=False)
471+
stacked=False,
472+
keep_gr_left=keep_gr_left)
433473

434474
if squeezed:
435475
ans = _squeeze_ans(ans)

dpnegf/runner/NEGF.py

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,20 @@ def __init__(self,
6969
self.rgf_device = rgf_device
7070
self.n_cpus = n_cpus
7171
self.e_batch_size = e_batch_size
72+
73+
# The RGF q-loop allocates/frees many small slabs; with the default
74+
# cudaMalloc-backed caching allocator this fragments quickly on long
75+
# energy grids. expandable_segments avoids that, but must be set before
76+
# torch initializes its CUDA context — by the time we get here it's
77+
# already live, so we can only nudge the user.
78+
if isinstance(self.rgf_device, torch.device) and self.rgf_device.type == "cuda":
79+
if "expandable_segments" not in os.environ.get("PYTORCH_CUDA_ALLOC_CONF", ""):
80+
log.warning(
81+
"RGF on CUDA can fragment the caching allocator on long energy "
82+
"grids. Consider exporting "
83+
"PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True BEFORE invoking "
84+
"dpnegf (must be set before torch's CUDA context initializes)."
85+
)
7286

7387
# get the parameters
7488
self.ele_T = ele_T
@@ -585,6 +599,37 @@ def prepare_self_energy(self, scf_require: bool) -> None:
585599

586600

587601

602+
def _auto_chunk_size(self, n_grid):
603+
"""Pick a chunk size from free CUDA memory when the user didn't set
604+
``e_batch_size``. Returns the full grid length on CPU / when the
605+
device geometry isn't probable yet.
606+
607+
Per-energy peak (post per-slot-release, complex128) approximated as
608+
bytes_per_E ~= C * K * n_max**2 * 16
609+
with C bundling the live tensors in the worst backward-sweep slot
610+
(grd full + grl + gru full + decaying gr_left tail + gU + transients).
611+
C=10 with a 0.7x free-memory budget; deliberately conservative because
612+
without expandable_segments the allocator can't defragment on demand.
613+
"""
614+
rgf_dev = self.rgf_device
615+
if not (isinstance(rgf_dev, torch.device) and rgf_dev.type == "cuda"):
616+
return n_grid
617+
try:
618+
free_bytes, _total = torch.cuda.mem_get_info(rgf_dev)
619+
n_max = max(int(b.shape[-1]) for b in self.deviceprop.hd)
620+
K = len(self.deviceprop.hd)
621+
except Exception:
622+
return n_grid
623+
per_e = 10 * K * (n_max ** 2) * 16
624+
if per_e <= 0:
625+
return n_grid
626+
b = max(1, min(n_grid, int(0.7 * free_bytes) // per_e))
627+
log.info(
628+
f"auto e_batch_size={b} (free={free_bytes/2**30:.2f} GiB, "
629+
f"per_E~={per_e/2**20:.1f} MiB, K={K}, n_max={n_max})"
630+
)
631+
return b
632+
588633
def negf_compute(self,scf_require=False,Vbias=None):
589634

590635
assert scf_require is not None, "scf_require should be set to True or False"
@@ -704,7 +749,10 @@ def negf_compute(self,scf_require=False,Vbias=None):
704749
self.out.setdefault('LDOS', {}).setdefault(str(k), []).append(self.compute_LDOS(k))
705750
else:
706751
# Non-SCF: solve a whole chunk of energies in one batched recursive_gf call.
707-
chunk = self.e_batch_size if self.e_batch_size is not None else len(self.uni_grid)
752+
if self.e_batch_size is not None:
753+
chunk = self.e_batch_size
754+
else:
755+
chunk = self._auto_chunk_size(len(self.uni_grid))
708756
for e_chunk in torch.split(self.uni_grid, chunk):
709757
e_batch_size = len(e_chunk)
710758
log.info(
@@ -741,19 +789,31 @@ def negf_compute(self,scf_require=False,Vbias=None):
741789
)
742790

743791
if self.out_dos:
744-
self.out.setdefault('DOS', {}).setdefault(str(k), []).append(self.compute_DOS(k).reshape(-1))
792+
self.out.setdefault('DOS', {}).setdefault(str(k), []).append(self.compute_DOS(k).reshape(-1).cpu())
745793
if self.out_tc or self.out_current_nscf:
746-
self.out.setdefault('T_k', {}).setdefault(str(k), []).append(self.compute_TC(k).reshape(-1))
794+
self.out.setdefault('T_k', {}).setdefault(str(k), []).append(self.compute_TC(k).reshape(-1).cpu())
747795
if self.out_ldos:
748796
ldos_chunk = self.compute_LDOS(k)
749797
if ldos_chunk.ndim == 1: # scalar-E chunk → [na]
750798
ldos_chunk = ldos_chunk.unsqueeze(0)
751-
self.out.setdefault('LDOS', {}).setdefault(str(k), []).append(ldos_chunk)
752-
753-
# Restore lead.se to scalar [n,n] so downstream scalar callers
754-
# (density modules, lcurrent loop, future SCF re-entry) see the expected shape.
755-
self.deviceprop.lead_L.se = seL_list[-1]
756-
self.deviceprop.lead_R.se = seR_list[-1]
799+
self.out.setdefault('LDOS', {}).setdefault(str(k), []).append(ldos_chunk.cpu())
800+
801+
# Restore lead.se to a scalar [n,n] before releasing the GF
802+
# dict. For B>1 we clone the last per-E tensor so the new
803+
# lead.se doesn't share storage with anything still
804+
# referenced through seL_list/seR_list, then drop both
805+
# lists so release_greenfuncs's empty_cache() has the per-E
806+
# and stacked [B,n,n] copies to release.
807+
if e_batch_size > 1:
808+
self.deviceprop.lead_L.se = seL_list[-1].detach().clone()
809+
self.deviceprop.lead_R.se = seR_list[-1].detach().clone()
810+
else:
811+
# B=1 path: lead.se already IS the per-E [n,n] tensor;
812+
# preserve byte-identical behavior for the scalar case.
813+
self.deviceprop.lead_L.se = seL_list[-1]
814+
self.deviceprop.lead_R.se = seR_list[-1]
815+
del seL_list, seR_list
816+
self.deviceprop.release_greenfuncs()
757817

758818
# over energy loop in uni_gird
759819
# The following code is for output properties before NEGF ends

0 commit comments

Comments
 (0)