@@ -674,27 +674,46 @@ class ContEngine : public BinaryEngine<Derived> {
674674 this ->init_perm (target_indices);
675675 general_repermute_ = (outer (target_indices) != outer (indices_));
676676
677- // the tile op operates on the folded (fused-mode-free) shapes
678- 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_);
679694 const auto right_op = to_cblas_op (right_outer_permtype_);
680695 if constexpr (!TiledArray::detail::is_tensor_of_tensor_v<value_type>) {
681- op_ = op_type (left_op, right_op, factor_, outer_size (indices_) - nh,
682- 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 ,
683698 outer_size (right_indices_) - nh);
684699 } else {
685700 // the batched tile op must be perm-free (BatchedContractReduce cannot
686- // host the folded-rank result permutation); the outer perm is empty by
687- // the interleaved-target gate above, so only an explicit inner result
688- // permutation can require one
689- if (!implicit_permute_inner_ && bool (inner (perm_)))
701+ // host the folded-rank result permutation); the outer perm is handled
702+ // by the streaming re-permute (general_repermute_), so only a genuine
703+ // (non-identity) explicit inner result permutation requires one. N.B.
704+ // perm_ may carry a non-null identity inner component when only the
705+ // outer modes are permuted (the bipartite perm is constructed whole).
706+ if (!implicit_permute_inner_ && bool (inner (perm_)) &&
707+ !inner (perm_).is_identity ())
690708 TA_EXCEPTION (
691709 " general products of tensors-of-tensors: a non-identity inner "
692710 " result permutation is not yet supported; reorder the inner "
693711 " annotation of the result" );
694712
695713 // factor_ is absorbed into element_nonreturn_op_
696714 op_ = op_type (left_op, right_op, scalar_type (1 ),
697- outer_size (indices_) - nh, outer_size (left_indices_) - nh,
715+ outer_size (indices_) - nh + u,
716+ outer_size (left_indices_) - nh + u,
698717 outer_size (right_indices_) - nh, BipartitePermutation{},
699718 this ->element_nonreturn_op_ , std::move (this ->arena_plan_ ));
700719 // ce+e, ce+ce_right and ce+ce_left are mutually exclusive; at most one
@@ -734,7 +753,11 @@ class ContEngine : public BinaryEngine<Derived> {
734753 trange_type make_trange_general () const {
735754 const unsigned int nh = n_fused_modes_;
736755 const unsigned int nc = op_.gemm_helper ().num_contract_ranks ();
737- 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;
738761 const unsigned int neB = op_.gemm_helper ().right_rank () - nc;
739762
740763 typename trange_type::Ranges ranges (nh + neA + neB);
@@ -786,7 +809,11 @@ class ContEngine : public BinaryEngine<Derived> {
786809 std::shared_ptr<const pmap_interface> pmap) {
787810 const unsigned int nh = n_fused_modes_;
788811 const unsigned int nc = op_.gemm_helper ().num_contract_ranks ();
789- 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;
790817 const unsigned int neB = op_.gemm_helper ().right_rank () - nc;
791818
792819 // Get pointers to the argument sizes
@@ -1707,7 +1734,11 @@ class ContEngine : public BinaryEngine<Derived> {
17071734 TiledArray::detail::is_contraction_arena_tot_v<
17081735 result_tile_type, left_tile_type, right_tile_type>;
17091736 if constexpr (arena_eligible_scale) {
1710- if (this ->outer_product_uses_summa ()) {
1737+ // the fused arena scale ops are factor-free; a non-unit
1738+ // expression-level prefactor (ScalMult) takes the fallback op,
1739+ // which absorbs it
1740+ if (this ->outer_product_uses_summa () &&
1741+ this ->factor_ == scalar_type (1 )) {
17111742 // The inner perm handed to the plan must match how the inner
17121743 // *result* permutation is applied for this result cell type --
17131744 // and the two cell types apply it in different places:
@@ -1741,45 +1772,48 @@ class ContEngine : public BinaryEngine<Derived> {
17411772 // cells. The Hadamard outer product is an assignment
17421773 // `result = (perm ^ tot) * scalar`, which needs value-returning
17431774 // `scale`; only owning inner cells support it.
1744- auto fallback_op = [perm = !this ->implicit_permute_inner_
1745- ? inner (this ->perm_ )
1746- : Permutation{},
1747- outer_uses_summa =
1748- this ->outer_product_uses_summa ()](
1749- result_tile_element_type& result,
1750- const left_tile_element_type& left,
1751- const right_tile_element_type& right) {
1752- if (outer_uses_summa) {
1753- using TiledArray::axpy_to;
1754- if constexpr (tot_x_t ) {
1755- if (left.empty ()) return ; // absent cell: no contribution
1756- if (perm)
1757- axpy_to (result, left, right, perm);
1758- else
1759- axpy_to (result, left, right);
1760- } else {
1761- if (right.empty ()) return ; // absent cell: no contribution
1762- if (perm)
1763- axpy_to (result, right, left, perm);
1764- else
1765- axpy_to (result, right, left);
1766- }
1767- } else {
1768- if constexpr (!TiledArray::is_tensor_view_v<
1769- result_tile_element_type>) {
1770- using TiledArray::scale;
1771- if constexpr (tot_x_t )
1772- result = perm ? scale (left, right, perm) : scale (left, right);
1773- else
1774- result = perm ? scale (right, left, perm) : scale (right, left);
1775- } else {
1776- TA_EXCEPTION (
1777- " Tensor<View> scale-inner Hadamard-outer product: a "
1778- " view result cell cannot be value-assigned a fresh "
1779- " scaled tensor" );
1780- }
1781- }
1782- };
1775+ // N.B. the expression-level scalar prefactor (factor_, != 1 for
1776+ // ScalMult expressions) multiplies the plain operand's element
1777+ auto fallback_op =
1778+ [perm = !this ->implicit_permute_inner_ ? inner (this ->perm_ )
1779+ : Permutation{},
1780+ outer_uses_summa = this ->outer_product_uses_summa (),
1781+ factor = this ->factor_ ](result_tile_element_type& result,
1782+ const left_tile_element_type& left,
1783+ const right_tile_element_type& right) {
1784+ if (outer_uses_summa) {
1785+ using TiledArray::axpy_to;
1786+ if constexpr (tot_x_t ) {
1787+ if (left.empty ()) return ; // absent cell: no contribution
1788+ if (perm)
1789+ axpy_to (result, left, right * factor, perm);
1790+ else
1791+ axpy_to (result, left, right * factor);
1792+ } else {
1793+ if (right.empty ()) return ; // absent cell: no contribution
1794+ if (perm)
1795+ axpy_to (result, right, left * factor, perm);
1796+ else
1797+ axpy_to (result, right, left * factor);
1798+ }
1799+ } else {
1800+ if constexpr (!TiledArray::is_tensor_view_v<
1801+ result_tile_element_type>) {
1802+ using TiledArray::scale;
1803+ if constexpr (tot_x_t )
1804+ result = perm ? scale (left, right * factor, perm)
1805+ : scale (left, right * factor);
1806+ else
1807+ result = perm ? scale (right, left * factor, perm)
1808+ : scale (right, left * factor);
1809+ } else {
1810+ TA_EXCEPTION (
1811+ " Tensor<View> scale-inner Hadamard-outer product: a "
1812+ " view result cell cannot be value-assigned a fresh "
1813+ " scaled tensor" );
1814+ }
1815+ }
1816+ };
17831817 if constexpr (arena_eligible_scale) {
17841818 if (this ->arena_plan_ ) {
17851819 if constexpr (tot_x_t )
0 commit comments