BonanDing commited on
Commit
cf7efaa
·
1 Parent(s): fc998bf

Align resume checkpoint logic with DeMemWM

Browse files
Files changed (2) hide show
  1. configurations/training.yaml +2 -0
  2. main.py +155 -19
configurations/training.yaml CHANGED
@@ -14,3 +14,5 @@ wandb:
14
 
15
  resume: null # wandb run id to resume logging and loading checkpoint from
16
  load: null # wandb run id containing checkpoint or a path to a checkpoint file
 
 
 
14
 
15
  resume: null # wandb run id to resume logging and loading checkpoint from
16
  load: null # wandb run id containing checkpoint or a path to a checkpoint file
17
+ auto_resume: true # automatically resume training from output_dir/checkpoints when available
18
+ resume_ckpt_path: null # explicit full Lightning checkpoint path for deterministic training resume
main.py CHANGED
@@ -11,6 +11,9 @@ Borrowed part of the code from David Charatan and wandb.
11
  import sys
12
  import subprocess
13
  import time
 
 
 
14
  from pathlib import Path
15
 
16
  import hydra
@@ -18,16 +21,125 @@ from omegaconf import DictConfig, OmegaConf
18
  from omegaconf.omegaconf import open_dict
19
 
20
  from utils.print_utils import cyan
21
- from utils.ckpt_utils import download_latest_checkpoint, is_run_id
22
  from utils.cluster_utils import submit_slurm_job
23
  from utils.distributed_utils import is_rank_zero
24
 
25
- def get_latest_checkpoint(checkpoint_folder: Path, pattern: str = '*.ckpt'):
26
- checkpoint_files = list(checkpoint_folder.glob(pattern))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  if not checkpoint_files:
28
  return None
29
- latest_checkpoint = max(checkpoint_files, key=lambda f: f.stat().st_mtime)
30
- return latest_checkpoint
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
  def run_local(cfg: DictConfig):
33
  # delay some imports in case they are not needed in non-local envs for submission
@@ -59,18 +171,41 @@ def run_local(cfg: DictConfig):
59
  OmegaConf.set_readonly(hydra_cfg, True)
60
 
61
  output_dir = Path(hydra_cfg.runtime.output_dir)
62
- output_dir.mkdir(parents=True, exist_ok=True)
63
 
64
  if is_rank_zero:
65
  print(cyan(f"Outputs will be saved to:"), output_dir)
66
  (output_dir.parents[1] / "latest-run").unlink(missing_ok=True)
67
  (output_dir.parents[1] / "latest-run").symlink_to(output_dir, target_is_directory=True)
68
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  # Set up logging with wandb.
70
  if cfg.wandb.mode != "disabled":
71
  # If resuming, merge into the existing run on wandb.
72
- resume = cfg.get("resume", None)
73
- name = f"{cfg.name} ({output_dir.parent.name}/{output_dir.name})" if resume is None else None
74
 
75
  if "_on_compute_node" in cfg and cfg.cluster.is_compute_node_offline:
76
  logger_cls = OfflineWandbLogger
@@ -86,29 +221,30 @@ def run_local(cfg: DictConfig):
86
  project=cfg.wandb.project,
87
  log_model=False,
88
  config=OmegaConf.to_container(cfg),
89
- id=resume,
90
  resume="auto"
91
  )
92
 
93
  else:
94
  logger = None
95
 
96
- # Load ckpt
97
- resume = cfg.get("resume", None)
98
  load = cfg.get("load", None)
99
- checkpoint_path = None
100
  load_id = None
101
- if load and not is_run_id(load):
102
  checkpoint_path = load
103
- if resume:
104
  load_id = resume
105
- elif load and is_run_id(load):
106
- load_id = load
107
- else:
108
- load_id = None
109
 
110
  if load_id:
111
- checkpoint_path = get_latest_checkpoint(output_dir / "checkpoints")
 
 
 
112
 
113
  if checkpoint_path and is_rank_zero:
114
  print(f"Will load checkpoint from {checkpoint_path}")
 
11
  import sys
12
  import subprocess
13
  import time
14
+ import os
15
+ import re
16
+ import zipfile
17
  from pathlib import Path
18
 
19
  import hydra
 
21
  from omegaconf.omegaconf import open_dict
22
 
23
  from utils.print_utils import cyan
24
+ from utils.ckpt_utils import is_run_id
25
  from utils.cluster_utils import submit_slurm_job
26
  from utils.distributed_utils import is_rank_zero
27
 
28
+ WANDB_RUN_ID_FILE = ".wandb_run_id"
29
+
30
+
31
+ def _process_rank() -> int:
32
+ return int(os.environ.get("RANK", 0))
33
+
34
+
35
+ def _checkpoint_step(path: Path) -> int:
36
+ step_match = re.search(r"step[=_-]?(\d+)", path.stem)
37
+ return int(step_match.group(1)) if step_match else -1
38
+
39
+
40
+ def _checkpoint_looks_complete(path: Path) -> bool:
41
+ if not path.exists() or not path.is_file() or path.suffix != ".ckpt" or path.stat().st_size == 0:
42
+ return False
43
+ with path.open("rb") as handle:
44
+ header = handle.read(2)
45
+ if header == b"PK":
46
+ return zipfile.is_zipfile(path)
47
+ return header.startswith(b"\x80")
48
+
49
+
50
+ def get_latest_checkpoint(checkpoint_folder: Path, pattern: str = "*.ckpt"):
51
+ if not checkpoint_folder.exists():
52
+ return None
53
+ checkpoint_files = [path for path in checkpoint_folder.glob(pattern) if _checkpoint_looks_complete(path)]
54
  if not checkpoint_files:
55
  return None
56
+
57
+ def checkpoint_key(path: Path):
58
+ return path.name == "last.ckpt", _checkpoint_step(path), path.stat().st_mtime
59
+
60
+ return max(checkpoint_files, key=checkpoint_key)
61
+
62
+
63
+ def validate_resume_checkpoint(checkpoint_path: Path) -> Path:
64
+ if not checkpoint_path.exists():
65
+ raise FileNotFoundError(f"Resume checkpoint does not exist: {checkpoint_path}")
66
+ if not checkpoint_path.is_file():
67
+ raise ValueError(f"Resume checkpoint is not a file: {checkpoint_path}")
68
+ if checkpoint_path.suffix != ".ckpt":
69
+ raise ValueError(f"Resume checkpoint must be a .ckpt file: {checkpoint_path}")
70
+ if checkpoint_path.stat().st_size == 0:
71
+ raise ValueError(f"Resume checkpoint is empty: {checkpoint_path}")
72
+ return checkpoint_path
73
+
74
+
75
+ def discover_wandb_run_id(output_dir: Path):
76
+ run_id_file = output_dir / WANDB_RUN_ID_FILE
77
+ if run_id_file.exists():
78
+ run_id = run_id_file.read_text().strip()
79
+ if not run_id:
80
+ raise ValueError(f"W&B run id file is empty: {run_id_file}")
81
+ return run_id
82
+
83
+ wandb_dir = output_dir / "wandb"
84
+ if wandb_dir.exists():
85
+ run_dirs = [path for path in wandb_dir.iterdir() if path.is_dir()]
86
+ run_dirs = [path for path in run_dirs if re.match(r"(offline-run|run)-.+-[A-Za-z0-9]+$", path.name)]
87
+ if run_dirs:
88
+ run_dir = max(run_dirs, key=lambda path: path.stat().st_mtime)
89
+ return run_dir.name.rsplit("-", 1)[-1]
90
+ return None
91
+
92
+
93
+ def wait_for_wandb_run_id(output_dir: Path, timeout_s: float = 300.0):
94
+ run_id_file = output_dir / WANDB_RUN_ID_FILE
95
+ deadline = time.time() + timeout_s
96
+ while time.time() < deadline:
97
+ if run_id_file.exists():
98
+ run_id = run_id_file.read_text().strip()
99
+ if run_id:
100
+ return run_id
101
+ time.sleep(0.5)
102
+ raise TimeoutError(f"Timed out waiting for rank 0 to create W&B run id file: {run_id_file}")
103
+
104
+
105
+ def create_wandb_run_id_on_rank_zero(output_dir: Path, requested_run_id=None):
106
+ if requested_run_id:
107
+ run_id = requested_run_id
108
+ else:
109
+ run_id = discover_wandb_run_id(output_dir)
110
+ if run_id is None:
111
+ import wandb
112
+ run_id = wandb.util.generate_id()
113
+
114
+ output_dir.mkdir(parents=True, exist_ok=True)
115
+ run_id_file = output_dir / WANDB_RUN_ID_FILE
116
+ if run_id_file.exists():
117
+ existing_run_id = run_id_file.read_text().strip()
118
+ if existing_run_id and existing_run_id != run_id:
119
+ raise ValueError(
120
+ f"Output directory already belongs to W&B run id {existing_run_id}, "
121
+ f"but {run_id} was requested. Use a different output_dir or resume id."
122
+ )
123
+ run_id_file.write_text(f"{run_id}\n")
124
+ return run_id
125
+
126
+
127
+ def get_or_create_wandb_run_id(output_dir: Path, requested_run_id=None):
128
+ if _process_rank() == 0:
129
+ return create_wandb_run_id_on_rank_zero(output_dir, requested_run_id=requested_run_id)
130
+ return wait_for_wandb_run_id(output_dir)
131
+
132
+
133
+ def validate_wandb_run_id(run_id: str, option_name: str) -> str:
134
+ run_id = str(run_id)
135
+ if not is_run_id(run_id):
136
+ raise ValueError(
137
+ f"{option_name}={run_id!r} is not an 8-character W&B run id. "
138
+ "Use load=/path/to/checkpoint for local weight loading or resume_ckpt_path=/path/to.ckpt "
139
+ "for deterministic Lightning training resume."
140
+ )
141
+ return run_id
142
+
143
 
144
  def run_local(cfg: DictConfig):
145
  # delay some imports in case they are not needed in non-local envs for submission
 
171
  OmegaConf.set_readonly(hydra_cfg, True)
172
 
173
  output_dir = Path(hydra_cfg.runtime.output_dir)
 
174
 
175
  if is_rank_zero:
176
  print(cyan(f"Outputs will be saved to:"), output_dir)
177
  (output_dir.parents[1] / "latest-run").unlink(missing_ok=True)
178
  (output_dir.parents[1] / "latest-run").symlink_to(output_dir, target_is_directory=True)
179
 
180
+ training_requested = "training" in cfg.experiment.tasks
181
+ checkpoint_dir = output_dir / "checkpoints"
182
+ auto_resume = bool(getattr(cfg, "auto_resume", True))
183
+ explicit_resume_ckpt = getattr(cfg, "resume_ckpt_path", None)
184
+ auto_resume_checkpoint_path = None
185
+ if training_requested:
186
+ if explicit_resume_ckpt:
187
+ auto_resume_checkpoint_path = validate_resume_checkpoint(Path(explicit_resume_ckpt))
188
+ elif auto_resume:
189
+ auto_resume_checkpoint_path = get_latest_checkpoint(checkpoint_dir)
190
+ if auto_resume_checkpoint_path is not None:
191
+ auto_resume_checkpoint_path = validate_resume_checkpoint(auto_resume_checkpoint_path)
192
+
193
+ if auto_resume_checkpoint_path and is_rank_zero:
194
+ print(cyan("Auto-resuming training from:"), auto_resume_checkpoint_path)
195
+
196
+ with open_dict(cfg):
197
+ cfg._auto_resuming = auto_resume_checkpoint_path is not None
198
+ cfg._resume_checkpoint_path = str(auto_resume_checkpoint_path) if auto_resume_checkpoint_path else None
199
+
200
+ resume = cfg.get("resume", None)
201
+ if resume:
202
+ resume = validate_wandb_run_id(resume, "resume")
203
+
204
  # Set up logging with wandb.
205
  if cfg.wandb.mode != "disabled":
206
  # If resuming, merge into the existing run on wandb.
207
+ wandb_run_id = get_or_create_wandb_run_id(output_dir, requested_run_id=resume)
208
+ name = None if auto_resume_checkpoint_path or resume else f"{cfg.name} ({output_dir.parent.name}/{output_dir.name})"
209
 
210
  if "_on_compute_node" in cfg and cfg.cluster.is_compute_node_offline:
211
  logger_cls = OfflineWandbLogger
 
221
  project=cfg.wandb.project,
222
  log_model=False,
223
  config=OmegaConf.to_container(cfg),
224
+ id=wandb_run_id,
225
  resume="auto"
226
  )
227
 
228
  else:
229
  logger = None
230
 
231
+ # Load ckpt. W&B run ids only identify the logging run; checkpoint loading
232
+ # uses local output_dir/checkpoints or explicit local/HF load paths.
233
  load = cfg.get("load", None)
234
+ checkpoint_path = auto_resume_checkpoint_path
235
  load_id = None
236
+ if checkpoint_path is None and load and not is_run_id(str(load)):
237
  checkpoint_path = load
238
+ if checkpoint_path is None and resume:
239
  load_id = resume
240
+ elif checkpoint_path is None and load and is_run_id(str(load)):
241
+ load_id = str(load)
 
 
242
 
243
  if load_id:
244
+ checkpoint_path = get_latest_checkpoint(checkpoint_dir)
245
+ if checkpoint_path is None:
246
+ raise FileNotFoundError(f"No checkpoint found under {checkpoint_dir} for run id {load_id}")
247
+ checkpoint_path = validate_resume_checkpoint(checkpoint_path)
248
 
249
  if checkpoint_path and is_rank_zero:
250
  print(f"Will load checkpoint from {checkpoint_path}")