-
Notifications
You must be signed in to change notification settings - Fork 5k
zero3: SDMA allgather via mori (sdma_allgather) #7999
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
37 commits
Select commit
Hold shift + click to select a range
55b24f3
add zero3 example
wuyl1 fbedb2f
enable sdma allgather
wuyl1 ccb634e
fix bug
wuyl1 e0eb510
fix bug
wuyl1 6512ecf
fix bug
wuyl1 d5f8489
add test case
wuyl1 4b2d44d
fix bug
wuyl1 f3a0d1b
copy_output_to_user=True
wuyl1 939cc0c
use same training sample
wuyl1 ca01795
add flops
wuyl1 33edc8a
add training log
wuyl1 5eb18e8
change to 2.7b
wuyl1 f7d587d
copy_output_to_user: bool = False
wuyl1 4053ea1
fix noncopy
wuyl1 72020df
fix bug
wuyl1 6b782d9
update
wuyl1 fc41552
use real txt
wuyl1 2c5104c
zero3: route SDMA allgather through mori_cpp.AllGatherIntoTensor
inkcherry f979a54
zero3: drop CPU sync from SDMA Work.wait() to match RCCL semantics
inkcherry 5644ae3
zero3: add sdma_allgather end-to-end examples (GPT + Qwen3-32B)
inkcherry 2f5eaa6
update readme
inkcherry e7bbe36
readme: 2000-step loss curve plots (off vs on)
inkcherry 57d929d
qwen3 trainer: chunked wikitext loader + cleaner 2000-step loss curve
inkcherry cec6dbf
readme: drop perf annotation from GPT loss plot (loss-only figures)
inkcherry 8f45f83
update readme
inkcherry 606f309
comm: move SDMA allgather into TorchBackend as a transparent fast-path
inkcherry 35e1102
sdma allgather: explicit opt-in env var + leave ZeRO-3 hot path untou…
inkcherry e7402be
examples/sdma_allgather/README: fill in GPT peak memory cell
inkcherry 0ce6bd2
examples/sdma_allgather: drop accidentally-committed baseline_pp.py
inkcherry e66d664
update
inkcherry 5e5c3fe
update comments
inkcherry bedb5eb
sdma allgather: fix CI format checks
inkcherry cc505f9
update
inkcherry 9415374
update readme
inkcherry 4111556
mori: move from deepspeed/runtime/comm to deepspeed/comm
inkcherry f0dc1f4
Merge branch 'master' of github.com:deepspeedai/DeepSpeed into sdma_ag_
inkcherry 24e8386
examples/sdma_allgather: fix CI format checks
inkcherry File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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: | ||
| 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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_ALLGATHERandDS_DMA_ALLGATHER_MAX_NUMELas 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.There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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.