mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 00:53:45 +08:00
169 lines
5.3 KiB
Python
169 lines
5.3 KiB
Python
"""One optional fitter process, nonblocking bounded IPC; no Python workerthread."""
|
|
|
|
import json
|
|
import os
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
|
|
MAX_INPUT = 8 * 1024 * 1024
|
|
MAX_OUTPUT = 65536
|
|
CHUNK = 65536
|
|
WALL_NS = 30_000_000_000
|
|
INTERVAL_NS = 60_000_000_000
|
|
|
|
|
|
class FitProcess:
|
|
worker_module = 'openpilot.starpilot.flm.fit_worker'
|
|
wall_ns = WALL_NS
|
|
interval_ns = INTERVAL_NS
|
|
|
|
def __init__(self):
|
|
self.child = None
|
|
self.pending = b''
|
|
self.position = 0
|
|
self.output = bytearray()
|
|
self.token = None
|
|
self.started_ns = self.last_start_ns = 0
|
|
self.reaping = False
|
|
self.reason = 'idle'
|
|
|
|
def submit(self, snapshot, *, token, now_ns):
|
|
if self.child is not None or self.last_start_ns and now_ns - self.last_start_ns < self.interval_ns:
|
|
return False
|
|
try:
|
|
payload = json.dumps(snapshot, separators=(',', ':'), allow_nan=False).encode()
|
|
except (TypeError, ValueError, MemoryError):
|
|
self.reason = 'fit_snapshot_invalid'
|
|
self.last_start_ns = now_ns
|
|
return False
|
|
return self.submit_bytes(payload, token=token, now_ns=now_ns)
|
|
|
|
def submit_bytes(self, payload, *, token, now_ns):
|
|
"""Bounded prepared IPC; the owned worker still validates its full input."""
|
|
if self.child is not None or self.last_start_ns and now_ns - self.last_start_ns < self.interval_ns:
|
|
return False
|
|
if type(payload) is not bytes or not 0 < len(payload) <= MAX_INPUT:
|
|
self.reason = 'fit_snapshot_bound'
|
|
self.last_start_ns = now_ns
|
|
return False
|
|
environment = dict(os.environ)
|
|
# Bound numerical backend thread creation only in this child.
|
|
for key in ('OPENBLAS_NUM_THREADS', 'OMP_NUM_THREADS', 'MKL_NUM_THREADS', 'NUMEXPR_NUM_THREADS'):
|
|
environment[key] = '1'
|
|
environment['STARPILOT_FLM_PARENT_PID'] = str(os.getpid())
|
|
try:
|
|
self.child = subprocess.Popen(
|
|
[sys.executable, '-B', '-m', self.worker_module],
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.DEVNULL,
|
|
start_new_session=True,
|
|
env=environment,
|
|
)
|
|
except OSError:
|
|
self.reason = 'fit_child_unavailable'
|
|
self.last_start_ns = now_ns
|
|
return False
|
|
try:
|
|
os.set_blocking(self.child.stdin.fileno(), False)
|
|
os.set_blocking(self.child.stdout.fileno(), False)
|
|
except OSError:
|
|
self.close('fit_pipe_setup_failed')
|
|
self.last_start_ns = now_ns
|
|
return False
|
|
self.pending, self.position, self.output = payload, 0, bytearray()
|
|
self.token, self.started_ns, self.last_start_ns = token, now_ns, now_ns
|
|
self.reaping = False
|
|
self.reason = 'fitting'
|
|
return True
|
|
|
|
def cancel(self, reason):
|
|
if self.child is not None and not self.reaping:
|
|
try:
|
|
os.killpg(self.child.pid, signal.SIGKILL)
|
|
except ProcessLookupError:
|
|
pass
|
|
self.reaping = True
|
|
self.pending = b''
|
|
self.output.clear()
|
|
self.reason = reason
|
|
|
|
def close(self, reason='fitter_closed'):
|
|
# Only parent shutdown/setup failure; the live20Hz loop uses cancel/poll.
|
|
self.cancel(reason)
|
|
if self.child is not None:
|
|
try:
|
|
self.child.wait(timeout=1.0)
|
|
except subprocess.TimeoutExpired:
|
|
return False
|
|
self._close()
|
|
return True
|
|
|
|
def poll(self, *, token, now_ns):
|
|
child = self.child
|
|
if child is None:
|
|
return None
|
|
if token != self.token or now_ns - self.started_ns > self.wall_ns:
|
|
self.cancel('fit_context_changed' if token != self.token else 'fit_wall_limit')
|
|
if self.reaping:
|
|
if child.poll() is not None:
|
|
self._close()
|
|
return None
|
|
if self.position < len(self.pending):
|
|
try:
|
|
self.position += os.write(child.stdin.fileno(), self.pending[self.position : self.position + CHUNK])
|
|
except BlockingIOError:
|
|
pass
|
|
except BrokenPipeError:
|
|
self.cancel('fit_input_closed')
|
|
return None
|
|
if self.position == len(self.pending) and child.stdin is not None:
|
|
child.stdin.close()
|
|
child.stdin = None
|
|
self.pending = b''
|
|
try:
|
|
part = os.read(child.stdout.fileno(), CHUNK)
|
|
except BlockingIOError:
|
|
part = None
|
|
if part:
|
|
self.output.extend(part)
|
|
if len(self.output) > MAX_OUTPUT:
|
|
self.cancel('fit_output_bound')
|
|
return None
|
|
code = child.poll()
|
|
if code is None:
|
|
return None
|
|
# Drain one final bounded chunk after exit; producer cannot consume a
|
|
# partially read report merely because the child exited successfully.
|
|
try:
|
|
tail = os.read(child.stdout.fileno(), CHUNK)
|
|
except BlockingIOError:
|
|
tail = b''
|
|
self.output.extend(tail)
|
|
result = None
|
|
if code == 0 and len(self.output) <= MAX_OUTPUT:
|
|
try:
|
|
result = json.loads(self.output)
|
|
except (ValueError, UnicodeError):
|
|
self.reason = 'fit_report_invalid'
|
|
else:
|
|
self.reason = 'fit_failed'
|
|
self._close()
|
|
return result
|
|
|
|
def _close(self):
|
|
child = self.child
|
|
if child is not None:
|
|
for stream in (child.stdin, child.stdout):
|
|
if stream is not None:
|
|
stream.close()
|
|
# poll() already reaped the direct child. This actor contract forbids any
|
|
# spawned descendants; actual lifecycle qualification is root-owned.
|
|
self.child = None
|
|
self.pending = b''
|
|
self.position = 0
|
|
self.output = bytearray()
|
|
self.token = None
|
|
self.reaping = False
|