Skip to content
Merged
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
102 changes: 43 additions & 59 deletions scripts/e2e_eval/run_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,7 @@ def _is_eval_target_available(ep: str | None, device: str | None) -> bool:
raise WinMLEPNotDiscovered(f"No locally installed EP found for {target.ep}.")
registry.auto_device(target)
except (DeviceNotFound, WinMLEPNotDiscovered, WinMLEPRegistrationFailed) as exc:
safe_print(
f"[SKIP] {target.ep}/{target.device} is not available on this machine: {exc}"
)
safe_print(f"[SKIP] {target.ep}/{target.device} is not available on this machine: {exc}")
return False
return True

Expand Down Expand Up @@ -901,6 +899,7 @@ def _run_subprocess(args: list[str], timeout: int) -> dict:
(log_dir / "stderr.log").open("w+b") as stderr_file,
(log_dir / "events.jsonl").open("a", encoding="utf-8") as events_file,
):

def report(event: str, message: str, **details) -> None:
record = {
"timestamp": _utc_now(),
Expand Down Expand Up @@ -976,9 +975,8 @@ def report(event: str, message: str, **details) -> None:
last_download_progress = snapshot["last_progress"]
download_active = snapshot["active"]
download_state_known = True
if (
now - last_download_observation >= _HF_DOWNLOAD_MONITOR_TIMEOUT
or (monitor is not None and monitor.poll() is not None)
if now - last_download_observation >= _HF_DOWNLOAD_MONITOR_TIMEOUT or (
monitor is not None and monitor.poll() is not None
):
download_monitor_stalled = True
download_active = False
Expand Down Expand Up @@ -1022,9 +1020,12 @@ def report(event: str, message: str, **details) -> None:
last_output_progress = now
last_output_sizes = output_sizes
monitor_state = (
"disabled" if download_monitor_stalled
else "starting" if not download_state_known
else "downloading" if download_active
"disabled"
if download_monitor_stalled
else "starting"
if not download_state_known
else "downloading"
if download_active
else "idle"
)
execution_remaining = max(0.0, timeout - execution_elapsed)
Expand Down Expand Up @@ -1053,8 +1054,10 @@ def report(event: str, message: str, **details) -> None:
else f"execution timeout ({timeout:g}s)"
)
report(
"timeout", reason,
timeout=timed_out, hf_download_stalled=hf_download_stalled,
"timeout",
reason,
timeout=timed_out,
hf_download_stalled=hf_download_stalled,
)
exit_code = -1
break
Expand Down Expand Up @@ -1452,32 +1455,32 @@ def _run_recipe_build(


def _extract_onnx_path(build_proc: dict, hf_id: str, task: str | None) -> str | None:
"""Extract ONNX path from build subprocess output."""
"""Extract the final or reused ONNX path from build subprocess output."""
# Rich may wrap a long artifact path across physical output lines. Rejoin
# those fragments before falling back to cache discovery.
markers = ("Final artifact:", "Existing artifact found:", "Artifact:")
markers = ("Final artifact:", "Existing artifact found:")
output = re.sub(
r"\x1b\[[0-?]*[ -/]*[@-~]",
"",
build_proc["stderr"] + build_proc["stdout"],
"\n".join((build_proc["stderr"], build_proc["stdout"])),
)
lines = output.splitlines()
for index, line in enumerate(lines):
for marker in markers:
for marker in markers:
for index, line in enumerate(lines):
if marker not in line:
continue
fragments = [line.split(marker, 1)[1].strip()]
for continuation in lines[index + 1 : index + 11]:
candidate = "".join(fragments)
if candidate and Path(candidate).is_file():
return candidate
if candidate.lower().endswith(".onnx"):
break
fragments.append(continuation.strip())

candidate = "".join(fragments)
if candidate and Path(candidate).is_file():
return candidate
fragments = [line.split(marker, 1)[1], *lines[index + 1 : index + 11]]
candidate = ""
for fragment in fragments:
candidate += fragment.strip()
if not candidate.lower().endswith(".onnx"):
continue
try:
if Path(candidate).is_file():
return candidate
except (OSError, ValueError) as exc:
logging.debug("Skipping invalid ONNX candidate path %r: %s", candidate, exc)
break

return _find_cached_model(hf_id, build_proc, task)

Expand Down Expand Up @@ -2223,11 +2226,7 @@ def _record(


def _is_finite_number(value: object) -> bool:
return (
isinstance(value, (int, float))
and not isinstance(value, bool)
and math.isfinite(value)
)
return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value)


def _validate_single_perf_result(result: dict, context: str = "result") -> str | None:
Expand Down Expand Up @@ -2378,9 +2377,7 @@ def _single_physical_dml_gpu_luid() -> str:
if adapter.device_type == "GPU"
}
if len(native_gpus) != 1:
raise RuntimeError(
f"DML CI pin requires exactly one DXCore GPU; found {list(native_gpus)}"
)
raise RuntimeError(f"DML CI pin requires exactly one DXCore GPU; found {list(native_gpus)}")
luid, adapter = next(iter(native_gpus.items()))
selected = WinMLEPRegistry.instance().auto_device(EPDeviceTarget(ep="dml", device="gpu"))
advertised = {
Expand Down Expand Up @@ -2856,9 +2853,7 @@ def _run_update_baseline(entries: list[ModelEntry], args: argparse.Namespace) ->
ds_config = get_dataset_config(entry.hf_id, entry.task) or {}
cached = _lookup_baseline_cache(entry.hf_id, entry.task, ds_config)
if cached is not None and not args.retry_failed:
safe_print(
f"{_progress_prefix(i, len(entries))} {label} (cached {cached['metric']})"
)
safe_print(f"{_progress_prefix(i, len(entries))} {label} (cached {cached['metric']})")
continue

safe_print(f"{_progress_prefix(i, len(entries))} {label} running baseline ...")
Expand Down Expand Up @@ -3026,9 +3021,7 @@ def _matches_hf_fetch_retry(existing: dict) -> bool:
accuracy = existing.get("accuracy") or {}
perf_failed = bool(perf) and not perf.get("passed")
accuracy_failed = (
bool(accuracy)
and not accuracy.get("skipped")
and accuracy_status(accuracy) != "PASS"
bool(accuracy) and not accuracy.get("skipped") and accuracy_status(accuracy) != "PASS"
)
if not perf_failed and not accuracy_failed:
return False
Expand Down Expand Up @@ -3236,8 +3229,7 @@ def _build_jobs(
# explicit per-model precision (e.g. fp16) skips this and is honored
# by the single-fallback branch below via _resolve_precision.
jobs.extend(
EvalJob(entry, None, fallback_precision=prec)
for prec in _NPU_FALLBACK_PRECISIONS
EvalJob(entry, None, fallback_precision=prec) for prec in _NPU_FALLBACK_PRECISIONS
)
else:
jobs.append(EvalJob(entry, None))
Expand Down Expand Up @@ -3381,10 +3373,7 @@ def parse_args() -> argparse.Namespace:
choices=["P0", "P1", "P2", "P3"],
default=["P0", "P1", "P2", "P3"],
metavar="{P0,P1,P2,P3}",
help=(
"Filter by priority. Pass one or more, e.g. --priority P0 P1. "
"Default: P0 P1 P2 P3."
),
help=("Filter by priority. Pass one or more, e.g. --priority P0 P1. Default: P0 P1 P2 P3."),
)
parser.add_argument(
"--release",
Expand Down Expand Up @@ -3639,10 +3628,9 @@ def main() -> None:
clean_cache_targets = _resolve_clean_cache_targets(args.clean_cache)
args.clean_cache_targets = clean_cache_targets

if (
not (args.list or args.list_json or args.update_baseline or args.build_only)
and not _is_eval_target_available(args.ep, args.device)
):
if not (
args.list or args.list_json or args.update_baseline or args.build_only
) and not _is_eval_target_available(args.ep, args.device):
return

# 1. Load registry
Expand Down Expand Up @@ -3922,9 +3910,7 @@ def main() -> None:
)
if timeout_rule is not None:
reason = timeout_rule.get("reason") or "timeout"
safe_print(
f"\n{_progress_prefix(i, total_jobs)} {label} (SKIP - TIMEOUT: {reason})"
)
safe_print(f"\n{_progress_prefix(i, total_jobs)} {label} (SKIP - TIMEOUT: {reason})")
model_dir.mkdir(parents=True, exist_ok=True)
timeout_result = build_eval_result(
entry=entry,
Expand Down Expand Up @@ -3982,16 +3968,14 @@ def main() -> None:
else "?"
)
safe_print(
f"\n{_progress_prefix(i, total_jobs)} {label} "
f"(RETRY - was {retry_label})"
f"\n{_progress_prefix(i, total_jobs)} {label} (RETRY - was {retry_label})"
)
except (json.JSONDecodeError, KeyError):
pass # Corrupted result file — re-run

if backfill_existing is None:
safe_print(
f"\n{_progress_prefix(i, total_jobs)} {label} "
f"({entry.priority}, {entry.group})"
f"\n{_progress_prefix(i, total_jobs)} {label} ({entry.priority}, {entry.group})"
)

try:
Expand Down
5 changes: 3 additions & 2 deletions src/winml/modelkit/commands/perf.py
Original file line number Diff line number Diff line change
Expand Up @@ -2522,8 +2522,6 @@ def _autobuild_genai_bundle(
existing bundle (in which case task/precision were not applied to it).
"""
from ..cache import get_cache_dir, get_model_dir
from ..loader import resolve_loader_config
from ..models.winml import build_genai_bundle, resolve_genai_bundle
from ..session import EPDeviceTarget, ep_to_device, resolve_device, short_ep_name
from ..utils.constants import normalize_ep_name

Expand Down Expand Up @@ -2553,6 +2551,9 @@ def _autobuild_genai_bundle(
console.print(f"[dim]Reusing cached genai bundle:[/dim] {bundle_dir}")
return bundle_dir, False

from ..loader import resolve_loader_config
from ..models.winml import build_genai_bundle, resolve_genai_bundle

# Cache miss (or forced rebuild): resolve the model family so its
# genai-bundle recipe can drive the build.
try:
Expand Down
2 changes: 2 additions & 0 deletions tests/e2e/test_perf_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -1549,6 +1549,8 @@ def genai_bundle(self, tmp_path_factory: pytest.TempPathFactory) -> Path:
proc = subprocess.run( # noqa: S603 -- trusted args (sys.executable + constants)
cmd,
capture_output=True,
encoding="utf-8",
errors="replace",
text=True,
timeout=1500,
check=False,
Expand Down
22 changes: 13 additions & 9 deletions tests/unit/commands/test_perf_genai.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from __future__ import annotations

import json
import sys
from io import StringIO
from pathlib import Path
from types import SimpleNamespace
Expand Down Expand Up @@ -1664,26 +1665,29 @@ def test_autobuild_rejects_unsupported_recipe_target(
builder.assert_not_called()
assert "config" not in capture_run

@pytest.mark.parametrize(
("arguments", "directory"),
[([], "genai-bundle"), (["--ep", "CPUExecutionProvider"], "genai-bundle-cpu-cpu")],
)
def test_autobuild_reuses_cached_bundle(
self, runner: CliRunner, tmp_path: Path, capture_run: dict, monkeypatch
self, runner: CliRunner, tmp_path: Path, capture_run: dict, monkeypatch,
arguments, directory,
) -> None:
import winml.modelkit.models.winml as winml_models
from winml.modelkit.cache import get_model_dir

monkeypatch.setenv("WINML_CACHE_DIR", str(tmp_path))
cached = get_model_dir("Qwen/Qwen3-0.6B", cache_dir=tmp_path) / "genai-bundle"
cached = get_model_dir("Qwen/Qwen3-0.6B", cache_dir=tmp_path) / directory
cached.mkdir(parents=True)
(cached / "genai_config.json").write_text("{}", encoding="utf-8")

build_calls: dict = {}
monkeypatch.setattr(
winml_models, "build_genai_bundle", _fake_build_genai_bundle(build_calls)
monkeypatch.setitem(sys.modules, "winml.modelkit.loader", None)
monkeypatch.setitem(sys.modules, "winml.modelkit.models.winml", None)
result = runner.invoke(
perf, ["-m", "Qwen/Qwen3-0.6B", "--runtime", "ort-genai", *arguments]
)

result = runner.invoke(perf, ["-m", "Qwen/Qwen3-0.6B", "--runtime", "ort-genai"])

assert result.exit_code == 0, result.output
assert "build" not in build_calls # cache hit: never rebuilt
assert "Reusing cached genai bundle" in result.output
assert capture_run["config"].bundle_dir == cached

def test_rebuild_forces_autobuild(
Expand Down
Loading