Parallel Force Evaluation¶
eOn supports parallel force evaluation in NEB, Dimer/ImprovedDimer, and
ProcessSearchJob. The threading model uses std::thread with per-image
potential ownership.
Threading Model¶
Two virtual methods on Potential control the behavior:
isThreadSafe()¶
Returns whether the same potential instance can be called from multiple
threads concurrently. Most empirical potentials (LJ, Morse, SW, etc.)
return true (default). Python-based potentials (ASE, CatLearn) return
false because CPython has the GIL.
When true, NEB spawns one thread per image and all threads call
force() on the shared potential instance.
needsPerImageInstance()¶
Returns whether NEB should create a separate Potential instance per
image via makePotential(). This is needed for potentials where:
The same instance cannot be called concurrently (internal state, caches)
But separate instances CAN run in parallel (each has its own state)
Examples:
MetatomicPotential: PyTorch model has internal caches. Same instance needs a mutex; separate instances run truly in parallel. Returns
needsPerImageInstance() = true.XTBPot: Fortran library has per-instance state (
xtb_TEnvironment,xtb_TCalculator). Same instance is not thread-safe; separate instances are independent. ReturnsneedsPerImageInstance() = true.
When needsPerImageInstance() is true, NEB creates N+2 potential
instances (one per image) at construction time. The parallel force
evaluation then proceeds lock-free.
Decision Table¶
|
|
Behavior |
Examples |
|---|---|---|---|
|
|
Shared instance, parallel threads |
LJ, Morse, LJCluster, EMT |
|
|
Per-image instances, parallel threads |
XTB, ASE, metatomic |
|
|
Per-image instances, parallel threads |
MetatomicPotential (mutex fallback) |
|
|
Sequential evaluation |
SW, EDIP, Lenosky, Tersoff, EAM-Al, FeHe, CuH2, TIP4P-H |
Where the answers come from¶
Potentials that reach eOn through RgpotAdapter do not hard-code either
flag: the adapter reads caps().reentrancy from the rgpot kernel, so the
answer lives next to the physics rather than in a list on the caller side.
SharedInstance maps to isThreadSafe(), PerInstance to
needsPerImageInstance(), and ProcessSerial to neither, which is the
last row above.
The Fortran kernels all declare ProcessSerial today. That matches how
they were treated before the port – they were on the old hard-coded
thread-safety blacklist – and it is deliberately the conservative
starting point rather than a claim about the rewritten code: the kernels
carry no mutable module state any more, so promoting one is a per-kernel
exercise in proving it and adding a test, not a sweeping change.
The parallel check in NEB is:
bool canParallel = pot->isThreadSafe() || perImagePotentials_;
if (numImages > 1 && params.main_options.parallel && canParallel) { ... }
Affected Code Paths¶
Component |
Parallel Units |
Per-Image Potential |
|---|---|---|
NEB |
N images |
Each |
Dimer |
center + forward |
|
ImprovedDimer |
x0 + x1 |
|
ProcessSearchJob |
min1 + min2 |
|
Performance¶
With the Morse empirical potential (337-atom Pt, 5 NEB images), parallel force evaluation gives a 2.3x speedup over SVN sequential.
With PET-MAD-S ML potential (14-atom Claisen, 10 NEB images):
Mutex-serialized (shared instance): 192 seconds
Per-image instances (true parallel): 69 seconds (2.8x speedup)
Adding a New Potential¶
If your potential has internal state that prevents concurrent calls on the same instance but supports independent instances:
Override
isThreadSafe()to returntrue(with internal mutex as fallback) orfalseOverride
needsPerImageInstance()to returntrueEnsure the constructor (called by
makePotential()) creates an independent instance (no shared static state)
The [Main] parallel = true config option (default) enables threading.
Set parallel = false to force sequential evaluation.