feat: auto-checkpoint on SIGTERM/SIGINT with DDP support
- Register SIGTERM/SIGINT handlers in training loop, set stop flag on signal - Check stop_requested at each epoch/batch boundary, break and call on_error to save checkpoint - LocalStrategy parent forwards signal to child processes via terminate(), waits up to 600s for graceful exit - TrainContext gains threading.Event-based stop_requested/request_stop - Tests verify SIGTERM/SIGINT trigger checkpoint save with exit code 0, works on both CPU and GPU
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import threading
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional, Self
|
||||
@@ -41,6 +42,15 @@ class TrainContext:
|
||||
rank: int = field(default=0)
|
||||
kwargs: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
_stop_event: threading.Event = field(default_factory=threading.Event)
|
||||
|
||||
@property
|
||||
def stop_requested(self) -> bool:
|
||||
return self._stop_event.is_set()
|
||||
|
||||
def request_stop(self) -> None:
|
||||
self._stop_event.set()
|
||||
|
||||
@property
|
||||
def optimizer_step(self) -> int:
|
||||
return self.consumed_samples // (
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
import torch.distributed as dist
|
||||
|
||||
from astrai.config import TrainConfig
|
||||
from astrai.parallel.setup import spawn_parallel_fn
|
||||
from astrai.parallel.signal_handler import (
|
||||
register_signal_handlers,
|
||||
unregister_signal_handlers,
|
||||
)
|
||||
from astrai.trainer.train_callback import (
|
||||
CallbackFactory,
|
||||
TrainCallback,
|
||||
@@ -58,6 +64,7 @@ class Trainer:
|
||||
.with_param_path(param_path, resume=resume)
|
||||
.build()
|
||||
)
|
||||
register_signal_handlers(context)
|
||||
executor = context.executor
|
||||
self._call_callbacks("on_train_begin", context)
|
||||
|
||||
@@ -65,10 +72,14 @@ class Trainer:
|
||||
context.model.train()
|
||||
|
||||
for epoch in range(context.epoch, context.config.n_epoch):
|
||||
if context.stop_requested:
|
||||
break
|
||||
context.epoch = epoch
|
||||
self._call_callbacks("on_epoch_begin", context)
|
||||
|
||||
for batch in context.dataloader:
|
||||
if context.stop_requested:
|
||||
break
|
||||
with executor.accumulate(context.model):
|
||||
self._call_callbacks("on_batch_begin", context)
|
||||
loss = context.strategy(batch)
|
||||
@@ -91,12 +102,21 @@ class Trainer:
|
||||
|
||||
self._call_callbacks("on_epoch_end", context)
|
||||
|
||||
if context.stop_requested:
|
||||
logger.warning(
|
||||
"Training interrupted by signal, saving emergency checkpoint..."
|
||||
)
|
||||
self._call_callbacks("on_error", context)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Training failed: %s", str(e), exc_info=True)
|
||||
self._call_callbacks("on_error", context)
|
||||
raise
|
||||
finally:
|
||||
self._call_callbacks("on_train_end", context)
|
||||
if executor.use_distributed and dist.is_initialized():
|
||||
dist.barrier()
|
||||
unregister_signal_handlers()
|
||||
|
||||
def train(self, param_path: Optional[str] = None, resume: bool = False):
|
||||
cfg = self.train_config
|
||||
|
||||
Reference in New Issue
Block a user