Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
ea1f389
Add shared benchmark infra and TeNPy cross-library benchmarks
IvanaGyro Jun 24, 2026
65d1bf0
Add quimb cross-library benchmarks with JAX and PyTorch AD backends
IvanaGyro Jun 24, 2026
6d30d81
Add Cytnx cross-library benchmarks with hand-rolled TEBD and manual g…
IvanaGyro Jun 24, 2026
78e9083
Add run_all.py orchestration script, plot_results.py, and README
IvanaGyro Jun 24, 2026
e6955a9
Fix ru_maxrss unit conversion for macOS in cpu_timed_block
IvanaGyro Jun 24, 2026
2c756a5
Fix cytnx_gpu_timed_block reporting free GPU memory instead of peak u…
IvanaGyro Jun 24, 2026
a0817d0
Fix Lanczos CvgCrit preventing the requested iteration count from run…
IvanaGyro Jun 24, 2026
8266571
Skip benchmark points that exceed a 120s wall-clock budget
IvanaGyro Jun 24, 2026
b93565f
Add converged answer field to benchmark CSV output
IvanaGyro Jun 24, 2026
010f77f
Add per-(chi,L) resume support to cross-library benchmark scripts
IvanaGyro Jun 24, 2026
1842234
Add cross-library + exact-diagonalization correctness validation script
IvanaGyro Jun 24, 2026
693ff87
Fix NaN energies in quimb variational_ad.py from per-tensor MPS renor…
IvanaGyro Jun 24, 2026
358ca03
Fix near-zero energies in cytnx variational_manual_grad.py from broke…
IvanaGyro Jun 24, 2026
b5cc411
Fix Lanczos LinOp dimension for block-sparse psi in dmrg_symmetric.py
IvanaGyro Jun 24, 2026
b43f08b
Fix gradient-descent normalization bug in variational manual-grad/AD …
IvanaGyro Jun 24, 2026
62ea490
Fix benchmark memory-measurement window to cover full run_one body
IvanaGyro Jun 24, 2026
7e86c98
Add pytest-benchmark/pytest-memray tests for cross_library benchmarks
IvanaGyro Jun 25, 2026
0deec9a
Drop unused run_one timing/memory fields from cross_library test unpa…
IvanaGyro Jun 25, 2026
05f650b
Parametrize cross_library pytest-benchmark/memray tests on (chi, length)
IvanaGyro Jun 25, 2026
65efed5
Fix wrong reference energy in cytnx_bench/test_tebd.py
IvanaGyro Jun 25, 2026
c20778c
Strip CSV/main()/timed-block scaffolding from cross_library algorithm…
IvanaGyro Jun 25, 2026
db0288b
Restructure cross_library test files into pytest-discoverable modules
IvanaGyro Jun 25, 2026
37755ba
Add full-grid sweep tests and pytest-timeout to cross_library benchmarks
IvanaGyro Jun 25, 2026
328cfff
Retire run_all.py's CSV/timeout harness and rewrite the cross_library…
IvanaGyro Jun 25, 2026
cc81430
Shrink cross_library benchmark grid to keep full-grid runs tractable
IvanaGyro Jun 25, 2026
c988cdc
cytnx_bench: merge algorithm scripts into their test files
IvanaGyro Jun 25, 2026
2ac935e
quimb_bench: merge algorithm scripts into their test files
IvanaGyro Jun 25, 2026
eb68acb
tenpy_bench: merge algorithm scripts into their test files
IvanaGyro Jun 25, 2026
8c74341
Remove unused param_grid() helper and refresh cross_library README
IvanaGyro Jun 25, 2026
4df2106
tenpy_bench: rebuild manual-gradient variational benchmark on raw Arrays
IvanaGyro Jun 25, 2026
d7e3799
cross_library: align TEBD/TDVP Hamiltonian conventions across quimb a…
IvanaGyro Jun 25, 2026
a53539d
quimb_bench: replace symmetric DMRG placeholder with annealed ITE sweep
IvanaGyro Jun 25, 2026
8dba507
quimb_bench: tune AD variational gradient descent to reach the shared…
IvanaGyro Jun 25, 2026
1fd966a
quimb_bench: rebuild variational_ad on the same one-site sweep as ten…
IvanaGyro Jun 26, 2026
1a04f66
Revert "quimb_bench: rebuild variational_ad on the same one-site swee…
IvanaGyro Jun 26, 2026
2fed00d
cross_library: port whole-network variational gradient descent to ten…
IvanaGyro Jun 26, 2026
4d6258f
cytnx_bench: parametrize dmrg_dense memory test over the full (chi, L…
IvanaGyro Jun 26, 2026
ba82f58
cross_library: filter --memray runs with an explicit pytest marker
IvanaGyro Jun 26, 2026
d7874a8
benchmarks/cross_library: drop conftest auto-filter for --memray
IvanaGyro Jun 26, 2026
7e5db94
benchmarks/cross_library: rename grid params to bond_dim/num_sites
IvanaGyro Jun 26, 2026
61c95ae
cytnx_bench/test_dmrg_dense: tighten energy tolerance to rel=1e-6
IvanaGyro Jun 26, 2026
0a78a30
Pass L/M1/M2/R as explicit parameters to _optimize_psi
IvanaGyro Jun 26, 2026
906b450
Construct Cytnx benchmark tensors directly on the target device
IvanaGyro Jun 26, 2026
8ad9b18
Rename qcntr/cq to running_charge/charge_step in test_dmrg_symmetric.py
IvanaGyro Jun 26, 2026
274cee5
Move test_variational_manual_grad.py's Network helpers into _Networks
IvanaGyro Jun 26, 2026
52e2016
Rename tenpy_bench/test_tdvp.py to test_tebd.py
IvanaGyro Jun 27, 2026
29c7263
Extend memray memory tests to the full benchmark grid
IvanaGyro Jun 27, 2026
9f3fd6d
Fix quimb symmetric DMRG backend conversion and confirm numpy default
IvanaGyro Jun 27, 2026
4e29cc0
Add a TeNPy-backed reference-energy generator to validate_correctness.py
IvanaGyro Jun 27, 2026
022dd9e
Cache Network templates in cytnx_bench/test_dmrg_dense.py
IvanaGyro Jun 28, 2026
494c89c
Cache Network templates in cytnx_bench/test_dmrg_symmetric.py
IvanaGyro Jun 28, 2026
13ea142
Avoid redundant gate construction/relabeling in cytnx_bench/test_tebd.py
IvanaGyro Jun 28, 2026
fa65872
Jit norm_sq in quimb_bench/test_variational_ad.py's JAX path
IvanaGyro Jun 28, 2026
be141f8
Fix LANCZOS_MAXITER doc comment in common/model.py
IvanaGyro Jun 28, 2026
4c0b832
Add magnitude-cutoff truncation to Cytnx Svd_truncate calls
IvanaGyro Jun 28, 2026
572c9fd
Upgrade Cytnx TEBD to a second-order Trotter step
IvanaGyro Jun 28, 2026
5c1f125
Unify TEBD initial product state across all three libraries
IvanaGyro Jun 28, 2026
928d215
Replace DMRG REFERENCE_ENERGIES with TeNPy ground-truth values
IvanaGyro Jun 28, 2026
3ed0525
Extend reference-energy generator to TEBD and variational benchmarks
IvanaGyro Jun 28, 2026
57dada2
Rename chain-length/bond-dimension params from L/chi to num_sites/bon…
IvanaGyro Jun 28, 2026
f527d5e
Rewrite Cytnx TEBD to even/odd Suzuki-Trotter checkerboard splitting
IvanaGyro Jun 28, 2026
fd34bf1
Unify variational benchmark self-consistency tolerance to rel=1e-6
IvanaGyro Jun 30, 2026
4ba235c
Fix stale quimb-jax (16,50) reference energy
IvanaGyro Jun 30, 2026
9f30073
Remove dead TFIM_HX_INITIAL from cross_library benchmark model
IvanaGyro Jul 1, 2026
8703f91
Equalize DMRG dense/symmetric fairness and tighten to rel=1e-6
IvanaGyro Jul 1, 2026
0fd23cd
Force quimb DMRG2 to run the full N_SWEEPS budget
IvanaGyro Jul 1, 2026
1a944a6
Fix stale cytnx TEBD (16,30) reference energy
IvanaGyro Jul 1, 2026
a03c7ae
Cache A_conj across variational gradient steps to avoid redundant Dag…
IvanaGyro Jul 1, 2026
98dd890
Use single-pass compute_local_expectation in quimb symmetric DMRG
IvanaGyro Jul 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions benchmarks/cross_library/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# Cross-library tensor-network benchmark suite

Compares **TeNPy**, **quimb**, and **Cytnx** on the same four classes of
tensor-network algorithm, using the same physical models and the same
`(bond_dim, num_sites)` parameter grid for every library, so that speed and
peak-memory differences reflect implementation/library choices rather than
differences in workload.

## Algorithm classes

| # | Class | Model | Library implementations |
|---|-------|-------|--------------------------|
| 1 | Finite two-site DMRG, dense | 1D spin-1/2 Heisenberg chain, no symmetry | `tenpy_bench/test_dmrg_dense.py`, `quimb_bench/test_dmrg.py`, `cytnx_bench/test_dmrg_dense.py` |
| 1' | Finite two-site DMRG, block-sparse | Same chain, U(1) total-Sz conserved | `tenpy_bench/test_dmrg_symmetric.py`, `quimb_bench/test_dmrg.py`, `cytnx_bench/test_dmrg_symmetric.py` |
| 2 | Real-time evolution after a field quench (TEBD) | 1D transverse-field Ising chain | `tenpy_bench/test_tebd.py`, `quimb_bench/test_tebd.py`, `cytnx_bench/test_tebd.py` |
| 3 | Variational MPS ground-state search by gradient descent | Same Heisenberg chain as class 1 | `tenpy_bench/test_variational_manual_grad.py`, `quimb_bench/test_variational_ad.py`, `cytnx_bench/test_variational_manual_grad.py` |

Each script's `run_one(chi, L)` lives directly in its `test_*.py` file
(there are no separate, non-test algorithm modules) so that every test
file is self-contained and importable as ordinary pytest-discovered code,
with no `sys.path` manipulation. `quimb_bench/test_dmrg.py` covers both
class 1 and class 1' in one file, since quimb's block-sparse variant
(`run_one_symmetric`) is a self-consistency check (imaginary-time
evolution from a random seeded state), not a ground-state energy
comparable across libraries — see the docstring in that file.

All four classes share the model/parameter definitions in `common/model.py`
(`HEISENBERG_J`, `TFIM_J`/`TFIM_HX_FINAL`/`TFIM_DT`, the
`(BOND_DIM_VALUES, NUM_SITES_VALUES)` grid).

### Gradient computation in class 3

All three libraries run the same optimization algorithm: one gradient step
of the Rayleigh quotient `E(psi) = <psi|H|psi>/<psi|psi>` is taken on every
MPS tensor simultaneously (no orthogonality center, no per-step
canonicalization), followed by a single global rescale derived from the
new `<psi|psi>` and distributed evenly across all `L` tensors. Only the
gradient-computation method differs.

quimb has a native autodiff path (JAX or PyTorch arrays under the hood), so
`quimb_bench/test_variational_ad.py` differentiates `<psi|H|psi>/<psi|psi>`
directly through the backend's `grad`/`backward`.

TeNPy and Cytnx have no autodiff backend. Each gets its own hand-derived
analytic gradient of the same Rayleigh quotient with respect to every MPS
tensor `A_i` simultaneously:

```
dE/dA_i* = (2 / den) * (H_eff,i(A_i) - E * N_eff,i(A_i))
```

where `den = <psi|psi>`, `H_eff,i` is the effective one-site Hamiltonian
built from the L/R H-boundary environments around site `i`, and `N_eff,i`
is the analogous contraction with trivial (no-MPO) norm-boundary
environments. The norm-environment term is necessary because, with every
tensor updated at once and no canonicalization step, the tensors away from
site `i` are not isometric, so `N_eff,i` does not collapse to `A_i` as it
would in a one-site sweep. All four environment sets (H-left, H-right,
norm-left, norm-right) are rebuilt from scratch every gradient step, since
every tensor changes simultaneously. The TeNPy and Cytnx implementations of
this formula are written independently (`np_conserved` contractions vs.
Cytnx `UniTensor`/`Network`/`Contract`), not via a shared abstraction, since
the point of the benchmark is to compare each library's own primitives.

This whole-network update is a weaker optimizer than a one-site sweep, so
its converged energy is sensitive to each library's own initial-state
construction and RNG; correctness is validated by checking that each
library's own implementation lands close to its own reference energy
(tight per-library tolerance), not by comparing energies across libraries
or by exact-diagonalization matching (unlike classes 1 and 2, which are
ED-validated — see the docstrings in `test_dmrg_dense.py`/`test_tebd.py`
for details).

## Parameter grid

`common/model.py` defines the `(bond_dim, num_sites)` grid shared by every
script:

```
BOND_DIM_VALUES = [16, 32, 64]
NUM_SITES_VALUES = [20, 30, 50]
```

Each `test_<name>.py` parametrizes its benchmark test over the full
Cartesian product of `BOND_DIM_VALUES` and `NUM_SITES_VALUES` (9 points), via
two stacked `@pytest.mark.parametrize` decorators — one over `bond_dim`, one
over `num_sites`. Every point is bounded by the per-point wall-clock budget
`GRID_POINT_TIMEOUT_SEC` (120s by default, enforced via `pytest-timeout`), so
a single slow large-bond_dim/large-num_sites point fails on its own rather
than hanging the rest of the run — see "pytest-benchmark / pytest-memray
regression tests" below for how to run an individual point.

## CPU vs. GPU

- **TeNPy**: CPU only (TeNPy itself has no GPU backend).
- **quimb**: CPU and GPU paths are both implemented for the AD benchmark
(`variational_ad.py`'s `run_one_jax`/`run_one_torch`); the DMRG/TEBD scripts
also carry a `DEVICE = "gpu"` switch.
- **Cytnx**: every script has a `DEVICE` module-level flag (`"cpu"` or
`"gpu"`); the GPU path moves every MPS/MPO `UniTensor` to
`cytnx.Device.cuda` before the sweep/step.

GPU code paths exist in every script that supports them but were written
and reviewed without a GPU available in the development environment, so
they are untested. Set `DEVICE = "gpu"` at the top of the relevant script(s)
to exercise them on a CUDA-capable machine.

## Running the suite

The whole suite is pytest-native: each script's `run_one(chi, L)` lives in
its `test_<name>.py` file, exercised across the full `(bond_dim, num_sites)`
grid. There is no separate orchestration script and no standalone algorithm
modules.

```sh
pip install tenpy quimb cytnx jax torch
pip install -e '.[benchmark]' # pytest-benchmark, pytest-memray, pytest-timeout

cd benchmarks/cross_library

# Full (bond_dim, num_sites) grid, timing only (skips the memory tests),
# with a pytest.approx assertion against a precomputed reference energy at
# every point:
python3 -m pytest --benchmark-only -q

# Memory only: --memray instruments every collected test, so select just
# the memory tests with the cytnx_memory marker (registered in
# pyproject.toml, applied to every *_memory test in this directory):
python3 -m pytest --memray -m cytnx_memory -q

# Run the whole grid for one script:
python3 -m pytest cytnx_bench/test_dmrg_dense.py --benchmark-only -q

# Or call pytest one point at a time, which doubles as a resume mechanism if
# a run gets interrupted partway through the grid:
python3 -m pytest "cytnx_bench/test_dmrg_dense.py::test_dmrg_dense_benchmark[16-20]" --benchmark-only -q
```

Run from `benchmarks/cross_library` (not the repo root) with `python3 -m
pytest` (not the bare `pytest` command) so that `cytnx`/`quimb`/`tenpy`
resolve to your installed packages rather than colliding with the repo's
source tree.

## pytest-benchmark / pytest-memray regression tests

Each of the 12 `test_<name>.py` files exercises its `run_one(chi, L)` across
the full `(bond_dim, num_sites)` grid through `pytest-benchmark`'s
`benchmark.pedantic`, asserting the returned energy against a
`REFERENCE_ENERGIES[(bond_dim, num_sites)]` dict via `pytest.approx`, so a
wrong physical answer fails the test rather than silently shipping a bad
timing number. The result is also recorded via `benchmark.extra_info["energy"]`;
pass `--benchmark-json=out.json` to capture it (and the timing statistics)
for every point in one file.

The same file's `test_<name>_memory` test takes no parameters — it calls
`run_one` directly (no `benchmark` fixture, so pytest-benchmark's overhead
doesn't contaminate memray's allocation trace) at the single canonical
`(chi=16, L=20)` point, under a `@pytest.mark.limit_memory(...)` decorator
tuned to that point's footprint.
Empty file.
Empty file.
65 changes: 65 additions & 0 deletions benchmarks/cross_library/common/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Shared physical model and parameter-grid definitions for the
TeNPy / quimb / Cytnx cross-library benchmark suite.

All four algorithm classes benchmark the *same* physical model and the
*same* (bond_dim, num_sites) parameter grid, so that the only thing that
varies between runs is the library/implementation:

- DMRG (dense) : 1D spin-1/2 Heisenberg chain, no symmetry
- DMRG (block-sparse) : same chain, U(1) total-Sz conserved
- TEBD/TDVP (dynamics) : 1D transverse-field Ising chain, field quench
- Variational AD/manual : same Heisenberg chain as the dense DMRG case
"""

# Heisenberg coupling J (isotropic, antiferromagnetic) for DMRG / variational.
HEISENBERG_J = 1.0

# Transverse-field Ising parameters for the TEBD/TDVP quench benchmark. All
# three implementations start from the all-down product state |0...0> and
# evolve it directly under this post-quench Hamiltonian
# H = -J * sum ZZ - hx_f * sum X (no separate initial-field ground state is
# prepared).
TFIM_J = 1.0
TFIM_HX_FINAL = 0.5
TFIM_DT = 0.05
TFIM_N_STEPS = 40

# 2D sweep grid shared by every algorithm/library: bond dimension vs.
# number of sites. This is the axis pair the user asked to scan in order to
# expose the memory (~O(num_sites * bond_dim^2) for an MPS) and speed
# (~O(num_sites * bond_dim^3) for a DMRG/TDVP local update) tradeoffs.
BOND_DIM_VALUES = [16, 32, 64]
NUM_SITES_VALUES = [20, 30, 50]

# Number of DMRG sweeps measured per (bond_dim, num_sites) point. Kept small
# because we only need a handful of sweeps to get a stable per-sweep timing
# and peak-memory estimate, not a converged ground state.
N_SWEEPS = 3

# Number of Lanczos iterations for Cytnx's local two-site eigensolver
# (cytnx_bench/test_dmrg_{dense,symmetric}.py). TeNPy's TwoSiteDMRGEngine
# is passed this same value via its `lanczos_params["N_max"]`
# (tenpy_bench/test_dmrg_{dense,symmetric}.py) so both libraries spend the
# same per-bond eigensolver budget converging to the same ground-state
# energy; quimb's DMRG2 has no equivalent maxiter knob in its public API, so
# its scipy-eigsh-backed solver is left at its native default convergence.
# 4 was not quite enough for cytnx_bench/test_dmrg_symmetric.py's hardest
# grid point (bond_dim=16, num_sites=50) to land within rel=1e-6 of the
# shared REFERENCE_ENERGIES value -- TeNPy's own Krylov-based solver already
# matches that value to ~1e-8 at this same N_max, so the gap is specific to
# Cytnx's own Lanczos implementation on that hardest local subspace, not a
# tolerance or parameter mismatch between libraries. Raising the shared
# budget to 6 (rather than loosening the tolerance or letting Cytnx diverge
# from TeNPy's setting) closes it to ~7e-7 across the full grid.
LANCZOS_MAXITER = 6

# SVD truncation magnitude cutoff, matching TeNPy's svd_min and quimb's
# cutoffs (both 1e-10 in this suite's dmrg/tebd scripts): singular values
# below this threshold are discarded even if the bond-dimension cap isn't
# reached. Passed to Cytnx's Svd_truncate via its `err` argument.
SVD_CUTOFF = 1e-10

# Per-(bond_dim, num_sites) wall-clock budget. Points that exceed this are
# skipped rather than measured, so a handful of slow large-bond_dim/
# large-num_sites points don't dominate the suite's total run time.
GRID_POINT_TIMEOUT_SEC = 120
Empty file.
Loading
Loading