perf(linalg): make batched einsum contraction allocation-free per slice - #489
Merged
Conversation
batched_contract ran a full contract_dense per batch slice, each paying two operand copies, two memory-order reorders, and a notation re-parse plus plan rebuild around a small GEMM. That fixed overhead dominates when the batch and per-slice dimensions are small, as in the randomized compression sweep's environment recursion. Rewrite the loop as one bare backend GEMM per slice into a preallocated output: permute and reorder both operands once so the batch axes are the slowest-varying in the backend's preferred order, making every per-slice (m, k) / (k, n) block a contiguous range fed straight to the kernel, with (m, n, k) and the execution policy computed once. Ranks and shared batch / contracted extents are validated up front, preserving the InvalidArgument the removed per-slice contract_dense returned. The GEMM stays in preferred_order (batch on the slowest-varying axis for that order), so the ComputeBackend contract -- compute kernels run in preferred_order, only transpose is layout-parametric -- is not widened. Output is unchanged up to floating-point reassociation; the existing batched einsum, SRC, and incremental-QR suites pass unchanged. mpo_mps_apply: adaptive/n8_chi8 -18%, adaptive/n12_chi16 -21%.
There was a problem hiding this comment.
Pull request overview
This PR optimizes the linalg einsum dispatcher’s batched 2-tensor contraction path by eliminating per-batch-slice operand copies/reorders and per-slice notation parsing, replacing it with a single pre-permute/reorder followed by one backend GEMM per slice into a preallocated output buffer. This targets small per-slice GEMMs common in SRC sweep environment recursion.
Changes:
- Reworked
batched_contractto precompute permutations/layout once, then runComputeBackend::gemmper batch slice into a preallocated buffer (plus upfront validation and degenerate short-circuits). - Added an in-crate unit test to exercise the row-major-preferred backend branch and compare against the native column-major backend.
- Added integration tests covering multi-batch asymmetric batched contractions and additional batched error/degenerate cases.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| crates/ariadnetor-linalg/src/einsum.rs | Rewrites batched 2-tensor einsum contraction to an allocation-free-per-slice GEMM loop with upfront validation and new helpers. |
| crates/ariadnetor-linalg/src/einsum/tests.rs | Adds unit coverage for row-major vs column-major backend agreement on the batched contraction path. |
| crates/ariadnetor-linalg/tests/einsum.rs | Adds integration tests for multi-batch asymmetric output order, invalid-argument cases, and zero-extent contracted axis behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Two batched-path issues in the einsum dispatcher, surfaced in review: - `einsum_pair` computed the GEMM sizes with `dim_of` before any rank check, so an operand whose rank is below its notation arity panicked out of bounds instead of returning `InvalidArgument`. Validate ranks once, before dispatch, covering both the Hadamard and batched paths (mirroring `contract_dense`'s check on the non-batched path). - The Hadamard path was selected when the free / contracted extents all happened to be 1 (`m == n == k == 1`), which misroutes a genuine batched contraction with unit free / contracted axes: `hadamard` builds its output shape from the batch axes alone and drops those axes. Gate on the index *sets* being empty instead, so a size-1 free or contracted axis still routes to the batched GEMM and is kept. Deleting the extent-based Hadamard test removed the only pre-dispatch `dim_of`, so the batched extent guard alone remains in `batched_contract`.
This was referenced Jul 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The batched einsum contraction ran a full
contract_denseper batch slice — two operand copies, two memory-order reorders, and a notation re-parse plus plan rebuild around a small GEMM. That fixed overhead dominates when the batch and per-slice dimensions are small, as in the successive randomized compression (SRC) sweep's environment recursion, where these tiny batched contractions were the largest single cost of an MPO–MPS apply. This rewrites the per-slice loop as one bare backend GEMM into a preallocated output.Closes #475
Changes
crates/ariadnetor-linalg/src/einsum.rs— rewritebatched_contract: permute and reorder both operands once so the batch axes are the slowest-varying in the backend's preferred order (batch-first for row-major, batch-last for column-major), making every per-slice(m, k)/(k, n)block a contiguous range; loop onebackend.gemmper slice into a preallocated buffer, with(m, n, k)and the execution policy computed once; validate operand ranks and shared batch/contracted extents up front (returningInvalidArgument); short-circuit degenerate (zero-extent / empty-batch) contractions; extractgroup_dims/group_extent; drop the now-unusedbuild_batch_free_notation.crates/ariadnetor-linalg/src/einsum/tests.rs(new) — a differential test asserting a row-major-preferred and the native column-major backend agree, exercising the row-major layout branch the native backend never selects.crates/ariadnetor-linalg/tests/einsum.rs— add multi-batch asymmetric interleaved-output, mismatched-extent, rank-exceeds-arity, and zero-extent cases.Impact
Any
einsumwhose pairwise decomposition carries a batch index routes throughbatched_contract(the MPS environment recursion, the top-crate einsum wrappers,host_ops). The GEMM stays inpreferred_order(batch on the slowest-varying axis), so theComputeBackendcontract — compute kernels run inpreferred_order, only transpose is layout-parametric — is not widened. Output is unchanged up to floating-point reassociation.Test plan
bik,bkj->bijand its output-order variants,bi,bi->b,aij,ajk,ak->ai,bkli,bjlk->bij) pass unchanged; they pin the numerical result (integer cases bit-exact, multi-contracted to1e-10).abik,abkj->baji), row-major / column-major backend agreement, mismatched contracted extent ->InvalidArgument, rank exceeds arity ->InvalidArgument, zero contracted extent -> zeros.cargo bench -p ariadnetor-algorithms --bench mpo_mps_apply, before/after on the same machine:adaptive/n8_chi8-18%,adaptive/n12_chi16-21%,fixedarms -14%.cargo make testgreen;cargo make clippyclean.Plan-vs-actual delta
batched_contract) was 20–26%. Items 1–5 are deferred as measured noise; only item 6 landed. A comment on Eliminate the per-round copies in the SRC sweep #475 records the per-site breakdown and the sizes measured.einsum_pair, next to the rewritten path:dim_ofran before the rank check (panicking on a rank-below-arity operand instead of returningInvalidArgument), and the Hadamard path was gated on the extents being 1 rather than the index sets being empty (dropping unit free/contracted axes). Both are fixed here with regression tests, since they sit in the code this PR rewrites.adaptive/n8_chi8no longer regresses against the pre-Phase 2: incremental QR and batched environment sketching for SRC #473 baseline (-18%) andadaptive/n12_chi16improves further (-21%); thefixedarms improved too (-14%).