| """Deeper audio projector for the HF Gemma4 unified model. |
| |
| The stock model maps audio encoder features into LM space with a single |
| `nn.Linear(1536 -> 5376, bias=False)` at `model.model.embed_audio.embedding_projection`. |
| |
| `DeepAudioProjector` is a drop-in replacement that keeps that linear as a |
| warm-started backbone and adds a *zero-initialised* residual MLP, so at init |
| the output is bit-identical to the original projector (we reuse the learned |
| phase-1 weights exactly), and SFT learns the deeper correction on top. |
| |
| It exposes a `.weight` property because the parent `embed_audio.forward` reads |
| `self.embedding_projection.weight.dtype` to decide the input cast dtype. |
| """ |
| from __future__ import annotations |
|
|
| import torch |
| import torch.nn as nn |
|
|
|
|
| class DeepAudioProjector(nn.Module): |
| def __init__( |
| self, |
| in_dim: int = 1536, |
| out_dim: int = 5376, |
| hidden: int = 4096, |
| n_hidden_layers: int = 2, |
| dropout: float = 0.0, |
| out_dtype: torch.dtype = torch.bfloat16, |
| ): |
| super().__init__() |
| self.in_dim = in_dim |
| self.out_dim = out_dim |
| self.out_dtype = out_dtype |
|
|
| |
| self.proj = nn.Linear(in_dim, out_dim, bias=False) |
|
|
| |
| self.ln = nn.LayerNorm(in_dim) |
| layers: list[nn.Module] = [] |
| d = in_dim |
| for _ in range(n_hidden_layers): |
| layers += [nn.Linear(d, hidden), nn.GELU()] |
| if dropout > 0: |
| layers.append(nn.Dropout(dropout)) |
| d = hidden |
| self.mlp = nn.Sequential(*layers) |
| self.out = nn.Linear(d, out_dim) |
| nn.init.zeros_(self.out.weight) |
| nn.init.zeros_(self.out.bias) |
|
|
| @property |
| def weight(self) -> torch.Tensor: |
| |
| return self.proj.weight |
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor: |
| wdt = self.proj.weight.dtype |
| x = x.to(wdt) |
| base = self.proj(x) |
| res = self.out(self.mlp(self.ln(x))) |
| return (base + res).to(self.out_dtype) |
|
|
| @classmethod |
| def from_linear(cls, linear: nn.Linear, **kwargs) -> "DeepAudioProjector": |
| out_dim, in_dim = linear.weight.shape |
| m = cls(in_dim=in_dim, out_dim=out_dim, **kwargs) |
| with torch.no_grad(): |
| m.proj.weight.copy_(linear.weight) |
| return m |
|
|
|
|
| def find_audio_projection_parent(model): |
| """Return (parent_module, attr_name) for the audio embedding_projection.""" |
| inner = getattr(model, "model", model) |
| embed_audio = getattr(inner, "embed_audio", None) |
| if embed_audio is None: |
| raise AttributeError("Could not find model.model.embed_audio") |
| if not hasattr(embed_audio, "embedding_projection"): |
| raise AttributeError("embed_audio has no embedding_projection") |
| return embed_audio, "embedding_projection" |
|
|
|
|
| def install_deep_projector( |
| model, |
| hidden: int = 4096, |
| n_hidden_layers: int = 2, |
| dropout: float = 0.0, |
| param_dtype: torch.dtype = torch.float32, |
| ): |
| """Replace the audio embedding_projection with a warm-started DeepAudioProjector. |
| |
| Returns the new projector module (params left in `param_dtype`, e.g. fp32 for |
| stable optimisation; forward output is cast back to the LM dtype). |
| """ |
| parent, attr = find_audio_projection_parent(model) |
| old = getattr(parent, attr) |
| assert isinstance(old, nn.Linear), f"expected nn.Linear, got {type(old)}" |
| out_dtype = old.weight.dtype |
| deep = DeepAudioProjector.from_linear( |
| old, hidden=hidden, n_hidden_layers=n_hidden_layers, |
| dropout=dropout, out_dtype=out_dtype, |
| ) |
| deep = deep.to(device=old.weight.device, dtype=param_dtype) |
| setattr(parent, attr, deep) |
| return deep |
|
|