Fix ENERGY CV gromacs2024/25 + fix ENERGY CV with multi MPI and negative float to double (v2.10, any MD engine) - #1445
Open
invemichele-peptone wants to merge 6 commits into
Conversation
GROMACS 2024 snapshots force_flags into stepWork before the PLUMED isEnergyNeeded query, so ENERGY was left at 0 except on nstcalcenergy steps. Rebuild the workload when PLUMED needs the potential energy. Also apply the float-to-double rounding to the magnitude of the value so negative energies are not turned into NaN in mixed precision. Fixes plumed#1205 Co-authored-by: Cursor <cursoragent@cursor.com>
The GROMACS 2025 PLUMED module is an IForceProvider and never calls setEnergy, so ENERGY silently stayed 0 for the whole run. Two obstacles: nothing could request GMX_FORCE_ENERGY per step, and calculateForces() runs before accumulatePotentialEnergies(), so neither the energy nor the complete force buffer exist yet. Add two defaulted IForceProvider hooks: requestsPotentialEnergy(step), called before setupStepWorkload() so the energy can still be scheduled, and applyAfterPotentialEnergy(), called once F_EPOT is accumulated and the total force is assembled. PLUMED answers the first by running the prepareDependencies() half of prepareCalc(), which needs only the step number, and the shareData() half later from calculateForces(). Energy biasing then rescales the total force in the late hook, using the same virial convention as the legacy patches. See plumed#1205 Co-authored-by: Cursor <cursoragent@cursor.com>
Energy minimisation and mdrun -rerun do reach applyAfterPotentialEnergy() with a valid potential energy, so listing them as unsupported was wrong. The only path that cannot supply the energy is the modular simulator, which bypasses the md.cpp loop where the energy is requested, and which is the default for the md-vv integrator. Point users at GMX_DISABLE_MODULAR_SIMULATOR=1 instead. Co-authored-by: Cursor <cursoragent@cursor.com>
The manual stated that ENERGY does not include long tail corrections. For GROMACS that is not the case: the correction is computed inside do_force() and folded into F_EPOT by sum_epot() before PLUMED reads it. Verified against the GROMACS energy file with DispCorr=EnerPres on 2023.5, 2024.3 and 2026.3, in NVT and in NPT where the volume varies by up to 29% and the correction itself by 40 kJ/mol: PLUMED tracks the GROMACS potential energy to 0.005 kJ/mol over 401 frames. The claim is left in place for other codes, which were not tested. Addresses plumed#567 Co-authored-by: Cursor <cursoragent@cursor.com>
Biasing ENERGY makes PLUMED rescale the forces of the MD code, but the rescaling ran over the total number of atoms instead of the atoms that are local to the rank. With domain decomposition the buffer of the MD code only holds the local atoms, so this wrote past its end and corrupted the heap as soon as a bias acted on ENERGY on more than one rank, which is what OPES multithermal and the well-tempered ensemble do. The branch that asks the domain decomposition for the local number of forces could never be reached, since it was guarded by a check on the action name that does not hold for the posx/posy/posz values that are rescaled, and it would have hit its own assertion if it had been. Use the flag that marks the values coming from the domains instead. Co-authored-by: Cursor <cursoragent@cursor.com>
Keep the note short and engine-agnostic: what matters to the user is that the energy PLUMED receives may not contain the long tail corrections, and that the energy of the MD engine should then be used for reweighting. Co-authored-by: Cursor <cursoragent@cursor.com> (cherry picked from commit 35689f2)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix ENERGY with GROMACS 2024, and support it with the native GROMACS 2025 interface
Branch:
fix-1205-gromacs2024-energy-v2.10→ basev2.10· Fixes: #1205 (and the root cause behind #1384)Companions: the same 2024 fix against
v2.9; the GROMACS 2026 counterpart stacked on #1439,which needs part 4 below and so should be merged after this one
Description
Energy-based PLUMED input is broken or unavailable on every GROMACS newer than 2023:
ENERGYsilently returns0.0except onnstcalcenergysteps (ENERGY bug, gromacs 2024 + plumed 2.9 #1205).setEnergy, soENERGYstays0.0for thewhole run with no error. The GROMACS manual lists "interaction with GROMACS energy" under the
interface Limitations.
from
floattodoubleby rounding them throughstd::log10, which isNaNfor a negativeargument. The energy is negative, so wherever it did arrive it arrived as
NaN.ENERGYwrites past the end ofthe force buffer of the MD code and corrupts the heap. This one is a v2.10 regression in the
core and is not specific to GROMACS.
This PR fixes all four, so
ENERGY,OPES_EXPANDEDwithECV_MULTITHERMAL, the well-temperedensemble and energy reweighting behave on 2024 and 2025 as they do on 2023.
Part 1 — GROMACS 2024:
ENERGYis zero on most steps. The patch asks for the energy byOR-ing bits into
force_flagsafterisEnergyNeeded. In 2023 that worked becauseforce_flagswent straight into the
do_force()call below; in 2024do_force()no longer takes flags andthe workload is snapshotted ~120 lines earlier by
setupStepWorkload(), so the PLUMED linebecame dead code.
do_force()zeroesterm[F_EPOT]every step and only refills it whenstepWork.computeEnergyis set, so PLUMED received0.0on 99 steps out of 100 with the defaultnstcalcenergy = 100. The virial was lost the same way, so the force rescaling used for energybiasing was wrong too. In
patches/gromacs-2024.3.diff/.../md.cpp: carry the request forwardbefore
setupStepWorkload(); rebuildrunScheduleWork_->stepWorkright afterisEnergyNeededanswers for the current step, which makes the fix exact rather than one step late;
gmx_fatalifthe energy is still not scheduled; treat
plumedNeedsEnergylikebCalcVirin the MD GPU graphreset and reuse conditions.
Part 2 — the
floattodoubleconversion returnsNaNfor negative scalars.DataPassingObjectTyped<float>::saveValueAsDoublerounds to 6 significant figures usingstd::log10(bvalue), to keep single-precision values from acquiring meaningless digits whenwidened.
std::log10isNaNfor a negative argument and-inffor zero, which then gives0/0, so any negative or zero scalar coming from a single-precision engine is turned intoNaN.The potential energy is essentially always negative, so
ENERGYarrived asNaNand poisonedevery bias built on it. Fixed by taking the magnitude from
std::fabs(bvalue)and leaving exactzeros alone. Nothing here is specific to GROMACS or to
ENERGY, butENERGYis the only valueaffected in practice: array data keeps the pointer of the MD code and is never rounded,
timestepand
kBTtake the same path but are strictly positive, andEXTRACVvalues are declaredMUTABLE, which sends them down the pointer branch instead.Part 3 — GROMACS 2025: teaching the native interface about the energy. From 2025 the
interface is an
IForceProviderrather than a source patch, and three things block energybiasing: nobody can ask GROMACS to compute the energy, since a force provider cannot influence
stepWork; atcalculateForces()time the energy does not exist yet, as it is called fromcomputeSpecialForces()several hundred lines beforeaccumulatePotentialEnergies(); and theforce-rescaling trick is not expressible, because biasing the energy multiplies the entire
force array by
1 - ∂V/∂E(Energy::apply()→rescaleForces) while a provider only owns aseparate additive
ForceWithVirialbuffer. Two hooks are added toIForceProvider, bothdefaulting to no-ops so no other module is affected:
md.cppcalls the first immediately beforesetupStepWorkload(), the last moment the workloadcan change. The answer must be exact for this step rather than inherited from the previous one,
so
requestsPotentialEnergy()splitsprepareCalc()in two: whether PLUMED needs the energy isknown after
prepareDependencies(), and that half needs only the step number(
setStepLong→prepareDependencies→isEnergyNeeded, the sequencesrc/generic/Plumed.cppalready uses, so no new PLUMED command is required).
calculateForces()runs the second half(
shareData) once positions are available, falling back to the fullprepareCalc()when theearly hook did not run for this step — which is what makes energy minimisation and
mdrun -rerunwork, since neither goes through the
md.cpploop but both always compute the energy.sim_util.cppcalls the second hook afteraccumulatePotentialEnergies()andpostProcessForces(),where
term[F_EPOT]is valid and the total force buffer is complete; there PLUMED doessetEnergy/setForceson the total force /setVirial/performCalc, with the same2 ×/
0.5 ×virial convention and virial replacement as the legacy 2023 and 2024 patches. Whenthe energy is not needed the existing additive path is unchanged.
Part 4 — energy biasing overran the force buffer under domain decomposition. Applying a force
on
ENERGYmakes PLUMED rescale all of the forces of the MD code by1 - ∂V/∂E(
Energy::apply()→ActionToPutData::rescaleForces). The loop length comes fromgetNumberOfForcesToRescale(), which returnedcopyOutput(0)->getNumberOfValues()— the totalnumber of atoms — while under domain decomposition the buffer of the MD code only holds the atoms
local to the rank. On 4 ranks PLUMED therefore wrote about four times past the end of that buffer,
which shows up as
double free or corruptionor a segfault the moment the bias switches on. Thedomain-aware branch that returns the local count exists but is unreachable: it is guarded by
getName()!="ENERGY", whereas the values actually being rescaled are theposx/posy/poszobjects created as
PUT FROM_DOMAINS, and it would have hit its ownplumed_assert(getDependencies().size()==1)if it had ever been entered. The fix keys the branchoff the existing
from_domainsflag instead. v2.9 is not affected — it rescales overgatindex,i.e. the local atoms — so this is a v2.10 regression introduced with the new data-passing
architecture, and it is present in 2.10.1 and in current
masteras well.This is the same combination as #1205: OPES multithermal and the well-tempered ensemble are
precisely the inputs that put a force on
ENERGY, so anyone running them on more than one rankwas hitting silent memory corruption.
Verification
216 SPC waters (648 atoms), OPLS-AA, 0.8 nm cut-offs,
dt = 2 fs, v-rescale at 300 K, LINCS onh-bonds,
nstcalcenergy = 100unless stated, mixed precision. GROMACS 2023.5 with the legacypatch is the reference throughout, since it predates the regression and works as shipped.
ENERGYevery step, 250 stepsENERGY == 0∂V/∂Enon-zero after observationDeltaFat 350 K after 3 ps (kJ/mol)Unpatched 2024.3 manages 4 / 251, exactly the
nstcalcenergysteps. The temperature count is thesharpest check, since OPES sizes that grid from the energy fluctuations seen during observation, so
a zero or intermittent energy collapses it to 2 temperatures; a control at
nstcalcenergy = 1picks the same 10. Trajectories are not bit-identical across GROMACS versions, so the residual
DeltaFspread is MD noise. Also checked:PRINT ARG=ene STRIDE=7againstnstcalcenergy = 100,a stride sharing no factor with the energy period and the case an inherited, one-step-late request
cannot serve; restart via
-cpifrom step 150, not a multiple ofnstcalcenergy, with the energycontinuous across the restart;
DISTANCE+RESTRAINTunchanged, confirming the additive path isuntouched; and
plumed patch -p -e gromacs-2025.0applying to a pristine tarball with no fuzz andno rejects, with
plumed patch -rrestoring the tree byte-for-byte.Part 4 needs a bigger box, so that one uses 1728 SPC waters (5184 atoms) with
OpenMPI,
-DGMX_MPI=ON, on 1, 2 and 4 ranks (DD grids2 x 1 x 1and4 x 1 x 1). Before thefix, every run with a force on
ENERGYdied —double free or corruptionon 2023.5 and on 2026.3alike, in NVT and NPT, always at the step the bias switched on;
ENERGYwithout a force, and aRESTRAINTon aDISTANCE, both survived, which is what isolates the rescaling. After the fix:DeltaFat 350 K agrees to 1 kJ/mol= 1 - ∂V/∂E, spread 2 × 10⁻⁵, same at 1 / 2 / 4 ranksENERGYvs.edrPotential,DispCorr = EnerPresregtest/basicandregtest/opesThe force-ratio row is the direct check: because a force on the energy only ever multiplies the
existing forces by a scalar, every atom on every rank must show the same ratio, and it must equal
1 - ∂V/∂E. It does. PLUMED 2.9 built with the same MPI stack was run through the same test andis clean at 1, 2 and 4 ranks, confirming the regression is 2.10-only.
Also: the long tail correction warning in the docs is stale (#567). While checking what the
energy PLUMED receives actually contains, it turned out that the
\bugnote onENERGY— "doesnot include long tail corrections … GROMACS
DispCorr Ener" — no longer holds for GROMACS. Thecorrection is computed inside
do_force()and folded intoF_EPOTbysum_epot(), both beforePLUMED reads the energy. With
DispCorr = EnerPres,ENERGYmatches thePotentialterm in theGROMACS energy file to 0.005 kJ/mol on 2023.5, 2024.3 and 2026.3, while the correction itself
is −154 kJ/mol. Under NPT, where the volume varies by up to 29% and the correction with it by
40 kJ/mol,
ENERGYstill tracks GROMACS to 0.005 kJ/mol over 401 frames — so the correction isnot merely present but correctly updated. The note is reworded rather than deleted, and kept short
and engine-agnostic: codes other than GROMACS, such as LAMMPS with
pair_modify tail yes, were nottested here, and where the mismatch is real the advice to reweight with the energy of the engine
still stands.
Remaining gaps. GPU was tested through the companion 2026 patch, which carries the identical
design, since GROMACS 2025 does not build against the CUDA toolkit available here:
-nb gpu,-nb gpu -pme gpu, fully GPU-resident-update gpuandGMX_CUDA_GRAPH=1all give correctenergies, matching GROMACS' own
.edrPotentialterm to ≤ 5 × 10⁻⁸ relative, and OPESmultithermal reproduces the CPU temperature grid and
DeltaF. The modular simulator is the oneunsupported path: it bypasses the
md.cpploop, sothe early hook never runs. It is the default for
md-vv, where energy-dependent input nowfails with a
NotImplementedErrornaming it and pointing atGMX_DISABLE_MODULAR_SIMULATOR=1;with that set,
md-vvworks. Input that does not use the energy is unaffected. Under MTS thetotal force is taken from the combined buffer only on slow steps. The two hooks are the minimal
shape of what would have to go upstream; GROMACS issue
#4939 does not list energy biasing among the
requirements for the PLUMED interface, which is probably why it was never designed in.
Target release
I would like my code to appear in release 2.10.
Type of contribution
Copyright
Tests
Parts 1 and 3 live entirely under
patches/, which PLUMED does not compile, so the test suitecannot exercise them; the GROMACS builds and MD runs above are the substitute. Part 2 is core
code and is covered by existing regtests, since it is on the path every float-precision engine
uses to pass a scalar. Part 4 is core code too, but no regtest can reach it today: the failure
needs
gatindex.size() < natoms, and with no MPI communicatorDomainDecomposition::shareAll()takes the branch that assumes every atom is local, so a serial fake-MD harness cannot set it up —
and the regtest framework has no multi-rank test type. It would need a small MPI harness; happy to
add one, here or separately, if you want the infrastructure.
regtest/basicandregtest/opeswere run with and without the fix and give identical results (the same two pre-existing failures,
rt-averageandrt-multi-1, in both).