Files
github-actions[bot] f9fcc7adab sunnypilot v2026.002.000 release
date: 2026-06-28T09:48:35
master commit: da6313dbe95b3f24bb5d8018b0e5f950f5823ca7
2026-06-28 09:49:29 +08:00

481 lines
21 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import argparse
import os
import pickle
import time
from functools import partial
from collections import defaultdict
import numpy as np
from tinygrad.tensor import Tensor
from tinygrad.device import Device
from tinygrad.engine.jit import TinyJit
from openpilot.selfdrive.modeld.compile_modeld import (
NV12Frame, make_frame_prepare,
shift_and_sample, sample_skip, sample_desire,
)
MODEL_TYPES = ('vision_policy', 'supercombo', 'vision_multi_policy')
def _detect_desire_key(policy_input_shapes):
for k in policy_input_shapes:
if k.startswith('desire'):
return k
return None
def _detect_vision_keys(vision_input_shapes):
img_keys = sorted([k for k in vision_input_shapes if 'img' in k])
road_key = next((k for k in img_keys if 'big' not in k), None)
wide_key = next((k for k in img_keys if 'big' in k), None)
if road_key is None or wide_key is None:
raise ValueError(f"Cannot determine road/wide image keys from {list(vision_input_shapes.keys())}")
return road_key, wide_key
def make_split_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, device):
road_key, _ = _detect_vision_keys(vision_input_shapes)
img = vision_input_shapes[road_key]
n_frames = img[1] // 6
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3])
fb = policy_input_shapes['features_buffer']
desire_key = _detect_desire_key(policy_input_shapes)
dp = policy_input_shapes[desire_key]
tc = policy_input_shapes.get('traffic_convention', (1, 2))
npy = {
'desire': np.zeros(dp[2], dtype=np.float32),
'traffic_convention': np.zeros(tc, dtype=np.float32),
'tfm': np.zeros((3, 3), dtype=np.float32),
'big_tfm': np.zeros((3, 3), dtype=np.float32),
}
handled = {'features_buffer', desire_key, 'traffic_convention'}
for key, shape in policy_input_shapes.items():
if key in handled:
continue
npy[key] = np.zeros(shape, 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 * (fb[1] - 1) + 1, fb[0], fb[2]), dtype=np.float32), device=device).contiguous().realize(),
'desire_q': Tensor(np.zeros((frame_skip * dp[1], dp[0], dp[2]), dtype=np.float32), device=device).contiguous().realize(),
**{k: Tensor(v, device='NPY').realize() for k, v in npy.items()},
}
return input_queues, npy
def make_run_split_policy(vision_runner, policy_runner, nv12: NV12Frame, model_w, model_h,
vision_features_slice, frame_skip, desire_key, extra_policy_keys,
vision_road_key, vision_wide_key, prepare_only=False):
frame_prepare = make_frame_prepare(nv12, model_w, model_h)
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip)
sample_desire_fn = partial(sample_desire, frame_skip=frame_skip)
def run_policy(img_q, big_img_q, feat_q, desire_q, desire, traffic_convention, tfm, big_tfm, frame, big_frame, **extra):
npy_tensors = [tfm.to(Device.DEFAULT), big_tfm.to(Device.DEFAULT),
desire.to(Device.DEFAULT), traffic_convention.to(Device.DEFAULT)]
extra_device = {k: extra[k].to(Device.DEFAULT) for k in extra_policy_keys}
Tensor.realize(*npy_tensors, *extra_device.values())
tfm, big_tfm, desire, traffic_convention = npy_tensors
img = shift_and_sample(img_q, frame_prepare(frame, tfm).unsqueeze(0), sample_skip_fn)
big_img = shift_and_sample(big_img_q, frame_prepare(big_frame, big_tfm).unsqueeze(0), sample_skip_fn)
if prepare_only:
return img, big_img
vision_out = next(iter(vision_runner({vision_road_key: img, vision_wide_key: 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)
desire_buf = shift_and_sample(desire_q, desire.reshape(1, 1, -1), sample_desire_fn)
inputs = {'features_buffer': feat_buf, desire_key: desire_buf, 'traffic_convention': traffic_convention, **extra_device}
policy_out = next(iter(policy_runner(inputs).values())).cast('float32')
return vision_out, policy_out
return run_policy
def compile_split_policy(nv12: NV12Frame, model_w, model_h, prepare_only, frame_skip,
vision_runner, policy_runner, vision_metadata, policy_metadata):
print(f"Compiling combined policy JIT for {nv12.width}x{nv12.height} (prepare_only={prepare_only})...")
vision_features_slice = vision_metadata['output_slices']['hidden_state']
vision_input_shapes = vision_metadata['input_shapes']
policy_input_shapes = policy_metadata['input_shapes']
desire_key = _detect_desire_key(policy_input_shapes)
extra_policy_keys = [k for k in policy_input_shapes if k not in ('features_buffer', desire_key, 'traffic_convention')]
vision_road_key, vision_wide_key = _detect_vision_keys(vision_input_shapes)
_run = make_run_split_policy(vision_runner, policy_runner, nv12, model_w, model_h,
vision_features_slice, frame_skip, desire_key, extra_policy_keys,
vision_road_key, vision_wide_key, prepare_only)
run_policy_jit = TinyJit(_run, prune=True)
SEED = 42
def random_inputs_run_fn(fn, seed, test_val=None, test_buffers=None, expect_match=True):
input_queues, npy = make_split_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, Device.DEFAULT)
np.random.seed(seed)
Tensor.manual_seed(seed)
testing = test_val is not None or test_buffers is not None
n_runs = 1 if testing else 3
for i in range(n_runs):
frame = Tensor.randint(nv12.size, low=0, high=256, dtype='uint8').realize()
big_frame = Tensor.randint(nv12.size, low=0, high=256, dtype='uint8').realize()
for v in npy.values():
v[:] = np.random.randn(*v.shape).astype(v.dtype)
Device.default.synchronize()
st = time.perf_counter()
outs = fn(**input_queues, frame=frame, big_frame=big_frame)
mt = time.perf_counter()
Device.default.synchronize()
et = time.perf_counter()
print(f" [{i+1}/{n_runs}] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms")
if i == 0:
val = [np.copy(v.numpy()) for v in outs]
buffers = [np.copy(v.numpy().copy()) for v in input_queues.values()]
if test_val is not None:
match = all(np.array_equal(a, b) for a, b in zip(val, test_val, strict=True))
assert match == expect_match, f"outputs {'differ from' if expect_match else 'match'} baseline (seed={seed})"
if test_buffers is not None:
match = all(np.array_equal(a, b) for a, b in zip(buffers, test_buffers, strict=True))
assert match == expect_match, f"buffers {'differ from' if expect_match else 'match'} baseline (seed={seed})"
return fn, val, buffers
print('capture + replay')
run_policy_jit, test_val, test_buffers = random_inputs_run_fn(run_policy_jit, SEED)
print('pickle round trip')
run_policy_jit = pickle.loads(pickle.dumps(run_policy_jit))
random_inputs_run_fn(run_policy_jit, SEED, test_val, test_buffers, expect_match=True)
random_inputs_run_fn(run_policy_jit, SEED+1, test_val, test_buffers, expect_match=False)
return run_policy_jit
def derive_frame_skip(vision_input_shapes, policy_input_shapes):
fb = policy_input_shapes.get('features_buffer')
if fb is None:
return 1
fb_history = fb[1]
if fb_history >= 99:
return 1
return 4
def make_supercombo_input_queues(input_shapes, frame_skip, device):
img_shape = input_shapes.get('img', input_shapes.get('input_imgs'))
if img_shape is None:
raise ValueError("No img input found in model shapes")
n_frames = img_shape[1] // 6
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img_shape[2], img_shape[3])
numpy_keys = {}
queue_keys = {}
for key, shape in input_shapes.items():
if 'img' in key:
continue
if len(shape) == 3 and shape[1] > 1:
if key.startswith('desire'):
numpy_keys[key] = np.zeros(shape[2], dtype=np.float32)
queue_keys[f'{key}_q'] = Tensor(
np.zeros((frame_skip * shape[1], shape[0], shape[2]), dtype=np.float32),
device=device).contiguous().realize()
elif key == 'features_buffer':
queue_keys['feat_q'] = Tensor(
np.zeros((frame_skip * (shape[1] - 1) + 1, shape[0], shape[2]), dtype=np.float32),
device=device).contiguous().realize()
else:
numpy_keys[key] = np.zeros(shape, dtype=np.float32)
elif len(shape) == 2:
numpy_keys[key] = np.zeros(shape, dtype=np.float32)
if 'traffic_convention' not in numpy_keys:
tc_shape = input_shapes.get('traffic_convention', (1, 2))
numpy_keys['traffic_convention'] = np.zeros(tc_shape, dtype=np.float32)
numpy_keys['tfm'] = np.zeros((3, 3), dtype=np.float32)
numpy_keys['big_tfm'] = np.zeros((3, 3), 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(),
**queue_keys,
**{k: Tensor(v, device='NPY').realize() for k, v in numpy_keys.items()},
}
return input_queues, numpy_keys
def make_run_supercombo(model_runner, nv12: NV12Frame, model_w, model_h,
features_slice, frame_skip, input_shapes, prepare_only=False):
frame_prepare = make_frame_prepare(nv12, model_w, model_h)
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip)
sample_desire_fn = partial(sample_desire, frame_skip=frame_skip)
desire_key = _detect_desire_key(input_shapes)
if desire_key is None:
raise ValueError(f"No desire* key found in input_shapes: {list(input_shapes.keys())}")
road_img_key, wide_img_key = _detect_vision_keys(input_shapes)
extra_policy_keys = [k for k in input_shapes
if k not in (desire_key, 'features_buffer', 'traffic_convention')
and 'img' not in k]
def run_supercombo(img_q, big_img_q, feat_q, desire_q,
frame, big_frame, **kwargs):
desire = kwargs.get(desire_key)
traffic_convention = kwargs.get('traffic_convention')
tfm = kwargs['tfm']
big_tfm = kwargs['big_tfm']
tfm = tfm.to(Device.DEFAULT)
big_tfm = big_tfm.to(Device.DEFAULT)
desire = desire.to(Device.DEFAULT)
traffic_convention = traffic_convention.to(Device.DEFAULT)
Tensor.realize(tfm, big_tfm, desire, traffic_convention)
img = shift_and_sample(img_q, frame_prepare(frame, tfm).unsqueeze(0), sample_skip_fn)
big_img = shift_and_sample(big_img_q, frame_prepare(big_frame, big_tfm).unsqueeze(0), sample_skip_fn)
if prepare_only:
return img, big_img
desire_buf = shift_and_sample(desire_q, desire.reshape(1, 1, -1), sample_desire_fn)
feat_buf = sample_skip_fn(feat_q)
inputs = {road_img_key: img, wide_img_key: big_img,
desire_key: desire_buf, 'features_buffer': feat_buf,
'traffic_convention': traffic_convention}
for k in extra_policy_keys:
if k in kwargs:
inputs[k] = kwargs[k].to(Device.DEFAULT)
model_out = next(iter(model_runner(inputs).values())).cast('float32')
new_feat = model_out[:, features_slice].reshape(1, -1).unsqueeze(0)
shift_and_sample(feat_q, new_feat, sample_skip_fn)
return model_out
return run_supercombo
def make_run_vision_multi_policy(vision_runner, policy_runners, nv12: NV12Frame, model_w, model_h,
vision_features_slice, frame_skip, desire_key, extra_policy_keys,
vision_road_key, vision_wide_key, prepare_only=False):
frame_prepare = make_frame_prepare(nv12, model_w, model_h)
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip)
sample_desire_fn = partial(sample_desire, frame_skip=frame_skip)
def run_multi_policy(img_q, big_img_q, feat_q, desire_q, desire,
traffic_convention, tfm, big_tfm, frame, big_frame, **extra):
npy_tensors = [tfm.to(Device.DEFAULT), big_tfm.to(Device.DEFAULT),
desire.to(Device.DEFAULT), traffic_convention.to(Device.DEFAULT)]
extra_device = {k: extra[k].to(Device.DEFAULT) for k in extra_policy_keys}
Tensor.realize(*npy_tensors, *extra_device.values())
tfm, big_tfm, desire, traffic_convention = npy_tensors
img = shift_and_sample(img_q, frame_prepare(frame, tfm).unsqueeze(0), sample_skip_fn)
big_img = shift_and_sample(big_img_q, frame_prepare(big_frame, big_tfm).unsqueeze(0), sample_skip_fn)
if prepare_only:
return img, big_img
vision_out = next(iter(vision_runner({vision_road_key: img, vision_wide_key: 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)
desire_buf = shift_and_sample(desire_q, desire.reshape(1, 1, -1), sample_desire_fn)
inputs = {'features_buffer': feat_buf, desire_key: desire_buf, 'traffic_convention': traffic_convention, **extra_device}
policy_outputs = []
for runner in policy_runners:
policy_out = next(iter(runner(inputs).values())).cast('float32')
policy_outputs.append(policy_out)
return (vision_out, *policy_outputs)
return run_multi_policy
def _warmup_and_serialize(run_jit, input_queues, npy, nv12):
for i in range(3):
np.random.seed(42 + i)
frame = Tensor.randint(nv12.size, low=0, high=256, dtype='uint8').realize()
big_frame = Tensor.randint(nv12.size, low=0, high=256, dtype='uint8').realize()
for v in npy.values():
v[:] = np.random.randn(*v.shape).astype(v.dtype)
Device.default.synchronize()
st = time.perf_counter()
run_jit(**input_queues, frame=frame, big_frame=big_frame)
mt = time.perf_counter()
Device.default.synchronize()
et = time.perf_counter()
print(f" [{i + 1}/3] enqueue {(mt - st) * 1e3:6.2f} ms -- total {(et - st) * 1e3:6.2f} ms")
return pickle.loads(pickle.dumps(run_jit))
def compile_supercombo(nv12: NV12Frame, model_w, model_h, prepare_only, frame_skip,
model_runner, metadata):
print(f"Compiling combined supercombo JIT for {nv12.width}x{nv12.height} (prepare_only={prepare_only})...")
features_slice = metadata['output_slices']['hidden_state']
input_shapes = metadata['input_shapes']
_run = make_run_supercombo(model_runner, nv12, model_w, model_h,
features_slice, frame_skip, input_shapes, prepare_only)
run_jit = TinyJit(_run, prune=True)
input_queues, npy = make_supercombo_input_queues(input_shapes, frame_skip, Device.DEFAULT)
run_jit = _warmup_and_serialize(run_jit, input_queues, npy, nv12)
return run_jit
def compile_multi_policy(nv12: NV12Frame, model_w, model_h, prepare_only, frame_skip,
vision_runner, policy_runners, vision_metadata, policy_metadata):
print(f"Compiling combined multi-policy JIT for {nv12.width}x{nv12.height} (prepare_only={prepare_only})...")
vision_features_slice = vision_metadata['output_slices']['hidden_state']
vision_input_shapes = vision_metadata['input_shapes']
policy_input_shapes = policy_metadata['input_shapes']
desire_key = _detect_desire_key(policy_input_shapes)
extra_policy_keys = [k for k in policy_input_shapes if k not in ('features_buffer', desire_key, 'traffic_convention')]
vision_road_key, vision_wide_key = _detect_vision_keys(vision_input_shapes)
_run = make_run_vision_multi_policy(vision_runner, policy_runners, nv12, model_w, model_h,
vision_features_slice, frame_skip, desire_key, extra_policy_keys,
vision_road_key, vision_wide_key, prepare_only)
run_jit = TinyJit(_run, prune=True)
input_queues, npy = make_split_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, Device.DEFAULT)
run_jit = _warmup_and_serialize(run_jit, input_queues, npy, nv12)
return run_jit
def _parse_size(s):
w, h = s.lower().split('x')
return int(w), int(h)
if __name__ == "__main__":
from tinygrad.nn.onnx import OnnxRunner
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
from openpilot.selfdrive.modeld.get_model_metadata import make_metadata_dict
p = argparse.ArgumentParser(description="Compile combined JIT pkl for sunnypilot modeld_v2")
p.add_argument('--model-type', choices=MODEL_TYPES, required=True)
p.add_argument('--model-size', type=_parse_size, required=True, help='model input WxH')
p.add_argument('--camera-resolutions', type=_parse_size, nargs='+', required=True)
p.add_argument('--frame-skip', type=int, default=None, help='frame skip value (auto-derived if not provided)')
p.add_argument('--output', required=True)
p.add_argument('--vision-onnx', help='vision ONNX (for split models)')
p.add_argument('--policy-onnx', help='policy ONNX (for vision_policy)')
p.add_argument('--off-policy-onnx', help='off-policy ONNX (for vision_multi_policy)')
p.add_argument('--on-policy-onnx', help='on-policy ONNX (for vision_multi_policy)')
p.add_argument('--supercombo-onnx', help='supercombo ONNX (for supercombo)')
args = p.parse_args()
out = defaultdict(dict)
if args.model_type == 'vision_policy':
assert args.vision_onnx and args.policy_onnx
vision_runner = OnnxRunner(args.vision_onnx)
policy_runner = OnnxRunner(args.policy_onnx)
out['metadata']['vision'] = make_metadata_dict(args.vision_onnx)
out['metadata']['policy'] = make_metadata_dict(args.policy_onnx)
frame_skip = args.frame_skip if args.frame_skip is not None else derive_frame_skip(out['metadata']['vision']['input_shapes'],
out['metadata']['policy']['input_shapes'])
for cam_w, cam_h in args.camera_resolutions:
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
model_w, model_h = args.model_size
out[(cam_w, cam_h)] = {
name: compile_split_policy(nv12, model_w, model_h, prepare_only, frame_skip,
vision_runner, policy_runner,
out['metadata']['vision'], out['metadata']['policy'])
for name, prepare_only in [('warp_enqueue', True), ('run_policy', False)]
}
elif args.model_type == 'supercombo':
assert args.supercombo_onnx
model_runner = OnnxRunner(args.supercombo_onnx)
out['metadata']['model'] = make_metadata_dict(args.supercombo_onnx)
frame_skip = args.frame_skip if args.frame_skip is not None else derive_frame_skip({}, out['metadata']['model']['input_shapes'])
for cam_w, cam_h in args.camera_resolutions:
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
model_w, model_h = args.model_size
out[(cam_w, cam_h)] = {
name: compile_supercombo(nv12, model_w, model_h, prepare_only, frame_skip,
model_runner, out['metadata']['model'])
for name, prepare_only in [('warp_enqueue', True), ('run_policy', False)]
}
elif args.model_type == 'vision_multi_policy':
assert args.vision_onnx
vision_runner = OnnxRunner(args.vision_onnx)
out['metadata']['vision'] = make_metadata_dict(args.vision_onnx)
policy_runners = []
policy_onnxes = []
if args.policy_onnx:
policy_onnxes.append(('policy', args.policy_onnx))
if args.off_policy_onnx:
policy_onnxes.append(('off_policy', args.off_policy_onnx))
if args.on_policy_onnx:
policy_onnxes.append(('on_policy', args.on_policy_onnx))
for name, onnx_path in policy_onnxes:
runner = OnnxRunner(onnx_path)
policy_runners.append(runner)
out['metadata'][name] = make_metadata_dict(onnx_path)
first_policy_key = policy_onnxes[0][0]
frame_skip = args.frame_skip if args.frame_skip is not None else derive_frame_skip(out['metadata']['vision']['input_shapes'],
out['metadata'][first_policy_key]['input_shapes'])
for cam_w, cam_h in args.camera_resolutions:
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
model_w, model_h = args.model_size
out[(cam_w, cam_h)] = {
name: compile_multi_policy(nv12, model_w, model_h, prepare_only, frame_skip,
vision_runner, policy_runners,
out['metadata']['vision'], out['metadata'][first_policy_key])
for name, prepare_only in [('warp_enqueue', True), ('run_policy', False)]
}
with open(args.output, "wb") as f:
pickle.dump(out, f)
pkl_size = os.path.getsize(args.output)
print(f"Saved combined JIT to {args.output} ({pkl_size / 1e6:.2f} MB)")
from openpilot.common.file_chunker import chunk_file, get_chunk_targets
chunk_targets = get_chunk_targets(args.output, pkl_size)
chunk_file(args.output, chunk_targets)
num_chunks = len(chunk_targets) - 1
print(f"Chunked into {num_chunks} file(s)")