Approach: compute per-parameter L2 norms for params and grads, form ratio = param_norm / (grad_norm + eps), clip to [lower, upper], then scaled_lr = base_lr * clipped_ratio. Use numerically stable ops (eps, max) and operate per-parameter or per-layer by grouping parameters.python
import torch
def layerwise_lr_multipliers(params, grads, base_lr, lower=0.0, upper=10.0, eps=1e-6, group_by=None):
"""
params, grads: iterables of Tensors (same order). For grouping, provide list of lists of indices or None.
Returns: list of learning rate multipliers aligned with params
"""
# compute per-tensor L2 norms (flattened)
p_norms = [p.detach().pow(2).sum().sqrt() for p in params]
g_norms = [g.detach().pow(2).sum().sqrt() for g in grads]
def safe_ratio(pn, gn):
# avoid divide-by-zero; if both near zero -> ratio = 1 (no scaling)
gn_safe = torch.max(gn, torch.tensor(eps, device=gn.device))
ratio = pn / gn_safe
# if pn is near zero and gn small, set ratio=1 to avoid weird scaling
near_zero = (pn <= eps) & (gn <= eps)
if near_zero:
return torch.tensor(1.0, device=pn.device)
return ratio
if group_by is None:
# per-parameter multipliers
multipliers = []
for pn, gn in zip(p_norms, g_norms):
r = safe_ratio(pn, gn)
r_clipped = torch.clamp(r, min=lower, max=upper)
multipliers.append((base_lr * r_clipped).item())
return multipliers
else:
# group_by: iterable of iterables of param indices -> compute one multiplier per group
group_mult = {}
for group in group_by:
# sum squares across group for a single group norm
pn_group = sum(p.pow(2).sum() for i,p in enumerate(params) if i in group).sqrt()
gn_group = sum(g.pow(2).sum() for i,g in enumerate(grads) if i in group).sqrt()
r = safe_ratio(pn_group, gn_group)
r_clipped = torch.clamp(r, min=lower, max=upper)
lr = (base_lr * r_clipped).item()
for i in group:
group_mult[i] = lr
return [group_mult[i] for i in range(len(params))]
Integration into optimizer step:- Compute grads via backward().- Collect params and grads (skip params with no grad).- Call layerwise_lr_multipliers to get per-param lr.- For each param: update using chosen optimizer rule (SGD/Adam) but replace base_lr with scaled lr (or multiply step by multiplier).- For adaptive optimizers (Adam), apply LAMB-style trust ratio: compute update direction, then apply param-wise scaling: param += -scaled_lr * update_dir / (1 + weight_decay).Complexity: O(N) to compute norms and apply updates. Edge cases: zero-valued params/grads, sparse grads (handle separately), mixed precision (use float32 eps and casts). This implementation is agnostic to model size; for very large models compute norms in fp32 and aggregate across devices using all-reduce when using distributed training.