Skip to content

Scale the inputs of xGGLSE and xGGGLM into the safe range, as the other least-squares drivers do - #1383

Open
rmlarsen wants to merge 1 commit into
Reference-LAPACK:masterfrom
rmlarsen:gglse-ggglm-scaling
Open

Scale the inputs of xGGLSE and xGGGLM into the safe range, as the other least-squares drivers do#1383
rmlarsen wants to merge 1 commit into
Reference-LAPACK:masterfrom
rmlarsen:gglse-ggglm-scaling

Conversation

@rmlarsen

@rmlarsen rmlarsen commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Disclaimer: This PR was prepared using Claude Code.

Summary

xGELS, xGELST, xGETSLS, xGELSY, xGELSD and xGELSS scale A and the right-hand side into [SMLNUM, BIGNUM] before factoring and undo the scaling on the solution. xGGLSE and xGGGLM are the only least-squares drivers that do not, so a problem whose entries approach the overflow threshold is factored and solved as given, and a well-conditioned problem scaled by 2^1021 comes back as NaN, or as a finite wrong x, with INFO = 0. This PR adds the scaling block of xGELS to both drivers. Nothing changes for a problem whose entries already lie in [SMLNUM, BIGNUM].

Description

For min ||c - A x|| subject to B x = d with a well-conditioned 6-by-4 A, 2-by-4 B and consistent c, d, all scaled by 2^1021 (largest entry 1e308); the solution is invariant under that scaling:

INFO x
master 0 NaN NaN NaN NaN
this branch 0 0.1 0.2 0.3 0.4 (exact solution)

DGGLSE computes the GRQ factorization of (B, A) in place, and its Householder generators overflow first (a separate PR); with that fixed, the reflector applications in xORMQR, the triangular solves and the matrix-vector products still overflow, so the drivers need the same protection the other least-squares drivers have. The documentation of both routines describes their rank checks as rudimentary but says nothing about range.

Fix. After the quick return, each driver scales A, B and the right-hand sides by exact powers of two so that their largest entries lie in [SMLNUM, BIGNUM): the shift of each operand is computed from the EXPONENT of its max-norm, applied with xLASCL (matrices) or xSCAL (vectors), and undone on exit by the opposite shift. A and B get independent factors 2^KA and 2^KB. In xGGLSE, c is scaled by 2^(KA+KT) and d by 2^(KB+KT), which keeps the constraint B x = d and the residual c - A x consistent and scales x by 2^KT; KT brings the larger of the two right-hand sides, each measured against its matrix, into range. On exit x is rescaled by 2^-KT and the residual left in elements N-P+1 to M of c by 2^-(KA+KT); the intermediate left in c(1:N-P) is not touched. In xGGGLM the three operands are independent: d is scaled by 2^KD, and x and y are rescaled by 2^(KA-KD) and 2^(KB-KD). Scaling by a power of two is exact, so a problem the unscaled computation can handle returns the same solution to the last bit (sweep below), and each undo is one exact operation. A norm that is zero, infinite or NaN takes no part. xLASCL is called with both endpoints at or above one, so forming the factor raises no spurious underflow, and a problem whose entries lie in [SMLNUM, BIGNUM) calls neither xLASCL nor xSCAL. As in xGELS, the factorizations returned in A and B are those of the scaled matrices when scaling took place. xLANGE( 'M' ) does not touch its work argument, so no workspace is needed and the workspace query is unchanged. Eight files: {s,d,c,z}gglse.f, {s,d,c,z}ggglm.f.

Known limit, the same as in xGELS: when a right-hand side has to be scaled down (largest entry above BIGNUM), its entries more than about 2^1990 below that entry lose precision and entries more than 2^2044 below it are lost; in xGGLSE the same holds between 2^KA c and 2^KB d. In xGGGLM, where d = A x + B y carries the scales of both A and B, a d whose B y part is that far below its A x part loses it: A = 2^1023, B = 2^-1023, d = (2^1023, 2^-1023) returns y = 0 instead of 1. Such a d is representable only when the two parts occupy separate entries, as here; master returns y = 1 because the factorization of that A is trivial.

Revision 2. A review of the first revision found three regressions against master, all reproduced by repro/review_cases.f90:

  1. A and B shared one factor from their combined max-norm, so for A = 2^1023, B = 2^-1023, c = 1, d = 2^-1023 (exact solution x = 1) the scaling rounded B to zero and the driver returned INFO = 1.
  2. The two undo scalings of x cancelled through an intermediate that could flush: x = (1, 2^-1050) came back as (1, 0).
  3. All of c was rescaled although only c(N-P+1:M) holds the residual; the intermediate in c(1:N-P) could overflow, and trap, on a problem master solves.

The revision fixes the three by construction (independent factors for A and B; exact factors undone in one operation; only the residual part of c rescaled). The five xGGLSE cases of review_cases.f90, the three findings and two neighbouring corners, agree with master bit for bit and raise the same IEEE flags. The same two-step undo exists in the six xGELS-family drivers (xGELS, xGELST, xGETSLS, xGELSY, xGELSD, xGELSS), whose first step flushes for the same input; #1391 fixes it there. The two PRs are independent and can land in either order.

Minimal reproducer

program minimal
  implicit none
  integer, parameter :: m = 6, n = 4, p = 2
  double precision :: a(m, n), b(p, n), c(m), d(p), x(n), xex(n), work(4096), s
  integer :: i, j, info
  do j = 1, n
    xex(j) = j / 10d0
    do i = 1, m
      a(i, j) = 1 + mod(3*i + 5*j, 7) / 7d0
      if (i == j) a(i, j) = a(i, j) + 3
    end do
    do i = 1, p
      b(i, j) = 1 + mod(2*i + 3*j, 5) / 5d0
    end do
  end do
  c = matmul(a, xex); d = matmul(b, xex)
  s = scale(1d0, 1021)
  a = a * s; b = b * s; c = c * s; d = d * s
  call dgglse(m, n, p, a, m, b, p, c, d, x, work, 4096, info)
  print '(a,i0,a,4es12.4)', 'DGGLSE: info = ', info, '  x = ', x
end program
BEFORE (master):      DGGLSE: info = 0  x =          NaN         NaN         NaN         NaN
AFTER (this branch):  DGGLSE: info = 0  x =   1.0000E-01  2.0000E-01  3.0000E-01  4.0000E-01

Validation

Exponent sweep of both drivers, all precisions, 2860 cases

repro/gg_sweep.f90 runs xGGLSE on the shapes (M,N,P) = (6,4,2), (4,4,4), (5,4,0), (2,4,4), (3,3,1) and xGGGLM on (N,M,P) = (6,4,6), (6,4,2), (5,5,0), (6,0,6), (3,1,3) in all four precisions, with the matrices scaled by 2^ka and the right-hand sides 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. The solution of each case is compared with the unscaled twin after rescaling, and printed in hex for a bit-for-bit comparison with master. Cases whose inputs carry fewer than about 12 significant bits (deep subnormal) are excluded from the accuracy judgment.

routine cases no scaling applied of which bit-identical to master master fails this branch fails
SGGLSE / CGGLSE 315 each 70 70 25 0
DGGLSE / ZGGLSE 400 each 95 95 25 0
SGGGLM / CGGGLM 315 each 70 70 15 0
DGGGLM / ZGGGLM 400 each 95 95 15 0

A case fails when INFO /= 0, the solution contains NaN or Inf, or it deviates from the unscaled twin by more than 5e-13 (2e-5 single; 2e-11 and 2e-3 when the inputs are subnormal and carry fewer bits). Of the 1480 cases with inputs in the normal range in which scaling is applied, this branch returns the solution of the unscaled twin bit for bit in 1388; the other 92 (single precision, and ZGGGLM with an exponent beyond 486) have entries beyond the thresholds at which xNRM2 switches accumulators, and agree with the twin to 1.6e-6 (single) and 7e-22 (double) relative to the largest solution entry. Master returns the twin bit for bit in 1124 of the 1480 and fails in 160. All of master's failures are at the largest matrix exponent (2^1021, or 2^125 in single precision); the xGGGLM failures there include finite, wrong solutions returned with INFO = 0. run_sweep.sh reproduces the table.

Regression test. xGLMTS and xLSETS solve their problem a second time with every operand scaled by one power of two, so that the largest entry sits just below the overflow threshold. The problem is exactly invariant under that scaling, so the residual of the second solution against the original data has to match the first, and a NaN result counts as a failure, which the plain comparison with the threshold would pass over.

On the parent commit the GLM twin fails one of the 48 ratios in every precision, and with the fix all of them pass. The LSE twin passes on both: there the right-hand side is the largest operand, so scaling it to the top of the range leaves the matrix below the exponent at which the reflector generator overflows. It covers the new scaling path rather than the defect, which is what the existing types could not reach at all, since xLATB9 fixes the norms of A and B at 10 and 1000 for these two paths.

Test suite. The full LAPACK test suite passes on this branch, including the xLSE and xGLM tests of the eigenvalue suite: 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).

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 (m = 2n, p = n/2) parent this branch ratio
DGGLSE (us/call) 16 8.1 9.5 1.17
64 318 331 1.04
256 21429 21616 1.01
512 170506 170767 1.00
DPOTRF control (us/call) 64 / 256 / 1024 0.99 / 0.97 / 1.00

The cost is the four max-norm scans over A, B, c and d (O(mn + pn) against the O(mn^2) factorization), about 1.4 us on the 32-by-16 problem and within the measurement noise from n = 256 on; it is the same work xGELS does. Nothing else runs on a problem whose entries lie in [SMLNUM, BIGNUM). (Revision 2 numbers; revision 1 measured 1.17 / 1.06 / 1.01 / 1.01.)

Checklist

  • The documentation has been updated. (No interface change; the routines' documentation does not describe the scaling, like xGELS.)
  • 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 92.37288% with 36 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.38%. Comparing base (f96546f) to head (c7a7e38).
⚠️ Report is 51 commits behind head on master.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
SRC/cggglm.f 90.90% 3 Missing ⚠️
SRC/cgglse.f 92.50% 3 Missing ⚠️
SRC/dggglm.f 90.90% 3 Missing ⚠️
SRC/dgglse.f 92.50% 3 Missing ⚠️
SRC/sggglm.f 90.90% 3 Missing ⚠️
SRC/sgglse.f 92.50% 3 Missing ⚠️
SRC/zggglm.f 90.90% 3 Missing ⚠️
SRC/zgglse.f 92.50% 3 Missing ⚠️
TESTING/EIG/clsets.f 91.30% 2 Missing ⚠️
TESTING/EIG/dlsets.f 91.30% 2 Missing ⚠️
... and 6 more
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1383      +/-   ##
==========================================
+ Coverage   69.01%   69.38%   +0.36%     
==========================================
  Files        6122     6122              
  Lines      486123   486809     +686     
  Branches    23286    23268      -18     
==========================================
+ Hits       335514   337766    +2252     
+ Misses     150420   148605    -1815     
- Partials      189      438     +249     
Components Coverage Δ
BLAS 97.94% <ø> (ø)
CBLAS 96.98% <ø> (+<0.01%) ⬆️
LAPACK 82.40% <91.78%> (+0.02%) ⬆️
LAPACKE 2.17% <ø> (+2.07%) ⬆️
TMGLIB 55.69% <ø> (ø)
BLAS testing 88.33% <ø> (ø)
CBLAS testing 89.63% <ø> (ø)
LAPACK testing 82.26% <93.33%> (-0.10%) ⬇️
LAPACKE testing ∅ <ø> (∅)
Files with missing lines Coverage Δ
TESTING/EIG/cglmts.f 95.23% <95.45%> (+0.23%) ⬆️
TESTING/EIG/dglmts.f 95.23% <95.45%> (+0.23%) ⬆️
TESTING/EIG/sglmts.f 95.23% <95.45%> (+0.23%) ⬆️
TESTING/EIG/zglmts.f 95.23% <95.45%> (+0.23%) ⬆️
TESTING/EIG/clsets.f 94.28% <91.30%> (-5.72%) ⬇️
TESTING/EIG/dlsets.f 94.28% <91.30%> (-5.72%) ⬇️
TESTING/EIG/slsets.f 94.28% <91.30%> (-5.72%) ⬇️
TESTING/EIG/zlsets.f 94.28% <91.30%> (-5.72%) ⬇️
SRC/cggglm.f 84.21% <90.90%> (+3.56%) ⬆️
SRC/cgglse.f 89.32% <92.50%> (+2.01%) ⬆️
... and 6 more

... and 148 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 f96546f...c7a7e38. Read the comment docs.

…y, by exact powers of two

Review of the previous revision found three regressions against the
unscaled drivers.  A and B shared one factor from their combined
max-norm, so A = 2^1023, B = 2^-1023, d = 2^-1023 (x = 1) had B rounded
to zero and returned INFO = 1.  The two undo scalings of x cancelled
through an intermediate that could flush, so x = (1, 2^-1050) came back
as (1, 0).  All of c was rescaled although only c(N-P+1:M) holds the
residual, and the intermediate left in c(1:N-P) could overflow.

Scale each operand by a power of two chosen from the EXPONENT of its
max-norm instead.  A and B get independent factors 2^KA and 2^KB.  In
xGGLSE, c carries 2^(KA+KT) and d carries 2^(KB+KT), which keeps the
constraint and the residual consistent and scales x by 2^KT, with KT
chosen to bring the larger right-hand side into range; on exit x is
rescaled by 2^-KT and the residual in c(N-P+1:M) by 2^-(KA+KT).  In
xGGGLM, A, B and d are scaled independently and x and y are rescaled by
2^(KA-KD) and 2^(KB-KD).  The factors are exact, so a problem the
unscaled computation handles returns the same solution to the last bit,
and each undo is one exact operation.  xLASCL is called with both
endpoints at or above one so that forming the factor raises no
underflow; vectors are scaled with xSCAL.  A zero, infinite or NaN norm
takes no part.

The three review cases and two neighbouring corners agree with the
parent bit for bit and raise the same IEEE flags.  Over the exponent
sweep of 2860 cases the branch fails in none (the parent in 160) and,
in the 1480 scaled cases with normal-range inputs, returns the solution
of the unscaled twin bit for bit in 1388; the rest have entries beyond
the thresholds at which xNRM2 switches accumulators and agree with the
twin to rounding.

xGLMTS and xLSETS solve their problem a second time with every operand
scaled by one power of two, so that the largest entry sits just below
the overflow threshold.  The problem is exactly invariant under that
scaling, so the residual of the second solution against the original
data has to match the first; a NaN counts as a failure, which the
comparison with the threshold would otherwise pass over.  On the parent
commit the GLM twin fails one of the 48 ratios in every precision.  The
LSE twin passes on both, because the right-hand side is the largest
operand there and scaling it to the top of the range leaves the matrix
below the exponent at which the reflector generator overflows; it
covers the new scaling path rather than the defect.

The full LAPACK test suite passes with the same totals as the parent.

xGLMTS and xLSETS declare the new xSCAL calls EXTERNAL: the
extended-API build renames only the routines a file declares, so
without the declaration the xeigtst*_64 executables failed to link
against the 64-bit BLAS.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@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.

Two differences from x86:

  • On master, the DGGLSE reproducer above does not fail on this machine; it returns the exact solution. The corresponding DGGGLM problem at the same scale does fail on master (NaN with INFO = 0) and is exact with the fix.
  • Without the fix, the new GLM twin test fails only in the complex precisions (CGLM and ZGLM); the real ones pass. The LSE twin passes with and without the fix, as described.

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