Files
nvidia-nemo--speech/nemo/utils/trainer_utils.py
T
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:28:58 +08:00

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)