Repository navigation
evd: nonsymmetric EVD fixes — ?GEEV input scaling, AED sort termination, ?LALN2 2x2 solves (#112, #144, #145) - #154
Merged
Merged
Conversation
This was referenced Oct 29, 2026
evd_imp reduced and iterated on the matrix as given. ?GEEV first scales it into [smlnum, bignum] (smlnum = sqrt(safmin) / eps). Without that, entries below about 2^-90 (32-bit) or 2^-960 (64-bit) went through the Hessenberg reduction and the Schur iteration in subnormals and the eigenvectors lost every digit (GET22 1e5 to 2e15, LAPACK 2 to 70), and the complex Schur iteration near the top of the range lost the factor that kept the c32 LAPACK group failing. The scaling is a power of two, so it is exact, and the eigenvalues are scaled back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
faer-029 reproduces the lost eigenvectors of a tiny matrix on faer 0.24.4 (f64 2^-960: GET22 1.1e15; f32 2^-100: 1.8e6; LAPACK 16 and 16) and on the fork prints 17.7 and 15.2 at every scale. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…view) faer's complex modulus is inf for 2^1023 + 0i and zero for 2^-1074 + 0i, so a modulus-based scan selected a zero factor (NaN eigenvalue with Ok) or no scaling at all. The tests reject nonfinite results before f64::max can drop them; the report notes that faer 0.24.4 returns nonfinite eigenvectors with Ok at 2^-1000 (f64) and 2^-100 (f32); the docs restrict their claims to the measured families. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
aggressive_early_deflation sorted the window with a bubble sort ported from LAPACK dlaqr3, but over the deflated part (dlaqr3's initial I = NS + 1 only sets the first pass's end) and with each next window set from the last swap's position. schur_swap reports success even when schur_swap_22 rejects an ill-conditioned swap, so the same pair was compared forever: Mat::eigen of the f32 matrix J_n + 1e-6 G scaled to 2^-100 did not return at n = 128 and 257. The sort now follows dlaqr3: it sorts the undeflated part infqr..ns, ends each pass at kend, one before where the previous pass stopped, and swaps with schur_swap_checked (the dlaexc ports for 1 x 2 and 2 x 1), leaving a rejected pair in place. Upstream faer 0.24.4 has the same code; the upstream report is faer-030. Closes #144 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… follow ?LALN2 (#145) The back substitution for a complex eigenvalue of a real Schur form solved each 2 x 2 diagonal block through its inverse and compared the block's determinant with the entry-sized clamp `eps_norm`. A determinant is a product of two entries, so every block with entries below `sqrt(eps_norm)` got the determinant `eps_norm` with its original numerators. After `scaled_into_range` the blocks of a strongly graded matrix are all that small, so every complex pair got a wrong eigenvector (GET22 up to 2.6e9). Stage comparison with LAPACK 3.12.0 on f64 n = 64: the Schur form is fine (backward error 8.0); on that same Schur form `dtrevc3` gives 2.2e-4 and faer 3.4e7, and every failing eigen-block is a complex pair. Balancing is not the cause (`dgeev` without it: 0.58; fixed faer: 0.45). Both 2 x 2 solves now use `?LALN2`'s method (`solve_shifted_2x2_cplx`): `eps_norm * I` when every entry is below `eps_norm`, otherwise elimination with complete pivoting and the second pivot raised to `eps_norm`. That covers the real-shift solve too, whose inverse through `a^2 - b c` underflowed to `inf` for tiny blocks. Raising only the determinant was rejected in review: with the original numerators it is not a solved system (4.5e14 on a 4 x 4 Schur form, now a test). Upstream faer 0.24.4 has the same comparison; it is added to upstream-reports as faer-031 with a 4 x 4 input that fails there and passes with this solve. Closes #145 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
shinaoka
force-pushed
the
fix/nonsymmetric-evd
branch
from
October 29, 2026 12:10
f986ec7 to
3ea3887
Compare
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.
Closes #112. Closes #144. Closes #145.
This PR combines #148 (#112), #151 (#144) and #152 (#145), as the maintainer directed on 2026-10-29 ("関連する修正は単一のPRにまとめてもいいよ"). All three change the nonsymmetric EVD path and conflicted with each other. The original commits are kept with their authorship and messages; only the upstream report numbers are renumbered (faer-029, -030, -031), because all three had taken faer-029. Each fix's regression tests, PATCHSET row and upstream report are kept.
1. Input scaling as
?GEEV(#112, from #148)Stage-by-stage comparison with
cgeev(c32 kinds 6/7, n = 63–65):?GEEVscales its input into[smlnum, bignum](smlnum = sqrt(safmin)/eps) before the reduction;evd_impdid not.Where the gap grows. A 372-matrix sweep against LAPACK
?geev(scipy/OpenBLAS) covered f32/f64/c32/c64, n = 16–257, the random, Grcar, near-Jordan and graded families, and scales up to the range limits. On random, Grcar and near-Jordan matrices:2^-90(32-bit).2^-100(32-bit) and2^-960(64-bit) and below, it was 1e4 to 2e15, against LAPACK's 2–70.NoConvergence, and two inputs did not return.faer0.24.4 returns NaN/inf eigenvectors withOkthere.Fix.
evd_impscales by a power of two to just inside the bound, and scales the eigenvalues back.|re|or|im|, because faer's complex modulus is inf for2^1023 + 0iand zero for2^-1074 + 0i. This was a post-review BLOCKER on evd: scale the input into range as ?GEEV does (#112) #148.Tests.
geev_scaling_tests::evd_scales_its_input_into_rangefails without the scaling (f322^-120: 1.7e6).evd_scaling_at_the_complex_range_endsfails with a modulus-based scan.The known-failures entry for #112 is removed.
Upstream report:
faer-029-evd-input-not-scaled.2. The real Schur deflation-window sort always ends (#144, from #151)
aggressive_early_deflation's bubble sort, ported fromdlaqr3, had two faults:ns..jwinstead of the undeflated partinfqr..ns.Since
schur_swapreported success for a rejected 2×2 swap, the sort compared the same pair forever.Mat::eigenof f32J_n + 1e-6 Gat2^-100hung at n = 128 and 257.Fix. The sort follows
dlaqr3:nspasses.schur_swap_checked, and a rejected swap leaves the pair in place.Test:
aed_sort_terminates_when_a_swap_fails, run throughhessenberg_schur_real(which, like?HSEQR, does not scale its input) under a 120 s timeout. Onmainit times out at n = 128.Upstream report:
faer-030-real-schur-aed-sort-hang. Neither size returns within 60 s on 0.24.4.3. The 2×2 block solves of the real eigenvector back substitution follow
?LALN2(#145, from #152)Stage-by-stage on f64 graded n = 64:
dtrevc3on the same T gives 2.2e-4.solve_cplx_shifted_upper_quasi_triangular_systemsolved each shifted 2×2 block through its inverse, and compared the determinant (a product of two entries) with the entry-sized clamp. So every block with entries belowsqrt(clamp)got a floored determinant with unchanged numerators.Fix. Both 2×2 solves use
?LALN2's method:clamp * I;Balancing (
?GEBAL) is not the cause. Unbalanceddgeevgives 0.58 on the same matrix, and the fixed faer gives 0.45. Adopting balancing is a separate decision.Tests:
lapack_nonsymmetric_evd_graded_pairs_keep_their_residual(f64 before: 5.9e8 at n = 16; 1.5e12 on a coupled 4×4);..._pair_solve_with_a_small_determinant;..._real_shift_block_below_the_square_root_of_min_positive(NoConvergencebefore).Upstream report:
faer-031-real-evd-pair-clamp. 0.24.4 gives 8.97e4 on a 4×4 wheredgeevgives 9.6e-14.Verification of the combined branch (primerose, 4 cores)
scripts/run-lapack-validation.py: every family exits 0, including nonsymmetric_evd 40/40, schur 12/12, hessenberg 8/8, svd 88/88, self_adjoint_evd 20/20, lu, qr, cholesky, indefinite, triangular and lstsq. The known-failures list is empty.cargo test -p t4a-faer --lib linalg::evd: 119 passed.D A D^-1, D = 1e±6)2^-10032-bit,2^-90064-bit), all types and familiesNoConvergenceand hangs2^-100(hung before)LAPACK sources were read only; no code was copied. Nothing was posted upstream.
🤖 Generated with Claude Code