LR Schedules¶
LR-schedule seam: one interface for zero, one, or many schedulers.
A runner may own more than one optimizer — :class:GANTrainingRunner owns
optimizer_G and optimizer_D — but the trainer takes a single
lr_scheduler. :class:SchedulerGroup is the composite that closes that
gap: the trainer always holds one of these, whether the run configured no
schedule, one, or one per optimizer.
See docs/design/lr-schedule-group-rfc.md for the full rationale.
LRSchedule
¶
Bases: Protocol
What the trainer, checkpointer and LR logger need from a schedule.
Structural, not nominal: every torch.optim.lr_scheduler satisfies it
without inheriting anything from SR-Forge — which is the point, since
inheriting :class:~torch.optim.lr_scheduler.LRScheduler forces a
subclass to supply an optimizer it may not have.
Static-only, deliberately: nothing isinstance-checks against this, so
it is not @runtime_checkable. A runtime check would compare method
names and not signatures, which is exactly the false assurance a
schedule protocol must not give.
The protocol carries no behaviour; the method bodies are ... and
it is never instantiated. All runtime behaviour lives in
:class:SchedulerGroup.
LR reporting is deliberately absent: a learning rate belongs to the
optimizer, not to the thing that mutates it. The LR loggers read
ctx.optimizers.
SchedulerGroup
¶
Zero or more LR schedules over a runner's optimizers, driven as one.
An empty group is the constant-LR case — step() iterating nothing
is the whole of a null schedule's behaviour, so no separate null class
is needed.
Reporting learning rates is deliberately not here. A learning rate
belongs to the optimizer; a schedule is only what mutates it. The LR
loggers read ctx.optimizers directly (see
:func:srforge.training.hooks.learning_rate_metrics). The optimizers
this class does hold are for validating its members against the runner
and naming their checkpoint state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
schedulers
|
Iterable[LRSchedule] | None
|
Schedules to drive. Each is expected to satisfy
:class: |
None
|
optimizers
|
Mapping[str, Optimizer] | None
|
The runner's optimizers, keyed as
:meth: |
None
|
of(value: LRSchedule | SchedulerGroup | None, runner: EpochRunner | None = None) -> SchedulerGroup
classmethod
¶
Return value as a group, whatever shape it arrived in.
A run can express its LR schedule three different ways, but the trainer wants one type to hold and step. This is the single place that converts:
None— no schedule configured — becomes an empty group: nothing to step, so the learning rate stays constant.- a single scheduler becomes a group of one.
- a :class:
SchedulerGroup, which is what a run with several optimizers builds, is returned unchanged.
Passing runner additionally binds its optimizers to the group. Binding is what lets checkpoint state be keyed by optimizer name rather than list position, and what catches a scheduler wired to an optimizer this runner does not train. A group that already carries optimizers keeps the ones it has.
Example — the trainer's entire scheduler setup is this one line::
self.lr_scheduler = SchedulerGroup.of(lr_scheduler, training_runner)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
LRSchedule | SchedulerGroup | None
|
Whatever the caller configured: |
required |
runner
|
EpochRunner | None
|
Training runner to bind against; only its
|
None
|
Returns:
| Type | Description |
|---|---|
SchedulerGroup
|
A group — never |
Raises:
| Type | Description |
|---|---|
ValueError
|
A scheduler in value drives an optimizer that runner does not own. |
step(metric: float | Tensor | None = None) -> None
¶
Step every member, passing metric only to those that want it.
The routing is not cosmetic: LRScheduler.step takes epoch and
ReduceLROnPlateau.step takes metrics, so handing a validation
loss to the wrong one does not raise — it lands in the epoch slot
and silently corrupts last_epoch.
state_dict() -> dict
¶
Member states keyed by optimizer name.
Keyed by name rather than by position so that reordering the
schedulers: list in a config between runs cannot silently swap
G's and D's state on resume.
load_state_dict(state_dict: dict) -> None
¶
Restore member states, accepting pre-group checkpoints.
A checkpoint written before this class existed holds one torch
scheduler's state directly, with no "schedulers" key. It is
routed to the single member; a group that has since gained or lost
schedulers warns and starts them fresh rather than failing the
resume.