perf(solvers): route eager Davidson RR GEMMs through OpenBLAS zgemm (measured 1.05x serial / 1.02x k-parallel) - #470
Closed
wladerer wants to merge 3 commits into
Closed
perf(solvers): route eager Davidson RR GEMMs through OpenBLAS zgemm (measured 1.05x serial / 1.02x k-parallel)#470wladerer wants to merge 3 commits into
wladerer wants to merge 3 commits into
Conversation
torch's CPU complex128 matmul is measured 2.6-5x slower than a plain
OpenBLAS zgemm on the batched Rayleigh-Ritz shapes the eager block
Davidson runs (asus: 210 vs 718 us @ batch 8 x 754x754; 1237 vs 6354 us
@ 27x1260x1260). Route those RR contractions — the subspace build S, the
Ritz combines X/HX, and the two-pass projection in _orthonormalize_b —
straight through cblas_zgemm via a tiny ctypes-loaded native symbol.
New blas_zbmm C symbol compiled into the SAME libdavnative.so as the
native Davidson solver (one .so, two symbols; build script extended).
blas_routing.zbmm dispatches to it when the .so is loadable and operands
are contiguous complex128 CPU tensors not tracking gradients, else falls
back to torch.matmul. Gated by GRADWAVE_BLAS_GEMM in {auto(default),on,off}:
on requires the .so, auto falls back silently (identical-math micro-routing),
off is pure torch. FFTs are already at torch/FFTW parity and are untouched.
Same math, different kernel: the eigensolve runs under no_grad (autograd
never sees it; zbmm additionally refuses grad-tracked operands), and the
routed result equals torch's to last-bit rounding.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018j6BBnPtBSjkrnASSc52mT
The k-parallel thread pool pins torch to 1 inside its tasks; without pinning OpenBLAS the same way, 8 concurrent zbmm calls would each spawn the full OpenBLAS pool (workers x cores oversubscription). Pass torch.get_num_threads() into the C call and set OpenBLAS threads to it, so BLAS threading follows torch's setting in both the serial and the k-parallel regimes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018j6BBnPtBSjkrnASSc52mT
The first measured A/B run's engagement counter caught the native path never serving the big GEMMs: _orthonormalize_b returns q.transpose(-1,-2) (a transpose VIEW) and the Ritz rotation is a u[:, :, :nb] slice, so the strict is_contiguous gate silently rejected the real Davidson operands and every arm ran torch. GEMM's lda/stride arguments express both layouts exactly: last-dim slices pass lda = stride(-2) (rows with a gap), transpose views pass the storage with the trans flag flipped (NoTrans <-> Trans). ConjTrans on transposed storage would need the nonstandard conj-no-trans op and still falls back (one small GEMM per solve). Adds native/fallback dispatch counters so a bench can PROVE engagement instead of trusting the wall clock. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018j6BBnPtBSjkrnASSc52mT
Owner
Author
|
Parked by measured verdict: exact and well-tested, but 1.047× serial / 1.021× k-parallel e2e (Al-4, asus, engagement-verified) doesn't justify a new C surface + routing layer — the per-op 2.6-5× OpenBLAS zgemm advantage Amdahl-shrinks to ~5% because the RR GEMMs are a thin slice of eager wall, and the main path now runs davidson-native (7.5s Al-4). Branch kept for the record; reopen only if the eager fallback scopes (USPP/mGGA/Fock) become a measured bottleneck at larger nb. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Routes the eager
davidson_batchedRayleigh-Ritz GEMMs — the subspace buildS = V·HV^H, the Ritz combinesX = U^T·V/HX = U^T·HV, and the two-pass projection in_orthonormalize_b— through OpenBLAScblas_zgemmvia a tiny ctypes-loaded native symbol, because torch's CPU complex128 matmul is measured 2.6-5x slower per-op than OpenBLAS zgemm on exactly these batched shapes (asus: 210 vs 718 µs @ batch 8 × 754×754; 1237 vs 6354 µs @ 27×1260×1260). FFTs are already at torch/FFTW parity and are untouched.src/gradwave/solvers/native/blas_gemm.c— oneblas_zbmmsymbol (strided batch loop overcblas_zgemm), compiled into the samelibdavnative.soas the native Davidson solver (one .so, two symbols;scripts/build_native_solver.shextended, CI builds it via the same script).src/gradwave/solvers/blas_routing.py—zbmm(a, b, *, conj_b_t, t_a): native when the .so loads and operands are complex128 CPU batched tensors in a GEMM-expressible layout; silent torch fallback otherwise (identical-math micro-routing, unlike davidson-native's hard error), with native/fallback dispatch counters so a bench can prove engagement.GRADWAVE_BLAS_GEMM∈ {auto(default),on,off} — read per call (in-process A/B safe, no import-frozen env);onerrors if the .so is missing,offis pure torch._orthonormalize_breturnsq.transpose(-1,-2)) and au[:, :, :nb]slice — a strictis_contiguous()gate silently rejects them all. GEMM'slda/stride arguments express both (slices vialda = stride(-2), transpose views via a trans-flag flip); only ConjTrans-of-transposed-storage still falls back (one small GEMM per solve).torch.get_num_threads(), so the k-parallel thread pool (torch pinned to 1 inside tasks) can't oversubscribe workers × cores.Numerically equivalent by construction: same contraction, different kernel (last-bit BLAS rounding only). The eigensolve runs under
no_grad;zbmmadditionally refuses grad-tracked operands so autograd can never be silently detached.Measured (asus, 22-core, Al-4 conv cell 4³ fermi-dirac, ecut=30 Ry, PBE, ONCV — the campaign standard; best of 2 per arm, idle box under the bench-lock protocol, engagement proven by the dispatch counters printed next to each wall time)
offonk_parallel=8Identical free energy to every printed digit (F = −7506.90978722 eV in all 8 runs) and identical SCF iteration counts (10) in every arm.
n_fallback = 0in bothonarms.Honest read: the 2.6-5× per-op deficit shrinks to ~4.7% end-to-end (serial) because the RR GEMMs are a modest fraction of eager wall (Amdahl), and to ~2% under
k_parallel=8where the per-task GEMMs are smaller and BLAS runs serial inside the pool. A real but small win that costs nothing when the .so is absent, and composes with the k-parallel path.Validation note: VASP is not runnable in this environment; QE is the external correctness proxy for the SCF this rides on (unchanged here — same eigenpairs, same iterations, kernel-level substitution only).
Tests
tests/unit/test_blas_routing.py— zbmm vs torch.matmul on random complex128 batches for all routed trans/conj combos (rtol 1e-12), the view layouts davidson actually produces (transpose view, last-dim slice) asserted to route natively, out-of-scope layouts asserted to fall back correctly, env gating (offbit-identical torch,onwithout .so raises,autosilent fallback), and adavidson_batchedA/B on a random Hermitian operator: same eigenvalues to 1e-10, samen_iter, routing on vs off. Skips cleanly when the .so is absent.Fast gate green on asus; ruff / ty / lint-imports / doc-refs clean; branch on current
origin/main.🤖 Generated with Claude Code
https://claude.ai/code/session_018j6BBnPtBSjkrnASSc52mT