Spaces:
Running
Running
Download ocr-training/scripts/benchmark_orchestrator.py from phucsd/cham-ocr-studio: direct link, hf CLI and curl.
- Browser
- Download file 20.5 kB
-
https://huggingface.co/spaces/phucsd/cham-ocr-studio/resolve/main/ocr-training/scripts/benchmark_orchestrator.py
- Command line
-
hf download hf://spaces/phucsd/cham-ocr-studio/ocr-training/scripts/benchmark_orchestrator.py
-
curl -L -o benchmark_orchestrator.py https://huggingface.co/spaces/phucsd/cham-ocr-studio/resolve/main/ocr-training/scripts/benchmark_orchestrator.py
20.5 kB
| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| """ | |
| Automated Multi-GPU Benchmark & Profiling Orchestrator. | |
| Discovers optimal num_workers, sweeps batch sizes, performs binary search on OOM, | |
| and computes 40-epoch economic projections (Time, Cost, IPS/$). | |
| """ | |
| import os | |
| import sys | |
| import time | |
| import json | |
| import argparse | |
| import subprocess | |
| import shutil | |
| if sys.stdout and hasattr(sys.stdout, 'reconfigure'): | |
| try: | |
| sys.stdout.reconfigure(encoding='utf-8') | |
| except Exception: | |
| pass | |
| if sys.stderr and hasattr(sys.stderr, 'reconfigure'): | |
| try: | |
| sys.stderr.reconfigure(encoding='utf-8') | |
| except Exception: | |
| pass | |
| PRICING_TABLE = { | |
| "A100": 2.19, | |
| "A100-SXM4-40GB": 2.19, | |
| "NVIDIA A100": 2.19, | |
| "L4": 0.79, | |
| "NVIDIA L4": 0.79, | |
| "L40S": 2.14, | |
| "NVIDIA L40S": 2.14, | |
| "T4": 0.40, | |
| "NVIDIA T4": 0.40, | |
| } | |
| TOTAL_DATASET_SAMPLES = 250000 | |
| TOTAL_EPOCHS = 40 | |
| TOTAL_TRAIN_SAMPLES = TOTAL_DATASET_SAMPLES * TOTAL_EPOCHS # 10,000,000 | |
| def detect_gpu_name(): | |
| try: | |
| res = subprocess.run( | |
| ["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"], | |
| capture_output=True, text=True, timeout=2.0 | |
| ) | |
| if res.returncode == 0 and res.stdout.strip(): | |
| return res.stdout.strip().split("\n")[0] | |
| except Exception: | |
| pass | |
| return "Unknown_GPU" | |
| def round_to_multiple_of_32(val): | |
| return max(32, (int(val) // 32) * 32) | |
| def round_to_multiple_of_16(val): | |
| return max(16, (int(val) // 16) * 16) | |
| def run_single_trial(python_bin, worker_script, config_path, batch_size, num_workers, warmup_steps, measure_steps, output_dir): | |
| trial_json = os.path.join(output_dir, f"trial_b{batch_size}_w{num_workers}.json") | |
| if os.path.exists(trial_json): | |
| try: | |
| os.remove(trial_json) | |
| except Exception: | |
| pass | |
| cmd = [ | |
| python_bin, | |
| worker_script, | |
| "-c", config_path, | |
| "-b", str(batch_size), | |
| "-w", str(num_workers), | |
| "--warmup_steps", str(warmup_steps), | |
| "--measure_steps", str(measure_steps), | |
| "-o", trial_json | |
| ] | |
| print(f"\n>>> [Trial] Launching subprocess: Batch={batch_size}, Workers={num_workers} (Warmup={warmup_steps}, Measure={measure_steps})", flush=True) | |
| t0 = time.time() | |
| res = subprocess.run(cmd) | |
| elapsed = time.time() - t0 | |
| if os.path.exists(trial_json): | |
| try: | |
| with open(trial_json, 'r', encoding='utf-8') as f: | |
| data = json.load(f) | |
| data["elapsed_wall_time"] = round(elapsed, 2) | |
| return data | |
| except Exception as e: | |
| print(f"Failed to read trial json: {e}") | |
| # Fallback if trial json was not written | |
| status = "OOM" if res.returncode in (137, -9) else "CRASH" | |
| return { | |
| "batch_size": batch_size, | |
| "num_workers": num_workers, | |
| "warmup_steps": warmup_steps, | |
| "measure_steps": measure_steps, | |
| "status": status, | |
| "ips": 0.0, | |
| "avg_reader_cost": 0.0, | |
| "avg_batch_cost": 0.0, | |
| "reader_ratio": 0.0, | |
| "peak_vram_mb": 0.0, | |
| "total_vram_mb": 0.0, | |
| "vram_pct": 0.0, | |
| "avg_gpu_util": 0.0, | |
| "avg_cpu_pct": 0.0, | |
| "error": f"Process exited with code {res.returncode}", | |
| "elapsed_wall_time": round(elapsed, 2) | |
| } | |
| def compute_economics(trial, cost_per_hour): | |
| ips = trial.get("ips", 0.0) | |
| if ips > 0: | |
| est_hours = TOTAL_TRAIN_SAMPLES / (ips * 3600.0) | |
| est_cost = est_hours * cost_per_hour | |
| ips_per_dollar = ips / cost_per_hour | |
| else: | |
| est_hours = 0.0 | |
| est_cost = 0.0 | |
| ips_per_dollar = 0.0 | |
| trial["est_40_epoch_hours"] = round(est_hours, 2) | |
| trial["est_40_epoch_cost"] = round(est_cost, 2) | |
| trial["ips_per_dollar"] = round(ips_per_dollar, 2) | |
| trial["cost_per_hour"] = cost_per_hour | |
| return trial | |
| def run_benchmark_suite(config_path, gpu_name=None, cost_per_hour=None, python_bin=None, warmup_steps=100, measure_steps=300, output_dir="benchmark_results"): | |
| os.makedirs(output_dir, exist_ok=True) | |
| if not python_bin: | |
| python_bin = sys.executable | |
| script_dir = os.path.dirname(os.path.abspath(__file__)) | |
| worker_script = os.path.join(script_dir, "benchmark_worker.py") | |
| if not os.path.exists(worker_script): | |
| raise FileNotFoundError(f"Worker script not found: {worker_script}") | |
| detected_gpu = detect_gpu_name() | |
| if not gpu_name: | |
| gpu_name = detected_gpu | |
| if cost_per_hour is None: | |
| cost_per_hour = 2.19 | |
| for k, v in PRICING_TABLE.items(): | |
| if k.lower() in gpu_name.lower(): | |
| cost_per_hour = v | |
| break | |
| cpu_count = os.cpu_count() or 8 | |
| print("=" * 80) | |
| print(f"🚀 PADDLEOCR AUTOMATED HARDWARE BENCHMARK SUITE") | |
| print(f"GPU: {gpu_name} (Detected: {detected_gpu}) | Cost: ${cost_per_hour:.2f}/h") | |
| print(f"CPU Cores: {cpu_count} | Python: {python_bin}") | |
| print(f"Config: {config_path}") | |
| print(f"Warmup: {warmup_steps} steps | Measure: {measure_steps} steps") | |
| print("=" * 80, flush=True) | |
| all_trials = [] | |
| # --------------------------------------------------------- | |
| # PHASE 1: FIND OPTIMAL NUM_WORKERS AT BASE BATCH | |
| # --------------------------------------------------------- | |
| base_batch = 128 | |
| initial_oom_batch = None | |
| print(f"\nTesting initial base batch {base_batch} with 4 workers...") | |
| test_init = run_single_trial( | |
| python_bin, worker_script, config_path, | |
| batch_size=base_batch, num_workers=4, | |
| warmup_steps=warmup_steps, measure_steps=measure_steps, | |
| output_dir=output_dir | |
| ) | |
| test_init = compute_economics(test_init, cost_per_hour) | |
| all_trials.append(test_init) | |
| if test_init["status"] == "OOM": | |
| initial_oom_batch = base_batch | |
| print(f"⚠️ Base batch {base_batch} encountered OOM on {gpu_name}! Stepping down to locate viable batch...") | |
| found_base = False | |
| for fallback_b in [96, 64, 48, 32]: | |
| print(f"Testing fallback batch {fallback_b}...") | |
| t_fb = run_single_trial( | |
| python_bin, worker_script, config_path, | |
| batch_size=fallback_b, num_workers=4, | |
| warmup_steps=warmup_steps, measure_steps=measure_steps, | |
| output_dir=output_dir | |
| ) | |
| t_fb = compute_economics(t_fb, cost_per_hour) | |
| all_trials.append(t_fb) | |
| if t_fb["status"] == "SUCCESS": | |
| base_batch = fallback_b | |
| found_base = True | |
| print(f"✅ Viable base batch found: {base_batch} (VRAM: {t_fb['vram_pct']:.1f}%)") | |
| break | |
| if not found_base: | |
| raise RuntimeError(f"Unable to find viable batch size even at batch 32 on {gpu_name}") | |
| print("\n" + "=" * 60) | |
| print(f"📍 [PHASE 1/3] SWEEPING NUM_WORKERS AT BATCH SIZE {base_batch}") | |
| print("=" * 60, flush=True) | |
| candidate_workers = [4, 8, 12, 16, 24] | |
| max_reasonable_workers = min(24, max(8, cpu_count * 2)) | |
| candidate_workers = [w for w in candidate_workers if w <= max_reasonable_workers] | |
| if 4 not in candidate_workers: | |
| candidate_workers.insert(0, 4) | |
| best_worker = 4 | |
| best_worker_ips = 0.0 | |
| worker_trials = [] | |
| for w in candidate_workers: | |
| # Check if already tested | |
| existing = [t for t in all_trials if t["batch_size"] == base_batch and t["num_workers"] == w] | |
| if existing: | |
| trial = existing[0] | |
| else: | |
| trial = run_single_trial( | |
| python_bin, worker_script, config_path, | |
| batch_size=base_batch, num_workers=w, | |
| warmup_steps=warmup_steps, measure_steps=measure_steps, | |
| output_dir=output_dir | |
| ) | |
| trial = compute_economics(trial, cost_per_hour) | |
| all_trials.append(trial) | |
| worker_trials.append(trial) | |
| if trial["status"] == "SUCCESS": | |
| print(f" -> Workers={w:2d} | IPS={trial['ips']:6.1f} | Reader={trial['avg_reader_cost']:.4f}s | Batch={trial['avg_batch_cost']:.4f}s | ReaderRatio={trial['reader_ratio']:.2f}") | |
| if trial["ips"] > best_worker_ips: | |
| best_worker_ips = trial["ips"] | |
| best_worker = w | |
| else: | |
| print(f" -> Workers={w:2d} | FAILED ({trial['status']}): {trial.get('error')}") | |
| print(f"\n✅ Optimal num_workers selected: {best_worker} (IPS: {best_worker_ips:.1f} samples/s)") | |
| # --------------------------------------------------------- | |
| # PHASE 2: BATCH SIZE SCALING | |
| # --------------------------------------------------------- | |
| print("\n" + "=" * 60) | |
| print(f"📍 [PHASE 2/3] BATCH SIZE PROGRESSION (WORKERS = {best_worker})") | |
| print("=" * 60, flush=True) | |
| if base_batch < 128: | |
| # Fine-grained progression for smaller GPUs | |
| batch_progression = [base_batch, 80, 96, 112, 128, 144, 160, 192, 224, 256] | |
| else: | |
| batch_progression = [128, 160, 192, 224, 256, 320, 384, 512, 768, 1024] | |
| # Filter only batches >= base_batch | |
| batch_progression = sorted(list(set([b for b in batch_progression if b >= base_batch]))) | |
| batch_trials = [] | |
| last_safe_batch = base_batch | |
| last_safe_trial = next((t for t in worker_trials if t["batch_size"] == base_batch and t["num_workers"] == best_worker and t["status"] == "SUCCESS"), None) | |
| oom_batch = initial_oom_batch | |
| best_batch = base_batch | |
| best_batch_ips = best_worker_ips | |
| diminishing_returns_detected = False | |
| for b in batch_progression: | |
| # If batch 128 was already tested in Phase 1 with the same worker, reuse result | |
| existing = [t for t in worker_trials if t["batch_size"] == b and t["num_workers"] == best_worker] | |
| if existing: | |
| trial = existing[0] | |
| else: | |
| trial = run_single_trial( | |
| python_bin, worker_script, config_path, | |
| batch_size=b, num_workers=best_worker, | |
| warmup_steps=warmup_steps, measure_steps=measure_steps, | |
| output_dir=output_dir | |
| ) | |
| trial = compute_economics(trial, cost_per_hour) | |
| all_trials.append(trial) | |
| batch_trials.append(trial) | |
| if trial["status"] == "SUCCESS": | |
| cur_ips = trial["ips"] | |
| vram_pct = trial["vram_pct"] | |
| reader_ratio = trial["reader_ratio"] | |
| # Calculate improvement over previous successful batch | |
| delta_ips_pct = 0.0 | |
| if last_safe_trial and last_safe_trial["ips"] > 0: | |
| delta_ips_pct = ((cur_ips - last_safe_trial["ips"]) / last_safe_trial["ips"]) * 100 | |
| print(f" -> Batch={b:4d} | IPS={cur_ips:6.1f} ({delta_ips_pct:+5.1f}%) | VRAM={trial['peak_vram_mb']:5.0f}MB ({vram_pct:4.1f}%) | ReaderRatio={reader_ratio:.2f} | Est: {trial['est_40_epoch_hours']:.1f}h (${trial['est_40_epoch_cost']:.2f})") | |
| # Check for best throughput batch | |
| if cur_ips > best_batch_ips: | |
| best_batch_ips = cur_ips | |
| best_batch = b | |
| # Check diminishing returns: if gain < 3% compared to previous batch | |
| if last_safe_trial and delta_ips_pct < 3.0: | |
| print(f" ⚠️ Diminishing returns detected at Batch={b} (Throughput gain {delta_ips_pct:.2f}% < 3.0%). Sweet spot identified.") | |
| diminishing_returns_detected = True | |
| last_safe_batch = b | |
| last_safe_trial = trial | |
| # Check if VRAM exceeds 92% (danger zone) | |
| if vram_pct >= 92.0: | |
| print(f" ⚠️ VRAM usage reached {vram_pct:.1f}% (>= 92%). Halting batch scaling to avoid sudden hard OOM.") | |
| break | |
| # If diminishing returns detected and VRAM is already high, stop scaling further | |
| if diminishing_returns_detected and vram_pct >= 85.0: | |
| print(f" 🏁 Stopping batch progression: Throughput saturated and VRAM comfortably utilized.") | |
| break | |
| elif trial["status"] == "OOM": | |
| print(f" 💥 OOM encountered at Batch={b}!") | |
| oom_batch = b | |
| break | |
| else: | |
| print(f" ❌ Trial failed at Batch={b} with status {trial['status']}: {trial.get('error')}") | |
| break | |
| # --------------------------------------------------------- | |
| # PHASE 3: BINARY SEARCH FOR EXACT MAX SAFE BATCH | |
| # --------------------------------------------------------- | |
| max_safe_batch = last_safe_batch | |
| if oom_batch is not None and last_safe_batch is not None: | |
| print("\n" + "=" * 60) | |
| print(f"📍 [PHASE 3/3] BINARY SEARCH BETWEEN {last_safe_batch} (SAFE) AND {oom_batch} (OOM)") | |
| print("=" * 60, flush=True) | |
| low = last_safe_batch | |
| high = oom_batch | |
| while (high - low) > 16: | |
| mid = round_to_multiple_of_16((low + high) / 2) | |
| if mid == low or mid == high: | |
| break | |
| print(f"\n[Binary Search] Testing midpoint Batch={mid} (range: [{low}, {high}])...") | |
| trial = run_single_trial( | |
| python_bin, worker_script, config_path, | |
| batch_size=mid, num_workers=best_worker, | |
| warmup_steps=warmup_steps, measure_steps=measure_steps, | |
| output_dir=output_dir | |
| ) | |
| trial = compute_economics(trial, cost_per_hour) | |
| all_trials.append(trial) | |
| if trial["status"] == "SUCCESS": | |
| print(f" -> Batch={mid} succeeded! IPS={trial['ips']:.1f}, VRAM={trial['vram_pct']:.1f}%") | |
| low = mid | |
| if trial["ips"] > best_batch_ips and not diminishing_returns_detected: | |
| best_batch_ips = trial["ips"] | |
| best_batch = mid | |
| else: | |
| print(f" -> Batch={mid} failed with {trial['status']}.") | |
| high = mid | |
| max_safe_batch = low | |
| print(f"\n🎯 Binary Search Complete: Max Safe Batch = {max_safe_batch}") | |
| # --------------------------------------------------------- | |
| # PHASE 4: BOTTLENECK ANALYSIS & REPORT GENERATION | |
| # --------------------------------------------------------- | |
| best_trial = next((t for t in all_trials if t["batch_size"] == best_batch and t["num_workers"] == best_worker and t["status"] == "SUCCESS"), last_safe_trial) | |
| # Identify primary bottleneck | |
| bottleneck = "Balanced" | |
| if best_trial: | |
| if best_trial["vram_pct"] >= 90.0: | |
| bottleneck = "VRAM Capacity Limited" | |
| elif best_trial["reader_ratio"] >= 0.40: | |
| bottleneck = "DataLoader / CPU Bound" | |
| elif best_trial["avg_gpu_util"] >= 80.0 and best_trial["reader_ratio"] < 0.20: | |
| bottleneck = "GPU Compute Saturated (Optimal)" | |
| elif best_trial["avg_cpu_pct"] >= 85.0: | |
| bottleneck = "Host CPU Bound" | |
| else: | |
| bottleneck = "Memory Bandwidth / Pipeline Latency" | |
| summary = { | |
| "gpu_name": gpu_name, | |
| "cost_per_hour": cost_per_hour, | |
| "optimal_workers": best_worker, | |
| "best_batch": best_batch, | |
| "max_safe_batch": max_safe_batch, | |
| "best_ips": best_trial["ips"] if best_trial else 0.0, | |
| "primary_bottleneck": bottleneck, | |
| "best_trial": best_trial, | |
| "all_trials": all_trials | |
| } | |
| # Save JSON summary | |
| summary_file = os.path.join(output_dir, f"benchmark_{gpu_name.replace(' ', '_')}.json") | |
| with open(summary_file, 'w', encoding='utf-8') as f: | |
| json.dump(summary, f, indent=2, ensure_ascii=False) | |
| # Generate Markdown Report | |
| md_report = generate_markdown_report(summary) | |
| md_file = os.path.join(output_dir, f"benchmark_{gpu_name.replace(' ', '_')}.md") | |
| with open(md_file, 'w', encoding='utf-8') as f: | |
| f.write(md_report) | |
| print("\n" + "=" * 80) | |
| print("📊 BENCHMARK REPORT COMPLETED") | |
| print(f"JSON Output: {summary_file}") | |
| print(f"MD Output: {md_file}") | |
| print("=" * 80) | |
| print(md_report) | |
| return summary | |
| def generate_markdown_report(summary): | |
| gpu = summary["gpu_name"] | |
| cost = summary["cost_per_hour"] | |
| best_b = summary["best_batch"] | |
| max_b = summary["max_safe_batch"] | |
| workers = summary["optimal_workers"] | |
| bottleneck = summary["primary_bottleneck"] | |
| trials = summary["all_trials"] | |
| lines = [] | |
| lines.append(f"# Benchmark & Hardware Limit Analysis: {gpu}") | |
| lines.append(f"**Cluster / GPU**: `{gpu}` | **Pricing**: `${cost:.2f}/h` | **Primary Bottleneck**: `{bottleneck}`\n") | |
| lines.append("## 1. Kết Quả Đo Lường Chi Tiết (Full Measurement Trials)") | |
| lines.append("| Batch | Workers | Status | IPS (mẫu/s) | GPU Util % | VRAM (MB) | VRAM % | Reader Cost | Batch Cost | Reader Ratio | Est 40 Epoch | Est Cost | IPS/$ |") | |
| lines.append("|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|") | |
| for t in trials: | |
| b = t["batch_size"] | |
| w = t["num_workers"] | |
| st = t["status"] | |
| if st == "SUCCESS": | |
| ips = f"{t['ips']:.1f}" | |
| gpu_u = f"{t['avg_gpu_util']:.0f}%" | |
| vram_mb = f"{t['peak_vram_mb']:.0f}" | |
| vram_p = f"{t['vram_pct']:.1f}%" | |
| r_cost = f"{t['avg_reader_cost']:.4f}s" | |
| b_cost = f"{t['avg_batch_cost']:.4f}s" | |
| r_ratio = f"{t['reader_ratio']:.2f}" | |
| est_h = f"{t['est_40_epoch_hours']:.2f}h" | |
| est_c = f"${t['est_40_epoch_cost']:.2f}" | |
| ips_d = f"{t['ips_per_dollar']:.1f}" | |
| else: | |
| ips = "-" | |
| gpu_u = "-" | |
| vram_mb = "-" | |
| vram_p = "-" | |
| r_cost = "-" | |
| b_cost = "-" | |
| r_ratio = "-" | |
| est_h = "-" | |
| est_c = "-" | |
| ips_d = "-" | |
| # Highlight best and max | |
| tag = "" | |
| if b == best_b and w == workers and st == "SUCCESS": | |
| tag = " 🌟 *(Best)*" | |
| elif b == max_b and w == workers and st == "SUCCESS": | |
| tag = " 🛡️ *(Max Safe)*" | |
| elif st == "OOM": | |
| tag = " 💥 *(OOM)*" | |
| lines.append(f"| {b}{tag} | {w} | {st} | {ips} | {gpu_u} | {vram_mb} | {vram_p} | {r_cost} | {b_cost} | {r_ratio} | {est_h} | {est_c} | {ips_d} |") | |
| lines.append("\n## 2. Kết Luận & Đề Xuất Cấu Hình") | |
| best_t = summary.get("best_trial") | |
| if best_t: | |
| lines.append(f"- **Max Batch Chạy Được (Hardware Limit)**: `{max_b}` (ngưỡng tối đa trước khi tràn VRAM).") | |
| lines.append(f"- **Best Batch Nên Dùng (Sweet Spot)**: `{best_b}` (đạt throughput `{best_t['ips']:.1f}` mẫu/s, khai thác `{best_t['vram_pct']:.1f}%` VRAM).") | |
| lines.append(f"- **Workers Tối Ưu**: `{workers}` (giữ `reader_ratio = {best_t['reader_ratio']:.2f}`, đảm bảo GPU không bị đói dữ liệu).") | |
| lines.append(f"- **Điểm Nghẽn Chính (Bottleneck)**: `{bottleneck}`.") | |
| lines.append(f"- **Thời Gian Huấn Luyện Full 40 Epoch (10M samples)**: `{best_t['est_40_epoch_hours']:.2f} giờ` (~`{best_t['est_40_epoch_hours']*60:.0f} phút`).") | |
| lines.append(f"- **Chi Phí Ước Tính Full 40 Epoch**: `${best_t['est_40_epoch_cost']:.2f}`.") | |
| lines.append(f"- **Hiệu Suất Kinh Tế (Throughput / $)**: `{best_t['ips_per_dollar']:.1f} samples / $`.") | |
| return "\n".join(lines) | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser(description="Automated PaddleOCR Benchmark Suite") | |
| parser.add_argument("-c", "--config_path", type=str, required=True, help="Path to YAML config") | |
| parser.add_argument("--gpu_name", type=str, default=None, help="GPU identifier name") | |
| parser.add_argument("--cost_per_hour", type=float, default=None, help="Hourly cost in USD") | |
| parser.add_argument("--python_bin", type=str, default=None, help="Path to Python interpreter") | |
| parser.add_argument("--warmup_steps", type=int, default=100, help="Warmup steps") | |
| parser.add_argument("--measure_steps", type=int, default=300, help="Measure steps") | |
| parser.add_argument("-o", "--output_dir", type=str, default="benchmark_results", help="Directory to save results") | |
| args = parser.parse_args() | |
| run_benchmark_suite( | |
| config_path=args.config_path, | |
| gpu_name=args.gpu_name, | |
| cost_per_hour=args.cost_per_hour, | |
| python_bin=args.python_bin, | |
| warmup_steps=args.warmup_steps, | |
| measure_steps=args.measure_steps, | |
| output_dir=args.output_dir | |
| ) | |