Skip to content

Commit e5693a4

Browse files
committed
expressions: deduce inner-node index sets top-down from the assignment target (Phase E)
An index shared by the two children of a product is fused iff the node's target carries it, contracted otherwise; an index neither the sibling nor the target carries is consumed within the child subtree and not demanded of it. available_indices() (per-subtree leaf-annotation union, valid before init) supplies the up-pass; each child dictates the ORDER of its demand via preferred_layout() (canonical (fused, left-free, right-free) for products, pass-through elsewhere). Expressions consumed without a target (reductions) retain the bottom-up contraction convention.
1 parent 23fc095 commit e5693a4

4 files changed

Lines changed: 135 additions & 36 deletions

File tree

src/TiledArray/expressions/binary_engine.h

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,35 @@ class BinaryEngine : public ExprEngine<Derived> {
229229
right_inner_permtype_ == PermutationType::general);
230230
}
231231

232+
/// \return the indices this subtree can supply (Phase E up-pass): the
233+
/// first-occurrence-ordered union of the children's available indices,
234+
/// outer and inner lists separately. Valid before init: it depends only on
235+
/// the leaf annotations, never on resolved (post-init) index sets.
236+
BipartiteIndexList available_indices() const {
237+
auto union_ = [](auto const& a, auto const& b) {
238+
container::svector<std::string> r(a.begin(), a.end());
239+
for (auto&& idx : b)
240+
if (!a.count(idx)) r.push_back(idx);
241+
return r;
242+
};
243+
auto const l = left_.available_indices();
244+
auto const r = right_.available_indices();
245+
auto const out = union_(outer(l), outer(r));
246+
auto const in = union_(inner(l), inner(r));
247+
return BipartiteIndexList(IndexList(out.begin(), out.end()),
248+
IndexList(in.begin(), in.end()));
249+
}
250+
251+
/// \return the layout this subtree prefers for producing the index set of
252+
/// \p demand: element-wise binary ops (Add/Subt) impose no layout of their
253+
/// own (both children must produce the same set and are aligned by
254+
/// permutation), so the demand is returned unchanged. MultEngine overrides
255+
/// this with its canonical product layout.
256+
const BipartiteIndexList& preferred_layout(
257+
const BipartiteIndexList& demand) const {
258+
return demand;
259+
}
260+
232261
/// Initialize result tensor structure
233262

234263
/// This function will initialize the permutation, tiled range, and shape

src/TiledArray/expressions/leaf_engine.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,18 @@ class LeafEngine : public ExprEngine<Derived> {
127127
/// This function is a noop since the index list is fixed.
128128
void init_indices() {}
129129

130+
/// \return the indices this subtree can supply (Phase E up-pass): for a
131+
/// leaf, simply its annotation (set at construction, valid before init)
132+
const BipartiteIndexList& available_indices() const { return indices_; }
133+
134+
/// \return the layout this subtree prefers for producing the index set of
135+
/// \p demand: a leaf accepts any ordering (the consumer aligns against the
136+
/// fixed annotation by permutation), so the demand is returned unchanged
137+
const BipartiteIndexList& preferred_layout(
138+
const BipartiteIndexList& demand) const {
139+
return demand;
140+
}
141+
130142
void init_distribution(World* world,
131143
const std::shared_ptr<const pmap_interface>& pmap) {
132144
ExprEngine_::init_distribution(world, (pmap ? pmap : array_.pmap()));

src/TiledArray/expressions/mult_engine.h

Lines changed: 84 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -281,44 +281,58 @@ class MultEngine : public ContEngine<MultEngine<Left, Right, Result>> {
281281

282282
/// \param target_indices The target index list for this expression
283283
void init_indices(const BipartiteIndexList& target_indices) {
284-
// to decide what type of product this is must initialize indices down
285-
// the tree.
286-
// N.B. since this may be a contraction we do not know the target indices
287-
// for the left and right, hence do target-neutral initialization
288-
BinaryEngine_::left_.init_indices();
289-
BinaryEngine_::right_.init_indices();
290-
291-
// Validate that the (bottom-up resolved) child indices are consistent
292-
// with the target: every outer index of each child must appear in the
293-
// other child or in the target (as a free, fused, or contracted index).
294-
// A violation usually means a *general* product (fused + contracted +
295-
// free indices) appears at an INNER node of the expression tree, where
296-
// the role of a shared index cannot be deduced bottom-up; e.g. in the
297-
// THC-like g("p,q,r,s") = X("p,r1") * X("q,r1") * Z("r1,r2") * ... the
298-
// index r1 is fused in X*X but contracted downstream, while the
299-
// bottom-up convention contracts it in X*X, orphaning the r1 of Z.
300-
// Resolving this requires pushing the needed-index sets down the
301-
// expression tree; until then materialize such inner products into
302-
// explicit intermediates, so that every general product appears as the
303-
// root of its own assignment (where the target determines the index
304-
// roles).
284+
// Deduce each child's index SET top-down (Phase E). The index set an
285+
// inner node must produce depends on what its ancestors need: a shared
286+
// index of the two children is fused iff this node's target carries it,
287+
// contracted here otherwise; an index of one child that neither the
288+
// sibling nor the target carries is consumed entirely within that
289+
// child's subtree and is not demanded of it.
290+
// Each child's demand is ordered target-kept-first (the indices this
291+
// node's result keeps, in target order) followed by the
292+
// contracted-here indices in the child's leaf-availability order --
293+
// fused indices thus lead, matching the canonical layouts of
294+
// GeneralPermutationOptimizer (h leading) so the nbatch fold stays
295+
// zero-copy. E.g. in the THC-like g("p,q,r,s") = X("p,r1") * X("q,r1")
296+
// * Z("r1,r2") * ... the index r1 is demanded of X*X by its consumer
297+
// (fused there), and contracted where Z meets the X*X subtree.
298+
// N.B. expressions consumed WITHOUT a target (reductions, e.g. dot)
299+
// take the no-target init_indices() overload below, which retains the
300+
// bottom-up contraction convention -- general products under
301+
// reductions remain unsupported.
305302
{
306-
auto const& left_outer = outer(BinaryEngine_::left_.indices());
307-
auto const& right_outer = outer(BinaryEngine_::right_.indices());
308-
auto const& target_outer = outer(target_indices);
309-
auto validate = [&](const IndexList& a, const IndexList& b) {
310-
for (auto&& idx : a)
311-
if (!b.count(idx) && !target_outer.count(idx))
312-
TA_EXCEPTION(
313-
"MultEngine: an argument index appears in neither the other "
314-
"argument nor the target. If a general product (fused + "
315-
"contracted + free indices) appears at an inner node of the "
316-
"expression tree, its index roles cannot be deduced "
317-
"bottom-up; materialize it into an explicit intermediate so "
318-
"that it appears as the root of its own assignment");
303+
auto const avail_l = BinaryEngine_::left_.available_indices();
304+
auto const avail_r = BinaryEngine_::right_.available_indices();
305+
auto demand = [](auto const& avail, auto const& sibling,
306+
auto const& tgt) {
307+
container::svector<std::string> r;
308+
for (auto&& idx : tgt)
309+
if (avail.count(idx)) r.push_back(idx);
310+
for (auto&& idx : avail) {
311+
if (tgt.count(idx)) continue;
312+
if (sibling.count(idx)) r.push_back(idx);
313+
// else: the index is consumed entirely within the child's own
314+
// subtree (contracted deeper down) -- not demanded here. A
315+
// genuinely orphaned index (single occurrence, demanded nowhere =
316+
// implicit trace, unsupported) surfaces as the leaf-level
317+
// target-is-not-a-permutation error.
318+
}
319+
return r;
319320
};
320-
validate(left_outer, right_outer);
321-
validate(right_outer, left_outer);
321+
auto bipartite_demand = [&demand](auto const& avail, auto const& sib,
322+
auto const& tgt) {
323+
auto const out = demand(outer(avail), outer(sib), outer(tgt));
324+
auto const in = demand(inner(avail), inner(sib), inner(tgt));
325+
return BipartiteIndexList(IndexList(out.begin(), out.end()),
326+
IndexList(in.begin(), in.end()));
327+
};
328+
// each child dictates the ORDER of its demand (preferred_layout): a
329+
// product child reorders to its canonical (h, eA, eB) layout -- a
330+
// general product cannot host a result permutation, and contraction
331+
// consumers absorb any child layout via the GEMM transpose forms
332+
BinaryEngine_::left_.init_indices(BinaryEngine_::left_.preferred_layout(
333+
bipartite_demand(avail_l, avail_r, target_indices)));
334+
BinaryEngine_::right_.init_indices(BinaryEngine_::right_.preferred_layout(
335+
bipartite_demand(avail_r, avail_l, target_indices)));
322336
}
323337

324338
this->product_type_ = compute_product_type(
@@ -377,6 +391,40 @@ class MultEngine : public ContEngine<MultEngine<Left, Right, Result>> {
377391
}
378392
}
379393

394+
/// \return the layout this product prefers for producing the index set of
395+
/// \p demand: the canonical general-product result layout (h, eA, eB) --
396+
/// indices supplied by both children (fused here) lead, then indices
397+
/// supplied by the left child only, then by the right child only, each
398+
/// group in demand order. h-leading is required when this node resolves to
399+
/// a general product (general results cannot host a result permutation and
400+
/// the nbatch fold needs the fused modes leading); for a pure contraction
401+
/// (empty h) the (eA, eB) order matches the GEMM result, and for a pure
402+
/// Hadamard the demand order is preserved.
403+
BipartiteIndexList preferred_layout(const BipartiteIndexList& demand) const {
404+
auto const avail_l = BinaryEngine_::left_.available_indices();
405+
auto const avail_r = BinaryEngine_::right_.available_indices();
406+
auto canonical = [](auto const& d, auto const& al, auto const& ar) {
407+
container::svector<std::string> h, ea, eb;
408+
for (auto&& idx : d) {
409+
bool const inl = al.count(idx);
410+
bool const inr = ar.count(idx);
411+
if (inl && inr)
412+
h.push_back(idx);
413+
else if (inl)
414+
ea.push_back(idx);
415+
else
416+
eb.push_back(idx);
417+
}
418+
h.insert(h.end(), ea.begin(), ea.end());
419+
h.insert(h.end(), eb.begin(), eb.end());
420+
return h;
421+
};
422+
auto const out = canonical(outer(demand), outer(avail_l), outer(avail_r));
423+
auto const in = canonical(inner(demand), inner(avail_l), inner(avail_r));
424+
return BipartiteIndexList(IndexList(out.begin(), out.end()),
425+
IndexList(in.begin(), in.end()));
426+
}
427+
380428
/// Initialize result tensor structure
381429

382430
/// This function will initialize the permutation, tiled range, and shape

src/TiledArray/expressions/unary_engine.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,16 @@ class UnaryEngine : ExprEngine<Derived> {
120120
indices_ = arg_.indices();
121121
}
122122

123+
/// \return the indices this subtree can supply (Phase E up-pass): a unary
124+
/// op supplies exactly what its argument supplies (valid before init)
125+
decltype(auto) available_indices() const { return arg_.available_indices(); }
126+
127+
/// \return the layout this subtree prefers for producing the index set of
128+
/// \p demand: a unary op defers to its argument's preference
129+
decltype(auto) preferred_layout(const BipartiteIndexList& demand) const {
130+
return arg_.preferred_layout(demand);
131+
}
132+
123133
/// Initialize result tensor structure
124134

125135
/// This function will initialize the permutation, tiled range, and shape

0 commit comments

Comments
 (0)