Skip to content

svd: dqds takes its work arrays from the caller's MemStack (#156) - #157

Merged
shinaoka merged 1 commit into
mainfrom
fix/156-dqds-scratch
Oct 29, 2026
Merged

shinaoka merged 1 commit into
mainfrom
fix/156-dqds-scratch

Conversation

@shinaoka

@shinaoka shinaoka commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Closes #156.

Problem

In 0.26.0 the values-only SVD allocates on the heap, although the caller passes a MemStack sized by svd_scratch. 0.25.0 did not allocate. tlinalg's alloc_counts test fails on it, which blocks the tlinalg 0.8.0 release.

There are two sources, both in dqds::singular_values:

  • Work arrays: the four arrays (the squared factors a, b and the ping-pong buffers a2, b2) were built with vec!. That is two allocations at 1 × 1 and four above.
  • Sort: the final descending sort was the stable sort_by. Its merge buffer moves from the stack to the heap for large slices: one allocation for f64 and c64 at 700 × 700 (f32 and c32 still fit on the stack).

Fix

  • dqds::singular_values_scratch::<T>(n) describes the four arrays, and singular_values takes them from the MemStack with make_with.
  • The values-only bidiagonal scratch (bidiag_real_svd_scratch with no U and no V) includes it, through any_of with the fallback paths, since dqds runs first and a failed dqds then runs the rotation path. So svd_scratch and the complex route include it too.
  • The sort is sort_unstable_by. All equal keys are equal values, so the result does not change.
  • No arithmetic change.

Tests

  • New faer/tests/svd_values_no_alloc.rs: its own test binary with a process-wide counting allocator.
    • It runs values-only svd on Par::Seq for f64/f32/c64/c32 at 1×1, 2×2, 17×17, 40×33, 33×40, 128×128, 300×60 (the QR route) and 700×700.
    • It warms up first, then counts allocations during the call.
    • Without the change it counts [2, 2, 2, 2] at 1 × 1 (the issue's numbers).
    • With the arrays fixed but the stable sort kept, it counts [1, 0, 1, 0] at 700 × 700.
    • With this change it counts zero everywhere.
  • dqds unit tests: updated to pass a scratch buffer.
  • cargo test -p t4a-faer --lib linalg::svd: 74 passed.
  • The LAPACK SVD tests (linalg::svd::tests --include-ignored): 88/88 passed (131 s).
    • In the full run-lapack-validation.py run, every other family passed.
    • The svd family alone hit the runner's 120 s limit inside parallel_paths, while primerose's load average was 60. Run directly without that limit, it passes, as above.
  • cargo check --no-default-features and --features std: no warning or error from this change.

Review

The independent post-review (gpt-6.1-sol) found no BLOCKER or IMPORTANT issues. It checked:

  • the scratch composition on the real, complex, square, QR-reduced and wide routes, and below the recursion threshold;
  • that any_of is right because dqds releases its arrays before the fallback runs;
  • alignment, and dropping the arrays for non-Copy scalars;
  • the other callers, which all request vectors.

Its one MINOR (a redundant f64 copy in the test) is applied. The test checks steady-state calls after a warm-up; Par::Team and cold calls are outside its scope.

Records

🤖 Generated with Claude Code

The values-only SVD allocated on the heap in 0.26.0 although the caller
passes a MemStack sized by svd_scratch: dqds::singular_values built its four
work arrays with vec! (two allocations at 1 x 1, four above), and its stable
sort moves its merge buffer to the heap for large slices (700 x 700 f64).
The arrays now come from the stack through dqds::singular_values_scratch,
which the values-only bidiagonal scratch (and so svd_scratch) includes, and
the sort is sort_unstable_by. faer/tests/svd_values_no_alloc.rs counts zero
allocations for f64/f32/c64/c32 from 1 x 1 to 700 x 700; before, [2, 2, 2, 2]
at 1 x 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shinaoka
shinaoka force-pushed the fix/156-dqds-scratch branch from eedf9c5 to 02fcefd Compare October 29, 2026 15:29
@shinaoka
shinaoka merged commit b277497 into main Oct 29, 2026
7 checks passed
@shinaoka
shinaoka deleted the fix/156-dqds-scratch branch October 29, 2026 15:46
shinaoka added a commit that referenced this pull request Oct 30, 2026
…allocation (#159)

The minor version moves because Par::Team and ParShape::Team carry
threadteam's types and the requirement moved from threadteam 0.1 to 0.2
(#158): a crate that lends a threadteam 0.1 team to faer does not compile
against it (reproduced: E0308 on Par::Team). Carries #157 (#156).
t4a-faer-traits stays 0.24.2. CHANGELOG and PATCHSET record the release.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

svd: the values-only dqds path allocates on the heap despite the caller's MemStack (0.26.0)

1 participant