Repository navigation
Performance 5/7: Qwen3.5 and Qwen3-Omni - #970
Conversation
…speech-language Brings in #858's new commits: TokenStop leaves the token budget to the decode loop's cap (generated_key is gone); the step closing a reply that ended at prefill withholds its token through StopCheck / BatchedStopCheck instead of the engine's token_stop pop, and the inline client emit gives a withheld row None in both graph runtimes; the ragged-attention op registry names each manager uniquely; a replica count that fails leaves warmup at one copy; and docs (the worker's MSTAR_PY_SWITCH_INTERVAL_SEC row, warmup's solo run, one_row_per_step, comments that name the loop cap as the budget). Semantic resolutions: TokenStop drops generated_key and keeps this branch's loopback_signal and withhold. Chatterbox's T3 and the Qwen3-TTS Talker drop their generated_key; their "generated" / "generated_frames" counters stay, since the T3's speech position and the Talker's min_new_tokens read them. Every TokenStop adopter this branch adds already caps its decode loop at the budget (Chatterbox and Qwen3-TTS at max_new_tokens - 1, the prefill's token counted; Orpheus, Higgs Audio and the Qwen3-Omni Thinker at max_output_tokens), so none needed a new cap; the two loop-cap comments that said the stop check counts the prefill's token now say the prefill samples it. In check_stop and check_stop_batched the step closing a reply that ended at prefill withholds buffer and withhold together, and a stop-id row still withholds withhold, ignore_eos or not. The cap-vs-stop-check comparisons for this branch's adopters (Orpheus, Chatterbox, Higgs Audio, the Qwen3-Omni Thinker, Qwen3-TTS) are deleted, as #858 deleted its own, with the Qwen3-TTS test that its stop check counts the prefill's frame; the Chatterbox, Orpheus, Higgs Audio and Qwen3-TTS stop-check tests now expect a row at its budget to go on. api_switch_interval_s and Model.API_SWITCH_INTERVAL_S stay: #858 kept the API server's key and only documented the worker's variable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…or inline (R3.1, R3.2) The data-worker thread publishes how many of its taken-up inputs are not being prepared (waiting_inputs), and queue_depth adds that to the queue, so one request preparing reads the same with or without a pool and the gauge no longer reads the thread's private deque. The four descriptions of the gauge now say the same thing, and that client-stream appends count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…comment (R3.3, R3.4) A class value that is not a positive integer now stops startup naming the model class, instead of a config value building a pool for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… into perf/split-3-generation Brings in #859's merge of #858's new commits (8266de2): TokenStop leaves the token budget to the decode loop's cap (generated_key is gone), the step closing a reply that ended at prefill withholds its token through StopCheck / BatchedStopCheck instead of the engine's pop, the inline client emit gives a withheld row None in both graph runtimes, the ragged-attention op registry names each manager uniquely, a replica count that fails leaves warmup at one copy, and docs. No semantic resolution was needed: this branch adds no TokenStop adopter, no generated_key, no cap-vs-stop-check test for a TokenStop adopter, and no withheld, inline-emit or Rust inline-value change; its own decode-style loops (BAGEL's denoise walks, V-JEPA 2's rollout) keep their own stop checks and caps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…artup comment (R4.2, R4.3) _positive_int moves from entrypoint.py into data_worker.py (entrypoint already imports from it) and checks both the class PREPROCESS_WORKERS and the config's preprocess_workers, replacing their two inline copies. The messages are unchanged. The comment above the check claimed it runs first, but check_media_limits() runs before it; it now gives the reason: nothing to close yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ngth (R4.4) _publish_waiting() runs before every hand-off and rescanned the whole _preparing deque each time, k^2/2 per pass. Without a preprocess pool no entry there is ever being prepared (the inline one is popped first), so len() is exact; the scan stays only with a pool. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The worker passes the value to sys.setswitchinterval, which refuses only values of 0 or below, so `nan` and `inf` are applied rather than ignored with a warning, as the row implied. The row now names what is ignored: a value float() cannot parse, or one of 0 or below. _env_number's docstring cited this variable as raising a too-small value to a minimum; it ignores one instead, so only MSTAR_RECOMPILE_LIMIT is left as the precedent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iable The key sets the API server's GIL switch interval only. The worker processes read theirs from MSTAR_PY_SWITCH_INTERVAL_SEC, whose row already points back to this key; the serving config row now says so too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a hand-off (R4.1) The pool case of test_queue_depth_counts_requests_waiting_for_the_data_worker could pass on the moment both requests were still in in_queue; it now also reads the gauge after the wait, when the pool has prepared #1 and it waits behind "hold". test_a_prepared_request_waits_while_a_peer_cannot_take_more records waiting_inputs at each hand-off, which pins the publish before it. Checked against mutated code: counting only unprepared entries reads 1 at the new assert (the old _wait still passed); dropping the publish before the hand-off records [2, 1]. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…it-4-shared This brings in main's #886 (StopCheck, BatchedStopCheck, TokenStop.withhold and the worker's withheld_outputs), which this branch carried as the same code, and #927's dense attention manager, which replaces this branch's per-segment FA3 path. Conflicts: - model/submodule_base.py: both sides now return set[str] | StopCheck; kept this branch's read-only Mapping annotation and its note on lazy host copies. - attn/dense.py, test_dense_attention.py, docs/models.rst: main's. The per-segment flash_attn_func branch ran ahead of #927's wrapper choice, so wherever the FA3 wheel imports it would bypass the FlashInfer wrapper #927 picks for a GPU FA3 has no kernel for. Main's test_dense_matches_sdpa_over_prefix_and_fresh covers what the dropped test did, on both wrappers. - linear_attn/wrappers.py: both; #877's _plan_snapshots beside this branch's _unpadded. - docs/adding_models.rst: main's StopCheck paragraph, which already says what this branch's did. - test_withheld_outputs.py: main's (its fake check_stop_batched takes no step_walk). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…split-2-speech-language Brings in #858's two merges of origin/main: the Fun-CosyVoice3 model (#495, with its OpenAI adapter, sentence splitting for long speech input, and a Chatterbox reference-audio error that names no server path), the admit fence that waits out a slow lockstep admit (#882), and the KV snapshot directory sweep keyed by launch token and lock (#637); and #858's docs for MSTAR_PY_SWITCH_INTERVAL_SEC and api_switch_interval_s. No conflicts. The MSTAR_PY_SWITCH_INTERVAL_SEC row now names this branch's model default as well: the API server's interval is the serving config's api_switch_interval_s, else the model's Model.API_SWITCH_INTERVAL_S. CosyVoice3 needs no change for this branch's shared-code edits: its LM declares no TokenStop and keeps its own check_stop, it declares no compile_regions or piecewise regions, and it calls none of the removed helpers (reads_device_loopback, lengths_to_mask, _non_pad_mask, the decode attention kernels). The Chatterbox parts it reuses (Snake, the S3 tokenizer, VoiceCache, _decode_audio) keep their interfaces. This branch changes nothing in the worker, the KV transfer code or the OpenAI adapters, so #882, #637 and the adapter edits arrive as they are on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_a_compiled_region_attends_through_a_rebuilt_resource still said the op names its manager by resource key, with a rebuilt resource taking over that key. Each manager now registers under its own op key, the resource key plus a number unique in the process. The docstring now says what the test checks: a region compiled against one manager, then called with a manager rebuilt under the same resource key, attends through the rebuilt one's plan while the old one is still registered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A withheld output has to leave by a streaming edge that no node this worker speculates into reads, and cannot be the loop-back signal. The startup check enforces this for TokenStop.withhold; a StopCheck returned from check_stop (the Qwen3-Omni Talker, Fish S2 Pro's AR) is not checked, so the paragraph that introduces StopCheck now states the constraint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ignature _prematerialize_for_check_stop returns _LazyHostOutputs, which copies an output name the first time it is read, not a dict of host copies of every tensor; its annotation and docstring now say so. Engine.extend_prefix_chains is annotated with the Mapping its body already checks for, and the resource runner's per-request fallback checks Mapping too, so one contract is checked one way (today only plain dicts reach that fallback). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ead ones The API server's message loop now waits in poll_for_messages instead of sleeping, so a stand-in that returns at once made a loop running on a thread spin on the GIL. api_server_helpers.idle_poll waits out the timeout as the socket's poll does, and every stand-in for the API server's loop uses it; their mis-indented and blank-padded copies go with them. The data-worker thread and the Rust-frontend bridge never poll, so the poll_for_messages entries on their stand-ins are removed. The conductor stand-in in test_kv_layout_intake.py had none, so each pass of the main loop raised AttributeError and was logged; it now answers as the other conductor stand-ins do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… not by rewriting declare_step attn_key used to overwrite the config's public declare_step field with a bound method in __post_init__. A dataclasses.replace of such a config then passed both fields and raised, and a shallow copy kept declaring through the original's method. The runner now asks the config for the region's step (region_step), which answers from attn_key or declare_step on the instance it is called on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…step that leased it A region that takes its slot before the outer declaration must replay only on a step that leased it: unleased, the step's own declare_step covers the region's resources, and a replay would declare, plan and commit them a second time. Each forward used to enforce this itself, against ModelInputsFromEngine.piecewise_leases (Qwen3.5 vision, Qwen3-TTS, V-JEPA2, Omnilingual, OmniVoice) or against engine_inputs.step being None (Qwen3-ASR, nemotron duplex). The engine now leaves an unleased region out of the runners it hands the forward (SubmoduleManagement.runners_for), so a forward replays whenever its runner is there. The piecewise_leases field on ModelInputsFromEngine and the per-model checks and their tests go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…top field TokenStop.loopback_signal bundled two concerns into the stop policy: which token ends the loop, and how a decode walk reads its token back on the device. Declaring it also made the walk's step input-free, which silently turned on pre-plan reuse for Whisper, Qwen3-ASR and Higgs decode, whose measured numbers predate that. Loop-back is now ARNodeSubmodule.device_loopback, a DeviceLoopback(walk, signal, sampler, buffer); the loop-back hooks (uniform_row_inputs, device_loopback_signals, inline_client_signals) follow from it, and one base read, loopback_inputs, replaces the four copies of the slot-master read and the routed-row overwrite (Whisper passes its timestamp rule state as a declared loop-back state). step_is_input_free is again each node's own answer: Qwen3.5 decode says so, the others keep the default. A walk is now a required field, so the engine's check for a missing one goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the loop-back signal too When a prefill walk samples a stop id, the loop's first step only closes the reply, and its outputs are withheld. It withheld the token's buffer (new_token), but a device loop-back node's client edge carries its loop-back signal (text_inputs), inline off that buffer: the worker nulls an inline value only when the signal itself is withheld, so the token sampled after the stop id reached the client. On the tensor path the routed signal leaked the same way. The closing step now withholds the node's DeviceLoopback signal beside the buffer, so prefill_walks and device loop-back compose. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t raising (R1.18) The engine asks `can_batch` with whatever rows survived `prepare_inputs`, so a step whose rows all failed reached `layout([])`, whose `max()` of no chunk widths raised and turned the rows' own failures into a step exception. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l ends the reply These prefills sample the reply's first token, but their TokenStop declared no prefill_walks. A stop id sampled there was either ignored (Higgs, Qwen3-ASR: only a decode row could stop) or a stop for a loop not yet entered, which the runtime drops (T3), so decode ran on to the cap. They now list their prefill walks: the loop's first step closes the reply and its outputs are withheld. As TokenStop says, that first token counts toward the budget, so Higgs and Qwen3-ASR now give the loop max_output_tokens - 1 and a capped reply is max_output_tokens long, not one more (T3 already did). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… rows bring A captured decode builds its rotary tables inside the graph from the planned positions, so preprocess skipped them, but only on the loop-back branch. A step whose rows all brought their token routed (each request's first decode step, or every step with MSTAR_DEVICE_LOOPBACK=0) still built the 3D positions and cos/sin, and the capture held two static buffers for them, all unread. preprocess now returns before them on any captured decode step, and the capture test reads engine_inputs.captured instead of probing the step's context for a slot lease. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1, R1.5) Omni's encoder node drove transformers' Qwen3OmniMoeAudioEncoder internals from mstar code, a second implementation of `components/aut_encoder.py`, which Qwen3-ASR already runs packed, windowed and captured. Omni now builds its tower as `AuTEncoder`, loaded from `thinker.audio_tower.*` as Qwen3-ASR loads it, and its config is `AuTEncoderConfig` (the Omni-only `AudioEncoderConfig` is gone). The frontend improvements move into the shared component, so Qwen3-ASR gets them too: - the trim back to each chunk's valid rows gathers by host-built indices (a pinned, non-blocking upload) instead of a pageable H2D plus boolean indexing, both host syncs; - the chunk count pads with empty chunks to a power of two (cuDNN plans each new conv shape); - token counts follow the checkpoint's `n_window` instead of 100 frames; - the eager path passes the layout's host boundaries, not a device tensor. The submodule keeps its packing policy (one replay a step, `can_lease` and the all-or-nothing `can_batch` past the largest bucket). Deleted with the HF copy: `AudioLayout`, `layout`, `_frontend`, `_layers`, the `_attn_fns` memo, the hand-written region declaration (now `attn_key=AUDIO_ATTN`), the double-clone of the leased output, the per-row layout lists rebuilt three to four times a step, the HF FA2 construction and its stale comments. GPU-unverified: compare audio_embeds against the base on real weights. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ath only when x, residual and weight share a dtype FlashInfer's fused add+RMSNorm reads the residual and the weight in x's dtype, but the kernel branched on x's dtype alone. mstar::rms_norm casts its weight; this op, which run_layers hands every decoder stack, did not, so a model whose norm weight or residual stream was in another dtype would have its buffers read as the wrong type. Any mix now takes the unfused add and norm, which casts the weight. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The engine's private _cudnn_benchmark, added for capture_cudnn_benchmark, was a line-for-line copy of wan_vae.cudnn_benchmark. The helper now lives once, in mstar.engine.fp32_precision next to fp32_precision_scope, the other process-wide backend flag the engine sets around a node's warm-up and capture. wan_vae imports it from there, so Cosmos3's VAE decode keeps calling it through wan_vae. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rce's ceiling (R1.2) `_fits_capture` compared a step's windows with the class constant, while the captured wrapper is sized by the resource's `max_segments_per_request`, which a deployment may override under `resources: audio_attn:` (docs/serving.rst). An override below the constant let a step lease a replay whose plan raised for too many segments, failing every clip in the step; one above it was ignored. It now reads the bound resource's ceiling, as the Qwen3.5 vision tower and Omnilingual do. The declared ceiling is derived from the encoder's geometry (the top bucket's full windows plus a short last one per clip) instead of a literal that assumed `n_window_infer` = 800. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and captured Qwen3-Omni's audio encoder ran transformers' Qwen3OmniMoeAudioEncoder eagerly, about 10 ms a call and mostly launches, with its own step splitter around it. That is the same AuT architecture, on the same thinker.audio_tower weights, that Qwen3-ASR already runs natively: every attention window of every clip is a segment of a ragged attention resource, and the 32-layer stack replays a CUDA graph by packed token count. The audio node is now that node (Qwen3ASREncoderSubmodule), built without a spectrogram config so it takes the API server's log-mel features as they arrive, over an AuTEncoder loaded from the checkpoint and a RaggedAttentionSpec sized for the longest clip (MAX_AUDIO_SECONDS). Omni's audio config is AuTEncoderConfig. The HF wrapper, its frame-budget splitter, its return_dict probe and their tests go; the host-sync test now checks the node Omni runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_per_step A region that replays a whole step as one row (one_row_per_step) captures at batch size 1, so its plan held max_segments_per_request segments for every request of the step: Qwen3.5's vision tower and Omnilingual set a "per request" ceiling that meant per step, Omnilingual's to its max batch size. RaggedAttentionConfig.max_segments_per_step now names that ceiling (and a deployment can override it like the others); when set, it sizes every captured plan of the resource. Both adopters declare and read it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The per-request fallback aliased text_inputs as new_token with a dict splat, which reads every output name, so the lazy host outputs copied all of them to the host. A ChainMap over the outputs reads only what the check reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ladder tests read only configs/qwen3_5*.yaml, so the Qwen3-Omni large_kv config's ladder was never checked. Every config that sets model_kwargs.prefill_token_buckets is now built into its own model's prefill capture; the Qwen3.5-specific checks keep to Qwen3.5 configs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every shipped ladder's model must take prefill_token_buckets as its own argument (it would otherwise vanish into **kwargs); the Thinker's capture check stays for Qwen3-Omni, whose ladder no other test builds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
merceod
left a comment
There was a problem hiding this comment.
This one can't go in as it stands. Once #969 lands, main conflicts with it in the Omni Thinker's audio and vision prefill (#1110), and the deepstack slice here is wrong on #1110's spliced rows, so the merge needs care and the Omni checks need a rerun on the merged tree. It also hasn't had CI, because the base isn't main. Two of the config changes don't seem to do anything on this base, the 9B rungs above 2176 and the Thinker mixing key, since #916's chunk budgets came after those numbers. The AuT changes reach Qwen3-ASR as well, so I'd like its parity test and a WER pass at this head. The model work itself (patch-embed GEMM, sync-free talker mask and AuT trim, one embed call for decode rows) looks good to me.
mstar/model/qwen3_omni/submodules.py:615— Heads up for the merge with main. #1110 builds a media row as the text before, the attachment, then the text after (_spliced_row), so [1:-1] only lines up when there's no text around the image. On a spliced row the media starts at len(before) + 1, so something like start = len(before) + 1 and full_deepstack[start:start + vision_len] = deepstack_inp keeps it host-known and works for both. Main's audio and vision blocks still do mm_mask[[0, -1]] = 0 too, so the scalar writes need to move over.mstar/model/components/aut_encoder.py:260— Qwen3-ASR runs this encoder too since #1072, and the PR lists its parity test and transcripts as not run at this head. Could you run test_qwen3_asr_parity.py and a LibriSpeech pass on Qwen3-ASR before this goes in?mstar/model/components/aut_encoder.py:305— mstar.utils.h2d.to_device_nonblocking already does the pinned copy, and it asks DeviceCapability instead of x.is_cuda. On XPU this branch ends up doing a pageable non_blocking copy, which syncs.configs/qwen3_5_9b.yaml:8— With chunked prefill in the base, a prefill_text step is cut at MAX_BATCH_TOKENS (2048) and a mixed step tops out at 2176, and these configs don't raise max_batch_tokens. So as far as I can tell 3072 to 8192 never replay. The +1.9/+4.5% at c=64 is from the pre-split tree, before #916, and the stack-top TP2 A/B at +0.9% within spread fits that. Can you show a step landing on one of them? If not I'd keep 2176 and drop the rest, since they cost captures and startup on every 9B boot.configs/qwen3omni_2gpu_large_kv.yaml:8— Same question. The Thinker's budget is 1024, so a burst of prompts gets cut into steps of about 1024 plus decode rows, and the 3.5 s to 0.45 s TTFT p99 is from before #916. Does anything reach 4096 to 8192 now?configs/qwen3omni_2gpu.yaml:26— I think this is already the default. speculation_mixes turns on for any walk whose combined walk has a budget, and THINKER_MIXED gets 1024 from get_chunking_policies. So this pins the default rather than turning mixing on, and the +12/+15% mixed-step row predates #916 making it the default. I'd drop it, or say in the comment that it pins the default.mstar/model/qwen3_omni/components/thinker.py:251— The stream starts from input_embeds, which under capture is the static buffer, so the fused norms write into it and the _stage docstring had to change to allow that. forward_residual's own docstring and run_layers start from a copy. You already clone once for layer_0_embed, so could the stream start from the clone and layer_0_embed take input_embeds instead? Same copy count, and static inputs stay read-only, assuming the runner is fine with an output that aliases a static input. With mask_padding off, which these configs use, padding rows also go through the router, so whatever is left in them now carries over between replays.docs/models.rst:435— The MMMU c32 4457 vs ~3100 is from the TP2 arm whose TP-async setting wasn't recorded. Your TP-async-on A/B has DP2 16.5% behind on MMMU at c=16 and within spread at c=32, so I'd name the setting or drop the MMMU part.mstar/model/qwen3_5/submodules.py:115— Someone who still sets MSTAR_QWEN35_PREFILL_TOKEN_BUCKETS now silently gets the default ladder. A one-line warning at load if it's set would help.
The shared part of the perf stack that #970-#972 build on: less host overhead between steps in the API server and worker (lazy host copies of a step's outputs, a stop check that copies only what it reads, a message loop that survives a transport error), a startup check for preprocess_workers and TokenStop.withhold, and fewer, fused kernels in the shared engine resources (GDN prefill, ragged attention with strided q/k/v, fused MoE tuned tiles under trace, fused_sample for small vocabularies, recurrent slot zeroing). Review fixes: lazy host copies land every routed name with one sync, the MoE launch drops num_warps/num_stages only when they are symbolic, the message loop logs one traceback and backs off to 1 s, GDN normalizes q and k in one compiled call, and serving.rst says what a shared seed means and that preprocess_workers must be a positive integer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main carries #969's squash (ce5f431); the files #969's final review changed take main's version, as this branch made no changes of its own there. In the Qwen3-Omni Thinker, #1110 splices an attachment's row between the text around it, so a vision row's deepstack goes to the host-known slice after the text before and the BOS, not [1:-1]; the media masks keep this branch's scalar sentinel writes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shared helper pins and copies without a host stall, in place of a hand-rolled pin_memory + non_blocking copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion default
Qwen3.5-9B's 2048-token step budget caps prefill and mixed steps, so
its ladder stops at 2176; the Thinker's is 1024, so
qwen3omni_2gpu_large_kv.yaml goes back to Omni's default ladder.
speculation_mixes_walks {Thinker: true} restated the default (the
Thinker's mixed walk has a token budget). The docs drop the claims
measured on those rungs, and the DP2 note keeps to video, with the
TP-async-on numbers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fused add + norms update the stream in place, so started from input_embeds they wrote into a captured graph's static input. The copy moves from layer_0_embed to the stream (same count), and _stage's docstring goes back to main's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It is no longer read; the warning names model_kwargs.prefill_token_buckets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@merceod thanks - all done, and main is merged in (1c6ac24), so CI runs against main now.
Omni rerun on the merged tree (H100, one boot per arm, vs main): greedy image/video/audio outputs that differ all split on a near-tie in the HF reference (logit gap 0-0.375), and this branch matches HF on more tokens than main (488 vs 431). Audio WER 2.1% vs 1.9% (one word). a2t tok/s and TTFT within 1.5% at c=1 and c=8. Note |
merceod
left a comment
There was a problem hiding this comment.
All addressed, thanks. The spliced-row deepstack start with its test, the AuT index through to_device_nonblocking, the ladders and the mixing key trimmed along with the docs, the stream starting from a copy with the test that input_embeds stays as staged, the warning for the old env var, and the ASR parity and WER pass. CI is green on main. This one goes first, then #971, #972 and #1140 as their bases retarget.
Conflicts in the Thinker's forward and in ThinkerSubmodule, where main's #970/#1140 landed beside the decode walk: - thinker.py: main's fused add + norm and its residual-stream copy stay; keep_states still drops layer_0_embed and the layer-N add when nothing reads them. - prepare_inputs: both made a decode row its token alone. Kept main's row and its later start_pos read; the loop-back row stays in front of it. - preprocess: both embedded a mixed step's decode rows in one call. Kept main's code; the tokens now come from _decode_tokens, which also reads loop-back rows off the sampler's slot master. Decode steps keep their own path, so the captured tables stay in the graph. - decode captures: one config per decode walk, with main's ids input. The text-decode runtime test moves to the Rust runtime (#1130 removed the Python one).
mstar-project#845's branch with upstream/main merged in. No conflict here: main's changes to this PR's files (mstar-project#970 in the data worker, mstar-project#1126 and mstar-project#1159 in the API server, mstar-project#1166 in the Rust frontend, mstar-project#1131 in the conductor and the runtime wrapper) are all away from the version plumbing. This PR's own change over mstar-project#845 is unchanged by the merge.
Gaurav-Shah05
left a comment
There was a problem hiding this comment.
On CPU the AuT encoder matches the parent exactly for packed batches and clips of a second or more, and the fused add and RMSNorm chain matches the unfused one to 1.4e-6 in fp32, deepstack included.
Overall lgtm, and I have only a comment on the prefill ladder.
| # ``self.PREFILL_TOKEN_BUCKETS``, or the override is silently ignored. | ||
| PREFILL_TOKEN_BUCKETS: list[int] | ||
|
|
||
| def __init__(self, prefill_token_buckets: Sequence[int] | None = None): |
There was a problem hiding this comment.
Only Qwen3.5, the Qwen3-Omni Thinker and GLM-5.2 pass this through, so model_kwargs.prefill_token_buckets on Orpheus, Whisper, Qwen3-ASR or the five other AR models that read self.PREFILL_TOKEN_BUCKETS is dropped by the model's **kwargs with no error, a case this class's comment doesn't cover.
The engine should apply a per-node, per-walk ladder from the serving config to the packed capture config, as ChunkingManagement.set_budgets applies max_batch_tokens, refuse a walk with no packed capture, and drop the rungs past the first one that holds the walk's budget, so no model plumbs the key and no config lists rungs that never replay.
| # kernel at ~17 ms for a 60 s video's patches, ~1 ms as a matmul. | ||
| weight = self.proj.weight | ||
| x = x.reshape(-1, weight[0].numel()).to(weight.dtype) | ||
| return nn.functional.linear(x, weight.view(self.embed_dim, -1), self.proj.bias) |
There was a problem hiding this comment.
forward now reads proj.weight directly, so module_lora_spec still binds patch_embed.proj as a patch conv, and an adapter's patch_embed.proj factors on the Qwen3.5 or Omni vision tower would validate and add nothing.
VisionPatchEmbed should declare proj in lora_unservable as SiglipEmbeddings in command_a_plus/components/vision.py does, with an ANNOTATED entry in test_lora_targets.py like bagel_converted_patch_embed.
Stacked on #969. Two encoders were the gaps here. Qwen3.5-9B trailed vLLM by up to 14% on video: its vision tower's block loop ran eagerly, and cuDNN ran the patch embed as a slow implicit-GEMM conv. Qwen3-Omni trailed SGLang-Omni on audio→text, partly because its audio encoder read its chunk layout off the device, so every step paid host syncs. Compiling the block loop is now #859's. This PR makes the patch embed one GEMM and takes the host syncs out of the AuT encoder, which Qwen3-ASR and Qwen3-Omni share since main's #1072. On the stack before the split, with both tower changes, Qwen3.5-9B was within 1.5% of vLLM on video and MMMU on one H200. On Qwen3-Omni, this PR against its parent cuts first-token latency on single images and 60 s videos by 6–9%.
The patch embed's conv has its kernel as its stride, so it is one GEMM over flattened patches (about 17 ms → 1 ms for a 60 s video's patches on Qwen3.5). It lives in
qwen3_vit.VisionPatchEmbed, which main's #1072 shares between Qwen3.5 and Qwen3-Omni, so both towers take it. The AuT encoder takes a list of clips instead of a padded stack, pads only the last conv launch, and builds the trim indices andcu_seqlenson the host, so it has no host sync. Its fp16 residual is clamped. The Talker's masks use device ops only, Thinker and Talker fuse the residual add into RMSNorm, and a Thinker step embeds its decode rows in one call. A deployment now sets the captured prefill ladder in its config (model_kwargs.prefill_token_buckets), which replacesMSTAR_QWEN35_PREFILL_TOKEN_BUCKETS. The 9B configs keep main's 2176 bucket, for a mixed step's decode rows beside four prompts, and capture 3072–7168 in place of main's 4352 and 8704 (with those two as well, the ladder would use Dynamo's whole recompile limit); On 9B TP2, 2176 captures for prefill (batch 1–16) and mixed steps (batch 4–128). The mixed-step buckets (MIXED_TOKEN_BUCKETS) now follow each deployment's ladder: its buckets up to 2176, then its powers of two. A test checks every shipped config that setsprefill_token_buckets,qwen3omni_2gpu_large_kv.yamlincluded.qwen3omni_2gpu.yaml, meant for 80 GB GPUs, keeps Omni's default 128–2048 text ladder, andqwen3omni_2gpu_large_kv.yamlruns it to 8192 on H200-class GPUs. We also addqwen3_5_9b_dp2.yaml(one replica per GPU), H200 MoE tiles for Omni's two expert shapes, and a Thinker prefill row that rides the speculated decode step (speculation_mixes_walks: {Thinker: true}). Two outputs change by design. Vision outputs of Qwen3.5 and Qwen3-Omni aren't bit-identical to main, because the GEMM rounds bf16 differently, so greedy output on long videos and a few MMMU images can diverge. MMMU-200 accuracy is unchanged on both; Video-MME accuracy was not run. And a sub-second Omni or ASR clip encoded alone differs from HF, because every clip's last chunk is padded to keep the encoder batch-invariant.Unless a row says otherwise: H200 on Eras, torch 2.12.1+cu132, ABBA with 3 boots per arm and a fresh Inductor cache per timed boot. Rows marked "pre-split" ran on the stack before it was split (base
29bf80124, #860's head then), where this PR also carried the compiled block loop. Rows marked "earlier stack top" ran #972's head0d242a90aagainst mainbb0e0a8d7, before many of main's changes since (among them #963, #919, #939, #1072, #1075, #858 and #920). The whole-stack Qwen3-Omni and Qwen3.5-9B TP2 numbers are in #972.qwen3omni_2gpu.yaml3e49609ee(this PR's Omni and vision changes as at this head; main's Omni code has moved since) vs its parent #969e2a8352b9; the whole PR, not the GEMM aloneqwen3_5_9b_dp2.yamlat #972's earlier heada2b96f6e6(this PR's config, before it took the 2176 bucket) vs main3e98682a8with its 9B config andnode_groupsranks [0, 1] (main has no DP2 config), ABBA with 2 boots per armdocs/models.rstnumbers (1520 / 4457 tok/s, from load-only routing) are within 2.4% / 0.1% of these. We did not run an arm with the old routingb27e0609e(the same code as #972's head) vs maineceea1bee, ABBA with 4 boots per arm, with a profile passqwen3omni_2gpu.yamlcarried the ladder thatqwen3omni_2gpu_large_kv.yamlhas nowqwen3omni_2gpu.yamlcaptures7cb636390vs mainad0a21ecaqwen3omni_2gpu_large_kv.yamlb27e0609evs maineceea1bee, and the stack with Omni's default ladder; one boot eachqwen3omni_2gpu.yaml): Whisper-large-v3 WER over 24 sentencesad0a21eca, #859576d05f4e, #969e2a8352b9and this PR at3e49609ee, 3 cold boots per arm, one seed shared by all 24 sentencesrun_layers) alone, the MoE tile files or speculation mixing changes none of it. These boots sent one seed (7) for all 24 sentences, and the paragraph below the table shows that the shared seed is what separates the arms: with a distinct seed per sentence, this PR is at main's levelad0a21ecaand the stack's heads then (this PR at3e49609ee), each after the same long timed history of audio, text and speech requests unless notedprefix_cache: false, this PR and #972 transcribe it (2/2). Cold boots transcribe it whichever order the clips run in, so the reading that its KV stays frozen from a batched first serving was not confirmed. It is not from the MoE tile files, speculation mixing, or 98691fc, ad54171 or 15f7a20 (each reverted alone); the paragraph below the table has the verdict. Main answers the other 4 of the 5 question clips too; without the five, audio→text WER is equal (#972 1.18–1.29%, main 1.22–1.26%). We could not read the margin: Omni chat dropslogprobsandtop_logprobswithout an errorruff check .; CPU Core; Rust tests--rust7c9b2f30bvs maineceea1bee; Rust at0fac73afbvs mainfdfa1239aruffclean. Core: no failures beyond main's (main's 35 come from the box's environment). Rust (test/rust/): 664 passedBoth Qwen3-Omni items above were followed up, and neither is a regression. The read-aloud drift came from the probe sending seed 7 on all 24 sentences. Since #969,
fused_sampledraws per-row Philox from the request's seed, so all 24 sentences replayed one random stream on every boot. With a distinct seed per sentence (4 cold boots per arm on this PR's earlier headecba1a4dd), WER is 1.19% withfused_sampleand 1.43% without it, and wrong first words are 0.0 vs 1.0 per boot. Main measured 1.38% in the same round. With no seed, WER is 2.19% vs 2.14%. Only the shared seed separates the arms (4.28% vs 1.47%).fused_sample's draws on 69 real Talker and 1019 real code-predictor rows match FlashInfer's at the FlashInfer-vs-FlashInfer noise floor, and the repetition penalty matches to 3.6e-7. Unseeded requests get a per-request seed, so serving traffic doesn't share a stream. The question clip is a near-tie between transcribing and answering: the answer responds to that clip's own question, and main also answered it on 1 of 3 boots of an earlier head.Not yet run on GPU at this head (
7c9b2f30b): Qwen3-ASR on the AuT encoder changes (transcripts against base, c=16 throughput, long-form batches,test_qwen3_asr_parity.py); Qwen3-Omni audio→text with this PR's encoder changes alone (audio_embedsagainst base for 5 s, 60 s and 10 min clips, encoder phase time); 8 concurrent 60 s clips throughaudio_attn, and fp16 on a 10 min clip; peak memory for 1 × 600 s + 7 × 2 s clips under the last-launch padding rule;qwen3omni_2gpu.yamlon an 80 GB GPU (whether every capture fits there is not measured); Talker codes and WER against base onDecoderLayer; Thinker padded decode and prefill captures at c=3 / 5 / 7 / 24; Video-MME accuracy.🤖 Generated with Claude Code