From 290af88b4b14010e1f01d4b28fd518d2c65a3852 Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Wed, 16 Sep 2026 15:15:24 -0700 Subject: [PATCH] fix(yolo-skill): don't require ultralytics on mps when framework_ok is False load_optimized()'s final fallback branch (taken when the optimized runtime check fails) unconditionally imported ultralytics. mps installs deliberately ship without torch/ultralytics (requirements_mps.txt: only onnxruntime is needed), so any mps machine where the CoreML EP check fails crashed with "No module named 'ultralytics'" instead of falling back to the pre-built ONNX model via plain CPUExecutionProvider. Now mps uses _load_onnx_coreml() (already used elsewhere in this same function) in that branch instead, matching the file's own stated intent. Addresses one of the symptoms in SharpAI/DeepCamera#207. Co-Authored-By: Claude Sonnet 5 --- .../yolo-detection-2026/scripts/env_config.py | 21 +++++- skills/lib/env_config.py | 21 +++++- skills/lib/test_env_config_mps_fallback.py | 64 +++++++++++++++++++ 3 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 skills/lib/test_env_config_mps_fallback.py diff --git a/skills/detection/yolo-detection-2026/scripts/env_config.py b/skills/detection/yolo-detection-2026/scripts/env_config.py index f559cd66..8c9b2e13 100644 --- a/skills/detection/yolo-detection-2026/scripts/env_config.py +++ b/skills/detection/yolo-detection-2026/scripts/env_config.py @@ -848,7 +848,26 @@ def load_optimized(self, model_name: str, use_optimized: bool = True): self.load_ms = (time.perf_counter() - t0) * 1000 return pt_model, "pytorch" - # No optimization requested or framework missing + # No optimization requested or framework missing. + # mps ships without torch/ultralytics (see requirements_mps.txt) — the + # pre-built .onnx is still usable via plain CPUExecutionProvider even + # when the CoreML EP check that sets framework_ok failed, so try that + # before assuming ultralytics is importable (SharpAI/DeepCamera#207: + # this branch used to crash with "No module named 'ultralytics'" on + # every mps machine where framework_ok was False). + if self.backend == "mps": + optimized_path = self.get_optimized_path(model_name) + if optimized_path.exists(): + model = self._load_onnx_coreml(str(optimized_path)) + self.load_ms = (time.perf_counter() - t0) * 1000 + _log(f"Loaded {self.export_format} model via ONNX CPU fallback ({self.load_ms:.0f}ms)") + return model, self.export_format + raise RuntimeError( + f"No optimized runtime available for mps and no pre-built " + f"{optimized_path} found — cannot load {model_name} without " + f"torch/ultralytics, which are not installed for mps." + ) + from ultralytics import YOLO model = YOLO(f"{model_name}.pt") fallback_device = self.device diff --git a/skills/lib/env_config.py b/skills/lib/env_config.py index f559cd66..8c9b2e13 100644 --- a/skills/lib/env_config.py +++ b/skills/lib/env_config.py @@ -848,7 +848,26 @@ def load_optimized(self, model_name: str, use_optimized: bool = True): self.load_ms = (time.perf_counter() - t0) * 1000 return pt_model, "pytorch" - # No optimization requested or framework missing + # No optimization requested or framework missing. + # mps ships without torch/ultralytics (see requirements_mps.txt) — the + # pre-built .onnx is still usable via plain CPUExecutionProvider even + # when the CoreML EP check that sets framework_ok failed, so try that + # before assuming ultralytics is importable (SharpAI/DeepCamera#207: + # this branch used to crash with "No module named 'ultralytics'" on + # every mps machine where framework_ok was False). + if self.backend == "mps": + optimized_path = self.get_optimized_path(model_name) + if optimized_path.exists(): + model = self._load_onnx_coreml(str(optimized_path)) + self.load_ms = (time.perf_counter() - t0) * 1000 + _log(f"Loaded {self.export_format} model via ONNX CPU fallback ({self.load_ms:.0f}ms)") + return model, self.export_format + raise RuntimeError( + f"No optimized runtime available for mps and no pre-built " + f"{optimized_path} found — cannot load {model_name} without " + f"torch/ultralytics, which are not installed for mps." + ) + from ultralytics import YOLO model = YOLO(f"{model_name}.pt") fallback_device = self.device diff --git a/skills/lib/test_env_config_mps_fallback.py b/skills/lib/test_env_config_mps_fallback.py new file mode 100644 index 00000000..c69da27d --- /dev/null +++ b/skills/lib/test_env_config_mps_fallback.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +""" +Regression test for SharpAI/DeepCamera#207: load_optimized() crashed with +"No module named 'ultralytics'" on mps machines where framework_ok is False +(the CoreML execution-provider check failed), because the final fallback +branch unconditionally did `from ultralytics import YOLO` — but mps installs +deliberately never ship torch/ultralytics (see requirements_mps.txt). + +Run: python -m pytest skills/lib/test_env_config_mps_fallback.py -v +""" + +import sys +from pathlib import Path +from unittest import mock + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from env_config import HardwareEnv # noqa: E402 + + +def _mps_env(framework_ok=False): + return HardwareEnv( + backend="mps", + device="mps", + export_format="onnx", + framework_ok=framework_ok, + ) + + +class TestMpsFrameworkMissingFallback: + """load_optimized() when framework_ok is False on mps (Brian's exact case).""" + + def test_uses_onnx_coreml_when_prebuilt_model_exists(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + onnx_path = tmp_path / "yolo26n.onnx" + onnx_path.write_bytes(b"fake-onnx") + + env = _mps_env(framework_ok=False) + sentinel = object() + with mock.patch.object(env, "_load_onnx_coreml", return_value=sentinel) as m: + model, fmt = env.load_optimized("yolo26n", use_optimized=True) + + m.assert_called_once_with("yolo26n.onnx") + assert model is sentinel + assert fmt == "onnx" + + def test_never_imports_ultralytics_when_framework_missing(self, tmp_path, monkeypatch): + """The historical bug: this path must not need ultralytics at all.""" + monkeypatch.chdir(tmp_path) + (tmp_path / "yolo26n.onnx").write_bytes(b"fake-onnx") + + env = _mps_env(framework_ok=False) + with mock.patch.object(env, "_load_onnx_coreml", return_value=object()): + with mock.patch.dict(sys.modules, {"ultralytics": None}): + # If the code path tried `import ultralytics` here, this would + # raise ImportError since sys.modules["ultralytics"] is None. + env.load_optimized("yolo26n", use_optimized=True) + + def test_raises_clear_error_when_no_prebuilt_model_and_no_framework(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + env = _mps_env(framework_ok=False) + with pytest.raises(RuntimeError, match="torch/ultralytics"): + env.load_optimized("yolo26n", use_optimized=True)