File size: 5,850 Bytes
e65937c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | """Single source of truth for distributed training topology.
Stores local_rank, SP/DP sizes and ranks, and process groups.
All other modules should import getters from here rather than
maintaining their own distributed state.
"""
from __future__ import annotations
import logging
import os
import torch
import torch.distributed as dist
from torch.distributed import ProcessGroup
from torch.distributed.fsdp import MixedPrecisionPolicy, fully_shard
logger = logging.getLogger("speculators")
# ---------------------------------------------------------------------------
# Module-level state
# ---------------------------------------------------------------------------
_local_rank: int = 0
_rank: int = 0
_world_size: int = 1
_is_distributed: bool = False
_sp_size: int = 1
_sp_rank: int = 0
_dp_size: int = 1
_dp_rank: int = 0
_sp_group: ProcessGroup | None = None
_dp_group: ProcessGroup | None = None
# ---------------------------------------------------------------------------
# Getters
# ---------------------------------------------------------------------------
def get_local_rank() -> int:
return _local_rank
def get_rank() -> int:
return _rank
def get_world_size() -> int:
return _world_size
def is_distributed() -> bool:
return _is_distributed
def get_sp_group() -> ProcessGroup | None:
return _sp_group
def get_dp_group() -> ProcessGroup | None:
return _dp_group
def get_sp_size() -> int:
return _sp_size
def get_sp_rank() -> int:
return _sp_rank
def get_dp_size() -> int:
return _dp_size
def get_dp_rank() -> int:
return _dp_rank
# ---------------------------------------------------------------------------
# Initialization
# ---------------------------------------------------------------------------
def _init_sp_process_groups(rank: int, world_size: int, sp_size: int) -> None:
"""Initialize sequence-parallel and data-parallel process groups.
SP groups use contiguous ranks (e.g. sp_size=2, world_size=4: {0,1}, {2,3}).
DP groups use strided ranks (e.g. sp_size=2, world_size=4: {0,2}, {1,3}).
"""
global _sp_group, _dp_group, _sp_size, _sp_rank, _dp_size, _dp_rank # noqa: PLW0603
if sp_size <= 0:
raise ValueError(f"sp_size must be positive, got {sp_size}")
if world_size % sp_size != 0:
raise ValueError(
f"world_size ({world_size}) must be divisible by sp_size ({sp_size})"
)
dp_size = world_size // sp_size
sp_group = None
for i in range(dp_size):
sp_ranks = list(range(i * sp_size, (i + 1) * sp_size))
pg = dist.new_group(sp_ranks)
if rank in sp_ranks:
sp_group = pg
dp_group = None
for i in range(sp_size):
dp_ranks = list(range(i, world_size, sp_size))
pg = dist.new_group(dp_ranks)
if rank in dp_ranks:
dp_group = pg
if sp_group is None or dp_group is None:
raise RuntimeError("Failed to initialize SP/DP process groups")
_sp_group = sp_group
_dp_group = dp_group
_sp_size = sp_size
_sp_rank = rank % sp_size
_dp_size = dp_size
_dp_rank = rank // sp_size
def maybe_setup_distributed(sp_size: int = 1) -> None:
"""Set up distributed training if launched with ``torchrun``.
Always populates the module-level topology state so that callers
can use the getter functions regardless of whether SP is enabled.
Process groups are always created when distributed — with
``sp_size == 1`` the DP group spans all ranks and each SP group
contains a single rank.
"""
global _local_rank, _rank, _is_distributed, _world_size # noqa: PLW0603
local_rank = int(os.environ.get("LOCAL_RANK", "0"))
distributed = "LOCAL_RANK" in os.environ
_local_rank = local_rank
_is_distributed = distributed
if not distributed:
return
torch.accelerator.set_device_index(local_rank)
acc = torch.accelerator.current_accelerator()
if acc is None:
raise ValueError("No accelerator found")
backend = torch.distributed.get_default_backend_for_device(acc)
dist.init_process_group(backend, device_id=local_rank)
_rank = dist.get_rank()
_world_size = dist.get_world_size()
_init_sp_process_groups(_rank, _world_size, sp_size)
logger.info(
f"Started distributed with local_rank={local_rank}, "
f"dp_size={_dp_size}, sp_size={_sp_size}",
extra={"override_rank0_filter": True},
)
def maybe_destroy_distributed() -> None:
"""Destroy the distributed process group if using distributed training."""
global _is_distributed, _local_rank, _rank, _world_size # noqa: PLW0603
global _sp_size, _sp_rank, _dp_size, _dp_rank # noqa: PLW0603
global _sp_group, _dp_group # noqa: PLW0603
if not _is_distributed:
return
dist.destroy_process_group()
logger.info(
"Destroyed distributed process group",
extra={"override_rank0_filter": True},
)
_is_distributed = False
_local_rank = 0
_rank = 0
_world_size = 1
_sp_size = 1
_sp_rank = 0
_dp_size = 1
_dp_rank = 0
_sp_group = None
_dp_group = None
def apply_fully_sharded(
model: torch.nn.Module, param_dtype: torch.dtype = torch.bfloat16
):
"""Applies torch FSDP fully_shard to the model, wrapping layers in FSDPModule.
Assumes the model has a `layers` attribute containing the decoder layers.
Model should be validated with SpeculatorModel.verify_training_compatible()
before calling this function.
"""
mp_policy = MixedPrecisionPolicy(
param_dtype=param_dtype,
reduce_dtype=torch.float32,
)
for layer in model.layers: # type: ignore[union-attr]
fully_shard(layer, mp_policy=mp_policy)
fully_shard(model, mp_policy=mp_policy)
return model
|