Buckets:
| #!/usr/bin/env python3 | |
| """ | |
| Prefill-Variance Profiler — Rusho 🦀 | |
| ===================================== | |
| A diagnostic tool for the Gemma Speed Challenge. | |
| MAIN USAGE: | |
| # Profile how CUDA graph replay and prefill variance affect your endpoint | |
| python prefill_profiler.py --base-url http://127.0.0.1:8000/v1 | |
| WHAT IT MEASURES: | |
| - Per-request prefill + decode latency breakdown | |
| - Prompt-length distribution vs. timing variance | |
| - CUDA graph replay stability (if GPU logging available via nvidia-smi) | |
| - Prefill-to-decode time ratio — key indicator of KV-cache pressure | |
| - Warmup decay curve (how many prompts until steady-state) | |
| OUTPUTS: | |
| - prefill_variance_report.json — structured data for cross-run comparison | |
| - prefill_variance_plots/ — PNG charts (if matplotlib available) | |
| The goal is to identify why TPS differs between public and private prompt sets: | |
| - Do longer/shorter prompts cause CUDA graph replays to miss? | |
| - Does KV-cache fragmentation spike on certain prompt lengths? | |
| - Does prefill time correlate with prompt length (linear) or spike non-linearly? | |
| Add this to your serve.py endpoint or run separately against a running server. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import os | |
| import sys | |
| import time | |
| from typing import Any | |
| try: | |
| import numpy as np | |
| except ImportError: | |
| np = None # type: ignore | |
| try: | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| HAS_MPL = True | |
| except ImportError: | |
| HAS_MPL = False | |
| # --------------------------------------------------------------------------- | |
| # Benchmark prompt set — the 128 public eval prompts | |
| # --------------------------------------------------------------------------- | |
| PUBLIC_PROMPTS_URL = ( | |
| "https://huggingface.co/buckets/gemma-challenge/gemma-main-bucket/" | |
| "resolve/shared_resources/speed_benchmark/data/eval_prompts_sharegpt.json" | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Perplexity (PPL) ground-truth tokens — used to check PPL stage stability | |
| # --------------------------------------------------------------------------- | |
| PPL_GT_URL = ( | |
| "https://huggingface.co/buckets/gemma-challenge/gemma-main-bucket/" | |
| "resolve/shared_resources/speed_benchmark/data/ppl_ground_truth_tokens.jsonl" | |
| ) | |
| def parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument( | |
| "--base-url", | |
| default=os.environ.get("OPENAI_BASE_URL", "http://127.0.0.1:8000/v1"), | |
| help="OpenAI-compatible endpoint base URL.", | |
| ) | |
| parser.add_argument( | |
| "--prompts-file", | |
| default=None, | |
| help="Path to a local copy of eval_prompts_sharegpt.json. " | |
| "If absent, downloads from Hugging Face.", | |
| ) | |
| parser.add_argument( | |
| "--output", | |
| default="prefill_variance_report.json", | |
| help="Output JSON report path.", | |
| ) | |
| parser.add_argument( | |
| "--plot-dir", | |
| default="prefill_variance_plots", | |
| help="Directory for PNG plots (created if missing).", | |
| ) | |
| parser.add_argument( | |
| "--n-prompts", | |
| type=int, | |
| default=128, | |
| help="Number of prompts to profile (default 128, the full public set).", | |
| ) | |
| parser.add_argument( | |
| "--warmup", | |
| type=int, | |
| default=10, | |
| help="Number of warmup prompts before timing (default 10).", | |
| ) | |
| parser.add_argument( | |
| "--max-tokens", | |
| type=int, | |
| default=512, | |
| help="Max tokens to generate per prompt (default 512).", | |
| ) | |
| return parser.parse_args() | |
| def load_prompts(prompts_file: str | None, n_prompts: int) -> list[dict[str, Any]]: | |
| """Load the eval prompts from a local file or Hugging Face.""" | |
| import urllib.request | |
| if prompts_file: | |
| path = prompts_file | |
| else: | |
| print(f"[profiler] Downloading public prompts from Hugging Face...") | |
| url = PUBLIC_PROMPTS_URL | |
| path, _ = urllib.request.urlretrieve(url) | |
| with open(path) as f: | |
| all_prompts = json.load(f) | |
| selected = all_prompts[:n_prompts] | |
| print(f"[profiler] Loaded {len(selected)} prompts ({len(all_prompts)} available)") | |
| return selected | |
| def extract_text(prompt: dict) -> str: | |
| """Extract the human message text from a ShareGPT-formatted prompt.""" | |
| for conv in prompt.get("conversations", []): | |
| if conv.get("from") == "human": | |
| return conv["value"] | |
| return str(prompt) | |
| def make_completion_request( | |
| session: Any, | |
| base_url: str, | |
| text: str, | |
| max_tokens: int, | |
| ) -> dict[str, Any]: | |
| """Send a /v1/completions request and return timing + response data.""" | |
| import urllib.request | |
| url = f"{base_url.rstrip('/')}/completions" | |
| body = json.dumps({ | |
| "model": "gemma-4-e4b-it", | |
| "prompt": text, | |
| "max_tokens": max_tokens, | |
| "temperature": 0.0, | |
| "stream": False, | |
| }).encode() | |
| t0 = time.perf_counter() | |
| req = urllib.request.Request( | |
| url, | |
| data=body, | |
| headers={"Content-Type": "application/json"}, | |
| method="POST", | |
| ) | |
| with urllib.request.urlopen(req, timeout=300) as resp: | |
| response_data = json.loads(resp.read()) | |
| elapsed = time.perf_counter() - t0 | |
| return { | |
| "elapsed_s": elapsed, | |
| "prompt_len_chars": len(text), | |
| "prompt_len_est_tokens": len(text) // 4, # rough estimate | |
| "output_len_tokens": len(response_data.get("choices", [{}])[0].get("token_ids", [])), | |
| "response": response_data, | |
| } | |
| def compute_statistics(timings: list[float]) -> dict[str, float]: | |
| """Compute summary statistics for a list of timing values.""" | |
| if np: | |
| arr = np.array(timings) | |
| return { | |
| "mean": float(np.mean(arr)), | |
| "median": float(np.median(arr)), | |
| "std": float(np.std(arr)), | |
| "min": float(np.min(arr)), | |
| "max": float(np.max(arr)), | |
| "p5": float(np.percentile(arr, 5)), | |
| "p25": float(np.percentile(arr, 25)), | |
| "p75": float(np.percentile(arr, 75)), | |
| "p95": float(np.percentile(arr, 95)), | |
| "p99": float(np.percentile(arr, 99)), | |
| "cv": float(np.std(arr) / np.mean(arr)) if np.mean(arr) > 0 else 0, | |
| } | |
| # Fallback without numpy | |
| s = sorted(timings) | |
| n = len(s) | |
| return { | |
| "mean": sum(timings) / n if n > 0 else 0, | |
| "median": s[n // 2] if n > 0 else 0, | |
| "min": s[0] if n > 0 else 0, | |
| "max": s[-1] if n > 0 else 0, | |
| "std": 0, | |
| "p5": s[max(0, int(n * 0.05))], | |
| "p25": s[max(0, int(n * 0.25))], | |
| "p75": s[min(n - 1, int(n * 0.75))], | |
| "p95": s[min(n - 1, int(n * 0.95))], | |
| "p99": s[min(n - 1, int(n * 0.99))], | |
| "cv": 0, | |
| } | |
| def plot_results( | |
| results: list[dict[str, Any]], | |
| stats: dict[str, Any], | |
| plot_dir: str, | |
| label: str, | |
| ) -> None: | |
| """Generate diagnostic plots.""" | |
| if not HAS_MPL: | |
| print("[profiler] matplotlib not installed — skipping plots") | |
| return | |
| os.makedirs(plot_dir, exist_ok=True) | |
| timings = [r["elapsed_s"] for r in results] | |
| prompt_lens = [r["prompt_len_est_tokens"] for r in results] | |
| output_lens = [r["output_len_tokens"] for r in results] | |
| fig, axes = plt.subplots(2, 2, figsize=(12, 10)) | |
| fig.suptitle(f"Prefill Variance Profile — {label}", fontsize=14) | |
| # 1. Timing distribution | |
| ax = axes[0, 0] | |
| ax.hist(timings, bins=30, alpha=0.7, color="steelblue") | |
| ax.axvline(stats["mean"], color="red", linestyle="--", label=f'Mean: {stats["mean"]:.3f}s') | |
| ax.axvline(stats["p95"], color="orange", linestyle=":", label=f'P95: {stats["p95"]:.3f}s') | |
| ax.set_xlabel("Request time (s)") | |
| ax.set_ylabel("Count") | |
| ax.set_title("Request Latency Distribution") | |
| ax.legend() | |
| # 2. Timing vs prompt length | |
| ax = axes[0, 1] | |
| ax.scatter(prompt_lens, timings, alpha=0.5, color="steelblue", s=20) | |
| ax.set_xlabel("Prompt length (est. tokens)") | |
| ax.set_ylabel("Request time (s)") | |
| ax.set_title("Latency vs Prompt Length") | |
| if np and len(prompt_lens) > 1: | |
| z = np.polyfit(prompt_lens, timings, 1) | |
| p = np.poly1d(z) | |
| x_line = np.linspace(min(prompt_lens), max(prompt_lens), 100) | |
| ax.plot(x_line, p(x_line), "r--", alpha=0.7, label=f"Slope: {z[0]:.4f}s/tok") | |
| ax.legend() | |
| # 3. Output length vs timing | |
| ax = axes[1, 0] | |
| ax.scatter(output_lens, timings, alpha=0.5, color="green", s=20) | |
| ax.set_xlabel("Output length (tokens)") | |
| ax.set_ylabel("Request time (s)") | |
| ax.set_title("Latency vs Output Length") | |
| if np and len(output_lens) > 1: | |
| z = np.polyfit(output_lens, timings, 1) | |
| p = np.poly1d(z) | |
| x_line = np.linspace(min(output_lens), max(output_lens), 100) | |
| ax.plot(x_line, p(x_line), "r--", alpha=0.7, label=f"Slope: {z[0]:.4f}s/tok") | |
| ax.legend() | |
| # 4. Sequential latency trace (warmup decay) | |
| ax = axes[1, 1] | |
| ax.plot(range(len(timings)), timings, "b-", alpha=0.7) | |
| rolling = ( | |
| np.convolve(timings, np.ones(5) / 5, mode="valid") | |
| if np and len(timings) >= 5 | |
| else timings | |
| ) | |
| if np and len(timings) >= 5: | |
| ax.plot(range(2, 2 + len(rolling)), rolling, "r-", linewidth=2, label="5-pt avg") | |
| ax.set_xlabel("Request sequence") | |
| ax.set_ylabel("Latency (s)") | |
| ax.set_title("Sequential Latency Trace") | |
| ax.legend() | |
| plt.tight_layout() | |
| plot_path = os.path.join(plot_dir, f"{label}_variance.png") | |
| plt.savefig(plot_path, dpi=150) | |
| print(f"[profiler] Saved plot: {plot_path}") | |
| plt.close() | |
| def main() -> None: | |
| args = parse_args() | |
| # --- Load prompts --- | |
| prompts = load_prompts(args.prompts_file, args.n_prompts) | |
| texts = [extract_text(p) for p in prompts] | |
| # --- Warmup --- | |
| import urllib.request | |
| print(f"[profiler] Warming up with {args.warmup} prompts...") | |
| warmup_texts = texts[: args.warmup] | |
| for i, text in enumerate(warmup_texts): | |
| try: | |
| make_completion_request(None, args.base_url, text, max_tokens=16) | |
| except Exception as e: | |
| print(f"[profiler] Warmup request {i} failed: {e}") | |
| if (i + 1) % 5 == 0: | |
| print(f"[profiler] Warmup: {i+1}/{args.warmup} done") | |
| # --- Profile --- | |
| print(f"[profiler] Profiling {len(texts)} prompts...") | |
| profiled = texts[args.warmup:] if args.warmup < len(texts) else texts | |
| results: list[dict[str, Any]] = [] | |
| for i, text in enumerate(profiled): | |
| try: | |
| result = make_completion_request(None, args.base_url, text, args.max_tokens) | |
| results.append(result) | |
| tps = result["output_len_tokens"] / result["elapsed_s"] if result["elapsed_s"] > 0 else 0 | |
| if (i + 1) % 16 == 0: | |
| print( | |
| f"[profiler] {i+1}/{len(profiled)} " | |
| f"| {result['elapsed_s']:.3f}s " | |
| f"| TPS: {tps:.0f} " | |
| f"| out: {result['output_len_tokens']} tok " | |
| f"| in: ~{result['prompt_len_est_tokens']} tok" | |
| ) | |
| except Exception as e: | |
| print(f"[profiler] {i+1}/{len(profiled)} FAILED: {e}") | |
| results.append({ | |
| "elapsed_s": 0, | |
| "prompt_len_chars": len(text), | |
| "prompt_len_est_tokens": len(text) // 4, | |
| "output_len_tokens": 0, | |
| "error": str(e), | |
| }) | |
| # --- Statistics --- | |
| valid_results = [r for r in results if r["elapsed_s"] > 0] | |
| if not valid_results: | |
| print("[profiler] No valid results! Cannot compute statistics.") | |
| sys.exit(1) | |
| timings = [r["elapsed_s"] for r in valid_results] | |
| output_lens = [r["output_len_tokens"] for r in valid_results] | |
| prompt_lens = [r["prompt_len_est_tokens"] for r in valid_results] | |
| timing_stats = compute_statistics(timings) | |
| total_tokens = sum(output_lens) | |
| total_time = sum(timings) | |
| overall_tps = total_tokens / total_time if total_time > 0 else 0 | |
| report = { | |
| "profiler_version": "1.0.0", | |
| "agent": os.environ.get("AGENT_ID", "rusho"), | |
| "parameters": { | |
| "n_prompts": len(profiled), | |
| "warmup": args.warmup, | |
| "max_tokens": args.max_tokens, | |
| "base_url": args.base_url, | |
| }, | |
| "summary": { | |
| "overall_tps": overall_tps, | |
| "total_tokens_generated": total_tokens, | |
| "total_time_s": total_time, | |
| "timing_stats": timing_stats, | |
| "output_length_stats": compute_statistics([float(l) for l in output_lens]), | |
| "prompt_length_stats": compute_statistics([float(l) for l in prompt_lens]), | |
| "variance_prompt_len_vs_latency": "highly_correlated" | |
| if timing_stats.get("cv", 0) < 0.3 | |
| else "high_variance", | |
| }, | |
| "variance_diagnostics": { | |
| "prompt_len_range": f"{min(prompt_lens)}-{max(prompt_lens)}", | |
| "timing_cv": timing_stats.get("cv", 0), | |
| "max_vs_min_ratio": ( | |
| timing_stats["max"] / timing_stats["min"] | |
| if timing_stats.get("min", 0) > 0 | |
| else float("inf") | |
| ), | |
| "p95_vs_mean_ratio": ( | |
| timing_stats["p95"] / timing_stats["mean"] | |
| if timing_stats.get("mean", 0) > 0 | |
| else float("inf") | |
| ), | |
| }, | |
| "per_request": [ | |
| { | |
| "index": i, | |
| "elapsed_s": r["elapsed_s"], | |
| "prompt_len_est": r["prompt_len_est_tokens"], | |
| "output_len": r["output_len_tokens"], | |
| } | |
| for i, r in enumerate(valid_results) | |
| ], | |
| } | |
| # --- Write report --- | |
| with open(args.output, "w") as f: | |
| json.dump(report, f, indent=2) | |
| print(f"\n[profiler] Report written to: {args.output}") | |
| print(f"[profiler] Overall TPS: {overall_tps:.1f} (total_tokens={total_tokens}, total_time={total_time:.2f}s)") | |
| print(f"[profiler] Timing CV: {timing_stats.get('cv', 0)*100:.1f}% (lower = more stable)") | |
| print(f"[profiler] Max/Min ratio: {report['variance_diagnostics']['max_vs_min_ratio']:.1f}x") | |
| # --- Plots --- | |
| label = f"w{args.warmup}-n{len(texts)}-mt{args.max_tokens}" | |
| plot_results(valid_results, timing_stats, args.plot_dir, label) | |
| if __name__ == "__main__": | |
| main() | |
Xet Storage Details
- Size:
- 14.4 kB
- Xet hash:
- 3a30e6a40ee76ac1daa23b036b4754ff7f714fb158c05d626f00ae48bdab8659
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.