Skip to content

feat(session): add GenaiSession for onnxruntime-genai pipeline inference#1015

Merged
DingmaomaoBJTU merged 9 commits into
mainfrom
dingmaomaobjtu-feat-genai-session-standalone
Jul 3, 2026
Merged

feat(session): add GenaiSession for onnxruntime-genai pipeline inference#1015
DingmaomaoBJTU merged 9 commits into
mainfrom
dingmaomaobjtu-feat-genai-session-standalone

Conversation

@DingmaomaoBJTU

@DingmaomaoBJTU DingmaomaoBJTU commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Closes #1011

Introduces GenaiSession, a generic wrapper around onnxruntime-genai (og.Model + og.Generator) for autoregressive text generation using multi-model decoder pipelines (ctx + iter + embeddings + lm_head), plus a winml-genai perf runtime so winml perf can benchmark these bundles.

Usage

Benchmark a genai bundle directory (one containing genai_config.json, as produced by a winml-cli export) with winml perf --runtime winml-genai:

# Mixed execution (default): ctx/iter route to the accelerator via each stage's
# session_options, embeddings/lm_head stay on CPU.
winml perf -m path/to/bundle --runtime winml-genai

# Pre-compile EPContext-capable stages (e.g. QNN) before benchmarking:
winml perf -m path/to/bundle --runtime winml-genai --compile \
  --prompt "Explain the theory of relativity in simple terms." \
  --max-new-tokens 128 --iterations 10 --warmup 2
image

genai-specific flags:

Flag Default Description
--runtime winml-genai winml Benchmark an onnxruntime-genai bundle (LLM generation) instead of single-shot ONNX inference.
--device auto auto→mixed, npu→qnn, gpu→dml, cpu→CPU-only.
--compile / --no-compile off Pre-compile EPContext-capable stages before the run (JIT otherwise).
--compile-timeout 300 Max seconds to compile each EPContext stage before falling back to the original ONNX.
--prompt built-in Prompt to generate from (wrapped in the bundle's own chat template before timing).
--max-new-tokens 128 New tokens generated per iteration.
--iterations / --warmup 10 / 2 Timed / warmup generations (genai-specific low defaults; a full generation is far costlier than one session.run()).
-o, --output auto JSON report path (~/.cache/winml/perf/<bundle>/<ts>.json otherwise).

Reported metrics come straight from onnxruntime-genai call boundaries — TTFT/prefill measured around append_tokens, decode tokens/sec and per-output-token latency (TPOT) around generate_next_token.

The benchmark prompt is wrapped in the bundle's own chat template (via og.Tokenizer.apply_chat_template) before timing, so prefill reflects realistic chat usage; bundles that ship no template benchmark the raw prompt.

Programmatic use:

from winml.modelkit.session import GenaiSession, GenerationConfig

with GenaiSession("path/to/bundle", ep="mixed") as sess:
    for tok in sess.generate_streaming("Hello, world!", GenerationConfig(max_new_tokens=64)):
        print(tok, end="", flush=True)

Changes

src/winml/modelkit/session/genai_session.py (new)

  • GenaiSession — context manager wrapping og.Model + og.Generator
  • generate_streaming() / generate() — streaming and full-string generation; generate_timed() returns a GenerationTiming with TTFT/TPOT sourced from ort-genai call boundaries
  • EPContext pre-compilation via _prepare_compiled_bundle(); each pipeline stage's EP is resolved generically from its genai_config.json provider_options via WinMLCompileConfig.for_provider() (QNN, OpenVINO, VitisAI, …) — no provider name is hardcoded — and compiled in an isolated subprocess that delegates to the shared compiler component (compile_onnx) with a configurable compile_timeout (default 300 s). Stages on non-EPContext EPs (CPU, DML) stay on JIT. A .meta.json marker invalidates a cached stage when its provider options change or the source weights (.onnx / .data sidecar) are newer.
  • WinML EP auto-registration for QNN/DML/mixed bundles; static KV cache length read from genai_config.json
  • apply_chat_template() renders the prompt through the bundle's own chat template (model-driven via og.Tokenizer; no chat format hardcoded)
  • _mirror_non_onnx_files() symlinks non-compiled assets into the _compiled/ subdir
  • GenerationConfig / GenerationTiming dataclasses and GenaiSessionError / GenaiNotInstalledError / GenaiLoadError hierarchy

src/winml/modelkit/commands/_perf_genai.py (new) — the winml-genai perf runtime: GenaiPerfConfig, the generation benchmark loop, LLM metric aggregation (TTFT percentiles, decode tokens/sec, avg output-token latency), and rich/JSON reporting.

src/winml/modelkit/commands/perf.py — adds --runtime {winml,winml-genai}, --prompt, --max-new-tokens, and --compile-timeout. winml-genai takes a bundle directory and defaults --device auto to mixed execution so each stage's per-stage EP routing (ctx/iter on the accelerator, embeddings/lm_head on CPU) is honored. winml-only flags are warned-and-ignored; genai imports are function-local so perf --help stays fast.

src/winml/modelkit/session/__init__.py — exports GenaiSession, GenerationConfig, GenerationTiming, and the exception classes.

pyproject.toml — adds onnxruntime-genai-winml==0.14.1.

Teststests/unit/session/test_genai_session.py and tests/unit/commands/test_perf_genai.py (mock onnxruntime_genai, no hardware needed): init/load lifecycle, EP registration, streaming, chat-template application, generic per-EP compile dispatch, EPContext cache invalidation, _prepare_compiled_bundle, _mirror_non_onnx_files, _patch_stage_filename, compile_timeout propagation, timing math with an injected clock, and full CLI dispatch/validation.

@DingmaomaoBJTU DingmaomaoBJTU requested a review from a team as a code owner July 1, 2026 07:16
Comment thread src/winml/modelkit/session/genai_session.py Fixed
Comment thread tests/unit/commands/test_perf_genai.py Fixed
github-actions Bot added 4 commits July 1, 2026 20:20
…oder inference

Introduces GenaiSession, a generic wrapper around onnxruntime-genai
(og.Model + og.Generator) for autoregressive text generation using
multi-model decoder pipelines (ctx + iter + embeddings + lm_head).

Key features:
- Context manager lifecycle (load/unload og.Model)
- Streaming token generation via generate_streaming()
- QNN EPContext pre-compilation with subprocess isolation and configurable timeout
- WinML EP auto-registration for QNN/DML/mixed EPs
- Static KV cache length read from genai_config.json
- apply_chatml_template() for ChatML-format models (Qwen, Yi, Mistral, etc.)
- _mirror_non_onnx_files() symlinks non-compiled assets into _compiled/ subdir

Generalization vs. earlier Qwen3-specific draft:
- Error message in __init__ no longer references Qwen3 export scripts
- compile_timeout constructor parameter (default 300s) replaces hardcoded value
- Removed model-specific timing comment from _compile_stage
…p_compile_worker module

Move _qnn_compile_worker out of genai_session.py into
src/winml/modelkit/_ep_compile_worker.py as qnn_compile_to_ep_context.

The worker must live at module scope for multiprocessing.spawn on Windows
(pickle serialises it by module+name). Moving it to modelkit/ level makes
it importable by any future caller without creating circular imports
(compiler/ already depends on session/, so the worker sits above both).

genai_session.py now imports it as:
    from .._ep_compile_worker import qnn_compile_to_ep_context as _qnn_compile_worker
…r component

Replace the hand-rolled QNN EPContext worker (_ep_compile_worker.py /
qnn_compile_to_ep_context, which duplicated the ORT SessionOptions +
add_ep_for_device + ModelCompiler wiring) with a thin subprocess target
that calls compiler.compile_onnx().

The compiler component already owns all EP-specific EPContext logic,
including provider registration, external .bin handling, and
ep_cache_context patching, so GenaiSession no longer reimplements it.
GenaiSession keeps the spawn-subprocess + configurable timeout wrapper
for hang protection; only the compile body now delegates.

Also add onnxruntime-genai-winml==0.14.1 to core dependencies so the
GenaiSession runtime is available out of the box alongside
onnxruntime-windowsml.

Tests: add TestCompileStageWorker covering delegation to compile_onnx,
provider-option forwarding onto the QNN EP config, and RuntimeError on
an unsuccessful CompileResult.
…hmarking

Adds a --runtime-type option to `winml perf` (default: winml). Selecting
winml-genai benchmarks a prebuilt onnxruntime-genai bundle folder with LLM
generation metrics instead of single-shot inference.

- New commands/_perf_genai.py: GenaiPerfBenchmark measures TTFT, decode
  tokens/sec, average per-token latency, and total generation time via
  GenaiSession.generate_streaming()
- New CLI options: --prompt, --max-new-tokens, --compile-timeout
- --device maps to the genai EP (cpu->cpu, npu->qnn, gpu->dml); honors
  --compile for QNN EPContext pre-compilation
- genai defaults: iterations=10, warmup=2 when not explicitly provided
- WinML-only flags (--memory/--monitor/--op-tracing) warn and are ignored
- Reuses existing report/output helpers; JSON and text output supported

Tests: tests/unit/commands/test_perf_genai.py plus coverage for the
existing GenaiSession.encode() used to pre-encode prompts.
@DingmaomaoBJTU DingmaomaoBJTU force-pushed the dingmaomaobjtu-feat-genai-session-standalone branch from 22e29cf to c411862 Compare July 2, 2026 01:03
…ve timing from ort-genai

Makes GenaiSession pre-compilation fully architecture-agnostic and sources
perf timing from onnxruntime-genai call boundaries instead of manual spans.

EPContext generalization (removes QNN-specific hardcoding):
- Resolve each pipeline stage EP generically via WinMLCompileConfig.for_provider
  instead of hardcoding for_qnn(); stages on non-EPContext EPs (CPU, DML) stay
  on JIT automatically (for_provider returns None).
- Thread ep_alias through _compile_stage / _compile_stage_worker; raise a clear
  RuntimeError when an EP has no offline EPContext compile step.
- Add _resolve_stage_ep() as the single point of EP dispatch (no provider name
  is hardcoded anywhere in the session).

Stale-cache fix:
- _epcontext_is_fresh() + a .meta.json marker invalidate a cached compiled
  stage on changed provider options or a newer external-weights .data sidecar,
  not just the source .onnx mtime.

Timing:
- Add GenerationTiming + generate_timed(); TTFT/TPOT are measured at
  append_tokens (prefill) and generate_next_token (decode) boundaries.

- EP-neutral wording in perf --compile-timeout help and docstrings.
Comment thread src/winml/modelkit/session/genai_session.py Fixed
- Treat onnxruntime-genai-winml as untyped in mypy config (no stubs) and drop the now-unused import-not-found ignore in ep_registry

- Type og.* handles (_model, _tokenizer, _import_og, _new_generator) as Any so attribute access checks against the untyped module

- Coerce tokenizer encode/decode and freshness-check returns to concrete types to satisfy warn_return_any

- Remove redundant 'del generator' try/finally blocks flagged by CodeQL (locals release on frame teardown)

- Replace empty '...' __init__ body with 'pass' in a test stub
Comment thread src/winml/modelkit/commands/perf.py Outdated
@DingmaomaoBJTU DingmaomaoBJTU changed the title feat(session): add GenaiSession for onnxruntime-genai decoder pipeline inference feat(session): add GenaiSession for onnxruntime-genai pipeline inference Jul 2, 2026
Comment thread src/winml/modelkit/commands/_perf_genai.py
Comment thread src/winml/modelkit/commands/perf.py Outdated
Comment thread src/winml/modelkit/session/genai_session.py
Comment thread src/winml/modelkit/session/genai_session.py Outdated
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 Outdated
Comment thread src/winml/modelkit/commands/_perf_genai.py Outdated
Comment thread src/winml/modelkit/commands/perf.py
…rf flag to --runtime

Address review feedback on the GenaiSession PR:

- Derive WinML EP registration and stage pre-compilation from the bundle's
  genai_config.json (new GenaiSession._bundle_uses_hardware_ep) instead of the
  hardcoded _NEEDS_WINML_EPS set; the `ep` argument is now informational.
  Overriding the config via --ep/--device is tracked in #1025.
- Reuse onnx.is_compiled_onnx to skip stages whose source ONNX is already an
  EPContext model, with a parse-failure fallback to the normal compile path.
- Remove the unused `og` parameter from _register_eps.
- Rename `winml perf --runtime-type` to `--runtime` (values winml|winml-genai)
  and the benchmark_info `runtime_type` key to `runtime`, converging with the
  --runtime flag proposed in #960.
Comment thread src/winml/modelkit/session/genai_session.py Outdated
Comment thread src/winml/modelkit/session/genai_session.py
github-actions Bot added 2 commits July 2, 2026 16:36
…Session

- perf: move the --prompt default into the click option and drop the
  default_genai_prompt() helper (the default is a plain literal, no import cost)
- perf: define RuntimeName/RUNTIME_NAMES and type --runtime as a closed Choice,
  mirroring the --compiler / COMPILER_NAMES convention
- perf: genai_output_path now delegates to perf.generate_output_path so both
  runtimes share one ~/.cache/winml/perf/<slug>/<ts>.json convention
- session: reword the onnxruntime_genai import error to cover both
  not-installed and installed-but-native-load-failed cases, and include the
  underlying exception
- session: drop the decorative _VALID_EPS frozenset and ep validation; ep is a
  free-form reporting label (EP routing is driven by genai_config.json; override
  path tracked in #1025)
… benchmark prompt

Replaces the hardcoded ChatML helper (apply_chatml_template) with a
model-driven GenaiSession.apply_chat_template() that renders the prompt
through og.Tokenizer.apply_chat_template using the bundle's own template
(the chat_template.jinja sidecar, or the tokenizer-embedded template), so
ChatML / Llama [INST] / Phi etc. all work with no model-specific code here.

winml perf --runtime winml-genai now wraps the prompt in that template
before timing (falling back to the raw prompt for bundles that ship none),
so the measured prefill reflects realistic chat usage.

Addresses the PR #1015 review thread on apply_chatml_template.
@DingmaomaoBJTU DingmaomaoBJTU merged commit 695c5b9 into main Jul 3, 2026
9 checks passed
@DingmaomaoBJTU DingmaomaoBJTU deleted the dingmaomaobjtu-feat-genai-session-standalone branch July 3, 2026 02:24
DingmaomaoBJTU pushed a commit that referenced this pull request Jul 3, 2026
Brings in winml perf --runtime winml-genai bundle benchmarking (#1015) and
drops stale pre-#836 leftovers from the PR diff by advancing the merge-base.
DingmaomaoBJTU added a commit that referenced this pull request Jul 6, 2026
…ing (#1042)

Closes #1030 (follow-up to #1015).

## Problem

`winml perf --runtime winml-genai` always wraps `--prompt` in the
bundle's own chat template before timing. There was no way to benchmark
a prompt that is **already** formatted (or a raw completion prompt), so
users couldn't test their own templated input.

## Change

Add `--apply-template/--no-apply-template` (default **on**, preserving
current behavior). With `--no-apply-template`, the prompt is encoded and
timed verbatim.

- `GenaiPerfConfig.apply_template` gates
`GenaiPerfBenchmark._prompt_text`; when disabled, the prompt is used
as-is with no `apply_chat_template` call.
- `apply_template` is recorded in the JSON report's `benchmark_info` for
reproducibility.
- The flag is wired through the `perf` command and documented in
`--prompt` / `--apply-template` help text.

## Usage

```bash
# Default: prompt wrapped in the bundle's chat template
winml perf -m path/to/bundle --runtime winml-genai --prompt "Hello"

# Benchmark a pre-formatted prompt verbatim
winml perf -m path/to/bundle --runtime winml-genai \
  --no-apply-template --prompt "<|user|>Hello<|end|><|assistant|>"
```

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@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.

feat: add genai inference to perf command

4 participants