Add dynamo export support and make it the default exporter#1156
Add dynamo export support and make it the default exporter#1156DingmaomaoBJTU wants to merge 15 commits into
Conversation
Flip `winml export` to torch's dynamo-based ONNX exporter by default (--dynamo/--no-dynamo, default True). Add DynamoMetadataTagger that reads torch's native pkg.torch.onnx.class_hierarchy / name_scopes node metadata into the existing winml.hierarchy.* tag contract, so module-hierarchy tags survive without a separate TorchScript trace. - config + CLI default flipped to dynamo=True; stale "not supported" warning removed - DynamoMetadataTagger drop-in tagger (same interface as ONNXNodeTagger) - HTP exporter skips the redundant TorchScript trace under dynamo and sets verbose=False to avoid a UnicodeEncodeError on Windows consoles - TorchScript-path-specific tests pin dynamo=False; docs updated
- test_dynamo_metadata_tagger: consolidate onnx imports to a single 'from onnx import ...' (CodeQL: module imported with both import and import-from) - docs/help: 'QNN-safe' reworded to name the real differentiator (opset/op decomposition, not shapes, since both exporters default to static shapes); add a static-shape export example
The dynamo exporter does not default to a higher final opset; winml passes the same opset (17) to both paths. torch's dynamo op library only implements ops at a minimum opset of 18, so it builds at 18 and then down-converts to the requested opset. Reword the export docs and concept doc to describe that lowering-and-conversion pass accurately instead of claiming a "newer opset" default.
torch's dynamo exporter targets a minimum opset of 18 and does not always lower to a smaller requested version (e.g. ResNet's ReduceMean keeps an opset-18-only attribute), so the export metadata previously misreported the requested opset (17) while the real graph was opset 18. - exporter: read the produced model's default-domain opset from the exported file and record that in the export monitor/metadata; warn when it differs from the requested opset. No post-export conversion is performed -- the requested opset is passed straight to the exporter and we report what it actually produced. - docs: correct the dynamo vs TorchScript opset wording (dynamo exports at opset 18, TorchScript at the configured opset 17) with the ResNet head decomposition example. - tests: add unit coverage for _read_default_opset.
When the dynamo exporter cannot down-convert a model to the requested opset, onnxscript's version_converter logs a WARNING with a full call stack (e.g. ResNet's ReduceMean during an 18->17 attempt). winml already surfaces a concise opset warning for that case, so the raw traceback is redundant noise in normal output. Add onnxscript.version_converter to the noisy-library logger floor so it is pinned at ERROR by default and only follows the CLI level once the user passes -v/-vv, matching how optimum's chatter is handled.
Resolves the CodeQL double-import alert on exporter.py: _read_default_opset now reads the proto through winml's own load_onnx helper (load_weights=False, validate=False) instead of a bare `import onnx`, matching the runtime onnx-loading style already used elsewhere in the file. Also floors torch's one-line "Setting ONNX exporter to use operator set version 18 ..." notice (logger torch.onnx._internal.exporter._compat) at ERROR by default, alongside the onnxscript version-converter traceback. winml already surfaces a concise opset warning, so both are redundant in normal output and only follow the CLI level at -v/-vv.
Two dynamo-path fidelity fixes from review: - DynamoMetadataTagger now folds each module's local scope name into its tag segment (Class.local) for every level, not just numeric indices, so same-class named siblings (an attention block's query/key/value Linears) stay independently addressable instead of collapsing to a single /.../Linear tag. Indexed children keep the existing Class.N form. - Add DynamoMetadataTagger.build_module_hierarchy to reconstruct the flat module hierarchy from node metadata. The exporter now recovers it after loading the exported ONNX and issues the deferred HIERARCHY monitor update, so the report/console/metadata module tree and the hierarchy_modules stat are populated for dynamo exports (previously 0). Output names for the dynamo path are sourced from the ONNX graph outputs instead of the empty traced outputs. Adds same-class-sibling tag tests and build_module_hierarchy unit tests.
Round-2 review follow-ups on the reconstructed dynamo module hierarchy. The tagger-level fixes from the previous commit were correct, but the shared tree writers could still drop or collapse modules: - metadata_writer: same-class named siblings (an attention block's query/key/value Linears) collapsed to one tree entry because children were keyed by bare class name. Key indexed children as Class.N (unchanged), disambiguate colliding same-class named siblings as Class.local, and keep the bare Class key for the unique case. Two-pass collision detection makes the keys order-independent, with a final full-remainder fallback for safety. - hierarchy_utils.find_immediate_children: rewrite traversal around a nearest-present-ancestor helper so a sparse hierarchy (an indexed child whose ModuleList container never emitted its own scope, e.g. blocks.0 with no blocks) attaches to its nearest real ancestor instead of being orphaned. metadata_writer now reuses this shared traversal, fixing the same root-scope limitation in the JSON report. - exporter: normalize the TorchScript output_names branch so output_names is always list[str] (infer_output_names returns list[str] | None), fixing the mypy union at the merge point. - docs: the dynamo path does not always export at opset 18. It targets a minimum opset of 18 and attempts to down-convert to a lower requested opset, which may succeed or fail; winml reports the opset actually produced. Update export.md and load-and-export.md accordingly. Add tests/unit/export/test_dynamo_hierarchy_report.py exercising the real MetadataWriter tree: all same-class siblings serialize and the sparse-root subtree is present; plus find_immediate_children sparse/dense nesting.
The dynamo export path reconstructs the module hierarchy from ONNX node metadata after export, but the reporting pipeline still described it as a TorchScript forward trace: the console/report claimed "Tracing module execution", the export summary said "Traced modules", and the metadata's execution_steps was set to the reconstructed-module count (a fabricated step total). Docs and the CLI docstring likewise presented forward-hook tracing as the default behaviour. Thread an explicit hierarchy source through the reporting pipeline so writers use source-appropriate wording and never report a trace that did not run: - step_data: add HIERARCHY_SOURCE_TRACE / HIERARCHY_SOURCE_ONNX_METADATA; HierarchyData.execution_steps is now int | None (None = no trace) and carries a source field. - exporter: tag the TorchScript HIERARCHY update as trace and the dynamo one as onnx_metadata, dropping the fabricated execution_steps count. - console/markdown writers: dynamo shows "Reconstructing/Recovered" with no execution-step line; the export summary label switches between "Recovered modules" and "Traced modules" by source. - metadata: record source + the real producer (DynamoMetadataTagger vs TracingHierarchyBuilder) and emit execution_steps=0 for dynamo instead of the module count; document the new source field in the schema. - docs + CLI docstring: distinguish the default dynamo metadata recovery from the --no-dynamo forward-hook trace. Add source-wording tests (console, markdown, and metadata) for both paths.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
| do_constant_folding: bool = True | ||
| verbose: bool = False | ||
| dynamo: bool = False # Use TorchScript exporter by default (dynamo=True for PyTorch 2.x) | ||
| dynamo: bool = True # TorchDynamo exporter by default; False = legacy TorchScript path |
There was a problem hiding this comment.
Flipping this default makes registered dynamo=False configs ineffective on the normal config-generation path. In generate_hf_build_config, a registered export config is only copied when it already contains input_tensors; otherwise _resolve_export_config_from_specs() returns a fresh config with this new dynamo=True default, and _assemble_config() uses it unchanged (_merge_export_config() is never called). The registered qwen3 transformer/embeddings/lm-head-only configs all specify WinMLExportConfig(dynamo=False, opset_version=18) without input tensors, so they now silently switch to Dynamo despite explicitly requiring TorchScript. The new submodule test only mutates an already-built parent and therefore misses this path. Please merge the registered export overrides (including an explicit false) into the resolved config and add a generation-level regression test.
Why
winml exportpreviously rejected--dynamoeven though PyTorch's dynamo ONNX exporter is the actively developed path. This PR makes dynamo a supported default while preserving the module hierarchy metadata used by inspection, reporting, benchmarking, and optimization tooling.What changed
WinMLExportConfigandwinml exportnow default todynamo=True.--no-dynamoremains available for the legacy TorchScript exporter, and generated submodule configs preserve an explicit parent exporter choice.DynamoMetadataTaggerreads genericpkg.torch.onnx.name_scopesandpkg.torch.onnx.class_hierarchymetadata. It handles sparse scopes and same-class siblings without architecture-, operator-, or tensor-specific logic.onnx_metadatarecovery fromtrace, use discovery-order wording for dynamo, and source output names from the exported ONNX graph.dynamo=false, and--no-dynamopreserves the existing TorchScript path.Validation