Files
2026-07-13 13:18:33 +08:00

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