Source code for nvalchemiops.torch.neighbors.batch_cluster_tile

# SPDX-FileCopyrightText: Copyright (c) 2025 - 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""PyTorch bindings for batched cluster-pair tile neighbor list.

All torch-side work (per-system Morton sort with system-major key,
per-system padding to a multiple of ``TILE_GROUP_SIZE``, SoA gather of
sorted positions, ``inv_cell_batch`` inversion, ``wp.from_torch``
conversion) lives in this module; the Warp layer at
``nvalchemiops.neighbors.cluster_tile`` only sees ``wp.array``
inputs.

Scope: float32, orthorhombic or triclinic PBC, one cell per system,
arbitrary natom per system (padded internally).
"""

from typing import NamedTuple

import torch
import warp as wp

from nvalchemiops.neighbors.cluster_tile import (
    TILE_GROUP_SIZE,
)
from nvalchemiops.neighbors.cluster_tile import (
    batch_build_cluster_tile_list as wp_batch_build_cluster_tile_list,
)
from nvalchemiops.neighbors.cluster_tile import (
    batch_query_cluster_tile as wp_batch_query_cluster_tile,
)
from nvalchemiops.neighbors.cluster_tile import (
    batch_query_cluster_tile_coo as wp_batch_query_cluster_tile_coo,
)
from nvalchemiops.neighbors.cluster_tile import (
    estimate_batch_cluster_tile_segments as wp_estimate_batch_cluster_tile_segments,
)
from nvalchemiops.neighbors.cluster_tile import (
    estimate_batch_max_tiles_per_group as wp_estimate_batch_max_tiles_per_group,
)
from nvalchemiops.neighbors.cluster_tile.kernels import (
    _get_query_cluster_tile_direct_csr_count_kernel,
    _get_query_cluster_tile_direct_csr_fill_kernel,
)
from nvalchemiops.neighbors.neighbor_utils import (
    _selective_fill_neighbor_matrix_tail as wp_selective_fill_neighbor_matrix_tail,
)
from nvalchemiops.neighbors.neighbor_utils import estimate_max_neighbors
from nvalchemiops.neighbors.neighbor_utils import (
    fill_neighbor_matrix_tail as wp_fill_neighbor_matrix_tail,
)
from nvalchemiops.neighbors.neighbor_utils import (
    selective_zero_num_neighbors as wp_selective_zero_num_neighbors,
)
from nvalchemiops.neighbors.output_args import (
    _has_partial_or_pair_outputs,
    _prepare_coo_pair_output_args,
)
from nvalchemiops.torch._warp_op_helpers import scoped_torch_warp_stream
from nvalchemiops.torch.neighbors._autograd import (
    _reconstruct_coo_geometry,
    _reconstruct_matrix_geometry,
)
from nvalchemiops.torch.neighbors._fixed_cell import _FixedCellGeometry
from nvalchemiops.torch.neighbors.neighbor_utils import (
    _check_neighbor_capacity,
    _check_tile_buffer_capacity,
    _compact_coo_prefix,
    _prepare_compact_coo_geometry_buffers,
    _validate_cluster_tile_matrix_outputs,
    _validate_compact_coo_outputs,
    _validate_segmented_coo_state,
)
from nvalchemiops.torch.types import get_wp_dtype

__all__ = [
    "TILE_GROUP_SIZE",
    "estimate_batch_max_tiles_per_group",
    "estimate_batch_cluster_tile_list_sizes",
    "estimate_batch_cluster_tile_segments",
    "allocate_batch_cluster_tile_list",
    "batch_build_cluster_tile_list",
    "batch_query_cluster_tile",
    "batch_query_cluster_tile_coo",
    "batch_cluster_tile_neighbor_list",
]


def _fixed_cluster_op_args(
    geometry: _FixedCellGeometry | None,
) -> tuple[
    bool,
    torch.Tensor | None,
    torch.Tensor | None,
    torch.Tensor | None,
    torch.Tensor | None,
    torch.Tensor | None,
]:
    """Return static fixed mode and readonly per-system geometry tensors."""
    if geometry is None:
        return False, None, None, None, None, None
    return (
        True,
        geometry.qr,
        geometry.axis_aligned,
        geometry.fractional_rounding_certified,
        geometry.qr_height_certified,
        geometry.bbox_cutoff_bounds,
    )


def _fixed_cluster_kwargs(
    geometry: _FixedCellGeometry | None,
) -> dict[str, object]:
    """Return fixed geometry tensors as private query kwargs."""
    return dict(
        zip(
            (
                "fixed_cell",
                "qr",
                "axis_aligned",
                "fractional_rounding_certified",
                "qr_height_certified",
                "bbox_cutoff_bounds",
            ),
            _fixed_cluster_op_args(geometry),
        )
    )


def _wp_fixed_cluster_geometry(
    fixed_cell: bool,
    qr: torch.Tensor | None,
    axis_aligned: torch.Tensor | None,
    fractional_rounding_certified: torch.Tensor | None,
    qr_height_certified: torch.Tensor | None,
    bbox_cutoff_bounds: torch.Tensor | None,
) -> dict[str, object]:
    """Convert optional prepared cluster geometry arrays for a Warp launcher."""
    return {
        "fixed_cell": fixed_cell,
        "qr": wp.from_torch(qr, dtype=wp.float32, requires_grad=False)
        if qr is not None
        else None,
        "axis_aligned": wp.from_torch(axis_aligned, dtype=wp.bool, requires_grad=False)
        if axis_aligned is not None
        else None,
        "fractional_rounding_certified": wp.from_torch(
            fractional_rounding_certified,
            dtype=wp.bool,
            requires_grad=False,
        )
        if fractional_rounding_certified is not None
        else None,
        "qr_height_certified": wp.from_torch(
            qr_height_certified, dtype=wp.bool, requires_grad=False
        )
        if qr_height_certified is not None
        else None,
        "bbox_cutoff_bounds": wp.from_torch(
            bbox_cutoff_bounds,
            dtype=wp.float32,
            requires_grad=False,
        )
        if bbox_cutoff_bounds is not None
        else None,
    }


def _wp_direct_csr_geometry_inputs(
    device: torch.device,
    geometry: _FixedCellGeometry | None,
) -> tuple[wp.array, wp.array, wp.array, wp.array, wp.array]:
    """Convert direct-CSR cache arrays or empty dynamic sentinels."""
    device_str = str(device)
    return (
        wp.from_torch(geometry.qr, dtype=wp.float32, requires_grad=False)
        if geometry is not None and geometry.qr is not None
        else wp.empty((0, 15), dtype=wp.float32, device=device_str),
        wp.from_torch(geometry.axis_aligned, dtype=wp.bool, requires_grad=False)
        if geometry is not None and geometry.axis_aligned is not None
        else wp.empty(0, dtype=wp.bool, device=device_str),
        wp.from_torch(
            geometry.fractional_rounding_certified,
            dtype=wp.bool,
            requires_grad=False,
        )
        if geometry is not None and geometry.fractional_rounding_certified is not None
        else wp.empty(0, dtype=wp.bool, device=device_str),
        wp.from_torch(
            geometry.qr_height_certified,
            dtype=wp.bool,
            requires_grad=False,
        )
        if geometry is not None and geometry.qr_height_certified is not None
        else wp.empty(0, dtype=wp.bool, device=device_str),
        wp.from_torch(
            geometry.bbox_cutoff_bounds, dtype=wp.float32, requires_grad=False
        )
        if geometry is not None and geometry.bbox_cutoff_bounds is not None
        else wp.empty((0, 3), dtype=wp.float32, device=device_str),
    )


@torch.library.custom_op(
    "nvalchemiops::_batch_cluster_tile_direct_csr_count", mutates_args=("row_counts",)
)
@scoped_torch_warp_stream
def _batch_cluster_tile_direct_csr_count_op(
    cutoff: float,
    natom: int,
    cell: torch.Tensor,
    inv_cell: torch.Tensor,
    sorted_atom_index: torch.Tensor,
    sorted_pos_x: torch.Tensor,
    sorted_pos_y: torch.Tensor,
    sorted_pos_z: torch.Tensor,
    num_tiles: torch.Tensor,
    tile_row_group: torch.Tensor,
    tile_col_group: torch.Tensor,
    tile_system: torch.Tensor,
    row_counts: torch.Tensor,
    launch_tiles: int,
    fixed_cell: bool,
    qr: torch.Tensor | None,
    axis_aligned: torch.Tensor | None,
    fractional_rounding_certified: torch.Tensor | None,
    qr_height_certified: torch.Tensor | None,
    bbox_cutoff_bounds: torch.Tensor | None,
) -> None:
    geometry = _wp_direct_csr_geometry_inputs(
        sorted_pos_x.device,
        _FixedCellGeometry(
            inv_cell=None,
            cell=None,
            cells_per_dimension=None,
            neighbor_search_radius=None,
            cells_per_system=None,
            cell_offsets=None,
            qr=qr,
            axis_aligned=axis_aligned,
            fractional_rounding_certified=fractional_rounding_certified,
            qr_height_certified=qr_height_certified,
            bbox_cutoff_bounds=bbox_cutoff_bounds,
        )
        if fixed_cell
        else None,
    )
    wp.launch_tiled(
        _get_query_cluster_tile_direct_csr_count_kernel(
            batched=True, fixed_cell=fixed_cell
        ),
        dim=[launch_tiles],
        inputs=[
            wp.from_torch(x, dtype=d, return_ctype=True)
            for x, d in (
                (sorted_pos_x, wp.float32),
                (sorted_pos_y, wp.float32),
                (sorted_pos_z, wp.float32),
                (sorted_atom_index, wp.int32),
                (cell.contiguous(), wp.mat33f),
                (inv_cell.contiguous(), wp.mat33f),
            )
        ]
        + list(geometry)
        + [
            float(cutoff * cutoff),
            int(natom),
            wp.from_torch(num_tiles, dtype=wp.int32, return_ctype=True),
            wp.from_torch(tile_row_group, dtype=wp.int32, return_ctype=True),
            wp.from_torch(tile_col_group, dtype=wp.int32, return_ctype=True),
            wp.from_torch(tile_system, dtype=wp.int32, return_ctype=True),
            wp.from_torch(row_counts, dtype=wp.int32, return_ctype=True),
        ],
        device=str(sorted_pos_x.device),
        block_dim=TILE_GROUP_SIZE,
    )


@_batch_cluster_tile_direct_csr_count_op.register_fake
def _(*args, **kwargs) -> None:
    return None


@torch.library.custom_op(
    "nvalchemiops::_batch_cluster_tile_direct_csr_fill",
    mutates_args=(
        "cursors",
        "coo_list",
        "coo_shifts",
        "neighbor_vectors",
        "neighbor_distances",
    ),
)
@scoped_torch_warp_stream
def _batch_cluster_tile_direct_csr_fill_op(
    cutoff: float,
    natom: int,
    cell: torch.Tensor,
    inv_cell: torch.Tensor,
    sorted_atom_index: torch.Tensor,
    sorted_pos_x: torch.Tensor,
    sorted_pos_y: torch.Tensor,
    sorted_pos_z: torch.Tensor,
    num_tiles: torch.Tensor,
    tile_row_group: torch.Tensor,
    tile_col_group: torch.Tensor,
    tile_system: torch.Tensor,
    cursors: torch.Tensor,
    coo_list: torch.Tensor,
    coo_shifts: torch.Tensor,
    neighbor_vectors: torch.Tensor,
    neighbor_distances: torch.Tensor,
    return_vectors: bool,
    return_distances: bool,
    max_pairs: int,
    launch_tiles: int,
    fixed_cell: bool,
    qr: torch.Tensor | None,
    axis_aligned: torch.Tensor | None,
    fractional_rounding_certified: torch.Tensor | None,
    qr_height_certified: torch.Tensor | None,
    bbox_cutoff_bounds: torch.Tensor | None,
) -> None:
    geometry = _wp_direct_csr_geometry_inputs(
        sorted_pos_x.device,
        _FixedCellGeometry(
            inv_cell=None,
            cell=None,
            cells_per_dimension=None,
            neighbor_search_radius=None,
            cells_per_system=None,
            cell_offsets=None,
            qr=qr,
            axis_aligned=axis_aligned,
            fractional_rounding_certified=fractional_rounding_certified,
            qr_height_certified=qr_height_certified,
            bbox_cutoff_bounds=bbox_cutoff_bounds,
        )
        if fixed_cell
        else None,
    )
    wp.launch_tiled(
        _get_query_cluster_tile_direct_csr_fill_kernel(
            batched=True,
            fixed_cell=fixed_cell,
            return_vectors=return_vectors,
            return_distances=return_distances,
        ),
        dim=[launch_tiles],
        inputs=[
            wp.from_torch(x, dtype=d, return_ctype=True)
            for x, d in (
                (sorted_pos_x, wp.float32),
                (sorted_pos_y, wp.float32),
                (sorted_pos_z, wp.float32),
                (sorted_atom_index, wp.int32),
                (cell.contiguous(), wp.mat33f),
                (inv_cell.contiguous(), wp.mat33f),
            )
        ]
        + list(geometry)
        + [
            float(cutoff * cutoff),
            int(natom),
            int(max_pairs),
            wp.from_torch(num_tiles, dtype=wp.int32, return_ctype=True),
            wp.from_torch(tile_row_group, dtype=wp.int32, return_ctype=True),
            wp.from_torch(tile_col_group, dtype=wp.int32, return_ctype=True),
            wp.from_torch(tile_system, dtype=wp.int32, return_ctype=True),
            wp.from_torch(cursors, dtype=wp.int32, return_ctype=True),
            wp.from_torch(coo_list, dtype=wp.int32, return_ctype=True),
            wp.from_torch(coo_shifts, dtype=wp.int32, return_ctype=True),
            wp.from_torch(neighbor_vectors, dtype=wp.vec3f, return_ctype=True),
            wp.from_torch(neighbor_distances, dtype=wp.float32, return_ctype=True),
            wp.empty((1, 1), dtype=wp.float32, device=str(sorted_pos_x.device)),
            wp.empty(1, dtype=wp.float32, device=str(sorted_pos_x.device)),
            wp.empty(1, dtype=wp.vec3f, device=str(sorted_pos_x.device)),
        ],
        device=str(sorted_pos_x.device),
        block_dim=TILE_GROUP_SIZE,
    )


@_batch_cluster_tile_direct_csr_fill_op.register_fake
def _(*args, **kwargs) -> None:
    return None


@scoped_torch_warp_stream
def _batch_cluster_tile_direct_csr_fill_pair_outputs(
    cutoff: float,
    natom: int,
    cell: torch.Tensor,
    inv_cell: torch.Tensor,
    sorted_atom_index: torch.Tensor,
    sorted_pos_x: torch.Tensor,
    sorted_pos_y: torch.Tensor,
    sorted_pos_z: torch.Tensor,
    num_tiles: torch.Tensor,
    tile_row_group: torch.Tensor,
    tile_col_group: torch.Tensor,
    tile_system: torch.Tensor,
    cursors: torch.Tensor,
    coo_list: torch.Tensor,
    coo_shifts: torch.Tensor,
    pair_fn: wp.Function,
    pair_params: torch.Tensor,
    pair_energies: torch.Tensor,
    pair_forces: torch.Tensor,
    neighbor_vectors: torch.Tensor | None,
    neighbor_distances: torch.Tensor | None,
    return_vectors: bool,
    return_distances: bool,
    max_pairs: int,
    launch_tiles: int,
    _fixed_cell_geometry: _FixedCellGeometry | None = None,
) -> None:
    """Fill batched compact CSR rows and raw pair outputs together."""
    device = str(sorted_pos_x.device)
    wp_pair_params = wp.from_torch(pair_params, dtype=wp.float32)
    wp_neighbor_vectors = (
        wp.from_torch(neighbor_vectors, dtype=wp.vec3f)
        if neighbor_vectors is not None
        else None
    )
    wp_neighbor_distances = (
        wp.from_torch(neighbor_distances, dtype=wp.float32)
        if neighbor_distances is not None
        else None
    )
    (
        vectors_arg,
        distances_arg,
        pair_params_arg,
        energies_arg,
        forces_arg,
    ) = _prepare_coo_pair_output_args(
        wp.float32,
        device,
        max_pairs,
        return_vectors=return_vectors,
        return_distances=return_distances,
        pair_fn=pair_fn,
        pair_params=wp_pair_params,
        neighbor_vectors=wp_neighbor_vectors,
        neighbor_distances=wp_neighbor_distances,
        pair_energies=wp.from_torch(pair_energies, dtype=wp.float32),
        pair_forces=wp.from_torch(pair_forces, dtype=wp.vec3f),
    )
    wp.launch_tiled(
        _get_query_cluster_tile_direct_csr_fill_kernel(
            batched=True,
            fixed_cell=_fixed_cell_geometry is not None,
            return_vectors=return_vectors,
            return_distances=return_distances,
            pair_fn=pair_fn,
        ),
        dim=[launch_tiles],
        inputs=[
            wp.from_torch(sorted_pos_x, dtype=wp.float32),
            wp.from_torch(sorted_pos_y, dtype=wp.float32),
            wp.from_torch(sorted_pos_z, dtype=wp.float32),
            wp.from_torch(sorted_atom_index, dtype=wp.int32),
            wp.from_torch(cell.contiguous(), dtype=wp.mat33f),
            wp.from_torch(inv_cell.contiguous(), dtype=wp.mat33f),
            *_wp_direct_csr_geometry_inputs(sorted_pos_x.device, _fixed_cell_geometry),
            float(cutoff * cutoff),
            int(natom),
            int(max_pairs),
            wp.from_torch(num_tiles, dtype=wp.int32),
            wp.from_torch(tile_row_group, dtype=wp.int32),
            wp.from_torch(tile_col_group, dtype=wp.int32),
            wp.from_torch(tile_system, dtype=wp.int32),
            wp.from_torch(cursors, dtype=wp.int32),
            wp.from_torch(coo_list, dtype=wp.int32),
            wp.from_torch(coo_shifts, dtype=wp.int32),
            vectors_arg,
            distances_arg,
            pair_params_arg,
            energies_arg,
            forces_arg,
        ],
        device=device,
        block_dim=TILE_GROUP_SIZE,
    )


@torch.library.custom_op(
    "nvalchemiops::_batch_cluster_tile_fill_neighbor_matrix_tail",
    mutates_args=("neighbor_matrix",),
)
@scoped_torch_warp_stream
def _batch_cluster_tile_fill_neighbor_matrix_tail_op(
    num_neighbors: torch.Tensor,
    neighbor_matrix: torch.Tensor,
    n_rows: int,
    max_neighbors: int,
    fill_value: int,
) -> None:
    if max_neighbors <= 0:
        return
    wp_fill_neighbor_matrix_tail(
        wp.from_torch(
            num_neighbors, dtype=wp.int32, requires_grad=False, return_ctype=True
        ),
        int(n_rows),
        int(max_neighbors),
        int(fill_value),
        wp.from_torch(
            neighbor_matrix, dtype=wp.int32, requires_grad=False, return_ctype=True
        ),
        str(neighbor_matrix.device),
    )


@_batch_cluster_tile_fill_neighbor_matrix_tail_op.register_fake
def _(
    num_neighbors: torch.Tensor,
    neighbor_matrix: torch.Tensor,
    n_rows: int,
    max_neighbors: int,
    fill_value: int,
) -> None:
    return None


# =============================================================================
# Sizing + allocation helpers
# =============================================================================
[docs] def estimate_batch_max_tiles_per_group( batch_ptr: torch.Tensor, cutoff: float, cell_batch: torch.Tensor, *, safety: float = 2.0, floor: int = 256, ) -> int: """Estimate batched ``max_tiles_per_group`` from per-system cells. Parameters ---------- batch_ptr : torch.Tensor, shape (num_systems + 1,) Cumulative atom counts. cutoff : float Cartesian cutoff used for cluster-tile construction. cell_batch : torch.Tensor, shape (num_systems, 3, 3) Per-system cell matrices. safety : float, default 2.0 Multiplier on the volumetric estimate. floor : int, default 256 Minimum returned value for batched compact buffers. Returns ------- int Shared ``max_tiles_per_group`` for the batched compact tile buffer. """ if batch_ptr.shape[0] < 2: raise ValueError("batch_ptr must have length at least 2") volumes = torch.linalg.det(cell_batch.to(torch.float64)).abs().view(-1) return wp_estimate_batch_max_tiles_per_group( batch_ptr, cutoff, volumes, safety=safety, floor=floor, )
[docs] def estimate_batch_cluster_tile_list_sizes( batch_ptr: torch.Tensor, max_tiles_per_group: int = 256, ) -> tuple[int, int, int, int, int]: """Estimate allocation sizes for the batched tile neighbor list state. Parameters ---------- batch_ptr : torch.Tensor, shape (num_systems + 1,), dtype=int32 Cumulative atom counts defining per-system ranges. max_tiles_per_group : int, default 256 Sets the capacity of the tile-pair buffer shared by all row groups. If system ``i`` has ``g_i`` groups, its compact capacity contribution is ``g_i * min(g_i, max_tiles_per_group)`` entries. Contributions are summed into one pooled buffer shared by the whole batch. Returns ------- n_padded : int Total padded atom count (sum of per-system ``ceil(natom/32)*32``). ngroup : int Number of 32-atom groups: ``n_padded // 32``. ngroup_padded : int Group-array pad length for in-bounds ``wp.tile_load`` at any TILE-aligned offset. max_tiles : int Allocated tile-pair list capacity. num_systems : int """ if batch_ptr.shape[0] < 2: raise ValueError("batch_ptr must have length at least 2") num_systems = int(batch_ptr.shape[0]) - 1 natom_per_system = (batch_ptr[1:] - batch_ptr[:-1]).to(torch.int64) natom_padded_per_system = ( (natom_per_system + TILE_GROUP_SIZE - 1) // TILE_GROUP_SIZE ) * TILE_GROUP_SIZE n_padded = int(natom_padded_per_system.sum().item()) ngroup_per_system = natom_padded_per_system // TILE_GROUP_SIZE ngroup = int(ngroup_per_system.sum().item()) ngroup_padded = ( (ngroup + TILE_GROUP_SIZE - 1) // TILE_GROUP_SIZE ) * TILE_GROUP_SIZE + TILE_GROUP_SIZE max_tiles = int( ( ngroup_per_system * torch.clamp(ngroup_per_system, max=int(max_tiles_per_group)) ) .sum() .item() ) return n_padded, ngroup, ngroup_padded, max_tiles, num_systems
[docs] def estimate_batch_cluster_tile_segments( batch_ptr: torch.Tensor, max_neighbors: int, max_tiles_per_group: int = 256, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: """Estimate fixed per-system tile and COO segment buffers. Returns ``(tile_capacities, tile_offsets, pair_capacities, pair_offsets)`` as int32 tensors on ``batch_ptr.device``. ``tile_offsets`` and ``pair_offsets`` are caller-owned fixed inputs for segmented cluster-tile build / COO query paths; ``tile_counts`` and ``pair_counts`` are separate output counters with length ``num_systems``. Parameters ---------- batch_ptr : torch.Tensor, shape (num_systems + 1,), dtype=int32 Cumulative atom counts defining per-system ranges. max_neighbors : int Upper bound on neighbors per atom used to size per-system COO segments. max_tiles_per_group : int, default 256 Sets each system's tile-pair segment capacity. A system with ``g_i`` groups receives ``g_i * min(g_i, max_tiles_per_group)`` entries. Its group count is ``ceil(num_atoms_i / 32)``. Returns ------- tile_capacities : torch.Tensor, shape (num_systems,), dtype=int32 Per-system tile buffer capacities. tile_offsets : torch.Tensor, shape (num_systems + 1,), dtype=int32 CSR-style offsets into the segmented tile buffer (fixed input to build). pair_capacities : torch.Tensor, shape (num_systems,), dtype=int32 Per-system pair buffer capacities. pair_offsets : torch.Tensor, shape (num_systems + 1,), dtype=int32 CSR-style offsets into the segmented COO pair buffer (fixed input to query). """ if batch_ptr.shape[0] < 2: raise ValueError("batch_ptr must have length at least 2") tile_caps, tile_offsets, pair_caps, pair_offsets = ( wp_estimate_batch_cluster_tile_segments( batch_ptr, max_neighbors=int(max_neighbors), max_tiles_per_group=int(max_tiles_per_group), ) ) device = batch_ptr.device return ( torch.tensor(tile_caps, dtype=torch.int32, device=device), torch.tensor(tile_offsets, dtype=torch.int32, device=device), torch.tensor(pair_caps, dtype=torch.int32, device=device), torch.tensor(pair_offsets, dtype=torch.int32, device=device), )
[docs] def allocate_batch_cluster_tile_list( batch_ptr: torch.Tensor, device: torch.device, dtype: torch.dtype = torch.float32, max_tiles_per_group: int = 256, ) -> tuple[ torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, ]: """Allocate all state tensors consumed by ``batch_build_cluster_tile_list``. Parameters ---------- batch_ptr : torch.Tensor, shape (num_systems + 1,), dtype=int32 Cumulative atom counts defining per-system ranges. device : torch.device Target device for all allocated tensors. dtype : torch.dtype, default torch.float32 Floating-point dtype for position and group-centroid tensors. max_tiles_per_group : int, default 256 Capacity factor for the intermediate tile-pair buffer. A system with ``g_i`` row groups contributes ``g_i * min(g_i, max_tiles_per_group)`` entries to one pooled buffer. Increasing the value uses more memory and accommodates more candidate tile pairs. See :ref:`cluster-tile-buffer-capacity` for sizing details. Returns ------- sorted_atom_index : torch.Tensor, shape (n_padded,), dtype=int32 Permutation mapping padded slot to original atom index; sentinel value ``N`` (total real atoms) marks padding slots. sort_inv : torch.Tensor, shape (N,), dtype=int32 Inverse permutation over the real (un-padded) atoms. sorted_pos_x : torch.Tensor, shape (n_padded,), dtype=dtype Morton-sorted x-coordinates in the padded SoA layout. sorted_pos_y : torch.Tensor, shape (n_padded,), dtype=dtype Morton-sorted y-coordinates in the padded SoA layout. sorted_pos_z : torch.Tensor, shape (n_padded,), dtype=dtype Morton-sorted z-coordinates in the padded SoA layout. batch_idx_sorted : torch.Tensor, shape (n_padded,), dtype=int32 System index for every padded slot after Morton sort. batch_ptr_padded : torch.Tensor, shape (num_systems + 1,), dtype=int32 Cumulative padded atom counts (multiples of ``TILE_GROUP_SIZE``). group_system : torch.Tensor, shape (ngroup,), dtype=int32 System index for each 32-atom group. group_ptr : torch.Tensor, shape (num_systems + 1,), dtype=int32 CSR group-level pointer derived from ``batch_ptr_padded``. group_ctr_x : torch.Tensor, shape (ngroup_padded,), dtype=dtype Group bounding-box centre x-coordinate (zeroed; written by build). group_ctr_y : torch.Tensor, shape (ngroup_padded,), dtype=dtype Group bounding-box centre y-coordinate (zeroed; written by build). group_ctr_z : torch.Tensor, shape (ngroup_padded,), dtype=dtype Group bounding-box centre z-coordinate (zeroed; written by build). group_ext_x : torch.Tensor, shape (ngroup_padded,), dtype=dtype Group bounding-box half-extent x (zeroed; written by build). group_ext_y : torch.Tensor, shape (ngroup_padded,), dtype=dtype Group bounding-box half-extent y (zeroed; written by build). group_ext_z : torch.Tensor, shape (ngroup_padded,), dtype=dtype Group bounding-box half-extent z (zeroed; written by build). num_tiles : torch.Tensor, shape (1,), dtype=int32 Scalar tile-pair count; written by build, read by query. tile_row_group : torch.Tensor, shape (max_tiles,), dtype=int32 Row group index for each tile pair (zeroed; written by build). tile_col_group : torch.Tensor, shape (max_tiles,), dtype=int32 Column group index for each tile pair (zeroed; written by build). tile_system : torch.Tensor, shape (max_tiles,), dtype=int32 System index for each tile pair (zeroed; written by build). """ n_padded, ngroup, ngroup_padded, max_tiles, num_systems = ( estimate_batch_cluster_tile_list_sizes( batch_ptr, max_tiles_per_group=max_tiles_per_group, ) ) N = int(batch_ptr[-1].item()) sorted_atom_index = torch.empty(n_padded, dtype=torch.int32, device=device) sort_inv = torch.empty(N, dtype=torch.int32, device=device) sorted_pos_x = torch.empty(n_padded, dtype=dtype, device=device) sorted_pos_y = torch.empty(n_padded, dtype=dtype, device=device) sorted_pos_z = torch.empty(n_padded, dtype=dtype, device=device) batch_idx_sorted = torch.empty(n_padded, dtype=torch.int32, device=device) batch_ptr_padded = torch.empty(num_systems + 1, dtype=torch.int32, device=device) group_system = torch.empty(ngroup, dtype=torch.int32, device=device) group_ptr = torch.empty(num_systems + 1, dtype=torch.int32, device=device) group_ctr_x = torch.zeros(ngroup_padded, dtype=dtype, device=device) group_ctr_y = torch.zeros(ngroup_padded, dtype=dtype, device=device) group_ctr_z = torch.zeros(ngroup_padded, dtype=dtype, device=device) group_ext_x = torch.zeros(ngroup_padded, dtype=dtype, device=device) group_ext_y = torch.zeros(ngroup_padded, dtype=dtype, device=device) group_ext_z = torch.zeros(ngroup_padded, dtype=dtype, device=device) num_tiles = torch.zeros(1, dtype=torch.int32, device=device) tile_row_group = torch.zeros(max_tiles, dtype=torch.int32, device=device) tile_col_group = torch.zeros(max_tiles, dtype=torch.int32, device=device) tile_system = torch.zeros(max_tiles, dtype=torch.int32, device=device) return ( sorted_atom_index, sort_inv, sorted_pos_x, sorted_pos_y, sorted_pos_z, batch_idx_sorted, batch_ptr_padded, group_system, group_ptr, group_ctr_x, group_ctr_y, group_ctr_z, group_ext_x, group_ext_y, group_ext_z, num_tiles, tile_row_group, tile_col_group, tile_system, )
# ============================================================================= # Morton sort + padded scatter (torch side) # ============================================================================= class _BatchPartitionMetadata(NamedTuple): """Tensors derived only from a fixed batch partition.""" atom_system: torch.Tensor batch_ptr_padded: torch.Tensor padded_slot_system: torch.Tensor real_sorted_rank_to_padded_slot: torch.Tensor group_ptr: torch.Tensor group_system: torch.Tensor def _prepare_batch_partition_metadata( batch_ptr: torch.Tensor, *, num_atoms: int, padded_slot_system: torch.Tensor, batch_ptr_padded: torch.Tensor, group_system: torch.Tensor, group_ptr: torch.Tensor, ) -> _BatchPartitionMetadata: """Build partition metadata into the fixed scratch views.""" num_systems = batch_ptr.shape[0] - 1 device = batch_ptr.device atoms_per_system = batch_ptr[1:] - batch_ptr[:-1] padded_atoms_per_system = ( (atoms_per_system + TILE_GROUP_SIZE - 1) // TILE_GROUP_SIZE ) * TILE_GROUP_SIZE atom_system = torch.repeat_interleave( torch.arange(num_systems, dtype=torch.int32, device=device), atoms_per_system.to(torch.int64), output_size=num_atoms, ).contiguous() padded_ptr = torch.zeros(num_systems + 1, dtype=torch.int32, device=device) torch.cumsum(padded_atoms_per_system, dim=0, out=padded_ptr[1:]) slot_system = torch.repeat_interleave( torch.arange(num_systems, dtype=torch.int32, device=device), padded_atoms_per_system.to(torch.int64), output_size=padded_slot_system.shape[0], ).contiguous() real_rank = torch.arange(num_atoms, dtype=torch.int32, device=device) padded_slot = ( padded_ptr[atom_system] + real_rank - batch_ptr[atom_system] ).contiguous() group_level_ptr = (padded_ptr // TILE_GROUP_SIZE).to(torch.int32).contiguous() group_owner = slot_system[::TILE_GROUP_SIZE].to(torch.int32).contiguous() batch_ptr_padded.copy_(padded_ptr) padded_slot_system.copy_(slot_system) group_ptr.copy_(group_level_ptr) group_system.copy_(group_owner) return _BatchPartitionMetadata( atom_system=atom_system, batch_ptr_padded=batch_ptr_padded, padded_slot_system=padded_slot_system, real_sorted_rank_to_padded_slot=padded_slot, group_ptr=group_ptr, group_system=group_system, ) def _per_atom_morton_codes( positions: torch.Tensor, batch_idx: torch.Tensor, inv_cell_batch: torch.Tensor, ) -> torch.Tensor: """30-bit Morton codes using per-atom fractional coords (triclinic-safe).""" inv_per_atom = inv_cell_batch[batch_idx] frac = torch.einsum("ni,nij->nj", positions, inv_per_atom) frac = frac - torch.floor(frac) bucket = (frac * 1024.0).clamp(0, 1023).to(torch.int32) ix, iy, iz = bucket.unbind(dim=-1) def _spread(x: torch.Tensor) -> torch.Tensor: x = x & 0x3FF x = (x | (x << 16)) & 0x030000FF x = (x | (x << 8)) & 0x0300F00F x = (x | (x << 4)) & 0x030C30C3 x = (x | (x << 2)) & 0x09249249 return x return (_spread(iz) << 2) | (_spread(iy) << 1) | _spread(ix) def _batched_morton_sort_padded( positions: torch.Tensor, batch_ptr: torch.Tensor, inv_cell_batch: torch.Tensor, atom_system: torch.Tensor, padded_slot_system: torch.Tensor, real_sorted_rank_to_padded_slot: torch.Tensor, sorted_atom_index: torch.Tensor, sort_inv: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, ) -> None: """Per-system Morton sort into the padded layout (torch ops only).""" N = positions.shape[0] device = positions.device codes = _per_atom_morton_codes(positions, atom_system, inv_cell_batch) # System-major key: system index in high 32 bits, Morton code in low. system_major = codes.to(torch.int64) | (atom_system.to(torch.int64) << 32) sorted_atom_index_real = torch.argsort(system_major).to(torch.int32) # Inverse permutation over real atoms. inv = torch.empty(N, dtype=torch.int32, device=device) inv[sorted_atom_index_real] = torch.arange( N, dtype=torch.int32, device=device, ) sort_inv.copy_(inv) # sorted_atom_index[k] = N is the padding sentinel. sp = torch.full( (sorted_atom_index.shape[0],), N, dtype=torch.int32, device=device, ) sp[real_sorted_rank_to_padded_slot] = sorted_atom_index_real sorted_atom_index.copy_(sp) # Padding slots duplicate each system's first real atom, so # tile_min / tile_max over the group remain well-formed. Duplicates # cannot cause a miss because they add a point already inside the # system's spatial region. first_atom_per_system = batch_ptr[:-1] position_index = torch.where( sp == N, first_atom_per_system[padded_slot_system], sp, ) sorted_pos = positions[position_index].contiguous() sorted_pos_x.copy_(sorted_pos[:, 0].contiguous()) sorted_pos_y.copy_(sorted_pos[:, 1].contiguous()) sorted_pos_z.copy_(sorted_pos[:, 2].contiguous()) # ============================================================================= # Component ops (torch wrappers around the warp launchers) # ============================================================================= @scoped_torch_warp_stream def _batch_build_cluster_tile_list_impl( positions: torch.Tensor, cutoff: float, cell_batch: torch.Tensor, inv_cell_batch: torch.Tensor, batch_ptr: torch.Tensor, atom_system: torch.Tensor, real_sorted_rank_to_padded_slot: torch.Tensor, sorted_atom_index: torch.Tensor, sort_inv: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, batch_idx_sorted: torch.Tensor, batch_ptr_padded: torch.Tensor, group_system: torch.Tensor, group_ptr: torch.Tensor, group_ctr_x: torch.Tensor, group_ctr_y: torch.Tensor, group_ctr_z: torch.Tensor, group_ext_x: torch.Tensor, group_ext_y: torch.Tensor, group_ext_z: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, rebuild_flags: torch.Tensor, tile_offsets: torch.Tensor, tile_counts: torch.Tensor, use_rebuild_flags: bool, use_segmented: bool, compute_inv_cell_batch: bool, fixed_cell: bool, qr: torch.Tensor | None, axis_aligned: torch.Tensor | None, fractional_rounding_certified: torch.Tensor | None, qr_height_certified: torch.Tensor | None, bbox_cutoff_bounds: torch.Tensor | None, ) -> None: device = positions.device if compute_inv_cell_batch: computed_inv_cell_batch, info = torch.linalg.inv_ex( cell_batch, check_errors=False, ) torch._assert_async( (info == 0).all(), "cell matrices must be non-singular", ) inv_cell_batch.copy_(computed_inv_cell_batch) wp_device = str(device) wp_dtype = get_wp_dtype(positions.dtype) _batched_morton_sort_padded( positions, batch_ptr, inv_cell_batch, atom_system, batch_idx_sorted, real_sorted_rank_to_padded_slot, sorted_atom_index, sort_inv, sorted_pos_x, sorted_pos_y, sorted_pos_z, ) if not use_segmented: num_tiles.zero_() group_ctr_x.zero_() group_ctr_y.zero_() group_ctr_z.zero_() group_ext_x.zero_() group_ext_y.zero_() group_ext_z.zero_() wp_batch_build_cluster_tile_list( sorted_pos_x=wp.from_torch(sorted_pos_x, dtype=wp_dtype, return_ctype=True), sorted_pos_y=wp.from_torch(sorted_pos_y, dtype=wp_dtype, return_ctype=True), sorted_pos_z=wp.from_torch(sorted_pos_z, dtype=wp_dtype, return_ctype=True), group_system=wp.from_torch(group_system, dtype=wp.int32, return_ctype=True), group_ptr=wp.from_torch(group_ptr, dtype=wp.int32, return_ctype=True), cell_batch=wp.from_torch( cell_batch.contiguous(), dtype=wp.mat33f, return_ctype=True, ), inv_cell_batch=wp.from_torch( inv_cell_batch.contiguous(), dtype=wp.mat33f, return_ctype=True, ), cutoff=float(cutoff), num_tiles=wp.from_torch(num_tiles, dtype=wp.int32, return_ctype=True), tile_row_group=wp.from_torch(tile_row_group, dtype=wp.int32, return_ctype=True), tile_col_group=wp.from_torch( tile_col_group, dtype=wp.int32, return_ctype=True, ), tile_system=wp.from_torch(tile_system, dtype=wp.int32, return_ctype=True), wp_dtype=wp_dtype, device=wp_device, rebuild_flags=( wp.from_torch(rebuild_flags, dtype=wp.bool, return_ctype=True) if use_rebuild_flags else None ), tile_offsets=( wp.from_torch(tile_offsets, dtype=wp.int32, return_ctype=True) if use_segmented else None ), tile_counts=( wp.from_torch(tile_counts, dtype=wp.int32, return_ctype=True) if use_segmented else None ), fixed_cell=fixed_cell, qr=( wp.from_torch(qr, dtype=wp.float32, return_ctype=True) if qr is not None else None ), axis_aligned=( wp.from_torch(axis_aligned, dtype=wp.bool, return_ctype=True) if axis_aligned is not None else None ), fractional_rounding_certified=( wp.from_torch( fractional_rounding_certified, dtype=wp.bool, return_ctype=True ) if fractional_rounding_certified is not None else None ), qr_height_certified=( wp.from_torch(qr_height_certified, dtype=wp.bool, return_ctype=True) if qr_height_certified is not None else None ), bbox_cutoff_bounds=( wp.from_torch(bbox_cutoff_bounds, dtype=wp.float32, return_ctype=True) if bbox_cutoff_bounds is not None else None ), group_ctr_x_buffer=wp.from_torch( group_ctr_x, dtype=wp_dtype, return_ctype=True ), group_ctr_y_buffer=wp.from_torch( group_ctr_y, dtype=wp_dtype, return_ctype=True ), group_ctr_z_buffer=wp.from_torch( group_ctr_z, dtype=wp_dtype, return_ctype=True ), group_ext_x_buffer=wp.from_torch( group_ext_x, dtype=wp_dtype, return_ctype=True ), group_ext_y_buffer=wp.from_torch( group_ext_y, dtype=wp_dtype, return_ctype=True ), group_ext_z_buffer=wp.from_torch( group_ext_z, dtype=wp_dtype, return_ctype=True ), ) @torch.library.custom_op( "nvalchemiops::_batch_build_cluster_tile_list", mutates_args=( "sorted_atom_index", "sort_inv", "sorted_pos_x", "sorted_pos_y", "sorted_pos_z", "group_ctr_x", "group_ctr_y", "group_ctr_z", "group_ext_x", "group_ext_y", "group_ext_z", "inv_cell_batch", "num_tiles", "tile_row_group", "tile_col_group", "tile_system", "tile_counts", ), ) def _batch_build_cluster_tile_list_op( positions: torch.Tensor, cutoff: float, cell_batch: torch.Tensor, inv_cell_batch: torch.Tensor, batch_ptr: torch.Tensor, atom_system: torch.Tensor, real_sorted_rank_to_padded_slot: torch.Tensor, sorted_atom_index: torch.Tensor, sort_inv: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, batch_idx_sorted: torch.Tensor, batch_ptr_padded: torch.Tensor, group_system: torch.Tensor, group_ptr: torch.Tensor, group_ctr_x: torch.Tensor, group_ctr_y: torch.Tensor, group_ctr_z: torch.Tensor, group_ext_x: torch.Tensor, group_ext_y: torch.Tensor, group_ext_z: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, rebuild_flags: torch.Tensor, tile_offsets: torch.Tensor, tile_counts: torch.Tensor, use_rebuild_flags: bool, use_segmented: bool, compute_inv_cell_batch: bool, ) -> None: _batch_build_cluster_tile_list_impl( positions, cutoff, cell_batch, inv_cell_batch, batch_ptr, atom_system, real_sorted_rank_to_padded_slot, sorted_atom_index, sort_inv, sorted_pos_x, sorted_pos_y, sorted_pos_z, batch_idx_sorted, batch_ptr_padded, group_system, group_ptr, group_ctr_x, group_ctr_y, group_ctr_z, group_ext_x, group_ext_y, group_ext_z, num_tiles, tile_row_group, tile_col_group, tile_system, rebuild_flags, tile_offsets, tile_counts, use_rebuild_flags, use_segmented, compute_inv_cell_batch, False, None, None, None, None, None, ) @torch.library.custom_op( "nvalchemiops::_batch_build_cluster_tile_list_fixed", mutates_args=( "sorted_atom_index", "sort_inv", "sorted_pos_x", "sorted_pos_y", "sorted_pos_z", "group_ctr_x", "group_ctr_y", "group_ctr_z", "group_ext_x", "group_ext_y", "group_ext_z", "num_tiles", "tile_row_group", "tile_col_group", "tile_system", "tile_counts", ), ) def _batch_build_cluster_tile_list_fixed_op( positions: torch.Tensor, cutoff: float, cell_batch: torch.Tensor, inv_cell_batch: torch.Tensor, batch_ptr: torch.Tensor, atom_system: torch.Tensor, real_sorted_rank_to_padded_slot: torch.Tensor, sorted_atom_index: torch.Tensor, sort_inv: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, batch_idx_sorted: torch.Tensor, batch_ptr_padded: torch.Tensor, group_system: torch.Tensor, group_ptr: torch.Tensor, group_ctr_x: torch.Tensor, group_ctr_y: torch.Tensor, group_ctr_z: torch.Tensor, group_ext_x: torch.Tensor, group_ext_y: torch.Tensor, group_ext_z: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, rebuild_flags: torch.Tensor, tile_offsets: torch.Tensor, tile_counts: torch.Tensor, use_rebuild_flags: bool, use_segmented: bool, qr: torch.Tensor, axis_aligned: torch.Tensor, fractional_rounding_certified: torch.Tensor, qr_height_certified: torch.Tensor, bbox_cutoff_bounds: torch.Tensor, ) -> None: _batch_build_cluster_tile_list_impl( positions, cutoff, cell_batch, inv_cell_batch, batch_ptr, atom_system, real_sorted_rank_to_padded_slot, sorted_atom_index, sort_inv, sorted_pos_x, sorted_pos_y, sorted_pos_z, batch_idx_sorted, batch_ptr_padded, group_system, group_ptr, group_ctr_x, group_ctr_y, group_ctr_z, group_ext_x, group_ext_y, group_ext_z, num_tiles, tile_row_group, tile_col_group, tile_system, rebuild_flags, tile_offsets, tile_counts, use_rebuild_flags, use_segmented, False, True, qr, axis_aligned, fractional_rounding_certified, qr_height_certified, bbox_cutoff_bounds, ) @_batch_build_cluster_tile_list_op.register_fake def _( positions: torch.Tensor, cutoff: float, cell_batch: torch.Tensor, inv_cell_batch: torch.Tensor, batch_ptr: torch.Tensor, atom_system: torch.Tensor, real_sorted_rank_to_padded_slot: torch.Tensor, sorted_atom_index: torch.Tensor, sort_inv: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, batch_idx_sorted: torch.Tensor, batch_ptr_padded: torch.Tensor, group_system: torch.Tensor, group_ptr: torch.Tensor, group_ctr_x: torch.Tensor, group_ctr_y: torch.Tensor, group_ctr_z: torch.Tensor, group_ext_x: torch.Tensor, group_ext_y: torch.Tensor, group_ext_z: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, rebuild_flags: torch.Tensor, tile_offsets: torch.Tensor, tile_counts: torch.Tensor, use_rebuild_flags: bool, use_segmented: bool, compute_inv_cell_batch: bool, ) -> None: return None @_batch_build_cluster_tile_list_fixed_op.register_fake def _( positions: torch.Tensor, cutoff: float, cell_batch: torch.Tensor, inv_cell_batch: torch.Tensor, batch_ptr: torch.Tensor, atom_system: torch.Tensor, real_sorted_rank_to_padded_slot: torch.Tensor, sorted_atom_index: torch.Tensor, sort_inv: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, batch_idx_sorted: torch.Tensor, batch_ptr_padded: torch.Tensor, group_system: torch.Tensor, group_ptr: torch.Tensor, group_ctr_x: torch.Tensor, group_ctr_y: torch.Tensor, group_ctr_z: torch.Tensor, group_ext_x: torch.Tensor, group_ext_y: torch.Tensor, group_ext_z: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, rebuild_flags: torch.Tensor, tile_offsets: torch.Tensor, tile_counts: torch.Tensor, use_rebuild_flags: bool, use_segmented: bool, qr: torch.Tensor, axis_aligned: torch.Tensor, fractional_rounding_certified: torch.Tensor, qr_height_certified: torch.Tensor, bbox_cutoff_bounds: torch.Tensor, ) -> None: return None def _validate_batch_cluster_tile_scratch( *, positions: torch.Tensor, batch_ptr: torch.Tensor, sorted_atom_index: torch.Tensor, sort_inv: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, batch_idx_sorted: torch.Tensor, batch_ptr_padded: torch.Tensor, group_system: torch.Tensor, group_ptr: torch.Tensor, group_ctr_x: torch.Tensor, group_ctr_y: torch.Tensor, group_ctr_z: torch.Tensor, group_ext_x: torch.Tensor, group_ext_y: torch.Tensor, group_ext_z: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, ) -> None: """Validate caller-owned batch build scratch before it is modified.""" int_tensors = { "sorted_atom_index": sorted_atom_index, "sort_inv": sort_inv, "batch_idx_sorted": batch_idx_sorted, "batch_ptr_padded": batch_ptr_padded, "group_system": group_system, "group_ptr": group_ptr, "num_tiles": num_tiles, "tile_row_group": tile_row_group, "tile_col_group": tile_col_group, "tile_system": tile_system, } float_tensors = { "sorted_pos_x": sorted_pos_x, "sorted_pos_y": sorted_pos_y, "sorted_pos_z": sorted_pos_z, "group_ctr_x": group_ctr_x, "group_ctr_y": group_ctr_y, "group_ctr_z": group_ctr_z, "group_ext_x": group_ext_x, "group_ext_y": group_ext_y, "group_ext_z": group_ext_z, } for name, tensor in int_tensors.items(): if tensor.device != positions.device or tensor.dtype != torch.int32: raise ValueError(f"{name} must be an int32 tensor on positions.device") if tensor.ndim != 1: raise ValueError(f"{name} must be 1D") for name, tensor in float_tensors.items(): if tensor.device != positions.device or tensor.dtype != positions.dtype: raise ValueError(f"{name} must match positions device and dtype") if tensor.ndim != 1: raise ValueError(f"{name} must be 1D") n_atoms = positions.shape[0] num_systems = batch_ptr.shape[0] - 1 if sort_inv.shape[0] != n_atoms: raise ValueError("sort_inv must have length N") if batch_ptr_padded.shape[0] != num_systems + 1: raise ValueError("batch_ptr_padded must have length S + 1") if group_ptr.shape[0] != num_systems + 1: raise ValueError("group_ptr must have length S + 1") if num_tiles.shape[0] != 1: raise ValueError("num_tiles must have length 1") tile_capacity = tile_row_group.shape[0] if ( tile_col_group.shape[0] != tile_capacity or tile_system.shape[0] != tile_capacity ): raise ValueError( "tile_row_group, tile_col_group, and tile_system must have equal capacity" ) padded_length = sorted_atom_index.shape[0] padded_tensors = (sorted_pos_x, sorted_pos_y, sorted_pos_z, batch_idx_sorted) if any(tensor.shape[0] != padded_length for tensor in padded_tensors): raise ValueError("padded atom scratch tensors must have a common length") if padded_length % TILE_GROUP_SIZE != 0: raise ValueError( "padded atom scratch length must be divisible by TILE_GROUP_SIZE" ) ngroup = padded_length // TILE_GROUP_SIZE if group_system.shape[0] != ngroup: raise ValueError("group_system must have length ngroup") group_padded = ( (ngroup + TILE_GROUP_SIZE - 1) // TILE_GROUP_SIZE ) * TILE_GROUP_SIZE + TILE_GROUP_SIZE if any( tensor.shape[0] != group_padded for tensor in ( group_ctr_x, group_ctr_y, group_ctr_z, group_ext_x, group_ext_y, group_ext_z, ) ): raise ValueError("group bounding-box scratch must have padded ngroup length") current_padded_atoms = ( ((batch_ptr[1:] - batch_ptr[:-1] + TILE_GROUP_SIZE - 1) // TILE_GROUP_SIZE) * TILE_GROUP_SIZE ).sum() if torch.compiler.is_compiling(): torch._assert_async( current_padded_atoms == padded_length, "scratch padded atom length must match the current batch_ptr", ) elif int(current_padded_atoms.item()) != padded_length: raise ValueError("scratch padded atom length must match the current batch_ptr") def _batch_build_cluster_tile_list_normalized( partition_metadata: _BatchPartitionMetadata | None, positions: torch.Tensor, cutoff: float, cell_batch: torch.Tensor, batch_ptr: torch.Tensor, sorted_atom_index: torch.Tensor, sort_inv: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, batch_idx_sorted: torch.Tensor, batch_ptr_padded: torch.Tensor, group_system: torch.Tensor, group_ptr: torch.Tensor, group_ctr_x: torch.Tensor, group_ctr_y: torch.Tensor, group_ctr_z: torch.Tensor, group_ext_x: torch.Tensor, group_ext_y: torch.Tensor, group_ext_z: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, inv_cell_batch: torch.Tensor | None = None, rebuild_flags: torch.Tensor | None = None, tile_offsets: torch.Tensor | None = None, tile_counts: torch.Tensor | None = None, force_compute_inv_cell: bool = False, _fixed_cell_geometry: _FixedCellGeometry | None = None, ) -> torch.Tensor: """Build batched tile neighbor list state into pre-allocated outputs. Runs the per-system Morton sort + padded SoA gather in torch, then the bbox reduction + tile-pair enumeration in warp. All output tensors are modified in-place. Parameters ---------- positions : torch.Tensor, shape (N, 3), dtype=float32 Concatenated atomic coordinates across all systems. cutoff : float Cartesian cutoff radius used for tile-pair enumeration. cell_batch : torch.Tensor, shape (num_systems, 3, 3), dtype=float32 Per-system unit cell matrices. batch_ptr : torch.Tensor, shape (num_systems + 1,), dtype=int32 CSR pointer giving atom ranges per system. sorted_atom_index : torch.Tensor, shape (n_padded,), dtype=int32 Output permutation array; modified in-place. sort_inv : torch.Tensor, shape (N,), dtype=int32 Output inverse permutation; modified in-place. sorted_pos_x : torch.Tensor, shape (n_padded,), dtype=float32 Output SoA x-positions; modified in-place. sorted_pos_y : torch.Tensor, shape (n_padded,), dtype=float32 Output SoA y-positions; modified in-place. sorted_pos_z : torch.Tensor, shape (n_padded,), dtype=float32 Output SoA z-positions; modified in-place. batch_idx_sorted : torch.Tensor, shape (n_padded,), dtype=int32 Output system index per padded slot; modified in-place. batch_ptr_padded : torch.Tensor, shape (num_systems + 1,), dtype=int32 Output padded CSR pointer; modified in-place. group_system : torch.Tensor, shape (ngroup,), dtype=int32 Output system index per group; modified in-place. group_ptr : torch.Tensor, shape (num_systems + 1,), dtype=int32 Output group-level CSR pointer; modified in-place. group_ctr_x : torch.Tensor, shape (ngroup_padded,), dtype=float32 Output group bounding-box centre x; modified in-place. group_ctr_y : torch.Tensor, shape (ngroup_padded,), dtype=float32 Output group bounding-box centre y; modified in-place. group_ctr_z : torch.Tensor, shape (ngroup_padded,), dtype=float32 Output group bounding-box centre z; modified in-place. group_ext_x : torch.Tensor, shape (ngroup_padded,), dtype=float32 Output group bounding-box half-extent x; modified in-place. group_ext_y : torch.Tensor, shape (ngroup_padded,), dtype=float32 Output group bounding-box half-extent y; modified in-place. group_ext_z : torch.Tensor, shape (ngroup_padded,), dtype=float32 Output group bounding-box half-extent z; modified in-place. num_tiles : torch.Tensor, shape (1,), dtype=int32 Output tile-pair count (scalar); modified in-place. tile_row_group : torch.Tensor, shape (max_tiles,), dtype=int32 Output row group per tile pair; modified in-place. tile_col_group : torch.Tensor, shape (max_tiles,), dtype=int32 Output column group per tile pair; modified in-place. tile_system : torch.Tensor, shape (max_tiles,), dtype=int32 Output system index per tile pair; modified in-place. inv_cell_batch : torch.Tensor, shape (num_systems, 3, 3), dtype=float32, optional Pre-computed inverse of ``cell_batch``. Computed internally when None. rebuild_flags : torch.Tensor, shape (num_systems,), dtype=bool, optional Per-system selective rebuild flags. Requires ``tile_offsets`` and ``tile_counts`` (segmented path). tile_offsets : torch.Tensor, shape (num_systems + 1,), dtype=int32, optional Fixed per-system tile segment offsets for the segmented build path. Must be provided together with ``tile_counts``. tile_counts : torch.Tensor, shape (num_systems,), dtype=int32, optional Output per-system tile counts for the segmented build path; modified in-place. Must be provided together with ``tile_offsets``. """ if positions.dtype != torch.float32: raise TypeError("positions must be float32") if ( cell_batch.dtype != torch.float32 or cell_batch.ndim != 3 or cell_batch.shape[1:] != (3, 3) ): raise ValueError( f"cell_batch must be (S, 3, 3) float32; got {tuple(cell_batch.shape)}", ) if partition_metadata is None: if batch_ptr.dtype != torch.int32 or batch_ptr.ndim != 1: raise ValueError("batch_ptr must be 1D int32") if batch_ptr.shape[0] < 2: raise ValueError("batch_ptr must have length at least 2") num_systems = cell_batch.shape[0] if batch_ptr.shape[0] != num_systems + 1: raise ValueError( f"batch_ptr length {batch_ptr.shape[0]} != num_systems+1 = {num_systems + 1}", ) if partition_metadata is None: N = positions.shape[0] if torch.compiler.is_compiling(): torch._assert_async( batch_ptr[-1] == N, "batch_ptr[-1] must equal positions.shape[0]", ) elif int(batch_ptr[-1].item()) != N: raise ValueError(f"batch_ptr[-1] ({int(batch_ptr[-1])}) != N ({N})") _validate_batch_cluster_tile_scratch( positions=positions, batch_ptr=batch_ptr, sorted_atom_index=sorted_atom_index, sort_inv=sort_inv, sorted_pos_x=sorted_pos_x, sorted_pos_y=sorted_pos_y, sorted_pos_z=sorted_pos_z, batch_idx_sorted=batch_idx_sorted, batch_ptr_padded=batch_ptr_padded, group_system=group_system, group_ptr=group_ptr, group_ctr_x=group_ctr_x, group_ctr_y=group_ctr_y, group_ctr_z=group_ctr_z, group_ext_x=group_ext_x, group_ext_y=group_ext_y, group_ext_z=group_ext_z, num_tiles=num_tiles, tile_row_group=tile_row_group, tile_col_group=tile_col_group, tile_system=tile_system, ) partition_metadata = _prepare_batch_partition_metadata( batch_ptr, num_atoms=positions.shape[0], padded_slot_system=batch_idx_sorted, batch_ptr_padded=batch_ptr_padded, group_system=group_system, group_ptr=group_ptr, ) elif ( partition_metadata.batch_ptr_padded is not batch_ptr_padded or partition_metadata.padded_slot_system is not batch_idx_sorted or partition_metadata.group_system is not group_system or partition_metadata.group_ptr is not group_ptr ): raise ValueError("prepared partition metadata must belong to batch scratch") compute_inv_cell_batch = _fixed_cell_geometry is None and ( force_compute_inv_cell or (inv_cell_batch is None and torch.compiler.is_compiling()) ) if inv_cell_batch is None: inv_cell_batch = ( torch.empty_like(cell_batch) if compute_inv_cell_batch else torch.linalg.inv(cell_batch).contiguous() ) use_segmented = (tile_offsets is not None) or (tile_counts is not None) if (tile_offsets is None) != (tile_counts is None): raise ValueError("Pass both 'tile_offsets' and 'tile_counts', or neither.") use_rebuild_flags = rebuild_flags is not None if use_rebuild_flags and not use_segmented: raise ValueError( "rebuild_flags requires tile_offsets and tile_counts for batch_cluster_tile" ) dummy_i32 = torch.empty(1, dtype=torch.int32, device=positions.device) dummy_bool = torch.empty(1, dtype=torch.bool, device=positions.device) build_args = ( positions, float(cutoff), cell_batch, inv_cell_batch, batch_ptr, partition_metadata.atom_system, partition_metadata.real_sorted_rank_to_padded_slot, sorted_atom_index, sort_inv, sorted_pos_x, sorted_pos_y, sorted_pos_z, batch_idx_sorted, batch_ptr_padded, group_system, group_ptr, group_ctr_x, group_ctr_y, group_ctr_z, group_ext_x, group_ext_y, group_ext_z, num_tiles, tile_row_group, tile_col_group, tile_system, rebuild_flags if rebuild_flags is not None else dummy_bool, tile_offsets if tile_offsets is not None else dummy_i32, tile_counts if tile_counts is not None else dummy_i32, bool(use_rebuild_flags), bool(use_segmented), ) if _fixed_cell_geometry is None: _batch_build_cluster_tile_list_op(*build_args, bool(compute_inv_cell_batch)) else: geometry = _fixed_cell_geometry if any( value is None for value in ( geometry.qr, geometry.axis_aligned, geometry.fractional_rounding_certified, geometry.qr_height_certified, geometry.bbox_cutoff_bounds, ) ): raise RuntimeError("prepared cluster geometry cache is incomplete") _batch_build_cluster_tile_list_fixed_op( *build_args, geometry.qr, geometry.axis_aligned, geometry.fractional_rounding_certified, geometry.qr_height_certified, geometry.bbox_cutoff_bounds, ) return inv_cell_batch
[docs] def batch_build_cluster_tile_list( positions: torch.Tensor, cutoff: float, cell_batch: torch.Tensor, batch_ptr: torch.Tensor, sorted_atom_index: torch.Tensor, sort_inv: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, batch_idx_sorted: torch.Tensor, batch_ptr_padded: torch.Tensor, group_system: torch.Tensor, group_ptr: torch.Tensor, group_ctr_x: torch.Tensor, group_ctr_y: torch.Tensor, group_ctr_z: torch.Tensor, group_ext_x: torch.Tensor, group_ext_y: torch.Tensor, group_ext_z: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, inv_cell_batch: torch.Tensor | None = None, rebuild_flags: torch.Tensor | None = None, tile_offsets: torch.Tensor | None = None, tile_counts: torch.Tensor | None = None, ) -> None: """Build batched cluster-tile state into caller-owned scratch.""" _batch_build_cluster_tile_list_normalized( None, positions, cutoff, cell_batch, batch_ptr, sorted_atom_index, sort_inv, sorted_pos_x, sorted_pos_y, sorted_pos_z, batch_idx_sorted, batch_ptr_padded, group_system, group_ptr, group_ctr_x, group_ctr_y, group_ctr_z, group_ext_x, group_ext_y, group_ext_z, num_tiles, tile_row_group, tile_col_group, tile_system, inv_cell_batch=inv_cell_batch, rebuild_flags=rebuild_flags, tile_offsets=tile_offsets, tile_counts=tile_counts, )
batch_build_cluster_tile_list.__doc__ = ( _batch_build_cluster_tile_list_normalized.__doc__ ) @torch.library.custom_op( "nvalchemiops::_batch_query_cluster_tile", mutates_args=( "neighbor_matrix", "num_neighbors", "neighbor_matrix_shifts", "neighbor_vectors", "neighbor_distances", ), ) @scoped_torch_warp_stream def _batch_query_cluster_tile_op( cutoff: float, natom: int, sorted_atom_index: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, cell_batch: torch.Tensor, inv_cell_batch: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, neighbor_matrix: torch.Tensor, num_neighbors: torch.Tensor, neighbor_matrix_shifts: torch.Tensor, neighbor_vectors: torch.Tensor, neighbor_distances: torch.Tensor, n_tiles: int, return_vectors: bool, return_distances: bool, fixed_cell: bool, qr: torch.Tensor | None, axis_aligned: torch.Tensor | None, fractional_rounding_certified: torch.Tensor | None, qr_height_certified: torch.Tensor | None, bbox_cutoff_bounds: torch.Tensor | None, ) -> None: # ``n_tiles`` (host-synced compact tile count) tightens the launch; the # kernel still guards per-tile defensively. ``n_tiles <= 0`` (segmented # path, count unknown here) falls back to the full-buffer launch. # Warp writes active pairs only. Clear padding inside the same opaque # mutation boundary before launching the query. if return_vectors: neighbor_vectors.zero_() if return_distances: neighbor_distances.zero_() wp_device = str(sorted_pos_x.device) wp_dtype = get_wp_dtype(sorted_pos_x.dtype) wp_batch_query_cluster_tile( n_tiles=int(n_tiles) if n_tiles > 0 else None, sorted_atom_index=wp.from_torch( sorted_atom_index, dtype=wp.int32, return_ctype=True ), sorted_pos_x=wp.from_torch(sorted_pos_x, dtype=wp_dtype, return_ctype=True), sorted_pos_y=wp.from_torch(sorted_pos_y, dtype=wp_dtype, return_ctype=True), sorted_pos_z=wp.from_torch(sorted_pos_z, dtype=wp_dtype, return_ctype=True), cell_batch=wp.from_torch( cell_batch.contiguous(), dtype=wp.mat33f, return_ctype=True, ), inv_cell_batch=wp.from_torch( inv_cell_batch.contiguous(), dtype=wp.mat33f, return_ctype=True, ), num_tiles=wp.from_torch(num_tiles, dtype=wp.int32, return_ctype=True), tile_row_group=wp.from_torch(tile_row_group, dtype=wp.int32, return_ctype=True), tile_col_group=wp.from_torch( tile_col_group, dtype=wp.int32, return_ctype=True, ), tile_system=wp.from_torch(tile_system, dtype=wp.int32, return_ctype=True), cutoff=float(cutoff), natom=int(natom), neighbor_matrix=wp.from_torch( neighbor_matrix, dtype=wp.int32, return_ctype=True, ), num_neighbors=wp.from_torch( num_neighbors, dtype=wp.int32, return_ctype=True, ), neighbor_matrix_shifts=wp.from_torch( neighbor_matrix_shifts, dtype=wp.int32, return_ctype=True, ), wp_dtype=wp_dtype, device=wp_device, return_vectors=bool(return_vectors), return_distances=bool(return_distances), neighbor_vectors=( wp.from_torch(neighbor_vectors, dtype=wp.vec3f, return_ctype=True) if return_vectors else None ), neighbor_distances=( wp.from_torch(neighbor_distances, dtype=wp_dtype, return_ctype=True) if return_distances else None ), **_wp_fixed_cluster_geometry( fixed_cell, qr, axis_aligned, fractional_rounding_certified, qr_height_certified, bbox_cutoff_bounds, ), ) @_batch_query_cluster_tile_op.register_fake def _( cutoff: float, natom: int, sorted_atom_index: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, cell_batch: torch.Tensor, inv_cell_batch: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, neighbor_matrix: torch.Tensor, num_neighbors: torch.Tensor, neighbor_matrix_shifts: torch.Tensor, neighbor_vectors: torch.Tensor, neighbor_distances: torch.Tensor, n_tiles: int, return_vectors: bool, return_distances: bool, fixed_cell: bool, qr: torch.Tensor | None, axis_aligned: torch.Tensor | None, fractional_rounding_certified: torch.Tensor | None, qr_height_certified: torch.Tensor | None, bbox_cutoff_bounds: torch.Tensor | None, ) -> None: return None @torch.library.custom_op( "nvalchemiops::_batch_query_cluster_tile_topology", mutates_args=( "neighbor_matrix", "num_neighbors", "neighbor_matrix_shifts", "neighbor_matrix2", "num_neighbors2", "neighbor_matrix_shifts2", "neighbor_vectors", "neighbor_distances", ), ) @scoped_torch_warp_stream def _batch_query_cluster_tile_topology_op( cutoff: float, cutoff2: float, natom: int, sorted_atom_index: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, cell_batch: torch.Tensor, inv_cell_batch: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, neighbor_matrix: torch.Tensor, num_neighbors: torch.Tensor, neighbor_matrix_shifts: torch.Tensor, neighbor_matrix2: torch.Tensor, num_neighbors2: torch.Tensor, neighbor_matrix_shifts2: torch.Tensor, neighbor_vectors: torch.Tensor, neighbor_distances: torch.Tensor, rebuild_flags: torch.Tensor, tile_offsets: torch.Tensor, tile_counts: torch.Tensor, batch_idx: torch.Tensor, n_tiles: int, use_cutoff2: bool, use_rebuild_flags: bool, use_segmented: bool, return_vectors: bool, return_distances: bool, fixed_cell: bool, qr: torch.Tensor | None, axis_aligned: torch.Tensor | None, fractional_rounding_certified: torch.Tensor | None, qr_height_certified: torch.Tensor | None, bbox_cutoff_bounds: torch.Tensor | None, ) -> None: if use_rebuild_flags: wp_selective_zero_num_neighbors( wp.from_torch( num_neighbors, dtype=wp.int32, requires_grad=False, return_ctype=True, ), wp.from_torch(batch_idx, dtype=wp.int32, return_ctype=True), wp.from_torch(rebuild_flags, dtype=wp.bool, return_ctype=True), str(sorted_pos_x.device), ) if use_cutoff2: wp_selective_zero_num_neighbors( wp.from_torch( num_neighbors2, dtype=wp.int32, requires_grad=False, return_ctype=True, ), wp.from_torch(batch_idx, dtype=wp.int32, return_ctype=True), wp.from_torch(rebuild_flags, dtype=wp.bool, return_ctype=True), str(sorted_pos_x.device), ) # Clear complete rows only for rebuilt systems. False-flag rows retain # active entries and padding exactly as supplied by the caller. row_update = rebuild_flags[batch_idx.to(torch.long)] if return_vectors: neighbor_vectors.masked_fill_(row_update[:, None, None], 0) if return_distances: neighbor_distances.masked_fill_(row_update[:, None], 0) else: if return_vectors: neighbor_vectors.zero_() if return_distances: neighbor_distances.zero_() _batch_query_cluster_tile_optional( cell_batch, inv_cell_batch, natom, cutoff, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, tile_system, neighbor_matrix, num_neighbors, neighbor_matrix_shifts, n_tiles=n_tiles, cutoff2=cutoff2 if use_cutoff2 else None, neighbor_matrix2=neighbor_matrix2 if use_cutoff2 else None, num_neighbors2=num_neighbors2 if use_cutoff2 else None, neighbor_matrix_shifts2=neighbor_matrix_shifts2 if use_cutoff2 else None, rebuild_flags=rebuild_flags if use_rebuild_flags else None, tile_offsets=tile_offsets if use_segmented else None, tile_counts=tile_counts if use_segmented else None, return_vectors=return_vectors, return_distances=return_distances, pair_fn=None, pair_params=None, neighbor_vectors=neighbor_vectors if return_vectors else None, neighbor_distances=neighbor_distances if return_distances else None, pair_energies=None, pair_forces=None, fixed_cell=fixed_cell, qr=qr, axis_aligned=axis_aligned, fractional_rounding_certified=fractional_rounding_certified, qr_height_certified=qr_height_certified, bbox_cutoff_bounds=bbox_cutoff_bounds, ) @_batch_query_cluster_tile_topology_op.register_fake def _( cutoff: float, cutoff2: float, natom: int, sorted_atom_index: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, cell_batch: torch.Tensor, inv_cell_batch: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, neighbor_matrix: torch.Tensor, num_neighbors: torch.Tensor, neighbor_matrix_shifts: torch.Tensor, neighbor_matrix2: torch.Tensor, num_neighbors2: torch.Tensor, neighbor_matrix_shifts2: torch.Tensor, neighbor_vectors: torch.Tensor, neighbor_distances: torch.Tensor, rebuild_flags: torch.Tensor, tile_offsets: torch.Tensor, tile_counts: torch.Tensor, batch_idx: torch.Tensor, n_tiles: int, use_cutoff2: bool, use_rebuild_flags: bool, use_segmented: bool, return_vectors: bool, return_distances: bool, fixed_cell: bool, qr: torch.Tensor | None, axis_aligned: torch.Tensor | None, fractional_rounding_certified: torch.Tensor | None, qr_height_certified: torch.Tensor | None, bbox_cutoff_bounds: torch.Tensor | None, ) -> None: return None @torch.library.custom_op( "nvalchemiops::_batch_cluster_tile_selective_fill_neighbor_matrix_tail", mutates_args=("neighbor_matrix", "neighbor_matrix2"), ) @scoped_torch_warp_stream def _batch_cluster_tile_selective_fill_neighbor_matrix_tail_op( num_neighbors: torch.Tensor, num_neighbors2: torch.Tensor, batch_idx: torch.Tensor, rebuild_flags: torch.Tensor, neighbor_matrix: torch.Tensor, neighbor_matrix2: torch.Tensor, max_neighbors: int, fill_value: int, use_cutoff2: bool, ) -> None: if max_neighbors <= 0: return wp_device = str(neighbor_matrix.device) wp_selective_fill_neighbor_matrix_tail( wp.from_torch( num_neighbors, dtype=wp.int32, requires_grad=False, return_ctype=True ), wp.from_torch( batch_idx, dtype=wp.int32, requires_grad=False, return_ctype=True ), wp.from_torch( rebuild_flags, dtype=wp.bool, requires_grad=False, return_ctype=True ), int(num_neighbors.shape[0]), int(max_neighbors), int(fill_value), wp.from_torch( neighbor_matrix, dtype=wp.int32, requires_grad=False, return_ctype=True ), wp_device, batched=True, ) if use_cutoff2: wp_selective_fill_neighbor_matrix_tail( wp.from_torch( num_neighbors2, dtype=wp.int32, requires_grad=False, return_ctype=True ), wp.from_torch( batch_idx, dtype=wp.int32, requires_grad=False, return_ctype=True ), wp.from_torch( rebuild_flags, dtype=wp.bool, requires_grad=False, return_ctype=True ), int(num_neighbors2.shape[0]), int(max_neighbors), int(fill_value), wp.from_torch( neighbor_matrix2, dtype=wp.int32, requires_grad=False, return_ctype=True, ), wp_device, batched=True, ) @_batch_cluster_tile_selective_fill_neighbor_matrix_tail_op.register_fake def _( num_neighbors: torch.Tensor, num_neighbors2: torch.Tensor, batch_idx: torch.Tensor, rebuild_flags: torch.Tensor, neighbor_matrix: torch.Tensor, neighbor_matrix2: torch.Tensor, max_neighbors: int, fill_value: int, use_cutoff2: bool, ) -> None: return None
[docs] @scoped_torch_warp_stream def batch_query_cluster_tile( sorted_atom_index: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, cell_batch: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, cutoff: float, natom: int, neighbor_matrix: torch.Tensor, num_neighbors: torch.Tensor, neighbor_matrix_shifts: torch.Tensor, inv_cell_batch: torch.Tensor | None = None, *, cutoff2: float | None = None, neighbor_matrix2: torch.Tensor | None = None, num_neighbors2: torch.Tensor | None = None, neighbor_matrix_shifts2: torch.Tensor | None = None, rebuild_flags: torch.Tensor | None = None, tile_offsets: torch.Tensor | None = None, tile_counts: torch.Tensor | None = None, batch_idx: torch.Tensor | None = None, return_vectors: bool = False, return_distances: bool = False, pair_fn: wp.Function | None = None, pair_params: torch.Tensor | None = None, neighbor_vectors: torch.Tensor | None = None, neighbor_distances: torch.Tensor | None = None, pair_energies: torch.Tensor | None = None, pair_forces: torch.Tensor | None = None, _fixed_cell_geometry: _FixedCellGeometry | None = None, ) -> None: """Convert the batched tile pair list to neighbor_matrix in place. Cluster-tile does not support partial neighbor lists; there is no ``target_indices`` kwarg. See :func:`nvalchemiops.neighbors.cluster_tile.batch_query_cluster_tile` for the full pair-output kwarg semantics. Pair outputs follow the same pattern as the single-system :func:`query_cluster_tile` binding: when any pair-output kwarg is set the call bypasses the torch custom op and forwards directly to the warp launcher (custom ops cannot carry callable ``pair_fn`` across their schema boundary). Parameters ---------- sorted_atom_index : torch.Tensor, shape (n_padded,), dtype=int32 Permutation from padded slot to original atom index (sentinel ``N`` marks padding slots). sorted_pos_x : torch.Tensor, shape (n_padded,), dtype=float32 Morton-sorted x-coordinates in the padded SoA layout. sorted_pos_y : torch.Tensor, shape (n_padded,), dtype=float32 Morton-sorted y-coordinates in the padded SoA layout. sorted_pos_z : torch.Tensor, shape (n_padded,), dtype=float32 Morton-sorted z-coordinates in the padded SoA layout. cell_batch : torch.Tensor, shape (num_systems, 3, 3), dtype=float32 Per-system unit cell matrices. num_tiles : torch.Tensor, shape (1,), dtype=int32 Scalar tile-pair count produced by ``batch_build_cluster_tile_list``. tile_row_group : torch.Tensor, shape (max_tiles,), dtype=int32 Row group index per tile pair. tile_col_group : torch.Tensor, shape (max_tiles,), dtype=int32 Column group index per tile pair. tile_system : torch.Tensor, shape (max_tiles,), dtype=int32 System index per tile pair. cutoff : float Cartesian cutoff radius for pair filtering. natom : int Total number of real atoms across all systems. neighbor_matrix : torch.Tensor, shape (natom, max_neighbors), dtype=int32 Output neighbor indices; modified in-place. num_neighbors : torch.Tensor, shape (natom,), dtype=int32 Output per-atom neighbor counts; modified in-place. neighbor_matrix_shifts : torch.Tensor, shape (natom, max_neighbors, 3), dtype=int32 Output periodic image shift vectors; modified in-place. inv_cell_batch : torch.Tensor, shape (num_systems, 3, 3), dtype=float32, optional Pre-computed inverse of ``cell_batch``. Computed internally when None. cutoff2 : float, optional Secondary cutoff for a second neighbor matrix. Matrix-format only. neighbor_matrix2 : torch.Tensor, shape (natom, max_neighbors), dtype=int32, optional Output neighbor indices for the secondary cutoff; modified in-place. num_neighbors2 : torch.Tensor, shape (natom,), dtype=int32, optional Output per-atom counts for the secondary cutoff; modified in-place. neighbor_matrix_shifts2 : torch.Tensor, shape (natom, max_neighbors, 3), dtype=int32, optional Output shift vectors for the secondary cutoff; modified in-place. rebuild_flags : torch.Tensor, shape (num_systems,), dtype=bool, optional Per-system selective rebuild flags. Requires ``batch_idx``, ``tile_offsets``, and ``tile_counts``. tile_offsets : torch.Tensor, shape (num_systems + 1,), dtype=int32, optional Fixed per-system tile segment offsets for the segmented path. tile_counts : torch.Tensor, shape (num_systems,), dtype=int32, optional Per-system tile counts for the segmented path. batch_idx : torch.Tensor, shape (natom,), dtype=int32, optional System index per atom; required when ``rebuild_flags`` is provided. return_vectors : bool, default False Write per-pair displacement vectors to ``neighbor_vectors``. return_distances : bool, default False Write per-pair scalar distances to ``neighbor_distances``. pair_fn : wp.Function, optional Module-scope Warp function evaluated for each pair. pair_params : torch.Tensor, optional Per-atom parameters forwarded to ``pair_fn``. neighbor_vectors : torch.Tensor, shape (natom, max_neighbors, 3), optional Output displacement vectors; modified in-place when ``return_vectors``. neighbor_distances : torch.Tensor, shape (natom, max_neighbors), optional Output scalar distances; modified in-place when ``return_distances``. pair_energies : torch.Tensor, shape (natom, max_neighbors), optional Output pair energies; modified in-place when ``pair_fn`` is set. pair_forces : torch.Tensor, shape (natom, max_neighbors, 3), optional Output pair forces; modified in-place when ``pair_fn`` is set. """ _validate_cluster_tile_matrix_outputs( device=sorted_pos_x.device, dtype=sorted_pos_x.dtype, natom=int(natom), max_neighbors=int(neighbor_matrix.shape[1]), cutoff2=cutoff2, neighbor_matrix2=neighbor_matrix2, num_neighbors2=num_neighbors2, neighbor_matrix_shifts2=neighbor_matrix_shifts2, return_vectors=return_vectors, return_distances=return_distances, neighbor_vectors=neighbor_vectors, neighbor_distances=neighbor_distances, allocate_missing=False, ) if inv_cell_batch is None: inv_cell_batch = torch.linalg.inv(cell_batch).contiguous() # Eager execution tightens the launch to the emitted tile count. Compiled # execution validates on device and launches the full static capacity. The # segmented path keeps the full-buffer launch and is checked per system. tile_capacity = int(tile_row_group.shape[0]) if tile_offsets is None: n_tiles = _check_tile_buffer_capacity(num_tiles, tile_capacity) else: n_tiles = 0 if tile_counts is not None: seg_caps = tile_offsets[1:] - tile_offsets[:-1] _check_tile_buffer_capacity( tile_counts, seg_caps, segmented=True, ) geometry_only = ( (return_vectors or return_distances) and cutoff2 is None and rebuild_flags is None and tile_offsets is None and tile_counts is None and pair_fn is None and pair_params is None and pair_energies is None and pair_forces is None ) needs_direct = ( cutoff2 is not None or rebuild_flags is not None or tile_offsets is not None or tile_counts is not None or _has_partial_or_pair_outputs( return_vectors=return_vectors, return_distances=return_distances, pair_fn=pair_fn, pair_params=pair_params, neighbor_vectors=neighbor_vectors, neighbor_distances=neighbor_distances, pair_energies=pair_energies, pair_forces=pair_forces, ) ) topology_only = not _has_partial_or_pair_outputs( return_vectors=return_vectors, return_distances=return_distances, pair_fn=pair_fn, pair_params=pair_params, neighbor_vectors=neighbor_vectors, neighbor_distances=neighbor_distances, pair_energies=pair_energies, pair_forces=pair_forces, ) geometry_without_callback = ( (return_vectors or return_distances) and pair_fn is None and pair_params is None and pair_energies is None and pair_forces is None ) if (topology_only or geometry_without_callback) and ( cutoff2 is not None or rebuild_flags is not None or tile_offsets is not None or tile_counts is not None ): if rebuild_flags is not None and batch_idx is None: raise ValueError("batch_idx is required when rebuild_flags is provided") device = sorted_pos_x.device dummy_matrix = torch.empty((1, 1), dtype=torch.int32, device=device) dummy_counts = torch.empty(1, dtype=torch.int32, device=device) dummy_shifts = torch.empty((1, 1, 3), dtype=torch.int32, device=device) dummy_vectors = torch.empty((1, 3), dtype=sorted_pos_x.dtype, device=device) dummy_distances = torch.empty(1, dtype=sorted_pos_x.dtype, device=device) dummy_i32 = torch.empty(1, dtype=torch.int32, device=device) dummy_bool = torch.empty(1, dtype=torch.bool, device=device) _batch_query_cluster_tile_topology_op( float(cutoff), float(cutoff2) if cutoff2 is not None else 0.0, int(natom), sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, cell_batch, inv_cell_batch, num_tiles, tile_row_group, tile_col_group, tile_system, neighbor_matrix, num_neighbors, neighbor_matrix_shifts, neighbor_matrix2 if neighbor_matrix2 is not None else dummy_matrix, num_neighbors2 if num_neighbors2 is not None else dummy_counts, neighbor_matrix_shifts2 if neighbor_matrix_shifts2 is not None else dummy_shifts, neighbor_vectors if neighbor_vectors is not None else dummy_vectors, neighbor_distances if neighbor_distances is not None else dummy_distances, rebuild_flags if rebuild_flags is not None else dummy_bool, tile_offsets if tile_offsets is not None else dummy_i32, tile_counts if tile_counts is not None else dummy_i32, batch_idx if batch_idx is not None else dummy_i32, int(n_tiles), bool(cutoff2 is not None), bool(rebuild_flags is not None), bool(tile_offsets is not None), bool(return_vectors), bool(return_distances), *_fixed_cluster_op_args(_fixed_cell_geometry), ) return if geometry_only: device = sorted_pos_x.device dummy_vectors = torch.empty((1, 3), dtype=sorted_pos_x.dtype, device=device) dummy_distances = torch.empty(1, dtype=sorted_pos_x.dtype, device=device) _batch_query_cluster_tile_op( cutoff, natom, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, cell_batch, inv_cell_batch, num_tiles, tile_row_group, tile_col_group, tile_system, neighbor_matrix, num_neighbors, neighbor_matrix_shifts, neighbor_vectors if neighbor_vectors is not None else dummy_vectors, neighbor_distances if neighbor_distances is not None else dummy_distances, n_tiles, bool(return_vectors), bool(return_distances), *_fixed_cluster_op_args(_fixed_cell_geometry), ) return if needs_direct: if rebuild_flags is not None: if batch_idx is None: raise ValueError("batch_idx is required when rebuild_flags is provided") wp_selective_zero_num_neighbors( wp.from_torch(num_neighbors, dtype=wp.int32, return_ctype=True), wp.from_torch(batch_idx, dtype=wp.int32, return_ctype=True), wp.from_torch(rebuild_flags, dtype=wp.bool, return_ctype=True), str(sorted_pos_x.device), ) if cutoff2 is not None and num_neighbors2 is not None: wp_selective_zero_num_neighbors( wp.from_torch(num_neighbors2, dtype=wp.int32, return_ctype=True), wp.from_torch(batch_idx, dtype=wp.int32, return_ctype=True), wp.from_torch(rebuild_flags, dtype=wp.bool, return_ctype=True), str(sorted_pos_x.device), ) _batch_query_cluster_tile_optional( cell_batch, inv_cell_batch, natom, cutoff, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, tile_system, neighbor_matrix, num_neighbors, neighbor_matrix_shifts, n_tiles=n_tiles, cutoff2=cutoff2, neighbor_matrix2=neighbor_matrix2, num_neighbors2=num_neighbors2, neighbor_matrix_shifts2=neighbor_matrix_shifts2, rebuild_flags=rebuild_flags, tile_offsets=tile_offsets, tile_counts=tile_counts, return_vectors=return_vectors, return_distances=return_distances, pair_fn=pair_fn, pair_params=pair_params, neighbor_vectors=neighbor_vectors, neighbor_distances=neighbor_distances, pair_energies=pair_energies, pair_forces=pair_forces, **_fixed_cluster_kwargs(_fixed_cell_geometry), ) return _batch_query_cluster_tile_op( cutoff, natom, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, cell_batch, inv_cell_batch, num_tiles, tile_row_group, tile_col_group, tile_system, neighbor_matrix, num_neighbors, neighbor_matrix_shifts, torch.empty((1, 3), dtype=sorted_pos_x.dtype, device=sorted_pos_x.device), torch.empty(1, dtype=sorted_pos_x.dtype, device=sorted_pos_x.device), n_tiles, False, False, *_fixed_cluster_op_args(_fixed_cell_geometry), )
@scoped_torch_warp_stream def _batch_query_cluster_tile_optional( cell_batch: torch.Tensor, inv_cell_batch: torch.Tensor, natom: int, cutoff: float, sorted_atom_index: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, neighbor_matrix: torch.Tensor, num_neighbors: torch.Tensor, neighbor_matrix_shifts: torch.Tensor, *, n_tiles: int, cutoff2: float | None, neighbor_matrix2: torch.Tensor | None, num_neighbors2: torch.Tensor | None, neighbor_matrix_shifts2: torch.Tensor | None, rebuild_flags: torch.Tensor | None, tile_offsets: torch.Tensor | None, tile_counts: torch.Tensor | None, return_vectors: bool, return_distances: bool, pair_fn: wp.Function | None, pair_params: torch.Tensor | None, neighbor_vectors: torch.Tensor | None, neighbor_distances: torch.Tensor | None, pair_energies: torch.Tensor | None, pair_forces: torch.Tensor | None, fixed_cell: bool = False, qr: torch.Tensor | None = None, axis_aligned: torch.Tensor | None = None, fractional_rounding_certified: torch.Tensor | None = None, qr_height_certified: torch.Tensor | None = None, bbox_cutoff_bounds: torch.Tensor | None = None, ) -> None: """Pair-output path for the batched binding. Bypasses the torch custom op and calls the warp launcher directly so a callable ``pair_fn`` can cross the binding boundary. ``n_tiles`` (compact tile count, or ``<= 0`` for the segmented full-buffer launch) sets the launch dimension; the kernel still guards per-tile defensively. """ device = sorted_pos_x.device wp_device = str(device) wp_dtype = get_wp_dtype(sorted_pos_x.dtype) wp_vec_dtype = wp.vec3f wp_pair_params = ( wp.from_torch(pair_params, dtype=wp_dtype) if pair_params is not None else None ) wp_neighbor_vectors = ( wp.from_torch(neighbor_vectors, dtype=wp_vec_dtype) if neighbor_vectors is not None else None ) wp_neighbor_distances = ( wp.from_torch(neighbor_distances, dtype=wp_dtype) if neighbor_distances is not None else None ) wp_pair_energies = ( wp.from_torch(pair_energies, dtype=wp_dtype) if pair_energies is not None else None ) wp_pair_forces = ( wp.from_torch(pair_forces, dtype=wp_vec_dtype) if pair_forces is not None else None ) wp_batch_query_cluster_tile( sorted_atom_index=wp.from_torch(sorted_atom_index, dtype=wp.int32), sorted_pos_x=wp.from_torch(sorted_pos_x, dtype=wp_dtype), sorted_pos_y=wp.from_torch(sorted_pos_y, dtype=wp_dtype), sorted_pos_z=wp.from_torch(sorted_pos_z, dtype=wp_dtype), cell_batch=wp.from_torch(cell_batch.contiguous(), dtype=wp.mat33f), inv_cell_batch=wp.from_torch(inv_cell_batch.contiguous(), dtype=wp.mat33f), num_tiles=wp.from_torch(num_tiles, dtype=wp.int32), tile_row_group=wp.from_torch(tile_row_group, dtype=wp.int32), tile_col_group=wp.from_torch(tile_col_group, dtype=wp.int32), tile_system=wp.from_torch(tile_system, dtype=wp.int32), cutoff=float(cutoff), natom=int(natom), neighbor_matrix=wp.from_torch(neighbor_matrix, dtype=wp.int32), num_neighbors=wp.from_torch(num_neighbors, dtype=wp.int32), neighbor_matrix_shifts=wp.from_torch(neighbor_matrix_shifts, dtype=wp.int32), wp_dtype=wp_dtype, device=wp_device, n_tiles=int(n_tiles) if n_tiles > 0 else None, cutoff2=cutoff2, neighbor_matrix2=( wp.from_torch(neighbor_matrix2, dtype=wp.int32) if neighbor_matrix2 is not None else None ), num_neighbors2=( wp.from_torch(num_neighbors2, dtype=wp.int32) if num_neighbors2 is not None else None ), neighbor_matrix_shifts2=( wp.from_torch(neighbor_matrix_shifts2, dtype=wp.int32) if neighbor_matrix_shifts2 is not None else None ), rebuild_flags=( wp.from_torch(rebuild_flags, dtype=wp.bool) if rebuild_flags is not None else None ), tile_offsets=( wp.from_torch(tile_offsets, dtype=wp.int32) if tile_offsets is not None else None ), tile_counts=( wp.from_torch(tile_counts, dtype=wp.int32) if tile_counts is not None else None ), return_vectors=bool(return_vectors), return_distances=bool(return_distances), pair_fn=pair_fn, pair_params=wp_pair_params, neighbor_vectors=wp_neighbor_vectors, neighbor_distances=wp_neighbor_distances, pair_energies=wp_pair_energies, pair_forces=wp_pair_forces, **_wp_fixed_cluster_geometry( fixed_cell, qr, axis_aligned, fractional_rounding_certified, qr_height_certified, bbox_cutoff_bounds, ), ) @torch.library.custom_op( "nvalchemiops::_batch_query_cluster_tile_coo", mutates_args=("pair_counter", "coo_list", "coo_shifts"), ) @scoped_torch_warp_stream def _batch_query_cluster_tile_coo_op( cutoff: float, natom: int, max_pairs: int, sorted_atom_index: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, cell_batch: torch.Tensor, inv_cell_batch: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, pair_counter: torch.Tensor, coo_list: torch.Tensor, coo_shifts: torch.Tensor, n_tiles: int, fixed_cell: bool, qr: torch.Tensor | None, axis_aligned: torch.Tensor | None, fractional_rounding_certified: torch.Tensor | None, qr_height_certified: torch.Tensor | None, bbox_cutoff_bounds: torch.Tensor | None, ) -> None: # ``n_tiles`` (host-synced compact tile count) tightens the launch; the # kernel still guards per-tile defensively. wp_device = str(sorted_pos_x.device) wp_dtype = get_wp_dtype(sorted_pos_x.dtype) wp_batch_query_cluster_tile_coo( n_tiles=int(n_tiles) if n_tiles > 0 else None, sorted_atom_index=wp.from_torch( sorted_atom_index, dtype=wp.int32, return_ctype=True ), sorted_pos_x=wp.from_torch(sorted_pos_x, dtype=wp_dtype, return_ctype=True), sorted_pos_y=wp.from_torch(sorted_pos_y, dtype=wp_dtype, return_ctype=True), sorted_pos_z=wp.from_torch(sorted_pos_z, dtype=wp_dtype, return_ctype=True), cell_batch=wp.from_torch( cell_batch.contiguous(), dtype=wp.mat33f, return_ctype=True, ), inv_cell_batch=wp.from_torch( inv_cell_batch.contiguous(), dtype=wp.mat33f, return_ctype=True, ), num_tiles=wp.from_torch(num_tiles, dtype=wp.int32, return_ctype=True), tile_row_group=wp.from_torch(tile_row_group, dtype=wp.int32, return_ctype=True), tile_col_group=wp.from_torch( tile_col_group, dtype=wp.int32, return_ctype=True, ), tile_system=wp.from_torch(tile_system, dtype=wp.int32, return_ctype=True), cutoff=float(cutoff), natom=int(natom), max_pairs=int(max_pairs), pair_counter=wp.from_torch(pair_counter, dtype=wp.int32, return_ctype=True), coo_list=wp.from_torch(coo_list, dtype=wp.int32, return_ctype=True), coo_shifts=wp.from_torch(coo_shifts, dtype=wp.int32, return_ctype=True), wp_dtype=wp_dtype, device=wp_device, **_wp_fixed_cluster_geometry( fixed_cell, qr, axis_aligned, fractional_rounding_certified, qr_height_certified, bbox_cutoff_bounds, ), ) @_batch_query_cluster_tile_coo_op.register_fake def _( cutoff: float, natom: int, max_pairs: int, sorted_atom_index: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, cell_batch: torch.Tensor, inv_cell_batch: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, pair_counter: torch.Tensor, coo_list: torch.Tensor, coo_shifts: torch.Tensor, n_tiles: int, fixed_cell: bool, qr: torch.Tensor | None, axis_aligned: torch.Tensor | None, fractional_rounding_certified: torch.Tensor | None, qr_height_certified: torch.Tensor | None, bbox_cutoff_bounds: torch.Tensor | None, ) -> None: return None @scoped_torch_warp_stream def _batch_query_cluster_tile_coo_optional( cell_batch: torch.Tensor, inv_cell_batch: torch.Tensor, natom: int, max_pairs: int, cutoff: float, sorted_atom_index: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, pair_counter: torch.Tensor, coo_list: torch.Tensor, coo_shifts: torch.Tensor, *, n_tiles: int, rebuild_flags: torch.Tensor | None, tile_offsets: torch.Tensor | None, tile_counts: torch.Tensor | None, pair_offsets: torch.Tensor | None, pair_counts: torch.Tensor | None, return_vectors: bool, return_distances: bool, pair_fn: wp.Function | None, pair_params: torch.Tensor | None, neighbor_vectors: torch.Tensor | None, neighbor_distances: torch.Tensor | None, pair_energies: torch.Tensor | None, pair_forces: torch.Tensor | None, fixed_cell: bool = False, qr: torch.Tensor | None = None, axis_aligned: torch.Tensor | None = None, fractional_rounding_certified: torch.Tensor | None = None, qr_height_certified: torch.Tensor | None = None, bbox_cutoff_bounds: torch.Tensor | None = None, ) -> None: """Pair-output COO path for the batched binding. ``n_tiles`` (compact tile count, or ``<= 0`` for the segmented full-buffer launch) sets the launch dimension; the kernel still guards per-tile. """ wp_device = str(sorted_pos_x.device) wp_dtype = get_wp_dtype(sorted_pos_x.dtype) wp_vec_dtype = wp.vec3f wp_pair_params = ( wp.from_torch(pair_params, dtype=wp_dtype) if pair_params is not None else None ) wp_neighbor_vectors = ( wp.from_torch(neighbor_vectors, dtype=wp_vec_dtype) if neighbor_vectors is not None else None ) wp_neighbor_distances = ( wp.from_torch(neighbor_distances, dtype=wp_dtype) if neighbor_distances is not None else None ) wp_pair_energies = ( wp.from_torch(pair_energies, dtype=wp_dtype) if pair_energies is not None else None ) wp_pair_forces = ( wp.from_torch(pair_forces, dtype=wp_vec_dtype) if pair_forces is not None else None ) wp_batch_query_cluster_tile_coo( sorted_atom_index=wp.from_torch(sorted_atom_index, dtype=wp.int32), sorted_pos_x=wp.from_torch(sorted_pos_x, dtype=wp_dtype), sorted_pos_y=wp.from_torch(sorted_pos_y, dtype=wp_dtype), sorted_pos_z=wp.from_torch(sorted_pos_z, dtype=wp_dtype), cell_batch=wp.from_torch(cell_batch.contiguous(), dtype=wp.mat33f), inv_cell_batch=wp.from_torch(inv_cell_batch.contiguous(), dtype=wp.mat33f), num_tiles=wp.from_torch(num_tiles, dtype=wp.int32), tile_row_group=wp.from_torch(tile_row_group, dtype=wp.int32), tile_col_group=wp.from_torch(tile_col_group, dtype=wp.int32), tile_system=wp.from_torch(tile_system, dtype=wp.int32), cutoff=float(cutoff), natom=int(natom), max_pairs=int(max_pairs), pair_counter=wp.from_torch(pair_counter, dtype=wp.int32), coo_list=wp.from_torch(coo_list, dtype=wp.int32), coo_shifts=wp.from_torch(coo_shifts, dtype=wp.int32), wp_dtype=wp_dtype, device=wp_device, n_tiles=int(n_tiles) if n_tiles > 0 else None, rebuild_flags=( wp.from_torch(rebuild_flags, dtype=wp.bool) if rebuild_flags is not None else None ), tile_offsets=( wp.from_torch(tile_offsets, dtype=wp.int32) if tile_offsets is not None else None ), tile_counts=( wp.from_torch(tile_counts, dtype=wp.int32) if tile_counts is not None else None ), pair_offsets=( wp.from_torch(pair_offsets, dtype=wp.int32) if pair_offsets is not None else None ), pair_counts=( wp.from_torch(pair_counts, dtype=wp.int32) if pair_counts is not None else None ), return_vectors=bool(return_vectors), return_distances=bool(return_distances), pair_fn=pair_fn, pair_params=wp_pair_params, neighbor_vectors=wp_neighbor_vectors, neighbor_distances=wp_neighbor_distances, pair_energies=wp_pair_energies, pair_forces=wp_pair_forces, **_wp_fixed_cluster_geometry( fixed_cell, qr, axis_aligned, fractional_rounding_certified, qr_height_certified, bbox_cutoff_bounds, ), ) @torch.library.custom_op( "nvalchemiops::_batch_query_cluster_tile_coo_segmented", mutates_args=("pair_counter", "pair_counts", "coo_list", "coo_shifts"), ) @scoped_torch_warp_stream def _batch_query_cluster_tile_coo_segmented_op( cell_batch: torch.Tensor, inv_cell_batch: torch.Tensor, natom: int, max_pairs: int, cutoff: float, sorted_atom_index: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, pair_counter: torch.Tensor, coo_list: torch.Tensor, coo_shifts: torch.Tensor, tile_offsets: torch.Tensor, tile_counts: torch.Tensor, pair_offsets: torch.Tensor, pair_counts: torch.Tensor, fixed_cell: bool, qr: torch.Tensor | None, axis_aligned: torch.Tensor | None, fractional_rounding_certified: torch.Tensor | None, qr_height_certified: torch.Tensor | None, bbox_cutoff_bounds: torch.Tensor | None, ) -> None: _batch_query_cluster_tile_coo_optional( cell_batch, inv_cell_batch, natom, max_pairs, cutoff, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, tile_system, pair_counter, coo_list, coo_shifts, n_tiles=0, rebuild_flags=None, tile_offsets=tile_offsets, tile_counts=tile_counts, pair_offsets=pair_offsets, pair_counts=pair_counts, return_vectors=False, return_distances=False, pair_fn=None, pair_params=None, neighbor_vectors=None, neighbor_distances=None, pair_energies=None, pair_forces=None, fixed_cell=fixed_cell, qr=qr, axis_aligned=axis_aligned, fractional_rounding_certified=fractional_rounding_certified, qr_height_certified=qr_height_certified, bbox_cutoff_bounds=bbox_cutoff_bounds, ) @_batch_query_cluster_tile_coo_segmented_op.register_fake def _( cell_batch: torch.Tensor, inv_cell_batch: torch.Tensor, natom: int, max_pairs: int, cutoff: float, sorted_atom_index: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, pair_counter: torch.Tensor, coo_list: torch.Tensor, coo_shifts: torch.Tensor, tile_offsets: torch.Tensor, tile_counts: torch.Tensor, pair_offsets: torch.Tensor, pair_counts: torch.Tensor, fixed_cell: bool, qr: torch.Tensor | None, axis_aligned: torch.Tensor | None, fractional_rounding_certified: torch.Tensor | None, qr_height_certified: torch.Tensor | None, bbox_cutoff_bounds: torch.Tensor | None, ) -> None: return None @torch.library.custom_op( "nvalchemiops::_batch_query_cluster_tile_coo_segmented_no_pair_fn", mutates_args=( "pair_counter", "pair_counts", "coo_list", "coo_shifts", "neighbor_vectors", "neighbor_distances", ), ) @scoped_torch_warp_stream def _batch_query_cluster_tile_coo_segmented_no_pair_fn_op( cell_batch: torch.Tensor, inv_cell_batch: torch.Tensor, natom: int, max_pairs: int, cutoff: float, sorted_atom_index: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, pair_counter: torch.Tensor, coo_list: torch.Tensor, coo_shifts: torch.Tensor, tile_offsets: torch.Tensor, tile_counts: torch.Tensor, pair_offsets: torch.Tensor, pair_counts: torch.Tensor, neighbor_vectors: torch.Tensor | None, neighbor_distances: torch.Tensor | None, return_vectors: bool, return_distances: bool, fixed_cell: bool, qr: torch.Tensor | None, axis_aligned: torch.Tensor | None, fractional_rounding_certified: torch.Tensor | None, qr_height_certified: torch.Tensor | None, bbox_cutoff_bounds: torch.Tensor | None, ) -> None: _batch_query_cluster_tile_coo_optional( cell_batch, inv_cell_batch, natom, max_pairs, cutoff, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, tile_system, pair_counter, coo_list, coo_shifts, n_tiles=0, rebuild_flags=None, tile_offsets=tile_offsets, tile_counts=tile_counts, pair_offsets=pair_offsets, pair_counts=pair_counts, return_vectors=return_vectors, return_distances=return_distances, pair_fn=None, pair_params=None, neighbor_vectors=neighbor_vectors, neighbor_distances=neighbor_distances, pair_energies=None, pair_forces=None, fixed_cell=fixed_cell, qr=qr, axis_aligned=axis_aligned, fractional_rounding_certified=fractional_rounding_certified, qr_height_certified=qr_height_certified, bbox_cutoff_bounds=bbox_cutoff_bounds, ) @_batch_query_cluster_tile_coo_segmented_no_pair_fn_op.register_fake def _( cell_batch: torch.Tensor, inv_cell_batch: torch.Tensor, natom: int, max_pairs: int, cutoff: float, sorted_atom_index: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, pair_counter: torch.Tensor, coo_list: torch.Tensor, coo_shifts: torch.Tensor, tile_offsets: torch.Tensor, tile_counts: torch.Tensor, pair_offsets: torch.Tensor, pair_counts: torch.Tensor, neighbor_vectors: torch.Tensor | None, neighbor_distances: torch.Tensor | None, return_vectors: bool, return_distances: bool, fixed_cell: bool, qr: torch.Tensor | None, axis_aligned: torch.Tensor | None, fractional_rounding_certified: torch.Tensor | None, qr_height_certified: torch.Tensor | None, bbox_cutoff_bounds: torch.Tensor | None, ) -> None: return None
[docs] def batch_query_cluster_tile_coo( sorted_atom_index: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, cell_batch: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, cutoff: float, natom: int, max_pairs: int, pair_counter: torch.Tensor, coo_list: torch.Tensor, coo_shifts: torch.Tensor, inv_cell_batch: torch.Tensor | None = None, *, rebuild_flags: torch.Tensor | None = None, tile_offsets: torch.Tensor | None = None, tile_counts: torch.Tensor | None = None, pair_offsets: torch.Tensor | None = None, pair_counts: torch.Tensor | None = None, return_vectors: bool = False, return_distances: bool = False, pair_fn: wp.Function | None = None, pair_params: torch.Tensor | None = None, neighbor_vectors: torch.Tensor | None = None, neighbor_distances: torch.Tensor | None = None, pair_energies: torch.Tensor | None = None, pair_forces: torch.Tensor | None = None, _fixed_cell_geometry: _FixedCellGeometry | None = None, ) -> None: """Convert the batched tile pair list to flat COO pair list in place. Cluster-tile does not support partial neighbor lists; there is no ``target_indices`` kwarg. Optional pair outputs use flat COO buffers with length ``max_pairs``; they are written in the same order as ``coo_list``. Parameters ---------- sorted_atom_index : torch.Tensor, shape (n_padded,), dtype=int32 Permutation from padded slot to original atom index. sorted_pos_x : torch.Tensor, shape (n_padded,), dtype=float32 Morton-sorted x-coordinates in the padded SoA layout. sorted_pos_y : torch.Tensor, shape (n_padded,), dtype=float32 Morton-sorted y-coordinates in the padded SoA layout. sorted_pos_z : torch.Tensor, shape (n_padded,), dtype=float32 Morton-sorted z-coordinates in the padded SoA layout. cell_batch : torch.Tensor, shape (num_systems, 3, 3), dtype=float32 Per-system unit cell matrices. num_tiles : torch.Tensor, shape (1,), dtype=int32 Scalar tile-pair count from ``batch_build_cluster_tile_list``. tile_row_group : torch.Tensor, shape (max_tiles,), dtype=int32 Row group index per tile pair. tile_col_group : torch.Tensor, shape (max_tiles,), dtype=int32 Column group index per tile pair. tile_system : torch.Tensor, shape (max_tiles,), dtype=int32 System index per tile pair. cutoff : float Cartesian cutoff radius for pair filtering. natom : int Total number of real atoms across all systems. max_pairs : int Upper bound on the total number of pairs written to ``coo_list``. pair_counter : torch.Tensor, shape (1,), dtype=int32 Output scalar pair count; zeroed then modified in-place. coo_list : torch.Tensor, shape (max_pairs, 2), dtype=int32 Output COO pair list ``[i, j]``; modified in-place. coo_shifts : torch.Tensor, shape (max_pairs, 3), dtype=int32 Output periodic image shift vectors per pair; modified in-place. inv_cell_batch : torch.Tensor, shape (num_systems, 3, 3), dtype=float32, optional Pre-computed inverse of ``cell_batch``. Computed internally when None. rebuild_flags : torch.Tensor, shape (num_systems,), dtype=bool, optional Per-system selective rebuild flags. Requires ``tile_offsets``, ``tile_counts``, ``pair_offsets``, and ``pair_counts``. tile_offsets : torch.Tensor, shape (num_systems + 1,), dtype=int32, optional Fixed per-system tile segment offsets for the segmented path. tile_counts : torch.Tensor, shape (num_systems,), dtype=int32, optional Per-system tile counts for the segmented path. pair_offsets : torch.Tensor, shape (num_systems + 1,), dtype=int32, optional Fixed per-system COO segment offsets for the segmented path. pair_counts : torch.Tensor, shape (num_systems,), dtype=int32, optional Output per-system pair counts for the segmented path; modified in-place. return_vectors : bool, default False Write per-pair displacement vectors to ``neighbor_vectors``. return_distances : bool, default False Write per-pair scalar distances to ``neighbor_distances``. pair_fn : wp.Function, optional Module-scope Warp function evaluated for each pair. pair_params : torch.Tensor, optional Per-atom parameters forwarded to ``pair_fn``. neighbor_vectors : torch.Tensor, shape (max_pairs, 3), optional Output displacement vectors; modified in-place when ``return_vectors``. neighbor_distances : torch.Tensor, shape (max_pairs,), optional Output scalar distances; modified in-place when ``return_distances``. pair_energies : torch.Tensor, shape (max_pairs,), optional Output pair energies; modified in-place when ``pair_fn`` is set. pair_forces : torch.Tensor, shape (max_pairs, 3), optional Output pair forces; modified in-place when ``pair_fn`` is set. """ if pair_offsets is not None: if pair_counts is None: raise ValueError("pair_counts is required with pair_offsets") _validate_segmented_coo_state( device=coo_list.device, num_systems=int(pair_offsets.shape[0]) - 1, neighbor_list=coo_list.transpose(0, 1), neighbor_list_shifts=coo_shifts, pair_offsets=pair_offsets, pair_counts=pair_counts, rebuild_flags=rebuild_flags, tile_offsets=tile_offsets, tile_counts=tile_counts, num_tiles=num_tiles, tile_row_group=tile_row_group, tile_col_group=tile_col_group, tile_system=tile_system, ) if inv_cell_batch is None: inv_cell_batch = torch.linalg.inv(cell_batch).contiguous() pair_counter.zero_() # Eager execution tightens the launch to the emitted tile count. Compiled # execution validates on device and launches the full static capacity. The # segmented path keeps the full-buffer launch and is checked per system. tile_capacity = int(tile_row_group.shape[0]) if tile_offsets is None: n_tiles = _check_tile_buffer_capacity(num_tiles, tile_capacity) else: n_tiles = 0 if tile_counts is not None: seg_caps = tile_offsets[1:] - tile_offsets[:-1] _check_tile_buffer_capacity( tile_counts, seg_caps, segmented=True, ) needs_direct = ( rebuild_flags is not None or tile_offsets is not None or tile_counts is not None or pair_offsets is not None or pair_counts is not None or _has_partial_or_pair_outputs( return_vectors=return_vectors, return_distances=return_distances, pair_fn=pair_fn, pair_params=pair_params, neighbor_vectors=neighbor_vectors, neighbor_distances=neighbor_distances, pair_energies=pair_energies, pair_forces=pair_forces, ) ) if needs_direct: if ( rebuild_flags is None and tile_offsets is not None and tile_counts is not None and pair_offsets is not None and pair_counts is not None and pair_fn is None and pair_params is None and pair_energies is None and pair_forces is None ): if return_vectors or return_distances: _batch_query_cluster_tile_coo_segmented_no_pair_fn_op( cell_batch, inv_cell_batch, natom, max_pairs, cutoff, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, tile_system, pair_counter, coo_list, coo_shifts, tile_offsets, tile_counts, pair_offsets, pair_counts, neighbor_vectors, neighbor_distances, return_vectors, return_distances, *_fixed_cluster_op_args(_fixed_cell_geometry), ) else: _batch_query_cluster_tile_coo_segmented_op( cell_batch, inv_cell_batch, natom, max_pairs, cutoff, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, tile_system, pair_counter, coo_list, coo_shifts, tile_offsets, tile_counts, pair_offsets, pair_counts, *_fixed_cluster_op_args(_fixed_cell_geometry), ) return _batch_query_cluster_tile_coo_optional( cell_batch, inv_cell_batch, natom, max_pairs, cutoff, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, tile_system, pair_counter, coo_list, coo_shifts, n_tiles=n_tiles, rebuild_flags=rebuild_flags, tile_offsets=tile_offsets, tile_counts=tile_counts, pair_offsets=pair_offsets, pair_counts=pair_counts, return_vectors=return_vectors, return_distances=return_distances, pair_fn=pair_fn, pair_params=pair_params, neighbor_vectors=neighbor_vectors, neighbor_distances=neighbor_distances, pair_energies=pair_energies, pair_forces=pair_forces, **_fixed_cluster_kwargs(_fixed_cell_geometry), ) return _batch_query_cluster_tile_coo_op( cutoff, natom, max_pairs, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, cell_batch, inv_cell_batch, num_tiles, tile_row_group, tile_col_group, tile_system, pair_counter, coo_list, coo_shifts, n_tiles, *_fixed_cluster_op_args(_fixed_cell_geometry), )
def _validate_batch_matrix_state( *, device: torch.device, natom: int, num_systems: int, max_neighbors: int, neighbor_matrix: torch.Tensor, num_neighbors: torch.Tensor, neighbor_matrix_shifts: torch.Tensor, num_tiles: torch.Tensor, tile_offsets: torch.Tensor, tile_counts: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, neighbor_matrix2: torch.Tensor | None = None, num_neighbors2: torch.Tensor | None = None, neighbor_matrix_shifts2: torch.Tensor | None = None, ) -> None: """Validate caller-owned selective matrix and tile state.""" state = { "neighbor_matrix": neighbor_matrix, "num_neighbors": num_neighbors, "neighbor_matrix_shifts": neighbor_matrix_shifts, "num_tiles": num_tiles, "tile_offsets": tile_offsets, "tile_counts": tile_counts, "tile_row_group": tile_row_group, "tile_col_group": tile_col_group, "tile_system": tile_system, } for name, value in state.items(): if value.device != device or value.dtype != torch.int32: raise ValueError(f"{name} must be an int32 tensor on positions.device") if neighbor_matrix.shape != (natom, max_neighbors): raise ValueError("neighbor_matrix must have shape (N, max_neighbors)") if num_neighbors.shape != (natom,): raise ValueError("num_neighbors must have shape (N,)") if neighbor_matrix_shifts.shape != (natom, max_neighbors, 3): raise ValueError("neighbor_matrix_shifts must have shape (N, max_neighbors, 3)") if ( tile_offsets.shape != (num_systems + 1,) or tile_counts.shape != (num_systems,) or num_tiles.shape != (1,) or tile_row_group.ndim != 1 or tile_col_group.shape != tile_row_group.shape or tile_system.shape != tile_row_group.shape ): raise ValueError("batched tile state has an invalid shape") for name, matrix, counts, shifts in ( ("secondary", neighbor_matrix2, num_neighbors2, neighbor_matrix_shifts2), ): if matrix is not None: if ( counts is None or shifts is None or matrix.device != device or matrix.dtype != torch.int32 or counts.device != device or counts.dtype != torch.int32 or shifts.device != device or shifts.dtype != torch.int32 or matrix.shape != (natom, max_neighbors) or counts.shape != (natom,) or shifts.shape != (natom, max_neighbors, 3) ): raise ValueError(f"{name} matrix state has an invalid shape or dtype") def _batch_cluster_tile_neighbor_list_impl( partition_metadata: _BatchPartitionMetadata | None, positions: torch.Tensor, cutoff: float | None = None, cell_batch: torch.Tensor | None = None, batch_ptr: torch.Tensor | None = None, max_neighbors: int | None = None, fill_value: int | None = None, format: str = "matrix", max_pairs: int | None = None, cutoff2: float | None = None, rebuild_flags: torch.Tensor | None = None, return_state: bool = False, # matrix-format outputs neighbor_matrix: torch.Tensor | None = None, neighbor_matrix_shifts: torch.Tensor | None = None, num_neighbors: torch.Tensor | None = None, neighbor_matrix2: torch.Tensor | None = None, neighbor_matrix_shifts2: torch.Tensor | None = None, num_neighbors2: torch.Tensor | None = None, # coo-format outputs neighbor_list: torch.Tensor | None = None, neighbor_list_shifts: torch.Tensor | None = None, pair_counter: torch.Tensor | None = None, pair_offsets: torch.Tensor | None = None, pair_counts: torch.Tensor | None = None, inv_cell_batch: torch.Tensor | None = None, # scratch buffers sorted_atom_index: torch.Tensor | None = None, sort_inv: torch.Tensor | None = None, sorted_pos_x: torch.Tensor | None = None, sorted_pos_y: torch.Tensor | None = None, sorted_pos_z: torch.Tensor | None = None, batch_idx_sorted: torch.Tensor | None = None, batch_ptr_padded: torch.Tensor | None = None, group_system: torch.Tensor | None = None, group_ptr: torch.Tensor | None = None, group_ctr_x: torch.Tensor | None = None, group_ctr_y: torch.Tensor | None = None, group_ctr_z: torch.Tensor | None = None, group_ext_x: torch.Tensor | None = None, group_ext_y: torch.Tensor | None = None, group_ext_z: torch.Tensor | None = None, num_tiles: torch.Tensor | None = None, tile_row_group: torch.Tensor | None = None, tile_col_group: torch.Tensor | None = None, tile_system: torch.Tensor | None = None, tile_offsets: torch.Tensor | None = None, tile_counts: torch.Tensor | None = None, # Optional matrix outputs / pair-fn surface return_vectors: bool = False, return_distances: bool = False, pair_fn: wp.Function | None = None, pair_params: torch.Tensor | None = None, neighbor_vectors: torch.Tensor | None = None, neighbor_distances: torch.Tensor | None = None, pair_energies: torch.Tensor | None = None, pair_forces: torch.Tensor | None = None, max_tiles_per_group: int | None = None, *, eager_rebuild_count: int | None = None, force_compute_inv_cell: bool = False, _fixed_cell_geometry: _FixedCellGeometry | None = None, _live_cell: torch.Tensor | None = None, ) -> tuple[torch.Tensor, ...]: """Build and query a batched cluster-pair tile neighbor list in one call. Batched PyTorch binding for the cluster-pair tile algorithm. Supports triclinic ``cell_batch`` of shape ``(num_systems, 3, 3)`` and arbitrary per-system atom counts (padded internally to a multiple of ``TILE_GROUP_SIZE``). Cluster-tile is CUDA float32 only. Parameters ---------- positions : torch.Tensor, shape (total_atoms, 3), dtype=float32 Concatenated atomic coordinates across systems. cutoff : float, optional Cutoff distance in Cartesian units. Must be positive. cell_batch : torch.Tensor, shape (num_systems, 3, 3), dtype=float32 Per-system unit cell matrices. Cluster-tile assumes fully periodic boundaries. Required on every call. batch_ptr : torch.Tensor, shape (num_systems + 1,), dtype=int32 or int64, optional CSR pointer separating systems. Assumes positions are laid out in system-contiguous order (system 0 atoms first, then system 1, and so on); interleaved layouts are **not supported** and will silently emit cross-system pairs. When invoked via :func:`nvalchemiops.torch.neighbors.neighbor_list` with a ``batch_idx`` argument, the dispatcher derives ``batch_ptr`` by assuming ``batch_idx`` is sorted by system — the same contract. Required for every call. max_neighbors : int, optional Falls back to ``estimate_max_neighbors`` using the larger active cutoff. Matrix format only. fill_value : int, optional Matrix sentinel; defaults to ``total_atoms``. format : {"matrix", "coo", "tile"}, default "matrix" Output representation. See Returns. max_pairs : int, optional Upper bound for COO output; defaults to ``total_atoms * max_neighbors``. cutoff2 : float, optional Cutoff for the second matrix. It is normally the outer cutoff and may equal ``cutoff``. Either order is accepted because the tile buffer is sized for the larger value. Dual cutoff is matrix-only and cannot be combined with pair-output buffers. rebuild_flags : torch.Tensor, shape (num_systems,), dtype=torch.bool, optional Per-system selective rebuild flags. Supported for matrix output and segmented COO output. Across selective calls, callers must retain and reuse ``tile_offsets``, ``tile_counts``, and the complete scratch/tile block triggered by ``sorted_atom_index``. Matrix output additionally requires persistent neighbor matrix/count/shift buffers for every active cutoff when an unflagged system must be preserved. return_state : bool, default=False When used with ``rebuild_flags``, append the reusable tile state ``(tile_offsets, tile_counts, num_tiles, tile_row_group, tile_col_group, tile_system)`` to matrix or segmented-COO output. These are the same tensor objects as caller-supplied state buffers. Requires ``rebuild_flags``. tile_offsets, tile_counts : torch.Tensor, optional Fixed per-system tile offsets and OUTPUT tile counters for segmented tile-list state. Use ``estimate_batch_cluster_tile_segments`` to size these arrays. Caller-owned persistent buffers are required across selective rebuild calls. pair_offsets, pair_counts : torch.Tensor, optional Fixed per-system COO offsets and OUTPUT pair counters for segmented COO output. Compact COO cannot be combined with ``rebuild_flags``. Before launch, offsets, counts, topology buffers, and tile state are validated against their physical capacities. Eager calls containing a false flag require caller-owned topology and tile state; batched ``torch.compile(fullgraph=True)`` selective COO is not supported. neighbor_matrix, num_neighbors, neighbor_matrix_shifts : torch.Tensor, optional Pre-allocated matrix-format outputs for the primary cutoff, shapes ``(total_atoms, max_neighbors)``, ``(total_atoms,)``, and ``(total_atoms, max_neighbors, 3)`` respectively, dtype int32. All-or-nothing: provide all three or none; when omitted, the trio is allocated. The complete trio must be caller-owned persistent state when selective rebuild must preserve prior neighbor topology for an unflagged system, and for compiled selective matrix calls. neighbor_matrix2 : torch.Tensor, shape (total_atoms, max_neighbors), dtype=torch.int32, optional Pre-allocated neighbor indices for the secondary cutoff (``cutoff2``). All-or-nothing with ``num_neighbors2`` and ``neighbor_matrix_shifts2``: provide the complete trio or none. With selective rebuild, retain and reuse the complete trio across calls that preserve an unflagged system. neighbor_matrix_shifts2 : torch.Tensor, shape (total_atoms, max_neighbors, 3), dtype=torch.int32, optional Pre-allocated periodic shift vectors for the secondary cutoff. num_neighbors2 : torch.Tensor, shape (total_atoms,), dtype=torch.int32, optional Pre-allocated per-atom neighbor counts for the secondary cutoff. neighbor_list, neighbor_list_shifts, pair_counter : torch.Tensor, optional Pre-allocated COO-format outputs. Shapes ``(2, max_pairs)``, ``(max_pairs, 3)``, and ``(1,)``, dtype int32 and on the same device as ``positions``. Compact COO buffers may be supplied independently; omitted buffers are allocated. Selective calls that preserve an unflagged system require complete caller-owned segmented topology state. inv_cell_batch : torch.Tensor, optional Pre-computed inverse cell matrices. sorted_atom_index, sort_inv, sorted_pos_x, sorted_pos_y, sorted_pos_z, batch_idx_sorted, batch_ptr_padded, group_system, group_ptr : torch.Tensor, optional Pre-allocated scratch buffers (shapes as returned by ``allocate_batch_cluster_tile_list``). All-or-nothing: provide every scratch tensor or none. The trigger is ``sorted_atom_index``. Retain the complete scratch block across selective rebuild calls. group_ctr_x : torch.Tensor, shape (ngroup_padded,), dtype=float32, optional Pre-allocated group bounding-box centre x-coordinate scratch; written by build. Part of the all-or-nothing scratch block triggered by ``sorted_atom_index``. group_ctr_y : torch.Tensor, shape (ngroup_padded,), dtype=float32, optional Pre-allocated group bounding-box centre y-coordinate scratch; written by build. group_ctr_z : torch.Tensor, shape (ngroup_padded,), dtype=float32, optional Pre-allocated group bounding-box centre z-coordinate scratch; written by build. group_ext_x : torch.Tensor, shape (ngroup_padded,), dtype=float32, optional Pre-allocated group bounding-box half-extent x scratch; written by build. group_ext_y : torch.Tensor, shape (ngroup_padded,), dtype=float32, optional Pre-allocated group bounding-box half-extent y scratch; written by build. group_ext_z : torch.Tensor, shape (ngroup_padded,), dtype=float32, optional Pre-allocated group bounding-box half-extent z scratch; written by build. num_tiles, tile_row_group, tile_col_group, tile_system : torch.Tensor, optional Pre-allocated tile-list state (shapes as returned by ``allocate_batch_cluster_tile_list``). Part of the all-or-nothing scratch block triggered by ``sorted_atom_index`` and persistent across selective rebuild calls. return_vectors, return_distances : bool, default False Return per-pair displacements / distances on nonselective calls and write their active prefixes to supplied ``neighbor_vectors`` / ``neighbor_distances`` buffers. Matrix format uses ``(total_atoms, max_neighbors, ...)`` buffers; compact COO uses flat capacity buffers and returns newly allocated exact geometry. pair_fn : wp.Function, optional Module-scope Warp ``@wp.func`` of signature ``(r_ij, distance, pair_params, i, j) -> (energy, force)``. pair_params : torch.Tensor, optional Per-atom pair-function parameters; required with ``pair_fn``. neighbor_vectors, neighbor_distances, pair_energies, pair_forces : torch.Tensor, optional OUTPUT buffers, written only when the corresponding enable flag / ``pair_fn`` is active. Geometry buffers must not require gradients. When geometry is reconstructed for autograd, supplied buffers receive detached value snapshots while the returned geometry uses separate differentiable tensors. Omitted geometry buffers are not allocated. max_tiles_per_group : int, optional Capacity factor for an internally allocated intermediate tile-pair buffer. A system with ``g_i`` row groups contributes ``g_i * min(g_i, max_tiles_per_group)`` entries to one pooled buffer. Eager calls estimate the value when it is ``None``. Caller-owned tile arrays determine the actual physical capacity. See :ref:`cluster-tile-buffer-capacity` for sizing details. Returns ------- tuple of torch.Tensor Shape depends on ``format``: - ``"matrix"`` (default): ``(neighbor_matrix, num_neighbors, neighbor_matrix_shifts)``. When ``cutoff2`` is set, the secondary ``(neighbor_matrix2, num_neighbors2, neighbor_matrix_shifts2)`` triple follows the primary triple. On the non-selective path without ``pair_fn``, optional distances and/or vectors are appended when ``return_distances`` / ``return_vectors`` is True. Selective queries and ``pair_fn`` queries instead write requested pair-output buffers in place and return only the topology triple (or dual-cutoff sextuple). Selective calls with ``return_state=True`` append ``(tile_offsets, tile_counts, num_tiles, tile_row_group, tile_col_group, tile_system)``, yielding nine tensors for one cutoff or twelve for two cutoffs. Differentiable reconstructed geometry does not alias supplied output buffers; otherwise returned geometry is the supplied or internally allocated buffer. - ``"coo"``: ``(neighbor_list, neighbor_ptr, neighbor_list_shifts)`` built directly in source-owned CSR rows without a matrix intermediate. Requested distances and/or vectors are appended in that order, have the exact pair length, and do not alias reusable capacity buffers. Pair callbacks keep the topology-only return and write their buffers in place. Pair order within a row is unspecified. With segmented COO, returns ``(neighbor_list, pair_offsets, pair_counts, neighbor_list_shifts)`` without trimming the caller-owned fixed segments; selective calls with ``return_state=True`` append the six state tensors above, yielding ten tensors. - ``"tile"``: 11-tuple ``(num_tiles, tile_row_group, tile_col_group, tile_system, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, batch_idx_sorted, batch_ptr_padded, group_ptr)`` --- per-tile and per-system mapping arrays a batch tile consumer needs. Notes ----- - Cluster-tile is CUDA float32 only; float64 ``positions`` is rejected. - Cluster-tile does not support partial neighbor lists (no ``target_indices`` kwarg). - ``torch.compile(fullgraph=True)`` supports tile and matrix output, including dual-cutoff matrices and differentiable geometry. On PyTorch 2.10 or newer, nonselective exact COO output also supports fullgraph and may return a different pair count on each call. Compiled calls require the complete scratch tuple returned by ``allocate_batch_cluster_tile_list``. Selective matrix calls also require fixed tile offsets, counters, and output buffers. Compiled capacity failures use asynchronous device assertions. Pair callbacks remain eager-only. - Build differentiable losses from returned geometry, not from supplied output buffers, which are non-differentiable value snapshots when reconstruction is required. - The unified :func:`nvalchemiops.torch.neighbors.neighbor_list` entry point may select this binding automatically when the selector guards and cost model prefer it; pass ``method="batch_cluster_tile"`` to force it. See Also -------- nvalchemiops.torch.neighbors.cluster_tile_neighbor_list : Single-system companion entry point. nvalchemiops.torch.neighbors.batch_cluster_tile.batch_build_cluster_tile_list : Lower-level build step exposed for caching across queries. nvalchemiops.torch.neighbors.batch_cluster_tile.batch_query_cluster_tile : Lower-level query step. """ if cutoff is None or cell_batch is None or batch_ptr is None: missing = [ name for name, value in ( ("cutoff", cutoff), ("cell_batch", cell_batch), ("batch_ptr", batch_ptr), ) if value is None ] raise ValueError("missing required arguments: " + ", ".join(missing)) if positions.dtype != torch.float32: raise TypeError("positions must be float32") if format not in ("matrix", "coo", "tile"): raise ValueError( f"format must be 'matrix' | 'coo' | 'tile'; got {format!r}", ) if max_tiles_per_group is not None and ( not isinstance(max_tiles_per_group, int) or isinstance(max_tiles_per_group, bool) or max_tiles_per_group <= 0 ): raise ValueError("max_tiles_per_group must be a positive integer") has_pair_outputs = ( bool(return_vectors) or bool(return_distances) or pair_fn is not None or pair_params is not None or neighbor_vectors is not None or neighbor_distances is not None or pair_energies is not None or pair_forces is not None ) is_compiling = torch.compiler.is_compiling() if rebuild_flags is not None and not is_compiling: if eager_rebuild_count is None: eager_rebuild_count = int(torch.count_nonzero(rebuild_flags).item()) eager_any_rebuild = eager_rebuild_count > 0 eager_all_true = eager_rebuild_count == rebuild_flags.numel() else: eager_any_rebuild = False eager_all_true = False if return_state and rebuild_flags is None: raise ValueError("return_state=True requires rebuild_flags") if has_pair_outputs and format == "tile": raise NotImplementedError( "Pair outputs (return_vectors / return_distances / pair_fn) " "are not supported with format='tile'. Use format='matrix' " "or format='coo'.", ) if cutoff2 is not None: if format != "matrix": raise ValueError( "cluster_tile cutoff2 is supported only with format='matrix'" ) if has_pair_outputs: raise ValueError( "cluster_tile cutoff2 cannot be combined with pair outputs" ) if rebuild_flags is not None and format == "tile": raise ValueError( "cluster_tile selective rebuild is not supported with format='tile'" ) if ( rebuild_flags is not None and format == "coo" and (pair_offsets is None or pair_counts is None) and (is_compiling or not eager_all_true or not return_state) ): raise ValueError( "cluster_tile selective COO requires pair_offsets and pair_counts" ) if (pair_offsets is None) != (pair_counts is None): raise ValueError("Pass both 'pair_offsets' and 'pair_counts', or neither.") if (tile_offsets is None) != (tile_counts is None): raise ValueError("Pass both 'tile_offsets' and 'tile_counts', or neither.") has_cached_partition_metadata = partition_metadata is not None if not has_cached_partition_metadata: if batch_ptr.dtype != torch.int32 or batch_ptr.ndim != 1: raise ValueError("batch_ptr must be 1D int32") if batch_ptr.shape[0] < 2: raise ValueError("batch_ptr must have length at least 2") device = positions.device N = positions.shape[0] num_systems = int(batch_ptr.shape[0]) - 1 if ( is_compiling and format == "coo" and rebuild_flags is None and pair_fn is not None ): raise NotImplementedError( "torch.compile(fullgraph=True) cluster-tile pair_fn is not supported" ) if not has_cached_partition_metadata: if is_compiling: torch._assert_async( batch_ptr[-1] == N, "batch_ptr[-1] must equal positions.shape[0]", ) elif int(batch_ptr[-1].item()) != N: raise ValueError(f"batch_ptr[-1] ({int(batch_ptr[-1])}) != N ({N})") scratch_values = ( sorted_atom_index, sort_inv, sorted_pos_x, sorted_pos_y, sorted_pos_z, batch_idx_sorted, batch_ptr_padded, group_system, group_ptr, group_ctr_x, group_ctr_y, group_ctr_z, group_ext_x, group_ext_y, group_ext_z, num_tiles, tile_row_group, tile_col_group, tile_system, ) has_complete_scratch = all(value is not None for value in scratch_values) if is_compiling and not has_complete_scratch: raise ValueError( "compiled batch_cluster_tile_neighbor_list requires complete scratch " "from allocate_batch_cluster_tile_list" ) if not is_compiling and sorted_atom_index is not None and not has_complete_scratch: raise ValueError( "batch_cluster_tile scratch must include every tensor returned by " "allocate_batch_cluster_tile_list" ) if rebuild_flags is not None and format == "coo": if neighbor_list is not None and pair_offsets is None and pair_counts is None: raise ValueError( "selective segmented COO bootstrap requires complete caller-owned " "state or no persistent state" ) eager_has_false = ( rebuild_flags is not None and not is_compiling and not eager_all_true ) if eager_has_false: required_state: dict[str, torch.Tensor | None] = { "num_tiles": num_tiles, "tile_row_group": tile_row_group, "tile_col_group": tile_col_group, "tile_system": tile_system, "tile_offsets": tile_offsets, "tile_counts": tile_counts, } if format == "coo": required_state.update( { "neighbor_list": neighbor_list, "neighbor_list_shifts": neighbor_list_shifts, "pair_offsets": pair_offsets, "pair_counts": pair_counts, } ) else: required_state.update( { "neighbor_matrix": neighbor_matrix, "num_neighbors": num_neighbors, "neighbor_matrix_shifts": neighbor_matrix_shifts, } ) if cutoff2 is not None: required_state.update( { "neighbor_matrix2": neighbor_matrix2, "num_neighbors2": num_neighbors2, "neighbor_matrix_shifts2": neighbor_matrix_shifts2, } ) missing_state = [ name for name, value in required_state.items() if value is None ] if missing_state: raise ValueError( "selective segmented COO with rebuild_flags=False requires " "caller-owned topology and tile state: " + ", ".join(missing_state) ) if is_compiling and rebuild_flags is not None and format == "matrix": compiled_state = ( neighbor_matrix, num_neighbors, neighbor_matrix_shifts, num_tiles, tile_offsets, tile_counts, tile_row_group, tile_col_group, tile_system, ) if any(value is None for value in compiled_state): raise ValueError( "selective compiled matrix requires complete caller-owned state" ) if cutoff2 is not None and any( value is None for value in (neighbor_matrix2, num_neighbors2, neighbor_matrix_shifts2) ): raise ValueError( "selective compiled dual-cutoff matrix requires both output triples" ) if max_neighbors is None: max_neighbors = max( estimate_max_neighbors( cutoff if cutoff2 is None else max(float(cutoff), float(cutoff2)) ), TILE_GROUP_SIZE, ) if fill_value is None: fill_value = N if format == "matrix": _validate_cluster_tile_matrix_outputs( device=device, dtype=positions.dtype, natom=N, max_neighbors=int(max_neighbors), cutoff2=cutoff2, neighbor_matrix2=neighbor_matrix2, num_neighbors2=num_neighbors2, neighbor_matrix_shifts2=neighbor_matrix_shifts2, return_vectors=return_vectors, return_distances=return_distances, neighbor_vectors=neighbor_vectors, neighbor_distances=neighbor_distances, allocate_missing=True, ) build_cutoff = cutoff if cutoff2 is None else max(float(cutoff), float(cutoff2)) needs_segmented_tiles = rebuild_flags is not None or pair_offsets is not None if max_tiles_per_group is None: if has_complete_scratch and not ( needs_segmented_tiles and tile_offsets is None and not is_compiling ): max_tiles_per_group = 1 else: max_tiles_per_group = estimate_batch_max_tiles_per_group( batch_ptr, build_cutoff, cell_batch ) else: max_tiles_per_group = int(max_tiles_per_group) if ( rebuild_flags is not None and format == "matrix" and neighbor_matrix is not None and num_neighbors is not None and neighbor_matrix_shifts is not None and num_tiles is not None and tile_offsets is not None and tile_counts is not None and tile_row_group is not None and tile_col_group is not None and tile_system is not None ): _validate_batch_matrix_state( device=device, natom=N, num_systems=num_systems, max_neighbors=int(max_neighbors), neighbor_matrix=neighbor_matrix, num_neighbors=num_neighbors, neighbor_matrix_shifts=neighbor_matrix_shifts, num_tiles=num_tiles, tile_offsets=tile_offsets, tile_counts=tile_counts, tile_row_group=tile_row_group, tile_col_group=tile_col_group, tile_system=tile_system, neighbor_matrix2=neighbor_matrix2, num_neighbors2=num_neighbors2, neighbor_matrix_shifts2=neighbor_matrix_shifts2, ) if needs_segmented_tiles and tile_offsets is None: if is_compiling: raise ValueError( "compiled segmented cluster-tile calls require tile_offsets and " "tile_counts" ) _tile_caps, tile_offsets, _pair_caps, default_pair_offsets = ( estimate_batch_cluster_tile_segments( batch_ptr, int(max_neighbors), max_tiles_per_group=int(max_tiles_per_group), ) ) tile_counts = torch.zeros( int(tile_offsets.shape[0]) - 1, dtype=torch.int32, device=device ) if format == "coo" and pair_offsets is None: pair_offsets = default_pair_offsets pair_counts = torch.zeros( int(pair_offsets.shape[0]) - 1, dtype=torch.int32, device=device ) if max_pairs is not None and int(max_pairs) != int(pair_offsets[-1].item()): raise ValueError( "max_pairs must equal the estimated segmented COO capacity" ) elif needs_segmented_tiles and tile_counts is None: raise ValueError("tile_counts is required when tile_offsets is provided") geometry_requested = ( (return_vectors or return_distances) and pair_fn is None and pair_params is None and pair_energies is None and pair_forces is None ) requires_reconstruction = ( geometry_requested and torch.is_grad_enabled() and ( positions.requires_grad or (_live_cell if _live_cell is not None else cell_batch).requires_grad ) ) geometry_cell_batch = _live_cell if _live_cell is not None else cell_batch if requires_reconstruction and geometry_cell_batch.ndim == 2: geometry_cell_batch = geometry_cell_batch.unsqueeze(0).expand( num_systems, -1, -1 ) snapshot_vectors = neighbor_vectors is not None snapshot_distances = neighbor_distances is not None if format == "coo" and pair_offsets is None: if max_pairs is None: max_pairs = N * max_neighbors _validate_compact_coo_outputs( device=device, capacity=int(max_pairs), neighbor_list=neighbor_list, neighbor_list_shifts=neighbor_list_shifts, pair_counter=pair_counter, ) if geometry_requested: neighbor_vectors, neighbor_distances = ( _prepare_compact_coo_geometry_buffers( device=device, dtype=positions.dtype, capacity=int(max_pairs), return_vectors=bool(return_vectors), return_distances=bool(return_distances), neighbor_vectors=neighbor_vectors, neighbor_distances=neighbor_distances, allocate_missing=not requires_reconstruction, ) ) if sorted_atom_index is None: previous_tile_state = ( num_tiles, tile_row_group, tile_col_group, tile_system, ) ( sorted_atom_index, sort_inv, sorted_pos_x, sorted_pos_y, sorted_pos_z, batch_idx_sorted, batch_ptr_padded, group_system, group_ptr, group_ctr_x, group_ctr_y, group_ctr_z, group_ext_x, group_ext_y, group_ext_z, num_tiles, tile_row_group, tile_col_group, tile_system, ) = allocate_batch_cluster_tile_list( batch_ptr, device, dtype=positions.dtype, max_tiles_per_group=max_tiles_per_group, ) ( previous_num_tiles, previous_tile_row_group, previous_tile_col_group, previous_tile_system, ) = previous_tile_state if previous_num_tiles is not None: num_tiles = previous_num_tiles if previous_tile_row_group is not None: tile_row_group = previous_tile_row_group if previous_tile_col_group is not None: tile_col_group = previous_tile_col_group if previous_tile_system is not None: tile_system = previous_tile_system segmented_pair_capacity: int | None = None if format == "coo" and pair_offsets is not None: if neighbor_list is None: capacity = int(pair_offsets[-1].item()) neighbor_list = torch.empty( (2, capacity), dtype=torch.int32, device=device, ) if neighbor_list_shifts is None: neighbor_list_shifts = torch.empty( (neighbor_list.shape[1], 3), dtype=torch.int32, device=device, ) segmented_pair_capacity = _validate_segmented_coo_state( device=device, num_systems=num_systems, neighbor_list=neighbor_list, neighbor_list_shifts=neighbor_list_shifts, pair_offsets=pair_offsets, pair_counts=pair_counts, rebuild_flags=rebuild_flags, tile_offsets=tile_offsets, tile_counts=tile_counts, num_tiles=num_tiles, tile_row_group=tile_row_group, tile_col_group=tile_col_group, tile_system=tile_system, ) if partition_metadata is None: _validate_batch_cluster_tile_scratch( positions=positions, batch_ptr=batch_ptr, sorted_atom_index=sorted_atom_index, sort_inv=sort_inv, sorted_pos_x=sorted_pos_x, sorted_pos_y=sorted_pos_y, sorted_pos_z=sorted_pos_z, batch_idx_sorted=batch_idx_sorted, batch_ptr_padded=batch_ptr_padded, group_system=group_system, group_ptr=group_ptr, group_ctr_x=group_ctr_x, group_ctr_y=group_ctr_y, group_ctr_z=group_ctr_z, group_ext_x=group_ext_x, group_ext_y=group_ext_y, group_ext_z=group_ext_z, num_tiles=num_tiles, tile_row_group=tile_row_group, tile_col_group=tile_col_group, tile_system=tile_system, ) if rebuild_flags is not None and not is_compiling and not eager_any_rebuild: if format == "coo": outputs = ( neighbor_list, pair_offsets, pair_counts, neighbor_list_shifts, ) elif cutoff2 is not None: outputs = ( neighbor_matrix, num_neighbors, neighbor_matrix_shifts, neighbor_matrix2, num_neighbors2, neighbor_matrix_shifts2, ) else: outputs = (neighbor_matrix, num_neighbors, neighbor_matrix_shifts) if return_state: return ( *outputs, tile_offsets, tile_counts, num_tiles, tile_row_group, tile_col_group, tile_system, ) return outputs if partition_metadata is None: partition_metadata = _prepare_batch_partition_metadata( batch_ptr, num_atoms=N, padded_slot_system=batch_idx_sorted, batch_ptr_padded=batch_ptr_padded, group_system=group_system, group_ptr=group_ptr, ) inv_cell_batch = _batch_build_cluster_tile_list_normalized( partition_metadata, positions.detach() if requires_reconstruction else positions, build_cutoff, cell_batch.detach() if requires_reconstruction else cell_batch, batch_ptr, sorted_atom_index, sort_inv, sorted_pos_x, sorted_pos_y, sorted_pos_z, batch_idx_sorted, batch_ptr_padded, group_system, group_ptr, group_ctr_x, group_ctr_y, group_ctr_z, group_ext_x, group_ext_y, group_ext_z, num_tiles, tile_row_group, tile_col_group, tile_system, inv_cell_batch=( inv_cell_batch.detach() if requires_reconstruction and inv_cell_batch is not None else inv_cell_batch ), rebuild_flags=rebuild_flags, tile_offsets=tile_offsets, tile_counts=tile_counts, force_compute_inv_cell=force_compute_inv_cell, _fixed_cell_geometry=_fixed_cell_geometry, ) if format == "tile": # Raw-tile callers must not receive a silently truncated tile list # (format='tile' is always the compact, non-selective path). tile_capacity = int(tile_row_group.shape[0]) _check_tile_buffer_capacity(num_tiles, tile_capacity) return ( num_tiles, tile_row_group, tile_col_group, tile_system, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, batch_idx_sorted, batch_ptr_padded, group_ptr, ) if format == "coo": segmented_coo = pair_offsets is not None if segmented_coo: if segmented_pair_capacity is None: raise RuntimeError("segmented COO state was not validated") max_pairs = segmented_pair_capacity elif max_pairs is None: max_pairs = N * max_neighbors if neighbor_list is None: coo_buf = torch.empty( (max_pairs, 2), dtype=torch.int32, device=device, ) else: coo_buf = neighbor_list.transpose(0, 1) if neighbor_list_shifts is None: neighbor_list_shifts = torch.empty( (max_pairs, 3), dtype=torch.int32, device=device, ) if pair_counter is None: pair_counter = torch.zeros(1, dtype=torch.int32, device=device) if segmented_coo and pair_counts is None: pair_counts = torch.empty( int(pair_offsets.shape[0]) - 1, dtype=torch.int32, device=device ) if not segmented_coo: if is_compiling: version = torch.__version__.split("+", maxsplit=1)[0] major, minor = (int(part) for part in version.split(".")[:2]) if (major, minor) < (2, 10): raise RuntimeError( "torch.compile(fullgraph=True) exact cluster-tile COO requires PyTorch 2.10 or newer" ) copy_vectors = bool( geometry_requested and return_vectors and not requires_reconstruction ) copy_distances = bool( geometry_requested and return_distances and not requires_reconstruction ) if not geometry_requested: prefix_vectors = torch.empty( (1, 3), dtype=positions.dtype, device=device ) prefix_distances = torch.empty(1, dtype=positions.dtype, device=device) if geometry_requested: prefix_vectors = neighbor_vectors prefix_distances = neighbor_distances tile_capacity = int(tile_row_group.shape[0]) n_tiles = _check_tile_buffer_capacity(num_tiles, tile_capacity) launch_tiles = tile_capacity if is_compiling else n_tiles row_counts = torch.zeros(N, dtype=torch.int32, device=device) direct_cell = cell_batch.detach() if requires_reconstruction else cell_batch direct_inv_cell = ( torch.linalg.inv(direct_cell).contiguous() if inv_cell_batch is None else ( inv_cell_batch.detach() if requires_reconstruction else inv_cell_batch ) ) _batch_cluster_tile_direct_csr_count_op( cutoff, N, direct_cell, direct_inv_cell, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, tile_system, row_counts, launch_tiles, *_fixed_cluster_op_args(_fixed_cell_geometry), ) neighbor_ptr = torch.cat( ( torch.zeros(1, dtype=torch.int32, device=device), torch.cumsum(row_counts, dim=0, dtype=torch.int32), ) ) total = neighbor_ptr[-1:] resolved_count = _check_neighbor_capacity( total, int(max_pairs), kind="coo", ) pair_counter.copy_(total) cursors = neighbor_ptr[:-1].clone() if pair_fn is None: _batch_cluster_tile_direct_csr_fill_op( cutoff, N, direct_cell, direct_inv_cell, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, tile_system, cursors, coo_buf, neighbor_list_shifts, prefix_vectors, prefix_distances, copy_vectors, copy_distances, int(max_pairs), launch_tiles, *_fixed_cluster_op_args(_fixed_cell_geometry), ) else: if pair_params is None or pair_energies is None or pair_forces is None: raise ValueError( "pair_fn requires pair_params, pair_energies, and pair_forces" ) _batch_cluster_tile_direct_csr_fill_pair_outputs( cutoff, N, direct_cell, direct_inv_cell, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, tile_system, cursors, coo_buf, neighbor_list_shifts, pair_fn, pair_params, pair_energies, pair_forces, neighbor_vectors, neighbor_distances, return_vectors, return_distances, int(max_pairs), launch_tiles, _fixed_cell_geometry=_fixed_cell_geometry, ) nl, nls, exact_vectors, exact_distances = _compact_coo_prefix( total, coo_buf, neighbor_list_shifts, prefix_vectors, prefix_distances, int(max_pairs), -1 if is_compiling else resolved_count, copy_vectors, copy_distances, ) if geometry_requested: if requires_reconstruction: batch_idx_atom = ( partition_metadata.atom_system if partition_metadata is not None else torch.repeat_interleave( torch.arange(num_systems, dtype=torch.int32, device=device), (batch_ptr[1:] - batch_ptr[:-1]).to(torch.int64), output_size=N, ) ) exact_distances, exact_vectors = _reconstruct_coo_geometry( positions, geometry_cell_batch, nl, nls, batch_idx_atom, ) if return_vectors and snapshot_vectors: neighbor_vectors[: exact_vectors.shape[0]].copy_( exact_vectors.detach() ) if return_distances and snapshot_distances: neighbor_distances[: exact_distances.shape[0]].copy_( exact_distances.detach() ) outputs = (nl, neighbor_ptr, nls) if return_distances: outputs = (*outputs, exact_distances) if return_vectors: outputs = (*outputs, exact_vectors) return outputs return nl, neighbor_ptr, nls batch_query_cluster_tile_coo( sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, cell_batch, num_tiles, tile_row_group, tile_col_group, tile_system, cutoff, N, int(max_pairs), pair_counter, coo_buf, neighbor_list_shifts, inv_cell_batch=inv_cell_batch, rebuild_flags=rebuild_flags, tile_offsets=tile_offsets, tile_counts=tile_counts, pair_offsets=pair_offsets, pair_counts=pair_counts, return_vectors=return_vectors, return_distances=return_distances, pair_fn=pair_fn, pair_params=pair_params, neighbor_vectors=neighbor_vectors, neighbor_distances=neighbor_distances, pair_energies=pair_energies, pair_forces=pair_forces, _fixed_cell_geometry=_fixed_cell_geometry, ) if segmented_coo: segment_caps = pair_offsets[1:] - pair_offsets[:-1] _check_neighbor_capacity( pair_counts, segment_caps, segmented=True, kind="coo", ) outputs = ( neighbor_list, pair_offsets, pair_counts, neighbor_list_shifts, ) if return_state: return ( *outputs, tile_offsets, tile_counts, num_tiles, tile_row_group, tile_col_group, tile_system, ) return outputs # ``format == "matrix"``: skip-prefill matrix path with tail fill. if neighbor_matrix is None: neighbor_matrix = torch.empty( (N, max_neighbors), dtype=torch.int32, device=device, ) if num_neighbors is None: num_neighbors = torch.zeros(N, dtype=torch.int32, device=device) elif rebuild_flags is None: num_neighbors.zero_() if neighbor_matrix_shifts is None: neighbor_matrix_shifts = torch.empty( (N, max_neighbors, 3), dtype=torch.int32, device=device, ) if cutoff2 is not None: if neighbor_matrix2 is None: neighbor_matrix2 = torch.empty( (N, max_neighbors), dtype=torch.int32, device=device ) if num_neighbors2 is None: num_neighbors2 = torch.zeros(N, dtype=torch.int32, device=device) elif rebuild_flags is None: num_neighbors2.zero_() if neighbor_matrix_shifts2 is None: neighbor_matrix_shifts2 = torch.empty( (N, max_neighbors, 3), dtype=torch.int32, device=device ) # Reconstructed geometry is returned directly and needs an output buffer # only when the caller requested a detached snapshot. if return_vectors and neighbor_vectors is None and not requires_reconstruction: neighbor_vectors = torch.empty( (N, max_neighbors, 3), dtype=positions.dtype, device=device, ) if return_distances and neighbor_distances is None and not requires_reconstruction: neighbor_distances = torch.empty( (N, max_neighbors), dtype=positions.dtype, device=device, ) if pair_fn is not None: if pair_energies is None: pair_energies = torch.empty( (N, max_neighbors), dtype=positions.dtype, device=device, ) if pair_forces is None: pair_forces = torch.empty( (N, max_neighbors, 3), dtype=positions.dtype, device=device, ) batch_idx_atom = None if rebuild_flags is not None or requires_reconstruction: batch_idx_atom = ( partition_metadata.atom_system if partition_metadata is not None else torch.repeat_interleave( torch.arange(num_systems, dtype=torch.int32, device=device), (batch_ptr[1:] - batch_ptr[:-1]).to(torch.int64), output_size=N, ) ) batch_query_cluster_tile( sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, cell_batch.detach() if requires_reconstruction else cell_batch, num_tiles, tile_row_group, tile_col_group, tile_system, cutoff, N, neighbor_matrix, num_neighbors, neighbor_matrix_shifts, inv_cell_batch=( inv_cell_batch.detach() if requires_reconstruction and inv_cell_batch is not None else inv_cell_batch ), cutoff2=cutoff2, neighbor_matrix2=neighbor_matrix2, num_neighbors2=num_neighbors2, neighbor_matrix_shifts2=neighbor_matrix_shifts2, rebuild_flags=rebuild_flags, tile_offsets=tile_offsets, tile_counts=tile_counts, batch_idx=batch_idx_atom, return_vectors=return_vectors and not requires_reconstruction, return_distances=return_distances and not requires_reconstruction, pair_fn=pair_fn, pair_params=pair_params, neighbor_vectors=None if requires_reconstruction else neighbor_vectors, neighbor_distances=None if requires_reconstruction else neighbor_distances, pair_energies=pair_energies, pair_forces=pair_forces, _fixed_cell_geometry=_fixed_cell_geometry, ) if max_neighbors > 0: if rebuild_flags is not None: secondary_matrix = ( neighbor_matrix2 if neighbor_matrix2 is not None else torch.empty((1, 1), dtype=torch.int32, device=device) ) secondary_counts = ( num_neighbors2 if num_neighbors2 is not None else torch.empty(1, dtype=torch.int32, device=device) ) _batch_cluster_tile_selective_fill_neighbor_matrix_tail_op( num_neighbors, secondary_counts, batch_idx_atom, rebuild_flags, neighbor_matrix, secondary_matrix, int(max_neighbors), int(fill_value), bool(cutoff2 is not None), ) else: _batch_cluster_tile_fill_neighbor_matrix_tail_op( num_neighbors, neighbor_matrix, int(N), int(max_neighbors), int(fill_value), ) if cutoff2 is not None: _batch_cluster_tile_fill_neighbor_matrix_tail_op( num_neighbors2, neighbor_matrix2, int(N), int(max_neighbors), int(fill_value), ) _check_neighbor_capacity(num_neighbors, int(max_neighbors)) if cutoff2 is not None and num_neighbors2 is not None: _check_neighbor_capacity(num_neighbors2, int(max_neighbors)) if requires_reconstruction: distances, vectors = _reconstruct_matrix_geometry( positions, geometry_cell_batch, neighbor_matrix, num_neighbors, neighbor_matrix_shifts, batch_idx_atom, ) row_update = ( rebuild_flags[batch_idx_atom.to(torch.long)] if rebuild_flags is not None else torch.ones(N, dtype=torch.bool, device=device) ) if return_vectors and snapshot_vectors: neighbor_vectors.copy_( torch.where( row_update[:, None, None], vectors.detach(), neighbor_vectors.detach(), ) ) if return_distances and snapshot_distances: neighbor_distances.copy_( torch.where( row_update[:, None], distances.detach(), neighbor_distances.detach(), ) ) if cutoff2 is not None: outputs = ( neighbor_matrix, num_neighbors, neighbor_matrix_shifts, neighbor_matrix2, num_neighbors2, neighbor_matrix_shifts2, ) else: outputs = (neighbor_matrix, num_neighbors, neighbor_matrix_shifts) if geometry_requested and rebuild_flags is None: if return_distances: outputs = ( *outputs, distances if requires_reconstruction else neighbor_distances, ) if return_vectors: outputs = ( *outputs, vectors if requires_reconstruction else neighbor_vectors, ) if return_state: return ( *outputs, tile_offsets, tile_counts, num_tiles, tile_row_group, tile_col_group, tile_system, ) return outputs
[docs] def batch_cluster_tile_neighbor_list( positions: torch.Tensor, cutoff: float, cell_batch: torch.Tensor, batch_ptr: torch.Tensor, max_neighbors: int | None = None, fill_value: int | None = None, format: str = "matrix", max_pairs: int | None = None, cutoff2: float | None = None, rebuild_flags: torch.Tensor | None = None, return_state: bool = False, neighbor_matrix: torch.Tensor | None = None, neighbor_matrix_shifts: torch.Tensor | None = None, num_neighbors: torch.Tensor | None = None, neighbor_matrix2: torch.Tensor | None = None, neighbor_matrix_shifts2: torch.Tensor | None = None, num_neighbors2: torch.Tensor | None = None, neighbor_list: torch.Tensor | None = None, neighbor_list_shifts: torch.Tensor | None = None, pair_counter: torch.Tensor | None = None, pair_offsets: torch.Tensor | None = None, pair_counts: torch.Tensor | None = None, inv_cell_batch: torch.Tensor | None = None, sorted_atom_index: torch.Tensor | None = None, sort_inv: torch.Tensor | None = None, sorted_pos_x: torch.Tensor | None = None, sorted_pos_y: torch.Tensor | None = None, sorted_pos_z: torch.Tensor | None = None, batch_idx_sorted: torch.Tensor | None = None, batch_ptr_padded: torch.Tensor | None = None, group_system: torch.Tensor | None = None, group_ptr: torch.Tensor | None = None, group_ctr_x: torch.Tensor | None = None, group_ctr_y: torch.Tensor | None = None, group_ctr_z: torch.Tensor | None = None, group_ext_x: torch.Tensor | None = None, group_ext_y: torch.Tensor | None = None, group_ext_z: torch.Tensor | None = None, num_tiles: torch.Tensor | None = None, tile_row_group: torch.Tensor | None = None, tile_col_group: torch.Tensor | None = None, tile_system: torch.Tensor | None = None, tile_offsets: torch.Tensor | None = None, tile_counts: torch.Tensor | None = None, return_vectors: bool = False, return_distances: bool = False, pair_fn: wp.Function | None = None, pair_params: torch.Tensor | None = None, neighbor_vectors: torch.Tensor | None = None, neighbor_distances: torch.Tensor | None = None, pair_energies: torch.Tensor | None = None, pair_forces: torch.Tensor | None = None, max_tiles_per_group: int | None = None, ) -> tuple[torch.Tensor, ...]: """Build and query a batched cluster-pair tile neighbor list in one call.""" return _batch_cluster_tile_neighbor_list_impl( None, positions, cutoff, cell_batch, batch_ptr, max_neighbors=max_neighbors, fill_value=fill_value, format=format, max_pairs=max_pairs, cutoff2=cutoff2, rebuild_flags=rebuild_flags, return_state=return_state, neighbor_matrix=neighbor_matrix, neighbor_matrix_shifts=neighbor_matrix_shifts, num_neighbors=num_neighbors, neighbor_matrix2=neighbor_matrix2, neighbor_matrix_shifts2=neighbor_matrix_shifts2, num_neighbors2=num_neighbors2, neighbor_list=neighbor_list, neighbor_list_shifts=neighbor_list_shifts, pair_counter=pair_counter, pair_offsets=pair_offsets, pair_counts=pair_counts, inv_cell_batch=inv_cell_batch, sorted_atom_index=sorted_atom_index, sort_inv=sort_inv, sorted_pos_x=sorted_pos_x, sorted_pos_y=sorted_pos_y, sorted_pos_z=sorted_pos_z, batch_idx_sorted=batch_idx_sorted, batch_ptr_padded=batch_ptr_padded, group_system=group_system, group_ptr=group_ptr, group_ctr_x=group_ctr_x, group_ctr_y=group_ctr_y, group_ctr_z=group_ctr_z, group_ext_x=group_ext_x, group_ext_y=group_ext_y, group_ext_z=group_ext_z, num_tiles=num_tiles, tile_row_group=tile_row_group, tile_col_group=tile_col_group, tile_system=tile_system, tile_offsets=tile_offsets, tile_counts=tile_counts, return_vectors=return_vectors, return_distances=return_distances, pair_fn=pair_fn, pair_params=pair_params, neighbor_vectors=neighbor_vectors, neighbor_distances=neighbor_distances, pair_energies=pair_energies, pair_forces=pair_forces, max_tiles_per_group=max_tiles_per_group, )
batch_cluster_tile_neighbor_list.__doc__ = ( _batch_cluster_tile_neighbor_list_impl.__doc__ )