Source code for nvalchemi.distributed._core.reshard

# SPDX-FileCopyrightText: Copyright (c) 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.
"""Per-element redistribution of ShardTensors based on destination rank.

``reshard_by_destination`` is the particle analogue of grid-based
``redistribute``.  Instead of changing placement strategy, it physically
moves elements between ranks based on a per-element destination map
(e.g., spatial rank assignment for atom migration).

Uses ``indexed_all_to_all_v_wrapper`` internally but returns a proper
``ShardTensor`` with updated ``sharding_shapes``.
"""

from __future__ import annotations

import logging
from typing import Any

import torch
import torch.distributed as dist

logger = logging.getLogger(__name__)


[docs] def reshard_by_destination( tensor: torch.Tensor, destinations: torch.Tensor, mesh: Any, # DeviceMesh at runtime ) -> torch.Tensor: """Redistribute tensor elements to new ranks based on per-element destinations. Unlike ``ShardTensor.redistribute()`` which changes placement strategy, this physically moves elements between ranks based on a destination map. Returns a plain ``torch.Tensor`` with the received elements. Parameters ---------- tensor : torch.Tensor Local tensor, shape ``(N_local, ...)``. destinations : torch.Tensor ``(N_local,)`` int tensor where ``destinations[i]`` is the rank that should own element ``i`` after resharding. mesh : DeviceMesh 1D device mesh for communication. Returns ------- torch.Tensor Received elements, shape ``(N_new_local, ...)``. """ # Single-process is a no-op: every element already lives on the only # rank. Gate on the *default* group's world size (not the mesh) so this # returns before touching ``mesh`` — both when ``dist`` is uninitialized # and when an ambient 1-rank group is up (e.g. a session-scoped gloo PG # under pytest). Resharding within a genuine multi-rank world proceeds. if not dist.is_initialized() or dist.get_world_size() == 1: return tensor from physicsnemo.distributed.utils import indexed_all_to_all_v_wrapper from nvalchemi.distributed._core.gather_primitives import mesh_group group = mesh_group(mesh) world_size = dist.get_world_size(group=group) device = tensor.device # Sort by destination for contiguous sends. destinations = destinations.to(torch.int64) counts = torch.bincount(destinations, minlength=world_size) sorted_idx = torch.argsort(destinations, stable=True) offsets = torch.cat( [torch.zeros(1, dtype=counts.dtype, device=device), counts.cumsum(0)] ) # Build per-rank send indices. send_indices: list[torch.Tensor] = [ sorted_idx[offsets[r] : offsets[r + 1]] for r in range(world_size) ] # All-gather send counts → sizes matrix. all_counts_list = [torch.zeros_like(counts) for _ in range(world_size)] dist.all_gather(all_counts_list, counts, group=group) sizes = [c.tolist() for c in all_counts_list] # Exchange. received = indexed_all_to_all_v_wrapper( tensor=tensor, indices=send_indices, sizes=sizes, dim=0, group=group, ) return received