Guest User

Untitled

a guest
Oct 30th, 2026
43
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 25.85 KB | None | 0 0
  1. #!/usr/bin/env python3
  2. """RTX 5090 Power Limitation Sweep for Basalt Inference Engine.
  3. Sweeps GPU power limits from 400W to 600W and measures prefill & decode throughput,
  4. actual power draw, SM clocks, temperatures, and energy efficiency (Tokens/Joule).
  5. """
  6.  
  7. import atexit
  8. import ctypes
  9. import json
  10. import os
  11. import subprocess
  12. import sys
  13. import threading
  14. import time
  15. import urllib.request
  16. import uuid
  17. import matplotlib
  18. matplotlib.use("Agg")
  19. import matplotlib.pyplot as plt
  20. import numpy as np
  21.  
  22. DEFAULT_POWER_W = 475
  23. POWER_LIMITS = [400, 425, 450, 475, 500, 525, 550, 575, 600]
  24. RUNS_PER_STEP = 2
  25. API_URL = "http://localhost:8080/v1/chat/completions"
  26.  
  27. class NvmlSampler:
  28. def __init__(self, sample_interval_s=0.05):
  29. self.interval = sample_interval_s
  30. self.samples = []
  31. self._stop = threading.Event()
  32. self._thread = None
  33. self._ok = False
  34. try:
  35. self.nvml = ctypes.CDLL("libnvidia-ml.so.1")
  36. self.nvml.nvmlInit_v2()
  37. self.handle = ctypes.c_void_p()
  38. self.nvml.nvmlDeviceGetHandleByIndex_v2(0, ctypes.byref(self.handle))
  39. self._ok = True
  40. except Exception as e:
  41. print(f"[warn] NVML init failed: {e}", file=sys.stderr)
  42.  
  43. def start(self):
  44. if not self._ok:
  45. return
  46. self.samples = []
  47. self._stop.clear()
  48. self._thread = threading.Thread(target=self._run, daemon=True)
  49. self._thread.start()
  50.  
  51. def _run(self):
  52. pwr = ctypes.c_uint()
  53. clk = ctypes.c_uint()
  54. temp = ctypes.c_uint()
  55. while not self._stop.is_set():
  56. t = time.time()
  57. self.nvml.nvmlDeviceGetPowerUsage(self.handle, ctypes.byref(pwr))
  58. self.nvml.nvmlDeviceGetClockInfo(self.handle, 1, ctypes.byref(clk)) # SM clock
  59. self.nvml.nvmlDeviceGetTemperature(self.handle, 0, ctypes.byref(temp)) # GPU temp
  60. self.samples.append({
  61. "time": t,
  62. "power_w": pwr.value / 1000.0,
  63. "sm_mhz": clk.value,
  64. "temp_c": temp.value
  65. })
  66. time.sleep(self.interval)
  67.  
  68. def stop(self):
  69. if not self._ok or self._thread is None:
  70. return {}
  71. self._stop.set()
  72. self._thread.join(timeout=2.0)
  73. if not self.samples:
  74. return {"avg_power_w": 0, "peak_power_w": 0, "avg_sm_mhz": 0, "max_temp_c": 0}
  75. powers = [s["power_w"] for s in self.samples]
  76. clocks = [s["sm_mhz"] for s in self.samples]
  77. temps = [s["temp_c"] for s in self.samples]
  78. return {
  79. "avg_power_w": round(float(np.mean(powers)), 1),
  80. "peak_power_w": round(float(np.max(powers)), 1),
  81. "avg_sm_mhz": round(float(np.mean(clocks)), 0),
  82. "max_temp_c": int(np.max(temps)),
  83. "samples_count": len(self.samples)
  84. }
  85.  
  86. def close(self):
  87. if self._ok:
  88. try:
  89. self.nvml.nvmlShutdown()
  90. except Exception:
  91. pass
  92.  
  93.  
  94. def set_gpu_power(watts):
  95. print(f"--> Setting GPU Power Limit to {watts}W...", flush=True)
  96. cmd = ["sudo", "/usr/bin/nvidia-smi", "-pl", str(watts)]
  97. res = subprocess.run(cmd, capture_output=True, text=True)
  98. if res.returncode != 0:
  99. print(f"Error setting power limit: {res.stderr}", file=sys.stderr)
  100. raise RuntimeError(res.stderr)
  101.  
  102.  
  103. def post_chat(messages, max_tokens, temperature=0.0):
  104. body = {
  105. "model": "basalt",
  106. "messages": messages,
  107. "max_tokens": max_tokens,
  108. "temperature": temperature
  109. }
  110. req = urllib.request.Request(
  111. API_URL,
  112. headers={"Content-Type": "application/json"},
  113. data=json.dumps(body).encode()
  114. )
  115. t0 = time.time()
  116. with urllib.request.urlopen(req, timeout=120) as resp:
  117. data = json.loads(resp.read().decode())
  118. wall_s = time.time() - t0
  119. return data, wall_s
  120.  
  121.  
  122. def run_prefill_benchmark(sampler):
  123. # Construct a ~16k token prompt with a unique UUID salt to bypass conversation cache
  124. unique_salt = f"Prompt-Run-{uuid.uuid4()}: "
  125. base_text = (
  126. "In the computational theory of large language models, matrix multiplications form the arithmetic backbone "
  127. "of both dense and mixture-of-experts transformer layers. As input sequences grow to tens of thousands of tokens, "
  128. "prefill processing shifts computational bottlenecks from memory bandwidth to tensor core arithmetic intensity. "
  129. "Quantized numerical representations such as NVFP4 and INT8 scale activation and weight dot-products into high-throughput tensor units. "
  130. )
  131. long_content = unique_salt + (base_text * 450)
  132. messages = [{"role": "user", "content": long_content}]
  133.  
  134. sampler.start()
  135. data, wall_s = post_chat(messages, max_tokens=1, temperature=0.0)
  136. stats = sampler.stop()
  137.  
  138. timings = data.get("timings", {})
  139. usage = data.get("usage", {})
  140. prompt_tokens = usage.get("prompt_tokens", 0)
  141. cached_tokens = usage.get("prompt_tokens_details", {}).get("cached_tokens", 0)
  142. prompt_tps = timings.get("prompt_per_second", 0.0)
  143. prompt_ms = timings.get("prompt_ms", wall_s * 1000.0)
  144.  
  145. return {
  146. "prompt_tokens": prompt_tokens,
  147. "cached_tokens": cached_tokens,
  148. "prompt_tps": prompt_tps,
  149. "prompt_ms": prompt_ms,
  150. "wall_s": round(wall_s, 2),
  151. **stats
  152. }
  153.  
  154.  
  155. def run_decode_benchmark(sampler):
  156. unique_salt = f"Request-{uuid.uuid4()}: "
  157. prompt = (
  158. f"{unique_salt}Write a detailed, highly technical analysis comparing speculative decoding "
  159. f"with standard autoregressive decoding. Cover speculative verification kernels, "
  160. f"draft token acceptance rates, KV cache update penalties, and hardware efficiency on modern Blackwell architecture."
  161. )
  162. messages = [{"role": "user", "content": prompt}]
  163.  
  164. sampler.start()
  165. data, wall_s = post_chat(messages, max_tokens=200, temperature=0.6)
  166. stats = sampler.stop()
  167.  
  168. timings = data.get("timings", {})
  169. usage = data.get("usage", {})
  170. gen_tokens = usage.get("completion_tokens", 0)
  171. decode_tps = timings.get("predicted_per_second", 0.0)
  172. decode_ms = timings.get("predicted_ms", wall_s * 1000.0)
  173. draft_n = timings.get("draft_n", 0)
  174. draft_acc = timings.get("draft_n_accepted", 0)
  175. acc_pct = round((draft_acc / draft_n * 100.0), 1) if draft_n > 0 else 0.0
  176.  
  177. return {
  178. "gen_tokens": gen_tokens,
  179. "decode_tps": decode_tps,
  180. "decode_ms": decode_ms,
  181. "wall_s": round(wall_s, 2),
  182. "draft_n": draft_n,
  183. "draft_acc": draft_acc,
  184. "draft_acc_pct": acc_pct,
  185. **stats
  186. }
  187.  
  188.  
  189. def generate_charts(results, output_png_path, output_html_path):
  190. # Extract arrays
  191. powers = [r["power_limit_w"] for r in results]
  192. prefill_tps = [r["prefill"]["prompt_tps"] for r in results]
  193. decode_tps = [r["decode"]["decode_tps"] for r in results]
  194. prefill_eff = [r["efficiency"]["prefill_tok_per_joule"] for r in results]
  195. decode_eff = [r["efficiency"]["decode_tok_per_joule"] for r in results]
  196. prefill_pwr = [r["prefill"]["avg_power_w"] for r in results]
  197. decode_pwr = [r["decode"]["avg_power_w"] for r in results]
  198. clocks = [r["prefill"]["avg_sm_mhz"] for r in results]
  199. temps = [max(r["prefill"]["max_temp_c"], r["decode"]["max_temp_c"]) for r in results]
  200.  
  201. # Matplotlib styling
  202. plt.style.use("dark_background")
  203. fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 11), dpi=200)
  204. fig.patch.set_facecolor("#0f172a")
  205.  
  206. for ax in (ax1, ax2, ax3, ax4):
  207. ax.set_facecolor("#1e293b")
  208. ax.grid(True, linestyle="--", alpha=0.3, color="#94a3b8")
  209. ax.tick_params(colors="#cbd5e1", labelsize=10)
  210. for spine in ax.spines.values():
  211. spine.set_color("#475569")
  212.  
  213. # 1. Prefill Throughput
  214. ax1.plot(powers, prefill_tps, marker="o", linewidth=2.5, markersize=7, color="#38bdf8", label="Prefill Throughput")
  215. for x, y in zip(powers, prefill_tps):
  216. ax1.annotate(f"{y:.0f}", (x, y), textcoords="offset points", xytext=(0, 8),
  217. ha="center", color="#7dd3fc", fontsize=9, fontweight="bold")
  218. ax1.axvline(475, color="#f59e0b", linestyle=":", linewidth=1.8, label="Default Baseline (475W)")
  219. ax1.set_title("Prefill Throughput vs Power Limit", fontsize=13, fontweight="bold", color="#f8fafc", pad=10)
  220. ax1.set_xlabel("GPU Power Limit (W)", color="#cbd5e1", fontsize=11)
  221. ax1.set_ylabel("Tokens / Second", color="#cbd5e1", fontsize=11)
  222. ax1.legend(loc="lower right", facecolor="#0f172a", edgecolor="#475569")
  223.  
  224. # 2. Decode Throughput
  225. ax2.plot(powers, decode_tps, marker="s", linewidth=2.5, markersize=7, color="#a855f7", label="Decode Throughput")
  226. for x, y in zip(powers, decode_tps):
  227. ax2.annotate(f"{y:.1f}", (x, y), textcoords="offset points", xytext=(0, 8),
  228. ha="center", color="#c084fc", fontsize=9, fontweight="bold")
  229. ax2.axvline(475, color="#f59e0b", linestyle=":", linewidth=1.8, label="Default Baseline (475W)")
  230. ax2.set_title("Decode Throughput vs Power Limit", fontsize=13, fontweight="bold", color="#f8fafc", pad=10)
  231. ax2.set_xlabel("GPU Power Limit (W)", color="#cbd5e1", fontsize=11)
  232. ax2.set_ylabel("Tokens / Second", color="#cbd5e1", fontsize=11)
  233. ax2.legend(loc="lower right", facecolor="#0f172a", edgecolor="#475569")
  234.  
  235. # 3. Energy Efficiency (Tokens per Joule)
  236. ax3.plot(powers, prefill_eff, marker="^", linewidth=2.5, markersize=7, color="#10b981", label="Prefill (Tok/J)")
  237. ax3.plot(powers, decode_eff, marker="d", linewidth=2.5, markersize=7, color="#ec4899", label="Decode (Tok/J)")
  238. for x, y in zip(powers, prefill_eff):
  239. ax3.annotate(f"{y:.2f}", (x, y), textcoords="offset points", xytext=(0, 8),
  240. ha="center", color="#6ee7b7", fontsize=9, fontweight="bold")
  241. ax3.axvline(475, color="#f59e0b", linestyle=":", linewidth=1.8, label="Default Baseline (475W)")
  242. ax3.set_title("Energy Efficiency (Tokens / Joule)", fontsize=13, fontweight="bold", color="#f8fafc", pad=10)
  243. ax3.set_xlabel("GPU Power Limit (W)", color="#cbd5e1", fontsize=11)
  244. ax3.set_ylabel("Tokens per Joule", color="#cbd5e1", fontsize=11)
  245. ax3.legend(loc="upper right", facecolor="#0f172a", edgecolor="#475569")
  246.  
  247. # 4. Hardware Clocks & Thermals
  248. ax4_t = ax4.twinx()
  249. p1 = ax4.plot(powers, clocks, marker="o", linewidth=2, color="#3b82f6", label="SM Clock (MHz)")
  250. p2 = ax4_t.plot(powers, temps, marker="x", linewidth=2, linestyle="--", color="#f43f5e", label="Peak Temp (°C)")
  251. ax4.set_title("Hardware Telemetry (SM Clock & Temp)", fontsize=13, fontweight="bold", color="#f8fafc", pad=10)
  252. ax4.set_xlabel("GPU Power Limit (W)", color="#cbd5e1", fontsize=11)
  253. ax4.set_ylabel("SM Clock (MHz)", color="#93c5fd", fontsize=11)
  254. ax4_t.set_ylabel("Temperature (°C)", color="#fda4af", fontsize=11)
  255. ax4_t.tick_params(colors="#fda4af", labelsize=10)
  256. ax4.spines["right"].set_color("#f43f5e")
  257. lines = p1 + p2
  258. labels = [l.get_label() for l in lines]
  259. ax4.legend(lines, labels, loc="lower right", facecolor="#0f172a", edgecolor="#475569")
  260.  
  261. fig.suptitle(
  262. "NVIDIA RTX 5090 (Blackwell sm_120) — Power Sweep Benchmark (400W to 600W)\n"
  263. "Model: Qwen3.8-FN-NVFP4 (Basalt Local Inference Engine)",
  264. fontsize=15, fontweight="bold", color="#ffffff", y=0.98
  265. )
  266.  
  267. plt.tight_layout(rect=[0, 0.03, 1, 0.95])
  268. plt.savefig(output_png_path, bbox_inches="tight", facecolor=fig.get_facecolor(), edgecolor="none")
  269. plt.close()
  270. print(f"Chart saved to {output_png_path}")
  271.  
  272. # Generate interactive standalone HTML report
  273. html_content = f"""<!DOCTYPE html>
  274. <html lang="en">
  275. <head>
  276. <meta charset="UTF-8">
  277. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  278. <title>RTX 5090 Power Sweep — Basalt Inference Engine</title>
  279. <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  280. <style>
  281. :root {{
  282. --bg: #0b0f19;
  283. --card: #151d30;
  284. --border: #24324f;
  285. --text: #f1f5f9;
  286. --muted: #94a3b8;
  287. --accent: #38bdf8;
  288. --purple: #a855f7;
  289. --green: #10b981;
  290. --amber: #f59e0b;
  291. }}
  292. * {{ box-sizing: border-box; margin: 0; padding: 0; }}
  293. body {{
  294. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
  295. background: var(--bg);
  296. color: var(--text);
  297. padding: 2rem;
  298. line-height: 1.5;
  299. }}
  300. .header {{
  301. text-align: center;
  302. margin-bottom: 2rem;
  303. }}
  304. .header h1 {{
  305. font-size: 2rem;
  306. font-weight: 800;
  307. background: linear-gradient(135deg, #38bdf8, #a855f7);
  308. -webkit-background-clip: text;
  309. -webkit-text-fill-color: transparent;
  310. }}
  311. .header p {{
  312. color: var(--muted);
  313. font-size: 0.95rem;
  314. margin-top: 0.5rem;
  315. }}
  316. .cards {{
  317. display: grid;
  318. grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  319. gap: 1.25rem;
  320. margin-bottom: 2.5rem;
  321. }}
  322. .card {{
  323. background: var(--card);
  324. border: 1px solid var(--border);
  325. border-radius: 12px;
  326. padding: 1.25rem;
  327. }}
  328. .card .label {{
  329. color: var(--muted);
  330. font-size: 0.8rem;
  331. text-transform: uppercase;
  332. font-weight: 600;
  333. letter-spacing: 0.05em;
  334. }}
  335. .card .value {{
  336. font-size: 1.75rem;
  337. font-weight: 700;
  338. color: var(--accent);
  339. margin-top: 0.25rem;
  340. }}
  341. .card .sub {{
  342. font-size: 0.8rem;
  343. color: var(--muted);
  344. margin-top: 0.25rem;
  345. }}
  346. .charts-grid {{
  347. display: grid;
  348. grid-template-columns: repeat(auto-fit, minmax(480px, 1fr));
  349. gap: 1.5rem;
  350. margin-bottom: 2.5rem;
  351. }}
  352. .chart-container {{
  353. background: var(--card);
  354. border: 1px solid var(--border);
  355. border-radius: 12px;
  356. padding: 1.5rem;
  357. position: relative;
  358. height: 380px;
  359. }}
  360. .chart-title {{
  361. font-size: 1.05rem;
  362. font-weight: 700;
  363. margin-bottom: 1rem;
  364. color: var(--text);
  365. }}
  366. table {{
  367. width: 100%;
  368. border-collapse: collapse;
  369. background: var(--card);
  370. border-radius: 12px;
  371. overflow: hidden;
  372. border: 1px solid var(--border);
  373. margin-top: 1.5rem;
  374. font-size: 0.9rem;
  375. }}
  376. th, td {{
  377. padding: 0.85rem 1rem;
  378. text-align: right;
  379. border-bottom: 1px solid var(--border);
  380. }}
  381. th:first-child, td:first-child {{
  382. text-align: left;
  383. }}
  384. th {{
  385. background: #1b2640;
  386. color: #cbd5e1;
  387. font-weight: 600;
  388. }}
  389. tr:hover {{
  390. background: #1e2c4a;
  391. }}
  392. .highlight {{
  393. color: var(--amber);
  394. font-weight: 700;
  395. }}
  396. </style>
  397. </head>
  398. <body>
  399. <div class="header">
  400. <h1>NVIDIA GeForce RTX 5090 Power Scaling Sweep</h1>
  401. <p>Engine: <strong>Basalt (sm_120 Blackwell)</strong> &bull; Model: <strong>Qwen3.8-FN-NVFP4 (122GB)</strong> &bull; Sweep Range: 400W &ndash; 600W</p>
  402. </div>
  403.  
  404. <div class="cards">
  405. <div class="card">
  406. <div class="label">Optimal Efficiency Point</div>
  407. <div class="value" id="optEff">-</div>
  408. <div class="sub">Best Tokens / Joule balance</div>
  409. </div>
  410. <div class="card">
  411. <div class="label">Prefill Range</div>
  412. <div class="value" id="prefillRange">-</div>
  413. <div class="sub">400W vs 600W scaling</div>
  414. </div>
  415. <div class="card">
  416. <div class="label">Decode Range</div>
  417. <div class="value" id="decodeRange">-</div>
  418. <div class="sub">400W vs 600W scaling</div>
  419. </div>
  420. <div class="card">
  421. <div class="label">Baseline (475W)</div>
  422. <div class="value" id="baselinePerf">-</div>
  423. <div class="sub">User's current profile</div>
  424. </div>
  425. </div>
  426.  
  427. <div class="charts-grid">
  428. <div class="chart-container">
  429. <div class="chart-title">Prefill &amp; Decode Throughput (Tokens/s)</div>
  430. <canvas id="throughputChart"></canvas>
  431. </div>
  432. <div class="chart-container">
  433. <div class="chart-title">Energy Efficiency (Tokens / Joule)</div>
  434. <canvas id="efficiencyChart"></canvas>
  435. </div>
  436. <div class="chart-container">
  437. <div class="chart-title">Measured Power Draw vs Power Limit (W)</div>
  438. <canvas id="powerChart"></canvas>
  439. </div>
  440. <div class="chart-container">
  441. <div class="chart-title">SM Clocks (MHz) &amp; Peak Thermals (°C)</div>
  442. <canvas id="hardwareChart"></canvas>
  443. </div>
  444. </div>
  445.  
  446. <h2 style="font-size: 1.25rem; font-weight: 700; margin-top: 2rem;">Full Sweep Results</h2>
  447. <table>
  448. <thead>
  449. <tr>
  450. <th>Power Limit</th>
  451. <th>Actual Prefill W</th>
  452. <th>Prefill TPS</th>
  453. <th>Prefill Tok/J</th>
  454. <th>Actual Decode W</th>
  455. <th>Decode TPS</th>
  456. <th>Decode Tok/J</th>
  457. <th>SM Clock</th>
  458. <th>Peak Temp</th>
  459. </tr>
  460. </thead>
  461. <tbody>
  462. """
  463. base_row = next((r for r in results if r["power_limit_w"] == 475), results[len(results)//2])
  464. for r in results:
  465. is_base = (r["power_limit_w"] == 475)
  466. cls = ' class="highlight"' if is_base else ""
  467. star = " ★ (Baseline)" if is_base else ""
  468. html_content += f""" <tr{cls}>
  469. <td><strong>{r["power_limit_w"]}W</strong>{star}</td>
  470. <td>{r["prefill"]["avg_power_w"]} W</td>
  471. <td><strong>{r["prefill"]["prompt_tps"]:.1f}</strong> t/s</td>
  472. <td>{r["efficiency"]["prefill_tok_per_joule"]:.2f}</td>
  473. <td>{r["decode"]["avg_power_w"]} W</td>
  474. <td><strong>{r["decode"]["decode_tps"]:.1f}</strong> t/s</td>
  475. <td>{r["efficiency"]["decode_tok_per_joule"]:.2f}</td>
  476. <td>{r["prefill"]["avg_sm_mhz"]:.0f} MHz</td>
  477. <td>{max(r["prefill"]["max_temp_c"], r["decode"]["max_temp_c"])} °C</td>
  478. </tr>
  479. """
  480.  
  481. html_content += f""" </tbody>
  482. </table>
  483.  
  484. <script>
  485. const data = {json.dumps(results)};
  486. const labels = data.map(d => d.power_limit_w + "W");
  487. const prefillTps = data.map(d => d.prefill.prompt_tps);
  488. const decodeTps = data.map(d => d.decode.decode_tps);
  489. const prefillEff = data.map(d => d.efficiency.prefill_tok_per_joule);
  490. const decodeEff = data.map(d => d.efficiency.decode_tok_per_joule);
  491. const prefillPwr = data.map(d => d.prefill.avg_power_w);
  492. const decodePwr = data.map(d => d.decode.avg_power_w);
  493. const smClocks = data.map(d => d.prefill.avg_sm_mhz);
  494. const temps = data.map(d => Math.max(d.prefill.max_temp_c, d.decode.max_temp_c));
  495.  
  496. // Summary cards
  497. const b = data.find(d => d.power_limit_w === 475) || data[0];
  498. document.getElementById("baselinePerf").innerText = b.decode.decode_tps.toFixed(1) + " t/s";
  499. document.getElementById("prefillRange").innerText = data[0].prefill.prompt_tps.toFixed(0) + " → " + data[data.length-1].prefill.prompt_tps.toFixed(0);
  500. document.getElementById("decodeRange").innerText = data[0].decode.decode_tps.toFixed(1) + " → " + data[data.length-1].decode.decode_tps.toFixed(1);
  501.  
  502. // Best efficiency
  503. let maxEffIdx = 0;
  504. for(let i=1; i<data.length; ++i) {{
  505. if(data[i].efficiency.prefill_tok_per_joule > data[maxEffIdx].efficiency.prefill_tok_per_joule) maxEffIdx = i;
  506. }}
  507. document.getElementById("optEff").innerText = data[maxEffIdx].power_limit_w + "W (" + data[maxEffIdx].efficiency.prefill_tok_per_joule.toFixed(2) + " T/J)";
  508.  
  509. Chart.defaults.color = "#94a3b8";
  510. Chart.defaults.borderColor = "#24324f";
  511.  
  512. new Chart(document.getElementById("throughputChart"), {{
  513. type: "line",
  514. data: {{
  515. labels: labels,
  516. datasets: [
  517. {{ label: "Prefill TPS", data: prefillTps, borderColor: "#38bdf8", backgroundColor: "#38bdf820", tension: 0.3, yAxisID: "y" }},
  518. {{ label: "Decode TPS", data: decodeTps, borderColor: "#a855f7", backgroundColor: "#a855f720", tension: 0.3, yAxisID: "y1" }}
  519. ]
  520. }},
  521. options: {{
  522. responsive: true, maintainAspectRatio: false,
  523. scales: {{
  524. y: {{ type: "linear", display: true, position: "left", title: {{ display: true, text: "Prefill TPS", color: "#38bdf8" }} }},
  525. y1: {{ type: "linear", display: true, position: "right", title: {{ display: true, text: "Decode TPS", color: "#a855f7" }}, grid: {{ drawOnChartArea: false }} }}
  526. }}
  527. }}
  528. }});
  529.  
  530. new Chart(document.getElementById("efficiencyChart"), {{
  531. type: "line",
  532. data: {{
  533. labels: labels,
  534. datasets: [
  535. {{ label: "Prefill (Tok/Joule)", data: prefillEff, borderColor: "#10b981", tension: 0.3 }},
  536. {{ label: "Decode (Tok/Joule)", data: decodeEff, borderColor: "#ec4899", tension: 0.3 }}
  537. ]
  538. }},
  539. options: {{
  540. responsive: true, maintainAspectRatio: false,
  541. scales: {{ y: {{ title: {{ display: true, text: "Tokens per Joule" }} }} }}
  542. }}
  543. }});
  544.  
  545. new Chart(document.getElementById("powerChart"), {{
  546. type: "bar",
  547. data: {{
  548. labels: labels,
  549. datasets: [
  550. {{ label: "Prefill Avg Draw (W)", data: prefillPwr, backgroundColor: "#38bdf8aa" }},
  551. {{ label: "Decode Avg Draw (W)", data: decodePwr, backgroundColor: "#a855f7aa" }}
  552. ]
  553. }},
  554. options: {{
  555. responsive: true, maintainAspectRatio: false,
  556. scales: {{ y: {{ title: {{ display: true, text: "Power Draw (Watts)" }} }} }}
  557. }}
  558. }});
  559.  
  560. new Chart(document.getElementById("hardwareChart"), {{
  561. type: "line",
  562. data: {{
  563. labels: labels,
  564. datasets: [
  565. {{ label: "SM Clock (MHz)", data: smClocks, borderColor: "#3b82f6", tension: 0.3, yAxisID: "y" }},
  566. {{ label: "Peak Temp (°C)", data: temps, borderColor: "#f43f5e", tension: 0.3, yAxisID: "y1" }}
  567. ]
  568. }},
  569. options: {{
  570. responsive: true, maintainAspectRatio: false,
  571. scales: {{
  572. y: {{ title: {{ display: true, text: "Clock (MHz)", color: "#3b82f6" }} }},
  573. y1: {{ position: "right", title: {{ display: true, text: "Temperature (°C)", color: "#f43f5e" }}, grid: {{ drawOnChartArea: false }} }}
  574. }}
  575. }}
  576. }});
  577. </script>
  578. </body>
  579. </html>
  580. """
  581. with open(output_html_path, "w", encoding="utf-8") as f:
  582. f.write(html_content)
  583. print(f"Interactive HTML report saved to {output_html_path}")
  584.  
  585.  
  586. def main():
  587. sampler = NvmlSampler()
  588. atexit.register(lambda: (set_gpu_power(DEFAULT_POWER_W), sampler.close()))
  589.  
  590. print(f"=== Starting RTX 5090 Power Sweep ({POWER_LIMITS[0]}W - {POWER_LIMITS[-1]}W) ===")
  591. results = []
  592.  
  593. try:
  594. for pwr in POWER_LIMITS:
  595. set_gpu_power(pwr)
  596. time.sleep(1.5) # Let power and clocks settle
  597.  
  598. # Run 2 passes to average out any transient jitter
  599. prefill_runs = []
  600. decode_runs = []
  601.  
  602. for r_idx in range(RUNS_PER_STEP):
  603. print(f" [{pwr}W] Run {r_idx+1}/{RUNS_PER_STEP}: Prefill test (~16k tokens)...", end="", flush=True)
  604. p_res = run_prefill_benchmark(sampler)
  605. print(f" -> {p_res['prompt_tps']:.1f} t/s ({p_res['avg_power_w']}W)", flush=True)
  606. prefill_runs.append(p_res)
  607. time.sleep(0.5)
  608.  
  609. print(f" [{pwr}W] Run {r_idx+1}/{RUNS_PER_STEP}: Decode test (~200 tokens)...", end="", flush=True)
  610. d_res = run_decode_benchmark(sampler)
  611. print(f" -> {d_res['decode_tps']:.1f} t/s ({d_res['avg_power_w']}W)", flush=True)
  612. decode_runs.append(d_res)
  613. time.sleep(0.5)
  614.  
  615. # Average metrics across runs
  616. avg_p_tps = float(np.mean([x["prompt_tps"] for x in prefill_runs]))
  617. avg_p_pwr = float(np.mean([x["avg_power_w"] for x in prefill_runs]))
  618. avg_p_sm = float(np.mean([x["avg_sm_mhz"] for x in prefill_runs]))
  619. max_p_temp = int(np.max([x["max_temp_c"] for x in prefill_runs]))
  620.  
  621. avg_d_tps = float(np.mean([x["decode_tps"] for x in decode_runs]))
  622. avg_d_pwr = float(np.mean([x["avg_power_w"] for x in decode_runs]))
  623. avg_d_sm = float(np.mean([x["avg_sm_mhz"] for x in decode_runs]))
  624. max_d_temp = int(np.max([x["max_temp_c"] for x in decode_runs]))
  625. avg_draft_acc = float(np.mean([x["draft_acc_pct"] for x in decode_runs]))
  626.  
  627. eff_p = avg_p_tps / avg_p_pwr if avg_p_pwr > 0 else 0.0
  628. eff_d = avg_d_tps / avg_d_pwr if avg_d_pwr > 0 else 0.0
  629.  
  630. step_data = {
  631. "power_limit_w": pwr,
  632. "prefill": {
  633. "prompt_tps": round(avg_p_tps, 1),
  634. "avg_power_w": round(avg_p_pwr, 1),
  635. "avg_sm_mhz": round(avg_p_sm, 0),
  636. "max_temp_c": max_p_temp,
  637. "runs": prefill_runs
  638. },
  639. "decode": {
  640. "decode_tps": round(avg_d_tps, 1),
  641. "avg_power_w": round(avg_d_pwr, 1),
  642. "avg_sm_mhz": round(avg_d_sm, 0),
  643. "max_temp_c": max_d_temp,
  644. "draft_acc_pct": round(avg_draft_acc, 1),
  645. "runs": decode_runs
  646. },
  647. "efficiency": {
  648. "prefill_tok_per_joule": round(eff_p, 3),
  649. "decode_tok_per_joule": round(eff_d, 3),
  650. "prefill_joules_per_tok": round(avg_p_pwr / avg_p_tps, 4) if avg_p_tps else 0,
  651. "decode_joules_per_tok": round(avg_d_pwr / avg_d_tps, 4) if avg_d_tps else 0
  652. }
  653. }
  654. results.append(step_data)
  655. print(f"==> [{pwr}W Summary] Prefill: {avg_p_tps:.1f} t/s ({eff_p:.2f} tok/J) | Decode: {avg_d_tps:.1f} t/s ({eff_d:.2f} tok/J)\n")
  656.  
  657. finally:
  658. set_gpu_power(DEFAULT_POWER_W)
  659. sampler.close()
  660.  
  661. # Save JSON results
  662. json_path = "scripts/power_sweep_results.json"
  663. with open(json_path, "w", encoding="utf-8") as f:
  664. json.dump(results, f, indent=2)
  665. print(f"Results written to {json_path}")
  666.  
  667. # Generate visual outputs
  668. png_path = "scripts/power_sweep_chart.png"
  669. html_path = "scripts/power_sweep_report.html"
  670. generate_charts(results, png_path, html_path)
  671. print("=== Sweep Complete ===")
  672.  
  673.  
  674. if __name__ == "__main__":
  675. main()
Advertisement
Add Comment
Please, Sign In to add comment