Skip to content

Undo the scaling of the least-squares solution in one step when A and B were scaled to the same end of the range - #1391

Open
rmlarsen wants to merge 1 commit into
Reference-LAPACK:masterfrom
rmlarsen:ls-drivers-undo-scaling
Open

rmlarsen wants to merge 1 commit into
Reference-LAPACK:masterfrom
rmlarsen:ls-drivers-undo-scaling

Conversation

@rmlarsen

@rmlarsen rmlarsen commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Disclaimer: This PR was prepared using Claude Code.

Summary

The six least-squares drivers xGELS, xGELST, xGETSLS, xGELSY, xGELSD and xGELSS scale A and B into [SMLNUM, BIGNUM] before solving and undo the two scalings on the solution one after the other. When A and B were both scaled to the same end of the range the two factors share a constant that cancels, but the first step is applied on its own and can flush a solution entry to zero (both above BIGNUM) or overflow it (both below SMLNUM) before the second step would have brought it back. For A = 2^1023 I, b = (2^1023, 2^-27), whose solution is x = (1, 2^-1050), all 24 drivers return x = (1, 0) with INFO = 0. This PR applies the quotient of the two factors in one step in that case; nothing changes when at most one of A, B is scaled or they are scaled to opposite ends.

Description

With IASCL = IBSCL = 2 the drivers compute x = x' * (BIGNUM/ANRM) * (BNRM/BIGNUM). The first factor is as small as 2^-54, so any entry of x' below 2^-1020 (2^-125 in single precision) is rounded to zero, and the second factor cannot recover it. The mirror case IASCL = IBSCL = 1 multiplies by SMLNUM/ANRM, up to 2^104, first; an intermediate overflow there needs x' above 2^920 with b below SMLNUM, which a full-rank problem cannot produce, so it is merged for symmetry. The two mixed cases apply two factors in the same direction and are left as they are. The change was found while reviewing #1383, whose first revision copied this block.

DGELS on A = 2^1023 I, b = (2^1023, 2^-27)
master x = (1, 0), INFO = 0
this branch x = (1, 2^-1050), INFO = 0 (exact)

Fix. In each driver, when IASCL .EQ. IBSCL .AND. IASCL .NE. 0, call xLASCL( 'G', 0, 0, ANRM, BNRM, ... ) once on the solution instead of the two calls; otherwise the existing per-flag calls run unchanged. xLASCL never overshoots its target, so the single call cannot flush or overflow an entry whose final value is representable. In xGELSY, xGELSD and xGELSS the rescaling of R or S, which involves the factor of A alone, is moved into its own IF after the solution block; it is unchanged. Where both factors apply, the solution is now rounded once instead of twice, so it can differ from master in the last bit. 24 files: {s,d,c,z}{gels,gelst,getsls,gelsy,gelsd,gelss}.f.

Minimal reproducer (repro/undo_cases.f90 runs the case, and its mirror below SMLNUM, through all 24 drivers)

program flush
  implicit none
  double precision :: a(2,2), b(2), work(100)
  integer :: info
  a = 0d0; a(1,1) = scale(1d0, 1023); a(2,2) = a(1,1)
  b = [scale(1d0, 1023), scale(1d0, -27)]           ! x = (1, 2^-1050)
  call dgels('N', 2, 2, 1, a, 2, b, 2, work, 100, info)
  print '(a,i0,a,2es12.4)', 'DGELS: info = ', info, '  x = ', b
end program
BEFORE (master):      DGELS: info = 0  x =   1.0000E+00  0.0000E+00
AFTER (this branch):  DGELS: info = 0  x =   1.0000E+00  8.2890-317

Validation

Exponent sweep, six drivers, four precisions, 2028 cases

repro/ls_sweep.f90 runs each driver on a well-conditioned 6-by-4 problem with A scaled by 2^ka and b by 2^kb over a grid of exponents from the subnormal range to the overflow threshold (double: -1070 to 1021; single: -148 to 125), skipping pairs whose exact solution is not representable, and compares the solution with the unscaled twin after rescaling; x(1) is printed in hex for a bit-for-bit diff against master. run_sweep.sh reproduces the table.

cases code path unchanged of which bit-identical to master both scaled to the same end of which bit-identical max deviation from the twin, master / branch failures, master / branch
S/C drivers (12) 86 each 68 68 18 2 to 14 1.3e-6 / 1.3e-6 0 / 0
D/Z drivers (12) 83 each 65 65 18 5 to 16 2.8e-14 / 2.8e-14 0 / 0

Every case whose code path is unchanged is bit-identical to master. In the merged cases the solution is rounded once instead of twice; the cases that still agree with master to the bit are those whose two norms have a power-of-two quotient. The twin problem has no solution entry near the underflow threshold, so the flush itself is covered by undo_cases.f90, not by the sweep.

Test suite. The full LAPACK test suite passes on this branch: 215 of 215 CTest entries, 5441901 LAPACK tests and 315872 BLAS tests with 0 numerical errors and 0 other errors, the same totals as the parent commit f96546fc9 built and run the same way (GCC 13.3, CMAKE_BUILD_TYPE=Release, BUILD_INDEX64_EXT_API=ON).

Regression test. xQRT13 scales its "scaled up" matrix to 1/(SFMIN/EPS) = 2^969, and xDRVLS now scales it to a fixed 2^1016 instead, above the drivers' threshold, so that the scaling block runs at all. In the scaled-up types xDRVLS also multiplies the last column of the exact solution by 256*SFMIN before forming the right-hand side, which puts it below what the first of the two undo steps can represent. xQRT16 normalizes its residual per right-hand side, so the flushed column is not hidden by the other columns: on the parent commit the xGELS, xGELST and xGETSLS blocks fail between 2395 and 5041 ratios per precision, and with the fix all four precisions pass. Raising the scale also gives IASCL and IBSCL their first coverage in the six drivers.

Why the test suite never saw it. The least-squares tests in xDRVLS take their "scaled up" and "scaled down" matrices from xQRT13, which scales the largest entry to 1/(SFMIN/DLAMCH('Epsilon')) = 2^969 (down: 2^-969), while the drivers scale when the largest entry is outside [DLAMCH('S')/DLAMCH('P'), 1/that] = [2^-970, 2^970]. The test matrices sit one binade inside the window on both sides, so IASCL and IBSCL are 0 for every matrix the suite generates and the undo block at the end of the drivers has never run under the suite. Moving the test scale one binade outward and giving one right-hand side a solution below 2^-1020 makes xQRT16's per-column residual catch the flush (ratios of 10^3 to 10^5 on master against a threshold of 30); that test-suite change is prepared separately so that this fix stays a 24-file source change.

Performance. No change on the common path: the merged call replaces two xLASCL calls by one and runs only when both matrices were scaled to the same end of the range.

Checklist

  • The documentation has been updated. (No interface 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.)

@rmlarsen

rmlarsen commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

For the reviewers, on ordering: this PR is independent of #1383 (it was found while reviewing that PR's first revision, which had copied the two-step undo) and of #1381 and #1382, and merges cleanly with each of them in either order. It shares {s,d,c,z}gelsd.f with #1382, which only changes the documentation of INFO at the top of those files; this PR changes the undo block at the bottom, and a test merge of the two branches has no conflicts.

@codecov

codecov Bot commented Sep 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 70.00000% with 78 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.36%. Comparing base (a6c6e74) to head (7668d18).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
SRC/cgelsd.f 60.00% 4 Missing ⚠️
SRC/cgelss.f 60.00% 4 Missing ⚠️
SRC/cgelsy.f 60.00% 4 Missing ⚠️
SRC/dgelsd.f 60.00% 4 Missing ⚠️
SRC/dgelss.f 60.00% 4 Missing ⚠️
SRC/dgelsy.f 60.00% 4 Missing ⚠️
SRC/sgelsd.f 60.00% 4 Missing ⚠️
SRC/sgelss.f 60.00% 4 Missing ⚠️
SRC/sgelsy.f 60.00% 4 Missing ⚠️
SRC/zgelsd.f 60.00% 4 Missing ⚠️
... and 14 more
Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff            @@
##           master    #1391    +/-   ##
========================================
  Coverage   69.36%   69.36%            
========================================
  Files        6122     6122            
  Lines      486711   486815   +104     
  Branches    23268    23268            
========================================
+ Hits       337584   337702   +118     
+ Misses     148689   148675    -14     
  Partials      438      438            
Components Coverage Δ
BLAS 97.94% <ø> (ø)
CBLAS 96.98% <ø> (ø)
LAPACK 82.40% <65.78%> (+0.01%) ⬆️
LAPACKE 2.17% <ø> (ø)
TMGLIB 55.69% <ø> (ø)
BLAS testing 88.33% <ø> (ø)
CBLAS testing 89.63% <ø> (ø)
LAPACK testing 82.21% <100.00%> (+<0.01%) ⬆️
LAPACKE testing ∅ <ø> (∅)
Files with missing lines Coverage Δ
TESTING/LIN/cdrvls.f 89.80% <100.00%> (+0.24%) ⬆️
TESTING/LIN/cqrt13.f 100.00% <100.00%> (ø)
TESTING/LIN/ddrvls.f 89.73% <100.00%> (+0.24%) ⬆️
TESTING/LIN/dqrt13.f 100.00% <100.00%> (ø)
TESTING/LIN/sdrvls.f 89.73% <100.00%> (+0.24%) ⬆️
TESTING/LIN/sqrt13.f 100.00% <100.00%> (ø)
TESTING/LIN/zdrvls.f 89.80% <100.00%> (+0.24%) ⬆️
TESTING/LIN/zqrt13.f 100.00% <100.00%> (ø)
SRC/cgels.f 93.75% <88.88%> (-0.80%) ⬇️
SRC/zgels.f 93.75% <88.88%> (-0.80%) ⬇️
... and 22 more

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...7668d18. Read the comment docs.

@rmlarsen
rmlarsen force-pushed the ls-drivers-undo-scaling branch from 5f4988f to 4bc258e Compare September 8, 2026 04:11
@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.

… B were scaled to the same end of the range

xGELS, xGELST, xGETSLS, xGELSY, xGELSD and xGELSS scale A and B into
[SMLNUM, BIGNUM] before solving and undo the two scalings on the
solution one after the other.  When both were scaled to the same end
the factors share a constant that cancels, but the first step runs on
its own: with both above BIGNUM it multiplies by BIGNUM/ANRM, as small
as 2^-54, and flushes any solution entry below 2^-1020 that the second
step, BNRM/BIGNUM, would have restored.  For A = 2^1023 I and
b = (2^1023, 2^-27), whose solution is (1, 2^-1050), all 24 drivers
return (1, 0) with INFO = 0.  The mirror case below SMLNUM applies the
large factor first and could overflow, though a full-rank problem
cannot reach it.

When IASCL = IBSCL /= 0, apply the quotient BNRM/ANRM in one xLASCL
call, which never overshoots its target; the other combinations apply
two factors in the same direction and are unchanged.  In xGELSY,
xGELSD and xGELSS the rescaling of R or S by the factor of A alone
moves into its own IF and is unchanged.  Where both factors apply the
solution is rounded once instead of twice and can differ from the
previous result in the last bit.

Found while reviewing the first revision of the xGGLSE/xGGGLM scaling
change, which copied this block.

The test suite never ran the block: xQRT13 scales its "scaled up"
matrix to 1/(SFMIN/EPS) = 2^969, one binade inside the drivers'
threshold of 2^970, so IASCL and IBSCL were zero for every matrix it
generates.  xQRT13 now scales up to a fixed 2^1016 instead, and xDRVLS
makes the last column of the exact solution 256*SFMIN in the scaled-up
types, which is small enough for the first of the two undo steps to
flush it.  xQRT16 normalizes its residual per right-hand side, so the
lost column is visible: the parent fails between 2395 and 5041 ratios
per precision, this branch none.

Over a sweep of 2028 (precision,
driver, exponent of A, exponent of b) cases every case whose code path
is unchanged is bit-identical to the parent, and the merged cases agree
with the unscaled twin as closely as before.  The full LAPACK test
suite passes with the same totals as the parent.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@rmlarsen
rmlarsen force-pushed the ls-drivers-undo-scaling branch from 4bc258e to 7668d18 Compare September 15, 2026 20:28
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