Skip to content

CHEEVD #5885 seems to have led to much less accurate eigenvectors with near-degenerate eigenvalues #5995

Description

@youyu3

(Analysis by Claude)

Summary

cheevd (single-precision complex Hermitian eigensolver, jobz='V') produces
substantially less accurate eigenvectors for uplo='L' than for uplo='U'
on the same input matrix, starting somewhere between OpenBLAS v0.3.33
(good) and v0.3.34 (bad). We bisected this to a single commit:

db20661747657af702011ff2fb94912bf6d2dd63
"Increase deflation tolerance to match recent ?SLASD changes (Reference-LAPACK PR 1317)"
Author: Martin Kroeker
Date:   Sun Jul 5 17:37:17 2026 +0200

Reverting only this commit on top of current develop fixes the failure.
Restoring it reproduces the failure again. See Bisection below.

Symptom

Running cheevd(jobz='V', uplo=..., n=100, ...) on a random Hermitian test
matrix (fixed seed, so fully reproducible), then comparing the returned
eigenpairs against the known-truth eigenvalues/eigenvectors used to
construct the matrix (phase-correcting each eigenvector, since A*v = lambda*v is equally satisfied by v' = alpha*v for any unit-modulus
complex alpha):

 _EIGEN_VAL_SPACING (gap) =   2.00000009E-03                                                                       
 thresh_evalmax   =   9.99999975E-06                                                                               
 thresh_ecolmax   =   1.19209290E-03   ( max of floor   5.00000024E-04 and   20.0000000     * eps/gap )                                                                                                                                
 thresh_ealphamax =   1.19209280E-02   ( max of floor   2.00000009E-03 and   200.000000     * eps/gap )                                                                                                                                
 uplo=U  evalmax=   9.53674316E-07  ecolmax=   4.86821118E-05  ealphamax=   7.85589218E-05                                                                                                                                             
 uplo=U  Passed                                                                                                    
 uplo=L  evalmax=   8.34465027E-07  ecolmax=   6.33120025E-03  ealphamax=   2.75581479E-02                                                                                                                                             
 uplo=L  Failed

uplo='U' and uplo='L' are just two different ways of reading the same
fully-populated Hermitian input matrix (our generator fills both
triangles), so this isn't a difference in test data -- it's cheevd
itself recovering the eigenvectors far less accurately for the
lower-triangular storage/reduction path, on this input.

The input matrix used in our test is from a generator derived from this one, with
an additional logic to enforce a minimum eigenvalue spacing.

The threshold value for the eigenvector verification is based on Davis-Kahan.
See comments in reproducer for details.

Environment

  • OpenBLAS develop @ 632ef8743 (also reproduces at tagged v0.3.34)
  • x86_64 (sapphirerapids), CMake build, USE_THREAD default
  • Reproduces identically with two different compilers/toolchains we tried
    (nvfortran two different releases, and gfortran 11.5.0), so it isn't
    compiler-specific.
  • v0.3.33 does not reproduce the failure (both uplo cases pass,
    ~1e-4 accuracy).

Root cause

It is found that the root cause is the change in #5885, confirmed by a revert/rebuild/rerun/restore round-trip:

--- a/lapack-netlib/SRC/slaed2.f
+++ b/lapack-netlib/SRC/slaed2.f
@@ -312,7 +312,7 @@
       EPS = SLAMCH( 'Epsilon' )
-      TOL = EIGHT*EPS*MAX( ABS( D( JMAX ) ), ABS( Z( IMAX ) ) )
+      TOL = EIGHT*EIGHT*EPS*MAX( ABS( D( JMAX ) ), ABS( Z( IMAX ) ) )

--- a/lapack-netlib/SRC/slaed8.f
+++ b/lapack-netlib/SRC/slaed8.f
@@ -355,7 +355,7 @@
       EPS = SLAMCH( 'Epsilon' )
-      TOL = EIGHT*EPS*ABS( D( JMAX ) )
+      TOL = EIGHT*EIGHT*EPS*ABS( D( JMAX ) )

(commit db2066174 also makes the equivalent change in dlaed2.f/
dlaed8.f for double precision). SLAED2/SLAED8 decide, during
SSTEDC/CSTEDC's divide-and-conquer merge, which eigenvalue pairs are
"close enough" to deflate -- skip solving the secular equation for them
and copy/rotate their eigenvectors directly instead. This commit widens
that tolerance 8x (8*EPS -> 64*EPS). CSTEDC (used by CHEEVD)
and SSTEDC (used by SSYEVD) both route their real-valued secular-
equation merge through this same SLAED2/SLAED8 code, so we'd expect
SSYEVD to be susceptible to the same failure mode, though we haven't
built a matrix that demonstrates it there yet.

Revert/rebuild/rerun round-trip (n=100, iseed=4321, _EIGEN_VAL_SPACING=2.d-3):

Build uplo='U' uplo='L'
develop HEAD (db2066174 present) Passed (ecolmax 4.87E-05) Failed (ecolmax 6.33E-03, ealphamax 2.76E-02)
develop HEAD with db2066174 reverted Passed (ecolmax 4.82E-05) Passed (ecolmax 4.82E-05, ealphamax 2.02E-04)
develop HEAD, commit restored Passed (ecolmax 4.87E-05) Failed (ecolmax 6.33E-03, matches row 1 exactly)
 _EIGEN_VAL_SPACING (gap) =   2.00000009E-03                                                                       
 thresh_evalmax   =   9.99999975E-06                                                                               
 thresh_ecolmax   =   1.19209290E-03   ( max of floor   5.00000024E-04 and   20.0000000     * eps/gap )                                                                                                                                
 thresh_ealphamax =   1.19209280E-02   ( max of floor   2.00000009E-03 and   200.000000     * eps/gap )                                                                                                                                
 uplo=U  evalmax=   9.53674316E-07  ecolmax=   4.81969910E-05  ealphamax=   7.83205032E-05                                                                                                                                             
 uplo=U  Passed                                                                                                    
 uplo=L  evalmax=   8.34465027E-07  ecolmax=   4.81597272E-05  ealphamax=   2.02119350E-04                                                                                                                                             
 uplo=L  Passed  

Sensitivity to eigenvalue spacing

Our matrix generator enforces a floor on the gap between generated
eigenvalues (_EIGEN_VAL_SPACING, build-time constant) to avoid
unrelated single-precision resolution issues with genuinely
near-degenerate eigenvalues. Sweeping this floor against the same
buggy develop build shows the failure is specifically about eigenvalue
proximity, and disappears once the floor is wide enough:

_EIGEN_VAL_SPACING uplo='L'
2.d-3 Failed (ecolmax 6.33E-03)
5.d-3 Failed (ecolmax 5.81E-03)
6.d-3 Passed (ecolmax 1.07E-04)
7.d-3 -- 1.d-1 Passed

This is consistent with the root cause: the new deflation tolerance
(TOL = 64*EPS*max(|D|,|Z|), ~1e-5 for float32 at this matrix's
eigenvalue scale) doesn't act directly on the globally enforced minimum
gap between the eigenvalues we feed in at generation time -- it acts on
gaps that appear locally, between intermediate eigenvalues inside the
divide-and-conquer merge tree (~7 levels for n=100), which can end up
much smaller than the global floor. A 2e-3 global floor still lets some
such local gap fall inside the widened deflation window on the uplo='L'
reduction path for this matrix (but not uplo='U'); a 6e-3+ floor
apparently gives enough margin to avoid it. This is a data point for
diagnosis, not a fix
-- widening the test's spacing floor would only
mask the regression, not correct it.

Reproducing

Run the whole thing end to end, from a fresh clone, in one shot, with the attached reproducer reproducer.tgz:

./reproduce.sh

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions