Barbados / bundle /source /eval /distributed_utils.py
Vykos's picture
Release Barbados looped MoE research prototype
d5147ba verified
Raw History Blame Contribute Delete
5.72 kB
"""Distributed helpers for multi-GPU evaluation."""
from __future__ import annotations
import os
import tempfile
import time
from pathlib import Path
from typing import TypeVar
import torch.distributed as dist
from utils.device import get_dist_backend, set_device
T = TypeVar("T")
def init_eval_distributed() -> tuple[int, int, bool]:
"""Initialize HCCL if launched via torchrun. Returns (rank, world_size, is_main)."""
if "LOCAL_RANK" not in os.environ:
return 0, 1, True
if not dist.is_initialized():
dist.init_process_group(backend=get_dist_backend())
rank = dist.get_rank()
world_size = dist.get_world_size()
set_device(int(os.environ["LOCAL_RANK"]))
return rank, world_size, rank == 0
def prep_done_flag() -> Path:
"""Shared-filesystem flag for multi-node; /tmp is only OK for single-node."""
port = os.environ.get("MASTER_PORT", "0")
base = os.environ.get("EVAL_PREP_FLAG_DIR", "").strip()
if base:
path = Path(base)
path.mkdir(parents=True, exist_ok=True)
return path / f"loom_eval_{port}_prep.done"
return Path(tempfile.gettempdir()) / f"loom_eval_{port}_prep.done"
def clear_prep_done_flag() -> None:
flag = prep_done_flag()
if flag.exists():
flag.unlink()
def signal_prep_done() -> None:
prep_done_flag().touch()
def wait_for_prep_done(rank: int, world_size: int) -> None:
"""Let rank 0 load datasets without holding HCCL collectives open."""
if world_size <= 1 or rank == 0:
return
flag = prep_done_flag()
while not flag.exists():
time.sleep(0.5)
def shard_by_rank(items: list[T], rank: int, world_size: int) -> list[tuple[int, T]]:
return [(i, item) for i, item in enumerate(items) if i % world_size == rank]
def shard_by_rank_balanced(
items: list[T],
rank: int,
world_size: int,
weight_fn=None,
) -> list[tuple[int, T]]:
"""Greedy load-balanced sharding by estimated workload (default: prompt char length)."""
if world_size <= 1:
return list(enumerate(items))
weights = [float(weight_fn(item)) if weight_fn is not None else 1.0 for item in items]
order = sorted(range(len(items)), key=lambda i: weights[i], reverse=True)
rank_loads = [0.0] * world_size
rank_indices: list[list[int]] = [[] for _ in range(world_size)]
for idx in order:
target = min(range(world_size), key=lambda r: rank_loads[r])
rank_indices[target].append(idx)
rank_loads[target] += weights[idx]
return [(idx, items[idx]) for idx in rank_indices[rank]]
def gather_shard_balance_stats(
prompts: list[str],
rank: int,
world_size: int,
*,
char_weights: list[float] | None = None,
token_weights: list[float] | None = None,
) -> list[dict[str, float | int]]:
"""Per-rank sample count and estimated workload for modulo vs balanced sharding."""
if char_weights is None:
char_weights = [float(len(p)) for p in prompts]
if token_weights is None:
token_weights = char_weights
def _balanced_indices(weights: list[float]) -> list[list[int]]:
order = sorted(range(len(prompts)), key=lambda i: weights[i], reverse=True)
rank_loads = [0.0] * world_size
rank_indices: list[list[int]] = [[] for _ in range(world_size)]
for idx in order:
target = min(range(world_size), key=lambda r: rank_loads[r])
rank_indices[target].append(idx)
rank_loads[target] += weights[idx]
return rank_indices
def _stats(indices: list[int], weights: list[float]) -> dict[str, float | int]:
ws = [weights[i] for i in indices]
return {
"count": len(indices),
"weight_sum": int(sum(ws)),
"weight_max": int(max(ws) if ws else 0),
"weight_avg": round(sum(ws) / len(ws), 1) if ws else 0.0,
}
modulo_indices = [i for i in range(len(prompts)) if i % world_size == rank]
char_indices = _balanced_indices(char_weights)
token_indices = _balanced_indices(token_weights)
local = {
"modulo": _stats(modulo_indices, char_weights),
"balanced_chars": _stats(char_indices[rank], char_weights),
"balanced_tokens": _stats(token_indices[rank], token_weights),
# Legacy alias for callers expecting "balanced"
"balanced": _stats(token_indices[rank], token_weights),
}
if not dist.is_initialized() or world_size == 1:
return [local]
gathered: list[dict | None] = [None] * world_size
dist.all_gather_object(gathered, local)
return gathered # type: ignore[return-value]
def gather_indexed_results(local_results: list[tuple[int, str]], total_size: int) -> list[str]:
"""All-gather (idx, text) pairs from every rank and rebuild the full ordered list."""
if not dist.is_initialized() or dist.get_world_size() == 1:
outputs = [""] * total_size
for idx, text in local_results:
outputs[idx] = text
return outputs
gathered: list[list[tuple[int, str]] | None] = [None] * dist.get_world_size()
dist.all_gather_object(gathered, local_results)
outputs = [""] * total_size
for rank_items in gathered:
for idx, text in rank_items:
outputs[idx] = text
return outputs
def broadcast_from_main(obj: T | None, rank: int) -> T:
if not dist.is_initialized() or dist.get_world_size() == 1:
return obj # type: ignore[return-value]
container: list[T | None] = [obj] if rank == 0 else [None]
dist.broadcast_object_list(container, src=0)
return container[0] # type: ignore[return-value]
def barrier() -> None:
if dist.is_initialized():
dist.barrier()