mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-07-24 18:32:06 +08:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6654e9cdf9 | |||
| 6523084bfc | |||
| 94737c523d | |||
| 46fd88376e | |||
| 3ac95a7475 | |||
| 4cc84c5680 | |||
| 0768b2408c | |||
| 402f3c8966 | |||
| ae44e4d998 | |||
| ccf40652b6 | |||
| 271ed5e091 | |||
| 41dea5d48d | |||
| dc11e5fd84 | |||
| ced4a664cc | |||
| 03db277c22 | |||
| 11ed3800bf | |||
| 92526b878c | |||
| 66ff8ae52c | |||
| d85cb76304 | |||
| b4c613680e | |||
| f7511491f7 | |||
| 88b30e199b | |||
| 2898f394dd | |||
| 554cf9ca4a |
@@ -172,7 +172,7 @@ jobs:
|
|||||||
output_file="${{ env.MODELS_DIR }}/${base_name}_tinygrad.pkl"
|
output_file="${{ env.MODELS_DIR }}/${base_name}_tinygrad.pkl"
|
||||||
|
|
||||||
echo "Compiling: $onnx_file -> $output_file"
|
echo "Compiling: $onnx_file -> $output_file"
|
||||||
QCOM=1 python3 "${{ env.TINYGRAD_PATH }}/examples/openpilot/compile3.py" "$onnx_file" "$output_file"
|
DEV=QCOM FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 IMAGE=2 python3 "${{ env.TINYGRAD_PATH }}/examples/openpilot/compile3.py" "$onnx_file" "$output_file"
|
||||||
DEV=QCOM FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 python3 "${{ env.MODELS_DIR }}/../get_model_metadata.py" "$onnx_file" || true
|
DEV=QCOM FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 python3 "${{ env.MODELS_DIR }}/../get_model_metadata.py" "$onnx_file" || true
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
sunnypilot Version 2026.002.000 (2026-xx-xx)
|
|
||||||
========================
|
|
||||||
|
|
||||||
sunnypilot Version 2026.001.000 (2026-05-06)
|
sunnypilot Version 2026.001.000 (2026-05-06)
|
||||||
========================
|
========================
|
||||||
* What's Changed (sunnypilot/sunnypilot)
|
* What's Changed (sunnypilot/sunnypilot)
|
||||||
|
|||||||
@@ -194,13 +194,6 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
|
|||||||
aTarget @5 :Float32;
|
aTarget @5 :Float32;
|
||||||
events @6 :List(OnroadEventSP.Event);
|
events @6 :List(OnroadEventSP.Event);
|
||||||
e2eAlerts @7 :E2eAlerts;
|
e2eAlerts @7 :E2eAlerts;
|
||||||
accelPersonality @8 :AccelerationPersonality;
|
|
||||||
|
|
||||||
enum AccelerationPersonality {
|
|
||||||
sport @0;
|
|
||||||
normal @1;
|
|
||||||
eco @2;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DynamicExperimentalControl {
|
struct DynamicExperimentalControl {
|
||||||
state @0 :DynamicExperimentalControlState;
|
state @0 :DynamicExperimentalControlState;
|
||||||
|
|||||||
+7
-5
@@ -2054,14 +2054,16 @@ struct DriverStateV2 {
|
|||||||
facePosition @2 :List(Float32);
|
facePosition @2 :List(Float32);
|
||||||
facePositionStd @3 :List(Float32);
|
facePositionStd @3 :List(Float32);
|
||||||
faceProb @4 :Float32;
|
faceProb @4 :Float32;
|
||||||
leftEyeProb @5 :Float32;
|
eyesVisibleProb @14 :Float32;
|
||||||
rightEyeProb @6 :Float32;
|
eyesClosedProb @15 :Float32;
|
||||||
leftBlinkProb @7 :Float32;
|
|
||||||
rightBlinkProb @8 :Float32;
|
|
||||||
sunglassesProb @9 :Float32;
|
|
||||||
phoneProb @13 :Float32;
|
phoneProb @13 :Float32;
|
||||||
|
|
||||||
deprecated :group {
|
deprecated :group {
|
||||||
|
leftEyeProb @5 :Float32;
|
||||||
|
rightEyeProb @6 :Float32;
|
||||||
|
leftBlinkProb @7 :Float32;
|
||||||
|
rightBlinkProb @8 :Float32;
|
||||||
|
sunglassesProb @9 :Float32;
|
||||||
notReadyProb @12 :List(Float32);
|
notReadyProb @12 :List(Float32);
|
||||||
occludedProb @10 :Float32;
|
occludedProb @10 :Float32;
|
||||||
readyProb @11 :List(Float32);
|
readyProb @11 :List(Float32);
|
||||||
|
|||||||
@@ -135,8 +135,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"Version", {PERSISTENT, STRING}},
|
{"Version", {PERSISTENT, STRING}},
|
||||||
|
|
||||||
// --- sunnypilot params --- //
|
// --- sunnypilot params --- //
|
||||||
{"AccelPersonality", {PERSISTENT | BACKUP, INT, std::to_string(static_cast<int>(cereal::LongitudinalPlanSP::AccelerationPersonality::NORMAL))}},
|
|
||||||
{"AccelPersonalityEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
|
|
||||||
{"ApiCache_DriveStats", {PERSISTENT, JSON}},
|
{"ApiCache_DriveStats", {PERSISTENT, JSON}},
|
||||||
{"AutoLaneChangeBsmDelay", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"AutoLaneChangeBsmDelay", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"AutoLaneChangeTimer", {PERSISTENT | BACKUP, INT, "0"}},
|
{"AutoLaneChangeTimer", {PERSISTENT | BACKUP, INT, "0"}},
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:56de402482b5987ed9a0ff3f793a1c89f857304b34fbb8a3deb5b5d4a332be1c
|
|
||||||
size 3688
|
|
||||||
@@ -313,14 +313,11 @@ class LongitudinalMpc:
|
|||||||
lead_xv = self.extrapolate_lead(x_lead, v_lead, a_lead, a_lead_tau)
|
lead_xv = self.extrapolate_lead(x_lead, v_lead, a_lead, a_lead_tau)
|
||||||
return lead_xv
|
return lead_xv
|
||||||
|
|
||||||
def update(self, radarstate, v_cruise, personality=log.LongitudinalPersonality.standard, a_cruise_min=None):
|
def update(self, radarstate, v_cruise, personality=log.LongitudinalPersonality.standard):
|
||||||
t_follow = get_T_FOLLOW(personality)
|
t_follow = get_T_FOLLOW(personality)
|
||||||
v_ego = self.x0[1]
|
v_ego = self.x0[1]
|
||||||
self.status = radarstate.leadOne.status or radarstate.leadTwo.status
|
self.status = radarstate.leadOne.status or radarstate.leadTwo.status
|
||||||
|
|
||||||
if a_cruise_min is None:
|
|
||||||
a_cruise_min = CRUISE_MIN_ACCEL
|
|
||||||
|
|
||||||
lead_xv_0 = self.process_lead(radarstate.leadOne)
|
lead_xv_0 = self.process_lead(radarstate.leadOne)
|
||||||
lead_xv_1 = self.process_lead(radarstate.leadTwo)
|
lead_xv_1 = self.process_lead(radarstate.leadTwo)
|
||||||
|
|
||||||
@@ -332,7 +329,7 @@ class LongitudinalMpc:
|
|||||||
|
|
||||||
# Fake an obstacle for cruise, this ensures smooth acceleration to set speed
|
# Fake an obstacle for cruise, this ensures smooth acceleration to set speed
|
||||||
# when the leads are no factor.
|
# when the leads are no factor.
|
||||||
v_lower = v_ego + (T_IDXS * a_cruise_min * 1.05)
|
v_lower = v_ego + (T_IDXS * CRUISE_MIN_ACCEL * 1.05)
|
||||||
# TODO does this make sense when max_a is negative?
|
# TODO does this make sense when max_a is negative?
|
||||||
v_upper = v_ego + (T_IDXS * CRUISE_MAX_ACCEL * 1.05)
|
v_upper = v_ego + (T_IDXS * CRUISE_MAX_ACCEL * 1.05)
|
||||||
v_cruise_clipped = np.clip(v_cruise * np.ones(N+1), v_lower, v_upper)
|
v_cruise_clipped = np.clip(v_cruise * np.ones(N+1), v_lower, v_upper)
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
|||||||
# No change cost when user is controlling the speed, or when standstill
|
# No change cost when user is controlling the speed, or when standstill
|
||||||
prev_accel_constraint = not (reset_state or sm['carState'].standstill)
|
prev_accel_constraint = not (reset_state or sm['carState'].standstill)
|
||||||
|
|
||||||
accel_clip = self.get_accel_clip(v_ego) or [ACCEL_MIN, get_max_accel(v_ego)]
|
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
|
||||||
steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg
|
steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg
|
||||||
accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP)
|
accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP)
|
||||||
|
|
||||||
@@ -138,8 +138,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
|||||||
|
|
||||||
self.mpc.set_weights(prev_accel_constraint, personality=sm['selfdriveState'].personality)
|
self.mpc.set_weights(prev_accel_constraint, personality=sm['selfdriveState'].personality)
|
||||||
self.mpc.set_cur_state(self.v_desired_filter.x, self.a_desired)
|
self.mpc.set_cur_state(self.v_desired_filter.x, self.a_desired)
|
||||||
self.mpc.update(sm['radarState'], v_cruise, personality=sm['selfdriveState'].personality,
|
self.mpc.update(sm['radarState'], v_cruise, personality=sm['selfdriveState'].personality)
|
||||||
a_cruise_min=self.get_cruise_min_accel(v_ego))
|
|
||||||
|
|
||||||
self.v_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.v_solution)
|
self.v_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.v_solution)
|
||||||
self.a_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.a_solution)
|
self.a_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.a_solution)
|
||||||
|
|||||||
+47
-17
@@ -1,10 +1,23 @@
|
|||||||
import glob
|
import glob
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
from itertools import product
|
||||||
from SCons.Script import Value
|
from SCons.Script import Value
|
||||||
from openpilot.common.file_chunker import chunk_file, get_chunk_paths
|
from openpilot.common.file_chunker import chunk_file, get_chunk_paths
|
||||||
|
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
|
||||||
|
from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE, DM_INPUT_SIZE
|
||||||
|
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||||
|
from openpilot.selfdrive.modeld.helpers import CompileConfig
|
||||||
from tinygrad import Device
|
from tinygrad import Device
|
||||||
|
|
||||||
|
CAMERA_CONFIGS = [
|
||||||
|
(_ar_ox_fisheye.width, _ar_ox_fisheye.height), # tici: 1928x1208
|
||||||
|
(_os_fisheye.width, _os_fisheye.height), # mici: 1344x760
|
||||||
|
]
|
||||||
|
MODELD_CONFIGS = [CompileConfig(cam_w, cam_h, prepare_only, 'driving_')
|
||||||
|
for (cam_w, cam_h), prepare_only in product(CAMERA_CONFIGS, [True, False])]
|
||||||
|
DM_WARP_CONFIGS = [CompileConfig(cam_w, cam_h, True, 'dm_') for cam_w, cam_h in CAMERA_CONFIGS]
|
||||||
|
|
||||||
Import('env', 'arch')
|
Import('env', 'arch')
|
||||||
chunker_file = File("#common/file_chunker.py")
|
chunker_file = File("#common/file_chunker.py")
|
||||||
lenv = env.Clone()
|
lenv = env.Clone()
|
||||||
@@ -16,18 +29,17 @@ tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "
|
|||||||
def estimate_pickle_max_size(onnx_size):
|
def estimate_pickle_max_size(onnx_size):
|
||||||
return 1.2 * onnx_size + 10 * 1024 * 1024 # 20% + 10MB is plenty
|
return 1.2 * onnx_size + 10 * 1024 * 1024 # 20% + 10MB is plenty
|
||||||
|
|
||||||
# THREADS=0 is need to prevent bug: https://github.com/tinygrad/tinygrad/issues/14689
|
|
||||||
# get fastest TG config
|
# get fastest TG config
|
||||||
available = set(Device.get_available_devices())
|
available = set(Device.get_available_devices())
|
||||||
# FIXME-SP: reset when we bump tg
|
if 'CUDA' in available:
|
||||||
if False: # 'CUDA' in available:
|
|
||||||
tg_backend = 'CUDA'
|
tg_backend = 'CUDA'
|
||||||
tg_flags = f'DEV={tg_backend}'
|
tg_flags = f'DEV={tg_backend}'
|
||||||
elif 'QCOM' in available:
|
elif 'QCOM' in available:
|
||||||
tg_backend = 'QCOM'
|
tg_backend = 'QCOM'
|
||||||
tg_flags = f'DEV={tg_backend} FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0'
|
tg_flags = f'DEV={tg_backend} FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1'
|
||||||
else:
|
else:
|
||||||
tg_backend = 'CPU' if arch == 'Darwin' else 'CPU CPU_LLVM=1' # FIXME-SP: reset when we bump tg
|
tg_backend = 'CPU' if arch == 'Darwin' else 'CPU:LLVM'
|
||||||
|
# THREADS=0 is need to prevent bug: https://github.com/tinygrad/tinygrad/issues/14689
|
||||||
tg_flags = f'DEV={tg_backend} THREADS=0'
|
tg_flags = f'DEV={tg_backend} THREADS=0'
|
||||||
|
|
||||||
def write_tg_compiled_flags(target, source, env):
|
def write_tg_compiled_flags(target, source, env):
|
||||||
@@ -54,14 +66,35 @@ for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']:
|
|||||||
image_flag = {
|
image_flag = {
|
||||||
'larch64': 'IMAGE=2',
|
'larch64': 'IMAGE=2',
|
||||||
}.get(arch, 'IMAGE=0')
|
}.get(arch, 'IMAGE=0')
|
||||||
script_files = [File(Dir("#selfdrive/modeld").File("compile_warp.py").abspath)]
|
modeld_dir = Dir("#selfdrive/modeld").abspath
|
||||||
compile_warp_cmd = f'{tg_flags} {mac_brew_string} python3 {Dir("#selfdrive/modeld").abspath}/compile_warp.py '
|
compile_modeld_script = [File(f"{modeld_dir}/compile_modeld.py")]
|
||||||
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
|
compile_dm_warp_script = [File(f"{modeld_dir}/compile_dm_warp.py")]
|
||||||
warp_targets = []
|
driving_onnx_deps = [File(f"models/{m}.onnx").abspath for m in ['driving_vision', 'driving_policy']]
|
||||||
for cam in [_ar_ox_fisheye, _os_fisheye]:
|
driving_metadata_deps = [File(f"models/{m}_metadata.pkl").abspath for m in ['driving_vision', 'driving_policy']]
|
||||||
w, h = cam.width, cam.height
|
|
||||||
warp_targets += [File(f"models/warp_{w}x{h}_tinygrad.pkl").abspath, File(f"models/dm_warp_{w}x{h}_tinygrad.pkl").abspath]
|
model_w, model_h = MEDMODEL_INPUT_SIZE
|
||||||
lenv.Command(warp_targets, tinygrad_files + script_files + [compiled_flags_node], compile_warp_cmd)
|
frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
||||||
|
for cfg in MODELD_CONFIGS:
|
||||||
|
cmd = (f'{tg_flags} {mac_brew_string} {image_flag} python3 {modeld_dir}/compile_modeld.py '
|
||||||
|
f'--model-size {model_w}x{model_h} '
|
||||||
|
f'--nv12 {",".join(str(x) for x in cfg.nv12)} '
|
||||||
|
f'--vision-onnx {File("models/driving_vision.onnx").abspath} '
|
||||||
|
f'--policy-onnx {File("models/driving_policy.onnx").abspath} '
|
||||||
|
f'--output {cfg.pkl_path} --frame-skip {frame_skip}'
|
||||||
|
+ (' --prepare-only' if cfg.prepare_only else ''))
|
||||||
|
node = lenv.Command(cfg.pkl_path, tinygrad_files + compile_modeld_script + driving_onnx_deps + driving_metadata_deps + [chunker_file, compiled_flags_node], cmd)
|
||||||
|
onnx_sizes_sum = sum(os.path.getsize(f) for f in driving_onnx_deps)
|
||||||
|
chunk_targets = get_chunk_paths(cfg.pkl_path, estimate_pickle_max_size(onnx_sizes_sum))
|
||||||
|
def do_chunk(target, source, env, pkl=cfg.pkl_path, chunks=chunk_targets):
|
||||||
|
chunk_file(pkl, chunks)
|
||||||
|
lenv.Command(chunk_targets, node, do_chunk)
|
||||||
|
|
||||||
|
dm_w, dm_h = DM_INPUT_SIZE
|
||||||
|
for cfg in DM_WARP_CONFIGS:
|
||||||
|
cmd = (f'{tg_flags} {mac_brew_string} {image_flag} python3 {modeld_dir}/compile_dm_warp.py '
|
||||||
|
f'--nv12 {",".join(str(x) for x in cfg.nv12)} --warp-to {dm_w}x{dm_h} '
|
||||||
|
f'--output {cfg.pkl_path}')
|
||||||
|
lenv.Command(cfg.pkl_path, tinygrad_files + compile_dm_warp_script + compile_modeld_script + [compiled_flags_node], cmd)
|
||||||
|
|
||||||
def tg_compile(flags, model_name):
|
def tg_compile(flags, model_name):
|
||||||
pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + '"'
|
pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + '"'
|
||||||
@@ -82,7 +115,4 @@ def tg_compile(flags, model_name):
|
|||||||
do_chunk,
|
do_chunk,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Compile small models
|
tg_compile(tg_flags, 'dmonitoring_model')
|
||||||
for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']:
|
|
||||||
tg_compile(tg_flags, model_name)
|
|
||||||
|
|
||||||
|
|||||||
Executable
+54
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import pickle
|
||||||
|
import time
|
||||||
|
|
||||||
|
from tinygrad.tensor import Tensor
|
||||||
|
from tinygrad.device import Device
|
||||||
|
from tinygrad.engine.jit import TinyJit
|
||||||
|
|
||||||
|
from openpilot.selfdrive.modeld.compile_modeld import NV12Frame, warp_perspective_tinygrad, _parse_size, _parse_nv12
|
||||||
|
|
||||||
|
|
||||||
|
def make_warp_dm(nv12: NV12Frame, dm_w, dm_h):
|
||||||
|
cam_w, cam_h, stride, _, _, _ = nv12
|
||||||
|
stride_pad = stride - cam_w
|
||||||
|
|
||||||
|
def warp_dm(input_frame, M_inv):
|
||||||
|
M_inv = M_inv.to(Device.DEFAULT).realize()
|
||||||
|
return warp_perspective_tinygrad(input_frame[:cam_h*stride], M_inv,
|
||||||
|
(dm_w, dm_h), (cam_h, cam_w), stride_pad).reshape(-1, dm_h * dm_w)
|
||||||
|
return warp_dm
|
||||||
|
|
||||||
|
|
||||||
|
def compile_dm_warp(nv12: NV12Frame, dm_w, dm_h, pkl_path):
|
||||||
|
print(f"Compiling DM warp for {nv12.width}x{nv12.height} -> {dm_w}x{dm_h}...")
|
||||||
|
|
||||||
|
warp_dm_jit = TinyJit(make_warp_dm(nv12, dm_w, dm_h), prune=True)
|
||||||
|
|
||||||
|
for i in range(10):
|
||||||
|
frame = Tensor.randint(nv12.size, low=0, high=256, dtype='uint8').realize()
|
||||||
|
M_inv = Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')
|
||||||
|
Device.default.synchronize()
|
||||||
|
st = time.perf_counter()
|
||||||
|
warp_dm_jit(frame, M_inv).realize()
|
||||||
|
mt = time.perf_counter()
|
||||||
|
Device.default.synchronize()
|
||||||
|
et = time.perf_counter()
|
||||||
|
print(f" [{i+1}/10] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms")
|
||||||
|
|
||||||
|
with open(pkl_path, "wb") as f:
|
||||||
|
pickle.dump(warp_dm_jit, f)
|
||||||
|
print(f" Saved to {pkl_path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
p = argparse.ArgumentParser()
|
||||||
|
p.add_argument('--nv12', type=_parse_nv12, required=True,
|
||||||
|
help=f'NV12 frame layout: {",".join(NV12Frame._fields)}')
|
||||||
|
p.add_argument('--warp-to', type=_parse_size, required=True, help='DM input WxH')
|
||||||
|
p.add_argument('--output', required=True)
|
||||||
|
args = p.parse_args()
|
||||||
|
|
||||||
|
dm_w, dm_h = args.warp_to
|
||||||
|
compile_dm_warp(args.nv12, dm_w, dm_h, args.output)
|
||||||
Executable
+253
@@ -0,0 +1,253 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import pickle
|
||||||
|
import time
|
||||||
|
from functools import partial
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from tinygrad.tensor import Tensor
|
||||||
|
from tinygrad.helpers import Context
|
||||||
|
from tinygrad.device import Device
|
||||||
|
from tinygrad.engine.jit import TinyJit
|
||||||
|
from tinygrad.nn.onnx import OnnxRunner
|
||||||
|
|
||||||
|
# https://github.com/tinygrad/tinygrad/issues/15682
|
||||||
|
from tinygrad.uop.ops import UOp, Ops
|
||||||
|
_orig = UOp.__reduce__
|
||||||
|
UOp.__reduce__ = lambda self: (UOp.unique, ()) if self.op is Ops.UNIQUE else _orig(self)
|
||||||
|
|
||||||
|
|
||||||
|
NV12Frame = namedtuple("NV12Frame", ['width', 'height', 'stride', 'y_height', 'uv_height', 'size'])
|
||||||
|
|
||||||
|
UV_SCALE_MATRIX = np.array([[0.5, 0, 0], [0, 0.5, 0], [0, 0, 1]], dtype=np.float32)
|
||||||
|
UV_SCALE_MATRIX_INV = np.linalg.inv(UV_SCALE_MATRIX)
|
||||||
|
|
||||||
|
|
||||||
|
def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad):
|
||||||
|
w_dst, h_dst = dst_shape
|
||||||
|
h_src, w_src = src_shape
|
||||||
|
|
||||||
|
x = Tensor.arange(w_dst).reshape(1, w_dst).expand(h_dst, w_dst).reshape(-1)
|
||||||
|
y = Tensor.arange(h_dst).reshape(h_dst, 1).expand(h_dst, w_dst).reshape(-1)
|
||||||
|
|
||||||
|
# inline 3x3 matmul as elementwise to avoid reduce op (enables fusion with gather)
|
||||||
|
src_x = M_inv[0, 0] * x + M_inv[0, 1] * y + M_inv[0, 2]
|
||||||
|
src_y = M_inv[1, 0] * x + M_inv[1, 1] * y + M_inv[1, 2]
|
||||||
|
src_w = M_inv[2, 0] * x + M_inv[2, 1] * y + M_inv[2, 2]
|
||||||
|
|
||||||
|
src_x = src_x / src_w
|
||||||
|
src_y = src_y / src_w
|
||||||
|
|
||||||
|
x_nn_clipped = Tensor.round(src_x).clip(0, w_src - 1).cast('int')
|
||||||
|
y_nn_clipped = Tensor.round(src_y).clip(0, h_src - 1).cast('int')
|
||||||
|
idx = y_nn_clipped * (w_src + stride_pad) + x_nn_clipped
|
||||||
|
|
||||||
|
return src_flat[idx]
|
||||||
|
|
||||||
|
|
||||||
|
def frames_to_tensor(frames):
|
||||||
|
H = (frames.shape[0] * 2) // 3
|
||||||
|
W = frames.shape[1]
|
||||||
|
in_img1 = Tensor.cat(frames[0:H:2, 0::2],
|
||||||
|
frames[1:H:2, 0::2],
|
||||||
|
frames[0:H:2, 1::2],
|
||||||
|
frames[1:H:2, 1::2],
|
||||||
|
frames[H:H+H//4].reshape((H//2, W//2)),
|
||||||
|
frames[H+H//4:H+H//2].reshape((H//2, W//2)), dim=0).reshape((6, H//2, W//2))
|
||||||
|
return in_img1
|
||||||
|
|
||||||
|
|
||||||
|
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, M_inv):
|
||||||
|
# UV_SCALE @ M_inv @ UV_SCALE_INV simplifies to elementwise scaling
|
||||||
|
M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]])
|
||||||
|
# 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],
|
||||||
|
M_inv, (model_w, model_h),
|
||||||
|
(cam_h, cam_w), stride_pad).realize()
|
||||||
|
u = warp_perspective_tinygrad(uv[:cam_h//2, :cam_w:2].flatten(),
|
||||||
|
M_inv_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(),
|
||||||
|
M_inv_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))
|
||||||
|
tensor = frames_to_tensor(yuv)
|
||||||
|
return tensor
|
||||||
|
return frame_prepare_tinygrad
|
||||||
|
|
||||||
|
|
||||||
|
def make_input_queues(vision_input_shapes, policy_input_shapes, frame_skip):
|
||||||
|
img = vision_input_shapes['img'] # (1, 12, 128, 256)
|
||||||
|
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'] # (1, 25, 512)
|
||||||
|
dp = policy_input_shapes['desire_pulse'] # (1, 25, 8)
|
||||||
|
tc = policy_input_shapes['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),
|
||||||
|
}
|
||||||
|
input_queues = {
|
||||||
|
'img_q': Tensor.zeros(img_buf_shape, dtype='uint8').contiguous().realize(),
|
||||||
|
'big_img_q': Tensor.zeros(img_buf_shape, dtype='uint8').contiguous().realize(),
|
||||||
|
'feat_q': Tensor.zeros(frame_skip * (fb[1] - 1) + 1, fb[0], fb[2]).contiguous().realize(),
|
||||||
|
'desire_q': Tensor.zeros(frame_skip * dp[1], dp[0], dp[2]).contiguous().realize(),
|
||||||
|
**{k: Tensor(v, device='NPY').realize() for k, v 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_run_policy(vision_runner, policy_runner, nv12: NV12Frame, model_w, model_h,
|
||||||
|
vision_features_slice, frame_skip, 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):
|
||||||
|
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
|
||||||
|
|
||||||
|
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)
|
||||||
|
desire_buf = shift_and_sample(desire_q, desire.reshape(1, 1, -1), sample_desire_fn)
|
||||||
|
|
||||||
|
inputs = {'features_buffer': feat_buf, 'desire_pulse': desire_buf, 'traffic_convention': traffic_convention}
|
||||||
|
policy_out = next(iter(policy_runner(inputs).values())).cast('float32')
|
||||||
|
|
||||||
|
return vision_out, policy_out
|
||||||
|
return run_policy
|
||||||
|
|
||||||
|
|
||||||
|
def compile_modeld(nv12: NV12Frame, model_w, model_h, prepare_only, frame_skip,
|
||||||
|
vision_onnx, policy_onnx, pkl_path):
|
||||||
|
from get_model_metadata import metadata_path_for
|
||||||
|
|
||||||
|
print(f"Compiling combined policy JIT for {nv12.width}x{nv12.height} (prepare_only={prepare_only})...")
|
||||||
|
|
||||||
|
vision_runner = OnnxRunner(vision_onnx)
|
||||||
|
policy_runner = OnnxRunner(policy_onnx)
|
||||||
|
|
||||||
|
with open(metadata_path_for(vision_onnx), 'rb') as f:
|
||||||
|
vision_metadata = pickle.load(f)
|
||||||
|
vision_features_slice = vision_metadata['output_slices']['hidden_state']
|
||||||
|
vision_input_shapes = vision_metadata['input_shapes']
|
||||||
|
with open(metadata_path_for(policy_onnx), 'rb') as f:
|
||||||
|
policy_input_shapes = pickle.load(f)['input_shapes']
|
||||||
|
|
||||||
|
_run = make_run_policy(vision_runner, policy_runner, nv12, model_w, model_h,
|
||||||
|
vision_features_slice, frame_skip, prepare_only)
|
||||||
|
run_policy_jit = TinyJit(_run, prune=True)
|
||||||
|
|
||||||
|
N_RUNS = 3
|
||||||
|
SEED = 42
|
||||||
|
|
||||||
|
def random_inputs_run_fn(fn, seed, test_val=None, test_buffers=None, expect_match=True):
|
||||||
|
input_queues, npy = make_input_queues(vision_input_shapes, policy_input_shapes, frame_skip)
|
||||||
|
np.random.seed(seed)
|
||||||
|
Tensor.manual_seed(seed)
|
||||||
|
|
||||||
|
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()
|
||||||
|
for o in outs:
|
||||||
|
# .realize() not needed once jitted, but needed for unjitted fn
|
||||||
|
o.realize()
|
||||||
|
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")
|
||||||
|
|
||||||
|
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('run unjitted')
|
||||||
|
_, test_val, test_buffers = random_inputs_run_fn(_run, seed=SEED)
|
||||||
|
print('capture + replay')
|
||||||
|
run_policy_jit, _, _ = random_inputs_run_fn(run_policy_jit, SEED, test_val, test_buffers)
|
||||||
|
|
||||||
|
print('pickle round trip')
|
||||||
|
with open(pkl_path, "wb") as f:
|
||||||
|
pickle.dump(run_policy_jit, f)
|
||||||
|
print(f" Saved to {pkl_path}")
|
||||||
|
with open(pkl_path, "rb") as f:
|
||||||
|
run_policy_jit = pickle.load(f)
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_size(s):
|
||||||
|
w, h = s.lower().split('x')
|
||||||
|
return int(w), int(h)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_nv12(s):
|
||||||
|
parts = s.split(',')
|
||||||
|
assert len(parts) == len(NV12Frame._fields), \
|
||||||
|
f"--nv12 expects {','.join(NV12Frame._fields)} (got {s!r})"
|
||||||
|
return NV12Frame(*(int(x) for x in parts))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
p = argparse.ArgumentParser()
|
||||||
|
p.add_argument('--model-size', type=_parse_size, required=True, help='model input WxH')
|
||||||
|
p.add_argument('--nv12', type=_parse_nv12, required=True,
|
||||||
|
help=f'NV12 frame layout: {",".join(NV12Frame._fields)}')
|
||||||
|
p.add_argument('--vision-onnx', required=True)
|
||||||
|
p.add_argument('--policy-onnx', required=True)
|
||||||
|
p.add_argument('--output', required=True)
|
||||||
|
p.add_argument('--prepare-only', action='store_true')
|
||||||
|
p.add_argument('--frame-skip', type=int, required=True)
|
||||||
|
args = p.parse_args()
|
||||||
|
|
||||||
|
model_w, model_h = args.model_size
|
||||||
|
compile_modeld(args.nv12, model_w, model_h, args.prepare_only, args.frame_skip,
|
||||||
|
args.vision_onnx, args.policy_onnx, args.output)
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import time
|
|
||||||
import pickle
|
|
||||||
import numpy as np
|
|
||||||
from pathlib import Path
|
|
||||||
from tinygrad.tensor import Tensor
|
|
||||||
from tinygrad.helpers import Context
|
|
||||||
from tinygrad.device import Device
|
|
||||||
from tinygrad.engine.jit import TinyJit
|
|
||||||
|
|
||||||
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
|
||||||
from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE, DM_INPUT_SIZE
|
|
||||||
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
|
|
||||||
|
|
||||||
MODELS_DIR = Path(__file__).parent / 'models'
|
|
||||||
|
|
||||||
CAMERA_CONFIGS = [
|
|
||||||
(_ar_ox_fisheye.width, _ar_ox_fisheye.height), # tici: 1928x1208
|
|
||||||
(_os_fisheye.width, _os_fisheye.height), # mici: 1344x760
|
|
||||||
]
|
|
||||||
|
|
||||||
UV_SCALE_MATRIX = np.array([[0.5, 0, 0], [0, 0.5, 0], [0, 0, 1]], dtype=np.float32)
|
|
||||||
UV_SCALE_MATRIX_INV = np.linalg.inv(UV_SCALE_MATRIX)
|
|
||||||
|
|
||||||
IMG_BUFFER_SHAPE = (30, MEDMODEL_INPUT_SIZE[1] // 2, MEDMODEL_INPUT_SIZE[0] // 2)
|
|
||||||
|
|
||||||
|
|
||||||
def warp_pkl_path(w, h):
|
|
||||||
return MODELS_DIR / f'warp_{w}x{h}_tinygrad.pkl'
|
|
||||||
|
|
||||||
|
|
||||||
def dm_warp_pkl_path(w, h):
|
|
||||||
return MODELS_DIR / f'dm_warp_{w}x{h}_tinygrad.pkl'
|
|
||||||
|
|
||||||
|
|
||||||
def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad):
|
|
||||||
w_dst, h_dst = dst_shape
|
|
||||||
h_src, w_src = src_shape
|
|
||||||
|
|
||||||
x = Tensor.arange(w_dst).reshape(1, w_dst).expand(h_dst, w_dst).reshape(-1)
|
|
||||||
y = Tensor.arange(h_dst).reshape(h_dst, 1).expand(h_dst, w_dst).reshape(-1)
|
|
||||||
|
|
||||||
# inline 3x3 matmul as elementwise to avoid reduce op (enables fusion with gather)
|
|
||||||
src_x = M_inv[0, 0] * x + M_inv[0, 1] * y + M_inv[0, 2]
|
|
||||||
src_y = M_inv[1, 0] * x + M_inv[1, 1] * y + M_inv[1, 2]
|
|
||||||
src_w = M_inv[2, 0] * x + M_inv[2, 1] * y + M_inv[2, 2]
|
|
||||||
|
|
||||||
src_x = src_x / src_w
|
|
||||||
src_y = src_y / src_w
|
|
||||||
|
|
||||||
x_nn_clipped = Tensor.round(src_x).clip(0, w_src - 1).cast('int')
|
|
||||||
y_nn_clipped = Tensor.round(src_y).clip(0, h_src - 1).cast('int')
|
|
||||||
idx = y_nn_clipped * (w_src + stride_pad) + x_nn_clipped
|
|
||||||
|
|
||||||
return src_flat[idx]
|
|
||||||
|
|
||||||
|
|
||||||
def frames_to_tensor(frames, model_w, model_h):
|
|
||||||
H = (frames.shape[0] * 2) // 3
|
|
||||||
W = frames.shape[1]
|
|
||||||
in_img1 = Tensor.cat(frames[0:H:2, 0::2],
|
|
||||||
frames[1:H:2, 0::2],
|
|
||||||
frames[0:H:2, 1::2],
|
|
||||||
frames[1:H:2, 1::2],
|
|
||||||
frames[H:H+H//4].reshape((H//2, W//2)),
|
|
||||||
frames[H+H//4:H+H//2].reshape((H//2, W//2)), dim=0).reshape((6, H//2, W//2))
|
|
||||||
return in_img1
|
|
||||||
|
|
||||||
|
|
||||||
def make_frame_prepare(cam_w, cam_h, model_w, model_h):
|
|
||||||
stride, y_height, uv_height, _ = get_nv12_info(cam_w, cam_h)
|
|
||||||
uv_offset = stride * y_height
|
|
||||||
stride_pad = stride - cam_w
|
|
||||||
|
|
||||||
def frame_prepare_tinygrad(input_frame, M_inv):
|
|
||||||
# UV_SCALE @ M_inv @ UV_SCALE_INV simplifies to elementwise scaling
|
|
||||||
M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]])
|
|
||||||
# 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],
|
|
||||||
M_inv, (model_w, model_h),
|
|
||||||
(cam_h, cam_w), stride_pad).realize()
|
|
||||||
u = warp_perspective_tinygrad(uv[:cam_h//2, :cam_w:2].flatten(),
|
|
||||||
M_inv_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(),
|
|
||||||
M_inv_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))
|
|
||||||
tensor = frames_to_tensor(yuv, model_w, model_h)
|
|
||||||
return tensor
|
|
||||||
return frame_prepare_tinygrad
|
|
||||||
|
|
||||||
|
|
||||||
def make_update_img_input(frame_prepare, model_w, model_h):
|
|
||||||
def update_img_input_tinygrad(tensor, frame, M_inv):
|
|
||||||
M_inv = M_inv.to(Device.DEFAULT)
|
|
||||||
new_img = frame_prepare(frame, M_inv)
|
|
||||||
tensor.assign(tensor[6:].cat(new_img, dim=0).contiguous())
|
|
||||||
return Tensor.cat(tensor[:6], tensor[-6:], dim=0).contiguous().reshape(1, 12, model_h//2, model_w//2)
|
|
||||||
return update_img_input_tinygrad
|
|
||||||
|
|
||||||
|
|
||||||
def make_update_both_imgs(frame_prepare, model_w, model_h):
|
|
||||||
update_img = make_update_img_input(frame_prepare, model_w, model_h)
|
|
||||||
|
|
||||||
def update_both_imgs_tinygrad(calib_img_buffer, new_img, M_inv,
|
|
||||||
calib_big_img_buffer, new_big_img, M_inv_big):
|
|
||||||
calib_img_pair = update_img(calib_img_buffer, new_img, M_inv)
|
|
||||||
calib_big_img_pair = update_img(calib_big_img_buffer, new_big_img, M_inv_big)
|
|
||||||
return calib_img_pair, calib_big_img_pair
|
|
||||||
return update_both_imgs_tinygrad
|
|
||||||
|
|
||||||
|
|
||||||
def make_warp_dm(cam_w, cam_h, dm_w, dm_h):
|
|
||||||
stride, y_height, _, _ = get_nv12_info(cam_w, cam_h)
|
|
||||||
stride_pad = stride - cam_w
|
|
||||||
|
|
||||||
def warp_dm(input_frame, M_inv):
|
|
||||||
M_inv = M_inv.to(Device.DEFAULT)
|
|
||||||
result = warp_perspective_tinygrad(input_frame[:cam_h*stride], M_inv, (dm_w, dm_h), (cam_h, cam_w), stride_pad).reshape(-1, dm_h * dm_w)
|
|
||||||
return result
|
|
||||||
return warp_dm
|
|
||||||
|
|
||||||
|
|
||||||
def compile_modeld_warp(cam_w, cam_h):
|
|
||||||
model_w, model_h = MEDMODEL_INPUT_SIZE
|
|
||||||
_, _, _, yuv_size = get_nv12_info(cam_w, cam_h)
|
|
||||||
|
|
||||||
print(f"Compiling modeld warp for {cam_w}x{cam_h}...")
|
|
||||||
|
|
||||||
frame_prepare = make_frame_prepare(cam_w, cam_h, model_w, model_h)
|
|
||||||
update_both_imgs = make_update_both_imgs(frame_prepare, model_w, model_h)
|
|
||||||
update_img_jit = TinyJit(update_both_imgs, prune=True)
|
|
||||||
|
|
||||||
full_buffer = Tensor.zeros(IMG_BUFFER_SHAPE, dtype='uint8').contiguous().realize()
|
|
||||||
big_full_buffer = Tensor.zeros(IMG_BUFFER_SHAPE, dtype='uint8').contiguous().realize()
|
|
||||||
new_frame_np = np.random.randint(0, 256, yuv_size, dtype=np.uint8)
|
|
||||||
new_big_frame_np = np.random.randint(0, 256, yuv_size, dtype=np.uint8)
|
|
||||||
for i in range(10):
|
|
||||||
img_inputs = [full_buffer,
|
|
||||||
Tensor.from_blob(new_frame_np.ctypes.data, (yuv_size,), dtype='uint8').realize(),
|
|
||||||
Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')]
|
|
||||||
big_img_inputs = [big_full_buffer,
|
|
||||||
Tensor.from_blob(new_big_frame_np.ctypes.data, (yuv_size,), dtype='uint8').realize(),
|
|
||||||
Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')]
|
|
||||||
inputs = img_inputs + big_img_inputs
|
|
||||||
Device.default.synchronize()
|
|
||||||
|
|
||||||
st = time.perf_counter()
|
|
||||||
_ = update_img_jit(*inputs)
|
|
||||||
mt = time.perf_counter()
|
|
||||||
Device.default.synchronize()
|
|
||||||
et = time.perf_counter()
|
|
||||||
print(f" [{i+1}/10] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms")
|
|
||||||
|
|
||||||
pkl_path = warp_pkl_path(cam_w, cam_h)
|
|
||||||
with open(pkl_path, "wb") as f:
|
|
||||||
pickle.dump(update_img_jit, f)
|
|
||||||
print(f" Saved to {pkl_path}")
|
|
||||||
|
|
||||||
jit = pickle.load(open(pkl_path, "rb"))
|
|
||||||
jit(*inputs)
|
|
||||||
|
|
||||||
|
|
||||||
def compile_dm_warp(cam_w, cam_h):
|
|
||||||
dm_w, dm_h = DM_INPUT_SIZE
|
|
||||||
_, _, _, yuv_size = get_nv12_info(cam_w, cam_h)
|
|
||||||
|
|
||||||
print(f"Compiling DM warp for {cam_w}x{cam_h}...")
|
|
||||||
|
|
||||||
warp_dm = make_warp_dm(cam_w, cam_h, dm_w, dm_h)
|
|
||||||
warp_dm_jit = TinyJit(warp_dm, prune=True)
|
|
||||||
|
|
||||||
new_frame_np = np.random.randint(0, 256, yuv_size, dtype=np.uint8)
|
|
||||||
for i in range(10):
|
|
||||||
inputs = [Tensor.from_blob(new_frame_np.ctypes.data, (yuv_size,), dtype='uint8').realize(),
|
|
||||||
Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')]
|
|
||||||
Device.default.synchronize()
|
|
||||||
st = time.perf_counter()
|
|
||||||
warp_dm_jit(*inputs)
|
|
||||||
mt = time.perf_counter()
|
|
||||||
Device.default.synchronize()
|
|
||||||
et = time.perf_counter()
|
|
||||||
print(f" [{i+1}/10] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms")
|
|
||||||
|
|
||||||
pkl_path = dm_warp_pkl_path(cam_w, cam_h)
|
|
||||||
with open(pkl_path, "wb") as f:
|
|
||||||
pickle.dump(warp_dm_jit, f)
|
|
||||||
print(f" Saved to {pkl_path}")
|
|
||||||
|
|
||||||
|
|
||||||
def run_and_save_pickle():
|
|
||||||
for cam_w, cam_h in CAMERA_CONFIGS:
|
|
||||||
compile_modeld_warp(cam_w, cam_h)
|
|
||||||
compile_dm_warp(cam_w, cam_h)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_and_save_pickle()
|
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import os
|
import os
|
||||||
from openpilot.selfdrive.modeld.tinygrad_helpers import MODELS_DIR, set_tinygrad_backend_from_compiled_flags
|
from openpilot.selfdrive.modeld.helpers import MODELS_DIR, CompileConfig, set_tinygrad_backend_from_compiled_flags
|
||||||
set_tinygrad_backend_from_compiled_flags()
|
set_tinygrad_backend_from_compiled_flags()
|
||||||
|
|
||||||
# FIXME-SP: remove once we bump tg
|
|
||||||
from openpilot.system.hardware import TICI
|
|
||||||
os.environ['DEV'] = 'QCOM' if TICI else 'CPU'
|
|
||||||
|
|
||||||
from tinygrad.tensor import Tensor
|
from tinygrad.tensor import Tensor
|
||||||
import time
|
import time
|
||||||
import pickle
|
import pickle
|
||||||
@@ -32,7 +28,7 @@ class ModelState:
|
|||||||
inputs: dict[str, np.ndarray]
|
inputs: dict[str, np.ndarray]
|
||||||
output: np.ndarray
|
output: np.ndarray
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, cam_w: int, cam_h: int):
|
||||||
with open(METADATA_PATH, 'rb') as f:
|
with open(METADATA_PATH, 'rb') as f:
|
||||||
model_metadata = pickle.load(f)
|
model_metadata = pickle.load(f)
|
||||||
self.input_shapes = model_metadata['input_shapes']
|
self.input_shapes = model_metadata['input_shapes']
|
||||||
@@ -44,22 +40,18 @@ class ModelState:
|
|||||||
|
|
||||||
self.warp_inputs_np = {'transform': np.zeros((3,3), dtype=np.float32)}
|
self.warp_inputs_np = {'transform': np.zeros((3,3), dtype=np.float32)}
|
||||||
self.warp_inputs = {k: Tensor(v, device='NPY') for k,v in self.warp_inputs_np.items()}
|
self.warp_inputs = {k: Tensor(v, device='NPY') for k,v in self.warp_inputs_np.items()}
|
||||||
self.frame_buf_params = None
|
self.frame_buf_params = get_nv12_info(cam_w, cam_h)
|
||||||
self.tensor_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()}
|
self.tensor_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()}
|
||||||
self._blob_cache : dict[int, Tensor] = {}
|
self._blob_cache : dict[int, Tensor] = {}
|
||||||
self.image_warp = None
|
|
||||||
self.model_run = pickle.loads(read_file_chunked(str(MODEL_PKL_PATH)))
|
self.model_run = pickle.loads(read_file_chunked(str(MODEL_PKL_PATH)))
|
||||||
|
with open(CompileConfig(cam_w, cam_h, prefix='dm_', prepare_only=True).pkl_path, "rb") as f:
|
||||||
|
self.image_warp = pickle.load(f)
|
||||||
|
|
||||||
def run(self, buf: VisionBuf, calib: np.ndarray, transform: np.ndarray) -> tuple[np.ndarray, float]:
|
def run(self, buf: VisionBuf, calib: np.ndarray, transform: np.ndarray) -> tuple[np.ndarray, float]:
|
||||||
self.numpy_inputs['calib'][0,:] = calib
|
self.numpy_inputs['calib'][0,:] = calib
|
||||||
|
|
||||||
t1 = time.perf_counter()
|
t1 = time.perf_counter()
|
||||||
|
|
||||||
if self.image_warp is None:
|
|
||||||
self.frame_buf_params = get_nv12_info(buf.width, buf.height)
|
|
||||||
warp_path = MODELS_DIR / f'dm_warp_{buf.width}x{buf.height}_tinygrad.pkl'
|
|
||||||
with open(warp_path, "rb") as f:
|
|
||||||
self.image_warp = pickle.load(f)
|
|
||||||
ptr = buf.data.ctypes.data
|
ptr = buf.data.ctypes.data
|
||||||
# There is a ringbuffer of imgs, just cache tensors pointing to all of them
|
# There is a ringbuffer of imgs, just cache tensors pointing to all of them
|
||||||
if ptr not in self._blob_cache:
|
if ptr not in self._blob_cache:
|
||||||
@@ -83,7 +75,7 @@ def parse_model_output(model_output):
|
|||||||
face_descs = model_output[f'face_descs_{ds_suffix}']
|
face_descs = model_output[f'face_descs_{ds_suffix}']
|
||||||
parsed[f'face_descs_{ds_suffix}'] = face_descs[:, :-6]
|
parsed[f'face_descs_{ds_suffix}'] = face_descs[:, :-6]
|
||||||
parsed[f'face_descs_{ds_suffix}_std'] = safe_exp(face_descs[:, -6:])
|
parsed[f'face_descs_{ds_suffix}_std'] = safe_exp(face_descs[:, -6:])
|
||||||
for key in ['face_prob', 'left_eye_prob', 'right_eye_prob','left_blink_prob', 'right_blink_prob', 'sunglasses_prob', 'using_phone_prob']:
|
for key in ['face_prob', 'eyes_visible_prob', 'eyes_closed_prob', 'using_phone_prob']:
|
||||||
parsed[f'{key}_{ds_suffix}'] = sigmoid(model_output[f'{key}_{ds_suffix}'])
|
parsed[f'{key}_{ds_suffix}'] = sigmoid(model_output[f'{key}_{ds_suffix}'])
|
||||||
return parsed
|
return parsed
|
||||||
|
|
||||||
@@ -93,11 +85,8 @@ def fill_driver_data(msg, model_output, ds_suffix):
|
|||||||
msg.facePosition = model_output[f'face_descs_{ds_suffix}'][0, 3:5].tolist()
|
msg.facePosition = model_output[f'face_descs_{ds_suffix}'][0, 3:5].tolist()
|
||||||
msg.facePositionStd = model_output[f'face_descs_{ds_suffix}_std'][0, 3:5].tolist()
|
msg.facePositionStd = model_output[f'face_descs_{ds_suffix}_std'][0, 3:5].tolist()
|
||||||
msg.faceProb = model_output[f'face_prob_{ds_suffix}'][0, 0].item()
|
msg.faceProb = model_output[f'face_prob_{ds_suffix}'][0, 0].item()
|
||||||
msg.leftEyeProb = model_output[f'left_eye_prob_{ds_suffix}'][0, 0].item()
|
msg.eyesVisibleProb = model_output[f'eyes_visible_prob_{ds_suffix}'][0, 0].item()
|
||||||
msg.rightEyeProb = model_output[f'right_eye_prob_{ds_suffix}'][0, 0].item()
|
msg.eyesClosedProb = model_output[f'eyes_closed_prob_{ds_suffix}'][0, 0].item()
|
||||||
msg.leftBlinkProb = model_output[f'left_blink_prob_{ds_suffix}'][0, 0].item()
|
|
||||||
msg.rightBlinkProb = model_output[f'right_blink_prob_{ds_suffix}'][0, 0].item()
|
|
||||||
msg.sunglassesProb = model_output[f'sunglasses_prob_{ds_suffix}'][0, 0].item()
|
|
||||||
msg.phoneProb = model_output[f'using_phone_prob_{ds_suffix}'][0, 0].item()
|
msg.phoneProb = model_output[f'using_phone_prob_{ds_suffix}'][0, 0].item()
|
||||||
|
|
||||||
def get_driverstate_packet(model_output, frame_id: int, location_ts: int, exec_time: float, gpu_exec_time: float):
|
def get_driverstate_packet(model_output, frame_id: int, location_ts: int, exec_time: float, gpu_exec_time: float):
|
||||||
@@ -116,9 +105,6 @@ def get_driverstate_packet(model_output, frame_id: int, location_ts: int, exec_t
|
|||||||
def main():
|
def main():
|
||||||
config_realtime_process(7, 5)
|
config_realtime_process(7, 5)
|
||||||
|
|
||||||
model = ModelState()
|
|
||||||
cloudlog.warning("models loaded, dmonitoringmodeld starting")
|
|
||||||
|
|
||||||
cloudlog.warning("connecting to driver stream")
|
cloudlog.warning("connecting to driver stream")
|
||||||
vipc_client = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_DRIVER, True)
|
vipc_client = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_DRIVER, True)
|
||||||
while not vipc_client.connect(False):
|
while not vipc_client.connect(False):
|
||||||
@@ -126,6 +112,9 @@ def main():
|
|||||||
assert vipc_client.is_connected()
|
assert vipc_client.is_connected()
|
||||||
cloudlog.warning(f"connected with buffer size: {vipc_client.buffer_len}")
|
cloudlog.warning(f"connected with buffer size: {vipc_client.buffer_len}")
|
||||||
|
|
||||||
|
model = ModelState(vipc_client.width, vipc_client.height)
|
||||||
|
cloudlog.warning("models loaded, dmonitoringmodeld starting")
|
||||||
|
|
||||||
sm = SubMaster(["liveCalibration"])
|
sm = SubMaster(["liveCalibration"])
|
||||||
pm = PubMaster(["driverStateV2"])
|
pm = PubMaster(["driverStateV2"])
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ from typing import Any
|
|||||||
|
|
||||||
from tinygrad.nn.onnx import OnnxPBParser
|
from tinygrad.nn.onnx import OnnxPBParser
|
||||||
|
|
||||||
|
def metadata_path_for(onnx_path) -> pathlib.Path:
|
||||||
|
p = pathlib.Path(onnx_path)
|
||||||
|
return p.parent / (p.stem + '_metadata.pkl')
|
||||||
|
|
||||||
|
|
||||||
class MetadataOnnxPBParser(OnnxPBParser):
|
class MetadataOnnxPBParser(OnnxPBParser):
|
||||||
def _parse_ModelProto(self) -> dict:
|
def _parse_ModelProto(self) -> dict:
|
||||||
@@ -48,7 +52,7 @@ if __name__ == "__main__":
|
|||||||
'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"]),
|
||||||
}
|
}
|
||||||
|
|
||||||
metadata_path = model_path.parent / (model_path.stem + '_metadata.pkl')
|
metadata_path = metadata_path_for(model_path)
|
||||||
with open(metadata_path, 'wb') as f:
|
with open(metadata_path, 'wb') as f:
|
||||||
pickle.dump(metadata, f)
|
pickle.dump(metadata, f)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||||
|
|
||||||
|
MODELS_DIR = Path(__file__).resolve().parent / 'models'
|
||||||
|
COMPILED_FLAGS_PATH = MODELS_DIR / 'tg_compiled_flags.json'
|
||||||
|
|
||||||
|
|
||||||
|
def set_tinygrad_backend_from_compiled_flags() -> None:
|
||||||
|
if os.path.isfile(COMPILED_FLAGS_PATH):
|
||||||
|
with open(COMPILED_FLAGS_PATH) as f:
|
||||||
|
os.environ['DEV'] = str(json.load(f)['DEV'])
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CompileConfig:
|
||||||
|
cam_w: int
|
||||||
|
cam_h: int
|
||||||
|
prepare_only: bool
|
||||||
|
prefix: str
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pkl_path(self):
|
||||||
|
return str(MODELS_DIR / f'{self.prefix}{"warp_" if self.prepare_only else ""}{self.cam_w}x{self.cam_h}_tinygrad.pkl')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nv12(self):
|
||||||
|
return (self.cam_w, self.cam_h, *get_nv12_info(self.cam_w, self.cam_h))
|
||||||
+35
-122
@@ -1,12 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import os
|
import os
|
||||||
from openpilot.selfdrive.modeld.tinygrad_helpers import MODELS_DIR, set_tinygrad_backend_from_compiled_flags
|
from openpilot.selfdrive.modeld.helpers import MODELS_DIR, CompileConfig, set_tinygrad_backend_from_compiled_flags
|
||||||
set_tinygrad_backend_from_compiled_flags()
|
set_tinygrad_backend_from_compiled_flags()
|
||||||
|
|
||||||
# FIXME-SP: remove once we bump tg
|
|
||||||
from openpilot.system.hardware import TICI
|
|
||||||
os.environ['DEV'] = 'QCOM' if TICI else 'CPU'
|
|
||||||
|
|
||||||
USBGPU = "USBGPU" in os.environ
|
USBGPU = "USBGPU" in os.environ
|
||||||
if USBGPU:
|
if USBGPU:
|
||||||
os.environ['DEV'] = 'AMD'
|
os.environ['DEV'] = 'AMD'
|
||||||
@@ -30,6 +26,7 @@ from openpilot.common.transformations.model import get_warp_matrix
|
|||||||
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
|
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
|
||||||
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value, get_curvature_from_plan
|
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value, get_curvature_from_plan
|
||||||
from openpilot.selfdrive.modeld.parse_model_outputs import Parser
|
from openpilot.selfdrive.modeld.parse_model_outputs import Parser
|
||||||
|
from openpilot.selfdrive.modeld.compile_modeld import make_input_queues
|
||||||
from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState
|
from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState
|
||||||
from openpilot.common.file_chunker import read_file_chunked
|
from openpilot.common.file_chunker import read_file_chunked
|
||||||
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
|
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
|
||||||
@@ -41,17 +38,13 @@ from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
|
|||||||
PROCESS_NAME = "selfdrive.modeld.modeld"
|
PROCESS_NAME = "selfdrive.modeld.modeld"
|
||||||
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
||||||
|
|
||||||
VISION_PKL_PATH = MODELS_DIR / 'driving_vision_tinygrad.pkl'
|
|
||||||
VISION_METADATA_PATH = MODELS_DIR / 'driving_vision_metadata.pkl'
|
VISION_METADATA_PATH = MODELS_DIR / 'driving_vision_metadata.pkl'
|
||||||
POLICY_PKL_PATH = MODELS_DIR / 'driving_policy_tinygrad.pkl'
|
|
||||||
POLICY_METADATA_PATH = MODELS_DIR / 'driving_policy_metadata.pkl'
|
POLICY_METADATA_PATH = MODELS_DIR / 'driving_policy_metadata.pkl'
|
||||||
|
|
||||||
LAT_SMOOTH_SECONDS = 0.0
|
LAT_SMOOTH_SECONDS = 0.0
|
||||||
LONG_SMOOTH_SECONDS = 0.3
|
LONG_SMOOTH_SECONDS = 0.3
|
||||||
MIN_LAT_CONTROL_SPEED = 0.3
|
MIN_LAT_CONTROL_SPEED = 0.3
|
||||||
|
|
||||||
IMG_QUEUE_SHAPE = (6*(ModelConstants.MODEL_RUN_FREQ//ModelConstants.MODEL_CONTEXT_FREQ + 1), 128, 256)
|
|
||||||
assert IMG_QUEUE_SHAPE[0] == 30
|
|
||||||
|
|
||||||
|
|
||||||
def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action,
|
def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action,
|
||||||
@@ -86,108 +79,39 @@ class FrameMeta:
|
|||||||
if vipc is not None:
|
if vipc is not None:
|
||||||
self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof
|
self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof
|
||||||
|
|
||||||
class InputQueues:
|
|
||||||
def __init__ (self, model_fps, env_fps, n_frames_input):
|
|
||||||
assert env_fps % model_fps == 0
|
|
||||||
assert env_fps >= model_fps
|
|
||||||
self.model_fps = model_fps
|
|
||||||
self.env_fps = env_fps
|
|
||||||
self.n_frames_input = n_frames_input
|
|
||||||
|
|
||||||
self.dtypes = {}
|
|
||||||
self.shapes = {}
|
|
||||||
self.q = {}
|
|
||||||
|
|
||||||
def update_dtypes_and_shapes(self, input_dtypes, input_shapes) -> None:
|
|
||||||
self.dtypes.update(input_dtypes)
|
|
||||||
if self.env_fps == self.model_fps:
|
|
||||||
self.shapes.update(input_shapes)
|
|
||||||
else:
|
|
||||||
for k in input_shapes:
|
|
||||||
shape = list(input_shapes[k])
|
|
||||||
if 'img' in k:
|
|
||||||
n_channels = shape[1] // self.n_frames_input
|
|
||||||
shape[1] = (self.env_fps // self.model_fps + (self.n_frames_input - 1)) * n_channels
|
|
||||||
else:
|
|
||||||
shape[1] = (self.env_fps // self.model_fps) * shape[1]
|
|
||||||
self.shapes[k] = tuple(shape)
|
|
||||||
|
|
||||||
def reset(self) -> None:
|
|
||||||
self.q = {k: np.zeros(self.shapes[k], dtype=self.dtypes[k]) for k in self.dtypes.keys()}
|
|
||||||
|
|
||||||
def enqueue(self, inputs:dict[str, np.ndarray]) -> None:
|
|
||||||
for k in inputs.keys():
|
|
||||||
if inputs[k].dtype != self.dtypes[k]:
|
|
||||||
raise ValueError(f'supplied input <{k}({inputs[k].dtype})> has wrong dtype, expected {self.dtypes[k]}')
|
|
||||||
input_shape = list(self.shapes[k])
|
|
||||||
input_shape[1] = -1
|
|
||||||
single_input = inputs[k].reshape(tuple(input_shape))
|
|
||||||
sz = single_input.shape[1]
|
|
||||||
self.q[k][:,:-sz] = self.q[k][:,sz:]
|
|
||||||
self.q[k][:,-sz:] = single_input
|
|
||||||
|
|
||||||
def get(self, *names) -> dict[str, np.ndarray]:
|
|
||||||
if self.env_fps == self.model_fps:
|
|
||||||
return {k: self.q[k] for k in names}
|
|
||||||
else:
|
|
||||||
out = {}
|
|
||||||
for k in names:
|
|
||||||
shape = self.shapes[k]
|
|
||||||
if 'img' in k:
|
|
||||||
n_channels = shape[1] // (self.env_fps // self.model_fps + (self.n_frames_input - 1))
|
|
||||||
out[k] = np.concatenate([self.q[k][:, s:s+n_channels] for s in np.linspace(0, shape[1] - n_channels, self.n_frames_input, dtype=int)], axis=1)
|
|
||||||
elif 'pulse' in k:
|
|
||||||
# any pulse within interval counts
|
|
||||||
out[k] = self.q[k].reshape((shape[0], shape[1] * self.model_fps // self.env_fps, self.env_fps // self.model_fps, -1)).max(axis=2)
|
|
||||||
else:
|
|
||||||
idxs = np.arange(-1, -shape[1], -self.env_fps // self.model_fps)[::-1]
|
|
||||||
out[k] = self.q[k][:, idxs]
|
|
||||||
return out
|
|
||||||
|
|
||||||
class ModelState(ModelStateBase):
|
class ModelState(ModelStateBase):
|
||||||
inputs: dict[str, np.ndarray]
|
|
||||||
output: np.ndarray
|
|
||||||
prev_desire: np.ndarray # for tracking the rising edge of the pulse
|
prev_desire: np.ndarray # for tracking the rising edge of the pulse
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, cam_w: int, cam_h: int):
|
||||||
ModelStateBase.__init__(self)
|
ModelStateBase.__init__(self)
|
||||||
self.LAT_SMOOTH_SECONDS = LAT_SMOOTH_SECONDS
|
self.LAT_SMOOTH_SECONDS = LAT_SMOOTH_SECONDS
|
||||||
|
|
||||||
with open(VISION_METADATA_PATH, 'rb') as f:
|
with open(VISION_METADATA_PATH, 'rb') as f:
|
||||||
vision_metadata = pickle.load(f)
|
vision_metadata = pickle.load(f)
|
||||||
self.vision_input_shapes = vision_metadata['input_shapes']
|
self.vision_input_shapes = vision_metadata['input_shapes']
|
||||||
self.vision_input_names = list(self.vision_input_shapes.keys())
|
self.vision_input_names = list(self.vision_input_shapes.keys())
|
||||||
self.vision_output_slices = vision_metadata['output_slices']
|
self.vision_output_slices = vision_metadata['output_slices']
|
||||||
vision_output_size = vision_metadata['output_shapes']['outputs'][1]
|
|
||||||
|
|
||||||
with open(POLICY_METADATA_PATH, 'rb') as f:
|
with open(POLICY_METADATA_PATH, 'rb') as f:
|
||||||
policy_metadata = pickle.load(f)
|
policy_metadata = pickle.load(f)
|
||||||
self.policy_input_shapes = policy_metadata['input_shapes']
|
self.policy_input_shapes = policy_metadata['input_shapes']
|
||||||
self.policy_output_slices = policy_metadata['output_slices']
|
self.policy_output_slices = policy_metadata['output_slices']
|
||||||
policy_output_size = policy_metadata['output_shapes']['outputs'][1]
|
|
||||||
|
|
||||||
self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
|
self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
|
||||||
|
|
||||||
# policy inputs
|
self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
||||||
self.numpy_inputs = {k: np.zeros(self.policy_input_shapes[k], dtype=np.float32) for k in self.policy_input_shapes}
|
self.input_queues, self.npy = make_input_queues(self.vision_input_shapes, self.policy_input_shapes, self.frame_skip)
|
||||||
self.full_input_queues = InputQueues(ModelConstants.MODEL_CONTEXT_FREQ, ModelConstants.MODEL_RUN_FREQ, ModelConstants.N_FRAMES)
|
|
||||||
for k in ['desire_pulse', 'features_buffer']:
|
|
||||||
self.full_input_queues.update_dtypes_and_shapes({k: self.numpy_inputs[k].dtype}, {k: self.numpy_inputs[k].shape})
|
|
||||||
self.full_input_queues.reset()
|
|
||||||
|
|
||||||
self.img_queues = {'img': Tensor.zeros(IMG_QUEUE_SHAPE, dtype='uint8').contiguous().realize(),
|
|
||||||
'big_img': Tensor.zeros(IMG_QUEUE_SHAPE, dtype='uint8').contiguous().realize()}
|
|
||||||
self.full_frames : dict[str, Tensor] = {}
|
self.full_frames : dict[str, Tensor] = {}
|
||||||
self._blob_cache : dict[int, Tensor] = {}
|
self._blob_cache : dict[int, Tensor] = {}
|
||||||
self.transforms_np = {k: np.zeros((3,3), dtype=np.float32) for k in self.img_queues}
|
|
||||||
self.transforms = {k: Tensor(v, device='NPY').realize() for k, v in self.transforms_np.items()}
|
|
||||||
self.vision_output = np.zeros(vision_output_size, dtype=np.float32)
|
|
||||||
self.policy_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()}
|
|
||||||
self.policy_output = np.zeros(policy_output_size, dtype=np.float32)
|
|
||||||
self.parser = Parser()
|
self.parser = Parser()
|
||||||
self.frame_buf_params : dict[str, tuple[int, int, int, int]] = {}
|
self.frame_buf_params = {k: get_nv12_info(cam_w, cam_h) for k in ('img', 'big_img')}
|
||||||
self.update_imgs = None
|
self.run_policy = pickle.loads(read_file_chunked(CompileConfig(cam_w, cam_h, prefix='driving_', prepare_only=False).pkl_path))
|
||||||
self.vision_run = pickle.loads(read_file_chunked(str(VISION_PKL_PATH)))
|
self.warp_enqueue = pickle.loads(read_file_chunked(CompileConfig(cam_w, cam_h, prefix='driving_', prepare_only=True).pkl_path))
|
||||||
self.policy_run = pickle.loads(read_file_chunked(str(POLICY_PKL_PATH)))
|
self.warp_enqueue(
|
||||||
|
**self.input_queues,
|
||||||
|
frame=Tensor.zeros(self.frame_buf_params['img'][3], dtype='uint8').contiguous().realize(),
|
||||||
|
big_frame=Tensor.zeros(self.frame_buf_params['big_img'][3], dtype='uint8').contiguous().realize())
|
||||||
|
|
||||||
def slice_outputs(self, model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]:
|
def slice_outputs(self, model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]:
|
||||||
parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()}
|
parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()}
|
||||||
@@ -195,18 +119,6 @@ class ModelState(ModelStateBase):
|
|||||||
|
|
||||||
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
|
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:
|
inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None:
|
||||||
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
|
|
||||||
inputs['desire_pulse'][0] = 0
|
|
||||||
new_desire = np.where(inputs['desire_pulse'] - self.prev_desire > .99, inputs['desire_pulse'], 0)
|
|
||||||
self.prev_desire[:] = inputs['desire_pulse']
|
|
||||||
if self.update_imgs is None:
|
|
||||||
for key in bufs.keys():
|
|
||||||
w, h = bufs[key].width, bufs[key].height
|
|
||||||
self.frame_buf_params[key] = get_nv12_info(w, h)
|
|
||||||
warp_path = MODELS_DIR / f'warp_{w}x{h}_tinygrad.pkl'
|
|
||||||
with open(warp_path, "rb") as f:
|
|
||||||
self.update_imgs = pickle.load(f)
|
|
||||||
|
|
||||||
for key in bufs.keys():
|
for key in bufs.keys():
|
||||||
ptr = bufs[key].data.ctypes.data
|
ptr = bufs[key].data.ctypes.data
|
||||||
yuv_size = self.frame_buf_params[key][3]
|
yuv_size = self.frame_buf_params[key][3]
|
||||||
@@ -215,30 +127,31 @@ class ModelState(ModelStateBase):
|
|||||||
if cache_key not in self._blob_cache:
|
if cache_key not in self._blob_cache:
|
||||||
self._blob_cache[cache_key] = Tensor.from_blob(ptr, (yuv_size,), dtype='uint8')
|
self._blob_cache[cache_key] = Tensor.from_blob(ptr, (yuv_size,), dtype='uint8')
|
||||||
self.full_frames[key] = self._blob_cache[cache_key]
|
self.full_frames[key] = self._blob_cache[cache_key]
|
||||||
for key in bufs.keys():
|
|
||||||
self.transforms_np[key][:,:] = transforms[key][:,:]
|
|
||||||
|
|
||||||
out = self.update_imgs(self.img_queues['img'], self.full_frames['img'], self.transforms['img'],
|
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
|
||||||
self.img_queues['big_img'], self.full_frames['big_img'], self.transforms['big_img'])
|
inputs['desire_pulse'][0] = 0
|
||||||
vision_inputs = {'img': out[0], 'big_img': out[1]}
|
self.npy['desire'][:] = np.where(inputs['desire_pulse'] - self.prev_desire > .99, inputs['desire_pulse'], 0)
|
||||||
|
self.prev_desire[:] = inputs['desire_pulse']
|
||||||
|
self.npy['traffic_convention'][:] = inputs['traffic_convention']
|
||||||
|
self.npy['tfm'][:,:] = transforms['img'][:,:]
|
||||||
|
self.npy['big_tfm'][:,:] = transforms['big_img'][:,:]
|
||||||
|
|
||||||
if prepare_only:
|
if prepare_only:
|
||||||
|
self.warp_enqueue(**self.input_queues, frame=self.full_frames['img'], big_frame=self.full_frames['big_img'])
|
||||||
return None
|
return None
|
||||||
|
|
||||||
self.vision_output = self.vision_run(**vision_inputs).contiguous().realize().uop.base.buffer.numpy().flatten()
|
vision_output, policy_output = self.run_policy(
|
||||||
vision_outputs_dict = self.parser.parse_vision_outputs(self.slice_outputs(self.vision_output, self.vision_output_slices))
|
**self.input_queues, frame=self.full_frames['img'], big_frame=self.full_frames['big_img']
|
||||||
|
)
|
||||||
|
|
||||||
self.full_input_queues.enqueue({'features_buffer': vision_outputs_dict['hidden_state'], 'desire_pulse': new_desire})
|
vision_output = vision_output.numpy().flatten()
|
||||||
for k in ['desire_pulse', 'features_buffer']:
|
policy_output = policy_output.numpy().flatten()
|
||||||
self.numpy_inputs[k][:] = self.full_input_queues.get(k)[k]
|
vision_outputs_dict = self.parser.parse_vision_outputs(self.slice_outputs(vision_output, self.vision_output_slices))
|
||||||
self.numpy_inputs['traffic_convention'][:] = inputs['traffic_convention']
|
policy_outputs_dict = self.parser.parse_policy_outputs(self.slice_outputs(policy_output, self.policy_output_slices))
|
||||||
|
|
||||||
self.policy_output = self.policy_run(**self.policy_inputs).contiguous().realize().uop.base.buffer.numpy().flatten()
|
|
||||||
policy_outputs_dict = self.parser.parse_policy_outputs(self.slice_outputs(self.policy_output, self.policy_output_slices))
|
|
||||||
combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict}
|
combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict}
|
||||||
if SEND_RAW_PRED:
|
|
||||||
combined_outputs_dict['raw_pred'] = np.concatenate([self.vision_output.copy(), self.policy_output.copy()])
|
|
||||||
|
|
||||||
|
if SEND_RAW_PRED:
|
||||||
|
combined_outputs_dict['raw_pred'] = np.concatenate([vision_output.copy(), policy_output.copy()])
|
||||||
return combined_outputs_dict
|
return combined_outputs_dict
|
||||||
|
|
||||||
|
|
||||||
@@ -250,11 +163,6 @@ def main(demo=False):
|
|||||||
# also need to move the aux USB interrupts for good timings
|
# also need to move the aux USB interrupts for good timings
|
||||||
config_realtime_process(7, 54)
|
config_realtime_process(7, 54)
|
||||||
|
|
||||||
st = time.monotonic()
|
|
||||||
cloudlog.warning("loading model")
|
|
||||||
model = ModelState()
|
|
||||||
cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting")
|
|
||||||
|
|
||||||
# visionipc clients
|
# visionipc clients
|
||||||
while True:
|
while True:
|
||||||
available_streams = VisionIpcClient.available_streams("camerad", block=False)
|
available_streams = VisionIpcClient.available_streams("camerad", block=False)
|
||||||
@@ -278,6 +186,11 @@ def main(demo=False):
|
|||||||
if use_extra_client:
|
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})")
|
cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})")
|
||||||
|
|
||||||
|
st = time.monotonic()
|
||||||
|
cloudlog.warning("loading model")
|
||||||
|
model = ModelState(vipc_client_main.width, vipc_client_main.height)
|
||||||
|
cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting")
|
||||||
|
|
||||||
# messaging
|
# messaging
|
||||||
pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"])
|
pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"])
|
||||||
sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"])
|
sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"])
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7aff7ff1dc08bbaf562a8f77380ab5e5914f8557dba2f760d87e4d953f5604b0
|
oid sha256:2fd471febb6e973313ac0d0c6755f6410c1937ba92230b58a433761e8c883072
|
||||||
size 7307246
|
size 7364290
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import json
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
MODELS_DIR = Path(__file__).parent / 'models'
|
|
||||||
COMPILED_FLAGS_PATH = MODELS_DIR / 'tg_compiled_flags.json'
|
|
||||||
|
|
||||||
|
|
||||||
def set_tinygrad_backend_from_compiled_flags() -> None:
|
|
||||||
if os.path.isfile(COMPILED_FLAGS_PATH):
|
|
||||||
with open(COMPILED_FLAGS_PATH) as f:
|
|
||||||
os.environ['DEV'] = str(json.load(f)['DEV'])
|
|
||||||
@@ -32,9 +32,8 @@ class DRIVER_MONITOR_SETTINGS:
|
|||||||
self._DISTRACTED_PROMPT_TIME_TILL_TERMINAL = 6.
|
self._DISTRACTED_PROMPT_TIME_TILL_TERMINAL = 6.
|
||||||
|
|
||||||
self._FACE_THRESHOLD = 0.7
|
self._FACE_THRESHOLD = 0.7
|
||||||
self._EYE_THRESHOLD = 0.65
|
self._EYE_THRESHOLD = 0.5
|
||||||
self._SG_THRESHOLD = 0.9
|
self._BLINK_THRESHOLD = 0.5
|
||||||
self._BLINK_THRESHOLD = 0.865
|
|
||||||
self._PHONE_THRESH = 0.5
|
self._PHONE_THRESH = 0.5
|
||||||
|
|
||||||
self._POSE_PITCH_THRESHOLD = 0.3133
|
self._POSE_PITCH_THRESHOLD = 0.3133
|
||||||
@@ -111,11 +110,6 @@ class DriverProb:
|
|||||||
self.prob_offseter = RunningStatFilter(raw_priors=raw_priors, max_trackable=max_trackable)
|
self.prob_offseter = RunningStatFilter(raw_priors=raw_priors, max_trackable=max_trackable)
|
||||||
self.prob_calibrated = False
|
self.prob_calibrated = False
|
||||||
|
|
||||||
class DriverBlink:
|
|
||||||
def __init__(self):
|
|
||||||
self.left = 0.
|
|
||||||
self.right = 0.
|
|
||||||
|
|
||||||
|
|
||||||
# model output refers to center of undistorted+leveled image
|
# model output refers to center of undistorted+leveled image
|
||||||
EFL = 598.0 # focal length in K
|
EFL = 598.0 # focal length in K
|
||||||
@@ -150,7 +144,7 @@ class DriverMonitoring:
|
|||||||
wheelpos_filter_raw_priors = (self.settings._WHEELPOS_DATA_AVG, self.settings._WHEELPOS_DATA_VAR, 2)
|
wheelpos_filter_raw_priors = (self.settings._WHEELPOS_DATA_AVG, self.settings._WHEELPOS_DATA_VAR, 2)
|
||||||
self.wheelpos = DriverProb(raw_priors=wheelpos_filter_raw_priors, max_trackable=self.settings._WHEELPOS_MAX_COUNT)
|
self.wheelpos = DriverProb(raw_priors=wheelpos_filter_raw_priors, max_trackable=self.settings._WHEELPOS_MAX_COUNT)
|
||||||
self.pose = DriverPose(settings=self.settings)
|
self.pose = DriverPose(settings=self.settings)
|
||||||
self.blink = DriverBlink()
|
self.blink_prob = 0.
|
||||||
self.phone_prob = 0.
|
self.phone_prob = 0.
|
||||||
|
|
||||||
self.always_on = always_on
|
self.always_on = always_on
|
||||||
@@ -253,7 +247,7 @@ class DriverMonitoring:
|
|||||||
if pitch_error > pitch_threshold or yaw_error > yaw_threshold:
|
if pitch_error > pitch_threshold or yaw_error > yaw_threshold:
|
||||||
distracted_types.append(DistractedType.DISTRACTED_POSE)
|
distracted_types.append(DistractedType.DISTRACTED_POSE)
|
||||||
|
|
||||||
if (self.blink.left + self.blink.right)*0.5 > self.settings._BLINK_THRESHOLD:
|
if self.blink_prob > self.settings._BLINK_THRESHOLD:
|
||||||
distracted_types.append(DistractedType.DISTRACTED_BLINK)
|
distracted_types.append(DistractedType.DISTRACTED_BLINK)
|
||||||
|
|
||||||
if self.phone_prob > self.settings._PHONE_THRESH:
|
if self.phone_prob > self.settings._PHONE_THRESH:
|
||||||
@@ -294,10 +288,7 @@ class DriverMonitoring:
|
|||||||
self.pose.yaw_std = driver_data.faceOrientationStd[1]
|
self.pose.yaw_std = driver_data.faceOrientationStd[1]
|
||||||
model_std_max = max(self.pose.pitch_std, self.pose.yaw_std)
|
model_std_max = max(self.pose.pitch_std, self.pose.yaw_std)
|
||||||
self.pose.low_std = model_std_max < self.settings._POSESTD_THRESHOLD
|
self.pose.low_std = model_std_max < self.settings._POSESTD_THRESHOLD
|
||||||
self.blink.left = driver_data.leftBlinkProb * (driver_data.leftEyeProb > self.settings._EYE_THRESHOLD) \
|
self.blink_prob = driver_data.eyesClosedProb * (driver_data.eyesVisibleProb > self.settings._EYE_THRESHOLD)
|
||||||
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
|
|
||||||
self.blink.right = driver_data.rightBlinkProb * (driver_data.rightEyeProb > self.settings._EYE_THRESHOLD) \
|
|
||||||
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
|
|
||||||
self.phone_prob = driver_data.phoneProb
|
self.phone_prob = driver_data.phoneProb
|
||||||
|
|
||||||
self.distracted_types = self._get_distracted_types()
|
self.distracted_types = self._get_distracted_types()
|
||||||
|
|||||||
@@ -20,10 +20,8 @@ def make_msg(face_detected, distracted=False, model_uncertain=False):
|
|||||||
ds.leftDriverData.faceOrientation = [0., 0., 0.]
|
ds.leftDriverData.faceOrientation = [0., 0., 0.]
|
||||||
ds.leftDriverData.facePosition = [0., 0.]
|
ds.leftDriverData.facePosition = [0., 0.]
|
||||||
ds.leftDriverData.faceProb = 1. * face_detected
|
ds.leftDriverData.faceProb = 1. * face_detected
|
||||||
ds.leftDriverData.leftEyeProb = 1.
|
ds.leftDriverData.eyesVisibleProb = 1.
|
||||||
ds.leftDriverData.rightEyeProb = 1.
|
ds.leftDriverData.eyesClosedProb = 1. * distracted
|
||||||
ds.leftDriverData.leftBlinkProb = 1. * distracted
|
|
||||||
ds.leftDriverData.rightBlinkProb = 1. * distracted
|
|
||||||
ds.leftDriverData.faceOrientationStd = [1.*model_uncertain, 1.*model_uncertain, 1.*model_uncertain]
|
ds.leftDriverData.faceOrientationStd = [1.*model_uncertain, 1.*model_uncertain, 1.*model_uncertain]
|
||||||
ds.leftDriverData.facePositionStd = [1.*model_uncertain, 1.*model_uncertain]
|
ds.leftDriverData.facePositionStd = [1.*model_uncertain, 1.*model_uncertain]
|
||||||
# TODO: test both separately when e2e is used
|
# TODO: test both separately when e2e is used
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ def generate_report(proposed, master, tmp, commit):
|
|||||||
(lambda x: get_idx_if_non_empty(x.wheelOnRightProb), "wheelOnRightProb"),
|
(lambda x: get_idx_if_non_empty(x.wheelOnRightProb), "wheelOnRightProb"),
|
||||||
(lambda x: get_idx_if_non_empty(x.leftDriverData.faceProb), "leftDriverData.faceProb"),
|
(lambda x: get_idx_if_non_empty(x.leftDriverData.faceProb), "leftDriverData.faceProb"),
|
||||||
(lambda x: get_idx_if_non_empty(x.leftDriverData.faceOrientation, 0), "leftDriverData.faceOrientation0"),
|
(lambda x: get_idx_if_non_empty(x.leftDriverData.faceOrientation, 0), "leftDriverData.faceOrientation0"),
|
||||||
(lambda x: get_idx_if_non_empty(x.leftDriverData.leftBlinkProb), "leftDriverData.leftBlinkProb"),
|
(lambda x: get_idx_if_non_empty(x.leftDriverData.eyesClosedProb), "leftDriverData.eyesClosedProb"),
|
||||||
(lambda x: get_idx_if_non_empty(x.leftDriverData.phoneProb), "leftDriverData.phoneProb"),
|
(lambda x: get_idx_if_non_empty(x.leftDriverData.phoneProb), "leftDriverData.phoneProb"),
|
||||||
(lambda x: get_idx_if_non_empty(x.rightDriverData.faceProb), "rightDriverData.faceProb"),
|
(lambda x: get_idx_if_non_empty(x.rightDriverData.faceProb), "rightDriverData.faceProb"),
|
||||||
], "driverStateV2")
|
], "driverStateV2")
|
||||||
|
|||||||
@@ -39,8 +39,6 @@ class BaseDriverCameraDialog(Widget):
|
|||||||
self._eye_fill_texture = None
|
self._eye_fill_texture = None
|
||||||
self._eye_orange_texture = None
|
self._eye_orange_texture = None
|
||||||
self._eye_size = 74
|
self._eye_size = 74
|
||||||
self._glasses_texture = None
|
|
||||||
self._glasses_size = 171
|
|
||||||
|
|
||||||
self._load_eye_textures()
|
self._load_eye_textures()
|
||||||
|
|
||||||
@@ -154,8 +152,6 @@ class BaseDriverCameraDialog(Widget):
|
|||||||
self._eye_fill_texture = gui_app.texture("icons_mici/onroad/eye_fill.png", self._eye_size, self._eye_size)
|
self._eye_fill_texture = gui_app.texture("icons_mici/onroad/eye_fill.png", self._eye_size, self._eye_size)
|
||||||
if self._eye_orange_texture is None:
|
if self._eye_orange_texture is None:
|
||||||
self._eye_orange_texture = gui_app.texture("icons_mici/onroad/eye_orange.png", self._eye_size, self._eye_size)
|
self._eye_orange_texture = gui_app.texture("icons_mici/onroad/eye_orange.png", self._eye_size, self._eye_size)
|
||||||
if self._glasses_texture is None:
|
|
||||||
self._glasses_texture = gui_app.texture("icons_mici/onroad/glasses.png", self._glasses_size, self._glasses_size)
|
|
||||||
|
|
||||||
def _draw_face_detection(self, rect: rl.Rectangle):
|
def _draw_face_detection(self, rect: rl.Rectangle):
|
||||||
dm_state = ui_state.sm["driverMonitoringState"]
|
dm_state = ui_state.sm["driverMonitoringState"]
|
||||||
@@ -202,31 +198,21 @@ class BaseDriverCameraDialog(Widget):
|
|||||||
eye_offset_x = 10
|
eye_offset_x = 10
|
||||||
eye_offset_y = 10
|
eye_offset_y = 10
|
||||||
eye_spacing = self._eye_size + 15
|
eye_spacing = self._eye_size + 15
|
||||||
|
eyes_prob = driver_data.eyesVisibleProb
|
||||||
|
|
||||||
left_eye_x = rect.x + eye_offset_x
|
left_eye_x = rect.x + eye_offset_x
|
||||||
left_eye_y = rect.y + eye_offset_y
|
left_eye_y = rect.y + eye_offset_y
|
||||||
left_eye_prob = driver_data.leftEyeProb
|
|
||||||
|
|
||||||
right_eye_x = rect.x + eye_offset_x + eye_spacing
|
right_eye_x = rect.x + eye_offset_x + eye_spacing
|
||||||
right_eye_y = rect.y + eye_offset_y
|
right_eye_y = rect.y + eye_offset_y
|
||||||
right_eye_prob = driver_data.rightEyeProb
|
|
||||||
|
|
||||||
# Draw eyes with opacity based on probability
|
# Draw eyes with opacity based on probability
|
||||||
for eye_x, eye_y, eye_prob in [(left_eye_x, left_eye_y, left_eye_prob), (right_eye_x, right_eye_y, right_eye_prob)]:
|
fill_opacity = eyes_prob
|
||||||
fill_opacity = eye_prob
|
orange_opacity = 1.0 - eyes_prob
|
||||||
orange_opacity = 1.0 - eye_prob
|
for eye_x, eye_y in [(left_eye_x, left_eye_y), (right_eye_x, right_eye_y)]:
|
||||||
|
|
||||||
rl.draw_texture_v(self._eye_orange_texture, (eye_x, eye_y), rl.Color(255, 255, 255, int(255 * orange_opacity)))
|
rl.draw_texture_v(self._eye_orange_texture, (eye_x, eye_y), rl.Color(255, 255, 255, int(255 * orange_opacity)))
|
||||||
rl.draw_texture_v(self._eye_fill_texture, (eye_x, eye_y), rl.Color(255, 255, 255, int(255 * fill_opacity)))
|
rl.draw_texture_v(self._eye_fill_texture, (eye_x, eye_y), rl.Color(255, 255, 255, int(255 * fill_opacity)))
|
||||||
|
|
||||||
# Draw sunglasses indicator based on sunglasses probability
|
|
||||||
# Position glasses centered between the two eyes at top left
|
|
||||||
glasses_x = rect.x + eye_offset_x - 4
|
|
||||||
glasses_y = rect.y
|
|
||||||
glasses_pos = rl.Vector2(glasses_x, glasses_y)
|
|
||||||
glasses_prob = driver_data.sunglassesProb
|
|
||||||
rl.draw_texture_v(self._glasses_texture, glasses_pos, rl.Color(70, 80, 161, int(255 * glasses_prob)))
|
|
||||||
|
|
||||||
|
|
||||||
class DriverCameraDialog(NavWidget, BaseDriverCameraDialog):
|
class DriverCameraDialog(NavWidget, BaseDriverCameraDialog):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
#define SUNNYPILOT_VERSION "2026.002.000"
|
#define SUNNYPILOT_VERSION "2026.001.000"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import glob
|
import glob
|
||||||
|
from tinygrad import Device
|
||||||
|
|
||||||
Import('env', 'arch')
|
Import('env', 'arch')
|
||||||
lenv = env.Clone()
|
lenv = env.Clone()
|
||||||
@@ -21,10 +22,19 @@ if PC:
|
|||||||
if outputs:
|
if outputs:
|
||||||
lenv.Command(outputs, inputs, cmd)
|
lenv.Command(outputs, inputs, cmd)
|
||||||
|
|
||||||
tg_flags = {
|
available = set(Device.get_available_devices())
|
||||||
'larch64': 'DEV=QCOM FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0',
|
if 'CUDA' in available:
|
||||||
'Darwin': f'DEV=CPU THREADS=0 HOME={os.path.expanduser("~")}',
|
tg_backend = 'CUDA'
|
||||||
}.get(arch, 'DEV=CPU CPU_LLVM=1 THREADS=0')
|
tg_flags = f'DEV={tg_backend}'
|
||||||
|
elif 'QCOM' in available:
|
||||||
|
tg_backend = 'QCOM'
|
||||||
|
tg_flags = f'DEV={tg_backend} FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0'
|
||||||
|
else:
|
||||||
|
tg_backend = 'CPU' if arch == 'Darwin' else 'CPU:LLVM'
|
||||||
|
# THREADS=0 is need to prevent bug: https://github.com/tinygrad/tinygrad/issues/14689
|
||||||
|
tg_flags = f'DEV={tg_backend} THREADS=0'
|
||||||
|
|
||||||
|
mac_brew_string = f'HOME={os.path.expanduser("~")}' if arch == 'Darwin' else ''
|
||||||
|
|
||||||
image_flag = {
|
image_flag = {
|
||||||
'larch64': 'IMAGE=2',
|
'larch64': 'IMAGE=2',
|
||||||
@@ -38,7 +48,7 @@ def tg_compile(flags, model_name):
|
|||||||
return lenv.Command(
|
return lenv.Command(
|
||||||
out,
|
out,
|
||||||
[fn + ".onnx"] + tinygrad_files,
|
[fn + ".onnx"] + tinygrad_files,
|
||||||
f'{pythonpath_string} {flags} {image_flag} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {out}'
|
f'{pythonpath_string} {tg_flags} {mac_brew_string} {image_flag} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {out}'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Compile models
|
# Compile models
|
||||||
@@ -46,9 +56,9 @@ for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'drivin
|
|||||||
if File(f"models/{model_name}.onnx").exists():
|
if File(f"models/{model_name}.onnx").exists():
|
||||||
tg_compile(tg_flags, model_name)
|
tg_compile(tg_flags, model_name)
|
||||||
|
|
||||||
script_files = [File("warp.py"), File(Dir("#selfdrive/modeld").File("compile_warp.py").abspath)]
|
script_files = [File("warp.py")]
|
||||||
pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + ':' + env.Dir("#").abspath + '"'
|
pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + ':' + env.Dir("#").abspath + '"'
|
||||||
compile_warp_cmd = f'{pythonpath_string} {tg_flags} python3 -m sunnypilot.modeld_v2.warp'
|
compile_warp_cmd = f'{pythonpath_string} {tg_flags} {mac_brew_string} {image_flag} python3 -m sunnypilot.modeld_v2.warp'
|
||||||
|
|
||||||
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
|
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
|
||||||
warp_targets = []
|
warp_targets = []
|
||||||
|
|||||||
@@ -129,8 +129,7 @@ class ModelState(ModelStateBase):
|
|||||||
self.numpy_inputs[key][:] = inputs[key]
|
self.numpy_inputs[key][:] = inputs[key]
|
||||||
|
|
||||||
imgs_tensors = self.warp.process(bufs, transforms)
|
imgs_tensors = self.warp.process(bufs, transforms)
|
||||||
for name, tensor in imgs_tensors.items():
|
self.model_runner.update_vision_inputs(imgs_tensors)
|
||||||
self.model_runner.inputs[name] = tensor
|
|
||||||
self.model_runner.prepare_inputs(self.numpy_inputs)
|
self.model_runner.prepare_inputs(self.numpy_inputs)
|
||||||
|
|
||||||
if prepare_only:
|
if prepare_only:
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ import os
|
|||||||
os.environ['DEV'] = 'CPU'
|
os.environ['DEV'] = 'CPU'
|
||||||
import pytest
|
import pytest
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from openpilot.selfdrive.modeld.compile_warp import get_nv12_info, CAMERA_CONFIGS
|
from openpilot.sunnypilot.modeld_v2.warp import CAMERA_CONFIGS
|
||||||
from openpilot.sunnypilot.modeld_v2.warp import Warp, MODEL_W, MODEL_H
|
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||||
|
from openpilot.sunnypilot.modeld_v2.warp import Warp
|
||||||
|
from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE
|
||||||
|
MODEL_W, MODEL_H = MEDMODEL_INPUT_SIZE
|
||||||
|
|
||||||
VISION_NAME_PAIRS = [ # needed to account for supercombos input_imgs
|
VISION_NAME_PAIRS = [ # needed to account for supercombos input_imgs
|
||||||
('img', 'big_img'),
|
('img', 'big_img'),
|
||||||
|
|||||||
+120
-23
@@ -6,29 +6,128 @@ from tinygrad.tensor import Tensor
|
|||||||
from tinygrad.engine.jit import TinyJit
|
from tinygrad.engine.jit import TinyJit
|
||||||
from tinygrad.device import Device
|
from tinygrad.device import Device
|
||||||
|
|
||||||
|
from typing import NamedTuple
|
||||||
|
# https://github.com/tinygrad/tinygrad/issues/15682
|
||||||
|
from tinygrad.uop.ops import UOp, Ops
|
||||||
|
_orig = UOp.__reduce__
|
||||||
|
UOp.__reduce__ = lambda self: (UOp.unique, ()) if self.op is Ops.UNIQUE else _orig(self)
|
||||||
|
|
||||||
|
from tinygrad.helpers import Context
|
||||||
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||||
from openpilot.selfdrive.modeld.compile_warp import (
|
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
|
||||||
CAMERA_CONFIGS, MEDMODEL_INPUT_SIZE, make_frame_prepare, make_update_both_imgs,
|
|
||||||
warp_pkl_path,
|
class NV12Frame(NamedTuple):
|
||||||
)
|
cam_w: int
|
||||||
|
cam_h: int
|
||||||
|
stride: int
|
||||||
|
y_height: int
|
||||||
|
uv_height: int
|
||||||
|
size: int
|
||||||
|
|
||||||
|
UV_SCALE_MATRIX = np.array([[0.5, 0, 0], [0, 0.5, 0], [0, 0, 1]], dtype=np.float32)
|
||||||
|
UV_SCALE_MATRIX_INV = np.linalg.inv(UV_SCALE_MATRIX)
|
||||||
|
|
||||||
|
CAMERA_CONFIGS = [
|
||||||
|
(_ar_ox_fisheye.width, _ar_ox_fisheye.height), # tici: 1928x1208
|
||||||
|
(_os_fisheye.width, _os_fisheye.height), # mici: 1344x760
|
||||||
|
]
|
||||||
|
from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE
|
||||||
|
|
||||||
MODELS_DIR = Path(__file__).parent / 'models'
|
MODELS_DIR = Path(__file__).parent / 'models'
|
||||||
MODEL_W, MODEL_H = MEDMODEL_INPUT_SIZE
|
|
||||||
UPSTREAM_BUFFER_LENGTH = 5
|
UPSTREAM_BUFFER_LENGTH = 5
|
||||||
|
|
||||||
|
def warp_pkl_path(cam_w, cam_h):
|
||||||
|
return MODELS_DIR / f'warp_{cam_w}x{cam_h}_tinygrad.pkl'
|
||||||
|
|
||||||
|
def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad):
|
||||||
|
w_dst, h_dst = dst_shape
|
||||||
|
h_src, w_src = src_shape
|
||||||
|
|
||||||
|
x = Tensor.arange(w_dst).reshape(1, w_dst).expand(h_dst, w_dst).reshape(-1)
|
||||||
|
y = Tensor.arange(h_dst).reshape(h_dst, 1).expand(h_dst, w_dst).reshape(-1)
|
||||||
|
|
||||||
|
# inline 3x3 matmul as elementwise to avoid reduce op (enables fusion with gather)
|
||||||
|
src_x = M_inv[0, 0] * x + M_inv[0, 1] * y + M_inv[0, 2]
|
||||||
|
src_y = M_inv[1, 0] * x + M_inv[1, 1] * y + M_inv[1, 2]
|
||||||
|
src_w = M_inv[2, 0] * x + M_inv[2, 1] * y + M_inv[2, 2]
|
||||||
|
|
||||||
|
src_x = src_x / src_w
|
||||||
|
src_y = src_y / src_w
|
||||||
|
|
||||||
|
x_nn_clipped = Tensor.round(src_x).clip(0, w_src - 1).cast('int')
|
||||||
|
y_nn_clipped = Tensor.round(src_y).clip(0, h_src - 1).cast('int')
|
||||||
|
idx = y_nn_clipped * (w_src + stride_pad) + x_nn_clipped
|
||||||
|
|
||||||
|
return src_flat[idx]
|
||||||
|
|
||||||
|
def frames_to_tensor(frames, model_w, model_h):
|
||||||
|
H = (frames.shape[0] * 2) // 3
|
||||||
|
W = frames.shape[1]
|
||||||
|
in_img1 = Tensor.cat(frames[0:H:2, 0::2],
|
||||||
|
frames[1:H:2, 0::2],
|
||||||
|
frames[0:H:2, 1::2],
|
||||||
|
frames[1:H:2, 1::2],
|
||||||
|
frames[H:H+H//4].reshape((H//2, W//2)),
|
||||||
|
frames[H+H//4:H+H//2].reshape((H//2, W//2)), dim=0).reshape((6, H//2, W//2))
|
||||||
|
return in_img1
|
||||||
|
|
||||||
|
def make_frame_prepare(cam_w, cam_h, model_w, model_h):
|
||||||
|
stride, y_height, uv_height, _ = get_nv12_info(cam_w, cam_h)
|
||||||
|
uv_offset = stride * y_height
|
||||||
|
stride_pad = stride - cam_w
|
||||||
|
|
||||||
|
def frame_prepare_tinygrad(input_frame, M_inv):
|
||||||
|
# UV_SCALE @ M_inv @ UV_SCALE_INV simplifies to elementwise scaling
|
||||||
|
M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]])
|
||||||
|
# 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],
|
||||||
|
M_inv, (model_w, model_h),
|
||||||
|
(cam_h, cam_w), stride_pad).realize()
|
||||||
|
u = warp_perspective_tinygrad(uv[:cam_h//2, :cam_w:2].flatten(),
|
||||||
|
M_inv_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(),
|
||||||
|
M_inv_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))
|
||||||
|
tensor = frames_to_tensor(yuv, model_w, model_h)
|
||||||
|
return tensor
|
||||||
|
return frame_prepare_tinygrad
|
||||||
|
|
||||||
|
def make_update_img_input(frame_prepare, model_w, model_h):
|
||||||
|
def update_img_input_tinygrad(tensor, frame, M_inv):
|
||||||
|
M_inv = M_inv.to(Device.DEFAULT)
|
||||||
|
new_img = frame_prepare(frame, M_inv)
|
||||||
|
tensor.assign(tensor[6:].cat(new_img, dim=0).contiguous())
|
||||||
|
return tensor, Tensor.cat(tensor[:6], tensor[-6:], dim=0).contiguous().reshape(1, 12, model_h//2, model_w//2)
|
||||||
|
return update_img_input_tinygrad
|
||||||
|
|
||||||
|
def make_update_both_imgs(frame_prepare, model_w, model_h):
|
||||||
|
update_img = make_update_img_input(frame_prepare, model_w, model_h)
|
||||||
|
|
||||||
|
def update_both_imgs_tinygrad(calib_img_buffer, new_img, M_inv,
|
||||||
|
calib_big_img_buffer, new_big_img, M_inv_big):
|
||||||
|
r1, r2 = update_img(calib_img_buffer, new_img, M_inv)
|
||||||
|
w1, w2 = update_img(calib_big_img_buffer, new_big_img, M_inv_big)
|
||||||
|
return r1, r2, w1, w2
|
||||||
|
return update_both_imgs_tinygrad
|
||||||
|
|
||||||
|
|
||||||
def v2_warp_pkl_path(cam_w, cam_h, buffer_length):
|
def v2_warp_pkl_path(cam_w, cam_h, buffer_length):
|
||||||
return MODELS_DIR / f'warp_{cam_w}x{cam_h}_b{buffer_length}_tinygrad.pkl'
|
return MODELS_DIR / f'warp_{cam_w}x{cam_h}_b{buffer_length}_tinygrad.pkl'
|
||||||
|
|
||||||
|
|
||||||
def compile_v2_warp(cam_w, cam_h, buffer_length):
|
def compile_v2_warp(cam_w, cam_h, buffer_length, model_w=MEDMODEL_INPUT_SIZE[0], model_h=MEDMODEL_INPUT_SIZE[1], pkl_path=None):
|
||||||
_, _, _, yuv_size = get_nv12_info(cam_w, cam_h)
|
_, _, _, yuv_size = get_nv12_info(cam_w, cam_h)
|
||||||
img_buffer_shape = (buffer_length * 6, MODEL_H // 2, MODEL_W // 2)
|
img_buffer_shape = (buffer_length * 6, model_h // 2, model_w // 2)
|
||||||
|
|
||||||
print(f"Compiling v2 warp for {cam_w}x{cam_h} buffer_length={buffer_length}...")
|
print(f"Compiling v2 warp for {cam_w}x{cam_h} buffer_length={buffer_length}...")
|
||||||
|
|
||||||
frame_prepare = make_frame_prepare(cam_w, cam_h, MODEL_W, MODEL_H)
|
frame_prepare = make_frame_prepare(cam_w, cam_h, model_w, model_h)
|
||||||
update_both_imgs = make_update_both_imgs(frame_prepare, MODEL_W, MODEL_H)
|
update_both_imgs = make_update_both_imgs(frame_prepare, model_w, model_h)
|
||||||
update_img_jit = TinyJit(update_both_imgs, prune=True)
|
update_img_jit = TinyJit(update_both_imgs, prune=True)
|
||||||
|
|
||||||
full_buffer = Tensor.zeros(img_buffer_shape, dtype='uint8').contiguous().realize()
|
full_buffer = Tensor.zeros(img_buffer_shape, dtype='uint8').contiguous().realize()
|
||||||
@@ -46,25 +145,25 @@ def compile_v2_warp(cam_w, cam_h, buffer_length):
|
|||||||
Device.default.synchronize()
|
Device.default.synchronize()
|
||||||
|
|
||||||
st = time.perf_counter()
|
st = time.perf_counter()
|
||||||
_ = update_img_jit(*inputs)
|
update_img_jit(*inputs)
|
||||||
mt = time.perf_counter()
|
mt = time.perf_counter()
|
||||||
Device.default.synchronize()
|
Device.default.synchronize()
|
||||||
et = time.perf_counter()
|
et = time.perf_counter()
|
||||||
print(f" [{i+1}/10] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms")
|
print(f" [{i+1}/10] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms")
|
||||||
|
|
||||||
pkl_path = v2_warp_pkl_path(cam_w, cam_h, buffer_length)
|
if pkl_path is None:
|
||||||
|
pkl_path = v2_warp_pkl_path(cam_w, cam_h, buffer_length)
|
||||||
with open(pkl_path, "wb") as f:
|
with open(pkl_path, "wb") as f:
|
||||||
pickle.dump(update_img_jit, f)
|
pickle.dump(update_img_jit, f)
|
||||||
print(f" Saved to {pkl_path}")
|
print(f" Saved to {pkl_path}")
|
||||||
|
|
||||||
jit = pickle.load(open(pkl_path, "rb"))
|
|
||||||
jit(*inputs)
|
|
||||||
|
|
||||||
|
|
||||||
class Warp:
|
class Warp:
|
||||||
def __init__(self, buffer_length=2):
|
def __init__(self, buffer_length=2, model_w=MEDMODEL_INPUT_SIZE[0], model_h=MEDMODEL_INPUT_SIZE[1]):
|
||||||
self.buffer_length = buffer_length
|
self.buffer_length = buffer_length
|
||||||
self.img_buffer_shape = (buffer_length * 6, MODEL_H // 2, MODEL_W // 2)
|
self.model_w = model_w
|
||||||
|
self.model_h = model_h
|
||||||
|
self.img_buffer_shape = (buffer_length * 6, model_h // 2, model_w // 2)
|
||||||
|
|
||||||
self.jit_cache = {}
|
self.jit_cache = {}
|
||||||
self.full_buffers = {k: Tensor.zeros(self.img_buffer_shape, dtype='uint8').contiguous().realize() for k in ['img', 'big_img']}
|
self.full_buffers = {k: Tensor.zeros(self.img_buffer_shape, dtype='uint8').contiguous().realize() for k in ['img', 'big_img']}
|
||||||
@@ -92,8 +191,8 @@ class Warp:
|
|||||||
with open(upstream_pkl, 'rb') as f:
|
with open(upstream_pkl, 'rb') as f:
|
||||||
self.jit_cache[key] = pickle.load(f)
|
self.jit_cache[key] = pickle.load(f)
|
||||||
if key not in self.jit_cache:
|
if key not in self.jit_cache:
|
||||||
frame_prepare = make_frame_prepare(cam_w, cam_h, MODEL_W, MODEL_H)
|
frame_prepare = make_frame_prepare(cam_w, cam_h, self.model_w, self.model_h)
|
||||||
update_both_imgs = make_update_both_imgs(frame_prepare, MODEL_W, MODEL_H)
|
update_both_imgs = make_update_both_imgs(frame_prepare, self.model_w, self.model_h)
|
||||||
self.jit_cache[key] = TinyJit(update_both_imgs, prune=True)
|
self.jit_cache[key] = TinyJit(update_both_imgs, prune=True)
|
||||||
|
|
||||||
if key not in self._nv12_cache:
|
if key not in self._nv12_cache:
|
||||||
@@ -107,7 +206,7 @@ class Warp:
|
|||||||
if wide_ptr not in self._blob_cache:
|
if wide_ptr not in self._blob_cache:
|
||||||
self._blob_cache[wide_ptr] = Tensor.from_blob(wide_ptr, (yuv_size,), dtype='uint8')
|
self._blob_cache[wide_ptr] = Tensor.from_blob(wide_ptr, (yuv_size,), dtype='uint8')
|
||||||
road_blob = self._blob_cache[road_ptr]
|
road_blob = self._blob_cache[road_ptr]
|
||||||
wide_blob = self._blob_cache[wide_ptr] if wide_ptr != road_ptr else Tensor.from_blob(wide_ptr, (yuv_size,), dtype='uint8')
|
wide_blob = self._blob_cache[wide_ptr]
|
||||||
np.copyto(self.transforms_np['img'], transforms[road].reshape(3, 3))
|
np.copyto(self.transforms_np['img'], transforms[road].reshape(3, 3))
|
||||||
np.copyto(self.transforms_np['big_img'], transforms[wide].reshape(3, 3))
|
np.copyto(self.transforms_np['big_img'], transforms[wide].reshape(3, 3))
|
||||||
|
|
||||||
@@ -116,13 +215,11 @@ class Warp:
|
|||||||
self.full_buffers['img'], road_blob, self.transforms['img'],
|
self.full_buffers['img'], road_blob, self.transforms['img'],
|
||||||
self.full_buffers['big_img'], wide_blob, self.transforms['big_img'],
|
self.full_buffers['big_img'], wide_blob, self.transforms['big_img'],
|
||||||
)
|
)
|
||||||
out_road = res[0].realize()
|
return {road: res[1].realize(), wide: res[3].realize()}
|
||||||
out_wide = res[1].realize()
|
|
||||||
|
|
||||||
return {road: out_road, wide: out_wide}
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
for cam_w, cam_h in CAMERA_CONFIGS:
|
for cam_w, cam_h in CAMERA_CONFIGS:
|
||||||
|
compile_v2_warp(cam_w, cam_h, 5, pkl_path=warp_pkl_path(cam_w, cam_h))
|
||||||
for bl in [2, 5]:
|
for bl in [2, 5]:
|
||||||
compile_v2_warp(cam_w, cam_h, bl)
|
compile_v2_warp(cam_w, cam_h, bl)
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ class ModelCache:
|
|||||||
|
|
||||||
class ModelFetcher:
|
class ModelFetcher:
|
||||||
"""Handles fetching and caching of model data from remote source"""
|
"""Handles fetching and caching of model data from remote source"""
|
||||||
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v16.json"
|
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v18.json"
|
||||||
|
|
||||||
def __init__(self, params: Params):
|
def __init__(self, params: Params):
|
||||||
self.params = params
|
self.params = params
|
||||||
|
|||||||
@@ -132,6 +132,11 @@ class ModelRunner(ModularRunner):
|
|||||||
return list(self._model_data.input_shapes.keys())
|
return list(self._model_data.input_shapes.keys())
|
||||||
raise ValueError("Model data is not available. Ensure the model is loaded correctly.")
|
raise ValueError("Model data is not available. Ensure the model is loaded correctly.")
|
||||||
|
|
||||||
|
def update_vision_inputs(self, vision_inputs: dict) -> None:
|
||||||
|
"""Updates the vision inputs in the runner."""
|
||||||
|
for name, tensor in vision_inputs.items():
|
||||||
|
self.inputs[name] = tensor
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def prepare_inputs(self, numpy_inputs: NumpyDict) -> dict:
|
def prepare_inputs(self, numpy_inputs: NumpyDict) -> dict:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -46,14 +46,13 @@ class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTiny
|
|||||||
assert "/dev/kgsl-3d0" not in str(e), "Model was built on C3 or C3X, but is being loaded on PC"
|
assert "/dev/kgsl-3d0" not in str(e), "Model was built on C3 or C3X, but is being loaded on PC"
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# Map input names to their required dtype and device from the loaded model
|
|
||||||
self.input_to_dtype = {}
|
self.input_to_dtype = {}
|
||||||
self.input_to_device = {}
|
self.input_to_device = {}
|
||||||
for idx, name in enumerate(self.model_run.captured.expected_names):
|
for idx, name in enumerate(self.model_run.captured.expected_names):
|
||||||
info = self.model_run.captured.expected_input_info[idx]
|
info = self.model_run.captured.expected_input_info[idx]
|
||||||
self.input_to_dtype[name] = info[2] # dtype
|
self.input_to_dtype[name] = info[2]
|
||||||
self.input_to_device[name] = info[3] # device
|
self.input_to_device[name] = info[3]
|
||||||
self._policy_cached = False
|
self.inputs[name] = Tensor.zeros(*self.input_shapes[name], dtype=info[2], device=info[3]).realize()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def vision_input_names(self) -> list[str]:
|
def vision_input_names(self) -> list[str]:
|
||||||
@@ -62,22 +61,23 @@ class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTiny
|
|||||||
|
|
||||||
|
|
||||||
def prepare_policy_inputs(self, numpy_inputs: NumpyDict):
|
def prepare_policy_inputs(self, numpy_inputs: NumpyDict):
|
||||||
if not self._policy_cached:
|
for key, value in numpy_inputs.items():
|
||||||
for key, value in numpy_inputs.items():
|
if key in self.inputs:
|
||||||
self.inputs[key] = Tensor(value, device='NPY').realize()
|
self.inputs[key].assign(Tensor(value, device=self.inputs[key].device))
|
||||||
self._policy_cached = True
|
|
||||||
|
|
||||||
def prepare_inputs(self, numpy_inputs: NumpyDict) -> dict:
|
def prepare_inputs(self, numpy_inputs: NumpyDict) -> dict:
|
||||||
"""Prepares all vision and policy inputs for the model."""
|
"""Prepares all vision and policy inputs for the model."""
|
||||||
self.prepare_policy_inputs(numpy_inputs)
|
self.prepare_policy_inputs(numpy_inputs)
|
||||||
for key in self.vision_input_names:
|
|
||||||
if key in self.inputs:
|
|
||||||
self.inputs[key] = self.inputs[key].cast(self.input_to_dtype[key])
|
|
||||||
return self.inputs
|
return self.inputs
|
||||||
|
|
||||||
|
def update_vision_inputs(self, vision_inputs: dict[str, Tensor]):
|
||||||
|
for name, tensor in vision_inputs.items():
|
||||||
|
if name in self.inputs:
|
||||||
|
self.inputs[name].assign(tensor)
|
||||||
|
|
||||||
def _run_model(self) -> NumpyDict:
|
def _run_model(self) -> NumpyDict:
|
||||||
"""Runs the Tinygrad model inference and parses the outputs."""
|
"""Runs the Tinygrad model inference and parses the outputs."""
|
||||||
outputs = self.model_run(**self.inputs).contiguous().realize().uop.base.buffer.numpy().flatten()
|
outputs = self.model_run(**self.inputs).numpy().flatten()
|
||||||
return self._parse_outputs(outputs)
|
return self._parse_outputs(outputs)
|
||||||
|
|
||||||
def _parse_outputs(self, model_outputs: np.ndarray) -> NumpyDict:
|
def _parse_outputs(self, model_outputs: np.ndarray) -> NumpyDict:
|
||||||
|
|||||||
@@ -1,156 +0,0 @@
|
|||||||
"""
|
|
||||||
Copyright (c) 2021-, rav4kumar, 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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from cereal import custom
|
|
||||||
import numpy as np
|
|
||||||
from openpilot.common.realtime import DT_MDL
|
|
||||||
from openpilot.common.params import Params
|
|
||||||
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
|
|
||||||
|
|
||||||
AccelPersonality = custom.LongitudinalPlanSP.AccelerationPersonality
|
|
||||||
|
|
||||||
|
|
||||||
A_MAX_BP = [0.0, 4.0, 8.0, 16.0, 40.0]
|
|
||||||
A_MAX_V = {
|
|
||||||
AccelPersonality.eco: [1.20, 1.40, 1.20, 0.40, 0.08],
|
|
||||||
AccelPersonality.normal: [1.80, 1.80, 1.35, 0.50, 0.15],
|
|
||||||
AccelPersonality.sport: [2.20, 2.20, 1.60, 0.70, 0.25],
|
|
||||||
}
|
|
||||||
|
|
||||||
COAST_DRAG_BP = [0.0, 10.0, 25.0, 40.0]
|
|
||||||
COAST_DRAG_V = {
|
|
||||||
AccelPersonality.eco: [-0.03, -0.05, -0.08, -0.12],
|
|
||||||
AccelPersonality.normal: [-0.04, -0.07, -0.12, -0.18],
|
|
||||||
AccelPersonality.sport: [-0.06, -0.10, -0.18, -0.28],
|
|
||||||
}
|
|
||||||
|
|
||||||
A_MIN_FLOOR_BP = [0.0, 5.0, 15.0, 40.0]
|
|
||||||
A_MIN_FLOOR_V = {
|
|
||||||
AccelPersonality.eco: [-0.20, -0.35, -0.55, -0.50],
|
|
||||||
AccelPersonality.normal: [-0.25, -0.45, -0.75, -0.65],
|
|
||||||
AccelPersonality.sport: [-0.35, -0.65, -1.00, -0.95],
|
|
||||||
}
|
|
||||||
|
|
||||||
DEFICIT_TO_FLOOR = 8.5
|
|
||||||
COAST_DEADBAND = 1.0
|
|
||||||
RAMP_OFF_RANGE = 5.0
|
|
||||||
|
|
||||||
A_MIN_TIGHTEN_RATE = 0.6
|
|
||||||
A_MIN_RELAX_RATE = 0.9
|
|
||||||
A_MAX_RATE_UP = 1.5
|
|
||||||
A_MAX_RATE_DOWN = 0.6
|
|
||||||
|
|
||||||
MIN_MAX_GAP = 0.05
|
|
||||||
|
|
||||||
PARAM_REFRESH_FRAMES = max(1, int(1.0 / DT_MDL))
|
|
||||||
|
|
||||||
|
|
||||||
class AccelPersonalityController:
|
|
||||||
def __init__(self):
|
|
||||||
self.params = Params()
|
|
||||||
self.frame = 0
|
|
||||||
self._first = True
|
|
||||||
|
|
||||||
val = self.params.get('AccelPersonality')
|
|
||||||
self._personality = val if val is not None else AccelPersonality.normal
|
|
||||||
self._enabled = self.params.get_bool('AccelPersonalityEnabled')
|
|
||||||
|
|
||||||
self._v_cruise = 0.0
|
|
||||||
self._a_min = -0.05
|
|
||||||
self._a_max = 1.50
|
|
||||||
|
|
||||||
self._cache_v: float | None = None
|
|
||||||
self._cache_v_cruise: float | None = None
|
|
||||||
self._cache_a_min = self._a_min
|
|
||||||
self._cache_a_max = self._a_max
|
|
||||||
|
|
||||||
def update(self, sm=None):
|
|
||||||
self.frame += 1
|
|
||||||
self._cache_v = None
|
|
||||||
self._cache_v_cruise = None
|
|
||||||
|
|
||||||
if sm is not None:
|
|
||||||
vc = sm['carState'].vCruise
|
|
||||||
self._v_cruise = float(vc) * (1000.0 / 3600.0) if vc != V_CRUISE_UNSET else 0.0
|
|
||||||
|
|
||||||
if self.frame % PARAM_REFRESH_FRAMES == 0:
|
|
||||||
val = self.params.get('AccelPersonality')
|
|
||||||
self._personality = val if val is not None else AccelPersonality.normal
|
|
||||||
new_enabled = self.params.get_bool('AccelPersonalityEnabled')
|
|
||||||
if new_enabled and not self._enabled:
|
|
||||||
self._first = True
|
|
||||||
self._enabled = new_enabled
|
|
||||||
|
|
||||||
def get_accel_personality(self) -> int:
|
|
||||||
return int(self._personality)
|
|
||||||
|
|
||||||
def is_enabled(self) -> bool:
|
|
||||||
return self._enabled
|
|
||||||
|
|
||||||
def get_accel_limits(self, v_ego: float) -> tuple[float, float]:
|
|
||||||
v_ego = max(0.0, v_ego)
|
|
||||||
if (self._cache_v is not None
|
|
||||||
and abs(self._cache_v - v_ego) < 0.01
|
|
||||||
and self._cache_v_cruise == self._v_cruise):
|
|
||||||
return self._cache_a_min, self._cache_a_max
|
|
||||||
self._cache_a_min, self._cache_a_max = self._step(v_ego)
|
|
||||||
self._cache_v = v_ego
|
|
||||||
self._cache_v_cruise = self._v_cruise
|
|
||||||
return self._cache_a_min, self._cache_a_max
|
|
||||||
|
|
||||||
def get_min_accel(self, v_ego: float) -> float:
|
|
||||||
return self.get_accel_limits(v_ego)[0]
|
|
||||||
|
|
||||||
def get_max_accel(self, v_ego: float) -> float:
|
|
||||||
return self.get_accel_limits(v_ego)[1]
|
|
||||||
|
|
||||||
def _ramp_off(self, v_ego: float) -> float:
|
|
||||||
if self._v_cruise <= 0.0:
|
|
||||||
return 1.0
|
|
||||||
return float(np.clip((self._v_cruise - v_ego) / RAMP_OFF_RANGE, 0.0, 1.0))
|
|
||||||
|
|
||||||
def _target_max(self, v_ego: float) -> float:
|
|
||||||
base = float(np.interp(v_ego, A_MAX_BP, A_MAX_V[self._personality]))
|
|
||||||
return base * self._ramp_off(v_ego)
|
|
||||||
|
|
||||||
def _target_min(self, v_ego: float) -> float:
|
|
||||||
coast = float(np.interp(v_ego, COAST_DRAG_BP, COAST_DRAG_V[self._personality]))
|
|
||||||
if self._v_cruise <= 0.0 or v_ego >= self._v_cruise:
|
|
||||||
return coast
|
|
||||||
floor = float(np.interp(v_ego, A_MIN_FLOOR_BP, A_MIN_FLOOR_V[self._personality]))
|
|
||||||
deficit = self._v_cruise - v_ego
|
|
||||||
t = float(np.clip(deficit / DEFICIT_TO_FLOOR, 0.0, 1.0)) ** 1.5
|
|
||||||
return coast + t * (floor - coast)
|
|
||||||
|
|
||||||
def _apply_coast_deadband(self, v_ego: float, t_min: float, t_max: float) -> tuple[float, float]:
|
|
||||||
if self._v_cruise <= 0.0 or abs(v_ego - self._v_cruise) >= COAST_DEADBAND:
|
|
||||||
return t_min, t_max
|
|
||||||
coast = float(np.interp(v_ego, COAST_DRAG_BP, COAST_DRAG_V[self._personality]))
|
|
||||||
return coast, max(0.05, t_max * 0.25)
|
|
||||||
|
|
||||||
def _rate_limit(self, last: float, target: float, rate_down: float, rate_up: float) -> float:
|
|
||||||
rate = rate_up if target > last else rate_down
|
|
||||||
step = rate * DT_MDL
|
|
||||||
return float(np.clip(target, last - step, last + step))
|
|
||||||
|
|
||||||
def _step(self, v_ego: float) -> tuple[float, float]:
|
|
||||||
t_max = self._target_max(v_ego)
|
|
||||||
t_min = self._target_min(v_ego)
|
|
||||||
t_min, t_max = self._apply_coast_deadband(v_ego, t_min, t_max)
|
|
||||||
|
|
||||||
if self._first:
|
|
||||||
self._a_min, self._a_max = t_min, t_max
|
|
||||||
self._first = False
|
|
||||||
return self._a_min, self._a_max
|
|
||||||
|
|
||||||
new_min = self._rate_limit(self._a_min, t_min, rate_down=A_MIN_TIGHTEN_RATE, rate_up=A_MIN_RELAX_RATE)
|
|
||||||
new_max = self._rate_limit(self._a_max, t_max, rate_down=A_MAX_RATE_DOWN, rate_up=A_MAX_RATE_UP)
|
|
||||||
|
|
||||||
new_min = min(new_min, new_max - MIN_MAX_GAP)
|
|
||||||
|
|
||||||
self._a_min, self._a_max = new_min, new_max
|
|
||||||
return self._a_min, self._a_max
|
|
||||||
@@ -17,9 +17,6 @@ from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_resolve
|
|||||||
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
|
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
|
||||||
from openpilot.sunnypilot.models.helpers import get_active_bundle
|
from openpilot.sunnypilot.models.helpers import get_active_bundle
|
||||||
|
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller import AccelPersonalityController
|
|
||||||
from opendbc.car.interfaces import ACCEL_MIN
|
|
||||||
|
|
||||||
DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimentalControlState
|
DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimentalControlState
|
||||||
LongitudinalPlanSource = custom.LongitudinalPlanSP.LongitudinalPlanSource
|
LongitudinalPlanSource = custom.LongitudinalPlanSP.LongitudinalPlanSource
|
||||||
|
|
||||||
@@ -29,7 +26,6 @@ class LongitudinalPlannerSP:
|
|||||||
self.events_sp = EventsSP()
|
self.events_sp = EventsSP()
|
||||||
self.resolver = SpeedLimitResolver()
|
self.resolver = SpeedLimitResolver()
|
||||||
self.dec = DynamicExperimentalController(CP, mpc)
|
self.dec = DynamicExperimentalController(CP, mpc)
|
||||||
self.accel_controller = AccelPersonalityController()
|
|
||||||
self.scc = SmartCruiseControl()
|
self.scc = SmartCruiseControl()
|
||||||
self.resolver = SpeedLimitResolver()
|
self.resolver = SpeedLimitResolver()
|
||||||
self.sla = SpeedLimitAssist(CP, CP_SP)
|
self.sla = SpeedLimitAssist(CP, CP_SP)
|
||||||
@@ -47,17 +43,6 @@ class LongitudinalPlannerSP:
|
|||||||
|
|
||||||
return experimental_mode and self.dec.mode() == "blended"
|
return experimental_mode and self.dec.mode() == "blended"
|
||||||
|
|
||||||
def get_accel_clip(self, v_ego: float) -> list[float] | None:
|
|
||||||
if not self.accel_controller.is_enabled():
|
|
||||||
return None
|
|
||||||
a_max = self.accel_controller.get_max_accel(v_ego)
|
|
||||||
return [ACCEL_MIN, max(ACCEL_MIN, a_max)]
|
|
||||||
|
|
||||||
def get_cruise_min_accel(self, v_ego: float) -> float | None:
|
|
||||||
if self.accel_controller.is_enabled():
|
|
||||||
return self.accel_controller.get_min_accel(v_ego)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def update_targets(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise: float) -> tuple[float, float]:
|
def update_targets(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise: float) -> tuple[float, float]:
|
||||||
CS = sm['carState']
|
CS = sm['carState']
|
||||||
v_cruise_cluster_kph = min(CS.vCruiseCluster, V_CRUISE_MAX)
|
v_cruise_cluster_kph = min(CS.vCruiseCluster, V_CRUISE_MAX)
|
||||||
@@ -92,7 +77,6 @@ class LongitudinalPlannerSP:
|
|||||||
self.events_sp.clear()
|
self.events_sp.clear()
|
||||||
self.dec.update(sm)
|
self.dec.update(sm)
|
||||||
self.e2e_alerts_helper.update(sm, self.events_sp)
|
self.e2e_alerts_helper.update(sm, self.events_sp)
|
||||||
self.accel_controller.update(sm)
|
|
||||||
|
|
||||||
def publish_longitudinal_plan_sp(self, sm: messaging.SubMaster, pm: messaging.PubMaster) -> None:
|
def publish_longitudinal_plan_sp(self, sm: messaging.SubMaster, pm: messaging.PubMaster) -> None:
|
||||||
plan_sp_send = messaging.new_message('longitudinalPlanSP')
|
plan_sp_send = messaging.new_message('longitudinalPlanSP')
|
||||||
@@ -111,8 +95,6 @@ class LongitudinalPlannerSP:
|
|||||||
dec.enabled = self.dec.enabled()
|
dec.enabled = self.dec.enabled()
|
||||||
dec.active = self.dec.active()
|
dec.active = self.dec.active()
|
||||||
|
|
||||||
longitudinalPlanSP.accelPersonality = int(self.accel_controller.get_accel_personality())
|
|
||||||
|
|
||||||
# Smart Cruise Control
|
# Smart Cruise Control
|
||||||
smartCruiseControl = longitudinalPlanSP.smartCruiseControl
|
smartCruiseControl = longitudinalPlanSP.smartCruiseControl
|
||||||
# Vision Control
|
# Vision Control
|
||||||
|
|||||||
@@ -1,147 +0,0 @@
|
|||||||
"""
|
|
||||||
Copyright (c) 2021-, rav4kumar, 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.
|
|
||||||
|
|
||||||
Coverage for AccelPersonalityController:
|
|
||||||
- live param flip via auto-refresh (no Python set_enabled() call needed)
|
|
||||||
- V_CRUISE_UNSET guard
|
|
||||||
- enable-transition snap to fresh target
|
|
||||||
- per-personality accel limit deltas vs stock get_max_accel
|
|
||||||
"""
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from cereal import custom
|
|
||||||
|
|
||||||
from openpilot.common.params import Params
|
|
||||||
from opendbc.car.interfaces import ACCEL_MIN
|
|
||||||
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
|
|
||||||
from openpilot.selfdrive.controls.lib.longitudinal_planner import get_max_accel as stock_get_max_accel
|
|
||||||
|
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller import (
|
|
||||||
AccelPersonalityController,
|
|
||||||
PARAM_REFRESH_FRAMES,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
AccelPersonality = custom.LongitudinalPlanSP.AccelerationPersonality
|
|
||||||
|
|
||||||
|
|
||||||
class FakeCarState:
|
|
||||||
def __init__(self, v_cruise=30.0):
|
|
||||||
self.vCruise = v_cruise
|
|
||||||
|
|
||||||
|
|
||||||
class FakeSM:
|
|
||||||
def __init__(self, v_cruise=30.0):
|
|
||||||
self._data = {'carState': FakeCarState(v_cruise)}
|
|
||||||
|
|
||||||
def __getitem__(self, k):
|
|
||||||
return self._data[k]
|
|
||||||
|
|
||||||
|
|
||||||
def _print_table(title, header, rows):
|
|
||||||
print(f"\n--- {title} ---")
|
|
||||||
print(" | ".join(f"{h:>12}" for h in header))
|
|
||||||
print("-" * (15 * len(header)))
|
|
||||||
for row in rows:
|
|
||||||
print(" | ".join(f"{v:>12.3f}" if isinstance(v, float) else f"{v:>12}" for v in row))
|
|
||||||
|
|
||||||
|
|
||||||
class TestAccelLiveFlip:
|
|
||||||
def test_enable_via_param(self):
|
|
||||||
Params().put_bool('AccelPersonalityEnabled', False)
|
|
||||||
c = AccelPersonalityController()
|
|
||||||
assert not c.is_enabled()
|
|
||||||
Params().put_bool('AccelPersonalityEnabled', True)
|
|
||||||
for _ in range(PARAM_REFRESH_FRAMES + 1):
|
|
||||||
c.update(FakeSM())
|
|
||||||
assert c.is_enabled()
|
|
||||||
|
|
||||||
def test_disable_via_param(self):
|
|
||||||
Params().put_bool('AccelPersonalityEnabled', True)
|
|
||||||
c = AccelPersonalityController()
|
|
||||||
assert c.is_enabled()
|
|
||||||
Params().put_bool('AccelPersonalityEnabled', False)
|
|
||||||
for _ in range(PARAM_REFRESH_FRAMES + 1):
|
|
||||||
c.update(FakeSM())
|
|
||||||
assert not c.is_enabled()
|
|
||||||
|
|
||||||
def test_personality_change_via_param(self):
|
|
||||||
Params().put('AccelPersonality', AccelPersonality.normal)
|
|
||||||
c = AccelPersonalityController()
|
|
||||||
assert c.get_accel_personality() == AccelPersonality.normal
|
|
||||||
Params().put('AccelPersonality', AccelPersonality.sport)
|
|
||||||
for _ in range(PARAM_REFRESH_FRAMES + 1):
|
|
||||||
c.update(FakeSM())
|
|
||||||
assert c.get_accel_personality() == AccelPersonality.sport
|
|
||||||
|
|
||||||
def test_refresh_boundary_below_threshold(self):
|
|
||||||
Params().put_bool('AccelPersonalityEnabled', False)
|
|
||||||
c = AccelPersonalityController()
|
|
||||||
Params().put_bool('AccelPersonalityEnabled', True)
|
|
||||||
for _ in range(PARAM_REFRESH_FRAMES - 1):
|
|
||||||
c.update(FakeSM())
|
|
||||||
assert not c.is_enabled()
|
|
||||||
|
|
||||||
def test_enable_transition_snaps_to_target(self):
|
|
||||||
Params().put_bool('AccelPersonalityEnabled', True)
|
|
||||||
Params().put('AccelPersonality', AccelPersonality.sport)
|
|
||||||
c = AccelPersonalityController()
|
|
||||||
for _ in range(PARAM_REFRESH_FRAMES + 1):
|
|
||||||
c.update(FakeSM(v_cruise=35.0))
|
|
||||||
c.get_accel_limits(25.0)
|
|
||||||
|
|
||||||
Params().put_bool('AccelPersonalityEnabled', False)
|
|
||||||
for _ in range(PARAM_REFRESH_FRAMES + 1):
|
|
||||||
c.update(FakeSM(v_cruise=35.0))
|
|
||||||
assert not c.is_enabled()
|
|
||||||
|
|
||||||
Params().put('AccelPersonality', AccelPersonality.eco)
|
|
||||||
Params().put_bool('AccelPersonalityEnabled', True)
|
|
||||||
for _ in range(PARAM_REFRESH_FRAMES + 1):
|
|
||||||
c.update(FakeSM(v_cruise=35.0))
|
|
||||||
assert c._first
|
|
||||||
|
|
||||||
def test_vcruise_unset_treated_as_zero(self):
|
|
||||||
Params().put_bool('AccelPersonalityEnabled', True)
|
|
||||||
c = AccelPersonalityController()
|
|
||||||
c.update(FakeSM(v_cruise=V_CRUISE_UNSET))
|
|
||||||
assert c._v_cruise == 0.0
|
|
||||||
|
|
||||||
|
|
||||||
class TestAccelUsageDiff:
|
|
||||||
def test_accel_clip_per_personality(self, capsys):
|
|
||||||
rows = []
|
|
||||||
speeds = [3.0, 10.0, 20.0, 30.0]
|
|
||||||
personalities = [
|
|
||||||
('eco', AccelPersonality.eco),
|
|
||||||
('normal', AccelPersonality.normal),
|
|
||||||
('sport', AccelPersonality.sport),
|
|
||||||
]
|
|
||||||
|
|
||||||
Params().put_bool('AccelPersonalityEnabled', True)
|
|
||||||
sm = FakeSM(v_cruise=35.0)
|
|
||||||
|
|
||||||
any_delta = False
|
|
||||||
for label, p in personalities:
|
|
||||||
Params().put('AccelPersonality', p)
|
|
||||||
c = AccelPersonalityController()
|
|
||||||
c.update(sm)
|
|
||||||
for v_ego in speeds:
|
|
||||||
stock_hi = float(stock_get_max_accel(v_ego))
|
|
||||||
c_lo, c_hi = c.get_accel_limits(v_ego)
|
|
||||||
delta_hi = c_hi - stock_hi
|
|
||||||
delta_lo = c_lo - ACCEL_MIN
|
|
||||||
if abs(delta_hi) > 0.01 or abs(delta_lo) > 0.01:
|
|
||||||
any_delta = True
|
|
||||||
rows.append((label, v_ego, stock_hi, c_hi, delta_hi, c_lo, delta_lo))
|
|
||||||
|
|
||||||
with capsys.disabled():
|
|
||||||
_print_table(
|
|
||||||
"AccelPersonalityController: a_max stock vs controller",
|
|
||||||
["personality", "v_ego", "stock_hi", "ctrl_hi", "delta_hi", "ctrl_lo", "delta_lo"],
|
|
||||||
rows,
|
|
||||||
)
|
|
||||||
assert any_delta
|
|
||||||
@@ -1,26 +1,4 @@
|
|||||||
{
|
{
|
||||||
"AccelPersonality": {
|
|
||||||
"title": "Acceleration Personality",
|
|
||||||
"description": "Select the acceleration personality profile. Sport provides more aggressive acceleration, Eco provides gentler acceleration.",
|
|
||||||
"options": [
|
|
||||||
{
|
|
||||||
"value": 0,
|
|
||||||
"label": "Sport"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": 1,
|
|
||||||
"label": "Normal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": 2,
|
|
||||||
"label": "Eco"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"AccelPersonalityEnabled": {
|
|
||||||
"title": "Custom Acceleration Personality",
|
|
||||||
"description": "Enable custom acceleration and braking profiles that adjust max acceleration and min deceleration based on speed and selected personality."
|
|
||||||
},
|
|
||||||
"AccessToken": {
|
"AccessToken": {
|
||||||
"title": "AccessTokenIsNice",
|
"title": "AccessTokenIsNice",
|
||||||
"description": ""
|
"description": ""
|
||||||
|
|||||||
@@ -620,65 +620,6 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"key": "AccelPersonalityEnabled",
|
|
||||||
"widget": "toggle",
|
|
||||||
"title": "Acceleration Personality",
|
|
||||||
"description": "Enable per-personality acceleration profiles. Sport allows stronger acceleration; Eco is gentler.",
|
|
||||||
"visibility": [
|
|
||||||
{
|
|
||||||
"type": "capability",
|
|
||||||
"field": "has_longitudinal_control",
|
|
||||||
"equals": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"enablement": [
|
|
||||||
{
|
|
||||||
"type": "capability",
|
|
||||||
"field": "has_longitudinal_control",
|
|
||||||
"equals": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "AccelPersonality",
|
|
||||||
"widget": "multiple_button",
|
|
||||||
"title": "Acceleration Profile",
|
|
||||||
"description": "Sport allows the most aggressive acceleration; Eco the gentlest. Normal sits between.",
|
|
||||||
"options": [
|
|
||||||
{
|
|
||||||
"value": 0,
|
|
||||||
"label": "Sport"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": 1,
|
|
||||||
"label": "Normal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": 2,
|
|
||||||
"label": "Eco"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"visibility": [
|
|
||||||
{
|
|
||||||
"type": "param",
|
|
||||||
"key": "AccelPersonalityEnabled",
|
|
||||||
"equals": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"enablement": [
|
|
||||||
{
|
|
||||||
"type": "capability",
|
|
||||||
"field": "has_longitudinal_control",
|
|
||||||
"equals": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "param",
|
|
||||||
"key": "AccelPersonalityEnabled",
|
|
||||||
"equals": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"key": "IntelligentCruiseButtonManagement",
|
"key": "IntelligentCruiseButtonManagement",
|
||||||
"widget": "toggle",
|
"widget": "toggle",
|
||||||
|
|||||||
@@ -43,34 +43,6 @@ sections:
|
|||||||
label: Relaxed
|
label: Relaxed
|
||||||
enablement:
|
enablement:
|
||||||
- $ref: '#/macros/longitudinal'
|
- $ref: '#/macros/longitudinal'
|
||||||
- key: AccelPersonalityEnabled
|
|
||||||
widget: toggle
|
|
||||||
title: Acceleration Personality
|
|
||||||
description: Enable per-personality acceleration profiles. Sport allows stronger acceleration; Eco is gentler.
|
|
||||||
visibility:
|
|
||||||
- $ref: '#/macros/longitudinal'
|
|
||||||
enablement:
|
|
||||||
- $ref: '#/macros/longitudinal'
|
|
||||||
- key: AccelPersonality
|
|
||||||
widget: multiple_button
|
|
||||||
title: Acceleration Profile
|
|
||||||
description: Sport allows the most aggressive acceleration; Eco the gentlest. Normal sits between.
|
|
||||||
options:
|
|
||||||
- value: 0
|
|
||||||
label: Sport
|
|
||||||
- value: 1
|
|
||||||
label: Normal
|
|
||||||
- value: 2
|
|
||||||
label: Eco
|
|
||||||
visibility:
|
|
||||||
- type: param
|
|
||||||
key: AccelPersonalityEnabled
|
|
||||||
equals: true
|
|
||||||
enablement:
|
|
||||||
- $ref: '#/macros/longitudinal'
|
|
||||||
- type: param
|
|
||||||
key: AccelPersonalityEnabled
|
|
||||||
equals: true
|
|
||||||
- key: IntelligentCruiseButtonManagement
|
- key: IntelligentCruiseButtonManagement
|
||||||
widget: toggle
|
widget: toggle
|
||||||
title: Intelligent Cruise Button Management (ICBM) (Alpha)
|
title: Intelligent Cruise Button Management (ICBM) (Alpha)
|
||||||
|
|||||||
+1
-1
Submodule tinygrad_repo updated: 3501a71478...4ad60723e9
Reference in New Issue
Block a user