Skip to content

ContractReduce: fall back to value-returning conj when a tile has no conj_to - #586

Merged
evaleev merged 2 commits into
masterfrom
evaleev/fix/conj-finalization-value-returning
Sep 11, 2026
Merged

ContractReduce: fall back to value-returning conj when a tile has no conj_to#586
evaleev merged 2 commits into
masterfrom
evaleev/fix/conj-finalization-value-returning

Conversation

@evaleev

@evaleev evaleev commented Sep 11, 2026

Copy link
Copy Markdown
Member

Closes #585.

Problem

TiledArray::conj and TiledArray::conj_to are independent ADL customization points, and the default conj_to in tile_op/tile_interface.h is SFINAE-constrained on a conj_to() member. A custom tile that implements only the value-returning conj is a legal partial implementation of the tile interface — the pattern AGENTS.md blesses, with tests/sparse_tile.h's EigenSparseTile omitting subt as precedent — but it could not be used in a conjugated contraction.

The two CPOs have exactly one call site each in the whole library, and it is the same function: ContractReduce's ComplexConjugate finalization, choosing between them on whether a result permutation is present. So for such a tile:

c("k,i") = conj(a("i,j") * b("j,k"));   // compiles   (permuted   -> conj)
c("i,k") = conj(a("i,j") * b("j,k"));   // hard error (unpermuted -> conj_to)

On master (84cc198) instantiating that finalization is
contract_reduce.h:574: no matching function for call to 'conj_to', and
:581 for the scaled specialization.

Unary conj(a("i,j")) was never affected — it is a Scal expression with a ComplexConjugate factor and goes through scale/scale_to.

Change

  • TiledArray::has_conj_to_v<T...> in tile_op/tile_interface.h, next to the CPOs it describes: whether the ADL call conj_to(args...) is viable.
  • detail::conj_finalize(temp, factor...) in contract_reduce.h: in place through conj_to when that is viable, otherwise through the value-returning conj. Both unpermuted branches of the finalization route through it; the permuted branches already used conj and are untouched.

The detection is on the ADL call, not on a conj_to() member. btas::Tensor has no such member — only free functions in namespace btas — so detail::has_member_function_conj_to_anyreturn_v reports false for it, and a member-based test would silently demote BTAS from in-place conjugation to an allocate/copy/free per tile. That is a performance regression that would pass every existing test, so tile_op_contract_reduce.cpp carries a static_assert pinning the distinction.

No behavior change for any tile in the tree: Tensor, TensorInterface, ArenaTensor, Tile, btas::Tensor and the nested Tensor<ArenaTensor<...>> / Tensor<Tensor<...>> forms all satisfy has_conj_to_v and keep the in-place path.

Tests

Three cases in tile_op_contract_reduce_suite:

  • conj_to_detectionhas_conj_to_v is true for Tensor<complex>, btas::Tensor<complex>, Tensor<ArenaTensor<complex>> and Tensor<Tensor<complex>>, with and without a scale; false for the fallback tile. Plus the static_assert recording that BTAS has no conj_to member.
  • conj_finalize_in_place_when_supported — the Tensor path conjugates the argument itself, i.e. no copy is made.
  • conj_finalization_without_conj_to — the regression: builds ContractReduce<ConjOnlyTile, …, ComplexConjugate<void>> and <…, ComplexConjugate<double>> and runs the finalization. ConjOnlyTile supplies the four value-returning conj overloads and no conj_to, member or free.

tile_op_contract_reduce 24/24 and tot_expressions (which exercises this finalization heavily on owning, arena and BTAS cells) pass under ASan. Verified that the regression bites: the same ContractReduce instantiation written against pre-existing API fails on master with the conj_to error above and compiles here.

…conj_to

conj and conj_to are independent tile-interface customization points, and the
default conj_to is SFINAE-constrained on a conj_to() member, so a tile that
implements only the value-returning conj is a legal partial implementation
that could not be used in a conjugated contraction. The two CPOs have exactly
one call site each in the library -- the same function, ContractReduce's
ComplexConjugate finalization -- picking between them on whether a result
permutation is present, so such a tile compiled

  c("k,i") = conj(a("i,j") * b("j,k"));   // permuted   -> conj
  c("i,k") = conj(a("i,j") * b("j,k"));   // unpermuted -> conj_to, hard error

- TiledArray::has_conj_to_v<T...>: whether the ADL call conj_to(args...) is
  viable, declared next to the CPOs it describes.
- detail::conj_finalize(temp, factor...): in place through conj_to when that
  is viable, else through conj. Both unpermuted branches of the finalization
  route through it; the permuted ones already used conj.

The detection is on the ADL CALL, not on a conj_to() member. btas::Tensor has
no member -- only free functions in namespace btas -- so a member-based test
would report false for it and silently move it from in-place conjugation to an
allocate/copy/free per tile, a performance regression no existing test would
catch. tile_op_contract_reduce.cpp static_asserts that distinction.

No behavior change for any tile in the tree: Tensor, TensorInterface,
ArenaTensor, Tile, btas::Tensor and the nested Tensor<ArenaTensor<...>> /
Tensor<Tensor<...>> forms all satisfy has_conj_to_v and keep the in-place path.

Tests: has_conj_to_v over those types with and without a scale (plus the BTAS
static_assert); the Tensor path conjugating its argument in place; and the
regression itself -- ContractReduce<ConjOnlyTile, ..., ComplexConjugate<void>>
and <..., ComplexConjugate<double>> finalizations run for a tile carrying the
four value-returning conj overloads and no conj_to.
perm() is a runtime value, so both branches of the ComplexConjugate
finalization are instantiated for every tile used there: the permuted one
always needs the value-returning conj(result, perm) (or conj(result, factor,
perm)) whether or not a permutation is ever applied, exactly as the primary
template's finalization unconditionally instantiates Permute<Result, Result>.
Only conj_to is optional, via the fallback added in the previous commit.

A tile missing one of those overloads currently fails inside tile_interface.h's
default conj with "too many arguments to function call", which names neither
the tile nor the requirement. Record the contract where it bites, and record
why it is prose rather than a static_assert: the four conj CPOs are constrained
only on Perm being a permutation and have a DEDUCED return type, so
decltype(conj(arg, perm)) has to instantiate the body -- detecting them
hard-errors instead of yielding false, unlike conj_to, whose CPO is constrained
on the member the way neg_to's is. Making conj detectable means constraining
those four overloads, a change to a public header's overload set that wants its
own PR.

Documentation only; no change to generated code.
@evaleev

evaleev commented Sep 11, 2026

Copy link
Copy Markdown
Member Author

Added the tile-requirement contract as documentation on the finalization (6955f11).

I tried it first as a static_assert with a has_conj_v trait mirroring has_conj_to_v, and that does not work — worth recording, because the asymmetry is not obvious:

  • conj_to's default CPO is constrained on the member existing (has_member_function_conj_to_anyreturn_v), like neg_to's. A tile without it is a clean substitution failure, so has_conj_to_v correctly yields false. That is what makes the fallback in this PR sound.
  • the four conj CPOs are constrained only on Perm being a permutation, and have a deduced return type. So decltype(conj(arg, perm)) must instantiate the body — detecting a tile that lacks the member is a hard error, not false. A has_conj_v built that way is a footgun: it hard-errors where callers expect it to answer, and the negative test showed the confusing tile_interface.h: too many arguments diagnostic still arriving first, with the static_assert only piling on after.

So the requirement is prose for now, and the note says what it would take to make it enforceable: constrain the four conj overloads the way conj_to and neg_to already are. That is a change to a public header's overload set and deserves its own PR rather than riding along here.

Verified: the sweep over every TU that instantiates this finalization — expressions{,_sparse,_complex,_btas,_mixed}, tot_dist_array_part{1,2}, einsum, tot_construction, tot_expressions, tile_op_contract_reduce — is clean, and tile_op_contract_reduce 24/24 passes under ASan.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Approval recommended

No unresolved blocking issues were identified, and regression coverage is included.

Pull request overview

Adds ADL-based conj_to detection and falls back to value-returning conj during ContractReduce finalization.

Changes:

  • Adds has_conj_to_v.
  • Selects in-place or fallback conjugation.
  • Adds regression and detection tests.
File summaries
File Summary
tests/tile_op_contract_reduce.cpp Tests detection and fallback behavior.
src/TiledArray/tile_op/tile_interface.h Adds ADL-based conj_to detection.
src/TiledArray/tile_op/contract_reduce.h Implements in-place or value-returning finalization.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@evaleev
evaleev merged commit 7993e80 into master Sep 11, 2026
10 checks passed
@evaleev
evaleev deleted the evaleev/fix/conj-finalization-value-returning branch September 11, 2026 16:45
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.

ContractReduce's conj finalization requires conj_to, so a tile with only value-returning conj fails on the unpermuted path

2 participants