diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 64bc4e2eda..6c4f7a04ee 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 diff --git a/src/TiledArray/dist_eval/zero_volume_eval.h b/src/TiledArray/dist_eval/zero_volume_eval.h new file mode 100644 index 0000000000..36796b20e7 --- /dev/null +++ b/src/TiledArray/dist_eval/zero_volume_eval.h @@ -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 . + * + */ + +#ifndef TILEDARRAY_DIST_EVAL_ZERO_VOLUME_EVAL_H__INCLUDED +#define TILEDARRAY_DIST_EVAL_ZERO_VOLUME_EVAL_H__INCLUDED + +#include + +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 +class ZeroVolumeEvalImpl final : public DistEvalImpl { + public: + typedef DistEvalImpl 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& pmap) + : DistEvalImpl_(world, trange, shape, pmap, Permutation{}) { + TA_ASSERT(trange.tiles_range().volume() == 0); + } + + Future get_tile(ordinal_type) const override { + TA_EXCEPTION("ZeroVolumeEvalImpl owns no tiles"); + return Future(); + } + + 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 diff --git a/src/TiledArray/expressions/cont_engine.h b/src/TiledArray/expressions/cont_engine.h index a7ff78c1d4..c0bc4512f3 100644 --- a/src/TiledArray/expressions/cont_engine.h +++ b/src/TiledArray/expressions/cont_engine.h @@ -28,6 +28,7 @@ #include #include +#include #include #include #include @@ -1120,6 +1121,19 @@ class ContEngine : public BinaryEngine { 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 + empty_impl_type; + return dist_eval_type( + std::make_shared(*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(); diff --git a/tests/general_product.cpp b/tests/general_product.cpp index e76fa24db9..ba2622de0a 100644 --- a/tests/general_product.cpp +++ b/tests/general_product.cpp @@ -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 +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: