Skip to content

Commit 3f00dae

Browse files
committed
expressions: native no-external general products via a synthetic unit left-external mode
A general product whose every outer index is fused or contracted (e.g. C("i,j;a,b") = A("x,i,j;a") * B("x,i,j;b")) folds to a GEMM with no free modes, i.e. rank-0 tensors, which the tile kernels do not support (this shape used to segfault through wild stride reads). Evaluate it with a synthetic unit left-external mode instead: the folded product becomes (1,K) x (K) -> (1), the exact shape of the already-supported one-sided neB == 0 case. The unit mode lives only in the tile op's GemmHelper; tranges, shapes and tiles carry the true (external-free) ranks, and BatchedContractReduce / SparseShape::gemm_batched detect the synthetic mode from the one-rank mismatch and pad their folded views with a unit extent. Replaces the interim gate. Tests: dense ToT (incl. the no-external root fed by a general T x ToT inner node), plain dense (the Hadamard-reduction shape), and block-sparse (exercising the gemm_batched unit handling), all differential-tested against legacy einsum.
1 parent 0abc2dc commit 3f00dae

4 files changed

Lines changed: 144 additions & 59 deletions

File tree

src/TiledArray/expressions/cont_engine.h

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -666,19 +666,6 @@ class ContEngine : public BinaryEngine<Derived> {
666666
TA_ASSERT(nh > 0u); // else this is a pure contraction
667667
n_fused_modes_ = nh;
668668

669-
// a general product of tensors-of-tensors with NO external (free) outer
670-
// indices (every outer index fused or contracted, e.g.
671-
// C("i,j;a,b") = A("x,i,j;a") * B("x,i,j;b")) is not supported by the
672-
// batched tile op yet (its folded GEMM has no free modes);
673-
// einsum() evaluates this shape natively
674-
if constexpr (TiledArray::detail::is_tensor_of_tensor_v<value_type>) {
675-
if (outer_size(indices_) == nh)
676-
TA_EXCEPTION(
677-
"general products of tensors-of-tensors without external (free) "
678-
"outer indices are not yet supported in the expression layer; "
679-
"use TiledArray::einsum() for this contraction");
680-
}
681-
682669
// initialize perm_; a target that differs from the canonical (fused...,
683670
// left-free..., right-free...) result layout cannot be folded into the
684671
// batched tile op (BatchedContractReduce must be perm-free), so the
@@ -687,12 +674,27 @@ class ContEngine : public BinaryEngine<Derived> {
687674
this->init_perm(target_indices);
688675
general_repermute_ = (outer(target_indices) != outer(indices_));
689676

690-
// the tile op operates on the folded (fused-mode-free) shapes
691-
const auto left_op = to_cblas_op(left_outer_permtype_);
677+
// A product with NO external (free) outer indices (every outer index
678+
// fused or contracted, e.g. C("i,j;a,b") = A("x,i,j;a") * B("x,i,j;b"))
679+
// folds to a GEMM with no free modes, i.e. rank-0 tensors, which the
680+
// tile kernels do not support. Evaluate it with a SYNTHETIC UNIT
681+
// left-external mode instead: the folded product becomes
682+
// (1,K) x (K) -> (1), the exact shape of the (supported) one-sided
683+
// neB == 0 case. The unit mode lives only in the tile op's GemmHelper;
684+
// tranges, shapes and tiles carry the true (external-free) ranks, and
685+
// BatchedContractReduce / SparseShape::gemm_batched detect the
686+
// synthetic mode from the one-rank mismatch and pad their folded views
687+
// with a unit extent.
688+
const unsigned int u = (outer_size(indices_) == nh) ? 1u : 0u;
689+
690+
// the tile op operates on the folded (fused-mode-free) shapes; the
691+
// synthetic unit mode leads the folded left operand, so it is NoTrans
692+
const auto left_op =
693+
u ? math::blas::NoTranspose : to_cblas_op(left_outer_permtype_);
692694
const auto right_op = to_cblas_op(right_outer_permtype_);
693695
if constexpr (!TiledArray::detail::is_tensor_of_tensor_v<value_type>) {
694-
op_ = op_type(left_op, right_op, factor_, outer_size(indices_) - nh,
695-
outer_size(left_indices_) - nh,
696+
op_ = op_type(left_op, right_op, factor_, outer_size(indices_) - nh + u,
697+
outer_size(left_indices_) - nh + u,
696698
outer_size(right_indices_) - nh);
697699
} else {
698700
// the batched tile op must be perm-free (BatchedContractReduce cannot
@@ -710,7 +712,8 @@ class ContEngine : public BinaryEngine<Derived> {
710712

711713
// factor_ is absorbed into element_nonreturn_op_
712714
op_ = op_type(left_op, right_op, scalar_type(1),
713-
outer_size(indices_) - nh, outer_size(left_indices_) - nh,
715+
outer_size(indices_) - nh + u,
716+
outer_size(left_indices_) - nh + u,
714717
outer_size(right_indices_) - nh, BipartitePermutation{},
715718
this->element_nonreturn_op_, std::move(this->arena_plan_));
716719
// ce+e, ce+ce_right and ce+ce_left are mutually exclusive; at most one
@@ -750,7 +753,11 @@ class ContEngine : public BinaryEngine<Derived> {
750753
trange_type make_trange_general() const {
751754
const unsigned int nh = n_fused_modes_;
752755
const unsigned int nc = op_.gemm_helper().num_contract_ranks();
753-
const unsigned int neA = op_.gemm_helper().left_rank() - nc;
756+
// the no-external case carries a synthetic unit left-external mode in
757+
// the GemmHelper only (see init_struct_general); the actual tranges do
758+
// not have it
759+
const unsigned int u = (outer_size(indices_) == n_fused_modes_) ? 1u : 0u;
760+
const unsigned int neA = op_.gemm_helper().left_rank() - nc - u;
754761
const unsigned int neB = op_.gemm_helper().right_rank() - nc;
755762

756763
typename trange_type::Ranges ranges(nh + neA + neB);
@@ -802,7 +809,11 @@ class ContEngine : public BinaryEngine<Derived> {
802809
std::shared_ptr<const pmap_interface> pmap) {
803810
const unsigned int nh = n_fused_modes_;
804811
const unsigned int nc = op_.gemm_helper().num_contract_ranks();
805-
const unsigned int neA = op_.gemm_helper().left_rank() - nc;
812+
// the no-external case carries a synthetic unit left-external mode in
813+
// the GemmHelper only (see init_struct_general); the actual tranges do
814+
// not have it
815+
const unsigned int u = (outer_size(indices_) == nh) ? 1u : 0u;
816+
const unsigned int neA = op_.gemm_helper().left_rank() - nc - u;
806817
const unsigned int neB = op_.gemm_helper().right_rank() - nc;
807818

808819
// Get pointers to the argument sizes

src/TiledArray/sparse_shape.h

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1720,42 +1720,54 @@ class SparseShape {
17201720
const auto* left_extent = tile_norms_.range().extent_data();
17211721
const auto* right_extent = other.tile_norms_.range().extent_data();
17221722

1723+
// a no-external product carries a SYNTHETIC unit left-external mode in
1724+
// the GemmHelper only (see ContEngine::init_struct_general); detect it
1725+
// from the one-rank mismatch with the actual norm tensor and pad the
1726+
// folded left/result views with a unit extent
1727+
const bool unit_external =
1728+
(tile_norms_.range().rank() + 1u == nfused + gemm_helper.left_rank());
1729+
const unsigned int u = unit_external ? 1u : 0u;
1730+
17231731
// check that the ranks match the folded gemm ranks plus the fused modes,
17241732
// and that the fused and contracted mode extents of the two shapes are
17251733
// congruent
1726-
TA_ASSERT(tile_norms_.range().rank() == nfused + gemm_helper.left_rank());
1734+
TA_ASSERT(tile_norms_.range().rank() + u ==
1735+
nfused + gemm_helper.left_rank());
17271736
TA_ASSERT(other.tile_norms_.range().rank() ==
17281737
nfused + gemm_helper.right_rank());
17291738
for (unsigned int d = 0u; d < nfused; ++d)
17301739
TA_ASSERT(left_extent[d] == right_extent[d]);
17311740
for (unsigned int i = gemm_helper.left_inner_begin(),
17321741
j = gemm_helper.right_inner_begin();
17331742
i < gemm_helper.left_inner_end(); ++i, ++j)
1734-
TA_ASSERT(left_extent[nfused + i] == right_extent[nfused + j]);
1743+
TA_ASSERT(left_extent[nfused + i - u] == right_extent[nfused + j]);
17351744

17361745
integer H = 1, M = 1, N = 1, K = 1;
17371746
for (unsigned int d = 0u; d < nfused; ++d) H *= left_extent[d];
1738-
for (unsigned int i = gemm_helper.left_outer_begin();
1739-
i < gemm_helper.left_outer_end(); ++i)
1740-
M *= left_extent[nfused + i];
1747+
if (!unit_external)
1748+
for (unsigned int i = gemm_helper.left_outer_begin();
1749+
i < gemm_helper.left_outer_end(); ++i)
1750+
M *= left_extent[nfused + i];
17411751
for (unsigned int i = gemm_helper.left_inner_begin();
17421752
i < gemm_helper.left_inner_end(); ++i)
1743-
K *= left_extent[nfused + i];
1753+
K *= left_extent[nfused + i - u];
17441754
for (unsigned int i = gemm_helper.right_outer_begin();
17451755
i < gemm_helper.right_outer_end(); ++i)
17461756
N *= right_extent[nfused + i];
17471757

17481758
// result size vectors: fused modes (from this), then the left and right
1749-
// outer modes
1750-
const unsigned int result_rank = nfused + gemm_helper.result_rank();
1759+
// outer modes (the synthetic unit left-external mode is absent from the
1760+
// actual result)
1761+
const unsigned int result_rank = nfused + gemm_helper.result_rank() - u;
17511762
std::shared_ptr<vector_type> result_size_vectors(
17521763
new vector_type[result_rank], std::default_delete<vector_type[]>());
17531764
unsigned int x = 0ul;
17541765
for (unsigned int i = 0u; i < nfused; ++i, ++x)
17551766
result_size_vectors.get()[x] = size_vectors_.get()[i];
1756-
for (unsigned int i = gemm_helper.left_outer_begin();
1757-
i < gemm_helper.left_outer_end(); ++i, ++x)
1758-
result_size_vectors.get()[x] = size_vectors_.get()[nfused + i];
1767+
if (!unit_external)
1768+
for (unsigned int i = gemm_helper.left_outer_begin();
1769+
i < gemm_helper.left_outer_end(); ++i, ++x)
1770+
result_size_vectors.get()[x] = size_vectors_.get()[nfused + i];
17591771
for (unsigned int i = gemm_helper.right_outer_begin();
17601772
i < gemm_helper.right_outer_end(); ++i, ++x)
17611773
result_size_vectors.get()[x] = other.size_vectors_.get()[nfused + i];
@@ -1770,11 +1782,12 @@ class SparseShape {
17701782
lobounds.push_back(tile_norms_.range().lobound_data()[d]);
17711783
upbounds.push_back(tile_norms_.range().upbound_data()[d]);
17721784
}
1773-
for (unsigned int i = gemm_helper.left_outer_begin();
1774-
i < gemm_helper.left_outer_end(); ++i) {
1775-
lobounds.push_back(tile_norms_.range().lobound_data()[nfused + i]);
1776-
upbounds.push_back(tile_norms_.range().upbound_data()[nfused + i]);
1777-
}
1785+
if (!unit_external)
1786+
for (unsigned int i = gemm_helper.left_outer_begin();
1787+
i < gemm_helper.left_outer_end(); ++i) {
1788+
lobounds.push_back(tile_norms_.range().lobound_data()[nfused + i]);
1789+
upbounds.push_back(tile_norms_.range().upbound_data()[nfused + i]);
1790+
}
17781791
for (unsigned int i = gemm_helper.right_outer_begin();
17791792
i < gemm_helper.right_outer_end(); ++i) {
17801793
lobounds.push_back(other.tile_norms_.range().lobound_data()[nfused + i]);
@@ -1784,10 +1797,13 @@ class SparseShape {
17841797

17851798
// the range spanned by modes [nfused, rank) of \p r, rebased to zero
17861799
// lobounds (scratch view for the slab-batched norm GEMM)
1787-
auto fold_range = [nfused](const range_type& r) {
1800+
auto fold_range = [nfused](const range_type& r,
1801+
const bool prepend_unit = false) {
17881802
const auto* extent = r.extent_data();
1789-
container::svector<index1_type> extents(extent + nfused,
1790-
extent + r.rank());
1803+
container::svector<index1_type> extents;
1804+
extents.reserve(r.rank() - nfused + (prepend_unit ? 1u : 0u));
1805+
if (prepend_unit) extents.push_back(1);
1806+
extents.insert(extents.end(), extent + nfused, extent + r.rank());
17911807
return range_type(extents);
17921808
};
17931809

@@ -1797,10 +1813,11 @@ class SparseShape {
17971813

17981814
if (k_rank > 0u) {
17991815
// the contracted-mode size vector; identical for every slab since the
1800-
// contracted modes follow the fused modes
1816+
// contracted modes follow the fused modes (helper coordinates carry
1817+
// the synthetic unit mode, the actual size vectors do not)
18011818
const vector_type k_sizes = recursive_outer_product(
1802-
size_vectors_.get() + nfused + gemm_helper.left_inner_begin(), k_rank,
1803-
[](const vector_type& size_vector) -> const vector_type& {
1819+
size_vectors_.get() + nfused + gemm_helper.left_inner_begin() - u,
1820+
k_rank, [](const vector_type& size_vector) -> const vector_type& {
18041821
return size_vector;
18051822
});
18061823

@@ -1828,10 +1845,11 @@ class SparseShape {
18281845
// slab-batched norm GEMM: fold the fused modes into the tensor batch
18291846
// dimension by zero-copy reshape; result_folded shares result_norms'
18301847
// buffer, so the accumulation lands in place
1831-
auto left_folded = left.reshape(fold_range(left.range()), H);
1848+
auto left_folded =
1849+
left.reshape(fold_range(left.range(), unit_external), H);
18321850
auto right_folded = right.reshape(fold_range(right.range()), H);
1833-
auto result_folded =
1834-
result_norms.reshape(fold_range(result_norms.range()), H);
1851+
auto result_folded = result_norms.reshape(
1852+
fold_range(result_norms.range(), unit_external), H);
18351853
result_folded.gemm(left_folded, right_folded, abs_factor, gemm_helper);
18361854

18371855
// Hard zero tiles that are below the zero threshold.

src/TiledArray/tile_op/batched_contract_reduce.h

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,16 @@ class BatchedContractReduce {
6262

6363
/// \return the range spanned by modes [nfused_, rank) of \p r, rebased to
6464
/// zero lobounds (the folded view is a GEMM scratch view; only extents
65-
/// matter)
65+
/// matter); \p prepend_unit prepends a unit extent (the synthetic
66+
/// left-external mode of a no-external product, see
67+
/// ContEngine::init_struct_general)
6668
template <typename Range_>
67-
Range_ fold_range(const Range_& r) const {
69+
Range_ fold_range(const Range_& r, const bool prepend_unit = false) const {
6870
const auto* extent = r.extent_data();
69-
container::svector<typename Range_::index1_type> extents(extent + nfused_,
70-
extent + r.rank());
71+
container::svector<typename Range_::index1_type> extents;
72+
extents.reserve(r.rank() - nfused_ + (prepend_unit ? 1u : 0u));
73+
if (prepend_unit) extents.push_back(1);
74+
extents.insert(extents.end(), extent + nfused_, extent + r.rank());
7175
return Range_(extents);
7276
}
7377

@@ -150,7 +154,13 @@ class BatchedContractReduce {
150154

151155
const auto& gh = op_.gemm_helper();
152156
const unsigned int nc = gh.num_contract_ranks();
153-
const unsigned int neA = gh.left_rank() - nc;
157+
// a no-external product carries a SYNTHETIC unit left-external mode in
158+
// the GemmHelper only (see ContEngine::init_struct_general); detect it
159+
// from the one-rank mismatch with the actual left tile and pad the
160+
// folded left/result views with a unit extent
161+
const bool unit_external =
162+
(left.range().rank() + 1u == nfused_ + gh.left_rank());
163+
const unsigned int neA = gh.left_rank() - nc - (unit_external ? 1u : 0u);
154164
const unsigned int neB = gh.right_rank() - nc;
155165

156166
// both args must carry the fused modes as their leading modes, with
@@ -163,7 +173,8 @@ class BatchedContractReduce {
163173
TA_ASSERT(batch == fused_volume(right.range()));
164174

165175
// folded, zero-copy argument views
166-
auto left_folded = left.reshape(fold_range(left.range()), batch);
176+
auto left_folded =
177+
left.reshape(fold_range(left.range(), unit_external), batch);
167178
auto right_folded = right.reshape(fold_range(right.range()), batch);
168179

169180
if (empty(result)) {
@@ -195,7 +206,8 @@ class BatchedContractReduce {
195206
} else {
196207
// accumulate through a folded, zero-copy view of the result
197208
const auto full_range = result.range();
198-
auto result_folded = result.reshape(fold_range(full_range), batch);
209+
auto result_folded =
210+
result.reshape(fold_range(full_range, unit_external), batch);
199211
op_(result_folded, left_folded, right_folded);
200212
// the wrapped op may REBIND the result instead of writing in place:
201213
// the arena grow-to-cover path (a later K-panel touching inner cells

tests/general_product.cpp

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -731,20 +731,64 @@ BOOST_AUTO_TEST_CASE(expression_general_kitchen_sink) {
731731
BOOST_CHECK_SMALL(tot_max_abs_diff(w, w_ref), 1e-10);
732732
}
733733

734-
BOOST_AUTO_TEST_CASE(expression_general_product_tot_no_externals_gated) {
734+
BOOST_AUTO_TEST_CASE(expression_general_product_tot_no_externals) {
735735
// a ToT x ToT general product with NO external (free) outer indices --
736-
// every outer index fused or contracted -- is not supported by the
737-
// batched tile op (and used to segfault in the folded GEMM); the engine
738-
// must reject it with an informative error (einsum() handles this shape
739-
// natively via its no-external regime)
736+
// every outer index fused or contracted. The folded product has no free
737+
// modes, so it is evaluated with a synthetic unit left-external mode
738+
// carried by the tile op's GemmHelper only (this shape used to segfault).
740739
auto& world = TA::get_default_world();
740+
ForceLegacyEinsum legacy_oracle; // keep the einsum reference independent
741741
TA::TiledRange tr{{0, 3, 5}, {0, 2, 4}, {0, 2, 3}}; // x, i, j
742742
auto a = make_patterned_tot_array(world, tr, {2}, 1.0);
743743
auto b = make_patterned_tot_array(world, tr, {3}, 2.0);
744744

745745
TArrayToT c;
746-
BOOST_CHECK_THROW(c("i,j;a,b") = a("x,i,j;a") * b("x,i,j;b"),
747-
TiledArray::Exception);
746+
BOOST_REQUIRE_NO_THROW(c("i,j;a,b") = a("x,i,j;a") * b("x,i,j;b"));
747+
auto c_ref = TA::einsum(a("x,i,j;a"), b("x,i,j;b"), "i,j;a,b");
748+
BOOST_CHECK_SMALL(tot_max_abs_diff(c, c_ref), 1e-10);
749+
750+
// the same no-external root with the left operand produced by a general
751+
// T x ToT product at an INNER node (the original motivating expression)
752+
TA::TiledRange tr_g{{0, 3, 5}, {0, 2, 4}}; // x, i
753+
TA::TiledRange tr_cv{{0, 3, 5}, {0, 2, 3}}; // x, j
754+
auto g = make_patterned_array(world, tr_g, 1.0);
755+
auto cv = make_patterned_tot_array(world, tr_cv, {2}, 2.0);
756+
TArrayToT i1, w, w_ref;
757+
i1("x,i,j;a") = g("x,i") * cv("x,j;a");
758+
w_ref("i,j;a,b") = i1("x,i,j;a") * b("x,i,j;b");
759+
BOOST_REQUIRE_NO_THROW(w("i,j;a,b") =
760+
(g("x,i") * cv("x,j;a")) * b("x,i,j;b"));
761+
BOOST_CHECK_SMALL(tot_max_abs_diff(w, w_ref), 1e-10);
762+
}
763+
764+
BOOST_AUTO_TEST_CASE(expression_general_product_no_externals) {
765+
// the plain-tensor analogue of the no-external general product (the
766+
// Hadamard-reduction shape): C("i") = A("i,j") * B("i,j")
767+
auto& world = TA::get_default_world();
768+
ForceLegacyEinsum legacy_oracle; // keep the einsum reference independent
769+
TA::TiledRange tr{{0, 2, 4}, {0, 3, 5}}; // i, j
770+
auto a = make_patterned_array(world, tr, 1.0);
771+
auto b = make_patterned_array(world, tr, 2.0);
772+
773+
TA::TArrayD c;
774+
BOOST_REQUIRE_NO_THROW(c("i") = a("i,j") * b("i,j"));
775+
auto c_ref = TA::einsum(a("i,j"), b("i,j"), "i");
776+
BOOST_CHECK_SMALL(diff_norm(c, c_ref, "i"), 1e-10);
777+
}
778+
779+
BOOST_AUTO_TEST_CASE(expression_general_product_sparse_no_externals) {
780+
// block-sparse no-external general product: exercises the synthetic
781+
// unit-mode handling in SparseShape::gemm_batched
782+
auto& world = TA::get_default_world();
783+
ForceLegacyEinsum legacy_oracle; // keep the einsum reference independent
784+
TA::TiledRange tr{{0, 2, 5}, {0, 3, 4}, {0, 2, 6, 7}}; // b, i, j
785+
auto a = make_patterned_sparse_array(world, tr, 1.0, 3);
786+
auto b = make_patterned_sparse_array(world, tr, 2.0, 4);
787+
788+
TA::TSpArrayD c;
789+
BOOST_REQUIRE_NO_THROW(c("b,i") = a("b,i,j") * b("b,i,j"));
790+
auto c_ref = TA::einsum(a("b,i,j"), b("b,i,j"), "b,i");
791+
BOOST_CHECK_SMALL(diff_norm_sp(c, c_ref, "b,i"), 1e-10);
748792
}
749793

750794
BOOST_AUTO_TEST_CASE(expression_general_product_tot_inner_outer_product) {

0 commit comments

Comments
 (0)