Repository navigation
Add the Qwen3.8-Flash QMoE offload package builder - #2821
Draft
Tianlei Wu (tianleiwu) wants to merge 6 commits into
Draft
Tianlei Wu (tianleiwu) wants to merge 6 commits into
Tianlei Wu (tianleiwu) wants to merge 6 commits into
Conversation
This was referenced Oct 30, 2026
Tianlei Wu (tianleiwu)
force-pushed
the
tlwu/20261030/qwen38-offload-package-builder
branch
from
October 30, 2026 05:45
2649ae7 to
3999aef
Compare
Akshay Sonawane (apsonawane)
left a comment
Contributor
There was a problem hiding this comment.
Reviewed all 4 changed files at 3999aef and the named ORT offload/profile contracts. Three new findings are inline. Most importantly, explicit/migration graphs emit legacy QMoEOffload although the claimed ORT #33622 runtime explicitly rejects that schema and requires standard QMoE plus session options. Synthetic Python checks also reproduced dangling nonaligned MTP pool bindings and lost uniform slot totals. No large-package conversion, complete exported-session creation or CUDA budget/performance run was performed. No approval submitted.
Add build_package_offload_v3.py, which converts a v12b package into a QMoE-offload package for GPUs that cannot hold the routed experts: - The 48 QMoE nodes of the target graph become QMoEOffload nodes with an explicit layer index and sizes. Resident and spare expert slots can be set directly or be derived from an expert-popularity profile. - model_s2.onnx is the rewritten graph; model_s3.onnx and model_s4.onnx extend it to 3 and 4 verify rows for two or three chained MTP drafts, and model.onnx is a copy of model_s4.onnx. - weights_pool.txt places the routed experts in CPU mmap placement and the dense weights and the token embedding in host-mapped placement. Everything else is hard-linked from the source package. - genai_config.json gets the CUDA-graph, arena and device-memory-limit options, and the stop token id can be replaced. - --out must not equal, contain or be nested in the source package. - --migrate-shared-onnx moves a single graph to the compact QMoEOffload schema without touching the weight data. The README documents the script and its options. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Let the flat-package builders write the S=2..4 target graphs that the offload package builder provides: - build_package_engine_v12b.py --clean-package links model_s2.onnx to model.onnx, generates model_s3.onnx and model_s4.onnx with build_multidraft_target_onnx, and writes four runtime profiles for GPUs with 20 to 34 GiB of device memory. Each profile selects model_s4.onnx and sets the QMoE offload session options (placement, device and host expert budgets, expert popularity file). --expert-profile copies an STRP v1 popularity file into the package; without it the builder prints a note and the file has to be copied in by hand. - build_package_v12d_int4_int8.py --clean-flat-package writes the same S=2..4 graphs. The reused config carries the runtime profiles over. The existing check for a consumer-gpu-24g profile in that config is kept as is; it matches nothing because the profiles are stored as a list. Both builders import build_multidraft_target_onnx lazily, so the flat packages need build_package_offload_v3.py in the same folder. The profiles use host-memory eligibility and a decoder session-option overlay, so the packages load only in a runtime whose profile parser accepts them. The README documents the graphs and the profiles. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep the standard com.microsoft::QMoE operator and configure expert offload through the session.moe.offload.* entries of the ORT QMoE offload contract (microsoft/onnxruntime#33622) instead of rewriting nodes to QMoEOffload with layer_index/resident_slots attributes. The legacy rewrite flags are rejected with an explanation, legacy QMoEOffload source graphs are refused, and the per-layer cap goes through node_overrides. Relocate every host-mapped MTP pool with an unaligned offset (not only the pool ids 607/608) into a layout-keyed aligned copy, and distribute the remainder of a uniform resident-slot total so the plan sums exactly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nfig surface The qwen38_flash_graph config parser rejects every model.qwen38_graph key it does not know. The offload builder picked the head session as fold.mtp, fold.drafter or fold.head_r2, whichever the source config had, and copied the rest of the section as it was. - Move the section of the source package to the current surface with package_config.migrate_qwen38_graph_config and print one line per change; the head session is fold.head_r2 only. - Refuse a source without fold.head_r2 or prefill, and a --prefill-chunk the engine rejects (not positive, above kv_capacity with prefill.enabled, odd without it), before anything is written to --out. - Test the migration of each removed key, the written config against the keys the engine accepts, the unchanged weights_pool.txt session labels and the chunk limits. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tianlei Wu (tianleiwu)
force-pushed
the
tlwu/20261030/qwen38-offload-package-builder
branch
from
October 31, 2026 02:20
3999aef to
b987282
Compare
…ad tests The build helper returned (out, *read_output(...)), a tuple whose length code scanning cannot see, so every call site that unpacks its five results was reported as a mismatched multiple assignment. It now returns the five values explicitly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The released onnxruntime 1.31.0 (the CPU wheel of CI) reports a QMoEOffload node as Fail (not a registered function/op); InvalidGraph is what a build that registers its own QMoEOffload schema reports. The test of the graphs that the previous builder emitted pinned InvalidGraph, so it only passed on such a build. It now accepts the failure categories of a failed load (Fail, InvalidGraph, NotImplemented) and still requires the message to name the operator or the attribute. The refusal of a legacy QMoEOffload dense graph names the path with the separator of the OS, and the test matched a forward slash. The expected path is now built with pathlib. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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
Adds
tools/qwen38_graph/build_package_offload_v3.py, which converts a v12b package into a QMoE-offload package for GPUs that cannot hold the 63 GiB of routed experts, and makes the two flat-package builders of #2820 write the multi-draft target graphs and the offloadruntime_profiles.Both use the offload of the ONNX Runtime CUDA execution provider (the open stack microsoft/onnxruntime#33450 → #33622, as for #2784). That runtime has no
QMoEOffloadoperator: it offloads standardcom.microsoft::QMoEnodes and is configured withsession.moe.offload.*session options. The graphs of this PR keep standardQMoEnodes, and there are two ways to set the options:build_package_offload_v3.py --from-package): the 48com.microsoft::QMoEnodes of a v12b package (NVFP4 + FP8, custom-op lineage) stayQMoEnodes (only therouter_weightsinput, which the offload backend does not support, is cleared). The decoder session getssession.moe.offload.mode,placementandcpu_execution(--offload-mode,--offload-placement), optional device and host expert budgets, the expert-popularity profile and per-node resident-slot caps (node_overrides), andweights_pool.txtplaces the routed experts incpummap placement.build_package_engine_v12b.py --clean-packageandbuild_package_v12d_int4_int8.py --clean-flat-package): the package gets fourruntime_profilesthat selectmodel_s4.onnxand set the same options. This is the path of the runtime in [Qwen38] Size the QMoE offload budgets from the context tier and run DFlash2 warmup in offload warmup mode #2784, which sizes the offload budgets from the context tier; Add decoder.session_options and host-memory eligibility to RuntimeProfile for portable MoE offload #2762 teaches the runtime to parse the profiles.Summary of Changes
tools/qwen38_graph/build_package_offload_v3.py--from-packagerewrites a layered or flat v12b package:model_s2.onnxis the prepared target graph (standardQMoE),model_s3.onnxandmodel_s4.onnxextend it to 3 and 4 verify rows (build_multidraft_target_onnx, 2 or 3 chained MTP drafts),model.onnxis a copy ofmodel_s4.onnx;weights_pool.txtputs the routed experts on the CPU and the dense weights and the token embedding in host-mapped placement (with--mtp-moe-placement host_mapped, the MTP expert pools that start at an unaligned offset, whatever their pool ids, move to one aligned file next to the source package);genai_config.jsongets thesession.moe.offload.*options, CUDA-graph capture andkSameAsRequestedarena options.--offload-mode required(the default) stops, before anything is written, at a node the runtime would not offload;autokeeps such nodes on the GPU.--target-resident-slots(a per-layer cap) or--expert-profilewith--total-resident-slots(--uniform-profile-slots,--min-slots-per-layer) set the resident experts, and a total that the profile cannot fill exactly is refused. The flags that only made sense for the legacyQMoEOffloadoperator (--migrate-shared-onnx,--out-onnx,--strip-phase,--strip-stats,--target-spare-slots,--dense-offload-qmoe,--dense-stream-min-rows,--dense-stream-wave) are rejected with the reason, and a source graph withQMoEOffloadnodes is refused.tools/qwen38_graph/build_package_engine_v12b.py--clean-packagelinksmodel_s2.onnxtomodel.onnx, generatesmodel_s3.onnxandmodel_s4.onnx, and writes fourruntime_profilesfor GPUs with 20 to 34 GiB of device memory.--expert-profilecopies an STRP v1 popularity file into the package asexpert-profile.bin; without it the builder prints a note.tools/qwen38_graph/build_package_v12d_int4_int8.py--clean-flat-packagewrites the same S=2..4 graphs; the profiles come with the config of--reuse-embed-from.tools/qwen38_graph/README.mdtest/python/builder/test_qwen38_offload_package_builder.py,_qwen38_qmoe_offload_contract.py1d5ae2f3,test_qmoe_offload_v4.py), so the tests check that every built graph passes them; the tests also create real sessions in the installed onnxruntime.Notes for review
minimum_effective_host_memory_bytes/maximum_effective_host_memory_bytesand a decodersession_optionsoverlay, so a runtime without Add decoder.session_options and host-memory eligibility to RuntimeProfile for portable MoE offload #2762 rejects thegenai_config.json.build_multidraft_target_onnxextends a custom-op graph (it needs theFoldMid2node ofqwen.fold). The contrib-operator packages ofbuild_package_contrib_ops.pyhave no such node, so that builder dropsmodel_s2/s3/s4.onnxand theruntime_profiles, and the rc5 INT4 / INT3 packages carry their own profiles.build_multidraft_target_onnxlazily, sobuild_package_offload_v3.pyhas to be in the same folder.build_package_offload_v3.pyexpects a v12b-lineage package:model.qwen38_graphwithfold.head_r2andprefillsections and standardQMoEnodes inmodel.onnx. A source config of an earlier config surface is migrated while it is read (see Add Qwen3.8-Flash graph package builders #2820), and the builtgenai_config.jsononly has the keys the reworked Add the qwen38_flash_graph model type and its device decode engine #2727 accepts:fold.head_r2.session_optionscarries the CUDA-graph defaults andprefill.chunkthe chunk, which is checked againstkv_capacitybefore anything is written. A package that already offloads through session options needs no conversion.router_weightsinput of theQMoEnodes because the offload backend does not take it; a node that had it then weights the selected experts byrouter_probs(the count isrouter_weights_clearedinpackage_manifest.json).model_s2/s3/s4.onnxof a source that already has them are regenerated, not hard-linked.consumer-gpu-24gprofile to setqmoe.offload_gpu_layers: it matched nothing, because the profiles are stored as a list.Testing
ruff checkandruff format --checkpass on the files of this PR; the five builders byte-compile and print--help.cd test/python/builder && python -m pytest -q test_qwen38_offload_package_builder.py: 290 passed in about 5 s with the released onnxruntime 1.31.0 CPU wheel that CI installs, no GPU. They build layered and flat packages from synthetic v12b packages and check the output: standardQMoEnodes that pass the modelled session creation of #33622 for each mode (required,auto) and placement (adaptive,static), real session creation in the installed onnxruntime, the source package unchanged, every requested resident-slot total met or refused, every host-mapped MTP pool kept with a defined binding, the config surface, and the rejected legacy flags and graphs. The tests of the fixed defects fail on the earlier builder.QMoEOffloadnodes), run on a copy of a flat NVFP4 + FP8 RC2 package (--vram-limit-gib 32 --expert-profile <expert-profile.bin> --total-resident-slots 960): built in about 3 s, converted 48QMoEnodes, wrote S=2 / 3 / 4 graphs of 1030 / 1056 / 1082 nodes and a pool of 309 entries (288 on the CPU). The current converter was not run on a real package.1d5ae2f3,test_qmoe_offload_v4.pyL290-L317, and against the released onnxruntime 1.31.0 CPU wheel, which ignores the offload options), and the two clean-package paths (they need the HF checkpoint and the v12b / v12d inputs). The runtime side of the offload is measured with the rc5 packages in [Qwen38] Size the QMoE offload budgets from the context tier and run DFlash2 warmup in offload warmup mode #2784.Checklist
test_qwen38_offload_package_builder.py; the clean-package paths run against source packages)tools/qwen38_graph/README.md)