2 * This file is part of the GROMACS molecular simulation package.
4 * Copyright (c) 2021, by the GROMACS development team, led by
5 * Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
6 * and including many others, as listed in the AUTHORS file in the
7 * top-level source directory and at http://www.gromacs.org.
9 * GROMACS is free software; you can redistribute it and/or
10 * modify it under the terms of the GNU Lesser General Public License
11 * as published by the Free Software Foundation; either version 2.1
12 * of the License, or (at your option) any later version.
14 * GROMACS is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 * Lesser General Public License for more details.
19 * You should have received a copy of the GNU Lesser General Public
20 * License along with GROMACS; if not, see
21 * http://www.gnu.org/licenses, or write to the Free Software Foundation,
22 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24 * If you want to redistribute modifications to GROMACS, please
25 * consider that scientific software is very special. Version
26 * control is crucial - bugs must be traceable. We will be happy to
27 * consider code for inclusion in the official distribution, but
28 * derived work must not be called official GROMACS. Details are found
29 * in the README & COPYING files - if they are missing, get the
30 * official version at http://www.gromacs.org.
32 * To help us fund GROMACS development, we humbly ask that you cite
33 * the research papers on the package. Check out http://www.gromacs.org.
37 * Tests for ObservablesReducer.
39 * \ingroup module_mdtypes
43 #include "gromacs/mdtypes/observablesreducer.h"
49 #include <gmock/gmock.h>
50 #include <gtest/gtest.h>
52 #include "gromacs/utility/arrayref.h"
53 #include "gromacs/utility/basedefinitions.h"
54 #include "gromacs/utility/stringutil.h"
61 TEST(ObservablesReducerTest, CanMoveAssign)
63 ObservablesReducerBuilder builder;
64 ObservablesReducer observablesReducer = builder.build();
65 EXPECT_TRUE(observablesReducer.communicationBuffer().empty())
66 << "no buffer available when no subscribers requested reduction";
69 TEST(ObservablesReducerTest, CanMoveConstruct)
71 ObservablesReducerBuilder builder;
72 ObservablesReducer observablesReducerOriginal = builder.build();
73 ObservablesReducer observablesReducer(std::move(observablesReducerOriginal));
74 EXPECT_TRUE(observablesReducer.communicationBuffer().empty())
75 << "no buffer available when no subscribers requested reduction";
78 TEST(ObservablesReducerTest, CanBuildAndUseWithNoSubscribers)
80 ObservablesReducerBuilder builder;
82 ObservablesReducer observablesReducer = builder.build();
83 EXPECT_TRUE(observablesReducer.communicationBuffer().empty())
84 << "no buffer available when no subscribers requested reduction";
85 observablesReducer.reductionComplete(0);
87 EXPECT_TRUE(observablesReducer.communicationBuffer().empty())
88 << "no buffer available after reductionComplete()";
89 observablesReducer.stepComplete();
92 TEST(ObservablesReducerTest, CanBuildAndUseWithOneSubscriber)
94 ObservablesReducerBuilder builder;
96 // This test implements the caller, the builder and the
97 // ObservablesReducer all in the one scope, which likely does not
98 // resemble any actual use case. More realistic test cases are
101 std::optional<int> stepUponWhichReductionOccured;
102 ObservablesReducerBuilder::CallbackToRequireReduction callbackToRequireReduction;
103 ArrayRef<double> bufferView;
104 ObservablesReducerBuilder::CallbackFromBuilder callbackFromBuilder =
105 [&](ObservablesReducerBuilder::CallbackToRequireReduction&& c, ArrayRef<double> b) {
106 callbackToRequireReduction = std::move(c);
110 ObservablesReducerBuilder::CallbackAfterReduction callbackAfterReduction =
111 [&stepUponWhichReductionOccured](Step step) { stepUponWhichReductionOccured = step; };
112 const int requiredBufferSize = 2;
113 builder.addSubscriber(
114 requiredBufferSize, std::move(callbackFromBuilder), std::move(callbackAfterReduction));
116 ObservablesReducer observablesReducer = builder.build();
117 EXPECT_TRUE(observablesReducer.communicationBuffer().empty())
118 << "no buffer available when no subscribers requested reduction";
119 ASSERT_EQ(requiredBufferSize, bufferView.size());
120 ASSERT_NE(callbackToRequireReduction, nullptr)
121 << "must have valid callback supplied by the builder";
122 EXPECT_FALSE(stepUponWhichReductionOccured.has_value())
123 << "no callbacks until reductionComplete() is called";
125 // Fill some dummy data, so we can check the zeroing later
130 SCOPED_TRACE("Test that ReductionRequirement::Eventually doesn't trigger behavior");
132 EXPECT_EQ(callbackToRequireReduction(ReductionRequirement::Eventually),
133 ObservablesReducerStatus::ReadyToReduce);
134 EXPECT_TRUE(observablesReducer.communicationBuffer().empty())
135 << "no buffer available when the only subscribers requested reduction eventually";
136 EXPECT_FALSE(stepUponWhichReductionOccured.has_value())
137 << "no callbacks until reductionComplete() is called";
139 // Note that there's nothing else to check here, because the
140 // empty buffer means that no reduction should take place.
141 observablesReducer.stepComplete();
144 SCOPED_TRACE("Test that ReductionRequirement::Soon does trigger behavior");
146 EXPECT_EQ(callbackToRequireReduction(ReductionRequirement::Soon),
147 ObservablesReducerStatus::ReadyToReduce);
148 EXPECT_EQ(observablesReducer.communicationBuffer().size(), requiredBufferSize)
149 << "buffer available when a subscriber requested reduction soon";
150 EXPECT_FALSE(stepUponWhichReductionOccured.has_value())
151 << "no callbacks until reductionComplete() is called";
153 // In the intended use case, some external component must do
154 // the actual reduction across ranks using the buffer at this
155 // point. Here, we just pretend it happened.
158 observablesReducer.reductionComplete(step);
159 ASSERT_TRUE(stepUponWhichReductionOccured.has_value()) << "reduction callbacks took place";
160 EXPECT_EQ(stepUponWhichReductionOccured.value(), step)
161 << "reduction step is passed through correctly";
162 EXPECT_THAT(bufferView, testing::AllOf(testing::SizeIs(requiredBufferSize), testing::Each(0.0)))
163 << "buffer is zeroed after reduction";
164 observablesReducer.stepComplete();
168 // Integration tests of ObservablesReducer, builder, and fake
169 // subscriber(s). These will model multiple ranks each with multiple
170 // subscribers. Building tests that used actual MPI would be extra
171 // complexity that is not needed at this time.
173 //! Helper class that models an MD module that needs to make a subscription to \c ObservablesReducer
177 //! Ensure that each subscriber sends an interesting amount of data
178 static constexpr int s_subscriberBufferMinimumSize = 3;
179 /*! \brief Base value used to ensure the data reduced by each
180 * subscriber is distinct, to help diagnose bugs.
182 * Also contributes to ensuring that the reduced total is
185 * Note that in a real use case, the subscribers will generally be
186 * located in multiple modules. */
187 static constexpr double s_subscriberOffset = 1000;
189 Subscriber(int subscriberIndex, int numRanks) :
190 // Ensure each subscriber sends a different amount of data, to expose bugs
191 sizeRequired_(s_subscriberBufferMinimumSize + subscriberIndex),
192 // Ensure each subscriber sends a distinct range of data, to expose bugs
193 valueOffset_(s_subscriberOffset * (subscriberIndex + 1)),
195 subscriberIndex_(subscriberIndex)
199 //! Make the subscription via the \c observablesReducerBuilder
200 void makeSubscription(ObservablesReducerBuilder* observablesReducerBuilder)
202 observablesReducerBuilder->addSubscriber(
204 [this](ObservablesReducerBuilder::CallbackToRequireReduction callback,
205 ArrayRef<double> bufferView) {
206 this->callbackWhenBufferAvailable(std::move(callback), bufferView);
208 [this](Step step) { this->callbackAfterReduction(step); });
211 //! Callback to receive data from the builder
212 void callbackWhenBufferAvailable(ObservablesReducerBuilder::CallbackToRequireReduction&& callbackToRequireReduction,
213 ArrayRef<double> bufferView)
215 SCOPED_TRACE("In callback from builder");
217 callbackToRequireReduction_ = std::move(callbackToRequireReduction);
218 communicationBuffer_ = bufferView;
219 EXPECT_THAT(communicationBuffer_, testing::AllOf(testing::SizeIs(sizeRequired_), testing::Each(0.0)))
220 << "size of buffer did not match request";
223 //! Pretend to do some simulation work characteristic of \c step
224 void doSimulationWork(Step step, ReductionRequirement reductionRequirement) const
226 // In a real case, MD simulation work for this PP rank and
227 // step would go here.
229 // Then we put values that model its intermediate output into
230 // the communication buffer. Those values vary with the step,
231 // so that we can test for correctness over multiple reduction
233 std::iota(communicationBuffer_.begin(), communicationBuffer_.end(), valueOffset_ + double(step));
234 // Then we require reduction.
235 EXPECT_EQ(callbackToRequireReduction_(reductionRequirement), ObservablesReducerStatus::ReadyToReduce);
238 //! After the reduction, check the values for this subscriber are as expected
239 void callbackAfterReduction(Step step)
241 SCOPED_TRACE(formatString("In callback after reduction for subscriber %d", subscriberIndex_));
243 // Expected values are different for each subscriber, and
244 // vary with step and number of ranks.
245 std::vector<double> expectedResult(communicationBuffer_.size());
246 std::iota(expectedResult.begin(), expectedResult.end(), valueOffset_ + double(step));
247 std::for_each(expectedResult.begin(), expectedResult.end(), [this](auto& v) {
248 v *= this->numRanks_;
250 EXPECT_THAT(communicationBuffer_, testing::Pointwise(testing::Eq(), expectedResult))
251 << "wrong values were reduced";
252 // Ensuring that zero is never computed by a reduction helps
253 // test that the zeroing of the communication buffer is
254 // working correctly, as we will only observe zero after
255 // zeroing and no subsequent activity.
256 EXPECT_THAT(communicationBuffer_, testing::Not(testing::Each(0)))
257 << "zero may not be the result of an reduction during testing";
260 //! The number of doubles required to reduce
262 //! The callback used to require reduction
263 ObservablesReducerBuilder::CallbackToRequireReduction callbackToRequireReduction_;
264 //! The buffer used for communication, supplied by an \c ObservablesReducer
265 ArrayRef<double> communicationBuffer_;
266 //! Offset that differentiates the values reduced by each subscriber
268 //! Number of ranks, used in constructing test expectations
270 //! Index within the group of subscribers
271 int subscriberIndex_;
274 //! Test fixture class
275 class ObservablesReducerIntegrationTest : public testing::TestWithParam<std::tuple<int, int>>
278 //! Helper struct to model data on a single MPI rank
281 /*! \brief Builder of \c observablesReducer for this "rank,"
282 * valid until after its build() method has been called. */
283 std::optional<ObservablesReducerBuilder> builder = ObservablesReducerBuilder{};
284 //! Subscribers to \c observablesReducer
285 std::vector<Subscriber> subscribers;
286 /*! \brief Manages reduction of observables on behalf of this
287 * "rank", valid only after the ObserbalesReducerBuilder
289 std::optional<ObservablesReducer> observablesReducer;
293 ObservablesReducerIntegrationTest() : numSubscribers_(std::get<0>(GetParam()))
295 int numRanks(std::get<1>(GetParam()));
297 rankData_.resize(numRanks);
298 for (auto& rankData : rankData_)
300 for (int i = 0; i < numSubscribers_; ++i)
302 // Ensure each subscriber sends a different (but small) amount of data
303 rankData.subscribers.emplace_back(Subscriber(i, numRanks));
305 // Now that the addresses of the subscribers are
306 // stable, set up the build-time callback.
307 for (auto& subscriber : rankData.subscribers)
309 subscriber.makeSubscription(&rankData.builder.value());
314 /*! \brief Performs the equivalent of MPI_Allreduce on the
315 * communication buffer over \c rankData_ */
316 void fakeMpiAllReduce()
318 std::vector<double> reducedValues(
319 rankData_[0].observablesReducer.value().communicationBuffer().size(), 0.0);
320 // Reduce the values across "ranks"
321 for (auto& rankData : rankData_)
323 for (size_t i = 0; i != reducedValues.size(); ++i)
325 reducedValues[i] += rankData.observablesReducer.value().communicationBuffer()[i];
328 // Copy the reduced values to all "ranks"
329 for (auto& rankData : rankData_)
331 auto buffer = rankData.observablesReducer.value().communicationBuffer();
332 std::copy(reducedValues.begin(), reducedValues.end(), buffer.begin());
336 //! The number of subscribers
338 //! Models data distributed over MPI ranks
339 std::vector<RankData> rankData_;
342 TEST_P(ObservablesReducerIntegrationTest, CanBuildAndUseSimply)
344 for (auto& rankData : rankData_)
346 rankData.observablesReducer = rankData.builder.value().build();
347 rankData.builder.reset();
348 EXPECT_TRUE(rankData.observablesReducer.value().communicationBuffer().empty())
349 << "no buffer available when no subscribers requested reduction";
353 for (auto& rankData : rankData_)
355 for (auto& subscriber : rankData.subscribers)
357 subscriber.doSimulationWork(step, ReductionRequirement::Soon);
359 EXPECT_EQ(numSubscribers_ == 0, rankData.observablesReducer.value().communicationBuffer().empty())
360 << "buffer should be available only when there are active subscribers";
363 // This does reduction work, and calls the callbacks that check
364 // the buffer contents.
367 for (auto& rankData : rankData_)
369 rankData.observablesReducer.value().reductionComplete(step);
370 EXPECT_TRUE(rankData.observablesReducer.value().communicationBuffer().empty())
371 << "no buffer available after reductionComplete()";
372 rankData.observablesReducer.value().stepComplete();
376 TEST_P(ObservablesReducerIntegrationTest, CanBuildAndUseOverMultipleSteps)
378 for (auto& rankData : rankData_)
380 rankData.observablesReducer = rankData.builder.value().build();
381 rankData.builder.reset();
382 EXPECT_TRUE(rankData.observablesReducer.value().communicationBuffer().empty())
383 << "no buffer available when no subscribers requested reduction";
386 for (Step step = 0; step < 20; step += 10)
388 for (auto& rankData : rankData_)
390 for (auto& subscriber : rankData.subscribers)
392 subscriber.doSimulationWork(step, ReductionRequirement::Soon);
394 EXPECT_EQ(numSubscribers_ == 0,
395 rankData.observablesReducer.value().communicationBuffer().empty())
396 << "buffer should be available only when there are subscribers";
399 // This does reduction work, and calls the callbacks that
400 // check the buffer contents.
403 for (auto& rankData : rankData_)
405 rankData.observablesReducer.value().reductionComplete(step);
406 EXPECT_TRUE(rankData.observablesReducer.value().communicationBuffer().empty())
407 << "no buffer available after reductionComplete()";
408 rankData.observablesReducer.value().stepComplete();
413 TEST_P(ObservablesReducerIntegrationTest, CanBuildAndUseWithoutAllNeedingReduction)
415 if (numSubscribers_ == 0)
417 // Test is meaningless with no subscribers
421 for (auto& rankData : rankData_)
423 rankData.observablesReducer = rankData.builder.value().build();
424 rankData.builder.reset();
425 EXPECT_TRUE(rankData.observablesReducer.value().communicationBuffer().empty())
426 << "no buffer available when no subscribers requested reduction";
429 // Only one subscriber does work leading to reduction
430 size_t subscriberNeedingReduction = 0;
432 for (auto& rankData : rankData_)
434 auto& subscriber = rankData.subscribers[subscriberNeedingReduction];
435 subscriber.doSimulationWork(step, ReductionRequirement::Soon);
436 EXPECT_FALSE(rankData.observablesReducer.value().communicationBuffer().empty())
437 << "buffer should be available when there is an active subscriber";
440 // This does reduction work, and calls the callbacks that check
441 // the buffer contents.
444 // Check that other subscribers didn't reduce anything
445 for (auto& rankData : rankData_)
447 for (size_t r = 0; r != rankData.subscribers.size(); ++r)
449 if (r == subscriberNeedingReduction)
453 EXPECT_THAT(rankData.subscribers[r].communicationBuffer_, testing::Each(0.0))
454 << "buffer for non-subscribers should be zero";
458 for (auto& rankData : rankData_)
460 rankData.observablesReducer.value().reductionComplete(step);
461 EXPECT_TRUE(rankData.observablesReducer.value().communicationBuffer().empty())
462 << "no buffer available after reductionComplete()";
463 rankData.observablesReducer.value().stepComplete();
467 TEST_P(ObservablesReducerIntegrationTest, CanBuildAndUseWhenASubscriberUsesEventually)
469 if (numSubscribers_ < 2)
471 // Test is meaningful only with multiple subscribers
475 for (auto& rankData : rankData_)
477 rankData.observablesReducer = rankData.builder.value().build();
478 rankData.builder.reset();
479 EXPECT_TRUE(rankData.observablesReducer.value().communicationBuffer().empty())
480 << "no buffer available when no subscribers requested reduction";
483 // Only one subscriber does work leading to reduction
484 size_t subscriberUsingEventually = 1;
486 for (auto& rankData : rankData_)
488 auto& subscriber = rankData.subscribers[subscriberUsingEventually];
489 subscriber.doSimulationWork(step, ReductionRequirement::Eventually);
490 EXPECT_TRUE(rankData.observablesReducer.value().communicationBuffer().empty())
491 << "buffer should not be available when the only active subscriber used "
492 "ReductionRequirement::Eventually";
495 // This will do nothing, as all the communication buffers are
496 // empty, but we can't directly test that nothing
497 // occured. Instead, we will later do some
498 // ReductionRequirement::Soon work and observe that result is
499 // consistent with exactly one reduction.
502 for (auto& rankData : rankData_)
504 for (size_t i = 0; i != rankData.subscribers.size(); ++i)
506 if (i == subscriberUsingEventually)
510 rankData.subscribers[i].doSimulationWork(step, ReductionRequirement::Soon);
512 EXPECT_FALSE(rankData.observablesReducer.value().communicationBuffer().empty())
513 << "buffer should be available since there are subscribers";
516 // This does reduction work, and calls the callbacks that check
517 // the buffer contents.
520 for (auto& rankData : rankData_)
522 rankData.observablesReducer.value().reductionComplete(step);
523 EXPECT_TRUE(rankData.observablesReducer.value().communicationBuffer().empty())
524 << "no buffer available after reductionComplete()";
525 rankData.observablesReducer.value().stepComplete();
529 //! Help GoogleTest name our test cases
530 std::string namesOfTests(const testing::TestParamInfo<ObservablesReducerIntegrationTest::ParamType>& info)
532 // NB alphanumeric characters only
533 return formatString("numSubscribers%dnumRanks%d", std::get<0>(info.param), std::get<1>(info.param));
535 INSTANTIATE_TEST_SUITE_P(WithVariousSubscriberCounts,
536 ObservablesReducerIntegrationTest,
537 testing::Combine(testing::Values(0, 1, 2, 3), // subscriber counts
538 testing::Values(1, 2, 3)), // rank counts
542 } // namespace gmx::test