Files
StarPilot/openpilot/starpilot/models/jetlink_adapter.py
T
Zeph Leggett 8b4902209c Jetlink: move the warp output when the small model loads
The output used to move after the small model's warmup had linked the
warp to its address, which needed the warp to link again by dropping a
private tinygrad cached property. Had that ever stopped working, the
small model's own warp would have kept writing the freed memory.

modeld now asks the adapter's coherent_warp() (Jetlink's make_coherent
with Jetlink on, off legacy9d) and load_verified_model moves the small
model's warp output before warmup(), as Jetlink's own warp moves its
output before its first call. CatalogWarp only reads the answer
(warp_copied): sent from where it is, or copied out, chosen once.
2026-10-26 19:32:00 -05:00

222 lines
8.3 KiB
Python

"""StarPilot's adapter to jetlink's openpilot layer, and the guard every Jetlink hook line calls.
The top level imports the standard library only: manager builds its process list from this module and the
gadget owner (jetlinkd) runs it.
"""
from __future__ import annotations
import functools
import os
import time
API = 3 # the jetlink.openpilot API this adapter is written to
UNSUPPORTED = "Jetlink requires a comma 3X or comma 4"
# AOL steers on its own axes, and a paused AOL is still armed: only its intent says so
AOL_SERVICES = ("aolIntentWire", "aolAxisState")
AOL_AXES = ("desiredLateral", "desiredLongitudinal", "lateralActive", "longitudinalActive")
# how long after AOL's reader is built an intent not yet heard counts as armed: its first polls run before the
# subscription connects, though card publishes the intent at 100 Hz
AOL_GRACE_S = 1.
# CatalogModelState's per-frame camera input, not history
FRAME_INPUTS = ("mapped_gpu_image",)
def supported_device(device_type=None) -> bool:
if device_type is None:
from openpilot.common.hardware import HARDWARE
device_type = HARDWARE.get_device_type()
return device_type in ("tizi", "mici")
def aol_in_control(intent, alive, axes, waiting=False) -> bool:
"""AOL armed (paused or not) or on its axes: the half of the swap's in-control rule that is not openpilot's,
shared by engaged() and selfdrived. `intent` is aolIntentWire's message, None until heard; `alive` that it still
is; `axes` the AOL_AXES flags, empty when unknown; `waiting` that its reader is younger than AOL_GRACE_S, when an
intent not heard yet may be armed."""
if intent is None:
if waiting:
return True
elif not alive:
return True
else:
from openpilot.starpilot.aol.wire import decode_intent
decoded = decode_intent(intent)
if decoded is None or decoded.allowedLatch or decoded.lateralArmed:
return True
return any(axes)
@functools.cache
def _adapter_class():
from jetlink.openpilot.interface import Keys
from jetlink.openpilot.stock import Stock
class StarPilotAdapter(Stock):
# No big-model slot or catalog: jetlink provisions its default model
KEYS = Keys(link="JetlinkMode", offroad="IsOffroad", progress="JetlinkProgress", spec="JetlinkSpec",
pointers="JetlinkPointers", charge_phone="JetlinkChargePhone")
IN_CONTROL_SERVICES = ("carControl", "selfdriveState")
_aol = None
_aol_since = 0.
def model_root(self):
from openpilot.starpilot.models.manager import ROOT
return ROOT
def engaged(self, sm) -> bool:
# openpilot, or aol_in_control(); Stock.in_control() answers True if this raises. AOL's reader is built and
# polled on every call, openpilot engaged or not, so it has heard AOL by the time an offer or a fall asks,
# rather than answering from its grace
if self._aol is None:
from openpilot.cereal import messaging
self._aol, self._aol_since = messaging.SubMaster(list(AOL_SERVICES)), time.monotonic()
aol = self._aol
aol.update(0)
if super().engaged(sm):
return True
heard = aol.seen["aolIntentWire"]
axes = aol["aolAxisState"] if aol.seen["aolAxisState"] and aol.alive["aolAxisState"] else None
return aol_in_control(aol["aolIntentWire"] if heard else None, aol.alive["aolIntentWire"],
() if axes is None else [getattr(axes, axis) for axis in AOL_AXES],
waiting=time.monotonic() - self._aol_since < AOL_GRACE_S)
def model_warp(self, small, cam_w, cam_h, model_w, model_h):
# None under legacy9d, whose tinygrad is the one Jetlink's stock pickle and its own warp are built for; any
# other runtime cannot load that pickle, so it sends the small model's own warp (jetlink_warp). A failure
# raises, and Jetlink logs it and keeps the small model
from openpilot.starpilot.models.runner import CatalogModelState
from openpilot.starpilot.models.runtime_bridge.runtime import selected_legacy
if selected_legacy():
return None
if not isinstance(small, CatalogModelState):
raise RuntimeError(f"the small model is a {type(small).__name__}, not a catalog model")
from openpilot.starpilot.models.jetlink_warp import CatalogWarp
warp = CatalogWarp(small, (model_w, model_h))
self.log.warning("jetlink: the link sends the small model's warp (%s, %d bytes a frame, %s)",
small.camera_warp_descriptor.output_shape, warp.output.nbytes, warp.sending)
return warp
def small_reset(self, small):
# CatalogModelState's history zeroed in place: GPU queues, numpy inputs (prev_feat, packed inputs) and
# the desire edge. Never _reset_state(), which makes tensors the run JIT was not captured with
from openpilot.starpilot.models.runner import CatalogModelState
if not isinstance(small, CatalogModelState):
return None
from tinygrad import Device
import numpy as np
queues = [q for name, q in small.input_queues.items() if q.device != "NPY" and name not in FRAME_INPUTS]
buffers = [q.uop.base.buffer for q in queues]
views = []
for buffer in buffers:
if not buffer.is_allocated() or not hasattr(buffer.get_storage().host, "mv"):
raise ValueError("Small model history has no allocated host mapping")
view = buffer.as_memoryview(allow_zero_copy=True)
if view.readonly or view.nbytes != buffer.nbytes:
raise ValueError("Small model history mapping is not writable or complete")
views.append(np.frombuffer(view, dtype=np.uint8))
devices = tuple(Device[name] for name in dict.fromkeys(q.device for q in queues))
arrays = tuple(small.npy.values())
def reset():
# The local history is idle at a handover. Keep the existing buffers
# and synchronize their device before zeroing their mapped storage.
for device in devices:
device.synchronize()
for view in views:
view.fill(0)
small.prev_desire.fill(0)
for array in arrays:
array.fill(0)
return reset
return StarPilotAdapter
def adapter():
return _adapter_class()()
def owner_config():
return _adapter_class().owner_config()
def main():
_adapter_class().main()
def link_on() -> bool:
"""The Jetlink setting, read as a file, so it works with no jetlink at all."""
prefix = os.environ.get("OPENPILOT_PREFIX", "")
root = os.environ.get("PARAMS_ROOT") or ("/data/params" if os.path.isfile("/AGNOS") else
os.path.expanduser(f"~/.comma{prefix}/params"))
try:
with open(os.path.join(root, os.environ.get("OPENPILOT_PREFIX", "d"), "JetlinkMode")) as f:
return f.read().strip() in ("1", "2")
except OSError:
return False
def coherent_warp():
"""What modeld's small model moves its warp's output with before warmup links the warp: Jetlink's
make_coherent, with Jetlink on and off legacy9d, where the link sends that output (jetlink_warp). Else None."""
if not link_on():
return None
from openpilot.starpilot.models.runtime_bridge.runtime import selected_legacy
if selected_legacy():
return None
try:
from jetlink.openpilot.warp import make_coherent
except Exception:
return None
return make_coherent
def _cloudlog():
from openpilot.common.swaglog import cloudlog
return cloudlog
@functools.cache
def hooks():
"""What every hook line calls, one per process. Never raises."""
try:
from jetlink.openpilot.hooks import Hooks, Off
except Exception as e:
return _NoJetlink(f"jetlink is missing: {e}")
try:
if not supported_device():
return Off(UNSUPPORTED, link_on=link_on)
return Hooks(API, adapter, link_on=link_on, log=_cloudlog)
except Exception as e:
return Off(str(e), link_on=link_on)
class _NoJetlink:
"""The hooks with no usable jetlink checkout, each answering link off. Only these are reached: modeld stops at
prepare()."""
def __init__(self, why):
self._why = why
def should_run(self, started, params, CP):
return False
def status(self):
return None
def reason(self):
return self._why if link_on() else None
def prepare(self, chestnut):
return False
def shutdown_step(self, should_shutdown, reason=""):
return should_shutdown
def runtime(self):
return None
def swap_events(self, tick_s):
return None