feat(perception): run PointPillars on RAVNOVES00

This commit is contained in:
DCCONSTRUCTIONS
2026-07-31 14:35:34 +03:00
parent b44c4b3265
commit 55b7821a5c
11 changed files with 1760 additions and 9 deletions
@@ -0,0 +1,123 @@
# L3.1 · PointPillars на RAVNOVES00 — 2026-07-31
## Решение
`L3.1` — это полный transfer-прогон текущего PointPillars-кандидата по
реальной записи нашего сканера `RAVNOVES00`. Внешний KITTI в эту лабораторную
работу не входит: прежний `L3` сохранён отдельно как публичный benchmark для
проверки измерителя и идеального box-truth контура.
Полный runtime-контракт на RAVNOVES00 выполнен, но модель не допускается в
operational detector. У записи нет независимых ориентированных 3D truth-боксов,
поэтому полученные боксы являются только визуально проверяемыми гипотезами.
Кроме того, повтор выбранных кадров совпал только в `50%` случаев.
Неизменяемый результат:
`l31-pointpillars-ravnoves-80a9715f64ea397222fbcfd700803f9152009e3751caf87e2e9dc5ec6fc01b72`
## Источник
- запись: `RAVNOVES00`;
- канонический session id: `20260720T065719Z_viewer_live`;
- replay pack:
`lidar-replay-pack-8fc0fb418578b8ee2ac88d502d2acbc63ae533437a9da14f1a9f9d8916f613ce`;
- logical content SHA-256:
`8d5642e5f394a48f5e07322e813384c601c96880f1ec349075f064a2d077b4ac`;
- LiDAR-кадров: `4,570`;
- pose-кадров: `4,598`;
- исходных точек: `10,751,258`.
Каждая порция point cloud была связана с ближайшей pose. Точки, сохранённые в
map frame, детерминированно преобразованы обратно в sensor-frame XYZI,
ожидаемый моделью. Возраст pose binding не превысил `73.797625 ms`.
## Исполнение
Прогон выполнен на Worker 006 внутри уже работающего канонического контейнера
`ndc-mission-core-triton`:
- второй контейнер и второй serving stack не создавались;
- существующий Triton не перезапускался;
- один последовательный worker;
- source pacing отключён;
- Synology не использовался;
- модель ONNX SHA-256:
`2dcabddc3a365e9608a112d7bbbb7db769a6dddeeaa59aa03611a83113326da1`;
- TensorRT engine SHA-256:
`12005d972a4632d56342a5da44442b632c1dcc5144fa3c70b162dec334532481`;
- worker package:
`l3-pointpillars-worker-package-6799b6f4ad776a4a959dc2d6366c6bcdf8ddb922990571c46841acf186e28c9b`.
## Полный результат
| Метрика | Значение |
| --- | ---: |
| Принято входных кадров | `4,570 / 4,570` (`100%`) |
| Валидных выходных контрактов | `4,570 / 4,570` (`100%`) |
| Кадров с гипотезами | `4,570` |
| Кадров с Vehicle-гипотезами | `4,559` |
| Всего гипотез | `48,005` |
| Vehicle | `28,097` |
| Pedestrian | `1,095` |
| Cyclist | `18,813` |
| Inference mean | `27.857662 ms` |
| Inference p50 | `26.324656 ms` |
| Inference p95 | `39.919944 ms` |
| Inference max | `143.260391 ms` |
| Повтор выбранных визуальных кадров | `9 / 18` (`50%`) |
Идентичность полного набора per-frame результатов:
`9e4bb0fe1fc7a31d77bbc7d4de08e9e42c1456d28728ae25f9cc88598f0ebac0`
## Визуальное доказательство
Из полного маршрута детерминированно выбраны `18` кадров: маршрут разделён на
равные временные окна, внутри каждого приоритет имеет число Vehicle-гипотез,
затем общее число гипотез и близость к центру окна.
Для каждого выбранного кадра опубликованы:
- ограниченная равномерная выборка реальных XYZI-точек RAVNOVES00;
- ориентированные 3D-боксы PointPillars;
- классы и confidence;
- исходная latency;
- результат повторного запуска того же кадра.
Mission Core загружает только выбранный кадр и показывает его в общем
лабораторном viewer в режимах `3D` и `BEV`. В интерфейсе боксы явно обозначены
как гипотезы модели, а не ground truth, TP/FP/FN или подтверждённые объекты.
## Что доказано
- реальный RAVNOVES00 проходит через текущий PointPillars input/output seam без
пропуска кадров и нарушения схемы;
- существующий Worker 006 и Triton способны последовательно обработать полный
маршрут;
- задержка inference измерена на всём наборе, а не на коротком smoke;
- фактические точки и гипотезы можно проверять визуально в Mission Core.
## Что не доказано
- точность, полнота и классовая корректность детектора;
- TP/FP/FN и пригодность боксов для продукта;
- source-paced queue/drop/deadline поведение;
- навигация, команды или safety;
- замена camera-first семантики LiDAR-only моделью.
Текущий source — vendor map increment без ring, firing time, scan geometry и
IMU. Модель обучена на другом proprietary solid-state LiDAR domain.
## Следующий gate
1. Разобрать визуально `18` опубликованных кадров и локализовать характер
доменного расхождения.
2. Выяснить причину `50%` повторяемости боксов при неизменном входе и sealed
engine.
3. Зафиксировать небольшой независимый RAVNOVES box-truth набор до любого
обучения или подбора порога.
4. Только после этого допускать новый train/adaptation candidate на Worker 006.
5. Основную продуктовую архитектуру сохранять как
`camera-first semantics + LiDAR metric geometry`; LiDAR-native detector
остаётся отдельным доказательным кандидатом.
@@ -1,5 +1,11 @@
# L3 PointPillars transfer report — 2026-07-31
> Контур размещения: это отдельный публичный benchmark на KITTI. Он сохранён
> для проверки измерителя, box-truth и cross-domain ideal/reference path, но не
> является основной лабораторной работой нашего сканера. Реальный прогон
> RAVNOVES00 оформлен отдельно как `L3.1`; см.
> `L31_POINTPILLARS_RAVNOVES_REPORT_2026-07-31.md`.
## Decision
The LiDAR-native detector seam is operational on Worker 006, but the frozen
@@ -230,10 +236,10 @@ the sensor-domain mismatch.
4. Export a new ONNX, build its TensorRT engine on Worker 006 and evaluate
through the already proven canonical Triton seam.
5. Require a useful public box-truth result before K1 accuracy language.
6. Run `RAVNOVES01` as a K1 representation/stability transfer after the public
candidate is useful. Because that session has no camera and no independent
3D cuboids, it can prove deterministic replay, output stability, latency,
queue/drop behavior and visual plausibility, but not detector accuracy.
6. Keep the public KITTI benchmark outside the primary scanner profile.
RAVNOVES transfer is now the separate `L3.1` laboratory work; its completed
baseline and limitations are recorded in
`L31_POINTPILLARS_RAVNOVES_REPORT_2026-07-31.md`.
The adaptation task is a new admission. This report does not authorize a
training container, a second serving stack, K1 quality claims or navigation
@@ -26,6 +26,9 @@ _SOURCE_FILES = {
"runtime/k1link/compute/pointpillars_postprocess.py": (
"src/k1link/compute/pointpillars_postprocess.py"
),
"runtime/k1link/compute/l31_pointpillars_ravnoves.py": (
"src/k1link/compute/l31_pointpillars_ravnoves.py"
),
"runtime/k1link/datasets/kitti_3d_admission.py": (
"src/k1link/datasets/kitti_3d_admission.py"
),
@@ -41,6 +44,9 @@ _SOURCE_FILES = {
"runtime/run_l3_pointpillars_public_baseline.py": (
"experiments/perception/worker/run_l3_pointpillars_public_baseline.py"
),
"runtime/run_l31_pointpillars_ravnoves.py": (
"experiments/perception/worker/run_l31_pointpillars_ravnoves.py"
),
"runtime/smoke_l3_pointpillars_live.py": (
"experiments/perception/worker/smoke_l3_pointpillars_live.py"
),
@@ -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())