Advanced configuration#
Most applications can start with the calls in the main guide. Use this page when you need to control storage, tune a cell grid, work with native tiles, or combine cutoffs. For compiled execution and reuse, see Repeated and compiled calculations.
Choose storage and capacity together#
A matrix has one row per source and max_neighbors slots per row. It is simple
to use and has fixed shapes, but a few crowded neighborhoods can force a wide
allocation for every atom. Compact COO stores only active pairs and suits edge
consumers, at the cost of packing and a data-dependent pair count.
Layout |
Valid data |
Useful when |
|---|---|---|
Matrix |
First |
Fixed-shape consumers, per-source reductions |
Compact COO |
All returned columns; |
Eager edge/graph consumers |
Fixed-capacity JAX COO |
Prefix ending at |
Compiled naive/cell-list edge consumers |
Segmented COO |
Active prefix of each system’s reserved segment |
Selective cluster-tile rebuilds |
Native cluster tiles |
Active tile records and their sorted atom groups |
Consumers designed to query tiles directly |
Estimate max_neighbors#
estimate_max_neighbors uses a cutoff-sphere volume and an assumed atomic
number density to suggest an initial capacity per atom:
By default, positive cutoffs reserve at least 16 slots, rounded up to a multiple of 16. When distances are in Å, supply density in atoms/ų. Converting typical mass densities to atom counts gives about 0.10 for liquid water near room temperature (NIST water density) and 0.08 for crystalline α-quartz (Handbook of Mineralogy). Count atoms, rather than molecules or formula units.
Use a higher input density to allow for expected local variation. The 0.15
below is a conservative example above those averages:
from nvalchemiops.neighbors.neighbor_utils import estimate_max_neighbors
width = estimate_max_neighbors(cutoff, atomic_density=0.15)
An average density does not bound every local neighborhood. Check required
counts on representative structures and allow for expected changes.
atomic_density during preparation estimates omitted widths; explicit
capacities take precedence.
Note
Density is a storage-estimation input, not a check of the system’s physical density. A low estimate can allocate too few neighbor slots. Before using a matrix, compare its required counts with its row width. Some direct matrix calls, as well as Torch prepared naive/cell calls, return excessive counts without raising an exception.
For JAX prepared execution, call check_neighbor_list_state on the successor
state before using the results; it raises NeighborOverflowError for
insufficient neighbor capacity. Cluster intermediate storage has separate
TileBufferOverflow checks; see Size cluster-tile storage.
Increase the explicit capacity, or the density estimate when capacity is inferred. Rebuild the list or prepare a new state, then check it before use. See Repeated and compiled calculations for compiled checks and reuse.
Trim unused matrix columns in Torch#
A matrix may reserve more columns than the current neighborhoods need. After
checking that every required count fits the allocated width, trim all rows to
K = counts.max(). Keep the image shifts and any distances or vectors aligned
with the same slice. Rows with fewer than K neighbors still need a valid mask.
This continues the main guide’s Torch matrix example:
if torch.any(counts > matrix.shape[1]):
raise RuntimeError("Increase neighbor capacity and rebuild before trimming")
@torch.compile(fullgraph=True)
def trim_matrix(matrix, counts, shifts):
K = counts.max()
matrix_view = matrix[:, :K]
shift_view = shifts[:, :K]
valid = torch.arange(matrix_view.shape[1], device=matrix.device)[None, :] < counts[:, None]
return matrix_view, shift_view, valid
matrix_view, shift_view, valid = trim_matrix(matrix, counts, shifts)
Tensor-bound slicing works with torch.compile(fullgraph=True) in PyTorch 2.14;
see the compile documentation.
It needs no explicit .item(), but obtaining the width can still synchronize
CUDA with the host. Measure the consumer savings together with that cost.
The slice is a view that retains the original allocation, may be noncontiguous,
and may borrow prepared-state storage. It reduces padded consumer work rather
than freeing the reserved buffer.
Before gathering positions, replace padded neighbor IDs with a valid index,
then mask their contribution. When present, slice distances and vectors to the
returned width with distances[:, :matrix_view.shape[1]] and
vectors[:, :matrix_view.shape[1]]. In JAX jit, keep fixed shapes and
masks; an exact width determined by runtime counts cannot be returned as a
standard dynamically sized array.
Fixed-capacity COO in JAX#
Pass a static coo_capacity for compiled naive or cell-list COO. Capacity bounds
the total stored pairs as well as the per-row width. The topology tuple appends
required row counts and metadata_valid; optional geometry follows those fields.
The pointer delimits what was actually stored, while required counts tell you
whether row or total capacity was sufficient.
Prepared JAX naive COO uses this global fixed-capacity layout even with
selective=True. Its outputs use neighbor_ptr, required row counts, and
metadata_valid, rather than per-system pair_offsets and pair_counts.
This continues the main guide’s JAX setup:
from nvalchemiops.jax.neighbors import (
check_neighbor_list_state, neighbor_list, prepare_neighbor_list,
)
state = prepare_neighbor_list(
positions, cutoff, cell=cell, pbc=pbc,
method="naive", strategy="scalar", max_neighbors=32,
return_neighbor_list=True, coo_capacity=1024,
)
@jax.jit
def build_edges(current_positions, current_cell, current_state):
return neighbor_list(current_positions, cell=current_cell, state=current_state)
(edges, ptr, shifts, required_counts, metadata_valid), state = build_edges(
positions, cell, state,
)
check_neighbor_list_state(state)
active_pairs = int(ptr[-1])
active_edges = edges[:, :active_pairs] # host-side inspection after the check
The unused tail is padding. Do not send all capacity columns to an edge consumer
as if they were valid. Within a compiled consumer, use the pointer and a valid
prefix mask without creating a data-dependent shape. If metadata is invalid,
refresh the configuration; its required counts cannot be trusted. For direct
calls, also compare required counts with pointer differences after leaving
jax.jit. The prepared-state check gathers the applicable failure status.
Segmented COO#
Selective cluster COO reserves a fixed segment for each system.
pair_offsets[b] marks its start and pair_counts[b] gives its active length. Consume
[pair_offsets[b], pair_offsets[b] + pair_counts[b]); the rest of the segment is
inactive. A false rebuild flag retains the previous segment.
Keep offsets and all persistent buffers together. Resizing segments changes the
state layout and requires a new state initialized with every system rebuilt.
Single-system selective cluster COO has one segment with offsets
[0, physical_capacity]. Exact tuple names differ between direct Torch and JAX
calls; prepared state manages the persistent storage.
Size cluster-tile storage#
Cluster construction first stores candidate tile pairs, then queries atom pairs. Its intermediate tile capacity is separate from the final matrix width or COO pair capacity. Sizing only the final output is insufficient.
For a nonempty system with \(g=\lceil N/32\rceil\) groups,
max_tiles_per_group=m reserves \(g\min(g,m)\) tile records. For a compact Torch
batch, the pooled capacity is the sum of those contributions over systems.
Increasing m up to the group count provides more room for candidate tiles at
the cost of memory. Eager combined calls estimate it when omitted. Under a
transformation that needs a static allocation shape, supply a positive Python
integer or complete caller-owned tile arrays.
TileBufferOverflow reports insufficient intermediate storage in checked eager
paths. Increase the tile capacity and rebuild. Compiled calls cannot always turn
a device count into a Python exception. Validate the intended capacities eagerly;
for a workflow that adapts capacities, use a separate lower-level build, check its
counts on the host, and query only a successful build. Selective segmented batches
may need successive retries as different systems overflow.
Native tile output#
Use the direct cluster API for format="tile"; the unified function exposes
matrix and COO. For a single Torch system:
from nvalchemiops.torch.neighbors import cluster_tile_neighbor_list
num_tiles, tile_rows, tile_cols, atom_order, x, y, z = cluster_tile_neighbor_list(
positions, cutoff, cell, format="tile", max_tiles_per_group=1,
)
This small fixture has one group; choose capacity for your actual system.
Tile row/column records refer to sorted groups, and atom_order maps their atom
slots back to input IDs. Sorted coordinates and padded lanes require a tile-aware
consumer; the records are not an ordinary atom-pair edge list. Request matrix or
COO when your next operation expects those layouts. Native tiles do not include
per-pair geometry or pair-function arrays.
Torch compact cluster COO with a pair function writes callback buffers in place and returns its topology tuple, unlike naive/cell-list calls that append pair arrays. Use matrix output or prepared result properties for a straightforward callback consumer, and consult the direct signature before unpacking a cluster-specific result.
Tune a cell grid#
Cell lists trade bin count against the number of atoms checked per bin. A finer
grid can reduce occupancy but requires a wider stencil and more bin work. A
coarser grid saves grid storage but increases candidate comparisons. max_nbins
caps grid storage; reducing it can increase occupancy. Compare build and query
together on the coordinate distribution you will use.
min_cells_per_dimension is a lower bound in configured sizing, not a requested
exact grid. Query coverage also depends on cutoff and cell geometry, especially
for skewed cells. If you pass a prebuilt grid to queries with several cutoffs,
configure its search radius for the largest cutoff. See
Build bins once, then query several cutoffs for a complete example.
Configured and adaptive sizing#
For eligible pair-centric queries, consider adaptive sizing for systems with approximately uniform spatial density, including batches with similar atom counts, cell shapes and volumes. It uses cell geometry and atom population to balance cell visits against estimated work per cell, which can reduce build/query cost compared with the configured grid. The cost model assumes roughly uniform bin occupancy; clustered distributions or very different systems in a batch can change the benefit. Compare both policies on your actual inputs.
grid_policy="configured" remains the default. Set grid_policy="adaptive"
explicitly to opt in. With neighbor_list(method="cell_list", ...), adaptive
sizing also applies when the automatic cell-list strategy selects pair-centric
execution; you do not need to pin that strategy to use the policy.
For an eager full-list CUDA query, the call is:
matrix, counts, shifts = neighbor_list(
positions, cutoff, cell=cell, pbc=pbc,
method="cell_list_pair_centric", max_neighbors=32,
grid_policy="adaptive",
)
Adaptive sizing is limited to eligible full-list pair-centric routes. It does not combine with selected rows, half fill, selective rebuilds, or fixed-cell execution. Prepared states require configured sizing. Torch compiled adaptive calls require complete cell-list workspace; JAX adaptive selection is eager, with additional restrictions on explicit launch metadata. Use the backend API for those workspace and metadata arguments. Grid selection is reused within the same construction, not cached across calls. Every construction still bins the current atoms and enumerates neighbors; it does not reuse a Verlet topology.
Prepared free-boundary spans#
When preparation synthesizes a nonperiodic cell from exemplar coordinates,
span_margin reserves growth in the coordinate spans. The synthesized box
recenters as the system translates, so a rigid translation alone does not
exhaust the margin. Choose it from the extent your workload can reach, and
re-prepare after a span failure. It is unrelated to the displacement margin of buffered topology.
An explicit appropriate cell gives you direct control over the domain.
Query two cutoffs together#
Two lists can serve a short-range MLIP supplemented by explicit long-range physics, as in DPLR. Here a 6 Å list illustrates local MLIP environments, while a 12 Å list illustrates the real-space part of Ewald/PME electrostatics. The reciprocal-space contribution is evaluated separately; a finite neighbor list alone does not provide the complete long-range interaction. See the LAMMPS KSpace documentation. These cutoffs are illustrative; the model and physics determine the values.
The "naive_dual_cutoff" method selects the scalar implementation and emits
two lists in one search; no extra strategy argument is needed. Direct search
is a useful option when the cutoffs cover much of the system’s physical domain.
Keep cutoff2 >= cutoff; equal cutoffs are valid. Each list has its own capacity:
matrix1, counts1, shifts1, matrix2, counts2, shifts2 = neighbor_list(
positions, 6.0, cutoff2=12.0, cell=cell, pbc=pbc,
method="naive_dual_cutoff", max_neighbors1=128, max_neighbors2=1024,
)
The widths 128 and 1024 are illustrative allocations for this example, not
capacity guarantees for water or another material. Estimate and check each
width for your actual structures. Doubling the cutoff from 6 to 12 multiplies
the cutoff-sphere volume by eight at fixed density.
Cluster tiles also support two cutoffs in matrix output, with fully periodic float32 input. Dual cluster output cannot combine with per-pair geometry or a pair function. There is no dual cell-list or tiled-naive implementation; a unified dual request uses a compatible scalar naive or cluster route.
Prepared width names differ: Torch uses max_neighbors and max_neighbors2;
JAX also accepts max_neighbors1 for the first width. Preparation estimates
omitted capacities, but each still needs enough room for its cutoff. Each
consumer uses its own geometric neighborhood; the lists do not define the
interaction or model.