Skip to content

aci: stop the elementwise sweep once the rank saturates at max_bond_dim - #591

Merged
shinaoka merged 1 commit into
mainfrom
fix/aci-rank-saturation-early-exit
Aug 10, 2026
Merged

aci: stop the elementwise sweep once the rank saturates at max_bond_dim#591
shinaoka merged 1 commit into
mainfrom
fix/aci-rank-saturation-early-exit

Conversation

@shinaoka

Copy link
Copy Markdown
Member

Problem

elementwise_batched in crates/tensor4all-aci/src/elementwise.rs only exits its sweep loop through the tolerance branch of convergence_criterion_like_julia, which requires errors.last() <= options.tolerance before the rank-plateau check can apply. Once the solution rank reaches options.max_bond_dim the sweep has no remaining freedom to add pivots, so under a binding cap the tolerance can never be met. The loop then burns every remaining max_iters sweep at full rank doing nearly dead work.

Found while running tensor4all/tensor4all-benchmark case elementwise_gauss2d_scaling. At N=32 (chi_in=116, tolerance 1e-8, max_bond_dim=chi_in) ACI reaches rank 116 on sweep 2 and then runs all 20 sweeps, per-sweep ranks [112, 116, 116, ..., 116], with the pivot error metric flat near 2.1e-8. Uncapped, the same problem converges in 3 sweeps. Same story at N=64.

Fix

Break when the solution rank has stayed at options.max_bond_dim for options.min_iters consecutive sweeps, added as a separate rank_is_saturated check next to the existing criterion rather than folded into it, so the ported criterion keeps its shape.

Design notes:

  • This is the all(lastranks .>= maxbonddim) disjunct of the Julia convergencecriterion that convergence_criterion_like_julia otherwise ports, evaluated over the same trailing window. tensor4all-treetci gained the equivalent exit in its own sweep loop in treetci: add missing convergence check to optimize's sweep loop #575, so the two sweep loops now agree.
  • The dwell of min_iters sweeps at the cap is deliberate. Breaking on the very sweep the cap is first hit would cut off pivot re-selection inside the now fixed rank, which can still lower the error; requiring the rank to sit at the cap for a full window keeps that opportunity.
  • min_iters == 0 disables the exit, matching how it disables the tolerance criterion.
  • max_bond_dim defaults to usize::MAX, which is unreachable, so unconstrained runs take exactly the path they took before. Behavior for non-saturated runs is unchanged.

A capped run now returns normally with the last AciResult::errors entry above tolerance. Since that could be misread as a failure, both the elementwise_batched rustdoc and the AciResult::errors field doc now state that a binding max_bond_dim finishes above tolerance by design, and point callers at the last ranks entry to tell a rank-limited run from a converged one.

Verification

End-to-end, benchmark elementwise_gauss2d_scaling with BENCH_NS=8,16,32,64 BENCH_RUNS=1 BENCH_WARMUPS=0, aci arm wall time:

N before after speedup
8 0.0434s 0.0440s 1.0x
16 0.0781s 0.0804s 1.0x
32 0.8066s 0.0772s 10.4x
64 1.7250s 0.1522s 11.3x

N=8 and N=16 do not hit the cap and are unchanged within run to run noise. Sampled relative errors stay at the same order (N=32: 1.04e-8 to 4.95e-9; N=64: 2.04e-8 to 1.85e-8), and the zipup_treetn and fit_treetn arms are unaffected. The before column was measured at the benchmark's current pin 7cfec22, which is identical to main for crates/tensor4all-aci (git log 7cfec22..main -- crates/tensor4all-aci is empty).

Tests:

  • New capped_elementwise_run_stops_once_rank_saturates: two value-dependent rank-4 trains whose pointwise product genuinely needs rank 16, run against max_bond_dim: 4 with an unreachable 1e-10 tolerance. It asserts the cap is actually binding, that the run uses at most min_iters + 2 sweeps, and that the trailing ranks entries equal the cap. On the unfixed loop it fails with expected an early exit, got 20 sweeps with ranks [4, 4, ..., 4].
  • New rank_saturation_needs_a_full_dwell_at_the_cap covers the dwell boundary, the unreachable default cap, and min_iters == 0.
  • cargo nextest run --release --workspace --exclude library-panic-audit: 2718 passed. The excluded library-panic-audit tool tests fail on this machine independently of this branch, which touches nothing under tools/.
  • cargo fmt --all -- --check, cargo clippy --workspace --all-targets -- -D warnings, cargo doc -p tensor4all-aci --no-deps, and python3 scripts/repository-rules-review.py --base main --worktree --dry-run (verdict: pass) all clean.

🤖 Generated with Claude Code

The elementwise ACI convergence loop only exits on the tolerance branch of
convergence_criterion_like_julia, which requires the pivot error to reach
options.tolerance before the rank-plateau check can apply. Once the solution
rank reaches options.max_bond_dim the sweep can no longer add pivots, so under
a binding cap the tolerance can never be met and the loop burns every
remaining max_iters sweep at full rank.

Add the missing exit: break when the rank has stayed at max_bond_dim for
min_iters consecutive sweeps. This is the all(lastranks .>= maxbonddim)
disjunct of the Julia convergencecriterion that the rest of the criterion
already ports, over the same trailing window. tensor4all-treetci gained the
equivalent exit in #575. The min_iters dwell means the loop does not stop on
the sweep where the cap is first reached, when pivot re-selection inside the
fixed rank can still improve the error, and min_iters == 0 disables the exit
exactly as it disables the tolerance criterion. The default max_bond_dim of
usize::MAX is unreachable, so unconstrained runs are unaffected.

Found while running tensor4all/tensor4all-benchmark case
elementwise_gauss2d_scaling. At N=32 (chi_in=116, tolerance 1e-8,
max_bond_dim=chi_in) ACI reached rank 116 on sweep 2 and then ran all 20
sweeps with the error flat near 2.1e-8. Measured aci wall time for
BENCH_NS=8,16,32,64 BENCH_RUNS=1:

  N     before    after
  8     0.0434s   0.0440s
  16    0.0781s   0.0804s
  32    0.8066s   0.0772s
  64    1.7250s   0.1522s

Sampled relative errors stay at the same order (N=32: 1.04e-8 to 4.95e-9,
N=64: 2.04e-8 to 1.85e-8), and the zipup_treetn and fit_treetn arms are
unchanged.

Because a capped run now returns with the last errors entry above tolerance,
document that outcome on both elementwise_batched and AciResult::errors so
callers read it as rank limiting rather than as a failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Repository rules review

Repository rules review (a6a51de4cc9a082265eba773bbaf65b828a43512...da55383b74a5ff71df7c7d764b1878534afd3a67)
Verdict: pass
Findings:
- [warn] llm-skipped (External LLM Review) <unknown>: External LLM review was skipped
  External LLM review is permanently disabled in this repository; deterministic rule checks run instead.

@shinaoka
shinaoka merged commit 1b9a517 into main Aug 10, 2026
9 checks passed
@shinaoka
shinaoka deleted the fix/aci-rank-saturation-early-exit branch August 10, 2026 11:10
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