Files
wehub-resource-sync a203934033
Lint test / lint (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:34:58 +08:00

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()