# 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