Spaces:
Paused
Paused
Download sensorflow/vitis/hil.py from whassan/SensorflowStudio: direct link, hf CLI and curl.
- Browser
- Download file 21.2 kB
-
https://huggingface.co/spaces/whassan/SensorflowStudio/resolve/main/sensorflow/vitis/hil.py
- Command line
-
hf download hf://spaces/whassan/SensorflowStudio/sensorflow/vitis/hil.py
-
curl -L -o hil.py https://huggingface.co/spaces/whassan/SensorflowStudio/resolve/main/sensorflow/vitis/hil.py
21.2 kB
| """HIL quantization-gap regression detection. | |
| Runs the SAME image-space detection pipeline through the float32 reference | |
| backend and through the Vitis emulated backend (fixed-point + streaming + | |
| LUT constraints) on identical frames, then answers three questions: | |
| 1. How far apart are the two paths, per object? (confidence drift, position | |
| drift, IoU, dropped/spurious detections, class flips) | |
| 2. WHY are they apart? One-factor-at-a-time ablation isolates precision | |
| loss (bit-width), streaming-depth artifacts (XFCVDEPTH), and HLS LUT | |
| approximation error, and attributes the observed gap to each factor. | |
| 3. Is the gap a REGRESSION in the sequential-testing sense? Paired | |
| per-object correctness deltas feed sensorflow.seqeval's anytime-valid | |
| PairedSequentialTest when importable; otherwise a local paired-t | |
| confidence-interval fallback produces the same three-outcome verdict. | |
| Frames come from the bevfusion synthetic scene generator (read-only import), | |
| so day/night/rain cohorts and planted occlusions are inherited for free. | |
| """ | |
| from __future__ import annotations | |
| import time | |
| from typing import Dict, List, Optional, Tuple | |
| import numpy as np | |
| from scipy import ndimage | |
| from sensorflow.bevfusion.scenes import SceneSequence, generate_sequences | |
| from sensorflow.vitis.backend import PipelineConfig, VisionBackend, get_backend | |
| from sensorflow.vitis.render import ( | |
| BEV_H, BEV_W, ego_to_bev_px, render_bev_frame, | |
| ) | |
| from sensorflow.vitis.store import new_id, save_run | |
| # Detection pipeline constants (identical for every backend). | |
| DETECT_THRESHOLD = 0.30 | |
| MIN_BLOB_AREA = 3 | |
| PROC_H, PROC_W = BEV_H // 2, BEV_W // 2 # detection runs at half resolution | |
| IOU_CORRECT = 0.30 # detection counts as correct vs GT | |
| # Ablation sweep uses I=4 integer bits (signal is in [0,1] with small gains). | |
| SWEEP_INT_BITS = 4 | |
| HIGH_PRECISION = (24, 8) # "constraint off" precision for ablation legs | |
| # -------------------------------------------------------------------------- | |
| # Sequential verdict: seqeval if available, local paired-t fallback otherwise | |
| # -------------------------------------------------------------------------- | |
| def _try_seqeval_test(delta: float, alpha: float): | |
| try: | |
| from sensorflow.seqeval.sequential import PairedSequentialTest | |
| return PairedSequentialTest(delta=delta, alpha=alpha) | |
| except Exception: | |
| return None | |
| def _fallback_verdict(frame_deltas: List[float], delta: float, | |
| alpha: float) -> Dict: | |
| """Paired-t CI on per-frame mean correctness deltas (d = vitis - ref). | |
| REGRESSION if the CI sits entirely below -delta, PASS if entirely above, | |
| else INSUFFICIENT_EVIDENCE. Not anytime-valid (fixed-n), which is why | |
| seqeval is preferred when importable. | |
| """ | |
| d = np.asarray(frame_deltas, dtype=np.float64) | |
| n = d.size | |
| if n < 3: | |
| return {"decision": "INSUFFICIENT_EVIDENCE", "mean_delta": float(d.mean()) if n else 0.0, | |
| "ci": [-1.0, 1.0], "n": int(n), "method": "paired_t_fallback"} | |
| from scipy import stats | |
| mean = float(d.mean()) | |
| sem = float(d.std(ddof=1)) / np.sqrt(n) | |
| tcrit = float(stats.t.ppf(1.0 - alpha / 2.0, n - 1)) | |
| lo, hi = mean - tcrit * max(sem, 1e-12), mean + tcrit * max(sem, 1e-12) | |
| if hi < -delta: | |
| decision = "REGRESSION" | |
| elif lo > -delta: | |
| decision = "PASS" | |
| else: | |
| decision = "INSUFFICIENT_EVIDENCE" | |
| return {"decision": decision, "mean_delta": mean, "ci": [lo, hi], | |
| "n": int(n), "method": "paired_t_fallback"} | |
| def sequential_verdict(frame_deltas: List[float], | |
| object_pairs: List[Tuple[bool, bool]], | |
| delta: float = 0.02, alpha: float = 0.05) -> Dict: | |
| """Three-outcome verdict on paired ref-vs-vitis correctness deltas.""" | |
| test = _try_seqeval_test(delta, alpha) | |
| if test is not None: | |
| try: | |
| test.update_clusters(frame_deltas) | |
| if object_pairs: | |
| b = np.array([p[0] for p in object_pairs], dtype=bool) | |
| c = np.array([p[1] for p in object_pairs], dtype=bool) | |
| test.record_objects(b, c) | |
| decision = test.evaluate() | |
| lo, hi = test.delta_interval() | |
| return {"decision": decision, | |
| "mean_delta": float(np.mean(frame_deltas)) if frame_deltas else 0.0, | |
| "ci": [float(lo), float(hi)], "n": len(frame_deltas), | |
| "method": "seqeval_anytime_valid"} | |
| except Exception: | |
| pass # seqeval mid-edit: degrade to the local fallback | |
| return _fallback_verdict(frame_deltas, delta, alpha) | |
| # -------------------------------------------------------------------------- | |
| # The detection pipeline (identical logic; only the backend differs) | |
| # -------------------------------------------------------------------------- | |
| def _classify_area(area_px: float) -> str: | |
| if area_px < 9: | |
| return "pedestrian" | |
| if area_px < 16: | |
| return "cyclist" | |
| if area_px < 30: | |
| return "vehicle" | |
| return "truck" | |
| def detect_frame(img: np.ndarray, backend: VisionBackend, | |
| gain: float = 1.4) -> List[Dict]: | |
| """Preprocess through backend ops, then extract blob detections. | |
| Every arithmetic step before thresholding runs through the backend, so | |
| fixed-point/streaming/LUT effects propagate into the detections. | |
| """ | |
| x = backend.gain_exposure(img, gain=gain, offset=-0.05) | |
| x = backend.gaussian_filter(x, sigma=1.0) | |
| x = backend.resize(x, PROC_H, PROC_W, method="bilinear") | |
| mask = x > DETECT_THRESHOLD | |
| labels, n = ndimage.label(mask) | |
| dets: List[Dict] = [] | |
| for i in range(1, n + 1): | |
| ys, xs = np.nonzero(labels == i) | |
| area = float(ys.size) | |
| if area < MIN_BLOB_AREA: | |
| continue | |
| vals = x[ys, xs] | |
| dets.append({ | |
| "cy": float(ys.mean()), "cx": float(xs.mean()), | |
| "h": float(ys.max() - ys.min() + 1), | |
| "w": float(xs.max() - xs.min() + 1), | |
| "area": area, | |
| "confidence": float(np.clip(vals.mean(), 0.0, 1.0)), | |
| "class_name": _classify_area(area), | |
| }) | |
| return dets | |
| def _gt_in_proc_px(gt) -> Dict: | |
| x, y, _, l, w, _, _ = gt.bbox_3d | |
| r, c = ego_to_bev_px(x, y) | |
| scale_r, scale_c = PROC_H / BEV_H, PROC_W / BEV_W | |
| return {"cy": r * scale_r, "cx": c * scale_c, | |
| "h": max(1.0, l / (80.0 / BEV_H) * scale_r), | |
| "w": max(1.0, w / (50.0 / BEV_W) * scale_c), | |
| "class_name": gt.class_name, "instance_id": gt.instance_id, | |
| "occluded": gt.occluded} | |
| def _iou(a: Dict, b: Dict) -> float: | |
| ax0, ax1 = a["cx"] - a["w"] / 2, a["cx"] + a["w"] / 2 | |
| ay0, ay1 = a["cy"] - a["h"] / 2, a["cy"] + a["h"] / 2 | |
| bx0, bx1 = b["cx"] - b["w"] / 2, b["cx"] + b["w"] / 2 | |
| by0, by1 = b["cy"] - b["h"] / 2, b["cy"] + b["h"] / 2 | |
| iw = max(0.0, min(ax1, bx1) - max(ax0, bx0)) | |
| ih = max(0.0, min(ay1, by1) - max(ay0, by0)) | |
| inter = iw * ih | |
| union = a["w"] * a["h"] + b["w"] * b["h"] - inter | |
| return inter / union if union > 0 else 0.0 | |
| def _match_to_gt(dets: List[Dict], gts: List[Dict], | |
| max_dist: float = 6.0) -> Dict[str, Dict]: | |
| """Greedy nearest match detection -> GT. Returns {instance_id: det}.""" | |
| matched: Dict[str, Dict] = {} | |
| used = set() | |
| for gt in gts: | |
| best, best_d = None, max_dist | |
| for j, det in enumerate(dets): | |
| if j in used: | |
| continue | |
| d = np.hypot(det["cy"] - gt["cy"], det["cx"] - gt["cx"]) | |
| if d < best_d: | |
| best, best_d = j, d | |
| if best is not None: | |
| used.add(best) | |
| matched[gt["instance_id"]] = dets[best] | |
| return matched | |
| # -------------------------------------------------------------------------- | |
| # Paired comparison over a scene set | |
| # -------------------------------------------------------------------------- | |
| def run_paired_comparison(sequences: List[SceneSequence], config: PipelineConfig, | |
| seed: int) -> Dict: | |
| """Run reference + vitis_emulated on identical frames; pair per object.""" | |
| ref = get_backend("reference") | |
| vit = get_backend("vitis_emulated", config) | |
| frame_deltas: List[float] = [] | |
| object_pairs: List[Tuple[bool, bool]] = [] | |
| per_object: List[Dict] = [] | |
| dropped = spurious = class_flips = 0 | |
| ref_detected = vit_detected = total_gt = 0 | |
| conf_drifts: List[float] = [] | |
| pos_drifts: List[float] = [] | |
| ious: List[float] = [] | |
| cohort_acc: Dict[str, List[float]] = {} | |
| for seq in sequences: | |
| cohort = f"{seq.time_of_day}/{seq.weather}" | |
| for frame in seq.frames: | |
| gts = [_gt_in_proc_px(g) for g in frame.gt] | |
| if not gts: | |
| continue | |
| img = render_bev_frame(frame, seq, seed) | |
| dets_ref = detect_frame(img, ref) | |
| dets_vit = detect_frame(img, vit) | |
| m_ref = _match_to_gt(dets_ref, gts) | |
| m_vit = _match_to_gt(dets_vit, gts) | |
| spurious += max(0, (len(dets_vit) - len(m_vit)) - | |
| (len(dets_ref) - len(m_ref))) | |
| frame_b, frame_c = [], [] | |
| for gt in gts: | |
| total_gt += 1 | |
| iid = gt["instance_id"] | |
| dr, dv = m_ref.get(iid), m_vit.get(iid) | |
| b_ok = bool(dr and _iou(dr, gt) >= IOU_CORRECT) | |
| c_ok = bool(dv and _iou(dv, gt) >= IOU_CORRECT) | |
| ref_detected += int(dr is not None) | |
| vit_detected += int(dv is not None) | |
| if dr and not dv: | |
| dropped += 1 | |
| object_pairs.append((b_ok, c_ok)) | |
| frame_b.append(b_ok) | |
| frame_c.append(c_ok) | |
| rec = {"instance_id": iid, "frame_id": frame.frame_id, | |
| "cohort": cohort, "occluded": gt["occluded"], | |
| "ref_detected": dr is not None, | |
| "vitis_detected": dv is not None, | |
| "ref_correct": b_ok, "vitis_correct": c_ok} | |
| if dr and dv: | |
| cd = dv["confidence"] - dr["confidence"] | |
| pd = float(np.hypot(dv["cy"] - dr["cy"], dv["cx"] - dr["cx"])) | |
| pair_iou = _iou(dr, dv) | |
| flip = dr["class_name"] != dv["class_name"] | |
| class_flips += int(flip) | |
| conf_drifts.append(cd) | |
| pos_drifts.append(pd) | |
| ious.append(pair_iou) | |
| rec.update({"confidence_drift": round(cd, 4), | |
| "position_drift_px": round(pd, 3), | |
| "pair_iou": round(pair_iou, 3), | |
| "class_flip": flip, | |
| "ref_class": dr["class_name"], | |
| "vitis_class": dv["class_name"]}) | |
| per_object.append(rec) | |
| d = float(np.mean(frame_c)) - float(np.mean(frame_b)) | |
| frame_deltas.append(d) | |
| cohort_acc.setdefault(cohort, []).append(d) | |
| n_paired = len(conf_drifts) | |
| gap_score = _gap_score(dropped, class_flips, total_gt, conf_drifts, ious) | |
| return { | |
| "config": config.to_dict(), | |
| "totals": { | |
| "gt_objects": total_gt, | |
| "ref_detected": ref_detected, | |
| "vitis_detected": vit_detected, | |
| "paired_detections": n_paired, | |
| "dropped_by_vitis": dropped, | |
| "spurious_in_vitis": spurious, | |
| "class_flips": class_flips, | |
| }, | |
| "drift": { | |
| "mean_confidence_drift": round(float(np.mean(conf_drifts)), 5) if conf_drifts else 0.0, | |
| "mean_abs_confidence_drift": round(float(np.mean(np.abs(conf_drifts))), 5) if conf_drifts else 0.0, | |
| "mean_position_drift_px": round(float(np.mean(pos_drifts)), 4) if pos_drifts else 0.0, | |
| "mean_pair_iou": round(float(np.mean(ious)), 4) if ious else 1.0, | |
| }, | |
| "gap_score": round(gap_score, 5), | |
| "frame_deltas": [round(d, 5) for d in frame_deltas], | |
| "object_pairs": object_pairs, | |
| "per_object": per_object, | |
| "cohort_delta": {k: round(float(np.mean(v)), 5) | |
| for k, v in sorted(cohort_acc.items())}, | |
| } | |
| def _gap_score(dropped: int, flips: int, total: int, conf_drifts: List[float], | |
| ious: List[float]) -> float: | |
| """Scalar quantization-gap severity in [0, ~1]; higher is worse.""" | |
| if total == 0: | |
| return 0.0 | |
| drop_rate = dropped / total | |
| flip_rate = flips / total | |
| conf = float(np.mean(np.abs(conf_drifts))) if conf_drifts else 0.0 | |
| iou_loss = 1.0 - (float(np.mean(ious)) if ious else 1.0) | |
| return 0.45 * drop_rate + 0.2 * flip_rate + 0.2 * conf + 0.15 * iou_loss | |
| # -------------------------------------------------------------------------- | |
| # Ablation + sweep + full run | |
| # -------------------------------------------------------------------------- | |
| def _ablation_configs(config: PipelineConfig) -> Dict[str, PipelineConfig]: | |
| """One-factor-at-a-time legs: each isolates a single constraint.""" | |
| w, i = config.precision_for("default") | |
| big_depth = 1 << 16 | |
| return { | |
| "precision_only": PipelineConfig( | |
| precision={"default": (w, i)}, max_line_buffer_depth=big_depth, | |
| use_lut_approx=False, device=config.device), | |
| "streaming_only": PipelineConfig( | |
| precision={"default": HIGH_PRECISION}, | |
| max_line_buffer_depth=config.max_line_buffer_depth, | |
| use_lut_approx=False, device=config.device), | |
| "hls_approx_only": PipelineConfig( | |
| precision={"default": HIGH_PRECISION}, max_line_buffer_depth=big_depth, | |
| use_lut_approx=True, lut_bits=config.lut_bits, device=config.device), | |
| } | |
| def run_hil(n_sequences: int = 4, frames_per_sequence: int = 14, seed: int = 7, | |
| width_bits: int = 10, int_bits: int = SWEEP_INT_BITS, | |
| max_line_buffer_depth: int = 2048, use_lut_approx: bool = True, | |
| lut_bits: int = 8, device: str = "versal-ai-edge", | |
| regression_delta: float = 0.02, alpha: float = 0.05, | |
| run_ablation: bool = True, persist: bool = True) -> Dict: | |
| """Full HIL run: paired comparison + ablation attribution + verdict.""" | |
| from sensorflow.vitis.backend import DeviceConfig | |
| t0 = time.perf_counter() | |
| dev = DeviceConfig(name=device) | |
| config = PipelineConfig(precision={"default": (width_bits, int_bits)}, | |
| max_line_buffer_depth=max_line_buffer_depth, | |
| use_lut_approx=use_lut_approx, lut_bits=lut_bits, | |
| device=dev) | |
| sequences = generate_sequences(n_sequences, frames_per_sequence, seed) | |
| full = run_paired_comparison(sequences, config, seed) | |
| verdict = sequential_verdict(full["frame_deltas"], full["object_pairs"], | |
| delta=regression_delta, alpha=alpha) | |
| ablation = None | |
| if run_ablation: | |
| legs = {} | |
| for name, leg_cfg in _ablation_configs(config).items(): | |
| leg = run_paired_comparison(sequences, leg_cfg, seed) | |
| legs[name] = {"gap_score": leg["gap_score"], | |
| "totals": leg["totals"], "drift": leg["drift"]} | |
| total_leg_gap = sum(l["gap_score"] for l in legs.values()) | |
| attribution = {name: (round(l["gap_score"] / total_leg_gap, 4) | |
| if total_leg_gap > 0 else 0.0) | |
| for name, l in legs.items()} | |
| ablation = {"legs": legs, "attribution": attribution, | |
| "note": ("One-factor-at-a-time: each leg enables exactly " | |
| "one constraint; attribution is that leg's gap " | |
| "share of the summed single-factor gaps.")} | |
| run_id = new_id("hilrun") | |
| payload = { | |
| "run_id": run_id, | |
| "created_at": time.strftime("%Y-%m-%dT%H:%M:%S"), | |
| "kind": "hil", | |
| "params": {"n_sequences": n_sequences, | |
| "frames_per_sequence": frames_per_sequence, "seed": seed, | |
| "width_bits": width_bits, "int_bits": int_bits, | |
| "max_line_buffer_depth": max_line_buffer_depth, | |
| "use_lut_approx": use_lut_approx, "lut_bits": lut_bits, | |
| "device": device, "regression_delta": regression_delta, | |
| "alpha": alpha}, | |
| "comparison": {k: v for k, v in full.items() | |
| if k not in ("object_pairs", "per_object", "frame_deltas")}, | |
| "per_object_sample": full["per_object"][:60], | |
| "verdict": verdict, | |
| "ablation": ablation, | |
| "elapsed_s": round(time.perf_counter() - t0, 2), | |
| "summary": { | |
| "decision": verdict["decision"], | |
| "gap_score": full["gap_score"], | |
| "dropped": full["totals"]["dropped_by_vitis"], | |
| "width_bits": width_bits, | |
| }, | |
| "emulation_note": ("vitis_emulated is a constraint-faithful CPU " | |
| "emulator; no FPGA hardware was used."), | |
| } | |
| if persist: | |
| save_run("hil", run_id, payload) | |
| return payload | |
| def run_bitwidth_sweep(n_sequences: int = 4, frames_per_sequence: int = 14, | |
| seed: int = 7, widths: Optional[List[int]] = None, | |
| int_bits: int = SWEEP_INT_BITS, | |
| max_line_buffer_depth: int = 2048, | |
| use_lut_approx: bool = True, lut_bits: int = 8, | |
| device: str = "versal-ai-edge", | |
| regression_delta: float = 0.02, alpha: float = 0.05, | |
| persist: bool = True) -> Dict: | |
| """Sweep W, find the minimal bit-width whose verdict is not REGRESSION.""" | |
| from sensorflow.vitis.backend import DeviceConfig | |
| t0 = time.perf_counter() | |
| widths = sorted(widths or [8, 10, 12, 14, 16], reverse=True) | |
| dev = DeviceConfig(name=device) | |
| sequences = generate_sequences(n_sequences, frames_per_sequence, seed) | |
| points = [] | |
| minimal_passing = None | |
| for w in widths: | |
| cfg = PipelineConfig(precision={"default": (w, int_bits)}, | |
| max_line_buffer_depth=max_line_buffer_depth, | |
| use_lut_approx=use_lut_approx, lut_bits=lut_bits, | |
| device=dev) | |
| cmp_ = run_paired_comparison(sequences, cfg, seed) | |
| verdict = sequential_verdict(cmp_["frame_deltas"], cmp_["object_pairs"], | |
| delta=regression_delta, alpha=alpha) | |
| points.append({ | |
| "width_bits": w, "int_bits": int_bits, | |
| "gap_score": cmp_["gap_score"], | |
| "dropped": cmp_["totals"]["dropped_by_vitis"], | |
| "class_flips": cmp_["totals"]["class_flips"], | |
| "mean_abs_confidence_drift": cmp_["drift"]["mean_abs_confidence_drift"], | |
| "mean_position_drift_px": cmp_["drift"]["mean_position_drift_px"], | |
| "mean_pair_iou": cmp_["drift"]["mean_pair_iou"], | |
| "decision": verdict["decision"], | |
| "mean_delta": verdict["mean_delta"], | |
| }) | |
| by_width = sorted(points, key=lambda p: p["width_bits"]) | |
| for p in by_width: # prefer the smallest width with an affirmative PASS | |
| if p["decision"] == "PASS": | |
| minimal_passing = {"width_bits": p["width_bits"], | |
| "int_bits": int_bits, "decision": "PASS"} | |
| break | |
| if minimal_passing is None: | |
| for p in by_width: | |
| if p["decision"] != "REGRESSION": | |
| minimal_passing = {"width_bits": p["width_bits"], | |
| "int_bits": int_bits, | |
| "decision": p["decision"], | |
| "note": ("No width reached an affirmative " | |
| "PASS; this is the smallest width " | |
| "without a detected regression.")} | |
| break | |
| run_id = new_id("hilsweep") | |
| payload = { | |
| "run_id": run_id, | |
| "created_at": time.strftime("%Y-%m-%dT%H:%M:%S"), | |
| "kind": "hil_sweep", | |
| "params": {"n_sequences": n_sequences, | |
| "frames_per_sequence": frames_per_sequence, "seed": seed, | |
| "widths": widths, "int_bits": int_bits, | |
| "max_line_buffer_depth": max_line_buffer_depth, | |
| "use_lut_approx": use_lut_approx, | |
| "regression_delta": regression_delta, "alpha": alpha, | |
| "device": device}, | |
| "points": sorted(points, key=lambda p: p["width_bits"]), | |
| "minimal_passing_config": minimal_passing, | |
| "elapsed_s": round(time.perf_counter() - t0, 2), | |
| "summary": {"minimal_passing_width": (minimal_passing or {}).get("width_bits"), | |
| "widths": widths}, | |
| "emulation_note": ("Sweep executed on the vitis_emulated CPU backend; " | |
| "no FPGA hardware was used."), | |
| } | |
| if persist: | |
| save_run("hil", run_id, payload) | |
| return payload | |