Skip to content

Onboard Qwen2.5-Omni model - #193

Open
rui-ren wants to merge 14 commits into
mainfrom
ruiren/onboard-qwen25-omni
Open

rui-ren wants to merge 14 commits into
mainfrom
ruiren/onboard-qwen25-omni

Conversation

@rui-ren

@rui-ren rui-ren commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Adds Qwen2.5-Omni Thinker support with:

  • Packed audio encoder
  • Omni vision encoder
  • Audio, image, and video embedding fusion
  • MRoPE text decoder
  • Dedicated four-model export task
  • Hugging Face config extraction and weight routing
  • Graph and CLI regression tests

@github-actions

github-actions Bot commented Apr 23, 2026

Copy link
Copy Markdown

Performance Comparison

Comparing 223b438e8dd555c8

Model Metric Baseline Current Delta
bert (feature-extraction) model_size_bytes 359 KB 359 KB +0.0%
bert (feature-extraction) num_nodes 68 68 +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 105 105 +0.0%
gpt2 model_size_bytes 324 KB 324 KB +0.0%
gpt2 num_nodes 54 54 +0.0%
llama model_size_bytes 425 KB 425 KB +0.0%
llama num_nodes 60 60 +0.0%
llama (static-cache) model_size_bytes 425 KB 425 KB +0.0%
llama (static-cache) num_nodes 56 56 +0.0%
mamba (ssm-text-generation) model_size_bytes 296 KB 296 KB +0.0%
mamba (ssm-text-generation) num_nodes 94 94 +0.0%
phi3 model_size_bytes 421 KB 421 KB +0.0%
phi3 num_nodes 58 58 +0.0%
phi3 (static-cache) model_size_bytes 421 KB 421 KB +0.0%
phi3 (static-cache) num_nodes 54 54 +0.0%
qwen2 model_size_bytes 425 KB 425 KB +0.0%
qwen2 num_nodes 60 60 +0.0%
qwen2 (static-cache) model_size_bytes 425 KB 425 KB +0.0%
qwen2 (static-cache) num_nodes 56 56 +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 265 265 +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 127 127 +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 450 450 +0.0%
t5 (seq2seq) model_size_bytes 836 KB 836 KB +0.0%
t5 (seq2seq) num_nodes 176 176 +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 Apr 23, 2026

Copy link
Copy Markdown

The author of this PR, rui-ren, is not an activated member of this organization on Codecov.
Please activate this user on Codecov to display this PR comment.
Coverage data is still being uploaded to Codecov.io for purposes of overall coverage calculations.
Please don't hesitate to email us at support@codecov.io with any questions.

Comment thread src/mobius/models/qwen25_omni.py Fixed
Comment thread src/mobius/models/qwen25_omni.py Fixed
Comment thread src/mobius/models/qwen25_omni.py Fixed
Comment thread src/mobius/models/qwen25_omni.py Fixed
@github-actions

github-actions Bot commented Apr 23, 2026

Copy link
Copy Markdown

🏗️ Architecture Diff

Comparing 223b438e8dd555c8

Model Sub-model Changes Status
bert (feature-extraction) model 0
falcon model 0
gemma2 model 0
gemma4 (gemma4) decoder 0
gemma4 (gemma4) embedding 0
gemma4 (gemma4) vision_encoder 0
gemma4_text model 0
gpt2 model 0
llama model 0
llama (static-cache) model 0
mamba (ssm-text-generation) model 0
phi3 model 0
phi3 (static-cache) model 0
qwen model 0
qwen (static-cache) model 0
qwen2 model 0
qwen2 (static-cache) model 0
qwen2_moe model 0
qwen2_moe (static-cache) model 0
qwen3 model 0
qwen3 (static-cache) model 0
qwen3_5_moe (hybrid-text-generation) model 0
qwen3_5_text (hybrid-text-generation) model 0
qwen3_5_vl (hybrid-qwen-vl) decoder 0
qwen3_5_vl (hybrid-qwen-vl) embedding 0
qwen3_5_vl (hybrid-qwen-vl) vision_encoder 0
qwen3_moe model 0
qwen3_moe (static-cache) model 0
qwen3_next (hybrid-text-generation) model 0
t5 (seq2seq) decoder 0
t5 (seq2seq) encoder 0
whisper (speech-to-text) decoder 0
whisper (speech-to-text) encoder 0

No architecture changes detected.


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

Comment thread src/mobius/models/qwen25_omni.py Fixed
Comment thread src/mobius/models/qwen25_omni.py Fixed
@rui-ren

rui-ren commented Apr 28, 2026

Copy link
Copy Markdown
Contributor Author

@copilot can you please use this skill of adding-a-new-model to review this PR, and provide good feedback of this PR.

Currently I am only working on the thinker component, please provide good feedback of this.

Copilot AI commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

@copilot can you please use this skill of adding-a-new-model to review this PR, and provide good feedback of this PR.

Currently I am only working on the thinker component, please provi...

Reviewed the thinker work at d5a4ec9 using the adding-a-new-model checklist. Focused feedback:

  • Blocking: src/mobius/models/qwen25_omni.py does not import/compile yet (python -m py_compile ... fails on an unmatched ) at line 302), so the registry import cannot work.
  • Blocking: Qwen25OmniThinkerForConditionalGeneration is still pass. Since it is registered with task="speech-language", it needs at least the audio_tower, embedding, and decoder attributes expected by SpeechLanguageTask; otherwise component validation will fail.
  • The decoder currently references DecoderLayer, RMSNorm, and initialize_rope without importing them.
  • The embedding path has several correctness issues: inputs_ids vs input_ids, _image_token_id vs _image_token_ids, returning input_embeds instead of inputs_embeds, and audio.image_token_id is accessed even when config.audio may be None.
  • The task split does not yet match the intended thinker shape if image support is included. SpeechLanguageTask only wires audio_encoder -> embedding -> decoder; it does not build or pass a vision_encoder/image_features model. Either keep this first PR audio-only or add a dedicated Omni thinker task/component spec for audio_encoder, vision_encoder, embedding, and decoder.
  • Weight loading is missing. Qwen Omni HF checkpoints will likely need thinker. prefix stripping and routing similar to Qwen3ASRForConditionalGeneration.preprocess_weights (thinker.model.embed_tokens to embedding, decoder layers/norm/lm_head to decoder, audio/vision towers to their submodules).
  • Checklist gaps before merge: export the model from models/__init__.py, add class metadata/docstring on the registered class, add a tiny qwen2_5_omni L1 build config/test, and then run the targeted build graph test once dependencies are available.

rui-ren and others added 3 commits May 11, 2026 11:03
Pick up the partial Qwen2.5-Omni onboarding (#193) and bring it to a
state where the file imports cleanly, the full pytest suite collects
without errors, and the registry-completeness test passes.

Concrete fixes:

* Syntax: removed stray ")" at end of Qwen25OmniDecoderModel that
  broke every test that imported the module (65 collection errors on
  plain main).
* Missing imports: RMSNorm, DecoderLayer, initialize_rope were
  referenced by Qwen25OmniDecoderModel but not imported. Also
  consolidated the OpBuilder import (was using the internal
  onnxscript._internal.builder module).
* Typos in Qwen25OmniEmbeddingModel:
  - Used audio.image_token_id (image_token_id lives on VisionConfig).
  - self._image_token_ids plural typo where _image_token_id was meant.
  - "return input_embeds" referenced a non-existent local; renamed.
  - Forward arg "inputs_ids" -> "input_ids".
* Composite class implemented: Qwen25OmniThinkerForConditionalGeneration
  was just `pass`. Now mirrors the gemma4 multi-encoder layout — owns
  decoder, embedding, vision_encoder, and audio_tower (named to match
  what SpeechLanguageTask looks up). forward() raises
  NotImplementedError because this is a multi-output split needing a
  dedicated task. preprocess_weights maps HF
  thinker.audio_tower.* / thinker.visual.* / thinker.model.* /
  thinker.lm_head.* onto the new module structure, drops Talker /
  codec-head keys, and applies the standard embed/lm_head weight
  tying.

Test infrastructure:

* Added qwen2_5_omni to _KNOWN_UNTESTED_MODEL_TYPES with a comment
  explaining the situation: the audio + vision + text 4-model split
  needs a dedicated task (like Gemma4Task); the existing
  speech-language task only drives audio + text. When that task is
  added in a follow-up, this entry should move into a dedicated graph
  test (mirroring test_gemma4_any_to_any_graph).

Also cleaned up: unused n_window lookup, unused dataclasses import,
trailing blank lines, and audio-attention docstring punctuation that
ruff flagged.

Tests: full tests/build_graph_test.py + src/mobius/ suite passes
(2767 passed, 43 skipped). Ruff check + format clean on touched files.

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

Copy link
Copy Markdown
Member

@copilot update to the latest and ensure the implementation is clean and correct

Copilot AI added 2 commits July 13, 2026 02:47
Signed-off-by: GitHub <noreply@github.com>
Add a dedicated four-model task and align audio, vision, decoder, configuration, and weight routing with the current Hugging Face Thinker implementation. Cover the nested config and complete graph package with focused regression tests.

Signed-off-by: GitHub <noreply@github.com>
@justinchuby
justinchuby marked this pull request as ready for review July 13, 2026 02:53
@justinchuby
justinchuby requested review from a team and Copilot July 13, 2026 02:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR onboards the Qwen2.5-Omni (Thinker) architecture into mobius, implementing a four-model ONNX split (audio encoder, vision encoder, embedding, decoder) and wiring it into the registry/config extraction paths.

Changes:

  • Added Qwen25OmniThinkerForConditionalGeneration + Qwen25OmniTask to build a 4-part ModelPackage (audio_encoder, vision_encoder, embedding, decoder).
  • Implemented new Omni audio encoder components and a generic Conv1d building block.
  • Extended config extraction to pull Omni’s nested thinker_config.vision_config (including video_token_id) and registered the new model/task in the public APIs and registry.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/build_graph_test.py Adds a graph-build test for the 4-model package and marks the model type as “specialized” for registry completeness.
src/mobius/tasks/_qwen25_omni.py New task to build the Omni 4-model split (audio/vision/embedding/decoder).
src/mobius/tasks/init.py Exposes Qwen25OmniTask and registers the task name qwen25-omni.
src/mobius/models/qwen25_omni.py New Thinker model implementation (audio + vision + embedding fusion + decoder) and HF weight routing.
src/mobius/models/qwen25_omni_test.py Unit tests for nested-config extraction and preprocess_weights() routing rules.
src/mobius/models/init.py Exports Qwen25OmniThinkerForConditionalGeneration.
src/mobius/components/_qwen25_omni_audio.py Adds Omni packed audio attention + encoder layer components.
src/mobius/components/_conv.py Adds a reusable Conv1d component.
src/mobius/components/init.py Exposes Omni audio components via the public components API.
src/mobius/_registry.py Registers model type qwen2_5_omni with task qwen25-omni.
src/mobius/_configs/per_model/_qwen25_omni_vision.py Adds a per-model hook to extract vision config from thinker_config.
src/mobius/_configs/per_model/init.py Imports the new Omni vision extractor hook.
src/mobius/_configs/_sub_configs.py Adds video_token_id to VisionConfig.
src/mobius/_configs/_extractors.py Lifts video_token_id as a shared vision field.
src/mobius/_configs/_base.py Adds top-level video_token_id and recognizes qwen2_5_omni_text for config extraction.

Comment on lines +85 to +89
q = op.Unsqueeze(op.Transpose(q, perm=[1, 0, 2]), [0])
k = op.Unsqueeze(op.Transpose(k, perm=[1, 0, 2]), [0])
v = op.Unsqueeze(op.Transpose(v, perm=[1, 0, 2]), [0])
attn_output = op.Attention(
q,
Comment on lines +14 to +17
import onnx_ir as ir
from onnxscript import nn
from onnxscript._internal import builder

Comment on lines +40 to +48
def build(self, module: nn.Module, config: ArchitectureConfig) -> ModelPackage:
self._validate_components(module)
models = {
"audio_encoder": self._build_audio(module.audio_encoder, config),
"vision_encoder": self._build_vision(module.vision_encoder, config),
"embedding": self._build_embedding(module.embedding, config),
"decoder": build_decoder_from_embeds(module.decoder, config, mrope=True),
}
return ModelPackage(models, config=config)
Comment thread src/mobius/tasks/_qwen25_omni.py
Comment thread src/mobius/models/qwen25_omni.py
Comment thread tests/build_graph_test.py Outdated

Copilot AI commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

@copilot update to the latest and ensure the implementation is clean and correct

Updated onto latest main and completed the clean Thinker implementation in 5ca65be: dedicated four-model task, HF-aligned packed audio/vision/decoder behavior, nested config extraction, checkpoint routing, and regression coverage. Full suite: 3,124 passed, 43 skipped; audio/vision/decoder synthetic parity also passes.

Copilot AI changed the title Onboard qwen25-omni model Onboard Qwen2.5-Omni Thinker model Jul 13, 2026
Copilot AI requested a review from justinchuby July 13, 2026 03:07
@rui-ren rui-ren changed the title Onboard Qwen2.5-Omni Thinker model Onboard Qwen2.5-Omni model Aug 5, 2026
@rui-ren

rui-ren commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

@copilot resolve the conflicts and feedback

Resolve config and registration conflicts while retaining both model sets. Move the Thinker graph test to the split build_graph suite introduced on main.

Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: rui-ren <15321482+rui-ren@users.noreply.github.com>
@CLAassistant

CLAassistant commented Sep 18, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
2 out of 4 committers have signed the CLA.

✅ justinchuby
✅ rui-ren
❌ ruiren_microsoft
❌ Copilot


ruiren_microsoft seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

Copilot AI and others added 2 commits September 18, 2026 00:59
Use public builder types and rank-3 audio attention, validate required encoders, and respect the configured audio input dtype. Match Hugging Face's final audio normalization epsilon and correct component documentation. Add shared tiny-config coverage and offline HF parity for packed audio, vision, batched modality fusion, and cached MRoPE decoding.

Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: rui-ren <15321482+rui-ren@users.noreply.github.com>
Keep the composite parent for audio and vision extraction while choosing Thinker text config instead of the sibling Talker. Preserve serialized token-index aliases in raw-config fallback and cover both full-checkpoint paths plus unchanged Qwen3-TTS selection.

Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: rui-ren <15321482+rui-ren@users.noreply.github.com>

Copilot AI commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the conflicts and feedback

Resolved in 83e7289e, 97a0899a, and 8dd555c8.

  • Merged latest main and migrated Omni graph tests to the new layout.
  • Addressed attention layout, public imports, missing-encoder validation, audio dtype, documentation, and shared-config coverage.
  • Fixed audio LayerNorm epsilon and full-checkpoint selection accidentally choosing Talker instead of Thinker.
  • Added HF parity for all four components, batched fusion, and cached decoding.

Final checks: 2,259 passed, 53 skipped, 40 xfailed; lint, secret scans, and scoped review pass. Combined validation/CodeQL could not complete because its merge-diff generation repeatedly timed out.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The audio ABI is not processor-native, and packed attention currently creates a quadratic mask.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (1)

src/mobius/tasks/_qwen25_omni.py:63

  • Use a float32 processor boundary here. The standard audio processor emits float32, while fp16/bf16 exports currently require callers to manufacture reduced-precision inputs (NumPy cannot directly supply bfloat16); Qwen25OmniAudioEncoder.forward already casts once to the convolution weight dtype. Keep the graph input FLOAT and update the dtype assertion accordingly.
            dtype=config.dtype,
  • Files reviewed: 19/19 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment on lines +61 to +65
input_features = builder.input(
"input_features",
dtype=config.dtype,
shape=[num_chunks, n_mels, chunk_len],
)
Comment on lines +78 to +83
attention_bias = op.Where(
same_segment,
op.CastLike(0.0, q),
op.CastLike(-1e9, q),
)
attention_bias = op.Unsqueeze(attention_bias, [0, 1])
_builder, "_load_transformers_config", lambda *args, **kwargs: (hf_config, raw_json)
)

package = _builder.build_transformers_model("test/qwen25-omni", load_weights=False)

@titaiwangms titaiwangms left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I completed a full review of the Qwen2.5-Omni onboarding at 8dd555c8.
The core audio/vision/text math is generally faithful to the Hugging Face
implementation, but I found several integration blockers.

Major findings

  1. ORT GenAI export generates an unsupported Omni configuration

    src/mobius/integrations/ort_genai/auto_export.py:1615-1785

    The package is detected as both VLM and speech, but Qwen2.5-Omni is not
    registered in the Qwen-VL/model-type mappings. It therefore takes the
    generic speech path, which identity-maps graph inputs such as
    input_features, chunk_lengths, and pool_indices. The code itself notes
    that ORT GenAI's speech input keys are a closed set and reject arbitrary
    graph input names. _write_audio_processor_config() also has no Omni branch.

    Until ORT GenAI has a defined Qwen2.5-Omni Thinker contract, please fail
    explicitly for --runtime ort-genai rather than emit a package that is
    expected to fail at load time.

  2. The registration violates the repository's L2 coverage contract

    src/mobius/_registry.py:967-970
    tests/model_coverage_test.py:505-537

    qwen2_5_omni has neither a test_model_id nor an entry in
    _COVERAGE_SKIP. TestL2ConfigValidation will therefore fail
    deterministically. Add a pinned test_model_id, or add a documented skip
    consistent with the other large audio/multimodal models.

  3. The four-component model does not declare HF_COMPONENT_SOURCES

    src/mobius/models/qwen25_omni.py:425-526
    src/mobius/_component_manifest.py:143-183

    Component inspection and component-aware quantization obtain source
    ownership from HF_COMPONENT_SOURCES. Without it, the four declared roles
    have empty HF source paths and thinker.* tensors cannot be attributed to
    the correct component.

    The mapping should cover at least:

    • audio_encoder: thinker.audio_tower
    • vision_encoder: thinker.visual
    • embedding: thinker.model.embed_tokens
    • decoder: thinker.model.layers, thinker.model.norm,
      thinker.model.rotary_emb, and thinker.lm_head
  4. The onnx-genai workflow has no producer for video_features

    src/mobius/integrations/onnx_genai/workflow_metadata.py:6330-6365
    src/mobius/tasks/_qwen25_omni.py:103-124

    produced_features contains the vision encoder's image_features and the
    audio outputs. The embedding graph additionally requires video_features,
    so workflow generation turns it into a required application request input
    instead of invoking the shared vision tower for video or defining an
    explicit packed image/video contract. The generated workflow therefore
    does not provide an end-to-end raw-video path.

  5. The implementation duplicates existing Qwen-family architecture and task
    contracts, and the copies have already drifted

    The Omni-specific audio frontend is justified, but the PR separately
    reimplements the decoder-from-embeddings loop, multimodal token replacement,
    four-model task plumbing, and Conv1d, despite existing equivalents in
    Qwen2.5-VL, Qwen3-ASR, Qwen2VLMultimediaTask, and the component library.

    This is not only a maintainability concern: the duplicated paths are where
    the missing component source metadata, incomplete video workflow, incorrect
    generic ORT GenAI routing, and unchecked feature-width assumptions occur.

    Please retain the Omni-specific audio chunking/pooling implementation, but
    reuse or extract shared primitives for:

    • decoder-from-embeddings with MRoPE;
    • ordered audio/image/video placeholder replacement;
    • Qwen-VL vision block/attention factories;
    • audio+vision four-component task construction;
    • the existing public Conv1d.

Additional correctness issues

  • src/mobius/tasks/_qwen25_omni.py:103-116 declares every feature input with
    width config.hidden_size, while the encoders produce
    audio.output_dim and vision.out_hidden_size. Validate equality at build
    time or use the actual encoder dimensions.
  • Qwen25OmniEmbeddingModel._replace_tokens() does not validate that the
    number of placeholder tokens equals the number of feature rows. Too few
    rows produce an opaque Gather bounds failure; excess rows are ignored.
  • The audio config exposes activation_function, but the implementation
    always emits GELU. Reject unsupported values or dispatch through the
    configured activation.
  • vc.fullatt_block_indexes or (7, 15, 23, 31) treats a valid empty list as
    missing and silently changes an all-windowed configuration.
  • _conv.py adds a second Conv1d even though the component library already
    exposes one with the required behavior.

The audio convolution, pooling-index arithmetic, packed cu_seqlens,
sinusoidal positional embeddings, projection-bias asymmetry, fp16 clamp, and
Omni-specific Qwen2.5-VL vision parameter layout were checked against the
reference implementation and appear correct.

This was a static review. I did not execute the PR tests or perform downstream
ORT GenAI/onnx-genai runtime loading.

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.

6 participants