Skip to content

feat(linalg): cuSOLVER GPU QR as fallback when cuQuantum is unavailable - #1231

Open
pcchen wants to merge 4 commits into
masterfrom
claude/cusolver-gpu-qr-54bbe8
Open

pcchen wants to merge 4 commits into
masterfrom
claude/cusolver-gpu-qr-54bbe8

Conversation

@pcchen

@pcchen pcchen commented Nov 13, 2026 •

Copy link
Copy Markdown
Collaborator

Posted by Claude Code on behalf of @pcchen

Closes #255. Closes #1111. Refs #1227, #1232.

Builds on #1228 (merged): the six Rsvd GPU tests this un-skips run through the gesvdj path that #1228 fixed. The audit of the other cuQuantum-gated operations that #1111 also asks for is in #1227, not in this PR.

This is new numerical code and needs human review. Points for the reviewer are listed under "For review".

Problem

In a GPU build without cuQuantum, linalg::Qr on a GPU tensor throws "QR decomposition on GPU not implemented without cuQuantum support!". That also breaks linalg::Rand_isometry and every linalg::Rsvd call that sketches, and the dense, block and fermionic UniTensor overloads.

Fix

  • New kernel linalg_internal::cuQr (src/backend/linalg_internal_gpu/cuQr_internal.cu): reduced QR of a row-major M x N matrix with cusolverDn<t>geqrf and orgqr / ungqr, for Float, Double, ComplexFloat and ComplexDouble.
    • cuSOLVER is column-major, so the kernel factors an explicit column-major copy and writes Q (M x K) and R (K x N), K = min(M, N), back in row-major order. R is the upper triangle / trapezoid; entries below the diagonal are written as exact zeros.
    • Wide input (M < N): geqrf on the full matrix, Q from the leading M x M block, which satisfies the m >= n >= k requirement of orgqr / ungqr.
    • The scalar factors of the reflectors (tau) are an internal buffer between geqrf and orgqr / ungqr and are not returned.
    • Dispatch is std::visit on the typed GPU pointer; no new *_ii lookup table. Shared cuSOLVER handle, DeviceBuffer, checkCudaErrors on every status, devInfo checked after geqrf and after orgqr / ungqr.
    • Extents that do not fit cuSOLVER's 32-bit int are rejected with cytnx_error_msg.
  • src/linalg/Qr.cpp, GPU branch: without UNI_CUQUANTUM the new kernel is used; with UNI_CUQUANTUM the cuQuantum path is used, as before.
  • Qr(..., is_tau=true) on a GPU tensor raises "Returning tau is not supported on GPU" in every GPU build. The CPU path still returns tau.
  • The eight #ifndef UNI_CUQUANTUM skip guards are removed (two in tests/gpu/linalg_test/linalg_test.cpp, six in tests/gpu/linalg_test/Rsvd_test.cpp).

linalg::Qdr on GPU still throws; out of scope.

For review

  1. No tau on GPU. An earlier revision of this PR returned tau from cuSOLVER (also in cuQuantum builds, where is_tau=true used to throw). That was removed: nothing in the repository consumes tau, it is of no use without the Householder vectors, which Qr does not return, and its complex convention on the CPU is an accident of the implementation (linalg.Qr(..., is_tau=True) returns the conjugate of the geqrf scalars for complex input #1232). Whether is_tau should be removed from the API altogether is for linalg.Qr(..., is_tau=True) returns the conjugate of the geqrf scalars for complex input #1232.
  2. Behaviour in cuQuantum builds is unchanged, except for the wording of the is_tau=true error, which is now raised before the factorization instead of after it.
  3. The CPU tau convention is documented, not changed (include/linalg.hpp, QR_internal.cpp): for complex dtypes the CPU returns the complex conjugate of the geqrf scalar, because it runs gelqf on the transposed layout.
  4. A status failure from cuSOLVER goes through checkCudaErrors, which exits the process (CUDA error paths still terminate the process with exit() #1164), as in the other cuSOLVER wrappers. A nonzero devInfo raises a catchable cytnx::error.
  5. The INT_MAX rejection is not covered by a test (it would need a matrix with more than 2^31 rows or columns).

Testing

New tests/gpu/linalg_test/Qr_test.cpp (11 tests). Expected values come from host code in the test file, not from another Cytnx path: a textbook Householder QR in the LAPACK geqrf convention, plain matrix products, and hand-computed cases.

Checked in every build (invariants): Q^H Q = I, Q R = A, R exactly zero below the diagonal, shapes / dtype / device. Checked only in builds without cuQuantum (values): Q and R against the Householder reference and the hand-computed cases. cuQuantum builds run these tests through cutensornetTensorQR, which does not document which of the valid QR factorizations it returns, so the two hand-computed tests are skipped there.

  • Four dtypes x tall (7x4, 5x1, 96x64), square (1x1, 5x5, 80x80), wide (4x7, 1x5, 64x96).
  • Hand-computed: A = [3; 4] gives Q = [-0.6; -0.8], R = [-5]; A = [3i; 4] gives Q = [-0.6i; -0.8], R = [-5].
  • Rank-deficient (rank 2 with an exactly zero column; 6x5, 5x5, 4x6): invariants only, because Q's completion depends on rounding.
  • Zero matrix: R = 0 exactly; with cuSOLVER, Q = leading columns of the identity exactly.
  • Non-contiguous input, Int64 input (promoted to Double), dense UniTensor with rowrank 1 and 2.
  • is_tau=true on a GPU Tensor and on a GPU dense UniTensor raises.

Suites run: Qr*, Rsvd*, RsvdNoTruncation*, Gesvd*, GesvdTruncate*, Svd*, SvdTruncate*, linalg_Test*, ZeroExtent*.

Current head (f3a7df73), ran / passed / skipped / failed:

Host Build master (c7fda0ad) This PR
Tesla V100, CUDA 12.9 cuQuantum OFF 77 / 67 / 10 / 0 88 / 86 / 2 / 0
A100, CUDA 13.3, cuTensorNet 2.13 cuQuantum ON not rerun on master 88 / 84 / 4 / 0

Earlier revision of this PR (the one that returned tau, on the older base eade1422). The factorization code of the kernel is the same; these hosts were not rerun for the current head:

Host Build Before After
Tesla P100, CUDA 12.9 toolkit, through Slurm cuQuantum OFF 74 / 64 / 10 / 0 85 / 83 / 2 / 0
GeForce MX450 2 GB, CUDA 13.2 (one test per process) cuQuantum OFF 74 / 54 / 10 / 0, plus 10 out of memory 85 / 73 / 2 / 0, plus 10 out of memory
  • The P100 nodes only have CUDA 12.4.1, against which Cytnx does not compile even before this change (<cuda/std/cstdlib> is missing from its bundled headers), so the P100 run used a CUDA 12.9 toolkit copied from the V100 host and gcc-toolset-13.
  • The 10 laptop out-of-memory aborts are the same Svd.* / SvdTruncate.* / linalg_Test.GpuDenseUtSvdTruncate tests before and after: a 2 GB hardware limit, not QR.
  • Pre-change check (new tests and kernel present, Qr.cpp from the base): on the V100, the P100 and the laptop 19 tests failed with the "not implemented without cuQuantum" error (the 11 Qr.* tests of that revision, 2 linalg_Test QR tests, 6 Rsvd).
  • CPU Qr against GPU cuSOLVER Qr (temporary probe, not committed), 4 dtypes x 5 shapes up to 96x64, V100 and P100: Q and R agree to rounding (largest difference 1.1e-14 in double, 8.0e-6 in single precision, both on R of the 64x96 case).
  • A100, cuTensorNet 2.13: on 20 probed inputs the cuQuantum path and the cuSOLVER kernel returned bitwise identical Q and R. That is an observation, not a documented guarantee. In the current head the cuQuantum build does not call the new kernel.

Not run locally: the CPU test_main suite and pytest (CPU code is changed in comments only; CI runs both), the full GPU suite outside the listed suites, and multi-GPU.

🤖 Generated with Claude Code

pcchen and others added 2 commits November 13, 2026 07:55
…ilable

linalg::Qr on a GPU tensor threw "QR decomposition on GPU not implemented
without cuQuantum support!" in builds without cuQuantum, which also broke
Rand_isometry and every Rsvd call that sketches. With cuQuantum, is_tau=true
threw because cutensornetTensorQR cannot return tau.

Add linalg_internal::cuQr (cuQr_internal.cu): reduced QR of a row-major
M x N matrix with cusolverDn<t>geqrf + orgqr/ungqr for Float, Double,
ComplexFloat and ComplexDouble, dispatched with std::visit on the typed GPU
pointer. It factors an explicit column-major copy, extracts R as the upper
triangle/trapezoid, and for wide input forms Q from the leading M x M block.
tau follows the CPU path's convention (gelqf on the transposed layout): the
geqrf scalars for real dtypes, their complex conjugates for complex dtypes.
Dimensions that do not fit cuSOLVER's 32-bit int are rejected.

Qr.cpp uses the new kernel when UNI_CUQUANTUM is undefined, and for
is_tau=true in any build. The cuQuantum path for is_tau=false is unchanged.

Tests: new tests/gpu/linalg_test/Qr_test.cpp checks Q^H Q = I, Q R = A,
R upper triangular, shapes/dtypes/device, and Q, R, tau against a host
Householder reference and hand-computed cases, for four dtypes and tall,
square, wide, rank-deficient, zero, non-contiguous, integer and dense
UniTensor inputs. The eight "#ifndef UNI_CUQUANTUM" skip guards on the QR
and Rsvd GPU tests are removed.

Refs #255, #1111, #1227.

Co-Authored-By: Claude <noreply@anthropic.com>
For complex dtypes the tau returned by linalg::Qr is the complex conjugate of
the LAPACK geqrf scalar, because the CPU path runs gelqf on the transposed
(row-major) layout; the GPU path reproduces it. Also correct the description
of the third output, which is the vector of scalar factors, not the reflectors.

Co-Authored-By: Claude <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-13T00:15:26.509994Z 1cf2925 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1cf29252dc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


} // namespace

void cuQr(const Tensor &in, Tensor &Q, Tensor &R, Tensor &tau) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Use snake_case for the new GPU QR entry point

This new CUDA implementation introduces cuQr and uppercase parameter names Q and R, contrary to the new-identifier convention in AGENTS.md. Because these are new files rather than additions to a consistently named legacy file, the existing-file exception does not apply. Rename the entry point to cu_qr and its output parameters to lowercase, updating the declaration and caller together.

AGENTS.md reference: AGENTS.md:L73-L75

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Posted by Claude Code on behalf of @pcchen

Not changed in this PR. Every sibling kernel entry point in src/backend/linalg_internal_gpu/ is named cuXxx (cuAdd_dispatch, cuDet_internal_d, cuEigh_internal_cf, cuConj_inplace_dispatch, ...): 32 of the 36 headers in that directory declare cuXxx functions and none declares a cu_xxx one. A lone cu_qr next to them would be harder to find than cuQr, and Q / R are the names the caller in src/linalg/Qr.cpp and the QR literature use for these outputs. Renaming the GPU kernels to snake_case is better done for the whole directory at once than for one function here.

@codecov

codecov Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 69.43%. Comparing base (c7fda0a) to head (f3a7df7).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1231   +/-   ##
=======================================
  Coverage   69.43%   69.43%           
=======================================
  Files         209      209           
  Lines       22614    22614           
  Branches       72       72           
=======================================
  Hits        15701    15701           
  Misses       6891     6891           
  Partials       22       22           
Flag Coverage Δ
cpp 69.50% <ø> (ø)
python 65.01% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
C++ backend 69.50% <ø> (ø)
Python bindings ∅ <ø> (∅)
Python package 65.01% <ø> (ø)
Files with missing lines Coverage Δ
src/backend/linalg_internal_cpu/QR_internal.cpp 36.95% <ø> (ø)
src/linalg/Qr.cpp 73.14% <ø> (ø)

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 c7fda0a...f3a7df7. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

pcchen and others added 2 commits November 13, 2026 08:33
Qr on a GPU tensor no longer returns tau: is_tau=true raises an error in
every GPU build, as it already did with cuQuantum. Builds with cuQuantum
use cutensornetTensorQR for every GPU Qr again, so this branch does not
change their behaviour. The cuSOLVER kernel keeps the geqrf scalars as an
internal buffer for orgqr / ungqr.

Tests: drop the tau checks, add a rejection test, and compare Q and R with
the Householder reference only in builds without cuQuantum.

Co-Authored-By: Claude <noreply@anthropic.com>
… and tests

Dimensions, bounds and loop counters are ordinary sizes, not dtype-backed
tensor scalars. Shape containers passed to the Tensor API stay
std::vector<cytnx_uint64>.

Co-Authored-By: Claude <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

Implement CUDA QR fallback when cuQuantum is unavailable cuSolver QR

1 participant