-
Notifications
You must be signed in to change notification settings - Fork 4
fix(build): honor --ep; require explicit device+ep in resolve_device; support cross-compile #947
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3a5446d
b762c59
baa659f
95d4cfd
f837191
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -145,9 +145,9 @@ def _get_available_eps() -> frozenset[EPName]: | |
|
|
||
|
|
||
| def resolve_device( | ||
| device: str = "auto", | ||
| device: str, | ||
| *, | ||
| ep: EPNameOrAlias | None = None, | ||
| ep: EPNameOrAlias | None, | ||
| ) -> tuple[str, list[str]]: | ||
| """Resolve target device with EP availability cross-check. | ||
|
|
||
|
|
@@ -233,7 +233,7 @@ def resolve_eps(resolved_device: str) -> list[EPName]: | |
|
|
||
|
|
||
| def resolve_check_device_ep( | ||
| *, device: str = "auto", ep: EPNameOrAlias | None = None | ||
| *, device: str, ep: EPNameOrAlias | None | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The docstring says 'Ideal for commands that do not need the device + ep actually exists on the system.' But static validation (no availability probe) only applies when BOTH device is explicit (not auto) AND ep is non-None. When either is absent the function delegates to resolve_device which does a full availability cross-check. A caller passing --device npu without --ep on a machine without NPU will still get a ValueError. The docstring should clarify this for users relying on this for cross-compile. |
||
| ) -> tuple[str, list[str], list[EPName]]: | ||
| """Resolve or check that the requested device and/or EP combination is valid, raising if not. | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1212,25 +1212,25 @@ def test_resolve_device_value_error_surfaces_as_usage_error( | |
| mock_build_api: MagicMock, | ||
| tmp_path: Path, | ||
| ) -> None: | ||
| """resolve_device raising (explicit device w/ no compatible EP) -> UsageError. | ||
| """resolve_check_device_ep raising (explicit device w/ no compatible EP) -> UsageError. | ||
|
|
||
| Uses default ``--device auto`` (no CLI flag) so the downstream | ||
| device-patch path isn't triggered; the only resolve_device call is | ||
| the one inside the auto-select block. | ||
| device-patch path isn't triggered; the only resolution call is the | ||
| ``resolve_check_device_ep`` inside the auto-select block. | ||
| """ | ||
| from winml.modelkit.commands.build import build | ||
|
|
||
| with patch( | ||
| "winml.modelkit.sysinfo.resolve_device", | ||
| side_effect=ValueError("simulated resolve_device failure"), | ||
| "winml.modelkit.sysinfo.resolve_check_device_ep", | ||
| side_effect=ValueError("simulated resolve failure"), | ||
| ): | ||
| result = runner.invoke( | ||
| build, | ||
| ["-c", str(sample_config_file), "-m", "test", "-o", str(tmp_path)], | ||
| obj={"debug": False}, | ||
| ) | ||
| assert result.exit_code != 0 | ||
| assert "simulated resolve_device failure" in result.output | ||
| assert "simulated resolve failure" in result.output | ||
| mock_build_api.assert_not_called() | ||
|
|
||
| def test_auto_selection_respects_resolve_eps_priority( | ||
|
|
@@ -1240,17 +1240,15 @@ def test_auto_selection_respects_resolve_eps_priority( | |
| mock_build_api: MagicMock, | ||
| tmp_path: Path, | ||
| ) -> None: | ||
| """First element of resolve_eps(resolved_device) is selected, not later ones.""" | ||
| """First element of resolve_check_device_ep's available_eps is selected.""" | ||
| from winml.modelkit.commands.build import build | ||
|
|
||
| with ( | ||
| patch( | ||
| "winml.modelkit.sysinfo.resolve_device", | ||
| return_value=("gpu", ["gpu", "cpu"]), | ||
| ), | ||
| patch( | ||
| "winml.modelkit.sysinfo.resolve_eps", | ||
| return_value=["DmlExecutionProvider", "OpenVINOExecutionProvider"], | ||
| with patch( | ||
| "winml.modelkit.sysinfo.resolve_check_device_ep", | ||
| return_value=( | ||
| "gpu", | ||
| ["gpu", "cpu"], | ||
| ["DmlExecutionProvider", "OpenVINOExecutionProvider"], | ||
| ), | ||
| ): | ||
| result = runner.invoke( | ||
|
|
@@ -1711,3 +1709,34 @@ def test_returns_compiled_path_when_file_exists( | |
|
|
||
| # current_path should be updated to compiled_path | ||
| assert result == compiled_path | ||
|
|
||
|
|
||
| class TestBuildEpResolution: | ||
| """--ep forwarding into config generation + the compile EP-availability gate.""" | ||
|
|
||
| def _base_args(self, cfg: str, tmp_path: Path) -> list[str]: | ||
| return ["-c", cfg, "-m", "microsoft/resnet-50", "-o", str(tmp_path / "out")] | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. _base_args is never called by any test in this class -- dead helper. If intended for future tests add a TODO comment; otherwise remove it. The args it builds also mix -c and -m which would not match the auto-config path the class is testing. |
||
|
|
||
| def test_ep_forwarded_to_generate_build_config( | ||
| self, tmp_path: Path, mock_run_single_build: MagicMock | ||
| ): | ||
| """On the auto-config path (-m, no -c), --ep reaches generate_build_config. | ||
|
|
||
| Regression: the build command dropped --ep when auto-generating a config, | ||
| so the requested EP never influenced the generated config (it failed or | ||
| analyzed/compiled for the wrong EP). | ||
| """ | ||
| fake_cfg = MagicMock() | ||
| fake_cfg.compile = None # no compile -> EP-availability gate is skipped | ||
| with ( | ||
| patch("winml.modelkit.config.generate_build_config", return_value=fake_cfg) as mock_gen, | ||
| patch( | ||
| "winml.modelkit.commands.build._validate_loader_tasks_for_model", | ||
| return_value=None, | ||
| ), | ||
| ): | ||
| result = _invoke( | ||
| ["-m", "microsoft/resnet-50", "--ep", "openvino", "-o", str(tmp_path / "out")] | ||
| ) | ||
| assert result.exit_code == 0, result.output | ||
| assert mock_gen.call_args.kwargs["ep"] == "openvino" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing regression test for the cross-compile static path: the PR's headline feature (--device npu --ep openvino --no-compile completing on a machine without NPU) is verified empirically but has no unit test. A test that mocks resolve_check_device_ep to return a static result and confirms the optimize stage proceeds would guard against future regressions (e.g. accidentally restoring resolve_device in the optimize path). |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor nit: inside the 'if ep is None:' block, passing ep=ep (which is provably None here) is technically correct but slightly opaque. Writing ep=None explicitly would remove any ambiguity.