Compare commits

..

6 Commits

Author SHA1 Message Date
github-actions[bot] 8cb737c241 modeld_v2: spatial features (PR-1934) 2026-08-23 08:12:05 +00:00
Jason Wen 211f990f6b models: fix sunnylink default model display and false big model re-downloading (#1941)
* big needs small

* no download

* actually

* send it
2026-08-23 04:04:46 -04:00
Jason Wen 97468e4fa4 [TIZI/TICI] ui: remove calibration reset dialog on model change (#1942) 2026-08-23 03:48:52 -04:00
Jason Wen 6c6fba9a14 ci: fix flaky LLK test (#1940) 2026-08-23 02:57:02 -04:00
Jason Wen 34621cf816 ci: refactor big model chunk handling (#1939) 2026-08-23 02:48:10 -04:00
Jason Wen 086530b7c6 [TIZI/TICI] ui: fix path width during gas and steering override (#1938) 2026-08-22 21:47:38 -04:00
16 changed files with 163 additions and 134 deletions
@@ -211,6 +211,8 @@ jobs:
needs: [ prepare_strategy ] needs: [ prepare_strategy ]
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
if: ${{ needs.prepare_strategy.outputs.include_big_model == 'true' }} if: ${{ needs.prepare_strategy.outputs.include_big_model == 'true' }}
outputs:
onnx_sha256: ${{ steps.resolve.outputs.onnx_sha256 }}
env: env:
HF_REPO: sunnypilot/sunnypilot_models_v1 HF_REPO: sunnypilot/sunnypilot_models_v1
HF_DEFAULTS_PATH: models/defaults/big HF_DEFAULTS_PATH: models/defaults/big
@@ -226,6 +228,7 @@ jobs:
run: | run: |
ACTUAL_ONNX_HASH=$(sha256sum "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx" | cut -d' ' -f1) ACTUAL_ONNX_HASH=$(sha256sum "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx" | cut -d' ' -f1)
echo "Repo ONNX hash: $ACTUAL_ONNX_HASH" echo "Repo ONNX hash: $ACTUAL_ONNX_HASH"
echo "onnx_sha256=$ACTUAL_ONNX_HASH" >> $GITHUB_OUTPUT
JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json" JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json"
@@ -267,36 +270,6 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Download big model chunks
run: |
ACTUAL_ONNX_HASH=$(sha256sum "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx" | cut -d' ' -f1)
JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json"
DEFAULTS=$(curl -fsSL "$JSON_URL")
BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ACTUAL_ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)')
mkdir -p big_model_chunks
ARTIFACT=$(echo "$BUNDLE" | jq -r '.models[0].artifact')
BASE_URL=$(echo "$ARTIFACT" | jq -r '.download_uri.url' | sed 's|/[^/]*$||')
NUM_CHUNKS=$(echo "$ARTIFACT" | jq -r '.chunks | length')
CANONICAL="big_driving_tinygrad.pkl"
echo "$ARTIFACT" | jq -r '.chunks[].file_name' | while read CHUNK_NAME; do
CHUNK_IDX=$(echo "$CHUNK_NAME" | grep -oP 'chunk\K[0-9]+of[0-9]+')
CANONICAL_CHUNK="${CANONICAL}.chunk${CHUNK_IDX}"
ENCODED_URL=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${BASE_URL}/${CHUNK_NAME}', safe=':/'))")
echo "Downloading $CHUNK_NAME -> $CANONICAL_CHUNK"
curl -fsSL -o "big_model_chunks/${CANONICAL_CHUNK}" "$ENCODED_URL"
done
echo "$NUM_CHUNKS" > "big_model_chunks/${CANONICAL}.chunkmanifest"
- name: Upload big model chunks
uses: actions/upload-artifact@v4
with:
name: big-model-chunks
path: big_model_chunks/
compression-level: 0
- name: Cancel run on failure - name: Cancel run on failure
if: failure() if: failure()
run: gh run cancel ${{ github.run_id }} run: gh run cancel ${{ github.run_id }}
@@ -339,12 +312,32 @@ jobs:
mkdir -p "${{ github.workspace }}/chestnut_output" mkdir -p "${{ github.workspace }}/chestnut_output"
tar xzf prebuilt.tar.gz -C "${{ github.workspace }}/chestnut_output" tar xzf prebuilt.tar.gz -C "${{ github.workspace }}/chestnut_output"
- name: Download big model chunks - name: Download big model chunks from HF
if: ${{ needs.prepare_chestnut.result == 'success' }} if: ${{ needs.prepare_chestnut.result == 'success' }}
uses: actions/download-artifact@v4 env:
with: HF_REPO: sunnypilot/sunnypilot_models_v1
name: big-model-chunks HF_DEFAULTS_PATH: models/defaults/big
path: big_model_chunks run: |
ONNX_HASH="${{ needs.prepare_chestnut.outputs.onnx_sha256 }}"
JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json"
DEFAULTS=$(curl -fsSL "$JSON_URL")
BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)')
mkdir -p big_model_chunks
ARTIFACT=$(echo "$BUNDLE" | jq -r '.models[0].artifact')
BASE_URL=$(echo "$ARTIFACT" | jq -r '.download_uri.url' | sed 's|/[^/]*$||')
NUM_CHUNKS=$(echo "$ARTIFACT" | jq -r '.chunks | length')
CANONICAL="big_driving_tinygrad.pkl"
echo "$ARTIFACT" | jq -r '.chunks[].file_name' | while read CHUNK_NAME; do
CHUNK_IDX=$(echo "$CHUNK_NAME" | grep -oP 'chunk\K[0-9]+of[0-9]+')
CANONICAL_CHUNK="${CANONICAL}.chunk${CHUNK_IDX}"
ENCODED_URL=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${BASE_URL}/${CHUNK_NAME}', safe=':/'))")
echo "Downloading $CHUNK_NAME -> $CANONICAL_CHUNK"
curl -fsSL -o "big_model_chunks/${CANONICAL_CHUNK}" "$ENCODED_URL"
done
echo "$NUM_CHUNKS" > "big_model_chunks/${CANONICAL}.chunkmanifest"
- name: Inject big model into chestnut - name: Inject big model into chestnut
if: ${{ needs.prepare_chestnut.result == 'success' }} if: ${{ needs.prepare_chestnut.result == 'success' }}
-1
View File
@@ -132,7 +132,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}}, {"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}},
{"UsbGpuActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}}, {"UsbGpuActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
{"UsbGpuLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}}, {"UsbGpuLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
{"UsbGpuLoadProgress", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, INT, "0"}},
{"Version", {PERSISTENT, STRING}}, {"Version", {PERSISTENT, STRING}},
// --- sunnypilot params --- // // --- sunnypilot params --- //
@@ -1,44 +0,0 @@
import os
from openpilot.common.file_chunker import open_file_chunked, get_existing_chunks
from openpilot.common.params import Params
PARAM = "UsbGpuLoadProgress"
class ProgressReader:
def __init__(self, inner, total):
self._inner = inner
self._total = total
self._params = Params()
self._read = 0
self._pct = -1
self._step = max(64 * 1024, total // 100)
def _bump(self, n):
self._read += n
if self._total:
pct = min(100, self._read * 100 // self._total)
if pct != self._pct:
self._pct = pct
self._params.put(PARAM, pct)
def read(self, size=-1):
data = self._inner.read(size)
self._bump(len(data))
return data
def readinto(self, b):
view = memoryview(b)
done = 0
while done < len(view):
n = self._inner.readinto(view[done:done + self._step])
if not n:
break
done += n
self._bump(n)
return done
def open_with_progress(pkl_path):
total = sum(os.path.getsize(p) for p in get_existing_chunks(pkl_path))
return ProgressReader(open_file_chunked(pkl_path), total)
+1 -2
View File
@@ -29,7 +29,6 @@ from openpilot.selfdrive.modeld.parse_model_outputs import Parser
from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_INPUTS, POLICY_INPUTS from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_INPUTS, POLICY_INPUTS
from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState
from openpilot.common.file_chunker import open_file_chunked from openpilot.common.file_chunker import open_file_chunked
from openpilot.selfdrive.modeld.load_progress import open_with_progress
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
from openpilot.selfdrive.modeld.helpers import usbgpu_present, usbgpu_compiled, modeld_pkl_path, get_tg_input_devices, load_oob from openpilot.selfdrive.modeld.helpers import usbgpu_present, usbgpu_compiled, modeld_pkl_path, get_tg_input_devices, load_oob
@@ -146,7 +145,7 @@ class ModelState(ModelStateBase):
ModelStateBase.__init__(self) ModelStateBase.__init__(self)
input_devices = get_tg_input_devices(PROCESS_NAME, usbgpu) input_devices = get_tg_input_devices(PROCESS_NAME, usbgpu)
self.WARP_DEV, self.QUEUE_DEV = input_devices['WARP_DEV'], input_devices['QUEUE_DEV'] self.WARP_DEV, self.QUEUE_DEV = input_devices['WARP_DEV'], input_devices['QUEUE_DEV']
jits = load_oob(open_with_progress(modeld_pkl_path(usbgpu)) if usbgpu else open_file_chunked(modeld_pkl_path(usbgpu))) jits = load_oob(open_file_chunked(modeld_pkl_path(usbgpu)))
metadata = jits['metadata'] metadata = jits['metadata']
self.input_shapes = metadata['input_shapes'] self.input_shapes = metadata['input_shapes']
self.vision_input_names = [k for k in self.input_shapes if 'img' in k] self.vision_input_names = [k for k in self.input_shapes if 'img' in k]
@@ -223,7 +223,7 @@ class HudRenderer(Widget):
if icon is not self._egpu_icon: if icon is not self._egpu_icon:
self._egpu_fade_time = rl.get_time() self._egpu_fade_time = rl.get_time()
self._egpu_icon = icon self._egpu_icon = icon
alpha = self._egpu_alpha_filter.update(True) alpha = self._egpu_alpha_filter.update(loading or 0 < rl.get_time() - self._egpu_fade_time < SET_SPEED_PERSISTENCE)
if alpha < 1e-2: if alpha < 1e-2:
return return
@@ -231,18 +231,6 @@ class HudRenderer(Widget):
rect.y + rect.height - 14 - (self._txt_wheel.height + icon.height) / 2) rect.y + rect.height - 14 - (self._txt_wheel.height + icon.height) / 2)
rl.draw_texture_ex(icon, pos, 0.0, 1.0, rl.Color(255, 255, 255, int(255 * opacity * alpha))) rl.draw_texture_ex(icon, pos, 0.0, 1.0, rl.Color(255, 255, 255, int(255 * opacity * alpha)))
if loading:
pct_text = f"{ui_state.usbgpu_load_progress}%"
size = FONT_SIZES.max_speed
cell = measure_text_cached(self._font_bold, "0", size)
widths = [cell.x if c.isdigit() else measure_text_cached(self._font_bold, c, size).x for c in pct_text]
x = pos.x - 8 - sum(widths)
y = pos.y + (icon.height - cell.y) / 2
for c, w in zip(pct_text, widths):
glyph = measure_text_cached(self._font_bold, c, size).x
rl.draw_text_ex(self._font_bold, c, rl.Vector2(x + (w - glyph) / 2, y), size, 0, rl.WHITE)
x += w
def _draw_steering_wheel(self, rect: rl.Rectangle) -> None: def _draw_steering_wheel(self, rect: rl.Rectangle) -> None:
wheel_txt = self._txt_wheel_critical if self._show_wheel_critical else self._txt_wheel wheel_txt = self._txt_wheel_critical if self._show_wheel_critical else self._txt_wheel
@@ -178,27 +178,14 @@ class ModelsLayout(Widget):
# circled_slash is authored grey; tinting it again only darkens it # circled_slash is authored grey; tinting it again only darkens it
return {"name": name, "text_color": rl.GRAY, "icon": "icons/circled_slash.png", "icon_color": rl.WHITE} return {"name": name, "text_color": rl.GRAY, "icon": "icons/circled_slash.png", "icon_color": rl.WHITE}
@staticmethod
def _show_reset_params_dialog():
def _callback(response):
if response == DialogResult.CONFIRM:
ui_state.params.remove("CalibrationParams")
ui_state.params.remove("LiveTorqueParameters")
msg = tr("Model download has started in the background. We suggest resetting calibration. Would you like to do that now?")
dialog = ConfirmDialog(msg, tr("Reset Calibration"), callback=_callback)
gui_app.push_widget(dialog)
def _on_model_selected(self, result): def _on_model_selected(self, result):
if result != DialogResult.CONFIRM: if result != DialogResult.CONFIRM:
return return
selected_ref = self.model_dialog.selection_ref selected_ref = self.model_dialog.selection_ref
if selected_ref == "Default": if selected_ref == "Default":
ui_state.params.remove("ModelManager_ActiveBundle") ui_state.params.remove("ModelManager_ActiveBundle")
self._show_reset_params_dialog()
elif selected_bundle := next((bundle for bundle in self.model_manager.availableBundles if bundle.ref == selected_ref), None): elif selected_bundle := next((bundle for bundle in self.model_manager.availableBundles if bundle.ref == selected_ref), None):
ui_state.params.put("ModelManager_DownloadIndex", selected_bundle.index) ui_state.params.put("ModelManager_DownloadIndex", selected_bundle.index)
if self.model_manager.activeBundle and selected_bundle.generation != self.model_manager.activeBundle.generation:
self._show_reset_params_dialog()
self.model_dialog = None self.model_dialog = None
@staticmethod @staticmethod
@@ -8,6 +8,7 @@ from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus 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.selfdrive.ui.sunnypilot.ui_state import MADSState
from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.application import gui_app
@@ -19,6 +20,11 @@ class ModelRendererSP:
@property @property
def _lateral_active(self) -> bool: def _lateral_active(self) -> bool:
sm = ui_state.sm
if sm.valid["selfdriveStateSP"]:
mads = sm["selfdriveStateSP"].mads
if mads.available:
return mads.enabled and mads.state != MADSState.paused
return ui_state.status in (UIStatus.ENGAGED, UIStatus.LAT_ONLY) return ui_state.status in (UIStatus.ENGAGED, UIStatus.LAT_ONLY)
def _get_path_half_width(self) -> float: def _get_path_half_width(self) -> float:
-4
View File
@@ -86,7 +86,6 @@ class UIState(UIStateSP):
self.usbgpu_compiled: bool = usbgpu_compiled() self.usbgpu_compiled: bool = usbgpu_compiled()
self.usbgpu_active: bool | None = self.params.get("UsbGpuActive") self.usbgpu_active: bool | None = self.params.get("UsbGpuActive")
self.usbgpu_loading: bool = self.params.get_bool("UsbGpuLoading") self.usbgpu_loading: bool = self.params.get_bool("UsbGpuLoading")
self.usbgpu_load_progress: int = self.params.get("UsbGpuLoadProgress", return_default=True)
self.started: bool = False self.started: bool = False
self.ignition: bool = False self.ignition: bool = False
self.recording_audio: bool = False self.recording_audio: bool = False
@@ -165,9 +164,6 @@ class UIState(UIStateSP):
# Update started state # Update started state
self.started = self.sm["deviceState"].started and self.ignition self.started = self.sm["deviceState"].started and self.ignition
if self.usbgpu_loading:
self.usbgpu_load_progress = self.params.get("UsbGpuLoadProgress", return_default=True)
# Update body state # Update body state
if self.CP is not None and self.is_body != self.CP.notCar: if self.CP is not None and self.is_body != self.CP.notCar:
self.is_body = self.CP.notCar self.is_body = self.CP.notCar
@@ -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:
+1 -2
View File
@@ -25,7 +25,6 @@ from opendbc.car.car_helpers import get_demo_car_params
from tinygrad.tensor import Tensor from tinygrad.tensor import Tensor
from openpilot.common.file_chunker import open_file_chunked from openpilot.common.file_chunker import open_file_chunked
from openpilot.selfdrive.modeld.load_progress import open_with_progress
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.filter_simple import FirstOrderFilter
@@ -108,7 +107,7 @@ class ModelState(ModelStateBase):
def _init_combined(self, pkl_path, cam_w, cam_h, bundle): def _init_combined(self, pkl_path, cam_w, cam_h, bundle):
cloudlog.warning(f"loading combined pkl: {pkl_path}") cloudlog.warning(f"loading combined pkl: {pkl_path}")
jits = load_oob(open_with_progress(pkl_path) if self.usbgpu else open_file_chunked(pkl_path)) jits = load_oob(open_file_chunked(pkl_path))
self.WARP_DEV = 'QCOM' if COMMA_HARDWARE else 'CPU' self.WARP_DEV = 'QCOM' if COMMA_HARDWARE else 'CPU'
self.DEV = 'AMD' if self.usbgpu else self.WARP_DEV self.DEV = 'AMD' if self.usbgpu else self.WARP_DEV
@@ -195,3 +195,85 @@ class TestReadFileChunkedToDisk(OpenpilotTestCase):
assert out.parent == Path(d) assert out.parent == Path(d)
assert out.read_bytes() == payload 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)
+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_v21.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
+12 -3
View File
@@ -143,13 +143,17 @@ class ModelManagerSP:
is_cached = False is_cached = False
if len(artifact.chunks) > 0: if len(artifact.chunks) > 0:
from openpilot.common.file_chunker import get_chunk_name from openpilot.common.file_chunker import get_chunk_name
num_chunks = len(artifact.chunks)
chunks_valid = True chunks_valid = True
for i, chunk in enumerate(artifact.chunks): for i, chunk in enumerate(artifact.chunks):
chunk_path = get_chunk_name(full_path, i, len(artifact.chunks)) chunk_path = get_chunk_name(full_path, i, num_chunks)
if not await verify_file(chunk_path, chunk.sha256): if not await verify_file(chunk_path, chunk.sha256):
chunks_valid = False chunks_valid = False
break break
if chunks_valid and len(artifact.chunks) > 0: artifact.downloadProgress.progress = ((i + 1) / num_chunks) * 100
self._sync_artifact_progress(artifact)
self._report_status()
if chunks_valid and num_chunks > 0:
is_cached = True is_cached = True
else: else:
if await verify_file(full_path, expected_hash): if await verify_file(full_path, expected_hash):
@@ -216,6 +220,9 @@ class ModelManagerSP:
"""Downloads all models in a bundle""" """Downloads all models in a bundle"""
self.selected_bundle = model_bundle self.selected_bundle = model_bundle
self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.downloading self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.downloading
for model in self.selected_bundle.models:
model.artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloading
self._report_status()
os.makedirs(destination_path, exist_ok=True) os.makedirs(destination_path, exist_ok=True)
try: try:
@@ -260,7 +267,9 @@ class ModelManagerSP:
self.active_bundle = get_active_bundle(self.params) self.active_bundle = get_active_bundle(self.params)
if (index_to_download := self.params.get("ModelManager_DownloadIndex")) is not None: if (index_to_download := self.params.get("ModelManager_DownloadIndex")) is not None:
if model_to_download := next((model for model in self.available_models if model.index == index_to_download), None): if self.active_bundle and self.active_bundle.index == index_to_download:
self.params.remove("ModelManager_DownloadIndex")
elif model_to_download := next((model for model in self.available_models if model.index == index_to_download), None):
try: try:
self.download(model_to_download, Paths.model_root()) self.download(model_to_download, Paths.model_root())
except Exception as e: except Exception as e:
@@ -83,9 +83,14 @@ class TestLocationdProc(OpenpilotTestCase):
self.pm.send(msg.which(), msg) self.pm.send(msg.which(), msg)
if msg.which() == "cameraOdometry": if msg.which() == "cameraOdometry":
self.pm.wait_for_readers_to_update(msg.which(), timeout=1, dt=0.005) self.pm.wait_for_readers_to_update(msg.which(), timeout=1, dt=0.005)
time.sleep(1) # wait for async params write for _ in range(50):
val = self.params.get('LastGPSPositionLLK')
if val is not None:
break
time.sleep(0.1)
lastGPS = json.loads(self.params.get('LastGPSPositionLLK')) self.assertIsNotNone(val, "LastGPSPositionLLK not written within 5s")
lastGPS = json.loads(val)
self.assertAlmostEqual(lastGPS['latitude'], self.lat, delta=0.001) self.assertAlmostEqual(lastGPS['latitude'], self.lat, delta=0.001)
self.assertAlmostEqual(lastGPS['longitude'], self.lon, delta=0.001) self.assertAlmostEqual(lastGPS['longitude'], self.lon, delta=0.001)
self.assertAlmostEqual(lastGPS['altitude'], self.alt, delta=0.001) self.assertAlmostEqual(lastGPS['altitude'], self.alt, delta=0.001)
@@ -28,7 +28,8 @@ 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 get_default_model from openpilot.selfdrive.modeld.helpers import usbgpu_present, usbgpu_compiled
from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL, DEFAULT_BIG_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 +182,10 @@ 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"] = get_default_model() # mirrors get_default_model() — ui_state unavailable in sunnylinkd process
show_big = (usbgpu_present() and usbgpu_compiled()
and (params.get_bool("UsbGpuActive") or params.get_bool("UsbGpuLoading") or params.get_bool("IsOffroad")))
schema["default_model"] = DEFAULT_BIG_MODEL if show_big else 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: