lip-forcing / lipforcing /utils /checkpointer.py
multimodalart's picture
multimodalart HF Staff
Initial Lip Forcing 14B streaming demo
9368ee7 verified
Raw
History Blame Contribute Delete
28.6 kB
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
import os.path
import io
import torch.nn
from typing import Optional, Dict, Union, Any
from torch.distributed.checkpoint import FileSystemWriter, FileSystemReader
import torch.distributed.checkpoint as dcp
from torch.distributed.checkpoint.state_dict import (
get_model_state_dict,
set_model_state_dict,
get_optimizer_state_dict,
set_optimizer_state_dict,
StateDictOptions,
)
from torch.distributed.checkpoint.stateful import Stateful
from lipforcing.configs.config import BaseCheckpointerConfig
from lipforcing.utils.distributed.s3_filesystem import S3StorageWriter, S3StorageReader
from lipforcing.utils.io_utils import s3_load, s3_save, latest_checkpoint
import lipforcing.utils.logging_utils as logger
from lipforcing.utils.distributed import synchronize, is_rank0
from lipforcing.callbacks.callback import CallbackDict
class Checkpointer:
"""Class to save and load model checkpoints"""
def __init__(self, config: BaseCheckpointerConfig):
self.config = config
def _save_checkpoint(self, save_dict: Dict[str, Any], path: str):
assert path.endswith(".pth"), f"{path} does not end with .pth"
if self.config.use_s3:
assert path.startswith("s3://"), f"{path} does not start with s3:// when using s3 storage"
logger.info(f"Saving the model to {path}")
# convert the save_dict to bytes
buffer = io.BytesIO()
torch.save(save_dict, buffer)
s3_save(path, buffer.getvalue())
else:
if not os.path.exists(os.path.dirname(path)):
os.makedirs(os.path.dirname(path), exist_ok=True)
torch.save(save_dict, path)
def _load_checkpoint(self, path: str, device: torch.device = "cpu") -> Dict[str, Any]:
assert path.endswith(".pth"), f"{path} does not end with .pth"
if self.config.use_s3:
assert path.startswith("s3://"), f"{path} does not start with s3:// when using s3 storage"
state = torch.load(s3_load(path), map_location=device)
else:
assert os.path.exists(path), f"{path} does not exist"
state = torch.load(path, map_location=device, weights_only=False)
return state
def save(
self,
model_dict: torch.nn.ModuleDict,
optimizer_dict: Dict[str, torch.optim.Optimizer] | None = None,
scheduler_dict: Dict[str, torch.optim.lr_scheduler.LambdaLR] | None = None,
grad_scaler: torch.amp.GradScaler | None = None,
callbacks: CallbackDict | None = None,
path: str | None = None,
iteration: int = 0,
) -> str:
"""Save a checkpoint of the model (and optionally optimizer, scheduler, and grad scaler to resume training)
Args:
model_dict (torch.nn.ModuleDict): The model dict to save
optimizer_dict (Dict[str, torch.optim.Optimizer]): The optimizer dict to save
scheduler_dict (Dict[str, torch.optim.lr_scheduler]): The scheduler dict to save
grad_scaler (torch.amp.GradScaler | None): The gradient scaler (for mixed precision training)
callbacks (CallbackDict | None): The callbacks to save
path (str): The path to save the checkpoint file
iteration (int): The iteration number
Returns:
str: The path to the saved checkpoint file
"""
synchronize()
model_state = {k: v.state_dict() for k, v in model_dict.items()}
optim_state = None if optimizer_dict is None else {k: v.state_dict() for k, v in optimizer_dict.items()}
scheduler_state = None if scheduler_dict is None else {k: v.state_dict() for k, v in scheduler_dict.items()}
grad_scaler_state = None if grad_scaler is None else grad_scaler.state_dict()
callbacks_state = None if callbacks is None else callbacks.state_dict()
save_dict = {
"model": model_state,
"optimizer": optim_state,
"scheduler": scheduler_state,
"grad_scaler": grad_scaler_state,
"callbacks": callbacks_state,
"iteration": iteration,
}
if is_rank0():
if path is None:
if self.config.use_s3:
path = os.path.join(self.config.s3_container, self.config.save_dir, f"{iteration:07d}.pth")
else:
path = os.path.join(self.config.save_dir, f"{iteration:07d}.pth")
if not self.config.use_s3:
os.makedirs(os.path.dirname(path), exist_ok=True)
logger.info(f"Saving the model to {path}")
self._save_checkpoint(save_dict, path)
logger.success(f"Model saved at iteration {iteration}")
synchronize()
return path
def load(
self,
model_dict: torch.nn.ModuleDict,
optimizer_dict: Dict[str, torch.optim.Optimizer] | None = None,
scheduler_dict: Dict[str, torch.optim.lr_scheduler.LambdaLR] | None = None,
grad_scaler: torch.amp.GradScaler | None = None,
callbacks: CallbackDict | None = None,
path: str | None = None,
device: Optional[torch.device] = "cpu",
) -> int:
"""Load the model checkpoint
Args:
model_dict (torch.nn.ModuleDict): The model dict to load
optimizer_dict (Dict[str, torch.optim.Optimizer]): The optimizer dict to load
scheduler_dict (Dict[str, torch.optim.lr_scheduler]): The scheduler dict to load
grad_scaler (torch.amp.GradScaler | None): The gradient scaler (for mixed precision training)
callbacks (CallbackDict | None): The callbacks to load
path (str): The path to the checkpoint file
device (Optional[torch.device]): The device to load the model to. Defaults to "cpu".
Returns:
int: The iteration number
"""
# TODO: rank 0 load and broadcast to all other ranks
if path is None:
if self.config.use_s3:
checkpoint_path = os.path.join(self.config.s3_container, self.config.save_dir)
else:
checkpoint_path = self.config.save_dir
path = latest_checkpoint(checkpoint_path) + ".pth"
if path == ".pth":
# no checkpoint found, starting from iteration 0
return 0
if not os.path.exists(path):
logger.critical(f"Checkpoint file not found at {path}")
return 0
logger.info(f"Loading model from {path}")
state = self._load_checkpoint(path, device=device)
logger.info("Loading the model_dict...")
for k, v in model_dict.items():
if k in state["model"] and v is not None:
# strict is False to allow evaluating external checkpoints without, e.g., logvar parameters
model_load_info = v.load_state_dict(state["model"][k], strict=False)
logger.info(f"Model {k}, loading info: {model_load_info}")
else:
logger.warning(f"Model {k} not found in checkpoint.")
if optimizer_dict is not None:
logger.info("Loading the optimizer_dict...")
for k, v in optimizer_dict.items():
if k in state["optimizer"] and v is not None:
v.load_state_dict(state["optimizer"][k])
else:
logger.warning(f"Optimizer {k} not found in checkpoint.")
if scheduler_dict is not None:
logger.info("Loading the scheduler_dict...")
for k, v in scheduler_dict.items():
if k in state["scheduler"] and v is not None:
v.load_state_dict(state["scheduler"][k])
else:
logger.warning(f"Scheduler {k} not found in checkpoint.")
if grad_scaler is not None:
logger.info("Loading the gradient scaler...")
# Check if saved grad_scaler state is non-empty (disabled scalers save empty state)
if state.get("grad_scaler") and len(state["grad_scaler"]) > 0:
grad_scaler.load_state_dict(state["grad_scaler"])
else:
logger.warning("Gradient scaler state is empty (likely saved from disabled scaler), skipping load.")
if callbacks is not None:
logger.info("Loading the callbacks...")
if "callbacks" in state:
callbacks.load_state_dict(state["callbacks"])
else:
logger.warning("Callbacks not found in checkpoint.")
if "iteration" not in state:
logger.warning("Iteration not found in checkpoint.")
return 0
return state["iteration"]
class ModelWrapper(Stateful):
"""
Wrapper for model state dict handling
Code taken from this tutorial: https://pytorch.org/tutorials/recipes/distributed_checkpoint_recipe.html
This is a useful wrapper for checkpointing the Application State. Since this object is compliant
with the Stateful protocol, DCP will automatically call state_dict/load_stat_dict as needed in the
dcp.save/load APIs.
"""
def __init__(self, model: torch.nn.Module, options: StateDictOptions | None = None):
self.model = model
# Default to strict=False so partial-state saves (e.g., LoRA +
# selective-unfreeze runs that filter to trainable params only at
# save time, see FSDPCheckpointer.save) can be loaded back without
# PyTorch raising on missing frozen-base keys. The frozen base
# values are filled in during model construction (load base
# safetensors + V2V adapter ckpt), so a strict=False load just
# leaves them at their construction-time values — which is exactly
# what we want for resume / inference.
if options is None:
options = StateDictOptions(strict=False)
self.options = options
def state_dict(self) -> Dict[str, Any]:
# this line automatically manages FSDP FQN's, and sets the default state dict type to FSDP.SHARDED_STATE_DICT
return get_model_state_dict(self.model, options=self.options)
def load_state_dict(self, state_dict: Dict[str, Any]):
set_model_state_dict(self.model, model_state_dict=state_dict, options=self.options)
class OptimizerWrapper(Stateful):
"""
Wrapper for optimizer state dict handling
Code taken from this tutorial: https://pytorch.org/tutorials/recipes/distributed_checkpoint_recipe.html
This is a useful wrapper for checkpointing the Application State. Since this object is compliant
with the Stateful protocol, DCP will automatically call state_dict/load_stat_dict as needed in the
dcp.save/load APIs.
"""
def __init__(
self, model: torch.nn.Module, optimizer: torch.optim.Optimizer, options: StateDictOptions | None = None
):
self.model = model
self.optimizer = optimizer
self.options = options
def state_dict(self) -> Dict[str, Any]:
# this line automatically manages FSDP FQN's, and sets the default state dict type to FSDP.SHARDED_STATE_DICT
optimizer_state_dict = get_optimizer_state_dict(self.model, self.optimizer, options=self.options)
return optimizer_state_dict
def load_state_dict(self, state_dict: Dict[str, Any]):
set_optimizer_state_dict(self.model, self.optimizer, optim_state_dict=state_dict, options=self.options)
class FSDPCheckpointer(Checkpointer):
"""Class to save and load model checkpoints"""
def get_storage_writer(self, checkpoint_path: str) -> Union[S3StorageWriter, FileSystemWriter]:
if self.config.use_s3:
return S3StorageWriter(
credential_path=self.config.s3_credential,
path=checkpoint_path,
)
return FileSystemWriter(path=checkpoint_path)
def get_storage_reader(self, checkpoint_path: str) -> Union[S3StorageReader, FileSystemReader]:
if self.config.use_s3:
return S3StorageReader(
credential_path=self.config.s3_credential,
path=checkpoint_path,
)
return FileSystemReader(checkpoint_path)
def save(
self,
model_dict: torch.nn.ModuleDict,
optimizer_dict: Dict[str, torch.optim.Optimizer] | None = None,
scheduler_dict: Dict[str, torch.optim.lr_scheduler.LambdaLR] | None = None,
grad_scaler: torch.amp.GradScaler | None = None,
callbacks: CallbackDict | None = None,
path: str | None = None,
iteration: int = 0,
) -> str:
"""Save a checkpoint of the model (and optionally optimizer, scheduler, and grad scaler to resume training)
Args:
model_dict (torch.nn.ModuleDict): The model dict to save
optimizer_dict (Dict[str, torch.optim.Optimizer]): The optimizer dict to save
scheduler_dict (Dict[str, torch.optim.lr_scheduler]): The scheduler dict to save
grad_scaler (torch.amp.GradScaler | None): The gradient scaler (for mixed precision training)
callbacks (CallbackDict | None): The callbacks to save
path (str): The path to save the checkpoint file
iteration (int): The iteration number
Returns:
str: The path to the saved checkpoint file
"""
if path is None:
if self.config.use_s3:
path = os.path.join(self.config.s3_container, self.config.save_dir, f"{iteration:07d}")
else:
path = os.path.join(self.config.save_dir, f"{iteration:07d}")
if not os.path.exists(self.config.save_dir):
os.makedirs(self.config.save_dir, exist_ok=True)
if path.endswith(".pth"): # In the case of autoresume
path = path[:-4]
logger.info(f"Saving FSDP model to prefix {path}")
synchronize()
# fsdp should save on all ranks
for k, v in model_dict.items():
model_state_dict = ModelWrapper(model=v).state_dict()
# Filter to params with requires_grad=True (trainable-only) when
# partial-freeze is in effect. No-op for full-FT runs (where
# every param has requires_grad=True; the filter keeps everything).
# For LoRA + selective-unfreeze runs at 14B, this collapses save
# size from ~56 GB (full base) to ~5 GB (LoRA + unfreeze).
#
# Non-parameter state (registered buffers, module-level Tensors
# that aren't nn.Parameters) is preserved — only nn.Parameters
# are filtered. ``v.named_parameters()`` enumerates only
# parameters; anything else in ``model_state_dict`` is kept.
params_dict = dict(v.named_parameters())
filtered = {
key: tensor
for key, tensor in model_state_dict.items()
if key not in params_dict or params_dict[key].requires_grad
}
n_kept = len(filtered)
n_total = len(model_state_dict)
if n_kept < n_total:
logger.info(
f"[FSDPCheckpointer] {k}_model: saving {n_kept}/{n_total} state-dict "
f"entries (filtered out {n_total - n_kept} frozen-param entries)"
)
storage_writer = self.get_storage_writer(checkpoint_path=f"{path}.{k}_model")
dcp.save(filtered, storage_writer=storage_writer)
if optimizer_dict is not None:
for k, v in optimizer_dict.items():
optim_state_dict = OptimizerWrapper(model=model_dict[k], optimizer=v).state_dict()
storage_writer = self.get_storage_writer(checkpoint_path=f"{path}.{k}_optim")
dcp.save(optim_state_dict, storage_writer=storage_writer)
# other scalars only save on rank 0
if is_rank0():
scheduler_state = None if scheduler_dict is None else {k: v.state_dict() for k, v in scheduler_dict.items()}
grad_scaler_state = None if grad_scaler is None else grad_scaler.state_dict()
callbacks_state = None if callbacks is None else callbacks.state_dict()
save_dict = {
"scheduler": scheduler_state,
"grad_scaler": grad_scaler_state,
"callbacks": callbacks_state,
"iteration": iteration,
}
self._save_checkpoint(save_dict, f"{path}.pth")
logger.success(f"Model saved at iteration {iteration}")
synchronize()
return path
def load(
self,
model_dict: torch.nn.ModuleDict,
optimizer_dict: Dict[str, torch.optim.Optimizer] | None = None,
scheduler_dict: Dict[str, torch.optim.lr_scheduler.LambdaLR] | None = None,
grad_scaler: torch.amp.GradScaler | None = None,
callbacks: CallbackDict | None = None,
path: str | None = None,
device: Optional[torch.device] = "cpu",
) -> int:
"""Load the model checkpoint
Args:
model_dict (torch.nn.ModuleDict): The model dict to load
optimizer_dict (Dict[str, torch.optim.Optimizer]): The optimizer dict to load
scheduler_dict (Dict[str, torch.optim.lr_scheduler]): The scheduler dict to load
grad_scaler (torch.amp.GradScaler | None): The gradient scaler (for mixed precision training)
callbacks (CallbackDict | None): The callbacks to load
path (str): The path to the checkpoint file
device (Optional[torch.device]): The device to load the model to. Defaults to "cpu".
Returns:
int: The iteration number
"""
if path is None:
if self.config.use_s3:
checkpoint_path = os.path.join(self.config.s3_container, self.config.save_dir)
else:
checkpoint_path = self.config.save_dir
path = latest_checkpoint(checkpoint_path)
if path == "":
# no checkpoint found, starting from iteration 0
return 0
if path.endswith(".pth"):
# An FSDPCheckpointer-saved checkpoint stores model/optim shards
# in sibling `<step>.{k}_model/` and `<step>.{k}_optim/` dirs and
# writes only metadata (iteration, scheduler, callbacks, grad_scaler)
# into the .pth file — there is NO `"model"` key in that .pth.
# The regular `Checkpointer.load` at L156 indexes `state["model"]`
# unconditionally and would KeyError. Detect the FSDP layout by
# looking for any sibling `*.<k>_model/` dir, and if present, fall
# through to the FSDP load path below (treating the .pth as a
# metadata stub for an FSDP-saved bundle).
stripped_path = path[:-4]
has_fsdp_layout = any(
os.path.isdir(f"{stripped_path}.{k}_model") for k in model_dict.keys()
)
if has_fsdp_layout:
logger.info(
f"Loading FSDP model (.pth metadata stub at {path}, "
f"sharded state in sibling *.{{k}}_model dirs)"
)
path = stripped_path
# fall through to the FSDP code path below
else:
# regular DDP-saved single-file checkpoint
logger.debug(f"Loading non-FSDP model from {path}")
return super().load(
model_dict,
optimizer_dict=optimizer_dict,
scheduler_dict=scheduler_dict,
grad_scaler=grad_scaler,
callbacks=callbacks,
path=path,
device=device,
)
if not os.path.exists(f"{path}.pth"):
logger.critical(f"Checkpoint file not found at {path}")
return 0
logger.info(f"Loading FSDP model from prefix {path}")
for k, v in model_dict.items():
logger.info(f"Loading the FSDP model dict for key {k}...")
assert os.path.exists(f"{path}.{k}_model"), f"Key {k} does not exist in FSDP model dict"
# Skip on ranks where parameters live on the meta device. This
# check MUST come before constructing the ModelWrapper / calling
# state_dict(), because get_model_state_dict on a non-FSDP-wrapped
# meta-init model can materialize the meta tensors as a side
# effect, defeating the very memory savings we're trying to
# protect.
#
# Context for the skip:
# - This load path runs in two contexts: pretrained_ckpt_path
# (BEFORE FSDP wrap, model is unsharded) and resume (AFTER FSDP
# wrap, model is sharded with real per-rank tensors).
# - Pre-FSDP-wrap, with fsdp_meta_init=True, only rank 0 holds
# real weights; ranks 1-3 hold meta tensors. dcp.load on a meta
# state_dict materializes those tensors, allocating real memory
# on every rank and defeating the meta-init memory savings —
# which combined with the bf16->fp32 cast in on_train_begin
# can push the cgroup over its memory cap.
# - The skip is safe because model_to_fsdp(sync_module_states=True)
# broadcasts rank-0's loaded state to all ranks during FSDP wrap
# via set_model_state_dict(broadcast_from_rank0=True), filling
# the per-rank shards with the loaded values.
# - Post-FSDP-wrap (resume path), parameters are real DTensors with
# per-rank shards — never on meta — so this check is False and
# the load proceeds normally.
has_meta_params = any(p.is_meta for p in v.parameters())
if has_meta_params:
# Participate in DCP's collective planning protocol with an
# empty state_dict so we don't allocate, but rank 0's dcp.load
# doesn't deadlock waiting for our all_gather. An empty dict
# tells the planner this rank wants nothing — no allocation,
# no disk read.
storage_reader = self.get_storage_reader(checkpoint_path=f"{path}.{k}_model")
logger.info(
f"[FSDPCheckpointer] {k}_model: meta-rank empty load "
f"(rank-0 broadcasts via sync_module_states later)"
)
dcp.load(state_dict={}, storage_reader=storage_reader)
continue
model_wrapper = ModelWrapper(model=v)
model_state_dict = model_wrapper.state_dict()
storage_reader = self.get_storage_reader(checkpoint_path=f"{path}.{k}_model")
# Symmetric to save filter: only load trainable param entries.
# Frozen base params remain at the model's current state (from
# PEFT injection + pretrained init). Non-parameter entries
# (registered buffers) pass through. No-op for full-FT runs.
params_dict = dict(v.named_parameters())
filtered_state_dict = {
key: tensor
for key, tensor in model_state_dict.items()
if key not in params_dict or params_dict[key].requires_grad
}
n_kept = len(filtered_state_dict)
n_total = len(model_state_dict)
if n_kept < n_total:
logger.info(
f"[FSDPCheckpointer] {k}_model: loading {n_kept}/{n_total} "
f"state-dict entries (skipping {n_total - n_kept} frozen-param entries)"
)
dcp.load(
state_dict=filtered_state_dict,
storage_reader=storage_reader,
)
model_wrapper.load_state_dict(model_state_dict)
if optimizer_dict is not None:
allow_missing_optim = os.environ.get("LIPFORCING_ALLOW_MISSING_FSDP_OPTIM", "").lower() in {
"1",
"true",
"yes",
}
for k, v in optimizer_dict.items():
logger.info(f"Loading the FSDP optimizer dict for key {k}...")
optim_wrapper = OptimizerWrapper(model=model_dict[k], optimizer=v)
# For fresh optimizers with no state, we need to initialize with fake gradients
# that are DTensors (not regular Tensors) to avoid the mixed Tensor/DTensor error
if len(v.state) == 0:
# Set fake DTensor gradients to initialize optimizer state
for param in model_dict[k].parameters():
if param.requires_grad and param.grad is None:
param.grad = torch.zeros_like(param)
optim_state_dict = optim_wrapper.state_dict()
assert os.path.exists(f"{path}.{k}_model"), f"Key {k} does not exist in FSDP model dict"
optim_path = f"{path}.{k}_optim"
if not os.path.exists(optim_path):
if allow_missing_optim:
logger.warning(
f"Optimizer checkpoint for {k} missing at {optim_path}; "
"LIPFORCING_ALLOW_MISSING_FSDP_OPTIM=1, so continuing with fresh optimizer state."
)
v.state.clear()
continue
raise FileNotFoundError(
f"Optimizer checkpoint for {k} not found at {optim_path}. "
"Set LIPFORCING_ALLOW_MISSING_FSDP_OPTIM=1 to resume model weights with fresh optimizer state."
)
storage_reader = self.get_storage_reader(checkpoint_path=optim_path)
try:
dcp.load(
state_dict=optim_state_dict,
storage_reader=storage_reader,
)
optim_wrapper.load_state_dict(optim_state_dict)
logger.success(f"Successfully loaded optimizer state for {k}")
except Exception as e:
error_msg = str(e)
if (
"Missing key" in error_msg
or "Unexpected key" in error_msg
or "CheckpointException" in error_msg
):
logger.warning(f"Optimizer checkpoint compatibility issue for {k}: {type(e).__name__}")
logger.warning(f"Initializing fresh optimizer state for {k} - training will continue")
# Reset to fresh optimizer state while preserving the optimizer's
# defaultdict-backed lazy state initialization.
v.state.clear()
logger.info(f"Reset optimizer state for {k} due to parameter mismatch")
else:
logger.error(f"Unexpected optimizer loading error for {k}: {e}")
raise e
state = self._load_checkpoint(f"{path}.pth", device=device)
if scheduler_dict is not None:
logger.info("Loading the scheduler_dict...")
for k, v in scheduler_dict.items():
if k in state["scheduler"]:
v.load_state_dict(state["scheduler"][k])
else:
logger.warning(f"Scheduler {k} not found in checkpoint.")
if grad_scaler is not None:
logger.info("Loading the gradient scaler...")
# Check if saved grad_scaler state is non-empty (disabled scalers save empty state)
if state.get("grad_scaler") and len(state["grad_scaler"]) > 0:
grad_scaler.load_state_dict(state["grad_scaler"])
else:
logger.warning("Gradient scaler state is empty (likely saved from disabled scaler), skipping load.")
if callbacks is not None:
logger.info("Loading the callbacks...")
if "callbacks" in state:
callbacks.load_state_dict(state["callbacks"])
else:
logger.warning("Callbacks not found in checkpoint.")
return state["iteration"]