Compare commits

..

13 Commits

Author SHA1 Message Date
DevTekVE f46f54582c Update input handling logic in modeld.py
Refined how input keys are processed to ensure proper assignment, excluding 'desire'. This improves flexibility and correctness when managing input buffers in the model pipeline.
2025-01-06 13:07:05 +01:00
DevTekVE bc33bea185 This was actually quite revealing! This also has correlation with behaviors I saw when porting to tinygrad! 2025-01-06 12:50:03 +01:00
DevTekVE 6be69e5a47 Cleanup 2025-01-06 12:25:23 +01:00
DevTekVE 7d361df254 Cleanup 2025-01-06 11:47:11 +01:00
DevTekVE 0339f103d1 Removing this method as it wouldn't really be scalable enough with the amount of inputs we need to define for now and the amount of dependencies they might have 2025-01-06 11:40:08 +01:00
DevTekVE 9eaa57a645 Refactor input handling for model metadata and additional params
Modify `model_metadata` usage for clarity and consistency, aligning variable names across the code. Add support for dynamic input handling with keys like `lateral_control_params`, `driving_style`, and navigation-related inputs. Clean up commented legacy code for better maintainability.
2025-01-06 11:39:19 +01:00
DevTekVE 409fa050a0 Refactor input handling and correct shape retrieval.
Flatten input arrays in `prepare_inputs` for consistency with legacy definitions. Update `desired_shape` to use metadata for improved reliability and maintainability.
2025-01-06 11:18:27 +01:00
DevTekVE b0c959f162 Fix the metadata path 2025-01-06 11:02:07 +01:00
DevTekVE c3ac2b0540 Refactor prepare_inputs to accept model_metadata as a parameter.
This change improves the flexibility of the `prepare_inputs` function by passing `model_metadata` as an argument instead of relying on an internal call. Additionally, fixed import paths for `sunnypilot` modules to ensure consistency and alignment.
2025-01-06 11:02:06 +01:00
DevTekVE bd3117f5d1 Refactor model input shaping and indexing logic.
Introduce dynamic reshaping for 'desire' inputs and improve indexing for 'features_buffer' to enhance maintainability and clarity. These changes reduce hardcoding and make the code more adaptable to varying input dimensions.
2025-01-06 10:42:52 +01:00
DevTekVE 2be0e84e9f Refactor model and metadata path handling logic
Simplified the `USE_ONNX` initialization and adjusted model paths for better consistency and clarity. Fixed variable naming in `load_metadata` to improve readability. These changes enhance code maintainability and correctness.
2025-01-06 10:38:03 +01:00
Jason Wen 5809ab3baa load model and metadata dynamically 2025-01-06 00:46:45 -05:00
Jason Wen 570789a179 parse inputs via metadata 2025-01-06 00:09:52 -05:00
12 changed files with 188 additions and 194 deletions
+1 -1
View File
@@ -396,7 +396,7 @@ SConscript(['third_party/SConscript'])
SConscript(['selfdrive/SConscript'])
# SConscript(['sunnypilot/SConscript'])
SConscript(['sunnypilot/SConscript'])
if Dir('#tools/cabana/').exists() and GetOption('extras'):
SConscript(['tools/replay/SConscript'])
+57 -42
View File
@@ -1,13 +1,21 @@
#!/usr/bin/env python3
import os
from openpilot.system.hardware import TICI
from openpilot.selfdrive.modeld.runners.model_runner import ONNXRunner, TinygradRunner
#
if TICI:
from tinygrad.tensor import Tensor
from tinygrad.dtype import dtypes
from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from_opencl_address
os.environ['QCOM'] = '1'
else:
from openpilot.selfdrive.modeld.runners.ort_helpers import make_onnx_cpu_runner
import time
import pickle
import numpy as np
import cereal.messaging as messaging
from cereal import car, log
from pathlib import Path
from setproctitle import setproctitle
from cereal.messaging import PubMaster, SubMaster
from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf
@@ -25,8 +33,13 @@ from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_pose_
from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.selfdrive.modeld.models.commonmodel_pyx import DrivingModelFrame, CLContext
PROCESS_NAME = "selfdrive.modeld.modeld"
PROCESS_NAME = "selfdrive.modeld.modeld"
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
MODEL_PATH = Path(__file__).parent / 'models/supercombo.onnx'
MODEL_PKL_PATH = Path(__file__).parent / 'models/supercombo_tinygrad.pkl'
METADATA_PATH = Path(__file__).parent / 'models/supercombo_metadata.pkl'
class FrameMeta:
frame_id: int = 0
@@ -48,25 +61,35 @@ class ModelState:
self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
self.full_features_20Hz = np.zeros((ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32)
self.desire_20Hz = np.zeros((ModelConstants.FULL_HISTORY_BUFFER_LEN + 1, ModelConstants.DESIRE_LEN), dtype=np.float32)
# Initialize model runner
self.model_runner = TinygradRunner(self.frames) if TICI else ONNXRunner(self.frames)
# img buffers are managed in openCL transform code
self.numpy_inputs = {}
self.numpy_inputs = {
'desire': np.zeros((1, (ModelConstants.HISTORY_BUFFER_LEN+1), ModelConstants.DESIRE_LEN), dtype=np.float32),
'traffic_convention': np.zeros((1, ModelConstants.TRAFFIC_CONVENTION_LEN), dtype=np.float32),
'features_buffer': np.zeros((1, ModelConstants.HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32),
}
for key, shape in self.model_runner.input_shapes.items():
if key not in self.frames: # Managed by opencl
self.numpy_inputs[key] = np.zeros(shape, dtype=np.float32)
with open(METADATA_PATH, 'rb') as f:
model_metadata = pickle.load(f)
self.input_shapes = model_metadata['input_shapes']
self.output_slices = model_metadata['output_slices']
net_output_size = model_metadata['output_shapes']['outputs'][1]
self.output = np.zeros(net_output_size, dtype=np.float32)
self.parser = Parser()
net_output_size = self.model_runner.model_metadata['output_shapes']['outputs'][1]
self.output = np.zeros(net_output_size, dtype=np.float32)
if TICI:
self.tensor_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()}
with open(MODEL_PKL_PATH, "rb") as f:
self.model_run = pickle.load(f)
else:
self.onnx_cpu_runner = make_onnx_cpu_runner(MODEL_PATH)
num_elements = self.numpy_inputs['features_buffer'].shape[1]
step_size = int(-100 / num_elements)
self.full_features_20Hz_idxs = np.arange(step_size, step_size * (num_elements + 1), step_size)[::-1]
self.desire_reshape_dims = (self.numpy_inputs['desire'].shape[0], self.numpy_inputs['desire'].shape[1], -1, self.numpy_inputs['desire'].shape[2])
def slice_outputs(self, model_outputs: np.ndarray) -> dict[str, np.ndarray]:
parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in self.output_slices.items()}
if SEND_RAW_PRED:
parsed_model_outputs['raw_pred'] = model_outputs.copy()
return parsed_model_outputs
def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_wide: np.ndarray,
inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None:
@@ -77,42 +100,36 @@ class ModelState:
self.desire_20Hz[:-1] = self.desire_20Hz[1:]
self.desire_20Hz[-1] = new_desire
self.numpy_inputs['desire'][:] = self.desire_20Hz.reshape(self.desire_reshape_dims).max(axis=2)
for key in self.numpy_inputs:
if key in inputs and key not in ['desire']:
self.numpy_inputs[key][:] = inputs[key]
self.numpy_inputs['desire'][:] = self.desire_20Hz.reshape((1,25,4,-1)).max(axis=2)
self.numpy_inputs['traffic_convention'][:] = inputs['traffic_convention']
imgs_cl = {'input_imgs': self.frames['input_imgs'].prepare(buf, transform.flatten()),
'big_input_imgs': self.frames['big_input_imgs'].prepare(wbuf, transform_wide.flatten())}
# Prepare inputs using the model runner
self.model_runner.prepare_inputs(imgs_cl, self.numpy_inputs)
if TICI:
# The imgs tensors are backed by opencl memory, only need init once
for key in imgs_cl:
if key not in self.tensor_inputs:
self.tensor_inputs[key] = qcom_tensor_from_opencl_address(imgs_cl[key].mem_address, self.input_shapes[key], dtype=dtypes.uint8)
else:
for key in imgs_cl:
self.numpy_inputs[key] = self.frames[key].buffer_from_cl(imgs_cl[key]).reshape(self.input_shapes[key])
if prepare_only:
return None
# Run model inference
self.output = self.model_runner.run_model()
outputs = self.parser.parse_outputs(self.model_runner.slice_outputs(self.output), self.numpy_inputs.keys())
if TICI:
self.output = self.model_run(**self.tensor_inputs).numpy().flatten()
else:
self.output = self.onnx_cpu_runner.run(None, self.numpy_inputs)[0].flatten()
outputs = self.parser.parse_outputs(self.slice_outputs(self.output))
self.full_features_20Hz[:-1] = self.full_features_20Hz[1:]
self.full_features_20Hz[-1] = outputs['hidden_state'][0, :]
self.numpy_inputs['features_buffer'][:] = self.full_features_20Hz[self.full_features_20Hz_idxs]
if "desired_curvature" in outputs:
input_name_prev = None
if "prev_desired_curvs" in self.numpy_inputs.keys():
input_name_prev = 'prev_desired_curvs'
elif "prev_desired_curv" in self.numpy_inputs.keys():
input_name_prev = 'prev_desired_curv'
if input_name_prev is not None:
len = outputs['desired_curvature'][0].size
self.numpy_inputs[input_name_prev][0, :-len, 0] = self.numpy_inputs[input_name_prev][0, len:, 0]
self.numpy_inputs[input_name_prev][0, -len:, 0] = outputs['desired_curvature'][0]
idxs = np.arange(-4,-100,-4)[::-1]
self.numpy_inputs['features_buffer'][:] = self.full_features_20Hz[idxs]
return outputs
@@ -173,6 +190,7 @@ def main(demo=False):
meta_main = FrameMeta()
meta_extra = FrameMeta()
if demo:
CP = get_demo_car_params()
else:
@@ -254,9 +272,6 @@ def main(demo=False):
'traffic_convention': traffic_convention,
}
if "lateral_control_params" in model.numpy_inputs.keys():
inputs['lateral_control_params'] = np.array([sm["carState"].vEgo, steer_delay], dtype=np.float32)
mt1 = time.perf_counter()
model_output = model.run(buf_main, buf_extra, model_transform_main, model_transform_extra, inputs, prepare_only)
mt2 = time.perf_counter()
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b31b504bc0b440d3bc72967507a00eb4f112285626fbfb3135011500325ee6d6
size 51452435
oid sha256:72d3d6f8d3c98f5431ec86be77b6350d7d4f43c25075c0106f1d1e7ec7c77668
size 49096168
+1 -4
View File
@@ -84,8 +84,7 @@ class Parser:
outs[name] = pred_mu_final.reshape(final_shape)
outs[name + '_stds'] = pred_std_final.reshape(final_shape)
def parse_outputs(self, outs: dict[str, np.ndarray], input_keys: [str]) -> dict[str, np.ndarray]:
""" Parse the model outputs into a dictionary of numpy arrays. The input_keys are used to determine how the output should be parsed. """
def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
self.parse_mdn('plan', outs, in_N=ModelConstants.PLAN_MHP_N, out_N=ModelConstants.PLAN_MHP_SELECTION,
out_shape=(ModelConstants.IDX_N,ModelConstants.PLAN_WIDTH))
self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_LANE_LINES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH))
@@ -97,8 +96,6 @@ class Parser:
out_shape=(ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH))
if 'lat_planner_solution' in outs:
self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(ModelConstants.IDX_N,ModelConstants.LAT_PLANNER_SOLUTION_WIDTH))
if 'desired_curvature' in outs and "prev_desired_curv" in input_keys:
self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(ModelConstants.DESIRED_CURV_WIDTH,))
for k in ['lead_prob', 'lane_lines_prob', 'meta']:
self.parse_binary_crossentropy(k, outs)
self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,))
-114
View File
@@ -1,114 +0,0 @@
import os
from openpilot.system.hardware import TICI
#
from tinygrad.tensor import Tensor, dtypes
from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from_opencl_address
from openpilot.selfdrive.modeld.runners.ort_helpers import make_onnx_cpu_runner, ORT_TYPES_TO_NP_TYPES
import pickle
import numpy as np
from pathlib import Path
from abc import ABC, abstractmethod
from openpilot.selfdrive.modeld.models.commonmodel_pyx import DrivingModelFrame, CLMem
if TICI:
os.environ['QCOM'] = '1'
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
MODEL_PATH = Path(__file__).parent / '../models/supercombo.onnx'
MODEL_PKL_PATH = Path(__file__).parent / '../models/supercombo_tinygrad.pkl'
METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl'
class ModelRunner(ABC):
"""Abstract base class for model runners that defines the interface for running ML models."""
def __init__(self):
"""Initialize the model runner with paths to model and metadata files."""
with open(METADATA_PATH, 'rb') as f:
self.model_metadata = pickle.load(f)
self.input_shapes = self.model_metadata['input_shapes']
self.output_slices = self.model_metadata['output_slices']
self.inputs: dict = {}
@abstractmethod
def prepare_inputs(self, imgs_cl: dict[str, CLMem], numpy_inputs: dict[str, np.ndarray])-> dict:
"""Prepare inputs for model inference."""
@abstractmethod
def run_model(self):
"""Run model inference with prepared inputs."""
def slice_outputs(self, model_outputs: np.ndarray) -> dict:
"""Slice model outputs according to metadata configuration."""
parsed_outputs = {k: model_outputs[np.newaxis, v] for k, v in self.output_slices.items()}
if SEND_RAW_PRED:
parsed_outputs['raw_pred'] = model_outputs.copy()
return parsed_outputs
class TinygradRunner(ModelRunner):
"""Tinygrad implementation of model runner for TICI hardware."""
def __init__(self, frames: dict[str, DrivingModelFrame] | None = None):
super().__init__()
# Load Tinygrad model
with open(MODEL_PKL_PATH, "rb") as f:
self.model_run = pickle.load(f)
self.input_to_dtype = {}
self.input_to_device = {}
for idx, name in enumerate(self.model_run.captured.expected_names):
self.input_to_dtype[name] = self.model_run.captured.expected_st_vars_dtype_device[idx][2] # 2 is the dtype
self.input_to_device[name] = self.model_run.captured.expected_st_vars_dtype_device[idx][3] # 3 is the device
assert TICI or frames is not None, "TinygradRunner requires frames for non-TICI hardware"
self.frames = frames
self.is_memory_model = None # Use None to indicate that it hasn't been determined yet
def prepare_inputs(self, imgs_cl: dict[str, CLMem], numpy_inputs: dict[str, np.ndarray]) -> dict:
if self.is_memory_model is None:
self.is_memory_model = any(self.input_to_dtype[key] == dtypes.uint8 for key in imgs_cl)
print(f"Memory model: {self.is_memory_model}")
# Initialize image tensors if not already done
for key in imgs_cl:
if TICI and self.is_memory_model and key not in self.inputs:
self.inputs[key] = qcom_tensor_from_opencl_address(imgs_cl[key].mem_address, self.input_shapes[key], dtype=dtypes.uint8)
elif not TICI or not self.is_memory_model:
shape = self.frames[key].buffer_from_cl(imgs_cl[key]).reshape(self.input_shapes[key])
self.inputs[key] = Tensor(shape, device=self.input_to_device[key], dtype=self.input_to_dtype[key]).realize()
# Update numpy inputs
for key, value in numpy_inputs.items():
if key not in imgs_cl:
self.inputs[key] = Tensor(value, device=self.input_to_device[key], dtype=self.input_to_dtype[key]).realize()
return self.inputs
def run_model(self):
return self.model_run(**self.inputs).numpy().flatten()
class ONNXRunner(ModelRunner):
"""ONNX implementation of model runner for non-TICI hardware."""
def __init__(self, frames: dict[str, DrivingModelFrame]):
super().__init__()
self.runner = make_onnx_cpu_runner(MODEL_PATH)
self.frames = frames
self.input_to_nptype = {
model_input.name: ORT_TYPES_TO_NP_TYPES[model_input.type]
for model_input in self.runner.get_inputs()
}
def prepare_inputs(self, imgs_cl: dict[str, CLMem], numpy_inputs: dict[str, np.ndarray]) -> dict:
self.inputs = numpy_inputs.copy()
for key in imgs_cl:
self.inputs[key] = self.frames[key].buffer_from_cl(imgs_cl[key]).astype(self.input_to_nptype[key]).reshape(self.input_shapes[key])
return self.inputs
def run_model(self):
return self.runner.run(None, self.inputs)[0].flatten()
+58 -26
View File
@@ -1,11 +1,9 @@
#!/usr/bin/env python3
import os
import time
import pickle
import numpy as np
import cereal.messaging as messaging
from cereal import car, log
from pathlib import Path
from setproctitle import setproctitle
from cereal.messaging import PubMaster, SubMaster
from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf
@@ -23,16 +21,14 @@ from openpilot.sunnypilot.modeld.parse_model_outputs import Parser
from openpilot.sunnypilot.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState
from openpilot.sunnypilot.modeld.constants import ModelConstants
from openpilot.sunnypilot.modeld.models.commonmodel_pyx import DrivingModelFrame, CLContext
from openpilot.common.realtime import DT_MDL
from openpilot.common.numpy_fast import interp
from openpilot.sunnypilot.modeld.runners.run_helpers import load_model, load_metadata, prepare_inputs
PROCESS_NAME = "sunnypilot.modeld.modeld"
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
MODEL_PATHS = {
ModelRunner.THNEED: Path(__file__).parent / 'models/supercombo.thneed',
ModelRunner.ONNX: Path(__file__).parent / 'models/supercombo.onnx'}
METADATA_PATH = Path(__file__).parent / 'models/supercombo_metadata.pkl'
class FrameMeta:
frame_id: int = 0
@@ -58,27 +54,29 @@ class ModelState:
self.full_features_20Hz = np.zeros((ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32)
self.desire_20Hz = np.zeros((ModelConstants.FULL_HISTORY_BUFFER_LEN + 1, ModelConstants.DESIRE_LEN), dtype=np.float32)
# img buffers are managed in openCL transform code
self.inputs = {
'desire': np.zeros(ModelConstants.DESIRE_LEN * (ModelConstants.HISTORY_BUFFER_LEN+1), dtype=np.float32),
'traffic_convention': np.zeros(ModelConstants.TRAFFIC_CONVENTION_LEN, dtype=np.float32),
'features_buffer': np.zeros(ModelConstants.HISTORY_BUFFER_LEN * ModelConstants.FEATURE_LEN, dtype=np.float32),
}
model_paths = load_model()
self.model_metadata = load_metadata()
self.inputs = prepare_inputs(self.model_metadata)
with open(METADATA_PATH, 'rb') as f:
model_metadata = pickle.load(f)
self.output_slices = model_metadata['output_slices']
net_output_size = model_metadata['output_shapes']['outputs'][1]
self.output_slices = self.model_metadata['output_slices']
net_output_size = self.model_metadata['output_shapes']['outputs'][1]
self.output = np.zeros(net_output_size, dtype=np.float32)
self.parser = Parser()
self.model = ModelRunner(MODEL_PATHS, self.output, Runtime.GPU, False, context)
self.model = ModelRunner(model_paths, self.output, Runtime.GPU, False, context)
self.model.addInput("input_imgs", None)
self.model.addInput("big_input_imgs", None)
for k,v in self.inputs.items():
self.model.addInput(k, v)
num_elements = self.model_metadata['input_shapes']['features_buffer'][1]
step_size = int(-100 / num_elements)
self.feature_buffer_idxs = np.arange(step_size, step_size * (num_elements + 1), step_size)[::-1]
desired_shape = self.model_metadata["input_shapes"]["desire"][1]
middle_dim = int(self.desire_20Hz.shape[0] / desired_shape)
self.desire_reshape_dims = (desired_shape, middle_dim, -1)
def slice_outputs(self, model_outputs: np.ndarray) -> dict[str, np.ndarray]:
parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in self.output_slices.items()}
if SEND_RAW_PRED:
@@ -94,7 +92,11 @@ class ModelState:
self.desire_20Hz[:-1] = self.desire_20Hz[1:]
self.desire_20Hz[-1] = new_desire
self.inputs['desire'][:] = self.desire_20Hz.reshape((25,4,-1)).max(axis=1).flatten()
self.inputs['desire'][:] = self.desire_20Hz.reshape(self.desire_reshape_dims).max(axis=1).flatten()
for key in self.inputs:
if key in inputs and key not in ['desire']:
self.inputs[key][:] = inputs[key]
self.inputs['traffic_convention'][:] = inputs['traffic_convention']
@@ -105,13 +107,29 @@ class ModelState:
return None
self.model.execute()
outputs = self.parser.parse_outputs(self.slice_outputs(self.output))
outputs = self.parser.parse_outputs(self.slice_outputs(self.output), self.inputs.keys())
self.full_features_20Hz[:-1] = self.full_features_20Hz[1:]
self.full_features_20Hz[-1] = outputs['hidden_state'][0, :]
idxs = np.arange(-4,-100,-4)[::-1]
self.inputs['features_buffer'][:] = self.full_features_20Hz[idxs].flatten()
self.inputs['features_buffer'][:] = self.full_features_20Hz[self.feature_buffer_idxs].flatten()
if "desired_curvature" in outputs:
input_name_prev = None
if "prev_desired_curvs" in self.inputs.keys():
input_name_prev = 'prev_desired_curvs'
elif "prev_desired_curv" in self.inputs.keys():
input_name_prev = 'prev_desired_curv'
if input_name_prev is not None:
len = outputs['desired_curvature'][0].size
self.inputs[input_name_prev][:-len] = self.inputs[input_name_prev][len:]
self.inputs[input_name_prev][-len:] = outputs['desired_curvature'][0, :]
if "lat_planner_solution" in outputs:
if "lat_planner_state" in self.inputs.keys():
self.inputs['lat_planner_state'][2] = interp(DT_MDL, ModelConstants.T_IDXS, outputs['lat_planner_solution'][0, :, 2])
self.inputs['lat_planner_state'][3] = interp(DT_MDL, ModelConstants.T_IDXS, outputs['lat_planner_solution'][0, :, 3])
return outputs
@@ -249,10 +267,24 @@ def main(demo=False):
if prepare_only:
cloudlog.error(f"skipping model eval. Dropped {vipc_dropped_frames} frames")
inputs:dict[str, np.ndarray] = {
inputs: dict[str, np.ndarray] = {
'desire': vec_desire,
'traffic_convention': traffic_convention,
}
}
if "lateral_control_params" in model.inputs.keys():
inputs['lateral_control_params'] = np.array([sm["carState"].vEgo, steer_delay], dtype=np.float32)
# TODO-SP: Below should be good, but I have not tested a model with it so I can't be sure until we test it
# if "driving_style" in model.inputs.keys():
# inputs['driving_style'] = np.array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], dtype=np.float32)
#
# if "nav_features" in model.inputs.keys():
# inputs['nav_features'] = np.zeros(ModelConstants.NAV_FEATURE_LEN, dtype=np.float32) # Get size from shape
#
# if "nav_instructions" in model.inputs.keys():
# inputs['nav_instructions'] = np.zeros(ModelConstants.NAV_INSTRUCTION_LEN, dtype=np.float32) # Get size from shape
mt1 = time.perf_counter()
model_output = model.run(buf_main, buf_extra, model_transform_main, model_transform_extra, inputs, prepare_only)
+4 -1
View File
@@ -84,7 +84,8 @@ class Parser:
outs[name] = pred_mu_final.reshape(final_shape)
outs[name + '_stds'] = pred_std_final.reshape(final_shape)
def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
def parse_outputs(self, outs: dict[str, np.ndarray], input_keys: [str]) -> dict[str, np.ndarray]:
""" Parse the model outputs into a dictionary of numpy arrays. The input_keys are used to determine how the output should be parsed. """
self.parse_mdn('plan', outs, in_N=ModelConstants.PLAN_MHP_N, out_N=ModelConstants.PLAN_MHP_SELECTION,
out_shape=(ModelConstants.IDX_N,ModelConstants.PLAN_WIDTH))
self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_LANE_LINES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH))
@@ -96,6 +97,8 @@ class Parser:
out_shape=(ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH))
if 'lat_planner_solution' in outs:
self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(ModelConstants.IDX_N,ModelConstants.LAT_PLANNER_SOLUTION_WIDTH))
if 'desired_curvature' in outs and "prev_desired_curv" in input_keys:
self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(ModelConstants.DESIRED_CURV_WIDTH,))
for k in ['lead_prob', 'lane_lines_prob', 'meta']:
self.parse_binary_crossentropy(k, outs)
self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,))
+6 -1
View File
@@ -40,6 +40,7 @@ class ONNXModel(RunModel):
def __init__(self, path, output, runtime, use_tf8, cl_context):
self.inputs = {}
self.output = output
self.use_tf8 = use_tf8
self.session = create_ort_session(path, fp16_to_fp32=True)
self.input_names = [x.name for x in self.session.get_inputs()]
@@ -63,7 +64,11 @@ class ONNXModel(RunModel):
return None
def execute(self):
inputs = {k: v.view(self.input_dtypes[k]) for k,v in self.inputs.items()}
# TODO-SP: The input below causes issues because its converting the input data when in reality it doesn't need conversion as it was already the target type.
# I am leaving this comment and the input down because this needs to be looked before merging. I had similar issues when trying the tinygrad runner...
# Also I checked to see if I found a similar change like this on thneed but I didn't find any, so probably thneed is still working fine.
# inputs = {k: v.view(self.input_dtypes[k]) for k,v in self.inputs.items()}
inputs = {k: (v.view(np.uint8) / 255. if self.use_tf8 and k == 'input_img' else v) for k,v in self.inputs.items()}
inputs = {k: v.reshape(self.input_shapes[k]).astype(self.input_dtypes[k]) for k,v in inputs.items()}
outputs = self.session.run(None, inputs)
assert len(outputs) == 1, "Only single model outputs are supported"
+57
View File
@@ -0,0 +1,57 @@
# Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
#
# This file is part of sunnypilot and is licensed under the MIT License.
# See the LICENSE.md file in the root directory for more details.
import os
import pickle
import numpy as np
from pathlib import Path
from cereal import custom
from openpilot.sunnypilot.modeld.runners import ModelRunner
from openpilot.sunnypilot.models.helpers import get_active_bundle
from openpilot.system.hardware import PC
from openpilot.system.hardware.hw import Paths
USE_ONNX = os.getenv('USE_ONNX', PC)
CUSTOM_MODEL_PATH = Paths.model_root()
METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl'
ModelManager = custom.ModelManagerSP
def load_model():
if USE_ONNX:
model_paths = {ModelRunner.ONNX: Path(__file__).parent / '../models/supercombo.onnx'}
elif bundle := get_active_bundle():
drive_model = next(model for model in bundle.models if model.type == ModelManager.Type.drive)
model_paths = {ModelRunner.THNEED: f"{CUSTOM_MODEL_PATH}/{drive_model.fileName}"}
else:
model_paths = {ModelRunner.THNEED: Path(__file__).parent / '../models/supercombo.thneed'}
return model_paths
def load_metadata():
if bundle := get_active_bundle():
metadata_model = next(model for model in bundle.models if model.type == ModelManager.Type.metadata)
metadata_path = f"{CUSTOM_MODEL_PATH}/{metadata_model.fileName}"
else:
metadata_path = METADATA_PATH
with open(metadata_path, 'rb') as f:
metadata = pickle.load(f)
return metadata
def prepare_inputs(model_metadata) -> dict[str, np.ndarray]:
# img buffers are managed in openCL transform code so we don't pass them as inputs
inputs: dict[str, np.ndarray] = {
key: np.zeros(shape, dtype=np.float32).flatten() # Inputs were defined flattened back then
for key, shape in model_metadata['input_shapes'].items()
if key not in ['input_imgs', 'big_input_imgs']
}
return inputs
+1 -1
View File
@@ -22,7 +22,7 @@ async def verify_file(file_path: str, expected_hash: str) -> bool:
return sha256_hash.hexdigest().lower() == expected_hash.lower()
def get_active_bundle(params: Params) -> custom.ModelManagerSP.ModelBundle:
def get_active_bundle(params: Params = None) -> custom.ModelManagerSP.ModelBundle:
"""Gets the active model bundle from cache"""
if params is None:
params = Params()
+1 -2
View File
@@ -75,8 +75,7 @@ def use_sunnylink_uploader_shim(started, params, CP: car.CarParams) -> bool:
def is_snpe_model(started, params, CP: car.CarParams) -> bool:
"""Check if the active model runner is SNPE."""
# TODO-SP: I want to do a little more optimization here to only check this once when we've transitioned from offroad to onroad.
return False
# return bool(get_active_model_runner(params) == custom.ModelManagerSP.Runner.snpe)
return bool(get_active_model_runner(params) == custom.ModelManagerSP.Runner.snpe)
def is_stock_model(started, params, CP: car.CarParams) -> bool:
"""Check if the active model runner is stock."""