Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 60 additions & 4 deletions docs/commands/compile.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# winml compile

> Compile an ONNX model to an EP-specific format for fast runtime loading.
> Compile ONNX to an EP-specific format, or explicitly compile CGIR to device-targeted CG Output IR.

## When to use this

Expand All @@ -18,14 +18,15 @@ $ winml compile [options]

| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
| `--model` | `-m` | path | *(required unless `--list`)* | Input ONNX model file. |
| `--model` | `-m` | path | *(required unless `--list`)* | Input ONNX model, or complete CGIR in explicit IR mode. |
| `--output` | `-o` | path | — | Output file path (e.g., `model_compiled.onnx`). Takes precedence over `--output-dir`. |
| `--output-dir` | | path | same dir as input | Directory to write compiled output artifacts. |
| `--device` | `-d` | choice | `auto` | Target device: `auto`, `npu`, `gpu`, or `cpu`. |
| `--device-luid` | | string | — | Explicit CGC IR mode only: GPU adapter LUID from `winml sys`, in `0xHHHHHHHH_0xLLLLLLLL` form. |
| `--ep` | | `TEXT` | — | Force a specific execution provider, overriding device-to-provider mapping. Accepts full names (e.g., `QNNExecutionProvider`) or aliases (`qnn`, `dml`, `openvino`, `vitisai`, `migraphx`, `cpu`, `nvtensorrtrtx`). |
| `--ep-options` | | `KEY=VALUE` | — | Execution-provider option for the compilation session. Repeat for multiple options; later duplicate keys win. Explicit CLI values override matching `compile.provider_options` keys from `--config`. |
| `--validate` / `--no-validate` | | flag | `--validate` | Run a post-compilation validation pass on the target hardware. Enabled by default; pass `--no-validate` to skip when the target hardware or driver is unavailable. |
| `--compiler` | | choice | `ort` | Compiler backend: `ort` (ONNX Runtime) or `qairt` (Qualcomm AI Runtime Tools). |
| `--validate` / `--no-validate` | | flag | `--validate` | EPContext: post-compilation inference validation. CG Output IR: reload, pipeline build and I/O schema validation, without inference. |
| `--compiler` | | choice | `ort` | Backends: `ort`, `ort_session`, `qairt`, `cgc`. `cgc` is inferred for a single `.mlir` or `.mlirbc` input. |
| `--qnn-sdk-root` | | path | `None` | Path to the QNN SDK root directory. |
| `--embed/--no-embed` | | flag | `false` | Embed the EP context blob inside the ONNX file instead of writing a separate `.bin` file. |
| `--list` | | flag | `false` | List available compiler backends for the selected device and exit without compiling. |
Expand All @@ -48,6 +49,61 @@ If a provider option names a compiler input file, list its key in
that option path and fingerprints its contents for EPContext cache identity;
other provider-option strings are always passed through unchanged.

## CGIR to CG Output IR

This explicit mode uses the Windows ML Runtime API directly, without an ONNX
Runtime EP. The input and output selectors make the representation unambiguous:

```powershell
winml compile `
-m input.mlir `
--compiler cgc `
--device gpu `
-o output.mlir
```

`--compiler cgc` is optional for `.mlir` and `.mlirbc` input. Specify it when
the CGIR file uses another suffix or when you want to make the backend explicit.

The command loads CGIR through Windows ML Runtime. Both textual and bytecode
CGIR are accepted; invalid or unsupported input is reported by the Runtime.
Supply a complete executable model with its weights, not a graph-only report
projection.

Compilation calls the selected GPU target's Runtime
`ModelCompiler.compile_to_file()` with `CompiledModelForm.DEVICE_TARGETED`.
The saved artifact is **MLIR bytecode**, even when named `.mlir`; text dumping
is not provided by this command. `.mlirbc` is also accepted as an output suffix.
No optimization or quantization pipeline runs.

This first version supports GPU only (the default in explicit IR mode). A single
GPU is selected automatically; multiple GPUs require `--device-luid`. GPU
discovery uses DXCore, not EP discovery or provider installation.

Default `--validate` reloads the compiled artifact, builds a Runtime pipeline and
checks that its I/O schema matches the input. Unlike the EPContext validation
path, this does not execute inference or certify accuracy. `--no-validate` skips
that reload/build check; genuine Input IR parsing and compilation remain required.
Run `winml perf` separately for performance.

The command publishes the artifact and any Runtime-produced resource companions
only after successful compilation and requested validation. It preserves an
existing input's `<stem>_metadata.json` I/O bindings as output metadata and writes
`<output-stem>_compile.json` with input/artifact hashes, output encoding, GPU LUID
and validation status. Metadata and resource collisions also require
`--overwrite`; the input and its metadata are never replaced. Without `-o`, the
default is `<input-stem>_output.mlir` in `--output-dir` or beside the input.

`--ep`, `--ep-options`, `--embed`/`--no-embed`, `--qnn-sdk-root`, `--list`,
`--config` and multiple `-m` inputs are rejected in this mode. Use the existing
ONNX-to-EPContext mode for EP compilation. Omitting the new selectors preserves
the original command behavior.

The preview `windowsml` payload must include Runtime APIs compatible with the
active CLI. If preview wheels override stable project pins, invoke the
environment directly or use `uv run --no-sync` to avoid restoring stable
packages.

## Examples

```bash
Expand Down
15 changes: 15 additions & 0 deletions docs/getting-started/cgc-onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,21 @@ winml export -m .\model.onnx --target cgir -o .\model.mlir

The model must already be compatible with the CGC converter.

### CGIR to device-targeted CG Output IR

To retain a compiled artifact from existing CGIR, use the explicit
[CGIR compile mode](../commands/compile.md#cgir-to-cg-output-ir):

```powershell
winml compile -m .\model.mlir --compiler cgc --device gpu -o .\model-output.mlir
```

For `.mlir` and `.mlirbc` input, `--compiler cgc` is optional.

The artifact is bytecode, not textual MLIR. This is a separate compilation stage,
without an EP, optimization or quantization. Default validation reloads/builds
the artifact and checks I/O; it does not run inference or performance tests.

## Tutorials and samples

- [ResNet-50 - PyTorch to CGIR (experimental)](../samples/resnet50-cgir.md) walks through
Expand Down
85 changes: 79 additions & 6 deletions src/winml/modelkit/commands/compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@


if TYPE_CHECKING:
from typing import Literal, TypeGuard

from ..utils.constants import CompilerName, EPName, EPNameOrAlias
from ..utils.logging import configure_logging
from ._ep_arg import EpAtSourceParamType
Expand All @@ -62,11 +64,16 @@ def _resolve_compile_provider(resolved_device: str, ep: EPNameOrAlias | None) ->
console = Console()


def _is_epcontext_compiler(value: str) -> TypeGuard[CompilerName]:
return value in COMPILER_NAMES


@click.command()
@cli_utils.model_path_option(
required=False,
multiple=True,
help_text="Input ONNX model file. Repeat -m to compile multiple models with a shared "
help_text="Input model file (ONNX by default; CGIR input is also supported). "
"Repeat -m to compile multiple ONNX models with a shared "
"EP context (weight sharing). Required unless --list.",
)
@cli_utils.output_option("Output file path (e.g., model_compiled.onnx)")
Expand All @@ -84,6 +91,9 @@ def _resolve_compile_provider(resolved_device: str, ep: EPNameOrAlias | None) ->
default=None,
help="Target device (default: deduced from --ep, or 'npu' if neither given)",
)
@cli_utils.device_luid_option(
optional_message="Only supported for CGIR to CG Output IR compilation."
)
@click.option(
"--ep",
type=EpAtSourceParamType(),
Expand All @@ -98,14 +108,16 @@ def _resolve_compile_provider(resolved_device: str, ep: EPNameOrAlias | None) ->
@click.option(
"--validate/--no-validate",
default=True,
help="Validate compiled model (default: enabled)",
help="Validate compiled model. EPContext: inference; CG Output IR: reload, pipeline "
"build and I/O schema (no inference).",
)
@click.option(
"--compiler",
type=click.Choice(list(COMPILER_NAMES)),
type=click.Choice([*COMPILER_NAMES, "cgc"]),
default="ort",
help="Compiler backend (default: ort). 'ort_session' compiles via "
"ort.InferenceSession (ep.context_enable) — required for shared-context multi-model.",
"ort.InferenceSession (ep.context_enable) — required for shared-context multi-model. "
"'cgc' compiles CGIR through Windows ML Runtime and is inferred for .mlir/.mlirbc input.",
)
@click.option(
"--qnn-sdk-root",
Expand Down Expand Up @@ -137,18 +149,19 @@ def compile(
output_dir: Path | None,
overwrite: bool,
device: str | None,
device_luid: str | None,
ep: tuple[str, str | None] | None,
ep_options: tuple[str, ...],
validate: bool,
verbose: int,
quiet: bool,
compiler: CompilerName,
compiler: CompilerName | Literal["cgc"],
qnn_sdk_root: Path | None,
Comment thread
fangyangci marked this conversation as resolved.
embed: bool,
list_compilers_flag: bool,
config_file: Path | None,
) -> None:
r"""Compile ONNX model to EP-specific format.
r"""Compile ONNX to EPContext, or CGIR to CG Output IR.

This command compiles an ONNX model to an EP-specific format (e.g., QNN
EPContext).
Expand All @@ -166,10 +179,64 @@ def compile(

# Compile using QAIRT SDK
winml compile -m model.onnx --compiler qairt --qnn-sdk-root /path/to/sdk

# Compile CGIR to device-targeted bytecode CG Output IR
winml compile -m input.mlir --compiler cgc --device gpu -o output.mlir
"""
# Merge top-level -v/-q with subcommand-level flags so either position works.
verbose, quiet = cli_utils.resolve_verbosity(ctx, verbose, quiet)

compiler_was_provided = cli_utils.is_cli_provided(ctx, "compiler")
inferred_cgc = (
not compiler_was_provided
and len(model) == 1
and model[0].suffix.casefold() in {".mlir", ".mlirbc"}
)
ir_mode = compiler == "cgc" or inferred_cgc
if ir_mode:
incompatible = {
"--ep": ep is not None,
"--ep-options": bool(ep_options),
"--qnn-sdk-root": qnn_sdk_root is not None,
"--embed/--no-embed": cli_utils.is_cli_provided(ctx, "embed"),
"--list": list_compilers_flag,
"--config": config_file is not None,
}
rejected = [name for name, present in incompatible.items() if present]
if rejected:
raise click.UsageError(
f"CG Output IR compilation does not support: {', '.join(rejected)}."
)
if len(model) != 1:
raise click.UsageError("CG Output IR compilation requires exactly one --model.")
if device not in {None, "gpu"}:
raise click.UsageError("CG Output IR compilation currently requires --device gpu.")
from ..compiler import compile_cgc_ir

source = model[0]
destination = output or (output_dir or source.parent) / f"{source.stem}_output.mlir"
configure_logging(verbosity=verbose, quiet=quiet)
try:
result_path = compile_cgc_ir(
source,
destination,
device_luid=device_luid,
validate=validate,
overwrite=overwrite,
)
except OSError as e:
raise click.ClickException(f"CGC compilation failed: {e}") from e
console.print(f"Output: {result_path}")
console.print("Format: MLIR bytecode; form: DEVICE_TARGETED (CG Output IR).")
console.print(
"Validation: reload, pipeline build and I/O schema passed (no inference)."
if validate
else "Validation: skipped by --no-validate."
)
return
if device_luid is not None:
raise click.UsageError("--device-luid is only supported for CG Output IR compilation.")

# --ep is parsed by EpAtSourceParamType at click parse time and
# arrives as (ep, source) or None — feeds the EPDeviceTarget's
# `source` axis (Scenarios A.5/A.6 per 2_coreloop.md §6.2). Unpack
Expand Down Expand Up @@ -227,6 +294,12 @@ def compile(
if verbose == 0 and not quiet and "verbose" in cc:
verbose = int(cc["verbose"])

if not _is_epcontext_compiler(compiler):
raise click.UsageError(
"CGC compilation requires a single .mlir/.mlirbc model or explicit "
"--compiler cgc; it cannot be selected through EPContext configuration."
)

configure_logging(verbosity=verbose, quiet=quiet)

# Resolve EP+device at the CLI boundary (plan §C / Decision B).
Expand Down
7 changes: 7 additions & 0 deletions src/winml/modelkit/compiler/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,19 @@
# triggering the heavy imports at runtime.
if TYPE_CHECKING:
from .compiler import Compiler, compile_multiple_onnx, compile_onnx, list_compilers
from .runtime import compile_cgc_ir
from .stages.compile import CompileStage
from .stages.optimize import OptimizeStage
from .stages.qformat import QFormatConvertStage


def __getattr__(name: str) -> Any:
"""Lazy-load heavy symbols that pull in session/torch to speed up import."""
if name == "compile_cgc_ir":
from .runtime import compile_cgc_ir

globals()[name] = compile_cgc_ir
return compile_cgc_ir
if name in {"Compiler", "compile_multiple_onnx", "compile_onnx", "list_compilers"}:
from .compiler import (
Compiler,
Expand Down Expand Up @@ -99,6 +105,7 @@ def __getattr__(name: str) -> Any:
"QFormatConvertStage",
"WinMLCompileConfig",
"clear_transforms",
"compile_cgc_ir",
"compile_multiple_onnx",
"compile_onnx",
"get_transforms_for_ep",
Expand Down
Loading