feat : add distributed checkpoint via executor checkpoint_context

- Add checkpoint_context context manager to BaseExecutor with entry/exit barrier
- Add _gather_state_dict hook overridden per executor (template method)
- DDPExecutor skips unwrap on non-rank-0 to avoid redundant state_dict gather
- FSDPExecutor uses rank0_only=True to reduce memory on non-writers
- Remove redundant rank-0 guard from Checkpoint.save and manual barrier from Callback
This commit is contained in:
ViperEkura 2026-07-13 12:27:09 +08:00
parent 9bcd696580
commit b092316385
3 changed files with 42 additions and 22 deletions

View File

@ -7,6 +7,7 @@ from contextlib import contextmanager
from typing import Optional, Tuple from typing import Optional, Tuple
import torch import torch
import torch.distributed as dist
import torch.nn as nn import torch.nn as nn
from torch.distributed.fsdp import FullStateDictConfig, StateDictType from torch.distributed.fsdp import FullStateDictConfig, StateDictType
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
@ -120,6 +121,21 @@ class BaseExecutor:
def unwrap_model(self, model: nn.Module): def unwrap_model(self, model: nn.Module):
return model.state_dict() return model.state_dict()
@contextmanager
def checkpoint_context(self, model: nn.Module):
if self.use_distributed:
dist.barrier()
state_dict = self._gather_state_dict(model)
yield state_dict
if self.use_distributed:
dist.barrier()
def _gather_state_dict(self, model: nn.Module):
state_dict = self.unwrap_model(model)
if self.use_distributed and get_rank() != 0:
return None
return state_dict
@property @property
def use_distributed(self) -> bool: def use_distributed(self) -> bool:
return get_world_size() > 1 return get_world_size() > 1
@ -208,6 +224,13 @@ class DDPExecutor(BaseExecutor):
return model.module.state_dict() return model.module.state_dict()
return model.state_dict() return model.state_dict()
def _gather_state_dict(self, model: nn.Module):
if not self.use_distributed:
return self.unwrap_model(model)
if get_rank() != 0:
return None
return self.unwrap_model(model)
@ExecutorFactory.register("fsdp") @ExecutorFactory.register("fsdp")
class FSDPExecutor(BaseExecutor): class FSDPExecutor(BaseExecutor):
@ -279,7 +302,7 @@ class FSDPExecutor(BaseExecutor):
with FSDP.state_dict_type( with FSDP.state_dict_type(
model, model,
StateDictType.FULL_STATE_DICT, StateDictType.FULL_STATE_DICT,
FullStateDictConfig(offload_to_cpu=True, rank0_only=False), FullStateDictConfig(offload_to_cpu=True, rank0_only=True),
): ):
return model.state_dict() return model.state_dict()

View File

@ -148,9 +148,6 @@ class Checkpoint:
save_path = Path(save_dir) save_path = Path(save_dir)
save_path.mkdir(parents=True, exist_ok=True) save_path.mkdir(parents=True, exist_ok=True)
if get_rank() != 0:
return
meta = { meta = {
"epoch": self.epoch, "epoch": self.epoch,
"consumed_samples": self.consumed_samples, "consumed_samples": self.consumed_samples,

View File

@ -14,7 +14,7 @@ from tqdm import tqdm
from astrai.factory import BaseFactory from astrai.factory import BaseFactory
from astrai.parallel import only_on_rank from astrai.parallel import only_on_rank
from astrai.parallel.setup import get_current_device, get_rank from astrai.parallel.setup import get_current_device
from astrai.serialization import Checkpoint from astrai.serialization import Checkpoint
from astrai.trainer.metric_util import ( from astrai.trainer.metric_util import (
ctx_get_grad_norm, ctx_get_grad_norm,
@ -142,25 +142,25 @@ class CheckpointCallback(TrainCallback):
self.last_ckpt_step = 0 self.last_ckpt_step = 0
def _save_checkpoint(self, context: TrainContext): def _save_checkpoint(self, context: TrainContext):
state_dict = context.executor.unwrap_model(context.model)
self.last_ckpt_step = context.optimizer_step self.last_ckpt_step = context.optimizer_step
if get_rank() == 0: with context.executor.checkpoint_context(context.model) as state_dict:
save_path = os.path.join( if state_dict is not None:
self.save_dir, save_path = os.path.join(
f"epoch_{context.epoch}_step_{context.optimizer_step}", self.save_dir,
) f"epoch_{context.epoch}_step_{context.optimizer_step}",
extra = self.save_extra_fn(context) )
meta = context.config.to_dict() extra = self.save_extra_fn(context)
context.checkpoint = Checkpoint( meta = context.config.to_dict()
state_dict=state_dict, context.checkpoint = Checkpoint(
epoch=context.epoch, state_dict=state_dict,
consumed_samples=context.consumed_samples, epoch=context.epoch,
config=context.model_config, consumed_samples=context.consumed_samples,
extra=extra, config=context.model_config,
meta=meta, extra=extra,
) meta=meta,
context.checkpoint.save(save_path) )
context.checkpoint.save(save_path)
def on_batch_end(self, context: TrainContext): def on_batch_end(self, context: TrainContext):
if context.optimizer_step - self.last_ckpt_step >= self.interval: if context.optimizer_step - self.last_ckpt_step >= self.interval: