from contextlib import contextmanager import datetime import importlib import json import logging import os import shutil import time import threading from collections import OrderedDict import numpy as np import torch from torch.distributed import barrier, is_initialized import torch.distributed.checkpoint as dcp from torch.distributed.checkpoint.state_dict import ( get_state_dict, set_state_dict, get_model_state_dict, get_optimizer_state_dict, StateDictOptions, ) from torch.distributed.fsdp import ( FullyShardedDataParallel as FSDP, FullStateDictConfig, FullOptimStateDictConfig, StateDictType, ) if ( importlib.util.find_spec("torch.nn.attention") is not None and importlib.util.find_spec("torch.nn.attention.sdpa_kernel") is not None ): TORCH_IS_NIGHTLY = True else: TORCH_IS_NIGHTLY = False def is_ddp(): return int(os.environ.get("RANK", -1)) != -1 def is_master(): if is_ddp(): return int(os.environ["RANK"]) == 0 return True def is_main_gpu_on_node(): if is_ddp(): return int(os.environ["LOCAL_RANK"]) == 0 return True def print_with_time(content): """Print the content with the current time.""" print(f"[{datetime.datetime.now().strftime('%Y-%m-%d_%H:%M:%S')}]: {content}") def print_with_time_master(content): if is_master(): print_with_time(content) def _write_json(info, out_fp): if is_master(): with open(out_fp, "w") as f: json.dump(info, f) def _read_json(fp): with open(fp, "r") as f: info = json.load(f) return info def hash_string_to_number(s, max_val=1023): return hash(s) % (max_val + 1) @contextmanager def suppress_logging(highest_level=logging.CRITICAL): previous_level = logging.root.manager.disable logging.disable(highest_level) try: yield finally: logging.disable(previous_level) def dist_barrier(): if is_initialized(): barrier() def _compare_dicts( dict1, dict2, ignore_set="compatibility", preload_strict=True, ): if isinstance(ignore_set, str) and ignore_set == "compatibility": ignore_set = set( [ "rope_theta", "dropout", "attention_type", "coarse_infer_token", "coarse_pad_token", "semantic_pad_token", "semantic_infer_token", "use_text_loss", "text_pad_token", ] ) if is_master(): missing_keys = set(dict1.keys()) - set(dict2.keys()) - ignore_set if len(missing_keys) > 0: if preload_strict: raise ValueError(f"checkpoint missing config keys: {missing_keys}") else: print_with_time_master(f"warning: checkpoint missing config keys: {missing_keys}") extra_keys = set(dict2.keys()) - set(dict1.keys()) - ignore_set if len(extra_keys) > 0: if preload_strict: raise ValueError(f"checkpoint has extra config keys: {extra_keys}") else: print_with_time_master(f"warning: checkpoint has extra config keys: {extra_keys}") for k in set(dict1.keys()) - ignore_set: if dict1.get(k) != dict2.get(k): if preload_strict: raise ValueError(f"checkpoint config key mismatch on {k}") else: print_with_time_master(f"warning: checkpoint config key mismatch on {k}") def verify_preload_model_args(model_args, preload_checkpoint, preload_strict=True): checkpoint_model_args = _read_json(preload_checkpoint.rstrip("/") + ".json")["model_args"] _compare_dicts(model_args, checkpoint_model_args, preload_strict=preload_strict) def _compare_partial_content(file1, file2, num_samples=10): # fast way to approximately compare two large files (should be fine for state dicts) print_with_time_master( "careful, using approximation for checkpoint loading. could be wrong in principle" ) stat1 = os.stat(file1) stat2 = os.stat(file2) if stat1.st_size != stat2.st_size: return False interval = stat1.st_size // num_samples with open(file1, "rb") as f1, open(file2, "rb") as f2: for i in range(num_samples): offset = i * interval f1.seek(offset) f2.seek(offset) if f1.read(1024) != f2.read(1024): # Read 1 KB at each sample point return False return True def load_old_state_dict( model_args, preload_checkpoint, model, local_cache_dir, preload_strict=True, use_mmap=False, ): if local_cache_dir is not None: local_ckpt_fp = os.path.join(local_cache_dir, "ckpt.pt") print_with_time_master("verifying model args...") if is_master(): if os.path.exists(local_ckpt_fp) and _compare_partial_content( preload_checkpoint, local_ckpt_fp ): checkpoint_model_args = torch.load(local_ckpt_fp, mmap=use_mmap)["model_args"] else: checkpoint_model_args = torch.load(preload_checkpoint, mmap=use_mmap)["model_args"] _compare_dicts(model_args, checkpoint_model_args, preload_strict=preload_strict) del checkpoint_model_args dist_barrier() if is_main_gpu_on_node(): os.makedirs(local_cache_dir, exist_ok=True) if not os.path.exists(local_ckpt_fp) or not _compare_partial_content( preload_checkpoint, local_ckpt_fp ): print_with_time_master("copying checkpoint file to local cachedir...") # If the file is large, consider using a buffered copy with open(preload_checkpoint, "rb") as src, open(local_ckpt_fp, "wb") as dst: shutil.copyfileobj(src, dst, length=1024 * 1024) # 1MB buffer else: local_ckpt_fp = preload_checkpoint dist_barrier() for n_gpu in range(torch.cuda.device_count()): print_with_time_master(f"loading model state_dict on gpu {n_gpu}") if n_gpu == int(os.environ["LOCAL_RANK"]): state_dict = torch.load(local_ckpt_fp, mmap=use_mmap)["model"] model_state_dict = model.state_dict() for k in model_state_dict.keys(): if ( k in model_state_dict and k in state_dict and model_state_dict[k].shape != state_dict[k].shape ): print_with_time_master(f"warning: state dict shape mismatch on {k}") del state_dict[k] if k not in state_dict: print_with_time_master(f"warning: state dict missing key {k}") # handle old heads if "transformer.wte_text.weight" in state_dict: state_dict["input_modules.text_input.wte_text.weight"] = state_dict.pop( "transformer.wte_text.weight" ) if "transformer.ln_text.weight" in state_dict: state_dict["input_modules.text_input.ln_text.weight"] = state_dict.pop( "transformer.ln_text.weight" ) if "transformer.wte_semantic.0.weight" in state_dict: state_dict["input_modules.semantic_input_0.wte_semantic.weight"] = state_dict.pop( "transformer.wte_semantic.0.weight" ) if "transformer.ln_semantic.weight" in state_dict: state_dict["input_modules.semantic_input_0.ln_semantic.weight"] = state_dict.pop( "transformer.ln_semantic.weight" ) if "input_modules.semantic_input.wte_semantic.weight" in state_dict: state_dict["input_modules.semantic_input_0.wte_semantic.weight"] = state_dict.pop( "input_modules.semantic_input.wte_semantic.weight" ) if "input_modules.semantic_input.ln_semantic.weight" in state_dict: state_dict["input_modules.semantic_input_0.ln_semantic.weight"] = state_dict.pop( "input_modules.semantic_input.ln_semantic.weight" ) if "output_modules.semantic_output.lm_head.weight" in state_dict: state_dict["output_modules.semantic_output_0.lm_head.weight"] = state_dict.pop( "output_modules.semantic_output.lm_head.weight" ) if "use_text_loss" not in model_args: # old model, copy over old heads if "lm_heads.0.weight" in state_dict: state_dict["output_modules.semantic_output_0.lm_head.weight"] = state_dict[ "lm_heads.0.weight" ] elif model_args.get("use_text_loss"): if "lm_heads.0.weight" in state_dict: state_dict["output_modules.text_output.lm_head.weight"] = state_dict.pop( "lm_heads.0.weight" ) if "lm_heads.1.weight" in state_dict: state_dict["output_modules.semantic_output_0.lm_head.weight"] = state_dict.pop( "lm_heads.1.weight" ) else: if "lm_heads.0.weight" in state_dict: state_dict["output_modules.semantic_output_0.lm_head.weight"] = state_dict.pop( "lm_heads.0.weight" ) # can't do assign=True, has some really weird effects in initialization model.load_state_dict(state_dict, strict=preload_strict) del state_dict, model_state_dict dist_barrier() def load_old_optimizer_state_dict( model, optimizer, ckpt_fp, ): assert os.path.exists(ckpt_fp) dist_barrier() iter_num = 0 n_tokens = 0 best_val_loss = 1e9 # Check if using FSDP2 or FSDP1 is_fsdp2 = not isinstance(model, FSDP) if is_fsdp2: # FSDP2: Load checkpoint on all ranks and use set_optimizer_state_dict checkpoint = torch.load(ckpt_fp, weights_only=False) optim_state_dict = checkpoint["optimizer"] iter_num = checkpoint["iter_num"] n_tokens = checkpoint.get("n_tokens", 0) best_val_loss = checkpoint["best_val_loss"] # Use FSDP2 API to load optimizer state # Note: full_state_dict=True tells it the loaded state is a full (gathered) state dict from torch.distributed.checkpoint.state_dict import set_optimizer_state_dict set_optimizer_state_dict( model, optimizer, optim_state_dict=optim_state_dict, options=StateDictOptions(strict=False, full_state_dict=True), ) del checkpoint, optim_state_dict else: # FSDP1: Use old API with sequential loading per GPU optim_state_dict = None for n_gpu in range(torch.cuda.device_count()): print_with_time_master(f"loading optimizer state_dict on gpu {n_gpu}") if n_gpu == int(os.environ["LOCAL_RANK"]): checkpoint = torch.load(ckpt_fp, weights_only=False) optim_state_dict = FSDP.optim_state_dict_to_load( model, optimizer, checkpoint["optimizer"] ) iter_num = checkpoint["iter_num"] n_tokens = checkpoint.get("n_tokens", 0) best_val_loss = checkpoint["best_val_loss"] del checkpoint dist_barrier() assert optim_state_dict is not None optimizer.load_state_dict(optim_state_dict) del optim_state_dict return iter_num, n_tokens, best_val_loss # DCP save info: # dcp save a directory eg 'last_ckpt' which needs to be loaded with dcp # additionally we save a json file at eg 'last_ckpt.json' to contain model args etc for verification def load_checkpoint( preload_checkpoint, preload_optimizer, model, optimizer, ): dist_barrier() iter_num = 0 n_tokens = 0 best_val_loss = 1e9 print_with_time_master(f"loading checkpoint from {preload_checkpoint}") model_state_dict, optimizer_state_dict = get_state_dict(model, optimizer) dist_barrier() state_dict = {"model": model_state_dict} if preload_optimizer: state_dict["optimizer"] = optimizer_state_dict dcp.load( state_dict=state_dict, checkpoint_id=preload_checkpoint, ) dist_barrier() # sets our state dicts on the model and optimizer, now that we've loaded set_state_dict( model, (), model_state_dict=model_state_dict, optim_state_dict=optimizer_state_dict if preload_optimizer else {}, ) dist_barrier() if preload_optimizer: info = _read_json(preload_checkpoint.rstrip("/") + ".json") iter_num = info["iter_num"] n_tokens = info.get("n_tokens", 0) best_val_loss = info["best_val_loss"] dist_barrier() print_with_time_master("done loading checkpoint") return iter_num, n_tokens, best_val_loss def _write_dcp_checkpoint(checkpoint, checkpoint_info, out_dir, ckpt_name): _write_json(checkpoint_info, os.path.join(out_dir, f"{ckpt_name}.json")) dist_barrier() if TORCH_IS_NIGHTLY: dcp.save(checkpoint, checkpoint_id=os.path.join(out_dir, ckpt_name)) else: fs_storage_writer = torch.distributed.checkpoint.FileSystemWriter( os.path.join(out_dir, ckpt_name) ) dcp.save(checkpoint, fs_storage_writer) dist_barrier() def save_checkpoint( out_dir, model, optimizer, best_val_loss, current_val_loss, step_save_iters, time_since_last_loss, model_args=None, iter_num=None, n_tokens=None, debug_val_only=False, ): if debug_val_only: return min(current_val_loss, best_val_loss) dist_barrier() t_save = time.time() model_state, optim_state = get_state_dict(model, optimizer) print_with_time_master(f"saving checkpoint to {out_dir}") checkpoint_info = { "model_args": model_args, "iter_num": iter_num, "n_tokens": n_tokens, "best_val_loss": current_val_loss, } checkpoint = { "model": model_state, "optimizer": optim_state, } if current_val_loss < best_val_loss: _write_dcp_checkpoint(checkpoint, checkpoint_info, out_dir, "best_ckpt") _write_dcp_checkpoint(checkpoint, checkpoint_info, out_dir, "last_ckpt") _write_dcp_checkpoint({"model": model_state}, checkpoint_info, out_dir, "last_ckpt_infer") if iter_num % step_save_iters == 0: _write_dcp_checkpoint(checkpoint, checkpoint_info, out_dir, f"step_{iter_num}_ckpt") estimation_time = time.time() - t_save eval_time_pct = np.clip(estimation_time / time_since_last_loss * 100, 0, 100) del model_state, optim_state torch.cuda.empty_cache() if current_val_loss < best_val_loss: best_val_loss = current_val_loss dist_barrier() print_with_time_master(f"saving took {estimation_time:.1f} seconds. ({eval_time_pct:.1f}% of loop)") return best_val_loss def save_old_checkpoint( out_dir, model, optimizer, best_val_loss, current_val_loss, step_save_iters, time_since_last_loss, model_args=None, iter_num=None, n_tokens=None, debug_val_only=False, save_best_ckpt=True, save_last_ckpt=True, ): if debug_val_only: return min(current_val_loss, best_val_loss) dist_barrier() t_save = time.time() # Check if using FSDP2 (no FSDP wrapper) or FSDP1 (has FSDP wrapper) is_fsdp2 = not isinstance(model, FSDP) if is_fsdp2: # FSDP2: Use new state_dict APIs options = StateDictOptions( full_state_dict=True, cpu_offload=True, ) model_state = get_model_state_dict(model, options=options) optim_state = get_optimizer_state_dict(model, optimizer, options=options) else: # FSDP1: Use old APIs FSDP.set_state_dict_type( model, StateDictType.FULL_STATE_DICT, FullStateDictConfig(rank0_only=True, offload_to_cpu=True), FullOptimStateDictConfig(rank0_only=True, offload_to_cpu=True), ) model_state = model.state_dict() original_osd = optimizer.state_dict() optim_state = FSDP.optim_state_dict(model, optimizer, optim_state_dict=original_osd) if is_master(): # only write state dicts on rank 0 assert model_state is not None, "some sort of distributed bug" assert optim_state is not None, "some sort of distributed bug" checkpoint = { "model": model_state, "optimizer": optim_state, "model_args": model_args, "iter_num": iter_num, "n_tokens": n_tokens, "best_val_loss": current_val_loss, } print_with_time(f"saving checkpoint to {out_dir}") if current_val_loss < best_val_loss and save_best_ckpt: torch.save(checkpoint, os.path.join(out_dir, "best_ckpt.pt")) def copy_checkpoint(): shutil.copy(os.path.join(out_dir, "best_ckpt.pt"), os.path.join(out_dir, "last_ckpt.pt")) # Start a new thread to copy best_ckpt.pt to last_ckpt.pt -- safe a bit of time thread = threading.Thread(target=copy_checkpoint) thread.start() elif save_last_ckpt: torch.save(checkpoint, os.path.join(out_dir, "last_ckpt.pt")) infer_sd = {k: checkpoint[k] for k in ["model", "model_args", "best_val_loss"]} infer_sd["model"] = OrderedDict({k: v.to(torch.bfloat16) for k, v in infer_sd["model"].items()}) # probably always want to save the last infer checkpoint torch.save(infer_sd, os.path.join(out_dir, "last_ckpt_infer.pt")) if iter_num % step_save_iters == 0 and iter_num > 0: torch.save( checkpoint, os.path.join(out_dir, f"step_{iter_num}_ckpt.pt"), ) torch.save(infer_sd, os.path.join(out_dir, f"step_{iter_num}_infer.pt")) estimation_time = time.time() - t_save eval_time_pct = np.clip(estimation_time / time_since_last_loss * 100, 0, 100) del model_state, optim_state torch.cuda.empty_cache() if current_val_loss < best_val_loss: best_val_loss = current_val_loss dist_barrier() print_with_time_master(f"saving took {estimation_time:.1f} seconds. ({eval_time_pct:.1f}% of loop)") return best_val_loss