smoosh smoosh

This commit is contained in:
firestar5683
2026-05-26 20:10:16 -05:00
parent 0fec51a5d9
commit e9935624f7
17 changed files with 2406 additions and 1286 deletions
+145 -22
View File
@@ -3,6 +3,7 @@ import argparse
import codecs
import os
import pickle
import json
import re
import shutil
import subprocess
@@ -10,11 +11,18 @@ import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.starpilot.common.model_versions import uses_combined_driving_artifacts
DEFAULT_INPUT_ROOT = Path("/data/openpilot/uncompiledmodels")
DEFAULT_OUTPUT_ROOT = Path("/data/openpilot/compiledmodels")
COMPILE_SCRIPT = REPO_ROOT / "tinygrad_repo/examples/openpilot/compile3.py"
COMBINED_COMPILE_SCRIPT = REPO_ROOT / "selfdrive/modeld/compile_modeld.py"
MODEL_VERSIONS_CACHE = Path("/data/models/.model_versions.json")
DM_MODEL_KEY = "dm"
DM_MODEL_NAME = "dmonitoring_model"
DM_TARGET_ALIASES = {DM_MODEL_KEY, "dmonitoring", DM_MODEL_NAME}
@@ -26,7 +34,33 @@ COMPONENT_ALIASES = {
"driving_policy": ("driving_policy", "policy"),
"driving_vision": ("driving_vision", "vision"),
}
REQUIRED_COMPONENTS = {"driving_policy", "driving_vision"}
MEDMODEL_INPUT_SIZE = (512, 256)
DEFAULT_CAMERA_RESOLUTIONS = (
(1928, 1208),
(1344, 760),
)
def build_compile_env(*, combined: bool = False) -> dict[str, str]:
env = os.environ.copy()
existing_pythonpath = env.get("PYTHONPATH", "")
env["PYTHONPATH"] = f"{REPO_ROOT}:{existing_pythonpath}" if existing_pythonpath else str(REPO_ROOT)
numeric_defaults = {
"DEBUG": "0",
"FLOAT16": "1",
"IMAGE": "2",
"JIT_BATCH_SIZE": "0",
"NOLOCALS": "1",
}
for key, default in numeric_defaults.items():
value = env.get(key)
try:
int(str(value), 0)
except (TypeError, ValueError):
env[key] = default
return env
def parse_args() -> argparse.Namespace:
@@ -37,6 +71,7 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--dm", action="store_true", help="Compile the driver monitoring model into dmonitoring_model_tinygrad.pkl.")
parser.add_argument("--input-dir", type=Path, default=DEFAULT_INPUT_ROOT, help="Directory containing staged ONNX files. Flat root files like driving_policy.onnx are preferred.")
parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT_ROOT, help="Directory for compiled tinygrad pkls and metadata.")
parser.add_argument("--version", help="Model version. v16+ uses the combined driving_tinygrad artifact path. If omitted, split-policy staged models default to the combined build.")
parser.add_argument("--list", action="store_true", help="List detected staged models and exit.")
parser.add_argument("--force", action="store_true", help="Legacy no-op. Compiled outputs are always cleared before a build.")
@@ -66,14 +101,6 @@ def detect_component(path: Path) -> str | None:
return None
def normalize_model_files(model_files: dict[str, Path]) -> dict[str, Path]:
normalized = dict(model_files)
on_policy_path = normalized.pop("driving_on_policy", None)
if on_policy_path is not None and "driving_policy" not in normalized and "driving_off_policy" in normalized:
normalized["driving_policy"] = on_policy_path
return normalized
def find_staged_dm(input_root: Path) -> Path | None:
if not input_root.is_dir():
return None
@@ -107,7 +134,6 @@ def find_staged_models(input_root: Path) -> dict[str, dict[str, Path]]:
component = detect_component(onnx_file)
if component:
model_files[component] = onnx_file
model_files = normalize_model_files(model_files)
if model_files:
found[child.name] = model_files
@@ -137,7 +163,7 @@ def find_staged_models(input_root: Path) -> dict[str, dict[str, Path]]:
flat_root_files[component] = onnx_file
if flat_root_files:
found["_root"] = normalize_model_files(flat_root_files)
found["_root"] = flat_root_files
return found
@@ -156,7 +182,7 @@ def resolve_model_files(input_root: Path, model_key: str) -> dict[str, Path]:
component = detect_component(onnx_file)
if component:
prefixed_files[component] = onnx_file
return normalize_model_files(prefixed_files)
return prefixed_files
def get_metadata_value_by_name(model, name: str):
@@ -190,17 +216,94 @@ def write_metadata(onnx_path: Path, output_path: Path) -> None:
def compile_component(onnx_path: Path, output_path: Path) -> None:
env = os.environ.copy()
existing_pythonpath = env.get("PYTHONPATH", "")
env["PYTHONPATH"] = f"{REPO_ROOT}:{existing_pythonpath}" if existing_pythonpath else str(REPO_ROOT)
subprocess.run(
[sys.executable, str(COMPILE_SCRIPT), str(onnx_path), str(output_path)],
cwd=REPO_ROOT,
env=env,
env=build_compile_env(combined=False),
check=True,
)
def compile_combined_model(component_paths: dict[str, Path], output_path: Path) -> None:
vision_path = component_paths["driving_vision"]
off_policy_path = component_paths["driving_off_policy"]
on_policy_path = component_paths.get("driving_on_policy") or component_paths.get("driving_policy")
if on_policy_path is None:
raise ValueError("Combined compile requires driving_on_policy.onnx (or driving_policy.onnx) alongside driving_off_policy.onnx")
frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
camera_resolutions = [f"{width}x{height}" for width, height in DEFAULT_CAMERA_RESOLUTIONS]
subprocess.run(
[
sys.executable,
str(COMBINED_COMPILE_SCRIPT),
"--model-size",
f"{MEDMODEL_INPUT_SIZE[0]}x{MEDMODEL_INPUT_SIZE[1]}",
"--camera-resolutions",
*camera_resolutions,
"--vision-onnx",
str(vision_path),
"--off-policy-onnx",
str(off_policy_path),
"--on-policy-onnx",
str(on_policy_path),
"--output",
str(output_path),
"--frame-skip",
str(frame_skip),
],
cwd=REPO_ROOT,
env=build_compile_env(combined=True),
check=True,
)
def infer_model_version(model_key: str, explicit_version: str | None) -> str:
if explicit_version:
return explicit_version.strip()
if MODEL_VERSIONS_CACHE.is_file():
try:
version_map = json.loads(MODEL_VERSIONS_CACHE.read_text())
version = version_map.get(model_key)
if isinstance(version, str) and version.strip():
return version.strip()
except Exception:
pass
return ""
def should_use_combined_artifacts(model_version: str, model_files: dict[str, Path]) -> bool:
if uses_combined_driving_artifacts(model_version):
return True
if model_version.strip():
return False
has_vision = "driving_vision" in model_files
has_off_policy = "driving_off_policy" in model_files
has_on_policy = "driving_on_policy" in model_files or "driving_policy" in model_files
return has_vision and has_off_policy and has_on_policy
def resolve_split_component_inputs(model_files: dict[str, Path]) -> dict[str, Path]:
resolved: dict[str, Path] = {}
vision_path = model_files.get("driving_vision")
if vision_path is not None:
resolved["driving_vision"] = vision_path
policy_path = model_files.get("driving_policy") or model_files.get("driving_on_policy")
if policy_path is not None:
resolved["driving_policy"] = policy_path
off_policy_path = model_files.get("driving_off_policy")
if off_policy_path is not None:
resolved["driving_off_policy"] = off_policy_path
return resolved
def clear_existing_outputs(output_dir: Path) -> list[Path]:
removed = []
for existing in sorted(output_dir.iterdir()):
@@ -277,18 +380,38 @@ def main() -> int:
f"or optionally {args.input_dir / model_key}/*.onnx"
)
missing = sorted(REQUIRED_COMPONENTS - set(files))
if missing:
raise SystemExit(f"Missing required ONNX files for {model_key}: {', '.join(missing)}")
model_version = infer_model_version(model_key, args.version)
use_combined_artifacts = should_use_combined_artifacts(model_version, files)
args.output_dir.mkdir(parents=True, exist_ok=True)
print(f"Compiling {model_key} from {args.input_dir} -> {args.output_dir}")
mode_label = "combined" if use_combined_artifacts else "split"
version_label = model_version or ("auto-combined" if use_combined_artifacts else "legacy-default")
print(f"Compiling {model_key} ({version_label}, {mode_label}) from {args.input_dir} -> {args.output_dir}")
removed = clear_existing_outputs(args.output_dir)
if removed:
print(f" cleared {len(removed)} existing output entries")
for component, onnx_path in sorted(files.items()):
if use_combined_artifacts:
required_components = {"driving_vision", "driving_off_policy"}
if not (files.get("driving_on_policy") or files.get("driving_policy")):
required_components.add("driving_on_policy")
missing = sorted(component for component in required_components if component not in files)
if missing:
raise SystemExit(f"Missing required ONNX files for combined compile of {model_key}: {', '.join(missing)}")
output_pkl = args.output_dir / f"{model_key}_driving_tinygrad.pkl"
compile_combined_model(files, output_pkl)
print(f" saved {output_pkl.name}")
print("Done.")
return 0
split_components = resolve_split_component_inputs(files)
missing = sorted(component for component in ("driving_policy", "driving_vision") if component not in split_components)
if missing:
raise SystemExit(f"Missing required ONNX files for {model_key}: {', '.join(missing)}")
for component, onnx_path in sorted(split_components.items()):
output_pkl = args.output_dir / f"{model_key}_{component}_tinygrad.pkl"
output_metadata = args.output_dir / f"{model_key}_{component}_metadata.pkl"
@@ -8,6 +8,7 @@ from openpilot.common.constants import CV
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.starpilot.common.model_versions import is_tinygrad_model_version
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import desired_follow_distance
@@ -434,7 +435,7 @@ class LongitudinalPlanner:
@property
def mlsim(self):
return self.generation in ("v8", "v10", "v11", "v12", "v13", "v14", "v15")
return is_tinygrad_model_version(self.generation)
def get_mpc_mode(self) -> str:
if not self.mlsim:
+358
View File
@@ -0,0 +1,358 @@
#!/usr/bin/env python3
import argparse
import atexit
import os
import pickle
import time
from collections import defaultdict, namedtuple
from functools import partial
import numpy as np
def _patch_tinygrad_fetch_fw():
import hashlib
import pathlib
import zstandard
from tinygrad import helpers
original_fetch_fw = getattr(helpers, "fetch_fw", None)
if original_fetch_fw is None:
return
def fetch_fw(path, name, sha256):
firmware_path = pathlib.Path(f"/lib/firmware/{path}/{name}.zst")
if firmware_path.is_file():
blob = zstandard.ZstdDecompressor().stream_reader(firmware_path.read_bytes()).read()
if hashlib.sha256(blob).hexdigest() == sha256:
return blob
return original_fetch_fw(path, name, sha256)
helpers.fetch_fw = fetch_fw
_patch_tinygrad_fetch_fw()
from tinygrad.device import Device
from tinygrad.engine.jit import TinyJit
from tinygrad.helpers import Context
from tinygrad.tensor import Tensor
NV12Frame = namedtuple("NV12Frame", ["width", "height", "stride", "y_height", "uv_height", "size"])
WARP_INPUTS = ["img_q", "big_img_q", "tfm", "big_tfm"]
POLICY_INPUTS = ["feat_q", "desire_q", "desire", "traffic_convention", "action_t"]
WARP_DEV = os.getenv("WARP_DEV")
def make_random_images(keys, shape, device=None):
return {key: Tensor.randint(shape, low=0, high=256, dtype="uint8", device=device).realize() for key in keys}
def warp_perspective_tinygrad(src_flat, matrix_inverse, dst_shape, src_shape, stride_pad, border_fill_val=None):
width_dst, height_dst = dst_shape
height_src, width_src = src_shape
x = Tensor.arange(width_dst, device=WARP_DEV).reshape(1, width_dst).expand(height_dst, width_dst).reshape(-1)
y = Tensor.arange(height_dst, device=WARP_DEV).reshape(height_dst, 1).expand(height_dst, width_dst).reshape(-1)
# Inline 3x3 matmul as elementwise to avoid reduce ops and enable fusion with gather.
src_x = matrix_inverse[0, 0] * x + matrix_inverse[0, 1] * y + matrix_inverse[0, 2]
src_y = matrix_inverse[1, 0] * x + matrix_inverse[1, 1] * y + matrix_inverse[1, 2]
src_w = matrix_inverse[2, 0] * x + matrix_inverse[2, 1] * y + matrix_inverse[2, 2]
src_x = src_x / src_w
src_y = src_y / src_w
x_round = Tensor.round(src_x)
y_round = Tensor.round(src_y)
x_nn_clipped = x_round.clip(0, width_src - 1).cast("int")
y_nn_clipped = y_round.clip(0, height_src - 1).cast("int")
idx = y_nn_clipped * (width_src + stride_pad) + x_nn_clipped
sampled = src_flat[idx]
if border_fill_val is None:
return sampled
in_bounds = ((x_round >= 0) & (x_round <= width_src - 1) &
(y_round >= 0) & (y_round <= height_src - 1)).cast(sampled.dtype)
return sampled * in_bounds + Tensor(border_fill_val, dtype=sampled.dtype) * (1 - in_bounds)
def frames_to_tensor(frames):
height = (frames.shape[0] * 2) // 3
width = frames.shape[1]
return Tensor.cat(
frames[0:height:2, 0::2],
frames[1:height:2, 0::2],
frames[0:height:2, 1::2],
frames[1:height:2, 1::2],
frames[height:height + height // 4].reshape((height // 2, width // 2)),
frames[height + height // 4:height + height // 2].reshape((height // 2, width // 2)),
dim=0,
).reshape((6, height // 2, width // 2))
def make_frame_prepare(nv12: NV12Frame, model_w, model_h):
cam_w, cam_h, stride, y_height, uv_height, _ = nv12
uv_offset = stride * y_height
stride_pad = stride - cam_w
def frame_prepare_tinygrad(input_frame, matrix_inverse):
# UV_SCALE @ M_inv @ UV_SCALE_INV simplifies to elementwise scaling.
matrix_inverse_uv = matrix_inverse * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]], device=WARP_DEV)
# Deinterleave NV12 UV plane (UVUV... -> separate U, V).
uv = input_frame[uv_offset:uv_offset + uv_height * stride].reshape(uv_height, stride)
with Context(SPLIT_REDUCEOP=0):
y = warp_perspective_tinygrad(
input_frame[:cam_h * stride],
matrix_inverse,
(model_w, model_h),
(cam_h, cam_w),
stride_pad,
).realize()
u = warp_perspective_tinygrad(
uv[:cam_h // 2, :cam_w:2].flatten(),
matrix_inverse_uv,
(model_w // 2, model_h // 2),
(cam_h // 2, cam_w // 2),
0,
).realize()
v = warp_perspective_tinygrad(
uv[:cam_h // 2, 1:cam_w:2].flatten(),
matrix_inverse_uv,
(model_w // 2, model_h // 2),
(cam_h // 2, cam_w // 2),
0,
).realize()
yuv = y.cat(u).cat(v).reshape((model_h * 3 // 2, model_w))
return frames_to_tensor(yuv)
return frame_prepare_tinygrad
def make_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, device):
img = vision_input_shapes["img"]
n_frames = img[1] // 6
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3])
features_buffer = policy_input_shapes["features_buffer"]
desire_pulse = policy_input_shapes["desire_pulse"]
traffic_convention = policy_input_shapes["traffic_convention"]
npy = {
"desire": np.zeros(desire_pulse[2], dtype=np.float32),
"traffic_convention": np.zeros(traffic_convention, dtype=np.float32),
"tfm": np.zeros((3, 3), dtype=np.float32),
"big_tfm": np.zeros((3, 3), dtype=np.float32),
}
if "action_t" in policy_input_shapes:
npy["action_t"] = np.zeros(policy_input_shapes["action_t"], dtype=np.float32)
input_queues = {
"img_q": Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
"big_img_q": Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
"feat_q": Tensor(
np.zeros((frame_skip * (features_buffer[1] - 1) + 1, features_buffer[0], features_buffer[2]), dtype=np.float32),
device=device,
).contiguous().realize(),
"desire_q": Tensor(
np.zeros((frame_skip * desire_pulse[1], desire_pulse[0], desire_pulse[2]), dtype=np.float32),
device=device,
).contiguous().realize(),
**{key: Tensor(value, device="NPY").realize() for key, value in npy.items()},
}
return input_queues, npy
def shift_and_sample(buf, new_val, sample_fn):
buf.assign(buf[1:].cat(new_val, dim=0).contiguous())
return sample_fn(buf)
def sample_skip(buf, frame_skip):
return buf[::frame_skip].contiguous().flatten(0, 1).unsqueeze(0)
def sample_desire(buf, frame_skip):
return buf.reshape(-1, frame_skip, *buf.shape[1:]).max(1).flatten(0, 1).unsqueeze(0)
def make_warp(nv12, model_w, model_h, frame_skip):
frame_prepare = make_frame_prepare(nv12, model_w, model_h)
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip)
def warp_enqueue(img_q, big_img_q, tfm, big_tfm, frame, big_frame):
tfm = tfm.to(WARP_DEV)
big_tfm = big_tfm.to(WARP_DEV)
Tensor.realize(tfm, big_tfm)
warped_frame = frame_prepare(frame, tfm).unsqueeze(0).to(Device.DEFAULT)
warped_big_frame = frame_prepare(big_frame, big_tfm).unsqueeze(0).to(Device.DEFAULT)
img = shift_and_sample(img_q, warped_frame, sample_skip_fn)
big_img = shift_and_sample(big_img_q, warped_big_frame, sample_skip_fn)
return img, big_img
return warp_enqueue
def make_run_policy(vision_runner, off_policy_runner, on_policy_runner, vision_features_slice, frame_skip):
sample_desire_fn = partial(sample_desire, frame_skip=frame_skip)
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip)
def run_policy(img, big_img, feat_q, desire_q, desire, traffic_convention, action_t):
desire = desire.to(Device.DEFAULT)
traffic_convention = traffic_convention.to(Device.DEFAULT)
action_t = action_t.to(Device.DEFAULT)
Tensor.realize(desire, traffic_convention, action_t)
desire_buf = shift_and_sample(desire_q, desire.reshape(1, 1, -1), sample_desire_fn)
vision_out = next(iter(vision_runner({"img": img, "big_img": big_img}).values())).cast("float32")
new_feat = vision_out[:, vision_features_slice].reshape(1, -1).unsqueeze(0)
feat_buf = shift_and_sample(feat_q, new_feat, sample_skip_fn)
inputs = {
"features_buffer": feat_buf,
"desire_pulse": desire_buf,
"traffic_convention": traffic_convention,
"action_t": action_t,
}
on_policy_out = next(iter(on_policy_runner(inputs).values())).cast("float32")
off_policy_out = next(iter(off_policy_runner(inputs).values())).cast("float32")
return vision_out, on_policy_out, off_policy_out
return run_policy
def compile_jit(jit, make_random_inputs, input_keys, frame_skip, vision_metadata, policy_metadata):
vision_input_shapes = vision_metadata["input_shapes"]
policy_input_shapes = policy_metadata["input_shapes"]
seed = 42
validation_rtol = 5e-3 if Device.DEFAULT == "QCOM" else 0.0
validation_atol = 5e-3 if Device.DEFAULT == "QCOM" else 0.0
def arrays_match(lhs, rhs):
if lhs.shape != rhs.shape:
return False
if np.issubdtype(lhs.dtype, np.floating) or np.issubdtype(rhs.dtype, np.floating):
return np.allclose(lhs, rhs, rtol=validation_rtol, atol=validation_atol, equal_nan=True)
return np.array_equal(lhs, rhs)
def random_inputs_run(fn, current_seed, test_val=None, test_buffers=None, expect_match=True):
input_queues, npy = make_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, Device.DEFAULT)
np.random.seed(current_seed)
Tensor.manual_seed(current_seed)
testing = test_val is not None or test_buffers is not None
n_runs = 1 if testing else 3
for idx in range(n_runs):
for value in npy.values():
value[:] = np.random.randn(*value.shape).astype(value.dtype)
Device.default.synchronize()
random_inputs = make_random_inputs()
start = time.perf_counter()
outs = fn(**{key: input_queues[key] for key in input_keys}, **random_inputs)
mid = time.perf_counter()
Device.default.synchronize()
end = time.perf_counter()
print(f" [{idx + 1}/{n_runs}] enqueue {(mid - start) * 1e3:6.2f} ms -- total {(end - start) * 1e3:6.2f} ms")
if idx == 0:
val = [np.copy(value.numpy()) for value in outs]
buffers = [np.copy(value.numpy().copy()) for value in input_queues.values()]
if Device.DEFAULT != "QCOM":
if test_val is not None:
match = all(arrays_match(lhs, rhs) for lhs, rhs in zip(val, test_val, strict=True))
assert match == expect_match, f"outputs {'differ from' if expect_match else 'match'} baseline (seed={current_seed})"
if test_buffers is not None:
match = all(arrays_match(lhs, rhs) for lhs, rhs in zip(buffers, test_buffers, strict=True))
assert match == expect_match, f"buffers {'differ from' if expect_match else 'match'} baseline (seed={current_seed})"
return val, buffers
print("capture + replay")
test_val, test_buffers = random_inputs_run(jit, seed)
print("pickle round trip")
jit = pickle.loads(pickle.dumps(jit))
random_inputs_run(jit, seed, test_val, test_buffers, expect_match=True)
random_inputs_run(jit, seed + 1, test_val, test_buffers, expect_match=False)
return jit
def _parse_size(size):
width, height = size.lower().split("x")
return int(width), int(height)
def read_file_chunked_to_shm(path):
from openpilot.common.file_chunker import read_file_chunked
from openpilot.system.hardware.hw import Paths
shm_path = os.path.join(Paths.shm_path(), os.path.basename(path))
atexit.register(lambda: os.path.exists(shm_path) and os.remove(shm_path))
with open(shm_path, "wb") as f:
f.write(read_file_chunked(path))
return shm_path
if __name__ == "__main__":
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
parser = argparse.ArgumentParser()
parser.add_argument("--model-size", type=_parse_size, required=True, help="model input WxH")
parser.add_argument("--camera-resolutions", type=_parse_size, nargs="+", required=True, help="camera resolutions WxH (one or more)")
parser.add_argument("--vision-onnx", required=True)
parser.add_argument("--off-policy-onnx", required=True)
parser.add_argument("--on-policy-onnx", required=True)
parser.add_argument("--output", required=True)
parser.add_argument("--frame-skip", type=int, required=True)
args = parser.parse_args()
out = defaultdict(dict)
vision_path = read_file_chunked_to_shm(args.vision_onnx)
off_policy_path = read_file_chunked_to_shm(args.off_policy_onnx)
on_policy_path = read_file_chunked_to_shm(args.on_policy_onnx)
model_w, model_h = args.model_size
vision_runner = OnnxRunner(vision_path)
off_policy_runner = OnnxRunner(off_policy_path)
on_policy_runner = OnnxRunner(on_policy_path)
vision_metadata = make_metadata_dict(vision_path)
off_policy_metadata = make_metadata_dict(off_policy_path)
on_policy_metadata = make_metadata_dict(on_policy_path)
assert off_policy_metadata["input_shapes"] == on_policy_metadata["input_shapes"]
run_policy_jit = TinyJit(
make_run_policy(
vision_runner,
off_policy_runner,
on_policy_runner,
vision_metadata["output_slices"]["hidden_state"],
args.frame_skip,
),
prune=True,
)
out["metadata"]["vision"] = vision_metadata
out["metadata"]["off_policy"] = off_policy_metadata
out["metadata"]["on_policy"] = on_policy_metadata
make_random_model_inputs = partial(make_random_images, keys=["img", "big_img"], shape=vision_metadata["input_shapes"]["img"])
out["run_policy"] = compile_jit(run_policy_jit, make_random_model_inputs, POLICY_INPUTS, args.frame_skip, vision_metadata, on_policy_metadata)
for cam_w, cam_h in args.camera_resolutions:
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
make_random_warp_inputs = partial(make_random_images, keys=["frame", "big_frame"], shape=nv12.size, device=WARP_DEV)
warp_enqueue = TinyJit(make_warp(nv12, model_w, model_h, args.frame_skip), prune=True)
out[(cam_w, cam_h)] = compile_jit(warp_enqueue, make_random_warp_inputs, WARP_INPUTS, args.frame_skip, vision_metadata, on_policy_metadata)
with open(args.output, "wb") as f:
pickle.dump(out, f)
print(f"Saved JITs to {args.output} ({os.path.getsize(args.output) / 1e6:.2f} MB)")
+12 -7
View File
@@ -1,11 +1,12 @@
#!/usr/bin/env python3
import sys
import pathlib
import onnx
import codecs
import pickle
import pathlib
import sys
from typing import Any
import onnx
def get_name_and_shape(value_info:onnx.ValueInfoProto) -> tuple[str, tuple[int,...]]:
shape = tuple([int(dim.dim_value) for dim in value_info.type.tensor_type.shape.dim])
name = value_info.name
@@ -17,19 +18,23 @@ def get_metadata_value_by_name(model:onnx.ModelProto, name:str) -> str | Any:
return prop.value
return None
if __name__ == "__main__":
model_path = pathlib.Path(sys.argv[1])
def make_metadata_dict(model_path: str | pathlib.Path) -> dict[str, Any]:
model = onnx.load(str(model_path))
output_slices = get_metadata_value_by_name(model, 'output_slices')
assert output_slices is not None, 'output_slices not found in metadata'
metadata = {
return {
'model_checkpoint': get_metadata_value_by_name(model, 'model_checkpoint'),
'output_slices': pickle.loads(codecs.decode(output_slices.encode(), "base64")),
'input_shapes': dict([get_name_and_shape(x) for x in model.graph.input]),
'output_shapes': dict([get_name_and_shape(x) for x in model.graph.output])
'output_shapes': dict([get_name_and_shape(x) for x in model.graph.output]),
}
if __name__ == "__main__":
model_path = pathlib.Path(sys.argv[1])
metadata = make_metadata_dict(model_path)
metadata_path = model_path.parent / (model_path.stem + '_metadata.pkl')
with open(metadata_path, 'wb') as f:
pickle.dump(metadata, f)
+17 -7
View File
@@ -28,6 +28,11 @@ from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_pose_
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
from openpilot.selfdrive.modeld.models.commonmodel_pyx import DrivingModelFrame, CLContext
from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from_opencl_address
from openpilot.starpilot.common.model_versions import (
is_tinygrad_model_version,
uses_combined_driving_artifacts,
uses_split_off_policy_artifacts,
)
from openpilot.starpilot.common.starpilot_variables import get_starpilot_toggles, MODELS_PATH, params_memory
@@ -309,7 +314,7 @@ class ModelState:
self.is_v14 = (self.policy_generation == "v14")
self.is_v15 = (self.policy_generation == "v15")
self.is_v9 = (self.policy_generation == "v9")
self.mlsim = (self.policy_generation in ("v8", "v10", "v11", "v12", "v13", "v14", "v15"))
self.mlsim = is_tinygrad_model_version(self.policy_generation)
self.policy_has_plan = 'plan' in self.policy_output_slices
self.frames = {name: DrivingModelFrame(context, ModelConstants.TEMPORAL_SKIP) for name in self.vision_input_names}
@@ -334,7 +339,7 @@ class ModelState:
self.off_policy_output: np.ndarray | None = None
off_policy_metadata = None
if self.policy_generation in ("v12", "v13", "v14", "v15") or OFF_POLICY_METADATA_PATH.is_file() or OFF_POLICY_PKL_PATH.is_file():
if uses_split_off_policy_artifacts(self.policy_generation) or OFF_POLICY_METADATA_PATH.is_file() or OFF_POLICY_PKL_PATH.is_file():
resolved_off_policy_meta = ensure_artifact(OFF_POLICY_METADATA_PATH, "driving_off_policy_metadata.pkl", optional=True)
if resolved_off_policy_meta is not None:
with open(resolved_off_policy_meta, 'rb') as f:
@@ -453,14 +458,14 @@ class ModelState:
self.full_prev_desired_curv[0,-1,:] = policy_outputs_dict['desired_curvature'][0, :]
if self.prev_desired_curv_key is not None:
# v9/v10/v11/v12/v13/v14/v15 models expect zeros for prev_desired_curv(s); others use history
if self.is_v9 or self.is_v10 or self.is_v11 or self.is_v12 or self.is_v13 or self.is_v14 or self.is_v15:
# Tinygrad-era policy models expect zeros for prev_desired_curv(s); older ones use history.
if is_tinygrad_model_version(self.policy_generation):
self.numpy_inputs[self.prev_desired_curv_key][:] = 0 * self.full_prev_desired_curv[0, self.temporal_idxs]
else:
self.numpy_inputs[self.prev_desired_curv_key][:] = self.full_prev_desired_curv[0, self.temporal_idxs]
if self.off_policy_enabled and self.off_policy_prev_desired_curv_key is not None:
if self.is_v9 or self.is_v12 or self.is_v13 or self.is_v14 or self.is_v15:
if self.is_v9 or uses_split_off_policy_artifacts(self.policy_generation) or uses_combined_driving_artifacts(self.policy_generation):
self.off_policy_numpy_inputs[self.off_policy_prev_desired_curv_key][:] = 0 * self.full_prev_desired_curv[0, self.temporal_idxs]
else:
self.off_policy_numpy_inputs[self.off_policy_prev_desired_curv_key][:] = self.full_prev_desired_curv[0, self.temporal_idxs]
@@ -486,6 +491,13 @@ class ModelState:
def main(demo=False):
params = Params()
selected_version = _resolve_mirrored_param(params, "ModelVersion", "DrivingModelVersion")
if uses_combined_driving_artifacts(selected_version):
from openpilot.selfdrive.modeld.modeld_v16 import main as combined_main
return combined_main(demo=demo)
cloudlog.warning("modeld init")
sentry.set_tag("daemon", PROCESS_NAME)
@@ -527,8 +539,6 @@ def main(demo=False):
sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay", "starpilotPlan"])
publish_state = PublishState()
params = Params()
# setup filter to track dropped frames
frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_FREQ)
frame_id = 0
+447
View File
@@ -0,0 +1,447 @@
#!/usr/bin/env python3
from __future__ import annotations
import os
import pickle
import time
from pathlib import Path
from openpilot.system.hardware import TICI
os.environ["DEV"] = "QCOM" if TICI else "LLVM"
import cereal.messaging as messaging
import numpy as np
from cereal import car, log
from msgq.visionipc import VisionBuf, VisionIpcClient, VisionStreamType
from opendbc.car.car_helpers import get_demo_car_params
from setproctitle import setproctitle
from tinygrad.device import Device
from tinygrad.tensor import Tensor
from openpilot.common.file_chunker import read_file_chunked
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.params import Params
from openpilot.common.realtime import DT_MDL, config_realtime_process
from openpilot.common.swaglog import cloudlog
from openpilot.common.transformations.camera import DEVICE_CAMERAS
from openpilot.common.transformations.model import get_warp_matrix
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
from openpilot.selfdrive.controls.lib.drive_helpers import smooth_value
from openpilot.selfdrive.modeld.compile_modeld import POLICY_INPUTS, WARP_INPUTS, make_input_queues
from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.selfdrive.modeld.fill_model_msg import PublishState, fill_model_msg, fill_pose_msg
from openpilot.selfdrive.modeld.parse_model_outputs import Parser
from openpilot.starpilot.assets.model_manager import ModelManager
from openpilot.starpilot.common.model_versions import uses_combined_driving_artifacts
from openpilot.starpilot.common.starpilot_variables import MODELS_PATH, get_starpilot_toggles, params_memory
from openpilot.system import sentry
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
PROCESS_NAME = "selfdrive.modeld.modeld"
SEND_RAW_PRED = os.getenv("SEND_RAW_PRED")
BUILTIN_MODEL_KEY = "sc2"
BUILTIN_MODEL_ALIASES = {BUILTIN_MODEL_KEY, "sc"}
LAT_SMOOTH_SECONDS = 0.0
LONG_SMOOTH_SECONDS = 0.3
MIN_LAT_CONTROL_SPEED = 0.3
def _get_param_str(params: Params, key: str, default: str = "") -> str:
try:
value = params.get(key)
except Exception:
return default
if value is None:
return default
if isinstance(value, bytes):
try:
return value.decode("utf-8")
except Exception:
return default
if isinstance(value, (dict, list)):
return default
return str(value)
def _get_default_param_str(params: Params, key: str) -> str:
try:
value = params.get_default_value(key)
except Exception:
return ""
if value is None:
return ""
if isinstance(value, bytes):
try:
return value.decode("utf-8")
except Exception:
return ""
return str(value)
def _resolve_mirrored_param(params: Params, primary_key: str, secondary_key: str) -> str:
primary_val = _get_param_str(params, primary_key).strip()
secondary_val = _get_param_str(params, secondary_key).strip()
if primary_val == secondary_val:
return secondary_val or primary_val
primary_default = _get_default_param_str(params, primary_key).strip()
secondary_default = _get_default_param_str(params, secondary_key).strip()
primary_non_default = bool(primary_val) and primary_val != primary_default
secondary_non_default = bool(secondary_val) and secondary_val != secondary_default
if secondary_non_default:
return secondary_val
if primary_non_default:
return primary_val
return secondary_val or primary_val
def _canonical_model_id(model_id: str) -> str:
key = (model_id or "").strip().lower()
return BUILTIN_MODEL_KEY if key in BUILTIN_MODEL_ALIASES else key
def _combined_model_path(model_id: str, use_builtin_model: bool) -> Path:
if use_builtin_model:
return Path(__file__).parent / "models" / "driving_tinygrad.pkl"
return MODELS_PATH / f"{model_id}_driving_tinygrad.pkl"
def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action, v_ego: float) -> log.ModelDataV2.Action:
desired_curv_unscaled, desired_accel = model_output["action"][0]
desired_curvature = float(desired_curv_unscaled) / max(1.0, v_ego) ** 2
should_stop = (v_ego < 0.3 and desired_accel < 0.1)
desired_accel = smooth_value(float(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(should_stop),
)
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
class ModelState:
prev_desire: np.ndarray
def __init__(self, cam_w: int, cam_h: int):
params = Params()
model_id_raw = _resolve_mirrored_param(params, "Model", "DrivingModel") or BUILTIN_MODEL_KEY
self.model_id = _canonical_model_id(model_id_raw)
self.model_version = _resolve_mirrored_param(params, "ModelVersion", "DrivingModelVersion")
if not uses_combined_driving_artifacts(self.model_version):
raise ValueError(f"Combined runtime requested for non-combined version {self.model_version!r}")
use_builtin_model = self.model_id == BUILTIN_MODEL_KEY
model_path = _combined_model_path(self.model_id, use_builtin_model)
if not model_path.is_file():
if use_builtin_model:
raise FileNotFoundError(
f"Missing builtin combined model artifact: {model_path}. "
"Rebuild/deploy the combined builtin model before selecting this version."
)
cloudlog.error(f"Missing combined model artifact {model_path}, downloading {self.model_id}...")
ModelManager(params, params_memory).download_model(self.model_id)
if not model_path.is_file():
raise FileNotFoundError(model_path)
jits = pickle.loads(read_file_chunked(model_path))
vision_metadata = jits["metadata"]["vision"]
off_policy_metadata = jits["metadata"]["off_policy"]
on_policy_metadata = jits["metadata"]["on_policy"]
self.vision_input_shapes = vision_metadata["input_shapes"]
self.vision_input_names = list(self.vision_input_shapes.keys())
self.vision_output_slices = vision_metadata["output_slices"]
self.off_policy_output_slices = off_policy_metadata["output_slices"]
self.policy_input_shapes = on_policy_metadata["input_shapes"]
self.policy_output_slices = on_policy_metadata["output_slices"]
self.desire_key = next(key for key in self.policy_input_shapes if key.startswith("desire"))
self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
self.dev = Device.DEFAULT
self.input_queues, self.npy = make_input_queues(self.vision_input_shapes, self.policy_input_shapes, self.frame_skip, device=self.dev)
self.full_frames: dict[str, Tensor] = {}
self._blob_cache: dict[tuple[str, int], Tensor] = {}
self.parser = Parser()
self.frame_buf_params = {key: get_nv12_info(cam_w, cam_h) for key in ("img", "big_img")}
self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
camera_jit = jits[(cam_w, cam_h)]
self.split_warp_layout = "run_policy" in jits and not isinstance(camera_jit, dict)
if self.split_warp_layout:
self.run_policy = jits["run_policy"]
self.warp_enqueue = camera_jit
else:
self.run_policy = camera_jit["run_policy"]
self.warp_enqueue = camera_jit["warp_enqueue"]
def slice_outputs(self, model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]:
return {key: model_outputs[np.newaxis, value] for key, value in output_slices.items()}
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray], inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None:
for key in bufs.keys():
ptr = np.frombuffer(bufs[key].data, dtype=np.uint8).ctypes.data
yuv_size = self.frame_buf_params[key][3]
cache_key = (key, ptr)
if cache_key not in self._blob_cache:
self._blob_cache[cache_key] = Tensor.from_blob(ptr, (yuv_size,), dtype="uint8", device=self.dev)
self.full_frames[key] = self._blob_cache[cache_key]
inputs[self.desire_key][0] = 0
self.npy["desire"][:] = np.where(inputs[self.desire_key] - self.prev_desire > 0.99, inputs[self.desire_key], 0)
self.prev_desire[:] = inputs[self.desire_key]
self.npy["traffic_convention"][:] = inputs["traffic_convention"]
if "action_t" in self.npy:
self.npy["action_t"][:] = inputs["action_t"]
self.npy["tfm"][:, :] = transforms["img"][:, :]
self.npy["big_tfm"][:, :] = transforms["big_img"][:, :]
if self.split_warp_layout:
img, big_img = self.warp_enqueue(
**{key: self.input_queues[key] for key in WARP_INPUTS},
frame=self.full_frames["img"],
big_frame=self.full_frames["big_img"],
)
if prepare_only:
return None
policy_inputs = {key: self.input_queues[key] for key in POLICY_INPUTS if key in self.input_queues}
vision_output, policy_output, off_policy_output = self.run_policy(**policy_inputs, img=img, big_img=big_img)
else:
if prepare_only:
self.warp_enqueue(**self.input_queues, frame=self.full_frames["img"], big_frame=self.full_frames["big_img"])
return None
vision_output, policy_output, off_policy_output = self.run_policy(
**self.input_queues,
frame=self.full_frames["img"],
big_frame=self.full_frames["big_img"],
)
vision_output = vision_output.numpy().flatten()
policy_output = policy_output.numpy().flatten()
off_policy_output = off_policy_output.numpy().flatten()
vision_outputs_dict = self.parser.parse_vision_outputs(self.slice_outputs(vision_output, self.vision_output_slices))
off_policy_outputs_dict = self.parser.parse_policy_outputs(self.slice_outputs(off_policy_output, self.off_policy_output_slices))
policy_outputs_dict = self.parser.parse_policy_outputs(self.slice_outputs(policy_output, self.policy_output_slices))
combined_outputs_dict = {**vision_outputs_dict, **off_policy_outputs_dict, **policy_outputs_dict}
if SEND_RAW_PRED:
combined_outputs_dict["raw_pred"] = np.concatenate([vision_output.copy(), policy_output.copy(), off_policy_output.copy()])
return combined_outputs_dict
def main(demo=False):
cloudlog.warning("modeld init")
sentry.set_tag("daemon", PROCESS_NAME)
cloudlog.bind(daemon=PROCESS_NAME)
setproctitle(PROCESS_NAME)
config_realtime_process(7, 54)
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_ROAD in available_streams
main_wide_camera = VisionStreamType.VISION_STREAM_ROAD not in available_streams
break
time.sleep(0.1)
vipc_client_main_stream = VisionStreamType.VISION_STREAM_WIDE_ROAD if main_wide_camera else VisionStreamType.VISION_STREAM_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})")
start_time = time.monotonic()
cloudlog.warning("loading combined model")
model = ModelState(vipc_client_main.width, vipc_client_main.height)
cloudlog.warning(f"combined model loaded in {time.monotonic() - start_time:.1f}s, modeld starting")
pm = messaging.PubMaster(["modelV2", "drivingModelData", "cameraOdometry", "starpilotModelV2"])
sm = messaging.SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay", "starpilotPlan"])
publish_state = PublishState()
params = Params()
frame_dropped_filter = FirstOrderFilter(0.0, 10.0, 1.0 / ModelConstants.MODEL_RUN_FREQ)
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)
live_calib_seen = False
buf_main, buf_extra = None, None
meta_main = FrameMeta()
meta_extra = FrameMeta()
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)
long_delay = CP.longitudinalActuatorDelay + LONG_SMOOTH_SECONDS
prev_action = log.ModelDataV2.Action()
desire_helper = DesireHelper()
starpilot_toggles = get_starpilot_toggles(sm)
while True:
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:
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}), "
f"extra: {meta_extra.frame_id} ({meta_extra.timestamp_sof / 1e9:.5f})"
)
else:
buf_extra = buf_main
meta_extra = meta_main
sm.update(0)
desire = desire_helper.desire
is_rhd = sm["driverMonitoringState"].isRHD
frame_id = sm["roadCameraState"].frameId
v_ego = max(sm["carState"].vEgo, 0.0)
lat_delay = sm["liveDelay"].lateralDelay + LAT_SMOOTH_SECONDS
if sm.updated["liveCalibration"] and sm.seen["roadCameraState"] and sm.seen["deviceState"]:
device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32)
dc = DEVICE_CAMERAS[(str(sm["deviceState"].deviceType), str(sm["roadCameraState"].sensor))]
model_transform_main = get_warp_matrix(
device_from_calib_euler,
dc.ecam.intrinsics if main_wide_camera else dc.fcam.intrinsics,
False,
).astype(np.float32)
model_transform_extra = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics, True).astype(np.float32)
live_calib_seen = True
traffic_convention = np.zeros(2, dtype=np.float32)
traffic_convention[int(is_rhd)] = 1
vec_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
if 0 <= desire < ModelConstants.DESIRE_LEN:
vec_desire[desire] = 1
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:
frame_dropped_filter.x = 0.0
frames_dropped = 0.0
run_count += 1
frame_drop_ratio = frames_dropped / (1 + frames_dropped)
prepare_only = vipc_dropped_frames > 0
if prepare_only:
cloudlog.error(f"skipping model eval. Dropped {vipc_dropped_frames} frames")
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
action_delay = DT_MDL / 2
lat_action_t = lat_delay + frame_delay + action_delay
long_action_t = long_delay + frame_delay + action_delay
inputs: dict[str, np.ndarray] = {
model.desire_key: vec_desire,
"traffic_convention": traffic_convention,
}
if "action_t" in model.npy:
inputs["action_t"] = np.array([lat_action_t, long_action_t], dtype=np.float32)
start = time.perf_counter()
model_output = model.run(bufs, transforms, inputs, prepare_only)
end = time.perf_counter()
model_execution_time = end - start
if model_output is not None:
modelv2_send = messaging.new_message("modelV2")
starpilot_modelv2_send = messaging.new_message("starpilotModelV2")
drivingdata_send = messaging.new_message("drivingModelData")
posenet_send = messaging.new_message("cameraOdometry")
action = get_action_from_model(model_output, prev_action, v_ego)
prev_action = action
fill_model_msg(
drivingdata_send,
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,
live_calib_seen,
)
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
desire_helper.update(sm["carState"], sm["carControl"].latActive, lane_change_prob, sm["starpilotPlan"], starpilot_toggles)
modelv2_send.modelV2.meta.laneChangeState = desire_helper.lane_change_state
modelv2_send.modelV2.meta.laneChangeDirection = desire_helper.lane_change_direction
starpilot_modelv2_send.starpilotModelV2.turnDirection = desire_helper.turn_direction
drivingdata_send.drivingModelData.meta.laneChangeState = desire_helper.lane_change_state
drivingdata_send.drivingModelData.meta.laneChangeDirection = desire_helper.lane_change_direction
fill_pose_msg(posenet_send, model_output, meta_main.frame_id, vipc_dropped_frames, meta_main.timestamp_eof, live_calib_seen)
pm.send("modelV2", modelv2_send)
pm.send("starpilotModelV2", starpilot_modelv2_send)
pm.send("drivingModelData", drivingdata_send)
pm.send("cameraOdometry", posenet_send)
last_vipc_frame_id = meta_main.frame_id
if sm.updated["starpilotPlan"]:
starpilot_toggles = get_starpilot_toggles(sm)
@@ -17,11 +17,15 @@ from openpilot.starpilot.assets.model_manager import (
MODEL_DOWNLOAD_ALL_PARAM,
MODEL_DOWNLOAD_PARAM,
ModelManager,
TINYGRAD_VERSIONS,
canonical_model_key,
is_builtin_model_key,
model_key_aliases,
)
from openpilot.starpilot.common.model_versions import (
is_tinygrad_model_version,
uses_combined_driving_artifacts,
uses_split_off_policy_artifacts,
)
from openpilot.starpilot.common.starpilot_variables import MODELS_PATH, update_starpilot_toggles
from openpilot.system.ui.lib.application import FontWeight, MouseEvent, MousePos, gui_app
from openpilot.system.ui.lib.multilang import tr
@@ -674,7 +678,7 @@ class StarPilotDrivingModelLayout(_SettingsPage):
if f"{model_key}.thneed" in files:
return True
if version in TINYGRAD_VERSIONS:
if is_tinygrad_model_version(version):
required_files = set(self._required_files_for_version(model_key, version))
return required_files.issubset(files)
@@ -684,6 +688,9 @@ class StarPilotDrivingModelLayout(_SettingsPage):
return any(file.startswith(f"{model_key}.") or file.startswith(f"{model_key}_") for file in files)
def _required_files_for_version(self, key: str, version: str) -> list[str]:
if uses_combined_driving_artifacts(version):
return [f"{key}_driving_tinygrad.pkl"]
files = [
f"{key}_driving_policy_tinygrad.pkl",
f"{key}_driving_vision_tinygrad.pkl",
@@ -691,7 +698,7 @@ class StarPilotDrivingModelLayout(_SettingsPage):
f"{key}_driving_vision_metadata.pkl",
]
if version in {"v12", "v13", "v14", "v15"}:
if uses_split_off_policy_artifacts(version):
files.extend(
[
f"{key}_driving_off_policy_tinygrad.pkl",
@@ -11,7 +11,11 @@ from openpilot.starpilot.assets.model_manager import (
CANCEL_DOWNLOAD_PARAM,
DOWNLOAD_PROGRESS_PARAM,
ModelManager,
TINYGRAD_VERSIONS,
)
from openpilot.starpilot.common.model_versions import (
is_tinygrad_model_version,
uses_combined_driving_artifacts,
uses_split_off_policy_artifacts,
)
from openpilot.starpilot.common.starpilot_variables import MODELS_PATH
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
@@ -715,9 +719,12 @@ class DrivingModelBigButton(BigButton):
return False
def _required_files_for_version(self, key: str, version: str) -> list[str]:
if version not in TINYGRAD_VERSIONS:
if not is_tinygrad_model_version(version):
return []
if uses_combined_driving_artifacts(version):
return [f"{key}_driving_tinygrad.pkl"]
files = [
f"{key}_driving_policy_tinygrad.pkl",
f"{key}_driving_vision_tinygrad.pkl",
@@ -725,7 +732,7 @@ class DrivingModelBigButton(BigButton):
f"{key}_driving_vision_metadata.pkl",
]
if version in {"v12", "v13", "v14", "v15"}:
if uses_split_off_policy_artifacts(version):
files.extend([
f"{key}_driving_off_policy_tinygrad.pkl",
f"{key}_driving_off_policy_metadata.pkl",
+1 -1
View File
@@ -83,7 +83,7 @@ def download_file(cancel_param, destination, progress_param, url, download_param
def get_remote_file_size(url, suppress_errors=False):
try:
response = requests.head(url, headers={"Accept-Encoding": "identity"}, timeout=10)
response = requests.head(url, headers={"Accept-Encoding": "identity"}, timeout=10, allow_redirects=True)
response.raise_for_status()
return int(response.headers.get("Content-Length", 0))
except Exception as error:
+133 -26
View File
@@ -14,12 +14,18 @@ from openpilot.starpilot.assets.download_functions import (
handle_request_error,
verify_download,
)
from openpilot.starpilot.common.model_versions import (
is_tinygrad_model_version,
uses_combined_driving_artifacts,
uses_split_off_policy_artifacts,
)
from openpilot.starpilot.common.starpilot_utilities import delete_file
from openpilot.starpilot.common.starpilot_variables import MODELS_PATH
MANIFEST_CANDIDATES = ("v21",)
TINYGRAD_VERSIONS = {"v8", "v9", "v10", "v11", "v12", "v13", "v14", "v15"}
TINYGRAD_VERSIONS = {f"v{i}" for i in range(8, 33)}
DEFAULT_MODEL_KEY = "sc2"
ARTIFACT_URLS_CACHE = ".model_artifact_urls.json"
MODEL_KEY_CANONICAL_MAP = {
"sc": DEFAULT_MODEL_KEY,
}
@@ -130,6 +136,13 @@ class ModelManager:
self.model_series = [entry for entry in self._param_text("AvailableModelSeries").split(",") if entry]
self.available_model_names = [entry for entry in self._param_text("AvailableModelNames").split(",") if entry]
@staticmethod
def _manifest_paths(manifest_version: str) -> tuple[str, ...]:
return (
f"Versions/model_names_{manifest_version}.json",
f"model_names_{manifest_version}.json",
)
def _set_model_param_keys(self, model_key: str | None = None, model_name: str | None = None, model_version: str | None = None):
if model_key is not None and model_key != "":
canonical_key = self._canonical_model_key(model_key)
@@ -182,9 +195,12 @@ class ModelManager:
return DEFAULT_MODEL_KEY
def _required_files(self, model_key: str, model_version: str) -> list[str]:
if model_version not in TINYGRAD_VERSIONS:
if not is_tinygrad_model_version(model_version):
return []
if uses_combined_driving_artifacts(model_version):
return [f"{model_key}_driving_tinygrad.pkl"]
filenames = [
f"{model_key}_driving_policy_tinygrad.pkl",
f"{model_key}_driving_vision_tinygrad.pkl",
@@ -192,7 +208,7 @@ class ModelManager:
f"{model_key}_driving_vision_metadata.pkl",
]
if model_version in {"v12", "v13", "v14", "v15"}:
if uses_split_off_policy_artifacts(model_version):
filenames += [
f"{model_key}_driving_off_policy_tinygrad.pkl",
f"{model_key}_driving_off_policy_metadata.pkl",
@@ -200,6 +216,72 @@ class ModelManager:
return filenames
@staticmethod
def _artifact_urls_cache_path() -> Path:
return MODELS_PATH / ARTIFACT_URLS_CACHE
def _load_artifact_url_map(self) -> dict[str, dict[str, str]]:
try:
cache_path = self._artifact_urls_cache_path()
if not cache_path.is_file():
return {}
payload = json.loads(cache_path.read_text())
if not isinstance(payload, dict):
return {}
normalized: dict[str, dict[str, str]] = {}
for model_key, urls in payload.items():
if not isinstance(urls, dict):
continue
normalized[str(model_key)] = {
str(filename): str(url)
for filename, url in urls.items()
if filename and url
}
return normalized
except Exception as error:
print(f"Failed to load artifact URL cache: {error}")
return {}
def _build_artifact_url_map(self, model_info: list[dict]) -> dict[str, dict[str, str]]:
artifact_url_map: dict[str, dict[str, str]] = {}
for model in model_info:
model_key = self._canonical_model_key(str(model.get("id") or "").strip())
model_version = str(model.get("version") or "").strip()
required_files = self._required_files(model_key, model_version)
if not model_key or not required_files:
continue
urls: dict[str, str] = {}
explicit_urls = model.get("artifact_urls") or model.get("download_urls")
if isinstance(explicit_urls, dict):
for filename, url in explicit_urls.items():
if filename and url:
urls[str(filename).strip()] = str(url).strip()
base_url = str(model.get("artifact_base_url") or model.get("download_base_url") or "").strip()
if base_url:
base_url = base_url.rstrip("/")
for filename in required_files:
urls.setdefault(filename, f"{base_url}/{filename}")
direct_url = str(model.get("artifact_url") or model.get("download_url") or "").strip()
if direct_url:
if len(required_files) == 1:
urls.setdefault(required_files[0], direct_url)
else:
matched_filename = next((filename for filename in required_files if Path(filename).name == Path(direct_url).name), None)
if matched_filename is not None:
urls.setdefault(matched_filename, direct_url)
if urls:
artifact_url_map[model_key] = urls
return artifact_url_map
def _is_model_downloaded(self, model_key: str, model_version: str) -> bool:
if is_builtin_model_key(model_key):
return True
@@ -296,16 +378,17 @@ class ModelManager:
def _get_manifest(self, repo_url: str) -> tuple[str | None, list[dict]]:
for manifest_version in MANIFEST_CANDIDATES:
model_info = self._fetch_manifest(f"{repo_url}/Versions/model_names_{manifest_version}.json")
if not model_info:
continue
for manifest_path in self._manifest_paths(manifest_version):
model_info = self._fetch_manifest(f"{repo_url}/{manifest_path}")
if not model_info:
continue
# Desktop/dev build is tinygrad-only.
filtered = [model for model in model_info if model.get("version") in TINYGRAD_VERSIONS]
if not filtered:
continue
# Desktop/dev build is tinygrad-only.
filtered = [model for model in model_info if is_tinygrad_model_version(model.get("version"))]
if not filtered:
continue
return manifest_version, filtered
return manifest_version, filtered
return None, []
@@ -366,6 +449,9 @@ class ModelManager:
versions_file = MODELS_PATH / ".model_versions.json"
versions_file.parent.mkdir(parents=True, exist_ok=True)
versions_file.write_text(json.dumps(version_map))
artifact_urls_file = self._artifact_urls_cache_path()
artifact_urls_file.write_text(json.dumps(self._build_artifact_url_map(model_info)))
except Exception as error:
print(f"Failed to write model versions cache: {error}")
@@ -411,6 +497,8 @@ class ModelManager:
self._load_catalog_from_params()
version_map = self._model_version_map()
model_version = version_map.get(model_to_download)
model_artifact_urls = self._load_artifact_url_map()
artifact_urls = model_artifact_urls.get(self._canonical_model_key(model_to_download)) or model_artifact_urls.get(model_to_download) or {}
required_files = self._required_files(model_to_download, model_version or "")
if not required_files:
handle_error(None, f"Unsupported model format for {model_to_download}", "Model download failed", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM, self.params_memory)
@@ -419,25 +507,40 @@ class ModelManager:
for filename in required_files:
file_path = MODELS_PATH / filename
candidate_urls: list[tuple[str, bool]] = []
custom_url = artifact_urls.get(filename, "").strip()
if custom_url:
candidate_urls.append((custom_url, True))
file_url = f"{repo_url}/Models/{filename}"
download_file(CANCEL_DOWNLOAD_PARAM, file_path, DOWNLOAD_PROGRESS_PARAM, file_url, MODEL_DOWNLOAD_PARAM, self.params_memory)
if self.params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
handle_error(None, "Download cancelled...", "Download cancelled...", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM, self.params_memory)
self.downloading_model = False
return
if verify_download(file_path, file_url):
continue
candidate_urls.append((file_url, False))
fallback_url = f"{GITLAB_URL}/Models/{filename}"
download_file(CANCEL_DOWNLOAD_PARAM, file_path, DOWNLOAD_PROGRESS_PARAM, fallback_url, MODEL_DOWNLOAD_PARAM, self.params_memory)
if self.params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
handle_error(None, "Download cancelled...", "Download cancelled...", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM, self.params_memory)
self.downloading_model = False
return
if fallback_url != file_url:
candidate_urls.append((fallback_url, False))
if not verify_download(file_path, fallback_url):
download_succeeded = False
for candidate_url, allow_unknown_size in candidate_urls:
download_file(
CANCEL_DOWNLOAD_PARAM,
file_path,
DOWNLOAD_PROGRESS_PARAM,
candidate_url,
MODEL_DOWNLOAD_PARAM,
self.params_memory,
allow_unknown_size=allow_unknown_size,
)
if self.params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
handle_error(None, "Download cancelled...", "Download cancelled...", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM, self.params_memory)
self.downloading_model = False
return
if verify_download(file_path, candidate_url, allow_unknown_size=allow_unknown_size):
download_succeeded = True
break
if not download_succeeded:
handle_error(file_path, "Verification failed...", f"Verification failed for {filename}", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM, self.params_memory)
self.downloading_model = False
return
@@ -489,6 +592,10 @@ class ModelManager:
if model_versions_file.is_file():
delete_file(model_versions_file, print_error=False)
artifact_urls_file = self._artifact_urls_cache_path()
if artifact_urls_file.is_file():
delete_file(artifact_urls_file, print_error=False)
self.params.put_bool("TinygradUpdateAvailable", False)
self.params_memory.remove(UPDATE_TINYGRAD_PARAM)
self.params_memory.remove(CANCEL_DOWNLOAD_PARAM)
+27
View File
@@ -0,0 +1,27 @@
from __future__ import annotations
def parse_model_version(version: str | None) -> int | None:
text = str(version or "").strip().lower()
if not text.startswith("v"):
return None
raw_number = text[1:]
if not raw_number.isdigit():
return None
return int(raw_number)
def is_tinygrad_model_version(version: str | None) -> bool:
parsed = parse_model_version(version)
return parsed is not None and parsed >= 8
def uses_split_off_policy_artifacts(version: str | None) -> bool:
parsed = parse_model_version(version)
return parsed is not None and 12 <= parsed < 16
def uses_combined_driving_artifacts(version: str | None) -> bool:
parsed = parse_model_version(version)
return parsed is not None and parsed >= 16
+2 -1
View File
@@ -26,6 +26,7 @@ from openpilot.common.constants import CV
from openpilot.common.params import Params
from openpilot.selfdrive.controls.lib.latcontrol_torque import KP
from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.starpilot.common.model_versions import is_tinygrad_model_version
from openpilot.starpilot.common.accel_profile import (
ACCELERATION_PROFILES,
CUSTOM_ACCEL_PROFILE_PARAM_KEYS,
@@ -1074,7 +1075,7 @@ class StarPilotVariables:
if isinstance(toggle.model_version, bytes):
toggle.model_version = toggle.model_version.decode("utf-8", "ignore")
toggle.classic_model = toggle.model_version in {"v1", "v2", "v3", "v4"}
toggle.tinygrad_model = toggle.model_version in {"v8", "v9", "v10", "v11", "v12", "v13", "v14", "v15"}
toggle.tinygrad_model = is_tinygrad_model_version(toggle.model_version)
toggle.tomb_raider = toggle.model == "space-lab"
toggle.model_ui = self.get_value("ModelUI")
+10 -2
View File
@@ -60,6 +60,11 @@ from openpilot.starpilot.common.maps_catalog import (
schedule_label,
schedule_param_value,
)
from openpilot.starpilot.common.model_versions import (
is_tinygrad_model_version,
uses_combined_driving_artifacts,
uses_split_off_policy_artifacts,
)
from openpilot.starpilot.common.experimental_state import sync_persist_experimental_state
from openpilot.starpilot.common.starpilot_utilities import delete_file, get_lock_status, run_cmd
from openpilot.starpilot.common.starpilot_variables import ACTIVE_THEME_PATH, ERROR_LOGS_PATH, EXCLUDED_KEYS, LEGACY_STARPILOT_PARAM_RENAMES, MAPS_PATH, MODELS_PATH, RESOURCES_REPO, SCREEN_RECORDINGS_PATH, STOCK_THEME_PATH, THEME_SAVE_PATH,\
@@ -4330,14 +4335,17 @@ def setup(app):
if f"{model_key}.thneed" in on_disk_files:
return True
if model_version in ("v8", "v9", "v10", "v11", "v12", "v13", "v14", "v15"):
if is_tinygrad_model_version(model_version):
if uses_combined_driving_artifacts(model_version):
return f"{model_key}_driving_tinygrad.pkl" in on_disk_files
required_files = {
f"{model_key}_driving_policy_tinygrad.pkl",
f"{model_key}_driving_vision_tinygrad.pkl",
f"{model_key}_driving_policy_metadata.pkl",
f"{model_key}_driving_vision_metadata.pkl",
}
if model_version in ("v12", "v13", "v14", "v15"):
if uses_split_off_policy_artifacts(model_version):
required_files |= {
f"{model_key}_driving_off_policy_tinygrad.pkl",
f"{model_key}_driving_off_policy_metadata.pkl",
+8 -1
View File
@@ -615,6 +615,7 @@ bool StarPilotModelPanel::isModelInstalled(const QString &key) const {
}
bool has_thneed = false;
bool has_combined_tg = false;
bool has_policy_meta = false;
bool has_policy_tg = false;
bool has_vision_meta = false;
@@ -635,7 +636,9 @@ bool StarPilotModelPanel::isModelInstalled(const QString &key) const {
if (ext == "thneed") {
has_thneed = true;
} else if (ext == "pkl") {
if (base.contains("_driving_policy_metadata")) {
if (base.contains("_driving_tinygrad")) {
has_combined_tg = true;
} else if (base.contains("_driving_policy_metadata")) {
has_policy_meta = true;
} else if (base.contains("_driving_policy_tinygrad")) {
has_policy_tg = true;
@@ -655,6 +658,10 @@ bool StarPilotModelPanel::isModelInstalled(const QString &key) const {
return true;
}
if (has_combined_tg) {
return true;
}
if (has_policy_meta && has_policy_tg && has_vision_meta && has_vision_tg) {
if (has_off_policy_meta || has_off_policy_tg) {
return has_off_policy_meta && has_off_policy_tg;
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff