feat(session): add GenaiSession for onnxruntime-genai pipeline inference#1015
Merged
DingmaomaoBJTU merged 9 commits intoJul 3, 2026
Merged
Conversation
…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.
22e29cf to
c411862
Compare
…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.
timenick
approved these changes
Jul 2, 2026
- 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
xieofxie
reviewed
Jul 2, 2026
xieofxie
reviewed
Jul 2, 2026
xieofxie
reviewed
Jul 2, 2026
xieofxie
reviewed
Jul 2, 2026
xieofxie
reviewed
Jul 2, 2026
xieofxie
reviewed
Jul 2, 2026
xieofxie
reviewed
Jul 2, 2026
xieofxie
reviewed
Jul 2, 2026
xieofxie
reviewed
Jul 2, 2026
xieofxie
reviewed
Jul 2, 2026
3 tasks
…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.
xieofxie
reviewed
Jul 2, 2026
xieofxie
reviewed
Jul 2, 2026
This was referenced Jul 2, 2026
Open
…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.
xieofxie
approved these changes
Jul 3, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1011
Introduces
GenaiSession, a generic wrapper aroundonnxruntime-genai(og.Model+og.Generator) for autoregressive text generation using multi-model decoder pipelines (ctx + iter + embeddings + lm_head), plus awinml-genaiperf runtime sowinml perfcan benchmark these bundles.Usage
Benchmark a genai bundle directory (one containing
genai_config.json, as produced by a winml-cli export) withwinml perf --runtime winml-genai:genai-specific flags:
--runtime winml-genaiwinml--deviceautoauto→mixed,npu→qnn,gpu→dml,cpu→CPU-only.--compile/--no-compile--compile-timeout300--prompt--max-new-tokens128--iterations/--warmup10/2session.run()).-o, --output~/.cache/winml/perf/<bundle>/<ts>.jsonotherwise).Reported metrics come straight from
onnxruntime-genaicall boundaries — TTFT/prefill measured aroundappend_tokens, decode tokens/sec and per-output-token latency (TPOT) aroundgenerate_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:
Changes
src/winml/modelkit/session/genai_session.py(new)GenaiSession— context manager wrappingog.Model+og.Generatorgenerate_streaming()/generate()— streaming and full-string generation;generate_timed()returns aGenerationTimingwith TTFT/TPOT sourced from ort-genai call boundaries_prepare_compiled_bundle(); each pipeline stage's EP is resolved generically from itsgenai_config.jsonprovider_optionsviaWinMLCompileConfig.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 configurablecompile_timeout(default 300 s). Stages on non-EPContext EPs (CPU, DML) stay on JIT. A.meta.jsonmarker invalidates a cached stage when its provider options change or the source weights (.onnx/.datasidecar) are newer.genai_config.jsonapply_chat_template()renders the prompt through the bundle's own chat template (model-driven viaog.Tokenizer; no chat format hardcoded)_mirror_non_onnx_files()symlinks non-compiled assets into the_compiled/subdirGenerationConfig/GenerationTimingdataclasses andGenaiSessionError/GenaiNotInstalledError/GenaiLoadErrorhierarchysrc/winml/modelkit/commands/_perf_genai.py(new) — thewinml-genaiperf 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-genaitakes a bundle directory and defaults--device autoto 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 soperf --helpstays fast.src/winml/modelkit/session/__init__.py— exportsGenaiSession,GenerationConfig,GenerationTiming, and the exception classes.pyproject.toml— addsonnxruntime-genai-winml==0.14.1.Tests —
tests/unit/session/test_genai_session.pyandtests/unit/commands/test_perf_genai.py(mockonnxruntime_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_timeoutpropagation, timing math with an injected clock, and full CLI dispatch/validation.