Skip to content

Never copy CUDA tensors to the host implicitly - #122

Merged
ianmccul merged 4 commits into
mainfrom
feature/no-implicit-host-copies
Nov 12, 2026
Merged

ianmccul merged 4 commits into
mainfrom
feature/no-implicit-host-copies

Conversation

@ianmccul

Copy link
Copy Markdown
Collaborator

Stacked on #121; review that first. The base branch will be retargeted to main once #121 merges.

Owning-tensor materialization ignored the source's storage domain. make_tensor(view) and Tensor CTAD always produced HostStorage, and the Tensor(TensorView const&) converting constructor accepted any readable view, so a CUDA source could become a host tensor without an explicit transfer. The linear-algebra value operations build their workspaces through make_tensor, so svd, svd_left, svd_right, truncated_svd, eigh, qr, lq, solve and LU on a CudaTensor silently copied it to the host and ran LAPACK.

Rule. to_host, to_device and copy(destination, source) are the only transfers between host and CUDA storage. Materialization stays in the source's domain, using the existing accessor-domain model: a source may be materialized into storage whose accessor shares one of its declared domains (host_access_domain, cuda_access_domain). Generated accessors are valid in both. Accessors that declare no domain keep the previous behaviour.

Changes

  • The converting constructor rejects cross-domain sources: DenseMatrix<T>(cuda) and CudaMatrix<T>(host) are no longer constructible. A CUDA result is allocated on the source buffer's device (previously the default device), so construction never implies a peer transfer either.
  • make_tensor(view) and CTAD from a view deduce CUDA storage for CUDA sources. make_tensor(cuda_tensor) makes a same-device copy. Owning CUDA tensors stay non-copyable, so Tensor t(cuda_tensor) (CTAD from an owning tensor picks the copy constructor) still does not compile. Bare-mdspan make_tensor requires a host-readable mdspan.
  • Async make_tensor follows the same rule and allocates on the source device. Async to_host no longer delegates to make_tensor.
  • Host-only value operations build their workspace through detail::make_host_work_tensor, whose static_assert reads: "this linear-algebra operation has no implementation for CUDA operands; transfer the operand explicitly with uni20::to_host() to compute on the host". singular_values keeps its cuSOLVER path from Add device-resident cuSOLVER singular values #121; without cuSOLVER it now gets the same static_assert.
  • least_squares never materialized and already failed to compile for CUDA operands, so it is unchanged.

Audit. After the restriction, a full -k 0 build of every target (library, tests, examples) found exactly one user of the implicit routes: test_cuda_copy.cpp asserted that async svd_left/svd_right/svd/eigh/truncated_svd were well-formed for Async<CudaMatrix>. Those assertions are replaced with checks of the new rules. Nothing else in the tree relied on them.

Tests. New static assertions: cross-domain construction is not constructible; make_tensor, CTAD and async make_tensor of CUDA sources give CUDA types; async to_host gives a host type; singular_values on Async<CudaMatrix> gives Async<CudaTensor<double, 1>>. New runtime tests: materialization of an owning tensor, a conjugated view and CTAD stays on device 1 with correct values, and async make_tensor stays on the device while async to_host transfers. The static_assert diagnostic was checked by compiling svd_left, eigh, qr, solve and lu_factor on a CudaMatrix; the repo has no compile-failure test harness, so that check is not automated.

Validation:

  • CPU-only GCC 13 Debug (ryzen): full build, ctest 2545/2545.
  • CUDA 12.9 + cuSOLVER, GCC 13 Debug, 2x Quadro GV100 (polaron): ctest 2405/2407. Failures: uni20_common_tests_NOT_BUILT (that target does not build in CUDA configurations on main either: missing cuda_runtime_api.h include path), and CudaTensorTest.StreamOrderedDescriptorAccessCanBeCoAwaitedAsMdspanLease.
  • CUDA 12.9 without cuSOLVER: ctest 2373/2375. Same NOT_BUILT target, and CudaTensorTest.StreamOrderedAccessCanBeCoAwaitedAsTensorView.
  • The CudaTensorTest.StreamOrdered* failures are pre-existing flakiness: a pristine main build of uni20_cuda_runtime_tests fails the same way (test_cuda_tensor.cpp:588, submitted false, then an active-task destruction CHECK) at roughly iteration 149 of a 200-repetition run.
  • clang-format clean on changed files. Not run: Clang builds, Release builds, CUDA 13.

🤖 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 15:28
@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-12T15:28:07.476014Z
We couldn’t complete this review. Comment @codex review to try again.
b0a4c65 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.

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.

@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.

@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 restricts implicit storage-domain transfers during tensor materialization and linear-algebra operations. Eager materialization via Tensor(view) and make_tensor(view) now preserves the source's storage domain (host or CUDA device), and host-only linear-algebra operations (such as SVD, QR, and LU) now reject CUDA operands at compile time rather than implicitly copying them. The review feedback suggests robustifying the newly introduced C++ concepts, SourceAllocationContext and HostReadableTensor, by normalizing their InputTensor template arguments with std::remove_cvref_t to correctly handle reference types.

Comment thread src/uni20/tensor/basic_tensor.hpp Outdated
Comment on lines +82 to +85
template <class Policy, class InputTensor>
concept SourceAllocationContext = requires { typename Policy::context_type; } && requires(InputTensor const& input) {
{ mdspec_of(input).data_descriptor().buffer().resources() } -> std::same_as<typename Policy::context_type&>;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

When designing C++ concepts that query observers or nested types, it is important to normalize the template arguments using std::remove_cvref_t to ensure they correctly handle reference types (such as those obtained via decltype). In SourceAllocationContext, InputTensor should be normalized to std::remove_cvref_t<InputTensor> so that input is consistently treated as a const lvalue reference, preventing collapsing to a non-const reference when a reference type is passed.

template <class Policy, class InputTensor>
concept SourceAllocationContext = requires { typename Policy::context_type; } && requires(std::remove_cvref_t<InputTensor> const& input) {
  { mdspec_of(input).data_descriptor().buffer().resources() } -> std::same_as<typename Policy::context_type&>;
};
References
  1. When designing C++ concepts or type traits that query nested types or observers, normalize the template arguments (e.g., using std::remove_cvref_t) to ensure they correctly handle reference types (such as those obtained via decltype).

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.

Applied in b785797. No current call site passes a reference type, so this is hardening rather than a fix.

Comment thread src/uni20/tensor/copy.hpp Outdated
Comment on lines +86 to +89
template <class InputTensor>
concept HostReadableTensor =
TensorView<InputTensor> && AccessDomainsCompatible<stdex::default_accessor<tensor_element_t<InputTensor> const>,
typename tensor_mdspec_t<InputTensor>::accessor_type>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

To ensure C++ concepts correctly handle reference types (such as those obtained via decltype), normalize the template arguments using std::remove_cvref_t. In HostReadableTensor, InputTensor should be normalized to std::remove_cvref_t<InputTensor> before querying its nested types and passing it to other traits.

Suggested change
template <class InputTensor>
concept HostReadableTensor =
TensorView<InputTensor> && AccessDomainsCompatible<stdex::default_accessor<tensor_element_t<InputTensor> const>,
typename tensor_mdspec_t<InputTensor>::accessor_type>;
template <class InputTensor>
concept HostReadableTensor =
TensorView<std::remove_cvref_t<InputTensor>> && AccessDomainsCompatible<stdex::default_accessor<tensor_element_t<std::remove_cvref_t<InputTensor>> const>,
typename tensor_mdspec_t<std::remove_cvref_t<InputTensor>>::accessor_type>;
References
  1. When designing C++ concepts or type traits that query nested types or observers, normalize the template arguments (e.g., using std::remove_cvref_t) to ensure they correctly handle reference types (such as those obtained via decltype).

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.

Applied in b785797. No current call site passes a reference type, so this is hardening rather than a fix.

@ian-review-bot

Copy link
Copy Markdown
Collaborator

Review Bot: review of exact head b0a4c65ecdaf1b1dd2d4e0357c770ce80330f4d7

Found 1 issue: a documentation sentence (inline comment on docs/tensor/scalar_tensors_and_transfer.md:250). I found no code defect.

What the PR does. Materializing a tensor now stays in the source's storage domain. CTAD, make_tensor and async make_tensor pick CudaStorage for CUDA-only accessors, and the result is allocated on the source buffer's device. The owning Tensor converting constructor rejects sources from the other domain (basic_tensor.hpp, copy.hpp, cuda_storage.hpp). The host-only value decompositions and solvers build their workspace through detail::make_host_work_tensor, which uses static_assert to reject CUDA operands where the code used to copy them to the host silently. Async to_host no longer delegates to make_tensor.

What I reviewed.

  • The full diff, including the four doc files.
  • The domain model (enable_accessor_in_domain in concepts.hpp, conjugate_accessor.hpp, transform_view.hpp, generated_accessor.hpp, cuda_accessor.hpp).
  • SourceAllocationContext against CudaStorage::context_type.
  • Consumers outside the diff that still materialize or copy into host work:
    • at_precision and mutable_async_tensor_value, which use CTAD.
    • materialize_self_adjoint_eigh_work_matrix and can_transfer_*_storage, which are restricted to HostStorage.
    • slogdet, which goes through lu_factor_impl and so hits the new static_assert.
    • Block SVD, which assembles host sector matrices.
  • The CUDA copy backend's host↔device planning (linalg/backends/cuda/copy.hpp).

Verified (polaron, GCC 13.3 Debug, CUDA 12.9, -DCMAKE_CUDA_ARCHITECTURES=70 -DUNI20_ENABLE_CUDA=ON -DUNI20_BACKEND_CUSOLVER=ON, 2× Quadro GV100):

  • Test binaries. These targets built and passed: uni20_linalg_cuda_tests (64), uni20_cuda_runtime_tests (86), uni20_cusolver_tests (24), uni20_cublas_tests (39), uni20_krylov_block_tensor_cuda_tests (8), uni20_tensor_tests (48), uni20_tensor_async_tests (18), uni20_linalg_tests (384), uni20_symmetry_tests (195), uni20_lu_tests (9), uni20_least_squares_tests (13), uni20_linalg_decompositions_tests (3) and uni20_docs_tests (1). The two new runtime tests ran rather than skipping, since two devices were visible. Both CudaTensorTest.StreamOrdered* tests passed in this single run.
  • Compile-failure probes (-fsyntax-only, one translation unit per case). On a CudaTensor<double, 2> these all fail at head with the new static_assert text: svd, consuming svd(std::move(m)), qr(conj(m)) on a complex CUDA matrix, async eigh(Async<CudaMatrix>), lu_factor, solve, slogdet and truncated_svd. The same calls compile against the base headers (0551fbe0), so the probes tell old from new behaviour.
  • Type checks. At head:
    • Tensor<double,2> from CudaTensor: not constructible (base: constructible).
    • CudaTensor<complex> from conj(host): not constructible (base: constructible).
    • make_tensor<RowMajor>(cuda): returns a row-major CudaTensor (base: a host tensor).
  • Extra runtime probes (two GPUs, sources on device 1):
    • make_tensor<RowMajor>(d) and CudaTensor<double,2,RowMajor>(d) stay on device 1 with correct values.
    • Async make_tensor stays on device 1 even with the scheduler bound to device 0.
    • Async to_host returns correct values.
  • No cuSOLVER (-DUNI20_BACKEND_CUSOLVER=OFF, configure only): singular_values(CudaTensor) hits the same static_assert, and tests/linalg/test_cuda_copy.cpp compiles with -Werror.
  • Clang 19.1.1 Debug, CPU-only (CI Clang options, -DUNI20_ENABLE_FLOAT80=OFF): uni20_tensor_tests, uni20_tensor_async_tests, uni20_linalg_tests (393), uni20_linalg_decompositions_tests, uni20_lu_tests, uni20_least_squares_tests (14), uni20_symmetry_tests and uni20_docs_tests built and passed. Clang 19 -fsyntax-only of test_cuda_copy.cpp in the CUDA+cuSOLVER configuration also succeeded.
  • Docs. The async pageable-transfer claim in scalar_tensors_and_transfer.md matches linalg/async/copy.hpp, and AsyncPageableCopiesAndMaterializationUseBlockingCudaBoundary passes. Renaming copy_to to copy matches the API.

Not checked.

  • A full unfiltered build and ctest in any configuration. I built only the targets listed above, so the CLI, examples and Python bindings were not built.
  • The Release, wide-scalar (MPLAPACK on/off) and MPREAL configurations.
  • A Clang build with CUDA enabled (only syntax-checked).
  • CUDA 13.
  • Bethe at its pinned commit. For host sources the deduced type is unchanged by reading (HostStorage with DefaultAccessorFactory), but I did not build Bethe.
  • least_squares on CUDA operands. I did not probe the claim that it already failed to compile.
  • The repeated-run flakiness of CudaTensorTest.StreamOrdered*.
  • GitHub CI results for this head; no CI logs were provided.

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


An explicit-output form may be useful for allocation reuse and asynchronous
execution:
The explicit-output form reuses an allocation and accepts strided views:

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 b0a4c65ecdaf1b1dd2d4e0357c770ce80330f4d7

This new sentence says copy(destination, source) "accepts strided views", but a host↔device copy refuses any pair that is not contiguous with the same physical order. Now that copy is one of the three allowed transfers, readers will reach for it to transfer a strided or opposite-layout CUDA view.

  • Code: src/uni20/linalg/backends/cuda/copy.hpp:214-224 sets runtime_copy_eligible only when both mappings are unique and exhaustive and physical_orders_match(output, input). If not, only device-to-device falls back to the elementwise kernel. Host↔device returns KernelAttempt::unsupported_layout.
  • Reproduced (CUDA 12.9, GCC 13.3 Debug, -DUNI20_ENABLE_CUDA=ON -DUNI20_BACKEND_CUSOLVER=ON, Quadro GV100). The source is a column-major CudaTensor<double, 2> (2×3) and the destination a preallocated Tensor<double, 2, HostStorage, RowMajor>, so both are contiguous and only the order differs. The reverse direction (column-major host source into a row-major CudaTensor) behaves the same:
    D2H colmajor->rowmajor threw: kernel dispatch failed for operation 'copy': all runtime candidates declined
    H2D colmajor->rowmajor threw: kernel dispatch failed for operation 'copy': all runtime candidates declined
    
    The next paragraph ("to_host and to_device are implemented for canonical contiguous Tensor layouts. Fixed-output copy uses the same transfer backend") already implies this limit, so the two paragraphs contradict each other.

Suggested fix: drop "and accepts strided views". Or say that host↔device copy needs contiguous operands with matching physical order, and that a strided CUDA view goes through make_tensor (same device) and then to_host.

Not checked. I did not test a padded layout_stride mapping that is otherwise in matching order (the code reading above covers it). I did not check whether a staging fallback is planned.

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.

Agreed, thanks. Fixed in b785797: the paragraph now says host/device copy requires contiguous operands with the same physical order, that only CUDA-to-CUDA copies accept strided or reordered views, and that a strided CUDA view goes through make_tensor on its device and then to_host.

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 b7857971fc6051f9296e64e1e245541082de930c

Confirmed fixed. The new paragraph in docs/tensor/scalar_tensors_and_transfer.md (lines 250–261) matches src/uni20/linalg/backends/cuda/copy.hpp:214-226. Host↔device copies need both mappings contiguous with matching physical order; only device-to-device copies fall back to the elementwise kernel.

I also ran the route the doc now recommends. Setup: CUDA 12.9, GCC 13.3 Debug, -DUNI20_ENABLE_CUDA=ON -DUNI20_BACKEND_CUSOLVER=ON, 2× Quadro GV100. The source is a layout_stride 2×3 CUDA view (MdspecTensorView over a CudaTensor<double, 1> on device 1, buffer offset 1, strides {1,3} and {3,1}).

  • make_tensor(view) returned a CudaStorage tensor on device 1.
  • to_host of that tensor gave the expected values in both cases.
  • A direct copy(host_tensor, view) was declined with "cuda_reference … unsupported layout", as the paragraph now says. Note that this aborted the process (exit 134) rather than throwing an exception I could catch. This behaviour is older than this PR, and the doc does not promise an exception.

Not checked. A padded but matching-order host↔device pair at runtime (I only read the code for that case). Clang and Release builds of the probe.

ianmccul and others added 3 commits November 13, 2026 00:45
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>
Owning-tensor materialization ignored the source's storage domain.
make_tensor(view) and Tensor CTAD always produced HostStorage, and the
Tensor converting constructor accepted any readable view, so a CUDA
source could become a host tensor without an explicit transfer. The
linear-algebra value operations materialize their workspaces through
make_tensor, so svd, eigh, qr, lq, solve and LU on a CudaTensor silently
copied it to the host and ran LAPACK.

Materialization now stays in the source's storage domain. Accessors
already declare where they may be evaluated (host_access_domain,
cuda_access_domain); a source may be materialized into storage whose
accessor shares one of its declared domains. Accessors that declare no
domain keep the previous behaviour.

- The converting constructor rejects cross-domain sources, so host and
  CUDA tensors no longer convert into each other. A CUDA result is
  allocated on the source buffer's device, so construction never implies
  a peer transfer either.
- make_tensor(view) and CTAD from a view deduce CUDA storage for CUDA
  sources. make_tensor of an owning CudaTensor makes a same-device copy;
  owning CUDA tensors remain non-copyable, so CTAD from one still does
  not compile. Bare-mdspan make_tensor requires a host-readable mdspan.
- Async make_tensor follows the same rule; async to_host no longer
  delegates to make_tensor.
- Host-only linear-algebra value operations build their workspace
  through make_host_work_tensor, which static_asserts that the operand
  is host-readable and points the caller at to_host.

to_host, to_device and copy(destination, source) are the only transfers
between host and CUDA storage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Host/device copy(destination, source) requires contiguous operands with
the same physical order; only CUDA-to-CUDA copies accept strided or
reordered views. Document that, and the make_tensor-then-to_host route
for a strided CUDA view, instead of claiming that copy accepts strided
views.

Normalize SourceAllocationContext and HostReadableTensor with
remove_cvref_t so reference template arguments are classified like
their referred-to tensor types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ianmccul
ianmccul force-pushed the feature/no-implicit-host-copies branch from b0a4c65 to b785797 Compare November 12, 2026 16:51
@ian-review-bot

Copy link
Copy Markdown
Collaborator

Review Bot: review of exact head b7857971fc6051f9296e64e1e245541082de930c

No defect found. The earlier documentation finding (#122 (comment)) is fixed; the recheck is in that thread.

What changed since b0a4c65.

  • The branch was rebased onto the updated Add device-resident cuSOLVER singular values #121 base, which gained 20f85e80 "Use consumed CUDA matrices as cuSOLVER workspace". git range-diff 0551fbe0..b0a4c65 20f85e80..b7857971 shows this PR's first commit as identical (=).
  • The one new commit, b7857971, does two things:
    • It rewrites the copy(destination, source) paragraph in scalar_tensors_and_transfer.md.
    • It normalizes SourceAllocationContext (basic_tensor.hpp) and HostReadableTensor (tensor/copy.hpp) with std::remove_cvref_t.

What I reviewed.

  • The new commit, and the doc paragraph against linalg/backends/cuda/copy.hpp:214-226.
  • How the rebased base interacts with this PR. The consuming singular_values overloads (svd.hpp:764-793) now also admit cuda_resident_singular_values<MatrixTensor>(). I checked that in builds without cuSOLVER a consumed CUDA matrix still falls through to the preserving overload, and so to the make_host_work_tensor static_assert.
  • The async consuming selection (linalg/async/svd.hpp:70-86).
  • The PR's documentation claims about cuSOLVER routing (docs/backends/cuda/cusolver.md).

Verified (polaron, GCC 13.3 Debug, CUDA 12.9, -DCMAKE_CUDA_ARCHITECTURES=70 -DUNI20_ENABLE_CUDA=ON -DUNI20_BACKEND_CUSOLVER=ON, OpenBLAS, 2× Quadro GV100):

  • Test binaries. These built and passed at this head: uni20_cusolver_tests (24, including the base's new ConsumingCallUsesOrCopiesDeviceWorkspace for all four scalars), uni20_linalg_cuda_tests (64), uni20_cuda_runtime_tests (86), uni20_cublas_tests (39), uni20_krylov_block_tensor_cuda_tests (8), uni20_tensor_tests (48), uni20_tensor_async_tests (18), uni20_linalg_tests (384), uni20_linalg_decompositions_tests (3), uni20_lu_tests (9), uni20_least_squares_tests (13), uni20_docs_tests (1) and uni20_symmetry_tests (195). Both CudaTensorTest.StreamOrdered* tests passed in this single run.
  • Concept normalization. These static_asserts compile in the CUDA configuration:
    • HostReadableTensor holds for Tensor<double,2>& and Tensor<double,2> const&, and does not hold for CudaTensor<double,2>& or CudaTensor<double,2> const&.
    • SourceAllocationContext<CudaStorage, X> holds for CudaTensor& and CudaTensor const&, and does not hold for Tensor&.
    • Tensor from CudaTensor, and CudaTensor from Tensor, are both still not constructible.
  • Doc route. For a layout_stride CUDA view on device 1, make_tensor stays on device 1 and to_host gives correct values. A direct host copy of that view is declined. Details are in the thread reply.
  • No cuSOLVER (-DUNI20_BACKEND_CUSOLVER=OFF, -fsyntax-only):
    • singular_values(std::move(cuda_matrix)), async singular_values(std::move(Async<CudaMatrix>)) and singular_values(cuda_matrix const&) all fail with the PR's static_assert text (tensor/copy.hpp:99).
    • tests/linalg/test_cuda_copy.cpp compiles with -Werror.

Not checked.

  • A full unfiltered build and ctest. The CLI, examples and Python bindings were not built.
  • Release, wide-scalar (MPLAPACK on/off), MPREAL and Clang 19 builds at this head. Clang was only checked at b0a4c65, and the new commit touches only the two concept definitions and docs.
  • CUDA 13.
  • Bethe at its pinned commit.
  • Repeated-run flakiness of CudaTensorTest.StreamOrdered*.
  • GitHub CI for this head; no CI logs were provided.

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

@ianmccul
ianmccul changed the base branch from feature/cusolver-singular-values to main November 12, 2026 18:32
@ianmccul
ianmccul merged commit 35f049d into main Nov 12, 2026
@ianmccul
ianmccul deleted the feature/no-implicit-host-copies branch November 12, 2026 19:03
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