Profile and optimise the decoding hot paths - #543
Conversation
Four deterministic workloads covering the hot paths: code-capacity surface decode, small-code depolarising decode, classical LDPC constraints + DMRG readout, and plain DMRG. Baseline on M-series (loaded machine): surface 52.6s, ldpc 15.0s, shor 2.4s, dmrg 0.9s. cProfile: 69% numpy SVD (22k calls), 18% importlib machinery triggered by a per-call 'import cupy' in _to_numpy. Profile artefacts stay untracked under benchmarks/results/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6mbxc9eJDQMVx7tjvYwud
1. Resolve the cupy import in _to_numpy once at module load. The per-call 'import cupy' inside the function re-ran a full failing module search on every conversion: 66k importlib invocations and 18% of a surface decode. 2. Reduce strongly rectangular SVDs (aspect >= 2) by QR/LQ before gesdd and SVD the small square factor: measured 1.23x on the (chi*d, d*chi*w) zip-up matrices that dominate decoding, agreement with direct SVD ~1e-14. Benchmark deltas (same machine, fingerprints unchanged to 1e-14): surface 52.6->38.5s, ldpc 15.0->8.5s, shor 2.4->0.40s, dmrg 0.89->0.26s. A QR fast path for pure orthogonality-centre moves was tried and reverted: QR is not rank-revealing, so the exact zero Schmidt directions that the SVD moves prune survive and inflate downstream bonds (ldpc regressed 8.6->13.6s). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6mbxc9eJDQMVx7tjvYwud
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #543 +/- ##
==========================================
- Coverage 97.29% 96.94% -0.36%
==========================================
Files 26 28 +2
Lines 5259 5537 +278
==========================================
+ Hits 5117 5368 +251
- Misses 142 169 +27 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…tion test
np.linalg.qr returns garbage on non-finite matrices instead of raising the
way svd does, so the QR/LQ reduction now checks finiteness first and lets the
direct call raise into the existing fallback chain.
The negative-amplitude test asserted the artefact at one exact chi_max, which
is numerical noise: it has migrated twice (4 -> 2 -> 3) under
behaviour-preserving SVD changes, and CI caught the latest move. The test now
asserts the phenomenon across chi_max in {2, 3, 4} and still requires a
converged run to stay clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S6mbxc9eJDQMVx7tjvYwud
CI on Linux proved that whether a seeded decode produces a negative logical
amplitude at a given chi_max is BLAS-dependent noise: the artefact appeared
at chi in {3,4} under Accelerate and at none of {2,3,4} under OpenBLAS. The
dense-readout scoring block moves out of decode_custom into
_score_dense_posterior, byte-identical in behaviour, and the test now feeds
that helper a vector that provably has (and provably lacks) a negative
amplitude, keeping only the converged-run assertion on real decodes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S6mbxc9eJDQMVx7tjvYwud
There was a problem hiding this comment.
🟡 Changes recommended
The DMRG fingerprint cannot detect incorrect states, and the new SVD branches lack deterministic coverage.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Optimizes decoding and MPS hot paths while adding profiling benchmarks.
Changes:
- Caches CuPy detection and accelerates rectangular SVDs using QR/LQ reduction.
- Extracts dense-posterior scoring for deterministic diagnostics testing.
- Adds four profiling workloads with fingerprints.
File summaries
| File | Description |
|---|---|
mdopt/utils/utils.py |
Optimizes conversion and SVD paths. |
mdopt/examples/decoding/decoding.py |
Extracts posterior scoring logic. |
tests/decoding/test_decoders.py |
Makes warning tests deterministic. |
benchmarks/bench_suite.py |
Adds profiling workloads. |
benchmarks/.gitignore |
Ignores benchmark results. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 2
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…rage The DMRG workload fingerprinted mps.norm(), which renormalised bond updates hold at ~1.0 for any state, correct or not; it now fingerprints the energy <psi|H|psi> (-30.1997 for the 24-site critical TFIM, i.e. -4/pi per site as it should be), which moves if the optimised state does. The QR/LQ-reduced SVD branches get deterministic tests: fixed real and complex matrices pinning wide-reduced, tall-reduced, both boundary-direct orientations and square-direct, comparing singular values, reconstruction, orthogonality, and chi_max truncation against direct numpy SVD; plus a non-finite input test asserting the whole fallback chain raises. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6mbxc9eJDQMVx7tjvYwud
The cupy fast path in _to_numpy can only execute on a machine with CuPy installed, which no CI runner has; codecov flagged it as the one missing patch line. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6mbxc9eJDQMVx7tjvYwud
There was a problem hiding this comment.
🟡 Changes recommended
The finiteness check disables GPU SVD for CuPy arrays, and several correctness checks are too weak to enforce the stated tolerance.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
tests/utils/test_utils.py:673
- The truncated-spectrum checks also inherit NumPy's default
rtol=1e-5, allowing materially larger drift than the stated tolerances. Setrtolexplicitly so this branch actually verifies the optimization's numerical contract.
assert np.allclose(s_t, s_ref[:chi], atol=1e-12), label
assert np.isclose(
err, float(np.linalg.norm(s_ref[chi:]) ** 2), atol=1e-12
), label
benchmarks/bench_suite.py:76
- As in the surface workload, recording only the 0/1 decoding verdict cannot detect numerical drift in the dense posterior unless it crosses a decision boundary. Include the posterior itself so this workload can enforce the PR's claimed 1e-10 behavior-preservation contract.
outputs.append(float(success))
- Files reviewed: 6/6 changed files
- Comments generated: 3
- Review effort level: Balanced
… rtol - The surface and Shor workloads now fingerprint the full returned posterior alongside the verdict, so a distorted posterior with an unmoved argmax still moves the fingerprint. - The finiteness pre-check in svd goes through the backend (xp.isfinite): np.isfinite rejects CuPy arrays, which would have pushed every GPU SVD onto the SciPy host fallback. - The SVD-agreement assertions pin rtol=0.0; the numpy default rtol=1e-5 would have masked order-1e-5 errors behind the documented 1e-11 bounds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6mbxc9eJDQMVx7tjvYwud
There was a problem hiding this comment.
🟡 Changes recommended
Benchmark timing is order-dependent, direct SVDs incur unnecessary scans, and one numerical assertion remains too permissive.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
tests/utils/test_utils.py:676
- This truncation-error check still inherits
np.isclose's defaultrtol=1e-5. Because the discarded-spectrum norm can be much larger than one, the assertion can accept errors orders of magnitude above the stated1e-12tolerance; setrtol=0.0here as in the other new equivalence checks.
- Files reviewed: 6/6 changed files
- Comments generated: 2
- Review effort level: Balanced
…check Workload imports hoisted to module level so wall times and profiles no longer depend on invocation order; the finiteness scan in svd now runs only when a QR/LQ reduction would actually be taken, sparing direct-path calls the O(rows*cols) pass and CuPy calls an unconditional device sync. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6mbxc9eJDQMVx7tjvYwud
There was a problem hiding this comment.
🔵 Needs a closer look
The truncation-error test retains a loose default relative tolerance that weakens the numerical correctness contract.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
tests/utils/test_utils.py:676
- This truncation-error comparison still uses NumPy's default
rtol=1e-5. Since the expected squared residual can be order 100, the test can accept errors around1e-3, so it does not enforce the stated numerical-equivalence contract. Setrtol=0.0as in the other new SVD comparisons.
- Files reviewed: 6/6 changed files
- Comments generated: 0 new
- Review effort level: Balanced
There was a problem hiding this comment.
🟡 Changes recommended
The truncation-error test retains a loose default relative tolerance and does not enforce the stated numerical bound.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 1
- Review effort level: Balanced
The discarded spectrum's norm-square is O(100) for these fixtures, so the default relative tolerance allowed ~1e-3 slack behind a nominal 1e-12 bound; rtol=0 with atol=1e-9 makes the bound genuinely absolute (3e-12 relative at this scale). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6mbxc9eJDQMVx7tjvYwud
The sweep in mps_mpo_contract evaluates the same two einsums thousands of times per decode, and contract() re-parses subscripts and rebuilds path metadata on every call even with an explicit optimize path (~5-7% of a decoding run). All eight contractor einsums now route through an lru_cache'd contract_expression keyed on (subscripts, path, shapes); bond dimensions cycle through a small set, so the cache stays tiny. Same machine, same conditions: surface 21.8 -> 20.3s; fingerprints unchanged; 105 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6mbxc9eJDQMVx7tjvYwud
A repositioning that requests no singular values and no renormalisation now factors each site with a single-site column-pivoted QR (scipy dgeqp3) instead of the two-site SVD. The factorisation is ~d times smaller and QR beats gesdd, but unlike the plain QR tried earlier the pivoted form reveals rank, so the numerically-dead Schmidt directions are pruned exactly as the SVD moves prune them -- the product-state round trip stays at bond 1 and no bond inflates (that inflation was the 58% ldpc regression the naive attempt caused). Any bond whose revealed rank somehow exceeds chi_max falls back to the SVD branch. Validated: dense() invariant to ~1e-16 across end-to-end centre moves, bonds provably non-growing, and the full 166-test suite passes -- including the exact-enumeration decoder references (ground truth, not just old-code agreement). Same idle machine, cumulative with the einsum cache: surface 20.3 -> 16.4s, ldpc 3.15 -> 2.66s; fingerprints unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6mbxc9eJDQMVx7tjvYwud
There was a problem hiding this comment.
🟡 Changes recommended
The classical LDPC fingerprint does not validate the constrained MPS, allowing contraction regressions that preserve the decoded codeword to pass.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 16/16 changed files
- Comments generated: 1
- Review effort level: Balanced
…d of aborting decode_dem raises ArithmeticError on a collapsed or negative class-mass vector; the harness let that abort the cell and every cell after it, and the deterministic resume replayed the same shot. The shot is now scored as a failure with an artefact field, as dem_rerun.py already does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6mbxc9eJDQMVx7tjvYwud
There was a problem hiding this comment.
🟡 Changes recommended
The bond-1 shortcut can assign an invalid orthogonality centre to partially biased product states.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
mdopt/mps/canonical.py:156
- The existing
test_canonical_reversealways uses a centre atnum_sites - 2, and the newly added move tests only reverse centres greater than zero, so the corrected site-0 branch is still untested. Add a case that reverses an MPS withorth_centre == 0and asserts the result's centre isnum_sites - 1.
- Files reviewed: 17/17 changed files
- Comments generated: 1
- Review effort level: Balanced
|
@Copilot investigate how to use QRs so that they do not break the decoding of BB codes |
NumPy's linalg.qr on the Accelerate framework (the macOS arm64 wheels, numpy 2.2.6) is not memory-safe on some of the decoders' matrices: on a 636x304 centre tensor taken from a [[72,12,6]] bivariate-bicycle decode at chi_max=400 it returns a factorisation whose product is not the input in a state-dependent fraction of calls (whole columns off by order one, 9 of 30 in one process) and a loop of 200 calls interleaved with allocations dies with SIGBUS every time; the same loop through scipy.linalg.qr, and every SVD driver, is exact and clean. With the pre-reduction on NumPy's qr this branch failed all 24 non-trivial chi_max=400 natural-order BB shots it was given, while main, the thesis-era code and this branch with the plain SVD decode them all. The pre-reduction now uses SciPy's qr on the NumPy backend (CuPy keeps its own); the speed-up of the reduction is kept. Two tests: a fast one that pushes graded rank-deficient matrices of the offending shape through svd with allocator churn (a guard, not a certain detector -- the fault is heap-state dependent), and a slow chi-convergence decode of the offending instance (MDOPT_RUN_SLOW=1) that fails deterministically on the previous code. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6mbxc9eJDQMVx7tjvYwud
With the pre-reduction on, the [[72,12,6]] bivariate-bicycle decode of a single-Z error at chi_max=400 in the natural order returned a flat or wrongly peaked posterior on every one of 24 shots (main, the thesis-era code and this branch with the plain SVD decode them all), while every unit test and benchmark fingerprint passed. Replaying one checkpointed contraction shows why nothing caught it: each SVD call agrees with a reference to 1e-15, yet the contraction's result depends on the heap layout of the process (three harness scripts, three different wrong norms; a fourth gets the right one), whichever LAPACK or BLAS performs the QR, the small SVD or the back-multiplication. NumPy's own linalg.qr on the Accelerate framework (macOS arm64 wheels, numpy 2.2.6) additionally dies with SIGBUS on a 636x304 centre tensor from that decode when called in a loop with allocations in between. The plain decomposition is deterministic and exact on the same inputs in every harness. The reduction now sits behind SVD_QR_PREREDUCTION (default False) with backend-aware helpers for the QR, the small SVD and the back-multiplication (SciPy's LAPACK/BLAS on the NumPy backend, the device's own on CuPy), to be enabled only where tests/decoding/test_convergence.py passes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6mbxc9eJDQMVx7tjvYwud
Correctness finding: the QR pre-reduction of
|
Forty depolarising errors from a fixed seed on the L=5 hypergraph-product surface code, decoded in the natural order at chi_max=16: at p=0.05 none may fail, at p=0.08 the failure count may not exceed the pinned four and the verdict pattern is pinned as well, since at this bond dimension the verdicts depend on how the truncation is carried out. main and this branch produce identical verdicts. About 35 s. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6mbxc9eJDQMVx7tjvYwud
…re from site norms Two review points. The classical_ldpc workload's fingerprint was the Dephasing DMRG verdict alone, which stays 1.0 whenever the MAP codeword is unchanged; it now leads with observables of the constrained state itself (overlaps with the transmitted codeword and the received message, the middle-bond Schmidt spectrum), with the baseline rewritten from main. The product-state shortcut in apply_constraints labelled site 0 as the centre for every bond-1 chain; the isometry scan reports the first non-isometric site instead, which differs when the first sites are normalised and a later one is biased. A (1, d, 1) tensor is an isometry exactly when its vector has unit norm, so the scan's answer now comes from the site norms (same tolerance), checked against the scan on biased and unbiased chains. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6mbxc9eJDQMVx7tjvYwud
There was a problem hiding this comment.
🟡 Changes recommended
The advertised rectangular-SVD optimization is disabled, and its reduced branches are not exercised by the new tests.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (2)
tests/utils/test_utils.py:688
- The global pre-reduction flag is
False, so this test currently feeds NaNs directly to the backend SVD and never verifies the documented reduced-QR failure/fallback chain. Enable the flag locally before callingsvd.
def test_svd_nonfinite_input_takes_the_fallback_chain():
"""A non-finite input must make the whole call raise.
There is no finiteness pre-scan: the reduced path's QR of a NaN matrix
yields a NaN factor whose SVD raises LinAlgError (LAPACK gesdd on NaN
input), which sends the call through the fallback chain, and the
jitter attempt cannot rescue a NaN either."""
tests/mps/test_canonical.py:960
- This test name still calls the optimized branch a QR path, but
move_orth_centrenow performs a direct one-site SVD and contains no QR factorization. Rename it to describe the one-site path so failures identify the implementation actually under test.
def test_move_orth_centre_qr_path_matches_svd_path_on_full_rank_states():
- Files reviewed: 19/19 changed files
- Comments generated: 3
- Review effort level: Balanced
…tually taken SVD_QR_PREREDUCTION is now set from MDOPT_SVD_QR_PREREDUCTION=1 and the PR text says the speed-ups are measured without it. The reduced-branch test switches the flag on for its small matrices, so those branches stay covered; the allocator-churn test runs the default path always and the pre-reduced path only when the environment opts in, since on NumPy/Accelerate builds that case does not fail but kills the interpreter (SIGSEGV, reproduced here). The slow decoding test compares the opt-in variable with "1" rather than testing truthiness. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6mbxc9eJDQMVx7tjvYwud
There was a problem hiding this comment.
🟡 Changes recommended
The reduced-SVD non-finite fallback is not exercised under the default test configuration.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (1)
tests/mps/test_canonical.py:960
- The optimized branch now performs a one-site SVD, not QR, so this test name is stale and obscures which path it covers. Rename it to identify the one-site path versus the two-site reference.
def test_move_orth_centre_qr_path_matches_svd_path_on_full_rank_states():
- Files reviewed: 19/19 changed files
- Comments generated: 1
- Review effort level: Balanced
The test's docstring described the reduced path, but the flag was off, so only the direct path's fallback chain ran; it is now parametrised over the flag (the 8x32 NaN matrix triggers the reduction when it is on). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6mbxc9eJDQMVx7tjvYwud
There was a problem hiding this comment.
🔵 Needs a closer look
The broad numerical and backend-sensitive optimizations warrant final human validation despite extensive regression coverage.
Review details
Suppressed comments (1)
tests/mps/test_canonical.py:960
- This test name is stale: the optimized branch now factors the centre with
svd, not QR. Rename it to identify the one-site path so future failures are attributed to the implementation actually under test.
def test_move_orth_centre_qr_path_matches_svd_path_on_full_rank_states():
- Files reviewed: 19/19 changed files
- Comments generated: 0 new
- Review effort level: Balanced
What this is
A profiling-driven optimisation pass over the MPS/decoding hot paths, with a
benchmark suite (
benchmarks/bench_suite.py) whose deterministic workloadsdouble as a correctness contract: exact fingerprints (energies, verdicts,
constrained-state overlaps and spectra) that must not move, and
chi-truncated posteriors that may move within truncation noise only for a
change that alters the gauge trajectory. Baselines are written from an
unoptimised
maincheckout; every change below matches them.Changes kept (in measured-impact order)
_to_numpyno longer imports cupy per call (the failing module searchran 66k times per decode, ~18% of runtime).
remaining opt_einsum expressions are cached per (subscripts, path).
neighbour is an isometry (checked per site) and nothing would be
truncated: the centre's spectrum is the two-site tensor's, so the cut and
the chi_max count are identical. Covers plain and renormalised moves and
returned spectra; a non-isometric neighbour falls back to the two-site
SVD.
inplacemovesand zip-ups), the cheaper isometry gate, the
reverse()centre-at-0 fix,the product-state centre from site norms.
classical LDPC, DMRG, circuit-level DEM d=3 and d=5) and
--qubit_order_strategyfor the four CSS campaign CLIs.Not delivered: the QR/LQ pre-reduction of strongly rectangular SVDs
It is in the code behind
SVD_QR_PREREDUCTION(opt-in throughMDOPT_SVD_QR_PREREDUCTION=1, backend-aware helpers for the QR, the smallSVD and the back-multiplication) but off by default, so the numbers
below do not include it. With it, the [[72,12,6]] bivariate-bicycle decode
of a single-Z error at chi_max=400 in the natural order failed all 24 shots
it was given (flat or wrongly peaked posterior) while
main, thethesis-era code and this branch without it decode every one of them, and
every unit test and benchmark fingerprint passed throughout. The cause is
on the NumPy/Accelerate side (macOS arm64 wheels, numpy 2.2.6): NumPy's
linalg.qrdies with SIGBUS on a 636x304 centre tensor from that decodewhen called in a loop with allocations in between, and a 12-second replay
of one contraction gives a heap-layout-dependent result with any variant of
the pre-reduction (NumPy or SciPy QR, SVD and matmul) while the plain
decomposition is deterministic and exact. Details in
this comment.
Enable the flag only where
tests/decoding/test_convergence.pypasses onthe target build.
Measured (idle Apple M5, single run of the suite, this branch vs
main)The earlier campaign reports (1,
2) quote
numbers measured with the pre-reduction on (dem_d5 17.8 s, surface 16.4 s);
those are superseded by the table above.
Tests
tests/decoding/test_convergence.py(opt-in,MDOPT_RUN_SLOW=1, 30-60min): the offending BB instance at chi_max 128 and 400 must decode to a
delta posterior on the identity; fails deterministically with the
pre-reduction on this machine.
tests/decoding/test_surface_ler_regression.py(35 s, in CI): fortyfixed-seed shots on the L=5 surface code at chi_max=16; no failure at
p=0.05, at most the pinned four at p=0.08, verdict pattern pinned.
tests/utils/test_utils.py:svdunder allocator churn on gradedrank-deficient matrices, for both values of the flag (a guard, not a
certain detector).
and the full suite (243 tests) pass.
Findings that matter beyond the code
qubit_order_strategy="Optimised"(reverse Cuthill-McKee) is 5x faster thanthe natural order on the 5x5 surface code at chi=64 and changes the
convergence of the BB codes; the BB campaign scripts still run the natural
order at chi=400.
(the BB ordering and DEM mass-loss experiments) is being regenerated.
🤖 Generated with Claude Code
https://claude.ai/code/session_01S6mbxc9eJDQMVx7tjvYwud