Skip to content

Commit 3031090

Browse files
committed
general product: handle fused-broadcast-on-the-right (rank-0 right operand)
A general product whose RIGHT operand is entirely fused with no contraction (a fused broadcast on the right, e.g. C("b,k") = A("b,k") * B("b")) folds to a rank-0 right operand, which the batched GEMM cannot host (TA rank-0 ranges are null/volume-0, and gemm asserts operand.rank()==gemm_helper.rank()). The synthetic-unit machinery covered only the rank-0 RESULT (no-external product) and rank-0 LEFT (fused-broadcast-left) cases via a unit LEFT-external mode; the rank-0 RIGHT case was unhandled, so folding the right operand aborted in BatchedContractReduce::contract_pair (or silently mis-shaped with asserts off). Add a symmetric synthetic unit RIGHT-external mode, mirroring the left one: - ContEngine::synthetic_unit_right_external() detects outer_size(right_indices_) == n_fused_modes_; u_right is threaded through init_struct_general (op_ ctors, right_op NoTranspose), make_trange_general and init_distribution_general (neB -= u_right). - SparseShape::gemm_batched detects the right phantom by the one-rank mismatch and guards the right-outer loops / result rank accordingly (shape-level analog); its fold_range lambda gains an append_unit option. - BatchedContractReduce::contract_pair detects unit_right_external, excludes it from neB, and pads the folded right (and accumulating result) views with a trailing unit; the member fold_range gains an append_unit option. The synthetic mode lives only in the GemmHelper; tranges, shapes and tiles carry the true ranks. The result gains a trailing unit mode (squeezed out of the actual result) exactly as the left case prepends one. Tests: general_product gains dense, ToT->ToT and block-sparse fused-broadcast- right cases through the expression layer (which respects operand order, unlike einsum's reordering); dot_inner gains a ->T denest fused-broadcast-right case. This was surfaced by MPQC PNO/CSV-CCk denest products evaluated via the native ToT*ToT->T dispatch.
1 parent 5333c33 commit 3031090

5 files changed

Lines changed: 177 additions & 42 deletions

File tree

src/TiledArray/expressions/cont_engine.h

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -735,12 +735,20 @@ class ContEngine : public BinaryEngine<Derived> {
735735
// the synthetic mode from the one-rank mismatch and pad their folded views
736736
// with a unit extent.
737737
const unsigned int u = synthetic_unit_left_external();
738+
// - a FUSED BROADCAST ON THE RIGHT (the right operand is entirely fused,
739+
// no contraction, e.g. C("b,k") = A("b,k") * B("b")) folds to a rank-0
740+
// RIGHT operand; a synthetic unit RIGHT-external mode restores a
741+
// supported (M,K) x (K,1) -> (M,1) shape (see
742+
// synthetic_unit_right_external()).
743+
const unsigned int u_right = synthetic_unit_right_external();
738744

739745
// the tile op operates on the folded (fused-mode-free) shapes; the
740-
// synthetic unit mode leads the folded left operand, so it is NoTrans
746+
// synthetic unit mode leads the folded left operand (trails the folded
747+
// right operand), so it is NoTrans on that side
741748
const auto left_op =
742749
u ? math::blas::NoTranspose : to_cblas_op(left_outer_permtype_);
743-
const auto right_op = to_cblas_op(right_outer_permtype_);
750+
const auto right_op =
751+
u_right ? math::blas::NoTranspose : to_cblas_op(right_outer_permtype_);
744752
// As in init_struct, the ContractReduce tile op needs the per-cell inner
745753
// element op when the operands are nested -- including the dot_inner regime
746754
// (denest_to_scalar), where the result tile is plain but the nested modes
@@ -749,9 +757,10 @@ class ContEngine : public BinaryEngine<Derived> {
749757
TiledArray::detail::is_tensor_of_tensor_v<value_type> ||
750758
denest_to_scalar;
751759
if constexpr (!tot_aware_op) {
752-
op_ = op_type(left_op, right_op, factor_, outer_size(indices_) - nh + u,
760+
op_ = op_type(left_op, right_op, factor_,
761+
outer_size(indices_) - nh + u + u_right,
753762
outer_size(left_indices_) - nh + u,
754-
outer_size(right_indices_) - nh);
763+
outer_size(right_indices_) - nh + u_right);
755764
} else {
756765
// the batched tile op must be perm-free (BatchedContractReduce cannot
757766
// host the folded-rank result permutation); the outer perm is handled
@@ -772,10 +781,11 @@ class ContEngine : public BinaryEngine<Derived> {
772781

773782
// factor_ is absorbed into element_nonreturn_op_
774783
op_ = op_type(left_op, right_op, scalar_type(1),
775-
outer_size(indices_) - nh + u,
784+
outer_size(indices_) - nh + u + u_right,
776785
outer_size(left_indices_) - nh + u,
777-
outer_size(right_indices_) - nh, BipartitePermutation{},
778-
this->element_nonreturn_op_, std::move(this->arena_plan_));
786+
outer_size(right_indices_) - nh + u_right,
787+
BipartitePermutation{}, this->element_nonreturn_op_,
788+
std::move(this->arena_plan_));
779789
// ce+e, ce+ce_right and ce+ce_left are mutually exclusive; at most one
780790
// is non-null and only one install fires (see init_struct)
781791
if constexpr (TiledArray::detail::is_tensor_of_tensor_v<value_type>) {
@@ -826,6 +836,20 @@ class ContEngine : public BinaryEngine<Derived> {
826836
: 0u;
827837
}
828838

839+
/// \return 1 if the folded general product needs a SYNTHETIC unit
840+
/// right-external mode, else 0. Mirror of synthetic_unit_left_external() for
841+
/// the RIGHT operand: a rank-0 RIGHT operand arises when the right argument
842+
/// is entirely fused with no contraction (a fused broadcast on the right,
843+
/// e.g. C("b,k") = A("b,k") * B("b")), i.e.
844+
/// outer_size(right_indices_) == n_fused_modes_. A left-external unit cannot
845+
/// fix it (the right operand stays rank-0), so a unit right-external mode
846+
/// (carried only in the GemmHelper) restores a supported (M,K) x (K,1) ->
847+
/// (M,1) shape. The result gains a trailing unit mode that is absent from the
848+
/// actual tranges/shapes/tiles, exactly as the left case prepends one.
849+
unsigned int synthetic_unit_right_external() const {
850+
return (outer_size(right_indices_) == n_fused_modes_) ? 1u : 0u;
851+
}
852+
829853
/// Tiled range factory function for a general product
830854

831855
/// \return The result tiled range: the fused mode ranges followed by the
@@ -837,8 +861,9 @@ class ContEngine : public BinaryEngine<Derived> {
837861
// GemmHelper only (see synthetic_unit_left_external()); the actual tranges
838862
// do not have it
839863
const unsigned int u = synthetic_unit_left_external();
864+
const unsigned int u_right = synthetic_unit_right_external();
840865
const unsigned int neA = op_.gemm_helper().left_rank() - nc - u;
841-
const unsigned int neB = op_.gemm_helper().right_rank() - nc;
866+
const unsigned int neB = op_.gemm_helper().right_rank() - nc - u_right;
842867

843868
typename trange_type::Ranges ranges(nh + neA + neB);
844869
unsigned int i = 0ul;
@@ -893,8 +918,9 @@ class ContEngine : public BinaryEngine<Derived> {
893918
// GemmHelper only (see synthetic_unit_left_external()); the actual tranges
894919
// do not have it
895920
const unsigned int u = synthetic_unit_left_external();
921+
const unsigned int u_right = synthetic_unit_right_external();
896922
const unsigned int neA = op_.gemm_helper().left_rank() - nc - u;
897-
const unsigned int neB = op_.gemm_helper().right_rank() - nc;
923+
const unsigned int neB = op_.gemm_helper().right_rank() - nc - u_right;
898924

899925
// Get pointers to the argument sizes
900926
const auto* MADNESS_RESTRICT const left_tiles_size =

src/TiledArray/sparse_shape.h

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1727,13 +1727,21 @@ class SparseShape {
17271727
const bool unit_external =
17281728
(tile_norms_.range().rank() + 1u == nfused + gemm_helper.left_rank());
17291729
const unsigned int u = unit_external ? 1u : 0u;
1730+
// a fused broadcast on the RIGHT carries a SYNTHETIC unit right-external
1731+
// mode in the GemmHelper only (mirror of unit_external; see
1732+
// ContEngine::synthetic_unit_right_external); detect it from the one-rank
1733+
// mismatch with the actual right norm tensor and pad the folded
1734+
// right/result views with a unit extent
1735+
const bool unit_right_external = (other.tile_norms_.range().rank() + 1u ==
1736+
nfused + gemm_helper.right_rank());
1737+
const unsigned int u_right = unit_right_external ? 1u : 0u;
17301738

17311739
// check that the ranks match the folded gemm ranks plus the fused modes,
17321740
// and that the fused and contracted mode extents of the two shapes are
17331741
// congruent
17341742
TA_ASSERT(tile_norms_.range().rank() + u ==
17351743
nfused + gemm_helper.left_rank());
1736-
TA_ASSERT(other.tile_norms_.range().rank() ==
1744+
TA_ASSERT(other.tile_norms_.range().rank() + u_right ==
17371745
nfused + gemm_helper.right_rank());
17381746
for (unsigned int d = 0u; d < nfused; ++d)
17391747
TA_ASSERT(left_extent[d] == right_extent[d]);
@@ -1751,14 +1759,16 @@ class SparseShape {
17511759
for (unsigned int i = gemm_helper.left_inner_begin();
17521760
i < gemm_helper.left_inner_end(); ++i)
17531761
K *= left_extent[nfused + i - u];
1754-
for (unsigned int i = gemm_helper.right_outer_begin();
1755-
i < gemm_helper.right_outer_end(); ++i)
1756-
N *= right_extent[nfused + i];
1762+
if (!unit_right_external)
1763+
for (unsigned int i = gemm_helper.right_outer_begin();
1764+
i < gemm_helper.right_outer_end(); ++i)
1765+
N *= right_extent[nfused + i];
17571766

17581767
// result size vectors: fused modes (from this), then the left and right
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;
1768+
// outer modes (the synthetic unit left-/right-external modes are absent
1769+
// from the actual result)
1770+
const unsigned int result_rank =
1771+
nfused + gemm_helper.result_rank() - u - u_right;
17621772
std::shared_ptr<vector_type> result_size_vectors(
17631773
new vector_type[result_rank], std::default_delete<vector_type[]>());
17641774
unsigned int x = 0ul;
@@ -1768,9 +1778,10 @@ class SparseShape {
17681778
for (unsigned int i = gemm_helper.left_outer_begin();
17691779
i < gemm_helper.left_outer_end(); ++i, ++x)
17701780
result_size_vectors.get()[x] = size_vectors_.get()[nfused + i];
1771-
for (unsigned int i = gemm_helper.right_outer_begin();
1772-
i < gemm_helper.right_outer_end(); ++i, ++x)
1773-
result_size_vectors.get()[x] = other.size_vectors_.get()[nfused + i];
1781+
if (!unit_right_external)
1782+
for (unsigned int i = gemm_helper.right_outer_begin();
1783+
i < gemm_helper.right_outer_end(); ++i, ++x)
1784+
result_size_vectors.get()[x] = other.size_vectors_.get()[nfused + i];
17741785

17751786
// the result norm tensor over (fused..., left outer..., right outer...)
17761787
using range_type = typename Tensor<value_type>::range_type;
@@ -1788,22 +1799,28 @@ class SparseShape {
17881799
lobounds.push_back(tile_norms_.range().lobound_data()[nfused + i]);
17891800
upbounds.push_back(tile_norms_.range().upbound_data()[nfused + i]);
17901801
}
1791-
for (unsigned int i = gemm_helper.right_outer_begin();
1792-
i < gemm_helper.right_outer_end(); ++i) {
1793-
lobounds.push_back(other.tile_norms_.range().lobound_data()[nfused + i]);
1794-
upbounds.push_back(other.tile_norms_.range().upbound_data()[nfused + i]);
1795-
}
1802+
if (!unit_right_external)
1803+
for (unsigned int i = gemm_helper.right_outer_begin();
1804+
i < gemm_helper.right_outer_end(); ++i) {
1805+
lobounds.push_back(
1806+
other.tile_norms_.range().lobound_data()[nfused + i]);
1807+
upbounds.push_back(
1808+
other.tile_norms_.range().upbound_data()[nfused + i]);
1809+
}
17961810
Tensor<value_type> result_norms(range_type(lobounds, upbounds), 0);
17971811

17981812
// the range spanned by modes [nfused, rank) of \p r, rebased to zero
17991813
// lobounds (scratch view for the slab-batched norm GEMM)
18001814
auto fold_range = [nfused](const range_type& r,
1801-
const bool prepend_unit = false) {
1815+
const bool prepend_unit = false,
1816+
const bool append_unit = false) {
18021817
const auto* extent = r.extent_data();
18031818
container::svector<index1_type> extents;
1804-
extents.reserve(r.rank() - nfused + (prepend_unit ? 1u : 0u));
1819+
extents.reserve(r.rank() - nfused + (prepend_unit ? 1u : 0u) +
1820+
(append_unit ? 1u : 0u));
18051821
if (prepend_unit) extents.push_back(1);
18061822
extents.insert(extents.end(), extent + nfused, extent + r.rank());
1823+
if (append_unit) extents.push_back(1);
18071824
return range_type(extents);
18081825
};
18091826

@@ -1847,9 +1864,11 @@ class SparseShape {
18471864
// buffer, so the accumulation lands in place
18481865
auto left_folded =
18491866
left.reshape(fold_range(left.range(), unit_external), H);
1850-
auto right_folded = right.reshape(fold_range(right.range()), H);
1867+
auto right_folded = right.reshape(
1868+
fold_range(right.range(), false, unit_right_external), H);
18511869
auto result_folded = result_norms.reshape(
1852-
fold_range(result_norms.range(), unit_external), H);
1870+
fold_range(result_norms.range(), unit_external, unit_right_external),
1871+
H);
18531872
result_folded.gemm(left_folded, right_folded, abs_factor, gemm_helper);
18541873

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

src/TiledArray/tile_op/batched_contract_reduce.h

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,19 @@ class BatchedContractReduce {
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
6565
/// 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)
66+
/// left-external mode of a no-external/left-broadcast product) and
67+
/// \p append_unit appends one (the synthetic right-external mode of a
68+
/// right-broadcast product), see ContEngine::init_struct_general
6869
template <typename Range_>
69-
Range_ fold_range(const Range_& r, const bool prepend_unit = false) const {
70+
Range_ fold_range(const Range_& r, const bool prepend_unit = false,
71+
const bool append_unit = false) const {
7072
const auto* extent = r.extent_data();
7173
container::svector<typename Range_::index1_type> extents;
72-
extents.reserve(r.rank() - nfused_ + (prepend_unit ? 1u : 0u));
74+
extents.reserve(r.rank() - nfused_ + (prepend_unit ? 1u : 0u) +
75+
(append_unit ? 1u : 0u));
7376
if (prepend_unit) extents.push_back(1);
7477
extents.insert(extents.end(), extent + nfused_, extent + r.rank());
78+
if (append_unit) extents.push_back(1);
7579
return Range_(extents);
7680
}
7781

@@ -154,14 +158,21 @@ class BatchedContractReduce {
154158

155159
const auto& gh = op_.gemm_helper();
156160
const unsigned int nc = gh.num_contract_ranks();
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+
// a no-external / left-broadcast product carries a SYNTHETIC unit
162+
// left-external mode in the GemmHelper only (see
163+
// ContEngine::init_struct_general); detect it from the one-rank mismatch
164+
// with the actual left tile and pad the folded left/result views with a
165+
// unit extent. A right-broadcast product (right operand entirely fused, no
166+
// contraction) likewise carries a synthetic unit right-external mode,
167+
// detected and padded symmetrically (the unit trails the folded right /
168+
// result views).
161169
const bool unit_external =
162170
(left.range().rank() + 1u == nfused_ + gh.left_rank());
171+
const bool unit_right_external =
172+
(right.range().rank() + 1u == nfused_ + gh.right_rank());
163173
const unsigned int neA = gh.left_rank() - nc - (unit_external ? 1u : 0u);
164-
const unsigned int neB = gh.right_rank() - nc;
174+
const unsigned int neB =
175+
gh.right_rank() - nc - (unit_right_external ? 1u : 0u);
165176

166177
// both args must carry the fused modes as their leading modes, with
167178
// equal extents
@@ -172,10 +183,13 @@ class BatchedContractReduce {
172183
const std::size_t batch = fused_volume(left.range());
173184
TA_ASSERT(batch == fused_volume(right.range()));
174185

175-
// folded, zero-copy argument views
186+
// folded, zero-copy argument views (the synthetic left-external unit
187+
// leads the folded left view; the synthetic right-external unit trails the
188+
// folded right view)
176189
auto left_folded =
177190
left.reshape(fold_range(left.range(), unit_external), batch);
178-
auto right_folded = right.reshape(fold_range(right.range()), batch);
191+
auto right_folded = right.reshape(
192+
fold_range(right.range(), false, unit_right_external), batch);
179193

180194
if (empty(result)) {
181195
// let the wrapped op allocate (and zero- or beta-0-initialize) the
@@ -206,8 +220,8 @@ class BatchedContractReduce {
206220
} else {
207221
// accumulate through a folded, zero-copy view of the result
208222
const auto full_range = result.range();
209-
auto result_folded =
210-
result.reshape(fold_range(full_range, unit_external), batch);
223+
auto result_folded = result.reshape(
224+
fold_range(full_range, unit_external, unit_right_external), batch);
211225
op_(result_folded, left_folded, right_folded);
212226
// the wrapped op may REBIND the result instead of writing in place:
213227
// the arena grow-to-cover path (a later K-panel touching inner cells

tests/dot_inner.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,31 @@ BOOST_AUTO_TEST_CASE(hadamard_outer) {
9494
BOOST_REQUIRE((ToTArrayFixture::are_equal<ShapeComp::True>(ref, out)));
9595
}
9696

97+
// RIGHT operand entirely fused at the outer level (a per-fused-block scale on
98+
// the right): bk;ab . b;ab -> bk. Outer b fused, k external (from left), the
99+
// right operand carries NO outer external/contracted mode, so the folded right
100+
// operand is rank-0 and needs the synthetic unit RIGHT-external mode
101+
// (ContEngine::synthetic_unit_right_external). This is the denest (->T) analog
102+
// of the CSV-CCk fused-broadcast-right shape; without the fix the folded right
103+
// reshape aborts. inner ab fully contracted.
104+
BOOST_AUTO_TEST_CASE(broadcast_right_outer) {
105+
TA::TiledRange a_tr{{0, 2, 4}, {0, 3, 4}}; // b, k
106+
TA::TiledRange b_tr{{0, 2, 4}}; // b
107+
auto A = random_array<ArrayToT>(a_tr, {3, 2});
108+
auto B = random_array<ArrayToT>(b_tr, {3, 2});
109+
110+
ArrayT ref;
111+
{
112+
LegacyEinsumGuard g;
113+
ref = TA::einsum<DeNest::True>("bk;mn,b;mn->bk", A, B);
114+
}
115+
116+
ArrayT out;
117+
out("b,k") = A("b,k;m,n").dot_inner(B("b;m,n"));
118+
119+
BOOST_REQUIRE((ToTArrayFixture::are_equal<ShapeComp::True>(ref, out)));
120+
}
121+
97122
// outer: ipk x iqk -> ipq (Hadamard i, external p & q, contracted-outer k);
98123
// inner ab fully contracted. Exercises the Contraction/General outer routing.
99124
BOOST_AUTO_TEST_CASE(hadamard_external_contracted_outer) {

0 commit comments

Comments
 (0)