forked from IQ.Lvbs/IQ.Pilot
IQ.Pilot Release Commit @ f2a861c
This commit is contained in:
@@ -4,7 +4,7 @@ Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed
|
||||
import glob
|
||||
import os
|
||||
|
||||
Import("env", "envCython", "arch", "cereal", "messaging", "common", "visionipc")
|
||||
Import("env", "envCython", "arch", "cereal", "messaging", "common", "visionipc", "tinygrad_dir")
|
||||
lenv = env.Clone()
|
||||
lenvCython = envCython.Clone()
|
||||
|
||||
@@ -23,9 +23,7 @@ def _inject_path_define(symbol, filename):
|
||||
|
||||
|
||||
def _tinygrad_sources():
|
||||
root = env.Dir("#tinygrad_repo").relpath
|
||||
workspace = env.Dir("#").abspath
|
||||
return ["#" + path for path in glob.glob(root + "/**", recursive=True, root_dir=workspace) if "pycache" not in path]
|
||||
return [path for path in glob.glob(tinygrad_dir + "/**", recursive=True) if "pycache" not in path]
|
||||
|
||||
|
||||
def _present_models():
|
||||
@@ -84,13 +82,12 @@ _queue_metadata_generation(present_models, tinygrad_files)
|
||||
|
||||
|
||||
def tg_compile(flags, model_name):
|
||||
pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + '"'
|
||||
fn = File(f"models/{model_name}").abspath
|
||||
return lenv.Command(
|
||||
fn + "_tinygrad.pkl",
|
||||
[fn + ".onnx"] + tinygrad_files,
|
||||
lenv.PrettyAction(
|
||||
f'${{PYWARN}} {pythonpath_string} {flags} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {fn}_tinygrad.pkl',
|
||||
f'${{PYWARN}} {flags} python3 {Dir("#iqpilot/selfdrive/iqmodeld/tools").abspath}/compile_model.py {fn}.onnx {fn}_tinygrad.pkl',
|
||||
'MODEL', logfile='${TARGET}.log')
|
||||
)
|
||||
|
||||
@@ -98,22 +95,21 @@ def tg_compile(flags, model_name):
|
||||
for model_name in present_models:
|
||||
tg_compile(_tinygrad_flags(), model_name)
|
||||
|
||||
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
|
||||
from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE
|
||||
from iqpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
|
||||
from iqpilot.common.transformations.model import MEDMODEL_INPUT_SIZE
|
||||
|
||||
FUSED_CAMERA_CONFIGS = [(_ar_ox_fisheye.width, _ar_ox_fisheye.height), (_os_fisheye.width, _os_fisheye.height)]
|
||||
FUSED_FRAME_SKIP = 4
|
||||
|
||||
|
||||
def tg_compile_fused(file_prefix, flags):
|
||||
pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + '"'
|
||||
model_dir = Dir("models").abspath
|
||||
model_w, model_h = MEDMODEL_INPUT_SIZE
|
||||
camera_args = " ".join(f"{cw}x{ch}" for cw, ch in FUSED_CAMERA_CONFIGS)
|
||||
out_pkl = File(f"models/{file_prefix}driving_fused_tinygrad.pkl").abspath
|
||||
onnx_deps = [File(f"models/{file_prefix}{model_name}.onnx") for model_name in FUSED_TRIPLET]
|
||||
cmd = (
|
||||
f'${{PYWARN}} {pythonpath_string} {flags} python3 {Dir("#iqpilot/selfdrive/iqmodeld/tools").abspath}/compile_daemon.py '
|
||||
f'${{PYWARN}} {flags} python3 {Dir("#iqpilot/selfdrive/iqmodeld/tools").abspath}/compile_daemon.py '
|
||||
f'--model-size {model_w}x{model_h} --camera-resolutions {camera_args} '
|
||||
f'--vision-onnx {model_dir}/{file_prefix}driving_vision.onnx '
|
||||
f'--off-policy-onnx {model_dir}/{file_prefix}driving_off_policy.onnx '
|
||||
|
||||
9
iqpilot/selfdrive/iqmodeld/big_catalog.py
Normal file
9
iqpilot/selfdrive/iqmodeld/big_catalog.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module
|
||||
|
||||
try:
|
||||
load_private_module(__name__, "iqpilot_private.models.big_catalog")
|
||||
except ProprietaryModuleMissing:
|
||||
from iqpilot.models_private_src.big_catalog import *
|
||||
@@ -3,7 +3,7 @@ Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
from openpilot.common.transformations.camera import DEVICE_CAMERAS
|
||||
from iqpilot.common.transformations.camera import DEVICE_CAMERAS
|
||||
|
||||
MAX_CAMERA_OFFSET_METERS = 0.35
|
||||
|
||||
@@ -31,7 +31,9 @@ def _camera_profile(sm):
|
||||
|
||||
|
||||
def _calibration_height(sm) -> float:
|
||||
return sm["liveCalibration"].height[0] if sm["liveCalibration"].height else 1.22
|
||||
from iqpilot.selfdrive.locationd.calibrationd import HEIGHT_SANE_MIN, HEIGHT_SANE_MAX
|
||||
h = sm["extrinsicsCalibration"].height[0] if sm["extrinsicsCalibration"].height else 1.22
|
||||
return h if HEIGHT_SANE_MIN <= h <= HEIGHT_SANE_MAX else 1.22
|
||||
|
||||
|
||||
def _sheared_transform(model_transform, intrinsics, height: float, lateral_offset: float):
|
||||
|
||||
@@ -6,9 +6,12 @@ from __future__ import annotations
|
||||
import numpy as np
|
||||
|
||||
|
||||
def index_function(index: int, max_val: float = 192, max_idx: int = 32) -> float:
|
||||
return max_val * ((index / max_idx) ** 2)
|
||||
|
||||
|
||||
def _quadratic_series(limit: float, steps: int) -> list[float]:
|
||||
peak_index = steps - 1
|
||||
return [limit * ((index / peak_index) ** 2) for index in range(steps)]
|
||||
return [index_function(index, max_val=limit, max_idx=steps - 1) for index in range(steps)]
|
||||
|
||||
|
||||
def _probability_window(*values: float) -> np.ndarray:
|
||||
|
||||
@@ -1,60 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import cereal.messaging as messaging
|
||||
import iqpilot.cereal.messaging as messaging
|
||||
import numpy as np
|
||||
|
||||
from cereal import car, custom, log
|
||||
from cereal.messaging import PubMaster, SubMaster
|
||||
from msgq.visionipc import VisionBuf, VisionIpcClient, VisionStreamType
|
||||
from iqpilot.cereal import car, custom, log
|
||||
from iqpilot.cereal.messaging import PubMaster, SubMaster
|
||||
from iqpilot.cereal.visionipc import VisionStreamType
|
||||
from msgq.visionipc import VisionBuf, VisionIpcClient
|
||||
from iqdbc.car.car_helpers import get_demo_car_params
|
||||
from setproctitle import setproctitle
|
||||
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.iq_perf import PerfSample, PerfTraceEmitter, PerfTraceRing
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import DT_MDL, config_realtime_process
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.common.transformations.camera import DEVICE_CAMERAS
|
||||
from openpilot.common.transformations.model import get_warp_matrix
|
||||
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import (
|
||||
from iqpilot.common.filter_simple import FirstOrderFilter
|
||||
from iqpilot.common.iq_perf import PerfSample, PerfTraceEmitter, PerfTraceRing
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.common.realtime import DT_MDL, config_realtime_process
|
||||
from iqpilot.common.swaglog import cloudlog
|
||||
from iqpilot.common.transformations.camera import DEVICE_CAMERAS
|
||||
from iqpilot.common.transformations.model import get_warp_matrix
|
||||
from iqpilot.selfdrive.controls.lib.desire_helper import DesireHelper
|
||||
from iqpilot.selfdrive.controls.lib.drive_helpers import (
|
||||
MODEL_SMOOTHING_MAX_TOTAL_SEC,
|
||||
dynamic_lat_smooth_extra_seconds,
|
||||
get_accel_from_plan,
|
||||
smooth_value,
|
||||
)
|
||||
from openpilot.selfdrive.locationd.calibration_helpers import get_calibrated_rpy
|
||||
from openpilot.system import sentry
|
||||
from iqpilot.selfdrive.locationd.calibration_helpers import get_calibrated_rpy
|
||||
from iqpilot.system import sentry
|
||||
|
||||
from openpilot.iqpilot.common.steer_delay import resolve_steer_delay
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.models.helpers import get_active_bundle
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.models.inference_state import InferenceStateBase
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.model_runner import get_model_runner
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.camera import CameraOffsetHelper
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.config import Plan
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.messaging import (
|
||||
from iqpilot.common.steer_delay import lateral_action_delay
|
||||
from iqpilot.selfdrive.iqmodeld.models.helpers import get_active_bundle
|
||||
from iqpilot.selfdrive.iqmodeld.models.inference_state import InferenceStateBase
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import get_model_runner
|
||||
from iqpilot.selfdrive.iqmodeld.camera import CameraOffsetHelper
|
||||
from iqpilot.selfdrive.iqmodeld.config import Plan
|
||||
from iqpilot.selfdrive.iqmodeld.messaging import (
|
||||
DrivePacketMemory,
|
||||
pick_curvature,
|
||||
populate_drive_messages,
|
||||
populate_odometry_message,
|
||||
)
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.metadata import select_meta_layout
|
||||
from iqpilot.selfdrive.iqmodeld.metadata import select_meta_layout
|
||||
|
||||
try:
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.native.iqmodel_pyx import RoadProjector, WarpContext
|
||||
from iqpilot.selfdrive.iqmodeld.native.iqmodel_pyx import RoadProjector, WarpContext
|
||||
except ModuleNotFoundError:
|
||||
class WarpContext:
|
||||
def __init__(self, *args, **kwargs):
|
||||
raise ModuleNotFoundError("openpilot.iqpilot.selfdrive.iqmodeld.native.iqmodel_pyx is not built")
|
||||
raise ModuleNotFoundError("iqpilot.selfdrive.iqmodeld.native.iqmodel_pyx is not built")
|
||||
|
||||
class RoadProjector:
|
||||
def __init__(self, *args, **kwargs):
|
||||
raise ModuleNotFoundError("openpilot.iqpilot.selfdrive.iqmodeld.native.iqmodel_pyx is not built")
|
||||
raise ModuleNotFoundError("iqpilot.selfdrive.iqmodeld.native.iqmodel_pyx is not built")
|
||||
|
||||
|
||||
PROCESS_NAME = "iqpilot.selfdrive.iqmodeld.daemon"
|
||||
@@ -62,6 +64,9 @@ IQP_NAV_MODEL_INFLUENCE_ENABLED = False
|
||||
TurnDirection = custom.IQTurnSignalDirection
|
||||
IQMODEL_EVAL_WARN_US = int(DT_MDL * 1_000_000)
|
||||
IQMODEL_EVAL_ERROR_US = IQMODEL_EVAL_WARN_US * 2
|
||||
_FRAME_STARVED_BACKOFF_POLLS = 5
|
||||
_FRAME_STARVED_BACKOFF_SECONDS = 0.005
|
||||
_FRAME_STARVED_LOG_EVERY = 200
|
||||
|
||||
|
||||
def _plan_y_std_1s(outputs: dict[str, np.ndarray]) -> float:
|
||||
@@ -422,12 +427,12 @@ class CalibrationAtlas:
|
||||
self._offset_tuner.set_offset(offset_value)
|
||||
|
||||
def refresh(self, sm: SubMaster, main_is_wide: bool, dual_camera: bool) -> tuple[np.ndarray, np.ndarray, bool]:
|
||||
if not (sm.seen["liveCalibration"] and sm.seen["roadCameraState"] and sm.seen["deviceState"]):
|
||||
if not (sm.seen["extrinsicsCalibration"] and sm.seen["roadCameraState"] and sm.seen["deviceState"]):
|
||||
return self.main_warp, self.extra_warp, self.ready
|
||||
|
||||
rpy = get_calibrated_rpy(sm["liveCalibration"])
|
||||
rpy = get_calibrated_rpy(sm["extrinsicsCalibration"])
|
||||
if rpy is None:
|
||||
live_calib = sm["liveCalibration"]
|
||||
live_calib = sm["extrinsicsCalibration"]
|
||||
if len(live_calib.rpyCalib) == 3:
|
||||
rpy = np.array(live_calib.rpyCalib, dtype=np.float32)
|
||||
else:
|
||||
@@ -467,7 +472,7 @@ class FrameDropMeter:
|
||||
|
||||
|
||||
class InferenceDaemon:
|
||||
def __init__(self, demo: bool = False):
|
||||
def __init__(self, demo: bool = False, channel_path: str | None = None):
|
||||
cloudlog.warning("iqmodeld init")
|
||||
sentry.set_tag("daemon", PROCESS_NAME)
|
||||
cloudlog.bind(daemon=PROCESS_NAME)
|
||||
@@ -481,11 +486,18 @@ class InferenceDaemon:
|
||||
self._meta_layout = select_meta_layout()
|
||||
cloudlog.warning("models loaded, iqmodeld starting")
|
||||
|
||||
self._channel = None
|
||||
if channel_path is not None:
|
||||
from iqpilot.selfdrive.iqmodeld.model_channel import ModelChannel
|
||||
self._channel = ModelChannel(channel_path, create=True)
|
||||
|
||||
self._cameras = CameraIngress(self._gpu)
|
||||
self._pub = PubMaster(["modelV2", "drivingModelData", "cameraOdometry", "iqDriveModelData", "iqPerfTrace"])
|
||||
pub_services = ["iqPerfTrace"] if self._channel is not None else [
|
||||
"modelV2", "drivingModelData", "cameraOdometry", "iqDriveModelData", "iqPerfTrace"]
|
||||
self._pub = PubMaster(pub_services)
|
||||
self._sub = SubMaster([
|
||||
"deviceState", "carState", "roadCameraState", "liveCalibration",
|
||||
"driverMonitoringState", "carControl", "liveDelay", "iqNavState", "radarState",
|
||||
"deviceState", "carState", "roadCameraState", "extrinsicsCalibration",
|
||||
"driverMonitoringState", "carControl", "lateralDelay", "iqNavState", "radarState",
|
||||
])
|
||||
self._message_memory = DrivePacketMemory()
|
||||
self._params = Params()
|
||||
@@ -509,7 +521,13 @@ class InferenceDaemon:
|
||||
def _refresh_tunables(self, tick: int) -> None:
|
||||
if tick % 60 != 0:
|
||||
return
|
||||
self._runtime.lat_delay = resolve_steer_delay(self._params, self._sub["liveDelay"].lateralDelay)
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected
|
||||
big_enabled = self._params.get_bool("IQEmacEnabled") or egpu_selected(self._params)
|
||||
if big_enabled != (self._channel is not None):
|
||||
# publish mode is fixed at startup: staying up would fight the selector for modelV2
|
||||
cloudlog.warning("iqmodeld: big backend toggled, restarting to switch publish mode")
|
||||
sys.exit(0)
|
||||
self._runtime.lat_delay = lateral_action_delay(self._params, self._car_params, self._sub["lateralDelay"].lateralDelay)
|
||||
self._runtime.PLANPLUS_CONTROL = self._params.get("PlanplusControl", return_default=True)
|
||||
self._runtime.model_smoothing_max_extra_sec = _model_lat_smooth_max_sec(self._params)
|
||||
self._warps.set_offset(self._params.get("CameraOffset", return_default=True))
|
||||
@@ -586,6 +604,7 @@ class InferenceDaemon:
|
||||
driving_msg.drivingModelData.meta.laneChangeState = self._desire_logic.lane_change_state
|
||||
driving_msg.drivingModelData.meta.laneChangeDirection = self._desire_logic.lane_change_direction
|
||||
iq_msg.iqDriveModelData.turnSignalDirection = self._desire_logic.lane_turn_direction
|
||||
iq_msg.iqDriveModelData.lateralEdgeBlock = self._desire_logic.lateral_edge_block
|
||||
|
||||
populate_odometry_message(
|
||||
pose_msg,
|
||||
@@ -596,6 +615,22 @@ class InferenceDaemon:
|
||||
live_calib_seen,
|
||||
)
|
||||
|
||||
if self._channel is not None:
|
||||
self._channel.write(main_stamp.frame_id, {
|
||||
"source": "small",
|
||||
"frame_id": main_stamp.frame_id,
|
||||
"timestamp_sof": int(main_stamp.timestamp_sof),
|
||||
"live_calib_seen": bool(live_calib_seen),
|
||||
"model_execution_time": float(execution_time),
|
||||
"msgs": {
|
||||
"modelV2": model_msg.to_bytes(),
|
||||
"drivingModelData": driving_msg.to_bytes(),
|
||||
"cameraOdometry": pose_msg.to_bytes(),
|
||||
"iqDriveModelData": iq_msg.to_bytes(),
|
||||
},
|
||||
})
|
||||
return
|
||||
|
||||
self._pub.send("modelV2", model_msg)
|
||||
self._pub.send("drivingModelData", driving_msg)
|
||||
self._pub.send("cameraOdometry", pose_msg)
|
||||
@@ -603,12 +638,21 @@ class InferenceDaemon:
|
||||
|
||||
def serve(self) -> None:
|
||||
tick = 0
|
||||
starved_polls = 0
|
||||
while True:
|
||||
frame_pair = self._cameras.pull()
|
||||
if frame_pair is None:
|
||||
cloudlog.debug("visionipc frame missing")
|
||||
starved_polls += 1
|
||||
if starved_polls >= _FRAME_STARVED_BACKOFF_POLLS:
|
||||
time.sleep(_FRAME_STARVED_BACKOFF_SECONDS)
|
||||
if starved_polls % _FRAME_STARVED_LOG_EVERY == 0:
|
||||
cloudlog.error(f"visionipc delivered no frames for {starved_polls} polls; model is not running")
|
||||
continue
|
||||
|
||||
if starved_polls:
|
||||
cloudlog.warning(f"visionipc recovered after {starved_polls} frameless polls")
|
||||
starved_polls = 0
|
||||
|
||||
main_buf, extra_buf, main_stamp, extra_stamp = frame_pair
|
||||
self._sub.update(0)
|
||||
self._refresh_tunables(tick)
|
||||
@@ -682,8 +726,15 @@ class InferenceDaemon:
|
||||
tick += 1
|
||||
|
||||
|
||||
def main(demo: bool = False):
|
||||
InferenceDaemon(demo=demo).serve()
|
||||
def main(demo: bool = False, channel_path: str | None = "auto"):
|
||||
if channel_path == "auto":
|
||||
channel_path = None
|
||||
params = Params()
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected
|
||||
if params.get_bool("IQEmacEnabled") or egpu_selected(params):
|
||||
from iqpilot.selfdrive.iqmodeld.model_channel import SMALL_CHANNEL
|
||||
channel_path = SMALL_CHANNEL
|
||||
InferenceDaemon(demo=demo, channel_path=channel_path).serve()
|
||||
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -1,62 +1,62 @@
|
||||
{
|
||||
"displayName": "Default (CD210)",
|
||||
"environment": "development",
|
||||
"generation": 12,
|
||||
"index": 56,
|
||||
"internalName": "C210M",
|
||||
"is20hz": true,
|
||||
"minimumSelectorVersion": 14,
|
||||
"models": [
|
||||
{
|
||||
"artifact": {
|
||||
"downloadUri": {
|
||||
"sha256": "ba5c459412310a8c65a11e02cb1f522fe439d515589754451561268f028f4fb0",
|
||||
"uri": "https://git.konn3kt.com/teal/IQModels/raw/branch/main/models/recompiled16/model-CD210%20Model%20%28January%2031%2C%202026%29-101/driving_policy_c210m_tinygrad.pkl"
|
||||
},
|
||||
"fileName": "driving_policy_c210m_tinygrad.pkl"
|
||||
},
|
||||
"metadata": {
|
||||
"downloadUri": {
|
||||
"sha256": "15c8c1ad9073424ee1101b1f7170421140ed308ebaa7c917001130fb1760420b",
|
||||
"uri": "https://git.konn3kt.com/teal/IQModels/raw/branch/main/models/recompiled16/model-CD210%20Model%20%28January%2031%2C%202026%29-101/driving_policy_c210m_metadata.pkl"
|
||||
},
|
||||
"fileName": "driving_policy_c210m_metadata.pkl"
|
||||
},
|
||||
"type": "policy"
|
||||
"displayName": "Default (CD210)",
|
||||
"environment": "development",
|
||||
"generation": 12,
|
||||
"index": 56,
|
||||
"internalName": "C210M",
|
||||
"is20hz": true,
|
||||
"minimumSelectorVersion": 14,
|
||||
"models": [
|
||||
{
|
||||
"artifact": {
|
||||
"downloadUri": {
|
||||
"sha256": "6bcf9668455c022ccaa6443ed2daddc5b4348ffbdf68dd5abd9c37a1b17bfe98",
|
||||
"uri": "https://git.konn3kt.com/teal/IQModels/raw/branch/main/models/recompiled18/model-C210M/driving_policy_c210m_tinygrad.pkl"
|
||||
},
|
||||
{
|
||||
"artifact": {
|
||||
"downloadUri": {
|
||||
"sha256": "c6c9cfba6c618a361d474d2fe11c4d8e7a249e9311dd9bcb42b287067ef75ae7",
|
||||
"uri": "https://git.konn3kt.com/teal/IQModels/raw/branch/main/models/recompiled16/model-CD210%20Model%20%28January%2031%2C%202026%29-101/driving_vision_c210m_tinygrad.pkl"
|
||||
},
|
||||
"fileName": "driving_vision_c210m_tinygrad.pkl"
|
||||
},
|
||||
"metadata": {
|
||||
"downloadUri": {
|
||||
"sha256": "a2be39088d38550e818f5ac1c6300a64605ba0bd0676a27fe7bbea9fddae90a1",
|
||||
"uri": "https://git.konn3kt.com/teal/IQModels/raw/branch/main/models/recompiled16/model-CD210%20Model%20%28January%2031%2C%202026%29-101/driving_vision_c210m_metadata.pkl"
|
||||
},
|
||||
"fileName": "driving_vision_c210m_metadata.pkl"
|
||||
},
|
||||
"type": "vision"
|
||||
}
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"key": "folder",
|
||||
"value": "Master Models"
|
||||
"fileName": "driving_policy_c210m_tinygrad.pkl"
|
||||
},
|
||||
"metadata": {
|
||||
"downloadUri": {
|
||||
"sha256": "15c8c1ad9073424ee1101b1f7170421140ed308ebaa7c917001130fb1760420b",
|
||||
"uri": "https://git.konn3kt.com/teal/IQModels/raw/branch/main/models/recompiled18/model-C210M/driving_policy_c210m_metadata.pkl"
|
||||
},
|
||||
{
|
||||
"key": "lat",
|
||||
"value": ".0"
|
||||
"fileName": "driving_policy_c210m_metadata.pkl"
|
||||
},
|
||||
"type": "policy"
|
||||
},
|
||||
{
|
||||
"artifact": {
|
||||
"downloadUri": {
|
||||
"sha256": "10fd116056fd1790553c20e09b2b640ebcb85e72418fc9685e0800a513bbe950",
|
||||
"uri": "https://git.konn3kt.com/teal/IQModels/raw/branch/main/models/recompiled18/model-C210M/driving_vision_c210m_tinygrad.pkl"
|
||||
},
|
||||
{
|
||||
"key": "long",
|
||||
"value": ".3"
|
||||
}
|
||||
],
|
||||
"ref": "default",
|
||||
"runner": "tinygrad",
|
||||
"status": "notDownloading"
|
||||
"fileName": "driving_vision_c210m_tinygrad.pkl"
|
||||
},
|
||||
"metadata": {
|
||||
"downloadUri": {
|
||||
"sha256": "a2be39088d38550e818f5ac1c6300a64605ba0bd0676a27fe7bbea9fddae90a1",
|
||||
"uri": "https://git.konn3kt.com/teal/IQModels/raw/branch/main/models/recompiled18/model-C210M/driving_vision_c210m_metadata.pkl"
|
||||
},
|
||||
"fileName": "driving_vision_c210m_metadata.pkl"
|
||||
},
|
||||
"type": "vision"
|
||||
}
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"key": "folder",
|
||||
"value": "Comma Models"
|
||||
},
|
||||
{
|
||||
"key": "lat",
|
||||
"value": ".0"
|
||||
},
|
||||
{
|
||||
"key": "long",
|
||||
"value": ".3"
|
||||
}
|
||||
],
|
||||
"ref": "default",
|
||||
"runner": "tinygrad",
|
||||
"status": "notDownloading"
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
46
iqpilot/selfdrive/iqmodeld/driving_action.py
Normal file
46
iqpilot/selfdrive/iqmodeld/driving_action.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
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 iqpilot.cereal import log
|
||||
|
||||
from iqpilot.selfdrive.controls.lib.drive_helpers import smooth_value
|
||||
|
||||
LAT_SMOOTH_SECONDS = 0.0
|
||||
LONG_SMOOTH_SECONDS = 0.3
|
||||
MIN_LAT_CONTROL_SPEED = 0.3
|
||||
DESIRE_LEN = 8
|
||||
|
||||
|
||||
def get_action_from_model(outputs: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action,
|
||||
v_ego: float, lat_action_t: float, long_action_t: float,
|
||||
lat_smooth_seconds: float | None = None) -> log.ModelDataV2.Action:
|
||||
if "action" in outputs:
|
||||
desired_accel = float(outputs["action"][0, 1])
|
||||
desired_curvature = float(outputs["action"][0, 0]) / (max(1.0, v_ego)) ** 2
|
||||
should_stop = bool(v_ego < 0.3 and desired_accel < 0.1)
|
||||
else:
|
||||
from iqpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, get_curvature_from_plan
|
||||
from iqpilot.selfdrive.iqmodeld.config import ModelConstants, Plan
|
||||
plan = outputs["plan"][0]
|
||||
desired_accel, should_stop = get_accel_from_plan(plan[:, Plan.VELOCITY][:, 0],
|
||||
plan[:, Plan.ACCELERATION][:, 0],
|
||||
ModelConstants.T_IDXS,
|
||||
action_t=long_action_t)
|
||||
desired_curvature = get_curvature_from_plan(plan[:, Plan.T_FROM_CURRENT_EULER][:, 2],
|
||||
plan[:, Plan.ORIENTATION_RATE][:, 2],
|
||||
ModelConstants.T_IDXS, v_ego, lat_action_t)
|
||||
desired_accel, should_stop = float(desired_accel), bool(should_stop)
|
||||
desired_curvature = float(desired_curvature)
|
||||
desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, LONG_SMOOTH_SECONDS)
|
||||
if v_ego > MIN_LAT_CONTROL_SPEED:
|
||||
lat_smooth = LAT_SMOOTH_SECONDS if lat_smooth_seconds is None else lat_smooth_seconds
|
||||
desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, lat_smooth)
|
||||
else:
|
||||
desired_curvature = prev_action.desiredCurvature
|
||||
return log.ModelDataV2.Action(desiredCurvature=float(desired_curvature),
|
||||
desiredAcceleration=float(desired_accel),
|
||||
shouldStop=should_stop)
|
||||
222
iqpilot/selfdrive/iqmodeld/egpu_helpers.py
Normal file
222
iqpilot/selfdrive/iqmodeld/egpu_helpers.py
Normal file
@@ -0,0 +1,222 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
|
||||
from iqpilot.common.swaglog import cloudlog
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
from iqpilot.system.hardware.usb import egpu_dock_ready
|
||||
|
||||
USB_SYSFS_ROOT = "/sys/bus/usb/devices"
|
||||
FIRMWARE_MIRROR = os.getenv("IQ_EGPU_FIRMWARE_MIRROR", "/data/firmware/tinygrad")
|
||||
TINYGRAD_CACHE = "/data/.cache"
|
||||
|
||||
COMMA_LFS_BATCH_URL = "https://gitlab.com/commaai/openpilot-lfs.git/info/lfs/objects/batch"
|
||||
|
||||
DOWNLOAD_CHUNK = 4 * 1024 * 1024
|
||||
|
||||
|
||||
def usbgpu_present(sysfs_root: str = USB_SYSFS_ROOT) -> bool:
|
||||
return egpu_dock_ready(Path(sysfs_root))
|
||||
|
||||
|
||||
def egpu_present_consented(params, sysfs_root: str = USB_SYSFS_ROOT) -> bool:
|
||||
try:
|
||||
if params is not None and params.get_bool("IQEgpuDisabled"):
|
||||
return False
|
||||
except Exception:
|
||||
pass
|
||||
return usbgpu_present(sysfs_root)
|
||||
|
||||
|
||||
def egpu_selected(params, sysfs_root: str = USB_SYSFS_ROOT) -> bool:
|
||||
try:
|
||||
if params is not None and params.get_bool("IQEgpuDisabled"):
|
||||
return False
|
||||
if params is not None and params.get_bool("IQEgpuEnabled"):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return usbgpu_present(sysfs_root)
|
||||
|
||||
|
||||
def resolve_backend(emac_enabled: bool, egpu_enabled: bool, egpu_present: bool = False) -> str | None:
|
||||
if egpu_present:
|
||||
return "egpu"
|
||||
if emac_enabled:
|
||||
return "emac"
|
||||
if egpu_enabled:
|
||||
return "egpu"
|
||||
return None
|
||||
|
||||
|
||||
def egpu_pkl_path(meta: dict) -> str:
|
||||
from iqpilot.system.hardware.hw import Paths
|
||||
return os.path.join(Paths.model_root(), f"egpu_{meta['key']}_{meta['sha256'][:8]}_amd_tinygrad.pkl")
|
||||
|
||||
|
||||
def egpu_policy_pkl_path(meta: dict) -> str:
|
||||
from iqpilot.system.hardware.hw import Paths
|
||||
return os.path.join(Paths.model_root(), f"egpu_{meta['key']}_{meta['sha256'][:8]}_amd_policy.pkl")
|
||||
|
||||
|
||||
def egpu_oob_pkl_path(meta: dict) -> str:
|
||||
from iqpilot.system.hardware.hw import Paths
|
||||
return os.path.join(Paths.model_root(), f"egpu_{meta['key']}_{meta['sha256'][:8]}_amd_policy_oob.pkl")
|
||||
|
||||
|
||||
def egpu_model_oob_pkl_path(meta: dict) -> str:
|
||||
from iqpilot.system.hardware.hw import Paths
|
||||
return os.path.join(Paths.model_root(), f"egpu_{meta['key']}_{meta['sha256'][:8]}_amd_model_oob.pkl")
|
||||
|
||||
|
||||
def onnx_cache_path(meta: dict) -> str:
|
||||
from iqpilot.system.hardware.hw import Paths
|
||||
return os.path.join(Paths.model_root(), f"{meta['model_name']}_{meta['sha256'][:8]}.onnx")
|
||||
|
||||
|
||||
def _sha256_file(path: str) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with open(path, "rb") as f:
|
||||
while chunk := f.read(DOWNLOAD_CHUNK):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def quarantine_artifact(path: str, why: str) -> None:
|
||||
try:
|
||||
if os.path.isfile(path):
|
||||
os.replace(path, path + ".unusable")
|
||||
except OSError:
|
||||
try:
|
||||
os.remove(path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def local_onnx(meta: dict) -> str | None:
|
||||
path = onnx_cache_path(meta)
|
||||
if not os.path.isfile(path):
|
||||
return None
|
||||
size = int(meta.get("download", {}).get("size", 0))
|
||||
if size and os.path.getsize(path) != size:
|
||||
quarantine_artifact(path, "onnx size mismatch")
|
||||
return None
|
||||
if _sha256_file(path) != meta["sha256"]:
|
||||
quarantine_artifact(path, "onnx sha256 mismatch")
|
||||
return None
|
||||
return path
|
||||
|
||||
|
||||
def resolve_download_url(download_url: str, sha256: str, size: int, timeout: float = 30.0) -> str:
|
||||
if download_url.startswith("commalfs:"):
|
||||
oid = download_url.split(":", 1)[1]
|
||||
body = json.dumps({"operation": "download", "transfers": ["basic"],
|
||||
"objects": [{"oid": oid, "size": size}]}).encode()
|
||||
req = urllib.request.Request(COMMA_LFS_BATCH_URL, data=body, headers={
|
||||
"Accept": "application/vnd.git-lfs+json", "Content-Type": "application/vnd.git-lfs+json"})
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
d = json.load(r)
|
||||
return d["objects"][0]["actions"]["download"]["href"]
|
||||
return download_url
|
||||
|
||||
|
||||
def download_onnx(meta: dict, progress_cb=None) -> str:
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_model import download_descriptor
|
||||
download_url, size = download_descriptor(meta)
|
||||
if not download_url:
|
||||
raise RuntimeError(f"model {meta['key']} has no download source; stage the onnx at {onnx_cache_path(meta)}")
|
||||
|
||||
path = onnx_cache_path(meta)
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
try:
|
||||
from iqpilot.selfdrive.iqmodeld.model_bundle_downloader import download_hf_file
|
||||
return download_hf_file(f"onnx/{meta['sha256']}.onnx", path, meta["sha256"], int(size or 0), progress_cb=progress_cb)
|
||||
except Exception as e:
|
||||
cloudlog.warning(f"onnx {meta['key']} unavailable from HF ({e}); falling back to {download_url.split(':', 1)[0]}")
|
||||
url = resolve_download_url(download_url, meta["sha256"], size)
|
||||
tmp = path + ".part"
|
||||
digest = hashlib.sha256()
|
||||
got = 0
|
||||
with urllib.request.urlopen(url, timeout=60) as r, open(tmp, "wb") as f:
|
||||
while chunk := r.read(DOWNLOAD_CHUNK):
|
||||
f.write(chunk)
|
||||
digest.update(chunk)
|
||||
got += len(chunk)
|
||||
if progress_cb is not None and size:
|
||||
progress_cb(got / size)
|
||||
if size and got != size:
|
||||
os.remove(tmp)
|
||||
raise RuntimeError(f"onnx download truncated: {got}/{size} bytes")
|
||||
if digest.hexdigest() != meta["sha256"]:
|
||||
os.remove(tmp)
|
||||
raise RuntimeError(f"onnx sha256 mismatch for {meta['key']}")
|
||||
os.replace(tmp, path)
|
||||
return path
|
||||
|
||||
|
||||
ARTIFACT_PATHS = {
|
||||
"egpu_model_oob_artifact": egpu_model_oob_pkl_path,
|
||||
"egpu_oob_artifact": egpu_oob_pkl_path,
|
||||
"egpu_policy_artifact": egpu_policy_pkl_path,
|
||||
"egpu_artifact": egpu_pkl_path,
|
||||
}
|
||||
|
||||
|
||||
def download_precompiled(meta: dict, progress_cb=None, policy: bool = False, oob: bool = False, field: str | None = None) -> str | None:
|
||||
field = field or ("egpu_oob_artifact" if oob else "egpu_policy_artifact" if policy else "egpu_artifact")
|
||||
art = meta.get(field)
|
||||
if not art or not (art.get("objects") or art.get("hf_path")):
|
||||
return None
|
||||
from iqpilot.selfdrive.iqmodeld.model_bundle_downloader import download_hf_file, download_lfs_bundle
|
||||
dest = ARTIFACT_PATHS[field](meta)
|
||||
if art.get("hf_path"):
|
||||
try:
|
||||
return download_hf_file(art["hf_path"], dest, art["sha256"], int(art.get("size", 0)), progress_cb=progress_cb)
|
||||
except Exception as e:
|
||||
cloudlog.warning(f"precompiled {meta['key']} unavailable from HF ({e}); trying LFS")
|
||||
if not art.get("objects"):
|
||||
raise
|
||||
return download_lfs_bundle(art["objects"], dest, art["sha256"], int(art.get("size", 0)), progress_cb=progress_cb)
|
||||
|
||||
|
||||
def patch_tinygrad_fetch_fw() -> None:
|
||||
import pathlib
|
||||
|
||||
import zstandard
|
||||
from tinygrad import helpers
|
||||
if getattr(helpers.fetch_fw, "_iq_patched", False):
|
||||
return
|
||||
_orig = helpers.fetch_fw
|
||||
|
||||
def fetch_fw(path, name, sha256):
|
||||
mirror = pathlib.Path(FIRMWARE_MIRROR) / path / name
|
||||
if mirror.is_file():
|
||||
blob = mirror.read_bytes()
|
||||
if hashlib.sha256(blob).hexdigest() == sha256:
|
||||
return blob
|
||||
p = pathlib.Path(f"/lib/firmware/{path}/{name}.zst")
|
||||
if p.is_file():
|
||||
blob = zstandard.ZstdDecompressor().stream_reader(p.read_bytes()).read()
|
||||
if hashlib.sha256(blob).hexdigest() == sha256:
|
||||
return blob
|
||||
blob = _orig(path, name, sha256)
|
||||
# The dock's GPU firmware otherwise lives only in tinygrad's per-user download cache, which is
|
||||
# a network fetch the first time a new HOME sees it; onroad the car is usually offline.
|
||||
try:
|
||||
mirror.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = mirror.with_suffix(mirror.suffix + ".part")
|
||||
tmp.write_bytes(blob)
|
||||
os.replace(tmp, mirror)
|
||||
except OSError:
|
||||
pass
|
||||
return blob
|
||||
|
||||
fetch_fw._iq_patched = True
|
||||
helpers.fetch_fw = fetch_fw
|
||||
9
iqpilot/selfdrive/iqmodeld/egpu_model.py
Normal file
9
iqpilot/selfdrive/iqmodeld/egpu_model.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module
|
||||
|
||||
try:
|
||||
load_private_module(__name__, "iqpilot_private.models.egpu_model")
|
||||
except ProprietaryModuleMissing:
|
||||
from iqpilot.models_private_src.egpu_model import *
|
||||
68
iqpilot/selfdrive/iqmodeld/egpu_pipeline.py
Normal file
68
iqpilot/selfdrive/iqmodeld/egpu_pipeline.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""
|
||||
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 iqpilot.selfdrive.iqmodeld.egpu_policy import ModelRunner, PolicyRunner
|
||||
from iqpilot.selfdrive.iqmodeld.temporal_state import MODEL_INPUT_SPEC, TemporalInputState, spec_from_meta
|
||||
|
||||
|
||||
class EgpuPipelineError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class EgpuOutputInvalid(EgpuPipelineError):
|
||||
pass
|
||||
|
||||
|
||||
class EgpuPipeline:
|
||||
|
||||
def __init__(self, meta: dict, infer_fn):
|
||||
if meta.get("split"):
|
||||
raise EgpuPipelineError(f"model {meta['key']} is a split model; eGPU v1 runs fused models only")
|
||||
self.meta = meta
|
||||
self.infer_fn = infer_fn
|
||||
self.state = TemporalInputState(meta["frame_skip"], spec_from_meta(meta) or MODEL_INPUT_SPEC)
|
||||
self.hidden_slice = meta["output_slices"]["hidden_state"]
|
||||
self.output_len = int(meta["output_len"])
|
||||
|
||||
def run(self, warped: np.ndarray, desire_vec: np.ndarray, traffic_convention: np.ndarray,
|
||||
action_t: np.ndarray) -> np.ndarray:
|
||||
if isinstance(self.infer_fn, PolicyRunner):
|
||||
out = np.asarray(self.infer_fn.run(warped, desire_vec, traffic_convention, action_t), dtype=np.float32).reshape(-1)
|
||||
else:
|
||||
inputs = self.state.push_and_materialize(warped, desire_vec, traffic_convention, action_t)
|
||||
out = np.asarray(self.infer_fn(inputs), dtype=np.float32).reshape(-1)
|
||||
self._check(out)
|
||||
if not isinstance(self.infer_fn, PolicyRunner):
|
||||
self.state.note_hidden_state(out, self.hidden_slice)
|
||||
return out
|
||||
|
||||
def run_frames(self, main_frame, extra_frame, tfm: np.ndarray, big_tfm: np.ndarray, desire_vec: np.ndarray,
|
||||
traffic_convention: np.ndarray, action_t: np.ndarray) -> np.ndarray:
|
||||
if not isinstance(self.infer_fn, ModelRunner):
|
||||
raise EgpuPipelineError("run_frames needs a format-3 (warp-on-dock) artifact")
|
||||
out = np.asarray(self.infer_fn.run(main_frame, extra_frame, tfm, big_tfm, desire_vec, traffic_convention, action_t),
|
||||
dtype=np.float32).reshape(-1)
|
||||
self._check(out)
|
||||
return out
|
||||
|
||||
def _check(self, out: np.ndarray) -> None:
|
||||
if out.shape[0] != self.output_len:
|
||||
raise EgpuPipelineError(f"eGPU output length {out.shape[0]} != {self.output_len}")
|
||||
if not np.isfinite(out).all():
|
||||
raise EgpuOutputInvalid("eGPU output contains non-finite values")
|
||||
|
||||
|
||||
def make_big_channel_payload(frame_id: int, live_calib_seen: bool, execution_time: float,
|
||||
egpu_exec_ms: float, msgs: dict[str, bytes]) -> dict:
|
||||
return {
|
||||
"source": "egpu_big",
|
||||
"frame_id": int(frame_id),
|
||||
"live_calib_seen": bool(live_calib_seen),
|
||||
"model_execution_time": float(execution_time),
|
||||
"egpu_exec_ms": float(egpu_exec_ms),
|
||||
"msgs": msgs,
|
||||
}
|
||||
341
iqpilot/selfdrive/iqmodeld/egpu_policy.py
Normal file
341
iqpilot/selfdrive/iqmodeld/egpu_policy.py
Normal file
@@ -0,0 +1,341 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import math
|
||||
import os
|
||||
import pickle
|
||||
import shutil
|
||||
import struct
|
||||
import tempfile
|
||||
|
||||
import numpy as np
|
||||
|
||||
POLICY_FORMAT = 2
|
||||
MODEL_FORMAT = 3
|
||||
OOB_MAGIC = b"IQEGPUOOB1"
|
||||
QUEUE_NAMES = ("img_q", "big_img_q", "feat_q", "desire_q")
|
||||
PACKED_ORDER = ("desire", "traffic_convention", "action_t", "prev_feat")
|
||||
MODELD_INPUTS = (*QUEUE_NAMES, "packed_npy_inputs")
|
||||
|
||||
|
||||
def packed_layout(input_spec: dict) -> tuple[dict[str, tuple[int, ...]], list[int]]:
|
||||
dp = input_spec["desire_pulse"][0]
|
||||
fb = input_spec["features_buffer"][0]
|
||||
shapes = {
|
||||
"desire": (dp[2],),
|
||||
"traffic_convention": tuple(input_spec["traffic_convention"][0]),
|
||||
"action_t": tuple(input_spec["action_t"][0]),
|
||||
"prev_feat": (fb[0], math.prod(fb[2:])),
|
||||
}
|
||||
return shapes, [math.prod(s) for s in shapes.values()]
|
||||
|
||||
|
||||
def queue_shapes(input_spec: dict, frame_skip: int) -> dict[str, tuple[tuple[int, ...], str]]:
|
||||
img = input_spec["img"][0]
|
||||
fb = input_spec["features_buffer"][0]
|
||||
dp = input_spec["desire_pulse"][0]
|
||||
n_frames = img[1] // 6
|
||||
img_buf = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3])
|
||||
return {
|
||||
"img_q": (img_buf, "uint8"),
|
||||
"big_img_q": (img_buf, "uint8"),
|
||||
"feat_q": ((frame_skip * fb[1], fb[0], math.prod(fb[2:])), "float32"),
|
||||
"desire_q": ((frame_skip * dp[1], dp[0], dp[2]), "float32"),
|
||||
}
|
||||
|
||||
|
||||
def make_queues(input_spec: dict, frame_skip: int, device: str) -> dict:
|
||||
from tinygrad.tensor import Tensor
|
||||
return {name: Tensor(np.zeros(shape, dtype=dtype), device=device).contiguous().realize()
|
||||
for name, (shape, dtype) in queue_shapes(input_spec, frame_skip).items()}
|
||||
|
||||
|
||||
class PackedInputs:
|
||||
def __init__(self, input_spec: dict):
|
||||
from tinygrad.tensor import Tensor
|
||||
self.shapes, self.sizes = packed_layout(input_spec)
|
||||
self.array = np.zeros(sum(self.sizes), dtype=np.float32)
|
||||
parts = np.split(self.array, np.cumsum(self.sizes[:-1]))
|
||||
self.views = {name: part.reshape(shape) for (name, shape), part in zip(self.shapes.items(), parts, strict=True)}
|
||||
self.tensor = Tensor(self.array, device="NPY").realize()
|
||||
|
||||
|
||||
def make_run_policy(model_runner, input_spec: dict, frame_skip: int, device: str):
|
||||
from tinygrad.tensor import Tensor
|
||||
shapes, sizes = packed_layout(input_spec)
|
||||
fb = input_spec["features_buffer"][0]
|
||||
|
||||
def shift_and_sample(buf, new_val, sample_fn):
|
||||
buf.assign(buf[1:].cat(new_val, dim=0).contiguous())
|
||||
return sample_fn(buf)
|
||||
|
||||
def sample_skip(buf):
|
||||
return buf[::frame_skip].contiguous().flatten(0, 1).unsqueeze(0)
|
||||
|
||||
def sample_desire(buf):
|
||||
return buf.reshape(-1, frame_skip, *buf.shape[1:]).max(1).flatten(0, 1).unsqueeze(0)
|
||||
|
||||
def run_policy(warped, img_q, big_img_q, feat_q, desire_q, packed_npy_inputs):
|
||||
packed_npy_inputs = packed_npy_inputs.to(device)
|
||||
warped = warped.to(device)
|
||||
Tensor.realize(packed_npy_inputs, warped)
|
||||
img = shift_and_sample(img_q, warped[0:1], sample_skip)
|
||||
big_img = shift_and_sample(big_img_q, warped[1:2], sample_skip)
|
||||
desire, traffic_convention, action_t, prev_feat = (t.reshape(s) for t, s in zip(packed_npy_inputs.split(sizes), shapes.values(), strict=True))
|
||||
desire_buf = shift_and_sample(desire_q, desire.reshape(1, 1, -1), sample_desire)
|
||||
feat_buf = shift_and_sample(feat_q, prev_feat.reshape(1, 1, -1), sample_skip)
|
||||
inputs = {
|
||||
"img": img,
|
||||
"big_img": big_img,
|
||||
"features_buffer": feat_buf.reshape(fb),
|
||||
"desire_pulse": desire_buf,
|
||||
"traffic_convention": traffic_convention,
|
||||
"action_t": action_t,
|
||||
}
|
||||
out = next(iter(model_runner(inputs).values())).cast("float32")
|
||||
return out.reshape(-1),
|
||||
|
||||
return run_policy
|
||||
|
||||
|
||||
class PolicyRunner:
|
||||
def __init__(self, jit, input_spec: dict, frame_skip: int, hidden_slice: slice, device: str):
|
||||
from tinygrad.tensor import Tensor
|
||||
self._Tensor = Tensor
|
||||
self._jit = jit
|
||||
self._queues = make_queues(input_spec, frame_skip, device)
|
||||
self._packed = PackedInputs(input_spec)
|
||||
self._hidden = hidden_slice
|
||||
self._prev_desire = np.zeros(input_spec["desire_pulse"][0][2], dtype=np.float32)
|
||||
self._warped_shape = (2, 6, *input_spec["img"][0][2:])
|
||||
|
||||
def run(self, warped: np.ndarray, desire_pulse: np.ndarray, traffic_convention: np.ndarray,
|
||||
action_t: np.ndarray) -> np.ndarray:
|
||||
cur = desire_pulse.astype(np.float32, copy=False)
|
||||
v = self._packed.views
|
||||
v["desire"][:] = np.where(cur - self._prev_desire > 0.99, cur, 0)
|
||||
self._prev_desire[:] = cur
|
||||
v["traffic_convention"][:] = np.asarray(traffic_convention, dtype=np.float32).reshape(v["traffic_convention"].shape)
|
||||
v["action_t"][:] = np.asarray(action_t, dtype=np.float32).reshape(v["action_t"].shape)
|
||||
warped_t = self._Tensor(np.ascontiguousarray(warped, dtype=np.uint8).reshape(self._warped_shape), device="NPY").realize()
|
||||
out, = self._jit(warped=warped_t, packed_npy_inputs=self._packed.tensor, **self._queues)
|
||||
flat = out.numpy().reshape(-1)
|
||||
v["prev_feat"][:] = flat[self._hidden].reshape(v["prev_feat"].shape)
|
||||
return flat
|
||||
|
||||
|
||||
def nv12_copy_size(stride: int, y_height: int, uv_height: int) -> int:
|
||||
return stride * (y_height + uv_height)
|
||||
|
||||
|
||||
def frame_layout(input_spec: dict) -> tuple[dict[str, tuple[int, ...]], list[int], int]:
|
||||
policy_shapes, _ = packed_layout(input_spec)
|
||||
shapes = {"tfm": (3, 3), "big_tfm": (3, 3)} | policy_shapes
|
||||
sizes = [math.prod(s) for s in shapes.values()]
|
||||
return shapes, sizes, sum(sizes) * np.dtype(np.float32).itemsize
|
||||
|
||||
|
||||
def model_size(input_spec: dict) -> tuple[int, int]:
|
||||
img = input_spec["img"][0]
|
||||
return img[3] * 2, img[2] * 2
|
||||
|
||||
|
||||
def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad, border_fill_val=None):
|
||||
from tinygrad.tensor import Tensor
|
||||
w_dst, h_dst = dst_shape
|
||||
h_src, w_src = src_shape
|
||||
|
||||
x = Tensor.arange(w_dst).reshape(1, w_dst).expand(h_dst, w_dst).reshape(-1)
|
||||
y = Tensor.arange(h_dst).reshape(h_dst, 1).expand(h_dst, w_dst).reshape(-1)
|
||||
|
||||
src_x = M_inv[0, 0] * x + M_inv[0, 1] * y + M_inv[0, 2]
|
||||
src_y = M_inv[1, 0] * x + M_inv[1, 1] * y + M_inv[1, 2]
|
||||
src_w = M_inv[2, 0] * x + M_inv[2, 1] * y + M_inv[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, w_src - 1).cast("int")
|
||||
y_nn_clipped = y_round.clip(0, h_src - 1).cast("int")
|
||||
idx = y_nn_clipped * (w_src + stride_pad) + x_nn_clipped
|
||||
sampled = src_flat[idx]
|
||||
|
||||
if border_fill_val is None:
|
||||
return sampled
|
||||
|
||||
in_bounds = ((x_round >= 0) & (x_round <= w_src - 1) &
|
||||
(y_round >= 0) & (y_round <= h_src - 1)).cast(sampled.dtype)
|
||||
return sampled * in_bounds + Tensor(border_fill_val, dtype=sampled.dtype) * (1 - in_bounds)
|
||||
|
||||
|
||||
def frames_to_tensor(frames):
|
||||
from tinygrad.tensor import Tensor
|
||||
H = (frames.shape[0] * 2) // 3
|
||||
W = frames.shape[1]
|
||||
in_img1 = Tensor.cat(frames[0:H:2, 0::2],
|
||||
frames[1:H:2, 0::2],
|
||||
frames[0:H:2, 1::2],
|
||||
frames[1:H:2, 1::2],
|
||||
frames[H:H + H // 4].reshape((H // 2, W // 2)),
|
||||
frames[H + H // 4:H + H // 2].reshape((H // 2, W // 2)), dim=0).reshape((6, H // 2, W // 2))
|
||||
return in_img1
|
||||
|
||||
|
||||
def make_frame_prepare(nv12: tuple[int, int, int, int, int], model_w: int, model_h: int, device: str):
|
||||
from tinygrad.helpers import Context
|
||||
from tinygrad.tensor import Tensor
|
||||
cam_w, cam_h, stride, y_height, uv_height = nv12
|
||||
uv_offset = stride * y_height
|
||||
stride_pad = stride - cam_w
|
||||
|
||||
def frame_prepare_tinygrad(input_frame, M_inv):
|
||||
M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]], device=device)
|
||||
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],
|
||||
M_inv, (model_w, model_h),
|
||||
(cam_h, cam_w), stride_pad).realize()
|
||||
u = warp_perspective_tinygrad(uv[:cam_h // 2, :cam_w:2].flatten(),
|
||||
M_inv_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(),
|
||||
M_inv_uv, (model_w // 2, model_h // 2),
|
||||
(cam_h // 2, cam_w // 2), 0).realize()
|
||||
yuv = y.cat(u).cat(v).reshape((model_h * 3 // 2, model_w))
|
||||
return frames_to_tensor(yuv)
|
||||
return frame_prepare_tinygrad
|
||||
|
||||
|
||||
def make_warp(nv12: tuple[int, int, int, int, int], model_w: int, model_h: int, device: str):
|
||||
from tinygrad.tensor import Tensor
|
||||
frame_prepare = make_frame_prepare(nv12, model_w, model_h, device)
|
||||
|
||||
def warp(tfm, big_tfm, frame, big_frame):
|
||||
tfm = tfm.to(device)
|
||||
big_tfm = big_tfm.to(device)
|
||||
frame = frame.to(device)
|
||||
big_frame = big_frame.to(device)
|
||||
Tensor.realize(tfm, big_tfm, frame, big_frame)
|
||||
|
||||
warped_frame = frame_prepare(frame, tfm).unsqueeze(0)
|
||||
warped_big_frame = frame_prepare(big_frame, big_tfm).unsqueeze(0)
|
||||
return Tensor.cat(warped_frame, warped_big_frame)
|
||||
|
||||
return warp
|
||||
|
||||
|
||||
def make_run_model(warp, run_policy, input_spec: dict, frame_copy_size: int, device: str):
|
||||
from tinygrad.tensor import Tensor
|
||||
_, policy_sizes = packed_layout(input_spec)
|
||||
_, _, packed_npy_size = frame_layout(input_spec)
|
||||
|
||||
def run_model(img_q, big_img_q, feat_q, desire_q, packed_npy_inputs):
|
||||
packed_input = packed_npy_inputs.to(device)
|
||||
Tensor.realize(packed_input)
|
||||
packed_npy_inputs = packed_input[:packed_npy_size].bitcast("float32")
|
||||
frame = packed_input[packed_npy_size:packed_npy_size + frame_copy_size]
|
||||
big_frame = packed_input[packed_npy_size + frame_copy_size:]
|
||||
tfm, big_tfm, policy_inputs = packed_npy_inputs.split([9, 9, sum(policy_sizes)])
|
||||
warped = warp(tfm.reshape(3, 3), big_tfm.reshape(3, 3), frame, big_frame)
|
||||
return run_policy(warped, img_q, big_img_q, feat_q, desire_q, policy_inputs)
|
||||
|
||||
return run_model
|
||||
|
||||
|
||||
class PackedFrames:
|
||||
def __init__(self, input_spec: dict, frame_copy_size: int):
|
||||
from tinygrad.tensor import Tensor
|
||||
self.shapes, self.sizes, npy_bytes = frame_layout(input_spec)
|
||||
self.frame_copy_size = frame_copy_size
|
||||
self.array = np.zeros(npy_bytes + 2 * frame_copy_size, dtype=np.uint8)
|
||||
npy = self.array[:npy_bytes].view(np.float32)
|
||||
self.views = dict(zip(self.shapes, [v.reshape(s) for s, v in zip(self.shapes.values(), np.split(npy, np.cumsum(self.sizes[:-1])), strict=True)],
|
||||
strict=True))
|
||||
frames = self.array[npy_bytes:]
|
||||
self.frames = {"img": frames[:frame_copy_size], "big_img": frames[frame_copy_size:]}
|
||||
self.tensor = Tensor(self.array, device="NPY").realize()
|
||||
|
||||
|
||||
def make_model_queues(input_spec: dict, frame_skip: int, device: str, frame_copy_size: int) -> tuple[dict, PackedFrames]:
|
||||
packed = PackedFrames(input_spec, frame_copy_size)
|
||||
return {**make_queues(input_spec, frame_skip, device), "packed_npy_inputs": packed.tensor}, packed
|
||||
|
||||
|
||||
class ModelRunner:
|
||||
def __init__(self, jit, input_spec: dict, frame_skip: int, hidden_slice: slice, device: str, frame_copy_size: int):
|
||||
self._jit = jit
|
||||
self._queues, self._packed = make_model_queues(input_spec, frame_skip, device, frame_copy_size)
|
||||
self._hidden = hidden_slice
|
||||
self._prev_desire = np.zeros(input_spec["desire_pulse"][0][2], dtype=np.float32)
|
||||
self.frame_copy_size = frame_copy_size
|
||||
|
||||
def run(self, main_frame, extra_frame, tfm: np.ndarray, big_tfm: np.ndarray, desire_pulse: np.ndarray,
|
||||
traffic_convention: np.ndarray, action_t: np.ndarray) -> np.ndarray:
|
||||
n = self.frame_copy_size
|
||||
v = self._packed.views
|
||||
f = self._packed.frames
|
||||
np.copyto(f["img"], np.frombuffer(main_frame, dtype=np.uint8, count=n))
|
||||
np.copyto(f["big_img"], np.frombuffer(extra_frame, dtype=np.uint8, count=n))
|
||||
v["tfm"][:, :] = tfm
|
||||
v["big_tfm"][:, :] = big_tfm
|
||||
cur = desire_pulse.astype(np.float32, copy=False)
|
||||
v["desire"][:] = np.where(cur - self._prev_desire > 0.99, cur, 0)
|
||||
self._prev_desire[:] = cur
|
||||
v["traffic_convention"][:] = np.asarray(traffic_convention, dtype=np.float32).reshape(v["traffic_convention"].shape)
|
||||
v["action_t"][:] = np.asarray(action_t, dtype=np.float32).reshape(v["action_t"].shape)
|
||||
out, = self._jit(**self._queues)
|
||||
flat = out.numpy().reshape(-1)
|
||||
v["prev_feat"][:] = flat[self._hidden].reshape(v["prev_feat"].shape)
|
||||
return flat
|
||||
|
||||
|
||||
def dump_oob(obj, f) -> None:
|
||||
# Out-of-band pickle buffers keep the host peak at one tensor while the weights stream to the
|
||||
# dock; a plain pickle keeps every weight referenced in the memo until load() returns (~1.7GB).
|
||||
f.write(OOB_MAGIC)
|
||||
with tempfile.TemporaryFile(dir=os.path.dirname(os.path.abspath(f.name)) or ".") as tmp:
|
||||
def buffer_callback(pb: pickle.PickleBuffer):
|
||||
m = pb.raw()
|
||||
tmp.write(struct.pack("<q", m.nbytes))
|
||||
tmp.write(m)
|
||||
pb.release()
|
||||
stream = io.BytesIO()
|
||||
pickle.Pickler(stream, protocol=5, buffer_callback=buffer_callback).dump(obj)
|
||||
opcodes = stream.getvalue()
|
||||
f.write(struct.pack("<q", len(opcodes)))
|
||||
f.write(opcodes)
|
||||
tmp.seek(0)
|
||||
shutil.copyfileobj(tmp, f)
|
||||
|
||||
|
||||
def is_oob(path: str) -> bool:
|
||||
with open(path, "rb") as f:
|
||||
return f.read(len(OOB_MAGIC)) == OOB_MAGIC
|
||||
|
||||
|
||||
def load_oob(f):
|
||||
if f.read(len(OOB_MAGIC)) != OOB_MAGIC:
|
||||
raise ValueError("not an out-of-band bundle")
|
||||
opcodes = f.read(struct.unpack("<q", f.read(8))[0])
|
||||
|
||||
def buffers():
|
||||
while (h := f.read(8)):
|
||||
pb = pickle.PickleBuffer(bytearray(struct.unpack("<q", h)[0]))
|
||||
f.readinto(pb)
|
||||
yield pb
|
||||
|
||||
return pickle.load(io.BytesIO(opcodes), buffers=buffers())
|
||||
|
||||
|
||||
def load_bundle(path: str):
|
||||
with open(path, "rb") as f:
|
||||
if f.read(len(OOB_MAGIC)) == OOB_MAGIC:
|
||||
f.seek(0)
|
||||
return load_oob(f)
|
||||
f.seek(0)
|
||||
return pickle.load(f)
|
||||
102
iqpilot/selfdrive/iqmodeld/egpu_prefetch.py
Normal file
102
iqpilot/selfdrive/iqmodeld/egpu_prefetch.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import glob
|
||||
import os
|
||||
os.environ.setdefault("XDG_CACHE_HOME", "/data/.cache")
|
||||
import time
|
||||
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.common.swaglog import cloudlog
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_helpers import (download_precompiled, egpu_oob_pkl_path, egpu_policy_pkl_path, patch_tinygrad_fetch_fw,
|
||||
usbgpu_present)
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_model import resolve_egpu_model
|
||||
|
||||
POLL_S = 30.0
|
||||
RETRY_S = 120.0
|
||||
|
||||
|
||||
def _selected_meta(params: Params) -> dict | None:
|
||||
key = params.get("IQEmacModel", encoding="utf8")
|
||||
try:
|
||||
return resolve_egpu_model(params, key)
|
||||
except Exception as e:
|
||||
cloudlog.warning(f"egpu_prefetch cannot resolve {key!r}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _drop_stale_partials(keep: str) -> None:
|
||||
root = os.path.dirname(keep)
|
||||
partial_globs = ("egpu_*_amd_policy.pkl.part", "egpu_*_amd_policy_oob.pkl.part", "big_driving_supercombo_*.onnx.part")
|
||||
for path in [p for g in partial_globs for p in glob.glob(os.path.join(root, g))]:
|
||||
if not path.startswith(keep):
|
||||
try:
|
||||
os.remove(path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def prefetch_once(params: Params) -> bool:
|
||||
if params.get_bool("IQEgpuDisabled"):
|
||||
return False
|
||||
meta = _selected_meta(params)
|
||||
if meta is None:
|
||||
return False
|
||||
oob = bool(meta.get("egpu_oob_artifact"))
|
||||
dst = egpu_oob_pkl_path(meta) if oob else egpu_policy_pkl_path(meta)
|
||||
if os.path.isfile(dst):
|
||||
return True
|
||||
if not oob and not meta.get("egpu_policy_artifact"):
|
||||
return False
|
||||
_drop_stale_partials(dst)
|
||||
params.put("UsbGpuSetupProgress", "0.0")
|
||||
last = [-1.0]
|
||||
|
||||
def _prog(p: float) -> None:
|
||||
if p - last[0] >= 0.02 or p >= 1.0:
|
||||
last[0] = p
|
||||
params.put("UsbGpuSetupProgress", f"{p:.3f}")
|
||||
|
||||
cloudlog.warning(f"egpu_prefetch downloading {meta['key']} {'streamable' if oob else 'policy'} artifact offroad")
|
||||
out = download_precompiled(meta, progress_cb=_prog, policy=not oob, oob=oob)
|
||||
cloudlog.warning(f"egpu_prefetch ready -> {out}")
|
||||
return out is not None
|
||||
|
||||
|
||||
_firmware_warm = False
|
||||
|
||||
|
||||
def warm_firmware() -> None:
|
||||
global _firmware_warm
|
||||
if _firmware_warm or not usbgpu_present():
|
||||
return
|
||||
os.environ.setdefault("DEV", "USB+AMD:LLVM")
|
||||
os.environ.setdefault("GMMU", "0")
|
||||
patch_tinygrad_fetch_fw()
|
||||
from tinygrad.device import Device
|
||||
Device["AMD"]
|
||||
_firmware_warm = True
|
||||
cloudlog.warning("egpu_prefetch: dock opened offroad; firmware cached and mirrored")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
params = Params()
|
||||
while True:
|
||||
try:
|
||||
warm_firmware()
|
||||
except Exception as e:
|
||||
cloudlog.warning(f"egpu_prefetch firmware warm failed: {e}")
|
||||
try:
|
||||
prefetch_once(params)
|
||||
except Exception as e:
|
||||
cloudlog.warning(f"egpu_prefetch failed: {e}")
|
||||
params.put("UsbGpuLastError", str(e)[:512])
|
||||
time.sleep(RETRY_S)
|
||||
continue
|
||||
time.sleep(POLL_S)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
106
iqpilot/selfdrive/iqmodeld/egpu_telemetry.py
Normal file
106
iqpilot/selfdrive/iqmodeld/egpu_telemetry.py
Normal file
@@ -0,0 +1,106 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
|
||||
from iqpilot.cereal import messaging
|
||||
from iqpilot.common.swaglog import cloudlog
|
||||
|
||||
METRICS_REFRESH_EVERY = 100
|
||||
|
||||
|
||||
class EgpuDockTelemetry:
|
||||
|
||||
def __init__(self, pm, big: bool):
|
||||
self.pm = pm
|
||||
self.big = big
|
||||
self.valid = True
|
||||
self.sends = 0
|
||||
self.metrics: dict[str, float] = {}
|
||||
self._power_limit: int | None = None
|
||||
self._asm_usb = None
|
||||
|
||||
def _device(self):
|
||||
from tinygrad.device import Device
|
||||
return Device
|
||||
|
||||
def _open_asm_usb(self):
|
||||
import usb1
|
||||
from iqpilot.system.hardware.usb import EGPU_DOCK_USB_IDS
|
||||
context = usb1.USBContext()
|
||||
for vendor_id, product_id in EGPU_DOCK_USB_IDS:
|
||||
handle = context.openByVendorIDAndProductID(vendor_id, product_id, skip_on_error=True)
|
||||
if handle is not None:
|
||||
return handle
|
||||
context.close()
|
||||
return None
|
||||
|
||||
def _read_ina(self):
|
||||
Device = self._device()
|
||||
if "AMD" in Device._opened_devices and self._asm_usb is None:
|
||||
try:
|
||||
raw = Device["AMD"].iface.pci_dev.usb.usb.control_read(0xC0, 5)
|
||||
return struct.unpack("<Hh?", bytes(raw))
|
||||
except Exception:
|
||||
pass
|
||||
if self._asm_usb is None:
|
||||
self._asm_usb = self._open_asm_usb()
|
||||
if self._asm_usb is None:
|
||||
raise RuntimeError("no egpu ASM usb handle")
|
||||
try:
|
||||
raw = self._asm_usb.controlRead(0xC0, 0xC0, 0, 0, 5, timeout=100)
|
||||
except Exception:
|
||||
self._asm_usb = None
|
||||
raise
|
||||
return struct.unpack("<Hh?", bytes(raw))
|
||||
|
||||
def power_limit(self, smu) -> int:
|
||||
if self._power_limit is None:
|
||||
self._power_limit = smu._send_msg(smu.smu_mod.PPSMC_MSG_GetPptLimit, 0, read_back_arg=True, timeout=100)
|
||||
return self._power_limit
|
||||
|
||||
def send(self) -> None:
|
||||
Device = self._device()
|
||||
msg = messaging.new_message("egpuDockState")
|
||||
state = msg.egpuDockState
|
||||
self.sends += 1
|
||||
|
||||
if self.big and "AMD" in Device._opened_devices and self.sends % METRICS_REFRESH_EVERY == 1:
|
||||
try:
|
||||
smu = Device["AMD"].iface.dev_impl.smu
|
||||
smu._send_msg(smu.smu_mod.PPSMC_MSG_TransferTableSmu2Dram, smu.smu_mod.TABLE_SMU_METRICS, timeout=100)
|
||||
metrics = smu.read_table(smu.smu_mod.SmuMetricsExternal_t, smu.smu_mod.TABLE_SMU_METRICS).SmuMetrics
|
||||
self.metrics = {"tempC": metrics.AvgTemperature[smu.smu_mod.TEMP_HOTSPOT],
|
||||
"memoryTempC": metrics.AvgTemperature[smu.smu_mod.TEMP_MEM],
|
||||
"powerDrawW": metrics.AverageSocketPower,
|
||||
"powerLimitW": self.power_limit(smu),
|
||||
"gpuUsagePercent": metrics.AverageGfxActivity,
|
||||
"gpuClockMhz": metrics.AverageGfxclkFrequencyPostDs,
|
||||
"fanSpeedRpm": metrics.AvgFanRpm}
|
||||
self.valid = True
|
||||
except Exception:
|
||||
if self.valid:
|
||||
cloudlog.exception("egpu dock state read failed")
|
||||
self.valid = False
|
||||
self.metrics.clear()
|
||||
|
||||
if self.big:
|
||||
for k, v in self.metrics.items():
|
||||
setattr(state, k, v)
|
||||
|
||||
asm_valid = False
|
||||
try:
|
||||
state.supplyVoltage, state.supplyCurrent, state.supplyFault = self._read_ina()
|
||||
asm_valid = True
|
||||
except Exception:
|
||||
pass
|
||||
if "AMD" in Device._opened_devices:
|
||||
try:
|
||||
state.pcieLtssm = Device["AMD"].iface.pci_dev.usb.read(0xB450, 1)[0]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
msg.valid = asm_valid and (not self.big or self.valid)
|
||||
self.pm.send("egpuDockState", msg)
|
||||
10
iqpilot/selfdrive/iqmodeld/emac_input_state.py
Normal file
10
iqpilot/selfdrive/iqmodeld/emac_input_state.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.temporal_state import (
|
||||
SplitTemporalState as SplitInputState,
|
||||
TemporalInputState as EmacInputState,
|
||||
)
|
||||
|
||||
__all__ = ["EmacInputState", "SplitInputState"]
|
||||
9
iqpilot/selfdrive/iqmodeld/emac_model_meta.py
Normal file
9
iqpilot/selfdrive/iqmodeld/emac_model_meta.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module
|
||||
|
||||
try:
|
||||
load_private_module(__name__, "iqpilot_private.models.emac_model_meta")
|
||||
except ProprietaryModuleMissing:
|
||||
from iqpilot.models_private_src.emac_model_meta import *
|
||||
580
iqpilot/selfdrive/iqmodeld/iqegpumodeld.py
Normal file
580
iqpilot/selfdrive/iqmodeld/iqegpumodeld.py
Normal file
@@ -0,0 +1,580 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
os.environ.setdefault("XDG_CACHE_HOME", "/data/.cache")
|
||||
import pickle
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
from iqpilot.system.hardware import TICI
|
||||
|
||||
os.environ.setdefault("GMMU", "0")
|
||||
if TICI:
|
||||
os.environ.setdefault("DEV", "QCOM")
|
||||
else:
|
||||
os.environ.setdefault("DEV", "CPU")
|
||||
|
||||
import numpy as np
|
||||
from setproctitle import setproctitle
|
||||
|
||||
import iqpilot.cereal.messaging as messaging
|
||||
from iqpilot.cereal import car, log
|
||||
from iqpilot.cereal.messaging import SubMaster
|
||||
from iqpilot.cereal.services import SERVICE_LIST
|
||||
from iqdbc.car.car_helpers import get_demo_car_params
|
||||
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.common.realtime import DT_MDL
|
||||
from iqpilot.common.swaglog import cloudlog
|
||||
from iqpilot.selfdrive.controls.lib.desire_helper import DesireHelper
|
||||
from iqpilot.system import sentry
|
||||
|
||||
from iqpilot.common.steer_delay import lateral_action_delay
|
||||
from iqpilot.selfdrive.iqmodeld.daemon import CalibrationAtlas, CameraIngress, FrameDropMeter
|
||||
from iqpilot.selfdrive.iqmodeld.driving_action import (
|
||||
DESIRE_LEN, LAT_SMOOTH_SECONDS, LONG_SMOOTH_SECONDS, get_action_from_model,
|
||||
)
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_helpers import (
|
||||
download_onnx, download_precompiled, egpu_model_oob_pkl_path, egpu_oob_pkl_path, egpu_pkl_path, egpu_policy_pkl_path, egpu_present_consented,
|
||||
egpu_selected, local_onnx,
|
||||
patch_tinygrad_fetch_fw, quarantine_artifact, resolve_backend, usbgpu_present,
|
||||
)
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_model import resolve_egpu_model
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_pipeline import EgpuOutputInvalid, EgpuPipeline, EgpuPipelineError, make_big_channel_payload
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_telemetry import EgpuDockTelemetry
|
||||
from iqpilot.selfdrive.iqmodeld.messaging import DrivePacketMemory, populate_drive_messages, populate_odometry_message
|
||||
from iqpilot.selfdrive.iqmodeld.metadata import Meta20hz
|
||||
from iqpilot.selfdrive.iqmodeld.model_channel import BIG_CHANNEL, ModelChannel
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_policy import MODEL_FORMAT, POLICY_FORMAT, ModelRunner, PolicyRunner, load_bundle
|
||||
from iqpilot.selfdrive.iqmodeld.model_warp import FrameWarp
|
||||
from iqpilot.selfdrive.iqmodeld.parser import PhaseParser
|
||||
|
||||
PROCESS_NAME = "iqpilot.selfdrive.iqmodeld.iqegpumodeld"
|
||||
|
||||
PRESENCE_POLL_S = 5.0
|
||||
COMPILE_TIMEOUT_S = 3600
|
||||
LINK_UP_TIMEOUT_S = 10.0
|
||||
SETUP_EXIT_AFTER = 3
|
||||
MIN_LOAD_AVAIL_MB = 350
|
||||
MEMORY_WAIT_S = 90.0
|
||||
SETUP_RETRY_BASE_S = 3.0
|
||||
SETUP_RETRY_MAX_S = 30.0
|
||||
MAX_INVALID_STREAK = 20
|
||||
DOCK_MIN_SUPPLY_MV = 5000
|
||||
DOCK_POWER_STABLE_POLLS = 4
|
||||
DOCK_POWER_POLL_S = 0.1
|
||||
DOCK_POWER_TIMEOUT_S = 30.0
|
||||
|
||||
|
||||
def park(reason: str) -> None:
|
||||
cloudlog.warning(f"iqegpumodeld parked: {reason}")
|
||||
params = Params()
|
||||
params.put_bool("UsbGpuFailed", True)
|
||||
params.put("UsbGpuLastError", reason[:512])
|
||||
while True:
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def _wait_for_egpu(params: Params) -> None:
|
||||
while not usbgpu_present():
|
||||
params.put_bool("UsbGpuPresent", False)
|
||||
time.sleep(PRESENCE_POLL_S)
|
||||
params.put_bool("UsbGpuPresent", True)
|
||||
try:
|
||||
from iqpilot.system.hardware.egpu_dock.flash import link_up
|
||||
except Exception:
|
||||
return
|
||||
deadline = time.monotonic() + LINK_UP_TIMEOUT_S
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
if link_up():
|
||||
break
|
||||
except Exception:
|
||||
return
|
||||
time.sleep(0.5)
|
||||
_wait_for_dock_power(params)
|
||||
|
||||
|
||||
def dock_power_ready(reading) -> bool:
|
||||
voltage, _current, fault = reading
|
||||
return int(voltage) >= DOCK_MIN_SUPPLY_MV and not fault
|
||||
|
||||
|
||||
def _wait_for_dock_power(params: Params) -> None:
|
||||
telemetry = EgpuDockTelemetry(None, big=False)
|
||||
deadline = time.monotonic() + DOCK_POWER_TIMEOUT_S
|
||||
stable = 0
|
||||
warned = False
|
||||
try:
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
reading = telemetry._read_ina()
|
||||
except Exception:
|
||||
return
|
||||
if reading is None:
|
||||
return
|
||||
stable = stable + 1 if dock_power_ready(reading) else 0
|
||||
if stable >= DOCK_POWER_STABLE_POLLS:
|
||||
return
|
||||
if stable == 0 and not warned:
|
||||
warned = True
|
||||
cloudlog.warning(f"iqegpumodeld dock supply not ready {reading}; waiting for a stable 5V rail")
|
||||
params.put("UsbGpuLastError", f"dock supply not ready (voltage={reading[0]}mV fault={reading[2]}); waiting")
|
||||
time.sleep(DOCK_POWER_POLL_S)
|
||||
cloudlog.warning("iqegpumodeld dock supply never stabilised; continuing")
|
||||
finally:
|
||||
handle = getattr(telemetry, "_asm_usb", None)
|
||||
if handle is not None:
|
||||
try:
|
||||
handle.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _compile_in_subprocess(meta: dict, onnx_path: str, pkl_path: str, cam_size: tuple[int, int]) -> None:
|
||||
cmd = [sys.executable, "-m", "iqpilot.selfdrive.iqmodeld.tools.compile_egpu_model",
|
||||
"--model", meta["key"], "--onnx", onnx_path, "--output", pkl_path,
|
||||
"--format", str(MODEL_FORMAT), "--camera-resolutions", f"{cam_size[0]}x{cam_size[1]}",
|
||||
"--progress-param", "UsbGpuSetupProgress", "--progress-base", "0.5", "--progress-span", "0.48"]
|
||||
compile_env = {**os.environ, "DEV": "USB+AMD:LLVM", "FLOAT16": "1",
|
||||
"JIT_BATCH_SIZE": "0", "GMMU": "0", "TC_OPT": "2"}
|
||||
proc = subprocess.run(cmd, timeout=COMPILE_TIMEOUT_S, capture_output=True, text=True,
|
||||
env=compile_env, preexec_fn=lambda: os.nice(20))
|
||||
if proc.returncode != 0:
|
||||
tail = (proc.stderr or proc.stdout or "").strip()[-800:]
|
||||
raise RuntimeError(f"eGPU model compile failed (rc={proc.returncode}): {tail}")
|
||||
|
||||
|
||||
_precompiled_tried = False
|
||||
_model_precompiled_tried = False
|
||||
|
||||
|
||||
def _ensure_artifact(params: Params, meta: dict, cam_size: tuple[int, int]) -> str:
|
||||
global _precompiled_tried, _model_precompiled_tried
|
||||
model_path = egpu_model_oob_pkl_path(meta)
|
||||
if os.path.isfile(model_path):
|
||||
return model_path
|
||||
if meta.get("egpu_model_oob_artifact") and not _model_precompiled_tried:
|
||||
_model_precompiled_tried = True
|
||||
params.put_bool("UsbGpuCompiled", False)
|
||||
params.put_bool("UsbGpuReady", False)
|
||||
params.put("UsbGpuSetupProgress", "0.0")
|
||||
model_last = [-1.0]
|
||||
|
||||
def _model_prog(p: float) -> None:
|
||||
if p - model_last[0] >= 0.02 or p >= 1.0:
|
||||
model_last[0] = p
|
||||
params.put("UsbGpuSetupProgress", f"{p:.3f}")
|
||||
|
||||
try:
|
||||
size_mb = int(meta["egpu_model_oob_artifact"].get("size", 0)) / 1e6
|
||||
cloudlog.warning(f"iqegpumodeld downloading precompiled {meta['key']} (warp-on-dock, {size_mb:.0f}MB)")
|
||||
precompiled = download_precompiled(meta, progress_cb=_model_prog, field="egpu_model_oob_artifact")
|
||||
if precompiled is not None:
|
||||
cloudlog.warning(f"iqegpumodeld precompiled ready -> {precompiled}")
|
||||
return precompiled
|
||||
except Exception as e:
|
||||
cloudlog.warning(f"iqegpumodeld warp-on-dock artifact unavailable ({e}); falling back")
|
||||
|
||||
oob_path = egpu_oob_pkl_path(meta)
|
||||
if os.path.isfile(oob_path):
|
||||
return oob_path
|
||||
policy_path = egpu_policy_pkl_path(meta)
|
||||
legacy_path = egpu_pkl_path(meta)
|
||||
|
||||
params.put_bool("UsbGpuCompiled", False)
|
||||
params.put_bool("UsbGpuReady", False)
|
||||
|
||||
if meta.get("egpu_oob_artifact") and not _precompiled_tried:
|
||||
_precompiled_tried = True
|
||||
params.put("UsbGpuSetupProgress", "0.0")
|
||||
oob_last = [-1.0]
|
||||
|
||||
def _oob_prog(p: float) -> None:
|
||||
if p - oob_last[0] >= 0.02 or p >= 1.0:
|
||||
oob_last[0] = p
|
||||
params.put("UsbGpuSetupProgress", f"{p:.3f}")
|
||||
|
||||
try:
|
||||
cloudlog.warning(f"iqegpumodeld downloading precompiled {meta['key']} (streamable) "
|
||||
f"({int(meta['egpu_oob_artifact'].get('size', 0)) / 1e6:.0f}MB)")
|
||||
precompiled = download_precompiled(meta, progress_cb=_oob_prog, oob=True)
|
||||
if precompiled is not None:
|
||||
cloudlog.warning(f"iqegpumodeld precompiled ready -> {precompiled}")
|
||||
return precompiled
|
||||
except Exception as e:
|
||||
cloudlog.warning(f"iqegpumodeld streamable artifact unavailable ({e}); falling back")
|
||||
|
||||
if os.path.isfile(policy_path):
|
||||
return policy_path
|
||||
|
||||
if meta.get("egpu_policy_artifact") and not _precompiled_tried:
|
||||
_precompiled_tried = True
|
||||
params.put("UsbGpuSetupProgress", "0.0")
|
||||
dl_last = [-1.0]
|
||||
|
||||
def _dl_prog(p: float) -> None:
|
||||
if p - dl_last[0] >= 0.02 or p >= 1.0:
|
||||
dl_last[0] = p
|
||||
params.put("UsbGpuSetupProgress", f"{p:.3f}")
|
||||
|
||||
try:
|
||||
cloudlog.warning(f"iqegpumodeld downloading precompiled {meta['key']} policy "
|
||||
f"({int(meta['egpu_policy_artifact'].get('size', 0)) / 1e6:.0f}MB)")
|
||||
precompiled = download_precompiled(meta, progress_cb=_dl_prog, policy=True)
|
||||
if precompiled is not None:
|
||||
cloudlog.warning(f"iqegpumodeld precompiled ready -> {precompiled}")
|
||||
return precompiled
|
||||
except Exception as e:
|
||||
cloudlog.warning(f"iqegpumodeld precompiled policy unavailable ({e}); falling back")
|
||||
|
||||
if os.path.isfile(legacy_path):
|
||||
cloudlog.warning(f"iqegpumodeld using legacy per-tensor artifact {legacy_path}; policy artifact not hosted yet")
|
||||
return legacy_path
|
||||
|
||||
onnx_path = local_onnx(meta)
|
||||
if onnx_path is None:
|
||||
params.put("UsbGpuSetupProgress", "0.0")
|
||||
cloudlog.warning(f"iqegpumodeld downloading {meta['key']} onnx ({meta.get('download', {}).get('size', 0) / 1e6:.0f}MB)")
|
||||
last = [-1.0]
|
||||
|
||||
def _prog(p: float) -> None:
|
||||
if p - last[0] >= 0.02 or p >= 1.0:
|
||||
last[0] = p
|
||||
params.put("UsbGpuSetupProgress", f"{p * 0.5:.3f}")
|
||||
|
||||
onnx_path = download_onnx(meta, progress_cb=_prog)
|
||||
|
||||
cloudlog.warning(f"iqegpumodeld compiling {meta['key']} for USB-AMD ({cam_size[0]}x{cam_size[1]}, one-time, can take minutes)")
|
||||
_compile_in_subprocess(meta, onnx_path, model_path, cam_size)
|
||||
cloudlog.warning(f"iqegpumodeld compiled -> {model_path}")
|
||||
return model_path
|
||||
|
||||
|
||||
def _mem_available_mb() -> int:
|
||||
try:
|
||||
with open("/proc/meminfo") as f:
|
||||
for line in f:
|
||||
if line.startswith("MemAvailable:"):
|
||||
return int(line.split()[1]) // 1024
|
||||
except OSError:
|
||||
pass
|
||||
return 1 << 20
|
||||
|
||||
|
||||
def _wait_for_memory(need_mb: int) -> None:
|
||||
deadline = time.monotonic() + MEMORY_WAIT_S
|
||||
avail = _mem_available_mb()
|
||||
while avail < need_mb and time.monotonic() < deadline:
|
||||
cloudlog.warning(f"iqegpumodeld waiting for memory: {avail}MB available, need {need_mb}MB")
|
||||
time.sleep(5.0)
|
||||
avail = _mem_available_mb()
|
||||
if avail < need_mb:
|
||||
raise RuntimeError(f"insufficient memory to load the dock model: {avail}MB available, need {need_mb}MB")
|
||||
|
||||
|
||||
def _load_infer_fn(pkl_path: str, meta: dict, cam_size: tuple[int, int]):
|
||||
patch_tinygrad_fetch_fw()
|
||||
from tinygrad.tensor import Tensor
|
||||
|
||||
_wait_for_memory(MIN_LOAD_AVAIL_MB)
|
||||
bundle = load_bundle(pkl_path)
|
||||
if bundle.get("model_sha256") != meta["sha256"]:
|
||||
quarantine_artifact(pkl_path, "pkl model sha mismatch")
|
||||
raise RuntimeError(f"artifact model sha {bundle.get('model_sha256')} != {meta['sha256']}")
|
||||
if int(bundle.get("output_len", -1)) != int(meta["output_len"]):
|
||||
quarantine_artifact(pkl_path, "pkl output_len mismatch")
|
||||
raise RuntimeError(f"artifact output_len {bundle.get('output_len')} != {meta['output_len']}")
|
||||
if bundle.get("format") == MODEL_FORMAT:
|
||||
jits = bundle["run_model"]
|
||||
if cam_size not in jits:
|
||||
have = ", ".join(f"{w}x{h}" for w, h in sorted(jits))
|
||||
raise RuntimeError(f"artifact has no warp for the {cam_size[0]}x{cam_size[1]} camera (bundled: {have})")
|
||||
runner = ModelRunner(jits[cam_size], bundle["input_spec"], int(bundle["frame_skip"]), meta["output_slices"]["hidden_state"],
|
||||
bundle.get("input_device", "AMD"), int(bundle["frame_copy_size"][cam_size]))
|
||||
return runner, bundle["input_spec"]
|
||||
if bundle.get("format") == POLICY_FORMAT:
|
||||
runner = PolicyRunner(bundle["run_policy"], bundle["input_spec"], int(bundle["frame_skip"]),
|
||||
meta["output_slices"]["hidden_state"], bundle.get("input_device", "AMD"))
|
||||
return runner, bundle["input_spec"]
|
||||
jit = bundle["run_model"]
|
||||
input_dev = bundle.get("input_device", "AMD")
|
||||
input_spec = bundle["input_spec"]
|
||||
|
||||
def infer(inputs: dict[str, np.ndarray]) -> np.ndarray:
|
||||
tensors = {name: Tensor(np.ascontiguousarray(inputs[name]), device=input_dev).realize()
|
||||
for name in input_spec}
|
||||
out, = jit(**tensors)
|
||||
return out.numpy().reshape(-1)
|
||||
|
||||
return infer, input_spec
|
||||
|
||||
|
||||
def _warmup(infer_fn, input_spec: dict, output_len: int) -> float:
|
||||
zeros = {name: np.zeros(shape, dtype=dtype) for name, (shape, dtype) in input_spec.items()}
|
||||
t0 = time.perf_counter()
|
||||
if isinstance(infer_fn, ModelRunner):
|
||||
n = infer_fn.frame_copy_size
|
||||
eye = np.eye(3, dtype=np.float32)
|
||||
out = infer_fn.run(np.zeros(n, dtype=np.uint8), np.zeros(n, dtype=np.uint8), eye, eye,
|
||||
np.zeros(input_spec["desire_pulse"][0][2], dtype=np.float32), np.zeros(2, dtype=np.float32), np.zeros(2, dtype=np.float32))
|
||||
elif isinstance(infer_fn, PolicyRunner):
|
||||
img = input_spec["img"][0]
|
||||
out = infer_fn.run(np.zeros((2, 6, img[2], img[3]), dtype=np.uint8), np.zeros(input_spec["desire_pulse"][0][2], dtype=np.float32),
|
||||
np.zeros(2, dtype=np.float32), np.zeros(2, dtype=np.float32))
|
||||
else:
|
||||
out = infer_fn(zeros)
|
||||
dt = time.perf_counter() - t0
|
||||
if out.shape[0] != output_len or not np.isfinite(out).all():
|
||||
raise RuntimeError(f"warmup produced invalid output (len={out.shape[0]})")
|
||||
return dt
|
||||
|
||||
|
||||
def main(demo: bool = False) -> None:
|
||||
cloudlog.warning("iqegpumodeld init")
|
||||
sentry.set_tag("daemon", PROCESS_NAME)
|
||||
cloudlog.bind(daemon=PROCESS_NAME)
|
||||
setproctitle(PROCESS_NAME)
|
||||
try:
|
||||
os.sched_setaffinity(0, {4, 5, 6})
|
||||
os.nice(-10)
|
||||
except OSError as e:
|
||||
cloudlog.warning(f"iqegpumodeld affinity/nice failed ({e}); continuing at defaults")
|
||||
|
||||
params = Params()
|
||||
backend = resolve_backend(params.get_bool("IQEmacEnabled"), egpu_selected(params), egpu_present_consented(params))
|
||||
if backend != "egpu":
|
||||
park(f"backend resolution is {backend!r}, not egpu; refusing to own the big channel")
|
||||
|
||||
channel = ModelChannel(BIG_CHANNEL, create=True)
|
||||
|
||||
cloudlog.warning("iqegpumodeld waiting for camerad")
|
||||
cameras = CameraIngress(None)
|
||||
layout = cameras.layout
|
||||
|
||||
_wait_for_egpu(params)
|
||||
params.put_bool("UsbGpuLoading", True)
|
||||
attempt = 0
|
||||
while True:
|
||||
try:
|
||||
meta = resolve_egpu_model(params)
|
||||
if meta is None:
|
||||
raise RuntimeError("selected big model is not in the catalog; check connectivity or pick another model")
|
||||
if meta.get("split"):
|
||||
params.put_bool("UsbGpuLoading", False)
|
||||
park(f"model {meta['key']} needs the Mac backend; the eGPU runs fused models only")
|
||||
cam_size = (int(cameras._primary.width), int(cameras._primary.height))
|
||||
pkl_path = _ensure_artifact(params, meta, cam_size)
|
||||
infer_fn, input_spec = _load_infer_fn(pkl_path, meta, cam_size)
|
||||
warp = None if isinstance(infer_fn, ModelRunner) else FrameWarp(cam_size[0], cam_size[1], meta["frame_skip"])
|
||||
warm_s = _warmup(infer_fn, input_spec, meta["output_len"])
|
||||
break
|
||||
except Exception as e:
|
||||
attempt += 1
|
||||
subs = "; ".join(f"{type(x).__name__}: {x}" for x in (getattr(e, "exceptions", None) or []))
|
||||
params.put("UsbGpuLastError", (f"{e} [{subs}]" if subs else str(e))[:512])
|
||||
cloudlog.warning(f"iqegpumodeld setup attempt {attempt} failed: {e}; {subs}; retrying")
|
||||
if attempt >= SETUP_EXIT_AFTER:
|
||||
# tinygrad keeps the dock's flock in a failed device init, so a stale process can never
|
||||
# reopen it; exit and let the manager respawn a clean one.
|
||||
cloudlog.error(f"iqegpumodeld giving up after {attempt} setup failures; exiting for a clean restart")
|
||||
sys.exit(1)
|
||||
if not usbgpu_present():
|
||||
from iqpilot.system.hardware.usb import ensure_host_role
|
||||
if ensure_host_role():
|
||||
cloudlog.warning("iqegpumodeld: Type-C controller was out of host mode; restored")
|
||||
time.sleep(2.0)
|
||||
_wait_for_egpu(params)
|
||||
time.sleep(min(SETUP_RETRY_MAX_S, SETUP_RETRY_BASE_S * attempt))
|
||||
|
||||
params.put_bool("UsbGpuLoading", False)
|
||||
params.put_bool("UsbGpuCompiled", True)
|
||||
params.put_bool("UsbGpuReady", True)
|
||||
params.put("UsbGpuSetupProgress", "1.0")
|
||||
cloudlog.warning(f"iqegpumodeld model: {meta['key']} ({meta['model_name']})")
|
||||
cloudlog.warning(f"iqegpumodeld model up (warmup {warm_s * 1e3:.0f}ms, {'warp on dock' if warp is None else 'warp on device'})")
|
||||
|
||||
pipeline = EgpuPipeline(meta, infer_fn)
|
||||
telemetry_pm = messaging.PubMaster(["egpuDockState"])
|
||||
telemetry = EgpuDockTelemetry(telemetry_pm, big=True)
|
||||
telemetry_every = max(1, round((1.0 / DT_MDL) / SERVICE_LIST["egpuDockState"].frequency))
|
||||
|
||||
sub = SubMaster(["deviceState", "carState", "roadCameraState", "extrinsicsCalibration",
|
||||
"driverMonitoringState", "carControl", "lateralDelay", "iqNavState", "radarState"])
|
||||
if demo:
|
||||
CP = get_demo_car_params()
|
||||
else:
|
||||
CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams)
|
||||
long_delay = CP.longitudinalActuatorDelay + LONG_SMOOTH_SECONDS
|
||||
|
||||
parser = PhaseParser()
|
||||
memory = DrivePacketMemory()
|
||||
desire_logic = DesireHelper()
|
||||
frame_meter = FrameDropMeter(20.0)
|
||||
warps = CalibrationAtlas()
|
||||
prev_action = log.ModelDataV2.Action()
|
||||
slices = {k: v for k, v in meta["output_slices"].items() if k != "pad"}
|
||||
|
||||
produced = 0
|
||||
stats: dict[str, list[float]] = {k: [] for k in ("pull", "warp", "infer", "publish", "loop")}
|
||||
iter_count = 0
|
||||
skip_count = 0
|
||||
invalid_streak = 0
|
||||
last_pulled_fid = -1
|
||||
last_frame_mono = time.monotonic()
|
||||
t_loop = time.perf_counter()
|
||||
cloudlog.warning("iqegpumodeld starting")
|
||||
|
||||
while True:
|
||||
frame_pair = cameras.pull()
|
||||
t_pull = time.perf_counter()
|
||||
if frame_pair is None:
|
||||
if time.monotonic() - last_frame_mono > 2.0:
|
||||
cloudlog.warning("iqegpumodeld camera stream silent >2s; reconnecting VisionIPC")
|
||||
cameras = CameraIngress(None)
|
||||
last_frame_mono = time.monotonic()
|
||||
continue
|
||||
last_frame_mono = time.monotonic()
|
||||
main_buf, extra_buf, main_stamp, extra_stamp = frame_pair
|
||||
|
||||
stats["pull"].append(t_pull - t_loop)
|
||||
stats["loop"].append(time.perf_counter() - t_loop)
|
||||
t_loop = time.perf_counter()
|
||||
if last_pulled_fid >= 0 and main_stamp.frame_id > last_pulled_fid + 1:
|
||||
skip_count += main_stamp.frame_id - last_pulled_fid - 1
|
||||
last_pulled_fid = main_stamp.frame_id
|
||||
iter_count += 1
|
||||
if iter_count % 200 == 0:
|
||||
pcts = {k: {"p50": round(sorted(v)[len(v) // 2] * 1e3, 1),
|
||||
"p90": round(sorted(v)[int(len(v) * 0.9)] * 1e3, 1)}
|
||||
for k, v in stats.items() if v}
|
||||
cloudlog.event("iqegpu_stats", **pcts, cam_skips=skip_count, window=iter_count)
|
||||
msg = " ".join(f"{k}=p50:{v['p50']:.0f}/p90:{v['p90']:.0f}ms" for k, v in pcts.items())
|
||||
cloudlog.warning(f"iqegpumodeld stages: {msg} cam_skips={skip_count} over {iter_count}")
|
||||
for v in stats.values():
|
||||
v.clear()
|
||||
skip_count = 0
|
||||
|
||||
sub.update(0)
|
||||
|
||||
v_ego = max(sub["carState"].vEgo, 0.0)
|
||||
lat_delay = lateral_action_delay(params, CP, sub["lateralDelay"].lateralDelay) + LAT_SMOOTH_SECONDS
|
||||
main_tfm, extra_tfm, live_calib_seen = warps.refresh(sub, layout.main_is_wide, layout.dual_camera)
|
||||
dropped_frames, frame_drop_ratio, _ = frame_meter.sample(main_stamp.frame_id)
|
||||
|
||||
traffic = np.zeros(2, dtype=np.float32)
|
||||
traffic[int(sub["driverMonitoringState"].isRHD)] = 1
|
||||
desire_vec = np.zeros(DESIRE_LEN, dtype=np.float32)
|
||||
if 0 <= desire_logic.desire < DESIRE_LEN:
|
||||
desire_vec[desire_logic.desire] = 1
|
||||
|
||||
frame_delay = DT_MDL
|
||||
action_delay = DT_MDL / 2
|
||||
lat_action_t = lat_delay + frame_delay + action_delay
|
||||
long_action_t = long_delay + frame_delay + action_delay
|
||||
action_t = np.array([lat_action_t, long_action_t], dtype=np.float32)
|
||||
|
||||
started_at = time.perf_counter()
|
||||
t_warp = started_at
|
||||
try:
|
||||
if warp is None:
|
||||
output = pipeline.run_frames(main_buf.data, extra_buf.data, main_tfm, extra_tfm, desire_vec, traffic, action_t)
|
||||
else:
|
||||
try:
|
||||
warped = warp.run(main_buf, extra_buf, main_tfm, extra_tfm)
|
||||
except Exception as e:
|
||||
park(f"warp run failed: {e}")
|
||||
t_warp = time.perf_counter()
|
||||
output = pipeline.run(warped, desire_vec, traffic, action_t)
|
||||
except EgpuOutputInvalid as e:
|
||||
invalid_streak += 1
|
||||
if invalid_streak == 1 or invalid_streak % MAX_INVALID_STREAK == 0:
|
||||
cloudlog.warning(f"iqegpumodeld dropping frame {main_stamp.frame_id}: {e} (streak {invalid_streak})")
|
||||
if invalid_streak >= MAX_INVALID_STREAK:
|
||||
params.put("UsbGpuLastError", f"{e} for {invalid_streak} consecutive frames"[:512])
|
||||
cloudlog.error(f"iqegpumodeld output invalid for {invalid_streak} frames; exiting for a clean restart")
|
||||
sys.exit(1)
|
||||
frame_meter.commit(main_stamp.frame_id)
|
||||
continue
|
||||
except EgpuPipelineError as e:
|
||||
park(str(e))
|
||||
except Exception as e:
|
||||
park(f"eGPU inference failed: {e}")
|
||||
invalid_streak = 0
|
||||
t_infer = time.perf_counter()
|
||||
stats["warp"].append(t_warp - started_at)
|
||||
stats["infer"].append(t_infer - t_warp)
|
||||
|
||||
execution_time = time.perf_counter() - started_at
|
||||
sliced = {k: output[np.newaxis, sl] for k, sl in slices.items()}
|
||||
outputs = parser.parse_vision_outputs(sliced)
|
||||
|
||||
action = get_action_from_model(outputs, prev_action, v_ego, float(lat_action_t), float(long_action_t),
|
||||
lat_smooth_seconds=meta.get("lat_smooth_seconds"))
|
||||
prev_action = action
|
||||
|
||||
model_msg = messaging.new_message("modelV2")
|
||||
driving_msg = messaging.new_message("drivingModelData")
|
||||
pose_msg = messaging.new_message("cameraOdometry")
|
||||
iq_msg = messaging.new_message("iqDriveModelData")
|
||||
|
||||
populate_drive_messages(
|
||||
driving_msg, model_msg, outputs, action, memory,
|
||||
main_stamp.frame_id, extra_stamp.frame_id, sub["roadCameraState"].frameId,
|
||||
frame_drop_ratio, main_stamp.timestamp_eof, execution_time,
|
||||
live_calib_seen, Meta20hz,
|
||||
)
|
||||
|
||||
model_msg.modelV2.big = True
|
||||
driving_msg.drivingModelData.big = True
|
||||
|
||||
desire_state = model_msg.modelV2.meta.desireState
|
||||
lane_change_prob = desire_state[log.Desire.laneChangeLeft] + desire_state[log.Desire.laneChangeRight]
|
||||
desire_logic.update(sub["carState"], sub["carControl"].latActive, lane_change_prob,
|
||||
sub["iqNavState"], model_msg.modelV2, sub["radarState"])
|
||||
model_msg.modelV2.meta.laneChangeState = desire_logic.lane_change_state
|
||||
model_msg.modelV2.meta.laneChangeDirection = desire_logic.lane_change_direction
|
||||
driving_msg.drivingModelData.meta.laneChangeState = desire_logic.lane_change_state
|
||||
driving_msg.drivingModelData.meta.laneChangeDirection = desire_logic.lane_change_direction
|
||||
iq_msg.iqDriveModelData.turnSignalDirection = desire_logic.lane_turn_direction
|
||||
|
||||
populate_odometry_message(pose_msg, outputs, main_stamp.frame_id, dropped_frames,
|
||||
main_stamp.timestamp_eof, live_calib_seen)
|
||||
|
||||
channel.write(main_stamp.frame_id, make_big_channel_payload(
|
||||
main_stamp.frame_id, live_calib_seen, execution_time, (t_infer - t_warp) * 1e3, {
|
||||
"modelV2": model_msg.to_bytes(),
|
||||
"drivingModelData": driving_msg.to_bytes(),
|
||||
"cameraOdometry": pose_msg.to_bytes(),
|
||||
"iqDriveModelData": iq_msg.to_bytes(),
|
||||
}))
|
||||
stats["publish"].append(time.perf_counter() - t_infer)
|
||||
produced += 1
|
||||
if produced == 1 or produced % 100 == 0:
|
||||
infer_ms = (t_infer - t_warp) * 1e3
|
||||
cloudlog.warning(f"iqegpumodeld producing: frame={main_stamp.frame_id} total={execution_time * 1e3:.0f}ms infer={infer_ms:.0f}ms count={produced}")
|
||||
|
||||
if produced % telemetry_every == 0:
|
||||
telemetry.send()
|
||||
|
||||
frame_meter.commit(main_stamp.frame_id)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
import argparse
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--demo", action="store_true")
|
||||
args = ap.parse_args()
|
||||
main(demo=args.demo)
|
||||
except KeyboardInterrupt:
|
||||
cloudlog.warning("iqegpumodeld got SIGINT")
|
||||
except Exception:
|
||||
import traceback
|
||||
sentry.capture_exception()
|
||||
cloudlog.exception("iqegpumodeld crashed, parking")
|
||||
park(f"crashed: {traceback.format_exc(limit=8)}")
|
||||
@@ -6,10 +6,10 @@ from dataclasses import dataclass, field
|
||||
import capnp
|
||||
import numpy as np
|
||||
|
||||
from cereal import log
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.models.helpers import plan_x_idxs_helper
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.config import ModelConstants, Plan
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import get_curvature_from_plan
|
||||
from iqpilot.cereal import log
|
||||
from iqpilot.selfdrive.iqmodeld.models.helpers import plan_x_idxs_helper
|
||||
from iqpilot.selfdrive.iqmodeld.config import ModelConstants, Plan
|
||||
from iqpilot.selfdrive.controls.lib.drive_helpers import get_curvature_from_plan
|
||||
|
||||
SEND_RAW_PRED = os.getenv("SEND_RAW_PRED")
|
||||
ConfidenceClass = log.ModelDataV2.ConfidenceClass
|
||||
@@ -67,7 +67,7 @@ def _assign_xyva(builder, t_points, x_track, y_track, v_track, a_track,
|
||||
builder.aStd = a_std.tolist()
|
||||
|
||||
|
||||
def _fit_path(builder, degree: int, x_track: np.ndarray, y_track: np.ndarray, z_track: np.ndarray) -> None:
|
||||
def fill_xyz_poly(builder, degree: int, x_track: np.ndarray, y_track: np.ndarray, z_track: np.ndarray) -> None:
|
||||
stacked = np.stack([x_track, y_track, z_track], axis=1)
|
||||
coeffs = np.polynomial.polynomial.polyfit(ModelConstants.T_IDXS, stacked, deg=degree)
|
||||
builder.xCoefficients = coeffs[:, 0].tolist()
|
||||
@@ -75,7 +75,7 @@ def _fit_path(builder, degree: int, x_track: np.ndarray, y_track: np.ndarray, z_
|
||||
builder.zCoefficients = coeffs[:, 2].tolist()
|
||||
|
||||
|
||||
def _lane_snapshot(builder, lane_lines, lane_probs: list[float]) -> None:
|
||||
def fill_lane_line_meta(builder, lane_lines, lane_probs: list[float]) -> None:
|
||||
builder.leftY = lane_lines[1].y[0]
|
||||
builder.leftProb = lane_probs[1]
|
||||
builder.rightY = lane_lines[2].y[0]
|
||||
@@ -123,7 +123,7 @@ def _write_plan_family(model_packet, driving_packet, outputs: dict[str, np.ndarr
|
||||
_assign_xyz(model_packet.acceleration, ModelConstants.T_IDXS, *plan_rows[:, Plan.ACCELERATION].T)
|
||||
_assign_xyz(model_packet.orientation, ModelConstants.T_IDXS, *plan_rows[:, Plan.T_FROM_CURRENT_EULER].T)
|
||||
_assign_xyz(model_packet.orientationRate, ModelConstants.T_IDXS, *plan_rows[:, Plan.ORIENTATION_RATE].T)
|
||||
_fit_path(driving_packet.path, ModelConstants.POLY_PATH_DEGREE, *plan_rows[:, Plan.POSITION].T)
|
||||
fill_xyz_poly(driving_packet.path, ModelConstants.POLY_PATH_DEGREE, *plan_rows[:, Plan.POSITION].T)
|
||||
|
||||
|
||||
def _write_temporal_pose(model_packet, outputs: dict[str, np.ndarray]) -> None:
|
||||
@@ -156,7 +156,7 @@ def _write_lane_family(model_packet, driving_packet, outputs: dict[str, np.ndarr
|
||||
)
|
||||
model_packet.laneLineStds = outputs["lane_lines_stds"][0, :, 0, 0].tolist()
|
||||
model_packet.laneLineProbs = outputs["lane_lines_prob"][0, 1::2].tolist()
|
||||
_lane_snapshot(driving_packet.laneLineMeta, model_packet.laneLines, model_packet.laneLineProbs)
|
||||
fill_lane_line_meta(driving_packet.laneLineMeta, model_packet.laneLines, model_packet.laneLineProbs)
|
||||
|
||||
model_packet.init("roadEdges", 2)
|
||||
for edge_idx in range(2):
|
||||
@@ -248,16 +248,19 @@ def populate_drive_messages(primary_msg: capnp._DynamicStructBuilder, extended_m
|
||||
def populate_odometry_message(msg: capnp._DynamicStructBuilder, outputs: dict[str, np.ndarray],
|
||||
vipc_frame_id: int, vipc_dropped_frames: int,
|
||||
timestamp_eof: int, live_calib_seen: bool) -> None:
|
||||
msg.valid = live_calib_seen & (vipc_dropped_frames < 1)
|
||||
pose = outputs["pose"][0, :6]
|
||||
pose_stds = outputs["pose_stds"][0, :6]
|
||||
pose_finite = bool(np.isfinite(pose).all() and np.isfinite(pose_stds).all())
|
||||
msg.valid = live_calib_seen & (vipc_dropped_frames < 1) & pose_finite
|
||||
odo = msg.cameraOdometry
|
||||
odo.frameId = vipc_frame_id
|
||||
odo.timestampEof = timestamp_eof
|
||||
odo.trans = outputs["pose"][0, :3].tolist()
|
||||
odo.rot = outputs["pose"][0, 3:].tolist()
|
||||
odo.trans = pose[:3].tolist()
|
||||
odo.rot = pose[3:6].tolist()
|
||||
odo.wideFromDeviceEuler = outputs["wide_from_device_euler"][0, :].tolist()
|
||||
odo.roadTransformTrans = outputs["road_transform"][0, :3].tolist()
|
||||
odo.transStd = outputs["pose_stds"][0, :3].tolist()
|
||||
odo.rotStd = outputs["pose_stds"][0, 3:].tolist()
|
||||
odo.transStd = pose_stds[:3].tolist()
|
||||
odo.rotStd = pose_stds[3:6].tolist()
|
||||
odo.wideFromDeviceEulerStd = outputs["wide_from_device_euler_stds"][0, :].tolist()
|
||||
odo.roadTransformTransStd = outputs["road_transform_stds"][0, :3].tolist()
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ import sys
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
from cereal import custom
|
||||
from iqpilot.cereal import custom
|
||||
from tinygrad.nn.onnx import OnnxPBParser
|
||||
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.models.helpers import get_active_bundle
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.config import Meta
|
||||
from iqpilot.selfdrive.iqmodeld.models.helpers import get_active_bundle
|
||||
from iqpilot.selfdrive.iqmodeld.config import Meta
|
||||
|
||||
|
||||
ModelBundle = custom.IQModelManager.ModelBundle
|
||||
|
||||
211
iqpilot/selfdrive/iqmodeld/model_bundle_downloader.py
Normal file
211
iqpilot/selfdrive/iqmodeld/model_bundle_downloader.py
Normal file
@@ -0,0 +1,211 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
|
||||
MODELS_BASE_URLS = (
|
||||
"https://git.konn3kt.com/teal/IQModels/raw/branch/main",
|
||||
"https://gitlvb.teallvbs.xyz/teal/IQModels/raw/branch/main",
|
||||
)
|
||||
CHUNK = 4 * 1024 * 1024
|
||||
HTTP_TIMEOUT_S = 60.0
|
||||
STREAM_RETRIES = 6
|
||||
|
||||
|
||||
def _requests_auth():
|
||||
import importlib
|
||||
for mod in ("iqpilot_private.models.git_auth", "iqpilot.models_private_src.git_auth",
|
||||
"iqpilot.selfdrive.iqmodeld.models.git_auth"):
|
||||
try:
|
||||
return importlib.import_module(mod).get_requests_auth()
|
||||
except Exception:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def _hf():
|
||||
import importlib
|
||||
for mod in ("iqpilot_private.models.git_auth", "iqpilot.selfdrive.iqmodeld.models.git_auth"):
|
||||
try:
|
||||
m = importlib.import_module(mod)
|
||||
return m.get_hf_headers(), m.hf_resolve_url
|
||||
except Exception:
|
||||
continue
|
||||
return None, None
|
||||
|
||||
|
||||
def download_hf_file(hf_path: str, dst: str, sha256: str, size: int, progress_cb=None) -> str:
|
||||
import requests
|
||||
headers, resolve = _hf()
|
||||
if resolve is None:
|
||||
raise RuntimeError("no HF credentials available")
|
||||
url = resolve(hf_path)
|
||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||
tmp = dst + ".hfpart"
|
||||
last_error: Exception | None = None
|
||||
for _attempt in range(STREAM_RETRIES):
|
||||
try:
|
||||
have = os.path.getsize(tmp) if os.path.isfile(tmp) else 0
|
||||
if size and have > size:
|
||||
os.remove(tmp)
|
||||
have = 0
|
||||
if not size or have < size:
|
||||
req_headers = dict(headers)
|
||||
if have:
|
||||
req_headers["Range"] = f"bytes={have}-"
|
||||
with requests.get(url, headers=req_headers, stream=True, timeout=HTTP_TIMEOUT_S, allow_redirects=True) as r:
|
||||
r.raise_for_status()
|
||||
if have and r.status_code != 206:
|
||||
have = 0
|
||||
with open(tmp, "ab" if have else "wb") as f:
|
||||
got = have
|
||||
for chunk in r.iter_content(CHUNK):
|
||||
f.write(chunk)
|
||||
got += len(chunk)
|
||||
if progress_cb is not None and size:
|
||||
progress_cb(min(1.0, got / size))
|
||||
digest = hashlib.sha256()
|
||||
with open(tmp, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(CHUNK), b""):
|
||||
digest.update(chunk)
|
||||
if size and os.path.getsize(tmp) != size:
|
||||
raise RuntimeError(f"size mismatch: {os.path.getsize(tmp)}/{size} bytes")
|
||||
if sha256 and digest.hexdigest() != sha256:
|
||||
os.remove(tmp)
|
||||
raise RuntimeError("sha256 mismatch")
|
||||
os.replace(tmp, dst)
|
||||
return dst
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
raise RuntimeError(f"HF download failed: {last_error}")
|
||||
|
||||
|
||||
def _lfs_endpoint(base_url: str) -> str:
|
||||
return base_url.split("/raw/", 1)[0] + ".git/info/lfs"
|
||||
|
||||
|
||||
def _resolve_oid(session, base_url: str, oid: str, size: int, auth):
|
||||
import requests
|
||||
batch = session.post(f"{_lfs_endpoint(base_url)}/objects/batch",
|
||||
data=json.dumps({"operation": "download", "transfers": ["basic"],
|
||||
"objects": [{"oid": oid, "size": size}]}),
|
||||
headers={"Content-Type": "application/vnd.git-lfs+json",
|
||||
"Accept": "application/vnd.git-lfs+json"},
|
||||
auth=auth, timeout=HTTP_TIMEOUT_S)
|
||||
batch.raise_for_status()
|
||||
entry = batch.json()["objects"][0]
|
||||
if "actions" not in entry:
|
||||
raise requests.RequestException(f"LFS object unavailable: {entry.get('error', oid)}")
|
||||
action = entry["actions"]["download"]
|
||||
return action["href"], action.get("header", {})
|
||||
|
||||
|
||||
def _part_path(dst: str, oid: str) -> str:
|
||||
return os.path.join(dst + ".parts", oid)
|
||||
|
||||
|
||||
def _part_complete(path: str, oid: str, size: int) -> bool:
|
||||
if not os.path.isfile(path) or os.path.getsize(path) != size:
|
||||
return False
|
||||
digest = hashlib.sha256()
|
||||
with open(path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(CHUNK), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest() == oid
|
||||
|
||||
|
||||
def _fetch_part(session, base_url: str, obj: dict, path: str, auth, progress) -> None:
|
||||
size = int(obj["size"])
|
||||
have = os.path.getsize(path) if os.path.isfile(path) else 0
|
||||
if have > size:
|
||||
os.remove(path)
|
||||
have = 0
|
||||
href, headers = _resolve_oid(session, base_url, obj["oid"], size, auth)
|
||||
obj_auth = None if headers.get("Authorization") else auth
|
||||
# LFS parts are content-addressed (oid == sha256), so a half-written part can be resumed with a
|
||||
# Range request and verified afterwards instead of being thrown away on every restart.
|
||||
if have:
|
||||
headers = {**headers, "Range": f"bytes={have}-"}
|
||||
with session.get(href, headers=headers, stream=True, timeout=HTTP_TIMEOUT_S, auth=obj_auth) as r:
|
||||
r.raise_for_status()
|
||||
if have and r.status_code != 206:
|
||||
have = 0
|
||||
with open(path, "ab" if have else "wb") as f:
|
||||
for chunk in r.iter_content(CHUNK):
|
||||
f.write(chunk)
|
||||
progress(len(chunk))
|
||||
|
||||
|
||||
def download_lfs_bundle(objects: list, dst: str, sha256: str, size: int, progress_cb=None) -> str:
|
||||
import requests
|
||||
auth = _requests_auth()
|
||||
session = requests.Session()
|
||||
os.makedirs(dst + ".parts", exist_ok=True)
|
||||
total = int(size) or sum(int(o["size"]) for o in objects)
|
||||
done_bytes = sum(int(o["size"]) for o in objects if _part_complete(_part_path(dst, o["oid"]), o["oid"], int(o["size"])))
|
||||
got = [done_bytes]
|
||||
|
||||
def progress(n: int) -> None:
|
||||
got[0] += n
|
||||
if progress_cb is not None and total:
|
||||
progress_cb(min(1.0, got[0] / total))
|
||||
|
||||
last_error: Exception | None = None
|
||||
for base_url in MODELS_BASE_URLS:
|
||||
for _attempt in range(STREAM_RETRIES):
|
||||
try:
|
||||
for obj in objects:
|
||||
path = _part_path(dst, obj["oid"])
|
||||
if _part_complete(path, obj["oid"], int(obj["size"])):
|
||||
continue
|
||||
got[0] = done_bytes
|
||||
_fetch_part(session, base_url, obj, path, auth, progress)
|
||||
if not _part_complete(path, obj["oid"], int(obj["size"])):
|
||||
if os.path.getsize(path) >= int(obj["size"]):
|
||||
os.remove(path)
|
||||
raise RuntimeError(f"part {obj['oid'][:12]} incomplete or failed verification")
|
||||
done_bytes += int(obj["size"])
|
||||
got[0] = done_bytes
|
||||
break
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
else:
|
||||
continue
|
||||
break
|
||||
else:
|
||||
raise RuntimeError(f"model bundle download failed: {last_error}")
|
||||
|
||||
tmp = dst + ".part"
|
||||
digest = hashlib.sha256()
|
||||
with open(tmp, "wb") as out:
|
||||
for obj in objects:
|
||||
with open(_part_path(dst, obj["oid"]), "rb") as f:
|
||||
for chunk in iter(lambda: f.read(CHUNK), b""):
|
||||
out.write(chunk)
|
||||
digest.update(chunk)
|
||||
if total and os.path.getsize(tmp) != total:
|
||||
os.remove(tmp)
|
||||
raise RuntimeError(f"size mismatch: {os.path.getsize(tmp) if os.path.exists(tmp) else 0}/{total} bytes")
|
||||
if sha256 and digest.hexdigest() != sha256:
|
||||
os.remove(tmp)
|
||||
for obj in objects:
|
||||
try:
|
||||
os.remove(_part_path(dst, obj["oid"]))
|
||||
except OSError:
|
||||
pass
|
||||
raise RuntimeError("sha256 mismatch")
|
||||
os.replace(tmp, dst)
|
||||
for obj in objects:
|
||||
try:
|
||||
os.remove(_part_path(dst, obj["oid"]))
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
os.rmdir(dst + ".parts")
|
||||
except OSError:
|
||||
pass
|
||||
return dst
|
||||
58
iqpilot/selfdrive/iqmodeld/model_channel.py
Normal file
58
iqpilot/selfdrive/iqmodeld/model_channel.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import mmap
|
||||
import os
|
||||
import pickle
|
||||
import struct
|
||||
|
||||
from iqpilot.common.swaglog import cloudlog
|
||||
|
||||
SMALL_CHANNEL = "/dev/shm/iqpilot_smallmodel"
|
||||
BIG_CHANNEL = "/dev/shm/iqpilot_bigmodel"
|
||||
SHM_SIZE = 8 * 1024 * 1024
|
||||
HEADER = struct.Struct("<QqQ")
|
||||
|
||||
|
||||
class ModelChannel:
|
||||
def __init__(self, path: str, create: bool):
|
||||
if create:
|
||||
fd = os.open(path, os.O_CREAT | os.O_RDWR, 0o600)
|
||||
os.ftruncate(fd, SHM_SIZE)
|
||||
else:
|
||||
fd = os.open(path, os.O_RDWR)
|
||||
self.mm = mmap.mmap(fd, SHM_SIZE)
|
||||
os.close(fd)
|
||||
if create:
|
||||
self.mm[:HEADER.size] = HEADER.pack(0, -1, 0)
|
||||
|
||||
def write(self, frame_id: int, payload: dict) -> None:
|
||||
data = pickle.dumps(payload, protocol=pickle.HIGHEST_PROTOCOL)
|
||||
if HEADER.size + len(data) > SHM_SIZE:
|
||||
cloudlog.error(f"model payload {len(data)} bytes exceeds shm {SHM_SIZE}, dropping frame {frame_id}")
|
||||
return
|
||||
seq = HEADER.unpack(self.mm[:HEADER.size])[0]
|
||||
HEADER.pack_into(self.mm, 0, seq + 1, frame_id, len(data))
|
||||
self.mm[HEADER.size:HEADER.size + len(data)] = data
|
||||
HEADER.pack_into(self.mm, 0, seq + 2, frame_id, len(data))
|
||||
|
||||
def peek_frame_id(self) -> int | None:
|
||||
seq, frame_id, length = HEADER.unpack(self.mm[:HEADER.size])
|
||||
if seq == 0 or seq % 2 != 0 or length == 0:
|
||||
return None
|
||||
return frame_id
|
||||
|
||||
def read(self) -> tuple[int, dict] | None:
|
||||
seq1, frame_id, length = HEADER.unpack(self.mm[:HEADER.size])
|
||||
if seq1 == 0 or seq1 % 2 != 0 or length == 0:
|
||||
return None
|
||||
data = bytes(self.mm[HEADER.size:HEADER.size + length])
|
||||
seq2 = HEADER.unpack(self.mm[:HEADER.size])[0]
|
||||
if seq1 != seq2:
|
||||
return None
|
||||
try:
|
||||
return frame_id, pickle.loads(data)
|
||||
except Exception:
|
||||
return None
|
||||
80
iqpilot/selfdrive/iqmodeld/model_warp.py
Normal file
80
iqpilot/selfdrive/iqmodeld/model_warp.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import pickle
|
||||
|
||||
import numpy as np
|
||||
|
||||
from iqpilot.common.swaglog import cloudlog
|
||||
from iqpilot.system.hardware.hw import Paths
|
||||
|
||||
|
||||
def _load_bundle(pkl_path: str, cam_w: int, cam_h: int, frame_skip: int) -> dict:
|
||||
with open(pkl_path, "rb") as f:
|
||||
bundle = pickle.load(f)
|
||||
if bundle.get("frame_skip") != frame_skip:
|
||||
raise RuntimeError(f"frame_skip {bundle.get('frame_skip')} != {frame_skip}")
|
||||
if (cam_w, cam_h) not in bundle:
|
||||
raise RuntimeError(f"missing {cam_w}x{cam_h}; has {[k for k in bundle if isinstance(k, tuple)]}")
|
||||
_verify_selftest(bundle, cam_w, cam_h)
|
||||
return bundle
|
||||
|
||||
|
||||
def _verify_selftest(bundle: dict, cam_w: int, cam_h: int) -> None:
|
||||
want = bundle.get("selftest")
|
||||
if not want:
|
||||
raise RuntimeError("warp artifact predates the self-test; recompiling")
|
||||
from iqpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||
from iqpilot.selfdrive.iqmodeld.tools.compile_warp import selftest_digest
|
||||
nv12_size = get_nv12_info(cam_w, cam_h)[3]
|
||||
got = selftest_digest(bundle[(cam_w, cam_h)], cam_w, cam_h, nv12_size)
|
||||
if got != want:
|
||||
raise RuntimeError(f"warp self-test {got[:12]} != {want[:12]}; artifact computes differently here")
|
||||
|
||||
|
||||
class FrameWarp:
|
||||
|
||||
def __init__(self, cam_w: int, cam_h: int, frame_skip: int):
|
||||
from tinygrad.tensor import Tensor
|
||||
|
||||
pkl_path = os.path.join(Paths.model_root(), f"emac_warp_{cam_w}x{cam_h}_tinygrad.pkl")
|
||||
bundle = None
|
||||
if os.path.isfile(pkl_path):
|
||||
try:
|
||||
bundle = _load_bundle(pkl_path, cam_w, cam_h, frame_skip)
|
||||
except Exception as e:
|
||||
cloudlog.warning(f"warp artifact unusable ({e}); discarding and recompiling")
|
||||
os.remove(pkl_path)
|
||||
if bundle is None:
|
||||
cloudlog.warning(f"warp artifact missing; compiling for {cam_w}x{cam_h} (one-time)")
|
||||
from iqpilot.selfdrive.iqmodeld.tools.compile_warp import compile_warp
|
||||
compile_warp(cam_w, cam_h, pkl_path, frame_skip=frame_skip)
|
||||
cloudlog.warning(f"warp compiled -> {pkl_path}")
|
||||
bundle = _load_bundle(pkl_path, cam_w, cam_h, frame_skip)
|
||||
self._jit = bundle[(cam_w, cam_h)]
|
||||
|
||||
self._npy = {"tfm": np.zeros((3, 3), dtype=np.float32), "big_tfm": np.zeros((3, 3), dtype=np.float32)}
|
||||
self._tensors = {k: Tensor(v, device="NPY").realize() for k, v in self._npy.items()}
|
||||
self._blob_cache: dict[tuple[str, int], object] = {}
|
||||
self._Tensor = Tensor
|
||||
|
||||
def _frame_tensor(self, key: str, buf):
|
||||
from tinygrad.device import Device
|
||||
arr = np.frombuffer(buf.data, dtype=np.uint8)
|
||||
ck = (key, arr.ctypes.data)
|
||||
t = self._blob_cache.get(ck)
|
||||
if t is None:
|
||||
t = self._Tensor.from_blob(arr.ctypes.data, (arr.size,), dtype="uint8", device=Device.DEFAULT)
|
||||
self._blob_cache[ck] = t
|
||||
return t
|
||||
|
||||
def run(self, main_buf, extra_buf, main_tfm: np.ndarray, extra_tfm: np.ndarray) -> np.ndarray:
|
||||
self._npy["tfm"][:] = main_tfm
|
||||
self._npy["big_tfm"][:] = extra_tfm
|
||||
warped = self._jit(tfm=self._tensors["tfm"], big_tfm=self._tensors["big_tfm"],
|
||||
frame=self._frame_tensor("img", main_buf),
|
||||
big_frame=self._frame_tensor("big_img", extra_buf))
|
||||
return warped.numpy().astype(np.uint8, copy=False)
|
||||
380
iqpilot/selfdrive/iqmodeld/modeld_selector.py
Normal file
380
iqpilot/selfdrive/iqmodeld/modeld_selector.py
Normal file
@@ -0,0 +1,380 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from collections import deque
|
||||
|
||||
from iqpilot.cereal.messaging import PubMaster, log_from_bytes
|
||||
from setproctitle import setproctitle
|
||||
|
||||
from iqpilot.common.filter_simple import FirstOrderFilter
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.common.realtime import config_realtime_process
|
||||
from iqpilot.common.swaglog import cloudlog
|
||||
from iqpilot.selfdrive.iqmodeld.model_channel import BIG_CHANNEL, SMALL_CHANNEL, ModelChannel
|
||||
|
||||
PROCESS_NAME = "iqpilot.selfdrive.iqmodeld.modeld_selector"
|
||||
|
||||
BIG_MODEL_DEADLINE = float(os.getenv("IQEMAC_BIG_DEADLINE_MS", "45")) / 1000.0
|
||||
BIG_MAX_LAG_FRAMES = int(os.getenv("IQEMAC_MAX_BIG_LAG_FRAMES", "6"))
|
||||
BIG_FUTURE_ACCEPT = int(os.getenv("IQEMAC_BIG_FUTURE_ACCEPT", "2"))
|
||||
BIG_ANCHOR_MS = float(os.getenv("IQEMAC_BIG_ANCHOR_MS", "90"))
|
||||
BIG_WAIT_FLOOR_S = 0.002
|
||||
BIG_WAIT_CEIL_S = float(os.getenv("IQEMAC_BIG_WAIT_CEIL_MS", "58")) / 1000.0
|
||||
BIG_MISS_LIMIT = int(os.getenv("IQEMAC_BIG_MISS_LIMIT", "80"))
|
||||
ACTIVATE_WINDOW = int(os.getenv("IQEMAC_ACTIVATE_WINDOW", "50"))
|
||||
ACTIVATE_FRAC = float(os.getenv("IQEMAC_ACTIVATE_FRAC", "0.7"))
|
||||
REARM_LIMIT = int(os.getenv("IQEMAC_REARM_LIMIT", "2"))
|
||||
MODEL_FREQ = 20.0
|
||||
WARMUP_FRAMES = 40
|
||||
STATUS_WINDOW = int(os.getenv("IQEMAC_STATUS_EVERY", "20"))
|
||||
|
||||
SELECTOR_SERVICES = ["modelV2", "drivingModelData", "cameraOdometry", "iqDriveModelData"]
|
||||
|
||||
EMAC_STATUS_KEYS = {
|
||||
"active": "MacModelActive", "failed": "MacModelFailed", "last_error": "MacModelLastError",
|
||||
"latency_ms": "MacModelLatencyMs", "status": "MacModelStatus",
|
||||
"reachable": "MacModelReachable", "progress": "MacModelDownloadProgress",
|
||||
}
|
||||
EGPU_STATUS_KEYS = {
|
||||
"active": "UsbGpuActive", "failed": "UsbGpuFailed", "last_error": "UsbGpuLastError",
|
||||
"latency_ms": "UsbGpuLatencyMs", "status": "UsbGpuStatus",
|
||||
"reachable": "UsbGpuPresent", "progress": "UsbGpuSetupProgress",
|
||||
}
|
||||
|
||||
|
||||
def backend_status_keys(emac_enabled: bool, egpu_enabled: bool, egpu_present: bool = False) -> dict[str, str]:
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_helpers import resolve_backend
|
||||
return EGPU_STATUS_KEYS if resolve_backend(emac_enabled, egpu_enabled, egpu_present) == "egpu" else EMAC_STATUS_KEYS
|
||||
|
||||
|
||||
def resolve_status_keys(params):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_present_consented, egpu_selected
|
||||
return backend_status_keys(params.get_bool("IQEmacEnabled"), egpu_selected(params), egpu_present_consented(params))
|
||||
|
||||
|
||||
def resolve_model_name(params, keys) -> str:
|
||||
if keys is EGPU_STATUS_KEYS:
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_model import DEFAULT_EGPU_MODEL, resolve_egpu_model
|
||||
resolved = resolve_egpu_model(params, allow_refresh=False)
|
||||
return resolved["key"] if resolved else DEFAULT_EGPU_MODEL
|
||||
name = params.get("IQEmacModel") or b"lebrowski"
|
||||
return name.decode() if isinstance(name, bytes) else name
|
||||
|
||||
|
||||
class AsyncParamWriter:
|
||||
|
||||
def __init__(self, params: Params):
|
||||
self._params = params
|
||||
self._pending: dict[str, object] = {}
|
||||
self._lock = threading.Lock()
|
||||
self._event = threading.Event()
|
||||
threading.Thread(target=self._drain, daemon=True).start()
|
||||
|
||||
def put(self, key: str, value) -> None:
|
||||
with self._lock:
|
||||
self._pending[key] = value
|
||||
self._event.set()
|
||||
|
||||
def put_bool(self, key: str, value: bool) -> None:
|
||||
self.put(key, bool(value))
|
||||
|
||||
def _drain(self) -> None:
|
||||
while True:
|
||||
self._event.wait()
|
||||
self._event.clear()
|
||||
with self._lock:
|
||||
batch, self._pending = self._pending, {}
|
||||
for key, value in batch.items():
|
||||
try:
|
||||
if isinstance(value, bool):
|
||||
self._params.put_bool(key, value)
|
||||
else:
|
||||
self._params.put(key, value)
|
||||
except Exception:
|
||||
cloudlog.exception(f"async param write failed: {key}")
|
||||
|
||||
|
||||
def wait_for_big(big_channel, target: int, deadline: float, min_frame: int = -1,
|
||||
max_lag_frames: int = BIG_MAX_LAG_FRAMES) -> tuple[dict | None, int | None]:
|
||||
big_peek = None
|
||||
grab_at = deadline - 0.004
|
||||
while time.perf_counter() < deadline:
|
||||
bfid = big_channel.peek_frame_id()
|
||||
big_peek = bfid
|
||||
if bfid == target - 1 and time.perf_counter() < grab_at:
|
||||
time.sleep(0.0005)
|
||||
continue
|
||||
if bfid is not None and min_frame < bfid <= target + BIG_FUTURE_ACCEPT and target - bfid <= max_lag_frames:
|
||||
got = big_channel.read()
|
||||
if got is not None and got[0] == bfid:
|
||||
return got[1], big_peek
|
||||
break
|
||||
if bfid is None or bfid <= min_frame or bfid > target + BIG_FUTURE_ACCEPT or target - bfid > max_lag_frames:
|
||||
break
|
||||
time.sleep(0.0005)
|
||||
return None, big_peek
|
||||
|
||||
|
||||
class BigLatch:
|
||||
|
||||
def __init__(self, miss_limit: int = BIG_MISS_LIMIT, activate_window: int = ACTIVATE_WINDOW,
|
||||
activate_frac: float = ACTIVATE_FRAC, rearm_limit: int = REARM_LIMIT):
|
||||
self.miss_limit = miss_limit
|
||||
self.activate_window = activate_window
|
||||
self.activate_need = int(round(activate_window * activate_frac))
|
||||
self.rearm_limit = rearm_limit
|
||||
self.active = False
|
||||
self.done = False
|
||||
self._miss = 0
|
||||
self._window: deque[bool] = deque(maxlen=activate_window)
|
||||
self._retires = 0
|
||||
|
||||
def update(self, used_big: bool) -> tuple[bool, bool]:
|
||||
if self.done:
|
||||
return False, False
|
||||
if not self.active:
|
||||
self._window.append(used_big)
|
||||
if len(self._window) >= self.activate_window and sum(self._window) >= self.activate_need:
|
||||
self.active = True
|
||||
self._miss = 0
|
||||
self._window.clear()
|
||||
return True, False
|
||||
if used_big:
|
||||
self._miss = 0
|
||||
elif self.active:
|
||||
self._miss += 1
|
||||
if self._miss >= self.miss_limit:
|
||||
self.active = False
|
||||
self._miss = 0
|
||||
self._window.clear()
|
||||
self._retires += 1
|
||||
self.done = self._retires > self.rearm_limit
|
||||
return False, True
|
||||
return False, False
|
||||
|
||||
|
||||
BIG_SOURCES = frozenset({"egpu_big", "mac_big"})
|
||||
|
||||
|
||||
def _patch_and_send(pm: PubMaster, payload: dict, frame_drop_perc: float, selector_dropped: int,
|
||||
target: int, source_lag: int, mismatch: bool | None = None) -> None:
|
||||
msgs = payload["msgs"]
|
||||
if mismatch is None:
|
||||
mismatch = source_lag > 0
|
||||
|
||||
big = payload.get("source") in BIG_SOURCES
|
||||
model_msg = log_from_bytes(msgs["modelV2"]).as_builder()
|
||||
if mismatch:
|
||||
model_msg.modelV2.frameId = target
|
||||
model_msg.modelV2.frameAge = max(model_msg.modelV2.frameAge, source_lag)
|
||||
model_msg.modelV2.frameDropPerc = frame_drop_perc
|
||||
model_msg.modelV2.big = big
|
||||
pm.send("modelV2", model_msg)
|
||||
|
||||
driving_msg = log_from_bytes(msgs["drivingModelData"]).as_builder()
|
||||
if mismatch:
|
||||
driving_msg.drivingModelData.frameId = target
|
||||
driving_msg.drivingModelData.frameDropPerc = frame_drop_perc
|
||||
driving_msg.drivingModelData.big = big
|
||||
pm.send("drivingModelData", driving_msg)
|
||||
|
||||
pose_msg = log_from_bytes(msgs["cameraOdometry"]).as_builder()
|
||||
if mismatch:
|
||||
pose_msg.cameraOdometry.frameId = target
|
||||
pose_msg.valid = bool(payload["live_calib_seen"]) and selector_dropped < 1 and not mismatch
|
||||
pm.send("cameraOdometry", pose_msg)
|
||||
|
||||
pm.send("iqDriveModelData", msgs["iqDriveModelData"])
|
||||
|
||||
|
||||
def _read_float(params, key: str, default: float) -> float:
|
||||
v = params.get(key)
|
||||
try:
|
||||
return float(v) if v is not None else default
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
|
||||
def main() -> None:
|
||||
cloudlog.warning("modeld_selector init")
|
||||
cloudlog.bind(daemon=PROCESS_NAME)
|
||||
setproctitle(PROCESS_NAME)
|
||||
config_realtime_process([0, 1, 2, 3], 54)
|
||||
|
||||
params = Params()
|
||||
keys = resolve_status_keys(params)
|
||||
pwriter = AsyncParamWriter(params)
|
||||
pwriter.put_bool(keys["active"], False)
|
||||
pwriter.put_bool(keys["failed"], False)
|
||||
pm = PubMaster(SELECTOR_SERVICES)
|
||||
|
||||
small_channel: ModelChannel | None = None
|
||||
big_channel: ModelChannel | None = None
|
||||
latch = BigLatch()
|
||||
big_used_count = 0
|
||||
run_count = 0
|
||||
last_published = -1
|
||||
last_big_published = -1
|
||||
frame_dropped_filter = FirstOrderFilter(0.0, 10.0, 1.0 / MODEL_FREQ)
|
||||
recent_big = deque(maxlen=STATUS_WINDOW)
|
||||
model_name = resolve_model_name(params, keys)
|
||||
last_backend_check = 0.0
|
||||
last_latency_ms = 0.0
|
||||
last_source_lag = 0
|
||||
miss_reasons = {"no_head": 0, "already_used": 0, "far_future": 0,
|
||||
"too_stale": 0, "head_prev_timeout": 0, "read_race": 0}
|
||||
|
||||
cloudlog.warning(f"modeld_selector starting (max_big_lag_frames={BIG_MAX_LAG_FRAMES})")
|
||||
while True:
|
||||
if small_channel is None:
|
||||
try:
|
||||
small_channel = ModelChannel(SMALL_CHANNEL, create=False)
|
||||
except OSError:
|
||||
time.sleep(0.05)
|
||||
continue
|
||||
if big_channel is None:
|
||||
try:
|
||||
big_channel = ModelChannel(BIG_CHANNEL, create=False)
|
||||
except OSError:
|
||||
big_channel = None
|
||||
|
||||
fid = small_channel.peek_frame_id()
|
||||
if fid is None or fid == last_published:
|
||||
time.sleep(0.0005)
|
||||
continue
|
||||
if last_published >= 0 and fid < last_published - 1:
|
||||
cloudlog.warning(f"modeld_selector frame reset {last_published} -> {fid}; re-arming")
|
||||
last_published = -1
|
||||
last_big_published = -1
|
||||
big_used_count = 0
|
||||
run_count = 0
|
||||
latch = BigLatch()
|
||||
pwriter.put_bool(keys["active"], False)
|
||||
pwriter.put_bool(keys["failed"], False)
|
||||
|
||||
now_mono = time.monotonic()
|
||||
if now_mono - last_backend_check > 1.0:
|
||||
last_backend_check = now_mono
|
||||
new_keys = resolve_status_keys(params)
|
||||
if new_keys is not keys:
|
||||
cloudlog.warning(f"modeld_selector backend changed {keys['active']} -> {new_keys['active']}; re-arming")
|
||||
pwriter.put_bool(keys["active"], False)
|
||||
pwriter.put_bool(keys["failed"], False)
|
||||
keys = new_keys
|
||||
model_name = resolve_model_name(params, keys)
|
||||
recent_big.clear()
|
||||
last_big_published = -1
|
||||
big_used_count = 0
|
||||
run_count = 0
|
||||
latch = BigLatch()
|
||||
pwriter.put_bool(keys["active"], False)
|
||||
pwriter.put_bool(keys["failed"], False)
|
||||
target = fid
|
||||
t_start = time.perf_counter()
|
||||
|
||||
small_payload = None
|
||||
got = small_channel.read()
|
||||
if got is not None and got[0] == target:
|
||||
small_payload = got[1]
|
||||
|
||||
payload = None
|
||||
used_big = False
|
||||
big_peek = None
|
||||
if big_channel is not None and not latch.done:
|
||||
deadline = t_start + BIG_MODEL_DEADLINE
|
||||
sof_ns = (small_payload or {}).get("timestamp_sof")
|
||||
if sof_ns:
|
||||
remaining = (BIG_ANCHOR_MS / 1000.0) - (time.clock_gettime(time.CLOCK_BOOTTIME) - sof_ns / 1e9)
|
||||
deadline = t_start + min(max(remaining, BIG_WAIT_FLOOR_S), BIG_WAIT_CEIL_S)
|
||||
payload, big_peek = wait_for_big(big_channel, target, deadline,
|
||||
last_big_published, BIG_MAX_LAG_FRAMES)
|
||||
used_big = payload is not None
|
||||
if not used_big:
|
||||
if big_peek is None:
|
||||
miss_reasons["no_head"] += 1
|
||||
elif big_peek <= last_big_published:
|
||||
miss_reasons["already_used"] += 1
|
||||
elif big_peek > target + BIG_FUTURE_ACCEPT:
|
||||
miss_reasons["far_future"] += 1
|
||||
elif target - big_peek > BIG_MAX_LAG_FRAMES:
|
||||
miss_reasons["too_stale"] += 1
|
||||
elif big_peek == target - 1:
|
||||
miss_reasons["head_prev_timeout"] += 1
|
||||
else:
|
||||
miss_reasons["read_race"] += 1
|
||||
|
||||
if payload is None:
|
||||
payload = small_payload
|
||||
if payload is None:
|
||||
got = small_channel.read()
|
||||
if got is not None and got[0] == target:
|
||||
payload = got[1]
|
||||
|
||||
activated_now, failed_now = latch.update(used_big)
|
||||
if activated_now:
|
||||
pwriter.put_bool(keys["active"], True)
|
||||
pwriter.put_bool(keys["failed"], False)
|
||||
cloudlog.warning(f"modeld_selector switched to BIG model at frame {target}")
|
||||
elif failed_now:
|
||||
pwriter.put_bool(keys["active"], False)
|
||||
pwriter.put_bool(keys["failed"], latch.done)
|
||||
pwriter.put(keys["last_error"], "big model stalled onroad; local fallback latched"
|
||||
if latch.done else "big model stalled onroad; small active, big may re-arm")
|
||||
if latch.done:
|
||||
cloudlog.warning(f"modeld_selector big stalled, staying on small until next ignition (frame {target})")
|
||||
else:
|
||||
cloudlog.warning(f"modeld_selector big stalled, small active; big may re-arm after a clean streak (frame {target})")
|
||||
|
||||
if payload is not None:
|
||||
selector_dropped = max(0, target - last_published - 1) if last_published >= 0 else 0
|
||||
frames_dropped = frame_dropped_filter.update(min(selector_dropped, 10))
|
||||
if run_count < WARMUP_FRAMES:
|
||||
frame_dropped_filter.x = 0.0
|
||||
frames_dropped = 0.0
|
||||
run_count += 1
|
||||
recent_big.append(used_big)
|
||||
if used_big:
|
||||
big_used_count += 1
|
||||
big_fid = int(payload.get("frame_id", big_peek if big_peek is not None else target))
|
||||
last_big_published = min(big_fid, target)
|
||||
last_latency_ms = float(payload.get("model_execution_time", 0.0)) * 1e3
|
||||
source_lag = max(0, target - int(payload.get("frame_id", target)))
|
||||
frame_mismatch = int(payload.get("frame_id", target)) != target
|
||||
last_source_lag = source_lag
|
||||
if run_count % STATUS_WINDOW == 0:
|
||||
hit_rate = (sum(recent_big) / len(recent_big)) if recent_big else 0.0
|
||||
pwriter.put(keys["latency_ms"], last_latency_ms)
|
||||
pwriter.put(keys["status"], json.dumps({
|
||||
"active": latch.active,
|
||||
"failed": latch.done,
|
||||
"hit_rate": round(hit_rate, 3),
|
||||
"latency_ms": round(last_latency_ms, 1),
|
||||
"source_lag_frames": last_source_lag,
|
||||
"model": model_name,
|
||||
"reachable": params.get_bool(keys["reachable"]),
|
||||
"download_progress": _read_float(params, keys["progress"], 1.0),
|
||||
"ts_mono": round(time.monotonic(), 1),
|
||||
}))
|
||||
if run_count % 100 == 0:
|
||||
cloudlog.warning(f"modeld_selector misses: {miss_reasons}")
|
||||
pwriter.put_bool(keys["active"], latch.active)
|
||||
cloudlog.warning(f"modeld_selector: big_used={big_used_count}/{run_count} "
|
||||
f"last_big_peek={big_peek} target={target} active={latch.active} "
|
||||
f"max_big_lag={BIG_MAX_LAG_FRAMES}")
|
||||
|
||||
frame_drop_perc = 100.0 * frames_dropped / (1.0 + frames_dropped)
|
||||
_patch_and_send(pm, payload, frame_drop_perc, selector_dropped, target, source_lag, frame_mismatch)
|
||||
last_published = target
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
cloudlog.warning("modeld_selector got SIGINT")
|
||||
@@ -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/
|
||||
"""
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
@@ -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}")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
@@ -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/
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
ONNX runner support for iqmodeld.
|
||||
"""
|
||||
@@ -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)
|
||||
@@ -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/
|
||||
"""
|
||||
@@ -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():
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()")
|
||||
|
||||
@@ -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()}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -4,24 +4,24 @@ from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.models.split_model_constants import SplitModelConstants
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.config import ModelConstants
|
||||
from iqpilot.selfdrive.iqmodeld.models.split_model_constants import SplitModelConstants
|
||||
from iqpilot.selfdrive.iqmodeld.config import ModelConstants
|
||||
|
||||
|
||||
def _bounded_exp(values, out=None):
|
||||
def safe_exp(values, out=None):
|
||||
return np.exp(np.clip(values, -np.inf, 11), out=out)
|
||||
|
||||
|
||||
def _sigmoid(values):
|
||||
return 1.0 / (1.0 + _bounded_exp(-values))
|
||||
def sigmoid(values):
|
||||
return 1.0 / (1.0 + safe_exp(-values))
|
||||
|
||||
|
||||
def _softmax_last(values, axis=-1):
|
||||
values -= np.max(values, axis=axis, keepdims=True)
|
||||
if values.dtype in (np.float32, np.float64):
|
||||
_bounded_exp(values, out=values)
|
||||
safe_exp(values, out=values)
|
||||
else:
|
||||
values = _bounded_exp(values)
|
||||
values = safe_exp(values)
|
||||
values /= np.sum(values, axis=axis, keepdims=True)
|
||||
return values
|
||||
|
||||
@@ -56,7 +56,7 @@ class _TensorKitchen:
|
||||
raw = self._grab(outputs, tensor_name)
|
||||
if raw is None:
|
||||
return
|
||||
outputs[tensor_name] = _sigmoid(raw)
|
||||
outputs[tensor_name] = sigmoid(raw)
|
||||
|
||||
def mixture(self, outputs: dict[str, np.ndarray], tensor_name: str, recipe: _MixtureRecipe) -> None:
|
||||
raw = self._grab(outputs, tensor_name)
|
||||
@@ -66,7 +66,7 @@ class _TensorKitchen:
|
||||
reshaped = raw.reshape((raw.shape[0], max(recipe.input_heads, 1), -1))
|
||||
value_count = (reshaped.shape[2] - recipe.output_heads) // 2
|
||||
means = reshaped[:, :, :value_count]
|
||||
stds = _bounded_exp(reshaped[:, :, value_count:2 * value_count])
|
||||
stds = safe_exp(reshaped[:, :, value_count:2 * value_count])
|
||||
|
||||
if recipe.input_heads > 1:
|
||||
weights = np.zeros((reshaped.shape[0], recipe.input_heads, recipe.output_heads), dtype=reshaped.dtype)
|
||||
|
||||
131
iqpilot/selfdrive/iqmodeld/temporal_state.py
Normal file
131
iqpilot/selfdrive/iqmodeld/temporal_state.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
|
||||
DEFAULT_FRAME_SKIP = 4
|
||||
|
||||
MODEL_INPUT_SPEC: dict[str, tuple[tuple[int, ...], str]] = {
|
||||
"img": ((1, 12, 128, 256), "uint8"),
|
||||
"big_img": ((1, 12, 128, 256), "uint8"),
|
||||
"desire_pulse": ((1, 25, 8), "float32"),
|
||||
"traffic_convention": ((1, 2), "float32"),
|
||||
"features_buffer": ((1, 24, 512), "float32"),
|
||||
"action_t": ((1, 2), "float32"),
|
||||
}
|
||||
|
||||
|
||||
def spec_from_meta(meta: dict) -> dict[str, tuple[tuple[int, ...], str]] | None:
|
||||
shapes = meta.get("input_shapes")
|
||||
if not shapes:
|
||||
return None
|
||||
return {name: (tuple(shape), "uint8" if name in ("img", "big_img") else "float32")
|
||||
for name, shape in shapes.items()}
|
||||
|
||||
|
||||
class TemporalInputState:
|
||||
def __init__(self, frame_skip: int, spec: dict[str, tuple[tuple[int, ...], str]] = MODEL_INPUT_SPEC):
|
||||
self.frame_skip = frame_skip
|
||||
img = spec["img"][0]
|
||||
fb = spec["features_buffer"][0]
|
||||
dp = spec["desire_pulse"][0]
|
||||
|
||||
self.n_frames = img[1] // 6
|
||||
img_q_shape = (frame_skip * (self.n_frames - 1) + 1, 6, img[2], img[3])
|
||||
self._img_shape = img
|
||||
self._fb_shape = fb
|
||||
self._dp_shape = dp
|
||||
feat_dim = math.prod(fb[2:])
|
||||
|
||||
self.img_q = np.zeros(img_q_shape, dtype=np.uint8)
|
||||
self.big_img_q = np.zeros(img_q_shape, dtype=np.uint8)
|
||||
self.feat_q = np.zeros((frame_skip * fb[1], fb[0], feat_dim), dtype=np.float32)
|
||||
self.desire_q = np.zeros((frame_skip * dp[1], dp[0], dp[2]), dtype=np.float32)
|
||||
self.prev_desire = np.zeros(dp[2], dtype=np.float32)
|
||||
self.prev_feat = np.zeros((fb[0], feat_dim), dtype=np.float32)
|
||||
|
||||
@staticmethod
|
||||
def _shift_append(q: np.ndarray, new_val: np.ndarray) -> None:
|
||||
q[:-1] = q[1:]
|
||||
q[-1] = new_val
|
||||
|
||||
def push_and_materialize(self, warped: np.ndarray, desire_pulse: np.ndarray,
|
||||
traffic_convention: np.ndarray, action_t: np.ndarray,
|
||||
) -> dict[str, np.ndarray]:
|
||||
fs = self.frame_skip
|
||||
|
||||
cur = desire_pulse.astype(np.float32).copy()
|
||||
cur[0] = 0
|
||||
pulse = np.where(cur - self.prev_desire > 0.99, cur, 0).astype(np.float32)
|
||||
self.prev_desire[:] = cur
|
||||
|
||||
self._shift_append(self.img_q, warped[0])
|
||||
self._shift_append(self.big_img_q, warped[1])
|
||||
self._shift_append(self.desire_q, pulse.reshape(self._dp_shape[0], self._dp_shape[2]))
|
||||
self._shift_append(self.feat_q, self.prev_feat)
|
||||
|
||||
dp = self._dp_shape
|
||||
return {
|
||||
"img": np.ascontiguousarray(self.img_q[::fs]).reshape(self._img_shape),
|
||||
"big_img": np.ascontiguousarray(self.big_img_q[::fs]).reshape(self._img_shape),
|
||||
"features_buffer": np.ascontiguousarray(self.feat_q[::fs]).reshape(self._fb_shape),
|
||||
"desire_pulse": self.desire_q.reshape(dp[1], fs, dp[0], dp[2]).max(axis=1).reshape(dp),
|
||||
"traffic_convention": traffic_convention.astype(np.float32).reshape(1, -1),
|
||||
"action_t": action_t.astype(np.float32).reshape(1, -1),
|
||||
}
|
||||
|
||||
def note_hidden_state(self, model_output: np.ndarray, hidden_slice: slice) -> None:
|
||||
self.prev_feat[:] = model_output[hidden_slice].reshape(self.prev_feat.shape)
|
||||
|
||||
|
||||
class SplitTemporalState:
|
||||
|
||||
def __init__(self, frame_skip: int, img_shape: tuple[int, ...],
|
||||
feature_shape: tuple[int, ...], desire_shape: tuple[int, ...]):
|
||||
self.frame_skip = frame_skip
|
||||
self._img_shape = tuple(img_shape)
|
||||
self._fb_shape = tuple(feature_shape)
|
||||
self._dp_shape = tuple(desire_shape)
|
||||
|
||||
n_frames = img_shape[1] // 6
|
||||
img_q_shape = (frame_skip * (n_frames - 1) + 1, 6, img_shape[2], img_shape[3])
|
||||
self.img_q = np.zeros(img_q_shape, dtype=np.uint8)
|
||||
self.big_img_q = np.zeros(img_q_shape, dtype=np.uint8)
|
||||
self.feat_q = np.zeros((frame_skip * (feature_shape[1] - 1) + 1, feature_shape[0], feature_shape[2]),
|
||||
dtype=np.float32)
|
||||
self.desire_q = np.zeros((frame_skip * desire_shape[1], desire_shape[0], desire_shape[2]), dtype=np.float32)
|
||||
self.prev_desire = np.zeros(desire_shape[2], dtype=np.float32)
|
||||
|
||||
def materialize_vision(self, warped: np.ndarray, desire: np.ndarray) -> dict[str, np.ndarray]:
|
||||
fs = self.frame_skip
|
||||
cur = desire.astype(np.float32).copy()
|
||||
cur[0] = 0
|
||||
pulse = np.where(cur - self.prev_desire > 0.99, cur, 0).astype(np.float32)
|
||||
self.prev_desire[:] = cur
|
||||
|
||||
TemporalInputState._shift_append(self.img_q, warped[0])
|
||||
TemporalInputState._shift_append(self.big_img_q, warped[1])
|
||||
TemporalInputState._shift_append(self.desire_q, pulse.reshape(self._dp_shape[0], self._dp_shape[2]))
|
||||
return {
|
||||
"img": np.ascontiguousarray(self.img_q[::fs]).reshape(self._img_shape),
|
||||
"big_img": np.ascontiguousarray(self.big_img_q[::fs]).reshape(self._img_shape),
|
||||
}
|
||||
|
||||
def materialize_policy(self, vision_feature: np.ndarray, traffic_convention: np.ndarray,
|
||||
action_t: np.ndarray | None = None) -> dict[str, np.ndarray]:
|
||||
fs = self.frame_skip
|
||||
TemporalInputState._shift_append(self.feat_q, vision_feature.reshape(self._fb_shape[0], self._fb_shape[2]))
|
||||
dp = self._dp_shape
|
||||
out = {
|
||||
"features_buffer": np.ascontiguousarray(self.feat_q[::fs]).reshape(self._fb_shape),
|
||||
"desire_pulse": self.desire_q.reshape(dp[1], fs, dp[0], dp[2]).max(axis=1).reshape(dp),
|
||||
"traffic_convention": traffic_convention.astype(np.float32).reshape(1, -1),
|
||||
}
|
||||
if action_t is not None:
|
||||
out["action_t"] = action_t.astype(np.float32).reshape(1, -1)
|
||||
return out
|
||||
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
@@ -1,5 +1,4 @@
|
||||
// Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
// clang++ -O2 repro.cc && ./a.out
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
@@ -5,12 +8,12 @@ from types import SimpleNamespace
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from cereal import log
|
||||
from iqpilot.cereal import log
|
||||
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.config import Plan
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.daemon import NeuralEngineState, _merged_plan
|
||||
import openpilot.iqpilot.selfdrive.iqmodeld.daemon as iqmodeld_daemon
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import smooth_value
|
||||
from iqpilot.selfdrive.iqmodeld.config import Plan
|
||||
from iqpilot.selfdrive.iqmodeld.daemon import NeuralEngineState, _merged_plan
|
||||
import iqpilot.selfdrive.iqmodeld.daemon as iqmodeld_daemon
|
||||
from iqpilot.selfdrive.controls.lib.drive_helpers import smooth_value
|
||||
|
||||
|
||||
def _fake_state(**overrides):
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
@@ -5,13 +8,14 @@ from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.models.combined_artifact import resolve_combined_split_artifact
|
||||
import openpilot.iqpilot.selfdrive.iqmodeld.models.runners.model_runner as runner_helpers
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.combined_split_runner import TinygradCombinedSplitRunner
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.tinygrad import combined_split_runner as combined_runner_mod
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelType
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.parser import PhaseParser
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.tests.test_iqmodeld_contracts import _phase_sample
|
||||
from iqpilot.selfdrive.iqmodeld.models.combined_artifact import resolve_combined_split_artifact
|
||||
import iqpilot.selfdrive.iqmodeld.models.runners.model_runner as runner_helpers
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad import tinygrad_runner as tinygrad_runner_mod
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.combined_split_runner import TinygradCombinedSplitRunner
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad import combined_split_runner as combined_runner_mod
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelType
|
||||
from iqpilot.selfdrive.iqmodeld.parser import PhaseParser
|
||||
from iqpilot.selfdrive.iqmodeld.tests.test_iqmodeld_contracts import _phase_sample
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -77,7 +81,7 @@ def test_resolve_combined_split_artifact_prefers_override(tmp_path: Path, monkey
|
||||
expected = tmp_path / "driving_combined_demo.pkl"
|
||||
expected.write_bytes(b"iq")
|
||||
|
||||
monkeypatch.setattr("openpilot.iqpilot.selfdrive.iqmodeld.models.combined_artifact._MODEL_ROOT", tmp_path)
|
||||
monkeypatch.setattr("iqpilot.selfdrive.iqmodeld.models.combined_artifact._MODEL_ROOT", tmp_path)
|
||||
|
||||
assert resolve_combined_split_artifact(bundle) == expected
|
||||
|
||||
@@ -89,7 +93,7 @@ def test_get_model_runner_prefers_combined_split_artifact(monkeypatch):
|
||||
], generation=11)
|
||||
|
||||
marker = object()
|
||||
monkeypatch.setattr(runner_helpers, "get_active_bundle", lambda: bundle)
|
||||
monkeypatch.setattr(runner_helpers, "_fetch_bundle", lambda: bundle)
|
||||
monkeypatch.setattr(runner_helpers, "has_combined_split_artifact", lambda _: True)
|
||||
monkeypatch.setattr(combined_runner_mod, "TinygradCombinedSplitRunner", lambda: marker)
|
||||
|
||||
@@ -103,9 +107,9 @@ def test_get_model_runner_keeps_split_bundle_on_existing_runner_without_combined
|
||||
], generation=12)
|
||||
|
||||
marker = object()
|
||||
monkeypatch.setattr(runner_helpers, "get_active_bundle", lambda: bundle)
|
||||
monkeypatch.setattr(runner_helpers, "_fetch_bundle", lambda: bundle)
|
||||
monkeypatch.setattr(runner_helpers, "has_combined_split_artifact", lambda _: False)
|
||||
monkeypatch.setattr(runner_helpers, "TinygradSplitRunner", lambda: marker)
|
||||
monkeypatch.setattr(tinygrad_runner_mod, "TinygradSplitRunner", lambda: marker)
|
||||
|
||||
assert runner_helpers.get_model_runner() is marker
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
"""
|
||||
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.tools.compile_supercombo import (
|
||||
from iqpilot.selfdrive.iqmodeld.tools.compile_supercombo import (
|
||||
_captured_devices,
|
||||
_captured_queue_depth,
|
||||
_validate_pose_outputs,
|
||||
)
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.supercombo_runner import _captured_queue_depth
|
||||
|
||||
|
||||
class _Captured:
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld import egpu_helpers as eh
|
||||
|
||||
|
||||
def test_fetch_fw_mirrors_and_serves_offline(tmp_path, monkeypatch):
|
||||
from tinygrad import helpers
|
||||
blob = os.urandom(4096)
|
||||
sha = hashlib.sha256(blob).hexdigest()
|
||||
calls = []
|
||||
|
||||
def orig(path, name, sha256):
|
||||
calls.append((path, name))
|
||||
return blob
|
||||
|
||||
monkeypatch.setattr(helpers, "fetch_fw", orig, raising=False)
|
||||
helpers.fetch_fw._iq_patched = False
|
||||
monkeypatch.setattr(eh, "FIRMWARE_MIRROR", str(tmp_path / "mirror"))
|
||||
eh.patch_tinygrad_fetch_fw()
|
||||
assert helpers.fetch_fw("amdgpu", "gc.bin", sha) == blob and calls == [("amdgpu", "gc.bin")]
|
||||
mirrored = tmp_path / "mirror" / "amdgpu" / "gc.bin"
|
||||
assert mirrored.read_bytes() == blob
|
||||
assert helpers.fetch_fw("amdgpu", "gc.bin", sha) == blob and len(calls) == 1
|
||||
mirrored.write_bytes(b"corrupt")
|
||||
assert helpers.fetch_fw("amdgpu", "gc.bin", sha) == blob and len(calls) == 2
|
||||
assert mirrored.read_bytes() == blob
|
||||
34
iqpilot/selfdrive/iqmodeld/tests/test_egpu_host_mock.py
Normal file
34
iqpilot/selfdrive/iqmodeld/tests/test_egpu_host_mock.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.tools.egpu_host_mock import tinygrad_tree
|
||||
|
||||
PROBE = """
|
||||
import os
|
||||
os.environ["JIT_BATCH_SIZE"] = "0"
|
||||
from iqpilot.selfdrive.iqmodeld.tools.egpu_host_mock import activate
|
||||
activate("gfx1200")
|
||||
from tinygrad import Tensor
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.engine.jit import TinyJit
|
||||
dev = Device["AMD"]
|
||||
assert dev.arch == "gfx1200", dev.arch
|
||||
assert type(dev.iface).__name__ == "MOCKUSBIface", type(dev.iface).__name__
|
||||
run = TinyJit(lambda x: (x * 2 + 1).sum(axis=1).realize())
|
||||
for i in range(3):
|
||||
run(Tensor.ones(64, 64, device="AMD") * i)
|
||||
print("MOCK_OK")
|
||||
"""
|
||||
|
||||
|
||||
@pytest.mark.skipif(not os.path.isdir(os.path.join(tinygrad_tree(), "test", "mockgpu")), reason="tinygrad mockgpu tree not checked out")
|
||||
def test_mock_dock_captures_a_jit_without_hardware():
|
||||
out = subprocess.run([sys.executable, "-c", PROBE], capture_output=True, text=True, timeout=600)
|
||||
assert out.returncode == 0, out.stderr[-2000:]
|
||||
assert "MOCK_OK" in out.stdout
|
||||
62
iqpilot/selfdrive/iqmodeld/tests/test_egpu_oob.py
Normal file
62
iqpilot/selfdrive/iqmodeld/tests/test_egpu_oob.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
import os
|
||||
import pickle
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
os.environ["DEV"] = "CPU"
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_policy import dump_oob, is_oob, load_bundle
|
||||
|
||||
|
||||
def _bundle():
|
||||
from tinygrad import Tensor
|
||||
w = Tensor(np.arange(4096, dtype=np.float32).reshape(64, 64), device="CPU").realize()
|
||||
return {"format": 2, "weights": w, "spec": {"a": ((1, 2), "float32")}, "blob": os.urandom(100_000)}
|
||||
|
||||
|
||||
def test_oob_round_trip_matches_plain_pickle(tmp_path):
|
||||
b = _bundle()
|
||||
oob = tmp_path / "b.oob"
|
||||
with open(oob, "wb") as f:
|
||||
dump_oob(b, f)
|
||||
assert is_oob(str(oob))
|
||||
got = load_bundle(str(oob))
|
||||
np.testing.assert_array_equal(got["weights"].numpy(), b["weights"].numpy())
|
||||
assert got["blob"] == b["blob"] and got["spec"] == b["spec"] and got["format"] == 2
|
||||
plain = tmp_path / "b.pkl"
|
||||
with open(plain, "wb") as f:
|
||||
pickle.dump({"x": 1, "blob": b["blob"]}, f, protocol=pickle.HIGHEST_PROTOCOL)
|
||||
assert not is_oob(str(plain))
|
||||
assert load_bundle(str(plain))["blob"] == b["blob"]
|
||||
|
||||
|
||||
def test_memory_guard_raises_when_starved(monkeypatch):
|
||||
from iqpilot.selfdrive.iqmodeld import iqegpumodeld as d
|
||||
monkeypatch.setattr(d, "_mem_available_mb", lambda: 90)
|
||||
monkeypatch.setattr(d, "MEMORY_WAIT_S", 0.0)
|
||||
with pytest.raises(RuntimeError, match="insufficient memory"):
|
||||
d._wait_for_memory(350)
|
||||
monkeypatch.setattr(d, "_mem_available_mb", lambda: 900)
|
||||
d._wait_for_memory(350)
|
||||
|
||||
|
||||
def test_opcode_rewrite_equals_oob_load(tmp_path):
|
||||
from tinygrad import Tensor
|
||||
from iqpilot.selfdrive.iqmodeld.tools.oob_rewrite import rewrite_oob
|
||||
big = Tensor(np.random.default_rng(0).standard_normal((512, 512)).astype(np.float32), device="CPU").realize()
|
||||
small = Tensor(np.arange(16, dtype=np.float32), device="CPU").realize()
|
||||
b = {"format": 2, "w": big, "s": small, "meta": {"k": "v"}, "raw": os.urandom(200_000)}
|
||||
plain = tmp_path / "plain.pkl"
|
||||
with open(plain, "wb") as f:
|
||||
pickle.dump(b, f, protocol=5)
|
||||
oob = tmp_path / "oob.pkl"
|
||||
moved, _ = rewrite_oob(str(plain), str(oob))
|
||||
assert moved >= 2 and is_oob(str(oob))
|
||||
got = load_bundle(str(oob))
|
||||
np.testing.assert_array_equal(got["w"].numpy(), b["w"].numpy())
|
||||
np.testing.assert_array_equal(got["s"].numpy(), b["s"].numpy())
|
||||
assert got["raw"] == b["raw"] and got["meta"] == {"k": "v"}
|
||||
161
iqpilot/selfdrive/iqmodeld/tests/test_egpu_policy.py
Normal file
161
iqpilot/selfdrive/iqmodeld/tests/test_egpu_policy.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
|
||||
os.environ["DEV"] = "CPU"
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_policy import PolicyRunner, make_run_policy, packed_layout, queue_shapes
|
||||
from iqpilot.selfdrive.iqmodeld.temporal_state import TemporalInputState
|
||||
|
||||
SPEC = {
|
||||
"img": ((1, 12, 8, 16), "uint8"),
|
||||
"big_img": ((1, 12, 8, 16), "uint8"),
|
||||
"desire_pulse": ((1, 25, 8), "float32"),
|
||||
"traffic_convention": ((1, 2), "float32"),
|
||||
"action_t": ((1, 2), "float32"),
|
||||
"features_buffer": ((1, 24, 512), "float32"),
|
||||
}
|
||||
FS = 4
|
||||
OUT_LEN = 2580
|
||||
HIDDEN = slice(1064, 1576)
|
||||
|
||||
|
||||
def _pack(inputs):
|
||||
from tinygrad.tensor import Tensor
|
||||
parts = [inputs[k].cast("float32").reshape(-1) for k in ("img", "big_img", "features_buffer", "desire_pulse", "traffic_convention", "action_t")]
|
||||
flat = Tensor.cat(*parts)
|
||||
hidden = (flat[:512] * 0.001).reshape(1, 512)
|
||||
return flat, hidden
|
||||
|
||||
|
||||
def _fake_model(inputs):
|
||||
from tinygrad.tensor import Tensor
|
||||
flat, hidden = _pack(inputs)
|
||||
n = flat.shape[0]
|
||||
head = flat[:min(n, HIDDEN.start)]
|
||||
out = Tensor.cat(head.pad((0, HIDDEN.start - head.shape[0])), hidden.reshape(-1), Tensor.zeros(OUT_LEN - HIDDEN.stop, device="CPU"))
|
||||
return {"outputs": out.reshape(1, -1)}
|
||||
|
||||
|
||||
class _Reference:
|
||||
def __init__(self):
|
||||
self.state = TemporalInputState(FS, SPEC)
|
||||
|
||||
def run(self, warped, desire, traffic, action_t):
|
||||
inputs = self.state.push_and_materialize(warped, desire, traffic, action_t)
|
||||
from tinygrad.tensor import Tensor
|
||||
t = {k: Tensor(np.ascontiguousarray(v), device="CPU") for k, v in inputs.items()}
|
||||
out = _fake_model(t)["outputs"].numpy().reshape(-1)
|
||||
self.state.note_hidden_state(out, HIDDEN)
|
||||
return out
|
||||
|
||||
|
||||
def test_policy_queues_match_temporal_state():
|
||||
from tinygrad.engine.jit import TinyJit
|
||||
jit = TinyJit(make_run_policy(_fake_model, SPEC, FS, "CPU"), prune=True)
|
||||
runner = PolicyRunner(jit, SPEC, FS, HIDDEN, "CPU")
|
||||
ref = _Reference()
|
||||
rng = np.random.default_rng(3)
|
||||
desire = np.zeros(8, dtype=np.float32)
|
||||
for i in range(14):
|
||||
warped = rng.integers(0, 256, (2, 6, 8, 16), dtype=np.int64).astype(np.uint8)
|
||||
if i in (2, 3, 9):
|
||||
desire[:] = 0
|
||||
desire[1 + (i % 3)] = 1
|
||||
elif i == 5:
|
||||
desire[:] = 0
|
||||
traffic = np.array([1.0, 0.0], dtype=np.float32) if i % 2 else np.array([0.0, 1.0], dtype=np.float32)
|
||||
action_t = np.array([0.1 * i, 0.2], dtype=np.float32)
|
||||
got = runner.run(warped, desire, traffic, action_t)
|
||||
want = ref.run(warped, desire, traffic, action_t)
|
||||
np.testing.assert_array_equal(got, want, err_msg=f"frame {i}")
|
||||
|
||||
|
||||
def test_layouts():
|
||||
shapes, sizes = packed_layout(SPEC)
|
||||
assert list(shapes) == ["desire", "traffic_convention", "action_t", "prev_feat"]
|
||||
assert sum(sizes) == 8 + 2 + 2 + 512
|
||||
q = queue_shapes(SPEC, FS)
|
||||
assert q["img_q"][0] == (5, 6, 8, 16) and q["feat_q"][0] == (96, 1, 512) and q["desire_q"][0] == (100, 1, 8)
|
||||
|
||||
|
||||
CAM = (64, 48)
|
||||
|
||||
|
||||
def _nv12(cam_w, cam_h):
|
||||
from iqpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||
stride, y_height, uv_height, _ = get_nv12_info(cam_w, cam_h)
|
||||
return (cam_w, cam_h, stride, y_height, uv_height)
|
||||
|
||||
|
||||
def _numpy_warp_plane(src, m, w_dst, h_dst):
|
||||
h_src, w_src = src.shape
|
||||
x = np.tile(np.arange(w_dst, dtype=np.float32), h_dst)
|
||||
y = np.repeat(np.arange(h_dst, dtype=np.float32), w_dst)
|
||||
sx = (m[0, 0] * x + m[0, 1] * y + m[0, 2]) / (m[2, 0] * x + m[2, 1] * y + m[2, 2])
|
||||
sy = (m[1, 0] * x + m[1, 1] * y + m[1, 2]) / (m[2, 0] * x + m[2, 1] * y + m[2, 2])
|
||||
xi = np.clip(np.round(sx), 0, w_src - 1).astype(np.int64)
|
||||
yi = np.clip(np.round(sy), 0, h_src - 1).astype(np.int64)
|
||||
return src[yi, xi].reshape(h_dst, w_dst)
|
||||
|
||||
|
||||
def _numpy_frame_prepare(frame, m, nv12, model_w, model_h):
|
||||
cam_w, cam_h, stride, y_height, uv_height = nv12
|
||||
m = m.astype(np.float32)
|
||||
y_src = frame[:cam_h * stride].reshape(cam_h, stride)
|
||||
uv = frame[stride * y_height:stride * y_height + uv_height * stride].reshape(uv_height, stride)
|
||||
m_uv = m * np.array([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]], dtype=np.float32)
|
||||
y = _numpy_warp_plane(y_src, m, model_w, model_h)
|
||||
u = _numpy_warp_plane(uv[:cam_h // 2, :cam_w:2], m_uv, model_w // 2, model_h // 2)
|
||||
v = _numpy_warp_plane(uv[:cam_h // 2, 1:cam_w:2], m_uv, model_w // 2, model_h // 2)
|
||||
f = np.concatenate([y.ravel(), u.ravel(), v.ravel()]).reshape(model_h * 3 // 2, model_w)
|
||||
H, W = model_h, model_w
|
||||
return np.stack([f[0:H:2, 0::2], f[1:H:2, 0::2], f[0:H:2, 1::2], f[1:H:2, 1::2],
|
||||
f[H:H + H // 4].reshape(H // 2, W // 2), f[H + H // 4:H + H // 2].reshape(H // 2, W // 2)])
|
||||
|
||||
|
||||
def _jittered_scale(rng, cam, model_w, model_h):
|
||||
m = np.array([[cam[0] / model_w, 0.0, 0.0], [0.0, cam[1] / model_h, 0.0], [0.0, 0.0, 1.0]], dtype=np.float32)
|
||||
m += (0.05 * rng.standard_normal((3, 3))).astype(np.float32) * np.array([[1, 1, 1], [1, 1, 1], [0.01, 0.01, 0.1]], dtype=np.float32)
|
||||
return m
|
||||
|
||||
|
||||
def test_frame_layout():
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_policy import frame_layout, model_size, nv12_copy_size
|
||||
shapes, sizes, npy_bytes = frame_layout(SPEC)
|
||||
assert list(shapes) == ["tfm", "big_tfm", "desire", "traffic_convention", "action_t", "prev_feat"]
|
||||
assert npy_bytes == (18 + 8 + 2 + 2 + 512) * 4
|
||||
assert model_size(SPEC) == (32, 16)
|
||||
assert nv12_copy_size(128, 64, 32) == 128 * 96
|
||||
|
||||
|
||||
def test_model_runner_matches_device_warp():
|
||||
from tinygrad.engine.jit import TinyJit
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_policy import ModelRunner, make_run_model, make_warp, model_size, nv12_copy_size
|
||||
nv12 = _nv12(*CAM)
|
||||
fcs = nv12_copy_size(nv12[2], nv12[3], nv12[4])
|
||||
model_w, model_h = model_size(SPEC)
|
||||
run_policy = make_run_policy(_fake_model, SPEC, FS, "CPU")
|
||||
jit = TinyJit(make_run_model(make_warp(nv12, model_w, model_h, "CPU"), run_policy, SPEC, fcs, "CPU"), prune=True)
|
||||
runner = ModelRunner(jit, SPEC, FS, HIDDEN, "CPU", fcs)
|
||||
ref = PolicyRunner(TinyJit(make_run_policy(_fake_model, SPEC, FS, "CPU"), prune=True), SPEC, FS, HIDDEN, "CPU")
|
||||
rng = np.random.default_rng(7)
|
||||
desire = np.zeros(8, dtype=np.float32)
|
||||
for i in range(10):
|
||||
main = rng.integers(0, 256, fcs, dtype=np.int64).astype(np.uint8)
|
||||
extra = rng.integers(0, 256, fcs, dtype=np.int64).astype(np.uint8)
|
||||
tfm = _jittered_scale(rng, CAM, model_w, model_h)
|
||||
big_tfm = _jittered_scale(rng, CAM, model_w, model_h)
|
||||
if i in (2, 6):
|
||||
desire[:] = 0
|
||||
desire[1 + i % 3] = 1
|
||||
traffic = np.array([1.0, 0.0], dtype=np.float32) if i % 2 else np.array([0.0, 1.0], dtype=np.float32)
|
||||
action_t = np.array([0.1 * i, 0.2], dtype=np.float32)
|
||||
got = runner.run(main, extra, tfm, big_tfm, desire, traffic, action_t)
|
||||
warped = np.stack([_numpy_frame_prepare(main, tfm, nv12, model_w, model_h), _numpy_frame_prepare(extra, big_tfm, nv12, model_w, model_h)])
|
||||
want = ref.run(warped, desire, traffic, action_t)
|
||||
np.testing.assert_array_equal(got, want, err_msg=f"frame {i}")
|
||||
163
iqpilot/selfdrive/iqmodeld/tests/test_egpu_stock_parity.py
Normal file
163
iqpilot/selfdrive/iqmodeld/tests/test_egpu_stock_parity.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
import types
|
||||
|
||||
import pytest
|
||||
|
||||
from iqpilot.cereal import log, messaging
|
||||
from iqpilot.cereal.services import SERVICE_LIST
|
||||
|
||||
|
||||
class TestTelemetryContract:
|
||||
def test_service_is_published_at_stock_cadence(self):
|
||||
assert "egpuDockState" in SERVICE_LIST
|
||||
assert SERVICE_LIST["egpuDockState"].frequency == 10.
|
||||
|
||||
def test_message_carries_every_stock_field(self):
|
||||
msg = messaging.new_message("egpuDockState")
|
||||
state = msg.egpuDockState
|
||||
for field in ("tempC", "memoryTempC", "powerDrawW", "powerLimitW", "gpuUsagePercent",
|
||||
"gpuClockMhz", "fanSpeedRpm", "pcieLtssm", "supplyVoltage", "supplyCurrent"):
|
||||
setattr(state, field, 1)
|
||||
assert getattr(state, field) == 1
|
||||
|
||||
def test_metrics_refresh_matches_stock(self):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_telemetry import METRICS_REFRESH_EVERY
|
||||
assert METRICS_REFRESH_EVERY == 100
|
||||
|
||||
def test_send_without_a_gpu_publishes_an_invalid_message(self):
|
||||
from iqpilot.selfdrive.iqmodeld import egpu_telemetry
|
||||
sent = []
|
||||
telemetry = egpu_telemetry.EgpuDockTelemetry(types.SimpleNamespace(send=lambda n, m: sent.append((n, m))), big=True)
|
||||
telemetry._device = lambda: types.SimpleNamespace(_opened_devices=set())
|
||||
telemetry.send()
|
||||
assert sent and sent[0][0] == "egpuDockState"
|
||||
assert sent[0][1].valid is False
|
||||
|
||||
|
||||
class TestBigFrameFlag:
|
||||
def test_model_message_carries_the_big_flag(self):
|
||||
msg = messaging.new_message("modelV2")
|
||||
msg.modelV2.big = True
|
||||
assert msg.modelV2.big
|
||||
|
||||
|
||||
class TestStatusParams:
|
||||
def test_loading_param_exists_and_is_cleared_like_stock(self):
|
||||
from pathlib import Path
|
||||
root = Path(__file__).resolve().parents[3]
|
||||
keys = (root / "common" / "params_keys.h").read_text()
|
||||
assert '{"UsbGpuLoading"' in keys
|
||||
line = next(ln for ln in keys.splitlines() if '"UsbGpuLoading"' in ln)
|
||||
for flag in ("CLEAR_ON_MANAGER_START", "CLEAR_ON_OFFROAD_TRANSITION", "CLEAR_ON_IGNITION_ON"):
|
||||
assert flag in line
|
||||
|
||||
|
||||
class TestAlerts:
|
||||
def test_both_stock_big_model_events_exist(self):
|
||||
assert hasattr(log.OnroadEvent.EventName, "bigModelLoading")
|
||||
assert hasattr(log.OnroadEvent.EventName, "bigModelFailed")
|
||||
|
||||
def test_alerts_are_wired_with_stock_severities(self):
|
||||
from iqpilot.selfdrive.selfdrived.events import EVENTS, ET
|
||||
EventName = log.OnroadEvent.EventName
|
||||
loading = EVENTS[EventName.bigModelLoading]
|
||||
failed = EVENTS[EventName.bigModelFailed]
|
||||
assert ET.NO_ENTRY in loading
|
||||
assert ET.SOFT_DISABLE in failed and ET.PERMANENT in failed
|
||||
|
||||
|
||||
class TestFirmwareGate:
|
||||
def test_runtime_refuses_a_dock_on_other_firmware(self, tmp_path):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_helpers import usbgpu_present
|
||||
from iqpilot.system.hardware.usb import EGPU_DOCK_FW_PRODUCT, EGPU_DOCK_USB_IDS
|
||||
vid, pid = EGPU_DOCK_USB_IDS[0]
|
||||
d = tmp_path / "1-1"
|
||||
d.mkdir()
|
||||
(d / "idVendor").write_text(f"{vid:04x}\n")
|
||||
(d / "idProduct").write_text(f"{pid:04x}\n")
|
||||
(d / "product").write_text("custom deadbeef-CLEAN\n")
|
||||
assert not usbgpu_present(str(tmp_path))
|
||||
(d / "product").write_text(EGPU_DOCK_FW_PRODUCT + "\n")
|
||||
assert usbgpu_present(str(tmp_path))
|
||||
|
||||
|
||||
class TestAutoFlash:
|
||||
def test_hardwared_drives_the_flasher_offroad_only(self):
|
||||
from iqpilot.system.hardware.hardwared import EgpuDockFlasher
|
||||
f = EgpuDockFlasher()
|
||||
calls = []
|
||||
f.flash = lambda: calls.append(1)
|
||||
stale = [{"vendorId": 0xADD1, "productId": 0x0001, "product": "custom deadbeef-CLEAN"}]
|
||||
f.update(False, stale)
|
||||
assert f.attempts == 0, "must not flash onroad"
|
||||
f.update(True, stale)
|
||||
assert f.attempts == 1
|
||||
if f.thread is not None:
|
||||
f.thread.join(timeout=5)
|
||||
|
||||
def test_matching_firmware_is_never_flashed(self):
|
||||
from iqpilot.system.hardware.egpu_dock.flash import bundled_version
|
||||
from iqpilot.system.hardware.hardwared import EgpuDockFlasher
|
||||
f = EgpuDockFlasher()
|
||||
f.flash = lambda: pytest.fail("flashed a dock that already matches")
|
||||
f.update(True, [{"vendorId": 0xADD1, "productId": 0x0001, "product": bundled_version()}])
|
||||
assert f.attempts == 0
|
||||
|
||||
def test_attempts_are_bounded_like_stock(self):
|
||||
from iqpilot.system.hardware.hardwared import EgpuDockFlasher
|
||||
assert EgpuDockFlasher.MAX_ATTEMPTS == 3
|
||||
assert EgpuDockFlasher.RETRY_INTERVAL == 20.
|
||||
|
||||
|
||||
class TestDockIsItsOwnConsent:
|
||||
|
||||
def _params(self, **flags):
|
||||
class P:
|
||||
def get_bool(self, k):
|
||||
return bool(flags.get(k, False))
|
||||
def get(self, k, *a, **kw):
|
||||
return None
|
||||
return P()
|
||||
|
||||
def _sysfs_with_dock(self, tmp_path, product=None):
|
||||
from iqpilot.system.hardware.usb import EGPU_DOCK_FW_PRODUCT, EGPU_DOCK_USB_IDS
|
||||
vid, pid = EGPU_DOCK_USB_IDS[0]
|
||||
d = tmp_path / "1-1"
|
||||
d.mkdir()
|
||||
(d / "idVendor").write_text(f"{vid:04x}\n")
|
||||
(d / "idProduct").write_text(f"{pid:04x}\n")
|
||||
(d / "product").write_text((product or EGPU_DOCK_FW_PRODUCT) + "\n")
|
||||
return str(tmp_path)
|
||||
|
||||
def test_a_plugged_in_dock_selects_itself(self, tmp_path):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected
|
||||
assert egpu_selected(self._params(), self._sysfs_with_dock(tmp_path))
|
||||
|
||||
def test_nothing_plugged_in_selects_nothing(self, tmp_path):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected
|
||||
assert not egpu_selected(self._params(), str(tmp_path))
|
||||
|
||||
def test_a_dock_on_foreign_firmware_does_not_select_itself(self, tmp_path):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected
|
||||
assert not egpu_selected(self._params(), self._sysfs_with_dock(tmp_path, "custom deadbeef-CLEAN"))
|
||||
|
||||
def test_the_user_can_force_it_off(self, tmp_path):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected
|
||||
root = self._sysfs_with_dock(tmp_path)
|
||||
assert not egpu_selected(self._params(IQEgpuDisabled=True), root)
|
||||
|
||||
def test_the_param_can_force_it_on_without_hardware(self, tmp_path):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected
|
||||
assert egpu_selected(self._params(IQEgpuEnabled=True), str(tmp_path))
|
||||
|
||||
def test_present_dock_wins_even_with_emac_enabled(self, tmp_path):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected, resolve_backend, usbgpu_present
|
||||
root = self._sysfs_with_dock(tmp_path)
|
||||
assert resolve_backend(True, egpu_selected(self._params(), root), usbgpu_present(root)) == "egpu"
|
||||
|
||||
def test_force_param_without_hardware_yields_to_emac(self, tmp_path):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected, resolve_backend, usbgpu_present
|
||||
assert resolve_backend(True, egpu_selected(self._params(IQEgpuEnabled=True), str(tmp_path)),
|
||||
usbgpu_present(str(tmp_path))) == "emac"
|
||||
509
iqpilot/selfdrive/iqmodeld/tests/test_egpu_worker.py
Normal file
509
iqpilot/selfdrive/iqmodeld/tests/test_egpu_worker.py
Normal file
@@ -0,0 +1,509 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import json
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_helpers import (
|
||||
resolve_backend, resolve_download_url, usbgpu_present,
|
||||
)
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_pipeline import (
|
||||
EgpuPipeline, EgpuPipelineError, make_big_channel_payload,
|
||||
)
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_model import EGPU_MODELS, get_egpu_model
|
||||
from iqpilot.selfdrive.iqmodeld.temporal_state import MODEL_INPUT_SPEC as INPUT_SPEC
|
||||
|
||||
|
||||
class FakeParams:
|
||||
def __init__(self, **flags):
|
||||
self._flags = {k: bool(v) for k, v in flags.items()}
|
||||
|
||||
def get_bool(self, key: str) -> bool:
|
||||
return self._flags.get(key, False)
|
||||
|
||||
|
||||
def _fake_usb_device(root, vid: str, pid: str, name: str = "1-1", product: str | None = None):
|
||||
from iqpilot.system.hardware.usb import EGPU_DOCK_FW_PRODUCT
|
||||
d = root / name
|
||||
d.mkdir()
|
||||
(d / "idVendor").write_text(vid + "\n")
|
||||
(d / "idProduct").write_text(pid + "\n")
|
||||
(d / "product").write_text((product if product is not None else EGPU_DOCK_FW_PRODUCT) + "\n")
|
||||
|
||||
|
||||
class TestPresence:
|
||||
def test_present(self, tmp_path):
|
||||
_fake_usb_device(tmp_path, "add1", "0001")
|
||||
assert usbgpu_present(str(tmp_path))
|
||||
|
||||
def test_foreign_firmware_absent(self, tmp_path):
|
||||
_fake_usb_device(tmp_path, "add1", "0001", product="custom deadbeef-CLEAN")
|
||||
assert not usbgpu_present(str(tmp_path))
|
||||
|
||||
def test_wrong_ids_absent(self, tmp_path):
|
||||
_fake_usb_device(tmp_path, "05ac", "12a8")
|
||||
assert not usbgpu_present(str(tmp_path))
|
||||
|
||||
def test_empty_bus_absent(self, tmp_path):
|
||||
assert not usbgpu_present(str(tmp_path))
|
||||
|
||||
def test_unreadable_entries_skipped(self, tmp_path):
|
||||
(tmp_path / "usb1").mkdir()
|
||||
_fake_usb_device(tmp_path, "add1", "0001", name="1-2")
|
||||
assert usbgpu_present(str(tmp_path))
|
||||
|
||||
|
||||
class TestBackendResolution:
|
||||
def test_none(self):
|
||||
assert resolve_backend(False, False) is None
|
||||
|
||||
def test_emac_only(self):
|
||||
assert resolve_backend(True, False) == "emac"
|
||||
|
||||
def test_egpu_only(self):
|
||||
assert resolve_backend(False, True) == "egpu"
|
||||
|
||||
def test_force_param_yields_to_emac_without_hardware(self):
|
||||
assert resolve_backend(True, True) == "emac"
|
||||
|
||||
def test_present_dock_wins_over_emac(self):
|
||||
assert resolve_backend(True, True, True) == "egpu"
|
||||
|
||||
|
||||
class TestManagerGating:
|
||||
@pytest.fixture
|
||||
def pc(self):
|
||||
return pytest.importorskip("iqpilot.system.manager.process_config")
|
||||
|
||||
def test_egpu_needs_presence(self, pc, monkeypatch):
|
||||
monkeypatch.setattr(pc, "usbgpu_present", lambda: True)
|
||||
assert pc.egpu_enabled(True, FakeParams(IQEgpuEnabled=True), None)
|
||||
assert pc.egpu_enabled(True, FakeParams(), None)
|
||||
monkeypatch.setattr(pc, "usbgpu_present", lambda: False)
|
||||
assert not pc.egpu_enabled(True, FakeParams(IQEgpuEnabled=True), None)
|
||||
assert not pc.egpu_enabled(True, FakeParams(), None)
|
||||
|
||||
def test_present_dock_wins_over_left_on_emac(self, pc, monkeypatch):
|
||||
monkeypatch.setattr(pc, "usbgpu_present", lambda: True)
|
||||
both = FakeParams(IQEmacEnabled=True, IQEgpuEnabled=True)
|
||||
assert not pc.emac_enabled(True, both, None)
|
||||
assert pc.egpu_enabled(True, both, None)
|
||||
|
||||
def test_emac_runs_when_no_dock(self, pc, monkeypatch):
|
||||
monkeypatch.setattr(pc, "usbgpu_present", lambda: False)
|
||||
assert pc.emac_enabled(True, FakeParams(IQEmacEnabled=True), None)
|
||||
|
||||
def test_disabled_dock_yields_to_emac(self, pc, monkeypatch):
|
||||
monkeypatch.setattr(pc, "usbgpu_present", lambda: True)
|
||||
both = FakeParams(IQEmacEnabled=True, IQEgpuDisabled=True)
|
||||
assert pc.emac_enabled(True, both, None)
|
||||
assert not pc.egpu_enabled(True, both, None)
|
||||
|
||||
def test_disabled_dock_runs_no_backend_when_no_emac(self, pc, monkeypatch):
|
||||
monkeypatch.setattr(pc, "usbgpu_present", lambda: True)
|
||||
off = FakeParams(IQEgpuDisabled=True)
|
||||
assert not pc.egpu_enabled(True, off, None)
|
||||
assert not pc.emac_enabled(True, off, None)
|
||||
|
||||
def test_selector_runs_for_either_backend(self, pc):
|
||||
assert pc.big_model_enabled(True, FakeParams(IQEmacEnabled=True), None)
|
||||
assert pc.big_model_enabled(True, FakeParams(IQEgpuEnabled=True), None)
|
||||
assert not pc.big_model_enabled(True, FakeParams(), None)
|
||||
|
||||
def test_iqegpumodeld_registered(self, pc):
|
||||
assert "iqegpumodeld" in pc.managed_processes
|
||||
assert "maciqmodeld" in pc.managed_processes
|
||||
|
||||
|
||||
class TestDownloadResolve:
|
||||
def test_direct_url_passthrough(self):
|
||||
assert resolve_download_url("https://x/y.onnx", "0" * 64, 5) == "https://x/y.onnx"
|
||||
|
||||
def test_commalfs_batch(self, monkeypatch):
|
||||
seen = {}
|
||||
|
||||
def fake_urlopen(req, timeout=0):
|
||||
seen["url"] = req.full_url
|
||||
seen["body"] = json.loads(req.data)
|
||||
return io.BytesIO(json.dumps(
|
||||
{"objects": [{"actions": {"download": {"href": "https://signed/url"}}}]}).encode())
|
||||
|
||||
monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen)
|
||||
sha = "a5" * 32
|
||||
url = resolve_download_url(f"commalfs:{sha}", sha, 1234)
|
||||
assert url == "https://signed/url"
|
||||
assert seen["body"]["objects"] == [{"oid": sha, "size": 1234}]
|
||||
assert seen["url"].endswith("/info/lfs/objects/batch")
|
||||
|
||||
|
||||
def _zero_infer(output_len: int, fill=None):
|
||||
calls = []
|
||||
|
||||
def infer(inputs):
|
||||
for name, (shape, dtype) in INPUT_SPEC.items():
|
||||
assert tuple(inputs[name].shape) == shape, name
|
||||
assert inputs[name].dtype == np.dtype(dtype), name
|
||||
calls.append({k: v.copy() for k, v in inputs.items()})
|
||||
out = np.zeros(output_len, dtype=np.float32)
|
||||
if fill is not None:
|
||||
out[:] = fill
|
||||
return out
|
||||
|
||||
infer.calls = calls
|
||||
return infer
|
||||
|
||||
|
||||
def _frame_inputs(seed=0):
|
||||
rng = np.random.default_rng(seed)
|
||||
warped = rng.integers(0, 256, (2, 6, 128, 256)).astype(np.uint8)
|
||||
desire = np.zeros(8, dtype=np.float32)
|
||||
traffic = np.array([1.0, 0.0], dtype=np.float32)
|
||||
action_t = np.array([0.25, 0.55], dtype=np.float32)
|
||||
return warped, desire, traffic, action_t
|
||||
|
||||
|
||||
class TestEgpuPipeline:
|
||||
def setup_method(self):
|
||||
self.meta = get_egpu_model()
|
||||
|
||||
def test_split_model_rejected(self):
|
||||
split_meta = {**get_egpu_model(), "key": "some_split", "split": True}
|
||||
with pytest.raises(EgpuPipelineError, match="split"):
|
||||
EgpuPipeline(split_meta, _zero_infer(split_meta["output_len"]))
|
||||
|
||||
def test_registry_is_fused_only(self):
|
||||
assert not any(m.get("split") for m in EGPU_MODELS.values())
|
||||
|
||||
def test_run_shapes_and_output(self):
|
||||
infer = _zero_infer(self.meta["output_len"])
|
||||
pipe = EgpuPipeline(self.meta, infer)
|
||||
out = pipe.run(*_frame_inputs())
|
||||
assert out.shape == (self.meta["output_len"],)
|
||||
assert len(infer.calls) == 1
|
||||
|
||||
def test_hidden_state_feeds_next_features_buffer(self):
|
||||
output_len = self.meta["output_len"]
|
||||
hidden = self.meta["output_slices"]["hidden_state"]
|
||||
|
||||
def infer(inputs):
|
||||
out = np.zeros(output_len, dtype=np.float32)
|
||||
out[hidden] = np.arange(hidden.stop - hidden.start, dtype=np.float32)
|
||||
return out
|
||||
|
||||
pipe = EgpuPipeline(self.meta, infer)
|
||||
pipe.run(*_frame_inputs(1))
|
||||
np.testing.assert_array_equal(
|
||||
pipe.state.prev_feat.reshape(-1), np.arange(hidden.stop - hidden.start, dtype=np.float32))
|
||||
pipe.run(*_frame_inputs(2))
|
||||
np.testing.assert_array_equal(
|
||||
pipe.state.feat_q[-1].reshape(-1), np.arange(hidden.stop - hidden.start, dtype=np.float32))
|
||||
|
||||
def test_desire_rising_edge_pulse(self):
|
||||
infer = _zero_infer(self.meta["output_len"])
|
||||
pipe = EgpuPipeline(self.meta, infer)
|
||||
warped, _, traffic, action_t = _frame_inputs()
|
||||
desire_on = np.zeros(8, dtype=np.float32)
|
||||
desire_on[3] = 1.0
|
||||
pipe.run(warped, desire_on, traffic, action_t)
|
||||
assert infer.calls[-1]["desire_pulse"][0, -1, 3] == 1.0
|
||||
for _ in range(5):
|
||||
pipe.run(warped, desire_on, traffic, action_t)
|
||||
assert infer.calls[-1]["desire_pulse"][0, :, 3].sum() == 1.0
|
||||
|
||||
def test_wrong_output_len_raises(self):
|
||||
pipe = EgpuPipeline(self.meta, _zero_infer(self.meta["output_len"] - 1))
|
||||
with pytest.raises(EgpuPipelineError, match="length"):
|
||||
pipe.run(*_frame_inputs())
|
||||
|
||||
def test_non_finite_output_raises(self):
|
||||
pipe = EgpuPipeline(self.meta, _zero_infer(self.meta["output_len"], fill=np.nan))
|
||||
with pytest.raises(EgpuPipelineError, match="finite"):
|
||||
pipe.run(*_frame_inputs())
|
||||
|
||||
|
||||
class TestChannelContract:
|
||||
def _real_msgs(self):
|
||||
import iqpilot.cereal.messaging as messaging
|
||||
msgs = {}
|
||||
for svc in ("modelV2", "drivingModelData", "cameraOdometry", "iqDriveModelData"):
|
||||
m = messaging.new_message(svc)
|
||||
msgs[svc] = m.to_bytes()
|
||||
return msgs
|
||||
|
||||
def test_payload_keys_match_selector_contract(self):
|
||||
payload = make_big_channel_payload(7, True, 0.031, 24.0, {"modelV2": b"x"})
|
||||
assert payload["source"] == "egpu_big"
|
||||
for key in ("frame_id", "live_calib_seen", "model_execution_time", "msgs"):
|
||||
assert key in payload
|
||||
|
||||
def test_selector_consumes_egpu_payload(self, tmp_path):
|
||||
from iqpilot.selfdrive.iqmodeld.model_channel import ModelChannel
|
||||
from iqpilot.selfdrive.iqmodeld.modeld_selector import wait_for_big
|
||||
|
||||
chan = ModelChannel(str(tmp_path / "big"), create=True)
|
||||
payload = make_big_channel_payload(100, True, 0.03, 25.0, self._real_msgs())
|
||||
chan.write(100, payload)
|
||||
|
||||
got, peek = wait_for_big(chan, 100, time.perf_counter() + 0.01)
|
||||
assert peek == 100
|
||||
assert got is not None
|
||||
assert got["source"] == "egpu_big"
|
||||
assert got["frame_id"] == 100
|
||||
|
||||
def test_selector_patch_and_send_parses_egpu_msgs(self):
|
||||
from iqpilot.selfdrive.iqmodeld.modeld_selector import _patch_and_send
|
||||
|
||||
sent = {}
|
||||
|
||||
class PM:
|
||||
def send(self, service, msg):
|
||||
sent[service] = msg
|
||||
|
||||
payload = make_big_channel_payload(42, True, 0.03, 25.0, self._real_msgs())
|
||||
_patch_and_send(PM(), payload, frame_drop_perc=0.0, selector_dropped=0, target=42, source_lag=0)
|
||||
assert set(sent) == {"modelV2", "drivingModelData", "cameraOdometry", "iqDriveModelData"}
|
||||
assert sent["modelV2"].modelV2.frameDropPerc == 0.0
|
||||
assert sent["cameraOdometry"].valid
|
||||
|
||||
def test_selector_lag_patches_frame_id(self):
|
||||
from iqpilot.selfdrive.iqmodeld.modeld_selector import _patch_and_send
|
||||
|
||||
sent = {}
|
||||
|
||||
class PM:
|
||||
def send(self, service, msg):
|
||||
sent[service] = msg
|
||||
|
||||
payload = make_big_channel_payload(40, True, 0.03, 25.0, self._real_msgs())
|
||||
_patch_and_send(PM(), payload, frame_drop_perc=0.0, selector_dropped=0, target=42, source_lag=2)
|
||||
assert sent["modelV2"].modelV2.frameId == 42
|
||||
assert not sent["cameraOdometry"].valid
|
||||
|
||||
|
||||
def _import_worker():
|
||||
try:
|
||||
import iqpilot.selfdrive.iqmodeld.iqegpumodeld as w
|
||||
return w
|
||||
except ImportError as e:
|
||||
if any(tag in str(e) for tag in ("pyx", "visionipc", "proprietary_runtime")):
|
||||
pytest.skip(f"device-only import chain unavailable on this host: {e}")
|
||||
raise
|
||||
|
||||
|
||||
class TestWorkerModule:
|
||||
def test_module_imports_off_device(self):
|
||||
w = _import_worker()
|
||||
assert w.PROCESS_NAME.endswith("iqegpumodeld")
|
||||
assert callable(w.main)
|
||||
|
||||
def test_warmup_validates_output(self):
|
||||
w = _import_worker()
|
||||
spec = {name: (shape, dtype) for name, (shape, dtype) in INPUT_SPEC.items()}
|
||||
def good(inputs):
|
||||
return np.zeros(10, dtype=np.float32)
|
||||
assert w._warmup(good, spec, 10) >= 0.0
|
||||
with pytest.raises(RuntimeError, match="invalid"):
|
||||
w._warmup(good, spec, 11)
|
||||
|
||||
|
||||
class TestSelectorBackendKeys:
|
||||
def test_emac_default(self):
|
||||
from iqpilot.selfdrive.iqmodeld.modeld_selector import EMAC_STATUS_KEYS, backend_status_keys
|
||||
assert backend_status_keys(False, False) is EMAC_STATUS_KEYS
|
||||
assert backend_status_keys(True, False) is EMAC_STATUS_KEYS
|
||||
|
||||
def test_egpu_selected(self):
|
||||
from iqpilot.selfdrive.iqmodeld.modeld_selector import EGPU_STATUS_KEYS, backend_status_keys
|
||||
assert backend_status_keys(False, True) is EGPU_STATUS_KEYS
|
||||
assert backend_status_keys(False, True)["active"] == "UsbGpuActive"
|
||||
assert backend_status_keys(False, True)["failed"] == "UsbGpuFailed"
|
||||
|
||||
def test_emac_wins_when_both(self):
|
||||
from iqpilot.selfdrive.iqmodeld.modeld_selector import EMAC_STATUS_KEYS, backend_status_keys
|
||||
assert backend_status_keys(True, True) is EMAC_STATUS_KEYS
|
||||
|
||||
def test_key_maps_cover_same_roles(self):
|
||||
from iqpilot.selfdrive.iqmodeld.modeld_selector import EGPU_STATUS_KEYS, EMAC_STATUS_KEYS
|
||||
assert set(EGPU_STATUS_KEYS) == set(EMAC_STATUS_KEYS)
|
||||
|
||||
|
||||
class TestBackendSeparation:
|
||||
EGPU_SOURCES = (
|
||||
"egpu_helpers.py", "egpu_pipeline.py", "egpu_model.py", "iqegpumodeld.py",
|
||||
"big_catalog.py", "tools/compile_egpu_model.py",
|
||||
)
|
||||
BANNED_IMPORTS = ("emac_input_state", "emac_model_meta", "maciqmodeld", "mac_protocol", "mac_client")
|
||||
|
||||
def _sources(self):
|
||||
import pathlib
|
||||
root = pathlib.Path(__file__).resolve().parents[1]
|
||||
return {name: (root / name).read_text() for name in self.EGPU_SOURCES}
|
||||
|
||||
def test_no_emac_module_imports(self):
|
||||
for name, src in self._sources().items():
|
||||
for banned in self.BANNED_IMPORTS:
|
||||
assert f"import {banned}" not in src and f"iqmodeld.{banned}" not in src, f"{name} imports {banned}"
|
||||
|
||||
def test_no_macmodel_params(self):
|
||||
for name, src in self._sources().items():
|
||||
assert "MacModel" not in src, f"{name} references MacModel* params"
|
||||
|
||||
def test_emac_shim_reexports_temporal_state(self):
|
||||
from iqpilot.selfdrive.iqmodeld import emac_input_state, temporal_state
|
||||
assert emac_input_state.EmacInputState is temporal_state.TemporalInputState
|
||||
assert emac_input_state.SplitInputState is temporal_state.SplitTemporalState
|
||||
|
||||
def test_emac_modules_are_not_in_the_public_tree(self):
|
||||
import pathlib
|
||||
root = pathlib.Path(__file__).resolve().parents[1]
|
||||
for gone in ("mac_protocol.py", "mac_client.py", "maciqmodeld.py", "bulk_transport.py"):
|
||||
assert not (root / gone).exists(), f"{gone} must live only in konn3kt_private"
|
||||
|
||||
|
||||
class TestMetaDrivenInputSpec:
|
||||
|
||||
def _run_one(self, meta):
|
||||
seen = {}
|
||||
def infer(inputs):
|
||||
seen.update({k: v.shape for k, v in inputs.items()})
|
||||
return np.zeros(meta["output_len"], dtype=np.float32)
|
||||
pipe = EgpuPipeline(meta, infer)
|
||||
pipe.run(np.zeros((2, 6, 128, 256), np.uint8), np.zeros(8, np.float32),
|
||||
np.array([1, 0], np.float32), np.zeros(2, np.float32))
|
||||
return seen
|
||||
|
||||
def test_default_contract_unchanged(self):
|
||||
meta = get_egpu_model()
|
||||
seen = self._run_one(meta)
|
||||
assert seen["features_buffer"] == (1, 24, 512)
|
||||
assert seen["desire_pulse"] == (1, 25, 8)
|
||||
|
||||
def test_registry_shapes_drive_the_state(self):
|
||||
meta = dict(get_egpu_model())
|
||||
meta["output_len"] = 18452
|
||||
meta["output_slices"] = dict(meta["output_slices"], hidden_state=slice(2066, 18450))
|
||||
meta["input_shapes"] = {
|
||||
"img": (1, 12, 128, 256), "big_img": (1, 12, 128, 256),
|
||||
"desire_pulse": (1, 33, 8), "traffic_convention": (1, 2),
|
||||
"action_t": (1, 2), "features_buffer": (1, 32, 32, 512),
|
||||
}
|
||||
seen = self._run_one(meta)
|
||||
assert seen["features_buffer"] == (1, 32, 32, 512)
|
||||
assert seen["desire_pulse"] == (1, 33, 8)
|
||||
|
||||
|
||||
class TestCatalogResolution:
|
||||
|
||||
def _params(self, model, doc=None):
|
||||
class P:
|
||||
def get(self, k):
|
||||
if k == "IQEmacModel":
|
||||
return model
|
||||
if k == "IQEmacCatalogCache":
|
||||
return json.dumps(doc) if doc else None
|
||||
return None
|
||||
return P()
|
||||
|
||||
def _doc(self):
|
||||
return {"schema": 1, "bundles": [{
|
||||
"short_name": "ttx", "display_name": "TTx", "index": 1,
|
||||
"model_name": "big_driving_supercombo",
|
||||
"wire": {"output_len": 2580, "frame_skip": 4, "pipeline": True,
|
||||
"output_slices": {"plan": [917, 1907], "hidden_state": [2066, 2578], "pad": [-2, None]},
|
||||
"input_shapes": {"img": [1, 12, 128, 256], "big_img": [1, 12, 128, 256],
|
||||
"desire_pulse": [1, 33, 8], "traffic_convention": [1, 2],
|
||||
"action_t": [1, 2], "features_buffer": [1, 32, 512]},
|
||||
"lat_smooth_seconds": 0.1},
|
||||
"source": {"kind": "comma_lfs", "sha256": "c" * 64, "size": 1},
|
||||
}]}
|
||||
|
||||
def test_unset_selection_is_the_builtin_default(self):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_model import resolve_egpu_model
|
||||
m = resolve_egpu_model(self._params(None))
|
||||
assert m["key"] == "lebrowski" and m["sha256"].startswith("a501760a")
|
||||
|
||||
def test_catalog_selection_resolves_with_shapes_and_smoothing(self):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_model import resolve_egpu_model
|
||||
m = resolve_egpu_model(self._params("ttx", self._doc()))
|
||||
assert m["key"] == "ttx"
|
||||
assert m["input_shapes"]["features_buffer"] == (1, 32, 512)
|
||||
assert m["input_shapes"]["desire_pulse"] == (1, 33, 8)
|
||||
assert m["lat_smooth_seconds"] == 0.1
|
||||
assert m["output_slices"]["pad"] == slice(-2, None)
|
||||
|
||||
def test_unknown_selection_is_a_park_not_a_silent_default(self):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_model import resolve_egpu_model
|
||||
assert resolve_egpu_model(self._params("ghost", self._doc()), allow_refresh=False) is None
|
||||
|
||||
def test_bench_model_is_not_selectable(self):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_model import resolve_egpu_model
|
||||
assert resolve_egpu_model(self._params("comma_small", self._doc()), allow_refresh=False) is None
|
||||
|
||||
def test_registry_carries_no_model_list(self):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_model import EGPU_MODELS
|
||||
assert set(EGPU_MODELS) == {"lebrowski", "comma_small"}
|
||||
|
||||
|
||||
class TestConsentAndIntegrity:
|
||||
def test_disabled_param_denies_present_dock(self, monkeypatch):
|
||||
from iqpilot.selfdrive.iqmodeld import egpu_helpers
|
||||
monkeypatch.setattr(egpu_helpers, "usbgpu_present", lambda sysfs_root=egpu_helpers.USB_SYSFS_ROOT: True)
|
||||
assert egpu_helpers.egpu_present_consented(FakeParams()) is True
|
||||
assert egpu_helpers.egpu_present_consented(FakeParams(IQEgpuDisabled=True)) is False
|
||||
|
||||
def test_local_onnx_quarantines_bad_content(self, tmp_path, monkeypatch):
|
||||
import hashlib
|
||||
from iqpilot.selfdrive.iqmodeld import egpu_helpers
|
||||
onnx = tmp_path / "m.onnx"
|
||||
onnx.write_bytes(b"good")
|
||||
meta = {"sha256": hashlib.sha256(b"good").hexdigest(), "download": {"size": 4}}
|
||||
monkeypatch.setattr(egpu_helpers, "onnx_cache_path", lambda m: str(onnx))
|
||||
assert egpu_helpers.local_onnx(meta) == str(onnx)
|
||||
onnx.write_bytes(b"bad!")
|
||||
assert egpu_helpers.local_onnx(meta) is None
|
||||
assert not onnx.exists()
|
||||
assert (tmp_path / "m.onnx.unusable").exists()
|
||||
|
||||
|
||||
class TestEgpuDockStatus:
|
||||
def _run(self, seq):
|
||||
from iqpilot.system.hardware.egpu_dock.status import EgpuDockStatus
|
||||
st = EgpuDockStatus()
|
||||
fired = {}
|
||||
def set_alert(name, cond, extra=None):
|
||||
fired[name] = (bool(cond), extra)
|
||||
for args in seq:
|
||||
st.update(*args, set_alert)
|
||||
return {k: v for k, v in fired.items() if v[0]}
|
||||
|
||||
def _dock(self, speed=10000, product="custom ed4e39b7-CLEAN"):
|
||||
return [{"vendorId": 0xADD1, "productId": 0x0001, "product": product, "speedMbps": speed}]
|
||||
|
||||
def test_no_dock_no_alerts(self):
|
||||
assert self._run([(True, [], False, False, None, True, None)]) == {}
|
||||
|
||||
def test_usb2_dock_warns_slow(self):
|
||||
fired = self._run([(True, self._dock(speed=480), False, False, None, True, None)])
|
||||
assert fired.get("Offroad_EgpuUsbSlow") == (True, "480 Mbps")
|
||||
|
||||
def test_power_fault_reports_pcie_unavailable(self):
|
||||
class St:
|
||||
supplyFault = True
|
||||
supplyVoltage = 0
|
||||
pcieLtssm = 0x78
|
||||
tempC = memoryTempC = 40.0
|
||||
fanSpeedRpm = 1500
|
||||
d = self._dock()
|
||||
fired = self._run([
|
||||
(True, d, False, False, None, True, None),
|
||||
(False, d, False, True, None, True, None),
|
||||
(False, d, False, False, b"1", True, St()),
|
||||
])
|
||||
assert "Offroad_EgpuPcieUnavailable" in fired
|
||||
111
iqpilot/selfdrive/iqmodeld/tests/test_emac_input_state.py
Normal file
111
iqpilot/selfdrive/iqmodeld/tests/test_emac_input_state.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
os.environ.setdefault("DEV", "CPU")
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.emac_input_state import EmacInputState
|
||||
from iqpilot.selfdrive.iqmodeld.emac_model_meta import FRAME_SKIP, OUTPUT_LEN, OUTPUT_SLICES
|
||||
from iqpilot.selfdrive.iqmodeld.temporal_state import MODEL_INPUT_SPEC as INPUT_SPEC
|
||||
|
||||
N_FRAMES_TEST = 30
|
||||
IMG_SHAPE = INPUT_SPEC["img"][0]
|
||||
DESIRE_LEN = INPUT_SPEC["desire_pulse"][0][2]
|
||||
|
||||
|
||||
class _CaptureRunner:
|
||||
|
||||
def __init__(self):
|
||||
self.captured: dict[str, np.ndarray] | None = None
|
||||
|
||||
def __call__(self, inputs):
|
||||
from tinygrad import Tensor
|
||||
self.captured = {k: v.numpy().copy() for k, v in inputs.items()}
|
||||
return {"outputs": Tensor(np.zeros((1, OUTPUT_LEN), dtype=np.float32))}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def reference():
|
||||
from iqpilot.selfdrive.iqmodeld.tools.compile_supercombo import (
|
||||
POLICY_INPUTS, make_input_queues, make_run_policy,
|
||||
)
|
||||
|
||||
input_shapes = {name: shape for name, (shape, _) in INPUT_SPEC.items()}
|
||||
metadata = {"input_shapes": input_shapes}
|
||||
capture = _CaptureRunner()
|
||||
run_policy = make_run_policy(capture, metadata, FRAME_SKIP)
|
||||
queues, npy = make_input_queues(input_shapes, FRAME_SKIP, device="CPU")
|
||||
return run_policy, queues, npy, capture, POLICY_INPUTS
|
||||
|
||||
|
||||
def _rising_edge(raw_desire: np.ndarray, prev: np.ndarray) -> np.ndarray:
|
||||
cur = raw_desire.astype(np.float32).copy()
|
||||
cur[0] = 0
|
||||
pulse = np.where(cur - prev > 0.99, cur, 0).astype(np.float32)
|
||||
prev[:] = cur
|
||||
return pulse
|
||||
|
||||
|
||||
def test_materialized_inputs_match_tinygrad_reference(reference):
|
||||
from tinygrad import Tensor
|
||||
|
||||
run_policy, queues, npy, capture, policy_inputs = reference
|
||||
rng = np.random.default_rng(1234)
|
||||
state = EmacInputState(FRAME_SKIP)
|
||||
ref_prev_desire = np.zeros(DESIRE_LEN, dtype=np.float32)
|
||||
|
||||
hidden = np.zeros((1, 512), dtype=np.float32)
|
||||
for frame in range(N_FRAMES_TEST):
|
||||
warped = rng.integers(0, 256, (2, 6, IMG_SHAPE[2], IMG_SHAPE[3]), dtype=np.int64).astype(np.uint8)
|
||||
raw_desire = np.zeros(DESIRE_LEN, dtype=np.float32)
|
||||
if frame % 3:
|
||||
raw_desire[int(rng.integers(0, DESIRE_LEN))] = 1.0
|
||||
traffic = rng.standard_normal(2).astype(np.float32)
|
||||
action_t = rng.standard_normal(2).astype(np.float32)
|
||||
|
||||
npy["desire"][:] = _rising_edge(raw_desire, ref_prev_desire)
|
||||
npy["traffic_convention"][:] = traffic
|
||||
npy["action_t"][:] = action_t
|
||||
npy["prev_feat"][:] = hidden
|
||||
run_policy(warped=Tensor(warped), **{k: queues[k] for k in policy_inputs})
|
||||
ref_inputs = capture.captured
|
||||
|
||||
state.prev_feat[:] = hidden
|
||||
mat = state.push_and_materialize(warped, raw_desire, traffic, action_t)
|
||||
|
||||
for name in INPUT_SPEC:
|
||||
assert ref_inputs[name].shape == tuple(INPUT_SPEC[name][0]), name
|
||||
np.testing.assert_array_equal(
|
||||
mat[name].astype(ref_inputs[name].dtype), ref_inputs[name],
|
||||
err_msg=f"frame {frame}: materialized {name} diverges from tinygrad reference")
|
||||
|
||||
fake_output = rng.standard_normal(OUTPUT_LEN).astype(np.float32)
|
||||
state.note_hidden_state(fake_output, OUTPUT_SLICES["hidden_state"])
|
||||
hidden = fake_output[OUTPUT_SLICES["hidden_state"]].reshape(1, 512).copy()
|
||||
|
||||
|
||||
def test_note_hidden_state_slice():
|
||||
state = EmacInputState(FRAME_SKIP)
|
||||
out = np.arange(OUTPUT_LEN, dtype=np.float32)
|
||||
state.note_hidden_state(out, OUTPUT_SLICES["hidden_state"])
|
||||
np.testing.assert_array_equal(state.prev_feat.reshape(-1), out[OUTPUT_SLICES["hidden_state"]])
|
||||
|
||||
|
||||
def test_desire_pulse_rising_edge_only_once():
|
||||
state = EmacInputState(FRAME_SKIP)
|
||||
held = np.zeros(DESIRE_LEN, dtype=np.float32)
|
||||
held[3] = 1.0
|
||||
warped = np.zeros((2, 6, IMG_SHAPE[2], IMG_SHAPE[3]), dtype=np.uint8)
|
||||
zeros2 = np.zeros(2, dtype=np.float32)
|
||||
|
||||
first = state.push_and_materialize(warped, held, zeros2, zeros2)
|
||||
assert first["desire_pulse"][0, -1, 3] == 1.0
|
||||
second = state.push_and_materialize(warped, held, zeros2, zeros2)
|
||||
assert state.desire_q[-1].max() == 0.0
|
||||
assert second["desire_pulse"][0, -1, 3] == 1.0
|
||||
128
iqpilot/selfdrive/iqmodeld/tests/test_fused_runner_guards.py
Normal file
128
iqpilot/selfdrive/iqmodeld/tests/test_fused_runner_guards.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pickle
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners import model_runner as model_runner_mod
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelType
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad import fused_runner as fused_mod
|
||||
|
||||
|
||||
class _View:
|
||||
def __init__(self, shape):
|
||||
self.shape = shape
|
||||
|
||||
|
||||
class _Captured:
|
||||
def __init__(self, expected_names, expected_input_info):
|
||||
self.expected_names = expected_names
|
||||
self.expected_input_info = expected_input_info
|
||||
|
||||
|
||||
class _FakeJit:
|
||||
def __init__(self, expected_names, expected_input_info):
|
||||
self.captured = _Captured(expected_names, expected_input_info)
|
||||
|
||||
def __call__(self, **kwargs):
|
||||
raise AssertionError("policy jit should not run in this test")
|
||||
|
||||
|
||||
class _FakeTensor:
|
||||
def __init__(self, arr, device=None):
|
||||
self.shape = tuple(np.asarray(arr).shape)
|
||||
|
||||
def contiguous(self):
|
||||
return self
|
||||
|
||||
def realize(self):
|
||||
return self
|
||||
|
||||
|
||||
class _FakeDevice:
|
||||
DEFAULT = "FAKE"
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Type:
|
||||
raw: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Artifact:
|
||||
fileName: str
|
||||
|
||||
|
||||
class _Model:
|
||||
def __init__(self, file_name):
|
||||
self.type = _Type(ModelType.vision)
|
||||
self.artifact = _Artifact(file_name)
|
||||
self.metadata = None
|
||||
|
||||
|
||||
class _Bundle:
|
||||
def __init__(self, file_name):
|
||||
self.models = [_Model(file_name)]
|
||||
self.is20hz = True
|
||||
|
||||
|
||||
POLICY_INPUTS = ["action_t", "big_img", "desire", "desire_q", "feat_q", "img", "traffic_convention"]
|
||||
POLICY_SHAPES = {
|
||||
"action_t": (1, 2), "big_img": (1, 12, 128, 256), "desire": (1, 8), "desire_q": (1, 100, 8),
|
||||
"feat_q": (1, 99, 512), "img": (1, 12, 128, 256), "traffic_convention": (1, 2),
|
||||
}
|
||||
|
||||
|
||||
def _write_fused_pkl(path, policy_inputs):
|
||||
info = [(_View(POLICY_SHAPES[n]), (), None, "NPY") for n in policy_inputs]
|
||||
role_meta = {
|
||||
"input_shapes": {"desire_pulse": (1, 100, 8), "traffic_convention": (1, 2), "features_buffer": (1, 99, 512)},
|
||||
"output_slices": {},
|
||||
}
|
||||
blob = {
|
||||
"metadata": {
|
||||
"vision": {"input_shapes": {"img": (1, 12, 128, 256), "big_img": (1, 12, 128, 256)}, "output_slices": {}},
|
||||
"on_policy": role_meta,
|
||||
"off_policy": role_meta,
|
||||
},
|
||||
"run_policy": _FakeJit(policy_inputs, info),
|
||||
"frame_skip": 4,
|
||||
(1928, 1208): _FakeJit(["frame"], [(_View((1,)), (), None, "NPY")]),
|
||||
}
|
||||
with open(path, "wb") as f:
|
||||
pickle.dump(blob, f)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fused_runner(tmp_path, monkeypatch):
|
||||
def _build(policy_inputs):
|
||||
name = "driving_fused_test.pkl"
|
||||
_write_fused_pkl(tmp_path / name, policy_inputs)
|
||||
monkeypatch.setattr(model_runner_mod, "_fetch_bundle", lambda params=None: _Bundle(name))
|
||||
monkeypatch.setattr(fused_mod, "CUSTOM_MODEL_PATH", str(tmp_path))
|
||||
monkeypatch.setattr(fused_mod, "_tinygrad_imports", lambda: (_FakeTensor, _FakeDevice))
|
||||
return fused_mod.TinygradFusedRunner()
|
||||
return _build
|
||||
|
||||
|
||||
def test_action_t_allocated_when_only_the_jit_declares_it(fused_runner):
|
||||
runner = fused_runner(POLICY_INPUTS)
|
||||
assert "action_t" not in runner._on_meta["input_shapes"]
|
||||
|
||||
runner._ensure_queues(1928, 1208)
|
||||
|
||||
assert runner._npy_buffers["action_t"].shape == POLICY_SHAPES["action_t"]
|
||||
assert runner._npy_buffers["traffic_convention"].shape == POLICY_SHAPES["traffic_convention"]
|
||||
|
||||
|
||||
def test_action_t_absent_when_the_jit_does_not_take_it(fused_runner):
|
||||
runner = fused_runner([n for n in POLICY_INPUTS if n != "action_t"])
|
||||
|
||||
runner._ensure_queues(1928, 1208)
|
||||
|
||||
assert "action_t" not in runner._npy_buffers
|
||||
@@ -1,20 +1,23 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
|
||||
import cereal.messaging as messaging
|
||||
import iqpilot.cereal.messaging as messaging
|
||||
import numpy as np
|
||||
from cereal import log
|
||||
from iqpilot.cereal import log
|
||||
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.config import Meta, ModelConstants
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.messaging import (
|
||||
from iqpilot.selfdrive.iqmodeld.config import Meta, ModelConstants
|
||||
from iqpilot.selfdrive.iqmodeld.messaging import (
|
||||
DrivePacketMemory,
|
||||
pick_curvature,
|
||||
populate_drive_messages,
|
||||
populate_odometry_message,
|
||||
)
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.models.split_model_constants import SplitModelConstants
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.parser import ArchiveParser, PhaseParser
|
||||
from iqpilot.selfdrive.iqmodeld.models.split_model_constants import SplitModelConstants
|
||||
from iqpilot.selfdrive.iqmodeld.parser import ArchiveParser, PhaseParser
|
||||
|
||||
|
||||
def _archive_sample(rng: np.random.Generator) -> dict[str, np.ndarray]:
|
||||
|
||||
73
iqpilot/selfdrive/iqmodeld/tests/test_lat_delay_source.py
Normal file
73
iqpilot/selfdrive/iqmodeld/tests/test_lat_delay_source.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from iqpilot.cereal import car
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.selfdrive.iqmodeld.daemon import InferenceDaemon
|
||||
|
||||
LIVE_DELAY = 0.4387
|
||||
RACK_DELAY = 0.10
|
||||
OFFSET = 0.05
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def params(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("PARAMS_ROOT", str(tmp_path))
|
||||
p = Params()
|
||||
p.put("IQSteerDelayCache", LIVE_DELAY)
|
||||
p.put("IQSoftwareSteerDelay", OFFSET)
|
||||
p.put_bool("ModelSmoothingEnabled", False)
|
||||
p.put("ModelLatSmoothSec", 0)
|
||||
p.put("PlanplusControl", 1.0)
|
||||
p.put("CameraOffset", 0.0)
|
||||
return p
|
||||
|
||||
|
||||
def _daemon(params, steer_control_type):
|
||||
car_params = car.CarParams.new_message()
|
||||
car_params.steerControlType = steer_control_type
|
||||
car_params.steerActuatorDelay = RACK_DELAY
|
||||
return SimpleNamespace(
|
||||
_params=params,
|
||||
_car_params=car_params,
|
||||
_channel=None,
|
||||
_sub={"lateralDelay": SimpleNamespace(lateralDelay=LIVE_DELAY)},
|
||||
_runtime=SimpleNamespace(lat_delay=None, PLANPLUS_CONTROL=None, model_smoothing_max_extra_sec=None),
|
||||
_warps=SimpleNamespace(set_offset=lambda _: None),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("live_enabled, expected", [(False, RACK_DELAY + OFFSET), (True, LIVE_DELAY)])
|
||||
def test_angle_cars_honour_the_self_tuning_toggle(params, live_enabled, expected):
|
||||
params.put_bool("IQLiveSteerDelay", live_enabled)
|
||||
daemon = _daemon(params, car.CarParams.SteerControlType.angle)
|
||||
InferenceDaemon._refresh_tunables(daemon, 0)
|
||||
assert daemon._runtime.lat_delay == pytest.approx(expected)
|
||||
|
||||
|
||||
def test_angle_cars_never_plan_against_the_live_estimate_when_disabled(params):
|
||||
params.put_bool("IQLiveSteerDelay", False)
|
||||
daemon = _daemon(params, car.CarParams.SteerControlType.angle)
|
||||
InferenceDaemon._refresh_tunables(daemon, 0)
|
||||
assert daemon._runtime.lat_delay != pytest.approx(LIVE_DELAY)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("live_enabled", [True, False])
|
||||
def test_torque_cars_keep_the_live_estimate(params, live_enabled):
|
||||
params.put_bool("IQLiveSteerDelay", live_enabled)
|
||||
daemon = _daemon(params, car.CarParams.SteerControlType.torque)
|
||||
InferenceDaemon._refresh_tunables(daemon, 0)
|
||||
assert daemon._runtime.lat_delay == pytest.approx(LIVE_DELAY)
|
||||
|
||||
|
||||
def test_refresh_is_throttled_to_every_sixtieth_tick(params):
|
||||
params.put_bool("IQLiveSteerDelay", False)
|
||||
daemon = _daemon(params, car.CarParams.SteerControlType.angle)
|
||||
InferenceDaemon._refresh_tunables(daemon, 1)
|
||||
assert daemon._runtime.lat_delay is None
|
||||
InferenceDaemon._refresh_tunables(daemon, 60)
|
||||
assert daemon._runtime.lat_delay == pytest.approx(RACK_DELAY + OFFSET)
|
||||
@@ -1,150 +0,0 @@
|
||||
"""
|
||||
Copyright (c) IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.models.manager import IQModelManager, _DOWNLOAD_INDEX_KEY
|
||||
|
||||
|
||||
@dataclass
|
||||
class _DownloadUri:
|
||||
sha256: str = ""
|
||||
uri: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Artifact:
|
||||
fileName: str = ""
|
||||
downloadUri: _DownloadUri = field(default_factory=_DownloadUri)
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Model:
|
||||
artifact: _Artifact = field(default_factory=_Artifact)
|
||||
metadata: _Artifact | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Bundle:
|
||||
index: int = 0
|
||||
ref: str = ""
|
||||
internalName: str = ""
|
||||
displayName: str = ""
|
||||
models: list = field(default_factory=list)
|
||||
|
||||
|
||||
class _FakeParams:
|
||||
def __init__(self):
|
||||
self.store = {}
|
||||
|
||||
def get(self, key):
|
||||
return self.store.get(key)
|
||||
|
||||
def put(self, key, value):
|
||||
self.store[key] = value
|
||||
|
||||
def remove(self, key):
|
||||
self.store.pop(key, None)
|
||||
|
||||
|
||||
def _bundle(index, name, sha, filename="driving_vision_test_tinygrad.pkl"):
|
||||
return _Bundle(
|
||||
index=index,
|
||||
ref=f"ref-{name}",
|
||||
internalName=name,
|
||||
displayName=f"{name} display",
|
||||
models=[_Model(artifact=_Artifact(fileName=filename, downloadUri=_DownloadUri(sha256=sha)))],
|
||||
)
|
||||
|
||||
|
||||
def _manager(active, available):
|
||||
mgr = IQModelManager.__new__(IQModelManager)
|
||||
mgr.params = _FakeParams()
|
||||
mgr.active_bundle = active
|
||||
mgr.available_models = available
|
||||
mgr._validated_active_key = None
|
||||
mgr._manifest_refresh_key = None
|
||||
return mgr
|
||||
|
||||
|
||||
def test_stale_active_bundle_queues_redownload_at_current_index():
|
||||
active = _bundle(55, "WMIV12", "a" * 64)
|
||||
counterpart = _bundle(12, "WMIV12", "b" * 64)
|
||||
mgr = _manager(active, [counterpart])
|
||||
|
||||
mgr._queue_active_manifest_refresh()
|
||||
|
||||
assert mgr.params.get(_DOWNLOAD_INDEX_KEY) == 12
|
||||
assert mgr.active_bundle is active
|
||||
|
||||
|
||||
def test_matching_shas_do_not_queue():
|
||||
active = _bundle(55, "WMIV12", "a" * 64)
|
||||
counterpart = _bundle(12, "WMIV12", "A" * 64)
|
||||
mgr = _manager(active, [counterpart])
|
||||
|
||||
mgr._queue_active_manifest_refresh()
|
||||
|
||||
assert mgr.params.get(_DOWNLOAD_INDEX_KEY) is None
|
||||
|
||||
|
||||
def test_retired_bundle_is_left_alone():
|
||||
active = _bundle(55, "WMIV12", "a" * 64)
|
||||
mgr = _manager(active, [_bundle(12, "OtherModel", "b" * 64)])
|
||||
|
||||
mgr._queue_active_manifest_refresh()
|
||||
|
||||
assert mgr.params.get(_DOWNLOAD_INDEX_KEY) is None
|
||||
assert mgr.active_bundle is active
|
||||
|
||||
|
||||
def test_default_bundle_is_never_refreshed():
|
||||
active = _bundle(0, "Default", "a" * 64)
|
||||
active.ref = "default"
|
||||
mgr = _manager(active, [_bundle(0, "Default", "b" * 64)])
|
||||
|
||||
mgr._queue_active_manifest_refresh()
|
||||
|
||||
assert mgr.params.get(_DOWNLOAD_INDEX_KEY) is None
|
||||
|
||||
|
||||
def test_pending_download_blocks_refresh():
|
||||
active = _bundle(55, "WMIV12", "a" * 64)
|
||||
mgr = _manager(active, [_bundle(12, "WMIV12", "b" * 64)])
|
||||
mgr.params.put(_DOWNLOAD_INDEX_KEY, 3)
|
||||
|
||||
mgr._queue_active_manifest_refresh()
|
||||
|
||||
assert mgr.params.get(_DOWNLOAD_INDEX_KEY) == 3
|
||||
|
||||
|
||||
def test_empty_manifest_hash_never_triggers():
|
||||
active = _bundle(55, "WMIV12", "a" * 64)
|
||||
mgr = _manager(active, [_bundle(12, "WMIV12", "")])
|
||||
|
||||
mgr._queue_active_manifest_refresh()
|
||||
|
||||
assert mgr.params.get(_DOWNLOAD_INDEX_KEY) is None
|
||||
|
||||
|
||||
def test_refresh_queued_once_per_run():
|
||||
active = _bundle(55, "WMIV12", "a" * 64)
|
||||
mgr = _manager(active, [_bundle(12, "WMIV12", "b" * 64)])
|
||||
|
||||
mgr._queue_active_manifest_refresh()
|
||||
assert mgr.params.get(_DOWNLOAD_INDEX_KEY) == 12
|
||||
|
||||
mgr.params.remove(_DOWNLOAD_INDEX_KEY)
|
||||
mgr._queue_active_manifest_refresh()
|
||||
assert mgr.params.get(_DOWNLOAD_INDEX_KEY) is None
|
||||
|
||||
|
||||
def test_counterpart_matched_by_name_not_index():
|
||||
active = _bundle(55, "WMIV12", "a" * 64)
|
||||
imposter = _bundle(55, "OtherModel", "c" * 64)
|
||||
counterpart = _bundle(12, "WMIV12", "b" * 64)
|
||||
mgr = _manager(active, [imposter, counterpart])
|
||||
|
||||
mgr._queue_active_manifest_refresh()
|
||||
|
||||
assert mgr.params.get(_DOWNLOAD_INDEX_KEY) == 12
|
||||
133
iqpilot/selfdrive/iqmodeld/tests/test_model_bundle_downloader.py
Normal file
133
iqpilot/selfdrive/iqmodeld/tests/test_model_bundle_downloader.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
import hashlib
|
||||
import http.server
|
||||
import os
|
||||
import threading
|
||||
|
||||
import pytest
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld import model_bundle_downloader as dl
|
||||
|
||||
|
||||
class _RangeHandler(http.server.BaseHTTPRequestHandler):
|
||||
store: dict[str, bytes] = {}
|
||||
cut_first: dict[str, int] = {}
|
||||
hits: list[tuple[str, str | None]] = []
|
||||
|
||||
def log_message(self, *a):
|
||||
pass
|
||||
|
||||
def do_GET(self):
|
||||
oid = self.path.rsplit("/", 1)[-1]
|
||||
data = self.store[oid]
|
||||
rng = self.headers.get("Range")
|
||||
self.hits.append((oid, rng))
|
||||
start = int(rng.split("=")[1].rstrip("-")) if rng else 0
|
||||
body = data[start:]
|
||||
cut = self.cut_first.pop(oid, None)
|
||||
if cut is not None:
|
||||
body = body[:cut]
|
||||
self.send_response(206 if rng else 200)
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
if rng:
|
||||
self.send_header("Content-Range", f"bytes {start}-{start + len(body) - 1}/{len(data)}")
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def server():
|
||||
srv = http.server.ThreadingHTTPServer(("127.0.0.1", 0), _RangeHandler)
|
||||
t = threading.Thread(target=srv.serve_forever, daemon=True)
|
||||
t.start()
|
||||
yield srv
|
||||
srv.shutdown()
|
||||
srv.server_close()
|
||||
|
||||
|
||||
def _objects(parts):
|
||||
return [{"oid": hashlib.sha256(p).hexdigest(), "size": len(p)} for p in parts]
|
||||
|
||||
|
||||
def test_resume_continues_a_cut_part_and_reuses_finished_parts(server, tmp_path, monkeypatch):
|
||||
parts = [os.urandom(300_000), os.urandom(300_000), os.urandom(120_000)]
|
||||
objs = _objects(parts)
|
||||
_RangeHandler.store = {o["oid"]: p for o, p in zip(objs, parts, strict=True)}
|
||||
_RangeHandler.hits = []
|
||||
_RangeHandler.cut_first = {objs[1]["oid"]: 100_000}
|
||||
port = server.server_address[1]
|
||||
monkeypatch.setattr(dl, "_requests_auth", lambda: None)
|
||||
monkeypatch.setattr(dl, "_resolve_oid", lambda session, base, oid, size, auth: (f"http://127.0.0.1:{port}/o/{oid}", {}))
|
||||
monkeypatch.setattr(dl, "MODELS_BASE_URLS", ("http://unused",))
|
||||
monkeypatch.setattr(dl, "STREAM_RETRIES", 3)
|
||||
monkeypatch.setattr(dl, "CHUNK", 64 * 1024)
|
||||
whole = b"".join(parts)
|
||||
dst = str(tmp_path / "model.pkl")
|
||||
out = dl.download_lfs_bundle(objs, dst, hashlib.sha256(whole).hexdigest(), len(whole))
|
||||
with open(dst, "rb") as f:
|
||||
assert out == dst and f.read() == whole
|
||||
assert not os.path.exists(dst + ".parts")
|
||||
ranges = [r for o, r in _RangeHandler.hits if o == objs[1]["oid"]]
|
||||
assert ranges[0] is None and ranges[1] == "bytes=100000-"
|
||||
assert sum(1 for o, _ in _RangeHandler.hits if o == objs[0]["oid"]) == 1
|
||||
|
||||
|
||||
def test_corrupt_finished_part_is_refetched(server, tmp_path, monkeypatch):
|
||||
parts = [os.urandom(200_000), os.urandom(50_000)]
|
||||
objs = _objects(parts)
|
||||
_RangeHandler.store = {o["oid"]: p for o, p in zip(objs, parts, strict=True)}
|
||||
_RangeHandler.hits = []
|
||||
_RangeHandler.cut_first = {}
|
||||
port = server.server_address[1]
|
||||
monkeypatch.setattr(dl, "_requests_auth", lambda: None)
|
||||
monkeypatch.setattr(dl, "_resolve_oid", lambda session, base, oid, size, auth: (f"http://127.0.0.1:{port}/o/{oid}", {}))
|
||||
monkeypatch.setattr(dl, "MODELS_BASE_URLS", ("http://unused",))
|
||||
dst = str(tmp_path / "model.pkl")
|
||||
os.makedirs(dst + ".parts")
|
||||
with open(dl._part_path(dst, objs[0]["oid"]), "wb") as f:
|
||||
f.write(os.urandom(200_000))
|
||||
whole = b"".join(parts)
|
||||
dl.download_lfs_bundle(objs, dst, hashlib.sha256(whole).hexdigest(), len(whole))
|
||||
with open(dst, "rb") as f:
|
||||
assert f.read() == whole
|
||||
|
||||
|
||||
def test_hf_single_file_resumes_after_cut(server, tmp_path, monkeypatch):
|
||||
data = os.urandom(700_000)
|
||||
oid = hashlib.sha256(data).hexdigest()
|
||||
_RangeHandler.store = {oid: data}
|
||||
_RangeHandler.hits = []
|
||||
_RangeHandler.cut_first = {oid: 250_000}
|
||||
port = server.server_address[1]
|
||||
monkeypatch.setattr(dl, "_hf", lambda: ({"Authorization": "Bearer test"}, lambda p: f"http://127.0.0.1:{port}/o/{oid}"))
|
||||
monkeypatch.setattr(dl, "STREAM_RETRIES", 3)
|
||||
monkeypatch.setattr(dl, "CHUNK", 64 * 1024)
|
||||
dst = str(tmp_path / "policy.pkl")
|
||||
out = dl.download_hf_file("egpu/policy/x.pkl", dst, oid, len(data))
|
||||
with open(dst, "rb") as f:
|
||||
assert out == dst and f.read() == data
|
||||
ranges = [r for o, r in _RangeHandler.hits if o == oid]
|
||||
assert ranges[0] is None and ranges[1] == "bytes=250000-"
|
||||
assert not os.path.exists(dst + ".hfpart")
|
||||
|
||||
|
||||
def test_download_onnx_prefers_hf_then_falls_back(tmp_path, monkeypatch):
|
||||
from iqpilot.selfdrive.iqmodeld import egpu_helpers as eh
|
||||
meta = {"key": "m", "sha256": "ab" * 32, "download": {"kind": "comma_lfs", "size": 5}}
|
||||
monkeypatch.setattr(eh, "onnx_cache_path", lambda m: str(tmp_path / "m.onnx"))
|
||||
monkeypatch.setattr("iqpilot.selfdrive.iqmodeld.egpu_model.download_descriptor", lambda m: ("commalfs:" + m["sha256"], 5), raising=False)
|
||||
calls = []
|
||||
import iqpilot.selfdrive.iqmodeld.model_bundle_downloader as dlm
|
||||
monkeypatch.setattr(dlm, "download_hf_file", lambda path, dst, sha, size, progress_cb=None: (calls.append(("hf", path)), open(dst, "wb").close(), dst)[2])
|
||||
monkeypatch.setattr(eh, "resolve_download_url", lambda *a, **k: (calls.append(("lfs",)), "http://unused")[1])
|
||||
out = eh.download_onnx(meta)
|
||||
assert calls == [("hf", "onnx/" + "ab" * 32 + ".onnx")] and out == str(tmp_path / "m.onnx")
|
||||
calls.clear()
|
||||
def boom(*a, **k):
|
||||
calls.append(("hf-fail",)); raise RuntimeError("hf down")
|
||||
monkeypatch.setattr(dlm, "download_hf_file", boom)
|
||||
with pytest.raises(Exception):
|
||||
eh.download_onnx(meta)
|
||||
assert calls[:2] == [("hf-fail",), ("lfs",)]
|
||||
@@ -1,17 +1,20 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from tinygrad.tensor import Tensor
|
||||
|
||||
import openpilot.iqpilot.selfdrive.iqmodeld.models.helpers as bundle_helpers
|
||||
import openpilot.iqpilot.selfdrive.iqmodeld.models.runners.model_runner as model_runner_mod
|
||||
import openpilot.iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.tinygrad_runner as tinygrad_runner_mod
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelType
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.tinygrad_runner import TinygradRunner
|
||||
import iqpilot.selfdrive.iqmodeld.models.helpers as bundle_helpers
|
||||
import iqpilot.selfdrive.iqmodeld.models.runners.model_runner as model_runner_mod
|
||||
import iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.tinygrad_runner as tinygrad_runner_mod
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelType
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.tinygrad_runner import TinygradRunner
|
||||
|
||||
|
||||
LOCAL_MODEL_DIR = Path(__file__).resolve().parents[1] / "default_model"
|
||||
@@ -49,6 +52,7 @@ def _seed_runner_inputs(runner: TinygradRunner) -> None:
|
||||
).realize()
|
||||
|
||||
|
||||
@pytest.mark.tici
|
||||
def test_local_tinygrad_models_execute(monkeypatch):
|
||||
bundle = _Bundle([
|
||||
_Model(ModelType.vision, "driving_vision_c210m_tinygrad.pkl", "driving_vision_c210m_metadata.pkl"),
|
||||
@@ -56,7 +60,7 @@ def test_local_tinygrad_models_execute(monkeypatch):
|
||||
])
|
||||
|
||||
monkeypatch.setattr(bundle_helpers, "get_active_bundle", lambda params=None: bundle, raising=False)
|
||||
monkeypatch.setattr(model_runner_mod, "get_active_bundle", lambda params=None: bundle, raising=False)
|
||||
monkeypatch.setattr(model_runner_mod, "_fetch_bundle", lambda params=None: bundle)
|
||||
monkeypatch.setattr(tinygrad_runner_mod, "CUSTOM_MODEL_PATH", str(LOCAL_MODEL_DIR), raising=False)
|
||||
monkeypatch.setattr(model_runner_mod, "CUSTOM_MODEL_PATH", str(LOCAL_MODEL_DIR), raising=False)
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld import metadata, messaging, parser
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.daemon import CaptureStamp, NeuralEngineState
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from iqpilot.selfdrive.iqmodeld import metadata, messaging, parser
|
||||
from iqpilot.selfdrive.iqmodeld.daemon import CaptureStamp, NeuralEngineState
|
||||
|
||||
|
||||
def test_public_module_surface():
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
@@ -5,15 +8,14 @@ from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from tinygrad.nn.onnx import OnnxRunner
|
||||
from tinygrad.tensor import Tensor
|
||||
|
||||
import openpilot.iqpilot.selfdrive.iqmodeld.models.helpers as bundle_helpers
|
||||
import openpilot.iqpilot.selfdrive.iqmodeld.models.runners.model_runner as model_runner_mod
|
||||
import openpilot.iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.tinygrad_runner as tinygrad_runner_mod
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelType
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.tinygrad_runner import TinygradRunner
|
||||
import iqpilot.selfdrive.iqmodeld.models.helpers as bundle_helpers
|
||||
import iqpilot.selfdrive.iqmodeld.models.runners.model_runner as model_runner_mod
|
||||
import iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.tinygrad_runner as tinygrad_runner_mod
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelType
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.tinygrad_runner import TinygradRunner
|
||||
|
||||
|
||||
SHARE_ROOT = Path(os.getenv("IQPILOT_SELECTOR_SHARE", "/Volumes/New New Vault/IQModels/models/recompiled16"))
|
||||
@@ -86,6 +88,7 @@ def _run_tinygrad_bundle(bundle_dir: Path, monkeypatch):
|
||||
bundle = _bundle_for_dir(bundle_dir)
|
||||
monkeypatch.setattr(bundle_helpers, "get_active_bundle", lambda params=None: bundle, raising=False)
|
||||
monkeypatch.setattr(model_runner_mod, "get_active_bundle", lambda params=None: bundle, raising=False)
|
||||
monkeypatch.setattr(model_runner_mod, "_fetch_bundle", lambda: bundle)
|
||||
monkeypatch.setattr(tinygrad_runner_mod, "CUSTOM_MODEL_PATH", str(bundle_dir), raising=False)
|
||||
monkeypatch.setattr(model_runner_mod, "CUSTOM_MODEL_PATH", str(bundle_dir), raising=False)
|
||||
|
||||
@@ -122,8 +125,9 @@ def _run_onnx_bundle(bundle_dir: Path):
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not SHARE_ROOT.is_dir(), reason="selector model share is not mounted")
|
||||
def test_three_selector_models_parse_via_share_onnx():
|
||||
if not SHARE_ROOT.is_dir():
|
||||
return
|
||||
selector_dirs = _find_selector_dirs(limit=3, require_onnx=True)
|
||||
assert len(selector_dirs) >= 3
|
||||
|
||||
@@ -133,8 +137,9 @@ def test_three_selector_models_parse_via_share_onnx():
|
||||
assert policy_raw.size > 0
|
||||
|
||||
|
||||
@pytest.mark.skipif(not SHARE_ROOT.is_dir(), reason="selector model share is not mounted")
|
||||
def test_selector_tinygrad_pkls_execute_when_host_compatible(monkeypatch):
|
||||
if not SHARE_ROOT.is_dir():
|
||||
return
|
||||
selector_dirs = _find_selector_dirs(limit=10)
|
||||
attempted = 0
|
||||
executed = 0
|
||||
@@ -151,6 +156,10 @@ def test_selector_tinygrad_pkls_execute_when_host_compatible(monkeypatch):
|
||||
if "/dev/kgsl-3d0" in str(exc):
|
||||
continue
|
||||
raise
|
||||
except TypeError as exc:
|
||||
if "DType.__init__()" in str(exc):
|
||||
continue
|
||||
raise
|
||||
|
||||
assert "pose" in vision_outputs
|
||||
assert "plan" in policy_outputs
|
||||
@@ -159,4 +168,4 @@ def test_selector_tinygrad_pkls_execute_when_host_compatible(monkeypatch):
|
||||
break
|
||||
|
||||
if executed == 0:
|
||||
pytest.skip(f"share tinygrad pkls are QCOM-only on this host; inspected {attempted} bundles")
|
||||
assert attempted > 0, "no selector bundles were inspected on the share"
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
|
||||
The eMac bundles ship `minimum_selector_version = 17`, and the version
|
||||
gate lives in the COMPILED private selector bundle, not in this repo. If that
|
||||
bundle is rebuilt from stale source the gate still reads 16, every eMac bundle
|
||||
is silently dropped as "too new", and the selector simply shows no eMac models
|
||||
— with no error anywhere. Assert the effective gate instead, so a stale
|
||||
private bundle fails here rather than on a device.
|
||||
"""
|
||||
from iqpilot.selfdrive.iqmodeld.emac_model_meta import EMAC_BUNDLE_MIN_SELECTOR_VERSION
|
||||
from iqpilot.selfdrive.iqmodeld.models.helpers import is_bundle_version_compatible
|
||||
|
||||
|
||||
def test_gate_accepts_the_version_our_emac_bundles_ship():
|
||||
assert is_bundle_version_compatible({"minimumSelectorVersion": EMAC_BUNDLE_MIN_SELECTOR_VERSION}), (
|
||||
f"the effective selector gate rejects minimumSelectorVersion="
|
||||
f"{EMAC_BUNDLE_MIN_SELECTOR_VERSION}; the private selector bundle is stale. "
|
||||
f"Rebuild it from BOTH iqpilot/models_private_src/helpers.py "
|
||||
f"(CURRENT_SELECTOR_VERSION) and fetcher.py (MANIFEST_VERSION)."
|
||||
)
|
||||
|
||||
|
||||
def test_gate_still_accepts_older_bundles():
|
||||
# the window is a range, not a floor: bumping it must not orphan the existing catalogue
|
||||
assert is_bundle_version_compatible({"minimumSelectorVersion": 12})
|
||||
assert is_bundle_version_compatible({"minimumSelectorVersion": 16})
|
||||
|
||||
|
||||
def test_gate_rejects_a_bundle_from_the_future():
|
||||
assert not is_bundle_version_compatible({"minimumSelectorVersion": EMAC_BUNDLE_MIN_SELECTOR_VERSION + 5})
|
||||
131
iqpilot/selfdrive/iqmodeld/tests/test_split_input_state.py
Normal file
131
iqpilot/selfdrive/iqmodeld/tests/test_split_input_state.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
|
||||
eMac split-model "prepared input equivalence": SplitInputState must reproduce,
|
||||
byte-exact, the queue semantics of compile_split_runtime's execute_bundle —
|
||||
the real tinygrad reference graph run on CPU with stub vision/policy runners,
|
||||
over a multi-frame random sequence with desire rising edges.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
os.environ.setdefault("DEV", "CPU")
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.emac_input_state import EmacInputState, SplitInputState
|
||||
|
||||
N_FRAMES_TEST = 30
|
||||
FRAME_SKIP = 4
|
||||
IMG_SHAPE = (1, 12, 16, 32) # small spatial dims: queue math is shape-generic
|
||||
FB_SHAPE = (1, 25, 512)
|
||||
DP_SHAPE = (1, 25, 8)
|
||||
VISION_OUT_LEN = 1576
|
||||
HIDDEN_SLICE = slice(1064, 1576)
|
||||
|
||||
VISION_SHAPES = {"img": IMG_SHAPE, "big_img": IMG_SHAPE}
|
||||
POLICY_SHAPES = {"desire_pulse": DP_SHAPE, "traffic_convention": (1, 2), "features_buffer": FB_SHAPE}
|
||||
|
||||
|
||||
class _StubRunner:
|
||||
"""Stands in for OnnxRunner inside execute_bundle: returns a preset output
|
||||
and records the materialized inputs it was fed."""
|
||||
|
||||
def __init__(self, out_len: int):
|
||||
self.out_len = out_len
|
||||
self.next_output: np.ndarray | None = None
|
||||
self.captured: dict[str, np.ndarray] | None = None
|
||||
|
||||
def __call__(self, inputs):
|
||||
from tinygrad import Tensor
|
||||
self.captured = {k: v.numpy().copy() for k, v in inputs.items()}
|
||||
out = self.next_output if self.next_output is not None else np.zeros((1, self.out_len), dtype=np.float32)
|
||||
return {"outputs": Tensor(out.astype(np.float32))}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def reference():
|
||||
from tinygrad import Tensor
|
||||
from iqpilot.selfdrive.iqmodeld.tools.compile_split_runtime import _role_executor
|
||||
|
||||
meta_by_role = {
|
||||
"vision": {"input_shapes": dict(VISION_SHAPES), "output_slices": {"hidden_state": HIDDEN_SLICE}},
|
||||
"policy": {"input_shapes": dict(POLICY_SHAPES), "output_slices": {}},
|
||||
}
|
||||
vision, policy = _StubRunner(VISION_OUT_LEN), _StubRunner(1000)
|
||||
execute_bundle = _role_executor({"vision": vision, "policy": policy}, meta_by_role, FRAME_SKIP)
|
||||
|
||||
feat_q = Tensor(np.zeros((FRAME_SKIP * (FB_SHAPE[1] - 1) + 1, FB_SHAPE[0], FB_SHAPE[2]), dtype=np.float32),
|
||||
device="CPU").contiguous().realize()
|
||||
desire_q = Tensor(np.zeros((FRAME_SKIP * DP_SHAPE[1], DP_SHAPE[0], DP_SHAPE[2]), dtype=np.float32),
|
||||
device="CPU").contiguous().realize()
|
||||
return execute_bundle, feat_q, desire_q, vision, policy
|
||||
|
||||
|
||||
def test_split_inputs_match_tinygrad_reference(reference):
|
||||
from tinygrad import Tensor
|
||||
|
||||
execute_bundle, feat_q, desire_q, vision_stub, policy_stub = reference
|
||||
rng = np.random.default_rng(4321)
|
||||
state = SplitInputState(FRAME_SKIP, IMG_SHAPE, FB_SHAPE, DP_SHAPE)
|
||||
ref_prev_desire = np.zeros(DP_SHAPE[2], dtype=np.float32)
|
||||
|
||||
for frame in range(N_FRAMES_TEST):
|
||||
warped = rng.integers(0, 256, (2, 6, IMG_SHAPE[2], IMG_SHAPE[3]), dtype=np.int64).astype(np.uint8)
|
||||
raw_desire = np.zeros(DP_SHAPE[2], dtype=np.float32)
|
||||
if frame % 3:
|
||||
raw_desire[int(rng.integers(0, DP_SHAPE[2]))] = 1.0
|
||||
traffic = rng.standard_normal((1, 2)).astype(np.float32)
|
||||
vision_out = rng.standard_normal((1, VISION_OUT_LEN)).astype(np.float32)
|
||||
vision_stub.next_output = vision_out
|
||||
|
||||
# --- ours ---
|
||||
vis_inputs = state.materialize_vision(warped, raw_desire)
|
||||
pol_inputs = state.materialize_policy(vision_out[0, HIDDEN_SLICE], traffic[0])
|
||||
|
||||
# --- reference graph: rising edge happens outside execute_bundle (run_fused) ---
|
||||
cur = raw_desire.copy()
|
||||
cur[0] = 0
|
||||
ref_pulse = np.where(cur - ref_prev_desire > 0.99, cur, 0).astype(np.float32)
|
||||
ref_prev_desire[:] = cur
|
||||
|
||||
execute_bundle(
|
||||
img=Tensor(vis_inputs["img"], device="CPU").realize(),
|
||||
big_img=Tensor(vis_inputs["big_img"], device="CPU").realize(),
|
||||
feat_q=feat_q, desire_q=desire_q,
|
||||
desire=Tensor(ref_pulse, device="CPU").realize(),
|
||||
traffic_convention=Tensor(traffic, device="CPU").realize(),
|
||||
action_t=Tensor(np.zeros((1, 2), dtype=np.float32), device="CPU").realize(),
|
||||
)
|
||||
ref = policy_stub.captured
|
||||
assert ref is not None
|
||||
|
||||
assert ref["features_buffer"].tobytes() == pol_inputs["features_buffer"].tobytes(), f"features frame {frame}"
|
||||
assert ref["desire_pulse"].tobytes() == pol_inputs["desire_pulse"].tobytes(), f"desire frame {frame}"
|
||||
assert ref["traffic_convention"].tobytes() == pol_inputs["traffic_convention"].tobytes()
|
||||
# vision saw exactly what our img queues materialized
|
||||
vref = vision_stub.captured
|
||||
assert vref["img"].tobytes() == vis_inputs["img"].tobytes(), f"img frame {frame}"
|
||||
assert vref["big_img"].tobytes() == vis_inputs["big_img"].tobytes(), f"big_img frame {frame}"
|
||||
|
||||
|
||||
def test_split_img_queue_matches_fused_state():
|
||||
# img/desire mechanics are shared with the fused mirror: same warps must
|
||||
# materialize identical img/big_img in both states
|
||||
rng = np.random.default_rng(7)
|
||||
fused_spec = {
|
||||
"img": (IMG_SHAPE, "uint8"), "big_img": (IMG_SHAPE, "uint8"),
|
||||
"desire_pulse": (DP_SHAPE, "float32"), "traffic_convention": ((1, 2), "float32"),
|
||||
"features_buffer": ((1, 24, 512), "float32"), "action_t": ((1, 2), "float32"),
|
||||
}
|
||||
fused = EmacInputState(FRAME_SKIP, fused_spec)
|
||||
split = SplitInputState(FRAME_SKIP, IMG_SHAPE, FB_SHAPE, DP_SHAPE)
|
||||
for _ in range(12):
|
||||
warped = rng.integers(0, 256, (2, 6, IMG_SHAPE[2], IMG_SHAPE[3]), dtype=np.int64).astype(np.uint8)
|
||||
desire = np.zeros(DP_SHAPE[2], dtype=np.float32)
|
||||
f = fused.push_and_materialize(warped, desire, np.zeros(2, dtype=np.float32), np.zeros(2, dtype=np.float32))
|
||||
s = split.materialize_vision(warped, desire)
|
||||
assert f["img"].tobytes() == s["img"].tobytes()
|
||||
assert f["big_img"].tobytes() == s["big_img"].tobytes()
|
||||
@@ -1,3 +1,6 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
@@ -5,10 +8,10 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from cereal import custom
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.models import helpers as model_helpers
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.tinygrad import supercombo_runner as supercombo_runner_mod
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.supercombo_runner import (
|
||||
from iqpilot.cereal import custom
|
||||
from iqpilot.selfdrive.iqmodeld.models import helpers as model_helpers
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad import supercombo_runner as supercombo_runner_mod
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.supercombo_runner import (
|
||||
TinygradSupercomboRunner,
|
||||
)
|
||||
|
||||
@@ -153,6 +156,31 @@ def test_select_default_model_clears_custom_download_state(tmp_path: Path, monke
|
||||
assert not pending_restore.exists()
|
||||
|
||||
|
||||
def test_seed_default_bundle_runs_while_a_download_is_queued(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(model_helpers, "ensure_default_model_files", lambda *a, **k: None)
|
||||
|
||||
params = _FakeParams()
|
||||
params.put("ModelManager_DownloadIndex", "81")
|
||||
|
||||
model_helpers.seed_default_bundle_if_unset(params)
|
||||
|
||||
active = params.get("ModelManager_ActiveBundle")
|
||||
assert active is not None and active.get("ref") == "default"
|
||||
assert params.get("ModelManager_DownloadIndex") == "81"
|
||||
|
||||
|
||||
def test_seed_default_bundle_leaves_an_existing_active_bundle_alone(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(model_helpers, "ensure_default_model_files", lambda *a, **k: None)
|
||||
|
||||
params = _FakeParams({"index": 81, "ref": "pop"})
|
||||
params.put("ModelManager_DownloadIndex", "81")
|
||||
|
||||
model_helpers.seed_default_bundle_if_unset(params)
|
||||
|
||||
assert params.get("ModelManager_ActiveBundle").get("ref") == "pop"
|
||||
assert params.get("ModelManager_DownloadIndex") == "81"
|
||||
|
||||
|
||||
def test_default_model_is_not_resolved_to_manifest_pop_bundle():
|
||||
pop_bundle = type("Bundle", (), {"internalName": "Pop (Default)", "displayName": "Pop (Default)"})()
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
@@ -6,9 +9,9 @@ from types import SimpleNamespace
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
import openpilot.iqpilot.selfdrive.iqmodeld.models.helpers as bundle_helpers
|
||||
import openpilot.iqpilot.selfdrive.iqmodeld.models.runners.model_runner as runner_helpers
|
||||
import openpilot.iqpilot.selfdrive.iqmodeld.daemon as iqmodeld_daemon
|
||||
import iqpilot.selfdrive.iqmodeld.models.helpers as bundle_helpers
|
||||
import iqpilot.selfdrive.iqmodeld.models.runners.model_runner as runner_helpers
|
||||
import iqpilot.selfdrive.iqmodeld.daemon as iqmodeld_daemon
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -9,8 +9,8 @@ import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
import cereal.messaging as messaging
|
||||
from openpilot.system.manager.process_config import managed_processes
|
||||
import iqpilot.cereal.messaging as messaging
|
||||
from iqpilot.system.manager.process_config import managed_processes
|
||||
|
||||
RUN_COUNT = int(os.getenv("N", "5"))
|
||||
WINDOW_SECONDS = int(os.getenv("TIME", "30"))
|
||||
|
||||
@@ -5,7 +5,7 @@ Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.tools.daemon_jit_compiler import main
|
||||
from iqpilot.selfdrive.iqmodeld.tools.daemon_jit_compiler import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
471
iqpilot/selfdrive/iqmodeld/tools/compile_egpu_model.py
Normal file
471
iqpilot/selfdrive/iqmodeld/tools/compile_egpu_model.py
Normal file
@@ -0,0 +1,471 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import gc
|
||||
import os
|
||||
import pickle
|
||||
import sys
|
||||
import time
|
||||
|
||||
os.environ.setdefault("FLOAT16", "1")
|
||||
os.environ.setdefault("JIT_BATCH_SIZE", "0")
|
||||
os.environ.setdefault("GMMU", "0")
|
||||
# TC_OPT=2 lets tinygrad pick tensor-core kernels; on some models a TC kernel miscompiles and biases
|
||||
# the output (documented on Metal). A parity gate below catches it and re-compiles with TC off.
|
||||
os.environ.setdefault("TC_OPT", "0" if ("--tc-off" in sys.argv or os.environ.get("IQ_EGPU_TC_OFF")) else "2")
|
||||
|
||||
HOST = "--host" in sys.argv
|
||||
if HOST:
|
||||
from iqpilot.selfdrive.iqmodeld.tools.egpu_host_mock import DEFAULT_ARCH, activate
|
||||
activate(sys.argv[sys.argv.index("--arch") + 1] if "--arch" in sys.argv else DEFAULT_ARCH)
|
||||
os.environ.setdefault("DEV", "USB+AMD:LLVM")
|
||||
|
||||
import numpy as np
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_pkl_path, local_onnx, patch_tinygrad_fetch_fw
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_model import EGPU_MODELS, get_egpu_model, resolve_egpu_model
|
||||
from iqpilot.selfdrive.iqmodeld.temporal_state import MODEL_INPUT_SPEC, spec_from_meta
|
||||
|
||||
INPUT_SPEC = dict(MODEL_INPUT_SPEC)
|
||||
|
||||
patch_tinygrad_fetch_fw()
|
||||
|
||||
SEED = 42
|
||||
|
||||
|
||||
class _ParityFail(RuntimeError):
|
||||
pass
|
||||
KERNEL_PROGRESS_SCALE = 260.0
|
||||
|
||||
|
||||
def _progress_sampler(param: str, base: float, span: float, stop) -> None:
|
||||
import math
|
||||
|
||||
from iqpilot.common.params import Params
|
||||
from tinygrad.helpers import GlobalCounters
|
||||
pm = Params()
|
||||
last = -1.0
|
||||
while not stop.wait(0.5):
|
||||
kernels = float(getattr(GlobalCounters, "kernel_count", 0))
|
||||
value = base + span * (1.0 - math.exp(-kernels / KERNEL_PROGRESS_SCALE))
|
||||
if value - last >= 0.01:
|
||||
last = value
|
||||
pm.put(param, f"{min(base + span, value):.3f}")
|
||||
|
||||
|
||||
def set_input_spec(meta: dict) -> None:
|
||||
spec = spec_from_meta(meta)
|
||||
if spec is not None:
|
||||
INPUT_SPEC.clear()
|
||||
INPUT_SPEC.update(spec)
|
||||
|
||||
|
||||
def make_run_model(model_runner):
|
||||
def run_model(**inputs):
|
||||
out = next(iter(model_runner({k: inputs[k] for k in INPUT_SPEC}).values())).cast("float32")
|
||||
return out.reshape(-1),
|
||||
return run_model
|
||||
|
||||
|
||||
def _random_inputs(seed: int):
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.tensor import Tensor
|
||||
rng = np.random.default_rng(seed)
|
||||
out = {}
|
||||
for name, (shape, dtype) in INPUT_SPEC.items():
|
||||
if dtype == "uint8":
|
||||
arr = rng.integers(0, 256, shape).astype(np.uint8)
|
||||
else:
|
||||
arr = rng.standard_normal(shape).astype(np.float32)
|
||||
out[name] = Tensor(arr, device=Device.DEFAULT).realize()
|
||||
return out
|
||||
|
||||
|
||||
def _run(fn, seed: int) -> np.ndarray:
|
||||
from tinygrad.device import Device
|
||||
st = time.perf_counter()
|
||||
outs = fn(**_random_inputs(seed))
|
||||
Device.default.synchronize()
|
||||
print(f" run(seed={seed}) {(time.perf_counter() - st) * 1e3:6.1f} ms")
|
||||
return outs[0].numpy().reshape(-1)
|
||||
|
||||
|
||||
def compile_model(meta: dict, onnx_path: str, out_path: str) -> str:
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.engine.jit import TinyJit
|
||||
from tinygrad.nn.onnx import OnnxRunner
|
||||
|
||||
if meta.get("split"):
|
||||
raise RuntimeError(f"model {meta['key']} is a split model; eGPU v1 compiles fused models only")
|
||||
|
||||
jit = TinyJit(make_run_model(OnnxRunner(onnx_path)), prune=True)
|
||||
|
||||
print("capture + replay")
|
||||
for _ in range(2):
|
||||
baseline = _run(jit, SEED)
|
||||
if baseline.shape[0] != meta["output_len"]:
|
||||
raise RuntimeError(f"model output length {baseline.shape[0]} != registry {meta['output_len']}")
|
||||
if not np.isfinite(baseline).all():
|
||||
raise RuntimeError("compiled model produced non-finite outputs")
|
||||
|
||||
bundle = {
|
||||
"run_model": jit,
|
||||
"model_key": meta["key"],
|
||||
"model_sha256": meta["sha256"],
|
||||
"output_len": int(meta["output_len"]),
|
||||
"frame_skip": int(meta["frame_skip"]),
|
||||
"input_spec": {name: (tuple(shape), dtype) for name, (shape, dtype) in INPUT_SPEC.items()},
|
||||
"input_device": Device.DEFAULT,
|
||||
}
|
||||
os.makedirs(os.path.dirname(out_path), exist_ok=True)
|
||||
tmp = out_path + ".part"
|
||||
print("serialize")
|
||||
with open(tmp, "wb") as f:
|
||||
pickle.dump(bundle, f, protocol=pickle.HIGHEST_PROTOCOL)
|
||||
|
||||
del bundle, jit
|
||||
gc.collect()
|
||||
|
||||
print("reload + validate")
|
||||
with open(tmp, "rb") as f:
|
||||
jit = pickle.load(f)["run_model"]
|
||||
if not np.array_equal(_run(jit, SEED), baseline):
|
||||
raise RuntimeError("outputs differ from baseline after pickle round trip")
|
||||
if np.array_equal(_run(jit, SEED + 1), baseline):
|
||||
raise RuntimeError("outputs insensitive to inputs after pickle round trip")
|
||||
|
||||
from tinygrad.tensor import Tensor
|
||||
zeros = {name: Tensor(np.zeros(shape, dtype=dtype), device=Device.DEFAULT).realize()
|
||||
for name, (shape, dtype) in INPUT_SPEC.items()}
|
||||
flat = jit(**zeros)[0].numpy().reshape(-1)
|
||||
from iqpilot.selfdrive.iqmodeld.parser import PhaseParser
|
||||
from iqpilot.selfdrive.iqmodeld.tools.compile_supercombo import _slice_outputs, _validate_pose_outputs
|
||||
_validate_pose_outputs(PhaseParser().parse_vision_outputs(_slice_outputs(flat, meta["output_slices"])))
|
||||
|
||||
os.replace(tmp, out_path)
|
||||
return out_path
|
||||
|
||||
|
||||
def _policy_frame(seed: int, input_spec: dict):
|
||||
from tinygrad.tensor import Tensor
|
||||
rng = np.random.default_rng(seed)
|
||||
img = input_spec["img"][0]
|
||||
warped = Tensor(rng.integers(0, 256, (2, 6, img[2], img[3])).astype(np.uint8), device="NPY").realize()
|
||||
return warped
|
||||
|
||||
|
||||
def _tc_off_reference(onnx_path: str, meta: dict, fmt: int = 2, resolutions: tuple[tuple[int, int], ...] = ()):
|
||||
"""Compile+run the model with tensor cores OFF in a child process and return the last of 3
|
||||
policy frames. This is the trusted reference: TC-off kernels are the conservative path the
|
||||
eMac gate also trusts. Used to catch a TC kernel miscompile that would bias steering."""
|
||||
import subprocess
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
ref = os.path.join(td, "ref.npz" if fmt == 3 else "ref.npy")
|
||||
env = {k: v for k, v in os.environ.items() if k not in ("TC_OPT", "BEAM")}
|
||||
env["TC_OPT"] = "0"
|
||||
env["IQ_EGPU_REFERENCE"] = ref
|
||||
cmd = [sys.executable, "-m", "iqpilot.selfdrive.iqmodeld.tools.compile_egpu_model",
|
||||
"--model", meta["key"], "--onnx", onnx_path, "--tc-off", "--format", str(fmt)]
|
||||
if resolutions:
|
||||
cmd += ["--camera-resolutions", *(f"{w}x{h}" for w, h in resolutions)]
|
||||
r = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=14400)
|
||||
if r.returncode != 0 or not os.path.isfile(ref):
|
||||
raise RuntimeError(f"parity reference compile failed:\n{r.stderr[-2000:]}")
|
||||
return np.load(ref)
|
||||
|
||||
|
||||
def _parity_check(key: str, got: np.ndarray, ref: np.ndarray, label: str = "") -> None:
|
||||
rel = float(np.abs(got - ref).mean() / max(1e-3, float(np.abs(ref).mean())))
|
||||
if rel > 0.01:
|
||||
raise _ParityFail(f"PARITY FAIL: TC kernels miscompiled {key} {label}(rel={rel:.4f} vs TC-off); recompiling with tensor cores disabled")
|
||||
print(f" parity vs TC-off reference {label}: rel={rel:.6f} OK")
|
||||
|
||||
|
||||
def compile_policy_model(meta: dict, onnx_path: str, out_path: str) -> str:
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.engine.jit import TinyJit
|
||||
from tinygrad.nn.onnx import OnnxRunner
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_policy import POLICY_FORMAT, PackedInputs, dump_oob, load_bundle, make_queues, make_run_policy
|
||||
|
||||
if meta.get("split"):
|
||||
raise RuntimeError(f"model {meta['key']} is a split model; eGPU compiles fused models only")
|
||||
input_spec = {name: (tuple(shape), dtype) for name, (shape, dtype) in INPUT_SPEC.items()}
|
||||
frame_skip = int(meta["frame_skip"])
|
||||
device = Device.DEFAULT
|
||||
jit = TinyJit(make_run_policy(OnnxRunner(onnx_path), input_spec, frame_skip, device), prune=True)
|
||||
queues = make_queues(input_spec, frame_skip, device)
|
||||
packed = PackedInputs(input_spec)
|
||||
|
||||
def step(seed: int) -> np.ndarray:
|
||||
packed.views["traffic_convention"][:] = [1, 0]
|
||||
packed.views["action_t"][:] = [0.2, 0.3]
|
||||
st = time.perf_counter()
|
||||
out, = jit(warped=_policy_frame(seed, input_spec), packed_npy_inputs=packed.tensor, **queues)
|
||||
flat = out.numpy().reshape(-1)
|
||||
print(f" policy step(seed={seed}) {(time.perf_counter() - st) * 1e3:6.1f} ms")
|
||||
packed.views["prev_feat"][:] = flat[meta["output_slices"]["hidden_state"]].reshape(packed.views["prev_feat"].shape)
|
||||
return flat
|
||||
|
||||
print("capture + replay")
|
||||
for i in range(3):
|
||||
baseline = step(SEED + i)
|
||||
if baseline.shape[0] != meta["output_len"]:
|
||||
raise RuntimeError(f"model output length {baseline.shape[0]} != registry {meta['output_len']}")
|
||||
if not HOST and not np.isfinite(baseline).all():
|
||||
raise RuntimeError("compiled policy produced non-finite outputs")
|
||||
|
||||
bundle = {
|
||||
"format": POLICY_FORMAT,
|
||||
"run_policy": jit,
|
||||
"model_key": meta["key"],
|
||||
"model_sha256": meta["sha256"],
|
||||
"output_len": int(meta["output_len"]),
|
||||
"frame_skip": frame_skip,
|
||||
"input_spec": input_spec,
|
||||
"input_device": device,
|
||||
}
|
||||
os.makedirs(os.path.dirname(out_path), exist_ok=True)
|
||||
tmp = out_path + ".part"
|
||||
print("serialize (out-of-band buffers)")
|
||||
with open(tmp, "wb") as f:
|
||||
dump_oob(bundle, f)
|
||||
|
||||
del bundle, jit, queues, packed
|
||||
gc.collect()
|
||||
|
||||
print("reload + validate")
|
||||
jit = load_bundle(tmp)["run_policy"]
|
||||
queues = make_queues(input_spec, frame_skip, device)
|
||||
packed = PackedInputs(input_spec)
|
||||
outs = []
|
||||
for i in range(3):
|
||||
packed.views["traffic_convention"][:] = [1, 0]
|
||||
packed.views["action_t"][:] = [0.2, 0.3]
|
||||
out, = jit(warped=_policy_frame(SEED + i, input_spec), packed_npy_inputs=packed.tensor, **queues)
|
||||
flat = out.numpy().reshape(-1)
|
||||
packed.views["prev_feat"][:] = flat[meta["output_slices"]["hidden_state"]].reshape(packed.views["prev_feat"].shape)
|
||||
outs.append(flat)
|
||||
ref_target = os.environ.get("IQ_EGPU_REFERENCE")
|
||||
if ref_target:
|
||||
np.save(ref_target, outs[-1])
|
||||
return out_path
|
||||
if HOST:
|
||||
os.replace(tmp, out_path)
|
||||
return out_path
|
||||
if not np.array_equal(outs[-1], baseline):
|
||||
raise RuntimeError("policy outputs differ from baseline after pickle round trip")
|
||||
if np.array_equal(outs[0], outs[-1]):
|
||||
raise RuntimeError("policy outputs insensitive to inputs after pickle round trip")
|
||||
if not all(np.isfinite(o).all() for o in outs):
|
||||
raise RuntimeError("reloaded policy produced non-finite outputs")
|
||||
from iqpilot.selfdrive.iqmodeld.parser import PhaseParser
|
||||
from iqpilot.selfdrive.iqmodeld.tools.compile_supercombo import _slice_outputs, _validate_pose_outputs
|
||||
_validate_pose_outputs(PhaseParser().parse_vision_outputs(_slice_outputs(outs[-1], meta["output_slices"])))
|
||||
|
||||
if os.environ.get("TC_OPT") != "0" and not os.environ.get("IQ_EGPU_SKIP_PARITY"):
|
||||
_parity_check(meta["key"], outs[-1], _tc_off_reference(onnx_path, meta))
|
||||
|
||||
os.replace(tmp, out_path)
|
||||
return out_path
|
||||
|
||||
|
||||
DEFAULT_CAMERA_RESOLUTIONS: tuple[tuple[int, int], ...] = ((1928, 1208), (1344, 760))
|
||||
|
||||
|
||||
def camera_nv12(cam_w: int, cam_h: int) -> tuple[int, int, int, int, int]:
|
||||
from iqpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||
stride, y_height, uv_height, _ = get_nv12_info(cam_w, cam_h)
|
||||
return (cam_w, cam_h, stride, y_height, uv_height)
|
||||
|
||||
|
||||
def _fill_model_frame(packed, seed: int, res: tuple[int, int], model_w: int, model_h: int) -> None:
|
||||
rng = np.random.default_rng(seed)
|
||||
cam_w, cam_h = res
|
||||
scale = np.array([[cam_w / model_w, 0.0, 0.0], [0.0, cam_h / model_h, 0.0], [0.0, 0.0, 1.0]], dtype=np.float32)
|
||||
for name in ("tfm", "big_tfm"):
|
||||
packed.views[name][:, :] = scale * (1.0 + 0.02 * rng.standard_normal((3, 3))).astype(np.float32)
|
||||
for v in packed.frames.values():
|
||||
v[:] = rng.integers(0, 256, size=v.shape, dtype=np.uint8)
|
||||
packed.views["traffic_convention"][:] = [1, 0]
|
||||
packed.views["action_t"][:] = [0.2, 0.3]
|
||||
|
||||
|
||||
def compile_model_v3(meta: dict, onnx_path: str, out_path: str,
|
||||
resolutions: tuple[tuple[int, int], ...] = DEFAULT_CAMERA_RESOLUTIONS) -> str:
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.engine.jit import TinyJit
|
||||
from tinygrad.nn.onnx import OnnxRunner
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_policy import (
|
||||
MODEL_FORMAT, dump_oob, load_bundle, make_model_queues, make_run_model, make_run_policy, make_warp, model_size, nv12_copy_size,
|
||||
)
|
||||
|
||||
if meta.get("split"):
|
||||
raise RuntimeError(f"model {meta['key']} is a split model; eGPU compiles fused models only")
|
||||
input_spec = {name: (tuple(shape), dtype) for name, (shape, dtype) in INPUT_SPEC.items()}
|
||||
frame_skip = int(meta["frame_skip"])
|
||||
hidden = meta["output_slices"]["hidden_state"]
|
||||
device = Device.DEFAULT
|
||||
model_w, model_h = model_size(input_spec)
|
||||
runner = OnnxRunner(onnx_path)
|
||||
run_policy = make_run_policy(runner, input_spec, frame_skip, device)
|
||||
|
||||
def step(jit, queues, packed, seed: int, res: tuple[int, int]) -> np.ndarray:
|
||||
_fill_model_frame(packed, seed, res, model_w, model_h)
|
||||
st = time.perf_counter()
|
||||
out, = jit(**queues)
|
||||
flat = out.numpy().reshape(-1)
|
||||
print(f" model step(seed={seed}, {res[0]}x{res[1]}) {(time.perf_counter() - st) * 1e3:6.1f} ms")
|
||||
packed.views["prev_feat"][:] = flat[hidden].reshape(packed.views["prev_feat"].shape)
|
||||
return flat
|
||||
|
||||
def run_three(jit, fcs: int, res: tuple[int, int]) -> list[np.ndarray]:
|
||||
queues, packed = make_model_queues(input_spec, frame_skip, device, fcs)
|
||||
return [step(jit, queues, packed, SEED + i, res) for i in range(3)]
|
||||
|
||||
jits: dict[tuple[int, int], object] = {}
|
||||
sizes: dict[tuple[int, int], int] = {}
|
||||
nv12s: dict[tuple[int, int], tuple[int, int, int, int, int]] = {}
|
||||
baselines: dict[tuple[int, int], np.ndarray] = {}
|
||||
for res in resolutions:
|
||||
nv12 = camera_nv12(*res)
|
||||
fcs = nv12_copy_size(nv12[2], nv12[3], nv12[4])
|
||||
jit = TinyJit(make_run_model(make_warp(nv12, model_w, model_h, device), run_policy, input_spec, fcs, device), prune=True)
|
||||
print(f"capture + replay {res[0]}x{res[1]} (frame copy {fcs} B)")
|
||||
baseline = run_three(jit, fcs, res)[-1]
|
||||
if baseline.shape[0] != meta["output_len"]:
|
||||
raise RuntimeError(f"model output length {baseline.shape[0]} != registry {meta['output_len']}")
|
||||
if not HOST and not np.isfinite(baseline).all():
|
||||
raise RuntimeError("compiled model produced non-finite outputs")
|
||||
jits[res], sizes[res], nv12s[res], baselines[res] = jit, fcs, nv12, baseline
|
||||
|
||||
bundle = {
|
||||
"format": MODEL_FORMAT,
|
||||
"run_model": jits,
|
||||
"frame_copy_size": sizes,
|
||||
"nv12": nv12s,
|
||||
"model_key": meta["key"],
|
||||
"model_sha256": meta["sha256"],
|
||||
"output_len": int(meta["output_len"]),
|
||||
"frame_skip": frame_skip,
|
||||
"input_spec": input_spec,
|
||||
"input_device": device,
|
||||
}
|
||||
os.makedirs(os.path.dirname(out_path), exist_ok=True)
|
||||
tmp = out_path + ".part"
|
||||
print("serialize (out-of-band buffers)")
|
||||
with open(tmp, "wb") as f:
|
||||
dump_oob(bundle, f)
|
||||
|
||||
del bundle, jits, run_policy, runner
|
||||
gc.collect()
|
||||
|
||||
print("reload + validate")
|
||||
loaded = load_bundle(tmp)
|
||||
outs = {res: run_three(loaded["run_model"][res], loaded["frame_copy_size"][res], res) for res in resolutions}
|
||||
ref_target = os.environ.get("IQ_EGPU_REFERENCE")
|
||||
if ref_target:
|
||||
np.savez(ref_target, **{f"{w}x{h}": outs[(w, h)][-1] for (w, h) in resolutions})
|
||||
return out_path
|
||||
if HOST:
|
||||
os.replace(tmp, out_path)
|
||||
return out_path
|
||||
for res in resolutions:
|
||||
if not np.array_equal(outs[res][-1], baselines[res]):
|
||||
raise RuntimeError(f"model outputs differ from baseline after pickle round trip ({res[0]}x{res[1]})")
|
||||
if np.array_equal(outs[res][0], outs[res][-1]):
|
||||
raise RuntimeError(f"model outputs insensitive to inputs after pickle round trip ({res[0]}x{res[1]})")
|
||||
if not all(np.isfinite(o).all() for o in outs[res]):
|
||||
raise RuntimeError(f"reloaded model produced non-finite outputs ({res[0]}x{res[1]})")
|
||||
from iqpilot.selfdrive.iqmodeld.parser import PhaseParser
|
||||
from iqpilot.selfdrive.iqmodeld.tools.compile_supercombo import _slice_outputs, _validate_pose_outputs
|
||||
_validate_pose_outputs(PhaseParser().parse_vision_outputs(_slice_outputs(outs[resolutions[0]][-1], meta["output_slices"])))
|
||||
|
||||
if os.environ.get("TC_OPT") != "0" and not os.environ.get("IQ_EGPU_SKIP_PARITY"):
|
||||
ref = _tc_off_reference(onnx_path, meta, fmt=3, resolutions=resolutions)
|
||||
for (w, h) in resolutions:
|
||||
_parity_check(meta["key"], outs[(w, h)][-1], ref[f"{w}x{h}"], label=f"{w}x{h} ")
|
||||
|
||||
os.replace(tmp, out_path)
|
||||
return out_path
|
||||
|
||||
|
||||
def _parse_resolution(text: str) -> tuple[int, int]:
|
||||
w, h = text.lower().split("x")
|
||||
return int(w), int(h)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--model", default=None, help=f"registry key, one of {sorted(EGPU_MODELS)}")
|
||||
p.add_argument("--onnx", default=None)
|
||||
p.add_argument("--output", default=None)
|
||||
p.add_argument("--progress-param", default=None)
|
||||
p.add_argument("--progress-base", type=float, default=None)
|
||||
p.add_argument("--progress-span", type=float, default=0.0)
|
||||
p.add_argument("--format", type=int, default=3, choices=(1, 2, 3),
|
||||
help="3 = warp on the dock from raw NV12 (comma master); 2 = device-warped policy bundle")
|
||||
p.add_argument("--camera-resolutions", type=_parse_resolution, nargs="+", default=list(DEFAULT_CAMERA_RESOLUTIONS),
|
||||
help="WxH camera sizes bundled into a format-3 artifact")
|
||||
p.add_argument("--host", action="store_true", help="compile on a mock dock (no AMD hardware); outputs need a dock parity gate")
|
||||
p.add_argument("--arch", default=None, help="target gfx arch for --host")
|
||||
p.add_argument("--tc-off", action="store_true", help="disable tensor-core kernels (conservative; auto-set on parity failure)")
|
||||
args = p.parse_args()
|
||||
if args.host and args.format == 1:
|
||||
raise SystemExit("--host supports formats 2 and 3 only")
|
||||
|
||||
if args.model is not None:
|
||||
if args.model in EGPU_MODELS:
|
||||
meta = get_egpu_model(args.model)
|
||||
else:
|
||||
from iqpilot.common.params import Params
|
||||
meta = resolve_egpu_model(Params(), args.model)
|
||||
if meta is None:
|
||||
raise SystemExit(f"unknown model {args.model!r}: not a built-in ({sorted(EGPU_MODELS)}) and not in the synced catalog")
|
||||
else:
|
||||
meta = get_egpu_model()
|
||||
set_input_spec(meta)
|
||||
|
||||
onnx_path = args.onnx or local_onnx(meta)
|
||||
if onnx_path is None or not os.path.isfile(onnx_path):
|
||||
raise SystemExit(f"onnx not found for {meta['key']}; pass --onnx or let iqegpumodeld download it first")
|
||||
|
||||
stop = None
|
||||
sampler = None
|
||||
if args.progress_param and args.progress_base is not None:
|
||||
import threading
|
||||
stop = threading.Event()
|
||||
sampler = threading.Thread(target=_progress_sampler,
|
||||
args=(args.progress_param, args.progress_base, args.progress_span, stop),
|
||||
daemon=True)
|
||||
sampler.start()
|
||||
try:
|
||||
if args.format == 3:
|
||||
from functools import partial
|
||||
build = partial(compile_model_v3, resolutions=tuple(args.camera_resolutions))
|
||||
else:
|
||||
build = compile_policy_model if args.format == 2 else compile_model
|
||||
try:
|
||||
out = build(meta, onnx_path, args.output or egpu_pkl_path(meta))
|
||||
except _ParityFail as e:
|
||||
if os.environ.get("TC_OPT") == "0" or args.format == 1:
|
||||
raise
|
||||
print(f"{e}\nretrying compile with tensor cores disabled", flush=True)
|
||||
os.environ["TC_OPT"] = "0"
|
||||
os.environ["IQ_EGPU_TC_OFF"] = "1"
|
||||
out = build(meta, onnx_path, args.output or egpu_pkl_path(meta))
|
||||
finally:
|
||||
if stop is not None:
|
||||
stop.set()
|
||||
if sampler is not None:
|
||||
sampler.join(timeout=2)
|
||||
print(f"saved eGPU jit to {out} ({os.path.getsize(out) / 1e6:.2f} MB)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
9
iqpilot/selfdrive/iqmodeld/tools/compile_emac_warp.py
Normal file
9
iqpilot/selfdrive/iqmodeld/tools/compile_emac_warp.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from iqpilot.selfdrive.iqmodeld.tools.compile_warp import MODEL_SIZE, compile_warp, main
|
||||
|
||||
__all__ = ["MODEL_SIZE", "compile_warp", "main"]
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
125
iqpilot/selfdrive/iqmodeld/tools/compile_model.py
Normal file
125
iqpilot/selfdrive/iqmodeld/tools/compile_model.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
import os
|
||||
import pickle
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
if "JIT_BATCH_SIZE" not in os.environ:
|
||||
os.environ["JIT_BATCH_SIZE"] = "0"
|
||||
|
||||
from tinygrad import Context, Device, GlobalCounters, Tensor, TinyJit, dtypes
|
||||
from tinygrad.helpers import DEBUG, getenv
|
||||
from tinygrad.nn.onnx import OnnxRunner
|
||||
from tinygrad.uop.ops import Ops
|
||||
|
||||
|
||||
def compile_model(onnx_file, output):
|
||||
run_onnx = OnnxRunner(onnx_file)
|
||||
print("loaded model")
|
||||
|
||||
input_shapes = {name: spec.shape for name, spec in run_onnx.graph_inputs.items()}
|
||||
input_types = {name: spec.dtype for name, spec in run_onnx.graph_inputs.items()}
|
||||
input_types = {key: dtypes.float32 if value is dtypes.float16 else value for key, value in input_types.items()}
|
||||
input_shapes = {key: tuple(value if isinstance(value, int) else 1 for value in shape) for key, shape in input_shapes.items()}
|
||||
|
||||
Tensor.manual_seed(100)
|
||||
inputs = {
|
||||
key: Tensor(Tensor.randn(*shape, dtype=input_types[key]).mul(8).realize().numpy(), device="NPY")
|
||||
for key, shape in sorted(input_shapes.items())
|
||||
}
|
||||
if not getenv("NPY_IMG"):
|
||||
inputs = {key: Tensor(value.numpy(), device=Device.DEFAULT).realize() if "img" in key else value for key, value in inputs.items()}
|
||||
print("created tensors")
|
||||
|
||||
run_onnx_jit = TinyJit(
|
||||
lambda **kwargs: next(iter(run_onnx({key: value.to(Device.DEFAULT) for key, value in kwargs.items()}).values())).cast("float32"),
|
||||
prune=True,
|
||||
)
|
||||
test_value = None
|
||||
for iteration in range(3):
|
||||
GlobalCounters.reset()
|
||||
print(f"run {iteration}")
|
||||
with Context(DEBUG=max(DEBUG.value, 2 if iteration == 2 else 1), OPENPILOT_HACKS=1):
|
||||
result = run_onnx_jit(**inputs).numpy()
|
||||
if iteration == 1:
|
||||
test_value = np.copy(result)
|
||||
|
||||
kernel_asts = {Ops.PROGRAM}
|
||||
kernel_calls = [
|
||||
node for node in run_onnx_jit.captured.linear.toposort(gate=lambda value: value.op not in kernel_asts)
|
||||
if node.op is Ops.CALL and node.src[0].op in kernel_asts
|
||||
]
|
||||
print(f"captured {len(kernel_calls)} kernels")
|
||||
np.testing.assert_equal(test_value, result, "JIT run failed")
|
||||
print("jit run validated")
|
||||
|
||||
kernel_count = 0
|
||||
read_image_count = 0
|
||||
gated_read_image_count = 0
|
||||
for call in kernel_calls:
|
||||
_, _, source, _ = call.src[0].src
|
||||
rendered = source.arg
|
||||
kernel_count += 1
|
||||
read_image_count += rendered.count("read_image")
|
||||
gated_read_image_count += rendered.count("?read_image")
|
||||
for value in (match.group(1) for match in re.finditer(r"(val\d+)\s*=\s*read_imagef\(", rendered)):
|
||||
if re.search(fr"[?:]{value}\.[xyzw]", rendered):
|
||||
gated_read_image_count += 1
|
||||
|
||||
print(f"{kernel_count=}, {read_image_count=}, {gated_read_image_count=}")
|
||||
expected = {
|
||||
"kernel count": (kernel_count, getenv("ALLOWED_KERNEL_COUNT", -1)),
|
||||
"read image count": (read_image_count, getenv("ALLOWED_READ_IMAGE", -1)),
|
||||
"gated read image count": (gated_read_image_count, getenv("ALLOWED_GATED_READ_IMAGE", -1)),
|
||||
}
|
||||
for name, (actual, allowed) in expected.items():
|
||||
if allowed != -1:
|
||||
assert actual == allowed, f"different {name}: {actual}, expected {allowed}"
|
||||
|
||||
with open(output, "wb") as handle:
|
||||
pickle.dump(run_onnx_jit, handle)
|
||||
print(f"model size is {os.path.getsize(onnx_file) / 1e6:.2f}M")
|
||||
print(f"pkl size is {os.path.getsize(output) / 1e6:.2f}M")
|
||||
return run_onnx_jit, inputs, test_value
|
||||
|
||||
|
||||
def test_compiled(run, inputs, test_value):
|
||||
step_times = []
|
||||
for _ in range(20):
|
||||
start = time.perf_counter()
|
||||
output = run(**inputs)
|
||||
queued = time.perf_counter()
|
||||
value = output.numpy()
|
||||
end = time.perf_counter()
|
||||
step_times.append((end - start) * 1e3)
|
||||
print(f"enqueue {(queued - start) * 1e3:6.2f} ms -- total run {step_times[-1]:6.2f} ms")
|
||||
|
||||
minimum = getenv("ASSERT_MIN_STEP_TIME", 0.0)
|
||||
if minimum:
|
||||
assert min(step_times) < minimum, f"expected minimum step time below {minimum} ms, got {min(step_times)} ms"
|
||||
np.testing.assert_equal(test_value, value)
|
||||
changed_inputs = {key: Tensor(item.numpy() * 2, device=item.device) for key, item in inputs.items()}
|
||||
changed_value = run(**changed_inputs).numpy()
|
||||
np.testing.assert_raises(AssertionError, np.testing.assert_array_equal, value, changed_value)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
model_path = sys.argv[1]
|
||||
output_path = sys.argv[2]
|
||||
if stash := os.environ.get("IQPILOT_MODEL_STASH"):
|
||||
stashed_model = os.path.join(stash, os.path.basename(output_path))
|
||||
if os.path.isfile(stashed_model) and os.path.getsize(stashed_model) > 0:
|
||||
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
|
||||
shutil.copyfile(stashed_model, output_path)
|
||||
print(f"restored device-compiled model: {output_path}")
|
||||
sys.exit(0)
|
||||
_, input_values, expected_value = compile_model(model_path, output_path)
|
||||
with open(output_path, "rb") as compiled_file:
|
||||
compiled_model = pickle.load(compiled_file)
|
||||
test_compiled(compiled_model, input_values, expected_value)
|
||||
@@ -62,8 +62,8 @@ WARP_DEVICE = os.getenv("WARP_DEV")
|
||||
|
||||
|
||||
def _read_shared_copy(path: str) -> str:
|
||||
from openpilot.common.file_chunker import read_file_chunked
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
from iqpilot.common.file_chunker import read_file_chunked
|
||||
from iqpilot.system.hardware.hw import Paths
|
||||
|
||||
shm_path = os.path.join(Paths.shm_path(), os.path.basename(path))
|
||||
atexit.register(lambda: os.path.exists(shm_path) and os.remove(shm_path))
|
||||
@@ -127,8 +127,8 @@ def _project_pixels(src_flat, inverse_matrix, dst_shape, src_shape, stride_pad,
|
||||
dst_w, dst_h = dst_shape
|
||||
src_h, src_w = src_shape
|
||||
|
||||
x_coords = Tensor.arange(dst_w, device=WARP_DEVICE).reshape(1, dst_w).expand(dst_h, dst_w).reshape(-1)
|
||||
y_coords = Tensor.arange(dst_h, device=WARP_DEVICE).reshape(dst_h, 1).expand(dst_h, dst_w).reshape(-1)
|
||||
x_coords = Tensor.arange(dst_w).to(WARP_DEVICE).reshape(1, dst_w).expand(dst_h, dst_w).reshape(-1)
|
||||
y_coords = Tensor.arange(dst_h).to(WARP_DEVICE).reshape(dst_h, 1).expand(dst_h, dst_w).reshape(-1)
|
||||
|
||||
src_x = inverse_matrix[0, 0] * x_coords + inverse_matrix[0, 1] * y_coords + inverse_matrix[0, 2]
|
||||
src_y = inverse_matrix[1, 0] * x_coords + inverse_matrix[1, 1] * y_coords + inverse_matrix[1, 2]
|
||||
@@ -352,8 +352,8 @@ def _arg_parser() -> argparse.ArgumentParser:
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.metadata import build_metadata_record
|
||||
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||
from iqpilot.selfdrive.iqmodeld.metadata import build_metadata_record
|
||||
from iqpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||
|
||||
args = _arg_parser().parse_args(argv)
|
||||
model_w, model_h = args.model_size
|
||||
|
||||
@@ -59,7 +59,6 @@ def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad,
|
||||
x = Tensor.arange(w_dst).reshape(1, w_dst).expand(h_dst, w_dst).reshape(-1)
|
||||
y = Tensor.arange(h_dst).reshape(h_dst, 1).expand(h_dst, w_dst).reshape(-1)
|
||||
|
||||
# inline 3x3 matmul as elementwise to avoid reduce op (enables fusion with gather)
|
||||
src_x = M_inv[0, 0] * x + M_inv[0, 1] * y + M_inv[0, 2]
|
||||
src_y = M_inv[1, 0] * x + M_inv[1, 1] * y + M_inv[1, 2]
|
||||
src_w = M_inv[2, 0] * x + M_inv[2, 1] * y + M_inv[2, 2]
|
||||
@@ -100,9 +99,7 @@ def make_frame_prepare(nv12: NV12Frame, model_w, model_h):
|
||||
stride_pad = stride - cam_w
|
||||
|
||||
def frame_prepare_tinygrad(input_frame, M_inv):
|
||||
# UV_SCALE @ M_inv @ UV_SCALE_INV simplifies to elementwise scaling
|
||||
M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]], device=WARP_DEV)
|
||||
# deinterleave NV12 UV plane (UVUV... -> separate U, V)
|
||||
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],
|
||||
@@ -142,7 +139,6 @@ def get_policy_npy_shapes(input_shapes):
|
||||
tc = input_shapes['traffic_convention'] # (1, 2)
|
||||
at = input_shapes['action_t'] # (1, 2)
|
||||
fb = input_shapes['features_buffer'] # (1, 24, 512)
|
||||
# TODO prev_feat shouldn't exist and be handled inside the JIT, but corrupt on QCOM for now
|
||||
shapes = {'desire': (dp[2],), 'traffic_convention': tuple(tc), 'action_t': tuple(at), 'prev_feat': (fb[0], fb[2])}
|
||||
return shapes, [math.prod(s) for s in shapes.values()]
|
||||
|
||||
@@ -155,7 +151,6 @@ def make_input_queues(input_shapes, frame_skip, device):
|
||||
|
||||
shapes, sizes = get_policy_npy_shapes(input_shapes)
|
||||
packed_npy_inputs = np.zeros(sum(sizes), dtype=np.float32)
|
||||
# views into the packed inputs, to be refilled at runtime
|
||||
npy.update({k: v.reshape(s) for (k, s), v in zip(shapes.items(), np.split(packed_npy_inputs, np.cumsum(sizes[:-1])), strict=True)})
|
||||
input_queues.update({
|
||||
'feat_q': Tensor(np.zeros((frame_skip * fb[1], fb[0], fb[2]), dtype=np.float32), device=device).contiguous().realize(),
|
||||
@@ -284,7 +279,7 @@ def _slice_outputs(model_outputs: np.ndarray, output_slices: dict[str, slice]) -
|
||||
|
||||
|
||||
def _validate_pose_outputs(parsed_outputs: dict[str, np.ndarray]) -> None:
|
||||
from openpilot.selfdrive.locationd.locationd import MIN_STD_SANITY_CHECK, ROTATION_SANITY_CHECK, TRANS_SANITY_CHECK
|
||||
from iqpilot.selfdrive.locationd.locationd import MIN_STD_SANITY_CHECK, ROTATION_SANITY_CHECK, TRANS_SANITY_CHECK
|
||||
|
||||
required = (
|
||||
'pose', 'pose_stds', 'wide_from_device_euler', 'wide_from_device_euler_stds',
|
||||
@@ -326,7 +321,7 @@ def _validate_pose_outputs(parsed_outputs: dict[str, np.ndarray]) -> None:
|
||||
|
||||
|
||||
def validate_supercombo_release(run_policy_jit, model_runner, model_metadata, frame_skip, expected_device: str) -> None:
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.parser import PhaseParser
|
||||
from iqpilot.selfdrive.iqmodeld.parser import PhaseParser
|
||||
|
||||
direct_fn = make_run_policy(model_runner, model_metadata, frame_skip)
|
||||
parser = PhaseParser()
|
||||
@@ -370,8 +365,8 @@ def _parse_size(s):
|
||||
|
||||
|
||||
def read_file_chunked_to_shm(path):
|
||||
from openpilot.common.file_chunker import read_file_chunked
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
from iqpilot.common.file_chunker import read_file_chunked
|
||||
from iqpilot.system.hardware.hw import Paths
|
||||
with tempfile.NamedTemporaryFile(prefix='compile_modeld_', dir=Paths.shm_path(), delete=False) as f:
|
||||
f.write(read_file_chunked(path))
|
||||
tmp_path = f.name
|
||||
@@ -381,8 +376,8 @@ def read_file_chunked_to_shm(path):
|
||||
|
||||
if __name__ == "__main__":
|
||||
from tinygrad.nn.onnx import OnnxRunner
|
||||
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.metadata import build_metadata_record
|
||||
from iqpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||
from iqpilot.selfdrive.iqmodeld.metadata import build_metadata_record
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument('--model-size', type=_parse_size, required=True, help='model input WxH')
|
||||
p.add_argument('--camera-resolutions', type=_parse_size, nargs='+', required=True,
|
||||
|
||||
119
iqpilot/selfdrive/iqmodeld/tools/compile_warp.py
Normal file
119
iqpilot/selfdrive/iqmodeld/tools/compile_warp.py
Normal file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
|
||||
Compile the backend-neutral warp-only artifact: NV12 camera frames + 3x3
|
||||
transforms -> (2, 6, model_h/2, model_w/2) uint8 warped tensor, on the device
|
||||
GPU (QCOM). maciqmodeld runs this locally
|
||||
and feed the output to their backend, so the big model's image pipeline is
|
||||
bit-identical to comma's fused pkl warp stage.
|
||||
|
||||
Run ON the device (needs the QCOM backend):
|
||||
cd /data/openpilot && DEV=QCOM WARP_DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 \
|
||||
python3 iqpilot/selfdrive/iqmodeld/tools/compile_warp.py \
|
||||
--camera-resolutions 1928x1208 --output /data/models/emac_warp.pkl
|
||||
The artifact is then split per-resolution into Paths.model_root().
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import os
|
||||
import pickle
|
||||
from functools import partial
|
||||
|
||||
import numpy as np
|
||||
|
||||
SELFTEST_SEED = 20260817
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.temporal_state import DEFAULT_FRAME_SKIP, MODEL_INPUT_SPEC
|
||||
from iqpilot.selfdrive.iqmodeld.tools.compile_supercombo import (
|
||||
NV12Frame, WARP_INPUTS, compile_jit, make_random_images, make_warp, make_warp_input_queues,
|
||||
)
|
||||
|
||||
MODEL_SIZE = (MODEL_INPUT_SPEC["img"][0][3] * 2, MODEL_INPUT_SPEC["img"][0][2] * 2) # (512, 256)
|
||||
|
||||
|
||||
def _parse_size(s: str) -> tuple[int, int]:
|
||||
w, h = s.lower().split("x")
|
||||
return int(w), int(h)
|
||||
|
||||
|
||||
def compile_warp(cam_w: int, cam_h: int, out_path: str | None = None,
|
||||
frame_skip: int = DEFAULT_FRAME_SKIP) -> str:
|
||||
"""Compile the warp-only QCOM JIT for one camera resolution and write the pkl.
|
||||
Returns the artifact path. Callable from the workers so a fresh device
|
||||
self-provisions the warp instead of erroring — needs the QCOM backend."""
|
||||
# the QCOM warp env must be set before tinygrad is imported here
|
||||
os.environ.setdefault("DEV", "QCOM")
|
||||
os.environ.setdefault("WARP_DEV", "QCOM")
|
||||
os.environ.setdefault("IMAGE", "1")
|
||||
os.environ.setdefault("FLOAT16", "1")
|
||||
os.environ.setdefault("NOLOCALS", "1")
|
||||
os.environ.setdefault("JIT_BATCH_SIZE", "0")
|
||||
from tinygrad.engine.jit import TinyJit
|
||||
from iqpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||
from iqpilot.system.hardware.hw import Paths
|
||||
|
||||
model_w, model_h = MODEL_SIZE
|
||||
input_shapes = {name: shape for name, (shape, _) in MODEL_INPUT_SPEC.items()}
|
||||
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
|
||||
make_random_warp_inputs = partial(make_random_images, keys=["frame", "big_frame"],
|
||||
shape=nv12.size, device=os.getenv("WARP_DEV"))
|
||||
warp_jit = TinyJit(make_warp(nv12, model_w, model_h, frame_skip), prune=True)
|
||||
make_warp_queues = partial(make_warp_input_queues, input_shapes, frame_skip)
|
||||
compiled = compile_jit(warp_jit, make_random_warp_inputs, WARP_INPUTS, make_warp_queues)
|
||||
|
||||
# historical artifact name: already-provisioned devices keep their warp
|
||||
out_path = out_path or os.path.join(Paths.model_root(), f"emac_warp_{cam_w}x{cam_h}_tinygrad.pkl")
|
||||
os.makedirs(os.path.dirname(out_path), exist_ok=True)
|
||||
tmp = out_path + ".part"
|
||||
bundle = {(cam_w, cam_h): compiled, "frame_skip": frame_skip, "model_size": MODEL_SIZE}
|
||||
bundle["selftest"] = selftest_digest(compiled, cam_w, cam_h, nv12.size)
|
||||
with open(tmp, "wb") as f:
|
||||
pickle.dump(bundle, f)
|
||||
os.replace(tmp, out_path) # atomic: a reader never sees a half-written pkl
|
||||
return out_path
|
||||
|
||||
|
||||
def main() -> None:
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--camera-resolutions", type=_parse_size, nargs="+", default=[(1928, 1208)])
|
||||
p.add_argument("--output", default=None)
|
||||
p.add_argument("--frame-skip", type=int, default=DEFAULT_FRAME_SKIP)
|
||||
args = p.parse_args()
|
||||
for cam_w, cam_h in args.camera_resolutions:
|
||||
out = compile_warp(cam_w, cam_h, args.output, frame_skip=args.frame_skip)
|
||||
print(f"saved warp JIT to {out} ({os.path.getsize(out) / 1e6:.2f} MB)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
def selftest_inputs(cam_w: int, cam_h: int, nv12_size: int):
|
||||
"""A fixed synthetic frame pair and pair of matrices. Deterministic so the
|
||||
digest is reproducible on the device that compiled the artifact."""
|
||||
rng = np.random.default_rng(SELFTEST_SEED)
|
||||
frame = rng.integers(0, 256, nv12_size, dtype=np.uint8)
|
||||
big_frame = rng.integers(0, 256, nv12_size, dtype=np.uint8)
|
||||
tfm = np.array([[0.7, 0.02, 300.0], [0.01, 0.7, 240.0], [0.0, 0.0, 1.0]], dtype=np.float32)
|
||||
big_tfm = np.array([[0.5, 0.01, 380.0], [0.02, 0.5, 300.0], [0.0, 0.0, 1.0]], dtype=np.float32)
|
||||
return frame, big_frame, tfm, big_tfm
|
||||
|
||||
|
||||
def selftest_digest(compiled, cam_w: int, cam_h: int, nv12_size: int) -> str:
|
||||
"""Hash the warp's output for a fixed input.
|
||||
|
||||
A warp artifact pinned to one tinygrad can still unpickle under another and
|
||||
then compute silently wrong, which reaches the model as a garbage image and
|
||||
looks like a bad model rather than a stale artifact. A version string cannot
|
||||
see that; running it can."""
|
||||
from tinygrad.tensor import Tensor
|
||||
frame, big_frame, tfm, big_tfm = selftest_inputs(cam_w, cam_h, nv12_size)
|
||||
dev = os.getenv("WARP_DEV") or "QCOM"
|
||||
out = compiled(tfm=Tensor(tfm, device="NPY").realize(),
|
||||
big_tfm=Tensor(big_tfm, device="NPY").realize(),
|
||||
frame=Tensor(frame, device=dev).realize(),
|
||||
big_frame=Tensor(big_frame, device=dev).realize())
|
||||
return hashlib.sha256(out.numpy().astype(np.uint8).tobytes()).hexdigest()
|
||||
44
iqpilot/selfdrive/iqmodeld/tools/convert_egpu_oob.py
Normal file
44
iqpilot/selfdrive/iqmodeld/tools/convert_egpu_oob.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import gc
|
||||
import os
|
||||
|
||||
os.environ.setdefault("DEV", "USB+AMD:LLVM")
|
||||
os.environ.setdefault("GMMU", "0")
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_helpers import patch_tinygrad_fetch_fw
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_policy import dump_oob, is_oob, load_bundle
|
||||
|
||||
|
||||
def convert(src: str, dst: str) -> str:
|
||||
patch_tinygrad_fetch_fw()
|
||||
if is_oob(src):
|
||||
if src != dst:
|
||||
os.replace(src, dst)
|
||||
return dst
|
||||
bundle = load_bundle(src)
|
||||
tmp = dst + ".part"
|
||||
with open(tmp, "wb") as f:
|
||||
dump_oob(bundle, f)
|
||||
del bundle
|
||||
gc.collect()
|
||||
load_bundle(tmp)
|
||||
os.replace(tmp, dst)
|
||||
return dst
|
||||
|
||||
|
||||
def main() -> None:
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("src")
|
||||
p.add_argument("--out", default=None)
|
||||
args = p.parse_args()
|
||||
out = convert(args.src, args.out or args.src)
|
||||
print(f"converted -> {out} ({os.path.getsize(out) / 1e6:.1f} MB)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -272,8 +272,8 @@ def _parse_size(text: str) -> tuple[int, int]:
|
||||
|
||||
|
||||
def _read_file_to_shared_memory(path: str) -> str:
|
||||
from openpilot.common.file_chunker import read_file_chunked
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
from iqpilot.common.file_chunker import read_file_chunked
|
||||
from iqpilot.system.hardware.hw import Paths
|
||||
|
||||
shm_path = os.path.join(Paths.shm_path(), os.path.basename(path))
|
||||
atexit.register(lambda: os.path.exists(shm_path) and os.remove(shm_path))
|
||||
@@ -295,8 +295,8 @@ def _arg_parser() -> argparse.ArgumentParser:
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
from openpilot.iqpilot.selfdrive.iqmodeld.metadata import build_metadata_record
|
||||
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||
from iqpilot.selfdrive.iqmodeld.metadata import build_metadata_record
|
||||
from iqpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||
|
||||
args = _arg_parser().parse_args(argv)
|
||||
model_w, model_h = args.model_size
|
||||
|
||||
64
iqpilot/selfdrive/iqmodeld/tools/egpu_host_mock.py
Normal file
64
iqpilot/selfdrive/iqmodeld/tools/egpu_host_mock.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
DEFAULT_ARCH = "gfx1200"
|
||||
MOCK_DEV = "MOCKUSB+AMD:LLVM"
|
||||
|
||||
|
||||
def tinygrad_tree() -> str:
|
||||
override = os.environ.get("IQ_TINYGRAD_TREE")
|
||||
if override:
|
||||
return override
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
root = os.path.abspath(os.path.join(here, "..", "..", "..", ".."))
|
||||
return os.path.join(root, "components", "tinygrad")
|
||||
|
||||
|
||||
def activate(arch: str = DEFAULT_ARCH, execute: bool = False) -> None:
|
||||
assert "tinygrad" not in sys.modules, "egpu_host_mock.activate must run before tinygrad is imported"
|
||||
os.environ["DEV"] = f"{MOCK_DEV}:{arch}"
|
||||
tree = tinygrad_tree()
|
||||
if tree not in sys.path:
|
||||
sys.path.insert(0, tree)
|
||||
from test.mockgpu.am import amgpu
|
||||
|
||||
# The mock dock models 512MB VRAM; big-model weights alone exceed that. Must be set before amdriver binds it.
|
||||
amgpu.VRAM_SIZE = int(os.environ.get("IQ_MOCK_VRAM_GB", "4")) << 30
|
||||
from tinygrad.runtime.autogen import libc
|
||||
if sys.platform == "darwin":
|
||||
# A Homebrew-LLVM gfx1200 kernel (no s_code_end padding) hung a real dock; ship only container-built artifacts.
|
||||
print("egpu_host_mock: native macOS LLVM output is for tests only; use scripts/iqpilot/host_egpu_compile_docker.sh for artifacts",
|
||||
file=sys.stderr)
|
||||
|
||||
def memfd_create(name, flags):
|
||||
fd, path = tempfile.mkstemp(prefix=b"iq_mock_" + bytes(name) + b"_")
|
||||
os.unlink(path)
|
||||
return fd
|
||||
libc.memfd_create = memfd_create
|
||||
if not hasattr(libc, "MFD_CLOEXEC"):
|
||||
libc.MFD_CLOEXEC = 1
|
||||
if not execute:
|
||||
import ctypes
|
||||
from test.mockgpu.amd import amdgpu
|
||||
amdgpu.remu.run_asm = lambda *args, **kwargs: 0
|
||||
pm4_wait = amdgpu.PM4Executor._exec_wait_reg_mem
|
||||
sdma_poll = amdgpu.SDMAExecutor._execute_poll_regmem
|
||||
|
||||
# Without kernel execution no memory wait carries information; a blocked wait would need a host write to re-poll it.
|
||||
def pm4_wait_passthrough(self, n):
|
||||
if not pm4_wait(self, n):
|
||||
self.rptr[0] += 7
|
||||
return True
|
||||
|
||||
def sdma_poll_passthrough(self):
|
||||
if not sdma_poll(self):
|
||||
self.rptr[0] += ctypes.sizeof(amdgpu.sdma_pkts.poll_regmem)
|
||||
return True
|
||||
amdgpu.PM4Executor._exec_wait_reg_mem = pm4_wait_passthrough
|
||||
amdgpu.SDMAExecutor._execute_poll_regmem = sdma_poll_passthrough
|
||||
@@ -13,7 +13,7 @@ from pathlib import Path
|
||||
|
||||
import onnx
|
||||
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
from iqpilot.system.hardware.hw import Paths
|
||||
|
||||
_MODEL_STEMS = ("driving_off_policy", "driving_on_policy", "driving_policy", "driving_vision")
|
||||
|
||||
|
||||
64
iqpilot/selfdrive/iqmodeld/tools/oob_rewrite.py
Normal file
64
iqpilot/selfdrive/iqmodeld/tools/oob_rewrite.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import pickletools
|
||||
import struct
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_policy import OOB_MAGIC
|
||||
|
||||
MIN_OOB_BYTES = 1 << 16
|
||||
NEXT_BUFFER = b"\x97"
|
||||
READONLY_BUFFER = b"\x98"
|
||||
|
||||
|
||||
def rewrite_oob(src: str, dst: str, min_bytes: int = MIN_OOB_BYTES) -> tuple[int, int]:
|
||||
# tinygrad pickles device buffers as PickleBuffers, which land in-band as BYTEARRAY8/BINBYTES8
|
||||
# without a buffer_callback; moving those opcodes out-of-band is byte-for-byte what a protocol-5
|
||||
# dump with a buffer_callback produces, so nothing has to be unpickled (no dock needed).
|
||||
with open(src, "rb") as f:
|
||||
data = f.read()
|
||||
ops = list(pickletools.genops(data))
|
||||
proto = next((arg for op, arg, _ in ops if op.name == "PROTO"), 0)
|
||||
if proto < 5:
|
||||
raise ValueError(f"{src} is pickle protocol {proto}; out-of-band buffers need protocol 5")
|
||||
moved = 0
|
||||
tmp = dst + ".part"
|
||||
with open(tmp, "wb") as out, open(tmp + ".buf", "wb") as bufs:
|
||||
ops_stream = bytearray()
|
||||
for i, (op, arg, pos) in enumerate(ops):
|
||||
end = ops[i + 1][2] if i + 1 < len(ops) else len(data)
|
||||
if op.name in ("BYTEARRAY8", "BINBYTES8", "BINBYTES") and len(arg) >= min_bytes:
|
||||
ops_stream += NEXT_BUFFER
|
||||
if op.name != "BYTEARRAY8":
|
||||
ops_stream += READONLY_BUFFER
|
||||
bufs.write(struct.pack("<q", len(arg)))
|
||||
bufs.write(arg)
|
||||
moved += 1
|
||||
else:
|
||||
ops_stream += data[pos:end]
|
||||
out.write(OOB_MAGIC)
|
||||
out.write(struct.pack("<q", len(ops_stream)))
|
||||
out.write(ops_stream)
|
||||
with open(tmp, "ab") as out, open(tmp + ".buf", "rb") as bufs:
|
||||
while chunk := bufs.read(1 << 24):
|
||||
out.write(chunk)
|
||||
os.remove(tmp + ".buf")
|
||||
os.replace(tmp, dst)
|
||||
return moved, len(ops)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("src")
|
||||
p.add_argument("dst")
|
||||
args = p.parse_args()
|
||||
moved, total = rewrite_oob(args.src, args.dst)
|
||||
print(f"{args.dst}: moved {moved} buffers out-of-band ({total} opcodes, {os.path.getsize(args.dst) / 1e6:.1f} MB)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user