1212import h5py
1313import glob
1414import psutil
15+ import time
1516
1617
1718log = 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
599735def 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