feat: Update CoreML device support and server controls

- Replace 'mps' with 'coreml' as the identifier for Apple CoreML Execution Provider across the codebase.
- Remove Apple chip generation checks and ensure CoreML is available on all macOS arm64 devices.
- Adjust ONNX Runtime session creation to use default CoreML configurations without forcing 'MLProgram'.
- Implement support for MoGe-2 to utilize CoreML with static input shape requirements.
- Enhance server panel to display restart and stop controls consistently across running states, including 'BUSY'.
- Update UI tests to verify new behavior for server controls and cached model display.
- Bump blendjob dependency version to 0.1.16 in packaging tests and configurations.
This commit is contained in:
2026-09-02 10:50:02 +08:00
parent 7f6e47432e
commit 5a011b3b57
24 changed files with 619 additions and 117 deletions

View File

@@ -10,7 +10,7 @@ type = "add-on"
blender_version_min = "4.5.0"
platforms = ["windows-x64", "macos-arm64", "linux-x64"]
license = ["SPDX:GPL-3.0-or-later"]
wheels = ["./wheels/blendjob-0.1.14-py3-none-any.whl"]
wheels = ["./wheels/blendjob-0.1.16-py3-none-any.whl"]
[permissions]
network = "Access the local Job Server and download environment dependencies and model weights"

View File

@@ -17,14 +17,14 @@ class ServerPanel(bpy.types.Panel):
def draw(self, _context):
layout = self.layout
running = runtime.server_busy()
busy = runtime.server_busy()
if not runtime.environment_ready():
layout.label(
text="Environment is not installed",
icon="ERROR",
)
setup_row = layout.row()
setup_row.enabled = not running
setup_row.enabled = not busy
setup_row.operator(
"anyimage.setup_ai_environment",
text="Set Up AI Environment…",
@@ -34,6 +34,7 @@ class ServerPanel(bpy.types.Panel):
status = runtime.server_status()
state = status.get("state", "STOPPED")
server_running = state in {"READY", "BUSY"}
icons = {
"READY": "RADIOBUT_ON",
"BUSY": "TIME",
@@ -43,7 +44,7 @@ class ServerPanel(bpy.types.Panel):
"STOPPED": "PAUSE",
}
row = layout.row(align=True)
if state in {"READY", "BUSY"}:
if server_running:
row.label(
text=(
"Running"
@@ -52,18 +53,17 @@ class ServerPanel(bpy.types.Panel):
),
icon=icons[state],
)
if state == "READY":
controls = row.row(align=True)
controls.operator(
"anyimage.restart_server",
text="",
icon="FILE_REFRESH",
)
controls.operator(
"anyimage.stop_server",
text="",
icon="PAUSE",
)
controls = row.row(align=True)
controls.operator(
"anyimage.restart_server",
text="",
icon="FILE_REFRESH",
)
controls.operator(
"anyimage.stop_server",
text="",
icon="PAUSE",
)
elif state == "STOPPED":
row.operator(
"anyimage.start_server",
@@ -82,7 +82,7 @@ class ServerPanel(bpy.types.Panel):
icon="PLAY",
)
if state in {"READY", "BUSY"}:
if server_running:
cached = (
status.get("resources", {})
.get("model_manager", {})
@@ -90,13 +90,19 @@ class ServerPanel(bpy.types.Panel):
)
cached_names = [name for name in cached.values() if name]
cached_row = layout.row(align=True)
cached_row.label(
text=(
f"Cached: {', '.join(cached_names)}"
if cached_names
else "Cached: None"
if len(cached_names) > 1:
cached_list = cached_row.column(align=True)
cached_list.label(text="Cached:")
for name in cached_names:
cached_list.label(text=name)
else:
cached_row.label(
text=(
f"Cached: {cached_names[0]}"
if cached_names
else "Cached: None"
)
)
)
clear = cached_row.row(align=True)
clear.enabled = state == "READY"
clear.operator(

View File

@@ -1,7 +1,4 @@
import functools
import platform
import re
import subprocess
import sys
import bpy
@@ -26,55 +23,34 @@ REQUIRED_MODEL_KEYS = (
DEVICE_LABELS = {
"cuda": ("CUDA", "NVIDIA CUDA"),
"directml": ("DirectML", "Windows GPU (DirectML)"),
"mps": ("MPS", "Apple Metal (MPS)"),
"coreml": ("CoreML", "Apple CoreML"),
"cpu": ("CPU", "CPU"),
}
APPLE_SILICON_PATTERN = re.compile(r"\bApple M(\d+)\b", re.IGNORECASE)
def parse_apple_silicon_generation(brand):
match = APPLE_SILICON_PATTERN.search(str(brand))
return int(match.group(1)) if match else None
@functools.lru_cache(maxsize=1)
def apple_silicon_generation():
if sys.platform != "darwin" or platform.machine().lower() not in {
def supports_coreml():
return sys.platform == "darwin" and platform.machine().lower() in {
"arm64",
"aarch64",
}:
return None
try:
result = subprocess.run(
("sysctl", "-n", "machdep.cpu.brand_string"),
check=True,
capture_output=True,
text=True,
timeout=2,
)
except (OSError, subprocess.SubprocessError):
return None
return parse_apple_silicon_generation(result.stdout)
def supports_mps(generation=None):
if generation is None:
generation = apple_silicon_generation()
return generation is not None and generation >= 4
}
def supported_devices():
devices = runtime.environment_manifest().get("devices")
if isinstance(devices, list) and devices:
names = [str(device).lower() for device in devices]
if not supports_mps():
names = [name for name in names if name != "mps"]
names = [
name
for device in devices
if (name := str(device).lower()) in DEVICE_LABELS
]
if not supports_coreml():
names = [name for name in names if name != "coreml"]
elif "cpu" in names:
names = ["cpu", *(name for name in names if name != "cpu")]
return names or ["cpu"]
if sys.platform == "win32":
return ["directml", "cpu"]
return ["mps", "cpu"] if supports_mps() else ["cpu"]
return ["cpu", "coreml"] if supports_coreml() else ["cpu"]
def device_enum_items(_owner, _context):

View File

@@ -10,6 +10,9 @@ from .onnx_runtime import select_providers as select_providers
ONNX_FILENAME = "model.onnx"
MIN_TOKENS = 1200
MAX_TOKENS = 3600
COREML_PROVIDER_OPTIONS = {
"RequireStaticInputShapes": "1",
}
def is_onnx_model(model_directory):
@@ -18,7 +21,11 @@ def is_onnx_model(model_directory):
def create_session(model_directory, requested_device="auto"):
path = Path(model_directory) / ONNX_FILENAME
return create_runtime_session(path, requested_device)
return create_runtime_session(
path,
requested_device,
coreml_options=COREML_PROVIDER_OPTIONS,
)
def normalized_view_plane_uv(width, height, dtype):

View File

@@ -1,12 +1,5 @@
"""Shared ONNX Runtime session helpers."""
COREML_PROVIDER_OPTIONS = {
"ModelFormat": "MLProgram",
"MLComputeUnits": "ALL",
"RequireStaticInputShapes": "0",
"EnableOnSubgraphs": "0",
}
CUDA_PROVIDER_OPTIONS = {
"arena_extend_strategy": "kSameAsRequested",
}
@@ -17,14 +10,20 @@ def select_providers(requested, available):
providers_for_device = {
"cuda": ("CUDAExecutionProvider", "DmlExecutionProvider"),
"directml": ("DmlExecutionProvider",),
"mps": ("CoreMLExecutionProvider",),
"coreml": ("CoreMLExecutionProvider",),
"cpu": ("CPUExecutionProvider",),
}
if requested != "auto":
candidates = providers_for_device[requested]
for provider in candidates:
if provider in available:
return [provider]
providers = [provider]
if (
provider == "CoreMLExecutionProvider"
and "CPUExecutionProvider" in available
):
providers.append("CPUExecutionProvider")
return providers
if "CPUExecutionProvider" in available:
return ["CPUExecutionProvider"]
raise RuntimeError(
@@ -44,7 +43,12 @@ def select_providers(requested, available):
return providers
def create_session(path, requested_device="auto"):
def create_session(
path,
requested_device="auto",
*,
coreml_options=None,
):
import onnxruntime
providers = select_providers(
@@ -55,8 +59,8 @@ def create_session(path, requested_device="auto"):
for provider in providers:
if provider == "CUDAExecutionProvider":
provider = (provider, dict(CUDA_PROVIDER_OPTIONS))
elif provider == "CoreMLExecutionProvider":
provider = (provider, dict(COREML_PROVIDER_OPTIONS))
elif provider == "CoreMLExecutionProvider" and coreml_options:
provider = (provider, dict(coreml_options))
configured_providers.append(provider)
return onnxruntime.InferenceSession(
str(path),