Files
StarPilot/openpilot/selfdrive/modeld/modeld.py
T

667 lines
33 KiB
Python
Executable File

#!/usr/bin/env python3
from collections.abc import Callable
from typing import TYPE_CHECKING, cast
import base64
import ctypes
import gc
from functools import cached_property
import os
os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom
os.environ.setdefault('AM_POWER_LIMIT', '100')
from openpilot.starpilot.models.runtime_bridge.runtime import prepare as prepare_model_runtime, selected_legacy
prepare_model_runtime('driving')
from tinygrad.device import Buffer, Device
from tinygrad.dtype import DType, dtypes
from tinygrad.engine.realize import lower_and_compile
from tinygrad.tensor import Tensor
from tinygrad.helpers import all_int, round_up
from tinygrad.uop.ops import UOp
if TYPE_CHECKING:
from openpilot.starpilot.models.runtime_bridge.legacy_types import LegacyBuffer, LegacyUOpFactory
import math
import pickle
import threading
import time
import numpy as np
import openpilot.cereal.messaging as messaging
from openpilot.cereal import log
from opendbc.car.structs import car
from openpilot.cereal.messaging import PubMaster, SubMaster
from openpilot.cereal.services import SERVICE_LIST
from openpilot.cereal.visionipc import VisionStreamType
from msgq.visionipc import VisionIpcClient, VisionBuf
from opendbc.car.car_helpers import get_demo_car_params
from openpilot.common.swaglog import cloudlog
from openpilot.common.params import Params
from openpilot.common.hardware.usb import cable_connected
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.realtime import config_realtime_process, set_core_affinity, DT_MDL
from openpilot.common.transformations.camera import DEVICE_CAMERAS
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
from openpilot.common.transformations.model import get_warp_matrix
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
from openpilot.starpilot.lateral.lane_change_preferences import LiveLaneChangePolicy
from openpilot.starpilot.lateral.auto_lane_change import ClockEpochGuard, auto_evidence, paired_clocks_ns, session_policy, steering_capable
from openpilot.starpilot.lateral.lane_change_status_wire import (
Direction as StatusDirection, LaneChangeStatus, Phase as StatusPhase, encode_optional as encode_lane_status,
)
from openpilot.starpilot.models.receipt import ModelReceiptOwner
from openpilot.starpilot.models.status import ModelVariant
from openpilot.starpilot.models.startup import report_load_timeout, wait_for_chestnut_power
from openpilot.starpilot.models.recovery import STARTUP_FALLBACK_EXIT
from openpilot.starpilot.navigation.intent import TurnIntent, matching_turn_signal
from openpilot.starpilot.models.catalog import BUNDLED_CURRENT, BY_ID, DEFAULT_SMALL, DEFAULT_SMALL_SHA256
from openpilot.starpilot.models.runner import CatalogModelState, load_verified_model
from openpilot.starpilot.models import jetlink_adapter
import uuid
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, should_stop, smooth_value, get_curvature_from_plan
from openpilot.selfdrive.modeld.parse_model_outputs import Parser
from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState
from openpilot.selfdrive.modeld.constants import LAT_SMOOTH_SECONDS, ModelConstants, Plan
from openpilot.selfdrive.modeld.helpers import MODELS_DIR, chestnut_present, modeld_pkl_path, load_oob, wait_for_chestnut
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
LONG_SMOOTH_SECONDS = 0.3
MIN_LAT_CONTROL_SPEED = 0.3
BIG_MODEL_TIMEOUT = 120
def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action,
lat_action_t: float, long_action_t: float, v_ego: float) -> log.ModelDataV2.Action:
if 'action' not in model_output:
plan = model_output['plan'][0]
desired_accel = 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)
else:
desired_accel = model_output['action'][0,1]
desired_curvature = model_output['action'][0,0] / (max(1.0, v_ego))**2
stop = should_stop(v_ego, desired_accel)
desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, LONG_SMOOTH_SECONDS)
if v_ego > MIN_LAT_CONTROL_SPEED:
desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, LAT_SMOOTH_SECONDS)
else:
desired_curvature = prev_action.desiredCurvature
return log.ModelDataV2.Action(desiredCurvature=float(desired_curvature),
desiredAcceleration=float(desired_accel),
shouldStop=bool(stop))
def drop_chestnut() -> None:
if "AMD" in Device._opened_devices:
for d in Device._opened_devices:
Device[d].pending.pop(Device["AMD"], None)
class ChestnutGpuState:
# GPU metrics require modeld's GPU context
def __init__(self, pm: PubMaster, big: bool):
self.pm = pm
self.big = big
self.valid = True
self.sends = 0
self.metrics = {}
@cached_property
def power_limit(self) -> int:
smu = Device["AMD"].iface.dev_impl.smu
return smu._send_msg(smu.smu_mod.PPSMC_MSG_GetPptLimit, 0, read_back_arg=True, timeout=100)
def send(self) -> None:
msg = messaging.new_message('chestnutGpuState')
state = msg.chestnutGpuState
self.sends += 1
if self.big and "AMD" in Device._opened_devices and self.sends % 100 == 1:
try:
smu = Device["AMD"].iface.dev_impl.smu
metrics_t = smu.smu_mod.SmuMetricsExternal_t
smu._send_msg(smu.smu_mod.PPSMC_MSG_TransferTableSmu2Dram, smu.smu_mod.TABLE_SMU_METRICS, timeout=100)
metrics_buf = bytearray(smu.adev.vram.view(smu.driver_table_paddr, ctypes.sizeof(metrics_t))[:])
metrics = metrics_t.from_buffer(metrics_buf).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,
'gpuUsagePercent': metrics.AverageGfxActivity,
'gpuClockMhz': metrics.AverageGfxclkFrequencyPostDs,
'fanSpeedRpm': metrics.AvgFanRpm}
self.valid = True
except Exception:
if self.valid:
cloudlog.exception("chestnut state read failed")
self.valid = False
self.metrics.clear()
if self.big:
for k, v in self.metrics.items():
setattr(state, k, v)
msg.valid = not self.big or (self.valid and bool(self.metrics))
self.pm.send('chestnutGpuState', msg)
class FrameMeta:
frame_id: int = 0
timestamp_sof: int = 0
timestamp_eof: int = 0
def __init__(self, vipc=None):
if vipc is not None:
self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof
def input_view(buffer: Buffer, shape: tuple[int, ...], dtype: DType, offset: int) -> Tensor:
from openpilot.starpilot.models.runtime_bridge.runtime import selected_legacy
if selected_legacy():
view = cast("LegacyBuffer", buffer).view(math.prod(shape), dtype, offset).ensure_allocated()
return Tensor(cast("LegacyUOpFactory", UOp).from_buffer(view)).reshape(shape)
view = buffer.view(math.prod(shape) * dtype.itemsize, offset).ensure_allocated()
return Tensor(UOp.from_buffer(view, dtype)).reshape(shape)
class ModelState:
lat_delay: float
frame_drop_ratio: float
prev_desire: np.ndarray # for tracking the rising edge of the pulse
get_action_from_model = staticmethod(get_action_from_model)
def __init__(self, cam_w: int, cam_h: int, chestnut: bool):
if not chestnut:
raise ValueError("Small models require the verified catalog loader")
jits = load_oob(modeld_pkl_path(chestnut), chestnut)
self.model_device = jits['input_specs']['new_img'][2]
self.input_shapes = {name: (shape, np.dtype(dtype)) for name, (shape, dtype, _) in jits['input_specs'].items()}
self.state_pairs = {name: f'next_{name}' for name in self.input_shapes if f'next_{name}' in jits['metadata']['output_shapes']}
self.vision_input_names = ('img', 'big_img')
self.output_slices = pickle.loads(base64.b64decode(jits['metadata']['metadata']['output_slices']))
self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
self.chestnut = chestnut
self.model_id = BUNDLED_CURRENT
stride, y_height, uv_height, _ = get_nv12_info(cam_w, cam_h)
self.frame_copy_size = stride * (y_height + uv_height)
self.pack_inputs()
with open(MODELS_DIR / f'{"big_" if chestnut else ""}driving_warp_{cam_w}x{cam_h}_tinygrad.pkl', 'rb') as f:
self.run_warp = pickle.load(f)['run']
self.run_warp.captured._linear = lower_and_compile(self.run_warp.captured._linear)
self.run_model = jits['run']
self.run_model.captured._linear = lower_and_compile(self.run_model.captured._linear)
self.outputs = {name: Tensor(np.zeros(shape, dtype=dtype), device=device).realize() for name, (shape, dtype, device) in jits['output_specs'].items()}
for name, next_name in self.state_pairs.items():
state = self.input_queues[name]
assert all_int(state.shape)
self.outputs[next_name] = input_view(state._buffer(), state.shape, state.dtype, 0)
self.parser = Parser()
def pack_inputs(self) -> None:
# Pack host inputs into one upload to reduce USB transfer overhead for the eGPU.
self.input_queues = {name: Tensor(np.zeros(shape, dtype=dtype), device=self.model_device).realize()
for name, (shape, dtype) in self.input_shapes.items() if name in self.state_pairs}
shapes = {'tfm': (2, 3, 3)} | {name: shape for name, (shape, _) in self.input_shapes.items()
if name not in self.state_pairs and name != 'new_img'}
npy_size = sum(round_up(math.prod(shape) * 4, 128) for shape in shapes.values())
self.packed_input = np.zeros(npy_size + 2 * self.frame_copy_size, dtype=np.uint8)
self.input_host = Tensor(self.packed_input, device='NPY')._buffer()
self.input_device = Tensor(self.packed_input, device=self.model_device)._buffer()
self.npy = {}
offset = 0
for name, shape in shapes.items():
self.npy[name] = np.ndarray(shape, dtype=np.float32, buffer=self.packed_input, offset=offset)
self.input_queues[name] = input_view(self.input_device, shape, dtypes.float32, offset)
offset += round_up(self.npy[name].nbytes, 128)
self.frames = self.packed_input[npy_size:].reshape(2, self.frame_copy_size)
self.warp_inputs = {'input_frame': input_view(self.input_device, self.frames.shape, dtypes.uint8, npy_size), 'M_inv': self.input_queues.pop('tfm')}
def slice_outputs(self, model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]:
return {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()}
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
inputs: dict[str, np.ndarray], after_enqueue: Callable[[], None] | None = None) -> dict[str, np.ndarray]:
for i, key in enumerate(self.vision_input_names):
np.copyto(self.frames[i], np.frombuffer(bufs[key].data, dtype=np.uint8, count=self.frame_copy_size))
self.npy['tfm'][i] = transforms[key]
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
inputs['desire_pulse'][0] = 0
self.npy['desire'][:] = np.where(inputs['desire_pulse'] - self.prev_desire > .99, inputs['desire_pulse'], 0)
self.prev_desire[:] = inputs['desire_pulse']
self.npy['traffic_convention'][:] = inputs['traffic_convention']
self.npy['action_t'][:] = inputs['action_t']
self.input_device.copy_from(self.input_host)
self.input_queues['new_img'] = self.run_warp(**self.warp_inputs)
self.run_model(output_buffers=self.outputs, **self.input_queues)
if (self.chestnut and not selected_legacy() and
(err := Device[cast(str, self.outputs['outputs'].device)].error_state.host.view(fmt='q')[0])):
raise RuntimeError(f"chestnut failed with {err}")
if after_enqueue is not None:
after_enqueue()
model_output = self.outputs['outputs'].numpy()[0]
if self.chestnut and not np.all(np.isfinite(model_output)):
raise RuntimeError("model output not finite")
outputs_dict = self.parser.parse_outputs(self.slice_outputs(model_output, self.output_slices))
if SEND_RAW_PRED:
outputs_dict['raw_pred'] = model_output.copy()
return outputs_dict
def warmup(self) -> None:
dummy_frames = {k: np.zeros(self.frame_copy_size, dtype=np.uint8) for k in self.vision_input_names}
eye = np.eye(3, dtype=np.float32)
dims = {'desire_pulse': ModelConstants.DESIRE_LEN, 'traffic_convention': 2, 'action_t': 2}
self.run(dummy_frames, dict.fromkeys(self.vision_input_names, eye), {k: np.zeros(v, dtype=np.float32) for k, v in dims.items()})
self.packed_input[:] = 0
for key in self.state_pairs:
self.input_queues[key].assign(0).realize()
self.prev_desire[:] = 0
def refresh_lane_change_policy(preferences, desire_helper, carstate, now_ns, vehicle_capable):
policy = session_policy(preferences.sample(now_ns,
signaling=bool(carstate.leftBlinker or carstate.rightBlinker),
maneuver_active=desire_helper.lane_change_state != log.LaneChangeState.off), vehicle_capable)
desire_helper.policy = policy
return policy
def release_compile_pool() -> None:
from tinygrad.engine.worker import terminate_worker_pool
from tinygrad.helpers import PARALLEL
terminate_worker_pool()
PARALLEL.value = 0
def main(demo=False):
cloudlog.warning("modeld init")
receipt_owner = ModelReceiptOwner(report=lambda reason: cloudlog.warning("modeld load identity unavailable: %s", reason))
receipt_owner.start()
from openpilot.starpilot.models.manager import resolve_runtime, requested_runtime_id
# a Jetson, PC or phone on the port looks like a chestnut to cable_connected(): the cable counts with Jetlink off only
chestnut_available = chestnut_present() or (cable_connected() and not jetlink_adapter.link_on())
recovery_small_only = os.getenv("STARPILOT_MODELD_RECOVERY_SMALL_ONLY") == "1"
selection = resolve_runtime(chestnut_available=chestnut_available and not recovery_small_only, randomize=not recovery_small_only)
requested_model_id = requested_runtime_id(chestnut_available)
CHESTNUT = selection.allow_big and selection.big_path is not None
if CHESTNUT:
from tinygrad.runtime.support.am import startup_trace
startup_trace.ENABLED = True
startup_trace.REPORT = lambda packet: cloudlog.event("chestnut.startup", **packet)
from tinygrad.runtime.ops_amd import AMDDevice
AMDDevice.wait_timeout_ms = 3000
params = Params()
params.put_bool("ChestnutLoading", CHESTNUT)
params.remove("ChestnutActive")
gc.disable()
# visionipc clients
while True:
available_streams = VisionIpcClient.available_streams("camerad", block=False)
if available_streams:
use_extra_client = VisionStreamType.VISION_STREAM_WIDE_ROAD in available_streams and VisionStreamType.VISION_STREAM_NARROW_ROAD in available_streams
main_wide_camera = VisionStreamType.VISION_STREAM_NARROW_ROAD not in available_streams
break
time.sleep(.1)
vipc_client_main_stream = VisionStreamType.VISION_STREAM_WIDE_ROAD if main_wide_camera else VisionStreamType.VISION_STREAM_NARROW_ROAD
vipc_client_main = VisionIpcClient("camerad", vipc_client_main_stream, True)
vipc_client_extra = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD, False)
cloudlog.warning(f"vision stream set up, main_wide_camera: {main_wide_camera}, use_extra_client: {use_extra_client}")
while not vipc_client_main.connect(False):
time.sleep(0.1)
while use_extra_client and not vipc_client_extra.connect(False):
time.sleep(0.1)
cloudlog.warning(f"connected main cam with buffer size: {vipc_client_main.buffer_len} ({vipc_client_main.width} x {vipc_client_main.height})")
if use_extra_client:
cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})")
if demo:
CP = get_demo_car_params()
else:
CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams)
cloudlog.info("modeld got CarParams: %s", CP.brand)
from openpilot.starpilot.car.subaru.smoothing import read_frozen, seconds as subaru_smooth_seconds, filter_action
subaru_smoothing = read_frozen(CP, params)
st = time.monotonic()
cloudlog.warning("loading model")
model = None
big_prepared = None
# with Jetlink on, the small model's warp output moves where the link sends it from, before warmup links it
coherent_warp = jetlink_adapter.coherent_warp()
def load_small():
if selection.small_path is not None:
try:
selected, prepared = load_verified_model(vipc_client_main.width, vipc_client_main.height,
selection.small_path, selection.small_version, False,
selection.small_id, selection.small_sha256, coherent_warp=coherent_warp)
return selected, prepared, False
except Exception:
cloudlog.exception("selected small model load failed, using shipped RDFv4")
from openpilot.starpilot.models.manager import shipped_default
path = shipped_default()
if path is None:
raise RuntimeError("Shipped RDFv4 is missing or corrupt")
fallback, prepared = load_verified_model(vipc_client_main.width, vipc_client_main.height,
path, BY_ID[DEFAULT_SMALL].version, False,
DEFAULT_SMALL, DEFAULT_SMALL_SHA256, coherent_warp=coherent_warp)
return fallback, prepared, selection.small_id != DEFAULT_SMALL
# Preload a custom fallback before the GPU worker takes the catalog lock.
# A timed-out GPU worker must never hold up access to the small runner.
small_model = small_prepared = None
selected_small_failed = False
if selection.big_path is not None and selection.small_path is not None:
small_model, small_prepared, selected_small_failed = load_small()
if CHESTNUT:
big_path = selection.big_path or modeld_pkl_path(True)
big_artifact_identity = receipt_owner.capture(big_path)
big_model = None
def load_big():
nonlocal big_model, big_prepared
try:
wait_for_chestnut()
if not demo:
wait_for_chestnut_power(CP, timeout=min(30., BIG_MODEL_TIMEOUT / 2))
set_core_affinity([7])
if selection.big_path is not None:
m, prepared = load_verified_model(vipc_client_main.width, vipc_client_main.height,
big_path, selection.big_version, True, selection.big_id, selection.big_sha256)
else:
m = ModelState(vipc_client_main.width, vipc_client_main.height, True)
m.warmup()
prepared = receipt_owner.prepare(big_path, big_artifact_identity)
big_prepared = prepared
big_model = m
except Exception:
cloudlog.exception("big model load failed")
loader = threading.Thread(target=load_big, daemon=True)
loader.start()
loader.join(BIG_MODEL_TIMEOUT)
if loader.is_alive():
report_load_timeout(loader, BIG_MODEL_TIMEOUT)
model = big_model
params.put_bool("ChestnutActive", model is not None)
if model is None:
# A timed-out loader can still mutate tinygrad's shared device state.
# The manager must confirm this process has exited before starting Small.
cloudlog.error("Chestnut startup failed; requesting isolated Small restart")
# Avoid Python/native finalizers retaining this failed GPU context.
os._exit(STARTUP_FALLBACK_EXIT)
if small_model is None:
small_model, small_prepared, selected_small_failed = load_small()
initial_chestnut_fallback = CHESTNUT and model is None
if model is None:
model = small_model
receipt_owner.loaded(big_prepared if model.chestnut else small_prepared,
ModelVariant.CHESTNUT if model.chestnut else ModelVariant.SMALL,
"chestnut-run-stalled" if recovery_small_only else
"chestnut-load-failed" if initial_chestnut_fallback else
"selected-load-failed" if (selected_small_failed and not model.chestnut) or requested_model_id != model.model_id else None,
model_id=model.model_id)
HOOKS = jetlink_adapter.hooks()
jetlink_model = None
# before going realtime: the GPU thread it starts would inherit SCHED_FIFO 54 on core 7
if HOOKS.prepare(CHESTNUT or recovery_small_only):
# the joining model drives as small_model until the big model is ready
model = HOOKS.attach(model, small_model, vipc_client_main.width, vipc_client_main.height)
jetlink_model = model if model is not small_model else None
if jetlink_model is not None:
from openpilot.starpilot.models.jetlink_status import start_latency_writer
start_latency_writer(jetlink_model)
params.put_bool("ChestnutLoading", False)
cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting")
release_compile_pool()
# gc stays off for the realtime loop, so drop the load's cyclic garbage once now
gc.collect()
config_realtime_process(7, 54)
# messaging
pub_socks = ["modelIdentity", "modelV2", "drivingModelData", "cameraOdometry", "laneChangeAssistWire"] + (["chestnutGpuState"] if CHESTNUT else [])
pm = PubMaster(pub_socks)
sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState",
"carControl", "controlsState", "lateralDelay", "starpilotNavigation", "longitudinalPlan"])
publish_state = PublishState()
params = Params()
chestnut_state = ChestnutGpuState(pm, model.chestnut) if CHESTNUT else None
# setup filter to track dropped frames
frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_RUN_FREQ)
frame_id = 0
last_vipc_frame_id = 0
run_count = 0
model_transform_main = np.zeros((3, 3), dtype=np.float32)
model_transform_extra = np.zeros((3, 3), dtype=np.float32)
extrinsics_calibration_seen = False
buf_main, buf_extra = None, None
meta_main = FrameMeta()
meta_extra = FrameMeta()
# TODO this needs more thought, use .2s extra for now to estimate other delays
# TODO Move smooth seconds to action function
long_delay = CP.longitudinalActuatorDelay + LONG_SMOOTH_SECONDS
prev_action = log.ModelDataV2.Action()
lane_preferences = LiveLaneChangePolicy(params, now_ns=time.monotonic_ns())
saved_lane_policy = lane_preferences.applied
auto_vehicle_capable = steering_capable(CP)
runtime_lane_policy = session_policy(saved_lane_policy, auto_vehicle_capable)
initial_mono_ns, initial_boot_ns, initial_skew_ns = paired_clocks_ns()
lane_clock_guard = ClockEpochGuard(initial_mono_ns, initial_boot_ns, initial_skew_ns)
DH = DesireHelper(runtime_lane_policy)
navigation_turn = TurnIntent(params, CP)
lane_status_session = uuid.uuid4().hex
lane_status_sequence = 0
while True:
# Keep receiving frames until we are at least 1 frame ahead of previous extra frame
while meta_main.timestamp_sof < meta_extra.timestamp_sof + 25000000:
buf_main = vipc_client_main.recv()
meta_main = FrameMeta(vipc_client_main)
if buf_main is None:
break
if buf_main is None:
cloudlog.debug("vipc_client_main no frame")
continue
if use_extra_client:
# Keep receiving extra frames until frame id matches main camera
while True:
buf_extra = vipc_client_extra.recv()
meta_extra = FrameMeta(vipc_client_extra)
if buf_extra is None or meta_main.timestamp_sof < meta_extra.timestamp_sof + 25000000:
break
if buf_extra is None:
cloudlog.debug("vipc_client_extra no frame")
continue
if abs(meta_main.timestamp_sof - meta_extra.timestamp_sof) > 10000000:
cloudlog.error(f"frames out of sync! main: {meta_main.frame_id} ({meta_main.timestamp_sof / 1e9:.5f}),\
extra: {meta_extra.frame_id} ({meta_extra.timestamp_sof / 1e9:.5f})")
else:
# Use single camera
buf_extra = buf_main
meta_extra = meta_main
sm.update(0)
turn_supported = bool(model.npy.get('desire') is not None and model.npy['desire'].shape[-1] == 8)
desire = navigation_turn.select(sm, time.monotonic_ns(), DH.desire,
supported=turn_supported, model_stop=bool(prev_action.shouldStop))
is_rhd = sm["driverMonitoringState"].isRHD
frame_id = sm["narrowRoadCameraState"].frameId
v_ego = max(sm["carState"].vEgo, 0.)
subaru_tau = subaru_smooth_seconds(v_ego, subaru_smoothing)
lat_delay = sm["lateralDelay"].lateralDelay + LAT_SMOOTH_SECONDS + subaru_tau
if sm.updated["extrinsicsCalibration"] and sm.seen['narrowRoadCameraState'] and sm.seen['deviceState']:
device_from_calib_euler = np.array(sm["extrinsicsCalibration"].rpyCalib, dtype=np.float32)
dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['narrowRoadCameraState'].sensor))]
main_intrinsics = dc.wide_road.intrinsics if main_wide_camera else dc.narrow_road.intrinsics
model_transform_main = get_warp_matrix(device_from_calib_euler, main_intrinsics, False).astype(np.float32)
has_wide_camera = use_extra_client or main_wide_camera
extra_intrinsics = dc.wide_road.intrinsics if has_wide_camera else dc.narrow_road.intrinsics
model_transform_extra = get_warp_matrix(device_from_calib_euler, extra_intrinsics, True).astype(np.float32)
extrinsics_calibration_seen = True
traffic_convention = np.zeros(2)
traffic_convention[int(is_rhd)] = 1
vec_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
if desire >= 0 and desire < ModelConstants.DESIRE_LEN:
vec_desire[desire] = 1
# tracked dropped frames
vipc_dropped_frames = max(0, meta_main.frame_id - last_vipc_frame_id - 1)
frames_dropped = frame_dropped_filter.update(min(vipc_dropped_frames, 10))
if run_count < 10: # let frame drops warm up
frame_dropped_filter.x = 0.
frames_dropped = 0.
run_count = run_count + 1
frame_drop_ratio = frames_dropped / (1 + frames_dropped)
bufs = {name: buf_extra if 'big' in name else buf_main for name in model.vision_input_names}
transforms = {name: model_transform_extra if 'big' in name else model_transform_main for name in model.vision_input_names}
frame_delay = DT_MDL # compensate for time passed since the frame was captured: current_time - timestamp_eof is 50ms on average
action_delay = DT_MDL / 2 # middle of the interval between model output (current state) and next frame (expected state)
lat_action_t = lat_delay + frame_delay + action_delay
long_action_t = long_delay + frame_delay + action_delay
inputs: dict[str, np.ndarray] = {
'desire_pulse': vec_desire,
'traffic_convention': traffic_convention,
'action_t': np.array([lat_action_t, long_action_t], dtype=np.float32),
}
if not sm['carControl'].latActive:
prev_action.desiredCurvature = sm['controlsState'].curvature
if isinstance(model, CatalogModelState) or model is jetlink_model:
inputs['prev_action'] = np.array([prev_action.desiredCurvature * max(1.0, v_ego) ** 2,
prev_action.desiredAcceleration], dtype=np.float32)
inputs['lateral_control_params'] = np.array([v_ego, lat_delay], dtype=np.float32)
model.lat_delay = lat_delay
model.frame_drop_ratio = frame_drop_ratio
mt1 = time.perf_counter()
try:
send_chestnut = (chestnut_state is not None and
run_count % round(ModelConstants.MODEL_RUN_FREQ / SERVICE_LIST['chestnutGpuState'].frequency) == 0)
model_output = model.run(bufs, transforms, inputs, chestnut_state.send if send_chestnut else None)
except Exception:
if not model.chestnut:
raise
# fallback to small model
fallback_reason = "chestnut-load-failed"
if model is jetlink_model:
model.close()
fallback_reason = None
else:
drop_chestnut()
params.put_bool("ChestnutActive", False)
cloudlog.exception("big model failed, fall back to small")
model = small_model
receipt_owner.loaded(small_prepared, ModelVariant.SMALL, fallback_reason, model_id=model.model_id)
if chestnut_state is not None:
chestnut_state.big = False
run_count = 0
model_output = None
mt2 = time.perf_counter()
model_execution_time = mt2 - mt1
if model is jetlink_model and HOOKS.handed_over(model):
frame_dropped_filter.x = 0.
frame_drop_ratio = 0.
if model.chestnut:
receipt_owner.loaded(None, ModelVariant.CHESTNUT)
else:
receipt_owner.loaded(small_prepared, ModelVariant.SMALL, model_id=small_model.model_id)
cloudlog.event("jetlinkHandover", state=HOOKS.state(model), handovers=model.handovers)
if model_output is not None and vipc_dropped_frames == 0:
modelv2_send = messaging.new_message('modelV2')
drivingdata_send = messaging.new_message('drivingModelData')
posenet_send = messaging.new_message('cameraOdometry')
action = model.get_action_from_model(model_output, prev_action, lat_action_t, long_action_t, v_ego)
filter_action(action, prev_action, v_ego, subaru_tau)
prev_action = action
fill_model_msg(modelv2_send, model_output, action,
publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id,
frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, extrinsics_calibration_seen)
modelv2_send.modelV2.big = model.chestnut
desire_state = modelv2_send.modelV2.meta.desireState
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
lane_change_prob = l_lane_change_prob + r_lane_change_prob
car_state = sm['carState']
direction = -1 if car_state.leftBlinker and not car_state.rightBlinker else (1 if car_state.rightBlinker and not car_state.leftBlinker else 0)
mono_now_ns, boot_now_ns, clock_skew_ns = paired_clocks_ns()
runtime_lane_policy = refresh_lane_change_policy(lane_preferences, DH, car_state, mono_now_ns, auto_vehicle_capable)
auto_ready = (runtime_lane_policy.auto_lane_change and direction != 0 and
lane_clock_guard.ready(sm, mono_now_ns, boot_now_ns, clock_skew_ns) and
auto_evidence(sm, modelv2_send.modelV2, direction, runtime_lane_policy.minimum_lane_width_m,
now_mono_ns=mono_now_ns, now_boot_ns=boot_now_ns,
model_valid=bool(modelv2_send.valid), vehicle_capable=auto_vehicle_capable))
engaged = bool(sm['carControl'].latActive)
DH.update(car_state, sm['carControl'].latActive, lane_change_prob,
auto_evidence=auto_ready, engaged=engaged,
navigation_turn=matching_turn_signal(sm, mono_now_ns, supported=turn_supported))
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
phase = {"manualRequired": StatusPhase.MANUAL_REQUIRED, "waitingForDelay": StatusPhase.WAITING_FOR_DELAY,
"laneUnavailable": StatusPhase.LANE_UNAVAILABLE, "blindspotBlocked": StatusPhase.BLINDSPOT_BLOCKED}[DH.auto_status]
status_direction = (StatusDirection.LEFT if DH.lane_change_direction == log.LaneChangeDirection.left else
StatusDirection.RIGHT if DH.lane_change_direction == log.LaneChangeDirection.right else StatusDirection.NONE)
lane_status = LaneChangeStatus(lane_status_session, lane_status_sequence,
int(modelv2_send.modelV2.frameId), int(modelv2_send.modelV2.timestampEof),
mono_now_ns, mono_now_ns + 150_000_000, phase, status_direction,
runtime_lane_policy.auto_lane_change, engaged)
lane_status_sequence += 1
lane_status_msg = None
status_bytes = encode_lane_status(lane_status)
if status_bytes is not None:
lane_status_msg = messaging.new_message('laneChangeAssistWire', 0)
lane_status_msg.valid = True
lane_status_msg.laneChangeAssistWire = status_bytes
fill_driving_model_data(drivingdata_send, modelv2_send)
fill_pose_msg(posenet_send, model_output, meta_main.frame_id, vipc_dropped_frames, meta_main.timestamp_eof, extrinsics_calibration_seen)
receipt_owner.publish(pm, time.monotonic_ns())
pm.send('modelV2', modelv2_send)
if lane_status_msg is not None:
pm.send('laneChangeAssistWire', lane_status_msg)
pm.send('drivingModelData', drivingdata_send)
pm.send('cameraOdometry', posenet_send)
last_vipc_frame_id = meta_main.frame_id
if __name__ == "__main__":
try:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--demo', action='store_true', help='A boolean for demo mode.')
args = parser.parse_args()
main(demo=args.demo)
except KeyboardInterrupt:
cloudlog.warning("got SIGINT")