mixle.models.mup module¶
muP (Maximal Update Parametrization) – width-independent lr/init transfer for mixle.models.transformer.
The idea. In the standard parametrization, the learning rate and init scale that work best for a
transformer depend on its width (d_model): as a model gets wider, activations/gradients grow (or
shrink) with width unless the parametrization compensates, so a wider model needs a different tuned
lr than a narrower one – hyperparameter search has to be repeated at every scale. muP (Yang et al.,
“Tensor Programs V: Tuning Large Neural Networks via Zero-Shot Hyperparameter Transfer”, 2022) chooses
init-variance and lr scaling rules per layer role (input/embedding, hidden, output/readout) such
that, in the infinite-width limit, the optimal lr stops moving with width. In practice this means: tune
lr (and init scale) once, cheaply, on a small model, then transfer those hyperparameters to a much
larger model at (almost) zero extra tuning cost – the mechanism a capacity ladder needs so its tuning
bill doesn’t scale with every rung.
Why implemented from scratch instead of depending on the `mup` package. Microsoft’s reference
mup library is not installed in this environment (checked: import mup fails) and adding it as a
dependency is unnecessary weight for what it buys here: the abc-parametrization rules for a transformer
are a short, precisely published table (a handful of closed-form multipliers), not a large or fiddly
algorithm, and mixle already treats torch as an optional soft dependency (see the _HAS_TORCH guard
pattern used across mixle.models) – pulling in a required third-party package for ~10 lines of
well-specified math would work against that. This module implements the rules directly against
mixle.models.transformer.CausalLM’s actual module structure.
The rules (muP for Adam; Tensor Programs V, Table 8 / the ``mup`` library’s Adam column). Let
width_mult = target_width / base_width (the ratio of the layer’s fan-in-scaling width to the base
width it was tuned at). Every parameter is classified into one of three roles:
input – embeddings (token, position) and LayerNorm affine params. Fan-in does not scale with
d_model(it’s the fixed vocab size, or LayerNorm has no fan-in at all), so muP leaves both init variance and lr unscaled (Theta(1)inwidth_mult).hidden – every
Linearinside a transformer block (attention qkv/proj, MLP in/out). Fan-in scales linearly with width. Init variance stays at the standard1/fan_in(std multiplierwidth_mult**-0.5); the lr is scaled down aswidth_mult**-1– this is the headline muP rule and the reason a wide model doesn’t blow up (or stall) with the narrow model’s lr.output – the unembedding/readout. Fan-in scales with width, fan-out (vocab) is fixed. Init variance gets an extra
1/width_multbeyond the hidden rule (std multiplierwidth_mult**-1), lr scales aswidth_mult**-1like hidden, and the forward pass gets an explicitwidth_mult**-1multiplier on the logits (the “c” of the abc-parametrization) so the output scale is also width-independent at init.
Weight tying. CausalLM ties head.weight = tok.weight (see mixle/models/transformer.py)
– the same nn.Parameter object plays both the embedding-lookup and the unembedding-projection role.
Since a single tensor can’t have two different init/lr rules at once, this module follows the standard
treatment for muP with tied embeddings (the same one the mup library’s MuReadout implements):
the shared parameter is classified under the input rule (fixed variance/lr – it is the embedding
table), and the muP output role is instead realized as a multiplicative rescale applied to the
logits at readout time (output_forward_multiplier()), independent of the (shared) weight’s own
init/lr. This reproduces the correct output-scale behavior without touching weight tying.
- class MuPParamGroup(role, lr, n_params)[source]
Bases:
objectOne torch-optimizer param group under muP, tagged with the role it was scaled for.
- apply_mup_init(model, *, base_width, base_std=0.02)[source]
Re-initialize
modelin place per the muP init rules, relative tobase_width.base_stdis the hidden-role init std tuned/measured atbase_width(the mixle default,0.02, matches common transformer practice and is a reasonable base-width value on its own).model.d_modelis read as the target width, sowidth_mult = model.d_model / base_width. LayerNorm weight/bias keep their identity init (1/0, unaffected by width, matching the"input"role’s no-rescale treatment); every other bias is zero-initialized (muP does not rescale bias init); every other weight matrix is drawnNormal(0, base_std * init_std_multiplier(role, width_mult)). Also turns on muP attention-logit scaling (enable_mup_attention()) on every block – the1/head_dimQK scaling is as much a part of “the model is parametrized under muP” as the init/lr rules above, and previously being left at the standard1/sqrt(head_dim)scale was an unintentional gap between what this module documented and what it actually configured.
- enable_mup_attention(model, enabled=True)[source]
Turn on (or off) muP attention-logit scaling on every block of a
CausalLM.Standard attention scales
QK^Tby1/sqrt(head_dim)(the usual “softmax temperature” choice). muP (Tensor Programs V, Table 3 – the attention-logit row of the abc-parametrization) instead requires1/head_dim: because muP’s hidden-role init/lr rules make the correlation structure ofq/kgrow with width (not just their per-coordinate variance, which standard1/sqrt(head_dim)scaling was already designed to control), the standard scale under-divides at wide models and lets attention-logit scale drift with width – exactly the kind of hidden per-layer-role mismatch that breaks the zero-shot lr-transfer guarantee. Seemixle.models.transformer.CausalAttention.mup_attention, which this flips.- Parameters:
enabled (bool)
- Return type:
None
- classify_causal_lm_params(model)[source]
Map every named parameter of a
mixle.models.transformer.CausalLMto its muP role.tok.weight/pos.weight(embeddings) ->"input"– fan-in is the fixed vocab / block length, notd_model.LayerNorm affine params (
blocks.*.ln1/ln2, top-levelln) ->"input"– no fan-in at all, muP leaves them at their standardTheta(1)scale/shift regardless of width.head.weight-> not a separate entry: it is the samenn.Parameterastok.weight(weight tying), somodel.named_parameters()already reports it once, under"tok.weight". See the module docstring for how the muP output role is instead applied at readout time.everything else (the attention qkv/proj and MLP
Linearweights/biases inside each block) ->"hidden"– fan-in scales linearly withd_model.
- init_std_multiplier(role, width_mult)[source]
Return the muP multiplier on init std for a parameter of
roleat the givenwidth_mult.width_mult = target_width / base_width. Multiply the BASE width’s tuned/measured init std by this factor to get the init std to use at the target width:"input"->1(fan-in fixed, e.g. vocab size – no rescale)"hidden"->width_mult ** -0.5(variance1/fan_in, standard, unchanged form)"output"->width_mult ** -1(variance1/fan_in**2– an extra1/width_mult)
- lr_multiplier(role, width_mult)[source]
Return the muP multiplier on learning rate for a parameter of
roleat the givenwidth_mult.width_mult = target_width / base_width. Multiply the BASE width’s tuned lr by this factor to get the transferred lr to use at the target width:"input"->1(constant lr – the muP “don’t touch it” role)"hidden"->width_mult ** -1(the headline muP rule: lr shrinks as the model widens)"output"->width_mult ** -1(same shrink as hidden, for Adam)
- mup_param_groups(model, *, base_width, lr)[source]
Build torch optimizer param groups implementing muP’s per-role lr scaling for
model.lris the BASE (hidden-role) learning rate – the one hyperparameter tuned once, cheaply, atbase_width.model.d_modelis read as the target width. Returns a list of{"params": [...], "lr": ..., "mup_role": ...}dicts suitable fortorch.optim.Adam(groups); the"input"group’s lr is unscaled, the"hidden"/"output"groups getlr * lr_multiplier(role, width_mult)– i.e. passing the same tunedlrat any target width reproduces exactly whattransfer_lr()predicts for that role.transfer_lris the formula; this is the mechanism that applies it to a live model + optimizer.
- output_forward_multiplier(width_mult)[source]
Return the muP readout multiplier (the “c” of abc-parametrization) applied to output-role logits.
Multiply the raw
headoutput by this factor so the readout’s output scale stays width-independent at init, even though (due to weight tying, see the module docstring)head.weightitself is parametrized under the"input"rule rather than a separate"output"init/lr rule.
- transfer_init_std(base_std, base_width, target_width, *, role='hidden')[source]
Rescale
base_std(tuned/measured atbase_width) to the muP init std attarget_width.
- transfer_lr(base_lr, base_width, target_width, *, role='hidden')[source]
Rescale
base_lr(tuned atbase_width) to the muP-predicted optimum attarget_width.This is the deliverable that “collapses the ladder’s tuning bill”: tune
role="hidden"lr once, cheaply, at a smallbase_width, then call this to predict the optimal lr at any largertarget_widthwith (ideally) no further search. Defaults torole="hidden"– the dominant parameter group (attention + MLP) and the one muP’s headline lr-transfer guarantee is about.