A bug spotted by my student Jerry Chen:
Summary
cytnx.linalg.Svd_truncate on symmetric (UTenType.Block) UniTensor appears to return the wrong value in the return_err path.
For block/symmetric tensors, the "discarded singular values" output can overlap with or match the kept singular values. This is inconsistent with the dense tensor path and appears to come from
selecting the kept tail of the sorted singular spectrum instead of the discarded prefix.
The same issue appears in both:
linalg::Svd_truncate(const UniTensor&, ...)
linalg::Gesvd_truncate(const UniTensor&, ...)
Versions
This was diagnosed looking at current master. However the bug was spotted using version 1.0.1. The bug has existed since the return_err mechanism was added in commit c8a65dc.
Actual behavior
For symmetric/block UniTensor:
return_err > 1 returns Sall.get({ac::tilend(smidx)}), which is the kept tail of the sorted singular values, not the discarded values.
return_err == 1 returns Smin, which is the smallest kept singular value.
This behavior is incorrect in both paths.
Expected behavior
The symmetric/block UniTensor path should match the dense tensor implementation:
return_err > 1 should return the discarded singular values only.
return_err == 1 should return the first discarded singular value, not the smallest kept one. This is documented in arxiv:2401.01921 as returning "the largest of the truncated singular values".
Relevant source locations
Block path in Svd_truncate:
src/linalg/Svd_truncate.cpp:312-316
Block path in Gesvd_truncate:
src/linalg/Gesvd_truncate.cpp:354-358
Dense/tensor reference behavior:
src/backend/linalg_internal_cpu/memcpyTruncation.cpp:197-207
Root cause
Both block implementations sort all singular values ascending and compute a truncation threshold Smin with index smidx, then do:
outCyT.push_back(UniTensor(Sall.get({ac::tilend(smidx)})));
That slice starts at smidx and runs to the end, which corresponds to the kept values, not the discarded ones.
Similarly, for return_err == 1, the code returns Smin, which is the truncation threshold / smallest kept value, while the dense path returns the first discarded value.
Additional issue noticed while tracing
There also seems to be a separate mindim inconsistency in the block implementations:
- src/linalg/Svd_truncate.cpp:193-198 ignores mindim in one branch
- src/linalg/Gesvd_truncate.cpp:246-250 ignores mindim in the opposite branch
This looks independent of the discarded-spectrum bug, but likely belongs in the same area.
A bug spotted by my student Jerry Chen:
Summary
cytnx.linalg.Svd_truncateon symmetric (UTenType.Block)UniTensorappears to return the wrong value in thereturn_errpath.For block/symmetric tensors, the "discarded singular values" output can overlap with or match the kept singular values. This is inconsistent with the dense tensor path and appears to come from
selecting the kept tail of the sorted singular spectrum instead of the discarded prefix.
The same issue appears in both:
linalg::Svd_truncate(const UniTensor&, ...)linalg::Gesvd_truncate(const UniTensor&, ...)Versions
This was diagnosed looking at current
master. However the bug was spotted using version 1.0.1. The bug has existed since thereturn_errmechanism was added in commit c8a65dc.Actual behavior
For symmetric/block
UniTensor:return_err > 1returnsSall.get({ac::tilend(smidx)}), which is the kept tail of the sorted singular values, not the discarded values.return_err == 1returnsSmin, which is the smallest kept singular value.This behavior is incorrect in both paths.
Expected behavior
The symmetric/block
UniTensorpath should match the dense tensor implementation:return_err > 1should return the discarded singular values only.return_err == 1should return the first discarded singular value, not the smallest kept one. This is documented in arxiv:2401.01921 as returning "the largest of the truncated singular values".Relevant source locations
Block path in
Svd_truncate:src/linalg/Svd_truncate.cpp:312-316Block path in
Gesvd_truncate:src/linalg/Gesvd_truncate.cpp:354-358Dense/tensor reference behavior:
src/backend/linalg_internal_cpu/memcpyTruncation.cpp:197-207Root cause
Both block implementations sort all singular values ascending and compute a truncation threshold
Sminwith indexsmidx, then do:outCyT.push_back(UniTensor(Sall.get({ac::tilend(smidx)})));That slice starts at smidx and runs to the end, which corresponds to the kept values, not the discarded ones.
Similarly, for return_err == 1, the code returns Smin, which is the truncation threshold / smallest kept value, while the dense path returns the first discarded value.
Additional issue noticed while tracing
There also seems to be a separate mindim inconsistency in the block implementations:
This looks independent of the discarded-spectrum bug, but likely belongs in the same area.