Skip to content

Keep a NaN inside its block in xSTEDC and report it through INFO - #1403

Open
rmlarsen wants to merge 2 commits into
Reference-LAPACK:masterfrom
rmlarsen:stedc-nan-block
Open

Keep a NaN inside its block in xSTEDC and report it through INFO#1403
rmlarsen wants to merge 2 commits into
Reference-LAPACK:masterfrom
rmlarsen:stedc-nan-block

Conversation

@rmlarsen

@rmlarsen rmlarsen commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Disclaimer: The initial changes were prepared using Claude Code; review and subsequent test fixes used Codex.

Summary

xSTEDC splits the tridiagonal matrix into independent blocks wherever an off-diagonal entry is negligible against its diagonal neighbours, and both halves of that test are false for a NaN: a NaN off-diagonal entry split the matrix and was dropped, and a NaN diagonal entry ended the block before it and was left as a 1 by 1 block. Whenever the block left over was large enough for the divide and conquer recursion (N > SMLSIZ = 25, which the sizes in the test inputs never reach), the routine returned INFO = 0 with a finite spectrum for a matrix with a NaN off the diagonal, or with the NaN reported as an eigenvalue and its coupling to the neighbours ignored. This PR keeps a NaN inside its block, as xSTEQR, xSTERF, xSTEBZ and xSTEMR do, and reports a NaN block through INFO with the encoding the QR fallback already uses. The numerical changes are in {s,d,c,z}stedc.f, with regression coverage in {s,d,c,z}chkst.f.

Description

The block search is

   20       CONTINUE
            IF( FINISH.LT.N ) THEN
               TINY = EPS*SQRT( ABS( D( FINISH ) ) )*
     $                    SQRT( ABS( D( FINISH+1 ) ) )
               IF( ABS( E( FINISH ) ).GT.TINY ) THEN
                  FINISH = FINISH + 1
                  GO TO 20
               END IF
            END IF

A NaN in E( FINISH ) fails the comparison and the block ends there, without the entry; a NaN in D( FINISH+1 ) makes TINY a NaN, and the block ends before it. The 1 by 1 block is skipped, so a NaN diagonal entry comes back as its own "eigenvalue" while the rest of the matrix is solved as if the neighbouring off-diagonal entries were zero. The complex routines have the same loop, and their COMPZ = 'I' path calls the real routine.

Fix. The block is extended unless the off-diagonal entry is known to be small, .NOT.( ABS( E( FINISH ) ).LE.TINY ), so a NaN stays with its neighbours. A block that reaches the recursion is scaled by its max-norm with xLASCL, which stops in XERBLA for a NaN, so the norm is tested first and a NaN block is reported as a failure on that block, INFO = START*( N+1 ) + FINISH, the code that the xSTEQR fallback on a small block already returns, and that the documentation describes as the submatrix in rows and columns INFO/(N+1) through mod(INFO,N+1). A block small enough for xSTEQR gets the NaN through the QR iteration, which returns INFO > 0 from its iteration limit, or NaN eigenvalues for a 2 by 2 block. Finite matrices take exactly the path they took before: the negated comparison differs from the original only for a NaN.

Minimal reproducer

! DSTEDC('I') on a 26x26 symmetric tridiagonal matrix with a NaN in E(13)
! and, separately, in D(26).
program minimal
  implicit none
  integer, parameter :: n = 26
  double precision :: d(n), e(n), z(n,n), work(4*n*n+8*n), zero
  integer :: iwork(8*n), info, i, nnan, jcase
  zero = 0d0
  do jcase = 1, 2
    do i = 1, n
      d(i) = i
      e(i) = 1d0 / (i + 1)
    end do
    if (jcase == 1) then
      e(13) = zero / zero
    else
      d(26) = zero / zero
    end if
    call dstedc('I', n, d, e, z, n, work, size(work), iwork, size(iwork), info)
    nnan = count(d /= d)
    print '(a,i0,a,i0,a,i0)', 'case ', jcase, ': info = ', info, '  NaN eigenvalues = ', nnan
  end do
end program
BEFORE (master):     case 1: info = 0  NaN eigenvalues = 0
                     case 2: info = 0  NaN eigenvalues = 1
AFTER (this branch): case 1: info = 53  NaN eigenvalues = 0
                     case 2: info = 53  NaN eigenvalues = 1

INFO = 53 = 1*(N+1) + 26: the failure is reported on rows 1 through 26, the whole matrix. With COMPZ = 'N' the routine calls xSTERF, which already returned INFO = 25 for the same matrices.

Regression test. xCHKST calls xSTEDC with COMPZ = 'I' and 'V', once with a NaN in the middle of E and once with a NaN at the end of D, and reports INFO = 0 as a failure. The case owns its diagonal, off-diagonal, and eigenvector arrays, with N = max(2, SMLSIZ+1) (26 for the default SMLSIZ = 25), so it reaches the divide and conquer path even when the input sizes are small. LDU is only a row stride and is not used to infer the capacity of caller arrays. On the parent commit all four calls return INFO = 0 in every precision; with the fix they return INFO > 0.

Validation

  • Current tests: gfortran 13.3 on x86-64, reference BLAS, -O2 -fcheck=all; all 8 focused sep.in/se2.in driver runs pass.
  • 32 AddressSanitizer cases pass with small caller arrays and compact/padded leading dimensions. The storage checks also use SMLSIZ = 25 and 40, and verify that the dedicated regression leaves caller matrix and eigenvalue arrays untouched.
  • The updated regression still fails against the parent numerical kernels in every affected precision.
  • The supplied minimal reproducer was independently checked against the parent and the numerical fix and matches the output above.
  • The full suite and cross-architecture/compiler matrix have not been rerun for this test-only follow-up. Earlier validation of the unchanged numerical kernels included the full LAPACK suite and the special-value/finite sweeps described in the original submission.

Found while auditing the symmetric tridiagonal eigensolvers for the NaN and overflow handling of #1377-#1391.

Update: Codex review identified that LDU does not establish the capacity of the caller arrays. The regression now owns arrays sized above SMLSIZ; sanitizer checks cover small caller arrays and padded leading dimensions.

xSTEDC splits the tridiagonal matrix into independent blocks where an
off-diagonal entry is negligible against its diagonal neighbours:

   TINY = EPS*SQRT( ABS( D( FINISH ) ) )*SQRT( ABS( D( FINISH+1 ) ) )
   IF( ABS( E( FINISH ) ).GT.TINY ) THEN extend the block

Both comparisons are false for a NaN, so a NaN off-diagonal entry
split the matrix and was dropped, and a NaN diagonal entry made TINY a
NaN, ended the block before it and left it as a 1 by 1 block.  The
routine then returned INFO = 0 with a finite spectrum for a matrix
with a NaN off the diagonal, or with the NaN reported as an eigenvalue
and the coupling to its neighbours ignored.  The other tridiagonal
solvers, xSTEQR, xSTERF, xSTEBZ and xSTEMR, keep a NaN in its block.

Extend the block unless the off-diagonal entry is known to be small,
so that a NaN stays with its neighbours.  A block that reaches the
divide and conquer recursion is scaled by its max-norm with xLASCL,
which stops in XERBLA for a NaN, so the norm is tested first and a NaN
block is reported as a failure on that block, INFO = START*(N+1) +
FINISH, the encoding the QR fallback already uses.  A block small
enough for xSTEQR gets the NaN through the QR iteration, which returns
INFO > 0 or, for a 2 by 2 block, NaN eigenvalues.  The same code sits
in cSTEDC and zSTEDC; their COMPZ = 'I' path calls the real routine.

xCHKST gets the case as a regression test for the divide and conquer
path, which the sizes in sep.in (N <= 20, below SMLSIZ = 25) never
reach: after the size and type loops it calls xSTEDC with COMPZ = 'I'
and 'V' on an N = LDU tridiagonal matrix, once with a NaN in the
middle of E and once with a NaN at the end of D, and reports INFO = 0
as a failure.  On the parent commit all four calls return INFO = 0 in
every precision; here they return INFO > 0.  Over a NaN and Inf sweep of xSTEDC and
xSTEVD (six positions, n = 1 to 64, every COMPZ and JOBZ, real and
complex) the parent returned INFO = 0 for a NaN matrix with n >= 3 in
90 cases (DSTEDC 36, ZSTEDC 36, DSTEVD 18) and this branch in none of
the 280 such cases; every finite case is bit-identical.

The full LAPACK test suite passes: 0 numerical errors, 0 other errors,
80 tests more than the parent from the new calls.

The regression test fails on the parent and passes with the fix, and the
reproducer prints the same before and after output, with gfortran 13
(x86-64 Release and Debug with -fcheck=all, and under QEMU on aarch64,
ppc64le, s390x and riscv64), flang-19 and Intel ifx 2025.3.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.93939% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.39%. Comparing base (9eaccc1) to head (8cca9b5).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
TESTING/EIG/cchkst.f 93.10% 2 Missing ⚠️
TESTING/EIG/dchkst.f 93.10% 2 Missing ⚠️
TESTING/EIG/schkst.f 93.10% 2 Missing ⚠️
TESTING/EIG/zchkst.f 93.10% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1403      +/-   ##
==========================================
+ Coverage   69.36%   69.39%   +0.03%     
==========================================
  Files        6122     6122              
  Lines      486337   486457     +120     
  Branches    23268    23268              
==========================================
+ Hits       337330   337572     +242     
+ Misses     148569   148447     -122     
  Partials      438      438              
Components Coverage Δ
BLAS 97.94% <ø> (ø)
CBLAS 96.98% <ø> (ø)
LAPACK 82.45% <100.00%> (+0.06%) ⬆️
LAPACKE 2.17% <ø> (ø)
TMGLIB 55.69% <ø> (ø)
BLAS testing 88.33% <ø> (ø)
CBLAS testing 89.63% <ø> (ø)
LAPACK testing 82.25% <93.10%> (+<0.01%) ⬆️
LAPACKE testing ∅ <ø> (∅)
Files with missing lines Coverage Δ
SRC/cstedc.f 74.79% <100.00%> (+30.53%) ⬆️
SRC/dstedc.f 65.57% <100.00%> (+25.90%) ⬆️
SRC/sstedc.f 65.57% <100.00%> (+25.90%) ⬆️
SRC/zstedc.f 74.79% <100.00%> (+25.61%) ⬆️
TESTING/EIG/cchkst.f 64.62% <93.10%> (+1.47%) ⬆️
TESTING/EIG/dchkst.f 64.65% <93.10%> (+1.49%) ⬆️
TESTING/EIG/schkst.f 64.65% <93.10%> (+1.49%) ⬆️
TESTING/EIG/zchkst.f 64.62% <93.10%> (+1.47%) ⬆️

Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 9eaccc1...8cca9b5. Read the comment docs.

Use local arrays sized above SMLSIZ instead of treating LDU as the capacity
of the caller's vectors and matrix columns. Preserve both NaN positions
and both eigenvector options.

Validation: 8 sep/se2 driver runs, 32 AddressSanitizer capacity cases,
and 4 runs against the parent kernels that retain the expected failures.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant