Skip to content

Add runtime-indexed prefill pruning for Qwen3-VL exports - #813

Open
hanbitmyths wants to merge 4 commits into
mainfrom
sunghcho/qwen-vl-cuda-pr-20261106
Open

hanbitmyths wants to merge 4 commits into
mainfrom
sunghcho/qwen-vl-cuda-pr-20261106

Conversation

@hanbitmyths

@hanbitmyths hanbitmyths commented Nov 6, 2026 •

Copy link
Copy Markdown

Summary

  • Opt-in Qwen3-VL prefill pruning: gather the selected hidden row before the LM head while preserving full-prompt attention and KV updates.
  • Expose prune_prefill_prefix, runtime_last_token_indices and disable_deepstack; existing defaults and upstream DeepStack support are unchanged.
  • Reserve last_token_indices as INT32 [B,1,1], outside generic decoder mappings; reject unsupported task-level options.

Companions: GenAI #2889, Olive #2794, recipe #694.

Reported validation: 45 targeted tests, Ruff/format; 20 full/pruned FP16 comparisons and 2,240 written KV-prefix pairs agree; CUDA ABI/capture/replay checked.

Known limit: padded captured/eager outputs differ and batched captured prefill fails, including unpruned controls. Full GPU CI is incomplete; capture compatibility is not fully passing.

Performance: Qwen3-VL-2B

Combined package/runtime results, not isolated gains from this PR. Production uses decoder INT4/group32; optimized uses 66 INT8 + 131 INT4 projections, FP16 vision/embeddings/KV, pruned prefill and decoder capture.

A100-SXM4-80GB

Historical production: Oct 28 (b679913a2310); optimized: Nov 5 frozen clean gate.

Tokens Production TTFT ms Optimized TTFT ms Production decode tok/s Optimized decode tok/s Change
128 21.25 10.73 232.16 349.41 +50.50%
512 29.52 17.66 230.47 340.51 +47.75%
2048 62.12 47.78 219.57 325.15 +48.08%
4096 122.50 100.42 212.16 304.94 +43.73%
7680 241.71 206.10 202.21 288.68 +42.76%

H200

Baseline: official qwen3-vl-2b-instruct-cuda-gpu:2 on published ORT 1.31.1 / GenAI 0.19.0, CUDA 13. Optimized: retained Nov 8 FPA1 results, not rerun.

Workload Baseline TTFT ms Optimized TTFT ms Baseline decode tok/s Optimized decode tok/s Change (unpaired)
Text 128 9.06 8.04 220.0 514.9 +134.08%
Text 512 12.29 11.08 216.0 516.7 +139.25%
Text 2048 26.07 25.98 211.7 504.8 +138.42%
Text 4096 52.39 51.55 205.3 481.0 +134.26%
Text 7680 104.96 103.56 201.6 467.3 +131.85%
Image + text 30.55 40.90 216.9 514.4 +137.16%

Controls: batch 1, context 8192, FP16 KV, chunk 2048, 128 outputs, 8 inference threads, no prefix reuse. TTFT is HTTP first-token latency; decode uses 127 first-to-last intervals. A100: 7 production / 14 optimized samples. H200 baseline: 14 samples/row after warmups, GPU 1 / CPU cores 8-15; pre-rendered prompts, tokenization/image processing inside HTTP.

Limits: H200 is unpaired: prompts/harness differ; the new 448x448 image has 244 input tokens versus 356 historically. Ratios are descriptive, not causal speedups. A100 is not matched to the new H200 runtime; no cross-GPU hardware claim. Optimized ORT 5481f20b / GenAI 4e51307d + 41c96365 are frozen, not current PR-head benchmarks.

Review required: historical optimized export loses 9 AI2D answers versus the accepted native recipe; padded/batched capture limitations remain. No default promotion.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@CLAassistant

CLAassistant commented Nov 6, 2026 •

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ hanbitmyths
❌ Copilot
You have signed the CLA already but the status is still pending? Let us recheck it.

@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Performance Comparison

Comparing 11000381 → d9a1ee58

Model Metric Baseline Current Delta
bert (feature-extraction) model_size_bytes 359 KB 359 KB +0.0% ⚪
bert (feature-extraction) num_nodes 68 68 +0.0% ⚪
falcon model_size_bytes 364 KB 364 KB +0.0% ⚪
falcon num_nodes 66 66 +0.0% ⚪
gemma2 model_size_bytes 428 KB 428 KB +0.0% ⚪
gemma2 num_nodes 105 105 +0.0% ⚪
gpt2 model_size_bytes 324 KB 324 KB +0.0% ⚪
gpt2 num_nodes 54 54 +0.0% ⚪
llama model_size_bytes 425 KB 425 KB +0.0% ⚪
llama num_nodes 60 60 +0.0% ⚪
llama (static-cache) model_size_bytes 425 KB 425 KB +0.0% ⚪
llama (static-cache) num_nodes 56 56 +0.0% ⚪
mamba (ssm-text-generation) model_size_bytes 296 KB 296 KB +0.0% ⚪
mamba (ssm-text-generation) num_nodes 94 94 +0.0% ⚪
phi3 model_size_bytes 421 KB 421 KB +0.0% ⚪
phi3 num_nodes 58 58 +0.0% ⚪
phi3 (static-cache) model_size_bytes 421 KB 421 KB +0.0% ⚪
phi3 (static-cache) num_nodes 54 54 +0.0% ⚪
qwen2 model_size_bytes 425 KB 425 KB +0.0% ⚪
qwen2 num_nodes 60 60 +0.0% ⚪
qwen2 (static-cache) model_size_bytes 425 KB 425 KB +0.0% ⚪
qwen2 (static-cache) num_nodes 56 56 +0.0% ⚪
qwen3_5_moe (hybrid-text-generation) model_size_bytes 506 KB 506 KB +0.0% ⚪
qwen3_5_moe (hybrid-text-generation) num_nodes 269 269 +0.0% ⚪
qwen3_5_text (hybrid-text-generation) model_size_bytes 458 KB 458 KB +0.0% ⚪
qwen3_5_text (hybrid-text-generation) num_nodes 131 131 +0.0% ⚪
qwen3_5_vl (hybrid-qwen-vl) model_size_bytes 977 KB 977 KB +0.0% ⚪
qwen3_5_vl (hybrid-qwen-vl) num_nodes 461 461 +0.0% ⚪
t5 (seq2seq) model_size_bytes 836 KB 836 KB +0.0% ⚪
t5 (seq2seq) num_nodes 176 176 +0.0% ⚪
whisper (speech-to-text) model_size_bytes 1008 KB 1008 KB +0.0% ⚪
whisper (speech-to-text) num_nodes 128 128 +0.0% ⚪

No performance regressions.

@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

🏗️ Architecture Diff

Comparing 11000381 → d9a1ee58

Model Sub-model Changes Status
bert (feature-extraction) model 0 ⚪
falcon model 0 ⚪
gemma2 model 0 ⚪
gemma4 (gemma4) decoder 0 ⚪
gemma4 (gemma4) embedding 0 ⚪
gemma4 (gemma4) vision_encoder 0 ⚪
gemma4_text model 0 ⚪
gpt2 model 0 ⚪
llama model 0 ⚪
llama (static-cache) model 0 ⚪
mamba (ssm-text-generation) model 0 ⚪
phi3 model 0 ⚪
phi3 (static-cache) model 0 ⚪
qwen model 0 ⚪
qwen (static-cache) model 0 ⚪
qwen2 model 0 ⚪
qwen2 (static-cache) model 0 ⚪
qwen2_moe model 0 ⚪
qwen2_moe (static-cache) model 0 ⚪
qwen3 model 0 ⚪
qwen3 (static-cache) model 0 ⚪
qwen3_5_moe (hybrid-text-generation) model 0 ⚪
qwen3_5_text (hybrid-text-generation) model 0 ⚪
qwen3_5_vl (hybrid-qwen-vl) decoder 0 ⚪
qwen3_5_vl (hybrid-qwen-vl) embedding 0 ⚪
qwen3_5_vl (hybrid-qwen-vl) vision_encoder 0 ⚪
qwen3_moe model 0 ⚪
qwen3_moe (static-cache) model 0 ⚪
qwen3_next (hybrid-text-generation) model 0 ⚪
t5 (seq2seq) decoder 0 ⚪
t5 (seq2seq) encoder 0 ⚪
whisper (speech-to-text) decoder 0 ⚪
whisper (speech-to-text) encoder 0 ⚪

No architecture changes detected. ✅


Legend: ⚪ No change · 🔵 Minor (attrs/inits) · 🟡 Moderate (nodes added/removed) · 🔴 Major (interface changed)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Pruning options are silently ignored by unsupported subclass build paths, and shared-weight decoder parity tests are missing to verify row selection and full-prompt cache preservation.

2 open issues
What changed in this PR

Adds opt-in runtime-indexed final-token projection to Qwen3-VL’s three-component export. The decoder selects hidden rows before the language-model head while preserving full-prompt attention and KV-cache processing; existing defaults remain unchanged.

Changes:

  • Thread pruning and runtime-index options through builders, tasks, and the decoder.
  • Add an explicit DeepStack opt-out and omit the runtime-reserved input from GenAI JSON mappings.
  • Document the runtime contract and add graph, option-validation, and row-selection tests.
File Description
tests/​build_graph/​vision_language_test.py Check pruning, runtime-input, and DeepStack graph contracts.
src/​mobius/​tasks/​_vision_language_3model.py Add Qwen-VL pruning options and model eligibility checks.
src/​mobius/​tasks/​_base.py Declare the optional decoder index input.
src/​mobius/​models/​qwen_vl.py Select hidden rows before the language-model head.
src/​mobius/​models/​base.py Support per-batch runtime indices in token selection.
src/​mobius/​models/​_models_test.py Test numerical row selection and unsupported task options.
src/​mobius/​integrations/​transformers/​_builder.py Forward runtime options and support explicitly disabling DeepStack.
src/​mobius/​integrations/​transformers/​_builder_test.py Test option forwarding, defaults, and rejection paths.
src/​mobius/​integrations/​ort_genai/​auto_export.py Exclude the reserved index input from decoder JSON mappings.
src/​mobius/​integrations/​ort_genai/​auto_export_test.py Verify reserved-input handling in generated configuration.
src/​mobius/​_builder.py Route pruning options to Qwen-VL tasks.
src/​mobius/​_builder_test.py Update the supported-task error expectation.
README.md Document runtime-indexed pruning and DeepStack compatibility controls.

Review effort: Balanced

Comment thread src/mobius/models/qwen_vl.py Resolved
Reject inherited pruning options on specialized VL task build paths instead of silently ignoring them. Add shared-weight decoder tests for static and runtime row selection, unequal right padding, DeepStack on/off, full K/V retention, and subsequent cached decoding. Document supported task paths.

Validation: 675 affected tests passed with 16 pre-existing skips, including all 12 new regressions; repository-pinned Ruff lint and formatting checks passed.
Signed-off-by: Sunghoon Choi <sunghcho@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The new test directly calls SerializeToString(), coupling it to the protobuf API instead of using ir.save or the existing OnnxModelSession wrapper.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Low severity Use IR serialization instead of direct protobuf serialization

src/​mobius/​models/​_models_test.py:56

This calls SerializeToString() directly on the returned protobuf model, coupling the new test to the protobuf API. Please serialize with ir.save and pass the resulting filename to ORT instead, or use the existing OnnxModelSession wrapper, which already handles IR serialization.

2 resolved since last review

Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The reserved-input ABI needs cross-stack validation of the rebased export/runtime pair because companion GenAI PR #2889 was inaccessible and the reported runtime gate predates the rebase.

No open issues 🎉

Review effort: Balanced

Save the tiny gather fixture through onnx_ir instead of calling protobuf serialization directly. Preserve the existing two batch-row selection cases and repository conventions. Six targeted gather and decoder/cache parity cases pass; Ruff 0.16.2 lint and format checks are clean.

Signed-off-by: Sunghoon Choi <sunghcho@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@hanbitmyths

Copy link
Copy Markdown
Author

Latest review follow-up: rebased exporter/runtime validation

Serialization review

Fixed in 6cfb33ca: the runtime-index test now uses ir.save with a pytest temporary file instead of direct protobuf serialization. All 6 targeted gather/shared-weight decoder/cache tests passed, with no skips. Ruff 0.16.2 lint/format and whitespace checks passed. This follow-up changes only the test fixture, not production/performance code.

Fresh reserved-input ABI verification

This is new validation of the rebased pair, not reuse of the pre-rebase runtime gate:

  • Exporter: Mobius 6cfb33ca.
  • Runtime: GenAI 725eebfd, with ORT 69685db4.
  • Fresh full/unpruned and runtime-index-pruned FP16 Qwen3-VL-2B packages, built through the public Mobius APIs from raw revision 89644892e4d85e24eaac8bacfd4f463576704203, with the explicit DeepStack compatibility opt-out. No historical exports or graph surgery were substituted.
  • Matching Python 3.13 native bindings and the frozen six-library runtime bundle were path/hash checked. Tests ran on an A100-SXM4-80GB with eight CPU threads.
  • The exported reserved last_token_indices input is INT32 [B,1,1], absent from generic decoder JSON mappings; the unpruned graph has no reserved input. Native observations verified chunk-relative final non-PAD indices during prefill and zero during decode.
Comparison Passing case pairs Result
Full/pruned eager, batches 1-4; one-token, text, padded and real-image prompts 16 All nine logit states and per-row argmax values agree
Eager/captured batch 1; one-token, text and real-image prompts 3 Logits are bitwise equal
Full/pruned captured batch-1 padded control 1 All nine logit states and argmax values agree

Across these 20 comparisons, all 2,240 written KV-prefix tensor pairs were bitwise equal, covering all 28 layers at prefill and after eight forced decode steps. Full/pruned eager logits passed atol=0.02, rtol=0.001 (maximum absolute difference 0.03125, within the combined tolerance); the captured padded full/pruned maximum was 0.0078125. Unused cache-capacity tails were not compared. The pruned batch-1 capture profile recorded 4 actual captures / 33 graph launches.

Capture compatibility is not fully passing. The padded batch-1 captured result differs substantially from eager: maximum logit difference 12.28125, with initial argmax 11958 -> 576. The same mismatch reproduces with pruning disabled; pruned/unpruned captured outputs preserve the same result and written caches. This establishes independence from this PR's pruning, not the root cause or correctness of padded capture. Batch-2 captured text prefill also hits batch_size must be 1 when sequence_length > 1 and past context is given at both chunk sizes 16 and 2048. That ORT guard is unchanged upstream; its trigger has not been fully isolated. No passing full batched capture gate is claimed.

These FP16 ABI controls are not a new performance or MMLU/AI2D evaluation. The historical Foundry-only benchmark, pre-rebase scope warning, DeepStack limitation and additional nine-answer AI2D regression remain unchanged; promotion remains REVIEW REQUIRED.

Why CI was incomplete

  • The required PR L4/L5 run exceeded the 60-minute job limit. Both jobs were cancelled, with failures already present in their incomplete logs; these are not completed suite verdicts.
  • The unchanged-main all-model run also reached the time limit. 24 exact L4 failing node IDs overlap with the PR run.
  • A narrow BLOOM probe on unchanged main, head 1100038138f84eeb7bd59ccb9471c392deda4820, reproduced CUBLAS failure 3: the resource allocation failed; expr=cublasCreate(&cublas_handle_) before numerical parity could be evaluated. This does not prove VRAM exhaustion or that every partial failure has the same cause.
  • Shared task/exporter changes intentionally select run_all=true. The optional pinned real SmolLM lane is intentionally skipped because the workflow passes run_real: false; its fast CPU lane passed. Draft status is not the reason for that skip.

The baseline-failure investigation is bounded. No required check, selector or timeout was weakened, and neither the full GPU CI suite nor the fresh captured/eager compatibility gate is represented as green.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Human confirmation of cross-runtime compatibility is needed because the reserved-input ABI depends on unreviewed GenAI changes, required L4/L5 CI was cancelled, and padded/batched capture validation remains unresolved.

No open issues 🎉

Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Enabling pruning silently resets a supplied QwenVLTask’s runtime-index setting, causing right-padded batches to select the last physical row instead of the requested valid row.

1 open issue

Review effort: Balanced

Comment thread src/mobius/_builder.py Resolved
Keep an existing QwenVLTask runtime-index setting when build_from_module enables pruning. Cover task/string flag combinations and actual right-padded batch prefill/decode/KV parity; document the option-preservation semantics.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@hanbitmyths
hanbitmyths marked this pull request as ready for review November 9, 2026 05:20
@hanbitmyths
hanbitmyths requested a review from a team November 9, 2026 05:20

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants