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.
Thread-safe interface¶
Potential and Matter are not internally synchronized except where a
mutex is documented (Metatomic inference lock). The contract is:
After
makePotential, the pot is used from one thread unlessisThreadSafe()is true.Matteris not shared across threads. Parallel NEB uses one Matter (or one pot instance) per image.layoutFlags()says whether the pot is in-process, needs cwd, or is a subprocess. Do not share aNeedsWorkingDirectorypot across threads that would race on cwd.
Threading model¶
Callers should use the nonmember queries in eon/PotCapabilities.h
(potIsThreadSafe, potAllowsSharedInstance) rather than naming the
virtuals at every NEB/dimer site. The virtuals stay the override point
on each backend; the free functions are a C++20 concept-constrained
adapter so a later trait cut does not touch those TUs. The header is
header-only (no extra object file).
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(). 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 independently. Returns
needsPerImageInstance() = true.XTBPot:
isThreadSafe() = falseandneedsPerImageInstance() = false.restart.f90uses global Fortran unit numbers; two XTB environments in one process collide. Parallel NEB with XTB stays serial until upstream fixes unit management.
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 |
ASE, CatLearn |
|
|
Per-image instances, parallel threads |
MetatomicPotential (mutex fallback) |
|
|
Sequential evaluation |
XTB ( |
Where the answers come from¶
Potentials that reach eOn through RgpotAdapter take both flags from
caps().reentrancy on the rgpot kernel, so the answer lives next to the
physics.
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. The kernels
carry no mutable module state any more, so promoting one is a per-kernel
exercise in checking the kernel and adding a test.
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 (lock-free): 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 returntrueThe constructor (called by
makePotential()) must create an independent instance (no shared static state)
The [Main] parallel = true config option (default) enables threading.
Set parallel = false to force sequential evaluation.