Files
openpilot-evo/sunnypilot/models/runners/tinygrad/tinygrad_runner.py
T
James Vecellio-Grant 7c6d887187 modeld_v2: infer model shapes from inputs (#1162)
* modeld_v2: dynamify temporal buffer management.

* skip redundant reshaping and flattening.

* simplify MHP checks for lead and plan

* modeld_v2: add unit tests for buffer logic and refactor index mapping

* Let’s possibly fail a test :)

* Update test_buffer_logic_inspect.py

* Update test_buffer_logic_inspect.py

* modeld_v2: better temporal mapping for non-split

* Bump to 10 I guess

* Downgrade CURRENT_SELECTOR_VERSION to 9

* red diff ya know?

* add dynamic buffer update tests and compare against legacy logic.

 Cover modelState.init and modelState.run

* send

* Revert "send"

This reverts commit 9e6c95fbfde134eeba952da6eef012baa0396fa0.

* format

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-08-24 22:08:55 -04:00

140 lines
6.5 KiB
Python

import pickle
import numpy as np
from openpilot.sunnypilot.modeld_v2.runners.tinygrad_helpers import qcom_tensor_from_opencl_address
from openpilot.sunnypilot.models.runners.constants import CLMemDict, FrameDict, NumpyDict, ModelType, ShapeDict, CUSTOM_MODEL_PATH, SliceDict
from openpilot.sunnypilot.models.runners.model_runner import ModelRunner
from openpilot.sunnypilot.models.runners.tinygrad.model_types import PolicyTinygrad, VisionTinygrad, SupercomboTinygrad
from openpilot.system.hardware import TICI
from openpilot.sunnypilot.models.split_model_constants import SplitModelConstants
from openpilot.sunnypilot.modeld_v2.constants import ModelConstants
from tinygrad.tensor import Tensor
class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTinygrad):
"""
A ModelRunner implementation for executing Tinygrad models.
Handles loading Tinygrad model artifacts (.pkl), preparing inputs as Tinygrad
Tensors (potentially using QCOM extensions on TICI), running inference,
and parsing the outputs.
:param model_type: The type of model (e.g., supercombo) to load and run.
"""
def __init__(self, model_type: int = ModelType.supercombo):
ModelRunner.__init__(self)
SupercomboTinygrad.__init__(self)
PolicyTinygrad.__init__(self)
VisionTinygrad.__init__(self)
self._constants = ModelConstants
self._model_data = self.models.get(model_type)
if not self._model_data or not self._model_data.model:
raise ValueError(f"Model data for type {model_type} not available.")
artifact_filename = self._model_data.model.artifact.fileName
assert artifact_filename.endswith('_tinygrad.pkl'), \
f"Invalid model file {artifact_filename} for TinygradRunner"
model_pkl_path = f"{CUSTOM_MODEL_PATH}/{artifact_filename}"
with open(model_pkl_path, "rb") as f:
try:
# Load the compiled Tinygrad model runner function
self.model_run = pickle.load(f)
except FileNotFoundError as e:
# Provide a helpful error message if the model was built for a different platform
assert "/dev/kgsl-3d0" not in str(e), "Model was built on C3 or C3X, but is being loaded on PC"
raise
# Map input names to their required dtype and device from the loaded model
self.input_to_dtype = {}
self.input_to_device = {}
for idx, name in enumerate(self.model_run.captured.expected_names):
info = self.model_run.captured.expected_st_vars_dtype_device[idx]
self.input_to_dtype[name] = info[2] # dtype
self.input_to_device[name] = info[3] # device
@property
def vision_input_names(self) -> list[str]:
"""Returns the list of vision input names from the input shapes."""
return [name for name in self.input_shapes.keys() if 'img' in name]
def prepare_vision_inputs(self, imgs_cl: CLMemDict, frames: FrameDict):
"""Prepares vision (image) inputs as Tinygrad Tensors."""
for key in imgs_cl:
if TICI and key not in self.inputs:
# On TICI, directly use OpenCL memory address for efficiency via QCOM extensions
self.inputs[key] = qcom_tensor_from_opencl_address(imgs_cl[key].mem_address, self.input_shapes[key], dtype=self.input_to_dtype[key])
elif not TICI:
# On other platforms, copy data from CL buffer to a numpy array first
shape = 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()
def prepare_policy_inputs(self, numpy_inputs: NumpyDict):
"""Prepares non-image (policy) inputs as Tinygrad Tensors."""
for key, value in numpy_inputs.items():
self.inputs[key] = Tensor(value, device=self.input_to_device[key], dtype=self.input_to_dtype[key]).realize()
def prepare_inputs(self, imgs_cl: CLMemDict, numpy_inputs: NumpyDict, frames: FrameDict) -> dict:
"""Prepares all vision and policy inputs for the model."""
self.prepare_vision_inputs(imgs_cl, frames)
self.prepare_policy_inputs(numpy_inputs)
return self.inputs
def _run_model(self) -> NumpyDict:
"""Runs the Tinygrad model inference and parses the outputs."""
outputs = self.model_run(**self.inputs).contiguous().realize().uop.base.buffer.numpy()
return self._parse_outputs(outputs)
def _parse_outputs(self, model_outputs: np.ndarray) -> NumpyDict:
"""Parses the raw model outputs using the standard Parser."""
if self._model_data is None:
raise ValueError("Model data is not available. Ensure the model is loaded correctly.")
result: NumpyDict = self.parser_method_dict[self._model_data.model.type.raw](model_outputs)
return result
class TinygradSplitRunner(ModelRunner):
"""
A ModelRunner that coordinates separate TinygradVisionRunner and TinygradPolicyRunner instances.
Manages the execution of split vision and policy models, combining their inputs and outputs.
"""
def __init__(self):
super().__init__()
self.is_20hz_3d = True
self.vision_runner = TinygradRunner(ModelType.vision)
self.policy_runner = TinygradRunner(ModelType.policy)
self._constants = SplitModelConstants
def _run_model(self) -> NumpyDict:
"""Runs both vision and policy models and merges their parsed outputs."""
policy_output = self.policy_runner.run_model()
vision_output = self.vision_runner.run_model()
return {**policy_output, **vision_output} # Combine results
@property
def vision_input_names(self) -> list[str]:
"""Returns the list of vision input names from the vision runner."""
return list(self.vision_runner.vision_input_names)
@property
def input_shapes(self) -> ShapeDict:
"""Returns the combined input shapes from both vision and policy models."""
return {**self.policy_runner.input_shapes, **self.vision_runner.input_shapes}
@property
def output_slices(self) -> SliceDict:
"""Returns the combined output slices from both vision and policy models."""
return {**self.policy_runner.output_slices, **self.vision_runner.output_slices}
def prepare_inputs(self, imgs_cl: CLMemDict, numpy_inputs: NumpyDict, frames: FrameDict) -> dict:
"""Prepares inputs for both vision and policy models."""
# Policy inputs only depend on numpy_inputs
self.policy_runner.prepare_policy_inputs(numpy_inputs)
# Vision inputs depend on imgs_cl and frames
self.vision_runner.prepare_vision_inputs(imgs_cl, frames)
# Return combined inputs (though they are stored within respective runners)
return {**self.policy_runner.inputs, **self.vision_runner.inputs}