Source code for kempnerforge.training.runtime

"""Process-level context for a training job: ranks, device, mesh, pipeline."""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any

import torch
from torch.distributed.device_mesh import DeviceMesh

from kempnerforge.config.job import JobConfig
from kempnerforge.distributed.setup import get_world_info, init_distributed
from kempnerforge.metrics.logger import get_logger
from kempnerforge.resilience.elastic import log_job_info

logger = get_logger(__name__)


[docs] @dataclass(frozen=True) class RuntimeContext: """Where this process sits: ranks, device, and the mesh built for them.""" rank: int local_rank: int world_size: int device: torch.device device_mesh: DeviceMesh | None
[docs] @dataclass(frozen=True) class PipelineBundle: """Pipeline-parallel state. Built only when ``distributed.pp > 1``. The PP process group is not held here: it is derived from the mesh at the call sites that need it, so a job that never runs a step never slices it. """ rank: int size: int schedule: Any
[docs] def pp_group(runtime: RuntimeContext) -> Any: """The PP process group for this rank, derived on demand.""" mesh = runtime.device_mesh if mesh is None: # unreachable: validate() rejects pp > 1 without a mesh raise RuntimeError("pipeline parallelism requires a device mesh") return mesh["pp"].get_group()
[docs] def setup_distributed(config: JobConfig) -> RuntimeContext: """Initialize the process group + device mesh, then validate the config against it.""" rank, local_rank, world_size = get_world_info() device = torch.device(f"cuda:{local_rank}" if torch.cuda.is_available() else "cpu") device_mesh = init_distributed(config.distributed, seed=config.train.seed) config.validate(world_size) log_job_info() logger.info(f"Training config: {config}") return RuntimeContext( rank=rank, local_rank=local_rank, world_size=world_size, device=device, device_mesh=device_mesh, )