IQ.Pilot Release Commit @ f2a861c

This commit is contained in:
IQ.Lvbs CI [bot]
2026-09-02 15:07:09 -05:00
parent b42569dbca
commit e8748fd704
5497 changed files with 316070 additions and 179848 deletions

View File

@@ -1,3 +1,3 @@
"""
IQ model selection and runner support that is actively used by iqmodeld.
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
"""

View File

@@ -8,7 +8,7 @@ import os
import re
from pathlib import Path
from openpilot.system.hardware.hw import Paths
from iqpilot.system.hardware.hw import Paths
_MODEL_ROOT = Path(Paths.model_root())

View File

@@ -1,12 +1,9 @@
#!/usr/bin/env python3
"""
Copyright (c) IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
Public entry point for the model-manifest fetcher: prefers the compiled private
bundle, falling back to the in-tree source. The default-runner fallback lives in
ManifestDecoder now, so no post-import patching is needed.
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
"""
from openpilot.iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module
from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module
try:
load_private_module(__name__, "iqpilot_private.models.fetcher")

View File

@@ -1,10 +0,0 @@
#!/usr/bin/env python3
"""
Copyright (c) IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""
from openpilot.iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module
try:
load_private_module(__name__, "iqpilot_private.models.git_auth")
except ProprietaryModuleMissing:
from iqpilot.models_private_src.git_auth import * # noqa: F403

View File

@@ -7,11 +7,11 @@ import os
import shutil
from pathlib import Path
from cereal import custom
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module
from openpilot.system.hardware.hw import Paths
from iqpilot.cereal import custom
from iqpilot.common.params import Params
from iqpilot.common.swaglog import cloudlog
from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module
from iqpilot.system.hardware.hw import Paths
try:
load_private_module(__name__, "iqpilot_private.models.helpers")
@@ -29,6 +29,7 @@ _ACTIVE_BUNDLE_KEY = "ModelManager_ActiveBundle"
_MODELS_CACHE_KEY = "ModelManager_ModelsCache"
_RUNNER_CACHE_KEY = "ModelRunnerTypeCache"
_DOWNLOAD_INDEX_KEY = "ModelManager_DownloadIndex"
_PENDING_INDEX_KEY = "ModelManager_PendingIndex"
_PENDING_MODEL_RESTORE_FILE = "/data/k3_pending_model_restore"
_STOCK_RUNNER = int(Runner.stock)
_TINYGRAD_RUNNER = int(Runner.tinygrad)
@@ -40,7 +41,6 @@ _DEFAULT_BUNDLE_REF = "default"
def get_default_model_bundle(_bundles):
"""Legacy compatibility hook: stock default is preinstalled, not a manifest bundle."""
return None
@@ -85,7 +85,7 @@ def _load_cached_manifest_bundles(params: Params):
continue
if "short_name" in raw_bundle:
from openpilot.iqpilot.selfdrive.iqmodeld.models.fetcher import ManifestDecoder
from iqpilot.selfdrive.iqmodeld.models.fetcher import ManifestDecoder
bundles.append(ManifestDecoder._decode_bundle(raw_bundle))
continue
@@ -227,6 +227,7 @@ def select_default_model(params: Params = None) -> None:
bundle_dict = _load_default_bundle_dict()
ensure_default_model_files(bundle_dict)
params.remove(_DOWNLOAD_INDEX_KEY)
params.remove(_PENDING_INDEX_KEY)
params.put(_ACTIVE_BUNDLE_KEY, bundle_dict)
params.remove(_RUNNER_CACHE_KEY)
params.put(_RUNNER_CACHE_KEY, _TINYGRAD_RUNNER)
@@ -239,10 +240,13 @@ def select_default_model(params: Params = None) -> None:
def seed_default_bundle_if_unset(params: Params = None) -> None:
params = Params() if params is None else params
if params.get(_ACTIVE_BUNDLE_KEY) or params.get(_DOWNLOAD_INDEX_KEY) is not None:
if params.get(_ACTIVE_BUNDLE_KEY):
return
queued_download = params.get(_DOWNLOAD_INDEX_KEY)
try:
select_default_model(params)
if queued_download is not None:
params.put(_DOWNLOAD_INDEX_KEY, queued_download)
cloudlog.warning("default_model: seeded Default (CD210) as active bundle")
except Exception as e:
cloudlog.exception(f"default_model: failed to seed default bundle: {e}")

View File

@@ -1,11 +1,7 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
Common base for the per-process inference/runtime states. It seeds the lateral
steer delay from the cached learned value so every subclass starts with a usable
number before its first liveDelay message arrives.
"""
from openpilot.iqpilot.common.steer_delay import cached_steer_delay
from iqpilot.common.steer_delay import cached_steer_delay
class InferenceStateBase:

View File

@@ -1,391 +0,0 @@
#!/usr/bin/env python3
"""
Copyright (c) IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""
import asyncio
import hashlib
import os
import time
from pathlib import Path
import aiohttp
from cereal import custom
from openpilot.common.realtime import Ratekeeper
from openpilot.common.time_helpers import system_time_valid
from openpilot.iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module
from openpilot.common.swaglog import cloudlog
from openpilot.system.hardware.hw import Paths
_TIME_SYNC_WAIT_TIMEOUT_S = 30.0
_TIME_SYNC_POLL_S = 0.5
def _wait_for_valid_clock(timeout: float = _TIME_SYNC_WAIT_TIMEOUT_S) -> None:
if system_time_valid():
return
cloudlog.warning("models_manager: system clock not yet valid, waiting for NTP before fetching")
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if system_time_valid():
cloudlog.warning("models_manager: system clock is now valid, resuming")
return
time.sleep(_TIME_SYNC_POLL_S)
cloudlog.warning("models_manager: gave up waiting for a valid clock, proceeding anyway")
try:
load_private_module(__name__, "iqpilot_private.models.manager")
_BaseIQModelManager = IQModelManager # noqa: F821
except ProprietaryModuleMissing:
from iqpilot.models_private_src.manager import IQModelManager as _BaseIQModelManager
from openpilot.iqpilot.selfdrive.iqmodeld.models.git_auth import get_aiohttp_auth
from openpilot.iqpilot.selfdrive.iqmodeld.models.helpers import (
bundle_files_ready,
get_active_bundle,
get_runtime_bundle_upgrade,
is_default_bundle,
persist_active_bundle,
)
_ACTIVE_BUNDLE_KEY = "ModelManager_ActiveBundle"
_DOWNLOAD_INDEX_KEY = "ModelManager_DownloadIndex"
_RUNNER_CACHE_KEY = "ModelRunnerTypeCache"
class IQModelManager(_BaseIQModelManager):
def __init__(self):
super().__init__()
self._validated_active_key: tuple[tuple[str, str], ...] | None = None
self._manifest_refresh_key: tuple[tuple[str, str], ...] | None = None
@staticmethod
def _bundle_index(bundle) -> int | None:
try:
return int(getattr(bundle, "index", -1))
except (TypeError, ValueError):
return None
@staticmethod
def _bundle_files(bundle) -> list[tuple[str, str]]:
files = []
for model in getattr(bundle, "models", []) or []:
for artifact in (getattr(model, "metadata", None), getattr(model, "artifact", None)):
filename = getattr(artifact, "fileName", "") if artifact is not None else ""
if not filename:
continue
download_uri = getattr(artifact, "downloadUri", None)
sha256 = getattr(download_uri, "sha256", "") if download_uri is not None else ""
files.append((filename, sha256 or ""))
return files
@staticmethod
def _safe_model_path(filename: str) -> Path | None:
if not filename or os.path.basename(filename) != filename:
cloudlog.warning(f"Ignoring unsafe model filename {filename!r}")
return None
root = Path(Paths.model_root()).resolve()
path = (root / filename).resolve()
try:
path.relative_to(root)
except ValueError:
cloudlog.warning(f"Ignoring model path outside model root {path}")
return None
return path
@staticmethod
def _verify_file_sync(path: Path, expected_hash: str) -> bool:
if not path.is_file():
return False
if not expected_hash:
return True
sha256_hash = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1024 * 1024), b""):
sha256_hash.update(chunk)
return sha256_hash.hexdigest().lower() == expected_hash.lower()
def _bundle_validation_key(self, bundle) -> tuple[tuple[str, str], ...]:
return tuple(self._bundle_files(bundle))
def _bundle_files_valid(self, bundle) -> bool:
for filename, expected_hash in self._bundle_files(bundle):
path = self._safe_model_path(filename)
if path is None or not self._verify_file_sync(path, expected_hash):
return False
return True
def _remove_bundle_files(self, bundle) -> None:
for filename, _expected_hash in self._bundle_files(bundle):
path = self._safe_model_path(filename)
if path is None:
continue
for candidate in (path, Path(f"{path}.download")):
try:
if candidate.is_file():
candidate.unlink()
except OSError as e:
cloudlog.exception(f"Failed to remove model artifact {candidate}: {e}")
def _find_available_bundle(self, target):
target_index = self._bundle_index(target)
target_ref = getattr(target, "ref", None)
target_internal = getattr(target, "internalName", None)
target_display = getattr(target, "displayName", None)
for bundle in self.available_models:
if target_index is not None and self._bundle_index(bundle) == target_index:
return bundle
if target_ref and getattr(bundle, "ref", None) == target_ref:
return bundle
if target_internal and getattr(bundle, "internalName", None) == target_internal:
return bundle
if target_display and getattr(bundle, "displayName", None) == target_display:
return bundle
return None
def _bundle_matches(self, left, right) -> bool:
if left is None or right is None:
return False
left_index = self._bundle_index(left)
right_index = self._bundle_index(right)
if left_index is not None and right_index is not None and left_index == right_index:
return True
for attr in ("ref", "internalName", "displayName"):
left_value = getattr(left, attr, None)
if left_value and left_value == getattr(right, attr, None):
return True
return False
def _clear_active_bundle(self) -> None:
self.params.remove(_ACTIVE_BUNDLE_KEY)
self.params.remove(_RUNNER_CACHE_KEY)
self.active_bundle = None
self._validated_active_key = None
def _download_request_matches(self, bundle) -> bool:
bundle_index = self._bundle_index(bundle)
return bundle_index is not None and self._download_index() == bundle_index
def _queue_active_redownload_if_invalid(self) -> None:
if self.active_bundle is None:
self._validated_active_key = None
return
validation_key = self._bundle_validation_key(self.active_bundle)
if validation_key == self._validated_active_key:
return
if self._bundle_files_valid(self.active_bundle):
self._validated_active_key = validation_key
return
bundle = self._find_available_bundle(self.active_bundle) or self.active_bundle
bundle_index = self._bundle_index(bundle)
cloudlog.warning(f"Active model {_display_bundle_name(self.active_bundle)} is missing or corrupt; queueing redownload")
self._remove_bundle_files(bundle)
self._clear_active_bundle()
if bundle_index is not None and self._download_index() is None:
self.params.put(_DOWNLOAD_INDEX_KEY, bundle_index)
def _find_manifest_counterpart(self, target):
# never match by index: indexes shift between manifest generations, and a
# positional match could redownload a different model than the user selected
for attr in ("ref", "internalName", "displayName"):
value = getattr(target, attr, None)
if not value:
continue
for bundle in self.available_models:
if getattr(bundle, attr, None) == value:
return bundle
return None
def _queue_active_manifest_refresh(self) -> None:
active = self.active_bundle
if active is None or is_default_bundle(active):
return
if self._download_index() is not None:
return
counterpart = self._find_manifest_counterpart(active)
if counterpart is None:
return
counterpart_index = self._bundle_index(counterpart)
if counterpart_index is None:
return
active_files = dict(self._bundle_files(active))
stale = False
for filename, sha in self._bundle_files(counterpart):
if not sha:
continue
active_sha = active_files.get(filename)
# an empty recorded hash can't prove a mismatch, so it never triggers a redownload
if active_sha is None or (active_sha and active_sha.lower() != sha.lower()):
stale = True
break
if not stale:
self._manifest_refresh_key = None
return
# the manifest may be an expired offline cache, so keep the active bundle and its
# files in place: the download flow replaces artifacts atomically and only persists
# the counterpart as active once everything landed. One attempt per bundle per run
# so a dead network doesn't turn the 1Hz loop into a download-retry storm.
key = self._bundle_validation_key(active)
if key == self._manifest_refresh_key:
return
self._manifest_refresh_key = key
cloudlog.warning(f"Active model {_display_bundle_name(active)} artifacts are stale vs current manifest; queueing redownload")
self.params.put(_DOWNLOAD_INDEX_KEY, counterpart_index)
async def _download_file(self, url: str, path: str, model) -> None:
temp_path = f"{path}.download"
self._download_start_times[model.fileName] = time.monotonic()
try:
if os.path.exists(temp_path):
os.remove(temp_path)
async with aiohttp.ClientSession(auth=get_aiohttp_auth()) as session:
async with session.get(url) as response:
response.raise_for_status()
total_size = int(response.headers.get("content-length", 0))
bytes_downloaded = 0
with open(temp_path, "wb") as f:
async for chunk in response.content.iter_chunked(self._chunk_size):
f.write(chunk)
bytes_downloaded += len(chunk)
if self._download_index() is None:
raise Exception("Download cancelled")
if total_size > 0:
progress = (bytes_downloaded / total_size) * 100
model.downloadProgress.status = custom.IQModelManager.DownloadStatus.downloading
model.downloadProgress.progress = progress
model.downloadProgress.eta = self._calculate_eta(model.fileName, progress)
self._report_status()
f.flush()
os.fsync(f.fileno())
os.replace(temp_path, path)
except Exception:
if os.path.exists(temp_path):
os.remove(temp_path)
raise
finally:
self._download_start_times.pop(model.fileName, None)
async def _download_bundle(self, model_bundle: custom.IQModelManager.ModelBundle, destination_path: str) -> None:
self.selected_bundle = model_bundle
self.selected_bundle.status = custom.IQModelManager.DownloadStatus.downloading
os.makedirs(destination_path, exist_ok=True)
try:
if not self._download_request_matches(model_bundle):
raise RuntimeError("Download cancelled")
tasks = [self._process_model(model, destination_path) for model in self.selected_bundle.models]
await asyncio.gather(*tasks)
if not self._download_request_matches(model_bundle):
raise RuntimeError("Download cancelled")
self.active_bundle = self.selected_bundle
self.active_bundle.status = custom.IQModelManager.DownloadStatus.downloaded
self.params.put(_ACTIVE_BUNDLE_KEY, self.active_bundle.to_dict())
self.params.remove(_RUNNER_CACHE_KEY)
self.selected_bundle = None
except Exception:
if self._download_request_matches(model_bundle) and self.selected_bundle is not None:
self.selected_bundle.status = custom.IQModelManager.DownloadStatus.failed
else:
self.selected_bundle = None
raise
finally:
self._report_status()
def download(self, model_bundle: custom.IQModelManager.ModelBundle, destination_path: str) -> None:
asyncio.run(self._download_bundle(model_bundle, destination_path))
def _queue_tinygrad_upgrade(self) -> None:
if self.active_bundle is None:
return
replacement = get_runtime_bundle_upgrade(self.active_bundle, self.params, self.available_models)
if replacement is None or replacement is self.active_bundle:
return
if bundle_files_ready(replacement):
persist_active_bundle(self.params, replacement)
self.active_bundle = replacement
return
if self._download_index() is None and getattr(replacement, "index", None) is not None:
self.params.put("ModelManager_DownloadIndex", int(replacement.index))
cloudlog.warning(f"Queued tinygrad upgrade for retired bundle {getattr(self.active_bundle, 'internalName', '<unknown>')}")
def main_thread(self) -> None:
_wait_for_valid_clock()
rk = Ratekeeper(1, print_delay_threshold=None)
while True:
try:
# before NTP the TLS cert reads "not yet valid" and every fetch SSL-fails; one line, not spam
if not system_time_valid():
if not getattr(self, "_ntp_wait_logged", False):
cloudlog.warning("models_manager: waiting for NTP before fetching (system clock not valid)")
self._ntp_wait_logged = True
rk.keep_time()
continue
self._ntp_wait_logged = False
self.available_models = self.model_fetcher.get_available_bundles()
self.active_bundle = get_active_bundle(self.params)
self._queue_active_redownload_if_invalid()
self._queue_tinygrad_upgrade()
self._queue_active_manifest_refresh()
if (index_to_download := self._download_index()) is not None:
if model_to_download := next((model for model in self.available_models if model.index == index_to_download), None):
try:
self.download(model_to_download, Paths.model_root())
except Exception as e:
cloudlog.exception(e)
finally:
self.params.remove("ModelManager_DownloadIndex")
self.selected_bundle = None
if self.params.get("ModelManager_ClearCache"):
self.clear_model_cache()
self.params.remove("ModelManager_ClearCache")
self._report_status()
rk.keep_time()
except Exception as e:
cloudlog.exception(f"Error in main thread: {str(e)}")
rk.keep_time()
def _display_bundle_name(bundle) -> str:
return getattr(bundle, "internalName", None) or getattr(bundle, "displayName", None) or "<unknown>"
def main():
IQModelManager().main_thread()
if __name__ == "__main__":
main()

View File

@@ -1,3 +1,3 @@
"""
Runner interfaces used by iqmodeld model execution.
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
"""

View File

@@ -7,20 +7,21 @@ from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any
import numpy as np
from cereal import custom
from openpilot.system.hardware import TICI
from openpilot.system.hardware.hw import Paths as _hw_paths
from openpilot.iqpilot.selfdrive.iqmodeld.models.helpers import get_active_bundle as _fetch_bundle
from openpilot.iqpilot.selfdrive.iqmodeld.models.combined_artifact import has_combined_split_artifact
from iqpilot.cereal import custom
from iqpilot.common.swaglog import cloudlog
from iqpilot.system.hardware import TICI
from iqpilot.system.hardware.hw import Paths as _hw_paths
from iqpilot.selfdrive.iqmodeld.models.helpers import get_active_bundle as _fetch_bundle
from iqpilot.selfdrive.iqmodeld.models.combined_artifact import has_combined_split_artifact
# ---- runtime type surface (native OpenCL/frame handles resolve to Any off-device) ----
if TYPE_CHECKING:
from openpilot.iqpilot.selfdrive.iqmodeld.native.iqmodel_pyx import GpuMemorySlot, RoadProjector
from iqpilot.selfdrive.iqmodeld.native.iqmodel_pyx import GpuMemorySlot, RoadProjector
else:
def _resolve_native_types() -> tuple[Any, Any]:
try:
from openpilot.iqpilot.selfdrive.iqmodeld.native.iqmodel_pyx import GpuMemorySlot as iq_clmem
from openpilot.iqpilot.selfdrive.iqmodeld.native.iqmodel_pyx import RoadProjector as iq_frame
from iqpilot.selfdrive.iqmodeld.native.iqmodel_pyx import GpuMemorySlot as iq_clmem
from iqpilot.selfdrive.iqmodeld.native.iqmodel_pyx import RoadProjector as iq_frame
return iq_clmem, iq_frame
except (ModuleNotFoundError, ImportError):
return Any, Any
@@ -59,11 +60,25 @@ def _configure_accelerator():
_configure_accelerator()
# real metadata pkls are a few KB; anything bigger is a model artifact wrongly
# referenced as metadata (pre-fix manifests self-referenced the artifact), and
# unpickling it here double-loads the model onto the GPU
_META_MAX_BYTES = 1 << 20
def load_artifact_metadata(metadata_filename):
"""Read one artifact's metadata pkl: (input shapes, output slices)."""
with open(os.path.join(CUSTOM_MODEL_PATH, metadata_filename), 'rb') as fh:
blob = _pk.load(fh)
return tuple(blob.get(field, {}) for field in _META_FIELDS)
try:
path = os.path.join(CUSTOM_MODEL_PATH, metadata_filename)
if os.path.getsize(path) > _META_MAX_BYTES:
cloudlog.error(f"metadata pkl {metadata_filename} is artifact-sized, refusing to unpickle it")
return tuple({} for _ in _META_FIELDS)
with open(path, 'rb') as fh:
blob = _pk.load(fh)
return tuple(blob.get(field, {}) for field in _META_FIELDS)
except Exception:
cloudlog.exception(f"unreadable metadata pkl {metadata_filename}, continuing without it")
return tuple({} for _ in _META_FIELDS)
@dataclass
@@ -114,7 +129,7 @@ class ModelRunner(RunnerRoot):
if not active:
raise ValueError("runner started without an active model bundle")
self.models = {spec.type.raw: ArtifactSpec(spec) for spec in active.models}
self.models = {spec.type.raw: ArtifactSpec(spec) for spec in _qcom_models(active)}
self.is_20hz_3d = False
self.is_20hz = active.is20hz
self.inputs = {}
@@ -165,8 +180,15 @@ class ModelRunner(RunnerRoot):
# ---- runner selection (which backend to build for the active bundle) ----------
def _qcom_models(bundle) -> list:
# usbeMac artifacts ride along in a bundle for the eGPU host; they are never
# loaded on QCOM and must not affect runner classification
return [m for m in bundle.models if m.type.raw != ModelType.usbeMac]
def _single_artifact_prefix(bundle, prefix: str) -> bool:
return len(bundle.models) == 1 and bundle.models[0].artifact.fileName.startswith(prefix)
models = _qcom_models(bundle)
return len(models) == 1 and models[0].artifact.fileName.startswith(prefix)
def _is_fused_bundle(bundle) -> bool:
@@ -178,7 +200,7 @@ def _is_supercombo_bundle(bundle) -> bool:
def _is_split_bundle(bundle) -> bool:
present = {m.type.raw for m in bundle.models}
present = {m.type.raw for m in _qcom_models(bundle)}
split_kinds = {ModelType.vision, ModelType.policy, ModelType.offPolicy, ModelType.onPolicy}
return not present.isdisjoint(split_kinds)
@@ -187,21 +209,23 @@ def get_model_runner() -> "ModelRunner":
"""Build the runner backend that fits the active bundle (supercombo / fused /
combined-split / split / single). Concrete runners are imported lazily so one
backend failing to load can't take down the others at import time."""
from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.tinygrad_runner import (TinygradRunner,
from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.tinygrad_runner import (TinygradRunner,
TinygradSplitRunner)
bundle = _fetch_bundle()
if not (bundle and bundle.models):
# an eMac-only bundle (no QCOM-loadable models) runs the stock default on
# device; the big host serves the bundle's precompiled artifact
if not (bundle and bundle.models and _qcom_models(bundle)):
return TinygradRunner(ModelType.supercombo)
if _is_supercombo_bundle(bundle):
from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.supercombo_runner import TinygradSupercomboRunner
from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.supercombo_runner import TinygradSupercomboRunner
return TinygradSupercomboRunner()
if _is_fused_bundle(bundle):
from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.fused_runner import TinygradFusedRunner
from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.fused_runner import TinygradFusedRunner
return TinygradFusedRunner()
if _is_split_bundle(bundle) and has_combined_split_artifact(bundle):
from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.combined_split_runner import TinygradCombinedSplitRunner
from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.combined_split_runner import TinygradCombinedSplitRunner
return TinygradCombinedSplitRunner()
if _is_split_bundle(bundle):
return TinygradSplitRunner()
return TinygradRunner(bundle.models[0].type.raw)
return TinygradRunner(_qcom_models(bundle)[0].type.raw)

View File

@@ -1,3 +0,0 @@
"""
ONNX runner support for iqmodeld.
"""

View File

@@ -1,57 +0,0 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
"""
from __future__ import annotations
import numpy as np
from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.model_runner import CLMemDict, FrameDict, ModelType, NumpyDict, ShapeDict
from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelRunner
from openpilot.iqpilot.selfdrive.iqmodeld import MODEL_PATH
from openpilot.iqpilot.selfdrive.iqmodeld.config import ModelConstants
from openpilot.iqpilot.selfdrive.iqmodeld.parser import ArchiveParser
from openpilot.iqpilot.selfdrive.iqmodeld.runtime.ort import ORT_TYPES_TO_NP_TYPES, make_onnx_cpu_runner
def _onnx_dtype_table(session) -> dict[str, np.dtype]:
return {
tensor_info.name: ORT_TYPES_TO_NP_TYPES[tensor_info.type]
for tensor_info in session.get_inputs()
}
class ONNXRunner(ModelRunner):
def __init__(self):
super().__init__()
self.runner = make_onnx_cpu_runner(MODEL_PATH)
self._constants = ModelConstants
self._model_data = self.models.get(ModelType.supercombo)
self._input_dtypes = _onnx_dtype_table(self.runner)
self._parser = ArchiveParser()
self.parser_method_dict[ModelType.supercombo] = self._parser.parse_outputs
@property
def input_shapes(self) -> ShapeDict:
return {tensor_info.name: tensor_info.shape for tensor_info in self.runner.get_inputs()}
def _frame_as_numpy(self, stream_name: str, imgs_cl: CLMemDict, frames: FrameDict) -> np.ndarray:
flattened = frames[stream_name].as_numpy(imgs_cl[stream_name])
shaped = flattened.reshape(self.input_shapes[stream_name])
return shaped.astype(self._input_dtypes[stream_name])
def prepare_inputs(self, imgs_cl: CLMemDict, numpy_inputs: NumpyDict, frames: FrameDict) -> dict:
staged_inputs = dict(numpy_inputs)
for stream_name in imgs_cl:
staged_inputs[stream_name] = self._frame_as_numpy(stream_name, imgs_cl, frames)
self.inputs = staged_inputs
return staged_inputs
def _parse_outputs(self, model_outputs: np.ndarray) -> NumpyDict:
if self._model_data is None:
raise ValueError("Model data is not available. Ensure the model is loaded correctly.")
return self.parser_method_dict[self._model_data.model.type.raw](self._slice_outputs(model_outputs))
def _run_model(self) -> NumpyDict:
combined = self.runner.run(None, self.inputs)[0].reshape(-1)
return self._parse_outputs(combined)

View File

@@ -1,3 +1,3 @@
"""
Tinygrad runner support for iqmodeld.
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
"""

View File

@@ -11,12 +11,12 @@ from typing import Any
import numpy as np
from openpilot.iqpilot.selfdrive.iqmodeld.models.combined_artifact import resolve_combined_split_artifact
from openpilot.iqpilot.selfdrive.iqmodeld.models.helpers import get_active_bundle
from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.model_runner import NumpyDict, ShapeDict, SliceDict
from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelRunner
from openpilot.iqpilot.selfdrive.iqmodeld.models.split_model_constants import SplitModelConstants
from openpilot.iqpilot.selfdrive.iqmodeld.parser import PhaseParser
from iqpilot.selfdrive.iqmodeld.models.combined_artifact import resolve_combined_split_artifact
from iqpilot.selfdrive.iqmodeld.models.helpers import get_active_bundle
from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import NumpyDict, ShapeDict, SliceDict
from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelRunner
from iqpilot.selfdrive.iqmodeld.models.split_model_constants import SplitModelConstants
from iqpilot.selfdrive.iqmodeld.parser import PhaseParser
def _tinygrad_imports():

View File

@@ -9,12 +9,12 @@ from typing import Any
import numpy as np
from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.model_runner import (
from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import (
CUSTOM_MODEL_PATH, NumpyDict, ShapeDict, SliceDict,
)
from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelRunner
from openpilot.iqpilot.selfdrive.iqmodeld.models.split_model_constants import SplitModelConstants
from openpilot.iqpilot.selfdrive.iqmodeld.parser import PhaseParser
from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelRunner
from iqpilot.selfdrive.iqmodeld.models.split_model_constants import SplitModelConstants
from iqpilot.selfdrive.iqmodeld.parser import PhaseParser
def _tinygrad_imports():
@@ -27,8 +27,6 @@ WARP_DEV = os.getenv('WARP_DEV')
class TinygradFusedRunner(ModelRunner):
"""Runs a fused warp+vision+policy pkl. Bundle ships one `driving_fused_*` artifact."""
uses_opencl_warp: bool = False
def __init__(self):
@@ -110,19 +108,30 @@ class TinygradFusedRunner(ModelRunner):
'feat_q': zeros_f32((self._frame_skip * (fb[1] - 1) + 1, fb[0], fb[2])),
'desire_q': zeros_f32((self._frame_skip * dp[1], dp[0], dp[2])),
}
# shapes must match the captured run_policy JIT inputs
on_shapes = self._on_meta['input_shapes']
captured = self._run_policy.captured
jit_shapes = {
name: tuple(int(s) for s in view.shape)
for name, (view, _vars, _dtype, _device) in zip(captured.expected_names, captured.expected_input_info)
}
def policy_input_shape(name):
shape = on_shapes.get(name, jit_shapes.get(name))
if shape is None:
raise ValueError(f"fused pkl declares no shape for policy input {name}")
return shape
self._npy_buffers = {
'desire': np.zeros(dp[2], dtype=np.float32),
'traffic_convention': np.zeros(on_shapes['traffic_convention'], dtype=np.float32),
'action_t': np.zeros(on_shapes['action_t'], dtype=np.float32),
'traffic_convention': np.zeros(policy_input_shape('traffic_convention'), dtype=np.float32),
'tfm': np.zeros((3, 3), dtype=np.float32),
'big_tfm': np.zeros((3, 3), dtype=np.float32),
}
if 'action_t' in jit_shapes:
self._npy_buffers['action_t'] = np.zeros(policy_input_shape('action_t'), dtype=np.float32)
self._cam_resolution = (cam_w, cam_h)
def run_fused(self, bufs: dict, transforms: dict[str, np.ndarray], numpy_inputs: NumpyDict) -> NumpyDict:
"""warp + vision + policy in one pass from raw NV12 bufs + transform matrices."""
Tensor, Device = _tinygrad_imports()
main_buf = bufs['img']
@@ -134,14 +143,13 @@ class TinygradFusedRunner(ModelRunner):
self._npy_buffers['desire'][:] = numpy_inputs[desire_key]
if 'traffic_convention' in numpy_inputs:
self._npy_buffers['traffic_convention'][:] = numpy_inputs['traffic_convention']
if 'action_t' in numpy_inputs:
if 'action_t' in numpy_inputs and 'action_t' in self._npy_buffers:
self._npy_buffers['action_t'][:] = numpy_inputs['action_t']
self._npy_buffers['tfm'][:] = transforms['img']
self._npy_buffers['big_tfm'][:] = transforms['big_img']
npy = lambda key: Tensor(self._npy_buffers[key], device='NPY')
# frames go on the compute device to match the captured warp JIT
frame = self._frame_tensor('img', bufs['img'])
big_frame = self._frame_tensor('big_img', bufs['big_img'])
@@ -149,12 +157,13 @@ class TinygradFusedRunner(ModelRunner):
img, big_img = warp_jit(img_q=self._queues['img_q'], big_img_q=self._queues['big_img_q'],
tfm=npy('tfm'), big_tfm=npy('big_tfm'), frame=frame, big_frame=big_frame)
vision_out_t, on_out_t, off_out_t = self._run_policy(
policy_inputs = dict(
img=img, big_img=big_img, feat_q=self._queues['feat_q'], desire_q=self._queues['desire_q'],
desire=npy('desire'), traffic_convention=npy('traffic_convention'), action_t=npy('action_t'))
desire=npy('desire'), traffic_convention=npy('traffic_convention'))
if 'action_t' in self._npy_buffers:
policy_inputs['action_t'] = npy('action_t')
vision_out_t, on_out_t, off_out_t = self._run_policy(**policy_inputs)
# parse each model's output on its own sliced dict; parsing a merged dict
# would run parse_dynamic_outputs twice and double-parse plan/lead
def _slice(tensor_out, meta) -> NumpyDict:
flat = tensor_out.numpy().flatten()
return {k: flat[np.newaxis, sl] for k, sl in meta['output_slices'].items() if k != 'pad'}

View File

@@ -9,9 +9,9 @@ from collections.abc import Callable
import numpy as np
from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelType, NumpyDict
from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.model_runner import RunnerRoot
from openpilot.iqpilot.selfdrive.iqmodeld.parser import ArchiveParser, PhaseParser
from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelType, NumpyDict
from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import RunnerRoot
from iqpilot.selfdrive.iqmodeld.parser import ArchiveParser, PhaseParser
class _ParserRole(RunnerRoot, ABC):

View File

@@ -12,11 +12,11 @@ from typing import Any
import numpy as np
from openpilot.common.params import Params
from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.model_runner import CUSTOM_MODEL_PATH, NumpyDict, ShapeDict, SliceDict
from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelRunner
from openpilot.iqpilot.selfdrive.iqmodeld.models.split_model_constants import SplitModelConstants
from openpilot.iqpilot.selfdrive.iqmodeld.parser import PhaseParser
from iqpilot.common.params import Params
from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import CUSTOM_MODEL_PATH, NumpyDict, ShapeDict, SliceDict
from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelRunner
from iqpilot.selfdrive.iqmodeld.models.split_model_constants import SplitModelConstants
from iqpilot.selfdrive.iqmodeld.parser import PhaseParser
def _tinygrad_imports():
@@ -68,8 +68,6 @@ def _is_jit_arg_mismatch(err: BaseException) -> bool:
class TinygradSupercomboRunner(ModelRunner):
"""Runs a single combined supercombo pkl. Bundle ships one `driving_supercombo_*` artifact."""
uses_opencl_warp: bool = False
def __init__(self):
@@ -282,7 +280,6 @@ class TinygradSupercomboRunner(ModelRunner):
zeros_u8 = lambda s: Tensor(np.zeros(s, dtype=np.uint8), device=Device.DEFAULT).contiguous().realize()
zeros_f32 = lambda s: Tensor(np.zeros(s, dtype=np.float32), device=Device.DEFAULT).contiguous().realize()
# packed npy block (single NPY tensor, mutated in place via views): order matches run_policy.split
shapes = {'desire': (dp[2],), 'traffic_convention': tuple(tc), 'action_t': tuple(at), 'prev_feat': (fb[0], fb[2])}
sizes = [math.prod(s) for s in shapes.values()]
packed = np.zeros(sum(sizes), dtype=np.float32)
@@ -318,7 +315,6 @@ class TinygradSupercomboRunner(ModelRunner):
self._npy['traffic_convention'][:] = numpy_inputs['traffic_convention']
if 'action_t' in numpy_inputs:
self._npy['action_t'][:] = numpy_inputs['action_t']
# self._npy['prev_feat'] holds last frame's hidden_state (zeros on the first frame)
frame = self._frame_tensor('img', bufs['img'])
big_frame = self._frame_tensor('big_img', bufs['big_img'])
@@ -334,11 +330,10 @@ class TinygradSupercomboRunner(ModelRunner):
raise
flat = out.numpy().flatten()
# feed hidden_state back as prev_feat for the next frame
self._npy['prev_feat'][:] = flat[self._hidden_slice].reshape(self._npy['prev_feat'].shape)
sliced = {k: flat[np.newaxis, sl] for k, sl in self._slices.items()}
return self._parser.parse_vision_outputs(sliced) # single-pass; parse_outputs double-parses a combined dict
return self._parser.parse_vision_outputs(sliced)
def _run_model(self) -> NumpyDict:
raise RuntimeError("supercombo path goes through run_fused(), not _run_model()")

View File

@@ -8,9 +8,10 @@ import pickle
from dataclasses import dataclass
import numpy as np
from tinygrad.dtype import dtypes
from tinygrad.tensor import Tensor
from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.model_runner import (
from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import (
CLMemDict,
CUSTOM_MODEL_PATH,
FrameDict,
@@ -19,18 +20,18 @@ from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.model_runner import (
ShapeDict,
SliceDict,
)
from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelRunner
from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.model_types import (
from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelRunner
from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.model_types import (
OffPolicyTinygrad,
OnPolicyTinygrad,
PolicyTinygrad,
SupercomboTinygrad,
VisionTinygrad,
)
from openpilot.iqpilot.selfdrive.iqmodeld.models.split_model_constants import SplitModelConstants
from openpilot.iqpilot.selfdrive.iqmodeld.config import ModelConstants
from openpilot.iqpilot.selfdrive.iqmodeld.runtime.tinygrad import qcom_tensor_from_opencl_address
from openpilot.system.hardware import TICI
from iqpilot.selfdrive.iqmodeld.models.split_model_constants import SplitModelConstants
from iqpilot.selfdrive.iqmodeld.config import ModelConstants
from iqpilot.selfdrive.iqmodeld.runtime.tinygrad import qcom_tensor_from_opencl_address
from iqpilot.system.hardware import TICI
@dataclass(frozen=True)
@@ -84,6 +85,9 @@ class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTiny
self.model_run = _load_program_blob(asset_name)
self._input_plan = _compile_input_plan(self.model_run.captured)
for name, spec in self._input_plan.items():
if "img" in name and spec.dtype is not dtypes.uint8:
raise ValueError(f"{asset_name}: image input {name} expects {spec.dtype}, incompatible with uint8 warp buffer")
self.input_to_dtype = {name: spec.dtype for name, spec in self._input_plan.items()}
self.input_to_device = {name: spec.device for name, spec in self._input_plan.items()}

View File

@@ -1,4 +1,3 @@
# openpilot model I/O constants (comma.ai, MIT — see LICENSE)
import numpy as np
@@ -7,7 +6,6 @@ def index_function(idx, max_val=192, max_idx=32):
class SplitModelConstants:
# time and distance indices
IDX_N = 33
T_IDXS = [index_function(idx, max_val=10.0) for idx in range(IDX_N)]
X_IDXS = [index_function(idx, max_val=192.0) for idx in range(IDX_N)]
@@ -15,7 +13,6 @@ class SplitModelConstants:
LEAD_T_OFFSETS = [0., 2., 4.]
META_T_IDXS = [2., 4., 6., 8., 10.]
# split-model temporal / history run parameters
MODEL_FREQ = 20
HISTORY_FREQ = 5
HISTORY_LEN_SECONDS = 5
@@ -31,7 +28,6 @@ class SplitModelConstants:
LATERAL_CONTROL_PARAMS_LEN = 2
PREV_DESIRED_CURV_LEN = 1
# model outputs constants
FCW_THRESHOLDS_5MS2 = np.array([.05, .05, .15, .15, .15], dtype=np.float32)
FCW_THRESHOLDS_3MS2 = np.array([.7, .7], dtype=np.float32)
FCW_5MS2_PROBS_WIDTH = 5
@@ -71,7 +67,6 @@ class SplitModelConstants:
POLY_PATH_DEGREE = 4
# model outputs slices
class Plan:
POSITION = slice(0, 3)
VELOCITY = slice(3, 6)
@@ -82,14 +77,12 @@ class Plan:
class Meta:
ENGAGED = slice(0, 1)
# next 2, 4, 6, 8, 10 seconds
GAS_DISENGAGE = slice(1, 31, 6)
BRAKE_DISENGAGE = slice(2, 31, 6)
STEER_OVERRIDE = slice(3, 31, 6)
HARD_BRAKE_3 = slice(4, 31, 6)
HARD_BRAKE_4 = slice(5, 31, 6)
HARD_BRAKE_5 = slice(6, 31, 6)
# next 0, 2, 4, 6, 8, 10 seconds
GAS_PRESS = slice(31, 55, 4)
BRAKE_PRESS = slice(32, 55, 4)
LEFT_BLINKER = slice(33, 55, 4)