Compare commits

..

1 Commits

Author SHA1 Message Date
nayan d68a6cd87d zoom zoom 2026-08-28 21:11:29 -04:00
42 changed files with 409 additions and 382 deletions
@@ -8,13 +8,13 @@ on:
required: true required: true
type: string type: string
target_hardware: target_hardware:
description: 'Hardware target to compile for (qcom or chestnut)' description: 'Hardware target to compile for (qcom or usbgpu)'
required: true required: true
type: choice type: choice
default: 'qcom' default: 'qcom'
options: options:
- qcom - qcom
- chestnut - usbgpu
hf_repo: hf_repo:
description: 'Hugging Face dataset repository' description: 'Hugging Face dataset repository'
required: false required: false
@@ -59,7 +59,7 @@ jobs:
id: get-json id: get-json
run: | run: |
cd docs/docs cd docs/docs
PREFIX="driving_models_${{ inputs.target_hardware == 'chestnut' && 'chestnut_' || '' }}v" PREFIX="driving_models_${{ inputs.target_hardware == 'usbgpu' && 'usbgpu_' || '' }}v"
latest=$(ls ${PREFIX}*.json | sed -E "s/${PREFIX}([0-9]+)\.json/\1/" | sort -n | tail -1) latest=$(ls ${PREFIX}*.json | sed -E "s/${PREFIX}([0-9]+)\.json/\1/" | sort -n | tail -1)
next=$((latest+1)) next=$((latest+1))
json_file="${PREFIX}${next}.json" json_file="${PREFIX}${next}.json"
+1 -1
View File
@@ -176,7 +176,7 @@ jobs:
build_big_model: build_big_model:
needs: resolve needs: resolve
if: ${{ inputs.target == 'big' }} if: ${{ inputs.target == 'big' }}
runs-on: [self-hosted, chestnut] runs-on: [self-hosted, usbgpu]
env: env:
BIG_ONNX: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx BIG_ONNX: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
BIG_PKL: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl BIG_PKL: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl
@@ -30,7 +30,7 @@ on:
type: boolean type: boolean
default: true default: true
target_hardware: target_hardware:
description: 'Hardware target to compile for (qcom or chestnut)' description: 'Hardware target to compile for (qcom or usbgpu)'
required: false required: false
type: string type: string
default: 'qcom' default: 'qcom'
@@ -101,7 +101,7 @@ on:
default: 'qcom' default: 'qcom'
options: options:
- qcom - qcom
- chestnut - usbgpu
hf_repo: hf_repo:
description: 'Hugging Face dataset repository' description: 'Hugging Face dataset repository'
required: false required: false
@@ -109,7 +109,7 @@ on:
default: 'sunnypilot/sunnypilot_models_v1' default: 'sunnypilot/sunnypilot_models_v1'
env: env:
RECOMPILED_DIR: recompiled${{ inputs.recompiled_dir }} RECOMPILED_DIR: recompiled${{ inputs.recompiled_dir }}
JSON_FILE: docs/docs/driving_models_${{ inputs.target_hardware == 'chestnut' && 'chestnut_v' || 'v' }}${{ inputs.json_version }}.json JSON_FILE: docs/docs/driving_models_${{ inputs.target_hardware == 'usbgpu' && 'usbgpu_v' || 'v' }}${{ inputs.json_version }}.json
jobs: jobs:
build_model: build_model:
@@ -31,7 +31,7 @@ on:
type: string type: string
default: '' default: ''
target_hardware: target_hardware:
description: 'Hardware target to compile for (qcom or chestnut)' description: 'Hardware target to compile for (qcom or usbgpu)'
required: false required: false
type: string type: string
default: 'qcom' default: 'qcom'
@@ -57,7 +57,7 @@ on:
type: choice type: choice
options: options:
- qcom - qcom
- chestnut - usbgpu
default: 'qcom' default: 'qcom'
@@ -102,7 +102,7 @@ jobs:
cat $GITHUB_OUTPUT cat $GITHUB_OUTPUT
- run: | - run: |
cd ${{ github.workspace }}/openpilot/openpilot cd ${{ github.workspace }}/openpilot/openpilot
if [ "${{ inputs.target_hardware }}" != "chestnut" ]; then if [ "${{ inputs.target_hardware }}" != "usbgpu" ]; then
git lfs pull -X "**/selfdrive/modeld/models/big_*.onnx,**/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
@@ -121,7 +121,7 @@ jobs:
if-no-files-found: error if-no-files-found: error
build_model: build_model:
runs-on: [self-hosted, chestnut] 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 }})
@@ -185,9 +185,9 @@ jobs:
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" TG_FLAGS_QCOM="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
if [ "${{ inputs.target_hardware }}" == "chestnut" ]; then if [ "${{ inputs.target_hardware }}" == "usbgpu" ]; then
echo "CHESTNUT build" echo "USBGPU build"
export CHESTNUT=1 export USBGPU=1
TG_FLAGS="DEBUG=1 DEV=USB+AMD:LLVM WARP_DEV=QCOM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2" TG_FLAGS="DEBUG=1 DEV=USB+AMD:LLVM WARP_DEV=QCOM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2"
OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl" OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl"
else else
-1
View File
@@ -353,7 +353,6 @@ struct OnroadEventSP @0xda96579883444c35 {
speedLimitPending @22; speedLimitPending @22;
e2eChime @23; e2eChime @23;
laneChangeRoadEdge @24; laneChangeRoadEdge @24;
bigModelReady @25;
} }
} }
+5 -6
View File
@@ -130,8 +130,8 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"UpdaterLastFetchTime", {PERSISTENT, TIME}}, {"UpdaterLastFetchTime", {PERSISTENT, TIME}},
{"UptimeOffroad", {PERSISTENT, FLOAT, "0.0"}}, {"UptimeOffroad", {PERSISTENT, FLOAT, "0.0"}},
{"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}}, {"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}},
{"ChestnutActive", {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}},
{"ChestnutLoading", {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}},
{"Version", {PERSISTENT, STRING}}, {"Version", {PERSISTENT, STRING}},
// --- sunnypilot params --- // // --- sunnypilot params --- //
@@ -195,16 +195,15 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
// Model Manager params // Model Manager params
{"ModelManager_ActiveBundle", {PERSISTENT, JSON}}, {"ModelManager_ActiveBundle", {PERSISTENT, JSON}},
{"ModelManager_ActiveBundleUSBGPU", {PERSISTENT, JSON}}, //TODO-SP: kept for migration, remove on next sync? {"ModelManager_ActiveBundleUSBGPU", {PERSISTENT, JSON}},
{"ModelManager_ActiveBundleChestnut", {PERSISTENT, JSON}},
{"ModelManager_ActiveJson", {CLEAR_ON_MANAGER_START, JSON}}, {"ModelManager_ActiveJson", {CLEAR_ON_MANAGER_START, JSON}},
{"ModelManager_ClearCache", {CLEAR_ON_MANAGER_START, BOOL}}, {"ModelManager_ClearCache", {CLEAR_ON_MANAGER_START, BOOL}},
{"ModelManager_DownloadRef", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, STRING}}, {"ModelManager_DownloadRef", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, STRING}},
{"ModelManager_Favs", {PERSISTENT | BACKUP, STRING}}, {"ModelManager_Favs", {PERSISTENT | BACKUP, STRING}},
{"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}}, {"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
{"ModelManager_LastSyncTime_Chestnut", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}}, {"ModelManager_LastSyncTime_USBGPU", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
{"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}}, {"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}},
{"ModelManager_ModelsCache_Chestnut", {PERSISTENT | BACKUP, JSON}}, {"ModelManager_ModelsCache_USBGPU", {PERSISTENT | BACKUP, JSON}},
// Neural Network Lateral Control // Neural Network Lateral Control
{"NeuralNetworkLateralControl", {PERSISTENT | BACKUP, BOOL, "0"}}, {"NeuralNetworkLateralControl", {PERSISTENT | BACKUP, BOOL, "0"}},
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:845c40ff0d37612e8f2f482a36845744b5ae91ce2fcfc8117990d7d278b59820
size 13079
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8a8c5fece2a1c7587feb41cbe04c6aee08e768ecd9b5d00da6af9832a4ccc842
size 2034
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7409c53d7c72681c24982fd83b56ce70f80797c9c0f936d9296a5c18557ac472
size 7279
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:58bd6155433f623b1f75d134bd8ca4745d9aa71f6767eb807cdbcf7deb3089a1
size 10876
+13 -13
View File
@@ -7,7 +7,7 @@ from openpilot.common.file_chunker import chunk_file, get_chunk_targets, get_exi
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE, DM_INPUT_SIZE from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE, DM_INPUT_SIZE
from openpilot.selfdrive.modeld.constants import ModelConstants from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.selfdrive.modeld.helpers import TG_INPUT_DEVICES_PATH, chestnut_present, modeld_pkl_path from openpilot.selfdrive.modeld.helpers import TG_INPUT_DEVICES_PATH, usbgpu_present, modeld_pkl_path
CAMERA_CONFIGS = [ CAMERA_CONFIGS = [
@@ -36,18 +36,18 @@ else:
tg_devices = { # which device to put jit inputs to at runtime tg_devices = { # which device to put jit inputs to at runtime
'openpilot.selfdrive.modeld.modeld': { 'openpilot.selfdrive.modeld.modeld': {
'default': {'WARP_DEV': tg_backend, 'QUEUE_DEV': tg_backend}, 'default': {'WARP_DEV': tg_backend, 'QUEUE_DEV': tg_backend},
'chestnut': {'WARP_DEV': tg_backend, 'QUEUE_DEV': 'AMD'} 'usbgpu': {'WARP_DEV': tg_backend, 'QUEUE_DEV': 'AMD'}
}, },
'openpilot.selfdrive.modeld.dmonitoringmodeld': { 'openpilot.selfdrive.modeld.dmonitoringmodeld': {
'default': {'DEV': tg_backend} 'default': {'DEV': tg_backend}
}, },
} }
CHESTNUT = chestnut_present() USBGPU = usbgpu_present()
if CHESTNUT: if USBGPU:
chestnut_tg_flags = f'DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2' usbgpu_tg_flags = f'DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2'
# the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it # the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it
chestnut_lock = File("models/.chestnut.lock").abspath usbgpu_lock = File("models/.usb_gpu.lock").abspath
def write_tg_devices(target, source, env): def write_tg_devices(target, source, env):
with open(str(target[0]), "w") as f: with open(str(target[0]), "w") as f:
@@ -74,10 +74,10 @@ model_w, model_h = MEDMODEL_INPUT_SIZE
frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
if not os.getenv('SKIP_TINYGRAD_COMPILE'): if not os.getenv('SKIP_TINYGRAD_COMPILE'):
for chestnut in [False, True] if CHESTNUT else [False]: for usbgpu in [False, True] if USBGPU else [False]:
target_pkl_path = File(modeld_pkl_path(chestnut)).abspath target_pkl_path = File(modeld_pkl_path(usbgpu)).abspath
# BIG_INTO_SMALL=1 builds the default target from the big model, e.g. to test it without a chestnut # BIG_INTO_SMALL=1 builds the default target from the big model, e.g. to test it without a USB GPU
file_prefix, cmd_flags = ('big_', chestnut_tg_flags) if chestnut else ('big_' if os.getenv('BIG_INTO_SMALL') else '', tg_flags) file_prefix, cmd_flags = ('big_', usbgpu_tg_flags) if usbgpu else ('big_' if os.getenv('BIG_INTO_SMALL') else '', tg_flags)
driving_onnx_deps = get_existing_chunks(File(f"models/{file_prefix}driving_supercombo.onnx").abspath) driving_onnx_deps = get_existing_chunks(File(f"models/{file_prefix}driving_supercombo.onnx").abspath)
camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in CAMERA_CONFIGS) camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in CAMERA_CONFIGS)
# CPU 7 is isolated with isolcpus on AGNOS, so explicitly pin the compiler to it. # CPU 7 is isolated with isolcpus on AGNOS, so explicitly pin the compiler to it.
@@ -104,14 +104,14 @@ if not os.getenv('SKIP_TINYGRAD_COMPILE'):
chunk_file(pkl, chunks) chunk_file(pkl, chunks)
def do_chunk(target, source, env, pkl=target_pkl_path, chunks=chunk_targets): def do_chunk(target, source, env, pkl=target_pkl_path, chunks=chunk_targets):
chunk_file(pkl, chunks) chunk_file(pkl, chunks)
actions = Action(do_compile, " [CHESTNUT] $TARGET") if chestnut else [cmd, Action(do_chunk, " [CHUNK] $TARGET")] actions = Action(do_compile, " [USBGPU] $TARGET") if usbgpu else [cmd, Action(do_chunk, " [CHUNK] $TARGET")]
node = lenv.Command( node = lenv.Command(
chunk_targets, chunk_targets,
tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(chunk_targets), chunker_file], tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(chunk_targets), chunker_file],
actions, actions,
) )
if chestnut: if usbgpu:
lenv.SideEffect(chestnut_lock, node) lenv.SideEffect(usbgpu_lock, node)
# get model metadata # get model metadata
fn = File(f"models/dmonitoring_model").abspath fn = File(f"models/dmonitoring_model").abspath
@@ -29,7 +29,7 @@ class ModelState:
output: np.ndarray output: np.ndarray
def __init__(self, cam_w: int, cam_h: int): def __init__(self, cam_w: int, cam_h: int):
self.DEV = get_tg_input_devices(PROCESS_NAME, chestnut=False)['DEV'] self.DEV = get_tg_input_devices(PROCESS_NAME, usbgpu=False)['DEV']
with open(METADATA_PATH, 'rb') as f: with open(METADATA_PATH, 'rb') as f:
model_metadata = pickle.load(f) model_metadata = pickle.load(f)
self.input_shapes = model_metadata['input_shapes'] self.input_shapes = model_metadata['input_shapes']
+7 -7
View File
@@ -13,12 +13,12 @@ MODELS_DIR = Path(__file__).resolve().parent / 'models'
TG_INPUT_DEVICES_PATH = MODELS_DIR / 'tg_input_devices.json' TG_INPUT_DEVICES_PATH = MODELS_DIR / 'tg_input_devices.json'
def get_tg_input_devices(process_name: str, chestnut: bool): def get_tg_input_devices(process_name: str, usbgpu: bool):
with open(TG_INPUT_DEVICES_PATH) as f: with open(TG_INPUT_DEVICES_PATH) as f:
return json.load(f)[process_name]['default' if not chestnut else 'chestnut'] return json.load(f)[process_name]['default' if not usbgpu else 'usbgpu']
def modeld_pkl_path(chestnut: bool): def modeld_pkl_path(usbgpu: bool):
prefix = 'big_' if chestnut else '' prefix = 'big_' if usbgpu else ''
return MODELS_DIR / f'{prefix}driving_tinygrad.pkl' return MODELS_DIR / f'{prefix}driving_tinygrad.pkl'
def dump_oob(obj, f): def dump_oob(obj, f):
@@ -45,7 +45,7 @@ def load_oob(f):
yield pb yield pb
return pickle.load(io.BytesIO(opcodes), buffers=buffers()) return pickle.load(io.BytesIO(opcodes), buffers=buffers())
def chestnut_present() -> bool: def usbgpu_present() -> bool:
for d in USB_DEVICES_PATH.glob("*"): for d in USB_DEVICES_PATH.glob("*"):
try: try:
usb_id = (int((d / "idVendor").read_text(), 16), int((d / "idProduct").read_text(), 16)) usb_id = (int((d / "idVendor").read_text(), 16), int((d / "idProduct").read_text(), 16))
@@ -56,5 +56,5 @@ def chestnut_present() -> bool:
pass pass
return False return False
def chestnut_compiled() -> bool: def usbgpu_compiled() -> bool:
return Path(get_manifest_path(modeld_pkl_path(chestnut=True))).is_file() return Path(get_manifest_path(modeld_pkl_path(usbgpu=True))).is_file()
+30 -32
View File
@@ -1,9 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from collections.abc import Callable
import ctypes
from functools import cached_property from functools import cached_property
import os import os
os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom os.environ['GMMU'] = '0' # for usbgpu fast loading, noop for qcom
from tinygrad.tensor import Tensor from tinygrad.tensor import Tensor
from tinygrad.device import Device from tinygrad.device import Device
import struct import struct
@@ -32,7 +30,7 @@ from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_IN
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.constants import ModelConstants, Plan from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_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
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
@@ -96,10 +94,8 @@ class ChestnutState:
if self.big and "AMD" in Device._opened_devices and self.sends % 100 == 1: if self.big and "AMD" in Device._opened_devices and self.sends % 100 == 1:
try: try:
smu = Device["AMD"].iface.dev_impl.smu smu = Device["AMD"].iface.dev_impl.smu
metrics_t = smu.smu_mod.SmuMetricsExternal_t
smu._send_msg(smu.smu_mod.PPSMC_MSG_TransferTableSmu2Dram, smu.smu_mod.TABLE_SMU_METRICS, timeout=100) smu._send_msg(smu.smu_mod.PPSMC_MSG_TransferTableSmu2Dram, smu.smu_mod.TABLE_SMU_METRICS, timeout=100)
metrics_buf = bytearray(smu.adev.vram.view(smu.driver_table_paddr, ctypes.sizeof(metrics_t))[:]) metrics = smu.read_table(smu.smu_mod.SmuMetricsExternal_t, smu.smu_mod.TABLE_SMU_METRICS).SmuMetrics
metrics = metrics_t.from_buffer(metrics_buf).SmuMetrics
self.metrics = {'tempC': metrics.AvgTemperature[smu.smu_mod.TEMP_HOTSPOT], self.metrics = {'tempC': metrics.AvgTemperature[smu.smu_mod.TEMP_HOTSPOT],
'memoryTempC': metrics.AvgTemperature[smu.smu_mod.TEMP_MEM], 'memoryTempC': metrics.AvgTemperature[smu.smu_mod.TEMP_MEM],
'powerDrawW': metrics.AverageSocketPower, 'powerDrawW': metrics.AverageSocketPower,
@@ -145,18 +141,18 @@ class FrameMeta:
class ModelState(ModelStateBase): class ModelState(ModelStateBase):
prev_desire: np.ndarray # for tracking the rising edge of the pulse prev_desire: np.ndarray # for tracking the rising edge of the pulse
def __init__(self, cam_w: int, cam_h: int, chestnut: bool): def __init__(self, cam_w: int, cam_h: int, usbgpu: bool):
ModelStateBase.__init__(self) ModelStateBase.__init__(self)
input_devices = get_tg_input_devices(PROCESS_NAME, chestnut) 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_file_chunked(modeld_pkl_path(chestnut))) 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]
self.output_slices = metadata['output_slices'] self.output_slices = metadata['output_slices']
self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
self.chestnut = chestnut self.usbgpu = usbgpu
self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV) self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV)
@@ -172,7 +168,7 @@ class ModelState(ModelStateBase):
return parsed_model_outputs return parsed_model_outputs
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray], def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
inputs: dict[str, np.ndarray], after_enqueue: Callable[[], None] | None = None) -> dict[str, np.ndarray]: inputs: dict[str, np.ndarray]) -> dict[str, np.ndarray] | None:
for key in bufs.keys(): for key in bufs.keys():
ptr = np.frombuffer(bufs[key].data, dtype=np.uint8).ctypes.data ptr = np.frombuffer(bufs[key].data, dtype=np.uint8).ctypes.data
yuv_size = self.frame_buf_params[key][3] yuv_size = self.frame_buf_params[key][3]
@@ -196,11 +192,11 @@ class ModelState(ModelStateBase):
outs, = self.run_policy( outs, = self.run_policy(
**{k: self.input_queues[k] for k in POLICY_INPUTS if k in self.input_queues}, warped=warped **{k: self.input_queues[k] for k in POLICY_INPUTS if k in self.input_queues}, warped=warped
) )
if after_enqueue is not None:
after_enqueue()
model_output = outs.numpy()[0] model_output = outs.numpy()[0]
if self.chestnut and not np.all(np.isfinite(model_output)): if self.usbgpu and not np.all(np.isfinite(model_output)):
raise RuntimeError("model output not finite") # TODO remove with prev_feat
cloudlog.error("model output not finite, dropping frame")
return None
outputs_dict = self.parser.parse_outputs(self.slice_outputs(model_output, self.output_slices)) outputs_dict = self.parser.parse_outputs(self.slice_outputs(model_output, self.output_slices))
self.npy['prev_feat'][:] = model_output[self.output_slices['hidden_state']] self.npy['prev_feat'][:] = model_output[self.output_slices['hidden_state']]
@@ -222,12 +218,12 @@ class ModelState(ModelStateBase):
def main(demo=False): def main(demo=False):
cloudlog.warning("modeld init") cloudlog.warning("modeld init")
CHESTNUT = chestnut_present() and chestnut_compiled() USBGPU = usbgpu_present() and usbgpu_compiled()
if CHESTNUT: if USBGPU:
os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000' os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000'
params = Params() params = Params()
params.put_bool("ChestnutLoading", CHESTNUT) params.put_bool("UsbGpuLoading", USBGPU)
params.remove("ChestnutActive") params.remove("UsbGpuActive")
config_realtime_process(7, 54) config_realtime_process(7, 54)
@@ -257,7 +253,7 @@ def main(demo=False):
st = time.monotonic() st = time.monotonic()
cloudlog.warning("loading model") cloudlog.warning("loading model")
model = None model = None
if CHESTNUT: if USBGPU:
big_model = None big_model = None
def load_big(): def load_big():
nonlocal big_model nonlocal big_model
@@ -271,23 +267,23 @@ def main(demo=False):
loader.start() loader.start()
loader.join(BIG_MODEL_TIMEOUT) loader.join(BIG_MODEL_TIMEOUT)
model = big_model model = big_model
params.put_bool("ChestnutActive", model is not None) params.put_bool("UsbGpuActive", model is not None)
small_model = ModelState(vipc_client_main.width, vipc_client_main.height, False) if model is None or CHESTNUT else None small_model = ModelState(vipc_client_main.width, vipc_client_main.height, False) if model is None or USBGPU else None
if model is None: if model is None:
model = small_model model = small_model
params.put_bool("ChestnutLoading", False) params.put_bool("UsbGpuLoading", False)
assert model is not None assert model is not None
cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting") cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting")
# messaging # messaging
pub_socks = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"] + (["chestnutState"] if CHESTNUT else []) pub_socks = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"] + (["chestnutState"] if USBGPU else [])
pm = PubMaster(pub_socks) pm = PubMaster(pub_socks)
sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"]) sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"])
publish_state = PublishState() publish_state = PublishState()
params = Params() params = Params()
chestnut_state = ChestnutState(pm, model.chestnut) if CHESTNUT else None chestnut_state = ChestnutState(pm, model.usbgpu) if USBGPU else None
# setup filter to track dropped frames # setup filter to track dropped frames
frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_RUN_FREQ) frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_RUN_FREQ)
@@ -397,15 +393,13 @@ def main(demo=False):
mt1 = time.perf_counter() mt1 = time.perf_counter()
try: try:
send_chestnut = (chestnut_state is not None and model_output = model.run(bufs, transforms, inputs)
run_count % round(ModelConstants.MODEL_RUN_FREQ / SERVICE_LIST['chestnutState'].frequency) == 0)
model_output = model.run(bufs, transforms, inputs, chestnut_state.send if send_chestnut else None)
except Exception: except Exception:
if not params.get_bool("ChestnutActive"): if not params.get_bool("UsbGpuActive"):
raise raise
# fallback to small model # fallback to small model
cloudlog.exception("big model failed, fall back to small") cloudlog.exception("big model failed, fall back to small")
params.put_bool("ChestnutActive", False) params.put_bool("UsbGpuActive", False)
assert small_model is not None assert small_model is not None
model = small_model model = small_model
if chestnut_state is not None: if chestnut_state is not None:
@@ -425,7 +419,7 @@ def main(demo=False):
fill_model_msg(modelv2_send, model_output, action, fill_model_msg(modelv2_send, model_output, action,
publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id,
frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, extrinsics_calibration_seen) frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, extrinsics_calibration_seen)
modelv2_send.modelV2.big = model.chestnut modelv2_send.modelV2.big = model.usbgpu
desire_state = modelv2_send.modelV2.meta.desireState desire_state = modelv2_send.modelV2.meta.desireState
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft] l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
@@ -447,6 +441,10 @@ def main(demo=False):
pm.send('modelDataV2SP', mdv2sp_send) pm.send('modelDataV2SP', mdv2sp_send)
last_vipc_frame_id = meta_main.frame_id last_vipc_frame_id = meta_main.frame_id
if chestnut_state is not None and run_count % round(ModelConstants.MODEL_RUN_FREQ / SERVICE_LIST['chestnutState'].frequency) == 0:
chestnut_state.send()
if __name__ == "__main__": if __name__ == "__main__":
try: try:
import argparse import argparse
+4 -5
View File
@@ -195,18 +195,17 @@ class SelfdriveD(CruiseHelper):
self.events.add(EventName.joystickDebug) self.events.add(EventName.joystickDebug)
self.startup_event = None self.startup_event = None
loading = self.params.get_bool("ChestnutLoading") loading = self.params.get_bool("UsbGpuLoading")
if self.big_model_loading and not loading: if self.big_model_loading and not loading:
self.big_model_ready_t = time.monotonic() self.big_model_ready_t = time.monotonic()
self.events_sp.add(custom.OnroadEventSP.EventName.bigModelReady)
self.big_model_loading = loading self.big_model_loading = loading
if self.big_model_loading: if self.big_model_loading:
self.events.add(EventName.bigModelLoading) self.events.add(EventName.bigModelLoading)
big_active = self.params.get("ChestnutActive") big_active = self.params.get("UsbGpuActive")
chestnut_present = self.sm['deviceState'].chestnutPresent usbgpu_present = self.sm['deviceState'].chestnutPresent
model_unavailable = big_active is True and self.sm.seen['modelV2'] and not self.sm.alive['modelV2'] model_unavailable = big_active is True and self.sm.seen['modelV2'] and not self.sm.alive['modelV2']
big_failed = big_active is False or model_unavailable or (self.big_model_active and not chestnut_present) big_failed = big_active is False or model_unavailable or (self.big_model_active and not usbgpu_present)
if big_failed and not self.big_model_failed: if big_failed and not self.big_model_failed:
self.events.add(EventName.bigModelFailed) self.events.add(EventName.bigModelFailed)
self.big_model_failed = big_failed self.big_model_failed = big_failed
+8 -8
View File
@@ -9,7 +9,7 @@ from openpilot.system.ui.widgets.layouts import HBoxLayout
from openpilot.system.ui.widgets.icon_widget import IconWidget from openpilot.system.ui.widgets.icon_widget import IconWidget
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.common.version import RELEASE_BRANCHES from openpilot.common.version import RELEASE_BRANCHES
HEAD_BUTTON_FONT_SIZE = 40 HEAD_BUTTON_FONT_SIZE = 40
@@ -139,8 +139,8 @@ class MiciHomeLayout(Widget):
self._version_text = self._get_version_text() self._version_text = self._get_version_text()
self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48)) self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48))
self._chestnut_icon = IconWidget("icons_mici/chestnut_green.png", (68, 40)) self._egpu_icon = IconWidget("icons_mici/egpu_green.png", (50, 37))
self._chestnut_failed_icon = IconWidget("icons_mici/chestnut_orange.png", (68, 40)) self._egpu_icon_gray = IconWidget("icons_mici/egpu_gray.png", (50, 37))
self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46)) self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46))
self._body_icon = IconWidget("icons_mici/body.png", (54, 37)) self._body_icon = IconWidget("icons_mici/body.png", (54, 37))
@@ -150,8 +150,8 @@ class MiciHomeLayout(Widget):
IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9), IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9),
NetworkIcon(), NetworkIcon(),
self._experimental_icon, self._experimental_icon,
self._chestnut_icon, self._egpu_icon,
self._chestnut_failed_icon, self._egpu_icon_gray,
self._body_icon, self._body_icon,
self._mic_icon, self._mic_icon,
], spacing=18) ], spacing=18)
@@ -249,10 +249,10 @@ class MiciHomeLayout(Widget):
# ***** Center-aligned bottom section icons ***** # ***** Center-aligned bottom section icons *****
self._experimental_icon.set_visible(ui_state.experimental_mode) self._experimental_icon.set_visible(ui_state.experimental_mode)
if gui_app.sunnypilot_ui(): if gui_app.sunnypilot_ui():
self._set_chestnut_visibility() self._set_egpu_visibility()
else: else:
self._chestnut_icon.set_visible(ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.LOADING, ChestnutState.ACTIVE)) self._egpu_icon.set_visible(ui_state.sm["deviceState"].chestnutPresent and ui_state.usbgpu_compiled)
self._chestnut_failed_icon.set_visible(ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED)) self._egpu_icon_gray.set_visible(ui_state.sm["deviceState"].chestnutPresent and not ui_state.usbgpu_compiled)
self._mic_icon.set_visible(ui_state.recording_audio) self._mic_icon.set_visible(ui_state.recording_audio)
self._body_icon.set_visible(bool(ui_state.is_body)) self._body_icon.set_visible(bool(ui_state.is_body))
@@ -3,7 +3,7 @@ import pyray as rl
from dataclasses import dataclass from dataclasses import dataclass
from openpilot.common.constants import CV from openpilot.common.constants import CV
from openpilot.selfdrive.ui.mici.onroad.torque_bar import TorqueBar from openpilot.selfdrive.ui.mici.onroad.torque_bar import TorqueBar
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus, ChestnutState from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
@@ -107,7 +107,8 @@ class HudRenderer(Widget):
self.speed: float = 0.0 self.speed: float = 0.0
self.v_ego_cluster_seen: bool = False self.v_ego_cluster_seen: bool = False
self._engaged: bool = False self._engaged: bool = False
self._chestnut_fade_time: float = 0 self._small_model_engaged: bool = False
self._egpu_fade_time: float = 0
self._can_draw_top_icons = True self._can_draw_top_icons = True
self._show_wheel_critical = False self._show_wheel_critical = False
@@ -123,15 +124,17 @@ class HudRenderer(Widget):
self._txt_wheel: rl.Texture = gui_app.texture('icons_mici/wheel.png', 50, 50) self._txt_wheel: rl.Texture = gui_app.texture('icons_mici/wheel.png', 50, 50)
self._txt_wheel_critical: rl.Texture = gui_app.texture('icons_mici/wheel_critical.png', 50, 50) self._txt_wheel_critical: rl.Texture = gui_app.texture('icons_mici/wheel_critical.png', 50, 50)
self._txt_exclamation_point: rl.Texture = gui_app.texture('icons_mici/exclamation_point.png', 9, 44) self._txt_exclamation_point: rl.Texture = gui_app.texture('icons_mici/exclamation_point.png', 9, 44)
self._txt_chestnut: rl.Texture = gui_app.texture('icons_mici/chestnut.png', 60, 44) self._txt_egpu: rl.Texture = gui_app.texture('icons_mici/egpu.png', 60, 44)
self._txt_chestnut_green: rl.Texture = gui_app.texture('icons_mici/chestnut_green.png', 60, 44) self._txt_egpu_green: rl.Texture = gui_app.texture('icons_mici/egpu_green.png', 60, 44)
self._txt_chestnut_orange: rl.Texture = gui_app.texture('icons_mici/chestnut_orange.png', 75, 44) self._txt_egpu_orange: rl.Texture = gui_app.texture('icons_mici/egpu_orange.png', 60, 44)
self._chestnut_icon: rl.Texture | None = None self._txt_egpu_crossed: rl.Texture = gui_app.texture('icons_mici/egpu_crossed.png', 60, 52)
self._egpu_icon: rl.Texture | None = None
self._wheel_alpha_filter = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps) self._wheel_alpha_filter = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps)
self._wheel_y_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps) self._wheel_y_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps)
self._set_speed_alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) self._set_speed_alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps)
self._chestnut_alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) self._egpu_alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps)
def set_wheel_critical_icon(self, critical: bool): def set_wheel_critical_icon(self, critical: bool):
"""Set the wheel icon to critical or normal state.""" """Set the wheel icon to critical or normal state."""
@@ -162,10 +165,13 @@ class HudRenderer(Widget):
controls_state.deprecated.vCruise if v_cruise_cluster == 0.0 else v_cruise_cluster controls_state.deprecated.vCruise if v_cruise_cluster == 0.0 else v_cruise_cluster
) )
engaged = sm['selfdriveState'].enabled engaged = sm['selfdriveState'].enabled
if (engaged and not self._engaged and not ui_state.usbgpu_loading and ui_state.usbgpu_active is not True and
ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame):
self._small_model_engaged = True
if engaged != self._engaged:
self._egpu_fade_time = rl.get_time() if engaged else 0
if (set_speed != self.set_speed and engaged) or (engaged and not self._engaged): if (set_speed != self.set_speed and engaged) or (engaged and not self._engaged):
self._set_speed_changed_time = rl.get_time() self._set_speed_changed_time = rl.get_time()
if engaged != self._engaged:
self._chestnut_fade_time = rl.get_time() if engaged else 0
self._engaged = engaged self._engaged = engaged
self.set_speed = set_speed self.set_speed = set_speed
self.is_cruise_set = 0 < self.set_speed < SET_SPEED_NA self.is_cruise_set = 0 < self.set_speed < SET_SPEED_NA
@@ -185,7 +191,8 @@ class HudRenderer(Widget):
if self.is_cruise_set: if self.is_cruise_set:
self._draw_set_speed(rect) self._draw_set_speed(rect)
self._draw_model_source(rect) if ui_state.usbgpu and ui_state.usbgpu_compiled:
self._draw_model_source(rect)
self._draw_steering_wheel(rect) self._draw_steering_wheel(rect)
@@ -193,24 +200,30 @@ class HudRenderer(Widget):
if ui_state.sm.recv_frame['selfdriveState'] < ui_state.started_frame: if ui_state.sm.recv_frame['selfdriveState'] < ui_state.started_frame:
return return
loading = ui_state.chestnut_state == ChestnutState.LOADING big_failed = (ui_state.usbgpu_active is False or not ui_state.sm['deviceState'].chestnutPresent or
(ui_state.usbgpu_active is True and ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame and
not ui_state.sm.alive['modelV2']) or
(ui_state.usbgpu_active is None and ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame))
self._small_model_engaged &= big_failed
loading = ui_state.usbgpu_loading or (ui_state.usbgpu_active is None and not big_failed)
if loading: if loading:
icon = self._txt_chestnut pulse = 0.5 - 0.5 * math.cos(rl.get_time() * 6.0)
opacity = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)) icon = self._txt_egpu
elif ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED): opacity = 0.35 + 0.65 * pulse
icon = self._txt_chestnut_orange elif self._small_model_engaged:
opacity = 1.0 icon = self._txt_egpu_crossed
elif ui_state.chestnut_state == ChestnutState.ACTIVE: opacity = 0.65
icon = self._txt_chestnut_green elif big_failed:
icon = self._txt_egpu_orange
opacity = 1.0 opacity = 1.0
else: else:
return icon = self._txt_egpu_green
opacity = 1.0
if icon is not self._chestnut_icon: if icon is not self._egpu_icon:
self._chestnut_fade_time = rl.get_time() self._egpu_fade_time = rl.get_time()
self._chestnut_icon = icon self._egpu_icon = icon
visible = loading or rl.get_time() - self._chestnut_fade_time < SET_SPEED_PERSISTENCE alpha = self._egpu_alpha_filter.update(loading or 0 < rl.get_time() - self._egpu_fade_time < SET_SPEED_PERSISTENCE)
alpha = self._chestnut_alpha_filter.update(visible)
if alpha < 1e-2: if alpha < 1e-2:
return return
@@ -62,14 +62,14 @@ class ModelsLayout(Widget):
self.big_model_item = ListItemSP( self.big_model_item = ListItemSP(
title=tr("Big Model"), title=tr("Big Model"),
action_item=ScrollingButtonAction(tr("SELECT")), action_item=ScrollingButtonAction(tr("SELECT")),
callback=lambda: self._open_source_dialog("chestnut") callback=lambda: self._open_source_dialog("usbgpu")
) )
self.download_item = download_status_item(lambda: tr("Download") if self._downloading else tr("Model Status")) self.download_item = download_status_item(lambda: tr("Download") if self._downloading else tr("Model Status"))
self.refresh_item = button_item(tr("Refresh Model List"), tr("REFRESH"), "", self.refresh_item = button_item(tr("Refresh Model List"), tr("REFRESH"), "",
lambda: (ui_state.params.put("ModelManager_LastSyncTime", 0), lambda: (ui_state.params.put("ModelManager_LastSyncTime", 0),
ui_state.params.put("ModelManager_LastSyncTime_Chestnut", 0), ui_state.params.put("ModelManager_LastSyncTime_USBGPU", 0),
gui_app.push_widget(alert_dialog(tr("Fetching Latest Models"))))) gui_app.push_widget(alert_dialog(tr("Fetching Latest Models")))))
self.clear_cache_item = ListItemSP( self.clear_cache_item = ListItemSP(
@@ -177,14 +177,14 @@ class ModelsLayout(Widget):
big_state = big_model_state() big_state = big_model_state()
carry_source, carry_internal, _ = carrying_model() carry_source, carry_internal, _ = carrying_model()
segments = [] segments = []
for source, label in (("qcom", tr("small")), ("chestnut", tr("big"))): for source, label in (("qcom", tr("small")), ("usbgpu", tr("big"))):
if segments: if segments:
segments.append(("|", rl.GRAY, None, None)) segments.append(("|", rl.GRAY, None, None))
bundle = get_selected_bundle(ui_state.params, source) bundle = get_selected_bundle(ui_state.params, source)
name = bundle.internalName if bundle else default_model_name(source) name = bundle.internalName if bundle else default_model_name(source)
color = ON_COLOR if (source == carry_source and name == carry_internal) else rl.LIGHTGRAY color = ON_COLOR if (source == carry_source and name == carry_internal) else rl.LIGHTGRAY
name = "" + name name = "" + name
if source == "chestnut": if source == "usbgpu":
if big_state == 'failed': if big_state == 'failed':
color = rl.RED color = rl.RED
elif big_state == 'loading': elif big_state == 'loading':
@@ -208,10 +208,10 @@ class ModelsLayout(Widget):
"""The failover story for the Model Status row. One-way big -> small, and the """The failover story for the Model Status row. One-way big -> small, and the
fallback is runner-matched: a Default big can only fall back to the Default fallback is runner-matched: a Default big can only fall back to the Default
small (stock modeld), a custom big has no automatic fallback yet.""" small (stock modeld), a custom big has no automatic fallback yet."""
if not ui_state.chestnut_present: if not ui_state.usbgpu:
return "" return ""
big_bundle = get_selected_bundle(ui_state.params, "chestnut") big_bundle = get_selected_bundle(ui_state.params, "usbgpu")
big_name = big_bundle.internalName if big_bundle else default_model_name("chestnut") big_name = big_bundle.internalName if big_bundle else default_model_name("usbgpu")
big_is_default = big_bundle is None big_is_default = big_bundle is None
fallback_name = default_model_name("qcom") fallback_name = default_model_name("qcom")
state = big_model_state() state = big_model_state()
@@ -225,7 +225,7 @@ class ModelsLayout(Widget):
return tr("Getting the big model ready.") return tr("Getting the big model ready.")
if big_is_default: if big_is_default:
return tr("{} will drive. If it fails during a drive, {} takes over until the next drive.").format(big_name, fallback_name) return tr("{} will drive. If it fails during a drive, {} takes over until the next drive.").format(big_name, fallback_name)
return tr("{} will drive when the chestnut is ready.").format(big_name) return tr("{} will drive when the eGPU is ready.").format(big_name)
@staticmethod @staticmethod
def _download_row_state(progresses, name: str) -> dict: def _download_row_state(progresses, name: str) -> dict:
@@ -261,7 +261,7 @@ class ModelsLayout(Widget):
ui_state.params.put("ModelManager_DownloadRef", selected_bundle.ref) ui_state.params.put("ModelManager_DownloadRef", selected_bundle.ref)
def _resolve_selected_bundle(self, ref): def _resolve_selected_bundle(self, ref):
source_bundles = {source: bundles_for_source(source) for source in ("qcom", "chestnut")} source_bundles = {source: bundles_for_source(source) for source in ("qcom", "usbgpu")}
resolved = resolve_bundle_by_ref(ref, source_bundles) resolved = resolve_bundle_by_ref(ref, source_bundles)
return resolved[0] if resolved else None return resolved[0] if resolved else None
@@ -329,7 +329,7 @@ class ModelsLayout(Widget):
self._handle_bundle_download_progress() self._handle_bundle_download_progress()
carry_source, _, carry_display = carrying_model() carry_source, _, carry_display = carrying_model()
for item, item_source in ((self.small_model_item, "qcom"), (self.big_model_item, "chestnut")): for item, item_source in ((self.small_model_item, "qcom"), (self.big_model_item, "usbgpu")):
bundle = get_selected_bundle(ui_state.params, item_source) bundle = get_selected_bundle(ui_state.params, item_source)
name = bundle.displayName if bundle else default_model_name(item_source) name = bundle.displayName if bundle else default_model_name(item_source)
color = ON_COLOR if (item_source == carry_source and name == carry_display) else style.ITEM_TEXT_VALUE_COLOR color = ON_COLOR if (item_source == carry_source and name == carry_display) else style.ITEM_TEXT_VALUE_COLOR
@@ -9,7 +9,7 @@ import math
import pyray as rl import pyray as rl
import time import time
from dataclasses import dataclass from dataclasses import dataclass
from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr_noop from openpilot.system.ui.lib.multilang import tr_noop
@@ -21,8 +21,8 @@ METRIC_MARGIN = 30
METRIC_START_Y = 300 METRIC_START_Y = 300
HOME_BTN = rl.Rectangle(60, 860, 180, 180) HOME_BTN = rl.Rectangle(60, 860, 180, 180)
CHESTNUT_ICON_WIDTH = 180 EGPU_ICON_WIDTH = 180
CHESTNUT_ICON_HEIGHT = 133 EGPU_ICON_HEIGHT = 133
# Color scheme # Color scheme
@@ -59,9 +59,10 @@ class MetricData:
class SidebarSP: class SidebarSP:
def __init__(self): def __init__(self):
self._sunnylink_status = MetricData(tr_noop("SUNNYLINK"), tr_noop("OFFLINE"), Colors.WARNING) self._sunnylink_status = MetricData(tr_noop("SUNNYLINK"), tr_noop("OFFLINE"), Colors.WARNING)
self._chestnut_green_img = gui_app.texture("icons_mici/chestnut_green.png", CHESTNUT_ICON_WIDTH, CHESTNUT_ICON_HEIGHT) self._egpu_green_img = gui_app.texture("icons_mici/egpu_green.png", EGPU_ICON_WIDTH, EGPU_ICON_HEIGHT)
self._chestnut_default_img = gui_app.texture("icons_mici/chestnut.png", CHESTNUT_ICON_WIDTH, CHESTNUT_ICON_HEIGHT) self._egpu_default_img = gui_app.texture("icons_mici/egpu.png", EGPU_ICON_WIDTH, EGPU_ICON_HEIGHT)
self._chestnut_orange_img = gui_app.texture("icons_mici/chestnut_orange.png", CHESTNUT_ICON_WIDTH, CHESTNUT_ICON_HEIGHT) self._egpu_orange_img = gui_app.texture("icons_mici/egpu_orange.png", EGPU_ICON_WIDTH, EGPU_ICON_HEIGHT)
self._egpu_gray_img = gui_app.texture("icons_mici/egpu_gray.png", EGPU_ICON_WIDTH, EGPU_ICON_HEIGHT)
def _update_sunnylink_status(self): def _update_sunnylink_status(self):
if not ui_state.params.get_bool("SunnylinkEnabled"): if not ui_state.params.get_bool("SunnylinkEnabled"):
@@ -89,17 +90,22 @@ class SidebarSP:
def _get_home_icon(self, default_img: rl.Texture) -> tuple[rl.Texture, rl.Vector2, float]: def _get_home_icon(self, default_img: rl.Texture) -> tuple[rl.Texture, rl.Vector2, float]:
default_pos = rl.Vector2(HOME_BTN.x, HOME_BTN.y) default_pos = rl.Vector2(HOME_BTN.x, HOME_BTN.y)
state = ui_state.chestnut_state if not ui_state.sm["deviceState"].chestnutPresent:
if state == ChestnutState.DISCONNECTED:
return default_img, default_pos, 1.0 return default_img, default_pos, 1.0
if state == ChestnutState.LOADING: big_model_selected = ui_state.usbgpu_compiled or ui_state.model_runner_tinygrad
icon = self._chestnut_default_img big_model_failed = ui_state.started and ui_state.big_model_failed
loading = ui_state.usbgpu_loading or (big_model_selected and ui_state.started and ui_state.usbgpu_active is None)
if loading:
icon = self._egpu_default_img
opacity = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)) opacity = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0))
elif state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED): elif big_model_selected and big_model_failed:
icon, opacity = self._chestnut_orange_img, 1.0 icon, opacity = self._egpu_orange_img, 1.0
elif big_model_selected:
icon, opacity = self._egpu_green_img, 1.0
else: else:
icon, opacity = self._chestnut_green_img, 1.0 icon, opacity = self._egpu_gray_img, 1.0
x = HOME_BTN.x + (HOME_BTN.width - icon.width) / 2 x = HOME_BTN.x + (HOME_BTN.width - icon.width) / 2
y = HOME_BTN.y + (HOME_BTN.height - icon.height) / 2 y = HOME_BTN.y + (HOME_BTN.height - icon.height) / 2
@@ -9,7 +9,7 @@ import math
import pyray as rl import pyray as rl
from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout
from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import FontWeight from openpilot.system.ui.lib.application import FontWeight
from openpilot.system.ui.widgets.icon_widget import IconWidget from openpilot.system.ui.widgets.icon_widget import IconWidget
from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.label import UnifiedLabel
@@ -19,16 +19,35 @@ class MiciHomeLayoutSP(MiciHomeLayout):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._openpilot_label = UnifiedLabel("sunnypilot", font_size=88, font_weight=FontWeight.AUDIOWIDE, max_width=480, wrap_text=False) self._openpilot_label = UnifiedLabel("sunnypilot", font_size=88, font_weight=FontWeight.AUDIOWIDE, max_width=480, wrap_text=False)
self._chestnut_loading_icon = IconWidget("icons_mici/chestnut.png", (68, 40)) self._egpu_icon_default = IconWidget("icons_mici/egpu.png", (50, 37))
self._chestnut_loading_icon.set_visible(False) self._egpu_icon_default.set_visible(False)
failed_idx = self._status_bar_layout.widgets.index(self._chestnut_failed_icon) self._egpu_icon_orange = IconWidget("icons_mici/egpu_orange.png", (50, 37))
self._status_bar_layout.widgets.insert(failed_idx + 1, self._chestnut_loading_icon) self._egpu_icon_orange.set_visible(False)
gray_idx = self._status_bar_layout.widgets.index(self._egpu_icon_gray)
self._status_bar_layout.widgets.insert(gray_idx + 1, self._egpu_icon_default)
self._status_bar_layout.widgets.insert(gray_idx + 2, self._egpu_icon_orange)
def _set_chestnut_visibility(self): def _set_egpu_visibility(self):
# stock has no loading tier: it shows green from the moment a big model is available. keep the chestnut = ui_state.sm["deviceState"].chestnutPresent
# pulse so the status bar and the onroad HUD agree on what loading looks like. if not chestnut:
loading = ui_state.chestnut_state == ChestnutState.LOADING self._egpu_icon.set_visible(False)
self._chestnut_loading_icon._opacity = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)) self._egpu_icon_default.set_visible(False)
self._chestnut_loading_icon.set_visible(loading) self._egpu_icon_orange.set_visible(False)
self._chestnut_icon.set_visible(not loading and ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE)) self._egpu_icon_gray.set_visible(False)
self._chestnut_failed_icon.set_visible(ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED)) return
big_model_selected = ui_state.usbgpu_compiled or ui_state.model_runner_tinygrad
big_model_failed = ui_state.started and ui_state.big_model_failed
loading = ui_state.usbgpu_loading or (big_model_selected and ui_state.started and ui_state.usbgpu_active is None)
if loading:
self._egpu_icon_default._opacity = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0))
self._egpu_icon_default.set_visible(True)
self._egpu_icon.set_visible(False)
self._egpu_icon_orange.set_visible(False)
self._egpu_icon_gray.set_visible(False)
else:
self._egpu_icon_default.set_visible(False)
self._egpu_icon.set_visible(big_model_selected and not big_model_failed)
self._egpu_icon_orange.set_visible(big_model_selected and big_model_failed)
self._egpu_icon_gray.set_visible(not big_model_selected)
@@ -27,14 +27,14 @@ def _model_info() -> tuple[str, str, str]:
state = big_model_state() state = big_model_state()
_, _, carry_display = carrying_model() _, _, carry_display = carrying_model()
if carry_display is None: if carry_display is None:
big = get_selected_bundle(ui_state.params, "chestnut") big = get_selected_bundle(ui_state.params, "usbgpu")
carry_display = big.displayName if big else default_model_name("chestnut") carry_display = big.displayName if big else default_model_name("usbgpu")
active_text = (carry_display or active_name).lower() active_text = (carry_display or active_name).lower()
if state == 'failed': if state == 'failed':
return active_text, tr("big model"), tr("unavailable") return active_text, tr("big model"), tr("unavailable")
if state == 'loading': if state == 'loading':
return active_text, tr("big model"), tr("getting ready") return active_text, tr("big model"), tr("getting ready")
header = tr("small model") if source == "chestnut" else tr("big model") header = tr("small model") if source == "usbgpu" else tr("big model")
return active_text, header, other_name.lower() return active_text, header, other_name.lower()
@@ -113,7 +113,7 @@ class ModelsLayoutMici(NavScroller):
hardware_btns = [] hardware_btns = []
active = active_source() active = active_source()
for source, label in (("qcom", tr("small models")), ("chestnut", tr("big models"))): for source, label in (("qcom", tr("small models")), ("usbgpu", tr("big models"))):
bundle = get_selected_bundle(ui_state.params, source) bundle = get_selected_bundle(ui_state.params, source)
value = (bundle.internalName if bundle else default_model_name(source)).lower() value = (bundle.internalName if bundle else default_model_name(source)).lower()
if source == active: if source == active:
@@ -7,6 +7,7 @@ See the LICENSE.md file in the root directory for more details.
import pyray as rl import pyray as rl
from openpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer from openpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.sunnypilot.onroad.blind_spot_indicators import BlindSpotIndicators from openpilot.selfdrive.ui.sunnypilot.onroad.blind_spot_indicators import BlindSpotIndicators
@@ -21,6 +22,8 @@ class HudRendererSP(HudRenderer):
def _render(self, rect: rl.Rectangle) -> None: def _render(self, rect: rl.Rectangle) -> None:
super()._render(rect) super()._render(rect)
if ui_state.usbgpu and not ui_state.usbgpu_compiled and ui_state.model_runner_tinygrad:
self._draw_model_source(rect)
self.blind_spot_indicators.render(rect) self.blind_spot_indicators.render(rect)
def _has_blind_spot_detected(self) -> bool: def _has_blind_spot_detected(self) -> bool:
+20 -17
View File
@@ -4,15 +4,15 @@ 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.selfdrive.ui.ui_state import ui_state, ChestnutState from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.sunnypilot.models.fetcher import get_cached_bundles from openpilot.sunnypilot.models.fetcher import get_cached_bundles
from openpilot.sunnypilot.models.helpers import get_active_source, get_selected_bundle, resolve_bundle_by_ref from openpilot.sunnypilot.models.helpers import get_active_source, get_selected_bundle, resolve_bundle_by_ref
from openpilot.sunnypilot.models.model_name import DEFAULT_BIG_MODEL, DEFAULT_MODEL from openpilot.sunnypilot.models.model_name import DEFAULT_BIG_MODEL, DEFAULT_MODEL
def active_source() -> str: def active_source() -> str:
return get_active_source(chestnut=ui_state.chestnut_present, return get_active_source(usbgpu=ui_state.usbgpu,
chestnut_active=ui_state.chestnut_active, chestnut_loading=ui_state.chestnut_loading, usbgpu_active=ui_state.usbgpu_active, usbgpu_loading=ui_state.usbgpu_loading,
offroad=ui_state.is_offroad()) offroad=ui_state.is_offroad())
@@ -23,7 +23,7 @@ def bundles_for_source(source: str):
def default_model(source: str) -> str: def default_model(source: str) -> str:
return DEFAULT_BIG_MODEL if source == 'chestnut' else DEFAULT_MODEL return DEFAULT_BIG_MODEL if source == 'usbgpu' else DEFAULT_MODEL
def default_model_name(source: str) -> str: def default_model_name(source: str) -> str:
@@ -31,10 +31,13 @@ def default_model_name(source: str) -> str:
def big_model_state() -> str | None: def big_model_state() -> str | None:
"""'failed' | 'loading' | None, from the same state the icons render.""" """'failed' | 'loading' | None, mirroring the sidebar's detection (#1969)."""
return {ChestnutState.UNCOMPILED: 'failed', if ui_state.started and ui_state.usbgpu and ui_state.big_model_failed:
ChestnutState.FAILED: 'failed', return 'failed'
ChestnutState.LOADING: 'loading'}.get(ui_state.chestnut_state) big_selected = ui_state.usbgpu_compiled or ui_state.model_runner_tinygrad
if ui_state.usbgpu_loading or (big_selected and ui_state.started and ui_state.usbgpu_active is None):
return 'loading'
return None
def carrying_model() -> tuple[str | None, str | None, str | None]: def carrying_model() -> tuple[str | None, str | None, str | None]:
@@ -42,14 +45,14 @@ def carrying_model() -> tuple[str | None, str | None, str | None]:
when a Default big cannot carry, stock modeld runs the Default small, never the when a Default big cannot carry, stock modeld runs the Default small, never the
small slot's pick; a custom big has no automatic fallback yet -> (None, None, None).""" small slot's pick; a custom big has no automatic fallback yet -> (None, None, None)."""
source = active_source() source = active_source()
if source == "chestnut": if source == "usbgpu":
bundle = get_selected_bundle(ui_state.params, "chestnut") bundle = get_selected_bundle(ui_state.params, "usbgpu")
if bundle: if bundle:
return "chestnut", bundle.internalName, bundle.displayName return "usbgpu", bundle.internalName, bundle.displayName
name = default_model_name("chestnut") name = default_model_name("usbgpu")
return "chestnut", name, name return "usbgpu", name, name
if ui_state.chestnut_present: if ui_state.usbgpu:
if get_selected_bundle(ui_state.params, "chestnut") is None: if get_selected_bundle(ui_state.params, "usbgpu") is None:
name = default_model_name("qcom") name = default_model_name("qcom")
return "qcom", name, name return "qcom", name, name
return None, None, None return None, None, None
@@ -63,7 +66,7 @@ def carrying_model() -> tuple[str | None, str | None, str | None]:
def queued_name(current_ref) -> str | None: def queued_name(current_ref) -> str | None:
ref = ui_state.params.get("ModelManager_DownloadRef") ref = ui_state.params.get("ModelManager_DownloadRef")
if ref and ref != current_ref: if ref and ref != current_ref:
source_bundles = {source: bundles_for_source(source) for source in ("qcom", "chestnut")} source_bundles = {source: bundles_for_source(source) for source in ("qcom", "usbgpu")}
if resolved := resolve_bundle_by_ref(ref, source_bundles): if resolved := resolve_bundle_by_ref(ref, source_bundles):
return resolved[0].internalName return resolved[0].internalName
return None return None
@@ -76,7 +79,7 @@ def model_info() -> tuple[str, str, str]:
manager republishes a tick after a chestnut change, so the stale bundle manager republishes a tick after a chestnut change, so the stale bundle
would flash the wrong model.""" would flash the wrong model."""
source = active_source() source = active_source()
other = "qcom" if source == "chestnut" else "chestnut" other = "qcom" if source == "usbgpu" else "usbgpu"
active_bundle = get_selected_bundle(ui_state.params, source) active_bundle = get_selected_bundle(ui_state.params, source)
other_bundle = get_selected_bundle(ui_state.params, other) other_bundle = get_selected_bundle(ui_state.params, other)
@@ -152,13 +152,10 @@ class UIStateSP:
self.has_icbm = self.CP_SP.intelligentCruiseButtonManagementAvailable and self.params.get_bool("IntelligentCruiseButtonManagement") self.has_icbm = self.CP_SP.intelligentCruiseButtonManagementAvailable and self.params.get_bool("IntelligentCruiseButtonManagement")
self._enforce_constraints() self._enforce_constraints()
source = get_active_source(chestnut=self.chestnut_present, chestnut_active=self.chestnut_active, source = get_active_source(usbgpu=self.usbgpu, usbgpu_active=self.usbgpu_active,
chestnut_loading=self.chestnut_loading, offroad=self.is_offroad()) usbgpu_loading=self.usbgpu_loading, offroad=self.is_offroad())
self.active_bundle = self.params.get(ACTIVE_BUNDLE_KEYS[source]) self.active_bundle = self.params.get(ACTIVE_BUNDLE_KEYS[source])
self.model_runner_tinygrad = self.active_bundle is not None and self.active_bundle.get("runner") == "tinygrad" self.model_runner_tinygrad = self.active_bundle is not None and self.active_bundle.get("runner") == "tinygrad"
# stock only counts the default big model's compiled pkl. a downloaded big bundle runs on the
# chestnut just the same, so ChestnutState has to see it as available too.
self.chestnut_compiled = self.chestnut_compiled or self.model_runner_tinygrad
self.blindspot = self.params.get_bool("BlindSpot") self.blindspot = self.params.get_bool("BlindSpot")
self.chevron_metrics = self.params.get("ChevronInfo") self.chevron_metrics = self.params.get("ChevronInfo")
self.custom_interactive_timeout = self.params.get("InteractivityTimeout", return_default=True) self.custom_interactive_timeout = self.params.get("InteractivityTimeout", return_default=True)
+20 -43
View File
@@ -12,7 +12,7 @@ from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.ui.lib.prime_state import PrimeState from openpilot.selfdrive.ui.lib.prime_state import PrimeState
from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.application import gui_app
from openpilot.common.hardware import HARDWARE, PC from openpilot.common.hardware import HARDWARE, PC
from openpilot.selfdrive.modeld.helpers import chestnut_compiled from openpilot.selfdrive.modeld.helpers import usbgpu_compiled
from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP, DeviceSP from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP, DeviceSP
@@ -28,15 +28,6 @@ class UIStatus(Enum):
LONG_ONLY = "long_only" LONG_ONLY = "long_only"
class ChestnutState(Enum):
DISCONNECTED = "disconnected"
UNCOMPILED = "uncompiled"
READY = "ready"
LOADING = "loading"
ACTIVE = "active"
FAILED = "failed"
class UIState(UIStateSP): class UIState(UIStateSP):
_instance: 'UIState | None' = None _instance: 'UIState | None' = None
@@ -91,11 +82,10 @@ class UIState(UIStateSP):
self.always_on_dm: bool = self.params.get_bool("AlwaysOnDM") self.always_on_dm: bool = self.params.get_bool("AlwaysOnDM")
self.experimental_mode: bool = self.params.get_bool("ExperimentalMode") self.experimental_mode: bool = self.params.get_bool("ExperimentalMode")
self.experimental_mode_confirmed: bool = self.params.get_bool("ExperimentalModeConfirmed") self.experimental_mode_confirmed: bool = self.params.get_bool("ExperimentalModeConfirmed")
self.chestnut_present: bool = False self.usbgpu: bool = False
self.chestnut_compiled: bool = chestnut_compiled() self.usbgpu_compiled: bool = usbgpu_compiled()
self.chestnut_active: bool | None = None self.usbgpu_active: bool | None = self.params.get("UsbGpuActive")
self.chestnut_loading: bool = False self.usbgpu_loading: bool = self.params.get_bool("UsbGpuLoading")
self.chestnut_state = ChestnutState.DISCONNECTED
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
@@ -122,6 +112,15 @@ class UIState(UIStateSP):
def add_on_body_changed_callbacks(self, callback: Callable[[], None]): def add_on_body_changed_callbacks(self, callback: Callable[[], None]):
self._on_body_changed_callbacks.append(callback) self._on_body_changed_callbacks.append(callback)
@property
def big_model_failed(self) -> bool:
# Mirrors the onroad HUD's four-condition check so sidebar and home icons reflect the same failure states
return (self.usbgpu_active is False or
not self.sm['deviceState'].chestnutPresent or
(self.usbgpu_active is True and self.sm.recv_frame['modelV2'] > self.started_frame and
not self.sm.alive['modelV2']) or
(self.usbgpu_active is None and self.sm.recv_frame['modelV2'] > self.started_frame))
@property @property
def engaged(self) -> bool: def engaged(self) -> bool:
return self.started and (self.sm["selfdriveState"].enabled or self.sm["selfdriveStateSP"].mads.enabled) return self.started and (self.sm["selfdriveState"].enabled or self.sm["selfdriveStateSP"].mads.enabled)
@@ -141,7 +140,6 @@ class UIState(UIStateSP):
self.sm.update(0) self.sm.update(0)
self._update_state() self._update_state()
self._update_status() self._update_status()
self._update_chestnut_state()
device.update() device.update()
UIStateSP.update(self) UIStateSP.update(self)
@@ -205,35 +203,12 @@ class UIState(UIStateSP):
self.status = UIStatus.DISENGAGED self.status = UIStatus.DISENGAGED
self.started_frame = self.sm.frame self.started_frame = self.sm.frame
self.started_time = time.monotonic() self.started_time = time.monotonic()
self.chestnut_present = self.sm["deviceState"].chestnutPresent
for callback in self._offroad_transition_callbacks: for callback in self._offroad_transition_callbacks:
callback() callback()
self._started_prev = self.started self._started_prev = self.started
def _update_chestnut_state(self) -> None:
detected = self.sm["deviceState"].chestnutPresent
if not self.started:
self.chestnut_present = detected
self.chestnut_state = (ChestnutState.READY if detected and self.chestnut_compiled else
ChestnutState.UNCOMPILED if detected else ChestnutState.DISCONNECTED)
return
model_seen = self.sm.recv_frame["modelV2"] > self.started_frame
if not self.chestnut_present:
self.chestnut_state = ChestnutState.DISCONNECTED
elif not self.chestnut_compiled:
self.chestnut_state = ChestnutState.UNCOMPILED
elif self.chestnut_state == ChestnutState.FAILED or not detected or (model_seen and (not self.sm.alive["modelV2"] or not self.sm["modelV2"].big)):
self.chestnut_state = ChestnutState.FAILED
elif self.chestnut_loading or not model_seen:
self.chestnut_state = ChestnutState.LOADING
elif self.chestnut_active is False:
self.chestnut_state = ChestnutState.FAILED
else:
self.chestnut_state = ChestnutState.ACTIVE
def update_params(self) -> None: def update_params(self) -> None:
# For slower operations # For slower operations
# Update longitudinal control state # Update longitudinal control state
@@ -250,10 +225,12 @@ class UIState(UIStateSP):
self.always_on_dm = self.params.get_bool("AlwaysOnDM") self.always_on_dm = self.params.get_bool("AlwaysOnDM")
self.experimental_mode = self.params.get_bool("ExperimentalMode") self.experimental_mode = self.params.get_bool("ExperimentalMode")
self.experimental_mode_confirmed = self.params.get_bool("ExperimentalModeConfirmed") self.experimental_mode_confirmed = self.params.get_bool("ExperimentalModeConfirmed")
if not self.chestnut_compiled: # keep usbgpu UI active until offroad transition when gpu disappears
self.chestnut_compiled = chestnut_compiled() self.usbgpu = self.sm["deviceState"].chestnutPresent or (self.usbgpu and self.started)
self.chestnut_active = self.params.get("ChestnutActive") if not self.usbgpu_compiled:
self.chestnut_loading = self.params.get_bool("ChestnutLoading") self.usbgpu_compiled = usbgpu_compiled()
self.usbgpu_active = self.params.get("UsbGpuActive")
self.usbgpu_loading = self.params.get_bool("UsbGpuLoading")
UIStateSP.update_params(self) UIStateSP.update_params(self)
@@ -298,7 +298,7 @@ def _load_policy_runners(args: argparse.Namespace) -> tuple[list, list]:
if __name__ == "__main__": if __name__ == "__main__":
if 'USB' in os.getenv('DEV', '') or os.getenv('CHESTNUT'): if 'USB' in os.getenv('DEV', '') or os.getenv('USBGPU'):
from openpilot.system.hardware.chestnut.flash import link_up from openpilot.system.hardware.chestnut.flash import link_up
for _ in range(10): for _ in range(10):
if link_up(): if link_up():
+21 -21
View File
@@ -9,7 +9,7 @@ See the LICENSE.md file in the root directory for more details.
import os import os
os.environ['GMMU'] = '0' os.environ['GMMU'] = '0'
from openpilot.common.hardware import COMMA_HARDWARE from openpilot.common.hardware import COMMA_HARDWARE
from openpilot.selfdrive.modeld.helpers import chestnut_present, load_oob from openpilot.selfdrive.modeld.helpers import usbgpu_present, load_oob
import time import time
import numpy as np import numpy as np
import openpilot.cereal.messaging as messaging import openpilot.cereal.messaging as messaging
@@ -84,14 +84,14 @@ class ModelState(ModelStateBase):
inputs: dict[str, np.ndarray] inputs: dict[str, np.ndarray]
prev_desire: np.ndarray prev_desire: np.ndarray
def __init__(self, cam_w: int, cam_h: int, chestnut: bool = False): def __init__(self, cam_w: int, cam_h: int, usbgpu: bool = False):
ModelStateBase.__init__(self) ModelStateBase.__init__(self)
env_pkl = os.environ.get('COMBINED_MODEL_PKL') env_pkl = os.environ.get('COMBINED_MODEL_PKL')
if env_pkl and os.path.exists(env_pkl): if env_pkl and os.path.exists(env_pkl):
model_bundle = None model_bundle = None
else: else:
model_bundle = get_active_bundle(chestnut=chestnut) model_bundle = get_active_bundle(usbgpu=usbgpu)
self.generation = model_bundle.generation if model_bundle is not None else None self.generation = model_bundle.generation if model_bundle is not None else None
overrides = {override.key: override.value for override in model_bundle.overrides} if model_bundle else {} overrides = {override.key: override.value for override in model_bundle.overrides} if model_bundle else {}
@@ -99,7 +99,7 @@ class ModelState(ModelStateBase):
self.LONG_SMOOTH_SECONDS = float(overrides.get('long', ".0")) self.LONG_SMOOTH_SECONDS = float(overrides.get('long', ".0"))
self.MIN_LAT_CONTROL_SPEED = 0.3 self.MIN_LAT_CONTROL_SPEED = 0.3
self.PLANPLUS_CONTROL: float = 1.0 self.PLANPLUS_CONTROL: float = 1.0
self.chestnut = chestnut self.usbgpu = usbgpu
pkl_path = _find_driving_pkl(model_bundle) pkl_path = _find_driving_pkl(model_bundle)
assert pkl_path is not None, "No driving pkl found — all models must be compiled with compile_modeld.py" assert pkl_path is not None, "No driving pkl found — all models must be compiled with compile_modeld.py"
@@ -110,7 +110,7 @@ class ModelState(ModelStateBase):
jits = load_oob(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.chestnut else self.WARP_DEV self.DEV = 'AMD' if self.usbgpu else self.WARP_DEV
self.QUEUE_DEV = self.DEV self.QUEUE_DEV = self.DEV
metadata = jits['metadata'] metadata = jits['metadata']
@@ -185,7 +185,7 @@ class ModelState(ModelStateBase):
else: else:
self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=frame_tensor, big_frame=big_frame_tensor) self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=frame_tensor, big_frame=big_frame_tensor)
if self.chestnut: if self.usbgpu:
self.warmup() self.warmup()
def warmup(self) -> None: def warmup(self) -> None:
@@ -287,7 +287,7 @@ class ModelState(ModelStateBase):
buf[0, :-1] = buf[0, 1:] buf[0, :-1] = buf[0, 1:]
buf[0, -1, :] = outputs['desired_curvature'][0, :] if not self.mlsim else 0 buf[0, -1, :] = outputs['desired_curvature'][0, :] if not self.mlsim else 0
if self.chestnut and not np.all(np.isfinite(outputs.get('plan', np.array([0.])))): if self.usbgpu and not np.all(np.isfinite(outputs.get('plan', np.array([0.])))):
cloudlog.error("model output not finite, dropping frame") cloudlog.error("model output not finite, dropping frame")
return None return None
@@ -327,13 +327,13 @@ def main(demo=False):
setproctitle(PROCESS_NAME) setproctitle(PROCESS_NAME)
config_realtime_process(7, 54) config_realtime_process(7, 54)
CHESTNUT = chestnut_present() USBGPU = usbgpu_present()
if CHESTNUT: if USBGPU:
os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000' os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000'
params = Params() params = Params()
params.put_bool("ChestnutLoading", CHESTNUT) params.put_bool("UsbGpuLoading", USBGPU)
params.remove("ChestnutActive") params.remove("UsbGpuActive")
# visionipc clients # visionipc clients
while True: while True:
@@ -362,31 +362,31 @@ def main(demo=False):
st = time.monotonic() st = time.monotonic()
model = None model = None
if CHESTNUT: if USBGPU:
import threading import threading
def load(): def load():
nonlocal model nonlocal model
model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, chestnut=True) model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, usbgpu=True)
t = threading.Thread(target=load, daemon=True) t = threading.Thread(target=load, daemon=True)
t.start() t.start()
t.join(60) t.join(60)
if model is None: if model is None:
params.put_bool("ChestnutActive", False) params.put_bool("UsbGpuActive", False)
raise RuntimeError("chestnut model load failed or timed out (60s)") raise RuntimeError("eGPU model load failed or timed out (60s)")
params.put_bool("ChestnutActive", True) params.put_bool("UsbGpuActive", True)
else: else:
model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, chestnut=False) model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, usbgpu=False)
params.put_bool("ChestnutLoading", False) params.put_bool("UsbGpuLoading", False)
cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting") cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting")
# messaging # messaging
pub_socks = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"] + (["chestnutState"] if CHESTNUT else []) pub_socks = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"] + (["chestnutState"] if USBGPU else [])
pm = PubMaster(pub_socks) pm = PubMaster(pub_socks)
sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"]) sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"])
publish_state = PublishState() publish_state = PublishState()
chestnut_state = ChestnutState(pm, CHESTNUT) if CHESTNUT else None chestnut_state = ChestnutState(pm, USBGPU) if USBGPU else None
# setup filter to track dropped frames # setup filter to track dropped frames
frame_dropped_filter = FirstOrderFilter(0., 10., 1. / model.constants.MODEL_FREQ) frame_dropped_filter = FirstOrderFilter(0., 10., 1. / model.constants.MODEL_FREQ)
@@ -524,7 +524,7 @@ def main(demo=False):
fill_model_msg(drivingdata_send, modelv2_send, model_output, action, fill_model_msg(drivingdata_send, modelv2_send, model_output, action,
publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id,
frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, live_calib_seen, meta_constants) frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, live_calib_seen, meta_constants)
modelv2_send.modelV2.big = model.chestnut modelv2_send.modelV2.big = model.usbgpu
desire_state = modelv2_send.modelV2.meta.desireState desire_state = modelv2_send.modelV2.meta.desireState
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft] l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
@@ -190,8 +190,8 @@ def tmp_path():
def patch_modeld(monkeypatch): def patch_modeld(monkeypatch):
def _patch(bundle): def _patch(bundle):
monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None, *, chestnut=None: bundle) monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None, *, usbgpu=None: bundle)
monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None, *, chestnut=None: bundle) monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None, *, usbgpu=None: bundle)
return _patch return _patch
@@ -59,8 +59,8 @@ class TestFindDrivingPkl(OpenpilotTestCase):
class TestModelStateCombinedInit(OpenpilotTestCase): class TestModelStateCombinedInit(OpenpilotTestCase):
def test_asserts_when_no_pkl(self, monkeypatch): def test_asserts_when_no_pkl(self, monkeypatch):
bundle = DummyBundle(models=[], is_20hz=True) bundle = DummyBundle(models=[], is_20hz=True)
monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None, *, chestnut=None: bundle) monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None, *, usbgpu=None: bundle)
monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None, *, chestnut=None: bundle) monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None, *, usbgpu=None: bundle)
with self.assertRaisesRegex(AssertionError, "No driving pkl found"): with self.assertRaisesRegex(AssertionError, "No driving pkl found"):
ModelState(cam_w=CAM_W, cam_h=CAM_H) ModelState(cam_w=CAM_W, cam_h=CAM_H)
+2 -2
View File
@@ -9,8 +9,8 @@ from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL, DEFAULT_BIG_MO
def get_default_model() -> str: def get_default_model() -> str:
show_big_model = (ui_state.chestnut_present show_big_model = (ui_state.usbgpu
and (ui_state.chestnut_active or ui_state.chestnut_loading or ui_state.is_offroad())) 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 return DEFAULT_BIG_MODEL if show_big_model else DEFAULT_MODEL
+7 -7
View File
@@ -139,11 +139,11 @@ 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_v21.json" MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v21.json"
MODEL_URL_CHESTNUT = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_chestnut_v22.json" MODEL_URL_USBGPU = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_usbgpu_v22.json"
MODEL_SOURCES = { MODEL_SOURCES = {
"qcom": (MODEL_URL, ""), "qcom": (MODEL_URL, ""),
"chestnut": (MODEL_URL_CHESTNUT, "_Chestnut"), "usbgpu": (MODEL_URL_USBGPU, "_USBGPU"),
} }
def __init__(self, params: Params): def __init__(self, params: Params):
@@ -156,12 +156,12 @@ class ModelFetcher:
self._refetched: set[str] = set() self._refetched: set[str] = set()
self.params.put("ModelManager_ActiveJson", { self.params.put("ModelManager_ActiveJson", {
"qcom": self.MODEL_URL, "qcom": self.MODEL_URL,
"chestnut": self.MODEL_URL_CHESTNUT, "usbgpu": self.MODEL_URL_USBGPU,
}, block=True) }, block=True)
@staticmethod @staticmethod
def active_source(chestnut_present: bool) -> str: def active_source(chestnut_present: bool) -> str:
return "chestnut" if chestnut_present else "qcom" return "usbgpu" if chestnut_present else "qcom"
def _fetch_and_cache_models(self, source: str) -> list[custom.ModelManagerSP.ModelBundle] | None: def _fetch_and_cache_models(self, source: str) -> list[custom.ModelManagerSP.ModelBundle] | None:
"""Fetches fresh model data from remote and updates cache. """Fetches fresh model data from remote and updates cache.
@@ -200,7 +200,7 @@ class ModelFetcher:
@staticmethod @staticmethod
def _cache_matches_source(source: str, cached_data: dict) -> bool: def _cache_matches_source(source: str, cached_data: dict) -> bool:
bundles = cached_data.get("bundles", []) bundles = cached_data.get("bundles", [])
if source == "chestnut": if source == "usbgpu":
return any(bundle.get("is_big") is True for bundle in bundles) return any(bundle.get("is_big") is True for bundle in bundles)
return not any(bundle.get("is_big") is True for bundle in bundles) return not any(bundle.get("is_big") is True for bundle in bundles)
@@ -261,10 +261,10 @@ def get_cached_bundles(params: Params, source: str) -> list[custom.ModelManagerS
if __name__ == "__main__": if __name__ == "__main__":
from openpilot.selfdrive.modeld.helpers import chestnut_present from openpilot.selfdrive.modeld.helpers import usbgpu_present
params = Params() params = Params()
model_fetcher = ModelFetcher(params) model_fetcher = ModelFetcher(params)
bundles = model_fetcher.get_bundles_for_source(ModelFetcher.active_source(chestnut_present())) bundles = model_fetcher.get_bundles_for_source(ModelFetcher.active_source(usbgpu_present()))
for bundle in bundles: for bundle in bundles:
for model in bundle.models: for model in bundle.models:
model_overrides = {override.key: override.value for override in bundle.overrides} model_overrides = {override.key: override.value for override in bundle.overrides}
+13 -18
View File
@@ -16,7 +16,7 @@ from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.sunnypilot.models.constants import Meta, MetaSimPose, MetaTombRaider from openpilot.sunnypilot.models.constants import Meta, MetaSimPose, MetaTombRaider
from openpilot.common.hardware.hw import Paths from openpilot.common.hardware.hw import Paths
from openpilot.selfdrive.modeld.helpers import chestnut_present from openpilot.selfdrive.modeld.helpers import usbgpu_present
# SET ME TO THE EXACT JSON VERSION WE SET IN SUNNYPILOT_MODELS REPO # SET ME TO THE EXACT JSON VERSION WE SET IN SUNNYPILOT_MODELS REPO
REQUIRED_JSON_VERSION = 18 REQUIRED_JSON_VERSION = 18
@@ -27,7 +27,7 @@ ModelManager = custom.ModelManagerSP
ACTIVE_BUNDLE_KEYS = { ACTIVE_BUNDLE_KEYS = {
"qcom": "ModelManager_ActiveBundle", "qcom": "ModelManager_ActiveBundle",
"chestnut": "ModelManager_ActiveBundleChestnut", "usbgpu": "ModelManager_ActiveBundleUSBGPU",
} }
_LAST_VALIDATED_RAW: dict[str, dict | None] = {} _LAST_VALIDATED_RAW: dict[str, dict | None] = {}
@@ -41,12 +41,7 @@ def _compute_hash(file_path: str) -> str | None:
return None return None
async def verify_file(file_path: str, expected_hash: str) -> bool: def verify_file(file_path: str, expected_hash: str) -> bool:
file_hash = _compute_hash(file_path)
return file_hash == expected_hash.lower() if file_hash else False
def _verify_file(file_path: str, expected_hash: str) -> bool:
file_hash = _compute_hash(file_path) file_hash = _compute_hash(file_path)
return file_hash == expected_hash.lower() if file_hash else False return file_hash == expected_hash.lower() if file_hash else False
@@ -81,7 +76,7 @@ def _bundle_artifacts(bundle: custom.ModelManagerSP.ModelBundle) -> list[tuple[s
def _bundle_is_valid_locally(bundle: custom.ModelManagerSP.ModelBundle) -> bool: def _bundle_is_valid_locally(bundle: custom.ModelManagerSP.ModelBundle) -> bool:
model_root = Paths.model_root() model_root = Paths.model_root()
return all(_verify_file(os.path.join(model_root, file_name), expected_hash) return all(verify_file(os.path.join(model_root, file_name), expected_hash)
for file_name, expected_hash in _bundle_artifacts(bundle)) for file_name, expected_hash in _bundle_artifacts(bundle))
@@ -126,20 +121,20 @@ def get_selected_bundle(params: Params | None = None, source: str = "qcom") -> "
return _parse_active_bundle(params.get(ACTIVE_BUNDLE_KEYS[source])) return _parse_active_bundle(params.get(ACTIVE_BUNDLE_KEYS[source]))
def get_active_source(chestnut: bool | None = None, chestnut_active: bool | None = None, def get_active_source(usbgpu: bool | None = None, usbgpu_active: bool | None = None,
chestnut_loading: bool | None = None, offroad: bool | None = None) -> str: usbgpu_loading: bool | None = None, offroad: bool | None = None) -> str:
if chestnut is None: if usbgpu is None:
chestnut = chestnut_present() usbgpu = usbgpu_present()
state_valid = chestnut_active is not None or chestnut_loading is not None or offroad is not None state_valid = usbgpu_active is not None or usbgpu_loading is not None or offroad is not None
big_active = chestnut and (not state_valid or chestnut_active or chestnut_loading or offroad) big_active = usbgpu and (not state_valid or usbgpu_active or usbgpu_loading or offroad)
return "chestnut" if big_active else "qcom" return "usbgpu" if big_active else "qcom"
def get_active_bundle(params: Params | None = None, *, chestnut: bool | None = None) -> "custom.ModelManagerSP.ModelBundle | None": def get_active_bundle(params: Params | None = None, *, usbgpu: bool | None = None) -> "custom.ModelManagerSP.ModelBundle | None":
# no cross-slot fallback: an empty active slot means the hardware default, which # no cross-slot fallback: an empty active slot means the hardware default, which
# only stock modeld can run - modeld_v2 requires a real bundle # only stock modeld can run - modeld_v2 requires a real bundle
params = params or Params() params = params or Params()
return get_selected_bundle(params, get_active_source(chestnut=chestnut)) return get_selected_bundle(params, get_active_source(usbgpu=usbgpu))
def resolve_bundle_by_ref( def resolve_bundle_by_ref(
+48 -23
View File
@@ -10,6 +10,7 @@ import os
import time import time
import requests import requests
from openpilot.common.file_chunker import get_chunk_name
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.common.realtime import Ratekeeper from openpilot.common.realtime import Ratekeeper
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
@@ -22,6 +23,8 @@ from openpilot.sunnypilot.models.helpers import (ACTIVE_BUNDLE_KEYS, get_active_
# (connect, read) seconds. read is per-request inactivity, not a total cap # (connect, read) seconds. read is per-request inactivity, not a total cap
DOWNLOAD_TIMEOUT = (30, 30) DOWNLOAD_TIMEOUT = (30, 30)
# how many download+verify rounds before giving up on a chunk that won't verify
MAX_CHUNK_VERIFY_ATTEMPTS = 3
class DownloadCancelled(Exception): class DownloadCancelled(Exception):
@@ -40,7 +43,7 @@ class ModelManagerSP:
self.available_models: list[custom.ModelManagerSP.ModelBundle] = [] self.available_models: list[custom.ModelManagerSP.ModelBundle] = []
self.source_models: dict[str, list[custom.ModelManagerSP.ModelBundle]] = {} self.source_models: dict[str, list[custom.ModelManagerSP.ModelBundle]] = {}
self.selected_bundle: custom.ModelManagerSP.ModelBundle = None self.selected_bundle: custom.ModelManagerSP.ModelBundle = None
self.active_bundle: custom.ModelManagerSP.ModelBundle = get_active_bundle(self.params, chestnut=self.chestnut_present) self.active_bundle: custom.ModelManagerSP.ModelBundle = get_active_bundle(self.params, usbgpu=self.chestnut_present)
self._chunk_size = 128 * 1000 # 128 KB chunks self._chunk_size = 128 * 1000 # 128 KB chunks
self._download_start_times: dict[str, float] = {} # Track start time per model self._download_start_times: dict[str, float] = {} # Track start time per model
self._download_ref: bytes | str | None = None self._download_ref: bytes | str | None = None
@@ -153,6 +156,35 @@ class ModelManagerSP:
os.remove(base_path) os.remove(base_path)
del self._download_start_times[artifact.fileName] del self._download_start_times[artifact.fileName]
async def _verify_chunks_parallel(self, artifact, full_path: str) -> set[int]:
num_chunks = len(artifact.chunks)
start = time.monotonic()
futures = []
chunk_index: dict[asyncio.Future, int] = {}
for i, chunk in enumerate(artifact.chunks):
fut = asyncio.ensure_future(asyncio.to_thread(verify_file, get_chunk_name(full_path, i, num_chunks), chunk.sha256))
futures.append(fut)
chunk_index[fut] = i
valid_chunks: set[int] = set()
pending = set(futures)
while pending:
if self._download_interrupted():
for t in pending:
t.cancel()
raise DownloadCancelled("Download cancelled")
done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED)
for fut in done:
if fut.result():
valid_chunks.add(chunk_index[fut])
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.verifying
artifact.downloadProgress.progress = (len(valid_chunks) / num_chunks) * 100
self._sync_artifact_progress(artifact)
self._report_status()
elapsed = time.monotonic() - start
cloudlog.info(f"Verified {len(valid_chunks)}/{num_chunks} chunks of {artifact.fileName} in {elapsed:.2f}s")
return valid_chunks
async def _process_artifact(self, artifact, destination_path: str) -> None: async def _process_artifact(self, artifact, destination_path: str) -> None:
if not artifact.downloadUri.uri: if not artifact.downloadUri.uri:
return None return None
@@ -170,20 +202,10 @@ class ModelManagerSP:
is_cached = False is_cached = False
valid_chunks: set[int] = set() valid_chunks: set[int] = set()
if len(artifact.chunks) > 0: if len(artifact.chunks) > 0:
from openpilot.common.file_chunker import get_chunk_name valid_chunks = await self._verify_chunks_parallel(artifact, full_path)
num_chunks = len(artifact.chunks) is_cached = len(valid_chunks) == len(artifact.chunks)
for i, chunk in enumerate(artifact.chunks):
if self._download_interrupted():
raise DownloadCancelled("Download cancelled")
if await verify_file(get_chunk_name(full_path, i, num_chunks), chunk.sha256):
valid_chunks.add(i)
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.verifying
artifact.downloadProgress.progress = (len(valid_chunks) / num_chunks) * 100
self._sync_artifact_progress(artifact)
self._report_status()
is_cached = len(valid_chunks) == num_chunks
else: else:
if await verify_file(full_path, expected_hash): if verify_file(full_path, expected_hash):
is_cached = True is_cached = True
if is_cached: if is_cached:
@@ -195,15 +217,18 @@ class ModelManagerSP:
return return
if len(artifact.chunks) > 0: if len(artifact.chunks) > 0:
await self._download_chunked(url, full_path, artifact, skip=valid_chunks) attempts = 0
from openpilot.common.file_chunker import get_chunk_name while len(valid_chunks) < len(artifact.chunks):
for i, chunk in enumerate(artifact.chunks): attempts += 1
chunk_path = get_chunk_name(full_path, i, len(artifact.chunks)) cloudlog.warning(f"Re-downloading {len(artifact.chunks) - len(valid_chunks)} invalid chunk(s) of {filename} (attempt {attempts})")
if not await verify_file(chunk_path, chunk.sha256): await self._download_chunked(url, full_path, artifact, skip=valid_chunks)
raise ValueError(f"Hash validation failed for chunk {i+1} of {filename}") valid_chunks = await self._verify_chunks_parallel(artifact, full_path)
if len(valid_chunks) < len(artifact.chunks) and attempts >= MAX_CHUNK_VERIFY_ATTEMPTS:
missing = next(i for i in range(len(artifact.chunks)) if i not in valid_chunks)
raise ValueError(f"Hash validation failed for chunk {missing+1} of {filename} after {attempts} attempts")
else: else:
await self._download_file(url, full_path, artifact) await self._download_file(url, full_path, artifact)
if not await verify_file(full_path, expected_hash): if not verify_file(full_path, expected_hash):
raise ValueError(f"Hash validation failed for {filename}") raise ValueError(f"Hash validation failed for {filename}")
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloaded artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloaded
@@ -280,7 +305,7 @@ class ModelManagerSP:
raise DownloadCancelled("Download cancelled") raise DownloadCancelled("Download cancelled")
self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.downloaded self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.downloaded
self.params.put(ACTIVE_BUNDLE_KEYS[source], model_bundle.to_dict(), block=True) self.params.put(ACTIVE_BUNDLE_KEYS[source], model_bundle.to_dict(), block=True)
self.active_bundle = get_active_bundle(self.params, chestnut=self.chestnut_present) self.active_bundle = get_active_bundle(self.params, usbgpu=self.chestnut_present)
except Exception: except Exception:
if self.selected_bundle is not None: if self.selected_bundle is not None:
@@ -326,7 +351,7 @@ class ModelManagerSP:
self.source_models = {source: self.model_fetcher.get_bundles_for_source(source) for source in ModelFetcher.MODEL_SOURCES} self.source_models = {source: self.model_fetcher.get_bundles_for_source(source) for source in ModelFetcher.MODEL_SOURCES}
self.available_models = self.source_models[ModelFetcher.active_source(self.chestnut_present)] self.available_models = self.source_models[ModelFetcher.active_source(self.chestnut_present)]
validate_active_bundles(self.params, self.source_models) validate_active_bundles(self.params, self.source_models)
self.active_bundle = get_active_bundle(self.params, chestnut=self.chestnut_present) self.active_bundle = get_active_bundle(self.params, usbgpu=self.chestnut_present)
self._process_download_requests() self._process_download_requests()
@@ -392,7 +392,7 @@ class TestManagerDownload(ManagerDownloadTestBase):
asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "qcom")) asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "qcom"))
assert "ModelManager_ActiveBundle" in store, "qcom download must write the qcom slot" assert "ModelManager_ActiveBundle" in store, "qcom download must write the qcom slot"
assert "ModelManager_ActiveBundleChestnut" not in store, "qcom download must not touch the chestnut slot" assert "ModelManager_ActiveBundleUSBGPU" not in store, "qcom download must not touch the usbgpu slot"
assert self.manager.selected_bundle.status == custom.ModelManagerSP.DownloadStatus.downloaded assert self.manager.selected_bundle.status == custom.ModelManagerSP.DownloadStatus.downloaded
assert self.manager.active_bundle is not None and self.manager.active_bundle.ref == "test-ref" assert self.manager.active_bundle is not None and self.manager.active_bundle.ref == "test-ref"
assert self.manager.active_bundle.status == custom.ModelManagerSP.DownloadStatus.downloaded assert self.manager.active_bundle.status == custom.ModelManagerSP.DownloadStatus.downloaded
@@ -401,18 +401,18 @@ class TestManagerDownload(ManagerDownloadTestBase):
assert missing == [], f"chunks missing from the cache: {missing}" assert missing == [], f"chunks missing from the cache: {missing}"
self.run_with_server(body) self.run_with_server(body)
def test_download_writes_chestnut_slot(self): def test_download_writes_usbgpu_slot(self):
"""A download resolved to the chestnut source writes the chestnut active bundle slot only.""" """A download resolved to the usbgpu source writes the usbgpu active bundle slot only."""
def body(): def body():
self.make_artifact(chunked=True) self.make_artifact(chunked=True)
self._bundle.ref = "big-ref" self._bundle.ref = "big-ref"
self._bundle.minimumSelectorVersion = 18 self._bundle.minimumSelectorVersion = 18
params, store = self._make_params_with_store() params, store = self._make_params_with_store()
self.manager.params = params self.manager.params = params
asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "chestnut")) asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "usbgpu"))
assert "ModelManager_ActiveBundleChestnut" in store, "chestnut download must write the chestnut slot" assert "ModelManager_ActiveBundleUSBGPU" in store, "usbgpu download must write the usbgpu slot"
assert "ModelManager_ActiveBundle" not in store, "chestnut download must not touch the qcom slot" assert "ModelManager_ActiveBundle" not in store, "usbgpu download must not touch the qcom slot"
assert self.manager.selected_bundle.status == custom.ModelManagerSP.DownloadStatus.downloaded assert self.manager.selected_bundle.status == custom.ModelManagerSP.DownloadStatus.downloaded
self.run_with_server(body) self.run_with_server(body)
@@ -447,20 +447,20 @@ class TestResolveBundleByRef(OpenpilotTestCase):
def test_qcom_ref_resolves_to_qcom_slot(self): def test_qcom_ref_resolves_to_qcom_slot(self):
small = self._bundle("small") small = self._bundle("small")
assert resolve_bundle_by_ref("small", {"qcom": [small], "chestnut": []}) == (small, "qcom") assert resolve_bundle_by_ref("small", {"qcom": [small], "usbgpu": []}) == (small, "qcom")
def test_chestnut_ref_resolves_to_chestnut_slot(self): def test_usbgpu_ref_resolves_to_usbgpu_slot(self):
big = self._bundle("big") big = self._bundle("big")
assert resolve_bundle_by_ref("big", {"qcom": [], "chestnut": [big]}) == (big, "chestnut") assert resolve_bundle_by_ref("big", {"qcom": [], "usbgpu": [big]}) == (big, "usbgpu")
def test_unknown_ref_returns_none(self): def test_unknown_ref_returns_none(self):
source_bundles = {"qcom": [self._bundle("small")], "chestnut": []} source_bundles = {"qcom": [self._bundle("small")], "usbgpu": []}
assert resolve_bundle_by_ref("nope", source_bundles) is None assert resolve_bundle_by_ref("nope", source_bundles) is None
def manifest_bundle(short_name: str, ref: str, index: int = 0, is_big: bool = False) -> dict: def manifest_bundle(short_name: str, ref: str, index: int = 0, is_big: bool = False) -> dict:
"""Minimal manifest bundle dict, version-compatible (no chunks to avoid disk side effects). """Minimal manifest bundle dict, version-compatible (no chunks to avoid disk side effects).
Big (chestnut) bundles carry `is_big: true` in the manifest JSON.""" Big (usbgpu) bundles carry `is_big: true` in the manifest JSON."""
return { return {
"index": index, "index": index,
"short_name": short_name, "short_name": short_name,
@@ -489,15 +489,15 @@ class TestModelFetcherSources(OpenpilotTestCase):
"""Both manifests are always maintained: get_bundles_for_source exposes either """Both manifests are always maintained: get_bundles_for_source exposes either
source by name, and active_source picks which one matches the attached hardware.""" source by name, and active_source picks which one matches the attached hardware."""
def _make_params(self, qcom_manifest, chestnut_manifest): def _make_params(self, qcom_manifest, usbgpu_manifest):
params = mock.MagicMock() params = mock.MagicMock()
def get(key): def get(key):
if key == "ModelManager_ModelsCache": if key == "ModelManager_ModelsCache":
return qcom_manifest return qcom_manifest
if key == "ModelManager_ModelsCache_Chestnut": if key == "ModelManager_ModelsCache_USBGPU":
return chestnut_manifest return usbgpu_manifest
if key in ("ModelManager_LastSyncTime", "ModelManager_LastSyncTime_Chestnut"): if key in ("ModelManager_LastSyncTime", "ModelManager_LastSyncTime_USBGPU"):
return fresh_sync_time() return fresh_sync_time()
return None return None
@@ -506,14 +506,14 @@ class TestModelFetcherSources(OpenpilotTestCase):
def test_active_source_follows_chestnut_presence(self): def test_active_source_follows_chestnut_presence(self):
assert ModelFetcher.active_source(False) == "qcom" assert ModelFetcher.active_source(False) == "qcom"
assert ModelFetcher.active_source(True) == "chestnut" assert ModelFetcher.active_source(True) == "usbgpu"
def test_get_bundles_for_source_returns_each_source(self): def test_get_bundles_for_source_returns_each_source(self):
params = self._make_params({"bundles": [manifest_bundle("small", "aaa")]}, params = self._make_params({"bundles": [manifest_bundle("small", "aaa")]},
{"bundles": [manifest_bundle("big", "bbb", is_big=True)]}) {"bundles": [manifest_bundle("big", "bbb", is_big=True)]})
fetcher = ModelFetcher(params) fetcher = ModelFetcher(params)
assert [bundle.ref for bundle in fetcher.get_bundles_for_source("qcom")] == ["aaa"] assert [bundle.ref for bundle in fetcher.get_bundles_for_source("qcom")] == ["aaa"]
assert [bundle.ref for bundle in fetcher.get_bundles_for_source("chestnut")] == ["bbb"] assert [bundle.ref for bundle in fetcher.get_bundles_for_source("usbgpu")] == ["bbb"]
def test_get_bundles_for_source_unknown(self): def test_get_bundles_for_source_unknown(self):
assert ModelFetcher(mock.MagicMock()).get_bundles_for_source("bogus") == [] assert ModelFetcher(mock.MagicMock()).get_bundles_for_source("bogus") == []
@@ -522,16 +522,16 @@ class TestModelFetcherSources(OpenpilotTestCase):
params = self._make_params({"bundles": [manifest_bundle("small", "aaa")]}, params = self._make_params({"bundles": [manifest_bundle("small", "aaa")]},
{"bundles": [manifest_bundle("big", "bbb", is_big=True)]}) {"bundles": [manifest_bundle("big", "bbb", is_big=True)]})
qcom_bundles = get_cached_bundles(params, "qcom") qcom_bundles = get_cached_bundles(params, "qcom")
chestnut_bundles = get_cached_bundles(params, "chestnut") usbgpu_bundles = get_cached_bundles(params, "usbgpu")
assert [b.ref for b in qcom_bundles] == ["aaa"] assert [b.ref for b in qcom_bundles] == ["aaa"]
assert [b.ref for b in chestnut_bundles] == ["bbb"] assert [b.ref for b in usbgpu_bundles] == ["bbb"]
assert qcom_bundles[0].displayName == "SMALL" assert qcom_bundles[0].displayName == "SMALL"
def test_get_cached_bundles_empty_when_missing(self): def test_get_cached_bundles_empty_when_missing(self):
params = mock.MagicMock() params = mock.MagicMock()
params.get.return_value = None params.get.return_value = None
assert get_cached_bundles(params, "qcom") == [] assert get_cached_bundles(params, "qcom") == []
assert get_cached_bundles(params, "chestnut") == [] assert get_cached_bundles(params, "usbgpu") == []
def test_get_cached_bundles_unknown_source(self): def test_get_cached_bundles_unknown_source(self):
assert get_cached_bundles(mock.MagicMock(), "bogus") == [] assert get_cached_bundles(mock.MagicMock(), "bogus") == []
@@ -543,27 +543,27 @@ class TestModelFetcherSources(OpenpilotTestCase):
assert active_json_calls, "expected ModelManager_ActiveJson to be written" assert active_json_calls, "expected ModelManager_ActiveJson to be written"
assert active_json_calls[-1].args[1] == { assert active_json_calls[-1].args[1] == {
"qcom": ModelFetcher.MODEL_URL, "qcom": ModelFetcher.MODEL_URL,
"chestnut": ModelFetcher.MODEL_URL_CHESTNUT, "usbgpu": ModelFetcher.MODEL_URL_USBGPU,
} }
class TestSourceCacheIntegrity(OpenpilotTestCase): class TestSourceCacheIntegrity(OpenpilotTestCase):
"""Each source's cached manifest must contain only that source's models; the """Each source's cached manifest must contain only that source's models; the
`is_big` flag in the JSON marks the big (chestnut) models. A mismatched cache is `is_big` flag in the JSON marks the big (usbgpu) models. A mismatched cache is
legacy data from before the per-source split (the active manifest was cached legacy data from before the per-source split (the active manifest was cached
under the unsuffixed key regardless of hardware) and is refetched. This under the unsuffixed key regardless of hardware) and is refetched. This
replaces the old one-time bundle migration.""" replaces the old one-time bundle migration."""
def _make_params(self, qcom_manifest, chestnut_manifest): def _make_params(self, qcom_manifest, usbgpu_manifest):
params = mock.MagicMock() params = mock.MagicMock()
def get(key): def get(key):
if key == "ModelManager_ModelsCache": if key == "ModelManager_ModelsCache":
return qcom_manifest return qcom_manifest
if key == "ModelManager_ModelsCache_Chestnut": if key == "ModelManager_ModelsCache_USBGPU":
return chestnut_manifest return usbgpu_manifest
if key in ("ModelManager_LastSyncTime", "ModelManager_LastSyncTime_Chestnut"): if key in ("ModelManager_LastSyncTime", "ModelManager_LastSyncTime_USBGPU"):
return fresh_sync_time() return fresh_sync_time()
return None return None
@@ -584,13 +584,13 @@ class TestSourceCacheIntegrity(OpenpilotTestCase):
bundles = fetcher.get_bundles_for_source("qcom") bundles = fetcher.get_bundles_for_source("qcom")
assert [bundle.ref for bundle in bundles] == ["aaa"] assert [bundle.ref for bundle in bundles] == ["aaa"]
def test_chestnut_cache_without_big_models_is_refetched(self): def test_usbgpu_cache_without_big_models_is_refetched(self):
params = self._make_params({"bundles": [manifest_bundle("small", "aaa")]}, params = self._make_params({"bundles": [manifest_bundle("small", "aaa")]},
{"bundles": [manifest_bundle("big2", "ccc")]}) {"bundles": [manifest_bundle("big2", "ccc")]})
fetcher = ModelFetcher(params) fetcher = ModelFetcher(params)
fetched = self._fetched(manifest_bundle("big", "bbb", is_big=True)) fetched = self._fetched(manifest_bundle("big", "bbb", is_big=True))
with mock.patch.object(fetcher, "_fetch_and_cache_models", return_value=fetched): with mock.patch.object(fetcher, "_fetch_and_cache_models", return_value=fetched):
bundles = fetcher.get_bundles_for_source("chestnut") bundles = fetcher.get_bundles_for_source("usbgpu")
assert [bundle.ref for bundle in bundles] == ["bbb"] assert [bundle.ref for bundle in bundles] == ["bbb"]
def test_matching_caches_are_used_without_fetch(self): def test_matching_caches_are_used_without_fetch(self):
@@ -599,7 +599,7 @@ class TestSourceCacheIntegrity(OpenpilotTestCase):
fetcher = ModelFetcher(params) fetcher = ModelFetcher(params)
with mock.patch.object(fetcher, "_fetch_and_cache_models", side_effect=AssertionError("cache should be used")): with mock.patch.object(fetcher, "_fetch_and_cache_models", side_effect=AssertionError("cache should be used")):
assert [bundle.ref for bundle in fetcher.get_bundles_for_source("qcom")] == ["aaa"] assert [bundle.ref for bundle in fetcher.get_bundles_for_source("qcom")] == ["aaa"]
assert [bundle.ref for bundle in fetcher.get_bundles_for_source("chestnut")] == ["bbb"] assert [bundle.ref for bundle in fetcher.get_bundles_for_source("usbgpu")] == ["bbb"]
def test_stale_version_cache_is_refetched(self): def test_stale_version_cache_is_refetched(self):
"""A source-matching cache whose bundles are all filtered by the selector version """A source-matching cache whose bundles are all filtered by the selector version
@@ -660,28 +660,28 @@ class TestActiveBundleValidation(OpenpilotTestCase):
bundle.runner = runner bundle.runner = runner
return bundle.to_dict() return bundle.to_dict()
def _params(self, qcom=None, chestnut=None): def _params(self, qcom=None, usbgpu=None):
params = mock.MagicMock() params = mock.MagicMock()
def get(key, *args, **kwargs): def get(key, *args, **kwargs):
return {"ModelManager_ActiveBundle": qcom, "ModelManager_ActiveBundleChestnut": chestnut}.get(key) return {"ModelManager_ActiveBundle": qcom, "ModelManager_ActiveBundleUSBGPU": usbgpu}.get(key)
params.get.side_effect = get params.get.side_effect = get
return params return params
def test_empty_catalog_does_not_reset_slot(self): def test_empty_catalog_does_not_reset_slot(self):
params = self._params(qcom=self._raw_bundle("small")) params = self._params(qcom=self._raw_bundle("small"))
with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=False): with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=False):
validate_active_bundles(params, {"qcom": [], "chestnut": []}) validate_active_bundles(params, {"qcom": [], "usbgpu": []})
params.remove.assert_not_called() params.remove.assert_not_called()
def test_reset_recomputes_runner_from_surviving_slot(self): def test_reset_recomputes_runner_from_surviving_slot(self):
tinygrad = int(custom.ModelManagerSP.Runner.tinygrad) tinygrad = int(custom.ModelManagerSP.Runner.tinygrad)
big_raw = self._raw_bundle("big", runner=tinygrad) big_raw = self._raw_bundle("big", runner=tinygrad)
params = self._params(qcom=self._raw_bundle("gone"), chestnut=big_raw) params = self._params(qcom=self._raw_bundle("gone"), usbgpu=big_raw)
catalog = {"qcom": [custom.ModelManagerSP.ModelBundle(**self._raw_bundle("other"))], catalog = {"qcom": [custom.ModelManagerSP.ModelBundle(**self._raw_bundle("other"))],
"chestnut": [custom.ModelManagerSP.ModelBundle(**big_raw)]} "usbgpu": [custom.ModelManagerSP.ModelBundle(**big_raw)]}
with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=True): with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=True):
validate_active_bundles(params, catalog) validate_active_bundles(params, catalog)
params.remove.assert_called_once_with("ModelManager_ActiveBundle") params.remove.assert_called_once_with("ModelManager_ActiveBundle")
runner_puts = [call for call in params.put.call_args_list if call.args[0] == "ModelRunnerTypeCache"] runner_puts = [call for call in params.put.call_args_list if call.args[0] == "ModelRunnerTypeCache"]
@@ -689,7 +689,7 @@ class TestActiveBundleValidation(OpenpilotTestCase):
class TestActiveBundleSelection(OpenpilotTestCase): class TestActiveBundleSelection(OpenpilotTestCase):
"""The effective active bundle is the active source's slot: chestnut when a GPU is """The effective active bundle is the active source's slot: usbgpu when a GPU is
present, qcom otherwise. An empty active slot means the hardware default (stock present, qcom otherwise. An empty active slot means the hardware default (stock
runner), never the other slot's pick - modeld_v2 requires a real bundle.""" runner), never the other slot's pick - modeld_v2 requires a real bundle."""
@@ -700,37 +700,37 @@ class TestActiveBundleSelection(OpenpilotTestCase):
bundle.minimumSelectorVersion = 18 bundle.minimumSelectorVersion = 18
return bundle.to_dict() return bundle.to_dict()
def _params(self, qcom=None, chestnut=None): def _params(self, qcom=None, usbgpu=None):
params = mock.MagicMock() params = mock.MagicMock()
def get(key, *args, **kwargs): def get(key, *args, **kwargs):
if key == "ModelManager_ActiveBundle": if key == "ModelManager_ActiveBundle":
return qcom return qcom
if key == "ModelManager_ActiveBundleChestnut": if key == "ModelManager_ActiveBundleUSBGPU":
return chestnut return usbgpu
return None return None
params.get.side_effect = get params.get.side_effect = get
return params return params
def test_selected_bundle_is_per_slot(self): def test_selected_bundle_is_per_slot(self):
params = self._params(qcom=self._raw_bundle("small"), chestnut=self._raw_bundle("big")) params = self._params(qcom=self._raw_bundle("small"), usbgpu=self._raw_bundle("big"))
assert get_selected_bundle(params, "qcom").ref == "small" assert get_selected_bundle(params, "qcom").ref == "small"
assert get_selected_bundle(params, "chestnut").ref == "big" assert get_selected_bundle(params, "usbgpu").ref == "big"
def test_no_gpu_uses_qcom_slot(self): def test_no_gpu_uses_qcom_slot(self):
params = self._params(qcom=self._raw_bundle("small"), chestnut=self._raw_bundle("big")) params = self._params(qcom=self._raw_bundle("small"), usbgpu=self._raw_bundle("big"))
with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=False): with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=False):
assert get_active_bundle(params).ref == "small" assert get_active_bundle(params).ref == "small"
def test_gpu_uses_chestnut_slot(self): def test_gpu_uses_usbgpu_slot(self):
params = self._params(qcom=self._raw_bundle("small"), chestnut=self._raw_bundle("big")) params = self._params(qcom=self._raw_bundle("small"), usbgpu=self._raw_bundle("big"))
with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=True): with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=True):
assert get_active_bundle(params).ref == "big" assert get_active_bundle(params).ref == "big"
def test_gpu_without_big_selection_is_hardware_default(self): def test_gpu_without_big_selection_is_hardware_default(self):
params = self._params(qcom=self._raw_bundle("small"), chestnut=None) params = self._params(qcom=self._raw_bundle("small"), usbgpu=None)
with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=True): with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=True):
assert get_active_bundle(params) is None assert get_active_bundle(params) is None
@@ -748,36 +748,36 @@ class TestEffectiveSource(OpenpilotTestCase):
return bundle.to_dict() return bundle.to_dict()
def test_runtime_no_gpu(self): def test_runtime_no_gpu(self):
with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=False): with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=False):
assert get_active_source() == "qcom" assert get_active_source() == "qcom"
def test_runtime_gpu_present(self): def test_runtime_gpu_present(self):
with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=True): with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=True):
assert get_active_source() == "chestnut" assert get_active_source() == "usbgpu"
def test_display_offroad_gpu_present_shows_big(self): def test_display_offroad_gpu_present_shows_big(self):
assert get_active_source(chestnut=True, chestnut_active=False, chestnut_loading=False, offroad=True) == "chestnut" assert get_active_source(usbgpu=True, usbgpu_active=False, usbgpu_loading=False, offroad=True) == "usbgpu"
def test_display_onroad_gpu_loading_shows_big(self): def test_display_onroad_gpu_loading_shows_big(self):
assert get_active_source(chestnut=True, chestnut_active=False, chestnut_loading=True, offroad=False) == "chestnut" assert get_active_source(usbgpu=True, usbgpu_active=False, usbgpu_loading=True, offroad=False) == "usbgpu"
def test_display_onroad_gpu_active_shows_big(self): def test_display_onroad_gpu_active_shows_big(self):
assert get_active_source(chestnut=True, chestnut_active=True, chestnut_loading=False, offroad=False) == "chestnut" assert get_active_source(usbgpu=True, usbgpu_active=True, usbgpu_loading=False, offroad=False) == "usbgpu"
def test_display_onroad_gpu_idle_shows_small(self): def test_display_onroad_gpu_idle_shows_small(self):
assert get_active_source(chestnut=True, chestnut_active=False, chestnut_loading=False, offroad=False) == "qcom" assert get_active_source(usbgpu=True, usbgpu_active=False, usbgpu_loading=False, offroad=False) == "qcom"
def test_display_active_none_is_idle(self): def test_display_active_none_is_idle(self):
assert get_active_source(chestnut=True, chestnut_active=None, chestnut_loading=False, offroad=False) == "qcom" assert get_active_source(usbgpu=True, usbgpu_active=None, usbgpu_loading=False, offroad=False) == "qcom"
def test_active_bundle_follows_source(self): def test_active_bundle_follows_source(self):
params = mock.MagicMock() params = mock.MagicMock()
params.get.side_effect = lambda key: {"ModelManager_ActiveBundle": self._raw_bundle("small"), params.get.side_effect = lambda key: {"ModelManager_ActiveBundle": self._raw_bundle("small"),
"ModelManager_ActiveBundleChestnut": self._raw_bundle("big")}.get(key) "ModelManager_ActiveBundleUSBGPU": self._raw_bundle("big")}.get(key)
with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=False): with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=False):
assert get_active_bundle(params).ref == "small" assert get_active_bundle(params).ref == "small"
assert get_selected_bundle(params, get_active_source(chestnut=True, chestnut_active=False, assert get_selected_bundle(params, get_active_source(usbgpu=True, usbgpu_active=False,
chestnut_loading=False, offroad=True)).ref == "big" usbgpu_loading=False, offroad=True)).ref == "big"
@unittest.skipUnless(os.environ.get('RUN_INTEGRATION_TESTS'), 'requires external network') @unittest.skipUnless(os.environ.get('RUN_INTEGRATION_TESTS'), 'requires external network')
@@ -252,12 +252,4 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = {
AlertStatus.userPrompt, AlertSize.small, AlertStatus.userPrompt, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 0.1), Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 0.1),
}, },
EventNameSP.bigModelReady: {
ET.PERMANENT: Alert(
"Big Model Ready",
"",
AlertStatus.normal, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 2.),
},
} }
@@ -183,7 +183,7 @@ def getParamsMetadata() -> str:
schema["capability_labels"] = CAPABILITY_LABELS schema["capability_labels"] = CAPABILITY_LABELS
schema["default_model"] = DEFAULT_MODEL schema["default_model"] = DEFAULT_MODEL
schema["default_big_model"] = DEFAULT_BIG_MODEL schema["default_big_model"] = DEFAULT_BIG_MODEL
schema["chestnut_active"] = params.get_bool("ChestnutActive") schema["usbgpu_active"] = params.get_bool("UsbGpuActive")
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 -1
View File
@@ -65,7 +65,7 @@ def sp_stats(end_event):
'MadsSteeringMode', 'MadsSteeringMode',
'MadsUnifiedEngagementMode', 'MadsUnifiedEngagementMode',
'ModelManager_ActiveBundle', 'ModelManager_ActiveBundle',
'ModelManager_ActiveBundleChestnut', 'ModelManager_ActiveBundleUSBGPU',
'ModelManager_Favs', 'ModelManager_Favs',
'EnableSunnylinkUploader', 'EnableSunnylinkUploader',
'SunnylinkEnabled', 'SunnylinkEnabled',
@@ -89,16 +89,12 @@ def _migrate_model_bundle_slots(_params):
# ActiveBundle. Seed both slots; validation drops whichever does not match # ActiveBundle. Seed both slots; validation drops whichever does not match
# its own manifest. # its own manifest.
try: try:
if _params.get("ModelManager_ActiveBundleChestnut") is not None: if _params.get("ModelManager_ActiveBundleUSBGPU") is not None:
return
if (chestnut_bundle := _params.get("ModelManager_ActiveBundleUSBGPU")) is not None:
_params.put("ModelManager_ActiveBundleChestnut", chestnut_bundle, block=True)
cloudlog.info("params_migration: seeded ModelManager_ActiveBundleChestnut from ModelManager_ActiveBundleUSBGPU")
return return
if (bundle := _params.get("ModelManager_ActiveBundle")) is None: if (bundle := _params.get("ModelManager_ActiveBundle")) is None:
return return
_params.put("ModelManager_ActiveBundleChestnut", bundle, block=True) _params.put("ModelManager_ActiveBundleUSBGPU", bundle, block=True)
cloudlog.info("params_migration: seeded ModelManager_ActiveBundleChestnut from ModelManager_ActiveBundle") cloudlog.info("params_migration: seeded ModelManager_ActiveBundleUSBGPU from ModelManager_ActiveBundle")
except Exception as e: except Exception as e:
cloudlog.exception(f"Error migrating model bundle slots: {e}") cloudlog.exception(f"Error migrating model bundle slots: {e}")
@@ -140,5 +136,5 @@ def run_migration(_params):
# seed TeslaMadsScreenButton for existing Tesla installs # seed TeslaMadsScreenButton for existing Tesla installs
_migrate_tesla_mads_screen_button(_params) _migrate_tesla_mads_screen_button(_params)
# seed the chestnut model slot from the pre-split single slot # seed the usbgpu model slot from the pre-split single slot
_migrate_model_bundle_slots(_params) _migrate_model_bundle_slots(_params)
@@ -15,22 +15,22 @@ class TestModelBundleSlotMigration(OpenpilotTestCase):
The migration seeds both slots; per-source validation later drops whichever does not The migration seeds both slots; per-source validation later drops whichever does not
match its own manifest.""" match its own manifest."""
def test_seeds_chestnut_slot_from_active_bundle(self): def test_seeds_usbgpu_slot_from_active_bundle(self):
params = Params() params = Params()
bundle = {"ref": "big", "minimumSelectorVersion": 18} bundle = {"ref": "big", "minimumSelectorVersion": 18}
params.put("ModelManager_ActiveBundle", bundle, block=True) params.put("ModelManager_ActiveBundle", bundle, block=True)
_migrate_model_bundle_slots(params) _migrate_model_bundle_slots(params)
assert params.get("ModelManager_ActiveBundleChestnut") == bundle assert params.get("ModelManager_ActiveBundleUSBGPU") == bundle
assert params.get("ModelManager_ActiveBundle") == bundle assert params.get("ModelManager_ActiveBundle") == bundle
def test_noop_when_chestnut_slot_already_set(self): def test_noop_when_usbgpu_slot_already_set(self):
params = Params() params = Params()
params.put("ModelManager_ActiveBundle", {"ref": "small"}, block=True) params.put("ModelManager_ActiveBundle", {"ref": "small"}, block=True)
params.put("ModelManager_ActiveBundleChestnut", {"ref": "big"}, block=True) params.put("ModelManager_ActiveBundleUSBGPU", {"ref": "big"}, block=True)
_migrate_model_bundle_slots(params) _migrate_model_bundle_slots(params)
assert params.get("ModelManager_ActiveBundleChestnut") == {"ref": "big"} assert params.get("ModelManager_ActiveBundleUSBGPU") == {"ref": "big"}
def test_noop_when_no_selection(self): def test_noop_when_no_selection(self):
params = Params() params = Params()
_migrate_model_bundle_slots(params) _migrate_model_bundle_slots(params)
assert params.get("ModelManager_ActiveBundleChestnut") is None assert params.get("ModelManager_ActiveBundleUSBGPU") is None
+2 -2
View File
@@ -16,7 +16,7 @@ from openpilot.common.utils import strip_deprecated_keys
from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.common.realtime import DT_HW from openpilot.common.realtime import DT_HW
from openpilot.selfdrive.modeld.helpers import MODELS_DIR, chestnut_compiled from openpilot.selfdrive.modeld.helpers import MODELS_DIR, usbgpu_compiled
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE
from openpilot.common.basedir import BASEDIR from openpilot.common.basedir import BASEDIR
@@ -239,7 +239,7 @@ def hardware_thread(end_event, hw_queue) -> None:
fan_controller = FanController(int(1./DT_HW)) fan_controller = FanController(int(1./DT_HW))
chestnut = Chestnut() chestnut = Chestnut()
big_model_available = (MODELS_DIR / 'big_driving_supercombo.onnx').is_file() or chestnut_compiled() big_model_available = (MODELS_DIR / 'big_driving_supercombo.onnx').is_file() or usbgpu_compiled()
while not end_event.is_set(): while not end_event.is_set():
sm.update(PANDA_STATES_TIMEOUT) sm.update(PANDA_STATES_TIMEOUT)