npx skills add ...
npx skills add nvidia/nvalchemi-toolkit --skill nvalchemi-dynamics-hooks
How to use and write dynamics hooks — callbacks that observe or modify batch state at specific points during each simulation step. Use when a simulation needs neighbor-list rebuilds, convergence checks or early stopping, temperature control, per-step logging or trajectory capture, or any custom per-step behavior attached to a dynamics run.
npx skills add nvidia/nvalchemi-toolkit --skill nvalchemi-dynamics-hooks
Hooks are callbacks that fire at specific points during each workflow step.
They observe or modify batch state without changing the engine itself.
The hook system is framework-wide: the same Hook protocol works for
dynamics and custom pipelines. Dynamics engines pass DynamicsContext;
custom engines can pass HookContext or their own context subclass.
Any object with these attributes satisfies the Hook protocol (runtime-checkable):
A hook fires when step_count % hook.frequency == 0 (so all hooks fire at
step 0), except ON_ADMISSION, which fires once per admission regardless of
frequency.
HookContext — base snapshot shared by hook-enabled workflows:
DynamicsContext — context passed by dynamics engines:
Access batch data via ctx.batch and dynamics step info via ctx.step_count.
DynamicsStageDynamics exposes 10 lifecycle stages. ON_ADMISSION fires once when a
batch is admitted, while the remaining 9 stages fire within each step():
Stage selection guidelines (dynamics):
| Goal | Stage |
|---|---|
| Validate or allocate for a newly admitted batch | DynamicsStage.ON_ADMISSION |
| Modify forces/energy after model | DynamicsStage.AFTER_COMPUTE |
| Observe final state (logging, snapshots) | DynamicsStage.AFTER_STEP |
| Wrap positions after velocity update | DynamicsStage.AFTER_POST_UPDATE |
| Instrument timing / profiling | DynamicsStage.BEFORE_STEP |
| React to convergence | DynamicsStage.ON_CONVERGE |
ON_ADMISSION is reset for every new run() and for managed membership
changes such as refill or pipeline communication. In FusedStage, it runs
outside compiled _step_impl, making it suitable for shape-dependent allocation
and Python setup that per-step hooks cannot safely perform under fullgraph=True.
It ignores the step-based frequency gate; a multi-stage hook's frequency still
applies at its other stages.
In FusedStage, fused-level hooks wrap sub-stage hooks at every shared boundary:
fused BEFORE_* hooks run before the corresponding sub-stage loop, and fused
AFTER_* hooks run after it. Every hook receives ctx.active_graph_mask for
the graphs participating at that boundary. Fused-level masks span all
participating sub-stages; sub-stage masks are further restricted to graphs
owned by that sub-stage.
During a force-reprime iteration, a graph participates in the step and shared compute but skips both integrator updates. Therefore:
BEFORE_STEP, BEFORE_COMPUTE, AFTER_COMPUTE, and AFTER_STEP include
reprime-pending graphs.BEFORE_PRE_UPDATE, AFTER_PRE_UPDATE, BEFORE_POST_UPDATE, and
AFTER_POST_UPDATE exclude them, at both the fused and sub-stage level.Update masks are intentionally fixed at step start, so clearing
reprime_pending after compute enables integrator updates on the next
iteration without enabling post-update in the current one.
ON_CONVERGE remains sub-stage-only because convergence is evaluated
independently per sub-stage. Fused sub-stages evaluate convergence every step;
registered ON_CONVERGE hooks run when allowed by hook.frequency and must
inspect ctx.converged_mask. BaseDynamics.step() calls them only when
convergence is detected.
Multiple hooks at the same stage fire in registration order.
Stage type enforcement: each engine declares _stage_type to restrict
which enum types are accepted. For example, BaseDynamics sets
_stage_type = DynamicsStage.
NaNDetectorHook — detect NaN/Inf in forces and energy.
MaxForceClampHook — clamp per-atom force vectors to a maximum L2 norm.
BiasedPotentialHook — add an external bias potential for enhanced sampling.
LoggingHook — log scalar observables.
SnapshotHook — save full batch state to a DataSink.
EnergyDriftMonitorHook — track total energy drift.
WrapPeriodicHook — wrap positions back into the unit cell.
StageTimingHook — per-stage NVTX ranges and wall-clock timing. Registers
itself at every profiled stage via _runs_on_stage, records timestamps, and
computes per-transition deltas (optionally written to CSV or console).
Call profiler.summary() after the run for aggregated per-stage timings. For
full kernel-level PyTorch profiler traces, use TorchProfilerHook, which
captures traces through PhysicsNeMo's profiler wrapper.
Implement the protocol directly — no inheritance needed.
_runs_on_stageFire at multiple stages by defining _runs_on_stage(stage) -> bool:
plum dispatchFor hooks that work with multiple stage enum types (e.g. DynamicsStage and
a custom enum), use plum.dispatch to overload __call__ with different
stage types:
Use this plum.dispatch pattern when one hook must handle several
context/stage types. Built-in multi-stage hooks like StageTimingHook
instead use the simpler _runs_on_stage approach from Option 2.
Register hooks in this order for correct behavior: