Source code for kempnerforge.config.training

"""Training configuration."""

from __future__ import annotations

from dataclasses import dataclass
from enum import StrEnum
from typing import TYPE_CHECKING, Literal

if TYPE_CHECKING:
    import torch

FP32_CROSS_ENTROPY_LOSSES = ("cross_entropy", "chunked_cross_entropy")

_FP16_UNSUPPORTED = (
    "train.mixed_precision='fp16' is not supported: it would train in pure fp16 "
    "with no loss scaling, so small gradients underflow to zero. Use 'bf16', or "
    "'fp32' on GPUs without bf16."
)


[docs] class ActivationCheckpointing(StrEnum): none = "none" full = "full" selective = "selective"
[docs] @dataclass class TrainConfig: """Training hyperparameters. ``"fp16"`` stays in the ``mixed_precision`` ``Literal`` only so the config loader passes it on to ``__post_init__``, which rejects it with the reason; without it the loader would fail first with a generic Literal error. """ batch_size: int = 8 seq_len: int = 2048 max_steps: int = 100000 grad_accum_steps: int = 1 grad_clip_norm: float = 1.0 seed: int = 42 data_seed: int | None = None compile_model: bool = True mixed_precision: Literal["bf16", "fp16", "fp32", "fp8"] = "bf16" fp32_master_weights: bool = False tf32: bool = False activation_checkpointing: ActivationCheckpointing = ActivationCheckpointing.none loss_fn: str = "cross_entropy" z_loss_weight: float = 0.0 ce_chunk_size: int = 0 fp32_cross_entropy: bool = False shutdown_timeout_sec: float = 600.0 nccl_health_check_interval: int = 0 def __post_init__(self) -> None: if self.batch_size <= 0: raise ValueError("batch_size must be positive") if self.seq_len <= 0: raise ValueError("seq_len must be positive") if self.max_steps <= 0: raise ValueError("max_steps must be positive") if self.grad_accum_steps <= 0: raise ValueError("grad_accum_steps must be positive") if self.grad_clip_norm <= 0: raise ValueError("grad_clip_norm must be positive") if self.mixed_precision == "fp16": raise ValueError(_FP16_UNSUPPORTED) if self.fp32_master_weights and self.mixed_precision == "fp32": raise ValueError( "train.fp32_master_weights has no effect with mixed_precision='fp32': the " "weights are already fp32. Use it with 'bf16' or 'fp8', or turn it off." ) if self.fp32_cross_entropy and self.loss_fn not in FP32_CROSS_ENTROPY_LOSSES: raise ValueError( f"train.fp32_cross_entropy applies to {', '.join(FP32_CROSS_ENTROPY_LOSSES)}, " f"not loss_fn={self.loss_fn!r}" ) @property def param_dtype(self) -> torch.dtype: """Resolve mixed_precision to the compute dtype, also the master weight dtype by default. FP8 resolves to bf16 -- FP8 is a compute mode, not a storage dtype. The storage dtype is :attr:`master_dtype`, which is fp32 with ``fp32_master_weights``. A config switched to ``"fp16"`` after construction skips ``__post_init__``, so ``"fp16"`` raises the same ``ValueError`` here. """ import torch if self.mixed_precision == "fp16": raise ValueError(_FP16_UNSUPPORTED) _DTYPE_MAP = { "bf16": torch.bfloat16, "fp32": torch.float32, "fp8": torch.bfloat16, } return _DTYPE_MAP[self.mixed_precision] @property def master_dtype(self) -> torch.dtype: """Dtype the parameters, their gradients and the optimizer state are kept in.""" import torch return torch.float32 if self.fp32_master_weights else self.param_dtype @property def is_fp8(self) -> bool: """Whether FP8 mixed precision is enabled.""" return self.mixed_precision == "fp8" @property def effective_data_seed(self) -> int: """Seed for data shuffling / batch composition. Falls back to ``seed`` when ``data_seed`` is unset, so existing configs reproduce their current trajectory. Kept independent from ``seed`` (parameter init) so stability studies can vary batch order while holding initialization fixed. Must stay identical across data-parallel ranks so the global shuffle is consistent before rank partitioning. """ return self.data_seed if self.data_seed is not None else self.seed