IQ.Pilot Release Commit @ 8d3c939

This commit is contained in:
IQ.Lvbs CI [bot]
2026-08-30 22:55:58 -05:00
parent c9629a3603
commit 90015b2835
57 changed files with 776 additions and 98 deletions

View File

@@ -22,6 +22,9 @@
"iqpilot.konn3kt.service_health",
"iqpilot.selfdrive.car.vehicle_catalog",
"iqpilot.selfdrive.iqmodeld.config",
"iqpilot.selfdrive.iqmodeld.egpu_helpers",
"iqpilot.selfdrive.iqmodeld.egpu_model",
"iqpilot.selfdrive.iqmodeld.model_bundle_downloader",
"iqpilot.selfdrive.iqmodeld.models",
"iqpilot.selfdrive.iqmodeld.models.fetcher",
"iqpilot.selfdrive.iqmodeld.models.helpers",
@@ -40,6 +43,7 @@
"iqpilot.system.hardware.tici.iwlist",
"iqpilot.system.hardware.tici.lpa",
"iqpilot.system.hardware.tici.pins",
"iqpilot.system.hardware.usb",
"iqpilot.system.loggerd.xattr_cache",
"iqpilot.system.manager.process",
"iqpilot.system.manager.process_config",

View File

@@ -0,0 +1,209 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
"""
from __future__ import annotations
import hashlib
import json
import os
from iqpilot.common.swaglog import cloudlog
import urllib.request
from pathlib import Path
from iqpilot.system.hardware.usb import egpu_dock_ready
USB_SYSFS_ROOT = "/sys/bus/usb/devices"
FIRMWARE_MIRROR = os.getenv("IQ_EGPU_FIRMWARE_MIRROR", "/data/firmware/tinygrad")
TINYGRAD_CACHE = "/data/.cache"
COMMA_LFS_BATCH_URL = "https://gitlab.com/commaai/openpilot-lfs.git/info/lfs/objects/batch"
DOWNLOAD_CHUNK = 4 * 1024 * 1024
def usbgpu_present(sysfs_root: str = USB_SYSFS_ROOT) -> bool:
return egpu_dock_ready(Path(sysfs_root))
def egpu_present_consented(params, sysfs_root: str = USB_SYSFS_ROOT) -> bool:
try:
if params is not None and params.get_bool("IQEgpuDisabled"):
return False
except Exception:
pass
return usbgpu_present(sysfs_root)
def egpu_selected(params, sysfs_root: str = USB_SYSFS_ROOT) -> bool:
try:
if params is not None and params.get_bool("IQEgpuDisabled"):
return False
if params is not None and params.get_bool("IQEgpuEnabled"):
return True
except Exception:
pass
return usbgpu_present(sysfs_root)
def resolve_backend(emac_enabled: bool, egpu_enabled: bool, egpu_present: bool = False) -> str | None:
if egpu_present:
return "egpu"
if emac_enabled:
return "emac"
if egpu_enabled:
return "egpu"
return None
def egpu_pkl_path(meta: dict) -> str:
from iqpilot.system.hardware.hw import Paths
return os.path.join(Paths.model_root(), f"egpu_{meta['key']}_{meta['sha256'][:8]}_amd_tinygrad.pkl")
def egpu_policy_pkl_path(meta: dict) -> str:
from iqpilot.system.hardware.hw import Paths
return os.path.join(Paths.model_root(), f"egpu_{meta['key']}_{meta['sha256'][:8]}_amd_policy.pkl")
def egpu_oob_pkl_path(meta: dict) -> str:
from iqpilot.system.hardware.hw import Paths
return os.path.join(Paths.model_root(), f"egpu_{meta['key']}_{meta['sha256'][:8]}_amd_policy_oob.pkl")
def onnx_cache_path(meta: dict) -> str:
from iqpilot.system.hardware.hw import Paths
return os.path.join(Paths.model_root(), f"{meta['model_name']}_{meta['sha256'][:8]}.onnx")
def _sha256_file(path: str) -> str:
digest = hashlib.sha256()
with open(path, "rb") as f:
while chunk := f.read(DOWNLOAD_CHUNK):
digest.update(chunk)
return digest.hexdigest()
def quarantine_artifact(path: str, why: str) -> None:
try:
if os.path.isfile(path):
os.replace(path, path + ".unusable")
except OSError:
try:
os.remove(path)
except OSError:
pass
def local_onnx(meta: dict) -> str | None:
path = onnx_cache_path(meta)
if not os.path.isfile(path):
return None
size = int(meta.get("download", {}).get("size", 0))
if size and os.path.getsize(path) != size:
quarantine_artifact(path, "onnx size mismatch")
return None
if _sha256_file(path) != meta["sha256"]:
quarantine_artifact(path, "onnx sha256 mismatch")
return None
return path
def resolve_download_url(download_url: str, sha256: str, size: int, timeout: float = 30.0) -> str:
if download_url.startswith("commalfs:"):
oid = download_url.split(":", 1)[1]
body = json.dumps({"operation": "download", "transfers": ["basic"],
"objects": [{"oid": oid, "size": size}]}).encode()
req = urllib.request.Request(COMMA_LFS_BATCH_URL, data=body, headers={
"Accept": "application/vnd.git-lfs+json", "Content-Type": "application/vnd.git-lfs+json"})
with urllib.request.urlopen(req, timeout=timeout) as r:
d = json.load(r)
return d["objects"][0]["actions"]["download"]["href"]
return download_url
def download_onnx(meta: dict, progress_cb=None) -> str:
from iqpilot.selfdrive.iqmodeld.egpu_model import download_descriptor
download_url, size = download_descriptor(meta)
if not download_url:
raise RuntimeError(f"model {meta['key']} has no download source; stage the onnx at {onnx_cache_path(meta)}")
path = onnx_cache_path(meta)
os.makedirs(os.path.dirname(path), exist_ok=True)
try:
from iqpilot.selfdrive.iqmodeld.model_bundle_downloader import download_hf_file
return download_hf_file(f"onnx/{meta['sha256']}.onnx", path, meta["sha256"], int(size or 0), progress_cb=progress_cb)
except Exception as e:
cloudlog.warning(f"onnx {meta['key']} unavailable from HF ({e}); falling back to {download_url.split(':', 1)[0]}")
url = resolve_download_url(download_url, meta["sha256"], size)
tmp = path + ".part"
digest = hashlib.sha256()
got = 0
with urllib.request.urlopen(url, timeout=60) as r, open(tmp, "wb") as f:
while chunk := r.read(DOWNLOAD_CHUNK):
f.write(chunk)
digest.update(chunk)
got += len(chunk)
if progress_cb is not None and size:
progress_cb(got / size)
if size and got != size:
os.remove(tmp)
raise RuntimeError(f"onnx download truncated: {got}/{size} bytes")
if digest.hexdigest() != meta["sha256"]:
os.remove(tmp)
raise RuntimeError(f"onnx sha256 mismatch for {meta['key']}")
os.replace(tmp, path)
return path
def download_precompiled(meta: dict, progress_cb=None, policy: bool = False, oob: bool = False) -> str | None:
field = "egpu_oob_artifact" if oob else "egpu_policy_artifact" if policy else "egpu_artifact"
art = meta.get(field)
if not art or not (art.get("objects") or art.get("hf_path")):
return None
from iqpilot.selfdrive.iqmodeld.model_bundle_downloader import download_hf_file, download_lfs_bundle
dest = egpu_oob_pkl_path(meta) if oob else egpu_policy_pkl_path(meta) if policy else egpu_pkl_path(meta)
if art.get("hf_path"):
try:
return download_hf_file(art["hf_path"], dest, art["sha256"], int(art.get("size", 0)), progress_cb=progress_cb)
except Exception as e:
cloudlog.warning(f"precompiled {meta['key']} unavailable from HF ({e}); trying LFS")
if not art.get("objects"):
raise
return download_lfs_bundle(art["objects"], dest, art["sha256"], int(art.get("size", 0)), progress_cb=progress_cb)
def patch_tinygrad_fetch_fw() -> None:
import pathlib
import zstandard
from tinygrad import helpers
if getattr(helpers.fetch_fw, "_iq_patched", False):
return
_orig = helpers.fetch_fw
def fetch_fw(path, name, sha256):
mirror = pathlib.Path(FIRMWARE_MIRROR) / path / name
if mirror.is_file():
blob = mirror.read_bytes()
if hashlib.sha256(blob).hexdigest() == sha256:
return blob
p = pathlib.Path(f"/lib/firmware/{path}/{name}.zst")
if p.is_file():
blob = zstandard.ZstdDecompressor().stream_reader(p.read_bytes()).read()
if hashlib.sha256(blob).hexdigest() == sha256:
return blob
blob = _orig(path, name, sha256)
# The dock's GPU firmware otherwise lives only in tinygrad's per-user download cache, which is
# a network fetch the first time a new HOME sees it; onroad the car is usually offline.
try:
mirror.parent.mkdir(parents=True, exist_ok=True)
tmp = mirror.with_suffix(mirror.suffix + ".part")
tmp.write_bytes(blob)
os.replace(tmp, mirror)
except OSError:
pass
return blob
fetch_fw._iq_patched = True
helpers.fetch_fw = fetch_fw

View File

@@ -0,0 +1,9 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
"""
from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module
try:
load_private_module(__name__, "iqpilot_private.models.egpu_model")
except ProprietaryModuleMissing:
from iqpilot.models_private_src.egpu_model import *

View File

@@ -0,0 +1,211 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
"""
from __future__ import annotations
import hashlib
import json
import os
MODELS_BASE_URLS = (
"https://git.konn3kt.com/teal/IQModels/raw/branch/main",
"https://gitlvb.teallvbs.xyz/teal/IQModels/raw/branch/main",
)
CHUNK = 4 * 1024 * 1024
HTTP_TIMEOUT_S = 60.0
STREAM_RETRIES = 6
def _requests_auth():
import importlib
for mod in ("iqpilot_private.models.git_auth", "iqpilot.models_private_src.git_auth",
"iqpilot.selfdrive.iqmodeld.models.git_auth"):
try:
return importlib.import_module(mod).get_requests_auth()
except Exception:
continue
return None
def _hf():
import importlib
for mod in ("iqpilot_private.models.git_auth", "iqpilot.selfdrive.iqmodeld.models.git_auth"):
try:
m = importlib.import_module(mod)
return m.get_hf_headers(), m.hf_resolve_url
except Exception:
continue
return None, None
def download_hf_file(hf_path: str, dst: str, sha256: str, size: int, progress_cb=None) -> str:
import requests
headers, resolve = _hf()
if resolve is None:
raise RuntimeError("no HF credentials available")
url = resolve(hf_path)
os.makedirs(os.path.dirname(dst), exist_ok=True)
tmp = dst + ".hfpart"
last_error: Exception | None = None
for _attempt in range(STREAM_RETRIES):
try:
have = os.path.getsize(tmp) if os.path.isfile(tmp) else 0
if size and have > size:
os.remove(tmp)
have = 0
if not size or have < size:
req_headers = dict(headers)
if have:
req_headers["Range"] = f"bytes={have}-"
with requests.get(url, headers=req_headers, stream=True, timeout=HTTP_TIMEOUT_S, allow_redirects=True) as r:
r.raise_for_status()
if have and r.status_code != 206:
have = 0
with open(tmp, "ab" if have else "wb") as f:
got = have
for chunk in r.iter_content(CHUNK):
f.write(chunk)
got += len(chunk)
if progress_cb is not None and size:
progress_cb(min(1.0, got / size))
digest = hashlib.sha256()
with open(tmp, "rb") as f:
for chunk in iter(lambda: f.read(CHUNK), b""):
digest.update(chunk)
if size and os.path.getsize(tmp) != size:
raise RuntimeError(f"size mismatch: {os.path.getsize(tmp)}/{size} bytes")
if sha256 and digest.hexdigest() != sha256:
os.remove(tmp)
raise RuntimeError("sha256 mismatch")
os.replace(tmp, dst)
return dst
except Exception as e:
last_error = e
raise RuntimeError(f"HF download failed: {last_error}")
def _lfs_endpoint(base_url: str) -> str:
return base_url.split("/raw/", 1)[0] + ".git/info/lfs"
def _resolve_oid(session, base_url: str, oid: str, size: int, auth):
import requests
batch = session.post(f"{_lfs_endpoint(base_url)}/objects/batch",
data=json.dumps({"operation": "download", "transfers": ["basic"],
"objects": [{"oid": oid, "size": size}]}),
headers={"Content-Type": "application/vnd.git-lfs+json",
"Accept": "application/vnd.git-lfs+json"},
auth=auth, timeout=HTTP_TIMEOUT_S)
batch.raise_for_status()
entry = batch.json()["objects"][0]
if "actions" not in entry:
raise requests.RequestException(f"LFS object unavailable: {entry.get('error', oid)}")
action = entry["actions"]["download"]
return action["href"], action.get("header", {})
def _part_path(dst: str, oid: str) -> str:
return os.path.join(dst + ".parts", oid)
def _part_complete(path: str, oid: str, size: int) -> bool:
if not os.path.isfile(path) or os.path.getsize(path) != size:
return False
digest = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(CHUNK), b""):
digest.update(chunk)
return digest.hexdigest() == oid
def _fetch_part(session, base_url: str, obj: dict, path: str, auth, progress) -> None:
size = int(obj["size"])
have = os.path.getsize(path) if os.path.isfile(path) else 0
if have > size:
os.remove(path)
have = 0
href, headers = _resolve_oid(session, base_url, obj["oid"], size, auth)
obj_auth = None if headers.get("Authorization") else auth
# LFS parts are content-addressed (oid == sha256), so a half-written part can be resumed with a
# Range request and verified afterwards instead of being thrown away on every restart.
if have:
headers = {**headers, "Range": f"bytes={have}-"}
with session.get(href, headers=headers, stream=True, timeout=HTTP_TIMEOUT_S, auth=obj_auth) as r:
r.raise_for_status()
if have and r.status_code != 206:
have = 0
with open(path, "ab" if have else "wb") as f:
for chunk in r.iter_content(CHUNK):
f.write(chunk)
progress(len(chunk))
def download_lfs_bundle(objects: list, dst: str, sha256: str, size: int, progress_cb=None) -> str:
import requests
auth = _requests_auth()
session = requests.Session()
os.makedirs(dst + ".parts", exist_ok=True)
total = int(size) or sum(int(o["size"]) for o in objects)
done_bytes = sum(int(o["size"]) for o in objects if _part_complete(_part_path(dst, o["oid"]), o["oid"], int(o["size"])))
got = [done_bytes]
def progress(n: int) -> None:
got[0] += n
if progress_cb is not None and total:
progress_cb(min(1.0, got[0] / total))
last_error: Exception | None = None
for base_url in MODELS_BASE_URLS:
for _attempt in range(STREAM_RETRIES):
try:
for obj in objects:
path = _part_path(dst, obj["oid"])
if _part_complete(path, obj["oid"], int(obj["size"])):
continue
got[0] = done_bytes
_fetch_part(session, base_url, obj, path, auth, progress)
if not _part_complete(path, obj["oid"], int(obj["size"])):
if os.path.getsize(path) >= int(obj["size"]):
os.remove(path)
raise RuntimeError(f"part {obj['oid'][:12]} incomplete or failed verification")
done_bytes += int(obj["size"])
got[0] = done_bytes
break
except Exception as e:
last_error = e
else:
continue
break
else:
raise RuntimeError(f"model bundle download failed: {last_error}")
tmp = dst + ".part"
digest = hashlib.sha256()
with open(tmp, "wb") as out:
for obj in objects:
with open(_part_path(dst, obj["oid"]), "rb") as f:
for chunk in iter(lambda: f.read(CHUNK), b""):
out.write(chunk)
digest.update(chunk)
if total and os.path.getsize(tmp) != total:
os.remove(tmp)
raise RuntimeError(f"size mismatch: {os.path.getsize(tmp) if os.path.exists(tmp) else 0}/{total} bytes")
if sha256 and digest.hexdigest() != sha256:
os.remove(tmp)
for obj in objects:
try:
os.remove(_part_path(dst, obj["oid"]))
except OSError:
pass
raise RuntimeError("sha256 mismatch")
os.replace(tmp, dst)
for obj in objects:
try:
os.remove(_part_path(dst, obj["oid"]))
except OSError:
pass
try:
os.rmdir(dst + ".parts")
except OSError:
pass
return dst

View File

@@ -253,6 +253,15 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
"Ensure road ahead is clear"),
},
EventName.bigModelLoading: {
ET.NO_ENTRY: NoEntryAlert("Big Model Loading"),
},
EventName.bigModelFailed: {
ET.SOFT_DISABLE: soft_disable_alert("Big Model Failed"),
ET.PERMANENT: NormalPermanentAlert("Big Model Failed ", "Restart the car to retry,\nsmall model is still available", duration=20.),
},
EventName.lateralManeuver: {
ET.WARNING: longitudinal_maneuver_alert,
ET.PERMANENT: NormalPermanentAlert("Lateral Maneuver Mode"),
@@ -366,7 +375,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
"Pay Attention",
"",
AlertStatus.normal, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.none, .1),
Priority.MID, VisualAlert.none, AudibleAlert.none, .1),
},
EventName.promptDriverDistracted: {
@@ -892,7 +901,7 @@ if HARDWARE.get_device_type() == 'mici':
"Pay Attention",
"",
AlertStatus.normal, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.none, 2),
Priority.MID, VisualAlert.none, AudibleAlert.none, 2),
},
EventName.promptDriverDistracted: {
ET.PERMANENT: Alert(

View File

@@ -0,0 +1,184 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
USB bus snapshot for deviceState: every enumerated device with its negotiated
speed and its controller's link-error count. Landing this in every rlog makes
cable/hub/link regressions diagnosable from a recorded route instead of only
live.
Link errors come from `portli` on the ssusb controller (IQ.OS 4.9.1+); on older
builds the file is absent and the counts read 0.
The USB eGPU dock is identified by VID/PID only. comma's internal codename for
it is deliberately not used here: IQ.Pilot runs these models on several
backends (eGPU dock, eMac), so the naming stays about the role, not the vendor.
"""
from pathlib import Path
# comma's USB eGPU dock, both shipped USB IDs. The ROM ids are the same board
# sitting in its bootloader (ASMedia) before vendor firmware is flashed — it
# enumerates but cannot serve a GPU in that state.
EGPU_DOCK_USB_IDS = ((0xADD1, 0x0001), (0x3801, 0x0001))
EGPU_DOCK_ROM_USB_IDS = ((0x174C, 0x2464), (0x174C, 0x2463))
# must equal image_product() of the bundled firmware; test_egpu_dock_flash pins them together
EGPU_DOCK_FW_PRODUCT = "custom ed4e39b7-CLEAN"
def is_egpu_usb_device(vendor_id: int, product_id: int, include_bootloader: bool = False) -> bool:
ids = EGPU_DOCK_USB_IDS + EGPU_DOCK_ROM_USB_IDS if include_bootloader else EGPU_DOCK_USB_IDS
return (vendor_id, product_id) in ids
USB_DEVICES_PATH = Path("/sys/bus/usb/devices")
UDC_PATH = Path("/sys/class/udc")
TYPEC_CC_ORIENTATION_PATH = Path("/sys/class/power_supply/usb/typec_cc_orientation")
USB3_LANES = {1: "a", 2: "b"} # 0 = unattached
SOC_PLATFORM_PATH = Path("/sys/devices/platform/soc")
CONTROLLER_SUFFIX = ".ssusb"
LINK_ERRORS_FILE = "portli"
def read(path: Path) -> str | None:
# a controller in peripheral mode fails portli's show(); that surfaces as TypeError, not OSError
try:
return path.read_text().strip()
except Exception:
return None
def read_int(path: Path, base: int = 10) -> int:
try:
return int(path.read_text(), base)
except Exception:
return 0
def read_hex_counter(path: Path) -> int:
"""sysfs counter printed as '0x0000002a' (portli), tolerating a bare hex value."""
raw = read(path)
if raw is None:
return 0
try:
return int(raw, 0) if raw.lower().startswith("0x") else int(raw, 16)
except ValueError:
return 0
def get_usb_topology(root: Path = USB_DEVICES_PATH) -> set[str]:
"""Names of everything on the bus; a cheap way to detect hotplug without
re-reading every attribute."""
try:
return {p.name for p in root.iterdir()}
except Exception:
return set()
def usb_devices(root: Path = USB_DEVICES_PATH) -> list[Path]:
try:
return sorted((d for d in root.glob("*") if (d / "idVendor").exists()), key=lambda p: p.name)
except Exception:
return []
def controller(device: Path) -> Path | None:
"""The SuperSpeed controller a device hangs off (…/a800000.ssusb)."""
try:
return next((p for p in device.resolve().parents if p.name.endswith(CONTROLLER_SUFFIX)), None)
except Exception:
return None
def usb_controllers(soc: Path = SOC_PLATFORM_PATH) -> list[Path]:
try:
return sorted(soc.glob(f"*{CONTROLLER_SUFFIX}"))
except Exception:
return []
def link_controller(udc_root: Path = UDC_PATH) -> str:
"""Name of the Type-C port's controller, derived from the UDC rather than
hardcoded: the gadget exposes `<addr>.dwc3`, whose address prefix is the
`<addr>.ssusb` controller behind the same connector. comma pins the 3X value
directly, which would be wrong on any other board."""
try:
udc = next(iter(sorted(p.name for p in udc_root.iterdir())), "")
except Exception:
return ""
return f"{udc.split('.')[0]}{CONTROLLER_SUFFIX}" if udc else ""
def usb3_lane(orientation: int | None = None) -> str:
"""Which SuperSpeed lane the Type-C connector landed on. Unattached reads 0,
which is 'unknown' rather than a lane."""
if orientation is None:
orientation = read_int(TYPEC_CC_ORIENTATION_PATH)
return USB3_LANES.get(orientation, "unknown")
def link_errors(ctrl: Path | None) -> int:
return read_hex_counter(ctrl / LINK_ERRORS_FILE) if ctrl is not None else 0
def get_link_error_count(soc: Path = SOC_PLATFORM_PATH) -> int:
"""Cumulative SS port link errors, read off the controller rather than a
device: in peripheral mode (eMac gadget link) the peer never enumerates on
our side, so there is no device row to carry the count."""
return sum(link_errors(c) for c in usb_controllers(soc))
def egpu_dock_present(root: Path = USB_DEVICES_PATH) -> bool:
"""A dock in ROM/bootloader state is deliberately NOT counted as present: it
enumerates but cannot serve a GPU until vendor firmware is flashed."""
return any((read_int(d / "idVendor", 16), read_int(d / "idProduct", 16)) in EGPU_DOCK_USB_IDS
for d in usb_devices(root))
def egpu_dock_ready(root: Path = USB_DEVICES_PATH) -> bool:
"""Present AND running the exact firmware we ship. A dock on any other
firmware enumerates fine but has not been validated with this stack, so the
runtime refuses it; the flasher still sees it via egpu_dock_present."""
return any((read_int(d / "idVendor", 16), read_int(d / "idProduct", 16)) in EGPU_DOCK_USB_IDS
and (read(d / "product") or "").strip() == EGPU_DOCK_FW_PRODUCT
for d in usb_devices(root))
def get_usb_state(root: Path = USB_DEVICES_PATH, udc_root: Path = UDC_PATH) -> list[dict]:
devices = []
lane, link_ctrl = usb3_lane(), link_controller(udc_root)
for device in usb_devices(root):
ctrl = controller(device)
devices.append({
"usb3Lane": lane if ctrl is not None and ctrl.name == link_ctrl else "unknown",
"busnum": read_int(device / "busnum"),
"devnum": read_int(device / "devnum"),
"vendorId": read_int(device / "idVendor", 16),
"productId": read_int(device / "idProduct", 16),
"speedMbps": read_int(device / "speed"),
"manufacturer": read(device / "manufacturer") or "",
"product": read(device / "product") or "",
# 16-bit field upstream, so mask rather than let a wrapped counter overflow it
"linkErrorCount": link_errors(ctrl) & 0xFFFF,
})
return devices
def set_usb_state(device_state, devices: list[dict], link_error_count: int = 0,
lane: str | None = None) -> None:
entries = device_state.usbState.init('devices', len(devices))
dock_present = False
for entry, device in zip(entries, devices, strict=True):
entry.busnum = device["busnum"]
entry.devnum = device["devnum"]
entry.vendorId = device["vendorId"]
entry.productId = device["productId"]
entry.speedMbps = device["speedMbps"]
entry.manufacturer = device["manufacturer"]
entry.product = device["product"]
entry.linkErrorCount = device.get("linkErrorCount", 0) & 0xFFFF
entry.usb3Lane = device.get("usb3Lane", "unknown")
if (entry.vendorId, entry.productId) in EGPU_DOCK_USB_IDS:
dock_present = True
device_state.usbState.linkErrorCount = link_error_count
device_state.usbState.usb3Lane = lane if lane is not None else usb3_lane()
device_state.egpuDockPresent = dock_present

View File

@@ -8,6 +8,7 @@ from iqpilot.system.hardware import HARDWARE, PC, TICI
from iqpilot.system.hardware.hw import Paths
from iqpilot.system.manager.process import PythonProcess, NativeProcess, BundleProcess
from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected, resolve_backend, usbgpu_present
from iqpilot.selfdrive.iqmodeld.models.helpers import get_active_model_runner
from iqpilot.konn3kt.service_health import hephaestus_ready
@@ -124,6 +125,22 @@ def is_tinygrad_model(started, params, CP: car.CarParams) -> bool:
"""Check if the active model runner is tinygrad."""
return bool(get_active_model_runner(params, not started) == custom.IQModelManager.Runner.tinygrad)
def _egpu_present(params) -> bool:
if params.get_bool("IQEgpuDisabled"):
return False
return usbgpu_present()
def emac_enabled(started, params, CP: car.CarParams) -> bool:
return resolve_backend(params.get_bool("IQEmacEnabled"), egpu_selected(params), _egpu_present(params)) == "emac"
def egpu_enabled(started, params, CP: car.CarParams) -> bool:
return (resolve_backend(params.get_bool("IQEmacEnabled"), egpu_selected(params), _egpu_present(params)) == "egpu"
and _egpu_present(params))
def big_model_enabled(started, params, CP: car.CarParams) -> bool:
return params.get_bool("IQEmacEnabled") or egpu_selected(params)
def hephaestus_ready_shim(started, params, CP: car.CarParams) -> bool:
return hephaestus_ready(params)
@@ -196,6 +213,17 @@ procs += [
# Models
BundleProcess("models_manager", "iqpilot_model_selector_private", "iqpilot_private.models.manager", and_(only_offroad, not_low_power)),
NativeProcess("iqmodeld", "iqpilot/selfdrive/iqmodeld", ["./iqmodeld"], and_(only_onroad, is_tinygrad_model), restart_if_crash=True),
# big-model backends: iqmodeld self-demotes to the small channel worker when
# either backend is enabled; the selector publishes, and exactly one big
# worker (Mac or eGPU, eMac wins) feeds the BIG channel
PythonProcess("modeld_selector", "iqpilot.selfdrive.iqmodeld.modeld_selector",
and_(only_onroad, and_(is_tinygrad_model, big_model_enabled)), restart_if_crash=True),
BundleProcess("maciqmodeld", "iqpilot_emac_private", "iqpilot_private.emac.maciqmodeld",
and_(only_onroad, and_(is_tinygrad_model, emac_enabled)), restart_if_crash=True),
PythonProcess("iqegpumodeld", "iqpilot.selfdrive.iqmodeld.iqegpumodeld",
and_(only_onroad, and_(is_tinygrad_model, egpu_enabled)), restart_if_crash=True),
PythonProcess("egpu_prefetch", "iqpilot.selfdrive.iqmodeld.egpu_prefetch",
and_(only_offroad, and_(is_tinygrad_model, egpu_enabled)), restart_if_crash=True),
BundleProcess("backup_manager_k3", "iqpilot_hephaestusd_private", "iqpilot_private.konn3kt.backups.backup_orchestrator",
and_(only_offroad, hephaestus_ready_shim, not_low_power)),