mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-07 21:13:41 +08:00
134 lines
7.3 KiB
Python
134 lines
7.3 KiB
Python
"""Guard legacy QCOM CPU submissions without changing compiled GPU payloads."""
|
|
from collections import Counter
|
|
from dataclasses import replace
|
|
import ctypes
|
|
import hashlib
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING, cast
|
|
|
|
from tinygrad.device import Buffer, HCQ_RUNTIME_DEV
|
|
from tinygrad.dtype import AddrSpace, dtypes
|
|
from tinygrad.engine.realize import lower_and_compile
|
|
from tinygrad.uop.ops import AxisType, Ops, UOp
|
|
from tinygrad.runtime.support.hcq2 import HCQInfo
|
|
|
|
if TYPE_CHECKING:
|
|
from openpilot.starpilot.models.runtime_bridge.legacy_types import LegacyBufferFactory, LegacyCall, LegacyUOpFactory
|
|
|
|
|
|
def byte_inventory(root):
|
|
return Counter(hashlib.sha256(u.arg).hexdigest() for u in root.toposort()
|
|
if u.op is Ops.BINARY and isinstance(u.arg, bytes))
|
|
|
|
|
|
class Protection:
|
|
def __init__(self, library):
|
|
self.library = ctypes.CDLL(str(Path(library).resolve()))
|
|
self.function = self.library.starpilot_bounded_wait
|
|
self.function.argtypes = (ctypes.POINTER(ctypes.c_uint64), ctypes.c_uint64)
|
|
self.function.restype = ctypes.c_uint64
|
|
address = ctypes.cast(self.function, ctypes.c_void_p).value
|
|
assert address is not None
|
|
self.pointer = cast("LegacyBufferFactory", Buffer)(cast(str, HCQ_RUNTIME_DEV.value), 1, dtypes.uint64,
|
|
initial_value=address.to_bytes(8, 'little'))
|
|
self.records = []
|
|
|
|
def protect_call(self, call):
|
|
info, program = call.arg.aux, call.body
|
|
if not isinstance(info, HCQInfo) or info.device != ('QCOM',):
|
|
raise ValueError('Only the explicit QCOM compatibility path is supported')
|
|
if program.op is not Ops.PROGRAM or program.arg.target.device != 'CPU' or len(program.src) != 4:
|
|
raise ValueError('Expected a stored CPU host submission')
|
|
sink = program.src[0]
|
|
if sink.op is not Ops.SINK or sink.arg.name != 'hcq_submit':
|
|
raise ValueError('Expected the exact host submission source')
|
|
if info.nargs != len(call.src) - 1:
|
|
raise ValueError('Host argument binding changed')
|
|
before = byte_inventory(sink)
|
|
# New pointer is appended to buffers; named ALU arguments follow all buffers.
|
|
params = {u: u.replace(arg=replace(u.arg, slot=u.arg.slot + 1)) for u in sink.toposort()
|
|
if u.op is Ops.PARAM and u.arg.addrspace is AddrSpace.ALU}
|
|
sink = sink.substitute(params, enter_calls=True)
|
|
pointer = UOp.param(info.nargs, dtypes.uint64, 1, cast(str, HCQ_RUNTIME_DEV.value), name='bounded_wait_pointer')
|
|
callee = UOp.custom_function('starpilot_bounded_wait', pointer.index(0).load())
|
|
replacements, seen_loops = {}, set()
|
|
for end in sink.toposort():
|
|
if end.op is not Ops.END or len(end.src) != 3:
|
|
continue
|
|
done, loop, condition = end.src
|
|
if loop.op is not Ops.RANGE or loop.arg != (0, AxisType.WEAK) or loop.src != (UOp(Ops.NOOP),):
|
|
continue
|
|
if (done.op is not Ops.LOAD or condition.op is not Ops.CMPLT or condition.src[0] is not done or
|
|
done.dtype != dtypes.uint64 or condition.src[1].dtype != dtypes.uint64):
|
|
raise ValueError('Unknown conditional fence')
|
|
index = done.src[0]
|
|
if index.op is not Ops.INDEX or len(index.src) != 2 or index.src[1].val != 0:
|
|
raise ValueError('Unknown signal index')
|
|
signal = index.src[0].without_after
|
|
if signal.op is not Ops.PARAM or signal.dtype != dtypes.uint64 or not 0 <= signal.arg.slot < info.nargs:
|
|
raise ValueError('Unknown timeline parameter')
|
|
outer = call.src[1 + signal.arg.slot].without_after
|
|
if outer.op is not Ops.PARAM or outer.tag != 'timeline' or tuple(outer.device) != ('QCOM',):
|
|
raise ValueError('Timeline is not the native QCOM fence')
|
|
target = condition.src[1]
|
|
if loop in target.toposort():
|
|
raise ValueError('Fence target depends on its polling loop')
|
|
signal_index = index.substitute({loop: UOp(Ops.NOOP)}, enter_calls=True)
|
|
replacements[end] = cast("LegacyCall", callee).call(signal_index, target, ret_dtype=dtypes.uint64)
|
|
seen_loops.add(loop)
|
|
weak = {u for u in sink.toposort() if u.op is Ops.RANGE and u.src and u.src[0].op is Ops.NOOP}
|
|
if len(replacements) != 1 or weak != seen_loops:
|
|
raise ValueError('Host fence shape is not exactly the qualified QCOM shape')
|
|
rewritten = sink.substitute(replacements, enter_calls=True)
|
|
if byte_inventory(rewritten) != before:
|
|
raise ValueError('Opaque GPU payload changed')
|
|
if any(u.op is Ops.RANGE and u.src and u.src[0].op is Ops.NOOP for u in rewritten.toposort()):
|
|
raise ValueError('An unbounded host loop remains')
|
|
if any(u.op is Ops.CALL and u.body.op in (Ops.SINK, Ops.PROGRAM) for u in rewritten.toposort()):
|
|
raise ValueError('Nested compilation request in host source')
|
|
# The only compilation request is this CPU SINK.
|
|
new_call = call.replace(src=(rewritten, *call.src[1:], cast("LegacyUOpFactory", UOp).from_buffer(self.pointer)),
|
|
arg=replace(call.arg, aux=replace(info, nargs=info.nargs + 1)))
|
|
if new_call.device != cast(str, HCQ_RUNTIME_DEV.value) or cast(str, HCQ_RUNTIME_DEV.value).split(':')[0] != 'CPU':
|
|
raise ValueError('Refusing any non-CPU host compilation')
|
|
compiled = lower_and_compile(UOp(Ops.LINEAR, src=(new_call,))).src[0]
|
|
if compiled.body.arg.target.device != 'CPU' or byte_inventory(compiled.body.src[0]) != before:
|
|
raise ValueError('Host-only compilation contract failed')
|
|
self.records.append({'kernels': len(info.kernels), 'fences': len(replacements),
|
|
'gpu_payload_hashes_preserved': True, 'new_globals': compiled.body.arg.globals})
|
|
return compiled
|
|
|
|
def artifact(self, artifact):
|
|
if artifact.get('execution_device') != 'QCOM':
|
|
raise ValueError('Chestnut bridge is not qualified')
|
|
count = 0
|
|
for jit in artifact.values():
|
|
cap = getattr(jit, 'captured', None)
|
|
if cap is None:
|
|
continue
|
|
if 'linear' in cap.__dict__:
|
|
raise ValueError('Protection must precede captured execution')
|
|
for entry in cap._linear.src:
|
|
call = entry.without_after
|
|
if call.op is not Ops.CALL or not (call.body.op is Ops.COPY or isinstance(call.arg.aux, HCQInfo)):
|
|
raise ValueError('Unexpected top-level captured schedule entry')
|
|
old_cpu = {u.src[-1] for u in cap._linear.toposort() if u.op is Ops.PROGRAM and u.arg.target.device == 'CPU'}
|
|
old_payload = Counter(hashlib.sha256(u.arg).hexdigest() for u in cap._linear.toposort()
|
|
if u.op is Ops.BINARY and u not in old_cpu)
|
|
replacements = {u: self.protect_call(u) for u in cap._linear.toposort()
|
|
if u.op is Ops.CALL and isinstance(u.arg.aux, HCQInfo)}
|
|
if not replacements:
|
|
raise ValueError('Captured graph has no qualified native submission')
|
|
proposed = cap._linear.substitute(replacements, enter_calls=True)
|
|
new_cpu = {u.src[-1] for u in proposed.toposort() if u.op is Ops.PROGRAM and u.arg.target.device == 'CPU'}
|
|
new_payload = Counter(hashlib.sha256(u.arg).hexdigest() for u in proposed.toposort()
|
|
if u.op is Ops.BINARY and u not in new_cpu)
|
|
if old_payload != new_payload:
|
|
raise ValueError('Captured opaque runtime payload changed')
|
|
self.records[-1]['opaque_payload_count'] = sum(old_payload.values())
|
|
cap._linear = proposed
|
|
count += 1
|
|
if count != 3:
|
|
raise ValueError('Expected policy and both camera captures')
|
|
return artifact
|