Source code for flashdreams.core.distributed

# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Distributed-training initialization helpers."""

from __future__ import annotations

import atexit
import ctypes
import math
import os
import sys
from datetime import timedelta

import pynvml
import torch
import torch.distributed as dist
from loguru import logger

# Side effect: install the stdlib-``logging`` filter that demotes benign
# Inductor autotuner-fallback ERROR records to WARNING so first-run
# warmup doesn't look like a hard failure. Pulled in here (rather than
# from ``flashdreams.core.__init__``) because every FlashDreams entry
# point that talks to torch.distributed already imports this module at
# process start, and the filter is process-global and idempotent.
from flashdreams.core import log_filters  # noqa: F401

DEFAULT_LOG_LEVEL = "INFO"


def _safe_destroy_pg() -> None:
    """Tear down the default process group on interpreter exit.

    Registered via :func:`atexit.register` from :func:`init` so NCCL stops
    printing the ``destroy_process_group() was not called before program
    exit`` warning at the end of every ``flashdreams-run`` / ``torchrun``
    invocation. Best-effort: never raises, so a teardown failure cannot
    mask the original exit code or exception.
    """
    try:
        if dist.is_available() and dist.is_initialized():
            dist.destroy_process_group()
    except Exception:  # noqa: BLE001 -- best-effort cleanup at exit
        pass


def shutdown(*, synchronize: bool = False, terminate_process: bool = False) -> None:
    """Perform bounded distributed shutdown for an owned process lifetime.

    Callers should request synchronization only after every rank completed its
    workload. ``terminate_process`` is intended for a successful NCCL console
    command whose process lifetime is owned by the caller. Compiled CUDA-graph
    inference can leave backend workers that make both process-group shutdown
    and abort block indefinitely; after the final barrier, immediate process
    termination is the only bounded cleanup for that case.
    """
    synchronization_complete = not synchronize
    try:
        if not is_distributed_initialized():
            return
        if synchronize:
            logger.info("Synchronizing distributed ranks before shutdown.")
            dist.barrier(device_ids=[torch.cuda.current_device()])
            synchronization_complete = True
            logger.info("Distributed shutdown synchronization complete.")
    finally:
        if is_distributed_initialized():
            terminate_nccl = terminate_process and dist.get_backend() == "nccl"
            if not synchronize:
                # A rank-local failure can leave peers inside outstanding NCCL
                # or compiled CUDA-graph work.  destroy_process_group() waits
                # for that work and can therefore hide the original exception
                # indefinitely.  Let the exception escape so torchrun can
                # terminate the remaining ranks as a group.
                logger.warning(
                    "Skipping process-group destruction after an "
                    "unsynchronized rank failure."
                )
            elif terminate_nccl:
                atexit.unregister(_safe_destroy_pg)
                if synchronization_complete:
                    logger.info("Exiting after coordinated NCCL shutdown barrier.")
                    sys.stdout.flush()
                    sys.stderr.flush()
                    os._exit(0)
            else:
                logger.info("Destroying the distributed process group.")
                dist.destroy_process_group()
                logger.info("Distributed process group destroyed.")
        atexit.unregister(_safe_destroy_pg)


def is_distributed_initialized() -> bool:
    """Return True when torch distributed is available and initialized."""
    return dist.is_available() and dist.is_initialized()


def get_global_rank() -> int:
    """Return the torch distributed global rank, or 0 outside distributed runs."""
    if is_distributed_initialized():
        return dist.get_rank()
    return 0


def get_global_rank_from_env() -> int:
    """Return the launch-provided rank, or 0 when unavailable."""
    try:
        return int(os.environ.get("RANK", "0"))
    except ValueError:
        return 0


def get_global_rank_for_logging() -> int:
    """Return the best available global rank for early process logging."""
    if is_distributed_initialized():
        return get_global_rank()
    return get_global_rank_from_env()


def configure_loguru_for_distributed(world_rank: int | None = None) -> None:
    """Keep rank 0 log levels intact and demote other ranks to DEBUG."""
    if world_rank is None:
        world_rank = get_global_rank_for_logging()

    log_level = os.environ.get("LOGURU_LEVEL", DEFAULT_LOG_LEVEL)
    debug_level = logger.level("DEBUG")

    def demote_non_rank0(record):
        if world_rank != 0:
            record["level"] = type(record["level"])(
                debug_level.name,
                debug_level.no,
                debug_level.icon,
            )

    logger.remove()
    logger.configure(patcher=demote_non_rank0)
    logger.add(sys.stderr, level=log_level)


configure_loguru_for_distributed()


[docs] class Device: """Lightweight wrapper around an NVML device handle for CPU-affinity queries.""" _nvml_affinity_elements = math.ceil((os.cpu_count() or 1) / 64) def __init__(self, device_idx: int): """Bind to the NVML handle for the GPU at ``device_idx``.""" super().__init__() self.handle = pynvml.nvmlDeviceGetHandleByIndex(device_idx)
[docs] def get_name(self) -> str: """Return the marketing name reported by NVML for this device.""" return pynvml.nvmlDeviceGetName(self.handle)
[docs] def get_cpu_affinity(self) -> list[int]: """Return the indices of CPUs ideally affined to this GPU per NVML.""" affinity_string = "" for j in pynvml.nvmlDeviceGetCpuAffinity( self.handle, Device._nvml_affinity_elements ): # NVML returns a sequence of 64-bit affinity bitmasks, low word first. affinity_string = "{:064b}".format(j) + affinity_string affinity_list = [int(x) for x in affinity_string] affinity_list.reverse() # so core 0 is in the 0th element of the list return [i for i, e in enumerate(affinity_list) if e != 0]
[docs] def init() -> int | None: """Initialize distributed training.""" if dist.is_initialized(): return torch.cuda.current_device() pynvml.nvmlInit() local_rank = int(os.getenv("LOCAL_RANK", 0)) try: device = Device(local_rank) sched_setaffinity = getattr(os, "sched_setaffinity", None) if sched_setaffinity is not None: sched_setaffinity(0, device.get_cpu_affinity()) except (pynvml.NVMLError, OSError) as e: logger.warning(f"Failed to set device affinity: {e}") os.environ["TORCH_NCCL_BLOCKING_WAIT"] = "0" os.environ["TORCH_NCCL_ASYNC_ERROR_HANDLING"] = "1" if dist.is_available(): torch.cuda.set_device(local_rank) timeout_seconds = os.getenv("TORCH_NCCL_HEARTBEAT_TIMEOUT_SEC", 1800) timeout_timedelta = timedelta(seconds=int(timeout_seconds)) dist.init_process_group( backend="nccl", init_method="env://", timeout=timeout_timedelta, device_id=local_rank, ) # Always destroy the process group on interpreter shutdown so NCCL # does not warn about a leaked group at the end of every run; the # handler is idempotent if a caller (e.g. a long-lived gRPC server) # already destroyed the group explicitly before exiting. atexit.register(_safe_destroy_pg) configure_loguru_for_distributed(get_global_rank()) logger.critical( f"Initialized distributed inference with local rank {local_rank} with timeout {timeout_seconds}", ) # Bump cudaLimitMaxL2FetchGranularity (id=0x05) to 128 bytes for better bandwidth. _libcudart = ctypes.CDLL("libcudart.so") p_value = ctypes.cast((ctypes.c_int * 1)(), ctypes.POINTER(ctypes.c_int)) _libcudart.cudaDeviceSetLimit(ctypes.c_int(0x05), ctypes.c_int(128)) _libcudart.cudaDeviceGetLimit(p_value, ctypes.c_int(0x05)) logger.info(f"Inference with {dist.get_world_size()} GPUs.")