feat(optimize/analyze): preview applicable optimizations and check their produced-operator support - #1238
Conversation
Add a --dry-run flag to `winml optimize` that probes every registered optimization capability independently against a model and reports which ones would actually change it, along with the specific nodes and constants affected -- without writing any output. The analysis is universal and architecture-agnostic: it mirrors the optimizer pipeline, computes a per-pipe baseline, then re-runs each pipe on a clone with a single default-off capability enabled and diffs the result (keyed by output tensor names, plus an initializer diff for constant-only surgery ops). Each probe is isolated and failure-tolerant. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Address CodeQL findings on analysis.py by replacing the duplicated `import onnx` statements (one under TYPE_CHECKING, two inline) with a single top-level `from onnx import AttributeProto, GraphProto, ModelProto, NodeProto`, matching the established source style. Apply the same single-style import in the test module. Fix the mypy no-any-return error in `_run_pipe` by binding the pipe result to a `ModelProto`-typed local before returning. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
should move to analyze command to take advantage of analyzer data |
…or support For each optimization that would change the model, reuse the optimize dry-run engine to obtain the model it would produce, then check whether the operators it introduces (added/modified nodes) are supported on the resolved EP/device using analyze's runtime rule data. - optim: add iter_optimization_outputs() yielding (finding, produced_model) pairs, sharing a new _iter_findings generator with analyze_model. - analyze: add RuntimeChecker.op_support(node_output_filter=...) to limit the check to specific produced nodes; add optim_output bridge module and the --check-optim-output CLI flag + renderer. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Rename optimize's --dry-run and analyze's --check-optim-output to a shared --check-optim flag for consistency. Both are unreleased and share the same open PR, so no backward-compat alias is kept. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
DingmaomaoBJTU
left a comment
There was a problem hiding this comment.
Review Summary
PR scope: Adds --check-optim to both winml optimize and winml analyze, with a clean dry-run analysis engine, produced-operator support checking, and good test/doc coverage. Well-structured — new modules (optim/analysis.py, analyze/optim_output.py) are cleanly separated. Inline comments below.
- _matched_node: log at debug when the PatternRuntime chain breaks so a rule-data change does not silently degrade produced-operator support to UNKNOWN without a trace. - _render_check_optim: accept console as a parameter (mirrors analyze's _render_optim_output_support) for testability. - analysis.py: document why the probe calls pipe.process directly instead of _run_pipe, why the pre-stage clone is required (infer_shapes mutates large models in place), and that pipes only ever receive deep clones. - tests: import onnx in a single style to clear the CodeQL duplicate-import finding. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
DingmaomaoBJTU
left a comment
There was a problem hiding this comment.
Second-pass deeper review — additional findings on correctness and performance.
…obe memory - Add TestClone verifying _clone is a fully independent deep copy, including nested subgraph attributes and initializers (mutating the original leaves the clone byte-for-byte unchanged). - Document in _node_identity that output renames surface as a remove+add pair rather than a modification, an intentional graph-delta semantics choice. - analyze.py: log the number of applicable optimizations materialized and note the memory-vs-recompute trade-off of retaining produced-model clones across targets. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
DingmaomaoBJTU
left a comment
There was a problem hiding this comment.
Third-pass review — error handling, edge cases, and correctness deep-dive.
Verified OK (no issues):
auto_enable_dependenciesreturns a new dict, no mutation risk between probe iterationsPIPESordering is shared between_iter_findingsandOptimizer.optimize()— consistentnode_output_filter.intersection(node.output)handles protobuf repeated fields and empty outputs correctly--check-optimproperly requires--model(validated before handler)- Optimization probe runs once, support check runs per EP/device target — good design for
--ep all
DingmaomaoBJTU
left a comment
There was a problem hiding this comment.
LGTM — approving after three review passes. Well-architected feature with clean separation (analysis.py for dry-run, optim_output.py for EP support checking).
Non-blocking follow-ups (can be addressed in later PRs):
- Add try/except around baseline
_run_pipecall (analysis.py:280) for robustness — low risk since default configs rarely fail, but would match the probe's error handling _collect_initializersmay produce false "modified" findings for external-data models (serialization includes file metadata)- For very large models with many applicable optimizations,
list(iter_optimization_outputs(...))materializes allModelProtoclones in memory — consider a warning or lazy approach
All other areas verified: dependency management, pipeline ordering, filter logic, multi-target reuse, --model validation — all clean.
…ep nesting) - Guard the baseline pipe run in _iter_findings with try/except: a pipe that fails on its default config now skips its probes (leaving the last good model for downstream pipes) instead of aborting the whole scan, mirroring the existing per-probe handling. - Compare initializers via a signature that strips external-data location metadata (path/offset/length + data_location), so a pipe re-saving external data no longer produces spurious modified_initializers; genuine content or shape changes are still detected. - Rewrite _collect_nodes as an iterative (explicit-stack) traversal so deeply nested If/Loop/Scan subgraphs cannot exhaust Python's recursion limit. - Tests: external-data offset-change (not modified) + shape-change (modified), and multi-level nested-subgraph collection. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
DingmaomaoBJTU
left a comment
There was a problem hiding this comment.
LGTM. The --check-optim feature is well-designed: the dry-run analysis walks the pipeline pipe-by-pipe with per-capability isolation, the graph diff via output-tensor identity is robust, and the produced-model handoff to the analyze bridge (--check-optim on analyze) cleanly separates target-independent probing from EP-specific support checks. Test coverage is thorough (diff helpers, end-to-end analysis, CLI wiring, rendering). A couple minor observations (non-blocking):$([char]10)$([char]10)1. _matmul_add_model() is duplicated across test_analysis.py, test_optim_output.py, and test_static_analyzer_cli.py -- consider a shared conftest fixture.$([char]10)2. In analyze.py the optim_outputs list is typed as list[tuple[Any, Any]] -- using the concrete types list[tuple[CapabilityFinding, ModelProto]] would be clearer and catch misuse.
Summary
Two related, opt-in features for reasoning about optimizations before applying them, sharing one consistent flag name
--check-optim:winml optimize --check-optim— answers "which optimizations actually apply to this model, and on which nodes?" without writing any output.winml analyze --check-optim— for each optimization that would change the model, checks whether the operators it introduces are supported on the resolved EP/device.optimize --check-optimProbes every registered optimization capability independently against the given model and reports only the ones that would genuinely change it, along with the specific nodes and constants affected.
Add
-vto list every affected node/constant instead of a sample.--check-optimrequires--modeland writes nothing to disk.analyze --check-optimReuses the optimize probe engine to obtain the model each optimization would produce, then feeds the produced (added/modified) nodes into analyze's
RuntimeCheckerand classifies them against the target EP/device rule data. Answers "if I apply this optimization, will its output run on my target?".Renders a per-target OPTIMIZATION OUTPUT SUPPORT section listing each applicable optimization, its
--enable-*flag, and the support level (supported / partial / unsupported / unknown) of every operator it would introduce. The probe is target-independent, so it is materialized once and reused across all resolved targets; only the support lookup runs per EP/device. Disabled by default; console-only.How it works (universal, no hardcoding)
The probe mirrors
Optimizer.optimize(): run mandatory shape inference, then walk the pipes in order. For each pipe it computes a baseline output, then re-runs the pipe on a clone of the upstream model with only one default-off capability enabled (plus auto-enabled dependencies) and diffs the result:clamp-constant-values) that modify no nodes.CopyFrom(not a serialize round-trip) because saving can convert tensors to external data in place, and to avoid the 2 GB protobuf limit.The support check correlates produced nodes back to
op_supportresults by output-tensor name, andRuntimeChecker.op_support(node_output_filter=...)restricts the check to just the produced nodes. No op/tensor/architecture names are hardcoded (CLAUDE.md Cardinal Rule #1).Changes
src/winml/modelkit/optim/analysis.py—analyze_model,iter_optimization_outputs,CapabilityFinding,NodeRefand diff helpers (shared_iter_findingsgenerator)src/winml/modelkit/optim/__init__.py— export the new public APIsrc/winml/modelkit/commands/optimize.py—--check-optimoption + report renderer (with rich-markup escaping of dynamic labels)src/winml/modelkit/analyze/optim_output.py— bridge that checks produced-operator EP/device supportsrc/winml/modelkit/analyze/core/runtime_checker.py—op_support(node_output_filter=...)to restrict the check to a node subsetsrc/winml/modelkit/commands/analyze.py—--check-optimflag + per-target renderertests/unit/optim/test_analysis.py,tests/unit/analyze/test_optim_output.py, and CLI coverage intests/unit/commands/test_optimize_cli.pyandtests/unit/analyze/test_static_analyzer_cli.pydocs/commands/optimize.mdanddocs/commands/analyze.md— flag-table rows + worked examplesValidation
ruffclean;mypy -p winml.modelkitclean; doc/code flag-alignment check ✅optim+analyzesuites pass (hardware/fixture-gated skips and one pre-existing xfail only)