Skip to content

Add device-resident cuSOLVER singular values - #121

Merged
ianmccul merged 2 commits into
mainfrom
feature/cusolver-singular-values
Nov 12, 2026
Merged

ianmccul merged 2 commits into
mainfrom
feature/cusolver-singular-values

Conversation

@ianmccul

Copy link
Copy Markdown
Collaborator

linalg::singular_values on a CudaTensor used to copy the matrix to the host and run LAPACK, returning a host Tensor. make_tensor<ColumnMajor>(view) always materializes HostStorage, so the GPU call silently became a device-to-host transfer plus a CPU SVD.

This adds a cuSOLVER singular_values_op backend for real and complex float and double, and makes singular_values keep CUDA-buffer inputs on their device, returning a CudaTensor there.

Backend. gesvd with jobu = jobvt = 'N' and null U/VT, which LAPACK and cuSOLVER leave unreferenced for job 'N'; rwork is allocated rather than passed as null. A matrix and its transpose share singular values, so the backend accepts any workspace whose provider shape is tall: tall column-major, or wide row-major read as its transpose. Wide column-major and tall row-major decline as unsupported_layout. Empty matrices succeed without device work. (Cytnx crashes on null U/V with gesvdj/gesvdp no-vector modes, Cytnx-dev/Cytnx#1102 and #831; gesvd with 'N' is the documented route.)

Tensor API. The preserving form makes one same-device workspace copy: column-major for tall input, row-major for wide, so cuSOLVER always sees a tall matrix and no transpose kernel is needed. The consuming form uses a compatible input as the workspace directly and otherwise makes the same device copy. Async forms route the same way. Builds without cuSOLVER keep the previous path.

Multi-device fix. cuSOLVER launches into the calling thread's current device, and the adapter never selected the operand device. The existing full SVD therefore failed with CUSOLVER_STATUS_EXECUTION_FAILED for operands on any device other than the current one; the existing tests only used device 0. Both cuSOLVER execution paths now select the operand device around the workspace query, solver call and devInfo copy. The new device-1 tests reproduce the failure before the fix.

Validation on polaron (2x Quadro GV100, sm_70, CUDA 12.9, GCC 13 Debug):

  • uni20_cusolver_tests: 24/24 passed, none skipped. Singular values are checked against matrices built as Householder × diag × Householder with known values (no second SVD path), for all four scalars, shapes 1x1 through 70x33/33x70, column- and row-major inputs, preserving/consuming/async forms, explicit selector, empty matrices, kernel declines, and values on device 1.
  • compute-sanitizer --tool memcheck --leak-check full over the 18 singular-values tests: 0 errors, 0 bytes leaked.
  • Full CUDA ctest: 2403/2405. The two failures are unrelated: uni20_common_tests does not build in CUDA configurations (cuda_runtime_api.h via precision_cases.hpp → tensor.hpp → linalg/backends/cuda/copy.hpp; no file in that chain changed), and CudaTensorTest.StreamOrderedAccessCanBeCoAwaitedAsTensorView failed once under ctest -j4 and passed 5/5 alone.
  • CUDA build with UNI20_BACKEND_CUSOLVER=OFF: uni20_linalg_cuda_tests 58 passed (4 peer-copy tests skipped on one visible GPU), uni20_linalg_tests 384 passed.
  • CPU-only GCC 13 Debug on ryzen: uni20_linalg_tests 384 and uni20_linalg_decompositions_tests 3 passed.
  • clang-format clean on changed files. Not run: Clang builds, Release builds, CUDA 13.

Follow-up (separate PR): audit and remove implicit CUDA→host copies. The Tensor(TensorView const&) converting constructor, its CTAD, and make_tensor(view) accept CUDA sources and target HostStorage, and the other linalg value APIs (svd_left, svd_right, svd, truncated_svd, eigh, qr, lq, solve, lu) materialize host work through them. Restricting those entry points to same-domain sources would turn every remaining implicit transfer into a compile error, including singular_values in builds without cuSOLVER.

🤖 Generated with Claude Code

linalg::singular_values on a CUDA matrix used to copy the matrix to the
host and run LAPACK, returning a host tensor. make_tensor materializes
host storage regardless of the source domain, so the GPU call silently
became a device-to-host transfer plus a CPU SVD.

Add a cuSOLVER singular_values_op backend for real and complex float and
double. It calls gesvd with jobu = jobvt = 'N' and null U/VT, which
LAPACK and cuSOLVER leave unreferenced for job 'N', and allocates rwork
rather than passing null. A matrix and its transpose share singular
values, so the backend accepts any workspace whose provider shape is
tall: tall column-major, or wide row-major read as its transpose.

singular_values now keeps CUDA-buffer inputs on their device and returns
a CudaTensor there. The preserving form copies to column-major work for
tall input and row-major work for wide input on the device; the
consuming form uses a compatible input as the workspace directly and
otherwise makes the same device copy. Async forms route the same way.
Builds without cuSOLVER keep the previous path.

Select the operand device around the cuSOLVER provider calls. cuSOLVER
launches into the calling thread's current device, so the existing full
SVD failed with CUSOLVER_STATUS_EXECUTION_FAILED for operands on any
device other than the current one. The new device-1 tests reproduce
that failure before the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings November 12, 2026 14:57
@chatgpt-codex-connector

Copy link
Copy Markdown
Codex Review Summary: ❌ Failed

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

Review Status Commit Review trigger
📝 Code Review ❌ Failed 2026-11-12T14:57:53.214377Z
We couldn’t complete this review. Comment @codex review to try again.
0551fbe 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

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request implements device-resident real and complex singular values using the cuSOLVER backend (cusolverDn{S,D,C,Z}gesvd with jobu = jobvt = 'N'). It introduces support for keeping the computation on the CUDA device for both preserving and consuming forms of singular_values when the cuSOLVER backend is enabled, and ensures that cuSOLVER calls are executed on the correct operand device using scoped device guards. Additionally, comprehensive unit tests and documentation updates have been added to cover these new capabilities. I have no feedback to provide as there are no review comments to address and the implementation is clean and well-tested.

@ian-review-bot

Copy link
Copy Markdown
Collaborator

Review Bot: review of exact head 0551fbe0753913ce93e981eba40f8bedd529da6b

No defect found. The compute host has no GPU, so none of the new device code was executed. Everything below comes from builds, compile-time probes and reading the code.

What the PR does. It adds a cuSOLVER singular_values_op kernel in linalg/backends/cusolver/svd.hpp. The kernel calls gesvd with jobu = jobvt = 'N' for s/d/c/z and accepts a provider-tall workspace: tall column-major, or wide row-major read as its transpose. linalg::singular_values in ops/svd.hpp and async/svd.hpp now keeps CudaStorage inputs on their device and returns a CudaTensor. The existing execute_svd gains a ScopedDevice for the operand device. Docs and a new 24-case test file are updated to match.

What I reviewed.

  • try_singular_values / execute_singular_values against the existing try_svd / execute_svd: rwork size min(m,n)-1 (floored at 1), workspace query on provider shape, device check, alias check, range checks.
  • Guard lifetimes. ExecutionLease and StreamAllocation already select their own device, and the new guard is destroyed after the allocations and buffer accesses.
  • try_provider_matrix_descriptor traced through blas::try_mdspan_matrix_metadata for 1xN, Nx1 and 1x1 in both layouts. Each case gives a tall provider shape with a valid leading dimension.
  • Routing in cuda_singular_values_from_copy and cuda_singular_values_from_consumed, and the async selectors. The CUDA elementwise copy used for layout changes supports all four scalars.
  • In-tree callers of singular_values (git grep). The only existing CUDA consumer is the concept check in tests/linalg/test_cuda_copy.cpp.
  • The test oracle. Householder × diag × Householder is independent of any SVD path. The static_assert on CudaTensor in expect_known_values makes the tests fail to compile against the old host-returning behaviour.

Verified (polaron, GCC 13.3, CUDA 12.9 nvcc, CMAKE_CUDA_ARCHITECTURES=70; otherwise the REVIEW-hint option set plus -DUNI20_ENABLE_CUDA=ON):

  • CUDA Debug with -DUNI20_BACKEND_CUSOLVER=ON builds these targets: uni20_cusolver_tests, uni20_linalg_cuda_tests, uni20_linalg_tests, uni20_linalg_decompositions_tests, uni20_symmetry_tests, uni20_krylov_block_tensor_cuda_tests, uni20_tensor_tests, uni20_models_tests, uni20_cublas_tests. In that build:
    • uni20_linalg_tests 384/384 pass and uni20_linalg_decompositions_tests 3/3 pass.
    • uni20_cusolver_tests exits 0 with every test skipped (no device).
  • CUDA Debug with -DUNI20_BACKEND_CUSOLVER=OFF builds uni20_linalg_cuda_tests and uni20_linalg_tests; uni20_linalg_tests 384/384 pass.
  • CPU-only Release (CI "Ubuntu Release" options): uni20_linalg_tests 384/384 and uni20_linalg_decompositions_tests 3/3 pass.
  • Compile-time probe against the cuSOLVER-ON headers. The preserving result is CudaTensor<real,1> for CudaTensor<double,2>, row-major CudaMatrix<double>, CudaTensor<complex<double>,2>, CudaTensor<complex<float>,2> and ConjugatedTensorView<CudaTensor<complex<double>,2>>. The consuming and Async forms also return CudaTensor, and a host Tensor<double,2> still returns Tensor<double,1>.
  • In cuSOLVER builds, singular_values(LapackBackend{}, cuda_matrix) no longer compiles: there is no matching dispatch_at_precision for CUDA mdspecs with a LAPACK selector. On base 697e49e7 the same call compiles and returns a host Tensor. This matches the new doc comment on the explicit-selector overload, and no in-tree code makes that call.
  • clang++-19 -fsyntax-only -Werror -pedantic-errors is clean on test_cusolver_singular_values.cpp, test_cusolver_svd.cpp and the probe.
  • scripts/check-docs.py passed and tests/docs passed 16/16. The #svd-boundary anchor exists.
  • uni20_common_tests built successfully in my CUDA cuSOLVER-ON Debug configuration. The build failure in your description did not reproduce here; it may depend on configuration.

Not checked.

  • Any GPU execution: the numerical results, the device-1 tests (including FullSvdRunsOnTheOperandDevice failing before the guard fix), compute-sanitizer, and the claim that the consuming form destroys compatible input in place without a copy. The tests check values only, not that no copy happens.
  • CUDA 13, other sm_ targets, CUDA Release builds, and a full Clang CUDA configuration (only syntax checks were run).
  • clang-format: no clang-format binary on polaron.
  • A full unfiltered CUDA ctest.
  • GitHub CI: it does not build CUDA, so the cuSOLVER code is covered only by your run and the builds above.

Review tier: full (code or new pull request) · model: claude-opus-5-5 · effort: high

auto const p = detail::decomposition_precision(matrix, requested);
return detail::singular_values_from_consumed_matrix(std::forward<BackendSelector>(selector),
#if UNI20_BACKEND_CUSOLVER
if constexpr (detail::cuda_resident_singular_values<MatrixTensor>())

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.

Review Bot: review of exact head 0551fbe0753913ce93e981eba40f8bedd529da6b

This branch never runs for a CudaTensor, so the consuming form still copies. The overload is constrained on uni20::MutableRankedImmediateTensorView<MatrixTensor, 2> (line 762). ImmediateTensorView requires a host mdspan() (src/uni20/tensor/concepts.hpp:100), and CudaMatrix doesn't provide one. As a result, singular_values(std::move(cuda_matrix)) resolves to the preserving MatrixTensor const& overload and goes through cuda_singular_values_from_copy. The async consuming form behaves the same way: co_consuming_singular_values calls singular_values(selector, std::move(matrix_value), ...) (async/svd.hpp:159), which also binds to the preserving overload. cuda_singular_values_from_consumed (line 439) can't be reached from either public entry point.

The results are still correct. What doesn't hold is the stated behaviour: a tall column-major or wide row-major consumed input is copied into a fresh device allocation instead of being used as the workspace. That contradicts:

  • the doc comment above this overload;
  • docs/backends/cuda/cusolver.md:254 ("The consuming form uses a tall column-major or wide row-major input as the workspace directly");
  • the comments at tests/backend/cusolver/test_cusolver_singular_values.cpp:185 and :192.

ConsumingCallUsesOrCopiesDeviceWorkspace checks only the values, so it passes either way.

How I saw it (polaron, 2x Quadro GV100, CUDA 12.9 nvcc, g++-13, Debug, -DUNI20_ENABLE_CUDA=ON -DUNI20_BACKEND_CUSOLVER=ON -DCMAKE_CUDA_ARCHITECTURES=70). I added two probe files to tests/backend/cusolver/ in a scratch worktree of this head:

  • A runtime-printed concept check gives OwningTensor<CudaMatrix<double>>=1, ImmediateTensorView=0, MutableRankedImmediateTensorView<.,2>=0, which makes the consuming overload not viable. The same check on Tensor<double,2> gives 1 for all three.
  • auto d = to_device(h, res); auto v = singular_values(std::move(d)); followed by to_host(d) gives back exactly h for 8x5 column-major and 5x8 row-major inputs, for all four scalars. gesvd with jobu = jobvt = 'N' overwrites its matrix argument, so an unchanged d means the solver ran on a copy.

Smallest fix I can see: give the CUDA case an overload that the constraint admits. For example, accept an owning MutableRankedTensorView<MatrixTensor, 2> when cuda_resident_singular_values<MatrixTensor>() holds. Then add an assertion that discriminates, such as the consumed buffer being modified or no allocation happening. Alternatively, if the copy is intended, correct the docs and comments to say so.

Not checked. Whether some other CudaStorage owning type in the tree satisfies ImmediateTensorView; I found none and probed only CudaMatrix in both layouts. Clang and CUDA 13 builds.

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.

Confirmed, thanks. Fixed in 20f85e8: both consuming overloads now also admit CUDA-resident owning tensors, so singular_values(std::move(cuda_matrix)) and the async consuming form reach cuda_singular_values_from_consumed. ConsumingCallUsesOrCopiesDeviceWorkspace now also checks whether the caller's tensor was overwritten (expected for tall column-major and wide row-major, unchanged otherwise, for shapes with min(rows, cols) > 1). Against the previous constraint that test fails for all four scalar types; with the fix uni20_cusolver_tests passes 24/24 on polaron and memcheck is clean.

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.

Review Bot: recheck of exact head 20f85e803be7cdbe014787ab60e170d94453deba

Confirmed: the fix works. Consumed CUDA matrices now reach cuda_singular_values_from_consumed, and the updated test catches the old behaviour.

  • Your test fails without the fix. I put the 0551fbe ops/svd.hpp into a copy of this head and kept the new test. ConsumingCallUsesOrCopiesDeviceWorkspace then fails for all four scalars. That is 32 failed assertions: the 8 in-place cases per type (4x4, 6x3, 8x5 and 70x33 column-major; 4x4, 3x6, 5x8 and 33x70 row-major). The other 14 singular-values tests still pass. On the real head, uni20_cusolver_tests passes 24/24.

  • Which call forms run in place. I added counters to the in-place and copy branches in a scratch copy and called each form, for all four scalars:

    • lvalue, const rvalue, and an rvalue conj(m) view: copy path, input unchanged;
    • singular_values(std::move(m)) and the async consuming form: in place.

    So only owning, non-const rvalues are consumed.

Setup and remaining gaps are in the general comment for this head.

@ian-review-bot

Copy link
Copy Markdown
Collaborator

Review Bot: review of exact head 0551fbe0753913ce93e981eba40f8bedd529da6b

Found 1 issue; details in the inline comment on src/uni20/linalg/ops/svd.hpp:769. This is a forced re-review of the same head. My earlier "no defect found" review ran on a host where the GPUs weren't visible. This run executed the device code, and that earlier verdict is now superseded.

What changed since 0551fbe. No new commits. The difference is that this run executed the device code on two GPUs.

The finding. The CUDA branch in the consuming singular_values overload can't be reached. CudaMatrix isn't an ImmediateTensorView, so singular_values(std::move(cuda_matrix)) and the async consuming form both resolve to the preserving overload. A tall column-major or wide row-major consumed input therefore gets copied, not used in place as the docs and doc comments say. The values are still correct.

What I reviewed.

  • try_singular_values / execute_singular_values in linalg/backends/cusolver/svd.hpp.
  • The routing in ops/svd.hpp and async/svd.hpp, including which overload an rvalue CudaMatrix binds to.
  • The new test file and its oracle.
  • The multi-device claim about execute_svd.

Verified (polaron, 2x Quadro GV100 sm_70, CUDA 12.9 nvcc, g++-13 host, Debug, REVIEW-hint option set plus -DUNI20_ENABLE_CUDA=ON -DUNI20_BACKEND_CUSOLVER=ON -DCMAKE_CUDA_ARCHITECTURES=70):

  • Your test results:

    • uni20_cusolver_tests: 24/24 pass, none skipped.
    • uni20_linalg_cuda_tests: 62/62 pass with both GPUs visible.
    • uni20_linalg_tests 384, uni20_linalg_decompositions_tests 3, uni20_krylov_block_tensor_cuda_tests 8, uni20_cublas_tests 39: all pass.
    • ctest -R '^(Cuda|Cusolver|Cublas|Krylov|Linalg|Svd|Lapack)': 169 pass. 9 Krylov example entries were "Not Run" because I didn't build those example executables.
  • Multi-device fix. I removed the two new cuda::ScopedDevice device_guard(plan.device); lines in a scratch worktree. ValuesStayOnTheInputDevice and FullSvdRunsOnTheOperandDevice then each abort with cusolverDnDgesvd failed on device 1: CUSOLVER_STATUS_EXECUTION_FAILED. With the lines they pass, so the claim reproduces and the tests discriminate.

  • Probe against host LAPACK. I compared against host LAPACK singular_values on non-structured matrices (not the PR's Householder construction):

    • shapes: 200x50, 50x200, 257x257, 1000x2, 2x1000, 2x2, 33x32;
    • all four scalars, column- and row-major, preserving and consuming forms;
    • conj(cuda_matrix) at 7x11.

    All agree within 64·eps·max(m,n)·σ_max. The largest difference was 4.6e-5 for float and 5.7e-14 for double at 257x257.

  • Operand device versus current device. I tested both directions (operand on device 1 with device 0 current, and the reverse) for preserving, consuming, async preserving and async consuming forms, complex double. Values are correct, the result lives on the operand device, and the thread's current device is restored afterwards.

  • compute-sanitizer.

    • --tool memcheck --leak-check full on the 18 singular-value tests: 0 errors, 0 bytes leaked.
    • --tool initcheck reports "Uninitialized access … by cudaMemcpy source" from inside libcusolver (a cuMemcpyDtoHAsync called from cusolverDnDgesvd). The existing full-SVD tests (CusolverSvd*) show the same pattern, so I don't attribute it to this PR.

Not checked.

  • CUDA 13.0 (installed on polaron but not used), CUDA Release, Clang CUDA builds, other sm_ targets.
  • A full unfiltered CUDA build and ctest. Your 2403/2405 and the uni20_common_tests build failure were not re-run.
  • GitHub CI has no CUDA job, so CI doesn't cover any of this.
  • clang-format.

Review tier: full (code or new pull request) · model: claude-opus-5-5 · effort: high

The consuming singular_values overloads required an immediate host
tensor view. CudaMatrix has no host mdspan, so singular_values on an
rvalue CUDA matrix, and the async consuming form, bound to the
preserving overload and always copied the input. The consuming CUDA
path was unreachable, contrary to the documentation.

Admit CUDA-resident owning tensors in both consuming overloads. A tall
column-major or wide row-major consumed matrix is now the gesvd
workspace; other layouts are copied on the device as documented.

The consuming test checked values only, so it passed either way. It now
also checks whether the caller's tensor was overwritten: gesvd with
jobu = jobvt = 'N' destroys its matrix, so the tensor changes exactly
when it was used in place. Without this fix the test fails for all four
scalar types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ian-review-bot

Copy link
Copy Markdown
Collaborator

Review Bot: review of exact head 20f85e803be7cdbe014787ab60e170d94453deba

No defect found. My earlier finding (unreachable consuming CUDA path) is fixed; the details are in the reply on that thread.

What changed since 0551fbe.

  • Both consuming singular_values overloads in ops/svd.hpp now also admit owning tensors for which detail::cuda_resident_singular_values<MatrixTensor>() holds.
  • ConsumingCallUsesOrCopiesDeviceWorkspace now checks whether the caller's buffer was overwritten (expected for tall column-major and wide row-major input), for shapes with min(rows, cols) > 1.

What I reviewed.

  • The new constraint and its behaviour when cuSOLVER is off: cuda_resident_singular_values is declared in every configuration and returns false there.
  • The async consuming path (co_consuming_singular_values, async/svd.hpp:159).
  • In-tree callers of singular_values: there are none outside linalg/ops and linalg/async.
  • Whether lvalues, const rvalues or views could now be consumed by mistake.

Verified. Setup: polaron, 2x Quadro GV100 (sm_70), CUDA 12.9 nvcc, g++-13, Debug, REVIEW-hint option set plus -DUNI20_ENABLE_CUDA=ON -DUNI20_BACKEND_CUSOLVER=ON -DCMAKE_CUDA_ARCHITECTURES=70.

  • This head:
    • uni20_cusolver_tests 24/24 pass, none skipped.
    • uni20_linalg_cuda_tests 62/62 pass.
  • Your test fails without the fix. With the 0551fbe ops/svd.hpp and the new test, ConsumingCallUsesOrCopiesDeviceWorkspace fails for float, double, complex and complex: 32 assertions, exactly the in-place shapes and layouts. The other 14 singular-values tests pass.
  • Which call forms run in place. I put counters on the in-place and copy branches of cuda_singular_values_from_consumed / cuda_singular_values_from_copy in a scratch copy, for all four scalars:
    • lvalue, const rvalue, rvalue conj(m), and async preserving: copy path, input unchanged;
    • sync rvalue and async consuming: in place.
  • In-place results match host LAPACK. I consumed non-structured matrices (not the Householder construction) and compared with host singular_values. Shapes: column-major 200x50, 257x257, 1000x2, 2x2 and 33x32; row-major 50x200, 2x1000, 32x33 and 257x257. Every scalar was within 64·eps·max(m,n)·σ_max. The largest differences were 7.7e-5 for float and 4.3e-14 for double, both at 257x257.
  • In place on a device other than the current one. Operand on device 1 with device 0 current, and the reverse, using sync 40x12 column-major and async 12x40 row-major. The in-place path ran, the values were correct and on the operand device, and the current device was restored.
  • compute-sanitizer: --tool memcheck --leak-check full over the 18 singular-values tests plus the 12 probes reports 0 errors and 0 bytes leaked. This covers the async case, where the consumed workspace is destroyed straight after the solver is enqueued.
  • CPU-only Release (the CI "Ubuntu Release" options): uni20_linalg_tests 384/384 and uni20_linalg_decompositions_tests 3/3 pass.
  • Clang: clang++-19 -fsyntax-only -Werror -pedantic-errors is clean on test_cusolver_singular_values.cpp with the cuSOLVER build's flags.

Not checked.

  • The PR's own tests still don't prove that the async consuming form works in place. AsyncFormsReturnDeviceValues checks only values, and it passed with the pre-fix header. Only my instrumented probe showed the in-place route.
  • CUDA 13.0, CUDA Release, a full Clang CUDA build, CUDA with UNI20_BACKEND_CUSOLVER=OFF on this head, and other sm_ targets.
  • A full unfiltered CUDA build and ctest, Bethe, and clang-format.
  • GitHub CI has no CUDA job, so it doesn't exercise any of this.

Review tier: full (code or new pull request) · model: claude-opus-5-5 · effort: high

@ianmccul
ianmccul merged commit 040baba into main Nov 12, 2026
6 checks passed
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.

3 participants