@@ -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
0 commit comments