Files
openpilot-evo/sunnypilot/models/helpers.py
T
James Vecellio-Grant c47aadee19 sunnypilot modeld: Sync Tinygrad (#1081)
* Tinygrad bump from sync-20250627

* bump tinygrad_repo

* Reformat metadata generator to match driving_models.json

* bump tinygrad

* Revert "bump tinygrad"

This reverts commit f479dfd5021266e22b34deca06cc3c4d531fb7e8.

* revert me after SP model compiled

* Model recompiled successfully, initiate "revert me after SP model compiled"

This reverts commit 95706eb688c9f5522903bee21892b8871ff8f957.

* The "FillMe" placeholder caused an extra 10 seconds of work

* bump to 22Jul2025

* Update build-all-tinygrad-models.yaml

* Update build-all-tinygrad-models.yaml

* Update build-all-tinygrad-models.yaml

* Update build-all-tinygrad-models.yaml

* Update build-all-tinygrad-models.yaml

* Update build-all-tinygrad-models.yaml

* Update build-all-tinygrad-models.yaml

* Allow more dynamic short names

This should hopefully be future-proof for now.. It's robust enough to return the correct word-digit format (see example on how it generates from given display name below):

'Last Horizon V2 (November 22, 2024)' -> LHV2
'Alabama (November 25, 2024)' -> ALABAMA
'PlayStation (December 03, 2024)' -> PLAYSTAT
'Postal Service (December 09, 2024)' -> PS
'Null Pointer (December 13, 2024)' -> NP
'North America (December 16, 2024)' -> NA
'National Public Radio (December 18, 2024)' -> NPR
'Filet o Fish (March 7, 2025)' -> FOF
'Tomb Raider 2 (April 18, 2025)' -> TR2
'Tomb Raider 3 (April 22, 2025)' -> TR3
'Tomb Raider 4 (April 25, 2025)' -> TR4
'Tomb Raider 5 (April 25, 2025)' -> TR5
'Tomb Raider 6 (April 30, 2025)' -> TR6
'Tomb Raider 7 (May 07, 2025)' -> TR7
'Down to Ride (Revision: May 10, 2025)' -> DTR
'SP Vikander Model (May 16, 2025)' -> SPVM
'VFF Driving (May 15, 2025)' -> VFFD
'Secret Good Openpilot (May 16, 2025)' -> SGO
'Vegetarian Filet o Fish (May 29, 2025)' -> VFOF
'Down To Ride (Revision: May 30, 2025)' -> DTR
'Vegetarian Filet o Fish v2 (June 05, 2025)' -> VFOFV2
'Kerrygold Driving (June 08, 2025)' -> KD
'Tomb Raider 10 (June 16, 2025)' -> TR10
'Organic Kerrygold (June 17, 2025)' -> OK
'Liquid Crystal Driving (June 21, 2025)' -> LCD
'Vegetarian Filet o Fish v3 (June 21, 2025)' -> VFOFV3
'Vibe Model [Custom Model]' -> VMCM
'Tomb Raider 13 (June 27, 2025)' -> TR13
'Aggressive TR (June 28, 2025)' -> ATR
'Tomb Raider 14 (June 30, 2025)' -> TR14
'Cookiemonster Tomb Raider (July 02, 2025)' -> CTR
'Down to Ride (Revision: July 07, 2025)' -> DTR
'Simple Plan Driving (July 07, 2025)' -> SPD
'Down to Ride (Revision: July 08, 2025)' -> DTR
'Tomb Raider 15 (July 09, 2025)' -> TR15
'Tomb Raider 15 rev-2 (July 11, 2025)' -> TR15R2
'Le Tomb Raider 14 (July 14, 2025)' -> LTR14
'Le Tomb Raider 14h (July 17, 2025)' -> LTR14H
'Tomb Raider 16 (July 18, 2025)' -> TR16
'Tomb Raider 16v2 (July 21, 2025)' -> TR16V2

* Update build-all-tinygrad-models.yaml

* Update build-all-tinygrad-models.yaml

* No need to sleep 3 seconds, just send it

* try dynamic

* cleanup

* Update build-single-tinygrad-model.yaml

* bc devtekve said. also, this is repetitive af

* Revert "bc devtekve said. also, this is repetitive af"

This reverts commit 3a0c1562de6eae1d2d213d2f53cfb2edb4eef40c.

* maybe we could use a script instead that both build all

That both build all and sunnypilot-build-model reference

* refactor: consolidate model building steps into a single workflow

* tweak

* tweakx2

* tweakx3

* tweakx4

* dunno dunno...

* output dir

* lots of changes

* Revert "lots of changes"

This reverts commit 4aadb0ee2929fdf9d902335161b22072bef4e068.

* fail if all fail

* no inputs needed

* make it easier for us

* note failure and exit 0

* Update build-all-tinygrad-models.yaml

* not needed unless we really want it

* Update build-single-tinygrad-model.yaml

* Merge branch 'sync-20250627-tinygrad' of github.com:sunnypilot/sunnypilot into sync-20250627-tinygrad

* retry for failed ?

* always run this step because sometimes one build fails

which causes the matrix to fail, but most builds still have uploaded artifacts.

* strip

* no escape

* Update build-all-tinygrad-models.yaml

* Test case from terminal run

(openpilot) james@Mac sunnypilot % jq -c '[.bundles[] | select(.runner=="tinygrad") | {ref, display_name: (.display_name | gsub(" \\([^)]*\\)"; "")), is_20hz}]' \
  /Users/james/Documents/GitHub/sunnypilot-docs/docs/driving_models_v6.json > matrix.json

mkdir -p output
touch "output/model-Tomb Raider 16v2 (July 21, 2025)-544"
touch "output/model-Space Lab Model (July 24, 2025)-547"
touch "output/model-Space Lab Model v1 (July 24, 2025)-548"

built=(); while IFS= read -r line; do built+=("$line"); done < <(
  ls output | sed -E 's/^model-//' | sed -E 's/-[0-9]+$//' | sed -E 's/ \([^)]*\)//' | awk '{gsub(/^ +| +$/, ""); print}'
)

jq -c --argjson built "$(printf '%s\n' "${built[@]}" | jq -R . | jq -s .)" \
  'map(select(.display_name as $n | ($built | index($n | gsub("^ +| +$"; "")) | not)))' \
  matrix.json > retry_matrix.json

cat retry_matrix.json

[]
(openpilot) james@Mac sunnypilot %

* always

* great success

* add suffix to retry artifact so it doesn't conflict

* retry to get_model too

* and there haha

* unnecessary hyphen

* compare built to missing. include retries

* adjust copy of artifacts.

* Update build-all-tinygrad-models.yaml

* Update model selector versioning and add documentation

* Update retry condition for failed models in build-all-tinygrad-models.yaml

* Update retry condition for failed models in build-all-tinygrad-models.yaml

* Update build-single-tinygrad-model.yaml

* false

* default none because why not

* red diff? i think?

* meh ... not needed i guess

* error error error

* Nayan is watching... always watching mike wazowski

* string all the way

* lots of retries just in case because im scared

* more robust

* ONLY ONE!!!!!!

* delete.... a lot

* fix artifacts

* fix artifacts

* make sure each is unique :)

* skip files like artifact duhhhh

* artifact name dir

* concurrency

* copy here

* Update build-single-tinygrad-model.yaml

* Update build-single-tinygrad-model.yaml

* bump

* bump tinygrad

* max parallel? if not, i have the other remedy ready in build-all

* revert me!

* I resynced tinygrad woo hoo

* setup shouldnt fail

* pull

* big ole diff

* condition

* Update build-all-tinygrad-models.yaml

* not always() never always() never!!!

* not failure instead of great success

* Update build-all-tinygrad-models.yaml

* yay that worked. lets invoke build-single one last time

* these arent used and are just taking up 250MB space

* really frog?

* bump back to 3

* self-hosted, tici

* rename to trigger tests

* split sync

---------

Co-authored-by: DevTekVE <devtekve@gmail.com>
2025-07-31 06:50:23 -07:00

188 lines
6.6 KiB
Python

"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import hashlib
import os
import pickle
import numpy as np
import json
from openpilot.common.params import Params
from cereal import custom
from openpilot.sunnypilot.modeld.constants import Meta, MetaTombRaider, MetaSimPose
from openpilot.sunnypilot.modeld.runners import ModelRunner
from openpilot.system.hardware import PC
from openpilot.system.hardware.hw import Paths
from pathlib import Path
# see the README.md for more details on the model selector versioning
CURRENT_SELECTOR_VERSION = 8
REQUIRED_MIN_SELECTOR_VERSION = 6
USE_ONNX = os.getenv('USE_ONNX', PC)
CUSTOM_MODEL_PATH = Paths.model_root()
METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl'
ModelManager = custom.ModelManagerSP
async def verify_file(file_path: str, expected_hash: str) -> bool:
"""Verifies file hash against expected hash"""
if not os.path.exists(file_path):
return False
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as file:
for chunk in iter(lambda: file.read(4096), b""):
sha256_hash.update(chunk)
return sha256_hash.hexdigest().lower() == expected_hash.lower()
def is_bundle_version_compatible(bundle: dict) -> bool:
"""
Checks whether the model bundle is compatible with the current selector version constraints.
The bundle specifies a `minimum_selector_version`, which defines the minimum selector version
required to load the model. This function ensures that:
1. The model is not too old: the bundle must require at least `REQUIRED_MIN_SELECTOR_VERSION`.
2. The model is not too new: it must support the current selector version (`CURRENT_SELECTOR_VERSION`).
This allows the selector to enforce both a minimum and maximum range of supported models,
even if a model would otherwise be compatible.
:param bundle: Dictionary containing `minimum_selector_version`, as defined by the model bundle.
:type bundle: Dict
:return: True if the selector version is within the accepted range for the bundle; otherwise False.
:rtype: Bool
"""
return bool(REQUIRED_MIN_SELECTOR_VERSION <= bundle.get("minimumSelectorVersion", 0) <= CURRENT_SELECTOR_VERSION)
def get_active_bundle(params: Params = None) -> custom.ModelManagerSP.ModelBundle:
"""Gets the active model bundle from cache"""
if params is None:
params = Params()
try:
if (active_bundle := json.loads(params.get("ModelManager_ActiveBundle") or "{}")) and is_bundle_version_compatible(active_bundle):
return custom.ModelManagerSP.ModelBundle(**active_bundle)
except Exception:
pass
return None
def get_active_model_runner(params: Params = None, force_check=False) -> custom.ModelManagerSP.Runner:
"""
Determines and returns the active model runner type, based on provided parameters.
The function utilizes caching to prevent redundant calculations and checks.
If the cached "ModelRunnerTypeCache" exists in the provided parameters and `force_check`
is set to False, the cached value is directly returned. Otherwise, the function determines
the runner type based on the active model bundle. If a model bundle containing a drive
model exists, the runner type is derived based on the filename of the drive model.
Finally, it updates the cache with the determined runner type, if needed.
:param params: The parameter set used to retrieve caching and runner details. If `None`,
a default `Params` instance is created internally.
:type params: Params
:param force_check: A flag indicating whether to bypass cached results and always
re-determine the runner type. Defaults to `False`.
:type force_check: bool
:return: The determined or cached model runner type.
:rtype: custom.ModelManagerSP.Runner
"""
if params is None:
params = Params()
if (cached_runner_type := params.get("ModelRunnerTypeCache")) and not force_check:
if isinstance(cached_runner_type, str) and cached_runner_type.isdigit():
return int(cached_runner_type)
runner_type = custom.ModelManagerSP.Runner.stock
if active_bundle := get_active_bundle(params):
runner_type = active_bundle.runner.raw
if cached_runner_type != runner_type:
params.put("ModelRunnerTypeCache", str(int(runner_type)))
return runner_type
def _get_model():
if bundle := get_active_bundle():
drive_model = next(model for model in bundle.models if model.type == ModelManager.Model.Type.supercombo)
return drive_model
return None
def get_model_path():
if USE_ONNX:
return {ModelRunner.ONNX: Path(__file__).parent / '../models/supercombo.onnx'}
if model := _get_model():
return {ModelRunner.THNEED: f"{CUSTOM_MODEL_PATH}/{model.artifact.fileName}"}
return {ModelRunner.THNEED: Path(__file__).parent / '../models/supercombo.thneed'}
def load_metadata():
metadata_path = METADATA_PATH
if model := _get_model():
metadata_path = f"{CUSTOM_MODEL_PATH}/{model.metadata.fileName}"
with open(metadata_path, 'rb') as f:
return pickle.load(f)
def prepare_inputs(model_metadata) -> dict[str, np.ndarray]:
# img buffers are managed in openCL transform code so we don't pass them as inputs
inputs = {
k: np.zeros(v, dtype=np.float32).flatten()
for k, v in model_metadata['input_shapes'].items()
if 'img' not in k
}
return inputs
def load_meta_constants(model_metadata):
"""
Determines and loads the appropriate meta model class based on the metadata provided. The function checks
specific keys and conditions within the provided metadata dictionary to identify the corresponding meta
model class to return.
:param model_metadata: Dictionary containing metadata about the model. It includes
details such as input shapes, output slices, and other configurations for identifying
metadata-dependent meta model classes.
:type model_metadata: dict
:return: The appropriate meta model class (Meta, MetaSimPose, or MetaTombRaider)
based on the conditions and metadata provided.
:rtype: type
"""
meta = Meta # Default Meta
if 'sim_pose' in model_metadata['input_shapes'].keys():
# Meta for models with sim_pose input
meta = MetaSimPose
else:
# Meta for Tomb Raider, it does not include sim_pose input but has the same meta slice as previous models
meta_slice = model_metadata['output_slices']['meta']
meta_tf_slice = slice(5868, 5921, None)
if (
meta_slice.start == meta_tf_slice.start and
meta_slice.stop == meta_tf_slice.stop and
meta_slice.step == meta_tf_slice.step
):
meta = MetaTombRaider
return meta