Skip to content

feat(genai): honor --ep/--device as an EP override for GenaiSession (#1025)#1046

Merged
DingmaomaoBJTU merged 9 commits into
mainfrom
dingmaomaobjtu-genai-session-ep-device-override
Jul 10, 2026
Merged

feat(genai): honor --ep/--device as an EP override for GenaiSession (#1025)#1046
DingmaomaoBJTU merged 9 commits into
mainfrom
dingmaomaobjtu-genai-session-ep-device-override

Conversation

@DingmaomaoBJTU

Copy link
Copy Markdown
Collaborator

Summary

Closes #1025.

Previously GenaiSession's ep argument (and winml perf --runtime winml-genai's --ep/--device) was informational only: EP registration and pre-compilation were decided purely from the bundle's genai_config.json per-stage session_options. This PR adds an explicit override path so callers can force EP routing that differs from the bundle config, with precedence explicit arg > bundle config.

What changed

GenaiSession

  • ep default is now None (was "cpu"), and the "mixed" sentinel is retired.
    • ep=Nonerespect the bundle config unchanged (registration + compile derive from genai_config.json exactly as before).
    • ep=<concrete EP>force the whole decoder pipeline onto that EP by rewriting every stage's provider_options:
      • cpu strips hardware providers (CPU fallback; no WinML EP registration/compile).
      • a hardware EP routes every stage to it (registers the WinML EPs it needs; pre-compiles when compile=True). A stage's existing options are carried over only when it already targeted the same canonical EP.
  • Registration and compile gates now derive from the effective (post-override) config via the existing generic _bundle_uses_hardware_ep — no hardcoded EP short-name → behavior map.
  • Because og.Config.clear_providers/append_provider can only touch the top-level provider (not per-stage session_options), an override loads og.Model from a derived _compiled/ bundle. _prepare_compiled_bundle now writes that derived bundle for override-only passes too (not just compilation).
  • Invalid ep values raise ValueError (validated against the shared EP union), so an override can never silently become a no-op.

perf --runtime winml-genai

  • Precedence: explicit --ep > concrete --device > respect config. Removed ep from _GENAI_IGNORED_FLAGS (it is now honored).
  • device_to_genai_ep returns EPNameOrAlias | Noneauto (and any unrecognized device) maps to None (respect config), replacing the old auto → "mixed".
  • Reporting (benchmark_info.ep / device line) shows the resolved override alias, or "config" when respecting the bundle.

Design notes / compatibility

  • The rewrite and validation are generic — EPs are compared/aliased through the shared EP union (normalize_ep_name / EP_NAME_TO_ALIAS / EP_NAMES); the only literal provider name is the universal cpu fallback.
  • The always-safe override direction is cpu. Forcing a hardware EP onto stages the bundle did not build for it (e.g. embeddings/lm_head) may fall back to JIT/CPU inside onnxruntime-genai, which is documented in the ep docstring.

…1025)

Previously GenaiSession's `ep` arg was informational only: EP registration and pre-compilation were decided purely from the bundle's genai_config.json per-stage session_options. This adds an explicit override path with precedence explicit arg > bundle config.

GenaiSession(ep=None) (new default, replacing `cpu` and retiring the `mixed` sentinel) respects the config unchanged. A concrete ep forces the whole decoder pipeline onto that EP by rewriting every stage's provider_options: `cpu` strips hardware providers (no WinML EP registration/compile), a hardware EP routes every stage to it (registering EPs, compiling when requested). Registration and compile gates now derive from the effective post-override config. Because og.Config cannot override per-stage session_options, an override loads og.Model from a derived _compiled/ bundle.

perf --runtime winml-genai now applies precedence explicit --ep > concrete --device > respect config; device_to_genai_ep maps auto/unknown to None; reporting shows the resolved override alias or `config`.

The rewrite/validation is generic (shared EP union, no hardcoded EP short-name behavior map). Adds 40+ tests (137 pass across the two affected modules).
@DingmaomaoBJTU
DingmaomaoBJTU requested a review from a team as a code owner July 3, 2026 09:31
Comment thread src/winml/modelkit/commands/_perf_genai.py Outdated
github-actions Bot added 5 commits July 6, 2026 11:33
The derived-bundle EPContext cache was keyed only on the pipeline stage
name ({stage}_ctx.onnx) and freshness compared provider_options alone.
The new ep override can route the same stage onto different
EPContext-capable providers (QNN/OpenVINO/VitisAI) across runs; when a
non-native EP is forced both runs present empty provider_options, so a
binary compiled for EP-A could be reused for EP-B (wrong-accelerator
load or silent misroute).

Encode the EP in the artifact name ({stage}_{ep}_ctx.onnx) so each EP
keeps a distinct cache, and additionally require the recorded EP to
match in the freshness check. Add regression tests for per-EP artifact
naming and cross-EP cache non-reuse.
…session-ep-device-override

# Conflicts:
#	src/winml/modelkit/session/genai_session.py
Replace the hardcoded _DEVICE_TO_GENAI_EP map (npu->qnn, gpu->dml) with
resolve_genai_ep(), which reuses the same resolve_device/resolve_eps path
the WinML ONNX runtime uses so a concrete --device picks the best EP
actually available for that device on this machine (e.g. an NPU that is
VitisAI/OpenVINO rather than QNN) instead of a static short-name guess.

Add a 'config' sentinel (winml-genai default) meaning 'respect the
bundle's genai_config.json per-stage routing': omitting --device maps to
it, --device config selects it explicitly, and the winml runtime rejects
it. --device auto now matches ONNX (best device -> best EP, whole
pipeline). Precedence unchanged: --ep > --device > config.
…session-ep-device-override

# Conflicts:
#	tests/unit/session/test_genai_session.py
Add two e2e tiers to test_perf_e2e.py for the genai EP-override path:

- TestPerfGenaiContract (no bundle): asserts perf --help advertises the
  'config' device sentinel and that --device config is rejected on the
  ONNX runtime with a genai-only usage error (exit 2).
- TestPerfGenai (slow, network): builds a tiny Qwen3-0.6B int4 CPU genai
  bundle once per class via the onnxruntime-genai model builder, then
  benchmarks the four device/ep resolutions -- default and --device config
  respect the bundle (device=ep=config), --ep cpu overrides the EP only,
  and --device cpu overrides both device and EP. Skips gracefully when the
  LLM stack is absent or the model build fails.
Comment thread tests/e2e/test_perf_e2e.py Fixed
Bind proc = None in the TimeoutExpired handler so proc is definitely
initialized on every path, then skip on None (timeout) vs non-zero
returncode (build failure). Resolves the code-scanning 'potentially
uninitialized local variable' alert; happy path is unchanged.
Comment thread src/winml/modelkit/session/genai_session.py Outdated
Comment thread src/winml/modelkit/session/genai_session.py
Comment thread src/winml/modelkit/session/genai_session.py
github-actions Bot added 2 commits July 7, 2026 15:07
Address PR review comments on the GenaiSession --ep/--device override:

- Device-aware provider_options: when re-routing a stage to a
  device-parameterized EP (OpenVINO/VitisAI) that the bundle defines no
  options for, synthesize {device_type: <DEVICE>} from --device (falling
  back to the EP's primary supported device). QNN reuses the bundle's own
  backend_path/soc_model when present, else warns and writes empty options
  instead of hardcoding a backend.

- Add a device param to GenaiSession so the forced EP can target a concrete
  device; perf threads --device through via _session_device.

- Report 'config' (not the requested EP) when an override took no effect
  (flat/all-CPU pipeline). effective_ep is computed from the post-override
  config, and load() warns when a requested override is a no-op.

- Only re-route pipeline stages already on a hardware EP; leave CPU-intended
  stages (embeddings/lm_head) on CPU. Forcing large CPU graphs onto QNN HTP
  triggered a multi-minute on-the-fly compile that looked like a hang on
  --device npu; skipping them keeps the forced-EP path as fast as config.

Verified end-to-end on a QNN bundle: --device npu reports ep=qnn/device=npu
and completes without the prior hang.
@DingmaomaoBJTU
DingmaomaoBJTU merged commit 130acfe into main Jul 10, 2026
9 checks passed
@DingmaomaoBJTU
DingmaomaoBJTU deleted the dingmaomaobjtu-genai-session-ep-device-override branch July 10, 2026 03:43
DingmaomaoBJTU added a commit that referenced this pull request Jul 20, 2026
On OpenVINO hosts, resolve_genai_ep('cpu') incorrectly resolved to
'openvino' because OpenVINO advertises (npu, gpu, cpu) support and
appears before CPUExecutionProvider in the priority list.

Generic fix: prefer EPs whose *primary* device (first entry in
EP_SUPPORTED_DEVICES) matches the resolved device. Multi-device EPs
like OpenVINO target npu primarily — when the user says --device cpu
or --device gpu they expect the native EP, not a cross-device
accelerator that also happens to support it.

Examples:
  --device cpu  → CPUExecutionProvider (not OpenVINO)
  --device gpu  → DmlExecutionProvider (not OpenVINO/QNN)
  --device npu  → QNN/OpenVINO/VitisAI (unchanged, all primary=npu)

Falls back to eps[0] when no native EP is available.

Fixes the born-broken test_device_cpu_overrides_device_and_ep on OV
hosts (PR #1046).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
DingmaomaoBJTU added a commit that referenced this pull request Jul 20, 2026
…1138)

## Summary

On OpenVINO hosts, `resolve_genai_ep("cpu")` incorrectly resolved to
`"openvino"` instead of `"cpu"`, causing
`test_device_cpu_overrides_device_and_ep` to fail (born-broken since
#1046).

## Root Cause

`resolve_eps("cpu")` returns EPs in `_DEVICE_EP_MAP` priority order
derived from `EP_SUPPORTED_DEVICES` declaration order. Since
`OpenVINOExecutionProvider` declares `("npu", "gpu", "cpu")` and appears
before `CPUExecutionProvider`, the old code blindly took `eps[0]` →
`"openvino"`.

## Fix (generic)

Prefer EPs whose **primary device** (first entry in
`EP_SUPPORTED_DEVICES`) matches the resolved device. Multi-device EPs
like OpenVINO target `npu` primarily — when the user says `--device cpu`
or `--device gpu` they expect the native EP, not a cross-device
accelerator.

| `--device` | Before (OV host) | After |
|---|---|---|
| `cpu` | `openvino` ❌ | `cpu` ✅ |
| `gpu` | `openvino` (if OV first) | `dml`/`cuda` ✅ |
| `npu` | `qnn`/`openvino` | unchanged (all primary=npu) |

Falls back to `eps[0]` when no device-native EP is available.

## Verification

- Unit tests updated (11 pass, including new GPU cross-device case)
- E2E `test_device_cpu_overrides_device_and_ep` passes on OV host
- Ruff clean

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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.

GenaiSession: support --ep/--device to override genai_config.json EP routing

3 participants