Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
55b24f3
add zero3 example
wuyl1 Mar 6, 2026
fbedb2f
enable sdma allgather
wuyl1 Mar 9, 2026
ccb634e
fix bug
wuyl1 Mar 9, 2026
e0eb510
fix bug
wuyl1 Mar 9, 2026
6512ecf
fix bug
wuyl1 Mar 9, 2026
d5f8489
add test case
wuyl1 Mar 9, 2026
4b2d44d
fix bug
wuyl1 Mar 9, 2026
f3a0d1b
copy_output_to_user=True
wuyl1 Mar 10, 2026
939cc0c
use same training sample
wuyl1 Mar 10, 2026
ca01795
add flops
wuyl1 Mar 10, 2026
33edc8a
add training log
wuyl1 Mar 10, 2026
5eb18e8
change to 2.7b
wuyl1 Mar 10, 2026
f7d587d
copy_output_to_user: bool = False
wuyl1 Mar 10, 2026
4053ea1
fix noncopy
wuyl1 Mar 11, 2026
72020df
fix bug
wuyl1 Mar 11, 2026
6b782d9
update
wuyl1 Mar 16, 2026
fc41552
use real txt
wuyl1 Mar 18, 2026
2c5104c
zero3: route SDMA allgather through mori_cpp.AllGatherIntoTensor
inkcherry May 7, 2026
f979a54
zero3: drop CPU sync from SDMA Work.wait() to match RCCL semantics
inkcherry May 7, 2026
5644ae3
zero3: add sdma_allgather end-to-end examples (GPT + Qwen3-32B)
inkcherry May 7, 2026
2f5eaa6
update readme
inkcherry May 7, 2026
e7bbe36
readme: 2000-step loss curve plots (off vs on)
inkcherry May 11, 2026
57d929d
qwen3 trainer: chunked wikitext loader + cleaner 2000-step loss curve
inkcherry May 11, 2026
cec6dbf
readme: drop perf annotation from GPT loss plot (loss-only figures)
inkcherry May 12, 2026
8f45f83
update readme
inkcherry May 12, 2026
606f309
comm: move SDMA allgather into TorchBackend as a transparent fast-path
inkcherry May 13, 2026
35e1102
sdma allgather: explicit opt-in env var + leave ZeRO-3 hot path untou…
inkcherry May 13, 2026
e7402be
examples/sdma_allgather/README: fill in GPT peak memory cell
inkcherry May 13, 2026
0ce6bd2
examples/sdma_allgather: drop accidentally-committed baseline_pp.py
inkcherry May 13, 2026
e66d664
update
inkcherry May 13, 2026
5e5c3fe
update comments
inkcherry May 13, 2026
bedb5eb
sdma allgather: fix CI format checks
inkcherry May 13, 2026
cc505f9
update
inkcherry May 13, 2026
9415374
update readme
inkcherry May 13, 2026
4111556
mori: move from deepspeed/runtime/comm to deepspeed/comm
inkcherry May 14, 2026
f0dc1f4
Merge branch 'master' of github.com:deepspeedai/DeepSpeed into sdma_ag_
inkcherry May 14, 2026
24e8386
examples/sdma_allgather: fix CI format checks
inkcherry May 14, 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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ The meeting is hosted on Zoom and can be joined [here](https://zoom-lfx.platform

## Latest News

* [2026/05] [System DMA (SDMA) for ZeRO-3: offload collectives off compute units on AMD GPUs for better overlap](https://github.com/deepspeedai/DeepSpeed/blob/master/examples/sdma_allgather/README.md)

* [2026/03] DeepSpeed Team gave a tutorial at ASPLOS 2026 titled ["Building Efficient Large-Scale Model Systems with DeepSpeed: From Open-Source Foundations to Emerging Research" ](https://supercomputing-system-ai-lab.github.io/events/asplos2026-llm-tutorial/index.html)

* [2026/03] [Our SuperOffload work received an Honorable Mention for the ASPLOS 2026 Best Paper Award](https://dl.acm.org/doi/10.1145/3760250.3762217)
Expand Down
228 changes: 228 additions & 0 deletions deepspeed/comm/mori.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0

# DeepSpeed Team
"""mori SDMA backend, plugged into ``TorchBackend.all_gather_into_tensor``.

When the user opts in, ``deepspeed.comm`` routes ``all_gather_into_tensor``
on the WORLD process group through ``mori_cpp.AllGatherIntoTensor``
(intra-node SDMA copy on AMD MI300). Any failure (mori missing,
non-AMD/ROCm runtime, shmem init error, oversized call, non-WORLD group)
yields ``None`` and the caller falls back to the underlying RCCL/NCCL
allgather.

User-visible controls (env vars, no ``ds_config`` field):

* ``DS_SDMA_ALLGATHER=1`` opt in to the SDMA path. Required:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we use DS_DMA_ALLGATHER and DS_DMA_ALLGATHER_MAX_NUMEL as behavior control environment variable? I'm thinking about in the future other accelerators wants to use similiar techniques, and can share the same environment variable name that is not brand binded.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I initially wanted to use a config file, but considering that currently only the AMD path is available, I set it as an environment variable.
I suggest adding it to the config file in the future if more accelerators support this approach, because environment variables tend to be more specific.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@inkcherry I feel that SDMA implementation should be a drop in replacement of RCCL implementation. So maybe it should be turn on by default. In comments, I saw descriptions that Mori allgather may not be bit by bit identical to RCCL implementation. However allgather has no computation, how could it be not bit by bit identical?

I agree for *MAX_NUMEL option, if in the future more accelerator use it, we need to move it as a config controled variable and give it a generic name.

@inkcherry inkcherry May 13, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

For now, set it as an environment variable. we don't want DeepSpeed's collective backend to silently change behaviour based on which extra packages happen to be installed — mori may end up on the system as a transitive dep of some other framework, not because the user explicitly chose to enable SDMA.

Allgather itself will not have changes. Due to timing variations (data dependencies are correct), end-to-end results might have bit-level differences at the tail due to the order of additions (which can be considered noise, and this is normal in training). However, the loss remains unchanged. Thanks, I have revised the wording to prevent ambiguity.

even when mori is installed, the
SDMA fast-path stays off unless
the user sets this explicitly.
When set, ``MORI_ENABLE_SDMA=1`` is
auto-exported on the user's behalf
so mori allocates uncached transit
buffers.
* ``DS_SDMA_ALLGATHER_MAX_NUMEL=N`` override the transit buffer size in
elements (default 64M = 256 MiB
per-rank input, ~2 GiB output on 8
ranks)
"""

import os
from typing import Optional

import torch

from deepspeed.accelerator import get_accelerator
from deepspeed.utils import logger

_handle = None
_dtype_map = None
_max_numel = 0
_init_attempted = False
_call_failed_warned = False


class _SdmaWork:
"""Duck-type compatible with ``torch.distributed.Work``.

``wait()`` issues a stream-level dependency only and does NOT block the
CPU, mirroring RCCL ``Work.wait()`` semantics. ZeRO-3's prefetch
pipeline relies on the CPU staying free so the next bucket can be
queued ahead of time while bucket N is in flight.
"""

def __init__(self, event):
self._event = event

def wait(self):
get_accelerator().current_stream().wait_event(self._event)

def is_completed(self) -> bool:
return self._event.query()


def _ensure_default_pg_registered():
"""Register the WORLD process group as 'default' in PyTorch's C++ GroupRegistry.

mori's shmem layer looks up the PG by the name "default"; the standard
DeepSpeed init path doesn't register WORLD under that label.
"""
world_group = torch.distributed.group.WORLD
assert world_group is not None, "torch.distributed must be initialized before SDMA allgather"
torch._C._distributed_c10d._register_process_group("default", world_group)


def _build_dtype_map():
"""torch.dtype -> mori_cpp.DataType (NCCL-style enum)."""
from mori.ccl import DataType
return {
torch.uint8: DataType.Uint8,
torch.int8: DataType.Int8,
torch.int16: DataType.Int16,
torch.int32: DataType.Int32,
torch.int64: DataType.Int64,
torch.float16: DataType.Float16,
torch.bfloat16: DataType.BFloat16,
torch.float32: DataType.Float32,
torch.float64: DataType.Float64,
}


_TRUTHY = {"1", "true", "True", "TRUE", "yes", "Yes", "YES", "on", "On", "ON"}


def _is_enabled_by_env() -> bool:
"""User must explicitly opt in via ``DS_SDMA_ALLGATHER=1``.

Default is off even when mori happens to be importable: mori is an
external dependency and we don't want DeepSpeed's collective backend
to silently change behaviour based on which extra packages are
installed. Keeping this opt-in also makes A/B baselines against the
stock RCCL path trivial without having to uninstall mori.
"""
return os.environ.get("DS_SDMA_ALLGATHER", "0") in _TRUTHY


def _resolve_max_numel(default: int) -> int:
raw = os.environ.get("DS_SDMA_ALLGATHER_MAX_NUMEL")
if raw is None:
return default
try:
return max(int(raw), 0)
except ValueError:
return default


def init(max_numel: int = 64 * 1024 * 1024) -> None:
"""Best-effort, idempotent SDMA handle construction.

Builds one ``mori_cpp.AllGatherIntoTensor`` (NCCL/RCCL-style C++
dispatcher) sized for the largest expected per-rank shard. All
subsequent allgather calls reuse this handle. Safe to call
unconditionally: any failure leaves ``_handle`` unset and logs a
single rank-0 info line, so callers transparently fall back to
RCCL/NCCL.
"""
global _handle, _dtype_map, _max_numel, _init_attempted
if _init_attempted:
return
_init_attempted = True

is_rank0 = torch.distributed.is_initialized() and torch.distributed.get_rank() == 0
if not _is_enabled_by_env():
# Silent no-op: SDMA stays off and dist.allgather is used. We
# don't log here because most users never set DS_SDMA_ALLGATHER and
# rank-0 spam on every backend init is noise.
return

max_numel = _resolve_max_numel(max_numel)
# mori's SymmMemManager only allocates the uncached transit buffers
# required by the SDMA kernel when MORI_ENABLE_SDMA is set; setdefault
# so users who already exported it (or want to override) win.
os.environ.setdefault("MORI_ENABLE_SDMA", "1")

try:
_ensure_default_pg_registered()
import mori.shmem as shmem
from mori.ccl import AllGatherIntoTensor

shmem.shmem_torch_process_group_init("default")
my_pe = shmem.shmem_mype()
npes = shmem.shmem_npes()
# Per-rank input transit buffer must hold the largest shard we'll
# ever see; output buffer = npes * input. 4 B/element is the SDMA
# kernel's uint32 lane width.
input_bytes = max_numel * 4
_handle = AllGatherIntoTensor(
my_pe=my_pe,
npes=npes,
input_buffer_size=input_bytes,
output_buffer_size=input_bytes * npes,
copy_output_to_user=True,
)
_dtype_map = _build_dtype_map()
_max_numel = max_numel
if is_rank0:
logger.info(f"SDMA allgather enabled via mori_cpp.AllGatherIntoTensor "
f"(max_numel={max_numel})")
except Exception as e:
_handle = None
_dtype_map = None
_max_numel = 0
if is_rank0:
logger.info(f"SDMA allgather unavailable ({type(e).__name__}: {e}); "
f"using RCCL/NCCL allgather")


def is_enabled() -> bool:
return _handle is not None


def supports(input_tensor: torch.Tensor, group=None) -> bool:
"""Cheap pre-check used by ``TorchBackend.all_gather_into_tensor``.

SDMA is only safe when:
- the backend is initialised (``_handle`` set),
- the call is on the WORLD process group (mori's shmem layer was
bound to "default"/WORLD at init time),
- the per-rank shard fits inside the pre-allocated transit buffer,
- the input dtype is in ``_dtype_map``.
"""
if _handle is None:
return False
if group is not None and group is not torch.distributed.group.WORLD:
return False
if input_tensor.numel() > _max_numel:
return False
if _dtype_map is None or input_tensor.dtype not in _dtype_map:
return False
return True


def allgather_into_tensor(input_tensor: torch.Tensor, output_tensor: torch.Tensor, group=None) -> Optional[_SdmaWork]:
"""Run one allgather_into_tensor through the SDMA handle.

Returns an ``_SdmaWork`` (Work-compatible) on success. Returns
``None`` when SDMA is not applicable for this call (uninitialised,
non-WORLD group, dtype not supported, shard larger than the transit
buffer) or the call fails for any reason — the caller falls back to
``dist.allgather_fn``.
"""
global _call_failed_warned
if not supports(input_tensor, group):
return None
try:
stream = get_accelerator().current_stream()
dtype = _dtype_map[input_tensor.dtype]
ok = _handle(input_tensor.data_ptr(), output_tensor.data_ptr(), input_tensor.numel(), dtype,
stream.cuda_stream)
if not ok:
return None
event = get_accelerator().Event()
event.record(stream)
return _SdmaWork(event)
except Exception as e:
if (not _call_failed_warned and torch.distributed.is_initialized() and torch.distributed.get_rank() == 0):
logger.warning(f"SDMA allgather failed ({e}); falling back to dist.allgather")
_call_failed_warned = True
return None
26 changes: 26 additions & 0 deletions deepspeed/comm/torch.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,23 @@ def __init__(self, backend, timeout, init_method, rank=-1, world_size=-1, name='
self.init_process_group(backend, timeout, init_method, rank, world_size)
if self.shm_comm_op != None:
self.shm_comm_op.initialize(self.get_world_size(), self.get_rank())
# Best-effort SDMA (mori) backend acquisition. Stays None on
# non-AMD/ROCm or when mori is unavailable; in that case
# all_gather_into_tensor below transparently falls through to
# torch.distributed.all_gather_into_tensor.
self._init_sdma_backend()

def _init_sdma_backend(self):
"""Try to enable the mori SDMA path for ``all_gather_into_tensor``.

Failure (non-AMD, mori not installed, shmem init error) leaves the
handle unset and the standard torch.distributed allgather is used.
"""
try:
from . import mori as _mori
_mori.init()
except Exception:
pass

@classmethod
@disable_compiler_collective
Expand Down Expand Up @@ -233,6 +250,15 @@ def all_gather(self, tensor_list, tensor, group=None, async_op=False):

@disable_compiler_collective
def all_gather_into_tensor(self, output_tensor, input_tensor, group=None, async_op=False):
# Transparent SDMA fast-path on AMD/ROCm: when the mori backend is
# available and the call is on the WORLD process group, route
# through mori_cpp.AllGatherIntoTensor. Any condition that makes
# SDMA unsafe (non-WORLD group, oversized shard, unsupported dtype,
# mori unavailable) yields None and we fall through to RCCL/NCCL.
from . import mori as _mori
sdma_work = _mori.allgather_into_tensor(input_tensor, output_tensor, group=group)
if sdma_work is not None:
return sdma_work
if self.has_all_gather_into_tensor():
return self.all_gather_function(output_tensor=output_tensor,
input_tensor=input_tensor,
Expand Down
Loading
Loading