Skip to main content

nvalchemi-dynamics-api

How to configure and run dynamics simulations, compose multi-stage pipelines (FusedStage, DistributedPipeline), use inflight batching, and manage data sinks. Use when writing any simulation script — molecular dynamics (NVE/NVT), structure relaxation or geometry optimization (e.g. FIRE2), equation-of-state or adsorption scans — or orchestrating many structures through a batched GPU pipeline.

소스 정보

저장소
NVIDIA/nvalchemi-toolkit
최근 소스 활동
2026년 10월 6일 05:51
감지된 SKILL.md 언어
영어
스타
191
포크
42

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
nvalchemi-dynamics-api
description
How to configure and run dynamics simulations, compose multi-stage pipelines (FusedStage, DistributedPipeline), use inflight batching, and manage data sinks. Use when writing any simulation script — molecular dynamics (NVE/NVT), structure relaxation or geometry optimization (e.g. FIRE2), equation-of-state or adsorption scans — or orchestrating many structures through a batched GPU pipeline.
# nvalchemi Dynamics API ## Overview The dynamics API provides tools to discover available dynamics classes, configure them, and scale simulations up (single GPU) and out (multi-rank pipelines). ```python from nvalchemi.dynamics import ( BaseDynamics, DemoDynamics, FusedStage, DistributedPipeline, ConvergenceHook, Hook, DynamicsStage, SizeAwareSampler, DataSink, GPUBuffer, HostMemory, ZarrData, hooks, ) ``` --- ## Available dynamics classes | Class | Description | |-------|-------------| | `BaseDynamics` | Abstract base — subclass to create integrators | | `DemoDynamics` | Velocity Verlet reference implementation (testing only) | To find all dynamics classes in a codebase, search for subclasses of `BaseDynamics`. --- ## Configuring a dynamics run ### Basic setup ```python from nvalchemi.dynamics import DemoDynamics, ConvergenceHook from nvalchemi.models.demo import DemoModelWrapper from nvalchemi.data import AtomicData, Batch import torch model = DemoModelWrapper() dynamics = DemoDynamics( model=model, n_steps=1000, # total steps for run() dt=0.5, # timestep (fs) ) # Create batch with required state data = AtomicData( atomic_numbers=torch.tensor([6, 8, 1], dtype=torch.long), positions=torch.randn(3, 3), ) batch = Batch.from_data_list([data]) batch.forces = torch.zeros(3, 3) batch.energy = torch.zeros(1, 1) result = dynamics.run(batch) ``` ### With convergence detection ```python dynamics = DemoDynamics( model=model, n_steps=10000, dt=0.5, convergence_hook=ConvergenceHook( criteria=[ {"key": "fmax", "threshold": 0.05, "reduce_op": "max"}, {"key": "energy_change", "threshold": 1e-6}, ], frequency=1, ), ) ``` Shorthand for force-based convergence: ```python dynamics = DemoDynamics( model=model, n_steps=10000, dt=0.5, convergence_hook=ConvergenceHook.from_fmax(threshold=0.05), ) ``` ### With hooks ```python from nvalchemi.dynamics.hooks import MaxForceClampHook, LoggingHook dynamics = DemoDynamics( model=model, n_steps=1000, dt=0.5, hooks=[ MaxForceClampHook(max_force=10.0), LoggingHook(backend="csv", log_path="md_log.csv", frequency=100), ], ) ``` --- ## Scaling up: FusedStage (single GPU, multiple stages) `FusedStage` composes multiple dynamics stages on one GPU with a **single shared model forward pass per step**. Samples migrate between stages via convergence. ### Composition with `+` operator ```python # Stage 0: geometry optimization opt = DemoDynamics( model=model, n_steps=100, dt=1.0, convergence_hook=ConvergenceHook.from_fmax(0.05), ) # Stage 1: production MD md = DemoDynamics(model=model, n_steps=500, dt=0.5) # Compose — auto-registers convergence hook to migrate status 0 → 1 fused = opt + md result = fused.run(batch) # runs until all samples reach exit_status ``` ### Constructor (explicit) ```python fused = FusedStage( sub_stages=[(0, opt), (1, md)], entry_status=0, # initial sample status exit_status=2, # auto-set to len(sub_stages) if -1 compile_step=False, # enable torch.compile compile_kwargs=None, # kwargs for torch.compile reprime_on_entry=None, # status codes needing a fresh force compute on entry ) ``` If a sub-stage needs freshly computed forces before it starts integrating (e.g. its `AFTER_COMPUTE` hooks differ from the stage the sample came from), pass its status code in `reprime_on_entry`. Newly entering samples skip one `pre_update`/`post_update` cycle while the shared compute and that stage's `AFTER_COMPUTE` hooks refresh their forces, then integrate normally the following iteration. This is separate from the initial batch-wide force prime and does not affect samples already in that status. ### With torch.compile ```python fused = (opt + md) fused.compile(fullgraph=True, mode="reduce-overhead") with fused: # lazy compilation on context entry result = fused.run(batch) ``` ### CUDA streams: `with` vs bare `run()` Prefer to use context manager: ```python with dynamics: dynamics.run(batch) ``` As best pattern, even without explicit CUDA stream usage, as it in turn supports `compile(mode="reduce-overhead")` Calling `dynamics.run(batch)` without context manager will reuse the current stream. ### How it works 1. Each sample has a `status` field (integer) 2. Each sub-stage processes only samples matching its status code 3. When a sub-stage's `ConvergenceHook` fires, converged samples' status increments 4. Samples at `exit_status` are graduated (no longer updated) 5. `run()` loops until all samples reach `exit_status` or sampler is exhausted ### Chaining more stages ```python fused_3 = opt + md + analysis # 3 sub-stages: status 0, 1, 2 fused_3 = fused + extra_stage # append to existing FusedStage ``` --- ## Scaling out: DistributedPipeline (multi-rank) `DistributedPipeline` chains dynamics stages across multiple ranks using `torch.distributed`. Each rank runs one stage; converged samples are sent to the next rank. This is pipeline parallelism for dynamics, distinct from data-parallel multi-GPU *training* (DDP). ### Composition with `|` operator ```python opt_stage = DemoDynamics(model=model, n_steps=100, dt=1.0) # rank 0 md_stage = DemoDynamics(model=model, n_steps=500, dt=0.5) # rank 1 pipeline = opt_stage | md_stage with pipeline: # initializes torch.distributed + setup pipeline.run() ``` ### Constructor (explicit) ```python pipeline = DistributedPipeline( stages={0: opt_stage, 1: md_stage}, synchronized=False, # True for debugging (adds barriers) ) ``` ### Communication modes Control how inter-rank buffers synchronize: ```python stage = DemoDynamics( model=model, n_steps=100, dt=0.5, comm_mode="async_recv", # default: deferred blocking # comm_mode="sync", # immediate blocking (debugging) # comm_mode="fully_async", # maximum overlap ) ``` The default `comm_mode` is `"async_recv"`. The three modes differ in when blocking occurs: - `"sync"`: `irecv` completes inline in `_prestep_sync_buffers`; simplest and good for debugging. - `"async_recv"`: `irecv` is posted in `_prestep_sync_buffers`, but `wait()` is deferred to `_complete_pending_recv` for communication overlap. - `"fully_async"`: send and receive are both deferred for maximum overlap. Pending sends from the prior step are drained at the start of the next `_prestep_sync_buffers`. ### Pre-allocated buffers For high-throughput pipelines, pre-allocate send/recv buffers: ```python from nvalchemi.dynamics.base import BufferConfig buffer_cfg = BufferConfig( num_systems=100, # max graphs num_nodes=5000, # total node capacity num_edges=20000, # total edge capacity ) stage = DemoDynamics( model=model, n_steps=100, dt=0.5, buffer_config=buffer_cfg, ) ``` Buffers are **lazily initialized** on the first step using the first concrete batch as a template for attribute keys, dtypes, and shapes. This means the first step has slightly more overhead. Adjacent stages must use identical `BufferConfig` values. This is validated in `DistributedPipeline.setup()`. --- ## Buffer semantics and communication ### Three buffer layers The dynamics framework manages data flow through three layers: | Layer | Location | Purpose | |-------|----------|---------| | **Active batch** | `_CommunicationMixin.active_batch` | Working set being integrated | | **Communication buffers** | `send_buffer` / `recv_buffer` | Pre-allocated `Batch.empty()` for zero-copy inter-rank transfer | | **Overflow sinks** | `DataSink` list (priority-ordered) | Staging when active batch is full | ### Communication protocol (DistributedPipeline) Each pipeline step follows a four-phase protocol: 1. `_prestep_sync_buffers()` zeros the send buffer and posts `irecv` from the prior rank. 2. `_complete_pending_recv()` waits on deferred receive, routes into the active batch, and drains overflow sinks. 3. `step()` runs dynamics integration. 4. `_poststep_sync_buffers(converged_indices)` extracts converged samples into the send buffer and sends them to the next rank. **Deadlock prevention:** when no samples converge, an empty send buffer is still sent so the downstream `irecv` completes. ### Back-pressure When `send_buffer` has limited capacity (via `BufferConfig`): - Only `min(converged_count, remaining_capacity)` samples are extracted - Excess converged samples remain in the active batch as **no-ops**. Their positions and velocities are saved before the integrator and restored after it runs. - Without `BufferConfig`, all converged samples are sent without constraints (backward compatible). ### Buffer lifecycle: put/defrag/zero ```python # Pre-allocated buffer created via Batch.empty() buffer = Batch.empty(num_systems=100, num_nodes=5000, num_edges=20000, template=batch) # Copy selected graphs into buffer (Warp GPU kernels, float32 only) mask = converged_mask # bool tensor, True = copy this graph buffer.put(src_batch, mask) # Remove copied graphs from source in-place src_batch.defrag() # Reset buffer for reuse (preserves allocated memory) buffer.zero() ``` **Important:** `Batch.put()` uses Warp GPU kernels that only handle float32 attributes. Adjacent pipeline stages must have identical `BufferConfig` values. ### Data routing methods | Method | Purpose | |--------|---------| | `_recv_to_batch(incoming)` | Route received data through recv buffer into active batch | | `_buffer_to_batch(incoming)` | Append to active batch, overflow to sinks if full | | `_batch_to_buffer(mask)` | Copy graduated samples into send buffer, defrag active batch | | `_overflow_to_sinks(batch)` | Write to first non-full sink in priority order | | `_drain_sinks_to_batch()` | Pull from sinks back into active batch when room available | --- ## Inflight batching with SizeAwareSampler For streaming workflows, `SizeAwareSampler` manages dataset access with bin-packing for size-matched batching. As samples converge and leave the batch, new samples are pulled from the dataset. ```python from nvalchemi.dynamics import SizeAwareSampler sampler = SizeAwareSampler( dataset=dataset, # must have __len__, __getitem__, get_metadata max_atoms=1000, # max total atoms per batch (None = auto from GPU) max_edges=5000, # max total edges per batch max_batch_size=32, # max graphs per batch bin_width=10, # group samples by atom count bins shuffle=False, max_gpu_memory_fraction=0.8, # for auto max_atoms estimation ) # Build initial batch via greedy bin-packing batch = sampler.build_initial_batch() # Request a replacement sample that fits constraints replacement = sampler.request_replacement(num_atoms=50, num_edges=200) # Check if all samples consumed sampler.exhausted # bool ``` ### How inflight replacement works (`_refill_check`) When `refill_frequency` triggers (every N steps), `_refill_check()`: 1. Identifies graduated graphs (`status >= exit_status`) 2. Writes graduated graphs to sinks 3. Extracts remaining graphs via `Batch.index_select` 4. Requests replacements from sampler (one per graduated slot, matching atom/edge budget) 5. Appends replacements via `Batch.append` 6. Rebuilds `status` (replacements get `0`) and `fmax` (replacements get `inf`) tensors This produces a **new** `Batch` object, not an in-place mutation. It returns `None` when the sampler is exhausted and no active samples remain. ### With FusedStage ```python opt = DemoDynamics( model=model, n_steps=100, dt=1.0, sampler=sampler, refill_frequency=1, # check for replacements every N steps convergence_hook=ConvergenceHook.from_fmax(0.05), ) md = DemoDynamics(model=model, n_steps=500, dt=0.5) fused = opt + md with fused: result = fused.run() # no batch arg — built from sampler ``` ### With DistributedPipeline (first stage only) ```python first_stage = DemoDynamics( model=model, n_steps=100, dt=1.0, sampler=sampler, refill_frequency=1, ) pipeline = first_stage | second_stage with pipeline: pipeline.run() # first stage auto-refills from sampler ``` --- ## Data sinks Sinks store graduated/snapshot data. Used by `SnapshotHook` and the communication layer. ```python from nvalchemi.dynamics import GPUBuffer, HostMemory, ZarrData ``` ### DataSink interface ```python class DataSink(ABC): def write(self, batch: Batch, mask: Tensor | None = None) -> None: ... def read(self) -> Batch: ... def zero(self) -> None: ... # clear contents def drain(self) -> Batch: ... # read() + zero() def is_full(self) -> bool: ... # len(self) >= capacity def __len__(self) -> int: ... @property def capacity(self) -> int: ... ``` ### Implementations
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기