IQ.Pilot Release Commit @ d2ce8a8
This commit is contained in:
163
iqpilot/selfdrive/iqmodeld/tests/test_egpu_stock_parity.py
Normal file
163
iqpilot/selfdrive/iqmodeld/tests/test_egpu_stock_parity.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
import types
|
||||
|
||||
import pytest
|
||||
|
||||
from iqpilot.cereal import log, messaging
|
||||
from iqpilot.cereal.services import SERVICE_LIST
|
||||
|
||||
|
||||
class TestTelemetryContract:
|
||||
def test_service_is_published_at_stock_cadence(self):
|
||||
assert "egpuDockState" in SERVICE_LIST
|
||||
assert SERVICE_LIST["egpuDockState"].frequency == 10.
|
||||
|
||||
def test_message_carries_every_stock_field(self):
|
||||
msg = messaging.new_message("egpuDockState")
|
||||
state = msg.egpuDockState
|
||||
for field in ("tempC", "memoryTempC", "powerDrawW", "powerLimitW", "gpuUsagePercent",
|
||||
"gpuClockMhz", "fanSpeedRpm", "pcieLtssm", "supplyVoltage", "supplyCurrent"):
|
||||
setattr(state, field, 1)
|
||||
assert getattr(state, field) == 1
|
||||
|
||||
def test_metrics_refresh_matches_stock(self):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_telemetry import METRICS_REFRESH_EVERY
|
||||
assert METRICS_REFRESH_EVERY == 100
|
||||
|
||||
def test_send_without_a_gpu_publishes_an_invalid_message(self):
|
||||
from iqpilot.selfdrive.iqmodeld import egpu_telemetry
|
||||
sent = []
|
||||
telemetry = egpu_telemetry.EgpuDockTelemetry(types.SimpleNamespace(send=lambda n, m: sent.append((n, m))), big=True)
|
||||
telemetry._device = lambda: types.SimpleNamespace(_opened_devices=set())
|
||||
telemetry.send()
|
||||
assert sent and sent[0][0] == "egpuDockState"
|
||||
assert sent[0][1].valid is False
|
||||
|
||||
|
||||
class TestBigFrameFlag:
|
||||
def test_model_message_carries_the_big_flag(self):
|
||||
msg = messaging.new_message("modelV2")
|
||||
msg.modelV2.big = True
|
||||
assert msg.modelV2.big
|
||||
|
||||
|
||||
class TestStatusParams:
|
||||
def test_loading_param_exists_and_is_cleared_like_stock(self):
|
||||
from pathlib import Path
|
||||
root = Path(__file__).resolve().parents[3]
|
||||
keys = (root / "common" / "params_keys.h").read_text()
|
||||
assert '{"UsbGpuLoading"' in keys
|
||||
line = next(ln for ln in keys.splitlines() if '"UsbGpuLoading"' in ln)
|
||||
for flag in ("CLEAR_ON_MANAGER_START", "CLEAR_ON_OFFROAD_TRANSITION", "CLEAR_ON_IGNITION_ON"):
|
||||
assert flag in line
|
||||
|
||||
|
||||
class TestAlerts:
|
||||
def test_both_stock_big_model_events_exist(self):
|
||||
assert hasattr(log.OnroadEvent.EventName, "bigModelLoading")
|
||||
assert hasattr(log.OnroadEvent.EventName, "bigModelFailed")
|
||||
|
||||
def test_alerts_are_wired_with_stock_severities(self):
|
||||
from iqpilot.selfdrive.selfdrived.events import EVENTS, ET
|
||||
EventName = log.OnroadEvent.EventName
|
||||
loading = EVENTS[EventName.bigModelLoading]
|
||||
failed = EVENTS[EventName.bigModelFailed]
|
||||
assert ET.NO_ENTRY in loading
|
||||
assert ET.SOFT_DISABLE in failed and ET.PERMANENT in failed
|
||||
|
||||
|
||||
class TestFirmwareGate:
|
||||
def test_runtime_refuses_a_dock_on_other_firmware(self, tmp_path):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_helpers import usbgpu_present
|
||||
from iqpilot.system.hardware.usb import EGPU_DOCK_FW_PRODUCT, EGPU_DOCK_USB_IDS
|
||||
vid, pid = EGPU_DOCK_USB_IDS[0]
|
||||
d = tmp_path / "1-1"
|
||||
d.mkdir()
|
||||
(d / "idVendor").write_text(f"{vid:04x}\n")
|
||||
(d / "idProduct").write_text(f"{pid:04x}\n")
|
||||
(d / "product").write_text("custom deadbeef-CLEAN\n")
|
||||
assert not usbgpu_present(str(tmp_path))
|
||||
(d / "product").write_text(EGPU_DOCK_FW_PRODUCT + "\n")
|
||||
assert usbgpu_present(str(tmp_path))
|
||||
|
||||
|
||||
class TestAutoFlash:
|
||||
def test_hardwared_drives_the_flasher_offroad_only(self):
|
||||
from iqpilot.system.hardware.hardwared import EgpuDockFlasher
|
||||
f = EgpuDockFlasher()
|
||||
calls = []
|
||||
f.flash = lambda: calls.append(1)
|
||||
stale = [{"vendorId": 0xADD1, "productId": 0x0001, "product": "custom deadbeef-CLEAN"}]
|
||||
f.update(False, stale)
|
||||
assert f.attempts == 0, "must not flash onroad"
|
||||
f.update(True, stale)
|
||||
assert f.attempts == 1
|
||||
if f.thread is not None:
|
||||
f.thread.join(timeout=5)
|
||||
|
||||
def test_matching_firmware_is_never_flashed(self):
|
||||
from iqpilot.system.hardware.egpu_dock.flash import bundled_version
|
||||
from iqpilot.system.hardware.hardwared import EgpuDockFlasher
|
||||
f = EgpuDockFlasher()
|
||||
f.flash = lambda: pytest.fail("flashed a dock that already matches")
|
||||
f.update(True, [{"vendorId": 0xADD1, "productId": 0x0001, "product": bundled_version()}])
|
||||
assert f.attempts == 0
|
||||
|
||||
def test_attempts_are_bounded_like_stock(self):
|
||||
from iqpilot.system.hardware.hardwared import EgpuDockFlasher
|
||||
assert EgpuDockFlasher.MAX_ATTEMPTS == 3
|
||||
assert EgpuDockFlasher.RETRY_INTERVAL == 20.
|
||||
|
||||
|
||||
class TestDockIsItsOwnConsent:
|
||||
|
||||
def _params(self, **flags):
|
||||
class P:
|
||||
def get_bool(self, k):
|
||||
return bool(flags.get(k, False))
|
||||
def get(self, k, *a, **kw):
|
||||
return None
|
||||
return P()
|
||||
|
||||
def _sysfs_with_dock(self, tmp_path, product=None):
|
||||
from iqpilot.system.hardware.usb import EGPU_DOCK_FW_PRODUCT, EGPU_DOCK_USB_IDS
|
||||
vid, pid = EGPU_DOCK_USB_IDS[0]
|
||||
d = tmp_path / "1-1"
|
||||
d.mkdir()
|
||||
(d / "idVendor").write_text(f"{vid:04x}\n")
|
||||
(d / "idProduct").write_text(f"{pid:04x}\n")
|
||||
(d / "product").write_text((product or EGPU_DOCK_FW_PRODUCT) + "\n")
|
||||
return str(tmp_path)
|
||||
|
||||
def test_a_plugged_in_dock_selects_itself(self, tmp_path):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected
|
||||
assert egpu_selected(self._params(), self._sysfs_with_dock(tmp_path))
|
||||
|
||||
def test_nothing_plugged_in_selects_nothing(self, tmp_path):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected
|
||||
assert not egpu_selected(self._params(), str(tmp_path))
|
||||
|
||||
def test_a_dock_on_foreign_firmware_does_not_select_itself(self, tmp_path):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected
|
||||
assert not egpu_selected(self._params(), self._sysfs_with_dock(tmp_path, "custom deadbeef-CLEAN"))
|
||||
|
||||
def test_the_user_can_force_it_off(self, tmp_path):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected
|
||||
root = self._sysfs_with_dock(tmp_path)
|
||||
assert not egpu_selected(self._params(IQEgpuDisabled=True), root)
|
||||
|
||||
def test_the_param_can_force_it_on_without_hardware(self, tmp_path):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected
|
||||
assert egpu_selected(self._params(IQEgpuEnabled=True), str(tmp_path))
|
||||
|
||||
def test_present_dock_wins_even_with_emac_enabled(self, tmp_path):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected, resolve_backend, usbgpu_present
|
||||
root = self._sysfs_with_dock(tmp_path)
|
||||
assert resolve_backend(True, egpu_selected(self._params(), root), usbgpu_present(root)) == "egpu"
|
||||
|
||||
def test_force_param_without_hardware_yields_to_emac(self, tmp_path):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected, resolve_backend, usbgpu_present
|
||||
assert resolve_backend(True, egpu_selected(self._params(IQEgpuEnabled=True), str(tmp_path)),
|
||||
usbgpu_present(str(tmp_path))) == "emac"
|
||||
509
iqpilot/selfdrive/iqmodeld/tests/test_egpu_worker.py
Normal file
509
iqpilot/selfdrive/iqmodeld/tests/test_egpu_worker.py
Normal file
@@ -0,0 +1,509 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import json
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_helpers import (
|
||||
resolve_backend, resolve_download_url, usbgpu_present,
|
||||
)
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_pipeline import (
|
||||
EgpuPipeline, EgpuPipelineError, make_big_channel_payload,
|
||||
)
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_model import EGPU_MODELS, get_egpu_model
|
||||
from iqpilot.selfdrive.iqmodeld.temporal_state import MODEL_INPUT_SPEC as INPUT_SPEC
|
||||
|
||||
|
||||
class FakeParams:
|
||||
def __init__(self, **flags):
|
||||
self._flags = {k: bool(v) for k, v in flags.items()}
|
||||
|
||||
def get_bool(self, key: str) -> bool:
|
||||
return self._flags.get(key, False)
|
||||
|
||||
|
||||
def _fake_usb_device(root, vid: str, pid: str, name: str = "1-1", product: str | None = None):
|
||||
from iqpilot.system.hardware.usb import EGPU_DOCK_FW_PRODUCT
|
||||
d = root / name
|
||||
d.mkdir()
|
||||
(d / "idVendor").write_text(vid + "\n")
|
||||
(d / "idProduct").write_text(pid + "\n")
|
||||
(d / "product").write_text((product if product is not None else EGPU_DOCK_FW_PRODUCT) + "\n")
|
||||
|
||||
|
||||
class TestPresence:
|
||||
def test_present(self, tmp_path):
|
||||
_fake_usb_device(tmp_path, "add1", "0001")
|
||||
assert usbgpu_present(str(tmp_path))
|
||||
|
||||
def test_foreign_firmware_absent(self, tmp_path):
|
||||
_fake_usb_device(tmp_path, "add1", "0001", product="custom deadbeef-CLEAN")
|
||||
assert not usbgpu_present(str(tmp_path))
|
||||
|
||||
def test_wrong_ids_absent(self, tmp_path):
|
||||
_fake_usb_device(tmp_path, "05ac", "12a8")
|
||||
assert not usbgpu_present(str(tmp_path))
|
||||
|
||||
def test_empty_bus_absent(self, tmp_path):
|
||||
assert not usbgpu_present(str(tmp_path))
|
||||
|
||||
def test_unreadable_entries_skipped(self, tmp_path):
|
||||
(tmp_path / "usb1").mkdir()
|
||||
_fake_usb_device(tmp_path, "add1", "0001", name="1-2")
|
||||
assert usbgpu_present(str(tmp_path))
|
||||
|
||||
|
||||
class TestBackendResolution:
|
||||
def test_none(self):
|
||||
assert resolve_backend(False, False) is None
|
||||
|
||||
def test_emac_only(self):
|
||||
assert resolve_backend(True, False) == "emac"
|
||||
|
||||
def test_egpu_only(self):
|
||||
assert resolve_backend(False, True) == "egpu"
|
||||
|
||||
def test_force_param_yields_to_emac_without_hardware(self):
|
||||
assert resolve_backend(True, True) == "emac"
|
||||
|
||||
def test_present_dock_wins_over_emac(self):
|
||||
assert resolve_backend(True, True, True) == "egpu"
|
||||
|
||||
|
||||
class TestManagerGating:
|
||||
@pytest.fixture
|
||||
def pc(self):
|
||||
return pytest.importorskip("iqpilot.system.manager.process_config")
|
||||
|
||||
def test_egpu_needs_presence(self, pc, monkeypatch):
|
||||
monkeypatch.setattr(pc, "usbgpu_present", lambda: True)
|
||||
assert pc.egpu_enabled(True, FakeParams(IQEgpuEnabled=True), None)
|
||||
assert pc.egpu_enabled(True, FakeParams(), None)
|
||||
monkeypatch.setattr(pc, "usbgpu_present", lambda: False)
|
||||
assert not pc.egpu_enabled(True, FakeParams(IQEgpuEnabled=True), None)
|
||||
assert not pc.egpu_enabled(True, FakeParams(), None)
|
||||
|
||||
def test_present_dock_wins_over_left_on_emac(self, pc, monkeypatch):
|
||||
monkeypatch.setattr(pc, "usbgpu_present", lambda: True)
|
||||
both = FakeParams(IQEmacEnabled=True, IQEgpuEnabled=True)
|
||||
assert not pc.emac_enabled(True, both, None)
|
||||
assert pc.egpu_enabled(True, both, None)
|
||||
|
||||
def test_emac_runs_when_no_dock(self, pc, monkeypatch):
|
||||
monkeypatch.setattr(pc, "usbgpu_present", lambda: False)
|
||||
assert pc.emac_enabled(True, FakeParams(IQEmacEnabled=True), None)
|
||||
|
||||
def test_disabled_dock_yields_to_emac(self, pc, monkeypatch):
|
||||
monkeypatch.setattr(pc, "usbgpu_present", lambda: True)
|
||||
both = FakeParams(IQEmacEnabled=True, IQEgpuDisabled=True)
|
||||
assert pc.emac_enabled(True, both, None)
|
||||
assert not pc.egpu_enabled(True, both, None)
|
||||
|
||||
def test_disabled_dock_runs_no_backend_when_no_emac(self, pc, monkeypatch):
|
||||
monkeypatch.setattr(pc, "usbgpu_present", lambda: True)
|
||||
off = FakeParams(IQEgpuDisabled=True)
|
||||
assert not pc.egpu_enabled(True, off, None)
|
||||
assert not pc.emac_enabled(True, off, None)
|
||||
|
||||
def test_selector_runs_for_either_backend(self, pc):
|
||||
assert pc.big_model_enabled(True, FakeParams(IQEmacEnabled=True), None)
|
||||
assert pc.big_model_enabled(True, FakeParams(IQEgpuEnabled=True), None)
|
||||
assert not pc.big_model_enabled(True, FakeParams(), None)
|
||||
|
||||
def test_iqegpumodeld_registered(self, pc):
|
||||
assert "iqegpumodeld" in pc.managed_processes
|
||||
assert "maciqmodeld" in pc.managed_processes
|
||||
|
||||
|
||||
class TestDownloadResolve:
|
||||
def test_direct_url_passthrough(self):
|
||||
assert resolve_download_url("https://x/y.onnx", "0" * 64, 5) == "https://x/y.onnx"
|
||||
|
||||
def test_commalfs_batch(self, monkeypatch):
|
||||
seen = {}
|
||||
|
||||
def fake_urlopen(req, timeout=0):
|
||||
seen["url"] = req.full_url
|
||||
seen["body"] = json.loads(req.data)
|
||||
return io.BytesIO(json.dumps(
|
||||
{"objects": [{"actions": {"download": {"href": "https://signed/url"}}}]}).encode())
|
||||
|
||||
monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen)
|
||||
sha = "a5" * 32
|
||||
url = resolve_download_url(f"commalfs:{sha}", sha, 1234)
|
||||
assert url == "https://signed/url"
|
||||
assert seen["body"]["objects"] == [{"oid": sha, "size": 1234}]
|
||||
assert seen["url"].endswith("/info/lfs/objects/batch")
|
||||
|
||||
|
||||
def _zero_infer(output_len: int, fill=None):
|
||||
calls = []
|
||||
|
||||
def infer(inputs):
|
||||
for name, (shape, dtype) in INPUT_SPEC.items():
|
||||
assert tuple(inputs[name].shape) == shape, name
|
||||
assert inputs[name].dtype == np.dtype(dtype), name
|
||||
calls.append({k: v.copy() for k, v in inputs.items()})
|
||||
out = np.zeros(output_len, dtype=np.float32)
|
||||
if fill is not None:
|
||||
out[:] = fill
|
||||
return out
|
||||
|
||||
infer.calls = calls
|
||||
return infer
|
||||
|
||||
|
||||
def _frame_inputs(seed=0):
|
||||
rng = np.random.default_rng(seed)
|
||||
warped = rng.integers(0, 256, (2, 6, 128, 256)).astype(np.uint8)
|
||||
desire = np.zeros(8, dtype=np.float32)
|
||||
traffic = np.array([1.0, 0.0], dtype=np.float32)
|
||||
action_t = np.array([0.25, 0.55], dtype=np.float32)
|
||||
return warped, desire, traffic, action_t
|
||||
|
||||
|
||||
class TestEgpuPipeline:
|
||||
def setup_method(self):
|
||||
self.meta = get_egpu_model()
|
||||
|
||||
def test_split_model_rejected(self):
|
||||
split_meta = {**get_egpu_model(), "key": "some_split", "split": True}
|
||||
with pytest.raises(EgpuPipelineError, match="split"):
|
||||
EgpuPipeline(split_meta, _zero_infer(split_meta["output_len"]))
|
||||
|
||||
def test_registry_is_fused_only(self):
|
||||
assert not any(m.get("split") for m in EGPU_MODELS.values())
|
||||
|
||||
def test_run_shapes_and_output(self):
|
||||
infer = _zero_infer(self.meta["output_len"])
|
||||
pipe = EgpuPipeline(self.meta, infer)
|
||||
out = pipe.run(*_frame_inputs())
|
||||
assert out.shape == (self.meta["output_len"],)
|
||||
assert len(infer.calls) == 1
|
||||
|
||||
def test_hidden_state_feeds_next_features_buffer(self):
|
||||
output_len = self.meta["output_len"]
|
||||
hidden = self.meta["output_slices"]["hidden_state"]
|
||||
|
||||
def infer(inputs):
|
||||
out = np.zeros(output_len, dtype=np.float32)
|
||||
out[hidden] = np.arange(hidden.stop - hidden.start, dtype=np.float32)
|
||||
return out
|
||||
|
||||
pipe = EgpuPipeline(self.meta, infer)
|
||||
pipe.run(*_frame_inputs(1))
|
||||
np.testing.assert_array_equal(
|
||||
pipe.state.prev_feat.reshape(-1), np.arange(hidden.stop - hidden.start, dtype=np.float32))
|
||||
pipe.run(*_frame_inputs(2))
|
||||
np.testing.assert_array_equal(
|
||||
pipe.state.feat_q[-1].reshape(-1), np.arange(hidden.stop - hidden.start, dtype=np.float32))
|
||||
|
||||
def test_desire_rising_edge_pulse(self):
|
||||
infer = _zero_infer(self.meta["output_len"])
|
||||
pipe = EgpuPipeline(self.meta, infer)
|
||||
warped, _, traffic, action_t = _frame_inputs()
|
||||
desire_on = np.zeros(8, dtype=np.float32)
|
||||
desire_on[3] = 1.0
|
||||
pipe.run(warped, desire_on, traffic, action_t)
|
||||
assert infer.calls[-1]["desire_pulse"][0, -1, 3] == 1.0
|
||||
for _ in range(5):
|
||||
pipe.run(warped, desire_on, traffic, action_t)
|
||||
assert infer.calls[-1]["desire_pulse"][0, :, 3].sum() == 1.0
|
||||
|
||||
def test_wrong_output_len_raises(self):
|
||||
pipe = EgpuPipeline(self.meta, _zero_infer(self.meta["output_len"] - 1))
|
||||
with pytest.raises(EgpuPipelineError, match="length"):
|
||||
pipe.run(*_frame_inputs())
|
||||
|
||||
def test_non_finite_output_raises(self):
|
||||
pipe = EgpuPipeline(self.meta, _zero_infer(self.meta["output_len"], fill=np.nan))
|
||||
with pytest.raises(EgpuPipelineError, match="finite"):
|
||||
pipe.run(*_frame_inputs())
|
||||
|
||||
|
||||
class TestChannelContract:
|
||||
def _real_msgs(self):
|
||||
import iqpilot.cereal.messaging as messaging
|
||||
msgs = {}
|
||||
for svc in ("modelV2", "drivingModelData", "cameraOdometry", "iqDriveModelData"):
|
||||
m = messaging.new_message(svc)
|
||||
msgs[svc] = m.to_bytes()
|
||||
return msgs
|
||||
|
||||
def test_payload_keys_match_selector_contract(self):
|
||||
payload = make_big_channel_payload(7, True, 0.031, 24.0, {"modelV2": b"x"})
|
||||
assert payload["source"] == "egpu_big"
|
||||
for key in ("frame_id", "live_calib_seen", "model_execution_time", "msgs"):
|
||||
assert key in payload
|
||||
|
||||
def test_selector_consumes_egpu_payload(self, tmp_path):
|
||||
from iqpilot.selfdrive.iqmodeld.model_channel import ModelChannel
|
||||
from iqpilot.selfdrive.iqmodeld.modeld_selector import wait_for_big
|
||||
|
||||
chan = ModelChannel(str(tmp_path / "big"), create=True)
|
||||
payload = make_big_channel_payload(100, True, 0.03, 25.0, self._real_msgs())
|
||||
chan.write(100, payload)
|
||||
|
||||
got, peek = wait_for_big(chan, 100, time.perf_counter() + 0.01)
|
||||
assert peek == 100
|
||||
assert got is not None
|
||||
assert got["source"] == "egpu_big"
|
||||
assert got["frame_id"] == 100
|
||||
|
||||
def test_selector_patch_and_send_parses_egpu_msgs(self):
|
||||
from iqpilot.selfdrive.iqmodeld.modeld_selector import _patch_and_send
|
||||
|
||||
sent = {}
|
||||
|
||||
class PM:
|
||||
def send(self, service, msg):
|
||||
sent[service] = msg
|
||||
|
||||
payload = make_big_channel_payload(42, True, 0.03, 25.0, self._real_msgs())
|
||||
_patch_and_send(PM(), payload, frame_drop_perc=0.0, selector_dropped=0, target=42, source_lag=0)
|
||||
assert set(sent) == {"modelV2", "drivingModelData", "cameraOdometry", "iqDriveModelData"}
|
||||
assert sent["modelV2"].modelV2.frameDropPerc == 0.0
|
||||
assert sent["cameraOdometry"].valid
|
||||
|
||||
def test_selector_lag_patches_frame_id(self):
|
||||
from iqpilot.selfdrive.iqmodeld.modeld_selector import _patch_and_send
|
||||
|
||||
sent = {}
|
||||
|
||||
class PM:
|
||||
def send(self, service, msg):
|
||||
sent[service] = msg
|
||||
|
||||
payload = make_big_channel_payload(40, True, 0.03, 25.0, self._real_msgs())
|
||||
_patch_and_send(PM(), payload, frame_drop_perc=0.0, selector_dropped=0, target=42, source_lag=2)
|
||||
assert sent["modelV2"].modelV2.frameId == 42
|
||||
assert not sent["cameraOdometry"].valid
|
||||
|
||||
|
||||
def _import_worker():
|
||||
try:
|
||||
import iqpilot.selfdrive.iqmodeld.iqegpumodeld as w
|
||||
return w
|
||||
except ImportError as e:
|
||||
if any(tag in str(e) for tag in ("pyx", "visionipc", "proprietary_runtime")):
|
||||
pytest.skip(f"device-only import chain unavailable on this host: {e}")
|
||||
raise
|
||||
|
||||
|
||||
class TestWorkerModule:
|
||||
def test_module_imports_off_device(self):
|
||||
w = _import_worker()
|
||||
assert w.PROCESS_NAME.endswith("iqegpumodeld")
|
||||
assert callable(w.main)
|
||||
|
||||
def test_warmup_validates_output(self):
|
||||
w = _import_worker()
|
||||
spec = {name: (shape, dtype) for name, (shape, dtype) in INPUT_SPEC.items()}
|
||||
def good(inputs):
|
||||
return np.zeros(10, dtype=np.float32)
|
||||
assert w._warmup(good, spec, 10) >= 0.0
|
||||
with pytest.raises(RuntimeError, match="invalid"):
|
||||
w._warmup(good, spec, 11)
|
||||
|
||||
|
||||
class TestSelectorBackendKeys:
|
||||
def test_emac_default(self):
|
||||
from iqpilot.selfdrive.iqmodeld.modeld_selector import EMAC_STATUS_KEYS, backend_status_keys
|
||||
assert backend_status_keys(False, False) is EMAC_STATUS_KEYS
|
||||
assert backend_status_keys(True, False) is EMAC_STATUS_KEYS
|
||||
|
||||
def test_egpu_selected(self):
|
||||
from iqpilot.selfdrive.iqmodeld.modeld_selector import EGPU_STATUS_KEYS, backend_status_keys
|
||||
assert backend_status_keys(False, True) is EGPU_STATUS_KEYS
|
||||
assert backend_status_keys(False, True)["active"] == "UsbGpuActive"
|
||||
assert backend_status_keys(False, True)["failed"] == "UsbGpuFailed"
|
||||
|
||||
def test_emac_wins_when_both(self):
|
||||
from iqpilot.selfdrive.iqmodeld.modeld_selector import EMAC_STATUS_KEYS, backend_status_keys
|
||||
assert backend_status_keys(True, True) is EMAC_STATUS_KEYS
|
||||
|
||||
def test_key_maps_cover_same_roles(self):
|
||||
from iqpilot.selfdrive.iqmodeld.modeld_selector import EGPU_STATUS_KEYS, EMAC_STATUS_KEYS
|
||||
assert set(EGPU_STATUS_KEYS) == set(EMAC_STATUS_KEYS)
|
||||
|
||||
|
||||
class TestBackendSeparation:
|
||||
EGPU_SOURCES = (
|
||||
"egpu_helpers.py", "egpu_pipeline.py", "egpu_model.py", "iqegpumodeld.py",
|
||||
"big_catalog.py", "tools/compile_egpu_model.py",
|
||||
)
|
||||
BANNED_IMPORTS = ("emac_input_state", "emac_model_meta", "maciqmodeld", "mac_protocol", "mac_client")
|
||||
|
||||
def _sources(self):
|
||||
import pathlib
|
||||
root = pathlib.Path(__file__).resolve().parents[1]
|
||||
return {name: (root / name).read_text() for name in self.EGPU_SOURCES}
|
||||
|
||||
def test_no_emac_module_imports(self):
|
||||
for name, src in self._sources().items():
|
||||
for banned in self.BANNED_IMPORTS:
|
||||
assert f"import {banned}" not in src and f"iqmodeld.{banned}" not in src, f"{name} imports {banned}"
|
||||
|
||||
def test_no_macmodel_params(self):
|
||||
for name, src in self._sources().items():
|
||||
assert "MacModel" not in src, f"{name} references MacModel* params"
|
||||
|
||||
def test_emac_shim_reexports_temporal_state(self):
|
||||
from iqpilot.selfdrive.iqmodeld import emac_input_state, temporal_state
|
||||
assert emac_input_state.EmacInputState is temporal_state.TemporalInputState
|
||||
assert emac_input_state.SplitInputState is temporal_state.SplitTemporalState
|
||||
|
||||
def test_emac_modules_are_not_in_the_public_tree(self):
|
||||
import pathlib
|
||||
root = pathlib.Path(__file__).resolve().parents[1]
|
||||
for gone in ("mac_protocol.py", "mac_client.py", "maciqmodeld.py", "bulk_transport.py"):
|
||||
assert not (root / gone).exists(), f"{gone} must live only in konn3kt_private"
|
||||
|
||||
|
||||
class TestMetaDrivenInputSpec:
|
||||
|
||||
def _run_one(self, meta):
|
||||
seen = {}
|
||||
def infer(inputs):
|
||||
seen.update({k: v.shape for k, v in inputs.items()})
|
||||
return np.zeros(meta["output_len"], dtype=np.float32)
|
||||
pipe = EgpuPipeline(meta, infer)
|
||||
pipe.run(np.zeros((2, 6, 128, 256), np.uint8), np.zeros(8, np.float32),
|
||||
np.array([1, 0], np.float32), np.zeros(2, np.float32))
|
||||
return seen
|
||||
|
||||
def test_default_contract_unchanged(self):
|
||||
meta = get_egpu_model()
|
||||
seen = self._run_one(meta)
|
||||
assert seen["features_buffer"] == (1, 24, 512)
|
||||
assert seen["desire_pulse"] == (1, 25, 8)
|
||||
|
||||
def test_registry_shapes_drive_the_state(self):
|
||||
meta = dict(get_egpu_model())
|
||||
meta["output_len"] = 18452
|
||||
meta["output_slices"] = dict(meta["output_slices"], hidden_state=slice(2066, 18450))
|
||||
meta["input_shapes"] = {
|
||||
"img": (1, 12, 128, 256), "big_img": (1, 12, 128, 256),
|
||||
"desire_pulse": (1, 33, 8), "traffic_convention": (1, 2),
|
||||
"action_t": (1, 2), "features_buffer": (1, 32, 32, 512),
|
||||
}
|
||||
seen = self._run_one(meta)
|
||||
assert seen["features_buffer"] == (1, 32, 32, 512)
|
||||
assert seen["desire_pulse"] == (1, 33, 8)
|
||||
|
||||
|
||||
class TestCatalogResolution:
|
||||
|
||||
def _params(self, model, doc=None):
|
||||
class P:
|
||||
def get(self, k):
|
||||
if k == "IQEmacModel":
|
||||
return model
|
||||
if k == "IQEmacCatalogCache":
|
||||
return json.dumps(doc) if doc else None
|
||||
return None
|
||||
return P()
|
||||
|
||||
def _doc(self):
|
||||
return {"schema": 1, "bundles": [{
|
||||
"short_name": "ttx", "display_name": "TTx", "index": 1,
|
||||
"model_name": "big_driving_supercombo",
|
||||
"wire": {"output_len": 2580, "frame_skip": 4, "pipeline": True,
|
||||
"output_slices": {"plan": [917, 1907], "hidden_state": [2066, 2578], "pad": [-2, None]},
|
||||
"input_shapes": {"img": [1, 12, 128, 256], "big_img": [1, 12, 128, 256],
|
||||
"desire_pulse": [1, 33, 8], "traffic_convention": [1, 2],
|
||||
"action_t": [1, 2], "features_buffer": [1, 32, 512]},
|
||||
"lat_smooth_seconds": 0.1},
|
||||
"source": {"kind": "comma_lfs", "sha256": "c" * 64, "size": 1},
|
||||
}]}
|
||||
|
||||
def test_unset_selection_is_the_builtin_default(self):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_model import resolve_egpu_model
|
||||
m = resolve_egpu_model(self._params(None))
|
||||
assert m["key"] == "lebrowski" and m["sha256"].startswith("a501760a")
|
||||
|
||||
def test_catalog_selection_resolves_with_shapes_and_smoothing(self):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_model import resolve_egpu_model
|
||||
m = resolve_egpu_model(self._params("ttx", self._doc()))
|
||||
assert m["key"] == "ttx"
|
||||
assert m["input_shapes"]["features_buffer"] == (1, 32, 512)
|
||||
assert m["input_shapes"]["desire_pulse"] == (1, 33, 8)
|
||||
assert m["lat_smooth_seconds"] == 0.1
|
||||
assert m["output_slices"]["pad"] == slice(-2, None)
|
||||
|
||||
def test_unknown_selection_is_a_park_not_a_silent_default(self):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_model import resolve_egpu_model
|
||||
assert resolve_egpu_model(self._params("ghost", self._doc()), allow_refresh=False) is None
|
||||
|
||||
def test_bench_model_is_not_selectable(self):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_model import resolve_egpu_model
|
||||
assert resolve_egpu_model(self._params("comma_small", self._doc()), allow_refresh=False) is None
|
||||
|
||||
def test_registry_carries_no_model_list(self):
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_model import EGPU_MODELS
|
||||
assert set(EGPU_MODELS) == {"lebrowski", "comma_small"}
|
||||
|
||||
|
||||
class TestConsentAndIntegrity:
|
||||
def test_disabled_param_denies_present_dock(self, monkeypatch):
|
||||
from iqpilot.selfdrive.iqmodeld import egpu_helpers
|
||||
monkeypatch.setattr(egpu_helpers, "usbgpu_present", lambda sysfs_root=egpu_helpers.USB_SYSFS_ROOT: True)
|
||||
assert egpu_helpers.egpu_present_consented(FakeParams()) is True
|
||||
assert egpu_helpers.egpu_present_consented(FakeParams(IQEgpuDisabled=True)) is False
|
||||
|
||||
def test_local_onnx_quarantines_bad_content(self, tmp_path, monkeypatch):
|
||||
import hashlib
|
||||
from iqpilot.selfdrive.iqmodeld import egpu_helpers
|
||||
onnx = tmp_path / "m.onnx"
|
||||
onnx.write_bytes(b"good")
|
||||
meta = {"sha256": hashlib.sha256(b"good").hexdigest(), "download": {"size": 4}}
|
||||
monkeypatch.setattr(egpu_helpers, "onnx_cache_path", lambda m: str(onnx))
|
||||
assert egpu_helpers.local_onnx(meta) == str(onnx)
|
||||
onnx.write_bytes(b"bad!")
|
||||
assert egpu_helpers.local_onnx(meta) is None
|
||||
assert not onnx.exists()
|
||||
assert (tmp_path / "m.onnx.unusable").exists()
|
||||
|
||||
|
||||
class TestEgpuDockStatus:
|
||||
def _run(self, seq):
|
||||
from iqpilot.system.hardware.egpu_dock.status import EgpuDockStatus
|
||||
st = EgpuDockStatus()
|
||||
fired = {}
|
||||
def set_alert(name, cond, extra=None):
|
||||
fired[name] = (bool(cond), extra)
|
||||
for args in seq:
|
||||
st.update(*args, set_alert)
|
||||
return {k: v for k, v in fired.items() if v[0]}
|
||||
|
||||
def _dock(self, speed=10000, product="custom ed4e39b7-CLEAN"):
|
||||
return [{"vendorId": 0xADD1, "productId": 0x0001, "product": product, "speedMbps": speed}]
|
||||
|
||||
def test_no_dock_no_alerts(self):
|
||||
assert self._run([(True, [], False, False, None, True, None)]) == {}
|
||||
|
||||
def test_usb2_dock_warns_slow(self):
|
||||
fired = self._run([(True, self._dock(speed=480), False, False, None, True, None)])
|
||||
assert fired.get("Offroad_EgpuUsbSlow") == (True, "480 Mbps")
|
||||
|
||||
def test_power_fault_reports_pcie_unavailable(self):
|
||||
class St:
|
||||
supplyFault = True
|
||||
supplyVoltage = 0
|
||||
pcieLtssm = 0x78
|
||||
tempC = memoryTempC = 40.0
|
||||
fanSpeedRpm = 1500
|
||||
d = self._dock()
|
||||
fired = self._run([
|
||||
(True, d, False, False, None, True, None),
|
||||
(False, d, False, True, None, True, None),
|
||||
(False, d, False, False, b"1", True, St()),
|
||||
])
|
||||
assert "Offroad_EgpuPcieUnavailable" in fired
|
||||
111
iqpilot/selfdrive/iqmodeld/tests/test_emac_input_state.py
Normal file
111
iqpilot/selfdrive/iqmodeld/tests/test_emac_input_state.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
os.environ.setdefault("DEV", "CPU")
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.emac_input_state import EmacInputState
|
||||
from iqpilot.selfdrive.iqmodeld.emac_model_meta import FRAME_SKIP, OUTPUT_LEN, OUTPUT_SLICES
|
||||
from iqpilot.selfdrive.iqmodeld.temporal_state import MODEL_INPUT_SPEC as INPUT_SPEC
|
||||
|
||||
N_FRAMES_TEST = 30
|
||||
IMG_SHAPE = INPUT_SPEC["img"][0]
|
||||
DESIRE_LEN = INPUT_SPEC["desire_pulse"][0][2]
|
||||
|
||||
|
||||
class _CaptureRunner:
|
||||
|
||||
def __init__(self):
|
||||
self.captured: dict[str, np.ndarray] | None = None
|
||||
|
||||
def __call__(self, inputs):
|
||||
from tinygrad import Tensor
|
||||
self.captured = {k: v.numpy().copy() for k, v in inputs.items()}
|
||||
return {"outputs": Tensor(np.zeros((1, OUTPUT_LEN), dtype=np.float32))}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def reference():
|
||||
from iqpilot.selfdrive.iqmodeld.tools.compile_supercombo import (
|
||||
POLICY_INPUTS, make_input_queues, make_run_policy,
|
||||
)
|
||||
|
||||
input_shapes = {name: shape for name, (shape, _) in INPUT_SPEC.items()}
|
||||
metadata = {"input_shapes": input_shapes}
|
||||
capture = _CaptureRunner()
|
||||
run_policy = make_run_policy(capture, metadata, FRAME_SKIP)
|
||||
queues, npy = make_input_queues(input_shapes, FRAME_SKIP, device="CPU")
|
||||
return run_policy, queues, npy, capture, POLICY_INPUTS
|
||||
|
||||
|
||||
def _rising_edge(raw_desire: np.ndarray, prev: np.ndarray) -> np.ndarray:
|
||||
cur = raw_desire.astype(np.float32).copy()
|
||||
cur[0] = 0
|
||||
pulse = np.where(cur - prev > 0.99, cur, 0).astype(np.float32)
|
||||
prev[:] = cur
|
||||
return pulse
|
||||
|
||||
|
||||
def test_materialized_inputs_match_tinygrad_reference(reference):
|
||||
from tinygrad import Tensor
|
||||
|
||||
run_policy, queues, npy, capture, policy_inputs = reference
|
||||
rng = np.random.default_rng(1234)
|
||||
state = EmacInputState(FRAME_SKIP)
|
||||
ref_prev_desire = np.zeros(DESIRE_LEN, dtype=np.float32)
|
||||
|
||||
hidden = np.zeros((1, 512), dtype=np.float32)
|
||||
for frame in range(N_FRAMES_TEST):
|
||||
warped = rng.integers(0, 256, (2, 6, IMG_SHAPE[2], IMG_SHAPE[3]), dtype=np.int64).astype(np.uint8)
|
||||
raw_desire = np.zeros(DESIRE_LEN, dtype=np.float32)
|
||||
if frame % 3:
|
||||
raw_desire[int(rng.integers(0, DESIRE_LEN))] = 1.0
|
||||
traffic = rng.standard_normal(2).astype(np.float32)
|
||||
action_t = rng.standard_normal(2).astype(np.float32)
|
||||
|
||||
npy["desire"][:] = _rising_edge(raw_desire, ref_prev_desire)
|
||||
npy["traffic_convention"][:] = traffic
|
||||
npy["action_t"][:] = action_t
|
||||
npy["prev_feat"][:] = hidden
|
||||
run_policy(warped=Tensor(warped), **{k: queues[k] for k in policy_inputs})
|
||||
ref_inputs = capture.captured
|
||||
|
||||
state.prev_feat[:] = hidden
|
||||
mat = state.push_and_materialize(warped, raw_desire, traffic, action_t)
|
||||
|
||||
for name in INPUT_SPEC:
|
||||
assert ref_inputs[name].shape == tuple(INPUT_SPEC[name][0]), name
|
||||
np.testing.assert_array_equal(
|
||||
mat[name].astype(ref_inputs[name].dtype), ref_inputs[name],
|
||||
err_msg=f"frame {frame}: materialized {name} diverges from tinygrad reference")
|
||||
|
||||
fake_output = rng.standard_normal(OUTPUT_LEN).astype(np.float32)
|
||||
state.note_hidden_state(fake_output, OUTPUT_SLICES["hidden_state"])
|
||||
hidden = fake_output[OUTPUT_SLICES["hidden_state"]].reshape(1, 512).copy()
|
||||
|
||||
|
||||
def test_note_hidden_state_slice():
|
||||
state = EmacInputState(FRAME_SKIP)
|
||||
out = np.arange(OUTPUT_LEN, dtype=np.float32)
|
||||
state.note_hidden_state(out, OUTPUT_SLICES["hidden_state"])
|
||||
np.testing.assert_array_equal(state.prev_feat.reshape(-1), out[OUTPUT_SLICES["hidden_state"]])
|
||||
|
||||
|
||||
def test_desire_pulse_rising_edge_only_once():
|
||||
state = EmacInputState(FRAME_SKIP)
|
||||
held = np.zeros(DESIRE_LEN, dtype=np.float32)
|
||||
held[3] = 1.0
|
||||
warped = np.zeros((2, 6, IMG_SHAPE[2], IMG_SHAPE[3]), dtype=np.uint8)
|
||||
zeros2 = np.zeros(2, dtype=np.float32)
|
||||
|
||||
first = state.push_and_materialize(warped, held, zeros2, zeros2)
|
||||
assert first["desire_pulse"][0, -1, 3] == 1.0
|
||||
second = state.push_and_materialize(warped, held, zeros2, zeros2)
|
||||
assert state.desire_q[-1].max() == 0.0
|
||||
assert second["desire_pulse"][0, -1, 3] == 1.0
|
||||
@@ -34,6 +34,7 @@ def _daemon(params, steer_control_type):
|
||||
return SimpleNamespace(
|
||||
_params=params,
|
||||
_car_params=car_params,
|
||||
_channel=None,
|
||||
_sub={"lateralDelay": SimpleNamespace(lateralDelay=LIVE_DELAY)},
|
||||
_runtime=SimpleNamespace(lat_delay=None, PLANPLUS_CONTROL=None, model_smoothing_max_extra_sec=None),
|
||||
_warps=SimpleNamespace(set_offset=lambda _: None),
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
|
||||
The eMac bundles ship `minimum_selector_version = 17`, and the version
|
||||
gate lives in the COMPILED private selector bundle, not in this repo. If that
|
||||
bundle is rebuilt from stale source the gate still reads 16, every eMac bundle
|
||||
is silently dropped as "too new", and the selector simply shows no eMac models
|
||||
— with no error anywhere. Assert the effective gate instead, so a stale
|
||||
private bundle fails here rather than on a device.
|
||||
"""
|
||||
from iqpilot.selfdrive.iqmodeld.emac_model_meta import EMAC_BUNDLE_MIN_SELECTOR_VERSION
|
||||
from iqpilot.selfdrive.iqmodeld.models.helpers import is_bundle_version_compatible
|
||||
|
||||
|
||||
def test_gate_accepts_the_version_our_emac_bundles_ship():
|
||||
assert is_bundle_version_compatible({"minimumSelectorVersion": EMAC_BUNDLE_MIN_SELECTOR_VERSION}), (
|
||||
f"the effective selector gate rejects minimumSelectorVersion="
|
||||
f"{EMAC_BUNDLE_MIN_SELECTOR_VERSION}; the private selector bundle is stale. "
|
||||
f"Rebuild it from BOTH iqpilot/models_private_src/helpers.py "
|
||||
f"(CURRENT_SELECTOR_VERSION) and fetcher.py (MANIFEST_VERSION)."
|
||||
)
|
||||
|
||||
|
||||
def test_gate_still_accepts_older_bundles():
|
||||
# the window is a range, not a floor: bumping it must not orphan the existing catalogue
|
||||
assert is_bundle_version_compatible({"minimumSelectorVersion": 12})
|
||||
assert is_bundle_version_compatible({"minimumSelectorVersion": 16})
|
||||
|
||||
|
||||
def test_gate_rejects_a_bundle_from_the_future():
|
||||
assert not is_bundle_version_compatible({"minimumSelectorVersion": EMAC_BUNDLE_MIN_SELECTOR_VERSION + 5})
|
||||
131
iqpilot/selfdrive/iqmodeld/tests/test_split_input_state.py
Normal file
131
iqpilot/selfdrive/iqmodeld/tests/test_split_input_state.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
|
||||
eMac split-model "prepared input equivalence": SplitInputState must reproduce,
|
||||
byte-exact, the queue semantics of compile_split_runtime's execute_bundle —
|
||||
the real tinygrad reference graph run on CPU with stub vision/policy runners,
|
||||
over a multi-frame random sequence with desire rising edges.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
os.environ.setdefault("DEV", "CPU")
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.emac_input_state import EmacInputState, SplitInputState
|
||||
|
||||
N_FRAMES_TEST = 30
|
||||
FRAME_SKIP = 4
|
||||
IMG_SHAPE = (1, 12, 16, 32) # small spatial dims: queue math is shape-generic
|
||||
FB_SHAPE = (1, 25, 512)
|
||||
DP_SHAPE = (1, 25, 8)
|
||||
VISION_OUT_LEN = 1576
|
||||
HIDDEN_SLICE = slice(1064, 1576)
|
||||
|
||||
VISION_SHAPES = {"img": IMG_SHAPE, "big_img": IMG_SHAPE}
|
||||
POLICY_SHAPES = {"desire_pulse": DP_SHAPE, "traffic_convention": (1, 2), "features_buffer": FB_SHAPE}
|
||||
|
||||
|
||||
class _StubRunner:
|
||||
"""Stands in for OnnxRunner inside execute_bundle: returns a preset output
|
||||
and records the materialized inputs it was fed."""
|
||||
|
||||
def __init__(self, out_len: int):
|
||||
self.out_len = out_len
|
||||
self.next_output: np.ndarray | None = None
|
||||
self.captured: dict[str, np.ndarray] | None = None
|
||||
|
||||
def __call__(self, inputs):
|
||||
from tinygrad import Tensor
|
||||
self.captured = {k: v.numpy().copy() for k, v in inputs.items()}
|
||||
out = self.next_output if self.next_output is not None else np.zeros((1, self.out_len), dtype=np.float32)
|
||||
return {"outputs": Tensor(out.astype(np.float32))}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def reference():
|
||||
from tinygrad import Tensor
|
||||
from iqpilot.selfdrive.iqmodeld.tools.compile_split_runtime import _role_executor
|
||||
|
||||
meta_by_role = {
|
||||
"vision": {"input_shapes": dict(VISION_SHAPES), "output_slices": {"hidden_state": HIDDEN_SLICE}},
|
||||
"policy": {"input_shapes": dict(POLICY_SHAPES), "output_slices": {}},
|
||||
}
|
||||
vision, policy = _StubRunner(VISION_OUT_LEN), _StubRunner(1000)
|
||||
execute_bundle = _role_executor({"vision": vision, "policy": policy}, meta_by_role, FRAME_SKIP)
|
||||
|
||||
feat_q = Tensor(np.zeros((FRAME_SKIP * (FB_SHAPE[1] - 1) + 1, FB_SHAPE[0], FB_SHAPE[2]), dtype=np.float32),
|
||||
device="CPU").contiguous().realize()
|
||||
desire_q = Tensor(np.zeros((FRAME_SKIP * DP_SHAPE[1], DP_SHAPE[0], DP_SHAPE[2]), dtype=np.float32),
|
||||
device="CPU").contiguous().realize()
|
||||
return execute_bundle, feat_q, desire_q, vision, policy
|
||||
|
||||
|
||||
def test_split_inputs_match_tinygrad_reference(reference):
|
||||
from tinygrad import Tensor
|
||||
|
||||
execute_bundle, feat_q, desire_q, vision_stub, policy_stub = reference
|
||||
rng = np.random.default_rng(4321)
|
||||
state = SplitInputState(FRAME_SKIP, IMG_SHAPE, FB_SHAPE, DP_SHAPE)
|
||||
ref_prev_desire = np.zeros(DP_SHAPE[2], dtype=np.float32)
|
||||
|
||||
for frame in range(N_FRAMES_TEST):
|
||||
warped = rng.integers(0, 256, (2, 6, IMG_SHAPE[2], IMG_SHAPE[3]), dtype=np.int64).astype(np.uint8)
|
||||
raw_desire = np.zeros(DP_SHAPE[2], dtype=np.float32)
|
||||
if frame % 3:
|
||||
raw_desire[int(rng.integers(0, DP_SHAPE[2]))] = 1.0
|
||||
traffic = rng.standard_normal((1, 2)).astype(np.float32)
|
||||
vision_out = rng.standard_normal((1, VISION_OUT_LEN)).astype(np.float32)
|
||||
vision_stub.next_output = vision_out
|
||||
|
||||
# --- ours ---
|
||||
vis_inputs = state.materialize_vision(warped, raw_desire)
|
||||
pol_inputs = state.materialize_policy(vision_out[0, HIDDEN_SLICE], traffic[0])
|
||||
|
||||
# --- reference graph: rising edge happens outside execute_bundle (run_fused) ---
|
||||
cur = raw_desire.copy()
|
||||
cur[0] = 0
|
||||
ref_pulse = np.where(cur - ref_prev_desire > 0.99, cur, 0).astype(np.float32)
|
||||
ref_prev_desire[:] = cur
|
||||
|
||||
execute_bundle(
|
||||
img=Tensor(vis_inputs["img"], device="CPU").realize(),
|
||||
big_img=Tensor(vis_inputs["big_img"], device="CPU").realize(),
|
||||
feat_q=feat_q, desire_q=desire_q,
|
||||
desire=Tensor(ref_pulse, device="CPU").realize(),
|
||||
traffic_convention=Tensor(traffic, device="CPU").realize(),
|
||||
action_t=Tensor(np.zeros((1, 2), dtype=np.float32), device="CPU").realize(),
|
||||
)
|
||||
ref = policy_stub.captured
|
||||
assert ref is not None
|
||||
|
||||
assert ref["features_buffer"].tobytes() == pol_inputs["features_buffer"].tobytes(), f"features frame {frame}"
|
||||
assert ref["desire_pulse"].tobytes() == pol_inputs["desire_pulse"].tobytes(), f"desire frame {frame}"
|
||||
assert ref["traffic_convention"].tobytes() == pol_inputs["traffic_convention"].tobytes()
|
||||
# vision saw exactly what our img queues materialized
|
||||
vref = vision_stub.captured
|
||||
assert vref["img"].tobytes() == vis_inputs["img"].tobytes(), f"img frame {frame}"
|
||||
assert vref["big_img"].tobytes() == vis_inputs["big_img"].tobytes(), f"big_img frame {frame}"
|
||||
|
||||
|
||||
def test_split_img_queue_matches_fused_state():
|
||||
# img/desire mechanics are shared with the fused mirror: same warps must
|
||||
# materialize identical img/big_img in both states
|
||||
rng = np.random.default_rng(7)
|
||||
fused_spec = {
|
||||
"img": (IMG_SHAPE, "uint8"), "big_img": (IMG_SHAPE, "uint8"),
|
||||
"desire_pulse": (DP_SHAPE, "float32"), "traffic_convention": ((1, 2), "float32"),
|
||||
"features_buffer": ((1, 24, 512), "float32"), "action_t": ((1, 2), "float32"),
|
||||
}
|
||||
fused = EmacInputState(FRAME_SKIP, fused_spec)
|
||||
split = SplitInputState(FRAME_SKIP, IMG_SHAPE, FB_SHAPE, DP_SHAPE)
|
||||
for _ in range(12):
|
||||
warped = rng.integers(0, 256, (2, 6, IMG_SHAPE[2], IMG_SHAPE[3]), dtype=np.int64).astype(np.uint8)
|
||||
desire = np.zeros(DP_SHAPE[2], dtype=np.float32)
|
||||
f = fused.push_and_materialize(warped, desire, np.zeros(2, dtype=np.float32), np.zeros(2, dtype=np.float32))
|
||||
s = split.materialize_vision(warped, desire)
|
||||
assert f["img"].tobytes() == s["img"].tobytes()
|
||||
assert f["big_img"].tobytes() == s["big_img"].tobytes()
|
||||
Reference in New Issue
Block a user