feat(perception): preserve resident runtime across recoverable input gaps

This commit is contained in:
DCCONSTRUCTIONS
2026-09-02 15:27:06 +03:00
parent 06a66a3da8
commit c31c46caa5
14 changed files with 1041 additions and 33 deletions
@@ -0,0 +1,281 @@
"""CPU-only recovery proof: real H264 fixture, synthetic clock/pose/points.
Not a full graph run: a resident CPU sentinel stands in for loaded GPU models.
The real decoder process must survive two IPC reconnects and reinitialize its
codec. The source clock/tick counter continues through each outage, no backlog.
"""
import argparse
import hashlib
import json
import os
import select
import socket
import struct
import subprocess
import sys
import tempfile
import threading
import time
from datetime import UTC, datetime
from pathlib import Path
from k1link.compute.live_perception import LiveIngressEvent
from k1link.perception.realtime_contract import StreamStart
from k1link.perception.streaming_continuity import ResumeEvidence, StreamSuspended
from k1link.perception.streaming_decoder_client import StreamingDecoderClient
from k1link.perception.streaming_ingress import StreamingIngress
from k1link.perception.streaming_lifecycle import StreamingLifecycle
from k1link.perception.streaming_queue import StreamMailbox
from k1link.perception.streaming_sender import StreamingSender
from k1link.perception.streaming_sensors import CausalSensorWindow, normalized_sensor
from k1link.perception.worker_lease import WorkerLeaseError
def run(output, camera):
output.mkdir()
with (camera / "index.jsonl").open() as stream:
raw = stream.readline(65537)
assert len(raw) <= 65536
row = json.loads(raw)
frame_path = (camera / row["path"]).resolve(strict=True)
assert frame_path.is_relative_to(camera.resolve())
assert 0 < frame_path.stat().st_size <= 1024 * 1024
assert 0 < (camera / "init.mp4").stat().st_size <= 65536
fragment, init = frame_path.read_bytes(), (camera / "init.mp4").read_bytes()
assert len(init) <= 65536 and hashlib.sha256(fragment).hexdigest() == row["sha256"]
report = {
"created_utc": datetime.now(UTC).isoformat(),
"started_monotonic_ns": time.monotonic_ns(),
"scope": (
"real decoder and Linux processes; synthetic source timestamps/pose/points; "
"NO GPU models or full graph"
),
"source_full_prepass": False,
"source_index_rows_read": 1,
"fixture_fragment_sha256": hashlib.sha256(fragment).hexdigest(),
"gpu_devices": sorted(str(p) for p in Path("/dev").glob("nvidia*")),
"cycles": [],
}
assert not report["gpu_devices"] and os.environ.get("CUDA_VISIBLE_DEVICES") == ""
activation = StreamStart(
output.name,
"synthetic-recovery",
"synthetic-worker",
"activation-1",
1,
"a" * 64,
"b" * 64,
hashlib.sha256(b"resumable-input-real-decoder-probe-v1").hexdigest(),
"d" * 64,
"worker-mapped-fixture-clock",
"live",
)
with tempfile.TemporaryDirectory(prefix="continuity-lease-") as directory:
runtime = StreamingLifecycle(
activation,
Path(directory),
StreamMailbox(),
threading.Event(),
recover_input=True,
source_clock_ns=time.monotonic_ns,
)
finish = threading.Event()
ticks = [0]
def controller():
while not finish.wait(0.05):
ticks[0] += 1
try:
runtime.renew(activation)
except WorkerLeaseError:
return
thread = threading.Thread(target=controller, daemon=True)
thread.start()
decoder = window = receiver = sender = None
try:
sentinel = runtime.spawn(
lambda: subprocess.Popen(
[
sys.executable,
"-c",
"import time; print('ready',flush=True); time.sleep(30)",
],
stdout=subprocess.PIPE,
start_new_session=True,
)
)
assert (
select.select([sentinel.stdout], [], [], 2)[0]
and sentinel.stdout.readline() == b"ready\n"
)
child = runtime.spawn(
lambda: subprocess.Popen(
["python3", "-B", "/probe/pilot_fragment_decoder.py"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
start_new_session=True,
)
)
decoder = StreamingDecoderClient(runtime, child.stdout.fileno(), child.stdin.fileno())
window = CausalSensorWindow(runtime.mailbox)
runtime.ready()
original_pids = [sentinel.pid, child.pid]
report["resident_pids"] = original_pids
epoch = activation
for cycle in range(3):
received = threading.Event()
item = {"cycle": cycle, "input_start": epoch.to_dict()}
def consume(event, cycle=cycle, epoch=epoch, item=item, received=received):
if event.modality == "camera-init":
if cycle:
decoder.reset(epoch, event.payload)
window.reset()
else:
decoder.configure(event.payload)
return
if event.modality in ("lidar", "pose"):
window.append(
normalized_sensor(
event.modality,
event.source_sequence,
event.received_monotonic_ns,
event.payload,
)
)
return
ticket = runtime.mailbox.reserve_ingress(1440000)
target = None
try:
target = bytearray(1440000)
decoded = decoder.decode(event.payload, target)
binding = window.bind(event.received_monotonic_ns)
assert binding.available and decoded["frame_index"] == 0
if cycle:
evidence = ResumeEvidence(
event.received_monotonic_ns,
window.pose.time_ns,
binding.increments[0].time_ns,
binding.increments[-1].time_ns,
decoder.frames == 1,
)
# Synthetic temporal-state owner only; not JointGraph/TGS reset proof.
runtime.resume_input(
epoch, evidence, lambda: item.update(temporal_fixture_reset=True)
)
with runtime.work(epoch, "gpu"):
item["bgr_sha256"] = hashlib.sha256(target).hexdigest()
runtime.validate_result_binding(epoch.to_dict())
window.finish_camera(event.received_monotonic_ns)
item["completed_monotonic_ns"] = time.monotonic_ns()
received.set()
finally:
target = None
ticket.release()
left, right = socket.socketpair()
receiver = StreamingIngress(
right,
runtime,
"fixture-acquisition",
1,
consume,
lambda _: None,
input_epoch=epoch,
)
receiver.start()
sender = StreamingSender(
left,
epoch,
"fixture-acquisition",
1,
lambda epoch=epoch: runtime.check_input(epoch, synchronizing=True),
)
stamp = time.monotonic_ns()
payloads = [
("camera-init", "camera", init),
("pose", "pose", struct.pack("<7d", 0, 0, 0, 0, 0, 0, 1)),
("lidar", "points", struct.pack("<I3dB", 1, 1, 0, 0, 1)),
("camera-frame", "camera", fragment),
]
for sequence, (modality, source, payload) in enumerate(payloads, 1):
sender.send(
LiveIngressEvent(
sequence,
"fixture-acquisition",
1,
modality,
source,
0,
0,
stamp,
payload,
)
)
assert received.wait(2), receiver.error
assert sentinel.poll() is None and child.poll() is None
if cycle == 2:
sender.end()
assert receiver.join() and receiver.terminal == "end"
else:
sender.close() # No End: a temporary cable/route loss.
assert receiver.join() and receiver.terminal == "paused"
assert not runtime.stop_event.is_set() and not runtime.lease.released
before = ticks[0]
delay = 0.15 if cycle == 0 else 2.2
assert not runtime.stop_event.wait(delay)
item["outage_seconds"] = delay
item["source_ticks_during_outage"] = ticks[0] - before
assert item["source_ticks_during_outage"] > 0
assert sentinel.poll() is None and child.poll() is None
with_rejected = False
try:
runtime.validate_result_binding(epoch.to_dict())
except StreamSuspended:
with_rejected = True
assert with_rejected
epoch = runtime.begin_input(activation)
item["receiver"] = receiver.snapshot()
item["resident_pids_unchanged"] = original_pids == [sentinel.pid, child.pid]
report["cycles"].append(item)
assert len({item["bgr_sha256"] for item in report["cycles"]}) == 1
report["passed"] = True
finally:
finish.set()
thread.join(timeout=1)
if sender:
sender.close()
runtime.request_stop("completed" if report.get("passed") else "failed")
if receiver:
receiver.join(timeout=2)
runtime.stop_children()
if window:
window.close()
if decoder:
decoder.close()
report["released"] = runtime.close()
report["runtime"] = runtime.snapshot()
report["input_bytes"] = runtime.mailbox.bytes
report["peak_input_bytes"] = runtime.mailbox.peak_bytes
report["finished_monotonic_ns"] = time.monotonic_ns()
report["source_ticks_total"] = ticks[0]
report["full_graph_recovery_proved"] = False
(output / "report.json").write_text(json.dumps(report, indent=2) + "\n")
for process in runtime._children:
for stream in (process.stdin, process.stdout):
if stream:
stream.close()
assert report["released"] and not report["input_bytes"]
print(
json.dumps({"passed": True, "cycles": 3, "same_decoder_pid": True, "full_graph": False})
)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--output", required=True)
parser.add_argument("--camera", required=True)
args = parser.parse_args()
run(Path(args.output), Path(args.camera))
@@ -20,6 +20,9 @@ def main():
if header == {"op": "init"}:
decoder.configure(raw)
send(sys.stdout.buffer, {"initialized": True})
elif header == {"op": "reset"}:
decoder.reset(raw)
send(sys.stdout.buffer, {"reset": True})
elif header == {"op": "decode"}:
image = decoder.decode(raw)
send(