Compare commits

..

6 Commits

Author SHA1 Message Date
Jason Wen 5ad2bfdb75 ci: deprecate GitHub runners (#1933) 2026-08-21 00:35:38 -04:00
Jason Wen b742557d62 sunnylink: add model resolver (#1931)
* models: add get_default_model resolver for sunnylink

* models: move get_default_model to default_model.py
2026-08-20 21:56:57 -04:00
Nayan 5ecd05aedf models: add big model to default model resolution (#1929)
* device

* sunnylink

* lint

* lfs?

* Revert "lfs?"

This reverts commit bcdaec6b4c.

* update path

* Scope the default big model down to the sunnylink schema

* Drop the mock-only default model test

* Move the default model resolver out to separate PR

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-08-20 21:31:32 -04:00
Jason Wen 5ae100aa1d models fetcher: bump big model to v21 2026-08-20 19:19:59 -04:00
Jason Wen be76a88b80 ci override LFS fetch exclude for real ONNX file retrieval (#1928)
ci: override lfs.fetchexclude so the model fetch pulls real ONNX files instead of pointers
2026-08-20 19:12:59 -04:00
James Vecellio-Grant 049d225d5a ci: Dedicated Model Runner (#1922)
* ci: Dedicated Model Runner

* recurse

* not needed

* fix wrapper

* whoops

* bypass

* modeld_v2: restore chestnut link check before big model build

* modeld_v2: stage onnx to disk instead of shared memory

* ci: clear unchunked onnx temps before model build

* ci: stream the pkl hash instead of loading it into memory

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-08-20 16:18:17 -04:00
14 changed files with 182 additions and 527 deletions
+34 -16
View File
@@ -103,20 +103,25 @@ jobs:
- run: | - run: |
cd ${{ github.workspace }}/openpilot/openpilot cd ${{ github.workspace }}/openpilot/openpilot
if [ "${{ inputs.target_hardware }}" != "usbgpu" ]; then if [ "${{ inputs.target_hardware }}" != "usbgpu" ]; then
git lfs pull -X "selfdrive/modeld/models/big_*.onnx" -X "selfdrive/modeld/models/dmonitoring_*.onnx" git lfs pull -X "**/selfdrive/modeld/models/big_*.onnx,**/selfdrive/modeld/models/dmonitoring_*.onnx"
rm -f selfdrive/modeld/models/big_*.onnx selfdrive/modeld/models/dmonitoring_*.onnx rm -f selfdrive/modeld/models/big_*.onnx selfdrive/modeld/models/dmonitoring_*.onnx
else else
git lfs pull -I "selfdrive/modeld/models/big_*.onnx" git lfs pull -I "**/selfdrive/modeld/models/big_*.onnx" -X ""
find selfdrive/modeld/models -name "*.onnx" ! -name "big_*.onnx" -delete find selfdrive/modeld/models -name "*.onnx" ! -name "big_*.onnx" -delete
fi fi
if grep -lIF "version https://git-lfs.github.com/spec/v1" selfdrive/modeld/models/*.onnx; then
echo "::error::the ONNX files above are still LFS pointers, not real models"
exit 1
fi
- name: 'Upload Artifact' - name: 'Upload Artifact'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: models-${{ env.REF }}${{ inputs.artifact_suffix }} name: models-${{ env.REF }}${{ inputs.artifact_suffix }}
path: ${{ github.workspace }}/openpilot/openpilot/selfdrive/modeld/models/*.onnx path: ${{ github.workspace }}/openpilot/openpilot/selfdrive/modeld/models/*.onnx
if-no-files-found: error
build_model: build_model:
runs-on: [self-hosted, tici] runs-on: [self-hosted, usbgpu]
needs: get_model needs: get_model
env: env:
MODEL_NAME: ${{ inputs.custom_name || inputs.upstream_branch }} (${{ needs.get_model.outputs.model_date }}) MODEL_NAME: ${{ inputs.custom_name || inputs.upstream_branch }} (${{ needs.get_model.outputs.model_date }})
@@ -127,7 +132,6 @@ jobs:
fetch-depth: 1 fetch-depth: 1
submodules: recursive submodules: recursive
- run: git lfs pull
- name: Set environment variables - name: Set environment variables
id: set-env id: set-env
@@ -160,7 +164,7 @@ jobs:
fi fi
source ${UV_PROJECT_ENVIRONMENT}/bin/activate source ${UV_PROJECT_ENVIRONMENT}/bin/activate
PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable
rm -rf ${{ env.MODELS_DIR }}/*.onnx rm -rf ${{ env.MODELS_DIR }}/*.onnx*
- name: Download model artifacts - name: Download model artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
@@ -180,6 +184,7 @@ jobs:
MODEL_SIZE=$(python3 -c "from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')") MODEL_SIZE=$(python3 -c "from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')")
CAMERA_RES=$(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')") CAMERA_RES=$(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')")
TG_FLAGS_QCOM="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
if [ "${{ inputs.target_hardware }}" == "usbgpu" ]; then if [ "${{ inputs.target_hardware }}" == "usbgpu" ]; then
echo "USBGPU build" echo "USBGPU build"
export USBGPU=1 export USBGPU=1
@@ -187,27 +192,40 @@ jobs:
OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl" OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl"
else else
echo "QCOM build" echo "QCOM build"
TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1" TG_FLAGS="$TG_FLAGS_QCOM"
OUTPUT_PKL="${{ env.MODELS_DIR }}/driving_tinygrad.pkl" OUTPUT_PKL="${{ env.MODELS_DIR }}/driving_tinygrad.pkl"
fi fi
# Generate metadata for all ONNX files # Generate metadata for all ONNX files
find "${{ env.MODELS_DIR }}" -maxdepth 1 -name '*.onnx' | while IFS= read -r onnx_file; do find "${{ env.MODELS_DIR }}" -maxdepth 1 -name '*.onnx' | while IFS= read -r onnx_file; do
echo "Generating metadata: $onnx_file" echo "Generating metadata: $onnx_file"
env ${TG_FLAGS} python3 "${{ env.MODELS_DIR }}/../get_model_metadata.py" "$onnx_file" || true env ${TG_FLAGS_QCOM} python3 "${{ env.MODELS_DIR }}/../get_model_metadata.py" "$onnx_file" || true
done done
# Detect model type and build compile args # Detect model type and build compile args
VISION_ONNX="${{ env.MODELS_DIR }}/driving_vision.onnx" VISION_ONNX=""
POLICY_ONNX="${{ env.MODELS_DIR }}/driving_policy.onnx" for f in "${{ env.MODELS_DIR }}/driving_vision.onnx" "${{ env.MODELS_DIR }}/big_driving_vision.onnx"; do
OFF_POLICY_ONNX="${{ env.MODELS_DIR }}/driving_off_policy.onnx" [ -f "$f" ] && VISION_ONNX="$f" && break
ON_POLICY_ONNX="${{ env.MODELS_DIR }}/driving_on_policy.onnx" done
POLICY_ONNX=""
for f in "${{ env.MODELS_DIR }}/driving_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_policy.onnx"; do
[ -f "$f" ] && POLICY_ONNX="$f" && break
done
OFF_POLICY_ONNX=""
for f in "${{ env.MODELS_DIR }}/driving_off_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_off_policy.onnx"; do
[ -f "$f" ] && OFF_POLICY_ONNX="$f" && break
done
ON_POLICY_ONNX=""
for f in "${{ env.MODELS_DIR }}/driving_on_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_on_policy.onnx"; do
[ -f "$f" ] && ON_POLICY_ONNX="$f" && break
done
SUPERCOMBO_ONNX="" SUPERCOMBO_ONNX=""
for f in "${{ env.MODELS_DIR }}/supercombo.onnx" "${{ env.MODELS_DIR }}/driving_supercombo.onnx"; do for f in "${{ env.MODELS_DIR }}/supercombo.onnx" "${{ env.MODELS_DIR }}/driving_supercombo.onnx" "${{ env.MODELS_DIR }}/big_supercombo.onnx" "${{ env.MODELS_DIR }}/big_driving_supercombo.onnx"; do
if [ -f "$f" ]; then [ -f "$f" ] && SUPERCOMBO_ONNX="$f" && break
SUPERCOMBO_ONNX="$f"
break
fi
done done
MODEL_TYPE="" ONNX_ARGS="" OUTPUT_NAME="" MODEL_TYPE="" ONNX_ARGS="" OUTPUT_NAME=""
@@ -4,13 +4,11 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License. This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details. See the LICENSE.md file in the root directory for more details.
""" """
from collections.abc import Callable
import pyray as rl import pyray as rl
from openpilot.cereal import custom from openpilot.cereal import custom
from openpilot.sunnypilot.models.default_model import DEFAULT_MODEL from openpilot.sunnypilot.models.default_model import DEFAULT_MODEL
from openpilot.selfdrive.ui.mici.widgets.button import BigButton from openpilot.selfdrive.ui.mici.widgets.button import BigButton
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialog
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.models import ModelsLayout from openpilot.selfdrive.ui.sunnypilot.layouts.settings.models import ModelsLayout
from openpilot.selfdrive.ui.ui_state import ui_state, device from openpilot.selfdrive.ui.ui_state import ui_state, device
from openpilot.system.ui.lib.application import FontWeight, gui_app from openpilot.system.ui.lib.application import FontWeight, gui_app
@@ -19,24 +17,6 @@ from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.scroller import NavScroller from openpilot.system.ui.widgets.scroller import NavScroller
def _build_folders() -> dict[str, list]:
manager = ui_state.sm["modelManagerSP"]
bundles = manager.availableBundles
folders = {}
for bundle in bundles:
folder = next((override.value for override in bundle.overrides if override.key == "folder"), "")
folders.setdefault(folder, []).append(bundle)
favs = ui_state.params.get("ModelManager_Favs")
favorites = set(favs.split(';')) if favs else set()
if favorites:
for fav_bundle in [bundle for bundle in bundles if bundle.ref in favorites]:
folders.setdefault("favorites", []).append(fav_bundle)
return folders
class CurrentModelInfo(Widget): class CurrentModelInfo(Widget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@@ -66,41 +46,6 @@ class CurrentModelInfo(Widget):
self.info_text.set_position(self._rect.x + 20, self._rect.y + 161 - 25) self.info_text.set_position(self._rect.x + 20, self._rect.y + 161 - 25)
self.info_text.render() self.info_text.render()
class FolderSelectionMici(NavScroller):
def __init__(self, folder_name: str | None = None,
select_default_callback: Callable | None = None,
select_folder_callback: Callable | None = None,
select_model_callback: Callable | None = None):
super().__init__()
folders = _build_folders()
btns = []
if folder_name is None:
assert select_default_callback is not None and select_folder_callback is not None
default_btn = BigButton(f"{DEFAULT_MODEL} (Default)".lower())
default_btn.set_click_callback(select_default_callback)
btns.append(default_btn)
for folder in sorted(folders.keys(), key=lambda f: max((bundle.index for bundle in folders[f]), default=-1), reverse=True):
btn = BigButton(folder.lower())
btn.set_click_callback(lambda f=folder: select_folder_callback(f))
if folder.lower() == "favorites":
btns.insert(0, btn)
else:
btns.append(btn)
else:
assert select_model_callback is not None
for bundle in sorted(folders.get(folder_name, []), key=lambda b: b.index, reverse=True):
btn = BigButton(bundle.displayName.lower())
btn.set_click_callback(lambda b=bundle: select_model_callback(b))
btns.append(btn)
self._scroller.add_widgets(btns)
class ModelsLayoutMici(NavScroller): class ModelsLayoutMici(NavScroller):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@@ -114,47 +59,81 @@ class ModelsLayoutMici(NavScroller):
self.select_model_btn = BigButton(tr("select model")) self.select_model_btn = BigButton(tr("select model"))
self.select_model_btn.set_click_callback(self._show_folders) self.select_model_btn.set_click_callback(self._show_folders)
self.clear_cache_btn = BigButton(tr("clear cache"), "")
self.clear_cache_btn.set_click_callback(self._clear_cache)
self.cancel_download_btn = BigButton(tr("cancel download")) self.cancel_download_btn = BigButton(tr("cancel download"))
self.cancel_download_btn.set_click_callback(lambda: ui_state.params.remove("ModelManager_DownloadIndex")) self.cancel_download_btn.set_click_callback(lambda: ui_state.params.remove("ModelManager_DownloadIndex"))
self.main_items = [self.current_model_info, self.select_model_btn, self.clear_cache_btn, self.cancel_download_btn] self.main_items = [self.current_model_info, self.select_model_btn, self.cancel_download_btn]
self._scroller.add_widgets(self.main_items) self._scroller.add_widgets(self.main_items)
@property @property
def model_manager(self): def model_manager(self):
return ui_state.sm["modelManagerSP"] return ui_state.sm["modelManagerSP"]
def _get_grouped_bundles(self, favorites = None):
bundles = self.model_manager.availableBundles
folders = {}
for bundle in bundles:
folder = next((override.value for override in bundle.overrides if override.key == "folder"), "")
folders.setdefault(folder, []).append(bundle)
if favorites:
for fav_bundle in [bundle for bundle in bundles if bundle.ref in favorites]:
folders.setdefault("favorites", []).append(fav_bundle)
return folders
def _push_selection_view(self, items):
scroller = NavScroller()
scroller._scroller.add_widgets(items)
gui_app.push_widget(scroller)
def _show_folders(self): def _show_folders(self):
self.focused_widget = self.select_model_btn self.focused_widget = self.select_model_btn
def select_default(): favs = ui_state.params.get("ModelManager_Favs")
ui_state.params.remove("ModelManager_ActiveBundle") favorites = set(favs.split(';')) if favs else set()
gui_app.pop_widgets_to(self, instant=True)
self._scroller.scroll_panel.set_offset(0)
self._scroller.scroll_to(0)
def select_model(bundle): folders = self._get_grouped_bundles(favorites)
ui_state.params.put("ModelManager_DownloadIndex", bundle.index) folder_buttons = []
gui_app.pop_widgets_to(self, instant=True) default_btn = BigButton(f"{DEFAULT_MODEL} (Default)".lower())
self._scroller.scroll_panel.set_offset(0) default_btn.set_click_callback(self._select_default)
self._scroller.scroll_to(0) folder_buttons.append(default_btn)
def select_folder(folder_name): for folder in sorted(folders.keys(), key=lambda f: max((bundle.index for bundle in folders[f]), default=-1), reverse=True):
gui_app.push_widget(FolderSelectionMici(folder_name, select_model_callback=select_model)) if folder.lower() in ["release models", "master models", "favorites"]:
btn = BigButton(folder.lower())
btn.set_click_callback(lambda f=folder: self._select_folder(f))
if folder.lower() == "favorites":
folder_buttons.insert(0, btn)
else:
folder_buttons.append(btn)
self._push_selection_view(folder_buttons)
gui_app.push_widget(FolderSelectionMici(select_default_callback=select_default, select_folder_callback=select_folder)) def _pop_to_main(self):
gui_app.pop_widgets_to(self)
def _clear_cache(self): def _select_model(self, bundle):
def confirm_callback(): ui_state.params.put("ModelManager_DownloadIndex", bundle.index)
ui_state.params.put_bool("ModelManager_ClearCache", True) self._pop_to_main()
lbl = tr("slide to clear cache") def _select_default(self):
icon = gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64) ui_state.params.remove("ModelManager_ActiveBundle")
dlg = BigConfirmationDialog(lbl, icon, confirm_callback=confirm_callback, red=True) self._pop_to_main()
gui_app.push_widget(dlg)
def _select_folder(self, folder_name):
favs = ui_state.params.get("ModelManager_Favs")
favorites = set(favs.split(';')) if favs else set()
folders = self._get_grouped_bundles(favorites)
bundles = sorted(folders.get(folder_name, []), key=lambda b: b.index, reverse=True)
btns = []
for bundle in bundles:
txt = bundle.displayName.lower()
btn = BigButton(txt)
btn.set_click_callback(lambda b=bundle: self._select_model(b))
btns.append(btn)
self._push_selection_view(btns)
def hide_event(self): def hide_event(self):
super().hide_event() super().hide_event()
@@ -166,7 +145,6 @@ class ModelsLayoutMici(NavScroller):
super()._update_state() super()._update_state()
self.select_model_btn.set_enabled(ui_state.is_offroad()) self.select_model_btn.set_enabled(ui_state.is_offroad())
self.clear_cache_btn.set_enabled(ui_state.is_offroad())
self.cancel_download_btn.set_visible(False) self.cancel_download_btn.set_visible(False)
self.current_model_info.current_model_header._shimmer = False self.current_model_info.current_model_header._shimmer = False
self.current_model_info.info_header._shimmer = False self.current_model_info.info_header._shimmer = False
@@ -213,3 +191,4 @@ class ModelsLayoutMici(NavScroller):
self.current_model_info.info_header.set_text(tr("progress") + self._download_progress) self.current_model_info.info_header.set_text(tr("progress") + self._download_progress)
self.current_model_info.info_header._shimmer = True self.current_model_info.info_header._shimmer = True
self.current_model_info.info_text.set_text(f"{progress/count:.2f}%") self.current_model_info.info_text.set_text(f"{progress/count:.2f}%")
@@ -272,18 +272,17 @@ def _parse_size(size_str: str) -> tuple[int, int]:
return int(width), int(height) return int(width), int(height)
def read_file_chunked_to_shm(path): def read_file_chunked_to_disk(path):
if not path: if not path:
return None return None
import atexit import atexit
import shutil import shutil
from openpilot.common.file_chunker import open_file_chunked from openpilot.common.file_chunker import open_file_chunked
from openpilot.common.hardware.hw import Paths tmp_path = f'{path}.unchunked'
shm_path = os.path.join(Paths.shm_path(), os.path.basename(path)) with open(tmp_path, 'wb') as f, open_file_chunked(path) as src:
atexit.register(lambda: os.path.exists(shm_path) and os.remove(shm_path)) shutil.copyfileobj(src, f)
with open(shm_path, 'wb') as dst, open_file_chunked(path) as src: atexit.register(lambda: os.path.exists(tmp_path) and os.remove(tmp_path))
shutil.copyfileobj(src, dst) return tmp_path
return shm_path
def _load_policy_runners(args: argparse.Namespace) -> tuple[list, list]: def _load_policy_runners(args: argparse.Namespace) -> tuple[list, list]:
@@ -327,11 +326,11 @@ if __name__ == "__main__":
model_w, model_h = args.model_size model_w, model_h = args.model_size
output_data = {} output_data = {}
args.vision_onnx = read_file_chunked_to_shm(args.vision_onnx) args.vision_onnx = read_file_chunked_to_disk(args.vision_onnx)
args.policy_onnx = read_file_chunked_to_shm(args.policy_onnx) args.policy_onnx = read_file_chunked_to_disk(args.policy_onnx)
args.off_policy_onnx = read_file_chunked_to_shm(args.off_policy_onnx) args.off_policy_onnx = read_file_chunked_to_disk(args.off_policy_onnx)
args.on_policy_onnx = read_file_chunked_to_shm(args.on_policy_onnx) args.on_policy_onnx = read_file_chunked_to_disk(args.on_policy_onnx)
args.supercombo_onnx = read_file_chunked_to_shm(args.supercombo_onnx) args.supercombo_onnx = read_file_chunked_to_disk(args.supercombo_onnx)
vision_runner = OnnxRunner(args.vision_onnx) if args.vision_onnx else None vision_runner = OnnxRunner(args.vision_onnx) if args.vision_onnx else None
@@ -5,10 +5,15 @@ This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details. See the LICENSE.md file in the root directory for more details.
""" """
import os
import tempfile
from pathlib import Path
import numpy as np import numpy as np
from openpilot.common.parameterized import parameterized from openpilot.common.parameterized import parameterized
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, _detect_desire_key from openpilot.common.file_chunker import chunk_file, get_chunk_targets
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, _detect_desire_key, read_file_chunked_to_disk
from openpilot.common.test import OpenpilotTestCase from openpilot.common.test import OpenpilotTestCase
@@ -160,3 +165,33 @@ class TestOutputSlicePreservation(OpenpilotTestCase):
policy_slices = {'plan': slice(0, 495), 'meta': slice(495, 550)} policy_slices = {'plan': slice(0, 495), 'meta': slice(495, 550)}
assert set(vision_slices.keys()) & set(policy_slices.keys()) == set(), \ assert set(vision_slices.keys()) & set(policy_slices.keys()) == set(), \
"vision and policy slices should not overlap in keys" "vision and policy slices should not overlap in keys"
class TestReadFileChunkedToDisk(OpenpilotTestCase):
def test_none_passthrough(self):
assert read_file_chunked_to_disk(None) is None
def test_unchunked_source_staged_on_disk(self):
with tempfile.TemporaryDirectory() as d:
src = Path(d) / "driving_supercombo.onnx"
payload = os.urandom(1024)
src.write_bytes(payload)
out = Path(read_file_chunked_to_disk(str(src)))
assert out.parent == Path(d)
assert out.name == "driving_supercombo.onnx.unchunked"
assert out.read_bytes() == payload
def test_chunked_source_reassembled_on_disk(self):
with tempfile.TemporaryDirectory() as d:
src = Path(d) / "driving_supercombo.onnx"
payload = os.urandom(4096)
src.write_bytes(payload)
chunk_file(str(src), get_chunk_targets(str(src), len(payload)))
assert not src.exists()
out = Path(read_file_chunked_to_disk(str(src)))
assert out.parent == Path(d)
assert out.read_bytes() == payload
+23 -30
View File
@@ -4,7 +4,13 @@ import hashlib
from openpilot.common.basedir import BASEDIR from openpilot.common.basedir import BASEDIR
from openpilot.sunnypilot import get_file_hash from openpilot.sunnypilot import get_file_hash
from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL from openpilot.selfdrive.modeld.helpers import usbgpu_present
from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL, DEFAULT_BIG_MODEL
def get_default_model() -> str:
return DEFAULT_BIG_MODEL if usbgpu_present() else DEFAULT_MODEL
DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "model_name.py") DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "model_name.py")
MODEL_HASH_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "tests", "model_hash") MODEL_HASH_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "tests", "model_hash")
@@ -13,7 +19,6 @@ SUPERCOMBO_ONNX_PATH = os.path.join(BASEDIR, "openpilot", "selfdrive", "modeld",
def update_model_hash(): def update_model_hash():
supercombo_hash = get_file_hash(SUPERCOMBO_ONNX_PATH) supercombo_hash = get_file_hash(SUPERCOMBO_ONNX_PATH)
combined_hash = hashlib.sha256(supercombo_hash.encode()).hexdigest() combined_hash = hashlib.sha256(supercombo_hash.encode()).hexdigest()
with open(MODEL_HASH_PATH, "w") as f: with open(MODEL_HASH_PATH, "w") as f:
@@ -22,40 +27,28 @@ def update_model_hash():
print(f"Generated and updated new combined model hash to {MODEL_HASH_PATH}") print(f"Generated and updated new combined model hash to {MODEL_HASH_PATH}")
def get_current_default_model_name(): def update_default_model_names(default_model_name: str, default_big_model_name: str):
print("[GET DEFAULT MODEL NAME]") print("[CHANGE DEFAULT MODEL NAMES]")
name = DEFAULT_MODEL
print(f'Current default model name: "{name}"')
return name
def update_default_model_name(name: str):
print("[CHANGE DEFAULT MODEL NAME]")
with open(DEFAULT_MODEL_NAME_PATH, "w") as f: with open(DEFAULT_MODEL_NAME_PATH, "w") as f:
f.write(f'DEFAULT_MODEL = "{name}"\n') f.write(f'DEFAULT_MODEL = "{default_model_name}"\n')
print(f'New default model name: "{name}"') f.write(f'DEFAULT_BIG_MODEL = "{default_big_model_name}"\n')
print(f'New default small model name: "{default_model_name}"')
print(f'New default big model name: "{default_big_model_name}"')
print("[DONE]") print("[DONE]")
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Update default model name and hash") parser = argparse.ArgumentParser(description="Update default model names and hash")
parser.add_argument("--new_name", type=str, help="New default model name") parser.add_argument("--new_small_model_name", type=str, help="New default small model name")
parser.add_argument("--new_big_model_name", type=str, help="New default big model name")
args = parser.parse_args() args = parser.parse_args()
if not args.new_name: if args.new_small_model_name is None and args.new_big_model_name is None:
print("Warning: No new default model name provided. Use --new_name to specify") new_name = input(f'Enter new default small model name (current: "{DEFAULT_MODEL}", leave empty to keep): ').strip()
print("Default model name and hash will not be updated! (aborted)") new_big_model_name = input(f'Enter new default big model name (current: "{DEFAULT_BIG_MODEL}", leave empty to keep): ').strip()
exit(0) else:
new_name, new_big_model_name = args.new_small_model_name, args.new_big_model_name
current_name = get_current_default_model_name() update_default_model_names(new_name or DEFAULT_MODEL, new_big_model_name or DEFAULT_BIG_MODEL)
new_name = args.new_name
if current_name == new_name:
print(f'Proposed default model name: "{new_name}"')
confirm = input("Proposed default model name is the same as the current default model name. Confirm? (y/n): ").upper().strip()
if confirm != "Y":
print("Default model name and hash will not be updated! (aborted)")
exit(0)
update_default_model_name(new_name)
update_model_hash() update_model_hash()
+1 -1
View File
@@ -141,7 +141,7 @@ class ModelCache:
class ModelFetcher: class ModelFetcher:
"""Handles fetching and caching of model data from remote source""" """Handles fetching and caching of model data from remote source"""
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v20.json" MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v20.json"
MODEL_URL_USBGPU = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_usbgpu_v20.json" MODEL_URL_USBGPU = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_usbgpu_v21.json"
def __init__(self, params: Params): def __init__(self, params: Params):
self.params = params self.params = params
@@ -1 +1,2 @@
DEFAULT_MODEL = "CD210" DEFAULT_MODEL = "CD210"
DEFAULT_BIG_MODEL = "Lebowski"
@@ -20,4 +20,4 @@ class TestDefaultModel(OpenpilotTestCase):
with open(MODEL_HASH_PATH) as f: with open(MODEL_HASH_PATH) as f:
current_hash = f.read().strip() current_hash = f.read().strip()
assert combined_hash == current_hash, "Run sunnypilot/models/default_model.py to update the default model name and hash" assert combined_hash == current_hash, "Run openpilot/sunnypilot/models/default_model.py to update the default model name and hash"
@@ -28,7 +28,7 @@ from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutExce
create_connection, WebSocketConnectionClosedException) create_connection, WebSocketConnectionClosedException)
import openpilot.cereal.messaging as messaging import openpilot.cereal.messaging as messaging
from openpilot.sunnypilot.models.default_model import DEFAULT_MODEL from openpilot.sunnypilot.models.default_model import get_default_model
from openpilot.sunnypilot.selfdrive.car.sync_sunnylink_params import update_car_list_param from openpilot.sunnypilot.selfdrive.car.sync_sunnylink_params import update_car_list_param
from openpilot.sunnypilot.sunnylink.api import SunnylinkApi from openpilot.sunnypilot.sunnylink.api import SunnylinkApi
from openpilot.sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, get_param_as_byte, save_param_from_base64_encoded_string from openpilot.sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, get_param_as_byte, save_param_from_base64_encoded_string
@@ -181,7 +181,7 @@ def getParamsMetadata() -> str:
schema = generate_schema() schema = generate_schema()
schema["capabilities"] = generate_capabilities() schema["capabilities"] = generate_capabilities()
schema["capability_labels"] = CAPABILITY_LABELS schema["capability_labels"] = CAPABILITY_LABELS
schema["default_model"] = DEFAULT_MODEL schema["default_model"] = get_default_model()
raw = json.dumps(schema, separators=(",", ":")).encode("utf-8") raw = json.dumps(schema, separators=(",", ":")).encode("utf-8")
return base64.b64encode(gzip.compress(raw)).decode("utf-8") return base64.b64encode(gzip.compress(raw)).decode("utf-8")
except Exception: except Exception:
-40
View File
@@ -1,40 +0,0 @@
#!/usr/bin/env bash
# Define the service name
SERVICE_NAME="actions.runner.sunnypilot.$(uname -n)"
# Function to control the service
control_service() {
local action=$1 # Store the function argument in a local variable
sudo systemctl $action ${SERVICE_NAME}
}
service_exists_and_is_loaded() {
sudo systemctl status ${SERVICE_NAME} &>/dev/null
if [[ $? -ne 4 ]]; then
return 0 # Service is known to systemd (i.e., loaded)
else
return 1 # Service is unknown to systemd (i.e., not loaded)
fi
}
# Check for required argument
if [[ -z $1 ]] || { [[ $1 != "start" ]] && [[ $1 != "stop" ]]; }; then
echo "Usage: $0 {start|stop}"
exit 1
fi
# Store the script argument in a descriptive variable
ACTION=$1
# Trap EXIT signal (Ctrl+C) and stop the service
trap 'control_service stop ; exit' SIGINT SIGKILL EXIT
# Enter the main loop
while true; do
# Check if the service is actually present on the system
if service_exists_and_is_loaded; then
control_service $ACTION # Call the function with the specified action
fi
sleep 1 # Pause before the next iteration
done
@@ -68,10 +68,6 @@ def only_offroad(started: bool, params: Params, CP: car.CarParams) -> bool:
def livestream(started: bool, params: Params, CP: car.CarParams) -> bool: def livestream(started: bool, params: Params, CP: car.CarParams) -> bool:
return params.get_bool("IsLiveStreaming") return params.get_bool("IsLiveStreaming")
def use_github_runner(started, params, CP: car.CarParams) -> bool:
return not PC and params.get_bool("EnableGithubRunner") and (
not params.get_bool("NetworkMetered") and not params.get_bool("GithubRunnerSufficientVoltage"))
def use_copyparty(started, params, CP: car.CarParams) -> bool: def use_copyparty(started, params, CP: car.CarParams) -> bool:
return bool(params.get_bool("EnableCopyparty")) return bool(params.get_bool("EnableCopyparty"))
@@ -189,10 +185,6 @@ procs += [
NativeProcess("locationd_llk", "openpilot/sunnypilot/selfdrive/locationd", ["./locationd"], only_onroad), NativeProcess("locationd_llk", "openpilot/sunnypilot/selfdrive/locationd", ["./locationd"], only_onroad),
] ]
if os.path.exists("./github_runner.sh"):
procs += [NativeProcess("github_runner_start", "openpilot/system/manager",
["./github_runner.sh", "start"], and_(only_offroad, use_github_runner), sigkill=False)]
if os.path.exists("../../sunnypilot/sunnylink/uploader.py"): if os.path.exists("../../sunnypilot/sunnylink/uploader.py"):
procs += [PythonProcess("sunnylink_uploader", "openpilot.sunnypilot.sunnylink.uploader", use_sunnylink_uploader_shim)] procs += [PythonProcess("sunnylink_uploader", "openpilot.sunnypilot.sunnylink.uploader", use_sunnylink_uploader_shim)]
-260
View File
@@ -1,260 +0,0 @@
#!/usr/bin/env bash
set -e
# Default values
DEFAULT_REPO_URL="https://github.com/sunnypilot"
START_AT_BOOT=false
RESTORE_MODE=false
RUNNER_VERSION="2.325.0"
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--start-at-boot)
START_AT_BOOT=true
shift
;;
--token)
GITHUB_TOKEN="$2"
shift 2
;;
--repo)
REPO_URL="$2"
shift 2
;;
--restore)
RESTORE_MODE=true
shift
;;
*)
if [ -z "$GITHUB_TOKEN" ]; then
GITHUB_TOKEN="$1"
elif [ -z "$REPO_URL" ]; then
REPO_URL="$1"
fi
shift
;;
esac
done
# Determine BASE_DIR based on mount point
if mountpoint -q /data/media; then
BASE_DIR="/data/media/0/github"
else
BASE_DIR="/data/github"
fi
# Constants
RUNNER_USER="github-runner"
USER_GROUPS="comma,gpu,gpio,sudo"
RUNNER_DIR="${BASE_DIR}/runner"
BUILDS_DIR="${BASE_DIR}/builds"
LOGS_DIR="${BASE_DIR}/logs"
CACHE_DIR="${BASE_DIR}/cache"
OPENPILOT_DIR="${BASE_DIR}/openpilot"
# Basic utility functions (no dependencies)
remount_rw() {
sudo mount -o remount,rw /
}
remount_ro() {
sync || true # Try to sync but continue even if it fails
sudo mount -o remount,ro / # Always try to remount as read-only
}
# Always ensure we try to remount as read-only on exit
trap remount_ro EXIT
setup_runner_user() {
sudo useradd --comment 'GitHub Runner' --create-home --home-dir ${BASE_DIR} ${RUNNER_USER} --shell /bin/bash -G ${USER_GROUPS} || sudo usermod -aG ${USER_GROUPS} ${RUNNER_USER}
}
create_sudoers_entry() {
sudo grep -qxF "${RUNNER_USER} ALL=(ALL) NOPASSWD: ALL" /etc/sudoers || echo "${RUNNER_USER} ALL=(ALL) NOPASSWD: ALL" | sudo tee -a /etc/sudoers
}
set_directory_permissions() {
sudo chown -R ${RUNNER_USER}:comma "$BASE_DIR"
sudo chmod -R g+rwx "$BASE_DIR"
sudo find "$BASE_DIR" -type d -exec chmod g+s {} +
}
setup_directories() {
echo "Creating necessary directories..."
sudo mkdir -p "$RUNNER_DIR" "$BUILDS_DIR" "$LOGS_DIR" "$CACHE_DIR" "$OPENPILOT_DIR"
mkdir -p "/data/openpilot"
sudo chown -R comma:comma "/data/openpilot"
sync
}
wipe_bash_logout() {
export BASE_DIR
sudo -u ${RUNNER_USER} bash -c "touch ${BASE_DIR}/.bash_logout"
sudo -u ${RUNNER_USER} bash -c "truncate -s 0 '${BASE_DIR}/.bash_logout'"
}
# System configuration functions (depends on basic utility functions)
setup_system_configs() {
echo "Setting up system configurations..."
remount_rw
setup_runner_user
create_sudoers_entry
remount_ro
set_directory_permissions
wipe_bash_logout
}
# Runner setup functions
install_runner() {
echo "Downloading and setting up runner..."
cd "$RUNNER_DIR"
curl -o actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz -L https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz
sudo -u ${RUNNER_USER} tar -xzf ./actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz
sudo rm ./actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz
sudo chmod +x ./config.sh
}
configure_runner() {
remount_rw
echo "Configuring runner..."
cd "$RUNNER_DIR"
sudo -u ${RUNNER_USER} ./config.sh --url "$REPO_URL" --token "$GITHUB_TOKEN" --name $(hostname) --runnergroup "tici-tizi" --labels "tici" --work "$BUILDS_DIR" --unattended
remount_ro
}
create_service_template() {
echo "Creating service template..."
cat <<EOL > "$RUNNER_DIR/bin/actions.runner.service.template"
[Unit]
Description={{Description}}
After=network-online.target nss-lookup.target time-sync.target
Wants=network-online.target nss-lookup.target time-sync.target
StartLimitInterval=5
StartLimitBurst=10
[Service]
Type=simple
User=root
ExecStart=/usr/bin/unshare -m -- /bin/bash -c 'mount --bind ${OPENPILOT_DIR} /data/openpilot && setpriv --reuid={{User}} --regid={{User}} --init-groups env HOME=${BASE_DIR} USER={{User}} LOGNAME={{User}} MAIL=/var/mail/{{User}} {{RunnerRoot}}/runsvc.sh'
WorkingDirectory={{RunnerRoot}}
KillMode=process
KillSignal=SIGTERM
TimeoutStopSec=5min
Restart=always
RestartSec=120
[Install]
WantedBy=multi-user.target
EOL
}
install_service() {
local service_name
if [ -f "${RUNNER_DIR}/.service" ]; then
service_name=$(cat "${RUNNER_DIR}/.service")
else
service_name="actions.runner.sunnypilot.$(uname -n)"
fi
create_service_template
remount_rw
local service_path="/etc/systemd/system/${service_name}"
echo "Installing systemd service..."
if [ -f "${service_path}" ]; then
echo "Service ${service_path} found in systemd, we will delete it"
sudo rm -f "${service_path}"
fi
cd "$RUNNER_DIR"
sudo ./svc.sh install $RUNNER_USER
if [ "$START_AT_BOOT" = false ]; then
sudo systemctl disable "${service_name}"
fi
remount_ro
}
check_restore_prerequisites() {
local can_restore=false
local service_name=""
# Check if base runner directory exists
if [ ! -d "${RUNNER_DIR}" ]; then
echo "ERROR: Runner directory ${RUNNER_DIR} does not exist"
echo "This directory is required for restore operations"
exit 1
fi
# First check if we have the required files for restoration
if [ -f "${RUNNER_DIR}/.credentials" ] && [ -f "${RUNNER_DIR}/.service" ]; then
can_restore=true
service_name=$(cat "${RUNNER_DIR}/.service")
echo "Found required runner configuration files"
else
echo "Missing required runner configuration files"
echo "Required: .credentials and .service files in ${RUNNER_DIR}"
exit 1
fi
if ! id "${RUNNER_USER}" &>/dev/null; then
echo "User ${RUNNER_USER} does not exist"
fi
# Only proceed if we can restore AND need to restore
if [ "$can_restore" = true ]; then
echo "Restoration is possible"
return 0
else
echo "No restoration possible"
exit 0
fi
}
perform_restore() {
echo "Starting runner restoration..."
setup_directories
setup_system_configs
install_service
echo "Runner restoration completed successfully"
}
perform_install() {
echo "Starting fresh installation..."
setup_directories
setup_system_configs
install_runner
set_directory_permissions
configure_runner
install_service
echo "Installation completed successfully"
}
main() {
if [ "$RESTORE_MODE" = true ]; then
echo "Running in restore mode - will only restore system configurations..."
check_restore_prerequisites
perform_restore
else
# Check required arguments for normal installation
if [ -z "$GITHUB_TOKEN" ]; then
echo "Usage: $0 [--start-at-boot] [--token <github_token>] [--repo <repository_url>] [--restore]"
echo "Required argument (except for --restore): github_token"
echo "Optional arguments:"
echo " --start-at-boot Enable auto-start at boot (default: false)"
echo " --repo Repository URL (default: ${DEFAULT_REPO_URL})"
echo " --restore Restore existing runner configuration"
exit 1
fi
# Set repository URL if not provided
REPO_URL="${REPO_URL:-$DEFAULT_REPO_URL}"
perform_install
fi
echo "Starting runner service..."
cd "$RUNNER_DIR"
sudo ./svc.sh start
}
main
+14 -10
View File
@@ -53,24 +53,28 @@ def create_pkl_name(full_name: str) -> str:
return pkl return pkl
def _read_pkl_bytes(pkl_path: Path) -> bytes: def _hash_pkl(pkl_path: Path) -> str:
manifest = Path(f"{pkl_path}.chunkmanifest") manifest = Path(f"{pkl_path}.chunkmanifest")
if manifest.exists(): if manifest.exists():
num_chunks = int(manifest.read_text().strip()) num_chunks = int(manifest.read_text().strip())
parts = [] paths = [Path(f"{pkl_path}.chunk{i + 1:02d}of{num_chunks:02d}") for i in range(num_chunks)]
for i in range(num_chunks): else:
chunk = Path(f"{pkl_path}.chunk{i + 1:02d}of{num_chunks:02d}") paths = [pkl_path]
parts.append(chunk.read_bytes())
return b''.join(parts) digest = hashlib.sha256()
return pkl_path.read_bytes() for path in paths:
with path.open('rb') as f:
while block := f.read(1024 * 1024):
digest.update(block)
return digest.hexdigest()
def _find_driving_pkl(output_path: Path) -> Path | None: def _find_driving_pkl(output_path: Path) -> Path | None:
for pattern in ('driving_tinygrad.pkl', 'driving_*_tinygrad.pkl'): for pattern in ('*driving_tinygrad.pkl', '*driving_*_tinygrad.pkl'):
matches = sorted(output_path.glob(pattern)) matches = sorted(output_path.glob(pattern))
if matches: if matches:
return matches[0] return matches[0]
for pattern in ('driving_tinygrad.pkl.chunkmanifest', 'driving_*_tinygrad.pkl.chunkmanifest'): for pattern in ('*driving_tinygrad.pkl.chunkmanifest', '*driving_*_tinygrad.pkl.chunkmanifest'):
matches = sorted(output_path.glob(pattern)) matches = sorted(output_path.glob(pattern))
if matches: if matches:
return Path(str(matches[0]).removesuffix('.chunkmanifest')) return Path(str(matches[0]).removesuffix('.chunkmanifest'))
@@ -87,7 +91,7 @@ def _rename_pkl_with_chunks(old_pkl: Path, new_pkl: Path) -> Path:
def generate_chunked_model(driving_pkl: Path) -> dict: def generate_chunked_model(driving_pkl: Path) -> dict:
tinygrad_hash = hashlib.sha256(_read_pkl_bytes(driving_pkl)).hexdigest() tinygrad_hash = _hash_pkl(driving_pkl)
chunks_config = [] chunks_config = []
manifest_file = Path(f"{driving_pkl}.chunkmanifest") manifest_file = Path(f"{driving_pkl}.chunkmanifest")
-66
View File
@@ -1,66 +0,0 @@
#!/usr/bin/env bash
# Determine BASE_DIR based on mount point
if mountpoint -q /data/media; then
GITHUB_BASE_DIR="/data/media/0/github"
else
GITHUB_BASE_DIR="/data/github"
fi
# Define directories and user
BIN_DIR="$GITHUB_BASE_DIR/bin"
BUILDS_DIR="$GITHUB_BASE_DIR/builds"
OPENPILOT_DIR="$GITHUB_BASE_DIR/openpilot"
LOGS_DIR="$GITHUB_BASE_DIR/logs"
CACHE_DIR="$GITHUB_BASE_DIR/cache"
RUNNER_USERNAME="github-runner"
# Define the systemd service name
SERVICE_NAME="github-runner"
USER_GROUPS="comma,gpu,gpio,sudo"
# Function to stop and disable the systemd service
stop_and_uninstall_service() {
cd $GITHUB_BASE_DIR/runner
sudo ./svc.sh stop
sudo ./svc.sh uninstall
}
# Function to remove the systemd service file
remove_runner() {
cd $GITHUB_BASE_DIR/runner
sudo rm .runner
sudo su -c './config.sh remove' github-runner
}
# Function to delete the Github Runner directories
delete_directories() {
sudo rm -rf "$BIN_DIR/github-runner"
sudo rm -rf "$GITHUB_BASE_DIR" "$BIN_DIR" "$BUILDS_DIR" "$LOGS_DIR" "$CACHE_DIR" "$OPENPILOT_DIR"
}
# Function to remove the Github Runner user
delete_user() {
for group in ${USER_GROUPS//,/ }
do
sudo gpasswd -d ${RUNNER_USERNAME} ${group}
done
sudo userdel -r ${RUNNER_USERNAME}
}
# Function to remove sudoers entry
remove_sudoers_entry() {
sudo sed -i.bak "/${RUNNER_USERNAME} ALL=(ALL) NOPASSWD: ALL/d" /etc/sudoers
}
# Make filesystem writable
sudo mount -o remount rw /
# Ensure filesystem is remounted as read-only on script exit
trap "sudo mount -o remount ro /" EXIT
# Call functions
stop_and_uninstall_service
remove_runner
delete_directories
delete_user
remove_sudoers_entry
# End of uninstall script