如何加速含8层嵌套循环的_get_result数值计算函数?
Looking at your code, the biggest bottleneck isn't actually the 4x4 permanent calculation—it's the O(L⁸) nested loops that make scaling to L≈64 impossible with your current approach. Let's break down actionable optimizations to cut redundant work, parallelize, and leverage hardware acceleration:
1. Extract Loop-Invariant Calculations (Critical First Step)
Right now, you're recalculating values like H_abef, f_exp_H_abef_t, inv, one_over_det, matrix, and phase1 every single time inside the c/d/g/h loops. These only depend on a, b, e, f—moving them outside the inner 4 loops will immediately reduce your computation by a factor of L⁴. That's a massive win for any L, let alone 64!
Here's the restructured code:
import numpy as np from numba import njit, prange dtype_complex = np.complex64 @njit def _perm(matrix): n = matrix.shape[0] d = np.ones(n, dtype=dtype_complex) s = 1 f = np.arange(n, dtype=np.uint32) v = matrix.sum(axis=0) # Manual product for small arrays (faster than np.prod in Numba) p = v[0] for i in range(1, n): p *= v[i] j = 0 while j < n - 1: v -= 2 * d[j] * matrix[j] d[j] = -d[j] s = -s prod = v[0] for i in range(1, n): prod *= v[i] p += s * prod f[0] = 0 f[j] = f[j+1] f[j+1] = j + 1 j = f[0] return p / (2 ** (n - 1)) @njit def _get_elements(one_over_det, matrix, lefts, rights): length = len(lefts) to_perm = np.zeros((length, length), dtype=dtype_complex) for idx1, lbit_idx1 in enumerate(lefts): for idx2, lbit_idx2 in enumerate(rights): to_perm[idx1, idx2] = one_over_det * matrix[lbit_idx2, lbit_idx1] return _perm(to_perm) @njit(parallel=True) def _get_result(t, L, B, ener, evec, f_mat, I, site1, site2): result = 0.0 + 0.0j # Precompute evec slices to avoid repeated indexing overhead evec_s2 = evec[site2] evec_s1 = evec[site1] # Parallelize outer 4 loops (completely independent iterations) for a in prange(L): s2_a = evec_s2[a] for b in np.arange(L): s2_b = evec_s2[b] for e in np.arange(L): s2_e = evec_s2[e] for f in np.arange(L): s2_f = evec_s2[f] # Calculate once per a,b,e,f (no more redundant work!) H_abef = 2 * (B[:,a] - B[:,b] + B[:,e] - B[:,f]) exp_diag = np.exp(1j * H_abef * t).astype(dtype_complex) f_exp_H_abef_t = f_mat @ np.diag(exp_diag) to_inv = I + f_mat - f_exp_H_abef_t inv = np.linalg.inv(to_inv) one_over_det = 1.0 / np.linalg.det(to_inv) matrix = f_exp_H_abef_t @ inv phase1 = np.exp(1j * ( ener[a] - ener[b] + ener[e] - ener[f] - B[a,a] - B[b,b] - B[e,e] - B[f,f] + 2 * ( B[a,b] + B[e,f] + B[f,a] + B[f,b] - B[e,a] - B[e,b] )) * t) # Inner loops using precomputed values for c in np.arange(L): s1_c = evec_s1[c] for d in np.arange(L): s1_d = evec_s1[d] phase2 = np.exp(2j * ( B[f,c] + B[f,d] - B[e,c] - B[e,d] ) * t) for g in np.arange(L): s1_g = evec_s1[g] for h in np.arange(L): s1_h = evec_s1[h] elements = _get_elements(one_over_det, matrix, [a, c, e, g], [b, d, f, h]) # 6th order terms if b == c: elements += _get_elements(one_over_det, matrix, [a, e, g], [d, f, h]) if d == e: elements += _get_elements(one_over_det, matrix, [a, c, g], [b, f, h]) if b == e: elements += _get_elements(one_over_det, matrix, [a, c, g], [d, f, h]) if f == g: elements += _get_elements(one_over_det, matrix, [a, c, e], [b, d, h]) if d == g: elements += _get_elements(one_over_det, matrix, [a, c, e], [b, f, h]) if b == g: elements += _get_elements(one_over_det, matrix, [a, c, e], [d, f, h]) # quartic terms if b == e and f == g: elements += _get_elements(one_over_det, matrix, [a, c], [d, h]) if b == e and d == g: elements += _get_elements(one_over_det, matrix, [a, c], [f, h]) if b == c and f == g: elements += _get_elements(one_over_det, matrix, [a, e], [d, h]) if b == c and d == g: elements += _get_elements(one_over_det, matrix, [a, c], [f, h]) if d == e and f == g: elements += _get_elements(one_over_det, matrix, [a, c], [b, h]) if b == c and d == e: elements += _get_elements(one_over_det, matrix, [a, g], [f, h]) if d == e and b == g: elements += _get_elements(one_over_det, matrix, [a, c], [f, h]) # quadratic term if b == c and d == e and f == g: elements += one_over_det * matrix[h, a] unitary = s2_a * s2_b * s1_c * s1_d * s2_e * s2_f * s1_g * s1_h result += unitary * phase1 * phase2 * elements return result
2. Parallelize Outer Loops with Numba Prange
The a, b, e, f loops have no shared state between iterations, so we can use Numba's prange to parallelize them across CPU cores. The parallel=True decorator tells Numba to handle thread-safe accumulation of the result scalar.
3. GPU Acceleration for Matrix Operations
Matrix inversions, multiplications, and diagonal exponentiation are GPU-friendly tasks. For L≈64, these operations will be orders of magnitude faster on a GPU than a CPU. Here's how to adapt the code using Numba CUDA:
from numba import cuda # Move data to GPU memory B_dev = cuda.to_device(B) ener_dev = cuda.to_device(ener) evec_dev = cuda.to_device(evec) f_mat_dev = cuda.to_device(f_mat) I_dev = cuda.to_device(I) @cuda.jit def _get_result_cuda(t, L, B, ener, evec, f_mat, I, site1, site2, result_out): # Map thread indices to a,b,e,f a, b, e, f = cuda.grid(4) if a >= L or b >= L or e >= L or f >= L: return # GPU-based matrix operations H_abef = 2 * (B[:,a] - B[:,b] + B[:,e] - B[:,f]) exp_diag = np.exp(1j * H_abef * t).astype(dtype_complex) f_exp_H_abef_t = f_mat @ np.diag(exp_diag) to_inv = I + f_mat - f_exp_H_abef_t inv = cuda.linalg.inv(to_inv) one_over_det = 1.0 / cuda.linalg.det(to_inv) matrix = f_exp_H_abef_t @ inv phase1 = np.exp(1j * ( ener[a] - ener[b] + ener[e] - ener[f] - B[a,a] - B[b,b] - B[e,e] - B[f,f] + 2 * ( B[a,b] + B[e,f] + B[f,a] + B[f,b] - B[e,a] - B[e,b] )) * t) evec_s2 = evec[site2] evec_s1 = evec[site1] s2_a, s2_b, s2_e, s2_f = evec_s2[a], evec_s2[b], evec_s2[e], evec_s2[f] # Inner loops (you can further parallelize these with shared memory if needed) for c in range(L): s1_c = evec_s1[c] for d in range(L): s1_d = evec_s1[d] phase2 = np.exp(2j * ( B[f,c] + B[f,d] - B[e,c] - B[e,d] ) * t) for g in range(L): s1_g = evec_s1[g] for h in range(L): s1_h = evec_s1[h] # Reuse _get_elements logic (port to CUDA if needed) elements = _get_elements(one_over_det, matrix, [a, c, e, g], [b, d, f, h]) # ... rest of the elements conditionals unitary = s2_a * s2_b * s1_c * s1_d * s2_e * s2_f * s1_g * s1_h # Atomic add to avoid race conditions cuda.atomic.add(result_out, 0, unitary * phase1 * phase2 * elements) # Launch the kernel threads_per_block = (4,4,4,4) blocks_per_grid = ( (L + threads_per_block[0]-1)//threads_per_block[0], (L + threads_per_block[1]-1)//threads_per_block[1], (L + threads_per_block[2]-1)//threads_per_block[2], (L + threads_per_block[3]-1)//threads_per_block[3] ) result_dev = cuda.device_array(1, dtype=dtype_complex) _get_result_cuda[blocks_per_grid, threads_per_block](t, L, B_dev, ener_dev, evec_dev, f_mat_dev, I_dev, site1, site2, result_dev) # Copy result back to CPU final_result = result_dev.copy_to_host()[0]
4. Trim Inner Loop Iterations with Symmetry
Look for ways to reduce unnecessary inner loop runs:
- Skip iterations where
unitaryis zero (if evec has sparse entries) - Reuse calculations for symmetric index pairs (e.g., swap c and d if the problem is symmetric, compute once and double the result)
- Precompute valid index combinations for conditional cases (like
b==c) instead of checking every iteration
Final Notes
Even with these optimizations, L=64 means 16 million outer loop iterations—combining GPU acceleration with parallel CPU loops is non-negotiable. Test with L=8/16 first to validate each optimization step, then scale up.
内容的提问来源于stack exchange,提问作者purestate

