From 75dbfbb519872b3a3d91ab4a9a1d7f1c11070dba Mon Sep 17 00:00:00 2001 From: Xiaoyu Zhang Date: Mon, 31 Aug 2026 15:40:16 -0700 Subject: [PATCH 1/5] Register OpenVINO IR extensions when loading models --- olive/model/handler/openvino.py | 26 ++++++++++++------- olive/passes/openvino/compression.py | 3 ++- olive/passes/openvino/encapsulation.py | 3 ++- olive/passes/openvino/io_update.py | 3 ++- test/model/test_openvino_model.py | 36 ++++++++++++++++++++++++++ 5 files changed, 58 insertions(+), 13 deletions(-) create mode 100644 test/model/test_openvino_model.py diff --git a/olive/model/handler/openvino.py b/olive/model/handler/openvino.py index 54197e0168..73b3aec3a0 100644 --- a/olive/model/handler/openvino.py +++ b/olive/model/handler/openvino.py @@ -12,6 +12,20 @@ from olive.resource_path import OLIVE_RESOURCE_ANNOTATIONS, create_resource_path +def create_openvino_core(): + """Create an OpenVINO Core with extensions needed to deserialize OpenVINO IR.""" + try: + import openvino as ov + except ImportError: + raise ImportError("Please install olive-ai[openvino] to use OpenVINO model") from None + + core = ov.Core() + group_query_attention_extension = getattr(getattr(ov, "op", None), "_GroupQueryAttentionExtension", None) + if group_query_attention_extension: + core.add_extension(group_query_attention_extension()) + return core + + @model_handler_registry("OpenVINOModel") class OpenVINOModelHandler(OliveModelHandler): """OpenVINO model handler. @@ -54,11 +68,7 @@ def model_config(self) -> dict[str, str]: } def load_model(self, rank: int = None, cache_model: bool = True): - try: - import openvino as ov - except ImportError: - raise ImportError("Please install olive-ai[openvino] to use OpenVINO model") from None - core = ov.Core() + core = create_openvino_core() return core.read_model(self.model_config["model"]) @property @@ -73,11 +83,7 @@ def prepare_session( execution_providers: Union[str, list[str]] = None, rank: Optional[int] = None, ): - try: - import openvino as ov - except ImportError: - raise ImportError("Please install olive-ai[openvino] to use OpenVINO model") from None - core = ov.Core() + core = create_openvino_core() if inference_settings and inference_settings.get("device_name"): device = inference_settings["device_name"] elif device == Device.INTEL_MYRIAD: diff --git a/olive/passes/openvino/compression.py b/olive/passes/openvino/compression.py index c751e7f2cd..959bb392c3 100644 --- a/olive/passes/openvino/compression.py +++ b/olive/passes/openvino/compression.py @@ -13,6 +13,7 @@ from olive.data.config import DataConfig from olive.hardware.accelerator import AcceleratorSpec, Device from olive.model.handler import CompositeModelHandler, HfModelHandler, ONNXModelHandler, OpenVINOModelHandler +from olive.model.handler.openvino import create_openvino_core from olive.passes import Pass from olive.passes.openvino.ov_utils import ( IgnoreScopeTypeEnum, @@ -802,7 +803,7 @@ def _run_openvino_pass( raise ImportError("Please install openvino to use OpenVINO weight compression") from None # load the OpenVINO model - core = ov.Core() + core = create_openvino_core() model_config = model.model_config loaded_model = core.read_model(model_config["model"]) diff --git a/olive/passes/openvino/encapsulation.py b/olive/passes/openvino/encapsulation.py index 2d7cd556e5..21d59c1fc7 100644 --- a/olive/passes/openvino/encapsulation.py +++ b/olive/passes/openvino/encapsulation.py @@ -13,6 +13,7 @@ from olive.common.utils import hardlink_copy_dir, hardlink_copy_file from olive.hardware.accelerator import AcceleratorSpec, Device from olive.model import ONNXModelHandler, OpenVINOModelHandler +from olive.model.handler.openvino import create_openvino_core from olive.passes import Pass from olive.passes.openvino.ov_utils import create_genai_config from olive.passes.pass_config import BasePassConfig, PassConfigParam @@ -141,7 +142,7 @@ def _run_single_target( else: ov_version = ov.get_version() - core = ov.Core() + core = create_openvino_core() model_name_path = Path(model.model_path) / (f"{model_name}.xml") weight_name_path = Path(model.model_path) / (f"{model_name}.bin") diff --git a/olive/passes/openvino/io_update.py b/olive/passes/openvino/io_update.py index f8ac7daa15..7ce6b2d100 100644 --- a/olive/passes/openvino/io_update.py +++ b/olive/passes/openvino/io_update.py @@ -9,6 +9,7 @@ from olive.common.utils import hardlink_copy_dir, hardlink_copy_file from olive.hardware.accelerator import AcceleratorSpec from olive.model import OpenVINOModelHandler +from olive.model.handler.openvino import create_openvino_core from olive.passes import Pass from olive.passes.pass_config import BasePassConfig, PassConfigParam, get_user_script_data_config @@ -103,7 +104,7 @@ def _run_pass( model_name = model.model_config["model_name"] - core = ov.Core() + core = create_openvino_core() model_name_path = Path(model.model_path) / (f"{model_name}.xml") weight_name_path = Path(model.model_path) / (f"{model_name}.bin") diff --git a/test/model/test_openvino_model.py b/test/model/test_openvino_model.py new file mode 100644 index 0000000000..f84b5c65ff --- /dev/null +++ b/test/model/test_openvino_model.py @@ -0,0 +1,36 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +# -------------------------------------------------------------------------- +import sys +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +from olive.model.handler.openvino import create_openvino_core + + +def test_create_openvino_core_registers_group_query_attention_extension(): + core = MagicMock() + extension = MagicMock() + openvino = SimpleNamespace( + Core=MagicMock(return_value=core), + op=SimpleNamespace(_GroupQueryAttentionExtension=extension), + ) + + with patch.dict(sys.modules, {"openvino": openvino}): + result = create_openvino_core() + + assert result is core + extension.assert_called_once_with() + core.add_extension.assert_called_once_with(extension.return_value) + + +def test_create_openvino_core_without_group_query_attention_extension(): + core = MagicMock() + openvino = SimpleNamespace(Core=MagicMock(return_value=core), op=SimpleNamespace()) + + with patch.dict(sys.modules, {"openvino": openvino}): + result = create_openvino_core() + + assert result is core + core.add_extension.assert_not_called() From 2b8c9d928759945a3847001a339a9a6f4e2d577f Mon Sep 17 00:00:00 2001 From: Xiaoyu Zhang Date: Mon, 31 Aug 2026 16:14:39 -0700 Subject: [PATCH 2/5] Expose Mobius execution provider selection --- olive/cli/capture_onnx.py | 12 +++++ olive/passes/onnx/mobius_model_builder.py | 32 +++++++++----- test/cli/test_cli.py | 44 +++++++++++++++++++ test/passes/onnx/test_mobius_model_builder.py | 25 ++++++++++- 4 files changed, 101 insertions(+), 12 deletions(-) diff --git a/olive/cli/capture_onnx.py b/olive/cli/capture_onnx.py index 3966b798c3..d349eebd7a 100644 --- a/olive/cli/capture_onnx.py +++ b/olive/cli/capture_onnx.py @@ -176,6 +176,14 @@ def register_subcommand(parser: ArgumentParser): required=False, help="Extra key-value pairs options to pass to the model builder. e.g., 'int4_is_symmetric=true,int4_op_types_to_quantize=MatMul/Gemm'.", ) + mb_group.add_argument( + "--execution_provider", + "--execution-provider", + dest="execution_provider", + type=str, + required=False, + help=("Mobius execution provider profile, such as 'openvino'. Only used with --use_mobius_builder."), + ) sub_parser.add_argument( "--use_ort_genai", @@ -247,6 +255,10 @@ def _get_run_config(self, tempdir: str) -> dict: del config["passes"]["c"] del config["passes"]["m"] to_replace.append((("passes", "b", "precision"), self.args.precision)) + if self.args.execution_provider: + to_replace.append((("passes", "b", "execution_provider"), self.args.execution_provider)) + elif self.args.execution_provider: + raise ValueError("--execution_provider is only supported with --use_mobius_builder.") elif is_diffusers_model: del config["passes"]["m"] del config["passes"]["b"] diff --git a/olive/passes/onnx/mobius_model_builder.py b/olive/passes/onnx/mobius_model_builder.py index bad4714356..b4a26ea256 100644 --- a/olive/passes/onnx/mobius_model_builder.py +++ b/olive/passes/onnx/mobius_model_builder.py @@ -111,6 +111,15 @@ def _default_config(cls, accelerator_spec: AcceleratorSpec) -> dict[str, PassCon "quantization pass (e.g. OnnxMatMulNBits) after this pass." ), ), + "execution_provider": PassConfigParam( + type_=str, + required=False, + default_value=None, + description=( + "Mobius execution provider profile to use, such as 'openvino'. " + "When omitted, the profile is inferred from the Olive accelerator." + ), + ), "components_to_export": PassConfigParam( type_=list[str], required=False, @@ -143,16 +152,19 @@ def _run_for_config( if not isinstance(model, HfModelHandler): raise ValueError(f"MobiusBuilder requires an HfModelHandler input, got {type(model).__name__}.") - # Map Olive EP to mobius EP. If unsupported/unknown, fall back to mobius default EP. - requested_ep = self.accelerator_spec.execution_provider - ep_str: str = EXECUTION_PROVIDER_TO_MOBIUS_EP.get(requested_ep, self.MobiusEP.DEFAULT) - if ep_str == self.MobiusEP.DEFAULT: - logger.warning( - "MobiusBuilder: execution provider '%s' on accelerator '%s' is not explicitly supported; " - "falling back to mobius default EP.", - requested_ep, - self.accelerator_spec.accelerator_type, - ) + if config.execution_provider: + ep_str = config.execution_provider + else: + # Map Olive EP to mobius EP. If unsupported/unknown, fall back to mobius default EP. + requested_ep = self.accelerator_spec.execution_provider + ep_str = EXECUTION_PROVIDER_TO_MOBIUS_EP.get(requested_ep, self.MobiusEP.DEFAULT) + if ep_str == self.MobiusEP.DEFAULT: + logger.warning( + "MobiusBuilder: execution provider '%s' on accelerator '%s' is not explicitly supported; " + "falling back to mobius default EP.", + requested_ep, + self.accelerator_spec.accelerator_type, + ) dtype_str: str = _PRECISION_TO_DTYPE.get(config.precision, "f32") model_id: str = model.model_name_or_path diff --git a/test/cli/test_cli.py b/test/cli/test_cli.py index 3db547f63d..9335fd4ef9 100644 --- a/test/cli/test_cli.py +++ b/test/cli/test_cli.py @@ -555,6 +555,50 @@ def test_capture_onnx_command_use_mobius_builder(_, mock_run, precision, use_ort assert mock_run.call_count == 1 +@patch("olive.workflows.run") +@patch("huggingface_hub.repo_exists", return_value=True) +@pytest.mark.parametrize("option", ["--execution_provider", "--execution-provider"]) +def test_capture_onnx_command_use_mobius_builder_execution_provider(_, mock_run, option, tmp_path): + cli_main( + [ + "capture-onnx-graph", + "-m", + "dummy-model-id", + "-o", + str(tmp_path / "output_dir"), + "--use_mobius_builder", + "--precision", + "fp32", + option, + "openvino", + ] + ) + + config = mock_run.call_args.args[0] + assert config["passes"]["b"]["execution_provider"] == "openvino" + assert config["systems"]["local_system"]["accelerators"][0] == { + "device": "cpu", + "execution_providers": ["CPUExecutionProvider"], + } + + +@patch("olive.workflows.run") +@patch("huggingface_hub.repo_exists", return_value=True) +def test_capture_onnx_command_execution_provider_requires_mobius_builder(_, __, tmp_path): + with pytest.raises(ValueError, match="only supported with --use_mobius_builder"): + cli_main( + [ + "capture-onnx-graph", + "-m", + "dummy-model-id", + "-o", + str(tmp_path / "output_dir"), + "--execution_provider", + "openvino", + ] + ) + + @patch("olive.workflows.run") @patch("huggingface_hub.repo_exists", return_value=True) def test_capture_onnx_command_use_mobius_builder_rejects_int4(_, __, tmp_path): diff --git a/test/passes/onnx/test_mobius_model_builder.py b/test/passes/onnx/test_mobius_model_builder.py index 92a925a47a..d809dd2cfe 100644 --- a/test/passes/onnx/test_mobius_model_builder.py +++ b/test/passes/onnx/test_mobius_model_builder.py @@ -181,13 +181,14 @@ def __exit__(self, *args): def test_default_config_params(): - """MobiusBuilder must declare precision, and must not declare execution_provider or trust_remote_code.""" + """MobiusBuilder must declare precision and an optional execution provider override.""" accelerator_spec = AcceleratorSpec( accelerator_type=Device.CPU, execution_provider=ExecutionProvider.CPUExecutionProvider ) config = MobiusBuilder._default_config(accelerator_spec) # pylint: disable=protected-access assert "precision" in config - assert "execution_provider" not in config + assert config["execution_provider"].default_value is None + assert config["execution_provider"].required is False assert "trust_remote_code" not in config @@ -241,6 +242,26 @@ def test_single_component_returns_onnx_handler(tmp_path): assert call_kwargs["dtype"] == "f32" +def test_execution_provider_config_overrides_accelerator(tmp_path): + """An explicit Mobius profile overrides the profile inferred from the Olive accelerator.""" + out = tmp_path / "out" + pkg = _fake_pkg(["model"], out) + accelerator_spec = AcceleratorSpec( + accelerator_type=Device.CPU, execution_provider=ExecutionProvider.CPUExecutionProvider + ) + p = create_pass_from_dict( + MobiusBuilder, + {"precision": "fp32", "execution_provider": "openvino"}, + disable_search=True, + accelerator_spec=accelerator_spec, + ) + + with _patch_build(pkg) as mock_build: + p.run(_make_hf_model("org/model"), out) + + assert mock_build.call_args.kwargs["execution_provider"] == "openvino" + + def test_model_onnx_exists_after_run(tmp_path): """The saved model.onnx file must exist on disk.""" out = tmp_path / "out" From 47878957bf913c032e7e09a62af1a273020fb94b Mon Sep 17 00:00:00 2001 From: Xiaoyu Zhang Date: Mon, 31 Aug 2026 18:37:00 -0700 Subject: [PATCH 3/5] Use one Mobius execution provider option --- olive/cli/capture_onnx.py | 2 -- test/cli/test_cli.py | 5 ++--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/olive/cli/capture_onnx.py b/olive/cli/capture_onnx.py index d349eebd7a..1174b5264b 100644 --- a/olive/cli/capture_onnx.py +++ b/olive/cli/capture_onnx.py @@ -178,8 +178,6 @@ def register_subcommand(parser: ArgumentParser): ) mb_group.add_argument( "--execution_provider", - "--execution-provider", - dest="execution_provider", type=str, required=False, help=("Mobius execution provider profile, such as 'openvino'. Only used with --use_mobius_builder."), diff --git a/test/cli/test_cli.py b/test/cli/test_cli.py index 9335fd4ef9..9a2788f53f 100644 --- a/test/cli/test_cli.py +++ b/test/cli/test_cli.py @@ -557,8 +557,7 @@ def test_capture_onnx_command_use_mobius_builder(_, mock_run, precision, use_ort @patch("olive.workflows.run") @patch("huggingface_hub.repo_exists", return_value=True) -@pytest.mark.parametrize("option", ["--execution_provider", "--execution-provider"]) -def test_capture_onnx_command_use_mobius_builder_execution_provider(_, mock_run, option, tmp_path): +def test_capture_onnx_command_use_mobius_builder_execution_provider(_, mock_run, tmp_path): cli_main( [ "capture-onnx-graph", @@ -569,7 +568,7 @@ def test_capture_onnx_command_use_mobius_builder_execution_provider(_, mock_run, "--use_mobius_builder", "--precision", "fp32", - option, + "--execution_provider", "openvino", ] ) From 9f910ed4251264da46976c75e6fe3fd7673cc58e Mon Sep 17 00:00:00 2001 From: xiaoyu-work Date: Mon, 14 Sep 2026 21:52:11 -0700 Subject: [PATCH 4/5] Validate Gemma4 NPU input ABI before encapsulation Treat the OpenVINO NPU CausalLM input contract as an export requirement. Reject flattened per_layer_inputs before creating an EPContext artifact, direct Mobius users to matching four-dimensional embedding/decoder exports, and preserve CPU, GPU, and explicitly non-CausalLM NPU behavior. Signed-off-by: xiaoyu-work --- .../features/ihv-integration/openvino.md | 21 ++++++ olive/passes/openvino/encapsulation.py | 45 +++++++++++- .../openvino/test_openvino_encapsulation.py | 73 ++++++++++++++++++- 3 files changed, 135 insertions(+), 4 deletions(-) diff --git a/docs/source/features/ihv-integration/openvino.md b/docs/source/features/ihv-integration/openvino.md index 373ea127ea..59a716ef0e 100644 --- a/docs/source/features/ihv-integration/openvino.md +++ b/docs/source/features/ihv-integration/openvino.md @@ -133,6 +133,27 @@ Please refer to [OpenVINOEncapsulation](https://microsoft.github.io/Olive/refere } ``` +### Gemma4 NPU input contract + +The OpenVINO EP's NPU CausalLM path expects Gemma4 `per_layer_inputs` to have shape +`[batch, sequence, layers, projection]`. A flattened `[batch, sequence, layers * projection]` +input is not compatible with that path. `OpenVINOEncapsulation` rejects this mismatch +before writing an EPContext model when NPU CausalLM execution is selected. CausalLM is +enabled by default in the generated GenAI configuration; an explicit +`enable_causallm: "False"` provider override retains the generic NPU path. + +When exporting with MobiusBuilder, use its OpenVINO profile: + +```bash +olive capture-onnx-graph -m MODEL --use_mobius_builder --execution_provider openvino --precision fp16 -o gemma4-openvino +``` + +Export the embedding and decoder with the same profile so their per-layer input +layouts match. `onnx-standard` also avoids GQA fusion, but retains the flattened +layout and is not a substitute for the OpenVINO profile. Passing the export checks +does not establish NPU execution: verify prefill and cached decode on the target +device without CPU fallback. + ## Optimum CLI Command for Generative AI workloads `OpenVINOOptimumConversion` pass will run [optimum-cli export openvino](https://huggingface.co/docs/optimum/main/en/intel/openvino/export) command on the input Huggingface models to convert those to OpenVINO models and perform weight compression and quantization if necessary to produce an output OpenVINO model. diff --git a/olive/passes/openvino/encapsulation.py b/olive/passes/openvino/encapsulation.py index 21d59c1fc7..51200d2113 100644 --- a/olive/passes/openvino/encapsulation.py +++ b/olive/passes/openvino/encapsulation.py @@ -2,10 +2,12 @@ # Copyright (c) Intel Corporation. All rights reserved. # Licensed under the MIT License. # -------------------------------------------------------------------------- +from __future__ import annotations + import logging import os from pathlib import Path -from typing import ClassVar, Union +from typing import TYPE_CHECKING, ClassVar import onnx.helper as helper from onnx import TensorProto, save @@ -18,6 +20,9 @@ from olive.passes.openvino.ov_utils import create_genai_config from olive.passes.pass_config import BasePassConfig, PassConfigParam +if TYPE_CHECKING: + import openvino as ov + logger = logging.getLogger(__name__) @@ -114,15 +119,47 @@ def _default_config(cls, accelerator_spec: AcceleratorSpec) -> dict[str, PassCon def _run_for_config( self, - model: Union[OpenVINOModelHandler], + model: OpenVINOModelHandler, config: type[BasePassConfig], output_model_path: str, ) -> ONNXModelHandler: return self._run_single_target(model, config, output_model_path) + @staticmethod + def _validate_npu_causallm_inputs( + input_info: dict[str, tuple[ov.PartialShape, ov.Type]], config: BasePassConfig + ) -> None: + """Check the per-layer input ABI required by the NPU CausalLM path.""" + if config.target_device != Device.NPU: + return + + session_overrides = ( + (config.genai_config_override or {}).get("model", {}).get("decoder", {}).get("session_options", {}) + ) + provider_options = session_overrides.get("provider_options") + # create_genai_config enables CausalLM by default; an explicit provider list replaces that default. + if provider_options is not None and not any( + options.get("enable_causallm") == "True" + for provider in provider_options + for name, options in provider.items() + if name.casefold() == "openvino" + ): + return + + for name, (shape, _) in input_info.items(): + if "per_layer_inputs" not in name: + continue + if shape.rank.is_dynamic or shape.rank.get_length() != 4: + raise ValueError( + f"NPU CausalLM input {name!r} requires rank 4 " + f"[batch, sequence, layers, projection], but got {shape}. " + "Export a matching four-dimensional embedding/decoder pair. " + "For MobiusBuilder, use execution_provider='openvino', not 'onnx-standard'." + ) + def _run_single_target( self, - model: Union[OpenVINOModelHandler], + model: OpenVINOModelHandler, config: type[BasePassConfig], output_model_path: str, ) -> ONNXModelHandler: @@ -161,6 +198,8 @@ def _run_single_target( raise ValueError("Incorrect IO names, please use OpenVINO reshape pass before this pass") from None input_info[name] = (inp.get_partial_shape(), inp.get_element_type()) + self._validate_npu_causallm_inputs(input_info, config) + # Get/Fix input names & ov shapes. output_info = {} for i, out in enumerate(loaded_model.outputs): diff --git a/test/passes/openvino/test_openvino_encapsulation.py b/test/passes/openvino/test_openvino_encapsulation.py index 0387bf89b4..2497a409cb 100644 --- a/test/passes/openvino/test_openvino_encapsulation.py +++ b/test/passes/openvino/test_openvino_encapsulation.py @@ -5,10 +5,12 @@ from pathlib import Path from unittest.mock import MagicMock, patch +import numpy as np +import onnx_ir as ir import pytest from olive.hardware.accelerator import AcceleratorSpec, Device -from olive.model import ONNXModelHandler +from olive.model import ONNXModelHandler, OpenVINOModelHandler from olive.passes.olive_pass import create_pass_from_dict from olive.passes.openvino.conversion import OpenVINOConversion from olive.passes.openvino.encapsulation import OpenVINOEncapsulation @@ -144,3 +146,72 @@ def side_effect(model, config, output_model_path): assert isinstance(result, ONNXModelHandler) assert result.model_attributes["ep"] == "OpenVINOExecutionProvider" assert result.model_attributes["sdk_version"] == "2025.1" + + +def _per_layer_input_model(tmp_path, shape): + import openvino as ov + from openvino import opset13 as ops + + per_layer_inputs = ops.parameter(shape, dtype=np.float32, name="per_layer_inputs") + per_layer_inputs.output(0).get_tensor().set_names({"per_layer_inputs"}) + output = ops.add(per_layer_inputs, ops.constant(1, dtype=np.float32)) + output.output(0).get_tensor().set_names({"output"}) + model = ov.Model([output], [per_layer_inputs]) + directory = tmp_path / "per_layer_model" + directory.mkdir() + ov.save_model(model, directory / "model.xml") + return OpenVINOModelHandler(model_path=str(directory)) + + +def _per_layer_encapsulation_config(device, causallm): + config = {"target_device": device, "keep_ov_dynamic_dims": True} + if causallm is not None: + config["genai_config_override"] = { + "model": { + "decoder": { + "session_options": { + "provider_options": [{"OpenVINO": {"device_type": device.upper(), "enable_causallm": causallm}}] + } + } + } + } + return config + + +@pytest.mark.parametrize("causallm", [None, "True"]) +@pytest.mark.parametrize("shape", [[1, 2, 48], [-1, -1, 48]]) +def test_npu_causallm_rejects_flat_per_layer_inputs_before_writing_context(tmp_path, causallm, shape): + model = _per_layer_input_model(tmp_path, shape) + p = create_pass_from_dict( + OpenVINOEncapsulation, _per_layer_encapsulation_config("npu", causallm), disable_search=True + ) + destination = tmp_path / "encapsulated" + + with pytest.raises(ValueError, match=r"requires rank 4.*execution_provider='openvino'"): + p.run(model, str(destination)) + + assert not list(destination.glob("*.onnx")) + + +@pytest.mark.parametrize( + ("device", "causallm", "shape"), + [ + ("npu", None, [-1, -1, 3, 16]), + ("npu", "True", [-1, -1, 3, 16]), + ("cpu", "True", [-1, -1, 48]), + ("gpu", "True", [-1, -1, 48]), + ("npu", "False", [1, 2, 48]), + ], +) +def test_encapsulation_preserves_supported_per_layer_input_layout(tmp_path, device, causallm, shape): + model = _per_layer_input_model(tmp_path, shape) + p = create_pass_from_dict( + OpenVINOEncapsulation, _per_layer_encapsulation_config(device, causallm), disable_search=True + ) + + result = p.run(model, str(tmp_path / "encapsulated")) + + exported = ir.load(result.model_path) + assert len(exported.graph) == 1 + assert next(iter(exported.graph)).op_type == "EPContext" + assert list(exported.graph.inputs[0].shape) == shape From d3e8c7e277b7776dd643d4a4640d6591e8b57c8f Mon Sep 17 00:00:00 2001 From: xiaoyu-work Date: Mon, 14 Sep 2026 23:05:16 -0700 Subject: [PATCH 5/5] Clarify NPU fallback controls for export validation Document that OpenVINO EP assignment alone does not establish NPU execution because NPUW can use CPU internally. Require checking both ORT and NPUW fallback policy when validating NPU exports. Signed-off-by: xiaoyu-work --- docs/source/features/ihv-integration/openvino.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/source/features/ihv-integration/openvino.md b/docs/source/features/ihv-integration/openvino.md index 59a716ef0e..0de379ff1f 100644 --- a/docs/source/features/ihv-integration/openvino.md +++ b/docs/source/features/ihv-integration/openvino.md @@ -154,6 +154,12 @@ layout and is not a substitute for the OpenVINO profile. Passing the export chec does not establish NPU execution: verify prefill and cached decode on the target device without CPU fallback. +For NPU attribution, ORT's `OpenVINOExecutionProvider` label alone is not sufficient: +NPUW can select CPU internally. Validation configurations should restrict +`NPUW_DEVICES` to `"NPU"` and set `NPUW_FALLBACK_EXEC` to `"NO"`, in addition to +disabling ORT CPU fallback. Check the actual target/execution devices and report +any intentionally CPU-based components separately. + ## Optimum CLI Command for Generative AI workloads `OpenVINOOptimumConversion` pass will run [optimum-cli export openvino](https://huggingface.co/docs/optimum/main/en/intel/openvino/export) command on the input Huggingface models to convert those to OpenVINO models and perform weight compression and quantization if necessary to produce an output OpenVINO model.