Files
StarPilot/openpilot/starpilot/models/jetlink_warp.py
T

71 lines
3.9 KiB
Python

"""Jetlink's warp (jetlink.openpilot.warp.Warp's interface) over the small model's own camera warp.
The stock driving warp Jetlink ships with is a legacy9d pickle, which a native02e modeld cannot load. A catalog
model on the policy image pipeline already runs the same warp every frame, compiled for whichever runtime it was
selected under, and makes the frame the link sends: both cameras as (2, 6, model_h/2, model_w/2) uint8, the same
YUV layout as examples/openpilot/compile_warp.py's yuv420 warp. Reusing it keeps the link on the user's small
model's runtime and loads no second warp.
Only one of the small and the large model runs a frame (JoiningModelState), so the warp JIT, its inputs and its
output buffer are never in use by both at once. The small model rewrites its transforms every frame it runs.
tinygrad maps a QCOM output write-combined, which the CPU reads uncached: 2.9 ms to copy the frame out. As
Jetlink's own warp does (jetlink.openpilot.warp.Warp), the small model moves its warp's output to IO-coherent
write-back memory when it loads (CatalogModelState, with Jetlink on), and the link sends straight from it.
"""
from __future__ import annotations
import numpy as np
PROBE = 77
class CatalogWarp:
def __init__(self, small, geometry: tuple[int, int]):
descriptor = small.camera_warp_descriptor
if descriptor is None:
raise RuntimeError("the small model's image history is not on the policy pipeline: it has no shared warp")
model_w, model_h = geometry
shape = (2, 6, model_h // 2, model_w // 2)
if tuple(descriptor.output_shape) != shape or descriptor.dtype.itemsize != 1:
raise RuntimeError(f"the small model's warp makes {descriptor.output_shape} {descriptor.dtype}, the link sends {shape} uint8")
self._small = small
# before modeld goes realtime: a blank frame finds the output buffer every frame writes (the small model's
# warmup captured the JIT, which returns the same one every call), then a flat frame proves `output` reads it
# live. as_memoryview() hands back a one-off copy where it finds no mapping, which would send the blank frame
# forever; a flat frame under the identity samples to itself
frames = [np.zeros(small.frame_buf_size, dtype=np.uint8) for _ in small.vision_input_names]
pointers = tuple(frame.ctypes.data for frame in frames)
eye = np.eye(3, dtype=np.float32)
buffer = small.warp_frames(pointers, eye, eye).uop.base.buffer
mapped = np.frombuffer(buffer.as_memoryview(allow_zero_copy=True), dtype=np.uint8)
expected = int(np.prod(shape))
if buffer.nbytes != expected or mapped.nbytes < expected:
raise RuntimeError(f"the warp's output is {buffer.nbytes} bytes ({mapped.nbytes} mapped), not {expected}")
mapped, synchronize = mapped[:expected], buffer.allocator.dev.synchronize
if small.warp_copied is None:
self.sending = "from coherent memory"
self.output, self.wait = memoryview(mapped), synchronize
else:
self.sending = f"copied out: {small.warp_copied}"
host = np.empty(expected, dtype=np.uint8)
self.output = memoryview(host)
def wait():
synchronize()
np.copyto(host, mapped)
self.wait = wait
for frame in frames:
frame.fill(PROBE)
self.start(pointers[0], pointers[1], eye, eye)
self.wait()
# the frames go with this call; their cache entries must not outlive them
for key in zip(small.vision_input_names, pointers, strict=True):
small._blob_cache.pop(key, None)
if not (np.frombuffer(self.output, dtype=np.uint8) == PROBE).all():
raise RuntimeError("the small model's warp output is not read live: the link would send a stale frame")
def start(self, frame: int, big_frame: int, tfm, big_tfm) -> None:
"""Warp the camera buffers at these addresses under their transforms; `output` holds it once wait() returns."""
self._small.warp_frames((frame, big_frame), tfm, big_tfm)