Skip to content

Add a patch for GROMACS 2026 - #1439

Open
aalhossary wants to merge 1 commit into
plumed:v2.10from
Smith-Group:gromacs-2026-patch
Open

Add a patch for GROMACS 2026#1439
aalhossary wants to merge 1 commit into
plumed:v2.10from
Smith-Group:gromacs-2026-patch

Conversation

@aalhossary

@aalhossary aalhossary commented Jul 30, 2026

Copy link
Copy Markdown
Contributor
Description

There is no patch for GROMACS 2026, so plumed patch -e offers nothing newer than gromacs-2025.0
and there is no supported way to build a PLUMED-enabled GROMACS 2026.

Applying the 2025.0 patch to 2026.4 does not work:

$ plumed patch -p -e gromacs-2025.0            # on a pristine 2026.4 tree
patching file ./src/gromacs/applied_forces/plumed/plumedforceprovider.cpp
Hunk #1 succeeded at 43 with fuzz 5.
Hunk #3 FAILED at 113.
1 out of 4 hunks FAILED -- saving rejects to ...plumedforceprovider.cpp.rej
patching file ./src/gromacs/CMakeLists.txt
Reversed (or previously applied) patch detected!  Skipping patch.
2 out of 2 hunks ignored -- saving rejects to ./src/gromacs/CMakeLists.txt.rej

Six of the remaining files apply only with fuzz 5, i.e. by luck rather than by matching.

The cause is db61f87e0a ("Use MpiComm in t_commrec"), which replaced the t_commrec plumbing with
the MpiComm abstraction, so PlumedOptions now carries const MpiComm* mpiComm_ where it used to
carry const t_commrec* cr_. The failing hunk is the one that hands PLUMED the multi-replica
communicators, and losing it is worse than a build error: GREX setMPIIntercomm is what populates
PlumedMain::multi_sim_comm (GREX.cpp), so without it multi_sim_comm stays MPI_COMM_SELF and
every multi-replica action silently degrades to a single replica — no error, just wrong science.
The CMakeLists.txt rejects are the opposite problem: GROMACS 2026 adopted the early
gmx_manage_plumed() call natively, so those hunks must simply be dropped.

This adds patches/gromacs-2026.0.diff, the 2025.0 patch carried onto the 2026 API:

  • the GREX/multisim block uses MpiComm::isMainRank() and MpiComm::comm()
  • src/gromacs/CMakeLists.txt is dropped entirely (already upstream in 2026)
  • setNumOMPthreads is carried over from Restore setNumOMPthreads in the GROMACS 2025 patch #1435
  • the replex_ plumbing, the PLUMED_LOG_FILE override and forcing GMX_USE_PLUMED=ON are
    unchanged in substance
It also fixes a bug in GROMACS's own PLUMED module

The same commit rewrote

-        plumed_->cmd("setMPIComm", &options.cr_->mpi_comm_mygroup);
+        plumed_->cmd("setMPIComm", options.mpiComm_->comm());

dropping the address-of. MpiComm::comm() returns MPI_Comm by value, but PLUMED reads that
argument as *(const MPI_Comm*) (Communicator::Set_comm). Where MPI_Comm is a pointer type — as
in Open MPI — this compiles and silently hands PLUMED the wrong handle, so a domain-decomposed run
looks fine and is not. The patch passes the address of a named local instead. I will report this to
GROMACS separately; until it is fixed there, the patch is the only thing protecting 2026 users.

Verification

On GROMACS 2026.4 with clang 20.1.1, Open MPI 5.0.7 and libc++:

  • plumed patch -p -e gromacs-2026.0 applies with no fuzz and no rejects, and plumed patch -r
    restores the tree byte-for-byte.

  • The patched GROMACS builds clean with GMX_MPI=ON -DGMX_THREAD_MPI=OFF -DGMX_OPENMP=ON; the four
    changed translation units compile with no errors and no warnings.

  • Multi-replica actually works. A two-replica mdrun -multidir rep0 rep1 with

    d: DISTANCE ATOMS=1,2
    r: RESTRAINT ARG=d AT=@replicas:0.10,0.99 KAPPA=100.0
    

    @replicas: is expanded using multi_sim_comm.Get_rank(), so it is a direct probe of whether the
    communicator was set up. Patched, the two replicas parse different values, and the log reports
    GROMACS-like replica exchange is on:

    rep0   PLUMED:   at 0.100000
    rep1   PLUMED:   at 0.990000
    

    As a control I built the same GROMACS 2026.4 commit unpatched (GMX_USE_PLUMED=ON, native
    module) and ran the identical input. Both replicas take the rank-0 value and the GREX line is
    absent:

    rep0   PLUMED:   at 0.100000
    rep1   PLUMED:   at 0.100000        <- same as rep0
    

    Both runs exit 0. That is the failure mode worth emphasising: unpatched, every replica believes it
    is replica 0, and nothing in the output says so.

Target release

I would like my code to appear in release 2.10.

This is filed against v2.10 so that it forward-merges v2.10 -> master in the usual way. GROMACS 2026
is already released, and users on PLUMED 2.10 have no way to patch it today, so the support is more
useful on the release branch than held back for the next feature release. patches/gromacs-2025.0.diff
already lives on v2.10, so this sits beside it rather than being split across branches. It adds a new
patches/ directory and touches nothing existing, so the forward merge is conflict-free.

Scope for 2027: the plumed module sources on GROMACS main are currently identical to
release-2026, and the runner.cpp and mdmodulesnotifiers.h edits apply there unchanged; only the
stored .preplumed snapshots would need regenerating once 2027 branches. Happy to add that patch too
when it makes sense.

Type of contribution
  • changes to code or doc authored by PLUMED developers
  • changes to a module not authored by you
  • new module contribution or edit of a module authored by you
Copyright
  • I agree to transfer the copyright of the code I have written to the PLUMED developers or to the
    author of the code I am modifying.
  • the code I am contributing is not mine and I am making it available under LGPL
Tests
  • I added a new regtest or modified an existing regtest to validate my changes.
  • I verified that all regtests are passed successfully on GitHub Actions.

No regtest is added, for the same reason the existing patches/ tree carries none: nothing under
patches/ is compiled by PLUMED, so there is no way for the test suite to exercise it. It is consumed
only when a user runs plumed patch against a GROMACS source tree, at which point GROMACS — not
PLUMED — compiles it. The verification above is the substitute, and it is a real GROMACS build and a
real two-replica MD run rather than a source-level check.

@aalhossary
aalhossary changed the base branch from master to v2.10 July 31, 2026 08:02
@aalhossary

Copy link
Copy Markdown
Contributor Author

I changed the target branch from master to 2.10 to allow more versions to use it.

@carlocamilloni

Copy link
Copy Markdown
Member

@aalhossary but the source branch should match v2.10 as well

There is no patch for GROMACS 2026, so `plumed patch -e` offers nothing newer than
gromacs-2025.0 and there is no supported way to build a PLUMED-enabled GROMACS 2026.

Applying the 2025.0 patch to 2026.4 does not work. GROMACS replaced the t_commrec
plumbing with the MpiComm abstraction in db61f87e0a ("Use MpiComm in t_commrec"), so
PlumedOptions now carries `const MpiComm* mpiComm_` instead of `const t_commrec* cr_`:

    $ plumed patch -p -e gromacs-2025.0        # on a 2026.4 tree
    patching file ./src/gromacs/applied_forces/plumed/plumedforceprovider.cpp
    Hunk plumed#3 FAILED at 113.
    1 out of 4 hunks FAILED -- saving rejects to ...plumedforceprovider.cpp.rej
    patching file ./src/gromacs/CMakeLists.txt
    Reversed (or previously applied) patch detected!  Skipping patch.
    2 out of 2 hunks ignored -- saving rejects to ./src/gromacs/CMakeLists.txt.rej

and six further files apply only with fuzz 5. The failing hunk is the one that hands
PLUMED the multi-replica communicators, which is the most damaging thing to lose
silently: `GREX setMPIIntercomm` is what populates PlumedMain::multi_sim_comm
(GREX.cpp), so without it every multi-replica action degrades to a single replica
without any error.

The new patch is the 2025.0 one carried onto the 2026 API:

  * the GREX/multisim block uses MpiComm::isMainRank() and MpiComm::comm()
  * src/gromacs/CMakeLists.txt is dropped entirely -- GROMACS 2026 already calls
    gmx_manage_plumed() early, which is why those hunks reported as already applied
  * setNumOMPthreads is carried over from plumed#1435
  * the replex_ plumbing, the PLUMED_LOG_FILE override and forcing GMX_USE_PLUMED=ON
    are unchanged in substance

It also fixes one bug in GROMACS's own code. The same MpiComm commit rewrote

    -        plumed_->cmd("setMPIComm", &options.cr_->mpi_comm_mygroup);
    +        plumed_->cmd("setMPIComm", options.mpiComm_->comm());

dropping the address-of. MpiComm::comm() returns MPI_Comm by value, but PLUMED reads
that argument as `*(const MPI_Comm*)` (Communicator::Set_comm), so where MPI_Comm is a
pointer type this compiles and silently passes the wrong handle. The patch passes the
address of a named local instead. This is worth reporting to GROMACS separately; until
it is fixed there, any domain-decomposed PLUMED run on 2026 is affected.

Verified on GROMACS 2026.4 with clang 20.1.1, OpenMPI 5.0.7 and libc++:

  * `plumed patch -p -e gromacs-2026.0` applies with no fuzz and no rejects, and
    `plumed patch -r` restores the tree byte-for-byte
  * the patched GROMACS builds clean (GMX_MPI=ON, GMX_THREAD_MPI=OFF, GMX_OPENMP=ON);
    the four changed translation units compile with no errors and no warnings
  * a two-replica `mdrun -multidir` run expands `@replicas:` to a different value per
    replica, which is only possible when multi_sim_comm has been set up

Scope for 2027: the plumed module sources on GROMACS main are currently identical to
release-2026, and the runner.cpp and mdmodulesnotifiers.h edits apply there unchanged;
only the stored .preplumed snapshots would need regenerating once 2027 branches.
@aalhossary
aalhossary force-pushed the gromacs-2026-patch branch from 831a00e to 34dcd1c Compare July 31, 2026 09:59
@aalhossary

Copy link
Copy Markdown
Contributor Author

Sorry. It slipped out of my mind (being just files addition).
I rebased on 2.10 and it should be fine now.
When both PRs related to patches are merged, may you propagate them to the master branch, please?

@aalhossary

Copy link
Copy Markdown
Contributor Author

Reading this PR: 17 files, but only 98 lines actually change

GitHub renders every file here as "new file, +N", so the diff view shows ~8800 additions and gives
no way to see what the patch actually does. That is an artifact of the storage format, not of the
change, so here is the effective diff and a per-file rationale.

Why the files come in pairs. patches/patch.sh builds the diff at apply time from two stored
copies of each file:

diff -U 5 "$diff/$bckfile" "$diff/$file" --label="$bckfile" --label="$file" |
patch -u -l -b -F 5 -N --suffix=.preplumed "$file"

So every patched file must be stored twice, in full. There is no option to store a diff instead. For
scale, this is the smallest of the GROMACS patches in the tree:

patch files stored lines lines actually changed
gromacs-2026.0 (this PR) 17 8794 98
gromacs-2025.0 18 9456 65
gromacs-2024.3 30 36517 654

It also carries one fewer build file than gromacs-2025.0: src/gromacs/CMakeLists.txt is gone,
because GROMACS 2026 adopted the early gmx_manage_plumed() call natively — applying the 2025 patch
to 2026 reports those hunks as "Reversed (or previously applied)".

What each file is for

file changed why it is needed
cmake/gmxManagePlumed.cmake 4 makes PLUMED mandatory once the tree is patched — see below
src/gromacs/applied_forces/plumed/plumedforceprovider.cpp 48 the substance: GREX/multisim setup, the setMPIComm fix, setNumOMPthreads, PLUMED_LOG_FILE, the replex guard
src/gromacs/applied_forces/plumed/plumedOptions.h 25 carries ms_ and replex_ in the options struct
src/gromacs/applied_forces/plumed/plumedMDModule.cpp 9 subscribes to the multisim notification, which is what supplies ms_
src/gromacs/applied_forces/plumed/plumedOptions.cpp 8 the two setters
src/gromacs/applied_forces/plumed/plumedforceprovider.h 1 the replex_ member
src/gromacs/mdrunutility/mdmodulesnotifiers.h 2 transports replex_ from the runner to the module
src/gromacs/mdrun/runner.cpp 1 the only place that knows -replex was given
total 98

On the cmake file, since it is the one that looks least related. GROMACS 2026's own logic is that
OFF skips the block entirely, so the patched module is never compiled, while AUTO — the stock
default — falls back to message(STATUS "PLUMED needs dlopen ... Disabling support.") and continues
the build. Only ON promotes that to FATAL_ERROR. Without this four-line change, someone who has
explicitly run plumed patch can still end up with a GROMACS binary containing none of the patch,
announced by nothing louder than a STATUS line. The 2025 patch makes the same change for the same
reason.

The three smallest files (runner.cpp, mdmodulesnotifiers.h, plumedforceprovider.h) exist only
to plumb the -replex flag through to the force provider, where it guards against combining replica
exchange with a PLUMED-computed bias. They account for most of the stored bulk while changing four
lines. I have kept them for parity with the 2025 patch: dropping them would shrink the PR by ~83% but
would turn a clear NotImplementedError into silently wrong results for anyone using -replex with a
bias. Happy to drop them if you would rather not carry that guard on 2026.

The effective diff — all 98 changed lines
--- a/cmake/gmxManagePlumed.cmake
+++ b/cmake/gmxManagePlumed.cmake
@@ -41,8 +41,8 @@
 
 gmx_option_multichoice(GMX_USE_PLUMED
     "Build the PLUMED wrapper with GROMACS"
-    AUTO
-    AUTO ON OFF)
+    ON
+    ON)
 mark_as_advanced(GMX_USE_PLUMED)
 
 function(gmx_manage_plumed)
--- a/src/gromacs/applied_forces/plumed/plumedforceprovider.cpp
+++ b/src/gromacs/applied_forces/plumed/plumedforceprovider.cpp
@@ -41,10 +41,14 @@
 
 #include "plumedforceprovider.h"
 
+#include <cstdlib>
+
 #include "gromacs/domdec/domdec.h"
 #include "gromacs/domdec/domdec_struct.h"
 #include "gromacs/math/units.h"
+#include "gromacs/mdlib/gmx_omp_nthreads.h"
 #include "gromacs/mdrunutility/handlerestart.h"
+#include "gromacs/mdrunutility/multisim.h"
 #include "gromacs/mdtypes/enerdata.h"
 #include "gromacs/mdtypes/forceoutput.h"
 #include "gromacs/utility/exceptions.h"
@@ -70,7 +74,7 @@
 
 PlumedForceProvider::~PlumedForceProvider() = default;
 PlumedForceProvider::PlumedForceProvider(const PlumedOptions& options)
-try : plumed_(std::make_unique<PLMD::Plumed>())
+try : plumed_(std::make_unique<PLMD::Plumed>()), replex_(options.replex_)
 {
     // I prefer to pass a struct with data because it stops the coupling
     // at the implementation and not at the function signature:
@@ -114,14 +118,45 @@
         }
     }
 
+    if (plumedAPIversion_ > 5)
+    {
+        /* Tell PLUMED how many OpenMP threads GROMACS is using, so that it can
+           parallelise its own work. Without this PLMD::OpenMP::getNumThreads()
+           always reports 1 unless PLUMED_NUM_THREADS is set by hand. */
+        int numOmpThreads = gmx_omp_nthreads_get(ModuleMultiThread::Default);
+        plumed_->cmd("setNumOMPthreads", &numOmpThreads);
+    }
+
+    if (isMultiSim(options.ms_))
+    {
+        /* Hand PLUMED the multi-replica communicators. This is what populates
+           PLMD::PlumedMain::multi_sim_comm, without which every multi-replica
+           action silently degrades to a single replica. */
+        if (options.mpiComm_->isMainRank())
+        {
+            plumed_->cmd("GREX setMPIIntercomm", &options.ms_->mainRanksComm_);
+        }
+        MPI_Comm intracomm = options.mpiComm_->comm();
+        plumed_->cmd("GREX setMPIIntracomm", &intracomm);
+        plumed_->cmd("GREX init", nullptr);
+    }
+
     if (options.mpiComm_->isParallel())
     {
-        plumed_->cmd("setMPIComm", options.mpiComm_->comm());
+        /* PLUMED dereferences this argument as an MPI_Comm*, so it must be given
+           the address of a communicator, not the communicator itself. */
+        MPI_Comm intracomm = options.mpiComm_->comm();
+        plumed_->cmd("setMPIComm", &intracomm);
     }
 
     plumed_->cmd("setNatoms", options.natoms_);
     plumed_->cmd("setMDEngine", "gromacs");
-    plumed_->cmd("setLogFile", "PLUMED.OUT");
+    {
+        const char*       logFileEnv = std::getenv("PLUMED_LOG_FILE");
+        const std::string logFile =
+                (logFileEnv && logFileEnv[0] != '\0') ? logFileEnv : "PLUMED.OUT";
+        plumed_->cmd("setLogFile", logFile.c_str());
+    }
 
     plumed_->cmd("setTimestep", &options.simulationTimeStep_);
     plumed_->cmd("init", nullptr);
@@ -190,6 +225,17 @@
     // Do the work
     plumed_->cmd("performCalc", nullptr);
 
+    if (replex_)
+    {
+        double bias = 0.0;
+        plumed_->cmd("getBias", &bias);
+        if (bias != 0.0)
+        {
+            GMX_THROW(NotImplementedError("The PLUMED patch is still not compatible"
+                                          " with the replica exchange if PLUMED computes biases"));
+        }
+    }
+
     msmul(plumed_vir, 0.5, plumed_vir);
     forceProviderOutput->forceWithVirial_.addVirialContribution(plumed_vir);
 }
--- a/src/gromacs/applied_forces/plumed/plumedforceprovider.h
+++ b/src/gromacs/applied_forces/plumed/plumedforceprovider.h
@@ -86,6 +86,7 @@
 private:
     std::unique_ptr<PLMD::Plumed> plumed_;
     int                           plumedAPIversion_;
+    bool                          replex_;
 
     std::optional<ArrayRef<const int>> globalAtomIndices_;
 };
--- a/src/gromacs/applied_forces/plumed/plumedMDModule.cpp
+++ b/src/gromacs/applied_forces/plumed/plumedMDModule.cpp
@@ -88,7 +88,14 @@
         // Access the plumed filename this is used to activate the plumed module
         notifier->simulationSetupNotifier_.subscribe(
                 [this](const PlumedInputFilename& plumedFilename)
-                { this->options_.setPlumedFile(plumedFilename.plumedFilename_); });
+                {
+                    this->options_.setPlumedFile(plumedFilename.plumedFilename_);
+                    this->options_.setReplex(plumedFilename.replex_);
+                });
+        // Retrieve the multi-simulation object, needed to set up PLUMED's
+        // multi-replica communicator
+        notifier->simulationSetupNotifier_.subscribe([this](const gmx_multisim_t* ms)
+                                                     { this->options_.setMultisim(ms); });
         // Access the temperature if it is constant during the simulation
         notifier->simulationSetupNotifier_.subscribe(
                 [this](const EnsembleTemperature& ensembleT)
--- a/src/gromacs/applied_forces/plumed/plumedOptions.cpp
+++ b/src/gromacs/applied_forces/plumed/plumedOptions.cpp
@@ -91,6 +91,16 @@
     opts_.mpiComm_ = &mpiComm;
 }
 
+void PlumedOptionProvider::setReplex(bool replex)
+{
+    opts_.replex_ = replex;
+}
+
+void PlumedOptionProvider::setMultisim(const gmx_multisim_t* ms)
+{
+    opts_.ms_ = ms;
+}
+
 bool PlumedOptionProvider::active() const
 {
     return opts_.active_;
--- a/src/gromacs/applied_forces/plumed/plumedOptions.h
+++ b/src/gromacs/applied_forces/plumed/plumedOptions.h
@@ -49,6 +49,7 @@
 
 struct gmx_domdec_t;
 struct gmx_mtop_t;
+struct gmx_multisim_t;
 
 namespace gmx
 {
@@ -58,13 +59,15 @@
 
 struct PlumedOptions
 {
-    std::string         plumedFile_;
-    int                 natoms_;
-    const MpiComm*      mpiComm_;
-    real                simulationTimeStep_;
-    std::optional<real> ensembleTemperature_{};
-    StartingBehavior    startingBehavior_{};
-    bool                active_{ false };
+    std::string           plumedFile_;
+    int                   natoms_;
+    const MpiComm*        mpiComm_;
+    const gmx_multisim_t* ms_{ nullptr };
+    real                  simulationTimeStep_;
+    std::optional<real>   ensembleTemperature_{};
+    StartingBehavior      startingBehavior_{};
+    bool                  active_{ false };
+    bool                  replex_{ false };
 };
 
 class PlumedOptionProvider
@@ -87,6 +90,14 @@
      *  @param  fname the (optional) name of the file
      */
     void setPlumedFile(const std::optional<std::string>& fname);
+    /*! @brief Sets the replica-exchange flag
+     * @param replex true when mdrun was started with -replex
+     */
+    void setReplex(bool replex);
+    /*! @brief Sets the address of the multi-simulation object
+     * @param ms the address of the multi-simulation object (may be nullptr)
+     */
+    void setMultisim(const gmx_multisim_t* ms);
     /*! @brief Sets the timestep
      * @param timeStep the timestep value
      */
--- a/src/gromacs/mdrun/runner.cpp
+++ b/src/gromacs/mdrun/runner.cpp
@@ -1134,6 +1134,7 @@
         gmx::PlumedInputFilename plumedFilename;
         if (opt2bSet(plumedOptionName, filenames.size(), filenames.data()))
         {
+            plumedFilename.replex_ = replExParams.exchangeInterval > 0;
             plumedFilename.plumedFilename_ =
                     std::string(opt2fn(plumedOptionName, filenames.size(), filenames.data()));
         }
--- a/src/gromacs/mdrunutility/mdmodulesnotifiers.h
+++ b/src/gromacs/mdrunutility/mdmodulesnotifiers.h
@@ -306,6 +306,8 @@
 {
     //! The name of plumed input file, empty by default
     std::optional<std::string> plumedFilename_{};
+    //! Whether replica exchange is active (-replex), needed by PLUMED's GREX setup
+    bool replex_{};
 };
 
 /*! \libinternal \brief Provides the constant ensemble temperature

@aalhossary

Copy link
Copy Markdown
Contributor Author

@carlocamilloni the PR is ready now for your full review.
Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants