CoolFace
Modelpublic

fredchu/MOSS-Audio-8B-Instruct-MLX

sourceHugging Faceapache-2.0updated 4mo agoView on Hugging Face
3likes
moss_audio_encoder_mlx.py216 linesDownload Raw Back to scripts
1"""MLX-native MossAudioEncoder.2 3Direct port of src/modeling_moss_audio.py:36-155 (MossAudioEncoder).4Adapted from ml-explore/mlx-examples/whisper/mlx_whisper/whisper.py with:5  - 3× Conv2d stride-2 stem (instead of Whisper's 2× Conv1d)6  - Pre-existing HF Whisper attribute names (q_proj/k_proj/v_proj/out_proj, fc1/fc2,7    self_attn_layer_norm/final_layer_norm) so weight remap is near-identity8  - DeepStack taps: capture hidden state AFTER layers in deepstack_layer_indexes9  - feature_lens-based padding mask10"""11from __future__ import annotations12 13import math14from dataclasses import dataclass, field15from typing import List, Optional, Tuple16 17import mlx.core as mx18import mlx.nn as nn19 20 21# ---- helpers ----------------------------------------------------------22 23 24def sinusoids(length: int, channels: int, max_timescale: float = 10000.0) -> mx.array:25    """Whisper-style sinusoidal position embeddings. Matches mlx-examples whisper."""26    assert channels % 2 == 027    log_timescale_increment = math.log(max_timescale) / (channels // 2 - 1)28    inv_timescales = mx.exp(-log_timescale_increment * mx.arange(channels // 2))29    scaled_time = mx.arange(length)[:, None] * inv_timescales[None, :]30    return mx.concatenate([mx.sin(scaled_time), mx.cos(scaled_time)], axis=1)31 32 33# ---- attention ------------------------------------------------------34 35 36class WhisperAttention(nn.Module):37    """HF-Whisper-style self-attention. Layer-scaling convention (`1/sqrt(head_dim)`38    applied to Q, not split between Q and K like mlx-examples does).39 40    Attribute names match HF so weight remap is identity: q_proj/k_proj/v_proj/out_proj.41    """42 43    def __init__(self, d_model: int, n_heads: int):44        super().__init__()45        self.n_heads = n_heads46        self.head_dim = d_model // n_heads47        assert d_model == self.head_dim * n_heads48        # HF Whisper: q/v/out have bias; k does not49        self.q_proj = nn.Linear(d_model, d_model, bias=True)50        self.k_proj = nn.Linear(d_model, d_model, bias=False)51        self.v_proj = nn.Linear(d_model, d_model, bias=True)52        self.out_proj = nn.Linear(d_model, d_model, bias=True)53 54    def __call__(self, x: mx.array, mask: Optional[mx.array] = None) -> mx.array:55        B, T, D = x.shape56        q = self.q_proj(x).reshape(B, T, self.n_heads, self.head_dim).transpose(0, 2, 1, 3)57        k = self.k_proj(x).reshape(B, T, self.n_heads, self.head_dim).transpose(0, 2, 1, 3)58        v = self.v_proj(x).reshape(B, T, self.n_heads, self.head_dim).transpose(0, 2, 1, 3)59        scale = self.head_dim ** -0.560        attn = (q * scale) @ k.transpose(0, 1, 3, 2)  # (B, H, T, T)61        if mask is not None:62            attn = attn + mask63        w = mx.softmax(attn, axis=-1, precise=True)64        out = (w @ v).transpose(0, 2, 1, 3).reshape(B, T, D)65        return self.out_proj(out)66 67 68# ---- encoder layer --------------------------------------------------69 70 71class WhisperEncoderBlock(nn.Module):72    """Pre-LN Whisper encoder block. Matches transformers.WhisperEncoderLayer."""73 74    def __init__(self, d_model: int, n_heads: int, ffn_dim: int):75        super().__init__()76        self.self_attn = WhisperAttention(d_model, n_heads)77        self.self_attn_layer_norm = nn.LayerNorm(d_model)78        self.fc1 = nn.Linear(d_model, ffn_dim)79        self.fc2 = nn.Linear(ffn_dim, d_model)80        self.final_layer_norm = nn.LayerNorm(d_model)81 82    def __call__(self, x: mx.array, mask: Optional[mx.array] = None) -> mx.array:83        h = self.self_attn_layer_norm(x)84        x = x + self.self_attn(h, mask=mask)85        h = self.final_layer_norm(x)86        x = x + self.fc2(nn.gelu(self.fc1(h)))87        return x88 89 90# ---- encoder --------------------------------------------------------91 92 93@dataclass94class EncoderConfig:95    num_mel_bins: int = 12896    downsample_hidden_size: int = 48097    d_model: int = 128098    n_heads: int = 2099    ffn_dim: int = 5120100    n_layers: int = 32101    max_source_positions: int = 1500102    layer_norm_eps: float = 1e-5103    output_dim: int = 1280104    deepstack_layer_indexes: List[int] = field(default_factory=lambda: [8, 16, 24])105 106 107class MossAudioEncoderMLX(nn.Module):108    def __init__(self, cfg: EncoderConfig):109        super().__init__()110        self.cfg = cfg111        # Conv2d stem: 1 → 480 → 480 → 480, each stride-2112        # MLX Conv2d expects NHWC, weight shape (OC, kH, kW, IC)113        self.conv1 = nn.Conv2d(1, cfg.downsample_hidden_size, kernel_size=3, stride=2, padding=1)114        self.conv2 = nn.Conv2d(cfg.downsample_hidden_size, cfg.downsample_hidden_size, kernel_size=3, stride=2, padding=1)115        self.conv3 = nn.Conv2d(cfg.downsample_hidden_size, cfg.downsample_hidden_size, kernel_size=3, stride=2, padding=1)116        # After 3× stride-2 on mel-axis (128→64→32→16): flat dim = 480*16 = 7680117        self.stem_proj = nn.Linear(cfg.downsample_hidden_size * 16, cfg.d_model)118        # Precomputed sinusoids, will be sliced119        self._positions = sinusoids(cfg.max_source_positions, cfg.d_model)120        self.layers = [121            WhisperEncoderBlock(cfg.d_model, cfg.n_heads, cfg.ffn_dim)122            for _ in range(cfg.n_layers)123        ]124        self.layer_norm = nn.LayerNorm(cfg.d_model, eps=cfg.layer_norm_eps)125        # MOSS has optional out_proj; for 4B output_dim==d_model, so it's Identity in PyTorch126        # We skip it entirely (equivalent).127        assert cfg.output_dim == cfg.d_model, "non-identity out_proj not yet implemented"128        self._deepstack_set = set(cfg.deepstack_layer_indexes)129 130    def _compute_downsampled_length(self, L: int) -> int:131        """3× stride-2 conv output length: ceil((((L-1)//2+1)-1)//2+1 ... )"""132        def step(n): return (n - 1) // 2 + 1133        return step(step(step(L)))134 135    def __call__(136        self,137        input_features: mx.array,      # (B, n_mels, T) bf16 mel spectrogram138        feature_lens: Optional[mx.array] = None,139        return_deepstack: bool = True,140    ) -> Tuple[mx.array, Optional[List[mx.array]]]:141        if input_features.ndim == 2:142            input_features = input_features[None]143        B, n_mels, T = input_features.shape144        if feature_lens is None:145            feature_lens = mx.full((B,), T, dtype=mx.int32)146 147        # (B, n_mels, T) → (B, n_mels, T, 1) [NHWC with channels-last = 1 input channel]148        # But MLX Conv2d expects input shape (B, H, W, C_in). We map:149        #   H = n_mels (128), W = T (frames), C_in = 1150        x = input_features[..., None]  # (B, n_mels, T, 1)151        x = nn.gelu(self.conv1(x))     # (B, 64, T/2, 480)152        x = nn.gelu(self.conv2(x))     # (B, 32, T/4, 480)153        x = nn.gelu(self.conv3(x))     # (B, 16, T/8, 480)154        # PyTorch reference: (B, C, F, T) → permute(0,3,1,2) → (B, T, C, F) → flatten → (B, T, C*F)155        # MLX is (B, F, T, C) post-conv. Need transpose to (B, T, C, F) to match PT's flatten order.156        B_, H_, W_, C_ = x.shape  # H_=F, W_=T, C_=C157        x = x.transpose(0, 2, 3, 1).reshape(B_, W_, C_ * H_)  # (B, T, C*F)158        x = self.stem_proj(x)  # (B, T', d_model)159 160        # Trim to actual downsampled length (in case input was padded)161        max_len = self._compute_downsampled_length(int(feature_lens.max().item()))162        if x.shape[1] > max_len:163            x = x[:, :max_len, :]164 165        # Add sinusoidal positions166        seq_len = x.shape[1]167        pos = self._positions[:seq_len].astype(x.dtype)168        x = x + pos169 170        # Build attention mask: (B, 1, 1, seq_len) additive171        # padding_mask[b, t] = True if t >= downsampled_len[b]  (this is where we mask out)172        dsl = mx.stack([173            mx.array(self._compute_downsampled_length(int(feature_lens[b].item())), dtype=mx.int32)174            for b in range(B)175        ])  # (B,)176        ar = mx.arange(seq_len, dtype=mx.int32)177        padding = ar[None, :] >= dsl[:, None]  # (B, seq_len) bool178        neg_inf = mx.array(-1e9, dtype=x.dtype)179        mask = mx.where(padding, neg_inf, mx.array(0.0, dtype=x.dtype))180        mask = mask[:, None, None, :]  # (B, 1, 1, seq_len)181 182        deepstack: List[mx.array] = []183        for layer_idx, layer in enumerate(self.layers):184            x = layer(x, mask=mask)185            if return_deepstack and layer_idx in self._deepstack_set:186                # Apply the final layer_norm snapshot at this point, per MOSS's output_deepstack_hidden_states187                # Actually, MOSS captures x BEFORE the final layer_norm — matches what PyTorch does.188                deepstack.append(x)189 190        x = self.layer_norm(x)191        return x, (deepstack if return_deepstack else None)192 193 194# ---- GatedMLP (for audio_adapter + deepstack_audio_merger_list) ----195 196 197class GatedMLP(nn.Module):198    """MOSS's GatedMLP: down(silu(gate(x)) * up(x)). SwiGLU convention.199 200    Matches MOSS/src/modeling_moss_audio.py:155-164.201    All linears are bias=False.202    """203 204    def __init__(self, input_size: int, hidden_size: int, output_size: int):205        super().__init__()206        self.gate_proj = nn.Linear(input_size, hidden_size, bias=False)207        self.up_proj = nn.Linear(input_size, hidden_size, bias=False)208        self.down_proj = nn.Linear(hidden_size, output_size, bias=False)209 210    def __call__(self, x: mx.array) -> mx.array:211        return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))212 213 214__all__ = ["sinusoids", "WhisperAttention", "WhisperEncoderBlock",215           "EncoderConfig", "MossAudioEncoderMLX", "GatedMLP"]216