Skip to content

fix(linalg): support non-contiguous tensor(==)tensor for GPU Cpr (#1003, #988)#1097

Merged
yingjerkao merged 2 commits into
masterfrom
fix/1003-gpu-cpr-noncontig
Jul 20, 2026
Merged

fix(linalg): support non-contiguous tensor(==)tensor for GPU Cpr (#1003, #988)#1097
yingjerkao merged 2 commits into
masterfrom
fix/1003-gpu-cpr-noncontig

Conversation

@yingjerkao

Copy link
Copy Markdown
Collaborator

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 the cuCpr_dispatch non-contiguous Bool-output kernel already 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 (flagged): GPU Cpr now accepts a non-contiguous operand and returns the correct Bool result (matching CPU) rather than throwing. This removes a restriction; it does not change any result that previously succeeded.

Testing

New CprNonContig tests in Cpr_test.cpp:

  • permuted-vs-contiguous in both operand orders, so each inverse mapper (invmapper_L and invmapper_R) is exercised;
  • GPU-vs-CPU across all dtypes;
  • an independent hand-computed identity-pattern literala[i][j]=3j+i (permuted) vs b[i][j]=3i+j (contiguous) are equal iff i==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_main builds 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 cuCpr non-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

…, #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>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

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

Comment thread src/linalg/Cpr.cpp
Comment on lines +77 to +80
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());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

critical

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.

Suggested change
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());

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

Comment thread src/linalg/Cpr.cpp
Comment on lines +77 to +80
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());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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

codecov Bot commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 72.71%. Comparing base (86cb96c) to head (c929b62).
✅ All tests successful. No failed tests found.

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           
Flag Coverage Δ
cpp 72.83% <ø> (ø)
python 64.13% <ø> (ø)

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

Components Coverage Δ
C++ backend 71.93% <ø> (ø)
Python bindings 77.70% <ø> (ø)
Python package 64.13% <ø> (ø)
Files with missing lines Coverage Δ
src/linalg/Cpr.cpp 78.10% <ø> (ø)

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 86cb96c...c929b62. 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.

@yingjerkao

Copy link
Copy Markdown
Collaborator Author

Thanks — addressed in the latest commit.

Length fix (Gemini, critical): switched the cuCpr_dispatch length argument to out._impl->storage()._impl->size() (the logical element count), matching the CPU non-contiguous path (Add.cpp:120) and the contiguous GPU branch.

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:

construction contiguous? storage vs logical
permute no equal
slice [1:3,2:5] yes (compacts) equal
permuteslice no equal

Slicing compacts to a contiguous tensor, and non-contiguity only arises from permute, which never enlarges storage — so left and out sizes coincide today and there is no live out-of-bounds write. The fix is still correct, consistent with the CPU path, and future-proof, so it's applied. (The same latent pattern exists in Add/Sub on master; I fixed the Mul/Div counterparts in #1096.)

Rank-13+ shared-memory guard (both bots): addressed centrally in #1095kGpuNonContigMaxRank is now 12 (the actual limit: 512 threads × rank × sizeof(uint64) ≤ 48 KB), so MakeGpuNonContigLayout raises a clear error for a rank the kernel cannot launch instead of failing cryptically. The by-value follow-up #1098 carries that guard for cuCpr too.

@yingjerkao
yingjerkao merged commit 6d1335c into master Jul 20, 2026
19 checks passed
@yingjerkao
yingjerkao deleted the fix/1003-gpu-cpr-noncontig branch July 20, 2026 09:48
@yingjerkao

Copy link
Copy Markdown
Collaborator Author

Superseded by #1098, which was built on top of this branch and merged to master (commit 014a386) — every commit here is now an ancestor of master, including the fix for the non-contiguous-length OOB that @gemini-code-assist flagged (the merged code uses the output's logical size, not left's storage size). Closing as already-merged.

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.

1 participant