Rate this Page

Source code for monarch.common.device_mesh

# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

# pyre-unsafe

import logging
import warnings
from contextlib import AbstractContextManager, contextmanager
from dataclasses import dataclass
from enum import Enum
from logging import Logger
from typing import (
    Any,
    Callable,
    Dict,
    List,
    NamedTuple,
    Optional,
    Sequence,
    Tuple,
    TYPE_CHECKING,
    Union,
)

import monarch.common.messages as messages
import torch
from monarch._src.actor.shape import MeshTrait, NDSlice, Shape
from torch.utils._python_dispatch import TorchDispatchMode
from torch.utils._pytree import tree_map
from torch.utils.weak import weakref

from ._tensor_to_table import tensor_to_table
from .context_manager import activate_first_context_manager
from .messages import Dims
from .reference import Referenceable
from .stream import Stream
from .tensor import MeshSliceTensor, Tensor

if TYPE_CHECKING:
    from monarch.common.client import Client

logger: Logger = logging.getLogger(__name__)


[docs]class RemoteProcessGroup(Referenceable): """ Client's view of a process group. """
[docs] def __init__(self, dims, device_mesh): logger.info(f"creating process group for {dims}") self.dims = dims self.device_mesh = device_mesh self.ref = self.device_mesh.client.new_ref() self._create_remotely() # A set of streams for which we've sent the split-comm message. self._split_comm_done = set()
def _create_remotely(self): msg = messages.CreateRemoteProcessGroup(self, self.device_mesh, self.dims) self.device_mesh._send(msg)
[docs] def ensure_split_comm_remotely(self, stream): """ If we haven't already, send a message to the worker to split off a communicator for this PG on the given stream. """ # Currently, the worker will error if we try to do the split-comm more # than once, so check for that here to allow this function to be called # lazily. if stream in self._split_comm_done: return self._split_comm_done.add(stream) msg = messages.SplitCommForProcessGroup( # pyrefly: ignore [bad-argument-type] remote_process_group=self, stream=stream, ) self.device_mesh.client.send_nocoalesce( self.device_mesh.client.all_ranks, msg, )
[docs] def delete_ref(self, ref: int): if not self.device_mesh.client.has_shutdown: self.device_mesh.client.handle_deletes(self.device_mesh.processes, [ref])
[docs] def drop(self): if self.ref is None: return self._drop_ref()
[docs] def size(self): return self.device_mesh.size(self.dims)
def _drop_ref(self): if self.ref is None: return self.delete_ref(self.ref) self.ref = None @property def dropped(self): return self.ref is None
class ActivateGuard: def __init__(self, iter): self.iter = iter next(iter) def __enter__(self): return def __exit__(self, exc_type, exc_val, exc_tb): try: next(self.iter) except StopIteration: pass class DeviceMeshStatus(Enum): """ Enum representing the status of a device mesh. Attributes: LIVE (str): The mesh has enough processes than the world size specified and all of them are healthy. UNHEALTHY (str): Either the mesh does not have enough processes or some of the processes are unhealthy. AWAITING_CREATION (str): The mesh is still being created by the scheduler. """ LIVE = "Live" UNHEALTHY = "Unhealthy" AWAITING_CREATION = "Awaiting Creation" @dataclass class DeviceMeshInfo: """ Data class representing information about a device mesh. Attributes: mesh_labels (Dict[str, str]): Maps mesh labels to values. devices_labels (List[Dict[str, str]]): MAps device labels to values. """ mesh_labels: Dict[str, str] devices_labels: List[Dict[str, str]] class DeviceMesh(Referenceable, MeshTrait): """A mesh of devices for distributed tensor operations. DeviceMesh represents a collection of devices arranged in a multidimensional grid for parallel computation. It manages communication between devices and enables distributed execution of operations across the mesh. Args: client (Client): The client connection to the mesh infrastructure processes (NDSlice): Multi-dimensional slice representing the process layout names (Dims): Names for each dimension of the mesh mesh_name (str, optional): Name identifier for the mesh. Default: "default" """ def __init__( self, client: "Client", processes: "NDSlice", names: Dims, mesh_name: str = "default", ): assert isinstance(processes, NDSlice) self.client = client assert processes.ndim == len(names) self.names = names self.mesh_name = mesh_name # processes are a list of processes that participate in this device mesh, encoded as an NDSlice self.processes = processes self.exit = lambda: None self.ref = None self._active_mesh_context = None self._subset_of: Optional[weakref.ReferenceType["DeviceMesh"]] = None def define_remotely(self): if self.ref is None: self.ref = self.client.new_ref() msg = messages.CreateDeviceMesh(self, self.names, self.processes) self.client.send(self.processes, msg) def process_group(self, dims: str | Dims) -> RemoteProcessGroup: self.define_remotely() if isinstance(dims, str): dims = (dims,) return RemoteProcessGroup(dims, self) def to_tensor(self): with no_mesh.activate(): vals = torch.tensor(list(self.processes), device="cpu", dtype=torch.int) return vals.view(self.processes.sizes) def to_table(self): with no_mesh.activate(): tensor = self.to_tensor() names = list(self.names) labels = [list(str(i) for i in range(i)) for i in tensor.shape] gpus_per_host = self.client.gpu_per_host def format_data(x): return f"{x // gpus_per_host}.gpu[{x % gpus_per_host}]" return tensor_to_table( tensor, format_data=format_data, axis_names=names, axis_labels=labels ) def __repr__(self): return f"<DeviceMesh(names({self.names}), processes({list(self.processes)})) at {hex(id(self))}>" def delete_ref(self, ref: int): if not self.client.has_shutdown: self.client.handle_deletes(self.processes, [ref]) def _send(self, cmd: NamedTuple): self.client.flush_deletes() self.client.send(self.processes, cmd) def stack(self, **kwargs): raise NotImplementedError() @property def _ndslice(self) -> NDSlice: return self.processes @property def _labels(self) -> Tuple[str, ...]: return self.names def _new_with_shape(self, shape: Shape) -> "DeviceMesh": mesh = DeviceMesh(self.client, shape.ndslice, tuple(shape.labels)) mesh.exit = self.exit mesh._subset_of = weakref.ref(self) return mesh def _is_subset_of(self, other: "DeviceMesh") -> bool: p = self while p is not None: if p is other: return True p = None if p._subset_of is None else p._subset_of() return False def __call__(self, **kwargs) -> "DeviceMesh": """ device_mesh(batch=3) or device_mesh(batch=slice(3, None)) """ warnings.warn( "The use of this method is deprecated. Please use mesh.slice instead.", DeprecationWarning, stacklevel=2, ) return self.slice(**kwargs) def rotate(self, **kwargs: Dict[str, int]): raise NotImplementedError() def rank(self, dims: Union[str, Sequence[str]]) -> torch.Tensor: self.define_remotely() if isinstance(dims, str): if dims not in self.names: raise KeyError(f"{self} does not have dimension {repr(dims)}") return _remote( _rank, propagate=lambda _self, _dims: torch.full((), 0, dtype=torch.long), )(self, dims) combined_rank: Any = 0 for dim in dims: combined_rank *= self.size(dim) combined_rank += self.rank(dim) return combined_rank @property def ranks(self) -> dict[str, torch.Tensor]: return {dim: self.rank(dim) for dim in self.names} def process_idx(self): self.define_remotely() return _remote( "monarch.worker.worker._process_idx", propagate=lambda _self: torch.full((), 0, dtype=torch.long), )(self) def _process(self, coordinates: Optional[Dict[str, int]]) -> NDSlice: if coordinates is None: return NDSlice(offset=self.processes.offset, sizes=[1], strides=[1]) if len(coordinates) > len(self.names): extra = set(coordinates.keys()) - set(self.names) raise KeyError(f"{list(extra)}") for name in self.names: if name not in coordinates: raise ValueError( f"Missing key '{name}' in shard map. Need all of {self.names}" ) flat = [coordinates[name] for name in self.names] return NDSlice(offset=self.processes.nditem(flat), sizes=[1], strides=[1]) def activate(self) -> AbstractContextManager: self._active_mesh_context = _active_mesh(self) return self._active_mesh_context def deactivate(self): if self._active_mesh_context is not None: self._active_mesh_context.__exit__(None, None, None) self._active_mesh_context = None _active: Optional[DeviceMesh] = None _dispatch_enabled = False
[docs]def get_active_mesh(): if _active is None: raise ValueError("no device mesh is active") return _active
class _ActiveMesh(TorchDispatchMode): ignore = ["profiler._record_function_exit._RecordFunction"] allowed_local_accessors = [ "aten._local_scalar_dense.default", "aten.set_.source_Storage_storage_offset", ] def __torch_dispatch__(self, func, types, args=(), kwargs=None): if _active is None: return func(*args, **kwargs) fnstr = str(func) if fnstr in self.ignore: return func(*args, **kwargs) # pyrefly: ignore [bad-index] if fnstr in self.allowed_local_accessors and not isinstance(args[0], Tensor): return func(*args, **kwargs) return _remote(func, propagate=func)(*args, **kwargs) def _rank(mesh, dim): return torch.full((), mesh.dims[dim].rank, dtype=torch.long) @contextmanager def _dispatch(): global _dispatch_enabled if _dispatch_enabled: yield else: _dispatch_enabled = True try: with _ActiveMesh(): yield finally: _dispatch_enabled = False _on_change: List[Callable] = [] @activate_first_context_manager def _active_mesh(mesh: Optional[DeviceMesh]): global _active for on_change in _on_change: on_change(_active, mesh) _active, old = mesh, _active try: with _dispatch(): yield finally: for on_change in _on_change: on_change(_active, old) _active = old class _NoMesh: def activate(self): return _active_mesh(None) no_mesh = _NoMesh() def _remote(*args, **kwargs): # device_mesh <-> tensor <-> remote are mututally recursive # we break the dependency to allow for separate files by # having device_mesh and tensor locally import the `remote` # entrypoint from monarch.common.remote import remote return remote(*args, **kwargs)
[docs]def to_mesh( tensors: Any, mesh: "DeviceMesh", stream: Optional[Stream] = None, ) -> Any: """ Move all tensors in tensors to the given mesh. """ def _to_mesh(tensor: Union["Tensor", "MeshSliceTensor"]) -> "Tensor": return tensor.to_mesh(mesh, stream) return tree_map(_to_mesh, tensors)
[docs]def slice_mesh( tensors: Any, **kwargs: Union[int, slice], ) -> Any: """ Performs the slice_mesh operation for each tensor in tensors. """ def _slice_mesh(tensor: "Tensor") -> "MeshSliceTensor": return tensor.slice_mesh(**kwargs) return tree_map(_slice_mesh, tensors)