mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-06 20:43:46 +08:00
233 lines
10 KiB
Python
233 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""Inspect device-build outputs without importing binaries or executing pickles."""
|
|
|
|
import argparse
|
|
from pathlib import Path
|
|
|
|
|
|
# Resolve this checkout's stdlib-only owner, even for an absolute CLI invocation
|
|
# with no PYTHONPATH or installed workspace package.
|
|
import importlib.util
|
|
_spec = importlib.util.spec_from_file_location("model_file_chunker", Path(__file__).resolve().parents[2] / "openpilot/common/file_chunker.py")
|
|
if _spec is None or _spec.loader is None:
|
|
raise RuntimeError("Unable to load this checkout's model file chunker")
|
|
_chunk_owner = importlib.util.module_from_spec(_spec)
|
|
_spec.loader.exec_module(_chunk_owner)
|
|
materialize_file_chunked = _chunk_owner.materialize_file_chunked
|
|
import pickletools
|
|
import struct
|
|
import sys
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
from openpilot.starpilot.models.catalog import DEFAULT_SMALL, DEFAULT_SMALL_SHA256, DEFAULT_SMALL_SIZE
|
|
|
|
|
|
EM_AARCH64 = 183
|
|
ELF_FILES = (
|
|
"openpilot/starpilot/flm/_primitive_walk.so",
|
|
"opendbc_repo/opendbc/can/_raw_value.so",
|
|
"openpilot/common/libparams_c.so",
|
|
"openpilot/selfdrive/pandad/pandad",
|
|
"openpilot/system/loggerd/loggerd",
|
|
"openpilot/system/loggerd/encoderd",
|
|
"openpilot/starpilot/ui/native/_raylib_extra.so",
|
|
"msgq_repo/msgq/_ipc.so",
|
|
"msgq_repo/msgq/visionipc/_visionipc.so",
|
|
"rednose_repo/rednose/helpers/ekf_sym_pyx.so",
|
|
"openpilot/tools/replay/replay",
|
|
"openpilot/tools/replay/libmedia_mux.so",
|
|
)
|
|
ARCHIVES = ("openpilot/cereal/libcereal.a", "openpilot/cereal/libsocketmaster.a")
|
|
MODEL_DIR = Path("openpilot/selfdrive/modeld/models")
|
|
# Both current fisheye camera configs in transformations/camera.py.
|
|
WARP_SIZES = ("1344x760", "1928x1208")
|
|
MODELS = ("dmonitoring_model_tinygrad.pkl",)
|
|
WARPS = tuple(f"{kind}_warp_{size}_tinygrad.pkl" for kind in ("driving", "dm") for size in WARP_SIZES)
|
|
|
|
|
|
def require_runtime_search_paths(data: bytes, name: str) -> None:
|
|
phoff = struct.unpack_from("<Q", data, 32)[0]
|
|
phentsize, phnum = struct.unpack_from("<HH", data, 54)
|
|
headers = [struct.unpack_from("<IIQQQQQQ", data, phoff + i * phentsize) for i in range(phnum)]
|
|
for header in headers:
|
|
if header[0] != 2: # PT_DYNAMIC
|
|
continue
|
|
offset, size = header[2], header[5]
|
|
if offset + size > len(data) or size % 16:
|
|
raise ValueError(f"{name}: malformed dynamic table")
|
|
tags = []
|
|
for pos in range(offset, offset + size, 16):
|
|
tag, value = struct.unpack_from("<qQ", data, pos)
|
|
if tag == 0:
|
|
break
|
|
tags.append((tag, value))
|
|
paths = [value for tag, value in tags if tag in (15, 29)] # DT_RPATH, DT_RUNPATH
|
|
if not paths:
|
|
continue
|
|
table = dict(tags)
|
|
address, length = table.get(5), table.get(10) # DT_STRTAB, DT_STRSZ
|
|
if address is None or length is None:
|
|
raise ValueError(f"{name}: missing dynamic string table")
|
|
loads = [h for h in headers if h[0] == 1 and h[3] <= address and address + length <= h[3] + h[5]]
|
|
if len(loads) != 1:
|
|
raise ValueError(f"{name}: invalid dynamic string table")
|
|
start = loads[0][2] + address - loads[0][3]
|
|
if start + length > len(data):
|
|
raise ValueError(f"{name}: truncated dynamic string table")
|
|
strings = data[start:start + length]
|
|
for value in paths:
|
|
end = strings.find(b"\0", value)
|
|
if value >= length or end < 0:
|
|
raise ValueError(f"{name}: invalid runtime search path")
|
|
for path in strings[value:end].decode().split(":"):
|
|
if path.startswith(("/work/", "/Users/", "/opt/tici-sysroot/")) or ".venv-linux-arm64" in path:
|
|
raise ValueError(f"{name}: build-host runtime search path: {path}")
|
|
|
|
|
|
def require_aarch64_elf(data: bytes, name: str, *, relocatable: bool = False) -> None:
|
|
if len(data) < 64 or data[:4] != b"\x7fELF" or data[4] != 2 or data[5] != 1:
|
|
raise ValueError(f"{name}: expected little-endian ELF64")
|
|
if struct.unpack_from("<H", data, 18)[0] != EM_AARCH64:
|
|
raise ValueError(f"{name}: expected AArch64 ELF machine")
|
|
e_type = struct.unpack_from("<H", data, 16)[0]
|
|
e_version = struct.unpack_from("<I", data, 20)[0]
|
|
phoff, shoff = struct.unpack_from("<QQ", data, 32)
|
|
ehsize, phentsize, phnum, shentsize, shnum = struct.unpack_from("<HHHHH", data, 52)
|
|
if e_version != 1 or ehsize != 64 or e_type not in ((1,) if relocatable else (2, 3)):
|
|
raise ValueError(f"{name}: invalid ELF type/version/header")
|
|
if relocatable:
|
|
if not shoff or not shnum or shentsize < 64 or shoff + shentsize * shnum > len(data):
|
|
raise ValueError(f"{name}: invalid ELF section table")
|
|
sections = [struct.unpack_from("<IIQQQQ", data, shoff + i * shentsize) for i in range(shnum)]
|
|
if any(section[1] not in (0, 8) and section[4] + section[5] > len(data) for section in sections):
|
|
raise ValueError(f"{name}: object section extends beyond file")
|
|
if not any(section[1] == 1 and section[5] > 0 for section in sections):
|
|
raise ValueError(f"{name}: missing object code/data section")
|
|
else:
|
|
if not phoff or not phnum or phentsize < 56 or phoff + phentsize * phnum > len(data):
|
|
raise ValueError(f"{name}: invalid ELF program table")
|
|
if not any(struct.unpack_from("<I", data, phoff + i * phentsize)[0] == 1 and
|
|
struct.unpack_from("<Q", data, phoff + i * phentsize + 32)[0] > 0 and
|
|
struct.unpack_from("<Q", data, phoff + i * phentsize + 8)[0] +
|
|
struct.unpack_from("<Q", data, phoff + i * phentsize + 32)[0] <= len(data)
|
|
for i in range(phnum)):
|
|
raise ValueError(f"{name}: missing bounded ELF load segment")
|
|
require_runtime_search_paths(data, name)
|
|
|
|
|
|
def require_aarch64_archive(data: bytes, name: str) -> None:
|
|
if not data.startswith(b"!<arch>\n"):
|
|
raise ValueError(f"{name}: expected a regular ar archive")
|
|
offset, objects = 8, 0
|
|
while offset < len(data):
|
|
header = data[offset:offset + 60]
|
|
if len(header) != 60 or header[58:] != b"`\n":
|
|
raise ValueError(f"{name}: malformed archive member")
|
|
try:
|
|
size = int(header[48:58].strip())
|
|
except ValueError as exc:
|
|
raise ValueError(f"{name}: invalid archive member size") from exc
|
|
start, end = offset + 60, offset + 60 + size
|
|
if end > len(data):
|
|
raise ValueError(f"{name}: truncated archive member")
|
|
member = data[start:end]
|
|
if member.startswith(b"\x7fELF"):
|
|
require_aarch64_elf(member, name, relocatable=True)
|
|
objects += 1
|
|
elif not header[:16].strip().startswith((b"/", b"__.SYMDEF")):
|
|
raise ValueError(f"{name}: non-ELF object member")
|
|
offset = end + (size % 2)
|
|
if not objects or offset != len(data):
|
|
raise ValueError(f"{name}: no complete AArch64 object members")
|
|
|
|
|
|
def pickle_opcodes(path: Path) -> bytes:
|
|
with path.open("rb") as file:
|
|
prefix = file.read(8)
|
|
if prefix.startswith(b"\x80"):
|
|
file.seek(0)
|
|
return file.read(100_000_001)
|
|
if len(prefix) != 8:
|
|
raise ValueError(f"{path}: truncated model")
|
|
size = struct.unpack("<q", prefix)[0]
|
|
if not 0 < size <= 100_000_000:
|
|
raise ValueError(f"{path}: invalid pickle opcode length")
|
|
data = file.read(size)
|
|
if len(data) != size:
|
|
raise ValueError(f"{path}: truncated pickle opcodes")
|
|
return data
|
|
|
|
|
|
def require_qcom_pickle(path: Path) -> None:
|
|
name = path.name
|
|
path = materialize_file_chunked(path)
|
|
data = pickle_opcodes(path)
|
|
if len(data) > 100_000_000:
|
|
raise ValueError(f"{path}: pickle opcodes exceed inspection bound")
|
|
try:
|
|
ops = list(pickletools.genops(data))
|
|
tokens = {arg for _, arg, _ in ops if isinstance(arg, str)}
|
|
except (ValueError, EOFError) as exc:
|
|
raise ValueError(f"{path}: malformed pickle opcode stream") from exc
|
|
if not ops or ops[-1][0].name != "STOP" or not {"metadata", "run", "input_specs", "QCOM"}.issubset(tokens):
|
|
raise ValueError(f"{path}: missing captured model/warp graph")
|
|
if name in MODELS:
|
|
if not {"output_specs", "output_slices", "input_shapes"}.issubset(tokens):
|
|
raise ValueError(f"{path}: missing model input/output specification")
|
|
if path.stat().st_size <= 8 + len(data):
|
|
raise ValueError(f"{path}: missing out-of-band model buffers")
|
|
# QCOM captures also serialize CPU host-dispatch/renderer nodes. A CPU token
|
|
# therefore does not identify the device backend; require QCOM above and
|
|
# reject explicitly captured foreign GPU backends here.
|
|
if any(token in ("METAL", "AMD") or token.startswith(("METAL:", "AMD:", "USB+AMD")) for token in tokens):
|
|
raise ValueError(f"{path}: foreign GPU captured backend")
|
|
tinyjit_global = False
|
|
captured_global = False
|
|
for index, (op, _arg, _) in enumerate(ops):
|
|
if op.name != "STACK_GLOBAL":
|
|
continue
|
|
names = [value for _, value, _ in ops[max(0, index - 4):index] if isinstance(value, str)]
|
|
if names[-2:] == ["tinygrad.engine.jit", "_TinyJit"]:
|
|
tinyjit_global = True
|
|
if names and names[-1] == "CapturedJit":
|
|
captured_global = True
|
|
if not tinyjit_global or not captured_global:
|
|
raise ValueError(f"{path}: missing TinyJit capture structure")
|
|
|
|
|
|
def validate(root: Path) -> None:
|
|
from tools.laptop_device_build.validate_flm_native import validate as validate_flm_native
|
|
validate_flm_native(root)
|
|
for name in ELF_FILES:
|
|
path = root / name
|
|
require_aarch64_elf(path.read_bytes(), name)
|
|
for name in ARCHIVES:
|
|
path = root / name
|
|
require_aarch64_archive(path.read_bytes(), name)
|
|
|
|
model_dir = root / MODEL_DIR
|
|
actual_warps = {p.name for p in model_dir.glob("driving_warp_*_tinygrad.pkl")}
|
|
actual_warps.update(p.name for p in model_dir.glob("dm_warp_*_tinygrad.pkl"))
|
|
if actual_warps != set(WARPS):
|
|
raise ValueError(f"camera warp set differs from current camera configs: {sorted(actual_warps)}")
|
|
for name in (*MODELS, *WARPS):
|
|
require_qcom_pickle(model_dir / name)
|
|
shipped = materialize_file_chunked(model_dir / f"{DEFAULT_SMALL}_driving_tinygrad.pkl", DEFAULT_SMALL_SHA256)
|
|
if shipped.stat().st_size != DEFAULT_SMALL_SIZE:
|
|
raise ValueError("Shipped default model size differs from the published artifact")
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("root", type=Path)
|
|
args = parser.parse_args()
|
|
try:
|
|
validate(args.root)
|
|
except (OSError, ValueError) as exc:
|
|
parser.exit(1, f"Device artifact validation failed: {exc}\n")
|
|
print("Device artifacts: static AArch64 and QCOM capture structure checks passed; runtime inference remains unverified.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|