IQ.Pilot Release Commit @ b6534c0
This commit is contained in:
1
iqpilot/selfdrive/iqmodeld/.gitignore
vendored
Normal file
1
iqpilot/selfdrive/iqmodeld/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*_pyx.cpp
|
||||
127
iqpilot/selfdrive/iqmodeld/SConscript
Normal file
127
iqpilot/selfdrive/iqmodeld/SConscript
Normal file
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
import glob
|
||||
import os
|
||||
|
||||
Import("env", "envCython", "arch", "cereal", "messaging", "common", "visionipc", "tinygrad_dir")
|
||||
lenv = env.Clone()
|
||||
lenvCython = envCython.Clone()
|
||||
|
||||
libs = [cereal, messaging, visionipc, common, "capnp", "kj", "pthread"]
|
||||
frameworks = []
|
||||
core_sources = ["native/iqmodel.cc", "transforms/yuv.cc", "transforms/warp_geometry.cc"]
|
||||
SMALL_MODEL_NAMES = ["supercombo", "driving_vision", "driving_off_policy", "driving_on_policy", "driving_policy"]
|
||||
FUSED_TRIPLET = ["driving_vision", "driving_off_policy", "driving_on_policy"]
|
||||
PC = not os.path.isfile("/TICI")
|
||||
|
||||
|
||||
def _inject_path_define(symbol, filename):
|
||||
quoted = f'-D{symbol}_PATH=\\"{File(filename).abspath}\\"'
|
||||
for active_env in (lenv, lenvCython):
|
||||
active_env["CXXFLAGS"].append(quoted)
|
||||
|
||||
|
||||
def _tinygrad_sources():
|
||||
return [path for path in glob.glob(tinygrad_dir + "/**", recursive=True) if "pycache" not in path]
|
||||
|
||||
|
||||
def _present_models():
|
||||
return [name for name in SMALL_MODEL_NAMES if File(f"models/{name}.onnx").exists()]
|
||||
|
||||
|
||||
def _tinygrad_flags():
|
||||
if arch == "larch64":
|
||||
return "DEV=QCOM IMAGE=2 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
|
||||
if arch == "Darwin":
|
||||
return f'DEV=CPU HOME={os.path.expanduser("~")} IMAGE=0'
|
||||
if arch == "x86_64":
|
||||
return "DEV=CPU:LLVM IMAGE=0"
|
||||
return "DEV=CPU:LLVM IMAGE=0"
|
||||
|
||||
|
||||
def _fused_flags():
|
||||
if arch == "larch64":
|
||||
return "DEV=QCOM IMAGE=2 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
|
||||
if arch == "Darwin":
|
||||
return f'DEV=CPU HOME={os.path.expanduser("~")} IMAGE=0 FLOAT16=1'
|
||||
if arch == "x86_64":
|
||||
return "DEV=CPU:LLVM IMAGE=0 FLOAT16=1"
|
||||
return "DEV=CPU:LLVM IMAGE=0 FLOAT16=1"
|
||||
|
||||
|
||||
def _queue_metadata_generation(model_names, tinygrad_files):
|
||||
if not PC:
|
||||
return
|
||||
inputs = tinygrad_files + [File(Dir("#iqpilot/selfdrive/iqmodeld/tools").File("install_models_pc.py").abspath)]
|
||||
outputs = []
|
||||
for model_name in model_names:
|
||||
inputs.extend([File(f"models/{model_name}.onnx"), File(f"models/{model_name}_tinygrad.pkl")])
|
||||
outputs.append(File(f"models/{model_name}_metadata.pkl"))
|
||||
if outputs:
|
||||
tool_dir = Dir("#iqpilot/selfdrive/iqmodeld/tools").abspath
|
||||
model_dir = Dir("models").abspath
|
||||
lenv.Command(outputs, inputs,
|
||||
lenv.PrettyAction(f"${{PYWARN}} python3 {tool_dir}/install_models_pc.py {model_dir}", 'META'))
|
||||
|
||||
|
||||
if arch == "Darwin":
|
||||
frameworks += ["OpenCL"]
|
||||
else:
|
||||
libs += ["OpenCL"]
|
||||
|
||||
for symbol, filename in {"TRANSFORM": "transforms/warp_geometry.cl", "LOADYUV": "transforms/yuv.cl"}.items():
|
||||
_inject_path_define(symbol, filename)
|
||||
|
||||
cython_libs = envCython["LIBS"] + libs
|
||||
iqmodel_lib = lenv.Library("iqmodel", core_sources)
|
||||
lenvCython.Program("native/iqmodel_pyx.so", "native/iqmodel_pyx.pyx", LIBS=[iqmodel_lib, *cython_libs], FRAMEWORKS=frameworks)
|
||||
tinygrad_files = _tinygrad_sources()
|
||||
present_models = _present_models()
|
||||
_queue_metadata_generation(present_models, tinygrad_files)
|
||||
|
||||
|
||||
def tg_compile(flags, model_name):
|
||||
fn = File(f"models/{model_name}").abspath
|
||||
return lenv.Command(
|
||||
fn + "_tinygrad.pkl",
|
||||
[fn + ".onnx"] + tinygrad_files,
|
||||
lenv.PrettyAction(
|
||||
f'${{PYWARN}} {flags} python3 {Dir("#iqpilot/selfdrive/iqmodeld/tools").abspath}/compile_model.py {fn}.onnx {fn}_tinygrad.pkl',
|
||||
'MODEL', logfile='${TARGET}.log')
|
||||
)
|
||||
|
||||
|
||||
for model_name in present_models:
|
||||
tg_compile(_tinygrad_flags(), model_name)
|
||||
|
||||
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):
|
||||
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}} {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 '
|
||||
f'--on-policy-onnx {model_dir}/{file_prefix}driving_on_policy.onnx '
|
||||
f'--output {out_pkl} --frame-skip {FUSED_FRAME_SKIP}'
|
||||
)
|
||||
return lenv.Command(out_pkl, onnx_deps + tinygrad_files,
|
||||
lenv.PrettyAction(cmd, 'MODEL', logfile='${TARGET}.log'))
|
||||
|
||||
|
||||
if all(File(f"models/{model_name}.onnx").exists() for model_name in FUSED_TRIPLET):
|
||||
tg_compile_fused("", _fused_flags())
|
||||
|
||||
if all(File(f"models/big_{model_name}.onnx").exists() for model_name in FUSED_TRIPLET):
|
||||
tg_compile_fused("big_", "DEV=USB+AMD:LLVM WARP_DEV=QCOM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0")
|
||||
23
iqpilot/selfdrive/iqmodeld/__init__.py
Normal file
23
iqpilot/selfdrive/iqmodeld/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _models_dir() -> Path:
|
||||
return Path(__file__).resolve().parent / "models"
|
||||
|
||||
|
||||
def _artifact_path(stem: str, suffix: str) -> Path:
|
||||
return _models_dir() / f"{stem}{suffix}"
|
||||
|
||||
|
||||
MODEL_ASSETS = {
|
||||
"onnx": _artifact_path("supercombo", ".onnx"),
|
||||
"tinygrad": _artifact_path("supercombo", "_tinygrad.pkl"),
|
||||
"metadata": _artifact_path("supercombo", "_metadata.pkl"),
|
||||
}
|
||||
|
||||
MODEL_PATH = MODEL_ASSETS["onnx"]
|
||||
MODEL_PKL_PATH = MODEL_ASSETS["tinygrad"]
|
||||
METADATA_PATH = MODEL_ASSETS["metadata"]
|
||||
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 *
|
||||
70
iqpilot/selfdrive/iqmodeld/camera.py
Normal file
70
iqpilot/selfdrive/iqmodeld/camera.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
from iqpilot.common.transformations.camera import DEVICE_CAMERAS
|
||||
|
||||
MAX_CAMERA_OFFSET_METERS = 0.35
|
||||
|
||||
|
||||
class _OffsetSmoother:
|
||||
def __init__(self, blend: float = 0.1):
|
||||
self._blend = blend
|
||||
self._value = 0.0
|
||||
|
||||
def step(self, target: float) -> float:
|
||||
self._value = ((1.0 - self._blend) * self._value) + (self._blend * float(target))
|
||||
return self._value
|
||||
|
||||
|
||||
def _clamped_offset(raw_offset) -> float:
|
||||
try:
|
||||
parsed = float(raw_offset)
|
||||
except (TypeError, ValueError):
|
||||
parsed = 0.0
|
||||
return float(np.clip(parsed, -MAX_CAMERA_OFFSET_METERS, MAX_CAMERA_OFFSET_METERS))
|
||||
|
||||
|
||||
def _camera_profile(sm):
|
||||
return DEVICE_CAMERAS[(str(sm["deviceState"].deviceType), str(sm["roadCameraState"].sensor))]
|
||||
|
||||
|
||||
def _calibration_height(sm) -> float:
|
||||
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):
|
||||
optical_center_y = intrinsics[1, 2]
|
||||
projection_bias = np.eye(3, dtype=np.float32)
|
||||
projection_bias[0, 1] = lateral_offset / height
|
||||
projection_bias[0, 2] = -(lateral_offset / height) * optical_center_y
|
||||
return (projection_bias @ model_transform).astype(np.float32)
|
||||
|
||||
|
||||
class CameraOffsetHelper:
|
||||
def __init__(self):
|
||||
self.camera_offset = 0.0
|
||||
self.actual_camera_offset = 0.0
|
||||
self._smoother = _OffsetSmoother()
|
||||
|
||||
@staticmethod
|
||||
def apply_camera_offset(model_transform, intrinsics, height, offset_param):
|
||||
return _sheared_transform(model_transform, intrinsics, height, offset_param)
|
||||
|
||||
def set_offset(self, offset):
|
||||
self.camera_offset = _clamped_offset(offset)
|
||||
|
||||
def update(self, model_transform_main, model_transform_extra, sm, main_wide_camera, extra_uses_wide_camera=True):
|
||||
self.actual_camera_offset = self._smoother.step(self.camera_offset)
|
||||
camera_bundle = _camera_profile(sm)
|
||||
camera_height = _calibration_height(sm)
|
||||
main_intrinsics = camera_bundle.ecam.intrinsics if main_wide_camera else camera_bundle.fcam.intrinsics
|
||||
extra_intrinsics = camera_bundle.ecam.intrinsics if extra_uses_wide_camera else camera_bundle.fcam.intrinsics
|
||||
|
||||
return (
|
||||
self.apply_camera_offset(model_transform_main, main_intrinsics, camera_height, self.actual_camera_offset),
|
||||
self.apply_camera_offset(model_transform_extra, extra_intrinsics, camera_height, self.actual_camera_offset),
|
||||
)
|
||||
133
iqpilot/selfdrive/iqmodeld/config.py
Normal file
133
iqpilot/selfdrive/iqmodeld/config.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
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]:
|
||||
return [index_function(index, max_val=limit, max_idx=steps - 1) for index in range(steps)]
|
||||
|
||||
|
||||
def _probability_window(*values: float) -> np.ndarray:
|
||||
return np.asarray(values, dtype=np.float32)
|
||||
|
||||
|
||||
def _field_group(start: int, stop: int, stride: int) -> slice:
|
||||
return slice(start, stop, stride)
|
||||
|
||||
|
||||
_IDX_COUNT = 33
|
||||
_T_AXIS = _quadratic_series(10.0, _IDX_COUNT)
|
||||
_X_AXIS = _quadratic_series(192.0, _IDX_COUNT)
|
||||
|
||||
|
||||
class ModelConstants:
|
||||
IDX_N = _IDX_COUNT
|
||||
T_IDXS = _T_AXIS
|
||||
X_IDXS = _X_AXIS
|
||||
LEAD_T_IDXS = [0.0, 2.0, 4.0, 6.0, 8.0, 10.0]
|
||||
LEAD_T_OFFSETS = [0.0, 2.0, 4.0]
|
||||
META_T_IDXS = [2.0, 4.0, 6.0, 8.0, 10.0]
|
||||
|
||||
MODEL_FREQ = 20
|
||||
FEATURE_LEN = 512
|
||||
FULL_HISTORY_BUFFER_LEN = 99
|
||||
HISTORY_BUFFER_LEN = FULL_HISTORY_BUFFER_LEN
|
||||
DESIRE_LEN = 8
|
||||
TRAFFIC_CONVENTION_LEN = 2
|
||||
NAV_FEATURE_LEN = 256
|
||||
NAV_INSTRUCTION_LEN = 150
|
||||
LAT_PLANNER_STATE_LEN = 4
|
||||
LATERAL_CONTROL_PARAMS_LEN = 2
|
||||
PREV_DESIRED_CURV_LEN = 1
|
||||
|
||||
FCW_THRESHOLDS_5MS2 = _probability_window(0.05, 0.05, 0.15, 0.15, 0.15)
|
||||
FCW_THRESHOLDS_3MS2 = _probability_window(0.7, 0.7)
|
||||
FCW_5MS2_PROBS_WIDTH = 5
|
||||
FCW_3MS2_PROBS_WIDTH = 2
|
||||
|
||||
DISENGAGE_WIDTH = 5
|
||||
POSE_WIDTH = 6
|
||||
WIDE_FROM_DEVICE_WIDTH = 3
|
||||
SIM_POSE_WIDTH = 6
|
||||
LEAD_WIDTH = 4
|
||||
LANE_LINES_WIDTH = 2
|
||||
ROAD_EDGES_WIDTH = 2
|
||||
PLAN_WIDTH = 15
|
||||
DESIRE_PRED_WIDTH = 8
|
||||
LAT_PLANNER_SOLUTION_WIDTH = 4
|
||||
DESIRED_CURV_WIDTH = 1
|
||||
|
||||
NUM_LANE_LINES = 4
|
||||
NUM_ROAD_EDGES = 2
|
||||
LEAD_TRAJ_LEN = 6
|
||||
DESIRE_PRED_LEN = 4
|
||||
|
||||
PLAN_MHP_N = 5
|
||||
LEAD_MHP_N = 2
|
||||
PLAN_MHP_SELECTION = 1
|
||||
LEAD_MHP_SELECTION = 3
|
||||
|
||||
FCW_THRESHOLD_5MS2_HIGH = 0.15
|
||||
FCW_THRESHOLD_5MS2_LOW = 0.05
|
||||
FCW_THRESHOLD_3MS2 = 0.7
|
||||
|
||||
CONFIDENCE_BUFFER_LEN = 5
|
||||
RYG_GREEN = 0.01165
|
||||
RYG_YELLOW = 0.06157
|
||||
POLY_PATH_DEGREE = 4
|
||||
|
||||
|
||||
class Plan:
|
||||
POSITION = slice(0, 3)
|
||||
VELOCITY = slice(3, 6)
|
||||
ACCELERATION = slice(6, 9)
|
||||
T_FROM_CURRENT_EULER = slice(9, 12)
|
||||
ORIENTATION_RATE = slice(12, 15)
|
||||
|
||||
|
||||
class Meta:
|
||||
ENGAGED = _field_group(0, 1, 1)
|
||||
GAS_DISENGAGE = _field_group(1, 31, 6)
|
||||
BRAKE_DISENGAGE = _field_group(2, 31, 6)
|
||||
STEER_OVERRIDE = _field_group(3, 31, 6)
|
||||
HARD_BRAKE_3 = _field_group(4, 31, 6)
|
||||
HARD_BRAKE_4 = _field_group(5, 31, 6)
|
||||
HARD_BRAKE_5 = _field_group(6, 31, 6)
|
||||
GAS_PRESS = _field_group(31, 55, 4)
|
||||
BRAKE_PRESS = _field_group(32, 55, 4)
|
||||
LEFT_BLINKER = _field_group(33, 55, 4)
|
||||
RIGHT_BLINKER = _field_group(34, 55, 4)
|
||||
|
||||
|
||||
class MetaTombRaider:
|
||||
ENGAGED = _field_group(0, 1, 1)
|
||||
GAS_DISENGAGE = _field_group(1, 41, 8)
|
||||
BRAKE_DISENGAGE = _field_group(2, 41, 8)
|
||||
STEER_OVERRIDE = _field_group(3, 41, 8)
|
||||
HARD_BRAKE_3 = _field_group(4, 41, 8)
|
||||
HARD_BRAKE_4 = _field_group(5, 41, 8)
|
||||
HARD_BRAKE_5 = _field_group(6, 41, 8)
|
||||
GAS_PRESS = _field_group(7, 41, 8)
|
||||
BRAKE_PRESS = _field_group(8, 41, 8)
|
||||
LEFT_BLINKER = _field_group(41, 53, 2)
|
||||
RIGHT_BLINKER = _field_group(42, 53, 2)
|
||||
|
||||
|
||||
class MetaSimPose:
|
||||
ENGAGED = _field_group(0, 1, 1)
|
||||
GAS_DISENGAGE = _field_group(1, 36, 7)
|
||||
BRAKE_DISENGAGE = _field_group(2, 36, 7)
|
||||
STEER_OVERRIDE = _field_group(3, 36, 7)
|
||||
HARD_BRAKE_3 = _field_group(4, 36, 7)
|
||||
HARD_BRAKE_4 = _field_group(5, 36, 7)
|
||||
HARD_BRAKE_5 = _field_group(6, 36, 7)
|
||||
GAS_PRESS = _field_group(7, 36, 7)
|
||||
LEFT_BLINKER = _field_group(36, 48, 2)
|
||||
RIGHT_BLINKER = _field_group(37, 48, 2)
|
||||
766
iqpilot/selfdrive/iqmodeld/daemon.py
Executable file
766
iqpilot/selfdrive/iqmodeld/daemon.py
Executable file
@@ -0,0 +1,766 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import iqpilot.cereal.messaging as messaging
|
||||
import numpy as np
|
||||
|
||||
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 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 iqpilot.selfdrive.locationd.calibration_helpers import get_calibrated_rpy
|
||||
from iqpilot.system import sentry
|
||||
|
||||
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 iqpilot.selfdrive.iqmodeld.metadata import select_meta_layout
|
||||
|
||||
try:
|
||||
from iqpilot.selfdrive.iqmodeld.native.iqmodel_pyx import RoadProjector, WarpContext
|
||||
except ModuleNotFoundError:
|
||||
class WarpContext:
|
||||
def __init__(self, *args, **kwargs):
|
||||
raise ModuleNotFoundError("iqpilot.selfdrive.iqmodeld.native.iqmodel_pyx is not built")
|
||||
|
||||
class RoadProjector:
|
||||
def __init__(self, *args, **kwargs):
|
||||
raise ModuleNotFoundError("iqpilot.selfdrive.iqmodeld.native.iqmodel_pyx is not built")
|
||||
|
||||
|
||||
PROCESS_NAME = "iqpilot.selfdrive.iqmodeld.daemon"
|
||||
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:
|
||||
# plan_stds is (batch, IDX_N, PLAN_WIDTH); index 10 ~= 1s ahead (see ModelConstants.T_IDXS),
|
||||
# POSITION is an (x, y, z) slice within PLAN_WIDTH so [1] picks the lateral (y) std.
|
||||
try:
|
||||
return float(outputs["plan_stds"][0, 10, Plan.POSITION][1])
|
||||
except (KeyError, IndexError):
|
||||
return 0.0
|
||||
|
||||
|
||||
def _model_lat_smooth_max_sec(params: Params) -> float:
|
||||
if not params.get_bool("ModelSmoothingEnabled"):
|
||||
return 0.0
|
||||
try:
|
||||
raw = params.get("ModelLatSmoothSec", return_default=True)
|
||||
raw = 0 if raw is None else int(raw)
|
||||
except (ValueError, TypeError):
|
||||
raw = 0
|
||||
return min(max(raw, 0), 30) * 0.01
|
||||
|
||||
|
||||
@dataclass
|
||||
class CaptureStamp:
|
||||
frame_id: int = 0
|
||||
timestamp_sof: int = 0
|
||||
timestamp_eof: int = 0
|
||||
|
||||
@classmethod
|
||||
def from_vipc(cls, client: VisionIpcClient) -> "CaptureStamp":
|
||||
return cls(client.frame_id, client.timestamp_sof, client.timestamp_eof)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StreamLayout:
|
||||
dual_camera: bool
|
||||
main_is_wide: bool
|
||||
primary_stream: VisionStreamType
|
||||
|
||||
|
||||
class ReplayLedger:
|
||||
def __init__(self, tensor_shapes: dict[str, tuple[int, ...]], frame_inputs: list[str]):
|
||||
self.inputs: dict[str, np.ndarray] = {}
|
||||
self.archive: dict[str, np.ndarray] = {}
|
||||
self.selectors: dict[str, np.ndarray] = {}
|
||||
self._frame_inputs = set(frame_inputs)
|
||||
self._pulse_name: str | None = None
|
||||
self._pulse_memory: np.ndarray | None = None
|
||||
|
||||
feature_shape = tensor_shapes.get("features_buffer")
|
||||
for tensor_name, tensor_shape in tensor_shapes.items():
|
||||
if tensor_name in self._frame_inputs:
|
||||
continue
|
||||
|
||||
self.inputs[tensor_name] = np.zeros(tensor_shape, dtype=np.float32)
|
||||
if len(tensor_shape) != 3 or tensor_shape[1] <= 1:
|
||||
continue
|
||||
|
||||
history_len = self._history_length(tensor_shape, feature_shape)
|
||||
self.archive[tensor_name] = np.zeros((1, history_len, tensor_shape[2]), dtype=np.float32)
|
||||
export_index = self._export_index(tensor_shape, history_len, feature_shape)
|
||||
if export_index is not None:
|
||||
self.selectors[tensor_name] = export_index
|
||||
|
||||
if tensor_name.startswith("desire"):
|
||||
self._pulse_name = tensor_name
|
||||
self._pulse_memory = np.zeros(tensor_shape[2], dtype=np.float32)
|
||||
|
||||
@staticmethod
|
||||
def _history_length(tensor_shape: tuple[int, ...], feature_shape: tuple[int, ...] | None) -> int:
|
||||
if tensor_shape[1] >= 99:
|
||||
return tensor_shape[1]
|
||||
if tensor_shape[1] in (24, 25) and feature_shape is not None and feature_shape[1] == 24:
|
||||
return (feature_shape[1] + 1) * 4
|
||||
return tensor_shape[1] * 4
|
||||
|
||||
@staticmethod
|
||||
def _export_index(tensor_shape: tuple[int, ...], history_len: int,
|
||||
feature_shape: tuple[int, ...] | None) -> np.ndarray | None:
|
||||
if tensor_shape[1] in (24, 25) and feature_shape is not None and feature_shape[1] == 24:
|
||||
stride = int(-history_len / tensor_shape[1])
|
||||
return np.arange(stride, stride * (tensor_shape[1] + 1), stride)[::-1]
|
||||
if tensor_shape[1] == 25:
|
||||
skip = history_len // tensor_shape[1]
|
||||
return np.arange(history_len)[-1 - (skip * (tensor_shape[1] - 1))::skip]
|
||||
if tensor_shape[1] >= 99:
|
||||
return np.arange(tensor_shape[1])
|
||||
return None
|
||||
|
||||
@property
|
||||
def pulse_name(self) -> str:
|
||||
if self._pulse_name is None:
|
||||
raise KeyError("No desire-like pulse input present in model inputs")
|
||||
return self._pulse_name
|
||||
|
||||
def _shift_archive(self, tensor_name: str) -> np.ndarray:
|
||||
history = self.archive[tensor_name]
|
||||
history[0, :-1] = history[0, 1:]
|
||||
return history
|
||||
|
||||
def inject_pulse(self, pulse_values: np.ndarray) -> None:
|
||||
pulse = pulse_values.copy()
|
||||
pulse[0] = 0
|
||||
assert self._pulse_memory is not None
|
||||
rising = np.where(pulse - self._pulse_memory > 0.99, pulse, 0)
|
||||
self._pulse_memory[:] = pulse
|
||||
|
||||
history = self._shift_archive(self.pulse_name)
|
||||
history[0, -1] = rising
|
||||
exported_shape = self.inputs[self.pulse_name].shape
|
||||
if history.shape[1] > exported_shape[1]:
|
||||
stride = history.shape[1] // exported_shape[1]
|
||||
self.inputs[self.pulse_name][:] = history[0].reshape(
|
||||
exported_shape[0], exported_shape[1], stride, -1
|
||||
).max(axis=2)
|
||||
return
|
||||
self.inputs[self.pulse_name][:] = history[0, self.selectors[self.pulse_name]]
|
||||
|
||||
def merge_inputs(self, fresh_inputs: dict[str, np.ndarray]) -> None:
|
||||
pulse_name = self.pulse_name
|
||||
for tensor_name, tensor_value in fresh_inputs.items():
|
||||
if tensor_name in self.inputs and tensor_name != pulse_name:
|
||||
self.inputs[tensor_name][:] = tensor_value
|
||||
|
||||
def note_hidden_state(self, hidden_state: np.ndarray) -> None:
|
||||
if "features_buffer" not in self.archive:
|
||||
return
|
||||
history = self._shift_archive("features_buffer")
|
||||
history[0, -1] = hidden_state[0]
|
||||
self.inputs["features_buffer"][:] = history[0, self.selectors["features_buffer"]]
|
||||
|
||||
def note_feedback(self, tensor_name: str, values: np.ndarray, zero_export: bool = False) -> None:
|
||||
if tensor_name not in self.archive:
|
||||
return
|
||||
history = self._shift_archive(tensor_name)
|
||||
history[0, -1, :] = values[0]
|
||||
exported = history[0, self.selectors[tensor_name]]
|
||||
self.inputs[tensor_name][:] = 0 * exported if zero_export else exported
|
||||
|
||||
|
||||
def _planplus_gain(vehicle_speed: float) -> float:
|
||||
return 0.75 if vehicle_speed >= 25.0 else 1.0
|
||||
|
||||
|
||||
def _merged_plan(runtime_state: "NeuralEngineState", outputs: dict[str, np.ndarray], vehicle_speed: float) -> np.ndarray:
|
||||
base_plan = outputs["plan"][0]
|
||||
if "planplus" not in outputs:
|
||||
return base_plan
|
||||
return base_plan + (runtime_state.PLANPLUS_CONTROL * _planplus_gain(vehicle_speed)) * outputs["planplus"][0]
|
||||
|
||||
|
||||
class NeuralEngineState(InferenceStateBase):
|
||||
frames: dict[str, RoadProjector]
|
||||
|
||||
def __init__(self, gpu_context: WarpContext):
|
||||
super().__init__()
|
||||
runner = get_model_runner()
|
||||
bundle = get_active_bundle()
|
||||
|
||||
self.model_runner = runner
|
||||
self.constants = runner.constants
|
||||
self.generation = bundle.generation if bundle is not None else None
|
||||
|
||||
knob_values = {entry.key: entry.value for entry in bundle.overrides} if bundle is not None else {}
|
||||
self.LAT_SMOOTH_SECONDS = float(knob_values.get("lat", ".0"))
|
||||
self.LONG_SMOOTH_SECONDS = float(knob_values.get("long", ".0"))
|
||||
self.MIN_LAT_CONTROL_SPEED = 0.3
|
||||
self.PLANPLUS_CONTROL = 1.0
|
||||
self.model_smoothing_max_extra_sec = 0.0
|
||||
|
||||
context_depth = 5 if runner.is_20hz else 2
|
||||
self.frames = {
|
||||
stream_name: RoadProjector(gpu_context, context_depth)
|
||||
for stream_name in runner.vision_input_names
|
||||
}
|
||||
|
||||
self._ledger = ReplayLedger(runner.input_shapes, runner.vision_input_names)
|
||||
self.numpy_inputs = self._ledger.inputs
|
||||
self.temporal_buffers = self._ledger.archive
|
||||
self.temporal_idxs_map = self._ledger.selectors
|
||||
|
||||
@property
|
||||
def mlsim(self) -> bool:
|
||||
return bool(self.generation is not None and self.generation >= 11)
|
||||
|
||||
@property
|
||||
def desire_key(self) -> str:
|
||||
return self._ledger.pulse_name
|
||||
|
||||
def _warp_frames(self, vision_bufs: dict[str, VisionBuf],
|
||||
transform_map: dict[str, np.ndarray]) -> dict[str, Any]:
|
||||
return {
|
||||
stream_name: self.frames[stream_name].stage(vision_bufs[stream_name], transform_map[stream_name].flatten())
|
||||
for stream_name in self.model_runner.vision_input_names
|
||||
}
|
||||
|
||||
def _run_split_model(self) -> dict[str, np.ndarray]:
|
||||
if hasattr(self.model_runner, "run_vision"):
|
||||
vision_packet = self.model_runner.run_vision()
|
||||
self._ledger.note_hidden_state(vision_packet["hidden_state"])
|
||||
self.model_runner.refresh_policy_features(self.numpy_inputs["features_buffer"])
|
||||
return {**vision_packet, **self.model_runner.run_policy()}
|
||||
|
||||
result = self.model_runner.run_model()
|
||||
if "hidden_state" in result:
|
||||
self._ledger.note_hidden_state(result["hidden_state"])
|
||||
return result
|
||||
|
||||
def _write_curvature_memory(self, outputs: dict[str, np.ndarray]) -> None:
|
||||
if "desired_curvature" not in outputs:
|
||||
return
|
||||
|
||||
feedback_slot = None
|
||||
if "prev_desired_curvs" in self.numpy_inputs:
|
||||
feedback_slot = "prev_desired_curvs"
|
||||
elif "prev_desired_curv" in self.numpy_inputs:
|
||||
feedback_slot = "prev_desired_curv"
|
||||
|
||||
if feedback_slot is not None:
|
||||
self._ledger.note_feedback(feedback_slot, outputs["desired_curvature"], zero_export=self.mlsim)
|
||||
|
||||
def run(self, vision_bufs: dict[str, VisionBuf], transform_map: dict[str, np.ndarray],
|
||||
fresh_inputs: dict[str, np.ndarray]) -> dict[str, np.ndarray] | None:
|
||||
if not getattr(self.model_runner, "uses_opencl_warp", True):
|
||||
return self.model_runner.run_fused(vision_bufs, transform_map, fresh_inputs)
|
||||
|
||||
self._ledger.inject_pulse(fresh_inputs[self.desire_key])
|
||||
self._ledger.merge_inputs(fresh_inputs)
|
||||
warped_frames = self._warp_frames(vision_bufs, transform_map)
|
||||
self.model_runner.prepare_inputs(warped_frames, self.numpy_inputs, self.frames)
|
||||
|
||||
outputs = self._run_split_model()
|
||||
self._write_curvature_memory(outputs)
|
||||
return outputs
|
||||
|
||||
def get_action_from_model(self, outputs: dict[str, np.ndarray], previous_action: log.ModelDataV2.Action,
|
||||
lat_action_t: float, long_action_t: float, vehicle_speed: float,
|
||||
lat_smooth_seconds: float | None = None) -> log.ModelDataV2.Action:
|
||||
if lat_smooth_seconds is None:
|
||||
lat_smooth_seconds = self.LAT_SMOOTH_SECONDS
|
||||
|
||||
if "action" in outputs:
|
||||
curvature_cmd = outputs["action"][0, 0] / (max(1.0, vehicle_speed)) ** 2
|
||||
accel_cmd = outputs["action"][0, 1]
|
||||
should_stop = bool(vehicle_speed < 0.3 and accel_cmd < 0.1)
|
||||
|
||||
accel_cmd = smooth_value(accel_cmd, previous_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS)
|
||||
if vehicle_speed > self.MIN_LAT_CONTROL_SPEED:
|
||||
curvature_cmd = smooth_value(curvature_cmd, previous_action.desiredCurvature, lat_smooth_seconds)
|
||||
else:
|
||||
curvature_cmd = previous_action.desiredCurvature
|
||||
|
||||
return log.ModelDataV2.Action(
|
||||
desiredCurvature=float(curvature_cmd),
|
||||
desiredAcceleration=float(accel_cmd),
|
||||
shouldStop=should_stop,
|
||||
)
|
||||
|
||||
plan_rows = _merged_plan(self, outputs, vehicle_speed)
|
||||
accel_cmd, should_stop = get_accel_from_plan(
|
||||
plan_rows[:, Plan.VELOCITY][:, 0],
|
||||
plan_rows[:, Plan.ACCELERATION][:, 0],
|
||||
self.constants.T_IDXS,
|
||||
action_t=long_action_t,
|
||||
)
|
||||
accel_cmd = smooth_value(accel_cmd, previous_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS)
|
||||
|
||||
curvature_cmd = pick_curvature(outputs, plan_rows, vehicle_speed, lat_action_t, self.mlsim)
|
||||
if self.generation is not None and self.generation >= 10:
|
||||
if vehicle_speed > self.MIN_LAT_CONTROL_SPEED:
|
||||
curvature_cmd = smooth_value(curvature_cmd, previous_action.desiredCurvature, lat_smooth_seconds)
|
||||
else:
|
||||
curvature_cmd = previous_action.desiredCurvature
|
||||
|
||||
return log.ModelDataV2.Action(
|
||||
desiredCurvature=float(curvature_cmd),
|
||||
desiredAcceleration=float(accel_cmd),
|
||||
shouldStop=bool(should_stop),
|
||||
)
|
||||
|
||||
|
||||
class CameraIngress:
|
||||
def __init__(self, gpu_context: WarpContext):
|
||||
self.layout = self._discover_layout()
|
||||
self._primary = VisionIpcClient("camerad", self.layout.primary_stream, True, gpu_context)
|
||||
self._secondary = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD, False, gpu_context)
|
||||
|
||||
while not self._primary.connect(False):
|
||||
time.sleep(0.1)
|
||||
while self.layout.dual_camera and not self._secondary.connect(False):
|
||||
time.sleep(0.1)
|
||||
|
||||
cloudlog.warning(
|
||||
f"connected main cam with buffer size: {self._primary.buffer_len} ({self._primary.width} x {self._primary.height})"
|
||||
)
|
||||
if self.layout.dual_camera:
|
||||
cloudlog.warning(
|
||||
f"connected extra cam with buffer size: {self._secondary.buffer_len} ({self._secondary.width} x {self._secondary.height})"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _discover_layout() -> StreamLayout:
|
||||
while True:
|
||||
available = VisionIpcClient.available_streams("camerad", block=False)
|
||||
if available:
|
||||
dual_camera = (
|
||||
VisionStreamType.VISION_STREAM_WIDE_ROAD in available
|
||||
and VisionStreamType.VISION_STREAM_ROAD in available
|
||||
)
|
||||
main_is_wide = VisionStreamType.VISION_STREAM_ROAD not in available
|
||||
primary_stream = VisionStreamType.VISION_STREAM_WIDE_ROAD if main_is_wide else VisionStreamType.VISION_STREAM_ROAD
|
||||
cloudlog.warning(
|
||||
f"vision stream set up, main_wide_camera: {main_is_wide}, use_extra_client: {dual_camera}"
|
||||
)
|
||||
return StreamLayout(dual_camera=dual_camera, main_is_wide=main_is_wide, primary_stream=primary_stream)
|
||||
time.sleep(0.1)
|
||||
|
||||
def pull(self) -> tuple[VisionBuf, VisionBuf, CaptureStamp, CaptureStamp] | None:
|
||||
main_buf = None
|
||||
wide_buf = None
|
||||
main_stamp = CaptureStamp()
|
||||
wide_stamp = CaptureStamp()
|
||||
|
||||
while main_stamp.timestamp_sof < wide_stamp.timestamp_sof + 25000000:
|
||||
main_buf = self._primary.recv()
|
||||
main_stamp = CaptureStamp.from_vipc(self._primary)
|
||||
if main_buf is None:
|
||||
return None
|
||||
|
||||
if not self.layout.dual_camera:
|
||||
return main_buf, main_buf, main_stamp, main_stamp
|
||||
|
||||
while True:
|
||||
wide_buf = self._secondary.recv()
|
||||
wide_stamp = CaptureStamp.from_vipc(self._secondary)
|
||||
if wide_buf is None or main_stamp.timestamp_sof < wide_stamp.timestamp_sof + 25000000:
|
||||
break
|
||||
|
||||
if wide_buf is None:
|
||||
return None
|
||||
|
||||
if abs(main_stamp.timestamp_sof - wide_stamp.timestamp_sof) > 10000000:
|
||||
cloudlog.error(
|
||||
f"frames out of sync! main: {main_stamp.frame_id} ({main_stamp.timestamp_sof / 1e9:.5f}),"
|
||||
f" extra: {wide_stamp.frame_id} ({wide_stamp.timestamp_sof / 1e9:.5f})"
|
||||
)
|
||||
return main_buf, wide_buf, main_stamp, wide_stamp
|
||||
|
||||
|
||||
class CalibrationAtlas:
|
||||
def __init__(self):
|
||||
self.main_warp = np.zeros((3, 3), dtype=np.float32)
|
||||
self.extra_warp = np.zeros((3, 3), dtype=np.float32)
|
||||
self.ready = False
|
||||
self._offset_tuner = CameraOffsetHelper()
|
||||
|
||||
def set_offset(self, offset_value: Any) -> None:
|
||||
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["extrinsicsCalibration"] and sm.seen["roadCameraState"] and sm.seen["deviceState"]):
|
||||
return self.main_warp, self.extra_warp, self.ready
|
||||
|
||||
rpy = get_calibrated_rpy(sm["extrinsicsCalibration"])
|
||||
if rpy is None:
|
||||
live_calib = sm["extrinsicsCalibration"]
|
||||
if len(live_calib.rpyCalib) == 3:
|
||||
rpy = np.array(live_calib.rpyCalib, dtype=np.float32)
|
||||
else:
|
||||
rpy = np.zeros(3, dtype=np.float32)
|
||||
|
||||
device_key = (str(sm["deviceState"].deviceType), str(sm["roadCameraState"].sensor))
|
||||
device_camera = DEVICE_CAMERAS[device_key]
|
||||
main_intrinsics = device_camera.ecam.intrinsics if main_is_wide else device_camera.fcam.intrinsics
|
||||
extra_uses_wide_camera = dual_camera or main_is_wide
|
||||
extra_intrinsics = device_camera.ecam.intrinsics if extra_uses_wide_camera else device_camera.fcam.intrinsics
|
||||
self.main_warp = get_warp_matrix(rpy, main_intrinsics, False).astype(np.float32)
|
||||
self.extra_warp = get_warp_matrix(rpy, extra_intrinsics, True).astype(np.float32)
|
||||
self.main_warp, self.extra_warp = self._offset_tuner.update(
|
||||
self.main_warp, self.extra_warp, sm, main_is_wide, extra_uses_wide_camera
|
||||
)
|
||||
self.ready = True
|
||||
return self.main_warp, self.extra_warp, self.ready
|
||||
|
||||
|
||||
class FrameDropMeter:
|
||||
def __init__(self, model_freq: float):
|
||||
self._smoother = FirstOrderFilter(0.0, 10.0, 1.0 / model_freq)
|
||||
self._warm_frames = 0
|
||||
self._last_frame_id = 0
|
||||
|
||||
def sample(self, frame_id: int) -> tuple[int, float, bool]:
|
||||
dropped = max(0, frame_id - self._last_frame_id - 1)
|
||||
smooth = self._smoother.update(min(dropped, 10))
|
||||
if self._warm_frames < 10:
|
||||
self._smoother.x = 0.0
|
||||
smooth = 0.0
|
||||
self._warm_frames += 1
|
||||
return dropped, smooth / (1 + smooth), dropped > 0
|
||||
|
||||
def commit(self, frame_id: int) -> None:
|
||||
self._last_frame_id = frame_id
|
||||
|
||||
|
||||
class InferenceDaemon:
|
||||
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)
|
||||
setproctitle(PROCESS_NAME)
|
||||
config_realtime_process(7, 54)
|
||||
|
||||
cloudlog.warning("setting up CL context")
|
||||
self._gpu = WarpContext()
|
||||
cloudlog.warning("CL context ready; loading model")
|
||||
self._runtime = NeuralEngineState(self._gpu)
|
||||
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)
|
||||
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", "extrinsicsCalibration",
|
||||
"driverMonitoringState", "carControl", "lateralDelay", "iqNavState", "radarState",
|
||||
])
|
||||
self._message_memory = DrivePacketMemory()
|
||||
self._params = Params()
|
||||
self._frame_meter = FrameDropMeter(self._runtime.constants.MODEL_FREQ)
|
||||
self._warps = CalibrationAtlas()
|
||||
self._perf = PerfTraceEmitter("iqmodeld", pubmaster=self._pub)
|
||||
self._perf_ring = PerfTraceRing()
|
||||
|
||||
self._car_params = self._load_car_params(demo)
|
||||
self._long_action_delay = self._car_params.longitudinalActuatorDelay + self._runtime.LONG_SMOOTH_SECONDS
|
||||
self._previous_action = log.ModelDataV2.Action()
|
||||
self._desire_logic = DesireHelper()
|
||||
self._lat_smooth_extra_sec = 0.0
|
||||
|
||||
def _load_car_params(self, demo: bool):
|
||||
car_params = get_demo_car_params() if demo else messaging.log_from_bytes(
|
||||
self._params.get("CarParams", block=True), car.CarParams)
|
||||
cloudlog.info("iqmodeld got CarParams: %s", car_params.brand)
|
||||
return car_params
|
||||
|
||||
def _refresh_tunables(self, tick: int) -> None:
|
||||
if tick % 60 != 0:
|
||||
return
|
||||
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))
|
||||
|
||||
def _traffic_side(self) -> np.ndarray:
|
||||
traffic = np.zeros(2, dtype=np.float32)
|
||||
traffic[int(self._sub["driverMonitoringState"].isRHD)] = 1
|
||||
return traffic
|
||||
|
||||
def _desire_pulse(self) -> np.ndarray:
|
||||
pulse = np.zeros(self._runtime.constants.DESIRE_LEN, dtype=np.float32)
|
||||
desire_idx = self._desire_logic.desire
|
||||
if 0 <= desire_idx < self._runtime.constants.DESIRE_LEN:
|
||||
pulse[desire_idx] = 1
|
||||
return pulse
|
||||
|
||||
def _compose_inputs(self, vehicle_speed: float, lat_horizon: float, long_horizon: float) -> dict[str, np.ndarray]:
|
||||
inputs: dict[str, np.ndarray] = {
|
||||
self._runtime.desire_key: self._desire_pulse(),
|
||||
"traffic_convention": self._traffic_side(),
|
||||
}
|
||||
if "lateral_control_params" in self._runtime.numpy_inputs:
|
||||
inputs["lateral_control_params"] = np.array([vehicle_speed, lat_horizon], dtype=np.float32)
|
||||
if "action_t" in self._runtime.numpy_inputs:
|
||||
inputs["action_t"] = np.array([lat_horizon, long_horizon], dtype=np.float32)
|
||||
return inputs
|
||||
|
||||
def _publish(self, outputs: dict[str, np.ndarray], main_stamp: CaptureStamp, extra_stamp: CaptureStamp,
|
||||
road_frame_id: int, frame_drop_ratio: float, dropped_frames: int,
|
||||
execution_time: float, live_calib_seen: bool,
|
||||
lat_horizon: float, long_horizon: float, vehicle_speed: float) -> None:
|
||||
model_msg = messaging.new_message("modelV2")
|
||||
driving_msg = messaging.new_message("drivingModelData")
|
||||
pose_msg = messaging.new_message("cameraOdometry")
|
||||
iq_msg = messaging.new_message("iqDriveModelData")
|
||||
|
||||
self._lat_smooth_extra_sec = dynamic_lat_smooth_extra_seconds(
|
||||
_plan_y_std_1s(outputs), self._runtime.model_smoothing_max_extra_sec
|
||||
)
|
||||
lat_smooth_total_sec = min(self._runtime.LAT_SMOOTH_SECONDS + self._lat_smooth_extra_sec, MODEL_SMOOTHING_MAX_TOTAL_SEC)
|
||||
action = self._runtime.get_action_from_model(
|
||||
outputs, self._previous_action, lat_horizon, long_horizon, vehicle_speed, lat_smooth_total_sec
|
||||
)
|
||||
self._previous_action = action
|
||||
|
||||
populate_drive_messages(
|
||||
driving_msg,
|
||||
model_msg,
|
||||
outputs,
|
||||
action,
|
||||
self._message_memory,
|
||||
main_stamp.frame_id,
|
||||
extra_stamp.frame_id,
|
||||
road_frame_id,
|
||||
frame_drop_ratio,
|
||||
main_stamp.timestamp_eof,
|
||||
execution_time,
|
||||
live_calib_seen,
|
||||
self._meta_layout,
|
||||
)
|
||||
|
||||
desire_state = model_msg.modelV2.meta.desireState
|
||||
lane_change_prob = desire_state[log.Desire.laneChangeLeft] + desire_state[log.Desire.laneChangeRight]
|
||||
self._desire_logic.update(
|
||||
self._sub["carState"],
|
||||
self._sub["carControl"].latActive,
|
||||
lane_change_prob,
|
||||
self._sub["iqNavState"],
|
||||
model_msg.modelV2,
|
||||
self._sub["radarState"],
|
||||
)
|
||||
model_msg.modelV2.meta.laneChangeState = self._desire_logic.lane_change_state
|
||||
model_msg.modelV2.meta.laneChangeDirection = self._desire_logic.lane_change_direction
|
||||
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
|
||||
|
||||
populate_odometry_message(
|
||||
pose_msg,
|
||||
outputs,
|
||||
main_stamp.frame_id,
|
||||
dropped_frames,
|
||||
main_stamp.timestamp_eof,
|
||||
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)
|
||||
self._pub.send("iqDriveModelData", iq_msg)
|
||||
|
||||
def serve(self) -> None:
|
||||
tick = 0
|
||||
starved_polls = 0
|
||||
while True:
|
||||
frame_pair = self._cameras.pull()
|
||||
if frame_pair is None:
|
||||
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)
|
||||
|
||||
vehicle_speed = max(self._sub["carState"].vEgo, 0.0)
|
||||
lat_horizon = self._runtime.lat_delay + self._runtime.LAT_SMOOTH_SECONDS + self._lat_smooth_extra_sec + DT_MDL
|
||||
long_horizon = self._long_action_delay + DT_MDL
|
||||
|
||||
main_warp, extra_warp, live_calib_seen = self._warps.refresh(
|
||||
self._sub, self._cameras.layout.main_is_wide, self._cameras.layout.dual_camera
|
||||
)
|
||||
dropped_frames, frame_drop_ratio, prepare_only = self._frame_meter.sample(main_stamp.frame_id)
|
||||
|
||||
vision_bufs = {
|
||||
stream_name: extra_buf if "big" in stream_name else main_buf
|
||||
for stream_name in self._runtime.model_runner.vision_input_names
|
||||
}
|
||||
warp_map = {
|
||||
stream_name: extra_warp if "big" in stream_name else main_warp
|
||||
for stream_name in self._runtime.model_runner.vision_input_names
|
||||
}
|
||||
fresh_inputs = self._compose_inputs(vehicle_speed, lat_horizon, long_horizon)
|
||||
|
||||
started_at = time.perf_counter()
|
||||
outputs = self._runtime.run(vision_bufs, warp_map, fresh_inputs)
|
||||
execution_time = time.perf_counter() - started_at
|
||||
execution_us = int(execution_time * 1_000_000)
|
||||
|
||||
sample = PerfSample(
|
||||
frame_id=main_stamp.frame_id,
|
||||
model_eval_us=execution_us,
|
||||
model_dropped_frames=dropped_frames,
|
||||
model_backlog=max(0, dropped_frames),
|
||||
)
|
||||
self._perf_ring.push(sample)
|
||||
if dropped_frames > 0 or execution_us >= IQMODEL_EVAL_WARN_US:
|
||||
severity = "warning"
|
||||
if dropped_frames > 0 or execution_us >= IQMODEL_EVAL_ERROR_US:
|
||||
severity = "error"
|
||||
self._perf.emit(
|
||||
"iqmodeld_dropped_frames" if dropped_frames > 0 else "iqmodeld_slow_eval",
|
||||
severity=severity,
|
||||
frame_id=main_stamp.frame_id,
|
||||
total_time_us=execution_us,
|
||||
dropped_frames=dropped_frames,
|
||||
backlog=max(0, dropped_frames),
|
||||
samples=self._perf_ring.snapshot(),
|
||||
detail=(
|
||||
f"model_eval_us={execution_us} dropped_frames={dropped_frames} prepare_only={int(prepare_only)} "
|
||||
f"road_frame_id={self._sub['roadCameraState'].frameId}"
|
||||
),
|
||||
min_interval_s=0.25,
|
||||
)
|
||||
|
||||
if outputs is not None:
|
||||
self._publish(
|
||||
outputs,
|
||||
main_stamp,
|
||||
extra_stamp,
|
||||
self._sub["roadCameraState"].frameId,
|
||||
frame_drop_ratio,
|
||||
dropped_frames,
|
||||
execution_time,
|
||||
live_calib_seen,
|
||||
lat_horizon,
|
||||
long_horizon,
|
||||
vehicle_speed,
|
||||
)
|
||||
|
||||
self._frame_meter.commit(main_stamp.frame_id)
|
||||
tick += 1
|
||||
|
||||
|
||||
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__ = [
|
||||
"PROCESS_NAME",
|
||||
"IQP_NAV_MODEL_INFLUENCE_ENABLED",
|
||||
"TurnDirection",
|
||||
"CaptureStamp",
|
||||
"ReplayLedger",
|
||||
"NeuralEngineState",
|
||||
"CameraIngress",
|
||||
"CalibrationAtlas",
|
||||
"FrameDropMeter",
|
||||
"InferenceDaemon",
|
||||
"main",
|
||||
]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--demo", action="store_true", help="Run iqmodeld in demo mode.")
|
||||
args = parser.parse_args()
|
||||
main(demo=args.demo)
|
||||
except KeyboardInterrupt:
|
||||
cloudlog.warning(f"child {PROCESS_NAME} got SIGINT")
|
||||
except Exception:
|
||||
sentry.capture_exception()
|
||||
raise
|
||||
62
iqpilot/selfdrive/iqmodeld/default_model/bundle.json
Normal file
62
iqpilot/selfdrive/iqmodeld/default_model/bundle.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
BIN
iqpilot/selfdrive/iqmodeld/default_model/driving_policy_c210m_metadata.pkl
Executable file
BIN
iqpilot/selfdrive/iqmodeld/default_model/driving_policy_c210m_metadata.pkl
Executable file
Binary file not shown.
BIN
iqpilot/selfdrive/iqmodeld/default_model/driving_policy_c210m_tinygrad.pkl
Executable file
BIN
iqpilot/selfdrive/iqmodeld/default_model/driving_policy_c210m_tinygrad.pkl
Executable file
Binary file not shown.
BIN
iqpilot/selfdrive/iqmodeld/default_model/driving_vision_c210m_metadata.pkl
Executable file
BIN
iqpilot/selfdrive/iqmodeld/default_model/driving_vision_c210m_metadata.pkl
Executable file
Binary file not shown.
BIN
iqpilot/selfdrive/iqmodeld/default_model/driving_vision_c210m_tinygrad.pkl
Executable file
BIN
iqpilot/selfdrive/iqmodeld/default_model/driving_vision_c210m_tinygrad.pkl
Executable file
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)
|
||||
158
iqpilot/selfdrive/iqmodeld/egpu_helpers.py
Normal file
158
iqpilot/selfdrive/iqmodeld/egpu_helpers.py
Normal file
@@ -0,0 +1,158 @@
|
||||
"""
|
||||
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
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
from iqpilot.system.hardware.usb import egpu_dock_ready
|
||||
|
||||
USB_SYSFS_ROOT = "/sys/bus/usb/devices"
|
||||
|
||||
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 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)}")
|
||||
|
||||
url = resolve_download_url(download_url, meta["sha256"], size)
|
||||
path = onnx_cache_path(meta)
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
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
|
||||
return _orig(path, name, sha256)
|
||||
|
||||
fetch_fw._iq_patched = True
|
||||
helpers.fetch_fw = fetch_fw
|
||||
100
iqpilot/selfdrive/iqmodeld/egpu_model.py
Normal file
100
iqpilot/selfdrive/iqmodeld/egpu_model.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
EGPU_MODELS: dict[str, dict] = {
|
||||
"lebrowski": {
|
||||
"model_name": "big_driving_supercombo",
|
||||
"sha256": "a501760a9d1d5fef0eab2b8c5d122d06124fc26dc8e0782e0aa94b82a208f0ff",
|
||||
"output_len": 2580,
|
||||
"frame_skip": 4,
|
||||
"download": {"kind": "comma_lfs", "size": 1757355221},
|
||||
"input_shapes": {
|
||||
"img": (1, 12, 128, 256),
|
||||
"big_img": (1, 12, 128, 256),
|
||||
"desire_pulse": (1, 25, 8),
|
||||
"traffic_convention": (1, 2),
|
||||
"action_t": (1, 2),
|
||||
"features_buffer": (1, 24, 512),
|
||||
},
|
||||
"output_slices": {
|
||||
"lane_lines": slice(0, 528),
|
||||
"lane_lines_prob": slice(528, 536),
|
||||
"road_edges": slice(536, 800),
|
||||
"meta": slice(800, 855),
|
||||
"desire_pred": slice(855, 887),
|
||||
"pose": slice(887, 899),
|
||||
"wide_from_device_euler": slice(899, 905),
|
||||
"road_transform": slice(905, 917),
|
||||
"plan": slice(917, 1907),
|
||||
"lead": slice(1907, 2051),
|
||||
"lead_prob": slice(2051, 2054),
|
||||
"desire_state": slice(2054, 2062),
|
||||
"action": slice(2062, 2066),
|
||||
"hidden_state": slice(2066, 2578),
|
||||
"pad": slice(-2, None),
|
||||
},
|
||||
},
|
||||
"comma_small": {
|
||||
"model_name": "driving_supercombo",
|
||||
"sha256": "659727c4d4839adc4992a254409a54259a8756a743f2d567bf5fdc6579f8009b",
|
||||
"output_len": 2576,
|
||||
"frame_skip": 4,
|
||||
"download": {"kind": "comma_lfs", "size": 60881999},
|
||||
"output_slices": {
|
||||
"meta": slice(0, 55),
|
||||
"desire_pred": slice(55, 87),
|
||||
"pose": slice(87, 99),
|
||||
"wide_from_device_euler": slice(99, 105),
|
||||
"road_transform": slice(105, 117),
|
||||
"lane_lines": slice(117, 645),
|
||||
"lane_lines_prob": slice(645, 653),
|
||||
"road_edges": slice(653, 917),
|
||||
"lead": slice(917, 1061),
|
||||
"lead_prob": slice(1061, 1064),
|
||||
"hidden_state": slice(1064, 1576),
|
||||
"plan": slice(1576, 2566),
|
||||
"desire_state": slice(2566, 2574),
|
||||
"pad": slice(-2, None),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
DEFAULT_EGPU_MODEL = "lebrowski"
|
||||
|
||||
BIG_MODEL_NAME = "big_driving_supercombo"
|
||||
|
||||
|
||||
def get_egpu_model(key: str | None = None) -> dict:
|
||||
name = key or DEFAULT_EGPU_MODEL
|
||||
if name not in EGPU_MODELS:
|
||||
raise KeyError(f"unknown eGPU model {name!r}; known: {sorted(EGPU_MODELS)}")
|
||||
return {"key": name, **EGPU_MODELS[name]}
|
||||
|
||||
|
||||
def resolve_egpu_model(params, selected: str | bytes | None = None, allow_refresh: bool = True) -> dict | None:
|
||||
name = selected if selected is not None else (params.get("IQEmacModel") if params is not None else None)
|
||||
name = name.decode() if isinstance(name, bytes) else (name or "")
|
||||
if not name or name == DEFAULT_EGPU_MODEL:
|
||||
return get_egpu_model()
|
||||
from iqpilot.selfdrive.iqmodeld.big_catalog import cached_catalog, refresh_catalog
|
||||
catalog = cached_catalog(params)
|
||||
if name not in catalog and params is not None and allow_refresh:
|
||||
refresh_catalog(params)
|
||||
catalog = cached_catalog(params)
|
||||
meta = catalog.get(name)
|
||||
if meta is None or meta.get("model_name") != BIG_MODEL_NAME:
|
||||
return None
|
||||
return {"key": name, **meta}
|
||||
|
||||
|
||||
def download_descriptor(meta: dict) -> tuple[str, int]:
|
||||
dl = meta.get("download")
|
||||
if not dl:
|
||||
return "", 0
|
||||
if dl["kind"] == "comma_lfs":
|
||||
return f"commalfs:{meta['sha256']}", int(dl["size"])
|
||||
if dl["kind"] == "url":
|
||||
return str(dl["url"]), int(dl.get("size", 0))
|
||||
return "", 0
|
||||
47
iqpilot/selfdrive/iqmodeld/egpu_pipeline.py
Normal file
47
iqpilot/selfdrive/iqmodeld/egpu_pipeline.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
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.temporal_state import MODEL_INPUT_SPEC, TemporalInputState, spec_from_meta
|
||||
|
||||
|
||||
class EgpuPipelineError(RuntimeError):
|
||||
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:
|
||||
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)
|
||||
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 EgpuPipelineError("eGPU output contains non-finite values")
|
||||
self.state.note_hidden_state(out, self.hidden_slice)
|
||||
return out
|
||||
|
||||
|
||||
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,
|
||||
}
|
||||
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 *
|
||||
377
iqpilot/selfdrive/iqmodeld/iqegpumodeld.py
Normal file
377
iqpilot/selfdrive/iqmodeld/iqegpumodeld.py
Normal file
@@ -0,0 +1,377 @@
|
||||
"""
|
||||
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 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, egpu_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 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.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_RETRY_BASE_S = 3.0
|
||||
SETUP_RETRY_MAX_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():
|
||||
return
|
||||
except Exception:
|
||||
return
|
||||
time.sleep(0.5)
|
||||
|
||||
|
||||
def _compile_in_subprocess(meta: dict, onnx_path: str, pkl_path: str) -> None:
|
||||
cmd = [sys.executable, "-m", "iqpilot.selfdrive.iqmodeld.tools.compile_egpu_model",
|
||||
"--model", meta["key"], "--onnx", onnx_path, "--output", pkl_path]
|
||||
compile_env = {**os.environ, "DEV": "USB+AMD:LLVM", "FLOAT16": "1",
|
||||
"JIT_BATCH_SIZE": "0", "GMMU": "0"}
|
||||
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}")
|
||||
|
||||
|
||||
def _ensure_artifact(params: Params, meta: dict) -> str:
|
||||
pkl_path = egpu_pkl_path(meta)
|
||||
if os.path.isfile(pkl_path):
|
||||
return pkl_path
|
||||
|
||||
params.put_bool("UsbGpuCompiled", False)
|
||||
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:.3f}")
|
||||
|
||||
onnx_path = download_onnx(meta, progress_cb=_prog)
|
||||
|
||||
cloudlog.warning(f"iqegpumodeld compiling {meta['key']} for USB-AMD (one-time, can take minutes)")
|
||||
_compile_in_subprocess(meta, onnx_path, pkl_path)
|
||||
cloudlog.warning(f"iqegpumodeld compiled -> {pkl_path}")
|
||||
return pkl_path
|
||||
|
||||
|
||||
def _load_infer_fn(pkl_path: str, meta: dict):
|
||||
patch_tinygrad_fetch_fw()
|
||||
from tinygrad.tensor import Tensor
|
||||
|
||||
with open(pkl_path, "rb") as f:
|
||||
bundle = pickle.load(f)
|
||||
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']}")
|
||||
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()
|
||||
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")
|
||||
warp = FrameWarp(cameras._primary.width, cameras._primary.height, meta["frame_skip"])
|
||||
pkl_path = _ensure_artifact(params, meta)
|
||||
infer_fn, input_spec = _load_infer_fn(pkl_path, meta)
|
||||
warm_s = _warmup(infer_fn, input_spec, meta["output_len"])
|
||||
break
|
||||
except Exception as e:
|
||||
attempt += 1
|
||||
params.put("UsbGpuLastError", str(e)[:512])
|
||||
cloudlog.warning(f"iqegpumodeld setup attempt {attempt} failed: {e}; retrying")
|
||||
if not usbgpu_present():
|
||||
_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("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)")
|
||||
|
||||
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
|
||||
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()
|
||||
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()
|
||||
stats["warp"].append(t_warp - started_at)
|
||||
|
||||
try:
|
||||
output = pipeline.run(warped, desire_vec, traffic, action_t)
|
||||
except EgpuPipelineError as e:
|
||||
park(str(e))
|
||||
except Exception as e:
|
||||
park(f"eGPU inference failed: {e}")
|
||||
t_infer = time.perf_counter()
|
||||
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
|
||||
|
||||
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)}")
|
||||
16
iqpilot/selfdrive/iqmodeld/iqmodeld
Executable file
16
iqpilot/selfdrive/iqmodeld/iqmodeld
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
script_home() {
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd
|
||||
}
|
||||
|
||||
main() {
|
||||
local here
|
||||
here="$(script_home)"
|
||||
exec "$here/daemon.py" "$@"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
269
iqpilot/selfdrive/iqmodeld/messaging.py
Normal file
269
iqpilot/selfdrive/iqmodeld/messaging.py
Normal file
@@ -0,0 +1,269 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import capnp
|
||||
import numpy as np
|
||||
|
||||
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
|
||||
|
||||
|
||||
def pick_curvature(outputs: dict[str, np.ndarray], plan_rows: np.ndarray, vehicle_speed: float,
|
||||
action_horizon: float, synthetic_lane_logic: bool) -> float:
|
||||
direct_signal = None if synthetic_lane_logic else outputs.get("desired_curvature")
|
||||
if direct_signal is not None:
|
||||
return float(direct_signal[0, 0])
|
||||
|
||||
yaw_track = plan_rows[:, Plan.T_FROM_CURRENT_EULER][:, 2]
|
||||
yaw_rate_track = plan_rows[:, Plan.ORIENTATION_RATE][:, 2]
|
||||
return float(get_curvature_from_plan(yaw_track, yaw_rate_track, ModelConstants.T_IDXS, vehicle_speed, action_horizon))
|
||||
|
||||
|
||||
@dataclass
|
||||
class DrivePacketMemory:
|
||||
disengage_rollup: np.ndarray = field(default_factory=lambda: np.zeros(
|
||||
ModelConstants.CONFIDENCE_BUFFER_LEN * ModelConstants.DISENGAGE_WIDTH, dtype=np.float32))
|
||||
brake_watch_5: np.ndarray = field(default_factory=lambda: np.zeros(
|
||||
ModelConstants.FCW_5MS2_PROBS_WIDTH, dtype=np.float32))
|
||||
brake_watch_3: np.ndarray = field(default_factory=lambda: np.zeros(
|
||||
ModelConstants.FCW_3MS2_PROBS_WIDTH, dtype=np.float32))
|
||||
|
||||
|
||||
def _assign_xyz(builder, t_points, x_track, y_track, z_track,
|
||||
x_std=None, y_std=None, z_std=None) -> None:
|
||||
builder.t = t_points
|
||||
builder.x = x_track.tolist()
|
||||
builder.y = y_track.tolist()
|
||||
builder.z = z_track.tolist()
|
||||
if x_std is not None:
|
||||
builder.xStd = x_std.tolist()
|
||||
if y_std is not None:
|
||||
builder.yStd = y_std.tolist()
|
||||
if z_std is not None:
|
||||
builder.zStd = z_std.tolist()
|
||||
|
||||
|
||||
def _assign_xyva(builder, t_points, x_track, y_track, v_track, a_track,
|
||||
x_std=None, y_std=None, v_std=None, a_std=None) -> None:
|
||||
builder.t = t_points
|
||||
builder.x = x_track.tolist()
|
||||
builder.y = y_track.tolist()
|
||||
builder.v = v_track.tolist()
|
||||
builder.a = a_track.tolist()
|
||||
if x_std is not None:
|
||||
builder.xStd = x_std.tolist()
|
||||
if y_std is not None:
|
||||
builder.yStd = y_std.tolist()
|
||||
if v_std is not None:
|
||||
builder.vStd = v_std.tolist()
|
||||
if a_std is not None:
|
||||
builder.aStd = a_std.tolist()
|
||||
|
||||
|
||||
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()
|
||||
builder.yCoefficients = coeffs[:, 1].tolist()
|
||||
builder.zCoefficients = coeffs[:, 2].tolist()
|
||||
|
||||
|
||||
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]
|
||||
builder.rightProb = lane_probs[2]
|
||||
|
||||
|
||||
def _roll_brake_watch(outputs: dict[str, np.ndarray], memory: DrivePacketMemory, meta_layout) -> bool:
|
||||
memory.brake_watch_5[:-1] = memory.brake_watch_5[1:]
|
||||
memory.brake_watch_5[-1] = outputs["meta"][0, meta_layout.HARD_BRAKE_5][0]
|
||||
memory.brake_watch_3[:-1] = memory.brake_watch_3[1:]
|
||||
memory.brake_watch_3[-1] = outputs["meta"][0, meta_layout.HARD_BRAKE_3][0]
|
||||
return bool(
|
||||
(memory.brake_watch_5 > ModelConstants.FCW_THRESHOLDS_5MS2).all()
|
||||
and (memory.brake_watch_3 > ModelConstants.FCW_THRESHOLDS_3MS2).all()
|
||||
)
|
||||
|
||||
|
||||
def _confidence_bucket(outputs: dict[str, np.ndarray], memory: DrivePacketMemory, meta_layout, frame_id: int):
|
||||
width = ModelConstants.DISENGAGE_WIDTH
|
||||
if frame_id % (2 * ModelConstants.MODEL_FREQ) == 0:
|
||||
brake_probs = outputs["meta"][0, meta_layout.BRAKE_DISENGAGE]
|
||||
gas_probs = outputs["meta"][0, meta_layout.GAS_DISENGAGE]
|
||||
steer_probs = outputs["meta"][0, meta_layout.STEER_OVERRIDE]
|
||||
takeover_curve = 1 - ((1 - brake_probs) * (1 - gas_probs) * (1 - steer_probs))
|
||||
independent = np.r_[takeover_curve[0], np.diff(takeover_curve) / (1 - takeover_curve[:-1])]
|
||||
memory.disengage_rollup[:-width] = memory.disengage_rollup[width:]
|
||||
memory.disengage_rollup[-width:] = independent
|
||||
|
||||
score = 0.0
|
||||
for idx in range(width):
|
||||
score += memory.disengage_rollup[idx * width + width - 1 - idx].item() / width
|
||||
|
||||
if score < ModelConstants.RYG_GREEN:
|
||||
return ConfidenceClass.green
|
||||
if score < ModelConstants.RYG_YELLOW:
|
||||
return ConfidenceClass.yellow
|
||||
return ConfidenceClass.red
|
||||
|
||||
|
||||
def _write_plan_family(model_packet, driving_packet, outputs: dict[str, np.ndarray]) -> None:
|
||||
plan_rows = outputs["plan"][0]
|
||||
plan_stds = outputs["plan_stds"][0]
|
||||
_assign_xyz(model_packet.position, ModelConstants.T_IDXS, *plan_rows[:, Plan.POSITION].T, *plan_stds[:, Plan.POSITION].T)
|
||||
_assign_xyz(model_packet.velocity, ModelConstants.T_IDXS, *plan_rows[:, Plan.VELOCITY].T)
|
||||
_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)
|
||||
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:
|
||||
pose_packet = model_packet.temporalPoseDEPRECATED
|
||||
if "sim_pose" in outputs:
|
||||
half_width = ModelConstants.POSE_WIDTH // 2
|
||||
pose_packet.trans = outputs["sim_pose"][0, :half_width].tolist()
|
||||
pose_packet.transStd = outputs["sim_pose_stds"][0, :half_width].tolist()
|
||||
pose_packet.rot = outputs["sim_pose"][0, half_width:].tolist()
|
||||
pose_packet.rotStd = outputs["sim_pose_stds"][0, half_width:].tolist()
|
||||
return
|
||||
|
||||
pose_packet.trans = outputs["plan"][0, 0, Plan.VELOCITY].tolist()
|
||||
pose_packet.transStd = outputs["plan_stds"][0, 0, Plan.VELOCITY].tolist()
|
||||
pose_packet.rot = outputs["plan"][0, 0, Plan.ORIENTATION_RATE].tolist()
|
||||
pose_packet.rotStd = outputs["plan_stds"][0, 0, Plan.ORIENTATION_RATE].tolist()
|
||||
|
||||
|
||||
def _write_lane_family(model_packet, driving_packet, outputs: dict[str, np.ndarray]) -> None:
|
||||
time_axis = plan_x_idxs_helper(ModelConstants, Plan, outputs)
|
||||
model_packet.init("laneLines", 4)
|
||||
for lane_idx in range(4):
|
||||
lane_builder = model_packet.laneLines[lane_idx]
|
||||
_assign_xyz(
|
||||
lane_builder,
|
||||
time_axis,
|
||||
np.array(ModelConstants.X_IDXS),
|
||||
outputs["lane_lines"][0, lane_idx, :, 0],
|
||||
outputs["lane_lines"][0, lane_idx, :, 1],
|
||||
)
|
||||
model_packet.laneLineStds = outputs["lane_lines_stds"][0, :, 0, 0].tolist()
|
||||
model_packet.laneLineProbs = outputs["lane_lines_prob"][0, 1::2].tolist()
|
||||
fill_lane_line_meta(driving_packet.laneLineMeta, model_packet.laneLines, model_packet.laneLineProbs)
|
||||
|
||||
model_packet.init("roadEdges", 2)
|
||||
for edge_idx in range(2):
|
||||
edge_builder = model_packet.roadEdges[edge_idx]
|
||||
_assign_xyz(
|
||||
edge_builder,
|
||||
time_axis,
|
||||
np.array(ModelConstants.X_IDXS),
|
||||
outputs["road_edges"][0, edge_idx, :, 0],
|
||||
outputs["road_edges"][0, edge_idx, :, 1],
|
||||
)
|
||||
model_packet.roadEdgeStds = outputs["road_edges_stds"][0, :, 0, 0].tolist()
|
||||
|
||||
|
||||
def _write_leads(model_packet, outputs: dict[str, np.ndarray]) -> None:
|
||||
model_packet.init("leadsV3", 3)
|
||||
for lead_idx in range(3):
|
||||
lead_builder = model_packet.leadsV3[lead_idx]
|
||||
_assign_xyva(
|
||||
lead_builder,
|
||||
ModelConstants.LEAD_T_IDXS,
|
||||
*outputs["lead"][0, lead_idx].T,
|
||||
*outputs["lead_stds"][0, lead_idx].T,
|
||||
)
|
||||
lead_builder.prob = outputs["lead_prob"][0, lead_idx].tolist()
|
||||
lead_builder.probTime = ModelConstants.LEAD_T_OFFSETS[lead_idx]
|
||||
|
||||
|
||||
def _write_meta(model_packet, outputs: dict[str, np.ndarray], memory: DrivePacketMemory, meta_layout, frame_id: int) -> None:
|
||||
meta = model_packet.meta
|
||||
meta.desireState = outputs["desire_state"][0].reshape(-1).tolist()
|
||||
meta.desirePrediction = outputs["desire_pred"][0].reshape(-1).tolist()
|
||||
meta.engagedProb = outputs["meta"][0, meta_layout.ENGAGED].item()
|
||||
meta.init("disengagePredictions")
|
||||
|
||||
pred = meta.disengagePredictions
|
||||
pred.t = ModelConstants.META_T_IDXS
|
||||
pred.brakeDisengageProbs = outputs["meta"][0, meta_layout.BRAKE_DISENGAGE].tolist()
|
||||
pred.gasDisengageProbs = outputs["meta"][0, meta_layout.GAS_DISENGAGE].tolist()
|
||||
pred.steerOverrideProbs = outputs["meta"][0, meta_layout.STEER_OVERRIDE].tolist()
|
||||
pred.brake3MetersPerSecondSquaredProbs = outputs["meta"][0, meta_layout.HARD_BRAKE_3].tolist()
|
||||
pred.brake4MetersPerSecondSquaredProbs = outputs["meta"][0, meta_layout.HARD_BRAKE_4].tolist()
|
||||
pred.brake5MetersPerSecondSquaredProbs = outputs["meta"][0, meta_layout.HARD_BRAKE_5].tolist()
|
||||
|
||||
if hasattr(meta_layout, "GAS_PRESS") and hasattr(meta_layout, "BRAKE_PRESS"):
|
||||
pred.gasPressProbs = outputs["meta"][0, meta_layout.GAS_PRESS].tolist()
|
||||
pred.brakePressProbs = outputs["meta"][0, meta_layout.BRAKE_PRESS].tolist()
|
||||
|
||||
meta.hardBrakePredicted = _roll_brake_watch(outputs, memory, meta_layout)
|
||||
model_packet.confidence = _confidence_bucket(outputs, memory, meta_layout, frame_id)
|
||||
|
||||
|
||||
def populate_drive_messages(primary_msg: capnp._DynamicStructBuilder, extended_msg: capnp._DynamicStructBuilder,
|
||||
outputs: dict[str, np.ndarray], action: log.ModelDataV2.Action,
|
||||
memory: DrivePacketMemory, vipc_frame_id: int, vipc_frame_id_extra: int,
|
||||
frame_id: int, frame_drop: float, timestamp_eof: int,
|
||||
model_execution_time: float, valid: bool, meta_layout) -> None:
|
||||
frame_age = frame_id - vipc_frame_id if frame_id > vipc_frame_id else 0
|
||||
frame_drop_percent = frame_drop * 100
|
||||
primary_msg.valid = valid
|
||||
extended_msg.valid = valid
|
||||
|
||||
driving_packet = primary_msg.drivingModelData
|
||||
driving_packet.frameId = vipc_frame_id
|
||||
driving_packet.frameIdExtra = vipc_frame_id_extra
|
||||
driving_packet.frameDropPerc = frame_drop_percent
|
||||
driving_packet.modelExecutionTime = model_execution_time
|
||||
driving_packet.action = action
|
||||
|
||||
model_packet = extended_msg.modelV2
|
||||
model_packet.frameId = vipc_frame_id
|
||||
model_packet.frameIdExtra = vipc_frame_id_extra
|
||||
model_packet.frameAge = frame_age
|
||||
model_packet.frameDropPerc = frame_drop_percent
|
||||
model_packet.timestampEof = timestamp_eof
|
||||
model_packet.modelExecutionTime = model_execution_time
|
||||
model_packet.action = action
|
||||
|
||||
_write_plan_family(model_packet, driving_packet, outputs)
|
||||
_write_temporal_pose(model_packet, outputs)
|
||||
_write_lane_family(model_packet, driving_packet, outputs)
|
||||
_write_leads(model_packet, outputs)
|
||||
_write_meta(model_packet, outputs, memory, meta_layout, vipc_frame_id)
|
||||
|
||||
if SEND_RAW_PRED:
|
||||
model_packet.rawPredictions = outputs["raw_pred"].tobytes()
|
||||
|
||||
|
||||
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)
|
||||
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.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.wideFromDeviceEulerStd = outputs["wide_from_device_euler_stds"][0, :].tolist()
|
||||
odo.roadTransformTransStd = outputs["road_transform_stds"][0, :3].tolist()
|
||||
|
||||
__all__ = [
|
||||
"DrivePacketMemory",
|
||||
"pick_curvature",
|
||||
"populate_drive_messages",
|
||||
"populate_odometry_message",
|
||||
]
|
||||
96
iqpilot/selfdrive/iqmodeld/metadata.py
Executable file
96
iqpilot/selfdrive/iqmodeld/metadata.py
Executable file
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
import codecs
|
||||
import pathlib
|
||||
import pickle
|
||||
import sys
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
from iqpilot.cereal import custom
|
||||
from tinygrad.nn.onnx import OnnxPBParser
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.models.helpers import get_active_bundle
|
||||
from iqpilot.selfdrive.iqmodeld.config import Meta
|
||||
|
||||
|
||||
ModelBundle = custom.IQModelManager.ModelBundle
|
||||
|
||||
|
||||
def _blank_proto_doc() -> dict[str, Any]:
|
||||
return {"graph": {"input": [], "output": []}, "metadata_props": []}
|
||||
|
||||
|
||||
class TelemetryEnvelopeParser(OnnxPBParser):
|
||||
def _parse_ModelProto(self) -> dict:
|
||||
envelope = _blank_proto_doc()
|
||||
for fid, wire_type in self._parse_message(self.reader.len):
|
||||
if fid == 7:
|
||||
envelope["graph"] = self._parse_GraphProto()
|
||||
elif fid == 14:
|
||||
envelope["metadata_props"].append(self._parse_StringStringEntryProto())
|
||||
else:
|
||||
self.reader.skip_field(wire_type)
|
||||
return envelope
|
||||
|
||||
|
||||
def _shape_fingerprint(value_info: dict[str, Any]) -> tuple[str, tuple[int, ...]]:
|
||||
resolved = []
|
||||
for axis in value_info["parsed_type"].shape:
|
||||
resolved.append(int(axis) if isinstance(axis, int) else 0)
|
||||
return value_info["name"], tuple(resolved)
|
||||
|
||||
|
||||
def _lookup_metadata(props: Iterable[dict[str, Any]], wanted_key: str) -> str | Any:
|
||||
for entry in props:
|
||||
if entry["key"] == wanted_key:
|
||||
return entry["value"]
|
||||
return None
|
||||
|
||||
|
||||
class Meta20hz(Meta):
|
||||
ENGAGED = slice(0, 1)
|
||||
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)
|
||||
GAS_PRESS = slice(31, 55, 4)
|
||||
BRAKE_PRESS = slice(32, 55, 4)
|
||||
LEFT_BLINKER = slice(33, 55, 4)
|
||||
RIGHT_BLINKER = slice(34, 55, 4)
|
||||
|
||||
|
||||
def select_meta_layout():
|
||||
active_bundle = get_active_bundle()
|
||||
return Meta20hz if active_bundle is not None and active_bundle.is20hz else Meta
|
||||
|
||||
|
||||
def _decoded_slices(props: Iterable[dict[str, Any]]):
|
||||
encoded = _lookup_metadata(props, "output_slices")
|
||||
assert encoded is not None, "output_slices not found in metadata"
|
||||
return pickle.loads(codecs.decode(encoded.encode(), "base64"))
|
||||
|
||||
|
||||
def _graph_shape_table(graph_doc: dict[str, Any], field_name: str) -> dict[str, tuple[int, ...]]:
|
||||
return dict(_shape_fingerprint(item) for item in graph_doc[field_name])
|
||||
|
||||
|
||||
def build_metadata_record(model_path):
|
||||
parsed = TelemetryEnvelopeParser(model_path).parse()
|
||||
props = parsed["metadata_props"]
|
||||
graph = parsed["graph"]
|
||||
return {
|
||||
"model_checkpoint": _lookup_metadata(props, "model_checkpoint"),
|
||||
"output_slices": _decoded_slices(props),
|
||||
"input_shapes": _graph_shape_table(graph, "input"),
|
||||
"output_shapes": _graph_shape_table(graph, "output"),
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
model_path = pathlib.Path(sys.argv[1])
|
||||
metadata_path = model_path.parent / f"{model_path.stem}_metadata.pkl"
|
||||
with open(metadata_path, "wb") as handle:
|
||||
pickle.dump(build_metadata_record(model_path), handle)
|
||||
print(f"saved metadata to {metadata_path}")
|
||||
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)
|
||||
373
iqpilot/selfdrive/iqmodeld/modeld_selector.py
Normal file
373
iqpilot/selfdrive/iqmodeld/modeld_selector.py
Normal file
@@ -0,0 +1,373 @@
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
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}")
|
||||
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")
|
||||
3
iqpilot/selfdrive/iqmodeld/models/__init__.py
Normal file
3
iqpilot/selfdrive/iqmodeld/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
100
iqpilot/selfdrive/iqmodeld/models/combined_artifact.py
Normal file
100
iqpilot/selfdrive/iqmodeld/models/combined_artifact.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from iqpilot.system.hardware.hw import Paths
|
||||
|
||||
|
||||
_MODEL_ROOT = Path(Paths.model_root())
|
||||
_OVERRIDE_KEYS = (
|
||||
"combinedRuntimeArtifact",
|
||||
"combinedSplitArtifact",
|
||||
"iqCombinedArtifact",
|
||||
)
|
||||
_SPLIT_ROLE_PATTERN = re.compile(r"^driving_(vision|policy|off_policy|on_policy)_(.+)_tinygrad\.pkl$")
|
||||
|
||||
|
||||
def _bundle_models(bundle) -> list:
|
||||
models = getattr(bundle, "models", None)
|
||||
return list(models) if models is not None else []
|
||||
|
||||
|
||||
def _bundle_override_map(bundle) -> dict[str, str]:
|
||||
result: dict[str, str] = {}
|
||||
for override in getattr(bundle, "overrides", None) or []:
|
||||
key = getattr(override, "key", None)
|
||||
value = getattr(override, "value", None)
|
||||
if key and value:
|
||||
result[str(key)] = str(value)
|
||||
return result
|
||||
|
||||
|
||||
def _artifact_name(model) -> str:
|
||||
return getattr(getattr(model, "artifact", None), "fileName", "") or ""
|
||||
|
||||
|
||||
def _split_suffixes(bundle) -> list[str]:
|
||||
suffixes: list[str] = []
|
||||
for model in _bundle_models(bundle):
|
||||
match = _SPLIT_ROLE_PATTERN.match(_artifact_name(model))
|
||||
if match:
|
||||
suffixes.append(match.group(2))
|
||||
return suffixes
|
||||
|
||||
|
||||
def _derived_candidates(bundle) -> list[str]:
|
||||
seen: set[str] = set()
|
||||
candidates: list[str] = []
|
||||
|
||||
for suffix in _split_suffixes(bundle):
|
||||
for candidate in (
|
||||
f"driving_combined_{suffix}.pkl",
|
||||
f"iqmodeld_combined_{suffix}.pkl",
|
||||
):
|
||||
if candidate not in seen:
|
||||
seen.add(candidate)
|
||||
candidates.append(candidate)
|
||||
|
||||
ref = getattr(bundle, "ref", None)
|
||||
if ref:
|
||||
short_ref = str(ref)[:8]
|
||||
for candidate in (
|
||||
f"driving_combined_{short_ref}.pkl",
|
||||
f"iqmodeld_combined_{short_ref}.pkl",
|
||||
):
|
||||
if candidate not in seen:
|
||||
seen.add(candidate)
|
||||
candidates.append(candidate)
|
||||
|
||||
return candidates
|
||||
|
||||
|
||||
def combined_split_artifact_candidates(bundle) -> list[Path]:
|
||||
explicit_env = os.getenv("IQMODEL_COMBINED_PKL")
|
||||
if explicit_env:
|
||||
explicit_path = Path(explicit_env)
|
||||
return [explicit_path if explicit_path.is_absolute() else _MODEL_ROOT / explicit_path]
|
||||
|
||||
overrides = _bundle_override_map(bundle)
|
||||
explicit_names = [overrides[key] for key in _OVERRIDE_KEYS if key in overrides]
|
||||
if explicit_names:
|
||||
return [_MODEL_ROOT / name for name in explicit_names]
|
||||
|
||||
return [_MODEL_ROOT / name for name in _derived_candidates(bundle)]
|
||||
|
||||
|
||||
def resolve_combined_split_artifact(bundle) -> Path | None:
|
||||
for candidate in combined_split_artifact_candidates(bundle):
|
||||
if candidate.is_file():
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def has_combined_split_artifact(bundle) -> bool:
|
||||
return resolve_combined_split_artifact(bundle) is not None
|
||||
11
iqpilot/selfdrive/iqmodeld/models/fetcher.py
Normal file
11
iqpilot/selfdrive/iqmodeld/models/fetcher.py
Normal file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
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.fetcher")
|
||||
except ProprietaryModuleMissing:
|
||||
from iqpilot.models_private_src.fetcher import * # noqa: F403
|
||||
313
iqpilot/selfdrive/iqmodeld/models/helpers.py
Normal file
313
iqpilot/selfdrive/iqmodeld/models/helpers.py
Normal file
@@ -0,0 +1,313 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copyright (c) IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
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")
|
||||
except ProprietaryModuleMissing:
|
||||
try:
|
||||
from iqpilot.models_private_src.helpers import * # noqa: F403
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
ModelBundle = custom.IQModelManager.ModelBundle
|
||||
Runner = custom.IQModelManager.Runner
|
||||
_MODEL_ROOT = Path(Paths.model_root())
|
||||
_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)
|
||||
_SNPE_RUNNER = int(Runner.snpe)
|
||||
|
||||
_DEFAULT_MODEL_DIR = Path(__file__).resolve().parents[1] / "default_model"
|
||||
_DEFAULT_BUNDLE_JSON = _DEFAULT_MODEL_DIR / "bundle.json"
|
||||
_DEFAULT_BUNDLE_REF = "default"
|
||||
|
||||
|
||||
def get_default_model_bundle(_bundles):
|
||||
return None
|
||||
|
||||
|
||||
def _coerce_runner_value(value) -> int | None:
|
||||
raw = getattr(value, "raw", value)
|
||||
try:
|
||||
return int(raw)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _bundle_models(bundle) -> list:
|
||||
models = getattr(bundle, "models", None)
|
||||
return list(models) if models is not None else []
|
||||
|
||||
|
||||
def _bundle_needs_runtime_upgrade(bundle) -> bool:
|
||||
if bundle is None:
|
||||
return False
|
||||
|
||||
if _coerce_runner_value(getattr(bundle, "runner", None)) == _SNPE_RUNNER:
|
||||
return True
|
||||
|
||||
for model in _bundle_models(bundle):
|
||||
file_name = getattr(getattr(model, "artifact", None), "fileName", "") or ""
|
||||
if file_name.endswith(".thneed"):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _load_cached_manifest_bundles(params: Params):
|
||||
cached = params.get(_MODELS_CACHE_KEY) or {}
|
||||
bundles = []
|
||||
for raw_bundle in cached.get("bundles", []):
|
||||
try:
|
||||
min_selector_version = int(raw_bundle.get("minimumSelectorVersion", raw_bundle.get("minimum_selector_version", 0)))
|
||||
compatibility_view = dict(raw_bundle)
|
||||
compatibility_view["minimumSelectorVersion"] = min_selector_version
|
||||
is_compatible = globals().get("is_bundle_version_compatible")
|
||||
if is_compatible is not None and not is_compatible(compatibility_view):
|
||||
continue
|
||||
|
||||
if "short_name" in raw_bundle:
|
||||
from iqpilot.selfdrive.iqmodeld.models.fetcher import ManifestDecoder
|
||||
bundles.append(ManifestDecoder._decode_bundle(raw_bundle))
|
||||
continue
|
||||
|
||||
if "internalName" in raw_bundle:
|
||||
bundles.append(ModelBundle(**raw_bundle))
|
||||
continue
|
||||
|
||||
bundle = ModelBundle()
|
||||
bundle.index = int(raw_bundle["index"])
|
||||
bundle.internalName = raw_bundle.get("short_name")
|
||||
bundle.displayName = raw_bundle.get("display_name")
|
||||
bundle.status = 0
|
||||
bundle.generation = int(raw_bundle["generation"])
|
||||
bundle.environment = raw_bundle["environment"]
|
||||
bundle.runner = raw_bundle.get("runner", Runner.tinygrad)
|
||||
bundle.is20hz = raw_bundle.get("is_20hz", False)
|
||||
bundle.minimumSelectorVersion = int(min_selector_version)
|
||||
bundle.ref = raw_bundle.get("ref")
|
||||
bundle.overrides = []
|
||||
for key, value in raw_bundle.get("overrides", {}).items():
|
||||
override = custom.IQModelManager.Override()
|
||||
override.key = key
|
||||
override.value = value
|
||||
bundle.overrides.append(override)
|
||||
|
||||
bundle.models = []
|
||||
for raw_model in raw_bundle.get("models", []):
|
||||
model = custom.IQModelManager.Model()
|
||||
model.type = raw_model.get("type")
|
||||
for attr_name in ("artifact", "metadata"):
|
||||
raw_artifact = raw_model.get(attr_name)
|
||||
if not raw_artifact:
|
||||
continue
|
||||
artifact = custom.IQModelManager.Artifact()
|
||||
artifact.fileName = raw_artifact.get("file_name")
|
||||
download_uri = custom.IQModelManager.DownloadUri()
|
||||
download_uri.uri = raw_artifact.get("download_uri", {}).get("url")
|
||||
download_uri.sha256 = raw_artifact.get("download_uri", {}).get("sha256")
|
||||
artifact.downloadUri = download_uri
|
||||
setattr(model, attr_name, artifact)
|
||||
bundle.models.append(model)
|
||||
|
||||
bundles.append(bundle)
|
||||
except Exception:
|
||||
continue
|
||||
return bundles
|
||||
|
||||
|
||||
def _bundle_match_key(bundle) -> tuple[str | None, str | None, str | None]:
|
||||
return (
|
||||
getattr(bundle, "ref", None),
|
||||
getattr(bundle, "internalName", None),
|
||||
getattr(bundle, "displayName", None),
|
||||
)
|
||||
|
||||
|
||||
def _find_runtime_upgrade(bundle, params: Params, available_bundles=None):
|
||||
if not _bundle_needs_runtime_upgrade(bundle):
|
||||
return bundle
|
||||
|
||||
candidate_bundles = available_bundles if available_bundles is not None else _load_cached_manifest_bundles(params)
|
||||
ref, internal_name, display_name = _bundle_match_key(bundle)
|
||||
|
||||
for candidate in candidate_bundles:
|
||||
if getattr(candidate, "ref", None) and getattr(candidate, "ref", None) == ref:
|
||||
return candidate
|
||||
|
||||
for candidate in candidate_bundles:
|
||||
if getattr(candidate, "internalName", None) == internal_name:
|
||||
return candidate
|
||||
|
||||
for candidate in candidate_bundles:
|
||||
if getattr(candidate, "displayName", None) == display_name:
|
||||
return candidate
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def bundle_files_ready(bundle) -> bool:
|
||||
if bundle is None:
|
||||
return False
|
||||
|
||||
for model in _bundle_models(bundle):
|
||||
artifact = getattr(model, "artifact", None)
|
||||
metadata = getattr(model, "metadata", None)
|
||||
for file_name in (getattr(metadata, "fileName", None), getattr(artifact, "fileName", None)):
|
||||
if file_name and not (_MODEL_ROOT / file_name).is_file():
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def persist_active_bundle(params: Params, bundle) -> None:
|
||||
params.put(_ACTIVE_BUNDLE_KEY, bundle.to_dict())
|
||||
params.remove(_RUNNER_CACHE_KEY)
|
||||
|
||||
|
||||
def _load_default_bundle_dict() -> dict:
|
||||
return json.loads(_DEFAULT_BUNDLE_JSON.read_text())
|
||||
|
||||
|
||||
def _default_bundle_filenames(bundle_dict: dict) -> list[str]:
|
||||
names = []
|
||||
for model in bundle_dict.get("models", []):
|
||||
for artifact in (model.get("metadata"), model.get("artifact")):
|
||||
file_name = artifact.get("fileName", "") if isinstance(artifact, dict) else ""
|
||||
if file_name:
|
||||
names.append(file_name)
|
||||
return names
|
||||
|
||||
|
||||
def is_default_bundle(bundle) -> bool:
|
||||
return bool(bundle is not None and getattr(bundle, "ref", None) == _DEFAULT_BUNDLE_REF)
|
||||
|
||||
|
||||
def ensure_default_model_files(bundle_dict: dict = None) -> None:
|
||||
bundle_dict = bundle_dict if bundle_dict is not None else _load_default_bundle_dict()
|
||||
try:
|
||||
_MODEL_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
except OSError as e:
|
||||
cloudlog.exception(f"default_model: cannot create model root: {e}")
|
||||
return
|
||||
for file_name in _default_bundle_filenames(bundle_dict):
|
||||
src = _DEFAULT_MODEL_DIR / file_name
|
||||
dst = _MODEL_ROOT / file_name
|
||||
if not src.is_file():
|
||||
cloudlog.error(f"default_model: shipped asset missing {src}")
|
||||
continue
|
||||
if dst.is_file() and dst.stat().st_size == src.stat().st_size:
|
||||
continue
|
||||
try:
|
||||
shutil.copy2(src, dst)
|
||||
cloudlog.warning(f"default_model: staged {file_name} into model root")
|
||||
except OSError as e:
|
||||
cloudlog.exception(f"default_model: failed staging {file_name}: {e}")
|
||||
|
||||
|
||||
def select_default_model(params: Params = None) -> None:
|
||||
params = Params() if params is None else params
|
||||
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)
|
||||
try:
|
||||
if os.path.isfile(_PENDING_MODEL_RESTORE_FILE):
|
||||
os.remove(_PENDING_MODEL_RESTORE_FILE)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def seed_default_bundle_if_unset(params: Params = None) -> None:
|
||||
params = Params() if params is None else params
|
||||
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}")
|
||||
|
||||
|
||||
def get_runtime_bundle_upgrade(bundle, params: Params = None, available_bundles=None):
|
||||
params = Params() if params is None else params
|
||||
return _find_runtime_upgrade(bundle, params, available_bundles)
|
||||
|
||||
|
||||
def get_active_bundle(params: Params = None):
|
||||
params = Params() if params is None else params
|
||||
|
||||
try:
|
||||
active_bundle = params.get(_ACTIVE_BUNDLE_KEY) or {}
|
||||
if not active_bundle:
|
||||
return None
|
||||
is_compatible = globals().get("is_bundle_version_compatible")
|
||||
if is_compatible is not None and not is_compatible(active_bundle):
|
||||
return None
|
||||
bundle = ModelBundle(**active_bundle)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
replacement = _find_runtime_upgrade(bundle, params)
|
||||
if replacement is not None and replacement is not bundle and bundle_files_ready(replacement):
|
||||
persist_active_bundle(params, replacement)
|
||||
return replacement
|
||||
|
||||
return bundle
|
||||
|
||||
|
||||
def get_active_model_runner(params: Params = None, force_check=False):
|
||||
params = Params() if params is None else params
|
||||
|
||||
active_bundle = get_active_bundle(params)
|
||||
if not active_bundle:
|
||||
seed_default_bundle_if_unset(params)
|
||||
active_bundle = get_active_bundle(params)
|
||||
if not active_bundle:
|
||||
if params.get(_RUNNER_CACHE_KEY) != str(_TINYGRAD_RUNNER):
|
||||
params.put(_RUNNER_CACHE_KEY, _TINYGRAD_RUNNER)
|
||||
return _TINYGRAD_RUNNER
|
||||
|
||||
cached_runner_type = params.get(_RUNNER_CACHE_KEY)
|
||||
if cached_runner_type and not force_check and isinstance(cached_runner_type, str) and cached_runner_type.isdigit():
|
||||
return int(cached_runner_type)
|
||||
|
||||
runner_type = _coerce_runner_value(active_bundle.runner)
|
||||
if runner_type == _SNPE_RUNNER:
|
||||
replacement = _find_runtime_upgrade(active_bundle, params)
|
||||
if replacement is not None and replacement is not active_bundle and bundle_files_ready(replacement):
|
||||
persist_active_bundle(params, replacement)
|
||||
runner_type = _coerce_runner_value(replacement.runner)
|
||||
else:
|
||||
if replacement is not None and getattr(replacement, "index", None) is not None and params.get(_DOWNLOAD_INDEX_KEY) is None:
|
||||
params.put(_DOWNLOAD_INDEX_KEY, int(replacement.index))
|
||||
cloudlog.warning(f"Queued tinygrad migration for retired bundle {getattr(active_bundle, 'internalName', '<unknown>')}")
|
||||
runner_type = _TINYGRAD_RUNNER
|
||||
|
||||
if cached_runner_type != runner_type:
|
||||
params.put(_RUNNER_CACHE_KEY, int(runner_type))
|
||||
|
||||
return runner_type
|
||||
9
iqpilot/selfdrive/iqmodeld/models/inference_state.py
Normal file
9
iqpilot/selfdrive/iqmodeld/models/inference_state.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.common.steer_delay import cached_steer_delay
|
||||
|
||||
|
||||
class InferenceStateBase:
|
||||
def __init__(self):
|
||||
self.lat_delay = cached_steer_delay()
|
||||
3
iqpilot/selfdrive/iqmodeld/models/runners/__init__.py
Normal file
3
iqpilot/selfdrive/iqmodeld/models/runners/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
231
iqpilot/selfdrive/iqmodeld/models/runners/model_runner.py
Normal file
231
iqpilot/selfdrive/iqmodeld/models/runners/model_runner.py
Normal file
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
import os
|
||||
import pickle as _pk
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
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 iqpilot.selfdrive.iqmodeld.native.iqmodel_pyx import GpuMemorySlot, RoadProjector
|
||||
else:
|
||||
def _resolve_native_types() -> tuple[Any, Any]:
|
||||
try:
|
||||
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
|
||||
|
||||
GpuMemorySlot, RoadProjector = _resolve_native_types()
|
||||
|
||||
NumpyDict = dict[str, np.ndarray]
|
||||
ShapeDict = dict[str, tuple[int, ...]]
|
||||
SliceDict = dict[str, slice]
|
||||
CLMemDict = dict[str, GpuMemorySlot]
|
||||
FrameDict = dict[str, RoadProjector]
|
||||
|
||||
ModelType = custom.IQModelManager.Model.Type
|
||||
Model = custom.IQModelManager.Model
|
||||
|
||||
SEND_RAW_PRED = os.getenv("SEND_RAW_PRED")
|
||||
CUSTOM_MODEL_PATH = _hw_paths.model_root()
|
||||
|
||||
_META_FIELDS = ("input_shapes", "output_slices")
|
||||
|
||||
USBGPU = "USBGPU" in os.environ
|
||||
|
||||
|
||||
def _configure_accelerator():
|
||||
"""Point tinygrad at the right backend. Must run before tinygrad is imported,
|
||||
which is why it fires at module import."""
|
||||
backend, extra = ("QCOM" if TICI else "CPU"), {}
|
||||
if USBGPU:
|
||||
backend, extra = "AMD", {"AMD_IFACE": "USB"}
|
||||
elif TICI:
|
||||
extra = {"QCOM_PRIORITY": "8"}
|
||||
os.environ["DEV"] = backend
|
||||
os.environ.update(extra)
|
||||
|
||||
|
||||
_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)."""
|
||||
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
|
||||
class ArtifactSpec:
|
||||
"""One model of the active bundle plus its unpacked metadata."""
|
||||
model: Any
|
||||
metadata: Any = None
|
||||
input_shapes: ShapeDict = field(default_factory=dict)
|
||||
output_slices: SliceDict = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self):
|
||||
self.metadata = self.model.metadata
|
||||
if self.metadata:
|
||||
self.input_shapes, self.output_slices = load_artifact_metadata(self.metadata.fileName)
|
||||
|
||||
|
||||
# kept name: some runners annotate against the old alias
|
||||
ModelData = ArtifactSpec
|
||||
|
||||
|
||||
class RunnerRoot:
|
||||
"""Shared root of the runner hierarchy.
|
||||
|
||||
Both ModelRunner and the per-model parser mixins (model_types.py) inherit
|
||||
this, so the concrete `TinygradRunner(ModelRunner, *Tinygrad)` diamond keeps
|
||||
one consistent parser registry + slice implementation.
|
||||
"""
|
||||
|
||||
parser_method_dict: dict
|
||||
_model_data: "ArtifactSpec | None"
|
||||
|
||||
def _slice_outputs(self, model_outputs):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class ModelRunner(RunnerRoot):
|
||||
"""Base for the tinygrad/ONNX runners.
|
||||
|
||||
Owns the active bundle's ArtifactSpecs and the shared slice/parse plumbing;
|
||||
subclasses provide input staging (prepare_inputs) and execution (_run_model).
|
||||
"""
|
||||
|
||||
# False for fused runners, which warp + manage temporal buffers inside the JIT
|
||||
uses_opencl_warp = True
|
||||
|
||||
def __init__(self):
|
||||
active = _fetch_bundle()
|
||||
if not active:
|
||||
raise ValueError("runner started without an active model bundle")
|
||||
|
||||
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 = {}
|
||||
self.parser_method_dict = {}
|
||||
self._model_data = None # active spec for the current operation
|
||||
self._parser = self._constants = None
|
||||
|
||||
def _active_spec(self):
|
||||
spec = self._model_data
|
||||
if spec is None:
|
||||
raise ValueError("Model data is not available. Ensure the model is loaded correctly.")
|
||||
return spec
|
||||
|
||||
# views proxied straight off the active artifact spec; kept out of the class
|
||||
# body (served via __getattr__) so the read surface stays data-driven
|
||||
_SPEC_VIEW = frozenset(("input_shapes", "output_slices"))
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name == "constants":
|
||||
return self._constants
|
||||
if name == "vision_input_names":
|
||||
return list(self._active_spec().input_shapes)
|
||||
if name in ModelRunner._SPEC_VIEW:
|
||||
return getattr(self._active_spec(), name)
|
||||
raise AttributeError(name)
|
||||
|
||||
def prepare_inputs(self, imgs_cl, numpy_inputs, frames):
|
||||
"""Stage image + numpy inputs for inference; implemented per backend."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _run_model(self):
|
||||
"""Execute inference over the staged inputs; implemented per backend."""
|
||||
raise NotImplementedError
|
||||
|
||||
def run_model(self):
|
||||
# parsing happens inside each backend's _run_model
|
||||
return self._run_model()
|
||||
|
||||
def _slice_outputs(self, model_outputs):
|
||||
"""Split the flat output vector into named views per the artifact's slice table."""
|
||||
sliced = {}
|
||||
for tag, span in self._active_spec().output_slices.items():
|
||||
sliced[tag] = model_outputs[np.newaxis, span]
|
||||
if SEND_RAW_PRED:
|
||||
sliced["raw_pred"] = model_outputs.copy()
|
||||
return sliced
|
||||
|
||||
|
||||
# ---- 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:
|
||||
models = _qcom_models(bundle)
|
||||
return len(models) == 1 and models[0].artifact.fileName.startswith(prefix)
|
||||
|
||||
|
||||
def _is_fused_bundle(bundle) -> bool:
|
||||
return _single_artifact_prefix(bundle, "driving_fused_")
|
||||
|
||||
|
||||
def _is_supercombo_bundle(bundle) -> bool:
|
||||
return _single_artifact_prefix(bundle, "driving_supercombo_")
|
||||
|
||||
|
||||
def _is_split_bundle(bundle) -> bool:
|
||||
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)
|
||||
|
||||
|
||||
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 iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.tinygrad_runner import (TinygradRunner,
|
||||
TinygradSplitRunner)
|
||||
bundle = _fetch_bundle()
|
||||
# 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 iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.supercombo_runner import TinygradSupercomboRunner
|
||||
return TinygradSupercomboRunner()
|
||||
if _is_fused_bundle(bundle):
|
||||
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 iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.combined_split_runner import TinygradCombinedSplitRunner
|
||||
return TinygradCombinedSplitRunner()
|
||||
if _is_split_bundle(bundle):
|
||||
return TinygradSplitRunner()
|
||||
return TinygradRunner(_qcom_models(bundle)[0].type.raw)
|
||||
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
@@ -0,0 +1,245 @@
|
||||
"""
|
||||
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
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
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():
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.tensor import Tensor
|
||||
return Tensor, Device
|
||||
|
||||
|
||||
def _phase_roles(meta_by_role: dict[str, dict]) -> list[str]:
|
||||
return [name for name in meta_by_role if name != "vision"]
|
||||
|
||||
|
||||
def _phase_desire_key(policy_shapes: dict[str, tuple[int, ...]]) -> str:
|
||||
for key in policy_shapes:
|
||||
if key.startswith("desire"):
|
||||
return key
|
||||
raise KeyError("No desire-like key found in policy inputs")
|
||||
|
||||
|
||||
def _phase_image_keys(vision_shapes: dict[str, tuple[int, ...]]) -> tuple[str, str]:
|
||||
names = sorted(name for name in vision_shapes if "img" in name)
|
||||
road_key = next((name for name in names if "big" not in name), None)
|
||||
wide_key = next((name for name in names if "big" in name), None)
|
||||
if road_key is None or wide_key is None:
|
||||
raise ValueError(f"Unable to resolve road/wide image keys from {list(vision_shapes)}")
|
||||
return road_key, wide_key
|
||||
|
||||
|
||||
def _base_policy_keys(policy_shapes: dict[str, tuple[int, ...]]) -> set[str]:
|
||||
desired_key = _phase_desire_key(policy_shapes)
|
||||
return {desired_key, "features_buffer", "traffic_convention", "action_t"}
|
||||
|
||||
|
||||
def _slice_map(raw_blob: np.ndarray, slices: dict[str, slice]) -> NumpyDict:
|
||||
return {name: raw_blob[np.newaxis, section] for name, section in slices.items() if name != "pad"}
|
||||
|
||||
|
||||
class TinygradCombinedSplitRunner(ModelRunner):
|
||||
uses_opencl_warp: bool = False
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._constants = SplitModelConstants
|
||||
self._parser = PhaseParser()
|
||||
self._bundle = get_active_bundle()
|
||||
self._artifact_path = resolve_combined_split_artifact(self._bundle)
|
||||
if self._artifact_path is None:
|
||||
raise FileNotFoundError("No IQ combined split artifact is available for the active bundle")
|
||||
|
||||
with open(self._artifact_path, "rb") as artifact:
|
||||
runtime_package: dict[Any, Any] = pickle.load(artifact)
|
||||
|
||||
self._meta_by_role = runtime_package.get("meta_by_role", runtime_package.get("metadata", {}))
|
||||
self._policy_roles = runtime_package.get("roles", _phase_roles(self._meta_by_role))
|
||||
self._camera_programs = {
|
||||
camera_key: spec
|
||||
for camera_key, spec in runtime_package.items()
|
||||
if isinstance(camera_key, tuple) and isinstance(spec, dict)
|
||||
}
|
||||
self._execute_bundle = runtime_package.get("execute_bundle", runtime_package.get("run_policy"))
|
||||
self._frame_stride = int(runtime_package.get("frame_stride", runtime_package.get("frame_skip", 1)))
|
||||
|
||||
if "vision" not in self._meta_by_role:
|
||||
raise ValueError("Combined split artifact is missing vision metadata")
|
||||
if not self._policy_roles:
|
||||
raise ValueError("Combined split artifact is missing policy roles")
|
||||
if self._execute_bundle is None:
|
||||
raise ValueError("Combined split artifact is missing execute_bundle")
|
||||
|
||||
self._vision_meta = self._meta_by_role["vision"]
|
||||
self._primary_policy_meta = self._meta_by_role[self._policy_roles[0]]
|
||||
self._desired_key = _phase_desire_key(self._primary_policy_meta["input_shapes"])
|
||||
self._road_key, self._wide_key = _phase_image_keys(self._vision_meta["input_shapes"])
|
||||
self._extra_policy_keys = [
|
||||
key for key in self._primary_policy_meta["input_shapes"]
|
||||
if key not in _base_policy_keys(self._primary_policy_meta["input_shapes"])
|
||||
]
|
||||
|
||||
self._queue_tensors: dict[str, Any] | None = None
|
||||
self._numpy_state: dict[str, np.ndarray] | None = None
|
||||
self._camera_shape: tuple[int, int] | None = None
|
||||
self._blob_cache: dict[tuple[str, int], Any] = {}
|
||||
self._last_desire = np.zeros(self._primary_policy_meta["input_shapes"][self._desired_key][2], dtype=np.float32)
|
||||
|
||||
@property
|
||||
def vision_input_names(self) -> list[str]:
|
||||
return [self._road_key, self._wide_key]
|
||||
|
||||
@property
|
||||
def input_shapes(self) -> ShapeDict:
|
||||
merged: ShapeDict = dict(self._vision_meta["input_shapes"])
|
||||
for role in self._policy_roles:
|
||||
merged.update(self._meta_by_role[role]["input_shapes"])
|
||||
return merged
|
||||
|
||||
@property
|
||||
def output_slices(self) -> SliceDict:
|
||||
merged: SliceDict = dict(self._vision_meta["output_slices"])
|
||||
for role in self._policy_roles:
|
||||
merged.update(self._meta_by_role[role]["output_slices"])
|
||||
return merged
|
||||
|
||||
def prepare_inputs(self, imgs_cl, numpy_inputs, frames):
|
||||
raise RuntimeError("Combined split runner manages its own warp + queue state; use run_fused()")
|
||||
|
||||
def _frame_blob(self, stream_name: str, buf):
|
||||
Tensor, Device = _tinygrad_imports()
|
||||
raw_frame = np.frombuffer(buf.data, dtype=np.uint8)
|
||||
cache_key = (stream_name, raw_frame.ctypes.data)
|
||||
tensor = self._blob_cache.get(cache_key)
|
||||
if tensor is None:
|
||||
tensor = Tensor.from_blob(raw_frame.ctypes.data, (raw_frame.size,), dtype="uint8", device=Device.DEFAULT)
|
||||
self._blob_cache[cache_key] = tensor
|
||||
return tensor
|
||||
|
||||
def _allocate_runtime_state(self, camera_width: int, camera_height: int) -> None:
|
||||
if self._queue_tensors is not None and self._camera_shape == (camera_width, camera_height):
|
||||
return
|
||||
if (camera_width, camera_height) not in self._camera_programs:
|
||||
raise RuntimeError(f"No combined split kernels available for {camera_width}x{camera_height}")
|
||||
|
||||
Tensor, Device = _tinygrad_imports()
|
||||
vision_shapes = self._vision_meta["input_shapes"]
|
||||
policy_shapes = self._primary_policy_meta["input_shapes"]
|
||||
|
||||
image_shape = vision_shapes[self._road_key]
|
||||
frame_history = image_shape[1] // 6
|
||||
queue_depth = self._frame_stride * (frame_history - 1) + 1
|
||||
frame_queue_shape = (queue_depth, 6, image_shape[2], image_shape[3])
|
||||
|
||||
feature_shape = policy_shapes["features_buffer"]
|
||||
desired_shape = policy_shapes[self._desired_key]
|
||||
traffic_shape = policy_shapes["traffic_convention"]
|
||||
action_shape = policy_shapes.get("action_t", traffic_shape)
|
||||
|
||||
numpy_state = {
|
||||
"tfm": np.zeros((3, 3), dtype=np.float32),
|
||||
"big_tfm": np.zeros((3, 3), dtype=np.float32),
|
||||
"desire": np.zeros(desired_shape[2], dtype=np.float32),
|
||||
"traffic_convention": np.zeros(traffic_shape, dtype=np.float32),
|
||||
"action_t": np.zeros(action_shape, dtype=np.float32),
|
||||
}
|
||||
for key in self._extra_policy_keys:
|
||||
numpy_state[key] = np.zeros(policy_shapes[key], dtype=np.float32)
|
||||
|
||||
queue_tensors = {
|
||||
"img_q": Tensor(np.zeros(frame_queue_shape, dtype=np.uint8), device=Device.DEFAULT).contiguous().realize(),
|
||||
"big_img_q": Tensor(np.zeros(frame_queue_shape, dtype=np.uint8), device=Device.DEFAULT).contiguous().realize(),
|
||||
"feat_q": Tensor(
|
||||
np.zeros((self._frame_stride * (feature_shape[1] - 1) + 1, feature_shape[0], feature_shape[2]), dtype=np.float32),
|
||||
device=Device.DEFAULT,
|
||||
).contiguous().realize(),
|
||||
"desire_q": Tensor(
|
||||
np.zeros((self._frame_stride * desired_shape[1], desired_shape[0], desired_shape[2]), dtype=np.float32),
|
||||
device=Device.DEFAULT,
|
||||
).contiguous().realize(),
|
||||
**{name: Tensor(value, device="NPY").realize() for name, value in numpy_state.items()},
|
||||
}
|
||||
|
||||
self._queue_tensors = queue_tensors
|
||||
self._numpy_state = numpy_state
|
||||
self._camera_shape = (camera_width, camera_height)
|
||||
|
||||
def _policy_inputs(self) -> dict[str, Any]:
|
||||
assert self._queue_tensors is not None
|
||||
tensor_names = ["feat_q", "desire_q", "desire", "traffic_convention", "action_t", *self._extra_policy_keys]
|
||||
return {name: self._queue_tensors[name] for name in tensor_names if name in self._queue_tensors}
|
||||
|
||||
def _merge_policy_outputs(self, raw_outputs: tuple[Any, ...]) -> NumpyDict:
|
||||
outputs = self._parser.parse_vision_outputs(
|
||||
_slice_map(raw_outputs[0].numpy().flatten(), self._vision_meta["output_slices"])
|
||||
)
|
||||
|
||||
has_on_policy = any(role == "on_policy" for role in self._policy_roles)
|
||||
for role_name, tensor_out in zip(self._policy_roles, raw_outputs[1:], strict=True):
|
||||
parsed = self._parser.parse_policy_outputs(
|
||||
_slice_map(tensor_out.numpy().flatten(), self._meta_by_role[role_name]["output_slices"])
|
||||
)
|
||||
if role_name == "off_policy" and has_on_policy:
|
||||
parsed.pop("plan", None)
|
||||
outputs.update(parsed)
|
||||
|
||||
if "planplus" in outputs and "plan" in outputs:
|
||||
outputs["plan"] = outputs["plan"] + outputs["planplus"]
|
||||
return outputs
|
||||
|
||||
def run_fused(self, bufs: dict, transforms: dict[str, np.ndarray], numpy_inputs: NumpyDict) -> NumpyDict:
|
||||
main_buf = bufs[self._road_key]
|
||||
self._allocate_runtime_state(main_buf.width, main_buf.height)
|
||||
assert self._queue_tensors is not None and self._numpy_state is not None and self._camera_shape is not None
|
||||
|
||||
self._numpy_state["tfm"][:] = transforms[self._road_key]
|
||||
self._numpy_state["big_tfm"][:] = transforms[self._wide_key]
|
||||
|
||||
current_desire = numpy_inputs[self._desired_key].copy()
|
||||
current_desire[0] = 0
|
||||
self._numpy_state["desire"][:] = np.where(current_desire - self._last_desire > 0.99, current_desire, 0)
|
||||
self._last_desire[:] = current_desire
|
||||
|
||||
if "traffic_convention" in numpy_inputs:
|
||||
self._numpy_state["traffic_convention"][:] = numpy_inputs["traffic_convention"]
|
||||
if "action_t" in numpy_inputs:
|
||||
self._numpy_state["action_t"][:] = numpy_inputs["action_t"]
|
||||
for key in self._extra_policy_keys:
|
||||
if key in numpy_inputs:
|
||||
self._numpy_state[key][:] = numpy_inputs[key]
|
||||
|
||||
stage_inputs = self._camera_programs[self._camera_shape].get("stage_inputs", self._camera_programs[self._camera_shape].get("warp_enqueue"))
|
||||
if stage_inputs is None:
|
||||
raise RuntimeError("Combined split artifact camera entry is missing stage_inputs")
|
||||
|
||||
staged_main, staged_wide = stage_inputs(
|
||||
img_q=self._queue_tensors["img_q"],
|
||||
big_img_q=self._queue_tensors["big_img_q"],
|
||||
tfm=self._queue_tensors["tfm"],
|
||||
big_tfm=self._queue_tensors["big_tfm"],
|
||||
frame=self._frame_blob(self._road_key, bufs[self._road_key]),
|
||||
big_frame=self._frame_blob(self._wide_key, bufs[self._wide_key]),
|
||||
)
|
||||
raw_outputs = self._execute_bundle(img=staged_main, big_img=staged_wide, **self._policy_inputs())
|
||||
if not isinstance(raw_outputs, tuple):
|
||||
raw_outputs = (raw_outputs,)
|
||||
return self._merge_policy_outputs(raw_outputs)
|
||||
|
||||
def _run_model(self) -> NumpyDict:
|
||||
raise RuntimeError("Combined split runner executes through run_fused()")
|
||||
@@ -0,0 +1,178 @@
|
||||
"""
|
||||
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
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
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():
|
||||
from tinygrad.tensor import Tensor
|
||||
from tinygrad.device import Device
|
||||
return Tensor, Device
|
||||
|
||||
|
||||
WARP_DEV = os.getenv('WARP_DEV')
|
||||
|
||||
|
||||
class TinygradFusedRunner(ModelRunner):
|
||||
uses_opencl_warp: bool = False
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._constants = SplitModelConstants
|
||||
self._parser = PhaseParser()
|
||||
|
||||
if len(self.models) != 1:
|
||||
raise ValueError(f"fused bundle must have exactly one artifact, got {list(self.models)}")
|
||||
self._model_data = next(iter(self.models.values()))
|
||||
|
||||
pkl_path = os.path.join(CUSTOM_MODEL_PATH, self._model_data.model.artifact.fileName)
|
||||
with open(pkl_path, 'rb') as f:
|
||||
self._fused: dict[Any, Any] = pickle.load(f)
|
||||
|
||||
self._vision_meta = self._fused['metadata']['vision']
|
||||
self._on_meta = self._fused['metadata']['on_policy']
|
||||
self._off_meta = self._fused['metadata']['off_policy']
|
||||
self._run_policy = self._fused['run_policy']
|
||||
self._warp_jits: dict[tuple[int, int], Any] = {k: v for k, v in self._fused.items() if isinstance(k, tuple)}
|
||||
if not self._warp_jits:
|
||||
raise ValueError("fused pkl has no warp JITs")
|
||||
|
||||
self._frame_skip: int = int(self._fused.get('frame_skip', 4))
|
||||
|
||||
self._queues: dict[str, Any] | None = None
|
||||
self._npy_buffers: dict[str, np.ndarray] | None = None
|
||||
self._cam_resolution: tuple[int, int] | None = None
|
||||
self._blob_cache: dict[tuple[str, int], Any] = {}
|
||||
|
||||
def _frame_tensor(self, key, buf):
|
||||
Tensor, Device = _tinygrad_imports()
|
||||
arr = np.frombuffer(buf.data, dtype=np.uint8)
|
||||
ck = (key, arr.ctypes.data)
|
||||
t = self._blob_cache.get(ck)
|
||||
if t is None:
|
||||
t = Tensor.from_blob(arr.ctypes.data, (arr.size,), dtype='uint8', device=Device.DEFAULT)
|
||||
self._blob_cache[ck] = t
|
||||
return t
|
||||
|
||||
@property
|
||||
def vision_input_names(self) -> list[str]:
|
||||
return ['img', 'big_img']
|
||||
|
||||
@property
|
||||
def input_shapes(self) -> ShapeDict:
|
||||
return {**self._vision_meta['input_shapes'], **self._on_meta['input_shapes']}
|
||||
|
||||
@property
|
||||
def output_slices(self) -> SliceDict:
|
||||
merged: SliceDict = {}
|
||||
for src in (self._vision_meta['output_slices'], self._on_meta['output_slices'], self._off_meta['output_slices']):
|
||||
merged.update({k: v for k, v in src.items() if k != 'pad'})
|
||||
return merged
|
||||
|
||||
def prepare_inputs(self, imgs_cl, numpy_inputs, frames):
|
||||
raise RuntimeError("fused runner has no OpenCL path; use run_fused()")
|
||||
|
||||
def _ensure_queues(self, cam_w: int, cam_h: int) -> None:
|
||||
if self._queues is not None and self._cam_resolution == (cam_w, cam_h):
|
||||
return
|
||||
if (cam_w, cam_h) not in self._warp_jits:
|
||||
raise RuntimeError(f"no warp JIT for {cam_w}x{cam_h}; have {sorted(self._warp_jits)}")
|
||||
|
||||
Tensor, Device = _tinygrad_imports()
|
||||
img_shape = self._vision_meta['input_shapes']['img']
|
||||
fb = self._on_meta['input_shapes']['features_buffer']
|
||||
dp = self._on_meta['input_shapes']['desire_pulse']
|
||||
n_frames = img_shape[1] // 6
|
||||
img_buf_shape = (self._frame_skip * (n_frames - 1) + 1, 6, img_shape[2], img_shape[3])
|
||||
|
||||
zeros_u8 = lambda shp: Tensor(np.zeros(shp, dtype=np.uint8), device=Device.DEFAULT).contiguous().realize()
|
||||
zeros_f32 = lambda shp: Tensor(np.zeros(shp, dtype=np.float32), device=Device.DEFAULT).contiguous().realize()
|
||||
|
||||
self._queues = {
|
||||
'img_q': zeros_u8(img_buf_shape),
|
||||
'big_img_q': zeros_u8(img_buf_shape),
|
||||
'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])),
|
||||
}
|
||||
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(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:
|
||||
Tensor, Device = _tinygrad_imports()
|
||||
|
||||
main_buf = bufs['img']
|
||||
self._ensure_queues(main_buf.width, main_buf.height)
|
||||
assert self._queues is not None and self._npy_buffers is not None
|
||||
|
||||
desire_key = next((k for k in numpy_inputs if k.startswith('desire')), None)
|
||||
if desire_key is not None:
|
||||
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 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')
|
||||
|
||||
frame = self._frame_tensor('img', bufs['img'])
|
||||
big_frame = self._frame_tensor('big_img', bufs['big_img'])
|
||||
|
||||
warp_jit = self._warp_jits[self._cam_resolution]
|
||||
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)
|
||||
|
||||
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'))
|
||||
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)
|
||||
|
||||
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'}
|
||||
|
||||
parsed: NumpyDict = {}
|
||||
parsed.update(self._parser.parse_vision_outputs(_slice(vision_out_t, self._vision_meta)))
|
||||
parsed.update(self._parser.parse_policy_outputs(_slice(off_out_t, self._off_meta)))
|
||||
parsed.update(self._parser.parse_policy_outputs(_slice(on_out_t, self._on_meta)))
|
||||
return parsed
|
||||
|
||||
def _run_model(self) -> NumpyDict:
|
||||
raise RuntimeError("fused path goes through run_fused(), not _run_model()")
|
||||
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC
|
||||
from collections.abc import Callable
|
||||
|
||||
import numpy as np
|
||||
|
||||
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):
|
||||
def _bind_parser_role(self,
|
||||
selector: int,
|
||||
parser_builder: Callable[[], object],
|
||||
projector: Callable[[object, NumpyDict], NumpyDict]) -> None:
|
||||
parser = parser_builder()
|
||||
self.parser_method_dict[selector] = lambda model_blob: projector(parser, self._slice_outputs(model_blob))
|
||||
|
||||
|
||||
def _phase_policy(parser: PhaseParser, sliced_outputs: NumpyDict) -> NumpyDict:
|
||||
return parser.parse_policy_outputs(sliced_outputs)
|
||||
|
||||
|
||||
def _phase_vision(parser: PhaseParser, sliced_outputs: NumpyDict) -> NumpyDict:
|
||||
return parser.parse_vision_outputs(sliced_outputs)
|
||||
|
||||
|
||||
def _archive_combined(parser: ArchiveParser, sliced_outputs: NumpyDict) -> NumpyDict:
|
||||
return parser.parse_outputs(sliced_outputs)
|
||||
|
||||
|
||||
class OffPolicyTinygrad(_ParserRole, ABC):
|
||||
def __init__(self):
|
||||
self._bind_parser_role(ModelType.offPolicy, PhaseParser, _phase_policy)
|
||||
|
||||
|
||||
class OnPolicyTinygrad(_ParserRole, ABC):
|
||||
def __init__(self):
|
||||
self._bind_parser_role(ModelType.onPolicy, PhaseParser, _phase_policy)
|
||||
|
||||
|
||||
class PolicyTinygrad(_ParserRole, ABC):
|
||||
def __init__(self):
|
||||
self._bind_parser_role(ModelType.policy, PhaseParser, _phase_policy)
|
||||
|
||||
|
||||
class VisionTinygrad(_ParserRole, ABC):
|
||||
def __init__(self):
|
||||
self._bind_parser_role(ModelType.vision, PhaseParser, _phase_vision)
|
||||
|
||||
|
||||
class SupercomboTinygrad(_ParserRole, ABC):
|
||||
def __init__(self):
|
||||
self._bind_parser_role(ModelType.supercombo, ArchiveParser, _archive_combined)
|
||||
@@ -0,0 +1,339 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import math
|
||||
import os
|
||||
import pickle
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
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():
|
||||
from tinygrad.tensor import Tensor
|
||||
from tinygrad.device import Device
|
||||
return Tensor, Device
|
||||
|
||||
|
||||
def _captured_queue_depth(warp_jit: Any) -> int | None:
|
||||
captured = getattr(warp_jit, "captured", None)
|
||||
infos = getattr(captured, "expected_input_info", None)
|
||||
if not infos or len(infos) < 2:
|
||||
return None
|
||||
|
||||
view_repr = repr(infos[1][0])
|
||||
dims = [int(val) for val in re.findall(r"arg=(\d+)", view_repr)]
|
||||
return dims[0] if len(dims) >= 4 else None
|
||||
|
||||
|
||||
def _captured_devices(warp_jit: Any) -> set[str]:
|
||||
captured = getattr(warp_jit, "captured", None)
|
||||
infos = getattr(captured, "expected_input_info", None)
|
||||
if not infos:
|
||||
return set()
|
||||
|
||||
devices: set[str] = set()
|
||||
for info in infos:
|
||||
if isinstance(info, tuple) and len(info) >= 4 and isinstance(info[3], str):
|
||||
devices.add(info[3])
|
||||
return devices
|
||||
|
||||
|
||||
def _captured_expected_names(jit_obj: Any) -> list[str]:
|
||||
captured = getattr(jit_obj, "captured", None)
|
||||
names = getattr(captured, "expected_names", None)
|
||||
return list(names) if names else []
|
||||
|
||||
|
||||
def _file_sha256(path: str) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with open(path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _is_jit_arg_mismatch(err: BaseException) -> bool:
|
||||
return "args mismatch in JIT" in str(err)
|
||||
|
||||
|
||||
class TinygradSupercomboRunner(ModelRunner):
|
||||
uses_opencl_warp: bool = False
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._constants = SplitModelConstants
|
||||
self._parser = PhaseParser()
|
||||
|
||||
if len(self.models) != 1:
|
||||
raise ValueError(f"supercombo bundle must have exactly one artifact, got {list(self.models)}")
|
||||
self._model_data = next(iter(self.models.values()))
|
||||
|
||||
pkl_path = os.path.join(CUSTOM_MODEL_PATH, self._model_data.model.artifact.fileName)
|
||||
self._pkl_path = pkl_path
|
||||
self._expected_sha256 = getattr(getattr(self._model_data.model.artifact, "downloadUri", None), "sha256", "") or ""
|
||||
self._verify_artifact_file()
|
||||
with open(pkl_path, 'rb') as f:
|
||||
self._m: dict[Any, Any] = pickle.load(f)
|
||||
|
||||
self._meta = self._m['metadata']
|
||||
self._ish = self._meta['input_shapes']
|
||||
self._slices = {k: v for k, v in self._meta['output_slices'].items() if k != 'pad'}
|
||||
self._hidden_slice = self._meta['output_slices']['hidden_state']
|
||||
self._run_policy = self._m['run_policy']
|
||||
self._warp_jits: dict[tuple[int, int], Any] = {k: v for k, v in self._m.items() if isinstance(k, tuple)}
|
||||
if not self._warp_jits:
|
||||
raise ValueError("supercombo pkl has no warp JITs")
|
||||
self._frame_skip = int(self._m.get('frame_skip', 4))
|
||||
self._validate_warp_jits(pkl_path)
|
||||
self._validate_jit_names()
|
||||
|
||||
self._queues: dict[str, Any] | None = None
|
||||
self._npy: dict[str, np.ndarray] | None = None
|
||||
self._cam: tuple[int, int] | None = None
|
||||
self._prev_desire = np.zeros(self._ish['desire_pulse'][2], dtype=np.float32)
|
||||
self._blob_cache: dict[tuple[str, int], Any] = {}
|
||||
|
||||
def _verify_artifact_file(self) -> None:
|
||||
if not self._expected_sha256:
|
||||
return
|
||||
|
||||
actual_sha256 = _file_sha256(self._pkl_path)
|
||||
if actual_sha256 == self._expected_sha256:
|
||||
return
|
||||
|
||||
try:
|
||||
os.remove(self._pkl_path)
|
||||
except OSError:
|
||||
pass
|
||||
redownload_msg = self._schedule_active_bundle_redownload()
|
||||
|
||||
raise RuntimeError(
|
||||
"supercombo artifact SHA mismatch: "
|
||||
f"expected {self._expected_sha256}, got {actual_sha256} for {self._pkl_path}. "
|
||||
f"Deleted the stale cached file{redownload_msg}."
|
||||
)
|
||||
|
||||
def _validate_warp_jits(self, pkl_path: str) -> None:
|
||||
img = self._ish['img']
|
||||
n_frames = img[1] // 6
|
||||
expected_depth = self._frame_skip * (n_frames - 1) + 1
|
||||
expected_device = os.getenv('DEV')
|
||||
|
||||
mismatches: list[str] = []
|
||||
for cam, warp_jit in sorted(self._warp_jits.items()):
|
||||
captured_depth = _captured_queue_depth(warp_jit)
|
||||
captured_devices = _captured_devices(warp_jit)
|
||||
if captured_depth is not None and captured_depth != expected_depth:
|
||||
mismatches.append(
|
||||
f"{cam[0]}x{cam[1]} queue-depth captured={captured_depth} expected={expected_depth}"
|
||||
)
|
||||
if expected_device and captured_devices and expected_device not in captured_devices:
|
||||
mismatches.append(
|
||||
f"{cam[0]}x{cam[1]} device captured={sorted(captured_devices)} expected={expected_device}"
|
||||
)
|
||||
|
||||
if mismatches:
|
||||
details = "; ".join(mismatches)
|
||||
raise RuntimeError(
|
||||
"supercombo warp JIT compatibility mismatch: "
|
||||
f"{details}. Bundle {pkl_path} was compiled with the wrong backend, frame_skip, or queue shape; "
|
||||
"re-download or rebuild this model artifact."
|
||||
)
|
||||
|
||||
def _validate_jit_names(self) -> None:
|
||||
expected_warp_names = ['big_frame', 'big_tfm', 'frame', 'tfm']
|
||||
expected_policy_names = ['big_img_q', 'desire_q', 'feat_q', 'img_q', 'packed_npy_inputs', 'warped']
|
||||
|
||||
mismatches: list[str] = []
|
||||
|
||||
policy_names = sorted(_captured_expected_names(self._run_policy))
|
||||
if policy_names and policy_names != expected_policy_names:
|
||||
mismatches.append(f"run_policy captured={policy_names} expected={expected_policy_names}")
|
||||
|
||||
for cam, warp_jit in sorted(self._warp_jits.items()):
|
||||
warp_names = sorted(_captured_expected_names(warp_jit))
|
||||
if warp_names and warp_names != expected_warp_names:
|
||||
mismatches.append(f"{cam[0]}x{cam[1]} warp captured={warp_names} expected={expected_warp_names}")
|
||||
|
||||
if mismatches:
|
||||
details = "; ".join(mismatches)
|
||||
actual_sha = None
|
||||
try:
|
||||
actual_sha = _file_sha256(self._pkl_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if actual_sha and self._expected_sha256 and actual_sha != self._expected_sha256:
|
||||
try:
|
||||
os.remove(self._pkl_path)
|
||||
except OSError:
|
||||
pass
|
||||
redownload_msg = self._schedule_active_bundle_redownload()
|
||||
raise RuntimeError(
|
||||
"supercombo artifact contract mismatch with stale cached SHA: "
|
||||
f"{details}. Expected SHA {self._expected_sha256}, got {actual_sha}. "
|
||||
f"Deleted the stale cached file{redownload_msg}."
|
||||
)
|
||||
|
||||
raise RuntimeError(
|
||||
"supercombo artifact JIT argument mismatch: "
|
||||
f"{details}. This model file does not match the current IQPilot runtime contract. "
|
||||
"Re-download or rebuild this model artifact."
|
||||
)
|
||||
|
||||
def _handle_runtime_jit_mismatch(self, err: BaseException) -> None:
|
||||
if not _is_jit_arg_mismatch(err):
|
||||
raise err
|
||||
|
||||
actual_sha = None
|
||||
try:
|
||||
actual_sha = _file_sha256(self._pkl_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if actual_sha and self._expected_sha256 and actual_sha != self._expected_sha256:
|
||||
try:
|
||||
os.remove(self._pkl_path)
|
||||
except OSError:
|
||||
pass
|
||||
redownload_msg = self._schedule_active_bundle_redownload()
|
||||
raise RuntimeError(
|
||||
"supercombo artifact runtime JIT mismatch with stale cached SHA: "
|
||||
f"expected {self._expected_sha256}, got {actual_sha} for {self._pkl_path}. "
|
||||
f"Deleted the stale cached file{redownload_msg}."
|
||||
) from err
|
||||
|
||||
raise RuntimeError(
|
||||
"supercombo artifact runtime JIT mismatch: "
|
||||
f"{err}. This model file does not match the current IQPilot runtime contract. "
|
||||
"Re-download or rebuild this model artifact."
|
||||
) from err
|
||||
|
||||
def _schedule_active_bundle_redownload(self) -> str:
|
||||
try:
|
||||
params = Params()
|
||||
active_bundle = params.get("ModelManager_ActiveBundle") or {}
|
||||
index = active_bundle.get("index") if isinstance(active_bundle, dict) else None
|
||||
if isinstance(index, str) and index.isdigit():
|
||||
index = int(index)
|
||||
if isinstance(index, int) and index >= 0:
|
||||
params.put("ModelManager_DownloadIndex", str(index))
|
||||
params.remove("ModelRunnerTypeCache")
|
||||
return "; scheduled automatic re-download of the active model"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return "; unable to schedule automatic re-download"
|
||||
|
||||
def _frame_tensor(self, key: str, buf):
|
||||
Tensor, Device = _tinygrad_imports()
|
||||
arr = np.frombuffer(buf.data, dtype=np.uint8)
|
||||
ck = (key, arr.ctypes.data)
|
||||
t = self._blob_cache.get(ck)
|
||||
if t is None:
|
||||
t = Tensor.from_blob(arr.ctypes.data, (arr.size,), dtype='uint8', device=Device.DEFAULT)
|
||||
self._blob_cache[ck] = t
|
||||
return t
|
||||
|
||||
@property
|
||||
def vision_input_names(self) -> list[str]:
|
||||
return ['img', 'big_img']
|
||||
|
||||
@property
|
||||
def input_shapes(self) -> ShapeDict:
|
||||
return dict(self._ish)
|
||||
|
||||
@property
|
||||
def output_slices(self) -> SliceDict:
|
||||
return dict(self._slices)
|
||||
|
||||
def prepare_inputs(self, imgs_cl, numpy_inputs, frames):
|
||||
raise RuntimeError("supercombo runner has no OpenCL path; use run_fused()")
|
||||
|
||||
def _ensure_queues(self, cam_w: int, cam_h: int) -> None:
|
||||
if self._queues is not None and self._cam == (cam_w, cam_h):
|
||||
return
|
||||
if (cam_w, cam_h) not in self._warp_jits:
|
||||
raise RuntimeError(f"no warp JIT for {cam_w}x{cam_h}; have {sorted(self._warp_jits)}")
|
||||
|
||||
Tensor, Device = _tinygrad_imports()
|
||||
fs = self._frame_skip
|
||||
img = self._ish['img']
|
||||
n_frames = img[1] // 6
|
||||
img_buf = (fs * (n_frames - 1) + 1, 6, img[2], img[3])
|
||||
fb = self._ish['features_buffer']
|
||||
dp = self._ish['desire_pulse']
|
||||
tc = self._ish['traffic_convention']
|
||||
at = self._ish['action_t']
|
||||
|
||||
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()
|
||||
|
||||
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)
|
||||
views = {k: v.reshape(s) for (k, s), v in zip(shapes.items(), np.split(packed, np.cumsum(sizes[:-1])), strict=True)}
|
||||
|
||||
self._npy = {'tfm': np.zeros((3, 3), dtype=np.float32), 'big_tfm': np.zeros((3, 3), dtype=np.float32), **views}
|
||||
self._queues = {
|
||||
'img_q': zeros_u8(img_buf),
|
||||
'big_img_q': zeros_u8(img_buf),
|
||||
'feat_q': zeros_f32((fs * fb[1], fb[0], fb[2])),
|
||||
'desire_q': zeros_f32((fs * dp[1], dp[0], dp[2])),
|
||||
'tfm': Tensor(self._npy['tfm'], device='NPY'),
|
||||
'big_tfm': Tensor(self._npy['big_tfm'], device='NPY'),
|
||||
'packed_npy_inputs': Tensor(packed, device='NPY'),
|
||||
}
|
||||
self._cam = (cam_w, cam_h)
|
||||
|
||||
def run_fused(self, bufs: dict, transforms: dict[str, np.ndarray], numpy_inputs: NumpyDict) -> NumpyDict:
|
||||
Tensor, Device = _tinygrad_imports()
|
||||
main_buf = bufs['img']
|
||||
self._ensure_queues(main_buf.width, main_buf.height)
|
||||
assert self._queues is not None and self._npy is not None
|
||||
|
||||
self._npy['tfm'][:] = transforms['img']
|
||||
self._npy['big_tfm'][:] = transforms['big_img']
|
||||
|
||||
desire_key = next((k for k in numpy_inputs if k.startswith('desire')), None)
|
||||
cur = numpy_inputs[desire_key].copy() if desire_key is not None else np.zeros_like(self._prev_desire)
|
||||
cur[0] = 0
|
||||
self._npy['desire'][:] = np.where(cur - self._prev_desire > .99, cur, 0)
|
||||
self._prev_desire[:] = cur
|
||||
if 'traffic_convention' in numpy_inputs:
|
||||
self._npy['traffic_convention'][:] = numpy_inputs['traffic_convention']
|
||||
if 'action_t' in numpy_inputs:
|
||||
self._npy['action_t'][:] = numpy_inputs['action_t']
|
||||
|
||||
frame = self._frame_tensor('img', bufs['img'])
|
||||
big_frame = self._frame_tensor('big_img', bufs['big_img'])
|
||||
|
||||
warp = self._warp_jits[self._cam]
|
||||
try:
|
||||
warped = warp(tfm=self._queues['tfm'], big_tfm=self._queues['big_tfm'], frame=frame, big_frame=big_frame)
|
||||
out, = self._run_policy(warped=warped, img_q=self._queues['img_q'], big_img_q=self._queues['big_img_q'],
|
||||
feat_q=self._queues['feat_q'], desire_q=self._queues['desire_q'],
|
||||
packed_npy_inputs=self._queues['packed_npy_inputs'])
|
||||
except Exception as err:
|
||||
self._handle_runtime_jit_mismatch(err)
|
||||
raise
|
||||
flat = out.numpy().flatten()
|
||||
|
||||
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)
|
||||
|
||||
def _run_model(self) -> NumpyDict:
|
||||
raise RuntimeError("supercombo path goes through run_fused(), not _run_model()")
|
||||
@@ -0,0 +1,190 @@
|
||||
"""
|
||||
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
|
||||
from tinygrad.dtype import dtypes
|
||||
from tinygrad.tensor import Tensor
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import (
|
||||
CLMemDict,
|
||||
CUSTOM_MODEL_PATH,
|
||||
FrameDict,
|
||||
ModelType,
|
||||
NumpyDict,
|
||||
ShapeDict,
|
||||
SliceDict,
|
||||
)
|
||||
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 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)
|
||||
class _TensorShapePlan:
|
||||
dtype: object
|
||||
device: str
|
||||
|
||||
|
||||
def _artifact_path(filename: str) -> str:
|
||||
return f"{CUSTOM_MODEL_PATH}/{filename}"
|
||||
|
||||
|
||||
def _load_program_blob(filename: str):
|
||||
with open(_artifact_path(filename), "rb") as artifact:
|
||||
try:
|
||||
return pickle.load(artifact)
|
||||
except FileNotFoundError as exc:
|
||||
assert "/dev/kgsl-3d0" not in str(exc), "Model was built on C3 or C3X, but is being loaded on PC"
|
||||
raise
|
||||
|
||||
|
||||
def _compile_input_plan(captured) -> dict[str, _TensorShapePlan]:
|
||||
plan: dict[str, _TensorShapePlan] = {}
|
||||
for name, info in zip(captured.expected_names, captured.expected_input_info, strict=True):
|
||||
plan[name] = _TensorShapePlan(dtype=info[2], device=info[3])
|
||||
return plan
|
||||
|
||||
|
||||
def _merge_step_outputs(output_groups: list[NumpyDict]) -> NumpyDict:
|
||||
stitched: NumpyDict = {}
|
||||
for payload in output_groups:
|
||||
stitched.update(payload)
|
||||
if "planplus" in stitched and "plan" in stitched:
|
||||
stitched["plan"] = stitched["plan"] + stitched["planplus"]
|
||||
return stitched
|
||||
|
||||
|
||||
class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTinygrad, OffPolicyTinygrad, OnPolicyTinygrad):
|
||||
def __init__(self, model_type: int = ModelType.supercombo):
|
||||
ModelRunner.__init__(self)
|
||||
for initializer in (SupercomboTinygrad, PolicyTinygrad, VisionTinygrad, OffPolicyTinygrad, OnPolicyTinygrad):
|
||||
initializer.__init__(self)
|
||||
|
||||
self._constants = ModelConstants
|
||||
self._model_data = self.models.get(model_type)
|
||||
if self._model_data is None or self._model_data.model is None:
|
||||
raise ValueError(f"Model data for type {model_type} not available.")
|
||||
|
||||
asset_name = self._model_data.model.artifact.fileName
|
||||
assert asset_name.endswith("_tinygrad.pkl"), f"Invalid model file {asset_name} for TinygradRunner"
|
||||
|
||||
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()}
|
||||
|
||||
@property
|
||||
def vision_input_names(self) -> list[str]:
|
||||
return [stream_name for stream_name in self.input_shapes if "img" in stream_name]
|
||||
|
||||
def _attach_vision_tensor(self, stream_name: str, frame_buffers: CLMemDict, frame_views: FrameDict) -> None:
|
||||
spec = self._input_plan[stream_name]
|
||||
frame_buffer = frame_buffers[stream_name]
|
||||
if TICI:
|
||||
self.inputs[stream_name] = qcom_tensor_from_opencl_address(frame_buffer.mem_address,
|
||||
self.input_shapes[stream_name],
|
||||
dtype=spec.dtype)
|
||||
return
|
||||
|
||||
mirrored = frame_views[stream_name].as_numpy(frame_buffer).reshape(self.input_shapes[stream_name])
|
||||
self.inputs[stream_name] = Tensor(mirrored, device=spec.device, dtype=spec.dtype).realize()
|
||||
|
||||
def _attach_state_tensor(self, tensor_name: str, tensor_value: np.ndarray) -> None:
|
||||
spec = self._input_plan[tensor_name]
|
||||
self.inputs[tensor_name] = Tensor(tensor_value, device=spec.device, dtype=spec.dtype).realize()
|
||||
|
||||
def prepare_vision_inputs(self, imgs_cl: CLMemDict, frames: FrameDict):
|
||||
for stream_name in imgs_cl:
|
||||
if stream_name not in self.inputs or not TICI:
|
||||
self._attach_vision_tensor(stream_name, imgs_cl, frames)
|
||||
|
||||
def prepare_policy_inputs(self, numpy_inputs: NumpyDict):
|
||||
for tensor_name, tensor_value in numpy_inputs.items():
|
||||
self._attach_state_tensor(tensor_name, tensor_value)
|
||||
|
||||
def prepare_inputs(self, imgs_cl: CLMemDict, numpy_inputs: NumpyDict, frames: FrameDict) -> dict:
|
||||
self.prepare_vision_inputs(imgs_cl, frames)
|
||||
self.prepare_policy_inputs(numpy_inputs)
|
||||
return self.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](model_outputs)
|
||||
|
||||
def _run_model(self) -> NumpyDict:
|
||||
raw_output = self.model_run(**self.inputs).numpy().reshape(-1)
|
||||
return self._parse_outputs(raw_output)
|
||||
|
||||
|
||||
class TinygradSplitRunner(ModelRunner):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.is_20hz_3d = True
|
||||
self._constants = SplitModelConstants
|
||||
self.vision_runner = TinygradRunner(ModelType.vision)
|
||||
self.policy_runner = TinygradRunner(ModelType.policy) if self.models.get(ModelType.policy) else None
|
||||
self.off_policy_runner = TinygradRunner(ModelType.offPolicy) if self.models.get(ModelType.offPolicy) else None
|
||||
self.on_policy_runner = TinygradRunner(ModelType.onPolicy) if self.models.get(ModelType.onPolicy) else None
|
||||
|
||||
def _policy_units(self) -> list[TinygradRunner]:
|
||||
return [runner for runner in (self.policy_runner, self.off_policy_runner, self.on_policy_runner) if runner is not None]
|
||||
|
||||
def run_vision(self) -> NumpyDict:
|
||||
return self.vision_runner.run_model()
|
||||
|
||||
def run_policy(self) -> NumpyDict:
|
||||
return _merge_step_outputs([runner.run_model() for runner in self._policy_units()])
|
||||
|
||||
def refresh_policy_features(self, features_buffer: np.ndarray) -> None:
|
||||
for runner in self._policy_units():
|
||||
if "features_buffer" in runner._input_plan:
|
||||
runner._attach_state_tensor("features_buffer", features_buffer)
|
||||
|
||||
def _run_model(self) -> NumpyDict:
|
||||
return _merge_step_outputs([self.run_vision(), self.run_policy()])
|
||||
|
||||
@property
|
||||
def vision_input_names(self) -> list[str]:
|
||||
return list(self.vision_runner.vision_input_names)
|
||||
|
||||
@property
|
||||
def input_shapes(self) -> ShapeDict:
|
||||
composite: ShapeDict = dict(self.vision_runner.input_shapes)
|
||||
for runner in self._policy_units():
|
||||
composite.update(runner.input_shapes)
|
||||
return composite
|
||||
|
||||
@property
|
||||
def output_slices(self) -> SliceDict:
|
||||
composite: SliceDict = dict(self.vision_runner.output_slices)
|
||||
for runner in self._policy_units():
|
||||
composite.update(runner.output_slices)
|
||||
return composite
|
||||
|
||||
def prepare_inputs(self, imgs_cl: CLMemDict, numpy_inputs: NumpyDict, frames: FrameDict) -> dict:
|
||||
self.vision_runner.prepare_vision_inputs(imgs_cl, frames)
|
||||
assembled_inputs = dict(self.vision_runner.inputs)
|
||||
for runner in self._policy_units():
|
||||
runner.prepare_policy_inputs(numpy_inputs)
|
||||
assembled_inputs.update(runner.inputs)
|
||||
self.inputs = assembled_inputs
|
||||
return assembled_inputs
|
||||
89
iqpilot/selfdrive/iqmodeld/models/split_model_constants.py
Normal file
89
iqpilot/selfdrive/iqmodeld/models/split_model_constants.py
Normal file
@@ -0,0 +1,89 @@
|
||||
import numpy as np
|
||||
|
||||
|
||||
def index_function(idx, max_val=192, max_idx=32):
|
||||
return max_val * ((idx/max_idx)**2)
|
||||
|
||||
|
||||
class SplitModelConstants:
|
||||
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)]
|
||||
LEAD_T_IDXS = [0., 2., 4., 6., 8., 10.]
|
||||
LEAD_T_OFFSETS = [0., 2., 4.]
|
||||
META_T_IDXS = [2., 4., 6., 8., 10.]
|
||||
|
||||
MODEL_FREQ = 20
|
||||
HISTORY_FREQ = 5
|
||||
HISTORY_LEN_SECONDS = 5
|
||||
TEMPORAL_SKIP = MODEL_FREQ // HISTORY_FREQ
|
||||
FULL_HISTORY_BUFFER_LEN = MODEL_FREQ * HISTORY_LEN_SECONDS
|
||||
INPUT_HISTORY_BUFFER_LEN = HISTORY_FREQ * HISTORY_LEN_SECONDS
|
||||
|
||||
FEATURE_LEN = 512
|
||||
|
||||
DESIRE_LEN = 8
|
||||
TRAFFIC_CONVENTION_LEN = 2
|
||||
LAT_PLANNER_STATE_LEN = 4
|
||||
LATERAL_CONTROL_PARAMS_LEN = 2
|
||||
PREV_DESIRED_CURV_LEN = 1
|
||||
|
||||
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
|
||||
FCW_3MS2_PROBS_WIDTH = 2
|
||||
|
||||
DISENGAGE_WIDTH = 5
|
||||
POSE_WIDTH = 6
|
||||
WIDE_FROM_DEVICE_WIDTH = 3
|
||||
LEAD_WIDTH = 4
|
||||
LANE_LINES_WIDTH = 2
|
||||
ROAD_EDGES_WIDTH = 2
|
||||
PLAN_WIDTH = 15
|
||||
DESIRE_PRED_WIDTH = 8
|
||||
LAT_PLANNER_SOLUTION_WIDTH = 4
|
||||
DESIRED_CURV_WIDTH = 1
|
||||
ACTION_WIDTH = 2
|
||||
|
||||
NUM_LANE_LINES = 4
|
||||
NUM_ROAD_EDGES = 2
|
||||
|
||||
LEAD_TRAJ_LEN = 6
|
||||
DESIRE_PRED_LEN = 4
|
||||
|
||||
PLAN_MHP_N = 5
|
||||
LEAD_MHP_N = 2
|
||||
PLAN_MHP_SELECTION = 1
|
||||
LEAD_MHP_SELECTION = 3
|
||||
|
||||
FCW_THRESHOLD_5MS2_HIGH = 0.15
|
||||
FCW_THRESHOLD_5MS2_LOW = 0.05
|
||||
FCW_THRESHOLD_3MS2 = 0.7
|
||||
|
||||
CONFIDENCE_BUFFER_LEN = 5
|
||||
RYG_GREEN = 0.01165
|
||||
RYG_YELLOW = 0.06157
|
||||
|
||||
POLY_PATH_DEGREE = 4
|
||||
|
||||
|
||||
class Plan:
|
||||
POSITION = slice(0, 3)
|
||||
VELOCITY = slice(3, 6)
|
||||
ACCELERATION = slice(6, 9)
|
||||
T_FROM_CURRENT_EULER = slice(9, 12)
|
||||
ORIENTATION_RATE = slice(12, 15)
|
||||
|
||||
|
||||
class Meta:
|
||||
ENGAGED = slice(0, 1)
|
||||
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)
|
||||
GAS_PRESS = slice(31, 55, 4)
|
||||
BRAKE_PRESS = slice(32, 55, 4)
|
||||
LEFT_BLINKER = slice(33, 55, 4)
|
||||
RIGHT_BLINKER = slice(34, 55, 4)
|
||||
3
iqpilot/selfdrive/iqmodeld/native/__init__.py
Normal file
3
iqpilot/selfdrive/iqmodeld/native/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
100
iqpilot/selfdrive/iqmodeld/native/iqmodel.cc
Normal file
100
iqpilot/selfdrive/iqmodeld/native/iqmodel.cc
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
*/
|
||||
#include "iqpilot/selfdrive/iqmodeld/native/iqmodel.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "common/clutil.h"
|
||||
|
||||
namespace {
|
||||
|
||||
void rotate_history_window(cl_command_queue queue, cl_mem timeline_cl, uint8_t history_slots, size_t frame_bytes) {
|
||||
for (int slot = 0; slot < (history_slots - 1); slot++) {
|
||||
CL_CHECK(clEnqueueCopyBuffer(queue, timeline_cl, timeline_cl,
|
||||
(slot + 1) * frame_bytes, slot * frame_bytes,
|
||||
frame_bytes, 0, nullptr, nullptr));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
FrameCropperBase::FrameCropperBase(cl_device_id device_id, cl_context context) {
|
||||
work_queue_ = CL_CHECK_ERR(clCreateCommandQueue(context, device_id, 0, &err));
|
||||
}
|
||||
|
||||
FrameCropperBase::~FrameCropperBase() {
|
||||
CL_CHECK(clReleaseCommandQueue(work_queue_));
|
||||
}
|
||||
|
||||
void FrameCropperBase::configure_planar_tiles(cl_device_id device_id, cl_context context, int output_width, int output_height) {
|
||||
y_plane_tile_cl_ = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, output_width * output_height, NULL, &err));
|
||||
u_plane_tile_cl_ = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, (output_width / 2) * (output_height / 2), NULL, &err));
|
||||
v_plane_tile_cl_ = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, (output_width / 2) * (output_height / 2), NULL, &err));
|
||||
warp_sampler_init(&sampler_state_, context, device_id);
|
||||
}
|
||||
|
||||
void FrameCropperBase::release_planar_tiles() {
|
||||
warp_sampler_release(&sampler_state_);
|
||||
CL_CHECK(clReleaseMemObject(v_plane_tile_cl_));
|
||||
CL_CHECK(clReleaseMemObject(u_plane_tile_cl_));
|
||||
CL_CHECK(clReleaseMemObject(y_plane_tile_cl_));
|
||||
}
|
||||
|
||||
void FrameCropperBase::project_frame(cl_mem yuv_cl, int output_width, int output_height,
|
||||
int frame_width, int frame_height, int frame_stride, int frame_uv_offset,
|
||||
const mat3 &projection) {
|
||||
warp_sampler_dispatch(&sampler_state_, work_queue_,
|
||||
yuv_cl, frame_width, frame_height, frame_stride, frame_uv_offset,
|
||||
y_plane_tile_cl_, u_plane_tile_cl_, v_plane_tile_cl_,
|
||||
output_width, output_height, projection);
|
||||
}
|
||||
|
||||
RoadHistoryAssembler::RoadHistoryAssembler(cl_device_id device_id, cl_context context, uint8_t history_slots)
|
||||
: FrameCropperBase(device_id, context), frame_bytes_(kFrameBytes * sizeof(uint8_t)), history_slots_(history_slots) {
|
||||
buf_size = kExportBytes;
|
||||
staging_bytes_ = std::make_unique<uint8_t[]>(buf_size);
|
||||
publish_pair_cl_ = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, buf_size, NULL, &err));
|
||||
timeline_cl_ = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, history_slots_ * frame_bytes_, NULL, &err));
|
||||
|
||||
latest_region_.origin = (history_slots_ - 1) * frame_bytes_;
|
||||
latest_region_.size = frame_bytes_;
|
||||
latest_slot_cl_ = CL_CHECK_ERR(clCreateSubBuffer(timeline_cl_, CL_MEM_READ_WRITE, CL_BUFFER_CREATE_TYPE_REGION, &latest_region_, &err));
|
||||
|
||||
packed_frame_kernels_init(&packer_, context, device_id, kOutputWidth, kOutputHeight);
|
||||
configure_planar_tiles(device_id, context, kOutputWidth, kOutputHeight);
|
||||
}
|
||||
|
||||
cl_mem *RoadHistoryAssembler::project_to_cl(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3 &projection) {
|
||||
project_frame(yuv_cl, kOutputWidth, kOutputHeight, frame_width, frame_height, frame_stride, frame_uv_offset, projection);
|
||||
rotate_history_window(work_queue_, timeline_cl_, history_slots_, frame_bytes_);
|
||||
packed_frame_emit(&packer_, work_queue_, y_plane_tile_cl_, u_plane_tile_cl_, v_plane_tile_cl_, latest_slot_cl_);
|
||||
packed_frame_clone_range(&packer_, work_queue_, timeline_cl_, publish_pair_cl_, 0, 0, frame_bytes_);
|
||||
packed_frame_clone_range(&packer_, work_queue_, latest_slot_cl_, publish_pair_cl_, 0, frame_bytes_, frame_bytes_);
|
||||
clFinish(work_queue_);
|
||||
return &publish_pair_cl_;
|
||||
}
|
||||
|
||||
RoadHistoryAssembler::~RoadHistoryAssembler() {
|
||||
release_planar_tiles();
|
||||
packed_frame_kernels_release(&packer_);
|
||||
CL_CHECK(clReleaseMemObject(publish_pair_cl_));
|
||||
CL_CHECK(clReleaseMemObject(timeline_cl_));
|
||||
CL_CHECK(clReleaseMemObject(latest_slot_cl_));
|
||||
}
|
||||
|
||||
CabinFrameSampler::CabinFrameSampler(cl_device_id device_id, cl_context context) : FrameCropperBase(device_id, context) {
|
||||
buf_size = kExportBytes;
|
||||
staging_bytes_ = std::make_unique<uint8_t[]>(buf_size);
|
||||
configure_planar_tiles(device_id, context, kOutputWidth, kOutputHeight);
|
||||
}
|
||||
|
||||
cl_mem *CabinFrameSampler::project_to_cl(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3 &projection) {
|
||||
project_frame(yuv_cl, kOutputWidth, kOutputHeight, frame_width, frame_height, frame_stride, frame_uv_offset, projection);
|
||||
clFinish(work_queue_);
|
||||
return &y_plane_tile_cl_;
|
||||
}
|
||||
|
||||
CabinFrameSampler::~CabinFrameSampler() {
|
||||
release_planar_tiles();
|
||||
}
|
||||
81
iqpilot/selfdrive/iqmodeld/native/iqmodel.h
Normal file
81
iqpilot/selfdrive/iqmodeld/native/iqmodel.h
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <cassert>
|
||||
#include <cfloat>
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
|
||||
#define CL_USE_DEPRECATED_OPENCL_1_2_APIS
|
||||
#ifdef __APPLE__
|
||||
#include <OpenCL/cl.h>
|
||||
#else
|
||||
#include <CL/cl.h>
|
||||
#endif
|
||||
|
||||
#include "common/mat.h"
|
||||
#include "iqpilot/selfdrive/iqmodeld/transforms/warp_geometry.h"
|
||||
#include "iqpilot/selfdrive/iqmodeld/transforms/yuv.h"
|
||||
|
||||
class FrameCropperBase {
|
||||
public:
|
||||
FrameCropperBase(cl_device_id device_id, cl_context context);
|
||||
virtual ~FrameCropperBase();
|
||||
virtual cl_mem *project_to_cl(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3 &projection) = 0;
|
||||
|
||||
uint8_t *copy_to_host(cl_mem *source_frames, int buffer_size) {
|
||||
CL_CHECK(clEnqueueReadBuffer(work_queue_, *source_frames, CL_TRUE, 0, buffer_size, staging_bytes_.get(), 0, nullptr, nullptr));
|
||||
clFinish(work_queue_);
|
||||
return &staging_bytes_[0];
|
||||
}
|
||||
|
||||
int buf_size;
|
||||
|
||||
protected:
|
||||
cl_command_queue work_queue_;
|
||||
std::unique_ptr<uint8_t[]> staging_bytes_;
|
||||
cl_mem y_plane_tile_cl_;
|
||||
cl_mem u_plane_tile_cl_;
|
||||
cl_mem v_plane_tile_cl_;
|
||||
WarpSamplerState sampler_state_;
|
||||
|
||||
void configure_planar_tiles(cl_device_id device_id, cl_context context, int output_width, int output_height);
|
||||
void release_planar_tiles();
|
||||
void project_frame(cl_mem yuv_cl, int output_width, int output_height,
|
||||
int frame_width, int frame_height, int frame_stride, int frame_uv_offset,
|
||||
const mat3 &projection);
|
||||
};
|
||||
|
||||
class RoadHistoryAssembler : public FrameCropperBase {
|
||||
public:
|
||||
RoadHistoryAssembler(cl_device_id device_id, cl_context context, uint8_t history_slots);
|
||||
~RoadHistoryAssembler() override;
|
||||
cl_mem *project_to_cl(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3 &projection) override;
|
||||
|
||||
static constexpr int kOutputWidth = 512;
|
||||
static constexpr int kOutputHeight = 256;
|
||||
static constexpr int kFrameBytes = kOutputWidth * kOutputHeight * 3 / 2;
|
||||
static constexpr int kExportBytes = kFrameBytes * 2;
|
||||
|
||||
private:
|
||||
PackedFrameKernels packer_;
|
||||
cl_mem timeline_cl_;
|
||||
cl_mem latest_slot_cl_;
|
||||
cl_mem publish_pair_cl_;
|
||||
cl_buffer_region latest_region_;
|
||||
size_t frame_bytes_;
|
||||
uint8_t history_slots_;
|
||||
};
|
||||
|
||||
class CabinFrameSampler : public FrameCropperBase {
|
||||
public:
|
||||
CabinFrameSampler(cl_device_id device_id, cl_context context);
|
||||
~CabinFrameSampler() override;
|
||||
cl_mem *project_to_cl(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3 &projection) override;
|
||||
|
||||
static constexpr int kOutputWidth = 1440;
|
||||
static constexpr int kOutputHeight = 960;
|
||||
static constexpr int kExportBytes = kOutputWidth * kOutputHeight;
|
||||
};
|
||||
29
iqpilot/selfdrive/iqmodeld/native/iqmodel.pxd
Normal file
29
iqpilot/selfdrive/iqmodeld/native/iqmodel.pxd
Normal file
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# distutils: language = c++
|
||||
# Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
|
||||
from msgq.visionipc.visionipc cimport cl_device_id, cl_context, cl_mem
|
||||
|
||||
cdef extern from "common/mat.h":
|
||||
cdef struct mat3:
|
||||
float v[9]
|
||||
|
||||
cdef extern from "common/clutil.h":
|
||||
cdef unsigned long CL_DEVICE_TYPE_DEFAULT
|
||||
cl_device_id cl_get_device_id(unsigned long)
|
||||
cl_context cl_create_context(cl_device_id)
|
||||
void cl_release_context(cl_context)
|
||||
|
||||
cdef extern from "iqpilot/selfdrive/iqmodeld/native/iqmodel.h":
|
||||
cppclass NativeFrameBridge "FrameCropperBase":
|
||||
int buf_size
|
||||
unsigned char * copy_to_host(cl_mem*, int);
|
||||
cl_mem * project_to_cl(cl_mem, int, int, int, int, mat3)
|
||||
|
||||
cppclass RoadFrameBridge "RoadHistoryAssembler":
|
||||
int buf_size
|
||||
RoadFrameBridge(cl_device_id, cl_context, unsigned char)
|
||||
|
||||
cppclass CabinFrameBridge "CabinFrameSampler":
|
||||
int buf_size
|
||||
CabinFrameBridge(cl_device_id, cl_context)
|
||||
12
iqpilot/selfdrive/iqmodeld/native/iqmodel_pyx.pxd
Normal file
12
iqpilot/selfdrive/iqmodeld/native/iqmodel_pyx.pxd
Normal file
@@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# distutils: language = c++
|
||||
# Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
|
||||
from msgq.visionipc.visionipc cimport cl_mem
|
||||
from msgq.visionipc.visionipc_pyx cimport CLContext as VisionIpcContextBase
|
||||
|
||||
cdef class WarpContext(VisionIpcContextBase):
|
||||
pass
|
||||
|
||||
cdef class GpuMemorySlot:
|
||||
cdef cl_mem * handle_ptr
|
||||
100
iqpilot/selfdrive/iqmodeld/native/iqmodel_pyx.pyx
Normal file
100
iqpilot/selfdrive/iqmodeld/native/iqmodel_pyx.pyx
Normal file
@@ -0,0 +1,100 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# distutils: language = c++
|
||||
# cython: c_string_encoding=ascii, language_level=3
|
||||
# Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
|
||||
import numpy as np
|
||||
cimport numpy as cnp
|
||||
from libc.string cimport memcpy
|
||||
from libc.stdint cimport uintptr_t
|
||||
|
||||
from msgq.visionipc.visionipc cimport cl_mem
|
||||
from msgq.visionipc.visionipc_pyx cimport VisionBuf, CLContext as VisionIpcContextBase
|
||||
from .iqmodel cimport CL_DEVICE_TYPE_DEFAULT, cl_get_device_id, cl_create_context, cl_release_context
|
||||
from .iqmodel cimport mat3, NativeFrameBridge, RoadFrameBridge, CabinFrameBridge
|
||||
|
||||
|
||||
cdef inline mat3 _projection_to_mat3(float[:] values):
|
||||
cdef mat3 warp_matrix
|
||||
memcpy(warp_matrix.v, &values[0], 9 * sizeof(float))
|
||||
return warp_matrix
|
||||
|
||||
|
||||
cdef inline GpuMemorySlot _borrow_cl_slot(void * raw_handle):
|
||||
cdef GpuMemorySlot carrier = GpuMemorySlot()
|
||||
carrier.handle_ptr = <cl_mem*>raw_handle
|
||||
return carrier
|
||||
|
||||
|
||||
cdef inline object _read_u8_view(unsigned char * payload, int length):
|
||||
return np.asarray(<cnp.uint8_t[:length]> payload)
|
||||
|
||||
|
||||
cdef class WarpContext(VisionIpcContextBase):
|
||||
def __cinit__(self):
|
||||
self.device_id = cl_get_device_id(CL_DEVICE_TYPE_DEFAULT)
|
||||
self.context = cl_create_context(self.device_id)
|
||||
|
||||
def __dealloc__(self):
|
||||
if self.context:
|
||||
cl_release_context(self.context)
|
||||
|
||||
cdef class GpuMemorySlot:
|
||||
@property
|
||||
def mem_address(self):
|
||||
return <uintptr_t>(self.handle_ptr)
|
||||
|
||||
|
||||
def cl_from_visionbuf(VisionBuf buf):
|
||||
return _borrow_cl_slot(<void*>&buf.buf.buf_cl)
|
||||
|
||||
|
||||
cdef class _ProjectionBridge:
|
||||
cdef NativeFrameBridge * _native_ptr
|
||||
cdef int _export_bytes
|
||||
|
||||
def __dealloc__(self):
|
||||
del self._native_ptr
|
||||
|
||||
cdef void _attach(self, NativeFrameBridge * native_frame, int export_bytes):
|
||||
self._native_ptr = native_frame
|
||||
self._export_bytes = export_bytes
|
||||
|
||||
cdef GpuMemorySlot _stage_image(self, VisionBuf buf, float[:] projection):
|
||||
cdef mat3 projection_spec = _projection_to_mat3(projection)
|
||||
cdef cl_mem * exported_slot = self._native_ptr.project_to_cl(
|
||||
buf.buf.buf_cl,
|
||||
buf.width,
|
||||
buf.height,
|
||||
buf.stride,
|
||||
buf.uv_offset,
|
||||
projection_spec,
|
||||
)
|
||||
return _borrow_cl_slot(exported_slot)
|
||||
|
||||
cdef object _export_host_bytes(self, GpuMemorySlot opencl_slot):
|
||||
cdef unsigned char * payload = self._native_ptr.copy_to_host(opencl_slot.handle_ptr, self._export_bytes)
|
||||
return _read_u8_view(payload, self._export_bytes)
|
||||
|
||||
|
||||
cdef class FrameProjector(_ProjectionBridge):
|
||||
def stage(self, VisionBuf buf, float[:] projection):
|
||||
return self._stage_image(buf, projection)
|
||||
|
||||
def as_numpy(self, GpuMemorySlot in_frames):
|
||||
return self._export_host_bytes(in_frames)
|
||||
|
||||
|
||||
cdef class RoadProjector(FrameProjector):
|
||||
cdef RoadFrameBridge * _road_ptr
|
||||
|
||||
def __cinit__(self, WarpContext context, int buffer_length=2):
|
||||
self._road_ptr = new RoadFrameBridge(context.device_id, context.context, buffer_length)
|
||||
self._attach(<NativeFrameBridge*>self._road_ptr, self._road_ptr.buf_size)
|
||||
|
||||
cdef class CabinProjector(FrameProjector):
|
||||
cdef CabinFrameBridge * _cabin_ptr
|
||||
|
||||
def __cinit__(self, WarpContext context):
|
||||
self._cabin_ptr = new CabinFrameBridge(context.device_id, context.context)
|
||||
self._attach(<NativeFrameBridge*>self._cabin_ptr, self._cabin_ptr.buf_size)
|
||||
219
iqpilot/selfdrive/iqmodeld/parser.py
Normal file
219
iqpilot/selfdrive/iqmodeld/parser.py
Normal file
@@ -0,0 +1,219 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.models.split_model_constants import SplitModelConstants
|
||||
from iqpilot.selfdrive.iqmodeld.config import ModelConstants
|
||||
|
||||
|
||||
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 + 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):
|
||||
safe_exp(values, out=values)
|
||||
else:
|
||||
values = safe_exp(values)
|
||||
values /= np.sum(values, axis=axis, keepdims=True)
|
||||
return values
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _MixtureRecipe:
|
||||
input_heads: int
|
||||
output_heads: int
|
||||
final_shape: tuple[int, ...]
|
||||
|
||||
|
||||
class _TensorKitchen:
|
||||
def __init__(self, ignore_missing: bool = False):
|
||||
self.ignore_missing = ignore_missing
|
||||
|
||||
def _grab(self, outputs: dict[str, np.ndarray], tensor_name: str) -> np.ndarray | None:
|
||||
if tensor_name not in outputs:
|
||||
if not self.ignore_missing:
|
||||
raise ValueError(f"Missing output {tensor_name}")
|
||||
return
|
||||
return outputs[tensor_name]
|
||||
|
||||
def categorical(self, outputs: dict[str, np.ndarray], tensor_name: str, shape=None) -> None:
|
||||
raw = self._grab(outputs, tensor_name)
|
||||
if raw is None:
|
||||
return
|
||||
if shape is not None:
|
||||
raw = raw.reshape((raw.shape[0],) + shape)
|
||||
outputs[tensor_name] = _softmax_last(raw, axis=-1)
|
||||
|
||||
def binary(self, outputs: dict[str, np.ndarray], tensor_name: str) -> None:
|
||||
raw = self._grab(outputs, tensor_name)
|
||||
if raw is None:
|
||||
return
|
||||
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)
|
||||
if raw is None:
|
||||
return
|
||||
|
||||
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 = 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)
|
||||
for output_idx in range(recipe.output_heads):
|
||||
weights[:, :, output_idx - recipe.output_heads] = _softmax_last(
|
||||
reshaped[:, :, output_idx - recipe.output_heads], axis=-1
|
||||
)
|
||||
|
||||
if recipe.output_heads == 1:
|
||||
for batch_idx in range(weights.shape[0]):
|
||||
order = np.argsort(weights[batch_idx][:, 0])[::-1]
|
||||
weights[batch_idx] = weights[batch_idx][order]
|
||||
means[batch_idx] = means[batch_idx][order]
|
||||
stds[batch_idx] = stds[batch_idx][order]
|
||||
|
||||
hypothesis_shape = (reshaped.shape[0], recipe.input_heads, *recipe.final_shape)
|
||||
outputs[f"{tensor_name}_weights"] = weights
|
||||
outputs[f"{tensor_name}_hypotheses"] = means.reshape(hypothesis_shape)
|
||||
outputs[f"{tensor_name}_stds_hypotheses"] = stds.reshape(hypothesis_shape)
|
||||
|
||||
picked_means = np.zeros((reshaped.shape[0], recipe.output_heads, value_count), dtype=reshaped.dtype)
|
||||
picked_stds = np.zeros((reshaped.shape[0], recipe.output_heads, value_count), dtype=reshaped.dtype)
|
||||
for batch_idx in range(weights.shape[0]):
|
||||
for output_idx in range(recipe.output_heads):
|
||||
order = np.argsort(weights[batch_idx, :, output_idx])[::-1]
|
||||
picked_means[batch_idx, output_idx] = means[batch_idx, order[0]]
|
||||
picked_stds[batch_idx, output_idx] = stds[batch_idx, order[0]]
|
||||
else:
|
||||
picked_means = means
|
||||
picked_stds = stds
|
||||
|
||||
final_shape = ((reshaped.shape[0], recipe.output_heads, *recipe.final_shape)
|
||||
if recipe.output_heads > 1 else (reshaped.shape[0], *recipe.final_shape))
|
||||
outputs[tensor_name] = picked_means.reshape(final_shape)
|
||||
outputs[f"{tensor_name}_stds"] = picked_stds.reshape(final_shape)
|
||||
|
||||
|
||||
class ArchiveParser(_TensorKitchen):
|
||||
def __init__(self, ignore_missing: bool = False):
|
||||
super().__init__(ignore_missing=ignore_missing)
|
||||
self._c = ModelConstants
|
||||
|
||||
def _recipes(self) -> list[tuple[str, _MixtureRecipe]]:
|
||||
c = self._c
|
||||
return [
|
||||
("plan", _MixtureRecipe(c.PLAN_MHP_N, c.PLAN_MHP_SELECTION, (c.IDX_N, c.PLAN_WIDTH))),
|
||||
("lane_lines", _MixtureRecipe(0, 0, (c.NUM_LANE_LINES, c.IDX_N, c.LANE_LINES_WIDTH))),
|
||||
("road_edges", _MixtureRecipe(0, 0, (c.NUM_ROAD_EDGES, c.IDX_N, c.LANE_LINES_WIDTH))),
|
||||
("pose", _MixtureRecipe(0, 0, (c.POSE_WIDTH,))),
|
||||
("road_transform", _MixtureRecipe(0, 0, (c.POSE_WIDTH,))),
|
||||
("wide_from_device_euler", _MixtureRecipe(0, 0, (c.WIDE_FROM_DEVICE_WIDTH,))),
|
||||
("lead", _MixtureRecipe(c.LEAD_MHP_N, c.LEAD_MHP_SELECTION, (c.LEAD_TRAJ_LEN, c.LEAD_WIDTH))),
|
||||
]
|
||||
|
||||
def parse_outputs(self, outputs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
|
||||
c = self._c
|
||||
for tensor_name, recipe in self._recipes():
|
||||
self.mixture(outputs, tensor_name, recipe)
|
||||
if "sim_pose" in outputs:
|
||||
self.mixture(outputs, "sim_pose", _MixtureRecipe(0, 0, (c.POSE_WIDTH,)))
|
||||
if "lat_planner_solution" in outputs:
|
||||
self.mixture(outputs, "lat_planner_solution", _MixtureRecipe(0, 0, (c.IDX_N, c.LAT_PLANNER_SOLUTION_WIDTH)))
|
||||
if "desired_curvature" in outputs:
|
||||
self.mixture(outputs, "desired_curvature", _MixtureRecipe(0, 0, (c.DESIRED_CURV_WIDTH,)))
|
||||
for name in ("lead_prob", "lane_lines_prob", "meta"):
|
||||
self.binary(outputs, name)
|
||||
self.categorical(outputs, "desire_state", shape=(c.DESIRE_PRED_WIDTH,))
|
||||
self.categorical(outputs, "desire_pred", shape=(c.DESIRE_PRED_LEN, c.DESIRE_PRED_WIDTH))
|
||||
return outputs
|
||||
|
||||
|
||||
class PhaseParser(_TensorKitchen):
|
||||
def __init__(self, ignore_missing: bool = False):
|
||||
super().__init__(ignore_missing=ignore_missing)
|
||||
self._c = SplitModelConstants
|
||||
|
||||
def _has_mixture_heads(self, outputs: dict[str, np.ndarray], tensor_name: str, flat_width: int) -> bool:
|
||||
raw = self._grab(outputs, tensor_name)
|
||||
if raw is None:
|
||||
return False
|
||||
return raw.shape[1] != 2 * flat_width
|
||||
|
||||
def _decode_dynamic_family(self, outputs: dict[str, np.ndarray]) -> None:
|
||||
c = self._c
|
||||
if "lead" in outputs:
|
||||
uses_heads = self._has_mixture_heads(outputs, "lead", c.LEAD_MHP_SELECTION * c.LEAD_TRAJ_LEN * c.LEAD_WIDTH)
|
||||
self.mixture(outputs, "lead", _MixtureRecipe(
|
||||
c.LEAD_MHP_N if uses_heads else 0,
|
||||
c.LEAD_MHP_SELECTION if uses_heads else 0,
|
||||
(c.LEAD_TRAJ_LEN, c.LEAD_WIDTH) if uses_heads else (c.LEAD_MHP_SELECTION, c.LEAD_TRAJ_LEN, c.LEAD_WIDTH),
|
||||
))
|
||||
|
||||
if "plan" in outputs:
|
||||
uses_heads = self._has_mixture_heads(outputs, "plan", c.IDX_N * c.PLAN_WIDTH)
|
||||
self.mixture(outputs, "plan", _MixtureRecipe(
|
||||
c.PLAN_MHP_N if uses_heads else 0,
|
||||
c.PLAN_MHP_SELECTION if uses_heads else 0,
|
||||
(c.IDX_N, c.PLAN_WIDTH),
|
||||
))
|
||||
|
||||
if "planplus" in outputs:
|
||||
self.mixture(outputs, "planplus", _MixtureRecipe(0, 0, (c.IDX_N, c.PLAN_WIDTH)))
|
||||
|
||||
def _decode_policy_family(self, outputs: dict[str, np.ndarray]) -> None:
|
||||
c = self._c
|
||||
if "action" in outputs:
|
||||
self.mixture(outputs, "action", _MixtureRecipe(0, 0, (c.ACTION_WIDTH,)))
|
||||
if "desired_curvature" in outputs:
|
||||
self.mixture(outputs, "desired_curvature", _MixtureRecipe(0, 0, (c.DESIRED_CURV_WIDTH,)))
|
||||
if "desire_pred" in outputs:
|
||||
self.categorical(outputs, "desire_pred", shape=(c.DESIRE_PRED_LEN, c.DESIRE_PRED_WIDTH))
|
||||
if "desire_state" in outputs:
|
||||
self.categorical(outputs, "desire_state", shape=(c.DESIRE_PRED_WIDTH,))
|
||||
if "lane_lines" in outputs:
|
||||
self.mixture(outputs, "lane_lines", _MixtureRecipe(0, 0, (c.NUM_LANE_LINES, c.IDX_N, c.LANE_LINES_WIDTH)))
|
||||
if "lane_lines_prob" in outputs:
|
||||
self.binary(outputs, "lane_lines_prob")
|
||||
if "lead_prob" in outputs:
|
||||
self.binary(outputs, "lead_prob")
|
||||
if "lat_planner_solution" in outputs:
|
||||
self.mixture(outputs, "lat_planner_solution", _MixtureRecipe(0, 0, (c.IDX_N, c.LAT_PLANNER_SOLUTION_WIDTH)))
|
||||
if "meta" in outputs:
|
||||
self.binary(outputs, "meta")
|
||||
if "road_edges" in outputs:
|
||||
self.mixture(outputs, "road_edges", _MixtureRecipe(0, 0, (c.NUM_ROAD_EDGES, c.IDX_N, c.LANE_LINES_WIDTH)))
|
||||
if "sim_pose" in outputs:
|
||||
self.mixture(outputs, "sim_pose", _MixtureRecipe(0, 0, (c.POSE_WIDTH,)))
|
||||
|
||||
def parse_vision_outputs(self, outputs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
|
||||
c = self._c
|
||||
self.mixture(outputs, "pose", _MixtureRecipe(0, 0, (c.POSE_WIDTH,)))
|
||||
self.mixture(outputs, "wide_from_device_euler", _MixtureRecipe(0, 0, (c.WIDE_FROM_DEVICE_WIDTH,)))
|
||||
self.mixture(outputs, "road_transform", _MixtureRecipe(0, 0, (c.POSE_WIDTH,)))
|
||||
self._decode_dynamic_family(outputs)
|
||||
self._decode_policy_family(outputs)
|
||||
return outputs
|
||||
|
||||
def parse_policy_outputs(self, outputs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
|
||||
self._decode_dynamic_family(outputs)
|
||||
self._decode_policy_family(outputs)
|
||||
return outputs
|
||||
|
||||
def parse_outputs(self, outputs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
|
||||
return self.parse_policy_outputs(self.parse_vision_outputs(outputs))
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ArchiveParser",
|
||||
"PhaseParser",
|
||||
]
|
||||
62
iqpilot/selfdrive/iqmodeld/runtime/ort.py
Normal file
62
iqpilot/selfdrive/iqmodeld/runtime/ort.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 itertools
|
||||
|
||||
import numpy as np
|
||||
import onnx
|
||||
import onnxruntime as ort
|
||||
|
||||
|
||||
ORT_TYPES_TO_NP_TYPES = {
|
||||
"tensor(float16)": np.float16,
|
||||
"tensor(float)": np.float32,
|
||||
"tensor(uint8)": np.uint8,
|
||||
}
|
||||
|
||||
|
||||
def _promote_raw_half_blob(attribute):
|
||||
float32_values = np.frombuffer(attribute.raw_data, dtype=np.float16)
|
||||
attribute.data_type = 1
|
||||
attribute.raw_data = float32_values.astype(np.float32).tobytes()
|
||||
|
||||
|
||||
def _rewrite_tensor_io_types(model):
|
||||
for value_info in itertools.chain(model.graph.input, model.graph.output):
|
||||
if value_info.type.tensor_type.elem_type == 10:
|
||||
value_info.type.tensor_type.elem_type = 1
|
||||
|
||||
|
||||
def _rewrite_cast_nodes(model):
|
||||
for node in model.graph.node:
|
||||
if node.op_type == "Cast" and node.attribute[0].i == 10:
|
||||
node.attribute[0].i = 1
|
||||
for attribute in node.attribute:
|
||||
if hasattr(attribute, "t") and attribute.t.data_type == 10:
|
||||
_promote_raw_half_blob(attribute.t)
|
||||
|
||||
|
||||
def attributeproto_fp16_to_fp32(attr):
|
||||
_promote_raw_half_blob(attr)
|
||||
|
||||
|
||||
def convert_fp16_to_fp32(model):
|
||||
for initializer in model.graph.initializer:
|
||||
if initializer.data_type == 10:
|
||||
_promote_raw_half_blob(initializer)
|
||||
_rewrite_tensor_io_types(model)
|
||||
_rewrite_cast_nodes(model)
|
||||
return model.SerializeToString()
|
||||
|
||||
|
||||
def _cpu_session_options():
|
||||
options = ort.SessionOptions()
|
||||
options.intra_op_num_threads = 4
|
||||
options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
|
||||
options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
|
||||
return options
|
||||
|
||||
|
||||
def make_onnx_cpu_runner(model_path):
|
||||
model_blob = convert_fp16_to_fp32(onnx.load(model_path))
|
||||
return ort.InferenceSession(model_blob, _cpu_session_options(), providers=["CPUExecutionProvider"])
|
||||
23
iqpilot/selfdrive/iqmodeld/runtime/tinygrad.py
Normal file
23
iqpilot/selfdrive/iqmodeld/runtime/tinygrad.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from tinygrad.tensor import Tensor
|
||||
from tinygrad.helpers import to_mv
|
||||
|
||||
_PTR_STRIDE = 8
|
||||
_RAW_GPU_PTR_SLOT = 20
|
||||
_RAW_GPU_PTR_VIEW_BYTES = 0x100
|
||||
|
||||
|
||||
def _descriptor_pointer(opencl_address: int) -> int:
|
||||
return to_mv(opencl_address, _PTR_STRIDE).cast("Q")[0]
|
||||
|
||||
|
||||
def _raw_gpu_pointer(descriptor_pointer: int) -> int:
|
||||
return to_mv(descriptor_pointer, _RAW_GPU_PTR_VIEW_BYTES).cast("Q")[_RAW_GPU_PTR_SLOT]
|
||||
|
||||
|
||||
def qcom_tensor_from_opencl_address(opencl_address, shape, dtype):
|
||||
descriptor_pointer = _descriptor_pointer(opencl_address)
|
||||
device_pointer = _raw_gpu_pointer(descriptor_pointer)
|
||||
return Tensor.from_blob(device_pointer, shape, dtype=dtype, device="QCOM")
|
||||
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
|
||||
3
iqpilot/selfdrive/iqmodeld/tests/__init__.py
Normal file
3
iqpilot/selfdrive/iqmodeld/tests/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
95
iqpilot/selfdrive/iqmodeld/tests/dmon_lag/repro.cc
Normal file
95
iqpilot/selfdrive/iqmodeld/tests/dmon_lag/repro.cc
Normal file
@@ -0,0 +1,95 @@
|
||||
// Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kModelWidth = 320;
|
||||
constexpr int kModelHeight = 640;
|
||||
constexpr int kRetryWindow = 20;
|
||||
constexpr int kSlowThresholdMs = 10;
|
||||
|
||||
double millis_since_boot() {
|
||||
timespec stamp{};
|
||||
#ifdef CLOCK_BOOTTIME
|
||||
clock_gettime(CLOCK_BOOTTIME, &stamp);
|
||||
#else
|
||||
clock_gettime(CLOCK_MONOTONIC, &stamp);
|
||||
#endif
|
||||
return stamp.tv_sec * 1000.0 + stamp.tv_nsec * 1e-6;
|
||||
}
|
||||
|
||||
inline float identity_input(uint8_t value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
void pack_monitoring_tensor(uint8_t *nv12_frame, float *tensor_out) {
|
||||
const int half_h = kModelHeight / 2;
|
||||
const int half_w = kModelWidth / 2;
|
||||
const int plane_area = half_w * half_h;
|
||||
const int uv_base = kModelWidth * kModelHeight;
|
||||
|
||||
for (int row = 0; row < half_h; ++row) {
|
||||
for (int col = 0; col < half_w; ++col) {
|
||||
const int slot = col * half_h + row;
|
||||
const int y_row = row * 2;
|
||||
const int y_col = col * 2;
|
||||
|
||||
tensor_out[slot] = identity_input(nv12_frame[(y_row * kModelWidth) + y_col]);
|
||||
tensor_out[slot + plane_area] = identity_input(nv12_frame[((y_row + 1) * kModelWidth) + y_col]);
|
||||
tensor_out[slot + (plane_area * 2)] = identity_input(nv12_frame[(y_row * kModelWidth) + y_col + 1]);
|
||||
tensor_out[slot + (plane_area * 3)] = identity_input(nv12_frame[((y_row + 1) * kModelWidth) + y_col + 1]);
|
||||
tensor_out[slot + (plane_area * 4)] = identity_input(nv12_frame[uv_base + (row * half_w) + col]);
|
||||
tensor_out[slot + (plane_area * 5)] = identity_input(nv12_frame[uv_base + plane_area + (row * half_w) + col]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double average_runtime_ms(uint8_t *nv12_frame, float *tensor_out) {
|
||||
double total_ms = 0.0;
|
||||
for (int i = 0; i < kRetryWindow; ++i) {
|
||||
const double start_ms = millis_since_boot();
|
||||
pack_monitoring_tensor(nv12_frame, tensor_out);
|
||||
total_ms += millis_since_boot() - start_ms;
|
||||
}
|
||||
return total_ms / static_cast<double>(kRetryWindow);
|
||||
}
|
||||
|
||||
void dump_stall_trace(uint8_t *nv12_frame, float *tensor_out) {
|
||||
for (int i = 0; i < 200; ++i) {
|
||||
const double start_ms = millis_since_boot();
|
||||
pack_monitoring_tensor(nv12_frame, tensor_out);
|
||||
printf("%.2f ", millis_since_boot() - start_ms);
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const size_t nv12_bytes = kModelWidth * kModelHeight * 3 / 2;
|
||||
const size_t tensor_floats = (kModelWidth / 2) * (kModelHeight / 2) * 6;
|
||||
|
||||
while (true) {
|
||||
auto *nv12_frame = static_cast<uint8_t *>(malloc(nv12_bytes));
|
||||
auto *tensor_out = static_cast<float *>(malloc(tensor_floats * sizeof(float)));
|
||||
printf("allocate -- %p 0x%zx -- %p 0x%zx\n", nv12_frame, nv12_bytes, tensor_out, tensor_floats * sizeof(float));
|
||||
|
||||
const double mean_ms = average_runtime_ms(nv12_frame, tensor_out);
|
||||
if (mean_ms > kSlowThresholdMs) {
|
||||
printf("HIT %.2f\n", mean_ms);
|
||||
printf("BAD\n");
|
||||
dump_stall_trace(nv12_frame, tensor_out);
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("got %.2f\n", mean_ms);
|
||||
}
|
||||
}
|
||||
81
iqpilot/selfdrive/iqmodeld/tests/test_action_dispatch.py
Normal file
81
iqpilot/selfdrive/iqmodeld/tests/test_action_dispatch.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
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
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from iqpilot.cereal import log
|
||||
|
||||
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):
|
||||
base = dict(
|
||||
PLANPLUS_CONTROL=1.0,
|
||||
LONG_SMOOTH_SECONDS=0.3,
|
||||
LAT_SMOOTH_SECONDS=0.1,
|
||||
MIN_LAT_CONTROL_SPEED=0.3,
|
||||
mlsim=True,
|
||||
generation=12,
|
||||
constants=SimpleNamespace(T_IDXS=np.arange(100), DESIRE_LEN=8),
|
||||
)
|
||||
base.update(overrides)
|
||||
return SimpleNamespace(**base)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("control", "vego", "factor"),
|
||||
[
|
||||
(0.55, 20.0, 1.0),
|
||||
(1.0, 25.0, 0.75),
|
||||
(1.5, 25.1, 0.75),
|
||||
(2.0, 20.0, 1.0),
|
||||
],
|
||||
)
|
||||
def test_planplus_merge_matches_speed_gate(control: float, vego: float, factor: float):
|
||||
state = _fake_state(PLANPLUS_CONTROL=control)
|
||||
base = np.random.rand(1, 100, 15).astype(np.float32)
|
||||
extra = np.random.rand(1, 100, 15).astype(np.float32)
|
||||
merged = _merged_plan(state, {"plan": base, "planplus": extra}, vego)
|
||||
expected = base[0] + (control * factor) * extra[0]
|
||||
np.testing.assert_allclose(merged, expected, rtol=1e-6, atol=1e-6)
|
||||
|
||||
|
||||
def test_action_dispatch_uses_merged_plan_for_longitudinal_choice(monkeypatch: pytest.MonkeyPatch):
|
||||
state = _fake_state()
|
||||
previous = log.ModelDataV2.Action()
|
||||
recorded_velocity: list[np.ndarray] = []
|
||||
|
||||
def fake_accel(plan_vel, plan_accel, t_idxs, action_t=0.0):
|
||||
recorded_velocity.append(plan_vel.copy())
|
||||
return 0.0, False
|
||||
|
||||
monkeypatch.setattr(iqmodeld_daemon, "get_accel_from_plan", fake_accel)
|
||||
monkeypatch.setattr(iqmodeld_daemon, "pick_curvature", lambda *args: 0.0)
|
||||
|
||||
plan = np.random.rand(1, 100, 15).astype(np.float32)
|
||||
planplus = np.random.rand(1, 100, 15).astype(np.float32)
|
||||
outputs = {"plan": plan.copy(), "planplus": planplus.copy()}
|
||||
|
||||
NeuralEngineState.get_action_from_model(state, outputs, previous, 0.0, 0.0, 25.0)
|
||||
expected = plan[0, :, Plan.VELOCITY][:, 0] + 0.75 * planplus[0, :, Plan.VELOCITY][:, 0]
|
||||
np.testing.assert_allclose(recorded_velocity[0], expected, rtol=1e-5, atol=1e-6)
|
||||
|
||||
|
||||
def test_action_dispatch_honors_direct_action_outputs():
|
||||
state = _fake_state(mlsim=False, generation=9)
|
||||
previous = log.ModelDataV2.Action(desiredCurvature=0.0, desiredAcceleration=0.0, shouldStop=False)
|
||||
outputs = {"action": np.array([[4.0, -0.25]], dtype=np.float32)}
|
||||
action = NeuralEngineState.get_action_from_model(state, outputs, previous, 0.0, 0.0, 10.0)
|
||||
expected_accel = smooth_value(-0.25, previous.desiredAcceleration, state.LONG_SMOOTH_SECONDS)
|
||||
expected_curvature = smooth_value(0.04, previous.desiredCurvature, state.LAT_SMOOTH_SECONDS)
|
||||
assert action.desiredAcceleration == pytest.approx(expected_accel)
|
||||
assert action.desiredCurvature == pytest.approx(expected_curvature)
|
||||
assert action.shouldStop is False
|
||||
188
iqpilot/selfdrive/iqmodeld/tests/test_combined_split_runner.py
Normal file
188
iqpilot/selfdrive/iqmodeld/tests/test_combined_split_runner.py
Normal file
@@ -0,0 +1,188 @@
|
||||
"""
|
||||
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
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
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
|
||||
class _TypeWrap:
|
||||
raw: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Artifact:
|
||||
fileName: str
|
||||
|
||||
|
||||
class _Model:
|
||||
def __init__(self, model_type: int, artifact_name: str):
|
||||
self.type = _TypeWrap(model_type)
|
||||
self.artifact = _Artifact(artifact_name)
|
||||
|
||||
|
||||
class _Override:
|
||||
def __init__(self, key: str, value: str):
|
||||
self.key = key
|
||||
self.value = value
|
||||
|
||||
|
||||
class _Bundle:
|
||||
def __init__(self, models: list[_Model], overrides: list[_Override] | None = None, generation: int = 10):
|
||||
self.models = models
|
||||
self.overrides = overrides or []
|
||||
self.generation = generation
|
||||
|
||||
|
||||
class _FakeTensor:
|
||||
def __init__(self, values):
|
||||
self._values = np.asarray(values, dtype=np.float32)
|
||||
|
||||
def numpy(self):
|
||||
return self._values
|
||||
|
||||
|
||||
class _FakeVisionBuf:
|
||||
width = 1928
|
||||
height = 1208
|
||||
data = memoryview(b"\x00" * 64)
|
||||
|
||||
|
||||
def _slice_pack(outputs: dict[str, np.ndarray]) -> tuple[np.ndarray, dict[str, slice]]:
|
||||
chunks = []
|
||||
slices: dict[str, slice] = {}
|
||||
cursor = 0
|
||||
for name, value in outputs.items():
|
||||
flat = value.reshape(-1)
|
||||
slices[name] = slice(cursor, cursor + flat.size)
|
||||
chunks.append(flat)
|
||||
cursor += flat.size
|
||||
return np.concatenate(chunks).astype(np.float32), slices
|
||||
|
||||
|
||||
def test_resolve_combined_split_artifact_prefers_override(tmp_path: Path, monkeypatch):
|
||||
bundle = _Bundle(
|
||||
[_Model(ModelType.vision, "driving_vision_demo_tinygrad.pkl"), _Model(ModelType.policy, "driving_policy_demo_tinygrad.pkl")],
|
||||
overrides=[_Override("combinedRuntimeArtifact", "driving_combined_demo.pkl")],
|
||||
)
|
||||
expected = tmp_path / "driving_combined_demo.pkl"
|
||||
expected.write_bytes(b"iq")
|
||||
|
||||
monkeypatch.setattr("iqpilot.selfdrive.iqmodeld.models.combined_artifact._MODEL_ROOT", tmp_path)
|
||||
|
||||
assert resolve_combined_split_artifact(bundle) == expected
|
||||
|
||||
|
||||
def test_get_model_runner_prefers_combined_split_artifact(monkeypatch):
|
||||
bundle = _Bundle([
|
||||
_Model(ModelType.vision, "driving_vision_demo_tinygrad.pkl"),
|
||||
_Model(ModelType.policy, "driving_policy_demo_tinygrad.pkl"),
|
||||
], generation=11)
|
||||
|
||||
marker = object()
|
||||
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)
|
||||
|
||||
assert runner_helpers.get_model_runner() is marker
|
||||
|
||||
|
||||
def test_get_model_runner_keeps_split_bundle_on_existing_runner_without_combined_artifact(monkeypatch):
|
||||
bundle = _Bundle([
|
||||
_Model(ModelType.vision, "driving_vision_demo_tinygrad.pkl"),
|
||||
_Model(ModelType.policy, "driving_policy_demo_tinygrad.pkl"),
|
||||
], generation=12)
|
||||
|
||||
marker = object()
|
||||
monkeypatch.setattr(runner_helpers, "_fetch_bundle", lambda: bundle)
|
||||
monkeypatch.setattr(runner_helpers, "has_combined_split_artifact", lambda _: False)
|
||||
monkeypatch.setattr(tinygrad_runner_mod, "TinygradSplitRunner", lambda: marker)
|
||||
|
||||
assert runner_helpers.get_model_runner() is marker
|
||||
|
||||
|
||||
def test_combined_split_runner_parses_single_policy_payload(monkeypatch):
|
||||
vision_raw = _phase_sample(np.random.default_rng(11))
|
||||
policy_raw = _phase_sample(np.random.default_rng(17))
|
||||
vision_blob, vision_slices = _slice_pack(vision_raw)
|
||||
policy_blob, policy_slices = _slice_pack(policy_raw)
|
||||
|
||||
runner = TinygradCombinedSplitRunner.__new__(TinygradCombinedSplitRunner)
|
||||
runner._vision_meta = {
|
||||
"input_shapes": {"img": (1, 12, 128, 256), "big_img": (1, 12, 128, 256)},
|
||||
"output_slices": vision_slices,
|
||||
}
|
||||
runner._meta_by_role = {
|
||||
"vision": runner._vision_meta,
|
||||
"policy": {
|
||||
"input_shapes": {
|
||||
"features_buffer": (1, 25, 512),
|
||||
"desire_pulse": (1, 25, 8),
|
||||
"traffic_convention": (1, 2),
|
||||
"action_t": (1, 2),
|
||||
},
|
||||
"output_slices": policy_slices,
|
||||
},
|
||||
}
|
||||
runner._policy_roles = ["policy"]
|
||||
runner._desired_key = "desire_pulse"
|
||||
runner._road_key = "img"
|
||||
runner._wide_key = "big_img"
|
||||
runner._extra_policy_keys = []
|
||||
runner._queue_tensors = {
|
||||
"img_q": object(),
|
||||
"big_img_q": object(),
|
||||
"feat_q": object(),
|
||||
"desire_q": object(),
|
||||
"tfm": object(),
|
||||
"big_tfm": object(),
|
||||
"desire": object(),
|
||||
"traffic_convention": object(),
|
||||
"action_t": object(),
|
||||
}
|
||||
runner._numpy_state = {
|
||||
"tfm": np.zeros((3, 3), dtype=np.float32),
|
||||
"big_tfm": np.zeros((3, 3), dtype=np.float32),
|
||||
"desire": np.zeros(8, dtype=np.float32),
|
||||
"traffic_convention": np.zeros((1, 2), dtype=np.float32),
|
||||
"action_t": np.zeros((1, 2), dtype=np.float32),
|
||||
}
|
||||
runner._camera_shape = (1928, 1208)
|
||||
runner._camera_programs = {
|
||||
(1928, 1208): {"stage_inputs": lambda **kwargs: ("road", "wide")},
|
||||
}
|
||||
runner._execute_bundle = lambda **kwargs: (_FakeTensor(vision_blob), _FakeTensor(policy_blob))
|
||||
runner._parser = PhaseParser()
|
||||
runner._last_desire = np.zeros(8, dtype=np.float32)
|
||||
runner._blob_cache = {}
|
||||
|
||||
monkeypatch.setattr(TinygradCombinedSplitRunner, "_allocate_runtime_state", lambda self, w, h: None)
|
||||
monkeypatch.setattr(TinygradCombinedSplitRunner, "_frame_blob", lambda self, name, buf: object())
|
||||
|
||||
outputs = runner.run_fused(
|
||||
{"img": _FakeVisionBuf(), "big_img": _FakeVisionBuf()},
|
||||
{"img": np.eye(3, dtype=np.float32), "big_img": np.eye(3, dtype=np.float32)},
|
||||
{
|
||||
"desire_pulse": np.array([1, 0, 0, 0, 0, 0, 0, 0], dtype=np.float32),
|
||||
"traffic_convention": np.zeros((1, 2), dtype=np.float32),
|
||||
"action_t": np.zeros((1, 2), dtype=np.float32),
|
||||
},
|
||||
)
|
||||
|
||||
assert "pose" in outputs
|
||||
assert "plan" in outputs
|
||||
assert outputs["plan"].shape == (1, 33, 15)
|
||||
assert outputs["action"].shape == (1, 2)
|
||||
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
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.tools.compile_supercombo import (
|
||||
_captured_devices,
|
||||
_validate_pose_outputs,
|
||||
)
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.supercombo_runner import _captured_queue_depth
|
||||
|
||||
|
||||
class _Captured:
|
||||
def __init__(self, expected_input_info):
|
||||
self.expected_input_info = expected_input_info
|
||||
|
||||
|
||||
class _FakeJit:
|
||||
def __init__(self, expected_input_info):
|
||||
self.captured = _Captured(expected_input_info)
|
||||
|
||||
|
||||
def test_captured_queue_helpers_extract_depth_and_device():
|
||||
infos = [
|
||||
("noop", (), "uchar", "QCOM"),
|
||||
("reshape(arg=None, src=(noop, stack(arg=None, src=(const(arg=5), const(arg=6), const(arg=128), const(arg=256)))))", (), "uchar", "QCOM"),
|
||||
("reshape(arg=None, src=(noop, const(arg=3)))", (), "float", "NPY"),
|
||||
]
|
||||
fake_jit = _FakeJit(infos)
|
||||
|
||||
assert _captured_queue_depth(fake_jit) == 5
|
||||
assert _captured_devices(fake_jit) == {"QCOM", "NPY"}
|
||||
|
||||
|
||||
def test_validate_pose_outputs_accepts_sane_odometry_payload():
|
||||
outputs = {
|
||||
"pose": np.array([[1.0, 0.5, 0.25, 0.1, 0.2, 0.3]], dtype=np.float32),
|
||||
"pose_stds": np.array([[0.5, 0.4, 0.3, 0.2, 0.2, 0.2]], dtype=np.float32),
|
||||
"wide_from_device_euler": np.array([[0.1, 0.2, 0.3]], dtype=np.float32),
|
||||
"wide_from_device_euler_stds": np.array([[0.2, 0.2, 0.2]], dtype=np.float32),
|
||||
"road_transform": np.array([[0.5, 0.4, 0.3, 0.2, 0.1, 0.0]], dtype=np.float32),
|
||||
"road_transform_stds": np.array([[0.3, 0.3, 0.3, 0.2, 0.2, 0.2]], dtype=np.float32),
|
||||
}
|
||||
|
||||
_validate_pose_outputs(outputs)
|
||||
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
|
||||
126
iqpilot/selfdrive/iqmodeld/tests/test_iqmodeld_contracts.py
Normal file
126
iqpilot/selfdrive/iqmodeld/tests/test_iqmodeld_contracts.py
Normal file
@@ -0,0 +1,126 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
|
||||
import iqpilot.cereal.messaging as messaging
|
||||
import numpy as np
|
||||
from iqpilot.cereal import log
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.config import Meta, ModelConstants
|
||||
from iqpilot.selfdrive.iqmodeld.messaging import (
|
||||
DrivePacketMemory,
|
||||
pick_curvature,
|
||||
populate_drive_messages,
|
||||
populate_odometry_message,
|
||||
)
|
||||
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]:
|
||||
return {
|
||||
"plan": rng.standard_normal((1, ModelConstants.PLAN_MHP_N * (2 * ModelConstants.IDX_N * ModelConstants.PLAN_WIDTH + ModelConstants.PLAN_MHP_SELECTION)), dtype=np.float32),
|
||||
"lane_lines": rng.standard_normal((1, 2 * ModelConstants.NUM_LANE_LINES * ModelConstants.IDX_N * ModelConstants.LANE_LINES_WIDTH), dtype=np.float32),
|
||||
"road_edges": rng.standard_normal((1, 2 * ModelConstants.NUM_ROAD_EDGES * ModelConstants.IDX_N * ModelConstants.LANE_LINES_WIDTH), dtype=np.float32),
|
||||
"pose": rng.standard_normal((1, 2 * ModelConstants.POSE_WIDTH), dtype=np.float32),
|
||||
"road_transform": rng.standard_normal((1, 2 * ModelConstants.POSE_WIDTH), dtype=np.float32),
|
||||
"sim_pose": rng.standard_normal((1, 2 * ModelConstants.POSE_WIDTH), dtype=np.float32),
|
||||
"wide_from_device_euler": rng.standard_normal((1, 2 * ModelConstants.WIDE_FROM_DEVICE_WIDTH), dtype=np.float32),
|
||||
"lead": rng.standard_normal((1, ModelConstants.LEAD_MHP_N * (2 * ModelConstants.LEAD_TRAJ_LEN * ModelConstants.LEAD_WIDTH + ModelConstants.LEAD_MHP_SELECTION)), dtype=np.float32),
|
||||
"lat_planner_solution": rng.standard_normal((1, 2 * ModelConstants.IDX_N * ModelConstants.LAT_PLANNER_SOLUTION_WIDTH), dtype=np.float32),
|
||||
"desired_curvature": rng.standard_normal((1, 2 * ModelConstants.DESIRED_CURV_WIDTH), dtype=np.float32),
|
||||
"lead_prob": rng.standard_normal((1, ModelConstants.LEAD_MHP_SELECTION), dtype=np.float32),
|
||||
"lane_lines_prob": rng.standard_normal((1, ModelConstants.NUM_LANE_LINES * 2), dtype=np.float32),
|
||||
"meta": rng.standard_normal((1, 55), dtype=np.float32),
|
||||
"desire_state": rng.standard_normal((1, ModelConstants.DESIRE_PRED_WIDTH), dtype=np.float32),
|
||||
"desire_pred": rng.standard_normal((1, ModelConstants.DESIRE_PRED_LEN * ModelConstants.DESIRE_PRED_WIDTH), dtype=np.float32),
|
||||
}
|
||||
|
||||
|
||||
def _phase_sample(rng: np.random.Generator) -> dict[str, np.ndarray]:
|
||||
c = SplitModelConstants
|
||||
return {
|
||||
"pose": rng.standard_normal((1, 2 * c.POSE_WIDTH), dtype=np.float32),
|
||||
"wide_from_device_euler": rng.standard_normal((1, 2 * c.WIDE_FROM_DEVICE_WIDTH), dtype=np.float32),
|
||||
"road_transform": rng.standard_normal((1, 2 * c.POSE_WIDTH), dtype=np.float32),
|
||||
"lead": rng.standard_normal((1, c.LEAD_MHP_N * (2 * c.LEAD_TRAJ_LEN * c.LEAD_WIDTH + c.LEAD_MHP_SELECTION)), dtype=np.float32),
|
||||
"plan": rng.standard_normal((1, c.PLAN_MHP_N * (2 * c.IDX_N * c.PLAN_WIDTH + c.PLAN_MHP_SELECTION)), dtype=np.float32),
|
||||
"planplus": rng.standard_normal((1, 2 * c.IDX_N * c.PLAN_WIDTH), dtype=np.float32),
|
||||
"action": rng.standard_normal((1, 2 * c.ACTION_WIDTH), dtype=np.float32),
|
||||
"desired_curvature": rng.standard_normal((1, 2 * c.DESIRED_CURV_WIDTH), dtype=np.float32),
|
||||
"desire_pred": rng.standard_normal((1, c.DESIRE_PRED_LEN * c.DESIRE_PRED_WIDTH), dtype=np.float32),
|
||||
"desire_state": rng.standard_normal((1, c.DESIRE_PRED_WIDTH), dtype=np.float32),
|
||||
"lane_lines": rng.standard_normal((1, 2 * c.NUM_LANE_LINES * c.IDX_N * c.LANE_LINES_WIDTH), dtype=np.float32),
|
||||
"lane_lines_prob": rng.standard_normal((1, c.NUM_LANE_LINES * 2), dtype=np.float32),
|
||||
"lead_prob": rng.standard_normal((1, c.LEAD_MHP_SELECTION), dtype=np.float32),
|
||||
"lat_planner_solution": rng.standard_normal((1, 2 * c.IDX_N * c.LAT_PLANNER_SOLUTION_WIDTH), dtype=np.float32),
|
||||
"meta": rng.standard_normal((1, 55), dtype=np.float32),
|
||||
"road_edges": rng.standard_normal((1, 2 * c.NUM_ROAD_EDGES * c.IDX_N * c.LANE_LINES_WIDTH), dtype=np.float32),
|
||||
"sim_pose": rng.standard_normal((1, 2 * c.POSE_WIDTH), dtype=np.float32),
|
||||
}
|
||||
|
||||
|
||||
def test_archive_parser_contract_snapshot():
|
||||
outputs = ArchiveParser().parse_outputs(copy.deepcopy(_archive_sample(np.random.default_rng(7))))
|
||||
|
||||
assert outputs["plan"].shape == (1, 33, 15)
|
||||
assert outputs["lane_lines"].shape == (1, 4, 33, 2)
|
||||
assert outputs["road_edges"].shape == (1, 2, 33, 2)
|
||||
assert outputs["desire_pred"].shape == (1, 4, 8)
|
||||
|
||||
np.testing.assert_allclose(outputs["pose"][0, 0], 0.45617363, rtol=1e-6, atol=1e-6)
|
||||
np.testing.assert_allclose(outputs["lane_lines_prob"][0, 2], 0.85733712, rtol=1e-6, atol=1e-6)
|
||||
np.testing.assert_allclose(outputs["desire_state"][0, 0], 0.44964141, rtol=1e-6, atol=1e-6)
|
||||
np.testing.assert_allclose(outputs["lead_prob"][0, 0], 0.21613698, rtol=1e-6, atol=1e-6)
|
||||
|
||||
|
||||
def test_phase_parser_contract_snapshot():
|
||||
raw = _phase_sample(np.random.default_rng(23))
|
||||
outputs = {**PhaseParser().parse_vision_outputs(copy.deepcopy(raw)), **PhaseParser().parse_policy_outputs(copy.deepcopy(raw))}
|
||||
|
||||
assert outputs["plan"].shape == (1, 33, 15)
|
||||
assert outputs["action"].shape == (1, 2)
|
||||
assert outputs["desired_curvature"].shape == (1, 1)
|
||||
assert outputs["road_edges"].shape == (1, 2, 33, 2)
|
||||
|
||||
np.testing.assert_allclose(outputs["plan"][0, 0, 0], 0.09684439, rtol=1e-6, atol=1e-6)
|
||||
np.testing.assert_allclose(outputs["action"][0, 0], 0.25458091, rtol=1e-6, atol=1e-6)
|
||||
np.testing.assert_allclose(outputs["desired_curvature"][0, 0], -0.97072351, rtol=1e-6, atol=1e-6)
|
||||
np.testing.assert_allclose(outputs["lane_lines_prob"][0, 0], 0.20073657, rtol=1e-6, atol=1e-6)
|
||||
|
||||
|
||||
def test_message_population_contract_snapshot():
|
||||
raw = _phase_sample(np.random.default_rng(23))
|
||||
outputs = {**PhaseParser().parse_vision_outputs(copy.deepcopy(raw)), **PhaseParser().parse_policy_outputs(copy.deepcopy(raw))}
|
||||
action = log.ModelDataV2.Action(desiredCurvature=0.031, desiredAcceleration=-0.12, shouldStop=False)
|
||||
|
||||
driving_msg = messaging.new_message("drivingModelData")
|
||||
model_msg = messaging.new_message("modelV2")
|
||||
odometry_msg = messaging.new_message("cameraOdometry")
|
||||
memory = DrivePacketMemory()
|
||||
|
||||
populate_drive_messages(
|
||||
driving_msg, model_msg, outputs, action, memory,
|
||||
2468, 2470, 2480, 0.05, 123456789, 0.014, True, Meta,
|
||||
)
|
||||
populate_odometry_message(odometry_msg, outputs, 2468, 0, 123456789, True)
|
||||
|
||||
np.testing.assert_allclose(driving_msg.drivingModelData.laneLineMeta.leftY, -0.21672775, rtol=1e-6, atol=1e-6)
|
||||
np.testing.assert_allclose(model_msg.modelV2.meta.engagedProb, 0.64853197, rtol=1e-6, atol=1e-6)
|
||||
np.testing.assert_allclose(odometry_msg.cameraOdometry.trans[0], 0.0360266, rtol=1e-6, atol=1e-6)
|
||||
assert int(model_msg.modelV2.confidence.raw) == 2
|
||||
|
||||
|
||||
def test_curvature_selection_contract_snapshot():
|
||||
raw = _phase_sample(np.random.default_rng(23))
|
||||
outputs = {**PhaseParser().parse_vision_outputs(copy.deepcopy(raw)), **PhaseParser().parse_policy_outputs(copy.deepcopy(raw))}
|
||||
plan_rows = outputs["plan"][0]
|
||||
|
||||
direct = pick_curvature(outputs, plan_rows, 27.5, 0.8, synthetic_lane_logic=False)
|
||||
fallback = pick_curvature(outputs, plan_rows, 27.5, 0.8, synthetic_lane_logic=True)
|
||||
|
||||
np.testing.assert_allclose(direct, -0.97072351, rtol=1e-6, atol=1e-6)
|
||||
np.testing.assert_allclose(fallback, -0.0689389, rtol=1e-6, atol=1e-6)
|
||||
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)
|
||||
77
iqpilot/selfdrive/iqmodeld/tests/test_model_runner_smoke.py
Normal file
77
iqpilot/selfdrive/iqmodeld/tests/test_model_runner_smoke.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
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
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from tinygrad.tensor import Tensor
|
||||
|
||||
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"
|
||||
|
||||
|
||||
@dataclass
|
||||
class _TypeWrap:
|
||||
raw: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Artifact:
|
||||
fileName: str
|
||||
|
||||
|
||||
class _Model:
|
||||
def __init__(self, model_type: int, artifact_name: str, metadata_name: str):
|
||||
self.type = _TypeWrap(model_type)
|
||||
self.artifact = _Artifact(artifact_name)
|
||||
self.metadata = _Artifact(metadata_name)
|
||||
|
||||
|
||||
class _Bundle:
|
||||
def __init__(self, models: list[_Model], is_20hz: bool = False):
|
||||
self.models = models
|
||||
self.is20hz = is_20hz
|
||||
|
||||
|
||||
def _seed_runner_inputs(runner: TinygradRunner) -> None:
|
||||
for name, shape in runner.input_shapes.items():
|
||||
runner.inputs[name] = Tensor(
|
||||
np.zeros(shape, dtype=np.float32),
|
||||
device=runner.input_to_device[name],
|
||||
dtype=runner.input_to_dtype[name],
|
||||
).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"),
|
||||
_Model(ModelType.policy, "driving_policy_c210m_tinygrad.pkl", "driving_policy_c210m_metadata.pkl"),
|
||||
])
|
||||
|
||||
monkeypatch.setattr(bundle_helpers, "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)
|
||||
|
||||
vision_runner = TinygradRunner(ModelType.vision)
|
||||
_seed_runner_inputs(vision_runner)
|
||||
vision_outputs = vision_runner.run_model()
|
||||
assert "pose" in vision_outputs
|
||||
assert "lane_lines" in vision_outputs
|
||||
|
||||
policy_runner = TinygradRunner(ModelType.policy)
|
||||
_seed_runner_inputs(policy_runner)
|
||||
policy_outputs = policy_runner.run_model()
|
||||
assert "plan" in policy_outputs
|
||||
assert "desire_state" in policy_outputs
|
||||
21
iqpilot/selfdrive/iqmodeld/tests/test_public_surface.py
Normal file
21
iqpilot/selfdrive/iqmodeld/tests/test_public_surface.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
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():
|
||||
assert hasattr(messaging, "DrivePacketMemory")
|
||||
assert hasattr(messaging, "pick_curvature")
|
||||
assert hasattr(messaging, "populate_drive_messages")
|
||||
assert hasattr(messaging, "populate_odometry_message")
|
||||
|
||||
assert hasattr(parser, "ArchiveParser")
|
||||
assert hasattr(parser, "PhaseParser")
|
||||
|
||||
assert hasattr(metadata, "select_meta_layout")
|
||||
assert hasattr(metadata, "build_metadata_record")
|
||||
|
||||
assert CaptureStamp.__name__ == "CaptureStamp"
|
||||
assert NeuralEngineState.__name__ == "NeuralEngineState"
|
||||
171
iqpilot/selfdrive/iqmodeld/tests/test_selector_share_smoke.py
Normal file
171
iqpilot/selfdrive/iqmodeld/tests/test_selector_share_smoke.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
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
|
||||
from tinygrad.nn.onnx import OnnxRunner
|
||||
from tinygrad.tensor import Tensor
|
||||
|
||||
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"))
|
||||
|
||||
|
||||
@dataclass
|
||||
class _TypeWrap:
|
||||
raw: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Artifact:
|
||||
fileName: str
|
||||
|
||||
|
||||
class _Model:
|
||||
def __init__(self, model_type: int, artifact_name: str, metadata_name: str):
|
||||
self.type = _TypeWrap(model_type)
|
||||
self.artifact = _Artifact(artifact_name)
|
||||
self.metadata = _Artifact(metadata_name)
|
||||
|
||||
|
||||
class _Bundle:
|
||||
def __init__(self, models: list[_Model], is_20hz: bool = False):
|
||||
self.models = models
|
||||
self.is20hz = is_20hz
|
||||
|
||||
|
||||
def _find_selector_dirs(limit: int = 3, require_onnx: bool = False) -> list[Path]:
|
||||
found: list[Path] = []
|
||||
if not SHARE_ROOT.is_dir():
|
||||
return found
|
||||
|
||||
for bundle_dir in sorted(SHARE_ROOT.iterdir()):
|
||||
if not bundle_dir.is_dir():
|
||||
continue
|
||||
vision = next(bundle_dir.glob("driving_vision*_tinygrad.pkl"), None)
|
||||
policy = next(bundle_dir.glob("driving_policy*_tinygrad.pkl"), None)
|
||||
vision_meta = next(bundle_dir.glob("driving_vision*_metadata.pkl"), None)
|
||||
policy_meta = next(bundle_dir.glob("driving_policy*_metadata.pkl"), None)
|
||||
has_onnx = (bundle_dir / "driving_vision.onnx").is_file() and (bundle_dir / "driving_policy.onnx").is_file()
|
||||
if vision and policy and vision_meta and policy_meta and (has_onnx or not require_onnx):
|
||||
found.append(bundle_dir)
|
||||
if len(found) >= limit:
|
||||
break
|
||||
return found
|
||||
|
||||
|
||||
def _seed_runner_inputs(runner: TinygradRunner) -> None:
|
||||
for name, shape in runner.input_shapes.items():
|
||||
runner.inputs[name] = Tensor(
|
||||
np.zeros(shape, dtype=np.float32),
|
||||
device=runner.input_to_device[name],
|
||||
dtype=runner.input_to_dtype[name],
|
||||
).realize()
|
||||
|
||||
|
||||
def _bundle_for_dir(bundle_dir: Path) -> _Bundle:
|
||||
vision = next(bundle_dir.glob("driving_vision*_tinygrad.pkl"))
|
||||
policy = next(bundle_dir.glob("driving_policy*_tinygrad.pkl"))
|
||||
vision_meta = next(bundle_dir.glob("driving_vision*_metadata.pkl"))
|
||||
policy_meta = next(bundle_dir.glob("driving_policy*_metadata.pkl"))
|
||||
return _Bundle([
|
||||
_Model(ModelType.vision, vision.name, vision_meta.name),
|
||||
_Model(ModelType.policy, policy.name, policy_meta.name),
|
||||
])
|
||||
|
||||
|
||||
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)
|
||||
|
||||
vision_runner = TinygradRunner(ModelType.vision)
|
||||
_seed_runner_inputs(vision_runner)
|
||||
vision_outputs = vision_runner.run_model()
|
||||
|
||||
policy_runner = TinygradRunner(ModelType.policy)
|
||||
_seed_runner_inputs(policy_runner)
|
||||
policy_outputs = policy_runner.run_model()
|
||||
|
||||
return vision_outputs, policy_outputs
|
||||
|
||||
|
||||
def _run_onnx_bundle(bundle_dir: Path):
|
||||
vision_session = OnnxRunner(bundle_dir / "driving_vision.onnx")
|
||||
policy_session = OnnxRunner(bundle_dir / "driving_policy.onnx")
|
||||
|
||||
def seed_inputs(session):
|
||||
seeded = {}
|
||||
for name, spec in session.graph_inputs.items():
|
||||
dtype_text = str(spec.dtype).lower()
|
||||
if "uchar" in dtype_text or "uint8" in dtype_text:
|
||||
seeded[name] = Tensor(np.zeros(spec.shape, dtype=np.uint8))
|
||||
elif "half" in dtype_text or "float16" in dtype_text:
|
||||
seeded[name] = Tensor(np.zeros(spec.shape, dtype=np.float16))
|
||||
else:
|
||||
seeded[name] = Tensor(np.zeros(spec.shape, dtype=np.float32))
|
||||
return seeded
|
||||
|
||||
return (
|
||||
vision_session(seed_inputs(vision_session))["outputs"].numpy().flatten(),
|
||||
policy_session(seed_inputs(policy_session))["outputs"].numpy().flatten(),
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
|
||||
for bundle_dir in selector_dirs:
|
||||
vision_raw, policy_raw = _run_onnx_bundle(bundle_dir)
|
||||
assert vision_raw.size > 0
|
||||
assert policy_raw.size > 0
|
||||
|
||||
|
||||
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
|
||||
|
||||
for bundle_dir in selector_dirs:
|
||||
attempted += 1
|
||||
try:
|
||||
vision_outputs, policy_outputs = _run_tinygrad_bundle(bundle_dir, monkeypatch)
|
||||
except AssertionError as exc:
|
||||
if "Model was built on C3 or C3X" in str(exc):
|
||||
continue
|
||||
raise
|
||||
except FileNotFoundError as exc:
|
||||
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
|
||||
executed += 1
|
||||
if executed >= 3:
|
||||
break
|
||||
|
||||
if executed == 0:
|
||||
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()
|
||||
@@ -0,0 +1,254 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
class _Captured:
|
||||
def __init__(self, expected_names):
|
||||
self.expected_names = expected_names
|
||||
|
||||
|
||||
class _FakeJit:
|
||||
def __init__(self, expected_names):
|
||||
self.captured = _Captured(expected_names)
|
||||
|
||||
|
||||
class _Boom:
|
||||
def __init__(self, err: Exception):
|
||||
self.err = err
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
raise self.err
|
||||
|
||||
|
||||
class _FakeParams:
|
||||
def __init__(self, active_bundle=None):
|
||||
self.store = {}
|
||||
if active_bundle is not None:
|
||||
self.store["ModelManager_ActiveBundle"] = active_bundle
|
||||
|
||||
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 test_verify_artifact_file_deletes_stale_cached_pkl(tmp_path: Path):
|
||||
pkl_path = tmp_path / "driving_supercombo_guard.pkl"
|
||||
pkl_path.write_bytes(b"stale-pkl")
|
||||
|
||||
runner = TinygradSupercomboRunner.__new__(TinygradSupercomboRunner)
|
||||
runner._pkl_path = str(pkl_path)
|
||||
runner._expected_sha256 = hashlib.sha256(b"fresh-pkl").hexdigest()
|
||||
|
||||
with pytest.raises(RuntimeError, match="SHA mismatch"):
|
||||
runner._verify_artifact_file()
|
||||
|
||||
assert not pkl_path.exists()
|
||||
|
||||
|
||||
def test_validate_jit_names_accepts_current_runtime_contract():
|
||||
runner = TinygradSupercomboRunner.__new__(TinygradSupercomboRunner)
|
||||
runner._pkl_path = "/tmp/does-not-matter.pkl"
|
||||
runner._expected_sha256 = ""
|
||||
runner._run_policy = _FakeJit(['warped', 'img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs'])
|
||||
runner._warp_jits = {
|
||||
(1344, 760): _FakeJit(['tfm', 'big_tfm', 'frame', 'big_frame']),
|
||||
}
|
||||
|
||||
runner._validate_jit_names()
|
||||
|
||||
|
||||
def test_validate_jit_names_raises_clear_error_for_contract_mismatch():
|
||||
runner = TinygradSupercomboRunner.__new__(TinygradSupercomboRunner)
|
||||
runner._pkl_path = "/tmp/does-not-matter.pkl"
|
||||
runner._expected_sha256 = ""
|
||||
runner._run_policy = _FakeJit(['img', 'big_img', 'feat_q', 'desire_q', 'desire', 'traffic_convention', 'action_t'])
|
||||
runner._warp_jits = {
|
||||
(1344, 760): _FakeJit(['img_q', 'big_img_q', 'tfm', 'big_tfm', 'frame', 'big_frame']),
|
||||
}
|
||||
|
||||
with pytest.raises(RuntimeError, match="JIT argument mismatch"):
|
||||
runner._validate_jit_names()
|
||||
|
||||
|
||||
def test_handle_runtime_jit_mismatch_deletes_stale_cached_pkl(tmp_path: Path):
|
||||
pkl_path = tmp_path / "driving_supercombo_guard.pkl"
|
||||
pkl_path.write_bytes(b"stale-pkl")
|
||||
|
||||
runner = TinygradSupercomboRunner.__new__(TinygradSupercomboRunner)
|
||||
runner._pkl_path = str(pkl_path)
|
||||
runner._expected_sha256 = hashlib.sha256(b"fresh-pkl").hexdigest()
|
||||
|
||||
with pytest.raises(RuntimeError, match="runtime JIT mismatch with stale cached SHA"):
|
||||
runner._handle_runtime_jit_mismatch(RuntimeError("args mismatch in JIT: stale bundle"))
|
||||
|
||||
assert not pkl_path.exists()
|
||||
|
||||
|
||||
def test_handle_runtime_jit_mismatch_raises_clear_error_without_sha_mismatch(tmp_path: Path):
|
||||
pkl_path = tmp_path / "driving_supercombo_guard.pkl"
|
||||
pkl_path.write_bytes(b"fresh-pkl")
|
||||
|
||||
runner = TinygradSupercomboRunner.__new__(TinygradSupercomboRunner)
|
||||
runner._pkl_path = str(pkl_path)
|
||||
runner._expected_sha256 = hashlib.sha256(b"fresh-pkl").hexdigest()
|
||||
|
||||
with pytest.raises(RuntimeError, match="runtime JIT mismatch"):
|
||||
runner._handle_runtime_jit_mismatch(RuntimeError("args mismatch in JIT: wrong contract"))
|
||||
|
||||
|
||||
def test_schedule_active_bundle_redownload_sets_download_index(monkeypatch: pytest.MonkeyPatch):
|
||||
params = _FakeParams({"index": 81})
|
||||
monkeypatch.setattr(supercombo_runner_mod, "Params", lambda: params)
|
||||
|
||||
runner = TinygradSupercomboRunner.__new__(TinygradSupercomboRunner)
|
||||
msg = runner._schedule_active_bundle_redownload()
|
||||
|
||||
assert params.get("ModelManager_DownloadIndex") == "81"
|
||||
assert msg == "; scheduled automatic re-download of the active model"
|
||||
|
||||
|
||||
def test_no_active_bundle_seeds_default_tinygrad(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(model_helpers, "ensure_default_model_files", lambda *a, **k: None)
|
||||
params = _FakeParams()
|
||||
|
||||
runner = model_helpers.get_active_model_runner(params)
|
||||
|
||||
assert runner == custom.IQModelManager.Runner.tinygrad
|
||||
active = params.get("ModelManager_ActiveBundle")
|
||||
assert active is not None and active.get("ref") == "default"
|
||||
|
||||
|
||||
def test_select_default_model_clears_custom_download_state(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
||||
pending_restore = tmp_path / "pending_model_restore"
|
||||
pending_restore.write_text("Pop")
|
||||
monkeypatch.setattr(model_helpers, "_PENDING_MODEL_RESTORE_FILE", str(pending_restore))
|
||||
monkeypatch.setattr(model_helpers, "ensure_default_model_files", lambda *a, **k: None)
|
||||
|
||||
params = _FakeParams({"index": 81, "ref": "pop"})
|
||||
params.put("ModelManager_DownloadIndex", "81")
|
||||
params.put("ModelRunnerTypeCache", int(custom.IQModelManager.Runner.tinygrad))
|
||||
|
||||
model_helpers.select_default_model(params)
|
||||
|
||||
assert params.get("ModelManager_DownloadIndex") is None
|
||||
active = params.get("ModelManager_ActiveBundle")
|
||||
assert active is not None and active.get("ref") == "default"
|
||||
assert int(params.get("ModelRunnerTypeCache")) == int(custom.IQModelManager.Runner.tinygrad)
|
||||
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)"})()
|
||||
|
||||
assert model_helpers.get_default_model_bundle([pop_bundle]) is None
|
||||
|
||||
|
||||
def test_verify_artifact_file_schedules_redownload_for_stale_cached_pkl(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
||||
params = _FakeParams({"index": 81})
|
||||
monkeypatch.setattr(supercombo_runner_mod, "Params", lambda: params)
|
||||
|
||||
pkl_path = tmp_path / "driving_supercombo_guard.pkl"
|
||||
pkl_path.write_bytes(b"stale-pkl")
|
||||
|
||||
runner = TinygradSupercomboRunner.__new__(TinygradSupercomboRunner)
|
||||
runner._pkl_path = str(pkl_path)
|
||||
runner._expected_sha256 = hashlib.sha256(b"fresh-pkl").hexdigest()
|
||||
|
||||
with pytest.raises(RuntimeError, match="scheduled automatic re-download"):
|
||||
runner._verify_artifact_file()
|
||||
|
||||
assert params.get("ModelManager_DownloadIndex") == "81"
|
||||
assert not pkl_path.exists()
|
||||
|
||||
|
||||
def test_run_fused_converts_raw_warp_jit_mismatch_to_runtime_error(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
||||
pkl_path = tmp_path / "driving_supercombo_guard.pkl"
|
||||
pkl_path.write_bytes(b"fresh-pkl")
|
||||
|
||||
runner = TinygradSupercomboRunner.__new__(TinygradSupercomboRunner)
|
||||
runner._pkl_path = str(pkl_path)
|
||||
runner._expected_sha256 = hashlib.sha256(b"fresh-pkl").hexdigest()
|
||||
runner._frame_skip = 4
|
||||
runner._cam = (1344, 760)
|
||||
runner._queues = {
|
||||
"tfm": object(),
|
||||
"big_tfm": object(),
|
||||
"img_q": object(),
|
||||
"big_img_q": object(),
|
||||
"feat_q": object(),
|
||||
"desire_q": object(),
|
||||
"packed_npy_inputs": object(),
|
||||
}
|
||||
runner._npy = {
|
||||
"tfm": [0.0],
|
||||
"big_tfm": [0.0],
|
||||
"desire": [0.0],
|
||||
"prev_feat": [0.0],
|
||||
}
|
||||
runner._prev_desire = [0.0]
|
||||
runner._warp_jits = {
|
||||
(1344, 760): _Boom(RuntimeError("args mismatch in JIT: self.captured.expected_names=['big_frame'] != ['frame']")),
|
||||
}
|
||||
runner._run_policy = _FakeJit(["warped", "img_q", "big_img_q", "feat_q", "desire_q", "packed_npy_inputs"])
|
||||
runner._hidden_slice = slice(0, 1)
|
||||
runner._slices = {"out": slice(0, 1)}
|
||||
runner._parser = type("P", (), {"parse_vision_outputs": staticmethod(lambda sliced: sliced)})()
|
||||
runner._frame_tensor = lambda *args, **kwargs: object()
|
||||
|
||||
monkeypatch.setattr(TinygradSupercomboRunner, "_ensure_queues", lambda self, cam_w, cam_h: None)
|
||||
|
||||
class _Buf:
|
||||
width = 1344
|
||||
height = 760
|
||||
data = memoryview(b"\x00")
|
||||
|
||||
with pytest.raises(RuntimeError, match="runtime JIT mismatch"):
|
||||
runner.run_fused(
|
||||
{"img": _Buf(), "big_img": _Buf()},
|
||||
{"img": [0.0], "big_img": [0.0]},
|
||||
{},
|
||||
)
|
||||
155
iqpilot/selfdrive/iqmodeld/tests/test_temporal_replay.py
Normal file
155
iqpilot/selfdrive/iqmodeld/tests/test_temporal_replay.py
Normal file
@@ -0,0 +1,155 @@
|
||||
"""
|
||||
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
|
||||
from types import SimpleNamespace
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
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
|
||||
class StubOverride:
|
||||
key: str
|
||||
value: str
|
||||
|
||||
|
||||
class StubBundle:
|
||||
def __init__(self, generation: int = 10):
|
||||
self.overrides = [StubOverride("lat", ".1"), StubOverride("long", ".3")]
|
||||
self.generation = generation
|
||||
|
||||
|
||||
class StubRunner:
|
||||
def __init__(self, input_shapes: dict[str, tuple[int, ...]]) -> None:
|
||||
self.input_shapes = input_shapes
|
||||
self.constants = SimpleNamespace(
|
||||
FULL_HISTORY_BUFFER_LEN=100,
|
||||
FEATURE_LEN=512,
|
||||
DESIRE_LEN=8,
|
||||
PREV_DESIRED_CURV_LEN=1,
|
||||
INPUT_HISTORY_BUFFER_LEN=25,
|
||||
TEMPORAL_SKIP=4,
|
||||
)
|
||||
self.vision_input_names: list[str] = []
|
||||
self.is_20hz = input_shapes.get(next(iter(input_shapes)), (1, 0, 0))[1] == 25
|
||||
|
||||
def prepare_inputs(self, imgs_cl, numpy_inputs, frames):
|
||||
return None
|
||||
|
||||
def run_model(self):
|
||||
return {
|
||||
"hidden_state": np.zeros((1, self.constants.FEATURE_LEN), dtype=np.float32),
|
||||
"desired_curvature": np.zeros((1, 1), dtype=np.float32),
|
||||
}
|
||||
|
||||
|
||||
def _install_runtime(monkeypatch: pytest.MonkeyPatch, shapes: dict[str, tuple[int, ...]], generation: int = 10):
|
||||
bundle = StubBundle(generation=generation)
|
||||
runner = StubRunner(shapes)
|
||||
monkeypatch.setattr(bundle_helpers, "get_active_bundle", lambda params=None: bundle, raising=False)
|
||||
monkeypatch.setattr(runner_helpers, "get_model_runner", lambda: runner, raising=False)
|
||||
monkeypatch.setattr(iqmodeld_daemon, "get_active_bundle", lambda params=None: bundle, raising=False)
|
||||
monkeypatch.setattr(iqmodeld_daemon, "get_model_runner", lambda: runner, raising=False)
|
||||
return iqmodeld_daemon.NeuralEngineState(None), runner
|
||||
|
||||
|
||||
def _expected_selector_indices(shape: tuple[int, ...], mode: str) -> np.ndarray | None:
|
||||
if mode == "split":
|
||||
full = 100
|
||||
return np.arange(full)[-1 - (4 * (25 - 1))::4]
|
||||
if mode == "20hz":
|
||||
step = int(-100 / shape[1])
|
||||
return np.arange(step, step * (shape[1] + 1), step)[::-1]
|
||||
if mode == "dense":
|
||||
return np.arange(shape[1])
|
||||
return None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("shapes", "mode"),
|
||||
[
|
||||
({"desire": (1, 100, 8), "features_buffer": (1, 99, 512), "prev_desired_curv": (1, 100, 1)}, "dense"),
|
||||
({"desire": (1, 25, 8), "features_buffer": (1, 24, 512)}, "20hz"),
|
||||
({"desire_pulse": (1, 25, 8), "features_buffer": (1, 25, 512)}, "split"),
|
||||
],
|
||||
)
|
||||
def test_replay_ledger_layout_matches_expected_history(monkeypatch: pytest.MonkeyPatch,
|
||||
shapes: dict[str, tuple[int, ...]],
|
||||
mode: str):
|
||||
state, _runner = _install_runtime(monkeypatch, shapes)
|
||||
|
||||
for tensor_name, tensor_shape in shapes.items():
|
||||
history = state.temporal_buffers.get(tensor_name)
|
||||
selector = state.temporal_idxs_map.get(tensor_name)
|
||||
if history is None:
|
||||
continue
|
||||
|
||||
if mode == "dense":
|
||||
expected_shape = (1, tensor_shape[1], tensor_shape[2])
|
||||
else:
|
||||
expected_shape = (1, 100, tensor_shape[2])
|
||||
|
||||
assert history.shape == expected_shape
|
||||
expected_selector = _expected_selector_indices(tensor_shape, mode)
|
||||
if expected_selector is None:
|
||||
assert selector is None or selector.size == 0
|
||||
else:
|
||||
assert np.array_equal(selector, expected_selector)
|
||||
|
||||
|
||||
def test_replay_ledger_rising_edge_and_hidden_state_updates(monkeypatch: pytest.MonkeyPatch):
|
||||
state, runner = _install_runtime(monkeypatch, {
|
||||
"desire": (1, 100, 8),
|
||||
"features_buffer": (1, 99, 512),
|
||||
"prev_desired_curv": (1, 100, 1),
|
||||
})
|
||||
|
||||
pulse = np.zeros(8, dtype=np.float32)
|
||||
pulse[3] = 1.0
|
||||
state.run({}, {}, {"desire": pulse})
|
||||
first_export = state.numpy_inputs["desire"].copy()
|
||||
assert np.count_nonzero(first_export) == 1
|
||||
|
||||
state.run({}, {}, {"desire": pulse})
|
||||
second_export = state.numpy_inputs["desire"].copy()
|
||||
assert np.count_nonzero(second_export) == 1
|
||||
assert second_export[0, -1, 3] == 0.0
|
||||
|
||||
hidden_value = np.arange(runner.constants.FEATURE_LEN, dtype=np.float32)
|
||||
|
||||
def hidden_state_run():
|
||||
return {
|
||||
"hidden_state": hidden_value.reshape(1, -1),
|
||||
"desired_curvature": np.array([[0.25]], dtype=np.float32),
|
||||
}
|
||||
|
||||
state.model_runner.run_model = hidden_state_run
|
||||
state.run({}, {}, {"desire": np.zeros(8, dtype=np.float32)})
|
||||
|
||||
np.testing.assert_allclose(state.numpy_inputs["features_buffer"][0, -1], hidden_value, rtol=0, atol=0)
|
||||
assert state.numpy_inputs["prev_desired_curv"][0, -1, 0] == pytest.approx(0.25)
|
||||
|
||||
|
||||
def test_replay_ledger_zeroes_feedback_for_mlsim_generation(monkeypatch: pytest.MonkeyPatch):
|
||||
state, _runner = _install_runtime(monkeypatch, {
|
||||
"desire": (1, 100, 8),
|
||||
"features_buffer": (1, 99, 512),
|
||||
"prev_desired_curv": (1, 100, 1),
|
||||
}, generation=11)
|
||||
|
||||
def ml_run():
|
||||
return {
|
||||
"hidden_state": np.zeros((1, 512), dtype=np.float32),
|
||||
"desired_curvature": np.array([[1.5]], dtype=np.float32),
|
||||
}
|
||||
|
||||
state.model_runner.run_model = ml_run
|
||||
state.run({}, {}, {"desire": np.zeros(8, dtype=np.float32)})
|
||||
assert np.count_nonzero(state.numpy_inputs["prev_desired_curv"]) == 0
|
||||
17
iqpilot/selfdrive/iqmodeld/tests/tf_test/build.sh
Executable file
17
iqpilot/selfdrive/iqmodeld/tests/tf_test/build.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TF_ROOT="${TF_ROOT:-/home/batman/one/external/tensorflow}"
|
||||
TF_INCLUDE_DIR="${TF_INCLUDE_DIR:-$TF_ROOT/include}"
|
||||
TF_LIB_DIR="${TF_LIB_DIR:-$TF_ROOT/lib}"
|
||||
CXX="${CXX:-clang++}"
|
||||
|
||||
exec "$CXX" \
|
||||
-std=c++17 \
|
||||
-I "$TF_INCLUDE_DIR" \
|
||||
-L "$TF_LIB_DIR" \
|
||||
-Wl,-rpath="$TF_LIB_DIR" \
|
||||
main.cc \
|
||||
-ltensorflow
|
||||
92
iqpilot/selfdrive/iqmodeld/tests/tf_test/main.cc
Normal file
92
iqpilot/selfdrive/iqmodeld/tests/tf_test/main.cc
Normal file
@@ -0,0 +1,92 @@
|
||||
// Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "tensorflow/c/c_api.h"
|
||||
|
||||
namespace {
|
||||
|
||||
struct FileBlob {
|
||||
std::vector<uint8_t> bytes;
|
||||
};
|
||||
|
||||
FileBlob read_blob(const std::filesystem::path &path) {
|
||||
FILE *handle = fopen(path.c_str(), "rb");
|
||||
if (handle == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
fseek(handle, 0, SEEK_END);
|
||||
const long byte_count = ftell(handle);
|
||||
rewind(handle);
|
||||
|
||||
FileBlob blob;
|
||||
blob.bytes.resize(byte_count);
|
||||
const size_t read_count = fread(blob.bytes.data(), static_cast<size_t>(byte_count), 1, handle);
|
||||
fclose(handle);
|
||||
|
||||
if (read_count != 1) {
|
||||
blob.bytes.clear();
|
||||
}
|
||||
return blob;
|
||||
}
|
||||
|
||||
void free_tf_buffer(void *data, size_t) {
|
||||
free(data);
|
||||
}
|
||||
|
||||
TF_Buffer *make_tf_buffer(FileBlob &&blob) {
|
||||
auto *buffer = TF_NewBuffer();
|
||||
auto *payload = static_cast<uint8_t *>(malloc(blob.bytes.size()));
|
||||
assert(payload != nullptr);
|
||||
memcpy(payload, blob.bytes.data(), blob.bytes.size());
|
||||
buffer->data = payload;
|
||||
buffer->length = blob.bytes.size();
|
||||
buffer->data_deallocator = free_tf_buffer;
|
||||
return buffer;
|
||||
}
|
||||
|
||||
std::string pb_path_from_prefix(const char *prefix) {
|
||||
return std::string(prefix) + ".pb";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
if (argc < 2) {
|
||||
printf("usage: %s <graph-prefix>\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const std::string pb_path = pb_path_from_prefix(argv[1]);
|
||||
printf("loading model %s\n", pb_path.c_str());
|
||||
|
||||
FileBlob blob = read_blob(pb_path);
|
||||
if (blob.bytes.empty()) {
|
||||
printf("FAIL: unable to read graph bytes\n");
|
||||
return 1;
|
||||
}
|
||||
printf("loaded model of size %zu\n", blob.bytes.size());
|
||||
|
||||
std::unique_ptr<TF_Status, decltype(&TF_DeleteStatus)> status(TF_NewStatus(), TF_DeleteStatus);
|
||||
std::unique_ptr<TF_Graph, decltype(&TF_DeleteGraph)> graph(TF_NewGraph(), TF_DeleteGraph);
|
||||
std::unique_ptr<TF_ImportGraphDefOptions, decltype(&TF_DeleteImportGraphDefOptions)> options(
|
||||
TF_NewImportGraphDefOptions(), TF_DeleteImportGraphDefOptions);
|
||||
std::unique_ptr<TF_Buffer, decltype(&TF_DeleteBuffer)> buffer(make_tf_buffer(std::move(blob)), TF_DeleteBuffer);
|
||||
|
||||
TF_GraphImportGraphDef(graph.get(), buffer.get(), options.get(), status.get());
|
||||
if (TF_GetCode(status.get()) != TF_OK) {
|
||||
printf("FAIL: %s\n", TF_Message(status.get()));
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("SUCCESS\n");
|
||||
return 0;
|
||||
}
|
||||
32
iqpilot/selfdrive/iqmodeld/tests/tf_test/pb_loader.py
Executable file
32
iqpilot/selfdrive/iqmodeld/tests/tf_test/pb_loader.py
Executable file
@@ -0,0 +1,32 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import tensorflow as tf
|
||||
|
||||
|
||||
def _load_graph_bytes(graph_path: Path) -> bytes:
|
||||
return graph_path.read_bytes()
|
||||
|
||||
|
||||
def _parse_graph(graph_path: Path) -> tf.compat.v1.GraphDef:
|
||||
graph = tf.compat.v1.GraphDef()
|
||||
graph.ParseFromString(_load_graph_bytes(graph_path))
|
||||
return graph
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
if len(argv) < 2:
|
||||
print("Usage: pb_loader.py <graph.pb>")
|
||||
return 1
|
||||
_parse_graph(Path(argv[1]))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv))
|
||||
54
iqpilot/selfdrive/iqmodeld/tests/timing/benchmark.py
Executable file
54
iqpilot/selfdrive/iqmodeld/tests/timing/benchmark.py
Executable file
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
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"))
|
||||
WARMUP_MESSAGES = 10
|
||||
|
||||
|
||||
def _collect_execution_samples(sock, duration_s: int) -> np.ndarray:
|
||||
samples: list[float] = []
|
||||
deadline = time.monotonic() + duration_s
|
||||
while time.monotonic() < deadline:
|
||||
for message in messaging.drain_sock(sock, wait_for_one=True):
|
||||
samples.append(message.modelV2.modelExecutionTime)
|
||||
return np.array(samples[WARMUP_MESSAGES:]) * 1000.0
|
||||
|
||||
|
||||
def _single_benchmark_pass(sock) -> np.ndarray:
|
||||
os.environ["LOGPRINT"] = "debug"
|
||||
managed_processes["modeld"].start()
|
||||
time.sleep(5)
|
||||
try:
|
||||
return _collect_execution_samples(sock, WINDOW_SECONDS)
|
||||
finally:
|
||||
managed_processes["modeld"].stop()
|
||||
|
||||
|
||||
def _report_run(index: int, values_ms: np.ndarray) -> None:
|
||||
print(
|
||||
f"run {index}: avg={values_ms.mean():0.2f}ms "
|
||||
f"min={values_ms.min():0.2f}ms max={values_ms.max():0.2f}ms"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
subscriber = messaging.sub_sock("modelV2", conflate=False, timeout=1000)
|
||||
all_runs = [_single_benchmark_pass(subscriber) for _ in range(RUN_COUNT)]
|
||||
|
||||
print("\n")
|
||||
print(f"ran modeld {RUN_COUNT} times for {WINDOW_SECONDS}s each")
|
||||
for index, values_ms in enumerate(all_runs, start=1):
|
||||
_report_run(index, values_ms)
|
||||
print("\n")
|
||||
12
iqpilot/selfdrive/iqmodeld/tools/compile_daemon.py
Normal file
12
iqpilot/selfdrive/iqmodeld/tools/compile_daemon.py
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.tools.daemon_jit_compiler import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
144
iqpilot/selfdrive/iqmodeld/tools/compile_egpu_model.py
Normal file
144
iqpilot/selfdrive/iqmodeld/tools/compile_egpu_model.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""
|
||||
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 pickle
|
||||
import time
|
||||
|
||||
os.environ.setdefault("DEV", "USB+AMD:LLVM")
|
||||
os.environ.setdefault("FLOAT16", "1")
|
||||
os.environ.setdefault("JIT_BATCH_SIZE", "0")
|
||||
os.environ.setdefault("GMMU", "0")
|
||||
|
||||
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
|
||||
|
||||
|
||||
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")
|
||||
|
||||
print("pickle round trip")
|
||||
jit = pickle.loads(pickle.dumps(jit))
|
||||
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"])))
|
||||
|
||||
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"
|
||||
with open(tmp, "wb") as f:
|
||||
pickle.dump(bundle, f, protocol=pickle.HIGHEST_PROTOCOL)
|
||||
os.replace(tmp, out_path)
|
||||
return out_path
|
||||
|
||||
|
||||
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)
|
||||
args = p.parse_args()
|
||||
|
||||
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")
|
||||
|
||||
out = compile_model(meta, onnx_path, args.output or egpu_pkl_path(meta))
|
||||
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)
|
||||
417
iqpilot/selfdrive/iqmodeld/tools/compile_split_runtime.py
Normal file
417
iqpilot/selfdrive/iqmodeld/tools/compile_split_runtime.py
Normal file
@@ -0,0 +1,417 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import atexit
|
||||
import os
|
||||
import pickle
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def _patch_firmware_fetch() -> None:
|
||||
import hashlib
|
||||
import pathlib
|
||||
|
||||
import zstandard
|
||||
from tinygrad import helpers
|
||||
|
||||
if not hasattr(helpers, "fetch_fw"):
|
||||
return
|
||||
|
||||
original_fetch = helpers.fetch_fw
|
||||
|
||||
def fetch_fw(path, name, sha256):
|
||||
archive_path = pathlib.Path(f"/lib/firmware/{path}/{name}.zst")
|
||||
if archive_path.is_file():
|
||||
blob = zstandard.ZstdDecompressor().stream_reader(archive_path.read_bytes()).read()
|
||||
if hashlib.sha256(blob).hexdigest() == sha256:
|
||||
return blob
|
||||
return original_fetch(path, name, sha256)
|
||||
|
||||
helpers.fetch_fw = fetch_fw
|
||||
|
||||
|
||||
_patch_firmware_fetch()
|
||||
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.engine.jit import TinyJit
|
||||
from tinygrad.helpers import Context
|
||||
from tinygrad.nn.onnx import OnnxRunner
|
||||
from tinygrad.tensor import Tensor
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CameraGeometry:
|
||||
width: int
|
||||
height: int
|
||||
stride: int
|
||||
y_height: int
|
||||
uv_height: int
|
||||
size: int
|
||||
|
||||
|
||||
WARP_DEVICE = os.getenv("WARP_DEV")
|
||||
|
||||
|
||||
def _read_shared_copy(path: str) -> str:
|
||||
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))
|
||||
with open(shm_path, "wb") as handle:
|
||||
handle.write(read_file_chunked(path))
|
||||
return shm_path
|
||||
|
||||
|
||||
def _parse_size(text: str) -> tuple[int, int]:
|
||||
width, height = text.lower().split("x")
|
||||
return int(width), int(height)
|
||||
|
||||
|
||||
def _rand_u8_inputs(keys: list[str], shape, device=None):
|
||||
return {key: Tensor.randint(shape, low=0, high=256, dtype="uint8", device=device).realize() for key in keys}
|
||||
|
||||
|
||||
def _phase_desire_key(policy_shapes: dict[str, tuple[int, ...]]) -> str:
|
||||
for key in policy_shapes:
|
||||
if key.startswith("desire"):
|
||||
return key
|
||||
raise KeyError("No desire-like key found in policy shapes")
|
||||
|
||||
|
||||
def _phase_image_keys(vision_shapes: dict[str, tuple[int, ...]]) -> tuple[str, str]:
|
||||
names = sorted(name for name in vision_shapes if "img" in name)
|
||||
road_key = next((name for name in names if "big" not in name), None)
|
||||
wide_key = next((name for name in names if "big" in name), None)
|
||||
if road_key is None or wide_key is None:
|
||||
raise ValueError(f"Unable to resolve road/wide image keys from {list(vision_shapes)}")
|
||||
return road_key, wide_key
|
||||
|
||||
|
||||
def _base_policy_keys(policy_shapes: dict[str, tuple[int, ...]]) -> set[str]:
|
||||
return {
|
||||
_phase_desire_key(policy_shapes),
|
||||
"features_buffer",
|
||||
"traffic_convention",
|
||||
"action_t",
|
||||
}
|
||||
|
||||
|
||||
def _common_policy_shapes(role_shapes: dict[str, dict[str, tuple[int, ...]]]) -> dict[str, tuple[int, ...]]:
|
||||
first_role = next(iter(role_shapes))
|
||||
baseline = role_shapes[first_role]
|
||||
for role_name, shape_map in role_shapes.items():
|
||||
if shape_map != baseline:
|
||||
raise ValueError(f"Policy input shapes differ for role {role_name}")
|
||||
return baseline
|
||||
|
||||
|
||||
def _phase_frame_skip(policy_shapes: dict[str, tuple[int, ...]]) -> int:
|
||||
feature_shape = policy_shapes.get("features_buffer")
|
||||
if feature_shape is None:
|
||||
return 1
|
||||
history_length = feature_shape[1]
|
||||
return 1 if history_length >= 99 else 4
|
||||
|
||||
|
||||
def _project_pixels(src_flat, inverse_matrix, dst_shape, src_shape, stride_pad, border_fill_val=None):
|
||||
dst_w, dst_h = dst_shape
|
||||
src_h, src_w = src_shape
|
||||
|
||||
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]
|
||||
scale = inverse_matrix[2, 0] * x_coords + inverse_matrix[2, 1] * y_coords + inverse_matrix[2, 2]
|
||||
|
||||
src_x = src_x / scale
|
||||
src_y = src_y / scale
|
||||
|
||||
rounded_x = Tensor.round(src_x)
|
||||
rounded_y = Tensor.round(src_y)
|
||||
gather_x = rounded_x.clip(0, src_w - 1).cast("int")
|
||||
gather_y = rounded_y.clip(0, src_h - 1).cast("int")
|
||||
gather_index = gather_y * (src_w + stride_pad) + gather_x
|
||||
sampled = src_flat[gather_index]
|
||||
|
||||
if border_fill_val is None:
|
||||
return sampled
|
||||
|
||||
inside = ((rounded_x >= 0) & (rounded_x <= src_w - 1) & (rounded_y >= 0) & (rounded_y <= src_h - 1)).cast(sampled.dtype)
|
||||
return sampled * inside + Tensor(border_fill_val, dtype=sampled.dtype) * (1 - inside)
|
||||
|
||||
|
||||
def _pack_nv12_planes(stacked_frame):
|
||||
y_height = (stacked_frame.shape[0] * 2) // 3
|
||||
frame_width = stacked_frame.shape[1]
|
||||
return Tensor.cat(
|
||||
stacked_frame[0:y_height:2, 0::2],
|
||||
stacked_frame[1:y_height:2, 0::2],
|
||||
stacked_frame[0:y_height:2, 1::2],
|
||||
stacked_frame[1:y_height:2, 1::2],
|
||||
stacked_frame[y_height:y_height + y_height // 4].reshape((y_height // 2, frame_width // 2)),
|
||||
stacked_frame[y_height + y_height // 4:y_height + y_height // 2].reshape((y_height // 2, frame_width // 2)),
|
||||
dim=0,
|
||||
).reshape((6, y_height // 2, frame_width // 2))
|
||||
|
||||
|
||||
def _warp_program(camera: CameraGeometry, model_w: int, model_h: int):
|
||||
uv_offset = camera.stride * camera.y_height
|
||||
stride_pad = camera.stride - camera.width
|
||||
|
||||
def prepare_frame(nv12_blob, inverse_matrix):
|
||||
uv_matrix = inverse_matrix * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]], device=WARP_DEVICE)
|
||||
uv_plane = nv12_blob[uv_offset:uv_offset + camera.uv_height * camera.stride].reshape(camera.uv_height, camera.stride)
|
||||
with Context(SPLIT_REDUCEOP=0):
|
||||
y_plane = _project_pixels(nv12_blob[:camera.height * camera.stride], inverse_matrix, (model_w, model_h), (camera.height, camera.width), stride_pad).realize()
|
||||
u_plane = _project_pixels(uv_plane[:camera.height // 2, :camera.width:2].flatten(), uv_matrix, (model_w // 2, model_h // 2), (camera.height // 2, camera.width // 2), 0).realize()
|
||||
v_plane = _project_pixels(uv_plane[:camera.height // 2, 1:camera.width:2].flatten(), uv_matrix, (model_w // 2, model_h // 2), (camera.height // 2, camera.width // 2), 0).realize()
|
||||
return _pack_nv12_planes(y_plane.cat(u_plane).cat(v_plane).reshape((model_h * 3 // 2, model_w)))
|
||||
|
||||
return prepare_frame
|
||||
|
||||
|
||||
def _sample_sparse(queue_tensor, frame_stride):
|
||||
return queue_tensor[::frame_stride].contiguous().flatten(0, 1).unsqueeze(0)
|
||||
|
||||
|
||||
def _sample_desire(queue_tensor, frame_stride):
|
||||
return queue_tensor.reshape(-1, frame_stride, *queue_tensor.shape[1:]).max(1).flatten(0, 1).unsqueeze(0)
|
||||
|
||||
|
||||
def _roll_queue(queue_tensor, incoming, sampler):
|
||||
queue_tensor.assign(queue_tensor[1:].cat(incoming, dim=0).contiguous())
|
||||
return sampler(queue_tensor)
|
||||
|
||||
|
||||
def _vision_queue_buffers(vision_shapes: dict[str, tuple[int, ...]], frame_stride: int, device):
|
||||
road_key, _ = _phase_image_keys(vision_shapes)
|
||||
image_shape = vision_shapes[road_key]
|
||||
frame_history = image_shape[1] // 6
|
||||
queue_depth = frame_stride * (frame_history - 1) + 1
|
||||
frame_queue_shape = (queue_depth, 6, image_shape[2], image_shape[3])
|
||||
|
||||
numpy_state = {
|
||||
"tfm": np.zeros((3, 3), dtype=np.float32),
|
||||
"big_tfm": np.zeros((3, 3), dtype=np.float32),
|
||||
}
|
||||
tensor_state = {
|
||||
"img_q": Tensor(np.zeros(frame_queue_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||
"big_img_q": Tensor(np.zeros(frame_queue_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||
**{name: Tensor(value, device="NPY").realize() for name, value in numpy_state.items()},
|
||||
}
|
||||
return tensor_state, numpy_state
|
||||
|
||||
|
||||
def _policy_queue_buffers(vision_shapes: dict[str, tuple[int, ...]], policy_shapes: dict[str, tuple[int, ...]], frame_stride: int, device):
|
||||
tensor_state, numpy_state = _vision_queue_buffers(vision_shapes, frame_stride, device)
|
||||
desired_key = _phase_desire_key(policy_shapes)
|
||||
feature_shape = policy_shapes["features_buffer"]
|
||||
desired_shape = policy_shapes[desired_key]
|
||||
traffic_shape = policy_shapes["traffic_convention"]
|
||||
action_shape = policy_shapes.get("action_t", traffic_shape)
|
||||
|
||||
numpy_policy = {
|
||||
"desire": np.zeros(desired_shape[2], dtype=np.float32),
|
||||
"traffic_convention": np.zeros(traffic_shape, dtype=np.float32),
|
||||
"action_t": np.zeros(action_shape, dtype=np.float32),
|
||||
}
|
||||
for key, shape in policy_shapes.items():
|
||||
if key not in _base_policy_keys(policy_shapes):
|
||||
numpy_policy[key] = np.zeros(shape, dtype=np.float32)
|
||||
|
||||
numpy_state.update(numpy_policy)
|
||||
tensor_state.update({
|
||||
"feat_q": Tensor(np.zeros((frame_stride * (feature_shape[1] - 1) + 1, feature_shape[0], feature_shape[2]), dtype=np.float32), device=device).contiguous().realize(),
|
||||
"desire_q": Tensor(np.zeros((frame_stride * desired_shape[1], desired_shape[0], desired_shape[2]), dtype=np.float32), device=device).contiguous().realize(),
|
||||
**{name: Tensor(value, device="NPY").realize() for name, value in numpy_policy.items()},
|
||||
})
|
||||
return tensor_state, numpy_state
|
||||
|
||||
|
||||
def _stage_program(camera: CameraGeometry, model_w: int, model_h: int, frame_stride: int):
|
||||
prepare_frame = _warp_program(camera, model_w, model_h)
|
||||
sparse_sampler = partial(_sample_sparse, frame_stride=frame_stride)
|
||||
|
||||
def stage_inputs(img_q, big_img_q, tfm, big_tfm, frame, big_frame):
|
||||
tfm = tfm.to(WARP_DEVICE)
|
||||
big_tfm = big_tfm.to(WARP_DEVICE)
|
||||
Tensor.realize(tfm, big_tfm)
|
||||
staged_main = prepare_frame(frame, tfm).unsqueeze(0).to(Device.DEFAULT)
|
||||
staged_wide = prepare_frame(big_frame, big_tfm).unsqueeze(0).to(Device.DEFAULT)
|
||||
return (
|
||||
_roll_queue(img_q, staged_main, sparse_sampler),
|
||||
_roll_queue(big_img_q, staged_wide, sparse_sampler),
|
||||
)
|
||||
|
||||
return stage_inputs
|
||||
|
||||
|
||||
def _role_executor(model_runners: dict[str, OnnxRunner], meta_by_role: dict[str, dict], frame_stride: int):
|
||||
desired_sampler = partial(_sample_desire, frame_stride=frame_stride)
|
||||
sparse_sampler = partial(_sample_sparse, frame_stride=frame_stride)
|
||||
vision_hidden_slice = meta_by_role["vision"]["output_slices"]["hidden_state"]
|
||||
policy_roles = [name for name in meta_by_role if name != "vision"]
|
||||
policy_shapes = _common_policy_shapes({name: meta_by_role[name]["input_shapes"] for name in policy_roles})
|
||||
desired_key = _phase_desire_key(policy_shapes)
|
||||
road_key, wide_key = _phase_image_keys(meta_by_role["vision"]["input_shapes"])
|
||||
extra_keys = [key for key in policy_shapes if key not in _base_policy_keys(policy_shapes)]
|
||||
|
||||
def execute_bundle(img, big_img, feat_q, desire_q, desire, traffic_convention, action_t, **extra):
|
||||
desired_tensor = desire.to(Device.DEFAULT)
|
||||
traffic_tensor = traffic_convention.to(Device.DEFAULT)
|
||||
action_tensor = action_t.to(Device.DEFAULT)
|
||||
extra_tensors = {key: extra[key].to(Device.DEFAULT) for key in extra_keys if key in extra}
|
||||
Tensor.realize(desired_tensor, traffic_tensor, action_tensor, *extra_tensors.values())
|
||||
|
||||
desire_buffer = _roll_queue(desire_q, desired_tensor.reshape(1, 1, -1), desired_sampler)
|
||||
vision_output = next(iter(model_runners["vision"]({road_key: img, wide_key: big_img}).values())).cast("float32")
|
||||
hidden_state = vision_output[:, vision_hidden_slice].reshape(1, -1).unsqueeze(0)
|
||||
feature_buffer = _roll_queue(feat_q, hidden_state, sparse_sampler)
|
||||
|
||||
common_inputs = {
|
||||
"features_buffer": feature_buffer,
|
||||
desired_key: desire_buffer,
|
||||
"traffic_convention": traffic_tensor,
|
||||
"action_t": action_tensor,
|
||||
**extra_tensors,
|
||||
}
|
||||
|
||||
role_outputs = []
|
||||
for role_name in policy_roles:
|
||||
role_outputs.append(next(iter(model_runners[role_name](common_inputs).values())).cast("float32"))
|
||||
return (vision_output, *role_outputs)
|
||||
|
||||
return execute_bundle
|
||||
|
||||
|
||||
def _capture_and_freeze(jit_runner, random_inputs_factory, queue_keys, queue_factory):
|
||||
seed_value = 42
|
||||
|
||||
def validate(fn, baseline_outputs=None, baseline_buffers=None, expect_match=True, replay_seed=seed_value):
|
||||
queue_tensors, numpy_values = queue_factory(Device.DEFAULT)
|
||||
np.random.seed(replay_seed)
|
||||
Tensor.manual_seed(replay_seed)
|
||||
|
||||
replay_count = 1 if (baseline_outputs is not None or baseline_buffers is not None) else 3
|
||||
for pass_index in range(replay_count):
|
||||
for value in numpy_values.values():
|
||||
value[:] = np.random.randn(*value.shape).astype(value.dtype)
|
||||
Device.default.synchronize()
|
||||
random_inputs = random_inputs_factory()
|
||||
start_time = time.perf_counter()
|
||||
outputs = fn(**{name: queue_tensors[name] for name in queue_keys}, **random_inputs)
|
||||
enqueue_time = time.perf_counter()
|
||||
Device.default.synchronize()
|
||||
total_time = time.perf_counter()
|
||||
print(f" [{pass_index + 1}/{replay_count}] enqueue {(enqueue_time - start_time) * 1e3:6.2f} ms -- total {(total_time - start_time) * 1e3:6.2f} ms")
|
||||
|
||||
if pass_index == 0:
|
||||
output_snapshot = [np.copy(value.numpy()) for value in outputs]
|
||||
buffer_snapshot = [np.copy(value.numpy().copy()) for value in queue_tensors.values()]
|
||||
|
||||
if baseline_outputs is not None:
|
||||
matches = all(np.array_equal(current, reference) for current, reference in zip(output_snapshot, baseline_outputs, strict=True))
|
||||
assert matches == expect_match, f"outputs {'differ from' if expect_match else 'match'} baseline"
|
||||
if baseline_buffers is not None:
|
||||
matches = all(np.array_equal(current, reference) for current, reference in zip(buffer_snapshot, baseline_buffers, strict=True))
|
||||
assert matches == expect_match, f"buffers {'differ from' if expect_match else 'match'} baseline"
|
||||
|
||||
return output_snapshot, buffer_snapshot
|
||||
|
||||
print("capture + replay")
|
||||
baseline_outputs, baseline_buffers = validate(jit_runner)
|
||||
print("pickle round trip")
|
||||
frozen = pickle.loads(pickle.dumps(jit_runner))
|
||||
validate(frozen, baseline_outputs, baseline_buffers, expect_match=True)
|
||||
validate(frozen, baseline_outputs, baseline_buffers, expect_match=False, replay_seed=seed_value + 1)
|
||||
return frozen
|
||||
|
||||
|
||||
def _arg_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--model-size", type=_parse_size, required=True, help="model input WxH")
|
||||
parser.add_argument("--camera-resolutions", type=_parse_size, nargs="+", required=True, help="camera resolutions WxH")
|
||||
parser.add_argument("--vision-onnx", required=True)
|
||||
parser.add_argument("--policy-onnx")
|
||||
parser.add_argument("--off-policy-onnx")
|
||||
parser.add_argument("--on-policy-onnx")
|
||||
parser.add_argument("--output", required=True)
|
||||
parser.add_argument("--frame-skip", type=int)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
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
|
||||
|
||||
policy_specs = [
|
||||
("policy", args.policy_onnx),
|
||||
("off_policy", args.off_policy_onnx),
|
||||
("on_policy", args.on_policy_onnx),
|
||||
]
|
||||
active_policy_specs = [(role, path) for role, path in policy_specs if path]
|
||||
if not active_policy_specs:
|
||||
raise SystemExit("At least one policy ONNX must be provided")
|
||||
|
||||
model_paths = {"vision": _read_shared_copy(args.vision_onnx)}
|
||||
for role_name, onnx_path in active_policy_specs:
|
||||
model_paths[role_name] = _read_shared_copy(onnx_path)
|
||||
|
||||
model_runners = {role_name: OnnxRunner(path) for role_name, path in model_paths.items()}
|
||||
meta_by_role = {role_name: build_metadata_record(path) for role_name, path in model_paths.items()}
|
||||
|
||||
shared_policy_shapes = _common_policy_shapes({
|
||||
role_name: meta_by_role[role_name]["input_shapes"] for role_name, _ in active_policy_specs
|
||||
})
|
||||
frame_stride = args.frame_skip if args.frame_skip is not None else _phase_frame_skip(shared_policy_shapes)
|
||||
|
||||
package: dict[Any, Any] = {
|
||||
"meta_by_role": meta_by_role,
|
||||
"roles": [role_name for role_name, _ in active_policy_specs],
|
||||
"frame_stride": frame_stride,
|
||||
}
|
||||
|
||||
executor_jit = TinyJit(_role_executor(model_runners, meta_by_role, frame_stride), prune=True)
|
||||
queue_factory = partial(_policy_queue_buffers, meta_by_role["vision"]["input_shapes"], shared_policy_shapes, frame_stride)
|
||||
image_shape = meta_by_role["vision"]["input_shapes"][_phase_image_keys(meta_by_role["vision"]["input_shapes"])[0]]
|
||||
package["execute_bundle"] = _capture_and_freeze(
|
||||
executor_jit,
|
||||
partial(_rand_u8_inputs, keys=["img", "big_img"], shape=image_shape),
|
||||
["feat_q", "desire_q", "desire", "traffic_convention", "action_t", *[k for k in shared_policy_shapes if k not in _base_policy_keys(shared_policy_shapes)]],
|
||||
queue_factory,
|
||||
)
|
||||
|
||||
for camera_width, camera_height in args.camera_resolutions:
|
||||
camera = CameraGeometry(camera_width, camera_height, *get_nv12_info(camera_width, camera_height))
|
||||
stage_jit = TinyJit(_stage_program(camera, model_w, model_h, frame_stride), prune=True)
|
||||
package[(camera_width, camera_height)] = {
|
||||
"stage_inputs": _capture_and_freeze(
|
||||
stage_jit,
|
||||
partial(_rand_u8_inputs, keys=["frame", "big_frame"], shape=camera.size, device=WARP_DEVICE),
|
||||
["img_q", "big_img_q", "tfm", "big_tfm"],
|
||||
partial(_vision_queue_buffers, meta_by_role["vision"]["input_shapes"], frame_stride),
|
||||
)
|
||||
}
|
||||
|
||||
with open(args.output, "wb") as handle:
|
||||
pickle.dump(package, handle)
|
||||
print(f"Saved combined split runtime to {args.output} ({os.path.getsize(args.output) / 1e6:.2f} MB)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
422
iqpilot/selfdrive/iqmodeld/tools/compile_supercombo.py
Normal file
422
iqpilot/selfdrive/iqmodeld/tools/compile_supercombo.py
Normal file
@@ -0,0 +1,422 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import atexit
|
||||
import math
|
||||
import os
|
||||
import pickle
|
||||
import re
|
||||
import tempfile
|
||||
import time
|
||||
from functools import partial
|
||||
from collections import namedtuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
def _patch_tinygrad_fetch_fw():
|
||||
import hashlib
|
||||
import pathlib
|
||||
import zstandard
|
||||
from tinygrad import helpers
|
||||
_orig = helpers.fetch_fw
|
||||
def fetch_fw(path, name, sha256):
|
||||
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
|
||||
return _orig(path, name, sha256)
|
||||
helpers.fetch_fw = fetch_fw
|
||||
_patch_tinygrad_fetch_fw()
|
||||
|
||||
from tinygrad.tensor import Tensor
|
||||
from tinygrad.helpers import Context
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.engine.jit import TinyJit
|
||||
|
||||
|
||||
NV12Frame = namedtuple("NV12Frame", ['width', 'height', 'stride', 'y_height', 'uv_height', 'size'])
|
||||
WARP_INPUTS = ['tfm', 'big_tfm']
|
||||
POLICY_INPUTS = ['img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs']
|
||||
|
||||
UV_SCALE_MATRIX = np.array([[0.5, 0, 0], [0, 0.5, 0], [0, 0, 1]], dtype=np.float32)
|
||||
UV_SCALE_MATRIX_INV = np.linalg.inv(UV_SCALE_MATRIX)
|
||||
|
||||
WARP_DEV = os.getenv('WARP_DEV')
|
||||
|
||||
|
||||
def make_random_images(keys, shape, device=None):
|
||||
return {k: Tensor.randint(shape, low=0, high=256, dtype='uint8', device=device).realize() for k in keys}
|
||||
|
||||
|
||||
def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad, border_fill_val=None):
|
||||
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):
|
||||
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: NV12Frame, model_w, model_h):
|
||||
cam_w, cam_h, stride, y_height, uv_height, _ = nv12
|
||||
uv_offset = stride * y_height
|
||||
stride_pad = stride - cam_w
|
||||
|
||||
def frame_prepare_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=WARP_DEV)
|
||||
uv = input_frame[uv_offset:uv_offset + uv_height * stride].reshape(uv_height, stride)
|
||||
with Context(SPLIT_REDUCEOP=0):
|
||||
y = warp_perspective_tinygrad(input_frame[:cam_h*stride],
|
||||
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))
|
||||
tensor = frames_to_tensor(yuv)
|
||||
return tensor
|
||||
return frame_prepare_tinygrad
|
||||
|
||||
|
||||
def make_warp_input_queues(vision_input_shapes, frame_skip, device):
|
||||
img = vision_input_shapes['img'] # (1, 12, 128, 256)
|
||||
n_frames = img[1] // 6
|
||||
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3])
|
||||
|
||||
npy = {
|
||||
'tfm': np.zeros((3, 3), dtype=np.float32),
|
||||
'big_tfm': np.zeros((3, 3), dtype=np.float32),
|
||||
}
|
||||
input_queues = {
|
||||
'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||
'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||
**{k: Tensor(v, device='NPY').realize() for k, v in npy.items()},
|
||||
}
|
||||
return input_queues, npy
|
||||
|
||||
|
||||
def get_policy_npy_shapes(input_shapes):
|
||||
dp = input_shapes['desire_pulse'] # (1, 25, 8)
|
||||
tc = input_shapes['traffic_convention'] # (1, 2)
|
||||
at = input_shapes['action_t'] # (1, 2)
|
||||
fb = input_shapes['features_buffer'] # (1, 24, 512)
|
||||
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()]
|
||||
|
||||
|
||||
def make_input_queues(input_shapes, frame_skip, device):
|
||||
input_queues, npy = make_warp_input_queues(input_shapes, frame_skip, device)
|
||||
|
||||
fb = input_shapes['features_buffer'] # (1, 24, 512), past features only; the model appends the current frame's feature
|
||||
dp = input_shapes['desire_pulse'] # (1, 25, 8)
|
||||
|
||||
shapes, sizes = get_policy_npy_shapes(input_shapes)
|
||||
packed_npy_inputs = np.zeros(sum(sizes), dtype=np.float32)
|
||||
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(),
|
||||
'desire_q': Tensor(np.zeros((frame_skip * dp[1], dp[0], dp[2]), dtype=np.float32), device=device).contiguous().realize(),
|
||||
'packed_npy_inputs': Tensor(packed_npy_inputs, device='NPY').realize(),
|
||||
})
|
||||
return input_queues, npy
|
||||
|
||||
|
||||
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, frame_skip):
|
||||
return buf[::frame_skip].contiguous().flatten(0, 1).unsqueeze(0)
|
||||
|
||||
|
||||
def sample_desire(buf, frame_skip):
|
||||
return buf.reshape(-1, frame_skip, *buf.shape[1:]).max(1).flatten(0, 1).unsqueeze(0)
|
||||
|
||||
|
||||
def make_warp(nv12, model_w, model_h, frame_skip):
|
||||
frame_prepare = make_frame_prepare(nv12, model_w, model_h)
|
||||
|
||||
def warp(tfm, big_tfm, frame, big_frame):
|
||||
tfm = tfm.to(WARP_DEV)
|
||||
big_tfm = big_tfm.to(WARP_DEV)
|
||||
Tensor.realize(tfm, big_tfm)
|
||||
|
||||
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_policy(model_runner, model_metadata, frame_skip):
|
||||
sample_desire_fn = partial(sample_desire, frame_skip=frame_skip)
|
||||
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip)
|
||||
npy_shapes, npy_sizes = get_policy_npy_shapes(model_metadata['input_shapes'])
|
||||
|
||||
def run_policy(warped, img_q, big_img_q, feat_q, desire_q, packed_npy_inputs):
|
||||
packed_npy_inputs = packed_npy_inputs.to(Device.DEFAULT)
|
||||
warped = warped.to(Device.DEFAULT)
|
||||
Tensor.realize(packed_npy_inputs, warped)
|
||||
|
||||
img = shift_and_sample(img_q, warped[0:1], sample_skip_fn)
|
||||
big_img = shift_and_sample(big_img_q, warped[1:2], sample_skip_fn)
|
||||
|
||||
desire, traffic_convention, action_t, prev_feat = (t.reshape(s) for t, s in zip(packed_npy_inputs.split(npy_sizes), npy_shapes.values(), strict=True))
|
||||
desire_buf = shift_and_sample(desire_q, desire.reshape(1, 1, -1), sample_desire_fn)
|
||||
feat_buf = shift_and_sample(feat_q, prev_feat.reshape(1, 1, -1), sample_skip_fn)
|
||||
|
||||
inputs = {
|
||||
'img': img,
|
||||
'big_img': big_img,
|
||||
'features_buffer': feat_buf,
|
||||
'desire_pulse': desire_buf,
|
||||
'traffic_convention': traffic_convention,
|
||||
'action_t': action_t,
|
||||
}
|
||||
out = next(iter(model_runner(inputs).values())).cast('float32')
|
||||
return out,
|
||||
return run_policy
|
||||
|
||||
|
||||
def compile_jit(jit, make_random_inputs, input_keys, make_queues):
|
||||
SEED = 42
|
||||
def random_inputs_run(fn, seed, test_val=None, test_buffers=None, expect_match=True):
|
||||
input_queues, npy = make_queues(Device.DEFAULT)
|
||||
np.random.seed(seed)
|
||||
Tensor.manual_seed(seed)
|
||||
|
||||
testing = test_val is not None or test_buffers is not None
|
||||
n_runs = 1 if testing else 3
|
||||
|
||||
for i in range(n_runs):
|
||||
for v in npy.values():
|
||||
v[:] = np.random.randn(*v.shape).astype(v.dtype)
|
||||
Device.default.synchronize()
|
||||
random_inputs = make_random_inputs()
|
||||
st = time.perf_counter()
|
||||
outs = fn(**{k: input_queues[k] for k in input_keys}, **random_inputs)
|
||||
mt = time.perf_counter()
|
||||
Device.default.synchronize()
|
||||
et = time.perf_counter()
|
||||
print(f" [{i+1}/{n_runs}] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms")
|
||||
|
||||
if i == 0:
|
||||
val = [np.copy(v.numpy()) for v in outs]
|
||||
buffers = [np.copy(v.numpy().copy()) for v in input_queues.values()]
|
||||
|
||||
if test_val is not None:
|
||||
match = all(np.array_equal(a, b) for a, b in zip(val, test_val, strict=True))
|
||||
assert match == expect_match, f"outputs {'differ from' if expect_match else 'match'} baseline (seed={seed})"
|
||||
if test_buffers is not None:
|
||||
match = all(np.array_equal(a, b) for a, b in zip(buffers, test_buffers, strict=True))
|
||||
assert match == expect_match, f"buffers {'differ from' if expect_match else 'match'} baseline (seed={seed})"
|
||||
return val, buffers
|
||||
|
||||
print('capture + replay')
|
||||
test_val, test_buffers = random_inputs_run(jit, SEED)
|
||||
print('pickle round trip')
|
||||
jit = pickle.loads(pickle.dumps(jit))
|
||||
random_inputs_run(jit, SEED, test_val, test_buffers, expect_match=True)
|
||||
random_inputs_run(jit, SEED+1, test_val, test_buffers, expect_match=False)
|
||||
return jit
|
||||
|
||||
|
||||
def _captured_devices(jit) -> set[str]:
|
||||
captured = getattr(jit, 'captured', None)
|
||||
infos = getattr(captured, 'expected_input_info', None)
|
||||
if not infos:
|
||||
return set()
|
||||
|
||||
devices: set[str] = set()
|
||||
for info in infos:
|
||||
if isinstance(info, tuple) and len(info) >= 4 and isinstance(info[3], str):
|
||||
devices.add(info[3])
|
||||
return devices
|
||||
|
||||
|
||||
def _slice_outputs(model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]:
|
||||
return {name: model_outputs[np.newaxis, tensor_slice] for name, tensor_slice in output_slices.items() if name != 'pad'}
|
||||
|
||||
|
||||
def _validate_pose_outputs(parsed_outputs: dict[str, np.ndarray]) -> None:
|
||||
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',
|
||||
'road_transform', 'road_transform_stds',
|
||||
)
|
||||
missing = [name for name in required if name not in parsed_outputs]
|
||||
if missing:
|
||||
raise AssertionError(f"parsed supercombo outputs missing required odometry tensors: {missing}")
|
||||
|
||||
for name in required:
|
||||
values = parsed_outputs[name]
|
||||
if not np.isfinite(values).all():
|
||||
raise AssertionError(f"parsed supercombo output {name} contains non-finite values")
|
||||
|
||||
pose = parsed_outputs['pose'][0]
|
||||
pose_stds = parsed_outputs['pose_stds'][0]
|
||||
road_transform_stds = parsed_outputs['road_transform_stds'][0]
|
||||
wide_stds = parsed_outputs['wide_from_device_euler_stds'][0]
|
||||
|
||||
if pose_stds.min() <= MIN_STD_SANITY_CHECK:
|
||||
raise AssertionError(f"pose_stds min {pose_stds.min()} <= {MIN_STD_SANITY_CHECK}")
|
||||
if road_transform_stds.min() <= MIN_STD_SANITY_CHECK:
|
||||
raise AssertionError(f"road_transform_stds min {road_transform_stds.min()} <= {MIN_STD_SANITY_CHECK}")
|
||||
if wide_stds.min() <= MIN_STD_SANITY_CHECK:
|
||||
raise AssertionError(f"wide_from_device_euler_stds min {wide_stds.min()} <= {MIN_STD_SANITY_CHECK}")
|
||||
|
||||
if np.linalg.norm(pose[:3]) > TRANS_SANITY_CHECK:
|
||||
raise AssertionError(f"pose translation norm {np.linalg.norm(pose[:3])} exceeds {TRANS_SANITY_CHECK}")
|
||||
if np.linalg.norm(pose[3:]) > ROTATION_SANITY_CHECK:
|
||||
raise AssertionError(f"pose rotation norm {np.linalg.norm(pose[3:])} exceeds {ROTATION_SANITY_CHECK}")
|
||||
if np.linalg.norm(pose_stds[:3]) > 10 * TRANS_SANITY_CHECK:
|
||||
raise AssertionError(
|
||||
f"pose translation std norm {np.linalg.norm(pose_stds[:3])} exceeds {10 * TRANS_SANITY_CHECK}"
|
||||
)
|
||||
if np.linalg.norm(pose_stds[3:]) > 10 * ROTATION_SANITY_CHECK:
|
||||
raise AssertionError(
|
||||
f"pose rotation std norm {np.linalg.norm(pose_stds[3:])} exceeds {10 * ROTATION_SANITY_CHECK}"
|
||||
)
|
||||
|
||||
|
||||
def validate_supercombo_release(run_policy_jit, model_runner, model_metadata, frame_skip, expected_device: str) -> None:
|
||||
from iqpilot.selfdrive.iqmodeld.parser import PhaseParser
|
||||
|
||||
direct_fn = make_run_policy(model_runner, model_metadata, frame_skip)
|
||||
parser = PhaseParser()
|
||||
queue_factory = partial(make_input_queues, model_metadata['input_shapes'], frame_skip)
|
||||
image_shape = model_metadata['input_shapes']['img']
|
||||
|
||||
jit_queues, jit_npy = queue_factory(Device.DEFAULT)
|
||||
direct_queues, direct_npy = queue_factory(Device.DEFAULT)
|
||||
|
||||
for payload in (jit_npy, direct_npy):
|
||||
for name, value in payload.items():
|
||||
value[:] = 0 if value.dtype.kind in ('i', 'u') else 0.0
|
||||
|
||||
zero_inputs = {
|
||||
'warped': Tensor(np.zeros((2, 6, *image_shape[2:]), dtype=np.uint8), device=Device.DEFAULT).realize(),
|
||||
}
|
||||
|
||||
direct_outs, = direct_fn(**{k: direct_queues[k] for k in POLICY_INPUTS}, **zero_inputs)
|
||||
jit_outs, = run_policy_jit(**{k: jit_queues[k] for k in POLICY_INPUTS}, **zero_inputs)
|
||||
|
||||
direct_flat = direct_outs.numpy().astype(np.float32).reshape(-1)
|
||||
jit_flat = jit_outs.numpy().astype(np.float32).reshape(-1)
|
||||
|
||||
if not np.allclose(direct_flat, jit_flat, atol=1e-4, rtol=1e-4):
|
||||
max_delta = float(np.max(np.abs(direct_flat - jit_flat)))
|
||||
raise AssertionError(f"JIT supercombo output diverges from direct ONNX execution; max abs delta {max_delta}")
|
||||
|
||||
parsed = parser.parse_vision_outputs(_slice_outputs(jit_flat, model_metadata['output_slices']))
|
||||
_validate_pose_outputs(parsed)
|
||||
|
||||
captured_devices = _captured_devices(run_policy_jit)
|
||||
if expected_device and captured_devices and expected_device not in captured_devices:
|
||||
raise AssertionError(
|
||||
f"compiled run_policy backend mismatch: captured {sorted(captured_devices)} expected {expected_device}"
|
||||
)
|
||||
|
||||
|
||||
def _parse_size(s):
|
||||
w, h = s.lower().split('x')
|
||||
return int(w), int(h)
|
||||
|
||||
|
||||
def read_file_chunked_to_shm(path):
|
||||
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
|
||||
atexit.register(lambda: os.path.exists(tmp_path) and os.remove(tmp_path))
|
||||
return tmp_path
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from tinygrad.nn.onnx import OnnxRunner
|
||||
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,
|
||||
help='camera resolutions WxH (one or more)')
|
||||
p.add_argument('--onnx', required=True)
|
||||
p.add_argument('--output', required=True)
|
||||
p.add_argument('--frame-skip', type=int, required=True)
|
||||
p.add_argument('--expected-device', default='QCOM', help='expected tinygrad backend baked into the JIT')
|
||||
args = p.parse_args()
|
||||
|
||||
model_path = read_file_chunked_to_shm(args.onnx)
|
||||
model_w, model_h = args.model_size
|
||||
|
||||
model_runner = OnnxRunner(model_path)
|
||||
out = {
|
||||
'metadata': build_metadata_record(model_path),
|
||||
'frame_skip': args.frame_skip,
|
||||
}
|
||||
|
||||
run_policy_jit = TinyJit(make_run_policy(model_runner, out['metadata'], args.frame_skip), prune=True)
|
||||
|
||||
make_policy_queues = partial(make_input_queues, out['metadata']['input_shapes'], args.frame_skip)
|
||||
make_random_model_inputs = partial(make_random_images, keys=['warped'], shape=(2, 6, *out['metadata']['input_shapes']['img'][2:]))
|
||||
out['run_policy'] = compile_jit(run_policy_jit, make_random_model_inputs, POLICY_INPUTS,
|
||||
make_policy_queues)
|
||||
validate_supercombo_release(out['run_policy'], model_runner, out['metadata'], args.frame_skip, args.expected_device)
|
||||
|
||||
for cam_w, cam_h in args.camera_resolutions:
|
||||
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=WARP_DEV)
|
||||
warp_enqueue = TinyJit(make_warp(nv12, model_w, model_h, args.frame_skip), prune=True)
|
||||
make_warp_queues = partial(make_warp_input_queues, out['metadata']['input_shapes'], args.frame_skip)
|
||||
out[(cam_w,cam_h)] = compile_jit(warp_enqueue, make_random_warp_inputs, WARP_INPUTS, make_warp_queues)
|
||||
captured_devices = _captured_devices(out[(cam_w,cam_h)])
|
||||
if args.expected_device and captured_devices and args.expected_device not in captured_devices:
|
||||
raise AssertionError(
|
||||
f"compiled warp backend mismatch for {cam_w}x{cam_h}: captured {sorted(captured_devices)} expected {args.expected_device}"
|
||||
)
|
||||
|
||||
with open(args.output, "wb") as f:
|
||||
pickle.dump(out, f)
|
||||
print(f"Saved JITs to {args.output} ({os.path.getsize(args.output) / 1e6:.2f} MB)")
|
||||
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()
|
||||
331
iqpilot/selfdrive/iqmodeld/tools/daemon_jit_compiler.py
Normal file
331
iqpilot/selfdrive/iqmodeld/tools/daemon_jit_compiler.py
Normal file
@@ -0,0 +1,331 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import atexit
|
||||
import os
|
||||
import pickle
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def _install_firmware_fetch_patch() -> None:
|
||||
import hashlib
|
||||
import pathlib
|
||||
|
||||
import zstandard
|
||||
from tinygrad import helpers
|
||||
|
||||
if not hasattr(helpers, "fetch_fw"):
|
||||
return
|
||||
|
||||
original_fetch = helpers.fetch_fw
|
||||
|
||||
def fetch_fw(path, name, sha256):
|
||||
archive_path = pathlib.Path(f"/lib/firmware/{path}/{name}.zst")
|
||||
if archive_path.is_file():
|
||||
blob = zstandard.ZstdDecompressor().stream_reader(archive_path.read_bytes()).read()
|
||||
if hashlib.sha256(blob).hexdigest() == sha256:
|
||||
return blob
|
||||
return original_fetch(path, name, sha256)
|
||||
|
||||
helpers.fetch_fw = fetch_fw
|
||||
|
||||
|
||||
_install_firmware_fetch_patch()
|
||||
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.engine.jit import TinyJit
|
||||
from tinygrad.helpers import Context
|
||||
from tinygrad.nn.onnx import OnnxRunner
|
||||
from tinygrad.tensor import Tensor
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FrameGeometry:
|
||||
width: int
|
||||
height: int
|
||||
stride: int
|
||||
y_height: int
|
||||
uv_height: int
|
||||
size: int
|
||||
|
||||
|
||||
WARP_INPUT_NAMES = ["img_q", "big_img_q", "tfm", "big_tfm"]
|
||||
POLICY_INPUT_NAMES = ["feat_q", "desire_q", "desire", "traffic_convention", "action_t"]
|
||||
WARP_DEV = os.getenv("WARP_DEV")
|
||||
|
||||
|
||||
def _random_tensor_inputs(keys: list[str], shape, device=None):
|
||||
return {key: Tensor.randint(shape, low=0, high=256, dtype="uint8", device=device).realize() for key in keys}
|
||||
|
||||
|
||||
def _project_frame(src_flat, inverse_matrix, dst_shape, src_shape, stride_pad, border_fill_val=None):
|
||||
dst_w, dst_h = dst_shape
|
||||
src_h, src_w = src_shape
|
||||
|
||||
x = Tensor.arange(dst_w, device=WARP_DEV).reshape(1, dst_w).expand(dst_h, dst_w).reshape(-1)
|
||||
y = Tensor.arange(dst_h, device=WARP_DEV).reshape(dst_h, 1).expand(dst_h, dst_w).reshape(-1)
|
||||
|
||||
src_x = inverse_matrix[0, 0] * x + inverse_matrix[0, 1] * y + inverse_matrix[0, 2]
|
||||
src_y = inverse_matrix[1, 0] * x + inverse_matrix[1, 1] * y + inverse_matrix[1, 2]
|
||||
src_w_scale = inverse_matrix[2, 0] * x + inverse_matrix[2, 1] * y + inverse_matrix[2, 2]
|
||||
|
||||
src_x = src_x / src_w_scale
|
||||
src_y = src_y / src_w_scale
|
||||
|
||||
rounded_x = Tensor.round(src_x)
|
||||
rounded_y = Tensor.round(src_y)
|
||||
clipped_x = rounded_x.clip(0, src_w - 1).cast("int")
|
||||
clipped_y = rounded_y.clip(0, src_h - 1).cast("int")
|
||||
gather_index = clipped_y * (src_w + stride_pad) + clipped_x
|
||||
sampled = src_flat[gather_index]
|
||||
|
||||
if border_fill_val is None:
|
||||
return sampled
|
||||
|
||||
inside = ((rounded_x >= 0) & (rounded_x <= src_w - 1) & (rounded_y >= 0) & (rounded_y <= src_h - 1)).cast(sampled.dtype)
|
||||
return sampled * inside + Tensor(border_fill_val, dtype=sampled.dtype) * (1 - inside)
|
||||
|
||||
|
||||
def _nv12_to_model_planes(yuv_frame):
|
||||
y_height = (yuv_frame.shape[0] * 2) // 3
|
||||
frame_width = yuv_frame.shape[1]
|
||||
return Tensor.cat(
|
||||
yuv_frame[0:y_height:2, 0::2],
|
||||
yuv_frame[1:y_height:2, 0::2],
|
||||
yuv_frame[0:y_height:2, 1::2],
|
||||
yuv_frame[1:y_height:2, 1::2],
|
||||
yuv_frame[y_height:y_height + y_height // 4].reshape((y_height // 2, frame_width // 2)),
|
||||
yuv_frame[y_height + y_height // 4:y_height + y_height // 2].reshape((y_height // 2, frame_width // 2)),
|
||||
dim=0,
|
||||
).reshape((6, y_height // 2, frame_width // 2))
|
||||
|
||||
|
||||
def _warp_kernel_factory(nv12: FrameGeometry, model_w: int, model_h: int):
|
||||
uv_offset = nv12.stride * nv12.y_height
|
||||
stride_pad = nv12.stride - nv12.width
|
||||
|
||||
def prepare_frame(nv12_blob, inverse_matrix):
|
||||
inverse_uv = inverse_matrix * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]], device=WARP_DEV)
|
||||
uv_plane = nv12_blob[uv_offset:uv_offset + nv12.uv_height * nv12.stride].reshape(nv12.uv_height, nv12.stride)
|
||||
with Context(SPLIT_REDUCEOP=0):
|
||||
y_plane = _project_frame(nv12_blob[:nv12.height * nv12.stride], inverse_matrix, (model_w, model_h), (nv12.height, nv12.width), stride_pad).realize()
|
||||
u_plane = _project_frame(uv_plane[:nv12.height // 2, :nv12.width:2].flatten(), inverse_uv, (model_w // 2, model_h // 2), (nv12.height // 2, nv12.width // 2), 0).realize()
|
||||
v_plane = _project_frame(uv_plane[:nv12.height // 2, 1:nv12.width:2].flatten(), inverse_uv, (model_w // 2, model_h // 2), (nv12.height // 2, nv12.width // 2), 0).realize()
|
||||
return _nv12_to_model_planes(y_plane.cat(u_plane).cat(v_plane).reshape((model_h * 3 // 2, model_w)))
|
||||
|
||||
return prepare_frame
|
||||
|
||||
|
||||
def _vision_queue_state(vision_shapes, frame_skip, device):
|
||||
img_shape = vision_shapes["img"]
|
||||
frame_history = img_shape[1] // 6
|
||||
queue_shape = (frame_skip * (frame_history - 1) + 1, 6, img_shape[2], img_shape[3])
|
||||
numpy_state = {
|
||||
"tfm": np.zeros((3, 3), dtype=np.float32),
|
||||
"big_tfm": np.zeros((3, 3), dtype=np.float32),
|
||||
}
|
||||
tensor_state = {
|
||||
"img_q": Tensor(np.zeros(queue_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||
"big_img_q": Tensor(np.zeros(queue_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||
**{name: Tensor(value, device="NPY").realize() for name, value in numpy_state.items()},
|
||||
}
|
||||
return tensor_state, numpy_state
|
||||
|
||||
|
||||
def _policy_queue_state(vision_shapes, policy_shapes, frame_skip, device):
|
||||
tensor_state, numpy_state = _vision_queue_state(vision_shapes, frame_skip, device)
|
||||
feature_shape = policy_shapes["features_buffer"]
|
||||
desire_shape = policy_shapes["desire_pulse"]
|
||||
traffic_shape = policy_shapes["traffic_convention"]
|
||||
action_shape = traffic_shape
|
||||
|
||||
policy_numpy = {
|
||||
"desire": np.zeros(desire_shape[2], dtype=np.float32),
|
||||
"traffic_convention": np.zeros(traffic_shape, dtype=np.float32),
|
||||
"action_t": np.zeros(action_shape, dtype=np.float32),
|
||||
}
|
||||
numpy_state.update(policy_numpy)
|
||||
tensor_state.update({
|
||||
"feat_q": Tensor(np.zeros((frame_skip * (feature_shape[1] - 1) + 1, feature_shape[0], feature_shape[2]), dtype=np.float32), device=device).contiguous().realize(),
|
||||
"desire_q": Tensor(np.zeros((frame_skip * desire_shape[1], desire_shape[0], desire_shape[2]), dtype=np.float32), device=device).contiguous().realize(),
|
||||
**{name: Tensor(value, device="NPY").realize() for name, value in policy_numpy.items()},
|
||||
})
|
||||
return tensor_state, numpy_state
|
||||
|
||||
|
||||
def _roll_queue(queue_tensor, incoming, sampler):
|
||||
queue_tensor.assign(queue_tensor[1:].cat(incoming, dim=0).contiguous())
|
||||
return sampler(queue_tensor)
|
||||
|
||||
|
||||
def _sample_sparse(queue_tensor, frame_skip):
|
||||
return queue_tensor[::frame_skip].contiguous().flatten(0, 1).unsqueeze(0)
|
||||
|
||||
|
||||
def _sample_desire(queue_tensor, frame_skip):
|
||||
return queue_tensor.reshape(-1, frame_skip, *queue_tensor.shape[1:]).max(1).flatten(0, 1).unsqueeze(0)
|
||||
|
||||
|
||||
def _build_warp_enqueuer(nv12: FrameGeometry, model_w: int, model_h: int, frame_skip: int):
|
||||
prepare_frame = _warp_kernel_factory(nv12, model_w, model_h)
|
||||
sparse_sampler = partial(_sample_sparse, frame_skip=frame_skip)
|
||||
|
||||
def enqueue(img_q, big_img_q, tfm, big_tfm, frame, big_frame):
|
||||
tfm = tfm.to(WARP_DEV)
|
||||
big_tfm = big_tfm.to(WARP_DEV)
|
||||
Tensor.realize(tfm, big_tfm)
|
||||
|
||||
warped_main = prepare_frame(frame, tfm).unsqueeze(0).to(Device.DEFAULT)
|
||||
warped_big = prepare_frame(big_frame, big_tfm).unsqueeze(0).to(Device.DEFAULT)
|
||||
return (
|
||||
_roll_queue(img_q, warped_main, sparse_sampler),
|
||||
_roll_queue(big_img_q, warped_big, sparse_sampler),
|
||||
)
|
||||
|
||||
return enqueue
|
||||
|
||||
|
||||
def _policy_executor(model_runners, model_metadata, frame_skip):
|
||||
desire_sampler = partial(_sample_desire, frame_skip=frame_skip)
|
||||
sparse_sampler = partial(_sample_sparse, frame_skip=frame_skip)
|
||||
hidden_slice = model_metadata["vision"]["output_slices"]["hidden_state"]
|
||||
|
||||
def execute(img, big_img, feat_q, desire_q, desire, traffic_convention, action_t):
|
||||
desire = desire.to(Device.DEFAULT)
|
||||
traffic_convention = traffic_convention.to(Device.DEFAULT)
|
||||
action_t = action_t.to(Device.DEFAULT)
|
||||
Tensor.realize(desire, traffic_convention, action_t)
|
||||
|
||||
desire_buffer = _roll_queue(desire_q, desire.reshape(1, 1, -1), desire_sampler)
|
||||
vision_output = next(iter(model_runners["vision"]({"img": img, "big_img": big_img}).values())).cast("float32")
|
||||
|
||||
hidden_state = vision_output[:, hidden_slice].reshape(1, -1).unsqueeze(0)
|
||||
feature_buffer = _roll_queue(feat_q, hidden_state, sparse_sampler)
|
||||
|
||||
on_inputs = {
|
||||
"features_buffer": feature_buffer,
|
||||
"desire_pulse": desire_buffer,
|
||||
"traffic_convention": traffic_convention,
|
||||
"action_t": action_t,
|
||||
}
|
||||
on_output = next(iter(model_runners["on_policy"](on_inputs).values())).cast("float32")
|
||||
off_output = next(iter(model_runners["off_policy"](on_inputs).values())).cast("float32")
|
||||
return vision_output, on_output, off_output
|
||||
|
||||
return execute
|
||||
|
||||
|
||||
def _replay_and_freeze(jit_runner, random_inputs_factory, queue_keys, queue_factory):
|
||||
seed = 42
|
||||
|
||||
def validate(fn, seed_value, baseline_output=None, baseline_buffers=None, expect_match=True):
|
||||
queue_tensors, numpy_values = queue_factory(Device.DEFAULT)
|
||||
np.random.seed(seed_value)
|
||||
Tensor.manual_seed(seed_value)
|
||||
|
||||
replay_count = 1 if (baseline_output is not None or baseline_buffers is not None) else 3
|
||||
for run_index in range(replay_count):
|
||||
for value in numpy_values.values():
|
||||
value[:] = np.random.randn(*value.shape).astype(value.dtype)
|
||||
Device.default.synchronize()
|
||||
random_inputs = random_inputs_factory()
|
||||
start = time.perf_counter()
|
||||
outputs = fn(**{key: queue_tensors[key] for key in queue_keys}, **random_inputs)
|
||||
enqueue_done = time.perf_counter()
|
||||
Device.default.synchronize()
|
||||
total_done = time.perf_counter()
|
||||
print(f" [{run_index + 1}/{replay_count}] enqueue {(enqueue_done - start) * 1e3:6.2f} ms -- total {(total_done - start) * 1e3:6.2f} ms")
|
||||
|
||||
if run_index == 0:
|
||||
output_snapshot = [np.copy(value.numpy()) for value in outputs]
|
||||
buffer_snapshot = [np.copy(value.numpy().copy()) for value in queue_tensors.values()]
|
||||
|
||||
if baseline_output is not None:
|
||||
matches = all(np.array_equal(current, reference) for current, reference in zip(output_snapshot, baseline_output, strict=True))
|
||||
assert matches == expect_match, f"outputs {'differ from' if expect_match else 'match'} baseline (seed={seed_value})"
|
||||
if baseline_buffers is not None:
|
||||
matches = all(np.array_equal(current, reference) for current, reference in zip(buffer_snapshot, baseline_buffers, strict=True))
|
||||
assert matches == expect_match, f"buffers {'differ from' if expect_match else 'match'} baseline (seed={seed_value})"
|
||||
return output_snapshot, buffer_snapshot
|
||||
|
||||
print("capture + replay")
|
||||
first_output, first_buffers = validate(jit_runner, seed)
|
||||
print("pickle round trip")
|
||||
frozen = pickle.loads(pickle.dumps(jit_runner))
|
||||
validate(frozen, seed, first_output, first_buffers, expect_match=True)
|
||||
validate(frozen, seed + 1, first_output, first_buffers, expect_match=False)
|
||||
return frozen
|
||||
|
||||
|
||||
def _parse_size(text: str) -> tuple[int, int]:
|
||||
width, height = text.lower().split("x")
|
||||
return int(width), int(height)
|
||||
|
||||
|
||||
def _read_file_to_shared_memory(path: str) -> str:
|
||||
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))
|
||||
with open(shm_path, "wb") as handle:
|
||||
handle.write(read_file_chunked(path))
|
||||
return shm_path
|
||||
|
||||
|
||||
def _arg_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--model-size", type=_parse_size, required=True, help="model input WxH")
|
||||
parser.add_argument("--camera-resolutions", type=_parse_size, nargs="+", required=True, help="camera resolutions WxH (one or more)")
|
||||
parser.add_argument("--vision-onnx", required=True)
|
||||
parser.add_argument("--off-policy-onnx", required=True)
|
||||
parser.add_argument("--on-policy-onnx", required=True)
|
||||
parser.add_argument("--output", required=True)
|
||||
parser.add_argument("--frame-skip", type=int, required=True)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
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
|
||||
|
||||
model_paths = {
|
||||
"vision": _read_file_to_shared_memory(args.vision_onnx),
|
||||
"off_policy": _read_file_to_shared_memory(args.off_policy_onnx),
|
||||
"on_policy": _read_file_to_shared_memory(args.on_policy_onnx),
|
||||
}
|
||||
model_runners = {name: OnnxRunner(path) for name, path in model_paths.items()}
|
||||
metadata = {name: build_metadata_record(path) for name, path in model_paths.items()}
|
||||
|
||||
assert metadata["off_policy"]["input_shapes"] == metadata["on_policy"]["input_shapes"]
|
||||
|
||||
output_package: dict = {"metadata": metadata}
|
||||
policy_jit = TinyJit(_policy_executor(model_runners, metadata, args.frame_skip), prune=True)
|
||||
policy_queue_factory = partial(_policy_queue_state, metadata["vision"]["input_shapes"], metadata["on_policy"]["input_shapes"], args.frame_skip)
|
||||
random_model_inputs = partial(_random_tensor_inputs, keys=["img", "big_img"], shape=metadata["vision"]["input_shapes"]["img"])
|
||||
output_package["run_policy"] = _replay_and_freeze(policy_jit, random_model_inputs, POLICY_INPUT_NAMES, policy_queue_factory)
|
||||
|
||||
for cam_w, cam_h in args.camera_resolutions:
|
||||
nv12 = FrameGeometry(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
|
||||
warp_jit = TinyJit(_build_warp_enqueuer(nv12, model_w, model_h, args.frame_skip), prune=True)
|
||||
warp_queue_factory = partial(_vision_queue_state, metadata["vision"]["input_shapes"], args.frame_skip)
|
||||
random_warp_inputs = partial(_random_tensor_inputs, keys=["frame", "big_frame"], shape=nv12.size, device=WARP_DEV)
|
||||
output_package[(cam_w, cam_h)] = _replay_and_freeze(warp_jit, random_warp_inputs, WARP_INPUT_NAMES, warp_queue_factory)
|
||||
|
||||
output_package["frame_skip"] = args.frame_skip
|
||||
with open(args.output, "wb") as handle:
|
||||
pickle.dump(output_package, handle)
|
||||
print(f"Saved JITs to {args.output} ({os.path.getsize(args.output) / 1e6:.2f} MB)")
|
||||
return 0
|
||||
128
iqpilot/selfdrive/iqmodeld/tools/install_models_pc.py
Executable file
128
iqpilot/selfdrive/iqmodeld/tools/install_models_pc.py
Executable 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 base64
|
||||
import pickle
|
||||
import shutil
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import onnx
|
||||
|
||||
from iqpilot.system.hardware.hw import Paths
|
||||
|
||||
_MODEL_STEMS = ("driving_off_policy", "driving_on_policy", "driving_policy", "driving_vision")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _ModelBundle:
|
||||
stem: str
|
||||
onnx_path: Path
|
||||
artifact_path: Path
|
||||
metadata_path: Path
|
||||
|
||||
|
||||
def _tensor_shape(value_info) -> tuple[int, ...]:
|
||||
return tuple(int(dim.dim_value) for dim in value_info.type.tensor_type.shape.dim)
|
||||
|
||||
|
||||
def _metadata_property(graph_model, key: str) -> str | None:
|
||||
for property_item in graph_model.metadata_props:
|
||||
if property_item.key == key:
|
||||
return property_item.value
|
||||
return None
|
||||
|
||||
|
||||
def _decode_output_slices(encoded_value: str):
|
||||
return pickle.loads(base64.b64decode(encoded_value.encode()))
|
||||
|
||||
|
||||
def _metadata_record(graph_model) -> dict:
|
||||
encoded_slices = _metadata_property(graph_model, "output_slices")
|
||||
if encoded_slices is None:
|
||||
raise ValueError("output_slices metadata missing")
|
||||
return {
|
||||
"model_checkpoint": _metadata_property(graph_model, "model_checkpoint"),
|
||||
"output_slices": _decode_output_slices(encoded_slices),
|
||||
"input_shapes": {item.name: _tensor_shape(item) for item in graph_model.graph.input},
|
||||
"output_shapes": {item.name: _tensor_shape(item) for item in graph_model.graph.output},
|
||||
}
|
||||
|
||||
|
||||
def generate_metadata_pkl(model_path, output_path):
|
||||
try:
|
||||
graph_model = onnx.load(str(model_path))
|
||||
metadata = _metadata_record(graph_model)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
with open(output_path, "wb") as handle:
|
||||
pickle.dump(metadata, handle)
|
||||
return True
|
||||
|
||||
|
||||
def _discover_model_bundles(model_dir: Path) -> list[_ModelBundle]:
|
||||
bundles: list[_ModelBundle] = []
|
||||
for stem in _MODEL_STEMS:
|
||||
onnx_path = model_dir / f"{stem}.onnx"
|
||||
if not onnx_path.exists():
|
||||
continue
|
||||
bundles.append(_ModelBundle(
|
||||
stem=stem,
|
||||
onnx_path=onnx_path,
|
||||
artifact_path=model_dir / f"{stem}_tinygrad.pkl",
|
||||
metadata_path=model_dir / f"{stem}_metadata.pkl",
|
||||
))
|
||||
return bundles
|
||||
|
||||
|
||||
def _prompt_short_name(found_stems: list[str]) -> str | None:
|
||||
try:
|
||||
response = input(f"Found models ({', '.join(found_stems)}). Enter model short name (e.g. wmiv4): ").strip()
|
||||
except EOFError:
|
||||
return None
|
||||
return response or None
|
||||
|
||||
|
||||
def _ensure_metadata_file(bundle: _ModelBundle) -> None:
|
||||
if bundle.metadata_path.exists():
|
||||
return
|
||||
generate_metadata_pkl(bundle.onnx_path, bundle.metadata_path)
|
||||
|
||||
|
||||
def _install_bundle(bundle: _ModelBundle, suffix: str, destination_root: Path) -> None:
|
||||
_ensure_metadata_file(bundle)
|
||||
renamed_artifact = destination_root / f"{bundle.stem}_{suffix}_tinygrad.pkl"
|
||||
renamed_metadata = destination_root / f"{bundle.stem}_{suffix}_metadata.pkl"
|
||||
if bundle.artifact_path.exists():
|
||||
shutil.move(str(bundle.artifact_path), str(renamed_artifact))
|
||||
if bundle.metadata_path.exists():
|
||||
shutil.move(str(bundle.metadata_path), str(renamed_metadata))
|
||||
|
||||
|
||||
def install_models(model_dir):
|
||||
source_root = Path(model_dir)
|
||||
bundles = _discover_model_bundles(source_root)
|
||||
if not bundles:
|
||||
return
|
||||
|
||||
short_name = _prompt_short_name([bundle.stem for bundle in bundles])
|
||||
if short_name is None:
|
||||
print("No name provided, skipping installation.")
|
||||
return
|
||||
|
||||
destination_root = Path(Paths.model_root())
|
||||
destination_root.mkdir(parents=True, exist_ok=True)
|
||||
for bundle in bundles:
|
||||
_install_bundle(bundle, short_name, destination_root)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: install_models_pc.py <model_dir>")
|
||||
sys.exit(1)
|
||||
install_models(sys.argv[1])
|
||||
94
iqpilot/selfdrive/iqmodeld/transforms/warp_geometry.cc
Normal file
94
iqpilot/selfdrive/iqmodeld/transforms/warp_geometry.cc
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
*/
|
||||
#include "iqpilot/selfdrive/iqmodeld/transforms/warp_geometry.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <cstring>
|
||||
|
||||
#include "common/clutil.h"
|
||||
|
||||
namespace {
|
||||
|
||||
void reset_sampler_state(WarpSamplerState *sampler) {
|
||||
memset(sampler, 0, sizeof(*sampler));
|
||||
}
|
||||
|
||||
void write_projection(cl_command_queue queue, cl_mem dst, const mat3 &projection) {
|
||||
CL_CHECK(clEnqueueWriteBuffer(queue, dst, CL_TRUE, 0, 3 * 3 * sizeof(float), (void *)projection.v, 0, NULL, NULL));
|
||||
}
|
||||
|
||||
void configure_sample_window(WarpSamplerState *sampler, cl_mem src, int src_stride, int src_px_stride,
|
||||
int src_offset, int src_rows, int src_cols,
|
||||
cl_mem dst, int dst_stride, int dst_offset, int dst_rows, int dst_cols,
|
||||
cl_mem projection_cl) {
|
||||
CL_CHECK(clSetKernelArg(sampler->bilinear_kernel, 0, sizeof(cl_mem), &src));
|
||||
CL_CHECK(clSetKernelArg(sampler->bilinear_kernel, 1, sizeof(cl_int), &src_stride));
|
||||
CL_CHECK(clSetKernelArg(sampler->bilinear_kernel, 2, sizeof(cl_int), &src_px_stride));
|
||||
CL_CHECK(clSetKernelArg(sampler->bilinear_kernel, 3, sizeof(cl_int), &src_offset));
|
||||
CL_CHECK(clSetKernelArg(sampler->bilinear_kernel, 4, sizeof(cl_int), &src_rows));
|
||||
CL_CHECK(clSetKernelArg(sampler->bilinear_kernel, 5, sizeof(cl_int), &src_cols));
|
||||
CL_CHECK(clSetKernelArg(sampler->bilinear_kernel, 6, sizeof(cl_mem), &dst));
|
||||
CL_CHECK(clSetKernelArg(sampler->bilinear_kernel, 7, sizeof(cl_int), &dst_stride));
|
||||
CL_CHECK(clSetKernelArg(sampler->bilinear_kernel, 8, sizeof(cl_int), &dst_offset));
|
||||
CL_CHECK(clSetKernelArg(sampler->bilinear_kernel, 9, sizeof(cl_int), &dst_rows));
|
||||
CL_CHECK(clSetKernelArg(sampler->bilinear_kernel, 10, sizeof(cl_int), &dst_cols));
|
||||
CL_CHECK(clSetKernelArg(sampler->bilinear_kernel, 11, sizeof(cl_mem), &projection_cl));
|
||||
}
|
||||
|
||||
void enqueue_sample_window(cl_command_queue queue, cl_kernel kernel, int width, int height) {
|
||||
const size_t work_size[2] = {static_cast<size_t>(width), static_cast<size_t>(height)};
|
||||
CL_CHECK(clEnqueueNDRangeKernel(queue, kernel, 2, NULL, work_size, NULL, 0, 0, NULL));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void warp_sampler_init(WarpSamplerState *sampler, cl_context ctx, cl_device_id device_id) {
|
||||
reset_sampler_state(sampler);
|
||||
cl_program program_handle = cl_program_from_file(ctx, device_id, TRANSFORM_PATH, "");
|
||||
sampler->bilinear_kernel = CL_CHECK_ERR(clCreateKernel(program_handle, "projectPlaneBilinear", &err));
|
||||
CL_CHECK(clReleaseProgram(program_handle));
|
||||
|
||||
sampler->full_res_matrix_cl = CL_CHECK_ERR(clCreateBuffer(ctx, CL_MEM_READ_WRITE, 3 * 3 * sizeof(float), NULL, &err));
|
||||
sampler->half_res_matrix_cl = CL_CHECK_ERR(clCreateBuffer(ctx, CL_MEM_READ_WRITE, 3 * 3 * sizeof(float), NULL, &err));
|
||||
}
|
||||
|
||||
void warp_sampler_release(WarpSamplerState *sampler) {
|
||||
CL_CHECK(clReleaseMemObject(sampler->full_res_matrix_cl));
|
||||
CL_CHECK(clReleaseMemObject(sampler->half_res_matrix_cl));
|
||||
CL_CHECK(clReleaseKernel(sampler->bilinear_kernel));
|
||||
}
|
||||
|
||||
void warp_sampler_dispatch(WarpSamplerState *sampler, cl_command_queue queue,
|
||||
cl_mem yuv, int in_width, int in_height, int in_stride, int in_uv_offset,
|
||||
cl_mem out_y, cl_mem out_u, cl_mem out_v,
|
||||
int out_width, int out_height,
|
||||
const mat3 &projection) {
|
||||
const mat3 luma_projection = projection;
|
||||
const mat3 chroma_projection = transform_scale_buffer(projection, 0.5);
|
||||
|
||||
write_projection(queue, sampler->full_res_matrix_cl, luma_projection);
|
||||
write_projection(queue, sampler->half_res_matrix_cl, chroma_projection);
|
||||
|
||||
configure_sample_window(sampler, yuv, in_stride, 1, 0, in_height, in_width,
|
||||
out_y, out_width, 0, out_height, out_width,
|
||||
sampler->full_res_matrix_cl);
|
||||
enqueue_sample_window(queue, sampler->bilinear_kernel, out_width, out_height);
|
||||
|
||||
const int chroma_width = in_width / 2;
|
||||
const int chroma_height = in_height / 2;
|
||||
const int out_chroma_width = out_width / 2;
|
||||
const int out_chroma_height = out_height / 2;
|
||||
const int in_u_offset = in_uv_offset;
|
||||
const int in_v_offset = in_uv_offset + 1;
|
||||
|
||||
configure_sample_window(sampler, yuv, in_stride, 2, in_u_offset, chroma_height, chroma_width,
|
||||
out_u, out_chroma_width, 0, out_chroma_height, out_chroma_width,
|
||||
sampler->half_res_matrix_cl);
|
||||
enqueue_sample_window(queue, sampler->bilinear_kernel, out_chroma_width, out_chroma_height);
|
||||
|
||||
configure_sample_window(sampler, yuv, in_stride, 2, in_v_offset, chroma_height, chroma_width,
|
||||
out_v, out_chroma_width, 0, out_chroma_height, out_chroma_width,
|
||||
sampler->half_res_matrix_cl);
|
||||
enqueue_sample_window(queue, sampler->bilinear_kernel, out_chroma_width, out_chroma_height);
|
||||
}
|
||||
57
iqpilot/selfdrive/iqmodeld/transforms/warp_geometry.cl
Normal file
57
iqpilot/selfdrive/iqmodeld/transforms/warp_geometry.cl
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
*/
|
||||
#define INTER_BITS 5
|
||||
#define INTER_TAB_SIZE (1 << INTER_BITS)
|
||||
#define INTER_REMAP_COEF_BITS 15
|
||||
#define INTER_REMAP_COEF_SCALE (1 << INTER_REMAP_COEF_BITS)
|
||||
|
||||
__kernel void projectPlaneBilinear(__global const uchar * src,
|
||||
int src_row_stride, int src_px_stride, int src_offset, int src_rows, int src_cols,
|
||||
__global uchar * dst,
|
||||
int dst_row_stride, int dst_offset, int dst_rows, int dst_cols,
|
||||
__constant float * M)
|
||||
{
|
||||
int dx = get_global_id(0);
|
||||
int dy = get_global_id(1);
|
||||
|
||||
if (dx < dst_cols && dy < dst_rows) {
|
||||
float x0 = M[0] * dx + M[1] * dy + M[2];
|
||||
float y0 = M[3] * dx + M[4] * dy + M[5];
|
||||
float w = M[6] * dx + M[7] * dy + M[8];
|
||||
w = w != 0.0f ? INTER_TAB_SIZE / w : 0.0f;
|
||||
|
||||
int x = rint(x0 * w);
|
||||
int y = rint(y0 * w);
|
||||
short sx = convert_short_sat(x >> INTER_BITS);
|
||||
short sy = convert_short_sat(y >> INTER_BITS);
|
||||
short min_col = (short)0;
|
||||
short max_col = convert_short_sat(src_cols - 1);
|
||||
short min_row = (short)0;
|
||||
short max_row = convert_short_sat(src_rows - 1);
|
||||
|
||||
short sx_clamp = clamp(sx, min_col, max_col);
|
||||
short sx_p1_clamp = clamp((short)(sx + 1), min_col, max_col);
|
||||
short sy_clamp = clamp(sy, min_row, max_row);
|
||||
short sy_p1_clamp = clamp((short)(sy + 1), min_row, max_row);
|
||||
|
||||
int top_left = convert_int(src[mad24(sy_clamp, src_row_stride, src_offset + sx_clamp * src_px_stride)]);
|
||||
int top_right = convert_int(src[mad24(sy_clamp, src_row_stride, src_offset + sx_p1_clamp * src_px_stride)]);
|
||||
int bottom_left = convert_int(src[mad24(sy_p1_clamp, src_row_stride, src_offset + sx_clamp * src_px_stride)]);
|
||||
int bottom_right = convert_int(src[mad24(sy_p1_clamp, src_row_stride, src_offset + sx_p1_clamp * src_px_stride)]);
|
||||
|
||||
short ay = (short)(y & (INTER_TAB_SIZE - 1));
|
||||
short ax = (short)(x & (INTER_TAB_SIZE - 1));
|
||||
float taby = 1.f / INTER_TAB_SIZE * ay;
|
||||
float tabx = 1.f / INTER_TAB_SIZE * ax;
|
||||
|
||||
int coeff0 = convert_short_sat_rte((1.0f - taby) * (1.0f - tabx) * INTER_REMAP_COEF_SCALE);
|
||||
int coeff1 = convert_short_sat_rte((1.0f - taby) * tabx * INTER_REMAP_COEF_SCALE);
|
||||
int coeff2 = convert_short_sat_rte(taby * (1.0f - tabx) * INTER_REMAP_COEF_SCALE);
|
||||
int coeff3 = convert_short_sat_rte(taby * tabx * INTER_REMAP_COEF_SCALE);
|
||||
|
||||
int blended = top_left * coeff0 + top_right * coeff1 + bottom_left * coeff2 + bottom_right * coeff3;
|
||||
int dst_index = mad24(dy, dst_row_stride, dst_offset + dx);
|
||||
dst[dst_index] = convert_uchar_sat((blended + (1 << (INTER_REMAP_COEF_BITS - 1))) >> INTER_REMAP_COEF_BITS);
|
||||
}
|
||||
}
|
||||
28
iqpilot/selfdrive/iqmodeld/transforms/warp_geometry.h
Normal file
28
iqpilot/selfdrive/iqmodeld/transforms/warp_geometry.h
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#define CL_USE_DEPRECATED_OPENCL_1_2_APIS
|
||||
#ifdef __APPLE__
|
||||
#include <OpenCL/cl.h>
|
||||
#else
|
||||
#include <CL/cl.h>
|
||||
#endif
|
||||
|
||||
#include "common/mat.h"
|
||||
|
||||
struct WarpSamplerState {
|
||||
cl_kernel bilinear_kernel;
|
||||
cl_mem full_res_matrix_cl;
|
||||
cl_mem half_res_matrix_cl;
|
||||
};
|
||||
|
||||
void warp_sampler_init(WarpSamplerState *sampler, cl_context ctx, cl_device_id device_id);
|
||||
void warp_sampler_release(WarpSamplerState *sampler);
|
||||
|
||||
void warp_sampler_dispatch(WarpSamplerState *sampler, cl_command_queue queue,
|
||||
cl_mem yuv, int in_width, int in_height, int in_stride, int in_uv_offset,
|
||||
cl_mem out_y, cl_mem out_u, cl_mem out_v,
|
||||
int out_width, int out_height,
|
||||
const mat3 &projection);
|
||||
82
iqpilot/selfdrive/iqmodeld/transforms/yuv.cc
Normal file
82
iqpilot/selfdrive/iqmodeld/transforms/yuv.cc
Normal file
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
*/
|
||||
#include "iqpilot/selfdrive/iqmodeld/transforms/yuv.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
namespace {
|
||||
|
||||
void clear_kernel_bundle(PackedFrameKernels *kernels) {
|
||||
memset(kernels, 0, sizeof(*kernels));
|
||||
}
|
||||
|
||||
void bind_kernel_bundle(PackedFrameKernels *kernels, cl_program cl_program_handle) {
|
||||
kernels->y_pair_kernel = CL_CHECK_ERR(clCreateKernel(cl_program_handle, "packLumaHalves", &err));
|
||||
kernels->uv_lane_kernel = CL_CHECK_ERR(clCreateKernel(cl_program_handle, "packChromaPlane", &err));
|
||||
kernels->span_copy_kernel = CL_CHECK_ERR(clCreateKernel(cl_program_handle, "copyPlaneBytes", &err));
|
||||
}
|
||||
|
||||
void launch_linear_kernel(cl_command_queue queue, cl_kernel kernel, size_t work_items) {
|
||||
CL_CHECK(clEnqueueNDRangeKernel(queue, kernel, 1, nullptr, &work_items, nullptr, 0, 0, nullptr));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void packed_frame_kernels_init(PackedFrameKernels *kernels, cl_context ctx, cl_device_id device_id, int width, int height) {
|
||||
clear_kernel_bundle(kernels);
|
||||
kernels->raster_width = width;
|
||||
kernels->raster_height = height;
|
||||
|
||||
char compiler_args[1024];
|
||||
snprintf(compiler_args, sizeof(compiler_args),
|
||||
"-cl-fast-relaxed-math -cl-denorms-are-zero "
|
||||
"-DTRANSFORMED_WIDTH=%d -DTRANSFORMED_HEIGHT=%d",
|
||||
width, height);
|
||||
|
||||
cl_program program_handle = cl_program_from_file(ctx, device_id, LOADYUV_PATH, compiler_args);
|
||||
bind_kernel_bundle(kernels, program_handle);
|
||||
CL_CHECK(clReleaseProgram(program_handle));
|
||||
}
|
||||
|
||||
void packed_frame_kernels_release(PackedFrameKernels *kernels) {
|
||||
CL_CHECK(clReleaseKernel(kernels->y_pair_kernel));
|
||||
CL_CHECK(clReleaseKernel(kernels->uv_lane_kernel));
|
||||
CL_CHECK(clReleaseKernel(kernels->span_copy_kernel));
|
||||
}
|
||||
|
||||
void packed_frame_emit(PackedFrameKernels *kernels, cl_command_queue queue,
|
||||
cl_mem y_plane_cl, cl_mem u_plane_cl, cl_mem v_plane_cl,
|
||||
cl_mem packed_frame_cl) {
|
||||
cl_int output_offset = 0;
|
||||
const size_t luma_work_items = (kernels->raster_width * kernels->raster_height) / 8;
|
||||
const size_t chroma_work_items = ((kernels->raster_width / 2) * (kernels->raster_height / 2)) / 8;
|
||||
|
||||
CL_CHECK(clSetKernelArg(kernels->y_pair_kernel, 0, sizeof(cl_mem), &y_plane_cl));
|
||||
CL_CHECK(clSetKernelArg(kernels->y_pair_kernel, 1, sizeof(cl_mem), &packed_frame_cl));
|
||||
CL_CHECK(clSetKernelArg(kernels->y_pair_kernel, 2, sizeof(cl_int), &output_offset));
|
||||
launch_linear_kernel(queue, kernels->y_pair_kernel, luma_work_items);
|
||||
|
||||
output_offset += kernels->raster_width * kernels->raster_height;
|
||||
CL_CHECK(clSetKernelArg(kernels->uv_lane_kernel, 0, sizeof(cl_mem), &u_plane_cl));
|
||||
CL_CHECK(clSetKernelArg(kernels->uv_lane_kernel, 1, sizeof(cl_mem), &packed_frame_cl));
|
||||
CL_CHECK(clSetKernelArg(kernels->uv_lane_kernel, 2, sizeof(cl_int), &output_offset));
|
||||
launch_linear_kernel(queue, kernels->uv_lane_kernel, chroma_work_items);
|
||||
|
||||
output_offset += (kernels->raster_width / 2) * (kernels->raster_height / 2);
|
||||
CL_CHECK(clSetKernelArg(kernels->uv_lane_kernel, 0, sizeof(cl_mem), &v_plane_cl));
|
||||
CL_CHECK(clSetKernelArg(kernels->uv_lane_kernel, 1, sizeof(cl_mem), &packed_frame_cl));
|
||||
CL_CHECK(clSetKernelArg(kernels->uv_lane_kernel, 2, sizeof(cl_int), &output_offset));
|
||||
launch_linear_kernel(queue, kernels->uv_lane_kernel, chroma_work_items);
|
||||
}
|
||||
|
||||
void packed_frame_clone_range(PackedFrameKernels *kernels, cl_command_queue queue, cl_mem src, cl_mem dst,
|
||||
size_t src_offset, size_t dst_offset, size_t size) {
|
||||
CL_CHECK(clSetKernelArg(kernels->span_copy_kernel, 0, sizeof(cl_mem), &src));
|
||||
CL_CHECK(clSetKernelArg(kernels->span_copy_kernel, 1, sizeof(cl_mem), &dst));
|
||||
CL_CHECK(clSetKernelArg(kernels->span_copy_kernel, 2, sizeof(cl_int), &src_offset));
|
||||
CL_CHECK(clSetKernelArg(kernels->span_copy_kernel, 3, sizeof(cl_int), &dst_offset));
|
||||
launch_linear_kernel(queue, kernels->span_copy_kernel, size / 8);
|
||||
}
|
||||
46
iqpilot/selfdrive/iqmodeld/transforms/yuv.cl
Normal file
46
iqpilot/selfdrive/iqmodeld/transforms/yuv.cl
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
*/
|
||||
#define UV_SIZE ((TRANSFORMED_WIDTH/2)*(TRANSFORMED_HEIGHT/2))
|
||||
|
||||
__kernel void packLumaHalves(__global uchar8 const * const in_luma,
|
||||
__global uchar * out_frame,
|
||||
int out_offset)
|
||||
{
|
||||
const int gid = get_global_id(0);
|
||||
const int output_index_start = gid * 8;
|
||||
const int row = output_index_start / TRANSFORMED_WIDTH;
|
||||
const int col = output_index_start % TRANSFORMED_WIDTH;
|
||||
const uchar8 luma_block = in_luma[gid];
|
||||
|
||||
__global uchar *top_or_left;
|
||||
__global uchar *bottom_or_right;
|
||||
if ((row & 1) == 0) {
|
||||
top_or_left = out_frame + out_offset;
|
||||
bottom_or_right = out_frame + out_offset + UV_SIZE * 2;
|
||||
} else {
|
||||
top_or_left = out_frame + out_offset + UV_SIZE;
|
||||
bottom_or_right = out_frame + out_offset + UV_SIZE * 3;
|
||||
}
|
||||
|
||||
const int row_stride = (row / 2) * (TRANSFORMED_WIDTH / 2) + col / 2;
|
||||
vstore4(luma_block.s0246, 0, top_or_left + row_stride);
|
||||
vstore4(luma_block.s1357, 0, bottom_or_right + row_stride);
|
||||
}
|
||||
|
||||
__kernel void packChromaPlane(__global uchar8 const * const in_plane,
|
||||
__global uchar8 * out_frame,
|
||||
int out_offset)
|
||||
{
|
||||
const int gid = get_global_id(0);
|
||||
out_frame[gid + out_offset / 8] = in_plane[gid];
|
||||
}
|
||||
|
||||
__kernel void copyPlaneBytes(__global uchar8 * in_plane,
|
||||
__global uchar8 * out_plane,
|
||||
int in_offset,
|
||||
int out_offset)
|
||||
{
|
||||
const int gid = get_global_id(0);
|
||||
out_plane[gid + out_offset / 8] = in_plane[gid + in_offset / 8];
|
||||
}
|
||||
24
iqpilot/selfdrive/iqmodeld/transforms/yuv.h
Normal file
24
iqpilot/selfdrive/iqmodeld/transforms/yuv.h
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "common/clutil.h"
|
||||
|
||||
struct PackedFrameKernels {
|
||||
int raster_width;
|
||||
int raster_height;
|
||||
cl_kernel y_pair_kernel;
|
||||
cl_kernel uv_lane_kernel;
|
||||
cl_kernel span_copy_kernel;
|
||||
};
|
||||
|
||||
void packed_frame_kernels_init(PackedFrameKernels *kernels, cl_context ctx, cl_device_id device_id, int width, int height);
|
||||
void packed_frame_kernels_release(PackedFrameKernels *kernels);
|
||||
|
||||
void packed_frame_emit(PackedFrameKernels *kernels, cl_command_queue queue,
|
||||
cl_mem y_plane_cl, cl_mem u_plane_cl, cl_mem v_plane_cl,
|
||||
cl_mem packed_frame_cl);
|
||||
|
||||
void packed_frame_clone_range(PackedFrameKernels *kernels, cl_command_queue queue, cl_mem src, cl_mem dst,
|
||||
size_t src_offset, size_t dst_offset, size_t size);
|
||||
Reference in New Issue
Block a user