Skip to content

Enable DeepSpeed support on Apple Silicon (MPS) with ZeRO Stage 1-3 - #8293

Merged
delock merged 6 commits into
masterfrom
mps-phase0
Aug 23, 2026
Merged

delock merged 6 commits into
masterfrom
mps-phase0

Conversation

@PKUWZP

@PKUWZP PKUWZP commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR is the first step (phase 0) of enabling Apple Silicon support for DeepSpeed: make single-device training work end to end with pure-PyTorch ops.

[To-Do in phase 1] Metal kernels will come later and plug into the op_builder/mps classes added here.

The MPS accelerator was a stub: memory queries returned None, no communication backend was set, fp16/bf16 were reported unsupported, and every op builder resolved to NotImplementedBuilder. deepspeed.initialize + one training step failed for every ZeRO stage on an Apple Silicon machine. This PR aims on enabling capabilities.

Changes

  • accelerator/mps_accelerator.py — real torch.mps memory stats, fp16/bf16 support (bf16 gated on macOS 14+), torch.mps.Event, gloo as the comm backend, and is_synchronized_device() = True (PyTorch's MPS backend effectively exposes a single in-order execution stream and currently provides no public CUDA-style stream API or record_stream mechanism.). Unified memory makes pin_memory a no-op (torch's pin_memory() also raises under MPS).
  • deepspeed/comm/torch.py — gloo cannot operate on MPS tensors (even at world size 1), so collectives stage MPS tensors through CPU copies via a stage_on_cpu decorator; async ops copy back on wait().
  • accelerator/abstract_accelerator.py + runtime/zero — MPS has no fp64. Gradient-norm accumulation now picks its dtype via a new concrete is_fp64_supported() (default True) and get_norm_dtype() instead of hard-coded .double().
  • op_builder/mps/ — new backend package (MPSOpBuilder, NotImplementedBuilder, FusedAdamBuilder). FusedAdam is implemented with torch._foreach_* ops and mirrors the math in csrc/adam/multi_tensor_adam.cu, following the HPU precedent of Python-backed builders.
  • tests/unit/common.py — MPS must use spawn (Metal's compiler service is lost in forkserver children, which hangs the harness) and reports its device count via the accelerator.
  • tests/unit/ops/adam/test_adamw.pytest_fused_adam_matches_torch checks FusedAdam against torch.optim.Adam/AdamW on the active accelerator (fp32/bf16 × Adam/AdamW), so it also guards the CUDA kernel.

Verified on an M5 Max (macOS 26.3, torch 2.13.0)

  • ZeRO 1/2/3 × fp32/bf16/fp16 train end to end with deepspeed.initialize (single process). Also tested on ZeRO stage 0 which disables ZeRO completely and falling back to standard data parallelism.
  • MPS FusedAdam matches torch.optim to 2e-7 in fp32.
  • DS_ACCELERATOR=mps pytest unit/runtime/test_ds_config_dict.py unit/runtime/test_ds_initialize.py unit/runtime/half_precision/test_fp16.py unit/runtime/half_precision/test_dynamic_loss_scale.py unit/runtime/zero/test_zero_grad_clip.py unit/runtime/zero/test_zero_context.py unit/checkpoint/test_zero_optimizer.py: 132 passed, 0 failed, 126 skipped (multi-device tests; device_count() == 1).

Known limitations / follow-ups

  • bf16 FusedAdam differs from the CUDA kernel by ~1 bf16 ulp (CUDA computes in fp32 and stores bf16; the _foreach path rounds in bf16).
  • The CPU-staged gloo path is only exercised at world size 1 here; multi-Mac runs are untested.
  • Follow-ups: macOS arm64 CI workflow, arm64 build of CPU Adam for ZeRO-Offload, Metal kernels via torch.mps.compile_shader, and an Apple Silicon tutorial page.

The MPS accelerator was a stub: memory queries returned None, no
communication backend was set, fp16/bf16 were reported unsupported,
and every op builder resolved to NotImplementedBuilder. Training with
any ZeRO stage failed on an Apple Silicon machine.

- accelerator/mps_accelerator.py: report real torch.mps memory stats,
  fp16/bf16 support, torch.mps.Event, gloo as the comm backend, and
  treat MPS as a synchronized device (single in-order command queue,
  no public streams). Unified memory makes pin_memory a no-op.
- deepspeed/comm/torch.py: gloo cannot operate on MPS tensors, so
  collectives stage MPS tensors through CPU copies (stage_on_cpu).
- accelerator/abstract_accelerator.py + runtime/zero: MPS has no fp64,
  so gradient-norm accumulation picks its dtype via the new
  is_fp64_supported()/get_norm_dtype() instead of hard-coded double().
- op_builder/mps: new backend package with a torch._foreach based
  FusedAdam that mirrors the CUDA multi_tensor_adam math; Metal
  kernels will later plug into the same builder classes.
- tests/unit/common.py: MPS must use spawn (Metal's compiler service
  is lost in forked children) and reports its own device count.
- tests/unit/ops/adam/test_adamw.py: check FusedAdam against
  torch.optim.Adam/AdamW on the active accelerator.

Signed-off-by: PKUWZP <zhipeng.rainbowserie@gmail.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1865963518

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread op_builder/mps/fused_adam.py Outdated
Comment thread deepspeed/comm/torch.py Outdated
Comment thread accelerator/mps_accelerator.py
setup.py imports every op builder even when torch is not installed,
so the torch.no_grad decorator must not run at class-definition time.

Signed-off-by: PKUWZP <zhipeng.rainbowserie@gmail.com>
@PKUWZP
PKUWZP requested a review from delock August 22, 2026 23:26
@PKUWZP PKUWZP changed the title Make DeepSpeed train on Apple Silicon (MPS) with ZeRO 0-3 Enable DeepSpeed support on Apple Silicon (MPS) with ZeRO 0-3 Aug 22, 2026
@PKUWZP PKUWZP changed the title Enable DeepSpeed support on Apple Silicon (MPS) with ZeRO 0-3 Enable DeepSpeed support on Apple Silicon (MPS) with ZeRO Stage 1-3 Aug 22, 2026
PKUWZP added 3 commits August 22, 2026 17:11
…licon

deepspeed.comm forwards async_op positionally, so the CPU-staging
wrapper must resolve it against the wrapped signature instead of
kwargs; otherwise async collectives copied back before completion.

Add an Apple Silicon section to the accelerator setup guide covering
installation, usage, and current limitations.

Signed-off-by: PKUWZP <zhipeng.rainbowserie@gmail.com>
Signed-off-by: PKUWZP <zhipeng.rainbowserie@gmail.com>
Signed-off-by: PKUWZP <zhipeng.rainbowserie@gmail.com>
Comment thread deepspeed/comm/torch.py
@delock
delock enabled auto-merge August 23, 2026 08:44
@delock
delock added this pull request to the merge queue Aug 23, 2026
Merged via the queue into master with commit 64fcec6 Aug 23, 2026
14 checks passed
@delock
delock deleted the mps-phase0 branch August 23, 2026 09:47
banxingmjj pushed a commit to openanolis/DeepSpeed that referenced this pull request Aug 24, 2026
…ync on MPS (deepspeedai#8303)

## Summary

Resolves @delock's review note on deepspeedai#8293
(deepspeedai#8293 (comment)):
`irecv` is asynchronous by contract and has no `async_op` parameter, so
the MPS CPU-staging wrapper must handle it explicitly.

Two fixes:

1. **`deepspeed/comm/comm.py`** — `isend`/`irecv` dispatched to the
*blocking* `cdb.send`/`cdb.recv` (since the original comm backend,
deepspeedai#1985). Callers got a blocking call and `recv`'s return value (the
source rank `int`) instead of a waitable handle, so
`dist.irecv(...).wait()` raised `AttributeError`. This affects every
backend, not just MPS — e.g. the 1-bit comm helpers
(`runtime/comm/{compressed,hccl,nccl}.py`) call
`dist.isend/irecv(...).wait()`. They now route to
`cdb.isend`/`cdb.irecv`.
2. **`deepspeed/comm/torch.py`** — with the routing fixed, the MPS
staging wrapper's copy-back decision (keyed on an `async_op` argument)
ran immediately for `irecv`, before the transfer completed. A new
`always_async` flag on `stage_on_cpu` defers the copy-back to the
handle's `wait()` for `isend`/`irecv`. `StagedWork.wait()` now also
returns the underlying work's wait result.

### Verified (M5 Max, macOS 26.3, torch 2.13)

- Real two-process gloo run with MPS tensors: on master, `dist.irecv`
returns an `int` and `.wait()` crashes; with this PR it returns a handle
and the buffer holds the correct payload after `wait()`.
- `DS_ACCELERATOR=mps pytest unit/comm/test_dist.py`: 10 passed
(multi-rank cases skip on 1 device).
- ZeRO-2/3 smoke training unaffected.

### Test

Adds `TestDistIsendIrecv` (world size 2) to the existing
`tests/unit/comm/test_dist.py`: rank 0 `isend`s, rank 1 `irecv`s, both
assert a waitable handle and verify the payload after `wait()`.
Backend-agnostic, so it exercises the routing fix on CUDA/CPU CI as
well.

### Relation to deepspeedai#8301

deepspeedai#8301 addresses the same note with a more extensive `StagedWork`
(futures, result identity restoration, weakref buffer tracking). This PR
makes the fix more concise and accurate: no current DeepSpeed users
calls `Work.result()`/`get_future()` on staged P2P ops, and the staged
CPU buffer for `isend` is kept alive by the deferred copy-back closure
until `wait()`. Huge Credit to @FU-max-boop for the thorough analysis of
the Work semantics and fixes.

---------

Signed-off-by: PKUWZP <zhipeng.rainbowserie@gmail.com>
banxingmjj pushed a commit to openanolis/DeepSpeed that referenced this pull request Aug 26, 2026
…am build for Apple Silicon (deepspeedai#8300)

## Summary

This PR implements the Phase 1 work of Apple Silicon support for
DeepSpeed (follow-up to deepspeedai#8293, which made single-device ZeRO 1–3
training work with pure-PyTorch ops). This PR adds the **op-builder
layer**:

- Metal kernels compiled at runtime, 
- a first real Metal kernel (FusedAdam), 
- and the C++ CPU Adam build so ZeRO-Offload works on Macs.

### Changes

- **`op_builder/mps/builder.py`** — new `MetalOpBuilder`. Subclasses
list `.metal` files in `metal_sources()`; the shader is compiled at
`load()` through `torch.mps.compile_shader`, which dispatches kernels on
PyTorch's own MPS command stream. No Xcode project, `.metallib`
packaging, or C++ extension build is involved. `is_compatible()`
additionally requires `torch.mps.compile_shader`.
- **`csrc/mps/fused_adam.metal` + `op_builder/mps/fused_adam.py`** —
`FusedAdam` becomes a Metal kernel (one launch per tensor). It does all
math in fp32 and stores in the parameter dtype, the same contract as
`csrc/adam/multi_tensor_adam.cu`; this also closes the bf16 ulp gap
noted in deepspeedai#8293. The `torch._foreach_*` implementation remains as a
fallback for torch builds without `compile_shader` and for
non-contiguous tensors.
- **`op_builder/mps/cpu_adam.py`** — builds `csrc/adam/cpu_adam*.cpp`
with the system clang (`-D__SCALAR__` on arm64). Apple clang has no
OpenMP, so the build uses Homebrew `libomp` when `brew --prefix libomp`
resolves and omits it otherwise; both paths verified. This enables
`DeepSpeedCPUAdam` and therefore ZeRO-Offload on Apple Silicon. Unified
memory means offloading does not copy parameters between separate
memories.
- **`tests/unit/ops/adam/test_adamw.py`** —
`test_fused_adam_matches_reference` checks `FusedAdam` against an
explicit fp32-math / storage-dtype-rounding reference for fp32, bf16,
and fp16 × Adam/AdamW (replaces the `torch.optim` comparison from deepspeedai#8293,
whose bf16 reference computes in bf16 and is a worse baseline).
Tolerance is 8 ulp of the storage dtype at tensor scale, which covers
measured fp32 op-order drift over 5 steps.
- **`tests/unit/ops/adam/test_cpu_adam.py`, `test_hybrid_adam.py`** —
`py-cpuinfo` has no `vendor_id_raw` on Apple Silicon; use `.get()`.
- **`MANIFEST.in`** — ship `.metal` sources. **Docs** — accelerator
setup guide updated for offload and the Metal/OpenMP notes.

### Verified on an M5 Max (macOS 26.3, torch 2.13.0)

- `DeepSpeedCPUAdam` matches `torch.optim` to ~1e-6; ZeRO-Offload trains
end to end for stage 1/2/3 × fp32/bf16/fp16 (optimizer offload; plus
param offload for stage 3).
- Metal `FusedAdam` vs foreach fallback, 4×100k params: 0.08 vs 0.27
ms/step (fp32), 0.03 vs 0.16 ms/step (bf16). Both implementations pass
the new reference test in all 6 cases.
- `DS_ACCELERATOR=mps pytest unit/ops/adam/test_cpu_adam.py
unit/ops/adam/test_hybrid_adam.py unit/ops/adam/test_adamw.py`: 74
passed, 7 skipped.
- `op_builder.mps` imports with torch absent (the sdist/install-smoke
path).

### Follow-ups

- macOS arm64 CI workflow so these paths are exercised upstream.
- Further Metal kernels (quantizer for ZeRO++).

---------

Signed-off-by: PKUWZP <zhipeng.rainbowserie@gmail.com>
banxingmjj pushed a commit to openanolis/DeepSpeed that referenced this pull request Aug 31, 2026
…erator (deepspeedai#8335)

## Summary

Closes the remaining gap in the Apple Silicon support series (deepspeedai#8293,
deepspeedai#8300, deepspeedai#8303, deepspeedai#8307): none of the MPS paths were exercised by CI — every
MPS-gated test skips on Linux runners, so regressions could only be
caught on a developer's Mac.

### macOS CI workflow (`mps-torch-latest.yml`)

Runs the MPS-green unit test subset on GitHub's arm64 macOS runners
(`macos-15`), which expose a working MPS device:

- `unit/ops/adam/test_adamw.py` — Metal/foreach FusedAdam vs fp32-math
reference, CPU Adam configs incl. ZeRO-Offload
- `unit/comm/test_dist.py` — gloo CPU-staging for collectives and P2P
(`TestMpsStagedP2P`)
- `unit/runtime/test_ds_config_dict.py` — config-driven
`deepspeed.initialize` + training steps

**Designed not to interfere with existing CI:**
- PR triggers are scoped via `paths:` to MPS-relevant files
(`accelerator/**`, `op_builder/mps/**`, `csrc/mps/**`,
`deepspeed/comm/**`, the two test dirs, and the workflow itself) — the
check does not even appear on unrelated PRs.
- Separate workflow, own concurrency group with cancel-in-progress, hard
`timeout-minutes: 45`.
- Not a required check (that's a branch-protection setting; nothing here
changes it), so even a red run cannot block merges of non-macOS work.
- Nightly `schedule` + `workflow_dispatch` for coverage between touching
PRs.

### torch floor check

`MPS_Accelerator.__init__` now fails with a clear message on torch older
than 2.3, where the `torch.mps` memory queries ZeRO depends on
(`recommended_max_memory`) do not exist — previously this surfaced as a
bare `AttributeError` deep inside ZeRO's flatten logic. Feature-detected
rather than version-parsed. (The Metal FusedAdam kernel already degrades
gracefully on torch without `compile_shader`.)

## Validation

- The workflow's exact pytest command passes locally on an M5 Max (macOS
26.3, torch 2.13): 65 passed, 23 skipped (multi-device), 1m54s —
comfortably inside the runner budget.
- Guard verified both ways: normal construction unaffected; with
`recommended_max_memory` hidden, construction raises the explicit
`ValueError`.

---------

Signed-off-by: PKUWZP <zhipeng.rainbowserie@gmail.com>
banxingmjj pushed a commit to openanolis/DeepSpeed that referenced this pull request Sep 14, 2026
## Summary

Phase 2 of Apple Silicon support (follow-up to deepspeedai#8293/deepspeedai#8300/deepspeedai#8335): the
CPU Adam kernel — the ZeRO-Offload optimizer path — ran scalar on
AArch64 machines without SVE, which includes every Apple Silicon Mac.
This adds a 4-lane NEON implementation of the existing SIMD macro layer.

### Changes

- **`csrc/includes/simd.h`** — a `__NEON__` branch defining the full
macro set (`SIMD_LOAD/STORE/SET/ADD/MUL/FMA/SQRT/DIV/AND/ANDNOT/OR/XOR`,
width 4):
  - fp16 via the hardware converters (`vcvt_f32_f16` / `vcvt_f16_f32`).
- bf16 via the same round-to-nearest-even + NaN-quieting flow as the
AVX512 `store_16_f32_as_bf16_nearest` (using `vaddhn_u32` for the
add-and-take-high-half step); loads are widen+shift.
- x86 `andnot(x, y) = ~x & y` maps to `vbicq(y, x)` — operand order
preserved (documented in a comment).
- The bf16 `simd_load`/`simd_store` guards widen from AVX512-only to
AVX512-or-NEON.
- **`csrc/includes/cpu_adam.h`** — `Step_AVX`'s non-AVX512 bf16 bailout
is lifted for NEON (this was silently sending bf16 back to the scalar
tail); the two Adam gates widen to include `__NEON__`.
- **`csrc/adam/cpu_adam_impl.cpp`** — same gate widening (4 sites,
including `kZenAdamAlign`). The NEON branch sits before the existing
`__SVE__` alternative and they remain mutually exclusive builder-emitted
defines.
- **`op_builder/builder.py`** — `simd_width()` advertises `-D__NEON__`
for `ARM_8` without SVE. 32-bit ARM keeps `__SCALAR__`: the
`vdivq_f32`/`vsqrtq_f32` intrinsics used are A64-only.
- **`op_builder/mps/cpu_adam.py`** — switches from `-D__SCALAR__` to
`-D__NEON__`.
- Lion/Adagrad/AIO gates are untouched and keep their current scalar
behavior on ARM (candidate follow-ups).

### Measured on Apple M5 Max (macOS 26.3, Apple clang, Homebrew libomp)

`DeepSpeedCPUAdam` step, 50M params, 10-step average, vs the
`-D__SCALAR__` build of the same tree:

| dtype | scalar | NEON | speedup |
|---|---|---|---|
| fp32 | 11.5 ms | 3.8 ms | 3.0× |
| fp16 | 11.5 ms | 3.3 ms | 3.5× |
| bf16 | 12.7 ms | 4.9 ms | 2.6× |

### Correctness

- NEON and scalar builds produce **bit-identical** fp16 results on
identical inputs (5 steps, 1M params).
- All dtypes (fp32/fp16/bf16 params; fp32 and bf16 moments) match an
fp32 `torch.optim.Adam/AdamW` oracle within storage rounding, at sizes
exercising pure-SIMD, SIMD+scalar-tail (1000003), and sub-width (3)
paths.
- Existing suites on the M5 Max: `test_cpu_adam.py` +
`test_hybrid_adam.py` + `test_adamw.py` — 122 passed, 7 skipped. The
`mps-torch-latest` CI workflow JIT-builds this kernel in its offload
configs, so the NEON path is exercised upstream on every touching PR.

---------

Signed-off-by: PKUWZP <zhipeng.rainbowserie@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants