| """Initialization utilities for the MoT generation path (mlp_g, layernorm_g). |
| |
| Two regimes are handled by a single entry point `maybe_init_generation_path`: |
| |
| (a) same-size copy : mlp_g.intermediate_size == mlp_v.intermediate_size |
| -> plain state_dict copy (legacy behavior). |
| (b) Net2Wider expansion: mlp_g.intermediate_size > mlp_v.intermediate_size |
| -> function-preserving neuron duplication (Chen et al., |
| "Net2Net") with small Gaussian noise to break symmetry. |
| |
| Only integer-multiple widening is exact; non-integer multiples use round-robin |
| replicate-and-trim with reciprocal-count scaling (still function-preserving, but |
| logged as a warning — integer multiples are strongly preferred). |
| |
| All layernorm_g state is always plain-copied from layernorm_v because layernorms |
| are on hidden_size and unaffected by MLP widening. |
| """ |
| from __future__ import annotations |
|
|
| import logging |
| from typing import Optional |
|
|
| import torch |
| import torch.nn as nn |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| @torch.no_grad() |
| def _net2wider_expand_mlp( |
| mlp_g: nn.Module, |
| mlp_v: nn.Module, |
| noise_std: float = 1e-4, |
| generator: Optional[torch.Generator] = None, |
| ) -> None: |
| """In-place initialize `mlp_g` to be function-equivalent to `mlp_v` via neuron |
| duplication. After this call, for any input x, `mlp_g(x) ~= mlp_v(x)` within |
| O(noise_std * ||x||). |
| |
| Expects a SwiGLU-style MLP with linear modules `.gate_proj`, `.up_proj`, |
| `.down_proj`, all bias-free, with shapes: |
| gate_proj / up_proj : (D, H) |
| down_proj : (H, D) |
| """ |
| d_v = mlp_v.gate_proj.out_features |
| d_g = mlp_g.gate_proj.out_features |
| assert mlp_v.gate_proj.in_features == mlp_g.gate_proj.in_features, ( |
| f"hidden_size mismatch: mlp_v={mlp_v.gate_proj.in_features}, " |
| f"mlp_g={mlp_g.gate_proj.in_features}" |
| ) |
| assert mlp_v.down_proj.out_features == mlp_g.down_proj.out_features, ( |
| f"hidden_size mismatch on down_proj: mlp_v={mlp_v.down_proj.out_features}, " |
| f"mlp_g={mlp_g.down_proj.out_features}" |
| ) |
| assert d_g >= d_v, ( |
| f"mlp_g width {d_g} < mlp_v width {d_v} (shrinking unsupported)" |
| ) |
|
|
| |
| if d_g == d_v: |
| mlp_g.load_state_dict(mlp_v.state_dict()) |
| return |
|
|
| if d_g % d_v != 0: |
| logger.warning( |
| f"Net2Wider: d_g={d_g} not an integer multiple of d_v={d_v}; " |
| f"using replicate-and-trim. Integer multiples (e.g. 2x) are strongly " |
| f"preferred for cleanest function preservation." |
| ) |
|
|
| device = mlp_v.gate_proj.weight.device |
| |
| |
| |
| |
| |
| |
| |
| indices = torch.arange(d_g, device=device) % d_v |
| counts = torch.bincount(indices, minlength=d_v) |
|
|
| |
| for name in ("gate_proj", "up_proj"): |
| src = getattr(mlp_v, name).weight.data |
| dst_param = getattr(mlp_g, name).weight |
| dst = src[indices].clone().to(dtype=dst_param.dtype) |
| if noise_std > 0: |
| noise = torch.empty( |
| dst.shape, dtype=dst.dtype, device=dst.device, |
| ).normal_(mean=0.0, std=noise_std, generator=generator) |
| dst.add_(noise) |
| dst_param.data.copy_(dst) |
|
|
| |
| |
| |
| |
| |
| |
| down_v = mlp_v.down_proj.weight.data |
| dst_param = mlp_g.down_proj.weight |
| scale = (1.0 / counts.to(down_v.dtype)) |
| dst = (down_v[:, indices] * scale[indices]).clone().to(dtype=dst_param.dtype) |
| if noise_std > 0: |
| noise = torch.empty( |
| dst.shape, dtype=dst.dtype, device=dst.device, |
| ).normal_(mean=0.0, std=noise_std, generator=generator) |
| dst.add_(noise) |
| dst_param.data.copy_(dst) |
|
|
|
|
| @torch.no_grad() |
| def init_mlp_g_from_mlp_v_net2wider( |
| layer, |
| noise_std: float = 1e-4, |
| generator: Optional[torch.Generator] = None, |
| ) -> dict: |
| """Initialize a single decoder layer's generation path (mlp_g + layernorm_g) |
| from its vision path (mlp_v + layernorm_v). |
| |
| Auto-detects same-size vs expansion case by comparing MLP widths. |
| Layernorms are always plain-copied (shape is hidden_size, unchanged). |
| |
| Defensive against meta-device parameters: when transformers' |
| `from_pretrained(dtype=...)` leaves `_g` params on the meta device (because |
| they aren't in the upstream state_dict), a plain `load_state_dict` on |
| those params doesn't materialize them — they stay meta and the `.to(...)` |
| that follows produces garbage GPU memory. To avoid that we rebuild the |
| `_g` parameters as fresh tensors on the same device/dtype as their `_v` |
| counterparts. |
| """ |
| d_v = layer.mlp_v.gate_proj.out_features |
| d_g = layer.mlp_g.gate_proj.out_features |
|
|
| |
| |
| def _materialize_linear(dst_lin: nn.Linear, src_lin: nn.Linear): |
| if dst_lin.weight.is_meta or dst_lin.weight.device.type == "meta": |
| new_w = torch.empty_like(src_lin.weight) |
| dst_lin.weight = nn.Parameter(new_w, requires_grad=dst_lin.weight.requires_grad) |
| if dst_lin.bias is not None: |
| new_b = torch.empty_like(src_lin.bias) if src_lin.bias is not None else torch.zeros( |
| dst_lin.out_features, dtype=src_lin.weight.dtype, device=src_lin.weight.device, |
| ) |
| dst_lin.bias = nn.Parameter(new_b, requires_grad=dst_lin.bias.requires_grad) |
|
|
| def _materialize_norm(dst_norm, src_norm): |
| if dst_norm.weight.is_meta or dst_norm.weight.device.type == "meta": |
| new_w = torch.empty_like(src_norm.weight) |
| dst_norm.weight = nn.Parameter(new_w, requires_grad=dst_norm.weight.requires_grad) |
|
|
| for name in ("gate_proj", "up_proj", "down_proj"): |
| if d_g == d_v: |
| _materialize_linear(getattr(layer.mlp_g, name), getattr(layer.mlp_v, name)) |
| |
| _materialize_norm(layer.input_layernorm_g, layer.input_layernorm_v) |
| _materialize_norm(layer.post_attention_layernorm_g, layer.post_attention_layernorm_v) |
|
|
| _net2wider_expand_mlp( |
| layer.mlp_g, layer.mlp_v, |
| noise_std=noise_std if d_g > d_v else 0.0, |
| generator=generator, |
| ) |
| layer.input_layernorm_g.load_state_dict(layer.input_layernorm_v.state_dict()) |
| layer.post_attention_layernorm_g.load_state_dict( |
| layer.post_attention_layernorm_v.state_dict() |
| ) |
| return {"D_v": d_v, "D_g": d_g, "expanded": d_g > d_v, "noise_std": noise_std} |
|
|
|
|
| @torch.no_grad() |
| def verify_mlp_g_equals_mlp_v( |
| layer, |
| num_samples: int = 4, |
| seq_len: int = 8, |
| ) -> float: |
| """Diagnostic: forward random activations through both paths (under their |
| respective post-attention layernorms) and return max absolute difference. |
| |
| Called from unit tests and logged right after init on layer 0 as a sanity |
| check. Uses fp32 computation when possible (casts output back if needed). |
| """ |
| hidden = layer.hidden_size |
| dev = layer.mlp_v.gate_proj.weight.device |
| dt = layer.mlp_v.gate_proj.weight.dtype |
| x = torch.randn(num_samples, seq_len, hidden, device=dev, dtype=dt) |
| y_v = layer.mlp_v(layer.post_attention_layernorm_v(x)) |
| y_g = layer.mlp_g(layer.post_attention_layernorm_g(x)) |
| return (y_v.float() - y_g.float()).abs().max().item() |
|
|
|
|
| def checkpoint_has_g_keys(model_load_path: Optional[str]) -> bool: |
| """True iff the checkpoint on disk already contains `_g.` weights. |
| |
| This is the robust way to tell "generation path was previously saved; load |
| it as-is" from "generation path is absent; we need to initialize it from |
| mlp_v". |
| |
| Returns False if the path does not exist or no safetensors file is found |
| (caller may fall back to a tensor-level check if needed). |
| """ |
| if model_load_path is None: |
| return False |
| import os |
| import json as _json |
| index_path = os.path.join(model_load_path, "model.safetensors.index.json") |
| single_path = os.path.join(model_load_path, "model.safetensors") |
| if os.path.exists(index_path): |
| with open(index_path) as f: |
| keys = set(_json.load(f)["weight_map"].keys()) |
| return any("_g." in k for k in keys) |
| if os.path.exists(single_path): |
| try: |
| from safetensors import safe_open |
| except ImportError: |
| return False |
| with safe_open(single_path, framework="pt") as f: |
| return any("_g." in k for k in f.keys()) |
| return False |
|
|
|
|
| def _resolve_decoder_layers(model): |
| """Walk common wrapper paths to find the list of transformer decoder layers. |
| |
| Supports: |
| UnifiedMoTForConditionalGeneration -> .model.language_model.model.layers |
| bare HunYuan MoT model -> .language_model.model.layers |
| even-barer HF model -> .model.layers |
| """ |
| for path in ( |
| "model.language_model.model.layers", |
| "language_model.model.layers", |
| "model.layers", |
| ): |
| obj = model |
| ok = True |
| for attr in path.split("."): |
| if not hasattr(obj, attr): |
| ok = False |
| break |
| obj = getattr(obj, attr) |
| if ok: |
| return obj |
| raise AttributeError( |
| "Could not locate decoder layers on model; tried " |
| "model.language_model.model.layers / language_model.model.layers / model.layers" |
| ) |
|
|
|
|
| def maybe_init_generation_path( |
| model, |
| model_load_path: Optional[str] = None, |
| noise_std: float = 1e-4, |
| logger_: Optional[logging.Logger] = None, |
| seed: int = 0xA17C0D1F, |
| target_mlp_g_intermediate_size: Optional[int] = None, |
| ) -> bool: |
| """Top-level entry point used by train / inference / eval scripts. |
| |
| Behavior: |
| * If `model_load_path` is provided and the checkpoint contains any `_g.` |
| key, do nothing and return False (generation path was already loaded |
| from the checkpoint — this is the T2I-resume / TI2I-continuation path). |
| * Otherwise iterate every decoder layer and call |
| `init_mlp_g_from_mlp_v_net2wider`: |
| - Same-size case -> plain state_dict copy (noise ignored). |
| - Expansion case -> Net2Wider neuron duplication + Gaussian noise |
| (std = `noise_std`). |
| |
| `target_mlp_g_intermediate_size`: if provided and larger than the current |
| `mlp_g.intermediate_size`, REBUILD each layer's `mlp_g` to the wider size |
| before running Net2Wider init. Use this when the base checkpoint was |
| loaded with mlp_g at mlp_v's width (so from_pretrained accepted the |
| shapes) and you want to widen post-load. |
| |
| Returns True if initialization was performed. |
| """ |
| log = logger_ or logger |
|
|
| |
| |
| |
| |
| pos_embed_module = None |
| for path in ("latent_pos_embed", "model.latent_pos_embed"): |
| obj = model |
| ok = True |
| for attr in path.split("."): |
| if not hasattr(obj, attr): |
| ok = False |
| break |
| obj = getattr(obj, attr) |
| if ok: |
| pos_embed_module = obj |
| break |
| if pos_embed_module is not None and hasattr(pos_embed_module, "_reset_parameters"): |
| pos_embed_module._reset_parameters() |
| log.info( |
| f"latent_pos_embed re-initialized from sin-cos table " |
| f"(shape={tuple(pos_embed_module.pos_embed.shape)})" |
| ) |
|
|
| if checkpoint_has_g_keys(model_load_path): |
| log.info( |
| "Generation path present in checkpoint (found `_g.` keys); " |
| "skipping mlp_g initialization." |
| ) |
| return False |
|
|
| layers = _resolve_decoder_layers(model) |
| d_v = layers[0].mlp_v.gate_proj.out_features |
| d_g_current = layers[0].mlp_g.gate_proj.out_features |
|
|
| |
| if target_mlp_g_intermediate_size is not None and target_mlp_g_intermediate_size > d_g_current: |
| from transformers.models.hunyuan_vl_mot.modeling_hunyuan_vl_mot import HunYuanVLMoTMLP |
| for layer in layers: |
| base_cfg = layer.mlp_g.gate_proj.weight |
| device = base_cfg.device |
| dtype = base_cfg.dtype |
| |
| class _MiniCfg: |
| pass |
| mini = _MiniCfg() |
| mini.hidden_size = layer.mlp_v.gate_proj.in_features |
| mini.intermediate_size = target_mlp_g_intermediate_size |
| mini.hidden_act = "silu" |
| mini.mlp_bias = (layer.mlp_v.gate_proj.bias is not None) |
| new_mlp_g = HunYuanVLMoTMLP(mini).to(device=device, dtype=dtype) |
| layer.mlp_g = new_mlp_g |
| d_g = target_mlp_g_intermediate_size |
| log.info(f"Rebuilt mlp_g for all layers at intermediate_size={d_g}.") |
| else: |
| d_g = d_g_current |
|
|
| expansion = d_g > d_v |
| mode = "copy" if not expansion else f"net2wider (x{d_g / d_v:.3f})" |
| log.info( |
| f"Initializing mlp_g/layernorm_g from mlp_v/layernorm_v: " |
| f"mode={mode} (d_v={d_v}, d_g={d_g}, " |
| f"noise_std={noise_std if expansion else 0.0:g})" |
| ) |
|
|
| |
| gen = torch.Generator(device=layers[0].mlp_v.gate_proj.weight.device) |
| gen.manual_seed(seed) |
|
|
| for layer in layers: |
| init_mlp_g_from_mlp_v_net2wider( |
| layer, |
| noise_std=noise_std if expansion else 0.0, |
| generator=gen, |
| ) |
|
|
| |
| |
| try: |
| diff = verify_mlp_g_equals_mlp_v(layers[0]) |
| log.info( |
| f" post-init forward diff (layer 0, random input): " |
| f"max |mlp_v - mlp_g| = {diff:.3e}" |
| ) |
| except Exception as e: |
| log.warning(f"verify_mlp_g_equals_mlp_v diagnostic failed: {e}") |
|
|
| return True |
|
|