rxbrain-embodied-cognition / model /mot_init_utils.py
multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
4fe31ed verified
Raw
History Blame Contribute Delete
15.4 kB
"""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)"
)
# Same-size fast path: plain copy, ignore noise.
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[j] = source neuron index for destination neuron j
# Round-robin: for D_g = k*D_v this produces [0,1,...,D_v-1, 0,1,...,D_v-1, ...]
# which makes twins "adjacent in source" rather than "adjacent in destination".
# Either layout works mathematically; this choice keeps each source's twins
# spread out along the destination axis, which is marginally friendlier to
# downstream gather/scatter operations if any later code happens to assume
# contiguity within a source group.
indices = torch.arange(d_g, device=device) % d_v
counts = torch.bincount(indices, minlength=d_v) # (D_v,)
# --- gate_proj and up_proj: row-wise replication ---------------------------
for name in ("gate_proj", "up_proj"):
src = getattr(mlp_v, name).weight.data # (D_v, H)
dst_param = getattr(mlp_g, name).weight
dst = src[indices].clone().to(dtype=dst_param.dtype) # (D_g, H)
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_proj: column-wise replication with reciprocal-count scaling -----
# down_v: (H, D_v), down_g: (H, D_g).
# For each destination column j with source s = indices[j]:
# down_g[:, j] = down_v[:, s] / counts[s]
# so that Sum_{j: indices[j]=s} down_g[:, j] * h_s
# = down_v[:, s] * h_s (function preserved exactly).
down_v = mlp_v.down_proj.weight.data # (H, D_v)
dst_param = mlp_g.down_proj.weight
scale = (1.0 / counts.to(down_v.dtype)) # (D_v,)
dst = (down_v[:, indices] * scale[indices]).clone().to(dtype=dst_param.dtype) # (H, D_g)
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
# Materialize meta-device params before in-place copy: rebuild _g linear
# weights on the same device/dtype as _v.
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))
# else: widening path will rebuild mlp_g entirely in maybe_init_generation_path
_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
# ---- Re-materialize the latent_pos_embed (sin-cos table) ---------------
# PositionEmbedding's parameter is computed at __init__ time but
# `from_pretrained(dtype=...)` uses meta-init that bypasses the
# constructor's data assignment, leaving the param uninitialized.
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
# ---- Optional: rebuild mlp_g at a wider size before init ----
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 # to inherit device/dtype
device = base_cfg.device
dtype = base_cfg.dtype
# Use a duck-typed mini config matching upstream MLP's expected fields
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})"
)
# Single deterministic generator for reproducibility across layers.
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,
)
# Spot-check layer 0: forward a random input through both paths. The diff
# should be ~O(noise_std * sqrt(D_v) * ||x||); much larger indicates a bug.
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: # pylint: disable=broad-except # pragma: no cover - diagnostic only
log.warning(f"verify_mlp_g_equals_mlp_v diagnostic failed: {e}")
return True