summarize_from_feedback/utils/dist_utils.py (141 lines of code) (raw):

import logging import os import socket from abc import ABC, abstractmethod from functools import lru_cache from typing import Dict, Tuple, Iterable, TypeVar, Generic import numpy as np import torch import torch.distributed as dist from summarize_from_feedback.model_layout import ModelLayout class Comm: """ Thin wrapper around a dist.Group that stores the ranks and can print when in verbose mode """ def __init__(self, ranks, my_rank): ranks = list(sorted(ranks)) self._group = dist.new_group(ranks) self._mpi_group = create_mpi_group(ranks) self.ranks = ranks self.size = len(ranks) self.my_rank = my_rank @property def my_index(self): return self.ranks.index(self.my_rank) ################################################# ################ MPI COMMS ###################### ################################################# def barrier(self, name): self._mpi_group.barrier() def mpi_all_gather(self, tensor, name, validate_data_safety=True): """ all_gather using MPI. Slower, but accepts a broader variety of input types """ if validate_data_safety: validate_data_is_mpi_safe(tensor) return self._mpi_group.allgather(tensor) #################################################################### ########################## STANDARD COMMS ########################## #################################################################### def broadcast(self, tensor, src, name): self._broadcast(tensor, src, name, async_op=False) return tensor def _broadcast(self, tensor, src, name, async_op=False): if dist.get_backend() == "nccl": assert ( tensor.is_cuda ), f"Bad tensor - NCCL backend only supports cuda tensors: {name}; {tensor}" if len(self.ranks) == 1: # Conform to the comm.broadcast and comm.all_reduce API, but do no work if async_op: return NoopPromise() else: return tensor return dist.broadcast(tensor, src, group=self._group, async_op=async_op) def all_reduce(self, tensor, name): if dist.get_backend() == "nccl": assert tensor.is_cuda, f"Bad tensor - NCCL backend only supports cuda tensors: {name}" if len(self.ranks) == 1: return tensor dist.all_reduce(tensor, group=self._group, async_op=False) return tensor def all_gather_no_backward(self, tensor, name): if dist.get_backend() == "nccl": assert tensor.is_cuda, f"Bad tensor - NCCL backend only supports cuda tensors: {name}" tensor_list = [ torch.zeros(tensor.size(), dtype=tensor.dtype, device=tensor.device) for _ in range(self.size) ] dist.all_gather(tensor_list, tensor, group=self._group) return tensor_list def setup_cuda_device_and_dist( backend="nccl", master_addr=None, port=29500, world_size=None, device="cuda" ) -> torch.device: """ Set up the cuda device and then initialize nccl. We do these together because it's important that we initialize dist *after* we set the cuda device, otherwise GPU 0 will be responsible for all NCCL comms and will hang / OOM :param master_addr: The address of the master rank. Set to "127.0.0.1" to run locally. :param backend: One of ['nccl', 'gloo']. NCCL is ~10x faster, but often fails silently on inappropriate inputs, whereas gloo will often give a useful error message. We therefore recommend using gloo for debugging. :param port: Port that will be used when the master receives connection during the TCP initialization dance. :return: cuda device for this rank """ # This must be imported in order to get errors from all ranks to show up from mpi4py import MPI mpi_rank = MPI.COMM_WORLD.Get_rank() mpi_size = world_size or MPI.COMM_WORLD.Get_size() if device == "cuda": # Pin this rank to a specific GPU on the node local_rank = mpi_rank % int(os.environ.get("NUM_GPU", "8")) device = torch.device("cuda", local_rank) torch.cuda.set_device(local_rank) else: device = torch.device(device) if dist.is_initialized(): return device if master_addr is None: # Get the ip-address for rank 0 and broadcast it to all the ranks master_addr = MPI.COMM_WORLD.bcast(socket.gethostbyname(socket.gethostname())) os.environ["RANK"] = str(mpi_rank) os.environ["WORLD_SIZE"] = str(mpi_size) os.environ["MASTER_ADDR"] = master_addr os.environ["MASTER_PORT"] = str(port) assert dist.is_available() if mpi_rank == 0: logging.info(f"All nodes will connecting to master_addr: {master_addr}") # It's important that we initialize dist *after* we set the cuda device, otherwise # GPU 0 will be responsible for all NCCL comms and will hang / OOM dist.init_process_group(backend=backend, init_method=f"env://") return device _comm_cache: Dict[Tuple[int], "Comm"] = {} def create_mpi_group(ranks): from mpi4py import MPI group = MPI.COMM_WORLD.group.Incl(ranks) return MPI.COMM_WORLD.Create_group(group) def validate_data_is_mpi_safe(data, name="<unknown>"): known_safe_types = (int, float, str, bool, type(None), np.ndarray, np.generic) if isinstance(data, known_safe_types): pass elif isinstance(data, torch.Tensor): if data.is_cuda: raise ValueError( f"Data name={name} was a cuda tensor. MPI cannot handle CUDA tensors" f" as they result in unexpected CUDA OOMs." ) elif isinstance(data, dict): for k, v in data.items(): validate_data_is_mpi_safe(k) validate_data_is_mpi_safe(v, name=k) elif isinstance(data, Iterable): for item in data: validate_data_is_mpi_safe(item) else: raise ValueError(f"Data name={name} had unsupported type: {type(data)}") T = TypeVar("T") class Promise(ABC, Generic[T]): @abstractmethod def wait(self) -> T: pass class NoopPromise(Promise[None]): def wait(self): return @lru_cache() # Memoize when using the same layout def create_data_parallel_comm(layout: ModelLayout) -> Comm: """When using NCCL, all ranks must participate in construction of communicators. We use this object to instantiate the NCCL communicators correctly and provide a simplified API""" _my_dp_comm = None for other_rank in layout.ranks_in_my_replica: other_layout = ModelLayout(layout=layout.layout, my_rank=other_rank) dp_group = Comm(other_layout.dp_sibling_ranks, my_rank=other_rank) if other_rank == layout.my_rank: _my_dp_comm = dp_group return _my_dp_comm @lru_cache() def create_within_replica_comm(layout): """ Create a comm for all the shards and depths within a single replica Note that when using NCCL, all ranks must participate in construction of communicators. We use this object to instantiate the NCCL communicators correctly and provide a simplified API""" _my_comm = None for sibling_rank in layout.dp_sibling_ranks: layout_for_sibling = ModelLayout(layout=layout.layout, my_rank=sibling_rank) ranks_in_replica = layout_for_sibling.ranks_in_my_replica within_replica_comm = Comm(ranks_in_replica, my_rank=sibling_rank) if sibling_rank == layout.my_rank: _my_comm = within_replica_comm return _my_comm def create_model_parallel_comm(layout: ModelLayout): """When using NCCL, all ranks must participate in construction of communicators. We use this object to instantiate the NCCL communicators correctly and provide a simplified API""" _my_mp_comm = None # Set up model-parallel communication for replica_idx, ranks in enumerate(layout.layout): mp_group = Comm(ranks, my_rank=ranks[layout.shard_idx]) if replica_idx == layout.replica_idx: _my_mp_comm = mp_group assert _my_mp_comm is not None return _my_mp_comm