IQ.Pilot Prebuilt Release @ 27f668a
This commit is contained in:
3
iqpilot/selfdrive/iqmodeld/tests/__init__.py
Normal file
3
iqpilot/selfdrive/iqmodeld/tests/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
81
iqpilot/selfdrive/iqmodeld/tests/test_action_dispatch.py
Normal file
81
iqpilot/selfdrive/iqmodeld/tests/test_action_dispatch.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from iqpilot.cereal import log
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.config import Plan
|
||||
from iqpilot.selfdrive.iqmodeld.daemon import NeuralEngineState, _merged_plan
|
||||
import iqpilot.selfdrive.iqmodeld.daemon as iqmodeld_daemon
|
||||
from iqpilot.selfdrive.controls.lib.drive_helpers import smooth_value
|
||||
|
||||
|
||||
def _fake_state(**overrides):
|
||||
base = dict(
|
||||
PLANPLUS_CONTROL=1.0,
|
||||
LONG_SMOOTH_SECONDS=0.3,
|
||||
LAT_SMOOTH_SECONDS=0.1,
|
||||
MIN_LAT_CONTROL_SPEED=0.3,
|
||||
mlsim=True,
|
||||
generation=12,
|
||||
constants=SimpleNamespace(T_IDXS=np.arange(100), DESIRE_LEN=8),
|
||||
)
|
||||
base.update(overrides)
|
||||
return SimpleNamespace(**base)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("control", "vego", "factor"),
|
||||
[
|
||||
(0.55, 20.0, 1.0),
|
||||
(1.0, 25.0, 0.75),
|
||||
(1.5, 25.1, 0.75),
|
||||
(2.0, 20.0, 1.0),
|
||||
],
|
||||
)
|
||||
def test_planplus_merge_matches_speed_gate(control: float, vego: float, factor: float):
|
||||
state = _fake_state(PLANPLUS_CONTROL=control)
|
||||
base = np.random.rand(1, 100, 15).astype(np.float32)
|
||||
extra = np.random.rand(1, 100, 15).astype(np.float32)
|
||||
merged = _merged_plan(state, {"plan": base, "planplus": extra}, vego)
|
||||
expected = base[0] + (control * factor) * extra[0]
|
||||
np.testing.assert_allclose(merged, expected, rtol=1e-6, atol=1e-6)
|
||||
|
||||
|
||||
def test_action_dispatch_uses_merged_plan_for_longitudinal_choice(monkeypatch: pytest.MonkeyPatch):
|
||||
state = _fake_state()
|
||||
previous = log.ModelDataV2.Action()
|
||||
recorded_velocity: list[np.ndarray] = []
|
||||
|
||||
def fake_accel(plan_vel, plan_accel, t_idxs, action_t=0.0):
|
||||
recorded_velocity.append(plan_vel.copy())
|
||||
return 0.0, False
|
||||
|
||||
monkeypatch.setattr(iqmodeld_daemon, "get_accel_from_plan", fake_accel)
|
||||
monkeypatch.setattr(iqmodeld_daemon, "pick_curvature", lambda *args: 0.0)
|
||||
|
||||
plan = np.random.rand(1, 100, 15).astype(np.float32)
|
||||
planplus = np.random.rand(1, 100, 15).astype(np.float32)
|
||||
outputs = {"plan": plan.copy(), "planplus": planplus.copy()}
|
||||
|
||||
NeuralEngineState.get_action_from_model(state, outputs, previous, 0.0, 0.0, 25.0)
|
||||
expected = plan[0, :, Plan.VELOCITY][:, 0] + 0.75 * planplus[0, :, Plan.VELOCITY][:, 0]
|
||||
np.testing.assert_allclose(recorded_velocity[0], expected, rtol=1e-5, atol=1e-6)
|
||||
|
||||
|
||||
def test_action_dispatch_honors_direct_action_outputs():
|
||||
state = _fake_state(mlsim=False, generation=9)
|
||||
previous = log.ModelDataV2.Action(desiredCurvature=0.0, desiredAcceleration=0.0, shouldStop=False)
|
||||
outputs = {"action": np.array([[4.0, -0.25]], dtype=np.float32)}
|
||||
action = NeuralEngineState.get_action_from_model(state, outputs, previous, 0.0, 0.0, 10.0)
|
||||
expected_accel = smooth_value(-0.25, previous.desiredAcceleration, state.LONG_SMOOTH_SECONDS)
|
||||
expected_curvature = smooth_value(0.04, previous.desiredCurvature, state.LAT_SMOOTH_SECONDS)
|
||||
assert action.desiredAcceleration == pytest.approx(expected_accel)
|
||||
assert action.desiredCurvature == pytest.approx(expected_curvature)
|
||||
assert action.shouldStop is False
|
||||
188
iqpilot/selfdrive/iqmodeld/tests/test_combined_split_runner.py
Normal file
188
iqpilot/selfdrive/iqmodeld/tests/test_combined_split_runner.py
Normal file
@@ -0,0 +1,188 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.models.combined_artifact import resolve_combined_split_artifact
|
||||
import iqpilot.selfdrive.iqmodeld.models.runners.model_runner as runner_helpers
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad import tinygrad_runner as tinygrad_runner_mod
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.combined_split_runner import TinygradCombinedSplitRunner
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad import combined_split_runner as combined_runner_mod
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelType
|
||||
from iqpilot.selfdrive.iqmodeld.parser import PhaseParser
|
||||
from iqpilot.selfdrive.iqmodeld.tests.test_iqmodeld_contracts import _phase_sample
|
||||
|
||||
|
||||
@dataclass
|
||||
class _TypeWrap:
|
||||
raw: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Artifact:
|
||||
fileName: str
|
||||
|
||||
|
||||
class _Model:
|
||||
def __init__(self, model_type: int, artifact_name: str):
|
||||
self.type = _TypeWrap(model_type)
|
||||
self.artifact = _Artifact(artifact_name)
|
||||
|
||||
|
||||
class _Override:
|
||||
def __init__(self, key: str, value: str):
|
||||
self.key = key
|
||||
self.value = value
|
||||
|
||||
|
||||
class _Bundle:
|
||||
def __init__(self, models: list[_Model], overrides: list[_Override] | None = None, generation: int = 10):
|
||||
self.models = models
|
||||
self.overrides = overrides or []
|
||||
self.generation = generation
|
||||
|
||||
|
||||
class _FakeTensor:
|
||||
def __init__(self, values):
|
||||
self._values = np.asarray(values, dtype=np.float32)
|
||||
|
||||
def numpy(self):
|
||||
return self._values
|
||||
|
||||
|
||||
class _FakeVisionBuf:
|
||||
width = 1928
|
||||
height = 1208
|
||||
data = memoryview(b"\x00" * 64)
|
||||
|
||||
|
||||
def _slice_pack(outputs: dict[str, np.ndarray]) -> tuple[np.ndarray, dict[str, slice]]:
|
||||
chunks = []
|
||||
slices: dict[str, slice] = {}
|
||||
cursor = 0
|
||||
for name, value in outputs.items():
|
||||
flat = value.reshape(-1)
|
||||
slices[name] = slice(cursor, cursor + flat.size)
|
||||
chunks.append(flat)
|
||||
cursor += flat.size
|
||||
return np.concatenate(chunks).astype(np.float32), slices
|
||||
|
||||
|
||||
def test_resolve_combined_split_artifact_prefers_override(tmp_path: Path, monkeypatch):
|
||||
bundle = _Bundle(
|
||||
[_Model(ModelType.vision, "driving_vision_demo_tinygrad.pkl"), _Model(ModelType.policy, "driving_policy_demo_tinygrad.pkl")],
|
||||
overrides=[_Override("combinedRuntimeArtifact", "driving_combined_demo.pkl")],
|
||||
)
|
||||
expected = tmp_path / "driving_combined_demo.pkl"
|
||||
expected.write_bytes(b"iq")
|
||||
|
||||
monkeypatch.setattr("iqpilot.selfdrive.iqmodeld.models.combined_artifact._MODEL_ROOT", tmp_path)
|
||||
|
||||
assert resolve_combined_split_artifact(bundle) == expected
|
||||
|
||||
|
||||
def test_get_model_runner_prefers_combined_split_artifact(monkeypatch):
|
||||
bundle = _Bundle([
|
||||
_Model(ModelType.vision, "driving_vision_demo_tinygrad.pkl"),
|
||||
_Model(ModelType.policy, "driving_policy_demo_tinygrad.pkl"),
|
||||
], generation=11)
|
||||
|
||||
marker = object()
|
||||
monkeypatch.setattr(runner_helpers, "_fetch_bundle", lambda: bundle)
|
||||
monkeypatch.setattr(runner_helpers, "has_combined_split_artifact", lambda _: True)
|
||||
monkeypatch.setattr(combined_runner_mod, "TinygradCombinedSplitRunner", lambda: marker)
|
||||
|
||||
assert runner_helpers.get_model_runner() is marker
|
||||
|
||||
|
||||
def test_get_model_runner_keeps_split_bundle_on_existing_runner_without_combined_artifact(monkeypatch):
|
||||
bundle = _Bundle([
|
||||
_Model(ModelType.vision, "driving_vision_demo_tinygrad.pkl"),
|
||||
_Model(ModelType.policy, "driving_policy_demo_tinygrad.pkl"),
|
||||
], generation=12)
|
||||
|
||||
marker = object()
|
||||
monkeypatch.setattr(runner_helpers, "_fetch_bundle", lambda: bundle)
|
||||
monkeypatch.setattr(runner_helpers, "has_combined_split_artifact", lambda _: False)
|
||||
monkeypatch.setattr(tinygrad_runner_mod, "TinygradSplitRunner", lambda: marker)
|
||||
|
||||
assert runner_helpers.get_model_runner() is marker
|
||||
|
||||
|
||||
def test_combined_split_runner_parses_single_policy_payload(monkeypatch):
|
||||
vision_raw = _phase_sample(np.random.default_rng(11))
|
||||
policy_raw = _phase_sample(np.random.default_rng(17))
|
||||
vision_blob, vision_slices = _slice_pack(vision_raw)
|
||||
policy_blob, policy_slices = _slice_pack(policy_raw)
|
||||
|
||||
runner = TinygradCombinedSplitRunner.__new__(TinygradCombinedSplitRunner)
|
||||
runner._vision_meta = {
|
||||
"input_shapes": {"img": (1, 12, 128, 256), "big_img": (1, 12, 128, 256)},
|
||||
"output_slices": vision_slices,
|
||||
}
|
||||
runner._meta_by_role = {
|
||||
"vision": runner._vision_meta,
|
||||
"policy": {
|
||||
"input_shapes": {
|
||||
"features_buffer": (1, 25, 512),
|
||||
"desire_pulse": (1, 25, 8),
|
||||
"traffic_convention": (1, 2),
|
||||
"action_t": (1, 2),
|
||||
},
|
||||
"output_slices": policy_slices,
|
||||
},
|
||||
}
|
||||
runner._policy_roles = ["policy"]
|
||||
runner._desired_key = "desire_pulse"
|
||||
runner._road_key = "img"
|
||||
runner._wide_key = "big_img"
|
||||
runner._extra_policy_keys = []
|
||||
runner._queue_tensors = {
|
||||
"img_q": object(),
|
||||
"big_img_q": object(),
|
||||
"feat_q": object(),
|
||||
"desire_q": object(),
|
||||
"tfm": object(),
|
||||
"big_tfm": object(),
|
||||
"desire": object(),
|
||||
"traffic_convention": object(),
|
||||
"action_t": object(),
|
||||
}
|
||||
runner._numpy_state = {
|
||||
"tfm": np.zeros((3, 3), dtype=np.float32),
|
||||
"big_tfm": np.zeros((3, 3), dtype=np.float32),
|
||||
"desire": np.zeros(8, dtype=np.float32),
|
||||
"traffic_convention": np.zeros((1, 2), dtype=np.float32),
|
||||
"action_t": np.zeros((1, 2), dtype=np.float32),
|
||||
}
|
||||
runner._camera_shape = (1928, 1208)
|
||||
runner._camera_programs = {
|
||||
(1928, 1208): {"stage_inputs": lambda **kwargs: ("road", "wide")},
|
||||
}
|
||||
runner._execute_bundle = lambda **kwargs: (_FakeTensor(vision_blob), _FakeTensor(policy_blob))
|
||||
runner._parser = PhaseParser()
|
||||
runner._last_desire = np.zeros(8, dtype=np.float32)
|
||||
runner._blob_cache = {}
|
||||
|
||||
monkeypatch.setattr(TinygradCombinedSplitRunner, "_allocate_runtime_state", lambda self, w, h: None)
|
||||
monkeypatch.setattr(TinygradCombinedSplitRunner, "_frame_blob", lambda self, name, buf: object())
|
||||
|
||||
outputs = runner.run_fused(
|
||||
{"img": _FakeVisionBuf(), "big_img": _FakeVisionBuf()},
|
||||
{"img": np.eye(3, dtype=np.float32), "big_img": np.eye(3, dtype=np.float32)},
|
||||
{
|
||||
"desire_pulse": np.array([1, 0, 0, 0, 0, 0, 0, 0], dtype=np.float32),
|
||||
"traffic_convention": np.zeros((1, 2), dtype=np.float32),
|
||||
"action_t": np.zeros((1, 2), dtype=np.float32),
|
||||
},
|
||||
)
|
||||
|
||||
assert "pose" in outputs
|
||||
assert "plan" in outputs
|
||||
assert outputs["plan"].shape == (1, 33, 15)
|
||||
assert outputs["action"].shape == (1, 2)
|
||||
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.tools.compile_supercombo import (
|
||||
_captured_devices,
|
||||
_validate_pose_outputs,
|
||||
)
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.supercombo_runner import _captured_queue_depth
|
||||
|
||||
|
||||
class _Captured:
|
||||
def __init__(self, expected_input_info):
|
||||
self.expected_input_info = expected_input_info
|
||||
|
||||
|
||||
class _FakeJit:
|
||||
def __init__(self, expected_input_info):
|
||||
self.captured = _Captured(expected_input_info)
|
||||
|
||||
|
||||
def test_captured_queue_helpers_extract_depth_and_device():
|
||||
infos = [
|
||||
("noop", (), "uchar", "QCOM"),
|
||||
("reshape(arg=None, src=(noop, stack(arg=None, src=(const(arg=5), const(arg=6), const(arg=128), const(arg=256)))))", (), "uchar", "QCOM"),
|
||||
("reshape(arg=None, src=(noop, const(arg=3)))", (), "float", "NPY"),
|
||||
]
|
||||
fake_jit = _FakeJit(infos)
|
||||
|
||||
assert _captured_queue_depth(fake_jit) == 5
|
||||
assert _captured_devices(fake_jit) == {"QCOM", "NPY"}
|
||||
|
||||
|
||||
def test_validate_pose_outputs_accepts_sane_odometry_payload():
|
||||
outputs = {
|
||||
"pose": np.array([[1.0, 0.5, 0.25, 0.1, 0.2, 0.3]], dtype=np.float32),
|
||||
"pose_stds": np.array([[0.5, 0.4, 0.3, 0.2, 0.2, 0.2]], dtype=np.float32),
|
||||
"wide_from_device_euler": np.array([[0.1, 0.2, 0.3]], dtype=np.float32),
|
||||
"wide_from_device_euler_stds": np.array([[0.2, 0.2, 0.2]], dtype=np.float32),
|
||||
"road_transform": np.array([[0.5, 0.4, 0.3, 0.2, 0.1, 0.0]], dtype=np.float32),
|
||||
"road_transform_stds": np.array([[0.3, 0.3, 0.3, 0.2, 0.2, 0.2]], dtype=np.float32),
|
||||
}
|
||||
|
||||
_validate_pose_outputs(outputs)
|
||||
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld import egpu_helpers as eh
|
||||
|
||||
|
||||
def test_fetch_fw_mirrors_and_serves_offline(tmp_path, monkeypatch):
|
||||
from tinygrad import helpers
|
||||
blob = os.urandom(4096)
|
||||
sha = hashlib.sha256(blob).hexdigest()
|
||||
calls = []
|
||||
|
||||
def orig(path, name, sha256):
|
||||
calls.append((path, name))
|
||||
return blob
|
||||
|
||||
monkeypatch.setattr(helpers, "fetch_fw", orig, raising=False)
|
||||
helpers.fetch_fw._iq_patched = False
|
||||
monkeypatch.setattr(eh, "FIRMWARE_MIRROR", str(tmp_path / "mirror"))
|
||||
eh.patch_tinygrad_fetch_fw()
|
||||
assert helpers.fetch_fw("amdgpu", "gc.bin", sha) == blob and calls == [("amdgpu", "gc.bin")]
|
||||
mirrored = tmp_path / "mirror" / "amdgpu" / "gc.bin"
|
||||
assert mirrored.read_bytes() == blob
|
||||
assert helpers.fetch_fw("amdgpu", "gc.bin", sha) == blob and len(calls) == 1
|
||||
mirrored.write_bytes(b"corrupt")
|
||||
assert helpers.fetch_fw("amdgpu", "gc.bin", sha) == blob and len(calls) == 2
|
||||
assert mirrored.read_bytes() == blob
|
||||
34
iqpilot/selfdrive/iqmodeld/tests/test_egpu_host_mock.py
Normal file
34
iqpilot/selfdrive/iqmodeld/tests/test_egpu_host_mock.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.tools.egpu_host_mock import tinygrad_tree
|
||||
|
||||
PROBE = """
|
||||
import os
|
||||
os.environ["JIT_BATCH_SIZE"] = "0"
|
||||
from iqpilot.selfdrive.iqmodeld.tools.egpu_host_mock import activate
|
||||
activate("gfx1200")
|
||||
from tinygrad import Tensor
|
||||
from tinygrad.device import Device
|
||||
from tinygrad.engine.jit import TinyJit
|
||||
dev = Device["AMD"]
|
||||
assert dev.arch == "gfx1200", dev.arch
|
||||
assert type(dev.iface).__name__ == "MOCKUSBIface", type(dev.iface).__name__
|
||||
run = TinyJit(lambda x: (x * 2 + 1).sum(axis=1).realize())
|
||||
for i in range(3):
|
||||
run(Tensor.ones(64, 64, device="AMD") * i)
|
||||
print("MOCK_OK")
|
||||
"""
|
||||
|
||||
|
||||
@pytest.mark.skipif(not os.path.isdir(os.path.join(tinygrad_tree(), "test", "mockgpu")), reason="tinygrad mockgpu tree not checked out")
|
||||
def test_mock_dock_captures_a_jit_without_hardware():
|
||||
out = subprocess.run([sys.executable, "-c", PROBE], capture_output=True, text=True, timeout=600)
|
||||
assert out.returncode == 0, out.stderr[-2000:]
|
||||
assert "MOCK_OK" in out.stdout
|
||||
62
iqpilot/selfdrive/iqmodeld/tests/test_egpu_oob.py
Normal file
62
iqpilot/selfdrive/iqmodeld/tests/test_egpu_oob.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
import os
|
||||
import pickle
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
os.environ["DEV"] = "CPU"
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_policy import dump_oob, is_oob, load_bundle
|
||||
|
||||
|
||||
def _bundle():
|
||||
from tinygrad import Tensor
|
||||
w = Tensor(np.arange(4096, dtype=np.float32).reshape(64, 64), device="CPU").realize()
|
||||
return {"format": 2, "weights": w, "spec": {"a": ((1, 2), "float32")}, "blob": os.urandom(100_000)}
|
||||
|
||||
|
||||
def test_oob_round_trip_matches_plain_pickle(tmp_path):
|
||||
b = _bundle()
|
||||
oob = tmp_path / "b.oob"
|
||||
with open(oob, "wb") as f:
|
||||
dump_oob(b, f)
|
||||
assert is_oob(str(oob))
|
||||
got = load_bundle(str(oob))
|
||||
np.testing.assert_array_equal(got["weights"].numpy(), b["weights"].numpy())
|
||||
assert got["blob"] == b["blob"] and got["spec"] == b["spec"] and got["format"] == 2
|
||||
plain = tmp_path / "b.pkl"
|
||||
with open(plain, "wb") as f:
|
||||
pickle.dump({"x": 1, "blob": b["blob"]}, f, protocol=pickle.HIGHEST_PROTOCOL)
|
||||
assert not is_oob(str(plain))
|
||||
assert load_bundle(str(plain))["blob"] == b["blob"]
|
||||
|
||||
|
||||
def test_memory_guard_raises_when_starved(monkeypatch):
|
||||
from iqpilot.selfdrive.iqmodeld import iqegpumodeld as d
|
||||
monkeypatch.setattr(d, "_mem_available_mb", lambda: 90)
|
||||
monkeypatch.setattr(d, "MEMORY_WAIT_S", 0.0)
|
||||
with pytest.raises(RuntimeError, match="insufficient memory"):
|
||||
d._wait_for_memory(350)
|
||||
monkeypatch.setattr(d, "_mem_available_mb", lambda: 900)
|
||||
d._wait_for_memory(350)
|
||||
|
||||
|
||||
def test_opcode_rewrite_equals_oob_load(tmp_path):
|
||||
from tinygrad import Tensor
|
||||
from iqpilot.selfdrive.iqmodeld.tools.oob_rewrite import rewrite_oob
|
||||
big = Tensor(np.random.default_rng(0).standard_normal((512, 512)).astype(np.float32), device="CPU").realize()
|
||||
small = Tensor(np.arange(16, dtype=np.float32), device="CPU").realize()
|
||||
b = {"format": 2, "w": big, "s": small, "meta": {"k": "v"}, "raw": os.urandom(200_000)}
|
||||
plain = tmp_path / "plain.pkl"
|
||||
with open(plain, "wb") as f:
|
||||
pickle.dump(b, f, protocol=5)
|
||||
oob = tmp_path / "oob.pkl"
|
||||
moved, _ = rewrite_oob(str(plain), str(oob))
|
||||
assert moved >= 2 and is_oob(str(oob))
|
||||
got = load_bundle(str(oob))
|
||||
np.testing.assert_array_equal(got["w"].numpy(), b["w"].numpy())
|
||||
np.testing.assert_array_equal(got["s"].numpy(), b["s"].numpy())
|
||||
assert got["raw"] == b["raw"] and got["meta"] == {"k": "v"}
|
||||
161
iqpilot/selfdrive/iqmodeld/tests/test_egpu_policy.py
Normal file
161
iqpilot/selfdrive/iqmodeld/tests/test_egpu_policy.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
|
||||
os.environ["DEV"] = "CPU"
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_policy import PolicyRunner, make_run_policy, packed_layout, queue_shapes
|
||||
from iqpilot.selfdrive.iqmodeld.temporal_state import TemporalInputState
|
||||
|
||||
SPEC = {
|
||||
"img": ((1, 12, 8, 16), "uint8"),
|
||||
"big_img": ((1, 12, 8, 16), "uint8"),
|
||||
"desire_pulse": ((1, 25, 8), "float32"),
|
||||
"traffic_convention": ((1, 2), "float32"),
|
||||
"action_t": ((1, 2), "float32"),
|
||||
"features_buffer": ((1, 24, 512), "float32"),
|
||||
}
|
||||
FS = 4
|
||||
OUT_LEN = 2580
|
||||
HIDDEN = slice(1064, 1576)
|
||||
|
||||
|
||||
def _pack(inputs):
|
||||
from tinygrad.tensor import Tensor
|
||||
parts = [inputs[k].cast("float32").reshape(-1) for k in ("img", "big_img", "features_buffer", "desire_pulse", "traffic_convention", "action_t")]
|
||||
flat = Tensor.cat(*parts)
|
||||
hidden = (flat[:512] * 0.001).reshape(1, 512)
|
||||
return flat, hidden
|
||||
|
||||
|
||||
def _fake_model(inputs):
|
||||
from tinygrad.tensor import Tensor
|
||||
flat, hidden = _pack(inputs)
|
||||
n = flat.shape[0]
|
||||
head = flat[:min(n, HIDDEN.start)]
|
||||
out = Tensor.cat(head.pad((0, HIDDEN.start - head.shape[0])), hidden.reshape(-1), Tensor.zeros(OUT_LEN - HIDDEN.stop, device="CPU"))
|
||||
return {"outputs": out.reshape(1, -1)}
|
||||
|
||||
|
||||
class _Reference:
|
||||
def __init__(self):
|
||||
self.state = TemporalInputState(FS, SPEC)
|
||||
|
||||
def run(self, warped, desire, traffic, action_t):
|
||||
inputs = self.state.push_and_materialize(warped, desire, traffic, action_t)
|
||||
from tinygrad.tensor import Tensor
|
||||
t = {k: Tensor(np.ascontiguousarray(v), device="CPU") for k, v in inputs.items()}
|
||||
out = _fake_model(t)["outputs"].numpy().reshape(-1)
|
||||
self.state.note_hidden_state(out, HIDDEN)
|
||||
return out
|
||||
|
||||
|
||||
def test_policy_queues_match_temporal_state():
|
||||
from tinygrad.engine.jit import TinyJit
|
||||
jit = TinyJit(make_run_policy(_fake_model, SPEC, FS, "CPU"), prune=True)
|
||||
runner = PolicyRunner(jit, SPEC, FS, HIDDEN, "CPU")
|
||||
ref = _Reference()
|
||||
rng = np.random.default_rng(3)
|
||||
desire = np.zeros(8, dtype=np.float32)
|
||||
for i in range(14):
|
||||
warped = rng.integers(0, 256, (2, 6, 8, 16), dtype=np.int64).astype(np.uint8)
|
||||
if i in (2, 3, 9):
|
||||
desire[:] = 0
|
||||
desire[1 + (i % 3)] = 1
|
||||
elif i == 5:
|
||||
desire[:] = 0
|
||||
traffic = np.array([1.0, 0.0], dtype=np.float32) if i % 2 else np.array([0.0, 1.0], dtype=np.float32)
|
||||
action_t = np.array([0.1 * i, 0.2], dtype=np.float32)
|
||||
got = runner.run(warped, desire, traffic, action_t)
|
||||
want = ref.run(warped, desire, traffic, action_t)
|
||||
np.testing.assert_array_equal(got, want, err_msg=f"frame {i}")
|
||||
|
||||
|
||||
def test_layouts():
|
||||
shapes, sizes = packed_layout(SPEC)
|
||||
assert list(shapes) == ["desire", "traffic_convention", "action_t", "prev_feat"]
|
||||
assert sum(sizes) == 8 + 2 + 2 + 512
|
||||
q = queue_shapes(SPEC, FS)
|
||||
assert q["img_q"][0] == (5, 6, 8, 16) and q["feat_q"][0] == (96, 1, 512) and q["desire_q"][0] == (100, 1, 8)
|
||||
|
||||
|
||||
CAM = (64, 48)
|
||||
|
||||
|
||||
def _nv12(cam_w, cam_h):
|
||||
from iqpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||
stride, y_height, uv_height, _ = get_nv12_info(cam_w, cam_h)
|
||||
return (cam_w, cam_h, stride, y_height, uv_height)
|
||||
|
||||
|
||||
def _numpy_warp_plane(src, m, w_dst, h_dst):
|
||||
h_src, w_src = src.shape
|
||||
x = np.tile(np.arange(w_dst, dtype=np.float32), h_dst)
|
||||
y = np.repeat(np.arange(h_dst, dtype=np.float32), w_dst)
|
||||
sx = (m[0, 0] * x + m[0, 1] * y + m[0, 2]) / (m[2, 0] * x + m[2, 1] * y + m[2, 2])
|
||||
sy = (m[1, 0] * x + m[1, 1] * y + m[1, 2]) / (m[2, 0] * x + m[2, 1] * y + m[2, 2])
|
||||
xi = np.clip(np.round(sx), 0, w_src - 1).astype(np.int64)
|
||||
yi = np.clip(np.round(sy), 0, h_src - 1).astype(np.int64)
|
||||
return src[yi, xi].reshape(h_dst, w_dst)
|
||||
|
||||
|
||||
def _numpy_frame_prepare(frame, m, nv12, model_w, model_h):
|
||||
cam_w, cam_h, stride, y_height, uv_height = nv12
|
||||
m = m.astype(np.float32)
|
||||
y_src = frame[:cam_h * stride].reshape(cam_h, stride)
|
||||
uv = frame[stride * y_height:stride * y_height + uv_height * stride].reshape(uv_height, stride)
|
||||
m_uv = m * np.array([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]], dtype=np.float32)
|
||||
y = _numpy_warp_plane(y_src, m, model_w, model_h)
|
||||
u = _numpy_warp_plane(uv[:cam_h // 2, :cam_w:2], m_uv, model_w // 2, model_h // 2)
|
||||
v = _numpy_warp_plane(uv[:cam_h // 2, 1:cam_w:2], m_uv, model_w // 2, model_h // 2)
|
||||
f = np.concatenate([y.ravel(), u.ravel(), v.ravel()]).reshape(model_h * 3 // 2, model_w)
|
||||
H, W = model_h, model_w
|
||||
return np.stack([f[0:H:2, 0::2], f[1:H:2, 0::2], f[0:H:2, 1::2], f[1:H:2, 1::2],
|
||||
f[H:H + H // 4].reshape(H // 2, W // 2), f[H + H // 4:H + H // 2].reshape(H // 2, W // 2)])
|
||||
|
||||
|
||||
def _jittered_scale(rng, cam, model_w, model_h):
|
||||
m = np.array([[cam[0] / model_w, 0.0, 0.0], [0.0, cam[1] / model_h, 0.0], [0.0, 0.0, 1.0]], dtype=np.float32)
|
||||
m += (0.05 * rng.standard_normal((3, 3))).astype(np.float32) * np.array([[1, 1, 1], [1, 1, 1], [0.01, 0.01, 0.1]], dtype=np.float32)
|
||||
return m
|
||||
|
||||
|
||||
def test_frame_layout():
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_policy import frame_layout, model_size, nv12_copy_size
|
||||
shapes, sizes, npy_bytes = frame_layout(SPEC)
|
||||
assert list(shapes) == ["tfm", "big_tfm", "desire", "traffic_convention", "action_t", "prev_feat"]
|
||||
assert npy_bytes == (18 + 8 + 2 + 2 + 512) * 4
|
||||
assert model_size(SPEC) == (32, 16)
|
||||
assert nv12_copy_size(128, 64, 32) == 128 * 96
|
||||
|
||||
|
||||
def test_model_runner_matches_device_warp():
|
||||
from tinygrad.engine.jit import TinyJit
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_policy import ModelRunner, make_run_model, make_warp, model_size, nv12_copy_size
|
||||
nv12 = _nv12(*CAM)
|
||||
fcs = nv12_copy_size(nv12[2], nv12[3], nv12[4])
|
||||
model_w, model_h = model_size(SPEC)
|
||||
run_policy = make_run_policy(_fake_model, SPEC, FS, "CPU")
|
||||
jit = TinyJit(make_run_model(make_warp(nv12, model_w, model_h, "CPU"), run_policy, SPEC, fcs, "CPU"), prune=True)
|
||||
runner = ModelRunner(jit, SPEC, FS, HIDDEN, "CPU", fcs)
|
||||
ref = PolicyRunner(TinyJit(make_run_policy(_fake_model, SPEC, FS, "CPU"), prune=True), SPEC, FS, HIDDEN, "CPU")
|
||||
rng = np.random.default_rng(7)
|
||||
desire = np.zeros(8, dtype=np.float32)
|
||||
for i in range(10):
|
||||
main = rng.integers(0, 256, fcs, dtype=np.int64).astype(np.uint8)
|
||||
extra = rng.integers(0, 256, fcs, dtype=np.int64).astype(np.uint8)
|
||||
tfm = _jittered_scale(rng, CAM, model_w, model_h)
|
||||
big_tfm = _jittered_scale(rng, CAM, model_w, model_h)
|
||||
if i in (2, 6):
|
||||
desire[:] = 0
|
||||
desire[1 + i % 3] = 1
|
||||
traffic = np.array([1.0, 0.0], dtype=np.float32) if i % 2 else np.array([0.0, 1.0], dtype=np.float32)
|
||||
action_t = np.array([0.1 * i, 0.2], dtype=np.float32)
|
||||
got = runner.run(main, extra, tfm, big_tfm, desire, traffic, action_t)
|
||||
warped = np.stack([_numpy_frame_prepare(main, tfm, nv12, model_w, model_h), _numpy_frame_prepare(extra, big_tfm, nv12, model_w, model_h)])
|
||||
want = ref.run(warped, desire, traffic, action_t)
|
||||
np.testing.assert_array_equal(got, want, err_msg=f"frame {i}")
|
||||
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"
|
||||
526
iqpilot/selfdrive/iqmodeld/tests/test_egpu_worker.py
Normal file
526
iqpilot/selfdrive/iqmodeld/tests/test_egpu_worker.py
Normal file
@@ -0,0 +1,526 @@
|
||||
"""
|
||||
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_big_flag_follows_payload_then_source(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 sent["modelV2"].modelV2.big and sent["drivingModelData"].drivingModelData.big
|
||||
|
||||
small_on_mac = {**make_big_channel_payload(43, True, 0.03, 25.0, self._real_msgs()), "source": "mac_big", "big": False}
|
||||
_patch_and_send(PM(), small_on_mac, frame_drop_perc=0.0, selector_dropped=0, target=43, source_lag=0)
|
||||
assert not sent["modelV2"].modelV2.big and not sent["drivingModelData"].drivingModelData.big
|
||||
|
||||
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
|
||||
128
iqpilot/selfdrive/iqmodeld/tests/test_fused_runner_guards.py
Normal file
128
iqpilot/selfdrive/iqmodeld/tests/test_fused_runner_guards.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pickle
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners import model_runner as model_runner_mod
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelType
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad import fused_runner as fused_mod
|
||||
|
||||
|
||||
class _View:
|
||||
def __init__(self, shape):
|
||||
self.shape = shape
|
||||
|
||||
|
||||
class _Captured:
|
||||
def __init__(self, expected_names, expected_input_info):
|
||||
self.expected_names = expected_names
|
||||
self.expected_input_info = expected_input_info
|
||||
|
||||
|
||||
class _FakeJit:
|
||||
def __init__(self, expected_names, expected_input_info):
|
||||
self.captured = _Captured(expected_names, expected_input_info)
|
||||
|
||||
def __call__(self, **kwargs):
|
||||
raise AssertionError("policy jit should not run in this test")
|
||||
|
||||
|
||||
class _FakeTensor:
|
||||
def __init__(self, arr, device=None):
|
||||
self.shape = tuple(np.asarray(arr).shape)
|
||||
|
||||
def contiguous(self):
|
||||
return self
|
||||
|
||||
def realize(self):
|
||||
return self
|
||||
|
||||
|
||||
class _FakeDevice:
|
||||
DEFAULT = "FAKE"
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Type:
|
||||
raw: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Artifact:
|
||||
fileName: str
|
||||
|
||||
|
||||
class _Model:
|
||||
def __init__(self, file_name):
|
||||
self.type = _Type(ModelType.vision)
|
||||
self.artifact = _Artifact(file_name)
|
||||
self.metadata = None
|
||||
|
||||
|
||||
class _Bundle:
|
||||
def __init__(self, file_name):
|
||||
self.models = [_Model(file_name)]
|
||||
self.is20hz = True
|
||||
|
||||
|
||||
POLICY_INPUTS = ["action_t", "big_img", "desire", "desire_q", "feat_q", "img", "traffic_convention"]
|
||||
POLICY_SHAPES = {
|
||||
"action_t": (1, 2), "big_img": (1, 12, 128, 256), "desire": (1, 8), "desire_q": (1, 100, 8),
|
||||
"feat_q": (1, 99, 512), "img": (1, 12, 128, 256), "traffic_convention": (1, 2),
|
||||
}
|
||||
|
||||
|
||||
def _write_fused_pkl(path, policy_inputs):
|
||||
info = [(_View(POLICY_SHAPES[n]), (), None, "NPY") for n in policy_inputs]
|
||||
role_meta = {
|
||||
"input_shapes": {"desire_pulse": (1, 100, 8), "traffic_convention": (1, 2), "features_buffer": (1, 99, 512)},
|
||||
"output_slices": {},
|
||||
}
|
||||
blob = {
|
||||
"metadata": {
|
||||
"vision": {"input_shapes": {"img": (1, 12, 128, 256), "big_img": (1, 12, 128, 256)}, "output_slices": {}},
|
||||
"on_policy": role_meta,
|
||||
"off_policy": role_meta,
|
||||
},
|
||||
"run_policy": _FakeJit(policy_inputs, info),
|
||||
"frame_skip": 4,
|
||||
(1928, 1208): _FakeJit(["frame"], [(_View((1,)), (), None, "NPY")]),
|
||||
}
|
||||
with open(path, "wb") as f:
|
||||
pickle.dump(blob, f)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fused_runner(tmp_path, monkeypatch):
|
||||
def _build(policy_inputs):
|
||||
name = "driving_fused_test.pkl"
|
||||
_write_fused_pkl(tmp_path / name, policy_inputs)
|
||||
monkeypatch.setattr(model_runner_mod, "_fetch_bundle", lambda params=None: _Bundle(name))
|
||||
monkeypatch.setattr(fused_mod, "CUSTOM_MODEL_PATH", str(tmp_path))
|
||||
monkeypatch.setattr(fused_mod, "_tinygrad_imports", lambda: (_FakeTensor, _FakeDevice))
|
||||
return fused_mod.TinygradFusedRunner()
|
||||
return _build
|
||||
|
||||
|
||||
def test_action_t_allocated_when_only_the_jit_declares_it(fused_runner):
|
||||
runner = fused_runner(POLICY_INPUTS)
|
||||
assert "action_t" not in runner._on_meta["input_shapes"]
|
||||
|
||||
runner._ensure_queues(1928, 1208)
|
||||
|
||||
assert runner._npy_buffers["action_t"].shape == POLICY_SHAPES["action_t"]
|
||||
assert runner._npy_buffers["traffic_convention"].shape == POLICY_SHAPES["traffic_convention"]
|
||||
|
||||
|
||||
def test_action_t_absent_when_the_jit_does_not_take_it(fused_runner):
|
||||
runner = fused_runner([n for n in POLICY_INPUTS if n != "action_t"])
|
||||
|
||||
runner._ensure_queues(1928, 1208)
|
||||
|
||||
assert "action_t" not in runner._npy_buffers
|
||||
126
iqpilot/selfdrive/iqmodeld/tests/test_iqmodeld_contracts.py
Normal file
126
iqpilot/selfdrive/iqmodeld/tests/test_iqmodeld_contracts.py
Normal file
@@ -0,0 +1,126 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
|
||||
import iqpilot.cereal.messaging as messaging
|
||||
import numpy as np
|
||||
from iqpilot.cereal import log
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld.config import Meta, ModelConstants
|
||||
from iqpilot.selfdrive.iqmodeld.messaging import (
|
||||
DrivePacketMemory,
|
||||
pick_curvature,
|
||||
populate_drive_messages,
|
||||
populate_odometry_message,
|
||||
)
|
||||
from iqpilot.selfdrive.iqmodeld.models.split_model_constants import SplitModelConstants
|
||||
from iqpilot.selfdrive.iqmodeld.parser import ArchiveParser, PhaseParser
|
||||
|
||||
|
||||
def _archive_sample(rng: np.random.Generator) -> dict[str, np.ndarray]:
|
||||
return {
|
||||
"plan": rng.standard_normal((1, ModelConstants.PLAN_MHP_N * (2 * ModelConstants.IDX_N * ModelConstants.PLAN_WIDTH + ModelConstants.PLAN_MHP_SELECTION)), dtype=np.float32),
|
||||
"lane_lines": rng.standard_normal((1, 2 * ModelConstants.NUM_LANE_LINES * ModelConstants.IDX_N * ModelConstants.LANE_LINES_WIDTH), dtype=np.float32),
|
||||
"road_edges": rng.standard_normal((1, 2 * ModelConstants.NUM_ROAD_EDGES * ModelConstants.IDX_N * ModelConstants.LANE_LINES_WIDTH), dtype=np.float32),
|
||||
"pose": rng.standard_normal((1, 2 * ModelConstants.POSE_WIDTH), dtype=np.float32),
|
||||
"road_transform": rng.standard_normal((1, 2 * ModelConstants.POSE_WIDTH), dtype=np.float32),
|
||||
"sim_pose": rng.standard_normal((1, 2 * ModelConstants.POSE_WIDTH), dtype=np.float32),
|
||||
"wide_from_device_euler": rng.standard_normal((1, 2 * ModelConstants.WIDE_FROM_DEVICE_WIDTH), dtype=np.float32),
|
||||
"lead": rng.standard_normal((1, ModelConstants.LEAD_MHP_N * (2 * ModelConstants.LEAD_TRAJ_LEN * ModelConstants.LEAD_WIDTH + ModelConstants.LEAD_MHP_SELECTION)), dtype=np.float32),
|
||||
"lat_planner_solution": rng.standard_normal((1, 2 * ModelConstants.IDX_N * ModelConstants.LAT_PLANNER_SOLUTION_WIDTH), dtype=np.float32),
|
||||
"desired_curvature": rng.standard_normal((1, 2 * ModelConstants.DESIRED_CURV_WIDTH), dtype=np.float32),
|
||||
"lead_prob": rng.standard_normal((1, ModelConstants.LEAD_MHP_SELECTION), dtype=np.float32),
|
||||
"lane_lines_prob": rng.standard_normal((1, ModelConstants.NUM_LANE_LINES * 2), dtype=np.float32),
|
||||
"meta": rng.standard_normal((1, 55), dtype=np.float32),
|
||||
"desire_state": rng.standard_normal((1, ModelConstants.DESIRE_PRED_WIDTH), dtype=np.float32),
|
||||
"desire_pred": rng.standard_normal((1, ModelConstants.DESIRE_PRED_LEN * ModelConstants.DESIRE_PRED_WIDTH), dtype=np.float32),
|
||||
}
|
||||
|
||||
|
||||
def _phase_sample(rng: np.random.Generator) -> dict[str, np.ndarray]:
|
||||
c = SplitModelConstants
|
||||
return {
|
||||
"pose": rng.standard_normal((1, 2 * c.POSE_WIDTH), dtype=np.float32),
|
||||
"wide_from_device_euler": rng.standard_normal((1, 2 * c.WIDE_FROM_DEVICE_WIDTH), dtype=np.float32),
|
||||
"road_transform": rng.standard_normal((1, 2 * c.POSE_WIDTH), dtype=np.float32),
|
||||
"lead": rng.standard_normal((1, c.LEAD_MHP_N * (2 * c.LEAD_TRAJ_LEN * c.LEAD_WIDTH + c.LEAD_MHP_SELECTION)), dtype=np.float32),
|
||||
"plan": rng.standard_normal((1, c.PLAN_MHP_N * (2 * c.IDX_N * c.PLAN_WIDTH + c.PLAN_MHP_SELECTION)), dtype=np.float32),
|
||||
"planplus": rng.standard_normal((1, 2 * c.IDX_N * c.PLAN_WIDTH), dtype=np.float32),
|
||||
"action": rng.standard_normal((1, 2 * c.ACTION_WIDTH), dtype=np.float32),
|
||||
"desired_curvature": rng.standard_normal((1, 2 * c.DESIRED_CURV_WIDTH), dtype=np.float32),
|
||||
"desire_pred": rng.standard_normal((1, c.DESIRE_PRED_LEN * c.DESIRE_PRED_WIDTH), dtype=np.float32),
|
||||
"desire_state": rng.standard_normal((1, c.DESIRE_PRED_WIDTH), dtype=np.float32),
|
||||
"lane_lines": rng.standard_normal((1, 2 * c.NUM_LANE_LINES * c.IDX_N * c.LANE_LINES_WIDTH), dtype=np.float32),
|
||||
"lane_lines_prob": rng.standard_normal((1, c.NUM_LANE_LINES * 2), dtype=np.float32),
|
||||
"lead_prob": rng.standard_normal((1, c.LEAD_MHP_SELECTION), dtype=np.float32),
|
||||
"lat_planner_solution": rng.standard_normal((1, 2 * c.IDX_N * c.LAT_PLANNER_SOLUTION_WIDTH), dtype=np.float32),
|
||||
"meta": rng.standard_normal((1, 55), dtype=np.float32),
|
||||
"road_edges": rng.standard_normal((1, 2 * c.NUM_ROAD_EDGES * c.IDX_N * c.LANE_LINES_WIDTH), dtype=np.float32),
|
||||
"sim_pose": rng.standard_normal((1, 2 * c.POSE_WIDTH), dtype=np.float32),
|
||||
}
|
||||
|
||||
|
||||
def test_archive_parser_contract_snapshot():
|
||||
outputs = ArchiveParser().parse_outputs(copy.deepcopy(_archive_sample(np.random.default_rng(7))))
|
||||
|
||||
assert outputs["plan"].shape == (1, 33, 15)
|
||||
assert outputs["lane_lines"].shape == (1, 4, 33, 2)
|
||||
assert outputs["road_edges"].shape == (1, 2, 33, 2)
|
||||
assert outputs["desire_pred"].shape == (1, 4, 8)
|
||||
|
||||
np.testing.assert_allclose(outputs["pose"][0, 0], 0.45617363, rtol=1e-6, atol=1e-6)
|
||||
np.testing.assert_allclose(outputs["lane_lines_prob"][0, 2], 0.85733712, rtol=1e-6, atol=1e-6)
|
||||
np.testing.assert_allclose(outputs["desire_state"][0, 0], 0.44964141, rtol=1e-6, atol=1e-6)
|
||||
np.testing.assert_allclose(outputs["lead_prob"][0, 0], 0.21613698, rtol=1e-6, atol=1e-6)
|
||||
|
||||
|
||||
def test_phase_parser_contract_snapshot():
|
||||
raw = _phase_sample(np.random.default_rng(23))
|
||||
outputs = {**PhaseParser().parse_vision_outputs(copy.deepcopy(raw)), **PhaseParser().parse_policy_outputs(copy.deepcopy(raw))}
|
||||
|
||||
assert outputs["plan"].shape == (1, 33, 15)
|
||||
assert outputs["action"].shape == (1, 2)
|
||||
assert outputs["desired_curvature"].shape == (1, 1)
|
||||
assert outputs["road_edges"].shape == (1, 2, 33, 2)
|
||||
|
||||
np.testing.assert_allclose(outputs["plan"][0, 0, 0], 0.09684439, rtol=1e-6, atol=1e-6)
|
||||
np.testing.assert_allclose(outputs["action"][0, 0], 0.25458091, rtol=1e-6, atol=1e-6)
|
||||
np.testing.assert_allclose(outputs["desired_curvature"][0, 0], -0.97072351, rtol=1e-6, atol=1e-6)
|
||||
np.testing.assert_allclose(outputs["lane_lines_prob"][0, 0], 0.20073657, rtol=1e-6, atol=1e-6)
|
||||
|
||||
|
||||
def test_message_population_contract_snapshot():
|
||||
raw = _phase_sample(np.random.default_rng(23))
|
||||
outputs = {**PhaseParser().parse_vision_outputs(copy.deepcopy(raw)), **PhaseParser().parse_policy_outputs(copy.deepcopy(raw))}
|
||||
action = log.ModelDataV2.Action(desiredCurvature=0.031, desiredAcceleration=-0.12, shouldStop=False)
|
||||
|
||||
driving_msg = messaging.new_message("drivingModelData")
|
||||
model_msg = messaging.new_message("modelV2")
|
||||
odometry_msg = messaging.new_message("cameraOdometry")
|
||||
memory = DrivePacketMemory()
|
||||
|
||||
populate_drive_messages(
|
||||
driving_msg, model_msg, outputs, action, memory,
|
||||
2468, 2470, 2480, 0.05, 123456789, 0.014, True, Meta,
|
||||
)
|
||||
populate_odometry_message(odometry_msg, outputs, 2468, 0, 123456789, True)
|
||||
|
||||
np.testing.assert_allclose(driving_msg.drivingModelData.laneLineMeta.leftY, -0.21672775, rtol=1e-6, atol=1e-6)
|
||||
np.testing.assert_allclose(model_msg.modelV2.meta.engagedProb, 0.64853197, rtol=1e-6, atol=1e-6)
|
||||
np.testing.assert_allclose(odometry_msg.cameraOdometry.trans[0], 0.0360266, rtol=1e-6, atol=1e-6)
|
||||
assert int(model_msg.modelV2.confidence.raw) == 2
|
||||
|
||||
|
||||
def test_curvature_selection_contract_snapshot():
|
||||
raw = _phase_sample(np.random.default_rng(23))
|
||||
outputs = {**PhaseParser().parse_vision_outputs(copy.deepcopy(raw)), **PhaseParser().parse_policy_outputs(copy.deepcopy(raw))}
|
||||
plan_rows = outputs["plan"][0]
|
||||
|
||||
direct = pick_curvature(outputs, plan_rows, 27.5, 0.8, synthetic_lane_logic=False)
|
||||
fallback = pick_curvature(outputs, plan_rows, 27.5, 0.8, synthetic_lane_logic=True)
|
||||
|
||||
np.testing.assert_allclose(direct, -0.97072351, rtol=1e-6, atol=1e-6)
|
||||
np.testing.assert_allclose(fallback, -0.0689389, rtol=1e-6, atol=1e-6)
|
||||
73
iqpilot/selfdrive/iqmodeld/tests/test_lat_delay_source.py
Normal file
73
iqpilot/selfdrive/iqmodeld/tests/test_lat_delay_source.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from iqpilot.cereal import car
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.selfdrive.iqmodeld.daemon import InferenceDaemon
|
||||
|
||||
LIVE_DELAY = 0.4387
|
||||
RACK_DELAY = 0.10
|
||||
OFFSET = 0.05
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def params(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("PARAMS_ROOT", str(tmp_path))
|
||||
p = Params()
|
||||
p.put("IQSteerDelayCache", LIVE_DELAY)
|
||||
p.put("IQSoftwareSteerDelay", OFFSET)
|
||||
p.put_bool("ModelSmoothingEnabled", False)
|
||||
p.put("ModelLatSmoothSec", 0)
|
||||
p.put("PlanplusControl", 1.0)
|
||||
p.put("CameraOffset", 0.0)
|
||||
return p
|
||||
|
||||
|
||||
def _daemon(params, steer_control_type):
|
||||
car_params = car.CarParams.new_message()
|
||||
car_params.steerControlType = steer_control_type
|
||||
car_params.steerActuatorDelay = RACK_DELAY
|
||||
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),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("live_enabled, expected", [(False, RACK_DELAY + OFFSET), (True, LIVE_DELAY)])
|
||||
def test_angle_cars_honour_the_self_tuning_toggle(params, live_enabled, expected):
|
||||
params.put_bool("IQLiveSteerDelay", live_enabled)
|
||||
daemon = _daemon(params, car.CarParams.SteerControlType.angle)
|
||||
InferenceDaemon._refresh_tunables(daemon, 0)
|
||||
assert daemon._runtime.lat_delay == pytest.approx(expected)
|
||||
|
||||
|
||||
def test_angle_cars_never_plan_against_the_live_estimate_when_disabled(params):
|
||||
params.put_bool("IQLiveSteerDelay", False)
|
||||
daemon = _daemon(params, car.CarParams.SteerControlType.angle)
|
||||
InferenceDaemon._refresh_tunables(daemon, 0)
|
||||
assert daemon._runtime.lat_delay != pytest.approx(LIVE_DELAY)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("live_enabled", [True, False])
|
||||
def test_torque_cars_keep_the_live_estimate(params, live_enabled):
|
||||
params.put_bool("IQLiveSteerDelay", live_enabled)
|
||||
daemon = _daemon(params, car.CarParams.SteerControlType.torque)
|
||||
InferenceDaemon._refresh_tunables(daemon, 0)
|
||||
assert daemon._runtime.lat_delay == pytest.approx(LIVE_DELAY)
|
||||
|
||||
|
||||
def test_refresh_is_throttled_to_every_sixtieth_tick(params):
|
||||
params.put_bool("IQLiveSteerDelay", False)
|
||||
daemon = _daemon(params, car.CarParams.SteerControlType.angle)
|
||||
InferenceDaemon._refresh_tunables(daemon, 1)
|
||||
assert daemon._runtime.lat_delay is None
|
||||
InferenceDaemon._refresh_tunables(daemon, 60)
|
||||
assert daemon._runtime.lat_delay == pytest.approx(RACK_DELAY + OFFSET)
|
||||
133
iqpilot/selfdrive/iqmodeld/tests/test_model_bundle_downloader.py
Normal file
133
iqpilot/selfdrive/iqmodeld/tests/test_model_bundle_downloader.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
import hashlib
|
||||
import http.server
|
||||
import os
|
||||
import threading
|
||||
|
||||
import pytest
|
||||
|
||||
from iqpilot.selfdrive.iqmodeld import model_bundle_downloader as dl
|
||||
|
||||
|
||||
class _RangeHandler(http.server.BaseHTTPRequestHandler):
|
||||
store: dict[str, bytes] = {}
|
||||
cut_first: dict[str, int] = {}
|
||||
hits: list[tuple[str, str | None]] = []
|
||||
|
||||
def log_message(self, *a):
|
||||
pass
|
||||
|
||||
def do_GET(self):
|
||||
oid = self.path.rsplit("/", 1)[-1]
|
||||
data = self.store[oid]
|
||||
rng = self.headers.get("Range")
|
||||
self.hits.append((oid, rng))
|
||||
start = int(rng.split("=")[1].rstrip("-")) if rng else 0
|
||||
body = data[start:]
|
||||
cut = self.cut_first.pop(oid, None)
|
||||
if cut is not None:
|
||||
body = body[:cut]
|
||||
self.send_response(206 if rng else 200)
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
if rng:
|
||||
self.send_header("Content-Range", f"bytes {start}-{start + len(body) - 1}/{len(data)}")
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def server():
|
||||
srv = http.server.ThreadingHTTPServer(("127.0.0.1", 0), _RangeHandler)
|
||||
t = threading.Thread(target=srv.serve_forever, daemon=True)
|
||||
t.start()
|
||||
yield srv
|
||||
srv.shutdown()
|
||||
srv.server_close()
|
||||
|
||||
|
||||
def _objects(parts):
|
||||
return [{"oid": hashlib.sha256(p).hexdigest(), "size": len(p)} for p in parts]
|
||||
|
||||
|
||||
def test_resume_continues_a_cut_part_and_reuses_finished_parts(server, tmp_path, monkeypatch):
|
||||
parts = [os.urandom(300_000), os.urandom(300_000), os.urandom(120_000)]
|
||||
objs = _objects(parts)
|
||||
_RangeHandler.store = {o["oid"]: p for o, p in zip(objs, parts, strict=True)}
|
||||
_RangeHandler.hits = []
|
||||
_RangeHandler.cut_first = {objs[1]["oid"]: 100_000}
|
||||
port = server.server_address[1]
|
||||
monkeypatch.setattr(dl, "_requests_auth", lambda: None)
|
||||
monkeypatch.setattr(dl, "_resolve_oid", lambda session, base, oid, size, auth: (f"http://127.0.0.1:{port}/o/{oid}", {}))
|
||||
monkeypatch.setattr(dl, "MODELS_BASE_URLS", ("http://unused",))
|
||||
monkeypatch.setattr(dl, "STREAM_RETRIES", 3)
|
||||
monkeypatch.setattr(dl, "CHUNK", 64 * 1024)
|
||||
whole = b"".join(parts)
|
||||
dst = str(tmp_path / "model.pkl")
|
||||
out = dl.download_lfs_bundle(objs, dst, hashlib.sha256(whole).hexdigest(), len(whole))
|
||||
with open(dst, "rb") as f:
|
||||
assert out == dst and f.read() == whole
|
||||
assert not os.path.exists(dst + ".parts")
|
||||
ranges = [r for o, r in _RangeHandler.hits if o == objs[1]["oid"]]
|
||||
assert ranges[0] is None and ranges[1] == "bytes=100000-"
|
||||
assert sum(1 for o, _ in _RangeHandler.hits if o == objs[0]["oid"]) == 1
|
||||
|
||||
|
||||
def test_corrupt_finished_part_is_refetched(server, tmp_path, monkeypatch):
|
||||
parts = [os.urandom(200_000), os.urandom(50_000)]
|
||||
objs = _objects(parts)
|
||||
_RangeHandler.store = {o["oid"]: p for o, p in zip(objs, parts, strict=True)}
|
||||
_RangeHandler.hits = []
|
||||
_RangeHandler.cut_first = {}
|
||||
port = server.server_address[1]
|
||||
monkeypatch.setattr(dl, "_requests_auth", lambda: None)
|
||||
monkeypatch.setattr(dl, "_resolve_oid", lambda session, base, oid, size, auth: (f"http://127.0.0.1:{port}/o/{oid}", {}))
|
||||
monkeypatch.setattr(dl, "MODELS_BASE_URLS", ("http://unused",))
|
||||
dst = str(tmp_path / "model.pkl")
|
||||
os.makedirs(dst + ".parts")
|
||||
with open(dl._part_path(dst, objs[0]["oid"]), "wb") as f:
|
||||
f.write(os.urandom(200_000))
|
||||
whole = b"".join(parts)
|
||||
dl.download_lfs_bundle(objs, dst, hashlib.sha256(whole).hexdigest(), len(whole))
|
||||
with open(dst, "rb") as f:
|
||||
assert f.read() == whole
|
||||
|
||||
|
||||
def test_hf_single_file_resumes_after_cut(server, tmp_path, monkeypatch):
|
||||
data = os.urandom(700_000)
|
||||
oid = hashlib.sha256(data).hexdigest()
|
||||
_RangeHandler.store = {oid: data}
|
||||
_RangeHandler.hits = []
|
||||
_RangeHandler.cut_first = {oid: 250_000}
|
||||
port = server.server_address[1]
|
||||
monkeypatch.setattr(dl, "_hf", lambda: ({"Authorization": "Bearer test"}, lambda p: f"http://127.0.0.1:{port}/o/{oid}"))
|
||||
monkeypatch.setattr(dl, "STREAM_RETRIES", 3)
|
||||
monkeypatch.setattr(dl, "CHUNK", 64 * 1024)
|
||||
dst = str(tmp_path / "policy.pkl")
|
||||
out = dl.download_hf_file("egpu/policy/x.pkl", dst, oid, len(data))
|
||||
with open(dst, "rb") as f:
|
||||
assert out == dst and f.read() == data
|
||||
ranges = [r for o, r in _RangeHandler.hits if o == oid]
|
||||
assert ranges[0] is None and ranges[1] == "bytes=250000-"
|
||||
assert not os.path.exists(dst + ".hfpart")
|
||||
|
||||
|
||||
def test_download_onnx_prefers_hf_then_falls_back(tmp_path, monkeypatch):
|
||||
from iqpilot.selfdrive.iqmodeld import egpu_helpers as eh
|
||||
meta = {"key": "m", "sha256": "ab" * 32, "download": {"kind": "comma_lfs", "size": 5}}
|
||||
monkeypatch.setattr(eh, "onnx_cache_path", lambda m: str(tmp_path / "m.onnx"))
|
||||
monkeypatch.setattr("iqpilot.selfdrive.iqmodeld.egpu_model.download_descriptor", lambda m: ("commalfs:" + m["sha256"], 5), raising=False)
|
||||
calls = []
|
||||
import iqpilot.selfdrive.iqmodeld.model_bundle_downloader as dlm
|
||||
monkeypatch.setattr(dlm, "download_hf_file", lambda path, dst, sha, size, progress_cb=None: (calls.append(("hf", path)), open(dst, "wb").close(), dst)[2])
|
||||
monkeypatch.setattr(eh, "resolve_download_url", lambda *a, **k: (calls.append(("lfs",)), "http://unused")[1])
|
||||
out = eh.download_onnx(meta)
|
||||
assert calls == [("hf", "onnx/" + "ab" * 32 + ".onnx")] and out == str(tmp_path / "m.onnx")
|
||||
calls.clear()
|
||||
def boom(*a, **k):
|
||||
calls.append(("hf-fail",)); raise RuntimeError("hf down")
|
||||
monkeypatch.setattr(dlm, "download_hf_file", boom)
|
||||
with pytest.raises(Exception):
|
||||
eh.download_onnx(meta)
|
||||
assert calls[:2] == [("hf-fail",), ("lfs",)]
|
||||
77
iqpilot/selfdrive/iqmodeld/tests/test_model_runner_smoke.py
Normal file
77
iqpilot/selfdrive/iqmodeld/tests/test_model_runner_smoke.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from tinygrad.tensor import Tensor
|
||||
|
||||
import iqpilot.selfdrive.iqmodeld.models.helpers as bundle_helpers
|
||||
import iqpilot.selfdrive.iqmodeld.models.runners.model_runner as model_runner_mod
|
||||
import iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.tinygrad_runner as tinygrad_runner_mod
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelType
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.tinygrad_runner import TinygradRunner
|
||||
|
||||
|
||||
LOCAL_MODEL_DIR = Path(__file__).resolve().parents[1] / "default_model"
|
||||
|
||||
|
||||
@dataclass
|
||||
class _TypeWrap:
|
||||
raw: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Artifact:
|
||||
fileName: str
|
||||
|
||||
|
||||
class _Model:
|
||||
def __init__(self, model_type: int, artifact_name: str, metadata_name: str):
|
||||
self.type = _TypeWrap(model_type)
|
||||
self.artifact = _Artifact(artifact_name)
|
||||
self.metadata = _Artifact(metadata_name)
|
||||
|
||||
|
||||
class _Bundle:
|
||||
def __init__(self, models: list[_Model], is_20hz: bool = False):
|
||||
self.models = models
|
||||
self.is20hz = is_20hz
|
||||
|
||||
|
||||
def _seed_runner_inputs(runner: TinygradRunner) -> None:
|
||||
for name, shape in runner.input_shapes.items():
|
||||
runner.inputs[name] = Tensor(
|
||||
np.zeros(shape, dtype=np.float32),
|
||||
device=runner.input_to_device[name],
|
||||
dtype=runner.input_to_dtype[name],
|
||||
).realize()
|
||||
|
||||
|
||||
@pytest.mark.tici
|
||||
def test_local_tinygrad_models_execute(monkeypatch):
|
||||
bundle = _Bundle([
|
||||
_Model(ModelType.vision, "driving_vision_c210m_tinygrad.pkl", "driving_vision_c210m_metadata.pkl"),
|
||||
_Model(ModelType.policy, "driving_policy_c210m_tinygrad.pkl", "driving_policy_c210m_metadata.pkl"),
|
||||
])
|
||||
|
||||
monkeypatch.setattr(bundle_helpers, "get_active_bundle", lambda params=None: bundle, raising=False)
|
||||
monkeypatch.setattr(model_runner_mod, "_fetch_bundle", lambda params=None: bundle)
|
||||
monkeypatch.setattr(tinygrad_runner_mod, "CUSTOM_MODEL_PATH", str(LOCAL_MODEL_DIR), raising=False)
|
||||
monkeypatch.setattr(model_runner_mod, "CUSTOM_MODEL_PATH", str(LOCAL_MODEL_DIR), raising=False)
|
||||
|
||||
vision_runner = TinygradRunner(ModelType.vision)
|
||||
_seed_runner_inputs(vision_runner)
|
||||
vision_outputs = vision_runner.run_model()
|
||||
assert "pose" in vision_outputs
|
||||
assert "lane_lines" in vision_outputs
|
||||
|
||||
policy_runner = TinygradRunner(ModelType.policy)
|
||||
_seed_runner_inputs(policy_runner)
|
||||
policy_outputs = policy_runner.run_model()
|
||||
assert "plan" in policy_outputs
|
||||
assert "desire_state" in policy_outputs
|
||||
21
iqpilot/selfdrive/iqmodeld/tests/test_public_surface.py
Normal file
21
iqpilot/selfdrive/iqmodeld/tests/test_public_surface.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from iqpilot.selfdrive.iqmodeld import metadata, messaging, parser
|
||||
from iqpilot.selfdrive.iqmodeld.daemon import CaptureStamp, NeuralEngineState
|
||||
|
||||
|
||||
def test_public_module_surface():
|
||||
assert hasattr(messaging, "DrivePacketMemory")
|
||||
assert hasattr(messaging, "pick_curvature")
|
||||
assert hasattr(messaging, "populate_drive_messages")
|
||||
assert hasattr(messaging, "populate_odometry_message")
|
||||
|
||||
assert hasattr(parser, "ArchiveParser")
|
||||
assert hasattr(parser, "PhaseParser")
|
||||
|
||||
assert hasattr(metadata, "select_meta_layout")
|
||||
assert hasattr(metadata, "build_metadata_record")
|
||||
|
||||
assert CaptureStamp.__name__ == "CaptureStamp"
|
||||
assert NeuralEngineState.__name__ == "NeuralEngineState"
|
||||
171
iqpilot/selfdrive/iqmodeld/tests/test_selector_share_smoke.py
Normal file
171
iqpilot/selfdrive/iqmodeld/tests/test_selector_share_smoke.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from tinygrad.nn.onnx import OnnxRunner
|
||||
from tinygrad.tensor import Tensor
|
||||
|
||||
import iqpilot.selfdrive.iqmodeld.models.helpers as bundle_helpers
|
||||
import iqpilot.selfdrive.iqmodeld.models.runners.model_runner as model_runner_mod
|
||||
import iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.tinygrad_runner as tinygrad_runner_mod
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelType
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.tinygrad_runner import TinygradRunner
|
||||
|
||||
|
||||
SHARE_ROOT = Path(os.getenv("IQPILOT_SELECTOR_SHARE", "/Volumes/New New Vault/IQModels/models/recompiled16"))
|
||||
|
||||
|
||||
@dataclass
|
||||
class _TypeWrap:
|
||||
raw: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Artifact:
|
||||
fileName: str
|
||||
|
||||
|
||||
class _Model:
|
||||
def __init__(self, model_type: int, artifact_name: str, metadata_name: str):
|
||||
self.type = _TypeWrap(model_type)
|
||||
self.artifact = _Artifact(artifact_name)
|
||||
self.metadata = _Artifact(metadata_name)
|
||||
|
||||
|
||||
class _Bundle:
|
||||
def __init__(self, models: list[_Model], is_20hz: bool = False):
|
||||
self.models = models
|
||||
self.is20hz = is_20hz
|
||||
|
||||
|
||||
def _find_selector_dirs(limit: int = 3, require_onnx: bool = False) -> list[Path]:
|
||||
found: list[Path] = []
|
||||
if not SHARE_ROOT.is_dir():
|
||||
return found
|
||||
|
||||
for bundle_dir in sorted(SHARE_ROOT.iterdir()):
|
||||
if not bundle_dir.is_dir():
|
||||
continue
|
||||
vision = next(bundle_dir.glob("driving_vision*_tinygrad.pkl"), None)
|
||||
policy = next(bundle_dir.glob("driving_policy*_tinygrad.pkl"), None)
|
||||
vision_meta = next(bundle_dir.glob("driving_vision*_metadata.pkl"), None)
|
||||
policy_meta = next(bundle_dir.glob("driving_policy*_metadata.pkl"), None)
|
||||
has_onnx = (bundle_dir / "driving_vision.onnx").is_file() and (bundle_dir / "driving_policy.onnx").is_file()
|
||||
if vision and policy and vision_meta and policy_meta and (has_onnx or not require_onnx):
|
||||
found.append(bundle_dir)
|
||||
if len(found) >= limit:
|
||||
break
|
||||
return found
|
||||
|
||||
|
||||
def _seed_runner_inputs(runner: TinygradRunner) -> None:
|
||||
for name, shape in runner.input_shapes.items():
|
||||
runner.inputs[name] = Tensor(
|
||||
np.zeros(shape, dtype=np.float32),
|
||||
device=runner.input_to_device[name],
|
||||
dtype=runner.input_to_dtype[name],
|
||||
).realize()
|
||||
|
||||
|
||||
def _bundle_for_dir(bundle_dir: Path) -> _Bundle:
|
||||
vision = next(bundle_dir.glob("driving_vision*_tinygrad.pkl"))
|
||||
policy = next(bundle_dir.glob("driving_policy*_tinygrad.pkl"))
|
||||
vision_meta = next(bundle_dir.glob("driving_vision*_metadata.pkl"))
|
||||
policy_meta = next(bundle_dir.glob("driving_policy*_metadata.pkl"))
|
||||
return _Bundle([
|
||||
_Model(ModelType.vision, vision.name, vision_meta.name),
|
||||
_Model(ModelType.policy, policy.name, policy_meta.name),
|
||||
])
|
||||
|
||||
|
||||
def _run_tinygrad_bundle(bundle_dir: Path, monkeypatch):
|
||||
bundle = _bundle_for_dir(bundle_dir)
|
||||
monkeypatch.setattr(bundle_helpers, "get_active_bundle", lambda params=None: bundle, raising=False)
|
||||
monkeypatch.setattr(model_runner_mod, "get_active_bundle", lambda params=None: bundle, raising=False)
|
||||
monkeypatch.setattr(model_runner_mod, "_fetch_bundle", lambda: bundle)
|
||||
monkeypatch.setattr(tinygrad_runner_mod, "CUSTOM_MODEL_PATH", str(bundle_dir), raising=False)
|
||||
monkeypatch.setattr(model_runner_mod, "CUSTOM_MODEL_PATH", str(bundle_dir), raising=False)
|
||||
|
||||
vision_runner = TinygradRunner(ModelType.vision)
|
||||
_seed_runner_inputs(vision_runner)
|
||||
vision_outputs = vision_runner.run_model()
|
||||
|
||||
policy_runner = TinygradRunner(ModelType.policy)
|
||||
_seed_runner_inputs(policy_runner)
|
||||
policy_outputs = policy_runner.run_model()
|
||||
|
||||
return vision_outputs, policy_outputs
|
||||
|
||||
|
||||
def _run_onnx_bundle(bundle_dir: Path):
|
||||
vision_session = OnnxRunner(bundle_dir / "driving_vision.onnx")
|
||||
policy_session = OnnxRunner(bundle_dir / "driving_policy.onnx")
|
||||
|
||||
def seed_inputs(session):
|
||||
seeded = {}
|
||||
for name, spec in session.graph_inputs.items():
|
||||
dtype_text = str(spec.dtype).lower()
|
||||
if "uchar" in dtype_text or "uint8" in dtype_text:
|
||||
seeded[name] = Tensor(np.zeros(spec.shape, dtype=np.uint8))
|
||||
elif "half" in dtype_text or "float16" in dtype_text:
|
||||
seeded[name] = Tensor(np.zeros(spec.shape, dtype=np.float16))
|
||||
else:
|
||||
seeded[name] = Tensor(np.zeros(spec.shape, dtype=np.float32))
|
||||
return seeded
|
||||
|
||||
return (
|
||||
vision_session(seed_inputs(vision_session))["outputs"].numpy().flatten(),
|
||||
policy_session(seed_inputs(policy_session))["outputs"].numpy().flatten(),
|
||||
)
|
||||
|
||||
|
||||
def test_three_selector_models_parse_via_share_onnx():
|
||||
if not SHARE_ROOT.is_dir():
|
||||
return
|
||||
selector_dirs = _find_selector_dirs(limit=3, require_onnx=True)
|
||||
assert len(selector_dirs) >= 3
|
||||
|
||||
for bundle_dir in selector_dirs:
|
||||
vision_raw, policy_raw = _run_onnx_bundle(bundle_dir)
|
||||
assert vision_raw.size > 0
|
||||
assert policy_raw.size > 0
|
||||
|
||||
|
||||
def test_selector_tinygrad_pkls_execute_when_host_compatible(monkeypatch):
|
||||
if not SHARE_ROOT.is_dir():
|
||||
return
|
||||
selector_dirs = _find_selector_dirs(limit=10)
|
||||
attempted = 0
|
||||
executed = 0
|
||||
|
||||
for bundle_dir in selector_dirs:
|
||||
attempted += 1
|
||||
try:
|
||||
vision_outputs, policy_outputs = _run_tinygrad_bundle(bundle_dir, monkeypatch)
|
||||
except AssertionError as exc:
|
||||
if "Model was built on C3 or C3X" in str(exc):
|
||||
continue
|
||||
raise
|
||||
except FileNotFoundError as exc:
|
||||
if "/dev/kgsl-3d0" in str(exc):
|
||||
continue
|
||||
raise
|
||||
except TypeError as exc:
|
||||
if "DType.__init__()" in str(exc):
|
||||
continue
|
||||
raise
|
||||
|
||||
assert "pose" in vision_outputs
|
||||
assert "plan" in policy_outputs
|
||||
executed += 1
|
||||
if executed >= 3:
|
||||
break
|
||||
|
||||
if executed == 0:
|
||||
assert attempted > 0, "no selector bundles were inspected on the share"
|
||||
@@ -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()
|
||||
@@ -0,0 +1,254 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from iqpilot.cereal import custom
|
||||
from iqpilot.selfdrive.iqmodeld.models import helpers as model_helpers
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad import supercombo_runner as supercombo_runner_mod
|
||||
from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.supercombo_runner import (
|
||||
TinygradSupercomboRunner,
|
||||
)
|
||||
|
||||
|
||||
class _Captured:
|
||||
def __init__(self, expected_names):
|
||||
self.expected_names = expected_names
|
||||
|
||||
|
||||
class _FakeJit:
|
||||
def __init__(self, expected_names):
|
||||
self.captured = _Captured(expected_names)
|
||||
|
||||
|
||||
class _Boom:
|
||||
def __init__(self, err: Exception):
|
||||
self.err = err
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
raise self.err
|
||||
|
||||
|
||||
class _FakeParams:
|
||||
def __init__(self, active_bundle=None):
|
||||
self.store = {}
|
||||
if active_bundle is not None:
|
||||
self.store["ModelManager_ActiveBundle"] = active_bundle
|
||||
|
||||
def get(self, key):
|
||||
return self.store.get(key)
|
||||
|
||||
def put(self, key, value):
|
||||
self.store[key] = value
|
||||
|
||||
def remove(self, key):
|
||||
self.store.pop(key, None)
|
||||
|
||||
|
||||
def test_verify_artifact_file_deletes_stale_cached_pkl(tmp_path: Path):
|
||||
pkl_path = tmp_path / "driving_supercombo_guard.pkl"
|
||||
pkl_path.write_bytes(b"stale-pkl")
|
||||
|
||||
runner = TinygradSupercomboRunner.__new__(TinygradSupercomboRunner)
|
||||
runner._pkl_path = str(pkl_path)
|
||||
runner._expected_sha256 = hashlib.sha256(b"fresh-pkl").hexdigest()
|
||||
|
||||
with pytest.raises(RuntimeError, match="SHA mismatch"):
|
||||
runner._verify_artifact_file()
|
||||
|
||||
assert not pkl_path.exists()
|
||||
|
||||
|
||||
def test_validate_jit_names_accepts_current_runtime_contract():
|
||||
runner = TinygradSupercomboRunner.__new__(TinygradSupercomboRunner)
|
||||
runner._pkl_path = "/tmp/does-not-matter.pkl"
|
||||
runner._expected_sha256 = ""
|
||||
runner._run_policy = _FakeJit(['warped', 'img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs'])
|
||||
runner._warp_jits = {
|
||||
(1344, 760): _FakeJit(['tfm', 'big_tfm', 'frame', 'big_frame']),
|
||||
}
|
||||
|
||||
runner._validate_jit_names()
|
||||
|
||||
|
||||
def test_validate_jit_names_raises_clear_error_for_contract_mismatch():
|
||||
runner = TinygradSupercomboRunner.__new__(TinygradSupercomboRunner)
|
||||
runner._pkl_path = "/tmp/does-not-matter.pkl"
|
||||
runner._expected_sha256 = ""
|
||||
runner._run_policy = _FakeJit(['img', 'big_img', 'feat_q', 'desire_q', 'desire', 'traffic_convention', 'action_t'])
|
||||
runner._warp_jits = {
|
||||
(1344, 760): _FakeJit(['img_q', 'big_img_q', 'tfm', 'big_tfm', 'frame', 'big_frame']),
|
||||
}
|
||||
|
||||
with pytest.raises(RuntimeError, match="JIT argument mismatch"):
|
||||
runner._validate_jit_names()
|
||||
|
||||
|
||||
def test_handle_runtime_jit_mismatch_deletes_stale_cached_pkl(tmp_path: Path):
|
||||
pkl_path = tmp_path / "driving_supercombo_guard.pkl"
|
||||
pkl_path.write_bytes(b"stale-pkl")
|
||||
|
||||
runner = TinygradSupercomboRunner.__new__(TinygradSupercomboRunner)
|
||||
runner._pkl_path = str(pkl_path)
|
||||
runner._expected_sha256 = hashlib.sha256(b"fresh-pkl").hexdigest()
|
||||
|
||||
with pytest.raises(RuntimeError, match="runtime JIT mismatch with stale cached SHA"):
|
||||
runner._handle_runtime_jit_mismatch(RuntimeError("args mismatch in JIT: stale bundle"))
|
||||
|
||||
assert not pkl_path.exists()
|
||||
|
||||
|
||||
def test_handle_runtime_jit_mismatch_raises_clear_error_without_sha_mismatch(tmp_path: Path):
|
||||
pkl_path = tmp_path / "driving_supercombo_guard.pkl"
|
||||
pkl_path.write_bytes(b"fresh-pkl")
|
||||
|
||||
runner = TinygradSupercomboRunner.__new__(TinygradSupercomboRunner)
|
||||
runner._pkl_path = str(pkl_path)
|
||||
runner._expected_sha256 = hashlib.sha256(b"fresh-pkl").hexdigest()
|
||||
|
||||
with pytest.raises(RuntimeError, match="runtime JIT mismatch"):
|
||||
runner._handle_runtime_jit_mismatch(RuntimeError("args mismatch in JIT: wrong contract"))
|
||||
|
||||
|
||||
def test_schedule_active_bundle_redownload_sets_download_index(monkeypatch: pytest.MonkeyPatch):
|
||||
params = _FakeParams({"index": 81})
|
||||
monkeypatch.setattr(supercombo_runner_mod, "Params", lambda: params)
|
||||
|
||||
runner = TinygradSupercomboRunner.__new__(TinygradSupercomboRunner)
|
||||
msg = runner._schedule_active_bundle_redownload()
|
||||
|
||||
assert params.get("ModelManager_DownloadIndex") == "81"
|
||||
assert msg == "; scheduled automatic re-download of the active model"
|
||||
|
||||
|
||||
def test_no_active_bundle_seeds_default_tinygrad(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(model_helpers, "ensure_default_model_files", lambda *a, **k: None)
|
||||
params = _FakeParams()
|
||||
|
||||
runner = model_helpers.get_active_model_runner(params)
|
||||
|
||||
assert runner == custom.IQModelManager.Runner.tinygrad
|
||||
active = params.get("ModelManager_ActiveBundle")
|
||||
assert active is not None and active.get("ref") == "default"
|
||||
|
||||
|
||||
def test_select_default_model_clears_custom_download_state(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
||||
pending_restore = tmp_path / "pending_model_restore"
|
||||
pending_restore.write_text("Pop")
|
||||
monkeypatch.setattr(model_helpers, "_PENDING_MODEL_RESTORE_FILE", str(pending_restore))
|
||||
monkeypatch.setattr(model_helpers, "ensure_default_model_files", lambda *a, **k: None)
|
||||
|
||||
params = _FakeParams({"index": 81, "ref": "pop"})
|
||||
params.put("ModelManager_DownloadIndex", "81")
|
||||
params.put("ModelRunnerTypeCache", int(custom.IQModelManager.Runner.tinygrad))
|
||||
|
||||
model_helpers.select_default_model(params)
|
||||
|
||||
assert params.get("ModelManager_DownloadIndex") is None
|
||||
active = params.get("ModelManager_ActiveBundle")
|
||||
assert active is not None and active.get("ref") == "default"
|
||||
assert int(params.get("ModelRunnerTypeCache")) == int(custom.IQModelManager.Runner.tinygrad)
|
||||
assert not pending_restore.exists()
|
||||
|
||||
|
||||
def test_seed_default_bundle_runs_while_a_download_is_queued(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(model_helpers, "ensure_default_model_files", lambda *a, **k: None)
|
||||
|
||||
params = _FakeParams()
|
||||
params.put("ModelManager_DownloadIndex", "81")
|
||||
|
||||
model_helpers.seed_default_bundle_if_unset(params)
|
||||
|
||||
active = params.get("ModelManager_ActiveBundle")
|
||||
assert active is not None and active.get("ref") == "default"
|
||||
assert params.get("ModelManager_DownloadIndex") == "81"
|
||||
|
||||
|
||||
def test_seed_default_bundle_leaves_an_existing_active_bundle_alone(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(model_helpers, "ensure_default_model_files", lambda *a, **k: None)
|
||||
|
||||
params = _FakeParams({"index": 81, "ref": "pop"})
|
||||
params.put("ModelManager_DownloadIndex", "81")
|
||||
|
||||
model_helpers.seed_default_bundle_if_unset(params)
|
||||
|
||||
assert params.get("ModelManager_ActiveBundle").get("ref") == "pop"
|
||||
assert params.get("ModelManager_DownloadIndex") == "81"
|
||||
|
||||
|
||||
def test_default_model_is_not_resolved_to_manifest_pop_bundle():
|
||||
pop_bundle = type("Bundle", (), {"internalName": "Pop (Default)", "displayName": "Pop (Default)"})()
|
||||
|
||||
assert model_helpers.get_default_model_bundle([pop_bundle]) is None
|
||||
|
||||
|
||||
def test_verify_artifact_file_schedules_redownload_for_stale_cached_pkl(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
||||
params = _FakeParams({"index": 81})
|
||||
monkeypatch.setattr(supercombo_runner_mod, "Params", lambda: params)
|
||||
|
||||
pkl_path = tmp_path / "driving_supercombo_guard.pkl"
|
||||
pkl_path.write_bytes(b"stale-pkl")
|
||||
|
||||
runner = TinygradSupercomboRunner.__new__(TinygradSupercomboRunner)
|
||||
runner._pkl_path = str(pkl_path)
|
||||
runner._expected_sha256 = hashlib.sha256(b"fresh-pkl").hexdigest()
|
||||
|
||||
with pytest.raises(RuntimeError, match="scheduled automatic re-download"):
|
||||
runner._verify_artifact_file()
|
||||
|
||||
assert params.get("ModelManager_DownloadIndex") == "81"
|
||||
assert not pkl_path.exists()
|
||||
|
||||
|
||||
def test_run_fused_converts_raw_warp_jit_mismatch_to_runtime_error(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
||||
pkl_path = tmp_path / "driving_supercombo_guard.pkl"
|
||||
pkl_path.write_bytes(b"fresh-pkl")
|
||||
|
||||
runner = TinygradSupercomboRunner.__new__(TinygradSupercomboRunner)
|
||||
runner._pkl_path = str(pkl_path)
|
||||
runner._expected_sha256 = hashlib.sha256(b"fresh-pkl").hexdigest()
|
||||
runner._frame_skip = 4
|
||||
runner._cam = (1344, 760)
|
||||
runner._queues = {
|
||||
"tfm": object(),
|
||||
"big_tfm": object(),
|
||||
"img_q": object(),
|
||||
"big_img_q": object(),
|
||||
"feat_q": object(),
|
||||
"desire_q": object(),
|
||||
"packed_npy_inputs": object(),
|
||||
}
|
||||
runner._npy = {
|
||||
"tfm": [0.0],
|
||||
"big_tfm": [0.0],
|
||||
"desire": [0.0],
|
||||
"prev_feat": [0.0],
|
||||
}
|
||||
runner._prev_desire = [0.0]
|
||||
runner._warp_jits = {
|
||||
(1344, 760): _Boom(RuntimeError("args mismatch in JIT: self.captured.expected_names=['big_frame'] != ['frame']")),
|
||||
}
|
||||
runner._run_policy = _FakeJit(["warped", "img_q", "big_img_q", "feat_q", "desire_q", "packed_npy_inputs"])
|
||||
runner._hidden_slice = slice(0, 1)
|
||||
runner._slices = {"out": slice(0, 1)}
|
||||
runner._parser = type("P", (), {"parse_vision_outputs": staticmethod(lambda sliced: sliced)})()
|
||||
runner._frame_tensor = lambda *args, **kwargs: object()
|
||||
|
||||
monkeypatch.setattr(TinygradSupercomboRunner, "_ensure_queues", lambda self, cam_w, cam_h: None)
|
||||
|
||||
class _Buf:
|
||||
width = 1344
|
||||
height = 760
|
||||
data = memoryview(b"\x00")
|
||||
|
||||
with pytest.raises(RuntimeError, match="runtime JIT mismatch"):
|
||||
runner.run_fused(
|
||||
{"img": _Buf(), "big_img": _Buf()},
|
||||
{"img": [0.0], "big_img": [0.0]},
|
||||
{},
|
||||
)
|
||||
155
iqpilot/selfdrive/iqmodeld/tests/test_temporal_replay.py
Normal file
155
iqpilot/selfdrive/iqmodeld/tests/test_temporal_replay.py
Normal file
@@ -0,0 +1,155 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from types import SimpleNamespace
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
import iqpilot.selfdrive.iqmodeld.models.helpers as bundle_helpers
|
||||
import iqpilot.selfdrive.iqmodeld.models.runners.model_runner as runner_helpers
|
||||
import iqpilot.selfdrive.iqmodeld.daemon as iqmodeld_daemon
|
||||
|
||||
|
||||
@dataclass
|
||||
class StubOverride:
|
||||
key: str
|
||||
value: str
|
||||
|
||||
|
||||
class StubBundle:
|
||||
def __init__(self, generation: int = 10):
|
||||
self.overrides = [StubOverride("lat", ".1"), StubOverride("long", ".3")]
|
||||
self.generation = generation
|
||||
|
||||
|
||||
class StubRunner:
|
||||
def __init__(self, input_shapes: dict[str, tuple[int, ...]]) -> None:
|
||||
self.input_shapes = input_shapes
|
||||
self.constants = SimpleNamespace(
|
||||
FULL_HISTORY_BUFFER_LEN=100,
|
||||
FEATURE_LEN=512,
|
||||
DESIRE_LEN=8,
|
||||
PREV_DESIRED_CURV_LEN=1,
|
||||
INPUT_HISTORY_BUFFER_LEN=25,
|
||||
TEMPORAL_SKIP=4,
|
||||
)
|
||||
self.vision_input_names: list[str] = []
|
||||
self.is_20hz = input_shapes.get(next(iter(input_shapes)), (1, 0, 0))[1] == 25
|
||||
|
||||
def prepare_inputs(self, imgs_cl, numpy_inputs, frames):
|
||||
return None
|
||||
|
||||
def run_model(self):
|
||||
return {
|
||||
"hidden_state": np.zeros((1, self.constants.FEATURE_LEN), dtype=np.float32),
|
||||
"desired_curvature": np.zeros((1, 1), dtype=np.float32),
|
||||
}
|
||||
|
||||
|
||||
def _install_runtime(monkeypatch: pytest.MonkeyPatch, shapes: dict[str, tuple[int, ...]], generation: int = 10):
|
||||
bundle = StubBundle(generation=generation)
|
||||
runner = StubRunner(shapes)
|
||||
monkeypatch.setattr(bundle_helpers, "get_active_bundle", lambda params=None: bundle, raising=False)
|
||||
monkeypatch.setattr(runner_helpers, "get_model_runner", lambda: runner, raising=False)
|
||||
monkeypatch.setattr(iqmodeld_daemon, "get_active_bundle", lambda params=None: bundle, raising=False)
|
||||
monkeypatch.setattr(iqmodeld_daemon, "get_model_runner", lambda: runner, raising=False)
|
||||
return iqmodeld_daemon.NeuralEngineState(None), runner
|
||||
|
||||
|
||||
def _expected_selector_indices(shape: tuple[int, ...], mode: str) -> np.ndarray | None:
|
||||
if mode == "split":
|
||||
full = 100
|
||||
return np.arange(full)[-1 - (4 * (25 - 1))::4]
|
||||
if mode == "20hz":
|
||||
step = int(-100 / shape[1])
|
||||
return np.arange(step, step * (shape[1] + 1), step)[::-1]
|
||||
if mode == "dense":
|
||||
return np.arange(shape[1])
|
||||
return None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("shapes", "mode"),
|
||||
[
|
||||
({"desire": (1, 100, 8), "features_buffer": (1, 99, 512), "prev_desired_curv": (1, 100, 1)}, "dense"),
|
||||
({"desire": (1, 25, 8), "features_buffer": (1, 24, 512)}, "20hz"),
|
||||
({"desire_pulse": (1, 25, 8), "features_buffer": (1, 25, 512)}, "split"),
|
||||
],
|
||||
)
|
||||
def test_replay_ledger_layout_matches_expected_history(monkeypatch: pytest.MonkeyPatch,
|
||||
shapes: dict[str, tuple[int, ...]],
|
||||
mode: str):
|
||||
state, _runner = _install_runtime(monkeypatch, shapes)
|
||||
|
||||
for tensor_name, tensor_shape in shapes.items():
|
||||
history = state.temporal_buffers.get(tensor_name)
|
||||
selector = state.temporal_idxs_map.get(tensor_name)
|
||||
if history is None:
|
||||
continue
|
||||
|
||||
if mode == "dense":
|
||||
expected_shape = (1, tensor_shape[1], tensor_shape[2])
|
||||
else:
|
||||
expected_shape = (1, 100, tensor_shape[2])
|
||||
|
||||
assert history.shape == expected_shape
|
||||
expected_selector = _expected_selector_indices(tensor_shape, mode)
|
||||
if expected_selector is None:
|
||||
assert selector is None or selector.size == 0
|
||||
else:
|
||||
assert np.array_equal(selector, expected_selector)
|
||||
|
||||
|
||||
def test_replay_ledger_rising_edge_and_hidden_state_updates(monkeypatch: pytest.MonkeyPatch):
|
||||
state, runner = _install_runtime(monkeypatch, {
|
||||
"desire": (1, 100, 8),
|
||||
"features_buffer": (1, 99, 512),
|
||||
"prev_desired_curv": (1, 100, 1),
|
||||
})
|
||||
|
||||
pulse = np.zeros(8, dtype=np.float32)
|
||||
pulse[3] = 1.0
|
||||
state.run({}, {}, {"desire": pulse})
|
||||
first_export = state.numpy_inputs["desire"].copy()
|
||||
assert np.count_nonzero(first_export) == 1
|
||||
|
||||
state.run({}, {}, {"desire": pulse})
|
||||
second_export = state.numpy_inputs["desire"].copy()
|
||||
assert np.count_nonzero(second_export) == 1
|
||||
assert second_export[0, -1, 3] == 0.0
|
||||
|
||||
hidden_value = np.arange(runner.constants.FEATURE_LEN, dtype=np.float32)
|
||||
|
||||
def hidden_state_run():
|
||||
return {
|
||||
"hidden_state": hidden_value.reshape(1, -1),
|
||||
"desired_curvature": np.array([[0.25]], dtype=np.float32),
|
||||
}
|
||||
|
||||
state.model_runner.run_model = hidden_state_run
|
||||
state.run({}, {}, {"desire": np.zeros(8, dtype=np.float32)})
|
||||
|
||||
np.testing.assert_allclose(state.numpy_inputs["features_buffer"][0, -1], hidden_value, rtol=0, atol=0)
|
||||
assert state.numpy_inputs["prev_desired_curv"][0, -1, 0] == pytest.approx(0.25)
|
||||
|
||||
|
||||
def test_replay_ledger_zeroes_feedback_for_mlsim_generation(monkeypatch: pytest.MonkeyPatch):
|
||||
state, _runner = _install_runtime(monkeypatch, {
|
||||
"desire": (1, 100, 8),
|
||||
"features_buffer": (1, 99, 512),
|
||||
"prev_desired_curv": (1, 100, 1),
|
||||
}, generation=11)
|
||||
|
||||
def ml_run():
|
||||
return {
|
||||
"hidden_state": np.zeros((1, 512), dtype=np.float32),
|
||||
"desired_curvature": np.array([[1.5]], dtype=np.float32),
|
||||
}
|
||||
|
||||
state.model_runner.run_model = ml_run
|
||||
state.run({}, {}, {"desire": np.zeros(8, dtype=np.float32)})
|
||||
assert np.count_nonzero(state.numpy_inputs["prev_desired_curv"]) == 0
|
||||
17
iqpilot/selfdrive/iqmodeld/tests/tf_test/build.sh
Executable file
17
iqpilot/selfdrive/iqmodeld/tests/tf_test/build.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TF_ROOT="${TF_ROOT:-/home/batman/one/external/tensorflow}"
|
||||
TF_INCLUDE_DIR="${TF_INCLUDE_DIR:-$TF_ROOT/include}"
|
||||
TF_LIB_DIR="${TF_LIB_DIR:-$TF_ROOT/lib}"
|
||||
CXX="${CXX:-clang++}"
|
||||
|
||||
exec "$CXX" \
|
||||
-std=c++17 \
|
||||
-I "$TF_INCLUDE_DIR" \
|
||||
-L "$TF_LIB_DIR" \
|
||||
-Wl,-rpath="$TF_LIB_DIR" \
|
||||
main.cc \
|
||||
-ltensorflow
|
||||
32
iqpilot/selfdrive/iqmodeld/tests/tf_test/pb_loader.py
Executable file
32
iqpilot/selfdrive/iqmodeld/tests/tf_test/pb_loader.py
Executable file
@@ -0,0 +1,32 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import tensorflow as tf
|
||||
|
||||
|
||||
def _load_graph_bytes(graph_path: Path) -> bytes:
|
||||
return graph_path.read_bytes()
|
||||
|
||||
|
||||
def _parse_graph(graph_path: Path) -> tf.compat.v1.GraphDef:
|
||||
graph = tf.compat.v1.GraphDef()
|
||||
graph.ParseFromString(_load_graph_bytes(graph_path))
|
||||
return graph
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
if len(argv) < 2:
|
||||
print("Usage: pb_loader.py <graph.pb>")
|
||||
return 1
|
||||
_parse_graph(Path(argv[1]))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv))
|
||||
54
iqpilot/selfdrive/iqmodeld/tests/timing/benchmark.py
Executable file
54
iqpilot/selfdrive/iqmodeld/tests/timing/benchmark.py
Executable file
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
import iqpilot.cereal.messaging as messaging
|
||||
from iqpilot.system.manager.process_config import managed_processes
|
||||
|
||||
RUN_COUNT = int(os.getenv("N", "5"))
|
||||
WINDOW_SECONDS = int(os.getenv("TIME", "30"))
|
||||
WARMUP_MESSAGES = 10
|
||||
|
||||
|
||||
def _collect_execution_samples(sock, duration_s: int) -> np.ndarray:
|
||||
samples: list[float] = []
|
||||
deadline = time.monotonic() + duration_s
|
||||
while time.monotonic() < deadline:
|
||||
for message in messaging.drain_sock(sock, wait_for_one=True):
|
||||
samples.append(message.modelV2.modelExecutionTime)
|
||||
return np.array(samples[WARMUP_MESSAGES:]) * 1000.0
|
||||
|
||||
|
||||
def _single_benchmark_pass(sock) -> np.ndarray:
|
||||
os.environ["LOGPRINT"] = "debug"
|
||||
managed_processes["modeld"].start()
|
||||
time.sleep(5)
|
||||
try:
|
||||
return _collect_execution_samples(sock, WINDOW_SECONDS)
|
||||
finally:
|
||||
managed_processes["modeld"].stop()
|
||||
|
||||
|
||||
def _report_run(index: int, values_ms: np.ndarray) -> None:
|
||||
print(
|
||||
f"run {index}: avg={values_ms.mean():0.2f}ms "
|
||||
f"min={values_ms.min():0.2f}ms max={values_ms.max():0.2f}ms"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
subscriber = messaging.sub_sock("modelV2", conflate=False, timeout=1000)
|
||||
all_runs = [_single_benchmark_pass(subscriber) for _ in range(RUN_COUNT)]
|
||||
|
||||
print("\n")
|
||||
print(f"ran modeld {RUN_COUNT} times for {WINDOW_SECONDS}s each")
|
||||
for index, values_ms in enumerate(all_runs, start=1):
|
||||
_report_run(index, values_ms)
|
||||
print("\n")
|
||||
Reference in New Issue
Block a user