169 lines
5.6 KiB
Python
169 lines
5.6 KiB
Python
# Copyright (c) ModelScope Contributors. All rights reserved.
|
|
from dataclasses import dataclass, field
|
|
from typing import Any, Dict, List, Tuple
|
|
|
|
from swift.utils import get_logger
|
|
|
|
logger = get_logger()
|
|
|
|
|
|
def sort_pgs_by_node_ip(pgs: List[Any]) -> List[Any]:
|
|
"""Sort placement groups by node IP for deterministic rank assignment.
|
|
|
|
Sorts PGs by node IP for deterministic ordering.
|
|
"""
|
|
import ray
|
|
|
|
node_ip = {node['NodeID']: node['NodeManagerAddress'] for node in ray.nodes()}
|
|
pg_ip = {}
|
|
for pg in pgs:
|
|
specs = ray._private.state.state.placement_group_table(pg.id)
|
|
node_id = specs['bundles_to_node_id'][0]
|
|
pg_ip[pg.id] = node_ip[node_id]
|
|
return sorted(pgs, key=lambda pg: pg_ip[pg.id])
|
|
|
|
|
|
@dataclass
|
|
class ResourcePool:
|
|
"""A pool of GPU resources backed by multiple Ray placement groups.
|
|
|
|
Args:
|
|
process_on_nodes: GPUs per node. ``[8]`` = 8 GPUs on 1 node,
|
|
``[4, 4]`` = 8 GPUs across 2 nodes.
|
|
max_colocate_count: How many WorkerGroups share these GPUs.
|
|
"""
|
|
|
|
process_on_nodes: List[int]
|
|
max_colocate_count: int = 1
|
|
|
|
pgs: List[Any] = field(default_factory=list, repr=False, init=False)
|
|
node_ips: List[str] = field(default_factory=list, repr=False, init=False)
|
|
bundle_infos: List[Tuple[str, str]] = field(default_factory=list, repr=False, init=False)
|
|
|
|
@property
|
|
def world_size(self) -> int:
|
|
return sum(self.process_on_nodes)
|
|
|
|
@property
|
|
def num_nodes(self) -> int:
|
|
return len(self.process_on_nodes)
|
|
|
|
@property
|
|
def visible_devices(self) -> List[int]:
|
|
"""Physical GPU ordinals: flat list across all nodes."""
|
|
return [int(info[1]) if info[1] else i for i, info in enumerate(self.bundle_infos)]
|
|
|
|
def create(self, device_name: str = 'GPU'):
|
|
"""Create one PG per node with STRICT_PACK strategy."""
|
|
import ray
|
|
from ray.util.placement_group import placement_group
|
|
|
|
if device_name == 'npu':
|
|
device_name = 'NPU'
|
|
elif device_name == 'cuda':
|
|
device_name = 'GPU'
|
|
|
|
bundle_template = {
|
|
device_name: 1,
|
|
'CPU': max(self.max_colocate_count, 1),
|
|
}
|
|
|
|
pgs = []
|
|
for n_gpus in self.process_on_nodes:
|
|
bundles = [bundle_template.copy() for _ in range(n_gpus)]
|
|
pg = placement_group(bundles, strategy='STRICT_PACK')
|
|
pgs.append(pg)
|
|
|
|
ray.get([pg.ready() for pg in pgs])
|
|
|
|
self.pgs = sort_pgs_by_node_ip(pgs)
|
|
self._discover_bundle_infos()
|
|
|
|
def _discover_bundle_infos(self):
|
|
"""Probe each bundle's accelerator_id via lightweight actors.
|
|
|
|
Swift-specific: needed because Swift uses num_gpus=0 + explicit
|
|
CUDA_VISIBLE_DEVICES (torchrun style).
|
|
"""
|
|
import os
|
|
import ray
|
|
from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy
|
|
from transformers.utils import is_torch_npu_available
|
|
|
|
@ray.remote(num_gpus=0.01, num_cpus=0.01)
|
|
def _probe_bundle():
|
|
ctx = ray.get_runtime_context()
|
|
acc_ids = ctx.get_accelerator_ids()
|
|
gpu_id = ''
|
|
for key in ('GPU', 'NPU'):
|
|
ids = acc_ids.get(key, [])
|
|
if ids:
|
|
gpu_id = ids[0]
|
|
break
|
|
return ctx.get_node_id(), gpu_id
|
|
|
|
all_infos: List[Tuple[str, str]] = []
|
|
node_id_to_ip = {node['NodeID']: node['NodeManagerAddress'] for node in ray.nodes()}
|
|
|
|
for pg_idx, pg in enumerate(self.pgs):
|
|
refs = [
|
|
_probe_bundle.options(
|
|
scheduling_strategy=PlacementGroupSchedulingStrategy(
|
|
placement_group=pg, placement_group_bundle_index=i), ).remote()
|
|
for i in range(self.process_on_nodes[pg_idx])
|
|
]
|
|
results = ray.get(refs)
|
|
for r in results:
|
|
all_infos.append(r)
|
|
|
|
vis_key = 'ASCEND_RT_VISIBLE_DEVICES' if is_torch_npu_available() else 'CUDA_VISIBLE_DEVICES'
|
|
parent_cvd = os.environ.get(vis_key, '')
|
|
if parent_cvd:
|
|
phys_ids = [x.strip() for x in parent_cvd.split(',')]
|
|
all_infos = [(nid, phys_ids[int(gid)] if gid.isdigit() and int(gid) < len(phys_ids) else gid)
|
|
for nid, gid in all_infos]
|
|
|
|
self.bundle_infos = all_infos
|
|
seen: set = set()
|
|
node_ips = []
|
|
for nid, _ in all_infos:
|
|
if nid not in seen:
|
|
seen.add(nid)
|
|
node_ips.append(node_id_to_ip.get(nid, ''))
|
|
self.node_ips = node_ips
|
|
logger.info('ResourcePool: %d PG(s), %d bundles, node_ips=%s', len(self.pgs), len(all_infos), self.node_ips)
|
|
|
|
def destroy(self):
|
|
if self.pgs:
|
|
import ray
|
|
for pg in self.pgs:
|
|
try:
|
|
ray.util.remove_placement_group(pg)
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
self.pgs = []
|
|
|
|
|
|
class ResourcePoolManager:
|
|
"""Manages multiple ResourcePools, deduplicating shared pools (colocate)."""
|
|
|
|
def __init__(self, pool_mapping: Dict[str, 'ResourcePool']):
|
|
self._pools = pool_mapping
|
|
|
|
def get_pool(self, group_name: str) -> 'ResourcePool':
|
|
return self._pools[group_name]
|
|
|
|
def create_all(self):
|
|
seen: set = set()
|
|
for pool in self._pools.values():
|
|
if id(pool) not in seen:
|
|
seen.add(id(pool))
|
|
pool.create()
|
|
|
|
def destroy_all(self):
|
|
seen: set = set()
|
|
for pool in self._pools.values():
|
|
if id(pool) not in seen:
|
|
seen.add(id(pool))
|
|
pool.destroy()
|