feat(perception): connect binary ingress to the supervised full graph
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
"""Binary input → existing full-graph bundles; source paths never enter this adapter."""
|
||||
|
||||
import hashlib
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from collections import Counter
|
||||
|
||||
import numpy as np
|
||||
|
||||
from k1link.perception.streaming_ingress import StreamingIngress
|
||||
from k1link.perception.streaming_sensors import (
|
||||
CausalSensorWindow,
|
||||
increment_identity,
|
||||
milliseconds,
|
||||
normalized_sensor,
|
||||
)
|
||||
|
||||
|
||||
class BinaryGraphBridge:
|
||||
def __init__(self, runtime, decoder, source, report):
|
||||
self.runtime, self.decoder, self.source, self.report = runtime, decoder, source, report
|
||||
self.source_zero, self.wall_zero = source.source_zero, source.wall_zero
|
||||
self.window = CausalSensorWindow(runtime.mailbox)
|
||||
self.accepted = [] # Bounded pilot diagnostics, not a runtime history queue.
|
||||
self.binding_reasons = Counter()
|
||||
self.preroll = 0
|
||||
self.bgr_hashes = []
|
||||
self.left = right = None
|
||||
try:
|
||||
self.left, right = socket.socketpair()
|
||||
self.receiver = StreamingIngress(
|
||||
right, runtime, "recorded-acquisition", 1, self.consume, self.notice
|
||||
)
|
||||
self.producer = threading.Thread(
|
||||
target=source.run, args=(self.left,), daemon=True, name="binary-recorded-source"
|
||||
)
|
||||
runtime.track_thread(self.producer)
|
||||
except BaseException:
|
||||
# No threads have started. A failed registration/lease must not
|
||||
# strand the preallocated cache or either end of the socket.
|
||||
for connection in (self.left, right):
|
||||
if connection is not None:
|
||||
connection.close()
|
||||
self.window.close()
|
||||
raise
|
||||
|
||||
def start(self):
|
||||
self.receiver.start()
|
||||
self.producer.start()
|
||||
|
||||
def notice(self, value):
|
||||
# A gap is not a new codec epoch. This profile requires a new run/lease
|
||||
# and fresh decoder state; never continue predictive decoding across it.
|
||||
raise ValueError("binary full profile requires restart after an explicit source gap")
|
||||
|
||||
def consume(self, event):
|
||||
self.runtime.check_current(self.runtime.start)
|
||||
stamp = event.received_monotonic_ns
|
||||
self.window.advance(stamp)
|
||||
if event.modality == "camera-init":
|
||||
if event.source_id != "sensor.camera.right":
|
||||
raise ValueError("camera source mapping changed")
|
||||
self.decoder.configure(event.payload)
|
||||
return
|
||||
if event.modality in ("lidar", "pose"):
|
||||
expected = {
|
||||
"lidar": "normalized-map-point-increments",
|
||||
"pose": "normalized-map-from-lidar-pose",
|
||||
}
|
||||
if event.source_id != expected[event.modality]:
|
||||
raise ValueError("normalized sensor mapping changed")
|
||||
self.window.append(
|
||||
normalized_sensor(event.modality, event.source_sequence, stamp, event.payload)
|
||||
)
|
||||
return
|
||||
if event.modality != "camera-frame" or event.source_id != "sensor.camera.right":
|
||||
raise ValueError("unsupported full-profile input")
|
||||
# Freeze the causal cut BEFORE the decoder RPC. The synchronous ingress
|
||||
# cannot consume future sensor messages while this callback is running.
|
||||
binding = self.window.bind(stamp)
|
||||
pose = self.window.pose
|
||||
rolling = tuple(self.window.rolling)
|
||||
fresh = binding.increments
|
||||
count = sum(len(e.value[0]) for e in fresh)
|
||||
rolling_count = sum(len(e.value[0]) for e in rolling)
|
||||
size = 1440000 + count * 24 + rolling_count * 32 + 4096
|
||||
reservation = self.runtime.mailbox.reserve_ingress(size)
|
||||
raw = image = points = rolling_points = rolling_times = bundle = None
|
||||
transferred = False
|
||||
decode_started = time.monotonic_ns()
|
||||
try:
|
||||
raw = bytearray(1440000)
|
||||
decoded = self.decoder.decode(event.payload, raw)
|
||||
image = np.frombuffer(raw, "u1").reshape(600, 800, 3)
|
||||
image.setflags(write=False)
|
||||
if decoded["frame_index"] != event.source_sequence:
|
||||
raise ValueError("camera and decoder frame identity diverged")
|
||||
points = np.concatenate([e.value[0] for e in fresh]) if fresh else np.empty((0, 3))
|
||||
rolling_points = (
|
||||
np.concatenate([e.value[0] for e in rolling]) if rolling else np.empty((0, 3))
|
||||
)
|
||||
rolling_times = np.empty(rolling_count, np.int64)
|
||||
offset = 0
|
||||
for e in rolling:
|
||||
length = len(e.value[0])
|
||||
rolling_times[offset : offset + length] = e.time_ns
|
||||
offset += length
|
||||
due = self.wall_zero + stamp - self.source_zero
|
||||
bundle = {
|
||||
"sequence": event.source_sequence,
|
||||
"time_ns": stamp,
|
||||
"source_ns": stamp - self.source_zero,
|
||||
"due_ns": due,
|
||||
"utc_ns": event.captured_at_epoch_ns,
|
||||
"image": image,
|
||||
"points": points,
|
||||
"rolling_points": rolling_points,
|
||||
"rolling_times": rolling_times,
|
||||
"pose": pose.value if pose else None,
|
||||
"available": binding.available,
|
||||
"binding_age_ms": milliseconds(binding.binding_age_ns),
|
||||
"sensor_binding": binding.document(),
|
||||
"lineage": {
|
||||
"camera_index_sequence": event.source_sequence + 1,
|
||||
"camera_host_monotonic_ns": stamp,
|
||||
"pose_sequence": pose.sequence if pose else None,
|
||||
"pose_host_monotonic_ns": pose.time_ns if pose else None,
|
||||
"point_increments": [increment_identity(e) for e in fresh],
|
||||
"pose_age_ms": milliseconds(binding.pose_age_ns),
|
||||
"newest_point_age_ms": milliseconds(binding.newest_point_age_ns),
|
||||
"oldest_point_age_ms": milliseconds(binding.oldest_point_age_ns),
|
||||
},
|
||||
"source_release_lag_ms": None, # Recorded independently by the source, not guessed.
|
||||
"decode_ms": decoded["decode_ms"],
|
||||
"decode_rpc_ms": (time.monotonic_ns() - decode_started) / 1e6,
|
||||
"enqueued_ns": time.monotonic_ns(),
|
||||
"payload_bytes": size,
|
||||
}
|
||||
self.bgr_hashes.append(hashlib.sha256(image).hexdigest())
|
||||
bundle["enqueued_ns"] = time.monotonic_ns()
|
||||
transferred = self.runtime.admit_reserved(self.runtime.start, bundle, reservation)
|
||||
self.accepted.append(event.source_sequence)
|
||||
self.binding_reasons.update(binding.reasons)
|
||||
self.preroll += sum(len(e.value[0]) for e in binding.history_only)
|
||||
self.report["last_camera_due_ns"] = due
|
||||
self.window.finish_camera(stamp)
|
||||
finally:
|
||||
raw = image = points = rolling_points = rolling_times = bundle = None
|
||||
if not transferred:
|
||||
reservation.release()
|
||||
|
||||
def close(self):
|
||||
if self.producer.ident is not None:
|
||||
self.producer.join(timeout=2)
|
||||
else:
|
||||
self.left.close()
|
||||
if self.receiver.thread.ident is None:
|
||||
self.receiver.connection.close()
|
||||
joined = self.receiver.join(timeout=2) if self.receiver.thread.ident is not None else True
|
||||
self.report.update(
|
||||
binary_ingress=self.receiver.snapshot(),
|
||||
decoder_bgr_sha256=self.bgr_hashes,
|
||||
failed_camera_sequences=sorted(set(self.source.released) - set(self.accepted)),
|
||||
sensor_binding_reasons=dict(self.binding_reasons),
|
||||
preroll_history_only_points=self.preroll,
|
||||
)
|
||||
if joined and not self.producer.is_alive():
|
||||
self.window.close()
|
||||
self.decoder.close()
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,222 @@
|
||||
"""CPU-only decoded-bundle oracle before a full GPU graph run; bounded prefix."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
from pilot_binary_bridge import BinaryGraphBridge
|
||||
from pilot_binary_source import RecordingSource
|
||||
|
||||
from k1link.perception.realtime_contract import StreamStart
|
||||
from k1link.perception.streaming_decoder_client import StreamingDecoderClient
|
||||
from k1link.perception.streaming_lifecycle import StreamingLifecycle
|
||||
from k1link.perception.streaming_queue import StreamMailbox
|
||||
from k1link.perception.worker_lease import WorkerLeaseError
|
||||
|
||||
|
||||
def digest(bundle):
|
||||
result = {
|
||||
key: bundle[key]
|
||||
for key in (
|
||||
"sequence",
|
||||
"time_ns",
|
||||
"utc_ns",
|
||||
"lineage",
|
||||
"sensor_binding",
|
||||
"available",
|
||||
"binding_age_ms",
|
||||
)
|
||||
}
|
||||
result["arrays"] = {
|
||||
key: hashlib.sha256(bundle[key]).hexdigest()
|
||||
for key in ("points", "rolling_points", "rolling_times")
|
||||
}
|
||||
result["pose"] = [a.tolist() for a in bundle["pose"]] if bundle["pose"] else None
|
||||
return result
|
||||
|
||||
|
||||
def run(args):
|
||||
root = Path(args.output)
|
||||
root.mkdir()
|
||||
report = {
|
||||
"created_utc": datetime.now(UTC).isoformat(),
|
||||
"started_monotonic_ns": time.monotonic_ns(),
|
||||
"scope": "CPU-only bundle parity",
|
||||
"synthetic_decoder_timeout": args.fault_decoder,
|
||||
}
|
||||
source_report = {}
|
||||
identity = StreamStart(
|
||||
run_id=root.name,
|
||||
source_id="RAVNOVES00-prefix",
|
||||
worker_id="worker-006",
|
||||
epoch_id="epoch-1",
|
||||
lease_generation=1,
|
||||
profile_sha256=hashlib.sha256(Path("/out/candidate-profile.json").read_bytes()).hexdigest(),
|
||||
image_sha256="664824aa25de1db178f177d67a81b01541a938b479812b9383ddbf03f6b59dbe",
|
||||
effective_config_sha256=hashlib.sha256(b"cpu-bundle-parity-v1").hexdigest(),
|
||||
calibration_sha256=hashlib.sha256(
|
||||
b"no-calibration-or-model-scene-in-this-probe"
|
||||
).hexdigest(),
|
||||
clock_domain_id="original-host-arrival-clock",
|
||||
input_mode="recorded-source-paced",
|
||||
)
|
||||
runtime = StreamingLifecycle(
|
||||
identity, Path("/tmp/cpu-bridge-lease"), StreamMailbox(), threading.Event()
|
||||
)
|
||||
heartbeat_stop = threading.Event()
|
||||
|
||||
def heartbeat():
|
||||
while not heartbeat_stop.wait(0.25):
|
||||
try:
|
||||
runtime.renew(identity)
|
||||
except WorkerLeaseError:
|
||||
# Stop/lease failure is already terminal in the common runtime.
|
||||
# Do not turn an expected asynchronous shutdown into thread noise.
|
||||
return
|
||||
|
||||
renewer = threading.Thread(target=heartbeat, daemon=True)
|
||||
renewer.start()
|
||||
client = bridge = None
|
||||
decoded = []
|
||||
with (root / "decoder.log").open("wb") as log:
|
||||
try:
|
||||
command = ["python3", "-B", "/probe/pilot_fragment_decoder.py"]
|
||||
if args.fault_decoder:
|
||||
command = [
|
||||
"python3",
|
||||
"-B",
|
||||
"-c",
|
||||
"import sys,time; from pilot_ipc import receive,send; "
|
||||
"send(sys.stdout.buffer,{'ready':'fragment-h264','pyav':'18.0.0',"
|
||||
"'as_limit_mib':1024}); "
|
||||
"receive(sys.stdin.buffer); send(sys.stdout.buffer,{'initialized':True}); "
|
||||
"receive(sys.stdin.buffer); time.sleep(10)",
|
||||
]
|
||||
child = runtime.spawn(
|
||||
lambda: subprocess.Popen(
|
||||
command,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=log,
|
||||
start_new_session=True,
|
||||
env={**os.environ, "CUDA_VISIBLE_DEVICES": ""},
|
||||
)
|
||||
)
|
||||
client = StreamingDecoderClient(runtime, child.stdout.fileno(), child.stdin.fileno())
|
||||
runtime.ready()
|
||||
source = RecordingSource(args, runtime, source_report)
|
||||
bridge = BinaryGraphBridge(runtime, client, source, source_report)
|
||||
bridge.start()
|
||||
while (bundle := runtime.mailbox.take()) is not None:
|
||||
decoded.append(digest(bundle))
|
||||
runtime.mailbox.release(bundle)
|
||||
if runtime.mailbox.error and not args.fault_decoder:
|
||||
raise ValueError(runtime.mailbox.error)
|
||||
finally:
|
||||
heartbeat_stop.set()
|
||||
renewer.join(timeout=1)
|
||||
runtime.request_stop("completed" if len(decoded) == args.frames else "failed")
|
||||
runtime.stop_children()
|
||||
report["bridge_stopped"] = bridge.close() if bridge else True
|
||||
if client:
|
||||
client.close()
|
||||
report["released"] = runtime.close()
|
||||
report["runtime"] = runtime.snapshot()
|
||||
report["source"] = source_report
|
||||
report["peak_input_bytes"] = runtime.mailbox.peak_bytes
|
||||
report["residual_input_bytes"] = runtime.mailbox.bytes
|
||||
(root / "report.json").write_text(json.dumps(report, indent=2) + "\n")
|
||||
if args.fault_decoder:
|
||||
assert not decoded and report["released"] and not runtime.mailbox.bytes
|
||||
assert "decoder RPC deadline" in source_report["binary_ingress"]["error"]
|
||||
assert source_report["failed_camera_sequences"]
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"synthetic_decoder_timeout": True,
|
||||
"released": report["released"],
|
||||
"source_failed": source_report["failed_camera_sequences"],
|
||||
"input_bytes": runtime.mailbox.bytes,
|
||||
}
|
||||
)
|
||||
)
|
||||
return
|
||||
# Independent oracle uses the old producer's exact assembly code AFTER the
|
||||
# binary run. No prepared arrays feed the new decoder or graph. Zero camera
|
||||
# bytes are used only here: image parity has its separate actual decoder test.
|
||||
import run_joint_pilot as legacy
|
||||
|
||||
reference = []
|
||||
|
||||
class Sink:
|
||||
error = None
|
||||
|
||||
def put(self, bundle):
|
||||
reference.append(digest(bundle))
|
||||
return True
|
||||
|
||||
def finish(self, error=None):
|
||||
self.error = error
|
||||
|
||||
sink = Sink()
|
||||
old_send, old_receive = legacy.send, legacy.receive
|
||||
image = bytes(1440000)
|
||||
legacy.send = lambda *args: None
|
||||
legacy.receive = lambda *args: ({"decode_ms": 0}, image)
|
||||
try:
|
||||
legacy.produce(
|
||||
args,
|
||||
SimpleNamespace(stdin=None, stdout=None),
|
||||
sink,
|
||||
SimpleNamespace(is_set=lambda: False, wait=lambda _: False),
|
||||
{},
|
||||
)
|
||||
finally:
|
||||
legacy.send, legacy.receive = old_send, old_receive
|
||||
assert sink.error is None, sink.error
|
||||
report.update(
|
||||
bundle_count=len(decoded),
|
||||
oracle_count=len(reference),
|
||||
exact_sensor_bundles=decoded == reference,
|
||||
model_runs=0,
|
||||
network_qualified=False,
|
||||
)
|
||||
(root / "decoded-bundles.json").write_text(json.dumps(decoded, indent=2) + "\n")
|
||||
(root / "reference-bundles.json").write_text(json.dumps(reference, indent=2) + "\n")
|
||||
(root / "report.json").write_text(json.dumps(report, indent=2) + "\n")
|
||||
assert decoded == reference and len(decoded) == args.frames and report["released"]
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
k: report[k]
|
||||
for k in (
|
||||
"bundle_count",
|
||||
"exact_sensor_bundles",
|
||||
"released",
|
||||
"peak_input_bytes",
|
||||
"residual_input_bytes",
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--output", required=True)
|
||||
parser.add_argument("--frames", type=int, default=32)
|
||||
parser.add_argument("--fault-decoder", action="store_true")
|
||||
parser.add_argument("--camera-root", default="/camera")
|
||||
parser.add_argument("--camera-index", default="/camera/index.jsonl")
|
||||
parser.add_argument("--sensor-archive", default="/sensor-source.npz")
|
||||
args = parser.parse_args()
|
||||
if not 1 <= args.frames <= 128:
|
||||
parser.error("CPU oracle is bounded to 128 cameras")
|
||||
run(args)
|
||||
@@ -0,0 +1,145 @@
|
||||
"""Recording adapter only: emits bounded camera/sensors at original 1x times.
|
||||
|
||||
No model or decoder is called, and processing speed never sets source pacing.
|
||||
Source inventory/count belong here, never to the binary consumer or full graph.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import struct
|
||||
import time
|
||||
import traceback
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
from pilot_source import SensorArchive, camera_events, merged_events
|
||||
|
||||
from k1link.compute.live_perception import LiveIngressEvent
|
||||
from k1link.perception.streaming_sender import StreamingSender
|
||||
|
||||
|
||||
def read_member(path, root, maximum):
|
||||
resolved = path.resolve(strict=True)
|
||||
if not resolved.is_relative_to(root.resolve(strict=True)):
|
||||
raise ValueError("source member escaped camera root")
|
||||
with resolved.open("rb") as stream:
|
||||
raw = stream.read(maximum + 1)
|
||||
if not 0 < len(raw) <= maximum:
|
||||
raise ValueError("source member exceeds bound")
|
||||
return raw
|
||||
|
||||
|
||||
class RecordingSource:
|
||||
def __init__(self, args, runtime, report):
|
||||
self.args, self.runtime, self.report = args, runtime, report
|
||||
first = next(camera_events(args.camera_index, 1)).time_ns
|
||||
self.source_zero = first - 500_000_000
|
||||
self.wall_zero = time.monotonic_ns() + 50_000_000
|
||||
self.released = [] # Diagnostic bounded prefix only, never runtime admission state.
|
||||
report.update(
|
||||
source_zero_ns=self.source_zero,
|
||||
wall_zero_ns=self.wall_zero,
|
||||
first_camera_source_ns=first,
|
||||
source_clock_speed=1.0,
|
||||
source_eof_required=False,
|
||||
full_source_prepass=False,
|
||||
)
|
||||
|
||||
def run(self, connection):
|
||||
archive = None
|
||||
sender = None
|
||||
arrivals, skipped = Counter(), Counter()
|
||||
lags = []
|
||||
clock = self.runtime.stop_event
|
||||
try:
|
||||
archive = SensorArchive(Path(self.args.sensor_archive))
|
||||
root = Path(self.args.camera_root)
|
||||
sender = StreamingSender(
|
||||
connection,
|
||||
self.runtime.start,
|
||||
"recorded-acquisition",
|
||||
1,
|
||||
lambda: self.runtime.check_current(self.runtime.start),
|
||||
)
|
||||
seq = 1
|
||||
sender.send(
|
||||
LiveIngressEvent(
|
||||
seq,
|
||||
"recorded-acquisition",
|
||||
1,
|
||||
"camera-init",
|
||||
"sensor.camera.right",
|
||||
0,
|
||||
0,
|
||||
self.source_zero,
|
||||
read_member(root / "init.mp4", root, 65536),
|
||||
)
|
||||
)
|
||||
for event in merged_events(archive, self.args.camera_index, self.args.frames):
|
||||
if event.time_ns < self.source_zero:
|
||||
skipped[event.channel] += 1
|
||||
continue
|
||||
due = self.wall_zero + event.time_ns - self.source_zero
|
||||
if clock.wait(max(0, (due - time.monotonic_ns()) / 1e9)):
|
||||
break
|
||||
arrivals[event.channel] += 1
|
||||
lags.append(max(0, time.monotonic_ns() - due) / 1e6)
|
||||
utc = 0 # These normalized sensor rows have no UTC evidence.
|
||||
if event.channel == "camera":
|
||||
self.released.append(event.sequence)
|
||||
row = event.value
|
||||
raw = read_member(root / row["path"], root, 1024 * 1024)
|
||||
if (
|
||||
len(raw) != row["length"]
|
||||
or hashlib.sha256(raw).hexdigest() != row["sha256"]
|
||||
):
|
||||
raise ValueError("camera fragment integrity changed")
|
||||
modality, source_id, utc = (
|
||||
"camera-frame",
|
||||
"sensor.camera.right",
|
||||
row["host_epoch_ns"],
|
||||
)
|
||||
elif event.channel == "points":
|
||||
xyz, intensity = event.value
|
||||
raw = struct.pack("<I", len(xyz)) + xyz.tobytes() + intensity.tobytes()
|
||||
modality, source_id = "lidar", "normalized-map-point-increments"
|
||||
else:
|
||||
position, quaternion = event.value
|
||||
raw = position.tobytes() + quaternion.tobytes()
|
||||
modality, source_id = "pose", "normalized-map-from-lidar-pose"
|
||||
seq += 1
|
||||
sender.send(
|
||||
LiveIngressEvent(
|
||||
seq,
|
||||
"recorded-acquisition",
|
||||
1,
|
||||
modality,
|
||||
source_id,
|
||||
event.sequence,
|
||||
utc,
|
||||
event.time_ns,
|
||||
raw,
|
||||
)
|
||||
)
|
||||
del raw
|
||||
if event.channel == "camera" and event.sequence == self.args.frames - 1:
|
||||
break # Sensor streams may continue beyond this bounded camera probe.
|
||||
if not clock.is_set():
|
||||
self.report["end_sent_monotonic_ns"] = time.monotonic_ns()
|
||||
sender.end()
|
||||
except Exception:
|
||||
self.report["source_error"] = traceback.format_exc()
|
||||
self.runtime.mailbox.finish(self.report["source_error"])
|
||||
self.runtime.request_stop("failed")
|
||||
finally:
|
||||
from run_joint_pilot import distribution
|
||||
|
||||
self.report.update(
|
||||
arrivals=dict(arrivals),
|
||||
skipped_prefix=dict(skipped),
|
||||
release_lag_ms=distribution(lags),
|
||||
incremental_reads=archive.counters() if archive else {},
|
||||
window_end_monotonic_ns=time.monotonic_ns(),
|
||||
)
|
||||
if archive:
|
||||
archive.close()
|
||||
connection.close()
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Supervised CPU decoder child. No source files, camera count, models or network."""
|
||||
|
||||
import resource
|
||||
import sys
|
||||
import time
|
||||
|
||||
from pilot_ipc import receive, send
|
||||
|
||||
from k1link.perception.streaming_decoder import FragmentDecoder
|
||||
|
||||
|
||||
def main():
|
||||
resource.setrlimit(resource.RLIMIT_AS, (1024**3, 1024**3))
|
||||
decoder = FragmentDecoder()
|
||||
send(sys.stdout.buffer, {"ready": "fragment-h264", "pyav": "18.0.0", "as_limit_mib": 1024})
|
||||
try:
|
||||
while True:
|
||||
header, raw = receive(sys.stdin.buffer, max_payload=1024 * 1024)
|
||||
begin = time.monotonic_ns()
|
||||
if header == {"op": "init"}:
|
||||
decoder.configure(raw)
|
||||
send(sys.stdout.buffer, {"initialized": True})
|
||||
elif header == {"op": "decode"}:
|
||||
image = decoder.decode(raw)
|
||||
send(
|
||||
sys.stdout.buffer,
|
||||
{
|
||||
"decode_ms": (time.monotonic_ns() - begin) / 1e6,
|
||||
"frame_index": decoder.frames - 1,
|
||||
},
|
||||
image.tobytes(),
|
||||
)
|
||||
del image
|
||||
else:
|
||||
raise ValueError("unsupported decoder operation")
|
||||
del raw
|
||||
finally:
|
||||
decoder.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -20,13 +20,13 @@ def exact(stream, size):
|
||||
return bytes(chunks)
|
||||
|
||||
|
||||
def receive(stream):
|
||||
def receive(stream, max_payload=MAX_PAYLOAD):
|
||||
length = struct.unpack("<I", exact(stream, 4))[0]
|
||||
if not 1 <= length <= 65536:
|
||||
raise ValueError("IPC header exceeds budget")
|
||||
header = json.loads(exact(stream, length))
|
||||
size = header.pop("payload_bytes")
|
||||
if type(size) is not int:
|
||||
if type(size) is not int or not 0 <= size <= max_payload:
|
||||
raise ValueError("invalid IPC byte count")
|
||||
return header, exact(stream, size)
|
||||
|
||||
|
||||
@@ -1,129 +1,23 @@
|
||||
"""Causal input binding with explicit preroll history and per-modality age.
|
||||
"""Compatibility imports for the common causal binding; no alternate rules."""
|
||||
|
||||
No new inference or interpolation. The caller retains all preroll points in its
|
||||
bounded rolling window; only the initial current-pair selection is narrowed.
|
||||
After the first camera, the original increment admission rules are unchanged.
|
||||
"""
|
||||
from k1link.perception.streaming_sensors import (
|
||||
NEWEST_POINT_AGE_NS,
|
||||
OLDEST_POINT_AGE_NS,
|
||||
POINT_POSE_SKEW_NS,
|
||||
POSE_AGE_NS,
|
||||
SensorBinding,
|
||||
bind_sensors,
|
||||
increment_identity,
|
||||
milliseconds,
|
||||
)
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
POSE_AGE_NS = 100_000_000
|
||||
NEWEST_POINT_AGE_NS = 100_000_000
|
||||
OLDEST_POINT_AGE_NS = 250_000_000
|
||||
POINT_POSE_SKEW_NS = 100_000_000
|
||||
|
||||
|
||||
def increment_identity(event):
|
||||
return {
|
||||
"sequence": event.sequence,
|
||||
"host_monotonic_ns": event.time_ns,
|
||||
"points": len(event.value[0]),
|
||||
}
|
||||
|
||||
|
||||
def milliseconds(value):
|
||||
return None if value is None else value / 1e6
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SensorBinding:
|
||||
increments: tuple
|
||||
history_only: tuple
|
||||
pose_age_ns: int | None
|
||||
newest_point_age_ns: int | None
|
||||
oldest_point_age_ns: int | None
|
||||
binding_age_ns: int | None
|
||||
pose_state: str
|
||||
points_state: str
|
||||
reasons: tuple[str, ...]
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
return not self.reasons
|
||||
|
||||
def document(self):
|
||||
return {
|
||||
"schema_version": "missioncore.pilot-sensor-binding/v1",
|
||||
"pose_state": self.pose_state,
|
||||
"points_state": self.points_state,
|
||||
"current_pair_available": self.available,
|
||||
"reason_codes": list(self.reasons),
|
||||
"pose_age_ms": milliseconds(self.pose_age_ns),
|
||||
"newest_point_age_ms": milliseconds(self.newest_point_age_ns),
|
||||
"oldest_point_age_ms": milliseconds(self.oldest_point_age_ns),
|
||||
"point_pose_skew_ms": milliseconds(self.binding_age_ns),
|
||||
"preroll_history_only": [increment_identity(e) for e in self.history_only],
|
||||
"preroll_history_disposition": "retained-in-bounded-rolling-window",
|
||||
}
|
||||
|
||||
|
||||
def bind_sensors(camera_time_ns, pose, increments, *, previous_camera_time_ns=None):
|
||||
increments = tuple(increments)
|
||||
if previous_camera_time_ns is not None and previous_camera_time_ns >= camera_time_ns:
|
||||
raise ValueError("camera binding clock must increase")
|
||||
if pose is not None and pose.time_ns > camera_time_ns:
|
||||
raise ValueError("future pose cannot bind to camera")
|
||||
if any(e.time_ns > camera_time_ns for e in increments):
|
||||
raise ValueError("future points cannot bind to camera")
|
||||
if any(a.time_ns > b.time_ns for a, b in zip(increments, increments[1:], strict=False)):
|
||||
raise ValueError("point binding clock moved backwards")
|
||||
|
||||
history_only = ()
|
||||
if previous_camera_time_ns is None:
|
||||
# History used to warm rolling geometry is not one current increment.
|
||||
# Preserve its identity separately; do not retimestamp or silently drop it.
|
||||
selected, history = [], []
|
||||
for event in increments:
|
||||
is_current = (
|
||||
pose is not None
|
||||
and camera_time_ns - event.time_ns <= OLDEST_POINT_AGE_NS
|
||||
and abs(event.time_ns - pose.time_ns) <= POINT_POSE_SKEW_NS
|
||||
)
|
||||
(selected if is_current else history).append(event)
|
||||
increments, history_only = tuple(selected), tuple(history)
|
||||
|
||||
pose_age = None if pose is None else camera_time_ns - pose.time_ns
|
||||
newest_age = None if not increments else camera_time_ns - increments[-1].time_ns
|
||||
oldest_age = None if not increments else camera_time_ns - increments[0].time_ns
|
||||
skew = (
|
||||
max(abs(e.time_ns - pose.time_ns) for e in increments)
|
||||
if increments and pose is not None
|
||||
else None
|
||||
)
|
||||
reasons = []
|
||||
if pose is None:
|
||||
pose_state = "unavailable"
|
||||
reasons.append("pose-unavailable")
|
||||
elif pose_age > POSE_AGE_NS:
|
||||
pose_state = "stale"
|
||||
reasons.append("pose-too-old")
|
||||
else:
|
||||
pose_state = (
|
||||
"held"
|
||||
if previous_camera_time_ns is not None and pose.time_ns <= previous_camera_time_ns
|
||||
else "current"
|
||||
)
|
||||
if not increments or not any(len(e.value[0]) for e in increments):
|
||||
points_state = "unavailable"
|
||||
reasons.append("point-increment-unavailable")
|
||||
else:
|
||||
points_state = "current"
|
||||
if newest_age > NEWEST_POINT_AGE_NS:
|
||||
points_state = "stale"
|
||||
reasons.append("newest-points-too-old")
|
||||
if oldest_age > OLDEST_POINT_AGE_NS:
|
||||
points_state = "stale"
|
||||
reasons.append("oldest-points-too-old")
|
||||
if skew is not None and skew > POINT_POSE_SKEW_NS:
|
||||
reasons.append("point-pose-skew")
|
||||
return SensorBinding(
|
||||
increments,
|
||||
history_only,
|
||||
pose_age,
|
||||
newest_age,
|
||||
oldest_age,
|
||||
skew,
|
||||
pose_state,
|
||||
points_state,
|
||||
tuple(reasons),
|
||||
)
|
||||
__all__ = [
|
||||
"NEWEST_POINT_AGE_NS",
|
||||
"OLDEST_POINT_AGE_NS",
|
||||
"POINT_POSE_SKEW_NS",
|
||||
"POSE_AGE_NS",
|
||||
"SensorBinding",
|
||||
"bind_sensors",
|
||||
"increment_identity",
|
||||
"milliseconds",
|
||||
]
|
||||
|
||||
@@ -35,6 +35,8 @@ from pilot_telemetry import NvmlSampler
|
||||
|
||||
|
||||
def distribution(values):
|
||||
# Missing diagnostics are not zero; binary source lag is measured separately.
|
||||
values = [value for value in values if value is not None]
|
||||
if not values:
|
||||
return None
|
||||
values = sorted(values)
|
||||
@@ -271,6 +273,7 @@ def run(args):
|
||||
"triton_verbose": args.triton_verbose,
|
||||
"costmap_freshness_mode": args.costmap_freshness,
|
||||
"ddrnet_min_source_interval_ms": args.ddrnet_min_source_interval_ms,
|
||||
"input_transport": args.input_transport,
|
||||
}
|
||||
children, logs, results, samples = [], [], [], []
|
||||
stop = threading.Event()
|
||||
@@ -279,6 +282,7 @@ def run(args):
|
||||
producer = monitor = graph = gpu_stage = ddr_backend = None
|
||||
cpu_bundle = None
|
||||
controller = None
|
||||
binary_bridge = decoder_client = None
|
||||
epoch_id = args.run_id
|
||||
|
||||
def child(name, command, env=None):
|
||||
@@ -385,10 +389,28 @@ def run(args):
|
||||
ddr_backend.infer(tensor)
|
||||
decoder = child(
|
||||
"decoder",
|
||||
[python, "-B", "/probe/pilot_model.py", "camera"],
|
||||
{**os.environ, "PYTHONPATH": "/probe", "CUDA_VISIBLE_DEVICES": ""},
|
||||
(
|
||||
["python3", "-B", "/probe/pilot_fragment_decoder.py"]
|
||||
if args.input_transport == "binary-ipc"
|
||||
else [python, "-B", "/probe/pilot_model.py", "camera"]
|
||||
),
|
||||
{
|
||||
**os.environ,
|
||||
"PYTHONPATH": os.environ["PYTHONPATH"]
|
||||
if args.input_transport == "binary-ipc"
|
||||
else "/probe",
|
||||
"CUDA_VISIBLE_DEVICES": "",
|
||||
},
|
||||
)
|
||||
report["decoder_ready"], _ = receive(decoder.stdout)
|
||||
if args.input_transport == "binary-ipc":
|
||||
from k1link.perception.streaming_decoder_client import StreamingDecoderClient
|
||||
|
||||
decoder_client = StreamingDecoderClient(
|
||||
controller.runtime, decoder.stdout.fileno(), decoder.stdin.fileno()
|
||||
)
|
||||
report["decoder_ready"] = decoder_client.ready
|
||||
else:
|
||||
report["decoder_ready"], _ = receive(decoder.stdout)
|
||||
tgs = child("tgs", ["/usr/local/bin/pilot-tgs"])
|
||||
from pilot_graph import JointGraph
|
||||
|
||||
@@ -489,12 +511,22 @@ def run(args):
|
||||
from pilot_lifecycle import FencedIngress
|
||||
|
||||
ingress = FencedIngress(controller)
|
||||
producer = threading.Thread(
|
||||
target=produce, args=(args, decoder, ingress, stop, source_report), daemon=True
|
||||
)
|
||||
if controller:
|
||||
controller.runtime.track_thread(producer)
|
||||
producer.start()
|
||||
if args.input_transport == "binary-ipc":
|
||||
from pilot_binary_bridge import BinaryGraphBridge
|
||||
from pilot_binary_source import RecordingSource
|
||||
|
||||
source = RecordingSource(args, controller.runtime, source_report)
|
||||
binary_bridge = BinaryGraphBridge(
|
||||
controller.runtime, decoder_client, source, source_report
|
||||
)
|
||||
binary_bridge.start()
|
||||
else:
|
||||
producer = threading.Thread(
|
||||
target=produce, args=(args, decoder, ingress, stop, source_report), daemon=True
|
||||
)
|
||||
if controller:
|
||||
controller.runtime.track_thread(producer)
|
||||
producer.start()
|
||||
if args.schedule == "overlap-cpu":
|
||||
gpu_stage = GpuStage(mailbox, compute_gpu, stop)
|
||||
if controller:
|
||||
@@ -666,6 +698,10 @@ def run(args):
|
||||
process.wait(timeout=1)
|
||||
if producer:
|
||||
producer.join(timeout=2)
|
||||
if binary_bridge:
|
||||
report["binary_bridge_stopped"] = binary_bridge.close()
|
||||
if decoder_client and (not binary_bridge or report["binary_bridge_stopped"]):
|
||||
decoder_client.close()
|
||||
if monitor:
|
||||
monitor.join(timeout=2)
|
||||
report["gpu_stage_stopped"] = gpu_stage.close() if gpu_stage else True
|
||||
@@ -813,6 +849,10 @@ if __name__ == "__main__":
|
||||
parser.add_argument("--output", required=True)
|
||||
parser.add_argument("--sensor-archive", default="/sensor-source.npz")
|
||||
parser.add_argument("--camera-index", default="/camera-index.jsonl")
|
||||
parser.add_argument("--camera-root", default="/camera")
|
||||
parser.add_argument(
|
||||
"--input-transport", choices=("legacy-pilot", "binary-ipc"), default="legacy-pilot"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ddrnet-layout", choices=("reference", "channels-last"), default="reference"
|
||||
)
|
||||
@@ -844,4 +884,6 @@ if __name__ == "__main__":
|
||||
parser.error("worker lease and launcher-verified image identity are required together")
|
||||
if args.stop_renew_after_sequence != -1 and not args.worker_lease_root:
|
||||
parser.error("lease-expiry injection requires a controller")
|
||||
if args.input_transport == "binary-ipc" and not args.worker_lease_root:
|
||||
parser.error("binary ingress requires the common lifecycle")
|
||||
raise SystemExit(run(args))
|
||||
|
||||
Reference in New Issue
Block a user