Repository navigation
Add merge_shared_input_projections option to the Qwen3.8 Flash builder - #2739
Draft
Tianlei Wu (tianleiwu) wants to merge 5 commits into
Draft
Tianlei Wu (tianleiwu) wants to merge 5 commits into
Tianlei Wu (tianleiwu) wants to merge 5 commits into
Conversation
Split model builders, loaders, quantization, export tooling, builder tests, and builder documentation from kvaishnavi/qwen-38-flash (bf8e32c).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move paged attention graph and runtime configuration helpers out of the core builder. Separate Qwen3.5 and Qwen3.8 graph expansions from model orchestration, and add dispatch regression coverage and architecture documentation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove the package conversion script and its dedicated test. Document re-exporting through Model Builder instead, preserving built-in IndexShare export support. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add an off-by-default option that merges the projections reading the same activation into one MatMul plus Split: linear-attention QKV/Z/A/B, sparse-attention QKV/gate/indexer, and the MoE router with the shared expert's gate/up. Split outputs keep the names of the unmerged MatMul outputs, so the graph interface and the runtime contract are unchanged. The option is exposed as target_options.optimizations.merge_shared_input_projections and is not applied to the MTP head. Add tests that export a synthetic Qwen3.8 checkpoint through the builder with and without the option and compare graph structure, concatenated weights and projection outputs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tianlei Wu (tianleiwu)
force-pushed
the
tlwu/20261014/qwen38-builder-graph-options
branch
from
October 19, 2026 08:30
889f0d8 to
5a186ef
Compare
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.
Description
Stacked on #2713 (base
kvaishnavi/qwen-38-flash-create, so the diff is only this change). Adds an off-by-defaultmerge_shared_input_projectionsoption to the Qwen3.8 Flash Next builder that merges projections reading the same activation into oneMatMulfollowed bySplit. The graph interface (inputs, outputs, state tensors, tensor names consumed downstream) is unchanged, so no runtime change is needed.Motivation and Context
Our research decode stack (a different export, built from the Mobius exporter and rewritten by a graph pass, not from this builder) measured these launch-count reductions at S=1 CUDA-graph replay, 2k context:
qk_l2_norm=1+ pre-castA_log/dt_biasAdd(w, 1)foldedBefore adding anything I checked what #2713's builder already emits for the decoder. The first two rows are already in #2713:
GatedDeltaNetis emitted withqk_l2_norm=1,A_log/dt_biasare float32 initializers consumed by the op, the recurrent state is the op's V-major layout with no transposes (paged path), the+1on the RMSNorm-style weights is folded at export, and no constantCast/Addor routerSoftmaxremains. This PR therefore does not touch them; it only adds a test that pins that behavior. What is left is the third row: #2713 already packs QKV+Z, A+B and the hyper-connection down/inject, but these groups still read one activation through separate MatMuls.Summary of Changes
New option
merge_shared_input_projections=true(extra option) ortarget_options.optimizations.merge_shared_input_projections(structured config). Defaultfalse. Qwen3.8 Flash Next only; the MTP head is exported unchanged.MatMul+Splitper layer)fuse_qkv)That is 96 groups for the official 48-layer checkpoint (the research stack had 97). The shared expert's one-wide output gate stays separate because a width that is not a multiple of 8 would misalign the merged GEMM. Each
Splitoutput keeps the name the unmergedMatMuloutput had. The option requires a dense, bias-free fp32/fp16/bf16 export and raises aValueErrorotherwise instead of silently skipping groups.Files
builders/qwen3_8.pymake_merged_projections, router/shared-expert and sparse-attention merge paths, GDN override, option validation, drop the option for the MTP headbuilders/qwen3_5.pymake_linear_attention_input_projso a subclass can supply all four projections (make_linear_attention_projections, default behavior unchanged)builder.py,builder_config.pytarget_options.optimizationsfieldREADME.md,docs/ModelBuilderConfiguration.mdtest/python/builder/_qwen38_tiny.pytest/python/builder/test_qwen38_merged_projections.py,test_builder_config.pyTesting
Run on CUDA GPU 1 (H200); the local transformers lacked
qwen4_exp, so the tests ran with a local shim that registers the config class (CI environments that already runtest_qwen38_nvfp4.pydo not need it).pytest test/python/builder/test_qwen38_merged_projections.py test/python/builder/test_builder_config.py: all pass (20 new merge tests + 3 new config tests). They export the synthetic checkpoint throughparse_extra_options/create_modelin dense and paged layouts and check: default graph already hasqk_l2_norm=1, fp32A_log/dt_bias, no constant Cast/Add, no state transposes in paged; merged graph changes only the expected MatMul/Split counts, keeps inputs/outputs and every produced tensor name, has no dangling inputs and the merged weights are the bitwise concatenation of the default ones; sub-graph outputs match the default export (CPU fp32allclose, CUDA bf16 within one bf16 step); unsupported inputs (quantized, biased, unaligned, mixed quantization exclusion, int4 export) are rejected; MTP head does not receive the option. Mutating the Split output order makes the numerical tests fail.test_qwen38_contrib_ops.py,test_qkv_fusion.py,test_qwen_gated_add.py,test_decoder_state_groups.py,test_qwen_mtp_export.py: the same 14 tests fail with and without this change (stale tests in the Add Qwen-3.8 Flash Next to model builder #2713 branch); everything else passes.test_qwen38_nvfp4.pycould not be imported locally (needsQwen4ExpVisionModel).ruff checkandruff format --checkon the changed test files.Indicative GEMM-level microbenchmark (PyTorch/cuBLAS CUDA graph, official shapes, bf16, M=1, merged GEMM plus Split copies versus separate GEMMs; not ORT and not this PR's graph): about 16.5 us saved per GatedDeltaNet group, 8.5 us per sparse-attention group, 11.5 us per MoE group, about 1.25 ms over 36/12/48 layers.
Not validated
-p int4/int8(the option rejects them),fuse_qkv=falsefor the sparse-attention group (kept separate by design) or on non-CUDA EPs.Checklist