Source code for gnrs.core.gpu

"""
GPU device management for MPI-parallel workloads.

Implements a worker/feeder pattern where only a subset of MPI ranks
(GPU workers) load models onto GPUs, while the remaining ranks (feeders)
send structures to workers via MPI for computation. This avoids GPU OOM
when running with many MPI ranks and few GPUs.

This source code is licensed under the BSD-3-Clause license found in the
LICENSE file in the root directory of this source tree.
"""
from __future__ import annotations

__author__ = ["Yi Yang"]
__email__ = "yiy5@andrew.cmu.edu"
__group__ = "https://www.noamarom.com/"

import logging
from typing import Optional

import torch
from mpi4py import MPI

logger = logging.getLogger("gpu")

# MPI tags for worker/feeder communication
TAG_WORK_REQUEST = 100
TAG_WORK_DATA = 101
TAG_WORK_RESULT = 102
TAG_SHUTDOWN = 103


[docs] class GPUDeviceManager: """ Manages GPU device allocation across MPI ranks. Partitions ranks into GPU workers and CPU feeders. Workers are assigned to GPUs on their own node. Feeders offload computation to workers via MPI. If no rank has a GPU, every rank is a worker computing on CPU. Typical usage in HPC: - 1 GPU node with 1-4 GPUs, 32-128 CPU cores - Workers: 1 per GPU (or configurable) - Feeders: all remaining ranks """
[docs] def __init__( self, comm: MPI.Comm, max_workers_per_gpu: int = 1, ) -> None: """ Initialize GPU device manager. Args: comm: MPI communicator. max_workers_per_gpu: Maximum number of worker ranks per GPU. """ if max_workers_per_gpu < 1: raise ValueError( f"max_workers_per_gpu must be >= 1, got {max_workers_per_gpu}" ) self.comm = comm self.rank = comm.Get_rank() self.size = comm.Get_size() self.max_workers_per_gpu = max_workers_per_gpu node_comm = comm.Split_type(MPI.COMM_TYPE_SHARED) self.local_rank = node_comm.Get_rank() self.local_size = node_comm.Get_size() node_comm.Free() self.num_gpus = torch.cuda.device_count() if torch.cuda.is_available() else 0 # Assign GPUs by node-local rank: the first # num_gpus * max_workers_per_gpu ranks on each node become workers. self.gpu_id: Optional[int] = None if self.num_gpus > 0: if self.local_rank < self.num_gpus * self.max_workers_per_gpu: self.gpu_id = self.local_rank % self.num_gpus is_node_lead = self.local_rank == 0 gathered = comm.allgather( ( self.gpu_id is not None, self.num_gpus if is_node_lead else 0, is_node_lead, ) ) worker_flags = [flag for flag, _, _ in gathered] if not any(worker_flags): worker_flags = [True] * self.size self._worker_ranks = [r for r, flag in enumerate(worker_flags) if flag] self._feeder_ranks = [r for r, flag in enumerate(worker_flags) if not flag] self._is_worker = worker_flags[self.rank] if self.gpu_id is not None: # Pin this rank to its GPU and expose the plain "cuda" device # string: some calculators reject "cuda:N" and accept only # "cuda" or "cpu". With the device pinned, "cuda" resolves to # the assigned GPU. torch.cuda.set_device(self.gpu_id) self._device = "cuda" else: self._device = "cpu" logger.debug( f"GPU rank assignment: rank={self.rank} local_rank={self.local_rank} " f"node_gpus={self.num_gpus} gpu_id={self.gpu_id} device={self._device}" ) if self.rank == 0: total_gpus = sum(count for _, count, _ in gathered) num_nodes = sum(1 for _, _, lead in gathered if lead) logger.info( f"GPU Device Manager: {total_gpus} GPU(s) across " f"{num_nodes} node(s), {self.num_workers} worker rank(s), " f"{self.num_feeders} feeder rank(s)" )
@property def device(self) -> str: """ The torch device string for this rank ("cuda" or "cpu"). """ return self._device @property def is_worker(self) -> bool: """ Whether this rank is a GPU worker. """ return self._is_worker @property def is_feeder(self) -> bool: """ Whether this rank is a CPU feeder. """ return not self._is_worker @property def num_workers(self) -> int: """ Total number of GPU worker ranks. """ return len(self._worker_ranks) @property def num_feeders(self) -> int: """ Total number of CPU feeder ranks. """ return len(self._feeder_ranks) @property def worker_ranks(self) -> list[int]: """ List of all worker rank IDs. """ return list(self._worker_ranks) @property def feeder_ranks(self) -> list[int]: """ List of all feeder rank IDs. """ return list(self._feeder_ranks)
[docs] def assigned_worker(self) -> int: """ Return the worker rank this feeder is assigned to (round-robin). Returns: Worker rank ID """ if self._is_worker: return self.rank feeder_index = self._feeder_ranks.index(self.rank) return self._worker_ranks[feeder_index % len(self._worker_ranks)]
[docs] def assigned_feeders(self) -> list[int]: """ Return the feeder ranks assigned to this worker (round-robin). Returns: Feeder rank IDs; empty if this rank is a feeder. """ if not self._is_worker: return [] worker_index = self._worker_ranks.index(self.rank) return [ feeder_rank for i, feeder_rank in enumerate(self._feeder_ranks) if i % len(self._worker_ranks) == worker_index ]