"""CPU-only regressions for modeld startup scheduling and runner-owned fallback.""" import ast from pathlib import Path from types import SimpleNamespace from unittest.mock import Mock import pytest from jetlink.openpilot.testing import FakeJoinedModel from openpilot.starpilot.models.status import ModelVariant MODEL_SOURCE = Path(__file__).resolve().parents[3] / "selfdrive/modeld/modeld.py" def main_body(): return next(node for node in ast.parse(MODEL_SOURCE.read_text()).body if isinstance(node, ast.FunctionDef) and node.name == "main").body def test_realtime_starts_after_model_load_and_receipt(): # Inspect startup without importing modeld's GPU runtime or loading artifacts. calls = [(node.lineno, ast.unparse(node.func)) for statement in main_body() for node in ast.walk(statement) if isinstance(node, ast.Call)] def line(name): return [lineno for lineno, function in calls if function == name] assert len(line("gc.disable")) == len(line("config_realtime_process")) == 1 assert line("gc.disable")[0] < min(line("load_small") + line("loader.join")) startup_receipt = next(statement for statement in main_body() if isinstance(statement, ast.Expr) and isinstance(statement.value, ast.Call) and ast.unparse(statement.value.func) == "receipt_owner.loaded") assert max(line("load_small") + line("loader.join")) < startup_receipt.lineno < line("config_realtime_process")[0] < line("PubMaster")[0] def test_jetlink_gets_the_frames_delay_before_every_run(): # a model that joins starts on the current delay; check_seam holds the frame_drop_ratio write loop = [node for node in main_body() if isinstance(node, ast.While)][-1] # the frame loop statements = [node for node in ast.walk(loop) if isinstance(node, (ast.Assign, ast.Try))] def line(source): return next(node.lineno for node in statements if isinstance(node, ast.Assign) and ast.unparse(node) == source) run = next(node.lineno for node in statements if isinstance(node, ast.Try) and any(isinstance(child, ast.Call) and ast.unparse(child.func) == "model.run" for child in ast.walk(node))) assert (line("subaru_tau = subaru_smooth_seconds(v_ego, subaru_smoothing)") < line("lat_delay = sm['lateralDelay'].lateralDelay + LAT_SMOOTH_SECONDS + subaru_tau") < line("model.lat_delay = lat_delay") < run) assignments = {ast.unparse(node.targets[0]): node for node in statements if isinstance(node, ast.Assign)} # Reuse the same joined runner across frames: each write must include the current delay and smoothing. model = SimpleNamespace(lat_delay=-1.) for delay, tau in ((.1, 0.), (.25, .4), (.05, 2.)): env = {"sm": {"lateralDelay": SimpleNamespace(lateralDelay=delay)}, "LAT_SMOOTH_SECONDS": .03, "subaru_tau": tau, "model": model} code = ast.Module(body=[assignments["lat_delay"], assignments["model.lat_delay"]], type_ignores=[]) exec(compile(ast.fix_missing_locations(code), str(MODEL_SOURCE), "exec"), env) assert model.lat_delay == pytest.approx(delay + .03 + tau) @pytest.mark.parametrize("runner_chestnut,stored_active", [(True, False), (True, True), (False, True), (False, False)]) def test_inference_failure_uses_runner_identity(runner_chestnut, stored_active): # Run the production try/except statement with deliberately stale status Params. fallback = next(node for statement in main_body() for node in ast.walk(statement) if isinstance(node, ast.Try) and any(isinstance(child, ast.Call) and ast.unparse(child.func) == "model.run" for child in ast.walk(node))) failure = RuntimeError("inference failed") failed_model = SimpleNamespace(chestnut=runner_chestnut, run=Mock(side_effect=failure)) small_model = SimpleNamespace(chestnut=False, model_id="fallback-small") params = Mock() params.get_bool.return_value = stored_active receipt = Mock() chestnut_state = SimpleNamespace(big=True) env: dict = {"model": failed_model, "small_model": small_model, "params": params, "receipt_owner": receipt, "chestnut_state": chestnut_state, "run_count": 1, "ModelConstants": SimpleNamespace(MODEL_RUN_FREQ=20), "SERVICE_LIST": {"chestnutGpuState": SimpleNamespace(frequency=1)}, "bufs": {}, "transforms": {}, "inputs": {}, "small_prepared": object(), "ModelVariant": ModelVariant, "jetlink_model": None, "drop_chestnut": Mock(), "cloudlog": Mock()} code = compile(ast.fix_missing_locations(ast.Module(body=[fallback], type_ignores=[])), str(MODEL_SOURCE), "exec") if runner_chestnut: exec(code, env) assert env["model"] is small_model assert env["model_output"] is None assert env["run_count"] == 0 assert not chestnut_state.big env["drop_chestnut"].assert_called_once_with() params.put_bool.assert_called_once_with("ChestnutActive", False) receipt.loaded.assert_called_once_with(env["small_prepared"], ModelVariant.SMALL, "chestnut-load-failed", model_id="fallback-small") else: with pytest.raises(RuntimeError, match="inference failed"): exec(code, env) assert env["model"] is failed_model params.put_bool.assert_not_called() env["drop_chestnut"].assert_not_called() receipt.loaded.assert_not_called() params.get_bool.assert_not_called() def test_remote_inference_failure_does_not_change_chestnut_status(): fallback = next(node for statement in main_body() for node in ast.walk(statement) if isinstance(node, ast.Try) and any(isinstance(child, ast.Call) and ast.unparse(child.func) == "model.run" for child in ast.walk(node))) failed_model = FakeJoinedModel(chestnut=True) failed_model.run = Mock(side_effect=RuntimeError("remote inference failed")) failed_model.close = Mock() small_model = SimpleNamespace(chestnut=False, model_id="fallback-small") params, receipt = Mock(), Mock() env = {"model": failed_model, "small_model": small_model, "params": params, "receipt_owner": receipt, "chestnut_state": None, "run_count": 1, "bufs": {}, "transforms": {}, "inputs": {}, "small_prepared": object(), "ModelVariant": ModelVariant, "jetlink_model": failed_model, "drop_chestnut": Mock(), "cloudlog": Mock()} execute([fallback], env) failed_model.close.assert_called_once_with() env["drop_chestnut"].assert_not_called() assert env["model"] is small_model assert env["model_output"] is None assert env["run_count"] == 0 params.put_bool.assert_not_called() assert receipt.loaded.call_args.args == (env["small_prepared"], ModelVariant.SMALL, None) assert receipt.loaded.call_args.kwargs["model_id"] == "fallback-small" def execute(statements, env): def exit_process(code): raise SystemExit(code) env.setdefault("os", SimpleNamespace(getenv=lambda key: None, _exit=exit_process)) env.setdefault("recovery_small_only", False) env.setdefault("coherent_warp", None) env.setdefault("set_core_affinity", Mock()) from openpilot.starpilot.models.recovery import STARTUP_FALLBACK_EXIT env.setdefault("STARTUP_FALLBACK_EXIT", STARTUP_FALLBACK_EXIT) exec(compile(ast.fix_missing_locations(ast.Module(body=statements, type_ignores=[])), str(MODEL_SOURCE), "exec"), env) @pytest.mark.parametrize("present,cable,allow_big,big_path,compiled,expected,link", [ (True, False, True, "verified-big", False, True, False), (False, True, True, "verified-big", False, True, False), (False, True, True, None, True, False, False), (False, False, False, None, True, False, False), (True, False, False, "verified-big", True, False, False), (False, True, True, None, False, False, False), # with Jetlink on, the cable is its host: no chestnut wait, no Big Model Failed, the link joins (False, True, True, "verified-big", False, False, True), (True, True, True, "verified-big", False, True, True), ]) def test_observed_availability_and_big_eligibility(monkeypatch, present, cable, allow_big, big_path, compiled, expected, link): from types import ModuleType import sys manager = ModuleType("openpilot.starpilot.models.manager") manager.resolve_runtime = Mock(side_effect=lambda chestnut_available, randomize: SimpleNamespace(allow_big=allow_big and chestnut_available, big_path=big_path)) manager.requested_runtime_id = Mock(return_value="selected") amd = ModuleType("tinygrad.runtime.ops_amd") amd.AMDDevice = type("FakeAMDDevice", (), {"wait_timeout_ms": 123}) monkeypatch.setitem(sys.modules, manager.__name__, manager) monkeypatch.setitem(sys.modules, amd.__name__, amd) body = main_body() start = next(i for i, node in enumerate(body) if isinstance(node, ast.ImportFrom) and node.module == manager.__name__) end = next(i for i, node in enumerate(body) if ast.unparse(node) == "gc.disable()") params = Mock() env: dict = {"chestnut_present": Mock(return_value=present), "cable_connected": Mock(return_value=cable), "chestnut_compiled": Mock(return_value=compiled), "Params": lambda: params, "jetlink_adapter": SimpleNamespace(link_on=lambda: link)} execute(body[start:end], env) available = present or (cable and not link) manager.resolve_runtime.assert_called_once_with(chestnut_available=available, randomize=True) manager.requested_runtime_id.assert_called_once_with(available) env["chestnut_present"].assert_called_once_with() assert env["cable_connected"].call_count == (0 if present else 1) assert env["CHESTNUT"] is expected assert amd.AMDDevice.wait_timeout_ms == (3000 if expected else 123) params.put_bool.assert_called_once_with("ChestnutLoading", expected) @pytest.mark.parametrize("timeout,worker_alive", [(False, False), (True, False), (True, True)]) @pytest.mark.parametrize("custom", [False, True]) def test_big_worker_waits_before_load_and_failure_requests_fresh_small_process(custom, timeout, worker_alive, monkeypatch): from openpilot.starpilot.models import manager monkeypatch.setattr(manager, "shipped_default", lambda: Path("bundled-small")) body = main_body() start = next(i for i, node in enumerate(body) if isinstance(node, ast.Assign) and ast.unparse(node.targets[0]) == "st") end = next(i for i, node in enumerate(body) if ast.unparse(node).startswith("config_realtime_process(")) events = [] worker = False class Thread: def __init__(self, target, daemon): assert daemon self.target = target def start(self): nonlocal worker worker = True self.target() worker = False def join(self, duration): events.append(("join", duration)) def is_alive(self): return worker_alive def wait(): assert worker events.append("wait") if timeout: raise TimeoutError("chestnut did not enumerate") def state(width, height, chestnut): events.append("big" if chestnut else "small") return SimpleNamespace(chestnut=chestnut, model_id="big" if chestnut else "small", warmup=Mock()) def verified(width, height, path, version, chestnut, model_id, sha, **kwargs): assert kwargs == ({} if chestnut else {"coherent_warp": None}) return state(width, height, chestnut), "prepared-" + model_id params, receipt = Mock(), Mock() from jetlink.openpilot.hooks import Hooks from openpilot.starpilot.models.jetlink_adapter import API bind = Mock(side_effect=AssertionError("a chestnut drive must not start jetlink")) hooks = Hooks(API, bind) selection = SimpleNamespace(big_path="selected-big" if custom else None, small_path="selected-small" if custom else None, small_version=1, small_id="small", small_sha256="small-hash", big_version=1, big_id="big", big_sha256="hash") from openpilot.starpilot.models.catalog import BY_ID, DEFAULT_SMALL, DEFAULT_SMALL_SHA256 env: dict = {"BY_ID": BY_ID, "DEFAULT_SMALL": DEFAULT_SMALL, "DEFAULT_SMALL_SHA256": DEFAULT_SMALL_SHA256, "time": SimpleNamespace(monotonic=lambda: 0), "cloudlog": Mock(), "selection": selection, "CHESTNUT": True, "vipc_client_main": SimpleNamespace(width=1928, height=1208), "wait_for_chestnut": wait, "ModelState": state, "load_verified_model": verified, "modeld_pkl_path": lambda big: "bundled-big" if big else "bundled-small", "receipt_owner": receipt, "threading": SimpleNamespace(Thread=Thread), "BIG_MODEL_TIMEOUT": 30, "report_load_timeout": Mock(), "params": params, "requested_model_id": "big", "ModelVariant": ModelVariant, "CP": SimpleNamespace(brand="mock"), "demo": False, "wait_for_chestnut_power": lambda CP, timeout: events.append("power"), "jetlink_adapter": SimpleNamespace(hooks=lambda: hooks, coherent_warp=lambda: None), "drop_chestnut": Mock(), "release_compile_pool": Mock(), "gc": Mock()} wrapper = ast.parse("def startup():\n pass").body[0] wrapper.body = body[start:end] + [ast.Return(value=ast.Call(func=ast.Name(id="locals", ctx=ast.Load()), args=[], keywords=[]))] execute([wrapper], env) if timeout: from openpilot.starpilot.models.recovery import STARTUP_FALLBACK_EXIT with pytest.raises(SystemExit) as failed: env["startup"]() assert failed.value.code == STARTUP_FALLBACK_EXIT env["drop_chestnut"].assert_not_called() receipt.loaded.assert_not_called() env["release_compile_pool"].assert_not_called() bind.assert_not_called() assert events.count("small") == int(custom) params.put_bool.assert_called_once_with("ChestnutActive", False) return env.update(env["startup"]()) bind.assert_not_called() assert env["jetlink_model"] is None assert events[0] == ("small" if custom else "wait") assert events.count("small") == 1 assert ("big" in events) is not timeout assert env["model"].chestnut is not timeout assert env["small_model"].chestnut is False assert events.index("wait") < events.index(("join", 30)) if not timeout: assert events.index("wait") < events.index("power") < events.index("big") assert env["drop_chestnut"].call_count == int(timeout) env["release_compile_pool"].assert_called_once_with() env["gc"].collect.assert_called_once_with() params.put_bool.assert_any_call("ChestnutActive", not timeout) params.put_bool.assert_any_call("ChestnutLoading", False) assert receipt.loaded.call_args.args[1] == (ModelVariant.SMALL if timeout else ModelVariant.CHESTNUT) assert receipt.loaded.call_args.args[2] == ("chestnut-load-failed" if timeout else None) def test_wait_for_enumeration_succeeds_without_real_sleep(monkeypatch): from openpilot.selfdrive.modeld import helpers present = Mock(side_effect=[False, False, True]) sleep = Mock() monkeypatch.setattr(helpers, "chestnut_present", present) monkeypatch.setattr(helpers, "time", SimpleNamespace(monotonic=Mock(side_effect=[0, 0, 0.1]), sleep=sleep)) helpers.wait_for_chestnut(timeout=1) assert present.call_count == 3 assert sleep.call_args_list == [((0.1,),), ((0.1,),)] def test_wait_for_enumeration_times_out_without_loading_hardware(monkeypatch): from openpilot.selfdrive.modeld import helpers sleep = Mock() monkeypatch.setattr(helpers, "chestnut_present", lambda: False) monkeypatch.setattr(helpers, "time", SimpleNamespace(monotonic=Mock(side_effect=[0, 0, 0.2]), sleep=sleep)) with pytest.raises(TimeoutError, match="chestnut did not enumerate"): helpers.wait_for_chestnut(timeout=0.1) sleep.assert_called_once_with(0.1) @pytest.mark.parametrize("previous", ["READY", "FAILED"]) def test_native_ui_keeps_loading_during_enumeration_then_reports_failure(previous): from enum import Enum source = MODEL_SOURCE.parents[1] / "ui/ui_state.py" body = ast.parse(source.read_text()).body chestnut_enum = next(node for node in body if isinstance(node, ast.ClassDef) and node.name == "ChestnutState") ui_class = next(node for node in body if isinstance(node, ast.ClassDef) and node.name == "UIState") update = next(node for node in ui_class.body if isinstance(node, ast.FunctionDef) and node.name == "_update_chestnut_state") display_time = next(node for node in ui_class.body if isinstance(node, ast.FunctionDef) and node.name == "display_time_ns") env: dict = {"Enum": Enum, "time": SimpleNamespace(monotonic_ns=lambda: 1_000_000_000)} execute([chestnut_enum, update, display_time], env) env["UIState"] = SimpleNamespace(display_time_ns=env["display_time_ns"]) states = env["ChestnutState"] class Messages(dict): recv_frame = {"modelV2": 2} alive = {"modelV2": True} valid = {"modelV2": True} logMonoTime = {"modelV2": 1_000_000_000} ui = SimpleNamespace(sm=Messages(deviceState=SimpleNamespace(chestnutPresent=False), modelV2=SimpleNamespace(big=False)), started=True, started_frame=1, chestnut_present=True, chestnut_compiled=True, chestnut_loading=True, chestnut_active=False, chestnut_output_seen=False, chestnut_state=states[previous]) env["_update_chestnut_state"](ui) assert ui.chestnut_state is states.FAILED ui.chestnut_loading = False env["_update_chestnut_state"](ui) assert ui.chestnut_state is states.FAILED ui.chestnut_present = False env["_update_chestnut_state"](ui) assert ui.chestnut_state is states.DISCONNECTED ui.chestnut_present, ui.chestnut_compiled = True, False env["_update_chestnut_state"](ui) assert ui.chestnut_state is states.FAILED ui.sm["deviceState"].chestnutPresent = True ui.sm["modelV2"].big = True ui.chestnut_active = True env["_update_chestnut_state"](ui) assert ui.chestnut_state is states.ACTIVE def test_recovery_child_resolves_small_without_randomizer_or_amd(monkeypatch): from types import ModuleType import sys manager = ModuleType("openpilot.starpilot.models.manager") manager.resolve_runtime = Mock(return_value=SimpleNamespace(allow_big=False, big_path=None)) manager.requested_runtime_id = Mock(return_value="requested-big") monkeypatch.setitem(sys.modules, manager.__name__, manager) body = main_body() start = next(i for i, node in enumerate(body) if isinstance(node, ast.ImportFrom) and node.module == manager.__name__) end = next(i for i, node in enumerate(body) if ast.unparse(node) == "gc.disable()") params = Mock() env: dict = {"chestnut_present": lambda: True, "cable_connected": lambda: True, "chestnut_compiled": Mock(side_effect=AssertionError("Big compiled probe forbidden")), "Params": lambda: params, "os": SimpleNamespace(getenv=lambda key: "1")} execute(body[start:end], env) manager.resolve_runtime.assert_called_once_with(chestnut_available=False, randomize=False) manager.requested_runtime_id.assert_called_once_with(True) assert env["CHESTNUT"] is False assert "AMDDevice" not in env assert params.put_bool.call_args_list == [(("ChestnutLoading", False),)] def test_recovery_small_receipt_reports_runtime_stall_not_load_failure(): statement = next(node for node in main_body() if isinstance(node, ast.Expr) and isinstance(node.value, ast.Call) and ast.unparse(node.value.func) == 'receipt_owner.loaded') receipt = Mock() env: dict = {'model': SimpleNamespace(chestnut=False, model_id='bundled-current'), 'small_prepared': object(), 'receipt_owner': receipt, 'ModelVariant': ModelVariant, 'initial_chestnut_fallback': False, 'recovery_small_only': True, 'selected_small_failed': False, 'requested_model_id': 'requested-big'} execute([statement], env) assert receipt.loaded.call_args.args[1:3] == (ModelVariant.SMALL, 'chestnut-run-stalled') assert receipt.loaded.call_args.kwargs['model_id'] == 'bundled-current' @pytest.mark.parametrize('fallback_failure', [None, 'missing', 'load']) def test_failed_selected_small_uses_only_verified_rdf_or_refuses_startup(monkeypatch, fallback_failure): from openpilot.starpilot.models import manager from openpilot.starpilot.models.catalog import BY_ID, DEFAULT_SMALL, DEFAULT_SMALL_SHA256 function = next(node for node in main_body() if isinstance(node, ast.FunctionDef) and node.name == 'load_small') rdf = SimpleNamespace(chestnut=False, model_id=DEFAULT_SMALL) shipped = Path('shipped-rdf.pkl') monkeypatch.setattr(manager, 'shipped_default', lambda: None if fallback_failure == 'missing' else shipped) loader = Mock(side_effect=[RuntimeError('selected failed'), RuntimeError('RDF failed') if fallback_failure == 'load' else (rdf, 'rdf-prepared')]) stock = Mock(side_effect=AssertionError('stock model must not load')) env: dict = {'selection': SimpleNamespace(small_path=Path('custom.pkl'), small_version='v15', small_id='gwm8223', small_sha256='custom'), 'vipc_client_main': SimpleNamespace(width=1928, height=1208), 'load_verified_model': loader, 'ModelState': stock, 'modeld_pkl_path': stock, 'cloudlog': Mock(), 'BY_ID': BY_ID, 'DEFAULT_SMALL': DEFAULT_SMALL, 'DEFAULT_SMALL_SHA256': DEFAULT_SMALL_SHA256} execute([function], env) if fallback_failure is None: assert env['load_small']() == (rdf, 'rdf-prepared', True) else: with pytest.raises(RuntimeError, match='Shipped RDFv4' if fallback_failure == 'missing' else 'RDF failed'): env['load_small']() stock.assert_not_called() if fallback_failure != 'missing': assert loader.call_args.args == (1928, 1208, shipped, 'v15', False, DEFAULT_SMALL, DEFAULT_SMALL_SHA256) @pytest.mark.parametrize("opened", [("QCOM",), ("QCOM", "AMD")]) def test_fallback_releases_only_chestnut_dependencies(opened): node = next(node for node in ast.parse(MODEL_SOURCE.read_text()).body if isinstance(node, ast.FunctionDef) and node.name == "drop_chestnut") class RuntimeDevice: def __init__(self): self.pending = {} class Devices(dict): _opened_devices = opened devices = Devices(QCOM=RuntimeDevice(), AMD=RuntimeDevice()) amd, unrelated = devices["AMD"], object() for device in devices.values(): device.pending = {amd: "gpu fence", unrelated: "other fence"} env: dict[str, object] = {"Device": devices} execute([node], env) drop = env["drop_chestnut"] assert callable(drop) drop() for name in opened: assert unrelated in devices[name].pending assert (amd in devices[name].pending) is ("AMD" not in opened)