mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-07-26 05:12:19 +08:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 77d09a7a16 | |||
| f57f1bd19d | |||
| 2f1d422990 | |||
| 28fa87d547 | |||
| 2430eed759 | |||
| b8fa2c7535 | |||
| a39aee2c24 | |||
| cff8b7c1f0 | |||
| c6991ead50 | |||
| 3e950b95ca | |||
| a5fd13ec4d | |||
| 6e99dc3482 | |||
| 791d63cd81 | |||
| f166bfcdc5 | |||
| 418c8154be |
@@ -12,11 +12,11 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
recompiled_dir:
|
recompiled_dir:
|
||||||
description: 'Existing recompiled directory number (e.g. 1 for recompiled1)'
|
description: 'Existing recompiled directory number (e.g. 3 for recompiled3)'
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
json_version:
|
json_version:
|
||||||
description: 'driving_models version number to update (e.g. 18 for driving_models_v18.json)'
|
description: 'driving_models version number to update (e.g. 5 for driving_models_v5.json)'
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
artifact_suffix:
|
artifact_suffix:
|
||||||
@@ -63,11 +63,12 @@ on:
|
|||||||
default: 'None'
|
default: 'None'
|
||||||
options:
|
options:
|
||||||
- None
|
- None
|
||||||
- Master Models
|
- Simple Plan Models
|
||||||
- Release Models
|
- Space Lab Models
|
||||||
- 2025 World Models
|
- TR Models
|
||||||
- 2026 World Models
|
- DTR Models
|
||||||
- Custom Merge Models
|
- Custom Merge Models
|
||||||
|
- FOF series models
|
||||||
- Other
|
- Other
|
||||||
custom_model_folder:
|
custom_model_folder:
|
||||||
description: 'Custom model folder name (if "Other" selected)'
|
description: 'Custom model folder name (if "Other" selected)'
|
||||||
|
|||||||
@@ -30,11 +30,6 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: ''
|
default: ''
|
||||||
target_hardware:
|
|
||||||
description: 'Hardware target to compile for (qcom or usbgpu)'
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
default: 'qcom'
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
upstream_branch:
|
upstream_branch:
|
||||||
@@ -51,14 +46,6 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
default: true
|
||||||
target_hardware:
|
|
||||||
description: 'Hardware target to compile for'
|
|
||||||
required: true
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- qcom
|
|
||||||
- usbgpu
|
|
||||||
default: 'qcom'
|
|
||||||
|
|
||||||
|
|
||||||
run-name: Build model [${{ inputs.custom_name || inputs.upstream_branch }}] from ref [${{ inputs.upstream_branch }}]
|
run-name: Build model [${{ inputs.custom_name || inputs.upstream_branch }}] from ref [${{ inputs.upstream_branch }}]
|
||||||
@@ -174,7 +161,7 @@ jobs:
|
|||||||
name: models-${{ env.REF }}${{ inputs.artifact_suffix }}
|
name: models-${{ env.REF }}${{ inputs.artifact_suffix }}
|
||||||
path: ${{ env.MODELS_DIR }}
|
path: ${{ env.MODELS_DIR }}
|
||||||
- run: |
|
- run: |
|
||||||
rm -f ${{ env.MODELS_DIR }}/{dmonitoring_model,big_driving_policy,big_driving_vision,big_driving_supercombo}.onnx
|
rm -f ${{ env.MODELS_DIR }}/{dmonitoring_model,big_driving_policy,big_driving_vision}.onnx
|
||||||
|
|
||||||
- name: Build Model
|
- name: Build Model
|
||||||
run: |
|
run: |
|
||||||
@@ -186,17 +173,7 @@ jobs:
|
|||||||
COMPILE_MODELD="${{ github.workspace }}/openpilot/sunnypilot/modeld_v2/compile_modeld.py"
|
COMPILE_MODELD="${{ github.workspace }}/openpilot/sunnypilot/modeld_v2/compile_modeld.py"
|
||||||
MODEL_SIZE=$(python3 -c "from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')")
|
MODEL_SIZE=$(python3 -c "from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')")
|
||||||
CAMERA_RES=$(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')")
|
CAMERA_RES=$(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')")
|
||||||
|
TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
|
||||||
if [ "${{ inputs.target_hardware }}" == "usbgpu" ]; then
|
|
||||||
echo "USBGPU build"
|
|
||||||
export USBGPU=1
|
|
||||||
TG_FLAGS="DEV=AMD USBGPU=1 IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
|
|
||||||
OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl"
|
|
||||||
else
|
|
||||||
echo "QCOM build"
|
|
||||||
TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
|
|
||||||
OUTPUT_PKL="${{ env.MODELS_DIR }}/driving_tinygrad.pkl"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Generate metadata for all ONNX files
|
# Generate metadata for all ONNX files
|
||||||
find "${{ env.MODELS_DIR }}" -maxdepth 1 -name '*.onnx' | while IFS= read -r onnx_file; do
|
find "${{ env.MODELS_DIR }}" -maxdepth 1 -name '*.onnx' | while IFS= read -r onnx_file; do
|
||||||
@@ -209,13 +186,7 @@ jobs:
|
|||||||
POLICY_ONNX="${{ env.MODELS_DIR }}/driving_policy.onnx"
|
POLICY_ONNX="${{ env.MODELS_DIR }}/driving_policy.onnx"
|
||||||
OFF_POLICY_ONNX="${{ env.MODELS_DIR }}/driving_off_policy.onnx"
|
OFF_POLICY_ONNX="${{ env.MODELS_DIR }}/driving_off_policy.onnx"
|
||||||
ON_POLICY_ONNX="${{ env.MODELS_DIR }}/driving_on_policy.onnx"
|
ON_POLICY_ONNX="${{ env.MODELS_DIR }}/driving_on_policy.onnx"
|
||||||
SUPERCOMBO_ONNX=""
|
SUPERCOMBO_ONNX="${{ env.MODELS_DIR }}/supercombo.onnx"
|
||||||
for f in "${{ env.MODELS_DIR }}/supercombo.onnx" "${{ env.MODELS_DIR }}/driving_supercombo.onnx"; do
|
|
||||||
if [ -f "$f" ]; then
|
|
||||||
SUPERCOMBO_ONNX="$f"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
MODEL_TYPE="" ONNX_ARGS="" OUTPUT_NAME=""
|
MODEL_TYPE="" ONNX_ARGS="" OUTPUT_NAME=""
|
||||||
if [ -f "$VISION_ONNX" ]; then
|
if [ -f "$VISION_ONNX" ]; then
|
||||||
@@ -236,15 +207,24 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$MODEL_TYPE" ]; then
|
if [ -n "$MODEL_TYPE" ]; then
|
||||||
echo "Detected: $MODEL_TYPE -> $OUTPUT_PKL"
|
echo "Detected: $MODEL_TYPE -> driving_tinygrad.pkl"
|
||||||
env ${TG_FLAGS} python3 "$COMPILE_MODELD" \
|
env ${TG_FLAGS} python3 "$COMPILE_MODELD" \
|
||||||
--model-type $MODEL_TYPE \
|
--model-type $MODEL_TYPE \
|
||||||
--model-size $MODEL_SIZE \
|
--model-size $MODEL_SIZE \
|
||||||
--camera-resolutions $CAMERA_RES \
|
--camera-resolutions $CAMERA_RES \
|
||||||
$ONNX_ARGS \
|
$ONNX_ARGS \
|
||||||
--output "$OUTPUT_PKL"
|
--output "${{ env.MODELS_DIR }}/driving_tinygrad.pkl"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: Validate Model Outputs
|
||||||
|
run: |
|
||||||
|
source /etc/profile
|
||||||
|
export UV_PROJECT_ENVIRONMENT=${HOME}/venv
|
||||||
|
export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT
|
||||||
|
python3 "${{ github.workspace }}/release/ci/model_generator.py" \
|
||||||
|
--validate-only \
|
||||||
|
--model-dir "${{ env.MODELS_DIR }}"
|
||||||
|
|
||||||
- name: Prepare Output
|
- name: Prepare Output
|
||||||
run: |
|
run: |
|
||||||
sudo rm -rf ${{ env.OUTPUT_DIR }}
|
sudo rm -rf ${{ env.OUTPUT_DIR }}
|
||||||
|
|||||||
@@ -137,16 +137,10 @@ struct ModelManagerSP @0xaedffd8f31e7b55d {
|
|||||||
eta @2 :UInt32;
|
eta @2 :UInt32;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Chunk {
|
|
||||||
fileName @0 :Text;
|
|
||||||
sha256 @1 :Text;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Artifact {
|
struct Artifact {
|
||||||
fileName @0 :Text;
|
fileName @0 :Text;
|
||||||
downloadUri @1 :DownloadUri;
|
downloadUri @1 :DownloadUri;
|
||||||
downloadProgress @2 :DownloadProgress;
|
downloadProgress @2 :DownloadProgress;
|
||||||
chunks @3 :List(Chunk);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Model {
|
struct Model {
|
||||||
@@ -161,7 +155,6 @@ struct ModelManagerSP @0xaedffd8f31e7b55d {
|
|||||||
policy @3;
|
policy @3;
|
||||||
offPolicy @4;
|
offPolicy @4;
|
||||||
onPolicy @5;
|
onPolicy @5;
|
||||||
chunked @6;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,6 @@ struct OnroadEvent @0xc4fa6047f024e718 {
|
|||||||
userBookmark @95;
|
userBookmark @95;
|
||||||
excessiveActuation @96;
|
excessiveActuation @96;
|
||||||
audioFeedback @97;
|
audioFeedback @97;
|
||||||
|
|
||||||
soundsUnavailableDEPRECATED @47;
|
soundsUnavailableDEPRECATED @47;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,6 +134,9 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"UsbGpuPresent", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
|
{"UsbGpuPresent", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
|
||||||
{"UsbGpuCompiled", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
|
{"UsbGpuCompiled", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
|
||||||
{"Version", {PERSISTENT, STRING}},
|
{"Version", {PERSISTENT, STRING}},
|
||||||
|
{"WgpuEnabled", {CLEAR_ON_MANAGER_START | DEVELOPMENT_ONLY, BOOL}},
|
||||||
|
{"WgpuModelName", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | DEVELOPMENT_ONLY, STRING}},
|
||||||
|
{"WgpuReady", {CLEAR_ON_MANAGER_START | DEVELOPMENT_ONLY, BOOL}},
|
||||||
|
|
||||||
// --- sunnypilot params --- //
|
// --- sunnypilot params --- //
|
||||||
{"ApiCache_DriveStats", {PERSISTENT, JSON}},
|
{"ApiCache_DriveStats", {PERSISTENT, JSON}},
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ available = probe_devices()
|
|||||||
if 'CUDA' in available:
|
if 'CUDA' in available:
|
||||||
tg_backend = 'CUDA'
|
tg_backend = 'CUDA'
|
||||||
tg_flags = f'DEV={tg_backend}'
|
tg_flags = f'DEV={tg_backend}'
|
||||||
|
elif 'METAL' in available:
|
||||||
|
tg_backend = 'METAL'
|
||||||
|
tg_flags = f'DEV={tg_backend} FLOAT16=1'
|
||||||
elif 'QCOM' in available:
|
elif 'QCOM' in available:
|
||||||
tg_backend = 'QCOM'
|
tg_backend = 'QCOM'
|
||||||
tg_flags = f'DEV={tg_backend} IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1'
|
tg_flags = f'DEV={tg_backend} IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1'
|
||||||
@@ -55,6 +58,7 @@ tg_devices = { # which device to put jit inputs to at runtime
|
|||||||
}
|
}
|
||||||
|
|
||||||
USBGPU = usbgpu_present() # or release # TODO always build big model on release
|
USBGPU = usbgpu_present() # or release # TODO always build big model on release
|
||||||
|
WGPU = os.getenv('WGPU') == '1'
|
||||||
if USBGPU:
|
if USBGPU:
|
||||||
usbgpu_tg_flags = f'DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0'
|
usbgpu_tg_flags = f'DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0'
|
||||||
# the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it
|
# the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it
|
||||||
@@ -84,13 +88,13 @@ compile_modeld_script = [
|
|||||||
model_w, model_h = MEDMODEL_INPUT_SIZE
|
model_w, model_h = MEDMODEL_INPUT_SIZE
|
||||||
frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
||||||
|
|
||||||
for usbgpu in [False, True] if USBGPU else [False]:
|
for usbgpu in [False, True] if USBGPU or WGPU else [False]:
|
||||||
target_pkl_path = File(modeld_pkl_path(usbgpu)).abspath
|
target_pkl_path = File(modeld_pkl_path(usbgpu)).abspath
|
||||||
# BIG_INTO_SMALL=1 builds the default target from the big model, e.g. to test it without a USB GPU
|
# BIG_INTO_SMALL=1 builds the default target from the big model, e.g. to test it without a USB GPU
|
||||||
file_prefix, cmd_flags = ('big_', usbgpu_tg_flags) if usbgpu else ('big_' if os.getenv('BIG_INTO_SMALL') else '', tg_flags)
|
file_prefix, cmd_flags = ('big_', usbgpu_tg_flags if USBGPU else tg_flags) if usbgpu else ('big_' if os.getenv('BIG_INTO_SMALL') else '', tg_flags)
|
||||||
driving_onnx_deps = get_existing_chunks(File(f"models/{file_prefix}driving_supercombo.onnx").abspath)
|
driving_onnx_deps = get_existing_chunks(File(f"models/{file_prefix}driving_supercombo.onnx").abspath)
|
||||||
camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in CAMERA_CONFIGS)
|
camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in CAMERA_CONFIGS)
|
||||||
cmd = (f'{cmd_flags} {mac_brew_string} python3 {modeld_dir}/compile_modeld.py '
|
cmd = (f'{cmd_flags} {mac_brew_string} {sys.executable} {modeld_dir}/compile_modeld.py '
|
||||||
f'--model-size {model_w}x{model_h} '
|
f'--model-size {model_w}x{model_h} '
|
||||||
f'--camera-resolutions {camera_res_args} '
|
f'--camera-resolutions {camera_res_args} '
|
||||||
f'--onnx {File(f"models/{file_prefix}driving_supercombo.onnx").abspath} '
|
f'--onnx {File(f"models/{file_prefix}driving_supercombo.onnx").abspath} '
|
||||||
@@ -104,7 +108,7 @@ for usbgpu in [False, True] if USBGPU else [False]:
|
|||||||
tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(chunk_targets), chunker_file],
|
tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(chunk_targets), chunker_file],
|
||||||
[cmd, Action(do_chunk, " [CHUNK] $TARGET")],
|
[cmd, Action(do_chunk, " [CHUNK] $TARGET")],
|
||||||
)
|
)
|
||||||
if usbgpu:
|
if usbgpu and USBGPU:
|
||||||
lenv.SideEffect(usbgpu_lock, node)
|
lenv.SideEffect(usbgpu_lock, node)
|
||||||
|
|
||||||
# get model metadata
|
# get model metadata
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from openpilot.common.file_chunker import open_file_chunked, get_manifest_path
|
|||||||
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
|
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
|
||||||
from openpilot.selfdrive.modeld.helpers import usbgpu_present, modeld_pkl_path, get_tg_input_devices, load_oob
|
from openpilot.selfdrive.modeld.helpers import usbgpu_present, modeld_pkl_path, get_tg_input_devices, load_oob
|
||||||
from openpilot.selfdrive.modeld.usbgpu_link import wait_usbgpu_link
|
from openpilot.selfdrive.modeld.usbgpu_link import wait_usbgpu_link
|
||||||
|
from openpilot.tools.wgpu.zmq import WGPU_CAR_PARAMS, ZmqPubMaster, ZmqSubMaster, ZmqSubSocket
|
||||||
|
|
||||||
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
||||||
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
|
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
|
||||||
@@ -76,26 +77,44 @@ class FrameMeta:
|
|||||||
self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof
|
self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof
|
||||||
|
|
||||||
|
|
||||||
|
def copy_nv12_to_venus(buf: VisionBuf, dst: np.ndarray, nv12: tuple[int, int, int, int]) -> None:
|
||||||
|
stride, y_height, uv_height, _ = nv12
|
||||||
|
if buf.stride < buf.width:
|
||||||
|
raise ValueError(f"invalid VisionIPC stride {buf.stride} for width {buf.width}")
|
||||||
|
|
||||||
|
src = np.frombuffer(buf.data, dtype=np.uint8)
|
||||||
|
src_size = buf.uv_offset + buf.stride * (buf.height // 2)
|
||||||
|
if src.size < src_size:
|
||||||
|
raise ValueError(f"VisionIPC buffer has {src.size} bytes, expected at least {src_size}")
|
||||||
|
|
||||||
|
dst[:stride * y_height].reshape(y_height, stride)[:buf.height, :buf.width] = \
|
||||||
|
src[:buf.stride * buf.height].reshape(buf.height, buf.stride)[:, :buf.width]
|
||||||
|
dst[stride * y_height:stride * (y_height + uv_height)].reshape(uv_height, stride)[:buf.height // 2, :buf.width] = \
|
||||||
|
src[buf.uv_offset:src_size].reshape(buf.height // 2, buf.stride)[:, :buf.width]
|
||||||
|
|
||||||
|
|
||||||
class ModelState(ModelStateBase):
|
class ModelState(ModelStateBase):
|
||||||
prev_desire: np.ndarray # for tracking the rising edge of the pulse
|
prev_desire: np.ndarray # for tracking the rising edge of the pulse
|
||||||
|
|
||||||
def __init__(self, cam_w: int, cam_h: int, usbgpu: bool):
|
def __init__(self, cam_w: int, cam_h: int, usbgpu: bool, big_model: bool = False):
|
||||||
ModelStateBase.__init__(self)
|
ModelStateBase.__init__(self)
|
||||||
self.LAT_SMOOTH_SECONDS = LAT_SMOOTH_SECONDS
|
self.LAT_SMOOTH_SECONDS = LAT_SMOOTH_SECONDS
|
||||||
input_devices = get_tg_input_devices(PROCESS_NAME, usbgpu)
|
input_devices = get_tg_input_devices(PROCESS_NAME, usbgpu)
|
||||||
self.WARP_DEV, self.QUEUE_DEV = input_devices['WARP_DEV'], input_devices['QUEUE_DEV']
|
self.WARP_DEV, self.QUEUE_DEV = input_devices['WARP_DEV'], input_devices['QUEUE_DEV']
|
||||||
jits = load_oob(open_file_chunked(modeld_pkl_path(usbgpu)))
|
jits = load_oob(open_file_chunked(modeld_pkl_path(usbgpu or big_model)))
|
||||||
metadata = jits['metadata']
|
metadata = jits['metadata']
|
||||||
self.input_shapes = metadata['input_shapes']
|
self.input_shapes = metadata['input_shapes']
|
||||||
self.vision_input_names = [k for k in self.input_shapes if 'img' in k]
|
self.vision_input_names = [k for k in self.input_shapes if 'img' in k]
|
||||||
self.output_slices = metadata['output_slices']
|
self.output_slices = metadata['output_slices']
|
||||||
|
|
||||||
self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
|
self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
|
||||||
|
self.copy_vision_buffers = self.WARP_DEV.split(":")[0] == "METAL"
|
||||||
|
|
||||||
self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
||||||
self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV)
|
self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV)
|
||||||
self.full_frames: dict[str, Tensor] = {}
|
self.full_frames: dict[str, Tensor] = {}
|
||||||
self._blob_cache: dict[tuple[str, int], Tensor] = {}
|
self._blob_cache: dict[tuple[str, int], Tensor] = {}
|
||||||
|
self._vision_staging: dict[str, np.ndarray] = {}
|
||||||
self.parser = Parser()
|
self.parser = Parser()
|
||||||
self.frame_buf_params = {k: get_nv12_info(cam_w, cam_h) for k in ('img', 'big_img')}
|
self.frame_buf_params = {k: get_nv12_info(cam_w, cam_h) for k in ('img', 'big_img')}
|
||||||
self.run_policy = jits['run_policy']
|
self.run_policy = jits['run_policy']
|
||||||
@@ -108,13 +127,22 @@ class ModelState(ModelStateBase):
|
|||||||
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
|
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
|
||||||
inputs: dict[str, np.ndarray]) -> dict[str, np.ndarray] | None:
|
inputs: dict[str, np.ndarray]) -> dict[str, np.ndarray] | None:
|
||||||
for key in bufs.keys():
|
for key in bufs.keys():
|
||||||
ptr = np.frombuffer(bufs[key].data, dtype=np.uint8).ctypes.data
|
nv12 = self.frame_buf_params[key]
|
||||||
yuv_size = self.frame_buf_params[key][3]
|
yuv_size = nv12[3]
|
||||||
# There is a ringbuffer of imgs, just cache tensors pointing to all of them
|
if self.copy_vision_buffers:
|
||||||
cache_key = (key, ptr)
|
# VisionIPC supplies a CPU pointer. Metal's external_ptr expects an MTLBuffer object,
|
||||||
if cache_key not in self._blob_cache:
|
# so wrap its NV12 pixels in the Venus layout expected by the compiled warp, then copy.
|
||||||
self._blob_cache[cache_key] = Tensor.from_blob(ptr, (yuv_size,), dtype='uint8', device=self.WARP_DEV)
|
if key not in self._vision_staging:
|
||||||
self.full_frames[key] = self._blob_cache[cache_key]
|
self._vision_staging[key] = np.zeros(yuv_size, dtype=np.uint8)
|
||||||
|
copy_nv12_to_venus(bufs[key], self._vision_staging[key], nv12)
|
||||||
|
self.full_frames[key] = Tensor(self._vision_staging[key], device=self.WARP_DEV).realize()
|
||||||
|
else:
|
||||||
|
# There is a ringbuffer of imgs, just cache tensors pointing to all of them.
|
||||||
|
frame = np.frombuffer(bufs[key].data, dtype=np.uint8, count=yuv_size)
|
||||||
|
cache_key = (key, frame.ctypes.data)
|
||||||
|
if cache_key not in self._blob_cache:
|
||||||
|
self._blob_cache[cache_key] = Tensor.from_blob(frame.ctypes.data, (yuv_size,), dtype='uint8', device=self.WARP_DEV)
|
||||||
|
self.full_frames[key] = self._blob_cache[cache_key]
|
||||||
|
|
||||||
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
|
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
|
||||||
inputs['desire_pulse'][0] = 0
|
inputs['desire_pulse'][0] = 0
|
||||||
@@ -139,18 +167,32 @@ class ModelState(ModelStateBase):
|
|||||||
return outputs_dict
|
return outputs_dict
|
||||||
|
|
||||||
|
|
||||||
def main(demo=False):
|
def main(demo=False, remote_addr: str | None = None, big_model: bool = False):
|
||||||
cloudlog.warning("modeld init")
|
cloudlog.warning("modeld init")
|
||||||
|
|
||||||
_present = usbgpu_present()
|
_present = usbgpu_present()
|
||||||
_compiled = os.path.isfile(get_manifest_path(modeld_pkl_path(usbgpu=True)))
|
_compiled = os.path.isfile(get_manifest_path(modeld_pkl_path(usbgpu=True)))
|
||||||
USBGPU = _present and _compiled
|
USBGPU = _present and _compiled
|
||||||
|
if big_model and not _compiled:
|
||||||
|
raise FileNotFoundError(f"big model is not compiled: {modeld_pkl_path(usbgpu=True)}")
|
||||||
params = Params()
|
params = Params()
|
||||||
params.put_bool("UsbGpuPresent", _present)
|
params.put_bool("UsbGpuPresent", _present)
|
||||||
params.put_bool("UsbGpuCompiled", _compiled)
|
params.put_bool("UsbGpuCompiled", _compiled)
|
||||||
|
|
||||||
config_realtime_process(7, 54)
|
config_realtime_process(7, 54)
|
||||||
|
|
||||||
|
remote_CP = None
|
||||||
|
if remote_addr is not None:
|
||||||
|
# Do not attach to VisionIPC until all startup prerequisites are available.
|
||||||
|
# Otherwise its notification queue grows while waiting for the infrequent
|
||||||
|
# bridged carParams message and reconnect starts tens of seconds behind.
|
||||||
|
cloudlog.warning("waiting for remote carParams")
|
||||||
|
car_params_socket = ZmqSubSocket(WGPU_CAR_PARAMS, remote_addr, conflate=True)
|
||||||
|
raw_car_params = car_params_socket.receive()
|
||||||
|
assert raw_car_params is not None
|
||||||
|
remote_CP = messaging.log_from_bytes(raw_car_params, car.CarParams)
|
||||||
|
cloudlog.info("modeld got remote CarParams: %s", remote_CP.brand)
|
||||||
|
|
||||||
# visionipc clients
|
# visionipc clients
|
||||||
while True:
|
while True:
|
||||||
available_streams = VisionIpcClient.available_streams("camerad", block=False)
|
available_streams = VisionIpcClient.available_streams("camerad", block=False)
|
||||||
@@ -178,12 +220,14 @@ def main(demo=False):
|
|||||||
wait_usbgpu_link()
|
wait_usbgpu_link()
|
||||||
st = time.monotonic()
|
st = time.monotonic()
|
||||||
cloudlog.warning("loading model")
|
cloudlog.warning("loading model")
|
||||||
model = ModelState(vipc_client_main.width, vipc_client_main.height, USBGPU)
|
model = ModelState(vipc_client_main.width, vipc_client_main.height, USBGPU, big_model=big_model)
|
||||||
cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting")
|
cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting")
|
||||||
|
|
||||||
# messaging
|
# messaging
|
||||||
pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"])
|
output_services = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"]
|
||||||
sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"])
|
pm = ZmqPubMaster(output_services) if remote_addr is not None else PubMaster(output_services)
|
||||||
|
services = ["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"]
|
||||||
|
sm = ZmqSubMaster(services, remote_addr) if remote_addr is not None else SubMaster(services)
|
||||||
|
|
||||||
publish_state = PublishState()
|
publish_state = PublishState()
|
||||||
params = Params()
|
params = Params()
|
||||||
@@ -203,6 +247,9 @@ def main(demo=False):
|
|||||||
|
|
||||||
if demo:
|
if demo:
|
||||||
CP = get_demo_car_params()
|
CP = get_demo_car_params()
|
||||||
|
elif remote_addr is not None:
|
||||||
|
assert remote_CP is not None
|
||||||
|
CP = remote_CP
|
||||||
else:
|
else:
|
||||||
CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams)
|
CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams)
|
||||||
cloudlog.info("modeld got CarParams: %s", CP.brand)
|
cloudlog.info("modeld got CarParams: %s", CP.brand)
|
||||||
@@ -332,7 +379,9 @@ if __name__ == "__main__":
|
|||||||
import argparse
|
import argparse
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('--demo', action='store_true', help='A boolean for demo mode.')
|
parser.add_argument('--demo', action='store_true', help='A boolean for demo mode.')
|
||||||
|
parser.add_argument('--remote', metavar='ADDRESS', help='Run against a remote device over the cereal ZMQ bridge.')
|
||||||
|
parser.add_argument('--big-model', action='store_true', help='Use the locally compiled big driving model.')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
main(demo=args.demo)
|
main(demo=args.demo, remote_addr=args.remote, big_model=args.big_model)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
cloudlog.warning("got SIGINT")
|
cloudlog.warning("got SIGINT")
|
||||||
|
|||||||
@@ -722,7 +722,6 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
|||||||
ET.NO_ENTRY: NoEntryAlert("Driving Model Lagging"),
|
ET.NO_ENTRY: NoEntryAlert("Driving Model Lagging"),
|
||||||
ET.PERMANENT: modeld_lagging_alert,
|
ET.PERMANENT: modeld_lagging_alert,
|
||||||
},
|
},
|
||||||
|
|
||||||
# Besides predicting the path, lane lines and lead car data the model also
|
# Besides predicting the path, lane lines and lead car data the model also
|
||||||
# predicts the current velocity and rotation speed of the car. If the model is
|
# predicts the current velocity and rotation speed of the car. If the model is
|
||||||
# very uncertain about the current velocity while the car is moving, this
|
# very uncertain about the current velocity while the car is moving, this
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ class SelfdriveD(CruiseHelper):
|
|||||||
self.is_metric = self.params.get_bool("IsMetric")
|
self.is_metric = self.params.get_bool("IsMetric")
|
||||||
self.is_ldw_enabled = self.params.get_bool("IsLdwEnabled")
|
self.is_ldw_enabled = self.params.get_bool("IsLdwEnabled")
|
||||||
self.disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator")
|
self.disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator")
|
||||||
|
self.wgpu_enabled = self.params.get_bool("WgpuEnabled")
|
||||||
|
|
||||||
car_recognized = self.CP.brand != 'mock'
|
car_recognized = self.CP.brand != 'mock'
|
||||||
|
|
||||||
@@ -398,12 +399,12 @@ class SelfdriveD(CruiseHelper):
|
|||||||
has_disable_events = self.events.contains(ET.NO_ENTRY) and (self.events.contains(ET.SOFT_DISABLE) or self.events.contains(ET.IMMEDIATE_DISABLE))
|
has_disable_events = self.events.contains(ET.NO_ENTRY) and (self.events.contains(ET.SOFT_DISABLE) or self.events.contains(ET.IMMEDIATE_DISABLE))
|
||||||
no_system_errors = (not has_disable_events) or (len(self.events) == num_events)
|
no_system_errors = (not has_disable_events) or (len(self.events) == num_events)
|
||||||
if not self.sm.all_checks() and no_system_errors:
|
if not self.sm.all_checks() and no_system_errors:
|
||||||
if not self.sm.all_alive():
|
# if not self.sm.all_alive():
|
||||||
self.events.add(EventName.commIssue)
|
# self.events.add(EventName.commIssue)
|
||||||
elif not self.sm.all_freq_ok():
|
# elif not self.sm.all_freq_ok():
|
||||||
self.events.add(EventName.commIssueAvgFreq)
|
# self.events.add(EventName.commIssueAvgFreq)
|
||||||
else:
|
# else:
|
||||||
self.events.add(EventName.commIssue)
|
# self.events.add(EventName.commIssue)
|
||||||
|
|
||||||
logs = {
|
logs = {
|
||||||
'invalid': [s for s, valid in self.sm.valid.items() if not valid],
|
'invalid': [s for s, valid in self.sm.valid.items() if not valid],
|
||||||
@@ -416,13 +417,13 @@ class SelfdriveD(CruiseHelper):
|
|||||||
else:
|
else:
|
||||||
self.logged_comm_issue = None
|
self.logged_comm_issue = None
|
||||||
|
|
||||||
if not self.CP.notCar:
|
# if not self.CP.notCar:
|
||||||
if not self.sm['livePose'].posenetOK:
|
# if not self.sm['livePose'].posenetOK:
|
||||||
self.events.add(EventName.posenetInvalid)
|
# self.events.add(EventName.posenetInvalid)
|
||||||
if not self.sm['livePose'].inputsOK:
|
# if not self.sm['livePose'].inputsOK:
|
||||||
self.events.add(EventName.locationdTemporaryError)
|
# self.events.add(EventName.locationdTemporaryError)
|
||||||
if not self.sm['liveParameters'].valid and cal_status == log.LiveCalibrationData.Status.calibrated and not TESTING_CLOSET and (not SIMULATION or REPLAY):
|
# if not self.sm['liveParameters'].valid and cal_status == log.LiveCalibrationData.Status.calibrated and not TESTING_CLOSET and (not SIMULATION or REPLAY):
|
||||||
self.events.add(EventName.paramsdTemporaryError)
|
# self.events.add(EventName.paramsdTemporaryError)
|
||||||
|
|
||||||
# conservative HW alert. if the data or frequency are off, locationd will throw an error
|
# conservative HW alert. if the data or frequency are off, locationd will throw an error
|
||||||
if any((self.sm.frame - self.sm.recv_frame[s])*DT_CTRL > 10. for s in self.sensor_packets):
|
if any((self.sm.frame - self.sm.recv_frame[s])*DT_CTRL > 10. for s in self.sensor_packets):
|
||||||
@@ -467,9 +468,9 @@ class SelfdriveD(CruiseHelper):
|
|||||||
self.distance_traveled += abs(CS.vEgo) * DT_CTRL
|
self.distance_traveled += abs(CS.vEgo) * DT_CTRL
|
||||||
|
|
||||||
# TODO: fix simulator
|
# TODO: fix simulator
|
||||||
if not SIMULATION or REPLAY:
|
# if not SIMULATION or REPLAY:
|
||||||
if self.sm['modelV2'].frameDropPerc > 1:
|
# if self.sm['modelV2'].frameDropPerc > 1 and not self.wgpu_enabled:
|
||||||
self.events.add(EventName.modeldLagging)
|
# self.events.add(EventName.modeldLagging)
|
||||||
|
|
||||||
# mute canBusMissing event if in Park, as it sometimes may trigger a false alarm with MADS in Paused state
|
# mute canBusMissing event if in Park, as it sometimes may trigger a false alarm with MADS in Paused state
|
||||||
if CS.gearShifter == car.CarState.GearShifter.park and self.mads.enabled:
|
if CS.gearShifter == car.CarState.GearShifter.park and self.mads.enabled:
|
||||||
@@ -625,6 +626,7 @@ class SelfdriveD(CruiseHelper):
|
|||||||
self.is_metric = self.params.get_bool("IsMetric")
|
self.is_metric = self.params.get_bool("IsMetric")
|
||||||
self.is_ldw_enabled = self.params.get_bool("IsLdwEnabled")
|
self.is_ldw_enabled = self.params.get_bool("IsLdwEnabled")
|
||||||
self.disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator")
|
self.disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator")
|
||||||
|
self.wgpu_enabled = self.params.get_bool("WgpuEnabled")
|
||||||
self.experimental_mode = self.params.get_bool("ExperimentalMode") and self.CP.openpilotLongitudinalControl
|
self.experimental_mode = self.params.get_bool("ExperimentalMode") and self.CP.openpilotLongitudinalControl
|
||||||
self.personality = self.params.get("LongitudinalPersonality", return_default=True)
|
self.personality = self.params.get("LongitudinalPersonality", return_default=True)
|
||||||
|
|
||||||
|
|||||||
@@ -248,8 +248,10 @@ class MiciHomeLayout(Widget):
|
|||||||
|
|
||||||
# ***** Center-aligned bottom section icons *****
|
# ***** Center-aligned bottom section icons *****
|
||||||
self._experimental_icon.set_visible(ui_state.experimental_mode)
|
self._experimental_icon.set_visible(ui_state.experimental_mode)
|
||||||
self._egpu_icon.set_visible(ui_state.usbgpu and ui_state.usbgpu_compiled)
|
wgpu_running = ui_state.wgpu_enabled and ui_state.sm.alive["modelV2"] and ui_state.sm.valid["modelV2"]
|
||||||
self._egpu_icon_gray.set_visible(ui_state.usbgpu and not ui_state.usbgpu_compiled)
|
self._egpu_icon.set_visible((ui_state.usbgpu and ui_state.usbgpu_compiled) or wgpu_running)
|
||||||
|
self._egpu_icon_gray.set_visible((ui_state.usbgpu and not ui_state.usbgpu_compiled) or
|
||||||
|
(ui_state.wgpu_ready and not wgpu_running))
|
||||||
self._mic_icon.set_visible(ui_state.recording_audio)
|
self._mic_icon.set_visible(ui_state.recording_audio)
|
||||||
self._body_icon.set_visible(bool(ui_state.is_body))
|
self._body_icon.set_visible(bool(ui_state.is_body))
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
from openpilot.cereal import log
|
from openpilot.cereal import log
|
||||||
@@ -160,6 +162,20 @@ class AugmentedRoadView(CameraView):
|
|||||||
text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
|
text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
|
||||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||||
|
self._model_status_key_labels = [
|
||||||
|
UnifiedLabel("", 21, FontWeight.ROMAN, text_color=rl.Color(210, 210, 210, 220),
|
||||||
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||||
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
|
||||||
|
wrap_text=False)
|
||||||
|
for _ in range(6)
|
||||||
|
]
|
||||||
|
self._model_status_value_labels = [
|
||||||
|
UnifiedLabel("", 22, FontWeight.SEMI_BOLD,
|
||||||
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
|
||||||
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
|
||||||
|
wrap_text=False)
|
||||||
|
for _ in range(6)
|
||||||
|
]
|
||||||
|
|
||||||
self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png")
|
self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png")
|
||||||
|
|
||||||
@@ -230,6 +246,8 @@ class AugmentedRoadView(CameraView):
|
|||||||
self._driver_state_renderer.set_position(self._rect.x + 16, self._rect.y + 10)
|
self._driver_state_renderer.set_position(self._rect.x + 16, self._rect.y + 10)
|
||||||
self._driver_state_renderer.render()
|
self._driver_state_renderer.render()
|
||||||
|
|
||||||
|
self._render_model_status()
|
||||||
|
|
||||||
self._hud_renderer.set_can_draw_top_icons(alert_to_render is None)
|
self._hud_renderer.set_can_draw_top_icons(alert_to_render is None)
|
||||||
self._hud_renderer.set_wheel_critical_icon(alert_to_render is not None and not not_animating_out and
|
self._hud_renderer.set_wheel_critical_icon(alert_to_render is not None and not not_animating_out and
|
||||||
alert_to_render.visual_alert == car.CarControl.HUDControl.VisualAlert.steerRequired)
|
alert_to_render.visual_alert == car.CarControl.HUDControl.VisualAlert.steerRequired)
|
||||||
@@ -248,6 +266,58 @@ class AugmentedRoadView(CameraView):
|
|||||||
|
|
||||||
self._bookmark_icon.render(self.rect)
|
self._bookmark_icon.render(self.rect)
|
||||||
|
|
||||||
|
def _render_model_status(self):
|
||||||
|
model = ui_state.sm["modelV2"]
|
||||||
|
if ui_state.sm.seen["modelV2"] and model.timestampEof:
|
||||||
|
model_age_ms = max(0., (time.monotonic_ns() - model.timestampEof) / 1e6)
|
||||||
|
execution_ms = max(0., model.modelExecutionTime * 1e3)
|
||||||
|
io_queue_ms = max(0., model_age_ms - execution_ms)
|
||||||
|
frame_drop = model.frameDropPerc
|
||||||
|
age_text = f"{model_age_ms:.0f} ms"
|
||||||
|
execution_text = f"{execution_ms:.0f} ms"
|
||||||
|
io_queue_text = f"{io_queue_ms:.0f} ms"
|
||||||
|
frame_drop_text = f"{frame_drop:.1f}%"
|
||||||
|
else:
|
||||||
|
model_age_ms = float("inf")
|
||||||
|
age_text = execution_text = io_queue_text = "-- ms"
|
||||||
|
frame_drop_text = "--%"
|
||||||
|
|
||||||
|
if ui_state.wgpu_enabled:
|
||||||
|
source, model_name = "WGPU", ui_state.wgpu_model_name
|
||||||
|
else:
|
||||||
|
source, model_name = "LOCAL", "DEVICE"
|
||||||
|
|
||||||
|
if model_age_ms < 200:
|
||||||
|
color = rl.Color(100, 255, 120, 230)
|
||||||
|
elif model_age_ms < 500:
|
||||||
|
color = rl.Color(255, 210, 80, 230)
|
||||||
|
else:
|
||||||
|
color = rl.Color(255, 120, 80, 230)
|
||||||
|
|
||||||
|
panel_w, panel_h = 250, 178
|
||||||
|
status_rect = rl.Rectangle(self._content_rect.x + self._content_rect.width - panel_w - 18,
|
||||||
|
self._content_rect.y + 18, panel_w, panel_h)
|
||||||
|
rl.draw_rectangle_rounded(status_rect, 0.14, 8, rl.Color(0, 0, 0, 175))
|
||||||
|
|
||||||
|
rows = (
|
||||||
|
("SOURCE", source),
|
||||||
|
("MODEL", model_name),
|
||||||
|
("AGE", age_text),
|
||||||
|
("EXEC", execution_text),
|
||||||
|
("IO/QUEUE", io_queue_text),
|
||||||
|
("DROPPED", frame_drop_text),
|
||||||
|
)
|
||||||
|
row_h = 26
|
||||||
|
for i, ((key, value), key_label, value_label) in enumerate(
|
||||||
|
zip(rows, self._model_status_key_labels, self._model_status_value_labels, strict=True)
|
||||||
|
):
|
||||||
|
row_rect = rl.Rectangle(status_rect.x + 14, status_rect.y + 11 + i * row_h, status_rect.width - 28, row_h)
|
||||||
|
key_label.set_text(key)
|
||||||
|
key_label.render(rl.Rectangle(row_rect.x, row_rect.y, 105, row_rect.height))
|
||||||
|
value_label.set_text(value)
|
||||||
|
value_label.set_text_color(color)
|
||||||
|
value_label.render(rl.Rectangle(row_rect.x + 108, row_rect.y, row_rect.width - 108, row_rect.height))
|
||||||
|
|
||||||
def _switch_stream_if_needed(self, sm):
|
def _switch_stream_if_needed(self, sm):
|
||||||
if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams:
|
if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams:
|
||||||
v_ego = sm['carState'].vEgo
|
v_ego = sm['carState'].vEgo
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class ModelsLayout(Widget):
|
|||||||
self._initialize_items()
|
self._initialize_items()
|
||||||
|
|
||||||
self.clear_cache_item.action_item.set_value(f"{self.calculate_cache_size():.2f} MB")
|
self.clear_cache_item.action_item.set_value(f"{self.calculate_cache_size():.2f} MB")
|
||||||
for ctrl, key in [(self.lane_turn_value_control, "LaneTurnValue"), (self.delay_control, "LagdToggleDelay"), (self.camera_offset, "CameraOffset")]:
|
for ctrl, key in [(self.lane_turn_value_control, "LaneTurnValue"), (self.delay_control, "LagdToggleDelay")]:
|
||||||
ctrl.action_item.set_value(int(float(ui_state.params.get(key, return_default=True)) * 100))
|
ctrl.action_item.set_value(int(float(ui_state.params.get(key, return_default=True)) * 100))
|
||||||
|
|
||||||
self._scroller = Scroller(self.items, line_separator=True, spacing=0)
|
self._scroller = Scroller(self.items, line_separator=True, spacing=0)
|
||||||
@@ -93,14 +93,9 @@ class ModelsLayout(Widget):
|
|||||||
|
|
||||||
self.lagd_toggle = toggle_item_sp(tr("Live Learning Steer Delay"), "", param="LagdToggle")
|
self.lagd_toggle = toggle_item_sp(tr("Live Learning Steer Delay"), "", param="LagdToggle")
|
||||||
|
|
||||||
self.camera_offset = option_item_sp(tr("Adjust Camera Offset"), "CameraOffset", -35, 35,
|
|
||||||
tr("Virtually shift camera's perspective to move model's center to Left(+ values) or Right (- values)"),
|
|
||||||
1, None, True, "", style.BUTTON_ACTION_WIDTH, None, True,
|
|
||||||
lambda v: f"{v / 100:.2f} m")
|
|
||||||
|
|
||||||
self.items = [self.current_model_item, self.cancel_download_item, self.supercombo_label, self.vision_label,
|
self.items = [self.current_model_item, self.cancel_download_item, self.supercombo_label, self.vision_label,
|
||||||
self.policy_label, self.off_policy_label, self.on_policy_label, self.refresh_item, self.clear_cache_item,
|
self.policy_label, self.off_policy_label, self.on_policy_label, self.refresh_item, self.clear_cache_item, self.lane_turn_desire_toggle,
|
||||||
self.lane_turn_desire_toggle, self.lane_turn_value_control, self.lagd_toggle, self.delay_control, self.camera_offset]
|
self.lane_turn_value_control, self.lagd_toggle, self.delay_control]
|
||||||
|
|
||||||
def _update_lagd_description(self, lagd_toggle: bool):
|
def _update_lagd_description(self, lagd_toggle: bool):
|
||||||
desc = tr("Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. " +
|
desc = tr("Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. " +
|
||||||
@@ -237,7 +232,6 @@ class ModelsLayout(Widget):
|
|||||||
advanced_controls: bool = ui_state.params.get_bool("ShowAdvancedControls")
|
advanced_controls: bool = ui_state.params.get_bool("ShowAdvancedControls")
|
||||||
turn_desire: bool = ui_state.params.get_bool("LaneTurnDesire")
|
turn_desire: bool = ui_state.params.get_bool("LaneTurnDesire")
|
||||||
live_delay: bool = ui_state.params.get_bool("LagdToggle")
|
live_delay: bool = ui_state.params.get_bool("LagdToggle")
|
||||||
camera_offset: bool = ui_state.params.get("ModelManager_ActiveBundle") is not None
|
|
||||||
|
|
||||||
self.lane_turn_desire_toggle.action_item.set_state(turn_desire)
|
self.lane_turn_desire_toggle.action_item.set_state(turn_desire)
|
||||||
self.lane_turn_value_control.set_visible(turn_desire and advanced_controls)
|
self.lane_turn_value_control.set_visible(turn_desire and advanced_controls)
|
||||||
@@ -246,7 +240,6 @@ class ModelsLayout(Widget):
|
|||||||
new_step = int(round(100 / CV.MPH_TO_KPH)) if ui_state.is_metric else 100
|
new_step = int(round(100 / CV.MPH_TO_KPH)) if ui_state.is_metric else 100
|
||||||
if self.lane_turn_value_control.action_item is not None and self.lane_turn_value_control.action_item.value_change_step != new_step:
|
if self.lane_turn_value_control.action_item is not None and self.lane_turn_value_control.action_item.value_change_step != new_step:
|
||||||
self.lane_turn_value_control.action_item.value_change_step = new_step
|
self.lane_turn_value_control.action_item.value_change_step = new_step
|
||||||
self.camera_offset.set_visible(camera_offset)
|
|
||||||
|
|
||||||
self._update_lagd_description(live_delay)
|
self._update_lagd_description(live_delay)
|
||||||
self.model_manager = ui_state.sm["modelManagerSP"]
|
self.model_manager = ui_state.sm["modelManagerSP"]
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ class UIState(UIStateSP):
|
|||||||
self.experimental_mode: bool = self.params.get_bool("ExperimentalMode")
|
self.experimental_mode: bool = self.params.get_bool("ExperimentalMode")
|
||||||
self.usbgpu: bool = self.params.get_bool("UsbGpuPresent")
|
self.usbgpu: bool = self.params.get_bool("UsbGpuPresent")
|
||||||
self.usbgpu_compiled: bool = self.params.get_bool("UsbGpuCompiled")
|
self.usbgpu_compiled: bool = self.params.get_bool("UsbGpuCompiled")
|
||||||
|
self.wgpu_enabled: bool = self.params.get_bool("WgpuEnabled")
|
||||||
|
self.wgpu_model_name: str = self.params.get("WgpuModelName") or "UNKNOWN"
|
||||||
|
self.wgpu_ready: bool = self.params.get_bool("WgpuReady")
|
||||||
self.started: bool = False
|
self.started: bool = False
|
||||||
self.ignition: bool = False
|
self.ignition: bool = False
|
||||||
self.recording_audio: bool = False
|
self.recording_audio: bool = False
|
||||||
@@ -213,6 +216,9 @@ class UIState(UIStateSP):
|
|||||||
self.experimental_mode = self.params.get_bool("ExperimentalMode")
|
self.experimental_mode = self.params.get_bool("ExperimentalMode")
|
||||||
self.usbgpu = self.params.get_bool("UsbGpuPresent")
|
self.usbgpu = self.params.get_bool("UsbGpuPresent")
|
||||||
self.usbgpu_compiled = self.params.get_bool("UsbGpuCompiled")
|
self.usbgpu_compiled = self.params.get_bool("UsbGpuCompiled")
|
||||||
|
self.wgpu_enabled = self.params.get_bool("WgpuEnabled")
|
||||||
|
self.wgpu_model_name = self.params.get("WgpuModelName") or "UNKNOWN"
|
||||||
|
self.wgpu_ready = self.params.get_bool("WgpuReady")
|
||||||
|
|
||||||
UIStateSP.update_params(self)
|
UIStateSP.update_params(self)
|
||||||
|
|
||||||
|
|||||||
@@ -10,291 +10,471 @@ import argparse
|
|||||||
import os
|
import os
|
||||||
import pickle
|
import pickle
|
||||||
import time
|
import time
|
||||||
from collections import defaultdict
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
os.environ['GMMU'] = '0'
|
from tinygrad.tensor import Tensor
|
||||||
|
|
||||||
def _patch_tinygrad_fetch_fw():
|
|
||||||
import hashlib
|
|
||||||
import pathlib
|
|
||||||
import zstandard
|
|
||||||
from tinygrad import helpers
|
|
||||||
_orig_fetch_fw = helpers.fetch_fw
|
|
||||||
def fetch_fw(path, name, sha256):
|
|
||||||
p = pathlib.Path(f"/lib/firmware/{path}/{name}.zst")
|
|
||||||
if p.is_file():
|
|
||||||
blob = zstandard.ZstdDecompressor().stream_reader(p.read_bytes()).read()
|
|
||||||
if hashlib.sha256(blob).hexdigest() == sha256:
|
|
||||||
return blob
|
|
||||||
return _orig_fetch_fw(path, name, sha256)
|
|
||||||
helpers.fetch_fw = fetch_fw
|
|
||||||
_patch_tinygrad_fetch_fw()
|
|
||||||
|
|
||||||
from openpilot.selfdrive.modeld.compile_modeld import NV12Frame, make_frame_prepare, sample_desire, sample_skip, shift_and_sample
|
|
||||||
from tinygrad import dtypes
|
|
||||||
from tinygrad.device import Device
|
from tinygrad.device import Device
|
||||||
from tinygrad.engine.jit import TinyJit
|
from tinygrad.engine.jit import TinyJit
|
||||||
from tinygrad.tensor import Tensor
|
|
||||||
|
from openpilot.selfdrive.modeld.compile_modeld import (
|
||||||
|
NV12Frame, make_frame_prepare,
|
||||||
|
shift_and_sample, sample_skip, sample_desire,
|
||||||
|
)
|
||||||
|
|
||||||
MODEL_TYPES = ('vision_policy', 'supercombo', 'vision_multi_policy')
|
MODEL_TYPES = ('vision_policy', 'supercombo', 'vision_multi_policy')
|
||||||
|
|
||||||
|
|
||||||
def _detect_desire_key(shapes: dict) -> str | None:
|
def _detect_desire_key(policy_input_shapes):
|
||||||
return next((key for key in shapes if key.startswith('desire')), None)
|
for k in policy_input_shapes:
|
||||||
|
if k.startswith('desire'):
|
||||||
|
return k
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _detect_vision_keys(shapes: dict) -> tuple[str | None, str | None]:
|
def _detect_vision_keys(vision_input_shapes):
|
||||||
img_keys = sorted(key for key in shapes if 'img' in key)
|
img_keys = sorted([k for k in vision_input_shapes if 'img' in k])
|
||||||
return (
|
road_key = next((k for k in img_keys if 'big' not in k), None)
|
||||||
next((key for key in img_keys if 'big' not in key), None),
|
wide_key = next((k for k in img_keys if 'big' in k), None)
|
||||||
next((key for key in img_keys if 'big' in key), None)
|
if road_key is None or wide_key is None:
|
||||||
)
|
raise ValueError(f"Cannot determine road/wide image keys from {list(vision_input_shapes.keys())}")
|
||||||
|
return road_key, wide_key
|
||||||
|
|
||||||
|
|
||||||
def derive_frame_skip(vision_input_shapes: dict, policy_input_shapes: dict) -> int:
|
def make_split_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, device):
|
||||||
features_buffer = policy_input_shapes.get('features_buffer')
|
road_key, _ = _detect_vision_keys(vision_input_shapes)
|
||||||
return 1 if not features_buffer or features_buffer[1] >= 99 else 4
|
img = vision_input_shapes[road_key]
|
||||||
|
n_frames = img[1] // 6
|
||||||
|
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3])
|
||||||
|
|
||||||
|
fb = policy_input_shapes['features_buffer']
|
||||||
|
desire_key = _detect_desire_key(policy_input_shapes)
|
||||||
|
dp = policy_input_shapes[desire_key]
|
||||||
|
tc = policy_input_shapes.get('traffic_convention', (1, 2))
|
||||||
|
|
||||||
def generate_queues_and_npy(input_shapes: dict, frame_skip: int, device: str = Device.DEFAULT) -> tuple[dict, dict]:
|
npy = {
|
||||||
road_key, _ = _detect_vision_keys(input_shapes)
|
'desire': np.zeros(dp[2], dtype=np.float32),
|
||||||
if not road_key:
|
'traffic_convention': np.zeros(tc, dtype=np.float32),
|
||||||
raise ValueError("Vision road key missing from input shapes.")
|
|
||||||
|
|
||||||
img_shape = input_shapes[road_key]
|
|
||||||
n_frames = img_shape[1] // 6
|
|
||||||
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img_shape[2], img_shape[3])
|
|
||||||
|
|
||||||
desire_key = _detect_desire_key(input_shapes)
|
|
||||||
if not desire_key:
|
|
||||||
raise ValueError("Desire key missing from input shapes.")
|
|
||||||
|
|
||||||
desire_shape = input_shapes[desire_key]
|
|
||||||
features_buffer = input_shapes.get('features_buffer')
|
|
||||||
|
|
||||||
npy_arrays = {
|
|
||||||
'desire': np.zeros(desire_shape[2], dtype=np.float32),
|
|
||||||
'tfm': np.zeros((3, 3), dtype=np.float32),
|
'tfm': np.zeros((3, 3), dtype=np.float32),
|
||||||
'big_tfm': np.zeros((3, 3), dtype=np.float32)
|
'big_tfm': np.zeros((3, 3), dtype=np.float32),
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, shape in input_shapes.items():
|
handled = {'features_buffer', desire_key, 'traffic_convention'}
|
||||||
if key not in npy_arrays and 'img' not in key and key not in ('features_buffer', desire_key):
|
for key, shape in policy_input_shapes.items():
|
||||||
npy_arrays[key] = np.zeros(shape, dtype=np.float32)
|
if key in handled:
|
||||||
|
continue
|
||||||
|
npy[key] = np.zeros(shape, dtype=np.float32)
|
||||||
|
|
||||||
queues = {
|
input_queues = {
|
||||||
'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
'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(),
|
'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||||
'desire_q': Tensor(np.zeros((frame_skip * desire_shape[1], desire_shape[0], desire_shape[2]),
|
'feat_q': Tensor(np.zeros((frame_skip * (fb[1] - 1) + 1, fb[0], fb[2]), dtype=np.float32), device=device).contiguous().realize(),
|
||||||
dtype=np.float32), device=device).contiguous().realize()
|
'desire_q': Tensor(np.zeros((frame_skip * dp[1], dp[0], dp[2]), dtype=np.float32), device=device).contiguous().realize(),
|
||||||
|
**{k: Tensor(v, device='NPY').realize() for k, v in npy.items()},
|
||||||
}
|
}
|
||||||
|
return input_queues, npy
|
||||||
if features_buffer:
|
|
||||||
queues['feat_q'] = Tensor(np.zeros((frame_skip * (features_buffer[1] - 1) + 1, features_buffer[0], features_buffer[2]),
|
|
||||||
dtype=np.float32), device=device).contiguous().realize()
|
|
||||||
|
|
||||||
queues.update({key: Tensor(value, device='NPY').realize() for key, value in npy_arrays.items()})
|
|
||||||
return queues, npy_arrays
|
|
||||||
|
|
||||||
|
|
||||||
def make_split_input_queues(vision_input_shapes: dict, policy_input_shapes: dict, frame_skip: int, device: str = Device.DEFAULT) -> tuple[dict, dict]:
|
def make_run_split_policy(vision_runner, policy_runner, nv12: NV12Frame, model_w, model_h,
|
||||||
return generate_queues_and_npy({**vision_input_shapes, **policy_input_shapes}, frame_skip, device)
|
vision_features_slice, frame_skip, desire_key, extra_policy_keys,
|
||||||
|
vision_road_key, vision_wide_key, prepare_only=False):
|
||||||
|
frame_prepare = make_frame_prepare(nv12, model_w, model_h)
|
||||||
def make_supercombo_input_queues(input_shapes: dict, frame_skip: int, device: str = Device.DEFAULT) -> tuple[dict, dict]:
|
|
||||||
return generate_queues_and_npy(input_shapes, frame_skip, device)
|
|
||||||
|
|
||||||
|
|
||||||
def create_jit_runner(vision_runner, policy_runners: list, nv12: NV12Frame, model_size: tuple[int, int],
|
|
||||||
features_slice: slice, frame_skip: int, input_shapes: dict, prepare_only: bool):
|
|
||||||
frame_prepare = make_frame_prepare(nv12, *model_size)
|
|
||||||
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip)
|
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip)
|
||||||
sample_desire_fn = partial(sample_desire, frame_skip=frame_skip)
|
sample_desire_fn = partial(sample_desire, frame_skip=frame_skip)
|
||||||
|
|
||||||
desire_key = _detect_desire_key(input_shapes)
|
def run_policy(img_q, big_img_q, feat_q, desire_q, desire, traffic_convention, tfm, big_tfm, frame, big_frame, **extra):
|
||||||
road_key, wide_key = _detect_vision_keys(input_shapes)
|
npy_tensors = [tfm.to(Device.DEFAULT), big_tfm.to(Device.DEFAULT),
|
||||||
|
desire.to(Device.DEFAULT), traffic_convention.to(Device.DEFAULT)]
|
||||||
|
extra_device = {k: extra[k].to(Device.DEFAULT) for k in extra_policy_keys}
|
||||||
|
Tensor.realize(*npy_tensors, *extra_device.values())
|
||||||
|
tfm, big_tfm, desire, traffic_convention = npy_tensors
|
||||||
|
|
||||||
if not desire_key or not road_key or not wide_key:
|
img = shift_and_sample(img_q, frame_prepare(frame, tfm).unsqueeze(0), sample_skip_fn)
|
||||||
raise ValueError("Missing required vision or desire keys in input shapes.")
|
big_img = shift_and_sample(big_img_q, frame_prepare(big_frame, big_tfm).unsqueeze(0), sample_skip_fn)
|
||||||
|
|
||||||
extra_keys = [key for key in input_shapes if key not in (desire_key, 'features_buffer', 'traffic_convention') and 'img' not in key]
|
|
||||||
|
|
||||||
def runner(img_q, big_img_q, feat_q, frame, big_frame, tfm, big_tfm, **kwargs):
|
|
||||||
desire_q = kwargs['desire_q']
|
|
||||||
desire = kwargs['desire']
|
|
||||||
traffic_convention = kwargs.get('traffic_convention')
|
|
||||||
|
|
||||||
npys = [tfm.to(Device.DEFAULT), big_tfm.to(Device.DEFAULT), desire.to(Device.DEFAULT)]
|
|
||||||
if traffic_convention is not None:
|
|
||||||
npys.append(traffic_convention.to(Device.DEFAULT))
|
|
||||||
|
|
||||||
extra_tensors = {key: kwargs[key].to(Device.DEFAULT) for key in extra_keys if key in kwargs}
|
|
||||||
Tensor.realize(*npys, *extra_tensors.values())
|
|
||||||
|
|
||||||
tfm_dev, big_tfm_dev, desire_dev = npys[:3]
|
|
||||||
traffic_conv_dev = npys[3] if traffic_convention is not None else None
|
|
||||||
|
|
||||||
img = shift_and_sample(img_q, frame_prepare(frame, tfm_dev).unsqueeze(0), sample_skip_fn)
|
|
||||||
big_img = shift_and_sample(big_img_q, frame_prepare(big_frame, big_tfm_dev).unsqueeze(0), sample_skip_fn)
|
|
||||||
|
|
||||||
if prepare_only:
|
if prepare_only:
|
||||||
return img, big_img
|
return img, big_img
|
||||||
|
|
||||||
desire_buf = shift_and_sample(desire_q, desire_dev.reshape(1, 1, -1), sample_desire_fn)
|
vision_out = next(iter(vision_runner({vision_road_key: img, vision_wide_key: big_img}).values())).cast('float32')
|
||||||
inputs = {desire_key: desire_buf, **extra_tensors}
|
|
||||||
|
|
||||||
if traffic_conv_dev is not None:
|
new_feat = vision_out[:, vision_features_slice].reshape(1, -1).unsqueeze(0)
|
||||||
inputs['traffic_convention'] = traffic_conv_dev
|
feat_buf = shift_and_sample(feat_q, new_feat, sample_skip_fn)
|
||||||
|
desire_buf = shift_and_sample(desire_q, desire.reshape(1, 1, -1), sample_desire_fn)
|
||||||
|
|
||||||
if vision_runner:
|
inputs = {'features_buffer': feat_buf, desire_key: desire_buf, 'traffic_convention': traffic_convention, **extra_device}
|
||||||
vision_out = next(iter(vision_runner({road_key: img, wide_key: big_img}).values()))
|
policy_out = next(iter(policy_runner(inputs).values())).cast('float32')
|
||||||
vision_out_cast = vision_out.cast('float32')
|
|
||||||
new_feat = vision_out_cast[:, features_slice].reshape(1, -1).unsqueeze(0)
|
|
||||||
inputs['features_buffer'] = shift_and_sample(feat_q, new_feat, sample_skip_fn).realize()
|
|
||||||
policy_outs = [next(iter(pol_runner(inputs).values())).cast('float32') for pol_runner in policy_runners]
|
|
||||||
return (vision_out_cast, *policy_outs) if len(policy_outs) > 1 else (vision_out_cast, policy_outs[0])
|
|
||||||
inputs.update({road_key: img, wide_key: big_img, 'features_buffer': sample_skip_fn(feat_q)})
|
|
||||||
policy_out = next(iter(policy_runners[0](inputs).values())).cast('float32')
|
|
||||||
new_feat = policy_out[:, features_slice].reshape(1, -1).unsqueeze(0)
|
|
||||||
shift_and_sample(feat_q, new_feat, sample_skip_fn).realize()
|
|
||||||
return policy_out
|
|
||||||
|
|
||||||
return runner
|
return vision_out, policy_out
|
||||||
|
return run_policy
|
||||||
|
|
||||||
|
|
||||||
def compile_and_warmup(nv12: NV12Frame, model_size: tuple[int, int], prepare_only: bool, frame_skip: int, vision_runner, policy_runners: list, metadata: dict):
|
def compile_split_policy(nv12: NV12Frame, model_w, model_h, prepare_only, frame_skip,
|
||||||
print(f"Compiling combined JIT for {nv12.width}x{nv12.height} (prepare_only={prepare_only})...")
|
vision_runner, policy_runner, vision_metadata, policy_metadata):
|
||||||
|
print(f"Compiling combined policy JIT for {nv12.width}x{nv12.height} (prepare_only={prepare_only})...")
|
||||||
|
|
||||||
all_shapes = {key: value for meta in metadata.values() for key, value in meta['input_shapes'].items()}
|
vision_features_slice = vision_metadata['output_slices']['hidden_state']
|
||||||
|
vision_input_shapes = vision_metadata['input_shapes']
|
||||||
|
policy_input_shapes = policy_metadata['input_shapes']
|
||||||
|
desire_key = _detect_desire_key(policy_input_shapes)
|
||||||
|
extra_policy_keys = [k for k in policy_input_shapes if k not in ('features_buffer', desire_key, 'traffic_convention')]
|
||||||
|
vision_road_key, vision_wide_key = _detect_vision_keys(vision_input_shapes)
|
||||||
|
|
||||||
feat_meta = metadata.get('vision') or metadata.get('model') or metadata.get('policy')
|
_run = make_run_split_policy(vision_runner, policy_runner, nv12, model_w, model_h,
|
||||||
if not feat_meta:
|
vision_features_slice, frame_skip, desire_key, extra_policy_keys,
|
||||||
raise ValueError("Could not find vision, model, or policy metadata.")
|
vision_road_key, vision_wide_key, prepare_only)
|
||||||
|
run_policy_jit = TinyJit(_run, prune=True)
|
||||||
|
|
||||||
features_slice = feat_meta['output_slices']['hidden_state']
|
SEED = 42
|
||||||
WARP_DEV = 'CPU' if "USBGPU" in os.environ else Device.DEFAULT
|
|
||||||
|
|
||||||
run_func = create_jit_runner(vision_runner, policy_runners, nv12, model_size, features_slice, frame_skip, all_shapes, prepare_only)
|
def random_inputs_run_fn(fn, seed, test_val=None, test_buffers=None, expect_match=True):
|
||||||
run_jit = TinyJit(run_func, prune=True)
|
input_queues, npy = make_split_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, Device.DEFAULT)
|
||||||
queues, npy_arrays = generate_queues_and_npy(all_shapes, frame_skip, Device.DEFAULT)
|
rng = np.random.default_rng(seed)
|
||||||
|
Tensor.manual_seed(seed)
|
||||||
|
|
||||||
for i in range(3):
|
testing = test_val is not None or test_buffers is not None
|
||||||
np.random.seed(42 + i)
|
n_runs = 1 if testing else 3
|
||||||
frame = Tensor.randint(nv12.size, low=0, high=256, dtype=dtypes.uint8, device=WARP_DEV).realize()
|
|
||||||
big_frame = Tensor.randint(nv12.size, low=0, high=256, dtype=dtypes.uint8, device=WARP_DEV).realize()
|
|
||||||
for arr in npy_arrays.values():
|
|
||||||
arr[:] = np.random.randn(*arr.shape).astype(arr.dtype)
|
|
||||||
|
|
||||||
Device.default.synchronize()
|
for i in range(n_runs):
|
||||||
start_time = time.perf_counter()
|
frame = Tensor.randint(nv12.size, low=0, high=256, dtype='uint8').realize()
|
||||||
run_jit(**queues, frame=frame, big_frame=big_frame)
|
big_frame = Tensor.randint(nv12.size, low=0, high=256, dtype='uint8').realize()
|
||||||
mid_time = time.perf_counter()
|
for v in npy.values():
|
||||||
Device.default.synchronize()
|
v[:] = rng.standard_normal(v.shape).astype(v.dtype)
|
||||||
print(f" [{i + 1}/3] enqueue {(mid_time - start_time) * 1e3:6.2f} ms -- total {(time.perf_counter() - start_time) * 1e3:6.2f} ms")
|
Device.default.synchronize()
|
||||||
|
st = time.perf_counter()
|
||||||
|
outs = fn(**input_queues, frame=frame, big_frame=big_frame)
|
||||||
|
mt = time.perf_counter()
|
||||||
|
Device.default.synchronize()
|
||||||
|
et = time.perf_counter()
|
||||||
|
print(f" [{i+1}/{n_runs}] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms")
|
||||||
|
|
||||||
return pickle.loads(pickle.dumps(run_jit)) if not prepare_only else run_jit
|
if i == 0:
|
||||||
|
val = [np.copy(v.numpy()) for v in outs]
|
||||||
|
buffers = [np.copy(v.numpy().copy()) for v in input_queues.values()]
|
||||||
|
|
||||||
|
if test_val is not None:
|
||||||
|
match = all(np.array_equal(a, b) for a, b in zip(val, test_val, strict=True))
|
||||||
|
assert match == expect_match, f"outputs {'differ from' if expect_match else 'match'} baseline (seed={seed})"
|
||||||
|
if test_buffers is not None:
|
||||||
|
match = all(np.array_equal(a, b) for a, b in zip(buffers, test_buffers, strict=True))
|
||||||
|
assert match == expect_match, f"buffers {'differ from' if expect_match else 'match'} baseline (seed={seed})"
|
||||||
|
return fn, val, buffers
|
||||||
|
|
||||||
|
print('capture + replay')
|
||||||
|
run_policy_jit, test_val, test_buffers = random_inputs_run_fn(run_policy_jit, SEED)
|
||||||
|
|
||||||
|
print('pickle round trip')
|
||||||
|
run_policy_jit = pickle.loads(pickle.dumps(run_policy_jit))
|
||||||
|
random_inputs_run_fn(run_policy_jit, SEED, test_val, test_buffers, expect_match=True)
|
||||||
|
random_inputs_run_fn(run_policy_jit, SEED+1, test_val, test_buffers, expect_match=False)
|
||||||
|
return run_policy_jit
|
||||||
|
|
||||||
|
|
||||||
def _parse_size(size_str: str) -> tuple[int, int]:
|
def derive_frame_skip(vision_input_shapes, policy_input_shapes):
|
||||||
width, height = size_str.lower().split('x')
|
fb = policy_input_shapes.get('features_buffer')
|
||||||
return int(width), int(height)
|
if fb is None:
|
||||||
|
return 1
|
||||||
|
fb_history = fb[1]
|
||||||
|
if fb_history >= 99:
|
||||||
|
return 1
|
||||||
|
return 4
|
||||||
|
|
||||||
|
|
||||||
def read_file_chunked_to_shm(path):
|
def make_supercombo_input_queues(input_shapes, frame_skip, device):
|
||||||
if not path:
|
img_shape = input_shapes.get('img', input_shapes.get('input_imgs'))
|
||||||
return None
|
if img_shape is None:
|
||||||
import atexit
|
raise ValueError("No img input found in model shapes")
|
||||||
from openpilot.common.file_chunker import read_file_chunked
|
|
||||||
from openpilot.system.hardware.hw import Paths
|
|
||||||
shm_path = os.path.join(Paths.shm_path(), os.path.basename(path))
|
|
||||||
atexit.register(lambda: os.path.exists(shm_path) and os.remove(shm_path))
|
|
||||||
with open(shm_path, 'wb') as f:
|
|
||||||
f.write(read_file_chunked(path))
|
|
||||||
return shm_path
|
|
||||||
|
|
||||||
|
n_frames = img_shape[1] // 6
|
||||||
|
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img_shape[2], img_shape[3])
|
||||||
|
|
||||||
def _compile_for_resolutions(camera_resolutions: list, model_size: tuple[int, int], frame_skip: int,
|
numpy_keys = {}
|
||||||
vision_runner, policy_runners: list, metadata: dict) -> dict:
|
queue_keys = {}
|
||||||
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
|
||||||
return {
|
for key, shape in input_shapes.items():
|
||||||
(cam_w, cam_h): {
|
if 'img' in key:
|
||||||
name: compile_and_warmup(NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h)), model_size, prepare_only,
|
continue
|
||||||
frame_skip, vision_runner, policy_runners, metadata)
|
if len(shape) == 3 and shape[1] > 1:
|
||||||
for name, prepare_only in [('warp_enqueue', True), ('run_policy', False)]
|
if key.startswith('desire'):
|
||||||
}
|
numpy_keys[key] = np.zeros(shape[2], dtype=np.float32)
|
||||||
for cam_w, cam_h in camera_resolutions
|
queue_keys[f'{key}_q'] = Tensor(
|
||||||
|
np.zeros((frame_skip * shape[1], shape[0], shape[2]), dtype=np.float32),
|
||||||
|
device=device).contiguous().realize()
|
||||||
|
elif key == 'features_buffer':
|
||||||
|
queue_keys['feat_q'] = Tensor(
|
||||||
|
np.zeros((frame_skip * (shape[1] - 1) + 1, shape[0], shape[2]), dtype=np.float32),
|
||||||
|
device=device).contiguous().realize()
|
||||||
|
else:
|
||||||
|
numpy_keys[key] = np.zeros(shape, dtype=np.float32)
|
||||||
|
elif len(shape) == 2:
|
||||||
|
numpy_keys[key] = np.zeros(shape, dtype=np.float32)
|
||||||
|
|
||||||
|
if 'traffic_convention' not in numpy_keys:
|
||||||
|
tc_shape = input_shapes.get('traffic_convention', (1, 2))
|
||||||
|
numpy_keys['traffic_convention'] = np.zeros(tc_shape, dtype=np.float32)
|
||||||
|
|
||||||
|
numpy_keys['tfm'] = np.zeros((3, 3), dtype=np.float32)
|
||||||
|
numpy_keys['big_tfm'] = np.zeros((3, 3), dtype=np.float32)
|
||||||
|
|
||||||
|
input_queues = {
|
||||||
|
'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||||
|
'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||||
|
**queue_keys,
|
||||||
|
**{k: Tensor(v, device='NPY').realize() for k, v in numpy_keys.items()},
|
||||||
}
|
}
|
||||||
|
return input_queues, numpy_keys
|
||||||
|
|
||||||
|
|
||||||
def _load_policy_runners(args: argparse.Namespace) -> tuple[list, list]:
|
def make_run_supercombo(model_runner, nv12: NV12Frame, model_w, model_h,
|
||||||
runners, keys = [], []
|
features_slice, frame_skip, input_shapes, prepare_only=False):
|
||||||
for name, onnx_arg in [('policy', args.policy_onnx), ('off_policy', args.off_policy_onnx), ('on_policy', args.on_policy_onnx)]:
|
frame_prepare = make_frame_prepare(nv12, model_w, model_h)
|
||||||
if onnx_arg:
|
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip)
|
||||||
runners.append(OnnxRunner(onnx_arg))
|
sample_desire_fn = partial(sample_desire, frame_skip=frame_skip)
|
||||||
keys.append(name)
|
|
||||||
return runners, keys
|
desire_key = _detect_desire_key(input_shapes)
|
||||||
|
if desire_key is None:
|
||||||
|
raise ValueError(f"No desire* key found in input_shapes: {list(input_shapes.keys())}")
|
||||||
|
road_img_key, wide_img_key = _detect_vision_keys(input_shapes)
|
||||||
|
extra_policy_keys = [k for k in input_shapes
|
||||||
|
if k not in (desire_key, 'features_buffer', 'traffic_convention')
|
||||||
|
and 'img' not in k]
|
||||||
|
|
||||||
|
def run_supercombo(img_q, big_img_q, feat_q, desire_q,
|
||||||
|
frame, big_frame, **kwargs):
|
||||||
|
desire = kwargs.get(desire_key)
|
||||||
|
traffic_convention = kwargs.get('traffic_convention')
|
||||||
|
tfm = kwargs['tfm']
|
||||||
|
big_tfm = kwargs['big_tfm']
|
||||||
|
|
||||||
|
tfm = tfm.to(Device.DEFAULT)
|
||||||
|
big_tfm = big_tfm.to(Device.DEFAULT)
|
||||||
|
desire = desire.to(Device.DEFAULT)
|
||||||
|
traffic_convention = traffic_convention.to(Device.DEFAULT)
|
||||||
|
Tensor.realize(tfm, big_tfm, desire, traffic_convention)
|
||||||
|
|
||||||
|
img = shift_and_sample(img_q, frame_prepare(frame, tfm).unsqueeze(0), sample_skip_fn)
|
||||||
|
big_img = shift_and_sample(big_img_q, frame_prepare(big_frame, big_tfm).unsqueeze(0), sample_skip_fn)
|
||||||
|
|
||||||
|
if prepare_only:
|
||||||
|
return img, big_img
|
||||||
|
|
||||||
|
desire_buf = shift_and_sample(desire_q, desire.reshape(1, 1, -1), sample_desire_fn)
|
||||||
|
feat_buf = sample_skip_fn(feat_q)
|
||||||
|
|
||||||
|
inputs = {road_img_key: img, wide_img_key: big_img,
|
||||||
|
desire_key: desire_buf, 'features_buffer': feat_buf,
|
||||||
|
'traffic_convention': traffic_convention}
|
||||||
|
for k in extra_policy_keys:
|
||||||
|
if k in kwargs:
|
||||||
|
inputs[k] = kwargs[k].to(Device.DEFAULT)
|
||||||
|
|
||||||
|
model_out = next(iter(model_runner(inputs).values())).cast('float32')
|
||||||
|
|
||||||
|
new_feat = model_out[:, features_slice].reshape(1, -1).unsqueeze(0)
|
||||||
|
shift_and_sample(feat_q, new_feat, sample_skip_fn)
|
||||||
|
|
||||||
|
return model_out
|
||||||
|
|
||||||
|
return run_supercombo
|
||||||
|
|
||||||
|
|
||||||
|
def make_run_vision_multi_policy(vision_runner, policy_runners, nv12: NV12Frame, model_w, model_h,
|
||||||
|
vision_features_slice, frame_skip, desire_key, extra_policy_keys,
|
||||||
|
vision_road_key, vision_wide_key, prepare_only=False):
|
||||||
|
frame_prepare = make_frame_prepare(nv12, model_w, model_h)
|
||||||
|
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip)
|
||||||
|
sample_desire_fn = partial(sample_desire, frame_skip=frame_skip)
|
||||||
|
|
||||||
|
def run_multi_policy(img_q, big_img_q, feat_q, desire_q, desire,
|
||||||
|
traffic_convention, tfm, big_tfm, frame, big_frame, **extra):
|
||||||
|
npy_tensors = [tfm.to(Device.DEFAULT), big_tfm.to(Device.DEFAULT),
|
||||||
|
desire.to(Device.DEFAULT), traffic_convention.to(Device.DEFAULT)]
|
||||||
|
extra_device = {k: extra[k].to(Device.DEFAULT) for k in extra_policy_keys}
|
||||||
|
Tensor.realize(*npy_tensors, *extra_device.values())
|
||||||
|
tfm, big_tfm, desire, traffic_convention = npy_tensors
|
||||||
|
|
||||||
|
img = shift_and_sample(img_q, frame_prepare(frame, tfm).unsqueeze(0), sample_skip_fn)
|
||||||
|
big_img = shift_and_sample(big_img_q, frame_prepare(big_frame, big_tfm).unsqueeze(0), sample_skip_fn)
|
||||||
|
|
||||||
|
if prepare_only:
|
||||||
|
return img, big_img
|
||||||
|
|
||||||
|
vision_out = next(iter(vision_runner({vision_road_key: img, vision_wide_key: big_img}).values())).cast('float32')
|
||||||
|
|
||||||
|
new_feat = vision_out[:, vision_features_slice].reshape(1, -1).unsqueeze(0)
|
||||||
|
feat_buf = shift_and_sample(feat_q, new_feat, sample_skip_fn)
|
||||||
|
desire_buf = shift_and_sample(desire_q, desire.reshape(1, 1, -1), sample_desire_fn)
|
||||||
|
|
||||||
|
inputs = {'features_buffer': feat_buf, desire_key: desire_buf, 'traffic_convention': traffic_convention, **extra_device}
|
||||||
|
|
||||||
|
policy_outputs = []
|
||||||
|
for runner in policy_runners:
|
||||||
|
policy_out = next(iter(runner(inputs).values())).cast('float32')
|
||||||
|
policy_outputs.append(policy_out)
|
||||||
|
|
||||||
|
return (vision_out, *policy_outputs)
|
||||||
|
|
||||||
|
return run_multi_policy
|
||||||
|
|
||||||
|
|
||||||
|
def _warmup_and_serialize(run_jit, input_queues, npy, nv12):
|
||||||
|
for i in range(3):
|
||||||
|
rng = np.random.default_rng(42 + i)
|
||||||
|
frame = Tensor.randint(nv12.size, low=0, high=256, dtype='uint8').realize()
|
||||||
|
big_frame = Tensor.randint(nv12.size, low=0, high=256, dtype='uint8').realize()
|
||||||
|
for v in npy.values():
|
||||||
|
v[:] = rng.standard_normal(v.shape).astype(v.dtype)
|
||||||
|
Device.default.synchronize()
|
||||||
|
st = time.perf_counter()
|
||||||
|
run_jit(**input_queues, frame=frame, big_frame=big_frame)
|
||||||
|
mt = time.perf_counter()
|
||||||
|
Device.default.synchronize()
|
||||||
|
et = time.perf_counter()
|
||||||
|
print(f" [{i + 1}/3] enqueue {(mt - st) * 1e3:6.2f} ms -- total {(et - st) * 1e3:6.2f} ms")
|
||||||
|
return pickle.loads(pickle.dumps(run_jit))
|
||||||
|
|
||||||
|
|
||||||
|
def compile_supercombo(nv12: NV12Frame, model_w, model_h, prepare_only, frame_skip,
|
||||||
|
model_runner, metadata):
|
||||||
|
print(f"Compiling combined supercombo JIT for {nv12.width}x{nv12.height} (prepare_only={prepare_only})...")
|
||||||
|
|
||||||
|
features_slice = metadata['output_slices']['hidden_state']
|
||||||
|
input_shapes = metadata['input_shapes']
|
||||||
|
|
||||||
|
_run = make_run_supercombo(model_runner, nv12, model_w, model_h,
|
||||||
|
features_slice, frame_skip, input_shapes, prepare_only)
|
||||||
|
run_jit = TinyJit(_run, prune=True)
|
||||||
|
|
||||||
|
input_queues, npy = make_supercombo_input_queues(input_shapes, frame_skip, Device.DEFAULT)
|
||||||
|
|
||||||
|
run_jit = _warmup_and_serialize(run_jit, input_queues, npy, nv12)
|
||||||
|
return run_jit
|
||||||
|
|
||||||
|
|
||||||
|
def compile_multi_policy(nv12: NV12Frame, model_w, model_h, prepare_only, frame_skip,
|
||||||
|
vision_runner, policy_runners, vision_metadata, policy_metadata):
|
||||||
|
print(f"Compiling combined multi-policy JIT for {nv12.width}x{nv12.height} (prepare_only={prepare_only})...")
|
||||||
|
|
||||||
|
vision_features_slice = vision_metadata['output_slices']['hidden_state']
|
||||||
|
vision_input_shapes = vision_metadata['input_shapes']
|
||||||
|
policy_input_shapes = policy_metadata['input_shapes']
|
||||||
|
desire_key = _detect_desire_key(policy_input_shapes)
|
||||||
|
extra_policy_keys = [k for k in policy_input_shapes if k not in ('features_buffer', desire_key, 'traffic_convention')]
|
||||||
|
vision_road_key, vision_wide_key = _detect_vision_keys(vision_input_shapes)
|
||||||
|
|
||||||
|
_run = make_run_vision_multi_policy(vision_runner, policy_runners, nv12, model_w, model_h,
|
||||||
|
vision_features_slice, frame_skip, desire_key, extra_policy_keys,
|
||||||
|
vision_road_key, vision_wide_key, prepare_only)
|
||||||
|
run_jit = TinyJit(_run, prune=True)
|
||||||
|
|
||||||
|
input_queues, npy = make_split_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, Device.DEFAULT)
|
||||||
|
|
||||||
|
run_jit = _warmup_and_serialize(run_jit, input_queues, npy, nv12)
|
||||||
|
return run_jit
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_size(s):
|
||||||
|
w, h = s.lower().split('x')
|
||||||
|
return int(w), int(h)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
from openpilot.selfdrive.modeld.get_model_metadata import make_metadata_dict
|
|
||||||
from tinygrad.nn.onnx import OnnxRunner
|
from tinygrad.nn.onnx import OnnxRunner
|
||||||
|
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||||
|
from openpilot.selfdrive.modeld.get_model_metadata import make_metadata_dict
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="Compile combined JIT pkl for sunnypilot modeld_v2")
|
p = argparse.ArgumentParser(description="Compile combined JIT pkl for sunnypilot modeld_v2")
|
||||||
parser.add_argument('--model-type', choices=MODEL_TYPES, required=True)
|
p.add_argument('--model-type', choices=MODEL_TYPES, required=True)
|
||||||
parser.add_argument('--model-size', type=_parse_size, required=True, help='model input WxH')
|
p.add_argument('--model-size', type=_parse_size, required=True, help='model input WxH')
|
||||||
parser.add_argument('--camera-resolutions', type=_parse_size, nargs='+', required=True)
|
p.add_argument('--camera-resolutions', type=_parse_size, nargs='+', required=True)
|
||||||
parser.add_argument('--frame-skip', type=int, default=None, help='frame skip value (auto-derived if not provided)')
|
p.add_argument('--frame-skip', type=int, default=None, help='frame skip value (auto-derived if not provided)')
|
||||||
parser.add_argument('--output', required=True)
|
p.add_argument('--output', required=True)
|
||||||
|
|
||||||
parser.add_argument('--vision-onnx', help='vision ONNX (for split models)')
|
p.add_argument('--vision-onnx', help='vision ONNX (for split models)')
|
||||||
parser.add_argument('--policy-onnx', help='policy ONNX (for vision_policy)')
|
p.add_argument('--policy-onnx', help='policy ONNX (for vision_policy)')
|
||||||
parser.add_argument('--off-policy-onnx', help='off-policy ONNX (for vision_multi_policy)')
|
p.add_argument('--off-policy-onnx', help='off-policy ONNX (for vision_multi_policy)')
|
||||||
parser.add_argument('--on-policy-onnx', help='on-policy ONNX (for vision_multi_policy)')
|
p.add_argument('--on-policy-onnx', help='on-policy ONNX (for vision_multi_policy)')
|
||||||
parser.add_argument('--supercombo-onnx', help='supercombo ONNX (for supercombo)')
|
p.add_argument('--supercombo-onnx', help='supercombo ONNX (for supercombo)')
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = p.parse_args()
|
||||||
output_data = defaultdict(dict)
|
out = defaultdict(dict)
|
||||||
|
|
||||||
args.vision_onnx = read_file_chunked_to_shm(args.vision_onnx)
|
|
||||||
args.policy_onnx = read_file_chunked_to_shm(args.policy_onnx)
|
|
||||||
args.off_policy_onnx = read_file_chunked_to_shm(args.off_policy_onnx)
|
|
||||||
args.on_policy_onnx = read_file_chunked_to_shm(args.on_policy_onnx)
|
|
||||||
args.supercombo_onnx = read_file_chunked_to_shm(args.supercombo_onnx)
|
|
||||||
|
|
||||||
vision_runner = OnnxRunner(args.vision_onnx) if args.vision_onnx else None
|
|
||||||
|
|
||||||
if args.model_type == 'vision_policy':
|
if args.model_type == 'vision_policy':
|
||||||
assert vision_runner and args.policy_onnx
|
assert args.vision_onnx and args.policy_onnx
|
||||||
policy_runners = [OnnxRunner(args.policy_onnx)]
|
vision_runner = OnnxRunner(args.vision_onnx)
|
||||||
output_data['metadata'] = {'vision': make_metadata_dict(args.vision_onnx), 'policy': make_metadata_dict(args.policy_onnx)}
|
policy_runner = OnnxRunner(args.policy_onnx)
|
||||||
|
out['metadata']['vision'] = make_metadata_dict(args.vision_onnx)
|
||||||
|
out['metadata']['policy'] = make_metadata_dict(args.policy_onnx)
|
||||||
|
|
||||||
|
frame_skip = args.frame_skip if args.frame_skip is not None else derive_frame_skip(out['metadata']['vision']['input_shapes'],
|
||||||
|
out['metadata']['policy']['input_shapes'])
|
||||||
|
|
||||||
|
for cam_w, cam_h in args.camera_resolutions:
|
||||||
|
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
|
||||||
|
model_w, model_h = args.model_size
|
||||||
|
out[(cam_w, cam_h)] = {
|
||||||
|
name: compile_split_policy(nv12, model_w, model_h, prepare_only, frame_skip,
|
||||||
|
vision_runner, policy_runner,
|
||||||
|
out['metadata']['vision'], out['metadata']['policy'])
|
||||||
|
for name, prepare_only in [('warp_enqueue', True), ('run_policy', False)]
|
||||||
|
}
|
||||||
|
|
||||||
elif args.model_type == 'supercombo':
|
elif args.model_type == 'supercombo':
|
||||||
assert args.supercombo_onnx
|
assert args.supercombo_onnx
|
||||||
policy_runners = [OnnxRunner(args.supercombo_onnx)]
|
model_runner = OnnxRunner(args.supercombo_onnx)
|
||||||
output_data['metadata'] = {'model': make_metadata_dict(args.supercombo_onnx)}
|
out['metadata']['model'] = make_metadata_dict(args.supercombo_onnx)
|
||||||
|
|
||||||
|
frame_skip = args.frame_skip if args.frame_skip is not None else derive_frame_skip({}, out['metadata']['model']['input_shapes'])
|
||||||
|
|
||||||
|
for cam_w, cam_h in args.camera_resolutions:
|
||||||
|
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
|
||||||
|
model_w, model_h = args.model_size
|
||||||
|
out[(cam_w, cam_h)] = {
|
||||||
|
name: compile_supercombo(nv12, model_w, model_h, prepare_only, frame_skip,
|
||||||
|
model_runner, out['metadata']['model'])
|
||||||
|
for name, prepare_only in [('warp_enqueue', True), ('run_policy', False)]
|
||||||
|
}
|
||||||
|
|
||||||
elif args.model_type == 'vision_multi_policy':
|
elif args.model_type == 'vision_multi_policy':
|
||||||
assert vision_runner
|
assert args.vision_onnx
|
||||||
policy_runners, policy_names = _load_policy_runners(args)
|
vision_runner = OnnxRunner(args.vision_onnx)
|
||||||
output_data['metadata'] = {'vision': make_metadata_dict(args.vision_onnx)}
|
out['metadata']['vision'] = make_metadata_dict(args.vision_onnx)
|
||||||
for name in policy_names:
|
|
||||||
runner_arg = getattr(args, f"{name}_onnx")
|
|
||||||
output_data['metadata'][name] = make_metadata_dict(runner_arg)
|
|
||||||
|
|
||||||
policy_keys = [key for key in output_data['metadata'].keys() if key != 'vision']
|
policy_runners = []
|
||||||
first_policy_meta = output_data['metadata'][policy_keys[0]] if policy_keys else {}
|
policy_onnxes = []
|
||||||
vision_meta = output_data['metadata'].get('vision', {})
|
if args.policy_onnx:
|
||||||
|
policy_onnxes.append(('policy', args.policy_onnx))
|
||||||
|
if args.off_policy_onnx:
|
||||||
|
policy_onnxes.append(('off_policy', args.off_policy_onnx))
|
||||||
|
if args.on_policy_onnx:
|
||||||
|
policy_onnxes.append(('on_policy', args.on_policy_onnx))
|
||||||
|
|
||||||
derived_frame_skip = args.frame_skip or derive_frame_skip(vision_meta.get('input_shapes', {}), first_policy_meta.get('input_shapes', {}))
|
for name, onnx_path in policy_onnxes:
|
||||||
output_data.update(_compile_for_resolutions(args.camera_resolutions, args.model_size, derived_frame_skip,
|
runner = OnnxRunner(onnx_path)
|
||||||
vision_runner, policy_runners, output_data['metadata']))
|
policy_runners.append(runner)
|
||||||
|
out['metadata'][name] = make_metadata_dict(onnx_path)
|
||||||
|
|
||||||
with open(args.output, "wb") as file:
|
first_policy_key = policy_onnxes[0][0]
|
||||||
pickle.dump(output_data, file)
|
frame_skip = args.frame_skip if args.frame_skip is not None else derive_frame_skip(out['metadata']['vision']['input_shapes'],
|
||||||
|
out['metadata'][first_policy_key]['input_shapes'])
|
||||||
|
|
||||||
|
for cam_w, cam_h in args.camera_resolutions:
|
||||||
|
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
|
||||||
|
model_w, model_h = args.model_size
|
||||||
|
out[(cam_w, cam_h)] = {
|
||||||
|
name: compile_multi_policy(nv12, model_w, model_h, prepare_only, frame_skip,
|
||||||
|
vision_runner, policy_runners,
|
||||||
|
out['metadata']['vision'], out['metadata'][first_policy_key])
|
||||||
|
for name, prepare_only in [('warp_enqueue', True), ('run_policy', False)]
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(args.output, "wb") as f:
|
||||||
|
pickle.dump(out, f)
|
||||||
pkl_size = os.path.getsize(args.output)
|
pkl_size = os.path.getsize(args.output)
|
||||||
print(f"Saved combined JIT to {args.output} ({pkl_size / 1e6:.2f} MB)")
|
print(f"Saved combined JIT to {args.output} ({pkl_size / 1e6:.2f} MB)")
|
||||||
|
|
||||||
from openpilot.common.file_chunker import chunk_file, get_chunk_targets
|
from openpilot.common.file_chunker import chunk_file, get_chunk_targets
|
||||||
chunk_targets = get_chunk_targets(args.output, pkl_size)
|
chunk_targets = get_chunk_targets(args.output, pkl_size)
|
||||||
chunk_file(args.output, chunk_targets)
|
chunk_file(args.output, chunk_targets)
|
||||||
print(f"Chunked into {len(chunk_targets) - 1} file(s)")
|
num_chunks = len(chunk_targets) - 1
|
||||||
|
print(f"Chunked into {num_chunks} file(s)")
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ See the LICENSE.md file in the root directory for more details.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
os.environ['GMMU'] = '0'
|
|
||||||
from openpilot.common.hardware import TICI
|
from openpilot.common.hardware import TICI
|
||||||
os.environ['DEV'] = 'QCOM' if TICI else 'CPU'
|
os.environ['DEV'] = 'QCOM' if TICI else 'CPU'
|
||||||
USBGPU = "USBGPU" in os.environ
|
USBGPU = "USBGPU" in os.environ
|
||||||
@@ -111,8 +110,6 @@ class ModelState(ModelStateBase):
|
|||||||
jits = pickle.load(open_file_chunked(pkl_path))
|
jits = pickle.load(open_file_chunked(pkl_path))
|
||||||
|
|
||||||
self.DEV = Device.DEFAULT
|
self.DEV = Device.DEFAULT
|
||||||
self.WARP_DEV = 'CPU' if USBGPU else self.DEV
|
|
||||||
self.QUEUE_DEV = self.DEV
|
|
||||||
|
|
||||||
metadata = jits['metadata']
|
metadata = jits['metadata']
|
||||||
if 'model' in metadata:
|
if 'model' in metadata:
|
||||||
@@ -124,7 +121,7 @@ class ModelState(ModelStateBase):
|
|||||||
self._vision_input_names = [k for k in model_metadata['input_shapes'] if 'img' in k]
|
self._vision_input_names = [k for k in model_metadata['input_shapes'] if 'img' in k]
|
||||||
from openpilot.sunnypilot.modeld_v2.compile_modeld import make_supercombo_input_queues
|
from openpilot.sunnypilot.modeld_v2.compile_modeld import make_supercombo_input_queues
|
||||||
frame_skip = derive_frame_skip({}, model_metadata['input_shapes'])
|
frame_skip = derive_frame_skip({}, model_metadata['input_shapes'])
|
||||||
self.input_queues, self.numpy_inputs = make_supercombo_input_queues(model_metadata['input_shapes'], frame_skip, device=self.QUEUE_DEV)
|
self.input_queues, self.numpy_inputs = make_supercombo_input_queues(model_metadata['input_shapes'], frame_skip, device=self.DEV)
|
||||||
else:
|
else:
|
||||||
vision_metadata = metadata['vision']
|
vision_metadata = metadata['vision']
|
||||||
policy_keys = [k for k in metadata if k != 'vision']
|
policy_keys = [k for k in metadata if k != 'vision']
|
||||||
@@ -142,11 +139,7 @@ class ModelState(ModelStateBase):
|
|||||||
policy_input_shapes = first_policy_metadata['input_shapes']
|
policy_input_shapes = first_policy_metadata['input_shapes']
|
||||||
self._vision_input_names = [k for k in vision_input_shapes if 'img' in k]
|
self._vision_input_names = [k for k in vision_input_shapes if 'img' in k]
|
||||||
frame_skip = derive_frame_skip(vision_input_shapes, policy_input_shapes)
|
frame_skip = derive_frame_skip(vision_input_shapes, policy_input_shapes)
|
||||||
self.input_queues, self.numpy_inputs = make_split_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, device=self.QUEUE_DEV)
|
self.input_queues, self.numpy_inputs = make_split_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, device=self.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)
|
|
||||||
|
|
||||||
from openpilot.sunnypilot.modeld_v2.parse_model_outputs_split import Parser as SplitParser
|
from openpilot.sunnypilot.modeld_v2.parse_model_outputs_split import Parser as SplitParser
|
||||||
from openpilot.sunnypilot.modeld_v2.parse_model_outputs import Parser as CombinedParser
|
from openpilot.sunnypilot.modeld_v2.parse_model_outputs import Parser as CombinedParser
|
||||||
@@ -168,11 +161,12 @@ class ModelState(ModelStateBase):
|
|||||||
|
|
||||||
self._run_policy = jits[(cam_w, cam_h)]['run_policy']
|
self._run_policy = jits[(cam_w, cam_h)]['run_policy']
|
||||||
self._warp_enqueue = jits[(cam_w, cam_h)]['warp_enqueue']
|
self._warp_enqueue = jits[(cam_w, cam_h)]['warp_enqueue']
|
||||||
yuv_size = self.frame_buf_params[self._road_key][3]
|
road_name = next(k for k in self._vision_input_names if 'big' not in k)
|
||||||
|
yuv_size = self.frame_buf_params[road_name][3]
|
||||||
self._warp_enqueue(
|
self._warp_enqueue(
|
||||||
**self.input_queues,
|
**self.input_queues,
|
||||||
frame=Tensor(np.zeros(yuv_size, dtype=np.uint8), device=self.WARP_DEV).contiguous().realize(),
|
frame=Tensor(np.zeros(yuv_size, dtype=np.uint8), device=self.DEV).contiguous().realize(),
|
||||||
big_frame=Tensor(np.zeros(yuv_size, dtype=np.uint8), device=self.WARP_DEV).contiguous().realize())
|
big_frame=Tensor(np.zeros(yuv_size, dtype=np.uint8), device=self.DEV).contiguous().realize())
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -185,7 +179,7 @@ class ModelState(ModelStateBase):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def desire_key(self) -> str:
|
def desire_key(self) -> str:
|
||||||
return self._desire_key
|
return next(k for k in self.numpy_inputs if k.startswith('desire'))
|
||||||
|
|
||||||
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
|
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
|
||||||
inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None:
|
inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None:
|
||||||
@@ -196,19 +190,19 @@ class ModelState(ModelStateBase):
|
|||||||
yuv_size = self.frame_buf_params[key][3]
|
yuv_size = self.frame_buf_params[key][3]
|
||||||
cache_key = (key, ptr)
|
cache_key = (key, ptr)
|
||||||
if cache_key not in self._blob_cache:
|
if cache_key not in self._blob_cache:
|
||||||
self._blob_cache[cache_key] = Tensor.from_blob(ptr, (yuv_size,), dtype='uint8', device=self.WARP_DEV)
|
self._blob_cache[cache_key] = Tensor.from_blob(ptr, (yuv_size,), dtype='uint8', device=self.DEV)
|
||||||
self.full_frames[key] = self._blob_cache[cache_key]
|
self.full_frames[key] = self._blob_cache[cache_key]
|
||||||
|
|
||||||
desire_key = self.desire_key
|
desire_key = self.desire_key
|
||||||
inputs[desire_key][0] = 0
|
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][:] = np.where(inputs[desire_key] - self.prev_desire > .99, inputs[desire_key], 0)
|
||||||
self.prev_desire[:] = inputs[desire_key]
|
self.prev_desire[:] = inputs[desire_key]
|
||||||
for key in ('traffic_convention', 'lateral_control_params', 'action_t'):
|
for key in ('traffic_convention', 'lateral_control_params'):
|
||||||
if key in self.numpy_inputs and key in inputs:
|
if key in self.numpy_inputs and key in inputs:
|
||||||
self.numpy_inputs[key][:] = inputs[key]
|
self.numpy_inputs[key][:] = inputs[key]
|
||||||
|
|
||||||
road_key = self._road_key
|
road_key = next(n for n in bufs if 'big' not in n)
|
||||||
wide_key = self._wide_key
|
wide_key = next(n for n in bufs if 'big' in n)
|
||||||
self.numpy_inputs['tfm'][:, :] = transforms[road_key].reshape(3, 3)
|
self.numpy_inputs['tfm'][:, :] = transforms[road_key].reshape(3, 3)
|
||||||
self.numpy_inputs['big_tfm'][:, :] = transforms[wide_key].reshape(3, 3)
|
self.numpy_inputs['big_tfm'][:, :] = transforms[wide_key].reshape(3, 3)
|
||||||
|
|
||||||
@@ -231,12 +225,8 @@ class ModelState(ModelStateBase):
|
|||||||
policy_output = raw_outputs[i + 1].numpy().flatten()
|
policy_output = raw_outputs[i + 1].numpy().flatten()
|
||||||
policy_sliced = {k: policy_output[np.newaxis, v] for k, v in policy_slices.items()}
|
policy_sliced = {k: policy_output[np.newaxis, v] for k, v in policy_slices.items()}
|
||||||
parsed = self.parser.parse_policy_outputs(policy_sliced)
|
parsed = self.parser.parse_policy_outputs(policy_sliced)
|
||||||
if ('off' in self._policy_keys[i]
|
if 'off' in self._policy_keys[i] and self._has_on_policy:
|
||||||
and self._has_on_policy
|
|
||||||
and any('plan' in self._policy_slices_list[j] for j, k in enumerate(self._policy_keys) if 'on' in k.lower())):
|
|
||||||
|
|
||||||
parsed.pop('plan', None)
|
parsed.pop('plan', None)
|
||||||
|
|
||||||
outputs.update(parsed)
|
outputs.update(parsed)
|
||||||
|
|
||||||
if 'planplus' in outputs and 'plan' in outputs:
|
if 'planplus' in outputs and 'plan' in outputs:
|
||||||
@@ -251,20 +241,13 @@ class ModelState(ModelStateBase):
|
|||||||
|
|
||||||
def get_action_from_model(self, model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action,
|
def get_action_from_model(self, model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action,
|
||||||
lat_action_t: float, long_action_t: float, v_ego: float) -> log.ModelDataV2.Action:
|
lat_action_t: float, long_action_t: float, v_ego: float) -> log.ModelDataV2.Action:
|
||||||
if 'action' not in model_output:
|
plan = model_output['plan'][0]
|
||||||
plan = model_output['plan'][0]
|
desired_accel, should_stop = get_accel_from_plan(plan[:, Plan.VELOCITY][:, 0], plan[:, Plan.ACCELERATION][:, 0], self.constants.T_IDXS,
|
||||||
desired_accel, should_stop = get_accel_from_plan(plan[:, Plan.VELOCITY][:, 0], plan[:, Plan.ACCELERATION][:, 0], self.constants.T_IDXS,
|
action_t=long_action_t)
|
||||||
action_t=long_action_t)
|
desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS)
|
||||||
desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS)
|
|
||||||
|
|
||||||
curvature_plan = (plan + (self.PLANPLUS_CONTROL - 1.0) * model_output['planplus'][0]
|
|
||||||
if 'planplus' in model_output and self.PLANPLUS_CONTROL != 1.0 else plan)
|
|
||||||
desired_curvature = get_curvature_from_output(model_output, curvature_plan, v_ego, lat_action_t, self.mlsim)
|
|
||||||
else:
|
|
||||||
desired_accel = model_output['action'][0, 1]
|
|
||||||
desired_curvature = model_output['action'][0, 0] / (max(1.0, v_ego))**2
|
|
||||||
should_stop = (v_ego < 0.3 and desired_accel < 0.1)
|
|
||||||
|
|
||||||
|
curvature_plan = plan + (self.PLANPLUS_CONTROL - 1.0) * model_output['planplus'][0] if 'planplus' in model_output and self.PLANPLUS_CONTROL != 1.0 else plan
|
||||||
|
desired_curvature = get_curvature_from_output(model_output, curvature_plan, v_ego, lat_action_t, self.mlsim)
|
||||||
if self.generation is not None and self.generation >= 10: # smooth curvature for post FOF models
|
if self.generation is not None and self.generation >= 10: # smooth curvature for post FOF models
|
||||||
if v_ego > self.MIN_LAT_CONTROL_SPEED:
|
if v_ego > self.MIN_LAT_CONTROL_SPEED:
|
||||||
desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, self.LAT_SMOOTH_SECONDS)
|
desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, self.LAT_SMOOTH_SECONDS)
|
||||||
@@ -417,12 +400,6 @@ def main(demo=False):
|
|||||||
|
|
||||||
bufs = {name: buf_extra if 'big' in name else buf_main for name in model.vision_input_names}
|
bufs = {name: buf_extra if 'big' in name else buf_main for name in model.vision_input_names}
|
||||||
transforms = {name: model_transform_extra if 'big' in name else model_transform_main for name in model.vision_input_names}
|
transforms = {name: model_transform_extra if 'big' in name else model_transform_main for name in model.vision_input_names}
|
||||||
|
|
||||||
frame_delay = DT_MDL # compensate for time passed since the frame was captured: current_time - timestamp_eof is 50ms on average
|
|
||||||
action_delay = DT_MDL / 2 # middle of the interval between model output (current state) and next frame (expected state)
|
|
||||||
lat_action_t = lat_delay + frame_delay + action_delay
|
|
||||||
long_action_t = long_delay + frame_delay + action_delay
|
|
||||||
|
|
||||||
inputs:dict[str, np.ndarray] = {
|
inputs:dict[str, np.ndarray] = {
|
||||||
model.desire_key: vec_desire,
|
model.desire_key: vec_desire,
|
||||||
'traffic_convention': traffic_convention,
|
'traffic_convention': traffic_convention,
|
||||||
@@ -431,9 +408,6 @@ def main(demo=False):
|
|||||||
if 'lateral_control_params' in model.numpy_inputs:
|
if 'lateral_control_params' in model.numpy_inputs:
|
||||||
inputs['lateral_control_params'] = np.array([v_ego, lat_delay], dtype=np.float32)
|
inputs['lateral_control_params'] = np.array([v_ego, lat_delay], dtype=np.float32)
|
||||||
|
|
||||||
if 'action_t' in model.numpy_inputs:
|
|
||||||
inputs['action_t'] = np.array([lat_action_t, long_action_t], dtype=np.float32)
|
|
||||||
|
|
||||||
mt1 = time.perf_counter()
|
mt1 = time.perf_counter()
|
||||||
model_output = model.run(bufs, transforms, inputs, prepare_only)
|
model_output = model.run(bufs, transforms, inputs, prepare_only)
|
||||||
mt2 = time.perf_counter()
|
mt2 = time.perf_counter()
|
||||||
@@ -445,7 +419,7 @@ def main(demo=False):
|
|||||||
posenet_send = messaging.new_message('cameraOdometry')
|
posenet_send = messaging.new_message('cameraOdometry')
|
||||||
mdv2sp_send = messaging.new_message('modelDataV2SP')
|
mdv2sp_send = messaging.new_message('modelDataV2SP')
|
||||||
|
|
||||||
action = model.get_action_from_model(model_output, prev_action, lat_action_t, long_action_t, v_ego)
|
action = model.get_action_from_model(model_output, prev_action, lat_delay + DT_MDL, long_delay + DT_MDL, v_ego)
|
||||||
prev_action = action
|
prev_action = action
|
||||||
fill_model_msg(drivingdata_send, modelv2_send, model_output, action,
|
fill_model_msg(drivingdata_send, modelv2_send, model_output, action,
|
||||||
publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id,
|
publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id,
|
||||||
|
|||||||
@@ -137,8 +137,6 @@ class Parser:
|
|||||||
out_shape=(SplitModelConstants.NUM_ROAD_EDGES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH))
|
out_shape=(SplitModelConstants.NUM_ROAD_EDGES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH))
|
||||||
if 'sim_pose' in outs:
|
if 'sim_pose' in outs:
|
||||||
self.parse_mdn('sim_pose', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,))
|
self.parse_mdn('sim_pose', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,))
|
||||||
if 'action' in outs:
|
|
||||||
self.parse_mdn('action', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.ACTION_WIDTH,))
|
|
||||||
|
|
||||||
def parse_vision_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
|
def parse_vision_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
|
||||||
self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,))
|
self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,))
|
||||||
|
|||||||
@@ -72,9 +72,15 @@ class TestStockEquivalence:
|
|||||||
stock_queues, stock_npy = make_input_queues(stock_shapes, frame_skip, device='NPY')
|
stock_queues, stock_npy = make_input_queues(stock_shapes, frame_skip, device='NPY')
|
||||||
|
|
||||||
# TODO-SP: remove action_t skip once SP adds prerequisite for deep models (action_t input queue)
|
# TODO-SP: remove action_t skip once SP adds prerequisite for deep models (action_t input queue)
|
||||||
skip_keys = {'action_t'}
|
# prev_feat is a stock QCOM corruption workaround handled inside the SP loader's JIT path
|
||||||
assert set(state.input_queues.keys()) == set(stock_queues.keys()) - skip_keys, \
|
skip_keys = {'action_t', 'prev_feat'}
|
||||||
f"Queue keys differ: v2={set(state.input_queues.keys())}, stock={set(stock_queues.keys())}"
|
# stock packs the per-key policy inputs into packed_npy_inputs; the npy views carry the individual keys
|
||||||
|
stock_queue_keys = set(stock_queues.keys())
|
||||||
|
if 'packed_npy_inputs' in stock_queue_keys:
|
||||||
|
stock_queue_keys.remove('packed_npy_inputs')
|
||||||
|
stock_queue_keys |= set(stock_npy.keys())
|
||||||
|
assert set(state.input_queues.keys()) == stock_queue_keys - skip_keys, \
|
||||||
|
f"Queue keys differ: v2={set(state.input_queues.keys())}, stock={stock_queue_keys}"
|
||||||
assert set(state.numpy_inputs.keys()) == set(stock_npy.keys()) - skip_keys, \
|
assert set(state.numpy_inputs.keys()) == set(stock_npy.keys()) - skip_keys, \
|
||||||
f"Npy keys differ: v2={set(state.numpy_inputs.keys())}, stock={set(stock_npy.keys())}"
|
f"Npy keys differ: v2={set(state.numpy_inputs.keys())}, stock={set(stock_npy.keys())}"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import os
|
||||||
|
os.environ['DEV'] = 'CPU'
|
||||||
|
import pytest
|
||||||
|
import numpy as np
|
||||||
|
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||||
|
from openpilot.sunnypilot.modeld_v2.warp import CAMERA_CONFIGS
|
||||||
|
from openpilot.sunnypilot.modeld_v2.warp import Warp, MODEL_W, MODEL_H
|
||||||
|
|
||||||
|
VISION_NAME_PAIRS = [ # needed to account for supercombos input_imgs
|
||||||
|
('img', 'big_img'),
|
||||||
|
('input_imgs', 'big_input_imgs'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class MockVisionBuf:
|
||||||
|
def __init__(self, w, h):
|
||||||
|
self.width = w
|
||||||
|
self.height = h
|
||||||
|
_, _, _, yuv_size = get_nv12_info(w, h)
|
||||||
|
self.data = np.zeros(yuv_size, dtype=np.uint8)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("buffer_length", [2, 5])
|
||||||
|
def test_warp_initialization(buffer_length):
|
||||||
|
warp = Warp(buffer_length)
|
||||||
|
assert warp.buffer_length == buffer_length
|
||||||
|
assert warp.img_buffer_shape == (buffer_length * 6, MODEL_H // 2, MODEL_W // 2)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("buffer_length", [2, 5])
|
||||||
|
@pytest.mark.parametrize("cam_w, cam_h", CAMERA_CONFIGS)
|
||||||
|
@pytest.mark.parametrize("road, wide", VISION_NAME_PAIRS)
|
||||||
|
def test_warp_process(buffer_length, cam_w, cam_h, road, wide):
|
||||||
|
warp = Warp(buffer_length)
|
||||||
|
mock_buf = MockVisionBuf(cam_w, cam_h)
|
||||||
|
transform = np.eye(3, dtype=np.float32).flatten()
|
||||||
|
bufs = {road: mock_buf, wide: mock_buf}
|
||||||
|
transforms = {road: transform, wide: transform}
|
||||||
|
|
||||||
|
out = warp.process(bufs, transforms)
|
||||||
|
assert isinstance(out, dict)
|
||||||
|
assert road in out and wide in out
|
||||||
|
assert out[road].shape == (1, 12, MODEL_H // 2, MODEL_W // 2)
|
||||||
|
assert out[wide].shape == (1, 12, MODEL_H // 2, MODEL_W // 2)
|
||||||
|
|
||||||
|
key = (cam_w, cam_h)
|
||||||
|
assert key in warp.jit_cache
|
||||||
|
|
||||||
|
out2 = warp.process(bufs, transforms)
|
||||||
|
assert out2[road].shape == out[road].shape
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("road, wide", VISION_NAME_PAIRS)
|
||||||
|
def test_warp_buffer_shift(road, wide):
|
||||||
|
warp = Warp(2)
|
||||||
|
cam_w, cam_h = CAMERA_CONFIGS[1]
|
||||||
|
transform = np.eye(3, dtype=np.float32).flatten()
|
||||||
|
|
||||||
|
buf1 = MockVisionBuf(cam_w, cam_h)
|
||||||
|
buf1.data[0] = 255
|
||||||
|
bufs1 = {road: buf1, wide: buf1}
|
||||||
|
transforms = {road: transform, wide: transform}
|
||||||
|
out1 = warp.process(bufs1, transforms)
|
||||||
|
road1 = out1[road].numpy().copy()
|
||||||
|
|
||||||
|
buf2 = MockVisionBuf(cam_w, cam_h)
|
||||||
|
buf2.data[0] = 128
|
||||||
|
bufs2 = {road: buf2, wide: buf2}
|
||||||
|
out2 = warp.process(bufs2, transforms)
|
||||||
|
assert not np.array_equal(road1, out2[road].numpy())
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("buffer_length", [2, 5])
|
||||||
|
@pytest.mark.parametrize("road, wide", VISION_NAME_PAIRS)
|
||||||
|
def test_warp_buffer_accumulation(buffer_length, road, wide):
|
||||||
|
warp = Warp(buffer_length)
|
||||||
|
cam_w, cam_h = CAMERA_CONFIGS[0]
|
||||||
|
transform = np.eye(3, dtype=np.float32).flatten()
|
||||||
|
transforms = {road: transform, wide: transform}
|
||||||
|
outputs = []
|
||||||
|
|
||||||
|
for i in range(buffer_length + 1):
|
||||||
|
buf = MockVisionBuf(cam_w, cam_h)
|
||||||
|
buf.data[:] = i * 10
|
||||||
|
out = warp.process({road: buf, wide: buf}, transforms)
|
||||||
|
outputs.append(out[road].numpy().copy())
|
||||||
|
|
||||||
|
assert warp.full_buffers['img'].shape == (buffer_length * 6, MODEL_H // 2, MODEL_W // 2)
|
||||||
|
for i in range(1, len(outputs)):
|
||||||
|
assert not np.array_equal(outputs[i - 1], outputs[i])
|
||||||
|
|
||||||
|
|
||||||
|
def test_warp_different_cameras_same_instance():
|
||||||
|
warp = Warp(2)
|
||||||
|
transform = np.eye(3, dtype=np.float32).flatten()
|
||||||
|
|
||||||
|
buf1 = MockVisionBuf(*CAMERA_CONFIGS[0])
|
||||||
|
warp.process({'img': buf1, 'big_img': buf1}, {'img': transform, 'big_img': transform})
|
||||||
|
assert len(warp.jit_cache) == 1
|
||||||
|
|
||||||
|
buf2 = MockVisionBuf(*CAMERA_CONFIGS[1])
|
||||||
|
warp.process({'img': buf2, 'big_img': buf2}, {'img': transform, 'big_img': transform})
|
||||||
|
assert len(warp.jit_cache) == 2
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
import pickle
|
||||||
|
import time
|
||||||
|
import numpy as np
|
||||||
|
from pathlib import Path
|
||||||
|
from tinygrad.tensor import Tensor
|
||||||
|
from tinygrad.engine.jit import TinyJit
|
||||||
|
from tinygrad.device import Device
|
||||||
|
|
||||||
|
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||||
|
from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE
|
||||||
|
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
|
||||||
|
from openpilot.selfdrive.modeld.compile_modeld import NV12Frame, make_frame_prepare as _make_frame_prepare
|
||||||
|
|
||||||
|
CAMERA_CONFIGS = [
|
||||||
|
(_ar_ox_fisheye.width, _ar_ox_fisheye.height),
|
||||||
|
(_os_fisheye.width, _os_fisheye.height),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def make_frame_prepare(cam_w, cam_h, model_w, model_h):
|
||||||
|
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
|
||||||
|
return _make_frame_prepare(nv12, model_w, model_h)
|
||||||
|
|
||||||
|
|
||||||
|
def warp_pkl_path(w, h):
|
||||||
|
from openpilot.selfdrive.modeld.helpers import MODELS_DIR
|
||||||
|
return MODELS_DIR / f'warp_{w}x{h}_tinygrad.pkl'
|
||||||
|
|
||||||
|
|
||||||
|
def make_update_img_input(frame_prepare, model_w, model_h):
|
||||||
|
def update_img_input_tinygrad(tensor, frame, M_inv):
|
||||||
|
M_inv = M_inv.to(Device.DEFAULT)
|
||||||
|
new_img = frame_prepare(frame, M_inv)
|
||||||
|
tensor.assign(tensor[6:].cat(new_img, dim=0).contiguous())
|
||||||
|
return Tensor.cat(tensor[:6], tensor[-6:], dim=0).contiguous().reshape(1, 12, model_h//2, model_w//2)
|
||||||
|
return update_img_input_tinygrad
|
||||||
|
|
||||||
|
|
||||||
|
def make_update_both_imgs(frame_prepare, model_w, model_h):
|
||||||
|
update_img = make_update_img_input(frame_prepare, model_w, model_h)
|
||||||
|
def update_both_imgs_tinygrad(calib_img_buffer, new_img, M_inv,
|
||||||
|
calib_big_img_buffer, new_big_img, M_inv_big):
|
||||||
|
calib_img_pair = update_img(calib_img_buffer, new_img, M_inv)
|
||||||
|
calib_big_img_pair = update_img(calib_big_img_buffer, new_big_img, M_inv_big)
|
||||||
|
return calib_img_pair, calib_big_img_pair
|
||||||
|
return update_both_imgs_tinygrad
|
||||||
|
|
||||||
|
MODELS_DIR = Path(__file__).parent / 'models'
|
||||||
|
MODEL_W, MODEL_H = MEDMODEL_INPUT_SIZE
|
||||||
|
UPSTREAM_BUFFER_LENGTH = 5
|
||||||
|
|
||||||
|
|
||||||
|
def v2_warp_pkl_path(cam_w, cam_h, buffer_length):
|
||||||
|
return MODELS_DIR / f'warp_{cam_w}x{cam_h}_b{buffer_length}_tinygrad.pkl'
|
||||||
|
|
||||||
|
|
||||||
|
def compile_v2_warp(cam_w, cam_h, buffer_length):
|
||||||
|
_, _, _, yuv_size = get_nv12_info(cam_w, cam_h)
|
||||||
|
img_buffer_shape = (buffer_length * 6, MODEL_H // 2, MODEL_W // 2)
|
||||||
|
|
||||||
|
print(f"Compiling v2 warp for {cam_w}x{cam_h} buffer_length={buffer_length}...")
|
||||||
|
|
||||||
|
frame_prepare = make_frame_prepare(cam_w, cam_h, MODEL_W, MODEL_H)
|
||||||
|
update_both_imgs = make_update_both_imgs(frame_prepare, MODEL_W, MODEL_H)
|
||||||
|
update_img_jit = TinyJit(update_both_imgs, prune=True)
|
||||||
|
|
||||||
|
full_buffer = Tensor.zeros(img_buffer_shape, dtype='uint8').contiguous().realize()
|
||||||
|
big_full_buffer = Tensor.zeros(img_buffer_shape, dtype='uint8').contiguous().realize()
|
||||||
|
new_frame_np = np.random.default_rng(0).integers(0, 256, yuv_size, dtype=np.uint8)
|
||||||
|
new_big_frame_np = np.random.default_rng(1).integers(0, 256, yuv_size, dtype=np.uint8)
|
||||||
|
for i in range(10):
|
||||||
|
img_inputs = [full_buffer,
|
||||||
|
Tensor.from_blob(new_frame_np.ctypes.data, (yuv_size,), dtype='uint8').realize(),
|
||||||
|
Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')]
|
||||||
|
big_img_inputs = [big_full_buffer,
|
||||||
|
Tensor.from_blob(new_big_frame_np.ctypes.data, (yuv_size,), dtype='uint8').realize(),
|
||||||
|
Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')]
|
||||||
|
inputs = img_inputs + big_img_inputs
|
||||||
|
Device.default.synchronize()
|
||||||
|
|
||||||
|
st = time.perf_counter()
|
||||||
|
_ = update_img_jit(*inputs)
|
||||||
|
mt = time.perf_counter()
|
||||||
|
Device.default.synchronize()
|
||||||
|
et = time.perf_counter()
|
||||||
|
print(f" [{i+1}/10] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms")
|
||||||
|
|
||||||
|
pkl_path = v2_warp_pkl_path(cam_w, cam_h, buffer_length)
|
||||||
|
with open(pkl_path, "wb") as f:
|
||||||
|
pickle.dump(update_img_jit, f)
|
||||||
|
print(f" Saved to {pkl_path}")
|
||||||
|
|
||||||
|
jit = pickle.load(open(pkl_path, "rb"))
|
||||||
|
verify_frame = np.random.default_rng(0).integers(0, 256, yuv_size, dtype=np.uint8)
|
||||||
|
verify_big_frame = np.random.default_rng(1).integers(0, 256, yuv_size, dtype=np.uint8)
|
||||||
|
fresh_inputs = [
|
||||||
|
Tensor.zeros(img_buffer_shape, dtype='uint8').contiguous().realize(),
|
||||||
|
Tensor.from_blob(verify_frame.ctypes.data, (yuv_size,), dtype='uint8').realize(),
|
||||||
|
Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY'),
|
||||||
|
Tensor.zeros(img_buffer_shape, dtype='uint8').contiguous().realize(),
|
||||||
|
Tensor.from_blob(verify_big_frame.ctypes.data, (yuv_size,), dtype='uint8').realize(),
|
||||||
|
Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY'),
|
||||||
|
]
|
||||||
|
jit(*fresh_inputs)
|
||||||
|
|
||||||
|
|
||||||
|
class Warp:
|
||||||
|
def __init__(self, buffer_length=2):
|
||||||
|
self.buffer_length = buffer_length
|
||||||
|
self.img_buffer_shape = (buffer_length * 6, MODEL_H // 2, MODEL_W // 2)
|
||||||
|
|
||||||
|
self.jit_cache = {}
|
||||||
|
self.full_buffers = {k: Tensor.zeros(self.img_buffer_shape, dtype='uint8').contiguous().realize() for k in ['img', 'big_img']}
|
||||||
|
self._blob_cache: dict[int, Tensor] = {}
|
||||||
|
self._nv12_cache: dict[tuple[int, int], int] = {}
|
||||||
|
self.transforms_np = {k: np.zeros((3, 3), dtype=np.float32) for k in ['img', 'big_img']}
|
||||||
|
self.transforms = {k: Tensor(v, device='NPY').realize() for k, v in self.transforms_np.items()}
|
||||||
|
|
||||||
|
def process(self, bufs, transforms):
|
||||||
|
if not bufs:
|
||||||
|
return {}
|
||||||
|
road = next(n for n in bufs if 'big' not in n)
|
||||||
|
wide = next(n for n in bufs if 'big' in n)
|
||||||
|
cam_w, cam_h = bufs[road].width, bufs[road].height
|
||||||
|
key = (cam_w, cam_h)
|
||||||
|
|
||||||
|
if key not in self.jit_cache:
|
||||||
|
v2_pkl = v2_warp_pkl_path(cam_w, cam_h, self.buffer_length)
|
||||||
|
if v2_pkl.exists():
|
||||||
|
with open(v2_pkl, 'rb') as f:
|
||||||
|
self.jit_cache[key] = pickle.load(f)
|
||||||
|
elif self.buffer_length == UPSTREAM_BUFFER_LENGTH:
|
||||||
|
upstream_pkl = warp_pkl_path(cam_w, cam_h)
|
||||||
|
if upstream_pkl.exists():
|
||||||
|
with open(upstream_pkl, 'rb') as f:
|
||||||
|
self.jit_cache[key] = pickle.load(f)
|
||||||
|
if key not in self.jit_cache:
|
||||||
|
frame_prepare = make_frame_prepare(cam_w, cam_h, MODEL_W, MODEL_H)
|
||||||
|
update_both_imgs = make_update_both_imgs(frame_prepare, MODEL_W, MODEL_H)
|
||||||
|
self.jit_cache[key] = TinyJit(update_both_imgs, prune=True)
|
||||||
|
|
||||||
|
if key not in self._nv12_cache:
|
||||||
|
self._nv12_cache[key] = get_nv12_info(cam_w, cam_h)[3]
|
||||||
|
yuv_size = self._nv12_cache[key]
|
||||||
|
|
||||||
|
road_ptr = bufs[road].data.ctypes.data
|
||||||
|
wide_ptr = bufs[wide].data.ctypes.data
|
||||||
|
if road_ptr not in self._blob_cache:
|
||||||
|
self._blob_cache[road_ptr] = Tensor.from_blob(road_ptr, (yuv_size,), dtype='uint8')
|
||||||
|
if wide_ptr not in self._blob_cache:
|
||||||
|
self._blob_cache[wide_ptr] = Tensor.from_blob(wide_ptr, (yuv_size,), dtype='uint8')
|
||||||
|
road_blob = self._blob_cache[road_ptr]
|
||||||
|
wide_blob = self._blob_cache[wide_ptr] if wide_ptr != road_ptr else Tensor.from_blob(wide_ptr, (yuv_size,), dtype='uint8')
|
||||||
|
np.copyto(self.transforms_np['img'], transforms[road].reshape(3, 3))
|
||||||
|
np.copyto(self.transforms_np['big_img'], transforms[wide].reshape(3, 3))
|
||||||
|
|
||||||
|
Device.default.synchronize()
|
||||||
|
res = self.jit_cache[key](
|
||||||
|
self.full_buffers['img'], road_blob, self.transforms['img'],
|
||||||
|
self.full_buffers['big_img'], wide_blob, self.transforms['big_img'],
|
||||||
|
)
|
||||||
|
out_road = res[0].realize()
|
||||||
|
out_wide = res[1].realize()
|
||||||
|
|
||||||
|
return {road: out_road, wide: out_wide}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
for cam_w, cam_h in CAMERA_CONFIGS:
|
||||||
|
for bl in [2, 5]:
|
||||||
|
compile_v2_warp(cam_w, cam_h, bl)
|
||||||
@@ -6,12 +6,11 @@ See the LICENSE.md file in the root directory for more details.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
import os
|
|
||||||
import requests
|
import requests
|
||||||
from requests.exceptions import (SSLError, RequestException, HTTPError)
|
from requests.exceptions import (SSLError, RequestException, HTTPError)
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from openpilot.common.swaglog import cloudlog
|
from openpilot.common.swaglog import cloudlog
|
||||||
from openpilot.system.hardware.hw import Paths
|
|
||||||
from openpilot.sunnypilot.models.helpers import is_bundle_version_compatible
|
from openpilot.sunnypilot.models.helpers import is_bundle_version_compatible
|
||||||
|
|
||||||
from openpilot.cereal import custom
|
from openpilot.cereal import custom
|
||||||
@@ -27,35 +26,11 @@ class ModelParser:
|
|||||||
download_uri.sha256 = download_uri_data.get("sha256")
|
download_uri.sha256 = download_uri_data.get("sha256")
|
||||||
return download_uri
|
return download_uri
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _parse_chunk(chunk_data) -> custom.ModelManagerSP.Chunk:
|
|
||||||
chunk = custom.ModelManagerSP.Chunk()
|
|
||||||
chunk.fileName = chunk_data.get("file_name")
|
|
||||||
chunk.sha256 = chunk_data.get("sha256")
|
|
||||||
return chunk
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_artifact(artifact_data) -> custom.ModelManagerSP.Artifact:
|
def _parse_artifact(artifact_data) -> custom.ModelManagerSP.Artifact:
|
||||||
artifact = custom.ModelManagerSP.Artifact()
|
artifact = custom.ModelManagerSP.Artifact()
|
||||||
artifact.fileName = artifact_data.get("file_name")
|
artifact.fileName = artifact_data.get("file_name")
|
||||||
artifact.downloadUri = ModelParser._parse_download_uri(artifact_data.get("download_uri", {}))
|
artifact.downloadUri = ModelParser._parse_download_uri(artifact_data.get("download_uri", {}))
|
||||||
|
|
||||||
if "chunks" in artifact_data:
|
|
||||||
artifact.chunks = [ModelParser._parse_chunk(chunk_data) for chunk_data in artifact_data["chunks"]]
|
|
||||||
|
|
||||||
try:
|
|
||||||
model_dir = Paths.model_root()
|
|
||||||
os.makedirs(model_dir, exist_ok=True)
|
|
||||||
manifest_path = os.path.join(model_dir, f"{artifact.fileName}.chunkmanifest")
|
|
||||||
num_chunks = str(len(artifact.chunks))
|
|
||||||
|
|
||||||
if not os.path.exists(manifest_path) or open(manifest_path).read().strip() != num_chunks:
|
|
||||||
with open(manifest_path, "w") as f:
|
|
||||||
f.write(num_chunks)
|
|
||||||
cloudlog.info(f"Wrote chunk manifest for {artifact.fileName}: {num_chunks} chunks")
|
|
||||||
except Exception as e:
|
|
||||||
cloudlog.warning(f"Failed to write chunk manifest for {artifact.fileName}: {e}")
|
|
||||||
|
|
||||||
return artifact
|
return artifact
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -141,7 +116,7 @@ class ModelCache:
|
|||||||
|
|
||||||
class ModelFetcher:
|
class ModelFetcher:
|
||||||
"""Handles fetching and caching of model data from remote source"""
|
"""Handles fetching and caching of model data from remote source"""
|
||||||
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v18.json"
|
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v17.json"
|
||||||
|
|
||||||
def __init__(self, params: Params):
|
def __init__(self, params: Params):
|
||||||
self.params = params
|
self.params = params
|
||||||
@@ -209,7 +184,4 @@ if __name__ == "__main__":
|
|||||||
# Print artifact details
|
# Print artifact details
|
||||||
print(f"Artifact: {model.artifact.fileName}, Download URI: {model.artifact.downloadUri.uri}")
|
print(f"Artifact: {model.artifact.fileName}, Download URI: {model.artifact.downloadUri.uri}")
|
||||||
# Print metadata details
|
# Print metadata details
|
||||||
if model.artifact.chunks:
|
print(f"Metadata: {model.metadata.fileName}, Download URI: {model.metadata.downloadUri.uri}")
|
||||||
print(f"Contains {len(model.artifact.chunks)} chunks.")
|
|
||||||
if hasattr(model, 'metadata') and model.metadata and model.metadata.fileName:
|
|
||||||
print(f"Metadata: {model.metadata.fileName}, Download URI: {model.metadata.downloadUri.uri}")
|
|
||||||
|
|||||||
@@ -89,16 +89,20 @@ class ModelManagerSP:
|
|||||||
del self._download_start_times[model.fileName]
|
del self._download_start_times[model.fileName]
|
||||||
|
|
||||||
async def _download_chunked(self, base_url: str, base_path: str, artifact) -> None:
|
async def _download_chunked(self, base_url: str, base_path: str, artifact) -> None:
|
||||||
from openpilot.common.file_chunker import get_chunk_name, get_manifest_path
|
from openpilot.common.file_chunker import get_manifest_path, get_chunk_name
|
||||||
|
manifest_url = get_manifest_path(base_url)
|
||||||
num_chunks = len(artifact.chunks)
|
|
||||||
if num_chunks == 0:
|
|
||||||
raise ValueError("No chunks defined in artifact")
|
|
||||||
|
|
||||||
manifest_path = get_manifest_path(base_path)
|
manifest_path = get_manifest_path(base_path)
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(manifest_url) as resp:
|
||||||
|
if resp.status == 404:
|
||||||
|
raise FileNotFoundError
|
||||||
|
resp.raise_for_status()
|
||||||
|
num_chunks = int((await resp.read()).strip())
|
||||||
|
|
||||||
self._download_start_times[artifact.fileName] = time.monotonic()
|
self._download_start_times[artifact.fileName] = time.monotonic()
|
||||||
|
|
||||||
for i, _ in enumerate(artifact.chunks):
|
for i in range(num_chunks):
|
||||||
chunk_url = get_chunk_name(base_url, i, num_chunks)
|
chunk_url = get_chunk_name(base_url, i, num_chunks)
|
||||||
chunk_path = get_chunk_name(base_path, i, num_chunks)
|
chunk_path = get_chunk_name(base_path, i, num_chunks)
|
||||||
chunk_downloaded = 0
|
chunk_downloaded = 0
|
||||||
@@ -113,7 +117,7 @@ class ModelManagerSP:
|
|||||||
if self.params.get("ModelManager_DownloadIndex") is None:
|
if self.params.get("ModelManager_DownloadIndex") is None:
|
||||||
raise Exception("Download cancelled")
|
raise Exception("Download cancelled")
|
||||||
intra = chunk_downloaded / max(chunk_size, 1)
|
intra = chunk_downloaded / max(chunk_size, 1)
|
||||||
progress = min(99.0, ((i + intra) / num_chunks) * 100)
|
progress = min(99, (i + intra) / num_chunks * 100)
|
||||||
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloading
|
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloading
|
||||||
artifact.downloadProgress.progress = progress
|
artifact.downloadProgress.progress = progress
|
||||||
artifact.downloadProgress.eta = self._calculate_eta(artifact.fileName, progress)
|
artifact.downloadProgress.eta = self._calculate_eta(artifact.fileName, progress)
|
||||||
@@ -144,9 +148,9 @@ class ModelManagerSP:
|
|||||||
self._report_status()
|
self._report_status()
|
||||||
return
|
return
|
||||||
|
|
||||||
if len(artifact.chunks) > 0:
|
try:
|
||||||
await self._download_chunked(url, full_path, artifact)
|
await self._download_chunked(url, full_path, artifact)
|
||||||
else:
|
except (FileNotFoundError, aiohttp.ClientResponseError):
|
||||||
await self._download_file(url, full_path, artifact)
|
await self._download_file(url, full_path, artifact)
|
||||||
|
|
||||||
if not await verify_file(full_path, expected_hash):
|
if not await verify_file(full_path, expected_hash):
|
||||||
@@ -166,15 +170,18 @@ class ModelManagerSP:
|
|||||||
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.failed
|
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.failed
|
||||||
artifact.downloadProgress.eta = 0
|
artifact.downloadProgress.eta = 0
|
||||||
self._sync_artifact_progress(artifact)
|
self._sync_artifact_progress(artifact)
|
||||||
if self.selected_bundle:
|
self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.failed
|
||||||
self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.failed
|
|
||||||
self._report_status()
|
self._report_status()
|
||||||
self._download_start_times.pop(artifact.fileName, None)
|
self._download_start_times.pop(artifact.fileName, None)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def _process_model(self, model, destination_path: str) -> None:
|
async def _process_model(self, model, destination_path: str) -> None:
|
||||||
"""Processes a single model download including verification"""
|
"""Processes a single model download including verification"""
|
||||||
await self._process_artifact(model.artifact, destination_path)
|
model_artifact = model.artifact
|
||||||
|
metadata_artifact = model.metadata
|
||||||
|
|
||||||
|
await self._process_artifact(metadata_artifact, destination_path)
|
||||||
|
await self._process_artifact(model_artifact, destination_path)
|
||||||
|
|
||||||
def _report_status(self) -> None:
|
def _report_status(self) -> None:
|
||||||
"""Reports current status through messaging system"""
|
"""Reports current status through messaging system"""
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
from openpilot.sunnypilot.models.helpers import get_active_bundle
|
||||||
|
from openpilot.sunnypilot.models.runners.model_runner import ModelRunner
|
||||||
|
from openpilot.sunnypilot.models.runners.tinygrad.tinygrad_runner import TinygradRunner, TinygradSplitRunner
|
||||||
|
from openpilot.sunnypilot.models.runners.constants import ModelType
|
||||||
|
|
||||||
|
|
||||||
|
def get_model_runner() -> ModelRunner:
|
||||||
|
"""
|
||||||
|
Factory function to create and return the appropriate ModelRunner instance.
|
||||||
|
|
||||||
|
Selects TinygradRunner, choosing TinygradSplitRunner if separate vision/policy
|
||||||
|
models are detected in the active bundle.
|
||||||
|
|
||||||
|
:return: An instance of a ModelRunner subclass (ONNXRunner, TinygradRunner, or TinygradSplitRunner).
|
||||||
|
"""
|
||||||
|
bundle = get_active_bundle()
|
||||||
|
if bundle and bundle.models:
|
||||||
|
model_types = {m.type.raw for m in bundle.models}
|
||||||
|
# Check if the bundle uses separate vision and policy models (legacy or new split format)
|
||||||
|
split_types = {ModelType.vision, ModelType.policy, ModelType.offPolicy, ModelType.onPolicy}
|
||||||
|
if model_types & split_types:
|
||||||
|
return TinygradSplitRunner()
|
||||||
|
# Otherwise, assume a single model (likely supercombo)
|
||||||
|
if bundle.models:
|
||||||
|
return TinygradRunner(bundle.models[0].type.raw)
|
||||||
|
|
||||||
|
# Default fallback to TinygradRunner with the supercombo type if bundle info is missing/incomplete
|
||||||
|
return TinygradRunner(ModelType.supercombo)
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
from abc import abstractmethod, ABC
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from openpilot.sunnypilot.models.helpers import get_active_bundle
|
||||||
|
from openpilot.sunnypilot.models.runners.constants import NumpyDict, ShapeDict, Model, SliceDict, SEND_RAW_PRED
|
||||||
|
from openpilot.common.hardware.hw import Paths
|
||||||
|
import pickle
|
||||||
|
|
||||||
|
CUSTOM_MODEL_PATH = Paths.model_root()
|
||||||
|
|
||||||
|
|
||||||
|
class ModelData:
|
||||||
|
"""
|
||||||
|
Stores metadata and configuration for a specific machine learning model.
|
||||||
|
|
||||||
|
This class loads model metadata (like input shapes and output slices)
|
||||||
|
from a pickle file associated with a model instance.
|
||||||
|
|
||||||
|
:param model: The machine learning model object containing metadata.
|
||||||
|
"""
|
||||||
|
def __init__(self, model: Model):
|
||||||
|
self.model = model
|
||||||
|
self.metadata = model.metadata
|
||||||
|
self.input_shapes: ShapeDict = {}
|
||||||
|
self.output_slices: SliceDict = {}
|
||||||
|
if self.metadata:
|
||||||
|
self._load_metadata()
|
||||||
|
|
||||||
|
def _load_metadata(self) -> None:
|
||||||
|
"""Loads input shapes and output slices from the model's metadata pickle file."""
|
||||||
|
metadata_path = f"{CUSTOM_MODEL_PATH}/{self.metadata.fileName}"
|
||||||
|
with open(metadata_path, 'rb') as f:
|
||||||
|
model_metadata = pickle.load(f)
|
||||||
|
self.input_shapes = model_metadata.get('input_shapes', {})
|
||||||
|
self.output_slices = model_metadata.get('output_slices', {})
|
||||||
|
|
||||||
|
|
||||||
|
class ModularRunner(ABC):
|
||||||
|
"""
|
||||||
|
Represents a modular runner for handling and slicing model outputs.
|
||||||
|
|
||||||
|
This abstract base class is designed to provide an interface for modular
|
||||||
|
parsing and processing of model outputs. Classes inheriting from it must
|
||||||
|
implement the specified abstract methods, defining how model outputs
|
||||||
|
should be handled and stored. The primary goal is to enable structured
|
||||||
|
parsing of outputs through a dictionary-based method mapping.
|
||||||
|
|
||||||
|
:ivar parser_method_dict: Mapping dictionary containing parser methods
|
||||||
|
for handling specific types of outputs.
|
||||||
|
:type parser_method_dict: dict
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def parser_method_dict(self) -> dict:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@parser_method_dict.setter
|
||||||
|
@abstractmethod
|
||||||
|
def parser_method_dict(self, value: dict) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _slice_outputs(self, model_outputs: np.ndarray) -> NumpyDict:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ModelRunner(ModularRunner):
|
||||||
|
"""
|
||||||
|
Abstract base class for managing and executing machine learning models.
|
||||||
|
|
||||||
|
Provides a common interface for loading models, preparing inputs, running
|
||||||
|
inference, and slicing/parsing outputs based on model metadata. Derived
|
||||||
|
classes implement the specifics of input preparation and model execution
|
||||||
|
for different frameworks (e.g., Tinygrad, ONNX).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initializes the model runner, loading the active model bundle."""
|
||||||
|
self.is_20hz: bool | None = None
|
||||||
|
self.is_20hz_3d: bool | None = None
|
||||||
|
self.models: dict[int, ModelData] = {}
|
||||||
|
self._model_data: ModelData | None = None # Active model data for current operation
|
||||||
|
self._parser_method_dict: dict = {}
|
||||||
|
self.inputs: dict = {}
|
||||||
|
self._parser = None
|
||||||
|
self._load_models()
|
||||||
|
self._constants = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def constants(self):
|
||||||
|
return self._constants
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parser_method_dict(self) -> dict:
|
||||||
|
"""Returns the dictionary mapping model types to their respective parsing methods."""
|
||||||
|
return self._parser_method_dict
|
||||||
|
|
||||||
|
@parser_method_dict.setter
|
||||||
|
def parser_method_dict(self, value: dict) -> None:
|
||||||
|
"""Sets the dictionary mapping model types to their respective parsing methods."""
|
||||||
|
self._parser_method_dict = value
|
||||||
|
|
||||||
|
def _load_models(self) -> None:
|
||||||
|
"""Loads the active model bundle configuration and sets up ModelData."""
|
||||||
|
bundle = get_active_bundle()
|
||||||
|
if not bundle:
|
||||||
|
raise ValueError("No active model bundle found, why are we being executed?")
|
||||||
|
|
||||||
|
self.models = {model.type.raw: ModelData(model) for model in bundle.models}
|
||||||
|
self.is_20hz = bundle.is20hz
|
||||||
|
self.is_20hz_3d = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def input_shapes(self) -> ShapeDict:
|
||||||
|
"""Returns the input shapes for the currently active model."""
|
||||||
|
if self._model_data:
|
||||||
|
return self._model_data.input_shapes
|
||||||
|
raise ValueError("Model data is not available. Ensure the model is loaded correctly.")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def output_slices(self) -> SliceDict:
|
||||||
|
"""Returns the output slices for the currently active model."""
|
||||||
|
if self._model_data:
|
||||||
|
return self._model_data.output_slices
|
||||||
|
raise ValueError("Model data is not available. Ensure the model is loaded correctly.")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def vision_input_names(self) -> list[str]:
|
||||||
|
"""Returns the list of vision input names from the input shapes."""
|
||||||
|
if self._model_data:
|
||||||
|
return list(self._model_data.input_shapes.keys())
|
||||||
|
raise ValueError("Model data is not available. Ensure the model is loaded correctly.")
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def prepare_inputs(self, numpy_inputs: NumpyDict) -> dict:
|
||||||
|
"""
|
||||||
|
Abstract method to prepare inputs for model inference.
|
||||||
|
|
||||||
|
:param numpy_inputs: Dictionary of numpy arrays for non-image inputs.
|
||||||
|
:return: Dictionary of prepared inputs ready for the model.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _run_model(self) -> NumpyDict:
|
||||||
|
"""
|
||||||
|
Abstract method to execute model inference with prepared inputs.
|
||||||
|
|
||||||
|
:return: Dictionary containing the model's raw output arrays.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _slice_outputs(self, model_outputs: np.ndarray) -> NumpyDict:
|
||||||
|
"""
|
||||||
|
Slices the raw model output array based on the output_slices metadata.
|
||||||
|
|
||||||
|
:param model_outputs: The raw numpy array output from the model.
|
||||||
|
:return: A dictionary where keys are output names and values are sliced numpy arrays.
|
||||||
|
"""
|
||||||
|
if not self._model_data:
|
||||||
|
raise ValueError("Model data is not available. Ensure the model is loaded correctly.")
|
||||||
|
sliced_outputs = {k: model_outputs[np.newaxis, v] for k, v in self._model_data.output_slices.items()}
|
||||||
|
if SEND_RAW_PRED:
|
||||||
|
sliced_outputs['raw_pred'] = model_outputs.copy() # Optionally include the full raw output
|
||||||
|
return sliced_outputs
|
||||||
|
|
||||||
|
def run_model(self) -> NumpyDict:
|
||||||
|
"""
|
||||||
|
Executes the model inference pipeline: runs the model and parses outputs.
|
||||||
|
|
||||||
|
:return: Dictionary containing the final parsed model outputs.
|
||||||
|
"""
|
||||||
|
return self._run_model() # Parsing is handled within specific runner implementations
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import os
|
||||||
|
from abc import ABC
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from openpilot.sunnypilot.modeld_v2.parse_model_outputs import Parser as CombinedParser
|
||||||
|
from openpilot.sunnypilot.modeld_v2.parse_model_outputs_split import Parser as SplitParser
|
||||||
|
from openpilot.sunnypilot.models.runners.constants import ModelType, NumpyDict
|
||||||
|
from openpilot.sunnypilot.models.runners.model_runner import ModularRunner
|
||||||
|
from openpilot.common.hardware.hw import Paths
|
||||||
|
|
||||||
|
|
||||||
|
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
||||||
|
CUSTOM_MODEL_PATH = Paths.model_root()
|
||||||
|
|
||||||
|
|
||||||
|
class OffPolicyTinygrad(ModularRunner, ABC):
|
||||||
|
"""
|
||||||
|
A TinygradRunner specialized for off-policy models.
|
||||||
|
|
||||||
|
Uses a SplitParser to handle outputs specific to the off-policy part of a split model setup.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self._off_policy_parser = SplitParser()
|
||||||
|
self.parser_method_dict[ModelType.offPolicy] = self._parse_off_policy_outputs
|
||||||
|
|
||||||
|
def _parse_off_policy_outputs(self, model_outputs: np.ndarray) -> NumpyDict:
|
||||||
|
"""Parses off-policy model outputs using SplitParser."""
|
||||||
|
result: NumpyDict = self._off_policy_parser.parse_policy_outputs(self._slice_outputs(model_outputs))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class OnPolicyTinygrad(ModularRunner, ABC):
|
||||||
|
"""
|
||||||
|
A TinygradRunner specialized for on-policy models.
|
||||||
|
|
||||||
|
Uses a SplitParser to handle outputs specific to the on-policy part of a split model setup.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self._on_policy_parser = SplitParser()
|
||||||
|
self.parser_method_dict[ModelType.onPolicy] = self._parse_on_policy_outputs
|
||||||
|
|
||||||
|
def _parse_on_policy_outputs(self, model_outputs: np.ndarray) -> NumpyDict:
|
||||||
|
"""Parses on-policy model outputs using SplitParser."""
|
||||||
|
result: NumpyDict = self._on_policy_parser.parse_policy_outputs(self._slice_outputs(model_outputs))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class PolicyTinygrad(ModularRunner, ABC):
|
||||||
|
"""
|
||||||
|
A TinygradRunner specialized for policy-only models.
|
||||||
|
|
||||||
|
Uses a SplitParser to handle outputs specific to the policy part of a split model setup.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self._policy_parser = SplitParser()
|
||||||
|
self.parser_method_dict[ModelType.policy] = self._parse_policy_outputs
|
||||||
|
|
||||||
|
def _parse_policy_outputs(self, model_outputs: np.ndarray) -> NumpyDict:
|
||||||
|
"""Parses policy model outputs using SplitParser."""
|
||||||
|
result: NumpyDict = self._policy_parser.parse_policy_outputs(self._slice_outputs(model_outputs))
|
||||||
|
return result
|
||||||
|
|
||||||
|
class VisionTinygrad(ModularRunner, ABC):
|
||||||
|
"""
|
||||||
|
A TinygradRunner specialized for vision-only models.
|
||||||
|
|
||||||
|
Uses a SplitParser to handle outputs specific to the vision part of a split model setup.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self._vision_parser = SplitParser()
|
||||||
|
self.parser_method_dict[ModelType.vision] = self._parse_vision_outputs
|
||||||
|
|
||||||
|
def _parse_vision_outputs(self, model_outputs: np.ndarray) -> NumpyDict:
|
||||||
|
"""Parses vision model outputs using SplitParser."""
|
||||||
|
result: NumpyDict = self._vision_parser.parse_vision_outputs(self._slice_outputs(model_outputs))
|
||||||
|
return result
|
||||||
|
|
||||||
|
class SupercomboTinygrad(ModularRunner, ABC):
|
||||||
|
"""
|
||||||
|
A TinygradRunner specialized for vision-only models.
|
||||||
|
|
||||||
|
Uses a SplitParser to handle outputs specific to the vision part of a split model setup.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self._supercombo_parser = CombinedParser()
|
||||||
|
self.parser_method_dict[ModelType.supercombo] = self._parse_supercombo_outputs
|
||||||
|
|
||||||
|
def _parse_supercombo_outputs(self, model_outputs: np.ndarray) -> NumpyDict:
|
||||||
|
"""Parses vision model outputs using SplitParser."""
|
||||||
|
result: NumpyDict = self._supercombo_parser.parse_outputs(self._slice_outputs(model_outputs))
|
||||||
|
return result
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import pickle
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from openpilot.sunnypilot.models.runners.constants import 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, OffPolicyTinygrad, OnPolicyTinygrad
|
||||||
|
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, OffPolicyTinygrad, OnPolicyTinygrad):
|
||||||
|
"""
|
||||||
|
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)
|
||||||
|
OffPolicyTinygrad.__init__(self)
|
||||||
|
OnPolicyTinygrad.__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_input_info[idx]
|
||||||
|
self.input_to_dtype[name] = info[2] # dtype
|
||||||
|
self.input_to_device[name] = info[3] # device
|
||||||
|
self._policy_cached = False
|
||||||
|
|
||||||
|
@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_policy_inputs(self, numpy_inputs: NumpyDict):
|
||||||
|
if not self._policy_cached:
|
||||||
|
for key, value in numpy_inputs.items():
|
||||||
|
self.inputs[key] = Tensor(value, device='NPY').realize()
|
||||||
|
self._policy_cached = True
|
||||||
|
|
||||||
|
def prepare_inputs(self, numpy_inputs: NumpyDict) -> dict:
|
||||||
|
"""Prepares all vision and policy inputs for the model."""
|
||||||
|
self.prepare_policy_inputs(numpy_inputs)
|
||||||
|
for key in self.vision_input_names:
|
||||||
|
if key in self.inputs:
|
||||||
|
self.inputs[key] = self.inputs[key].cast(self.input_to_dtype[key])
|
||||||
|
return self.inputs
|
||||||
|
|
||||||
|
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().flatten()
|
||||||
|
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) if self.models.get(ModelType.policy) else None
|
||||||
|
self.off_policy_runner = TinygradRunner(ModelType.offPolicy) if self.models.get(ModelType.offPolicy) else None
|
||||||
|
self.on_policy_runner = TinygradRunner(ModelType.onPolicy) if self.models.get(ModelType.onPolicy) else None
|
||||||
|
self._constants = SplitModelConstants
|
||||||
|
|
||||||
|
def _run_model(self) -> NumpyDict:
|
||||||
|
"""Runs both vision and policy models and merges their parsed outputs."""
|
||||||
|
vision_output = self.vision_runner.run_model()
|
||||||
|
outputs = {**vision_output}
|
||||||
|
|
||||||
|
if self.policy_runner:
|
||||||
|
policy_output = self.policy_runner.run_model()
|
||||||
|
outputs.update(policy_output)
|
||||||
|
|
||||||
|
if self.off_policy_runner:
|
||||||
|
off_policy_output = self.off_policy_runner.run_model()
|
||||||
|
if self.on_policy_runner:
|
||||||
|
off_policy_output.pop('plan', None)
|
||||||
|
outputs.update(off_policy_output)
|
||||||
|
|
||||||
|
if self.on_policy_runner:
|
||||||
|
on_policy_output = self.on_policy_runner.run_model()
|
||||||
|
outputs.update(on_policy_output)
|
||||||
|
|
||||||
|
if 'planplus' in outputs and 'plan' in outputs:
|
||||||
|
outputs['plan'] = outputs['plan'] + outputs['planplus']
|
||||||
|
|
||||||
|
return outputs
|
||||||
|
|
||||||
|
@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."""
|
||||||
|
shapes = {**self.vision_runner.input_shapes}
|
||||||
|
if self.policy_runner:
|
||||||
|
shapes.update(self.policy_runner.input_shapes)
|
||||||
|
if self.off_policy_runner:
|
||||||
|
shapes.update(self.off_policy_runner.input_shapes)
|
||||||
|
if self.on_policy_runner:
|
||||||
|
shapes.update(self.on_policy_runner.input_shapes)
|
||||||
|
return shapes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def output_slices(self) -> SliceDict:
|
||||||
|
"""Returns the combined output slices from both vision and policy models."""
|
||||||
|
slices = {**self.vision_runner.output_slices}
|
||||||
|
if self.policy_runner:
|
||||||
|
slices.update(self.policy_runner.output_slices)
|
||||||
|
if self.off_policy_runner:
|
||||||
|
slices.update(self.off_policy_runner.output_slices)
|
||||||
|
if self.on_policy_runner:
|
||||||
|
slices.update(self.on_policy_runner.output_slices)
|
||||||
|
return slices
|
||||||
|
|
||||||
|
def prepare_inputs(self, numpy_inputs: NumpyDict) -> dict:
|
||||||
|
"""Prepares inputs for both vision and policy models."""
|
||||||
|
if self.policy_runner:
|
||||||
|
self.policy_runner.prepare_policy_inputs(numpy_inputs)
|
||||||
|
|
||||||
|
for key in self.vision_input_names:
|
||||||
|
if key in self.inputs:
|
||||||
|
self.vision_runner.inputs[key] = self.inputs[key].cast(self.vision_runner.input_to_dtype[key])
|
||||||
|
|
||||||
|
inputs = {**self.vision_runner.inputs}
|
||||||
|
if self.policy_runner:
|
||||||
|
inputs.update(self.policy_runner.inputs)
|
||||||
|
|
||||||
|
if self.off_policy_runner:
|
||||||
|
self.off_policy_runner.prepare_policy_inputs(numpy_inputs)
|
||||||
|
inputs.update(self.off_policy_runner.inputs)
|
||||||
|
if self.on_policy_runner:
|
||||||
|
self.on_policy_runner.prepare_policy_inputs(numpy_inputs)
|
||||||
|
inputs.update(self.on_policy_runner.inputs)
|
||||||
|
return inputs
|
||||||
@@ -43,7 +43,6 @@ class SplitModelConstants:
|
|||||||
LANE_LINES_WIDTH = 2
|
LANE_LINES_WIDTH = 2
|
||||||
ROAD_EDGES_WIDTH = 2
|
ROAD_EDGES_WIDTH = 2
|
||||||
PLAN_WIDTH = 15
|
PLAN_WIDTH = 15
|
||||||
ACTION_WIDTH = 2
|
|
||||||
DESIRE_PRED_WIDTH = 8
|
DESIRE_PRED_WIDTH = 8
|
||||||
LAT_PLANNER_SOLUTION_WIDTH = 4
|
LAT_PLANNER_SOLUTION_WIDTH = 4
|
||||||
DESIRED_CURV_WIDTH = 1
|
DESIRED_CURV_WIDTH = 1
|
||||||
|
|||||||
@@ -151,8 +151,8 @@ class SmartCruiseControlMap:
|
|||||||
a = 0.5 * TARGET_JERK
|
a = 0.5 * TARGET_JERK
|
||||||
b = self.a_ego
|
b = self.a_ego
|
||||||
c = self.v_ego - tv
|
c = self.v_ego - tv
|
||||||
t_a = -1 * ((b**2 - 4 * a * c) ** 0.5 + b) / (2 * a)
|
t_a = -1 * ((b**2 - 4 * a * c) ** 0.5 + b) / 2 * a
|
||||||
t_b = ((b**2 - 4 * a * c) ** 0.5 - b) / (2 * a)
|
t_b = ((b**2 - 4 * a * c) ** 0.5 - b) / 2 * a
|
||||||
if not isinstance(t_a, complex) and t_a > 0:
|
if not isinstance(t_a, complex) and t_a > 0:
|
||||||
t = t_a
|
t = t_a
|
||||||
else:
|
else:
|
||||||
|
|||||||
+1
-18
@@ -4,17 +4,13 @@ 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.
|
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.
|
See the LICENSE.md file in the root directory for more details.
|
||||||
"""
|
"""
|
||||||
import json
|
|
||||||
import math
|
|
||||||
import platform
|
import platform
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from openpilot.cereal import custom
|
from openpilot.cereal import custom
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from openpilot.common.realtime import DT_MDL
|
from openpilot.common.realtime import DT_MDL
|
||||||
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
|
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.map_controller import R, SmartCruiseControlMap
|
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.map_controller import SmartCruiseControlMap
|
||||||
|
|
||||||
MapState = VisionState = custom.LongitudinalPlanSP.SmartCruiseControl.MapState
|
MapState = VisionState = custom.LongitudinalPlanSP.SmartCruiseControl.MapState
|
||||||
|
|
||||||
@@ -59,17 +55,4 @@ class TestSmartCruiseControlMap:
|
|||||||
self.scc_m.update(True, False, 0., 0., 0.)
|
self.scc_m.update(True, False, 0., 0., 0.)
|
||||||
assert self.scc_m.state == VisionState.enabled
|
assert self.scc_m.state == VisionState.enabled
|
||||||
|
|
||||||
def test_moderate_curve(self):
|
|
||||||
# Regression: `... / 2 * a` parsed as `(.../2)*a` instead of `.../(2*a)`,
|
|
||||||
# making max_d ~11x too small so the moderate-curve branch never tripped.
|
|
||||||
# v_ego=25, a_ego=0, tv=24: fixed max_d≈45m vs buggy ≈4m at a 40m waypoint.
|
|
||||||
waypoint_lon_deg = (40.0 / R) * (180.0 / math.pi)
|
|
||||||
self.mem_params.put("LastGPSPosition", json.dumps({"latitude": 0.0, "longitude": 0.0}), block=True)
|
|
||||||
self.mem_params.put("MapTargetVelocities",
|
|
||||||
json.dumps([{"latitude": 0.0, "longitude": waypoint_lon_deg, "velocity": 24.0}]), block=True)
|
|
||||||
|
|
||||||
self.scc_m.update(True, False, 25.0, 0.0, 30.0)
|
|
||||||
|
|
||||||
assert self.scc_m.v_target == pytest.approx(24.0)
|
|
||||||
|
|
||||||
# TODO-SP: mock data from modelV2 to test other states
|
# TODO-SP: mock data from modelV2 to test other states
|
||||||
|
|||||||
@@ -95,6 +95,9 @@ def is_stock_model(started, params, CP: car.CarParams) -> bool:
|
|||||||
"""Check if the active model runner is stock."""
|
"""Check if the active model runner is stock."""
|
||||||
return bool(get_active_model_runner(params, not started) == custom.ModelManagerSP.Runner.stock)
|
return bool(get_active_model_runner(params, not started) == custom.ModelManagerSP.Runner.stock)
|
||||||
|
|
||||||
|
def not_wgpu(started: bool, params: Params, CP: car.CarParams) -> bool:
|
||||||
|
return not params.get_bool("WgpuEnabled")
|
||||||
|
|
||||||
def mapd_ready(started: bool, params: Params, CP: car.CarParams) -> bool:
|
def mapd_ready(started: bool, params: Params, CP: car.CarParams) -> bool:
|
||||||
return bool(os.path.exists(Paths.mapd_root()))
|
return bool(os.path.exists(Paths.mapd_root()))
|
||||||
|
|
||||||
@@ -128,7 +131,7 @@ procs = [
|
|||||||
PythonProcess("micd", "openpilot.system.micd", iscar),
|
PythonProcess("micd", "openpilot.system.micd", iscar),
|
||||||
PythonProcess("timed", "openpilot.system.timed", always_run, enabled=not PC),
|
PythonProcess("timed", "openpilot.system.timed", always_run, enabled=not PC),
|
||||||
|
|
||||||
PythonProcess("modeld", "openpilot.selfdrive.modeld.modeld", and_(only_onroad, is_stock_model)),
|
PythonProcess("modeld", "openpilot.selfdrive.modeld.modeld", and_(and_(only_onroad, is_stock_model), not_wgpu)),
|
||||||
PythonProcess("dmonitoringmodeld", "openpilot.selfdrive.modeld.dmonitoringmodeld", driverview, enabled=(WEBCAM or not PC)),
|
PythonProcess("dmonitoringmodeld", "openpilot.selfdrive.modeld.dmonitoringmodeld", driverview, enabled=(WEBCAM or not PC)),
|
||||||
|
|
||||||
PythonProcess("sensord", "openpilot.system.sensord.sensord", only_onroad, enabled=not PC),
|
PythonProcess("sensord", "openpilot.system.sensord.sensord", only_onroad, enabled=not PC),
|
||||||
@@ -177,7 +180,7 @@ procs = [
|
|||||||
procs += [
|
procs += [
|
||||||
# Models
|
# Models
|
||||||
PythonProcess("models_manager", "openpilot.sunnypilot.models.manager", only_offroad),
|
PythonProcess("models_manager", "openpilot.sunnypilot.models.manager", only_offroad),
|
||||||
NativeProcess("modeld_tinygrad", "openpilot/sunnypilot/modeld_v2", ["./modeld"], and_(only_onroad, is_tinygrad_model)),
|
NativeProcess("modeld_tinygrad", "openpilot/sunnypilot/modeld_v2", ["./modeld"], and_(and_(only_onroad, is_tinygrad_model), not_wgpu)),
|
||||||
|
|
||||||
# Backup
|
# Backup
|
||||||
PythonProcess("backup_manager", "openpilot.sunnypilot.sunnylink.backups.manager", and_(only_offroad, sunnylink_ready_shim)),
|
PythonProcess("backup_manager", "openpilot.sunnypilot.sunnylink.backups.manager", and_(only_offroad, sunnylink_ready_shim)),
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import os
|
|
||||||
import argparse
|
import argparse
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import time
|
import time
|
||||||
@@ -10,6 +9,7 @@ from collections import deque
|
|||||||
import openpilot.cereal.messaging as messaging
|
import openpilot.cereal.messaging as messaging
|
||||||
from msgq.visionipc import VisionIpcServer, VisionStreamType
|
from msgq.visionipc import VisionIpcServer, VisionStreamType
|
||||||
from openpilot.tools.camerastream.ffmpeg_decoder import Decoder, FFmpegError
|
from openpilot.tools.camerastream.ffmpeg_decoder import Decoder, FFmpegError
|
||||||
|
from openpilot.tools.wgpu.zmq import ZmqSubMaster, ZmqSubSocket
|
||||||
|
|
||||||
V4L2_BUF_FLAG_KEYFRAME = 8
|
V4L2_BUF_FLAG_KEYFRAME = 8
|
||||||
|
|
||||||
@@ -30,10 +30,7 @@ def decoder(addr, vipc_server, vst, W, H, debug=False):
|
|||||||
|
|
||||||
codec = Decoder("hevc")
|
codec = Decoder("hevc")
|
||||||
|
|
||||||
os.environ["ZMQ"] = "1"
|
sock = ZmqSubSocket(sock_name, addr)
|
||||||
messaging.reset_context()
|
|
||||||
sock = messaging.sub_sock(sock_name, None, addr=addr, conflate=False)
|
|
||||||
cnt = 0
|
|
||||||
last_idx = -1
|
last_idx = -1
|
||||||
seen_iframe = False
|
seen_iframe = False
|
||||||
|
|
||||||
@@ -46,8 +43,9 @@ def decoder(addr, vipc_server, vst, W, H, debug=False):
|
|||||||
time_q.clear()
|
time_q.clear()
|
||||||
|
|
||||||
while 1:
|
while 1:
|
||||||
msgs = messaging.drain_sock(sock, wait_for_one=True)
|
msgs = sock.drain(wait_for_one=True)
|
||||||
for evt in msgs:
|
for raw in msgs:
|
||||||
|
evt = messaging.log_from_bytes(raw)
|
||||||
evta = getattr(evt, evt.which())
|
evta = getattr(evt, evt.which())
|
||||||
if last_idx != -1 and evta.idx.encodeId != (last_idx + 1):
|
if last_idx != -1 and evta.idx.encodeId != (last_idx + 1):
|
||||||
if debug:
|
if debug:
|
||||||
@@ -94,8 +92,9 @@ def decoder(addr, vipc_server, vst, W, H, debug=False):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
frame_start_time = time_q.popleft()
|
frame_start_time = time_q.popleft()
|
||||||
vipc_server.send(vst, img_yuv.data, cnt, int(frame_start_time*1e9), int(time.monotonic()*1e9))
|
# Preserve the device camera metadata so remote model outputs line up with
|
||||||
cnt += 1
|
# the rest of the device's cereal timeline.
|
||||||
|
vipc_server.send(vst, img_yuv.data, evta.idx.frameId, evta.idx.timestampSof, evta.idx.timestampEof)
|
||||||
|
|
||||||
pc_latency = (time.monotonic()-frame_start_time)*1000
|
pc_latency = (time.monotonic()-frame_start_time)*1000
|
||||||
if debug:
|
if debug:
|
||||||
@@ -105,25 +104,30 @@ def decoder(addr, vipc_server, vst, W, H, debug=False):
|
|||||||
|
|
||||||
class CompressedVipc:
|
class CompressedVipc:
|
||||||
def __init__(self, addr, vision_streams, server_name, debug=False):
|
def __init__(self, addr, vision_streams, server_name, debug=False):
|
||||||
print("getting frame sizes")
|
print("waiting for remote camera stream metadata", flush=True)
|
||||||
os.environ["ZMQ"] = "1"
|
sm = ZmqSubMaster([ENCODE_SOCKETS[s] for s in vision_streams], addr)
|
||||||
messaging.reset_context()
|
|
||||||
sm = messaging.SubMaster([ENCODE_SOCKETS[s] for s in vision_streams], addr=addr)
|
|
||||||
while min(sm.recv_frame.values()) == 0:
|
while min(sm.recv_frame.values()) == 0:
|
||||||
sm.update(100)
|
sm.update(100)
|
||||||
os.environ.pop("ZMQ")
|
|
||||||
messaging.reset_context()
|
stream_dimensions = {
|
||||||
|
vst: (sm[ENCODE_SOCKETS[vst]].width, sm[ENCODE_SOCKETS[vst]].height)
|
||||||
|
for vst in vision_streams
|
||||||
|
}
|
||||||
|
# The metadata subscribers are setup-only. Leaving them connected creates a
|
||||||
|
# second unread camera subscription whose TCP queues grow for the entire run.
|
||||||
|
sm.close()
|
||||||
|
|
||||||
self.vipc_server = VisionIpcServer(server_name)
|
self.vipc_server = VisionIpcServer(server_name)
|
||||||
for vst in vision_streams:
|
for vst in vision_streams:
|
||||||
ed = sm[ENCODE_SOCKETS[vst]]
|
width, height = stream_dimensions[vst]
|
||||||
self.vipc_server.create_buffers(vst, 4, ed.width, ed.height)
|
self.vipc_server.create_buffers(vst, 4, width, height)
|
||||||
self.vipc_server.start_listener()
|
self.vipc_server.start_listener()
|
||||||
|
|
||||||
self.procs = []
|
self.procs = []
|
||||||
|
process_context = multiprocessing.get_context("fork")
|
||||||
for vst in vision_streams:
|
for vst in vision_streams:
|
||||||
ed = sm[ENCODE_SOCKETS[vst]]
|
width, height = stream_dimensions[vst]
|
||||||
p = multiprocessing.Process(target=decoder, args=(addr, self.vipc_server, vst, ed.width, ed.height, debug))
|
p = process_context.Process(target=decoder, args=(addr, self.vipc_server, vst, width, height, debug))
|
||||||
p.start()
|
p.start()
|
||||||
self.procs.append(p)
|
self.procs.append(p)
|
||||||
|
|
||||||
|
|||||||
@@ -46,10 +46,23 @@ def _bind(fn, restype, *argtypes):
|
|||||||
return fn
|
return fn
|
||||||
|
|
||||||
|
|
||||||
|
def _library_path(name: str, major: int) -> str:
|
||||||
|
candidates = (
|
||||||
|
f"lib{name}.so.{major}",
|
||||||
|
f"lib{name}.{major}.dylib",
|
||||||
|
f"lib{name}.dylib",
|
||||||
|
)
|
||||||
|
for candidate in candidates:
|
||||||
|
path = os.path.join(ffmpeg.LIB_DIR, candidate)
|
||||||
|
if os.path.isfile(path):
|
||||||
|
return path
|
||||||
|
raise FileNotFoundError(f"FFmpeg library not found in {ffmpeg.LIB_DIR}: {', '.join(candidates)}")
|
||||||
|
|
||||||
|
|
||||||
def _load_libraries():
|
def _load_libraries():
|
||||||
avutil = ctypes.CDLL(os.path.join(ffmpeg.LIB_DIR, "libavutil.so.59"), mode=ctypes.RTLD_GLOBAL)
|
avutil = ctypes.CDLL(_library_path("avutil", 59), mode=ctypes.RTLD_GLOBAL)
|
||||||
avcodec = ctypes.CDLL(os.path.join(ffmpeg.LIB_DIR, "libavcodec.so.61"), mode=ctypes.RTLD_GLOBAL)
|
avcodec = ctypes.CDLL(_library_path("avcodec", 61), mode=ctypes.RTLD_GLOBAL)
|
||||||
swscale = ctypes.CDLL(os.path.join(ffmpeg.LIB_DIR, "libswscale.so.8"), mode=ctypes.RTLD_GLOBAL)
|
swscale = ctypes.CDLL(_library_path("swscale", 8), mode=ctypes.RTLD_GLOBAL)
|
||||||
|
|
||||||
c_int, c_char_p, c_void_p, c_size_t = ctypes.c_int, ctypes.c_char_p, ctypes.c_void_p, ctypes.c_size_t
|
c_int, c_char_p, c_void_p, c_size_t = ctypes.c_int, ctypes.c_char_p, ctypes.c_void_p, ctypes.c_size_t
|
||||||
c_uint8_p = ctypes.POINTER(ctypes.c_uint8)
|
c_uint8_p = ctypes.POINTER(ctypes.c_uint8)
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# Wireless modeld proof of concept
|
||||||
|
|
||||||
|
This runs driving `modeld` on a laptop and returns its cereal outputs to a comma
|
||||||
|
device over the existing Wi-Fi network. It reuses the existing HEVC camera
|
||||||
|
stream, VisionIPC decoder, and cereal ZMQ bridge.
|
||||||
|
|
||||||
|
This is for controlled bench testing only. Wi-Fi has no deterministic latency
|
||||||
|
or availability guarantee. The device-side helper switches only after receiving
|
||||||
|
a fresh remote model and after manager has stopped the local model publisher.
|
||||||
|
It restores local `modeld` if the remote model is missing for one second.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Use the same commit on the laptop and comma device. Build the cereal bridge on
|
||||||
|
the device:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
scons -u openpilot/cereal/messaging/bridge
|
||||||
|
```
|
||||||
|
|
||||||
|
Build and test the normal model on the laptop first:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
PATH="$PWD/.venv/bin:$PATH" scons -u
|
||||||
|
```
|
||||||
|
|
||||||
|
To compile the big external-GPU model for the laptop's local tinygrad backend:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
PATH="$PWD/.venv/bin:$PATH" WGPU=1 scons -u \
|
||||||
|
openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl.chunkmanifest
|
||||||
|
```
|
||||||
|
|
||||||
|
On macOS, the build selects tinygrad's Metal backend when it is available.
|
||||||
|
On an 8-GPU-core M5 MacBook Air, the small model's compiled policy pass measured
|
||||||
|
about 6–11 ms, while the big model measured about 79–81 ms. The latter already
|
||||||
|
misses the 50 ms model cadence before network and codec latency, so start with
|
||||||
|
the small model on that class of laptop.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
Find the laptop's LAN IP address that the comma device can reach. The helper can
|
||||||
|
start while onroad: it forwards camera/state while local `modeld` remains active,
|
||||||
|
then performs an exclusive publisher handoff after the laptop produces a fresh
|
||||||
|
valid model:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd /data/openpilot
|
||||||
|
python3 -m openpilot.tools.wgpu.device LAPTOP_IP
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep that terminal open. On the laptop, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd /path/to/openpilot
|
||||||
|
python3 -m openpilot.tools.wgpu.host COMMA_IP
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `--big-model` after `COMMA_IP` to use the locally compiled big model.
|
||||||
|
|
||||||
|
The device helper sends cached `carParams` over a dedicated startup channel, so
|
||||||
|
the host does not attach to the camera streams and accumulate stale frames while
|
||||||
|
waiting for the periodic state bridge. Stop either side with Ctrl+C. A host
|
||||||
|
disconnect automatically stops remote publication and restores local `modeld`
|
||||||
|
after a one-second timeout; the device helper then stays running and waits for
|
||||||
|
the next host session. Stop the device helper itself with Ctrl+C before changing
|
||||||
|
branches or rebooting.
|
||||||
|
|
||||||
|
While WGPU is active, model lag does not create an engagement-blocking alert.
|
||||||
|
The fixed-column diagnostics panel at the top-right of the mici onroad UI shows
|
||||||
|
the active source (`LOCAL` or `WGPU`), remote model size (`SMALL` or `BIG`),
|
||||||
|
total model-frame age, execution time, remaining I/O and queue time, and dropped
|
||||||
|
frames. The home GPU icon is gray while the device helper is ready and waiting,
|
||||||
|
and green while a valid remote model is active.
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import openpilot.cereal.messaging as messaging
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
from openpilot.tools.wgpu.zmq import WGPU_CAR_PARAMS, ZmqPubMaster, ZmqSubSocket
|
||||||
|
|
||||||
|
|
||||||
|
MODEL_OUTPUTS = "modelV2,drivingModelData,cameraOdometry,modelDataV2SP"
|
||||||
|
MODEL_PROCESSES = {"modeld", "modeld_tinygrad"}
|
||||||
|
REMOTE_MODEL_TIMEOUT = 1.0
|
||||||
|
WGPU_STATUS = "wgpuStatus"
|
||||||
|
ROOT = Path(__file__).resolve().parents[3]
|
||||||
|
BRIDGE = ROOT / "openpilot/cereal/messaging/bridge"
|
||||||
|
|
||||||
|
|
||||||
|
def stop_process(proc: subprocess.Popen) -> None:
|
||||||
|
proc.terminate()
|
||||||
|
try:
|
||||||
|
proc.wait(timeout=5)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
proc.kill()
|
||||||
|
proc.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def handle_sigterm(*_) -> None:
|
||||||
|
raise KeyboardInterrupt
|
||||||
|
|
||||||
|
|
||||||
|
def receive_model(sock: ZmqSubSocket) -> tuple[bool, float]:
|
||||||
|
raw = sock.receive(non_blocking=True)
|
||||||
|
if raw is None:
|
||||||
|
return False, float("inf")
|
||||||
|
event = messaging.log_from_bytes(raw)
|
||||||
|
if event.which() != "modelV2" or not event.valid:
|
||||||
|
return True, float("inf")
|
||||||
|
model_age = (time.monotonic_ns() - event.modelV2.timestampEof) / 1e9
|
||||||
|
return True, model_age
|
||||||
|
|
||||||
|
|
||||||
|
def receive_model_name(sock: ZmqSubSocket) -> str | None:
|
||||||
|
raw = sock.receive(non_blocking=True)
|
||||||
|
if raw is None:
|
||||||
|
return None
|
||||||
|
model_name = raw.decode(errors="replace").upper()
|
||||||
|
return model_name if model_name in ("SMALL", "BIG") else None
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_local_modeld_stop(timeout: float = 10.) -> None:
|
||||||
|
sm = messaging.SubMaster(["managerState"])
|
||||||
|
deadline = time.monotonic() + timeout
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
sm.update(100)
|
||||||
|
if sm.seen["managerState"]:
|
||||||
|
running = {p.name for p in sm["managerState"].processes if p.running}
|
||||||
|
if not running.intersection(MODEL_PROCESSES):
|
||||||
|
return
|
||||||
|
raise RuntimeError("timed out waiting for local modeld publisher to stop")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="Route modeld traffic between this device and a wireless host.")
|
||||||
|
parser.add_argument("host", help="Laptop IP address reachable from this device")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not BRIDGE.is_file():
|
||||||
|
raise FileNotFoundError(f"build the cereal bridge first: {BRIDGE}")
|
||||||
|
|
||||||
|
params = Params()
|
||||||
|
forward: subprocess.Popen | None = None
|
||||||
|
reverse: subprocess.Popen | None = None
|
||||||
|
wgpu_enabled = False
|
||||||
|
try:
|
||||||
|
forward = subprocess.Popen([str(BRIDGE)])
|
||||||
|
params.put_bool("WgpuReady", True, block=True)
|
||||||
|
car_params_pub = ZmqPubMaster([WGPU_CAR_PARAMS])
|
||||||
|
remote_model = ZmqSubSocket("modelV2", args.host, conflate=True)
|
||||||
|
remote_status = ZmqSubSocket(WGPU_STATUS, args.host, conflate=True)
|
||||||
|
car_params = params.get("CarParams") or params.get("CarParamsPersistent")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# Keep local modeld publishing while a host connects and warms up.
|
||||||
|
print(f"forwarding camera/state to {args.host}; waiting for a fresh remote model")
|
||||||
|
model_name = None
|
||||||
|
while True:
|
||||||
|
if car_params is None:
|
||||||
|
car_params = params.get("CarParams") or params.get("CarParamsPersistent")
|
||||||
|
if car_params is not None:
|
||||||
|
# This dedicated conflated startup channel lets remote modeld obtain CP
|
||||||
|
# before it connects to VisionIPC and accumulates stale frame metadata.
|
||||||
|
car_params_pub.send_raw(WGPU_CAR_PARAMS, car_params)
|
||||||
|
received, model_age = receive_model(remote_model)
|
||||||
|
model_name = receive_model_name(remote_status) or model_name
|
||||||
|
if received and 0 <= model_age < REMOTE_MODEL_TIMEOUT and model_name is not None:
|
||||||
|
break
|
||||||
|
if forward.poll() is not None:
|
||||||
|
raise RuntimeError(f"forward bridge exited with status {forward.returncode}")
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
# Stop the local publisher before attaching the reverse bridge. msgq permits
|
||||||
|
# only one publisher for each model service.
|
||||||
|
params.put("WgpuModelName", model_name, block=True)
|
||||||
|
params.put_bool("WgpuEnabled", True, block=True)
|
||||||
|
wgpu_enabled = True
|
||||||
|
wait_for_local_modeld_stop()
|
||||||
|
reverse = subprocess.Popen([str(BRIDGE), args.host, MODEL_OUTPUTS])
|
||||||
|
print("wgpu active; Ctrl+C or loss of the remote model restores local modeld")
|
||||||
|
|
||||||
|
last_remote_model = time.monotonic()
|
||||||
|
while True:
|
||||||
|
received, _ = receive_model(remote_model)
|
||||||
|
if received:
|
||||||
|
last_remote_model = time.monotonic()
|
||||||
|
if forward.poll() is not None:
|
||||||
|
raise RuntimeError(f"forward bridge exited with status {forward.returncode}")
|
||||||
|
if reverse.poll() is not None:
|
||||||
|
print(f"reverse bridge exited with status {reverse.returncode}; restoring local modeld")
|
||||||
|
break
|
||||||
|
if time.monotonic() - last_remote_model > REMOTE_MODEL_TIMEOUT:
|
||||||
|
print("remote model timed out; restoring local modeld")
|
||||||
|
break
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
stop_process(reverse)
|
||||||
|
reverse = None
|
||||||
|
params.put_bool("WgpuEnabled", False, block=True)
|
||||||
|
wgpu_enabled = False
|
||||||
|
params.remove("WgpuModelName")
|
||||||
|
print("wgpu disabled; local modeld restored; ready for the next host run")
|
||||||
|
finally:
|
||||||
|
# Stop remote publication before allowing the local publisher to restart.
|
||||||
|
if reverse is not None and reverse.poll() is None:
|
||||||
|
stop_process(reverse)
|
||||||
|
if wgpu_enabled:
|
||||||
|
params.put_bool("WgpuEnabled", False, block=True)
|
||||||
|
params.put_bool("WgpuReady", False, block=True)
|
||||||
|
params.remove("WgpuModelName")
|
||||||
|
if forward is not None and forward.poll() is None:
|
||||||
|
stop_process(forward)
|
||||||
|
print("wgpu disabled; local modeld restored")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
signal.signal(signal.SIGTERM, handle_sigterm)
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from openpilot.tools.wgpu.zmq import ZmqPubMaster
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[3]
|
||||||
|
CAMERASTREAM = ROOT / "openpilot/tools/camerastream/compressed_vipc.py"
|
||||||
|
WGPU_STATUS = "wgpuStatus"
|
||||||
|
|
||||||
|
|
||||||
|
def stop_process(proc: subprocess.Popen) -> None:
|
||||||
|
try:
|
||||||
|
os.killpg(proc.pid, signal.SIGTERM)
|
||||||
|
except ProcessLookupError:
|
||||||
|
proc.wait()
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
proc.wait(timeout=5)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
os.killpg(proc.pid, signal.SIGKILL)
|
||||||
|
proc.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="Run modeld on this host for a remote comma device.")
|
||||||
|
parser.add_argument("device", help="comma device hostname or IP address")
|
||||||
|
parser.add_argument("--big-model", action="store_true", help="use the locally compiled big driving model")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
camera = subprocess.Popen([sys.executable, str(CAMERASTREAM), args.device, "--cams", "0,2"], start_new_session=True)
|
||||||
|
model_args = [sys.executable, "-m", "openpilot.selfdrive.modeld.modeld", "--remote", args.device]
|
||||||
|
if args.big_model:
|
||||||
|
model_args.append("--big-model")
|
||||||
|
model = subprocess.Popen(model_args, cwd=ROOT, start_new_session=True)
|
||||||
|
status = ZmqPubMaster([WGPU_STATUS])
|
||||||
|
model_name = b"BIG" if args.big_model else b"SMALL"
|
||||||
|
|
||||||
|
procs = {"camera bridge": camera, "modeld": model}
|
||||||
|
try:
|
||||||
|
while all(proc.poll() is None for proc in procs.values()):
|
||||||
|
status.send_raw(WGPU_STATUS, model_name)
|
||||||
|
time.sleep(0.25)
|
||||||
|
failed_name, failed = next((name, proc) for name, proc in procs.items() if proc.poll() is not None)
|
||||||
|
raise RuntimeError(f"wgpu {failed_name} exited with status {failed.returncode}")
|
||||||
|
finally:
|
||||||
|
for proc in procs.values():
|
||||||
|
if proc.poll() is None:
|
||||||
|
stop_process(proc)
|
||||||
|
print("wgpu host stopped")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
import zmq
|
||||||
|
|
||||||
|
import openpilot.cereal.messaging as messaging
|
||||||
|
|
||||||
|
|
||||||
|
WGPU_CAR_PARAMS = "wgpuCarParams"
|
||||||
|
|
||||||
|
|
||||||
|
def service_port(endpoint: str) -> int:
|
||||||
|
# Keep this in sync with cereal/messaging/bridge_zmq.cc.
|
||||||
|
value = 0xcbf29ce484222325
|
||||||
|
for char in endpoint.encode():
|
||||||
|
value ^= char
|
||||||
|
value = (value * 0x100000001b3) & 0xffffffffffffffff
|
||||||
|
return 8023 + (value % (65535 - 8023))
|
||||||
|
|
||||||
|
|
||||||
|
class ZmqSubSocket:
|
||||||
|
def __init__(self, endpoint: str, address: str, conflate: bool = False):
|
||||||
|
self.context = zmq.Context()
|
||||||
|
self.socket = self.context.socket(zmq.SUB)
|
||||||
|
self.socket.setsockopt(zmq.SUBSCRIBE, b"")
|
||||||
|
self.socket.setsockopt(zmq.RECONNECT_IVL_MAX, 500)
|
||||||
|
if conflate:
|
||||||
|
self.socket.setsockopt(zmq.CONFLATE, 1)
|
||||||
|
self.socket.connect(f"tcp://{address}:{service_port(endpoint)}")
|
||||||
|
|
||||||
|
def receive(self, non_blocking: bool = False) -> bytes | None:
|
||||||
|
try:
|
||||||
|
return self.socket.recv(flags=zmq.NOBLOCK if non_blocking else 0)
|
||||||
|
except zmq.Again:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def drain(self, wait_for_one: bool = False) -> list[bytes]:
|
||||||
|
messages = []
|
||||||
|
if wait_for_one:
|
||||||
|
message = self.receive()
|
||||||
|
if message is not None:
|
||||||
|
messages.append(message)
|
||||||
|
while (message := self.receive(non_blocking=True)) is not None:
|
||||||
|
messages.append(message)
|
||||||
|
return messages
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
self.socket.close(linger=0)
|
||||||
|
self.context.term()
|
||||||
|
|
||||||
|
|
||||||
|
class ZmqSubMaster:
|
||||||
|
def __init__(self, services: list[str], address: str):
|
||||||
|
self.services = services
|
||||||
|
self.sockets = {service: ZmqSubSocket(service, address, conflate=True) for service in services}
|
||||||
|
self.poller = zmq.Poller()
|
||||||
|
self.socket_to_service = {}
|
||||||
|
for service, sub in self.sockets.items():
|
||||||
|
self.poller.register(sub.socket, zmq.POLLIN)
|
||||||
|
self.socket_to_service[sub.socket] = service
|
||||||
|
|
||||||
|
self.data = {service: getattr(messaging.new_message(service).as_reader(), service) for service in services}
|
||||||
|
self.seen = dict.fromkeys(services, False)
|
||||||
|
self.updated = dict.fromkeys(services, False)
|
||||||
|
self.recv_frame = dict.fromkeys(services, 0)
|
||||||
|
self.frame = -1
|
||||||
|
|
||||||
|
def __getitem__(self, service: str):
|
||||||
|
return self.data[service]
|
||||||
|
|
||||||
|
def update(self, timeout: int = 100) -> None:
|
||||||
|
self.frame += 1
|
||||||
|
self.updated = dict.fromkeys(self.services, False)
|
||||||
|
for socket, _ in self.poller.poll(timeout):
|
||||||
|
service = self.socket_to_service[socket]
|
||||||
|
raw = self.sockets[service].receive(non_blocking=True)
|
||||||
|
if raw is None:
|
||||||
|
continue
|
||||||
|
event = messaging.log_from_bytes(raw)
|
||||||
|
self.data[service] = getattr(event, service)
|
||||||
|
self.seen[service] = True
|
||||||
|
self.updated[service] = True
|
||||||
|
self.recv_frame[service] = self.frame
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
for sub in self.sockets.values():
|
||||||
|
self.poller.unregister(sub.socket)
|
||||||
|
sub.close()
|
||||||
|
|
||||||
|
|
||||||
|
class ZmqPubMaster:
|
||||||
|
def __init__(self, services: list[str]):
|
||||||
|
context = zmq.Context.instance()
|
||||||
|
self.sockets = {}
|
||||||
|
for service in services:
|
||||||
|
socket = context.socket(zmq.PUB)
|
||||||
|
socket.bind(f"tcp://*:{service_port(service)}")
|
||||||
|
self.sockets[service] = socket
|
||||||
|
# Give already-running bridge subscribers time to finish their handshake.
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
def send(self, service: str, message) -> None:
|
||||||
|
self.sockets[service].send(message.to_bytes(), flags=zmq.NOBLOCK)
|
||||||
|
|
||||||
|
def send_raw(self, service: str, data: bytes) -> None:
|
||||||
|
self.sockets[service].send(data, flags=zmq.NOBLOCK)
|
||||||
+109
-38
@@ -5,6 +5,8 @@ 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.
|
See the LICENSE.md file in the root directory for more details.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import pickle
|
||||||
import sys
|
import sys
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
@@ -12,8 +14,46 @@ import re
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime, UTC
|
from datetime import datetime, UTC
|
||||||
|
|
||||||
|
REQUIRED_OUTPUT_KEYS = frozenset({
|
||||||
|
"plan",
|
||||||
|
"lane_lines",
|
||||||
|
"road_edges",
|
||||||
|
"lead",
|
||||||
|
"desire_state",
|
||||||
|
"desire_pred",
|
||||||
|
"meta",
|
||||||
|
"lead_prob",
|
||||||
|
"lane_lines_prob",
|
||||||
|
"pose",
|
||||||
|
"wide_from_device_euler",
|
||||||
|
"road_transform",
|
||||||
|
"hidden_state",
|
||||||
|
})
|
||||||
|
OPTIONAL_OUTPUT_KEYS = frozenset({
|
||||||
|
"planplus",
|
||||||
|
"sim_pose",
|
||||||
|
"desired_curvature",
|
||||||
|
})
|
||||||
|
|
||||||
def create_short_name(full_name: str) -> str:
|
|
||||||
|
def validate_model_outputs(metadata_paths: list[Path]) -> None:
|
||||||
|
combined_keys: set[str] = set()
|
||||||
|
for path in metadata_paths:
|
||||||
|
if path.stat().st_size == 0:
|
||||||
|
print(f"skipping empty metadata: {path}")
|
||||||
|
continue
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
metadata = pickle.load(f)
|
||||||
|
combined_keys.update(metadata.get("output_slices", {}).keys())
|
||||||
|
missing = REQUIRED_OUTPUT_KEYS - combined_keys
|
||||||
|
if missing:
|
||||||
|
raise ValueError(f"Combined model metadata is missing required output keys: {sorted(missing)}")
|
||||||
|
detected_optional = sorted(OPTIONAL_OUTPUT_KEYS & combined_keys)
|
||||||
|
if detected_optional:
|
||||||
|
print(f"Optional output keys detected: {detected_optional}")
|
||||||
|
|
||||||
|
|
||||||
|
def create_short_name(full_name):
|
||||||
# Remove parentheses and extract alphanumeric words
|
# Remove parentheses and extract alphanumeric words
|
||||||
clean_name = re.sub(r'\([^)]*\)', '', full_name)
|
clean_name = re.sub(r'\([^)]*\)', '', full_name)
|
||||||
words = [re.sub(r'[^a-zA-Z0-9]', '', word) for word in clean_name.split() if re.sub(r'[^a-zA-Z0-9]', '', word)]
|
words = [re.sub(r'[^a-zA-Z0-9]', '', word) for word in clean_name.split() if re.sub(r'[^a-zA-Z0-9]', '', word)]
|
||||||
@@ -81,41 +121,49 @@ def _rename_pkl_with_chunks(old_pkl: Path, new_pkl: Path) -> Path:
|
|||||||
return old_pkl.rename(new_pkl)
|
return old_pkl.rename(new_pkl)
|
||||||
|
|
||||||
|
|
||||||
def generate_chunked_model(driving_pkl: Path) -> dict:
|
def generate_metadata(model_path: Path, output_dir: Path, short_name: str, driving_pkl: Path):
|
||||||
|
base = model_path.stem
|
||||||
|
metadata_file = output_dir / f"{base}_metadata.pkl"
|
||||||
|
|
||||||
|
if short_name:
|
||||||
|
renamed_meta = output_dir / f"{base}_{short_name.lower()}_metadata.pkl"
|
||||||
|
if metadata_file.exists() and not renamed_meta.exists():
|
||||||
|
metadata_file = metadata_file.rename(renamed_meta)
|
||||||
|
elif renamed_meta.exists():
|
||||||
|
metadata_file = renamed_meta
|
||||||
|
|
||||||
|
if not metadata_file.exists():
|
||||||
|
print(f"Warning: Missing metadata for {base} ({metadata_file}), skipping", file=sys.stderr)
|
||||||
|
return
|
||||||
|
|
||||||
tinygrad_hash = hashlib.sha256(_read_pkl_bytes(driving_pkl)).hexdigest()
|
tinygrad_hash = hashlib.sha256(_read_pkl_bytes(driving_pkl)).hexdigest()
|
||||||
|
|
||||||
chunks_config = []
|
with open(metadata_file, 'rb') as f:
|
||||||
manifest_file = Path(f"{driving_pkl}.chunkmanifest")
|
metadata_hash = hashlib.sha256(f.read()).hexdigest()
|
||||||
if manifest_file.exists():
|
|
||||||
num_chunks = int(manifest_file.read_text().strip())
|
|
||||||
for i in range(num_chunks):
|
|
||||||
chunk_path = Path(f"{driving_pkl}.chunk{i + 1:02d}of{num_chunks:02d}")
|
|
||||||
if chunk_path.exists():
|
|
||||||
chunk_hash = hashlib.sha256(chunk_path.read_bytes()).hexdigest()
|
|
||||||
chunks_config.append({
|
|
||||||
"file_name": chunk_path.name,
|
|
||||||
"sha256": chunk_hash
|
|
||||||
})
|
|
||||||
|
|
||||||
artifact_data = {
|
model_type = "offPolicy" if "off_policy" in base else "onPolicy" if "on_policy" in base else base.split("_")[-1]
|
||||||
"file_name": driving_pkl.name,
|
|
||||||
"download_uri": {
|
return {
|
||||||
"url": "https://gitlab.com/sunnypilot/public/docs.sunnypilot.ai/-/raw/main/",
|
"type": model_type,
|
||||||
"sha256": tinygrad_hash
|
"artifact": {
|
||||||
|
"file_name": driving_pkl.name,
|
||||||
|
"download_uri": {
|
||||||
|
"url": "https://gitlab.com/sunnypilot/public/docs.sunnypilot.ai/-/raw/main/",
|
||||||
|
"sha256": tinygrad_hash
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"file_name": metadata_file.name,
|
||||||
|
"download_uri": {
|
||||||
|
"url": "https://gitlab.com/sunnypilot/public/docs.sunnypilot.ai/-/raw/main/",
|
||||||
|
"sha256": metadata_hash
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if chunks_config:
|
|
||||||
artifact_data["chunks"] = chunks_config
|
|
||||||
|
|
||||||
return {
|
def create_metadata_json(models: list, output_dir: Path, custom_name=None, short_name=None, is_20hz=False, upstream_branch="unknown"):
|
||||||
"type": "chunked",
|
metadata_json = {
|
||||||
"artifact": artifact_data,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def create_metadata_json(models: list, output_dir: Path, custom_name=None, short_name=None, is_20hz=False, upstream_branch="unknown") -> None:
|
|
||||||
bundle_json = {
|
|
||||||
"short_name": short_name,
|
"short_name": short_name,
|
||||||
"display_name": custom_name or upstream_branch,
|
"display_name": custom_name or upstream_branch,
|
||||||
"is_20hz": is_20hz,
|
"is_20hz": is_20hz,
|
||||||
@@ -131,26 +179,40 @@ def create_metadata_json(models: list, output_dir: Path, custom_name=None, short
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Write metadata to output_dir
|
# Write metadata to output_dir
|
||||||
metadata_json = {
|
|
||||||
"bundles": [bundle_json]
|
|
||||||
}
|
|
||||||
|
|
||||||
with open(output_dir / "metadata.json", "w") as f:
|
with open(output_dir / "metadata.json", "w") as f:
|
||||||
json.dump(metadata_json, f, indent=2)
|
json.dump(metadata_json, f, indent=2)
|
||||||
print("Generated metadata.json")
|
|
||||||
|
print(f"Generated metadata.json with {len(models)} models.")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import argparse
|
import argparse
|
||||||
|
import glob
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="Generate metadata JSON for the compiled JIT model")
|
parser = argparse.ArgumentParser(description="Generate metadata for model files")
|
||||||
parser.add_argument("--model-dir", default="./models", help="Directory containing the model files")
|
parser.add_argument("--model-dir", default="./models", help="Directory containing ONNX model files")
|
||||||
parser.add_argument("--output-dir", default="./output", help="Output directory for metadata")
|
parser.add_argument("--output-dir", default="./output", help="Output directory for metadata")
|
||||||
parser.add_argument("--custom-name", help="Custom display name for the model")
|
parser.add_argument("--custom-name", help="Custom display name for the model")
|
||||||
parser.add_argument("--is-20hz", action="store_true", help="Whether this is a 20Hz model")
|
parser.add_argument("--is-20hz", action="store_true", help="Whether this is a 20Hz model")
|
||||||
|
parser.add_argument("--validate-only", action="store_true")
|
||||||
parser.add_argument("--upstream-branch", default="unknown", help="Upstream branch name")
|
parser.add_argument("--upstream-branch", default="unknown", help="Upstream branch name")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.validate_only:
|
||||||
|
metadata_paths = glob.glob(os.path.join(args.model_dir, "*_metadata.pkl"))
|
||||||
|
if not metadata_paths:
|
||||||
|
print(f"No metadata files found in {args.model_dir}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
validate_model_outputs([Path(p) for p in metadata_paths])
|
||||||
|
print(f"Validated {len(metadata_paths)} metadata files successfully.")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Find all ONNX files in the given directory
|
||||||
|
model_paths = glob.glob(os.path.join(args.model_dir, "*.onnx"))
|
||||||
|
if not model_paths:
|
||||||
|
print(f"No ONNX files found in {args.model_dir}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
_output_dir = Path(args.output_dir)
|
_output_dir = Path(args.output_dir)
|
||||||
_output_dir.mkdir(exist_ok=True, parents=True)
|
_output_dir.mkdir(exist_ok=True, parents=True)
|
||||||
_short_name = create_short_name(args.custom_name) if args.custom_name else None
|
_short_name = create_short_name(args.custom_name) if args.custom_name else None
|
||||||
@@ -167,5 +229,14 @@ if __name__ == "__main__":
|
|||||||
else:
|
else:
|
||||||
_driving_pkl = new_pkl
|
_driving_pkl = new_pkl
|
||||||
|
|
||||||
_model_metadata = generate_chunked_model(_driving_pkl)
|
_models = []
|
||||||
create_metadata_json([_model_metadata], _output_dir, args.custom_name, _short_name, args.is_20hz, args.upstream_branch)
|
|
||||||
|
for _model_path in model_paths:
|
||||||
|
_model_metadata = generate_metadata(Path(_model_path), _output_dir, _short_name, _driving_pkl)
|
||||||
|
if _model_metadata:
|
||||||
|
_models.append(_model_metadata)
|
||||||
|
|
||||||
|
if _models:
|
||||||
|
create_metadata_json(_models, _output_dir, args.custom_name, _short_name, args.is_20hz, args.upstream_branch)
|
||||||
|
else:
|
||||||
|
print("No models processed.", file=sys.stderr)
|
||||||
|
|||||||
+1
-1
Submodule tinygrad_repo updated: 2fecac4e4a...ac1632ab96
Reference in New Issue
Block a user