feat(perception): enforce six-layer freshness through receipt
This commit is contained in:
@@ -181,6 +181,9 @@ class LayerEvidence:
|
||||
source_time_ns: int | None
|
||||
state: str
|
||||
payload_sha256: str | None
|
||||
# Old callers omit this field. New layered results require it explicitly:
|
||||
# recomputing a derived layer cannot refresh the age of its required inputs.
|
||||
oldest_required_input_time_ns: int | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.layer not in REQUIRED_LAYERS:
|
||||
@@ -191,13 +194,22 @@ class LayerEvidence:
|
||||
if self.state == "unavailable":
|
||||
if any(
|
||||
x is not None
|
||||
for x in (self.source_sequence, self.source_time_ns, self.payload_sha256)
|
||||
for x in (
|
||||
self.source_sequence,
|
||||
self.source_time_ns,
|
||||
self.payload_sha256,
|
||||
self.oldest_required_input_time_ns,
|
||||
)
|
||||
):
|
||||
raise RealtimeContractError("unavailable evidence cannot claim a payload")
|
||||
else:
|
||||
_integer(self.source_sequence, "source_sequence")
|
||||
_integer(self.source_time_ns, "source_time_ns")
|
||||
_digest(self.payload_sha256, "payload_sha256")
|
||||
if self.oldest_required_input_time_ns is not None:
|
||||
_integer(self.oldest_required_input_time_ns, "oldest_required_input_time_ns")
|
||||
if self.oldest_required_input_time_ns > cast(int, self.source_time_ns):
|
||||
raise RealtimeContractError("required input is newer than its layer")
|
||||
|
||||
|
||||
def validate_scene_layers(
|
||||
@@ -223,7 +235,10 @@ def validate_scene_layers(
|
||||
raise RealtimeContractError("layer belongs to another stream epoch")
|
||||
if layer.source_time_ns is None:
|
||||
continue
|
||||
age_ns = source_time_ns - layer.source_time_ns
|
||||
if layer.source_time_ns > source_time_ns:
|
||||
raise RealtimeContractError("future observations are not causal input")
|
||||
stamp = layer.oldest_required_input_time_ns
|
||||
age_ns = source_time_ns - (layer.source_time_ns if stamp is None else stamp)
|
||||
if age_ns < 0:
|
||||
raise RealtimeContractError("future observations are not causal input")
|
||||
if age_ns > maximum_layer_age_ms * 1_000_000 and layer.state in ("current", "held"):
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
"""Bounded six-layer freshness ABI; no I/O, models or control authority.
|
||||
|
||||
This wraps existing domain payloads. It is not the transport/session admission
|
||||
protocol: a transport must also validate StreamStart identities and lease fences.
|
||||
All times here are in ONE mapped source timeline, never raw remote worker clocks.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, replace
|
||||
from typing import Final
|
||||
|
||||
from .realtime_contract import (
|
||||
REQUIRED_LAYERS,
|
||||
LayerEvidence,
|
||||
RealtimeContractError,
|
||||
_identifier,
|
||||
_integer,
|
||||
_number,
|
||||
)
|
||||
|
||||
SCENE_FRESHNESS_SCHEMA: Final = "missioncore.perception-scene-freshness/v1"
|
||||
_INT64_FIELDS: Final = ("source_sequence", "source_time_ns", "oldest_required_input_time_ns")
|
||||
|
||||
|
||||
def _wire_integer(value: int) -> str:
|
||||
_integer(value, "wire integer")
|
||||
if value > 2**63 - 1:
|
||||
raise RealtimeContractError("wire integer exceeds int64")
|
||||
return str(value)
|
||||
|
||||
|
||||
def _read_wire_integer(value: object) -> int:
|
||||
if (
|
||||
not isinstance(value, str)
|
||||
or not value.isascii()
|
||||
or not value.isdecimal()
|
||||
or len(value) > 19
|
||||
or (len(value) > 1 and value.startswith("0"))
|
||||
):
|
||||
raise RealtimeContractError("int64 must be a canonical decimal string")
|
||||
parsed = int(value)
|
||||
_wire_integer(parsed)
|
||||
return parsed
|
||||
|
||||
|
||||
def _layer_document(item: LayerEvidence) -> dict[str, object]:
|
||||
value = asdict(item)
|
||||
for name in _INT64_FIELDS:
|
||||
number = value[name]
|
||||
value[name] = None if number is None else _wire_integer(number)
|
||||
return value
|
||||
|
||||
|
||||
# Geometry includes detector association; history may add prohibitions, not
|
||||
# independently authorize terrain. This is the full profile's dependency graph.
|
||||
DEPENDENCIES: Final = {
|
||||
"segmentation": (),
|
||||
"objects": (),
|
||||
"geometry": ("objects",),
|
||||
"motion": ("geometry",),
|
||||
"costmap": ("segmentation", "geometry", "motion"),
|
||||
"policy": ("costmap", "objects", "motion"),
|
||||
}
|
||||
|
||||
|
||||
def derive_layer(
|
||||
layer: str,
|
||||
*,
|
||||
epoch_id: str,
|
||||
source_sequence: int,
|
||||
source_time_ns: int,
|
||||
payload_sha256: str,
|
||||
inputs: tuple[LayerEvidence, ...],
|
||||
oldest_required_input_time_ns: int,
|
||||
) -> LayerEvidence:
|
||||
"""Propagate missing/stale inputs and their original ages, not execution time."""
|
||||
if layer not in DEPENDENCIES or tuple(x.layer for x in inputs) != DEPENDENCIES[layer]:
|
||||
raise RealtimeContractError("derived layer dependencies changed")
|
||||
if any(x.epoch_id != epoch_id for x in inputs):
|
||||
raise RealtimeContractError("derived input belongs to another epoch")
|
||||
if any(x.state == "unavailable" for x in inputs):
|
||||
return LayerEvidence(layer, epoch_id, None, None, "unavailable", None)
|
||||
stamps = [oldest_required_input_time_ns]
|
||||
for item in inputs:
|
||||
if item.oldest_required_input_time_ns is None:
|
||||
raise RealtimeContractError("required input age is missing")
|
||||
stamps.append(item.oldest_required_input_time_ns)
|
||||
return LayerEvidence(
|
||||
layer,
|
||||
epoch_id,
|
||||
source_sequence,
|
||||
source_time_ns,
|
||||
"stale" if any(x.state == "stale" for x in inputs) else "current",
|
||||
payload_sha256,
|
||||
min(stamps),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class FreshnessAssessment:
|
||||
checked_at_source_time_ns: int
|
||||
clock_uncertainty_ms: float
|
||||
layers: tuple[LayerEvidence, ...]
|
||||
age_upper_bound_ms: tuple[float | None, ...]
|
||||
failures: tuple[str, ...]
|
||||
|
||||
@property
|
||||
def fresh_complete(self) -> bool:
|
||||
return not self.failures
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"checked_at_source_time_ns": _wire_integer(self.checked_at_source_time_ns),
|
||||
"clock_uncertainty_ms": self.clock_uncertainty_ms,
|
||||
"layers": {
|
||||
item.layer: {**_layer_document(item), "age_upper_bound_ms": age}
|
||||
for item, age in zip(self.layers, self.age_upper_bound_ms, strict=True)
|
||||
},
|
||||
"fresh_complete": self.fresh_complete,
|
||||
"failures": list(self.failures),
|
||||
"commands_enabled": False,
|
||||
"actuation_allowed": False,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SceneFreshness:
|
||||
epoch_id: str
|
||||
clock_domain_id: str
|
||||
source_sequence: int
|
||||
source_time_ns: int
|
||||
layers: tuple[LayerEvidence, ...]
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_identifier(self.epoch_id, "epoch_id")
|
||||
_identifier(self.clock_domain_id, "clock_domain_id")
|
||||
_integer(self.source_sequence, "source_sequence")
|
||||
_integer(self.source_time_ns, "source_time_ns")
|
||||
if (
|
||||
not isinstance(self.layers, tuple)
|
||||
or tuple(x.layer for x in self.layers) != REQUIRED_LAYERS
|
||||
):
|
||||
raise RealtimeContractError("six layers required in canonical order")
|
||||
indexed = {item.layer: item for item in self.layers}
|
||||
for item in self.layers:
|
||||
if item.epoch_id != self.epoch_id:
|
||||
raise RealtimeContractError("layer belongs to another epoch")
|
||||
if item.state == "unavailable":
|
||||
continue
|
||||
if (
|
||||
item.source_sequence is None
|
||||
or item.source_time_ns is None
|
||||
or item.oldest_required_input_time_ns is None
|
||||
):
|
||||
raise RealtimeContractError("layer input lineage missing")
|
||||
if (
|
||||
item.source_sequence > self.source_sequence
|
||||
or item.source_time_ns > self.source_time_ns
|
||||
):
|
||||
raise RealtimeContractError("future layer evidence")
|
||||
if item.state == "current" and (
|
||||
item.source_sequence != self.source_sequence
|
||||
or item.source_time_ns != self.source_time_ns
|
||||
):
|
||||
raise RealtimeContractError("retained evidence must be held, not current")
|
||||
if item.state == "held" and item.source_sequence == self.source_sequence:
|
||||
raise RealtimeContractError("held layer must retain an earlier observation")
|
||||
for name in DEPENDENCIES[item.layer]:
|
||||
parent = indexed[name]
|
||||
if parent.oldest_required_input_time_ns is None:
|
||||
raise RealtimeContractError("available layer depends on unavailable input")
|
||||
if item.oldest_required_input_time_ns > parent.oldest_required_input_time_ns:
|
||||
raise RealtimeContractError("derived layer refreshed an older input")
|
||||
if parent.state == "stale" and item.state != "stale":
|
||||
raise RealtimeContractError("derived layer hid stale input")
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": SCENE_FRESHNESS_SCHEMA,
|
||||
"epoch_id": self.epoch_id,
|
||||
"clock_domain_id": self.clock_domain_id,
|
||||
"source_sequence": _wire_integer(self.source_sequence),
|
||||
"source_time_ns": _wire_integer(self.source_time_ns),
|
||||
"layers": [_layer_document(item) for item in self.layers],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, value: object) -> SceneFreshness:
|
||||
if not isinstance(value, dict) or set(value) != set(cls.__dataclass_fields__) | {
|
||||
"schema_version"
|
||||
}:
|
||||
raise RealtimeContractError("scene freshness fields changed")
|
||||
if value["schema_version"] != SCENE_FRESHNESS_SCHEMA:
|
||||
raise RealtimeContractError("scene freshness schema changed")
|
||||
raw = value["layers"]
|
||||
if not isinstance(raw, list) or len(raw) != len(REQUIRED_LAYERS):
|
||||
raise RealtimeContractError("six layers required")
|
||||
if any(
|
||||
not isinstance(x, dict) or set(x) != set(LayerEvidence.__dataclass_fields__)
|
||||
for x in raw
|
||||
):
|
||||
raise RealtimeContractError("layer fields changed")
|
||||
layers = []
|
||||
for item in raw:
|
||||
parsed = dict(item)
|
||||
for name in _INT64_FIELDS:
|
||||
parsed[name] = None if item[name] is None else _read_wire_integer(item[name])
|
||||
layers.append(LayerEvidence(**parsed))
|
||||
return cls(
|
||||
value["epoch_id"],
|
||||
value["clock_domain_id"],
|
||||
_read_wire_integer(value["source_sequence"]),
|
||||
_read_wire_integer(value["source_time_ns"]),
|
||||
tuple(layers),
|
||||
)
|
||||
|
||||
def assess(
|
||||
self,
|
||||
*,
|
||||
epoch_id: str,
|
||||
clock_domain_id: str,
|
||||
observed_source_time_ns: int,
|
||||
clock_uncertainty_ms: float,
|
||||
maximum_clock_uncertainty_ms: float,
|
||||
maximum_layer_age_ms: float,
|
||||
) -> FreshnessAssessment:
|
||||
"""Re-evaluate at publication, receipt AND use. Uncertainty adds to age.
|
||||
|
||||
Unknown/unmapped clocks must not call this with a guessed zero offset.
|
||||
The source adapter owns the mapping; excessive known uncertainty yields
|
||||
degraded output, never a complete fresh-scene claim.
|
||||
"""
|
||||
if (epoch_id, clock_domain_id) != (self.epoch_id, self.clock_domain_id):
|
||||
raise RealtimeContractError("epoch or mapped clock domain mismatch")
|
||||
_integer(observed_source_time_ns, "observed_source_time_ns")
|
||||
_number(clock_uncertainty_ms, "clock_uncertainty_ms")
|
||||
_number(maximum_clock_uncertainty_ms, "maximum_clock_uncertainty_ms", minimum=0.001)
|
||||
_number(maximum_layer_age_ms, "maximum_layer_age_ms", minimum=0.001)
|
||||
if observed_source_time_ns < self.source_time_ns:
|
||||
raise RealtimeContractError("receipt precedes source observation")
|
||||
clock_failed = clock_uncertainty_ms > maximum_clock_uncertainty_ms
|
||||
failures = ["clock-uncertainty"] if clock_failed else []
|
||||
evaluated, ages = [], []
|
||||
for item in self.layers:
|
||||
stamp = item.oldest_required_input_time_ns
|
||||
age = (
|
||||
None
|
||||
if stamp is None
|
||||
else (observed_source_time_ns - stamp) / 1e6 + clock_uncertainty_ms
|
||||
)
|
||||
if age is not None and (age > maximum_layer_age_ms or clock_failed):
|
||||
item = replace(item, state="stale")
|
||||
if item.state in ("stale", "unavailable"):
|
||||
failures.append(f"{item.layer}-{item.state}")
|
||||
evaluated.append(item)
|
||||
ages.append(age)
|
||||
return FreshnessAssessment(
|
||||
observed_source_time_ns,
|
||||
clock_uncertainty_ms,
|
||||
tuple(evaluated),
|
||||
tuple(ages),
|
||||
tuple(failures),
|
||||
)
|
||||
@@ -0,0 +1,153 @@
|
||||
"""Read-only post-warmup readiness checks, not GPU management or qualification.
|
||||
|
||||
An envelope is preregistered for a measured hardware/software context. A failed
|
||||
check does not prohibit an explicitly labelled overload experiment. Passing is
|
||||
only a prerequisite: it cannot certify clocks throughout a run, latency, model
|
||||
quality, standalone packaging or network delivery. No host setters live here.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .realtime_contract import (
|
||||
RealtimeContractError,
|
||||
StreamStart,
|
||||
_digest,
|
||||
_identifier,
|
||||
_integer,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class WorkerOperatingEnvelope:
|
||||
envelope_id: str
|
||||
gpu_name: str
|
||||
driver_version: str
|
||||
# Runtime versions and all model/CPU-thread settings belong to the pinned
|
||||
# image/effective config in StreamStart; resource limits are explicit here.
|
||||
cpu_limit_millicores: int
|
||||
memory_limit_mib: int
|
||||
minimum_sm_clock_mhz: int
|
||||
minimum_memory_clock_mhz: int
|
||||
maximum_snapshot_age_ms: int = 1000
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_identifier(self.envelope_id, "envelope_id")
|
||||
for field in ("gpu_name", "driver_version"):
|
||||
value = getattr(self, field)
|
||||
if not isinstance(value, str) or not value.strip() or len(value) > 160:
|
||||
raise RealtimeContractError(f"{field} is missing or unbounded")
|
||||
for field in (
|
||||
"cpu_limit_millicores",
|
||||
"memory_limit_mib",
|
||||
"minimum_sm_clock_mhz",
|
||||
"minimum_memory_clock_mhz",
|
||||
"maximum_snapshot_age_ms",
|
||||
):
|
||||
_integer(getattr(self, field), field, minimum=1)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class WorkerSnapshot:
|
||||
worker_id: str
|
||||
clock_domain_id: str
|
||||
observed_monotonic_ns: int
|
||||
gpu_name: str | None
|
||||
driver_version: str | None
|
||||
image_sha256: str | None
|
||||
effective_config_sha256: str | None
|
||||
cpu_limit_millicores: int | None
|
||||
memory_limit_mib: int | None
|
||||
sm_clock_mhz: int | None
|
||||
memory_clock_mhz: int | None
|
||||
# Authoritative controller lease, NOT inferred from low GPU utilization.
|
||||
gpu_owner_run_id: str | None
|
||||
lease_generation: int | None
|
||||
competing_gpu_clients: tuple[str, ...] | None
|
||||
warmup_complete: bool | None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_identifier(self.worker_id, "worker_id")
|
||||
_identifier(self.clock_domain_id, "clock_domain_id")
|
||||
_integer(self.observed_monotonic_ns, "observed_monotonic_ns")
|
||||
for field in ("image_sha256", "effective_config_sha256"):
|
||||
value = getattr(self, field)
|
||||
if value is not None:
|
||||
_digest(value, field)
|
||||
for field in ("gpu_name", "driver_version"):
|
||||
value = getattr(self, field)
|
||||
if value is not None and (
|
||||
not isinstance(value, str) or not value.strip() or len(value) > 160
|
||||
):
|
||||
raise RealtimeContractError(f"{field} is invalid")
|
||||
for field in (
|
||||
"cpu_limit_millicores",
|
||||
"memory_limit_mib",
|
||||
"sm_clock_mhz",
|
||||
"memory_clock_mhz",
|
||||
):
|
||||
value = getattr(self, field)
|
||||
if value is not None:
|
||||
# Docker 0 means unlimited; never confuse it with unknown=None.
|
||||
_integer(value, field)
|
||||
if self.gpu_owner_run_id is not None:
|
||||
_identifier(self.gpu_owner_run_id, "gpu_owner_run_id")
|
||||
if self.lease_generation is not None:
|
||||
_integer(self.lease_generation, "lease_generation", minimum=1)
|
||||
if self.competing_gpu_clients is not None:
|
||||
if (
|
||||
not isinstance(self.competing_gpu_clients, tuple)
|
||||
or len(self.competing_gpu_clients) > 64
|
||||
):
|
||||
raise RealtimeContractError("GPU client inventory is unbounded")
|
||||
for client in self.competing_gpu_clients:
|
||||
_identifier(client, "competing_gpu_client")
|
||||
if self.warmup_complete is not None and type(self.warmup_complete) is not bool:
|
||||
raise RealtimeContractError("warmup_complete must be boolean or unknown")
|
||||
|
||||
|
||||
def operating_envelope_failures(
|
||||
expected: WorkerOperatingEnvelope,
|
||||
start: StreamStart,
|
||||
observed: WorkerSnapshot,
|
||||
*,
|
||||
now_monotonic_ns: int,
|
||||
clock_domain_id: str,
|
||||
) -> tuple[str, ...]:
|
||||
"""Check facts collected in the Worker's clock domain by a trusted controller.
|
||||
|
||||
No resource expansion, frequency lock, container stop, lease acquisition or
|
||||
host mutation. Snapshot refresh and ongoing telemetry remain caller-owned.
|
||||
Changing hardware/resource conditions requires a new measured envelope, not
|
||||
pretending that an old qualification applies to all compatible machines.
|
||||
"""
|
||||
_integer(now_monotonic_ns, "now_monotonic_ns")
|
||||
if observed.clock_domain_id != clock_domain_id:
|
||||
raise RealtimeContractError("worker snapshot clock domain mismatch")
|
||||
age = now_monotonic_ns - observed.observed_monotonic_ns
|
||||
if age < 0:
|
||||
raise RealtimeContractError("worker snapshot is from the future")
|
||||
failures = []
|
||||
if age > expected.maximum_snapshot_age_ms * 1_000_000:
|
||||
failures.append("worker-snapshot-expired")
|
||||
for field in ("worker_id", "image_sha256", "effective_config_sha256"):
|
||||
if getattr(observed, field) != getattr(start, field):
|
||||
failures.append(f"{field}-mismatch-or-unknown")
|
||||
for field in ("gpu_name", "driver_version", "cpu_limit_millicores", "memory_limit_mib"):
|
||||
if getattr(observed, field) != getattr(expected, field):
|
||||
failures.append(f"{field}-outside-envelope-or-unknown")
|
||||
for field in ("sm_clock_mhz", "memory_clock_mhz"):
|
||||
value = getattr(observed, field)
|
||||
if value is None or value < getattr(expected, "minimum_" + field):
|
||||
failures.append(f"{field}-below-envelope-or-unknown")
|
||||
if (observed.gpu_owner_run_id, observed.lease_generation) != (
|
||||
start.run_id,
|
||||
start.lease_generation,
|
||||
):
|
||||
failures.append("exclusive-worker-lease-unproved")
|
||||
if observed.competing_gpu_clients != ():
|
||||
failures.append("competing-gpu-clients-or-inventory-unknown")
|
||||
if observed.warmup_complete is not True:
|
||||
failures.append("warmup-not-complete")
|
||||
return tuple(failures)
|
||||
Reference in New Issue
Block a user