feat(perception): add PointPillars transfer gate
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Admit the canonical archive-only KITTI release on Worker 006."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.datasets.kitti_3d_admission import (
|
||||
admit_kitti_3d_object_release,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--dataset-root",
|
||||
type=Path,
|
||||
default=Path("/mnt/d/NDC_MISSIONCORE/datasets"),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
result = admit_kitti_3d_object_release(args.dataset_root)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"source_id": result["source_id"],
|
||||
"status": result["status"],
|
||||
"release_identity_sha256": result["release_identity_sha256"],
|
||||
"training_frame_count": result["alignment"][
|
||||
"training_frame_count"
|
||||
],
|
||||
"validation_frame_count": result["alignment"]["split_counts"][
|
||||
"validation"
|
||||
],
|
||||
"next_action": result["next_action"],
|
||||
},
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Promote the verified PointPillars engine into canonical Triton exactly once."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import urllib.request
|
||||
from contextlib import suppress
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from k1link.compute.l3_pointpillars_admission import (
|
||||
read_l3_pointpillars_admission,
|
||||
)
|
||||
|
||||
PROVENANCE_SCHEMA = "missioncore.triton-model-provenance/v1"
|
||||
MODEL_NAME = "pointpillars"
|
||||
EXPECTED_ENGINE_SHA256 = (
|
||||
"12005d972a4632d56342a5da44442b632c1dcc5144fa3c70b162dec334532481"
|
||||
)
|
||||
EXPECTED_ENGINE_BYTES = 8_785_436
|
||||
EXPECTED_CONFIG_SHA256 = (
|
||||
"a68e7e37ae611b7d4c3fb633b31360ddbdf26ab0a37a363cc566357e614a385c"
|
||||
)
|
||||
EXPECTED_SOURCE_SHA256 = (
|
||||
"2dcabddc3a365e9608a112d7bbbb7db769a6dddeeaa59aa03611a83113326da1"
|
||||
)
|
||||
EXPECTED_LABEL_SHA256 = (
|
||||
"0adaeb5a374421b61bf83b8fa4522e11abd68461f239a4c72cf5627de913b3da"
|
||||
)
|
||||
CANONICAL_MODEL_REPOSITORY = Path("/mnt/d/NDC_MISSIONCORE/runtime/models")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--admission-result", type=Path, required=True)
|
||||
parser.add_argument("--engine", type=Path, required=True)
|
||||
parser.add_argument("--config", type=Path, required=True)
|
||||
parser.add_argument(
|
||||
"--model-repository",
|
||||
type=Path,
|
||||
default=CANONICAL_MODEL_REPOSITORY,
|
||||
)
|
||||
parser.add_argument("--triton-url", default="http://127.0.0.1:8000")
|
||||
args = parser.parse_args()
|
||||
|
||||
admission = read_l3_pointpillars_admission(args.admission_result)
|
||||
if (
|
||||
admission.report.get("status") != "blocked-foundation-assets"
|
||||
or admission.report.get("blocker_codes")
|
||||
!= ["pointpillars-model-not-installed-live"]
|
||||
or admission.report.get("eligible_public_probe_dataset_ids")
|
||||
!= ["kitti-3d-object/v2017"]
|
||||
or admission.report.get("detector", {}).get("staged_target_engine_ready")
|
||||
is not True
|
||||
):
|
||||
raise RuntimeError(
|
||||
"L3 admission does not authorize canonical PointPillars promotion"
|
||||
)
|
||||
engine = args.engine.resolve(strict=True)
|
||||
config = args.config.resolve(strict=True)
|
||||
if engine.stat().st_size != EXPECTED_ENGINE_BYTES or _sha256(
|
||||
engine
|
||||
) != EXPECTED_ENGINE_SHA256:
|
||||
raise RuntimeError("PointPillars target engine identity changed")
|
||||
if _sha256(config) != EXPECTED_CONFIG_SHA256:
|
||||
raise RuntimeError("PointPillars Triton configuration identity changed")
|
||||
repository = args.model_repository.expanduser().absolute()
|
||||
if repository != CANONICAL_MODEL_REPOSITORY or not repository.is_dir():
|
||||
raise RuntimeError("PointPillars promotion requires the canonical model repository")
|
||||
destination = repository / MODEL_NAME
|
||||
if destination.exists():
|
||||
_validate_existing(destination)
|
||||
_require_model_ready(args.triton_url)
|
||||
print(
|
||||
json.dumps(
|
||||
{"status": "already-installed", "model": MODEL_NAME},
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
provenance = {
|
||||
"schema_version": PROVENANCE_SCHEMA,
|
||||
"model_name": MODEL_NAME,
|
||||
"upstream_model_id": "nvidia/tao/pointpillarnet",
|
||||
"upstream_version": "deployable_v1.1",
|
||||
"source_format": "onnx",
|
||||
"source_model_sha256": EXPECTED_SOURCE_SHA256,
|
||||
"source_label_sha256": EXPECTED_LABEL_SHA256,
|
||||
"engine_sha256": EXPECTED_ENGINE_SHA256,
|
||||
"engine_byte_length": EXPECTED_ENGINE_BYTES,
|
||||
"config_sha256": EXPECTED_CONFIG_SHA256,
|
||||
"engine_built_on_target": True,
|
||||
"worker_host_id": "worker-006",
|
||||
"gpu": "NVIDIA GeForce RTX 4090",
|
||||
"compute_capability": "8.9",
|
||||
"tensorrt_version": "11.0.0",
|
||||
"precision": "strongly-typed",
|
||||
"admission_result_id": admission.result_id,
|
||||
"installed_at_utc": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
|
||||
"authority": {
|
||||
"shadow_only": True,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
}
|
||||
staging = Path(
|
||||
tempfile.mkdtemp(
|
||||
dir=repository,
|
||||
prefix=f".{MODEL_NAME}.",
|
||||
suffix=".incomplete",
|
||||
)
|
||||
)
|
||||
try:
|
||||
version = staging / "1"
|
||||
version.mkdir(mode=0o700)
|
||||
shutil.copyfile(engine, version / "model.plan")
|
||||
shutil.copyfile(config, staging / "config.pbtxt")
|
||||
_atomic_json(staging / "provenance.json", provenance)
|
||||
if (
|
||||
_sha256(version / "model.plan") != EXPECTED_ENGINE_SHA256
|
||||
or _sha256(staging / "config.pbtxt") != EXPECTED_CONFIG_SHA256
|
||||
):
|
||||
raise RuntimeError("PointPillars staged model package changed during copy")
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
|
||||
request = urllib.request.Request(
|
||||
f"{args.triton_url.rstrip('/')}/v2/repository/models/{MODEL_NAME}/load",
|
||||
data=b"{}",
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=180) as response:
|
||||
if response.status != 200:
|
||||
raise RuntimeError("canonical Triton rejected PointPillars load")
|
||||
_require_model_ready(args.triton_url)
|
||||
except OSError as exc:
|
||||
raise RuntimeError(
|
||||
"PointPillars package is installed but canonical Triton did not load it"
|
||||
) from exc
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"status": "installed-and-ready",
|
||||
"model": MODEL_NAME,
|
||||
"engine_sha256": EXPECTED_ENGINE_SHA256,
|
||||
"provenance_sha256": _sha256(destination / "provenance.json"),
|
||||
},
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def _validate_existing(destination: Path) -> None:
|
||||
engine = destination / "1/model.plan"
|
||||
config = destination / "config.pbtxt"
|
||||
provenance_path = destination / "provenance.json"
|
||||
if (
|
||||
not engine.is_file()
|
||||
or not config.is_file()
|
||||
or not provenance_path.is_file()
|
||||
or engine.stat().st_size != EXPECTED_ENGINE_BYTES
|
||||
or _sha256(engine) != EXPECTED_ENGINE_SHA256
|
||||
or _sha256(config) != EXPECTED_CONFIG_SHA256
|
||||
):
|
||||
raise RuntimeError("existing PointPillars model package is not admitted")
|
||||
try:
|
||||
provenance = json.loads(provenance_path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise RuntimeError("existing PointPillars provenance is invalid") from exc
|
||||
if (
|
||||
not isinstance(provenance, dict)
|
||||
or provenance.get("schema_version") != PROVENANCE_SCHEMA
|
||||
or provenance.get("model_name") != MODEL_NAME
|
||||
or provenance.get("engine_sha256") != EXPECTED_ENGINE_SHA256
|
||||
or provenance.get("source_model_sha256") != EXPECTED_SOURCE_SHA256
|
||||
or provenance.get("source_label_sha256") != EXPECTED_LABEL_SHA256
|
||||
or provenance.get("engine_built_on_target") is not True
|
||||
):
|
||||
raise RuntimeError("existing PointPillars provenance is invalid")
|
||||
|
||||
|
||||
def _require_model_ready(url: str) -> None:
|
||||
request = urllib.request.Request(
|
||||
f"{url.rstrip('/')}/v2/models/{MODEL_NAME}/ready",
|
||||
method="GET",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=30) as response:
|
||||
if response.status != 200:
|
||||
raise RuntimeError("PointPillars is not ready")
|
||||
except OSError as exc:
|
||||
raise RuntimeError("PointPillars is not ready in canonical Triton") from exc
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as source:
|
||||
for chunk in iter(lambda: source.read(8 * 1024**2), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _canonical_json(payload: Any) -> bytes:
|
||||
return json.dumps(
|
||||
payload,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def _atomic_json(path: Path, payload: dict[str, Any]) -> None:
|
||||
descriptor, temporary = tempfile.mkstemp(
|
||||
dir=path.parent,
|
||||
prefix=f".{path.name}.",
|
||||
suffix=".tmp",
|
||||
)
|
||||
try:
|
||||
with os.fdopen(descriptor, "wb") as target:
|
||||
target.write(_canonical_json(payload) + b"\n")
|
||||
target.flush()
|
||||
os.fsync(target.fileno())
|
||||
os.replace(temporary, path)
|
||||
except BaseException:
|
||||
with suppress(OSError):
|
||||
os.unlink(temporary)
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,588 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the complete KITTI validation baseline through canonical Triton."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.request
|
||||
import zipfile
|
||||
from contextlib import suppress
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from k1link.compute.kitti_pointpillars_benchmark import (
|
||||
PointPillarsFramePrediction,
|
||||
evaluate_pointpillars_predictions,
|
||||
read_kitti_validation_truth,
|
||||
)
|
||||
from k1link.compute.pointpillars_postprocess import (
|
||||
POINTPILLARS_EMBEDDED_SCORE_THRESHOLD,
|
||||
POINTPILLARS_MODEL_POINT_CLOUD_RANGE,
|
||||
PointPillarsBox,
|
||||
decode_pointpillars_output,
|
||||
)
|
||||
from k1link.datasets.kitti_3d_admission import (
|
||||
KITTI_3D_RELEASE_ROOT,
|
||||
KITTI_CALIB_ARCHIVE,
|
||||
KITTI_LABEL_ARCHIVE,
|
||||
KITTI_VELODYNE_ARCHIVE,
|
||||
read_kitti_3d_admission,
|
||||
read_kitti_standard_splits,
|
||||
)
|
||||
|
||||
RUN_SCHEMA = "missioncore.l3-pointpillars-kitti-transfer-run/v1"
|
||||
FRAME_SCHEMA = "missioncore.l3-pointpillars-kitti-transfer-frame/v1"
|
||||
REPORT_SCHEMA = "missioncore.l3-pointpillars-kitti-transfer-report/v1"
|
||||
MANIFEST_SCHEMA = "missioncore.l3-pointpillars-kitti-transfer-result/v1"
|
||||
WORKER_PACKAGE_SCHEMA = "missioncore.l3-pointpillars-worker-package/v1"
|
||||
MODEL_NAME = "pointpillars"
|
||||
MAXIMUM_POINTS = 204_800
|
||||
EXPECTED_MODEL_SHA256 = (
|
||||
"2dcabddc3a365e9608a112d7bbbb7db769a6dddeeaa59aa03611a83113326da1"
|
||||
)
|
||||
EXPECTED_LABEL_SHA256 = (
|
||||
"0adaeb5a374421b61bf83b8fa4522e11abd68461f239a4c72cf5627de913b3da"
|
||||
)
|
||||
EXPECTED_ENGINE_SHA256 = (
|
||||
"12005d972a4632d56342a5da44442b632c1dcc5144fa3c70b162dec334532481"
|
||||
)
|
||||
EXPECTED_ONNX_CONTRACT_SHA256 = (
|
||||
"2fd29cd054ab058c2cfec3dfba305c71e123ef3f04b457d0c64de0c8dac2e1be"
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--dataset-root", type=Path, required=True)
|
||||
parser.add_argument("--profile", type=Path, required=True)
|
||||
parser.add_argument("--model-provenance", type=Path, required=True)
|
||||
parser.add_argument("--worker-package", type=Path, required=True)
|
||||
parser.add_argument(
|
||||
"--output-root",
|
||||
type=Path,
|
||||
default=Path(
|
||||
"/mnt/d/NDC_MISSIONCORE/runtime/experiments/l3/pointpillars-kitti"
|
||||
),
|
||||
)
|
||||
parser.add_argument("--triton-url", default="http://127.0.0.1:8000")
|
||||
args = parser.parse_args()
|
||||
|
||||
dataset_root = args.dataset_root.expanduser().absolute()
|
||||
admission = read_kitti_3d_admission(dataset_root)
|
||||
splits = read_kitti_standard_splits(dataset_root)
|
||||
validation_frame_ids = splits["validation"]
|
||||
profile_path = args.profile.resolve(strict=True)
|
||||
provenance_path = args.model_provenance.resolve(strict=True)
|
||||
profile = _read_json(profile_path)
|
||||
provenance = _read_json(provenance_path)
|
||||
_validate_profile_and_provenance(profile, provenance)
|
||||
worker_package = _read_worker_package(args.worker_package)
|
||||
_require_triton_ready(args.triton_url)
|
||||
|
||||
identity = {
|
||||
"schema_version": RUN_SCHEMA,
|
||||
"dataset_source_id": admission["source_id"],
|
||||
"dataset_release_identity_sha256": admission["release_identity_sha256"],
|
||||
"validation_frame_count": len(validation_frame_ids),
|
||||
"validation_frame_ids_sha256": hashlib.sha256(
|
||||
"\n".join(validation_frame_ids).encode("ascii") + b"\n"
|
||||
).hexdigest(),
|
||||
"profile_sha256": _sha256(profile_path),
|
||||
"model_provenance_sha256": _sha256(provenance_path),
|
||||
"worker_package_id": worker_package["package_id"],
|
||||
"worker_package_identity_sha256": worker_package["identity_sha256"],
|
||||
"engine_sha256": EXPECTED_ENGINE_SHA256,
|
||||
"producer_sha256": _sha256(Path(__file__)),
|
||||
"execution": {
|
||||
"worker_host_id": "worker-006",
|
||||
"triton_model_name": MODEL_NAME,
|
||||
"sequential": True,
|
||||
"parallel_workers": 1,
|
||||
},
|
||||
"authority": {
|
||||
"shadow_only": True,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
run_id = f"l3-pointpillars-kitti-{identity_sha256}"
|
||||
run_root = args.output_root.expanduser().absolute() / run_id
|
||||
frames_root = run_root / "frames"
|
||||
frames_root.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
_write_once(run_root / "identity.json", identity)
|
||||
|
||||
archive_root = dataset_root / KITTI_3D_RELEASE_ROOT / "archives"
|
||||
points_archive = archive_root / KITTI_VELODYNE_ARCHIVE
|
||||
labels_archive = archive_root / KITTI_LABEL_ARCHIVE
|
||||
calibrations_archive = archive_root / KITTI_CALIB_ARCHIVE
|
||||
truth_by_frame = read_kitti_validation_truth(
|
||||
labels_archive=labels_archive,
|
||||
calibrations_archive=calibrations_archive,
|
||||
validation_frame_ids=validation_frame_ids,
|
||||
)
|
||||
|
||||
predictions: list[PointPillarsFramePrediction] = []
|
||||
try:
|
||||
with zipfile.ZipFile(points_archive.resolve(strict=True)) as points_zip:
|
||||
for completed, frame_id in enumerate(validation_frame_ids, start=1):
|
||||
member = f"training/velodyne/{frame_id}.bin"
|
||||
point_bytes = points_zip.read(member)
|
||||
point_sha256 = hashlib.sha256(point_bytes).hexdigest()
|
||||
frame_path = frames_root / f"{frame_id}.json"
|
||||
if frame_path.exists():
|
||||
prediction = _read_frame(
|
||||
frame_path,
|
||||
frame_id=frame_id,
|
||||
point_sha256=point_sha256,
|
||||
)
|
||||
else:
|
||||
prediction = _run_frame(
|
||||
triton_url=args.triton_url,
|
||||
frame_id=frame_id,
|
||||
point_bytes=point_bytes,
|
||||
)
|
||||
_atomic_json(
|
||||
frame_path,
|
||||
_frame_payload(prediction, point_sha256=point_sha256),
|
||||
)
|
||||
predictions.append(prediction)
|
||||
if completed == 1 or completed % 50 == 0:
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"run_id": run_id,
|
||||
"completed": completed,
|
||||
"total": len(validation_frame_ids),
|
||||
"frame_id": frame_id,
|
||||
},
|
||||
sort_keys=True,
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
except (OSError, KeyError, zipfile.BadZipFile) as exc:
|
||||
raise RuntimeError("KITTI validation point clouds could not be read") from exc
|
||||
|
||||
metrics = evaluate_pointpillars_predictions(
|
||||
truth_by_frame=truth_by_frame,
|
||||
predictions=tuple(predictions),
|
||||
)
|
||||
report = {
|
||||
"schema_version": REPORT_SCHEMA,
|
||||
"run_id": run_id,
|
||||
"status": "public-cross-domain-transfer-probe-measured",
|
||||
"dataset_source_id": admission["source_id"],
|
||||
"dataset_release_identity_sha256": admission["release_identity_sha256"],
|
||||
"model": {
|
||||
"name": MODEL_NAME,
|
||||
"source_model_sha256": EXPECTED_MODEL_SHA256,
|
||||
"source_label_sha256": EXPECTED_LABEL_SHA256,
|
||||
"engine_sha256": EXPECTED_ENGINE_SHA256,
|
||||
},
|
||||
"metrics": metrics,
|
||||
"completed_at_utc": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
|
||||
"authority": identity["authority"],
|
||||
}
|
||||
_atomic_json(run_root / "report.json", report)
|
||||
manifest = {
|
||||
"schema_version": MANIFEST_SCHEMA,
|
||||
"run_id": run_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"status": report["status"],
|
||||
"artifacts": [
|
||||
_artifact(run_root / "identity.json", "run-identity"),
|
||||
_artifact(run_root / "report.json", "benchmark-report"),
|
||||
],
|
||||
"frame_result_count": len(predictions),
|
||||
"frame_results_identity_sha256": _frame_results_identity(frames_root),
|
||||
"authority": identity["authority"],
|
||||
}
|
||||
_atomic_json(run_root / "manifest.json", manifest)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"run_id": run_id,
|
||||
"status": report["status"],
|
||||
"frame_count": len(predictions),
|
||||
"bev_map40": metrics["aggregates"]["bev_map40"],
|
||||
"3d_map40": metrics["aggregates"]["3d_map40"],
|
||||
"false_occupied_rate": metrics["aggregates"][
|
||||
"false_occupied_rate"
|
||||
],
|
||||
},
|
||||
sort_keys=True,
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def _run_frame(
|
||||
*,
|
||||
triton_url: str,
|
||||
frame_id: str,
|
||||
point_bytes: bytes,
|
||||
) -> PointPillarsFramePrediction:
|
||||
if not point_bytes or len(point_bytes) % 16:
|
||||
raise RuntimeError("KITTI point frame is not packed XYZI")
|
||||
native = np.frombuffer(point_bytes, dtype="<f4").reshape(-1, 4)
|
||||
if not np.isfinite(native).all() or native.shape[0] > MAXIMUM_POINTS:
|
||||
raise RuntimeError("KITTI point frame violates the model input contract")
|
||||
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,
|
||||
)
|
||||
boxes = decode_pointpillars_output(output_boxes, output_count)
|
||||
return PointPillarsFramePrediction(
|
||||
frame_id=frame_id,
|
||||
boxes=boxes,
|
||||
inference_ms=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_header = _canonical_json(header)
|
||||
request = urllib.request.Request(
|
||||
f"{triton_url.rstrip('/')}/v2/models/{MODEL_NAME}/infer",
|
||||
data=encoded_header + points_binary + count_binary,
|
||||
headers={
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Inference-Header-Content-Length": str(len(encoded_header)),
|
||||
},
|
||||
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 PointPillars 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 PointPillars output descriptor is invalid")
|
||||
byte_length = parameters.get("binary_data_size")
|
||||
if (
|
||||
isinstance(byte_length, bool)
|
||||
or not isinstance(byte_length, int)
|
||||
or byte_length < 1
|
||||
):
|
||||
raise RuntimeError("Triton PointPillars binary output size is invalid")
|
||||
binary = payload[offset : offset + byte_length]
|
||||
if len(binary) != byte_length:
|
||||
raise RuntimeError("Triton PointPillars binary output is truncated")
|
||||
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 binary payload is invalid")
|
||||
return decoded["output_boxes"], decoded["num_boxes"], elapsed_ms
|
||||
|
||||
|
||||
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 _validate_profile_and_provenance(
|
||||
profile: dict[str, Any],
|
||||
provenance: dict[str, Any],
|
||||
) -> None:
|
||||
detector = profile.get("detector")
|
||||
postprocessing = (
|
||||
detector.get("postprocessing") if isinstance(detector, dict) else None
|
||||
)
|
||||
if (
|
||||
profile.get("schema_version")
|
||||
!= "missioncore.l3-pointpillars-benchmark-profile/v1"
|
||||
or not isinstance(detector, dict)
|
||||
or not isinstance(postprocessing, dict)
|
||||
or detector.get("candidate_source_sha256") != EXPECTED_MODEL_SHA256
|
||||
or detector.get("candidate_label_sha256") != EXPECTED_LABEL_SHA256
|
||||
or detector.get("triton_model_name") != MODEL_NAME
|
||||
or detector.get("model_classes")
|
||||
!= ["Vehicle", "Pedestrian", "Cyclist"]
|
||||
or detector.get("point_cloud_range")
|
||||
!= list(POINTPILLARS_MODEL_POINT_CLOUD_RANGE)
|
||||
or detector.get("training_domain")
|
||||
!= "proprietary-solid-state-lidar"
|
||||
or detector.get("training_ground_truth_publicly_reproducible") is not False
|
||||
or detector.get("onnx_contract_sha256")
|
||||
!= EXPECTED_ONNX_CONTRACT_SHA256
|
||||
or postprocessing.get("embedded_score_threshold")
|
||||
!= POINTPILLARS_EMBEDDED_SCORE_THRESHOLD
|
||||
or postprocessing.get("embedded_contract_source")
|
||||
!= "onnx-node-attributes"
|
||||
):
|
||||
raise RuntimeError("L3 PointPillars profile is invalid")
|
||||
if (
|
||||
provenance.get("model_name") != MODEL_NAME
|
||||
or provenance.get("source_model_sha256") != EXPECTED_MODEL_SHA256
|
||||
or provenance.get("source_label_sha256") != EXPECTED_LABEL_SHA256
|
||||
or provenance.get("engine_sha256") != EXPECTED_ENGINE_SHA256
|
||||
or provenance.get("engine_built_on_target") is not True
|
||||
or provenance.get("worker_host_id") != "worker-006"
|
||||
):
|
||||
raise RuntimeError("live PointPillars provenance is invalid")
|
||||
|
||||
|
||||
def _read_worker_package(path: Path) -> dict[str, Any]:
|
||||
package_root = path.expanduser().resolve(strict=True)
|
||||
runtime_package_root = Path(__file__).resolve(strict=True).parents[1]
|
||||
if (
|
||||
not package_root.is_dir()
|
||||
or package_root != runtime_package_root
|
||||
or package_root.is_symlink()
|
||||
):
|
||||
raise RuntimeError("L3 runner is not executing from its declared worker package")
|
||||
manifest = _read_json(package_root / "manifest.json")
|
||||
identity = manifest.get("identity")
|
||||
identity_sha256 = manifest.get("identity_sha256")
|
||||
package_id = manifest.get("package_id")
|
||||
artifacts = manifest.get("artifacts")
|
||||
if (
|
||||
manifest.get("schema_version") != WORKER_PACKAGE_SCHEMA
|
||||
or not isinstance(identity, dict)
|
||||
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 package_root.name != package_id
|
||||
or not isinstance(artifacts, list)
|
||||
):
|
||||
raise RuntimeError("L3 worker package identity is invalid")
|
||||
expected_paths = identity.get("artifact_paths")
|
||||
if not isinstance(expected_paths, list) or not expected_paths:
|
||||
raise RuntimeError("L3 worker package file set is invalid")
|
||||
expected = set(expected_paths)
|
||||
actual = {
|
||||
member.relative_to(package_root).as_posix()
|
||||
for member in package_root.rglob("*")
|
||||
if member.is_file()
|
||||
}
|
||||
if actual != expected | {"manifest.json"} or len(artifacts) != len(expected):
|
||||
raise RuntimeError("L3 worker package file set changed")
|
||||
observed: set[str] = set()
|
||||
for descriptor in artifacts:
|
||||
if not isinstance(descriptor, dict):
|
||||
raise RuntimeError("L3 worker package artifact is invalid")
|
||||
relative = descriptor.get("path")
|
||||
artifact = package_root / str(relative)
|
||||
if (
|
||||
not isinstance(relative, str)
|
||||
or relative not in expected
|
||||
or relative in observed
|
||||
or Path(relative).is_absolute()
|
||||
or ".." in Path(relative).parts
|
||||
or not artifact.is_file()
|
||||
or artifact.is_symlink()
|
||||
or descriptor.get("kind") != relative
|
||||
or descriptor.get("byte_length") != artifact.stat().st_size
|
||||
or descriptor.get("sha256") != _sha256(artifact)
|
||||
):
|
||||
raise RuntimeError("L3 worker package artifact changed")
|
||||
observed.add(relative)
|
||||
if observed != expected:
|
||||
raise RuntimeError("L3 worker package artifact coverage changed")
|
||||
return manifest
|
||||
|
||||
|
||||
def _frame_payload(
|
||||
prediction: PointPillarsFramePrediction,
|
||||
*,
|
||||
point_sha256: str,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"schema_version": FRAME_SCHEMA,
|
||||
"frame_id": prediction.frame_id,
|
||||
"point_sha256": point_sha256,
|
||||
"inference_ms": prediction.inference_ms,
|
||||
"boxes": [
|
||||
{
|
||||
"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,
|
||||
}
|
||||
for box in prediction.boxes
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _read_frame(
|
||||
path: Path,
|
||||
*,
|
||||
frame_id: str,
|
||||
point_sha256: str,
|
||||
) -> PointPillarsFramePrediction:
|
||||
payload = _read_json(path)
|
||||
boxes = payload.get("boxes")
|
||||
if (
|
||||
payload.get("schema_version") != FRAME_SCHEMA
|
||||
or payload.get("frame_id") != frame_id
|
||||
or payload.get("point_sha256") != point_sha256
|
||||
or not isinstance(boxes, list)
|
||||
):
|
||||
raise RuntimeError("cached PointPillars frame result is invalid")
|
||||
try:
|
||||
decoded = tuple(PointPillarsBox(**box) for box in boxes)
|
||||
inference_ms = float(payload["inference_ms"])
|
||||
except (KeyError, TypeError, ValueError) as exc:
|
||||
raise RuntimeError("cached PointPillars frame result is invalid") from exc
|
||||
if not math.isfinite(inference_ms) or inference_ms <= 0.0:
|
||||
raise RuntimeError("cached PointPillars frame latency is invalid")
|
||||
return PointPillarsFramePrediction(
|
||||
frame_id=frame_id,
|
||||
boxes=decoded,
|
||||
inference_ms=inference_ms,
|
||||
)
|
||||
|
||||
|
||||
def _frame_results_identity(frames_root: Path) -> str:
|
||||
descriptors = [
|
||||
{
|
||||
"name": path.name,
|
||||
"sha256": _sha256(path),
|
||||
"byte_length": path.stat().st_size,
|
||||
}
|
||||
for path in sorted(frames_root.glob("*.json"))
|
||||
]
|
||||
return hashlib.sha256(_canonical_json(descriptors)).hexdigest()
|
||||
|
||||
|
||||
def _artifact(path: Path, role: str) -> dict[str, Any]:
|
||||
return {
|
||||
"path": path.name,
|
||||
"role": role,
|
||||
"media_type": "application/json",
|
||||
"sha256": _sha256(path),
|
||||
"byte_length": path.stat().st_size,
|
||||
}
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
try:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise RuntimeError(f"{path.name} is invalid") from exc
|
||||
if not isinstance(payload, dict):
|
||||
raise RuntimeError(f"{path.name} is not an object")
|
||||
return payload
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as source:
|
||||
for chunk in iter(lambda: source.read(8 * 1024**2), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _canonical_json(payload: Any) -> bytes:
|
||||
return json.dumps(
|
||||
payload,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def _write_once(path: Path, payload: dict[str, Any]) -> None:
|
||||
encoded = _canonical_json(payload) + b"\n"
|
||||
if path.exists():
|
||||
if path.read_bytes() != encoded:
|
||||
raise RuntimeError(f"{path.name} identity changed")
|
||||
return
|
||||
_atomic_bytes(path, encoded)
|
||||
|
||||
|
||||
def _atomic_json(path: Path, payload: dict[str, Any]) -> None:
|
||||
_atomic_bytes(path, _canonical_json(payload) + b"\n")
|
||||
|
||||
|
||||
def _atomic_bytes(path: Path, payload: bytes) -> None:
|
||||
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
descriptor, temporary = tempfile.mkstemp(
|
||||
dir=path.parent,
|
||||
prefix=f".{path.name}.",
|
||||
suffix=".tmp",
|
||||
)
|
||||
try:
|
||||
with os.fdopen(descriptor, "wb") as target:
|
||||
target.write(payload)
|
||||
target.flush()
|
||||
os.fsync(target.fileno())
|
||||
os.replace(temporary, path)
|
||||
except BaseException:
|
||||
with suppress(OSError):
|
||||
os.unlink(temporary)
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute one schema-only PointPillars smoke frame in canonical Triton."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import zipfile
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
from run_l3_pointpillars_public_baseline import (
|
||||
EXPECTED_ENGINE_SHA256,
|
||||
EXPECTED_LABEL_SHA256,
|
||||
EXPECTED_MODEL_SHA256,
|
||||
_atomic_json,
|
||||
_read_json,
|
||||
_require_triton_ready,
|
||||
_run_frame,
|
||||
_validate_profile_and_provenance,
|
||||
)
|
||||
|
||||
from k1link.datasets.kitti_3d_admission import (
|
||||
KITTI_3D_RELEASE_ROOT,
|
||||
KITTI_VELODYNE_ARCHIVE,
|
||||
read_kitti_3d_admission,
|
||||
read_kitti_standard_splits,
|
||||
)
|
||||
|
||||
SCHEMA = "missioncore.l3-pointpillars-live-smoke/v1"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--dataset-root", type=Path, required=True)
|
||||
parser.add_argument("--profile", type=Path, required=True)
|
||||
parser.add_argument("--model-provenance", type=Path, required=True)
|
||||
parser.add_argument("--frame-id")
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--triton-url", default="http://127.0.0.1:8000")
|
||||
args = parser.parse_args()
|
||||
|
||||
dataset_root = args.dataset_root.expanduser().absolute()
|
||||
admission = read_kitti_3d_admission(dataset_root)
|
||||
validation = read_kitti_standard_splits(dataset_root)["validation"]
|
||||
frame_id = args.frame_id or validation[0]
|
||||
if frame_id not in validation:
|
||||
raise RuntimeError("smoke frame is not in the admitted validation split")
|
||||
profile = _read_json(args.profile.resolve(strict=True))
|
||||
provenance = _read_json(args.model_provenance.resolve(strict=True))
|
||||
_validate_profile_and_provenance(profile, provenance)
|
||||
_require_triton_ready(args.triton_url)
|
||||
|
||||
points_archive = (
|
||||
dataset_root
|
||||
/ KITTI_3D_RELEASE_ROOT
|
||||
/ "archives"
|
||||
/ KITTI_VELODYNE_ARCHIVE
|
||||
)
|
||||
member = f"training/velodyne/{frame_id}.bin"
|
||||
try:
|
||||
with zipfile.ZipFile(points_archive.resolve(strict=True)) as source:
|
||||
point_bytes = source.read(member)
|
||||
except (OSError, KeyError, zipfile.BadZipFile) as exc:
|
||||
raise RuntimeError("smoke point frame could not be read") from exc
|
||||
prediction = _run_frame(
|
||||
triton_url=args.triton_url,
|
||||
frame_id=frame_id,
|
||||
point_bytes=point_bytes,
|
||||
)
|
||||
result = {
|
||||
"schema_version": SCHEMA,
|
||||
"status": "engine-schema-executed",
|
||||
"source_id": admission["source_id"],
|
||||
"dataset_release_identity_sha256": admission[
|
||||
"release_identity_sha256"
|
||||
],
|
||||
"frame_id": frame_id,
|
||||
"point_frame_sha256": hashlib.sha256(point_bytes).hexdigest(),
|
||||
"point_count": len(point_bytes) // 16,
|
||||
"post_nms_box_count": len(prediction.boxes),
|
||||
"observed_model_classes": sorted(
|
||||
{box.model_class for box in prediction.boxes}
|
||||
),
|
||||
"inference_ms": prediction.inference_ms,
|
||||
"model": {
|
||||
"name": "pointpillars",
|
||||
"source_model_sha256": EXPECTED_MODEL_SHA256,
|
||||
"source_label_sha256": EXPECTED_LABEL_SHA256,
|
||||
"engine_sha256": EXPECTED_ENGINE_SHA256,
|
||||
},
|
||||
"observed_at_utc": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
|
||||
"claim_boundary": {
|
||||
"accuracy_measured": False,
|
||||
"k1_transfer_evaluated": False,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
}
|
||||
_atomic_json(args.output.expanduser().absolute(), result)
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user