Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ TiledArray/dist_eval/binary_eval.h
TiledArray/dist_eval/contraction_eval.h
TiledArray/dist_eval/dist_eval.h
TiledArray/dist_eval/unary_eval.h
TiledArray/dist_eval/zero_volume_eval.h
TiledArray/einsum/index.h
TiledArray/einsum/range.h
TiledArray/einsum/string.h
Expand Down
69 changes: 69 additions & 0 deletions src/TiledArray/dist_eval/zero_volume_eval.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* This file is a part of TiledArray.
* Copyright (C) 2026 Virginia Tech
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

#ifndef TILEDARRAY_DIST_EVAL_ZERO_VOLUME_EVAL_H__INCLUDED
#define TILEDARRAY_DIST_EVAL_ZERO_VOLUME_EVAL_H__INCLUDED

#include <TiledArray/dist_eval/dist_eval.h>

namespace TiledArray {
namespace detail {

/// A distributed evaluator over a zero-volume tiled range: it owns no tiles,
/// so evaluation produces nothing and no tile can be requested. The general
/// (fused x contracted) product evaluates a zero-volume result through it,
/// since its SUMMA evaluator needs a process grid and ProcGrid requires at
/// least one row and one column (see ContEngine::init_distribution_general).
/// \tparam Tile The output tile type
/// \tparam Policy The tensor policy class
template <typename Tile, typename Policy>
class ZeroVolumeEvalImpl final : public DistEvalImpl<Tile, Policy> {
public:
typedef DistEvalImpl<Tile, Policy> DistEvalImpl_; ///< The base class type
typedef typename DistEvalImpl_::ordinal_type ordinal_type; ///< Ordinal type
typedef typename DistEvalImpl_::trange_type trange_type; ///< Tiled range
typedef typename DistEvalImpl_::shape_type shape_type; ///< Shape type
typedef typename DistEvalImpl_::pmap_interface pmap_interface; ///< Pmap
typedef typename DistEvalImpl_::value_type value_type; ///< Tile

/// \param world The world of the result
/// \param trange The result tiled range; its tile range must be empty
/// \param shape The result shape
/// \param pmap The result process map
ZeroVolumeEvalImpl(World& world, const trange_type& trange,
const shape_type& shape,
const std::shared_ptr<const pmap_interface>& pmap)
: DistEvalImpl_(world, trange, shape, pmap, Permutation{}) {
TA_ASSERT(trange.tiles_range().volume() == 0);
}

Future<value_type> get_tile(ordinal_type) const override {
TA_EXCEPTION("ZeroVolumeEvalImpl owns no tiles");
return Future<value_type>();
}

void discard_tile(ordinal_type) const override {}

int internal_eval() override { return 0; }
};

} // namespace detail
} // namespace TiledArray

#endif // TILEDARRAY_DIST_EVAL_ZERO_VOLUME_EVAL_H__INCLUDED
14 changes: 14 additions & 0 deletions src/TiledArray/expressions/cont_engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

#include <TiledArray/dist_eval/contraction_eval.h>
#include <TiledArray/dist_eval/unary_eval.h>
#include <TiledArray/dist_eval/zero_volume_eval.h>
#include <TiledArray/expressions/binary_engine.h>
#include <TiledArray/expressions/permopt.h>
#include <TiledArray/pmap/slabbed_pmap.h>
Expand Down Expand Up @@ -1120,6 +1121,19 @@ class ContEngine : public BinaryEngine<Derived> {
batched_op_type, typename Derived::policy>
impl_type;

// A zero-volume result (some result mode has no tiles): the corner case
// of init_distribution_general skipped the process grid (ProcGrid needs
// at least one row and one column) and the slabbed process maps, so
// neither the Summa evaluator nor its canonical pmap can be built here.
// The result has no tiles, so evaluate it through the tile-less
// evaluator in the target layout.
if (trange_.tiles_range().volume() == 0) {
typedef TiledArray::detail::ZeroVolumeEvalImpl<value_type, policy>
empty_impl_type;
return dist_eval_type(
std::make_shared<empty_impl_type>(*world_, trange_, shape_, pmap_));
}

typename left_type::dist_eval_type left = left_.make_dist_eval();
typename right_type::dist_eval_type right = right_.make_dist_eval();

Expand Down
101 changes: 101 additions & 0 deletions tests/general_product.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,107 @@ BOOST_AUTO_TEST_CASE(expression_general_product_sparse_no_externals) {
BOOST_CHECK_SMALL(diff_norm_sp(c, c_ref, "b,i"), 1e-10);
}

namespace {

/// tranges for C("b,i,k") = A("b,i,j") * B("b,j,k") in which exactly one
/// result mode has no tiles, so the result has zero volume. Which mode is
/// empty selects which of the general-product distribution extents vanishes:
/// `i` -> M == 0, `b` -> n_slabs_ == 0, `k` -> N == 0 (see
/// ContEngine::init_distribution_general).
struct ZeroVolumeTRanges {
TA::TiledRange a; ///< b, i, j
TA::TiledRange b; ///< b, j, k
};

/// \param empty_mode one of "i" (M == 0), "b" (n_slabs_ == 0), "k" (N == 0)
ZeroVolumeTRanges make_zero_volume_tranges(const std::string& empty_mode) {
const TA::TiledRange1 none{0}; // no tiles
const TA::TiledRange1 tr_b{0, 2, 5}, tr_i{0, 3, 4}, tr_j{0, 2, 6, 7},
tr_k{0, 4, 5};
if (empty_mode == "i")
return {TA::TiledRange{tr_b, none, tr_j}, TA::TiledRange{tr_b, tr_j, tr_k}};
if (empty_mode == "b")
return {TA::TiledRange{none, tr_i, tr_j}, TA::TiledRange{none, tr_j, tr_k}};
BOOST_REQUIRE_EQUAL(empty_mode, "k");
return {TA::TiledRange{tr_b, tr_i, tr_j}, TA::TiledRange{tr_b, tr_j, none}};
}

/// checks that \p array carries no tiles and no elements
template <typename Array>
void check_zero_volume(const Array& array) {
BOOST_CHECK_EQUAL(array.trange().tiles_range().volume(), 0ul);
BOOST_CHECK_EQUAL(array.trange().elements_range().volume(), 0ul);
BOOST_CHECK(array.begin() == array.end());
}

} // namespace

BOOST_AUTO_TEST_CASE(expression_general_product_zero_volume_dense) {
// a result mode with no tiles => zero-volume result.
// init_distribution_general skips the process grid in that corner case
// (ProcGrid needs at least one row and one column), so make_dist_eval_general
// must not build the Summa evaluator or its canonical slabbed pmap: it
// evaluates through the tile-less ZeroVolumeEvalImpl instead. Before that,
// this tripped TA_ASSERT(world_) in ProcGrid::make_pmap.
auto& world = TA::get_default_world();

for (const std::string empty_mode : {"i", "b", "k"}) {
BOOST_TEST_CONTEXT("empty mode = " << empty_mode) {
const auto tr = make_zero_volume_tranges(empty_mode);
auto a = make_patterned_array(world, tr.a, 1.0);
auto b = make_patterned_array(world, tr.b, 2.0);

// canonical target
TA::TArrayD c;
BOOST_REQUIRE_NO_THROW(c("b,i,k") = a("b,i,j") * b("b,j,k"));
check_zero_volume(c);

// repermuted target: the result layout differs from the canonical
// (h, e_A, e_B) one, so the evaluator is built in the target layout
TA::TArrayD d;
BOOST_REQUIRE_NO_THROW(d("i,b,k") = a("b,i,j") * b("b,j,k"));
check_zero_volume(d);
}
}
}

BOOST_AUTO_TEST_CASE(expression_general_product_zero_volume_sparse) {
// same three degenerate shapes on SparsePolicy: the zero-volume path runs
// before any shape-driven work, so it must behave identically
auto& world = TA::get_default_world();

for (const std::string empty_mode : {"i", "b", "k"}) {
BOOST_TEST_CONTEXT("empty mode = " << empty_mode) {
const auto tr = make_zero_volume_tranges(empty_mode);
auto a = make_patterned_sparse_array(world, tr.a, 1.0, 3);
auto b = make_patterned_sparse_array(world, tr.b, 2.0, 4);

TA::TSpArrayD c;
BOOST_REQUIRE_NO_THROW(c("b,i,k") = a("b,i,j") * b("b,j,k"));
check_zero_volume(c);

TA::TSpArrayD d;
BOOST_REQUIRE_NO_THROW(d("i,b,k") = a("b,i,j") * b("b,j,k"));
check_zero_volume(d);
}
}
}

BOOST_AUTO_TEST_CASE(expression_general_product_zero_volume_contracted_mode) {
// control: a zero-tile CONTRACTED mode leaves the result volume nonzero, so
// the ordinary (process-grid) path still runs and produces a zero array
auto& world = TA::get_default_world();
const TA::TiledRange1 none{0};
const TA::TiledRange1 tr_b{0, 2, 5}, tr_i{0, 3, 4}, tr_k{0, 4, 5};
auto a = make_patterned_array(world, TA::TiledRange{tr_b, tr_i, none}, 1.0);
auto b = make_patterned_array(world, TA::TiledRange{tr_b, none, tr_k}, 2.0);

TA::TArrayD c;
BOOST_REQUIRE_NO_THROW(c("b,i,k") = a("b,i,j") * b("b,j,k"));
BOOST_CHECK_EQUAL(c.trange().tiles_range().volume(), 8ul); // 2 * 2 * 2
BOOST_CHECK_SMALL(c("b,i,k").norm().get(), 1e-10);
}

BOOST_AUTO_TEST_CASE(expression_general_product_tot_inner_outer_product) {
// the PNO-CC PPL building-block shape: ToT x ToT with an EMPTY right
// outer-external set and an inner OUTER-product:
Expand Down
Loading