Compare commits

..

16 Commits

Author SHA1 Message Date
discountchubbs ab06c54834 dont forget to update tg 2026-09-11 15:56:10 -07:00
discountchubbs de47ec611d past the point of no return 2026-09-11 15:53:28 -07:00
discountchubbs 73dd58f6f2 models: prepare for upstream grand slam 2026-09-11 13:37:58 -07:00
James Vecellio-Grant 243fe59dc5 Update test_models.yml 2026-09-11 07:45:53 -07:00
James Vecellio-Grant d6b16842f4 Update test_models.py 2026-09-09 00:55:01 -07:00
James Vecellio-Grant 4bac4c4be9 License 2026-09-09 00:54:42 -07:00
discountchubbs 215cee1d7d make therapy therpeutic, or ... you know, just combine the urls 2026-09-08 20:02:36 -07:00
discountchubbs cb707bef83 I think this is the latest? 2026-09-08 19:50:21 -07:00
discountchubbs 0dc286ff8f loop in the 🌰 (start with one random sample to see what gh action says) 2026-09-08 19:49:42 -07:00
discountchubbs 3615762b6d we introduced oob in v19 whoops 2026-09-08 19:33:19 -07:00
discountchubbs 890c4ef336 hehe. revert me after the tests run 2026-09-08 19:28:11 -07:00
discountchubbs 8579eaf57f just the pointer 2026-09-08 17:21:34 -07:00
discountchubbs 9e1a22b813 simply simply 2026-09-08 17:15:42 -07:00
discountchubbs 48aab1bc1d fuck that 2026-09-08 17:14:10 -07:00
discountchubbs 67436ab555 keep forgettng they removed my bae, pytest 2026-09-08 17:08:28 -07:00
discountchubbs 356bd1a96e models: test tinygrad concurrency 2026-09-08 15:07:04 -07:00
18 changed files with 712 additions and 289 deletions
+75 -62
View File
@@ -103,21 +103,23 @@ jobs:
- run: |
cd ${{ github.workspace }}/openpilot/openpilot
if [ "${{ inputs.target_hardware }}" != "chestnut" ]; then
git lfs pull -X "**/selfdrive/modeld/models/big_*.onnx,**/selfdrive/modeld/models/dmonitoring_*.onnx"
rm -f selfdrive/modeld/models/big_*.onnx selfdrive/modeld/models/dmonitoring_*.onnx
git lfs pull -X "**/selfdrive/modeld/models/big_*.onnx,**/selfdrive/modeld/models/dmonitoring_*.onnx,**/selfdrive/modeld/models/big_*.pkl,**/selfdrive/modeld/models/dmonitoring_*.pkl"
rm -f selfdrive/modeld/models/big_*.onnx selfdrive/modeld/models/dmonitoring_*.onnx selfdrive/modeld/models/big_*.pkl selfdrive/modeld/models/dmonitoring_*.pkl
else
git lfs pull -I "**/selfdrive/modeld/models/big_*.onnx" -X ""
find selfdrive/modeld/models -name "*.onnx" ! -name "big_*.onnx" -delete
git lfs pull -I "**/selfdrive/modeld/models/big_*.onnx,**/selfdrive/modeld/models/big_*.pkl" -X ""
find selfdrive/modeld/models -type f \( -name "*.onnx" -o -name "*.pkl" \) ! -name "big_*.onnx" ! -name "big_*.pkl" -delete
fi
if grep -lIF "version https://git-lfs.github.com/spec/v1" selfdrive/modeld/models/*.onnx; then
echo "::error::the ONNX files above are still LFS pointers, not real models"
if grep -lIF "version https://git-lfs.github.com/spec/v1" selfdrive/modeld/models/*.onnx selfdrive/modeld/models/*.pkl 2>/dev/null; then
echo "::error::the ONNX or PKL files above are still LFS pointers, not real models"
exit 1
fi
- name: 'Upload Artifact'
uses: actions/upload-artifact@v4
with:
name: models-${{ env.REF }}${{ inputs.artifact_suffix }}
path: ${{ github.workspace }}/openpilot/openpilot/selfdrive/modeld/models/*.onnx
path: |
${{ github.workspace }}/openpilot/openpilot/selfdrive/modeld/models/*.onnx
${{ github.workspace }}/openpilot/openpilot/selfdrive/modeld/models/*.pkl
if-no-files-found: error
build_model:
@@ -196,64 +198,75 @@ jobs:
OUTPUT_PKL="${{ env.MODELS_DIR }}/driving_tinygrad.pkl"
fi
# Generate metadata for all ONNX files
find "${{ env.MODELS_DIR }}" -maxdepth 1 -name '*.onnx' | while IFS= read -r onnx_file; do
echo "Generating metadata: $onnx_file"
env ${TG_FLAGS_QCOM} python3 "${{ env.MODELS_DIR }}/../get_model_metadata.py" "$onnx_file" || true
done
NATIVE_PKL=$(find "${{ env.MODELS_DIR }}" -maxdepth 1 -name "*.pkl" -print -quit)
# Detect model type and build compile args
VISION_ONNX=""
for f in "${{ env.MODELS_DIR }}/driving_vision.onnx" "${{ env.MODELS_DIR }}/big_driving_vision.onnx"; do
[ -f "$f" ] && VISION_ONNX="$f" && break
done
POLICY_ONNX=""
for f in "${{ env.MODELS_DIR }}/driving_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_policy.onnx"; do
[ -f "$f" ] && POLICY_ONNX="$f" && break
done
OFF_POLICY_ONNX=""
for f in "${{ env.MODELS_DIR }}/driving_off_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_off_policy.onnx"; do
[ -f "$f" ] && OFF_POLICY_ONNX="$f" && break
done
ON_POLICY_ONNX=""
for f in "${{ env.MODELS_DIR }}/driving_on_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_on_policy.onnx"; do
[ -f "$f" ] && ON_POLICY_ONNX="$f" && break
done
SUPERCOMBO_ONNX=""
for f in "${{ env.MODELS_DIR }}/supercombo.onnx" "${{ env.MODELS_DIR }}/driving_supercombo.onnx" "${{ env.MODELS_DIR }}/big_supercombo.onnx" "${{ env.MODELS_DIR }}/big_driving_supercombo.onnx"; do
[ -f "$f" ] && SUPERCOMBO_ONNX="$f" && break
done
MODEL_TYPE="" ONNX_ARGS="" OUTPUT_NAME=""
if [ -f "$VISION_ONNX" ]; then
ONNX_ARGS="--vision-onnx $VISION_ONNX"
if [ -f "$ON_POLICY_ONNX" ] && [ -f "$OFF_POLICY_ONNX" ]; then
MODEL_TYPE=vision_multi_policy
ONNX_ARGS="$ONNX_ARGS --off-policy-onnx $OFF_POLICY_ONNX --on-policy-onnx $ON_POLICY_ONNX"
elif [ -f "$OFF_POLICY_ONNX" ] && [ -f "$POLICY_ONNX" ]; then
MODEL_TYPE=vision_multi_policy
ONNX_ARGS="$ONNX_ARGS --policy-onnx $POLICY_ONNX --off-policy-onnx $OFF_POLICY_ONNX"
elif [ -f "$POLICY_ONNX" ]; then
MODEL_TYPE=vision_policy
ONNX_ARGS="$ONNX_ARGS --policy-onnx $POLICY_ONNX"
if [ -n "$NATIVE_PKL" ]; then
echo "Found native precompiled pkl: $NATIVE_PKL"
if [ "$NATIVE_PKL" != "$OUTPUT_PKL" ]; then
mv "$NATIVE_PKL" "$OUTPUT_PKL"
fi
elif [ -f "$SUPERCOMBO_ONNX" ]; then
MODEL_TYPE=supercombo
ONNX_ARGS="--supercombo-onnx $SUPERCOMBO_ONNX"
fi
echo "Chunking pkl"
python3 -c "from openpilot.common.file_chunker import chunk_file, get_chunk_targets; import os; p='$OUTPUT_PKL'; chunk_file(p, get_chunk_targets(p, os.path.getsize(p)))"
else
# Generate metadata for all ONNX files
find "${{ env.MODELS_DIR }}" -maxdepth 1 -name '*.onnx' | while IFS= read -r onnx_file; do
echo "Generating metadata: $onnx_file"
env ${TG_FLAGS_QCOM} python3 "${{ env.MODELS_DIR }}/../get_model_metadata.py" "$onnx_file" || true
done
if [ -n "$MODEL_TYPE" ]; then
echo "Detected: $MODEL_TYPE -> $OUTPUT_PKL"
env ${TG_FLAGS} python3 "$COMPILE_MODELD" \
--model-type $MODEL_TYPE \
--model-size $MODEL_SIZE \
--camera-resolutions $CAMERA_RES \
$ONNX_ARGS \
--output "$OUTPUT_PKL"
# Detect model type and build compile args
VISION_ONNX=""
for f in "${{ env.MODELS_DIR }}/driving_vision.onnx" "${{ env.MODELS_DIR }}/big_driving_vision.onnx"; do
[ -f "$f" ] && VISION_ONNX="$f" && break
done
POLICY_ONNX=""
for f in "${{ env.MODELS_DIR }}/driving_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_policy.onnx"; do
[ -f "$f" ] && POLICY_ONNX="$f" && break
done
OFF_POLICY_ONNX=""
for f in "${{ env.MODELS_DIR }}/driving_off_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_off_policy.onnx"; do
[ -f "$f" ] && OFF_POLICY_ONNX="$f" && break
done
ON_POLICY_ONNX=""
for f in "${{ env.MODELS_DIR }}/driving_on_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_on_policy.onnx"; do
[ -f "$f" ] && ON_POLICY_ONNX="$f" && break
done
SUPERCOMBO_ONNX=""
for f in "${{ env.MODELS_DIR }}/supercombo.onnx" "${{ env.MODELS_DIR }}/driving_supercombo.onnx" "${{ env.MODELS_DIR }}/big_supercombo.onnx" "${{ env.MODELS_DIR }}/big_driving_supercombo.onnx"; do
[ -f "$f" ] && SUPERCOMBO_ONNX="$f" && break
done
MODEL_TYPE="" ONNX_ARGS="" OUTPUT_NAME=""
if [ -f "$VISION_ONNX" ]; then
ONNX_ARGS="--vision-onnx $VISION_ONNX"
if [ -f "$ON_POLICY_ONNX" ] && [ -f "$OFF_POLICY_ONNX" ]; then
MODEL_TYPE=vision_multi_policy
ONNX_ARGS="$ONNX_ARGS --off-policy-onnx $OFF_POLICY_ONNX --on-policy-onnx $ON_POLICY_ONNX"
elif [ -f "$OFF_POLICY_ONNX" ] && [ -f "$POLICY_ONNX" ]; then
MODEL_TYPE=vision_multi_policy
ONNX_ARGS="$ONNX_ARGS --policy-onnx $POLICY_ONNX --off-policy-onnx $OFF_POLICY_ONNX"
elif [ -f "$POLICY_ONNX" ]; then
MODEL_TYPE=vision_policy
ONNX_ARGS="$ONNX_ARGS --policy-onnx $POLICY_ONNX"
fi
elif [ -f "$SUPERCOMBO_ONNX" ]; then
MODEL_TYPE=supercombo
ONNX_ARGS="--supercombo-onnx $SUPERCOMBO_ONNX"
fi
if [ -n "$MODEL_TYPE" ]; then
echo "Detected: $MODEL_TYPE -> $OUTPUT_PKL"
env ${TG_FLAGS} python3 "$COMPILE_MODELD" \
--model-type $MODEL_TYPE \
--model-size $MODEL_SIZE \
--camera-resolutions $CAMERA_RES \
$ONNX_ARGS \
--output "$OUTPUT_PKL"
fi
fi
- name: Prepare Output
+79
View File
@@ -0,0 +1,79 @@
name: Test Models Compatibility With Tinygrad Changes
on:
pull_request:
paths:
- 'tinygrad_repo'
workflow_dispatch:
jobs:
generate-matrix:
runs-on: ubuntu-latest
outputs:
models: ${{ steps.set-matrix.outputs.models }}
steps:
- uses: actions/checkout@v4
- name: Fetch and Parse json
id: set-matrix
run: |
python3 -c '
import json, urllib.request, os, re
with open("openpilot/sunnypilot/models/fetcher.py", "r") as f:
urls = re.findall(r"MODEL_URL(?:_CHESTNUT)?\s*=\s*[\"'"'"']([^\"'"'"']+)[\"'"'"']", f.read())
artifacts = []
for url in urls:
data = json.loads(urllib.request.urlopen(url).read())
for bundle in data.get("bundles", []):
for model in bundle.get("models", []):
if "artifact" in model:
artifacts.append(model["artifact"])
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
f.write(f"models={json.dumps(artifacts)}\n")
'
test-model:
name: Test ${{ matrix.artifact.file_name }}
needs: generate-matrix
runs-on: ubuntu-latest
container: ghcr.io/commaai/openpilot-base:latest
strategy:
fail-fast: false
matrix:
artifact: ${{ fromJson(needs.generate-matrix.outputs.models) }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Download Model Chunks in Parallel
run: |
mkdir -p /tmp/model_chunks
echo '${{ toJson(matrix.artifact.chunks) }}' > chunks.json
BASE_URL="${{ matrix.artifact.download_uri.url }}"
export BASE_DIR=$(dirname "$BASE_URL")
python3 -c '
import json, os
with open("chunks.json") as f:
chunks = json.load(f)
manifest_path = f"/tmp/model_chunks/${{ matrix.artifact.file_name }}.chunkmanifest"
with open(manifest_path, "w") as f:
f.write(str(len(chunks)))
base_dir = os.environ["BASE_DIR"]
with open("/tmp/curl_config.txt", "w") as f:
for c in chunks:
fn = c["file_name"]
f.write(f"url = \"{base_dir}/{fn}\"\noutput = \"/tmp/model_chunks/{fn}\"\n")
'
curl -Z --parallel-immediate --parallel-max 16 -s -S -f -L -K /tmp/curl_config.txt
- name: Run Model Compatibility Test
env:
MODEL_BASE_NAME: ${{ matrix.artifact.file_name }}
MODEL_CHUNK_DIR: "/tmp/model_chunks"
PYTHONPATH: ".:./tinygrad_repo"
run: |
python3 -m pytest openpilot/sunnypilot/modeld_v2/tests/test_models.py
-1
View File
@@ -139,7 +139,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ChestnutActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
{"ChestnutLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
{"ChestnutModelError", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
{"AuxPowerSave", {PERSISTENT | BACKUP, BOOL}},
{"Version", {PERSISTENT, STRING}},
// --- sunnypilot params --- //
+27 -55
View File
@@ -1,10 +1,13 @@
#!/usr/bin/env python3
from collections.abc import Callable
import ctypes
import codecs
from functools import cached_property
import os
import pickle
os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom
from tinygrad.device import Device
from tinygrad_repo.examples.openpilot.helpers import allocate_inputs, load_pickle
import usb1
import struct
import threading
@@ -23,21 +26,15 @@ from openpilot.common.params import Params
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.realtime import config_realtime_process, DT_MDL
from openpilot.common.transformations.camera import DEVICE_CAMERAS
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
from openpilot.common.transformations.model import get_warp_matrix
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, should_stop, smooth_value, get_curvature_from_plan
from openpilot.selfdrive.modeld.parse_model_outputs import Parser
from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, nv12_copy_size, MODELD_INPUTS
from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState
from openpilot.common.file_chunker import open_file_chunked
from openpilot.common.hardware.usb import CHESTNUT_USB_IDS
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, chestnut_ready, modeld_pkl_path, load_oob
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, modeld_pkl_path
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
@@ -175,28 +172,30 @@ class FrameMeta:
self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof
class ModelState(ModelStateBase):
class ModelState:
prev_desire: np.ndarray # for tracking the rising edge of the pulse
def __init__(self, cam_w: int, cam_h: int, chestnut: bool):
ModelStateBase.__init__(self)
jits = load_oob(open_file_chunked(modeld_pkl_path(chestnut)))
input_devices = jits['input_devices']
self.model_device = input_devices['model']
jits = load_pickle(open_file_chunked(modeld_pkl_path(chestnut)), out_of_band=True)
variant = jits['variants'][f'{cam_w}x{cam_h}']
metadata = jits['metadata']
self.input_shapes = metadata['input_shapes']
self.vision_input_names = [k for k in self.input_shapes if 'img' in k]
self.output_slices = metadata['output_slices']
self.output_slices = pickle.loads(codecs.decode(metadata['metadata']['output_slices'].encode(), 'base64'))
self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
self.chestnut = chestnut
self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
self.frame_copy_size = nv12_copy_size(*get_nv12_info(cam_w, cam_h)[:3])
self.input_queues, self.npy, self.frame_views = make_input_queues(
self.input_shapes, self.frame_skip, device=self.model_device, frame_copy_size=self.frame_copy_size)
self.input_specs = variant['input_specs']
self.packed_specs = variant['packed_specs']
self.reset_inputs()
self.parser = Parser()
self.run_model = jits['run_model'][(cam_w,cam_h)]
self.run_model = variant['run']
def reset_inputs(self) -> None:
self.input_queues, views = allocate_inputs(self.input_specs, self.packed_specs)
self.frame_views = {name: views[name] for name in self.vision_input_names}
self.npy = {name: views[name] for name in self.packed_specs if name not in self.frame_views}
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()}
@@ -205,62 +204,48 @@ class ModelState(ModelStateBase):
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
inputs: dict[str, np.ndarray], after_enqueue: Callable[[], None] | None = None) -> dict[str, np.ndarray]:
for key, buf in bufs.items():
np.copyto(self.frame_views[key], np.frombuffer(buf.data, dtype=np.uint8, count=self.frame_copy_size))
np.copyto(self.frame_views[key], np.frombuffer(buf.data, dtype=np.uint8, count=self.frame_views[key].size))
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
inputs['desire_pulse'][0] = 0
self.npy['desire'][:] = np.where(inputs['desire_pulse'] - self.prev_desire > .99, inputs['desire_pulse'], 0)
self.npy['desire'].flat[:] = 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['action_t'][:] = inputs['action_t']
self.npy['tfm'][:,:] = transforms['img'][:,:]
self.npy['big_tfm'][:,:] = transforms['big_img'][:,:]
outs, = self.run_model(**{k: self.input_queues[k] for k in MODELD_INPUTS})
outs = self.run_model(**self.input_queues)
if after_enqueue is not None:
after_enqueue()
model_output = outs.numpy()[0]
if self.chestnut and not np.all(np.isfinite(model_output)):
raise RuntimeError("model output not finite")
outputs_dict = self.parser.parse_outputs(self.slice_outputs(model_output, self.output_slices))
self.npy['prev_feat'][:] = model_output[self.output_slices['hidden_state']]
self.npy['prev_feat'].flat[:] = model_output[self.output_slices['hidden_state']]
if SEND_RAW_PRED:
outputs_dict['raw_pred'] = model_output.copy()
return outputs_dict
def warmup(self) -> None:
dummy_frames = {k: np.zeros(self.frame_copy_size, dtype=np.uint8) for k in self.vision_input_names}
dummy_frames = {k: np.zeros_like(v) for k, v in self.frame_views.items()}
eye = np.eye(3, dtype=np.float32)
dims = {'desire_pulse': ModelConstants.DESIRE_LEN, 'traffic_convention': 2, 'action_t': 2}
self.run(dummy_frames, dict.fromkeys(self.vision_input_names, eye), {k: np.zeros(v, dtype=np.float32) for k, v in dims.items()})
self.input_queues, self.npy, self.frame_views = make_input_queues(
self.input_shapes, self.frame_skip, device=self.model_device, frame_copy_size=self.frame_copy_size)
self.reset_inputs()
self.prev_desire[:] = 0
def main(demo=False):
cloudlog.warning("modeld init")
chestnut_available = chestnut_present() and chestnut_compiled()
CHESTNUT = False
if chestnut_available:
poller = messaging.Poller()
sock = messaging.sub_sock("chestnutState", poller=poller, conflate=True)
deadline = time.monotonic() + 4. / SERVICE_LIST['deviceState'].frequency
while not CHESTNUT and (remaining := deadline - time.monotonic()) > 0.:
if not poller.poll(round(remaining * 1000)):
break
msg = messaging.recv_one_or_none(sock)
CHESTNUT = msg is not None and msg.valid and chestnut_ready(msg.chestnutState)
CHESTNUT = chestnut_present() and chestnut_compiled()
if CHESTNUT:
os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000'
params = Params()
params.put_bool("ChestnutLoading", CHESTNUT)
if chestnut_available and not CHESTNUT:
params.put_bool("ChestnutActive", False)
else:
params.remove("ChestnutActive")
params.remove("ChestnutActive")
config_realtime_process(7, 54)
@@ -304,21 +289,16 @@ def main(demo=False):
loader.start()
loader.join(BIG_MODEL_TIMEOUT)
model = big_model
if model is None:
params.put_bool("ChestnutModelError", True)
params.put_bool("ChestnutActive", model is not None)
if model is not None:
params.remove("ChestnutModelError")
small_model = ModelState(vipc_client_main.width, vipc_client_main.height, False) if model is None or CHESTNUT else None
if model is None:
model = small_model
params.put_bool("ChestnutLoading", False)
assert model is not None
cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting")
# messaging
pub_socks = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"] + (["chestnutState"] if CHESTNUT else [])
pub_socks = ["modelV2", "drivingModelData", "cameraOdometry"] + (["chestnutState"] if CHESTNUT else [])
pm = PubMaster(pub_socks)
sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"])
@@ -351,7 +331,6 @@ def main(demo=False):
prev_action = log.ModelDataV2.Action()
DH = DesireHelper()
RELC = RoadEdgeLaneChangeController()
while True:
# Keep receiving frames until we are at least 1 frame ahead of previous extra frame
@@ -391,7 +370,6 @@ def main(demo=False):
is_rhd = sm["driverMonitoringState"].isRHD
frame_id = sm["narrowRoadCameraState"].frameId
v_ego = max(sm["carState"].vEgo, 0.)
model.lat_delay = get_lat_delay(params, sm["lateralDelay"].lateralDelay)
lat_delay = sm["lateralDelay"].lateralDelay + LAT_SMOOTH_SECONDS
if sm.updated["extrinsicsCalibration"] and sm.seen['narrowRoadCameraState'] and sm.seen['deviceState']:
device_from_calib_euler = np.array(sm["extrinsicsCalibration"].rpyCalib, dtype=np.float32)
@@ -442,9 +420,7 @@ def main(demo=False):
raise
# fallback to small model
cloudlog.exception("big model failed, fall back to small")
params.put_bool("ChestnutModelError", True)
params.put_bool("ChestnutActive", False)
assert small_model is not None
model = small_model
if chestnut_state is not None:
chestnut_state.big = False
@@ -469,19 +445,15 @@ def main(demo=False):
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
lane_change_prob = l_lane_change_prob + r_lane_change_prob
mdv2sp_send = messaging.new_message('modelDataV2SP')
left_edge, right_edge = RELC.update_and_fill(modelv2_send.modelV2, mdv2sp_send.modelDataV2SP, v_ego)
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, left_edge, right_edge)
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
fill_driving_model_data(drivingdata_send, modelv2_send)
fill_pose_msg(posenet_send, model_output, meta_main.frame_id, vipc_dropped_frames, meta_main.timestamp_eof, extrinsics_calibration_seen)
pm.send('modelV2', modelv2_send)
pm.send('drivingModelData', drivingdata_send)
pm.send('cameraOdometry', posenet_send)
pm.send('modelDataV2SP', mdv2sp_send)
last_vipc_frame_id = meta_main.frame_id
if __name__ == "__main__":
-1
View File
@@ -225,7 +225,6 @@ class UIState(UIStateSP):
ChestnutState.UNCOMPILED if detected else ChestnutState.DISCONNECTED)
return
self.chestnut_present = self.chestnut_present or detected
model_seen = self.sm.recv_frame["modelV2"] > self.started_frame
if not self.chestnut_present:
self.chestnut_state = ChestnutState.DISCONNECTED
@@ -32,7 +32,7 @@ def _patch_tinygrad_fetch_fw():
helpers.fetch_fw = fetch_fw
_patch_tinygrad_fetch_fw()
import openpilot.selfdrive.modeld.compile_modeld as stock
import openpilot.sunnypilot.modeld_v2.stock_dependencies as stock
from tinygrad import dtypes
from tinygrad.device import Device
from tinygrad.engine.jit import TinyJit
+100
View File
@@ -0,0 +1,100 @@
"""
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 io
import struct
import pickle
import inspect
import importlib
import enum
def _pad_args(func, args, kwargs):
try:
sig = inspect.signature(func)
except Exception:
return args, kwargs
params = list(sig.parameters.values())
if inspect.isfunction(func) and params and params[0].name in ('cls', 'self'):
params = params[1:]
new_args = list(args)
has_varargs = any(p.kind == inspect.Parameter.VAR_POSITIONAL for p in params)
if len(new_args) > len(params) and not has_varargs:
new_args = new_args[:len(params)]
for i in range(len(new_args), len(params)):
param = params[i]
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
continue
val = param.default if param.default is not inspect.Parameter.empty else None
new_args.append(val)
return new_args, kwargs
def _enum_factory(enum_class):
def factory(*args, **kwargs):
try:
return enum_class(*args, **kwargs)
# OptOps and UOp objects in the .pkl are left over from the compilation phase,
# reassignment does nothing because they aren't tied to the execution graph
# It never executes or evaluates the UOp nodes again.
except ValueError:
return list(enum_class)[0]
factory.__name__ = enum_class.__name__
factory.__module__ = enum_class.__module__
return factory
def _dynamic_factory(real_class):
if isinstance(real_class, type) and issubclass(real_class, enum.Enum):
return _enum_factory(real_class)
def factory(*args, **kwargs):
try:
return real_class(*args, **kwargs)
except TypeError:
new_args, new_kwargs = _pad_args(real_class, args, kwargs)
return real_class(*new_args, **new_kwargs)
class DynamicMeta(type(real_class)):
def __call__(cls, *args, **kwargs):
return factory(*args, **kwargs)
class DynamicProxy(real_class, metaclass=DynamicMeta):
__slots__ = ()
def __new__(cls, *args, **kwargs):
return factory(*args, **kwargs)
DynamicProxy.__name__ = real_class.__name__
DynamicProxy.__module__ = real_class.__module__
return DynamicProxy
class DynamicTinygradUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module == "tinygrad.ops":
try:
importlib.import_module("tinygrad.uops")
module = "tinygrad.uops"
except ImportError:
pass
real_class = getattr(importlib.import_module(module), name)
if module.startswith("tinygrad"):
return _dynamic_factory(real_class)
return real_class
def load_oob(f):
opcodes = f.read(struct.unpack('<q', f.read(8))[0])
def buffers():
while (h := f.read(8)):
pb = pickle.PickleBuffer(bytearray(struct.unpack('<q', h)[0]))
f.readinto(pb)
yield pb
return DynamicTinygradUnpickler(io.BytesIO(opcodes), buffers=buffers()).load()
@@ -0,0 +1,174 @@
"""
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 codecs
import pickle
import numpy as np
from tinygrad.tensor import Tensor
from tinygrad_repo.examples.openpilot.helpers import allocate_inputs
from openpilot.sunnypilot.modeld_v2.stock_dependencies import MODELD_INPUTS, make_input_queues as make_stock_input_queues
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
from openpilot.sunnypilot.modeld_v2.compile_modeld import (derive_frame_skip, make_split_input_queues, make_supercombo_input_queues,
WARP_INPUTS, POLICY_INPUTS, nv12_copy_size)
class BaseModelAdapter:
def __init__(self, jits, cam_w, cam_h, model_device, queue_device, warp_device, chestnut=False):
self.jits = jits
self.DEV = model_device
self.QUEUE_DEV = queue_device
self.WARP_DEV = warp_device
self.chestnut = chestnut
self.cam_w = cam_w
self.cam_h = cam_h
self._combined_model_type = 'supercombo'
self._policy_slices_list = []
self._has_on_policy = False
self.policy_output_slices = {}
self._policy_keys = []
self.full_frames = {}
self._blob_cache = {}
self.frame_buffers = {}
self.frame_views = {}
self.nv12_info = get_nv12_info(cam_w, cam_h)
self.is_native = False
def _init_common(self):
self._desire_key = next((key for key in getattr(self, 'numpy_inputs', {}) if key.startswith('desire')), 'desire')
self._road_key = next((key for key in getattr(self, '_vision_input_names', []) if 'big' not in key), 'img')
self._wide_key = next((key for key in getattr(self, '_vision_input_names', []) if 'big' in key), 'big_img')
self.frame_buf_params = dict.fromkeys(getattr(self, '_vision_input_names', ['img', 'big_img']), self.nv12_info)
def get_dummy_inputs(self):
dummy_size = getattr(self, 'frame_copy_size', self.frame_buf_params[self._road_key][3])
if getattr(self, 'is_run_model', True) is False:
dummy_size = self.frame_buf_params[self._road_key][3]
dummy_frames = {
k: np.zeros(self.frame_views[k].size, dtype=np.uint8) if self.is_native else np.zeros(dummy_size, dtype=np.uint8)
for k in self._vision_input_names
}
transforms = {k: np.eye(3, dtype=np.float32) for k in [self._road_key, self._wide_key] if k}
dummy_inputs = {k: np.zeros(v.shape, dtype=v.dtype) for k, v in self.numpy_inputs.items() if k not in ['tfm', 'big_tfm', 'prev_feat']}
return dummy_frames, transforms, dummy_inputs
class LegacyModelAdapter(BaseModelAdapter):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
metadata = self.jits['metadata']
self.is_run_model = 'run_model' in self.jits
self.frame_copy_size = nv12_copy_size(*self.nv12_info[:3])
if self.is_run_model or 'model' in metadata:
model_metadata = metadata.get('model', metadata)
self.input_shapes = model_metadata['input_shapes']
self.vision_output_slices = model_metadata['output_slices']
self._vision_input_names = [key for key in self.input_shapes if 'img' in key]
self.frame_skip = derive_frame_skip({}, self.input_shapes)
if self.is_run_model:
self.input_queues, self.numpy_inputs, self.frame_buffers = make_stock_input_queues(
self.input_shapes, self.frame_skip, device=self.DEV, frame_copy_size=self.frame_copy_size)
self.frame_views, self.npy = self.frame_buffers, self.numpy_inputs
self.run_model, self.run_policy, self.warp = self.jits['run_model'][(self.cam_w, self.cam_h)], None, None
else:
self.input_queues, self.numpy_inputs = make_supercombo_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV)
self.run_model, self.run_policy, self.warp = None, self.jits['run_policy'], self.jits[(self.cam_w, self.cam_h)]
else:
self.run_model, self.run_policy, self.warp = None, self.jits['run_policy'], self.jits[(self.cam_w, self.cam_h)]
vision_metadata = metadata['vision']
policy_keys = [k for k in metadata if k not in ('vision', 'warp_dev')]
self._combined_model_type = 'split' if policy_keys == ['policy'] else 'multi_policy'
self.vision_output_slices = vision_metadata['output_slices']
self._policy_keys = policy_keys
self._policy_slices_list = [metadata[k]['output_slices'] for k in policy_keys]
self.policy_output_slices = self._policy_slices_list[0]
self._has_on_policy = any('on' in k.lower() for k in policy_keys)
self._vision_input_names = [key for key in vision_metadata['input_shapes'] if 'img' in key]
first_policy_meta = metadata[policy_keys[0]]
self.frame_skip = derive_frame_skip(vision_metadata['input_shapes'], first_policy_meta['input_shapes'])
self.input_queues, self.numpy_inputs = make_split_input_queues(vision_metadata['input_shapes'],
first_policy_meta['input_shapes'],
self.frame_skip, device=self.QUEUE_DEV)
self._init_common()
if self.warp is not None:
self.full_frames = {k: Tensor(np.zeros(self.nv12_info[3], dtype=np.uint8), device=self.WARP_DEV).contiguous().realize() for k in self._vision_input_names}
self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=self.full_frames[self._road_key], big_frame=self.full_frames[self._wide_key])
def copy_frames(self, bufs):
if getattr(self, 'is_run_model', True):
for key, buf in bufs.items():
data = buf.data if hasattr(buf, 'data') else buf
np.copyto(self.frame_buffers[key], np.frombuffer(data, dtype=np.uint8, count=self.frame_copy_size))
else:
for key, buf in bufs.items():
ptr = np.frombuffer(buf.data, dtype=np.uint8).ctypes.data
cache_key = (key, ptr)
if cache_key not in self._blob_cache:
self._blob_cache[cache_key] = Tensor.from_blob(ptr, (self.frame_buf_params[key][3],), dtype='uint8', device=self.WARP_DEV)
self.full_frames[key] = self._blob_cache[cache_key]
def reset_warmup_buffers(self):
if getattr(self, 'is_run_model', True):
self.input_queues, self.numpy_inputs, self.frame_buffers = make_stock_input_queues(
self.input_shapes, self.frame_skip, device=self.DEV, frame_copy_size=self.frame_copy_size)
self.frame_views = self.frame_buffers
self.npy = self.numpy_inputs
else:
for v in self.numpy_inputs.values():
v[:] = 0
self.full_frames.clear()
self._blob_cache.clear()
def run(self):
if self.run_model is not None:
outs, = self.run_model(**{k: self.input_queues[k] for k in MODELD_INPUTS})
return outs
else:
assert self.warp is not None and self.run_policy is not None
warped = self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=self.full_frames[self._road_key], big_frame=self.full_frames[self._wide_key])
raw_outputs = self.run_policy(**{k: self.input_queues[k] for k in POLICY_INPUTS if k in self.input_queues}, warped=warped)
return raw_outputs
class NativeTinygradAdapter(BaseModelAdapter):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.is_native = True
variant = self.jits['variants'][f'{self.cam_w}x{self.cam_h}']
self.input_specs = variant['input_specs']
self.packed_specs = variant['packed_specs']
self.run_model = variant['run']
self.vision_output_slices = pickle.loads(codecs.decode(self.jits['metadata']['metadata']['output_slices'].encode(), 'base64'))
self._vision_input_names = ['img', 'big_img']
self.reset_warmup_buffers()
self._init_common()
def copy_frames(self, bufs):
for key, buf in bufs.items():
data = buf.data if hasattr(buf, 'data') else buf
if key in self.frame_views:
np.copyto(self.frame_views[key], np.frombuffer(data, dtype=np.uint8, count=self.frame_views[key].size))
def reset_warmup_buffers(self) -> None:
self.input_queues, views = allocate_inputs(self.input_specs, self.packed_specs)
self.frame_views = {name: views[name] for name in self._vision_input_names if name in views}
self.numpy_inputs = {name: views[name] for name in views if name not in self.frame_views}
def run(self):
outs = self.run_model(**self.input_queues)
return outs
def get_model_adapter(jits, cam_w, cam_h, model_device, queue_device, warp_device, chestnut=False):
if 'variants' in jits:
return NativeTinygradAdapter(jits, cam_w, cam_h, model_device, queue_device, warp_device, chestnut=chestnut)
else:
return LegacyModelAdapter(jits, cam_w, cam_h, model_device, queue_device, warp_device, chestnut=chestnut)
+27 -101
View File
@@ -13,11 +13,10 @@ import numpy as np
import threading
import time
from setproctitle import setproctitle
from tinygrad.tensor import Tensor
import openpilot.cereal.messaging as messaging
from openpilot.common.hardware import COMMA_HARDWARE
from openpilot.selfdrive.modeld.helpers import chestnut_present, load_oob
from openpilot.selfdrive.modeld.helpers import chestnut_present
from openpilot.cereal import log
from opendbc.car.structs import car
from openpilot.cereal.services import SERVICE_LIST
@@ -33,25 +32,19 @@ from openpilot.common.realtime import config_realtime_process, DT_MDL
from openpilot.common.transformations.camera import DEVICE_CAMERAS
from openpilot.common.transformations.model import get_warp_matrix
from openpilot.system import sentry
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value
from openpilot.selfdrive.modeld.modeld import ChestnutState
from openpilot.selfdrive.modeld.compile_modeld import (
MODELD_INPUTS,
make_input_queues as make_stock_input_queues,
)
from openpilot.sunnypilot.modeld_v2.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState, get_curvature_from_output
from openpilot.sunnypilot.modeld_v2.parse_model_outputs import Parser
from openpilot.sunnypilot.modeld_v2.constants import ModelConstants, Plan
from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants
from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper
from openpilot.sunnypilot.modeld_v2.compile_modeld import (derive_frame_skip, make_split_input_queues,
make_supercombo_input_queues, nv12_copy_size,
WARP_INPUTS, POLICY_INPUTS)
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
from openpilot.sunnypilot.modeld_v2.helpers import load_oob
from openpilot.sunnypilot.modeld_v2.model_adapters import get_model_adapter
from openpilot.sunnypilot.models.helpers import get_active_bundle
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
@@ -122,52 +115,19 @@ class ModelState(ModelStateBase):
self.WARP_DEV = metadata.get('warp_dev', 'QCOM') if COMMA_HARDWARE else 'CPU'
self.DEV = ('AMD' if self.chestnut else 'QCOM') if COMMA_HARDWARE else 'CPU'
self.QUEUE_DEV = self.DEV
self.is_run_model = 'run_model' in jits
nv12_info = get_nv12_info(cam_w, cam_h)
self.frame_copy_size = nv12_copy_size(*nv12_info[:3])
self.full_frames: dict = {}
self._blob_cache: dict = {}
self.frame_buffers: dict = {}
if self.is_run_model or 'model' in metadata:
model_metadata = metadata.get('model', metadata)
self.input_shapes = model_metadata['input_shapes']
self.vision_output_slices = model_metadata['output_slices']
self.policy_output_slices = {}
self._policy_slices_list = []
self._combined_model_type = 'supercombo'
self._vision_input_names = [key for key in self.input_shapes if 'img' in key]
self.frame_skip = derive_frame_skip({}, self.input_shapes)
if self.is_run_model:
self.input_queues, self.numpy_inputs, self.frame_buffers = make_stock_input_queues(
self.input_shapes, self.frame_skip, device=self.DEV, frame_copy_size=self.frame_copy_size)
self.frame_views, self.npy = self.frame_buffers, self.numpy_inputs
self.run_model, self.run_policy, self.warp = jits['run_model'][(cam_w, cam_h)], None, None
else:
self.input_queues, self.numpy_inputs = make_supercombo_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV)
self.run_model, self.run_policy, self.warp = None, jits['run_policy'], jits[(cam_w, cam_h)]
else:
self.run_model, self.run_policy, self.warp = None, jits['run_policy'], jits[(cam_w, cam_h)]
vision_metadata = metadata['vision']
policy_keys = [k for k in metadata if k not in ('vision', 'warp_dev')]
self._combined_model_type = 'split' if policy_keys == ['policy'] else 'multi_policy'
self.vision_output_slices = vision_metadata['output_slices']
self._policy_keys = policy_keys
self._policy_slices_list = [metadata[k]['output_slices'] for k in policy_keys]
self.policy_output_slices = self._policy_slices_list[0]
self._has_on_policy = any('on' in k.lower() for k in policy_keys)
self._vision_input_names = [key for key in vision_metadata['input_shapes'] if 'img' in key]
first_policy_meta = metadata[policy_keys[0]]
frame_skip = derive_frame_skip(vision_metadata['input_shapes'], first_policy_meta['input_shapes'])
self.input_queues, self.numpy_inputs = make_split_input_queues(vision_metadata['input_shapes'],
first_policy_meta['input_shapes'],
frame_skip, device=self.QUEUE_DEV)
self._desire_key = next(key for key in self.numpy_inputs if key.startswith('desire'))
self._road_key = next(key for key in self._vision_input_names if 'big' not in key)
self._wide_key = next(key for key in self._vision_input_names if 'big' in key)
self.frame_buf_params = dict.fromkeys(self._vision_input_names, nv12_info)
self.adapter = get_model_adapter(jits, cam_w, cam_h, self.DEV, self.QUEUE_DEV, self.WARP_DEV, self.chestnut)
self.vision_output_slices = self.adapter.vision_output_slices
self.policy_output_slices = self.adapter.policy_output_slices
self._policy_slices_list = self.adapter._policy_slices_list
self._combined_model_type = self.adapter._combined_model_type
self._vision_input_names = self.adapter._vision_input_names
self.numpy_inputs = self.adapter.numpy_inputs
self._policy_keys = self.adapter._policy_keys
self._has_on_policy = self.adapter._has_on_policy
self._desire_key = self.adapter._desire_key
self._road_key = self.adapter._road_key
self._wide_key = self.adapter._wide_key
self.frame_buf_params = self.adapter.frame_buf_params
is_20hz = bundle.is20hz if bundle else self._combined_model_type in ('split', 'multi_policy')
if is_20hz:
@@ -179,26 +139,10 @@ class ModelState(ModelStateBase):
self.parser = Parser()
self.prev_desire = np.zeros(self.constants.DESIRE_LEN, dtype=np.float32)
if self.warp is not None:
self.full_frames = {k: Tensor(np.zeros(nv12_info[3], dtype=np.uint8), device=self.WARP_DEV).contiguous().realize() for k in self._vision_input_names}
self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=self.full_frames[self._road_key], big_frame=self.full_frames[self._wide_key])
def warmup(self) -> None:
dummy_size = self.frame_copy_size if self.is_run_model else self.frame_buf_params[self._road_key][3]
dummy_frames = {k: np.zeros(dummy_size, dtype=np.uint8) for k in self._vision_input_names}
transforms = {k: np.eye(3, dtype=np.float32) for k in [self._road_key, self._wide_key] if k}
dummy_inputs = {k: np.zeros(v.shape, dtype=v.dtype) for k, v in self.numpy_inputs.items() if k not in ['tfm', 'big_tfm', 'prev_feat']}
dummy_frames, transforms, dummy_inputs = self.adapter.get_dummy_inputs()
self.run(dummy_frames, transforms, dummy_inputs)
if self.is_run_model:
self.input_queues, self.numpy_inputs, self.frame_buffers = make_stock_input_queues(
self.input_shapes, self.frame_skip, device=self.DEV, frame_copy_size=self.frame_copy_size)
self.frame_views = self.frame_buffers
self.npy = self.numpy_inputs
else:
for v in self.numpy_inputs.values():
v[:] = 0
self.full_frames.clear()
self._blob_cache.clear()
self.adapter.reset_warmup_buffers()
self.prev_desire[:] = 0
@property
@@ -216,21 +160,12 @@ class ModelState(ModelStateBase):
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
inputs: dict[str, np.ndarray],
after_enqueue: Callable[[], None] | None = None) -> dict[str, np.ndarray] | None:
if self.is_run_model:
for key, buf in bufs.items():
data = buf.data if hasattr(buf, 'data') else buf
np.copyto(self.frame_buffers[key], np.frombuffer(data, dtype=np.uint8, count=self.frame_copy_size))
else:
for key, buf in bufs.items():
ptr = np.frombuffer(buf.data, dtype=np.uint8).ctypes.data
cache_key = (key, ptr)
if cache_key not in self._blob_cache:
self._blob_cache[cache_key] = Tensor.from_blob(ptr, (self.frame_buf_params[key][3],), dtype='uint8', device=self.WARP_DEV)
self.full_frames[key] = self._blob_cache[cache_key]
self.adapter.copy_frames(bufs)
desire_key = self.desire_key
inputs[desire_key][0] = 0
self.numpy_inputs[desire_key][:] = np.where(inputs[desire_key] - self.prev_desire > .99, inputs[desire_key], 0)
(self.numpy_inputs[desire_key].flat if self.adapter.is_native else self.numpy_inputs[desire_key])[:] = \
np.where(inputs[desire_key] - self.prev_desire > .99, inputs[desire_key], 0)
self.prev_desire[:] = inputs[desire_key]
for key in ('traffic_convention', 'lateral_control_params', 'action_t'):
@@ -240,13 +175,7 @@ class ModelState(ModelStateBase):
self.numpy_inputs['tfm'][:, :] = transforms[self._road_key].reshape(3, 3)
self.numpy_inputs['big_tfm'][:, :] = transforms[self._wide_key].reshape(3, 3)
if self.run_model is not None:
outs, = self.run_model(**{k: self.input_queues[k] for k in MODELD_INPUTS})
raw_outputs = outs
else:
assert self.warp is not None and self.run_policy is not None
warped = self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=self.full_frames[self._road_key], big_frame=self.full_frames[self._wide_key])
raw_outputs = self.run_policy(**{k: self.input_queues[k] for k in POLICY_INPUTS if k in self.input_queues}, warped=warped)
raw_outputs = self.adapter.run()
if after_enqueue is not None:
after_enqueue()
@@ -258,14 +187,16 @@ class ModelState(ModelStateBase):
sliced = {k: model_output[np.newaxis, v] for k, v in self.vision_output_slices.items()}
outputs = self.parser.parse_outputs(sliced)
if 'prev_feat' in self.numpy_inputs and 'hidden_state' in self.vision_output_slices:
self.numpy_inputs['prev_feat'][:] = model_output[self.vision_output_slices['hidden_state']]
(self.numpy_inputs['prev_feat'].flat if self.adapter.is_native else self.numpy_inputs['prev_feat'])[:] = \
model_output[self.vision_output_slices['hidden_state']]
else:
vision_output = raw_outputs[0].numpy().flatten()
vision_sliced = {k: vision_output[np.newaxis, v] for k, v in self.vision_output_slices.items()}
outputs = self.parser.parse_vision_outputs(vision_sliced)
if 'prev_feat' in self.numpy_inputs and 'hidden_state' in self.vision_output_slices:
self.numpy_inputs['prev_feat'][:] = vision_output[self.vision_output_slices['hidden_state']]
(self.numpy_inputs['prev_feat'].flat if self.adapter.is_native else self.numpy_inputs['prev_feat'])[:] = \
vision_output[self.vision_output_slices['hidden_state']]
for i, policy_slices in enumerate(self._policy_slices_list):
policy_output = raw_outputs[i + 1].numpy().flatten()
@@ -372,11 +303,7 @@ def main(demo=False):
loader.start()
loader.join(BIG_MODEL_TIMEOUT)
model = big_model
if model is None:
params.put_bool("ChestnutModelError", True)
params.put_bool("ChestnutActive", model is not None)
if model is not None:
params.remove("ChestnutModelError")
small_model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, chestnut=False) if model is None or CHESTNUT else None
if model is None:
@@ -518,8 +445,7 @@ def main(demo=False):
except Exception:
if not params.get_bool("ChestnutActive"):
raise
cloudlog.exception("chestnut failed, falling back to small")
params.put_bool("ChestnutModelError", True)
cloudlog.exception("big model failed, fall back to small")
params.put_bool("ChestnutActive", False)
assert small_model is not None
model = small_model
+167
View File
@@ -0,0 +1,167 @@
"""
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 math
import numpy as np
from collections import namedtuple
from tinygrad.tensor import Tensor
from tinygrad.helpers import Context
from tinygrad.device import Device
"""
Frozen in time compile_modeld dependencies to support all models prior to transition to tinygrad compilation.
This file is not meant to be modified.
"""
NV12Frame = namedtuple("NV12Frame", ['width', 'height', 'stride', 'y_height', 'uv_height', 'size'])
MODELD_INPUTS = ['img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs']
def nv12_copy_size(stride: int, y_height: int, uv_height: int) -> int:
return stride * (y_height + uv_height)
def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad, border_fill_val=None):
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)
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_round = Tensor.round(src_x)
y_round = Tensor.round(src_y)
x_nn_clipped = x_round.clip(0, w_src - 1).cast('int')
y_nn_clipped = y_round.clip(0, h_src - 1).cast('int')
idx = y_nn_clipped * (w_src + stride_pad) + x_nn_clipped
sampled = src_flat[idx]
if border_fill_val is None:
return sampled
in_bounds = ((x_round >= 0) & (x_round <= w_src - 1) &
(y_round >= 0) & (y_round <= h_src - 1)).cast(sampled.dtype)
return sampled * in_bounds + Tensor(border_fill_val, dtype=sampled.dtype) * (1 - in_bounds)
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):
M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]], device=Device.DEFAULT)
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 get_policy_npy_shapes(input_shapes):
dp = input_shapes['desire_pulse']
tc = input_shapes['traffic_convention']
at = input_shapes['action_t']
fb = input_shapes['features_buffer']
feat_dim = math.prod(fb[2:])
shapes = {'desire': (dp[2],), 'traffic_convention': tuple(tc), 'action_t': tuple(at), 'prev_feat': (fb[0], feat_dim)}
return shapes, [math.prod(s) for s in shapes.values()]
def make_input_queues(input_shapes, frame_skip, device, frame_copy_size):
img = input_shapes['img']
fb = input_shapes['features_buffer']
feat_dim = math.prod(fb[2:])
dp = input_shapes['desire_pulse']
n_frames = img[1] // 6
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3])
policy_shapes, _ = get_policy_npy_shapes(input_shapes)
shapes = {'tfm': (3, 3), 'big_tfm': (3, 3)} | policy_shapes
sizes = [math.prod(s) for s in shapes.values()]
packed_npy_size = sum(sizes) * np.dtype(np.float32).itemsize
packed_input = np.zeros(packed_npy_size + 2 * frame_copy_size, dtype=np.uint8)
packed_npy_inputs = packed_input[:packed_npy_size].view(np.float32)
frames = packed_input[packed_npy_size:]
frame_views = {'img': frames[:frame_copy_size], 'big_img': frames[frame_copy_size:]}
npy = {k: v.reshape(s) for (k, s), v in zip(shapes.items(), np.split(packed_npy_inputs, np.cumsum(sizes[:-1])), strict=True)}
input_queues = {
'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
'feat_q': Tensor(np.zeros((frame_skip * fb[1], fb[0], feat_dim), dtype=np.float32), device=device).contiguous().realize(),
'desire_q': Tensor(np.zeros((frame_skip * dp[1], dp[0], dp[2]), dtype=np.float32), device=device).contiguous().realize(),
'packed_npy_inputs': Tensor(packed_input, device='NPY').realize(),
}
return input_queues, npy, frame_views
def shift_and_sample(buf, new_val, sample_fn):
buf.assign(buf[1:].cat(new_val, dim=0).contiguous())
return sample_fn(buf)
def sample_skip(buf, frame_skip):
return buf[::frame_skip].contiguous().flatten(0, 1).unsqueeze(0)
def sample_desire(buf, frame_skip):
return buf.reshape(-1, frame_skip, *buf.shape[1:]).max(1).flatten(0, 1).unsqueeze(0)
def make_warp(nv12, model_w, model_h):
frame_prepare = make_frame_prepare(nv12, model_w, model_h)
def warp(tfm, big_tfm, frame, big_frame):
tfm = tfm.to(Device.DEFAULT)
big_tfm = big_tfm.to(Device.DEFAULT)
frame = frame.to(Device.DEFAULT)
big_frame = big_frame.to(Device.DEFAULT)
Tensor.realize(tfm, big_tfm, frame, big_frame)
warped_frame = frame_prepare(frame, tfm).unsqueeze(0)
warped_big_frame = frame_prepare(big_frame, big_tfm).unsqueeze(0)
return Tensor.cat(warped_frame, warped_big_frame)
return warp
def make_run_model(warp, run_policy, model_metadata, frame_copy_size):
_, policy_sizes = get_policy_npy_shapes(model_metadata['input_shapes'])
packed_npy_size = (18 + sum(policy_sizes)) * np.dtype(np.float32).itemsize
def run_model(img_q, big_img_q, feat_q, desire_q, packed_npy_inputs):
packed_input = packed_npy_inputs.to(Device.DEFAULT)
Tensor.realize(packed_input)
packed_npy_inputs = packed_input[:packed_npy_size].bitcast('float32')
frame = packed_input[packed_npy_size:packed_npy_size + frame_copy_size]
big_frame = packed_input[packed_npy_size + frame_copy_size:]
tfm, big_tfm, policy_inputs = packed_npy_inputs.split([9, 9, sum(policy_sizes)])
warped = warp(tfm.reshape(3, 3), big_tfm.reshape(3, 3), frame, big_frame)
return run_policy(warped, img_q, big_img_q, feat_q, desire_q, policy_inputs)
return run_model
@@ -7,6 +7,8 @@ See the LICENSE.md file in the root directory for more details.
import pathlib
import tempfile
import codecs
import pickle
import openpilot.sunnypilot.models.helpers as helpers
import openpilot.sunnypilot.modeld_v2.modeld as modeld_module
@@ -161,6 +163,16 @@ ARCHETYPES = {
def make_pkl_data(archetype):
if archetype.expected_model_type == 'supercombo':
slices_b64 = codecs.encode(pickle.dumps(archetype.metadata_structure['model']['output_slices']), 'base64').decode()
return {
'metadata': {
'metadata': {'output_slices': slices_b64},
'input_shapes': archetype.metadata_structure['model']['input_shapes']
},
'variants': {f'{CAM_W}x{CAM_H}': {'input_specs': {}, 'packed_specs': {}, 'run': _noop_jit}},
}
return {
'metadata': archetype.metadata_structure,
'run_policy': _noop_jit,
@@ -12,7 +12,7 @@ import openpilot.sunnypilot.modeld_v2.modeld as modeld_module
from openpilot.sunnypilot.modeld_v2.modeld import _find_driving_pkl
from openpilot.sunnypilot.modeld_v2.tests import helpers as tests_helpers
from openpilot.sunnypilot.modeld_v2.tests.helpers import DummyModel, DummyBundle, ARCHETYPES, CAM_W, CAM_H, \
SPLIT_VISION_INPUT_SHAPES, SPLIT_POLICY_INPUT_SHAPES
SPLIT_VISION_INPUT_SHAPES
from openpilot.common.test import OpenpilotTestCase
# resolved by name from this module when a test asks for them
@@ -66,21 +66,6 @@ class TestModelStateCombinedInit(OpenpilotTestCase):
class TestStockEquivalence(OpenpilotTestCase):
def test_split_queue_keys_match_stock(self, model_state_factory):
from openpilot.selfdrive.modeld.compile_modeld import make_input_queues
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip
state = model_state_factory(ARCHETYPES['vision_policy_split'])
frame_skip = derive_frame_skip(SPLIT_VISION_INPUT_SHAPES, SPLIT_POLICY_INPUT_SHAPES)
stock_shapes = {**SPLIT_VISION_INPUT_SHAPES, **SPLIT_POLICY_INPUT_SHAPES, 'action_t': (1, 2)}
stock_queues, stock_npy, _frame_views = make_input_queues(stock_shapes, frame_skip, device='NPY', frame_copy_size=49152)
# sunnypilot split pipeline has tfm/big_tfm as queues (stock has them in npy only)
assert set(stock_queues.keys()) <= set(state.input_queues.keys())
assert {'desire', 'traffic_convention'} <= set(state.numpy_inputs.keys())
def test_split_queue_keys_work_with_desire_key(self, model_state_factory):
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues
@@ -108,8 +93,11 @@ class TestStockEquivalence(OpenpilotTestCase):
from openpilot.selfdrive.modeld.helpers import dump_oob
shapes = {'img': (1, 12, 128, 256), 'big_img': (1, 12, 128, 256), 'features_buffer': (1, 24, 32, 512),
'desire_pulse': (1, 25, 8), 'traffic_convention': (1, 2), 'action_t': (1, 2)}
pkl_data = {'metadata': {'model': {'input_shapes': shapes, 'output_slices': {}}},
'run_model': {(CAM_W, CAM_H): tests_helpers._noop_jit}}
import codecs
import pickle
slices_b64 = codecs.encode(pickle.dumps({}), 'base64').decode()
pkl_data = {'metadata': {'metadata': {'output_slices': slices_b64}},
'variants': {f'{CAM_W}x{CAM_H}': {'input_specs': {}, 'packed_specs': {}, 'run': tests_helpers._noop_jit}}}
with open(tmp_path / 'driving_test_tinygrad.pkl', 'wb') as f:
dump_oob(pkl_data, f)
bundle = DummyBundle(models=[DummyModel('supercombo', 'driving_test_tinygrad.pkl')])
@@ -0,0 +1,43 @@
"""
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 unittest
from unittest.mock import patch
from openpilot.common.file_chunker import open_file_chunked
from openpilot.sunnypilot.modeld_v2.helpers import load_oob
from tinygrad.device import Device
class TestLegacyModels(unittest.TestCase):
def test_legacy_model_load(self):
base_name = os.environ.get("MODEL_BASE_NAME")
if not base_name:
raise unittest.SkipTest("MODEL_BASE_NAME env var not set, skipping integration test.")
chunk_dir = os.environ.get("MODEL_CHUNK_DIR", "/tmp/model_chunks")
base_path = os.path.join(chunk_dir, base_name)
try:
f = open_file_chunked(base_path)
except Exception as error:
self.fail(f"Failed to open chunked file {base_path}: {error}")
self.addCleanup(f.close)
real_getitem = Device.__class__.__getitem__
def safe_getitem(device_self, ix):
if ix == "QCOM" and not os.path.exists("/dev/kgsl-3d0"):
return real_getitem(device_self, "CPU")
if ix == "AMD" and not os.path.exists("/dev/kfd"):
return real_getitem(device_self, "CPU")
return real_getitem(device_self, ix)
with patch.object(Device.__class__, "__getitem__", safe_getitem):
obj = load_oob(f)
assert isinstance(obj, dict), "Parsed object is not a dictionary"
assert "metadata" in obj, "Metadata key is missing"
@@ -1,24 +0,0 @@
import requests
from openpilot.sunnypilot.models.tinygrad_ref import get_tinygrad_ref
from openpilot.sunnypilot.models.fetcher import ModelFetcher
from openpilot.common.test import OpenpilotTestCase
def fetch_tinygrad_ref():
response = requests.get(ModelFetcher.MODEL_URL, timeout=10)
response.raise_for_status()
json_data = response.json()
return json_data.get("tinygrad_ref")
class TestTinygradRef(OpenpilotTestCase):
def test_tinygrad_ref(self):
current_ref = get_tinygrad_ref()
remote_ref = fetch_tinygrad_ref()
assert remote_ref == current_ref, (
f"""tinygrad_repo ref does not match remote tinygrad_ref of current compiled driving models json.
Current: {current_ref}
Remote: {remote_ref}
Please run build-all workflow to update models."""
)
print("tinygrad_repo ref matches current compiled driving models json ref.")
@@ -1675,12 +1675,6 @@
"widget": "toggle",
"title": "Onroad Uploads"
},
{
"key": "AuxPowerSave",
"widget": "toggle",
"title": "Disable Aux Port When Offroad",
"description": "Power off the aux USB-C port while offroad to save power. It powers back on automatically when you go onroad."
},
{
"key": "MaxTimeOffroad",
"widget": "option",
@@ -30,10 +30,6 @@ sections:
- key: OnroadUploads
widget: toggle
title: Onroad Uploads
- key: AuxPowerSave
widget: toggle
title: Disable Aux Port When Offroad
description: Power off the aux USB-C port while offroad to save power. It powers back on automatically when you go onroad.
- key: MaxTimeOffroad
widget: option
title: Max Time Offroad
-15
View File
@@ -21,7 +21,6 @@ from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE
from openpilot.common.basedir import BASEDIR
from openpilot.common.git import get_short_branch
from openpilot.common.hardware.usb import CHESTNUT_FW_VERSION, CHESTNUT_USB_PRODUCT, get_usb_state, get_usb_topology, is_chestnut_usb_id, set_usb_state
from openpilot.system.hardware.chestnut.flash import VBUS_PATH
from openpilot.common.linux import LinuxSystemStats
from openpilot.system.loggerd.config import get_available_percent
from openpilot.common.swaglog import cloudlog
@@ -51,10 +50,6 @@ class Chestnut:
self.last_attempt = 0.
self.flashed = False
self.mismatch = False
self.vbus_on = None
self.params = Params()
self.powersave = False
self.last_offroad = None
@property
def failed(self) -> bool:
@@ -66,19 +61,9 @@ class Chestnut:
cloudlog.event("chestnut flash done", returncode=ret.returncode, output=ret.stdout[-1000:], error=ret.returncode != 0)
self.flashed = ret.returncode == 0
def set_vbus(self, on: bool) -> None:
if on == self.vbus_on:
return
subprocess.run(["sudo", "tee", VBUS_PATH], input=b"1" if on else b"0", stdout=subprocess.DEVNULL, check=False)
self.vbus_on = on
def update(self, offroad: bool, usb_state: list[dict]) -> None:
self.mismatch = any(is_chestnut_usb_id(d["vendorId"], d["productId"], include_bootloader=True) and
d["product"] != CHESTNUT_USB_PRODUCT for d in usb_state)
if offroad != self.last_offroad:
self.powersave = self.params.get_bool("AuxPowerSave")
self.last_offroad = offroad
self.set_vbus((not offroad or self.mismatch) or not self.powersave)
if not self.mismatch:
self.flashed = False
return