Skip to content

Avoid overflow in the 2x2 pivot inverse of the symmetric-indefinite factorizations - #1377

Open
rmlarsen wants to merge 4 commits into
Reference-LAPACK:masterfrom
rmlarsen:bunch-kaufman-2x2-overflow
Open

Avoid overflow in the 2x2 pivot inverse of the symmetric-indefinite factorizations#1377
rmlarsen wants to merge 4 commits into
Reference-LAPACK:masterfrom
rmlarsen:bunch-kaufman-2x2-overflow

Conversation

@rmlarsen

@rmlarsen rmlarsen commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Disclaimer: This PR was prepared using Claude Code / Fable 5.1.

Description

The Bunch–Kaufman symmetric-indefinite factorizations invert a 2×2 pivot block

$$D_k=\begin{pmatrix}d_{11}&d_{21}\\ d_{21}&d_{22}\end{pmatrix}$$

by dividing through by the off-diagonal d21, so the scaled determinant denom = d11*d22/d21^2 - 1 stays O(1). The pivot test keeps denom in the open interval $(-1-\alpha^2,\ \alpha^2-1)$ with $\alpha = (1+\sqrt{17})/8$. A factor entry is

$$l_j=\frac{1}{\mathrm{denom}}\cdot\frac{(d_{11}/d_{21})\,w_{0j}-w_{1j}}{d_{21}},$$

a division by d21 and a scaling by T = 1/denom. The reference code does these two steps in an order that overflows at the extremes of the exponent range — silently in one family, with a false INFO in the other:

  • Classic unblocked/packed (xSYTF2, xHETF2, xSPTRF, xHPTRF) and classic panel (xLASYF, xLAHEF) hoist the reciprocal as D21 = T/d21 and then multiply. T/d21 overflows to ±Inf when d21 is subnormal, so a well-conditioned system with a subnormal off-diagonal is factored into Inf/NaN and solved to NaN with INFO = 0 (a silently wrong answer).
  • Rook/RK (xSYTF2_ROOK, xHETF2_ROOK, xSYTF2_RK, xHETF2_RK) form T*(d22*w0 - w1) and divide by d21 afterwards. The intermediate T*(...) overflows when the block is near the overflow threshold, although the factor entry is O(1); the Inf trips the isnan pivot guard and reports INFO > 0, a false singularity, for a nonsingular matrix.

Each family is broken at exactly one end of the range and correct at the other, so neither is caught by the other's inputs.

Fix. Divide each entry by d21 first and scale by T afterwards, uniformly in all 30 routines:

l_j = T * ( ( (d11/d21)*w0_j - w1_j ) / d21 )

The quotient equals denom*l_j, within a factor 1 + alpha^2 < 2 of l_j, so it overflows only where l_j itself is within that factor of the threshold, while the subnormal case becomes a single correctly-rounded division. This is the order xSYTRS/xSYTRS_3 already use to apply the inverse of D in the solve. The hoisted 1/d21 becomes a per-entry division: the added cost is one division per factor entry of a 2×2 pivot.

Known gfortran limitation. The new order can expose internal overflow in gfortran 13.3’s complex division even when the quotient is small, at both -O0 and -O3 without fast-math (reproducer). CLADIV/ZLADIV avoid this but add overhead; that workaround is not included in this PR.

Minimal reproducer

program minimal
  implicit none
  double precision :: A(3,3), b(3), work(96), s, m
  integer :: ipiv(3), info
  s = 2d0**(-1032)                       ! a subnormal (denormal) double
  m = huge(1d0)
  ! (1) subnormal 2x2 block  A = [[s,4s,s],[4s,s,s],[s,s,1]]
  A = reshape([ s, 4*s, s,  4*s, s, s,  s, s, 1d0 ], [3,3])
  b = A(:,3)
  call dsysv('L', 3, 1, A, 3, ipiv, b, 3, work, size(work), info)
  print '(a,i0,a,3es13.5)', 'DSYSV    subnormal: info=', info, '  x=', b
  ! (2) near-maximum 2x2 block  A = M*[[1/2,1,0],[1,1/2,9/10],[0,9/10,0]]
  A = reshape([ m/2, m, 0d0,  m, m/2, 0.9d0*m,  0d0, 0.9d0*m, 0d0 ], [3,3])
  b = A(:,3)
  call dsysv_rk('L', 3, 1, A, 3, work(1:2), ipiv, b, 3, work(3), size(work)-2, info)
  print '(a,i0,a,3es13.5)', 'DSYSV_RK near-max : info=', info, '  x=', b
  print '(a)', 'Expected in both: info=0, x = (0, 0, 1).'
end program

Both systems have exact solution e3.

BEFORE (master):
DSYSV    subnormal: info=0  x=          NaN          NaN          NaN
DSYSV_RK near-max : info=3  x=  0.00000E+00  1.61792+308  0.00000E+00

AFTER (this branch):
DSYSV    subnormal: info=0  x=  0.00000E+00  0.00000E+00  1.00000E+00
DSYSV_RK near-max : info=0  x=  0.00000E+00  0.00000E+00  1.00000E+00

A 3×3 block embedded at the top (L) or bottom (U) of a 130×130 identity exercises the blocked panel path (xLASYF/xLAHEF) and shows the same before/after for every driver in the family (?SYSV, ?SYSV_ROOK, ?SYSV_RK, ?SPSV, ?HESV, ?HESV_ROOK, ?HESV_RK, ?HPSV, real/complex, Hermitian and complex-symmetric). At master, ZHPSV/ZSPSV on the subnormal block corrupt the heap (double free or corruption); the fix removes that too.

Regression test. No type of the ?SY path could hold a subnormal 2 by 2 pivot block: xLATMS scales what it generates to the norm xLATB4 asks for, and that is at most 1/(SFMIN/EPS). Type 11 takes the type 2 matrix, scales two adjacent rows and columns into the subnormal range, and gives them a block with d11 = d22 = s and d21 = 4 s, so the pivot test chooses it; the block sits at the end the factorization starts from, the last two indices for 'U' and the first two for 'L'. The reconstruction residual of xSYTRF is the test, and on the parent commit it is a NaN for 24 shapes per precision. A NaN is not .GE. THRESH, so the comparison that prints a failure now tests for one as well.

Such a matrix sits at the edge of the representable range, so only the factorization is meaningful there: its inverse overflows whatever the pivot inversion does, and the reciprocal of its norm is not a condition estimate. The type sets TRFCON, which is how the path already skips those tests, and skips the condition estimate too. The complex full-storage routines take the same code with a complex d21, where the division forms |d21|^2 and returns before the reciprocal overflows, so the type is added for the real precisions; the packed and Hermitian routines are covered by the sweep below.

Validation. The full LAPACK linear-equation test suite (xlintst{s,d,c,z} on {s,d,c,z}test.in) passes with no new failures — every SY/SR/SK/SA/S2/SP and HE/HR/HK/HA family at threshold, e.g. DSY 2918, DSR 1840, DSK 1840, DSP 2476 tests. Built and run with GCC 13.3.

Checklist

  • The documentation has been updated. (The D**(-1) derivation comments in xLASYF/xLAHEF and the inline comments now describe the divide-then-scale order.)
  • If the PR solves a specific issue, it is set to be closed on merge. (No tracking issue; happy to open one.)

…actorizations

The Bunch-Kaufman 2x2 pivot block D_k = [[d11, d21],[d21, d22]] is inverted
by dividing through by the off-diagonal d21 so that the scaled determinant
DENOM = d11*d22/d21^2 - 1 stays O(1). The factor entries are

    l_j = (1/DENOM) * ((d11/d21)*w0_j - w1_j) / d21    (and its d22 partner),

a per-entry division by d21 and a scaling by T = 1/DENOM. The reference code
does these two operations in an order that overflows at the extremes of the
exponent range, silently in the classic path and with a false INFO in the
rook/RK path:

* The classic unblocked/packed routines (xSYTF2, xHETF2, xSPTRF, xHPTRF) and
  the classic panel routines (xLASYF, xLAHEF) hoist the reciprocal as
  D21 = T/d21 and then multiply. T/d21 overflows to +-Inf when d21 is
  subnormal, so a well-conditioned system with a subnormal off-diagonal is
  factored into Inf/NaN and solved to NaN with INFO = 0.

* The rook/RK routines (xSYTF2_ROOK, xHETF2_ROOK, xSYTF2_RK, xHETF2_RK) form
  T*(d22*w0_j - w1_j) and divide by d21 afterwards. The intermediate
  T*(...) overflows when the block is near the overflow threshold, even though
  the factor entry is O(1); the resulting Inf trips the isnan pivot guard and
  reports INFO > 0 (a false singularity) for a nonsingular matrix.

Divide each entry by d21 first and scale by T afterwards, in every routine:

    l_j = T * (((d11/d21)*w0_j - w1_j) / d21).

The quotient equals DENOM*l_j, which is within a factor 1 + alpha^2 (< 2) of
the result, so it overflows only where l_j itself is within that factor of the
overflow threshold, while the subnormal case is a plain correctly-rounded
division. This is the order xSYTRS/xSYTRS_3 already use to apply D^-1 in the
solve. The hoisted 1/d21 becomes a per-entry division; the extra cost is one
division per factor entry of a 2x2 pivot.

The full LAPACK linear-equation test suite (xlintst{s,d,c,z}) passes with no
new failures across the SY/SR/SK/SA/SP and HE/HR/HK/HA families.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
langou
langou previously approved these changes Sep 7, 2026
@codecov

codecov Bot commented Sep 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.57983% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 69.36%. Comparing base (cee0183) to head (a59e306).
⚠️ Report is 53 commits behind head on master.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
TESTING/LIN/alahd.f 0.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1377      +/-   ##
==========================================
+ Coverage   69.01%   69.36%   +0.34%     
==========================================
  Files        6122     6122              
  Lines      486123   486335     +212     
  Branches    23286    23268      -18     
==========================================
+ Hits       335514   337328    +1814     
+ Misses     150420   148569    -1851     
- Partials      189      438     +249     
Components Coverage Δ
BLAS 97.94% <ø> (ø)
CBLAS 96.98% <ø> (+<0.01%) ⬆️
LAPACK 82.38% <100.00%> (+0.01%) ⬆️
LAPACKE 2.17% <ø> (+2.07%) ⬆️
TMGLIB 55.69% <ø> (ø)
BLAS testing 88.33% <ø> (ø)
CBLAS testing 89.63% <ø> (ø)
LAPACK testing 82.25% <97.56%> (-0.11%) ⬇️
LAPACKE testing ∅ <ø> (∅)
Files with missing lines Coverage Δ
SRC/chetf2.f 100.00% <100.00%> (ø)
SRC/chetf2_rk.f 95.16% <100.00%> (ø)
SRC/chetf2_rook.f 94.69% <100.00%> (ø)
SRC/chptrf.f 97.90% <100.00%> (-0.03%) ⬇️
SRC/clahef.f 98.91% <100.00%> (-0.02%) ⬇️
SRC/clasyf.f 98.76% <100.00%> (-0.02%) ⬇️
SRC/csptrf.f 100.00% <100.00%> (ø)
SRC/csytf2.f 100.00% <100.00%> (ø)
SRC/csytf2_rk.f 94.49% <100.00%> (ø)
SRC/csytf2_rook.f 93.87% <100.00%> (ø)
... and 25 more

... and 146 files with indirect coverage changes


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 cee0183...a59e306. Read the comment docs.

@rmlarsen

rmlarsen commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

@langou I am going to amend this by one commit to fix a potential overflow regression in gfortran's intrinsic complex division, and add a regression test. Sorry for the churn.

@rmlarsen

rmlarsen commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

There is a numerical robustness limitation in gfortran’s default complex division. I reproduced it at both -O0 and -O3, without fast-math: with M = HUGE(1d0), (M*(17/32 + 17i/32)) / (M*(-1/2 - i/2)) returns -Inf instead of -1.0625. LAPACK's ZLADIV/CLADIV return the correct results, but produce slower code. Investigating an alternative approach...

Use scaled complex division for extreme operands. Retain reciprocal multiplication only when the pivot, reciprocal, and numerator bounds exclude intermediate overflow.

Add regression coverage for all four precisions, both triangles, packed storage, and blocked panels, including a large numerator with a moderate pivot. Register the tests in both CMake and Make builds.
@rmlarsen

rmlarsen commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

@langou working around the issue in gfortran's complex division adds a lot of code complexity and the fix arguably belongs in gfortran. Reverted to the original version you reviewed and added a comment in the PR description.

@rmlarsen
rmlarsen requested a review from langou September 7, 2026 21:01
Comment thread TESTING/LIN/dchksy.f
DOUBLE PRECISION ZERO
PARAMETER ( ZERO = 0.0D+0 )
DOUBLE PRECISION FOUR
PARAMETER ( FOUR = 4.0D+0 )

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah my favorite kind of named constant: The obvious one.

The matrix generator scales what it produces to the requested norm and
xLATB4 asks for at most 1/(SFMIN/EPS), so no type of the SY path holds
a 2x2 pivot block whose off-diagonal entry is subnormal, which is what
the hoisted reciprocal T/d21 overflows on.

Type 11 of xCHKSY takes the type 2 matrix, scales two adjacent rows and
columns into the subnormal range, and gives them a 2 by 2 block with
d11 = d22 = s and d21 = 4 s: the pivot test then chooses that block,
and it sits at the end the factorization starts from, the last two
indices for 'U' and the first two for 'L'.  The reconstruction residual
of xSYTRF is the test; on the parent commit it is a NaN for 24 of the
shapes per precision, and a NaN is not .GE. THRESH, so the comparison
that prints a failure now tests for one.

Such a matrix is at the edge of the representable range, so only the
factorization is meaningful: its inverse overflows whatever the pivot
inversion does, and the reciprocal of its norm is not a condition
estimate.  The type therefore sets TRFCON, which is how the path
already skips those tests, and skips the condition estimate as well.

The complex full-storage path takes the same code with a complex d21,
where the division computes |d21|^2 and returns before the reciprocal
overflows, so the type is added for the real precisions only; the
sweep in the pull request covers the packed and Hermitian routines.

The test files declare the new xSCAL calls EXTERNAL: the extended-API
build renames only the routines a file declares, so without the
declaration xlintsts_64 and xlintstd_64 failed to link against the
64-bit BLAS.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@rmlarsen
rmlarsen force-pushed the bunch-kaufman-2x2-overflow branch from 0791d6e to a59e306 Compare September 10, 2026 07:18
@rmlarsen

rmlarsen commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

Verified on an Apple M4 (macOS, Homebrew gfortran 16.2, Release build with the CI flags). With this branch merged onto current master, the full test suite passes, the new tests fail without the fix, and the reproducer behaves as described above.

One difference from x86: on master, the DSYSV_RK case of the reproducer returns NaN with INFO = 0 on this machine instead of the false INFO = 3. With the fix both cases give the exact solution.

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.

2 participants