File size: 7,611 Bytes
309b968 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | """DaisyChain dashboard (CLI: `daisychain-dashboard`). Tailwind-styled page with
a readiness banner, P2P connectivity scan, pooled resource + capacity plan, and
live training status. Config via DAISY_NODES_FILE. Serves on :8080."""
import json
import os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
try:
from daisychain.dashboard.scanner import scan_cluster
except Exception:
from scanner import scan_cluster # running from the folder directly
NODES_FILE = os.environ.get("DAISY_NODES_FILE", "config/nodes.example.json")
PORT = int(os.environ.get("DAISY_DASH_PORT", "8080"))
EXPECTED_WORLD = os.environ.get("DAISY_EXPECTED_WORLD")
BASE_BATCH = int(os.environ.get("DAISY_BASE_BATCH", "32"))
def load_nodes():
with open(NODES_FILE) as f:
return json.load(f)
def _chip(ok, yes="OK", no="DOWN"):
cls = ("bg-emerald-500/20 text-emerald-600 dark:text-emerald-400" if ok
else "bg-rose-500/20 text-rose-600 dark:text-rose-400")
return f'<span class="px-2 py-0.5 rounded text-xs font-semibold {cls}">{yes if ok else no}</span>'
def render(d):
ready = d["ready"]
banner = ("bg-emerald-500", "✓ CLUSTER READY — all nodes connected") if ready \
else ("bg-rose-500", f"✗ NOT READY — {d['reachable']}/{d['total']} nodes reachable")
rows = ""
for n in d["nodes"]:
lat = f'{n["latency_ms"]} ms' if n["latency_ms"] is not None else "—"
res = n.get("resources") or {}
dev = res.get("device", "—")
gpu = (res.get("gpu") or {}).get("name", "")
devlabel = f"{dev}" + (f" ({gpu})" if gpu else "")
rows += f"""<tr class="border-b border-slate-100 dark:border-slate-800">
<td class="py-2 px-3 font-medium">{n['name']}</td>
<td class="py-2 px-3 text-slate-500">{n['host']}</td>
<td class="py-2 px-3">{_chip(n['reachable'],'reachable','unreachable')}</td>
<td class="py-2 px-3">{devlabel}</td>
<td class="py-2 px-3 tabular-nums">{lat}</td></tr>"""
plan = d["plan"]
pr = ""
for p in plan["per_node"]:
dv = p["device"] + (f" ({p['gpu']})" if p.get("gpu") else "")
ram = f'{p["ram_gb"]} GB' if p.get("ram_gb") is not None else "—"
pr += f"""<tr class="border-b border-slate-100 dark:border-slate-800">
<td class="py-2 px-3 font-medium">{p['name']}</td>
<td class="py-2 px-3">{dv}</td>
<td class="py-2 px-3 tabular-nums">{ram}</td>
<td class="py-2 px-3 tabular-nums">{p['capacity']}</td>
<td class="py-2 px-3 tabular-nums">{p['weight']}</td>
<td class="py-2 px-3 tabular-nums">{p['batch']}</td></tr>"""
tot_ram = f'{plan["total_ram_gb"]} GB' if plan["total_ram_gb"] is not None else "—"
t = d["training"]
if t:
step, total = t.get("step", 0), t.get("total_steps", 1)
pct = int(100 * (step + 1) / max(1, total)); loss = t.get("cluster_avg_loss")
badge = _chip(True, "DONE") if t.get("done") else '<span class="text-xs text-sky-500 animate-pulse">training…</span>'
train = f"""<div class="flex items-center justify-between mb-2">
<span class="text-sm text-slate-500">step {step} / {total}</span>{badge}</div>
<div class="w-full h-3 rounded-full bg-slate-200 dark:bg-slate-700 overflow-hidden">
<div class="h-3 bg-sky-500" style="width:{pct}%"></div></div>
<p class="mt-3 text-2xl font-semibold tabular-nums">{loss:.5f}
<span class="text-sm font-normal text-slate-500">cluster-avg loss</span></p>"""
else:
train = '<p class="text-slate-400 text-sm">No active run detected (waiting for rank 0 status)…</p>'
return f"""<!doctype html><html><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="refresh" content="3"><title>DaisyChain — Cluster</title>
<script src="https://cdn.tailwindcss.com"></script></head>
<body class="bg-slate-50 dark:bg-slate-900 text-slate-800 dark:text-slate-100 min-h-screen">
<div class="max-w-5xl mx-auto p-6 space-y-6">
<header class="flex items-center justify-between">
<div><h1 class="text-2xl font-bold">\U0001f33c DaisyChain</h1>
<p class="text-sm text-slate-500">Old Hardware Training Pipeline · scanned {d['scanned_at']}</p></div>
<span class="text-xs text-slate-400">auto-refresh 3s</span></header>
<div class="{banner[0]} text-white rounded-xl px-5 py-4 font-semibold text-lg shadow">{banner[1]}</div>
<div class="grid md:grid-cols-3 gap-4">
<div class="rounded-xl border border-slate-200 dark:border-slate-700 p-4"><p class="text-xs uppercase tracking-wide text-slate-400">Nodes</p><p class="text-3xl font-bold tabular-nums">{d['reachable']}/{d['total']}</p></div>
<div class="rounded-xl border border-slate-200 dark:border-slate-700 p-4"><p class="text-xs uppercase tracking-wide text-slate-400">Total cores</p><p class="text-3xl font-bold tabular-nums">{plan['total_cores']}</p></div>
<div class="rounded-xl border border-slate-200 dark:border-slate-700 p-4"><p class="text-xs uppercase tracking-wide text-slate-400">Total RAM</p><p class="text-3xl font-bold tabular-nums">{tot_ram}</p></div></div>
<section class="rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden">
<h2 class="px-4 py-3 font-semibold border-b border-slate-100 dark:border-slate-800">Connectivity scan</h2>
<table class="w-full text-sm"><thead class="text-left text-slate-400"><tr>
<th class="py-2 px-3">Node</th><th class="py-2 px-3">Host</th><th class="py-2 px-3">Agent</th>
<th class="py-2 px-3">Device</th><th class="py-2 px-3">Latency</th></tr></thead><tbody>{rows}</tbody></table></section>
<div class="grid md:grid-cols-2 gap-6">
<section class="rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden">
<h2 class="px-4 py-3 font-semibold border-b border-slate-100 dark:border-slate-800">Capacity plan · global batch {plan['global_batch']}</h2>
<table class="w-full text-sm"><thead class="text-left text-slate-400"><tr>
<th class="py-2 px-3">Node</th><th class="py-2 px-3">Device</th><th class="py-2 px-3">RAM</th>
<th class="py-2 px-3">Capacity</th><th class="py-2 px-3">Weight</th><th class="py-2 px-3">Batch</th></tr></thead><tbody>{pr}</tbody></table></section>
<section class="rounded-xl border border-slate-200 dark:border-slate-700 p-4">
<h2 class="font-semibold mb-3">Live training</h2>{train}</section></div>
<footer class="text-center text-xs text-slate-400 pt-2">DaisyChainAI · pools compute, not memory · small models on spare hardware</footer>
</div></body></html>"""
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
data = scan_cluster(load_nodes(),
int(EXPECTED_WORLD) if EXPECTED_WORLD else None, BASE_BATCH)
if self.path.startswith("/api"):
body = json.dumps(data).encode(); ctype = "application/json"
else:
body = render(data).encode(); ctype = "text/html; charset=utf-8"
self.send_response(200); self.send_header("Content-Type", ctype)
self.send_header("Content-Length", str(len(body))); self.end_headers()
self.wfile.write(body)
def log_message(self, *a):
pass
def main():
print(f"[dashboard] :{PORT} nodes={NODES_FILE}", flush=True)
ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever()
if __name__ == "__main__":
main()
|