Skip to content

Commit 324407d

Browse files
Improve evaluation display and optimize analyze performance
1 parent ef920f7 commit 324407d

20 files changed

Lines changed: 261 additions & 60 deletions

File tree

scripts/e2e_eval/run_eval.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
compute_delta,
5252
derive_verdict,
5353
derive_verdicts,
54+
format_delta,
5455
)
5556
from utils.dataset_config import get_dataset_config, register_from_registry
5657
from utils.registry import ModelEntry, filter_registry, load_registry, make_adhoc_entry
@@ -1264,8 +1265,9 @@ def main() -> None:
12641265
acc_tag = f" acc=SKIP/{accuracy_result['skip_reason']}"
12651266
else:
12661267
verdict = derive_verdict(accuracy_result).value
1267-
delta_rel = accuracy_result.get("delta_relative")
1268-
delta_str = f" {delta_rel:.1%}" if delta_rel is not None else ""
1268+
delta_str = format_delta(accuracy_result)
1269+
if delta_str:
1270+
delta_str = f" {delta_str}"
12691271
acc_tag = f" acc={verdict}{delta_str}"
12701272

12711273
if perf_proc is not None:

scripts/e2e_eval/utils/accuracy.py

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ def compute_delta(
6363
Returns (None, None) if either is missing or baseline value is zero.
6464
6565
Note: For error-rate metrics (WER — lower is better) a positive delta
66-
means the WinML pipeline is *worse*. The threshold in derive_verdict()
67-
uses abs(delta_relative) to handle both directions uniformly.
66+
means the WinML pipeline is *worse*. derive_verdict() normalizes the
67+
sign using higher_is_better from METRIC_COMPARE_STRATEGY.
6868
"""
6969
if winml_metric is None or baseline_metric is None:
7070
return None, None
@@ -78,6 +78,24 @@ def compute_delta(
7878
return round(delta_abs, 6), round(delta_abs / base_val, 6)
7979

8080

81+
def format_delta(accuracy: dict) -> str:
82+
"""Format the comparison delta as a display string using the metric strategy.
83+
84+
Returns e.g. ``"-6.0%"`` for relative metrics or ``"-1.27"`` for absolute.
85+
Returns ``""`` if delta is unavailable.
86+
"""
87+
metric_name = (accuracy.get("dataset_config") or {}).get("metric")
88+
delta_key = METRIC_COMPARE_STRATEGY.get(
89+
metric_name, METRIC_COMPARE_STRATEGY["default"]
90+
)[0]
91+
d = accuracy.get(delta_key)
92+
if d is None:
93+
return ""
94+
if delta_key == "delta_absolute":
95+
return f"{d:+.2f}"
96+
return f"{d:.1%}"
97+
98+
8199
# ---------------------------------------------------------------------------
82100
# Verdict derivation (always from stored facts, never from a stored verdict)
83101
# ---------------------------------------------------------------------------
@@ -214,10 +232,6 @@ def _val(acc: dict, key: str) -> str:
214232
v = (acc.get(key) or {}).get("value")
215233
return f"{v:.4f}" if isinstance(v, float) else str(v) if v is not None else "N/A"
216234

217-
def _pct(acc: dict) -> str:
218-
d = acc.get("delta_relative")
219-
return f"{d:.1%}" if d is not None else "N/A"
220-
221235
lines = [
222236
"# Accuracy Evaluation Summary",
223237
"",
@@ -256,15 +270,15 @@ def _pct(acc: dict) -> str:
256270
lines += ["", "## Accuracy Regressions", ""]
257271
if regressions:
258272
lines += [
259-
"| Model | Task | WinML | Baseline | Delta% |",
260-
"|-------|------|-----|----------|--------|",
273+
"| Model | Task | WinML | Baseline | Delta |",
274+
"|-------|------|-------|----------|-------|",
261275
]
262276
for r in regressions:
263277
acc = r["accuracy"]
264278
lines.append(
265279
f"| {r['model']} | {r.get('task', '')} "
266280
f"| {_val(acc, 'winml_metric')} | {_val(acc, 'pytorch_baseline_metric')} "
267-
f"| {_pct(acc)} |"
281+
f"| {format_delta(acc)} |"
268282
)
269283
else:
270284
lines.append("_No regressions._")
@@ -277,15 +291,15 @@ def _pct(acc: dict) -> str:
277291
lines += ["", "## At-Risk Models", ""]
278292
if at_risk:
279293
lines += [
280-
"| Model | Task | WinML | Baseline | Delta% |",
281-
"|-------|------|-----|----------|--------|",
294+
"| Model | Task | WinML | Baseline | Delta |",
295+
"|-------|------|-------|----------|-------|",
282296
]
283297
for r in at_risk:
284298
acc = r["accuracy"]
285299
lines.append(
286300
f"| {r['model']} | {r.get('task', '')} "
287301
f"| {_val(acc, 'winml_metric')} | {_val(acc, 'pytorch_baseline_metric')} "
288-
f"| {_pct(acc)} |"
302+
f"| {format_delta(acc)} |"
289303
)
290304
else:
291305
lines.append("_No at-risk models._")

scripts/e2e_eval/utils/reporter.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@ def generate_html_report(
316316
output_path: Path,
317317
registry_path: Path | None = None,
318318
) -> None:
319+
from .accuracy import format_delta
319320
"""Generate interactive HTML report with Perf and Accuracy tabs."""
320321
results = report_data.get("results", [])
321322

@@ -356,8 +357,8 @@ def generate_html_report(
356357
if acc is not None
357358
else None
358359
),
359-
"delta_relative": (
360-
acc.get("delta_relative") if acc and not acc.get("skipped") else None
360+
"delta_display": (
361+
format_delta(acc) if acc and not acc.get("skipped") else ""
361362
),
362363
}
363364
)
@@ -510,11 +511,9 @@ def generate_html_report(
510511
return n.toLocaleString();
511512
}}
512513
function formatDate(iso) {{ return iso ? new Date(iso).toLocaleDateString('en-CA') : '-'; }}
513-
function formatDelta(v) {{
514-
if (v == null) return '-';
515-
const pct = (v * 100).toFixed(1);
516-
const cls = Math.abs(v) < 0.05 ? 'badge-acc-pass' : Math.abs(v) < 0.10 ? 'badge-acc-risk' : 'badge-acc-reg'; // yellow ≥5%, red ≥10%
517-
return `<span class="badge ${{cls}}">${{pct}}%</span>`;
514+
function formatDelta(display, verdictCls) {{
515+
if (!display) return '-';
516+
return `<span class="badge ${{verdictCls}}">${{display}}</span>`;
518517
}}
519518
520519
let filters = {{
@@ -628,7 +627,7 @@ def generate_html_report(
628627
<td class="col-sep">${{d.accuracy_verdict
629628
? `<span class="badge ${{verdictCls}}">${{d.accuracy_verdict}}</span>`
630629
: '<span style="color:var(--text2);font-size:11px">N/A</span>'}}</td>
631-
<td>${{formatDelta(d.delta_relative)}}</td>` : '';
630+
<td>${{formatDelta(d.delta_display, verdictCls)}}</td>` : '';
632631
return `<tr${{errTip ? ` title="${{errTip}}"` : ''}}>
633632
<td><a class="hf-link" href="https://huggingface.co/${{d.hf_id}}" target="_blank">${{d.hf_id}}</a></td>
634633
<td><span class="badge badge-task">${{d.task||'-'}}</span></td>

src/winml/modelkit/_warnings.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,29 @@ def filter(self, record: logging.LogRecord) -> bool:
4848
_DiffusersDistributionFilter()
4949
)
5050

51+
class _HFPipelineFalsePositiveFilter(logging.Filter):
52+
"""Filter false-positive HF pipeline warnings when using WinML models.
53+
54+
HF pipeline emits these because WinMLModel wraps ONNX via ORT, not a
55+
native HF model class. These are expected and not actionable.
56+
"""
57+
58+
_FALSE_POSITIVES = (
59+
"WinMLModel", # False positive warning which says WinML is not native HF model class
60+
"Device set to use", # PyTorch tensor device, not ONNX device
61+
"Using a slow image processor", # expected when using processor with pipeline.
62+
)
63+
64+
def filter(self, record: logging.LogRecord) -> bool:
65+
msg = record.getMessage()
66+
return not any(phrase in msg for phrase in self._FALSE_POSITIVES)
67+
68+
for _name in (
69+
"transformers.pipelines.base",
70+
"transformers.models.auto.image_processing_auto",
71+
):
72+
logging.getLogger(_name).addFilter(_HFPipelineFalsePositiveFilter())
73+
5174
# =========================================================================
5275
# Warning filters (for warnings.warn() calls)
5376
# =========================================================================

src/winml/modelkit/analyze/analyzer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,7 @@ def analyze_from_proto(
727727
ep=current_ep,
728728
model=onnx_model,
729729
device=device_to_use,
730+
shape_inferred_model_proto=runtime_checker.get_shape_inferred_model_proto(),
730731
)
731732
information_list[current_ep] = engine.summary() # Use EP name as key
732733

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,26 @@ class DocConstraintChecker:
6767
# Add more checker functions here as they are implemented
6868
}
6969

70-
def __init__(self, model_proto: onnx.ModelProto, ep_name: str, device_type: str) -> None:
70+
def __init__(
71+
self,
72+
model_proto: onnx.ModelProto,
73+
ep_name: str,
74+
device_type: str,
75+
skip_shape_inference: bool = False,
76+
) -> None:
7177
"""Initialize doc constraint checker.
7278
7379
Args:
7480
model_proto: ONNX model proto
7581
ep_name: Execution provider name (e.g., "QNNExecutionProvider")
7682
device_type: Device type (e.g., "NPU")
83+
skip_shape_inference: If True, assume model_proto already has shape
84+
inference applied (avoids expensive redundant inference).
7785
"""
78-
self.model_proto = shape_inference.infer_shapes(model_proto)
86+
if skip_shape_inference:
87+
self.model_proto = model_proto
88+
else:
89+
self.model_proto = shape_inference.infer_shapes(model_proto)
7990
self.ep_name = ep_name
8091
self.device_type = device_type
8192
self.valueinfo = collect_valueinfo_dict(self.model_proto)

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
if TYPE_CHECKING:
1919
from pathlib import Path
2020

21+
import onnx
22+
2123
from ..models.information import Action, Information
2224
from ..models.onnx_model import ONNXModel
2325
from ..models.runtime_checks import PatternRuntime
@@ -66,6 +68,7 @@ def __init__(
6668
model: ONNXModel,
6769
device: str,
6870
rules_dir: Path | None = None,
71+
shape_inferred_model_proto: onnx.ModelProto | None = None,
6972
) -> None:
7073
"""Initialize information engine.
7174
@@ -77,6 +80,8 @@ def __init__(
7780
model: Optional ONNX model for model-level validation.
7881
If provided, model-level validation checks will be run.
7982
device: Device type (e.g., "NPU", "GPU", "CPU") for device-specific validation.
83+
shape_inferred_model_proto: Pre-inferred model proto to avoid redundant
84+
shape inference. If provided, DocConstraintChecker will reuse it.
8085
8186
Implementation:
8287
- Stores operator and subgraph runtime results separately
@@ -128,10 +133,17 @@ def __init__(
128133
try:
129134
from .doc_constraint_checker import DocConstraintChecker
130135

131-
# Get model proto from ONNXModel
132-
model_proto = self._model.get_model()
136+
# Prefer the pre-inferred model proto to avoid redundant shape inference
137+
if shape_inferred_model_proto is not None:
138+
model_proto = shape_inferred_model_proto
139+
skip_inference = True
140+
else:
141+
model_proto = self._model.get_model()
142+
skip_inference = False
133143

134-
self._doc_checker = DocConstraintChecker(model_proto, ep, self._device)
144+
self._doc_checker = DocConstraintChecker(
145+
model_proto, ep, self._device, skip_shape_inference=skip_inference
146+
)
135147
logger.info(
136148
"Initialized Doc Constraint Checker with %d operators",
137149
len(self._doc_checker.get_operators_with_constraints()),

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,12 @@ def _get_query(self) -> RuntimeCheckerQuery:
142142

143143
return self._query
144144

145+
def get_shape_inferred_model_proto(self) -> onnx.ModelProto | None:
146+
"""Return the shape-inferred model proto from the cached query, if available."""
147+
if self._query is not None:
148+
return self._query.model_proto
149+
return None
150+
145151
def op_support(
146152
self,
147153
run_unknown_op: bool = True,

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,9 @@ def _sanitize_df(df: pd.DataFrame) -> pd.DataFrame:
109109
Apply make_hashable to convert lists/dicts to tuples.
110110
"""
111111
for col in df.columns:
112-
# Make values hashable (lists -> tuples, floats -> DUMMY_FLOAT)
113-
df[col] = df[col].apply(make_hashable)
112+
# Bypass pandas .apply() overhead — iterate directly over the numpy array.
113+
raw = df[col].to_numpy()
114+
df[col] = [make_hashable(v) for v in raw]
114115
return df
115116

116117

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
from pathlib import Path
99
from typing import Any
1010

11-
from jsonschema import validate
12-
1311

1412
def validate_json_schema(data: dict[str, Any], schema_path: Path) -> bool:
1513
"""Validate JSON data against a JSON schema file.
@@ -30,6 +28,8 @@ def validate_json_schema(data: dict[str, Any], schema_path: Path) -> bool:
3028

3129
schema = json.loads(schema_path.read_text(encoding="utf-8"))
3230

31+
from jsonschema import validate
32+
3333
validate(instance=data, schema=schema)
3434
return True
3535

0 commit comments

Comments
 (0)