191 lines
8.2 KiB
Python
191 lines
8.2 KiB
Python
# Copyright (c) DeepSpeed Team.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
# DeepSpeed Team
|
|
|
|
import os
|
|
import math
|
|
import torch
|
|
import psutil
|
|
from deepspeed import comm as dist
|
|
from deepspeed.accelerator import get_accelerator
|
|
|
|
# How long the training side blocks on a single semaphore wait for the optimizer process before
|
|
# waking up to check that the process is still alive. A normal step completes far sooner; this
|
|
# only bounds how long we hang if the optimizer process dies mid-step.
|
|
ZENFLOW_OPTIMIZER_WAIT_POLL_SECONDS = 60
|
|
|
|
|
|
def _flatten_dense_tensors(tensors):
|
|
"""Flatten dense tensors into a contiguous 1D buffer. Assume tensors are of
|
|
same dense type.
|
|
|
|
Since inputs are dense, the resulting tensor will be a concatenated 1D
|
|
buffer. Element-wise operation on this buffer will be equivalent to
|
|
operating individually.
|
|
|
|
Args:
|
|
tensors (Iterable[Tensor]): dense tensors to flatten.
|
|
|
|
Returns:
|
|
A contiguous 1D buffer containing input tensors.
|
|
"""
|
|
transposed_tensors = [t.transpose(0, 1).contiguous() if t.dim() == 2 else t for t in tensors]
|
|
return torch._C._nn.flatten_dense_tensors(transposed_tensors)
|
|
|
|
|
|
def _unflatten_dense_tensors(flat, tensors):
|
|
"""View a flat buffer using the sizes of tensors. Assume that tensors are of
|
|
same dense type, and that flat is given by _flatten_dense_tensors.
|
|
|
|
Args:
|
|
flat (Tensor): flattened dense tensors to unflatten.
|
|
tensors (Iterable[Tensor]): dense tensors whose sizes will be used to
|
|
unflatten flat.
|
|
|
|
Returns:
|
|
Unflattened dense tensors with sizes same as tensors and values from
|
|
flat.
|
|
"""
|
|
transposed_tensors = [t.transpose(0, 1) if t.dim() == 2 else t for t in tensors]
|
|
unflat = torch._C._nn.unflatten_dense_tensors(flat, transposed_tensors)
|
|
return [t.transpose(0, 1) if t.dim() == 2 else t for t in unflat]
|
|
|
|
|
|
def disable_accelerator():
|
|
accelerator = get_accelerator()
|
|
accelerator.is_available = lambda: False
|
|
accelerator.device_count = lambda: 0
|
|
accelerator.current_device = lambda: -1
|
|
# Optionally mark it as initialized if needed
|
|
if hasattr(accelerator, "_initialized"):
|
|
accelerator._initialized = True
|
|
|
|
|
|
def all_tensors_equal(tensor_list):
|
|
first_tensor = tensor_list[0]
|
|
for tensor in tensor_list[1:]:
|
|
if not torch.equal(first_tensor, tensor):
|
|
return False
|
|
return True
|
|
|
|
|
|
def _split_affinity(cores, pt_reserved_cores_perc):
|
|
"""Split a rank's core list into (zf_affinity, pt_affinity): reserve the first
|
|
ceil(pt_reserved_cores_perc * n) cores for the training thread and give the rest to the
|
|
optimizer. If the reserve rounds to zero or to every core, both share the full set (there
|
|
is nothing to gain from isolating an empty side)."""
|
|
pt_num_cores = math.ceil(pt_reserved_cores_perc * len(cores))
|
|
if 0 < pt_num_cores < len(cores):
|
|
return cores[pt_num_cores:], cores[:pt_num_cores]
|
|
return cores, cores
|
|
|
|
|
|
def _compute_zf_pt_affinity(zf_optimizer):
|
|
"""Split this rank's cores into a ZenFlow-optimizer set and a training (PyTorch) set.
|
|
When every rank reports the same affinity the launcher did not bind workers, so do a
|
|
soft per-rank bind first, then carve off pt_reserved_cores_perc for training."""
|
|
curr_rank = dist.get_rank()
|
|
total_rank = dist.get_world_size()
|
|
|
|
current_affinity = psutil.Process().cpu_affinity()
|
|
all_affinities = [
|
|
torch.zeros(len(current_affinity),
|
|
dtype=type(current_affinity[0]),
|
|
device=get_accelerator().current_device_name()) for _ in range(total_rank)
|
|
]
|
|
dist.all_gather(
|
|
all_affinities,
|
|
torch.tensor(current_affinity, dtype=type(current_affinity[0]),
|
|
device=get_accelerator().current_device_name()))
|
|
if all_tensors_equal(all_affinities):
|
|
num_phy_cores = psutil.cpu_count(logical=False)
|
|
available_phy_cores = [i for i in current_affinity if i < num_phy_cores]
|
|
cores_per_rank = len(available_phy_cores) // total_rank
|
|
current_affinity = available_phy_cores[curr_rank * cores_per_rank:(curr_rank + 1) * cores_per_rank]
|
|
|
|
return _split_affinity(current_affinity, zf_optimizer.pt_reserved_cores_perc)
|
|
|
|
|
|
def zenflow_optimizer_process(groups, ctrl, ready, zf_affinity, adamw_mode):
|
|
"""ZenFlow overlapped optimizer process (ZeRO stage 1/2/3). Builds the native ZenFlowAdam
|
|
pinned pool and runs the worker loop driven by the shared-memory control block (no pickling
|
|
pipe). The Adam state is allocated here, in this process pinned to the optimizer cores, so
|
|
it is NUMA-local to the pool -- which is what makes a separate process worthwhile over an
|
|
in-process thread for large, memory-bandwidth-bound updates."""
|
|
disable_accelerator()
|
|
current_process = psutil.Process()
|
|
current_process.cpu_affinity(zf_affinity)
|
|
os.environ['OMP_NUM_THREADS'] = str(len(zf_affinity))
|
|
|
|
from deepspeed.ops.op_builder import CPUAdamBuilder
|
|
op = CPUAdamBuilder().load()
|
|
op.create_adam(0, 1e-3, 0.9, 0.999, 1e-8, 0.0, adamw_mode, False)
|
|
handle = op.zenflow_adam_create(0, list(zf_affinity))
|
|
for param, overlap_grad0, overlap_grad1, stale in groups:
|
|
exp_avg0 = torch.zeros_like(param)
|
|
exp_avg1 = torch.zeros_like(param)
|
|
exp_avg_sq0 = torch.zeros_like(param)
|
|
exp_avg_sq1 = torch.zeros_like(param)
|
|
op.zenflow_adam_register_group(handle, param, overlap_grad0, overlap_grad1, exp_avg0, exp_avg1, exp_avg_sq0,
|
|
exp_avg_sq1, stale)
|
|
ready.set()
|
|
op.zenflow_adam_run_worker(handle, ctrl.data_ptr())
|
|
op.zenflow_adam_destroy(handle)
|
|
op.destroy_adam(0)
|
|
|
|
|
|
def start_optimizer_process(zf_optimizer):
|
|
"""Start ZenFlow's overlapped optimizer (ZeRO stage 1/2/3) in a dedicated process,
|
|
coordinated through a shared-memory semaphore control block. A separate process keeps the
|
|
Adam state NUMA-local to the optimizer cores and free of contention with the training
|
|
thread, while the native control block avoids per-step Python/IPC overhead."""
|
|
from multiprocessing import get_context
|
|
from deepspeed.ops.op_builder import CPUAdamBuilder
|
|
|
|
op = CPUAdamBuilder().load()
|
|
zf_optimizer.zf_op = op
|
|
|
|
# Stage 3 steps each flattened sub-group partition; stage 1/2 steps one flat partition per
|
|
# param group. Both carry overlap_grad double buffers and a stale snapshot.
|
|
if zf_optimizer.zf_stage3:
|
|
params = list(zf_optimizer.fp32_partitioned_groups_flat)
|
|
else:
|
|
params = [group["params"][0] for group in zf_optimizer.optimizer.param_groups]
|
|
|
|
# Share the tensors the optimizer process reads/writes; the Adam state stays process-local.
|
|
groups = []
|
|
for param in params:
|
|
param.data.share_memory_()
|
|
if not hasattr(param, "stale_param"):
|
|
param.stale_param = torch.zeros_like(param.data, dtype=param.dtype, device=param.device)
|
|
param.stale_param.data.share_memory_()
|
|
param.overlap_grad[0].data.share_memory_()
|
|
param.overlap_grad[1].data.share_memory_()
|
|
groups.append((param.data, param.overlap_grad[0].data, param.overlap_grad[1].data, param.stale_param.data))
|
|
|
|
ctrl = torch.zeros(op.zenflow_adam_ctrl_size(), dtype=torch.uint8).share_memory_()
|
|
op.zenflow_adam_ctrl_init(ctrl.data_ptr(), len(groups))
|
|
zf_optimizer.zf_ctrl = ctrl
|
|
|
|
zf_affinity, pt_affinity = _compute_zf_pt_affinity(zf_optimizer)
|
|
|
|
ctx = get_context("spawn")
|
|
ready = ctx.Event()
|
|
proc = ctx.Process(target=zenflow_optimizer_process, args=(groups, ctrl, ready, zf_affinity, True))
|
|
proc.daemon = True
|
|
proc.start()
|
|
# Wait for the optimizer process to finish building its pool and registering tensors.
|
|
# If it crashed during init (e.g. it never signals), fail loudly instead of blocking the
|
|
# training process forever on the first step's wait.
|
|
if not ready.wait(timeout=600):
|
|
proc.terminate()
|
|
raise RuntimeError("ZenFlow optimizer process failed to become ready (it likely crashed "
|
|
"during initialization; check the optimizer process traceback above)")
|
|
zf_optimizer.process = proc
|
|
|
|
psutil.Process().cpu_affinity(pt_affinity)
|
|
os.environ['OMP_NUM_THREADS'] = str(len(pt_affinity))
|
|
|
|
zf_optimizer.process_optimizer_established = True
|