Files
StarPilot/openpilot/starpilot/models/runner.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

331 lines
18 KiB
Python

"""Run catalog models using their declared history and output contracts."""
from collections.abc import Callable
from dataclasses import dataclass
import os
from pathlib import Path
from typing import Any
import weakref
import numpy as np
from tinygrad import Tensor, dtypes
from openpilot.cereal import log
from openpilot.selfdrive.controls.lib.drive_helpers import MIN_SPEED, smooth_value
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
from openpilot.selfdrive.modeld.helpers import load_oob
from openpilot.starpilot.models.parser import Parser
from openpilot.starpilot.models.catalog import ARTIFACT_ABI, COMPILER_REVISION
from openpilot.starpilot.models.receipt import PreparedArtifact, artifact_identity, prepare_artifact
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
SUPPORTED_VERSIONS = frozenset(f"v{version}" for version in range(8, 17))
REQUIRED_OUTPUTS = frozenset(("plan", "lane_lines", "lane_lines_prob", "road_edges", "lead", "lead_prob",
"meta", "desire_pred", "pose", "wide_from_device_euler", "road_transform"))
@dataclass(frozen=True)
class CameraWarpDescriptor:
camera_size: tuple[int, int]
nv12_layout: tuple[int, int, int, int]
image_layouts: tuple[tuple[int, ...], tuple[int, ...]]
output_shape: tuple[int, ...]
dtype: object
device: str
@dataclass(frozen=True)
class SharedCameraWarp:
tensor: Tensor
descriptor: CameraWarpDescriptor
frame_pointers: tuple[int, int]
transforms: tuple[bytes, bytes]
producer: weakref.ReferenceType
def load_verified_model(cam_w: int, cam_h: int, artifact_path: Path, behavior_version: str, chestnut: bool,
model_id: str, expected_sha256: str | None, *, root: Path | None = None,
coherent_warp: Callable[[Any], None] | None = None) -> tuple["CatalogModelState", PreparedArtifact]:
from openpilot.starpilot.models.manager import ROOT, state_lock
if expected_sha256 is None:
raise ValueError("selected catalog model has no verified digest")
with state_lock(ROOT if root is None else root):
identity = artifact_identity(artifact_path)
prepared = prepare_artifact(artifact_path, identity)
if prepared.sha256 != expected_sha256:
raise ValueError("selected catalog model changed before loading")
model = CatalogModelState(cam_w, cam_h, artifact_path, behavior_version, chestnut)
if coherent_warp is not None:
model.move_warp_output(coherent_warp)
model.warmup()
if artifact_identity(artifact_path) != identity:
raise ValueError("selected catalog model changed while loading")
model.model_id = model_id
return model, prepared
def validate_artifact(artifact: dict, version: str, camera_size: tuple[int, int]) -> None:
if (not isinstance(artifact, dict) or artifact.get("artifact_abi") != ARTIFACT_ABI or
artifact.get("compiler_revision") != COMPILER_REVISION or artifact.get("format_version") != 1):
raise ValueError("catalog model compiler/ABI mismatch")
if version not in SUPPORTED_VERSIONS or artifact.get("behavior_version") != version:
raise ValueError("catalog model behavior version mismatch")
if artifact.get("model_type") not in ("supercombo", "vision_policy", "vision_multi_policy"):
raise ValueError("unsupported catalog model layout")
if artifact.get("image_history_pipeline") not in ("policy", "warp"):
raise ValueError("unsupported catalog model history")
if type(artifact.get("frame_skip")) is not int or not 1 <= artifact["frame_skip"] <= 20:
raise ValueError("unsupported catalog model frame interval")
if camera_size not in artifact or not callable(artifact.get("run_policy")) or not callable(artifact[camera_size]):
raise ValueError("catalog model camera/runner unavailable")
metadata = artifact.get("metadata")
if not isinstance(metadata, dict):
raise ValueError("missing catalog model metadata")
keys = ["model"] if artifact["model_type"] == "supercombo" else ["vision", *artifact.get("policy_order", [])]
if not keys or (keys[0] == "vision" and not any(key in keys for key in ("on_policy", "policy"))):
raise ValueError("catalog model has no primary policy")
names = set()
for key in keys:
entry = metadata.get(key)
if not isinstance(entry, dict) or not isinstance(entry.get("input_shapes"), dict) or not isinstance(entry.get("output_slices"), dict):
raise ValueError("invalid catalog model metadata")
shape = entry.get("output_shapes", {}).get("outputs")
if (not isinstance(shape, (tuple, list)) or len(shape) != 2 or shape[0] != 1 or
type(shape[1]) is not int or shape[1] <= 0):
raise ValueError("catalog model output width unavailable")
width = shape[1]
for name, section in entry["output_slices"].items():
if (not isinstance(section, slice) or section.step not in (None, 1) or
any(bound is not None and (type(bound) is not int or not -width <= bound <= width)
for bound in (section.start, section.stop))):
raise ValueError("catalog model output slice exceeds output")
# Metadata padding aliases use ordinary Python negative/open endpoints.
# Check explicit bounds before indices() so its clipping cannot hide a bad slice.
start, stop, _ = section.indices(width)
if not 0 <= start < stop <= width:
raise ValueError("catalog model output slice is empty or reversed")
names.add(name)
if not REQUIRED_OUTPUTS <= names or (version in ("v14", "v15", "v16") and "action" not in names):
raise ValueError("catalog model is missing required driving outputs")
def action_from_outputs(outputs: dict[str, np.ndarray], version: str, previous: log.ModelDataV2.Action,
lat_action_t: float, long_action_t: float, v_ego: float,
lat_smooth_seconds: float = 0.0, long_smooth_seconds: float = 0.3) -> log.ModelDataV2.Action:
"""Preserve the catalog's generation-specific action units and v9 curvature."""
if version not in SUPPORTED_VERSIONS or min(lat_action_t, long_action_t) <= 0:
raise ValueError("invalid catalog model action contract")
if version in ("v14", "v15", "v16"):
curvature, acceleration = outputs["action"][0]
curvature = curvature / (100.0 if version == "v14" else max(1.0, v_ego) ** 2)
stop = v_ego < 0.3 and acceleration < 0.1
else:
plan = outputs["plan"][0]
if "planplus" in outputs:
plan = plan + outputs["planplus"][0]
speeds, accelerations = plan[:, Plan.VELOCITY][:, 0], plan[:, Plan.ACCELERATION][:, 0]
acceleration = 2 * (np.interp(long_action_t, ModelConstants.T_IDXS, speeds) - speeds[0]) / long_action_t - accelerations[0]
stop = speeds[0] < 0.3 and acceleration < 0.1
if version == "v9":
curvature = float(outputs["desired_curvature"][0, 0]) if "desired_curvature" in outputs else previous.desiredCurvature
else:
yaw = np.interp(lat_action_t, ModelConstants.T_IDXS, plan[:, Plan.T_FROM_CURRENT_EULER][:, 2])
yaw_rate = plan[0, Plan.ORIENTATION_RATE][2]
curvature = 2 * yaw / (max(MIN_SPEED, v_ego) * lat_action_t) - yaw_rate / max(MIN_SPEED, v_ego)
acceleration = smooth_value(float(acceleration), previous.desiredAcceleration, long_smooth_seconds)
curvature = (smooth_value(float(curvature), previous.desiredCurvature, lat_smooth_seconds)
if v_ego > 0.3 else previous.desiredCurvature)
if not np.isfinite((acceleration, curvature)).all():
raise ValueError("non-finite catalog model action")
return log.ModelDataV2.Action(desiredCurvature=float(curvature), desiredAcceleration=float(acceleration), shouldStop=bool(stop))
class CatalogModelState:
"""Current modeld interface around a current-pin unified catalog artifact."""
lat_delay: float
frame_drop_ratio: float
vision_input_names = ("img", "big_img")
def __init__(self, cam_w: int, cam_h: int, artifact_path: Path, behavior_version: str, chestnut: bool):
from openpilot.starpilot.models.compile import _detect_vision_keys, stateful_image_shapes, stateful_host_shapes
artifact = load_oob(artifact_path, chestnut)
validate_artifact(artifact, behavior_version, (cam_w, cam_h))
self.chestnut = chestnut
self.behavior_version = behavior_version
self.model_type = artifact["model_type"]
self.metadata = artifact["metadata"]
self.policy_order = artifact.get("policy_order", [])
self.frame_skip = artifact["frame_skip"]
self.image_history_pipeline = artifact["image_history_pipeline"]
self.warp_input_keys = tuple(artifact["warp_input_keys"])
self.policy_input_keys = tuple(artifact["policy_input_keys"])
self.run_policy = artifact["run_policy"]
self.warp_enqueue = artifact[(cam_w, cam_h)]
self.onnx_history = self.model_type == "supercombo" and bool(self.metadata["model"].get("state_pairs"))
self.queue_device = artifact["execution_device"]
self.warp_device = artifact["warp_device"]
if (not isinstance(self.queue_device, str) or not isinstance(self.warp_device, str) or
chestnut != (self.queue_device.rsplit("+", 1)[-1].split(":", 1)[0] == "AMD")):
raise ValueError("catalog model execution hardware mismatch")
if self.onnx_history:
metadata = self.metadata["model"]
image_shapes = stateful_image_shapes(metadata)
self.policy_input_shapes = stateful_host_shapes(metadata)
self.output_slices = metadata["output_slices"]
elif self.model_type == "supercombo":
image_shapes = self.policy_input_shapes = self.metadata["model"]["input_shapes"]
self.output_slices = self.metadata["model"]["output_slices"]
else:
image_shapes = self.metadata["vision"]["input_shapes"]
primary = "on_policy" if "on_policy" in self.policy_order else "policy"
self.policy_input_shapes = self.metadata[primary]["input_shapes"]
self.road_key, self.wide_key = _detect_vision_keys(image_shapes)
self.desire_key = next(key for key in self.policy_input_shapes if key.startswith("desire"))
self.parser = Parser()
self.aux_parser = Parser(ignore_missing=True)
self.frame_buf_size = get_nv12_info(cam_w, cam_h)[3]
image_layouts = (tuple(image_shapes[self.road_key][-2:]), tuple(image_shapes[self.wide_key][-2:]))
self.camera_warp_descriptor = (CameraWarpDescriptor((cam_w, cam_h), get_nv12_info(cam_w, cam_h), image_layouts,
(2, 6, *image_layouts[0]), dtypes.uint8, self.warp_device)
if self.image_history_pipeline == "policy" else None)
# why Jetlink's link copies this warp's output out every frame (jetlink_warp); None once it is moved
self.warp_copied: str | None = "Jetlink was off when the model loaded"
self._reset_state()
def move_warp_output(self, coherent_warp: Callable[[Any], None]) -> None:
"""On a comma, move this warp's output to memory the CPU reads cached with `coherent_warp` (Jetlink's
make_coherent), for the link to send from. Before warmup(), whose first call links the warp to its address."""
buffer = self.warp_enqueue.captured.ret.uop.base.buffer
if not buffer.device.startswith("QCOM"):
self.warp_copied = f"a {buffer.device} buffer"
return
try:
coherent_warp(buffer)
except RuntimeError as e: # the buffer stays where it was
self.warp_copied = str(e)
return
self.warp_copied = None
def _reset_state(self) -> None:
from openpilot.starpilot.models.compile import make_stateful_input_queues, make_supercombo_input_queues, make_split_input_queues
if self.onnx_history:
self.input_queues, self.npy = make_stateful_input_queues(self.metadata["model"], self.queue_device)
elif self.model_type == "supercombo":
self.input_queues, self.npy = make_supercombo_input_queues(self.policy_input_shapes, self.frame_skip, self.queue_device)
else:
self.input_queues, self.npy = make_split_input_queues(self.metadata["vision"]["input_shapes"], self.policy_input_shapes,
self.frame_skip, self.queue_device)
self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
self._blob_cache: dict[tuple[str, int], Tensor] = {}
self.last_warp: SharedCameraWarp | None = None
self._last_shared_warp: SharedCameraWarp | None = None
def _validate_shared_warp(self, shared: SharedCameraWarp, pointers: tuple[int, int], transforms: tuple[bytes, bytes]) -> None:
if (not isinstance(shared, SharedCameraWarp) or self.camera_warp_descriptor is None or
shared.descriptor != self.camera_warp_descriptor):
raise ValueError("catalog models have incompatible camera warp descriptors/history")
tensor = shared.tensor
if (tuple(tensor.shape) != shared.descriptor.output_shape or tensor.dtype != shared.descriptor.dtype or
tensor.device != shared.descriptor.device):
raise ValueError("catalog shared camera warp tensor contract mismatch")
producer = shared.producer()
if producer is None or producer.last_warp is not shared or shared is self._last_shared_warp:
raise ValueError("catalog shared camera warp is stale or already consumed")
if shared.frame_pointers != pointers or shared.transforms != transforms:
raise ValueError("catalog shared camera warp frame/transforms mismatch")
@staticmethod
def slice_outputs(output: np.ndarray, sections: dict[str, slice]) -> dict[str, np.ndarray]:
return {key: output[np.newaxis, section].copy() for key, section in sections.items()}
def _parse(self, outputs: list[np.ndarray]) -> dict[str, np.ndarray]:
if self.model_type == "supercombo":
parsed = self.parser.parse_outputs(self.slice_outputs(outputs[0], self.output_slices))
if "prev_feat" in self.npy and "hidden_state" in self.output_slices:
self.npy["prev_feat"][:] = outputs[0][self.output_slices["hidden_state"]]
return parsed
vision_output, *policies = outputs
parsed = self.parser.parse_vision_outputs(self.slice_outputs(vision_output, self.metadata["vision"]["output_slices"]))
results = {}
for key, output in zip(self.policy_order, policies, strict=True):
slices = self.slice_outputs(output, self.metadata[key]["output_slices"])
results[key] = self.aux_parser.parse_off_policy_outputs(slices) if key == "off_policy" else self.parser.parse_policy_outputs(slices)
for key in self.policy_order:
if key not in ("on_policy", "policy"):
parsed.update(results[key])
parsed.update(results["on_policy" if "on_policy" in results else "policy"])
return parsed
def get_action_from_model(self, outputs: dict[str, np.ndarray], previous: log.ModelDataV2.Action,
lat_action_t: float, long_action_t: float, v_ego: float) -> log.ModelDataV2.Action:
"""modeld's action call: this catalog generation's units, with modeld's smoothing."""
from openpilot.selfdrive.modeld.modeld import LAT_SMOOTH_SECONDS, LONG_SMOOTH_SECONDS
return action_from_outputs(outputs, self.behavior_version, previous, lat_action_t, long_action_t, v_ego,
LAT_SMOOTH_SECONDS, LONG_SMOOTH_SECONDS)
def warmup(self) -> None:
frames = {name: np.zeros(self.frame_buf_size, dtype=np.uint8) for name in self.vision_input_names}
self._blob_cache.update({(name, frame.ctypes.data): Tensor(frame, device=self.warp_device).realize()
for name, frame in frames.items()})
inputs = {"desire_pulse": np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)}
self.run(frames, dict.fromkeys(self.vision_input_names, np.eye(3, dtype=np.float32)), inputs)
self._reset_state()
def warp_frames(self, pointers: tuple[int, int], tfm: np.ndarray, big_tfm: np.ndarray) -> Tensor:
"""Enqueue the camera warp of the frames at these addresses, on the inputs its JIT was captured with.
run() calls it, and so does Jetlink's link (jetlink_warp) on the frames the large model drives."""
self.npy["tfm"][:] = tfm
self.npy["big_tfm"][:] = big_tfm
frames = {}
for name, pointer in zip(self.vision_input_names, pointers, strict=True):
key = (name, pointer)
if key not in self._blob_cache:
self._blob_cache[key] = Tensor.from_blob(pointer, (self.frame_buf_size,), dtype="uint8", device=self.warp_device)
frames[name] = self._blob_cache[key]
return self.warp_enqueue(**{key: self.input_queues[key] for key in self.warp_input_keys},
frame=frames["img"], big_frame=frames["big_img"])
def run(self, bufs: dict, transforms: dict[str, np.ndarray], inputs: dict[str, np.ndarray],
after_enqueue: Callable[[], None] | None = None, *, shared_warp: SharedCameraWarp | None = None) -> dict[str, np.ndarray]:
self.last_warp = None
road_pointer, wide_pointer = (np.frombuffer(bufs[name].data, dtype=np.uint8).ctypes.data for name in self.vision_input_names)
pointers = (road_pointer, wide_pointer)
road_transform, wide_transform = (np.asarray(transforms[name], dtype=np.float32).tobytes() for name in self.vision_input_names)
transform_signature = (road_transform, wide_transform)
if shared_warp is not None:
self._validate_shared_warp(shared_warp, pointers, transform_signature)
self._last_shared_warp = shared_warp
desire = inputs["desire_pulse"].copy()
desire[0] = 0
self.npy["desire"][:] = np.where(desire - self.prev_desire > 0.99, desire, 0)
self.prev_desire[:] = desire
for name, value in self.npy.items():
if name not in ("desire", "tfm", "big_tfm", "prev_feat") and name in inputs:
value[:] = inputs[name]
if shared_warp is None:
warped = self.warp_frames(pointers, transforms["img"], transforms["big_img"])
else:
warped = shared_warp.tensor
policy_inputs = {key: self.input_queues[key] for key in self.policy_input_keys}
output_tensors = (self.run_policy(**policy_inputs, warped=warped) if self.image_history_pipeline == "policy" else
self.run_policy(**policy_inputs, img=warped[0], big_img=warped[1]))
if after_enqueue is not None:
after_enqueue()
outputs = [output.numpy().flatten() for output in output_tensors]
if not all(np.isfinite(output).all() for output in outputs):
raise ValueError("catalog model output not finite")
parsed = self._parse(outputs)
if not REQUIRED_OUTPUTS <= parsed.keys() or not all(np.isfinite(value).all() for value in parsed.values()):
raise ValueError("catalog model parsed outputs invalid")
if os.getenv("SEND_RAW_PRED"):
parsed["raw_pred"] = np.concatenate(outputs)
if self.camera_warp_descriptor is not None:
self.last_warp = (shared_warp if shared_warp is not None else
SharedCameraWarp(warped, self.camera_warp_descriptor, pointers, transform_signature, weakref.ref(self)))
return parsed