Skip to content

feat(xla): add Qwen3-VL DeepStack vision path - #921

Merged
inureyes merged 11 commits into
mainfrom
feature/issue-868-qwen3-vl-xla
Oct 15, 2026
Merged

inureyes merged 11 commits into
mainfrom
feature/issue-868-qwen3-vl-xla

Conversation

@inureyes

@inureyes inureyes commented Jul 24, 2026 •

Copy link
Copy Markdown
Member

Summary

Implements the OpenXLA/IREE vision encoder, DeepStack intermediate layer feature extraction/injection, and multi-axis M-RoPE language prefill path for qwen3_vl (#868).

Key Changes

  • StableHLO Vision Encoder & DeepStack Mergers (src/lib/mlxcel-xla/src/vision.rs, src/lib/mlxcel-xla/src/qwen3_vl_runtime.rs):
    • Compiles Qwen3VLVisionConfig with bilinear position-embedding interpolation, 2D rotary embeddings across spatial blocks, main patch merger (use_postshuffle_norm = false), and deepstack_visual_indexes intermediate mergers (use_postshuffle_norm = true).
    • Executes vision artifacts per image (execute_vision_Sync) and returns both main visual features [N_vis, D] and per-branch DeepStack visual residuals [N_vis, D].
  • DeepStack Language Prefill Lowering & Runtime (src/lib/mlxcel-xla/src/lowering_prefill.rs, src/lib/mlxcel-xla/src/iree.rs, src/lib/mlxcel-xla/src/batch.rs):
    • Adds emit_prefill_with_deepstack and PreparedDeepStackPrefill with dense [B, S, D] per-branch visual residual scatter aligned to <|image_pad|> token positions.
    • Adds XlaBucketSet::submit_deepstack_prepared and wires XlaServingEngine and Qwen3VlIreeHostPreprocessor for both CLI and HTTP server multimodal requests.
  • Stage-by-Stage DeepStack Oracle (src/lib/mlxcel-xla/src/diagnostics/qwen3_vl_deepstack.rs, examples/xla_qwen3_vl_deepstack_check.rs):
    • Compares MLX vs XLA across vision.patch_embedding, vision.position_embedding, vision.positioned_embedding, vision.block_0..23, vision.main_merger, vision.deepstack_merger_0..2, language prefill embedding, layer_0..27, final_norm, logits, same-input control, and negative controls (dropped branch, zeroed branch, shifted visual span).

Qualification & Testing (gfx1151 ROCm, Qwen3-VL-2B-Instruct-4bit)

  • Stage-by-Stage Oracle (xla_qwen3_vl_deepstack_check --device hip):
    • tests/fixtures/test_image.png and qwen25_test_1.png:
      • 0 vision stage failures across all 24 transformer blocks (max_abs <= 0.00416), vision.main_merger (max_abs = 0.000186), and vision.deepstack_merger_0..2 (max_abs <= 0.0000839).
      • same_input_control_failures: [] (max_abs = 0.0).
      • Negative controls (negative_dropped_branch_detected: true, negative_zeroed_branch_detected: true).
      • greedy_top1_exact: true (final_top1 == eager_top1 == 1748 on test_image.png, 16 on qwen25_test_1.png), passed: true.
  • 16-Token Greedy Generation Parity (tests/fixtures/test_image.png):
    • Prompt: "First state the color of this image in one word, then count from 1 to 10 separated by commas." (--max-tokens 16 --temp 0)
    • MLXCEL_BACKEND=mlx CLI: "Orange, 1, 2, 3, 4, 5"
    • MLXCEL_BACKEND=xla CLI (--device hip): "Orange, 1, 2, 3, 4, 5"
    • MLXCEL_BACKEND=xla HTTP Server (/v1/chat/completions, --device hip): "Orange, 1, 2, 3, 4, 5"
  • Unit Tests & Clippy:
    • cargo test -p mlxcel-xla --lib --features diagnostics -- qwen3_vl deepstack
    • cargo test --lib --features rocm,xla-iree -- qwen3_vl (42 passed)
    • cargo test --lib --features rocm,xla-iree -- host_preprocessor (30 passed)
    • cargo clippy --bin mlxcel --example xla_qwen3_vl_deepstack_check --features xla-diagnostics-rocm -- -D warnings

Closes #868

@inureyes inureyes added status:review Under review type:enhancement New features, capabilities, or significant additions priority:low Low priority area:models Model architectures, weights, loading, metadata area:inference Generation, sampling, decoding (incl. speculative, DRY) labels Jul 24, 2026
@inureyes
inureyes force-pushed the feature/issue-868-qwen3-vl-xla branch from 396950c to e0c9317 Compare July 27, 2026 00:19
@inureyes

Copy link
Copy Markdown
Member Author

Rebased onto main@7fe1412d (including #923 and #934) and force-pushed as e0c9317f.

Post-rebase validation:

  • cargo fmt --all -- --check
  • git diff --check
  • cargo test -p mlxcel-xla qwen3_vl: 9 passed
  • cargo check -p mlxcel-xla --features iree: passed with existing warnings

The runtime now uses the explicit new_legacy_unqualified auxiliary-artifact path. A post-#923 CUDA/Q4 numeric rerun is still required because the prior 396950c9 oracle predates the deterministic CUDA QMM kernel update. The current host cannot perform that rerun: nvidia-smi cannot communicate with the driver and /dev/nvidia* is absent. No fresh numeric pass is claimed; this PR remains draft and #868 remains blocked by #932.

@inureyes
inureyes force-pushed the feature/issue-868-qwen3-vl-xla branch from e0c9317 to a4b641e Compare July 27, 2026 03:51
@inureyes

Copy link
Copy Markdown
Member Author

Rebased this draft onto main@ec6fbb6d and force-with-lease updated the branch at
a4b641e6.

The only manual resolution was a formatting-only conflict in the DeepStack C
ABI source assertion; the latest main form was retained. The merged shared
numeric foundation remains authoritative.

Validation after rebase:

  • cargo test -p mlxcel-xla --features diagnostics qwen3 --lib: 16 passed
  • cargo test -p mlxcel-xla --features diagnostics deepstack --lib: 14 passed
  • cargo fmt --all --check
  • git diff --check

The PR remains draft. The post-#923 production-reference CUDA Q4 probe required
by #932 is unavailable on this host, so no intermediate/token-exact claim is
upgraded and no tolerance is changed.

@inureyes

Copy link
Copy Markdown
Member Author

Post-#923 current-head CUDA actual at a4b641e6

Run on the GB10 qualification host (driver 580.159.03, CUDA 13.0, IREE 3.12.0rc20260721,
Rust 1.93.1, MLX pin b7c3dd6d27f4). Working tree clean at a4b641e6.

Builds for this host must pin MLX_CUDA_ARCHITECTURES=121a. src/lib/mlxcel-core/build.rs
falls back to the literal "90a" when detect_cuda_arch() cannot reach nvidia-smi, and
the resulting binary cannot launch MLX kernels on sm_121. That is a build-environment
failure and is recorded separately from the numeric result below.

Fast pre-gate

  • cargo test --locked -p mlxcel-xla qwen3_vl --lib: 9 passed
  • cargo test --locked -p mlxcel-xla deepstack --lib: 14 passed

Actual

--model qwen3-vl-2b-4bit --image tests/fixtures/test_image.png --device cuda --context-capacity 256, unchanged atol=0.08, rtol=0.08, no tolerance override.
Report at /var/tmp/mlxcel-cuda-qual/pr921/report.json. Grid [1,16,16], vision shape
[64,2048], 3 DeepStack branches.

Result is passed=false with 39 failing comparisons.

Both negative controls behaved correctly: negative_dropped_branch_detected=true and
negative_zeroed_branch_detected=true.

Clean through the early graph: patch_embedding (max_abs 0.027463), position_embedding,
positioned_embedding, block_0 and block_1 all report 0 failures.

First failure is vision.block_2 with 2 values, max_abs 0.22236824. From there the
divergence is monotonic:

stage max_abs failures
vision.block_2 0.22236824 2
vision.block_9 0.31624985 31
vision.block_10 8.69421 169
vision.block_23 1078.1646 89017

The mergers all fail downstream of that: main_merger 6953, deepstack_merger_2.fc1 25624,
deepstack_merger_1 78. deepstack_merger_0 output still passes with 0 failures.

The substage evidence points at accumulation, not at a per-operator defect

Within block 2, the individual operators agree, while the normalizations fail only on
accumulated input:

stage max_abs failures
vision.block_2.input 0.221995 0
vision.block_2.attention 0.0225568 0
vision.block_2.mlp 0.0696449 0
vision.block_2.norm1 0.27341986 1
vision.block_2.norm2 0.4035015 283

and the same-input controls pass outright:

stage max_abs failures
vision.block_2.norm1.same_input 0.0849018 0
vision.block_2.norm2.same_input 0.0423183 0
vision.block_0.output.same_input.fused_q 0.111899 0
vision.block_1.output.same_input.host_dequant 0.0266669 0

Given identical inputs, the graph reproduces the reference within tolerance at every probed
operator, including the fused Q4 path. The failures appear only once each block consumes the
previous block's drifted output, and the normalization is where a small input difference is
rescaled across the row and becomes visible.

This is the same amplifier identified on Qwen2.5-VL in #920, where replaying the reference's
own normalization output through the projection reproduced the candidate exactly. In both
families the failing elements are small-|expected| values with absolute differences near
0.2; the very large max-relative figures come from near-zero elements that are not the
failures themselves.

DeepStack language-layer injection, final-position logits, greedy token exactness, mixed
batching, cancellation and slot reuse were not reached, since the first strict intermediate
failure stands. This PR stays draft and the capability stays fail-closed.

@inureyes

Copy link
Copy Markdown
Member Author

TF32 reference policy checked here: immaterial, so the numbers above stand

I filed on #932 that MLX defaults MLX_ENABLE_TF32=1 and that
src/lib/mlxcel-xla/README.md declares that mode is not valid F32 reference evidence. It
decided the Molmo2 gate in #916, so it had to be re-checked here.

Re-run at a4b641e6 with MLX_ENABLE_TF32=0, everything else identical
(--device cuda, --context-capacity 256, unchanged atol=0.08, rtol=0.08,
MLX_CUDA_ARCHITECTURES=121). Report at /var/tmp/mlxcel-cuda-qual/pr921/report-notf32.json.
Unchanged: passed=false, the same 39 failing comparisons, identical values.

stage TF32 default MLX_ENABLE_TF32=0
vision.block_0 0.236899, 0 fail 0.236899, 0 fail
vision.block_1 0.221995, 0 fail 0.221995, 0 fail
vision.block_2 0.222368, 2 fail 0.222368, 2 fail
vision.block_10 8.69421, 169 fail 8.69421, 169 fail
vision.block_23 1078.16, 89017 fail 1078.16, 89017 fail
vision.main_merger 2.58248, 6953 fail 2.58248, 6953 fail

Consistent with the mechanism: the flag reaches genuine f32 contractions only, and this
tower runs at the checkpoint's narrow dtype. The same check was likewise unchanged on the
Qwen2.5-VL tower in #920 and the Gemma3 BF16 tower in #917, while both Molmo gates moved.

So the divergence recorded here is a property of the emitted graph, not of the reference
policy, and the blocker is unchanged. The .same_input controls passing while the
accumulated-input comparisons fail remains the most useful signal on this PR.

Add a resident IREE Qwen3-VL vision emitter and runtime with learned-position interpolation, packed nonwindowed attention, the main merger, and every ordered DeepStack branch from the immutable checkpoint.

Derive the dense nested text contract, preserve interleaved M-RoPE, route CLI and server image requests through owned DeepStack prefill payloads, and fail closed when an ordinary prefill would omit side features.

Validate emitter/runtime fixtures, host and server routing boundaries, the XLA-IREE build and clippy paths, and one pinned CUDA/IREE image generation that returned `Orange`.

Refs #868
Read the checkpoint pixel bounds and PIL bicubic resampler instead of using the historical Qwen2 defaults, and include the effective policy in the compiled artifact identity so incompatible processor artifacts cannot be reused.

Qualify the 512-patch bucket required by two canonical 256x256 images, retain per-image packed attention isolation, cap the corresponding DeepStack payload at 128 merged positions, and cover processor, overflow, ABI, and context-capacity boundaries.

Refs #868
Compare the eager MLX and resident IREE main vision merger and every ordered DeepStack branch from one qualified processor payload.

Reuse the production DeepStack prefill loop to expose post-injection hidden states and final logits without changing serving outputs, and reject dropped or zeroed actual-checkpoint branches.

Refs #868
Use the checkpoint's gelu_pytorch_tanh contract in the eager vision blocks while preserving exact GELU in the main and DeepStack patch mergers. Add eager numerical and emitted-graph regression coverage for the activation split.
Block-level comparisons can identify LayerNorm as the first failing stage even when its input already contains tolerated quantized-matmul drift.

Re-run the eager norm1 and norm2 operators on the exact IREE block input and post-attention residual, then report dedicated same-input comparisons. Add deterministic eager and StableHLO contract tests for the centered-variance F32 LayerNorm formula.

Validated with rustfmt, focused eager and emitter tests, and the xla-diagnostics example check.

Refs #868
The same-input LayerNorm probe proves block 2 normalization is within tolerance when both runtimes consume identical activations, so the remaining divergence is accumulated upstream rather than a LayerNorm precision defect.

Capture block 0 input, norm1, attention, residual, norm2, MLP, and output on both eager MLX and IREE diagnostic paths. The oracle now reports these stages before block 2 without changing production precision, tolerances, or checkpoint execution.

Validated with the focused Qwen3-VL emitter tests, diagnostics library checks, the diagnostics-enabled oracle example check, and Clippy for mlxcel-xla.

Refs #868
Qwen3-VL vision parity first crosses tolerance at block 2 after block 0 and block 1 remain within tolerance, leaving fused quantized projections and IREE's host-dequantized dense F32 weights as the primary unresolved arithmetic boundary.

Add diagnostics-only same-input block 0 and block 1 controls that run the production fused-QMM path beside a dense F32 path using the exact affine host widening shared with the IREE checkpoint loader. Record both outputs in the actual-checkpoint oracle without changing production execution or tolerance policy.

Validated with cargo fmt, the focused dense-F32 control unit test, cargo check for the xla-diagnostics example on the GB10/IREE CUDA setup, and cargo clippy for the same example.

Refs #868
Keep the rebased Qwen3-VL runtime on the explicit legacy artifact path until its Q4 numeric oracle is rerun against the current CUDA kernel.\n\nRefs #868
@inureyes
inureyes force-pushed the feature/issue-868-qwen3-vl-xla branch from a4b641e to 7085564 Compare October 15, 2026 17:39
@inureyes inureyes added the status:done Completed label Oct 15, 2026
@inureyes
inureyes marked this pull request as ready for review October 15, 2026 17:40
@inureyes
inureyes force-pushed the feature/issue-868-qwen3-vl-xla branch from 7085564 to 8463378 Compare October 15, 2026 18:09
@inureyes
inureyes merged commit 2a1fb24 into main Oct 15, 2026
27 checks passed
@inureyes
inureyes deleted the feature/issue-868-qwen3-vl-xla branch October 15, 2026 19:58
inureyes added a commit that referenced this pull request Oct 15, 2026
The Qwen3-VL DeepStack test added in #921 indexes `packed_attention_bias[48 * 64 + 0]`, which `clippy::identity_op` rejects under `-D warnings`. Nightly verify run 38813575943 on main a426055 failed its `make verify-clippy` step on this line (`src/lib/mlxcel-xla/src/emitter/qwen3_vl.rs:1298`), while its test and video steps passed. The index is now `48 * 64`, the same element (row 48, column 0).

Validation: `cargo clippy -p mlxcel-xla --all-targets -- -D warnings` and `cargo fmt --all -- --check` are clean, and the 7 `emitter::qwen3_vl` tests pass.

Refs #921
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:inference Generation, sampling, decoding (incl. speculative, DRY) area:models Model architectures, weights, loading, metadata priority:low Low priority status:done Completed status:review Under review type:enhancement New features, capabilities, or significant additions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(xla): support Qwen3-VL and DeepStack vision features

1 participant