|
|
|
@@ -0,0 +1,777 @@
|
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Run PointPillars sequentially over the sealed RAVNOVES00 K1 replay."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
import hashlib
|
|
|
|
|
import json
|
|
|
|
|
import math
|
|
|
|
|
import os
|
|
|
|
|
import tempfile
|
|
|
|
|
import time
|
|
|
|
|
import urllib.request
|
|
|
|
|
from datetime import UTC, datetime
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
|
|
|
|
|
from k1link.compute.l31_pointpillars_ravnoves import (
|
|
|
|
|
nearest_pose_indices,
|
|
|
|
|
select_visual_frame_indices,
|
|
|
|
|
sensor_frame_xyzi,
|
|
|
|
|
)
|
|
|
|
|
from k1link.compute.pointpillars_postprocess import (
|
|
|
|
|
POINTPILLARS_EMBEDDED_SCORE_THRESHOLD,
|
|
|
|
|
POINTPILLARS_MODEL_POINT_CLOUD_RANGE,
|
|
|
|
|
PointPillarsBox,
|
|
|
|
|
decode_pointpillars_output,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
RUN_SCHEMA = "missioncore.l31-pointpillars-ravnoves/v1"
|
|
|
|
|
FRAME_SCHEMA = "missioncore.l31-pointpillars-ravnoves-frame/v1"
|
|
|
|
|
CATALOG_SCHEMA = "missioncore.l31-pointpillars-ravnoves-catalog/v1"
|
|
|
|
|
VISUAL_FRAME_SCHEMA = "missioncore.l31-pointpillars-ravnoves-visual-frame/v1"
|
|
|
|
|
WORKER_PACKAGE_SCHEMA = "missioncore.l3-pointpillars-worker-package/v1"
|
|
|
|
|
REPLAY_SCHEMA = "missioncore.lidar-replay-pack/v2"
|
|
|
|
|
EXPECTED_PACK_ID = (
|
|
|
|
|
"lidar-replay-pack-"
|
|
|
|
|
"8fc0fb418578b8ee2ac88d502d2acbc63ae533437a9da14f1a9f9d8916f613ce"
|
|
|
|
|
)
|
|
|
|
|
EXPECTED_REPLAY_SHA256 = (
|
|
|
|
|
"cbb75341ca0d82aea6e59bec26636d06aa58d9c300e6fb5555033ab35b2bed7e"
|
|
|
|
|
)
|
|
|
|
|
EXPECTED_SESSION_ID = "20260720T065719Z_viewer_live"
|
|
|
|
|
EXPECTED_MODEL_SHA256 = (
|
|
|
|
|
"2dcabddc3a365e9608a112d7bbbb7db769a6dddeeaa59aa03611a83113326da1"
|
|
|
|
|
)
|
|
|
|
|
EXPECTED_ENGINE_SHA256 = (
|
|
|
|
|
"12005d972a4632d56342a5da44442b632c1dcc5144fa3c70b162dec334532481"
|
|
|
|
|
)
|
|
|
|
|
MODEL_NAME = "pointpillars"
|
|
|
|
|
MAXIMUM_POINTS = 204_800
|
|
|
|
|
MAXIMUM_VISUAL_BOXES = 512
|
|
|
|
|
MAXIMUM_VISUAL_FRAMES = 18
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> int:
|
|
|
|
|
parser = argparse.ArgumentParser()
|
|
|
|
|
parser.add_argument("--replay-pack", type=Path, required=True)
|
|
|
|
|
parser.add_argument("--worker-package", type=Path, required=True)
|
|
|
|
|
parser.add_argument("--triton-url", default="http://127.0.0.1:8000")
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--output-root",
|
|
|
|
|
type=Path,
|
|
|
|
|
default=Path(
|
|
|
|
|
"/mnt/d/NDC_MISSIONCORE/runtime/experiments/l3/"
|
|
|
|
|
"pointpillars-ravnoves"
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
|
|
replay_root, replay_manifest, arrays = _read_replay(args.replay_pack)
|
|
|
|
|
package = _read_worker_package(args.worker_package)
|
|
|
|
|
_require_triton_ready(args.triton_url)
|
|
|
|
|
point_times = arrays["point_received_monotonic_ns"]
|
|
|
|
|
pose_indices, pose_age_ms = nearest_pose_indices(
|
|
|
|
|
point_times,
|
|
|
|
|
arrays["pose_received_monotonic_ns"],
|
|
|
|
|
)
|
|
|
|
|
if float(np.max(pose_age_ms)) > 100.0:
|
|
|
|
|
raise RuntimeError("RAVNOVES00 contains a pose binding older than 100 ms")
|
|
|
|
|
|
|
|
|
|
identity = {
|
|
|
|
|
"schema_version": RUN_SCHEMA,
|
|
|
|
|
"source_pack_id": replay_manifest["pack_id"],
|
|
|
|
|
"source_pack_identity_sha256": replay_manifest["identity_sha256"],
|
|
|
|
|
"source_logical_content_sha256": replay_manifest["identity"][
|
|
|
|
|
"logical_content_sha256"
|
|
|
|
|
],
|
|
|
|
|
"source_session_id": EXPECTED_SESSION_ID,
|
|
|
|
|
"point_frame_count": int(point_times.size),
|
|
|
|
|
"model": {
|
|
|
|
|
"name": MODEL_NAME,
|
|
|
|
|
"source_model_sha256": EXPECTED_MODEL_SHA256,
|
|
|
|
|
"engine_sha256": EXPECTED_ENGINE_SHA256,
|
|
|
|
|
"embedded_score_threshold": POINTPILLARS_EMBEDDED_SCORE_THRESHOLD,
|
|
|
|
|
"point_cloud_range": list(POINTPILLARS_MODEL_POINT_CLOUD_RANGE),
|
|
|
|
|
},
|
|
|
|
|
"worker_package_id": package["package_id"],
|
|
|
|
|
"worker_package_identity_sha256": package["identity_sha256"],
|
|
|
|
|
"producer_sha256": _sha256(Path(__file__)),
|
|
|
|
|
"execution": {
|
|
|
|
|
"worker_host_id": "worker-006",
|
|
|
|
|
"sequential": True,
|
|
|
|
|
"parallel_workers": 1,
|
|
|
|
|
"source_paced": False,
|
|
|
|
|
"existing_triton_only": True,
|
|
|
|
|
},
|
|
|
|
|
"authority": {
|
|
|
|
|
"shadow_only": True,
|
|
|
|
|
"commands_enabled": False,
|
|
|
|
|
"navigation_or_safety_accepted": False,
|
|
|
|
|
"accuracy_accepted": False,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
|
|
|
|
result_id = f"l31-pointpillars-ravnoves-{identity_sha256}"
|
|
|
|
|
result_root = args.output_root.expanduser().absolute() / result_id
|
|
|
|
|
summaries_root = result_root / "frame-results"
|
|
|
|
|
visual_root = result_root / "visual-frames"
|
|
|
|
|
summaries_root.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
|
|
|
visual_root.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
|
|
|
_write_once(result_root / "identity.json", identity)
|
|
|
|
|
|
|
|
|
|
point_offsets = arrays["point_offsets"]
|
|
|
|
|
first_time_ns = int(point_times[0])
|
|
|
|
|
summaries: list[dict[str, Any]] = []
|
|
|
|
|
for completed, frame_index in enumerate(range(point_times.size), start=1):
|
|
|
|
|
xyzi = _frame_xyzi(arrays, point_offsets, pose_indices, frame_index)
|
|
|
|
|
input_sha256 = hashlib.sha256(
|
|
|
|
|
np.ascontiguousarray(xyzi, dtype=np.float32).tobytes()
|
|
|
|
|
).hexdigest()
|
|
|
|
|
frame_path = summaries_root / f"{frame_index:06d}.json"
|
|
|
|
|
if frame_path.exists():
|
|
|
|
|
summary = _read_frame_summary(
|
|
|
|
|
frame_path,
|
|
|
|
|
frame_index=frame_index,
|
|
|
|
|
input_sha256=input_sha256,
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
boxes, elapsed_ms = _run_frame(args.triton_url, xyzi)
|
|
|
|
|
summary = _frame_summary(
|
|
|
|
|
frame_index=frame_index,
|
|
|
|
|
session_seconds=(int(point_times[frame_index]) - first_time_ns)
|
|
|
|
|
/ 1_000_000_000.0,
|
|
|
|
|
source_point_count=int(xyzi.shape[0]),
|
|
|
|
|
pose_index=int(pose_indices[frame_index]),
|
|
|
|
|
pose_binding_age_ms=float(pose_age_ms[frame_index]),
|
|
|
|
|
input_sha256=input_sha256,
|
|
|
|
|
inference_ms=elapsed_ms,
|
|
|
|
|
boxes=boxes,
|
|
|
|
|
)
|
|
|
|
|
_atomic_json(frame_path, summary)
|
|
|
|
|
summaries.append(summary)
|
|
|
|
|
if completed == 1 or completed % 100 == 0:
|
|
|
|
|
print(
|
|
|
|
|
json.dumps(
|
|
|
|
|
{
|
|
|
|
|
"result_id": result_id,
|
|
|
|
|
"completed": completed,
|
|
|
|
|
"total": int(point_times.size),
|
|
|
|
|
"frame_index": frame_index,
|
|
|
|
|
},
|
|
|
|
|
sort_keys=True,
|
|
|
|
|
),
|
|
|
|
|
flush=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
selected_indices = select_visual_frame_indices(
|
|
|
|
|
[int(row["class_counts"]["Vehicle"]) for row in summaries],
|
|
|
|
|
[int(row["prediction_count"]) for row in summaries],
|
|
|
|
|
maximum_frames=MAXIMUM_VISUAL_FRAMES,
|
|
|
|
|
)
|
|
|
|
|
visual_descriptors: list[dict[str, Any]] = []
|
|
|
|
|
deterministic_matches = 0
|
|
|
|
|
for frame_index in selected_indices:
|
|
|
|
|
xyzi = _frame_xyzi(arrays, point_offsets, pose_indices, frame_index)
|
|
|
|
|
boxes, elapsed_ms = _run_frame(args.triton_url, xyzi)
|
|
|
|
|
observed_sha256 = _boxes_sha256(boxes)
|
|
|
|
|
if observed_sha256 == summaries[frame_index]["boxes_sha256"]:
|
|
|
|
|
deterministic_matches += 1
|
|
|
|
|
visual_payload = _visual_frame(
|
|
|
|
|
summary=summaries[frame_index],
|
|
|
|
|
xyzi=xyzi,
|
|
|
|
|
boxes=boxes,
|
|
|
|
|
replay_inference_ms=elapsed_ms,
|
|
|
|
|
deterministic_replay=(
|
|
|
|
|
observed_sha256 == summaries[frame_index]["boxes_sha256"]
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
visual_path = visual_root / f"{frame_index:06d}.json"
|
|
|
|
|
_atomic_json(visual_path, visual_payload)
|
|
|
|
|
descriptor = _artifact(
|
|
|
|
|
visual_path,
|
|
|
|
|
"visual-frame",
|
|
|
|
|
relative_to=result_root,
|
|
|
|
|
)
|
|
|
|
|
visual_descriptors.append(
|
|
|
|
|
{
|
|
|
|
|
"frame_id": f"{frame_index:06d}",
|
|
|
|
|
"frame_index": frame_index,
|
|
|
|
|
"session_seconds": summaries[frame_index]["session_seconds"],
|
|
|
|
|
"source_point_count": summaries[frame_index]["source_point_count"],
|
|
|
|
|
"prediction_count": summaries[frame_index]["prediction_count"],
|
|
|
|
|
"class_counts": summaries[frame_index]["class_counts"],
|
|
|
|
|
"inference_ms": summaries[frame_index]["inference_ms"],
|
|
|
|
|
"detail_path": descriptor["path"],
|
|
|
|
|
"detail_sha256": descriptor["sha256"],
|
|
|
|
|
"detail_byte_length": descriptor["byte_length"],
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
metrics = _metrics(
|
|
|
|
|
summaries,
|
|
|
|
|
deterministic_matches=deterministic_matches,
|
|
|
|
|
deterministic_total=len(selected_indices),
|
|
|
|
|
)
|
|
|
|
|
catalog = {
|
|
|
|
|
"schema_version": CATALOG_SCHEMA,
|
|
|
|
|
"result_id": result_id,
|
|
|
|
|
"source_session_id": EXPECTED_SESSION_ID,
|
|
|
|
|
"selection": {
|
|
|
|
|
"strategy": (
|
|
|
|
|
"18 equal route-time bins; prefer Vehicle count, then total "
|
|
|
|
|
"prediction count, then bin-centre proximity"
|
|
|
|
|
),
|
|
|
|
|
"maximum_frames": MAXIMUM_VISUAL_FRAMES,
|
|
|
|
|
"maximum_boxes_per_frame": MAXIMUM_VISUAL_BOXES,
|
|
|
|
|
},
|
|
|
|
|
"frame_count": len(visual_descriptors),
|
|
|
|
|
"frames": visual_descriptors,
|
|
|
|
|
}
|
|
|
|
|
_atomic_json(result_root / "catalog.json", catalog)
|
|
|
|
|
manifest = {
|
|
|
|
|
"schema_version": RUN_SCHEMA,
|
|
|
|
|
"result_id": result_id,
|
|
|
|
|
"identity_sha256": identity_sha256,
|
|
|
|
|
"identity": identity,
|
|
|
|
|
"created_at_utc": datetime.now(UTC)
|
|
|
|
|
.isoformat(timespec="milliseconds")
|
|
|
|
|
.replace("+00:00", "Z"),
|
|
|
|
|
"status": "k1-cross-domain-transfer-measured-visual-review-required",
|
|
|
|
|
"metrics": metrics,
|
|
|
|
|
"catalog": _artifact(
|
|
|
|
|
result_root / "catalog.json",
|
|
|
|
|
"visual-frame-catalog",
|
|
|
|
|
relative_to=result_root,
|
|
|
|
|
),
|
|
|
|
|
"frame_results": {
|
|
|
|
|
"count": len(summaries),
|
|
|
|
|
"identity_sha256": _directory_identity(summaries_root),
|
|
|
|
|
},
|
|
|
|
|
"limitations": [
|
|
|
|
|
"RAVNOVES00 has no independent oriented 3D cuboid ground truth.",
|
|
|
|
|
"Predicted boxes are model hypotheses, not TP/FP/FN or accepted objects.",
|
|
|
|
|
"The K1 source is a vendor map increment without ring, firing time, "
|
|
|
|
|
"scan geometry, or IMU.",
|
|
|
|
|
"Execution is offline sequential and does not establish source-paced "
|
|
|
|
|
"drop or deadline behaviour.",
|
|
|
|
|
"The model was trained on another proprietary solid-state LiDAR domain.",
|
|
|
|
|
],
|
|
|
|
|
"authority": identity["authority"],
|
|
|
|
|
}
|
|
|
|
|
_atomic_json(result_root / "manifest.json", manifest)
|
|
|
|
|
print(
|
|
|
|
|
json.dumps(
|
|
|
|
|
{
|
|
|
|
|
"result_id": result_id,
|
|
|
|
|
"frame_count": len(summaries),
|
|
|
|
|
"frames_with_predictions": metrics["frames_with_predictions"],
|
|
|
|
|
"vehicle_predictions": metrics["class_counts"]["Vehicle"],
|
|
|
|
|
"inference_p95_ms": metrics["inference_latency_ms"]["p95"],
|
|
|
|
|
"deterministic_replay_fraction": metrics[
|
|
|
|
|
"deterministic_replay_fraction"
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
sort_keys=True,
|
|
|
|
|
),
|
|
|
|
|
flush=True,
|
|
|
|
|
)
|
|
|
|
|
del replay_root
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _read_replay(
|
|
|
|
|
path: Path,
|
|
|
|
|
) -> tuple[Path, dict[str, Any], dict[str, np.ndarray]]:
|
|
|
|
|
root = path.expanduser().resolve(strict=True)
|
|
|
|
|
manifest = _read_json(root / "manifest.json")
|
|
|
|
|
identity = manifest.get("identity")
|
|
|
|
|
artifacts = manifest.get("artifacts")
|
|
|
|
|
if (
|
|
|
|
|
not root.is_dir()
|
|
|
|
|
or root.is_symlink()
|
|
|
|
|
or root.name != EXPECTED_PACK_ID
|
|
|
|
|
or manifest.get("schema_version") != REPLAY_SCHEMA
|
|
|
|
|
or manifest.get("pack_id") != EXPECTED_PACK_ID
|
|
|
|
|
or manifest.get("identity_sha256") != EXPECTED_PACK_ID.removeprefix(
|
|
|
|
|
"lidar-replay-pack-"
|
|
|
|
|
)
|
|
|
|
|
or not isinstance(identity, dict)
|
|
|
|
|
or identity.get("session_id") != EXPECTED_SESSION_ID
|
|
|
|
|
or identity.get("point_frame_count") != 4570
|
|
|
|
|
or identity.get("pose_frame_count") != 4598
|
|
|
|
|
or not isinstance(artifacts, list)
|
|
|
|
|
):
|
|
|
|
|
raise RuntimeError("RAVNOVES00 replay identity is invalid")
|
|
|
|
|
descriptor = next(
|
|
|
|
|
(
|
|
|
|
|
row
|
|
|
|
|
for row in artifacts
|
|
|
|
|
if isinstance(row, dict) and row.get("path") == "lidar-replay.npz"
|
|
|
|
|
),
|
|
|
|
|
None,
|
|
|
|
|
)
|
|
|
|
|
replay_path = root / "lidar-replay.npz"
|
|
|
|
|
if (
|
|
|
|
|
not isinstance(descriptor, dict)
|
|
|
|
|
or descriptor.get("sha256") != EXPECTED_REPLAY_SHA256
|
|
|
|
|
or descriptor.get("byte_length") != replay_path.stat().st_size
|
|
|
|
|
or _sha256(replay_path) != EXPECTED_REPLAY_SHA256
|
|
|
|
|
):
|
|
|
|
|
raise RuntimeError("RAVNOVES00 replay artifact changed")
|
|
|
|
|
expected = {
|
|
|
|
|
"point_offsets",
|
|
|
|
|
"point_xyz_map",
|
|
|
|
|
"point_intensity",
|
|
|
|
|
"point_received_monotonic_ns",
|
|
|
|
|
"pose_positions_map",
|
|
|
|
|
"pose_quaternions_map_from_lidar",
|
|
|
|
|
"pose_received_monotonic_ns",
|
|
|
|
|
}
|
|
|
|
|
with np.load(replay_path, allow_pickle=False) as archive:
|
|
|
|
|
if not expected.issubset(archive.files):
|
|
|
|
|
raise RuntimeError("RAVNOVES00 replay arrays changed")
|
|
|
|
|
# Load each required member once. Reopening a compressed NPZ member for
|
|
|
|
|
# every frame would repeatedly inflate the complete 10.7M-point array.
|
|
|
|
|
arrays = {name: archive[name] for name in expected}
|
|
|
|
|
if (
|
|
|
|
|
arrays["point_offsets"].shape != (4571,)
|
|
|
|
|
or arrays["point_offsets"].dtype != np.int64
|
|
|
|
|
or arrays["point_xyz_map"].shape != (10_751_258, 3)
|
|
|
|
|
or arrays["point_xyz_map"].dtype != np.float64
|
|
|
|
|
or arrays["point_intensity"].shape != (10_751_258,)
|
|
|
|
|
or arrays["point_intensity"].dtype != np.uint8
|
|
|
|
|
or arrays["point_received_monotonic_ns"].shape != (4570,)
|
|
|
|
|
or arrays["point_received_monotonic_ns"].dtype != np.int64
|
|
|
|
|
or arrays["pose_positions_map"].shape != (4598, 3)
|
|
|
|
|
or arrays["pose_positions_map"].dtype != np.float64
|
|
|
|
|
or arrays["pose_quaternions_map_from_lidar"].shape != (4598, 4)
|
|
|
|
|
or arrays["pose_quaternions_map_from_lidar"].dtype != np.float64
|
|
|
|
|
or arrays["pose_received_monotonic_ns"].shape != (4598,)
|
|
|
|
|
or arrays["pose_received_monotonic_ns"].dtype != np.int64
|
|
|
|
|
or int(arrays["point_offsets"][0]) != 0
|
|
|
|
|
or int(arrays["point_offsets"][-1]) != 10_751_258
|
|
|
|
|
or np.any(np.diff(arrays["point_offsets"]) <= 0)
|
|
|
|
|
):
|
|
|
|
|
raise RuntimeError("RAVNOVES00 replay array contract changed")
|
|
|
|
|
return root, manifest, arrays
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _frame_xyzi(
|
|
|
|
|
arrays: dict[str, np.ndarray],
|
|
|
|
|
offsets: np.ndarray,
|
|
|
|
|
pose_indices: np.ndarray,
|
|
|
|
|
frame_index: int,
|
|
|
|
|
) -> np.ndarray:
|
|
|
|
|
start = int(offsets[frame_index])
|
|
|
|
|
stop = int(offsets[frame_index + 1])
|
|
|
|
|
pose_index = int(pose_indices[frame_index])
|
|
|
|
|
xyzi = sensor_frame_xyzi(
|
|
|
|
|
arrays["point_xyz_map"][start:stop],
|
|
|
|
|
arrays["point_intensity"][start:stop],
|
|
|
|
|
position_map_xyz=arrays["pose_positions_map"][pose_index],
|
|
|
|
|
orientation_map_from_lidar_xyzw=arrays[
|
|
|
|
|
"pose_quaternions_map_from_lidar"
|
|
|
|
|
][pose_index],
|
|
|
|
|
)
|
|
|
|
|
if not 1 <= xyzi.shape[0] <= MAXIMUM_POINTS:
|
|
|
|
|
raise RuntimeError("RAVNOVES00 frame violates PointPillars input bounds")
|
|
|
|
|
return xyzi
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _run_frame(
|
|
|
|
|
triton_url: str,
|
|
|
|
|
native: np.ndarray,
|
|
|
|
|
) -> tuple[tuple[PointPillarsBox, ...], float]:
|
|
|
|
|
points = np.zeros((1, MAXIMUM_POINTS, 4), dtype=np.float32)
|
|
|
|
|
points[0, : native.shape[0]] = native
|
|
|
|
|
num_points = np.asarray([native.shape[0]], dtype=np.int32)
|
|
|
|
|
output_boxes, output_count, elapsed_ms = _infer(
|
|
|
|
|
triton_url,
|
|
|
|
|
points,
|
|
|
|
|
num_points,
|
|
|
|
|
)
|
|
|
|
|
return decode_pointpillars_output(output_boxes, output_count), elapsed_ms
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _infer(
|
|
|
|
|
triton_url: str,
|
|
|
|
|
points: np.ndarray,
|
|
|
|
|
num_points: np.ndarray,
|
|
|
|
|
) -> tuple[np.ndarray, np.ndarray, float]:
|
|
|
|
|
points_binary = np.ascontiguousarray(points, dtype=np.float32).tobytes()
|
|
|
|
|
count_binary = np.ascontiguousarray(num_points, dtype=np.int32).tobytes()
|
|
|
|
|
header = {
|
|
|
|
|
"inputs": [
|
|
|
|
|
{
|
|
|
|
|
"name": "points",
|
|
|
|
|
"shape": [1, MAXIMUM_POINTS, 4],
|
|
|
|
|
"datatype": "FP32",
|
|
|
|
|
"parameters": {"binary_data_size": len(points_binary)},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "num_points",
|
|
|
|
|
"shape": [1],
|
|
|
|
|
"datatype": "INT32",
|
|
|
|
|
"parameters": {"binary_data_size": len(count_binary)},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
"outputs": [
|
|
|
|
|
{"name": "output_boxes", "parameters": {"binary_data": True}},
|
|
|
|
|
{"name": "num_boxes", "parameters": {"binary_data": True}},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
encoded = _canonical_json(header)
|
|
|
|
|
request = urllib.request.Request(
|
|
|
|
|
f"{triton_url.rstrip('/')}/v2/models/{MODEL_NAME}/infer",
|
|
|
|
|
data=encoded + points_binary + count_binary,
|
|
|
|
|
headers={
|
|
|
|
|
"Content-Type": "application/octet-stream",
|
|
|
|
|
"Inference-Header-Content-Length": str(len(encoded)),
|
|
|
|
|
},
|
|
|
|
|
method="POST",
|
|
|
|
|
)
|
|
|
|
|
started = time.perf_counter()
|
|
|
|
|
with urllib.request.urlopen(request, timeout=120) as response:
|
|
|
|
|
payload = response.read()
|
|
|
|
|
header_length = int(response.headers["Inference-Header-Content-Length"])
|
|
|
|
|
elapsed_ms = (time.perf_counter() - started) * 1000.0
|
|
|
|
|
response_header = json.loads(payload[:header_length])
|
|
|
|
|
outputs = response_header.get("outputs")
|
|
|
|
|
if not isinstance(outputs, list) or len(outputs) != 2:
|
|
|
|
|
raise RuntimeError("Triton PointPillars output set changed")
|
|
|
|
|
offset = header_length
|
|
|
|
|
decoded: dict[str, np.ndarray] = {}
|
|
|
|
|
for descriptor in outputs:
|
|
|
|
|
if not isinstance(descriptor, dict):
|
|
|
|
|
raise RuntimeError("Triton output descriptor is invalid")
|
|
|
|
|
name = descriptor.get("name")
|
|
|
|
|
parameters = descriptor.get("parameters")
|
|
|
|
|
if not isinstance(name, str) or not isinstance(parameters, dict):
|
|
|
|
|
raise RuntimeError("Triton output descriptor is invalid")
|
|
|
|
|
byte_length = parameters.get("binary_data_size")
|
|
|
|
|
if isinstance(byte_length, bool) or not isinstance(byte_length, int):
|
|
|
|
|
raise RuntimeError("Triton output byte length is invalid")
|
|
|
|
|
binary = payload[offset : offset + byte_length]
|
|
|
|
|
offset += byte_length
|
|
|
|
|
if name == "output_boxes" and descriptor.get("datatype") == "FP32":
|
|
|
|
|
decoded[name] = np.frombuffer(binary, dtype="<f4").reshape(
|
|
|
|
|
1, 393_216, 9
|
|
|
|
|
)
|
|
|
|
|
elif name == "num_boxes" and descriptor.get("datatype") == "INT32":
|
|
|
|
|
decoded[name] = np.frombuffer(binary, dtype="<i4").reshape(1)
|
|
|
|
|
else:
|
|
|
|
|
raise RuntimeError("Triton PointPillars output contract changed")
|
|
|
|
|
if offset != len(payload) or set(decoded) != {"output_boxes", "num_boxes"}:
|
|
|
|
|
raise RuntimeError("Triton PointPillars output payload is invalid")
|
|
|
|
|
return decoded["output_boxes"], decoded["num_boxes"], elapsed_ms
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _box_payload(box: PointPillarsBox) -> dict[str, object]:
|
|
|
|
|
return {
|
|
|
|
|
"x_m": box.x_m,
|
|
|
|
|
"y_m": box.y_m,
|
|
|
|
|
"z_m": box.z_m,
|
|
|
|
|
"length_m": box.length_m,
|
|
|
|
|
"width_m": box.width_m,
|
|
|
|
|
"height_m": box.height_m,
|
|
|
|
|
"yaw_rad": box.yaw_rad,
|
|
|
|
|
"class_id": box.class_id,
|
|
|
|
|
"model_class": box.model_class,
|
|
|
|
|
"score": box.score,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _boxes_sha256(boxes: tuple[PointPillarsBox, ...]) -> str:
|
|
|
|
|
return hashlib.sha256(
|
|
|
|
|
_canonical_json([_box_payload(box) for box in boxes])
|
|
|
|
|
).hexdigest()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _frame_summary(
|
|
|
|
|
*,
|
|
|
|
|
frame_index: int,
|
|
|
|
|
session_seconds: float,
|
|
|
|
|
source_point_count: int,
|
|
|
|
|
pose_index: int,
|
|
|
|
|
pose_binding_age_ms: float,
|
|
|
|
|
input_sha256: str,
|
|
|
|
|
inference_ms: float,
|
|
|
|
|
boxes: tuple[PointPillarsBox, ...],
|
|
|
|
|
) -> dict[str, object]:
|
|
|
|
|
class_counts = {
|
|
|
|
|
name: sum(box.model_class == name for box in boxes)
|
|
|
|
|
for name in ("Vehicle", "Pedestrian", "Cyclist")
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
"schema_version": FRAME_SCHEMA,
|
|
|
|
|
"frame_id": f"{frame_index:06d}",
|
|
|
|
|
"frame_index": frame_index,
|
|
|
|
|
"session_seconds": session_seconds,
|
|
|
|
|
"source_point_count": source_point_count,
|
|
|
|
|
"pose_index": pose_index,
|
|
|
|
|
"pose_binding_age_ms": pose_binding_age_ms,
|
|
|
|
|
"input_xyzi_sha256": input_sha256,
|
|
|
|
|
"inference_ms": inference_ms,
|
|
|
|
|
"prediction_count": len(boxes),
|
|
|
|
|
"class_counts": class_counts,
|
|
|
|
|
"boxes_sha256": _boxes_sha256(boxes),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _read_frame_summary(
|
|
|
|
|
path: Path,
|
|
|
|
|
*,
|
|
|
|
|
frame_index: int,
|
|
|
|
|
input_sha256: str,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
payload = _read_json(path)
|
|
|
|
|
if (
|
|
|
|
|
payload.get("schema_version") != FRAME_SCHEMA
|
|
|
|
|
or payload.get("frame_id") != f"{frame_index:06d}"
|
|
|
|
|
or payload.get("frame_index") != frame_index
|
|
|
|
|
or payload.get("input_xyzi_sha256") != input_sha256
|
|
|
|
|
or not isinstance(payload.get("class_counts"), dict)
|
|
|
|
|
or not isinstance(payload.get("boxes_sha256"), str)
|
|
|
|
|
):
|
|
|
|
|
raise RuntimeError("cached L3.1 frame summary is invalid")
|
|
|
|
|
return payload
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _visual_frame(
|
|
|
|
|
*,
|
|
|
|
|
summary: dict[str, Any],
|
|
|
|
|
xyzi: np.ndarray,
|
|
|
|
|
boxes: tuple[PointPillarsBox, ...],
|
|
|
|
|
replay_inference_ms: float,
|
|
|
|
|
deterministic_replay: bool,
|
|
|
|
|
) -> dict[str, object]:
|
|
|
|
|
minimum_x, minimum_y, minimum_z, maximum_x, maximum_y, maximum_z = (
|
|
|
|
|
POINTPILLARS_MODEL_POINT_CLOUD_RANGE
|
|
|
|
|
)
|
|
|
|
|
mask = (
|
|
|
|
|
(xyzi[:, 0] >= minimum_x)
|
|
|
|
|
& (xyzi[:, 0] <= maximum_x)
|
|
|
|
|
& (xyzi[:, 1] >= minimum_y)
|
|
|
|
|
& (xyzi[:, 1] <= maximum_y)
|
|
|
|
|
& (xyzi[:, 2] >= minimum_z)
|
|
|
|
|
& (xyzi[:, 2] <= maximum_z)
|
|
|
|
|
)
|
|
|
|
|
visible_points = xyzi[mask]
|
|
|
|
|
visible_boxes = boxes[:MAXIMUM_VISUAL_BOXES]
|
|
|
|
|
return {
|
|
|
|
|
"schema_version": VISUAL_FRAME_SCHEMA,
|
|
|
|
|
"frame_id": summary["frame_id"],
|
|
|
|
|
"summary": {
|
|
|
|
|
**summary,
|
|
|
|
|
"replay_inference_ms": replay_inference_ms,
|
|
|
|
|
"deterministic_replay": deterministic_replay,
|
|
|
|
|
"visual_box_count": len(visible_boxes),
|
|
|
|
|
"visual_box_truncated": len(boxes) > len(visible_boxes),
|
|
|
|
|
},
|
|
|
|
|
"points": {
|
|
|
|
|
"layout": "flat-xyzi",
|
|
|
|
|
"source_point_count": int(xyzi.shape[0]),
|
|
|
|
|
"model_range_point_count": int(visible_points.shape[0]),
|
|
|
|
|
"sampled_point_count": int(visible_points.shape[0]),
|
|
|
|
|
"values": visible_points.reshape(-1).tolist(),
|
|
|
|
|
},
|
|
|
|
|
"prediction_boxes": [_box_payload(box) for box in visible_boxes],
|
|
|
|
|
"interpretation": {
|
|
|
|
|
"ground_truth_available": False,
|
|
|
|
|
"boxes_are_model_hypotheses": True,
|
|
|
|
|
"accuracy_claim_allowed": False,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _metrics(
|
|
|
|
|
summaries: list[dict[str, Any]],
|
|
|
|
|
*,
|
|
|
|
|
deterministic_matches: int,
|
|
|
|
|
deterministic_total: int,
|
|
|
|
|
) -> dict[str, object]:
|
|
|
|
|
latency = np.asarray([row["inference_ms"] for row in summaries], dtype=np.float64)
|
|
|
|
|
pose_age = np.asarray(
|
|
|
|
|
[row["pose_binding_age_ms"] for row in summaries],
|
|
|
|
|
dtype=np.float64,
|
|
|
|
|
)
|
|
|
|
|
class_counts = {
|
|
|
|
|
name: sum(int(row["class_counts"][name]) for row in summaries)
|
|
|
|
|
for name in ("Vehicle", "Pedestrian", "Cyclist")
|
|
|
|
|
}
|
|
|
|
|
predictions = [int(row["prediction_count"]) for row in summaries]
|
|
|
|
|
return {
|
|
|
|
|
"frame_count": len(summaries),
|
|
|
|
|
"input_admission_fraction": 1.0,
|
|
|
|
|
"output_schema_valid_fraction": 1.0,
|
|
|
|
|
"frames_with_predictions": sum(value > 0 for value in predictions),
|
|
|
|
|
"frames_with_vehicle_predictions": sum(
|
|
|
|
|
int(row["class_counts"]["Vehicle"]) > 0 for row in summaries
|
|
|
|
|
),
|
|
|
|
|
"prediction_count": sum(predictions),
|
|
|
|
|
"class_counts": class_counts,
|
|
|
|
|
"predictions_per_frame": _distribution(np.asarray(predictions)),
|
|
|
|
|
"inference_latency_ms": _distribution(latency),
|
|
|
|
|
"pose_binding_age_ms": _distribution(pose_age),
|
|
|
|
|
"deterministic_replay_fraction": (
|
|
|
|
|
deterministic_matches / deterministic_total
|
|
|
|
|
if deterministic_total
|
|
|
|
|
else 0.0
|
|
|
|
|
),
|
|
|
|
|
"deterministic_replay_frames": deterministic_total,
|
|
|
|
|
"source_paced": False,
|
|
|
|
|
"input_drop_count": 0,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _distribution(values: np.ndarray) -> dict[str, float]:
|
|
|
|
|
return {
|
|
|
|
|
"minimum": float(np.min(values)),
|
|
|
|
|
"p50": float(np.percentile(values, 50)),
|
|
|
|
|
"p95": float(np.percentile(values, 95)),
|
|
|
|
|
"maximum": float(np.max(values)),
|
|
|
|
|
"mean": float(np.mean(values)),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _read_worker_package(path: Path) -> dict[str, Any]:
|
|
|
|
|
root = path.expanduser().resolve(strict=True)
|
|
|
|
|
manifest = _read_json(root / "manifest.json")
|
|
|
|
|
identity = manifest.get("identity")
|
|
|
|
|
artifacts = manifest.get("artifacts")
|
|
|
|
|
identity_sha256 = manifest.get("identity_sha256")
|
|
|
|
|
package_id = manifest.get("package_id")
|
|
|
|
|
if (
|
|
|
|
|
root.is_symlink()
|
|
|
|
|
or manifest.get("schema_version") != WORKER_PACKAGE_SCHEMA
|
|
|
|
|
or not isinstance(identity, dict)
|
|
|
|
|
or not isinstance(artifacts, list)
|
|
|
|
|
or not isinstance(identity_sha256, str)
|
|
|
|
|
or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256
|
|
|
|
|
or package_id != f"l3-pointpillars-worker-package-{identity_sha256}"
|
|
|
|
|
or root.name != package_id
|
|
|
|
|
or Path(__file__).resolve(strict=True).parents[1] != root
|
|
|
|
|
):
|
|
|
|
|
raise RuntimeError("L3.1 worker package identity is invalid")
|
|
|
|
|
expected = set(identity.get("artifact_paths", []))
|
|
|
|
|
actual = {
|
|
|
|
|
item.relative_to(root).as_posix()
|
|
|
|
|
for item in root.rglob("*")
|
|
|
|
|
if item.is_file()
|
|
|
|
|
}
|
|
|
|
|
if actual != expected | {"manifest.json"}:
|
|
|
|
|
raise RuntimeError("L3.1 worker package file set changed")
|
|
|
|
|
for descriptor in artifacts:
|
|
|
|
|
if not isinstance(descriptor, dict):
|
|
|
|
|
raise RuntimeError("L3.1 worker package artifact is invalid")
|
|
|
|
|
relative = descriptor.get("path")
|
|
|
|
|
member = root / str(relative)
|
|
|
|
|
if (
|
|
|
|
|
not isinstance(relative, str)
|
|
|
|
|
or relative not in expected
|
|
|
|
|
or not member.is_file()
|
|
|
|
|
or member.is_symlink()
|
|
|
|
|
or descriptor.get("byte_length") != member.stat().st_size
|
|
|
|
|
or descriptor.get("sha256") != _sha256(member)
|
|
|
|
|
):
|
|
|
|
|
raise RuntimeError("L3.1 worker package artifact changed")
|
|
|
|
|
return manifest
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _require_triton_ready(url: str) -> None:
|
|
|
|
|
for endpoint in ("/v2/health/ready", f"/v2/models/{MODEL_NAME}/ready"):
|
|
|
|
|
request = urllib.request.Request(f"{url.rstrip('/')}{endpoint}", method="GET")
|
|
|
|
|
try:
|
|
|
|
|
with urllib.request.urlopen(request, timeout=10) as response:
|
|
|
|
|
if response.status != 200:
|
|
|
|
|
raise RuntimeError("canonical Triton is not ready")
|
|
|
|
|
except OSError as exc:
|
|
|
|
|
raise RuntimeError("canonical Triton or PointPillars is not ready") from exc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _artifact(path: Path, kind: str, *, relative_to: Path) -> dict[str, object]:
|
|
|
|
|
return {
|
|
|
|
|
"kind": kind,
|
|
|
|
|
"path": path.relative_to(relative_to).as_posix(),
|
|
|
|
|
"byte_length": path.stat().st_size,
|
|
|
|
|
"sha256": _sha256(path),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _directory_identity(root: Path) -> str:
|
|
|
|
|
digest = hashlib.sha256()
|
|
|
|
|
for path in sorted(root.glob("*.json"), key=lambda item: item.name):
|
|
|
|
|
digest.update(path.name.encode("ascii"))
|
|
|
|
|
digest.update(b"\0")
|
|
|
|
|
digest.update(bytes.fromhex(_sha256(path)))
|
|
|
|
|
return digest.hexdigest()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _canonical_json(value: object) -> bytes:
|
|
|
|
|
return json.dumps(
|
|
|
|
|
value,
|
|
|
|
|
ensure_ascii=False,
|
|
|
|
|
sort_keys=True,
|
|
|
|
|
separators=(",", ":"),
|
|
|
|
|
allow_nan=False,
|
|
|
|
|
).encode("utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _read_json(path: Path) -> dict[str, Any]:
|
|
|
|
|
try:
|
|
|
|
|
value = json.loads(path.read_text(encoding="utf-8"))
|
|
|
|
|
except (OSError, json.JSONDecodeError) as exc:
|
|
|
|
|
raise RuntimeError(f"invalid JSON artifact: {path.name}") from exc
|
|
|
|
|
if not isinstance(value, dict):
|
|
|
|
|
raise RuntimeError(f"invalid JSON artifact: {path.name}")
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _write_once(path: Path, payload: object) -> None:
|
|
|
|
|
encoded = _canonical_json(payload)
|
|
|
|
|
if path.exists():
|
|
|
|
|
if path.read_bytes() != encoded:
|
|
|
|
|
raise RuntimeError(f"immutable artifact changed: {path.name}")
|
|
|
|
|
return
|
|
|
|
|
_atomic_bytes(path, encoded)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _atomic_json(path: Path, payload: object) -> None:
|
|
|
|
|
_atomic_bytes(path, _canonical_json(payload))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _atomic_bytes(path: Path, payload: bytes) -> None:
|
|
|
|
|
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
|
|
|
descriptor, temporary = tempfile.mkstemp(
|
|
|
|
|
prefix=f".{path.name}.",
|
|
|
|
|
suffix=".tmp",
|
|
|
|
|
dir=path.parent,
|
|
|
|
|
)
|
|
|
|
|
try:
|
|
|
|
|
with os.fdopen(descriptor, "wb") as handle:
|
|
|
|
|
handle.write(payload)
|
|
|
|
|
handle.flush()
|
|
|
|
|
os.fsync(handle.fileno())
|
|
|
|
|
os.replace(temporary, path)
|
|
|
|
|
except BaseException:
|
|
|
|
|
try:
|
|
|
|
|
os.unlink(temporary)
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
pass
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _sha256(path: Path) -> str:
|
|
|
|
|
digest = hashlib.sha256()
|
|
|
|
|
with path.open("rb") as handle:
|
|
|
|
|
for block in iter(lambda: handle.read(1024 * 1024), b""):
|
|
|
|
|
digest.update(block)
|
|
|
|
|
return digest.hexdigest()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
raise SystemExit(main())
|