Repository navigation
fix(perf): fuse QKV and add text-only fast path for Qwen3-VL decode - #29
Merged
Merged
Conversation
Qwen3-VL and Qwen3-VL-MoE text-only decode went through the full VLM forward path: three separate `q_proj`/`k_proj`/`v_proj` matmuls per layer, MRoPE delta computation against per-sequence position state, visual position masks, and DeepStack visual-embedding fan-in — all of it dead weight when there is no image in the prompt. This change introduces two optimizations for both `qwen3_vl` and `qwen3_vl_moe`: 1. Fuse Q/K/V into a single `FusedQKVLinear` (`qkv_proj`). One concatenated matmul per layer replaces three, which is the dominant decode-time saving on Apple Silicon where the per-call MLX overhead matters more than raw FLOPs. The shared `FusedQKVLinear` already serves Llama3/Qwen2/Qwen3/Gemma/Mistral/Cohere2/StarCoder2/InternLM3; the docstring is updated to include the Qwen3-VL variants. 2. Add a `forward_text_only` fast path on `Attention`, `DecoderLayer`, and the top-level model. It is taken when `input_embeddings` is `None` and the model has no live MRoPE state, visual position mask, or DeepStack embeddings for the sequence. The fast path uses plain `fast_rope` keyed on the cache offset instead of computing 3D MRoPE position ids, and skips the visual-state propagation entirely. To keep the gate sound across speculative-decoding sequences, MRoPE and DeepStack state are explicitly cleared at the start of any fresh (`cache_offset == 0`, no input embeddings) sequence. Measured on M5 Max (decode tok/s, 4-bit): - Qwen3-VL-30B-A3B-Instruct-4bit: 56.00 -> 146.29 (2.61x; 38% -> 99% of mlx-lm parity) - Qwen3-VL-32B-Instruct-4bit: 18.79 -> 27.33 (1.45x; 66% -> 94% of mlx-lm parity) - Qwen3-VL-2B-Instruct-4bit: unchanged (already on the fast path the small model never left) Image-in-prompt and DeepStack flows are unchanged: the original `forward` path is still taken whenever any visual state is present, so VLM correctness on multimodal inputs is preserved.
inureyes
added a commit
that referenced
this pull request
May 18, 2026
This release focuses on Apple Silicon decode performance for VLM and linear-attention model families, plus a CLI fix for `mlxcel list`. Performance: - Gemma3n bf16 prefill path: gemma3n-e4b-bf16 decode 11.16 to 38.81 tok/s on M5 Max (#28). - Qwen3-VL text-only decode: QKV fusion + `forward_text_only` fast path. Qwen3-VL-30B-A3B-Instruct-4bit decode 56.00 to 146.29 tok/s (2.61x, 99% of mlx-lm parity); Qwen3-VL-32B-Instruct-4bit decode 18.79 to 27.33 tok/s (1.45x, 94% parity); image-in-prompt and DeepStack paths unchanged (#29). - GatedDeltaNet fast RMSNorm in Qwen3.5 / Qwen3-Next / KimiLinear: `mlx::fast::rms_norm` replaces the expanded square / mean / sqrt / divide graph for Q/K and gated output. qwen3.5-0.8b-4bit decode 425.16 to 535.43 tok/s on M5 Max (+25.9%, 96% parity); Qwen3.5 speculative-decoding verify pass updated alongside prefill / decode to preserve draft / verify dtype agreement (#30). Fixed: - Rewrite `mlxcel list` output to drive from the `ModelType` registry: fixes stale count, missing VLM family, missing ~30 model types, and removes a broken docs link (#27). Docs: - Refresh README performance snapshot and `docs/benchmarks.md` to match the latest M5 Max sweep.
sjswerdloff
added a commit
to sjswerdloff/mlxcel
that referenced
this pull request
Jul 10, 2026
…xisting on base, stride-gate + supervisor approved
sjswerdloff
added a commit
to sjswerdloff/mlxcel
that referenced
this pull request
Jul 10, 2026
…truction] msa_fetch/fp16_gathered (Clement seat closed: approve, no must-fixes; Xander's seat open at his wake) Env instruments demote to default seeds; store is single source of truth at every dispatch site. Runtime msa_core makes the G core axis boot-free for the morning A/B leg (task lablup#28); construction keys are boot-frozen with warn-on-divergence. Header echo: path=..; core=..; v=N. Both gathered cores carry first-dispatch witnesses. Latch-order property (review note a): init_and_watch's boot-time file load precedes first dispatch on the server path; TOML-less processes latch env-seeded defaults at first touch by design. Verification (sharded per lablup#29 convention, isolated target): decode_config 26, server::routes 77, m3 50, core cache 480, bins build. Full-suite SIGTRAP reproduced on BASE without this change — pre-existing, lablup#29 updated with the datum.
sjswerdloff
added a commit
to sjswerdloff/mlxcel
that referenced
this pull request
Jul 11, 2026
…ge (draft for Xander's seat) Drives the REAL engine write path (update_kvarn8, fresh v_bits=4 cache per harvest event) on the SAME real-tile population the boot-night screen licensed (RESULTS_kvarn4_realtile_2026-07-11), and pins every stored field — K side (5) and V side (4) plus 4 structure pins — BITWISE against the dual-approved math layer (kvarn_quantize@8 / kvarn_quantize_v4@gs32) on the identical event batch. Structural first act: assert_round_half_even_parity() is the gating test's first statement (Violet PM pin 1, Xander confirmation). Scope (coverage mapping, so the green is never over-read): - gates the STORAGE roundtrip ONLY; gather_qmm fused consumption stays §4.2 tolerance-gated - live-session bit-equality out of scope BY CONSTRUCTION: dumps sampled ≤8 spread tiles/event AND sinkhorn best-so-far is batch-global — the contract is engine-vs-reference on identical per-event batches - ALL comparisons bitwise; no dequant-level tolerance door opened (cancellation-pricing birth constraint honored by construction) Population pinned, not assumed: replicates the screen's load_pairs() exactly (sorted K sidecars, np.linspace truncation semantics — pinned by unit test — seq+1 V pairing on matching cache+offset); asserts exactly 512 events / 4096 tiles; loader fail-louds on role/shape/size/non-finite. Self-pins (in-suite, no bank needed) prove the harness can go red: - comparator catches a single flipped byte (and stays quiet on identical) - comparator refuses length mismatch (no zip-shortest false green) - linspace selection matches exact integer floor for the bank geometry - synthetic-event roundtrip: harness helpers reproduce the engine bitwise end-to-end (10222 wiring idiom re-proven THROUGH the helpers) Evidence: cache:: scope 483/483 green (479 prior + 4 self-pins), gating test ignored pending review. Full-suite SIGTRAP is the known upstream MLX clear_streams teardown double-free (lablup#29, stash-proven not ours). SEQUENCING: the ignored gating run does NOT execute until Xander's review clears it (standing commitment: his seat before it gates anything). Invocation, post-review: MLXCEL_KVARN_GOLDEN_DIR=$HOME/kvarn_harvest_20260710 \ cargo test -p mlxcel-core --release k8v4_golden_harness_real_tiles \ -- --ignored --nocapture Co-Authored-By: clement-7074f29f <clement-7074f29f@sjstargetedsolutions.co.nz>
sjswerdloff
added a commit
to sjswerdloff/mlxcel
that referenced
this pull request
Jul 11, 2026
The gate's record (2026-07-11, Violet's verdict 20:06): completions BIT-IDENTICAL to blocked across kernel schedules at 295K (1,500 greedy tokens, diff empty), sdpa +3.4–8.1% tok/s. Safe everywhere by construction — the masked SDPA core consumes fp16 compact windows regardless of source (fp16-gathered or kvarn-dequanted). Env instrument keeps switch semantics against the NEW default: MLXCEL_MSA_CORE=blocked selects the non-default; anything else (including parse garbage) means sdpa. The two default-pinning tests flipped WITH the default, deliberately, citing the gate — exactly the red a default flip owes. Evidence: decode_config 26/26, msa 29/29, C contracts 2/2 (their gathered reference now runs the sdpa core live — still 1e-3-matched, consistent with the gate's bit-identical finding), root lib 1,876 passed / 0 failed before the known lablup#29 teardown SIGTRAP. Co-Authored-By: clement-7074f29f <clement-7074f29f@sjstargetedsolutions.co.nz>
sjswerdloff
added a commit
to sjswerdloff/mlxcel
that referenced
this pull request
Jul 12, 2026
… wider / store-admission narrower), Violet server-premise refuted, lablup#29 mechanism found Co-Authored-By: violet-14057653 <violet@sjstargetedsolutions.co.nz> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
be-free-snowboy
pushed a commit
to freestylejs/snowbrain-mlxcel
that referenced
this pull request
Oct 27, 2026
…ablup#29) * fix(perf): fuse QKV and add text-only fast path for Qwen3-VL decode Qwen3-VL and Qwen3-VL-MoE text-only decode went through the full VLM forward path: three separate `q_proj`/`k_proj`/`v_proj` matmuls per layer, MRoPE delta computation against per-sequence position state, visual position masks, and DeepStack visual-embedding fan-in — all of it dead weight when there is no image in the prompt. This change introduces two optimizations for both `qwen3_vl` and `qwen3_vl_moe`: 1. Fuse Q/K/V into a single `FusedQKVLinear` (`qkv_proj`). One concatenated matmul per layer replaces three, which is the dominant decode-time saving on Apple Silicon where the per-call MLX overhead matters more than raw FLOPs. The shared `FusedQKVLinear` already serves Llama3/Qwen2/Qwen3/Gemma/Mistral/Cohere2/StarCoder2/InternLM3; the docstring is updated to include the Qwen3-VL variants. 2. Add a `forward_text_only` fast path on `Attention`, `DecoderLayer`, and the top-level model. It is taken when `input_embeddings` is `None` and the model has no live MRoPE state, visual position mask, or DeepStack embeddings for the sequence. The fast path uses plain `fast_rope` keyed on the cache offset instead of computing 3D MRoPE position ids, and skips the visual-state propagation entirely. To keep the gate sound across speculative-decoding sequences, MRoPE and DeepStack state are explicitly cleared at the start of any fresh (`cache_offset == 0`, no input embeddings) sequence. Measured on M5 Max (decode tok/s, 4-bit): - Qwen3-VL-30B-A3B-Instruct-4bit: 56.00 -> 146.29 (2.61x; 38% -> 99% of mlx-lm parity) - Qwen3-VL-32B-Instruct-4bit: 18.79 -> 27.33 (1.45x; 66% -> 94% of mlx-lm parity) - Qwen3-VL-2B-Instruct-4bit: unchanged (already on the fast path the small model never left) Image-in-prompt and DeepStack flows are unchanged: the original `forward` path is still taken whenever any visual state is present, so VLM correctness on multimodal inputs is preserved. * style: apply cargo fmt to layers.rs use ordering
be-free-snowboy
pushed a commit
to freestylejs/snowbrain-mlxcel
that referenced
this pull request
Oct 27, 2026
This release focuses on Apple Silicon decode performance for VLM and linear-attention model families, plus a CLI fix for `mlxcel list`. Performance: - Gemma3n bf16 prefill path: gemma3n-e4b-bf16 decode 11.16 to 38.81 tok/s on M5 Max (lablup#28). - Qwen3-VL text-only decode: QKV fusion + `forward_text_only` fast path. Qwen3-VL-30B-A3B-Instruct-4bit decode 56.00 to 146.29 tok/s (2.61x, 99% of mlx-lm parity); Qwen3-VL-32B-Instruct-4bit decode 18.79 to 27.33 tok/s (1.45x, 94% parity); image-in-prompt and DeepStack paths unchanged (lablup#29). - GatedDeltaNet fast RMSNorm in Qwen3.5 / Qwen3-Next / KimiLinear: `mlx::fast::rms_norm` replaces the expanded square / mean / sqrt / divide graph for Q/K and gated output. qwen3.5-0.8b-4bit decode 425.16 to 535.43 tok/s on M5 Max (+25.9%, 96% parity); Qwen3.5 speculative-decoding verify pass updated alongside prefill / decode to preserve draft / verify dtype agreement (lablup#30). Fixed: - Rewrite `mlxcel list` output to drive from the `ModelType` registry: fixes stale count, missing VLM family, missing ~30 model types, and removes a broken docs link (lablup#27). Docs: - Refresh README performance snapshot and `docs/benchmarks.md` to match the latest M5 Max sweep.
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.
Summary
Two related optimizations to
qwen3_vlandqwen3_vl_moe:q_proj/k_proj/v_projmatmuls per layer with a singleFusedQKVLinear(qkv_proj). The shared layer is already used by Llama3 / Qwen2/3 / Gemma / Mistral / Cohere2 / StarCoder2 / InternLM3; docstring updated to list Qwen3-VL and Qwen3-VL-MoE.forward_text_onlyroute onAttention,DecoderLayer, and the top-level model. Taken wheninput_embeddingsisNoneand the sequence has no live MRoPE state, visual position mask, or DeepStack embeddings — i.e. a normal text prompt with no image. Uses plainfast_ropekeyed on the cache offset instead of computing 3D MRoPE position ids, and skips visual-state propagation entirely. MRoPE / DeepStack state is explicitly cleared at the start of any fresh sequence (cache_offset == 0, no input embeddings) so the gate is sound across speculative-decoding turns.Performance (M5 Max, 4-bit, decode tok/s)
Changes
src/models/qwen3_vl.rs,src/models/qwen3_vl_moe.rs— switchAttentiontoFusedQKVLinear; addforward_text_onlyon Attention/DecoderLayer/Model; auto-clear MRoPE + DeepStack state at sequence start.src/lib/mlxcel-core/src/layers.rs— extendFusedQKVLinear"Used by" docstring; minoruse-ordering fix.Test plan
cargo check --features metal,accelerateQwen3-VL-30B-A3B-Instruct-4bittext-only decode reproduces ~146 tok/s.Qwen3-VL-32B-Instruct-4bittext-only decode reproduces ~27 tok/s.Qwen3-VL-2B-Instruct-4bit(or any Qwen3-VL with an actual image) still produces correct output — fast path must NOT engage when visual state is present.