-
Notifications
You must be signed in to change notification settings - Fork 7
feat(optimize/analyze): preview applicable optimizations and check their produced-operator support #1238
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
feat(optimize/analyze): preview applicable optimizations and check their produced-operator support #1238
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
0e1f191
feat(optimize): add --dry-run flag to preview applicable optimizations
61118a8
fix(optimize): consolidate onnx imports and fix mypy return type
64b0a89
feat(analyze): add --check-optim-output flag to check produced-operat…
f483e2b
refactor(cli): unify optimize/analyze flags to --check-optim
46f966d
refactor(optim): address review feedback on --check-optim
573a1df
test(optim): add clone deep-copy test; document diff semantics and pr…
63894bf
fix(optim): robuster dry-run diff (baseline errors, external-data, de…
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,244 @@ | ||
| # ------------------------------------------------------------------------- | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. | ||
| # -------------------------------------------------------------------------- | ||
| """EP/device support of the operators an optimization would produce. | ||
|
|
||
| This bridges the optimizer's dry-run applicability analysis with the analyze | ||
| runtime-support rule data. For each applicable optimization it takes the model | ||
| that optimization would produce, then checks whether the operators the | ||
| optimization *introduces* (its added and modified nodes) are supported on a | ||
| target execution provider and device — answering "if I apply this optimization, | ||
| will its output run on my target?". | ||
|
|
||
| The check is universal and architecture-agnostic (CARDINAL RULE #1): the set of | ||
| produced operators is derived entirely from the concrete graph diff carried on | ||
| each :class:`~winml.modelkit.optim.CapabilityFinding`; nothing about specific | ||
| operators or model architectures is hardcoded. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| from collections import Counter | ||
| from dataclasses import dataclass, field | ||
| from typing import TYPE_CHECKING, cast | ||
|
|
||
| from .models.onnx_model import ONNXModel | ||
| from .models.support_level import SupportLevel | ||
|
|
||
|
|
||
| if TYPE_CHECKING: | ||
| from collections.abc import Iterable | ||
|
|
||
| from onnx import ModelProto, NodeProto | ||
|
|
||
| from ..optim import CapabilityFinding, NodeRef | ||
| from ..utils.constants import EPName | ||
|
|
||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| # Ordering used to summarize a mix of per-operator support levels into a single | ||
| # "worst" level. Higher rank = more concerning for the target. | ||
| _SEVERITY: dict[SupportLevel, int] = { | ||
| SupportLevel.SUPPORTED: 0, | ||
| SupportLevel.UNKNOWN: 1, | ||
| SupportLevel.PARTIAL: 2, | ||
| SupportLevel.UNSUPPORTED: 3, | ||
| } | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class ProducedOperatorSupport: | ||
| """Support of a single operator an optimization would introduce. | ||
|
|
||
| Attributes: | ||
| op_type: The produced operator's op type (e.g. ``"Gemm"``). | ||
| label: Human-readable node label (op type + name/output). | ||
| change: Whether the node is ``"added"`` or ``"modified"`` by the | ||
| optimization. | ||
| support: Support level of the operator on the target EP/device. | ||
| reason: Optional reason string from the runtime check. | ||
| """ | ||
|
|
||
| op_type: str | ||
| label: str | ||
| change: str | ||
| support: SupportLevel | ||
| reason: str | None = None | ||
|
|
||
|
|
||
| @dataclass | ||
| class OptimizationOutputSupport: | ||
| """Per-optimization view of its produced operators' target support. | ||
|
|
||
| Attributes: | ||
| name: Capability name (kebab-case, e.g. ``"matmul-add-fusion"``). | ||
| enable_flag: CLI flag that turns the optimization on. | ||
| category: Capability category (e.g. ``"matmul"``). | ||
| description: Human-readable capability description. | ||
| pipe_name: Name of the pipe that owns the capability. | ||
| operators: Per-operator support for every produced (added/modified) node. | ||
| error: Non-empty when the support check could not be completed. | ||
| """ | ||
|
|
||
| name: str | ||
| enable_flag: str | ||
| category: str | ||
| description: str | ||
| pipe_name: str | ||
| operators: list[ProducedOperatorSupport] = field(default_factory=list) | ||
| error: str | None = None | ||
|
|
||
| @property | ||
| def worst_support(self) -> SupportLevel: | ||
| """Most concerning support level across produced operators. | ||
|
|
||
| Returns ``SUPPORTED`` when there are no produced operators to check. | ||
| """ | ||
| if not self.operators: | ||
| return SupportLevel.SUPPORTED | ||
| return max((op.support for op in self.operators), key=lambda level: _SEVERITY[level]) | ||
|
|
||
| def support_counts(self) -> dict[SupportLevel, int]: | ||
| """Return a count of produced operators per support level.""" | ||
| return dict(Counter(op.support for op in self.operators)) | ||
|
|
||
|
|
||
| def _produced_node_refs( | ||
| finding: CapabilityFinding, | ||
| ) -> list[tuple[str, NodeRef]]: | ||
| """Return ``(change_kind, NodeRef)`` pairs for a finding's produced nodes. | ||
|
|
||
| Produced nodes are those an optimization introduces: newly added nodes and | ||
| nodes modified in place. Removed nodes are excluded — they no longer exist | ||
| after the optimization, so their target support is irrelevant. | ||
| """ | ||
| pairs: list[tuple[str, NodeRef]] = [] | ||
| pairs.extend(("added", ref) for ref in finding.added_nodes) | ||
| pairs.extend(("modified", ref) for ref in finding.modified_nodes) | ||
| return pairs | ||
|
|
||
|
|
||
| def _check_one( | ||
| finding: CapabilityFinding, | ||
| produced_model: ModelProto, | ||
| ep: EPName, | ||
| device: str, | ||
| model_path: str, | ||
| ) -> OptimizationOutputSupport: | ||
| """Check target support for the operators one optimization would produce.""" | ||
| from .core.runtime_checker import RuntimeChecker | ||
|
|
||
| result = OptimizationOutputSupport( | ||
| name=finding.name, | ||
| enable_flag=finding.enable_flag, | ||
| category=finding.category, | ||
| description=finding.description, | ||
| pipe_name=finding.pipe_name, | ||
| ) | ||
|
|
||
| produced = _produced_node_refs(finding) | ||
| if not produced: | ||
| return result | ||
|
|
||
| # Output tensor names uniquely identify produced nodes within the graph. | ||
| produced_outputs = {out for _, ref in produced for out in ref.outputs} | ||
|
|
||
| try: | ||
| onnx_model = ONNXModel.from_onnx_model(produced_model, model_path) | ||
| checker = RuntimeChecker(ep=ep, device=device, model=onnx_model) | ||
| # Only evaluate the produced nodes; skip the rest of the graph. | ||
| runtimes = checker.op_support( | ||
| node_output_filter=produced_outputs, | ||
| on_node_result=lambda _r: None, | ||
| ) | ||
| except Exception as exc: # pragma: no cover - defensive, keeps analyze alive | ||
| logger.warning( | ||
| "Could not check produced-operator support for '%s' on %s/%s: %s", | ||
| finding.name, | ||
| ep, | ||
| device, | ||
| exc, | ||
| ) | ||
| result.error = str(exc) | ||
| return result | ||
|
|
||
| # Map each produced output tensor to its support result. | ||
| support_by_output: dict[str, tuple[SupportLevel, str | None]] = {} | ||
| for runtime in runtimes: | ||
| node = _matched_node(runtime) | ||
| outputs = tuple(node.output) if node is not None else () | ||
|
xieofxie marked this conversation as resolved.
|
||
| classification = runtime.result.classification | ||
| reason = runtime.result.reason | ||
| for out in outputs: | ||
| support_by_output[out] = (classification, reason) | ||
|
|
||
| for change, ref in produced: | ||
| support, reason = _lookup_support(ref, support_by_output) | ||
| result.operators.append( | ||
| ProducedOperatorSupport( | ||
| op_type=ref.op_type, | ||
| label=ref.label(), | ||
| change=change, | ||
| support=support, | ||
| reason=reason, | ||
| ) | ||
| ) | ||
|
|
||
| return result | ||
|
|
||
|
|
||
| def _matched_node(runtime: object) -> NodeProto | None: | ||
| """Best-effort extraction of the ONNX node behind a ``PatternRuntime``.""" | ||
| pattern_match = getattr(runtime, "pattern_match", None) | ||
| skeleton = getattr(pattern_match, "skeleton_match_result", None) | ||
| matched_nodes = getattr(skeleton, "matched_nodes", None) | ||
| if matched_nodes: | ||
| return cast("NodeProto", matched_nodes[0]) | ||
| logger.debug( | ||
| "Could not extract matched node from runtime (type=%s); produced operators " | ||
| "correlated via this runtime will report UNKNOWN support", | ||
| type(runtime).__name__, | ||
| ) | ||
| return None | ||
|
|
||
|
|
||
| def _lookup_support( | ||
| ref: NodeRef, | ||
| support_by_output: dict[str, tuple[SupportLevel, str | None]], | ||
| ) -> tuple[SupportLevel, str | None]: | ||
| """Resolve a produced node's support via any of its output tensors.""" | ||
| for out in ref.outputs: | ||
| if out in support_by_output: | ||
| return support_by_output[out] | ||
| return SupportLevel.UNKNOWN, "not evaluated" | ||
|
|
||
|
|
||
| def check_optimization_output_support( | ||
| optimization_outputs: Iterable[tuple[CapabilityFinding, ModelProto]], | ||
| ep: EPName, | ||
| device: str, | ||
| model_path: str, | ||
| ) -> list[OptimizationOutputSupport]: | ||
| """Check produced-operator support for applicable optimizations on a target. | ||
|
|
||
| Args: | ||
| optimization_outputs: ``(finding, produced_model)`` pairs from | ||
| :func:`winml.modelkit.optim.iter_optimization_outputs`. Materialize | ||
| the iterator once and reuse it across targets — the dry-run that | ||
| produces these pairs is target-independent, so only the support | ||
| lookup needs to run per EP/device. | ||
| ep: Target execution provider (canonical name). | ||
| device: Target device string (e.g. ``"NPU"``). | ||
| model_path: Path to the source model, used for external-data resolution. | ||
|
|
||
| Returns: | ||
| One :class:`OptimizationOutputSupport` per applicable optimization, in | ||
| the same order as ``optimization_outputs``. | ||
| """ | ||
| return [ | ||
| _check_one(finding, produced_model, ep, device, model_path) | ||
| for finding, produced_model in optimization_outputs | ||
| ] | ||
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.