Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/naming-convention.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Domain acronyms in PascalCase class names **retain their uppercase form**, excep
| Acronym | Meaning | Class Casing | Example |
|---------|---------|--------------|---------|
| ONNX | Open Neural Network Exchange | `ONNX` | `ONNXStaticAnalyzer`, `ONNXLoader` |
| EP | Execution Provider | `EP` | `EPChecker`, `EPConfig`, `EPMonitor` |
| EP | Execution Provider | `EP` | `EPChecker`, `EPConfig`, `WinMLEPMonitor` |
| QDQ | Quantize-Dequantize | `QDQ` | `QDQParameterConfig`, `QDQGenerator` |
| QNN | Qualcomm Neural Network | `QNN` | `QNNMonitor` |
| Op | Operator (2-letter prefix) | `Op` | `OpUnsupportedError` |
Expand Down
7 changes: 7 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,13 @@ module = [
# Not a dependency of the main/CI environment, so it has no stubs here.
"qairt",
"qairt.*",
# WinUI3 / Windows App SDK bootstrap, WinRT projections, and pywin32 — Windows
# runtime packages imported in ep_path.py / ep_device.py. Installed and usable
# at runtime but ship no type stubs, so treat as untyped here.
"winui3",
"winui3.*",
"winrt.*",
"win32api",
]
ignore_missing_imports = true

Expand Down
10 changes: 8 additions & 2 deletions src/winml/modelkit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@

logging.getLogger(__name__).addHandler(logging.NullHandler())


def _preload_bundled_onnxruntime_dll() -> None:
# Windows ships C:\Windows\System32\onnxruntime.dll (older API version)
# as part of the system WindowsML component. When WinML EP plugin DLLs
Expand Down Expand Up @@ -74,7 +73,14 @@ def _preload_bundled_onnxruntime_dll() -> None:

_preload_bundled_onnxruntime_dll()

from . import _warnings # Configure warning filters before importing subpackages
# _warnings configures filters before any subpackage imports.
# transformers_compat arms a sys.meta_path hook — the shim fires lazily
# the first time anything imports optimum.*; lightweight commands
# (``winml sys``, ``winml --help``) never pay the transformers cost.
from . import _warnings # noqa: I001
from . import transformers_compat

transformers_compat.arm()


try:
Expand Down
29 changes: 23 additions & 6 deletions src/winml/modelkit/_warnings.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import logging
import os
import sys
import warnings

from ._env import env_flag_enabled
Expand Down Expand Up @@ -91,12 +92,15 @@ def filter(self, record: logging.LogRecord) -> bool:
for _cat in (FutureWarning, DeprecationWarning, UserWarning):
warnings.filterwarnings("ignore", category=_cat, module=r"torch\..*")

# NOTE: TracerWarning (from torch.jit) is intentionally NOT filtered here.
# Importing torch.jit at startup would pull all of torch (~1.7s) into
# `winml --help` and violate the CLI import budget (tests/cli/test_import_time.py).
# During ONNX export, export_pytorch() wraps torch.onnx.export in
# `warnings.catch_warnings()` + `filterwarnings("ignore")`, which is strictly
# broader than a TracerWarning-only filter.
# TracerWarning (from torch.jit, inherits Warning not UserWarning)
# fires during ONNX export tracing — safe to suppress in both torch and
# transformers. Only register the filter if torch has ALREADY been
# imported; otherwise loading torch here would add ~2s to every
# lightweight command (``winml sys`` etc.) that never touches ONNX
# export. The export path re-triggers this by calling
# :func:`install_torch_tracer_filter` after loading torch.
if "torch" in sys.modules:
install_torch_tracer_filter()

# Diffusers
warnings.filterwarnings(
Expand Down Expand Up @@ -140,5 +144,18 @@ def filter(self, record: logging.LogRecord) -> bool:
logging.getLogger("transformers.modeling_utils").addFilter(_TransformersWeightsFilter())


def install_torch_tracer_filter() -> None:
"""Register the ``TracerWarning`` filter — call after ``import torch``.

Idempotent — ``warnings.filterwarnings`` de-duplicates identical entries.
"""
try:
# torch.jit exposes TracerWarning at runtime but its stubs don't export it.
from torch.jit import TracerWarning # type: ignore[attr-defined]
except ImportError:
return # torch not installed
warnings.filterwarnings("ignore", category=TracerWarning)


# Auto-configure on import
_configure()
65 changes: 35 additions & 30 deletions src/winml/modelkit/analyze/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
import time
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, cast

from ..optim.config import WinMLOptimizationConfig
from ..utils.constants import EP_SUPPORTED_DEVICES, EPName, EPNameOrAlias, normalize_ep_name
from ..utils.constants import normalize_ep_name
from .models.information import Information
from .models.output import RuntimeDebugSummaryEntry
from .models.support_level import SupportLevel
Expand All @@ -30,6 +30,7 @@

import onnx

from ..utils.constants import EPName, EPNameOrAlias

Check notice

Code scanning / CodeQL

Unused import Note

Import of 'EPNameOrAlias' is not used.
from .models.information import Action
from .models.output import AnalysisOutput
from .models.runtime_checks import PatternRuntime, RuntimeTestResult
Expand Down Expand Up @@ -186,7 +187,7 @@
pattern_count = sum(self.output.metadata.detected_pattern_count.values())
return f"AnalysisResult(patterns={pattern_count})"

def is_fully_supported(self, ep: EPNameOrAlias | None = None) -> bool:
def is_fully_supported(self, ep: str | None = None) -> bool:
"""Check if model is fully supported on the target EP and device.

Args:
Expand Down Expand Up @@ -214,7 +215,7 @@
return False

# Normalize EP if specified
ep_normalized = normalize_ep_name(ep) if ep else None
ep_normalized = normalize_ep_name(cast("EPNameOrAlias", ep)) if ep else None

# Track if we found the target EP when filtering
found_target = ep_normalized is None # True if not filtering
Expand All @@ -227,7 +228,7 @@
return False
return found_target

def has_errors(self, ep: EPNameOrAlias | None = None) -> bool:
def has_errors(self, ep: str | None = None) -> bool:
"""Check if there are any unsupported patterns (blocking errors).

Args:
Expand All @@ -251,7 +252,7 @@
return False

# Normalize EP if specified
ep_normalized = normalize_ep_name(ep) if ep else None
ep_normalized = normalize_ep_name(cast("EPNameOrAlias", ep)) if ep else None

for ep_support in self.output.results:
if ep_normalized and ep_support.ep_type != ep_normalized:
Expand All @@ -260,7 +261,7 @@
return True
return False

def has_warnings(self, ep: EPNameOrAlias | None = None) -> bool:
def has_warnings(self, ep: str | None = None) -> bool:
"""Check if there are any partial patterns (warnings/optimization opportunities).

Args:
Expand All @@ -284,7 +285,7 @@
return False

# Normalize EP if specified
ep_normalized = normalize_ep_name(ep) if ep else None
ep_normalized = normalize_ep_name(cast("EPNameOrAlias", ep)) if ep else None

for ep_support in self.output.results:
if ep_normalized and ep_support.ep_type != ep_normalized:
Expand All @@ -293,7 +294,7 @@
return True
return False

def get_lint_result(self, ep: EPNameOrAlias | None = None) -> LintResult:
def get_lint_result(self, ep: str | None = None) -> LintResult:
"""Get lint-style result with error/warning/info counts.

Args:
Expand Down Expand Up @@ -332,7 +333,7 @@
)

# Normalize EP if specified
ep_normalized = normalize_ep_name(ep) if ep else None
ep_normalized = normalize_ep_name(cast("EPNameOrAlias", ep)) if ep else None

# Aggregate counts and lists
error_patterns: list[str] = []
Expand Down Expand Up @@ -374,7 +375,7 @@
optimization_config=optimization_config,
)

def get_unsupported_operators(self, ep: EPNameOrAlias | None = None) -> list[str]:
def get_unsupported_operators(self, ep: str | None = None) -> list[str]:
"""Get list of unsupported operators for the target EP and device.

Args:
Expand All @@ -395,7 +396,7 @@
... print(f"Unsupported: {op_name}")
"""
# Normalize EP if specified
ep_normalized = normalize_ep_name(ep) if ep else None
ep_normalized = normalize_ep_name(cast("EPNameOrAlias", ep)) if ep else None

unsupported = []
for ep_support in self.output.results:
Expand All @@ -409,7 +410,7 @@

return unsupported

def get_optimization_opportunities(self, ep: EPNameOrAlias | None = None) -> list[Action]:
def get_optimization_opportunities(self, ep: str | None = None) -> list[Action]:
"""Get actions for patterns that could be optimized (UNSUPPORTED or PARTIAL status).

Args:
Expand All @@ -432,7 +433,7 @@
... print(f"Optimize: {action.pattern_from_id} -> {action.action}")
"""
# Normalize EP if specified
ep_normalized = normalize_ep_name(ep) if ep else None
ep_normalized = normalize_ep_name(cast("EPNameOrAlias", ep)) if ep else None

actions: list[Action] = []
seen_patterns: set[tuple[str, str]] = set()
Expand All @@ -452,7 +453,7 @@
seen_patterns.add(pattern_key)
return actions

def get_optimization_config(self, ep: EPNameOrAlias | None = None) -> WinMLOptimizationConfig:
def get_optimization_config(self, ep: str | None = None) -> WinMLOptimizationConfig:
"""Generate WinML optimization configuration based on action items.

This method extracts optimization settings from action_items in Actions,
Expand Down Expand Up @@ -594,7 +595,7 @@
def analyze(
self,
model_path: str,
ep: EPNameOrAlias | None = None,
ep: str | None = None,
device: str | None = None,
enable_information: bool = True,
htp_metadata_path: str | None = None,
Expand Down Expand Up @@ -675,7 +676,7 @@
total_start = time.perf_counter()

# Normalize EP name (convert aliases to full names)
ep_normalized = normalize_ep_name(ep)
ep_normalized = normalize_ep_name(cast("EPNameOrAlias | None", ep))
if ep != ep_normalized:
logger.debug("EP alias '%s' normalized to '%s'", ep, ep_normalized)

Expand Down Expand Up @@ -728,7 +729,7 @@
def analyze_from_proto(
self,
model_proto: onnx.ModelProto,
ep: EPNameOrAlias | None = None,
ep: str | None = None,
device: str | None = None,
enable_information: bool = True,
model_path: str | None = None,
Expand Down Expand Up @@ -785,18 +786,17 @@

# Normalize EP name (convert aliases to full names)
total_start = time.perf_counter()
ep_normalized = normalize_ep_name(ep)
ep_normalized = normalize_ep_name(cast("EPNameOrAlias | None", ep))
if ep != ep_normalized:
logger.debug("EP alias '%s' normalized to '%s'", ep, ep_normalized)

logger.info("Analyzing model from ModelProto")

# Resolve device — rule files are device-specific (CPU/GPU/NPU).
if device is not None and device.lower() == "auto":
from ..sysinfo import resolve_device
from ..session import auto_detect_device

resolved, _ = resolve_device("auto", ep=ep_normalized)
device_to_use = resolved.upper()
device_to_use = auto_detect_device().upper()
logger.info("Device 'auto' resolved to: %s", device_to_use)
else:
device_to_use = device if device is not None else "NPU"
Expand All @@ -805,13 +805,18 @@
# Determine which EPs to analyze
eps_to_analyze: list[EPName] = []
if ep_normalized is None:
# Analyze all EPs that support the target device
eps_to_analyze = [
ep_name
for ep_name, supported_devices in EP_SUPPORTED_DEVICES.items()
if device_to_use.lower() in supported_devices
]
logger.info("No EP specified, analyzing all supported EPs: %s", eps_to_analyze)
# Derive the EP list from the catalog so future EP additions
# are automatically included. sorted() gives deterministic order.
from ..session import eps_for_device

# eps_for_device returns EP full names as ``str``; they are members of
# the ``EPName`` Literal by construction (catalog parity is test-enforced).
eps_to_analyze = cast("list[EPName]", sorted(eps_for_device(device_to_use.lower())))
logger.info(
"No EP specified, analyzing all %s-capable EPs: %s",
device_to_use,
eps_to_analyze,
)
else:
eps_to_analyze = [ep_normalized]

Expand Down Expand Up @@ -964,7 +969,7 @@
def analyze_onnx(
model: str | Path,
*,
ep: EPNameOrAlias | None = None,
ep: str | None = None,
device: str | None = None,
autoconf: bool = True,
run_unknown_op: bool = False,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@


if TYPE_CHECKING:
from ....utils.constants import EPName
from ...models.information import Information
from ...models.onnx_model import ONNXModel
from ...models.runtime_checks import PatternRuntime
Expand Down Expand Up @@ -78,8 +77,8 @@ def __init__(
enabled_validators: list[str] | None = None,
op_runtime_results: list[PatternRuntime] | None = None,
*,
device: str,
ep: EPName,
device: str | None = None,
ep: str | None = None,
) -> None:
"""Initialize validator manager.

Expand All @@ -100,7 +99,7 @@ def __init__(
self.model = model
self.model_proto = model.get_model()
self.op_runtime_results = op_runtime_results or []
self.device = device
self.device = device or "NPU"
self.ep = ep
self.enabled_validators = enabled_validators or list(self.VALIDATORS.keys())

Expand Down
Loading
Loading