Skip to content

Scale down in xLARFG and xLARFGP when |BETA| exceeds half the overflow threshold - #1381

Open
rmlarsen wants to merge 3 commits into
Reference-LAPACK:masterfrom
rmlarsen:larfg-top-end-scaling
Open

rmlarsen wants to merge 3 commits into
Reference-LAPACK:masterfrom
rmlarsen:larfg-top-end-scaling

Conversation

@rmlarsen

@rmlarsen rmlarsen commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Disclaimer: This PR was prepared using Claude Code.

Summary

xLARFG and xLARFGP rescale their input only at the small end of the exponent range. At the large end they form ALPHA-BETA (xLARFG) and ALPHA+BETA (xLARFGP), sums of two like-signed terms each bounded by |BETA|, so the sum overflows whenever |BETA| > OVFL/2 even though BETA and the reflector are representable. xLARFG then returns TAU = Inf and v = 0; xLARFGP gets ALPHA = Inf, computes TAU = 0, takes its flush branch and returns H = I with the tail of the column not annihilated. Every QR-type factorization built on the generators returns a wrong R, or Inf/NaN, with INFO = 0 for any column whose leading entry exceeds about 9e307 (1.7e38 in single precision). This PR adds the missing large-end branch to the eight generators. Nothing changes for an input the old code handled.

Description

For a well-conditioned 6-by-4 matrix scaled by 2^1021 (largest entry 1.03e308), compared with the factorization of the unscaled matrix (Householder QR is exactly invariant under a power-of-two scaling):

routine master this branch
DGEQRF, DGELQF, DTZRZF, ZGEQRF Inf in the factor, INFO = 0 identical up to rounding of the norm
DGEQRT, DGEQR, DGEQP3, DGEQP3RK NaN, INFO = 0 identical up to rounding of the norm
DGEQRFP, DGEQLF finite but wrong, INFO = 0 identical up to rounding of the norm

At 2^1019 (largest entry 2.6e307) every routine above is bit-identical between master and this branch. DGELS, DGELST, DGETSLS, DGELSY, DGELSD and DGELSS pre-scale A into [SMLNUM, BIGNUM] and are not affected; DGGLSE and DGGGLM do not pre-scale and are the subject of a separate PR.

LAWN 203 (section 2) analyzes the like-signed rearrangement and the small-end rescaling of these generators but does not consider the large end; #938 fixed the small-end 1/ALPHA overflow of xLARFGP only.

Fix. When |BETA| > OVFL/2, scale X and ALPHA by SAFMIN (2^-969), recompute XNORM and BETA, and multiply BETA back by 1/SAFMIN on exit, mirroring the existing small-end branch (KNT = -1 marks the case). Since |ALPHA| <= |BETA|, the sum cannot overflow when |BETA| <= OVFL/2, so the test is placed exactly there and no input that did not overflow before takes the new branch. The threshold is written HALF*HUGE( ZERO ); HUGE( ZERO ) is what xLAMCH( 'O' ) returns, and as a compile-time constant it costs the common path one compare and no arithmetic that could raise a floating-point exception. (The first revision of this PR guarded the branch with |BETA|*SAFMIN > 1 to keep xLAMCH( 'O' ) off the common path; that product underflows for every |BETA| < 1, so ordinary inputs such as ALPHA = X(1) = 1e-100 set the IEEE underflow flag and aborted under -ffpe-trap=underflow. repro/precheck_underflow.f90 checks the flag after each of the eight generators.) Eight files: {s,d,c,z}larfg.f, {s,d,c,z}larfgp.f.

Minimal reproducer

program minimal
  implicit none
  double precision :: alpha, x(1), tau
  alpha = 0.6d0 * huge(1d0)
  x = 1d0
  call dlarfg(2, alpha, x, 1, tau)
  print '(a,es10.3,a,es10.3,a,es10.3)', 'DLARFG: beta = ', alpha, '  tau = ', tau, '  v(2) = ', x(1)
end program
BEFORE (master):     DLARFG: beta = -1.079+308  tau =   Infinity  v(2) =  0.000E+00
AFTER (this branch): DLARFG: beta = -1.079+308  tau =  2.000E+00  v(2) =  4.636-309

The same input gives tau = Infinity from SLARFG (with 0.6*huge(1.0)) and ZLARFG. Through DGEQRF, on the 6-by-4 matrix above:

                              R(1,1)        R(1,2)        R(1,3)        R(1,4)
unscaled R * 2**1021:  -1.184286+308 -9.371609+307 -8.423136+307 -8.266508+307
master:                -1.184286+308     -Infinity     -Infinity     -Infinity   (INFO = 0)
this branch:           -1.184286+308 -9.371609+307 -8.423136+307 -8.266508+307

Validation

Exponent sweep of the eight generators, 856704 cases, verified in quadruple precision

repro/larfg_sweep.f90 runs xLARFG and xLARFGP in all four precisions for n in {1, 2, 3, 4, 8, 33}, INCX in {1, 2}, two signs of ALPHA, three tail patterns, and every pair of exponents from a grid over the whole range (every 64 binades plus the binades next to the underflow, subnormal, SAFMIN, OVFL/2 and OVFL boundaries). Every output is printed in hex, and every reflector is checked against its defining relation $H^H x = \beta e_1$ in quadruple precision (double for the single-precision routines); a case fails if an output is not finite, the residual exceeds 1e-13 (1e-5 single), or xLARFGP returns a negative BETA. Cases whose true BETA is subnormal or above OVFL are not judged for accuracy (the routines document the loss of accuracy for a subnormal result) but still must not produce a non-finite output when BETA is representable.

generator cases bit-identical changed, all with true |BETA| > OVFL/2 changed with |BETA| <= OVFL/2 master fails this branch fails
SLARFG 32016 29100 2916 0 1324 0
SLARFGP 32016 30128 1888 0 310 0
DLARFG 182160 174780 7380 0 3244 0
DLARFGP 182160 177726 4434 0 296 0
CLARFG 32016 28600 3416 0 1392 0
CLARFGP 32016 28608 3408 0 1392 0
ZLARFG 182160 173820 8340 0 3312 0
ZLARFGP 182160 173830 8330 0 3312 0

This branch never returns a non-finite output when the true BETA is representable. xLARFGP fails less often on master because its flush branch happens to give an acceptable H = I when the tail is negligible relative to ALPHA.

Among the changed cases where master was also accurate, the two libraries compute the same reflector to rounding; the maximum residual over those cases is smaller on this branch in every precision (double: 1.1e-15 on master vs 6.9e-16 here; complex double: 1.8e-15 vs 9.4e-16; single: 7.2e-7 vs 3.5e-7; complex single: 1.0e-6 vs 4.1e-7). In single precision a handful of cases (8 to 20 per generator) move by one or two ulps in the other direction, all within tolerance. The changes come from xNRM2 rounding the scaled tail differently, and they occur only for |BETA| > OVFL/2, where master's result is either these same bits or an overflow.

run_sweep.sh reproduces the table; classify_sweep.py produces it from the two output files.

Regression test. The ?QR, ?LQ, ?QL and ?RQ paths get a matrix type 9: the random matrix of type 4 with the entry the first reflector works on raised to three quarters of the overflow threshold, A( 1, 1 ) for QR and LQ and A( M, N ) for QL and RQ. The generator cannot produce such a matrix, since xLATMS scales what it generates to the requested norm, and a whole matrix scaled to the top of the range does not trigger the defect either: for a random column, $\lvert\alpha\rvert \approx \|a\|_2/\sqrt{M}$, so the sum that overflows needs a single entry close to the norm of its column. The one-norm of the new matrix stays finite, so the orthogonality ratio and the xORGQR and xORMQR ratios remain meaningful.

The existing ratios detect the failure only once their comparison with the threshold is guarded with xISNAN, because a NaN is not .GE. THRESH and every ratio of the new type would otherwise pass on the parent commit. With the guard, the parent fails 5670 ratios per precision and this branch none. The guard is on the print loop of the four checkers, so it also covers the existing types.

Test suite. The full LAPACK test suite passes on this branch: 215 of 215 CTest entries, 5506497 LAPACK tests and 315872 BLAS tests with 0 numerical errors and 0 other errors, built and run the same way as the parent commit f96546fc9 (GCC 13.3, CMAKE_BUILD_TYPE=Release, BUILD_INDEX64_EXT_API=ON). The 64596 tests above the parent are the four new matrix types; no existing type produced a NaN ratio that the new guard turned into a failure.

Performance. Timed on a 13th Gen Intel(R) Core(TM) i7-13700HX under WSL2 with the reference BLAS, GCC 13.3, -O2. To separate the change from code-placement effects (which move untouched routines by up to 28% between two separately linked static libraries on this machine), the parent library is a shared object shared by both sides, and each benchmark binary carries its own copy of only the changed routines, parent or branch, which interposes over the library's; everything else is byte-identical. Four rounds in alternating order, one core, one process per run, on an idle machine; medians of the per-round medians, with DPOTRF as an untouched control. The benchmark driver and raw output are available on request.

routine n parent this branch ratio
DLARFG (ns/call) 2 45.2 45.4 1.00
4 46.6 46.0 0.99
16 55.6 54.9 0.99
64 97.0 98.1 1.01
1024 774 776 1.00
DGEQRF (us/call) 8 0.67 0.67 1.00
32 7.92 7.84 0.99
128 426 411 0.96
512 36301 35436 0.98
1024 698404 701355 1.00
DPOTRF control (us/call) 64 / 256 / 1024 1.00 / 1.06 / 1.01

No measurable difference: the common path gains one compare per reflector, below the resolution of the measurement (the round-to-round spread of DLARFG is 2 ns). The timing was taken on the first revision, whose common path also carried a multiply; the current one does strictly less work there.

Checklist

  • The documentation has been updated. (No interface or documented-behavior change; the routines' documentation does not describe the scaling.)
  • If the PR solves a specific issue, it is set to be closed on merge. (No tracking issue; happy to open one.)

@codecov

codecov Bot commented Sep 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.97059% with 30 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.37%. Comparing base (a6c6e74) to head (5aa9f46).
⚠️ Report is 2 commits behind head on master.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
SRC/clarfgp.f 20.00% 8 Missing ⚠️
SRC/zlarfgp.f 20.00% 8 Missing ⚠️
SRC/dlarfgp.f 22.22% 7 Missing ⚠️
SRC/slarfgp.f 22.22% 7 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1381      +/-   ##
==========================================
+ Coverage   69.36%   69.37%   +0.01%     
==========================================
  Files        6122     6124       +2     
  Lines      486711   486945     +234     
  Branches    23268    23274       +6     
==========================================
+ Hits       337584   337800     +216     
- Misses     148689   148707      +18     
  Partials      438      438              
Components Coverage Δ
BLAS 97.94% <ø> (ø)
CBLAS 96.98% <ø> (ø)
LAPACK 82.38% <60.52%> (-0.01%) ⬇️
LAPACKE 2.17% <ø> (ø)
TMGLIB 55.69% <ø> (ø)
BLAS testing 88.33% <ø> (ø)
CBLAS testing 89.63% <ø> (ø)
LAPACK testing 82.23% <100.00%> (+0.02%) ⬆️
LAPACKE testing ∅ <ø> (∅)
Files with missing lines Coverage Δ
SRC/clarfg.f 92.85% <100.00%> (+2.23%) ⬆️
SRC/dlarfg.f 97.22% <100.00%> (+0.92%) ⬆️
SRC/slarfg.f 97.22% <100.00%> (+0.92%) ⬆️
SRC/zlarfg.f 92.85% <100.00%> (+2.23%) ⬆️
TESTING/LIN/alahd.f 0.00% <ø> (ø)
TESTING/LIN/cchkaa.F 72.47% <100.00%> (ø)
TESTING/LIN/cchklq.f 89.77% <100.00%> (+0.23%) ⬆️
TESTING/LIN/cchkql.f 89.41% <100.00%> (+0.25%) ⬆️
TESTING/LIN/cchkqr.f 88.88% <100.00%> (+0.25%) ⬆️
TESTING/LIN/cchkrq.f 89.41% <100.00%> (+0.25%) ⬆️
... and 29 more

... and 2 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 a6c6e74...5aa9f46. Read the comment docs.

@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.

rmlarsen and others added 2 commits September 15, 2026 13:19
…w threshold

The Householder generators rescale their input only at the small end,
when |BETA| < SAFMIN.  At the large end they form ALPHA-BETA (xLARFG)
and ALPHA+BETA (xLARFGP), sums of two like-signed terms each bounded by
|BETA|, which overflow whenever |BETA| > OVFL/2 although BETA itself and
the reflector are representable.  xLARFG then returns TAU = Inf and
v = 0; xLARFGP gets ALPHA = Inf, TAU = 0, takes its flush branch and
returns H = I with the tail of the column not annihilated.  Every
QR-type factorization built on them (xGEQRF, xGEQRFP, xGELQF, xGEQLF,
xGERQF, xGEQRT, xGEQR, xGEQP3, xGEQP3RK, xTZRZF) returns a wrong R or
Inf/NaN with INFO = 0 for a column whose leading entry exceeds about
9e307 (1.7e38 in single precision).  The least-squares drivers pre-scale
A and are not affected.

When |BETA| > OVFL/2, scale X and ALPHA by SAFMIN, recompute XNORM and
BETA, and multiply BETA back by 1/SAFMIN on exit, mirroring the existing
small-end branch.  The factors are powers of two, so TAU and v are the
same reflector.  Since |ALPHA| <= |BETA|, the sums cannot overflow for
|BETA| <= OVFL/2, and the test is placed there so that no input the old
code handled takes the new branch.  The overflow threshold is fetched
from xLAMCH( 'O' ) only when |BETA|*SAFMIN > 1, which keeps the extra
cost on the common path to one multiply and one compare.

The QR, LQ, QL and RQ test paths get a matrix type for it: type 9 is
the random matrix of type 4 with the entry the first reflector works on
raised to three quarters of the overflow threshold, A( 1, 1 ) for QR and
LQ and A( M, N ) for QL and RQ.  The generator cannot produce such a
matrix, because it scales what it generates by the requested norm.  The
existing test ratios detect the failure, but only once the comparison
with the threshold is guarded with xISNAN: a NaN is not .GE. THRESH, so
without the guard every ratio of the new type passes on the parent
commit.  With it, the parent fails 5670 ratios per precision and this
branch none.

Over a sweep of 856704 (precision, generator, n, INCX, exponent of
ALPHA, exponent of X, sign, pattern) cases the outputs are bit-identical
to the parent commit whenever the true |BETA| <= OVFL/2; the parent
returns Inf, NaN or a reflector with residual above tolerance in 12622
cases with a representable BETA, this branch in none, and each reflector
was verified against its defining relation in quadruple precision.  The
full LAPACK test suite passes: 5506497 LAPACK and 315872 BLAS tests,
0 numerical errors, 0 other errors; the 64596 tests above the parent
are the four new matrix types.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The top-end QL cases can overflow the tau*w update while applying Q to
B, even when the reflectors and the final solution are representable.
Scale large right hand sides before applying Q and restore their scale
after the triangular solve. Use component magnitudes for complex B so
the scaling decision itself cannot overflow.

This preserves the extreme-value factorization cases and fixes their
least-squares residual failures. All four linear test suites pass with
Flang 20; GNU 13 passes with both 32-bit and 64-bit integer APIs.
@rmlarsen
rmlarsen force-pushed the larfg-top-end-scaling branch from 809532e to f9d174c Compare September 15, 2026 20:28
@rmlarsen

Copy link
Copy Markdown
Contributor Author

Maintainers: It looks like the valgrind job died due to an infra error. Please rerun.

@martin-frbg

Copy link
Copy Markdown
Collaborator

I've restarted it now

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