Skip to content

Commit 5009bf7

Browse files
Merge pull request #36 from AsymmetryChou/rgf_acc
Add BLAS thread control and update docstring
2 parents b755ae4 + c3b86c5 commit 5009bf7

1 file changed

Lines changed: 185 additions & 22 deletions

File tree

dpnegf/negf/lead_property.py

Lines changed: 185 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import h5py
1313
import glob
1414
import psutil
15+
import time
1516

1617

1718
log = logging.getLogger(__name__)
@@ -522,7 +523,8 @@ def _estimate_worker_memory(lead_L, lead_R, kpoint=None, temp_allocation_factor=
522523
return total_estimate
523524

524525

525-
def _get_safe_n_jobs(lead_L, lead_R, requested_n_jobs=-1, max_memory_fraction=0.9, min_workers=1, kpoint=None, n_cpus=None):
526+
def _get_safe_n_jobs(lead_L, lead_R, requested_n_jobs=-1, max_memory_fraction=0.9,
527+
min_workers=1, kpoint=None, n_cpus=None):
526528
"""
527529
Calculate safe number of parallel workers based on available system memory.
528530
@@ -538,6 +540,8 @@ def _get_safe_n_jobs(lead_L, lead_R, requested_n_jobs=-1, max_memory_fraction=0.
538540
Minimum number of workers to use. Default 1.
539541
kpoint : array-like, optional
540542
A sample k-point for fetching Hamiltonian matrices to estimate memory.
543+
n_cpus : int or None
544+
Number of CPU cores to use for memory estimation. If None, uses os.cpu_count().
541545
542546
Returns
543547
-------
@@ -593,12 +597,144 @@ def _get_safe_n_jobs(lead_L, lead_R, requested_n_jobs=-1, max_memory_fraction=0.
593597

594598
log.info(f"Estimated safe n_jobs={safe_n_worker} based on available memory.")
595599
return safe_n_worker
596-
600+
601+
602+
def _autotune_blas_threads(leadL_pack, sample_kpoint, sample_energy, eta_lead,
603+
n_jobs, cpu_count, se_numba_jit, requested=None):
604+
"""Pick per-worker BLAS threads by timing the real surface-green code path.
605+
606+
The Lopez-Sancho loop in ``surface_green._surface_green_{numba,scipy}_core``
607+
(surface_green.py) issues repeated complex128 ``solve`` on shape ``(N, 2N)``
608+
and several ``(N, N)`` matmuls per iteration. The payoff from BLAS threading
609+
is hardware-dependent (OpenBLAS vs MKL, cache size, N), so instead of a
610+
static table we probe the actual `_compute_self_energy_from_pack` at a few
611+
candidate thread counts and keep the fastest. Runs once per
612+
``compute_all_self_energy`` call, in the parent process, before workers fan
613+
out. Cost is bounded to ``2 * len(candidates)`` surface-green calls at the
614+
sample ``(k, E)``.
615+
616+
Parameters
617+
----------
618+
leadL_pack : dict
619+
Precomputed lead pack (see `_precompute_lead_kdata`). Only the left
620+
lead is timed; the right lead has the same principal-layer size in
621+
normal use, so measuring one halves the probe cost.
622+
sample_kpoint, sample_energy : array-like, float
623+
The (k, E) pair used for the probe. Take from the grid actually being
624+
computed so N and dtype match production.
625+
eta_lead : float
626+
Same broadening as production.
627+
n_jobs : int
628+
Number of joblib workers about to be launched; bounds per-worker CPU.
629+
cpu_count : int
630+
Total CPU budget.
631+
se_numba_jit : bool or None
632+
Same flag workers will see. The first call absorbs numba JIT compile;
633+
the timed second call measures steady-state.
634+
requested : int or None
635+
User override. When a positive int, skip the bench and clamp to the
636+
per-worker budget.
637+
638+
Returns
639+
-------
640+
int
641+
BLAS threads to give each loky worker via ``threadpool_limits``.
642+
"""
643+
n_jobs = max(int(n_jobs), 1)
644+
per_worker_budget = max(int(cpu_count) // n_jobs, 1)
645+
646+
if requested is not None:
647+
if not isinstance(requested, int) or requested < 1:
648+
log.warning(f"Requested blas_threads={requested} is not a positive int. "
649+
"Falling back to autotune.")
650+
else:
651+
if requested > per_worker_budget:
652+
log.warning(f"Requested blas_threads={requested} exceeds per-worker "
653+
f"CPU budget ({per_worker_budget} = cpu_count // n_jobs). "
654+
f"Clamping to {per_worker_budget}.")
655+
threads = min(requested, per_worker_budget)
656+
log.info(f"BLAS threads per worker: {threads} (user-requested).")
657+
return threads
658+
659+
if per_worker_budget == 1:
660+
log.info("BLAS threads per worker: 1 (no CPU budget left for BLAS threading).")
661+
return 1
662+
663+
if sample_kpoint is None or not leadL_pack.get("kdata"):
664+
log.info("BLAS threads per worker: 1 (empty sample; skipping autotune).")
665+
return 1
666+
667+
sample_dim = _sample_principal_layer_dim(leadL_pack, sample_kpoint)
668+
if sample_dim < 64:
669+
log.info(f"BLAS threads per worker: 1 (N={sample_dim} < 64; skipping autotune).")
670+
return 1
671+
672+
candidates = sorted({1, 2, 4, 8, per_worker_budget})
673+
candidates = [c for c in candidates if c <= per_worker_budget]
674+
675+
# Wall-clock cap for the whole probe. stop between
676+
# candidates (not mid-call) and pick the best-so-far.
677+
max_autotune_seconds = 90.0
678+
bench_start = time.perf_counter()
679+
680+
timings = {}
681+
for t in candidates:
682+
if time.perf_counter() - bench_start > max_autotune_seconds:
683+
log.warning(f"BLAS autotune exceeded {max_autotune_seconds:.0f}s wall-clock cap; "
684+
f"stopping after {len(timings)}/{len(candidates)} candidates.")
685+
break
686+
try:
687+
with threadpool_limits(limits=t, user_api='blas'):
688+
_compute_self_energy_from_pack(
689+
leadL_pack, sample_kpoint, sample_energy,
690+
eta_lead, se_numba_jit=se_numba_jit,
691+
)
692+
start = time.perf_counter()
693+
_compute_self_energy_from_pack(
694+
leadL_pack, sample_kpoint, sample_energy,
695+
eta_lead, se_numba_jit=se_numba_jit,
696+
)
697+
elapsed = time.perf_counter() - start
698+
except Exception as e:
699+
log.warning(f"BLAS autotune t={t} failed ({e!r}); skipping candidate.")
700+
continue
701+
timings[t] = elapsed
702+
log.info(f"BLAS autotune t={t}: {elapsed * 1e3:.1f} ms")
703+
704+
if not timings:
705+
log.warning("BLAS autotune found no working candidate; falling back to 1.")
706+
return 1
707+
708+
best = min(timings, key=timings.get)
709+
log.info(f"BLAS threads per worker: {best} "
710+
f"(autotuned, N={sample_dim}, n_jobs={n_jobs}, cpu_count={cpu_count}).")
711+
return best
712+
713+
714+
def _sample_principal_layer_dim(pack, sample_kpoint):
715+
"""Return the principal-layer Hamiltonian dimension N from a precomputed
716+
lead pack, handling both non-Bloch and Bloch branches. Returns 0 if the
717+
sample entry cannot be located, letting callers fall back to a safe
718+
default."""
719+
if sample_kpoint is None:
720+
return 0
721+
try:
722+
entry = pack["kdata"][_k_key(sample_kpoint)]
723+
except (KeyError, TypeError):
724+
return 0
725+
if "HLk" in entry:
726+
HLk = entry["HLk"]
727+
else:
728+
bloch_entries = entry.get("bloch_entries") or []
729+
if not bloch_entries:
730+
return 0
731+
HLk = bloch_entries[0]["HLk"]
732+
return int(HLk.shape[0])
597733

598734

599735
def compute_all_self_energy(eta, lead_L, lead_R, kpoints_grid, energy_grid,
600-
self_energy_save_path=None, n_jobs=-1, batch_size=200,
601-
n_cpus=None, se_numba_jit=None):
736+
self_energy_save_path=None, ek_batch_size=200,
737+
n_cpus=None, n_jobs=-1, se_numba_jit=None, blas_threads=None):
602738
"""
603739
Computes and saves self-energy matrices for all combinations of k-points and energy values
604740
for left and right leads.
@@ -620,15 +756,20 @@ def compute_all_self_energy(eta, lead_L, lead_R, kpoints_grid, energy_grid,
620756
List or array of energy values to compute self-energy for.
621757
self_energy_save_path : str or None, optional
622758
Directory to save self-energy files. If None, uses lead_L's results_path.
623-
n_jobs : int, optional
624-
Number of parallel jobs to use. Default is -1 (use all available CPUs).
625-
batch_size : int, optional
759+
ek_batch_size : int, optional
626760
Number of (k, e) tasks per parallel batch. Default is 200.
627761
n_cpus : int or None, optional
628762
Number of CPU cores to use for memory estimation. If None, uses os.cpu_count().
763+
n_jobs : int, optional
764+
Number of parallel jobs to use. Default is -1 (use all available CPUs).
629765
se_numba_jit : bool or None, optional
630766
Boolean flag controlling whether to use the Numba-accelerated surface Green's function core.
631767
If None, Numba will be used when available. Default is None.
768+
blas_threads : int or None, optional
769+
BLAS threads to give each worker. None (default) autotunes by timing
770+
`_compute_self_energy_from_pack` across a few candidate thread counts and picking the fastest.
771+
Pass an int to force that value; it is still clamped to `cpu_count // n_jobs` to avoid
772+
oversubscription.
632773
633774
Returns
634775
-------
@@ -644,7 +785,10 @@ def compute_all_self_energy(eta, lead_L, lead_R, kpoints_grid, energy_grid,
644785
# Calculate safe number of workers based on available memory
645786
# Use first k-point for memory estimation
646787
sample_kpoint = kpoints_grid[0] if len(kpoints_grid) > 0 else None
647-
safe_n_jobs = _get_safe_n_jobs(lead_L, lead_R, requested_n_jobs=n_jobs, kpoint=sample_kpoint, n_cpus=n_cpus)
788+
safe_n_jobs = _get_safe_n_jobs(lead_L, lead_R,
789+
requested_n_jobs=n_jobs,
790+
kpoint=sample_kpoint,
791+
n_cpus=n_cpus)
648792
if n_jobs == -1:
649793
log.info(f"Auto-detected safe n_jobs={safe_n_jobs} based on available memory")
650794
elif safe_n_jobs < n_jobs:
@@ -656,22 +800,34 @@ def compute_all_self_energy(eta, lead_L, lead_R, kpoints_grid, energy_grid,
656800
leadL_pack = _precompute_lead_kdata(lead_L, kpoints_grid)
657801
leadR_pack = _precompute_lead_kdata(lead_R, kpoints_grid)
658802

803+
# Choose BLAS threads-per-worker by autotuning on the real code path at the
804+
# sample (k, E). Cheaper than a hardware-agnostic table and always correct
805+
# for the actual N / BLAS backend the workers will run under.
806+
cpu_budget = n_cpus if n_cpus is not None else os.cpu_count()
807+
sample_energy = energy_grid[0] if len(energy_grid) > 0 else 0.0
808+
blas_threads_per_worker = _autotune_blas_threads(
809+
leadL_pack, sample_kpoint, sample_energy, eta,
810+
safe_n_jobs, cpu_budget, se_numba_jit, requested=blas_threads,
811+
)
812+
659813
total_tasks = [(k, e) for k in kpoints_grid for e in energy_grid]
660814
# Capture the parent's log level so loky workers (which start with a clean
661815
# logging state and the WARNING default) can match it when they reinit.
662816
parent_log_level = logging.getLogger().getEffectiveLevel()
663-
if len(total_tasks) <= batch_size:
817+
if len(total_tasks) <= ek_batch_size:
664818
Parallel(n_jobs=safe_n_jobs, backend="loky")(
665-
delayed(_self_energy_worker_blas1)(k, e, eta, leadL_pack, leadR_pack,
666-
self_energy_save_path, se_numba_jit, parent_log_level)
819+
delayed(_self_energy_worker_blas)(k, e, eta, leadL_pack, leadR_pack,
820+
self_energy_save_path, se_numba_jit,
821+
parent_log_level, blas_threads_per_worker)
667822
for k, e in total_tasks
668823
)
669824
else:
670-
for i in range(0, len(total_tasks), batch_size):
671-
batch = total_tasks[i:i+batch_size]
825+
for i in range(0, len(total_tasks), ek_batch_size):
826+
batch = total_tasks[i:i+ek_batch_size]
672827
Parallel(n_jobs=safe_n_jobs, backend="loky")(
673-
delayed(_self_energy_worker_blas1)(k, e, eta, leadL_pack, leadR_pack,
674-
self_energy_save_path, se_numba_jit, parent_log_level)
828+
delayed(_self_energy_worker_blas)(k, e, eta, leadL_pack, leadR_pack,
829+
self_energy_save_path, se_numba_jit,
830+
parent_log_level, blas_threads_per_worker)
675831
for k, e in batch
676832
)
677833

@@ -875,18 +1031,25 @@ def _self_energy_worker_pure(k, e, eta, leadL_pack, leadR_pack, self_energy_save
8751031
write_to_hdf5(save_tmp_R, k, e, seR)
8761032

8771033

878-
def _self_energy_worker_blas1(k, e, eta, leadL_pack, leadR_pack, self_energy_save_path, se_numba_jit, log_level):
879-
"""Loky entry point that pins this worker's BLAS/LAPACK runtime to a
880-
single thread, then delegates to `_self_energy_worker_pure`.
1034+
def _self_energy_worker_blas(k, e, eta, leadL_pack, leadR_pack, self_energy_save_path,
1035+
se_numba_jit, log_level, blas_threads=1):
1036+
"""Loky entry point that pins this worker's BLAS/LAPACK runtime to a bounded
1037+
thread count, then delegates to `_self_energy_worker_pure`.
8811038
8821039
Each loky worker is a separate process whose BLAS library would otherwise
8831040
autodetect every physical core, leading to N_workers * N_cores threads
8841041
contending for N_cores cores. The Lopez-Sancho iteration in
885-
`surface_green._surface_green_{numba,scipy}_core` issues many small
886-
`solve` / `inv` / matmul calls where single-threaded BLAS already wins
887-
per-call; outer joblib-level parallelism handles scaling.
1042+
`surface_green._surface_green_{numba,scipy}_core` issues repeated
1043+
`solve` / `inv` / matmul calls whose payoff from BLAS threading depends on
1044+
the principal-layer dimension N: single-threaded already wins for small N,
1045+
but multi-threaded solve/GEMM helps for larger N when the CPU budget left
1046+
over from the memory-driven `n_jobs` cap is not zero.
1047+
1048+
`blas_threads` is chosen by `_autotune_blas_threads` in the parent and
1049+
passed in per call; default 1 preserves the historical behavior for any
1050+
external caller.
8881051
"""
889-
with threadpool_limits(limits=1, user_api='blas'):
1052+
with threadpool_limits(limits=blas_threads, user_api='blas'):
8901053
return _self_energy_worker_pure(
8911054
k, e, eta, leadL_pack, leadR_pack,
8921055
self_energy_save_path, se_numba_jit, log_level,

0 commit comments

Comments
 (0)