Use MpiContextManager to provide MPI communicator.
authorM. Eric Irrgang <ericirrgang@gmail.com>
Mon, 28 Sep 2020 12:35:43 +0000 (15:35 +0300)
committerMark Abraham <mark.j.abraham@gmail.com>
Tue, 29 Sep 2020 06:09:40 +0000 (06:09 +0000)
* Give MpiContextManager a MPI_Comm and make it movable so that it can
  be provided while creating the ContextImpl.
* In gmxapi::ContextImpl::launch(), use an MpiContextManager instance as
  the source for the SimulationContext communicator.

Refs #3644

api/gmxapi/cpp/context.cpp
api/gmxapi/cpp/context_impl.h

index c1555579a78148b52ce5e679a7f2b52ed81f34b9..befb90ad59b95027e6d6d2c2505f4225f8350f16 100644 (file)
 namespace gmxapi
 {
 
-MpiContextManager::MpiContextManager()
+MpiContextManager::MpiContextManager() :
+    communicator_(std::make_unique<MPI_Comm>(GMX_LIB_MPI ? MPI_COMM_WORLD : MPI_COMM_NULL))
 {
+    // Safely increments the GROMACS MPI initialization counter after checking
+    // whether the MPI library is already initialized. After this call, MPI_Init
+    // or MPI_Init_thread has been called exactly once.
     gmx::init(nullptr, nullptr);
     GMX_RELEASE_ASSERT(!GMX_LIB_MPI || gmx_mpi_initialized(),
                        "MPI should be initialized before reaching this point.");
+    if (this->communicator() != MPI_COMM_NULL)
+    {
+        // Synchronise at the point of acquiring a MpiContextManager.
+        gmx_barrier(this->communicator());
+    }
 };
 
 MpiContextManager::~MpiContextManager()
 {
-    // This is always safe to call. It is a no-op if
-    // thread-MPI, and if the constructor completed then the
-    // MPI library is initialized with reference counting.
-    gmx::finalize();
+    if (communicator_)
+    {
+        // This is always safe to call. It is a no-op if
+        // thread-MPI, and if the constructor completed then the
+        // MPI library is initialized with reference counting.
+        gmx::finalize();
+    }
+}
+
+MPI_Comm MpiContextManager::communicator() const
+{
+    if (!communicator_)
+    {
+        throw UsageError("Invalid MpiContextManager. Accessed after `move`?");
+    }
+    return *communicator_;
 }
 
 ContextImpl::~ContextImpl() = default;
@@ -101,24 +122,27 @@ ContextImpl::~ContextImpl() = default;
 Context createContext()
 {
     MpiContextManager contextmanager;
-    auto              impl = ContextImpl::create();
+    auto              impl = ContextImpl::create(std::move(contextmanager));
     GMX_ASSERT(impl, "ContextImpl creation method should not be able to return null.");
     auto context = Context(impl);
     return context;
 }
 
-ContextImpl::ContextImpl() noexcept(std::is_nothrow_constructible_v<gmx::LegacyMdrunOptions>) :
-    mpi_(MpiContextManager())
+ContextImpl::ContextImpl(MpiContextManager&& mpi) noexcept(std::is_nothrow_constructible_v<gmx::LegacyMdrunOptions>) :
+    mpi_(std::move(mpi))
 {
+    // Confirm our understanding of the MpiContextManager invariant.
+    GMX_ASSERT(mpi_.communicator() == MPI_COMM_NULL ? !GMX_LIB_MPI : GMX_LIB_MPI,
+               "Precondition is an appropriate communicator for the library environment.");
     // Make sure we didn't change the data members and overlook implementation details.
     GMX_ASSERT(session_.expired(),
                "This implementation assumes an expired weak_ptr at initialization.");
 }
 
-std::shared_ptr<ContextImpl> ContextImpl::create()
+std::shared_ptr<ContextImpl> ContextImpl::create(MpiContextManager&& mpi)
 {
     std::shared_ptr<ContextImpl> impl;
-    impl.reset(new ContextImpl());
+    impl.reset(new ContextImpl(std::move(mpi)));
     return impl;
 }
 
@@ -204,7 +228,10 @@ std::shared_ptr<Session> ContextImpl::launch(const Workflow& work)
         // lifetime. The gmx wrapper binary uses the same infrastructure,
         // but the lifetime is now trivially that of the invocation of the
         // wrapper binary.
-        auto              communicator = GMX_LIB_MPI ? MPI_COMM_WORLD : MPI_COMM_NULL;
+        auto communicator = mpi_.communicator();
+        // Confirm the precondition for simulationContext().
+        GMX_ASSERT(communicator == MPI_COMM_NULL ? !GMX_LIB_MPI : GMX_LIB_MPI,
+                   "Context communicator does not have an appropriate value for the environment.");
         SimulationContext simulationContext(communicator, multiSimDirectoryNames);
 
 
@@ -273,7 +300,7 @@ std::shared_ptr<Session> ContextImpl::launch(const Workflow& work)
 }
 
 // As of gmxapi 0.0.3 there is only one Context type
-Context::Context() : Context{ ContextImpl::create() }
+Context::Context() : Context{ ContextImpl::create(MpiContextManager()) }
 {
     GMX_ASSERT(impl_, "Context requires a non-null implementation member.");
 }
index 59acbaa67ec7c44138f8957889060c9cfb5e9c7c..7d912f4b81b54444a455e963187be27b15fc180a 100644 (file)
@@ -46,6 +46,7 @@
 
 #include "gromacs/mdrun/legacymdrunoptions.h"
 #include "gromacs/mdtypes/mdrunoptions.h"
+#include "gromacs/utility/gmxmpi.h"
 
 #include "gmxapi/context.h"
 #include "gmxapi/session.h"
@@ -54,23 +55,39 @@ namespace gmxapi
 {
 
 /*!
- * \brief Provide RAII management of communications resource state.
+ * \brief Provide RAII management of library MPI context.
  *
  * To acquire an MpiContextManager is to have assurance that any external MPI
  * environment is ready to use. When the MpiContextManager is released or
  * goes out of scope, the destructor finalizes the resources.
  *
+ * It is plausible that one or more ranks may not participate in an API session,
+ * but we have not historically handled such use cases at this level.
+ * For GROMACS built with an MPI library, the root communicator in the MpiContextManager
+ * must be valid. For other builds (e.g. tMPI), the communicator member must be MPI_COMM_NULL.
+ *
+ * If the client provides a communicator, the client promises to maintain the validity
+ * of the communicator for the life of the MpiContextManager.
+ *
  * Note that thread-MPI chooses the number of ranks and constructs its
  * MPI communicator internally, so does not and is unlikely to ever
  * participate here.
  *
- * \todo There is no resource for logging or reporting errors during initialization
+ * \todo Log errors during set up or tear down.
+ * There is no resource for logging or reporting errors during initialization or when misused.
  *
  * \ingroup gmxapi
  */
 class MpiContextManager
 {
 public:
+    /*!
+     * \brief Default constructor.
+     *
+     * Construct a valid instance with an appropriate default value for the
+     * base communicator. (Note that appropriate default value depends on whether
+     * the library was compiled with an external MPI library.)
+     */
     MpiContextManager();
 
     ~MpiContextManager();
@@ -85,13 +102,33 @@ public:
     //! \}
 
     /*!
-     * \brief Move semantics are non-trivial.
+     * \brief Transfer ownership of the managed GROMACS MPI context.
      *
      * \{
      */
-    MpiContextManager(MpiContextManager&&) noexcept = delete;
-    MpiContextManager& operator=(MpiContextManager&&) noexcept = delete;
+    MpiContextManager(MpiContextManager&& source) noexcept = default;
+    MpiContextManager& operator=(MpiContextManager&& source) noexcept = default;
     //! \}
+
+    /*!
+     * \brief Get the communicator for this context.
+     *
+     * \return Communicator with an appropriate initialized state for the current library
+     * configuration.
+     *
+     * \throws gmxapi::UsageError if MpiContextManager is in an invalid state, such as after
+     *  being moved from.
+     */
+    [[nodiscard]] MPI_Comm communicator() const;
+
+private:
+    /*!
+     * \brief The resources provided by this manager.
+     *
+     * A valid MpiContextManager has a value for communicator_ that is appropriate
+     * for the library configuration.
+     */
+    std::unique_ptr<MPI_Comm> communicator_;
 };
 
 /*!
@@ -127,7 +164,7 @@ public:
      *
      * \return ownership of a new object
      */
-    static std::shared_ptr<ContextImpl> create();
+    static std::shared_ptr<ContextImpl> create(MpiContextManager&& mpi);
 
     /*!
      * \brief Copy disallowed because Session state would become ambiguous.
@@ -206,6 +243,15 @@ public:
      * Part of the ContextImpl invariant establishes a point where MPI initialization status is
      * known.
      *
+     * Note that the MpiContextManager invariant is dependent on configure-time options to the
+     * GROMACS library. Specifically, MpiContextManager::communicator() is guaranteed to be
+     * MPI_COMM_NULL if and only if the library was built without an external MPI library.
+     * This is confusing and we should split either this class or the
+     * MpiContextManager class. Another alternative would be to use a std::optional here and to
+     * allow the std::unique_ptr<MpiContextManager> provided to ContextImpl::create() to be null,
+     * but that means a slight change of documented behavior protocol. See #3688 and #3650 for
+     * follow up.
+     *
      * To ensure the MpiContextManager is initialized only once, we use a const member that must
      * be initialized at construction.
      */
@@ -218,7 +264,7 @@ private:
      * Don't use this. Use create() to get a shared pointer right away.
      * Otherwise, shared_from_this() is potentially dangerous.
      */
-    explicit ContextImpl() noexcept(std::is_nothrow_constructible_v<gmx::LegacyMdrunOptions>);
+    explicit ContextImpl(MpiContextManager&& mpi) noexcept(std::is_nothrow_constructible_v<gmx::LegacyMdrunOptions>);
 };