mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-08-22 19:53:45 +08:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e9bafbd353 | |||
| e372046ff1 | |||
| 07558166c8 | |||
| ca9338812e | |||
| 4667241fe7 | |||
| df5695ba08 | |||
| 76279f6540 | |||
| a49c260927 | |||
| 5ad2bfdb75 | |||
| b742557d62 | |||
| 5ecd05aedf | |||
| 5ae100aa1d | |||
| be76a88b80 | |||
| 049d225d5a |
@@ -0,0 +1,78 @@
|
|||||||
|
name: Build default big model
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
HF_REPO: sunnypilot/sunnypilot_models_v1
|
||||||
|
HF_DEFAULTS_PATH: models/defaults/big
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_model:
|
||||||
|
uses: ./.github/workflows/sunnypilot-build-model.yaml
|
||||||
|
with:
|
||||||
|
upstream_branch: ${{ github.sha }}
|
||||||
|
custom_name: default-big-model
|
||||||
|
target_hardware: usbgpu
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
upload_defaults:
|
||||||
|
needs: build_model
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
- run: git lfs pull -I "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx"
|
||||||
|
|
||||||
|
- name: Install huggingface_hub
|
||||||
|
run: pip install --upgrade "huggingface_hub>=0.22.0"
|
||||||
|
|
||||||
|
- name: Download artifact name
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: artifact-name-default-big-model
|
||||||
|
path: artifact_name
|
||||||
|
|
||||||
|
- name: Read artifact name
|
||||||
|
id: artifact
|
||||||
|
run: |
|
||||||
|
ARTIFACT_NAME=$(cat artifact_name/artifact_name.txt)
|
||||||
|
echo "artifact_name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Download model artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ steps.artifact.outputs.artifact_name }}
|
||||||
|
path: output
|
||||||
|
|
||||||
|
- name: Upload model to HF defaults
|
||||||
|
env:
|
||||||
|
HF_OIDC_RESOURCE: datasets/${{ env.HF_REPO }}
|
||||||
|
ARTIFACT_NAME: ${{ steps.artifact.outputs.artifact_name }}
|
||||||
|
run: |
|
||||||
|
rm -f output/artifact_name.txt
|
||||||
|
hf upload ${{ env.HF_REPO }} \
|
||||||
|
output/ \
|
||||||
|
"${HF_DEFAULTS_PATH}/${ARTIFACT_NAME}/" \
|
||||||
|
--repo-type=dataset
|
||||||
|
|
||||||
|
- name: Get tinygrad ref and ONNX hash
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
export PYTHONPATH=$(pwd)
|
||||||
|
echo "tinygrad_ref=$(python3 openpilot/sunnypilot/models/tinygrad_ref.py)" >> $GITHUB_OUTPUT
|
||||||
|
echo "onnx_sha256=$(sha256sum openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx | cut -d' ' -f1)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Update default_models.json on HF
|
||||||
|
env:
|
||||||
|
HF_OIDC_RESOURCE: datasets/${{ env.HF_REPO }}
|
||||||
|
ARTIFACT_NAME: ${{ steps.artifact.outputs.artifact_name }}
|
||||||
|
run: |
|
||||||
|
python3 release/ci/upload_default_model.py \
|
||||||
|
--hf-repo "${{ env.HF_REPO }}" \
|
||||||
|
--hf-defaults-path "${{ env.HF_DEFAULTS_PATH }}" \
|
||||||
|
--artifact-name "$ARTIFACT_NAME" \
|
||||||
|
--metadata-path "output/metadata.json" \
|
||||||
|
--onnx-sha256 "${{ steps.meta.outputs.onnx_sha256 }}" \
|
||||||
|
--tinygrad-ref "${{ steps.meta.outputs.tinygrad_ref }}"
|
||||||
@@ -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=""
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP):
|
|||||||
|
|
||||||
max_idx = self._get_path_length_idx(path_x_array, max_distance)
|
max_idx = self._get_path_length_idx(path_x_array, max_distance)
|
||||||
self._path.projected_points = self._map_line_to_polygon(
|
self._path.projected_points = self._map_line_to_polygon(
|
||||||
self._path.raw_points, 0.9, self._path_offset_z, max_idx, max_distance, allow_invert=False
|
self._path.raw_points, self._get_path_half_width(), self._path_offset_z, max_idx, max_distance, allow_invert=False
|
||||||
)
|
)
|
||||||
|
|
||||||
self._update_experimental_gradient()
|
self._update_experimental_gradient()
|
||||||
@@ -292,7 +292,7 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP):
|
|||||||
allow_throttle = sm['longitudinalPlan'].allowThrottle or not self._longitudinal_control
|
allow_throttle = sm['longitudinalPlan'].allowThrottle or not self._longitudinal_control
|
||||||
self._blend_filter.update(int(allow_throttle))
|
self._blend_filter.update(int(allow_throttle))
|
||||||
|
|
||||||
if ui_state.rainbow_path:
|
if ui_state.rainbow_path and self._lateral_active:
|
||||||
self.rainbow_path.draw_rainbow_path(self._rect, self._path)
|
self.rainbow_path.draw_rainbow_path(self._rect, self._path)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import time
|
|||||||
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 get_default_model
|
||||||
from openpilot.common.constants import CV
|
from openpilot.common.constants import CV
|
||||||
from openpilot.selfdrive.ui.ui_state import device, ui_state
|
from openpilot.selfdrive.ui.ui_state import device, ui_state
|
||||||
from openpilot.system.ui.lib.multilang import tr
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
@@ -211,7 +211,8 @@ class ModelsLayout(Widget):
|
|||||||
for bundle in bundles:
|
for bundle in bundles:
|
||||||
folders.setdefault(next((ov_ride.value for ov_ride in bundle.overrides if ov_ride.key == "folder"), ""), []).append(bundle)
|
folders.setdefault(next((ov_ride.value for ov_ride in bundle.overrides if ov_ride.key == "folder"), ""), []).append(bundle)
|
||||||
|
|
||||||
folders_list = [TreeFolder("", [TreeNode("Default", {'display_name': f"{DEFAULT_MODEL} (Default)", 'short_name': "Default"})])]
|
folders_list = [TreeFolder("", [TreeNode("Default", {'display_name': f"{get_default_model()} (Default)",
|
||||||
|
'short_name': "Default"})])]
|
||||||
for folder, folder_bundles in sorted(folders.items(), key=lambda x: max((bundle.index for bundle in x[1]), default=-1), reverse=True):
|
for folder, folder_bundles in sorted(folders.items(), key=lambda x: max((bundle.index for bundle in x[1]), default=-1), reverse=True):
|
||||||
folder_bundles.sort(key=lambda bundle: bundle.index, reverse=True)
|
folder_bundles.sort(key=lambda bundle: bundle.index, reverse=True)
|
||||||
name = folder + (f" - (Updated: {m.group(1)})" if folder_bundles and (m := re.search(r'\(([^)]*)\)[^(]*$', folder_bundles[0].displayName)) else "")
|
name = folder + (f" - (Updated: {m.group(1)})" if folder_bundles and (m := re.search(r'\(([^)]*)\)[^(]*$', folder_bundles[0].displayName)) else "")
|
||||||
@@ -249,7 +250,8 @@ class ModelsLayout(Widget):
|
|||||||
self._update_lagd_description(live_delay)
|
self._update_lagd_description(live_delay)
|
||||||
self.model_manager = ui_state.sm["modelManagerSP"]
|
self.model_manager = ui_state.sm["modelManagerSP"]
|
||||||
self._handle_bundle_download_progress()
|
self._handle_bundle_download_progress()
|
||||||
active_name = self.model_manager.activeBundle.displayName if self.model_manager and self.model_manager.activeBundle.ref else f"{DEFAULT_MODEL} (Default)"
|
default_label = f"{get_default_model()} (Default)"
|
||||||
|
active_name = self.model_manager.activeBundle.displayName if self.model_manager and self.model_manager.activeBundle.ref else default_label
|
||||||
self.current_model_item.action_item.set_value(active_name)
|
self.current_model_item.action_item.set_value(active_name)
|
||||||
|
|
||||||
if not ui_state.is_offroad():
|
if not ui_state.is_offroad():
|
||||||
|
|||||||
@@ -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 get_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__()
|
||||||
@@ -47,7 +27,7 @@ class CurrentModelInfo(Widget):
|
|||||||
subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65))
|
subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65))
|
||||||
max_width = int(self._rect.width - 20)
|
max_width = int(self._rect.width - 20)
|
||||||
self.current_model_header = UnifiedLabel(tr("active model"), 48, max_width=max_width, text_color=header_color, font_weight=FontWeight.DISPLAY)
|
self.current_model_header = UnifiedLabel(tr("active model"), 48, max_width=max_width, text_color=header_color, font_weight=FontWeight.DISPLAY)
|
||||||
default_text = f"{DEFAULT_MODEL} (Default)".lower()
|
default_text = f"{get_default_model()} (Default)".lower()
|
||||||
self.current_model_text = UnifiedLabel(default_text, 32, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN, scroll=True)
|
self.current_model_text = UnifiedLabel(default_text, 32, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN, scroll=True)
|
||||||
|
|
||||||
self.info_header = UnifiedLabel("cache size", 48, max_width=max_width, text_color=header_color, font_weight=FontWeight.DISPLAY)
|
self.info_header = UnifiedLabel("cache size", 48, max_width=max_width, text_color=header_color, font_weight=FontWeight.DISPLAY)
|
||||||
@@ -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"{get_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
|
||||||
@@ -184,7 +162,8 @@ class ModelsLayoutMici(NavScroller):
|
|||||||
self._was_downloading = is_downloading
|
self._was_downloading = is_downloading
|
||||||
|
|
||||||
self.current_model_info.current_model_header.set_text(tr("active model"))
|
self.current_model_info.current_model_header.set_text(tr("active model"))
|
||||||
model_text = manager.activeBundle.displayName.lower() if manager.activeBundle.ref else f"{DEFAULT_MODEL} (Default)".lower()
|
default_model_text = f"{get_default_model()} (Default)".lower()
|
||||||
|
model_text = manager.activeBundle.displayName.lower() if manager.activeBundle.ref else default_model_text
|
||||||
self.current_model_info.current_model_text.set_text(model_text)
|
self.current_model_info.current_model_text.set_text(model_text)
|
||||||
self.current_model_info.info_header.set_text(tr("cache size"))
|
self.current_model_info.info_header.set_text(tr("cache size"))
|
||||||
self.current_model_info.info_text.set_text(f"{ModelsLayout.calculate_cache_size():.2f} MB")
|
self.current_model_info.info_text.set_text(f"{ModelsLayout.calculate_cache_size():.2f} MB")
|
||||||
|
|||||||
@@ -4,11 +4,23 @@ 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 openpilot.common.filter_simple import FirstOrderFilter
|
||||||
|
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||||
from openpilot.selfdrive.ui.sunnypilot.onroad.chevron_metrics import ChevronMetrics
|
from openpilot.selfdrive.ui.sunnypilot.onroad.chevron_metrics import ChevronMetrics
|
||||||
from openpilot.selfdrive.ui.sunnypilot.onroad.rainbow_path import RainbowPath
|
from openpilot.selfdrive.ui.sunnypilot.onroad.rainbow_path import RainbowPath
|
||||||
|
from openpilot.system.ui.lib.application import gui_app
|
||||||
|
|
||||||
|
|
||||||
class ModelRendererSP:
|
class ModelRendererSP:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.rainbow_path = RainbowPath()
|
self.rainbow_path = RainbowPath()
|
||||||
self.chevron_metrics = ChevronMetrics()
|
self.chevron_metrics = ChevronMetrics()
|
||||||
|
self._width_filter = FirstOrderFilter(0.9, 0.1, 1 / gui_app.target_fps)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _lateral_active(self) -> bool:
|
||||||
|
return ui_state.status in (UIStatus.ENGAGED, UIStatus.LAT_ONLY)
|
||||||
|
|
||||||
|
def _get_path_half_width(self) -> float:
|
||||||
|
target = 0.9 if self._lateral_active else 0.40
|
||||||
|
return self._width_filter.update(target)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ See the LICENSE.md file in the root directory for more details.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import math
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
@@ -66,14 +67,15 @@ def get_policy_npy_shapes(input_shapes: dict, is_supercombo: bool = False) -> tu
|
|||||||
if desire_key:
|
if desire_key:
|
||||||
shapes['desire'] = (input_shapes[desire_key][2],)
|
shapes['desire'] = (input_shapes[desire_key][2],)
|
||||||
|
|
||||||
if is_supercombo and 'features_buffer' in input_shapes:
|
|
||||||
fb = input_shapes['features_buffer']
|
|
||||||
shapes['prev_feat'] = (fb[0], fb[2])
|
|
||||||
|
|
||||||
for key, shape in input_shapes.items():
|
for key, shape in input_shapes.items():
|
||||||
if key not in (desire_key, 'features_buffer') and 'img' not in key:
|
if key not in (desire_key, 'features_buffer') and 'img' not in key:
|
||||||
shapes[key] = tuple(shape)
|
shapes[key] = tuple(shape)
|
||||||
|
|
||||||
|
if is_supercombo and 'features_buffer' in input_shapes:
|
||||||
|
fb = input_shapes['features_buffer']
|
||||||
|
feat_dim = math.prod(fb[2:])
|
||||||
|
shapes['prev_feat'] = (fb[0], feat_dim)
|
||||||
|
|
||||||
sizes = [int(np.prod(size)) for size in shapes.values()]
|
sizes = [int(np.prod(size)) for size in shapes.values()]
|
||||||
return shapes, sizes
|
return shapes, sizes
|
||||||
|
|
||||||
@@ -117,8 +119,9 @@ def generate_queues_and_npy(input_shapes: dict, frame_skip: int, device: str = D
|
|||||||
}
|
}
|
||||||
|
|
||||||
if features_buffer:
|
if features_buffer:
|
||||||
|
feat_dim = math.prod(features_buffer[2:])
|
||||||
feat_q_len = frame_skip * features_buffer[1] if is_supercombo else frame_skip * (features_buffer[1] - 1) + 1
|
feat_q_len = frame_skip * features_buffer[1] if is_supercombo else frame_skip * (features_buffer[1] - 1) + 1
|
||||||
queues['feat_q'] = Tensor(np.zeros((feat_q_len, features_buffer[0], features_buffer[2]),
|
queues['feat_q'] = Tensor(np.zeros((feat_q_len, features_buffer[0], feat_dim),
|
||||||
dtype=np.float32), device=device).contiguous().realize()
|
dtype=np.float32), device=device).contiguous().realize()
|
||||||
|
|
||||||
queues.update({key: Tensor(value, device='NPY').realize() for key, value in npy_arrays.items() if key in ('tfm', 'big_tfm')})
|
queues.update({key: Tensor(value, device='NPY').realize() for key, value in npy_arrays.items() if key in ('tfm', 'big_tfm')})
|
||||||
@@ -183,14 +186,14 @@ def make_run_policy(vision_runner, policy_runners: list, features_slice: slice,
|
|||||||
warped_dev = warped.to(Device.DEFAULT)
|
warped_dev = warped.to(Device.DEFAULT)
|
||||||
Tensor.realize(packed_npy_inputs_dev, warped_dev)
|
Tensor.realize(packed_npy_inputs_dev, warped_dev)
|
||||||
|
|
||||||
img = shift_and_sample(img_q, warped_dev[0:1], sample_skip_fn).realize()
|
img = shift_and_sample(img_q, warped_dev[0:1], sample_skip_fn)
|
||||||
big_img = shift_and_sample(big_img_q, warped_dev[1:2], sample_skip_fn).realize()
|
big_img = shift_and_sample(big_img_q, warped_dev[1:2], sample_skip_fn)
|
||||||
|
|
||||||
unpacked_tensors = [tensor.reshape(shape) for tensor, shape in zip(packed_npy_inputs_dev.split(npy_sizes), npy_shapes.values(), strict=True)]
|
unpacked_tensors = [tensor.reshape(shape) for tensor, shape in zip(packed_npy_inputs_dev.split(npy_sizes), npy_shapes.values(), strict=True)]
|
||||||
unpacked_dict = dict(zip(npy_shapes.keys(), unpacked_tensors, strict=True))
|
unpacked_dict = dict(zip(npy_shapes.keys(), unpacked_tensors, strict=True))
|
||||||
|
|
||||||
desire_dev = unpacked_dict['desire']
|
desire_dev = unpacked_dict['desire']
|
||||||
desire_buf = shift_and_sample(desire_q, desire_dev.reshape(1, 1, -1), sample_desire_fn).realize()
|
desire_buf = shift_and_sample(desire_q, desire_dev.reshape(1, 1, -1), sample_desire_fn)
|
||||||
|
|
||||||
inputs = {desire_key: desire_buf}
|
inputs = {desire_key: desire_buf}
|
||||||
for key, tensor_val in unpacked_dict.items():
|
for key, tensor_val in unpacked_dict.items():
|
||||||
@@ -199,19 +202,22 @@ def make_run_policy(vision_runner, policy_runners: list, features_slice: slice,
|
|||||||
|
|
||||||
if 'prev_feat' in unpacked_dict:
|
if 'prev_feat' in unpacked_dict:
|
||||||
prev_feat_dev = unpacked_dict['prev_feat']
|
prev_feat_dev = unpacked_dict['prev_feat']
|
||||||
inputs['features_buffer'] = shift_and_sample(feat_q, prev_feat_dev.reshape(1, 1, -1), sample_skip_fn).realize()
|
feat_buf = shift_and_sample(feat_q, prev_feat_dev.reshape(1, 1, -1), sample_skip_fn)
|
||||||
|
inputs['features_buffer'] = feat_buf if len(fb := input_shapes['features_buffer']) <= 3 else feat_buf.reshape(fb)
|
||||||
|
|
||||||
if vision_runner:
|
if vision_runner:
|
||||||
vision_out_cast = next(iter(vision_runner({road_key: img, wide_key: big_img}).values())).cast('float32').realize()
|
vision_out_cast = next(iter(vision_runner({road_key: img, wide_key: big_img}).values())).cast('float32').realize()
|
||||||
if 'features_buffer' not in inputs:
|
if 'features_buffer' not in inputs:
|
||||||
new_feat = vision_out_cast[:, features_slice].reshape(1, -1).unsqueeze(0)
|
new_feat = vision_out_cast[:, features_slice].reshape(1, -1).unsqueeze(0)
|
||||||
inputs['features_buffer'] = shift_and_sample(feat_q, new_feat, sample_skip_fn).realize()
|
feat_buf = shift_and_sample(feat_q, new_feat, sample_skip_fn).realize()
|
||||||
|
inputs['features_buffer'] = feat_buf if len(fb := input_shapes['features_buffer']) <= 3 else feat_buf.reshape(fb)
|
||||||
policy_outs = [next(iter(pol_runner(inputs).values())).cast('float32').realize() for pol_runner in policy_runners]
|
policy_outs = [next(iter(pol_runner(inputs).values())).cast('float32').realize() for pol_runner in policy_runners]
|
||||||
return (vision_out_cast, *policy_outs) if len(policy_outs) > 1 else (vision_out_cast, policy_outs[0])
|
return (vision_out_cast, *policy_outs) if len(policy_outs) > 1 else (vision_out_cast, policy_outs[0])
|
||||||
|
|
||||||
inputs.update({road_key: img, wide_key: big_img})
|
inputs.update({road_key: img, wide_key: big_img})
|
||||||
if 'features_buffer' not in inputs:
|
if 'features_buffer' not in inputs:
|
||||||
inputs['features_buffer'] = sample_skip_fn(feat_q)
|
feat_buf = sample_skip_fn(feat_q)
|
||||||
|
inputs['features_buffer'] = feat_buf if len(fb := input_shapes['features_buffer']) <= 3 else feat_buf.reshape(fb)
|
||||||
|
|
||||||
policy_out = next(iter(policy_runners[0](inputs).values())).cast('float32').realize()
|
policy_out = next(iter(policy_runners[0](inputs).values())).cast('float32').realize()
|
||||||
if 'features_buffer' not in inputs and features_slice is not None:
|
if 'features_buffer' not in inputs and features_slice is not None:
|
||||||
@@ -272,18 +278,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 +332,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,115 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
class Test4DFeaturesBuffer(OpenpilotTestCase):
|
||||||
|
def test_get_policy_npy_shapes_4d(self):
|
||||||
|
from openpilot.sunnypilot.modeld_v2.compile_modeld import get_policy_npy_shapes
|
||||||
|
input_shapes = {
|
||||||
|
'desire_pulse': (1, 25, 8),
|
||||||
|
'features_buffer': (1, 24, 32, 512), # compare 4d to 3d for regression
|
||||||
|
'traffic_convention': (1, 2),
|
||||||
|
'action_t': (1, 2)
|
||||||
|
}
|
||||||
|
shapes, sizes = get_policy_npy_shapes(input_shapes, is_supercombo=True)
|
||||||
|
assert shapes['prev_feat'] == (1, 16384)
|
||||||
|
assert sizes == [8, 2, 2, 16384]
|
||||||
|
|
||||||
|
def test_get_policy_npy_shapes_3d(self):
|
||||||
|
from openpilot.sunnypilot.modeld_v2.compile_modeld import get_policy_npy_shapes
|
||||||
|
input_shapes = {
|
||||||
|
'desire_pulse': (1, 25, 8),
|
||||||
|
'features_buffer': (1, 24, 512),
|
||||||
|
'traffic_convention': (1, 2),
|
||||||
|
'action_t': (1, 2)
|
||||||
|
}
|
||||||
|
shapes, sizes = get_policy_npy_shapes(input_shapes, is_supercombo=True)
|
||||||
|
assert shapes['prev_feat'] == (1, 512)
|
||||||
|
assert sizes == [8, 2, 2, 512]
|
||||||
|
|
||||||
|
|
||||||
|
class TestStockCompileModeldEquivalence(OpenpilotTestCase):
|
||||||
|
def test_get_policy_npy_shapes_matches_stock(self):
|
||||||
|
from openpilot.selfdrive.modeld.compile_modeld import get_policy_npy_shapes as stock_get_policy_npy_shapes
|
||||||
|
from openpilot.sunnypilot.modeld_v2.compile_modeld import get_policy_npy_shapes as sunny_get_policy_npy_shapes
|
||||||
|
|
||||||
|
stock_input_shapes = {
|
||||||
|
'desire_pulse': (1, 25, 8),
|
||||||
|
'features_buffer': (1, 24, 512), # see below comment
|
||||||
|
'traffic_convention': (1, 2),
|
||||||
|
'action_t': (1, 2),
|
||||||
|
}
|
||||||
|
|
||||||
|
stock_shapes, stock_sizes = stock_get_policy_npy_shapes(stock_input_shapes)
|
||||||
|
sunny_shapes, sunny_sizes = sunny_get_policy_npy_shapes(stock_input_shapes, is_supercombo=True)
|
||||||
|
|
||||||
|
assert sunny_shapes == stock_shapes
|
||||||
|
assert sunny_sizes == stock_sizes
|
||||||
|
assert sunny_shapes['prev_feat'] == (1, 512)
|
||||||
|
|
||||||
|
def test_make_input_queues_full_stock_equivalence(self):
|
||||||
|
from openpilot.selfdrive.modeld.compile_modeld import make_input_queues as stock_make_input_queues
|
||||||
|
from openpilot.sunnypilot.modeld_v2.compile_modeld import make_supercombo_input_queues as sunny_make_supercombo_input_queues
|
||||||
|
input_shapes = {
|
||||||
|
'img': (1, 12, 128, 256),
|
||||||
|
'desire_pulse': (1, 25, 8),
|
||||||
|
'features_buffer': (1, 24, 512), # when https://github.com/commaai/openpilot/pull/38681 merges, update to 1,24,32,512
|
||||||
|
'traffic_convention': (1, 2),
|
||||||
|
'action_t': (1, 2),
|
||||||
|
}
|
||||||
|
frame_skip = 4
|
||||||
|
|
||||||
|
stock_queues, stock_npy = stock_make_input_queues(input_shapes, frame_skip, device='NPY')
|
||||||
|
sunny_queues, sunny_npy = sunny_make_supercombo_input_queues(input_shapes, frame_skip, device='NPY')
|
||||||
|
assert set(sunny_queues.keys()) == set(stock_queues.keys())
|
||||||
|
for key in stock_queues:
|
||||||
|
assert sunny_queues[key].shape == stock_queues[key].shape, \
|
||||||
|
f"Queue shape mismatch for {key}: sunny {sunny_queues[key].shape} != stock {stock_queues[key].shape}"
|
||||||
|
assert set(sunny_npy.keys()) == set(stock_npy.keys())
|
||||||
|
for key in stock_npy:
|
||||||
|
assert sunny_npy[key].shape == stock_npy[key].shape, \
|
||||||
|
f"Numpy array shape mismatch for {key}: sunny {sunny_npy[key].shape} != stock {stock_npy[key].shape}"
|
||||||
|
|
||||||
|
def test_make_warp_queues_stock_equivalence(self):
|
||||||
|
from openpilot.selfdrive.modeld.compile_modeld import make_warp_input_queues as stock_make_warp_queues
|
||||||
|
from openpilot.sunnypilot.modeld_v2.compile_modeld import make_warp_queues as sunny_make_warp_queues
|
||||||
|
stock_vision_shapes = {'img': (1, 12, 128, 256)} # for now?
|
||||||
|
stock_queues, stock_npy = stock_make_warp_queues(stock_vision_shapes, frame_skip=4, device='NPY')
|
||||||
|
sunny_queues, sunny_npy = sunny_make_warp_queues(device='NPY')
|
||||||
|
|
||||||
|
assert set(sunny_npy.keys()) == set(stock_npy.keys()) == {'tfm', 'big_tfm'}
|
||||||
|
for key in sunny_npy:
|
||||||
|
assert sunny_npy[key].shape == stock_npy[key].shape == (3, 3)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,17 @@ import os
|
|||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
from openpilot.common.basedir import BASEDIR
|
from openpilot.common.basedir import BASEDIR
|
||||||
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
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.sunnypilot.models.model_name import DEFAULT_MODEL, DEFAULT_BIG_MODEL
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_model() -> str:
|
||||||
|
show_big_model = (ui_state.usbgpu and ui_state.usbgpu_compiled
|
||||||
|
and (ui_state.usbgpu_active or ui_state.usbgpu_loading or ui_state.is_offroad()))
|
||||||
|
|
||||||
|
return DEFAULT_BIG_MODEL if show_big_model 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 +22,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 +30,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()
|
||||||
|
|||||||
@@ -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_v22.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:
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
See the LICENSE.md file in the root directory for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from huggingface_hub import HfApi, hf_hub_download
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--hf-repo", required=True)
|
||||||
|
parser.add_argument("--hf-defaults-path", required=True)
|
||||||
|
parser.add_argument("--artifact-name", required=True)
|
||||||
|
parser.add_argument("--metadata-path", required=True)
|
||||||
|
parser.add_argument("--onnx-sha256", required=True)
|
||||||
|
parser.add_argument("--tinygrad-ref", required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
with open(args.metadata_path) as f:
|
||||||
|
metadata = json.load(f)
|
||||||
|
|
||||||
|
bundle = metadata['bundles'][0]
|
||||||
|
bundle['onnx_sha256'] = args.onnx_sha256
|
||||||
|
|
||||||
|
artifact = bundle['models'][0]['artifact']
|
||||||
|
hf_base = f"https://huggingface.co/datasets/{args.hf_repo}/resolve/main/{args.hf_defaults_path}/{args.artifact_name}"
|
||||||
|
artifact['download_uri']['url'] = f"{hf_base}/{artifact['file_name']}"
|
||||||
|
for chunk in artifact.get('chunks', []):
|
||||||
|
chunk['url'] = f"{hf_base}/{chunk['file_name']}"
|
||||||
|
|
||||||
|
json_filename = f"{args.hf_defaults_path}/default_models.json"
|
||||||
|
try:
|
||||||
|
local_path = hf_hub_download(repo_id=args.hf_repo, repo_type='dataset', filename=json_filename)
|
||||||
|
with open(local_path) as f:
|
||||||
|
defaults_json = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
defaults_json = {"tinygrad_ref": args.tinygrad_ref, "bundles": []}
|
||||||
|
|
||||||
|
defaults_json['tinygrad_ref'] = args.tinygrad_ref
|
||||||
|
|
||||||
|
existing_idx = next((i for i, b in enumerate(defaults_json['bundles'])
|
||||||
|
if b.get('display_name') == bundle.get('display_name')), None)
|
||||||
|
if existing_idx is not None:
|
||||||
|
defaults_json['bundles'][existing_idx] = bundle
|
||||||
|
else:
|
||||||
|
defaults_json['bundles'].append(bundle)
|
||||||
|
|
||||||
|
print(json.dumps(defaults_json, indent=2))
|
||||||
|
|
||||||
|
api = HfApi()
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||||
|
json.dump(defaults_json, f, indent=2)
|
||||||
|
tmp_path = f.name
|
||||||
|
|
||||||
|
api.upload_file(
|
||||||
|
path_or_fileobj=tmp_path,
|
||||||
|
path_in_repo=json_filename,
|
||||||
|
repo_id=args.hf_repo,
|
||||||
|
repo_type="dataset",
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Updated {json_filename}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user