Mahmoud-Wael/gpu-benchmark-counter
05
1"""2GPU benchmark for the people-counting pipeline.3 4Purpose: run the exact detection + tracking workload against a test video,5on whatever GPU the current machine has, and report whether that GPU can6keep up with the real camera in real time (25 fps @ 3200x1800).7 8GPU isolation strategy9----------------------10Wall-clock times (time.perf_counter) always include CPU dispatch latency.11To isolate ONLY the GPU's own work, we use torch.cuda.Event timers:12 - start_event.record() → drops a timestamp marker into the CUDA stream13 - end_event.record() → drops a second marker into the same stream14 - torch.cuda.synchronize() → blocks the CPU until ALL previously-queued15 GPU kernels have completed16 - start_event.elapsed_time(end_event) → returns the GPU-internal duration17 between the two markers in milliseconds, measured entirely on-device with18 no CPU-dispatch jitter or OS scheduling noise.19This is the only number that lets you compare GPU-to-GPU fairly, regardless20of the CPU speed or driver overhead of the rental machine.21 22Usage:23 python benchmark.py24 25Edit the CONFIG section below before each run.26"""27 28import time29import json30 31import cv232import numpy as np33import torch34from ultralytics import YOLO35 36# ---------------------------------------------------------------------------37# CONFIG - edit these for each test38# ---------------------------------------------------------------------------39 40VIDEO_PATH = "Test Video_Counter.mp4" # a real clip from your camera, ideally at native 3200x180041MODEL_PATH = "yolo26l.engine"42CONFIDENCE_THRESHOLD = 0.443DEVICE = "cuda" # "cuda" on a GPU instance, "cpu" for comparison44 45CAMERA_FPS = 25 # your real camera's frame rate46WARMUP_FRAMES = 10 # frames run first and excluded from timing47 48ROI_LIST = [49 np.array([(904, 245), (1931, 89), (2450, 1718), (1096, 1768)], np.int32),50 np.array([(481, 386), (479, 1292), (1033, 1264), (921, 291)], np.int32),51]52 53with open("regions.json", "r") as f:54 regions_data = json.load(f)55 REGION1 = np.array(regions_data["region1"], np.int32)56 REGION2 = np.array(regions_data["region2"], np.int32)57 58 59# ---------------------------------------------------------------------------60# Same tracking logic as the original script - unchanged61# ---------------------------------------------------------------------------62 63def point_in_polygon(point, polygon):64 return cv2.pointPolygonTest(polygon, point, False) >= 065 66 67def get_center_position(bbox):68 x1, y1, x2, y2 = bbox69 return (int((x1 + x2) / 2), int((y1 + y2) / 2))70 71 72def process_tracking(track_id, current_position, tracked_states, counted_ids, counts):73 in_region1 = point_in_polygon(current_position, REGION1)74 in_region2 = point_in_polygon(current_position, REGION2)75 current_region = 1 if in_region1 else (2 if in_region2 else None)76 77 previous_region = tracked_states.get(track_id, None)78 79 if track_id not in counted_ids and previous_region and current_region:80 if previous_region != current_region:81 if previous_region == 1 and current_region == 2:82 counts["in"] += 183 counted_ids.add(track_id)84 elif previous_region == 2 and current_region == 1:85 counts["out"] += 186 counted_ids.add(track_id)87 88 if current_region:89 tracked_states[track_id] = current_region90 91 92# ---------------------------------------------------------------------------93# GPU spec helper94# ---------------------------------------------------------------------------95 96def print_gpu_specs():97 """Print the name and key specs of the active CUDA device."""98 if not torch.cuda.is_available():99 print("No CUDA device detected - running on CPU.")100 return101 dev = torch.cuda.current_device()102 props = torch.cuda.get_device_properties(dev)103 vram_gb = props.total_memory / (1024 ** 3)104 print("=" * 50)105 print("GPU SPECIFICATIONS")106 print("=" * 50)107 print(f" GPU Name: {props.name}")108 print(f" CUDA Device Index: {dev}")109 print(f" Total VRAM: {vram_gb:.2f} GB")110 print(f" Multiprocessors (SM): {props.multi_processor_count}")111 print(f" CUDA Capability: {props.major}.{props.minor}")112 print(f" PyTorch CUDA version: {torch.version.cuda}")113 print("=" * 50)114 115 116# ---------------------------------------------------------------------------117# Benchmark run118# ---------------------------------------------------------------------------119 120def main():121 print_gpu_specs()122 123 print(f"\nLoading model on device: {DEVICE}")124 model = YOLO(MODEL_PATH)125 126 tracked_states = {}127 counted_ids = set()128 counts = {"in": 0, "out": 0}129 130 cap = cv2.VideoCapture(VIDEO_PATH)131 if not cap.isOpened():132 print(f"ERROR: could not open video at {VIDEO_PATH}")133 return134 135 native_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))136 native_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))137 print(f"Video resolution: {native_w}x{native_h}\n")138 139 frame_times = [] # seconds per frame, total (decode + inference), timed frames only140 decode_times = [] # seconds per frame spent just reading/decoding (CPU-bound)141 inference_times = [] # seconds per frame spent in model.track() + tracking logic (CPU clock, mostly GPU-bound)142 gpu_times_ms = [] # milliseconds per frame spent ONLY on the GPU itself (torch.cuda.Event, cuda only)143 frame_index = 0144 145 use_cuda_events = (DEVICE == "cuda") and torch.cuda.is_available()146 if use_cuda_events:147 # One pair of events reused each frame - record() is asynchronous and148 # does not block the CPU; elapsed_time() is called only after synchronize().149 start_event = torch.cuda.Event(enable_timing=True)150 end_event = torch.cuda.Event(enable_timing=True)151 152 # Wall-clock timestamp marking the start of the first timed frame's GPU work.153 # Used to compute total GPU session duration below.154 gpu_session_start = None155 gpu_session_end = None156 157 while cap.isOpened():158 t_decode_start = time.perf_counter()159 ret, frame = cap.read()160 t_decode_end = time.perf_counter()161 162 if not ret:163 break164 165 frame_index += 1166 is_warmup = frame_index <= WARMUP_FRAMES167 168 t_infer_start = time.perf_counter()169 170 if use_cuda_events:171 # record() inserts the marker into the current CUDA stream NOW.172 # No CPU work between start_event.record() and end_event.record()173 # except the YOLO call itself, so the elapsed time captures ONLY174 # the GPU kernels dispatched by model.track().175 start_event.record()176 177 results = model.track(178 frame,179 persist=True,180 tracker="bytetrack.yaml",181 classes=[0],182 conf=CONFIDENCE_THRESHOLD,183 verbose=False184 )185 186 if use_cuda_events:187 end_event.record()188 # synchronize() blocks the CPU until ALL GPU work queued up to this189 # point (including every kernel launched by model.track()) has190 # actually completed. Without this, elapsed_time() would measure an191 # incomplete range and t_infer_end would be taken before the GPU192 # finishes, making both the wall-clock and CUDA-event numbers wrong.193 torch.cuda.synchronize()194 gpu_ms = start_event.elapsed_time(end_event) # true GPU-side duration, ms195 196 if results[0].boxes is not None and results[0].boxes.id is not None:197 boxes = results[0].boxes.xyxy.cpu().numpy()198 track_ids = results[0].boxes.id.cpu().numpy().astype(int)199 200 for box, track_id in zip(boxes, track_ids):201 center_pos = get_center_position(box)202 203 is_inside_any_roi = any(point_in_polygon(center_pos, roi) for roi in ROI_LIST)204 if not is_inside_any_roi:205 continue206 207 process_tracking(track_id, center_pos, tracked_states, counted_ids, counts)208 209 t_infer_end = time.perf_counter()210 211 if not is_warmup:212 if gpu_session_start is None:213 gpu_session_start = t_infer_start # first timed frame214 gpu_session_end = t_infer_end # updated every timed frame215 216 decode_times.append(t_decode_end - t_decode_start)217 inference_times.append(t_infer_end - t_infer_start)218 frame_times.append((t_decode_end - t_decode_start) + (t_infer_end - t_infer_start))219 if use_cuda_events:220 gpu_times_ms.append(gpu_ms)221 222 if frame_index % 25 == 0:223 print(f"...processed {frame_index} frames")224 225 cap.release()226 227 # -----------------------------------------------------------------------228 # Report229 # -----------------------------------------------------------------------230 231 if not frame_times:232 print("No timed frames were collected (video too short?). Nothing to report.")233 return234 235 total_frames_in_video = frame_index236 timed_frames = len(frame_times)237 238 frame_times_ms = [t * 1000 for t in frame_times]239 decode_times_ms = [t * 1000 for t in decode_times]240 inference_times_ms = [t * 1000 for t in inference_times]241 242 avg_ms = sum(frame_times_ms) / len(frame_times_ms)243 min_ms = min(frame_times_ms)244 max_ms = max(frame_times_ms)245 avg_fps = 1000 / avg_ms246 worst_case_fps = 1000 / max_ms247 248 avg_decode_ms = sum(decode_times_ms) / len(decode_times_ms)249 avg_infer_ms = sum(inference_times_ms) / len(inference_times_ms)250 decode_share = (avg_decode_ms / avg_ms) * 100251 infer_share = (avg_infer_ms / avg_ms) * 100252 253 target_ms_per_frame = 1000 / CAMERA_FPS254 255 # Total wall-clock time the GPU section ran (from first timed inference start256 # to last timed inference end) - includes CPU overhead between frames but257 # gives a realistic "how long did the whole video take on this GPU" figure.258 total_gpu_session_s = (gpu_session_end - gpu_session_start) if (gpu_session_start is not None) else 0.0259 260 # Sum of pure CUDA event times - true GPU compute time with no CPU noise.261 total_pure_gpu_ms = sum(gpu_times_ms) if gpu_times_ms else 0.0262 263 print("\n" + "=" * 50)264 print("BENCHMARK RESULTS")265 print("=" * 50)266 print(f"Device: {DEVICE}")267 print(f"Resolution: {native_w}x{native_h}")268 print(f"Total frames in video: {total_frames_in_video}")269 print(f"Warmup frames skipped: {WARMUP_FRAMES}")270 print(f"Frames timed: {timed_frames}")271 print(f"Avg time/frame: {avg_ms:.1f} ms (total: decode + inference)")272 print(f"Min time/frame: {min_ms:.1f} ms")273 print(f"Max time/frame: {max_ms:.1f} ms")274 print(f"Avg throughput: {avg_fps:.1f} fps")275 print(f"Worst-case throughput: {worst_case_fps:.1f} fps")276 print(f"Total video duration (GPU session, wall-clock): {total_gpu_session_s:.2f} s")277 print("-" * 50)278 print("DECODE vs INFERENCE BREAKDOWN")279 print(f"Avg decode time: {avg_decode_ms:.1f} ms ({decode_share:.0f}% of total) - CPU-bound (cap.read)")280 print(f"Avg inference time: {avg_infer_ms:.1f} ms ({infer_share:.0f}% of total) - mostly GPU-bound (model.track)")281 282 if decode_share >= 25:283 print("NOTE: decode time is a significant chunk of total time - the CPU on this "284 "machine may be limiting your results as much as the GPU is. Compare this "285 "share across different rentals before trusting GPU-to-GPU differences.")286 else:287 print("Decode time is a small share of the total - the GPU is the dominant factor "288 "in these results, so GPU-to-GPU comparisons on this metric should be fair.")289 290 if gpu_times_ms:291 avg_gpu_ms = sum(gpu_times_ms) / len(gpu_times_ms)292 min_gpu_ms = min(gpu_times_ms)293 max_gpu_ms = max(gpu_times_ms)294 gpu_fps = 1000 / avg_gpu_ms295 print("-" * 50)296 print("PURE GPU TIME (torch.cuda.Event - excludes CPU dispatch/decode noise entirely)")297 print(f"Frames measured: {len(gpu_times_ms)}")298 print(f"Avg GPU time/frame: {avg_gpu_ms:.1f} ms → {gpu_fps:.1f} fps at this pace")299 print(f"Min GPU time/frame: {min_gpu_ms:.1f} ms (fastest single frame the GPU processed)")300 print(f"Max GPU time/frame: {max_gpu_ms:.1f} ms (slowest single frame the GPU processed)")301 print(f"Total pure GPU time: {total_pure_gpu_ms:.1f} ms ({total_pure_gpu_ms/1000:.2f} s)")302 print("This is the number to trust most when comparing GPUs directly - it reflects")303 print("only the GPU's own work, regardless of what CPU it happened to be paired with.")304 305 print("-" * 50)306 print(f"Camera requires: {CAMERA_FPS} fps ({target_ms_per_frame:.1f} ms/frame budget)")307 308 if avg_ms <= target_ms_per_frame:309 headroom = (target_ms_per_frame / avg_ms - 1) * 100310 print(f"RESULT: Keeps up with real-time on average ({headroom:.0f}% headroom).")311 else:312 deficit = (avg_ms / target_ms_per_frame - 1) * 100313 print(f"RESULT: Falls behind real-time on average (needs {deficit:.0f}% more speed).")314 315 if max_ms > target_ms_per_frame:316 print("NOTE: worst-case frame time exceeds the real-time budget - expect occasional "317 "stutter/backlog even if the average looks fine.")318 319 print(f"\nFinal counts (for reference, not the point of this test): "320 f"in={counts['in']}, out={counts['out']}")321 322 323if __name__ == "__main__":324 main()