feat(perception): integrate native RF-DETR shadow provider
This commit is contained in:
@@ -4,7 +4,10 @@ import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.perception.detector import RF_DETR_SHADOW_PROVIDER_ID
|
||||
from k1link.perception.detector import (
|
||||
RF_DETR_NATIVE_SHADOW_PROVIDER_ID,
|
||||
RF_DETR_SHADOW_PROVIDER_ID,
|
||||
)
|
||||
from k1link.perception.m48s_advisory import (
|
||||
AdvisoryFamily,
|
||||
advisory_policy_matrix,
|
||||
@@ -17,6 +20,10 @@ GRAPH_CONFIG = (
|
||||
REPOSITORY_ROOT
|
||||
/ "config/perception/m48s-rf-detr-reference-graph-shadow-v0.json"
|
||||
)
|
||||
NATIVE_GRAPH_CONFIG = (
|
||||
REPOSITORY_ROOT
|
||||
/ "config/perception/m48n-rf-detr-native-reference-graph-shadow-v0.json"
|
||||
)
|
||||
|
||||
|
||||
def test_m48s_reference_graph_replaces_only_the_detector_pin() -> None:
|
||||
@@ -64,6 +71,51 @@ def test_m48s_reference_graph_pins_every_profile_digest() -> None:
|
||||
assert pins[role].sha256 == hashlib.sha256(payload).hexdigest()
|
||||
|
||||
|
||||
def test_m48n_native_reference_graph_replaces_only_the_detector_pin() -> None:
|
||||
native = ReferencePerceptionGraphConfigV2.from_dict(
|
||||
json.loads(NATIVE_GRAPH_CONFIG.read_text("utf-8"))
|
||||
)
|
||||
legacy = ReferencePerceptionGraphConfigV2.from_dict(
|
||||
json.loads(GRAPH_CONFIG.read_text("utf-8"))
|
||||
)
|
||||
native_pins = {item.role: item for item in native.providers}
|
||||
legacy_pins = {item.role: item for item in legacy.providers}
|
||||
|
||||
assert native.graph_id == legacy.graph_id == "reference-perception-graph/v2"
|
||||
assert native.source_profile_id == legacy.source_profile_id
|
||||
assert native.queues == legacy.queues
|
||||
assert native.authority == legacy.authority
|
||||
assert (
|
||||
native_pins[ProviderRole.DETECTOR].provider_id
|
||||
== RF_DETR_NATIVE_SHADOW_PROVIDER_ID
|
||||
)
|
||||
assert all(
|
||||
native_pins[role] == legacy_pins[role]
|
||||
for role in ProviderRole
|
||||
if role is not ProviderRole.DETECTOR
|
||||
)
|
||||
|
||||
|
||||
def test_m48n_native_reference_graph_pins_every_profile_digest() -> None:
|
||||
config = ReferencePerceptionGraphConfigV2.from_dict(
|
||||
json.loads(NATIVE_GRAPH_CONFIG.read_text("utf-8"))
|
||||
)
|
||||
paths = {
|
||||
ProviderRole.SOURCE: "m4-recorded-realtime-baseline-v1.json",
|
||||
ProviderRole.DETECTOR: "rf-detr-large-native-kb4-risk-shadow-v0.json",
|
||||
ProviderRole.GEOMETRY: "m4-geometry-association-v1.json",
|
||||
ProviderRole.TEMPORAL: "m4-temporal-motion-v1.json",
|
||||
ProviderRole.MOTION: "m4-temporal-motion-v1.json",
|
||||
ProviderRole.ROLLING: "m4-rolling-local-map-v1.json",
|
||||
ProviderRole.THREAT: "m4-replay-threat-v3.json",
|
||||
}
|
||||
pins = {item.role: item for item in config.providers}
|
||||
|
||||
for role, name in paths.items():
|
||||
payload = (REPOSITORY_ROOT / "config/perception" / name).read_bytes()
|
||||
assert pins[role].sha256 == hashlib.sha256(payload).hexdigest()
|
||||
|
||||
|
||||
def test_m48s_advisory_policy_is_bounded_distinct_and_commandless() -> None:
|
||||
matrix = advisory_policy_matrix()
|
||||
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from k1link.perception.contracts import (
|
||||
ClockBasis,
|
||||
ModalityOutcome,
|
||||
ModalityStatus,
|
||||
SourceEnvelope,
|
||||
TimestampBundle,
|
||||
)
|
||||
from k1link.perception.detector import (
|
||||
RF_DETR_NATIVE_SHADOW_MODEL_ID,
|
||||
RF_DETR_NATIVE_SHADOW_PREPROCESS_ID,
|
||||
RF_DETR_NATIVE_SHADOW_PROVIDER_ID,
|
||||
DetectorFrameTiming,
|
||||
NativeRfDetrShadowDetectorProvider,
|
||||
)
|
||||
from k1link.perception.m48s_reference_graph_runtime import _validate_detector_profile
|
||||
from k1link.perception.providers import SourcePacket
|
||||
from k1link.perception.rf_detr_native_object_detector import (
|
||||
RF_DETR_NATIVE_CONFIG,
|
||||
RF_DETR_NATIVE_ENGINE_SHA256,
|
||||
NativeRfDetrConfig,
|
||||
NativeRfDetrDetectorError,
|
||||
TritonNativeRfDetrHttpInferenceBackend,
|
||||
postprocess_native_rf_detr,
|
||||
prepare_raw_kb4_rf_detr_native,
|
||||
)
|
||||
from k1link.perception.rf_detr_object_detector import RfDetrRawOutput
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _status() -> ModalityStatus:
|
||||
return ModalityStatus(True, ModalityOutcome.AVAILABLE, "test-available")
|
||||
|
||||
|
||||
def _packet(sequence: int, image: object) -> SourcePacket:
|
||||
return SourcePacket(
|
||||
envelope=SourceEnvelope(
|
||||
source_id="RAVNOVES00",
|
||||
session_id="20260720T065719Z_viewer_live",
|
||||
frame_id=f"frame-{sequence:06d}",
|
||||
sequence=sequence,
|
||||
timestamps=TimestampBundle(
|
||||
utc_ns=1_000 + sequence,
|
||||
monotonic_ns=2_000 + sequence,
|
||||
source_ns=3_000 + sequence,
|
||||
clock_basis=ClockBasis.RECORDED_HOST,
|
||||
),
|
||||
source_age_ns=0,
|
||||
binding_reason="test-recorded-source",
|
||||
calibration_id="camera-1-kb4-test",
|
||||
representation_id="registered-map-increment-v1",
|
||||
image=_status(),
|
||||
registered_point_increment=_status(),
|
||||
pose=_status(),
|
||||
),
|
||||
image_payload=image,
|
||||
registered_point_increment_payload=("points", sequence),
|
||||
pose_payload=("pose", sequence),
|
||||
)
|
||||
|
||||
|
||||
def _output() -> RfDetrRawOutput:
|
||||
boxes = np.zeros((1, 300, 4), dtype=np.float16)
|
||||
logits = np.full((1, 300, 91), -20.0, dtype=np.float16)
|
||||
boxes[0, 0] = (0.5, 0.5, 0.25, 0.25)
|
||||
logits[0, 0, 18] = np.float16(math.log(3.0)) # dog, score 0.75
|
||||
boxes[0, 1] = (0.25, 0.25, 0.1, 0.2)
|
||||
logits[0, 1, 1] = np.float16(math.log(4.0)) # person, score 0.80
|
||||
boxes[0, 2] = (0.75, 0.25, 0.1, 0.2)
|
||||
logits[0, 2, 62] = np.float16(math.log(9.0)) # chair, non-risk
|
||||
return RfDetrRawOutput(boxes=boxes, logits=logits)
|
||||
|
||||
|
||||
class _Backend:
|
||||
def __init__(self, output: RfDetrRawOutput) -> None:
|
||||
self.output = output
|
||||
self.calls = 0
|
||||
|
||||
def infer(self, tensor: NDArray[np.uint8]) -> RfDetrRawOutput:
|
||||
assert tensor.shape == (1, 600, 800, 3)
|
||||
assert tensor.dtype == np.uint8
|
||||
assert tensor.flags.c_contiguous
|
||||
self.calls += 1
|
||||
return self.output
|
||||
|
||||
|
||||
def test_native_prepare_preserves_every_raw_pixel_without_geometric_transform() -> None:
|
||||
raster = np.arange(600 * 800 * 3, dtype=np.uint8).reshape(600, 800, 3)
|
||||
non_contiguous = raster[:, ::-1]
|
||||
|
||||
tensor = prepare_raw_kb4_rf_detr_native(non_contiguous)
|
||||
|
||||
assert tensor.shape == (1, 600, 800, 3)
|
||||
assert tensor.dtype == np.uint8
|
||||
assert tensor.flags.c_contiguous
|
||||
assert tensor.nbytes == 1_440_000
|
||||
np.testing.assert_array_equal(tensor[0], non_contiguous)
|
||||
|
||||
|
||||
def test_native_postprocess_uses_608_model_canvas_then_clips_to_raw_raster() -> None:
|
||||
result = postprocess_native_rf_detr(
|
||||
_output(),
|
||||
np.ones((600, 800), dtype=np.bool_),
|
||||
)
|
||||
|
||||
assert tuple(item.label for item in result.detections) == ("person", "dog")
|
||||
assert result.detections[0].score == pytest.approx(0.8, abs=0.001)
|
||||
assert result.detections[1].score == pytest.approx(0.75, abs=0.001)
|
||||
assert result.detections[1].bbox_xyxy == pytest.approx(
|
||||
(300.0, 228.0, 500.0, 380.0),
|
||||
abs=0.03,
|
||||
)
|
||||
assert dict(result.rejected) == {"non-risk-class": 1}
|
||||
|
||||
|
||||
def test_native_shadow_provider_uses_one_raw_pass_and_preserves_identity() -> None:
|
||||
backend = _Backend(_output())
|
||||
provider = NativeRfDetrShadowDetectorProvider(
|
||||
mask=np.ones((600, 800), dtype=np.bool_),
|
||||
backend=backend,
|
||||
clock_ns=iter((10, 30)).__next__,
|
||||
)
|
||||
|
||||
proposals = provider.detect(_packet(7, np.zeros((600, 800, 3), dtype=np.uint8)))
|
||||
|
||||
assert backend.calls == 1
|
||||
assert tuple(item.semantic_hint for item in proposals) == ("person", "dog")
|
||||
assert all(item.provider_id == RF_DETR_NATIVE_SHADOW_PROVIDER_ID for item in proposals)
|
||||
assert all(item.model_id == RF_DETR_NATIVE_SHADOW_MODEL_ID for item in proposals)
|
||||
assert all(item.preprocess_id == RF_DETR_NATIVE_SHADOW_PREPROCESS_ID for item in proposals)
|
||||
assert provider.snapshot().completed_frames == 1
|
||||
assert provider.snapshot().proposal_count == 2
|
||||
assert provider.snapshot().core_duration_ns == 20
|
||||
|
||||
|
||||
def test_native_shadow_provider_reports_prepare_transport_and_postprocess_timing() -> None:
|
||||
observed: list[DetectorFrameTiming] = []
|
||||
provider = NativeRfDetrShadowDetectorProvider(
|
||||
mask=np.ones((600, 800), dtype=np.bool_),
|
||||
backend=_Backend(_output()),
|
||||
clock_ns=iter((10, 20, 50, 70)).__next__,
|
||||
timing_observer=observed.append,
|
||||
)
|
||||
|
||||
provider.detect(_packet(7, np.zeros((600, 800, 3), dtype=np.uint8)))
|
||||
|
||||
assert [item.to_dict() for item in observed] == [
|
||||
{
|
||||
"sequence": 7,
|
||||
"preprocess_duration_ns": 10,
|
||||
"inference_transport_duration_ns": 30,
|
||||
"postprocess_duration_ns": 20,
|
||||
"total_duration_ns": 60,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_native_shadow_warmup_is_idempotent_and_excluded_from_frame_counts() -> None:
|
||||
backend = _Backend(_output())
|
||||
provider = NativeRfDetrShadowDetectorProvider(
|
||||
mask=np.ones((600, 800), dtype=np.bool_),
|
||||
backend=backend,
|
||||
clock_ns=iter((10, 20, 50, 70)).__next__,
|
||||
)
|
||||
|
||||
first = provider.warm_up()
|
||||
second = provider.warm_up()
|
||||
|
||||
assert first is second
|
||||
assert backend.calls == 1
|
||||
assert first.total_duration_ns == 60
|
||||
assert provider.snapshot().input_frames == 0
|
||||
assert provider.snapshot().completed_frames == 0
|
||||
|
||||
|
||||
def test_native_profile_is_frozen_and_transport_pins_model_version() -> None:
|
||||
assert RF_DETR_NATIVE_CONFIG.minimum_score == 0.25
|
||||
with pytest.raises(NativeRfDetrDetectorError, match="cannot be tuned"):
|
||||
NativeRfDetrConfig(minimum_score=0.5)
|
||||
|
||||
backend = TritonNativeRfDetrHttpInferenceBackend("http://127.0.0.1:8100")
|
||||
try:
|
||||
assert backend.path == (
|
||||
"/v2/models/rf_detr_large_native_kb4/versions/1/infer"
|
||||
)
|
||||
finally:
|
||||
backend.close()
|
||||
with pytest.raises(NativeRfDetrDetectorError, match="explicit HTTP origin"):
|
||||
TritonNativeRfDetrHttpInferenceBackend("http://user:secret@127.0.0.1:8100")
|
||||
|
||||
|
||||
def test_native_engine_identity_is_pinned() -> None:
|
||||
assert RF_DETR_NATIVE_ENGINE_SHA256 == (
|
||||
"b8a40b3580edff001ec9680de68707242294ff590ab296000fae371f1083f695"
|
||||
)
|
||||
|
||||
|
||||
def test_native_shadow_profile_records_failed_legacy_agreement_without_authority() -> None:
|
||||
profile = json.loads(
|
||||
(
|
||||
REPOSITORY_ROOT
|
||||
/ "config/perception/rf-detr-large-native-kb4-risk-shadow-v0.json"
|
||||
).read_text("utf-8")
|
||||
)
|
||||
|
||||
assert profile["provider_id"] == RF_DETR_NATIVE_SHADOW_PROVIDER_ID
|
||||
assert profile["model"]["worker_006_rtx4090_tensorrt_11_engine_sha256"] == (
|
||||
RF_DETR_NATIVE_ENGINE_SHA256
|
||||
)
|
||||
assert profile["preprocessing"]["geometric_resampling"] is False
|
||||
assert profile["preprocessing"]["resize"] is False
|
||||
assert profile["qualification"]["native_pytorch_tensorrt_parity"]["passed"] is True
|
||||
assert (
|
||||
profile["qualification"]["full_ravnoves00_native_vs_legacy_704"]
|
||||
["legacy_box_agreement_gate_passed"]
|
||||
is False
|
||||
)
|
||||
assert profile["status"]["production_accepted"] is False
|
||||
assert not any(profile["authority"].values())
|
||||
assert (
|
||||
_validate_detector_profile(
|
||||
REPOSITORY_ROOT
|
||||
/ "config/perception/rf-detr-large-native-kb4-risk-shadow-v0.json"
|
||||
)
|
||||
== "native-kb4"
|
||||
)
|
||||
Reference in New Issue
Block a user