Repository navigation
Qwen3-Omni: encoders on ragged attention, no flash_attn - #1072
Merged
Merged
Conversation
Qwen3-Omni is about to need both, so move them out of qwen3_asr and qwen3_5 into mstar/model/components. Code moves as is; the ViT attention takes its resource key as an argument.
The HF audio and vision encoders were built with FlashAttention-2, which has no sm_103 kernels and isn't installed on Blackwell boxes, so the server couldn't start. Use the native AuT encoder and a ViT tower with deepstack, both attending through declared ragged resources, with their block stacks captured.
vasilevklart
approved these changes
Oct 31, 2026
vasilevklart
left a comment
Collaborator
There was a problem hiding this comment.
lgtm. On de85d7b on 2x H200 without flash_attn, qwen3omni_2gpu gets 1.79% WER on 20 LibriSpeech clips, with sane image and spoken replies. Both towers on real weights sit at or under HF bf16's error against HF fp32 SDPA. There is a conflict with #987 in qwen3_5/components/vision.py since both move the same Vit blocks out, to components/qwen3_vit.py here and components/qwen_vision.py there, can rebase if you ar e ready to merge
Collaborator
Author
|
Thanks for the H200 run. Merging now (branch brought up to date with main first, so CI reruns); #987 can rebase onto it after, and either file is fine with me as the shared home for the ViT blocks. |
This was referenced Nov 1, 2026
This was referenced Nov 1, 2026
vasilevklart
added a commit
that referenced
this pull request
Nov 1, 2026
Main's #1072 moved the Qwen3-VL ViT's shared parts into components/qwen3_vit.py and built Qwen3.5's and Qwen3-Omni's towers on it. qwen3_5/components/vision.py takes main's version; components/qwen_vision.py keeps only what Qwen3-VL adds on that ViT (its patch mergers and deepstack, fp32 rotary tables), the vision config Qwen3.5 shares, and FrameSDPA. The shared VisionAttention keeps its binding on a node that doesn't declare the resource, since the Qwen3-VL prompt encoder binds per-frame SDPA itself. Claude-Session: https://claude.ai/code/session_01EM3QYUVD2VuhDBCfazeGcy
dghosef
added a commit
that referenced
this pull request
Nov 2, 2026
Resolutions beyond picking a side: - Stops: the scaffold's decoder declares main's TokenStop (its prefill walks and decode), and its own stop checks are gone; Qwen3.5 keeps main's runtime stop check (runtime_check=True). - ViT: Qwen3.5 stays on main's shared components/qwen3_vit.py (#1072), and Muse Glimmer moves onto it too: qwen3_vit gains bilinear_table_taps (with zero padding), VisionRotaryEmbedding's keep_fp32 and output dtype, and PackedVisionAttention, the attention base a tower subclasses with its q/k/v projections (Qwen3.5's fused VisionAttention is one). components/vit.py is deleted. - #988: per-walk compile modes and the mixed token ladder are SampledTokenDecoder.capture_compile_mode(walk) and MIXED_TOKEN_BUCKETS; Qwen3.5 sets both as main does. - #852: the loop-back splice (_splice_row_ids) is in the shared decoder. - #977: the block-loop encoder replays exactly when its step leased the region. - #916: Qwen3.5 declares its chunking policies on the scaffold, and the decoder reports prompt and decode lengths. - #794: the decode loop's cap is max_output_tokens - 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Qwen3-Omni couldn't start on Blackwell. Both encoders were HF modules built with
attn_implementation="flash_attention_2", FA3 has no sm_103 kernels, and flash_attn 2.x isn't installed on these boxes, so any config with the encoders died at worker init. This moves them onto the engine's attention resources, the way LingBot and Cosmos3 went, andconfigs/qwen3omni_2gpu.yamlnow boots on 2 x B300 with flash_attn mapped in no worker (checked in/proc/<pid>/maps).What needed flash_attn
Only the two encoders. The Thinker and Talker were already on the paged FlashInfer resource, and Code2Wav uses its own Triton sliding-window kernel, which runs on sm_103 as is.
audio_encoderQwen3OmniMoeAudioEncoder, FA2 varlenAuTEncoderthroughRaggedAttentionSpecaudio_attn, one segment per 8 s windowvision_encoderQwen3OmniMoeVisionEncoder, FA2 varlenQwen3OmniVisionTowerthroughRaggedAttentionSpecvision_attn, one segment per image or frame groupChanges
components/aut_submodule.py: Qwen3-ASR's encoder node moved here asAuTEncoderSubmodule, with the resource key as an argument. Qwen3-ASR and Omni subclass it forprepare_inputsonly, so Omni gets request packing and the captured stack.components/qwen3_vit.py: Qwen3.5's ViT blocks and grid helpers moved here, with the attention key as an argument. Mergers stay per model since their parameter names differ.qwen3_omni/components/vision.py: Omni's tower on those blocks, plus the deepstack taps and mergers. Names matchthinker.visual.*, so the checkpoint loads by name.VisionEncoderSubmodulefollows Qwen3.5's node: one request per step, block loop captured by patch bucket up to 32768. The deepstack taps are extra outputs of the captured region.disable_torch_compile. Their stacks are captured, and a compiled wrapper around them would recompile per frame count while the prefix cache holds what earlier graphs computed.inv_freqtable fp32 through the worker's dtype cast, as the reference does (median embed error 5.7e-2 to 5.3e-2 against a 4.7e-2 yardstick).max_windows_per_replaysizesaudio_attnfrom what the largest bucket can hold (40 windows a request).patch_hf_fa2_int_maxlenand the encoders'AutoConfigcalls are gone. README,docs/installation.rst,docs/models.rst,pyproject.tomland the e2erequires:lists no longer say Omni needs flash-attn.No model code branches on device or kernel. FlashInfer picks the backend and a deployment can override it under
resources:. Ragged kernels on sm_103 at these head dims, error against fp32 SDPA on random q/k/v, time for one layer:autocutlassfa2cudnnfa3,trtllm-genParity
Reference: HF's modules on SDPA, since flash_attn isn't available on this hardware. HF's SDPA path splits per window and frame, so it attends the same spans FA2 does.
Encoder outputs, relative L2 against HF in fp32 (SDPA math backend), median (max) over 20 clips, and 20 images plus a 16-frame clip. The yardstick is HF's own module in bf16:
Everything is at the bf16 yardstick except the final vision embeds, which sit about 12% above it. Four packed clips against each alone differ by 1.3e-2.
End to end, greedy, 128 tokens, 20 inputs per modality at c=1, HF's
thinker.generateon M*'s own preprocessing, so a difference is model compute. Long greedy answers flip on rounding alone, so exact counts only mean something next to the yardsticks. Exact matches:What the answers get right doesn't move: transcript WER on these 20 clips 1.81% for mstar and 2.26% for HF, 15/20 video answers on both, Food101 class named in 12-15 of 20 across all runs (HF bf16 13, HF fp32 15), reference answer present in 8 of 20 spoken questions on both.
To separate the encoders from the Thinker, one HF bf16 thinker was fed by different towers, with HF's fp32 towers as gold: mstar's replayed towers match it on 47/80, HF's own bf16 towers on 46/80. The remaining gap to HF is on the Thinker side, which this PR doesn't touch: with no encoder running at all, text-only input matches 3/20 against 5/20 for HF's own bf16. Attributing that to FlashInfer vs SDPA rounding is a hypothesis.
Qwen3-ASR and Qwen3.5 had code moved, so both ran main vs branch at c=1 on GPU, twice each: 20/20 identical transcripts for Qwen3-ASR 0.6B, 20/20 identical answers for Qwen3.5-0.8B image input.
Numbers
2 x B300 SXM6 (sm_103), torch 2.12.1+cu132, FlashInfer 0.7.2 at
-O2. Each engine on its default 2-GPU split with prefix caching off, thinker greedy with a 256-token cap, talker at its default sampling, same OpenAI chat payload throughbenchmark/request.py. Two server sessions per engine, interleaved, server on 8 cores, client on 4, no CPU throttling. Mean (session range). All 2,200 requests of every session succeeded on all three engines.A2T (200 LibriSpeech clips)
A2S (100 spoken questions)
I2T (200 images)
V2T (50 clips, 1-token answers, so TTFT is the number)
T2S (200 Seed-TTS sentences, read aloud)
Peak VRAM: M* 23.0 + 91.5 GB, vllm-omni 250 + 187 GB, sglang-omni 255 + 253 GB. The competitors reserve by fraction, so theirs is reservation rather than need.
What the others ran: vllm-omni 0.30.0 with FLASH_ATTN on the ViT and FlashInfer plus Triton MoE on the thinker and talker (from its log),
--media-io-kwargsset to 2 fps to match. sglang-omni 0.2.0 withtrtllm_mhaon the thinker and talker (from its log), Triton packed attention on the encoders (from its code).Where the time goes, and what's next
recv -> preprocess doneis 527 ms at c=1 and 1563 ms of the 2190 ms at c=4, while the encoder is 43 ms and the Thinker prefill 60 ms.PreprocessWorkerThreadrunsload_videoandvideo_processorinline, 604 ms a clip when timed alone against 556 ms a clip of throughput at c=4. A pool per request would likely bring c=4 TTFT near c=1 (hypothesis, not tried).event_syncp50 0.08 ms, plan about 1.4 ms, launch about 2.1 ms).Not in this PR
inv_freqon main. Left alone so its numerics don't change here; its effect there isn't measured.max_windows_per_replaywould fix it; the guard above ran both arms withMSTAR_WORKSPACE_BUFFER_MB=2048.Tests
ruff check .: pass.OMP_NUM_THREADS=2: 8928 passed, 828 skipped, 53 failed vs 8925, 830, 49. All 49 fail on both. The other 4 aretest_eval_compare*job-lifecycle tests. All 4 pass on rerun with the machine idle, and the two I retried under load failed the same way on main.test_flex_attention_resource,test_waypoint_componentsandtest_waypoint_ditcrash on both.test/ci/test_cpu_selection.pypasses.test/modular/test_qwen3_omni_encoders.py(9 tests): the declared specs, the backend built withflash_attnabsent, audio windows and deepstack against HF on tiny weights through a stand-in resource, the capture ceiling, the fp32 rotary table, parameter names.test_qwen3_omni_prefix_cache.py: the HF-wrapper cases are replaced by a check that the engine leaves both encoders uncompiled.test_trust_remote_code.py: the two encoder cases are gone with theAutoConfigcalls.