feat: add --resume flag to decouple weight loading from training resumption

- Add --resume bool flag to train.py CLI
- --param_path always loads weights only by default
- --resume restores epoch, consumed_samples, optimizer & scheduler
- Checkpoint.load() now preserves full meta dict
- Update test_early_stopping to use new param_path/resume API
This commit is contained in:
ViperEkura 2026-07-16 14:23:23 +08:00
parent b14f301730
commit 84ed2327f5
5 changed files with 44 additions and 25 deletions

View File

@ -2,7 +2,6 @@
import io import io
import json import json
import os
import time import time
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
@ -178,6 +177,7 @@ class Checkpoint:
epoch=meta.get("epoch", 0), epoch=meta.get("epoch", 0),
consumed_samples=meta.get("consumed_samples", 0), consumed_samples=meta.get("consumed_samples", 0),
extra=extra, extra=extra,
meta=meta,
config=config, config=config,
) )

View File

@ -54,10 +54,12 @@ class TrainContextBuilder:
config: TrainConfig, config: TrainConfig,
): ):
self.config = config self.config = config
self._resume_dir: Optional[str] = None self._param_path: Optional[str] = None
self._resume: bool = False
def with_resume_dir(self, resume_dir: Optional[str]) -> Self: def with_param_path(self, param_path: Optional[str], resume: bool = False) -> Self:
self._resume_dir = resume_dir self._param_path = param_path
self._resume = resume
return self return self
def build(self) -> TrainContext: def build(self) -> TrainContext:
@ -74,8 +76,8 @@ class TrainContextBuilder:
model = model.to(device=device) model = model.to(device=device)
model_config = {} model_config = {}
if self._resume_dir: if self._param_path:
config_path = Path(self._resume_dir) / "config.json" config_path = Path(self._param_path) / "config.json"
if config_path.exists(): if config_path.exists():
model_config = load_json(config_path) model_config = load_json(config_path)
@ -91,23 +93,29 @@ class TrainContextBuilder:
executor=executor, executor=executor,
) )
if self._resume_dir: if self._param_path:
checkpoint = Checkpoint.load_any(self._resume_dir) checkpoint = Checkpoint.load_any(self._param_path)
if checkpoint is not None: if checkpoint is not None:
model.load_state_dict(checkpoint.state_dict, strict=False) model.load_state_dict(checkpoint.state_dict, strict=False)
if checkpoint.config: if checkpoint.config:
context.model_config = checkpoint.config context.model_config = checkpoint.config
context.epoch = checkpoint.epoch or cfg.start_epoch
if checkpoint.consumed_samples > 0: if self._resume:
per_step = ( context.epoch = checkpoint.epoch or cfg.start_epoch
cfg.batch_per_device * context.world_size * cfg.grad_accum_steps if checkpoint.consumed_samples > 0:
) per_step = (
context.consumed_samples = ( cfg.batch_per_device
checkpoint.consumed_samples // per_step * context.world_size
) * per_step * cfg.grad_accum_steps
else: )
context.consumed_samples = cfg.start_samples * context.world_size context.consumed_samples = (
context.checkpoint = checkpoint checkpoint.consumed_samples // per_step
) * per_step
else:
context.consumed_samples = (
cfg.start_samples * context.world_size
)
context.checkpoint = checkpoint
if cfg.lora is not None: if cfg.lora is not None:
inject_lora( inject_lora(

View File

@ -52,9 +52,11 @@ class Trainer:
if method: if method:
method(context) method(context)
def _trainer_loop(self, resume_dir: Optional[str] = None): def _trainer_loop(self, param_path: Optional[str] = None, resume: bool = False):
context = ( context = (
TrainContextBuilder(self.train_config).with_resume_dir(resume_dir).build() TrainContextBuilder(self.train_config)
.with_param_path(param_path, resume=resume)
.build()
) )
executor = context.executor executor = context.executor
self._call_callbacks("on_train_begin", context) self._call_callbacks("on_train_begin", context)
@ -95,7 +97,7 @@ class Trainer:
finally: finally:
self._call_callbacks("on_train_end", context) self._call_callbacks("on_train_end", context)
def train(self, resume_dir: Optional[str] = None): def train(self, param_path: Optional[str] = None, resume: bool = False):
cfg = self.train_config cfg = self.train_config
spawn_parallel_fn( spawn_parallel_fn(
self._trainer_loop, self._trainer_loop,
@ -105,5 +107,6 @@ class Trainer:
master_port=cfg.master_port, master_port=cfg.master_port,
device_type=cfg.device_type, device_type=cfg.device_type,
start_method=cfg.start_method, start_method=cfg.start_method,
resume_dir=resume_dir, param_path=param_path,
resume=resume,
) )

View File

@ -116,6 +116,13 @@ def parse_args() -> argparse.Namespace:
required=True, required=True,
help="Path to the model parameters or resume checkpoint.", help="Path to the model parameters or resume checkpoint.",
) )
parser.add_argument(
"--resume",
action="store_true",
default=False,
help="Resume training from checkpoint at --param_path "
"(restore epoch, consumed_samples, optimizer & scheduler state).",
)
parser.add_argument( parser.add_argument(
"--n_epoch", type=int, default=1, help="Number of epochs to train." "--n_epoch", type=int, default=1, help="Number of epochs to train."
@ -385,6 +392,7 @@ def train(
train_type: str, train_type: str,
param_path: str, param_path: str,
data_root_path: str, data_root_path: str,
resume: bool,
n_epoch: int, n_epoch: int,
batch_per_device: int, batch_per_device: int,
start_epoch: int, start_epoch: int,
@ -531,7 +539,7 @@ def train(
) )
trainer = Trainer(train_config) trainer = Trainer(train_config)
trainer.train(resume_dir=param_path) trainer.train(param_path=param_path, resume=resume)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -46,7 +46,7 @@ def test_early_stopping_simulation(base_test_env, early_stopping_dataset):
# Resume from latest checkpoint # Resume from latest checkpoint
load_dir = os.path.join(base_test_env["test_dir"], "epoch_0_step_1") load_dir = os.path.join(base_test_env["test_dir"], "epoch_0_step_1")
trainer = Trainer(train_config) trainer = Trainer(train_config)
trainer.train(resume_dir=load_dir) trainer.train(param_path=load_dir, resume=True)
# Verify checkpoint was saved at expected step # Verify checkpoint was saved at expected step
load_dir = os.path.join(base_test_env["test_dir"], "epoch_1_step_5") load_dir = os.path.join(base_test_env["test_dir"], "epoch_1_step_5")