ba4be087d5
Create PR to main with cherry-pick from release / cherry-pick (push) Failing after 0s
CICD NeMo / pre-flight (push) Failing after 0s
CICD NeMo / configure (push) Has been skipped
Build, validate, and release Neural Modules / pre-flight (push) Failing after 1s
CICD NeMo / code-linting (push) Has been skipped
Build, validate, and release Neural Modules / release (push) Has been skipped
Build, validate, and release Neural Modules / release-summary (push) Has been cancelled
CICD NeMo / cicd-test-container-build (push) Has been cancelled
CICD NeMo / cicd-import-tests (push) Has been cancelled
CICD NeMo / L0_Setup_Test_Data_And_Models (push) Has been cancelled
CICD NeMo / cicd-main-unit-tests (push) Has been cancelled
CICD NeMo / cicd-main-speech (push) Has been cancelled
CICD NeMo / Nemo_CICD_Test (push) Has been cancelled
CICD NeMo / Coverage (e2e) (push) Has been cancelled
CICD NeMo / Coverage (unit-test) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CICD NeMo / cicd-wait-in-queue (push) Has been cancelled
252 lines
9.4 KiB
Python
252 lines
9.4 KiB
Python
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
|
|
#
|
|
# 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.
|
|
# pylint: disable=C0116
|
|
|
|
from contextlib import nullcontext
|
|
from typing import Any, ContextManager, Mapping, Sequence
|
|
|
|
import torch
|
|
from lightning.fabric.plugins.precision.utils import _convert_fp_tensor
|
|
from lightning.pytorch.plugins import HalfPrecision
|
|
from lightning.pytorch.plugins.precision.precision import Precision
|
|
from lightning_utilities import apply_to_collection
|
|
from omegaconf import DictConfig, OmegaConf
|
|
from torch import Tensor
|
|
from typing_extensions import override
|
|
|
|
from nemo.core.classes.common import safe_instantiate
|
|
|
|
|
|
_FLASH_PRECISION_ALIASES = {
|
|
"fp16-flash": "fp16-flash",
|
|
"bf16-flash": "bf16-flash",
|
|
# Temporary backward-compatible aliases retained during migration.
|
|
"fp16-automodel": "fp16-flash",
|
|
"bf16-automodel": "bf16-flash",
|
|
}
|
|
|
|
|
|
def resolve_trainer_cfg(trainer_cfg: DictConfig) -> DictConfig:
|
|
"""
|
|
Resolves and processes a trainer configuration.
|
|
|
|
This function handles specific trainer configuration details:
|
|
- For half precision setups, replaces precision settings with custom plugins
|
|
- Instantiates strategy objects from mapping configurations
|
|
- Instantiates custom callbacks from sequences
|
|
|
|
Args:
|
|
trainer_cfg: A DictConfig containing trainer configuration parameters
|
|
|
|
Returns:
|
|
A processed DictConfig with resolved configuration values
|
|
"""
|
|
trainer_cfg = OmegaConf.to_container(trainer_cfg, resolve=True)
|
|
|
|
# Avoids downcasting 'audio' tensors in half precision setups and enables
|
|
# the specialized flash precision plugin without mutating global dtype state.
|
|
precision = trainer_cfg.get("precision")
|
|
if precision in ("fp16-true", "bf16-true"):
|
|
trainer_cfg.pop("precision", None)
|
|
trainer_cfg["plugins"] = [HalfPrecisionForAudio(precision)]
|
|
elif (flash_precision := _normalize_flash_precision(precision)) is not None:
|
|
trainer_cfg.pop("precision", None)
|
|
trainer_cfg["plugins"] = [FlashPrecision(flash_precision)]
|
|
|
|
# Allows customizable strategies (eg ModelParallelStrategy) in YAML configs.
|
|
if (strategy := trainer_cfg.get("strategy", None)) is not None and isinstance(strategy, Mapping):
|
|
trainer_cfg["strategy"] = safe_instantiate(strategy)
|
|
# Convert dict-valued nemo_automodel configs to proper dataclass instances.
|
|
# This must happen AFTER Hydra instantiation because Hydra's recursive
|
|
# processing chokes on dataclass fields with Union types (e.g. MoEParallelizerConfig).
|
|
_resolve_automodel_configs(trainer_cfg["strategy"])
|
|
|
|
# Allows to add custom callbacks (e.g. NsysCallback) from YAML config.
|
|
if (cbs := trainer_cfg.get("callbacks", None)) is not None and isinstance(cbs, Sequence):
|
|
resolved = []
|
|
for cb in cbs:
|
|
resolved.append(safe_instantiate(cb))
|
|
trainer_cfg["callbacks"] = resolved
|
|
|
|
return trainer_cfg
|
|
|
|
|
|
def _resolve_automodel_configs(strategy) -> None:
|
|
"""Convert plain dicts for ``distributed_config`` and ``moe_config`` to nemo_automodel objects.
|
|
|
|
When :class:`AutomodelParallelStrategy` is specified in YAML, ``distributed_config``
|
|
and ``moe_config`` arrive as plain dicts (Hydra passes them through as-is).
|
|
This function converts them to proper dataclass instances on the
|
|
already-instantiated strategy object.
|
|
|
|
Does nothing if the strategy doesn't have these attributes or if they are
|
|
already proper objects (not dicts).
|
|
"""
|
|
if isinstance(getattr(strategy, '_distributed_config', None), Mapping):
|
|
from nemo_automodel.components.distributed.config import FSDP2Config
|
|
|
|
cfg = strategy._distributed_config
|
|
# Instantiate any nested _target_ dicts (e.g. a custom mp_policy)
|
|
resolved = {}
|
|
for k, v in cfg.items():
|
|
if isinstance(v, Mapping) and "_target_" in v:
|
|
resolved[k] = safe_instantiate(v)
|
|
else:
|
|
resolved[k] = v
|
|
strategy._distributed_config = FSDP2Config(**resolved)
|
|
|
|
if isinstance(getattr(strategy, '_moe_config', None), Mapping):
|
|
from nemo_automodel.components.moe.config import MoEParallelizerConfig
|
|
|
|
strategy._moe_config = MoEParallelizerConfig(**strategy._moe_config)
|
|
|
|
|
|
class HalfPrecisionForAudio(HalfPrecision):
|
|
"""
|
|
Adjusted Pytorch Lightning plugin for training with half precision.
|
|
It avoids downcasting audio to bfloat16 when the mini-batch is a dict
|
|
with 'audio' string in the keys corresponding to audio tensors.
|
|
"""
|
|
|
|
@override
|
|
def convert_input(self, data: Any) -> Any:
|
|
"""
|
|
Converts input data to the appropriate precision format, preserving audio tensor precision.
|
|
|
|
This method overrides the parent class implementation to avoid downcasting tensors
|
|
with 'audio' in their dictionary keys. It processes input data recursively when
|
|
encountering nested dictionaries.
|
|
|
|
Args:
|
|
data: The input data to convert (can be tensor, dict, or other types)
|
|
|
|
Returns:
|
|
The converted data with appropriate precision for each element
|
|
"""
|
|
if not isinstance(data, dict):
|
|
return super().convert_input(data)
|
|
|
|
return _convert_audio_preserving(data, self._desired_input_dtype)
|
|
|
|
|
|
class FlashPrecision(Precision):
|
|
"""Precision plugin for flash optimizer training.
|
|
|
|
Unlike Lightning's :class:`HalfPrecision`, this does **not** call
|
|
:func:`torch.set_default_dtype` and does **not** use :func:`torch.autocast`.
|
|
It's recommended to use this class together with ``flashoptim`` optimizers.
|
|
|
|
This ensures that model-specific fp32 escapes (for example custom norms or
|
|
gating layers) and FlashOptim's master-weight correction terms are never
|
|
silently downcast by a global dtype override.
|
|
|
|
Note: it won't downcast your model's weights to half precision if some of them
|
|
have already been downcast (manual downcasting) or if the model is using DTensor
|
|
(in that case you have to downcast them yourself, typically in configure_model()).
|
|
|
|
Opt in by setting ``trainer.precision: bf16-flash`` in the YAML config.
|
|
"""
|
|
|
|
precision: str = "bf16-flash"
|
|
|
|
def __init__(self, precision: str = "bf16-flash") -> None:
|
|
normalized = _normalize_flash_precision(precision) or precision
|
|
self.precision = normalized
|
|
self._desired_input_dtype = torch.bfloat16 if "bf16" in normalized else torch.float16
|
|
|
|
@override
|
|
def convert_module(self, module: torch.nn.Module) -> torch.nn.Module:
|
|
# Some models manage dtype explicitly inside configure_model() and may
|
|
# intentionally keep select parameters in fp32. Only cast modules that
|
|
# are still entirely plain fp32 tensors.
|
|
if _should_skip_flash_module_conversion(module):
|
|
return module
|
|
|
|
from flashoptim import cast_model
|
|
|
|
cast_model(module, dtype=self._desired_input_dtype)
|
|
return module
|
|
|
|
@override
|
|
def forward_context(self) -> ContextManager:
|
|
return nullcontext()
|
|
|
|
@override
|
|
def convert_input(self, data: Any) -> Any:
|
|
if not isinstance(data, dict):
|
|
return apply_to_collection(
|
|
data, function=_convert_fp_tensor, dtype=Tensor, dst_type=self._desired_input_dtype
|
|
)
|
|
|
|
return _convert_audio_preserving(data, self._desired_input_dtype)
|
|
|
|
|
|
def _convert_audio_preserving(data: dict, dtype: torch.dtype) -> dict:
|
|
"""Convert dict batch to *dtype*, keeping tensors whose key contains ``'audio'`` in fp32."""
|
|
|
|
def _convert(v):
|
|
if isinstance(v, dict):
|
|
ans = {}
|
|
for k, v in v.items():
|
|
if "audio" not in k or not torch.is_tensor(v):
|
|
v = _convert(v)
|
|
ans[k] = v
|
|
return ans
|
|
if isinstance(v, torch.Tensor) and torch.is_floating_point(v):
|
|
return v.to(dtype)
|
|
return v
|
|
|
|
return _convert(data)
|
|
|
|
|
|
def _normalize_flash_precision(precision: str | None) -> str | None:
|
|
if precision is None:
|
|
return None
|
|
|
|
return _FLASH_PRECISION_ALIASES.get(precision)
|
|
|
|
|
|
def _should_skip_flash_module_conversion(module: torch.nn.Module) -> bool:
|
|
"""Return True when a module should keep its existing parameter dtype policy."""
|
|
|
|
saw_fp_tensor = False
|
|
for tensor in _iter_module_tensors(module):
|
|
if not torch.is_floating_point(tensor):
|
|
continue
|
|
|
|
saw_fp_tensor = True
|
|
if _is_distributed_tensor(tensor):
|
|
return True
|
|
if tensor.dtype != torch.float32:
|
|
return True
|
|
|
|
return not saw_fp_tensor
|
|
|
|
|
|
def _iter_module_tensors(module: torch.nn.Module):
|
|
yield from module.parameters()
|
|
yield from module.buffers()
|
|
|
|
|
|
def _is_distributed_tensor(tensor: Tensor) -> bool:
|
|
if hasattr(tensor, "device_mesh") or hasattr(tensor, "placements"):
|
|
return True
|
|
|
|
try:
|
|
from torch.distributed.tensor import DTensor
|
|
except (ImportError, ModuleNotFoundError):
|
|
return False
|
|
|
|
return isinstance(tensor, DTensor)
|