Skip to content

Commit 933dcc7

Browse files
authored
Merge pull request #212 from SharpAI/develop
chore: promote develop to master (yolo mps ultralytics fallback fix)
2 parents 2264fcb + ed07d68 commit 933dcc7

3 files changed

Lines changed: 104 additions & 2 deletions

File tree

‎skills/detection/yolo-detection-2026/scripts/env_config.py‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -848,7 +848,26 @@ def load_optimized(self, model_name: str, use_optimized: bool = True):
848848
self.load_ms = (time.perf_counter() - t0) * 1000
849849
return pt_model, "pytorch"
850850

851-
# No optimization requested or framework missing
851+
# No optimization requested or framework missing.
852+
# mps ships without torch/ultralytics (see requirements_mps.txt) — the
853+
# pre-built .onnx is still usable via plain CPUExecutionProvider even
854+
# when the CoreML EP check that sets framework_ok failed, so try that
855+
# before assuming ultralytics is importable (SharpAI/DeepCamera#207:
856+
# this branch used to crash with "No module named 'ultralytics'" on
857+
# every mps machine where framework_ok was False).
858+
if self.backend == "mps":
859+
optimized_path = self.get_optimized_path(model_name)
860+
if optimized_path.exists():
861+
model = self._load_onnx_coreml(str(optimized_path))
862+
self.load_ms = (time.perf_counter() - t0) * 1000
863+
_log(f"Loaded {self.export_format} model via ONNX CPU fallback ({self.load_ms:.0f}ms)")
864+
return model, self.export_format
865+
raise RuntimeError(
866+
f"No optimized runtime available for mps and no pre-built "
867+
f"{optimized_path} found — cannot load {model_name} without "
868+
f"torch/ultralytics, which are not installed for mps."
869+
)
870+
852871
from ultralytics import YOLO
853872
model = YOLO(f"{model_name}.pt")
854873
fallback_device = self.device

‎skills/lib/env_config.py‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -848,7 +848,26 @@ def load_optimized(self, model_name: str, use_optimized: bool = True):
848848
self.load_ms = (time.perf_counter() - t0) * 1000
849849
return pt_model, "pytorch"
850850

851-
# No optimization requested or framework missing
851+
# No optimization requested or framework missing.
852+
# mps ships without torch/ultralytics (see requirements_mps.txt) — the
853+
# pre-built .onnx is still usable via plain CPUExecutionProvider even
854+
# when the CoreML EP check that sets framework_ok failed, so try that
855+
# before assuming ultralytics is importable (SharpAI/DeepCamera#207:
856+
# this branch used to crash with "No module named 'ultralytics'" on
857+
# every mps machine where framework_ok was False).
858+
if self.backend == "mps":
859+
optimized_path = self.get_optimized_path(model_name)
860+
if optimized_path.exists():
861+
model = self._load_onnx_coreml(str(optimized_path))
862+
self.load_ms = (time.perf_counter() - t0) * 1000
863+
_log(f"Loaded {self.export_format} model via ONNX CPU fallback ({self.load_ms:.0f}ms)")
864+
return model, self.export_format
865+
raise RuntimeError(
866+
f"No optimized runtime available for mps and no pre-built "
867+
f"{optimized_path} found — cannot load {model_name} without "
868+
f"torch/ultralytics, which are not installed for mps."
869+
)
870+
852871
from ultralytics import YOLO
853872
model = YOLO(f"{model_name}.pt")
854873
fallback_device = self.device
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Regression test for SharpAI/DeepCamera#207: load_optimized() crashed with
4+
"No module named 'ultralytics'" on mps machines where framework_ok is False
5+
(the CoreML execution-provider check failed), because the final fallback
6+
branch unconditionally did `from ultralytics import YOLO` — but mps installs
7+
deliberately never ship torch/ultralytics (see requirements_mps.txt).
8+
9+
Run: python -m pytest skills/lib/test_env_config_mps_fallback.py -v
10+
"""
11+
12+
import sys
13+
from pathlib import Path
14+
from unittest import mock
15+
16+
import pytest
17+
18+
sys.path.insert(0, str(Path(__file__).resolve().parent))
19+
from env_config import HardwareEnv # noqa: E402
20+
21+
22+
def _mps_env(framework_ok=False):
23+
return HardwareEnv(
24+
backend="mps",
25+
device="mps",
26+
export_format="onnx",
27+
framework_ok=framework_ok,
28+
)
29+
30+
31+
class TestMpsFrameworkMissingFallback:
32+
"""load_optimized() when framework_ok is False on mps (Brian's exact case)."""
33+
34+
def test_uses_onnx_coreml_when_prebuilt_model_exists(self, tmp_path, monkeypatch):
35+
monkeypatch.chdir(tmp_path)
36+
onnx_path = tmp_path / "yolo26n.onnx"
37+
onnx_path.write_bytes(b"fake-onnx")
38+
39+
env = _mps_env(framework_ok=False)
40+
sentinel = object()
41+
with mock.patch.object(env, "_load_onnx_coreml", return_value=sentinel) as m:
42+
model, fmt = env.load_optimized("yolo26n", use_optimized=True)
43+
44+
m.assert_called_once_with("yolo26n.onnx")
45+
assert model is sentinel
46+
assert fmt == "onnx"
47+
48+
def test_never_imports_ultralytics_when_framework_missing(self, tmp_path, monkeypatch):
49+
"""The historical bug: this path must not need ultralytics at all."""
50+
monkeypatch.chdir(tmp_path)
51+
(tmp_path / "yolo26n.onnx").write_bytes(b"fake-onnx")
52+
53+
env = _mps_env(framework_ok=False)
54+
with mock.patch.object(env, "_load_onnx_coreml", return_value=object()):
55+
with mock.patch.dict(sys.modules, {"ultralytics": None}):
56+
# If the code path tried `import ultralytics` here, this would
57+
# raise ImportError since sys.modules["ultralytics"] is None.
58+
env.load_optimized("yolo26n", use_optimized=True)
59+
60+
def test_raises_clear_error_when_no_prebuilt_model_and_no_framework(self, tmp_path, monkeypatch):
61+
monkeypatch.chdir(tmp_path)
62+
env = _mps_env(framework_ok=False)
63+
with pytest.raises(RuntimeError, match="torch/ultralytics"):
64+
env.load_optimized("yolo26n", use_optimized=True)

0 commit comments

Comments
 (0)