File size: 8,315 Bytes
6342806
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
"""Compute deduplication over the bevfusion pipeline + quantified savings.

The insight: when N models are evaluated on the same scenario, the sensor
simulation, BEV rasterization and fusion decode (the "backbone-equivalent"
stages of sensorflow.bevfusion — build_modality_map / fuse_maps / decode_bev)
are IDENTICAL across models. Only the model-specific head (confidence
recalibration + class thresholds here) differs. So:

    naive cost      = scenarios x models x (backbone + head)
    optimized cost  = scenarios x backbone  +  scenarios x models x head

The shared stage is cached content-addressed (cache.py) under
(scenario_id, sensor_version, preprocessing_version, backbone_version,
feature_schema_version); a version bump on any component is a miss by
construction. `benchmark` runs the real pipeline twice and reports MEASURED
timings and hit rates — no estimated numbers.

Honest scope: the "backbone" is the numpy bevfusion pipeline, not a neural
network, and the "heads" are deterministic calibration/threshold profiles.
The cost STRUCTURE (shared expensive stage, cheap per-model stage) is the
real thing being demonstrated; absolute times would scale with a real stack.
"""

from __future__ import annotations

import time
from typing import Dict, List, Optional
from uuid import uuid4

from sensorflow.bevfusion.fusion import build_modality_map, decode_bev, fuse_maps
from sensorflow.bevfusion.geometry import BEVGrid
from sensorflow.bevfusion.scenes import SceneSequence, generate_sequences
from sensorflow.bevfusion.sensors import (
    camera_rng, lidar_rng, simulate_camera, simulate_lidar,
)
from sensorflow.nextgen import store
from sensorflow.nextgen.cache import CacheKeyVersions, get_feature_cache
from sensorflow.nextgen.models import ComputeOptimizationResult

# Model-specific head profiles: deterministic post-processing over the shared
# fused features. Cheap by design — that is the point of the split.
HEAD_PROFILES: Dict[str, Dict[str, float]] = {
    "candidate-v4": {"confidence_gain": 1.05, "min_confidence": 0.30,
                     "vru_boost": 1.10},
    "candidate-v4.1": {"confidence_gain": 1.02, "min_confidence": 0.35,
                       "vru_boost": 1.15},
    "baseline-v3": {"confidence_gain": 1.00, "min_confidence": 0.40,
                    "vru_boost": 1.00},
}
VRU = {"pedestrian", "cyclist", "motorcycle"}


def compute_backbone_features(seq: SceneSequence, seed: int = 7,
                              seq_index: int = 0) -> List[Dict]:
    """Shared stage: sensor sim -> BEV rasterization -> fusion -> decode.
    REUSES the bevfusion pipeline verbatim; returns JSON-serializable
    per-frame decoded fused boxes (the cacheable IR)."""
    grid = BEVGrid()
    cam_gen = camera_rng(seed, seq_index)
    lid_gen = lidar_rng(seed, seq_index)
    frames: List[Dict] = []
    for frame in seq.frames:
        cam = simulate_camera(frame, seq, cam_gen)
        lid = simulate_lidar(frame, seq, lid_gen)
        fused = fuse_maps(build_modality_map(cam, grid, "camera"),
                          build_modality_map(lid, grid, "lidar"))
        decoded = decode_bev(fused)
        frames.append({"frame_id": frame.frame_id,
                       "boxes": [{"bbox_3d": b["bbox_3d"],
                                  "class_name": b["class_name"],
                                  "confidence": round(float(b["confidence"]), 4)}
                                 for b in decoded]})
    return frames


def run_model_head(features: List[Dict], model_version: str) -> Dict:
    """Model-specific stage: recalibrate + threshold the shared features."""
    prof = HEAD_PROFILES.get(model_version, HEAD_PROFILES["baseline-v3"])
    n_out = 0
    n_vru = 0
    conf_sum = 0.0
    for frame in features:
        for b in frame["boxes"]:
            c = b["confidence"] * prof["confidence_gain"]
            if b["class_name"] in VRU:
                c *= prof["vru_boost"]
            if c >= prof["min_confidence"]:
                n_out += 1
                conf_sum += min(c, 1.0)
                if b["class_name"] in VRU:
                    n_vru += 1
    return {"model_version": model_version, "n_detections": n_out,
            "n_vru": n_vru,
            "mean_confidence": round(conf_sum / n_out, 4) if n_out else None}


def evaluate_scenarios(sequences: List[SceneSequence], models: List[str],
                       versions: Optional[CacheKeyVersions] = None,
                       seed: int = 7) -> Dict:
    """Evaluate models x scenarios with backbone dedup. Returns measured
    per-stage timings + head outputs."""
    versions = versions or CacheKeyVersions()
    cache = get_feature_cache()
    backbone_times: List[float] = []
    head_times: List[float] = []
    outputs = []
    t_start = time.perf_counter()
    for si, seq in enumerate(sequences):
        key = versions.key_for(seq.sequence_id, kind="bev-features")

        def _compute(seq=seq, si=si):
            t0 = time.perf_counter()
            feats = compute_backbone_features(seq, seed=seed, seq_index=si)
            backbone_times.append(time.perf_counter() - t0)
            return feats

        features = cache.get_or_compute(key, _compute, kind="bev-features")
        for model in models:
            t0 = time.perf_counter()
            head = run_model_head(features, model)
            head_times.append(time.perf_counter() - t0)
            outputs.append({"scenario_id": seq.sequence_id, **head})
    wall = time.perf_counter() - t_start
    return {"outputs": outputs, "wall_s": wall,
            "backbone_times": backbone_times, "head_times": head_times}


def benchmark(n_scenarios: int = 6, frames_per_sequence: int = 20,
              models: Optional[List[str]] = None, seed: int = 7,
              versions: Optional[CacheKeyVersions] = None,
              persist: bool = True) -> ComputeOptimizationResult:
    """Real benchmark: cold pass (misses) + warm pass (hits) over the same
    suite; every timing measured, nothing estimated except the naive
    extrapolation which uses the MEASURED mean backbone/head times."""
    models = models or list(HEAD_PROFILES)
    versions = versions or CacheKeyVersions()
    cache = get_feature_cache()
    cache.reset_stats()

    sequences = generate_sequences(n_sequences=n_scenarios,
                                   frames_per_sequence=frames_per_sequence,
                                   seed=seed)
    cold = evaluate_scenarios(sequences, models, versions, seed)
    warm = evaluate_scenarios(sequences, models, versions, seed)

    stats = cache.stats
    all_backbone = cold["backbone_times"] + warm["backbone_times"]
    all_heads = cold["head_times"] + warm["head_times"]
    mean_backbone = sum(all_backbone) / len(all_backbone) if all_backbone else 0.0
    mean_head = sum(all_heads) / len(all_heads) if all_heads else 0.0

    n_units = 2 * len(sequences) * len(models)  # two passes
    naive_cost = n_units * (mean_backbone + mean_head)
    optimized_cost = cold["wall_s"] + warm["wall_s"]

    result = ComputeOptimizationResult(
        report_id=f"compute-{uuid4().hex[:8]}",
        n_scenarios=len(sequences), n_models=len(models),
        naive_full_inferences=n_units,
        optimized_backbone_computes=stats.misses,
        optimized_head_computes=len(all_heads),
        cache_hits=stats.hits, cache_misses=stats.misses,
        hit_rate=round(stats.hit_rate, 4),
        naive_cost_s=round(naive_cost, 4),
        optimized_cost_s=round(optimized_cost, 4),
        savings_ratio=round(1.0 - optimized_cost / naive_cost, 4) if naive_cost else 0.0,
        measured_backbone_s=round(mean_backbone, 4),
        measured_head_s=round(mean_head, 6),
        invalidation="content-addressed keys over (scenario_id, sensor, "
                     "preprocessing, backbone, feature-schema versions); any "
                     "version bump is a new key (cross-version reuse "
                     "impossible); LRU eviction bounds disk growth")
    if persist:
        store.write_json(result.model_dump(mode="json"), "compute",
                         f"{result.report_id}.json")
        store.write_json(result.model_dump(mode="json"), "compute", "latest.json")
    return result


def latest_report() -> Optional[Dict]:
    return store.read_json("compute", "latest.json")