Generalize AsyCost#538
Conversation
9156e91 to
8f33716
Compare
There was a problem hiding this comment.
Pull request overview
This PR generalizes AsyCost from a fixed occupied/virtual (O/V) polynomial representation to a map keyed by arbitrary IndexSpace, enabling asymptotic cost and memory scalings to be expressed over any number of index spaces (including auxiliary and batching spaces).
Changes:
- Replaced O/V exponent pairs with an
IndexSpace -> exponentmap and updated printing/comparison semantics accordingly. - Updated flop/memory accounting in
eval_nodeto count indices byIndexSpace(including aux slots). - Updated and expanded unit tests to construct/compare costs via the exponent-map API and to cover generalized spaces.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
SeQuant/core/asy_cost.hpp |
Updates AsyCost public API and documentation to use IndexSpace-keyed exponent maps. |
SeQuant/core/asy_cost.cpp |
Implements generalized exponent-map storage, printing, ordering, and numeric evaluation via extents. |
SeQuant/core/eval/eval_node.hpp |
Updates flop/memory estimation to count indices per IndexSpace (including auxiliary indices). |
SeQuant/core/eval/cache_manager.hpp |
Adjusts documentation to match generalized AsyCost semantics. |
tests/unit/test_asy_cost.cpp |
Refactors tests for new API and adds coverage for >2 spaces and missing extents behavior. |
tests/unit/test_eval_node.cpp |
Updates expected costs to the new API and adds tests for density-fitting, batching, and MR-like examples. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
2ffed47 to
5a94615
Compare
evaleev
left a comment
There was a problem hiding this comment.
Review: Generalize AsyCost
Overview
This PR replaces AsyCost's hard-coded two-space (occupied O / virtual V) cost representation with a general IndexSpace → exponent map. A cost term becomes a rational prefactor times a product of arbitrary index-space sizes raised to integer powers. The API changes accordingly: constructors take an ExponentMap, ops() takes an ExtentMap of per-space sizes, printing uses each space's base_key(), and ordering compares total polynomial degree first, then space-by-space. eval_node.hpp is rewritten to tally indices per IndexSpace (now including aux slots, so DF/THC auxiliary spaces show up), and tests are migrated plus extended with multi-space, density-fitting, batched-index, and MRCC cases.
This is a clean, well-motivated generalization. Code quality is high, documentation is thorough, and test coverage is genuinely improved. Points worth discussing below.
Correctness
The new ordering loses the implicit V≫O weighting — please confirm this is intended. The old operator< compared the virtual exponent first, then occupied. Since V is typically much larger than O, that ordering tracked true magnitude reasonably well. The new ordering sorts by total degree (sum of exponents) first. These disagree whenever total degrees differ:
O²V⁴(degree 6) vsO⁵V²(degree 7): old ranksO²V⁴greater; new ranksO⁵V²greater. WithV≫O,O²V⁴is the larger cost, so the new ordering disagrees with actual magnitude.
This is defensible for a space-agnostic representation (you can't weight spaces without their sizes), and the same-degree tie-break by IndexSpace priority reproduces the old virt-dominant behavior within a degree. The key mitigant — which I verified — is that AsyCost is analysis/reporting only: it is consumed solely by Flops/Memory/min_storage/asy_cost in eval_node.hpp and by peak_cache (std::max(curr, max) in cache_manager.cpp). The optimize module does not use AsyCost for path selection, so contraction-order decisions are unaffected. Worth a one-line note in the PR/commit that this changes how peak_cache and reported costs rank, even though no optimizer decision depends on it.
ops() dropped the temp > 1 ? … : 0 guard — minor behavior change. The old code skipped a term whose substituted value was ≤ 1; the new loop always adds prefactor * temp. Since entries with empty exponents collapse to zero and never persist, the only divergence is when an extent of 0 or 1 is supplied (pow(1, e) = 1), which previously zeroed the term and now keeps it. Harmless for realistic extents (≥10) and arguably more correct — just flagging it.
Batched-index logic is correct. ContractedIndexCount now dedupes all indices across L/R/T into a set and counts distinct indices per space. A contracted index (in both operands) and an external index (operand + result) each collapse to one entry; a batched index (in L, R and T) also collapses to one, yielding the right a³z¹ scaling. The batched index test confirms this. 👍
Code Quality
- Excellent doc comments on the new API and the ordering rationale; the
space_countsnote about union-space indices (MR holeI=i∪ucounted as a single symbol, not decomposed) is exactly the kind of subtlety worth recording. - Moving the helpers from an anonymous namespace into
detailand fixing theis_outerpod→is_outerprodtypo are nice cleanups. - Dead code:
ContractedIndexCount::rank(NodePos)now has no consumers —Flopsonly usesunique_counts()andis_outerprod().ranks_is still needed internally for the outer-product check, but the publicrank()getter can be dropped. - Micro-nit: the zero-exponent purge in the constructor could be
std::erase_if(exponents_, [](auto& kv){ return kv.second == 0; }), though the manual loop is fine.
Tests
Strong coverage: multi-space (>2) printing/equality/eval, extent fallback to approximate_size(), default-empty ops(), total-degree vs priority ordering, DF factorization, batched indices, and an MRCC active-space case. Two small suggestions:
- The
generalized spacesLaTeX assertions check substring presence but not term order. Since ordering is a core behavior change, consider one assertion on the full rendered string to pin down term sequence. - No test exercises
AsyCost::max()flowing throughops()(should returninfinity) or through the newis_maxbranch ofoperator<. Cheap to add and the newis_max_sentinel logic is otherwise only indirectly covered.
Risk
Low. The blast radius is contained to eval cost reporting; all production call sites (eval_node.hpp, cache_manager) are updated in this PR and no external callers of the removed AsyCost(size_t,size_t) / ops(size_t,size_t) overloads remain. The main thing to consciously sign off on is the total-degree-first ordering as the intended new semantics for peak_cache/reported costs.
Recommendation
Approve with minor follow-ups: (1) note the ordering-semantics change in the commit/PR description, (2) drop the now-unused rank() getter, (3) optionally add max()-through-ops() and term-order tests.
Replace the hard-coded (active-occupied, virtual) pair with an ExponentMap<IndexSpace, size_t> so an AsyCost term can carry any number of index spaces. Ordering is driven by IndexSpace's own ordering. ops() now takes an ExtentMap and falls back to extent 1 for any space absent from it. Entries print via IndexSpace::base_key(). The old AsyCost(nocc, nvirt) constructors are removed in favor of AsyCost(ExponentMap, count=1).
Sort by the sum of exponents (overall polynomial / worst-case scaling) first, falling back to the existing space-by-space comparison only as a tie-breaker. Update the generalized-spaces test that asserted the old "highest space dominates" semantics.
space_counts was a non-template free function with internal linkage, living in an anonymous namespace in a header; TUs that include the header but never call it tripped -Wunneeded-internal-declaration under -Werror, which a [[maybe_unused]] attribute had been silencing. Move NodePos, space_counts, and ContractedIndexCount into sequant::detail and mark space_counts inline. The function now has external linkage with a single merged definition, so the warning is gone for the right reason and the attribute is no longer needed.
- Include aux slots (const_braketaux_indices) in space_counts so DF/THC auxiliary spaces contribute to the cost - Support batched indices: present in left, right, and result - Add eval_node tests covering PPL + density-fitting, batched-index cost, and an MRCC-like example
Drop the unused counts(NodePos) accessor and add comments
Rename the rational multiplier from `count` to `prefactor` across AsyCostEntry and the AsyCost constructor. Add documentation for AsyCostEntry and its members.
Rename the test-local cost builders to descriptive names and have them return AsyCost directly: `ov`/`ovx` -> `occ_virt_cost`/`occ_virt_aux_cost` (and `ov_map` -> `occ_virt_map`), standardizing on a prefactor-first signature across both test files. Addresses review feedback.
Make AsyCost::ops() substitute a space's IndexSpace::approximate_size() when that space is absent from the supplied extent map, and default the map to empty so every space uses its approximate_size(). Added new unit test.
…er tests Following the review of the AsyCost generalization: - core/eval: drop the now-unused ContractedIndexCount::rank(NodePos) getter. Flops only consumes unique_counts() and is_outerprod(); ranks_ is still needed internally for the outer-product check, so only the public getter is removed. - tests/AsyCost: pin the full rendered string (text() and to_latex()) in the "generalized spaces" case so term/space sequence is locked down, not just substring presence; add a multi-term case asserting total-degree-first ordering in the rendered output; add a "max() sentinel" section covering max() flowing through ops() (-> infinity) and the is_max branch of operator< (max compares above any finite cost and equals itself).
1ee06c7 to
0e7265a
Compare
|
good to merge once checks pass |
Generalize
AsyCostfrom O/V to arbitraryIndexSpaceAsyCostpreviously hard-coded asymptotic cost as a polynomial in exactly two orbital spaces: occupied (O) and virtual (V), with each entry storing a fixed (occ, virt) exponent pair.This PR replaces the fixed O/V representation with a general map from
IndexSpaceto integer exponent, so a cost term is now a rational multiplier times a product of arbitrary space sizes raised to integer powersChanges
IndexSpaceto exponent instead of fixed occupied/virtual powers, so any number of spaces is supported.ops()takes a map of space extents instead of two numbers.IndexSpace.test_asy_cost.cppandtest_eval_node.cppupdated to construct costs via the exponent-map API, and new tests are added with generalIndexSpaces.