Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
ec752ba
Run multi-rank CPU unit tests in CI via LOCAL_SIZE
delock Sep 1, 2026
f2b7d0b
Disable dist-env reuse for the full multi-rank CPU run
delock Sep 1, 2026
f2793e7
Split CPU unit tests into halves with per-half timeouts
delock Sep 2, 2026
4d4019d
Merge branch 'master' into ci/cpu-multi-rank-local-size
delock Sep 15, 2026
89ca163
Isolate shm-allreduce segments per test pool and clean them up
delock Sep 16, 2026
453f60f
Pre-download HF fixtures and share one cache across all test phases
delock Sep 16, 2026
6799643
Download HF fixtures with the hf CLI
delock Sep 16, 2026
2654cac
Disable Xet transfers for the anonymous HF fixture download
delock Sep 16, 2026
d429f62
Merge remote-tracking branch 'origin/master' into pr-8381-new
delock Sep 17, 2026
132298c
Merge remote-tracking branch 'origin/master' into pr-8381-new
delock Sep 17, 2026
dfb0c50
Scope the bf16 version floors to NCCL transports in the test check
delock Sep 17, 2026
0194033
Merge remote-tracking branch 'origin/master' into pr-8381-new
delock Sep 18, 2026
3fba8dc
Stop pinning DDP device_ids in the autocast baseline on CPU
delock Sep 18, 2026
3b74fba
Merge remote-tracking branch 'origin/master' into pr-8381-new
delock Sep 18, 2026
33e7dcc
Skip the dynamic offload-state tests on the cpu accelerator
delock Sep 18, 2026
940d404
Merge remote-tracking branch 'origin/master' into ci/cpu-multi-rank-l…
delock Sep 21, 2026
61abf25
Fix pipe tests failures on CPU
jinyouzhi Sep 20, 2026
68fdd74
Skip fp16 ZeroPP tests on accelerators without fp16 support
jinyouzhi Sep 20, 2026
2adfc0d
Skip ZERO++ Quantized in CPU
jinyouzhi Sep 20, 2026
08c966f
Merge remote-tracking branch 'origin/master' into pr-8381-new
delock Sep 22, 2026
09e3cba
Skip fp16 universal-checkpoint fixtures without fp16 support
delock Sep 22, 2026
0cb5cdc
Merge remote-tracking branch 'origin/master' into pr-8381-new
delock Sep 24, 2026
2b23590
Skip fp16 coalesce tests on accelerators without fp16 support
delock Sep 24, 2026
e4ec54f
Loosen the late-iteration gradient tolerance in the checkpointing test
delock Sep 24, 2026
2bdbbdf
Allow fp16 ulp noise in autocast loss parity below ZeRO-3
delock Sep 24, 2026
8fa6e22
Pre-build the shm comm op so rank workers never race the JIT lock
delock Sep 24, 2026
18ee3ec
Skip fp16 no_sync tests on accelerators without fp16 support
delock Sep 25, 2026
ecc198e
Allow fp16 ulp noise in autocast loss parity at ZeRO-3
delock Sep 26, 2026
5770829
Move the CPU workflow to torch 2.14.0 for this branch
delock Sep 26, 2026
a84e1b7
Run only the multi-rank tests in this workflow
delock Sep 26, 2026
5914ddc
Fix Ulysses SP registration check and seqlen exchange on gloo
delock Sep 27, 2026
86eeb05
Adapt the ulysses_sp_hf tests for non-CUDA accelerators
delock Sep 27, 2026
c81a577
Raise the non-v1 half timeout to 180m
delock Sep 27, 2026
a0680d6
De-hardcode the disable-in-eval test device
delock Sep 27, 2026
1e6872a
Skip fp16 MoE checkpoint tests on accelerators without fp16 support
delock Sep 27, 2026
e6c3d95
Skip the remaining fp16-config tests without fp16 support
delock Sep 27, 2026
30a96e4
Raise the non-v1 half timeout to 210m
delock Sep 28, 2026
73425cb
Dump the stack of a wedged rank instead of blocking its pool forever
delock Sep 28, 2026
766f04b
Guard the shm inference all-reduce on the host ISA
delock Sep 29, 2026
2dcf94d
Skip the CPUAdam GPU-error test on the cpu accelerator
delock Sep 29, 2026
e3d6f5c
Merge remote-tracking branch 'origin/master' into ci/cpu-multi-rank-l…
delock Sep 29, 2026
ead6cce
Merge remote-tracking branch 'origin/master' into pr-8381-new
delock Oct 3, 2026
123e65e
Assert state removal only when offload leaves the accelerator
delock Oct 3, 2026
23c4e93
Merge remote-tracking branch 'origin/master' into ci/cpu-multi-rank-l…
delock Oct 10, 2026
271b774
Merge remote-tracking branch 'origin/master' into ci/cpu-multi-rank-l…
delock Oct 16, 2026
61a2dbd
Merge remote-tracking branch 'origin/master' into ci/cpu-multi-rank-l…
delock Oct 21, 2026
bd89097
Drop a CPUAdam GPU-error skip already provided by #8836
delock Oct 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 49 additions & 3 deletions .github/workflows/cpu-torch-latest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,20 @@ jobs:
runs-on: ubuntu-24.04

env:
DEFAULT_TORCH_PRESET: '2.10.0-cpu'
# The runner is single-socket, so CPU_Accelerator.device_count() reports 1
# NUMA node and the per-device gate in tests/unit/common.py skips every
# multi-rank test. CPU ranks are plain processes over gloo, so advertise 4
# local devices to let world_size<=4 tests run. The test harness re-sets
# LOCAL_SIZE per worker, so this value only affects the launch gate.
LOCAL_SIZE: '4'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep CUDA-only distributed tests out of the CPU run

In the cpu-torch-latest job, advertising four devices admits every distributed test with world_size <= 4, not only CPU-safe tests. For example, tests/unit/ulysses_alst/test_ulysses_sp_hf.py:240-263 defines an unguarded two-rank test that creates tensors on cuda:<rank>; because CPU_Accelerator.is_available() returns true, the harness does not skip it, and the all-unit pytest invocation at workflow line 283 will fail on the CPU-only PyTorch installation. Scope this override to an explicitly CPU-compatible subset or add CPU capability checks before enabling the previously skipped tests.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add the required sign-off trailer

This is a non-merge commit, but its commit message has no Signed-off-by trailer. Add the author sign-off so the commit satisfies the repository's commit and CI requirements.

AGENTS.md reference: AGENTS.md:L8-L8

Useful? React with 👍 / 👎.

# Multi-rank tests churn mp pools for hours; reused pools eventually hang in
# cleanup and stall workers until the 6h job limit. Fresh pools per test are
# slower but let the suite finish (knob documented in tests/unit/common.py).
DS_DISABLE_REUSE_DIST_ENV: '1'
# Only the multi-rank tests this LOCAL_SIZE gate admits run here; single-rank
# tests are deselected because the plain cpu-torch-latest run guards them.
DS_MULTIRANK_ONLY: '1'
DEFAULT_TORCH_PRESET: '2.14.0-cpu'
DEFAULT_TRANSFORMERS_SOURCE: 'git'
# Manual PyPI fallback only; scheduled and default manual runs use Git.
DEFAULT_TRANSFORMERS_VERSION: '4.51.3'
Expand Down Expand Up @@ -137,6 +150,11 @@ jobs:
torchvision_install_version='0.25.0'
torch_test_version='2.10'
;;
'2.14.0-cpu')
torch_install_version='2.14.0'
torchvision_install_version='0.29.0'
torch_test_version='2.14'
;;
*)
echo "Unsupported torch_preset: $selected_preset" >&2
exit 1
Expand Down Expand Up @@ -249,9 +267,37 @@ jobs:
run: |
pip list

- name: Pre-build JIT ops
# Rank workers JIT-load ops concurrently on first use; torch's build
# lock has no stale handling, so a leftover baton from a killed build
# wedges every later load. Build the hot ops once up front so tests
# only hit the disk cache.
run: |
python -c "
from deepspeed.comm.torch import build_shm_op
build_shm_op()
import torch
from deepspeed.ops.adam import DeepSpeedCPUAdam
DeepSpeedCPUAdam([torch.nn.Parameter(torch.randn(8))])
"

- name: Pre-download HF test fixtures
# Concurrent xdist workers downloading the same model race on the
# transformers cache lock and fail with PermissionError; populate the
# shared cache once so tests only hit it read-only.
run: |
HF_HUB_DISABLE_XET=1 HF_HOME=/tmp/hf_home hf download bert-base-uncased

- name: Unit tests
run: |
unset TORCH_CUDA_ARCH_LIST # only jit compile for current arch
cd tests
HF_HOME=/tmp/hf_home/ pytest $PYTEST_OPTS --forked -n 4 unit/ --torch_ver="$TORCH_TEST_VERSION"
HF_HOME=/tmp/hf_home/ pytest $PYTEST_OPTS --forked -m 'sequential' unit/ --torch_ver="$TORCH_TEST_VERSION"
# The suite is split so each half gets fresh xdist workers: multi-rank pool
# teardown eventually wedges a worker, and one 3400-test process never
# reaches its summary inside the 6h job limit. maxfail is raised so every
# failure is listed, and timeout caps each half.
overall=0
HF_HOME=/tmp/hf_home timeout 210m pytest $PYTEST_OPTS --maxfail=100000 --forked -n 4 unit/ --ignore=unit/v1 --torch_ver="$TORCH_TEST_VERSION" || overall=$?
HF_HOME=/tmp/hf_home timeout 150m pytest $PYTEST_OPTS --maxfail=100000 --forked -n 4 unit/v1 --torch_ver="$TORCH_TEST_VERSION" || overall=$?
HF_HOME=/tmp/hf_home/ pytest $PYTEST_OPTS --forked -m 'sequential' unit/ --torch_ver="$TORCH_TEST_VERSION" || overall=$?
exit $overall
53 changes: 53 additions & 0 deletions tests/unit/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

# DeepSpeed Team

import faulthandler
import itertools
import os
import re
import time
Expand Down Expand Up @@ -44,12 +46,21 @@ def get_xdist_worker_id():
return None


_master_port_counter = itertools.count()


def get_master_port(base_port=29500, port_range_size=1000):
xdist_worker_id = get_xdist_worker_id()
if xdist_worker_id is not None:
# Make xdist workers use different port ranges to avoid race conditions
base_port += port_range_size * xdist_worker_id

# The bind-and-release probe below hands every pool the same first-free port
# when each test runs in a fresh process (--forked), but this port also keys
# the shm allreduce segments, so pools must not share it. Offset the probe
# start per process and per pool.
base_port += (os.getpid() + next(_master_port_counter)) % (port_range_size - 100)

# Select first open port in range
port = base_port
max_port = base_port + port_range_size
Expand All @@ -64,6 +75,25 @@ def get_master_port(base_port=29500, port_range_size=1000):
raise IOError('no free ports')


def _warm_shm_comm_op():
# Rank workers JIT-load the shm comm op on first init_distributed; concurrent
# loads serialize on torch's FileBaton, which has no stale-lock handling, so a
# baton left by a killed process deadlocks every later load (the CI worker
# wedges). Drop a stale baton before it blocks anything, then build once here
# so workers only hit the disk cache.
try:
from torch.utils.cpp_extension import _get_build_directory
build_dir = Path(_get_build_directory("deepspeed_shm_comm", verbose=False))
lock = build_dir / "lock"
if (build_dir /
"deepspeed_shm_comm.so").exists() and lock.exists() and time.time() - lock.stat().st_mtime > 600:
lock.unlink(missing_ok=True)
from deepspeed.comm.torch import build_shm_op
build_shm_op()
except Exception:
pass


def _get_cpu_socket_count():
import shlex
p1 = subprocess.Popen(shlex.split("cat /proc/cpuinfo"), stdout=subprocess.PIPE)
Expand Down Expand Up @@ -182,6 +212,7 @@ def _get_fixture_kwargs(self, request, func):
def _launch_daemonic_procs(self, num_procs, init_method):
# Create process pool or use cached one
master_port = None
_warm_shm_comm_op()

if get_accelerator().device_name() == 'hpu':
if self.reuse_dist_env:
Expand All @@ -198,6 +229,7 @@ def _launch_daemonic_procs(self, num_procs, init_method):
master_port = get_master_port()

# Run the test
self._master_port = master_port
args = [(local_rank, num_procs, master_port, init_method) for local_rank in range(num_procs)]
skip_msgs_async = pool.starmap_async(self._dist_run, args)

Expand All @@ -222,6 +254,7 @@ def _launch_non_daemonic_procs(self, num_procs, init_method):
assert not self.reuse_dist_env, "Cannot reuse distributed environment with non-daemonic processes"

master_port = get_master_port()
self._master_port = master_port
skip_msg = mp.Queue() # Allows forked processes to share pytest.skip reason
processes = []
prev_start_method = mp.get_start_method()
Expand Down Expand Up @@ -252,6 +285,7 @@ def _launch_non_daemonic_procs(self, num_procs, init_method):
# Wait for all other processes to complete
for p in processes:
p.join(self.exec_timeout)
self._remove_shm_segments(num_procs)

failed = [(rank, p) for rank, p in enumerate(processes) if p.exitcode != 0]
for rank, p in failed:
Expand Down Expand Up @@ -296,6 +330,9 @@ def _launch_procs(self, num_procs, init_method):
self._launch_daemonic_procs(num_procs, init_method)

def _dist_run(self, local_rank, num_procs, master_port, init_method, skip_msg=""):
# A wedged rank would otherwise block its pool worker forever and take
# the whole xdist session down with it; dump the stack and exit instead.
faulthandler.dump_traceback_later(self.exec_timeout, exit=True)
if dist.is_initialized():
if get_accelerator().is_available():
# local_rank might not match the rank in the previous run if you are reusing the environment
Expand Down Expand Up @@ -363,6 +400,22 @@ def _close_pool(self, pool, num_procs, force=False):
pool.starmap(self._dist_destroy, [() for _ in range(num_procs)])
pool.close()
pool.join()
self._remove_shm_segments(num_procs)

def _remove_shm_segments(self, num_procs):
# The shm-based allreduce (active when LOCAL_SIZE matches the pool size)
# leaves one ~66MB segment per rank under /dev/shm keyed by the master
# port. The op never unlinks them, which exhausts /dev/shm over a full
# run, so remove them once the pool is gone.
master_port = getattr(self, '_master_port', None)
if master_port is None or not hasattr(os, 'getuid'):
return
for rank in range(num_procs):
seg = f"/dev/shm/deepspeed_allreduce_buffer_{os.getuid()}_127.0.0.1_{master_port}_{rank}"
try:
os.remove(seg)
except OSError:
pass


class DistributedFixture(DistributedExec):
Expand Down
31 changes: 26 additions & 5 deletions tests/unit/v1/zero/test_zero_autocast.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,18 @@ def forward(self, x, y):
return self.cross_entropy_loss(x, y)


def step_amp(enabled, baseline_model, baseline_optimizer, target_engine, dtype, enable_autocast_outside,
baseline_scaler, step, x, y, expect_match):
def step_amp(enabled,
baseline_model,
baseline_optimizer,
target_engine,
dtype,
enable_autocast_outside,
baseline_scaler,
step,
x,
y,
expect_match,
zero_stage=None):
device_type = get_accelerator().device_name()

# Runs the forward pass with autocasting.
Expand All @@ -57,7 +67,16 @@ def step_amp(enabled, baseline_model, baseline_optimizer, target_engine, dtype,

# reduce-scatter in `dtype` makes a difference in the loss.
if step <= 1 and expect_match:
allclose_on_all_ranks(baseline_loss, target_loss)
# The engine reduces gradients in the autocast dtype while the DDP baseline
# reduces in fp32; gloo round-trips fp16 through fp32 and adds ulp noise,
# so allow a low-precision relative tolerance below ZeRO-3.
if dtype == torch.float16 and zero_stage is not None and zero_stage < 3:
allclose_on_all_ranks(baseline_loss, target_loss, rtol=2e-3, atol=2e-2)
elif dtype == torch.float16 and zero_stage == 3:
# ZeRO-3 also rounds partitioned params through the autocast dtype, adding percent-level noise.
allclose_on_all_ranks(baseline_loss, target_loss, rtol=5e-2, atol=5e-2)
else:
allclose_on_all_ranks(baseline_loss, target_loss)

target_engine.backward(target_loss)
target_engine.step()
Expand Down Expand Up @@ -96,7 +115,9 @@ def compare_loss(model_cls,

i = get_accelerator().current_device()
device = get_accelerator().current_device_name()
baseline_model = DDP(deepcopy(model).to(device=device, dtype=torch.float32), device_ids=[i], output_device=i)
# Only indexed devices take device_ids/output_device; CPU modules live on one shared device.
ddp_kwargs = {'device_ids': [i], 'output_device': i} if torch.device(device).type != 'cpu' else {}
baseline_model = DDP(deepcopy(model).to(device=device, dtype=torch.float32), **ddp_kwargs)
baseline_optimizer = torch.optim.AdamW(baseline_model.parameters(), lr=lr, weight_decay=0.0)
baseline_scaler = torch.amp.GradScaler()

Expand Down Expand Up @@ -128,7 +149,7 @@ def compare_loss(model_cls,

for i, (x, y) in enumerate(zip(xs, ys)):
step_amp(enable, baseline_model, baseline_optimizer, target_engine, dtype, enable_autocast_outside,
baseline_scaler, i, x, y, expect_match)
baseline_scaler, i, x, y, expect_match, zero_stage)

for module in target_engine.modules():
for p in module.parameters(recurse=False):
Expand Down
Loading