Skip to content

Commit 9602d4f

Browse files
committed
einsum: delegate nesting-preserving products to the expression layer
Two general-product shapes that einsum handled with bespoke machinery are nesting-preserving (the result keeps the operand nesting), so the general- product expression engine can own them; only the type-mutating ToT*ToT->T denest reduction remains einsum's by default. * Fused broadcast C(h,e) = A(h) * B(h,e) (one operand entirely fused, no contraction): replaces the replicate_array() expand-then-Hadamard with a direct delegation. The engine evaluates it natively once the fully-fused operand leads -- its folded left tile is rank-0, restored by a synthetic unit left-external mode. ContEngine::synthetic_unit_left_external() now fires for a rank-0 LEFT operand (broadcast), not only a rank-0 RESULT (no-external). einsum orders the fully-fused operand first and applies any inner result permutation afterward. * No-external products C(h;..) = A(h,i;..) * B(h,i;..) (fused + contracted, no free index) now default to the expression layer (the existing no-external synthetic-unit mode). The legacy local kernel is retained only behind einsum_legacy_subworld() as the cross-check oracle, mirroring the generalized-subworld path. Tests: dense fused broadcast (both operand orders), ToT no-external with inner Hadamard and inner contraction.
1 parent 355f9c8 commit 9602d4f

3 files changed

Lines changed: 157 additions & 41 deletions

File tree

src/TiledArray/einsum/tiledarray.h

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -673,23 +673,38 @@ auto einsum(expressions::TsrExpr<ArrayA_> A, expressions::TsrExpr<ArrayB_> B,
673673
auto range_map =
674674
(RangeMap(a, A.array().trange()) | RangeMap(b, B.array().trange()));
675675

676-
// special Hadamard
676+
// Fused broadcast: one operand is ENTIRELY fused (h == its index set) and
677+
// there is no contraction -- C(h,e) = A * B, a per-fused-block scale. The
678+
// general-product expression engine evaluates this natively when the
679+
// fully-fused operand LEADS (it folds to a rank-0 left tile, restored by a
680+
// synthetic unit left-external mode; see ContEngine::
681+
// synthetic_unit_left_external). Order the fully-fused operand first and
682+
// delegate -- no physical replication needed.
677683
if (h.size() == a.size() || h.size() == b.size()) {
678684
TA_ASSERT(!i && e);
679-
bool const small_a = h.size() == a.size();
680-
auto const delta_trng = make_trange(range_map, e);
685+
_ein_call.branch = "fused-broadcast-expression";
686+
const bool a_leads = (h.size() == a.size()); // A entirely fused?
687+
// the engine produces the result in its canonical inner layout
688+
// (inner fused, then left-then-right inner externals, in the order the
689+
// operands are handed to it) and rejects a non-identity inner result
690+
// permutation; evaluate canonically, then apply the inner permutation
691+
// (cf. the generalized-inner-perm-recurse path below). The canonical
692+
// inner-external order tracks the operand order, so mirror the swap.
693+
std::string canon_inner;
694+
if constexpr (IsArrayToT<ArrayC>) {
695+
auto inner_e = a_leads ? (inner.A ^ inner.B) : (inner.B ^ inner.A);
696+
canon_inner = ";" + (std::string)(inner.h + inner_e);
697+
}
698+
std::string canon_layout = std::string(c) + canon_inner;
699+
ArrayC C0;
700+
if (a_leads) // A fully fused -> already leads
701+
C0(canon_layout) = A * B;
702+
else // B fully fused -> put it first (multiplication commutes)
703+
C0(canon_layout) = B * A;
681704
std::string target_layout = std::string(c) + inner.c;
705+
if (target_layout == canon_layout) return C0;
682706
ArrayC C;
683-
if (small_a) {
684-
auto temp = replicate_array(A.array(), delta_trng);
685-
std::string temp_layout = std::string(e) + "," + A.annotation();
686-
C(target_layout) = temp(temp_layout) * B;
687-
} else {
688-
auto temp = replicate_array(B.array(), delta_trng);
689-
std::string temp_layout = std::string(e) + "," + B.annotation();
690-
C(target_layout) = A * temp(temp_layout);
691-
}
692-
707+
C(target_layout) = C0(canon_layout); // inner-permute to requested layout
693708
return C;
694709
}
695710

@@ -791,7 +806,28 @@ auto einsum(expressions::TsrExpr<ArrayA_> A, expressions::TsrExpr<ArrayB_> B,
791806
}
792807
}
793808

794-
if (!e) { // hadamard reduction
809+
if (!e) { // no external outer index (fused + contracted): a nesting-
810+
// preserving reduction. By default the expression layer handles
811+
// it natively (synthetic unit left-external mode, see
812+
// ContEngine::synthetic_unit_left_external); the legacy local
813+
// kernel below is retained only as the opt-in legacy
814+
// cross-check oracle (cf. the generalized-subworld path).
815+
if (!detail::einsum_legacy_subworld()) {
816+
_ein_call.branch = "no-external-expression";
817+
// evaluate in the engine's canonical inner layout, then apply any
818+
// inner result permutation (cf. the broadcast / inner-perm-recurse)
819+
std::string canon_inner;
820+
if constexpr (IsArrayToT<ArrayC>)
821+
canon_inner = ";" + (std::string)(inner.h + inner.e);
822+
std::string canon_layout = std::string(c) + canon_inner;
823+
ArrayC C0;
824+
C0(canon_layout) = A * B;
825+
std::string target_layout = std::string(c) + inner.c;
826+
if (target_layout == canon_layout) return C0;
827+
ArrayC C;
828+
C(target_layout) = C0(canon_layout);
829+
return C;
830+
}
795831

796832
_ein_call.branch = "hadamard-reduction-local";
797833
const auto _ein_he_t0 = _ein_call.active ? now() : time_point{};

src/TiledArray/expressions/cont_engine.h

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -677,18 +677,20 @@ class ContEngine : public BinaryEngine<Derived> {
677677
this->init_perm(target_indices);
678678
general_repermute_ = (outer(target_indices) != outer(indices_));
679679

680-
// A product with NO external (free) outer indices (every outer index
681-
// fused or contracted, e.g. C("i,j;a,b") = A("x,i,j;a") * B("x,i,j;b"))
682-
// folds to a GEMM with no free modes, i.e. rank-0 tensors, which the
683-
// tile kernels do not support. Evaluate it with a SYNTHETIC UNIT
684-
// left-external mode instead: the folded product becomes
685-
// (1,K) x (K) -> (1), the exact shape of the (supported) one-sided
686-
// neB == 0 case. The unit mode lives only in the tile op's GemmHelper;
687-
// tranges, shapes and tiles carry the true (external-free) ranks, and
688-
// BatchedContractReduce / SparseShape::gemm_batched detect the
689-
// synthetic mode from the one-rank mismatch and pad their folded views
680+
// Some degenerate folded shapes would carry a rank-0 tensor, which the
681+
// tile kernels do not support (see synthetic_unit_left_external()):
682+
// - a NO-EXTERNAL product (every outer index fused or contracted, e.g.
683+
// C("i,j;a,b") = A("x,i,j;a") * B("x,i,j;b")) folds to a rank-0 RESULT;
684+
// - a FUSED BROADCAST (the left operand is entirely fused, no contraction,
685+
// e.g. C("b,k") = A("b") * B("b,k")) folds to a rank-0 LEFT operand.
686+
// Evaluate both with a SYNTHETIC UNIT left-external mode: the folded
687+
// product becomes (1,K) x (K,N) -> (1,N), a supported shape (the no-
688+
// external case has N == 1, the broadcast has K == 1). The unit mode lives
689+
// only in the tile op's GemmHelper; tranges, shapes and tiles carry the
690+
// true ranks, and BatchedContractReduce / SparseShape::gemm_batched detect
691+
// the synthetic mode from the one-rank mismatch and pad their folded views
690692
// with a unit extent.
691-
const unsigned int u = (outer_size(indices_) == nh) ? 1u : 0u;
693+
const unsigned int u = synthetic_unit_left_external();
692694

693695
// the tile op operates on the folded (fused-mode-free) shapes; the
694696
// synthetic unit mode leads the folded left operand, so it is NoTrans
@@ -749,17 +751,34 @@ class ContEngine : public BinaryEngine<Derived> {
749751
}
750752
}
751753

754+
/// \return 1 if the folded general product needs a SYNTHETIC unit
755+
/// left-external mode, else 0. The folded (fused-mode-free) GEMM cannot host
756+
/// a rank-0 tensor, which arises in two degenerate cases:
757+
/// - rank-0 RESULT: every outer index is fused or contracted (no external),
758+
/// i.e. outer_size(indices_) == n_fused_modes_;
759+
/// - rank-0 LEFT operand: the left argument is entirely fused with no
760+
/// contraction (a fused broadcast / per-fused-block scale), i.e.
761+
/// outer_size(left_indices_) == n_fused_modes_.
762+
/// A unit left-external mode (carried only in the GemmHelper) restores a
763+
/// supported (1,K) x (K,N) -> (1,N) shape in both cases.
764+
unsigned int synthetic_unit_left_external() const {
765+
return (outer_size(indices_) == n_fused_modes_ ||
766+
outer_size(left_indices_) == n_fused_modes_)
767+
? 1u
768+
: 0u;
769+
}
770+
752771
/// Tiled range factory function for a general product
753772

754773
/// \return The result tiled range: the fused mode ranges followed by the
755774
/// left- and right-external mode ranges
756775
trange_type make_trange_general() const {
757776
const unsigned int nh = n_fused_modes_;
758777
const unsigned int nc = op_.gemm_helper().num_contract_ranks();
759-
// the no-external case carries a synthetic unit left-external mode in
760-
// the GemmHelper only (see init_struct_general); the actual tranges do
761-
// not have it
762-
const unsigned int u = (outer_size(indices_) == n_fused_modes_) ? 1u : 0u;
778+
// degenerate folds carry a synthetic unit left-external mode in the
779+
// GemmHelper only (see synthetic_unit_left_external()); the actual tranges
780+
// do not have it
781+
const unsigned int u = synthetic_unit_left_external();
763782
const unsigned int neA = op_.gemm_helper().left_rank() - nc - u;
764783
const unsigned int neB = op_.gemm_helper().right_rank() - nc;
765784

@@ -812,10 +831,10 @@ class ContEngine : public BinaryEngine<Derived> {
812831
std::shared_ptr<const pmap_interface> pmap) {
813832
const unsigned int nh = n_fused_modes_;
814833
const unsigned int nc = op_.gemm_helper().num_contract_ranks();
815-
// the no-external case carries a synthetic unit left-external mode in
816-
// the GemmHelper only (see init_struct_general); the actual tranges do
817-
// not have it
818-
const unsigned int u = (outer_size(indices_) == nh) ? 1u : 0u;
834+
// degenerate folds carry a synthetic unit left-external mode in the
835+
// GemmHelper only (see synthetic_unit_left_external()); the actual tranges
836+
// do not have it
837+
const unsigned int u = synthetic_unit_left_external();
819838
const unsigned int neA = op_.gemm_helper().left_rank() - nc - u;
820839
const unsigned int neB = op_.gemm_helper().right_rank() - nc;
821840

tests/general_product.cpp

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,37 @@ BOOST_AUTO_TEST_CASE(expression_general_product_dense_batched_outer) {
237237
BOOST_CHECK_SMALL(diff_norm(c, c_ref, "b,i,k"), 1e-10);
238238
}
239239

240+
BOOST_AUTO_TEST_CASE(expression_general_product_dense_broadcast) {
241+
// fused broadcast: one operand is ENTIRELY fused, no contraction, the other
242+
// carries an extra external -- C(b,k) = A(b) * B(b,k). Per fused block b this
243+
// is scalar * vector (a scale). The general-product engine handles it via a
244+
// synthetic unit left-external mode; einsum no longer needs replicate_array.
245+
auto& world = TA::get_default_world();
246+
247+
TA::TiledRange tr_s{{0, 2, 5}}; // b
248+
TA::TiledRange tr_v{{0, 2, 5}, {0, 4, 5}}; // b, k
249+
auto s = make_patterned_array(world, tr_s, 1.0);
250+
auto v = make_patterned_array(world, tr_v, 2.0);
251+
252+
// independent oracle: physically replicate s over k (NOT through the
253+
// general-product engine), then a plain Hadamard multiply
254+
TA::TiledRange tr_k{{0, 4, 5}}; // k
255+
auto s_rep = TA::Einsum::replicate_array(s, tr_k); // s_rep(k, b)
256+
TA::TArrayD ref;
257+
ref("b,k") = s_rep("k,b") * v("b,k");
258+
259+
// (a) LEFT operand fully fused, straight through the expression layer
260+
TA::TArrayD cc;
261+
BOOST_REQUIRE_NO_THROW(cc("b,k") = s("b") * v("b,k"));
262+
BOOST_CHECK_SMALL(diff_norm(cc, ref, "b,k"), 1e-10);
263+
264+
// (b) einsum, both operand orders (RIGHT operand fully fused too)
265+
auto c_lr = TA::einsum(s("b"), v("b,k"), "b,k");
266+
BOOST_CHECK_SMALL(diff_norm(c_lr, ref, "b,k"), 1e-10);
267+
auto c_rl = TA::einsum(v("b,k"), s("b"), "b,k");
268+
BOOST_CHECK_SMALL(diff_norm(c_rl, ref, "b,k"), 1e-10);
269+
}
270+
240271
BOOST_AUTO_TEST_CASE(expression_general_product_noncanonical_root_target) {
241272
// a root-level general product with a NON-canonical target layout: the
242273
// product evaluates canonically (r1,p,q) and is re-permuted to the target
@@ -778,6 +809,42 @@ BOOST_AUTO_TEST_CASE(expression_general_product_no_externals) {
778809
BOOST_CHECK_SMALL(diff_norm(c, c_ref, "i"), 1e-10);
779810
}
780811

812+
BOOST_AUTO_TEST_CASE(
813+
expression_general_product_tot_no_externals_inner_hadamard) {
814+
// ToT no-external product with an INNER HADAMARD: outer i,j fused, x
815+
// contracted, no free outer index; inner m fused. This is the einsum
816+
// "hadamard-reduction-local" shape with an inner-Hadamard cell op -- verify
817+
// the expression layer (synthetic unit left-external + inner Hadamard)
818+
// reproduces it.
819+
auto& world = TA::get_default_world();
820+
ForceLegacyEinsum legacy_oracle; // keep the einsum reference independent
821+
TA::TiledRange tr{{0, 3, 5}, {0, 2, 4}, {0, 2, 3}}; // x, i, j
822+
auto a = make_patterned_tot_array(world, tr, {2}, 1.0);
823+
auto b = make_patterned_tot_array(world, tr, {2}, 2.0);
824+
825+
TArrayToT c;
826+
BOOST_REQUIRE_NO_THROW(c("i,j;m") = a("x,i,j;m") * b("x,i,j;m"));
827+
auto c_ref = TA::einsum(a("x,i,j;m"), b("x,i,j;m"), "i,j;m");
828+
BOOST_CHECK_SMALL(tot_max_abs_diff(c, c_ref), 1e-10);
829+
}
830+
831+
BOOST_AUTO_TEST_CASE(
832+
expression_general_product_tot_no_externals_inner_contraction) {
833+
// ToT no-external product with an INNER CONTRACTION: outer i,j fused, x
834+
// contracted, no free outer index; inner c contracted, m/n free. The einsum
835+
// "hadamard-reduction-local" shape with an inner-contraction cell op.
836+
auto& world = TA::get_default_world();
837+
ForceLegacyEinsum legacy_oracle; // keep the einsum reference independent
838+
TA::TiledRange tr{{0, 3, 5}, {0, 2, 4}, {0, 2, 3}}; // x, i, j
839+
auto a = make_patterned_tot_array(world, tr, {2, 3}, 1.0); // inner m, c
840+
auto b = make_patterned_tot_array(world, tr, {3, 2}, 2.0); // inner c, n
841+
842+
TArrayToT c;
843+
BOOST_REQUIRE_NO_THROW(c("i,j;m,n") = a("x,i,j;m,c") * b("x,i,j;c,n"));
844+
auto c_ref = TA::einsum(a("x,i,j;m,c"), b("x,i,j;c,n"), "i,j;m,n");
845+
BOOST_CHECK_SMALL(tot_max_abs_diff(c, c_ref), 1e-10);
846+
}
847+
781848
BOOST_AUTO_TEST_CASE(expression_general_product_sparse_no_externals) {
782849
// block-sparse no-external general product: exercises the synthetic
783850
// unit-mode handling in SparseShape::gemm_batched
@@ -1241,13 +1308,8 @@ BOOST_AUTO_TEST_CASE(expression_general_product_csv_like) {
12411308
ArenaSpArr c1, c2;
12421309
{
12431310
ScopedEinsumRoute expression_route(false);
1244-
try {
1245-
c1 = TA::einsum(a("i2,i1,m;a"), b("m,i2,K"), "i1,i2,K;a");
1246-
c2 = TA::einsum(a("i2,i1,m;a"), b("m,i2,K"), "i1,i2,K;a");
1247-
} catch (std::exception& ex) {
1248-
std::cerr << "EXPRESSION ROUTE THREW: " << ex.what() << std::endl;
1249-
throw;
1250-
}
1311+
c1 = TA::einsum(a("i2,i1,m;a"), b("m,i2,K"), "i1,i2,K;a");
1312+
c2 = TA::einsum(a("i2,i1,m;a"), b("m,i2,K"), "i1,i2,K;a");
12511313
}
12521314

12531315
// BISECT: same shape, CANONICAL target, direct expression (no einsum
@@ -1278,7 +1340,6 @@ BOOST_AUTO_TEST_CASE(expression_general_product_csv_like) {
12781340
}
12791341
}
12801342
}
1281-
std::cerr << "CANONICAL-TARGET DIRECT-EXPR max_diff = " << md << std::endl;
12821343
BOOST_CHECK_SMALL(md, 1e-10);
12831344
}
12841345

0 commit comments

Comments
 (0)