feat(perception): trace M4.8S pipeline latency
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.perception.detector import DetectorFrameTiming
|
||||
from k1link.perception.recorded_source import DecodedFrameTiming
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||
RUNNER_PATH = (
|
||||
REPOSITORY_ROOT / "experiments/perception/run_m48s_reference_graph_shadow_worker.py"
|
||||
)
|
||||
SPEC = importlib.util.spec_from_file_location("m48s_timed_shadow_runner", RUNNER_PATH)
|
||||
assert SPEC is not None and SPEC.loader is not None
|
||||
RUNNER = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(RUNNER)
|
||||
|
||||
|
||||
def test_frame_timing_store_closes_provider_and_unattributed_time() -> None:
|
||||
store = RUNNER.FrameTimingStore()
|
||||
store.observe_decode(DecodedFrameTiming(sequence=7, duration_ns=5))
|
||||
store.observe_detector(
|
||||
DetectorFrameTiming(
|
||||
sequence=7,
|
||||
preprocess_duration_ns=10,
|
||||
inference_transport_duration_ns=20,
|
||||
postprocess_duration_ns=30,
|
||||
total_duration_ns=60,
|
||||
)
|
||||
)
|
||||
for stage_id, duration_ns in (
|
||||
("geometry", 1),
|
||||
("temporal", 2),
|
||||
("motion", 3),
|
||||
("rolling", 4),
|
||||
("threat", 5),
|
||||
):
|
||||
store.observe_provider(stage_id, 7, duration_ns)
|
||||
|
||||
document = store.take(sequence=7, source_age_ns=100, completion_age_ns=300)
|
||||
|
||||
assert document["schema_version"] == RUNNER.PIPELINE_TIMING_SCHEMA
|
||||
assert document["graph_admission_to_delivery_ns"] == 200
|
||||
assert document["graph_attributed_provider_ns"] == 75
|
||||
assert document["graph_unattributed_ns"] == 125
|
||||
assert document["decode_to_delivery_processing_ns"] == 205
|
||||
assert store.delivered() == (document,)
|
||||
|
||||
|
||||
def test_pipeline_timing_metrics_preserve_single_pass_stage_breakdown() -> None:
|
||||
store = RUNNER.FrameTimingStore()
|
||||
store.observe_decode(DecodedFrameTiming(sequence=3, duration_ns=1_000_000))
|
||||
store.observe_detector(
|
||||
DetectorFrameTiming(
|
||||
sequence=3,
|
||||
preprocess_duration_ns=2_000_000,
|
||||
inference_transport_duration_ns=3_000_000,
|
||||
postprocess_duration_ns=4_000_000,
|
||||
total_duration_ns=9_000_000,
|
||||
)
|
||||
)
|
||||
for stage_id, duration_ns in (
|
||||
("geometry", 1_000_000),
|
||||
("temporal", 2_000_000),
|
||||
("motion", 3_000_000),
|
||||
("rolling", 4_000_000),
|
||||
("threat", 5_000_000),
|
||||
):
|
||||
store.observe_provider(stage_id, 3, duration_ns)
|
||||
document = store.take(
|
||||
sequence=3,
|
||||
source_age_ns=0,
|
||||
completion_age_ns=30_000_000,
|
||||
)
|
||||
|
||||
metrics = RUNNER._pipeline_timing_metrics([document])
|
||||
|
||||
assert metrics["sample_count"] == 1
|
||||
assert metrics["detector_ms"]["inference_transport"]["maximum"] == 3.0
|
||||
assert metrics["provider_ms"]["threat"]["maximum"] == 5.0
|
||||
assert metrics["graph_unattributed_ms"]["maximum"] == 6.0
|
||||
assert metrics["maximum_graph_sequence"] == 3
|
||||
assert metrics["additional_inference_passes"] == 0
|
||||
@@ -60,6 +60,7 @@ from k1link.perception.providers import (
|
||||
SourcePacket,
|
||||
)
|
||||
from k1link.perception.recorded_source import (
|
||||
DecodedFrameTiming,
|
||||
DecodedRecordedSource,
|
||||
RecordedRavnoves00Source,
|
||||
RecordedSourceError,
|
||||
@@ -869,6 +870,34 @@ def test_decoded_recorded_source_attaches_images_without_detector_logic(tmp_path
|
||||
assert int(packets[1].image_payload[0, 0, 0]) == 7
|
||||
|
||||
|
||||
def test_decoded_recorded_source_reports_per_frame_decode_timing(tmp_path: Path) -> None:
|
||||
camera_path, timeline_path = _write_recorded_fixture(tmp_path)
|
||||
source = RecordedRavnoves00Source(
|
||||
camera_index_path=camera_path,
|
||||
source_pack_path=timeline_path,
|
||||
expected_frame_count=2,
|
||||
expected_source_pack_sha256=None,
|
||||
)
|
||||
observed: list[DecodedFrameTiming] = []
|
||||
|
||||
class Decoder:
|
||||
def frames(self, _stop_event: Event) -> Iterator[np.ndarray]:
|
||||
yield np.zeros((600, 800, 3), dtype=np.uint8)
|
||||
yield np.zeros((600, 800, 3), dtype=np.uint8)
|
||||
|
||||
packets = list(
|
||||
DecodedRecordedSource(
|
||||
source=source,
|
||||
decoder=Decoder(),
|
||||
timing_observer=observed.append,
|
||||
clock_ns=iter((10, 30, 40, 90, 100)).__next__,
|
||||
).packets(Event())
|
||||
)
|
||||
|
||||
assert len(packets) == 2
|
||||
assert observed == [DecodedFrameTiming(0, 20), DecodedFrameTiming(1, 50)]
|
||||
|
||||
|
||||
def test_decoded_recorded_source_primes_decoder_before_source_clock() -> None:
|
||||
events: list[str] = []
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from k1link.perception.detector import (
|
||||
RF_DETR_SHADOW_MODEL_ID,
|
||||
RF_DETR_SHADOW_PREPROCESS_ID,
|
||||
RF_DETR_SHADOW_PROVIDER_ID,
|
||||
DetectorFrameTiming,
|
||||
RfDetrShadowDetectorProvider,
|
||||
)
|
||||
from k1link.perception.providers import SourcePacket
|
||||
@@ -170,6 +171,28 @@ def test_shadow_provider_uses_one_pass_and_preserves_semantic_hints() -> None:
|
||||
assert provider.snapshot().core_duration_ns == 20
|
||||
|
||||
|
||||
def test_shadow_provider_reports_preprocess_transport_and_postprocess_timing() -> None:
|
||||
observed: list[DetectorFrameTiming] = []
|
||||
provider = RfDetrShadowDetectorProvider(
|
||||
mask=np.ones((600, 800), dtype=np.bool_),
|
||||
backend=_Backend(_output()),
|
||||
resizer=_Resizer(),
|
||||
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 len(observed) == 1
|
||||
assert observed[0].to_dict() == {
|
||||
"sequence": 7,
|
||||
"preprocess_duration_ns": 10,
|
||||
"inference_transport_duration_ns": 30,
|
||||
"postprocess_duration_ns": 20,
|
||||
"total_duration_ns": 60,
|
||||
}
|
||||
|
||||
|
||||
def test_shadow_profile_is_fixed_and_transport_pins_model_version() -> None:
|
||||
assert RF_DETR_CONFIG.minimum_score == 0.25
|
||||
with pytest.raises(RfDetrDetectorError, match="cannot be tuned"):
|
||||
|
||||
Reference in New Issue
Block a user