mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 00:53:45 +08:00
Compile native Chestnut models
This commit is contained in:
@@ -6,7 +6,7 @@ from SCons.Script import Action, Value
|
||||
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.system.camerad.cameras.nv12_info import get_nv12_info
|
||||
from tools.laptop_device_build.qcom_artifact_contract import import_artifact
|
||||
from tools.laptop_device_build.qcom_artifact_contract import import_artifact, native_records, verify_native_defaults
|
||||
|
||||
|
||||
Import('env', 'arch')
|
||||
@@ -35,6 +35,31 @@ laptop_qcom_import = arch == 'comma_arm64' and os.getenv('COMMA_LAPTOP_CROSS_BUI
|
||||
qcom_package = Dir('#.cache/qcom-artifact-package').abspath
|
||||
|
||||
|
||||
# Native defaults are release artifacts, not outputs of the legacy model compiler.
|
||||
# Validate their current source/vendor/guard contract without regenerating them.
|
||||
native_bundle = native_records(Path(Dir('#').abspath)) if arch == 'comma_arm64' else None
|
||||
if native_bundle is not None:
|
||||
native_root = Path(Dir('#').abspath)
|
||||
native_bridge = native_root / 'openpilot/starpilot/models/runtime_bridge'
|
||||
native_sources = [native_bridge / name for name in (
|
||||
'native.py', 'native-policy.json', 'native_guards.py', 'bounded_wait.c', 'usb_guards.c',
|
||||
'libbounded_wait.so', 'libusb_guards.so')]
|
||||
native_sources += [native_bridge.parent / 'catalog-native02e.json']
|
||||
for name in native_bundle:
|
||||
base = native_root / 'openpilot/selfdrive/modeld/models' / name
|
||||
native_sources += [path for path in (base, *base.parent.glob(base.name + '.chunk*')) if path.is_file()]
|
||||
|
||||
def verify_native_bundle(target, source, env):
|
||||
records = verify_native_defaults(native_root, prefer_regular=True)
|
||||
stamp = Path(str(target[0]))
|
||||
stamp.parent.mkdir(parents=True, exist_ok=True)
|
||||
stamp.write_text(str({name: row['artifact_sha256'] for name, row in records.items()}) + '\n')
|
||||
|
||||
native_node = lenv.Command('#.cache/native-model-defaults/verified', [File(str(path)) for path in native_sources],
|
||||
Action(verify_native_bundle, ' [NATIVE MODEL VERIFY] $TARGET'))
|
||||
lenv.AlwaysBuild(native_node)
|
||||
|
||||
|
||||
def model_action(command):
|
||||
if not laptop_qcom_import:
|
||||
return Action(command, " [ONNX] $TARGET")
|
||||
@@ -77,7 +102,8 @@ def compile_model(onnx_path, pkl_path):
|
||||
if laptop_qcom_import:
|
||||
lenv.AlwaysBuild(node)
|
||||
|
||||
compile_model('models/dmonitoring_model.onnx', 'models/dmonitoring_model_tinygrad.pkl')
|
||||
if native_bundle is None:
|
||||
compile_model('models/dmonitoring_model.onnx', 'models/dmonitoring_model_tinygrad.pkl')
|
||||
|
||||
model_w, model_h = MEDMODEL_INPUT_SIZE
|
||||
for cam_w, cam_h in camera_configs:
|
||||
@@ -92,7 +118,7 @@ for cam_w, cam_h in camera_configs:
|
||||
lenv.AlwaysBuild(node)
|
||||
|
||||
dm_w, dm_h = DM_INPUT_SIZE
|
||||
for cam_w, cam_h in camera_configs:
|
||||
for cam_w, cam_h in (() if native_bundle is not None else camera_configs):
|
||||
dm_pkl_path = File(f"models/dm_warp_{cam_w}x{cam_h}_tinygrad.pkl").abspath
|
||||
stride, y_height, uv_height, frame_size = get_nv12_info(cam_w, cam_h)
|
||||
cmd = (f'{tg_flags} {mac_brew_string} python3 "{compiler}/compile_warp.py" '
|
||||
|
||||
@@ -47,8 +47,8 @@ class ModelState:
|
||||
self.model_run = jits['run']
|
||||
self.outputs = {name: Tensor(np.zeros(shape, dtype=dtype), device=device).realize() for name, (shape, dtype, device) in jits['output_specs'].items()}
|
||||
warp_path = MODELS_DIR / f'dm_warp_{cam_w}x{cam_h}_tinygrad.pkl'
|
||||
from openpilot.starpilot.models.runtime_bridge.runtime import selected_legacy, load_warp
|
||||
if selected_legacy():
|
||||
from openpilot.starpilot.models.runtime_bridge.runtime import prepared, load_warp
|
||||
if prepared():
|
||||
self.image_warp = load_warp(warp_path)
|
||||
else:
|
||||
with warp_path.open('rb') as f:
|
||||
|
||||
@@ -15,9 +15,9 @@ def modeld_pkl_path(chestnut: bool):
|
||||
return materialize_file_chunked(path) if path.is_file() or Path(f'{path}.chunkmanifest').exists() else path
|
||||
|
||||
def load_oob(path, chestnut=False):
|
||||
from openpilot.starpilot.models.runtime_bridge.runtime import selected_legacy, load_oob as protected_load
|
||||
from openpilot.starpilot.models.runtime_bridge.runtime import prepared, load_oob as protected_load
|
||||
if AGNOS:
|
||||
if not selected_legacy():
|
||||
if not prepared():
|
||||
raise RuntimeError('Prepare the model runtime before loading a device artifact')
|
||||
return protected_load(materialize_file_chunked(path), chestnut)
|
||||
from tinygrad import Context
|
||||
|
||||
@@ -8,8 +8,9 @@ from functools import cached_property
|
||||
import os
|
||||
os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom
|
||||
os.environ.setdefault('AM_POWER_LIMIT', '100')
|
||||
from openpilot.starpilot.models.runtime_bridge.runtime import prepare as prepare_model_runtime, selected_legacy
|
||||
from openpilot.starpilot.models.runtime_bridge.runtime import prepare as prepare_model_runtime, selected_legacy, selected_plan
|
||||
prepare_model_runtime('driving')
|
||||
MODEL_RUNTIME_PLAN = selected_plan()
|
||||
from tinygrad.device import Buffer, Device
|
||||
from tinygrad.dtype import DType, dtypes
|
||||
from tinygrad.engine.realize import lower_and_compile
|
||||
@@ -50,7 +51,7 @@ from openpilot.starpilot.models.status import ModelVariant
|
||||
from openpilot.starpilot.models.startup import report_load_timeout, wait_for_chestnut_power
|
||||
from openpilot.starpilot.models.recovery import STARTUP_FALLBACK_EXIT
|
||||
from openpilot.starpilot.navigation.intent import TurnIntent, matching_turn_signal
|
||||
from openpilot.starpilot.models.catalog import BUNDLED_CURRENT, BY_ID, DEFAULT_SMALL, DEFAULT_SMALL_SHA256
|
||||
from openpilot.starpilot.models.catalog import BUNDLED_CURRENT, DEFAULT_SMALL
|
||||
from openpilot.starpilot.models.runner import CatalogModelState, load_verified_model
|
||||
from openpilot.starpilot.models import jetlink_adapter
|
||||
import uuid
|
||||
@@ -290,8 +291,8 @@ def main(demo=False):
|
||||
# a Jetson, PC or phone on the port looks like a chestnut to cable_connected(): the cable counts with Jetlink off only
|
||||
chestnut_available = chestnut_present() or (cable_connected() and not jetlink_adapter.link_on())
|
||||
recovery_small_only = os.getenv("STARPILOT_MODELD_RECOVERY_SMALL_ONLY") == "1"
|
||||
selection = resolve_runtime(chestnut_available=chestnut_available and not recovery_small_only, randomize=not recovery_small_only)
|
||||
requested_model_id = requested_runtime_id(chestnut_available)
|
||||
selection = resolve_runtime(chestnut_available=chestnut_available and not recovery_small_only, plan=MODEL_RUNTIME_PLAN)
|
||||
requested_model_id = requested_runtime_id(chestnut_available, plan=MODEL_RUNTIME_PLAN)
|
||||
CHESTNUT = selection.allow_big and selection.big_path is not None
|
||||
if CHESTNUT:
|
||||
from tinygrad.runtime.support.am import startup_trace
|
||||
@@ -350,13 +351,12 @@ def main(demo=False):
|
||||
return selected, prepared, False
|
||||
except Exception:
|
||||
cloudlog.exception("selected small model load failed, using shipped RDFv4")
|
||||
from openpilot.starpilot.models.manager import shipped_default
|
||||
path = shipped_default()
|
||||
path = selection.fallback_path
|
||||
if path is None:
|
||||
raise RuntimeError("Shipped RDFv4 is missing or corrupt")
|
||||
fallback, prepared = load_verified_model(vipc_client_main.width, vipc_client_main.height,
|
||||
path, BY_ID[DEFAULT_SMALL].version, False,
|
||||
DEFAULT_SMALL, DEFAULT_SMALL_SHA256)
|
||||
path, selection.fallback_version, False,
|
||||
DEFAULT_SMALL, selection.fallback_sha256)
|
||||
return fallback, prepared, selection.small_id != DEFAULT_SMALL
|
||||
|
||||
# Preload a custom fallback before the GPU worker takes the catalog lock.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
3
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"bytes": 125895084,
|
||||
"sha256": "e23f65b3790a2092348024603966717506c2ad1fae10b4312bdfe1f2b932df61",
|
||||
"parts": [
|
||||
{
|
||||
"bytes": 47185920,
|
||||
"sha256": "268527c074b659a76c495d91afac8aee21e12100cc0191daa116f0db633b9165"
|
||||
},
|
||||
{
|
||||
"bytes": 47185920,
|
||||
"sha256": "8622fdec72524dbc80c744fa17b92edea59d5889f0e1c4d02d3e4e8d44521d2b"
|
||||
},
|
||||
{
|
||||
"bytes": 31523244,
|
||||
"sha256": "9cff2854787bf48fd7fd0f64a4a243a0011ed76cc3c09fb3399c0d3d9661d1d1"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"bytes": 20768359,
|
||||
"sha256": "7f7198c63d2d9b0d015cc860fdd836db50a4b9a75ab5d56cffd4d4eee5eb7b97",
|
||||
"bytes": 20778338,
|
||||
"sha256": "2dfdf58712f29be92d37f0b6b06d6ae78626831c3b9e7a0c990d40e8ef84b3a2",
|
||||
"parts": [
|
||||
{
|
||||
"bytes": 20768359,
|
||||
"sha256": "7f7198c63d2d9b0d015cc860fdd836db50a4b9a75ab5d56cffd4d4eee5eb7b97"
|
||||
"bytes": 20778338,
|
||||
"sha256": "2dfdf58712f29be92d37f0b6b06d6ae78626831c3b9e7a0c990d40e8ef84b3a2"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"bytes": 125895084,
|
||||
"sha256": "e23f65b3790a2092348024603966717506c2ad1fae10b4312bdfe1f2b932df61",
|
||||
"bytes": 114218310,
|
||||
"sha256": "efc9c780a824c798c629776601dc7b26aaa1e1270e048d9f90dd2f164de380a9",
|
||||
"parts": [
|
||||
{
|
||||
"bytes": 47185920,
|
||||
"sha256": "268527c074b659a76c495d91afac8aee21e12100cc0191daa116f0db633b9165"
|
||||
"sha256": "3e6df8f84030d138d5fdee925b8eeb80bc33468c3406340fd2a026e86ec51b33"
|
||||
},
|
||||
{
|
||||
"bytes": 47185920,
|
||||
"sha256": "8622fdec72524dbc80c744fa17b92edea59d5889f0e1c4d02d3e4e8d44521d2b"
|
||||
"sha256": "8a538d5122992ad2d137164a23df0be5fd322d4f6cbb1c9a4561b903ace1420d"
|
||||
},
|
||||
{
|
||||
"bytes": 31523244,
|
||||
"sha256": "9cff2854787bf48fd7fd0f64a4a243a0011ed76cc3c09fb3399c0d3d9661d1d1"
|
||||
"bytes": 19846470,
|
||||
"sha256": "2021d5b79a036efb899ac7423f3c84ec5e59b8ab2859c01402bcba757200767e"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"artifact_abi": "tinygrad_single_v2_arena",
|
||||
"compiler_revision": "02e698160eed1751738212144a7ea2cfb160b3f1",
|
||||
"generation": "native02e-v1",
|
||||
"models": [
|
||||
{
|
||||
"artifact_chunk_count": 18,
|
||||
"artifact_format": "tinygrad_single_v2_arena",
|
||||
"artifact_sha256": "868b3e775ab92e223a1618e5b9f2690976cbdb61e27516e50a43db96375586b8",
|
||||
"artifact_size": 804719273,
|
||||
"community_favorite": false,
|
||||
"id": "traintrack",
|
||||
"name": "Train Track \ud83d\udc40\ud83d\udce1",
|
||||
"qualification_scope": "Native ONNX compilation with original five-frame capture/reload checks; exact retained GPU kernels and weights during bounded-guard preparation; fresh final-source guarded CatalogModelState/Parser runner, four seeded frames at 1928x1208. No physical acceptance or startup speedup asserted.",
|
||||
"qualification_sha256": "3d7f485199fa8532b41b2d1ce3e484718bffb4c17a587a18b86ceee24ebbf5ff",
|
||||
"released": "2026-10-17",
|
||||
"series": "OP Series",
|
||||
"source_bytes": 792467570,
|
||||
"source_sha256": "ceb38c593005b3338aee4ca057d5ad9ee64c58f8fdba0b41641a9d4cc6c36e36",
|
||||
"uses_external_gpu": true,
|
||||
"version": "v16"
|
||||
},
|
||||
{
|
||||
"artifact_chunk_count": 18,
|
||||
"artifact_format": "tinygrad_single_v2_arena",
|
||||
"artifact_sha256": "6a9ccfb1d9c6a518435cddc13e990bd1c89bc87f2d3a5591a8e1a69d4cd552e2",
|
||||
"artifact_size": 804719273,
|
||||
"community_favorite": false,
|
||||
"id": "mountaindewv3",
|
||||
"name": "Mountain Dew v3 \ud83d\udc40\ud83d\udce1",
|
||||
"qualification_scope": "Native ONNX compilation with original five-frame capture/reload checks; exact retained GPU kernels and weights during bounded-guard preparation; fresh final-source guarded CatalogModelState/Parser runner, four seeded frames at 1344x760. No physical acceptance or startup speedup asserted.",
|
||||
"qualification_sha256": "ac3637cb238f9599a43f77144bc332be6930627d77bad098acbcfd6f1d777748",
|
||||
"released": "2026-10-14",
|
||||
"series": "OP Series",
|
||||
"source_bytes": 792542056,
|
||||
"source_sha256": "1cfd2c45353d876b5b257b24d77537de58ab90a44ae595f15b1f64b6ea3fd770",
|
||||
"uses_external_gpu": true,
|
||||
"version": "v16"
|
||||
},
|
||||
{
|
||||
"artifact_chunk_count": 18,
|
||||
"artifact_format": "tinygrad_single_v2_arena",
|
||||
"artifact_sha256": "d4bd838f4cfe2465c2ecac3efa32aeeddd9d1a3c0ed80b1987ee87a1a3470358",
|
||||
"artifact_size": 804718917,
|
||||
"community_favorite": false,
|
||||
"id": "mountaindewv2",
|
||||
"name": "Mountain Dew v2 \ud83d\udc40\ud83d\udce1",
|
||||
"qualification_scope": "Native ONNX compilation with original five-frame capture/reload checks; exact retained GPU kernels and weights during bounded-guard preparation; fresh final-source guarded CatalogModelState/Parser runner, four seeded frames at 1344x760. No physical acceptance or startup speedup asserted.",
|
||||
"qualification_sha256": "5b79ede3cf0809fce8497bb7f8feca86be6f387c68c0e706177ed73068927683",
|
||||
"released": "2026-10-10",
|
||||
"series": "OP Series",
|
||||
"source_bytes": 792430559,
|
||||
"source_sha256": "87fe86ca0813c099efddf9173ba503be2e40ea4c5362c36812a44141f4cb07d4",
|
||||
"uses_external_gpu": true,
|
||||
"version": "v16"
|
||||
},
|
||||
{
|
||||
"artifact_chunk_count": 18,
|
||||
"artifact_format": "tinygrad_single_v2_arena",
|
||||
"artifact_sha256": "787b13a040362f4efcfca15bf95f7a36694ba21fb8175d8557ed579ceffde282",
|
||||
"artifact_size": 804719273,
|
||||
"community_favorite": false,
|
||||
"id": "mountaindewv1",
|
||||
"name": "Mountain Dew v1 \ud83d\udc40\ud83d\udce1",
|
||||
"qualification_scope": "Native ONNX compilation with original five-frame capture/reload checks; exact retained GPU kernels and weights during bounded-guard preparation; fresh final-source guarded CatalogModelState/Parser runner, four seeded frames at 1344x760. No physical acceptance or startup speedup asserted.",
|
||||
"qualification_sha256": "497a0f262de83a911ba4dbb1d63f9ffbf51567e155574e72170f94949cafa88d",
|
||||
"released": "2026-10-08",
|
||||
"series": "OP Series",
|
||||
"source_bytes": 792487890,
|
||||
"source_sha256": "c392ba1aa1b4b89e850366024fa6c1beac1f1fdc3a7182e3c3b0234c55e08eeb",
|
||||
"uses_external_gpu": true,
|
||||
"version": "v16"
|
||||
},
|
||||
{
|
||||
"artifact_chunk_count": 18,
|
||||
"artifact_format": "tinygrad_single_v2_arena",
|
||||
"artifact_sha256": "10aa579f2181154996afc04f30337a7b42c7768f92d2bbdc92d8685acbd725ca",
|
||||
"artifact_size": 804718766,
|
||||
"community_favorite": false,
|
||||
"id": "resfit",
|
||||
"name": "ResFit",
|
||||
"qualification_scope": "Native ONNX compilation with original five-frame capture/reload checks; exact retained GPU kernels and weights during bounded-guard preparation; fresh final-source guarded CatalogModelState/Parser runner, four seeded frames at 1344x760. No physical acceptance or startup speedup asserted.",
|
||||
"qualification_sha256": "cb84da0893e25cfec8f581c7c043949a0f6a830ab9e4745d2f7ab0e8b9e86d43",
|
||||
"released": "2026-10-06",
|
||||
"series": "OP Series",
|
||||
"source_bytes": 792485215,
|
||||
"source_sha256": "1563b85f6bd00d9e2edf50bdb69646f0d95d5ac0f218d5abe3fe54a13427d71b",
|
||||
"uses_external_gpu": true,
|
||||
"version": "v16"
|
||||
},
|
||||
{
|
||||
"artifact_chunk_count": 17,
|
||||
"artifact_format": "tinygrad_single_v2_arena",
|
||||
"artifact_sha256": "d4613485c31cc28ab9b75fc8c14072354586c83e3e799e94682dcf06026b8f80",
|
||||
"artifact_size": 778866789,
|
||||
"community_favorite": false,
|
||||
"id": "cinquev3",
|
||||
"name": "Cinque v3 \ud83d\udc40\ud83d\udce1",
|
||||
"qualification_scope": "Native ONNX compilation with original five-frame capture/reload checks; exact retained GPU kernels and weights during bounded-guard preparation; fresh final-source guarded CatalogModelState/Parser runner, four seeded frames at 1344x760. No physical acceptance or startup speedup asserted.",
|
||||
"qualification_sha256": "b169cef607dbb650f055ef87ace3c76f43013da3798ea3d3af384ae2b95026e5",
|
||||
"released": "2026-09-18",
|
||||
"series": "OP Series",
|
||||
"source_bytes": 766354845,
|
||||
"source_sha256": "404a18cfd86d29637d20c697dfde245bb47c666ae016730ab674c65f4d1e1aa4",
|
||||
"uses_external_gpu": true,
|
||||
"version": "v16"
|
||||
},
|
||||
{
|
||||
"artifact_chunk_count": 3,
|
||||
"artifact_format": "tinygrad_single_v2_arena",
|
||||
"artifact_sha256": "efc9c780a824c798c629776601dc7b26aaa1e1270e048d9f90dd2f164de380a9",
|
||||
"artifact_size": 114218310,
|
||||
"community_favorite": false,
|
||||
"id": "rdf43",
|
||||
"model_lab_eligible": true,
|
||||
"model_size": "small",
|
||||
"name": "Regret Driven Framework V4 \ud83d\udc40\ud83d\udce1",
|
||||
"qualification_scope": "Native ONNX compilation with original five-frame capture/reload checks; exact retained GPU kernels and weights during bounded-guard preparation; fresh final-source guarded CatalogModelState/Parser runner, four seeded frames at 1344x760. No physical acceptance or startup speedup asserted.",
|
||||
"qualification_sha256": "0d0ff59d57da106f042ec79636c65dbcd805d4d15c81a79ae9a7551b46dc5ecd",
|
||||
"released": "2026-08-10",
|
||||
"series": "OP Series",
|
||||
"source_bytes": 96636386,
|
||||
"source_sha256": "b9550d192e32769f1fca82b2e2f2fae65e335b9f6fbbda2b455fcc8b2207bef6",
|
||||
"version": "v15"
|
||||
}
|
||||
],
|
||||
"status": "QUALIFIED_NATIVE_RELEASE",
|
||||
"stock": {
|
||||
"dm_warp_1344x760_tinygrad.pkl": {
|
||||
"artifact_sha256": "095467b3397526455aba3573e2458d960cbe195e23f8f91d46ef5e886074006c",
|
||||
"artifact_size": 1397668,
|
||||
"qualification_scope": "Actual four-frame driver-monitoring ModelState and both camera warps; 88 output arrays and state histories exact per camera against raw native captures. No road acceptance or speedup asserted.",
|
||||
"qualification_sha256": "6d9b4ad05d673a352f152b2b6327bcff6de6dbc76c3c96204fb839b67b920fc0",
|
||||
"source_geometry": {
|
||||
"border_fill": 16,
|
||||
"camera": [
|
||||
1344,
|
||||
760
|
||||
],
|
||||
"layout": "luma",
|
||||
"nv12": [
|
||||
1408,
|
||||
768,
|
||||
384,
|
||||
2428928
|
||||
],
|
||||
"transform_device": "NPY",
|
||||
"warp_to": [
|
||||
1440,
|
||||
960
|
||||
]
|
||||
}
|
||||
},
|
||||
"dm_warp_1928x1208_tinygrad.pkl": {
|
||||
"artifact_sha256": "05021aae07d80fe8ccdd593581398e6432c52d7132384f706c0e2f75d69a7a3b",
|
||||
"artifact_size": 1397620,
|
||||
"qualification_scope": "Actual four-frame driver-monitoring ModelState and both camera warps; 88 output arrays and state histories exact per camera against raw native captures. No road acceptance or speedup asserted.",
|
||||
"qualification_sha256": "6d9b4ad05d673a352f152b2b6327bcff6de6dbc76c3c96204fb839b67b920fc0",
|
||||
"source_geometry": {
|
||||
"border_fill": 16,
|
||||
"camera": [
|
||||
1928,
|
||||
1208
|
||||
],
|
||||
"layout": "luma",
|
||||
"nv12": [
|
||||
2048,
|
||||
1216,
|
||||
608,
|
||||
4804608
|
||||
],
|
||||
"transform_device": "NPY",
|
||||
"warp_to": [
|
||||
1440,
|
||||
960
|
||||
]
|
||||
}
|
||||
},
|
||||
"dmonitoring_model_tinygrad.pkl": {
|
||||
"artifact_sha256": "2dfdf58712f29be92d37f0b6b06d6ae78626831c3b9e7a0c990d40e8ef84b3a2",
|
||||
"artifact_size": 20778338,
|
||||
"qualification_scope": "Actual four-frame driver-monitoring ModelState and both camera warps; 88 output arrays and state histories exact per camera against raw native captures. No road acceptance or speedup asserted.",
|
||||
"qualification_sha256": "6d9b4ad05d673a352f152b2b6327bcff6de6dbc76c3c96204fb839b67b920fc0",
|
||||
"source_bytes": 7844499,
|
||||
"source_sha256": "dee5a294e8afaacc9295ac5d100e00733ecac278e79264b96331e40a3ede1b04"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
"""Reviewed local driving-model catalog. This module never loads model artifacts."""
|
||||
|
||||
from dataclasses import dataclass, replace
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.starpilot.models.runtime_selection import NATIVE_ABI, NATIVE_REVISION
|
||||
|
||||
|
||||
BUNDLED_CURRENT = "bundled-current"
|
||||
DEFAULT_SMALL = "rdf43"
|
||||
@@ -14,6 +16,7 @@ GENERATION = "v26"
|
||||
COMPILER_REVISION = "9d0446a4ba8a532c8b674fb6ad795af015cd9dcf"
|
||||
ARTIFACT_ABI = "tinygrad_single_v1_arena"
|
||||
CATALOG_PATH = Path(__file__).with_name("catalog-v26.json")
|
||||
NATIVE_CATALOG_PATH = Path(__file__).with_name("catalog-native02e.json")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -33,10 +36,10 @@ BUNDLED = (
|
||||
ModelEntry(
|
||||
model_id=BUNDLED_CURRENT,
|
||||
name="Regret Driven Framework V4",
|
||||
source_sha256=DEFAULT_SMALL_SHA256,
|
||||
source_sha256="",
|
||||
source_path="openpilot/selfdrive/modeld/models/rdf43_driving_tinygrad.pkl",
|
||||
compiler_revision="9d0446a4ba8a532c8b674fb6ad795af015cd9dcf",
|
||||
artifact_abi=ARTIFACT_ABI,
|
||||
compiler_revision=NATIVE_REVISION,
|
||||
artifact_abi=NATIVE_ABI,
|
||||
selectable=True,
|
||||
version="v15",
|
||||
),
|
||||
@@ -44,9 +47,10 @@ BUNDLED = (
|
||||
|
||||
CATALOG = BUNDLED + tuple(ModelEntry(
|
||||
model_id=m["id"], name=m["name"],
|
||||
source_sha256=DEFAULT_SMALL_SHA256 if m["id"] == DEFAULT_SMALL else "",
|
||||
source_sha256="",
|
||||
source_path="openpilot/selfdrive/modeld/models/rdf43_driving_tinygrad.pkl" if m["id"] == DEFAULT_SMALL else "",
|
||||
compiler_revision=COMPILER_REVISION, artifact_abi=ARTIFACT_ABI, selectable=True,
|
||||
compiler_revision=NATIVE_REVISION if m["id"] == DEFAULT_SMALL else COMPILER_REVISION,
|
||||
artifact_abi=NATIVE_ABI if m["id"] == DEFAULT_SMALL else ARTIFACT_ABI, selectable=True,
|
||||
version=m["version"], uses_external_gpu=bool(m.get("uses_external_gpu", False)),
|
||||
) for m in json.loads(CATALOG_PATH.read_text())["models"])
|
||||
|
||||
@@ -66,9 +70,11 @@ def _cached_entries(root: Path, identities: tuple) -> tuple[ModelEntry, ...]:
|
||||
from openpilot.starpilot.models.manager import catalog
|
||||
entries = dict(BY_ID)
|
||||
for model_id, row in catalog(root).items():
|
||||
entries[model_id] = (replace(BY_ID[model_id], name=row["name"]) if model_id in BY_ID else
|
||||
ModelEntry(model_id, row["name"], "", "", COMPILER_REVISION, ARTIFACT_ABI,
|
||||
True, row["version"], row.get("uses_external_gpu", False)))
|
||||
entries[model_id] = ModelEntry(model_id, row["name"],
|
||||
row.get("artifact_sha256", "") if model_id == DEFAULT_SMALL else "",
|
||||
BY_ID[model_id].source_path if model_id in BY_ID else "",
|
||||
row.get("compiler_revision", COMPILER_REVISION), row.get("artifact_format", ARTIFACT_ABI),
|
||||
True, row["version"], row.get("uses_external_gpu", False))
|
||||
return tuple(entries.values())
|
||||
|
||||
|
||||
@@ -76,7 +82,8 @@ def model_entries(root: Path | None = None) -> dict[str, ModelEntry]:
|
||||
"""Resolve validated cached metadata without mutating the bundled catalog."""
|
||||
from openpilot.starpilot.models.manager import ROOT
|
||||
root = ROOT if root is None else root
|
||||
identities = (_document_identity(CATALOG_PATH), _document_identity(root / "catalog.json"))
|
||||
identities = (_document_identity(CATALOG_PATH), _document_identity(root / "catalog.json"),
|
||||
_document_identity(NATIVE_CATALOG_PATH), _document_identity(root / "native02e-v1/catalog.json"))
|
||||
return {entry.model_id: entry for entry in _cached_entries(root, identities)}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,753 @@
|
||||
"""Compile StarPilot unified driving artifacts against the vendored tinygrad runtime."""
|
||||
import argparse
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import pickle
|
||||
import tempfile
|
||||
import time
|
||||
from collections import namedtuple
|
||||
from functools import partial
|
||||
|
||||
import numpy as np
|
||||
from tinygrad import Device, TinyJit, Context, Tensor
|
||||
from tinygrad.dtype import _to_np_dtype, dtypes
|
||||
from tinygrad.device import Buffer, BufferSpec
|
||||
from tinygrad.uop.ops import UOp
|
||||
from tinygrad_repo.examples.openpilot.helpers import dump_pickle, load_pickle
|
||||
|
||||
COMPILER_REVISION = "02e698160eed1751738212144a7ea2cfb160b3f1"
|
||||
ARTIFACT_ABI = "tinygrad_single_v2_arena"
|
||||
_PRE_HCQ_RECORDS = {}
|
||||
|
||||
|
||||
def make_metadata_dict(path, runner):
|
||||
from tinygrad.nn.onnx import OnnxPBParser
|
||||
|
||||
parser = OnnxPBParser(path, load_external_data=False)
|
||||
properties, output_shapes = {}, {}
|
||||
for field, wire_type in parser._parse_message(parser.reader.len):
|
||||
if field == 14:
|
||||
entry = parser._parse_StringStringEntryProto()
|
||||
properties[entry['key']] = entry['value']
|
||||
elif field == 7:
|
||||
for field, wire_type in parser._parse_message(parser._decode_end_pos()):
|
||||
if field == 12:
|
||||
value = parser._parse_ValueInfoProto()
|
||||
output_shapes[value['name']] = value['parsed_type'].shape if value['parsed_type'] is not None else ()
|
||||
else:
|
||||
parser.reader.skip_field(wire_type)
|
||||
else:
|
||||
parser.reader.skip_field(wire_type)
|
||||
if "output_slices" not in properties:
|
||||
raise ValueError(f"Missing output_slices metadata: {path}")
|
||||
return {
|
||||
"model_checkpoint": properties.get("model_checkpoint"),
|
||||
"output_slices": pickle.loads(base64.b64decode(properties["output_slices"])),
|
||||
"input_shapes": {name: tuple(spec.shape) for name, spec in runner.graph_inputs.items()},
|
||||
"output_shapes": output_shapes,
|
||||
}
|
||||
|
||||
|
||||
ARTIFACT_FORMAT_VERSION = 1
|
||||
MODEL_TYPES = ("vision_policy", "vision_multi_policy", "supercombo")
|
||||
NV12Frame = namedtuple("NV12Frame", ["width", "height", "stride", "y_height", "uv_height", "size"])
|
||||
IMAGE_HISTORY_IN_WARP = "warp"
|
||||
IMAGE_HISTORY_IN_POLICY = "policy"
|
||||
IMAGE_HISTORY_PIPELINES = (IMAGE_HISTORY_IN_WARP, IMAGE_HISTORY_IN_POLICY)
|
||||
LEGACY_WARP_INPUTS = ("img_q", "big_img_q", "tfm", "big_tfm")
|
||||
FAST_WARP_INPUTS = ("tfm", "big_tfm")
|
||||
BASE_POLICY_INPUTS = ("feat_q", "desire_q", "packed_npy_inputs")
|
||||
FAST_POLICY_INPUTS = ("img_q", "big_img_q", *BASE_POLICY_INPUTS)
|
||||
WARP_INPUTS = LEGACY_WARP_INPUTS
|
||||
SPLIT_POLICY_INPUTS = BASE_POLICY_INPUTS
|
||||
SUPERCOMBO_POLICY_INPUTS = BASE_POLICY_INPUTS
|
||||
WARP_DEV = os.getenv("WARP_DEV")
|
||||
|
||||
|
||||
|
||||
def _detect_desire_key(input_shapes):
|
||||
return next((key for key in input_shapes if key.startswith("desire")), None)
|
||||
|
||||
|
||||
def _detect_vision_keys(input_shapes):
|
||||
image_keys = sorted(key for key in input_shapes if "img" in key)
|
||||
road_key = next((key for key in image_keys if "big" not in key), None)
|
||||
wide_key = next((key for key in image_keys if "big" in key), None)
|
||||
if road_key is None or wide_key is None:
|
||||
raise ValueError(f"Cannot determine road/wide image keys from {list(input_shapes)}")
|
||||
return road_key, wide_key
|
||||
|
||||
|
||||
def derive_frame_skip(input_shapes):
|
||||
features_shape = input_shapes.get("features_buffer")
|
||||
if features_shape is None:
|
||||
return 1
|
||||
return 1 if features_shape[1] >= 99 else 4
|
||||
|
||||
|
||||
def make_random_images(keys, shape, device=None, *, rng=None):
|
||||
return {key: Tensor.randint(shape, low=0, high=256, dtype="uint8", device=device).realize() for key in keys}
|
||||
|
||||
|
||||
def make_random_blob_images(keys, size, device=None):
|
||||
keepalive: list[np.ndarray] = []
|
||||
|
||||
def make_inputs(*, rng):
|
||||
nonlocal keepalive
|
||||
keepalive = []
|
||||
tensors = {}
|
||||
for key in keys:
|
||||
frame = (32 * rng.standard_normal(size).astype(np.float32) + 128).clip(0, 255).astype(np.uint8)
|
||||
keepalive.append(frame)
|
||||
tensors[key] = Tensor.from_blob(frame.ctypes.data, (size,), dtype="uint8", device=device).realize()
|
||||
return tensors
|
||||
|
||||
return make_inputs
|
||||
|
||||
|
||||
def warp_perspective_tinygrad(src_flat, matrix_inverse, dst_shape, src_shape, stride_pad, border_fill_val=None):
|
||||
width_dst, height_dst = dst_shape
|
||||
height_src, width_src = src_shape
|
||||
|
||||
x = Tensor.arange(width_dst).reshape(1, width_dst).expand(height_dst, width_dst).reshape(-1)
|
||||
y = Tensor.arange(height_dst).reshape(height_dst, 1).expand(height_dst, width_dst).reshape(-1)
|
||||
|
||||
src_x = matrix_inverse[0, 0] * x + matrix_inverse[0, 1] * y + matrix_inverse[0, 2]
|
||||
src_y = matrix_inverse[1, 0] * x + matrix_inverse[1, 1] * y + matrix_inverse[1, 2]
|
||||
src_w = matrix_inverse[2, 0] * x + matrix_inverse[2, 1] * y + matrix_inverse[2, 2]
|
||||
src_x = src_x / src_w
|
||||
src_y = src_y / src_w
|
||||
|
||||
x_round = Tensor.round(src_x)
|
||||
y_round = Tensor.round(src_y)
|
||||
x_nn_clipped = x_round.clip(0, width_src - 1).cast("int")
|
||||
y_nn_clipped = y_round.clip(0, height_src - 1).cast("int")
|
||||
sampled = src_flat[y_nn_clipped * (width_src + stride_pad) + x_nn_clipped]
|
||||
|
||||
if border_fill_val is None:
|
||||
return sampled
|
||||
|
||||
in_bounds = ((x_round >= 0) & (x_round <= width_src - 1) &
|
||||
(y_round >= 0) & (y_round <= height_src - 1)).cast(sampled.dtype)
|
||||
return sampled * in_bounds + Tensor(border_fill_val, dtype=sampled.dtype) * (1 - in_bounds)
|
||||
|
||||
|
||||
def frames_to_tensor(frames):
|
||||
height = (frames.shape[0] * 2) // 3
|
||||
width = frames.shape[1]
|
||||
return Tensor.cat(
|
||||
frames[0:height:2, 0::2],
|
||||
frames[1:height:2, 0::2],
|
||||
frames[0:height:2, 1::2],
|
||||
frames[1:height:2, 1::2],
|
||||
frames[height:height + height // 4].reshape((height // 2, width // 2)),
|
||||
frames[height + height // 4:height + height // 2].reshape((height // 2, width // 2)),
|
||||
dim=0,
|
||||
).reshape((6, height // 2, width // 2))
|
||||
|
||||
|
||||
def make_frame_prepare(nv12: NV12Frame, model_w, model_h):
|
||||
cam_w, cam_h, stride, y_height, uv_height, _ = nv12
|
||||
uv_offset = stride * y_height
|
||||
stride_pad = stride - cam_w
|
||||
|
||||
def frame_prepare(input_frame, matrix_inverse):
|
||||
matrix_inverse_uv = matrix_inverse * Tensor(
|
||||
[[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]],
|
||||
device=WARP_DEV,
|
||||
)
|
||||
uv = input_frame[uv_offset:uv_offset + uv_height * stride].reshape(uv_height, stride)
|
||||
with Context(SPLIT_REDUCEOP=0):
|
||||
y = warp_perspective_tinygrad(
|
||||
input_frame[:cam_h * stride], matrix_inverse, (model_w, model_h), (cam_h, cam_w), stride_pad,
|
||||
).realize()
|
||||
u = warp_perspective_tinygrad(
|
||||
uv[:cam_h // 2, :cam_w:2].flatten(), matrix_inverse_uv,
|
||||
(model_w // 2, model_h // 2), (cam_h // 2, cam_w // 2), 0,
|
||||
).realize()
|
||||
v = warp_perspective_tinygrad(
|
||||
uv[:cam_h // 2, 1:cam_w:2].flatten(), matrix_inverse_uv,
|
||||
(model_w // 2, model_h // 2), (cam_h // 2, cam_w // 2), 0,
|
||||
).realize()
|
||||
return frames_to_tensor(y.cat(u).cat(v).reshape((model_h * 3 // 2, model_w)))
|
||||
|
||||
return frame_prepare
|
||||
|
||||
|
||||
def make_warp_input_queues(vision_input_shapes, frame_skip, device):
|
||||
road_key, _ = _detect_vision_keys(vision_input_shapes)
|
||||
image_shape = vision_input_shapes[road_key]
|
||||
frame_count = image_shape[1] // 6
|
||||
image_buffer_shape = (frame_skip * (frame_count - 1) + 1, 6, image_shape[2], image_shape[3])
|
||||
|
||||
npy = {
|
||||
"tfm": np.zeros((3, 3), dtype=np.float32),
|
||||
"big_tfm": np.zeros((3, 3), dtype=np.float32),
|
||||
}
|
||||
queues = {
|
||||
"img_q": Tensor(np.zeros(image_buffer_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||
"big_img_q": Tensor(np.zeros(image_buffer_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||
**{key: Tensor(value, device="NPY").realize() for key, value in npy.items()},
|
||||
}
|
||||
return queues, npy
|
||||
|
||||
|
||||
def _packed_policy_shapes(input_shapes, include_prev_feature=False):
|
||||
desire_key = _detect_desire_key(input_shapes)
|
||||
if desire_key is None:
|
||||
raise ValueError(f"No desire input found in {list(input_shapes)}")
|
||||
|
||||
shapes = {"desire": (input_shapes[desire_key][2],)}
|
||||
for key, shape in input_shapes.items():
|
||||
if key in ("features_buffer", desire_key) or "img" in key:
|
||||
continue
|
||||
shapes[key] = tuple(shape)
|
||||
if include_prev_feature:
|
||||
features_shape = input_shapes["features_buffer"]
|
||||
shapes["prev_feat"] = (features_shape[0], math.prod(features_shape[2:]))
|
||||
return shapes, [math.prod(shape) for shape in shapes.values()]
|
||||
|
||||
|
||||
def make_split_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, device):
|
||||
queues, npy = make_warp_input_queues(vision_input_shapes, frame_skip, device)
|
||||
features_shape = policy_input_shapes["features_buffer"]
|
||||
feature_dim = math.prod(features_shape[2:])
|
||||
desire_key = _detect_desire_key(policy_input_shapes)
|
||||
desire_shape = policy_input_shapes[desire_key]
|
||||
packed_shapes, packed_sizes = _packed_policy_shapes(policy_input_shapes)
|
||||
|
||||
packed_inputs = np.zeros(sum(packed_sizes), dtype=np.float32)
|
||||
npy.update({
|
||||
key: value.reshape(shape)
|
||||
for (key, shape), value in zip(
|
||||
packed_shapes.items(), np.split(packed_inputs, np.cumsum(packed_sizes[:-1])), strict=True,
|
||||
)
|
||||
})
|
||||
queues.update({
|
||||
"feat_q": Tensor(
|
||||
np.zeros((frame_skip * (features_shape[1] - 1) + 1, features_shape[0], feature_dim), dtype=np.float32),
|
||||
device=device,
|
||||
).contiguous().realize(),
|
||||
"desire_q": Tensor(
|
||||
np.zeros((frame_skip * desire_shape[1], desire_shape[0], desire_shape[2]), dtype=np.float32),
|
||||
device=device,
|
||||
).contiguous().realize(),
|
||||
"packed_npy_inputs": Tensor(packed_inputs, device="NPY").realize(),
|
||||
})
|
||||
return queues, npy
|
||||
|
||||
|
||||
def make_supercombo_input_queues(input_shapes, frame_skip, device):
|
||||
queues, npy = make_warp_input_queues(input_shapes, frame_skip, device)
|
||||
features_shape = input_shapes["features_buffer"]
|
||||
feature_dim = math.prod(features_shape[2:])
|
||||
desire_key = _detect_desire_key(input_shapes)
|
||||
desire_shape = input_shapes[desire_key]
|
||||
packed_shapes, packed_sizes = _packed_policy_shapes(input_shapes, include_prev_feature=True)
|
||||
|
||||
packed_inputs = np.zeros(sum(packed_sizes), dtype=np.float32)
|
||||
npy.update({
|
||||
key: value.reshape(shape)
|
||||
for (key, shape), value in zip(
|
||||
packed_shapes.items(), np.split(packed_inputs, np.cumsum(packed_sizes[:-1])), strict=True,
|
||||
)
|
||||
})
|
||||
queues.update({
|
||||
"feat_q": Tensor(
|
||||
np.zeros((frame_skip * features_shape[1], features_shape[0], feature_dim), dtype=np.float32),
|
||||
device=device,
|
||||
).contiguous().realize(),
|
||||
"desire_q": Tensor(
|
||||
np.zeros((frame_skip * desire_shape[1], desire_shape[0], desire_shape[2]), dtype=np.float32),
|
||||
device=device,
|
||||
).contiguous().realize(),
|
||||
"packed_npy_inputs": Tensor(packed_inputs, device="NPY").realize(),
|
||||
})
|
||||
return queues, npy
|
||||
|
||||
|
||||
def shift_and_sample(buffer, new_value, sample_fn):
|
||||
buffer.assign(buffer[1:].cat(new_value, dim=0).contiguous())
|
||||
return sample_fn(buffer)
|
||||
|
||||
|
||||
def sample_skip(buffer, frame_skip):
|
||||
return buffer[::frame_skip].contiguous().flatten(0, 1).unsqueeze(0)
|
||||
|
||||
|
||||
def sample_desire(buffer, frame_skip):
|
||||
return buffer.reshape(-1, frame_skip, *buffer.shape[1:]).max(1).flatten(0, 1).unsqueeze(0)
|
||||
|
||||
|
||||
def make_warp(nv12, model_w, model_h, frame_skip, image_history_pipeline=IMAGE_HISTORY_IN_POLICY):
|
||||
frame_prepare = make_frame_prepare(nv12, model_w, model_h)
|
||||
|
||||
if image_history_pipeline == IMAGE_HISTORY_IN_POLICY:
|
||||
def warp(tfm, big_tfm, frame, big_frame):
|
||||
tfm = tfm.to(WARP_DEV)
|
||||
big_tfm = big_tfm.to(WARP_DEV)
|
||||
Tensor.realize(tfm, big_tfm)
|
||||
|
||||
return Tensor.cat(
|
||||
frame_prepare(frame, tfm).unsqueeze(0),
|
||||
frame_prepare(big_frame, big_tfm).unsqueeze(0),
|
||||
)
|
||||
|
||||
return warp
|
||||
|
||||
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip)
|
||||
|
||||
def warp_enqueue(img_q, big_img_q, tfm, big_tfm, frame, big_frame):
|
||||
tfm = tfm.to(WARP_DEV)
|
||||
big_tfm = big_tfm.to(WARP_DEV)
|
||||
Tensor.realize(tfm, big_tfm)
|
||||
|
||||
warped = Tensor.cat(
|
||||
frame_prepare(frame, tfm).unsqueeze(0),
|
||||
frame_prepare(big_frame, big_tfm).unsqueeze(0),
|
||||
).to(Device.DEFAULT)
|
||||
img = shift_and_sample(img_q, warped[0:1], sample_skip_fn)
|
||||
big_img = shift_and_sample(big_img_q, warped[1:2], sample_skip_fn)
|
||||
return img, big_img
|
||||
|
||||
return warp_enqueue
|
||||
|
||||
|
||||
def make_run_split_policy(vision_runner, policy_runners, metadata, policy_order, frame_skip,
|
||||
image_history_pipeline=IMAGE_HISTORY_IN_POLICY):
|
||||
sample_desire_fn = partial(sample_desire, frame_skip=frame_skip)
|
||||
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip)
|
||||
vision_metadata = metadata["vision"]
|
||||
policy_metadata = metadata[policy_order[0]]
|
||||
vision_features_slice = vision_metadata["output_slices"]["hidden_state"]
|
||||
desire_key = _detect_desire_key(policy_metadata["input_shapes"])
|
||||
packed_shapes, packed_sizes = _packed_policy_shapes(policy_metadata["input_shapes"])
|
||||
road_key, wide_key = _detect_vision_keys(vision_metadata["input_shapes"])
|
||||
|
||||
def run_model(img, big_img, feat_q, desire_q, packed_npy_inputs):
|
||||
unpacked = {
|
||||
key: tensor.reshape(shape)
|
||||
for (key, shape), tensor in zip(
|
||||
packed_shapes.items(), packed_npy_inputs.split(packed_sizes), strict=True,
|
||||
)
|
||||
}
|
||||
desire_buffer = shift_and_sample(
|
||||
desire_q, unpacked.pop("desire").reshape(1, 1, -1), sample_desire_fn,
|
||||
)
|
||||
vision_output = next(iter(vision_runner({road_key: img, wide_key: big_img}).values())).cast("float32")
|
||||
new_feature = vision_output[:, vision_features_slice].reshape(1, -1).unsqueeze(0)
|
||||
features_buffer = shift_and_sample(feat_q, new_feature, sample_skip_fn)
|
||||
features_buffer = features_buffer.reshape(policy_metadata["input_shapes"]["features_buffer"])
|
||||
|
||||
policy_inputs = {
|
||||
"features_buffer": features_buffer,
|
||||
desire_key: desire_buffer,
|
||||
**unpacked,
|
||||
}
|
||||
policy_outputs = [
|
||||
next(iter(policy_runners[key](policy_inputs).values())).cast("float32")
|
||||
for key in policy_order
|
||||
]
|
||||
return (vision_output, *policy_outputs)
|
||||
|
||||
if image_history_pipeline == IMAGE_HISTORY_IN_POLICY:
|
||||
def run_policy(warped, img_q, big_img_q, feat_q, desire_q, packed_npy_inputs):
|
||||
packed_npy_inputs = packed_npy_inputs.to(Device.DEFAULT)
|
||||
warped = warped.to(Device.DEFAULT)
|
||||
Tensor.realize(packed_npy_inputs, warped)
|
||||
img = shift_and_sample(img_q, warped[0:1], sample_skip_fn)
|
||||
big_img = shift_and_sample(big_img_q, warped[1:2], sample_skip_fn)
|
||||
return run_model(img, big_img, feat_q, desire_q, packed_npy_inputs)
|
||||
|
||||
return run_policy
|
||||
|
||||
def run_policy(img, big_img, feat_q, desire_q, packed_npy_inputs):
|
||||
packed_npy_inputs = packed_npy_inputs.to(Device.DEFAULT).realize()
|
||||
return run_model(img, big_img, feat_q, desire_q, packed_npy_inputs)
|
||||
|
||||
return run_policy
|
||||
|
||||
|
||||
def make_run_supercombo(model_runner, metadata, frame_skip, image_history_pipeline=IMAGE_HISTORY_IN_POLICY):
|
||||
input_shapes = metadata["model"]["input_shapes"]
|
||||
sample_desire_fn = partial(sample_desire, frame_skip=frame_skip)
|
||||
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip)
|
||||
desire_key = _detect_desire_key(input_shapes)
|
||||
packed_shapes, packed_sizes = _packed_policy_shapes(input_shapes, include_prev_feature=True)
|
||||
road_key, wide_key = _detect_vision_keys(input_shapes)
|
||||
|
||||
def run_model(img, big_img, feat_q, desire_q, packed_npy_inputs):
|
||||
unpacked = {
|
||||
key: tensor.reshape(shape)
|
||||
for (key, shape), tensor in zip(
|
||||
packed_shapes.items(), packed_npy_inputs.split(packed_sizes), strict=True,
|
||||
)
|
||||
}
|
||||
desire_buffer = shift_and_sample(
|
||||
desire_q, unpacked.pop("desire").reshape(1, 1, -1), sample_desire_fn,
|
||||
)
|
||||
previous_feature = unpacked.pop("prev_feat")
|
||||
features_buffer = shift_and_sample(
|
||||
feat_q, previous_feature.reshape(1, 1, -1), sample_skip_fn,
|
||||
)
|
||||
features_buffer = features_buffer.reshape(input_shapes["features_buffer"])
|
||||
model_inputs = {
|
||||
road_key: img,
|
||||
wide_key: big_img,
|
||||
"features_buffer": features_buffer,
|
||||
desire_key: desire_buffer,
|
||||
**unpacked,
|
||||
}
|
||||
model_output = next(iter(model_runner(model_inputs).values())).cast("float32")
|
||||
return model_output,
|
||||
|
||||
if image_history_pipeline == IMAGE_HISTORY_IN_POLICY:
|
||||
def run_policy(warped, img_q, big_img_q, feat_q, desire_q, packed_npy_inputs):
|
||||
packed_npy_inputs = packed_npy_inputs.to(Device.DEFAULT)
|
||||
if (isinstance(warped.device, str) and warped.device.split(":")[0] == "QCOM" and
|
||||
Device.DEFAULT.split(":")[0] == "AMD" and getattr(Device.default, "is_usb", False)):
|
||||
warped = warped.to("CPU").realize()
|
||||
warped = warped.to(Device.DEFAULT)
|
||||
Tensor.realize(packed_npy_inputs, warped)
|
||||
img = shift_and_sample(img_q, warped[0:1], sample_skip_fn)
|
||||
big_img = shift_and_sample(big_img_q, warped[1:2], sample_skip_fn)
|
||||
return run_model(img, big_img, feat_q, desire_q, packed_npy_inputs)
|
||||
|
||||
return run_policy
|
||||
|
||||
def run_policy(img, big_img, feat_q, desire_q, packed_npy_inputs):
|
||||
packed_npy_inputs = packed_npy_inputs.to(Device.DEFAULT).realize()
|
||||
return run_model(img, big_img, feat_q, desire_q, packed_npy_inputs)
|
||||
|
||||
return run_policy
|
||||
|
||||
|
||||
def stateful_image_shapes(metadata):
|
||||
shape = tuple(metadata['input_shapes']['new_img'])
|
||||
if len(shape) != 4 or shape[:2] != (2, 6):
|
||||
raise ValueError(f"Unsupported stateful image shape: {shape}")
|
||||
return dict.fromkeys(('img', 'big_img'), (1, *shape[1:]))
|
||||
|
||||
|
||||
def stateful_host_shapes(metadata):
|
||||
return {name: shape for name, shape in metadata['input_shapes'].items()
|
||||
if name != 'new_img' and name not in metadata['state_pairs']}
|
||||
|
||||
|
||||
def make_stateful_input_queues(metadata, device):
|
||||
queues, npy = make_warp_input_queues(stateful_image_shapes(metadata), 1, device)
|
||||
shapes = stateful_host_shapes(metadata)
|
||||
sizes = [math.prod(shape) for shape in shapes.values()]
|
||||
packed = np.zeros(sum(sizes), dtype=np.float32)
|
||||
npy.update({name: value.reshape(shape) for (name, shape), value in
|
||||
zip(shapes.items(), np.split(packed, np.cumsum(sizes[:-1])), strict=True)})
|
||||
queues['packed_npy_inputs'] = Tensor(packed, device='NPY').realize()
|
||||
if metadata.get('mapped_image_input', False):
|
||||
image_shape = tuple(metadata['input_shapes']['new_img'])
|
||||
buffer = Buffer(device, math.prod(image_shape) * dtypes.uint8.itemsize,
|
||||
options=BufferSpec(cpu_access=True), preallocate=True)
|
||||
view = UOp.from_buffer(buffer, dtypes.uint8)
|
||||
queues['mapped_gpu_image'] = Tensor(view).reshape(image_shape)
|
||||
for name in metadata['state_pairs']:
|
||||
queues[name] = Tensor(np.zeros(metadata['input_shapes'][name], dtype=metadata['input_dtypes'][name]),
|
||||
device=device).contiguous().realize()
|
||||
return queues, npy
|
||||
|
||||
|
||||
def make_run_stateful_supercombo(model_runner, metadata):
|
||||
shapes = stateful_host_shapes(metadata)
|
||||
sizes = [math.prod(shape) for shape in shapes.values()]
|
||||
|
||||
def run_policy(warped, packed_npy_inputs, mapped_gpu_image=None, **state):
|
||||
packed = packed_npy_inputs.to(Device.DEFAULT).realize()
|
||||
inputs = {name: value.reshape(shape).cast(model_runner.graph_inputs[name].dtype)
|
||||
for (name, shape), value in zip(shapes.items(), packed.split(sizes), strict=True)}
|
||||
if metadata.get('mapped_image_input', False):
|
||||
if mapped_gpu_image is None:
|
||||
raise ValueError('Mapped stateful image queue is missing')
|
||||
image = warped.to('CPU').realize().to(Device.DEFAULT)
|
||||
image = mapped_gpu_image.assign(image).realize()
|
||||
else:
|
||||
image = warped.to(Device.DEFAULT)
|
||||
inputs['new_img'] = image.cast(model_runner.graph_inputs['new_img'].dtype)
|
||||
outputs = {name: value.contiguous() for name, value in model_runner(inputs | state).items()}
|
||||
for name, next_name in metadata['state_pairs'].items():
|
||||
if outputs[next_name].dtype != state[name].dtype:
|
||||
raise ValueError(f'State dtype mismatch: {name} -> {next_name}')
|
||||
Tensor.realize(*outputs.values())
|
||||
Tensor.realize(*(state[name].assign(outputs[next_name]) for name, next_name in metadata['state_pairs'].items()))
|
||||
return outputs['outputs'].cast('float32'),
|
||||
|
||||
return run_policy
|
||||
|
||||
|
||||
def compile_jit(jit, make_random_inputs, input_keys, make_queues, validation_runs=1):
|
||||
seed = 42
|
||||
|
||||
def random_inputs_run(fn, current_seed, test_values=None, test_buffers=None, expect_match=True):
|
||||
input_queues, npy = make_queues(Device.DEFAULT)
|
||||
rng = np.random.default_rng(current_seed)
|
||||
Tensor.manual_seed(current_seed)
|
||||
testing = test_values is not None or test_buffers is not None
|
||||
run_count = validation_runs if testing else max(3, validation_runs)
|
||||
values, buffers = [], []
|
||||
|
||||
for index in range(run_count):
|
||||
for value in npy.values():
|
||||
value[:] = rng.standard_normal(value.shape).astype(value.dtype)
|
||||
Device.default.synchronize()
|
||||
random_inputs = make_random_inputs(rng=rng)
|
||||
start = time.perf_counter()
|
||||
outputs = fn(**{key: input_queues[key] for key in input_keys}, **random_inputs)
|
||||
mid = time.perf_counter()
|
||||
Device.default.synchronize()
|
||||
end = time.perf_counter()
|
||||
print(f" [{index + 1}/{run_count}] enqueue {(mid - start) * 1e3:6.2f} ms -- total {(end - start) * 1e3:6.2f} ms")
|
||||
|
||||
if index < validation_runs:
|
||||
values.extend(np.copy(value.numpy()) for value in outputs)
|
||||
buffers.extend(np.copy(value.numpy()) for value in input_queues.values())
|
||||
if not all(np.isfinite(value).all() for value in values):
|
||||
raise ValueError("Compiled JIT produced non-finite outputs")
|
||||
|
||||
if test_values is not None:
|
||||
match = all(np.array_equal(lhs, rhs) for lhs, rhs in zip(values, test_values, strict=True))
|
||||
assert match == expect_match, f"outputs {'differ from' if expect_match else 'match'} baseline (seed={current_seed})"
|
||||
if test_buffers is not None:
|
||||
match = all(np.array_equal(lhs, rhs) for lhs, rhs in zip(buffers, test_buffers, strict=True))
|
||||
assert match == expect_match, f"buffers {'differ from' if expect_match else 'match'} baseline (seed={current_seed})"
|
||||
return values, buffers
|
||||
|
||||
from tinygrad.engine import realize
|
||||
from unittest.mock import patch
|
||||
original_hcq_compile = realize.hcq_compile
|
||||
schedules = []
|
||||
|
||||
def retain_schedule(linear, input_uops, profile, cache=False):
|
||||
compiled = original_hcq_compile(linear, input_uops, profile, cache=cache)
|
||||
schedules.append((linear, compiled))
|
||||
return compiled
|
||||
|
||||
print("capture + replay")
|
||||
with patch.object(realize, 'hcq_compile', retain_schedule):
|
||||
test_values, test_buffers = random_inputs_run(jit, seed)
|
||||
captured = jit.captured
|
||||
matching = [linear for linear, compiled in schedules if compiled is captured._linear]
|
||||
if len(matching) != 1:
|
||||
raise ValueError("Missing or ambiguous pre-HCQ schedule for captured JIT")
|
||||
_PRE_HCQ_RECORDS[id(jit)] = {'linear': matching[0], 'ret': captured.ret,
|
||||
'expected_names': captured.expected_names,
|
||||
'expected_input_info': captured.expected_input_info}
|
||||
print("pickle round trip")
|
||||
with tempfile.TemporaryDirectory(prefix="starpilot-jit-") as temporary:
|
||||
path = Path(temporary) / "captured.pkl"
|
||||
dump_pickle(jit, path)
|
||||
loaded = load_pickle(path, out_of_band=True)
|
||||
random_inputs_run(loaded, seed, test_values, test_buffers, expect_match=True)
|
||||
random_inputs_run(loaded, seed + 1, test_values, test_buffers, expect_match=False)
|
||||
return jit
|
||||
|
||||
|
||||
def _parse_size(value):
|
||||
width, height = value.lower().split("x")
|
||||
return int(width), int(height)
|
||||
|
||||
|
||||
def validate_metadata(metadata):
|
||||
output_shapes = metadata.get("output_shapes", {})
|
||||
output_shape = output_shapes.get("outputs")
|
||||
if not output_shape or len(output_shape) < 2:
|
||||
raise ValueError(f"Invalid model output shape metadata: {output_shapes}")
|
||||
output_size = output_shape[-1]
|
||||
for name, output_slice in metadata.get("output_slices", {}).items():
|
||||
start, stop, step = output_slice.indices(output_size)
|
||||
if step != 1 or start < 0 or stop < start or stop > output_size:
|
||||
raise ValueError(f"Invalid output slice {name}={output_slice} for output size {output_size}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--model-type", choices=MODEL_TYPES, required=True)
|
||||
parser.add_argument("--model-size", type=_parse_size, required=True)
|
||||
parser.add_argument("--camera-resolutions", type=_parse_size, nargs="+", required=True)
|
||||
parser.add_argument("--frame-skip", type=int)
|
||||
parser.add_argument("--behavior-version")
|
||||
parser.add_argument("--output", required=True)
|
||||
parser.add_argument("--out-of-band", action="store_true", help="Compatibility flag; all artifacts use the current buffer arena format.")
|
||||
parser.add_argument("--vision-onnx")
|
||||
parser.add_argument("--policy-onnx")
|
||||
parser.add_argument("--off-policy-onnx")
|
||||
parser.add_argument("--on-policy-onnx")
|
||||
parser.add_argument("--supercombo-onnx")
|
||||
parser.add_argument(
|
||||
"--image-history-pipeline",
|
||||
choices=IMAGE_HISTORY_PIPELINES,
|
||||
default=IMAGE_HISTORY_IN_POLICY,
|
||||
help="Where img/big_img history queues are updated. 'policy' is the newer faster ABI; 'warp' reproduces legacy v22 artifacts.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
if args.frame_skip is not None and args.frame_skip < 1:
|
||||
parser.error("--frame-skip must be positive")
|
||||
|
||||
from tinygrad.nn.onnx import OnnxRunner
|
||||
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||
|
||||
output = {
|
||||
"format_version": ARTIFACT_FORMAT_VERSION,
|
||||
"artifact_abi": ARTIFACT_ABI,
|
||||
"compiler_revision": COMPILER_REVISION,
|
||||
"execution_device": Device.DEFAULT,
|
||||
"warp_device": WARP_DEV or Device.DEFAULT,
|
||||
"model_type": args.model_type,
|
||||
"metadata": {},
|
||||
"image_history_pipeline": args.image_history_pipeline,
|
||||
}
|
||||
if args.behavior_version:
|
||||
output["behavior_version"] = args.behavior_version
|
||||
|
||||
if args.model_type == "supercombo":
|
||||
if not args.supercombo_onnx:
|
||||
parser.error("--supercombo-onnx is required for supercombo")
|
||||
model_path = Path(args.supercombo_onnx)
|
||||
model_runner = OnnxRunner(model_path)
|
||||
output["metadata"]["model"] = make_metadata_dict(model_path, model_runner)
|
||||
validate_metadata(output["metadata"]["model"])
|
||||
policy_shapes = output["metadata"]["model"]["input_shapes"]
|
||||
if 'new_img' in policy_shapes:
|
||||
if args.image_history_pipeline != IMAGE_HISTORY_IN_POLICY:
|
||||
parser.error('ONNX-managed history requires --image-history-pipeline policy')
|
||||
metadata = output['metadata']['model']
|
||||
metadata['state_pairs'] = {name: f'next_{name}' for name in policy_shapes
|
||||
if f'next_{name}' in metadata['output_shapes']}
|
||||
if not metadata['state_pairs']:
|
||||
raise ValueError('Stateful supercombo is missing next-state outputs')
|
||||
metadata['input_dtypes'] = {name: np.dtype(_to_np_dtype(spec.dtype)).name for name, spec in model_runner.graph_inputs.items()}
|
||||
for name, next_name in metadata['state_pairs'].items():
|
||||
if policy_shapes[name] != metadata['output_shapes'][next_name]:
|
||||
raise ValueError(f'State shape mismatch: {name} -> {next_name}')
|
||||
metadata['mapped_image_input'] = True
|
||||
frame_skip = 1
|
||||
make_policy_queues = partial(make_stateful_input_queues, metadata)
|
||||
run_policy = make_run_stateful_supercombo(model_runner, metadata)
|
||||
image_shapes = stateful_image_shapes(metadata)
|
||||
policy_input_keys = ('packed_npy_inputs', 'mapped_gpu_image', *metadata['state_pairs'])
|
||||
else:
|
||||
frame_skip = args.frame_skip or derive_frame_skip(policy_shapes)
|
||||
make_policy_queues = partial(make_supercombo_input_queues, policy_shapes, frame_skip)
|
||||
run_policy = make_run_supercombo(
|
||||
model_runner, output["metadata"], frame_skip, args.image_history_pipeline,
|
||||
)
|
||||
image_shapes = policy_shapes
|
||||
policy_input_keys = FAST_POLICY_INPUTS if args.image_history_pipeline == IMAGE_HISTORY_IN_POLICY else SUPERCOMBO_POLICY_INPUTS
|
||||
else:
|
||||
if not args.vision_onnx:
|
||||
parser.error("--vision-onnx is required for split models")
|
||||
policy_paths = {}
|
||||
if args.policy_onnx:
|
||||
policy_paths["policy"] = args.policy_onnx
|
||||
if args.off_policy_onnx:
|
||||
policy_paths["off_policy"] = args.off_policy_onnx
|
||||
if args.on_policy_onnx:
|
||||
policy_paths["on_policy"] = args.on_policy_onnx
|
||||
if args.model_type == "vision_policy" and set(policy_paths) != {"policy"}:
|
||||
parser.error("vision_policy requires --policy-onnx")
|
||||
if args.model_type == "vision_multi_policy" and not policy_paths:
|
||||
parser.error("vision_multi_policy requires at least one policy ONNX")
|
||||
|
||||
vision_path = Path(args.vision_onnx)
|
||||
resolved_policy_paths = {key: Path(path) for key, path in policy_paths.items()}
|
||||
vision_runner = OnnxRunner(vision_path)
|
||||
policy_runners = {key: OnnxRunner(path) for key, path in resolved_policy_paths.items()}
|
||||
output["metadata"]["vision"] = make_metadata_dict(vision_path, vision_runner)
|
||||
validate_metadata(output["metadata"]["vision"])
|
||||
for key, path in resolved_policy_paths.items():
|
||||
output["metadata"][key] = make_metadata_dict(path, policy_runners[key])
|
||||
validate_metadata(output["metadata"][key])
|
||||
|
||||
policy_order = [key for key in ("on_policy", "off_policy", "policy") if key in policy_runners]
|
||||
output["policy_order"] = policy_order
|
||||
first_policy_shapes = output["metadata"][policy_order[0]]["input_shapes"]
|
||||
for key in policy_order[1:]:
|
||||
if output["metadata"][key]["input_shapes"] != first_policy_shapes:
|
||||
raise ValueError(f"Policy input shapes differ between {policy_order[0]} and {key}")
|
||||
frame_skip = args.frame_skip or derive_frame_skip(first_policy_shapes)
|
||||
make_policy_queues = partial(
|
||||
make_split_input_queues,
|
||||
output["metadata"]["vision"]["input_shapes"],
|
||||
first_policy_shapes,
|
||||
frame_skip,
|
||||
)
|
||||
run_policy = make_run_split_policy(
|
||||
vision_runner, policy_runners, output["metadata"], policy_order, frame_skip,
|
||||
args.image_history_pipeline,
|
||||
)
|
||||
image_shapes = output["metadata"]["vision"]["input_shapes"]
|
||||
policy_input_keys = FAST_POLICY_INPUTS if args.image_history_pipeline == IMAGE_HISTORY_IN_POLICY else SPLIT_POLICY_INPUTS
|
||||
|
||||
output["frame_skip"] = frame_skip
|
||||
output["policy_input_keys"] = policy_input_keys
|
||||
warp_input_keys = FAST_WARP_INPUTS if args.image_history_pipeline == IMAGE_HISTORY_IN_POLICY else LEGACY_WARP_INPUTS
|
||||
output["warp_input_keys"] = warp_input_keys
|
||||
run_policy_jit = TinyJit(run_policy, prune=True)
|
||||
road_key, wide_key = _detect_vision_keys(image_shapes)
|
||||
if args.image_history_pipeline == IMAGE_HISTORY_IN_POLICY:
|
||||
make_random_model_inputs = partial(
|
||||
make_random_images,
|
||||
keys=["warped"],
|
||||
shape=(2, 6, *image_shapes[road_key][2:]),
|
||||
device=WARP_DEV,
|
||||
)
|
||||
else:
|
||||
make_random_model_inputs = partial(
|
||||
make_random_images,
|
||||
keys=[road_key, wide_key],
|
||||
shape=image_shapes[road_key],
|
||||
)
|
||||
output["run_policy"] = compile_jit(
|
||||
run_policy_jit, make_random_model_inputs, policy_input_keys, make_policy_queues,
|
||||
validation_runs=5 if output['metadata'].get('model', {}).get('state_pairs') else 1,
|
||||
)
|
||||
|
||||
model_w, model_h = args.model_size
|
||||
for cam_w, cam_h in args.camera_resolutions:
|
||||
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
|
||||
warp_enqueue = TinyJit(
|
||||
make_warp(nv12, model_w, model_h, frame_skip, args.image_history_pipeline),
|
||||
prune=True,
|
||||
)
|
||||
make_random_warp_inputs = make_random_blob_images(
|
||||
keys=["frame", "big_frame"], size=nv12.size, device=WARP_DEV,
|
||||
)
|
||||
make_warp_queues = partial(make_warp_input_queues, image_shapes, frame_skip)
|
||||
output[(cam_w, cam_h)] = compile_jit(
|
||||
warp_enqueue, make_random_warp_inputs, warp_input_keys, make_warp_queues,
|
||||
)
|
||||
|
||||
dump_pickle(output, args.output)
|
||||
with open(args.output, 'rb') as native_file:
|
||||
native_sha256 = hashlib.file_digest(native_file, 'sha256').hexdigest()
|
||||
launch_path = Path(args.output).with_suffix('.launch.pkl')
|
||||
launch = {
|
||||
'format': 'tinygrad_pre_hcq_v1', 'compiler_revision': COMPILER_REVISION,
|
||||
'artifact_abi': ARTIFACT_ABI, 'native_artifact_sha256': native_sha256,
|
||||
'native_artifact_name': Path(args.output).name,
|
||||
'captures': {key: _PRE_HCQ_RECORDS.pop(id(value)) for key, value in output.items()
|
||||
if key == 'run_policy' or isinstance(key, tuple)},
|
||||
}
|
||||
dump_pickle(launch, launch_path)
|
||||
with launch_path.open('rb') as launch_file:
|
||||
launch_sha256 = hashlib.file_digest(launch_file, 'sha256').hexdigest()
|
||||
launch_path.with_suffix('.json').write_text(json.dumps({
|
||||
'format': launch['format'], 'compiler_revision': COMPILER_REVISION, 'artifact_abi': ARTIFACT_ABI,
|
||||
'native_artifact_sha256': native_sha256, 'launch_sha256': launch_sha256,
|
||||
}, indent=2) + '\n')
|
||||
print(f"Saved JITs to {args.output} ({os.path.getsize(args.output) / 1e6:.2f} MB)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -22,7 +22,11 @@ from urllib.request import urlopen
|
||||
|
||||
from openpilot.starpilot.models.catalog import (
|
||||
ARTIFACT_ABI, BUNDLED_CURRENT, BY_ID, CATALOG_PATH, COMPILER_REVISION, DEFAULT_SMALL, DEFAULT_SMALL_SHA256,
|
||||
DEFAULT_SMALL_SIZE, GENERATION, model_entries,
|
||||
DEFAULT_SMALL_SIZE, GENERATION, NATIVE_CATALOG_PATH, model_entries,
|
||||
)
|
||||
|
||||
from openpilot.starpilot.models.runtime_selection import (
|
||||
NATIVE_ABI, NATIVE_GENERATION, NATIVE_REVISION, RuntimePlan, freeze_plan, selection_family,
|
||||
)
|
||||
|
||||
RESOURCE_URL = "https://huggingface.co/buckets/StarPilot-Driving/StarPilot-Resources/resolve"
|
||||
@@ -74,9 +78,12 @@ def state_lock(root: Path):
|
||||
yield
|
||||
|
||||
|
||||
def validate_manifest(data: object) -> dict[str, dict]:
|
||||
if (not isinstance(data, dict) or data.get("generation") != GENERATION or data.get("compiler_revision") != COMPILER_REVISION or
|
||||
data.get("artifact_abi") != ARTIFACT_ABI or not isinstance(data.get("models"), list)):
|
||||
def validate_manifest(data: object, *, native: bool = False) -> dict[str, dict]:
|
||||
generation = NATIVE_GENERATION if native else GENERATION
|
||||
revision = NATIVE_REVISION if native else COMPILER_REVISION
|
||||
abi = NATIVE_ABI if native else ARTIFACT_ABI
|
||||
if (not isinstance(data, dict) or data.get("generation") != generation or data.get("compiler_revision") != revision or
|
||||
data.get("artifact_abi") != abi or not isinstance(data.get("models"), list)):
|
||||
raise ModelError("The model catalog targets a different runtime")
|
||||
rows = {}
|
||||
for entry in data["models"]:
|
||||
@@ -107,7 +114,7 @@ def validate_manifest(data: object) -> dict[str, dict]:
|
||||
amd = variants.get("amd")
|
||||
if amd is not None and (not isinstance(amd, dict) or entry.get("uses_external_gpu", False) or
|
||||
entry.get("model_lab_eligible") is not True or amd.get("execution_device") != "AMD" or
|
||||
amd.get("compiler_revision") != COMPILER_REVISION):
|
||||
amd.get("compiler_revision") != revision):
|
||||
raise ModelError("Invalid laboratory artifact hardware or runtime")
|
||||
runtime_artifacts = entry.get("runtime_artifacts", [])
|
||||
if not isinstance(runtime_artifacts, list):
|
||||
@@ -127,7 +134,7 @@ def validate_manifest(data: object) -> dict[str, dict]:
|
||||
size, chunks = artifact.get("artifact_size"), artifact.get("artifact_chunk_count")
|
||||
if (not isinstance(artifact["artifact_sha256"], str) or not SHA.fullmatch(artifact["artifact_sha256"]) or
|
||||
type(size) is not int or not 0 < size <= MAX_ARTIFACT or type(chunks) is not int or not 0 <= chunks <= 99 or
|
||||
artifact.get("artifact_format") != ARTIFACT_ABI):
|
||||
artifact.get("artifact_format") != abi):
|
||||
raise ModelError("Invalid compiled artifact metadata")
|
||||
row = dict(entry)
|
||||
compatible = [artifact for artifact in runtime_artifacts if artifact["min_runner_revision"] <= MODEL_RUNNER_REVISION]
|
||||
@@ -136,19 +143,97 @@ def validate_manifest(data: object) -> dict[str, dict]:
|
||||
row.update({key: value for key, value in selected.items() if key != "min_runner_revision"})
|
||||
if known is None and "artifact_sha256" not in row:
|
||||
raise ModelError("New models require a compatible compiled artifact")
|
||||
if native and (model_id != DEFAULT_SMALL and row.get("uses_external_gpu") is not True or
|
||||
"artifact_sha256" not in row or not isinstance(row.get("qualification_sha256"), str) or
|
||||
not SHA.fullmatch(row["qualification_sha256"]) or variants or runtime_artifacts):
|
||||
raise ModelError("Native rows require qualified RDF or Chestnut artifacts")
|
||||
row.pop("_artifact_path", None)
|
||||
row.update(compiler_revision=revision, _runtime_family="native02e" if native else "legacy9d",
|
||||
_generation=generation)
|
||||
rows[model_id] = row
|
||||
return rows
|
||||
|
||||
|
||||
def native_catalog(root: Path = ROOT) -> dict[str, dict]:
|
||||
rows = {}
|
||||
for path in (NATIVE_CATALOG_PATH, root / NATIVE_GENERATION / "catalog.json"):
|
||||
try:
|
||||
rows.update(validate_manifest(read_json(path), native=True))
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
return rows
|
||||
|
||||
|
||||
def native_stock(root: Path = ROOT) -> dict[str, dict]:
|
||||
# Stock DM is release-pinned, never supplied by the downloadable model catalog.
|
||||
document = read_json(NATIVE_CATALOG_PATH)
|
||||
validate_manifest(document, native=True)
|
||||
stock = document.get("stock")
|
||||
required = {"dmonitoring_model_tinygrad.pkl", "dm_warp_1928x1208_tinygrad.pkl", "dm_warp_1344x760_tinygrad.pkl"}
|
||||
if not isinstance(stock, dict) or set(stock) != required:
|
||||
raise ModelError("Native monitoring release is not qualified")
|
||||
for row in stock.values():
|
||||
if (not isinstance(row, dict) or not isinstance(row.get("artifact_sha256"), str) or
|
||||
not SHA.fullmatch(row["artifact_sha256"]) or type(row.get("artifact_size")) is not int or
|
||||
not 0 < row["artifact_size"] <= MAX_ARTIFACT or not isinstance(row.get("qualification_sha256"), str) or
|
||||
not SHA.fullmatch(row["qualification_sha256"])):
|
||||
raise ModelError("Invalid native monitoring artifact identity")
|
||||
return stock
|
||||
|
||||
|
||||
def catalog(root: Path = ROOT) -> dict[str, dict]:
|
||||
base = validate_manifest(read_json(CATALOG_PATH))
|
||||
try:
|
||||
base.update(validate_manifest(read_json(root / "catalog.json")))
|
||||
except (OSError, ValueError, TypeError):
|
||||
pass
|
||||
# Qualified native entries replace only matching IDs; all other v26 payloads remain usable.
|
||||
try:
|
||||
base.update(native_catalog(root))
|
||||
except (OSError, ValueError, TypeError):
|
||||
pass
|
||||
return base
|
||||
|
||||
|
||||
def default_record(family: str, root: Path = ROOT) -> dict:
|
||||
if family == "legacy9d":
|
||||
return {"path": str(SHIPPED_MODELS / "compat9d" / f"{DEFAULT_SMALL}_driving_tinygrad.pkl"),
|
||||
"artifact_sha256": DEFAULT_SMALL_SHA256, "artifact_size": DEFAULT_SMALL_SIZE,
|
||||
"version": BY_ID[DEFAULT_SMALL].version, "runtime_family": family}
|
||||
if family != "native02e":
|
||||
raise ModelError("Unknown default runtime")
|
||||
row = native_catalog(root).get(DEFAULT_SMALL)
|
||||
if row is None:
|
||||
raise ModelError("Native RDF default is not qualified")
|
||||
native_stock(root)
|
||||
return {"path": str(SHIPPED_MODELS / f"{DEFAULT_SMALL}_driving_tinygrad.pkl"),
|
||||
"artifact_sha256": row["artifact_sha256"], "artifact_size": row["artifact_size"],
|
||||
"version": row["version"], "runtime_family": family}
|
||||
|
||||
|
||||
def freeze_runtime(*, root: Path = ROOT, recovery_small_only: bool = False,
|
||||
chestnut_available: bool | None = None, randomize: bool = True) -> RuntimePlan:
|
||||
# All called modules are stdlib-only until prepare() chooses the family.
|
||||
if chestnut_available is None:
|
||||
from openpilot.selfdrive.modeld.helpers import chestnut_present
|
||||
from openpilot.common.hardware.usb import cable_connected
|
||||
from openpilot.starpilot.models.jetlink_adapter import link_on
|
||||
chestnut_available = chestnut_present() or (cable_connected() and not link_on())
|
||||
if randomize and not recovery_small_only:
|
||||
randomize_next_start(chestnut_available, root=root)
|
||||
with state_lock(root):
|
||||
prefs, entries = preferences(root), catalog(root)
|
||||
family = selection_family(prefs, entries, recovery_small_only=recovery_small_only)
|
||||
for mid, row in entries.items():
|
||||
directory = root / NATIVE_GENERATION if row.get("_runtime_family") == "native02e" else root
|
||||
row["_artifact_path"] = str(directory / mid / f"{mid}_driving_tinygrad.pkl")
|
||||
fallback = default_record(family, root)
|
||||
plan = freeze_plan(prefs, entries, fallback, recovery_small_only=recovery_small_only)
|
||||
if shipped_default(plan) is None:
|
||||
raise ModelError("Selected runtime RDF fallback is missing or corrupt")
|
||||
return plan
|
||||
|
||||
|
||||
def preferences(root: Path = ROOT) -> dict:
|
||||
default = {"small": DEFAULT_SMALL, "big": "", "userFavorites": [], "sortMode": "name",
|
||||
"randomizer": False, "blacklistedModels": []}
|
||||
@@ -172,14 +257,16 @@ def preferences(root: Path = ROOT) -> dict:
|
||||
return default
|
||||
|
||||
|
||||
def artifact_path(model_id: str, root: Path = ROOT, variant: str = "standard") -> Path:
|
||||
def artifact_path(model_id: str, root: Path = ROOT, variant: str = "standard", *, family: str | None = None) -> Path:
|
||||
entries = model_entries(root)
|
||||
if not isinstance(model_id, str) or model_id not in entries or model_id == BUNDLED_CURRENT:
|
||||
raise ModelError("Unknown downloadable model")
|
||||
if variant not in ("standard", "amd") or (variant == "amd" and entries[model_id].uses_external_gpu):
|
||||
raise ModelError("Unknown model artifact variant")
|
||||
suffix = "amd_" if variant == "amd" else ""
|
||||
return root / model_id / f"{model_id}_driving_{suffix}tinygrad.pkl"
|
||||
family = catalog(root).get(model_id, {}).get("_runtime_family") if family is None else family
|
||||
directory = root / NATIVE_GENERATION if family == "native02e" else root
|
||||
return directory / model_id / f"{model_id}_driving_{suffix}tinygrad.pkl"
|
||||
|
||||
|
||||
def artifact_entry(model_id: str, entries: dict[str, dict], variant: str = "standard") -> dict:
|
||||
@@ -195,7 +282,8 @@ def verified_artifact(model_id: str, entries: dict[str, dict], root: Path = ROOT
|
||||
row = artifact_entry(model_id, entries, variant)
|
||||
if "artifact_sha256" not in row:
|
||||
return None
|
||||
path = artifact_path(model_id, root, variant)
|
||||
path = Path(row["_artifact_path"]) if variant == "standard" and "_artifact_path" in row else artifact_path(
|
||||
model_id, root, variant, family=row.get("_runtime_family"))
|
||||
try:
|
||||
if path.is_symlink() or not path.is_file() or path.stat().st_size != row["artifact_size"]:
|
||||
return None
|
||||
@@ -206,13 +294,13 @@ def verified_artifact(model_id: str, entries: dict[str, dict], root: Path = ROOT
|
||||
return None
|
||||
|
||||
|
||||
def shipped_default() -> Path | None:
|
||||
def shipped_default(plan: RuntimePlan | None = None) -> Path | None:
|
||||
from openpilot.common.file_chunker import materialize_file_chunked
|
||||
|
||||
try:
|
||||
path = materialize_file_chunked(SHIPPED_MODELS / f"{DEFAULT_SMALL}_driving_tinygrad.pkl",
|
||||
expected_sha256=DEFAULT_SMALL_SHA256)
|
||||
return path if path.stat().st_size == DEFAULT_SMALL_SIZE else None
|
||||
row = json.loads(plan.default_json) if plan is not None else default_record("native02e")
|
||||
path = materialize_file_chunked(Path(row["path"]), expected_sha256=row["artifact_sha256"])
|
||||
return path if path.stat().st_size == row["artifact_size"] else None
|
||||
except (OSError, ValueError, TypeError, KeyError):
|
||||
return None
|
||||
|
||||
@@ -228,10 +316,13 @@ class RuntimeSelection:
|
||||
allow_big: bool = True
|
||||
small_sha256: str | None = None
|
||||
big_sha256: str | None = None
|
||||
fallback_path: Path | None = None
|
||||
fallback_version: str = "current"
|
||||
fallback_sha256: str | None = None
|
||||
|
||||
|
||||
def requested_runtime_id(chestnut_available: bool, *, root: Path = ROOT) -> str:
|
||||
prefs = preferences(root)
|
||||
def requested_runtime_id(chestnut_available: bool, *, root: Path = ROOT, plan: RuntimePlan | None = None) -> str:
|
||||
prefs = json.loads(plan.preferences_json) if plan is not None else preferences(root)
|
||||
return prefs["big"] if chestnut_available and prefs["big"] else prefs["small"]
|
||||
|
||||
|
||||
@@ -253,26 +344,36 @@ def randomize_next_start(chestnut_available: bool, *, root: Path = ROOT, chooser
|
||||
return prefs
|
||||
|
||||
|
||||
def resolve_runtime(chestnut_available: bool, *, root: Path = ROOT, randomize: bool = False) -> RuntimeSelection:
|
||||
prefs = randomize_next_start(chestnut_available, root=root) if randomize else preferences(root)
|
||||
entries = catalog(root)
|
||||
small, big = prefs["small"], prefs["big"]
|
||||
small_path = verified_artifact(small, entries, root)
|
||||
def resolve_runtime(chestnut_available: bool, *, root: Path = ROOT, randomize: bool = False,
|
||||
plan: RuntimePlan | None = None) -> RuntimeSelection:
|
||||
if plan is None:
|
||||
plan = freeze_runtime(root=root, chestnut_available=chestnut_available, randomize=randomize)
|
||||
elif randomize:
|
||||
raise ModelError("Randomize before freezing the model runtime")
|
||||
entries = json.loads(plan.entries_json)
|
||||
fallback = json.loads(plan.default_json)
|
||||
small, big = plan.small_id, plan.big_id
|
||||
if big and entries.get(big, {}).get("_runtime_family") != plan.family:
|
||||
raise ModelError("Big artifact does not match the selected runtime")
|
||||
if small != DEFAULT_SMALL and entries.get(small, {}).get("_runtime_family") != plan.family:
|
||||
raise ModelError("Small artifact does not match the selected runtime")
|
||||
small_path = verified_artifact(small, entries, root) if entries.get(small, {}).get("_runtime_family") == plan.family else None
|
||||
shipped = small == DEFAULT_SMALL and small_path is None
|
||||
if shipped:
|
||||
small_path = shipped_default()
|
||||
small_path = shipped_default(plan)
|
||||
if small_path is None:
|
||||
small = DEFAULT_SMALL
|
||||
shipped = True
|
||||
small_path = shipped_default()
|
||||
small_path = shipped_default(plan)
|
||||
if small_path is None:
|
||||
raise ModelError("Shipped RDFv4 is missing or corrupt; reinstall the validated model package")
|
||||
big_path = verified_artifact(big, entries, root) if big not in ("", BUNDLED_CURRENT) and chestnut_available else None
|
||||
allow_big = chestnut_available and big_path is not None
|
||||
return RuntimeSelection(small, small_path, entries[small]["version"], big, big_path,
|
||||
return RuntimeSelection(small, small_path, fallback["version"] if shipped else entries[small]["version"], big, big_path,
|
||||
entries[big]["version"] if big in entries else "current", allow_big,
|
||||
(DEFAULT_SMALL_SHA256 if shipped else entries[small]["artifact_sha256"]) if small_path is not None else None,
|
||||
entries[big]["artifact_sha256"] if big_path is not None else None)
|
||||
(fallback["artifact_sha256"] if shipped else entries[small]["artifact_sha256"]) if small_path is not None else None,
|
||||
entries[big]["artifact_sha256"] if big_path is not None else None,
|
||||
shipped_default(plan), fallback["version"], fallback["artifact_sha256"])
|
||||
|
||||
|
||||
class ModelManager:
|
||||
@@ -391,7 +492,7 @@ class ModelManager:
|
||||
parts.append(sidecar)
|
||||
identity = tuple((str(part), part.stat().st_ino, part.stat().st_size,
|
||||
part.stat().st_mtime_ns, part.stat().st_ctime_ns) for part in parts)
|
||||
return (0, 0, DEFAULT_SMALL_SIZE, 0, 0, DEFAULT_SMALL_SHA256, DEFAULT_SMALL_SIZE,
|
||||
return (0, 0, row["artifact_size"], 0, 0, row["artifact_sha256"], row["artifact_size"],
|
||||
any(part.is_symlink() for part in parts), identity)
|
||||
st = path.stat()
|
||||
return (st.st_dev, st.st_ino, st.st_size, st.st_mtime_ns, st.st_ctime_ns,
|
||||
@@ -417,7 +518,11 @@ class ModelManager:
|
||||
|
||||
def _snapshot_installed(self, mid: str, row: dict, variant: str = "standard") -> tuple[bool, bool]:
|
||||
if mid == DEFAULT_SMALL and variant == "standard" and self._signature(mid, row, variant) is None:
|
||||
row = {**row, "shipped": True, "artifact_sha256": DEFAULT_SMALL_SHA256, "artifact_size": DEFAULT_SMALL_SIZE}
|
||||
try:
|
||||
default = default_record("native02e", self.root)
|
||||
except (OSError, ValueError, TypeError, KeyError):
|
||||
return False, False
|
||||
row = {**row, "shipped": True, "artifact_sha256": default["artifact_sha256"], "artifact_size": default["artifact_size"]}
|
||||
artifact = artifact_entry(mid, {mid: row}, variant)
|
||||
if "artifact_sha256" not in artifact:
|
||||
return False, False
|
||||
@@ -643,14 +748,17 @@ class ModelManager:
|
||||
raise ModelError("Unknown model action")
|
||||
|
||||
def _refresh(self) -> None:
|
||||
url = f"{RESOURCE_URL}/manifests/model_names_{GENERATION}.json"
|
||||
with self.opener(url, timeout=30) as response:
|
||||
raw = response.read(MAX_MANIFEST + 1)
|
||||
if len(raw) > MAX_MANIFEST:
|
||||
raise ModelError("Model catalog is too large")
|
||||
data = json.loads(raw)
|
||||
validate_manifest(data)
|
||||
atomic_json(self.root / "catalog.json", data)
|
||||
documents = []
|
||||
for generation, native in ((GENERATION, False), (NATIVE_GENERATION, True)):
|
||||
with self.opener(f"{RESOURCE_URL}/manifests/model_names_{generation}.json", timeout=30) as response:
|
||||
raw = response.read(MAX_MANIFEST + 1)
|
||||
if len(raw) > MAX_MANIFEST:
|
||||
raise ModelError("Model catalog is too large")
|
||||
data = json.loads(raw)
|
||||
validate_manifest(data, native=native)
|
||||
documents.append((self.root / NATIVE_GENERATION / "catalog.json" if native else self.root / "catalog.json", data))
|
||||
for path, data in documents:
|
||||
atomic_json(path, data)
|
||||
|
||||
def _work(self, action: str, payload: dict, lock) -> None:
|
||||
failed = True
|
||||
@@ -693,7 +801,7 @@ class ModelManager:
|
||||
digest, received = hashlib.sha256(), 0
|
||||
with os.fdopen(fd, "wb") as out:
|
||||
for piece in pieces:
|
||||
with self.opener(f"{RESOURCE_URL}/models/{GENERATION}/{mid}/{piece}", timeout=30) as response:
|
||||
with self.opener(f"{RESOURCE_URL}/models/{entry.get('_generation', GENERATION)}/{mid}/{piece}", timeout=30) as response:
|
||||
while data := response.read(1024 * 1024):
|
||||
self._check_cancelled()
|
||||
received += len(data)
|
||||
|
||||
@@ -14,7 +14,8 @@ from openpilot.selfdrive.controls.lib.drive_helpers import MIN_SPEED, smooth_val
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
|
||||
from openpilot.selfdrive.modeld.helpers import load_oob
|
||||
from openpilot.starpilot.models.parser import Parser
|
||||
from openpilot.starpilot.models.catalog import ARTIFACT_ABI, COMPILER_REVISION
|
||||
from openpilot.starpilot.models.runtime_bridge.runtime import selected_plan
|
||||
from openpilot.starpilot.models.runtime_selection import require_artifact_contract
|
||||
from openpilot.starpilot.models.receipt import PreparedArtifact, artifact_identity, prepare_artifact
|
||||
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||
|
||||
@@ -63,9 +64,7 @@ def load_verified_model(cam_w: int, cam_h: int, artifact_path: Path, behavior_ve
|
||||
|
||||
|
||||
def validate_artifact(artifact: dict, version: str, camera_size: tuple[int, int]) -> None:
|
||||
if (not isinstance(artifact, dict) or artifact.get("artifact_abi") != ARTIFACT_ABI or
|
||||
artifact.get("compiler_revision") != COMPILER_REVISION or artifact.get("format_version") != 1):
|
||||
raise ValueError("catalog model compiler/ABI mismatch")
|
||||
require_artifact_contract(artifact, selected_plan())
|
||||
if version not in SUPPORTED_VERSIONS or artifact.get("behavior_version") != version:
|
||||
raise ValueError("catalog model behavior version mismatch")
|
||||
if artifact.get("model_type") not in ("supercombo", "vision_policy", "vision_multi_policy"):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,281 @@
|
||||
"""Integrity gate and native02e loader; no tinygrad imports before prepare."""
|
||||
from contextlib import contextmanager
|
||||
import atexit
|
||||
import hashlib
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path, PurePosixPath
|
||||
import stat
|
||||
import sys
|
||||
import tempfile
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Protocol, cast
|
||||
|
||||
from openpilot.starpilot.models.runtime_selection import NATIVE_REVISION
|
||||
from openpilot.starpilot.models.runtime_bridge.bootstrap import (
|
||||
IMPORT_FAMILIES, _check_source_package, _namespace, _private_cache,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tinygrad.device import Buffer
|
||||
|
||||
|
||||
class PersistentLoad(Protocol):
|
||||
persistent_load: Callable[[tuple[int, int]], "Buffer"]
|
||||
|
||||
# Deliberately unpublishable until native source/guard and model evidence is sealed.
|
||||
POLICY_SHA256 = '6608a8280152a1b37c39370942a910c8f34180f4c0626f7ee73340b32a6b6c7e'
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
BRIDGE = Path(__file__).parent
|
||||
_LIBRARIES = {
|
||||
'libbounded_wait.so': '24ed559861c8636c1026cad9a244854bb2236b5ee8b91bf21c5efbf688fa4a69',
|
||||
'libusb_guards.so': '66a3dfe48ac17899862ab80160cf6dac2c9b9707234527ba0d808f3b68404114',
|
||||
}
|
||||
_role = None
|
||||
_allowed = {}
|
||||
_protection = None
|
||||
|
||||
|
||||
def _plain_file(path):
|
||||
for parent in (*reversed(path.parents), path):
|
||||
mode = parent.lstat().st_mode
|
||||
if stat.S_ISLNK(mode):
|
||||
raise ValueError('Symlink in native runtime input')
|
||||
if not path.is_file():
|
||||
raise ValueError('Missing native runtime input')
|
||||
|
||||
|
||||
def _digest(path):
|
||||
_plain_file(path)
|
||||
with path.open('rb') as stream:
|
||||
return hashlib.file_digest(stream, 'sha256').hexdigest()
|
||||
|
||||
|
||||
def _verify_policy():
|
||||
path = BRIDGE / 'native-policy.json'
|
||||
_plain_file(path)
|
||||
raw = path.read_bytes()
|
||||
if hashlib.sha256(raw).hexdigest() != POLICY_SHA256:
|
||||
raise RuntimeError('Native runtime policy is not qualified')
|
||||
policy = json.loads(raw)
|
||||
required = {'native_guards.py', 'bounded_wait.c', 'usb_guards.c', *_LIBRARIES}
|
||||
if (policy.get('compiler_revision') != NATIVE_REVISION or policy.get('native_abi') != 'linux-aarch64-64bit' or
|
||||
policy.get('host_runtime_dev') not in ('CPU:LLVM', 'CPU:CLANG') or
|
||||
not isinstance(policy.get('files'), dict) or set(policy['files']) != required or
|
||||
not isinstance(policy.get('vendor_files'), dict) or not policy['vendor_files']):
|
||||
raise ValueError('Native runtime identity mismatch')
|
||||
for name, expected in policy['files'].items():
|
||||
if _digest(BRIDGE / name) != expected or name in _LIBRARIES and expected != _LIBRARIES[name]:
|
||||
raise ValueError('Native guard source/artifact changed')
|
||||
if _digest(BRIDGE.parent / 'catalog-native02e.json') != policy.get('catalog_sha256'):
|
||||
raise ValueError('Native stock release metadata changed')
|
||||
vendor = ROOT / 'tinygrad_repo'
|
||||
expected_files = policy['vendor_files']
|
||||
actual = set()
|
||||
for directory, dirs, files in os.walk(vendor, followlinks=False):
|
||||
dirs[:] = [name for name in dirs if name not in ('__pycache__', '.git')]
|
||||
for name in dirs:
|
||||
if (Path(directory) / name).is_symlink():
|
||||
raise ValueError('Symlink in native runtime source')
|
||||
for name in files:
|
||||
if name.endswith(('.pyc', '.pyo')):
|
||||
raise ValueError('Standalone native vendor bytecode is not source-pinned')
|
||||
if name == 'AGENTS.md':
|
||||
continue
|
||||
actual.add((Path(directory) / name).relative_to(vendor).as_posix())
|
||||
if actual != set(expected_files):
|
||||
raise ValueError('Native vendor inventory changed')
|
||||
for name, expected in expected_files.items():
|
||||
relative = PurePosixPath(name)
|
||||
if relative.is_absolute() or '..' in relative.parts or '\\' in name or str(relative) != name:
|
||||
raise ValueError('Invalid native vendor path')
|
||||
if _digest(vendor / name) != expected:
|
||||
raise ValueError('Native vendor source changed')
|
||||
return policy
|
||||
|
||||
|
||||
def _remove_empty_prefix(path):
|
||||
try:
|
||||
os.rmdir(path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def prepare(role, plan):
|
||||
"""Run before any tinygrad import; root seals policy after actual qualification."""
|
||||
global _role, _allowed
|
||||
if role not in ('driving', 'monitoring') or role == 'driving' and (plan is None or plan.family != 'native02e'):
|
||||
raise ValueError('Invalid native runtime role/plan')
|
||||
if _role is not None:
|
||||
raise RuntimeError('Native runtime was already selected')
|
||||
if any(name == family or name.startswith(family + '.') for name in sys.modules for family in IMPORT_FAMILIES):
|
||||
raise RuntimeError('Select native runtime before importing tinygrad')
|
||||
policy = _verify_policy()
|
||||
from openpilot.starpilot.models.manager import SHIPPED_MODELS, native_stock
|
||||
stock = native_stock()
|
||||
if role == 'monitoring':
|
||||
allowed = {str(SHIPPED_MODELS / name): row for name, row in stock.items()}
|
||||
else:
|
||||
default = json.loads(plan.default_json)
|
||||
entries = json.loads(plan.entries_json)
|
||||
allowed = {default['path']: default}
|
||||
small = entries.get(plan.small_id)
|
||||
if small is not None and small.get('_runtime_family') == 'native02e':
|
||||
allowed[small['_artifact_path']] = small
|
||||
if plan.big_id:
|
||||
row = entries[plan.big_id]
|
||||
allowed[row['_artifact_path']] = row
|
||||
cache = Path('/data/starpilot-model-runtime') / NATIVE_REVISION / role / POLICY_SHA256
|
||||
_private_cache(cache)
|
||||
_private_cache(cache / 'xdg')
|
||||
bytecode_prefix = Path(tempfile.mkdtemp(prefix='python-', dir=cache))
|
||||
_private_cache(bytecode_prefix)
|
||||
if any(bytecode_prefix.iterdir()):
|
||||
raise RuntimeError('Native bytecode prefix is not empty')
|
||||
atexit.register(_remove_empty_prefix, bytecode_prefix)
|
||||
tg_root = ROOT / 'tinygrad_repo'
|
||||
sys.path[:0] = [str(ROOT), str(tg_root)]
|
||||
sys.dont_write_bytecode = True
|
||||
# A fresh private prefix isolates READS too; dont_write_bytecode alone does not.
|
||||
sys.pycache_prefix = str(bytecode_prefix)
|
||||
os.environ.pop('PYTHONPYCACHEPREFIX', None)
|
||||
os.environ['HCQ_RUNTIME_DEV'] = policy['host_runtime_dev']
|
||||
os.environ.update(CACHEDB=str(cache / 'tinygrad.db'), XDG_CACHE_HOME=str(cache / 'xdg'), PYTHONDONTWRITEBYTECODE='1')
|
||||
sys.modules['tinygrad_repo'] = _namespace('tinygrad_repo', tg_root)
|
||||
sys.modules['extra'] = _namespace('extra', tg_root / 'extra')
|
||||
importlib.invalidate_caches()
|
||||
_check_source_package('tinygrad', tg_root / 'tinygrad/__init__.py')
|
||||
_check_source_package('examples', tg_root / 'examples/__init__.py')
|
||||
_role, _allowed = role, allowed
|
||||
return {'role': role, 'family': 'native02e', 'compiler_revision': NATIVE_REVISION,
|
||||
'manifest_sha256': POLICY_SHA256, 'runtime_files': len(policy['vendor_files']),
|
||||
'tinygrad_origin': str(tg_root / 'tinygrad/__init__.py'), 'cache': str(cache)}
|
||||
|
||||
|
||||
def _guard():
|
||||
global _protection
|
||||
if _role is None:
|
||||
raise RuntimeError('Native runtime is not prepared')
|
||||
if _protection is None:
|
||||
_verify_policy()
|
||||
from openpilot.starpilot.models.runtime_bridge import native_guards
|
||||
_protection = native_guards.install(_role, BRIDGE / 'libbounded_wait.so', BRIDGE / 'libusb_guards.so')
|
||||
return _protection
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _verified(path):
|
||||
path = Path(path)
|
||||
record = _allowed.get(str(path).removesuffix('.unchunked'))
|
||||
if record is None:
|
||||
raise ValueError('Artifact is outside the frozen native selection')
|
||||
fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK | os.O_NOFOLLOW | os.O_CLOEXEC)
|
||||
with os.fdopen(fd, 'rb') as stream:
|
||||
before = os.fstat(stream.fileno())
|
||||
def identity(value):
|
||||
return value.st_dev, value.st_ino, value.st_size, value.st_mtime_ns, value.st_ctime_ns
|
||||
if (not stat.S_ISREG(before.st_mode) or before.st_size != record['artifact_size'] or
|
||||
hashlib.file_digest(stream, 'sha256').hexdigest() != record['artifact_sha256']):
|
||||
raise ValueError('Native artifact identity mismatch')
|
||||
stream.seek(0)
|
||||
def unchanged():
|
||||
named = path.lstat()
|
||||
if (not stat.S_ISREG(named.st_mode) or identity(before) != identity(os.fstat(stream.fileno())) or
|
||||
identity(before) != identity(named)):
|
||||
raise ValueError('Native artifact changed while loading')
|
||||
unchanged()
|
||||
yield stream, unchanged
|
||||
unchanged()
|
||||
|
||||
|
||||
def _load_driving(stream, unchanged):
|
||||
import io
|
||||
import mmap
|
||||
import pickle
|
||||
import struct
|
||||
from tinygrad import Device
|
||||
from tinygrad.device import Buffer, HCQ_RUNTIME_DEV
|
||||
length_raw = stream.read(8)
|
||||
if len(length_raw) != 8:
|
||||
raise ValueError('Missing native OOB envelope')
|
||||
length = struct.unpack('<q', length_raw)[0]
|
||||
if not 0 < length <= os.fstat(stream.fileno()).st_size - 8:
|
||||
raise ValueError('Invalid native OOB envelope')
|
||||
opcodes = stream.read(length)
|
||||
if stream.tell() == os.fstat(stream.fileno()).st_size:
|
||||
unchanged()
|
||||
return pickle.loads(opcodes)
|
||||
# Map the already authenticated inode. Do not reopen a path or use a cached
|
||||
# DISK:/proc/self/fd device whose descriptor number can be reused later.
|
||||
mapping = mmap.mmap(stream.fileno(), 0, access=mmap.ACCESS_COPY)
|
||||
view = memoryview(mapping)[stream.tell():]
|
||||
buffers = Buffer(HCQ_RUNTIME_DEV.device, len(view), opaque=view)
|
||||
arena, chunk_size = Buffer(Device.DEFAULT, buffers.nbytes, preallocate=True), 8 << 20
|
||||
unchanged()
|
||||
for off in range(0, buffers.nbytes, chunk_size):
|
||||
size = min(chunk_size, buffers.nbytes - off)
|
||||
arena.view(size, off).ensure_allocated().copy_from(buffers.view(size, off).ensure_allocated())
|
||||
Device.default.synchronize()
|
||||
unchanged()
|
||||
del buffers
|
||||
view.release()
|
||||
mapping.close()
|
||||
unpickler = pickle.Unpickler(io.BytesIO(opcodes))
|
||||
cast("PersistentLoad", unpickler).persistent_load = lambda record: arena.view(*record)
|
||||
return unpickler.load()
|
||||
|
||||
|
||||
def _load_monitoring(stream, unchanged):
|
||||
import io
|
||||
import pickle
|
||||
import struct
|
||||
from tinygrad import Device
|
||||
from tinygrad.device import Buffer
|
||||
data = stream.read()
|
||||
unchanged()
|
||||
if len(data) < 8:
|
||||
raise ValueError('Missing native monitoring envelope')
|
||||
length = struct.unpack('<q', data[:8])[0]
|
||||
if not 0 < length <= len(data) - 8:
|
||||
raise ValueError('Invalid native monitoring envelope')
|
||||
end = 8 + length
|
||||
if end == len(data):
|
||||
unchanged()
|
||||
return pickle.loads(data[8:end])
|
||||
arena = Buffer(Device.DEFAULT, len(data) - end, initial_value=data[end:])
|
||||
unchanged()
|
||||
unpickler = pickle.Unpickler(io.BytesIO(data[8:end]))
|
||||
cast("PersistentLoad", unpickler).persistent_load = lambda record: arena.view(*record)
|
||||
return unpickler.load()
|
||||
|
||||
|
||||
def load_oob(path, chestnut=False):
|
||||
if _role is None or chestnut and _role != 'driving':
|
||||
raise ValueError('Invalid native artifact role')
|
||||
from tinygrad import Context, Device
|
||||
from tinygrad.runtime.ops_qcom import QCOMDevice
|
||||
protection = _guard()
|
||||
with _verified(path) as (stream, unchanged), Context(DEV='USB+AMD:LLVM' if chestnut else 'QCOM'):
|
||||
if not isinstance(Device['QCOM'], QCOMDevice):
|
||||
raise RuntimeError('Expected the native QCOM device')
|
||||
artifact = _load_monitoring(stream, unchanged) if _role == 'monitoring' else _load_driving(stream, unchanged)
|
||||
return protection.artifact(artifact, chestnut=chestnut)
|
||||
|
||||
|
||||
def load_warp(path):
|
||||
if _role != 'monitoring':
|
||||
raise ValueError('Native warp loader requires monitoring role')
|
||||
import pickle
|
||||
from tinygrad import Context, Device
|
||||
from tinygrad.runtime.ops_qcom import QCOMDevice
|
||||
protection = _guard()
|
||||
with _verified(path) as (stream, unchanged), Context(DEV='QCOM'):
|
||||
if not isinstance(Device['QCOM'], QCOMDevice):
|
||||
raise RuntimeError('Expected the native QCOM device')
|
||||
data = stream.read()
|
||||
unchanged()
|
||||
artifact = pickle.loads(data)
|
||||
if not isinstance(artifact, dict) or set(artifact) != {'metadata', 'run', 'input_specs'}:
|
||||
raise ValueError('Unexpected native monitoring warp layout')
|
||||
return protection.warp(artifact['run'])
|
||||
@@ -0,0 +1,593 @@
|
||||
"""Compile-only port of the existing native guards to pinned tinygrad 02e.
|
||||
|
||||
PRIVATE AND UNQUALIFIED. Stored artifacts are prepared offline. Inference only
|
||||
verifies their guarded form and lets the ordinary HCQ linker bind fresh addresses.
|
||||
"""
|
||||
from collections import Counter
|
||||
from dataclasses import replace
|
||||
import ctypes
|
||||
import hashlib
|
||||
import itertools
|
||||
from pathlib import Path
|
||||
import struct
|
||||
|
||||
from tinygrad import Device
|
||||
from tinygrad.device import Buffer, Compiled, HCQ_RUNTIME_DEV
|
||||
from tinygrad.dtype import AddrSpace, dtypes
|
||||
from tinygrad.engine import realize
|
||||
from tinygrad.renderer.cstyle import ClangRenderer
|
||||
from tinygrad.runtime.autogen import libusb
|
||||
from tinygrad.runtime.support import hcq2
|
||||
from tinygrad.runtime.support.c import DLL
|
||||
from tinygrad.uop.ops import Ops, PatternMatcher, ProgramInfo, UOp, UPat, graph_rewrite, pm_renumber_slots
|
||||
from tinygrad.schedule.prepare import resolve_function, pm_inline_calls
|
||||
|
||||
|
||||
FORMAT = "starpilot_native02e_guard_v1"
|
||||
REVISION = "02e698160eed1751738212144a7ea2cfb160b3f1"
|
||||
FENCE_SHA256 = "24ed559861c8636c1026cad9a244854bb2236b5ee8b91bf21c5efbf688fa4a69"
|
||||
USB_SHA256 = "66a3dfe48ac17899862ab80160cf6dac2c9b9707234527ba0d808f3b68404114"
|
||||
FFI_TAG = "starpilot_native02e_libusb_functions"
|
||||
FFI_NAMES = ("libusb_control_transfer", "libusb_bulk_transfer", "libusb_submit_transfer", "libusb_handle_events_timeout")
|
||||
WRAPPERS = dict(zip(FFI_NAMES, ("sp_control", "sp_bulk", "sp_submit", "sp_events"), strict=True))
|
||||
GUARD_NAMES = frozenset((*WRAPPERS.values(), "sp_loop_touch", "sp_loop_check", "starpilot_bounded_wait"))
|
||||
_PINNED_NATIVE = {"libbounded_wait.so": FENCE_SHA256, "libusb_guards.so": USB_SHA256}
|
||||
_INSTALLATION = None
|
||||
|
||||
|
||||
def _current_native_ffi_buffer(device):
|
||||
# Called at link time, not captured when tinygrad compiles a matcher callback.
|
||||
# The module-level indirection keeps a Protection/Buffer (and its addresses)
|
||||
# out of deconstruct_function's copied/pickled callback globals.
|
||||
protection = _INSTALLATION
|
||||
if protection is None or protection.role != "driving" or protection.ffi_buffer is None:
|
||||
raise ValueError("Native function table has no active driving installation")
|
||||
buffer = protection.ffi_buffer
|
||||
if device != HCQ_RUNTIME_DEV.device or buffer.device != device or buffer.nbytes != len(FFI_NAMES) * 8:
|
||||
raise ValueError("Native function table runtime storage changed")
|
||||
return buffer
|
||||
|
||||
|
||||
def _resolve_native_ffi(b):
|
||||
if b.op is not Ops.ALLOC or b.tag != FFI_TAG or b.dtype is not dtypes.uint64 or b.shape != (len(FFI_NAMES),):
|
||||
raise ValueError("Native function table binding changed")
|
||||
return _current_native_ffi_buffer(b.device)
|
||||
|
||||
|
||||
def _nodes(root):
|
||||
return tuple(root.toposort(enter_calls=True))
|
||||
|
||||
|
||||
def _payloads(root):
|
||||
cpu = {u.src[-1] for u in _nodes(root)
|
||||
if u.op is Ops.PROGRAM and isinstance(u.arg, ProgramInfo) and u.arg.target.device == "CPU"}
|
||||
return Counter(hashlib.sha256(u.arg).hexdigest() for u in _nodes(root)
|
||||
if u.op is Ops.BINARY and isinstance(u.arg, bytes) and u not in cpu)
|
||||
|
||||
|
||||
def _constant(u, value):
|
||||
return u.vmin == u.vmax == value
|
||||
|
||||
|
||||
def _name(call):
|
||||
return call.body.arg.name if call.op is Ops.CALL and call.body.op is Ops.CUSTOM_FUNCTION else None
|
||||
|
||||
|
||||
def _calls(root):
|
||||
return tuple(u for u in _nodes(root) if _name(u) is not None)
|
||||
|
||||
|
||||
def _predicate_leaves(condition, mandatory=True):
|
||||
"""Boolean terms only: do not walk through load/callback side-effect inputs."""
|
||||
if condition.dtype is not dtypes.bool:
|
||||
raise ValueError("Native polling predicate is not boolean")
|
||||
if condition.op in (Ops.AND, Ops.OR):
|
||||
if len(condition.src) != 2:
|
||||
raise ValueError("Unknown native boolean connector")
|
||||
required = mandatory and condition.op is Ops.AND
|
||||
return tuple(term for child in condition.src for term in _predicate_leaves(child, required))
|
||||
if condition.op not in (Ops.CMPLT, Ops.CMPEQ, Ops.CMPNE):
|
||||
raise ValueError("Unknown native polling predicate")
|
||||
return ((condition, mandatory),)
|
||||
|
||||
|
||||
def _pending_status(condition):
|
||||
# UOp.eq is represented as (value != 255) != True before simplification.
|
||||
if (condition.op is Ops.CMPNE and condition.src[1].dtype is dtypes.bool and
|
||||
_constant(condition.src[1], True)):
|
||||
inner = condition.src[0]
|
||||
return (inner.op is Ops.CMPNE and inner.src[0].op is Ops.LOAD and
|
||||
inner.src[0].dtype is dtypes.uint32 and _constant(inner.src[1], 0xff))
|
||||
return (condition.op is Ops.CMPEQ and condition.src[0].op is Ops.LOAD and
|
||||
condition.src[0].dtype is dtypes.uint32 and _constant(condition.src[1], 0xff))
|
||||
|
||||
|
||||
def _polling_condition(condition):
|
||||
leaves = _predicate_leaves(condition)
|
||||
timers = [(term, limit, required) for term, required in leaves if term.op is Ops.CMPLT
|
||||
for limit in (1000, 30000) if _constant(term.src[1], limit)]
|
||||
if len(timers) == 1 and timers[0][2]:
|
||||
timer, limit, _ = timers[0]
|
||||
# The exact recognized deadline is delegated to the fixed native C guard.
|
||||
# All status/sequence/error predicates, including their dependencies, stay.
|
||||
return condition.substitute({timer: UOp.const(True, dtypes.bool)}, enter_calls=True), limit * 1_000_000
|
||||
if not timers and sum(required and _pending_status(term) for term, required in leaves) == 1:
|
||||
return condition, 10_000_000_000
|
||||
raise ValueError("Unqualified native USB polling condition")
|
||||
|
||||
|
||||
def _checked_poll_exit(condition):
|
||||
"""Exactly a uint64 guard result converted to boolean, before/after lowering."""
|
||||
if condition.dtype is not dtypes.bool:
|
||||
return False
|
||||
if condition.op is Ops.CAST and len(condition.src) == 1:
|
||||
call = condition.src[0]
|
||||
elif condition.op is Ops.CMPNE and len(condition.src) == 2:
|
||||
call, zero = condition.src
|
||||
if (zero.dtype is not dtypes.uint64 or
|
||||
not (zero.op is Ops.CONST and _constant(zero, 0) or
|
||||
zero.op is Ops.CAST and len(zero.src) == 1 and
|
||||
zero.src[0].op is Ops.CONST and _constant(zero.src[0], 0))):
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
return call.dtype is dtypes.uint64 and len(call.src) == 3 and _name(call) == "sp_loop_check"
|
||||
|
||||
|
||||
def _weak(root):
|
||||
return tuple(u for u in _nodes(root) if u.op is Ops.RANGE and u.dtype is dtypes.void)
|
||||
|
||||
|
||||
def _host(call):
|
||||
if call.op is not Ops.CALL or not isinstance(call.arg.aux, hcq2.HCQInfo):
|
||||
raise ValueError("Expected actual 02e HCQ call")
|
||||
if call.arg.aux.device not in (("QCOM",), ("AMD",)):
|
||||
raise ValueError("Unsupported native guard device")
|
||||
program = call.body
|
||||
if (program.op is not Ops.PROGRAM or len(program.src) != 4 or
|
||||
not isinstance(program.arg, ProgramInfo) or program.arg.target.device != "CPU" or
|
||||
program.src[-1].op is not Ops.BINARY):
|
||||
raise ValueError("Expected complete compiled CPU host")
|
||||
sink = program.src[0]
|
||||
if sink.op is not Ops.SINK or sink.arg.name != "hcq_submit":
|
||||
raise ValueError("Unexpected CPU submission source")
|
||||
if any(u.op is Ops.CALL and u.body.op in (Ops.SINK, Ops.PROGRAM) for u in _nodes(sink)):
|
||||
raise ValueError("Nested submission was not completely lowered")
|
||||
return program, sink
|
||||
|
||||
|
||||
def _proof(call):
|
||||
program, sink = _host(call)
|
||||
return {
|
||||
"device": list(call.arg.aux.device),
|
||||
"cpu_target": str(program.arg.target),
|
||||
"cpu_binary_sha256": hashlib.sha256(program.src[3].arg).hexdigest(),
|
||||
"cpu_source_sha256": hashlib.sha256(program.src[2].arg.encode()).hexdigest(),
|
||||
"opaque_payloads": dict(_payloads(sink)),
|
||||
"arguments": len(call.src) - 1,
|
||||
"native_calls": dict(Counter(_name(u) for u in _calls(sink))),
|
||||
}
|
||||
|
||||
|
||||
def _load_library(path, expected, key, symbols):
|
||||
path = Path(path)
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise ValueError("Missing qualified native guard library")
|
||||
raw = path.read_bytes()
|
||||
if (hashlib.sha256(raw).hexdigest() != expected or len(raw) < 20 or
|
||||
raw[:6] != b"\x7fELF\x02\x01" or struct.unpack_from("<HH", raw, 16) != (3, 183)):
|
||||
raise ValueError("Native guard library identity or ABI changed")
|
||||
if key in DLL._loaded_:
|
||||
raise ValueError("Native guard library key already registered")
|
||||
lib = ctypes.CDLL(str(path.resolve()))
|
||||
for symbol in symbols:
|
||||
expected_address = ctypes.cast(getattr(lib, symbol), ctypes.c_void_p).value
|
||||
if not expected_address:
|
||||
raise ValueError("Missing guard export")
|
||||
for previous in DLL._loaded_.values():
|
||||
try:
|
||||
other = ctypes.cast(getattr(previous, symbol), ctypes.c_void_p).value
|
||||
except AttributeError:
|
||||
continue
|
||||
if other != expected_address:
|
||||
raise ValueError("Native guard symbol collision")
|
||||
DLL._loaded_[key] = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _fresh_scope(c):
|
||||
if not c.is_inline_call:
|
||||
return None
|
||||
# Do not enter nested function bodies or substitute their PARAM namespace.
|
||||
# Freshen this invocation's REG storage and ranges BEFORE argument binding,
|
||||
# so distinct locals cannot hash-cons together after PARAM substitution.
|
||||
body = graph_rewrite(c.body, pm_renumber_slots, ctx=UOp.unique_num, walk=True)
|
||||
return resolve_function(c.replace(src=(body, *c.src[1:])))
|
||||
|
||||
|
||||
_INLINE = PatternMatcher([(UPat(Ops.CALL, name='c'), _fresh_scope)]) + pm_inline_calls
|
||||
|
||||
|
||||
def inline_host_sink(sink):
|
||||
if sink.op is not Ops.SINK or sink.arg is None or sink.arg.name != 'hcq_submit':
|
||||
raise ValueError('Expected fresh outer native hcq_submit SINK')
|
||||
outer_params = {u for u in sink.toposort() if u.op is Ops.PARAM}
|
||||
flat = sink
|
||||
for _ in range(64):
|
||||
pending = [u for u in flat.toposort(enter_calls=True)
|
||||
if u.op is Ops.CALL and u.body.op in (Ops.SINK, Ops.LINEAR, Ops.PROGRAM)]
|
||||
if not pending:
|
||||
break
|
||||
if any(not u.is_inline_call for u in pending):
|
||||
raise ValueError('Unexpected already-outlined or non-inline host function')
|
||||
updated = graph_rewrite(flat, _INLINE, walk=True)
|
||||
if updated is flat:
|
||||
raise ValueError('Host function inlining made no progress')
|
||||
flat = updated
|
||||
else:
|
||||
raise ValueError('Host function nesting exceeds bounded inlining limit')
|
||||
if any(u.op is Ops.PARAM and u not in outer_params for u in flat.toposort()):
|
||||
raise ValueError('An outlined function-local PARAM escaped binding')
|
||||
flat = graph_rewrite(flat, pm_renumber_slots, ctx=itertools.count(), walk=True)
|
||||
ranges = [u for u in flat.toposort() if u.op is Ops.RANGE]
|
||||
regs = [u for u in flat.toposort() if u.op is Ops.BUFFER and u.addrspace is AddrSpace.REG]
|
||||
if len({u.arg for u in ranges}) != len(ranges) or len({u.arg.slot for u in regs}) != len(regs):
|
||||
raise ValueError('Flattened local identities are not unique')
|
||||
if sink.arg != flat.arg or sink.tag != flat.tag:
|
||||
raise ValueError('Outer host source identity changed')
|
||||
return flat
|
||||
|
||||
|
||||
|
||||
|
||||
def _native_ffi_slot(sink, argument_count):
|
||||
params = tuple(u for u in _nodes(sink) if u.op is Ops.PARAM)
|
||||
slot = min((u.arg.slot for u in params if u.addrspace is AddrSpace.ALU and u.arg.slot >= 0), default=argument_count)
|
||||
if not 0 <= slot <= argument_count or any(u.addrspace is not AddrSpace.ALU and u.arg.slot >= slot for u in params):
|
||||
raise ValueError("Native host pointer/scalar parameter order changed")
|
||||
return slot
|
||||
|
||||
|
||||
def _shift_native_params(sink, first_slot, argument_count):
|
||||
original = tuple(u for u in _nodes(sink) if u.op is Ops.PARAM)
|
||||
moving = tuple(u for u in original if u.arg.slot >= first_slot)
|
||||
if not moving:
|
||||
return sink
|
||||
# substitute recursively rewrites replacements. Disjoint temporary slots
|
||||
# make both phases simultaneous; no original or final slot is a temp key.
|
||||
temporary_start = max(argument_count, *(u.arg.slot for u in original)) + 2
|
||||
temporary = {u: u.replace(arg=replace(u.arg, slot=temporary_start + i)) for i, u in enumerate(moving)}
|
||||
final = {temporary[u]: u.replace(arg=replace(u.arg, slot=u.arg.slot + 1)) for u in moving}
|
||||
shifted = sink.substitute(temporary, enter_calls=True).substitute(final, enter_calls=True)
|
||||
expected = {u.replace(arg=replace(u.arg, slot=u.arg.slot + 1)) if u in temporary else u for u in original}
|
||||
actual = {u for u in _nodes(shifted) if u.op is Ops.PARAM}
|
||||
if actual != expected or len(actual) != len(original):
|
||||
raise ValueError("Native host parameter renaming lost an original binding")
|
||||
return shifted
|
||||
|
||||
|
||||
def _verify_cpu_parameter_abi(program):
|
||||
# Clang emits PARAM declarations in the compiled LINEAR's order. CPUProgram
|
||||
# invokes every globals pointer first, then every ProgramInfo.vars scalar.
|
||||
linear = program.src[1]
|
||||
if linear.op is not Ops.LINEAR:
|
||||
raise ValueError("Native host has no compiled parameter declaration order")
|
||||
params = tuple(u for u in linear.src if u.op is Ops.PARAM)
|
||||
expected = (*program.arg.globals, *(u.arg.slot for u in program.arg.vars))
|
||||
if tuple(u.arg.slot for u in params) != expected:
|
||||
raise ValueError("Native CPU parameter ABI is misordered")
|
||||
boundary = len(program.arg.globals)
|
||||
if (any(u.addrspace is AddrSpace.ALU for u in params[:boundary]) or
|
||||
any(u.addrspace is not AddrSpace.ALU for u in params[boundary:])):
|
||||
raise ValueError("Native CPU parameter ABI mixes pointers and scalars")
|
||||
|
||||
|
||||
def _fresh_host(call):
|
||||
if call.op is not Ops.CALL or not isinstance(call.arg.aux, hcq2.HCQInfo):
|
||||
raise ValueError('Expected fresh actual 02e HCQ call')
|
||||
if call.arg.aux.device not in (('QCOM',), ('AMD',)) or call.body.op is not Ops.SINK:
|
||||
raise ValueError('Native guards must precede CPU compilation/outlining')
|
||||
if call.body.arg is None or call.body.arg.name != 'hcq_submit':
|
||||
raise ValueError('Unknown fresh CPU host source')
|
||||
return call.body
|
||||
|
||||
|
||||
class Protection:
|
||||
def __init__(self, role, fence_library, usb_library):
|
||||
if role not in ("driving", "monitoring"):
|
||||
raise ValueError("Unknown native model role")
|
||||
if HCQ_RUNTIME_DEV.device.split(":")[0] != "CPU":
|
||||
raise ValueError("Native guard requires actual CPU helper target")
|
||||
renderer = Device[HCQ_RUNTIME_DEV.device]._select_renderer(HCQ_RUNTIME_DEV)
|
||||
if not isinstance(renderer, ClangRenderer):
|
||||
raise ValueError("Native guard requires its qualified Clang host renderer")
|
||||
self.role, self.renderer, self.records = role, renderer, []
|
||||
self.fence_library = _load_library(fence_library, FENCE_SHA256, "starpilot_native02e_fence",
|
||||
("starpilot_bounded_wait",))
|
||||
self.usb_library = None
|
||||
self.ffi_buffer = None
|
||||
self.ffi = None
|
||||
if role == "driving":
|
||||
self.usb_library = _load_library(usb_library, USB_SHA256, "starpilot_native02e_usb",
|
||||
(*WRAPPERS.values(), "sp_loop_touch", "sp_loop_check"))
|
||||
fields = {name: (typ, offset) for name, typ, offset, *_ in libusb.struct_libusb_transfer._real_fields_}
|
||||
if any(fields[name][1] != offset or ctypes.sizeof(fields[name][0]) != 4
|
||||
for name, offset in (("status", 16), ("length", 20), ("actual_length", 24))):
|
||||
raise ValueError("Unqualified native libusb transfer layout")
|
||||
if ctypes.sizeof(ctypes.c_void_p) != 8 or ctypes.sizeof(ctypes.c_ulong) != 8:
|
||||
raise ValueError("Unqualified native host ABI")
|
||||
addresses = [ctypes.cast(getattr(libusb.dll, name), ctypes.c_void_p).value for name in FFI_NAMES]
|
||||
if not all(addresses):
|
||||
raise ValueError("Missing native libusb functions")
|
||||
self.ffi_buffer = Buffer(HCQ_RUNTIME_DEV.device, len(addresses) * 8,
|
||||
initial_value=b"".join(address.to_bytes(8, "little") for address in addresses))
|
||||
self.ffi = UOp.alloc((len(addresses),), dtypes.uint64, 0, device=HCQ_RUNTIME_DEV.device).rtag(FFI_TAG)
|
||||
previous = Compiled.pm_bufferize
|
||||
Compiled.pm_bufferize = previous + PatternMatcher([(UPat(Ops.ALLOC, tag=FFI_TAG, name="b"), _resolve_native_ffi)])
|
||||
self.identity = {
|
||||
"format": FORMAT, "compiler_revision": REVISION,
|
||||
"implementation_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
|
||||
"fence_sha256": FENCE_SHA256, "usb_sha256": USB_SHA256,
|
||||
"host_target": str(self.renderer.target),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _native(name, args, dtype):
|
||||
return UOp.custom_function(name, dtype=dtype).call(*args)
|
||||
|
||||
def _compile(self, original, rewritten, args):
|
||||
before = _payloads(_fresh_host(original))
|
||||
if _payloads(rewritten) != before:
|
||||
raise ValueError("Opaque GPU payload changed before CPU compilation")
|
||||
updated = original.replace(src=(rewritten, *args))
|
||||
if updated.device != HCQ_RUNTIME_DEV.device:
|
||||
raise ValueError("Guard rewrite attempted non-CPU compilation")
|
||||
compiled = realize.lower_and_compile(UOp(Ops.LINEAR, src=(updated,))).src[0]
|
||||
program, sink = _host(compiled)
|
||||
if program.arg.target != self.renderer.target or _payloads(sink) != before:
|
||||
raise ValueError("CPU target or opaque GPU payload changed")
|
||||
_verify_cpu_parameter_abi(program)
|
||||
self._verify_call(compiled)
|
||||
self.records.append(_proof(compiled))
|
||||
return compiled
|
||||
|
||||
def _qcom(self, call, sink):
|
||||
loops, replacements = _weak(sink), {}
|
||||
if len(loops) != 1:
|
||||
raise ValueError("Expected exactly one native QCOM fence")
|
||||
loop = loops[0]
|
||||
ends = [u for u in _nodes(sink) if u.op is Ops.BACKEDGE and u.src[1] is loop]
|
||||
if len(ends) != 1:
|
||||
raise ValueError("Unknown native QCOM loop boundary")
|
||||
done, _, condition = ends[0].src
|
||||
if done.op is not Ops.LOAD or done.dtype is not dtypes.uint64:
|
||||
raise ValueError("Unknown native QCOM timeline load")
|
||||
comparisons = [u for u in _nodes(condition) if u.op is Ops.CMPLT and u.src[0] is done]
|
||||
timers = [u for u in _nodes(condition) if u.op is Ops.CMPLT and _constant(u.src[1], 30000)]
|
||||
if len(comparisons) != 1 or len(timers) != 1:
|
||||
raise ValueError("Unknown native QCOM deadline condition")
|
||||
target = comparisons[0].src[1]
|
||||
if target.dtype is not dtypes.uint64 or loop in target.ranges:
|
||||
raise ValueError("Unknown native QCOM fence target")
|
||||
index = done.src[0]
|
||||
if index.op is not Ops.INDEX or len(index.src) != 2 or not _constant(index.src[1], 0):
|
||||
raise ValueError("Unknown native QCOM signal address")
|
||||
signal = index.src[0].without_after
|
||||
arguments = realize.get_call_arg_uops(call)
|
||||
if signal.op is not Ops.PARAM or not 0 <= signal.arg.slot < len(arguments):
|
||||
raise ValueError("Native QCOM timeline is not an outer parameter")
|
||||
outer = arguments[signal.arg.slot].without_after
|
||||
if outer.op is not Ops.ALLOC or outer.tag != "timeline" or outer.device != "QCOM":
|
||||
raise ValueError("Timeline is not the native QCOM allocation")
|
||||
pointer = index.substitute({loop: UOp(Ops.NOOP)}, enter_calls=True)
|
||||
replacements[ends[0]] = self._native("starpilot_bounded_wait", (pointer, target), dtypes.uint64)
|
||||
rewritten = sink.substitute(replacements, enter_calls=True)
|
||||
if _weak(rewritten):
|
||||
raise ValueError("An unguarded QCOM polling range remains")
|
||||
return self._compile(call, rewritten, call.src[1:])
|
||||
|
||||
def _usb(self, call, sink):
|
||||
if self.role != "driving" or self.ffi is None or not Device["AMD"].is_usb:
|
||||
raise ValueError("Expected the actual native USB AMD device")
|
||||
return self._usb_rewrite(call, sink)
|
||||
|
||||
def _usb_rewrite(self, call, sink):
|
||||
args = list(call.src[1:])
|
||||
argument_count = len(realize.get_call_arg_uops(call))
|
||||
if any(not a.is_bound_var for a in args[argument_count:]) or any(a.is_bound_var for a in args[:argument_count]):
|
||||
raise ValueError("Native bound-variable argument order changed")
|
||||
ffi_slot = _native_ffi_slot(sink, argument_count)
|
||||
sink = _shift_native_params(sink, ffi_slot, argument_count)
|
||||
pointer = UOp.param(ffi_slot, dtypes.uint64, (len(FFI_NAMES),), HCQ_RUNTIME_DEV.device, name="native_usb_functions")
|
||||
loops = _weak(sink)
|
||||
if not 1 <= len(loops) < 256 or len({r.arg for r in loops}) != len(loops):
|
||||
raise ValueError("Unknown native USB polling structure")
|
||||
specifications, ends = {}, {}
|
||||
for slot, loop in enumerate(loops):
|
||||
boundaries = [u for u in _nodes(sink) if u.op is Ops.BACKEDGE and u.src[1] is loop]
|
||||
if len(boundaries) != 1:
|
||||
raise ValueError("Unknown native USB loop boundary")
|
||||
end = boundaries[0]
|
||||
condition, timeout = _polling_condition(end.src[2])
|
||||
specifications[loop] = (slot, timeout)
|
||||
ends[end] = condition
|
||||
calls = _calls(sink)
|
||||
if any(_name(u) not in {*FFI_NAMES, "memcpy", "clock_gettime"} for u in calls):
|
||||
raise ValueError("Unqualified native USB callback")
|
||||
def scalar64(u):
|
||||
return UOp(Ops.CUSTOMI, src=(u,), arg=("((unsigned long)({0}))", dtypes.uint64))
|
||||
substitutions, touched = {}, set()
|
||||
for callback in calls:
|
||||
name = _name(callback)
|
||||
if name not in WRAPPERS:
|
||||
continue
|
||||
touches = []
|
||||
for loop, (slot, timeout) in specifications.items():
|
||||
token = next((a for a in callback.src[1:] if loop in a.ranges), None)
|
||||
if token is not None:
|
||||
touches.append(self._native("sp_loop_touch", (
|
||||
UOp.const(slot, dtypes.uint64), UOp.const(timeout, dtypes.uint64), scalar64(token)), dtypes.uint64))
|
||||
touched.add(loop)
|
||||
marker = sum(touches[1:], touches[0]) if touches else UOp.const(0, dtypes.uint64)
|
||||
function = pointer.index(FFI_NAMES.index(name)).load()
|
||||
substitutions[callback] = self._native(WRAPPERS[name], (
|
||||
function, *(scalar64(a) for a in callback.src[1:]), marker), dtypes.int32)
|
||||
if touched != set(specifications) or not substitutions:
|
||||
raise ValueError("A native USB polling loop has no entry guard")
|
||||
sink = sink.substitute(substitutions, enter_calls=True)
|
||||
# Substitution can rebuild range identities; immutable unique range args
|
||||
# are the previously checked stable lookup keys.
|
||||
by_arg = {loop.arg: (slot, timeout) for loop, (slot, timeout) in specifications.items()}
|
||||
expected_conditions = {end.src[1].arg: condition for end, condition in ends.items()}
|
||||
replacements = {}
|
||||
for end in _nodes(sink):
|
||||
if end.op is Ops.BACKEDGE and end.src[1].arg in by_arg:
|
||||
slot, _ = by_arg[end.src[1].arg]
|
||||
condition = expected_conditions[end.src[1].arg].substitute(substitutions, enter_calls=True)
|
||||
check = self._native("sp_loop_check", (condition.cast(dtypes.uint64), UOp.const(slot, dtypes.uint64)), dtypes.uint64)
|
||||
replacements[end] = end.replace(src=(end.src[0], end.src[1], check.cast(dtypes.bool)))
|
||||
if len(replacements) != len(loops):
|
||||
raise ValueError("Native USB loop protection is incomplete")
|
||||
rewritten = sink.substitute(replacements, enter_calls=True)
|
||||
args.insert(ffi_slot, self.ffi)
|
||||
if len(args) != len(call.src) or any(args[i + (i >= ffi_slot)] is not original for i, original in enumerate(call.src[1:])):
|
||||
raise ValueError("Native host call argument insertion lost an original binding")
|
||||
return self._compile(call, rewritten, args)
|
||||
|
||||
def protect_call(self, call):
|
||||
original = _fresh_host(call)
|
||||
sink = inline_host_sink(original)
|
||||
if _payloads(sink) != _payloads(original):
|
||||
raise ValueError("Host scope binding changed opaque payloads")
|
||||
if any(_name(u) in GUARD_NAMES for u in _calls(sink)):
|
||||
raise ValueError("Refusing partial or repeated native protection")
|
||||
return self._qcom(call, sink) if call.arg.aux.device == ("QCOM",) else self._usb(call, sink)
|
||||
|
||||
def _verify_call(self, call):
|
||||
program, sink = _host(call)
|
||||
if program.arg.target != self.renderer.target:
|
||||
raise ValueError("Native CPU target changed")
|
||||
_verify_cpu_parameter_abi(program)
|
||||
calls = _calls(sink)
|
||||
if call.arg.aux.device == ("QCOM",):
|
||||
if _weak(sink) or sum(_name(u) == "starpilot_bounded_wait" for u in calls) != 1:
|
||||
raise ValueError("QCOM native guard proof missing")
|
||||
else:
|
||||
if self.role != "driving" or not Device["AMD"].is_usb:
|
||||
raise ValueError("Native Chestnut identity missing")
|
||||
if any(_name(u) in FFI_NAMES for u in calls):
|
||||
raise ValueError("An unchecked native USB function remains")
|
||||
weak = _weak(sink)
|
||||
boundaries = [u for u in _nodes(sink) if u.op is Ops.BACKEDGE and u.src[1] in weak]
|
||||
if not weak or len(boundaries) != len(weak):
|
||||
raise ValueError("Native USB polling proof missing")
|
||||
for end in boundaries:
|
||||
cond = end.src[2]
|
||||
if not _checked_poll_exit(cond):
|
||||
raise ValueError("An unchecked native polling exit remains")
|
||||
if not any(_name(u) == "sp_loop_touch" for u in calls):
|
||||
raise ValueError("Native polling entry proof missing")
|
||||
markers = [u for u in _nodes(UOp.sink(*call.src[1:])) if u.op is Ops.ALLOC and u.tag == FFI_TAG]
|
||||
if len(markers) != 1 or markers[0].dtype is not dtypes.uint64 or markers[0].shape != (len(FFI_NAMES),):
|
||||
raise ValueError("Fresh native function table marker missing")
|
||||
return _proof(call)
|
||||
|
||||
def _prepare(self, jit):
|
||||
# The only preparation path is fresh HCQ re-encoding from authenticated
|
||||
# pre-HCQ input. Stored outlined PROGRAMs are never reverse-inlined.
|
||||
self._verify(jit)
|
||||
return jit
|
||||
|
||||
def _verify(self, jit):
|
||||
cap = getattr(jit, "captured", None)
|
||||
if cap is None or "linear" in cap.__dict__:
|
||||
raise ValueError("Native guard verification must precede captured execution/linking")
|
||||
for entry in cap._linear.src:
|
||||
call = entry.without_after
|
||||
if call.op is not Ops.CALL or not (call.body.op is Ops.STORE or isinstance(call.arg.aux, hcq2.HCQInfo)):
|
||||
raise ValueError("Unknown native captured schedule entry")
|
||||
calls = [u for u in _nodes(cap._linear) if u.op is Ops.CALL and isinstance(u.arg.aux, hcq2.HCQInfo)]
|
||||
if not calls:
|
||||
raise ValueError("Missing native guarded submission")
|
||||
return [self._verify_call(call) for call in calls]
|
||||
|
||||
@staticmethod
|
||||
def _capture_items(artifact):
|
||||
return [(key, value) for key, value in artifact.items() if getattr(value, "captured", None) is not None]
|
||||
|
||||
def prepare_artifact(self, artifact, chestnut=False):
|
||||
items = self._capture_items(artifact)
|
||||
expected = 3 if self.role == "driving" else 1
|
||||
if len(items) != expected or "_starpilot_native_guard" in artifact:
|
||||
raise ValueError("Unknown native artifact capture count or repeated preparation")
|
||||
if self.role == "driving" and (artifact.get("execution_device") != ("AMD" if chestnut else "QCOM") or
|
||||
artifact.get("warp_device") != "QCOM"):
|
||||
raise ValueError("Native driving backend contract changed")
|
||||
for _, jit in items:
|
||||
self._prepare(jit)
|
||||
artifact["_starpilot_native_guard"] = {
|
||||
"identity": self.identity,
|
||||
"captures": {repr(key): self._verify(jit) for key, jit in items},
|
||||
}
|
||||
return artifact
|
||||
|
||||
def artifact(self, artifact, chestnut=False):
|
||||
items = self._capture_items(artifact)
|
||||
expected = 3 if self.role == "driving" else 1
|
||||
if len(items) != expected:
|
||||
raise ValueError("Unknown native artifact capture count")
|
||||
proof = artifact.get("_starpilot_native_guard")
|
||||
if not isinstance(proof, dict) or proof.get("identity") != self.identity:
|
||||
raise ValueError("Native artifact guard identity changed or missing")
|
||||
actual = {repr(key): self._verify(jit) for key, jit in items}
|
||||
if actual != proof.get("captures"):
|
||||
raise ValueError("Native compiled host proof changed")
|
||||
if self.role == "driving" and (artifact.get("execution_device") != ("AMD" if chestnut else "QCOM") or
|
||||
artifact.get("warp_device") != "QCOM"):
|
||||
raise ValueError("Native driving backend contract changed")
|
||||
return artifact
|
||||
|
||||
def prepare_warp(self, jit):
|
||||
return self._prepare(jit)
|
||||
|
||||
def warp(self, jit):
|
||||
# The loader must authenticate the complete known stock warp file first.
|
||||
self._verify(jit)
|
||||
return jit
|
||||
|
||||
def install_fresh_guard(self):
|
||||
original = hcq2.lower_and_compile
|
||||
if original is not realize.lower_and_compile:
|
||||
raise ValueError("Native host compiler was already replaced")
|
||||
def guarded(linear, *args, **kwargs):
|
||||
replacements = {}
|
||||
for call in _nodes(linear):
|
||||
if call.op is not Ops.CALL or not isinstance(call.arg.aux, hcq2.HCQInfo):
|
||||
continue
|
||||
if call.body.op is Ops.SINK:
|
||||
replacements[call] = self.protect_call(call)
|
||||
elif call.body.op is Ops.PROGRAM:
|
||||
self._verify_call(call)
|
||||
else:
|
||||
raise ValueError("Unknown fresh native host body")
|
||||
prepared = linear.substitute(replacements, enter_calls=True)
|
||||
for call in _nodes(prepared):
|
||||
if call.op is Ops.CALL and realize._get_call_to_compile(call) is not None:
|
||||
raise ValueError("Residual unqualified compilation after native guards")
|
||||
# Every compiled host is guarded before returning to HCQ. This does not
|
||||
# execute the intermediate graph or ask a GPU compiler to rebuild kernels.
|
||||
return original(prepared, *args, **kwargs)
|
||||
|
||||
hcq2.lower_and_compile = guarded
|
||||
|
||||
|
||||
def install(role, fence_library, usb_library):
|
||||
global _INSTALLATION
|
||||
if _INSTALLATION is not None:
|
||||
if _INSTALLATION.role != role:
|
||||
raise ValueError("Native guard role cannot change")
|
||||
return _INSTALLATION
|
||||
candidate = Protection(role, fence_library, usb_library)
|
||||
candidate.install_fresh_guard()
|
||||
_INSTALLATION = candidate
|
||||
return candidate
|
||||
@@ -18,10 +18,11 @@ _GUARD_HASHES = {
|
||||
_POLICY_SHA256 = 'c7eae9e0fab99e8c5fa56854b5a1e1ca8c36c81ef73668dd18ede27de61e3f4b'
|
||||
_selection = None
|
||||
_protection = None
|
||||
_plan = None
|
||||
|
||||
|
||||
def prepare(role):
|
||||
global _selection
|
||||
global _selection, _plan
|
||||
if _selection is not None and _selection.get('purpose') == 'compiler':
|
||||
raise RuntimeError('A model compiler cannot become an inference child')
|
||||
from openpilot.common.hardware import AGNOS
|
||||
@@ -31,9 +32,20 @@ def prepare(role):
|
||||
if _selection['role'] != role:
|
||||
raise RuntimeError('A model child cannot change its execution role')
|
||||
return _selection
|
||||
_verify_policy()
|
||||
cache = Path('/data/starpilot-model-runtime')
|
||||
_selection = activate(BUNDLE, MANIFEST_SHA256, LEGACY_REVISION, role, cache)
|
||||
if role not in ('driving', 'monitoring'):
|
||||
raise ValueError('Unknown model runtime role')
|
||||
if role == 'driving':
|
||||
import os
|
||||
from openpilot.starpilot.models.manager import freeze_runtime
|
||||
_plan = freeze_runtime(recovery_small_only=os.getenv('STARPILOT_MODELD_RECOVERY_SMALL_ONLY') == '1')
|
||||
if role == 'monitoring' or _plan.family == 'native02e':
|
||||
from openpilot.starpilot.models.runtime_bridge import native
|
||||
_selection = native.prepare(role, _plan)
|
||||
else:
|
||||
_verify_policy()
|
||||
cache = Path('/data/starpilot-model-runtime')
|
||||
_selection = activate(BUNDLE, MANIFEST_SHA256, LEGACY_REVISION, role, cache)
|
||||
_selection['family'] = 'legacy9d'
|
||||
return _selection
|
||||
|
||||
|
||||
@@ -46,6 +58,7 @@ def prepare_compiler():
|
||||
selection = activate(BUNDLE, MANIFEST_SHA256, LEGACY_REVISION, 'driving',
|
||||
Path(tempfile.gettempdir()).resolve() / 'starpilot-model-compiler')
|
||||
selection['purpose'] = 'compiler'
|
||||
selection['family'] = 'legacy9d'
|
||||
_selection = selection
|
||||
return _selection
|
||||
|
||||
@@ -84,7 +97,15 @@ def _library(name):
|
||||
|
||||
|
||||
def selected_legacy():
|
||||
return _selection is not None
|
||||
return _selection is not None and _selection.get('family') == 'legacy9d'
|
||||
|
||||
|
||||
def prepared():
|
||||
return _selection is not None and _selection.get('purpose') != 'compiler'
|
||||
|
||||
|
||||
def selected_plan():
|
||||
return _plan
|
||||
|
||||
|
||||
def _guard(chestnut):
|
||||
@@ -197,6 +218,9 @@ def _load_chestnut(path):
|
||||
|
||||
|
||||
def load_oob(path, chestnut=False):
|
||||
if _selection is not None and _selection.get('family') == 'native02e':
|
||||
from openpilot.starpilot.models.runtime_bridge import native
|
||||
return native.load_oob(path, chestnut)
|
||||
if _selection is None:
|
||||
raise RuntimeError('A device model loader requires the selected runtime')
|
||||
if _selection.get('purpose') == 'compiler':
|
||||
@@ -232,6 +256,9 @@ def load_oob(path, chestnut=False):
|
||||
|
||||
|
||||
def load_warp(path):
|
||||
if _selection is not None and _selection.get('family') == 'native02e':
|
||||
from openpilot.starpilot.models.runtime_bridge import native
|
||||
return native.load_warp(path)
|
||||
import pickle
|
||||
if _selection is None or _selection['role'] != 'monitoring':
|
||||
raise RuntimeError('A monitoring warp loader requires the selected runtime')
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Freeze one driving runtime before importing either tinygrad family."""
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
NATIVE_REVISION = "02e698160eed1751738212144a7ea2cfb160b3f1"
|
||||
LEGACY_REVISION = "9d0446a4ba8a532c8b674fb6ad795af015cd9dcf"
|
||||
NATIVE_ABI = "tinygrad_single_v2_arena"
|
||||
LEGACY_ABI = "tinygrad_single_v1_arena"
|
||||
NATIVE_GENERATION = "native02e-v1"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RuntimePlan:
|
||||
family: str
|
||||
compiler_revision: str
|
||||
artifact_abi: str
|
||||
small_id: str
|
||||
big_id: str
|
||||
preferences_json: str
|
||||
entries_json: str
|
||||
default_json: str
|
||||
snapshot_sha256: str
|
||||
|
||||
|
||||
def selection_family(preferences: dict, entries: dict, *, recovery_small_only: bool = False) -> str:
|
||||
# Entries come from validated catalogs: native rows include qualification identity.
|
||||
if recovery_small_only:
|
||||
return "native02e"
|
||||
small, big = preferences["small"], preferences["big"]
|
||||
if big and big != "bundled-current":
|
||||
return "native02e" if entries.get(big, {}).get("_runtime_family") == "native02e" else "legacy9d"
|
||||
return "native02e" if small == "rdf43" or small not in entries else "legacy9d"
|
||||
|
||||
|
||||
def freeze_plan(preferences: dict, entries: dict, default: dict, *, recovery_small_only: bool = False) -> RuntimePlan:
|
||||
# Keep unconverted BIG and its fallback on legacy9d in the SAME process.
|
||||
small, big = preferences["small"], preferences["big"]
|
||||
family = selection_family(preferences, entries, recovery_small_only=recovery_small_only)
|
||||
native = family == "native02e"
|
||||
if default["runtime_family"] != family:
|
||||
raise ValueError("wrong runtime fallback for frozen selection")
|
||||
selected_big = big if not recovery_small_only and entries.get(big, {}).get("_runtime_family") == family else ""
|
||||
parts = [json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False) for value in (preferences, entries, default)]
|
||||
return RuntimePlan(family, NATIVE_REVISION if native else LEGACY_REVISION, NATIVE_ABI if native else LEGACY_ABI,
|
||||
"rdf43" if native else small, selected_big, *parts,
|
||||
hashlib.sha256("\n".join(parts).encode()).hexdigest())
|
||||
|
||||
|
||||
def require_artifact_contract(artifact: dict, plan: RuntimePlan | None) -> None:
|
||||
revision, abi = (plan.compiler_revision, plan.artifact_abi) if plan is not None else (LEGACY_REVISION, LEGACY_ABI)
|
||||
if (not isinstance(artifact, dict) or artifact.get("compiler_revision") != revision or
|
||||
artifact.get("artifact_abi") != abi or artifact.get("format_version") != 1):
|
||||
raise ValueError("catalog model compiler/ABI mismatch")
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Release-shaped metadata fixtures; never qualify or execute fixture model bytes."""
|
||||
from contextlib import contextmanager, ExitStack
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from openpilot.common.file_chunker import package_file
|
||||
from openpilot.starpilot.models import manager
|
||||
from openpilot.starpilot.models import catalog as catalog_module
|
||||
from openpilot.starpilot.models.runtime_selection import NATIVE_ABI, NATIVE_GENERATION, NATIVE_REVISION
|
||||
|
||||
|
||||
@contextmanager
|
||||
def native_fixture(root: Path, data: bytes):
|
||||
shipped = root / 'shipped'
|
||||
source = root / 'native-fixture-source.pkl'
|
||||
source.write_bytes(data)
|
||||
for relative in ('rdf43_driving_tinygrad.pkl', 'compat9d/rdf43_driving_tinygrad.pkl'):
|
||||
destination = shipped / relative
|
||||
if not Path(f'{destination}.chunkmanifest').exists():
|
||||
package_file(source, destination)
|
||||
source.unlink()
|
||||
identity = {'artifact_sha256': hashlib.sha256(data).hexdigest(), 'artifact_size': len(data),
|
||||
'qualification_sha256': hashlib.sha256(b'metadata fixture, no runtime qualification').hexdigest()}
|
||||
doc = {'generation': NATIVE_GENERATION, 'compiler_revision': NATIVE_REVISION, 'artifact_abi': NATIVE_ABI,
|
||||
'models': [{'id': 'rdf43', 'name': 'RDF metadata fixture', 'version': 'v15', 'uses_external_gpu': False,
|
||||
'artifact_format': NATIVE_ABI, 'artifact_chunk_count': 1, **identity}],
|
||||
'stock': {name: dict(identity) for name in ('dmonitoring_model_tinygrad.pkl',
|
||||
'dm_warp_1928x1208_tinygrad.pkl', 'dm_warp_1344x760_tinygrad.pkl')}}
|
||||
path = root / 'native-fixture-catalog.json'
|
||||
path.write_text(json.dumps(doc))
|
||||
with ExitStack() as stack:
|
||||
for module in (manager, catalog_module):
|
||||
stack.enter_context(patch.object(module, 'NATIVE_CATALOG_PATH', path))
|
||||
yield doc
|
||||
@@ -2,13 +2,33 @@ import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
import tempfile
|
||||
from unittest.mock import patch
|
||||
|
||||
from openpilot.starpilot.models import manager
|
||||
from openpilot.starpilot.models.runtime_selection import NATIVE_ABI
|
||||
from openpilot.starpilot.models.tests.native_fixture import native_fixture
|
||||
from openpilot.starpilot.models.catalog import BUNDLED_CURRENT, CATALOG, CATALOG_PATH, DEFAULT_SMALL, resolve_selection
|
||||
from openpilot.starpilot.models.status import (ModelHealth, ModelLoad, ModelOutput, ModelProcess,
|
||||
ModelVariant, project_status)
|
||||
|
||||
|
||||
class TestModelCatalogStatus(unittest.TestCase):
|
||||
def setUp(self):
|
||||
temporary = self.enterContext(tempfile.TemporaryDirectory())
|
||||
self.root = Path(temporary)
|
||||
self.enterContext(patch.object(manager, "SHIPPED_MODELS", self.root / "shipped"))
|
||||
document = self.enterContext(native_fixture(self.root, b"catalog source identity fixture"))
|
||||
# Status requests need a qualified published BIG identity, not a pending
|
||||
# historical catalog row. No artifact is executed or marked installed.
|
||||
historical = json.loads(CATALOG_PATH.read_text())["models"]
|
||||
big = next(row for row in historical if row["id"] == "cinquev3")
|
||||
document["models"].append({"id": big["id"], "name": big["name"], "version": big["version"],
|
||||
"uses_external_gpu": True, "artifact_sha256": "b" * 64,
|
||||
"artifact_size": 123, "artifact_chunk_count": 1,
|
||||
"artifact_format": NATIVE_ABI, "qualification_sha256": "c" * 64})
|
||||
manager.NATIVE_CATALOG_PATH.write_text(json.dumps(document))
|
||||
|
||||
def test_catalog_pins_current_source_and_lists_model_metadata(self):
|
||||
manifest_ids = [row['id'] for row in json.loads(CATALOG_PATH.read_text())['models']]
|
||||
self.assertEqual(len(manifest_ids), len(set(manifest_ids)))
|
||||
@@ -17,7 +37,7 @@ class TestModelCatalogStatus(unittest.TestCase):
|
||||
self.assertCountEqual(catalog_ids, [BUNDLED_CURRENT, *manifest_ids])
|
||||
entry = resolve_selection(None)
|
||||
self.assertEqual(entry.model_id, DEFAULT_SMALL)
|
||||
source = Path(__file__).resolve().parents[4] / entry.source_path
|
||||
source = manager.SHIPPED_MODELS / Path(entry.source_path).name
|
||||
from openpilot.common.file_chunker import get_existing_chunks
|
||||
digest = hashlib.sha256()
|
||||
for part in get_existing_chunks(source)[1:]:
|
||||
|
||||
@@ -10,6 +10,7 @@ from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from openpilot.starpilot.models.tests.native_fixture import native_fixture
|
||||
from openpilot.starpilot.models.catalog import ARTIFACT_ABI, BUNDLED_CURRENT, CATALOG_PATH, DEFAULT_SMALL
|
||||
from openpilot.starpilot.models.manager import (
|
||||
ModelError, ModelManager, atomic_json, catalog, preferences, randomize_next_start, resolve_runtime, validate_manifest,
|
||||
@@ -17,28 +18,30 @@ from openpilot.starpilot.models.manager import (
|
||||
|
||||
|
||||
def run_download_owner(root, manifest, data, entered, release, result):
|
||||
class BlockedPart(io.BytesIO):
|
||||
def read(self, size=-1):
|
||||
entered.set()
|
||||
if not release.wait(5):
|
||||
raise OSError('fixture stalled')
|
||||
return super().read(size)
|
||||
with native_fixture(Path(root), data) as native_document:
|
||||
class BlockedPart(io.BytesIO):
|
||||
def read(self, size=-1):
|
||||
entered.set()
|
||||
if not release.wait(5):
|
||||
raise OSError('fixture stalled')
|
||||
return super().read(size)
|
||||
|
||||
def opener(url, timeout):
|
||||
if '/manifests/' in url:
|
||||
return io.BytesIO(json.dumps(manifest).encode())
|
||||
if url.endswith('chunk01of02'):
|
||||
return io.BytesIO(data[:10])
|
||||
return BlockedPart(data[10:])
|
||||
def opener(url, timeout):
|
||||
if '/manifests/' in url:
|
||||
document = native_document if 'native02e-v1' in url else manifest
|
||||
return io.BytesIO(json.dumps(document).encode())
|
||||
if url.endswith('chunk01of02'):
|
||||
return io.BytesIO(data[:10])
|
||||
return BlockedPart(data[10:])
|
||||
|
||||
manager = ModelManager(root=Path(root), parked=lambda: True, gpu_present=lambda: False, opener=opener)
|
||||
with patch('openpilot.starpilot.models.manager.shutil.disk_usage', return_value=SimpleNamespace(free=1024**3)):
|
||||
try:
|
||||
manager.action('download', {'model': 'gwm8223'})
|
||||
manager.worker.join(6)
|
||||
result.put(manager.snapshot())
|
||||
finally:
|
||||
manager.close()
|
||||
manager = ModelManager(root=Path(root), parked=lambda: True, gpu_present=lambda: False, opener=opener)
|
||||
with patch('openpilot.starpilot.models.manager.shutil.disk_usage', return_value=SimpleNamespace(free=1024**3)):
|
||||
try:
|
||||
manager.action('download', {'model': 'gwm8223'})
|
||||
manager.worker.join(6)
|
||||
result.put(manager.snapshot())
|
||||
finally:
|
||||
manager.close()
|
||||
|
||||
|
||||
class ModelManagerTest(unittest.TestCase):
|
||||
@@ -63,6 +66,7 @@ class ModelManagerTest(unittest.TestCase):
|
||||
identity = patch('openpilot.starpilot.models.manager.' + attribute, value)
|
||||
identity.start()
|
||||
self.addCleanup(identity.stop)
|
||||
self.native_manifest = self.enterContext(native_fixture(self.root, self.data))
|
||||
self.manifest = json.loads(CATALOG_PATH.read_text())
|
||||
self.row = next(x for x in self.manifest['models'] if x['id'] == 'gwm8223')
|
||||
self.row.update(artifact_format=ARTIFACT_ABI, artifact_sha256=hashlib.sha256(self.data).hexdigest(),
|
||||
@@ -74,7 +78,8 @@ class ModelManagerTest(unittest.TestCase):
|
||||
self.assertTrue(url.startswith('https://huggingface.co/buckets/StarPilot-Driving/StarPilot-Resources/resolve/'))
|
||||
self.urls.append(url)
|
||||
if '/manifests/' in url:
|
||||
return io.BytesIO(json.dumps(self.manifest).encode())
|
||||
document = self.native_manifest if 'native02e-v1' in url else self.manifest
|
||||
return io.BytesIO(json.dumps(document).encode())
|
||||
if url.endswith('chunk01of02'):
|
||||
return io.BytesIO(self.data[:10])
|
||||
if url.endswith('chunk02of02'):
|
||||
@@ -136,6 +141,42 @@ class ModelManagerTest(unittest.TestCase):
|
||||
assert self.manager.selected_gpu_status() == (False, False)
|
||||
assert not (self.root / '.download-job.json').exists()
|
||||
|
||||
def test_downloaded_native_rdf_respects_frozen_big_runtime_family(self):
|
||||
from openpilot.starpilot.models.runtime_selection import NATIVE_ABI, NATIVE_GENERATION
|
||||
|
||||
# Metadata fixture bytes are never unpickled or submitted to a device.
|
||||
native_rdf = self.root / NATIVE_GENERATION / 'rdf43/rdf43_driving_tinygrad.pkl'
|
||||
native_rdf.parent.mkdir(parents=True)
|
||||
native_rdf.write_bytes(self.data)
|
||||
legacy_big = next(row for row in self.manifest['models'] if row['id'] == 'cinquev3')
|
||||
legacy_big.update(artifact_format=ARTIFACT_ABI, artifact_sha256=hashlib.sha256(self.data).hexdigest(),
|
||||
artifact_size=len(self.data), artifact_chunk_count=0)
|
||||
atomic_json(self.root / 'catalog.json', self.manifest)
|
||||
for native in (False, True):
|
||||
with self.subTest(native_big=native):
|
||||
document = json.loads(json.dumps(self.native_manifest))
|
||||
if native:
|
||||
document['models'].append({**legacy_big, 'artifact_format': NATIVE_ABI,
|
||||
'qualification_sha256': document['models'][0]['qualification_sha256']})
|
||||
atomic_json(self.root / 'native-fixture-catalog.json', document)
|
||||
big_path = self.root / NATIVE_GENERATION / 'cinquev3/cinquev3_driving_tinygrad.pkl' if native else (
|
||||
self.root / 'cinquev3/cinquev3_driving_tinygrad.pkl')
|
||||
big_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
big_path.write_bytes(self.data)
|
||||
atomic_json(self.root / 'preferences.json', {'small': 'rdf43', 'big': 'cinquev3'})
|
||||
selection = resolve_runtime(True, root=self.root)
|
||||
self.assertTrue(selection.allow_big)
|
||||
self.assertEqual(selection.big_path, big_path)
|
||||
if native:
|
||||
self.assertEqual(selection.small_path, native_rdf)
|
||||
self.assertNotIn('compat9d', selection.fallback_path.parts)
|
||||
else:
|
||||
self.assertEqual(selection.small_path, selection.fallback_path)
|
||||
self.assertIn('compat9d', selection.small_path.parts)
|
||||
self.assertNotEqual(selection.small_path, native_rdf)
|
||||
self.assertEqual(selection.small_path.read_bytes(), self.data)
|
||||
self.assertEqual(preferences(self.root)['big'], 'cinquev3')
|
||||
|
||||
def test_chunk_download_verifies_and_selects_next_start(self):
|
||||
self.download()
|
||||
self.assertEqual(self.manager.progress, 'Downloaded!')
|
||||
@@ -144,7 +185,8 @@ class ModelManagerTest(unittest.TestCase):
|
||||
selected = resolve_runtime(True, root=self.root)
|
||||
self.assertEqual(selected.small_id, 'gwm8223')
|
||||
self.assertEqual(selected.small_path.read_bytes(), self.data)
|
||||
self.assertTrue(all('/v26/' in u or 'model_names_v26.json' in u for u in self.urls))
|
||||
self.assertTrue(all('/v26/' in u or u.endswith(('/manifests/model_names_v26.json',
|
||||
'/manifests/model_names_native02e-v1.json')) for u in self.urls))
|
||||
|
||||
def test_corruption_never_publishes_or_selects(self):
|
||||
self.bad = True
|
||||
@@ -654,7 +696,7 @@ class ModelManagerTest(unittest.TestCase):
|
||||
validate_manifest({**good, "models": [known]})
|
||||
|
||||
def test_remote_metadata_cache_refresh_and_root_isolation(self):
|
||||
from openpilot.starpilot.models.catalog import BY_ID, model_entries
|
||||
from openpilot.starpilot.models.catalog import model_entries
|
||||
row = dict(self.row, id="remote_cache", name="First name", version="v16")
|
||||
atomic_json(self.root / "catalog.json", {**self.manifest, "models": [row]})
|
||||
self.assertEqual(model_entries(self.root)[row["id"]].name, "First name")
|
||||
@@ -664,7 +706,7 @@ class ModelManagerTest(unittest.TestCase):
|
||||
self.assertEqual(model_entries(self.root)[row["id"]].name, "Second name")
|
||||
with tempfile.TemporaryDirectory() as empty:
|
||||
self.assertNotIn(row["id"], model_entries(Path(empty)))
|
||||
self.assertEqual(model_entries(self.root)[DEFAULT_SMALL].source_sha256, BY_ID[DEFAULT_SMALL].source_sha256)
|
||||
self.assertEqual(model_entries(self.root)[DEFAULT_SMALL].source_sha256, self.native_manifest["models"][0]["artifact_sha256"])
|
||||
|
||||
def test_catalog_page_refresh_is_async_bounded_and_parked_only(self):
|
||||
entered, release = threading.Event(), threading.Event()
|
||||
|
||||
@@ -115,6 +115,7 @@ def test_remote_inference_failure_does_not_change_chestnut_status():
|
||||
def execute(statements, env):
|
||||
env.setdefault("os", SimpleNamespace(getenv=lambda key: None))
|
||||
env.setdefault("recovery_small_only", False)
|
||||
env.setdefault("MODEL_RUNTIME_PLAN", object())
|
||||
from openpilot.starpilot.models.recovery import STARTUP_FALLBACK_EXIT
|
||||
env.setdefault("STARTUP_FALLBACK_EXIT", STARTUP_FALLBACK_EXIT)
|
||||
exec(compile(ast.fix_missing_locations(ast.Module(body=statements, type_ignores=[])), str(MODEL_SOURCE), "exec"), env)
|
||||
@@ -136,13 +137,16 @@ def test_observed_availability_and_big_eligibility(monkeypatch, present, cable,
|
||||
import sys
|
||||
|
||||
manager = ModuleType("openpilot.starpilot.models.manager")
|
||||
manager.resolve_runtime = Mock(side_effect=lambda chestnut_available, randomize:
|
||||
manager.resolve_runtime = Mock(side_effect=lambda chestnut_available, plan:
|
||||
SimpleNamespace(allow_big=allow_big and chestnut_available, big_path=big_path))
|
||||
manager.requested_runtime_id = Mock(return_value="selected")
|
||||
amd = ModuleType("tinygrad.runtime.ops_amd")
|
||||
amd.AMDDevice = type("FakeAMDDevice", (), {"wait_timeout_ms": 123})
|
||||
monkeypatch.setitem(sys.modules, manager.__name__, manager)
|
||||
monkeypatch.setitem(sys.modules, amd.__name__, amd)
|
||||
am = ModuleType("tinygrad.runtime.support.am")
|
||||
am.startup_trace = SimpleNamespace(ENABLED=False, REPORT=None)
|
||||
monkeypatch.setitem(sys.modules, am.__name__, am)
|
||||
body = main_body()
|
||||
start = next(i for i, node in enumerate(body) if isinstance(node, ast.ImportFrom) and node.module == manager.__name__)
|
||||
end = next(i for i, node in enumerate(body) if ast.unparse(node) == "gc.disable()")
|
||||
@@ -152,8 +156,8 @@ def test_observed_availability_and_big_eligibility(monkeypatch, present, cable,
|
||||
"jetlink_adapter": SimpleNamespace(link_on=lambda: link)}
|
||||
execute(body[start:end], env)
|
||||
available = present or (cable and not link)
|
||||
manager.resolve_runtime.assert_called_once_with(chestnut_available=available, randomize=True)
|
||||
manager.requested_runtime_id.assert_called_once_with(available)
|
||||
manager.resolve_runtime.assert_called_once_with(chestnut_available=available, plan=env["MODEL_RUNTIME_PLAN"])
|
||||
manager.requested_runtime_id.assert_called_once_with(available, plan=env["MODEL_RUNTIME_PLAN"])
|
||||
env["chestnut_present"].assert_called_once_with()
|
||||
assert env["cable_connected"].call_count == (0 if present else 1)
|
||||
assert env["CHESTNUT"] is expected
|
||||
@@ -209,6 +213,9 @@ def test_big_worker_waits_before_load_and_failure_requests_fresh_small_process(c
|
||||
small_version=1, small_id="small", small_sha256="small-hash",
|
||||
big_version=1, big_id="big", big_sha256="hash")
|
||||
from openpilot.starpilot.models.catalog import BY_ID, DEFAULT_SMALL, DEFAULT_SMALL_SHA256
|
||||
selection.fallback_path = "bundled-small"
|
||||
selection.fallback_version = BY_ID[DEFAULT_SMALL].version
|
||||
selection.fallback_sha256 = DEFAULT_SMALL_SHA256
|
||||
env: dict = {"BY_ID": BY_ID, "DEFAULT_SMALL": DEFAULT_SMALL, "DEFAULT_SMALL_SHA256": DEFAULT_SMALL_SHA256,
|
||||
"time": SimpleNamespace(monotonic=lambda: 0), "cloudlog": Mock(), "selection": selection,
|
||||
"CHESTNUT": True, "vipc_client_main": SimpleNamespace(width=1928, height=1208), "wait_for_chestnut": wait,
|
||||
@@ -332,8 +339,8 @@ def test_recovery_child_resolves_small_without_randomizer_or_amd(monkeypatch):
|
||||
"chestnut_compiled": Mock(side_effect=AssertionError("Big compiled probe forbidden")),
|
||||
"Params": lambda: params, "os": SimpleNamespace(getenv=lambda key: "1")}
|
||||
execute(body[start:end], env)
|
||||
manager.resolve_runtime.assert_called_once_with(chestnut_available=False, randomize=False)
|
||||
manager.requested_runtime_id.assert_called_once_with(True)
|
||||
manager.resolve_runtime.assert_called_once_with(chestnut_available=False, plan=env["MODEL_RUNTIME_PLAN"])
|
||||
manager.requested_runtime_id.assert_called_once_with(True, plan=env["MODEL_RUNTIME_PLAN"])
|
||||
assert env["CHESTNUT"] is False
|
||||
assert "AMDDevice" not in env
|
||||
assert params.put_bool.call_args_list == [(("ChestnutLoading", False),)]
|
||||
@@ -362,7 +369,9 @@ def test_failed_selected_small_uses_only_verified_rdf_or_refuses_startup(monkeyp
|
||||
monkeypatch.setattr(manager, 'shipped_default', lambda: None if fallback_failure == 'missing' else shipped)
|
||||
loader = Mock(side_effect=[RuntimeError('selected failed'), RuntimeError('RDF failed') if fallback_failure == 'load' else (rdf, 'rdf-prepared')])
|
||||
stock = Mock(side_effect=AssertionError('stock model must not load'))
|
||||
env: dict = {'selection': SimpleNamespace(small_path=Path('custom.pkl'), small_version='v15', small_id='gwm8223', small_sha256='custom'),
|
||||
env: dict = {'selection': SimpleNamespace(small_path=Path('custom.pkl'), small_version='v15', small_id='gwm8223', small_sha256='custom',
|
||||
fallback_path=None if fallback_failure == 'missing' else shipped,
|
||||
fallback_version='v15', fallback_sha256=DEFAULT_SMALL_SHA256),
|
||||
'vipc_client_main': SimpleNamespace(width=1928, height=1208), 'load_verified_model': loader,
|
||||
'ModelState': stock, 'modeld_pkl_path': stock, 'cloudlog': Mock(), 'BY_ID': BY_ID,
|
||||
'DEFAULT_SMALL': DEFAULT_SMALL, 'DEFAULT_SMALL_SHA256': DEFAULT_SMALL_SHA256}
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
"""Metadata-only tests of actual selection functions; no runtime admission proof."""
|
||||
import ast
|
||||
from dataclasses import dataclass, FrozenInstanceError, replace
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
SOURCE = Path(os.environ.get("STARPILOT_PARTIAL_NATIVE_SOURCE_ROOT", Path(__file__).resolve().parents[4]))
|
||||
MODELS = SOURCE / "openpilot/starpilot/models"
|
||||
|
||||
|
||||
def extract(path, names, namespace):
|
||||
tree = ast.parse(path.read_text(), filename=str(path))
|
||||
selected = [node for node in tree.body if isinstance(node, (ast.FunctionDef, ast.ClassDef)) and node.name in names]
|
||||
if {node.name for node in selected} != set(names):
|
||||
raise AssertionError("Missing actual source definitions")
|
||||
# Compile the original nodes unchanged, including decorators and annotations.
|
||||
exec(compile(ast.Module(body=selected, type_ignores=[]), str(path), "exec"), namespace)
|
||||
|
||||
|
||||
def subjects():
|
||||
ns = {"__name__": __name__, "dataclass": dataclass, "hashlib": hashlib, "json": json, "Path": Path, "re": re}
|
||||
# This entire module has only standard-library imports; no production imports.
|
||||
exec(compile((MODELS / "runtime_selection.py").read_text(), str(MODELS / "runtime_selection.py"), "exec"), ns)
|
||||
ns.update(ROOT=Path("/metadata-fixture"), DEFAULT_SMALL="rdf43", BUNDLED_CURRENT="bundled-current",
|
||||
GENERATION="v26", COMPILER_REVISION=ns["LEGACY_REVISION"], ARTIFACT_ABI=ns["LEGACY_ABI"],
|
||||
MODEL_ID=re.compile(r"[a-z0-9][a-z0-9_-]{0,63}\Z"), SHA=re.compile(r"[0-9a-f]{64}\Z"),
|
||||
SUPPORTED_VERSIONS={f"v{i}" for i in range(8, 17)}, MAX_ARTIFACT=2 * 1024**3,
|
||||
MODEL_RUNNER_REVISION=2, BY_ID={},
|
||||
RUNTIME_ARTIFACT_KEYS={"min_runner_revision", "artifact_format", "artifact_sha256", "artifact_size",
|
||||
"artifact_chunk_count", "conversion_provenance", "validation"},
|
||||
CATALOG_PATH=Path("/metadata-fixture/legacy.json"), NATIVE_CATALOG_PATH=Path("/metadata-fixture/native.json"))
|
||||
extract(MODELS / "manager.py", ["ModelError", "validate_manifest", "native_stock", "catalog", "RuntimeSelection",
|
||||
"requested_runtime_id", "resolve_runtime"], ns)
|
||||
ns["ModelEntry"] = types.SimpleNamespace
|
||||
extract(MODELS / "catalog.py", ["resolve_selection"], ns)
|
||||
return ns
|
||||
|
||||
|
||||
class TestPartialNativeSelection(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.n = subjects()
|
||||
self.prefs = {"small": "optional-small", "big": "traintrack", "userFavorites": ["optional-small"],
|
||||
"blacklistedModels": ["another-small"], "randomizer": False, "sortMode": "favorites"}
|
||||
self.entries = {
|
||||
"optional-small": {"id": "optional-small", "version": "v15", "_runtime_family": "legacy9d", "artifact_sha256": "a" * 64},
|
||||
"traintrack": {"id": "traintrack", "version": "v16", "_runtime_family": "native02e", "artifact_sha256": "b" * 64},
|
||||
}
|
||||
self.default = {"runtime_family": "native02e", "version": "v15", "artifact_sha256": "c" * 64}
|
||||
|
||||
def plan(self, **kw):
|
||||
return self.n["freeze_plan"](self.prefs, self.entries, self.default, **kw)
|
||||
|
||||
def resolve(self, plan, available=True, installed=True, fallback=True):
|
||||
self.n["verified_artifact"] = Mock(side_effect=lambda mid, *_: Path("/verified") / mid if installed else None)
|
||||
self.n["shipped_default"] = Mock(return_value=Path("/verified/rdf43") if fallback else None)
|
||||
return self.n["resolve_runtime"](available, plan=plan)
|
||||
|
||||
def document(self, native=True):
|
||||
return {"generation": self.n["NATIVE_GENERATION"] if native else "v26",
|
||||
"compiler_revision": self.n["NATIVE_REVISION"] if native else self.n["LEGACY_REVISION"],
|
||||
"artifact_abi": self.n["NATIVE_ABI"] if native else self.n["LEGACY_ABI"],
|
||||
"models": [{"id": "traintrack", "name": "TrainTrack", "version": "v16", "uses_external_gpu": True,
|
||||
"artifact_sha256": "b" * 64, "artifact_size": 123, "artifact_chunk_count": 0,
|
||||
"artifact_format": self.n["NATIVE_ABI"] if native else self.n["LEGACY_ABI"],
|
||||
"qualification_sha256": "d" * 64}]}
|
||||
|
||||
def test_native_big_uses_native_rdf_without_rewriting_saved_small(self):
|
||||
before = json.dumps(self.prefs, sort_keys=True)
|
||||
plan = self.plan()
|
||||
self.assertEqual((plan.family, plan.small_id, plan.big_id), ("native02e", "rdf43", "traintrack"))
|
||||
selected = self.resolve(plan)
|
||||
self.assertTrue(selected.allow_big)
|
||||
self.assertEqual((selected.small_path, selected.big_path, selected.fallback_path),
|
||||
(Path("/verified/rdf43"), Path("/verified/traintrack"), Path("/verified/rdf43")))
|
||||
self.assertEqual(json.dumps(self.prefs, sort_keys=True), before)
|
||||
self.assertEqual(json.loads(plan.preferences_json), self.prefs)
|
||||
|
||||
def test_default_rdf_selects_native_without_a_big_request(self):
|
||||
self.prefs.update(small="rdf43", big="")
|
||||
plan = self.plan()
|
||||
self.assertEqual((plan.family, plan.small_id, plan.big_id), ("native02e", "rdf43", ""))
|
||||
selected = self.resolve(plan)
|
||||
self.assertEqual(selected.small_sha256, "c" * 64)
|
||||
self.assertFalse(selected.allow_big)
|
||||
|
||||
def test_unconverted_big_keeps_legacy_payload_custom_small_and_saved_request(self):
|
||||
self.entries["traintrack"]["_runtime_family"] = "legacy9d"
|
||||
self.default["runtime_family"] = "legacy9d"
|
||||
plan = self.plan()
|
||||
self.assertEqual((plan.family, plan.small_id, plan.big_id), ("legacy9d", "optional-small", "traintrack"))
|
||||
selected = self.resolve(plan)
|
||||
self.assertTrue(selected.allow_big)
|
||||
self.assertEqual((selected.small_id, selected.big_id), ("optional-small", "traintrack"))
|
||||
self.assertEqual(self.n["requested_runtime_id"](True, plan=plan), "traintrack")
|
||||
self.assertEqual(self.prefs["big"], "traintrack")
|
||||
|
||||
def test_optional_small_only_uses_legacy_and_legacy_fallback(self):
|
||||
self.prefs["big"] = ""
|
||||
self.default["runtime_family"] = "legacy9d"
|
||||
plan = self.plan()
|
||||
selected = self.resolve(plan)
|
||||
self.assertEqual((plan.family, plan.compiler_revision, plan.artifact_abi),
|
||||
("legacy9d", self.n["LEGACY_REVISION"], self.n["LEGACY_ABI"]))
|
||||
self.assertEqual(selected.small_id, "optional-small")
|
||||
self.assertEqual(selected.small_sha256, "a" * 64)
|
||||
self.assertFalse(selected.allow_big)
|
||||
missing = self.resolve(plan, installed=False)
|
||||
self.assertEqual((missing.small_id, missing.small_path), ("rdf43", Path("/verified/rdf43")))
|
||||
|
||||
def test_absent_chestnut_or_artifact_retains_big_request_but_disables_execution(self):
|
||||
for available, installed in ((False, True), (True, False)):
|
||||
with self.subTest(available=available, installed=installed):
|
||||
selected = self.resolve(self.plan(), available, installed)
|
||||
self.assertEqual(selected.big_id, "traintrack")
|
||||
self.assertIsNone(selected.big_path)
|
||||
self.assertFalse(selected.allow_big)
|
||||
self.assertEqual(selected.small_id, "rdf43")
|
||||
|
||||
def test_recovery_forces_native_rdf_and_preserves_preferences(self):
|
||||
before = json.dumps(self.prefs, sort_keys=True)
|
||||
plan = self.plan(recovery_small_only=True)
|
||||
self.assertEqual((plan.family, plan.small_id, plan.big_id), ("native02e", "rdf43", ""))
|
||||
self.assertFalse(self.resolve(plan).allow_big)
|
||||
self.assertEqual(json.dumps(self.prefs, sort_keys=True), before)
|
||||
|
||||
def test_frozen_benchmark_snapshot_is_detached_and_does_not_write_preferences(self):
|
||||
before = json.dumps((self.prefs, self.entries, self.default), sort_keys=True)
|
||||
first = self.plan()
|
||||
self.assertEqual(first, self.plan())
|
||||
self.assertEqual(json.dumps((self.prefs, self.entries, self.default), sort_keys=True), before)
|
||||
self.assertEqual(first.snapshot_sha256, hashlib.sha256("\n".join(
|
||||
(first.preferences_json, first.entries_json, first.default_json)).encode()).hexdigest())
|
||||
self.prefs["small"] = "later-choice"
|
||||
self.entries["traintrack"]["version"] = "v15"
|
||||
self.assertEqual(self.n["requested_runtime_id"](True, plan=first), "traintrack")
|
||||
self.assertEqual(json.loads(first.entries_json)["traintrack"]["version"], "v16")
|
||||
self.assertNotEqual(first.snapshot_sha256, self.plan().snapshot_sha256)
|
||||
with self.assertRaises(FrozenInstanceError):
|
||||
first.small_id = "mutated"
|
||||
|
||||
def test_wrong_fallback_and_missing_verified_default_fail_closed(self):
|
||||
self.default["runtime_family"] = "legacy9d"
|
||||
with self.assertRaisesRegex(ValueError, "wrong runtime fallback"):
|
||||
self.plan()
|
||||
self.default["runtime_family"] = "native02e"
|
||||
with self.assertRaisesRegex(self.n["ModelError"], "missing or corrupt"):
|
||||
self.resolve(self.plan(), fallback=False)
|
||||
|
||||
def test_unknown_or_pending_selector_is_rejected_without_silent_default(self):
|
||||
rdf = types.SimpleNamespace(selectable=True)
|
||||
self.n["model_entries"] = lambda _: {"rdf43": rdf, "pending-big": types.SimpleNamespace(selectable=False)}
|
||||
for requested in ("unknown", "pending-big", 7):
|
||||
with self.subTest(requested=requested), self.assertRaisesRegex(ValueError, "unqualified"):
|
||||
self.n["resolve_selection"](requested)
|
||||
self.assertIs(self.n["resolve_selection"](None), rdf)
|
||||
self.assertIs(self.n["resolve_selection"]("bundled-current"), rdf)
|
||||
|
||||
def test_artifact_revision_abi_and_format_are_exact(self):
|
||||
for plan in (self.plan(), None):
|
||||
artifact = {"compiler_revision": plan.compiler_revision if plan else self.n["LEGACY_REVISION"],
|
||||
"artifact_abi": plan.artifact_abi if plan else self.n["LEGACY_ABI"], "format_version": 1}
|
||||
self.n["require_artifact_contract"](artifact, plan)
|
||||
for key, bad in (("compiler_revision", "0" * 40), ("compiler_revision", "corrupt"),
|
||||
("artifact_abi", "unknown"), ("format_version", 2)):
|
||||
with self.subTest(key=key, bad=bad), self.assertRaisesRegex(ValueError, "compiler/ABI mismatch"):
|
||||
self.n["require_artifact_contract"]({**artifact, key: bad}, plan)
|
||||
|
||||
def test_native_manifest_requires_exact_revision_abi_and_qualification(self):
|
||||
good = self.document()
|
||||
row = self.n["validate_manifest"](good, native=True)["traintrack"]
|
||||
self.assertEqual((row["_runtime_family"], row["compiler_revision"]), ("native02e", self.n["NATIVE_REVISION"]))
|
||||
for key, bad in (("compiler_revision", self.n["LEGACY_REVISION"]), ("compiler_revision", "corrupt"),
|
||||
("artifact_abi", self.n["LEGACY_ABI"]), ("generation", "unknown")):
|
||||
with self.subTest(key=key), self.assertRaises(self.n["ModelError"]):
|
||||
self.n["validate_manifest"]({**good, key: bad}, native=True)
|
||||
for key, bad in (("qualification_sha256", "bad"), ("uses_external_gpu", False), ("artifact_format", "unknown")):
|
||||
doc = self.document()
|
||||
doc["models"][0][key] = bad
|
||||
with self.subTest(key=key), self.assertRaises(self.n["ModelError"]):
|
||||
self.n["validate_manifest"](doc, native=True)
|
||||
|
||||
def test_catalog_preserves_unconverted_legacy_big_payload(self):
|
||||
doc = self.document(native=False)
|
||||
small = {**doc["models"][0], "id": "optional-small", "uses_external_gpu": False}
|
||||
doc["models"].append(small)
|
||||
self.n["read_json"] = Mock(side_effect=[doc, FileNotFoundError()])
|
||||
self.n["native_catalog"] = Mock(return_value={})
|
||||
rows = self.n["catalog"]()
|
||||
self.assertEqual(set(rows), {"traintrack", "optional-small"})
|
||||
self.assertEqual(rows["traintrack"]["_runtime_family"], "legacy9d")
|
||||
self.assertEqual(rows["traintrack"]["artifact_sha256"], "b" * 64)
|
||||
self.assertEqual(rows["optional-small"]["_runtime_family"], "legacy9d")
|
||||
self.assertEqual(rows["optional-small"]["artifact_sha256"], "b" * 64)
|
||||
|
||||
def test_legacy_big_forces_legacy_even_with_saved_default_rdf(self):
|
||||
self.prefs["small"] = "rdf43"
|
||||
self.entries["traintrack"]["_runtime_family"] = "legacy9d"
|
||||
self.default["runtime_family"] = "legacy9d"
|
||||
plan = self.plan()
|
||||
selected = self.resolve(plan)
|
||||
self.assertEqual((plan.family, plan.small_id, plan.big_id), ("legacy9d", "rdf43", "traintrack"))
|
||||
self.assertTrue(selected.allow_big)
|
||||
self.assertEqual(selected.small_sha256, "c" * 64)
|
||||
|
||||
def test_native_counterpart_switches_new_snapshot_only(self):
|
||||
self.entries["traintrack"]["_runtime_family"] = "legacy9d"
|
||||
self.default["runtime_family"] = "legacy9d"
|
||||
old = self.plan()
|
||||
self.entries["traintrack"]["_runtime_family"] = "native02e"
|
||||
self.default["runtime_family"] = "native02e"
|
||||
new = self.plan()
|
||||
self.assertEqual((old.family, new.family), ("legacy9d", "native02e"))
|
||||
self.assertEqual((old.small_id, new.small_id), ("optional-small", "rdf43"))
|
||||
self.assertTrue(self.resolve(old).allow_big)
|
||||
self.assertTrue(self.resolve(new).allow_big)
|
||||
self.assertEqual(self.prefs["small"], "optional-small")
|
||||
|
||||
def test_native_overlay_replaces_only_converted_big_id(self):
|
||||
doc = self.document(native=False)
|
||||
doc["models"].append({**doc["models"][0], "id": "unconverted-big"})
|
||||
native = self.n["validate_manifest"](self.document(), native=True)
|
||||
self.n["read_json"] = Mock(side_effect=[doc, FileNotFoundError()])
|
||||
self.n["native_catalog"] = Mock(return_value=native)
|
||||
rows = self.n["catalog"]()
|
||||
self.assertEqual(set(rows), {"traintrack", "unconverted-big"})
|
||||
self.assertEqual(rows["traintrack"]["_runtime_family"], "native02e")
|
||||
self.assertEqual(rows["unconverted-big"]["_runtime_family"], "legacy9d")
|
||||
self.assertEqual(rows["unconverted-big"]["artifact_sha256"], "b" * 64)
|
||||
|
||||
def test_legacy_big_missing_hardware_or_payload_uses_same_family_fallback(self):
|
||||
self.entries["traintrack"]["_runtime_family"] = "legacy9d"
|
||||
self.default["runtime_family"] = "legacy9d"
|
||||
plan = self.plan()
|
||||
for available, installed in ((False, True), (True, False)):
|
||||
selected = self.resolve(plan, available=available, installed=installed)
|
||||
self.assertFalse(selected.allow_big)
|
||||
self.assertEqual(selected.big_id, "traintrack")
|
||||
self.assertEqual(selected.fallback_sha256, "c" * 64)
|
||||
self.assertEqual(plan.family, "legacy9d")
|
||||
|
||||
def test_bundled_big_sentinel_does_not_force_a_legacy_session(self):
|
||||
self.prefs.update(small="rdf43", big="bundled-current")
|
||||
plan = self.plan()
|
||||
self.assertEqual((plan.family, plan.small_id, plan.big_id), ("native02e", "rdf43", ""))
|
||||
|
||||
def test_cross_family_big_frozen_row_is_rejected(self):
|
||||
plan = self.plan()
|
||||
entries = json.loads(plan.entries_json)
|
||||
entries["traintrack"]["_runtime_family"] = "legacy9d"
|
||||
with self.assertRaisesRegex(self.n["ModelError"], "Big artifact does not match"):
|
||||
self.resolve(replace(plan, entries_json=json.dumps(entries)))
|
||||
|
||||
def test_native_monitoring_stock_is_separate_complete_release(self):
|
||||
doc = self.document()
|
||||
names = {"dmonitoring_model_tinygrad.pkl", "dm_warp_1928x1208_tinygrad.pkl", "dm_warp_1344x760_tinygrad.pkl"}
|
||||
doc["stock"] = {name: {"artifact_sha256": "e" * 64, "artifact_size": 23, "qualification_sha256": "f" * 64}
|
||||
for name in names}
|
||||
self.n["read_json"] = lambda _: doc
|
||||
self.assertEqual(set(self.n["native_stock"]()), names)
|
||||
del doc["stock"]["dm_warp_1344x760_tinygrad.pkl"]
|
||||
with self.assertRaisesRegex(self.n["ModelError"], "not qualified"):
|
||||
self.n["native_stock"]()
|
||||
|
||||
def test_monitoring_prepare_never_freezes_driving_preferences(self):
|
||||
ns = {"__name__": __name__, "_selection": None, "_plan": None}
|
||||
extract(MODELS / "runtime_bridge/runtime.py", ["prepare"], ns)
|
||||
hardware = types.ModuleType("openpilot.common.hardware")
|
||||
hardware.AGNOS = True
|
||||
bridge = types.ModuleType("openpilot.starpilot.models.runtime_bridge")
|
||||
bridge.native = types.SimpleNamespace(prepare=Mock(return_value={"role": "monitoring", "family": "native02e"}))
|
||||
with patch.dict("sys.modules", {"openpilot.common.hardware": hardware,
|
||||
"openpilot.starpilot.models.runtime_bridge": bridge}):
|
||||
selected = ns["prepare"]("monitoring")
|
||||
bridge.native.prepare.assert_called_once_with("monitoring", None)
|
||||
self.assertEqual(selected, {"role": "monitoring", "family": "native02e"})
|
||||
self.assertIsNone(ns["_plan"])
|
||||
|
||||
def test_cross_family_small_and_postfreeze_randomization_rejected(self):
|
||||
self.prefs["big"] = ""
|
||||
self.default["runtime_family"] = "legacy9d"
|
||||
plan = self.plan()
|
||||
bad_entries = json.loads(plan.entries_json)
|
||||
bad_entries["optional-small"]["_runtime_family"] = "native02e"
|
||||
with self.assertRaisesRegex(self.n["ModelError"], "does not match"):
|
||||
self.resolve(replace(plan, entries_json=json.dumps(bad_entries)))
|
||||
with self.assertRaisesRegex(self.n["ModelError"], "Randomize before freezing"):
|
||||
self.n["resolve_runtime"](True, plan=plan, randomize=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,22 +1,41 @@
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from openpilot.common.file_chunker import package_file
|
||||
from openpilot.starpilot.models import manager
|
||||
from openpilot.starpilot.models.catalog import BUNDLED_CURRENT, DEFAULT_SMALL, DEFAULT_SMALL_SHA256
|
||||
from openpilot.starpilot.models.catalog import BUNDLED_CURRENT, DEFAULT_SMALL
|
||||
from openpilot.starpilot.models.runtime_selection import freeze_plan, selection_family
|
||||
from openpilot.starpilot.models.tests.native_fixture import native_fixture
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def release_metadata(tmp_path, monkeypatch):
|
||||
data = b"verified fixture"
|
||||
monkeypatch.setattr(manager, "SHIPPED_MODELS", tmp_path / "shipped")
|
||||
monkeypatch.setattr(manager, "DEFAULT_SMALL_SHA256", hashlib.sha256(data).hexdigest())
|
||||
monkeypatch.setattr(manager, "DEFAULT_SMALL_SIZE", len(data))
|
||||
with native_fixture(tmp_path, data):
|
||||
yield
|
||||
|
||||
|
||||
def frozen_plan(root):
|
||||
prefs, entries = manager.preferences(root), manager.catalog(root)
|
||||
family = selection_family(prefs, entries)
|
||||
return freeze_plan(prefs, entries, manager.default_record(family, root))
|
||||
|
||||
|
||||
def test_unset_small_loads_shipped_rdf_v4_with_v15_parser(tmp_path, monkeypatch):
|
||||
artifact = tmp_path / "shipped.pkl"
|
||||
artifact.write_bytes(b"verified fixture")
|
||||
monkeypatch.setattr(manager, "shipped_default", lambda: artifact)
|
||||
selected = manager.resolve_runtime(False, root=tmp_path)
|
||||
monkeypatch.setattr(manager, "shipped_default", lambda plan=None: artifact)
|
||||
selected = manager.resolve_runtime(False, root=tmp_path, plan=frozen_plan(tmp_path))
|
||||
assert manager.preferences(tmp_path)["small"] == DEFAULT_SMALL
|
||||
assert selected.small_id == "rdf43"
|
||||
assert selected.small_version == "v15"
|
||||
assert selected.small_path == artifact
|
||||
assert selected.small_sha256 == DEFAULT_SMALL_SHA256
|
||||
assert selected.small_sha256 == manager.default_record("native02e", tmp_path)["artifact_sha256"]
|
||||
assert not selected.allow_big
|
||||
|
||||
|
||||
@@ -24,16 +43,16 @@ def test_unset_small_loads_shipped_rdf_v4_with_v15_parser(tmp_path, monkeypatch)
|
||||
def test_legacy_alias_and_missing_selected_artifact_use_rdf_only(tmp_path, monkeypatch, selection):
|
||||
manager.atomic_json(tmp_path / "preferences.json", {"small": selection})
|
||||
path = tmp_path / "rdf.pkl"
|
||||
monkeypatch.setattr(manager, "shipped_default", lambda: path)
|
||||
selected = manager.resolve_runtime(False, root=tmp_path)
|
||||
monkeypatch.setattr(manager, "shipped_default", lambda plan=None: path)
|
||||
selected = manager.resolve_runtime(False, root=tmp_path, plan=frozen_plan(tmp_path))
|
||||
assert selected.small_id == DEFAULT_SMALL and selected.small_path == path
|
||||
assert manager.preferences(tmp_path)["small"] == (DEFAULT_SMALL if selection == BUNDLED_CURRENT else selection)
|
||||
|
||||
|
||||
def test_missing_or_corrupt_only_shipped_model_refuses_startup(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(manager, "shipped_default", lambda: None)
|
||||
monkeypatch.setattr(manager, "shipped_default", lambda plan=None: None)
|
||||
with pytest.raises(manager.ModelError, match="Shipped RDFv4 is missing or corrupt"):
|
||||
manager.resolve_runtime(False, root=tmp_path)
|
||||
manager.resolve_runtime(False, root=tmp_path, plan=frozen_plan(tmp_path))
|
||||
|
||||
|
||||
def test_big_default_disabled_and_explicit_download_preserved(tmp_path):
|
||||
@@ -48,30 +67,42 @@ def test_verified_download_precedes_shipped_default(tmp_path, monkeypatch):
|
||||
path = tmp_path / "download.pkl"
|
||||
monkeypatch.setattr(manager, "catalog", lambda _: {DEFAULT_SMALL: {"artifact_sha256": "download-hash", "version": "v15"}})
|
||||
monkeypatch.setattr(manager, "verified_artifact", lambda *args: path)
|
||||
def unexpected_shipped_default():
|
||||
raise AssertionError("download replaced")
|
||||
monkeypatch.setattr(manager, "shipped_default", unexpected_shipped_default)
|
||||
selected = manager.resolve_runtime(False, root=tmp_path)
|
||||
fallback = tmp_path / "fallback.pkl"
|
||||
def verified_fallback(plan):
|
||||
assert plan is not None
|
||||
return fallback
|
||||
monkeypatch.setattr(manager, "shipped_default", verified_fallback)
|
||||
selected = manager.resolve_runtime(False, root=tmp_path, plan=frozen_plan(tmp_path))
|
||||
assert selected.small_path == path
|
||||
assert selected.small_sha256 == "download-hash"
|
||||
assert selected.fallback_path == fallback
|
||||
assert selected.small_path != selected.fallback_path
|
||||
|
||||
|
||||
@pytest.mark.parametrize("family", ["native02e", "legacy9d"])
|
||||
@pytest.mark.parametrize("damage", [None, "bytes", "size", "missing"])
|
||||
def test_shipped_parts_require_pinned_hash_and_size(tmp_path, monkeypatch, damage):
|
||||
def test_shipped_parts_require_pinned_hash_and_size(tmp_path, monkeypatch, damage, family):
|
||||
data = b"small isolated compiled artifact fixture"
|
||||
source = tmp_path / "source.pkl"
|
||||
source.write_bytes(data)
|
||||
destination = tmp_path / "rdf43_driving_tinygrad.pkl"
|
||||
destination = tmp_path / ("compat9d/rdf43_driving_tinygrad.pkl" if family == "legacy9d" else "rdf43_driving_tinygrad.pkl")
|
||||
package_file(source, destination)
|
||||
monkeypatch.setattr(manager, "SHIPPED_MODELS", tmp_path)
|
||||
monkeypatch.setattr(manager, "DEFAULT_SMALL_SHA256", hashlib.sha256(data).hexdigest())
|
||||
monkeypatch.setattr(manager, "DEFAULT_SMALL_SIZE", len(data) + (damage == "size"))
|
||||
part = tmp_path / "rdf43_driving_tinygrad.pkl.chunk01of01"
|
||||
with native_fixture(tmp_path, data) as document:
|
||||
if damage == "size":
|
||||
document["models"][0]["artifact_size"] += 1
|
||||
manager.NATIVE_CATALOG_PATH.write_text(json.dumps(document))
|
||||
record = manager.default_record(family, tmp_path)
|
||||
plan = freeze_plan({"small": "rdf43" if family == "native02e" else "gwm8223", "big": ""},
|
||||
{"gwm8223": {"_runtime_family": "legacy9d"}}, record)
|
||||
part = destination.with_name(destination.name + ".chunk01of01")
|
||||
if damage == "bytes":
|
||||
part.write_bytes(b"x" * len(data))
|
||||
elif damage == "missing":
|
||||
part.unlink()
|
||||
result = manager.shipped_default()
|
||||
result = manager.shipped_default(plan)
|
||||
assert (result is not None) == (damage is None)
|
||||
if result is not None:
|
||||
assert result.read_bytes() == data
|
||||
@@ -86,15 +117,16 @@ def test_shipped_default_is_verified_asynchronously_in_menu(tmp_path, monkeypatc
|
||||
monkeypatch.setattr(manager, "SHIPPED_MODELS", shipped)
|
||||
monkeypatch.setattr(manager, "DEFAULT_SMALL_SHA256", hashlib.sha256(data).hexdigest())
|
||||
monkeypatch.setattr(manager, "DEFAULT_SMALL_SIZE", len(data))
|
||||
owner = manager.ModelManager(root=tmp_path / "preferences", parked=lambda: True, gpu_present=lambda: False)
|
||||
try:
|
||||
owner.snapshot()
|
||||
if owner.verify_worker is not None:
|
||||
owner.verify_worker.join(2)
|
||||
row = next(row for row in owner.snapshot()["models"] if row["value"] == DEFAULT_SMALL)
|
||||
assert row["installed"] and row["selectable"] and not row["checking"]
|
||||
owner.action("active", {"profile": "small", "model": DEFAULT_SMALL})
|
||||
selected = manager.resolve_runtime(False, root=owner.root)
|
||||
assert selected.small_id == DEFAULT_SMALL and selected.small_path.read_bytes() == data
|
||||
finally:
|
||||
owner.close()
|
||||
with native_fixture(tmp_path, data):
|
||||
owner = manager.ModelManager(root=tmp_path / "preferences", parked=lambda: True, gpu_present=lambda: False)
|
||||
try:
|
||||
owner.snapshot()
|
||||
if owner.verify_worker is not None:
|
||||
owner.verify_worker.join(2)
|
||||
row = next(row for row in owner.snapshot()["models"] if row["value"] == DEFAULT_SMALL)
|
||||
assert row["installed"] and row["selectable"] and not row["checking"]
|
||||
owner.action("active", {"profile": "small", "model": DEFAULT_SMALL})
|
||||
selected = manager.resolve_runtime(False, root=owner.root, plan=frozen_plan(owner.root))
|
||||
assert selected.small_id == DEFAULT_SMALL and selected.small_path.read_bytes() == data
|
||||
finally:
|
||||
owner.close()
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"protected_inventory_sha256": "3f83520d0fdaa65f801f3314d8b6e41714090eac5fb8fcd3ed70df2e0dc539b0", "schema_version": 1}
|
||||
{"protected_inventory_sha256": "513a506b42cd5503d092aa0ae67f235b9db263f46127f1a934dff5af710c5484", "schema_version": 1}
|
||||
|
||||
@@ -57,7 +57,7 @@ def sha256_file(path: Path) -> str:
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def compile_env(gpu: bool = False) -> dict[str, str]:
|
||||
def compile_env(gpu: bool = False, *, native: bool = False) -> dict[str, str]:
|
||||
env = dict(os.environ)
|
||||
env["PYTHONPATH"] = os.pathsep.join((str(REPO_ROOT), str(REPO_ROOT / "tinygrad_repo"), env.get("PYTHONPATH", "")))
|
||||
defaults = {"DEV": "QCOM", "IMAGE": "1", "FLOAT16": "1", "NOLOCALS": "1", "JIT_BATCH_SIZE": "0", "OPENPILOT_HACKS": "1"}
|
||||
@@ -67,6 +67,8 @@ def compile_env(gpu: bool = False) -> dict[str, str]:
|
||||
for key in ("IMAGE", "NOLOCALS", "OPENPILOT_HACKS"):
|
||||
env.pop(key, None)
|
||||
env.update(DEV="USB+AMD:LLVM", WARP_DEV="QCOM", GMMU="0", TC_OPT="2", TC_MIN_GLOBALS="32")
|
||||
if native:
|
||||
env["BEAM"] = "0"
|
||||
return env
|
||||
|
||||
|
||||
@@ -92,12 +94,17 @@ def parse_args(argv=None):
|
||||
return args
|
||||
|
||||
|
||||
def bank_compiler_revision() -> str:
|
||||
def bank_compiler_revision(*, native: bool = False) -> str:
|
||||
# The global runtime can advance while the compiled bank keeps its own ABI.
|
||||
old_path = sys.path[:]
|
||||
try:
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
from openpilot.starpilot.models import catalog
|
||||
if native:
|
||||
from openpilot.starpilot.models import runtime_selection
|
||||
if Path(runtime_selection.__file__).resolve() != (REPO_ROOT / "openpilot/starpilot/models/runtime_selection.py").resolve():
|
||||
raise ValueError("Native compiler identity came from a different checkout")
|
||||
return runtime_selection.NATIVE_REVISION
|
||||
from openpilot.starpilot.models.runtime_bridge import bootstrap, runtime
|
||||
for module, relative in ((catalog, "catalog.py"), (bootstrap, "runtime_bridge/bootstrap.py"),
|
||||
(runtime, "runtime_bridge/runtime.py")):
|
||||
@@ -126,11 +133,13 @@ def main(argv=None):
|
||||
output_dir = args.output_dir.resolve()
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
output = output_dir / f"{args.model}_driving_tinygrad.pkl"
|
||||
env = compile_env(args.gpu)
|
||||
native = args.gpu or args.model == "rdf43"
|
||||
env = compile_env(args.gpu, native=native)
|
||||
with tempfile.TemporaryDirectory(prefix=f".{args.model}-compile-", dir=output_dir) as temporary:
|
||||
env["TMPDIR"] = temporary
|
||||
staged = Path(temporary) / output.name
|
||||
command = [sys.executable, "-m", "openpilot.starpilot.models.compile", "--model-type", model_type,
|
||||
compiler_module = "openpilot.starpilot.models.compile_native" if native else "openpilot.starpilot.models.compile"
|
||||
command = [sys.executable, "-m", compiler_module, "--model-type", model_type,
|
||||
"--model-size", "512x256", "--camera-resolutions", "1928x1208", "1344x760",
|
||||
"--behavior-version", args.version, "--output", str(staged), "--out-of-band",
|
||||
"--image-history-pipeline", args.image_history_pipeline, *source_args]
|
||||
@@ -139,12 +148,26 @@ def main(argv=None):
|
||||
subprocess.run(command, cwd=REPO_ROOT, env=env, check=True)
|
||||
if {path.name: sha256_file(path) for path in sources} != before:
|
||||
raise ValueError("ONNX source changed during compilation")
|
||||
revision = bank_compiler_revision()
|
||||
revision = bank_compiler_revision(native=native)
|
||||
receipt = {"model_id": args.model, "behavior_version": args.version, "model_type": model_type,
|
||||
"artifact_abi": "tinygrad_single_v1_arena", "compiler_revision": revision,
|
||||
"artifact_abi": "tinygrad_single_v2_arena" if native else "tinygrad_single_v1_arena", "compiler_revision": revision,
|
||||
"source_sha256": before, "artifact_sha256": sha256_file(staged), "artifact_size": staged.stat().st_size,
|
||||
"execution_device": env["DEV"], "warp_device": env.get("WARP_DEV", env["DEV"]),
|
||||
"image_history_pipeline": args.image_history_pipeline, "camera_resolutions": [[1928, 1208], [1344, 760]]}
|
||||
if native:
|
||||
receipt["qualification_status"] = "RAW_UNGUARDED_NOT_PUBLISHABLE"
|
||||
launch, launch_receipt = staged.with_suffix(".launch.pkl"), staged.with_suffix(".launch.json")
|
||||
companion = json.loads(launch_receipt.read_text())
|
||||
if (companion.get("format") != "tinygrad_pre_hcq_v1" or companion.get("compiler_revision") != revision or
|
||||
companion.get("artifact_abi") != receipt["artifact_abi"] or
|
||||
companion.get("native_artifact_sha256") != receipt["artifact_sha256"] or
|
||||
companion.get("launch_sha256") != sha256_file(launch)):
|
||||
raise ValueError("Native compiler companion identity mismatch")
|
||||
receipt["launch_sha256"] = companion["launch_sha256"]
|
||||
# Offline export only; inference never loads the companion. Guard
|
||||
# preparation and artifact/device qualification remain separate gates.
|
||||
launch.replace(output.with_suffix(".launch.pkl"))
|
||||
launch_receipt.replace(output.with_suffix(".launch.json"))
|
||||
staged.replace(output)
|
||||
receipt_path = output.with_suffix(".build.json")
|
||||
temporary_receipt = Path(temporary) / receipt_path.name
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Package the compiled small-model outputs using established Dom model parts."""
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
# Resolve this checkout's stdlib-only owner, even for an absolute CLI invocation
|
||||
@@ -16,9 +17,16 @@ def main():
|
||||
parser.add_argument('--source', type=Path, required=True)
|
||||
parser.add_argument('--destination', type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(root))
|
||||
from tools.laptop_device_build.qcom_artifact_contract import native_records, verified_native_asset
|
||||
native = native_records(root) if args.source.resolve() == (root / 'openpilot/selfdrive/modeld/models').resolve() else None
|
||||
for name in ('dmonitoring_model_tinygrad.pkl', 'rdf43_driving_tinygrad.pkl'):
|
||||
source = (_chunk_owner.materialize_file_chunked(args.source / name)
|
||||
if name == 'rdf43_driving_tinygrad.pkl' else args.source / name)
|
||||
if native is not None:
|
||||
source = verified_native_asset(root, name, prefer_regular=True)
|
||||
else:
|
||||
source = (_chunk_owner.materialize_file_chunked(args.source / name)
|
||||
if name == 'rdf43_driving_tinygrad.pkl' or not (args.source / name).is_file() else args.source / name)
|
||||
package_file(source, args.destination / name)
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
"""Exact source and artifact contract for laptop-only DM and warp QCOM imports."""
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import importlib
|
||||
import importlib.util
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
@@ -68,6 +71,140 @@ def compiler_tree(root: Path) -> Path:
|
||||
return legacy / 'tinygrad_repo'
|
||||
|
||||
|
||||
|
||||
NATIVE_ASSETS = ("rdf43_driving_tinygrad.pkl", "dmonitoring_model_tinygrad.pkl",
|
||||
"dm_warp_1344x760_tinygrad.pkl", "dm_warp_1928x1208_tinygrad.pkl")
|
||||
NATIVE_BUILD_SOURCES = (
|
||||
"openpilot/common/transformations/camera.py",
|
||||
"openpilot/common/transformations/model.py",
|
||||
"openpilot/system/camerad/cameras/nv12_info.py",
|
||||
"openpilot/selfdrive/modeld/models/dmonitoring_model.onnx",
|
||||
"openpilot/starpilot/models/compile_native.py",
|
||||
)
|
||||
NATIVE_REVISION = "02e698160eed1751738212144a7ea2cfb160b3f1"
|
||||
|
||||
|
||||
def _logical_source(path: Path) -> dict:
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise ValueError(f"Missing regular native build input: {path}")
|
||||
with path.open("rb") as stream:
|
||||
prefix = stream.read(1024)
|
||||
if prefix.startswith(b"version https://git-lfs.github.com/spec/v1\n"):
|
||||
match = re.fullmatch(rb"version https://git-lfs.github.com/spec/v1\noid sha256:([0-9a-f]{64})\nsize ([1-9][0-9]*)\n?", prefix)
|
||||
if match is None or len(prefix) != path.stat().st_size:
|
||||
raise ValueError(f"Invalid native build LFS pointer: {path}")
|
||||
return {"sha256": match[1].decode(), "bytes": int(match[2])}
|
||||
return {"sha256": _sha(path), "bytes": path.stat().st_size}
|
||||
|
||||
|
||||
def _native_dm_geometry(root: Path, width: int, height: int) -> dict:
|
||||
path = root / "openpilot/system/camerad/cameras/nv12_info.py"
|
||||
spec = importlib.util.spec_from_file_location("_native_build_nv12", path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise ValueError("Missing native NV12 source loader")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
tree = ast.parse((root / "openpilot/common/transformations/model.py").read_text())
|
||||
sizes = [ast.literal_eval(node.value) for node in tree.body if isinstance(node, ast.Assign) and
|
||||
any(isinstance(target, ast.Name) and target.id == "DM_INPUT_SIZE" for target in node.targets)]
|
||||
if len(sizes) != 1:
|
||||
raise ValueError("Missing native DM target geometry")
|
||||
return {"camera": [width, height], "nv12": list(module.get_nv12_info(width, height)),
|
||||
"warp_to": list(sizes[0]), "layout": "luma", "border_fill": 16, "transform_device": "NPY"}
|
||||
|
||||
|
||||
def native_records(root: Path, *, verify_runtime: bool = False) -> dict | None:
|
||||
"""Read sealed native defaults separately from the unchanged legacy compiler package."""
|
||||
root = root.resolve()
|
||||
bridge = root / "openpilot/starpilot/models/runtime_bridge"
|
||||
source, catalog = bridge / "native.py", bridge.parent / "catalog-native02e.json"
|
||||
if not source.exists() and not catalog.exists():
|
||||
return None
|
||||
for path in (source, catalog, bridge / "native-policy.json"):
|
||||
if path.is_symlink() or not path.is_file() or path.stat().st_size > 2 * 1024**2:
|
||||
raise ValueError("Missing or invalid sealed native build contract")
|
||||
nodes = ast.parse(source.read_text()).body
|
||||
pins = [ast.literal_eval(node.value) for node in nodes if isinstance(node, ast.Assign) and
|
||||
any(isinstance(target, ast.Name) and target.id == "POLICY_SHA256" for target in node.targets)]
|
||||
if len(pins) != 1 or not _valid_sha(pins[0]) or _sha(bridge / "native-policy.json") != pins[0]:
|
||||
raise ValueError("Native policy is not release-qualified")
|
||||
policy = json.loads((bridge / "native-policy.json").read_text(), object_pairs_hook=_unique_pairs)
|
||||
document = json.loads(catalog.read_text(), object_pairs_hook=_unique_pairs)
|
||||
if (policy.get("compiler_revision") != NATIVE_REVISION or policy.get("catalog_sha256") != _sha(catalog) or
|
||||
document.get("compiler_revision") != NATIVE_REVISION or document.get("generation") != "native02e-v1" or
|
||||
document.get("artifact_abi") != "tinygrad_single_v2_arena"):
|
||||
raise ValueError("Native build runtime/catalog identity changed")
|
||||
build_sources = policy.get("build_sources")
|
||||
if not isinstance(build_sources, dict) or set(build_sources) != set(NATIVE_BUILD_SOURCES):
|
||||
raise ValueError("Native release lacks exact build source qualification")
|
||||
for name, expected in build_sources.items():
|
||||
if _logical_source(root / name) != expected:
|
||||
raise ValueError("Native build source changed; requalification required: " + name)
|
||||
rows = document.get("models")
|
||||
if not isinstance(rows, list):
|
||||
raise ValueError("Invalid native model catalog")
|
||||
rdf = [row for row in rows if isinstance(row, dict) and row.get("id") == "rdf43"]
|
||||
stock = document.get("stock")
|
||||
if len(rdf) != 1 or not isinstance(stock, dict) or set(stock) != set(NATIVE_ASSETS) - {NATIVE_ASSETS[0]}:
|
||||
raise ValueError("Native bundled model set changed")
|
||||
records = {NATIVE_ASSETS[0]: rdf[0], **stock}
|
||||
for name, row in records.items():
|
||||
if (not isinstance(row, dict) or not _valid_sha(row.get("artifact_sha256")) or
|
||||
type(row.get("artifact_size")) is not int or not 0 < row["artifact_size"] <= 2 * 1024**3 or
|
||||
not _valid_sha(row.get("qualification_sha256"))):
|
||||
raise ValueError("Invalid qualified native artifact: " + name)
|
||||
if name in NATIVE_ASSETS[:2]:
|
||||
if (not _valid_sha(row.get("source_sha256")) or type(row.get("source_bytes")) is not int or
|
||||
not 0 < row["source_bytes"] <= 2 * 1024**3):
|
||||
raise ValueError("Missing native ONNX source identity: " + name)
|
||||
else:
|
||||
width, height = map(int, name.removeprefix("dm_warp_").removesuffix("_tinygrad.pkl").split("x"))
|
||||
if row.get("source_geometry") != _native_dm_geometry(root, width, height):
|
||||
raise ValueError("Native DM warp geometry changed")
|
||||
dm = records[NATIVE_ASSETS[1]]
|
||||
if {"sha256": dm["source_sha256"], "bytes": dm["source_bytes"]} != build_sources[NATIVE_BUILD_SOURCES[3]]:
|
||||
raise ValueError("Native DM ONNX identity changed")
|
||||
if verify_runtime:
|
||||
sys.path.insert(0, str(root))
|
||||
native = importlib.import_module("openpilot.starpilot.models.runtime_bridge.native")
|
||||
if Path(native.__file__).resolve() != source.resolve() or native.ROOT.resolve() != root:
|
||||
raise ValueError("Native build runtime import escaped this checkout")
|
||||
if native._verify_policy() != policy:
|
||||
raise ValueError("Native runtime/vendor/guard contract changed")
|
||||
return records
|
||||
|
||||
|
||||
def verified_native_asset(root: Path, name: str, *, prefer_regular: bool = False) -> Path:
|
||||
"""Authenticate shipped assets; never regenerate them from legacy recipes."""
|
||||
records = native_records(root)
|
||||
if records is None or name not in records:
|
||||
raise ValueError("Unknown native bundled artifact")
|
||||
from openpilot.common.file_chunker import materialize_file_chunked
|
||||
row, path = records[name], root / MODEL_DIR / name
|
||||
if prefer_regular and path.is_file() and not path.is_symlink() and _sha(path) == row["artifact_sha256"]:
|
||||
result = path
|
||||
else:
|
||||
result = materialize_file_chunked(path, expected_sha256=row["artifact_sha256"])
|
||||
if result.is_symlink() or result.stat().st_size != row["artifact_size"] or _sha(result) != row["artifact_sha256"]:
|
||||
raise ValueError("Native bundled artifact changed: " + name)
|
||||
return result
|
||||
|
||||
|
||||
def verify_native_defaults(root: Path, *, prefer_regular: bool = False) -> dict:
|
||||
records = native_records(root, verify_runtime=True)
|
||||
if records is None:
|
||||
raise ValueError("Native bundle is not selected")
|
||||
for name in NATIVE_ASSETS:
|
||||
verified_native_asset(root, name, prefer_regular=prefer_regular)
|
||||
# Optional legacy models need their separately retained legacy RDF fallback.
|
||||
from openpilot.common.file_chunker import materialize_file_chunked
|
||||
from openpilot.starpilot.models.catalog import DEFAULT_SMALL_SHA256, DEFAULT_SMALL_SIZE
|
||||
compat = materialize_file_chunked(root / MODEL_DIR / "compat9d/rdf43_driving_tinygrad.pkl", DEFAULT_SMALL_SHA256)
|
||||
if compat.is_symlink() or compat.stat().st_size != DEFAULT_SMALL_SIZE:
|
||||
raise ValueError("Retained legacy RDF fallback changed")
|
||||
return records
|
||||
|
||||
|
||||
def commands(root: Path) -> dict[str, str]:
|
||||
sys.path.insert(0, str(root))
|
||||
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
|
||||
@@ -106,7 +243,8 @@ def _sources(root: Path) -> dict[str, str]:
|
||||
# Logical compiler paths retain the original package provenance after relocation.
|
||||
return ('tinygrad_repo/' + path.relative_to(compiler).as_posix() if path.is_relative_to(compiler)
|
||||
else path.relative_to(root).as_posix())
|
||||
return {source_name(path): _sha(path) for path in sorted(paths)}
|
||||
return {source_name(path): (_logical_source(path)["sha256"] if path.suffix == ".onnx" else _sha(path))
|
||||
for path in sorted(paths)}
|
||||
|
||||
|
||||
def _tokens(command: str, root: Path) -> list[str]:
|
||||
|
||||
@@ -209,9 +209,13 @@ def validate(root: Path) -> None:
|
||||
raise ValueError(f"camera warp set differs from current camera configs: {sorted(actual_warps)}")
|
||||
for name in (*MODELS, *WARPS):
|
||||
require_qcom_pickle(model_dir / name)
|
||||
shipped = materialize_file_chunked(model_dir / f"{DEFAULT_SMALL}_driving_tinygrad.pkl", DEFAULT_SMALL_SHA256)
|
||||
if shipped.stat().st_size != DEFAULT_SMALL_SIZE:
|
||||
raise ValueError("Shipped default model size differs from the published artifact")
|
||||
from tools.laptop_device_build.qcom_artifact_contract import native_records, verify_native_defaults
|
||||
if native_records(root) is not None:
|
||||
verify_native_defaults(root)
|
||||
else:
|
||||
shipped = materialize_file_chunked(model_dir / f"{DEFAULT_SMALL}_driving_tinygrad.pkl", DEFAULT_SMALL_SHA256)
|
||||
if shipped.stat().st_size != DEFAULT_SMALL_SIZE:
|
||||
raise ValueError("Shipped default model size differs from the published artifact")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
||||
Reference in New Issue
Block a user