Skip to content

bench: round-1 investigation harness (Float64/DMRG2 parity, TDVP + thread-scaling suites, Rusty scripts, diagnostics)#450

Draft
lkdvos wants to merge 8 commits into
bench/itensor-comparisonfrom
bench/itensor-comparison-work
Draft

bench: round-1 investigation harness (Float64/DMRG2 parity, TDVP + thread-scaling suites, Rusty scripts, diagnostics)#450
lkdvos wants to merge 8 commits into
bench/itensor-comparisonfrom
bench/itensor-comparison-work

Conversation

@lkdvos

@lkdvos lkdvos commented Jul 5, 2026

Copy link
Copy Markdown
Member

Round 1 of the MPSKit-vs-ITensorMPS performance investigation, per the methodology decisions settled on 2026-07-05.
Targets the `bench/itensor-comparison` branch, not `main`.

What changed

Suites 1–2 (finite DMRG) reworked for exact parity

  • Float64 on both sides (MPSKit's ComplexF64 constructor default was a 2–4x BLAS handicap on a real-symmetric Hamiltonian).
  • Both suites now compare MPSKit `DMRG2(trscheme = truncrank(χ))` against ITensor's default two-site `dmrg` — ITensorMPS exposes no single-site DMRG, so the earlier single-site suite-1 variant is gone.
  • Sweep budget 30 → 10 (pilot at χ = 256 running; follow-up commit if it says otherwise).
  • Parity table rewritten; rows 2/4/5 now matched/exact.

New suite 5 — TDVP throughput

  • Global quench from the Néel state, spin-1 Heisenberg N = 100; two-site growth phase then single-site measure phase, mirrored protocols; per-step (⟨Sz⟩, χ, walltime) trajectories; wall-seconds per unit physical time as the metric.
  • Krylov exponentiation tolerance matched at 1e-10 on both sides (ITensor would otherwise inherit KrylovKit's tighter 1e-12 and do strictly more work).
  • Smoke gate: mid-chain ⟨Sz⟩ agrees across libraries to ~1e-13 at both χ — same trajectory, same protocol.

New suite 7 — thread scaling

  • Suite-1 workload at one (N, χ) point, one process per (JULIA_NUM_THREADS × BLAS-threads) grid point, grid driven by the sbatch scripts.

Rusty submission scripts (`benchmark/slurm/`)

  • Six sbatch scripts (single exclusive node, constraint placeholder to pin one CPU generation) + README with submission order and the sanity-gate checklist. Manual submission only.

Diagnostics

  • Per-sweep GC seconds + allocated bytes recorded on both sides.
  • `profile_sweep.jl`: flat/tree profile of one DMRG2 sweep.
  • `plot_results.jl`: cross-library overlays, sanity-gate + time-to-accuracy tables, TDVP observable-gate/throughput panels, thread-scaling speedups.
  • `ANALYSIS.md`: question-driven skeleton, filled only from real cluster results.

Verification

  • All four suites smoke-run clean on both libraries (workstation, JULIA_NUM_THREADS=1, BLAS=1).
  • DMRG smoke energies agree to leading digits; U(1) χ=8 pair agrees to 6e-5; suite-1 χ=8 ITensor point sits in its known no-noise fixed-χ local minimum (documented).
  • TDVP observable trajectories agree to ~1e-13.
  • `plot_results.jl` runs end-to-end on the smoke JSONs and emits all comparison artifacts.

Not in this PR

Full-scale numbers (they come from Rusty via `benchmark/slurm/`, submitted manually), any `docs/src/benchmarks.md` content, SU(2)/TeNPy/cylinder suites.

🤖 Generated with Claude Code

lkdvos and others added 5 commits July 5, 2026 08:09
ITensorMPS exposes only two-site dmrg, so the single-site suite-1 variant is
dropped and both suites now compare MPSKit DMRG2 (trscheme = truncrank(chi))
against ITensor's default dmrg.
Both sides now run Float64 (MPSKit's ComplexF64 default was a 2-4x BLAS
handicap on this real-symmetric Hamiltonian).
Sweep budget cut from the 30-sweep placeholder to 10, pending the chi = 256
pilot.
Trajectory recorders on both sides additionally capture per-sweep GC seconds
and allocated bytes (investigation diagnostics).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Suite 5: global quench from the Néel state on the spin-1 Heisenberg chain,
two-site growth phase then single-site measure phase, mirrored on both
libraries; per-step (Sz, chi, walltime) trajectories and wall-seconds per
unit physical time. Krylov exponentiation tolerance matched at 1e-10 on
both sides. Smoke gate: mid-chain Sz agrees across libraries to ~1e-13.

Suite 7: suite-1 workload at one (N, chi) point, one process per
(JULIA_NUM_THREADS x BLAS-threads) grid point, driven externally.

Both run.jl entry points gain --suite=5,7 and --blas-threads=n (default 1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…iagnostic

- benchmark/slurm/: one manually-submitted sbatch per (workload x library),
  single exclusive node, pinned-constraint placeholder, plus a README with
  the sanity-gate checklist.
- plot_results.jl: cross-library trajectory overlays, sanity-gate +
  time-to-accuracy tables (E_ref = best energy across both libraries),
  TDVP observable-gate/throughput panels, thread-scaling speedup plot.
- profile_sweep.jl: flat + tree profile of one DMRG2 sweep, with GC and
  allocation totals.
- ANALYSIS.md: question-driven skeleton, to be filled only from real
  cluster results.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…me limits)

The chi = 256, N = 100 pilot crossed the 1e-8 relative gate on sweep 1 and
moved < 5e-9 absolute over 11 further sweeps, so suite 1 drops to 6 sweeps
(suite 2 keeps 10: seed-growth needs ~6 sweeps to reach chi = 512 first).
chi = 1024 leaves the default schedule — extrapolated single-threaded cost
is days per point; extend once the chi <= 512 numbers justify it.
Suite 7 moves to chi = 256 x 3 sweeps so the 7-point thread grid fits a day.
sbatch limits sized accordingly (48 h DMRG, 24 h TDVP/threads); pilot also
logged an odd/even sweep-cost alternation, queued in ANALYSIS.md Q5.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the six per-workload sbatch scripts with one driver so every
compared number comes from the same machine by construction:
- phases 1-2 (DMRG, TDVP) run the two libraries concurrently, each
  single-threaded and taskset-pinned to one core on opposite sockets
  (2 of 96 cores active; no shared core, L3, or memory controller);
- phase 3 (thread grid) runs one point at a time on the idle node;
- inside every process, pin_cores() uses ThreadPinning.jl to pin Julia
  threads 1:1 within the affinity mask (pinthreads(:affinitymask)) and
  gives OpenBLAS workers disjoint mask cores when the pool is real.

SMOKE=1 bash benchmark/slurm/run_all.sbatch rehearses the whole driver
locally as plain bash; rehearsal passed on a 2-socket workstation with
no pinning warnings, and disjoint 1:1 placement was asserted directly.

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

codecov Bot commented Jul 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
see 80 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Jutho

Jutho commented Jul 5, 2026

Copy link
Copy Markdown
Member

This looks interesting. You had a discussion about this today? With Claude then? What is Rusty? Where can I see the results 😄 ?

@lkdvos

lkdvos commented Jul 5, 2026

Copy link
Copy Markdown
Member Author

Accidentally opened a PR for this, still trying to run the benchmark on our cluster (rusty)

lkdvos and others added 3 commits July 6, 2026 10:20
Two failures from the first Rusty submissions:
- job 6579930: batch shells have no interactive PATH, so 'taskset ...
  julia' died with 'No such file or directory';
- job 6579944: 'module load julia' is 1.11.2, which silently ignores the
  repo's Pkg [workspace] section, so --project=benchmark cannot see the
  root manifest and errors with a misleading 'Package ThreadPinning ...
  does not seem to be installed'.

The script now resolves julia itself: $JULIA override, else the newest
1.12+ binary in the shared-home juliaup depot (~/.julia/juliaup, GPFS,
identical on all nodes and the same install that resolved the
manifests), else PATH — then prints the choice and fail-fasts on
julia < 1.12. Also adds an ERR trap pointing at the per-process logs.
Verified by rehearsing the full driver under env -i with no julia on
PATH (the cluster batch-shell condition): all phases pass.

Keeps the user's partition=gen edit; drops the module load.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'suite1_..._N100_<ts>.json' sorts lexically before 'suite1_..._N20_<ts>.json',
so the first cluster analysis silently compared stale N=20 smoke files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All sanity gates pass (energies <= 4.6e-9 relative at matched chi; TDVP
Sz trajectories agree to ~1.5e-8). Headline findings:
- TDVP (matched inner Krylov tol): MPSKit 2-4x faster at all chi.
- Two-site DMRG: ITensor 1.5-1.6x faster dense, ~12x faster per U(1)
  sweep at chi=512 — attributed (REVIEW-flagged) to inner-eigensolver
  policy: ITensor caps local solves at krylovdim=3/maxiter=1 while
  MPSKit's dynamic tolerances drive krylovdim-30 solves toward 1e-14
  under the run-past-convergence protocol; per-sweep costs and
  allocation volumes match that story.
- U(1) grow-from-seed reaches the 1e-8 gate in ~2 s at any chi cap
  (workflow win, labelled as such).
- Thread grid: MPSKit scales to 4.8x at (8,8) vs ITensor's 2.2x, from
  an 8x slower single-core baseline on this workload.

pin_cores fix: read the affinity mask BEFORE pinthreads narrows the
calling thread (JULIA_NUM_THREADS=1 previously saw a 1-core mask and
bailed out of BLAS pinning — grid points (1,4)/(1,8) ran with floating
BLAS workers), and re-pin Julia threads after openblas_pinthreads,
whose pool includes the calling thread at nthreads=1. Verified locally
for both regimes.

Co-Authored-By: Claude Fable 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.

2 participants