Skip to content

Commit 8d492d9

Browse files
justinchubyCopilotCopilot
authored
Add MMS (Massively Multilingual Speech) CTC ASR model (#96)
## Summary Adds support for facebook/mms-300m and facebook/mms-1b-all — Meta's Massively Multilingual Speech models covering 1,100+ languages. ## Architecture - **Feature extractor**: 7-layer causal CNN (reuses existing `Wav2Vec2Model`) - **Encoder**: transformer encoder with positional convolutions - **Language adapter** (`add_adapter=True`): N strided Conv1d + GLU layers for language-specific adaptation - **CTC head**: linear projection → per-frame logits for connectionist temporal classification ## Implementation - `src/mobius/models/wav2vec2_ctc.py` — `Wav2Vec2ForCTCModel` (extends `Wav2Vec2Model`), `_AdapterLayer`, `_Adapter` - `src/mobius/tasks/_ctc_asr.py` — `CTCAsrTask` (single-model, `input_values` + `attention_mask` → `logits`) - `src/mobius/_configs.py` — `MMSConfig` (subclass of `ArchitectureConfig` with adapter fields) - Registry: `mms` → `Wav2Vec2ForCTCModel` / `ctc-asr` ## Weight name alignment Uses bare `nn.Parameter` for adapter conv weights (matching the wav2vec2 feature extractor pattern) with `preprocess_weights` renames for HF's `nn.Conv1d` layout. ## Testing 6 new tests in `TestBuildMMSGraph` — all pass. Full suite: 2676 passed. --------- Signed-off-by: Justin Chu <justinchuby@users.noreply.github.com> Signed-off-by: Justin Chu <11205048+justinchuby@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Justin Chu <11205048+justinchuby@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 133f0e9 commit 8d492d9

18 files changed

Lines changed: 1824 additions & 1 deletion

examples/mms.py

Lines changed: 661 additions & 0 deletions
Large diffs are not rendered by default.

scripts/generate_golden.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from __future__ import annotations
3939

4040
import argparse
41+
import contextlib
4142
import fnmatch
4243
import sys
4344
import time
@@ -804,6 +805,88 @@ def _generate_audio_feature_extraction(case: TestCase, json_path: Path, device:
804805
)
805806

806807

808+
def _generate_ctc_asr(case: TestCase, json_path: Path, device: str) -> None:
809+
"""Generate golden data for CTC-based ASR (Wav2Vec2ForCTC / MMS).
810+
811+
The model output is per-frame logits over a vocabulary; we save the
812+
top-K over the final frame's logit vector (matching the existing
813+
audio-feature-extraction pattern), and when L5 is requested we also
814+
save the CTC-greedy-decoded transcript as a token-id sequence so the
815+
end-to-end test can compare against the runtime's greedy decode.
816+
817+
MMS specifically requires picking a target language adapter via
818+
``processor.tokenizer.set_target_lang(lang)`` and
819+
``model.load_adapter(lang)`` before the forward pass. The language is
820+
read from ``case.generation_params['lang']`` (default ``"eng"``).
821+
"""
822+
import librosa
823+
import torch
824+
import transformers
825+
826+
from mobius._testing.golden import save_generation_json, save_golden_ref
827+
828+
lang = case.generation_params.get("lang", "eng")
829+
830+
processor = transformers.AutoProcessor.from_pretrained(
831+
case.model_id, trust_remote_code=case.trust_remote_code, target_lang=lang
832+
)
833+
model = transformers.Wav2Vec2ForCTC.from_pretrained(
834+
case.model_id,
835+
torch_dtype=torch.float32,
836+
device_map=device,
837+
trust_remote_code=case.trust_remote_code,
838+
target_lang=lang,
839+
ignore_mismatched_sizes=True, # MMS lm_head shape changes per language
840+
)
841+
# For MMS, switching languages also requires loading the per-language adapter.
842+
# Non-MMS Wav2Vec2ForCTC checkpoints don't have language adapters;
843+
# the missing-adapter case is expected and harmless there.
844+
if hasattr(model, "load_adapter"):
845+
with contextlib.suppress(ValueError, KeyError, OSError):
846+
model.load_adapter(lang)
847+
model.eval()
848+
849+
audio_path = Path("testdata") / case.audio[0]
850+
audio_array, sample_rate = librosa.load(str(audio_path), sr=16000)
851+
processed = processor(audio_array, sampling_rate=sample_rate, return_tensors="pt").to(
852+
next(model.parameters()).device
853+
)
854+
855+
with torch.no_grad():
856+
outputs = model(**processed)
857+
858+
# CTC logits: (batch, num_frames, vocab_size). Use last frame for top-K.
859+
logits = outputs.logits[0] # (num_frames, vocab_size)
860+
last_logits = logits[-1].cpu().numpy()
861+
golden = _extract_logits_golden(last_logits)
862+
863+
save_golden_ref(
864+
json_path,
865+
top1_id=golden["top1_id"],
866+
top2_id=golden["top2_id"],
867+
top10_ids=golden["top10_ids"],
868+
top10_logits=golden["top10_logits"],
869+
logits_summary=golden["logits_summary"],
870+
input_ids=np.array([[0]], dtype=np.int64), # placeholder
871+
)
872+
873+
if "L5" in case.level:
874+
# CTC greedy decode: argmax over vocab per frame, then collapse
875+
# repeats and remove blanks. Save the post-collapse token IDs (and
876+
# the decoded text for human inspection) into the standard
877+
# ``*_generation.json`` sidecar.
878+
predicted_ids = torch.argmax(logits, dim=-1).cpu().numpy()
879+
transcript = processor.batch_decode(predicted_ids[np.newaxis, :])[0]
880+
gen_path = json_path.with_name(json_path.stem + "_generation.json")
881+
save_generation_json(
882+
gen_path,
883+
model_id=case.model_id,
884+
prompt=str(audio_path),
885+
generated_tokens=predicted_ids.tolist(),
886+
generated_text=transcript,
887+
)
888+
889+
807890
def _generate_image_classification(case: TestCase, json_path: Path, device: str) -> None:
808891
"""Generate golden data for image classification (ViT, CLIP, etc.).
809892
@@ -1186,6 +1269,7 @@ def _generate_phi4mm_multimodal(case: TestCase, json_path: Path, device: str) ->
11861269
"speech-to-text": _generate_speech_to_text,
11871270
"speech-language": _generate_speech_language,
11881271
"audio-feature-extraction": _generate_audio_feature_extraction,
1272+
"ctc-asr": _generate_ctc_asr,
11891273
# Vision tasks that produce last_hidden_state — reuse image classification.
11901274
"depth-estimation": _generate_image_classification,
11911275
"image-segmentation": _generate_image_classification,

src/mobius/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"ModelRegistration",
2323
"ModelRegistry",
2424
"ModelTask",
25+
"MMSConfig",
2526
"OPSET_VERSION",
2627
"Sam2Config",
2728
"SegformerConfig",
@@ -67,6 +68,7 @@
6768
Gemma4Config,
6869
MambaConfig,
6970
MllamaConfig,
71+
MMSConfig,
7072
Sam2Config,
7173
SegformerConfig,
7274
VisionConfig,

src/mobius/_builder.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,15 @@ def build(
370370
model_type = "qwen3_5_moe_vl"
371371
hf_config = hf_config.text_config
372372

373+
# Wav2Vec2 / HuBERT / WavLM ship ``model_type="wav2vec2"`` (etc.) for both
374+
# feature-extraction and CTC checkpoints. Switch to the ``mms`` registration
375+
# (Wav2Vec2ForCTCModel + ctc-asr task) when the architecture indicates a
376+
# CTC head — this covers both MMS and vanilla Wav2Vec2ForCTC fine-tunes.
377+
if model_type in ("wav2vec2", "hubert", "wavlm"):
378+
architectures = getattr(parent_config, "architectures", None) or []
379+
if any("ForCTC" in arch for arch in architectures):
380+
model_type = "mms"
381+
373382
if module_class is None:
374383
if model_type in registry:
375384
module_class = registry.get(model_type)

src/mobius/_configs/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
Mamba2Config,
4040
MambaConfig,
4141
MllamaConfig,
42+
MMSConfig,
4243
NanoChatConfig,
4344
NemotronHConfig,
4445
Sam2Config,
@@ -98,6 +99,7 @@
9899
"Mamba2Config",
99100
"MambaConfig",
100101
"MllamaConfig",
102+
"MMSConfig",
101103
"NanoChatConfig",
102104
"NemotronHConfig",
103105
"QuantizationConfig",

src/mobius/_configs/_base.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2024,3 +2024,43 @@ def from_transformers(cls, config) -> WhisperConfig:
20242024
options["dtype"] = resolved
20252025

20262026
return cls(**options)
2027+
2028+
2029+
@dataclasses.dataclass
2030+
class MMSConfig(ArchitectureConfig):
2031+
"""Configuration for MMS (Massively Multilingual Speech) CTC models.
2032+
2033+
Extends ``ArchitectureConfig`` with the adapter parameters used in
2034+
``facebook/mms-1b-all`` and related checkpoints. When ``add_adapter=True``
2035+
the adapter layers are included in the exported ONNX graph; set this after
2036+
calling ``model.load_adapter(lang_code)`` to bake a specific language's
2037+
weights into the model.
2038+
2039+
HuggingFace class: ``Wav2Vec2ForCTC`` with ``config.model_type == "wav2vec2"``
2040+
"""
2041+
2042+
add_adapter: bool = False
2043+
output_hidden_size: int = 0 # 0 → use hidden_size
2044+
adapter_kernel_size: int = 3
2045+
adapter_stride: int = 2
2046+
num_adapter_layers: int = 3
2047+
2048+
def __post_init__(self):
2049+
if self.output_hidden_size == 0:
2050+
self.output_hidden_size = self.hidden_size
2051+
2052+
@classmethod
2053+
def from_transformers(cls, config, parent_config=None) -> MMSConfig:
2054+
"""Extract MMSConfig from a HuggingFace Wav2Vec2Config."""
2055+
base = ArchitectureConfig.from_transformers(config, parent_config=parent_config)
2056+
base_fields = _shallow_fields(base)
2057+
return cls(
2058+
**base_fields,
2059+
add_adapter=getattr(config, "add_adapter", False),
2060+
output_hidden_size=getattr(
2061+
config, "output_hidden_size", base_fields["hidden_size"]
2062+
),
2063+
adapter_kernel_size=getattr(config, "adapter_kernel_size", 3),
2064+
adapter_stride=getattr(config, "adapter_stride", 2),
2065+
num_adapter_layers=getattr(config, "num_adapter_layers", 3),
2066+
)

src/mobius/_registry.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from mobius._configs import (
2626
BaseModelConfig,
2727
Gemma4Config,
28+
MMSConfig,
2829
WhisperConfig,
2930
)
3031
from mobius.models import (
@@ -126,6 +127,7 @@
126127
from mobius.models.trocr import TrOCRForConditionalGeneration
127128
from mobius.models.vit import ViTModel
128129
from mobius.models.wav2vec2 import Wav2Vec2Model
130+
from mobius.models.wav2vec2_ctc import Wav2Vec2ForCTCModel
129131
from mobius.models.xlm import XLMCausalLMModel
130132
from mobius.models.yolos import YolosForObjectDetection
131133
from mobius.models.zamba2 import Zamba2CausalLMModel
@@ -661,6 +663,7 @@ def _detect_fallback_registration(hf_config) -> ModelRegistration | None:
661663
"wav2vec2-bert": ModelRegistration(Wav2Vec2Model, task="audio-feature-extraction"),
662664
"wav2vec2-conformer": ModelRegistration(Wav2Vec2Model, task="audio-feature-extraction"),
663665
"wavlm": ModelRegistration(Wav2Vec2Model, task="audio-feature-extraction"),
666+
"mms": ModelRegistration(Wav2Vec2ForCTCModel, task="ctc-asr", config_class=MMSConfig),
664667
}
665668

666669

@@ -862,6 +865,7 @@ def _create_default_registry() -> ModelRegistry:
862865
"qwen3_asr": "Qwen/Qwen3-ASR-0.6B",
863866
"fun_asr": "justinchuby/Fun-ASR-Nano-2512",
864867
"sensevoice_small": "mlx-community/SenseVoiceSmall",
868+
"mms": "facebook/mms-300m",
865869
"speecht5": "microsoft/speecht5_asr",
866870
"sew": "asapp/sew-tiny-100k",
867871
"sew-d": "asapp/sew-d-tiny-100k",
@@ -1080,6 +1084,7 @@ def _create_default_registry() -> ModelRegistry:
10801084
"wav2vec2-conformer": "wav2vec2",
10811085
"hubert": "wav2vec2",
10821086
"wavlm": "wav2vec2",
1087+
"mms": "wav2vec2",
10831088
"vit": "vit",
10841089
"vit_hybrid": "vit",
10851090
"vit_mae": "vit",

src/mobius/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@
126126
"UNet2DConditionModel",
127127
"ViTModel",
128128
"VideoAutoencoderModel",
129+
"Wav2Vec2ForCTCModel",
129130
"Wav2Vec2Model",
130131
"WhisperForConditionalGeneration",
131132
"XLMCausalLMModel",
@@ -259,6 +260,7 @@
259260
from mobius.models.video_vae import VideoAutoencoderModel
260261
from mobius.models.vit import ViTModel
261262
from mobius.models.wav2vec2 import Wav2Vec2Model
263+
from mobius.models.wav2vec2_ctc import Wav2Vec2ForCTCModel
262264
from mobius.models.whisper import WhisperForConditionalGeneration
263265
from mobius.models.xlm import XLMCausalLMModel
264266
from mobius.models.zamba2 import Zamba2CausalLMModel

0 commit comments

Comments
 (0)