Skip to content

Commit dc7d1e3

Browse files
xieofxiehualxie
andauthored
fix: migrate EPAlias to EPNameOrAlias for better alignment; fix infer_ihv_from_ep_name (#1005)
## Summary Close #646 (migrate `EPAlias` to `EPName` where possible). `EPNameOrAlias` is the public-facing input type; `EPName` is the internal canonical form. This PR removes places where a bare alias was used where a canonical name belongs, and stops comparing against single alias literals. ### Changes - **`analyze/utils/ep_utils.py`** — `infer_ihv_from_ep_name` now does a direct, exact `dict[EPName, IHVType]` lookup covering every member of the `EPName` Literal. An unknown name now **raises `ValueError`** instead of silently defaulting to `IHVType.MICROSOFT` (the callers that matter already handle it: `InformationEngine` catches `ValueError` and falls back to loading all rules; the model validator catches it defensively). Also fixes a latent miss where `CUDAExecutionProvider` resolved to `MICROSOFT` instead of `NVIDIA`. - **`serve/schema.py`** — `EpSwitchRequest.ep` was the only `ep` field still typed bare `EPAlias`; every sibling is `EPNameOrAlias` and `manager.switch_ep` already accepts `EPNameOrAlias`. Widened it for consistency so it accepts both `"qnn"` and `"QNNExecutionProvider"`. - **`compiler/utils.py`** — `needs_format_conversion` compared `ep == "qnn"`, a single alias literal. An EP can have multiple aliases (e.g. `nv_tensorrt_rtx` / `nvtensorrtrtx`), and the canonical name itself wouldn't match. Now normalizes via `normalize_ep_name` and compares against the canonical `"QNNExecutionProvider"`; parameter widened to `EPNameOrAlias`. ### Notes - The compiler's `EPConfig.provider` deliberately remains alias-typed: every value it holds is an alias (`provider="qnn"`, etc.), so retyping to `EPName` would require a value migration with serialization back-compat — out of scope here. - An audit of all ~90 `EPNameOrAlias` call sites confirmed every one either normalizes before a canonical sink or forwards to a callee that does — no other un-normalized canonical-sink usages were found. ### Tests - Replaced the per-EP `infer_ihv` unit tests with one that verifies every `EP_NAMES` member resolves to an `IHVType` (enforcing map/Literal parity) plus an unknown-raises test. - Updated fixtures that relied on the old lenient behavior to use canonical EP names (`ACEExecutionProvider` → `VitisAIExecutionProvider`; alias `"openvino"` → `"OpenVINOExecutionProvider"` in the validator tests). - Added `test_qlinear_for_qnn_canonical_name` covering the canonical-name case that previously failed in `needs_format_conversion`. --------- Co-authored-by: hualxie <hualxie@microsoft.com>
1 parent ffc6d55 commit dc7d1e3

12 files changed

Lines changed: 99 additions & 105 deletions

File tree

src/winml/modelkit/analyze/core/information_engine.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,19 +117,24 @@ def __init__(
117117
self._device = device
118118

119119
# Load predefined information rules
120+
from ..models.ihv_type import IHVType
120121
from ..utils import infer_ihv_from_ep_name
121122
from ..utils.rule_loader import RuleLoader
122123

123124
self._rule_loader = RuleLoader(rules_dir=rules_dir)
124125

125-
# Infer IHV from EP name for per-IHV rule loading
126+
# Infer IHV from EP name for per-IHV rule loading. An unrecognized EP
127+
# resolves to IHVType.UNKNOWN, which we treat as "no IHV filter" so the
128+
# loader falls back to loading all rules.
126129
infer_ihv_start = time.perf_counter()
127-
try:
128-
ihv_type = infer_ihv_from_ep_name(self._ep)
129-
logger.info("Inferred IHV type %s from EP %s", ihv_type.value, self._ep)
130-
except ValueError as e:
131-
logger.warning("Could not infer IHV from EP %s: %s. Loading all rules.", self._ep, e)
130+
inferred_ihv = infer_ihv_from_ep_name(self._ep)
131+
ihv_type: IHVType | None
132+
if inferred_ihv is IHVType.UNKNOWN:
133+
logger.warning("Could not infer IHV from EP %s. Loading all rules.", self._ep)
132134
ihv_type = None
135+
else:
136+
logger.info("Inferred IHV type %s from EP %s", inferred_ihv.value, self._ep)
137+
ihv_type = inferred_ihv
133138
infer_ihv_ms = int((time.perf_counter() - infer_ihv_start) * 1000)
134139

135140
load_predefined_start = time.perf_counter()

src/winml/modelkit/analyze/core/model_validators/batched_const_matmul_validator.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,9 @@ def _is_enabled(self) -> bool:
5353
ep = self.ep
5454
if not ep:
5555
return False
56-
try:
57-
from ...models.ihv_type import IHVType
56+
from ...models.ihv_type import IHVType
5857

59-
return infer_ihv_from_ep_name(ep) == IHVType.INTEL
60-
except Exception: # pragma: no cover - defensive
61-
return False
58+
return infer_ihv_from_ep_name(ep) == IHVType.INTEL
6259

6360
def validate(self) -> Information | None:
6461
"""Detect batched MatMul with a single constant rank>=3 operand."""

src/winml/modelkit/analyze/models/ihv_type.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ class IHVType(str, Enum):
1515
AMD = "AMD"
1616
NVIDIA = "NVIDIA"
1717
MICROSOFT = "Microsoft"
18+
UNKNOWN = "Unknown"

src/winml/modelkit/analyze/utils/ep_utils.py

Lines changed: 29 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,65 +13,59 @@
1313
if TYPE_CHECKING:
1414
from pathlib import Path
1515

16-
from ...utils.constants import EPName
16+
from ...utils.constants import EPName, EPNameOrAlias
1717
from ..models.ihv_type import IHVType
1818

1919

2020
logger = logging.getLogger(__name__)
2121

2222

23-
def infer_ihv_from_ep_name(ep_name: EPName) -> IHVType:
24-
"""Infer IHVType from Execution Provider name.
23+
def infer_ihv_from_ep_name(ep_name: EPNameOrAlias) -> IHVType:
24+
"""Infer IHVType from an Execution Provider name or alias.
2525
26-
Maps an execution provider name to its corresponding IHV type.
27-
Supports multiple name variations for each provider.
28-
Unknown EPs (e.g., CPUExecutionProvider, DmlExecutionProvider) resolve
29-
to IHVType.MICROSOFT.
26+
Accepts either a canonical ``EPName`` or a shorthand ``EPAlias`` (e.g.
27+
``"openvino"``); aliases are normalized to their canonical name before the
28+
exact lookup, which covers every member of the canonical set. Names that
29+
are neither a known EP nor a known alias resolve to ``IHVType.UNKNOWN``
30+
rather than raising, so callers can treat inference as total.
3031
3132
Args:
32-
ep_name: Execution Provider name (e.g., QNNExecutionProvider, OpenVINOExecutionProvider)
33+
ep_name: Execution Provider name or alias (see ``utils.constants``).
3334
3435
Returns:
35-
IHVType: Inferred IHV type (QC, INTEL, AMD, NVIDIA, or MICROSOFT)
36+
IHVType: Inferred IHV type (QC, INTEL, AMD, NVIDIA, MICROSOFT, or
37+
UNKNOWN for unrecognized names).
3638
3739
Examples:
3840
>>> infer_ihv_from_ep_name("QNNExecutionProvider")
3941
<IHVType.QC: 'QC'>
40-
>>> infer_ihv_from_ep_name("OpenVINOExecutionProvider")
41-
<IHVType.INTEL: 'INTEL'>
42+
>>> infer_ihv_from_ep_name("openvino")
43+
<IHVType.INTEL: 'Intel'>
4244
>>> infer_ihv_from_ep_name("VitisAIExecutionProvider")
4345
<IHVType.AMD: 'AMD'>
4446
>>> infer_ihv_from_ep_name("NvTensorRTRTXExecutionProvider")
4547
<IHVType.NVIDIA: 'NVIDIA'>
4648
>>> infer_ihv_from_ep_name("CPUExecutionProvider")
4749
<IHVType.MICROSOFT: 'Microsoft'>
50+
>>> infer_ihv_from_ep_name("TotallyFakeEP")
51+
<IHVType.UNKNOWN: 'Unknown'>
4852
"""
53+
from ...utils.constants import normalize_ep_name
4954
from ..models.ihv_type import IHVType
5055

51-
ep_lower = ep_name.lower()
52-
53-
# QNN / Qualcomm
54-
if "qnn" in ep_lower or "qualcomm" in ep_lower:
55-
return IHVType.QC
56-
57-
# OpenVINO / Intel
58-
if "openvino" in ep_lower or "intel" in ep_lower:
59-
return IHVType.INTEL
60-
61-
# VitisAI / MIGraphX / AMD / ACE (AMD)
62-
amd_keywords = ("amd", "quark", "vitis", "ace", "migraphx")
63-
if any(kw in ep_lower for kw in amd_keywords):
64-
return IHVType.AMD
65-
66-
# NVIDIA / TensorRT RTX
67-
# This is intentionally a permissive substring fallback to cover common
68-
# TensorRT naming variants. Callers should prefer canonical EP names.
69-
nvidia_keywords = ("nvidia", "nvtensorrt", "trtrtx", "tensorrt", "rtx")
70-
if any(kw in ep_lower for kw in nvidia_keywords):
71-
return IHVType.NVIDIA
72-
73-
# Default: Microsoft (e.g., CPUExecutionProvider, DmlExecutionProvider)
74-
return IHVType.MICROSOFT
56+
ep_name_to_ihv: dict[EPName, IHVType] = {
57+
"QNNExecutionProvider": IHVType.QC,
58+
"OpenVINOExecutionProvider": IHVType.INTEL,
59+
"VitisAIExecutionProvider": IHVType.AMD,
60+
"MIGraphXExecutionProvider": IHVType.AMD,
61+
"NvTensorRTRTXExecutionProvider": IHVType.NVIDIA,
62+
"CUDAExecutionProvider": IHVType.NVIDIA,
63+
"CPUExecutionProvider": IHVType.MICROSOFT,
64+
"DmlExecutionProvider": IHVType.MICROSOFT,
65+
}
66+
67+
canonical = normalize_ep_name(ep_name)
68+
return ep_name_to_ihv.get(canonical, IHVType.UNKNOWN) # type: ignore[arg-type]
7569

7670

7771
def get_devices_with_rule_data(ep_name: EPName) -> list[str]:

src/winml/modelkit/compiler/utils.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,33 @@
1212
if TYPE_CHECKING:
1313
from pathlib import Path
1414

15-
from ..utils.constants import EPAlias
15+
from ..utils.constants import EPNameOrAlias
1616

1717
# Canonical definition of ONNX QDQ operator types.
1818
# Import this constant instead of redefining {"QuantizeLinear", "DequantizeLinear"}.
1919
QDQ_OP_TYPES: frozenset[str] = frozenset({"QuantizeLinear", "DequantizeLinear"})
2020

2121

22-
def needs_format_conversion(model_path: Path, ep: EPAlias) -> bool:
22+
def needs_format_conversion(model_path: Path, ep: EPNameOrAlias) -> bool:
2323
"""Check if model's quant format is compatible with target EP.
2424
2525
Minimal detection: checks for QLinear ops targeting QDQ-only EPs.
2626
FIXME: Expand to full EP-to-format compatibility matrix.
2727
"""
2828
from ..onnx import load_onnx
29+
from ..utils.constants import normalize_ep_name
2930

3031
model = load_onnx(model_path, load_weights=False, validate=False)
3132
op_types = {n.op_type for n in model.graph.node}
3233
has_qlinear = any(op.startswith("QLinear") for op in op_types)
3334
has_qdq = bool(op_types & QDQ_OP_TYPES)
3435

35-
if ep == "qnn" and has_qlinear and not has_qdq: # noqa: SIM103
36+
# Compare against the canonical EP name, not a single alias: one EP can have
37+
# several aliases (e.g. nv_tensorrt_rtx / nvtensorrtrtx), so an alias-literal
38+
# comparison would miss the others.
39+
ep_canonical = normalize_ep_name(ep)
40+
41+
if ep_canonical == "QNNExecutionProvider" and has_qlinear and not has_qdq: # noqa: SIM103
3642
return True # QNN requires QDQ format
3743
# FIXME: add more EP rules as needed
3844
return False

src/winml/modelkit/models/auto.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -363,19 +363,22 @@ def from_pretrained(
363363
if task is not None:
364364
from .winml.composite_model import COMPOSITE_MODEL_REGISTRY
365365

366-
_known_composite_tasks = {t for (_, t) in COMPOSITE_MODEL_REGISTRY}
367-
if task in _known_composite_tasks:
368-
from transformers import AutoConfig
369-
370-
_hf_cfg = AutoConfig.from_pretrained(model_id, trust_remote_code=trust_remote_code)
371-
_model_type = getattr(_hf_cfg, "model_type", None)
372-
else:
373-
_model_type = None
374-
375366
# Explicit override wins so a variant composite (e.g.
376367
# "qwen3_transformer_only") can be selected over the native type.
368+
_model_type: str | None
377369
if model_type is not None:
378370
_model_type = model_type
371+
else:
372+
_known_composite_tasks = {t for (_, t) in COMPOSITE_MODEL_REGISTRY}
373+
if task in _known_composite_tasks:
374+
from transformers import AutoConfig
375+
376+
_hf_cfg = AutoConfig.from_pretrained(
377+
model_id, trust_remote_code=trust_remote_code
378+
)
379+
_model_type = getattr(_hf_cfg, "model_type", None)
380+
else:
381+
_model_type = None
379382

380383
if _model_type is not None and (_model_type, task) in COMPOSITE_MODEL_REGISTRY:
381384
from .winml.composite_model import WinMLCompositeModel

src/winml/modelkit/serve/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,7 @@ async def get_mcp_schema() -> dict[str, Any]:
490490
# ------------------------------------------------------------------
491491
@app.post("/v1/ep", tags=["management"], summary="Switch execution provider")
492492
async def switch_ep(request: EpSwitchRequest) -> dict[str, Any]:
493-
# Pydantic already validates ep against the EPAlias Literal (rejects
493+
# Pydantic already validates ep against the EPNameOrAlias Literal (rejects
494494
# unknown values with a 422 at parse time), so no extra check needed.
495495
ep = request.ep
496496
mgr = _get_mgr()

src/winml/modelkit/serve/schema.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from pydantic import BaseModel, Field
1616

17-
from ..utils.constants import EPAlias, EPNameOrAlias
17+
from ..utils.constants import EPNameOrAlias
1818

1919

2020
# ---------------------------------------------------------------------------
@@ -25,7 +25,9 @@
2525
class EpSwitchRequest(BaseModel):
2626
"""POST /v1/ep — switch execution provider."""
2727

28-
ep: EPAlias = Field(..., description="EP short name: cpu, dml, qnn, openvino")
28+
ep: EPNameOrAlias = Field(
29+
..., description="EP name or short alias (e.g. cpu, dml, qnn, QNNExecutionProvider)"
30+
)
2931

3032

3133
class PredictJsonRequest(BaseModel):

tests/unit/analyze/core/test_output_aggregator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,7 @@ def test_full_workflow_multiple_ihv(self, sample_metadata: ModelStats) -> None:
510510
result=RuntimeTestResult(compile=True, run=True),
511511
),
512512
],
513-
"ACEExecutionProvider": [
513+
"VitisAIExecutionProvider": [
514514
PatternRuntime(
515515
pattern_id="OP/ai.onnx/Add",
516516
result=RuntimeTestResult(compile=False, run=False),
@@ -521,7 +521,7 @@ def test_full_workflow_multiple_ihv(self, sample_metadata: ModelStats) -> None:
521521
information_list = {
522522
"QNNExecutionProvider": [],
523523
"OpenVINOExecutionProvider": [],
524-
"ACEExecutionProvider": [
524+
"VitisAIExecutionProvider": [
525525
Information(
526526
explanation="Add not supported",
527527
pattern_id="OP/ai.onnx/Add",

tests/unit/analyze/test_analyzer.py

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
ModelStats,
2424
ONNXStaticAnalyzer,
2525
SupportLevel,
26-
infer_ihv_from_ep_name,
2726
)
2827
from winml.modelkit.analyze.analyzer import _build_runtime_debug_details_summary
2928
from winml.modelkit.analyze.models.runtime_checks import PatternRuntime, RuntimeTestResult
@@ -864,34 +863,6 @@ def test_init_custom_config(self) -> None:
864863
assert analyzer.config.enable_information is True
865864
assert analyzer.config.max_memory_mb == 4096
866865

867-
def test_map_ep_to_ihv_qnn(self) -> None:
868-
"""Test EP to IHV mapping for QNN."""
869-
assert infer_ihv_from_ep_name("QNNExecutionProvider") == IHVType.QC
870-
assert infer_ihv_from_ep_name("qnnexecutionprovider") == IHVType.QC
871-
assert infer_ihv_from_ep_name("QualcommProvider") == IHVType.QC
872-
873-
def test_map_ep_to_ihv_openvino(self) -> None:
874-
"""Test EP to IHV mapping for OpenVINO."""
875-
assert infer_ihv_from_ep_name("OpenVINOExecutionProvider") == IHVType.INTEL
876-
assert infer_ihv_from_ep_name("openvino") == IHVType.INTEL
877-
assert infer_ihv_from_ep_name("IntelProvider") == IHVType.INTEL
878-
879-
def test_map_ep_to_ihv_vitisai(self) -> None:
880-
"""Test EP to IHV mapping for VitisAI."""
881-
assert infer_ihv_from_ep_name("VitisAIExecutionProvider") == IHVType.AMD
882-
assert infer_ihv_from_ep_name("vitis") == IHVType.AMD
883-
assert infer_ihv_from_ep_name("AMDProvider") == IHVType.AMD
884-
885-
def test_map_ep_to_ihv_nvidia(self) -> None:
886-
"""Test EP to IHV mapping for NvTensorRTRTX."""
887-
assert infer_ihv_from_ep_name("NvTensorRTRTXExecutionProvider") == IHVType.NVIDIA
888-
assert infer_ihv_from_ep_name("nvtensorrtx") == IHVType.NVIDIA
889-
assert infer_ihv_from_ep_name("TensorRTProvider") == IHVType.NVIDIA
890-
891-
def test_map_ep_to_ihv_invalid(self) -> None:
892-
"""Test EP to IHV mapping with unrecognized EP resolves to MICROSOFT."""
893-
assert infer_ihv_from_ep_name("InvalidEP") == IHVType.MICROSOFT
894-
895866
def test_analyze_file_not_found(self) -> None:
896867
"""Test analyze with non-existent file."""
897868
analyzer = ONNXStaticAnalyzer()

0 commit comments

Comments
 (0)