Planner, gate and fallback¶
These are the planner's inputs: the keyword arguments of fm.Simulation
that choose the tier and the checks, the fm.Workload record, and five
configuration records:
FallbackConfig: what happens when a build, compile, capture, self-check or gate fails;GateConfig: where the gate's float64 references may run;RunnerConfig: the child process that runs a plan this process cannot;AdaptationConfig: runtime tier switches;CalibrationConfig: the optional backend measurement before a run.
The planner guide explains how to read a plan. This page lists every option. Engine and neighbour-list options are on Engine and neighbour list.
import fastmace as fm
from fastmace.config import AdaptationConfig, FallbackConfig, GateConfig, RunnerConfig
model = fm.load("mh1", head="matpes_r2scan")
sim = fm.Simulation(
atoms, model,
integrator=fm.integrators.Langevin(temperature_K=600, friction_per_fs=0.02),
seed=1,
workload=fm.Workload(max_temperature_K=900),
check="gate",
fallback=FallbackConfig(fallback="refuse"),
gate=GateConfig(cpu_float64_time_limit_s=600),
runner=RunnerConfig(stall_timeout_s=3600),
adaptation=AdaptationConfig(adapt="off"),
)
print(sim.plan) # tier, backend, precision and the decisions that matter
print(sim.gate) # the gate record
fm.Workload is in the fm namespace. The other records are imported from
fastmace.config. Every record is frozen and checks its values when you
build it (ValueError). Options the planner cannot honour raise
PlanRefused with every reason when the Simulation is built, before
anything runs.
fm.md takes mode=, allow_reference=, check=, seed=, fallback=,
gate=, runner=, engine= and neighbours= and passes them on
unchanged. It builds the Workload from its own arguments (the highest
target temperature of the schedule, and the declared MC moves), and takes
calibrate=True instead of a CalibrationConfig. It has no adaptation=
or allow_naive_cueq=; use fm.Simulation for those.
The options are recorded in simulation.json next to the restart.
Simulation.resume plans with the same options.
Tiers¶
A tier is the way the engine executes each step. Every tier runs FastMACE's own integrators, noise, output and restart, so a trajectory has the same physics on every tier.
| Tier | What runs | When the planner picks it |
|---|---|---|
graph |
The compiled model, with each stage of a step recorded once as a CUDA graph and replayed. Edges are padded to buckets so that one graph fits many steps. Fastest. | The default for MD and relaxations. On a CPU device the same engine runs its stages without graphs. |
compiled |
The compiled model without CUDA graphs. Exact edge counts; a new edge count does not compile again. Each step is launched from the host. | Without mode=, when padded edges would change this model's energies (padding is not exact). With mode="compiled". As the target of a runtime switch. |
eager |
The graph tier's backend, rewrites and precision, with no compilation and no CUDA graphs. Exact edge counts. | With mode="eager". When the installed MACE cannot capture this model's readout (some multi-head models), even for mode="graph". As a fallback candidate. |
reference |
MACE's own calculator through the versioned reference record: stock uncompiled cuEquivariance model, no rewrites, no padding, MACE's own neighbour list. Slowest. | With mode="reference". As the last fallback candidate, if it fits on the device and reference_last=True. |
Host callbacks (Workload.host_callbacks_every) keep the graph tier, with a
host synchronisation at each callback: the compiled tier measured 1.3–4.7×
slower with a callback every 1–10 steps (MH-1, hybrid float32, NaCl and
NbCl5 on an RTX 5060 Ti).
Backends¶
| Backend | What it is | When EngineConfig.backend="auto" picks it |
|---|---|---|
hybrid |
cuEquivariance for the symmetric contraction and linear layers, OpenEquivariance kernels for the convolutions (message passing). Most rewrites need it. Float32 by default. | OpenEquivariance can build, the device is CUDA, MACE was not imported before FastMACE prepared the OpenEquivariance runtime, and the process has not compiled a plain cuEq model. |
cueq |
Plain cuEquivariance throughout, with its fused convolution unless the plan is reproducible. | Every other case, with the reason in the plan. reproducible=True always uses it. |
One process can hold compiled models of only one of the two backends, because
MACE's compile setting is process-wide. A plan of the other backend runs in
a child process through the subprocess runner (RunnerConfig). Calibration
measures the two backends against each other on your structure.
Simulation keyword arguments¶
| Option | Values | Default (auto means) |
What it does | Notes |
|---|---|---|---|---|
mode |
None, "graph", "compiled", "eager", "reference" |
None: the graph tier, or the compiled tier when padding is not exact for the model |
Asks for a tier. | An explicit "graph" with inexact padding is refused. An explicit mode also stops runtime tier switches. |
check |
"skip", "gate", "allow_unavailable" |
"skip" |
Whether the gate runs before the first step. "gate": a failed gate goes to the fallback, or raises GateFailed; a gate whose reference cannot run raises GateUnavailable. "allow_unavailable": as "gate", but an unavailable reference is recorded and the run goes on. |
MC runs always run the move-energy part of the gate, whatever check is. The record is sim.gate. |
allow_reference |
True, False |
False |
Recorded in the plan. | It does not lift any refusal and is not needed for mode="reference". |
allow_naive_cueq |
True, False |
False |
Accepts cuEquivariance's slow fallback kernels when its fused kernels (cuequivariance_ops_torch) do not load. |
Without it, such a plan is refused with the import error. Recorded. |
seed |
int | required | The seed of the integrator's noise. MC streams derive their own seeds from it. | fm.md(seed=None) draws a seed and records it in metadata.json. |
workload |
fm.Workload or None |
None: fm.Workload(max_temperature_K=<integrator's temperature_K>); for a relaxation max_temperature_K=0 |
What the run will do; see fm.Workload. |
An integrator without a temperature (NVE) needs an explicit workload. A starting target above max_temperature_K is refused. |
engine |
EngineConfig or None |
None: the default planner's |
Engine options; see Engine and neighbour list. | A record you pass replaces the default as a whole. |
neighbours |
NeighbourConfig or None |
None: the default planner's |
Neighbour-list options; see Engine and neighbour list. | |
fallback |
FallbackConfig or None |
None: FallbackConfig() |
See FallbackConfig. |
|
gate |
GateConfig or None |
None: GateConfig() |
See GateConfig. |
The record is sim.gate_config; the result is sim.gate. |
runner |
RunnerConfig or None |
None: RunnerConfig() |
See RunnerConfig. |
|
adaptation |
AdaptationConfig or None |
None: AdaptationConfig() |
See AdaptationConfig. |
|
calibration |
CalibrationConfig or None |
None: CalibrationConfig() |
See CalibrationConfig. |
fm.Workload¶
The workload holds the facts about the run that no other object knows.
Periodicity and constraints come from the Atoms, the ensemble from the
integrator, and the precision and species superset from the ForceField.
| Option | Values | Default (auto means) |
What it does | Notes |
|---|---|---|---|---|
max_temperature_K |
float ≥ 0, K | required (Simulation fills it from the integrator) |
The highest thermostat target of the run. | Simulation.run refuses a schedule that goes above it. For MC and species-change runs it sizes the skin and the pair buffers; for plain MD it is recorded. |
species_changes |
True, False |
False |
Declares that species will change (swaps, transmutations, species events). | Required for "swap" and "transmute" moves. Keeps the graph tier and builds species-dependent tables for the whole superset. Makes the planner size the skin and buffers from the workload. |
atom_count_changes |
True, False |
False |
Declares that atoms will be added or removed. | Accepted by the record and refused by the planner: not supported in 1.0. |
min_mc_temperature_K |
float > 0, K, or None |
None |
The lowest MC target temperature. Sets the move-energy gate's limit, 0.01 k_B T_min. | Required when mc_moves is not empty. Attaching MC moves while the target temperature is below it is refused. |
host_callbacks_every |
int > 0 or None |
None |
Declares that user Python runs every n steps. | Keeps the graph tier, with a host synchronisation at each callback. The ASE front end sets it from its attached observers. |
needs_per_atom |
set of names, for example {"energies", "charges"} |
empty | Per-atom outputs the run needs. | Recorded in the plan. |
mc_moves |
tuple of "swap", "transmute", "displace" |
() |
The MC move types the run will attach through Simulation.run(mc=...). |
Attaching a move type not declared here is refused. Declaring moves turns on the move-energy gate. |
reproducible |
True, False |
False |
Asks for bitwise reproducible trajectories on the same hardware, driver, software, model, plan, seeds and restart cadence. | Qualified only for plain cuEq with the unfused convolution, the MH-1 architecture, the graph or eager tier and the checked cuequivariance_ops version; everything else is refused with the reason. The process is set to deterministic mode before CUDA starts; if CUDA already started, the plan runs in a child process. |
mc_energy_precision |
"auto", "float32", "float64" |
"auto": the force precision if the move-energy gate passes, else float64 |
The precision of MC trial energies. MD forces keep their own precision. | "float32" with float64 forces is refused. |
max_displacement_A |
float > 0, Å, or None |
None |
The largest displacement any Displace move of the run may use, adaptive steps included. |
Required when "displace" is in mc_moves. Adds 2 × this value to the skin. A Displace with a larger delta_A is refused. |
The gate¶
The gate checks the planned engine on your structure before the first step.
It is opt-in: check="gate".
- Copies. The gate makes 2 copies of the structure, each rattled by 0.05 Å. For a variable-cell integrator it adds 2 copies strained by up to ±2 % and then rattled.
- References. On each copy it evaluates MACE's own model in float64 (the reference) and in float32 with the energy summed in float64 (the yardstick). These run before the production engine is built, or in a child process.
- Production. It evaluates each copy through the planned engine, in the mode it will run (captured graphs on the graph tier), and restores the engine's state bit for bit afterwards.
- Rule. For energy per atom, forces and stress, the production error against the float64 reference, maximised over all copies, must be at most 2 × the yardstick's error, or at most the floor: 0.01 meV/atom, 1×10⁻⁵ eV/Å (largest force component), 1×10⁻⁷ eV/ų (largest stress component; 5×10⁻⁷ eV/ų for PolarMACE). Any NaN or infinity fails. NVT plans have no stress path and record "stress: not in plan".
Move energies. A run with MC moves always runs the move-energy part,
whatever check is. It samples trial moves (2 swaps, 1 transmutation,
4 displacements per declared type) and requires every
|ΔE_production − ΔE_reference| ≤ 0.01 k_B T_min, which keeps acceptance
probabilities within about 1 %. With mc_energy_precision="auto", a
failure switches the MC energies to float64.
Resources. If the float64 reference runs out of GPU memory, the gate
tries, in order: the CPU in float64 on the full cell (if its estimated time
is within cpu_float64_time_limit_s and its memory fits); a reduced cell
on the GPU; the reduced cell on the CPU. A pass on a reduced cell is
recorded as passed-reduced. A reference that fails for any reason other
than memory makes the gate unavailable at once.
GateConfig¶
| Option | Values | Default (auto means) |
What it does | Notes |
|---|---|---|---|---|
cpu_float64_time_limit_s |
float ≥ 0, s | 300.0 |
After a GPU out-of-memory, the CPU float64 reference is tried only if its estimated time is within this limit. | 0 never tries the CPU. The estimate comes from timing two small boxes cut from your structure, in the same run. |
reduced_cell |
"auto", "off" |
"auto": the largest supercell of the same primitive cell, smaller than the production cell, whose every height is at least reduced_cell_min_height_cutoffs × the cutoff |
Allows the reference on a reduced cell when the full cell does not fit. | "off": never; the gate is unavailable instead. On a reduced cell the production path is evaluated by a separate uncaptured force field with the same rewrites and precision, and the captured replay is checked against it on the full cell. |
reduced_cell_min_height_cutoffs |
float ≥ 1 | 2.0 |
The reduced cell's smallest height, in cutoffs. | Below 1 a periodic image would sit inside the cutoff. |
FallbackConfig¶
The fallback acts when something fails before the first step: the build,
compilation, the first CUDA graph capture, a rewrite self-check, or the
gate. With fallback="auto" it searches for the closest plan that passes:
- Diagnose. It runs the failing check again on the eager tier with the same rewrites. If eager passes, the CUDA graphs or compilation were the problem and the tier changes. If not, it narrows down the rewrite set (turning rewrites off in groups, respecting their dependencies), each try checked with a reduced gate: one rattled copy, the failing quantity only.
- Budget. At most
diagnosis_budgetreduced-gate runs. Each candidate that would become active also gets a full gate run, counted apart. - Terminal sequence. When the budget is spent: the same tier with
every rewrite off; eager with every rewrite off; the reference tier if
it fits and
reference_last=True. - Nothing passes.
FallbackExhaustedwith every candidate tried.
Candidates are built from the pristine model, never by undoing rewrites in
place. A candidate becomes active only after it passed. The physics
(integrator, force precision, structure) never changes. Every candidate is
listed in sim.plan.fallbacks and in metadata.json. With check="skip"
the fallback still validates its candidate against float64 references; the
gate record stays skipped.
During run(), a capture error at a re-capture or a compile error on the
compiled tier starts the same search from the last committed restart
point. A second such failure before the next restart point stops the run
with FallbackExhausted. Out-of-memory errors, non-finite values,
refusals and process constraints never start the fallback.
| Option | Values | Default (auto means) |
What it does | Notes |
|---|---|---|---|---|
fallback |
"auto", "refuse" |
"auto": search for a validated slower plan and record it |
What a failure does. "refuse" raises the original error, with the record. |
No effect on a run where nothing fails. |
diagnosis_budget |
int > 0 | 64 |
The most reduced-gate runs of one fallback. | Full gate runs (one per candidate that would become active) are not counted. |
reference_last |
True, False |
True |
Allows the reference tier as the last candidate, if it fits on the plan's device: the gate's reference ran there on the full cell, or a trial evaluation runs. | False ends the search with FallbackExhausted instead. |
RunnerConfig¶
Some plans cannot run in the current process:
- a compiled model of the other backend after this process compiled one;
reproducible=Trueafter CUDA was started in this process, or a plan that is not reproducible in a process that was made reproducible.
The planner then marks the plan runner="subprocess". The Simulation
builds nothing in this process; a child process plans, gates and builds,
and the two hand the state over through restart files in
<output>/.fastmace-handoff/ (or the cache's handoff/ without output).
sim.plan, sim.gate, sim.report and metadata.json reflect the
child. The runner is also used for calibration.
| Option | Values | Default (auto means) |
What it does | Notes |
|---|---|---|---|---|
python |
path or None |
None: this process's sys.executable |
The child's Python interpreter. | The child imports the same FastMACE as the parent. |
environment |
mapping or (name, value) pairs of strings |
() |
Environment variables set in the child, over the inherited ones. | A mapping is stored as sorted pairs. |
inherit_environment |
True, False |
True |
The child starts from the parent's environment. | False: from environment alone. |
timeout_s |
float > 0, s, or None |
None: no limit |
The total time limit of one child run. | A child past it is killed; SubprocessRunnerError is raised. |
stall_timeout_s |
float > 0, s | 1800.0 |
A child with no progress (no committed step, read from its heartbeat file) for this long is killed. | The heartbeat changes when the child's phase or committed step changes. One long setup phase (compilation, the gate's reference) counts as no progress, so keep this above your longest setup phase. |
persistent |
True, False |
True |
One child serves every run() of the Simulation and keeps its compiled engine. |
False: a fresh child for each run(), resuming from the last hand-off. Windows always uses fresh children. Both write the same restart points. |
AdaptationConfig¶
During a run the monitor reads the engine's counters at each restart point
and at the end of each run(): re-captures and their time, overflows
redone, neighbour-list rebuilds. These are in metadata.json and the
report's runtime line.
The implemented adaptation is one switch from the graph tier to the compiled tier. It happens when both:
- the run has made more re-captures than
EngineConfig.recapture_budget; - re-capturing took more than
recapture_time_fractionof the wall time since the start (or the last switch).
The switch is made at the next committed step outside an MC block, which becomes a restart point. The new plan has the same backend, rewrites, precision and physics; it is a new plan revision, validated before it becomes active. A resume rebuilds the same revision. A run that never reaches the trigger is unchanged.
There is no switch with reproducible=True, with an explicit mode=, for
a relaxation, or on any tier other than graph.
Refused values. The record can hold more options than are
implemented. Simulation refuses, with PlanRefused (code
adaptation-unsupported), any record where:
triggeris"recapture-budget";skin_replanisTrue;max_switchesisNoneor above 1;switch_back_afteris notNone;min_points_betweenis notNone;reproducibleisTrue.
This holds even with adapt="off". To turn adaptation off, change only
adapt (or trigger="none", or max_switches=0).
| Option | Values | Default (auto means) |
What it does | Notes |
|---|---|---|---|---|
adapt |
"auto", "off" |
"auto": switch as described above |
Whether the monitor may switch tiers. | "off": the monitor still records. |
trigger |
"recapture-time", "none"; "recapture-budget" is refused |
"recapture-time" |
What fires a switch. "recapture-time": budget exceeded and re-capture time above recapture_time_fraction. "none": no switch. |
|
recapture_time_fraction |
float, > 0 and < 1 | 0.2 |
The share of wall time spent re-capturing above which an exceeded budget switches tiers. | Measured in the run, so it holds on any GPU. |
skin_replan |
False only |
False |
Reserved: re-plan the skin from the measured rebuild rate. | True is refused. |
max_switches |
0, 1 |
1 |
The most tier switches one run may make. A resume counts the switches already in its plan history. | 0 disables switches. None (no limit) and values above 1 are refused. |
switch_back_after |
None only |
None: switches are one way |
Reserved: go back to the graph tier after this many quiet segments. | Any other value is refused. |
min_points_between |
None only |
None |
Reserved: a minimum number of restart points between two switches. | Any other value is refused. |
reproducible |
False only |
False |
Reserved: allow switches under reproducible=True. |
True is refused. |
CalibrationConfig¶
Calibration is off by default. With calibrate=True the run first measures
the two backends on your structure, when the rules rank them equal: the
planner chose hybrid because OpenEquivariance builds, and plain cuEq is
also allowed. It does nothing when you set EngineConfig.backend, for
reproducible runs, for the reference tier, or when the plan runs in a child
process.
Each measurement is a fresh child process that builds the plan, runs 50
warm-up steps, then times steps steps. The blocks alternate hybrid, cuEq,
hybrid, cuEq (2 pairs). Plain cuEq replaces hybrid only when it is faster
by more than margin in every pair. The four child builds are the
cost; the report shows it as setup: calibration. Nothing is stored or
reused by another run. The chosen backend is recorded, so a resume plans
the same one; a resume never calibrates.
calibrate=True is refused (code calibration-workload) for relaxations
and for runs with MC moves: their step time is not a steady MD step.
fm.md(calibrate=True) sets it.
| Option | Values | Default (auto means) |
What it does | Notes |
|---|---|---|---|---|
calibrate |
True, False |
False |
Measures the backends before the run. | MD runs only. |
steps |
int ≥ 1 | 50 |
Timed steps per block, after the warm-up. | |
margin |
float, > 0 and < 1 | 0.05 |
How much faster plain cuEq must be, in every pair, to replace hybrid. | The default is above the cross-process timing noise of about 3 %. |
choose |
True only |
True |
The measurement may change the backend. | False (measure and record only) is not implemented: Simulation refuses it with code calibration-unsupported, even with calibrate=False. |
When to change these¶
- Debugging a failure. Pass
mode="eager": same backend, rewrites and precision, no compilation and no graphs, so a traceback points at the failing operation.fallback=FallbackConfig(fallback="refuse")raises the original error instead of searching for another plan. - New model or new structure. Run once with
check="gate". If the gate is unavailable on a large cell and you accept running without it, usecheck="allow_unavailable". Raisecpu_float64_time_limit_sif the CPU reference is declined for time and you can wait. - Gate is slow. The gate's float64 reference dominates setup on large
cells. It is a one-off cost per run; leave
check="skip"for production runs of a validated model and structure. - MC runs. Declare every move type in
Workload.mc_moves, withmin_mc_temperature_Kand, for displacements,max_displacement_A. Setmax_temperature_Kto the highest target of the whole run, not of the first segment. - Hot or changing structures. If re-captures are frequent, the graph
tier loses its advantage. Leave
adaptationat its default, so the run can move to the compiled tier, or planmode="compiled"yourself. - Long setups in a child process. A child that compiles and runs the
gate on a large cell can take a long time before its first step. Raise
stall_timeout_sif it is killed during setup, and settimeout_son shared machines so that a stuck child cannot hold the GPU. - Persistent runner. Keep
persistent=True: one child keeps its compiled engine acrossrun()calls. UseFalseonly to get a fresh process perrun(), for example to release GPU memory between calls. - Comparing backends on one structure. Use
calibrate=True(orfm.md(calibrate=True)) for an MD run. It costs four extra builds, so use it for long runs only.