diff --git a/selfdrive/ui/layouts/settings/starpilot/driving_model.py b/selfdrive/ui/layouts/settings/starpilot/driving_model.py index adc58f2b5..74990a727 100644 --- a/selfdrive/ui/layouts/settings/starpilot/driving_model.py +++ b/selfdrive/ui/layouts/settings/starpilot/driving_model.py @@ -19,7 +19,9 @@ from openpilot.starpilot.assets.model_manager import ( MODEL_DOWNLOAD_PARAM, ModelManager, canonical_model_key, + external_gpu_available, is_builtin_model_key, + model_uses_external_gpu, model_key_aliases, ) from openpilot.starpilot.common.starpilot_variables import MODELS_PATH, update_starpilot_toggles @@ -103,6 +105,7 @@ class ModelCatalogEntry: partial: bool community_favorite: bool user_favorite: bool + requires_external_gpu: bool = False def _clean_model_name(name: str) -> str: @@ -637,6 +640,8 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget): badge_parts.append(tr("Built-in")) if entry.partial: badge_parts.append(tr("Incomplete")) + if entry.requires_external_gpu: + badge_parts.append(tr("GPU required")) if entry.user_favorite: badge_parts.append(tr("Saved")) elif entry.community_favorite: @@ -963,6 +968,7 @@ class StarPilotDrivingModelLayout(_SettingsPage): partial=partial, community_favorite=(key in self._community_favorites), user_favorite=(key in self._user_favorites), + requires_external_gpu=model_uses_external_gpu(key), ) def _update_model_metadata(self): @@ -1176,6 +1182,9 @@ class StarPilotDrivingModelLayout(_SettingsPage): if entry is None or not entry.installed: gui_app.push_widget(alert_dialog(tr("Model is not available on this device."))) return False + if entry.requires_external_gpu and not external_gpu_available(): + gui_app.push_widget(alert_dialog(tr("This model requires a detected external GPU."))) + return False if self._params.get_bool("ModelRandomizer"): gui_app.push_widget(alert_dialog(tr("Turn off Model Randomizer to choose a model manually."))) return False @@ -1209,6 +1218,9 @@ class StarPilotDrivingModelLayout(_SettingsPage): if entry is None: gui_app.push_widget(alert_dialog(tr("Unknown model."))) return False + if entry.requires_external_gpu and not external_gpu_available(): + gui_app.push_widget(alert_dialog(tr("This model requires a detected external GPU."))) + return False if entry.installed: gui_app.push_widget(alert_dialog(tr("Model is already on this device."))) return False diff --git a/selfdrive/ui/mici/layouts/settings/driving_model.py b/selfdrive/ui/mici/layouts/settings/driving_model.py index 2a3b4535d..749e49c28 100644 --- a/selfdrive/ui/mici/layouts/settings/driving_model.py +++ b/selfdrive/ui/mici/layouts/settings/driving_model.py @@ -9,6 +9,7 @@ from pathlib import Path from openpilot.common.file_chunker import get_chunk_name, get_manifest_path from openpilot.common.params import Params +from openpilot.starpilot.assets.model_manager import external_gpu_available, model_uses_external_gpu from openpilot.selfdrive.ui.mici.widgets.button import BigButton from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigDialogBase, BigMultiOptionDialog from openpilot.selfdrive.ui.ui_state import ui_state @@ -62,6 +63,7 @@ class ModelEntry: version: str released: str community_favorite: bool + requires_external_gpu: bool = False def _clean_model_name(name: str) -> str: @@ -515,6 +517,10 @@ class DrivingModelBigButton(BigButton): return entry_list def _start_model_download(self, model_key: str): + entry = next((item for item in self._load_model_entries() if item.key == model_key), None) + if entry is not None and entry.requires_external_gpu and not external_gpu_available(): + self._show_message("GPU required", "This model requires a detected external GPU.", return_to_manager=True) + return if not self._start_worker("download", self._run_download_one, model_key): self._show_message("Model manager busy", "Please wait for the current task.", return_to_manager=True) return @@ -546,6 +552,10 @@ class DrivingModelBigButton(BigButton): self._show_message("Model unavailable", "Refresh manifest and try again.", return_to_manager=True) return + if entry.requires_external_gpu and not external_gpu_available(): + self._show_message("GPU required", "This model requires a detected external GPU.", return_to_manager=True) + return + if not self._is_model_installed(entry.key, entry.version): self._show_message("Model not downloaded", "Download this model first.", return_to_manager=True) return @@ -662,6 +672,7 @@ class DrivingModelBigButton(BigButton): version=version, released=released, community_favorite=(key in community_favs), + requires_external_gpu=model_uses_external_gpu(key), )) return entries diff --git a/starpilot/assets/model_manager.py b/starpilot/assets/model_manager.py index e49cd5a79..fa594703f 100644 --- a/starpilot/assets/model_manager.py +++ b/starpilot/assets/model_manager.py @@ -22,8 +22,9 @@ from openpilot.starpilot.common.model_versions import ( ) from openpilot.starpilot.common.starpilot_utilities import delete_file from openpilot.starpilot.common.starpilot_variables import MODELS_PATH +from openpilot.system.hardware.usb import chestnut_firmware_ready -MANIFEST_CANDIDATES = ("v23",) +MANIFEST_CANDIDATES = ("v24",) MODEL_NAMESPACE_SUFFIX = "3" DEFAULT_MODEL_KEY = "rdf43" LOCAL_MODEL_PREFIX = "local-" @@ -100,6 +101,14 @@ def model_uses_external_gpu(model_key: str) -> bool: return bool(load_model_artifact_metadata(model_key).get("uses_external_gpu", False)) +def external_gpu_available() -> bool: + """Return whether the supported external GPU link is ready for modeld.""" + try: + return bool(chestnut_firmware_ready()) + except Exception: + return False + + class ModelManager: def __init__(self, params, params_memory, boot_run=False): self.params = params @@ -377,6 +386,8 @@ class ModelManager: canonical_key = self._canonical_model_key(model_key) if canonical_key in blacklisted_keys or canonical_key in seen_keys: continue + if model_uses_external_gpu(canonical_key) and not external_gpu_available(): + continue model_version = version_map.get(model_key) or version_map.get(canonical_key) or "" if not model_version and is_builtin_model_key(canonical_key): @@ -484,6 +495,13 @@ class ModelManager: return selected = self._selected_model() + if model_uses_external_gpu(selected) and not external_gpu_available(): + default_name = self._default_param_text("DrivingModelName") or "Regret Driven Framework V4" + default_version = self._default_param_text("ModelVersion") or self._default_param_text("DrivingModelVersion") or "v15" + self._set_model_param_keys(DEFAULT_MODEL_KEY, default_name, default_version) + print(f"Model {selected} requires an external GPU; selected built-in model instead.") + return + if is_builtin_model_key(selected): self._sync_selected_model_version() return @@ -673,6 +691,11 @@ class ModelManager: self.downloading_model = False return + if model_uses_external_gpu(model_to_download) and not external_gpu_available(): + handle_error(None, "External GPU required...", "This model requires a detected external GPU.", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM, self.params_memory) + self.downloading_model = False + return + # Local models have no upstream URL; a download attempt would 404 and then # delete_file() the artifact on verification failure. if is_local_model_key(model_to_download): @@ -787,6 +810,9 @@ class ModelManager: if is_local_model_key(model_key): continue + if model_uses_external_gpu(model_key) and not external_gpu_available(): + continue + artifact_format = artifact_format_map.get(model_key, "") if self._is_model_downloaded(model_key, artifact_format): continue diff --git a/starpilot/assets/tests/test_model_pipeline.py b/starpilot/assets/tests/test_model_pipeline.py index a9ce3fc97..95bdfc903 100644 --- a/starpilot/assets/tests/test_model_pipeline.py +++ b/starpilot/assets/tests/test_model_pipeline.py @@ -15,12 +15,12 @@ from openpilot.starpilot.assets.model_manager import MANIFEST_CANDIDATES, ModelM from openpilot.starpilot.common.model_versions import UNIFIED_ARTIFACT_FORMAT -def test_v23_is_the_only_manifest_candidate(): - assert MANIFEST_CANDIDATES == ("v23",) +def test_v24_is_the_only_manifest_candidate(): + assert MANIFEST_CANDIDATES == ("v24",) -def test_v23_manifest_is_loaded_from_models_checkout(): - assert ModelManager._manifest_paths("v23") == ("Models/model_names_v23.json",) +def test_v24_manifest_is_loaded_from_models_checkout(): + assert ModelManager._manifest_paths("v24") == ("Models/model_names_v24.json",) def test_old_manifest_ids_resolve_to_v23_namespace(): diff --git a/starpilot/system/the_galaxy/the_galaxy.py b/starpilot/system/the_galaxy/the_galaxy.py index 2b2a3e564..6706f2550 100644 --- a/starpilot/system/the_galaxy/the_galaxy.py +++ b/starpilot/system/the_galaxy/the_galaxy.py @@ -48,7 +48,13 @@ from openpilot.system.version import get_build_metadata from openpilot.tools.longitudinal_maneuvers.capabilities import get_longitudinal_maneuver_support from panda import Panda -from openpilot.starpilot.assets.model_manager import canonical_model_key, is_builtin_model_key, model_key_aliases +from openpilot.starpilot.assets.model_manager import ( + canonical_model_key, + external_gpu_available, + is_builtin_model_key, + model_key_aliases, + model_uses_external_gpu, +) from openpilot.starpilot.assets.theme_manager import HOLIDAY_THEME_PATH, THEME_COMPONENT_PARAMS from openpilot.starpilot.common.accel_profile import ( CUSTOM_ACCEL_PROFILE_INITIALIZED_KEY, @@ -5008,6 +5014,8 @@ def setup(app): selected_model = canonical_model_key(str_val.strip()) if not selected_model: return jsonify({"error": "Driving model cannot be empty."}), 400 + if model_uses_external_gpu(selected_model) and not external_gpu_available(): + return jsonify({"error": "This model requires a detected external GPU."}), 409 params.put("Model", selected_model) params.put("DrivingModel", selected_model) @@ -5376,6 +5384,8 @@ def setup(app): if model["installed"]: return jsonify({"message": f"\"{model['label']}\" is already installed."}), 200 + if model["requiresGpu"] and not model["gpuAvailable"]: + return jsonify({"error": "This model requires a detected external GPU."}), 409 params_memory.remove(MODEL_CANCEL_DOWNLOAD_PARAM) params_memory.remove(MODEL_DOWNLOAD_ALL_PARAM) @@ -5392,7 +5402,7 @@ def setup(app): if params_memory.get_bool(MODEL_DOWNLOAD_ALL_PARAM) or (params_memory.get(MODEL_DOWNLOAD_PARAM, encoding="utf-8") or ""): return jsonify({"error": "A model download is already in progress."}), 409 - missing_models = [model for model in get_model_catalog() if not model["installed"]] + missing_models = [model for model in get_model_catalog() if not model["installed"] and (not model["requiresGpu"] or model["gpuAvailable"])] if not missing_models: return jsonify({"message": "All models are already installed."}), 200 @@ -5684,6 +5694,7 @@ def setup(app): except Exception: on_disk_files = set() + external_gpu_present = external_gpu_available() models_by_key = {} for i, key in enumerate(available): canonical_key = canonical_model_key(key) @@ -5695,6 +5706,8 @@ def setup(app): artifact_format = artifact_formats[i] if i < len(artifact_formats) else "" model_series = series[i] if i < len(series) and series[i] else "Custom Series" released = released_dates[i] if i < len(released_dates) else "" + requires_external_gpu = model_uses_external_gpu(canonical_key) + gpu_available = not requires_external_gpu or external_gpu_present existing = models_by_key.get(canonical_key) if existing is None: @@ -5704,6 +5717,8 @@ def setup(app): "series": model_series, "version": model_version, "artifactFormat": artifact_format, + "requiresGpu": requires_external_gpu, + "gpuAvailable": gpu_available, "released": released, "builtin": is_builtin_model_key(canonical_key), "communityFavorite": canonical_key in community_favorites, @@ -5724,6 +5739,8 @@ def setup(app): existing["builtin"] = existing["builtin"] or is_builtin_model_key(canonical_key) existing["communityFavorite"] = existing["communityFavorite"] or canonical_key in community_favorites existing["userFavorite"] = existing["userFavorite"] or canonical_key in user_favorites + existing["requiresGpu"] = existing["requiresGpu"] or requires_external_gpu + existing["gpuAvailable"] = not existing["requiresGpu"] or external_gpu_present default_key = _default_model_key() default_entry = models_by_key.setdefault(default_key, { @@ -5732,6 +5749,8 @@ def setup(app): "series": "Custom Series", "version": _default_model_version(), "artifactFormat": "tinygrad_single_v1", + "requiresGpu": False, + "gpuAvailable": True, "released": "", "builtin": True, "communityFavorite": default_key in community_favorites,