feat(perception): run PointPillars on RAVNOVES00
This commit is contained in:
@@ -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())
|
||||
Reference in New Issue
Block a user