Skip to content

Lift the >2-tensor limit on external auxiliary hyperindices (TNv3 canonicalization + BTAS batched eval)#574

Merged
evaleev merged 6 commits into
masterfrom
worktree-fix-highorder-hyperindex-canon
Jul 7, 2026
Merged

Lift the >2-tensor limit on external auxiliary hyperindices (TNv3 canonicalization + BTAS batched eval)#574
evaleev merged 6 commits into
masterfrom
worktree-fix-highorder-hyperindex-canon

Conversation

@evaleev

@evaleev evaleev commented Jul 7, 2026

Copy link
Copy Markdown
Member

Summary

SeQuant already supports hyperindices — indices shared among more than two tensors — as exercised by the existing tensor-hypercontraction / THC tests and the order-3 T{a1;i1} T{a2;i1} T{a3;i1} test. This PR does not add hyperindex support; it lifts a limitation that only shows up at higher order.

In every existing test the hyperindex is either contracted (an internal/anonymous mode — skipped by the named-index handling in canonicalization) or external but shared by at most two tensor slots. Neither reaches the one code path that assumed a named index's edge connects at most two vertices, so the limit was never hit — the existing coverage tops out at order-3 contracted hyperindices.

Laplace-transform MP2 is the first case with an external hyperindex of order > 2: a batching/quadrature index z1 rides in the aux slot of every factor and of the result E{;;z1} (order 9 here). That tripped the assertion, and — once past it — could not be evaluated. Both layers are addressed:

1. Tree building (TNv3 canonicalization)

TensorNetworkV3::canonicalize_slots classified each named index by its edge's vertex count, handling 1 (an open bra/ket/aux slot) or 2 (an internal contraction that is also named, e.g. a protoindex) and asserting vertex_count() == 2 otherwise. A named index shared by >2 tensor slots forms a hyperedge with >2 vertices and tripped that assert.

Fix: classify an all-aux, >2-vertex named edge as a TensorAux slot. A non-auxiliary high-order hyperindex has no well-defined slot type and is rejected with an explicit throw (in every build config, not only assertion-enabled ones).

2. Numerical evaluation

Each contraction node over such an index is a batched (Hadamard) contraction: the index is present in the left operand, the right operand and the result, so it must be iterated, not summed.

  • BTASbtas::contract has no batch category (an index in both operands is always summed), so it aborted. ResultTensorBTAS::prod now detects batch axes (batch_indices) and routes through a new batched_contract() that permutes the batch axes to the front (making each slice contiguous in row-major storage) and contracts each slice — falling back to a dot or a scaling for the degenerate slice shapes.
  • TiledArray — no backend change needed; einsum supports batch axes natively. Only the layer-1 fix was required.

Tests

  • binarize_highorder_aux_hyperindex (test_eval_btas.cpp) — reproduces the Laplace-MP2 energy expression; asserts the eval tree builds and carries z1 in the aux slot of every tensor node. Confirmed red→green against the fix.
  • btas_batched_contract — unit-tests prod() on every slice shape (contract+batch, dot+batch, scalar·tensor+batch, non-canonical result order).
  • eval_btas_batched_over_aux — end-to-end binarize + evaluate of a 3-tensor batched-over-aux contraction against a hand-computed reference.
  • test_eval_ta.cpp non-covariant section — end-to-end TA evaluation of the same class of expression against a TA::einsum reference.

Full BTAS unit suite: 7194 assertions / 65 cases pass. TA eval_with_tiledarray case passes with the new test.

Notes

  • IndexSlotType::TensorAuxAux is removed — it had no users, and a high-order auxiliary hyperindex is classified as TensorAux, so there is no distinct aux-aux slot type.
  • The batch-extent agreement of the two operands is asserted in batched_contract before slicing.

evaleev added 5 commits July 7, 2026 11:36
TensorNetworkV3::canonicalize_slots classified each named (external) index
by its edge's vertex count, asserting the count was 1 (an open bra/ket/aux
slot) or 2 (an internal contraction edge that is also named). A named index
shared among more than two tensor slots -- a high-order hyperindex, e.g. an
auxiliary/batching index riding in the aux slot of every factor as in
Laplace-transform MP2 or tensor hypercontraction -- forms an edge with >2
vertices and tripped `SEQUANT_ASSERT(edge_it->vertex_count() == 2)`.

This made `binarize<EvalExprBTAS>(deserialize<ResultExpr>(...))` throw for any
expression whose result carries such an index (whereas hyperindices that are
fully contracted are anonymous and already handled, since the named-index loop
skips them). Classify a >2-vertex named edge whose vertices are all aux as a
TensorAux slot; a non-aux high-order hyperindex remains unsupported and now
fails with an explanatory assertion message rather than a bare count check.

Adds a regression test reproducing the Laplace-MP2 energy expression that
verifies the eval tree builds and carries the batching index z1 in the aux
slot of every tensor node. NB this covers tree construction only; numerically
evaluating such a tree is a separate concern -- every node is a batched
(Hadamard) contraction over z1, which the BTAS backend's btas::contract does
not support (an index in both operands is always summed, never batched).
btas::contract classifies every index as free-left, free-right or contracted --
an index shared by both operands is always summed -- so it cannot evaluate a
contraction that carries a batch axis: a label present in both operands AND the
result, e.g. an auxiliary/quadrature index ridden through the contraction as in
Laplace-transform MP2 or tensor hypercontraction. Such a product previously
aborted inside btas::contract.

ResultTensorBTAS::prod now detects batch axes (batch_indices: labels common to
left, right and result) and, when present, routes through batched_contract():
the batch axes are permuted to the front of both operands (making each batch
slice a contiguous block in row-major storage) and each slice is contracted
with plain btas::contract, or -- for the degenerate slice shapes -- a dot
(empty result remainder) or a scaling (a scalar operand slice). The per-slice
results are assembled and permuted back into the requested result order.

Tests (test_eval_btas.cpp):
- btas_batched_contract: unit-tests prod() directly on every slice shape
  (contract+batch, dot+batch, scalar*tensor+batch, non-canonical result order).
- eval_btas_batched_over_aux: end-to-end binarize + evaluate of
  R{;;z1} = A{;a1;z1} B{a1;a2;z1} C{a2;;z1} (z1 shared by 3 tensors and carried
  into the result) against a hand-computed reference.
…or clean when assertions are disabled (SEQUANT_ASSERT_BEHAVIOR=IGNORE)
…-end

Adds a case to the "non-covariant indices" section evaluating
R{;;x1} = A{;a1;x1} B{a1;a2;x1} C{a2;;x1}, where the auxiliary index x1 is
shared by 3 tensors AND carried into the result -- the >2-tensor named
hyperedge unlocked by the TNv3 canonicalize_slots fix. TiledArray evaluates the
resulting batched-over-x1 contraction natively (einsum keeps an index common to
both operands and the result), matching a hand-written einsum reference. No
backend change is needed for TA (unlike BTAS, whose btas::contract required the
batched_contract path).
The enumerator had no users; a high-order auxiliary hyperindex is classified as
TensorAux (see canonicalize_slots), not a distinct aux-aux bundle type.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for external high-order (>2-tensor) auxiliary hyperindices by updating TNv3 slot canonicalization and extending the BTAS backend to evaluate batched (Hadamard) contractions over shared aux axes.

Changes:

  • TNv3 canonicalize_slots now recognizes named hyperedges with >2 vertices as TensorAux when all incident slots are auxiliary.
  • BTAS backend detects batch axes (labels present in left, right, and result) and evaluates them via per-batch slicing + contraction.
  • Adds TA + BTAS unit/e2e tests covering high-order aux hyperindices and BTAS batched contraction behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/unit/test_eval_ta.cpp Adds TA regression test for a >2-tensor external aux hyperindex carried into the result.
tests/unit/test_eval_btas.cpp Adds BTAS tests for TNv3 tree building and BTAS batched-over-aux evaluation paths.
SeQuant/core/tensor_network/v3.cpp Extends slot classification to handle named >2-vertex aux hyperedges.
SeQuant/core/tensor_network/slot.hpp Removes TensorAuxAux from IndexSlotType.
SeQuant/core/eval/backends/btas/result.hpp Implements batch-axis detection and batched_contract() for BTAS tensor products.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread SeQuant/core/tensor_network/v3.cpp Outdated
Comment on lines +805 to +815
[[maybe_unused]] bool all_aux = true;
for (std::size_t v = 0; v != edge_it->vertex_count(); ++v)
if (edge_it->vertex(v).getOrigin() != Origin::Aux) {
all_aux = false;
break;
}
SEQUANT_ASSERT(all_aux &&
"TensorNetworkV3::canonicalize_slots: high-order "
"(shared among >2 tensor slots) non-auxiliary "
"hyperindices are not supported");
slot_type = IndexSlotType::TensorAux;
Comment on lines +189 to +202
auto const nb = batch.size();
auto tail_extents = [](T const& t, std::size_t skip) {
container::svector<std::size_t> e;
std::size_t sz = 1;
for (std::size_t i = skip; i != t.rank(); ++i) {
e.push_back(t.extent(i));
sz *= t.extent(i);
}
return std::pair{std::move(e), sz};
};
std::size_t P = 1;
for (std::size_t i = 0; i != nb; ++i) P *= Lp.extent(i);
auto const [rlext, rlsz] = tail_extents(Lp, nb);
auto const [rrext, rrsz] = tail_extents(Rp, nb);
Comment on lines 38 to 42
/// connecting two tensor vector index slots
TensorBraKet,
/// in tensor aux index slot
TensorAux,
/// connecting two tensor aux index slots
TensorAuxAux,
};
- canonicalize_slots: a non-auxiliary high-order (>2-slot) hyperindex now throws
  unconditionally instead of via SEQUANT_ASSERT, so it hard-errors in every
  build config (previously, with assertions disabled it silently fell through to
  TensorAux). all_aux is now read unconditionally, so the [[maybe_unused]] is
  gone.
- batched_contract: assert the two operands agree on every batch extent before
  deriving the batch count P from the left operand and slicing.
@evaleev

evaleev commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

Thanks for the review. Addressed in 8474a9d:

  1. v3.cpp — non-aux hyperindex silently classified under IGNORE (valid): changed the SEQUANT_ASSERT to an unconditional throw Exception, so a non-auxiliary high-order hyperindex hard-errors in every build config rather than falling through to TensorAux when assertions are compiled out. all_aux is now read unconditionally (the [[maybe_unused]] is gone).

  2. result.hppP derived only from the left operand (valid): added a SEQUANT_ASSERT that the two operands agree on every batch extent (their leading nb axes after the batch-first permute) before computing P and slicing.

  3. slot.hpp — keep TensorAuxAux as a legacy alias (declined): the enumerator had no users anywhere in the tree, and its removal was explicitly requested. A high-order auxiliary hyperindex is classified as TensorAux, so there is no distinct aux-aux slot type to preserve. Leaving it removed.

@evaleev evaleev changed the title Support high-order (>2-tensor) auxiliary hyperindices: TNv3 canonicalization + BTAS batched contraction Lift the >2-tensor limit on external auxiliary hyperindices (TNv3 canonicalization + BTAS batched eval) Jul 7, 2026
@evaleev
evaleev merged commit 9fef955 into master Jul 7, 2026
26 of 28 checks passed
@evaleev
evaleev deleted the worktree-fix-highorder-hyperindex-canon branch July 7, 2026 23:09
evaleev added a commit that referenced this pull request Jul 10, 2026
Reconcile the predicted-peak / batching / dry-run work with master's merged
#576 (outer-product pruning + build_context skip + fast_flops), #575/#574
(iterative evaluate()/binary_node), and TNv3 high-order aux work.

Conflict resolutions:
- options.hpp: CostParams and OptimizeOptions carry BOTH the branch's
  peak_threshold / term_batch_axes and master's prune_outer_products.
- single_term.hpp: keep the perf-first objective dispatch (perf_first,
  DenseTimeSpace/DenseTimeSpaceBatched, peak_threshold, out_axes) and thread
  master's prune_outer_products into every model.
- cost_model.hpp: PeakModel / PeakBatchedModel keep the branch's perf-first
  members (perf_first, peak_threshold, numeric_size, charge_batch_recompute)
  and gain master's prune_outer_products; build_context bodies auto-merged so
  the pruning skip + fast_flops precompute coexist with the perf-first tables.
- optimize.cpp: CostParams init passes both peak_threshold and
  prune_outer_products.
- eval.hpp: keep the branch's malloc_trim release_after_op() hook at each op.
- test_optimize.cpp: keep both the branch's threshold/perf-first tests and
  master's pruning + fast-flops-parity tests.

The branch's own (superseded) outer-product-pruning commits were dropped before
this merge since #576 supersedes them; the predict-hook add/remove experiment
nets to no hook, matching master.
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