Coverage for cafl4ds/schedule.py: 100%
17 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-17 14:39 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-17 14:39 +0000
1"""Learning-rate schedules for the training loops.
3Phase-0 P0.1/P0.2 ran a **flat** learning rate over a single ~25-step pass. P0.2.1 found that
4a genuine healthy SimSiam baseline (a RankMe that recovers and holds, rather than decaying
5indistinguishably from a forced collapse) needs a *fair* training regime: multiple epochs with
6a **warmup + cosine-decay** schedule. As the LR anneals, the intact model's effective rank
7re-expands from the early-training dip — the healthy U-shape that separates it from the
8collapsed arm. This module supplies that schedule as a plain, Hydra-instantiable factory.
9"""
11from __future__ import annotations
13import math
15from torch import optim
16from torch.optim.lr_scheduler import LambdaLR
19def warmup_cosine_schedule(
20 optimizer: optim.Optimizer,
21 total_steps: int,
22 warmup_frac: float = 0.1,
23 min_lr_frac: float = 0.0,
24) -> LambdaLR:
25 """Linear warmup then cosine decay, expressed as a multiplier on the base LR.
27 The returned scheduler is stepped **once per optimizer step** (not per epoch). The
28 multiplier ramps linearly from ``0`` to ``1`` over the first ``warmup_frac`` of
29 ``total_steps``, then follows a half-cosine down to ``min_lr_frac`` at the final step.
31 Args:
32 optimizer: The optimizer whose base LR(s) are scaled.
33 total_steps: Total number of optimizer steps over the whole run
34 (``epochs * batches_per_epoch``).
35 warmup_frac: Fraction of ``total_steps`` spent warming up, in ``[0, 1)``.
36 min_lr_frac: Final LR as a fraction of the base LR (``0`` decays fully to zero).
38 Returns:
39 A :class:`~torch.optim.lr_scheduler.LambdaLR` implementing the schedule.
41 Raises:
42 ValueError: If ``total_steps`` is not positive or ``warmup_frac`` is out of range.
43 """
44 if total_steps <= 0:
45 raise ValueError(f"total_steps must be positive, got {total_steps}.")
46 if not 0.0 <= warmup_frac < 1.0:
47 raise ValueError(f"warmup_frac must be in [0, 1), got {warmup_frac}.")
48 warmup_steps = max(1, int(round(warmup_frac * total_steps)))
50 def lr_lambda(step: int) -> float:
51 if step < warmup_steps:
52 return (step + 1) / warmup_steps
53 progress = (step - warmup_steps) / max(1, total_steps - warmup_steps)
54 cosine = 0.5 * (1.0 + math.cos(math.pi * min(1.0, progress)))
55 return min_lr_frac + (1.0 - min_lr_frac) * cosine
57 return LambdaLR(optimizer, lr_lambda)