Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7f7d3a1
fix(eval): filter unsupported QNN GPU targets
github-actions[bot] Jul 28, 2026
33aa809
Merge remote-tracking branch 'origin/main' into dingmaomaobjtu-run-e2…
github-actions[bot] Jul 28, 2026
297ca1e
fix(e2e): stabilize QNN CI failures
github-actions[bot] Jul 28, 2026
be04a83
fix(e2e): classify unsupported QNN eval failures
github-actions[bot] Jul 28, 2026
862a861
fix(export): use eager attention during ONNX export
github-actions[bot] Jul 29, 2026
46c5954
refactor(export): colocate transformers attention compat
github-actions[bot] Jul 29, 2026
71660c3
fix(e2e): continue after build teardown crashes
github-actions[bot] Jul 29, 2026
50cafe7
docs: design export device policy
github-actions[bot] Jul 29, 2026
010f6ed
docs: plan export device policy
github-actions[bot] Jul 29, 2026
4cecdf5
docs: correct export policy plan
github-actions[bot] Jul 29, 2026
f991880
feat(export): add export compatibility policy resolver
github-actions[bot] Jul 29, 2026
c643fa1
feat(export): persist export compatibility config
github-actions[bot] Jul 29, 2026
5e3b011
test: fold export compatibility tests into canonical test files
github-actions[bot] Jul 29, 2026
8374a22
feat(config): resolve export compatibility policy
github-actions[bot] Jul 29, 2026
9da2fa9
chore: ignore SDD scratch files
github-actions[bot] Jul 29, 2026
34745c0
feat(export): propagate export policy targets
github-actions[bot] Jul 29, 2026
fda1edb
feat(export): apply attention compatibility by policy
github-actions[bot] Jul 29, 2026
d4fff05
fix(export): address export policy review findings
github-actions[bot] Jul 29, 2026
34e11ca
fix(export): address final export policy review findings
github-actions[bot] Jul 29, 2026
505fdde
merge main into qnn e2e fix
github-actions[bot] Jul 29, 2026
b315fcd
chore: remove superpowers ignore rule
github-actions[bot] Jul 29, 2026
b782d68
chore: prune qnn export policy changes
github-actions[bot] Jul 29, 2026
88e8f99
refactor(export): load compatibility rules from json
github-actions[bot] Jul 29, 2026
4c5e8a0
refactor(export): centralize compatibility target resolution
github-actions[bot] Jul 29, 2026
7e34834
Apply global export compatibility policy
github-actions[bot] Jul 29, 2026
49bc2b5
Fix export policy review issues
github-actions[bot] Jul 29, 2026
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
1 change: 1 addition & 0 deletions docs/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ stages based on the target device and precision.
| `do_constant_folding` | `bool` | `true` | Fold constants during export. |
| `verbose` | `bool` | `false` | Verbose export logging. |
| `dynamo` | `bool` | `false` | Use PyTorch's TorchDynamo ONNX exporter; the default `false` selects the legacy TorchScript exporter. |
| `compatibility` | `dict \| null` | omitted | Resolved export compatibility knobs. Currently supports `transformers_attention: "eager"` to request eager Transformers attention during ONNX export. |
| `enable_hierarchy_tags` | `bool` | `true` | Add module hierarchy tags to ONNX nodes. |
| `clean_onnx` | `bool` | `false` | Strip hierarchy tags after export. |
| `hierarchy_tag_format` | `"full" \| "module_only"` | `"full"` | Tag detail level. |
Expand Down
14 changes: 14 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,19 @@ override-dependencies = [
"onnxruntime ; sys_platform == 'unobtainium'",
]

[[tool.uv.index]]
name = "pytorch-cpu"
url = "https://download.pytorch.org/whl/cpu"
explicit = true

[tool.uv.sources]
torch = [
{ index = "pytorch-cpu", marker = "sys_platform == 'linux' and platform_machine == 'x86_64'" },
]
torchvision = [
{ index = "pytorch-cpu", marker = "sys_platform == 'linux' and platform_machine == 'x86_64'" },
]

[dependency-groups]
dev = [
"jupyter>=1.1.1",
Expand Down Expand Up @@ -186,6 +199,7 @@ include = [ "winml", "winml.*" ]
"runtime_checker/need_rerun_errors.json",
]
"winml.modelkit.export" = [
"compatibility_rules.json",
"htp/htp_metadata_schema.json",
]
"winml.modelkit.pattern" = [
Expand Down
51 changes: 26 additions & 25 deletions src/winml/modelkit/commands/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -901,7 +901,6 @@ def build(
"EPNameOrAlias | None",
_reject_ep_source(ep, "winml build"),
)

# Validate mutual exclusion
if output_dir and use_cache:
raise click.UsageError("--output-dir and --use-cache are mutually exclusive.")
Expand Down Expand Up @@ -933,20 +932,6 @@ def build(
dynamic_axes=dynamic_axes,
)

# Resolve an omitted EP and the requested device as one target. Forwarding
# both concrete axes keeps analyzer/build output aligned with the target
# selected by the catalog-backed resolver.
if ep_value is None:
from ..session import EPDeviceTarget, resolve_device

try:
resolved_target = resolve_device(EPDeviceTarget(ep="auto", device=device))
except ValueError as e:
raise click.UsageError(str(e)) from e
device = resolved_target.device
ep_value = cast("EPNameOrAlias", resolved_target.ep)
logger.info("Auto-resolved device=%s, EP=%s", device, ep_value)

try:
# Hub-hosted ONNX (e.g. ``onnx-community/sam3-tracker-ONNX/onnx/...``)
# is downloaded once and treated as a local .onnx file thereafter.
Expand Down Expand Up @@ -989,6 +974,9 @@ def build(
]
else:
config_or_configs = merge_export_overrides(config_or_configs, export_overrides)
from ..config.build import apply_export_compatibility_policy

apply_export_compatibility_policy(config_or_configs, device=device, ep=ep_value)
else:
if not model:
raise click.UsageError("-m/--model is required when -c is not provided.")
Expand Down Expand Up @@ -1036,6 +1024,19 @@ def build(
if no_compile:
config_or_configs.compile = None

runtime_device = device
runtime_ep_value = ep_value
if runtime_ep_value is None:
from ..session import EPDeviceTarget, resolve_device

try:
resolved_target = resolve_device(EPDeviceTarget(ep="auto", device=runtime_device))
except ValueError as e:
raise click.UsageError(str(e)) from e
runtime_device = resolved_target.device
runtime_ep_value = cast("EPNameOrAlias", resolved_target.ep)
logger.info("Auto-resolved device=%s, EP=%s", runtime_device, runtime_ep_value)

# If --device or --precision was explicitly provided, patch quant/compile
# to honor the requested policy. fp16/fp32 clear quant; npu/int8 etc set it.
if cli_utils.is_cli_provided(ctx, "device") or cli_utils.is_cli_provided(ctx, "precision"):
Expand All @@ -1045,7 +1046,7 @@ def _patch_device(cfg: WinMLBuildConfig) -> None:
from ..config import resolve_quant_compile_config

resolved_quant, _ = resolve_quant_compile_config(
device=device, precision=precision, ep=ep_value
device=runtime_device, precision=precision, ep=runtime_ep_value
)
if cfg.skip_optimize or not quant or resolved_quant is None:
cfg.quant = None
Expand Down Expand Up @@ -1073,7 +1074,7 @@ def _patch_device(cfg: WinMLBuildConfig) -> None:
cfg.precision = precision.lower() # type: ignore[attr-defined]
if cfg.compile is not None and cfg.compile.ep_config is not None:
provider = cfg.compile.ep_config.provider
patched = WinMLCompileConfig.for_provider(provider, device=device)
patched = WinMLCompileConfig.for_provider(provider, device=runtime_device)
if patched is not None:
cfg.compile = patched

Expand Down Expand Up @@ -1142,8 +1143,8 @@ def _patch_device(cfg: WinMLBuildConfig) -> None:
preloaded_hf_config=preloaded_hf_config,
output_dir=output_dir,
use_cache=use_cache,
device=device,
ep=ep_value,
device=runtime_device,
ep=runtime_ep_value,
precision=precision,
rebuild=rebuild,
submodel=submodel,
Expand Down Expand Up @@ -1190,8 +1191,8 @@ def _patch_device(cfg: WinMLBuildConfig) -> None:
configs=configs,
output_dir=resolved_dir,
rebuild=rebuild,
ep=ep_value,
device=device,
ep=runtime_ep_value,
device=runtime_device,
allow_unsupported_nodes=allow_unsupported_nodes,
)

Expand Down Expand Up @@ -1399,8 +1400,8 @@ def _patch_device(cfg: WinMLBuildConfig) -> None:
resolved_dir=resolved_dir,
rebuild=rebuild,
cache_key=name,
ep=ep_value,
device=device,
ep=runtime_ep_value,
device=runtime_device,
extra_kwargs=dict(extra_kwargs),
preloaded_hf_config=preloaded_hf_config,
)
Expand All @@ -1417,8 +1418,8 @@ def _patch_device(cfg: WinMLBuildConfig) -> None:
resolved_dir=resolved_dir,
rebuild=rebuild,
cache_key=cache_key,
ep=ep_value,
device=device,
ep=runtime_ep_value,
device=runtime_device,
extra_kwargs=extra_kwargs,
preloaded_hf_config=preloaded_hf_config,
)
Expand Down
9 changes: 8 additions & 1 deletion src/winml/modelkit/commands/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,12 @@ def export(
if not cli_utils.is_cli_provided(ctx, "dynamo") and "dynamo" in ec:
dynamo = ec["dynamo"]

from ..export import InputTensorSpec, OutputTensorSpec, WinMLExportConfig
from ..export import (
InputTensorSpec,
OutputTensorSpec,
WinMLExportConfig,
resolve_export_compatibility,
)
from ..export import export_pytorch as export_onnx
from ..loader import load_hf_model

Expand Down Expand Up @@ -430,6 +435,8 @@ def _run_component_export(component_task: str | None, out_path: Path) -> None:

try:
cfg = WinMLExportConfig.from_dict(config_kwargs)
if not cfg.compatibility:
cfg.compatibility = resolve_export_compatibility()
except Exception as e:
console.print(f"[bold red]Configuration error:[/bold red] {e}")
logger.exception("Failed to create export config")
Expand Down
11 changes: 7 additions & 4 deletions src/winml/modelkit/commands/perf.py
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,8 @@ def _load_model(self) -> None:
"task": resolved_task,
"config": override,
"ep_device": self._ep_device,
"device": self.config.device,
"ep": self.config.ep,
"precision": self.config.precision,
"provider_options": self.config.ep_options,
"use_cache": use_cache,
Expand Down Expand Up @@ -1346,8 +1348,10 @@ def _perf_modules(
from ..session import EPDeviceTarget, WinMLEPRegistry, resolve_device
from .build import _instantiate_parent_model

request_device = (device or "auto").lower()
request_ep = ep
resolved_target = resolve_device(
EPDeviceTarget(ep=ep or "auto", device=device or "auto", source=ep_source)
EPDeviceTarget(ep=request_ep or "auto", device=request_device, source=ep_source)
)
resolved_ep_device = WinMLEPRegistry.instance().auto_device(resolved_target)
resolved_device = resolved_target.device
Expand All @@ -1360,9 +1364,9 @@ def _perf_modules(
model_id=hf_model,
task=task,
module=module_class,
device=resolved_device,
device=request_device,
precision=precision,
ep=ep,
ep=request_ep,
)
except SubmoduleClassNotFoundError as e:
# User-error: --module pattern didn't match. List what's available so
Expand Down Expand Up @@ -2669,7 +2673,6 @@ def perf(
except Exception as e:
raise click.ClickException(f"Failed to resolve Hub-hosted ONNX path {model!r}: {e}") from e
model = hf_model

# AC 11 (mockup spec): --top-k requires --op-tracing. Outside the
# op-tracing section the flag is meaningless, so reject it explicitly
# rather than silently ignoring a user's intent.
Expand Down
45 changes: 44 additions & 1 deletion src/winml/modelkit/config/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
WinMLExportConfig,
_resolve_export_config_from_specs,
)
from ..export.policy import export_policy_targets_for_request, resolve_export_compatibility
from ..loader.config import WinMLLoaderConfig, resolve_loader_config
from ..optim.config import WinMLOptimizationConfig
from ..quant.config import WinMLQuantizationConfig
Expand All @@ -71,7 +72,7 @@


if TYPE_CHECKING:
from collections.abc import Mapping
from collections.abc import Mapping, Sequence

import torch
from torch import nn
Expand Down Expand Up @@ -457,6 +458,34 @@ def _apply_target_policy(
)


def _is_explicit_export_policy_target(*, device: str | None, ep: str | None) -> bool:
"""Return whether the request named a specific EP/device export target."""
return (ep is not None and ep.lower() != "auto") or (
device is not None and device.lower() != "auto"
)


def apply_export_compatibility_policy(
config: WinMLBuildConfig | Sequence[WinMLBuildConfig],
*,
device: str | None = "auto",
ep: str | None = None,
) -> None:
"""Populate export compatibility when the config has an export stage."""
export_policy_targets = export_policy_targets_for_request(
ep=ep,
device=device,
target_was_explicit=_is_explicit_export_policy_target(device=device, ep=ep),
)
configs = (config,) if isinstance(config, WinMLBuildConfig) else config
for cfg in configs:
if cfg.export is None:
continue
if cfg.export.compatibility:
continue
cfg.export.compatibility = resolve_export_compatibility(export_policy_targets)


def resolve_quant_compile_config(
*,
device: str = "auto",
Expand Down Expand Up @@ -1078,6 +1107,10 @@ class name. Uses torchinfo to discover submodules and infer
if no_compile:
parent_config.compile = None

# Apply export compatibility policy so parent_config.export.compatibility is populated
# (used for serialization/cache-key participation and inheritance by submodules).
apply_export_compatibility_policy(parent_config, device=device, ep=ep)

# =========================================================================
# STEP 5: Specialize for submodules if requested
# =========================================================================
Expand Down Expand Up @@ -1305,6 +1338,11 @@ def _input_name(i: int) -> str:
if parent_config.export is not None
else WinMLExportConfig().dynamo
),
compatibility=(
copy.deepcopy(parent_config.export.compatibility)
if parent_config.export is not None
else WinMLExportConfig().compatibility
),
# opset_version and batch_size use dataclass defaults from WinMLExportConfig
),
optim=copy.deepcopy(parent_config.optim),
Expand Down Expand Up @@ -1390,6 +1428,11 @@ def _merge_export_config(
if override.hierarchy_tag_format != defaults.hierarchy_tag_format
else base.hierarchy_tag_format
),
compatibility=(
copy.deepcopy(override.compatibility)
if override.compatibility
else copy.deepcopy(base.compatibility)
),
)


Expand Down
2 changes: 2 additions & 0 deletions src/winml/modelkit/eval/evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,8 @@ def _load_model(config: WinMLEvaluationConfig) -> WinMLPreTrainedModel | WinMLCo
config.model_id,
ep_device,
task=config.task,
device=config.device,
ep=config.ep,
precision=config.precision,
allow_unsupported_nodes=config.allow_unsupported_nodes,
config=build_override,
Expand Down
12 changes: 12 additions & 0 deletions src/winml/modelkit/export/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@
WinMLExportConfig,
resolve_export_config,
)
from .policy import (
ExportCompatibilityConfig,
ExportCompatibilityRule,
ExportPolicyTarget,
export_policy_targets_for_request,
resolve_export_compatibility,
)


# Static type re-exports for the names exposed by ``__getattr__`` below.
Expand All @@ -40,15 +47,20 @@
__version__ = "2.1.0"

__all__ = [
"ExportCompatibilityConfig",
"ExportCompatibilityRule",
"ExportPolicyTarget",
"InputTensorSpec",
"MaxLengthTextInputGenerator",
"ONNXConfigNotFoundError",
"OutputTensorSpec",
"WinMLExportConfig",
"export_onnx",
"export_policy_targets_for_request",
"export_pytorch",
"generate_dummy_inputs",
"register_onnx_overwrite",
"resolve_export_compatibility",
"resolve_export_config",
"resolve_io_specs",
]
Expand Down
15 changes: 15 additions & 0 deletions src/winml/modelkit/export/compatibility_rules.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"schema_version": 1,
"rules": [
{
"match": {
"ep": null,
"device": null
},
"export": {
"transformers_attention": "eager"
},
"reason": "Transformers SDPA-exported attention guard paths are not broadly portable."
}
]
}
Loading
Loading