Skip to content

Commit d15ec96

Browse files
[Debug] cache_dir + wandb
1 parent f84c7c2 commit d15ec96

6 files changed

Lines changed: 75 additions & 30 deletions

File tree

stable_pretraining/_config.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@
3434
"CRITICAL",
3535
)
3636

37+
# Sentinel distinguishing "argument omitted" from an explicit ``None`` in
38+
# :func:`set`. ``cache_dir=None`` must raise (caching is mandatory for
39+
# parallel-safe run isolation), while omitting it is a no-op.
40+
_UNSET = object()
41+
3742
_CLEANUP_KEYS = (
3843
"checkpoints",
3944
"logs",
@@ -251,6 +256,21 @@ def cache_dir(self, value: Optional[str]) -> None:
251256
)
252257
if not value.strip():
253258
raise ValueError("cache_dir must not be empty")
259+
# Forbid relative paths: the run_dir is resolved with ``.resolve()``
260+
# (manager) against the *current* CWD, which Hydra's ``job.chdir``
261+
# silently changes — a relative cache_dir would then land in a
262+
# different (and CWD-dependent) place per job. We expanduser first
263+
# so ``~/...`` (which expands to an absolute path) is accepted and
264+
# stored verbatim for portability; only genuinely relative paths
265+
# like ``runs`` or ``./out`` are rejected.
266+
if not os.path.isabs(os.path.expanduser(value)):
267+
raise ValueError(
268+
f"cache_dir must be an absolute path, got {value!r}. "
269+
"A relative path is resolved against the current working "
270+
"directory, which Hydra's job.chdir changes — making the "
271+
"run directory non-deterministic across jobs. Pass an "
272+
"absolute path (or a '~/...' path)."
273+
)
254274
self._cache_dir = value
255275

256276
# -- requeue_checkpoint ----------------------------------------------------
@@ -334,7 +354,7 @@ def set(
334354
log_rank: Optional[Union[int, Literal["all"]]] = None,
335355
default_callbacks: Optional[Dict[str, bool]] = None,
336356
default_loggers: Optional[Dict[str, bool]] = None,
337-
cache_dir: Optional[str] = None,
357+
cache_dir: Union[str, object] = _UNSET,
338358
requeue_checkpoint: Optional[bool] = None,
339359
requeue_checkpoint_every_n_steps: Optional[int] = None,
340360
exclude_bias_norm: Optional[bool] = None,
@@ -382,8 +402,9 @@ def set(
382402
ensuring no path collisions across parallel sweep jobs.
383403
Defaults to ``~/.cache/stable-pretraining``. Can be
384404
overridden via the ``SPT_CACHE_DIR`` environment variable.
385-
Set to ``None`` to disable and preserve the standard
386-
Lightning / Hydra directory behavior.
405+
``None`` is **not** allowed — a cache directory is mandatory
406+
so every run gets a unique, parallel-safe output directory.
407+
Passing ``cache_dir=None`` raises ``ValueError``.
387408
388409
.. note::
389410
SLURM ``.out`` / ``.err`` files are created by the
@@ -446,7 +467,16 @@ def set(
446467
if default_loggers is not None:
447468
cfg.default_loggers = default_loggers
448469

449-
if cache_dir is not None:
470+
if cache_dir is not _UNSET:
471+
if cache_dir is None:
472+
raise ValueError(
473+
"cache_dir cannot be None — a cache directory is mandatory so "
474+
"that every run gets a unique, parallel-safe output directory "
475+
"(without it, concurrent jobs collide on files such as "
476+
"wandb_resume.json in the working directory). Pass a path, set "
477+
"the SPT_CACHE_DIR environment variable, or leave it unset to "
478+
"use the default (~/.cache/stable-pretraining)."
479+
)
450480
cfg.cache_dir = cache_dir
451481

452482
if requeue_checkpoint is not None:

stable_pretraining/callbacks/checkpoint_sklearn.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -292,15 +292,6 @@ def on_save_checkpoint(self, trainer, pl_module, checkpoint):
292292
sidecar.parent.mkdir(parents=True, exist_ok=True)
293293
sidecar.write_text(json.dumps(resume_info))
294294
logging.info(f" Wrote {sidecar.resolve()}")
295-
# Also write to CWD if it differs (backward compat),
296-
# but skip when cache_dir is active to avoid polluting CWD.
297-
from stable_pretraining._config import get_config
298-
299-
if get_config().cache_dir is None:
300-
cwd_sidecar = Path(_WANDB_RESUME_FILENAME)
301-
if cwd_sidecar.resolve() != sidecar.resolve():
302-
cwd_sidecar.write_text(json.dumps(resume_info))
303-
logging.info(f" Wrote {cwd_sidecar.resolve()} (compat)")
304295

305296
def on_load_checkpoint(self, trainer, pl_module, checkpoint):
306297
if "wandb" not in checkpoint:

stable_pretraining/callbacks/checkpoint_swanlab.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,6 @@ def on_save_checkpoint(
8080
sidecar.write_text(json.dumps(resume_info))
8181
logging.info(f" Wrote {sidecar.resolve()}")
8282

83-
from stable_pretraining._config import get_config
84-
85-
if get_config().cache_dir is None:
86-
cwd_sidecar = Path(_SWANLAB_RESUME_FILENAME)
87-
if cwd_sidecar.resolve() != sidecar.resolve():
88-
cwd_sidecar.write_text(json.dumps(resume_info))
89-
logging.info(f" Wrote {cwd_sidecar.resolve()} (compat)")
90-
9183
def on_load_checkpoint(
9284
self,
9385
trainer: Trainer,

stable_pretraining/callbacks/checkpoint_trackio.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,6 @@ def on_save_checkpoint(
7979
sidecar.write_text(json.dumps(resume_info))
8080
logging.info(f" Wrote {sidecar.resolve()}")
8181

82-
from stable_pretraining._config import get_config
83-
84-
if get_config().cache_dir is None:
85-
cwd_sidecar = Path(_TRACKIO_RESUME_FILENAME)
86-
if cwd_sidecar.resolve() != sidecar.resolve():
87-
cwd_sidecar.write_text(json.dumps(resume_info))
88-
logging.info(f" Wrote {cwd_sidecar.resolve()} (compat)")
89-
9082
def on_load_checkpoint(
9183
self,
9284
trainer: Trainer,

stable_pretraining/manager.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1636,6 +1636,22 @@ def init_and_sync_wandb(self):
16361636
if wandb_logger is None:
16371637
return
16381638
log_header("Wandb")
1639+
1640+
# Anchor wandb's local files (the ``wandb/`` run tree) to the unique
1641+
# run_dir *before* the experiment is created — accessing
1642+
# ``.experiment`` below triggers ``wandb.init(**_wandb_init)``.
1643+
# Lightning's WandbLogger defaults ``dir``/``save_dir`` to CWD, which
1644+
# is NOT covered by ``default_root_dir`` (unlike CSV/TensorBoard
1645+
# loggers). Without this, parallel jobs sharing a CWD all dump their
1646+
# ``wandb/`` trees into the same place instead of each job's run_dir.
1647+
run_dir = getattr(self, "_run_dir", None)
1648+
already_running = WANDB_AVAILABLE and wandb.run is not None
1649+
if run_dir is not None and not already_running:
1650+
run_dir_str = str(run_dir)
1651+
wandb_logger._wandb_init["dir"] = run_dir_str
1652+
wandb_logger._save_dir = run_dir_str
1653+
logging.info(f" Anchored wandb dir to {run_dir_str}")
1654+
16391655
exp = wandb_logger.experiment
16401656

16411657
if exp.offline:

stable_pretraining/tests/unit/test_cache_dir.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,24 @@ def test_set_via_property(self, tmp_path):
6868
cfg.cache_dir = str(tmp_path)
6969
assert cfg.cache_dir == str(tmp_path)
7070

71-
def test_set_to_none(self, tmp_path):
71+
def test_set_to_none_via_property(self, tmp_path):
72+
# The property setter still permits None — the JAX backend uses it to
73+
# disable checkpointing (see jax/manager.py). Only the public spt.set()
74+
# API prohibits it.
7275
spt_set(cache_dir=str(tmp_path))
7376
cfg = get_config()
7477
cfg.cache_dir = None
7578
assert cfg.cache_dir is None
7679

80+
def test_spt_set_none_raises(self, tmp_path):
81+
# Public API must reject an explicit None: caching is mandatory so each
82+
# run gets a unique, parallel-safe output dir (no CWD collisions).
83+
spt_set(cache_dir=str(tmp_path))
84+
with pytest.raises(ValueError, match="cache_dir cannot be None"):
85+
spt_set(cache_dir=None)
86+
# The failed call must not have mutated the existing value.
87+
assert get_config().cache_dir == str(tmp_path)
88+
7789
def test_rejects_empty_string(self):
7890
with pytest.raises(ValueError, match="must not be empty"):
7991
spt_set(cache_dir="")
@@ -86,6 +98,18 @@ def test_rejects_non_string(self):
8698
with pytest.raises(TypeError, match="must be a str"):
8799
get_config().cache_dir = 123
88100

101+
def test_rejects_relative_path(self):
102+
with pytest.raises(ValueError, match="must be an absolute path"):
103+
spt_set(cache_dir="runs")
104+
with pytest.raises(ValueError, match="must be an absolute path"):
105+
get_config().cache_dir = "./out"
106+
107+
def test_accepts_tilde_path(self):
108+
# '~/...' expands to an absolute path, so it is accepted and stored
109+
# verbatim (expansion happens later in _resolve_run_dir).
110+
spt_set(cache_dir="~/spt_cache_test")
111+
assert get_config().cache_dir == "~/spt_cache_test"
112+
89113
def test_reset_restores_default_cache_dir(self, tmp_path, monkeypatch):
90114
monkeypatch.delenv("SPT_CACHE_DIR", raising=False)
91115
spt_set(cache_dir=str(tmp_path))

0 commit comments

Comments
 (0)