Skip to content

Svd_truncate/Gesvd_truncate return wrong discarded singular values for symmetric UniTensor #762

@ianmccul

Description

@ianmccul

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions