Skip to content

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

Draft
inureyes wants to merge 10 commits into
mainfrom
feature/issue-868-qwen3-vl-xla
Draft

feat(xla): add Qwen3-VL DeepStack vision path#921
inureyes wants to merge 10 commits into
mainfrom
feature/issue-868-qwen3-vl-xla

Conversation

@inureyes

@inureyes inureyes commented Jul 24, 2026

Copy link
Copy Markdown
Member

Summary

Add dense Qwen3-VL image execution to the OpenXLA backend with a resident IREE vision graph that implements learned-position interpolation, packed nonwindowed attention, the main merger, and every configured DeepStack branch. Route CLI and continuous-batch server requests through compact per-layer DeepStack payloads, while failing closed if an ordinary prefill would omit those side features.

Honor the checkpoint processor contract by reading its smart-resize pixel bounds and PIL bicubic resampler, binding those values into the compiled artifact identity, and qualifying a 512-patch bucket for two canonical 256x256 images and 128 merged DeepStack positions.

Add diagnostics that drive eager MLX and IREE from the same production processor patch/grid payload. The ordered oracle compares patch projection, interpolated position embeddings, their sum, every vision block, the main merger and DeepStack merger internals, final merger outputs, hidden state after each target language layer's deepstack_process, and final-position logits. Dropped and zeroed actual-checkpoint DeepStack branches remain negative controls.

Align the eager Qwen3-VL transformer-block MLP with the checkpoint's gelu_pytorch_tanh contract. The IREE block path already used tanh GELU, while the main and DeepStack patch mergers correctly retain exact GELU.

Capture blocks 0 and 2 at seven ordered boundaries on both eager MLX and IREE: input, norm1, attention, post-attention residual, norm2, MLP, and output. Numeric failures are accumulated so every vision comparison runs, and --report is persisted before the oracle exits.

Add a same-input LayerNorm probe that re-runs eager block 2 norm1 and norm2 on the exact IREE block input and post-attention residual. This separates normalization arithmetic from upstream quantized-matmul drift without changing production arithmetic, comparison thresholds, or CSV files.

Add diagnostics-only same-input projection controls for blocks 0 and 1. Each control runs the production fused-QMM path beside a dense F32 path that uses the exact affine host widening shared with the IREE checkpoint loader, isolating projection accumulation order without changing production execution.

Related issues

Closes #868

Parent epic: #566

Type of change

  • feat — new user-visible feature
  • fix — bug fix
  • perf — performance improvement
  • refactor — internal restructuring without behavior change
  • chore — build, CI, dependencies, release infrastructure
  • docs — documentation only
  • test — tests only

Test plan

  • cargo fmt --all -- --check
  • cargo check --features xla-iree
  • cargo check --example xla_qwen3_vl_deepstack_check --features xla-diagnostics
  • cargo clippy -p mlxcel-xla --features iree --fix --allow-dirty --allow-staged
  • cargo clippy --features xla-iree --lib (existing warnings in unrelated XLA/audio code)
  • cargo clippy --example xla_qwen3_vl_deepstack_check --features xla-diagnostics (existing unrelated warnings)
  • cargo test -p mlxcel-xla qwen3_vl
  • focused DeepStack diagnostics tests
  • focused root --lib --features xla-iree processor/prefill/server tests
  • structural two-image validation
  • post-fix real CUDA/IREE generation with the pinned dense qwen3-vl-2b checkpoint (Orange, 1 token, exit 0; reported generation time 28.13s)
  • actual-checkpoint oracle at 49f9a4f9: patch projection, position embeddings, positioned embeddings, block 0, and block 1 pass; the first failure is vision.block_2 with only 2 failing values and max_abs=0.22236824
  • the tanh-GELU correction removed the former isolated main-merger semantic mismatch; remaining merger differences occur after accumulated upstream block drift
  • bounded actual-checkpoint oracle at b10c81d1 persisted /tmp/mlxcel-qwen3-vl-b10c81d1-report.json after comparing every vision stage
  • block 2 substage localization at b10c81d1: input passes; norm1 has 1 failure; attention passes; post-attention residual has 1 failure; norm2 has 283 failures; MLP passes; output has 2 failures
  • bounded actual-checkpoint oracle at 7a26a928 completed in 29.5 seconds and persisted /tmp/mlxcel-qwen3-vl-7a26a928-report.json
  • block 2 same-input LayerNorm localization at 7a26a928: norm1 passes with 0 failures and max_abs=0.084901; norm2 passes with 0 failures and max_abs=0.042318
  • focused dense-F32 projection-control unit test
  • bounded actual-checkpoint oracle at 396950c9 persisted /tmp/mlxcel-qwen3-vl-396950c9-cuda-report.json
  • block 0 exact-IREE-input control at 396950c9: fused QMM passes with max_abs=0.1118994; host-dequantized dense F32 passes and is closer with max_abs=0.0191097
  • block 1 exact-IREE-input control at 396950c9: fused QMM passes with max_abs=0.0400119; host-dequantized dense F32 passes and is closer with max_abs=0.0266669
  • ordinary block 2 at 396950c9 remains the first failure with 2 failing values and max_abs=0.22236824; both same-input block 2 LayerNorm probes pass
  • cargo test -p mlxcel-xla emitter::qwen3_vl::tests::diagnostic_bucket_appends (2 passed)
  • cargo test -p mlxcel-xla emitter::qwen3_vl::tests::layer_norm_emits_centered_variance_f32_contract
  • cargo test --lib vision::encoders::qwen3_vl::tests::block_layer_norm_matches_centered_variance_f32_contract
  • direct rustfmt for the five block 0 probe files
  • cargo check -p mlxcel-xla --features diagnostics
  • cargo clippy -p mlxcel-xla --features diagnostics --fix --allow-dirty --allow-staged (existing warnings only; unrelated automatic fixes were excluded)
  • cargo deny check (CI)
  • cargo clippy --all-targets -- -D warnings
  • cargo test --release

Notes for reviewers

The 396950c9 report proves the block 2 LayerNorm implementation is not the source of the tolerance failures. With ordinary accumulated inputs, norm1 has 1 failure at max_abs=0.27341986 and norm2 has 283 failures at max_abs=0.40350151; with the exact IREE inputs fed to eager MLX, both operators pass with zero failures. Attention and MLP also pass in the ordinary run, while the block output returns to only 2 failures. The evidence therefore points to upstream accumulated drift.

Static precision audit does not support an F16/BF16/F32 production fix. The Qwen3-VL emitter uses the default F32 builder and F32 inputs, weights, and contractions. Eager patch projection promotes F32 input and BF16 weights to F32, while the quantized blocks use MLX CUDA's F32 qmm_naive path for this input shape. The remaining arithmetic seam is operator layout: eager MLX dequantizes Q4 weights on the fly inside fused QMM, whereas IREE consumes host-dequantized dense F32 weights in an F32 dot. Their reduction and dequantization order can differ even though their exposed dtype is the same.

The exact-IREE-input block controls confirm that seam. For both block 0 and block 1, the IREE result is consistently closer to eager host-dequantized dense F32 than to eager fused QMM. This is a non-MoE dense architecture, but the local checkpoint configuration uses Q4 affine projections. The evidence identifies MLX fused Q4 QMM versus IREE host-dequantized dense F32 accumulation-order drift; it does not establish a production semantic bug.

Strict intermediate MLX tolerance remains blocked without either emulating MLX CUDA fused-QMM kernel ordering in the IREE path or explicitly changing the acceptance/reference policy. Neither change is made in this PR.

Position interpolation is the only confirmed dtype mismatch: eager MLX performs the interpolation in BF16 before adding it to the F32 patch projection, while IREE widens the table and interpolation weights to F32. That stage passes the actual oracle, so changing production interpolation precision would be speculative.

The pre-fix Orange result was invalid canonical evidence because it used the wrong resize grid and resampler. After the processor-contract fix, a bounded CUDA/IREE run with the pinned checkpoint and fixture completed successfully, produced Orange as one token, and exited 0.

The unrestricted targeted cargo test --features xla-iree ... command attempted to link unrelated integration binary pipeline_cli_real_models and failed in the platform IREE static runtime with __stack_chk_guard@@GLIBC_2.17; DSO missing from command line. The equivalent library-only focused tests pass. A diagnostics-feature test build also encounters an unrelated existing gemma3n_qmv test-only Builder import error outside the Qwen3-VL path; diagnostics library and oracle example checks pass.

Checklist

  • PR title uses a Conventional Commits prefix
  • One logical change per PR
  • Updated docs/ if user-facing behavior or supported models changed
  • Updated // Used by: ... comments on any shared function modified
  • No secrets, credentials, .env, or CSV files committed

@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 added 10 commits July 27, 2026 12:50
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 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.

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: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