Skip to content

Updating Computing the T factor for OR/UNGQR family, - #1373

Open
jprhyne wants to merge 15 commits into
Reference-LAPACK:masterfrom
jprhyne:ut-based-larft
Open

Updating Computing the T factor for OR/UNGQR family,#1373
jprhyne wants to merge 15 commits into
Reference-LAPACK:masterfrom
jprhyne:ut-based-larft

Conversation

@jprhyne

@jprhyne jprhyne commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Description

In order to make my ORGQR optimization PR more digestible and revertible, I am breaking up my original draft PR into different parts. This part is concerned with the computation of the triangular factor and adding the UT-based transform discussed
by Joffrain et. al: https://doi.org/10.1145/1141885.1141886

Following their notation, we will refer to $S$ as the triangular factor that must be applied with a solve and $T$ to be the triangular factor that must be applied with a multiplication (Note that $T=S^{-1}$)

We added a few new blas-like kernels (listed first) that are not currently optimized by vendors but rely on existing routines where possible. These kernels are required for either using the new factor $S$ or computing the existing factor $T$.

For all recursive kernels, we also provide a level2 implementation, pick a bailing condition that works fine on my laptop but no further investigation on other machines. We do not list all the level2 implementations below, but they are a part of this PR.

{c,z}hetrrk: A complex hermitian triangular rank k update assuming the update matrix is triangular (thank you Mathieu Faverge for the naming!)
{s,d}sytrrk: A real symmetric triangular rank k update assuming the update matrix is triangular

{s,d,c,z}trmmoop: An out of place triangular-general matrix product. Similar to a trmm + an add.

{s,d,c,z]trmvoop: An out of place triangular-vector product. Similar to a trmv + axpy

{c,z}acxpy: computing $\mathbf{y}=\alpha\text{conjg}\left(\mathbf{x}\right) + \mathbf{y}$

{c,z,s,d}trsm_mod: A modified triangular solve that allows for solving singular systems with the assumption that A has a zero row/column where there are explicit 0s on the diagonal and that B has the necessary values equal to 0. We do not enforce this condition in the code. The purpose of this is to be able to use the new larft_ut scheme by applying $S (T^{-1})$ via a triangular solve instead of inverting the matrix explicitly. Not currently used, but is necessary if you want to use this new triangular factor computed with larft_ut on entry with apply_t = 's'

{c,z,s,d}trtri_mod: A modified recursive triangular inverse to mimic the behavior of larft when inverting the triangular factor before returning.

{c,z,s,d}trti2_mod: A modified level2 triangular inverse to mimic the behavior of larft_lvl2 when inverting the triangular factor before returning.

The previous two routines are necessary when we generate a reflector that doesn't do anything (IE: $\tau=0$ from larfg) There are two main cases we found that use this:

  1. generating the reflector for the final column for a square input matrix ($m=n=k$ in or/ungqr)
  2. when A was already triangular (or A has a column that is already in the proper form at any step of the process)

When the triangular matrix is non-singular (ie all $\tau$ values are non-zero), then it is equivalent to the existing routines trtri and trti2

Another option would be to change larfg to return a reflector with $\tau=2$, but this would cause a triangular input to have it's diagonals sign's flipped, which is not necessarily desirable.

The following existing routines were modified as well:

{c,z,s,d}larft:

  1. We added 2 new paths that allow for computing the (conjugate) transpose of the triangular factor used in the LQ and RQ factorizations. These paths are not yet needed, but will be a part of a future PR where we optimize the Q generation family, I felt it best to include these paths here as it affects the triangular factor generation. I can wait to add these paths until we explicitly need them if desired.
  2. Changed the base case of larft to use larft_ut as some experiments we have show the ut-based version is more performant for certain matrix sizes. This can be removed or changed back to level2 if desired.

Checklist

  • The documentation has been updated.
  • If the PR solves a specific issue, it is set to be closed on merge. (No Issue for this PR exists to my knowledge)

Johnathan Rhyne added 6 commits August 25, 2026 09:38
@codecov

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 30.81603% with 1933 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.66%. Comparing base (dd51569) to head (e32e0a8).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
SRC/dtrmvoop.f 20.80% 217 Missing ⚠️
SRC/ctrmvoop.f 0.00% 139 Missing ⚠️
SRC/dtrmmoop.f 0.00% 126 Missing ⚠️
SRC/strmmoop.f 0.00% 126 Missing ⚠️
SRC/ctrsm_lvl2_mod.f 26.03% 125 Missing ⚠️
SRC/ctrmmoop.f 0.00% 124 Missing ⚠️
SRC/ctrmcvoop.f 26.27% 101 Missing ⚠️
SRC/ctrmmoop_lvl2.f 20.90% 87 Missing ⚠️
SRC/dtrsm_lvl2_mod.f 33.59% 85 Missing ⚠️
SRC/strsm_lvl2_mod.f 33.59% 85 Missing ⚠️
... and 23 more
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1373      +/-   ##
==========================================
- Coverage   69.01%   68.66%   -0.36%     
==========================================
  Files        6122     6166      +44     
  Lines      486123   489976    +3853     
  Branches    23286    23286              
==========================================
+ Hits       335514   336418     +904     
- Misses     150420   153369    +2949     
  Partials      189      189              
Components Coverage Δ
BLAS 97.96% <ø> (+0.02%) ⬆️
CBLAS 96.98% <ø> (ø)
LAPACK 81.20% <30.81%> (-1.18%) ⬇️
LAPACKE 0.10% <ø> (ø)
TMGLIB 55.69% <ø> (ø)
BLAS testing 88.33% <ø> (ø)
CBLAS testing 89.63% <ø> (ø)
LAPACK testing 82.35% <ø> (ø)
LAPACKE testing ∅ <ø> (∅)
Files with missing lines Coverage Δ
SRC/strti2_mod.f 75.75% <ø> (ø)
SRC/strtri_mod.f 80.00% <ø> (ø)
SRC/zacxpy.f 81.81% <ø> (ø)
SRC/zhetrrk.f 17.07% <ø> (ø)
SRC/zhetrrk_lvl2.f 37.11% <ø> (ø)
SRC/zlarft.f 74.68% <ø> (-25.32%) ⬇️
SRC/zlarft_ut.f 75.00% <ø> (ø)
SRC/ztrmcvoop.f 26.27% <ø> (ø)
SRC/ztrmmoop.f 0.00% <ø> (ø)
SRC/ztrmmoop_lvl2.f 20.90% <ø> (ø)
... and 38 more

... and 5 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 dd51569...e32e0a8. Read the comment docs.

@ACSimon33

Copy link
Copy Markdown
Collaborator

@jprhyne fyi, the failing extended-api-only tests, mean that some routines are missing external declarations for functions or subroutines that are used. For example, SSYTRRK uses SSYTRRK_LVL2 but never declares it.

@jprhyne

jprhyne commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Thank you! I forgot to add those! I'll get that fixed and pushed shortly

@langou

langou commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

It is a beautiful pull request @jprhyne. Good job! I love it. ❤️

@ACSimon33

ACSimon33 commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

@jprhyne I think you could use xLAUMM instead of xSYTRRK and xHETRRK:

-         CALL DSYTRRK('Lower', 'Upper', 'Transpose', 'Unit', K, ONE,
-     $         V, LDV, ZERO, T, LDT)
+         DO J = 1, K
+            T(J,J) = ONE
+            DO I = 1, J-1
+               T(I,J) = V(J,I)
+            END DO
+         END DO
+         CALL DLAUUM('Upper', K, T, LDT, INFO

That way we could get rid of these new BLAS-like routines:

{s,d,c,z}trmmoop.f
{s,d,c,z}trmmoop_lvl2.f
{s,d,c,z}trmvoop.f
{c,z}trmcvoop.f
{c,z}acxpy.f
{s,d}sytrrk.f  {c,z}hetrrk.f
{s,d}sytrrk_lvl2.f  {c,z}hetrrk_lvl2.f

As far as I can tell, we don't need those anywhere else. So adding them just to replicate this existing functionality seems a bit overkill and redundant to me. Performance-wise, it would be the same, since xLAUMM is also blocked.

@ACSimon33

Copy link
Copy Markdown
Collaborator

@jprhyne You can ignore the errors in the AppVeyor pipelines. They are caused by the recent LLVMFlang 23.1.0 release, which seems to break some Fortran standard module paths...

@jprhyne

jprhyne commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

@ACSimon33 Thanks for that catch!

I planned to use the LQT and RQT paths in a future PR breaking up #1112, which is where xLAUM is not sufficient. For these two cases we do need x{sy,he}trrk as the former requires the output be the same shape as the input.

While these paths are not hit yet by any code (they will be in a future PR I want to make), this behavior is already present in xLARFT, which would call xLARFT_UT as a terminating case. So, I'd like to keep the same functionality (plus keeping all my changes to LARFT in one PR)

If it is desired, I am more than happy to remove this behavior from just xLARFT_UT and only add it in when it's actually called inside a file here.

@jprhyne

jprhyne commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

I feel that I should explain more about why I think xLAUUM is insufficient in case I am missing something here.

For the RQT case we have the requirement that T ends up as upper triangular without modifying the strictly lower triangular part of T at any point. We need to first compute

T = ut(V_1 * V_1**H)

where ut(.) denotes the upper triangular part of a matrix (including diagonal) and V_1 is a unit lower triangular matrix.

In order to use xLAUUM here, we would need to store an upper triangular matrix in T since xLAUUM requires the input triangle is the same shape as the output shape.

However, the only upper triangular matrix available is V_1**H.

This means that we would need to compute

T = ut( U**H * U ), which is not supported by xLAUUM as it only computes
T = ut( U * U**H)

@jprhyne

jprhyne commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

@ACSimon33 I just noticed after I sent that previous comment, we also need this new routine for the QL case (so I revise my statement about being willing to wait to add these routines, I propose we need them here)

In the QL case we must end with T being lower triangular without modifying the strictly upper triangular part. So we must compute

T = lt(V_1**H * V_1)

where lt(.) is the lower triangular part and V_1 is unit upper triangular.

Since we need to store the lower triangular component, we must enter xLAUUM with T holding a lower triangular matrix, and the only one available in this operation is V_1**H. However we would then need to compute

T = lt(L * L**H) which is not supported by xLAUUM

unless there's another routine that I've missed that does compute these two operations

Comment thread SRC/dsytrrk.f Outdated
Comment thread SRC/dlarft.f
* Base case
*
IF(N.EQ.1.OR.K.EQ.1) THEN
IF(K.EQ.1) THEN

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Only this precision on the routine removed the N.EQ.1.OR. from the quick return (also see line 207: N.EQ.0.OR.). That can result in garbage output or errors:

DLARFT N=1, K=2 -> ** On entry to DSYRK parameter number 4 had an illegal value
DLARFT N=0, K=2 -> ** On entry to DSYRK parameter number 4 had an illegal value

Comment thread SRC/ctrsm_mod.f
INFO = 11
END IF
IF (INFO.NE.0) THEN
CALL XERBLA('DTRSM ',INFO)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

XERBLA('DTRSM ') called from CTRSM_MOD. This is one of 15 sites where the name string does not match the routine:

{s,d,c,z}trsm_mod.f:234        XERBLA('DTRSM ')
{s,d,z}trsm_lvl2_mod.f:243     XERBLA('DTRSM ')
ctrsm_lvl2_mod.f:243           XERBLA('CTRSM ')
{s,d,c,z}trti2_mod.f:161       XERBLA('DTRTI2')
{s,c,z}trtri_mod.f:158         XERBLA('DTRTRI_MOD')

Comment thread SRC/dlarft_ut.f
* Definition:
* ===========
*
* SUBROUTINE DLARFT_UT( DIRECT, STOREV, N, K, V, LDV, TAU, T, LDT )

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

APPLYT missing. See other precisions as well.

Comment thread SRC/dlarft.f
*>
*> \verbatim
*>
*> DLARFT forms the triangular factor T of a real block reflector H

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The new DIRECT=T and STOREV=T options are not documented in {s,d,c,z}larft.f and {s,d,c,z}larft_ut.f. It would be good for users to know, since these are public API changes.

@ACSimon33

ACSimon33 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

@ACSimon33 I just noticed after I sent that previous comment, we also need this new routine for the QL case (so I revise my statement about being willing to wait to add these routines, I propose we need them here)

In the QL case we must end with T being lower triangular without modifying the strictly upper triangular part. So we must compute

T = lt(V_1**H * V_1)

where lt(.) is the lower triangular part and V_1 is unit upper triangular.

Since we need to store the lower triangular component, we must enter xLAUUM with T holding a lower triangular matrix, and the only one available in this operation is V_1**H. However we would then need to compute

T = lt(L * L**H) which is not supported by xLAUUM

unless there's another routine that I've missed that does compute these two operations

I built and benchmarked it, and I think the xLAUUM question has a better answer than either side of the exchange above.

xLAUUM could cover QL (and RQ, and RQT)

You are right that the direct placement fails. But the obstruction is not the output triangle, it is which product xLAUUM offers relative to the shape of V1, and reversing both indices swaps that pairing. With W(i,j) = V1(k+1-i, k+1-j):

        V1  (unit upper)                W = flip(V1)  (unit lower)
   (  1  12  13  14  15 )          (  1                  )
   (      1  23  24  25 )          ( 45   1              )
   (          1  34  35 )   --->   ( 35  34   1          )
   (              1  45 )          ( 25  24  23   1      )
   (                  1 )          ( 15  14  13  12   1  )

W**H W = flip( V1**H V1 ), so the half you could not reach becomes the half xLAUUM does produce. Undoing it is (i,j) -> (k+1-j, k+1-i), an involution of each triangle onto itself, so T's opposite triangle is never touched.

QL via xLAUUM:

         DO 80 J = 1, K
            T( J, J ) = ONE
            DO 70 I = J + 1, K
               T( I, J ) = V( N+1-I, K+1-J )
   70       CONTINUE
   80    CONTINUE
         CALL DLAUUM( 'Lower', K, T, LDT, INFO )
*        undo the flip: c = i+j maps to 2k+2-c, order preserving, and the
*        stride along an anti-diagonal is 1-LDT, so each pair of anti-diagonals 
*        is one DSWAP
         DO 85 C = 3, K
            IF( C-1-C/2.GT.0 )
     $         CALL DSWAP( C-1-C/2, T( C-1, 1 ), LDT-1,
     $                     T( K, K+2-C ), LDT-1 )
   85    CONTINUE
         IF( N.GT.K )
     $      CALL DSYRK( 'Lower', 'Transpose', K, N-K, ONE, V, LDV, ONE,
     $                  T, LDT )

In the complex case the undo also needs a ZLACGV per column for QL and RQT, but not for RQ, where the copy already transposes so the two conjugations cancel.

QL without xLAUUM or the flip:

The implicit 1 on V1's diagonal contributes one term per entry, and that term is one row of V, so write it into T and let DGEMV accumulate the rest:

         DO 80 J = 1, K - 1
            DO 70 I = J + 1, K
               T( I, J ) = V( N-K+J, I )
   70       CONTINUE
            IF( J.GT.1 )
     $         CALL DGEMV( 'Transpose', J-1, K-J, ONE,
     $                     V( N-K+1, J+1 ), LDV, V( N-K+1, J ), 1, ONE,
     $                     T( J+1, J ), 1 )
   80    CONTINUE
         IF( N.GT.K )
     $      CALL DSYRK( 'Lower', 'Transpose', K, N-K, ONE, V, LDV, ONE,
     $                  T, LDT )

Both variants exist as complete drop-in {d,z}larft_ut.f covering all six cases, matching xLARFT_LVL2 to 1e-16 in all four precisions. Since the xGEMV implementation is more consistent across all six cases I think it would be the nicest one.

Benchmarks

ILAENV(3,...) returns 0 for DSYTRRK, ZHETRRK, DTRMMOOP and DTRSM_MOD, so the crossover never fires and every kernel recurses to 1x1 leaves. I set NX=64 for the benchmarks.

Minimum of 100 samples, microseconds per T factor, QL. The first five value columns are the netlib reference BLAS at /O2, the last five the same five variants relinked against sequential MKL BLAS.

n K LVL2 PR NX=0 PR NX=64 xLAUUM xGEMV LVL2 (MKL) PR NX=0 (MKL) PR NX=64 (MKL) xLAUUM (MKL) xGEMV (MKL)
512 16 31.6 69.1 45.9 45.8 45.3 7.5 89.7 40.6 39.6 40.0
512 32 127 265 174 173 172 30.0 323 144 144 142
512 64 491 1024 664 655 653 116 1212 547 530 501
1024 16 67.0 109 86.0 85.9 85.4 15.3 99.0 50.0 50.2 48.3
1024 32 273 422 330 328 327 57.7 360 166 164 168
1024 64 1084 1634 1276 1266 1264 219 1286 590 581 576
2048 16 138 189 166 166 166 27.6 117 66.6 65.3 65.0
2048 32 563 731 640 638 637 108 398 214 214 211
2048 64 2266 2856 2498 2487 2486 433 1368 729 678 685

Once NX is set the three formulations are basically indistinguishable — 0.3–1.8% spread on minimums with the reference BLAS. Note the whole grid above is K <= 64, since ILAENV(3,'DLARFT') caps the UT path at 64 and larger blocks never reach it. Note also that ILAENV(1,'xLAUUM') is 64, so on a k-by-k block xLAUUM itself falls through to the unblocked xLAUU2. The blocked path is dead there too, which is why the xLAUUM and xGEMV variants tie (another reason to prefer the xGEMV version).

@jprhyne

jprhyne commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

I don't really follow the

*        undo the flip: c = i+j maps to 2k+2-c, order preserving, and the
*        stride along an anti-diagonal is 1-LDT, so each pair of anti-diagonals
*        is one DSWAP
         DO 85 C = 3, K
            IF( C-1-C/2.GT.0 )
     $         CALL DSWAP( C-1-C/2, T( C-1, 1 ), LDT-1,
     $                     T( K, K+2-C ), LDT-1 )
   85    CONTINUE

block. If there was a bug that was traced back to this, I would really hate to be the one to debug this.

In addition, it is a bit involved for what I would expect to be a reference implementation. If this is the
accepted way of working with the data from QL, why would we ever bother with QL in the first place? From what
I have seen, it seems like QL is just like doing QR on flip(A), so why not just make the user flip their data
and only have QR and LQ available?

I do like the second one, it is very clean!

However for a later PR where we are computing a panel factorization, we will need to compute the following
kernel (below is a recursive implementation of the QR )

T = T V**T
( T_11 T_12 )   ( T_11 T_12 ) ( V_11**T V_21**T )
( 0    T_22 ) = ( 0    T_22 ) ( 0       V_22**T )

which will mean to compute T_12 we need to compute

T_12 = T_11 V_21**T + T_12 V_22**T

which will require the routine trmmoop regardless (or we can handroll it here I guess)

So since there is two places that I have where we need trmmoop I think it's still necessary to add.

One thing to note is also that your table shows the 3 methods are basically the same in execution time (which
you did say), so I am still in favor of adding the new routines for the following reasons

  1. The new kernels below {sy,he}trrk are useful in other applications that I am preparing to make a PR
    for (I wanted to get some input on this one before making the other two)
  2. The performance is basically the same when using an optimized BLAS
  3. The readability of the higher level codes are a lot more explainable and we sensibly break apart
    the operations from a linear algebra perspective allowing for a lower barrier of entry for folks to
    get started and be able to understand what operations are actually being computed

One more note on point number 2, some experiments I have ran show that trmm is about twice as fast as trmmoop, indicating
that if such an operation were to be optimized, it could make this new version much better (along with
us being able to fully remove new workspace requirements for dorgqr and family. We still need a single
vector for if we do not exceed NX).

I am also interested in how this behaves when comparing against AOCL since that's been my main
benchmark since the machines I use are AMD and the manufacturer suggested library is what most people
will use, so I'll get a version up and working. I also want to look at the spread of the times as I
don't personally like the minimum value only.

@mgates3

mgates3 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

0.3–1.8% spread on minimums with the reference BLAS.

The reference BLAS are not useful for performance comparisons. Use any optimized BLAS -- OpenBLAS, MKL, BLIS, ...

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.

4 participants