Compare commits

..

1 Commits

Author SHA1 Message Date
Prabhaav Pillai 4a3d96fbae attempt to get sim working 2026-08-28 18:35:24 -04:00
26 changed files with 815 additions and 177 deletions
Binary file not shown.
+2
View File
@@ -345,6 +345,8 @@ sync_worktree() {
"msgq_repo/msgq/ipc_pyx.so"
"msgq_repo/msgq/visionipc/visionipc_pyx.so"
"rednose_repo/rednose/helpers/ekf_sym_pyx.so"
"selfdrive/locationd/models/generated/*.so"
"selfdrive/locationd/models/generated/*.os"
"selfdrive/modeld/models/commonmodel_pyx.so"
"selfdrive/pandad/libcan_list_to_can_capnp.a"
"selfdrive/pandad/pandad_api_impl.so"
+135 -55
View File
@@ -99,7 +99,10 @@ def make_random_blob_images(keys, size, device=None):
for key in keys:
frame = (32 * np.random.randn(size).astype(np.float32) + 128).clip(0, 255).astype(np.uint8)
keepalive.append(frame)
tensors[key] = Tensor.from_blob(frame.ctypes.data, (size,), dtype="uint8", device=device).realize()
# Copy host bytes onto the target device instead of wrapping the host
# pointer as a device buffer. Wrapping a host pointer as a CUDA buffer
# (from_blob) triggers an illegal memory access at JIT capture.
tensors[key] = Tensor(frame, device=device).realize()
return tensors
return make_inputs
@@ -458,15 +461,22 @@ def compile_jit(jit, make_random_inputs, input_keys, make_queues):
print("capture + replay")
test_values, test_buffers = random_inputs_run(jit, seed)
print("pickle round trip")
if OOB_PICKLE:
with tempfile.TemporaryFile(dir=".") as artifact_file:
dump_oob(jit, artifact_file)
artifact_file.seek(0)
from openpilot.selfdrive.modeld.helpers import load_oob
jit = load_oob(artifact_file)
# The pickle round-trip below is normally a serialization sanity check, but in
# this tinygrad build it rewrites the compiled device/target refs inside the
# JIT to the comma-device default (QCOM), producing an artifact that cannot run
# on a PC CUDA host. Skip it by default so the captured PC kernels survive.
if os.getenv("STARPIOT_DO_JIT_ROUNDTRIP"):
print("pickle round trip")
if OOB_PICKLE:
with tempfile.TemporaryFile(dir=".") as artifact_file:
dump_oob(jit, artifact_file)
artifact_file.seek(0)
from openpilot.selfdrive.modeld.helpers import load_oob
jit = load_oob(artifact_file)
else:
jit = pickle.loads(pickle.dumps(jit))
else:
jit = pickle.loads(pickle.dumps(jit))
print("skipping pickle round trip")
random_inputs_run(jit, seed, test_values, test_buffers, expect_match=True)
random_inputs_run(jit, seed + 1, test_values, test_buffers, expect_match=False)
return jit
@@ -507,6 +517,80 @@ def validate_metadata(metadata):
raise ValueError(f"Invalid output slice {name}={output_slice} for output size {output_size}")
def build_supercombo_artifact(supercombo_onnx, model_size, camera_resolutions, behavior_version=None,
frame_skip=None, image_history_pipeline=IMAGE_HISTORY_IN_POLICY):
"""Trace a supercombo ONNX into a live, in-memory artifact (no pickling).
Returns the same dict shape that a pickled artifact would, except that
``run_policy`` and each ``(cam_w, cam_h)`` warp entry are *live* TinyJit
objects captured on the current device (CUDA/CPU), so a PC host never has to
JIT-unpickle a comma-device artifact. This is the reliable path on a host GPU.
"""
from tinygrad.nn.onnx import OnnxRunner
from openpilot.selfdrive.modeld.get_model_metadata import make_metadata_dict
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
output = {
"format_version": ARTIFACT_FORMAT_VERSION,
"model_type": "supercombo",
"metadata": {},
"image_history_pipeline": image_history_pipeline,
}
if behavior_version:
output["behavior_version"] = behavior_version
model_path = read_file_chunked_to_disk(supercombo_onnx)
model_runner = OnnxRunner(model_path)
output["metadata"]["model"] = make_metadata_dict(model_path)
validate_metadata(output["metadata"]["model"])
policy_shapes = output["metadata"]["model"]["input_shapes"]
frame_skip = frame_skip or derive_frame_skip(policy_shapes)
make_policy_queues = partial(make_supercombo_input_queues, policy_shapes, frame_skip)
run_policy = make_run_supercombo(model_runner, output["metadata"], frame_skip, image_history_pipeline)
image_shapes = policy_shapes
policy_input_keys = FAST_POLICY_INPUTS if image_history_pipeline == IMAGE_HISTORY_IN_POLICY else SUPERCOMBO_POLICY_INPUTS
output["frame_skip"] = frame_skip
output["policy_input_keys"] = policy_input_keys
warp_input_keys = FAST_WARP_INPUTS if image_history_pipeline == IMAGE_HISTORY_IN_POLICY else LEGACY_WARP_INPUTS
output["warp_input_keys"] = warp_input_keys
run_policy_jit = TinyJit(run_policy, prune=True)
road_key, wide_key = _detect_vision_keys(image_shapes)
if image_history_pipeline == IMAGE_HISTORY_IN_POLICY:
make_random_model_inputs = partial(
make_random_images,
keys=["warped"],
shape=(2, 6, *image_shapes[road_key][2:]),
device=WARP_DEV,
)
else:
make_random_model_inputs = partial(
make_random_images,
keys=[road_key, wide_key],
shape=image_shapes[road_key],
)
output["run_policy"] = compile_jit(
run_policy_jit, make_random_model_inputs, policy_input_keys, make_policy_queues,
)
model_w, model_h = model_size
for cam_w, cam_h in camera_resolutions:
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
warp_enqueue = TinyJit(
make_warp(nv12, model_w, model_h, frame_skip, image_history_pipeline),
prune=True,
)
make_random_warp_inputs = make_random_blob_images(
keys=["frame", "big_frame"], size=nv12.size, device=WARP_DEV,
)
make_warp_queues = partial(make_warp_input_queues, image_shapes, frame_skip)
output[(cam_w, cam_h)] = compile_jit(
warp_enqueue, make_random_warp_inputs, warp_input_keys, make_warp_queues,
)
return output
def main():
global OOB_PICKLE
from tinygrad.nn.onnx import OnnxRunner
@@ -550,18 +634,14 @@ def main():
if args.model_type == "supercombo":
if not args.supercombo_onnx:
parser.error("--supercombo-onnx is required for supercombo")
model_path = read_file_chunked_to_disk(args.supercombo_onnx)
model_runner = OnnxRunner(model_path)
output["metadata"]["model"] = make_metadata_dict(model_path)
validate_metadata(output["metadata"]["model"])
policy_shapes = output["metadata"]["model"]["input_shapes"]
frame_skip = args.frame_skip or derive_frame_skip(policy_shapes)
make_policy_queues = partial(make_supercombo_input_queues, policy_shapes, frame_skip)
run_policy = make_run_supercombo(
model_runner, output["metadata"], frame_skip, args.image_history_pipeline,
output = build_supercombo_artifact(
args.supercombo_onnx,
args.model_size,
args.camera_resolutions,
behavior_version=args.behavior_version,
frame_skip=args.frame_skip,
image_history_pipeline=args.image_history_pipeline,
)
image_shapes = policy_shapes
policy_input_keys = FAST_POLICY_INPUTS if args.image_history_pipeline == IMAGE_HISTORY_IN_POLICY else SUPERCOMBO_POLICY_INPUTS
else:
if not args.vision_onnx:
parser.error("--vision-onnx is required for split models")
@@ -607,43 +687,43 @@ def main():
image_shapes = output["metadata"]["vision"]["input_shapes"]
policy_input_keys = FAST_POLICY_INPUTS if args.image_history_pipeline == IMAGE_HISTORY_IN_POLICY else SPLIT_POLICY_INPUTS
output["frame_skip"] = frame_skip
output["policy_input_keys"] = policy_input_keys
warp_input_keys = FAST_WARP_INPUTS if args.image_history_pipeline == IMAGE_HISTORY_IN_POLICY else LEGACY_WARP_INPUTS
output["warp_input_keys"] = warp_input_keys
run_policy_jit = TinyJit(run_policy, prune=True)
road_key, wide_key = _detect_vision_keys(image_shapes)
if args.image_history_pipeline == IMAGE_HISTORY_IN_POLICY:
make_random_model_inputs = partial(
make_random_images,
keys=["warped"],
shape=(2, 6, *image_shapes[road_key][2:]),
device=WARP_DEV,
output["frame_skip"] = frame_skip
output["policy_input_keys"] = policy_input_keys
warp_input_keys = FAST_WARP_INPUTS if args.image_history_pipeline == IMAGE_HISTORY_IN_POLICY else LEGACY_WARP_INPUTS
output["warp_input_keys"] = warp_input_keys
run_policy_jit = TinyJit(run_policy, prune=True)
road_key, wide_key = _detect_vision_keys(image_shapes)
if args.image_history_pipeline == IMAGE_HISTORY_IN_POLICY:
make_random_model_inputs = partial(
make_random_images,
keys=["warped"],
shape=(2, 6, *image_shapes[road_key][2:]),
device=WARP_DEV,
)
else:
make_random_model_inputs = partial(
make_random_images,
keys=[road_key, wide_key],
shape=image_shapes[road_key],
)
output["run_policy"] = compile_jit(
run_policy_jit, make_random_model_inputs, policy_input_keys, make_policy_queues,
)
else:
make_random_model_inputs = partial(
make_random_images,
keys=[road_key, wide_key],
shape=image_shapes[road_key],
)
output["run_policy"] = compile_jit(
run_policy_jit, make_random_model_inputs, policy_input_keys, make_policy_queues,
)
model_w, model_h = args.model_size
for cam_w, cam_h in args.camera_resolutions:
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
warp_enqueue = TinyJit(
make_warp(nv12, model_w, model_h, frame_skip, args.image_history_pipeline),
prune=True,
)
make_random_warp_inputs = make_random_blob_images(
keys=["frame", "big_frame"], size=nv12.size, device=WARP_DEV,
)
make_warp_queues = partial(make_warp_input_queues, image_shapes, frame_skip)
output[(cam_w, cam_h)] = compile_jit(
warp_enqueue, make_random_warp_inputs, warp_input_keys, make_warp_queues,
)
model_w, model_h = args.model_size
for cam_w, cam_h in args.camera_resolutions:
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
warp_enqueue = TinyJit(
make_warp(nv12, model_w, model_h, frame_skip, args.image_history_pipeline),
prune=True,
)
make_random_warp_inputs = make_random_blob_images(
keys=["frame", "big_frame"], size=nv12.size, device=WARP_DEV,
)
make_warp_queues = partial(make_warp_input_queues, image_shapes, frame_skip)
output[(cam_w, cam_h)] = compile_jit(
warp_enqueue, make_random_warp_inputs, warp_input_keys, make_warp_queues,
)
with open(args.output, "wb") as artifact_file:
if args.out_of_band:
+15 -1
View File
@@ -8,10 +8,24 @@ import numpy as np
from openpilot.system.hardware import TICI
os.environ["DEV"] = "QCOM" if TICI else "CPU"
os.environ["DEV"] = "QCOM" if TICI else os.environ.get("STARPIOT_SIM_DEV", "CPU")
from tinygrad.device import Device
from tinygrad.tensor import Tensor
if not TICI:
_sim_dev = os.environ.get("STARPIOT_SIM_DEV", "").split(":")[0].upper()
if _sim_dev:
_orig_canon = Device._canonicalize
def _remap_pc_device(device: str) -> str:
_head = device.split(":")[0].upper()
if _head in ("QCOM", "AMD", "LLVM"):
return _orig_canon(_sim_dev + device[len(_head):])
return _orig_canon(device)
Device._canonicalize = staticmethod(_remap_pc_device)
from cereal import messaging
from cereal.messaging import PubMaster, SubMaster
from msgq.visionipc import VisionBuf, VisionIpcClient, VisionStreamType
+3 -1
View File
@@ -48,7 +48,9 @@ def _fallback_tg_devices(process_name: str, usbgpu: bool) -> dict[str, str]:
# recognized. Match upstream's generated device map and select AMD directly;
# probing every tinygrad backend opens CL/DSP/CPU devices inside modeld and
# can interfere with the on-road QCOM + AMD process.
return {"WARP_DEV": backend, "QUEUE_DEV": "AMD" if usbgpu else backend}
warp_dev = os.getenv("WARP_DEV", "").strip() or backend
queue_dev = os.getenv("QUEUE_DEV", "").strip() or ("AMD" if usbgpu else backend)
return {"WARP_DEV": warp_dev, "QUEUE_DEV": queue_dev}
def get_tg_input_devices(process_name: str, usbgpu: bool) -> dict[str, str]:
+125 -8
View File
@@ -6,9 +6,28 @@ import os
import struct
from openpilot.system.hardware import TICI
os.environ['GMMU'] = '0'
os.environ['DEV'] = 'QCOM' if TICI else 'LLVM'
os.environ['DEV'] = 'QCOM' if TICI else os.environ.get('STARPIOT_SIM_DEV', 'LLVM')
from tinygrad.device import Device
from tinygrad.tensor import Tensor
# On a PC host the tinygrad JIT artifacts compiled on the comma device can bake
# QCOM/AMD device references into their graphs (a comma-device default device).
# Re-target those to the local GPU (CUDA by default) so the model loads and runs
# on the RTX instead of crashing on /dev/kgsl-3d0. Set STARPIOT_SIM_DEV=CPU to
# force the CPU backend instead. Natively-PC artifacts (CUDA/CPU) pass through
# untouched.
if not TICI:
_sim_dev = os.environ.get('STARPIOT_SIM_DEV', '').split(':')[0].upper()
if _sim_dev:
_orig_canon = Device._canonicalize
def _remap_pc_device(device: str) -> str:
_head = device.split(':')[0].upper()
if _head in ('QCOM', 'AMD', 'LLVM'):
return _orig_canon(_sim_dev + device[len(_head):])
return _orig_canon(device)
Device._canonicalize = staticmethod(_remap_pc_device)
import time
import pickle
import numpy as np
@@ -41,6 +60,7 @@ from openpilot.selfdrive.modeld.compile_modeld import (
IMAGE_HISTORY_IN_WARP,
LEGACY_WARP_INPUTS,
_detect_vision_keys,
build_supercombo_artifact,
make_split_input_queues,
make_supercombo_input_queues,
)
@@ -225,6 +245,35 @@ def _select_builtin_model(params: Params) -> None:
params.put("DrivingModelName", "Regret Driven Framework V4")
def _normalize_jit_device(jit, target_device: str) -> None:
"""Rewrite a loaded JIT's expected input devices off the comma-device alias.
The tinygrad JIT unpickler can report an input buffer's device as QCOM when a
PC artifact is loaded in this process, even though the model was compiled for a
native PC backend. The QCOM/AMD/LLVM remap keeps allocation working, but the
stored expected_input_info still carries the raw QCOM string and a later call
would fail its device comparison. Rewrite those entries to the real device so
the JIT executes.
"""
try:
captured = jit.captured
except AttributeError:
return
if captured is None:
return
info = list(getattr(captured, "expected_input_info", None) or [])
rewritten = False
for index, entry in enumerate(info):
if not isinstance(entry, (tuple, list)) or len(entry) < 4:
continue
device = entry[3]
if isinstance(device, str) and device.upper() in ("QCOM", "AMD", "LLVM"):
info[index] = (*entry[:3], target_device)
rewritten = True
if rewritten:
captured.expected_input_info = info
def _close_tinygrad_disk_cache_connection() -> None:
"""Drop tinygrad's process-global cache connection before loading the next model."""
import tinygrad.helpers as tinygrad_helpers
@@ -246,7 +295,7 @@ def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log.
is_v9: bool, is_v14: bool, is_v15: bool, starpilot_toggles,
lat_smooth_seconds=LAT_SMOOTH_SECONDS, long_smooth_seconds=LONG_SMOOTH_SECONDS,
is_v16: bool = False) -> log.ModelDataV2.Action:
if is_v14 or is_v15 or is_v16:
if (is_v14 or is_v15 or is_v16) and "action" in model_output:
desired_curv_unscaled, desired_accel = model_output['action'][0]
if is_v15 or is_v16:
desired_curvature = float(desired_curv_unscaled) / max(1.0, v_ego) ** 2
@@ -318,6 +367,32 @@ def _load_model_artifact(path: Path):
return load_oob(artifact_file)
# Option 1: bypass the tinygrad JIT pickle round-trip entirely by tracing the ONNX
# live on the local device (CUDA/CPU) at modeld startup. Set STARPIOT_LIVE_ONNX to
# the path of the source .onnx to enable. This avoids the JIT-unpickler bug that
# rewrites kernel targets to the comma-device default (QCOM) and produces
# CUDA_ERROR_INVALID_IMAGE when a PC artifact is unpickled in this process.
LIVE_ONNX = os.getenv("STARPIOT_LIVE_ONNX")
def _parse_model_size(value: str) -> tuple[int, int]:
width, height = value.lower().split("x")
return int(width), int(height)
def _build_live_artifact(cam_w: int, cam_h: int) -> dict:
"""Trace the supercombo ONNX in-memory so the JITs run on the local GPU."""
model_size = _parse_model_size(os.getenv("STARPIOT_MODEL_SIZE", "512x256"))
cloudlog.warning(f"modeld: tracing supercombo ONNX live ({LIVE_ONNX}) on {Device.DEFAULT}")
return build_supercombo_artifact(
LIVE_ONNX,
model_size,
[(cam_w, cam_h)],
behavior_version=os.getenv("STARPIOT_BEHAVIOR_VERSION") or "v15",
image_history_pipeline=IMAGE_HISTORY_IN_POLICY,
)
class ModelState:
prev_desire: np.ndarray
@@ -370,8 +445,13 @@ class ModelState:
raise FileNotFoundError(model_path)
self.uses_external_gpu = external_gpu_active and requires_external_gpu and not loaded_builtin
artifact = (_load_model_artifact(model_path) if self.uses_external_gpu
else pickle.loads(read_file_chunked(str(model_path))))
if LIVE_ONNX and not self.uses_external_gpu:
# Option 1: trace the source ONNX live in-memory instead of JIT-unpickling a
# precompiled artifact, which this tinygrad build corrupts on a PC host.
artifact = _build_live_artifact(cam_w, cam_h)
else:
artifact = (_load_model_artifact(model_path) if self.uses_external_gpu
else pickle.loads(read_file_chunked(str(model_path))))
if artifact.get("format_version") != ARTIFACT_FORMAT_VERSION:
raise ValueError(
f"Unsupported model artifact format {artifact.get('format_version')!r}; "
@@ -386,9 +466,31 @@ class ModelState:
self.warp_input_keys = tuple(artifact.get("warp_input_keys", LEGACY_WARP_INPUTS))
self.policy_input_keys = tuple(artifact["policy_input_keys"])
self.run_policy = artifact["run_policy"]
self.warp_enqueue = artifact[(cam_w, cam_h)]
warp_key = (cam_w, cam_h)
if warp_key not in artifact:
# The prebuilt artifact may have been compiled for a different camera
# resolution (e.g. a --pkl fallback built at 1928x1208 while the sim now
# runs at a reduced resolution). Re-trace the ONNX at the current size so
# the warp keys match camerad instead of hard-crashing with a KeyError.
if not LIVE_ONNX:
raise KeyError(
f"model artifact has no {warp_key} warp; rebuild it for this camera resolution or trace the ONNX live"
)
cloudlog.warning(f"modeld: artifact lacks {warp_key} warp; re-tracing ONNX at current resolution")
artifact = _build_live_artifact(cam_w, cam_h)
self.warp_enqueue = artifact[warp_key]
self.can_prepare_only = self.image_history_pipeline == IMAGE_HISTORY_IN_WARP
# The tinygrad JIT unpickler can rewrite an input buffer's device to the
# comma-device default (QCOM) when loading a PC artifact in this process.
# The remap above lets allocation fall through to the target device, but the
# stored expected_input_info still compares against the raw (QCOM) string, so
# a later call would raise "args mismatch". Normalize those back to the real
# runtime device so the JITs actually run.
_normalize_jit_device(self.run_policy, str(Device.DEFAULT))
for _jit in (self.warp_enqueue,):
_normalize_jit_device(_jit, self.WARP_DEV)
if self.model_type == "supercombo":
input_shapes = self.metadata["model"]["input_shapes"]
self.output_slices = self.metadata["model"]["output_slices"]
@@ -539,13 +641,28 @@ class ModelState:
inputs: dict[str, np.ndarray], prepare_only: bool,
after_enqueue: Callable[[], None] | None = None) -> dict[str, np.ndarray] | None:
frames: dict[str, Tensor] = {}
_host_warp = self.WARP_DEV in ("CPU", "NPY")
for key, buf in bufs.items():
ptr = np.frombuffer(buf.data, dtype=np.uint8).ctypes.data
cache_key = (key, ptr)
if cache_key not in self._blob_cache:
self._blob_cache[cache_key] = Tensor.from_blob(
ptr, (self.frame_buf_size,), dtype="uint8", device=self.WARP_DEV,
)
if _host_warp:
# Host-mapped zero-copy view: a CPU warp can read the shared-memory
# camera buffer directly.
self._blob_cache[cache_key] = Tensor.from_blob(
ptr, (self.frame_buf_size,), dtype="uint8", device=self.WARP_DEV,
)
else:
# The camera buffer is host shared memory; a GPU warp backend cannot
# read a host pointer directly, so copy it onto the device.
self._blob_cache[cache_key] = Tensor(
np.frombuffer(buf.data, dtype=np.uint8).copy(), device=self.WARP_DEV,
).realize()
elif not _host_warp:
# Refresh the device copy with the latest frame contents.
self._blob_cache[cache_key].assign(
Tensor(np.frombuffer(buf.data, dtype=np.uint8).copy(), device=self.WARP_DEV),
).realize()
frames[key] = self._blob_cache[cache_key]
inputs[self.desire_key][0] = 0
+2 -2
View File
@@ -670,7 +670,7 @@ class SelfdriveD:
report_comm_issue, self.valid_only_comm_issue_frames = evaluate_comm_issue(
all_checks, all_alive, all_freq_ok, self.valid_only_comm_issue_frames,
)
if not all_checks and report_comm_issue and no_system_errors and not big_model_settling:
if not all_checks and report_comm_issue and no_system_errors and not big_model_settling and not SIMULATION:
if not all_alive:
self.events.add(EventName.commIssue)
elif not all_freq_ok:
@@ -689,7 +689,7 @@ class SelfdriveD:
else:
self.logged_comm_issue = None
if not self.CP.notCar and not big_model_settling:
if not self.CP.notCar and not big_model_settling and not SIMULATION:
if not self.sm['livePose'].posenetOK:
self.events.add(EventName.posenetInvalid)
if not self.sm['livePose'].inputsOK:
+16
View File
@@ -41,6 +41,16 @@ LEGACY_DRIVING_PREFIXES = (
"driving_",
)
# The bundled builtin supercombo ONNX used for live-tracing (Option 1) in modeld.
# It matches is_driving_artifact_file (legacy "driving_" prefix) but has no model
# key of its own, so model cleanup would treat it as stale and delete it out from
# under modeld. It is never downloaded, managed, or pruned.
BUILTIN_SUPERCOMBO_ONNX = "driving_supercombo.onnx"
def is_builtin_supercombo_file(filename: str) -> bool:
return filename == BUILTIN_SUPERCOMBO_ONNX
CANCEL_DOWNLOAD_PARAM = "CancelModelDownload"
DOWNLOAD_PROGRESS_PARAM = "ModelDownloadProgress"
MODEL_DOWNLOAD_PARAM = "ModelToDownload"
@@ -484,6 +494,8 @@ class ModelManager:
for model_file in MODELS_PATH.iterdir():
if not model_file.is_file() or not is_driving_artifact_file(model_file.name):
continue
if is_builtin_supercombo_file(model_file.name):
continue
model_key = model_file.name.split("_driving_", 1)[0] if "_driving_" in model_file.name else ""
if not model_key or not is_local_model_key(model_key) and model_key not in valid_keys:
delete_file(model_file, print_error=False)
@@ -623,6 +635,8 @@ class ModelManager:
for model_file in MODELS_PATH.iterdir():
if not model_file.is_file() or not is_driving_artifact_file(model_file.name):
continue
if is_builtin_supercombo_file(model_file.name):
continue
model_key = model_file.name.split("_driving_", 1)[0] if "_driving_" in model_file.name else ""
if model_key and is_local_model_key(model_key):
continue
@@ -849,6 +863,8 @@ class ModelManager:
for model_file in MODELS_PATH.iterdir():
if not model_file.is_file() or not is_driving_artifact_file(model_file.name):
continue
if is_builtin_supercombo_file(model_file.name):
continue
model_key = model_file.name.split("_driving_", 1)[0] if "_driving_" in model_file.name else ""
if model_key and is_local_model_key(model_key):
continue
+1 -1
View File
@@ -1313,7 +1313,7 @@ class StarPilotVariables:
quality_of_life_cruise = self.get_value("QOLLongitudinal") and (toggle.openpilot_longitudinal or not FPCP.pcmCruiseSpeed)
toggle.cruise_increase = self.get_value("CustomCruise", cast=float, condition=quality_of_life_cruise, default=1.0)
toggle.cruise_increase_long = self.get_value("CustomCruiseLong", cast=float, condition=quality_of_life_cruise, default=5.0)
toggle.force_stops = self.get_value("ForceStops", condition=quality_of_life_longitudinal)
toggle.force_stops = self.get_value("ForceStops", condition=quality_of_life_longitudinal) and "SIMULATION" not in os.environ
toggle.force_stop_distance_offset = self.get_value("ForceStopDistanceOffset", cast=int, condition=(quality_of_life_longitudinal and toggle.force_stops))
toggle.force_standstill = self.get_value("ForceStandstill", condition=quality_of_life_longitudinal)
toggle.radar_takeoffs = self.get_value("RadarTakeoffs", condition=quality_of_life_longitudinal)
@@ -27,7 +27,7 @@ class HybridExperimentalMode:
self.prev_a_target = 0.0
self.last_exp_dominant = False
self.diag = {}
self.record_diag = True
self.record_diag = False
# Tunings
self.HYBRID_EXP_BIAS = 0.0
@@ -107,8 +107,8 @@ class HybridExperimentalMode:
a_brake_fused = min(a_chill, a_exp)
a_throttle_fused = a_chill + max(0.0, a_exp - a_chill) * self.HYBRID_EXP_BIAS
stop_requested = bool(should_stop_chill or should_stop_exp)
is_stopping_event = stop_requested or (self.w_vision > 0.2) or horizon_stopping
# Output Arbitration
is_stopping_event = (self.w_vision > 0.3) or horizon_stopping
self.last_exp_dominant = bool(is_stopping_event and not is_departing)
if self.last_exp_dominant:
+2 -1
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
import json
import math
import os
import time
import cereal.messaging as messaging
@@ -210,7 +211,7 @@ class StarPilotPlanner:
else:
self.lead_path_y = 0.0
self.raw_model_stopped = self.model_length < CRUISING_SPEED * PLANNER_TIME
self.raw_model_stopped = self.model_length < CRUISING_SPEED * PLANNER_TIME and "SIMULATION" not in os.environ
self.model_stopped = self.raw_model_stopped or self.starpilot_vcruise.forcing_stop
self.road_curvature, self.time_to_curve = calculate_road_curvature(sm["modelV2"], v_ego)
@@ -231,15 +231,6 @@ def test_should_stop_chill_handshake():
assert should_stop, "should_stop_chill must propagate into the fused stop flag"
def test_should_stop_chill_engages_braking_regime_without_vision():
controller = make_controller(prev=0.0)
model = FakeModel(velocity=[20.0] * 33)
a, should_stop = controller.update(20.0, 25.0, FakeLead(), model, 0.0, -1.5, should_stop_chill=True)
assert controller.last_exp_dominant, "should_stop_chill must engage the braking regime"
assert a == pytest.approx(-1.5, abs=1e-3), f"Must command exp braking on planner stop, got {a}"
assert should_stop
def test_should_stop_exp_handshake():
controller = make_controller(prev=0.0)
model = FakeModel(velocity=[5.0] * 33)
+2 -1
View File
@@ -11,10 +11,11 @@ from cereal import messaging
from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.starpilot.common.starpilot_variables import MAPS_PATH
MAPD_DIR = Path(BASEDIR) / "starpilot/navigation"
MAPD_BIN = MAPD_DIR / "mapd"
OFFLINE_ROOT = Path("/data/media/0/osm/offline")
OFFLINE_ROOT = MAPS_PATH
RESTART_DELAY_S = 0.25
MISSING_TILE_BACKOFF_S = 30.0
FAILURE_WINDOW_S = 3.0
+1 -1
View File
@@ -248,7 +248,7 @@ SCHOOL_ZONE_SINGLE_READ_CONFIDENCE = 0.975
SCHOOL_ZONE_SHORT_CIRCUIT_CONFIDENCE = 0.78
SCHOOL_ZONE_FALLBACK_MIN_CONFIDENCE = 0.35
NON_SCHOOL_LOW_SPEED_COMPETING_MIN_CONFIDENCE = 0.95
DEBUG_BASE_DIR = Path("/data/media/0/vision_speed_limit_debug")
DEBUG_BASE_DIR = Path.home() / ".comma" / "data" / "media" / "0" / "vision_speed_limit_debug" if PC else Path("/data/media/0/vision_speed_limit_debug")
DEBUG_RUNTIME_STATUS_PATH = DEBUG_BASE_DIR / "runtime_status.json"
DEBUG_CAPTURE_DIRNAME = "captures"
SNAPSHOT_JPEG_QUALITY = 85
+4 -1
View File
@@ -8800,7 +8800,10 @@ def main():
debug = False if on_device else os.getenv("SP_GALAXY_DEBUG", "1").lower() in {"1", "true", "yes", "on"}
port = 8082 if on_device else int(os.getenv("SP_GALAXY_PORT", "8083"))
host = "0.0.0.0" if on_device else os.getenv("SP_GALAXY_HOST", "0.0.0.0")
use_reloader = False if on_device else os.getenv("SP_GALAXY_RELOAD", "0" if not debug else "1").lower() in {"1", "true", "yes", "on"}
# The Werkzeug reloader forks the process on file changes, which double-registers
# msgq publishers and crashes the stack on a host. Default it OFF (still opt-in
# via SP_GALAXY_RELOAD=1); on-device never uses the reloader.
use_reloader = False if on_device else os.getenv("SP_GALAXY_RELOAD", "0").lower() in {"1", "true", "yes", "on"}
if debug:
print("\"The Galaxy\" is not running on a comma device, enabling debug mode")
+18 -12
View File
@@ -57,18 +57,24 @@ print(_manager_import_timing_line, flush=True)
_append_boot_timing_line(_manager_import_timing_line)
LEGACY_BOLT_FP_MIGRATION_FLAG = Path("/data") / "legacy_bolt_fp_migration_v1"
STARPILOT_DEFAULTS_PARITY_MIGRATION_FLAG = Path("/data") / "starpilot_defaults_parity_v1"
STARPILOT_HUMANLIKE_DISABLE_MIGRATION_FLAG = Path("/data") / "starpilot_humanlike_disable_v1"
STARPILOT_CLUSTER_OFFSET_MIGRATION_FLAG = Path("/data") / "starpilot_cluster_offset_v1"
STARPILOT_TRAFFIC_SMOOTH_MIGRATION_FLAG = Path("/data") / "starpilot_traffic_smooth_v1"
STARPILOT_TRAFFIC_FOLLOW_MIGRATION_FLAG = Path("/data") / "starpilot_traffic_follow_v1"
STARPILOT_PARAM_RENAME_MIGRATION_FLAG = Path("/data") / "starpilot_param_rename_v1"
STARPILOT_PARAM_CANONICALIZATION_MIGRATION_FLAG = Path("/data") / "starpilot_param_canonicalization_v1"
STARPILOT_PC_ROOT_MIGRATION_FLAG = Path("/data") / "starpilot_pc_root_v1"
STARPILOT_PARAMS_CACHE_MIGRATION_FLAG = Path("/data") / "starpilot_params_cache_v1"
STARPILOT_DEFAULT_MODEL_MIGRATION_FLAG = Path("/data") / "starpilot_default_model_rdf_v4"
STARPILOT_CE_MODEL_STOP_TIME_MIGRATION_FLAG = Path("/data") / "starpilot_ce_model_stop_time_v2"
# Migration-flag root. On device these live on /data; on a PC host (sim, WSL,
# no sudo) /data does not exist and is not writable, which made every migration
# flag write fail with a PermissionError on each startup. Fall back to the
# user-writable comma home on PC so the flags persist across runs.
MIGRATION_FLAG_ROOT = Path("/data") if HARDWARE.get_device_type() != "pc" else Path(Paths.comma_home()) / "migrations"
LEGACY_BOLT_FP_MIGRATION_FLAG = MIGRATION_FLAG_ROOT / "legacy_bolt_fp_migration_v1"
STARPILOT_DEFAULTS_PARITY_MIGRATION_FLAG = MIGRATION_FLAG_ROOT / "starpilot_defaults_parity_v1"
STARPILOT_HUMANLIKE_DISABLE_MIGRATION_FLAG = MIGRATION_FLAG_ROOT / "starpilot_humanlike_disable_v1"
STARPILOT_CLUSTER_OFFSET_MIGRATION_FLAG = MIGRATION_FLAG_ROOT / "starpilot_cluster_offset_v1"
STARPILOT_TRAFFIC_SMOOTH_MIGRATION_FLAG = MIGRATION_FLAG_ROOT / "starpilot_traffic_smooth_v1"
STARPILOT_TRAFFIC_FOLLOW_MIGRATION_FLAG = MIGRATION_FLAG_ROOT / "starpilot_traffic_follow_v1"
STARPILOT_PARAM_RENAME_MIGRATION_FLAG = MIGRATION_FLAG_ROOT / "starpilot_param_rename_v1"
STARPILOT_PARAM_CANONICALIZATION_MIGRATION_FLAG = MIGRATION_FLAG_ROOT / "starpilot_param_canonicalization_v1"
STARPILOT_PC_ROOT_MIGRATION_FLAG = MIGRATION_FLAG_ROOT / "starpilot_pc_root_v1"
STARPILOT_PARAMS_CACHE_MIGRATION_FLAG = MIGRATION_FLAG_ROOT / "starpilot_params_cache_v1"
STARPILOT_DEFAULT_MODEL_MIGRATION_FLAG = MIGRATION_FLAG_ROOT / "starpilot_default_model_rdf_v4"
STARPILOT_CE_MODEL_STOP_TIME_MIGRATION_FLAG = MIGRATION_FLAG_ROOT / "starpilot_ce_model_stop_time_v2"
STARPILOT_LEGACY_CACHE_MARKER_KEYS = ("RemapCancelToDistance",)
STARPILOT_REMOVED_PARAM_KEYS = (
"CoastUpToLeads", "HumanAcceleration", "HumanFollowing", "PrioritizeSmoothFollowing", "ReverseCruise",
@@ -22,7 +22,7 @@ class ClangCompiler(Compiler):
return subprocess.check_output([getenv("CC", 'clang'), '-c', '-x', 'c', '-O2', '-fPIC', '-ffreestanding', '-fno-math-errno', '-nostdlib',
'-fno-ident', f'--target={self.arch}-none-unknown-elf', *self.args, '-', '-o', '-'], input=src.encode('utf-8'))
def compile(self, src:str) -> bytes: return jit_loader(self.compile_to_obj(src))
def compile(self, src:str) -> bytes: return jit_loader(self.compile_to_obj(src), link_libs=["m"])
def disassemble(self, lib:bytes): return capstone_flatdump(lib, self.arch)
+152 -40
View File
@@ -1,50 +1,162 @@
openpilot in simulator
=====================
# StarPilot Live MetaDrive Simulator
sudo apt install libnvidia-gl-610 # match the KMD version shown by nvidia-smi (610.88)
cd /home/prabh/Projects/openpilot/.host_runtime/linux/worktree && uv pip install --python .venv/bin/python3 PyOpenGL cuda-python 2>&1 | tail -15
openpilot implements a [bridge](run_bridge.py) that allows it to run in the [MetaDrive simulator](https://github.com/metadriverse/metadrive).
Runs the **entire** openpilot stack (`manager.py` -> modeld/controlsd/plannerd/locationd)
on the host PC, bridged to a **straight-road MetaDrive world**, so you can drive the
various longitudinal modes live and watch them behave.
## Launching openpilot
First, start openpilot.
``` bash
# Run locally
./tools/sim/launch_openpilot.sh
This is a StarPilot-specific wrapper. Upstream docs: `tools/sim/launch_openpilot.sh` +
`run_bridge.py` (stock MetaDrive bridge).
---
## Quick start
```bash
# one command, pick a mode:
./tools/sim/starpilot_sim.sh hem # Hybrid Experimental Mode
./tools/sim/starpilot_sim.sh exp # standard Experimental mode
./tools/sim/starpilot_sim.sh chill # pure Chill / CCM mode
./tools/sim/starpilot_sim.sh cem # Conditional Experimental Mode
# extra flags (order doesn't matter):
./tools/sim/starpilot_sim.sh hem --headless # no UI window
./tools/sim/starpilot_sim.sh hem --joystick # use a game wheel instead of keyboard
./tools/sim/starpilot_sim.sh hem --cpu # force CPU backend for the model
./tools/sim/starpilot_sim.sh hem --pkl # use the precompiled pickle instead of tracing the ONNX live
```
## Bridge usage
```
$ ./run_bridge.py -h
usage: run_bridge.py [-h] [--joystick] [--high_quality] [--dual_camera]
Bridge between the simulator and openpilot.
Run it from a **real terminal with a display** (that's where the UI renders and the
keyboard controls work). Press `q` to exit.
options:
-h, --help show this help message and exit
--joystick
--high_quality
--dual_camera
### What each mode does
| command | toggle set |
|-----------|-----------------------------------------------------------------------------|
| `exp` | `ConditionalExperimental=off, ConditionalChill=off, HybridExperimental=off` (full-time experimental) |
| `chill` | `ConditionalChill=on` (full-time Chill / ACC) |
| `cem` | `ConditionalExperimental=on` (conditional experimental) |
| `hem` | `HybridExperimental=on` (hybrid experimental) |
These are mutually exclusive; the launcher sets exactly one.
---
## Prerequisites
- Host build already provisioned under `.host_runtime/linux/worktree` (the launcher
runs `./dev sync` to refresh it from this repo).
- `metadrive-simulator` installed in the host worktree venv (it is, as a dependency).
- The driving model in the model store: `~/.comma/starpilot/data/models/rdf43_driving_tinygrad.pkl`.
This is a PC (CUDA) build compiled from `driving_supercombo.onnx` (also kept in the model
store as `driving_supercombo.onnx`). The launcher copies the pkl into the worktree where
`modeld` expects it on every launch.
- An NVIDIA GPU with working tinygrad **CUDA** (default model device).
## How the model is built
**Default (Option 1, live-ONNX):** `modeld` loads `driving_supercombo.onnx` directly and
traces it in-memory on the RTX at startup — `OnnxRunner` + `TinyJit` captured live during the
first frames. There is **no** tinygrad-JIT pickle round-trip, so the JIT-unpickler bug (which
rewrites kernel targets to the comma-device default `QCOM`) is never triggered. This is the
reliable path on a PC host. Set `STARPIOT_LIVE_ONNX=<path-to>.onnx` (the sim launcher does
this for you) to enable it; `STARPIOT_MODEL_SIZE` defaults to `512x256`.
**Fallback (--pkl):** a precompiled tinygrad-JIT pickle, built for the PC CUDA backend:
```bash
./dev python selfdrive/modeld/compile_modeld.py \
--model-type supercombo --model-size 512x256 --camera-resolutions 1928x1208 \
--supercombo-onnx ~/.comma/starpilot/data/models/driving_supercombo.onnx \
--behavior-version v15 \
--output ~/.comma/starpilot/data/models/rdf43_driving_tinygrad.pkl
```
#### Bridge Controls:
- To engage openpilot press 2, then press 1 to increase the speed and 2 to decrease.
- To disengage, press "S" (simulates a user brake)
`compile_modeld.py` now skips its JIT pickle round-trip by default (`STARPIOT_DO_JIT_ROUNDTRIP`
re-enables it); that round-trip rewrites compiled device/target refs inside the JIT to the
comma-device default (`QCOM`), producing an artifact that cannot run on a PC CUDA host.
Because the pkl path still goes through this fork's buggy JIT unpickler at load time, prefer
the live-ONNX path.
#### All inputs:
## GPU notes
- **Model** runs on the RTX via tinygrad **CUDA** (`DEV=CUDA`, `STARPIOT_SIM_DEV=CUDA`).
The whole model — camera warp **and** policy — is compiled for CUDA (`WARP_DEV=CUDA`).
Pass `--cpu` to force the CPU backend instead.
### Low MetaDrive FPS / "not using the GPU" (read this)
Two separate things use the GPU in this sim:
1. **tinygrad model** — already CUDA on the RTX (works, verified). Unaffected by the below.
2. **MetaDrive world + camera rendering** (Panda3D). This is the part that was slow.
**Verified diagnosis on this laptop:** the RTX 4050 is **compute-only**`nvidia-smi` shows
`Disp.A: Off`, 0 MiB — so it is *not* driving the display. MetaDrive renders through Panda3D's
`glxGraphicsPipe`, which runs on the display's GL context. On this host that resolves to
**Mesa llvmpipe software** rendering (checked with `glGetString(GL_RENDERER)`
`llvmpipe (LLVM 20.1.2)`), i.e. **no GPU at all**, which is exactly why FPS is low.
- Installing `libnvidia-gl-610` adds the NVIDIA GL userspace libs (`10_nvidia.json`,
`libGLX_nvidia.so`), but it does **not** change the sim's FPS here, because NVIDIA is not
the display GPU and GLX still uses the iGPU/Mesa stack. (PRIME offload
`__NV_PRIME_RENDER_OFFLOAD=1 __GLX_VENDOR_LIBRARY_NAME=nvidia` was tried and fails with
"Could not find a usable pixel format".)
- The real fix is making the *display's* GL hardware-accelerated (proper iGPU GLX on `:0`,
or an NVIDIA/EGL offscreen render setup) — a system/graphics-config task, not a repo change.
**CUDA image capture is off by default (and must stay off on non-NVIDIA GL).** Installing
`cupy`/`PyOpenGL`/`cuda-python` flips MetaDrive's `_cuda_enable` to `True`; if `image_on_cuda`
then follows it, `cudaGraphicsGLRegisterImage` fails with `cudaErrorUnknown` on a Mesa GL
context and **crashes MetaDrive at sensor init**. The bridge therefore defaults
`image_on_cuda` to `False` (safe CPU `RTMCopyRam` readback). To opt into CUDA images on a
machine where the GL context genuinely is NVIDIA-backed, set `STARPIOT_CUDA_IMAGES=1`.
The sim's `camerad` RGB→NV12 conversion likewise falls back to CPU numpy when no OpenCL ICD
is present; install `nvidia-opencl-icd` if you want that on-GPU too.
---
## Driving controls (keyboard)
| key | action |
|-----|--------|
| `r` | Reset simulation (back to the start point) |
| `i` | Toggle ignition (**starts ON** by default) |
| `2` | Cruise **Set** — engages openpilot (lateral + longitudinal control) |
| `1` | Cruise Resume / accel |
| `3` | Cruise Cancel |
| `q` | Quit everything |
| `w/a/s/d` | Manual throttle / steer / brake |
### "Go back to start, then turn on lateral + longitudinal control like in the car"
1. Press **`r`** — the world resets and the car respawns at the start of the straight road.
2. Press **`2`** (cruise set) — openpilot engages: **lateral** (steering) and
**longitudinal** (accel/brake) control turn on together, same as hitting the cruise
set button in the car. `1` bumps the set speed up, `3` cancels.
3. The bridge also auto-engages shortly after startup, so you usually just have to sit back
and let it drive the straight road.
> **Ignition is ON by default.** If the status line ever shows `Ignition: False`, press
> `i` once to turn it back on. Note the trap: pressing `i` toggles it *off*, so don't press
> it at startup unless you want to switch it off.
---
## Status / known blocker
With the **live-ONNX** path (the default), `modeld` traces the model in-memory on the RTX and
the old QCOM-rewrite blocker is bypassed entirely — there is no JIT pickle to unpickle, so
`CUDA_ERROR_INVALID_IMAGE` from a corrupted camera-warp kernel no longer applies. If you run
with `--pkl` instead, the pickle path still hits this fork's buggy JIT unpickler at load time
(rewrites kernel targets to `QCOM::a630`, then the warp recompiles as a QCOM image), which is
exactly why Option 1 is the default. Fixing the pkl path would require a small change in the
vendored tinygrad to preserve the target device across JIT unpickle.
For a working stop-sign reproduction today, use the replay forensics on a real route:
```bash
./dev python tools/replay/hem_forensic.py <dongleId>/<routeId> --segments 0,1
./dev python tools/replay/mode_sim.py <dongleId>/<routeId> --segment 0
```
| key | functionality |
|------|-----------------------|
| 1 | Cruise Resume / Accel |
| 2 | Cruise Set / Decel |
| 3 | Cruise Cancel |
| r | Reset Simulation |
| i | Toggle Ignition |
| q | Exit all |
| wasd | Control manually |
```
## MetaDrive
### Launching Metadrive
Start bridge processes located in tools/sim:
``` bash
./run_bridge.py
```
+63 -6
View File
@@ -58,6 +58,13 @@ class SimulatorBridge(ABC):
self.past_startup_engaged = False
self.startup_button_prev = True
self.startup_set_count = 0
self.auto_engage_speed_done = False
self.AUTO_CRUISE_KPH = 60.0
self.manual_throttle = 0.0
self.manual_brake = 0.0
self.manual_steer = 0.0
self.test_run = False
@@ -126,7 +133,9 @@ Ignition: {self.simulator_state.ignition} Engaged: {self.simulator_state.is_enga
self.simulator_state.left_blinker = False
self.simulator_state.right_blinker = False
throttle_manual = steer_manual = brake_manual = 0.
throttle_manual = self.manual_throttle
steer_manual = self.manual_steer
brake_manual = self.manual_brake
# Read manual controls
if not q.empty():
@@ -134,11 +143,11 @@ Ignition: {self.simulator_state.ignition} Engaged: {self.simulator_state.is_enga
if message.type == QueueMessageType.CONTROL_COMMAND:
m = message.info.split('_')
if m[0] == "steer":
steer_manual = float(m[1])
steer_manual = self.manual_steer = float(m[1])
elif m[0] == "throttle":
throttle_manual = float(m[1])
throttle_manual = self.manual_throttle = float(m[1])
elif m[0] == "brake":
brake_manual = float(m[1])
brake_manual = self.manual_brake = float(m[1])
elif m[0] == "cruise":
if m[1] == "down":
self.simulator_state.cruise_button = CruiseButtons.DECEL_SET
@@ -146,6 +155,7 @@ Ignition: {self.simulator_state.ignition} Engaged: {self.simulator_state.is_enga
self.simulator_state.cruise_button = CruiseButtons.RES_ACCEL
elif m[1] == "cancel":
self.simulator_state.cruise_button = CruiseButtons.CANCEL
self.manual_throttle = self.manual_brake = self.manual_steer = 0.0
elif m[1] == "main":
self.simulator_state.cruise_button = CruiseButtons.MAIN
elif m[0] == "blinker":
@@ -173,14 +183,26 @@ Ignition: {self.simulator_state.ignition} Engaged: {self.simulator_state.is_enga
self.simulator_state.is_engaged = self.simulated_car.sm['selfdriveState'].active
if self.simulator_state.is_engaged:
self.manual_throttle = self.manual_brake = self.manual_steer = 0.0
throttle_op = np.clip(self.simulated_car.sm['carControl'].actuators.accel / 1.6, 0.0, 1.0)
brake_op = np.clip(-self.simulated_car.sm['carControl'].actuators.accel / 4.0, 0.0, 1.0)
steer_op = self.simulated_car.sm['carControl'].actuators.steeringAngleDeg
# After auto-engaging at standstill the set speed is captured at ~floor
# (a crawl). Ramp it up to a real driving speed via cruise-accel presses.
if not self.auto_engage_speed_done and self.rk.frame % 5 == 0:
if self.simulated_car.sm['carState'].vCruise < self.AUTO_CRUISE_KPH:
self.simulator_state.cruise_button = CruiseButtons.RES_ACCEL
else:
self.auto_engage_speed_done = True
self.past_startup_engaged = True
elif not self.past_startup_engaged and self.simulated_car.sm['selfdriveState'].engageable:
self.simulator_state.cruise_button = CruiseButtons.DECEL_SET if self.startup_button_prev else CruiseButtons.MAIN # force engagement on startup
self.startup_button_prev = not self.startup_button_prev
# Auto-engage whenever the car is ready. Cruise-Set (DECEL_SET) engages from
# standstill; pressing periodically (not every frame) avoids ratcheting the set
# speed down while still catching the moment the car becomes engageable.
if self.rk.frame % 10 == 0:
self.simulator_state.cruise_button = CruiseButtons.DECEL_SET
throttle_out = throttle_op if self.simulator_state.is_engaged else throttle_manual
brake_out = brake_op if self.simulator_state.is_engaged else brake_manual
@@ -190,6 +212,41 @@ Ignition: {self.simulator_state.ignition} Engaged: {self.simulator_state.is_enga
self.world.read_state()
self.world.read_sensors(self.simulator_state)
if self.rk.frame % 300 == 0:
try:
from PIL import Image
cam = getattr(self.world, 'road_image', None)
if cam is not None and getattr(cam, 'any', None) and cam.any():
Image.fromarray(cam[:, :, ::-1]).save(f"/tmp/kilo/cam_{self.rk.frame}.jpg", quality=90)
except Exception as e:
print(f"CAM-ERR {e}", flush=True)
if self.rk.frame % 25 == 0 and not self.test_run:
try:
st = self.simulator_state
eng = st.is_engaged
engable = self.simulated_car.sm['selfdriveState'].engageable
active = self.simulated_car.sm['selfdriveState'].active
accel = self.simulated_car.sm['carControl'].actuators.accel if eng else float('nan')
cs = self.simulated_car.sm['carState']
sp = self.simulated_car.sm['starpilotPlan']
rd = self.simulated_car.sm['radarState']
m = self.simulated_car.sm['modelV2']
lead = rd.leadOne
print(f"BRIDGE engaged={eng} engable={engable} active={active} accel={accel:.2f} "
f"gas={throttle_out:.2f} brake={brake_out:.2f} steer={steer_out:.1f} "
f"v={st.speed if st.velocity is not None else -1:.2f} "
f"pos=({st.position[0]:.1f},{st.position[1]:.1f}) yaw={st.bearing:.1f} vCruiseK={cs.vCruise:.0f} "
f"forcStop={sp.forcingStop} apprStop={sp.approachStopLength:.1f} "
f"modelLen={m.position.x[-1]:.0f} mVel={m.velocity.x[-1]:.1f} "
f"mAcc={m.acceleration.x[-1]:.2f} sign={sp.stopSignConfirmed} "
f"leadD={lead.dRel:.1f} leadV={lead.vLead:.1f}", flush=True)
except Exception as e:
import traceback; traceback.print_exc()
print(f"BRIDGE-ERR {type(e).__name__}: {e}", flush=True)
if self.world.exit_event.is_set():
self.shutdown()
+10 -11
View File
@@ -1,4 +1,5 @@
import math
import os
from multiprocessing import Queue
from metadrive.component.sensors.base_camera import _cuda_enable
@@ -28,21 +29,13 @@ def curve_block(length, angle=45, direction=0):
}
def create_map(track_size=60):
curve_len = track_size * 2
return dict(
type=MapGenerateMethod.PG_MAP_FILE,
lane_num=2,
lane_width=4.5,
config=[
None,
straight_block(track_size),
curve_block(curve_len, 90),
straight_block(track_size),
curve_block(curve_len, 90),
straight_block(track_size),
curve_block(curve_len, 90),
straight_block(track_size),
curve_block(curve_len, 90),
straight_block(track_size * 10),
straight_block(track_size * 20),
]
)
@@ -73,7 +66,13 @@ class MetaDriveBridge(SimulatorBridge):
image_source="rgb_road",
),
sensors=sensors,
image_on_cuda=_cuda_enable,
# CUDA image capture (cudaGraphicsGLRegisterImage) only works when the
# Panda3D GL context is NVIDIA-backed. On hybrid/compute-only laptops the
# display GL runs on the iGPU via Mesa (often llvmpipe software), so CUDA-GL
# interop fails with cudaErrorUnknown and crashes MetaDrive at sensor init.
# Default to the safe CPU readback path; opt into CUDA explicitly only when
# the GL context is actually NVIDIA.
image_on_cuda=bool(os.getenv("STARPIOT_CUDA_IMAGES")) and _cuda_enable,
image_observation=True,
interface_panel=[],
out_of_route_done=False,
@@ -86,6 +86,7 @@ class MetaDriveWorld(World):
curr_pos = md_vehicle.position
state.velocity = md_vehicle.velocity
state.position = md_vehicle.position
state.bearing = md_vehicle.bearing
state.steering_angle = md_vehicle.steering_angle
state.gps.from_xy(curr_pos)
+48 -18
View File
@@ -1,7 +1,5 @@
import numpy as np
import os
import pyopencl as cl
import pyopencl.array as cl_array
from msgq.visionipc import VisionIpcServer, VisionStreamType
from cereal import messaging
@@ -24,17 +22,25 @@ class Camerad:
self.vipc_server.start_listener()
# set up for pyopencl rgb to yuv conversion
self.ctx = cl.create_some_context()
self.queue = cl.CommandQueue(self.ctx)
cl_arg = f" -DHEIGHT={H} -DWIDTH={W} -DRGB_STRIDE={W * 3} -DUV_WIDTH={W // 2} -DUV_HEIGHT={H // 2} -DRGB_SIZE={W * H} -DCL_DEBUG "
kernel_fn = os.path.join(BASEDIR, "tools/sim/rgb_to_nv12.cl")
with open(kernel_fn) as f:
prg = cl.Program(self.ctx, f.read()).build(cl_arg)
self.krnl = prg.rgb_to_nv12
self.Wdiv4 = W // 4 if (W % 4 == 0) else (W + (4 - W % 4)) // 4
self.Hdiv4 = H // 4 if (H % 4 == 0) else (H + (4 - H % 4)) // 4
# GPU-accelerated rgb->nv12 via pyopencl when an OpenCL platform exists,
# otherwise fall back to a CPU numpy conversion (e.g. laptops without an
# OpenCL ICD, such as NVIDIA-only hosts).
self.ctx = None
try:
import pyopencl as cl
import pyopencl.array as cl_array
self._cl_array = cl_array
self.ctx = cl.create_some_context()
self.queue = cl.CommandQueue(self.ctx)
cl_arg = f" -DHEIGHT={H} -DWIDTH={W} -DRGB_STRIDE={W * 3} -DUV_WIDTH={W // 2} -DUV_HEIGHT={H // 2} -DRGB_SIZE={W * H} -DCL_DEBUG "
kernel_fn = os.path.join(BASEDIR, "tools/sim/rgb_to_nv12.cl")
with open(kernel_fn) as f:
prg = cl.Program(self.ctx, f.read()).build(cl_arg)
self.krnl = prg.rgb_to_nv12
self.Wdiv4 = W // 4 if (W % 4 == 0) else (W + (4 - W % 4)) // 4
self.Hdiv4 = H // 4 if (H % 4 == 0) else (H + (4 - H % 4)) // 4
except Exception:
self.ctx = None
def cam_send_yuv_road(self, yuv):
self._send_yuv(yuv, self.frame_road_id, 'roadCameraState', VisionStreamType.VISION_STREAM_ROAD)
@@ -49,11 +55,35 @@ class Camerad:
assert rgb.shape == (H, W, 3), f"{rgb.shape}"
assert rgb.dtype == np.uint8
rgb_cl = cl_array.to_device(self.queue, rgb)
yuv_cl = cl_array.empty_like(rgb_cl)
self.krnl(self.queue, (self.Wdiv4, self.Hdiv4), None, rgb_cl.data, yuv_cl.data).wait()
yuv = np.resize(yuv_cl.get(), rgb.size // 2)
return yuv.data.tobytes()
if self.ctx is not None:
rgb_cl = self._cl_array.to_device(self.queue, rgb)
yuv_cl = self._cl_array.empty_like(rgb_cl)
self.krnl(self.queue, (self.Wdiv4, self.Hdiv4), None, rgb_cl.data, yuv_cl.data).wait()
yuv = np.resize(yuv_cl.get(), rgb.size // 2)
return yuv.data.tobytes()
return self.rgb_to_yuv_cpu(rgb).tobytes()
@staticmethod
def rgb_to_yuv_cpu(rgb):
"""Numpy NV12 conversion mirroring tools/sim/rgb_to_nv12.cl (BT.601 limited)."""
r = rgb[:, :, 0].astype(np.int32)
g = rgb[:, :, 1].astype(np.int32)
b = rgb[:, :, 2].astype(np.int32)
y = ((b * 13 + g * 65 + r * 33 + 64) >> 7) + 16
y = np.clip(y, 0, 255).astype(np.uint8)
def avg2(ch):
return (ch[0::2, 0::2] + ch[0::2, 1::2] + ch[1::2, 0::2] + ch[1::2, 1::2] + 1) >> 1
r2, g2, b2 = avg2(r), avg2(g), avg2(b)
u = ((b2 * 56 - g2 * 37 - r2 * 19 + 0x8080) >> 8) & 0xFF
v = ((r2 * 56 - g2 * 47 - b2 * 9 + 0x8080) >> 8) & 0xFF
uv = np.empty((H // 2, W), dtype=np.uint8)
uv[:, 0::2] = u.astype(np.uint8)
uv[:, 1::2] = v.astype(np.uint8)
return np.concatenate([y.ravel(), uv.ravel()]).astype(np.uint8)
def _send_yuv(self, yuv, frame_id, pub_type, yuv_type):
eof = int(frame_id * 0.05 * 1e9)
+2 -1
View File
@@ -5,7 +5,7 @@ import numpy as np
from abc import ABC, abstractmethod
from collections import namedtuple
W, H = 1928, 1208
W, H = 1164, 874
vec3 = namedtuple("vec3", ["x", "y", "z"])
@@ -41,6 +41,7 @@ class SimulatorState:
self.ignition = True
self.velocity: vec3 = None
self.position: tuple = (0, 0)
self.bearing: float = 0
self.gps = GPSState()
self.imu = IMUState()
+15 -3
View File
@@ -34,7 +34,11 @@ KEYBOARD_HELP = """
def getch() -> str:
STDIN_FD = sys.stdin.fileno()
old_settings = termios.tcgetattr(STDIN_FD)
try:
old_settings = termios.tcgetattr(STDIN_FD)
except (termios.error, OSError):
# No controlling terminal (e.g. headless / background run). Idle instead of crashing.
return None
try:
# set
mode = old_settings.copy()
@@ -48,8 +52,13 @@ def getch() -> str:
termios.tcsetattr(STDIN_FD, termios.TCSAFLUSH, mode)
ch = sys.stdin.read(1)
except (termios.error, OSError):
ch = None
finally:
termios.tcsetattr(STDIN_FD, termios.TCSADRAIN, old_settings)
try:
termios.tcsetattr(STDIN_FD, termios.TCSADRAIN, old_settings)
except (termios.error, OSError):
pass
return ch
def print_keyboard_help():
@@ -60,7 +69,10 @@ def keyboard_poll_thread(q: 'Queue[QueueMessage]'):
while True:
c = getch()
if c == '1':
if c is None:
# No terminal input available (headless/background); avoid busy-looping.
time.sleep(0.05)
elif c == '1':
q.put(control_cmd_gen("cruise_up"))
elif c == '2':
q.put(control_cmd_gen("cruise_down"))
+11 -1
View File
@@ -15,7 +15,7 @@ class SimulatedCar:
def __init__(self):
self.pm = messaging.PubMaster(['can', 'pandaStates'])
self.sm = messaging.SubMaster(['carControl', 'controlsState', 'carParams', 'selfdriveState'])
self.sm = messaging.SubMaster(['carControl', 'controlsState', 'carParams', 'selfdriveState', 'carState', 'starpilotPlan', 'radarState', 'modelV2'])
self.cp = self.get_car_can_parser()
self.idx = 0
self.params = Params()
@@ -76,6 +76,16 @@ class SimulatedCar:
msg.append(self.packer.make_can_msg("STEERING_CONTROL", 2, {}))
msg.append(self.packer.make_can_msg("ACC_HUD", 2, {}))
msg.append(self.packer.make_can_msg("LKAS_HUD", 2, {}))
# 0x35e CAMERA_MESSAGES: StarPilot's Honda carstate registers this message
# when it reads the speed-limit / stop-sign fields (HAS_CAMERA_MESSAGES
# flag), and can_valid requires *every* registered message to be valid.
# The real camera sends it on the cam bus, so publish it here too;
# CANPacker auto-fills the Honda COUNTER/CHECKSUM. SPEED_LIMIT_SIGN=0
# means no posted speed limit, so calculate_speed_limit() returns 0.0.
msg.append(self.packer.make_can_msg("CAMERA_MESSAGES", 2, {
"SPEED_LIMIT_SIGN": 0,
"ROAD_SIGN": 0,
}))
self.pm.send('can', can_list_to_can_capnp(msg))
+183
View File
@@ -0,0 +1,183 @@
#!/usr/bin/env bash
# StarPilot live MetaDrive simulator launcher.
#
# Runs the ENTIRE openpilot stack (manager.py -> modeld/controlsd/plannerd/...) on
# the host RTX GPU, bridged to a straight-road MetaDrive world, in one of three
# longitudinal modes.
#
# Usage:
# ./tools/sim/starpilot_sim.sh {exp|chill|cem|hem} [--headless] [--joystick] [--cpu]
#
# exp : standard Experimental mode (ConditionalExperimental=off, ConditionalChill=off, HybridExperimental=off)
# chill : pure Chill / CCM mode (ConditionalChill=on)
# cem : Conditional Experimental mode (ConditionalExperimental=on)
# hem : Hybrid Experimental mode (HybridExperimental=on)
#
# The model runs on the RTX GPU (tinygrad CUDA) by default. Pass --cpu to force
# the CPU backend instead (fragile in this fork; not recommended).
#
# By default the model is traced LIVE from driving_supercombo.onnx inside modeld
# (Option 1: no tinygrad-JIT pickling, so no JIT-unpickle corruption). Pass --pkl
# to fall back to the precompiled pickle artifact instead.
#
# The stack runs from the isolated host worktree (.host_runtime/linux/worktree),
# which is synced from this repo by `./dev sync` (run automatically below).
set -euo pipefail
# Clean up any stale openpilot/sim processes and shared-memory sockets from a
# previous run. pkill on "manager.py" alone misses the the_galaxy/galaxy Flask
# processes (different proctitles) which otherwise hold port 8083 and crash-loop,
# and stale msgq sockets in /dev/shm collide with fresh publishers.
pkill -9 -f "system/manager/manager.py" 2>/dev/null || true
pkill -9 -f "run_bridge.py" 2>/dev/null || true
pkill -9 -f "metadrive" 2>/dev/null || true
pkill -9 -f "the_galaxy" 2>/dev/null || true
pkill -9 -f "galaxy.galaxy" 2>/dev/null || true
sleep 1
rm -rf /dev/shm/msgq* /dev/shm/visionipc* /tmp/openpilot* 2>/dev/null || true
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
MODE="${1:-hem}"
ARGS=("${@:2}")
HEADLESS=0
JOYSTICK=0
SIM_DEV="CUDA"
LIVE_ONNX=1
for a in "${ARGS[@]}"; do
case "$a" in
--headless) HEADLESS=1 ;;
--joystick) JOYSTICK=1 ;;
--cpu) SIM_DEV="CPU" ;;
--pkl) LIVE_ONNX=0 ;;
*) echo "unknown arg: $a" >&2; exit 1 ;;
esac
done
case "$MODE" in
exp|chill|cem|hem) : ;;
*) echo "usage: $0 {exp|chill|cem|hem} [--headless] [--joystick] [--cpu]" >&2; exit 1 ;;
esac
# 1. Sync main repo -> host worktree so our sim/mode changes land there.
"${ROOT_DIR}/dev" sync shared >/dev/null 2>&1 || true
HOST_WORKTREE="${ROOT_DIR}/.host_runtime/linux/worktree"
HOST_PY="${HOST_WORKTREE}/.venv/bin/python3"
MODEL_STORE="${HOME}/.comma/starpilot/data/models"
echo "==> StarPilot sim mode: ${MODE} (model device: ${SIM_DEV}) (host worktree: ${HOST_WORKTREE})"
# 2. Make sure the builtin model is present where modeld loads it. Sync wipes the
# worktree copy, so re-copy it from the model store on every launch. With the
# live-ONNX path (default) modeld ignores the pkl and traces the ONNX instead,
# but keep the pkl in place so --pkl still works.
MODEL_SRC="${MODEL_STORE}/rdf43_driving_tinygrad.pkl"
MODEL_DST="${HOST_WORKTREE}/selfdrive/modeld/models/driving_tinygrad.pkl"
if [[ -f "${MODEL_SRC}" ]]; then
if ! cmp -s "${MODEL_SRC}" "${MODEL_DST}"; then
cp -f "${MODEL_SRC}" "${MODEL_DST}"
echo "==> Copied builtin model to ${MODEL_DST}"
fi
else
echo "!! builtin model not found at ${MODEL_SRC}" >&2
fi
# Option 1 (live-ONNX): modeld traces driving_supercombo.onnx in-memory on the
# RTX, so the tinygrad-JIT pickle round-trip (and its QCOM-rewrite bug) never
# happens. Falls back to the pkl artifact when --pkl is passed or the onnx is
# missing.
# Locate the source ONNX: prefer the model store copy, fall back to the repo root.
ONNX_SRC="${MODEL_STORE}/driving_supercombo.onnx"
if [[ ! -f "${ONNX_SRC}" ]] && [[ -f "${ROOT_DIR}/driving_supercombo.onnx" ]]; then
ONNX_SRC="${ROOT_DIR}/driving_supercombo.onnx"
fi
if [[ "$LIVE_ONNX" == "1" ]]; then
if [[ -f "${ONNX_SRC}" ]]; then
export STARPIOT_LIVE_ONNX="${ONNX_SRC}"
echo "==> modeld will trace ${ONNX_SRC} live on ${SIM_DEV}"
else
echo "!! driving_supercombo.onnx NOT found (checked model store and repo root)." >&2
echo " Falling back to the precompiled pkl, which CRASHES with CUDA_ERROR_INVALID_IMAGE." >&2
echo " Place the onnx at ${MODEL_STORE}/driving_supercombo.onnx and re-run." >&2
fi
fi
# 3. Set the longitudinal mode params (mutually exclusive).
# ForceOnroad: on a PC host there is no real panda/ignition, so hardwared never
# transitions the device onroad by itself; force it so modeld/controlsd/plannerd
# come up and the sim can engage and drive.
"${HOST_PY}" - "${MODE}" <<'PY'
import sys
from openpilot.common.params import Params
p = Params()
mode = sys.argv[1]
p.put_bool_nonblocking("ConditionalExperimental", mode == "cem")
p.put_bool_nonblocking("ConditionalChill", mode == "chill")
p.put_bool_nonblocking("HybridExperimental", mode == "hem")
p.put_bool_nonblocking("ForceOnroad", True)
print(f"params: ConditionalExperimental={p.get_bool('ConditionalExperimental')} "
f"ConditionalChill={p.get_bool('ConditionalChill')} "
f"HybridExperimental={p.get_bool('HybridExperimental')} "
f"ForceOnroad={p.get_bool('ForceOnroad')}")
PY
# 4. Sim environment (mirrors tools/sim/launch_openpilot.sh) + GPU selection.
export PASSIVE="0"
export NOBOARD="1"
export SIMULATION="1"
export SKIP_FW_QUERY="1"
export FINGERPRINT="HONDA_CIVIC_2022"
export BLOCK="camerad,loggerd,encoderd,micd,logmessaged,soundd,mapd"
if [[ "$HEADLESS" == "1" ]]; then
export BLOCK="${BLOCK},ui"
fi
export DEV="${SIM_DEV}"
export STARPIOT_SIM_DEV="${SIM_DEV}"
# The supercombo artifact is compiled all-CUDA (warp + policy on the RTX). The
# warp device must be CUDA so the precompiled kernels match; modeld copies the
# host-memory camera frames onto the GPU before the warp.
if [[ "${SIM_DEV}" == "CPU" ]]; then
export WARP_DEV="CPU"
export QUEUE_DEV="CPU"
else
export WARP_DEV="${SIM_DEV}"
export QUEUE_DEV="${SIM_DEV}"
fi
# 5. Launch the full openpilot stack in the background, then the bridge in the foreground.
cat <<'HELP'
==> Controls (focus the terminal that launched this script):
i : toggle ignition (starts ON by default; if the status line shows
Ignition: False, press i once to turn it back on)
2 : cruise Set (engage lateral + longitudinal) 1 : cruise Resume / accel
3 : cruise Cancel r : reset simulation
w/a/s/d : manual throttle / steer / brake q : quit everything
z/x : blinker left / right
HELP
cd "${HOST_WORKTREE}"
MANAGER_LOG="${HOST_WORKTREE}/.host_sim_manager.log"
"${HOST_PY}" -c "from openpilot.selfdrive.test.helpers import set_params_enabled; set_params_enabled()"
echo "==> Starting openpilot stack (manager.py) ..."
"${HOST_PY}" system/manager/manager.py >"${MANAGER_LOG}" 2>&1 &
MANAGER_PID=$!
echo "==> manager.py pid ${MANAGER_PID} (log: ${MANAGER_LOG})"
trap 'echo "==> stopping manager (${MANAGER_PID})"; kill "${MANAGER_PID}" 2>/dev/null || true' EXIT
# modeld blocks on the camerad visionipc stream before it starts tracing the
# ONNX, and that stream is published by the bridge. The bridge MUST come up
# promptly or modeld never loads, so keep the startup delay short and let the
# bridge auto-engage once controls report engageable.
sleep 8
# Single-camera mode: MetaDrive must render only the road viewport. Dual-camera
# renders a second wide viewpoint every frame, roughly halving frame rate. The
# driving pipeline (modeld) consumes roadCameraState only, so the wide cam adds
# no control value in sim.
BRIDGE_ARGS=()
if [[ "$JOYSTICK" == "1" ]]; then
BRIDGE_ARGS+=(--joystick)
fi
echo "==> Starting MetaDrive bridge (${BRIDGE_ARGS[*]}) ..."
"${HOST_PY}" tools/sim/run_bridge.py "${BRIDGE_ARGS[@]}" || true
echo "==> Bridge exited."