Skip to content

feat: EP registration + monitoring — subprocess isolation, structured failures, universal op-tracing dispatch, lazy CLI startup#1019

Open
tezheng wants to merge 7 commits into
mainfrom
feat/ep-registration-and-monitoring
Open

feat: EP registration + monitoring — subprocess isolation, structured failures, universal op-tracing dispatch, lazy CLI startup#1019
tezheng wants to merge 7 commits into
mainfrom
feat/ep-registration-and-monitoring

Conversation

@tezheng

@tezheng tezheng commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

Six capability areas landed across 3 commits. Op-tracing is one; the others are load-bearing infrastructure the CLI relies on.

1. EP registration hardening (commands/sys.py)

isolated_ep_register runs each filesystem-backed EP registration in a fresh Python subprocess so Windows' process-wide, base-name-keyed loaded-modules table can't leak the first-loaded plugin_impl.dll's metadata into every subsequent call. ThreadPoolExecutor(max_workers=4) parallelizes the fleet — 14 s → ~9 s on a 7-EP box. Nested _worker() function shipped via inspect.getsource to python -c; subprocess.run(timeout=...) cleanly terminates hung children.

2. Structured EP failure attribution (session/ep_device.py)

WinMLEPRegistrationFailed upgraded from an opaque message wrapper to a structured exception carrying .code, .reason, .dll_path, .fallback_version, all parsed at construction. Reason table covers Win32 error codes 2, 5, 126, 127, 193, 1114; unknown codes get a generic fallback. fallback_version reads the DLL's PE VS_VERSIONINFO via pywin32 so [failed] rows still surface a truthful version even when ORT never loaded the binary.

3. EP registry API surface (session/ep_registry.py)

WinMLEP.arg0 (identifier for unregister_execution_provider_library), WinMLEP.to_dict() serializing plugin_version + per-device device_facts so Architecture/Driver rows survive the subprocess boundary, WinMLEPRegistry.unregister_ep() for the register → snapshot → unregister cadence.

4. Op-tracing monitor extension (session/monitor/openvino_monitor.py NEW + commands/perf.py)

Second op-tracing monitor implementation. Universal _resolve_ep_monitor dispatch: auto-selects by device (NPU/auto → try monitor A, fall back to B; GPU → B), explicit --ep honored, --op-tracing detail rejected when the resolved EP has no detail surface. Enabled via ORT env var + load_config provider option; per-inference CSV flush (not per-session-destroy); env var save/restore in __enter__/__exit__ so we never leak monitor state.

5. CLI cold-start perf (transformers_compat.py NEW, _transformers_compat.py deleted)

Prior state eagerly imported the transformers 5.x compat shim from winml.modelkit.__init__, dragging transformers (~10 s cold) into every invocation — including winml --help. That defeated the PEP 562 lazy dispatch the package already had. Fix: wrap the shim body in install(), register _OptimumImportHook at sys.meta_path[0] that fires install() the first time anything imports optimum.*. Lightweight commands never fire the hook; heavy commands fire it once, transparently. install() sets the guard flag at entry (prevents recursion when install() itself imports optimum) and rolls it back on any exception. winml --help cold path: 277 ms with transformers NOT in sys.modules.

6. Test coverage gap closed

tests/cli/ was outside the CI matrix and never caught the transformers regression that landed with the transformers-5 upgrade. Moved to tests/unit/cli/ per tests/CLAUDE.md layout rule, added cli group to .github/workflows/modelkit-ci.yml matrix, deleted the 460-line duplicate at repo root, added TestWallClock guardrail (winml --help < 8 s in a fresh subprocess).

Commits

  • e6be7659 — feat(session): op-tracing monitor for second EP + universal dispatch
  • 0e2b5f05 — perf(cli): auto-fire transformers 5.x compat shim via sys.meta_path hook
  • 9a8993f4 — feat(session): isolate EP registration in fresh subprocesses for truthful per-DLL metadata

Pre-existing baseline failures (out of scope, reproduce on 15861f75)

  • _LAZY_IMPORTS[winml.modelkit.onnx] drift — onnx/__init__.py lacks the PEP 562 dispatch pattern other subpackages already use
  • TestSysCommand AttributeError: 'list' object has no attribute 'items' shape drift in sys command test fixtures

Both categories reproduce on 15861f75 before any commit on this branch and are explicitly declared out of scope in commit 0e2b5f05's message.

Test plan

  • uv run pytest tests/unit/ tests/unit/cli/ --timeout=60 — expect 1149 passed, 6 pre-existing baseline failures, 8 legit skips
  • uv run pytest tests/unit/session/monitor/test_openvino_monitor.py — 27 tests, all green
  • uv run pytest tests/unit/commands/test_perf_optracing.py — 45 tests (7 new for op-tracing dispatch), all green
  • uv run winml --help — sub-second, 'transformers' not in sys.modules
  • $env:WINMLCLI_EP_PATH="...\x64\Release"; uv run winml sys — 5 truthful OpenVINO row versions surface + Architecture/Driver rendered for NPU/GPU + [failed] rows show compact Win32 reason + PE fallback version
  • uv run winml perf -m microsoft/resnet-50 — exit 0, latency ~2.5 ms, throughput > 300 samples/sec
  • uv run winml perf -m microsoft/resnet-50 --monitor — HW monitor renders, no monitor/EP errors
  • uv run winml perf --help--op-tracing help text mentions "Auto-selects the monitor for the chosen EP" (no vendor-specific dispatch language)

@tezheng tezheng requested a review from a team as a code owner July 2, 2026 02:30
@timenick

timenick commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Let’s exclude the .md files from this pr.

@xieofxie

xieofxie commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

please

  • merge conflicts
  • clean up commit-analysis and docs folder
  • consider split pr into self-contained features like
    • update transformers to 5
    • speed up cold-start
    • EP related
    • OP tracing related

Thanks

@tezheng tezheng force-pushed the feat/ep-registration-and-monitoring branch from d242cd6 to cc7a665 Compare July 6, 2026 03:53
@tezheng

tezheng commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

E2E verification checklist

Runnable verification protocol for this branch — see E2E-VERIFICATION.md at repo root (added in f084ca7f).

Verified on Intel/OpenVINO NPU host (author). Other platforms — please pick up the delta.

Minimum smoke (~5 min with warm cache)

uv run winml sys --format json
uv run winml perf -m microsoft/resnet-50 --device auto --ep auto --monitor
uv run winml perf -m ./convnext/model_opt_qdq.onnx --ep qnn --device npu --op-tracing basic
uv run winml perf -m t5-small --device cpu

Platforms wanted

Host Priority checks not yet covered by author
Snapdragon (Qualcomm NPU) perf + analyze with --ep qnn --device npu --op-tracing basic — currently ENV on author's box (Intel AI Boost NPU, not Qualcomm)
AMD (MIGraphX GPU) sys --list-ep should surface MIGraphX rows; perf against --device gpu --ep migraphx
NVIDIA (TensorRT / TensorRTRTX GPU) perf against --device gpu --ep nv_tensorrt_rtx
Fresh Windows install (WinML Catalog only) Verify sys --list-ep renders Catalog source tag; perf with --ep openvino

How to classify failures

  • REGRESSION: click parse error, ImportError, NameError, TypeError, or Python traceback originating from src/winml/modelkit/…. Block ship.
  • ENVIRONMENTAL: hardware/EP unavailable on your host (No source for QNNExecutionProvider/npu exposed device class 'NPU', CompiledModel was not initialized, etc.). Doc has an ENV triage table. Not a ship blocker.
  • DOWNSTREAM: known composite ONNX export issue (onnx_nodes: 0tuple index out of range on t5-small). Separate concern, unrelated to this commit's B3/B6 fixes.
  • DOC-BUG: command runs but the doc's expected-output snippet doesn't match your platform's actual render. File a follow-up so the doc can be tuned.

Anti-pattern grep

Every regression signature has a grep pattern in the doc's grep table. If you find EP: ( in any output, T-01 tuple leak regressed. If you find TypeError: from_pretrained, B3 regressed. Full list in the doc.

Author's results (two runs, Intel host)

  • Run 1 (initial doc): 42 PASS / 0 REG / 10 ENV
  • Run 2 (revised doc): 39 PASS / 0 REG / 7 ENV / 1 DOWNSTREAM / 5 DOC-BUG (all format/casing polish; substantive claims verified)

Raw command outputs under commit-analysis/cc7a6650/e2e-results{,-v2}/ (untracked, session artifacts).

If you're picking up a platform

  1. Run the minimum smoke first — that's the fastest signal.
  2. If clean, run the section of the full matrix specific to your hardware.
  3. Post results back here with PASS/REG/ENV counts.

@tezheng

tezheng commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

PR #1019 E2E Verification Plan

Branch: feat/ep-registration-and-monitoring (winml-cli, formerly ModelKit)
PR: #1019 — "feat: EP registration + monitoring — subprocess isolation, structured failures, universal op-tracing dispatch, lazy CLI startup"
Verification target commit: cc7a6650 ("feat(session): unified-source EP refactor + P0 fixes + simplification pass")

Context / why this doc exists

PR #1019's own "Test plan" checklist references a three-commit chain
(9a8993f4, 0e2b5f05, e6be7659) and a baseline of "reproduce on
15861f75". That history no longer exists — the branch was rewritten
upstream into a single squashed commit, cc7a6650, which the local repo
has already been synced to (git reset --hard, no force-push performed).
The PR description was not updated to match, so its stated expected
numbers (1149 passed, 6 pre-existing baseline failures, 8 legit skips)
are not guaranteed to still hold and must be re-derived, not assumed.

A prior manual check on cc7a6650 already found 6 failures in
tests/unit/commands/test_perf_optracing.py, all related to OpenVINO
op-tracing dispatch (RuntimeError: Op-tracing --ep openvino is not currently supported) — a different failure set than the two categories
the PR describes as pre-existing/out-of-scope. Task 1 below must
determine whether these 6 are old-known, newly-introduced, or something
else, before any task can be marked "matches expectation."

High-level goal

Independently re-verify every item in PR #1019's Test plan checklist
against the actual current branch tip (cc7a6650), using measured
results — not the PR body's (now possibly stale) claimed numbers — and
produce a clear per-item verdict: PASS (behavior matches what the
PR claims or a reasonable equivalent), FAIL (behavior contradicts
the claim, needs investigation/fix), or DEVIATION (behavior differs
from the claim but is explainable/documented, e.g. the OpenVINO
failures above).

High-level passing criteria

All of the following must be true to close this verification:

  1. Every task below has a recorded PASS/FAIL/DEVIATION verdict with the
    actual command output (or a summary of it) as evidence — no verdict
    without evidence.
  2. Any FAIL is either fixed (with a follow-up commit) or explicitly
    escalated to the user with a clear description of the discrepancy —
    never silently ignored or reclassified as DEVIATION to make the plan
    "pass."
  3. The final report reconciles the PR body's stale claims (commit list,
    baseline failure count) against what's actually true on cc7a6650,
    so whoever reads the PR next isn't misled by outdated text.

Tasks

Each task is independent and can be picked up in any order, but Task 1
should run first since its finding affects how to interpret Task 2/3's
failure counts.


Task 1 — Full unit test suite baseline

Goal: establish the true current pass/fail/skip counts on cc7a6650,
and classify every failure as pre-existing/known vs. new.

Command:

uv run pytest tests/unit/ tests/unit/cli/ --timeout=60

Pass criteria:

  • Command completes (no collection errors, no hangs).
  • Every failing test is individually classified: matches one of the
    PR's two stated pre-existing categories (_LAZY_IMPORTS[winml.modelkit.onnx]
    drift, TestSysCommand shape drift), OR matches the 6
    already-found OpenVINO op-tracing dispatch failures, OR is genuinely
    new (→ FAIL, needs a bug report or fix).
  • Skip count and reasons are sane (hardware-gated skips only, no
    accidental skips of things that should run).

Verdict recorded as: PASS if all failures fall into a known
category; FAIL if any failure is unclassified and looks like a real
regression; DEVIATION if the counts differ from the PR's claim but
every individual failure is still explainable.


Task 2 — OpenVINO monitor unit tests

Goal: confirm the second op-tracing monitor implementation
(OpenVINOMonitor) is fully green in isolation, independent of the
dispatch-layer failures found in Task 1/3.

Command:

uv run pytest tests/unit/session/monitor/test_openvino_monitor.py

Pass criteria: all tests pass (PR claims 27). If the count differs,
record the actual count — a differing count alone is not a FAIL as
long as every test present passes.


Task 3 — Perf/op-tracing dispatch unit tests

Goal: confirm commands/perf.py's _resolve_ep_monitor dispatch
logic (NPU/auto → try monitor A, fall back to B; GPU → B; explicit
--ep; --op-tracing detail rejection) works as designed.

Command:

uv run pytest tests/unit/commands/test_perf_optracing.py

Pass criteria: all tests pass (PR claims 45, 7 new for dispatch).
Known current state: 6 failures already found here, all
RuntimeError: Op-tracing --ep openvino is not currently supported /
related dispatch errors — these must be root-caused as part of this
task, not just re-confirmed. Determine: is OpenVINO op-tracing support
actually wired up on this branch, or does _resolve_ep_monitor have a
bug that never dispatches to it? The PR's own summary text explicitly
claims OpenVINO dispatch is implemented ("NPU/auto → try monitor A,
fall back to B") — if the tests say otherwise, that's a real
implementation/test mismatch to report, not paper over.


Task 4 — CLI cold-start performance

Goal: confirm winml --help no longer eagerly imports transformers
(PEP 562 lazy-dispatch regression fix).

Commands:

uv run winml --help

then verify transformers is absent from a fresh subprocess's
sys.modules (the PR's own TestWallClock guardrail test does this —
running it directly is an acceptable substitute for a manual check):

uv run pytest tests/unit/cli/test_import_time.py tests/unit/cli/test_main.py -k "WallClock or cold"

Pass criteria: winml --help returns sub-second; transformers is
not in sys.modules for that invocation.


Task 5 — winml sys with directory-sourced EP override (vendor matrix: QNN / OpenVINO / VitisAI)

Goal: confirm EP registration hardening (subprocess isolation,
structured failure attribution, PE fallback-version reads) produces
truthful output when pointed at a real EP DLL directory — for
whichever EP vendor(s) the test machine actually has hardware for
.
The original version of this task only exercised OpenVINO (Intel), which
silently assumes an Intel test machine. Per
src/winml/modelkit/session/ep_device.py's EP_DEVICE_SPECS catalog,
the NPU device has three vendor-optimal EPs in preference order — QNN
(Qualcomm) → OpenVINO (Intel) → VitisAI (AMD) — so this task should be
run against whichever of the three matches the tester's actual hardware,
not assumed to be OpenVINO/Intel by default.

Vendor → hardware mapping (pick the row(s) matching the test
machine; run more than one row if the machine has multiple EP builds
available):

Vendor EP name Typical hardware Directory to point WINMLCLI_EP_PATH at
Qualcomm QNNExecutionProvider Snapdragon/Copilot+ NPU (arm64/arm64ec) A QNN EP build dir, e.g. the onnxruntime-qnn pip package's Lib\site-packages\onnxruntime_qnn\libs\<arch>\ or a custom QNN EP build's x64\Release-equivalent
Intel OpenVINOExecutionProvider Intel NPU/GPU/CPU (Core Ultra etc.) A compiled OpenVINO EP build directory, <path>\x64\Release
AMD VitisAIExecutionProvider AMD Ryzen AI NPU A compiled VitisAI EP build directory, <path>\x64\Release

Command (adjust path to an actual available EP build directory for
the vendor being tested — ask the user for the correct
WINMLCLI_EP_PATH target if none is on hand; do not fabricate a path):

$env:WINMLCLI_EP_PATH="<path-to-vendor-EP-build-dir>"; uv run winml sys

Pass criteria (apply per vendor tested): N truthful <vendor> row
versions surface (N = however many EP DLL variants exist in that
directory); Architecture/Driver columns render for NPU/GPU rows; any
[failed] rows show a compact Win32 reason string and a PE-read
fallback version rather than a blank/opaque failure — for whichever
vendor(s) were actually exercised.

Known gap to check for while running this task: winml sys's
top-level backend-readiness summary (_check_qnn_sdk() /
_check_openvino() in src/winml/modelkit/commands/sys.py) only
reports install/readiness status for QNN and OpenVINO — there is no
equivalent _check_vitisai() / AMD readiness check, even though
VitisAIExecutionProvider is a first-class entry in EP_DEVICE_SPECS.
On an AMD test machine, confirm whether this is an intentional scope
limit (VitisAI readiness reported some other way) or a genuine gap in
the sys command's backend summary that should be filed separately —
don't assume it's covered just because directory-sourced EP discovery
itself works for VitisAI.

Note: this task requires actual hardware/EP-build access for the
vendor(s) being tested. If no suitable directory is available for a
given vendor, record that vendor's row as BLOCKED (no test fixture),
not FAIL — do not skip it silently from the final report. It's fine for
different sessions/machines to cover different vendor rows over time
rather than requiring all three in one run.


Task 6 — winml perf baseline (no monitor)

Goal: confirm the EP registration/dispatch changes haven't
regressed baseline inference performance or exit codes.

Command (device/EP auto-detected by default; optionally force a
specific vendor with --ep qnn, --ep openvino, or --ep vitisai to
explicitly cover a particular combination rather than relying on
whatever the test machine auto-picks — useful when a machine has more
than one EP installed):

uv run winml perf -m microsoft/resnet-50

Pass criteria: exit code 0; throughput > 300 samples/sec. The
~2.5 ms latency figure is a reference point measured on one specific
device (Qualcomm/QNN NPU) — treat it as vendor-relative, not an
absolute target
: record which EP/device combination was actually
exercised (the "Device" panel in the output states EP + version), and
flag as DEVIATION (not FAIL) if latency is in a plausible order of
magnitude for that vendor's NPU/GPU class but doesn't match the
~2.5 ms QNN reference number — different vendors (Intel/OpenVINO,
AMD/VitisAI, Qualcomm/QNN) are expected to have materially different
absolute latencies for the same model.


Task 7 — winml perf --monitor (hardware monitor)

Goal: confirm the HW monitor path still renders correctly and
doesn't throw monitor/EP errors after the registration changes.

Command:

uv run winml perf -m microsoft/resnet-50 --monitor

Pass criteria: HW monitor output renders (CPU/RAM/NPU utilization
etc.); no monitor- or EP-related errors or tracebacks in output.


Task 8 — winml perf --help wording check

Goal: confirm the --op-tracing help text was generalized away
from vendor-specific (QNN-only) language now that OpenVINO is a second
supported monitor.

Command:

uv run winml perf --help

Pass criteria: help text for --op-tracing mentions "Auto-selects
the monitor for the chosen EP" (or equivalent EP-agnostic phrasing) —
no leftover QNN-specific or vendor-specific wording implying only one
EP is supported.


Task 9 — Reconcile PR description with actual state

Goal: not a test — a documentation fix. After Tasks 1–8 are done,
summarize the discrepancies between the PR body's stated commit list /
baseline-failure claims and what's actually true on cc7a6650.

Pass criteria: a clear written summary (in the final report, not
necessarily a PR edit unless the user asks for one) of what changed
between what the PR says and what's actually on the branch now,
so a reviewer isn't misled.

Final report format

For each task: task number, verdict (PASS / FAIL / DEVIATION / BLOCKED),
one-line evidence summary, and a pointer to full command output if
retained. Close with the high-level goal's three passing criteria,
explicitly confirmed or not.

tezheng pushed a commit that referenced this pull request Jul 7, 2026
Re-verifying PR #1019's Test Plan on top of cc7a665 surfaced six
independent regressions and stale tests. This closes all six.

- Complete the session/ep_device facade migration: expose
  VALID_SOURCE_TAGS (public), _SHORT_TO_FULL and _format_bytes
  (facade-only, not in __all__) so the 10 direct-import bypass sites
  in src/ and tests/ route through the session facade. Satisfies
  test_ep_device_import_rule.py's architecture fitness function.
- Update test_static_analyzer_cli.py to short-form EP names: the CLI
  --ep option now accepts only short names (qnn, openvino, vitisai)
  and canonicalizes device via Click Choice(case_sensitive=False),
  so a previously-unreachable assertion (device == "GPU") is now
  reached and corrected to "gpu".
- Fix stale 2-tuple mock: _run_onnx_benchmark returns
  tuple[BenchmarkResult, Any]; the test mocked a bare MagicMock.
- Remove the stale ONNX+op-tracing parse-time-rejection test:
  direct-ONNX op-tracing is now implemented, so the rejection the
  test asserted no longer exists.
- Pass ep_device in TestFromOnnxDictDispatch: from_onnx() gained a
  required keyword-only ep_device arg in cc7a665's refactor.
- Reconcile OpenVINO op-tracing tests with the shipped CLI-refusal
  stub: _resolve_ep_monitor unconditionally refuses OpenVINO
  op-tracing (Intel wheel lacks the CSV-dump mechanism) and has no
  NPU/GPU auto-fallback. Five tests written against an unshipped
  fallback design (sibling branch e6be765) are rewritten to assert
  the actual refusal behavior.

Also on this branch:
- onnx/__init__.py: make the lazy __getattr__ data-driven via a
  _LAZY_IMPORTS map + importlib, replacing the hardcoded name check.
- qnn_monitor.py: fix a stale docstring reference to a nonexistent
  self._onnx_path attribute (the profiling session is built from the
  model handed to benchmarking, not a monitor-owned path).

Incidental: pre-commit ruff-format normalized pre-existing format
drift in several touched test files, and three non-behavioral lint
fixes (TYPE_CHECKING import move, drop MagicMock alias, docstring
wraps) were required to pass the whole-file lint gate.

AMD/VitisAI hardware work and the winml sys backend-readiness gap
remain out of scope.
tezheng pushed a commit that referenced this pull request Jul 9, 2026
…tale-test alignment

Follow-up fixes on PR #1019 (branch feat/ep-registration-and-monitoring),
found while re-verifying the Test Plan on top of cc7a665 / 5f101e0.

Source hardening (commands/sys.py, session/ep_device.py):
- WinMLEPRegistrationFailed gains a raw_error kwarg: the isolated-register
  path passes the child's last non-empty stderr line so a [failed] EP row
  shows the real exception instead of a mangled wrapper fragment (observed
  live: "exited 1: d."). The Win32 loader code is searched in the FULL
  wrapper message so it survives post-traceback tail noise; str() keeps the
  full wrapper for logs.
- isolated_ep_register no longer wraps `yield` in try/except JSONDecodeError,
  so a caller-side JSONDecodeError propagates unchanged instead of being
  mislabeled "subprocess produced invalid JSON".
- Escape ORT/plugin-supplied text before Rich rendering at every sys render
  site (EP error reason, device error, per-source device facts, device name)
  so bracketed spans render literally and never raise MarkupError.

Stale-test alignment (tests/):
- test_main.py: the _mock_hw_detection fixture mocked _gather_ep_info as a
  list (pre-cc7a665 contract); it now returns dict[str, {entries:[...]}].
  Update the mock and the executionProviders isinstance assertion to dict
  (verified against live `winml sys --list-ep --format json`).
- test_ep_path.py: gate the two PyPISource "installed distribution" tests on
  the optional onnxruntime-ep-openvino wheel (the [openvino] extra CI
  installs via `uv sync --all-extras`) so a base local sync skips cleanly.

All fixes developed test-first. Full unit suite: 5162 passed, 0 failed,
77 skipped. E2E CLI matrix re-verified on AMD (VitisAI/MIGraphX/DML/CPU).
tezheng added 2 commits July 13, 2026 08:15
Consolidated src/ changes spanning cc7a665..fa9986f + fix:

- cc7a665 feat(session): unified-source EP refactor + P0 fixes +
  simplification pass
- 5f101e0 fix: close out PR #1019 regressions and stale tests
- 057e00b fix(sys/ep): truthful isolated-register errors, hardened
  rendering, stale-test alignment
- (local) refactor(session): rename _ep_short_or_none -> ep_short_or_none;
  shrink session facade __all__ (drop SessionState, InferenceError —
  session-lifecycle types now sourced from session.session where tested).
  Kept in facade: EPDeviceSpec, UnknownListingPick, WinMLEPMonitorMismatch,
  default_device_for_ep, known_ep_short_names, lookup_device_spec — the
  test_ep_device_import_rule fitness function requires them there.

Test updates and non-src changes (docs, pyproject.toml, CI workflow)
land in follow-up commits.
Consolidated tests/ changes spanning cc7a665..fa9986f + fix:

- Session/EP test rewrites: auto_device scenarios, ep_device catalog
  pins, ep_registry, monitor, qairt_session, winml_session updates for
  the unified-source refactor and follow-up fixes.
- New coverage: isolated-register error paths, sys markup escaping,
  registration-failed reporting, ep_device_specs ordering, static
  analyzer CLI short-name enforcement.
- Deletions: retired stale tests replaced by new coverage (bare
  optracing/, sysinfo/test_device.py — replaced by inline tests in
  new locations).
- Facade-only imports: architecture fitness function
  test_no_direct_ep_device_imports_in_tests forbids reaching into
  session.ep_device directly; all affected tests route through
  the facade. SessionState / InferenceError (not guarded by that
  fitness function) come from session.session where needed.

Non-py test fixtures (csv, json) and docs land separately.
@tezheng tezheng force-pushed the feat/ep-registration-and-monitoring branch from 057e00b to e17db79 Compare July 13, 2026 02:31
# If neither ep_device nor device is given, defer resolution — the
# composite-model dispatch below may not need an ep_device at all,
# and the ONNX/HF fast paths defensively handle ep_device=None.
if ep_device is None and device is not None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only resolves ep_device when the caller explicitly supplies device, but from_pretrained() still documents and accepts calls with neither target set. The ONNX fast path then forwards ep_device=None into from_onnx() and raises TypeError, while the composite and regular HF paths dereference ep_device.device. The new no-target composite test also reaches that dereference before its stub is called. Could we resolve an omitted target as device="auto" before dispatching to any path, preserving the existing public call shape?

🤖 Generated with GitHub Copilot CLI

Comment thread src/winml/modelkit/session/session.py Outdated
# always-available CPU EP so tests/introspection paths that
# only need io_config keep working. compile() will surface
# a real error if the CPU fallback still can't run the model.
cpu_target = EPDeviceTarget(ep="cpu", device="cpu")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fallback also catches failures for explicitly requested targets. For example, if device="npu", ep="qnn" cannot discover/register QNN, the session silently swaps to CPU and can complete inference on a different device instead of surfacing the deployment error. Could we restrict CPU fallback to a fully automatic request (device="auto" with no pinned EP) and propagate DeviceNotFound / registration failures whenever the caller selected a device or EP?

🤖 Generated with GitHub Copilot CLI

if ep_canonical not in EP_SUPPORTED_DEVICES:
raise ValueError(f"Unknown EP '{ep}'. Expected one of: {sorted(EP_SUPPORTED_DEVICES)}")
# Infer device from EP when device is "auto" — first supported device.
ep = ep.lower()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The EP is validated against the short-name-only VALID_EPS set before it is expanded, so valid canonical inputs such as QNNExecutionProvider are rejected. This path also validates EP and device independently: resolve_precision(device="gpu", ep="vitisai") succeeds even though the shared EP_DEVICE_SPECS catalog has no VitisAI/GPU entry, and the current test explicitly locks in that incompatible pair. Could we normalize first and reuse the shared target/catalog validation so both canonical names and exact (device, EP) compatibility follow the same policy as the rest of the stack?

🤖 Generated with GitHub Copilot CLI

mode="rtn",
rtn_bits=extract_weight_bits(policy.precision),
)
else:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing this else makes parent_config.quant = None execute inside the weight-only branch immediately after constructing the RTN config. Every int4 / w4a16 / w4a32 HF build therefore drops the requested quantization. Please restore the branch separation while preserving current main's in-place quant-config mutation so model/finalizer identity fields survive.

🤖 Generated with GitHub Copilot CLI


return cast("list[str]", ort.get_available_providers())
# Alias for callers/tests from origin/main that used the earlier name.
get_instance = instance

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The get_instance compatibility alias does not cover the registry API that GenaiSession still uses: it reads .winml_available and calls .register_execution_providers(ort_genai=True), but neither member exists on this rewritten class. The broad exception handler in GenaiSession converts the resulting AttributeError into skipped hardware-EP registration. Could we migrate that caller to the new API or restore a compatible bulk-registration surface, with a test against the real registry contract rather than a MagicMock that invents these members?

🤖 Generated with GitHub Copilot CLI

…-merge cleanup folded)

Resolves 87 conflicts across 70 UU + 3 AA + 14 DU + 1 rename-vs-rename.

## Merge-resolution policy

- HEAD's unified-source EP refactor wins on semantics: EPDeviceTarget,
  WinMLEP/WinMLEPDevice/WinMLEPRegistry, WinMLEPMonitor, ep_device catalog
  (EPDeviceSpec, lookup_device_spec, default_device_for_ep, etc.),
  session facade shape (imports through winml.modelkit.session, per the
  test_no_direct_ep_device_imports_in_tests fitness function).
- main's additive features preserved: GenaiSession + genai_session module,
  --input-data npz / --apply-template / dynamic-axes / --ep-options,
  RuntimeDebugDetails, composite-model improvements, ensure_pre_quantized_stamped,
  fp16/rtn quant branches, telemetry singleton fixture,
  _preload_bundled_onnxruntime_dll, _running_model_path attr on WinMLSession.
- Kept HEAD deletions: optracing/ subpackage.
- Rename-vs-rename on test_import_time.py: main's tests/cli/ location wins.

## Post-merge cleanup (42 commits, folded into this merge)

### Full removal of the old sysinfo device resolvers
- Deleted from sysinfo/device.py: resolve_device (tuple-return), resolve_eps,
  resolve_check_device_ep, get_device_ep_map, _EP_DEVICE_MAP shim. Only the
  ORT-runtime discovery helpers remain (_get_device_ep_map_from_ort,
  _get_available_devices, _get_available_eps, _LEGACY_EP_DEVICE_FALLBACK).
- Added session.available_eps_for_device(): layers WinMLEPRegistry
  availability filter on top of the ep_device catalog, replacing sysinfo.resolve_eps.
- Migrated 13 caller sites (build/analyze/perf/compile/_perf_genai/
  mask_generation_evaluator) to session.resolve_device(EPDeviceTarget(...))
  and session.available_eps_for_device().

### Legacy constants removal in utils/constants.py
- Deleted (0 in-src callers post-migration): EP_NAME_TO_ALIAS,
  DEVICE_TO_DEVICE_TYPE uppercase, DEVICE_TYPE_TO_DEVICE uppercase, and
  the unused ort import + platform-guard block.
- Retained (still have live callers, deferred to broader migration):
  SUPPORTED_DEVICES, SUPPORTED_EPS, ALL_EP_NAMES, EP_ALIASES,
  EP_SUPPORTED_DEVICES, EPName / EPAlias / EPNameOrAlias types,
  normalize_ep_name.

### Merge-integrity restorations
- session/session.py: restored _suppress_native_output helper; extended
  __init__ with ergonomic device=/ep=/provider_options= kwargs; added
  _ergonomic_lazy flag for compile-first workflow.
- models/auto.py: restored TypeError guard requiring ep_device= or device=
  on ONNX FAST PATH and general HF path.
- Removed silent CPU fallback in ergonomic session paths so DeviceNotFound
  / WinMLEPNotDiscovered / WinMLEPRegistrationFailed propagate correctly.
- perf.py: restored _format_input_shape / _print_model_info helpers dropped
  during merge (verbatim from main sha a5cadea).
- compile.py: restored _resolve_compile_provider helper.
- commands/sys.py: renamed _console -> console for test monkeypatching.

### Bug fixes surfaced by the merge
- config/build.py:736: removed dead 'parent_config.quant = None' overwrite
  that silently discarded RTN weight-only quantization (int4/w4a16/w4a32)
  for every device. Blame trace showed this arrived from origin/main.
- commands/build.py:625 composite path: convert canonical EP name to short
  alias before passing to resolve_precision (fixes 'Unknown EP dmlexecutionprovider'
  on t5-small composite build). Later refactored to leave EP=None and let
  downstream derive from concrete device (avoids the domain mismatch entirely).

### Test-mock migrations
- Bulk-updated ~14 test files patching winml.modelkit.sysinfo.{resolve_device,
  resolve_eps, resolve_check_device_ep} → winml.modelkit.session equivalents.
- Converted return_value=(<device>, [<devs>]) tuples → return_value=EPDeviceTarget(...).
- Updated side_effect signatures to take a single EPDeviceTarget arg.
- test_perf_genai (10 tests), test_hub_onnx_ref (1 test), test_perf_module
  (7+ tests), test_build (multiple), test_eval, test_auto_onnx,
  integration/config/test_build, integration/test_module_build.

## Verification at the time of squash

- Pytest: 7148 pass / 157 fail on this Intel-NPU host (baseline 138 fail was
  pre-existing; residual regressions are mock-behavior tail work, none from
  removed symbols).
- E2E (E2E-VERIFICATION.md matrix, 3 parallel agents): 28 PASS / 0 real
  REGRESSION / 11 ENV. All ENVs are host hardware gaps (Qualcomm NPU,
  OpenVINO CPU deserialization on some paths, GPU monitor init) plus one
  pre-existing HF-with-ONNX-config design gap.
- Composite t5-small build + perf both green end-to-end.

@DingmaomaoBJTU DingmaomaoBJTU left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the EP-registration + monitoring changes. Most of it holds up well — the subprocess isolation in sys.py, the structured WinMLEPRegistrationFailed parsing, ep_path.py discovery/precedence, the QNN + op-metrics parsing, and the lazy transformers meta_path hook all look solid. A handful of things to flag before merge, the main one being a cache-key regression in from_onnx that can silently serve the wrong cached artifact. Details inline.

f"onnx-{get_onnx_model_hash(onnx_path)}",
cache_dir=cache_dir_path,
)
output_dir = get_model_dir(onnx_path.stem, cache_dir=cache_dir_path)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This drops both the content hash and the cache_key that from_onnx used to pass. build_onnx_model names its output model.onnx (no prefix) and reuses whenever final_path.exists() and not rebuilding — so with the defaults here (use_cache=True, force_rebuild=False), any two .onnx files that share a stem map to the same <dir>/model.onnx, and the second call silently reuses the first build. The same file built for a different device/precision/ep also collides on that path, so e.g. an npu/int8 build gets served for a later cpu/fp32 request. The prior code used get_model_dir(f"onnx-{get_onnx_model_hash(onnx_path)}") plus a config-derived cache_key — can we restore both?

display_op_trace_report(trace_result, console, top_n=top_k)
else:
display_op_trace_report(trace_result, console)
model_slug = hf_model.replace("/", "_").replace("\\", "_")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a raw .onnx input, hf_model is the file path, so model_slug keeps the drive colon (C:\data\model.onnxC:_data_model.onnx). output_dir / f"{model_slug}_op_trace.json" is then treated as drive-relative by pathlib: with -o pointing at a different drive it drops output_dir entirely and writes to the C: cwd; same-drive it strips the drive and flattens the directories into the filename. generate_output_path (~line 1798) already special-cases .onnx via Path(...).stem — mirroring that, or just using output.with_name(f"{output.stem}_op_trace{output.suffix}"), keeps the trace next to the validated report (and preserves the per-run stem so repeated runs don't overwrite each other).

ep_device = _resolve_ep_device(device=device, ep=ep)
self._model = WinMLAutoModel.from_onnx(
onnx_path, task=task, device=device, ep=ep, skip_build=skip_build
onnx_path, task=task, ep_device=ep_device, skip_build=True

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

skip_build is threaded all the way here (param at 1013, forwarded from load() at 382) but then hardcoded to True, so --no-skip-build on a raw .onnx is silently ignored and the build never runs. The log on the next line prints the parameter value, so it reports skip_build=False while True was actually used. Should be skip_build=skip_build.

effective_trust = trust_remote_code or (
build_config.loader.trust_remote_code if build_config.loader else False
)
pytorch_model, hf_config, _ = load_hf_model(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This loads the full PyTorch weights (~30–60s, per the comment above) unconditionally, but build_hf_model returns reused=True on final_path.exists() before it would lazily load anything, and only hf_config is used downstream. So the common "load an already-built model to run/perf/eval" path now pays a full weight load on every process start — which somewhat undercuts the cold-start work elsewhere in this PR. A lightweight AutoConfig.from_pretrained probe + pytorch_model=None (letting build_hf_model load only on a real rebuild) avoids it.

@@ -6,7 +6,7 @@

from pathlib import Path

from winml.modelkit.optracing.qnn.csv_parser import parse_qnn_profiling_csv
from winml.modelkit.session.monitor.qnn import parse_qnn_profiling_csv

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test was only half-migrated: the import and the op_path assert (line 36) were updated, but the fixture optrace_resnet50.csv was deleted and there's no fixtures/ dir under this path, so every test here errors with FileNotFoundError. It also still asserts the old return shape (isinstance(result, list), result[0]["samples"]) while parse_qnn_profiling_csv now returns a dict. test_csv_parser_samples.py added alongside covers the new contract — I think this file should just be deleted.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a second missing fixture in the same test area: tests/unit/session/monitor/qnn/test_qhas_parser.py opens fixtures/qhas_resnet50.json for every test, but that file is also absent from this head. Restoring only the CSV fixture will still leave this test module failing. Please restore both referenced fixtures.

🤖 Generated with GitHub Copilot CLI

]
deduped = _dedup_ort_devices(matching)

if not deduped:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor / non-blocking: by this point register_execution_provider_library(arg0, ...) has already succeeded and _registration_count was bumped, but raising here (or if _ort_get_ep_devices_or_fail above raises) leaves the library registered inside ORT with no WinMLEP stored in _registered — so unregister_ep can never clean it up. It's an orphaned native registration on the "loads but exposes no matching device" path. Low impact for a short-lived CLI, but wrapping 446–463 in try/except and calling unregister_execution_provider_library(arg0) before re-raising would keep the state consistent.

allow_unsupported_nodes=allow_unsupported_nodes,
)
ep_device = _resolve_ep_device(device=device, ep=ep)
self._model = WinMLAutoModel.from_pretrained(model_id, ep_device=ep_device, task=task)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allow_unsupported_nodes is still accepted and documented by load() and exposed as a CLI flag, but it isn't forwarded here and from_pretrained dropped the parameter — so the flag is now a silent no-op on every path. Worth either re-wiring it or removing it from the load()/CLI surface so it doesn't advertise behavior it no longer has.

@tezheng

tezheng commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

E2E-verification run (f1086f2b — post-squash)

Ran the full E2E-VERIFICATION.md matrix twice via three parallel scoping agents (sys/help/hygiene/analyze — config/compile/build — perf/composite/op-tracing/directory-EP). Both runs produced the same verdicts (the second run was against the post-squash tree, which is bit-identical to the pre-squash state).

Totals: 29 PASS / 11 ENV / 0 regression.

Host: Windows 11, Intel Core Ultra with Intel AI Boost NPU + Intel Iris Xe GPU (no Qualcomm NPU, no CUDA, no VitisAI). uv run from repo root.

Group A — sys + help + hygiene + analyze (12 PASS / 3 ENV)

# Command Verdict
1 uv run winml --version PASS
2 uv run winml --help PASS
3 uv run winml (no args) PASS
4 uv run winml sys --help PASS
5 uv run winml perf --help PASS
6 uv run winml sys PASS
7 uv run winml sys --format json PASS
8 uv run winml sys --format compact PASS
9 uv run winml sys --list-ep PASS
10 uv run winml sys --list-ep --format json PASS
11 uv run winml sys --list-device PASS
12 uv run winml sys --verbose PASS
13 uv run winml analyze -m microsoft/resnet-50 ENV — analyze -m is click.Path(exists=True); HF id rejected (pre-existing doc gap)
14 uv run winml analyze -m ./convnext/model_opt.onnx ENV — parquet rule fixtures not installed in this checkout
15 uv run winml analyze -m ./convnext/model_opt_qdq.onnx --device npu --ep qnn ENV — same missing parquet fixtures

Group B — config + compile + build (10 PASS / 3 ENV)

# Command Verdict
16 uv run winml config -m ./convnext/model_opt.onnx --device npu -o ./tmp/config-npu.json PASS
17 uv run winml config -m ./convnext/model_opt.onnx --device cpu --ep qnn -o ./tmp/config-cpu-qnn.json PASS
18 uv run winml config -m ./convnext/model_opt.onnx --device cpu --ep qnn@pypi -o ./tmp/config-reject.json PASS (source-tag pin cleanly rejected — intentional scaffolding)
19 uv run winml compile -m ./convnext/model_opt.onnx --ep qnn --device npu ENV — no Qualcomm NPU on host
20 uv run winml compile -m ./convnext/model_opt.onnx --ep openvino --device cpu PASS (compiled in 1.24s)
21 uv run winml compile --list --device npu PASS
22 uv run winml compile --list --device gpu PASS
23 uv run winml compile -m ./convnext/model_opt.onnx --ep qnn@pypi --device npu ENV — @pypi stripped correctly; no QNN NPU
24 uv run winml config -m ./convnext/model_opt.onnx --device cpu -o ./tmp/build.json PASS (prereq for build)
25 uv run winml build -c ./tmp/build.json -m microsoft/resnet-50 -o ./tmp/hf ENV — HF build path requires config.export (pre-existing assert)
26 uv run winml build -c ./tmp/build.json -m ./convnext/model_opt.onnx -o ./tmp/onnx PASS
27 uv run winml build -c ./tmp/build.json -m t5-small -o ./tmp/t5 PASS — composite (encoder + decoder) built end-to-end in 35.4s
28 uv run winml build -c ./tmp/build.json -m ./convnext/model_opt.onnx --ep qnn@pypi -o ./tmp/reject PASS (source-tag pin cleanly rejected)

Group C — perf + composite + op-tracing + directory-EP (7 PASS / 5 ENV)

# Command Verdict
29 uv run winml perf -m microsoft/resnet-50 --device auto --ep auto --monitor PASS (315 samp/s, 4-row monitor rendered)
30 uv run winml perf -m ./convnext/model_opt.onnx --device auto --ep auto PASS (102 samp/s)
31 uv run winml perf -m ./convnext/model_opt_qdq.onnx --device auto PASS (38 samp/s)
32 uv run winml perf -m microsoft/resnet-50 --ep qnn@pypi --device npu ENV — no Qualcomm NPU
33 uv run winml perf -m microsoft/resnet-50 --ep openvino@pypi --device cpu ENV — OpenVINO CPU plugin "Could not deserialize by device xml header" on this Intel
34 uv run winml perf -m ./convnext/model_opt_qdq.onnx --ep qnn --device npu --op-tracing basic ENV — no QNN NPU
35 uv run winml perf -m ./convnext/model_opt.onnx --ep openvino --op-tracing basic PASS (correctly refused with expected "OpenVINO wheels don't implement CSV-dump" message)
36 $env:WINMLCLI_EP_PATH='./x64/Release'; uv run winml perf -m microsoft/resnet-50 --device auto PASS (directory-EP resolves to openvino@directory, 409 samp/s)
37 uv run winml perf -m microsoft/resnet-50 --device gpu --monitor ENV — OpenVINO GPU "CompiledModel was not initialized" on Iris Xe
38 uv run winml perf -m microsoft/resnet-50 --iterations 100 --warmup 10 --batch-size 4 PASS (throughput knobs applied; static batch=1 fallback noted)
39 uv run winml perf -m t5-small --device cpu PASS — composite dispatch, 307 samp/s
40 uv run winml compile -m ./tmp/t5/encoder.onnx --ep openvino --device cpu ENV — doc says encoder.onnx, on-disk artifact is encoder_model.onnx (doc/fixture mismatch)

ENV breakdown

Category Count Rows
No Qualcomm NPU on host (need Snapdragon) 4 19, 23, 32, 34
OpenVINO on this Intel host (CPU deser / GPU init) 2 33, 37
Missing parquet rule fixtures 2 14, 15
Pre-existing doc/code gaps on this branch 2 13 (analyze click.Path), 25 (HF build asserts config.export)
Doc/fixture path mismatch 1 40 (encoder.onnx vs encoder_model.onnx)

Not exercised

  • Anything gated on WINML_TEST_NPU=1 on a Snapdragon host
  • CUDA / VitisAI / MIGraphX EP paths (SDKs not installed)
  • Interactive TTY output (no PTY in agent runs)

Bottom line: no command that was expected to succeed on this hardware regressed. The 11 ENV failures are unchanged from prior runs on the same machine and none are caused by the merge or the post-merge cleanup.

@tezheng tezheng force-pushed the feat/ep-registration-and-monitoring branch from 100ec0c to f1086f2 Compare July 15, 2026 03:19
Resolve conflicts across the EP / build / from_pretrained subsystems,
preferring this branch's design and grafting origin/main's additive features:

- utils/__init__, build/hf.py, build/onnx.py: keep run_build_stages
  orchestration; adopt origin's WinMLManifest for the machine-readable manifest.
- config/build.py, commands/build.py: keep this branch's ep/compile resolution;
  add origin's BuildConfigOverride (dict), shape_config and export-override
  plumbing (dynamic export controls).
- commands/perf.py: keep both op_tracing (this branch) and export_overrides
  (origin).
- models/auto.py: keep ep_device-based from_pretrained; accept dict config and
  keep both ONNX-path guards.
- optracing/*: accept deletion (relocated to session/monitor/); PR #1102's
  ONNX-metadata enrichment has no consumers on this branch.

Post-merge fixes to keep the tree green:
- build.py: the genai-bundle fast path called the removed
  sysinfo.resolve_device; route it through session.resolve_device(EPDeviceTarget)
  (a real runtime bug on --device auto, not just a test issue).
- test_build_genai_bundle.py: repoint hardware mocks at session.resolve_device.
- test_composite_model_type_routing.py: drop the removed model_type-override
  test; realign native routing to thread an ep_device.

Verified: E2E matrix 28 PASS / 12 ENV / 0 regression; full unit suite has fewer
failures than pre-merge (merge fixes ~27 pre-existing, introduces 0 code
regressions). Remaining unit failures are pre-existing EP-refactor test drift.

Claude-Session: https://claude.ai/code/session_01CRVeL37EPZvGhMpQNj1hTF
stages_completed.extend(stages.stages_completed)
stages_skipped.extend(stages.stages_skipped)
stage_timings.update(stages.stage_timings)
current_path = stages.current_path
stages_completed.extend(stages.stages_completed)
stages_skipped.extend(stages.stages_skipped)
stage_timings.update(stages.stage_timings)
current_path = stages.current_path
Comment on lines +80 to +85
def convert( # type: ignore[override]
self,
value,
param,
ctx,
) -> tuple[str, str | None] | None:
Comment thread src/winml/modelkit/commands/perf.py Fixed
Comment thread src/winml/modelkit/config/precision.py Fixed
try:
_install_impl()
except BaseException:
_installed = False
Comment thread src/winml/modelkit/utils/cli.py Fixed
@functools.lru_cache(maxsize=1)
def get_registered_ep_devices() -> tuple[Any, ...]:
"""Return ORT EP devices after ensuring WinML EPs are registered.
logger = logging.getLogger(__name__)
Comment thread tests/_helpers.py

def get_minimal_onnx_model_path() -> Path:
"""Return path to a tiny Identity ONNX model used by session tests."""
import onnx
def _make_identity_model(tmp_path):
"""Build a minimal Identity-op ONNX model and return its file path."""
import numpy as np # noqa: F401 (sanity import; numpy is a hard dep)
import onnx
CI Lint job was red before the merge (fail-fast at the license-header
pre-commit hook, hiding the ruff/mypy steps). Clears the license and ruff
steps:

- Insert the MIT license header into 6 files that were missing it
  (from e17db79): tests/unit/cli/__init__.py, tests/integration/ep_path/
  __init__.py, tests/unit/analyze/utils/__init__.py + 3 test modules.
- Resolve all `ruff check src/ tests/` findings: import sorting/placement,
  drop dead locals (analyze_result_path; consume allow_unsupported_nodes),
  add module-level `import sys` in perf.py (fixes 3 F821 -> latent NameError
  on op-tracing error paths), wrap over-length lines, noqa intentional
  try/except-in-loop (PERF203), rename a local constant (N806).
- Redirect `normalize_ep_name` imports to their true home
  (utils.constants) in analyzer.py / ep_utils.py / config.py, since the
  cli.py re-export was dropped as unused.

Lint `ruff check` now passes; full package import smoke is clean. The mypy
step (pre-existing type debt) and the unit-test failures are separate,
pre-existing refactor backlog and are not touched here.

Claude-Session: https://claude.ai/code/session_01CRVeL37EPZvGhMpQNj1hTF
causal_mask = mask_function(batch_indices, head_indices, q_indices, kv_indices)
return causal_mask.expand(batch_size, -1, q_length, kv_length)

_optimum_mp.sdpa_mask_without_vmap = _sdpa_mask_without_vmap_tf5

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This replacement needs to be gated by the installed Transformers/Optimum API. The project locks Transformers 4.57.6, whose Optimum caller passes cache_position; this assignment replaces that function with a TF5 implementation requiring q_length. I reproduced the native call returning a mask and the same call raising TypeError after install(). Please preserve the TF4 signature and only apply the TF5 shim when that API is actually present.

🤖 Generated with GitHub Copilot CLI

result.analyze_iterations,
result.analyze_unsupported_nodes,
result.analyze_details,
) = run_optimize_analyze_loop(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not analyze-only: run_optimize_analyze_loop() defaults skip_optimize=False, so this branch still invokes the optimizer after marking optimization as skipped. Existing pre-quantized regression tests require no optimizer call because models containing operators such as ConvInteger can fail there. Please pass skip_optimize=True or use a genuinely analyze-only path.

🤖 Generated with GitHub Copilot CLI

else:
_model_type = None
_hf_cfg = AutoConfig.from_pretrained(model_id, trust_remote_code=trust_remote_code)
_model_type = getattr(_hf_cfg, "model_type", None)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The explicit model_type supplied by genai_bundle.py remains in **kwargs, while this probe always reloads the model’s native HF type. For an override such as qwen3_transformer_only, this therefore selects the stock qwen3 composite instead of the requested registered variant, despite the comments below claiming the override is honored. Please consume the explicit override before this lookup and use it as the registry key.

🤖 Generated with GitHub Copilot CLI

ep=kwargs.get("ep"),
no_compile=no_compile,
model_type=model_type,
ep=short_ep_name(ep_device.device.ep_name),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All remaining build controls are dropped on the non-composite HF path. perf.py passes no_compile, skip_optimize, and hack_max_optim_iterations, and GenAI companion builds pass no_compile, but neither this config call nor the later build_hf_model() call receives them. Those callers silently compile/optimize with defaults instead. Please explicitly consume and forward these controls, including their cache-key effects.

🤖 Generated with GitHub Copilot CLI

device=device, # pass user's original device string; WinMLSession handles "auto"
ep=resolved_ep,
provider_options=provider_options,
ep_device=ep_device,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The single-model wrappers receive only ep_device. provider_options and session_options accepted upstream never reach WinMLSession, so perf --ep-options silently has no effect and caller-supplied session settings are discarded on both ONNX and HF single-model paths. Please thread these runtime options through the wrapper constructor into WinMLSession.

🤖 Generated with GitHub Copilot CLI

samples : list[list[dict]] -- per-sample operator lists
"""
rows = _read_csv(csv_path)
metadata = _extract_metadata(rows)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parser keeps only the first sample’s ROOT timing metadata but aggregates operator cycles across every sample. QNNMonitor then converts all samples with that first sample’s cycle-to-time ratio, which is invalid when clocking changes. In addition, the monitor observes both warmup and measured iterations, but all parsed samples are included in averages, percentiles, and counts. Please preserve per-sample timing metadata and exclude the configured warmup samples before aggregation.

🤖 Generated with GitHub Copilot CLI

and parse this JSON directly. Used by
:py:meth:`parse_existing_artifacts` for offline analysis.
"""
csv_path = self._csv_path

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This accepts any existing profiling_output.csv, but __enter__() neither removes the fixed-path file nor records its timestamp. If the current run produces no CSV, a stale artifact from an earlier run is parsed and reported as fresh profiling data. Please remove/rotate the artifact before starting or verify that it was created by the current run.

🤖 Generated with GitHub Copilot CLI

output_dir = self.config.output_path.parent if self.config.output_path else Path.cwd()
ep_monitor = _open_ep_monitor_or_exit(
op_tracing=self.config.op_tracing,
ep=self.config.ep,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Monitor dispatch is using the raw CLI value instead of the resolved EP. A documented canonical input such as QNNExecutionProvider resolves successfully earlier, but _resolve_ep_monitor() lowercases it to qnnexecutionprovider, which never matches its "qnn" branch and raises “not available.” Please dispatch using the normalized short name from the resolved target.

🤖 Generated with GitHub Copilot CLI


# P0: Temporal Localization
start_time_us: float | None = None
duration_us: float = 0.0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This replacement drops the ONNX metadata enrichment already present on the target branch. OperatorMetrics previously exposed onnx_attributes and onnx_inputs, populated when WINMLCLI_OP_ADD_DATA was enabled; the new dataclass has no corresponding fields, and the QNN monitor only builds an op-type map. Existing consumers enabling that feature now silently lose the data. Please preserve the fields, population path, and regression coverage.

🤖 Generated with GitHub Copilot CLI

probe availability get a coherent answer without having to know
about the CLI-layer refusal.
"""
return False

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR describes this file as the second op-tracing monitor implementation, but the implementation is permanently unavailable and perf.py unconditionally rejects even basic OpenVINO tracing. The added tests only pin that stub behavior, so none of the advertised OpenVINO tracing path is delivered. Please either provide the working monitor and dispatch path or remove this feature from the PR’s claimed scope rather than shipping an unreachable implementation.

🤖 Generated with GitHub Copilot CLI

tezheng added 2 commits July 16, 2026 15:24
The prior lint commit dropped the cli.py re-exports of extract_ep_options /
normalize_ep_name (they were unused there). This test imported both *through*
that facade, so collection broke. Import from their true home
(utils.constants) instead. Full-suite collection is clean again (8393 tests).

Claude-Session: https://claude.ai/code/session_01CRVeL37EPZvGhMpQNj1hTF
Main advanced 9 commits (telemetry, recipes, --export-type #1104), leaving
PR #1019 conflicting — which blocks the pull_request merge ref and therefore
all CI. Re-merge to make the PR mergeable again.

Conflicts, all in commands/build.py + its test (the genai-bundle path #1104
touched):
- Keep origin's --export-type optimized restructure of _maybe_build_genai_bundle
  (want_optimized vs implicit-shortcut), but graft this branch's
  session.resolve_device(EPDeviceTarget) API into it and into the new
  _resolve_optimized_target helper (origin still called the removed
  sysinfo.resolve_device).
- Keep this branch's defer-EP comment on build()'s ep-resolution (matches the
  actual resolve_device code; origin's resolve_check_device_ep comment did not).
- test_build_genai_bundle.py: restore MagicMock import (origin's new _fake_ctx
  needs it), and repoint origin's new --export-type tests from the removed
  sysinfo.resolve_device / resolve_check_device_ep onto session.resolve_device.

All 26 genai-bundle tests pass; build imports + ruff clean.

Claude-Session: https://claude.ai/code/session_01CRVeL37EPZvGhMpQNj1hTF
# Re-export resolve_eps at the module level so tests can patch it here.
# Aliased from session.available_eps_for_device — same shape (registered EPs
# for a device in catalog priority order); the sysinfo shim has been removed.
from ..session import available_eps_for_device as resolve_eps # noqa: F401
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants