v0.2.5 — stdlib logging, level-aware retention, ETA in maybe_save
Status: current baseline. Supersedes v0.2.4 as the default main state.
Version source: pyproject.toml (version = "0.2.5").
What landed
Three concerns from the v0.2.5 brief, plus the missing resume-from-checkpoint guide:
- Migrate the per-run log to stdlib :mod:
logging. Drop the 1 KB hard cap. Keep INFO+ forever, prune DEBUG older than 3 h. Terminal stays tqdm-only — every strayprint(...)that could clobber an active progress bar is gone. - Add ETA to every Bark milestone push (not just end-of-epoch saves). Per-batch ETA flows from the training loops into
CheckpointManager.maybe_saveso each periodic save fires a push with a currentETA Xhline. - DEBUG-level retention. The new :class:
~lavender_2.logging_setup.DebugRetentionFileHandlerrewrites the log file in place every five minutes, dropping DEBUG records older thanDEBUG_RETENTION_SEC(3 h) while leaving every INFO / WARNING / ERROR / CRITICAL line untouched. - Resume-from-checkpoint guide at
docs/operations/resume.md. - Resume-from-checkpoint helper in
lavender_2.checkpoints:CheckpointManager.latest_checkpoint(run_dir=None, stage=None)picks the newest.pt(filterable by stage prefix);CheckpointManager.restore(path=None, stage=None, ...)reattaches weights and optimizer state to an existing manager; top-levelresume_from_checkpoint(path=None, stage=None)returns(model_state, opt_state, meta)for the build-from-fresh notebook flow. The notebook's pretrain cell now uses the helper before entering the loop, and end-of-epoch saves carryepoch_complete=Truewhile periodic saves carryepoch_complete=False, so resume does not accidentally skip an in-flight epoch. Replaces the deadresume_from_checkpoint(...)stub the notebook's "Resuming training" markdown advertised against the abandoned pre-v0.2.4checkpoints/ckpt_<stage>_NNNN.ptflat layout.
New module: lavender_2.logging_setup
from lavender_2.logging_setup import setup_logging, get_loggersetup_logging(level=DEBUG, name="lavender_2", log_dir="logs") -> strconfigures the root logger with a single :class:DebugRetentionFileHandler, returns the per-run file path. Idempotent across notebook restarts (existing handlers are stripped first).get_logger(suffix=None) -> logging.Loggeris the canonical way for every module in :mod:lavender_2to grab its own child logger under thelavender_2.<suffix>namespace. Re-tuninglogging.getLogger("lavender_2").setLevel(...)from the notebook re-tunes every module at once.
Log file shape
2026-04-26 01:30:58 [INFO ] RUN start log=logs/run-20260426-013058.log level=DEBUG
2026-04-26 01:30:58 [INFO ] device=cuda
2026-04-26 01:30:58 [INFO ] DATA 1,123,456 convs, 12 sources
2026-04-26 01:30:58 [INFO ] TOKEN ready vocab=200023
2026-04-26 01:30:58 [INFO ] run dir checkpoints/run-2026-04-26_01-30-58
2026-04-26 02:00:58 [INFO ] [Pretrain 1/3] progress: 100,000/2,165,434 (4.6%) CE=3.812 Aux=0.000 stage-ETA=128.5h
2026-04-26 02:30:58 [INFO ] SAVE pretrain-02-30-58-0001.pt @ 02:30:58
2026-04-26 02:30:58 [INFO ] milestone 'checkpoint saved: pretrain-02-30-58-0001.pt, epoch 1/3, CE 3.812, ETA 128.5h' bark ok=1 fail=0
2026-04-26 02:30:58 [DEBUG ] [torch.save pretrain-02-30-58-0001.pt] done in 1.2s
2026-04-26 02:30:58 [DEBUG ] [Checkpoint] checkpoints/run-2026-04-26_01-30-58/pretrain-02-30-58-0001.pt (saved 02:30:58)Three hours after that block lands, every [DEBUG ] line above is gone — pruning rewrites the file with the same INFO trail and a sliding window of recent DEBUG. Hour 30 of the same run still has all of those SAVE / progress: / milestone lines on disk.
Retention rules
| Level | Retention |
|---|---|
CRITICAL, ERROR, WARNING, INFO | forever |
DEBUG | dropped when now - timestamp > 3 h |
Pruning runs at most once every 5 minutes (PRUNE_INTERVAL_SEC), so even a many-MB log only rewrites a few times per hour. The rewrite is atomic (tmp + os.replace); a crash mid-prune leaves the previous valid file in place and opens a fresh handle on next emit.
Module changes
| Module | What moved to logging |
|---|---|
utils.Heartbeat | START/END at DEBUG; in-block ping at DEBUG (with one-line tqdm.write for terminal interleave). set_logger(...) is now a deprecated no-op kept for source compatibility. |
utils.log_memory | INFO line per probe; missing psutil is a one-shot WARNING. |
utils.check_memory_budget | WARNING when RSS crosses 90% of ceiling; raises MemoryBudgetExceeded at 100%. |
notify.send_bark | ok → DEBUG; failure → WARNING (was a print line). |
notify.notify_milestone | INFO log on every milestone with bark ok=N fail=N. |
notify.SwapWatchdog | swap-over-threshold → WARNING; under-threshold poll → DEBUG. |
cache.read_meta / write_meta | corrupt / mismatched / failed-write paths now WARNING. |
data.PretrainDataset / SFTDataset / DPODataset | every "cache hit" / "resuming at conv X" line is INFO; tokenisation tqdm bars are unchanged. |
ingest.print_preflight / load_all_datasets | every per-source line is INFO; skipped datasets are WARNING. |
checkpoints.CheckpointManager | SAVE <file> @ HH:MM:SS at INFO (persists forever); RETAIN dropped N at INFO; full-path [Checkpoint] /…/file (saved …) at DEBUG (gone after 3 h). |
training.pretrain_epoch / sft_train_epoch / dpo_train_epoch | first-batch / slow-batch lines are INFO and WARNING respectively; new periodic INFO progress: X/Y (P%) loss=L stage-ETA=Hh every 5 minutes; per-batch ETA passed to maybe_save. |
ETA in every push
The v0.2.4 wiring computed ETA only at end-of-epoch boundaries. v0.2.5 adds a per-batch ETA inside the training loops:
eta_total = _eta_hours_within_epoch(
epoch_started, epoch_idx, total_epochs,
n_batches, n_total_batches,
)
ckpt_mgr.maybe_save(
stage=stage, epoch=epoch_idx, epochs=total_epochs,
ce_loss=total_loss / n_batches,
remaining_hours=eta_total,
)Every periodic save (every 30 min by default) now fires a Bark push with the up-to-date ETA in the body: [lavender-2] checkpoint saved: pretrain-02-30-58-0001.pt, epoch 1/3, CE 3.812, ETA 128.5h.
The new _eta_hours_within_epoch helper treats batch progress as fractional epochs and projects forward to total_epochs, so the ETA reacts within minutes to a slowdown rather than waiting until the next epoch boundary.
Notebook clean-up
- The imports cell now calls
setup_logging()first, then prints exactly one line on the terminal:lavender_2 v0.2.5 log: logs/run-<ts>.log. Everything else is logger calls. - Orchestration cells dropped their
print(...)lines in favour of_log.info(...)— the file gets the same content; the terminal stays clean. - Interactive
chat()keeps itsprint()calls because it's a REPL withinput(); no tqdm bar can be active at the same time by construction. - The v0.2.3
TrainingLoggerimport is gone. The class itself stays as a deprecation shim inlavender_2/logs.pyso old notebook cells still parse.
Resume guide
docs/operations/resume.md walks through resuming run-2026-04-24_18-53-41 end-to-end:
- What's in the checkpoint vs. what's lost (DataLoader iterator state, RNG state).
- Which cells re-run, and what each one costs in real wall time on the cached corpus.
- The actual cell to insert between
CheckpointManagerinstantiation and the pretrain loop. - Two flavours of "resume the in-flight epoch" vs. "skip ahead to the next epoch."
The TL;DR for the open question — "Or do I need to redo all the datasets on the model again?" — is no. Tokenisation is mmap on disk; the new run picks them up in seconds.
What's not in this release
- Within-epoch resume. Saving / restoring the DataLoader iterator state would require either a deterministic shuffle seed plus the batch index, or a pickleable iterator. Out of scope for v0.2.5; would need a checkpoint-shape change. The resume guide documents the workaround.
- Live Bark server still untested in CI. Same caveat as v0.2.3 / v0.2.4 —
notify_milestoneis verified offline; live call works against the*.bark.envtemplate that already exists on this machine.
Upstream references
- Module-by-module reference:
docs/development/modules.md. - Resume from checkpoint:
docs/operations/resume.md. - Push notifications + run log:
v0.2.3(now superseded by stdlib logging here).