Skip to content

fix(UniTensor): rebuild block metadata instead of mutating shared blocks in-place#998

Merged
pcchen merged 1 commit into
masterfrom
fix/blockut-permute-shared-blocks
Jul 7, 2026
Merged

fix(UniTensor): rebuild block metadata instead of mutating shared blocks in-place#998
pcchen merged 1 commit into
masterfrom
fix/blockut-permute-shared-blocks

Conversation

@yingjerkao

Copy link
Copy Markdown
Collaborator

Problem

Fixes the core of #724. UniTensors can share block Tensors (e.g. relabel() copies the block handles), but the in-place permute_/reshape_/contiguous_/combineBonds implementations mutated per-block metadata directly — corrupting every other holder of the same block (wrong shapes, out-of-bounds reads).

Fix

Per the approach agreed in the issue thread (manuschneider's proposal, endorsed by ianmccul): 13 sites across DenseUniTensor/BlockUniTensor/BlockFermionicUniTensor now rebuild block metadata via the non-mutating Tensor::permute()/reshape()/contiguous() and rebind (_blocks[i] = _blocks[i].permute(...)) — storage stays shared, metadata detaches. The one deliberate remainder: Block*UniTensor::contract's transient mutate-restore of blocks (three hot-path branches) is marked with in-code KNOWN ISSUE (#724) blocks and breadcrumbs and is being fixed separately (in-flight follow-up session).

Testing

Six new gtests: shared-block permute_ (Dense, Block, BlockFermionic — the latter also pins the signflip vector), shared-block reshape_ (Dense), and two non-shared control tests. Red-proved by reverting fix sites. Full suite: 1073 passed / 11 skipped / only the 4 pre-existing failures addressed by #977.

🤖 Generated with Claude Code

…cks in-place (#724)

permute_/reshape_/contiguous_ on a UniTensor whose blocks are shared with
another UniTensor (e.g. via relabel(), which is documented to share block
storage) corrupted the other holder's shape/metadata, since the in-place
Tensor::permute_()/reshape_()/contiguous_() mutate the block's Tensor_impl
metadata (shape/mapper/invmapper) directly, and that Tensor_impl can be
aliased by several Tensor handles.

Fix per the consensus in the issue thread (manuschneider's proposal,
endorsed by ianmccul): build new per-block Tensor metadata via the
non-mutating Tensor::permute()/reshape()/contiguous() and assign the
result back into the block slot (_block / _blocks[i]), instead of calling
the in-place Tensor::permute_()/reshape_()/contiguous_(). Storage stays
shared (no data copy); only the block's own metadata is rebuilt, so other
UniTensors sharing that block keep their original metadata intact.

Converted sites:
- DenseUniTensor::permute_ (both cytnx_int64- and label-mapper overloads)
- DenseUniTensor::reshape_ (non-diag branch)
- DenseUniTensor::combineBonds (in-place reshape of _block)
- DenseUniTensor::contract (tmpL reshape, which may alias this->_block
  when already contiguous; the post-Kron "reshape back" is now unneeded
  since tmpL is rebound rather than mutated)
- DenseUniTensor::contiguous_ (include/UniTensor.hpp)
- BlockUniTensor::permute_
- BlockUniTensor::combineBonds (in-place reshape of _blocks[b])
- BlockUniTensor::contiguous_ (include/UniTensor.hpp)
- BlockFermionicUniTensor::permute_ and permute_nosignflip_
- BlockFermionicUniTensor::combineBonds (in-place reshape of _blocks[b])
- BlockFermionicUniTensor::contiguous_ (include/UniTensor.hpp)

Left unconverted (documented in-code as a known follow-up, not fixed
here): the block permute_/reshape_ pairs inside
BlockUniTensor::contract/BlockFermionicUniTensor::contract that
temporarily mutate this->_blocks[a]/Rtn->_blocks[b] in place and restore
them afterward (three near-duplicate branches: the UNI_MKL integer-dtype
Matmul fallback, the UNI_MKL Gemm_Batch path, and the non-MKL Matmul
fallback). Converting these requires threading local Tensor variables
through the Gemm_Batch batching loop and dtype-casting/cleanup logic in a
hot path; risking a rushed rewrite there was judged worse than leaving it
flagged for a follow-up.

Tests (gtest, TDD red/green): two UniTensors sharing blocks via relabel(),
permute_/reshape_ one and assert the other's shape/labels/data are
unchanged and still readable (DenseUniTensor: permute_ and reshape_;
BlockUniTensor: permute_; BlockFermionicUniTensor: permute_ incl. the
sign-flip state), plus control tests confirming non-shared (cloned)
UniTensors are unaffected as before.

Full C++ suite: 1088 ran / 1073 passed / 11 skipped / 4 known-failing
(pre-existing linalg_Test.BkUt_*Svd_truncate_return_err* failures,
unrelated to this change).

Co-Authored-By: Claude Fable 5 <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 addresses issue #724 where in-place operations (such as contiguous_(), permute_(), and reshape_()) on a UniTensor sharing underlying block Tensors with another instance (e.g., via relabel()) would corrupt the shared block's metadata. The fix replaces these in-place mutations with non-mutating equivalents and rebinds the block variables. Additionally, comprehensive regression and control tests have been added across BlockFermionicUniTensor, BlockUniTensor, and DenseUniTensor to verify the fix. There are no review comments to address, and I have no further feedback to provide.

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.

@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 13.33333% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 30.92%. Comparing base (d02cb29) to head (9a28e38).
⚠️ Report is 4 commits behind head on master.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/DenseUniTensor.cpp 0.00% 1 Missing and 4 partials ⚠️
include/UniTensor.hpp 40.00% 0 Missing and 3 partials ⚠️
src/BlockFermionicUniTensor.cpp 0.00% 1 Missing and 2 partials ⚠️
src/BlockUniTensor.cpp 0.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #998      +/-   ##
==========================================
+ Coverage   30.88%   30.92%   +0.04%     
==========================================
  Files         229      229              
  Lines       34758    34761       +3     
  Branches    14409    14401       -8     
==========================================
+ Hits        10734    10751      +17     
+ Misses      16720    16699      -21     
- Partials     7304     7311       +7     
Flag Coverage Δ
cpp 30.44% <13.33%> (+0.04%) ⬆️
python 59.41% <ø> (ø)

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

Components Coverage Δ
C++ backend 32.26% <13.33%> (+0.05%) ⬆️
Python bindings 17.28% <ø> (ø)
Python package 59.41% <ø> (ø)
Files with missing lines Coverage Δ
src/BlockUniTensor.cpp 37.26% <0.00%> (ø)
include/UniTensor.hpp 47.84% <40.00%> (+0.12%) ⬆️
src/BlockFermionicUniTensor.cpp 32.76% <0.00%> (ø)
src/DenseUniTensor.cpp 50.17% <0.00%> (-0.06%) ⬇️

... and 1 file with indirect coverage changes


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 d02cb29...9a28e38. 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

pcchen commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Code Review

Core of #724: UniTensors can share block Tensors (e.g. relabel() copies handles), and the in-place permute_/reshape_/contiguous_/combineBonds paths mutated per-block metadata directly, corrupting every other holder. This converts 13 sites to the non-mutating rebind pattern (_blocks[i] = _blocks[i].permute(...)) — storage stays shared, metadata detaches.

Correctness — verified (read all 13 sites)

  • The rebind pattern is correct throughout. permute()/reshape()/contiguous() return a fresh Tensor (new metadata, shared storage — no data copy for permute/reshape); reassigning _blocks[i]/_block gives this UniTensor a new block object while the old one (held by shared holders) is untouched. ✅
  • All families consistent:contiguous_ (Dense/Block/BlockFermionic), 5× permute_ (2 Dense + 1 Block + 2 BlockFermionic), 4× reshape_, plus the Kron site. ✅
  • Good judgment, not a blind sweep: the is_diag reshape branch (DenseUniTensor.cpp:718) is left as in-place reshape_ because linalg::Diag() allocates a fresh, never-shared Tensor — correctly identified and documented. ✅
  • The one non-mechanical site — Kron (DenseUniTensor.cpp:1071): old code did tmpL.reshape_(shape_L) … reshape_(old_shape_L) (mutate + restore); tmpL can alias the shared _block, so the transient mutation was the hazard. New code rebinds tmpL = tmpL.reshape(shape_L) and drops the restore since tmpL is terminal (last use is Kron). Correct, and documented. ✅
  • Deferred contract() paths are honestly marked with KNOWN ISSUE (#724) breadcrumbs across all three branches — the remainder fix(UniTensor): thread non-mutating block views through contract() #1005 completes. ✅

Tests — strong and CI-verified

Six gtests across Dense/Block/BlockFermionic: shared-block permute_/reshape_ assert the other holder is fully intact (shape, labels, values — and signflip for fermionic), with reads wrapped in try/catch to catch the out-of-bounds a corrupted mapping would cause; plus non-shared control tests to guard against over-isolation. Red-proved by reverting the fixes, and BuildAndTest passes on all platforms here (real coverage, not local-only).

Nits

  • Unrelated cosmetic whitespace change in the fermionic test (const char *fnameconst char* fname, line 244) — harmless, likely clang-format.
  • codecov/patch red is advisory (the deferred-contract KNOWN ISSUE lines aren't covered — expected).

Verdict

Clean, correct, well-tested foundation fix — the rebind pattern is applied consistently and thoughtfully (including the two sites correctly left alone), the contract() deferral is honest, and it's fully CI-green including BuildAndTest. Approve. Pairs with #1005, which completes contract().

Posted by Claude Code on behalf of @pcchen

@pcchen pcchen left a comment

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.

Approving. Correct and well-tested core #724 fix: the non-mutating rebind pattern (_blocks[i] = _blocks[i].permute(...)) is applied consistently across all 13 sites — storage stays shared, metadata detaches — with good judgment on the two sites correctly left in-place (the is_diag/Diag() branch and the terminal Kron scratch). The contract() paths are honestly deferred to #1005 with in-code KNOWN ISSUE breadcrumbs. Six regression gtests (shared + control, cross-holder intactness incl. the fermionic signflip, OOB-guarded reads) are red-proved and pass in CI on all platforms (BuildAndTest green). Only codecov/patch is red (advisory — the deferred KNOWN ISSUE lines).

Posted by Claude Code on behalf of @pcchen

@pcchen
pcchen merged commit 246fa1a into master Jul 7, 2026
18 of 19 checks passed
@pcchen
pcchen deleted the fix/blockut-permute-shared-blocks branch July 7, 2026 14:55
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.

2 participants