fix(linalg): support non-contiguous tensor(==)tensor for GPU Cpr (#1003, #988)#1097
Conversation
…, #988) Problem: the GPU out-of-place Cpr front end rejected a non-contiguous operand with "two tensors must be contiguous" -- the last GPU elementwise op to do so (Add/Sub already accepted it; Mul/Div were enabled in #1096) -- even though the CPU path and cuCpr_dispatch's non-contiguous Bool-output kernel handle it. Fix: Cpr.cpp now passes the layout (shape + inverse mappers) to cuCpr_dispatch in the non-contiguous branch, mirroring Add/Sub/Mul/Div, instead of erroring. cuCpr already had a working non-contiguous kernel; it was simply unreachable. Behavior change: GPU Cpr now accepts a non-contiguous operand and returns the correct Bool result (matching CPU) rather than throwing. Testing: new CprNonContig tests in Cpr_test.cpp -- permuted-vs-contiguous in both operand orders (each inverse mapper exercised), GPU-vs-CPU across all dtypes AND a hand-computed identity-pattern literal (equal iff i==j, a mix a wrong gather would break). Both fail on the pre-fix code (which threw). On an RTX 4070 Ti (CUDA 13, sm_89): gpu_test_main builds clean, the new tests pass, and the full Cpr GPU suite is green. clang-format-14 clean. With #1096 this closes the GPU non-contiguous gap (#988) for all five elementwise ops (Add/Sub/Mul/Div/Cpr). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request enables non-contiguous tensor comparisons on the GPU in Cpr.cpp by dispatching to cuCpr_dispatch with layout mappers, and adds corresponding tests. A critical issue was identified where passing the storage size of the left tensor to cuCpr_dispatch can cause out-of-bounds writes and memory corruption if the tensor is sliced; it is recommended to use the output tensor's storage size instead.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| cytnx::linalg_internal::cuCpr_dispatch( | ||
| out._impl->storage()._impl, left._impl->storage()._impl, right._impl->storage()._impl, | ||
| left._impl->storage()._impl->size(), left._impl->shape(), left._impl->invmapper(), | ||
| right._impl->invmapper()); |
There was a problem hiding this comment.
Passing left._impl->storage()._impl->size() as the size parameter to cuCpr_dispatch is incorrect and dangerous. If left is a non-contiguous tensor (for example, a slice of a larger tensor), its underlying storage size (left._impl->storage()._impl->size()) can be significantly larger than its logical size. This will cause the GPU kernel to execute more threads than the size of the output tensor out, leading to an out-of-bounds write and potential memory corruption on the GPU.
Instead, you should pass the size of the output tensor, out._impl->storage()._impl->size(), which represents the correct logical number of elements to be computed (matching the contiguous branch on line 52).
Why the tests passed:
In the new tests, permuted is created using arange(0, 9, 1, dtype).reshape({3, 3}).permute({1, 0}). Since permute only changes the strides/layout without changing the underlying storage size, both the logical size and the underlying storage size of permuted are exactly 9. If you were to test with a sliced tensor (e.g., using get_slice), the underlying storage size would be larger than the logical size, and this bug would trigger an out-of-bounds write.
| cytnx::linalg_internal::cuCpr_dispatch( | |
| out._impl->storage()._impl, left._impl->storage()._impl, right._impl->storage()._impl, | |
| left._impl->storage()._impl->size(), left._impl->shape(), left._impl->invmapper(), | |
| right._impl->invmapper()); | |
| cytnx::linalg_internal::cuCpr_dispatch( | |
| out._impl->storage()._impl, left._impl->storage()._impl, right._impl->storage()._impl, | |
| out._impl->storage()._impl->size(), left._impl->shape(), left._impl->invmapper(), | |
| right._impl->invmapper()); |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c1dfb8e694
ℹ️ 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".
| cytnx::linalg_internal::cuCpr_dispatch( | ||
| out._impl->storage()._impl, left._impl->storage()._impl, right._impl->storage()._impl, | ||
| left._impl->storage()._impl->size(), left._impl->shape(), left._impl->invmapper(), | ||
| right._impl->invmapper()); |
There was a problem hiding this comment.
Guard high-rank non-contiguous Cpr launches
For non-contiguous CUDA tensors with rank >= 13 on devices using the common 48 KiB dynamic-shared-memory limit, this unconditionally enters cuCpr_dispatch's non-contiguous kernel, which launches with 512 * shape.size() * sizeof(cytnx_uint64) bytes of dynamic shared memory. That exceeds 48 KiB at rank 13, so the newly enabled path can abort through the CUDA error checks instead of returning a Bool tensor; please add a fallback/guard for ranks the kernel cannot launch.
Useful? React with 👍 / 👎.
…Cpr (#1003) Review follow-up (#1097): Gemini flagged (critical) that passing left._impl->storage()._impl->size() as the kernel length risks an out-of-bounds write for a strided view whose storage exceeds its logical size. Use out._impl->storage()._impl->size() -- the logical element count -- matching the CPU non-contiguous path (Add.cpp:120) and the contiguous GPU branch. Verified the specific counterexample is not currently reachable: Cytnx compacts slices (a sliced tensor comes back contiguous), so a non-contiguous tensor's storage always equals its logical size and left/out sizes coincide today. The fix is for correctness/consistency and future-proofing rather than a live crash. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1097 +/- ##
=======================================
Coverage 72.71% 72.71%
=======================================
Files 226 226
Lines 28151 28151
Branches 71 71
=======================================
Hits 20471 20471
Misses 7659 7659
Partials 21 21
Flags with carried forward coverage won't be shown. Click here to find out more.
Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
|
Thanks — addressed in the latest commit. Length fix (Gemini, critical): switched the On triggerability, I verified the specific counterexample (a strided view whose storage is larger than its logical size) and it does not currently occur in Cytnx:
Slicing compacts to a contiguous tensor, and non-contiguity only arises from Rank-13+ shared-memory guard (both bots): addressed centrally in #1095 — |
|
Superseded by #1098, which was built on top of this branch and merged to |
Part of #1003. With #1096 this closes the GPU non-contiguous gap (#988) for all five elementwise ops.
Problem
The GPU out-of-place Cpr front end rejected a non-contiguous operand with
two tensors must be contiguous. Call Contiguous_() or Contiguous() first— the last GPU elementwise op to do so (Add/Sub already accepted it; Mul/Div were enabled in #1096) — even though the CPU path and thecuCpr_dispatchnon-contiguous Bool-output kernel already handle it.Fix
Cpr.cppnow passes the layout (shape+ inverse mappers) tocuCpr_dispatchin the non-contiguous branch, mirroringAdd/Sub/Mul/Div, instead of erroring.cuCpralready had a working non-contiguous kernel — it was simply unreachable.Cprnow accepts a non-contiguous operand and returns the correctBoolresult (matching CPU) rather than throwing. This removes a restriction; it does not change any result that previously succeeded.Testing
New
CprNonContigtests inCpr_test.cpp:invmapper_Landinvmapper_R) is exercised;a[i][j]=3j+i(permuted) vsb[i][j]=3i+j(contiguous) are equal iffi==j, a mix a wrong gather would break.Both new tests fail on the pre-fix code (which threw "must be contiguous"). On an RTX 4070 Ti (CUDA 13, sm_89):
gpu_test_mainbuilds clean; the new tests pass; the full Cpr GPU suite (82 tests) is green. clang-format-14 clean.Note: GPU results are from the local RTX 4070 Ti — there is no GPU CI runner. The
cuCprnon-contiguous kernel still uses per-call device allocation; applying the #1095 by-value layout to it (now that this path is live) is a natural follow-up.🤖 Generated with Claude Code