chore: import upstream snapshot with attribution
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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:28:58 +08:00
commit ba4be087d5
2316 changed files with 2668701 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
# Copyright (c) 2023, 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.
from nemo.utils.callbacks.cuda_graph import CUDAGraphCallback
from nemo.utils.callbacks.nemo_model_checkpoint import NeMoModelCheckpoint
from nemo.utils.callbacks.preemption import PreemptionCallback
+451
View File
@@ -0,0 +1,451 @@
# Copyright (c) 2023, 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.
# CUDAGraphCallback is a full iteration CUDA graph callback designed for
# models with PyTorch Lightning first, this has been tested with Stable
# Diffusion right now.
#
# Prerequisites for this callback:
# 1. Capturable: user has to make sure (almost) all the host & device
# synchronizations are removed, some of the syncs regarding logging
# of metrics introduced by PyTorch Lightning itself have been removed
# by this callback. This ensures the graph can be captured.
# 2. Topology: user has to make sure there's no dynamic control flow
# within the iteration. Please use APEX alternatives for building
# blocks that contain dynamic control flow, e.g. gradient clipping.
# Otherwise the captured graph can run, but may raise silent failure,
# e.g. NaN loss.
# 3. Parameters: user has to make sure pointers involved in the graph
# capturing range don't change across iterations. In this case users
# have to ensure that data is copied to static tensors. Otherwise this
# can also lead to silent failure.
import os
import time
from dataclasses import dataclass
from types import MethodType
from typing import Any, Dict
import lightning.pytorch as pl
import torch
from lightning.pytorch import LightningModule
from lightning.pytorch.callbacks import Callback
from lightning.pytorch.loops.optimization.automatic import ClosureResult
from lightning.pytorch.trainer.connectors.logger_connector.result import _ResultCollection, _ResultMetric
from lightning.pytorch.utilities import CombinedLoader, rank_zero_info
from lightning.pytorch.utilities.signature_utils import is_param_in_hook_signature
from lightning.pytorch.utilities.types import STEP_OUTPUT
from torch.nn.parallel import DistributedDataParallel
__all__ = ["CUDAGraphCallback"]
def struct_copy_one(src):
if isinstance(src, tuple):
return tuple(struct_copy_one(i) for i in src)
elif isinstance(src, list):
return list(struct_copy_one(i) for i in src)
elif isinstance(src, dict):
return {k: struct_copy_one(src[k]) for k in src}
elif isinstance(src, torch.Tensor):
return src.clone().detach().cuda()
else:
return src
def struct_copy_two(tgt, src):
if isinstance(src, tuple):
raise Exception(f"Unsupported copy for tuple yet: {type(src)}")
elif isinstance(src, list):
for i in range(len(src)):
if isinstance(src[i], (tuple, list, dict, torch.Tensor)):
struct_copy_two(tgt[i], src[i])
else:
tgt[i] = src[i]
elif isinstance(src, dict):
for k in src:
if isinstance(src[k], (tuple, list, dict, torch.Tensor)):
struct_copy_two(tgt[k], src[k])
else:
tgt[k] = src[k]
elif isinstance(src, torch.Tensor):
tgt.copy_(src, non_blocking=True)
else:
raise Exception(f"Expect top-level as container type but got: {type(src)}")
class StaticBufferLoader:
"""Load data to static buffers."""
def __init__(self, loader):
self.loader = loader
self.stream = torch.cuda.Stream()
self.static = None
def __iter__(self):
for inputs in self.loader:
if self.static is None:
with torch.cuda.stream(self.stream):
self.static = struct_copy_one(inputs)
with torch.cuda.stream(self.stream):
struct_copy_two(self.static, inputs)
torch.cuda.current_stream().wait_stream(self.stream)
yield self.static
def __len__(self):
return len(self.loader)
def get_lr(lr_scheduler):
lrs = lr_scheduler.__orig_get_lr__()
if not hasattr(lr_scheduler, "static_lrs"):
lr_scheduler.static_lrs = lrs
for i in range(len(lrs)):
lr_scheduler.static_lrs[i].copy_(lrs[i])
return lr_scheduler.static_lrs
def zero_grad(optimizer, *args, **kwargs):
# We invoke zero_grad before graph capturing.
if torch.cuda.is_current_stream_capturing():
rank_zero_info("CUDAGraphCallback: set optimizer.zero_grad as nop during graph capturing.")
else:
optimizer.__orig_zero_grad__(*args, **kwargs)
def to_tensor(self, value, name):
# Log metrics in PyTorch Lightning often invokes CPU & GPU synchronizations. Here
# we implement smart metrics to avoid those synchronizations.
# Refer to: https://github.com/Lightning-AI/pytorch-lightning/blob/2.0.7/src/lightning/pytorch/core/module.py#L615
value = value.clone().detach() if isinstance(value, torch.Tensor) else torch.tensor(value)
if not torch.numel(value) == 1:
raise ValueError(
f"`self.log({name}, {value})` was called, but the tensor must have a single element."
f" You can try doing `self.log({name}, {value}.mean())`"
)
value = value.squeeze()
return value
def get_optimizer_step(state):
def optimizer_step(
self,
epoch,
batch_idx,
optimizer,
optimizer_closure=None,
) -> None:
# Not all optimizer supports set_to_none.
if not hasattr(optimizer, "support_set_to_none"):
optimizer.support_set_to_none = is_param_in_hook_signature(
optimizer.zero_grad, "set_to_none", explicit=True
)
if optimizer.support_set_to_none:
zero_grad_kwargs = {"set_to_none": True}
else:
zero_grad_kwargs = {}
if 0 <= state.current_iteration < state.capture_iteration or state.capture_iteration < 0:
state.stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(state.stream):
optimizer.zero_grad(**zero_grad_kwargs)
self.__orig_optimizer_step__(
epoch,
batch_idx,
optimizer,
optimizer_closure=optimizer_closure,
)
torch.cuda.current_stream().wait_stream(state.stream)
if state.current_iteration == state.capture_iteration:
torch.cuda.synchronize()
# Sleep for one second to let environment stable
time.sleep(1)
rank_zero_info("CUDAGraphCallback: capturing CUDA graph for module %s.", self.__class__.__name__)
with torch.cuda.graph(state.graph, stream=state.stream, capture_error_mode="global"):
# PyTorch CUDA graph doc for whole-network capturing mentions:
#
# Sets grads to None before capture, so backward() will create
# .grad attributes with allocations from the graph's private pool
#
# But it's not necessary, and it can lead to CUDA kernels inside
# `zero_grad()` being not captured.
optimizer.zero_grad(**zero_grad_kwargs)
self.__orig_optimizer_step__(
epoch,
batch_idx,
optimizer,
optimizer_closure=optimizer_closure,
)
torch.cuda.synchronize()
# Graph replay and reconstruct missing result
if state.current_iteration >= state.capture_iteration >= 0:
state.graph.replay()
optimizer_closure._result = ClosureResult.from_training_step_output(state.output)
# If something is not capturable, try to put it there, e.g. `self.log()`.
if hasattr(self, "non_cuda_graph_capturable"):
self.non_cuda_graph_capturable()
state.current_iteration += 1
return optimizer_step
def get_training_step(state):
def training_step(self, batch):
results = self.__orig_training_step__(batch)
if state.output is None:
state.output = struct_copy_one(results)
# Copy results to static buffer to rebuild states required by PL.
with torch.no_grad():
struct_copy_two(state.output, results)
return results
return training_step
def get_amp_autocast_init(state):
def amp_autocast_init(self, *args, **kwargs):
if "cache_enabled" not in kwargs:
kwargs["cache_enabled"] = False
if state.current_iteration == 0:
rank_zero_info("CUDAGraphCallback: disable autocast cache.")
return self.__orig_init__(*args, **kwargs)
return amp_autocast_init
def get_ddp_init(state):
def init(self, *args, **kwargs):
rank_zero_info("CUDAGraphCallback: init DDP on side stream.")
with torch.cuda.stream(state.stream):
self.__orig_init__(*args, **kwargs)
return init
@dataclass
class CUDAGraphState:
current_iteration: int = 0
capture_iteration: int = -1 # -1 to disable
stream: torch.cuda.Stream = None
graph: torch.cuda.CUDAGraph = None
output: Any = None # static forward output
class CUDAGraphCallback(Callback):
"""Full iteration CUDA graph callback.
Dataloader and LR scheduler are not included in the CUDA graph with this callback.
"""
def __init__(self, capture_iteration=-1):
super().__init__()
# Required by CUDA graph with DDP
# Ref: https://pytorch.org/docs/stable/notes/cuda.html#usage-with-distributeddataparallel
if 0 <= capture_iteration <= 11:
raise Exception("Warmup must run at least 11 DDP-enabled eager iterations before capture.")
if torch.distributed.is_initialized():
raise Exception("CUDAGraphCallback should be initialized before process group.")
os.environ["TORCH_NCCL_ASYNC_ERROR_HANDLING"] = "0"
self.state = CUDAGraphState(capture_iteration=capture_iteration)
def setup(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule", stage: str) -> None:
"""Called when fit, validate, test, predict, or tune begins."""
if self.state.capture_iteration < 0:
return
# Hack to avoid CUDA graph issue with AMP, PyTorch Lightning doesn't support
# changing autocast arguments for now.
# https://github.com/pytorch/pytorch/blob/v1.13.1/torch/cuda/graphs.py#L234
torch.autocast.__orig_init__ = torch.autocast.__init__
torch.autocast.__init__ = get_amp_autocast_init(self.state)
# Before full-backward capture, DDP must be constructed in a side-stream context.
# We've merged the change that init DDP on side stream to PyTorch Lightning V2,
# but not all user defined strategy init DDP on side stream.
DistributedDataParallel.__orig_init__ = DistributedDataParallel.__init__
DistributedDataParallel.__init__ = get_ddp_init(self.state)
def teardown(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule", stage: str) -> None:
"""Called when fit, validate, test, predict, or tune ends."""
if self.state.capture_iteration < 0:
return
torch.autocast.__init__ = torch.autocast.__orig_init__
del torch.autocast.__orig_init__
DistributedDataParallel.__init__ = DistributedDataParallel.__orig_init__
del DistributedDataParallel.__orig_init__
def on_fit_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
"""Called when fit begins."""
if self.state.capture_iteration < 0:
return
if is_param_in_hook_signature(pl_module.training_step, "dataloader_iter", explicit=True):
raise Exception(
"Found `dataloader_iter` argument in the `training_step`. This is "
"not supported by full iteration CUDA graph capturing yet since "
"dataloader will be within the CUDA graph capturing range.\n"
"Try to change `dataloader_iter` to `batch` and remove "
"`next(dataloader_iter)` from `training_step`."
)
# Now that CUDA device has been set, we can init stream and graph now
self.state.stream = torch.cuda.Stream()
self.state.graph = torch.cuda.CUDAGraph()
def on_fit_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
"""Called when fit ends."""
if self.state.capture_iteration < 0:
return
def on_train_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
"""Called when the train begins."""
if self.state.capture_iteration < 0:
return
# Ensure training dataloader loads data to static buffer
dataloader = trainer.fit_loop._combined_loader._iterables
assert isinstance(
dataloader, torch.utils.data.dataloader.DataLoader
), f"Expect Dataloader type but got {type(dataloader)}"
static_loader = StaticBufferLoader(dataloader)
_mode = trainer.fit_loop._combined_loader._mode
combined_loader = CombinedLoader(static_loader, mode=_mode)
trainer.fit_loop.__orig_combined_loader__ = trainer.fit_loop._combined_loader
trainer.fit_loop._combined_loader = combined_loader
trainer.fit_loop._data_fetcher.setup(trainer.fit_loop._combined_loader)
iter(trainer.fit_loop._data_fetcher)
# Warn if `optimizer.zero_grad()` invoked during graph capturing
for optimizer in trainer.optimizers:
assert isinstance(optimizer, torch.optim.Optimizer), f"Expect Optimizer type but got {type(optimizer)}"
optimizer.__orig_zero_grad__ = optimizer.zero_grad
optimizer.zero_grad = MethodType(zero_grad, optimizer)
# Ensure LR scheduler writes to static buffer
# We don't include LR scheduler in the full CUDA graph for now since
# its overhead is very small.
for config in trainer.lr_scheduler_configs:
assert isinstance(
config.scheduler, torch.optim.lr_scheduler._LRScheduler
), f"Expect _LRScheduler type but got {type(config.scheduler)}"
config.scheduler.__orig_get_lr__ = config.scheduler.get_lr
config.scheduler.get_lr = MethodType(get_lr, config.scheduler)
# Use smart metrics to avoid syncs
LightningModule.__orig_to_tensor__ = LightningModule._LightningModule__to_tensor
LightningModule._LightningModule__to_tensor = to_tensor
# Save model outputs to static buffer for PL states reconstruct
pl_module.__orig_training_step__ = pl_module.training_step
training_step = get_training_step(self.state)
pl_module.training_step = MethodType(training_step, pl_module)
# Capture CUDA graph from model forward propagation to optimizer step
pl_module.__orig_optimizer_step__ = pl_module.optimizer_step
optimizer_step = get_optimizer_step(self.state)
pl_module.optimizer_step = MethodType(optimizer_step, pl_module)
def on_train_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
"""Called when the train ends."""
if self.state.capture_iteration < 0:
return
trainer.fit_loop._combined_loader = trainer.fit_loop.__orig_combined_loader__
trainer.fit_loop._data_fetcher.setup(trainer.fit_loop._combined_loader)
iter(trainer.fit_loop._data_fetcher)
del trainer.fit_loop.__orig_combined_loader__
for optimizer in trainer.optimizers:
optimizer.zero_grad = optimizer.__orig_zero_grad__
del optimizer.__orig_zero_grad__
for config in trainer.lr_scheduler_configs:
config.scheduler.get_lr = config.scheduler.__orig_get_lr__
del config.scheduler.__orig_get_lr__
LightningModule._LightningModule__to_tensor = LightningModule.__orig_to_tensor__
del LightningModule.__orig_to_tensor__
pl_module.training_step = pl_module.__orig_training_step__
del pl_module.__orig_training_step__
pl_module.optimizer_step = pl_module.__orig_optimizer_step__
del pl_module.__orig_optimizer_step__
def on_train_epoch_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
"""Called when the train epoch begins."""
pass
def on_train_epoch_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
"""Called when the train epoch ends.
To access all batch outputs at the end of the epoch, either:
1. Implement `training_epoch_end` in the `LightningModule` and access outputs via the module OR
2. Cache data across train batch hooks inside the callback implementation to post-process in this hook.
"""
pass
def on_train_batch_start(
self, trainer: "pl.Trainer", pl_module: "pl.LightningModule", batch: Any, batch_idx: int
) -> None:
"""Called when the train batch begins."""
pass
def on_train_batch_end(
self, trainer: "pl.Trainer", pl_module: "pl.LightningModule", outputs: STEP_OUTPUT, batch: Any, batch_idx: int
) -> None:
"""Called when the train batch ends.
Note:
The value ``outputs["loss"]`` here will be the normalized value w.r.t ``accumulate_grad_batches`` of the
loss returned from ``training_step``.
"""
pass
def on_save_checkpoint(
self, trainer: "pl.Trainer", pl_module: "pl.LightningModule", checkpoint: Dict[str, Any]
) -> None:
r"""
Called when saving a checkpoint to give you a chance to store anything else you might want to save.
Args:
trainer: the current :class:`~lightning.pytorch.trainer.Trainer` instance.
pl_module: the current :class:`~lightning.pytorch.core.module.LightningModule` instance.
checkpoint: the checkpoint dictionary that will be saved.
"""
# Since we've add bound method to optimizer and lr_scheduler, it can lead to more
# CUDA tensors passed to consumer process unexpectedly.
if "optimizer_states" in checkpoint:
for optimizer_state in checkpoint["optimizer_states"]:
for k in list(optimizer_state.keys()):
v = optimizer_state[k]
if isinstance(v, MethodType) and hasattr(v, "__self__"):
del optimizer_state[k]
if "lr_schedulers" in checkpoint:
for lr_scheduler in checkpoint["lr_schedulers"]:
for k in list(lr_scheduler.keys()):
v = lr_scheduler[k]
if isinstance(v, MethodType) and hasattr(v, "__self__"):
del lr_scheduler[k]
+484
View File
@@ -0,0 +1,484 @@
# 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.
import shutil
from abc import ABC, abstractmethod
from contextlib import contextmanager
from time import time
from typing import Any, Dict, Optional, Union
import lightning.pytorch as pl
import torch
from lightning.fabric.plugins import CheckpointIO
from lightning.fabric.utilities.cloud_io import get_filesystem
from lightning.fabric.utilities.types import _PATH
from lightning.pytorch import Callback
from lightning.pytorch.plugins.io.wrapper import _WrappingCheckpointIO
from nemo.utils import logging
try:
from megatron.core import dist_checkpointing
from megatron.core.dist_checkpointing.dict_utils import extract_matching_values
from megatron.core.dist_checkpointing.mapping import ShardedBase
from megatron.core.dist_checkpointing.serialization import (
get_default_load_sharded_strategy,
get_default_save_sharded_strategy,
)
from megatron.core.dist_checkpointing.strategies import tensorstore
from megatron.core.dist_checkpointing.strategies.async_utils import AsyncCallsQueue, AsyncRequest
from megatron.core.dist_checkpointing.strategies.base import SaveShardedStrategy
from megatron.core.dist_checkpointing.strategies.fully_parallel import (
FullyParallelLoadStrategyWrapper,
FullyParallelSaveStrategyWrapper,
)
from megatron.core.dist_checkpointing.strategies.torch import TorchDistSaveShardedStrategy
from megatron.core.dist_checkpointing.validation import StrictHandling
from megatron.core.parallel_state import get_data_parallel_group
HAVE_MEGATRON_CORE = True
except (ImportError, ModuleNotFoundError) as e:
HAVE_MEGATRON_CORE = False
IMPORT_ERROR = (
"megatron-core was not found. "
"Please see the NeMo README for installation instructions: https://github.com/NVIDIA/NeMo#megatron-gpt."
f" Exact error: {e}"
)
@contextmanager
def _debug_time(name: str):
"""Simple context manager for timing functions/code blocks."""
start = time()
try:
yield
finally:
logging.debug(f'{name} took {time() - start:.3f}s')
class AsyncCompatibleCheckpointIO(CheckpointIO, ABC):
"""CheckpointIO that can be used together with async saving.
Differs from the regular CheckpointIO only by the `save_checkpoint`
return type. The `save_checkpoint` method itself is synchronous, but returns
callbacks that can be performed asynchronously.
"""
@abstractmethod
def save_checkpoint(
self, checkpoint: Dict[str, Any], path: _PATH, storage_options: Optional[Any] = None
) -> 'AsyncRequest':
"""Interface to implement save_checkpoint and return an AsyncRequest"""
raise NotImplementedError
class AsyncFinalizableCheckpointIO(_WrappingCheckpointIO):
"""CheckpointIO wrapper for async checkpoint saving and synchronous finalization.
Runs main part of the checkpoint save in a separate process (not thread as the PTL
AsyncCheckpointIO does). Allows to perform a (synchronous) finalization
function after all ranks finish checkpoint saving.
NOTE: for correctness, this plugin must be used together with the
AsyncFinalizerCallback callback which performs the finalization checks.
Args:
checkpoint_io (CheckpointIO): wrapped checkpoint_io object. Must be
of type AsyncCompatibleCheckpointIO.
Requires the underlying checkpoint_io.save_checkpoint to return save_fn, save_args, finalize_fn.
"""
def __init__(self, checkpoint_io: AsyncCompatibleCheckpointIO) -> None:
if not HAVE_MEGATRON_CORE:
raise ImportError(IMPORT_ERROR)
if not isinstance(checkpoint_io, AsyncCompatibleCheckpointIO):
raise ValueError(f'Incompatible wrapped checkpoint_io type: {type(checkpoint_io)}')
super().__init__(checkpoint_io)
self.async_calls_queue = AsyncCallsQueue()
def save_checkpoint(
self,
checkpoint: Dict[str, Any],
path: _PATH,
storage_options: Optional[Any] = None,
) -> None:
"""Executes async request returned from the underlying checkpoint_io asynchronously.
Requires the underlying checkpoint_io.save_checkpoint to return an AsyncRequest.
It is then applied with `self.async_calls_queue` asynchronously.
Args:
checkpoint (Dict[str, Any]): checkpoint to save. Passed to underlying
checkpoint_io without modifications.
path (_PATH): path to save the checkpoint. Passed to underlying
checkpoint_io without modifications.
storage_options (Any, optional): storage control modifiers. This class
consumed the `finalize_fn` parameter (if any), which is expected to be
a callback and is appended to async finalization functions.
Applies underlying checkpoint_io finalize callback first, then the external one (postfix order).
"""
external_finalize_fn = (storage_options or {}).pop('finalize_fn', None)
assert isinstance(self.checkpoint_io, AsyncCompatibleCheckpointIO), type(self.checkpoint_io)
async_request = self.checkpoint_io.save_checkpoint(checkpoint, path, storage_options)
if external_finalize_fn is not None:
async_request.add_finalize_fn(external_finalize_fn)
call_idx = self.async_calls_queue.schedule_async_request(async_request)
logging.debug(f'Scheduled an async call #{call_idx}')
@_debug_time('AsyncFinalizableCheckpointIO.maybe_finalize_save_checkpoint')
def maybe_finalize_save_checkpoint(self, blocking: bool = False):
"""Performs checkpoint finalization (if possible).
Args:
blocking (bool, optional): if True, waits until all async saves are
completed. Otherwise, finalizes only those async calls which are
already done on all ranks. Defaults to False.
"""
if self.async_calls_queue.get_num_unfinalized_calls() == 0:
return False
start_time = time()
call_idx_finalized = self.async_calls_queue.maybe_finalize_async_calls(blocking)
if call_idx_finalized:
logging.debug(f'Finalized async calls: {[f"#{idx}" for idx in call_idx_finalized]}')
end_time = time()
logging.info(f"Async finalization time took {end_time - start_time:.3f} s")
return len(call_idx_finalized) > 0
def teardown(self) -> None:
"""Warns if there are any pending checkpoint saves."""
super().teardown()
if self.async_calls_queue.get_num_unfinalized_calls() > 0:
# Can't do finalization now because some ranks might be lost
logging.warning('Some async checkpoint saves might be not finalized properly.')
class AsyncFinalizerCallback(Callback):
"""Callback which finalizes async saves initiated by the AsyncFinalizableCheckpointIO.
Tries to perform non-blocking finalization on train_batch_end and train_epoch_end.
On train_end performs a blocking finalization of all pending checkpoints.
"""
def on_train_batch_end(self, trainer: "pl.Trainer", *args, **kwargs) -> None:
"""Override hook to finalize pending checkpoint(s) if they exist."""
self._get_checkpoint_io(trainer).maybe_finalize_save_checkpoint(blocking=False)
def on_train_epoch_end(self, trainer: "pl.Trainer", *args, **kwargs) -> None:
"""Override hook to finalize pending checkpoint(s) if they exist."""
self._get_checkpoint_io(trainer).maybe_finalize_save_checkpoint(blocking=False)
def on_train_end(self, trainer: "pl.Trainer", *args, **kwargs) -> None:
"""Override hook to finalize pending checkpoint(s) if they exist."""
checkpoint_io = self._get_checkpoint_io(trainer)
if checkpoint_io.async_calls_queue.get_num_unfinalized_calls() > 0:
logging.info('Pending async checkpoint saves. Finalizing them synchronously now')
self._get_checkpoint_io(trainer).maybe_finalize_save_checkpoint(blocking=True)
def _get_checkpoint_io(self, trainer) -> AsyncFinalizableCheckpointIO:
checkpoint_io = trainer.strategy.checkpoint_io
if not isinstance(checkpoint_io, AsyncFinalizableCheckpointIO):
raise ValueError(
f'Async finalizer requires an async compatible CheckpointIO, got: {checkpoint_io.__class__}'
)
return checkpoint_io
class DistributedCheckpointIO(AsyncCompatibleCheckpointIO):
"""CheckpointIO for a distributed checkpoint format.
Args:
save_ckpt_format (str): Distributed checkpoint format to use for checkpoint saving.
load_directly_on_device (bool, optional): if True, loads the weights directly
on GPU. Has effect only for `zarr` based checkpoints (PyT Distributed
always loads on device). Defaults to True.
load_strictness (StrictHandling, optional): defines loading strictness.
If not None, overwrites the `strict` flag passed to `load_checkpoint`.
Defaults to None.
async_save (bool): whether to save asynchronously. Should be set to True if
this class will be wrapped with AsyncFinalizableCheckpointIO.
torch_dist_multiproc (int, optional): number of extra processes per rank
used during ckpt save with PyTorch distributed format. Defaults, to None
which means using an MCore default (2).
parallel_save (bool): parallelizes the save across ranks. Defaults to True
parallel_load (bool): parallelizes the load across ranks (followed by params all gather).
Defaults to False due to some extra memory usage requirement.
"""
def __init__(
self,
save_ckpt_format: str,
load_directly_on_device: bool = True,
load_strictness: Optional['StrictHandling'] = None,
async_save: bool = False,
torch_dist_multiproc: Optional[int] = None,
assume_constant_structure: bool = False,
parallel_save: bool = False,
parallel_save_within_dp: bool = False,
parallel_load: bool = False,
):
super().__init__()
if not HAVE_MEGATRON_CORE:
raise ImportError(IMPORT_ERROR)
self.save_ckpt_format = save_ckpt_format
self.load_directly_on_device = load_directly_on_device
self.load_strictness = load_strictness
self.async_save = async_save
self.torch_dist_multiproc = torch_dist_multiproc
self.assume_constant_structure = assume_constant_structure
self.parallel_save = parallel_save
self.parallel_save_within_dp = parallel_save_within_dp
self.parallel_load = parallel_load
self._save_sharded_strategy = None
self.validated_consistency = False
@classmethod
def from_config(cls, model_cfg: dict, async_save: bool = False):
"""Instantiates a DistributedCheckpointIO from a config dict.
Args:
model_cfg (dict): model config dict. Most of the configuration
is extracted from this config.
async_save (bool, optional): async_save flag is not part of the model config,
it should be provided separately. Defaults to False.
"""
return cls(
save_ckpt_format=model_cfg.get('dist_ckpt_format', 'torch_dist'),
load_directly_on_device=model_cfg.get('dist_ckpt_load_on_device', True),
load_strictness=model_cfg.get('dist_ckpt_load_strictness', None),
async_save=async_save,
torch_dist_multiproc=model_cfg.get('dist_ckpt_torch_dist_multiproc', None),
parallel_save=model_cfg.get('dist_ckpt_parallel_save', False),
parallel_save_within_dp=model_cfg.get('dist_ckpt_parallel_save_within_dp', False),
parallel_load=model_cfg.get('dist_ckpt_parallel_load', False),
)
@_debug_time('DistributedCheckpointIO.save_checkpoint')
def save_checkpoint(
self, checkpoint: Dict[str, Any], path: _PATH, storage_options: Optional[Any] = None
) -> Optional['AsyncRequest']:
"""Saves a distributed checkpoint. Creates the checkpoint root directory if doesn't exist.
Args:
checkpoint (Dict[str, Any]): sharded state dict to save
path (_PATH): checkpoint directory
storage_options (Any, optional): Optional parameters when saving the checkpoint
"""
fs = get_filesystem(path)
fs.makedirs(path, exist_ok=True)
validate_sharding_integrity = not (self.validated_consistency and self.assume_constant_structure)
self.validated_consistency = True
rank = torch.distributed.get_rank()
iteration = _get_iteration_from_checkpoint(checkpoint)
start_time = time()
async_save_request = dist_checkpointing.save(
sharded_state_dict=checkpoint,
checkpoint_dir=path,
sharded_strategy=self.save_sharded_strategy,
validate_access_integrity=validate_sharding_integrity,
async_sharded_save=self.async_save,
)
end_time = time()
log_parts = (
"Global Checkpoint Save",
f"Rank: {rank}",
f"Iteration: {iteration}" if iteration is not None else None,
f"Start time: {start_time:.3f}s",
f"Save duration: {end_time - start_time:.3f}s",
)
log_message = " : ".join(part for part in log_parts if part is not None)
logging.info(log_message)
def iter_finalize_fn():
logging.info(f'Successfully saved checkpoint from iteration {int(iteration):7d} to {path}')
if self.async_save:
assert async_save_request is not None
async_save_request.add_finalize_fn(iter_finalize_fn)
return async_save_request
@_debug_time('DistributedCheckpointIO.load_checkpoint')
def load_checkpoint(
self,
path: _PATH,
map_location: Optional[Any] = None,
sharded_state_dict: Dict[str, Any] = None,
strict: Union[None, bool, 'StrictHandling'] = None,
validate_access_integrity: Optional[bool] = True,
) -> Dict[str, Any]:
"""Loads a distributed checkpoint.
Args:
path (_PATH): checkpoint directory
map_location (Any, optional): required to be None in this implementation
sharded_state_dict (Dict[str, Any], optional): state dict which
defines the loading procedure for the distributed checkpoint.
Defaults to None to comply with the CheckpointIO interface,
but it's a required argument.
strict (bool, StrictHandling, optional): adjust load strictness. bool value
is translated to StrictHandling instance. Gets overwritten by
`self.load_strictness`. Defaults to None. If `self.load_strictness`
is also None, strict becomes StrictHandling.ASSUME_OK_UNEXPECTED.
Returns:
Dist[str, Any]: loaded checkpoint.
"""
if sharded_state_dict is None:
raise ValueError('DistributedCheckpointIO requires passing sharded_state_dict argument to load_checkpoint')
if map_location is not None:
raise ValueError('DistributedCheckpointIO doesnt handle map_location argument')
if self.save_ckpt_format == 'zarr' and self.load_directly_on_device:
sharded_strategy = tensorstore.TensorStoreLoadShardedStrategy(load_directly_on_device=True)
else:
sharded_strategy = None
if self.parallel_load:
if sharded_strategy is None:
sharded_strategy = get_default_load_sharded_strategy(path)
sharded_strategy = FullyParallelLoadStrategyWrapper(
sharded_strategy, get_data_parallel_group(with_context_parallel=True)
)
if sharded_strategy is not None:
logging.info(f'Using {sharded_strategy} dist-ckpt load strategy.')
if isinstance(strict, bool):
# For backward-compatibility reasons and a bug in MCore (strict check not applied to factories)
# we must apply a simple strict check here.
if not strict:
sharded_state_dict = self.adjust_non_strict_load(path, sharded_state_dict)
strict = StrictHandling.ASSUME_OK_UNEXPECTED if strict else StrictHandling.LOG_ALL
if self.load_strictness is not None:
# Overwrites function argument
strict = self.load_strictness
if strict is None:
# Default behavior
strict = StrictHandling.ASSUME_OK_UNEXPECTED
logging.debug(f'Dist ckpt load strictness: {strict}')
start_time = time()
ret = dist_checkpointing.load(
sharded_state_dict=sharded_state_dict,
checkpoint_dir=path,
sharded_strategy=sharded_strategy,
validate_access_integrity=validate_access_integrity,
strict=strict,
)
end_time = time()
duration = end_time - start_time
logging.info(
"Global Checkpoint Load : "
f"Rank : {torch.distributed.get_rank()} : "
f"Start time : {start_time:.3f}s : "
f"Time spent in load_checkpoint: {duration:.3f}s"
)
return ret
def adjust_non_strict_load(self, path: _PATH, sharded_state_dict: Dict[str, Any]):
"""Remove unexpected keys from being loaded into the state dict."""
ckpt_sharded_metadata = dist_checkpointing.load_tensors_metadata(path)
loaded_keys = []
unexpected_keys = []
def should_remove_missing_sharded_base(x: Any):
if isinstance(x, ShardedBase):
if x.key in ckpt_sharded_metadata:
loaded_keys.append(x.key)
return False
else:
unexpected_keys.append(x.key)
return True
return False
_, sharded_state_dict = extract_matching_values(sharded_state_dict, should_remove_missing_sharded_base)
logging.info(f'The following keys are not in the checkpoint and will not be loaded: {unexpected_keys}')
# TODO: compute missing_keys by:
# 1. all_gather_object of loaded_keys
# 2. missing_keys = ckpt_sharded_metadata.keys() - loaded_keys
return sharded_state_dict
@_debug_time('DistributedCheckpointIO.remove_checkpoint')
def remove_checkpoint(self, path: _PATH) -> None:
"""Remove a distributed checkpoint.
Due to potentially large number of files, the implementation remove the whole directory at once.
"""
shutil.rmtree(path, ignore_errors=True)
@property
def save_sharded_strategy(self) -> 'SaveShardedStrategy':
"""Conditionally initialize and get the sharded strategy to use for saving."""
if self._save_sharded_strategy is None:
self._save_sharded_strategy = self._determine_dist_ckpt_save_strategy()
return self._save_sharded_strategy
def _determine_dist_ckpt_save_strategy(self):
"""Determine the saving strategy based on constructor args.
Relies on the default MCore strategy unless extra PyT Distributed format arguments
are passed in config or in case of a fully parallel save in which case
a parallelization wrapper is applied.
"""
if self.save_ckpt_format == 'zarr':
logging.warning(
'`zarr` distributed checkpoint backend is deprecated.'
' Distributed optimizer checkpoint saving might be extremely slow.'
' Please switch to PyTorch Distributed format (model.dist_ckpt_format=torch_dist).'
)
if self.async_save and self.save_ckpt_format != 'torch_dist':
raise ValueError('Async dist-ckpt save supported only for torch_dist format')
torch_dist_kwargs = {} if self.torch_dist_multiproc is None else dict(thread_count=self.torch_dist_multiproc)
if self.save_ckpt_format == 'torch_dist' and torch_dist_kwargs:
save_strategy = TorchDistSaveShardedStrategy(self.save_ckpt_format, 1, **torch_dist_kwargs)
else:
save_strategy = get_default_save_sharded_strategy(self.save_ckpt_format, 1)
# MCore v0.8 introduces `use_cached_ckpt_structure` attribute
if hasattr(save_strategy, 'use_cached_ckpt_structure'):
save_strategy.use_cached_ckpt_structure = self.assume_constant_structure
if self.parallel_save:
parallelization_group = (
get_data_parallel_group(with_context_parallel=True) if self.parallel_save_within_dp else None
)
save_strategy = FullyParallelSaveStrategyWrapper(
save_strategy, parallelization_group, self.assume_constant_structure
)
logging.info(f'Using {save_strategy} dist-ckpt save strategy.')
return save_strategy
def _get_iteration_from_checkpoint(checkpoint: Dict[str, Any]) -> Optional[int]:
return (
checkpoint.get("loops", {})
.get("fit_loop", {})
.get("epoch_loop.batch_progress", {})
.get("total", {})
.get("completed", None)
)
@@ -0,0 +1,740 @@
# Copyright (c) 2023, 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.
import os
import re
import shutil
import time
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Union
import torch
from _weakref import proxy
from lightning.fabric.utilities.cloud_io import get_filesystem
from lightning.pytorch.callbacks.model_checkpoint import ModelCheckpoint, _is_local_file_protocol
from lightning.pytorch.trainer import call
from lightning.pytorch.utilities import rank_zero_info
from nemo.collections.common.callbacks import EMA
from nemo.utils import logging
from nemo.utils.app_state import AppState
from nemo.utils.callbacks.dist_ckpt_io import AsyncFinalizableCheckpointIO
from nemo.utils.get_rank import is_global_rank_zero
from nemo.utils.model_utils import ckpt_to_dir, inject_model_parallel_rank, uninject_model_parallel_rank
from nemo.utils.msc_utils import import_multistorageclient, is_multistorageclient_url
class NeMoModelCheckpoint(ModelCheckpoint):
"""Light wrapper around Lightning's ModelCheckpoint to force a saved checkpoint on train_end.
Extends Lightning's on_save_checkpoint func to save the .nemo file. Saves the .nemo file based
on the best checkpoint saved (according to the monitor value).
Also contains func to save the EMA copy of the model.
"""
UNFINISHED_CHECKPOINT_SUFFIX = "-unfinished"
def __init__(
self,
always_save_nemo: bool = False,
save_nemo_on_train_end: bool = True,
save_best_model: bool = False,
postfix: str = ".nemo",
n_resume: bool = False,
model_parallel_size: int = None,
async_save: bool = False, # controls only finalize callbacks
save_last_n_optim_states: int = -1,
**kwargs,
):
# Parse and store "extended" parameters: save_best model and postfix.
self.always_save_nemo = always_save_nemo
self.save_nemo_on_train_end = save_nemo_on_train_end
self.save_best_model = save_best_model
self.save_last_n_optim_states = save_last_n_optim_states
if self.save_best_model and not self.save_nemo_on_train_end:
logging.warning(
(
"Found save_best_model is True and save_nemo_on_train_end is False. "
"Set save_nemo_on_train_end to True to automatically save the best model."
)
)
self.postfix = postfix
self.previous_best_path = ""
self.model_parallel_size = model_parallel_size
self.async_save = async_save
self.async_finalize_cb = None
# Checkpoints which removal is deferred until async save is done.
# Each element of `deferred_ckpts_to_remove` is a growing list
# that `self._remove_checkpoint` adds to. Once `self._save_checkpoint`
# is called, the last element is frozen and a new element is added.
self.deferred_ckpts_to_remove: List[List[str]] = []
# `prefix` is deprecated
if 'prefix' in kwargs:
self.prefix = kwargs.pop('prefix')
else:
self.prefix = ""
# Call the parent class constructor with the remaining kwargs.
super().__init__(**kwargs)
if self.save_top_k != -1 and n_resume:
logging.debug("Checking previous runs")
self.nemo_topk_check_previous_run()
def nemo_topk_check_previous_run(self):
"""
Check if there are previous runs.
"""
try:
self.best_k_models
self.kth_best_model_path
self.best_model_score
self.best_model_path
except AttributeError:
raise AttributeError("Lightning's ModelCheckpoint was updated. NeMoModelCheckpoint will need an update.")
self.best_k_models = {}
self.kth_best_model_path = ""
self.best_model_score = None
self.best_model_path = ""
checkpoints = list(path for path in self._saved_checkpoint_paths if not self._is_ema_filepath(path))
for checkpoint in checkpoints:
if 'mp_rank' in str(checkpoint) or 'tp_rank' in str(checkpoint):
checkpoint = uninject_model_parallel_rank(checkpoint)
checkpoint = str(checkpoint)
# second case is for distributed checkpoints, since they are a directory there's no extension
if checkpoint[-10:] == '-last.ckpt' or checkpoint[-5:] == '-last':
continue
index = checkpoint.find(self.monitor) + len(self.monitor) + 1 # Find monitor in str + 1 for '='
if index != len(self.monitor):
match = re.search('[A-z]', checkpoint[index:])
if match:
value = checkpoint[index : index + match.start() - 1] # -1 due to separator hypen
self.best_k_models[checkpoint] = float(value)
if len(self.best_k_models) < 1:
return # No saved checkpoints yet
_reverse = False if self.mode == "min" else True
best_k_models = sorted(self.best_k_models, key=self.best_k_models.get, reverse=_reverse)
# This section should be ok as rank zero will delete all excess checkpoints, since all other ranks are
# instantiated after rank zero. models_to_delete should be 0 for all other ranks.
if self.model_parallel_size is not None:
# check for distributed checkpoint
if checkpoints[0].is_dir():
models_to_delete = len(best_k_models) - self.save_top_k
else:
models_to_delete = len(best_k_models) - self.model_parallel_size * self.save_top_k
else:
models_to_delete = len(best_k_models) - self.save_top_k
models_to_delete = max(0, models_to_delete)
logging.debug(f'Number of models to delete: {models_to_delete}')
# If EMA enabled, delete the additional EMA weights
ema_enabled = self._has_ema_ckpts(self._saved_checkpoint_paths)
for _ in range(models_to_delete):
model = best_k_models.pop(-1)
self.best_k_models.pop(model)
self._del_model_without_trainer(model)
if ema_enabled and self._fs.exists(self._ema_format_filepath(model)):
self._del_model_without_trainer(self._ema_format_filepath(model))
logging.debug(f"Removed checkpoint: {model}")
self.kth_best_model_path = best_k_models[-1]
self.best_model_path = best_k_models[0]
self.best_model_score = self.best_k_models[self.best_model_path]
def _remove_invalid_entries_from_topk(self):
# Removes invalid (incomplete or not existing) checkpoints from topk checkpoints.
# This might be needed if the checkpointing was abruptly terminated.
def __is_ckpt_ok(ckpt_path: str) -> bool:
exists = (
os.path.isfile(ckpt_path)
or os.path.isfile(inject_model_parallel_rank(ckpt_path))
or os.path.isdir(ckpt_path.removesuffix('.ckpt'))
)
return exists and not self.is_checkpoint_unfinished(ckpt_path)
self.best_k_models = {k: v for k, v in self.best_k_models.items() if __is_ckpt_ok(k)}
if len(self.best_k_models) > 0:
reverse_arr = self.mode != "min"
best_k_models_arr = sorted(self.best_k_models, key=self.best_k_models.get, reverse=reverse_arr)
self.kth_best_model_path = best_k_models_arr[-1]
self.kth_value = self.best_k_models[self.kth_best_model_path]
self.best_model_path = best_k_models_arr[0]
self.best_model_score = self.best_k_models[self.best_model_path]
else:
self.kth_best_model_path = ""
self.kth_value = None
self.best_model_path = ""
self.best_model_score = None
def load_state_dict(self, state_dict: Dict[str, Any]) -> None:
"""
Load the state dict.
"""
super().load_state_dict(state_dict)
self._remove_invalid_entries_from_topk()
def setup(self, trainer, pl_module, stage: str) -> None:
"""
Setup the checkpoint.
"""
if is_global_rank_zero():
logging.debug("Removing unfinished checkpoints if any...")
NeMoModelCheckpoint._remove_unfinished_checkpoints(self.dirpath)
# Ensure that all ranks continue with unfinished checkpoints removed
if torch.distributed.is_initialized():
torch.distributed.barrier()
super().setup(trainer, pl_module, stage)
# When using S3 checkpointing, only Rank 0 has the checkpoint and model path set in exp_manager.
# Sync the values across all ranks to ensure consistency.
path = trainer.strategy.broadcast(trainer.ckpt_path)
trainer.ckpt_path = path
self.last_model_path = trainer.strategy.broadcast(self.last_model_path)
def on_save_checkpoint(self, trainer, pl_module, checkpoint):
"""
Save the checkpoint.
"""
output = super().on_save_checkpoint(trainer, pl_module, checkpoint)
if not self.always_save_nemo:
return output
# Load the best model and then re-save it
app_state = AppState()
if app_state.model_parallel_size is not None and app_state.model_parallel_size > 1:
logging.warning('always_save_nemo will slow down training for model_parallel > 1.')
# since we are creating tarfile artifacts we need to update .nemo path
app_state.model_restore_path = self._format_nemo_checkpoint_name()
if app_state.model_parallel_size is not None and app_state.model_parallel_size > 1:
maybe_injected_best_model_path = inject_model_parallel_rank(self.best_model_path)
else:
maybe_injected_best_model_path = self.best_model_path
if self.save_best_model:
if not os.path.exists(maybe_injected_best_model_path):
return
if self.best_model_path == self.previous_best_path:
logging.debug('Best model has not changed, skipping save.')
return output
self.previous_best_path = self.best_model_path
if torch.distributed.is_initialized():
torch.distributed.barrier()
backup_path = self._backup_existing_nemo_ckpt(trainer)
pl_module.save_to(save_path=app_state.model_restore_path)
logging.info(f"New best .nemo model saved to: {app_state.model_restore_path}")
else:
if torch.distributed.is_initialized():
torch.distributed.barrier()
backup_path = self._backup_existing_nemo_ckpt(trainer)
pl_module.save_to(save_path=app_state.model_restore_path)
logging.info(f"New .nemo model saved to: {app_state.model_restore_path}")
if backup_path is not None and is_global_rank_zero():
logging.info(f'Removing old .nemo backup {backup_path}')
get_filesystem(backup_path).rm(backup_path)
return output
def on_train_end(self, trainer, pl_module):
"""
Save the checkpoint on train end.
"""
if trainer.fast_dev_run:
return None
# check if we need to save a last checkpoint manually as validation isn't always run based on the interval
if self.save_last and trainer.val_check_interval != 0:
should_save_last_checkpoint = False
if isinstance(trainer.val_check_interval, float) and trainer.val_check_interval % trainer.global_step != 0:
should_save_last_checkpoint = True
if isinstance(trainer.val_check_interval, int) and trainer.global_step % trainer.val_check_interval != 0:
should_save_last_checkpoint = True
if should_save_last_checkpoint:
monitor_candidates = self._monitor_candidates(trainer)
if self.last_model_path == self.format_checkpoint_name(monitor_candidates, self.CHECKPOINT_NAME_LAST):
logging.debug(f'Last checkpoint {self.last_model_path} already saved')
else:
super()._save_last_checkpoint(trainer, monitor_candidates)
# Call parent on_train_end() to save the -last checkpoint
super().on_train_end(trainer, pl_module)
# Load the best model and then re-save it
if self.save_best_model:
# wait for all processes
trainer.strategy.barrier("SaveBestCheckpointConnector.resume_end")
if self.best_model_path == "":
logging.warning(
f"{self} was told to save the best checkpoint at the end of training, but no saved checkpoints "
"were found. Saving latest model instead."
)
else:
if os.path.isdir(self.best_model_path.split('.ckpt')[0]):
self.best_model_path = self.best_model_path.split('.ckpt')[0]
self.best_model_path = trainer.strategy.broadcast(self.best_model_path)
trainer._checkpoint_connector.restore(self.best_model_path)
if self.save_nemo_on_train_end:
save_to = getattr(pl_module, "save_to", None)
if not callable(save_to):
logging.warning(
f"{type(pl_module).__name__} does not implement save_to(); "
"skipping automatic .nemo export at train end."
)
return
backup_path = self._backup_existing_nemo_ckpt(trainer)
save_to(save_path=self._format_nemo_checkpoint_name())
if backup_path is not None and is_global_rank_zero():
logging.info(f'Removing old .nemo backup {backup_path}')
get_filesystem(backup_path).rm(backup_path)
def _backup_existing_nemo_ckpt(self, trainer) -> Optional[str]:
"""Search for an available name with version infix and rename existing checkpoint.
NOTE: this behavior is slightly different from regular checkpoints.
PTL creates new regular checkpoint with the first available name.
Here, for backward compatibility, we create .nemo checkpoint as before
and create a backup under the first available name.
Args:
trainer (Trainer): trainer instance.
Returns:
Path to the backup checkpoint or None, if no backup was created
"""
base_path = self._format_nemo_checkpoint_name()
available_path = base_path
if self._enable_version_counter:
version_cnt = self.STARTING_VERSION
while self.file_exists(available_path, trainer, check_dist_ckpt=False):
available_path = self._format_nemo_checkpoint_name(version_cnt)
version_cnt += 1
if available_path == base_path:
# no existing ckpt, no need to backup
return None
if trainer.is_global_zero:
logging.info(f'{base_path} already exists, moving existing checkpoint to {available_path}')
if is_multistorageclient_url(base_path):
# TODO: multistorageclient doesn't have "rename" function, therefore no-op but we should
# refactor this once multistorageclient have rename function supported.
pass
else:
shutil.move(base_path, available_path)
trainer.strategy.barrier()
return available_path
def _format_nemo_checkpoint_name(self, ver: Optional[int] = None) -> str:
version_infix = '' if ver is None else f'{self.CHECKPOINT_JOIN_CHAR}v{ver}'
if is_multistorageclient_url(self.dirpath):
return f"{self.dirpath}/{self.prefix + version_infix + self.postfix}"
return os.path.abspath(
os.path.expanduser(os.path.join(self.dirpath, self.prefix + version_infix + self.postfix))
)
def _del_model_without_trainer(self, filepath: str) -> None:
filepath = Path(filepath)
# check if filepath is a distributed a checkpoint
if ckpt_to_dir(filepath).is_dir():
if is_global_rank_zero():
try:
dist_ckpt = ckpt_to_dir(filepath)
shutil.rmtree(dist_ckpt, ignore_errors=True)
logging.info(f"Removed distributed checkpoint: {dist_ckpt}")
except:
logging.info(f"Tried to remove distributed checkpoint: {dist_ckpt} but failed.")
else:
app_state = AppState()
# legacy model parallel checkpoint
if app_state.model_parallel_size is not None and app_state.model_parallel_size > 1:
# filepath needs to be updated to include mp_rank
filepath = inject_model_parallel_rank(filepath)
# each model parallel rank needs to remove its model
if is_global_rank_zero() or (
app_state.model_parallel_size is not None and app_state.data_parallel_rank == 0
):
try:
self._fs.rm(filepath)
logging.info(f"Removed checkpoint: {filepath}")
except:
logging.info(f"Tried to remove checkpoint: {filepath} but failed.")
def _ema_callback(self, trainer: 'lightning.pytorch.Trainer') -> Optional[EMA]: # noqa: F821
ema_callback = None
for callback in trainer.callbacks:
if isinstance(callback, EMA):
ema_callback = callback
return ema_callback
def _drop_optimizer_states(self, trainer, filepath: Union[str, Path], storage_options: Optional[Any]) -> None:
# Get list of saved checkpoints
checkpoints = self._get_checkpoints_list(filepath)
suffix = "-no-optim"
# Drop optimizer states
checkpoint_index = len(checkpoints) - self.save_last_n_optim_states - 1
if len(checkpoints) > self.save_last_n_optim_states:
checkpoint_path = checkpoints[checkpoint_index]
logging.info(f"Loading '{checkpoint_path}' checkpoint to drop optimizer states...")
checkpoint = trainer.strategy.load_checkpoint(checkpoint_path=checkpoint_path, load_optimizer_states=False)
# Load related state dict
self._load_current_state_dict(trainer, checkpoint)
# Save the checkpoint without optimizer states
if storage_options is None:
storage_options = dict(include_optimizer=False)
else:
storage_options["include_optimizer"] = False
trainer.save_checkpoint(
f"{checkpoint_path}{suffix}.ckpt", self.save_weights_only, storage_options=storage_options
)
# Remove the checkpoint version with optimizer states
if is_global_rank_zero():
trainer.strategy.remove_checkpoint(checkpoint_path)
shutil.move(f"{checkpoint_path}{suffix}", checkpoint_path)
if torch.distributed.is_initialized():
torch.distributed.barrier()
# Load the correct state_dict for current checkpoint.
# Temporary solution.
checkpoint = trainer.strategy.load_checkpoint(
checkpoint_path=ckpt_to_dir(filepath), load_optimizer_states=False
)
self._load_current_state_dict(trainer, checkpoint)
logging.info(f"Successfully dropped optimizer states for '{checkpoint_path}' checkpoint.")
def _get_checkpoints_list(self, filepath: Union[str, Path]) -> List[str]:
# Get a checkpoints directory
checkpoints_dir = os.path.dirname(filepath)
# Get a list of saved checkpoints
checkpoints = [
d
for d in os.listdir(checkpoints_dir)
if os.path.isdir(os.path.join(checkpoints_dir, d)) and '-last' not in d
]
checkpoints = sorted(checkpoints, key=lambda x: int(x.split('-step=')[1].split('-')[0]))
checkpoints = [os.path.join(checkpoints_dir, checkpoint) for checkpoint in checkpoints]
return checkpoints
def _load_current_state_dict(self, trainer, checkpoint) -> None:
# Temporary solution for loading the correct state dict
# when dropping optimizer states "on the fly" during training.
# TODO @dimapihtar @mikolajblaz: provide a more elegant solution at the mcore level.
call._call_lightning_module_hook(trainer, "on_load_checkpoint", checkpoint)
# Load model state_dict
trainer.strategy.load_model_state_dict(
checkpoint,
strict=trainer.lightning_module.strict_loading,
)
@staticmethod
def format_checkpoint_unfinished_marker_path(checkpoint_path: Union[Path, str]) -> Path:
"""Format the path to the unfinished checkpoint marker file.
If the marker file exists, corresponding checkpoint is considered unfinished/incomplete.
NOTE: Marker path for the EMA checkpoint part is the same as for the original checkpoint.
Args:
checkpoint_path: Path to the checkpoint file or dir.
Does not need to exist.
Returns:
Path to the unfinished checkpoint marker file.
"""
marker_filepath = str(uninject_model_parallel_rank(checkpoint_path))
marker_filepath = marker_filepath.removesuffix(".nemo")
marker_filepath = marker_filepath.removesuffix(".ckpt")
marker_filepath = marker_filepath.removesuffix("-EMA")
return Path(marker_filepath + NeMoModelCheckpoint.UNFINISHED_CHECKPOINT_SUFFIX)
@staticmethod
def is_checkpoint_unfinished(checkpoint_path: Union[Path, str]) -> bool:
"""Check if the checkpoint is unfinished.
Args:
checkpoint_path: Path to the checkpoint file or dir.
Does not need to exist.
Returns:
True if the checkpoint is unfinished, False otherwise.
"""
return NeMoModelCheckpoint.format_checkpoint_unfinished_marker_path(checkpoint_path).exists()
@staticmethod
def set_checkpoint_unfinished_marker(checkpoint_path: Union[Path, str], barrier_after=False) -> None:
"""Marks given checkpoint as unfinished.
Args:
checkpoint_filepath: Path to the checkpoint file or dir.
Does not need to exist.
barrier_after: Synchronize ranks after writing the marker file.
Defaults to False.
"""
if is_global_rank_zero():
marker_path = NeMoModelCheckpoint.format_checkpoint_unfinished_marker_path(checkpoint_path)
marker_path.parent.mkdir(parents=True, exist_ok=True)
marker_path.touch()
if barrier_after and torch.distributed.is_initialized():
torch.distributed.barrier()
@staticmethod
def remove_checkpoint_unfinished_marker(checkpoint_path: Union[Path, str], barrier_before=False) -> None:
"""Clear unfinished marker for given checkpoint.
Args:
checkpoint_path: Path to the checkpoint file or dir.
Does not need to exist.
barrier_before: Synchronize ranks before removing the marker file.
Defaults to False.
"""
try:
if barrier_before and torch.distributed.is_initialized():
torch.distributed.barrier()
if is_global_rank_zero():
marker_path = NeMoModelCheckpoint.format_checkpoint_unfinished_marker_path(checkpoint_path)
if marker_path.exists():
marker_path.unlink()
except:
return
def file_exists(
self, filepath: str, trainer: "lightning.pytorch.Trainer", check_dist_ckpt: bool = True # noqa: F821
) -> bool:
"""Checks if a file or a file without a suffix (distributed checkpoint) exists."""
if is_multistorageclient_url(filepath):
exists = self._fs.exists(filepath)
else:
exists = self._fs.exists(filepath) or (check_dist_ckpt and self._fs.exists(ckpt_to_dir(filepath)))
return trainer.strategy.broadcast(exists)
def _save_checkpoint(self, trainer: 'lightning.pytorch.Trainer', filepath: str) -> None: # noqa: F821
# barrier_after=True, so all ranks continue after the unfinished checkpoint marker is placed.
# if anything goes wrong during checkpointing, we should be able to detect that data is incomplete.
self.set_checkpoint_unfinished_marker(filepath, barrier_after=True)
ema_callback = self._ema_callback(trainer)
if ema_callback is not None:
if self.async_save:
raise ValueError('async_save with EMA not supported')
with ema_callback.save_original_optimizer_state(trainer):
super()._save_checkpoint(trainer, filepath)
# save EMA copy of the model as well.
with ema_callback.save_ema_model(trainer):
filepath = self._ema_format_filepath(filepath)
if self.verbose:
rank_zero_info(f"Saving EMA weights to separate checkpoint {filepath}")
super()._save_checkpoint(trainer, filepath)
self.remove_checkpoint_unfinished_marker(filepath, barrier_before=True)
else:
# Async save passed the finalization function to checkpoint_io,
# sync save calls the finalization function immediately after save.
finalize_fn = self._get_finalize_save_checkpoint_callback(trainer, filepath, trainer.global_step)
if self.async_save:
checkpoint_io = trainer.strategy.checkpoint_io
if not isinstance(checkpoint_io, AsyncFinalizableCheckpointIO):
raise ValueError('Async save requires async compatible CheckpointIO')
storage_options = dict(finalize_fn=finalize_fn)
# Each upcoming ckpt removal request will be executed as part of this save finalization
self.deferred_ckpts_to_remove.append([])
else:
storage_options = None
logging.info(f'Checkpoint save for step {trainer.global_step} started at {time.time()}.')
trainer.save_checkpoint(filepath, self.save_weights_only, storage_options=storage_options)
if self.async_save:
logging.info(f'Scheduled async checkpoint save for {filepath}')
else:
finalize_fn()
if self.save_last_n_optim_states >= 0 and '-last' in filepath:
self._drop_optimizer_states(trainer, filepath, storage_options)
def _get_finalize_save_checkpoint_callback(
self, trainer: 'lightning.pytorch.Trainer', filepath: str, global_step: int # noqa: F821
):
"""Creates a callback that can be used to finalize async (and sync) ckpt saves."""
def _cb():
logging.debug(f'Finalize callback called for step {global_step}, filepath {filepath}')
self._last_global_step_saved = global_step
self._last_checkpoint_saved = filepath
# notify loggers
if trainer.is_global_zero:
for logger in trainer.loggers:
logger.after_save_checkpoint(proxy(self))
# barrier_before=True, so all ranks synchronize before removing the unfinished checkpoint marker
# we don't want to remove the marker until all checkpointing is done.
self.remove_checkpoint_unfinished_marker(filepath, barrier_before=True)
if not self.async_save:
return
logging.info(
f'Async checkpoint save for step {global_step} ({filepath}) finalized successfully at {time.time()}.'
)
# Remove checkpoints marked for removal by `self._remove_checkpoint`
# For each finalization there is exactly one entry in self.deferred_ckpts_to_remove
assert self.deferred_ckpts_to_remove
ckpts_to_remove = self.deferred_ckpts_to_remove.pop(0)
logging.debug(f'Checkpoints to remove: {ckpts_to_remove}')
for ckpt_to_remove in ckpts_to_remove:
self._remove_checkpoint(trainer, ckpt_to_remove, override_async=True)
return _cb
def _remove_checkpoint(
self, trainer: "lightning.pytorch.Trainer", filepath: str, override_async=False # noqa: F821
) -> None:
"""Performs checkpoint removal or deferred removal.
With async save, `self._remove_checkpoint` is called before the checkpoint
is actually finished so we can't remove it. Instead we add it to
`self.deferred_ckpts_to_remove` for future removal.
"""
if self.async_save and not override_async:
# Register checkpoint removal in the last (active) checkpoint removal list
self.deferred_ckpts_to_remove[-1].append(filepath)
return
# barrier_after=True, so all ranks continue after the unfinished checkpoint marker is placed.
# if anything goes wrong during removal, we should be able to detect that data is incomplete.
self.set_checkpoint_unfinished_marker(filepath, barrier_after=True)
super()._remove_checkpoint(trainer, filepath)
ema_callback = self._ema_callback(trainer)
if ema_callback is not None:
# remove EMA copy of the state dict as well.
filepath = self._ema_format_filepath(filepath)
super()._remove_checkpoint(trainer, filepath)
# barrier_before=True, so all ranks synchronize before removing the unfinished checkpoint marker
# we don't want to remove the marker until the checkpoint is actually removed.
self.remove_checkpoint_unfinished_marker(filepath, barrier_before=True)
def _ema_format_filepath(self, filepath: str) -> str:
return filepath.replace(self.FILE_EXTENSION, f'-EMA{self.FILE_EXTENSION}')
def _has_ema_ckpts(self, checkpoints: Iterable[Path]) -> bool:
return any(self._is_ema_filepath(checkpoint_path) for checkpoint_path in checkpoints)
def _is_ema_filepath(self, filepath: Union[Path, str]) -> bool:
return str(filepath).endswith(f'-EMA{self.FILE_EXTENSION}')
@property
def _saved_checkpoint_paths(self) -> Iterable[Path]:
# distributed checkpoints are directories so we check for them here
# we filter out unfinished checkpoints, these should be deleted during next cleanup
if is_multistorageclient_url(self.dirpath):
msc = import_multistorageclient()
return msc.glob(f"{self.dirpath}/*.ckpt")
else:
dist_checkpoints = [d for d in Path(self.dirpath).glob("*") if d.is_dir()]
if dist_checkpoints:
return filter(lambda p: not self.is_checkpoint_unfinished(p), dist_checkpoints)
else:
checkpoint_files = [f for f in Path(self.dirpath).rglob("*.ckpt")]
return filter(lambda p: not self.is_checkpoint_unfinished(p), checkpoint_files)
@staticmethod
def _remove_unfinished_checkpoints(checkpoint_dir: Union[Path, str]) -> None:
# Delete unfinished checkpoints from the filesystems.
# "Unfinished marker" files are removed as well.
if not is_global_rank_zero():
raise AssertionError("_remove_unfinished_checkpoints should run only on rank 0")
if is_multistorageclient_url(checkpoint_dir):
msc = import_multistorageclient()
existing_marker_filepaths = msc.glob(
f"{checkpoint_dir}*{NeMoModelCheckpoint.UNFINISHED_CHECKPOINT_SUFFIX}"
)
fs = get_filesystem(checkpoint_dir)
for ckpt_filepath in existing_marker_filepaths:
fs.rm(ckpt_filepath)
else:
checkpoint_dir = Path(checkpoint_dir)
existing_marker_filepaths = {
f.resolve()
for f in checkpoint_dir.glob(f"*{NeMoModelCheckpoint.UNFINISHED_CHECKPOINT_SUFFIX}")
if f.is_file()
}
checkpoint_filepaths = {f.resolve() for f in checkpoint_dir.rglob("*.ckpt") if f.is_file()}
for ckpt_filepath in checkpoint_filepaths:
possible_marker_path = NeMoModelCheckpoint.format_checkpoint_unfinished_marker_path(ckpt_filepath)
if possible_marker_path in existing_marker_filepaths:
logging.warning(f'Removing unfinished checkpoint: {ckpt_filepath}')
os.remove(ckpt_filepath)
# some directories might be distributed checkpoints, we remove these if they have a unfinished marker
all_dirpaths = {d.resolve() for d in checkpoint_dir.glob("*") if d.is_dir()}
for ckpt_dirpath in all_dirpaths:
possible_marker_path = NeMoModelCheckpoint.format_checkpoint_unfinished_marker_path(ckpt_dirpath)
if possible_marker_path in existing_marker_filepaths:
logging.warning(f'Removing unfinished dist checkpoint: {ckpt_dirpath}')
shutil.rmtree(ckpt_dirpath)
# delete markers
for marker_path in existing_marker_filepaths:
os.remove(marker_path)
def _should_remove_checkpoint(self, trainer: "pl.Trainer", previous: str, current: str) -> bool: # noqa: F821
"""Checks if the previous checkpoint should be deleted.
A checkpoint won't be deleted if any of the cases apply:
- The previous checkpoint is the same as the current checkpoint (means the old was already overwritten by new)
- The previous checkpoint is not in the current checkpoint directory and the filesystem is local
- The previous checkpoint is the checkpoint the Trainer resumed from and the filesystem is local
and the resumed from checkpoint is not the last checkpoint
"""
if previous == current:
return False
if not _is_local_file_protocol(previous):
return True
previous = Path(previous).absolute()
resume_path = Path(trainer.ckpt_path).absolute() if trainer.ckpt_path is not None else None
if resume_path is not None and previous == resume_path:
if str(current).endswith("-last.ckpt") and resume_path.name.endswith("-last.ckpt"):
# delete the previous `-last.ckpt` checkpoint when current saved checkpoint is also `-last.ckpt`,
# if they're in the same directory
pass
else:
return False
if self.dirpath is None:
raise ValueError(f"{self.__class__}.dirpath is None.")
dirpath = Path(self.dirpath).absolute()
return dirpath in previous.parents
+118
View File
@@ -0,0 +1,118 @@
# Copyright (c) 2023, 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.
import signal
import sys
import torch
from lightning.pytorch.callbacks import Callback
from nemo.utils import logging
class PreemptionCallback(Callback):
"""
PreemptionCallback class creates a callback that checks for preemption during training at the end of every step.
Upon preemption the callback provides a function to gracefully exit the training immediately and also saves the
current state in a checkpoint as *last.ckpt.
(to be able to start from the same step without wasting any compute while resuming the next time).
PreemptionCallback is always enabled by default via the arg create_preemption_callback under ExpManagerConfig.
To disable please pass create_preemption_callback: False in your config file.
"""
def __init__(self, checkpoint_callback, sig=None):
"""Store the checkpoint callback and the signal to listen for (defaults to SIGTERM)."""
self.sig = sig
if self.sig is None:
self.sig = signal.SIGTERM
self.checkpoint_callback = checkpoint_callback
self.preemption_enabled = False
@property
def interrupted(self):
"""Return whether a preemption signal was received, broadcasting rank 0's state to all ranks."""
interrupted = torch.tensor(self._interrupted, device=torch.cuda.current_device(), dtype=torch.int32)
torch.distributed.broadcast(interrupted, 0)
interrupted = bool(interrupted.item())
return interrupted
def on_train_start(self, trainer, pl_module):
"""
Defines custom handlers at the beginning of training to be executed when the
preemption signal is received.
"""
# Check if torch distributed is initialised, required for broadcasting the preemption signal to all the ranks
if not (torch.distributed.is_available() and torch.distributed.is_initialized()):
logging.info("Preemption requires torch distributed to be initialized, disabling preemption")
else:
self.preemption_enabled = True
# Bool var that's initialized to false and made True upon receving the preemption signal
self._interrupted = False
self.released = False
self.original_handler = signal.getsignal(self.sig)
# Master handler on rank 0 only upon preemption signal to avoid deadlock conditions
def master_handler(signum, frame):
self.release()
self._interrupted = True
# Handler executed by the non zero ranks
def ignoring_handler(signum, frame):
self.release()
self.private_rank = torch.distributed.get_rank()
if self.private_rank == 0:
signal.signal(self.sig, master_handler)
else:
signal.signal(self.sig, ignoring_handler)
return self
def on_train_end(self, trainer, pl_module):
"""Restore the original signal handler when training finishes."""
if self.preemption_enabled:
self.release()
def on_train_batch_end(self, trainer, pl_module, outputs, batch, batch_idx: int):
"""Check for preemption after each batch and, if signaled, save a last checkpoint and exit."""
if self.preemption_enabled:
# check if the job was preempted at the end of every training step/iteration
# NOTE: "self.interrupted" is a property which triggers a
# distributed broadcast of "_interrupted" flag from rank 0 to all other
# ranks, to avoid performance overheads it's best to store the result in
# a regular local variable
interrupted = self.interrupted
if interrupted:
logging.info("Received SIGTERM, saving checkpoint and exiting")
# Same off-by-one as in StatelessTimer: on_train_batch_end fires before
# batch_progress.increment_completed(), but the batch's optim step has
# already advanced global_step. Flush the in-flight batch so resume
# doesn't replay it and double-count the optim step.
from nemo.utils.exp_manager import _flush_in_flight_batch_progress
_flush_in_flight_batch_progress(trainer)
monitor_candidates = self.checkpoint_callback._monitor_candidates(trainer)
self.checkpoint_callback._save_last_checkpoint(trainer, monitor_candidates)
sys.exit(0)
def release(self):
"""Restore the original signal handler; returns False if already released, True otherwise."""
if self.released:
return False
signal.signal(self.sig, self.original_handler)
self.released = True
return True
+289
View File
@@ -0,0 +1,289 @@
# 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.
import os
import time
from concurrent.futures import ProcessPoolExecutor
from io import BytesIO
from multiprocessing import get_start_method
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import Any, Callable, Dict, Optional, Union
import torch
from lightning.fabric.plugins.io.checkpoint_io import CheckpointIO
from nemo.utils import logging
from nemo.utils.s3_utils import (
DEFAULT_CHUNK_SIZE_MB,
DEFAULT_MAX_READ_CONCURRENCY,
DEFAULT_MAX_WRITE_CONCURRENCY,
SHARED_MEM_DIR,
S3Utils,
)
class S3CheckpointIO(CheckpointIO):
"""A custom S3CheckpointIO module that supports checkpoint reading/writing with s3 when filepath
is a s3 url.
"""
def __init__(
self,
dirpath: str,
chunk_size_MB=DEFAULT_CHUNK_SIZE_MB,
max_read_concurrency=DEFAULT_MAX_READ_CONCURRENCY,
max_write_concurrency=DEFAULT_MAX_WRITE_CONCURRENCY,
async_checkpointing=False,
):
"""
Initialize the transfer configuration with custom values.
This method overrides the default TransferConfig values in boto3.
See https://boto3.amazonaws.com/v1/documentation/api/latest/_modules/boto3/s3/transfer.html#TransferConfig
Args:
chunk_size_MB (int, optional): The size of chunks to use when transferring files.
Default is 64 (MB).
max_read_concurrency (int, optional): The maximum number of threads that will be making
requests to perform a download. Default is 15.
max_write_concurrency (int, optional): The maximum number of threads that will be making
requests to perform an upload. Default is 10.
async_checkpointing (bool, optional): Uses a ProcessPoolExecutor to do the main saving logic.
This feature should be used with save_top_k as it's possible a previous checkpoint is removed while
the current checkpoint write fails.
"""
if not S3Utils.is_s3_url(dirpath):
raise AssertionError(
f"Error attempting to initialize an S3CheckpointIO when {dirpath} is not an S3 url. Please use TorchCheckpointIO when using a non-S3 dirpath."
)
self.chunk_size_MB = chunk_size_MB
self.max_read_concurrency = max_read_concurrency
self.max_write_concurrency = max_write_concurrency
self._async_checkpointing = async_checkpointing
'''
When using shared memory, we create a temporary file to hold the checkpoint before uploading to S3.
This list will track those temporary files, and clean up any leaked files that are still around during teardown.
'''
self._temp_files = []
if self.async_checkpointing:
# create an executor that will asynchronously run functions
self._executor = ProcessPoolExecutor(max_workers=1) if self.async_checkpointing else None
# Eager creating a subprocess now so that forked subprocess does not inherit cuda context from parent
if get_start_method() == 'fork' and torch.cuda.is_initialized() is True:
raise Exception(
f'torch.cuda should not be initialized when checkpointing subprocess is created by fork method'
)
logging.info(f'Creating asynchronous checkpointing subprocess')
future = self._executor.submit(dummy_func)
try:
future.result()
logging.info(f'Asynchronous heckpointing subprocess created successfully')
except Exception as e:
logging.error(f'Failed to create asynchronous checkpointing subprocess, exception: {e}')
raise e
self._futures = []
super().__init__()
@property
def async_checkpointing(self):
return self._async_checkpointing
def _serialize_checkpoint_to_shm(self, checkpoint: Dict, path: str) -> str:
"""
Returns:
filename of the temporary file in shared memory.
"""
start_time = time.perf_counter()
tempfile = NamedTemporaryFile(dir=SHARED_MEM_DIR, delete=False)
torch.save(checkpoint, tempfile)
logging.info(
f'Time elapsed saving checkpoint dict to {tempfile.name} for {path}: {(time.perf_counter() - start_time):.2f} seconds, rank {torch.distributed.get_rank()}'
)
del checkpoint
return tempfile.name
def _serialize_checkpoint_to_bytes(self, checkpoint: Dict, path: str) -> BytesIO:
"""
Returns:
The bytestring of the checkpoint.
"""
ss = time.perf_counter()
bytes = BytesIO()
torch.save(checkpoint, bytes)
tt = time.perf_counter() - ss
logging.info(
f'Time elapsed saving checkpoint dict to bytes for {path}: {tt:.2f} seconds, rank {torch.distributed.get_rank()}'
)
del checkpoint
return bytes
def _check_uploading_results_so_far(self):
"""
self._future is a list of tuples of form (future, destination path, source path)
This function checks the result of all the futures, and updates the self._futures list appropriately.
It also updates the list of self._temp_files, which is used to clean up leaked temporary files in SHARED_MEM during teardown.
"""
if not self._futures:
return
start_time = time.perf_counter()
done_futures = []
in_progress_futures = []
for item in self._futures:
if item[0].done():
done_futures.append(item)
else:
in_progress_futures.append(item)
for item in done_futures:
try:
item[0].result()
except Exception as e:
logging.error(f'Failed to upload {item[2]} to {item[1]}, exception: {e}')
raise e
# If the future is complete, we can remove the temp file since we choose to clear the temp file when uploading.
try:
self._temp_files.remove(item[2])
except:
pass # When not using shared memory, we do not append anything to the temp_files list, so remove will do nothing.
self._futures = in_progress_futures
logging.debug(
f'Time elapsed checking uploading future results: {(time.perf_counter() - start_time):.2f} seconds'
)
def save_checkpoint(
self, checkpoint: Dict[str, Any], path: Union[str, Path], storage_options: Optional[Any] = None
) -> None:
# if we have a shared memory directory, we can serialize as a file to shared memory instead of as bytes.
if os.path.exists(SHARED_MEM_DIR):
localfile = self._serialize_checkpoint_to_shm(checkpoint, path)
self._temp_files.append(localfile)
saved_as_file = True
else:
bytes = self._serialize_checkpoint_to_bytes(checkpoint, path)
saved_as_file = False
if self.async_checkpointing:
self._check_uploading_results_so_far()
logging.info(f'Uploading checkpoint to {path} in asynchronous mode, rank {torch.distributed.get_rank()}')
if saved_as_file:
future = self._executor.submit(
_upload_file_to_s3, localfile, path, self.chunk_size_MB, self.max_write_concurrency, True
)
self._futures.append((future, path, localfile))
else:
future = self._executor.submit(
_upload_bytes_to_s3, bytes, path, self.chunk_size_MB, self.max_write_concurrency
)
self._futures.append((future, path, 'bytes'))
else:
logging.info(f'Uploading checkpoint to {path} in synchronous mode, rank {torch.distributed.get_rank()}')
if saved_as_file:
_upload_file_to_s3(localfile, path, self.chunk_size_MB, self.max_write_concurrency, True)
self._temp_files.remove(localfile)
else:
_upload_bytes_to_s3(bytes, path, self.chunk_size_MB, self.max_write_concurrency)
def load_checkpoint(
self, path: Union[str, Path], map_location: Optional[Callable] = lambda storage, loc: storage
) -> Dict[str, Any]:
if os.path.exists(SHARED_MEM_DIR):
with NamedTemporaryFile(dir=SHARED_MEM_DIR, delete=True) as tempfile:
logging.info(
f'Loading checkpoint {path} into a temp file in shared memory {tempfile.name}, rank {torch.distributed.get_rank()}'
)
S3Utils.download_s3_file_to_path(
s3_path=path,
file_path=tempfile.name,
chunk_size_MB=self.chunk_size_MB,
max_concurrency=self.max_read_concurrency,
)
checkpoint = torch.load(tempfile.name)
else:
file_stream: BytesIO = S3Utils.download_s3_file_to_stream(
s3_path=path, chunk_size_MB=self.chunk_size_MB, max_concurrency=self.max_read_concurrency
)
checkpoint = torch.load(file_stream)
return checkpoint
def remove_checkpoint(self, path: Union[str, Path]) -> None:
if S3Utils.is_s3_url(path):
S3Utils.remove_object(path)
else:
super().remove_checkpoint(path)
def teardown(self) -> None:
# this ensure we wait for final checkpoint to finish uploading at train end.
rank = torch.distributed.get_rank()
if self.async_checkpointing:
logging.info(f'Entering teardown, waiting for all jobs to finish, rank {rank}')
start_time = time.perf_counter()
self._executor.shutdown(wait=True)
logging.info(f'executor shut down after {(time.perf_counter() - start_time):.2f} seconds, rank {rank}')
'''
this will be non-empty at the end of training if using asynchronous uploading since the futures are not processed with _check_uploading_results_so_far.
therefore, we check that the path exists first before trying to delete.
'''
if self._temp_files:
for tfile in self._temp_files:
if os.path.exists(tfile):
try:
os.remove(tfile)
except Exception as e:
logging.info(f"Error occurred while deleting file {tfile}: {e}")
def _clean_up_conflicting_checkpoint(filepath: str) -> None:
'''
before saving to s3, clean up any existing object with the same prefix megatron_gpt+step_count
e.g. before we save "megatron_gpt--step=1400-validation_loss=6.32-consumed_samples=55920.0-last.ckpt"
we need to clean up "megatron_gpt--step=1400-validation_loss=xxx-consumed_samples=yyy-last.ckpt"
so that in case later we need to resume from step 1400, it has a single checkpoint file at step 1400
'''
if S3Utils.is_s3_url(filepath):
prefix_with_step = S3Utils.parse_prefix_with_step(filepath)
logging.info(f'Looking for conflicting checkpoint under prefix {prefix_with_step}')
conflict_last_ckpts = S3Utils.find_files_with_suffix(
base_path=prefix_with_step, suffix='last.ckpt', return_key_only=False
)
for last_ckpt in conflict_last_ckpts:
logging.info(f'Cleaning up conflicting last ckpt {last_ckpt} before saving {filepath}')
S3Utils.remove_object(last_ckpt)
def _upload_file_to_s3(localfile, path, chunk_size_MB, max_write_concurrency, remove_file):
try:
_clean_up_conflicting_checkpoint(path)
S3Utils.upload_file(localfile, path, chunk_size_MB, max_write_concurrency, remove_file)
except Exception as e:
raise e
def _upload_bytes_to_s3(bytes, path, chunk_size_MB, max_write_concurrency):
try:
_clean_up_conflicting_checkpoint(path)
S3Utils.upload_file_stream_to_s3(bytes, path, chunk_size_MB, max_write_concurrency)
except Exception as e:
raise e
def dummy_func():
time.sleep(0.01)