Source code for pimm.engines.hooks.optimizer
"""Optimizer mutation and scheduling hooks."""
from pimm.utils.scheduler import CosineScheduler
from .default import HookBase
from .builder import HOOKS
[docs]
@HOOKS.register_module()
class WeightDecayExclusion(HookBase):
"""Rewrite optimizer param groups to exclude selected params from weight decay.
Runs once in ``before_train``: walks the optimizer's existing parameter
groups (preserving any layer-wise learning rates) and splits each into a
decay group and a no-decay group based on parameter name/shape. Excluded
parameters get ``weight_decay=0.0`` and are flagged ``apply_wd=False``; kept
parameters are flagged ``apply_wd=True``. These flags are honoured by
:class:`WeightDecayScheduler`. Logs the resulting group counts. Registered
as ``WeightDecayExclusion``.
Args:
exclude_bias_from_wd (bool): Exclude parameters whose name ends in
``.bias``. Defaults to ``True``.
exclude_norm_from_wd (bool): Exclude parameters with ``"norm"`` in their
name. Defaults to ``True``.
exclude_gamma_from_wd (bool): Exclude parameters with ``"gamma"`` in
their name. Defaults to ``True``.
exclude_token_from_wd (bool): Exclude parameters with ``"token"`` in
their name. Defaults to ``True``.
exclude_ndim_1_from_wd (bool): Exclude any 1-D parameter (biases,
norm/scale vectors, etc.). Defaults to ``True``.
Note:
Mutates ``trainer.optimizer.param_groups`` in place before training. Pair
with :class:`WeightDecayScheduler` to schedule weight decay only on the
kept (``apply_wd=True``) groups; place this hook before the scheduler.
Example:
Add to ``cfg.hooks`` before the weight-decay scheduler; once in
``before_train`` it rewrites the optimizer's parameter groups:
.. code-block:: python
hooks = [
dict(type="WeightDecayExclusion"),
dict(type="WeightDecayScheduler", base_value=0.04,
final_value=0.2),
]
# → splits each optimizer param group into an apply_wd=True group and an
# apply_wd=False group (weight_decay=0.0) holding biases/norm/gamma/
# token/1-D params; logs the resulting with/without-wd group counts
"""
def __init__(
self,
exclude_bias_from_wd=True,
exclude_norm_from_wd=True,
exclude_gamma_from_wd=True,
exclude_token_from_wd=True,
exclude_ndim_1_from_wd=True,
):
self.exclude_bias_from_wd = exclude_bias_from_wd
self.exclude_norm_from_wd = exclude_norm_from_wd
self.exclude_gamma_from_wd = exclude_gamma_from_wd
self.exclude_token_from_wd = exclude_token_from_wd
self.exclude_ndim_1_from_wd = exclude_ndim_1_from_wd
def _should_exclude_from_wd(self, name, param):
if self.exclude_bias_from_wd and name.endswith('.bias'):
return True
if self.exclude_norm_from_wd and 'norm' in name.lower():
return True
if self.exclude_gamma_from_wd and 'gamma' in name.lower():
return True
if self.exclude_token_from_wd and 'token' in name.lower():
return True
if self.exclude_ndim_1_from_wd and param.ndim == 1:
return True
return False
[docs]
def before_train(self):
model = self.trainer.model
if hasattr(model, 'module'): # DDP case
model = model.module
# Get original parameter groups configuration
original_groups = self.trainer.optimizer.param_groups.copy()
# Create new parameter groups
new_param_groups = []
for group in original_groups:
# Split this group into two: with and without weight decay
wd_params = []
no_wd_params = []
for param in group['params']:
# Find parameter name
param_name = None
for name, model_param in model.named_parameters():
if model_param is param:
param_name = name
break
if param_name and self._should_exclude_from_wd(param_name, param):
no_wd_params.append(param)
else:
wd_params.append(param)
# Create group with weight decay if there are parameters
if wd_params:
wd_group = group.copy()
wd_group['params'] = wd_params
wd_group['apply_wd'] = True # Mark for weight decay scheduler
new_param_groups.append(wd_group)
# Create group without weight decay if there are parameters
if no_wd_params:
no_wd_group = group.copy()
no_wd_group['params'] = no_wd_params
no_wd_group['weight_decay'] = 0.0
no_wd_group['apply_wd'] = False # Mark to skip weight decay scheduler
new_param_groups.append(no_wd_group)
# Update optimizer with new parameter groups
self.trainer.optimizer.param_groups = new_param_groups
self.trainer.logger.info(f"Reorganized optimizer into {len(new_param_groups)} parameter groups")
# Log parameter counts for debugging
wd_count = sum(len(g['params']) for g in new_param_groups if g.get('apply_wd', True))
no_wd_count = sum(len(g['params']) for g in new_param_groups if not g.get('apply_wd', True))
self.trainer.logger.info(f"Parameter groups with weight decay: {wd_count}")
self.trainer.logger.info(f"Parameter groups without weight decay: {no_wd_count}")
[docs]
@HOOKS.register_module()
class WeightDecayScheduler(HookBase):
"""Apply a cosine schedule to optimizer parameter-group weight decay.
In ``before_train`` it builds a :class:`~pimm.utils.scheduler.CosineScheduler`
spanning ``cfg.scheduler.total_steps * warmup_ratio`` iterations (advanced to
the current step so resumes continue smoothly). In ``before_step`` it steps
the schedule and writes the resulting weight decay onto every optimizer group
flagged ``apply_wd=True`` (groups flagged ``False`` keep their original, e.g.
``0.0``), then logs it to the writer as ``params/wd``. Registered as
``WeightDecayScheduler``.
Args:
base_value (float): Starting weight decay. Defaults to ``0.04``.
final_value (float): Ending weight decay after the schedule. Defaults to
``0.2``.
warmup_ratio (float): Fraction of ``cfg.scheduler.total_steps`` over
which the cosine schedule runs (scales the schedule length).
Defaults to ``1.0``.
Note:
The ``apply_wd`` flags are produced by :class:`WeightDecayExclusion`;
without it every group defaults to ``apply_wd=True`` and is scheduled.
Place this hook after ``WeightDecayExclusion``.
Example:
Add to ``cfg.hooks`` after ``WeightDecayExclusion``; it cosine-schedules
weight decay each step:
.. code-block:: python
hooks = [
dict(type="WeightDecayExclusion"),
dict(type="WeightDecayScheduler", base_value=0.04,
final_value=0.2, warmup_ratio=1.0),
]
# → every step (before_step) sets param_group["weight_decay"] (cosine
# 0.04 -> 0.2) on every apply_wd=True group and writes the scalar
# "params/wd" to the writer; apply_wd=False groups keep weight_decay=0.0
"""
def __init__(
self,
base_value=0.04,
final_value=0.2,
warmup_ratio=1.0,
):
self.base_value = base_value
self.final_value = final_value
self.warmup_ratio = warmup_ratio
self.scheduler = None
[docs]
def before_train(self):
curr_step = getattr(self.trainer, "global_step", 0) or (
self.trainer.start_epoch * len(self.trainer.train_loader)
)
self.scheduler = CosineScheduler(
base_value=self.base_value,
final_value=self.final_value,
total_iters=self.trainer.cfg.scheduler.total_steps * self.warmup_ratio,
)
self.scheduler.iter = curr_step
[docs]
def before_step(self):
wd = self.scheduler.step()
for param_group in self.trainer.optimizer.param_groups:
# Only apply scheduled weight decay to groups marked for it
if param_group.get('apply_wd', True):
param_group["weight_decay"] = wd
# Groups with apply_wd=False keep their original weight_decay (should be 0.0)
if self.trainer.writer is not None:
self.trainer.writer.add_scalar("params/wd", wd, self.scheduler.iter)