diff --git a/config/perception/lab-v1-vegetation-integrated-multirate-phased-shadow-v3.json b/config/perception/lab-v1-vegetation-integrated-multirate-phased-shadow-v3.json new file mode 100644 index 0000000..c1a4e2c --- /dev/null +++ b/config/perception/lab-v1-vegetation-integrated-multirate-phased-shadow-v3.json @@ -0,0 +1,82 @@ +{ + "schema_version": "missioncore.lab-v1-vegetation-integrated-shadow-profile/v3", + "profile_id": "lab-v1-ravnoves00-ddrnet-m49-integrated-multirate-phased-shadow/v3", + "source": { + "source_id": "RAVNOVES00", + "expected_timeline_frames": 4489, + "requested_source_rate_hz": 12.0, + "shared_start_barrier": true, + "ground_truth_available": false + }, + "stages": { + "m49_graph_tgs": { + "profile": "m49-tgs-integrated-graph-shadow-v1.json", + "profile_sha256": "b61e018b2d04eec58802e2d4186ce7a3dd3a15b254db106b57b609e903eeef80", + "candidate": "frozen-native-rf-detr-plus-cpu-tgs", + "parameters_unchanged": true, + "timeline_rate_hz": 12.0 + }, + "vegetation": { + "candidate_id": "ddrnet_39-goose-fine-64", + "candidate_key": "ddrnet", + "checkpoint_sha256": "b99c2838051bcd7b092fd3970aa62a77d5c0bbb809c9b9afb2ff4b0ebdaa4ee6", + "config_sha256": "96a427a8baae387b827ec9c0bf7ca42e3fb9114b8fa9a8671bbc9d10877670b9", + "policy_sha256": "b75c4ac841d7b4bcc57f7a9c8417ca2317d8ecfa499e72a9af8a8591a2ec0d35", + "provider_map_sha256": "f2b69046b6a740fd9532d2d88e7fabae7c20fb662f783c9502adc9026406f352", + "container_image": "ndc/mission-core-lab-v1-goose:sg3.2.0-cu117-v1", + "container_image_id": "sha256:591cb382c099eeb05e7ec16e2371e0b2da54d2bb5c49ec0f4ac88dbf72b0f0cd", + "timeline_rate_hz": 12.0, + "inference_rate_hz": 6.0, + "inference_stride": 2, + "inference_phase_offset_ms": 40.0, + "held_evidence_fail_closed": true, + "semantic_output_persisted": false, + "one_heavy_vegetation_candidate_at_a_time": true + } + }, + "acceptance": { + "minimum_graph_world_state_fps": 11.209069, + "minimum_vegetation_timeline_fps": 11.209069, + "minimum_vegetation_inference_fps": 5.604534, + "maximum_vegetation_inference_completion_p95_ms": 125.0, + "maximum_semantic_evidence_source_age_ms": 125.0, + "maximum_combined_output_age_p99_ms": 125.0, + "capacity_drop_count_max": 0, + "unaccounted_frame_count_max": 0 + }, + "telemetry": { + "sample_interval_seconds": 1.0, + "required_roles": [ + "graph", + "triton", + "tgs", + "vegetation" + ] + }, + "invariants": { + "raw_fisheye_immutable": true, + "reference_graph_parameters_unchanged": true, + "tgs_parameters_unchanged": true, + "ddrnet_parameters_unchanged": true, + "safety_layers_remain_12hz": true, + "vegetation_gpu_phase_follows_safety_detector": true, + "held_semantic_evidence_is_advisory_only": true, + "vegetation_source_buffer_bounded": true, + "vegetation_full_route_rgb_prefetch_allowed": false, + "ppliteseg_concurrent_run_allowed": false, + "camera_semantics_can_clear_rigid_geometry": false, + "canonical_triton_mutation_allowed": false, + "runtime_shared_source_frame_target": true, + "gauss_or_playcanvas_in_scope": false + }, + "authority": { + "visual_quality_accepted": false, + "route_truth_available": false, + "traversability_accepted": false, + "physical_free_space_accepted": false, + "commands_enabled": false, + "actuation_allowed": false, + "navigation_or_safety_accepted": false, + "production_accepted": false + } +} diff --git a/experiments/perception/worker/Invoke-M49TgsIntegratedGraphShadow.ps1 b/experiments/perception/worker/Invoke-M49TgsIntegratedGraphShadow.ps1 index 217c7c8..f2d6778 100644 --- a/experiments/perception/worker/Invoke-M49TgsIntegratedGraphShadow.ps1 +++ b/experiments/perception/worker/Invoke-M49TgsIntegratedGraphShadow.ps1 @@ -148,12 +148,12 @@ foreach ($directory in @("bin", "control", "graph", "tgs", "vegetation")) { $releaseDocument = Get-Content -LiteralPath (Join-Path $payload "release.json") -Raw | ConvertFrom-Json $expectedReleaseSchema = if ($VegetationLoadGate) { - "missioncore.lab-v1-vegetation-integrated-worker-release/v2" + "missioncore.lab-v1-vegetation-integrated-worker-release/v3" } else { "missioncore.m49-tgs-integrated-graph-worker-release/v1" } $expectedTransition = if ($VegetationLoadGate) { - "lab-v1-vegetation-m49-integrated-multirate-shadow/v2" + "lab-v1-vegetation-m49-integrated-multirate-phased-shadow/v3" } else { "m49-tgs-native-risk-integrated-shadow/v1" } @@ -413,6 +413,7 @@ try { --runtime-video-cache /tmp/vegetation-right.mp4 ` --source-rate-hz $rate ` --inference-stride 2 ` + --inference-phase-offset-ms 40.0 ` --minimum-effective-timeline-fps 11.209069 ` --minimum-effective-inference-fps 5.604534 ` --maximum-inference-completion-p95-ms 125.0 ` @@ -562,7 +563,7 @@ try { --volume ($dockerRelease + ":/release:ro") ` --volume ($dockerRun + ":/shared:rw") ` $ParityImageTag /release/build_vegetation_integrated_graph_evidence.py ` - --profile /release/lab-v1-vegetation-integrated-multirate-shadow-v2.json ` + --profile /release/lab-v1-vegetation-integrated-multirate-phased-shadow-v3.json ` --m49-result /shared/m49-result.json ` --graph-frames /shared/graph/frames.jsonl ` --tgs-timing /shared/tgs/tgs-full-timing.tsv ` @@ -591,7 +592,7 @@ if (-not (Test-Path -LiteralPath $resultPath -PathType Leaf)) { $result = Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json $summary = [ordered]@{ schema_version = if ($VegetationLoadGate) { - "missioncore.lab-v1-vegetation-integrated-worker-summary/v2" + "missioncore.lab-v1-vegetation-integrated-worker-summary/v3" } else { "missioncore.m49-tgs-integrated-graph-worker-summary/v1" } diff --git a/experiments/perception/worker/lab_v1_vegetation_goose/run_vegetation_integrated_load.py b/experiments/perception/worker/lab_v1_vegetation_goose/run_vegetation_integrated_load.py index b3541a0..c1d3413 100644 --- a/experiments/perception/worker/lab_v1_vegetation_goose/run_vegetation_integrated_load.py +++ b/experiments/perception/worker/lab_v1_vegetation_goose/run_vegetation_integrated_load.py @@ -28,7 +28,7 @@ from run_goose_vegetation_benchmark import ( validate_contracts, ) -SCHEMA = "missioncore.lab-v1-vegetation-integrated-load/v2" +SCHEMA = "missioncore.lab-v1-vegetation-integrated-load/v3" FRAME_SCHEMA = "missioncore.lab-v1-vegetation-integrated-frame/v2" FRAME_COUNT = 4_489 AUTHORITY = { @@ -58,6 +58,7 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--runtime-video-cache", type=Path, required=True) parser.add_argument("--source-rate-hz", type=float, required=True) parser.add_argument("--inference-stride", type=int, required=True) + parser.add_argument("--inference-phase-offset-ms", type=float, required=True) parser.add_argument("--minimum-effective-timeline-fps", type=float, required=True) parser.add_argument("--minimum-effective-inference-fps", type=float, required=True) parser.add_argument("--maximum-inference-completion-p95-ms", type=float, required=True) @@ -154,6 +155,12 @@ def run() -> int: not math.isfinite(value) or value <= 0 for value in positive_finite_values ): raise IntegratedLoadError("integrated-load thresholds must be positive and finite") + if ( + not math.isfinite(args.inference_phase_offset_ms) + or args.inference_phase_offset_ms < 0 + or args.inference_phase_offset_ms >= 1000.0 / args.source_rate_hz + ): + raise IntegratedLoadError("inference phase offset must fit inside one source interval") validate_sha256(args.release_sha256, "release") validate_sha256(args.video_sha256, "video") if args.output.exists() or args.frame_ledger.exists(): @@ -210,12 +217,15 @@ def run() -> int: with args.frame_ledger.open("x", encoding="utf-8") as ledger: for sequence in range(FRAME_COUNT): scheduled_ns = start_ns + round(sequence * interval_ns) - remaining_ns = scheduled_ns - time.monotonic_ns() + inference_executed = sequence % args.inference_stride == 0 + execution_target_ns = scheduled_ns + if inference_executed: + execution_target_ns += round(args.inference_phase_offset_ms * 1_000_000.0) + remaining_ns = execution_target_ns - time.monotonic_ns() if remaining_ns > 0: time.sleep(remaining_ns / 1_000_000_000.0) admitted_ns = time.monotonic_ns() source = decode_source(source_capture, expected_size) - inference_executed = sequence % args.inference_stride == 0 inference_ms: float | None = None if inference_executed: tensor, _ = preprocess(source) @@ -253,6 +263,9 @@ def run() -> int: "completion_age_ms": round(completion_age_ms, 6), "stage_ms": round(stage_ms, 6), "inference_executed": inference_executed, + "inference_phase_offset_ms": args.inference_phase_offset_ms + if inference_executed + else 0.0, "inference_ms": round(inference_ms, 6) if inference_ms is not None else None, "semantic_source_sequence": last_inference_sequence, "semantic_evidence_source_age_ms": round(evidence_source_age_ms, 6), @@ -276,6 +289,8 @@ def run() -> int: checks = { "all_frames_accounted": len(completion_ages_ms) == FRAME_COUNT, "exact_multirate_schedule": inference_frame_count == expected_inference_frames, + "inference_phase_offset_preserved": args.inference_phase_offset_ms + < 1000.0 / args.source_rate_hz, "minimum_effective_timeline_fps": effective_timeline_fps >= args.minimum_effective_timeline_fps, "minimum_effective_inference_fps": effective_inference_fps @@ -313,6 +328,7 @@ def run() -> int: "effective_timeline_fps": round(effective_timeline_fps, 6), "effective_inference_fps": round(effective_inference_fps, 6), "inference_stride": args.inference_stride, + "inference_phase_offset_ms": args.inference_phase_offset_ms, "inference_frame_count": inference_frame_count, "held_evidence_frame_count": FRAME_COUNT - inference_frame_count, "frame_count": FRAME_COUNT, @@ -360,6 +376,7 @@ def run() -> int: "predeclared_thresholds": { "minimum_effective_timeline_fps": args.minimum_effective_timeline_fps, "minimum_effective_inference_fps": args.minimum_effective_inference_fps, + "inference_phase_offset_ms": args.inference_phase_offset_ms, "maximum_inference_completion_p95_ms": ( args.maximum_inference_completion_p95_ms ), diff --git a/experiments/perception/worker/m49_t3_travel/build_vegetation_integrated_graph_evidence.py b/experiments/perception/worker/m49_t3_travel/build_vegetation_integrated_graph_evidence.py index bba1a26..c82675c 100644 --- a/experiments/perception/worker/m49_t3_travel/build_vegetation_integrated_graph_evidence.py +++ b/experiments/perception/worker/m49_t3_travel/build_vegetation_integrated_graph_evidence.py @@ -15,10 +15,10 @@ from typing import Any import numpy as np -PROFILE_SCHEMA = "missioncore.lab-v1-vegetation-integrated-shadow-profile/v2" +PROFILE_SCHEMA = "missioncore.lab-v1-vegetation-integrated-shadow-profile/v3" M49_SCHEMA = "missioncore.m49-tgs-integrated-graph-shadow-result/v1" -VEGETATION_SCHEMA = "missioncore.lab-v1-vegetation-integrated-load/v2" -RESULT_SCHEMA = "missioncore.lab-v1-vegetation-integrated-shadow-result/v2" +VEGETATION_SCHEMA = "missioncore.lab-v1-vegetation-integrated-load/v3" +RESULT_SCHEMA = "missioncore.lab-v1-vegetation-integrated-shadow-result/v3" FRAME_COUNT = 4_489 @@ -93,7 +93,11 @@ def tgs_completion_ages(path: Path) -> list[float]: def vegetation_frame_metrics( - path: Path, *, inference_stride: int, source_rate_hz: float + path: Path, + *, + inference_stride: int, + inference_phase_offset_ms: float, + source_rate_hz: float, ) -> dict[str, object]: completion_ages: list[float] = [] evidence_source_ages: list[float] = [] @@ -112,6 +116,12 @@ def vegetation_frame_metrics( expected_inference = expected % inference_stride == 0 if inference_executed is not expected_inference: raise VegetationIntegratedError("vegetation inference schedule changed") + expected_phase = inference_phase_offset_ms if expected_inference else 0.0 + phase = row.get("inference_phase_offset_ms") + if not isinstance(phase, (int, float)) or float(phase) != expected_phase: + raise VegetationIntegratedError("vegetation inference phase changed") + if expected_inference and float(age) + 0.001 < expected_phase: + raise VegetationIntegratedError("vegetation inference phase attribution changed") expected_source = expected - (expected % inference_stride) if row.get("semantic_source_sequence") != expected_source: raise VegetationIntegratedError("vegetation evidence source changed") @@ -231,11 +241,24 @@ def build( source_rate_hz = float(profile["source"]["requested_source_rate_hz"]) inference_stride = int(profile["stages"]["vegetation"]["inference_stride"]) + inference_phase_offset_ms = float( + profile["stages"]["vegetation"]["inference_phase_offset_ms"] + ) + if ( + not math.isfinite(source_rate_hz) + or source_rate_hz <= 0 + or inference_stride <= 0 + or not math.isfinite(inference_phase_offset_ms) + or inference_phase_offset_ms < 0 + or inference_phase_offset_ms >= 1000.0 / source_rate_hz + ): + raise VegetationIntegratedError("vegetation multirate schedule is invalid") graph_ages = graph_completion_ages(graph_frames_path) tgs_ages = tgs_completion_ages(tgs_timing_path) vegetation_frames = vegetation_frame_metrics( vegetation_frames_path, inference_stride=inference_stride, + inference_phase_offset_ms=inference_phase_offset_ms, source_rate_hz=source_rate_hz, ) vegetation_ages = vegetation_frames["completion_ages"] @@ -280,6 +303,8 @@ def build( "vegetation_load_gate_passed": vegetation.get("integrated_load_gate_passed") is True, "vegetation_multirate_schedule_frozen": ( vegetation_execution.get("inference_stride") == inference_stride + and vegetation_execution.get("inference_phase_offset_ms") + == inference_phase_offset_ms and vegetation_execution.get("inference_frame_count") == vegetation_frames["inference_count"] and vegetation_execution.get("held_evidence_frame_count") diff --git a/scripts/build_lab_v1_vegetation_integrated_worker_artifact.py b/scripts/build_lab_v1_vegetation_integrated_worker_artifact.py index e4e44bc..08da6b6 100644 --- a/scripts/build_lab_v1_vegetation_integrated_worker_artifact.py +++ b/scripts/build_lab_v1_vegetation_integrated_worker_artifact.py @@ -46,7 +46,7 @@ SOURCES = M49_SOURCES + ( Path("config/perception/lab-v1-goose-vegetation-benchmark-v1.json"), Path("config/perception/lab-v1-vegetation-mission-policy-v1.json"), Path("config/perception/lab-v1-vegetation-provider-label-map-v1.json"), - Path("config/perception/lab-v1-vegetation-integrated-multirate-shadow-v2.json"), + Path("config/perception/lab-v1-vegetation-integrated-multirate-phased-shadow-v3.json"), ) @@ -85,9 +85,9 @@ def build_artifact( wheel_destination.write_bytes(wheel.read_bytes()) copied.append(wheel_destination) release = { - "schema_version": "missioncore.lab-v1-vegetation-integrated-worker-release/v2", + "schema_version": "missioncore.lab-v1-vegetation-integrated-worker-release/v3", "patch_id": patch_id, - "transition": "lab-v1-vegetation-m49-integrated-multirate-shadow/v2", + "transition": "lab-v1-vegetation-m49-integrated-multirate-phased-shadow/v3", "code_revision": selected_revision, "worker_id": "worker-006", "source_pack_sha256": ( @@ -100,6 +100,7 @@ def build_artifact( "requested_source_rate_hz": 12.0, "semantic_inference_rate_hz": 6.0, "semantic_inference_stride": 2, + "semantic_inference_phase_offset_ms": 40.0, "native_engine_sha256": ( "b8a40b3580edff001ec9680de68707242294ff590ab296000fae371f1083f695" ), diff --git a/tests/test_lab_v1_vegetation_integrated_shadow.py b/tests/test_lab_v1_vegetation_integrated_shadow.py index 08888c8..c843432 100644 --- a/tests/test_lab_v1_vegetation_integrated_shadow.py +++ b/tests/test_lab_v1_vegetation_integrated_shadow.py @@ -55,6 +55,7 @@ def test_three_layer_gate_joins_exact_frames_and_preserves_false_authority( "policy_sha256": "d" * 64, "provider_map_sha256": "e" * 64, "inference_stride": 2, + "inference_phase_offset_ms": 40.0, }, }, "acceptance": { @@ -115,6 +116,7 @@ def test_three_layer_gate_joins_exact_frames_and_preserves_false_authority( "effective_timeline_fps": 11.75, "effective_inference_fps": 5.875, "inference_stride": 2, + "inference_phase_offset_ms": 40.0, "inference_frame_count": 2245, "held_evidence_frame_count": 2244, "capacity_drop_count": 0, @@ -160,10 +162,11 @@ def test_three_layer_gate_joins_exact_frames_and_preserves_false_authority( { "schema_version": "missioncore.lab-v1-vegetation-integrated-frame/v2", "sequence": index, - "completion_age_ms": 20.0, + "completion_age_ms": 60.0 if index % 2 == 0 else 20.0, "inference_executed": index % 2 == 0, + "inference_phase_offset_ms": 40.0 if index % 2 == 0 else 0.0, "semantic_source_sequence": index - (index % 2), - "semantic_evidence_source_age_ms": 20.0 + "semantic_evidence_source_age_ms": 60.0 if index % 2 == 0 else 103.333333, } @@ -205,7 +208,7 @@ def test_three_layer_gate_joins_exact_frames_and_preserves_false_authority( assert result["status"] == "passed" assert result["source"]["joined_frame_count"] == EVIDENCE.FRAME_COUNT - assert result["performance"]["three_layer_output_age_ms"]["p99"] == 40.0 + assert result["performance"]["three_layer_output_age_ms"]["p99"] == 60.0 assert result["checks"]["authority_remains_false"] is True assert result["production_accepted"] is False @@ -242,8 +245,12 @@ def test_integrated_release_is_deterministic_and_contains_one_vegetation_candida release = json.loads(release_stream.read()) assert "payload/run_vegetation_integrated_load.py" in names assert "payload/build_vegetation_integrated_graph_evidence.py" in names - assert "payload/lab-v1-vegetation-integrated-multirate-shadow-v2.json" in names + assert ( + "payload/lab-v1-vegetation-integrated-multirate-phased-shadow-v3.json" + in names + ) assert release["semantic_inference_rate_hz"] == 6.0 + assert release["semantic_inference_phase_offset_ms"] == 40.0 assert release["scope"]["heavy_vegetation_candidates"] == ["ddrnet"] assert all(value is False for value in release["authority"].values()) @@ -261,6 +268,7 @@ def test_worker_gate_reuses_shared_barrier_and_keeps_canonical_triton_unchanged( assert '"camera_semantics_can_clear_rigid_geometry": False' in runner assert "--runtime-video-cache /tmp/vegetation-right.mp4" in wrapper assert "--inference-stride 2" in wrapper + assert "--inference-phase-offset-ms 40.0" in wrapper assert '--tmpfs "/tmp:rw,noexec,nosuid,size=2g"' in wrapper assert "$VegetationLoadGate" in wrapper assert '"vegetation"' in wrapper