Skip to content

configs refactor 2/3: registry-based audio extractor hooks - #310

Merged
justinchuby merged 5 commits into
mainfrom
refactor-configs-2-extractor-registry
May 21, 2026
Merged

configs refactor 2/3: registry-based audio extractor hooks#310
justinchuby merged 5 commits into
mainfrom
refactor-configs-2-extractor-registry

Conversation

@justinchuby

Copy link
Copy Markdown
Member

Part 2 of 3 — stacked on #309

Replaces the 135-line model_type switch in _extract_audio_config with a tiny plugin registry. Each model now contributes a hook in its own file under src/mobius/_configs/per_model/, mutating an audio_fields dict or short-circuiting with a fully-formed sub-config.

Adding a new audio-capable architecture no longer requires editing _base.py.

Layout

src/mobius/_configs/
├── _extractors.py                 # @register_audio_hook + dispatch
└── per_model/
    ├── _audio_default.py          # audio_processor / embd_layer / speech_lora
    ├── _phi4mm_audio.py           # phi4mm audio_token_id
    ├── _qwen3_asr_audio.py        # thinker_config.audio_config + token ids
    ├── _gemma4_audio.py           # short-circuit to Gemma4AudioConfig
    └── _sensevoice_audio.py       # encoder_conf + frontend_conf mapping

_extract_audio_config in _base.py shrinks to a 5-line shim that triggers the per_model side-effect import and calls the dispatcher.

No behavior change: every existing audio-capable model still produces the same AudioConfig from the same HF config.

Hook protocol

@register_audio_hook
def _my_model(config, parent_config, model_type: str, fields: dict) -> dict | None:
    if model_type != "my_model":
        return None
    fields.update(attention_dim=..., ...)
    return None  # contribute fields, defer to default AudioConfig(**fields)
    # OR
    return {"audio": MySubclassAudioConfig(...)}  # short-circuit

Deferred

Vision-side conversion (also a mega-switch) is intentionally deferred to a follow-up so reviewers can verify the registry pattern on the smaller surface first.

Tests

Suite Result
pytest src/ tests/build_graph_test.py tests/cli_test.py -n auto 2769 passed, 41 skipped
pytest tests/arch_validation_test.py -k 'sensevoice_small or phi4mm or qwen3_asr or gemma4' 15 passed
Ruff clean

justinchuby and others added 2 commits May 20, 2026 06:41
_configs.py grew to 2591 lines (9 sub-configs, 19 per-model configs,
3 mega-extractor switch functions, a 530-line ArchitectureConfig.
from_transformers). This is the first of three mechanical refactors
that carve it into a scalable package layout. No behavior change in
this PR — every public name is still importable from mobius._configs.

src/mobius/_configs/
├── __init__.py        # re-exports everything that was in _configs.py
├── _sub_configs.py    # pure-data dataclasses (RoPE/Vision/Audio/Codec/TTS)
├── _quantization.py   # QuantizationConfig + from_transformers
└── _base.py           # BaseModelConfig, ArchitectureConfig, per-model
                      # subclasses, and the _extract_* helpers

Follow-up PRs in this series:
  Part 2/3 — convert the _extract_audio_config / _extract_vision_config
             model_type switches into a decorator-registered dispatch
             so new models add a file under per_model/ instead of a
             branch in the central function.
  Part 3/3 — move per-model config subclasses (Gemma2Config,
             MllamaConfig, NemotronHConfig, ...) into per_model/ and
             carve up ArchitectureConfig.from_transformers.

Tests: 2769 passed (full src/ + tests/build_graph_test.py + cli_test.py).
Ruff: clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Justin Chu <justinchu@microsoft.com>
Replaces the 135-line model_type switch in _extract_audio_config with
a tiny plugin registry. Each model now contributes a hook in its own
file under src/mobius/_configs/per_model/, mutating an audio_fields
dict or short-circuiting with a fully-formed sub-config. Adding a new
audio-capable architecture no longer requires editing _base.py.

New module:
  src/mobius/_configs/_extractors.py    # register_audio_hook + dispatch

Per-model files (one per former branch):
  per_model/_audio_default.py    # audio_processor, embd_layer, speech_lora
  per_model/_phi4mm_audio.py     # phi4mm audio_token_id
  per_model/_qwen3_asr_audio.py  # thinker_config.audio_config + token ids
  per_model/_gemma4_audio.py     # short-circuit to Gemma4AudioConfig
  per_model/_sensevoice_audio.py # encoder_conf + frontend_conf mapping

_extract_audio_config in _base.py shrinks to a 5-line shim that
triggers the per_model side-effect import and calls the dispatcher.
No behavior change: every existing audio-capable model still produces
the same AudioConfig from the same HF config.

Vision-side conversion (also a mega-switch) is intentionally deferred
to a follow-up so reviewers can verify the registry pattern on the
smaller surface first.

Tests:
  pytest src/ tests/build_graph_test.py tests/cli_test.py -n auto
    2769 passed, 41 skipped
  pytest tests/arch_validation_test.py -k 'sensevoice_small or phi4mm
                                            or qwen3_asr or gemma4'
    15 passed
Ruff: clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Justin Chu <justinchu@microsoft.com>
@github-actions

github-actions Bot commented May 20, 2026

Copy link
Copy Markdown

🏗️ Architecture Diff

Comparing 5d592c645ddf6a

Model Sub-model Changes Status

No architecture changes detected.


Legend: ⚪ No change · 🔵 Minor (attrs/inits) · 🟡 Moderate (nodes added/removed) · 🔴 Major (interface changed)

@github-actions

github-actions Bot commented May 20, 2026

Copy link
Copy Markdown

Performance Comparison

Comparing 5d592c645ddf6a

Model Metric Baseline Current Delta
bert (feature-extraction) model_size_bytes 359 KB 359 KB +0.0%
bert (feature-extraction) num_nodes 60 60 +0.0%
falcon model_size_bytes 364 KB 364 KB +0.0%
falcon num_nodes 66 66 +0.0%
gemma2 model_size_bytes 428 KB 428 KB +0.0%
gemma2 num_nodes 107 107 +0.0%
gpt2 model_size_bytes 388 KB 388 KB +0.0%
gpt2 num_nodes 53 53 +0.0%
llama model_size_bytes 425 KB 425 KB +0.0%
llama num_nodes 61 61 +0.0%
llama (static-cache) model_size_bytes 425 KB 425 KB +0.0%
llama (static-cache) num_nodes 58 58 +0.0%
mamba (ssm-text-generation) model_size_bytes 296 KB 296 KB +0.0%
mamba (ssm-text-generation) num_nodes 98 98 +0.0%
phi3 model_size_bytes 421 KB 421 KB +0.0%
phi3 num_nodes 59 59 +0.0%
phi3 (static-cache) model_size_bytes 421 KB 421 KB +0.0%
phi3 (static-cache) num_nodes 56 56 +0.0%
qwen2 model_size_bytes 425 KB 425 KB +0.0%
qwen2 num_nodes 61 61 +0.0%
qwen2 (static-cache) model_size_bytes 425 KB 425 KB +0.0%
qwen2 (static-cache) num_nodes 58 58 +0.0%
qwen3_5_moe (hybrid-text-generation) model_size_bytes 506 KB 506 KB +0.0%
qwen3_5_moe (hybrid-text-generation) num_nodes 275 275 +0.0%
qwen3_5_text (hybrid-text-generation) model_size_bytes 458 KB 458 KB +0.0%
qwen3_5_text (hybrid-text-generation) num_nodes 129 129 +0.0%
qwen3_5_vl (hybrid-qwen-vl) model_size_bytes 977 KB 977 KB +0.0%
qwen3_5_vl (hybrid-qwen-vl) num_nodes 413 413 +0.0%
t5 (seq2seq) model_size_bytes 836 KB 836 KB +0.0%
t5 (seq2seq) num_nodes 166 166 +0.0%
whisper (speech-to-text) model_size_bytes 1008 KB 1008 KB +0.0%
whisper (speech-to-text) num_nodes 128 128 +0.0%

No performance regressions.

@codecov

codecov Bot commented May 20, 2026

Copy link
Copy Markdown

justinchuby and others added 2 commits May 20, 2026 14:17
Before: every hook body had to open with
    if model_type != "phi4mm":
        return None

That's noisy and easy to get wrong when copy-pasting a hook for a new
model. Make the decorator accept an optional filter and have the
dispatcher skip hooks whose filter doesn't match the current
model_type:

    @register_audio_hook                  # runs for every model_type
    def _default(...): ...

    @register_audio_hook("phi4mm")        # phi4mm only
    def _phi4mm(...): ...

    @register_audio_hook("gemma4", "gemma4_text")
    def _gemma4_simple_case(...): ...

Hooks that also need to look at parent_config (Gemma4 audio,
Qwen3-ASR via thinker_config) keep the manual if-guard inside the
body, since the decorator filter can't reach beyond model_type.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Justin Chu <justinchu@microsoft.com>
Two concerns covered:

1. Mechanism — register_audio_hook works as a bare decorator and as a
   parameterised factory; the dispatcher applies model_type filters;
   a hook can short-circuit by returning a dict.

2. Cross-contamination — every per-model audio hook is verified to NOT
   fire for unrelated model_types. For filtered hooks (phi4mm,
   sensevoice), the dispatcher's filter must skip non-matching types
   even when the input config has matching shape (e.g. an audio_config
   dict that would normally trigger phi4mm). Bare hooks (default,
   qwen3_asr, gemma4) must return cleanly on a vanilla text-model
   config.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Justin Chu <justinchu@microsoft.com>
Base automatically changed from refactor-configs-1-package-skeleton to main May 20, 2026 15:03
@justinchuby
justinchuby requested a review from a team May 20, 2026 15:03
Signed-off-by: Justin Chu <justinchuby@users.noreply.github.com>
@justinchuby
justinchuby merged commit 9e7d1ad into main May 21, 2026
23 checks passed
@justinchuby
justinchuby deleted the refactor-configs-2-extractor-registry branch May 21, 2026 00:15
justinchuby added a commit that referenced this pull request May 27, 2026
## Part 3 of 3 — stacked on #310

Mirrors part 2/3 for the vision side: replaces the 185-line
`_extract_vision_config` switch with a per-model hook registry. The
vision dispatcher carries slightly more logic than audio because vision
also lifts a fixed set of shared fields (`image_token_id`,
`mrope_section`, `spatial_merge_size`, …) up to the top-level of the
returned dict.

## New per_model files

| File | Purpose |
|---|---|
| `_vision_default.py` | canonical HF `vision_config` + LoRA /
embd_layer / mrope_section |
| `_phi4mm_vision.py` | hard-coded SigLIP encoder dims (not in JSON) |
| `_hunyuan_vl_mot_vision.py` | InternViT-style ViT, flat config.json |
| `_internvl_vision.py` | default `image_token_id` for InternVL chain |

## End state

After this 3-PR series, **every model-type switch in the original
`_extract_*_config` functions has been moved into discoverable per-model
files**. New audio- or vision-capable models add a single file under
`src/mobius/_configs/per_model/` instead of editing `_base.py`.

The follow-on refactor (out of scope here) is to do the same for
per-model config subclasses (`Gemma2Config`, `MllamaConfig`, etc.) and
to carve up `ArchitectureConfig.from_transformers` itself.

## Tests

| Suite | Result |
|---|---|
| `pytest src/ tests/build_graph_test.py tests/cli_test.py -n auto` |
2769 passed, 41 skipped |
| `pytest tests/arch_validation_test.py -k 'phi4mm or hunyuan_vl_mot or
qwen2_vl or qwen3_vl or gemma4'` | 30 passed |
| Ruff | clean |

---------

Signed-off-by: Justin Chu <justinchu@microsoft.com>
Signed-off-by: Justin Chu <justinchuby@users.noreply.github.com>
Signed-off-by: justinchuby <11205048+justinchuby@users.noreply.github.com>
Signed-off-by: justinchuby <11205safetensors048+justinchuby@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: justinchuby <11205048+justinchuby@users.noreply.github.com>
Co-authored-by: justinchuby <11205safetensors048+justinchuby@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant