Skip to content

Commit cf8e9f0

Browse files
authored
Merge pull request #563 from ValeevGroup/evaleev/feature/general-product-tree-deduction
expressions: tree-general index deduction (Phase E) — inner-node general products
2 parents 23fc095 + f8f4090 commit cf8e9f0

6 files changed

Lines changed: 287 additions & 65 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/cont_engine.h

Lines changed: 86 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#define TILEDARRAY_EXPRESSIONS_CONT_ENGINE_H__INCLUDED
2828

2929
#include <TiledArray/dist_eval/contraction_eval.h>
30+
#include <TiledArray/dist_eval/unary_eval.h>
3031
#include <TiledArray/expressions/binary_engine.h>
3132
#include <TiledArray/expressions/permopt.h>
3233
#include <TiledArray/pmap/slabbed_pmap.h>
@@ -36,6 +37,7 @@
3637
#include <TiledArray/tile_op/batched_contract_reduce.h>
3738
#include <TiledArray/tile_op/contract_reduce.h>
3839
#include <TiledArray/tile_op/mult.h>
40+
#include <TiledArray/tile_op/noop.h>
3941

4042
namespace TiledArray {
4143
namespace expressions {
@@ -179,6 +181,10 @@ class ContEngine : public BinaryEngine<Derived> {
179181
// General (fused + contracted + free indices) products only:
180182
unsigned int n_fused_modes_ = 0; ///< # of leading fused (outer) modes
181183
size_type n_slabs_ = 1; ///< # of fused-index tile slabs
184+
bool general_repermute_ = false; ///< whether the target layout differs
185+
///< from the canonical result layout, so
186+
///< the evaluated result is re-permuted
187+
///< by a streaming unary eval
182188

183189
static unsigned int find(const BipartiteIndexList& indices,
184190
const std::string& index_label, unsigned int i,
@@ -660,16 +666,13 @@ class ContEngine : public BinaryEngine<Derived> {
660666
TA_ASSERT(nh > 0u); // else this is a pure contraction
661667
n_fused_modes_ = nh;
662668

663-
// initialize perm_; an interleaved target (a result permutation that
664-
// mixes fused and free modes) is not yet supported -- the canonical
665-
// result layout must equal the target
669+
// initialize perm_; a target that differs from the canonical (fused...,
670+
// left-free..., right-free...) result layout cannot be folded into the
671+
// batched tile op (BatchedContractReduce must be perm-free), so the
672+
// product is evaluated in its canonical layout and re-permuted to the
673+
// target by a streaming unary eval (see make_dist_eval_general)
666674
this->init_perm(target_indices);
667-
if (outer(target_indices) != outer(indices_))
668-
TA_EXCEPTION(
669-
"general products (fused + contracted + free indices): targets "
670-
"that interleave fused and free indices are not yet supported; "
671-
"reorder the result annotation to (fused..., left-free..., "
672-
"right-free...)");
675+
general_repermute_ = (outer(target_indices) != outer(indices_));
673676

674677
// the tile op operates on the folded (fused-mode-free) shapes
675678
const auto left_op = to_cblas_op(left_outer_permtype_);
@@ -712,6 +715,12 @@ class ContEngine : public BinaryEngine<Derived> {
712715

713716
trange_ = make_trange_general();
714717
shape_ = make_shape_general();
718+
if (general_repermute_) {
719+
// consumers see the target layout; the canonical structures are
720+
// recomputed in make_dist_eval_general for the inner Summa
721+
trange_ = outer(perm_) * trange_;
722+
shape_ = shape_.perm(outer(perm_));
723+
}
715724

716725
if (ExprEngine_::override_ptr_ && ExprEngine_::override_ptr_->shape) {
717726
shape_ = shape_.mask(*ExprEngine_::override_ptr_->shape);
@@ -834,9 +843,37 @@ class ContEngine : public BinaryEngine<Derived> {
834843
}
835844
}
836845

846+
/// Streaming tile re-permute op for general products whose target layout
847+
/// differs from the canonical (fused..., free...) result layout: the
848+
/// batched tile op must stay perm-free, so the consumer-side unary eval
849+
/// applies the result permutation per tile instead
850+
struct GeneralRepermuteOp {
851+
typedef value_type result_type;
852+
typedef value_type argument_type;
853+
static constexpr bool is_consumable = false;
854+
/// Only the *outer* (result-layout) permutation is applied here; inner
855+
/// (within-cell) permutation of tensor-of-tensor results is handled
856+
/// separately (see init_struct_general / implicit_permute_inner_), so this
857+
/// op stores a plain outer Permutation to avoid accidentally permuting
858+
/// inner contents.
859+
Permutation perm;
860+
/// false when the consumer fuses the permutation into its own operation
861+
/// (implicit permute, e.g. a transposed GEMM): then only the tile
862+
/// ordinals/trange are remapped (by the host UnaryEvalImpl) and the tile
863+
/// contents are delivered in the canonical layout
864+
bool permute_contents = true;
865+
result_type operator()(const argument_type& tile) const {
866+
if (!permute_contents) return tile;
867+
TiledArray::detail::Noop<value_type, value_type, false> noop;
868+
return noop(tile, perm);
869+
}
870+
};
871+
837872
/// Construct the distributed evaluator of a general product
838873

839-
/// \return The batched-Summa distributed evaluator for this expression
874+
/// \return The batched-Summa distributed evaluator for this expression,
875+
/// wrapped in a streaming re-permute when the target layout differs from
876+
/// the canonical result layout
840877
dist_eval_type make_dist_eval_general() const {
841878
typedef TiledArray::detail::BatchedContractReduce<op_type> batched_op_type;
842879
typedef TiledArray::detail::Summa<typename left_type::dist_eval_type,
@@ -847,11 +884,46 @@ class ContEngine : public BinaryEngine<Derived> {
847884
typename left_type::dist_eval_type left = left_.make_dist_eval();
848885
typename right_type::dist_eval_type right = right_.make_dist_eval();
849886

850-
std::shared_ptr<impl_type> pimpl = std::make_shared<impl_type>(
851-
left, right, *world_, trange_, shape_, pmap_, perm_,
852-
batched_op_type(op_, n_fused_modes_), K_, proc_grid_, n_slabs_);
887+
if (!general_repermute_) {
888+
std::shared_ptr<impl_type> pimpl = std::make_shared<impl_type>(
889+
left, right, *world_, trange_, shape_, pmap_, perm_,
890+
batched_op_type(op_, n_fused_modes_), K_, proc_grid_, n_slabs_);
891+
return dist_eval_type(pimpl);
892+
}
853893

854-
return dist_eval_type(pimpl);
894+
// evaluate in the canonical layout (Summa with perm-free op), then
895+
// re-permute tiles to the target layout with a streaming unary eval;
896+
// trange_/shape_ hold the target-layout structures (see
897+
// init_struct_general), the canonical ones are recomputed here
898+
auto const canonical_trange = make_trange_general();
899+
auto const canonical_shape = [this]() {
900+
auto s = make_shape_general();
901+
if (ExprEngine_::override_ptr_ && ExprEngine_::override_ptr_->shape) {
902+
// the consumer-supplied mask is expressed in the target layout
903+
auto const inv_perm = outer(perm_).inv();
904+
s = s.mask(ExprEngine_::override_ptr_->shape->perm(inv_perm));
905+
}
906+
return s;
907+
}();
908+
// the inner Summa's result placement must be slab-replicated (the owner
909+
// of a tile independent of its slab index), regardless of the
910+
// (target-layout) pmap the consumer supplied for this node
911+
auto canonical_pmap = std::make_shared<TiledArray::detail::SlabbedPmap>(
912+
*world_, proc_grid_.make_pmap(), n_slabs_);
913+
std::shared_ptr<impl_type> pimpl = std::make_shared<impl_type>(
914+
left, right, *world_, canonical_trange, canonical_shape, canonical_pmap,
915+
BipartitePermutation{}, batched_op_type(op_, n_fused_modes_), K_,
916+
proc_grid_, n_slabs_);
917+
dist_eval_type canonical(pimpl);
918+
919+
typedef TiledArray::detail::UnaryEvalImpl<
920+
dist_eval_type, GeneralRepermuteOp, typename Derived::policy>
921+
repermute_impl_type;
922+
std::shared_ptr<repermute_impl_type> wrapper =
923+
std::make_shared<repermute_impl_type>(
924+
canonical, *world_, trange_, shape_, pmap_, perm_,
925+
GeneralRepermuteOp{outer(perm_), !this->implicit_permute_outer_});
926+
return dist_eval_type(wrapper);
855927
}
856928

857929
/// Expression identification tag

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: 91 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -281,44 +281,65 @@ 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+
// Materialize the demands as named lvalues: some preferred_layout()
333+
// overloads (leaf/binary/unary) return a reference to their argument, so
334+
// binding that to a temporary demand would be needlessly fragile.
335+
const BipartiteIndexList left_demand =
336+
bipartite_demand(avail_l, avail_r, target_indices);
337+
const BipartiteIndexList right_demand =
338+
bipartite_demand(avail_r, avail_l, target_indices);
339+
BinaryEngine_::left_.init_indices(
340+
BinaryEngine_::left_.preferred_layout(left_demand));
341+
BinaryEngine_::right_.init_indices(
342+
BinaryEngine_::right_.preferred_layout(right_demand));
322343
}
323344

324345
this->product_type_ = compute_product_type(
@@ -377,6 +398,40 @@ class MultEngine : public ContEngine<MultEngine<Left, Right, Result>> {
377398
}
378399
}
379400

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

382437
/// 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)