PrismML Deploy commited on
Commit
0633a27
Β·
1 Parent(s): 4339749

Bonsai-demo: multi-GPU llama.cpp server with live dashboard and metrics

Browse files
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ bin/*.so.* filter=lfs diff=lfs merge=lfs -text
37
+ bin/llama-* filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM nvidia/cuda:12.8.0-runtime-ubuntu22.04
2
+
3
+ RUN apt-get update && apt-get install -y --no-install-recommends \
4
+ curl ca-certificates git libgomp1 nginx openssl python3 \
5
+ && rm -rf /var/lib/apt/lists/* \
6
+ && chown -R 1000:1000 /var/lib/nginx /var/log/nginx /run
7
+
8
+ RUN useradd -m -u 1000 user
9
+ USER user
10
+ ENV HOME=/home/user PATH="/home/user/.local/bin:/app/bin:$PATH"
11
+
12
+ WORKDIR /app
13
+
14
+ COPY --chown=user bin/ /app/bin/
15
+
16
+ ENV LD_LIBRARY_PATH=/app/bin
17
+
18
+ COPY --chown=user entrypoint.sh /app/entrypoint.sh
19
+ COPY --chown=user nginx.conf /app/nginx.conf
20
+ COPY --chown=user dashboard.html /app/dashboard.html
21
+ COPY --chown=user metrics_pusher.py /app/metrics_pusher.py
22
+ RUN chmod +x /app/entrypoint.sh
23
+
24
+ EXPOSE 7860
25
+
26
+ CMD ["/app/entrypoint.sh"]
README.md CHANGED
@@ -1,10 +1,26 @@
1
  ---
2
- title: Website Demo
3
- emoji: πŸ“Š
4
  colorFrom: green
5
- colorTo: green
6
- sdk: static
 
 
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Bonsai Demo
3
+ emoji: 🌿
4
  colorFrom: green
5
+ colorTo: blue
6
+ sdk: docker
7
+ app_port: 7860
8
+ suggested_hardware: l40sx1
9
  pinned: false
10
  ---
11
 
12
+ # Bonsai Demo
13
+
14
+ Interactive demo for [Bonsai](https://huggingface.co/collections/prism-ml/bonsai), end-to-end 1-bit language models by [Prism ML](https://prismml.com).
15
+
16
+ > **This demo will be available for a limited time (approximately 1–2 weeks).** Enjoy it while it lasts!
17
+
18
+ ## Privacy
19
+
20
+ - **We do not log any messages.** Chat content is never stored on the server.
21
+ - This demo uses the built-in llama-server UI, which saves your conversation history **in your browser's local storage only**. Clearing your browser cache will erase it.
22
+ - That said, **please do not submit sensitive, private, or confidential information** in your messages.
23
+
24
+ ## Fair Use
25
+
26
+ We've allocated multiple GPUs to keep this demo responsive, but resources are shared across all users. Under heavy load you may experience slower responses or brief queuing. Please be mindful of usage and avoid sending large bursts of automated requests so everyone can enjoy the demo.
bin/libggml-base.so ADDED
@@ -0,0 +1 @@
 
 
1
+ libggml-base.so.0
bin/libggml-base.so.0 ADDED
@@ -0,0 +1 @@
 
 
1
+ libggml-base.so.0.9.7
bin/libggml-base.so.0.9.7 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:34c567f790d8f6b6ce63ae446431d737e382d10bbb7f634e79855749d75c79a6
3
+ size 787520
bin/libggml-cpu.so ADDED
@@ -0,0 +1 @@
 
 
1
+ libggml-cpu.so.0
bin/libggml-cpu.so.0 ADDED
@@ -0,0 +1 @@
 
 
1
+ libggml-cpu.so.0.9.7
bin/libggml-cpu.so.0.9.7 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:57b6c7bf5e9424bc52df6fc8f5503b22f029df912fee65f342b441d5032fb8e3
3
+ size 1107144
bin/libggml-cuda.so ADDED
@@ -0,0 +1 @@
 
 
1
+ libggml-cuda.so.0
bin/libggml-cuda.so.0 ADDED
@@ -0,0 +1 @@
 
 
1
+ libggml-cuda.so.0.9.7
bin/libggml-cuda.so.0.9.7 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f9274ff637e09a5beb5d2ec55a475790bbd22ea3b6095b975ab4bbc18001ccd9
3
+ size 55252864
bin/libggml.so ADDED
@@ -0,0 +1 @@
 
 
1
+ libggml.so.0
bin/libggml.so.0 ADDED
@@ -0,0 +1 @@
 
 
1
+ libggml.so.0.9.7
bin/libggml.so.0.9.7 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cebe9400f91bbea8c0c1663d8a7582299835bd1d040dcd4a326ac067b4adddd9
3
+ size 55184
bin/libllama.so ADDED
@@ -0,0 +1 @@
 
 
1
+ libllama.so.0
bin/libllama.so.0 ADDED
@@ -0,0 +1 @@
 
 
1
+ libllama.so.0.0.8190
bin/libllama.so.0.0.8190 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2303b49c572d5b7b36123046cad7469d8b3caf8778834b37dc6df546fb341588
3
+ size 3203944
bin/libmtmd.so ADDED
@@ -0,0 +1 @@
 
 
1
+ libmtmd.so.0
bin/libmtmd.so.0 ADDED
@@ -0,0 +1 @@
 
 
1
+ libmtmd.so.0.0.8190
bin/libmtmd.so.0.0.8190 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ff0f675a01f13961e6f545ef414ae6cc50fcfa65379aa9ca1032437cd16194a3
3
+ size 943272
bin/llama-bench ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:52825c5cbf617cd86db04a143188bee3d666e789168a897ced04001882afe2aa
3
+ size 592680
bin/llama-cli ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4c09f893698bf498dc872b950635bcae961de90ce67dadc83c62d34c9812ead0
3
+ size 5665432
bin/llama-completion ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1b126040b118cb1bf0c041e2951e78a00aa2c619c414cc380b361c59c1d5980d
3
+ size 4642520
bin/llama-perplexity ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:445b36510eaa53b9ee1ee641a5672b0dd7642be921a2a043f7ebd639b084dd64
3
+ size 4680640
bin/llama-quantize ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6114cda3bf733678f49dab714d689b53f8505889ccae88650003d7a86a90f783
3
+ size 442464
bin/llama-server ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b33f7d0abc362fdd49175df2b66741b4784c737f348cba5175220c6c871246dd
3
+ size 7354688
dashboard.html ADDED
@@ -0,0 +1,415 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Bonsai-demo β€” Dashboard</title>
7
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
8
+ <style>
9
+ :root {
10
+ --bg: #0d0f14;
11
+ --surface: #161920;
12
+ --border: #252830;
13
+ --text: #e2e4ea;
14
+ --muted: #6b7280;
15
+ --green: #22c55e;
16
+ --blue: #3b82f6;
17
+ --orange: #f97316;
18
+ --purple: #a855f7;
19
+ --red: #ef4444;
20
+ }
21
+ * { box-sizing: border-box; margin: 0; padding: 0; }
22
+ body {
23
+ background: var(--bg);
24
+ color: var(--text);
25
+ font-family: 'SF Mono', 'Fira Code', monospace;
26
+ font-size: 13px;
27
+ min-height: 100vh;
28
+ padding: 20px 24px;
29
+ }
30
+
31
+ /* ── header ── */
32
+ header {
33
+ display: flex;
34
+ align-items: center;
35
+ justify-content: space-between;
36
+ margin-bottom: 20px;
37
+ gap: 16px;
38
+ flex-wrap: wrap;
39
+ }
40
+ header h1 { font-size: 15px; font-weight: 600; letter-spacing: 0.05em; }
41
+ .header-right { display: flex; align-items: center; gap: 20px; }
42
+ #status { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--muted); }
43
+ #dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); transition: background 0.3s; }
44
+ #dot.live { background: var(--green); box-shadow: 0 0 6px var(--green); }
45
+ #dot.error { background: var(--red); }
46
+ .analytics-ts { font-size: 11px; color: var(--muted); }
47
+ .analytics-ts span { color: var(--text); }
48
+
49
+ /* ── server bar ── */
50
+ .url-bar { display: flex; gap: 8px; margin-bottom: 20px; align-items: center; }
51
+ .url-bar label { color: var(--muted); font-size: 11px; white-space: nowrap; }
52
+ .url-bar input {
53
+ flex: 1; background: var(--surface); border: 1px solid var(--border);
54
+ border-radius: 6px; color: var(--text); font-family: inherit;
55
+ font-size: 12px; padding: 6px 10px; outline: none;
56
+ }
57
+ .url-bar input:focus { border-color: var(--blue); }
58
+ .url-bar button {
59
+ background: var(--blue); border: none; border-radius: 6px; color: #fff;
60
+ cursor: pointer; font-family: inherit; font-size: 12px; padding: 6px 14px;
61
+ }
62
+
63
+ /* ── section labels ── */
64
+ .section-label {
65
+ font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;
66
+ color: var(--muted); margin-bottom: 10px; margin-top: 20px;
67
+ }
68
+ .section-label:first-of-type { margin-top: 0; }
69
+
70
+ /* ── stat grids ── */
71
+ .stats-live {
72
+ display: grid;
73
+ grid-template-columns: repeat(4, 1fr);
74
+ gap: 10px;
75
+ margin-bottom: 14px;
76
+ }
77
+ .stats-hist {
78
+ display: grid;
79
+ grid-template-columns: repeat(5, 1fr);
80
+ gap: 10px;
81
+ margin-bottom: 20px;
82
+ }
83
+ @media (max-width: 900px) {
84
+ .stats-live { grid-template-columns: repeat(2, 1fr); }
85
+ .stats-hist { grid-template-columns: repeat(3, 1fr); }
86
+ }
87
+ .stat {
88
+ background: var(--surface); border: 1px solid var(--border);
89
+ border-radius: 8px; padding: 14px 16px;
90
+ }
91
+ .stat-label { font-size: 10px; letter-spacing: 0.07em; color: var(--muted); text-transform: uppercase; margin-bottom: 6px; }
92
+ .stat-value { font-size: 26px; font-weight: 700; line-height: 1; }
93
+ .stat-unit { font-size: 10px; color: var(--muted); margin-top: 4px; }
94
+
95
+ /* ── charts ── */
96
+ .charts-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 20px; }
97
+ @media (max-width: 900px) { .charts-2 { grid-template-columns: 1fr; } }
98
+ .chart-card {
99
+ background: var(--surface); border: 1px solid var(--border);
100
+ border-radius: 8px; padding: 16px;
101
+ }
102
+ .chart-card h2 {
103
+ font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase;
104
+ color: var(--muted); margin-bottom: 12px;
105
+ }
106
+ .chart-card canvas { max-height: 160px; }
107
+
108
+ /* ── GPU ── */
109
+ .gpu-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 10px; }
110
+ .gpu-card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 14px; }
111
+ .gpu-name { font-size: 12px; font-weight: 600; margin-bottom: 10px; }
112
+ .gpu-index { color: var(--muted); font-weight: 400; }
113
+ .gpu-row { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
114
+ .gpu-label { width: 70px; font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; flex-shrink: 0; }
115
+ .bar-track { flex: 1; height: 7px; background: var(--border); border-radius: 4px; overflow: hidden; }
116
+ .bar-fill { height: 100%; border-radius: 4px; transition: width .4s ease; }
117
+ .gpu-val { width: 60px; text-align: right; font-size: 11px; color: var(--text); flex-shrink: 0; }
118
+ .gpu-meta { display: flex; gap: 14px; margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--border); flex-wrap: wrap; }
119
+ .gpu-meta-item { font-size: 11px; color: var(--muted); }
120
+ .gpu-meta-item span { color: var(--text); }
121
+ </style>
122
+ </head>
123
+ <body>
124
+
125
+ <header>
126
+ <h1>🌿 Bonsai-demo · Dashboard</h1>
127
+ <div class="header-right">
128
+ <div class="analytics-ts">requests updated <span id="u-updated">β€”</span></div>
129
+ <div id="status">
130
+ <div id="dot"></div>
131
+ <span id="status-text">connecting…</span>
132
+ </div>
133
+ </div>
134
+ </header>
135
+
136
+ <div class="url-bar">
137
+ <label>SERVER</label>
138
+ <input id="server-url" type="text" value="" placeholder="(same origin β€” leave blank for deployed, or http://host:port for local)">
139
+ <button onclick="reconnect()">Connect</button>
140
+ </div>
141
+
142
+ <!-- ── Live ── -->
143
+ <div class="section-label">Live</div>
144
+ <div class="stats-live" style="grid-template-columns: repeat(6, 1fr);">
145
+ <div class="stat">
146
+ <div class="stat-label">Active</div>
147
+ <div class="stat-value" id="v-active" style="color:var(--orange)">β€”</div>
148
+ <div class="stat-unit">requests processing</div>
149
+ </div>
150
+ <div class="stat">
151
+ <div class="stat-label">Queued</div>
152
+ <div class="stat-value" id="v-queued" style="color:var(--purple)">β€”</div>
153
+ <div class="stat-unit">requests waiting</div>
154
+ </div>
155
+ <div class="stat">
156
+ <div class="stat-label">Avg Latency</div>
157
+ <div class="stat-value" id="v-latency-avg" style="color:var(--text)">β€”</div>
158
+ <div class="stat-unit">ms Β· last 5 min</div>
159
+ </div>
160
+ <div class="stat">
161
+ <div class="stat-label">p90 Latency</div>
162
+ <div class="stat-value" id="v-latency-p90" style="color:var(--text)">β€”</div>
163
+ <div class="stat-unit">ms Β· last 5 min</div>
164
+ </div>
165
+ <div class="stat">
166
+ <div class="stat-label">Gen Speed</div>
167
+ <div class="stat-value" id="v-gen" style="color:var(--green)">β€”</div>
168
+ <div class="stat-unit">tok / s Β· avg</div>
169
+ </div>
170
+ <div class="stat">
171
+ <div class="stat-label">Prompt Speed</div>
172
+ <div class="stat-value" id="v-prompt" style="color:var(--blue)">β€”</div>
173
+ <div class="stat-unit">tok / s Β· avg</div>
174
+ </div>
175
+ </div>
176
+
177
+ <div class="charts-2">
178
+ <div class="chart-card">
179
+ <h2>Concurrency β€” active &amp; queued slots</h2>
180
+ <canvas id="chart-slots"></canvas>
181
+ </div>
182
+ <div class="chart-card">
183
+ <h2>Generation β€” tok / s (current)</h2>
184
+ <canvas id="chart-gen"></canvas>
185
+ </div>
186
+ <div class="chart-card">
187
+ <h2>Prompt processing β€” tok / s (current)</h2>
188
+ <canvas id="chart-prompt"></canvas>
189
+ </div>
190
+ </div>
191
+
192
+ <!-- ── Historical ── -->
193
+ <div class="section-label">Historical</div>
194
+ <div class="stats-hist">
195
+ <div class="stat">
196
+ <div class="stat-label">Requests β€” 24h</div>
197
+ <div class="stat-value" id="u-req-1d" style="color:var(--blue)">β€”</div>
198
+ <div class="stat-unit">chat completions</div>
199
+ </div>
200
+ <div class="stat">
201
+ <div class="stat-label">Requests β€” 7d</div>
202
+ <div class="stat-value" id="u-req-7d" style="color:var(--blue)">β€”</div>
203
+ <div class="stat-unit">chat completions</div>
204
+ </div>
205
+ <div class="stat">
206
+ <div class="stat-label">Requests β€” Total</div>
207
+ <div class="stat-value" id="u-req-total" style="color:var(--text)">β€”</div>
208
+ <div class="stat-unit">since last restart</div>
209
+ </div>
210
+ <div class="stat">
211
+ <div class="stat-label">Tokens Generated</div>
212
+ <div class="stat-value" id="v-total-tok" style="color:var(--text)">β€”</div>
213
+ <div class="stat-unit">cumulative</div>
214
+ </div>
215
+ <div class="stat">
216
+ <div class="stat-label">Decodes</div>
217
+ <div class="stat-value" id="v-decodes" style="color:var(--text)">β€”</div>
218
+ <div class="stat-unit">llama_decode() calls</div>
219
+ </div>
220
+ </div>
221
+
222
+ <div class="charts-2">
223
+ <div class="chart-card">
224
+ <h2>Requests per hour β€” last 24h</h2>
225
+ <canvas id="chart-req-hour"></canvas>
226
+ </div>
227
+ <div class="chart-card">
228
+ <h2>Tokens generated (cumulative)</h2>
229
+ <canvas id="chart-tokens"></canvas>
230
+ </div>
231
+ </div>
232
+
233
+ <!-- ── GPU ── -->
234
+ <div class="section-label">GPU Health</div>
235
+ <div id="gpu-panel"><div style="color:var(--muted);font-size:12px;">loading…</div></div>
236
+
237
+ <script>
238
+ const MAX_POINTS = 120;
239
+ let serverUrl = '', timer = null;
240
+
241
+ // ── chart defaults ──────────────────────────────────────────────
242
+ const chartDefaults = {
243
+ responsive: true, animation: false,
244
+ plugins: { legend: { labels: { color: '#6b7280', font: { size: 11 } } } },
245
+ scales: {
246
+ x: { ticks: { color: '#6b7280', maxTicksLimit: 6, font: { size: 10 } }, grid: { color: '#1e2028' } },
247
+ y: { ticks: { color: '#6b7280', font: { size: 10 } }, grid: { color: '#1e2028' }, beginAtZero: true },
248
+ },
249
+ };
250
+ const BAR_DEFAULTS = {
251
+ responsive: true, animation: false,
252
+ plugins: { legend: { display: false } },
253
+ scales: {
254
+ x: { ticks: { color: '#6b7280', font: { size: 10 }, maxRotation: 45 }, grid: { color: '#1e2028' } },
255
+ y: { ticks: { color: '#6b7280', font: { size: 10 } }, grid: { color: '#1e2028' }, beginAtZero: true },
256
+ },
257
+ };
258
+
259
+ function mkLine(id, datasets) {
260
+ return new Chart(document.getElementById(id), {
261
+ type: 'line', data: { labels: [], datasets },
262
+ options: JSON.parse(JSON.stringify(chartDefaults)),
263
+ });
264
+ }
265
+
266
+ const charts = {
267
+ slots: mkLine('chart-slots', [
268
+ { label: 'Active', data: [], borderColor: '#f97316', backgroundColor: '#f9731628', fill: true, tension: 0.3, pointRadius: 0, borderWidth: 2 },
269
+ { label: 'Queued', data: [], borderColor: '#a855f7', backgroundColor: '#a855f720', fill: true, tension: 0.3, pointRadius: 0, borderWidth: 2 },
270
+ ]),
271
+ gen: mkLine('chart-gen', [
272
+ { label: 'Gen tok/s', data: [], borderColor: '#22c55e', backgroundColor: '#22c55e18', fill: true, tension: 0.3, pointRadius: 0, borderWidth: 2 },
273
+ ]),
274
+ prompt: mkLine('chart-prompt', [
275
+ { label: 'Prompt tok/s', data: [], borderColor: '#3b82f6', backgroundColor: '#3b82f618', fill: true, tension: 0.3, pointRadius: 0, borderWidth: 2 },
276
+ ]),
277
+ tokens: mkLine('chart-tokens', [
278
+ { label: 'Tokens', data: [], borderColor: '#e2e4ea', backgroundColor: '#e2e4ea10', fill: true, tension: 0.3, pointRadius: 0, borderWidth: 2 },
279
+ ]),
280
+ };
281
+ const aC = {
282
+ reqHour: new Chart(document.getElementById('chart-req-hour'), {
283
+ type: 'bar',
284
+ data: { labels: [], datasets: [{ data: [], backgroundColor: '#3b82f660', borderColor: '#3b82f6', borderWidth: 1 }] },
285
+ options: JSON.parse(JSON.stringify(BAR_DEFAULTS)),
286
+ }),
287
+ };
288
+
289
+ // ── helpers ─────────────────────────────────────────────────────
290
+ function parseMetrics(text) {
291
+ const m = {};
292
+ for (const line of text.split('\n')) {
293
+ if (line.startsWith('#') || !line.trim()) continue;
294
+ const match = line.match(/^([^\s{]+)(?:\{[^}]*\})?\s+([\d.eE+\-]+)/);
295
+ if (match) m[match[1]] = parseFloat(match[2]);
296
+ }
297
+ return m;
298
+ }
299
+ function fmt(v, dec = 1) {
300
+ if (v === undefined || isNaN(v)) return 'β€”';
301
+ return v >= 1000 ? (v / 1000).toFixed(1) + 'k' : v.toFixed(dec);
302
+ }
303
+ function push(chart, label, ...values) {
304
+ chart.data.labels.push(label);
305
+ values.forEach((v, i) => chart.data.datasets[i].data.push(v));
306
+ if (chart.data.labels.length > MAX_POINTS) {
307
+ chart.data.labels.shift();
308
+ chart.data.datasets.forEach(d => d.data.shift());
309
+ }
310
+ chart.update('none');
311
+ }
312
+
313
+ // ── live metrics tick ────────────────────────────────────────────
314
+ async function tick() {
315
+ const dot = document.getElementById('dot');
316
+ const statusText = document.getElementById('status-text');
317
+ const ts = new Date().toLocaleTimeString();
318
+ try {
319
+ const res = await fetch(serverUrl + '/metrics-2e215f981f3f?t=' + Date.now());
320
+ if (!res.ok) throw new Error('HTTP ' + res.status);
321
+ const m = parseMetrics(await res.text());
322
+
323
+ dot.className = 'live';
324
+ statusText.textContent = 'live Β· ' + ts;
325
+
326
+ // stat cards: lifetime averages (always meaningful, never stuck at 0)
327
+ const genAvg = m['llamacpp:predicted_tokens_seconds'] ?? 0;
328
+ const promptAvg = m['llamacpp:prompt_tokens_seconds'] ?? 0;
329
+ const active = m['llamacpp:requests_processing'] || 0;
330
+ const queued = m['llamacpp:requests_deferred'] || 0;
331
+
332
+ document.getElementById('v-active').textContent = fmt(active, 0);
333
+ document.getElementById('v-queued').textContent = fmt(queued, 0);
334
+ document.getElementById('v-gen').textContent = fmt(genAvg);
335
+ document.getElementById('v-prompt').textContent = fmt(promptAvg);
336
+ document.getElementById('v-total-tok').textContent = fmt(m['llamacpp:tokens_predicted_total'], 0);
337
+ document.getElementById('v-decodes').textContent = fmt(m['llamacpp:n_decode_total'], 0);
338
+
339
+ // charts: delta-based current rates (go to 0 when idle)
340
+ const genRate = m['bonsai:gen_rate_current'] || 0;
341
+ const promptRate = m['bonsai:prompt_rate_current'] || 0;
342
+
343
+ push(charts.slots, ts, active, queued);
344
+ push(charts.gen, ts, genRate);
345
+ push(charts.prompt, ts, promptRate);
346
+ push(charts.tokens, ts, m['llamacpp:tokens_predicted_total'] || 0);
347
+ } catch (e) {
348
+ dot.className = 'error';
349
+ statusText.textContent = 'error Β· ' + e.message;
350
+ }
351
+ }
352
+
353
+ function reconnect() {
354
+ serverUrl = document.getElementById('server-url').value.replace(/\/$/, '');
355
+ if (timer) clearInterval(timer);
356
+ tick();
357
+ timer = setInterval(tick, 2000);
358
+ }
359
+ reconnect();
360
+
361
+ // ── GPU ─────────────────────────────────────────────────────────
362
+ function utilColor(pct) { return pct >= 90 ? '#ef4444' : pct >= 60 ? '#f97316' : '#22c55e'; }
363
+
364
+ function renderGPU(gpus) {
365
+ const panel = document.getElementById('gpu-panel');
366
+ if (!gpus || !gpus.length) {
367
+ panel.innerHTML = '<div style="color:var(--muted);font-size:12px;">No GPU data.</div>';
368
+ return;
369
+ }
370
+ panel.innerHTML = '<div class="gpu-grid">' + gpus.map(g => {
371
+ const utilPct = g.util_gpu.toFixed(0);
372
+ const memPct = (g.mem_used_mib / g.mem_total_mib * 100).toFixed(0);
373
+ const memUsedGB = (g.mem_used_mib / 1024).toFixed(1);
374
+ const memTotGB = (g.mem_total_mib / 1024).toFixed(0);
375
+ const powerPct = g.power_limit_w > 0 ? (g.power_w / g.power_limit_w * 100).toFixed(0) : 0;
376
+ return `<div class="gpu-card">
377
+ <div class="gpu-name"><span class="gpu-index">GPU ${g.index} &nbsp;Β·&nbsp;</span>${g.name}</div>
378
+ <div class="gpu-row"><div class="gpu-label">Compute</div><div class="bar-track"><div class="bar-fill" style="width:${utilPct}%;background:${utilColor(g.util_gpu)}"></div></div><div class="gpu-val">${utilPct}%</div></div>
379
+ <div class="gpu-row"><div class="gpu-label">VRAM</div><div class="bar-track"><div class="bar-fill" style="width:${memPct}%;background:${utilColor(+memPct)}"></div></div><div class="gpu-val">${memUsedGB} / ${memTotGB} GB</div></div>
380
+ <div class="gpu-row"><div class="gpu-label">Power</div><div class="bar-track"><div class="bar-fill" style="width:${powerPct}%;background:#3b82f6"></div></div><div class="gpu-val">${g.power_w.toFixed(0)} W</div></div>
381
+ <div class="gpu-meta">
382
+ <div class="gpu-meta-item">Temp <span>${g.temp_c.toFixed(0)}Β°C</span></div>
383
+ <div class="gpu-meta-item">SM clock <span>${g.clock_sm_mhz.toFixed(0)} MHz</span></div>
384
+ <div class="gpu-meta-item">Mem util <span>${g.util_mem.toFixed(0)}%</span></div>
385
+ </div></div>`;
386
+ }).join('') + '</div>';
387
+ }
388
+
389
+ async function fetchGPU() {
390
+ try { const r = await fetch(serverUrl + '/gpu-2e215f981f3f?t=' + Date.now()); if (r.ok) renderGPU((await r.json()).gpus); } catch (_) {}
391
+ }
392
+ fetchGPU(); setInterval(fetchGPU, 5000);
393
+
394
+ // ── analytics ───────────────────────────────────────────────────
395
+ async function fetchAnalytics() {
396
+ try {
397
+ const r = await fetch(serverUrl + '/analytics-2e215f981f3f?t=' + Date.now()); if (!r.ok) return;
398
+ const d = await r.json();
399
+ document.getElementById('u-req-1d').textContent = d.summary_24h?.requests ?? 'β€”';
400
+ document.getElementById('u-req-7d').textContent = d.summary_7d?.requests ?? 'β€”';
401
+ document.getElementById('u-req-total').textContent = d.summary_total?.requests ?? 'β€”';
402
+ document.getElementById('u-updated').textContent = d.updated_at ? new Date(d.updated_at).toLocaleTimeString() : 'β€”';
403
+ const lat = d.latency_5m || {};
404
+ document.getElementById('v-latency-avg').textContent = lat.sample_count > 0 ? fmt(lat.avg_ms, 0) : 'β€”';
405
+ document.getElementById('v-latency-p90').textContent = lat.sample_count > 0 ? fmt(lat.p90_ms, 0) : 'β€”';
406
+ const hours = d.requests_by_hour || [];
407
+ aC.reqHour.data.labels = hours.map(h => h.hour.slice(11, 16));
408
+ aC.reqHour.data.datasets[0].data = hours.map(h => h.requests);
409
+ aC.reqHour.update('none');
410
+ } catch (_) {}
411
+ }
412
+ fetchAnalytics(); setInterval(fetchAnalytics, 10000);
413
+ </script>
414
+ </body>
415
+ </html>
entrypoint.sh ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ echo "Starting Bonsai-demo entrypoint..."
5
+
6
+ # ── Validate required secrets ─────────────────────────────────────────────────
7
+ if [ -z "$MODEL_REPO" ] || [ -z "$MODEL_FILE" ]; then
8
+ echo ""
9
+ echo "ERROR: MODEL_REPO and MODEL_FILE secrets must be set."
10
+ echo " Go to Space Settings β†’ Repository Secrets and add:"
11
+ echo " MODEL_REPO = prism-ml/Bonsai-8B-gguf"
12
+ echo " MODEL_FILE = Bonsai-8B.gguf"
13
+ echo ""
14
+ exit 1
15
+ fi
16
+
17
+ MODEL_DIR="/app/models"
18
+ mkdir -p "$MODEL_DIR"
19
+
20
+ download_model() {
21
+ local repo file path retries=5
22
+ repo=$(echo "$1" | tr -d '[:space:]')
23
+ file=$(echo "$2" | tr -d '[:space:]')
24
+ path="$MODEL_DIR/$file"
25
+ local url="https://huggingface.co/$repo/resolve/main/$file"
26
+ for attempt in $(seq 1 $retries); do
27
+ echo "Downloading (attempt $attempt/$retries): $url"
28
+ rm -f "$path"
29
+ if curl -fL --retry 3 --retry-delay 5 -C - -o "$path" "$url" 2>&1; then
30
+ echo "Downloaded: $(ls -lh "$path")"
31
+ return 0
32
+ fi
33
+ echo "Download failed, retrying in 10s..."
34
+ sleep 10
35
+ done
36
+ echo "ERROR: Failed to download $url after $retries attempts"
37
+ exit 1
38
+ }
39
+
40
+ # Primary model (required)
41
+ MODEL_REPO=$(echo "$MODEL_REPO" | tr -d '[:space:]')
42
+ MODEL_FILE=$(echo "$MODEL_FILE" | tr -d '[:space:]')
43
+ download_model "$MODEL_REPO" "$MODEL_FILE"
44
+
45
+ # Additional models (optional)
46
+ MODELS_MAX=1
47
+ for suffix in B C; do
48
+ repo_var="MODEL_REPO_${suffix}"
49
+ file_var="MODEL_FILE_${suffix}"
50
+ repo=$(echo "${!repo_var:-}" | tr -d '[:space:]')
51
+ file=$(echo "${!file_var:-}" | tr -d '[:space:]')
52
+ if [ -n "$repo" ] && [ -n "$file" ]; then
53
+ download_model "$repo" "$file"
54
+ MODELS_MAX=$((MODELS_MAX + 1))
55
+ fi
56
+ done
57
+
58
+ # ── Dashboard auth ────────────────────────────────────────────────────────────
59
+ if [ -n "$DASHBOARD_KEY" ]; then
60
+ HASH=$(openssl passwd -apr1 "$DASHBOARD_KEY")
61
+ echo "admin:$HASH" > /tmp/.htpasswd
62
+ echo "Dashboard auth: enabled (user=admin)"
63
+ else
64
+ echo "WARNING: DASHBOARD_KEY not set, /dash-2e215f981f3f is unprotected"
65
+ printf 'admin:$apr1$open$open\n' > /tmp/.htpasswd
66
+ fi
67
+
68
+ # ── nginx temp dirs ───────────────────────────────────────────────────────────
69
+ mkdir -p /tmp/nginx-{client-body,proxy,fastcgi,uwsgi,scgi}
70
+
71
+ # ── Detect GPUs and start one llama-server per GPU ───────────────────────────
72
+ GPU_COUNT=$(nvidia-smi -L 2>/dev/null | wc -l || echo 1)
73
+ echo "GPUs detected: $GPU_COUNT"
74
+
75
+ BACKENDS=""
76
+ for i in $(seq 0 $((GPU_COUNT - 1))); do
77
+ PORT=$((7861 + i))
78
+ echo "Starting llama-server on GPU $i β†’ port $PORT"
79
+ CUDA_VISIBLE_DEVICES=$i /app/bin/llama-server \
80
+ -m "$MODEL_DIR/$MODEL_FILE" \
81
+ --host 127.0.0.1 \
82
+ --port "$PORT" \
83
+ -ngl 99 \
84
+ -fa on \
85
+ -np 4 \
86
+ -c 262144 \
87
+ --metrics \
88
+ --temp 0.5 --top-p 0.85 --top-k 20 --min-p 0 \
89
+ --reasoning-budget 0 --reasoning-format none \
90
+ --chat-template-kwargs '{"enable_thinking": false}' \
91
+ --log-disable &
92
+ BACKENDS="$BACKENDS server 127.0.0.1:$PORT;\n"
93
+ done
94
+
95
+ printf "upstream llama_backends {\n least_conn;\n${BACKENDS}}\n" > /tmp/nginx-upstream.conf
96
+
97
+ # ── Write stub files so /gpu and /analytics never 404 before first tick ──────
98
+ echo '{"ts":null,"gpus":[]}' > /tmp/gpu-stats.json
99
+ echo '# waiting for first metrics scrape...' > /tmp/llama-metrics.txt
100
+ echo '{"updated_at":null,"summary_24h":{"requests":0,"unique_users":0},"summary_7d":{"requests":0,"unique_users":0},"summary_total":{"requests":0,"unique_users":0},"requests_by_hour":[],"requests_by_day":[],"top_users":[]}' > /tmp/analytics.json
101
+
102
+ # ── Start metrics pusher ──────────────────────────────────────────────────────
103
+ python3 /app/metrics_pusher.py &
104
+
105
+ echo ""
106
+ echo "=== Bonsai-demo ==="
107
+ echo " Models: $(ls "$MODEL_DIR"/*.gguf | xargs -n1 basename | tr '\n' ', ' | sed 's/,$//')"
108
+ echo " GPUs: $GPU_COUNT"
109
+ echo " Port: 7860 (nginx β†’ llama-server)"
110
+ echo ""
111
+
112
+ exec nginx -c /app/nginx.conf
index.html DELETED
@@ -1,19 +0,0 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
metrics_pusher.py ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Scrapes /metrics + nvidia-smi every METRICS_INTERVAL seconds.
4
+ Appends one JSON line per snapshot to /tmp/metrics-YYYY-MM-DD.jsonl.
5
+ Pushes daily file to HF Dataset repo every METRICS_PUSH_INTERVAL seconds.
6
+ Also writes /tmp/gpu-stats.json for the live dashboard GPU panel.
7
+ """
8
+ import base64, json, os, subprocess, threading, time, urllib.request, urllib.error
9
+ from datetime import datetime, timezone, timedelta
10
+ from pathlib import Path
11
+
12
+ BASE_PORT = 7861
13
+ LOG_DIR = Path("/tmp")
14
+ GPU_STATS_FILE = Path("/tmp/gpu-stats.json")
15
+ LLAMA_METRICS_FILE = Path("/tmp/llama-metrics.txt")
16
+ GPU_INTERVAL_SECS = int(os.environ.get("GPU_INTERVAL", "10"))
17
+ SNAPSHOT_SECS = int(os.environ.get("METRICS_INTERVAL", "3"))
18
+ NGINX_LOG = Path("/tmp/nginx-access.log")
19
+ ANALYTICS_FILE = Path("/tmp/analytics.json")
20
+ PUSH_SECS = int(os.environ.get("METRICS_PUSH_INTERVAL", "600"))
21
+ METRICS_REPO = os.environ.get("METRICS_REPO", "")
22
+ HF_TOKEN = os.environ.get("HF_TOKEN", "")
23
+
24
+ def now_utc(): return datetime.now(timezone.utc)
25
+
26
+ def detect_backends():
27
+ backends = []
28
+ for i in range(16):
29
+ url = f"http://127.0.0.1:{BASE_PORT + i}"
30
+ try:
31
+ urllib.request.urlopen(f"{url}/health", timeout=1).close()
32
+ backends.append(url)
33
+ except Exception:
34
+ break
35
+ return backends or [f"http://127.0.0.1:{BASE_PORT}"]
36
+
37
+ def parse_prometheus(text):
38
+ values = {}
39
+ for line in text.splitlines():
40
+ if line.startswith("#") or not line.strip(): continue
41
+ parts = line.split()
42
+ if len(parts) < 2: continue
43
+ name = parts[0].split("{")[0].replace("llamacpp:", "")
44
+ try: values[name] = float(parts[1])
45
+ except ValueError: pass
46
+ return values
47
+
48
+ _prev_tokens = {"predicted": 0.0, "prompt": 0.0, "ts": 0.0}
49
+
50
+ def scrape(backends):
51
+ GAUGES_AVG = {"prompt_tokens_seconds", "predicted_tokens_seconds", "kv_cache_usage_ratio"}
52
+ GAUGES_SUM = {"requests_processing", "requests_deferred"}
53
+ combined = {}
54
+ raw_texts = []
55
+ for url in backends:
56
+ try:
57
+ with urllib.request.urlopen(f"{url}/metrics", timeout=5) as r:
58
+ text = r.read().decode()
59
+ raw_texts.append(text)
60
+ for k, v in parse_prometheus(text).items():
61
+ combined.setdefault(k, []).append(v)
62
+ except Exception as e:
63
+ print(f"[metrics] scrape error {url}: {e}")
64
+ if not combined: return None
65
+
66
+ result = {k: sum(v)/len(v) if k in GAUGES_AVG else sum(v) for k, v in combined.items()}
67
+ result["backend_count"] = len(backends)
68
+
69
+ # Compute delta-based current rates (go to 0 when idle)
70
+ now = time.time()
71
+ elapsed = now - _prev_tokens["ts"] if _prev_tokens["ts"] else SNAPSHOT_SECS
72
+ if elapsed > 0 and _prev_tokens["ts"]:
73
+ cur_pred = result.get("tokens_predicted_total", 0)
74
+ cur_prmpt = result.get("prompt_tokens_total", 0)
75
+ result["gen_rate_current"] = max(0, cur_pred - _prev_tokens["predicted"]) / elapsed
76
+ result["prompt_rate_current"] = max(0, cur_prmpt - _prev_tokens["prompt"]) / elapsed
77
+ else:
78
+ result["gen_rate_current"] = 0.0
79
+ result["prompt_rate_current"] = 0.0
80
+
81
+ _prev_tokens["predicted"] = result.get("tokens_predicted_total", 0)
82
+ _prev_tokens["prompt"] = result.get("prompt_tokens_total", 0)
83
+ _prev_tokens["ts"] = now
84
+
85
+ # Write aggregated metrics as Prometheus text for the dashboard
86
+ active = result.get("requests_processing", 0)
87
+ per_slot = result["gen_rate_current"] / max(active, 1) if active else 0.0
88
+ lines = []
89
+ for k, v in result.items():
90
+ if k in ("backend_count", "gen_rate_current", "prompt_rate_current"):
91
+ continue
92
+ lines.append(f"llamacpp:{k} {v}")
93
+ lines.append(f"bonsai:gen_rate_current {result['gen_rate_current']:.3f}")
94
+ lines.append(f"bonsai:prompt_rate_current {result['prompt_rate_current']:.3f}")
95
+ lines.append(f"bonsai:gen_rate_per_slot {per_slot:.3f}")
96
+ lines.append(f"bonsai:backend_count {result['backend_count']}")
97
+ LLAMA_METRICS_FILE.write_text("\n".join(lines) + "\n")
98
+
99
+ return result
100
+
101
+ def scrape_gpus():
102
+ try:
103
+ out = subprocess.check_output([
104
+ "nvidia-smi",
105
+ "--query-gpu=index,name,utilization.gpu,utilization.memory,"
106
+ "memory.used,memory.total,temperature.gpu,power.draw,power.limit,clocks.sm",
107
+ "--format=csv,noheader,nounits",
108
+ ], timeout=5, stderr=subprocess.DEVNULL).decode()
109
+ gpus = []
110
+ for line in out.strip().splitlines():
111
+ p = [x.strip() for x in line.split(",")]
112
+ if len(p) < 10: continue
113
+ try:
114
+ gpus.append({"index": int(p[0]), "name": p[1],
115
+ "util_gpu": float(p[2]), "util_mem": float(p[3]),
116
+ "mem_used_mib": float(p[4]), "mem_total_mib": float(p[5]),
117
+ "temp_c": float(p[6]), "power_w": float(p[7]),
118
+ "power_limit_w": float(p[8]), "clock_sm_mhz": float(p[9])})
119
+ except (ValueError, IndexError): pass
120
+ return gpus
121
+ except Exception: return []
122
+
123
+ def compute_analytics():
124
+ """Parse nginx access log and write analytics.json with request counts + avg latency."""
125
+ try:
126
+ if not NGINX_LOG.exists():
127
+ return
128
+ now = datetime.now(timezone.utc)
129
+ cutoff_24h = now.replace(minute=0, second=0, microsecond=0) - timedelta(hours=24)
130
+ cutoff_7d = now.replace(hour=0, minute=0, second=0, microsecond=0) - timedelta(days=7)
131
+ cutoff_5m = now - timedelta(minutes=5)
132
+
133
+ by_hour = {}
134
+ by_day = {}
135
+ total = 0
136
+ latencies_5m = []
137
+
138
+ for line in NGINX_LOG.read_text(errors='replace').splitlines():
139
+ parts = line.split('|')
140
+ if len(parts) < 6: continue
141
+ ts_str, method, uri, status = parts[0], parts[1], parts[2], parts[3]
142
+ req_time = parts[5] # nginx $request_time in seconds
143
+ if method != 'POST' or not uri.startswith('/v1/'): continue
144
+ if not status.startswith('2'): continue
145
+ try:
146
+ ts = datetime.fromisoformat(ts_str)
147
+ if ts.tzinfo is None:
148
+ ts = ts.replace(tzinfo=timezone.utc)
149
+ except Exception:
150
+ continue
151
+ total += 1
152
+ hour_key = ts.strftime('%Y-%m-%dT%H')
153
+ day_key = ts.strftime('%Y-%m-%d')
154
+ by_hour[hour_key] = by_hour.get(hour_key, 0) + 1
155
+ by_day[day_key] = by_day.get(day_key, 0) + 1
156
+ if ts >= cutoff_5m:
157
+ try:
158
+ latencies_5m.append(float(req_time) * 1000)
159
+ except ValueError:
160
+ pass
161
+
162
+ hours_24 = sorted(k for k in by_hour if k >= cutoff_24h.strftime('%Y-%m-%dT%H'))
163
+ days_7 = sorted(k for k in by_day if k >= cutoff_7d.strftime('%Y-%m-%d'))
164
+
165
+ req_24h = sum(by_hour[h] for h in hours_24)
166
+ req_7d = sum(by_day[d] for d in days_7)
167
+
168
+ avg_latency_ms = sum(latencies_5m) / len(latencies_5m) if latencies_5m else 0
169
+ p50_latency_ms = sorted(latencies_5m)[len(latencies_5m)//2] if latencies_5m else 0
170
+ p90_latency_ms = sorted(latencies_5m)[int(len(latencies_5m)*0.9)] if latencies_5m else 0
171
+
172
+ analytics = {
173
+ "updated_at": now.isoformat(),
174
+ "summary_24h": {"requests": req_24h, "unique_users": 0},
175
+ "summary_7d": {"requests": req_7d, "unique_users": 0},
176
+ "summary_total": {"requests": total, "unique_users": 0},
177
+ "requests_by_hour": [{"hour": h + ":00", "requests": by_hour[h]} for h in hours_24],
178
+ "requests_by_day": [{"day": d, "requests": by_day[d]} for d in days_7],
179
+ "top_users": [],
180
+ "latency_5m": {
181
+ "avg_ms": round(avg_latency_ms),
182
+ "p50_ms": round(p50_latency_ms),
183
+ "p90_ms": round(p90_latency_ms),
184
+ "sample_count": len(latencies_5m),
185
+ },
186
+ }
187
+ ANALYTICS_FILE.write_text(json.dumps(analytics))
188
+ except Exception as e:
189
+ print(f"[analytics] error: {e}")
190
+
191
+ def hf_push(local_path):
192
+ if not METRICS_REPO or not HF_TOKEN: return
193
+ dest = f"metrics/{local_path.name}"
194
+ content = base64.b64encode(local_path.read_bytes()).decode()
195
+ payload = json.dumps({"commit_message": f"update {local_path.name}",
196
+ "files": [{"path": dest, "encoding": "base64", "content": content}]}).encode()
197
+ req = urllib.request.Request(
198
+ f"https://huggingface.co/api/datasets/{METRICS_REPO}/commit/main",
199
+ data=payload, method="POST",
200
+ headers={"Authorization": f"Bearer {HF_TOKEN}", "Content-Type": "application/json"})
201
+ try:
202
+ with urllib.request.urlopen(req, timeout=30) as r:
203
+ print(f"[metrics] pushed {local_path.name} β†’ {METRICS_REPO}/{dest} ({r.status})")
204
+ except Exception as e:
205
+ print(f"[metrics] push failed: {e}")
206
+
207
+ def gpu_loop():
208
+ """Fast loop: update gpu-stats.json every GPU_INTERVAL_SECS seconds."""
209
+ print(f"[gpu] polling every {GPU_INTERVAL_SECS}s")
210
+ while True:
211
+ gpus = scrape_gpus()
212
+ if gpus:
213
+ ts = now_utc().isoformat()
214
+ GPU_STATS_FILE.write_text(json.dumps({"ts": ts, "gpus": gpus}))
215
+ time.sleep(GPU_INTERVAL_SECS)
216
+
217
+ def wait_for_backends():
218
+ """Block until at least one llama-server is healthy (retries every 5s)."""
219
+ print("[metrics] waiting for llama-server to be ready...")
220
+ while True:
221
+ for i in range(16):
222
+ url = f"http://127.0.0.1:{BASE_PORT + i}"
223
+ try:
224
+ with urllib.request.urlopen(f"{url}/health", timeout=2) as r:
225
+ if r.status == 200:
226
+ print(f"[metrics] backend ready: {url}")
227
+ return
228
+ except Exception:
229
+ pass
230
+ time.sleep(5)
231
+
232
+ def metrics_loop():
233
+ """Slow loop: scrape llama metrics, append JSONL, push to HF every SNAPSHOT_SECS seconds."""
234
+ print(f"[metrics] snapshot={SNAPSHOT_SECS}s push={PUSH_SECS}s repo={METRICS_REPO or '(local only)'}")
235
+ wait_for_backends()
236
+ last_push, backends, first_push_done = 0.0, [], False
237
+ while True:
238
+ if not backends:
239
+ backends = detect_backends()
240
+ print(f"[metrics] backends: {backends}")
241
+ ts = now_utc()
242
+ data = scrape(backends)
243
+ compute_analytics()
244
+ if data is None:
245
+ print(f"[metrics] scrape returned no data β€” will retry next tick")
246
+ backends = [] # force re-detect next iteration
247
+ else:
248
+ gpus = json.loads(GPU_STATS_FILE.read_text()).get("gpus", []) if GPU_STATS_FILE.exists() else []
249
+ gpu_s = {f"gpu{g['index']}_util": g["util_gpu"] for g in gpus}
250
+ gpu_s.update({f"gpu{g['index']}_mem_used_mib": g["mem_used_mib"] for g in gpus})
251
+ row = {"ts": ts.isoformat(), **data, **gpu_s}
252
+ day = ts.strftime("%Y-%m-%d")
253
+ path = LOG_DIR / f"metrics-{day}.jsonl"
254
+ with open(path, "a") as f: f.write(json.dumps(row) + "\n")
255
+ gpu_str = " ".join(f"GPU{g['index']} {g['util_gpu']:.0f}% {g['mem_used_mib']/1024:.1f}GB {g['temp_c']:.0f}Β°C" for g in gpus)
256
+ print(f"[metrics] {ts.strftime('%H:%M:%S')} gen={data.get('predicted_tokens_seconds',0):.0f} tok/s active={data.get('requests_processing',0):.0f} {gpu_str}")
257
+ if not first_push_done or time.time() - last_push >= PUSH_SECS:
258
+ hf_push(path)
259
+ last_push = time.time()
260
+ first_push_done = True
261
+ time.sleep(SNAPSHOT_SECS)
262
+
263
+ def main():
264
+ t = threading.Thread(target=gpu_loop, daemon=True)
265
+ t.start()
266
+ metrics_loop()
267
+
268
+ if __name__ == "__main__":
269
+ main()
nginx.conf ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ daemon off;
2
+ pid /tmp/nginx.pid;
3
+ worker_processes 1;
4
+
5
+ events {
6
+ worker_connections 1024;
7
+ }
8
+
9
+ http {
10
+ include /etc/nginx/mime.types;
11
+ default_type application/octet-stream;
12
+
13
+ client_body_temp_path /tmp/nginx-client-body;
14
+ proxy_temp_path /tmp/nginx-proxy;
15
+ fastcgi_temp_path /tmp/nginx-fastcgi;
16
+ uwsgi_temp_path /tmp/nginx-uwsgi;
17
+ scgi_temp_path /tmp/nginx-scgi;
18
+
19
+ # Generated at startup by entrypoint.sh
20
+ include /tmp/nginx-upstream.conf;
21
+
22
+ # No usernames, no chat content β€” only method, path, status, size, timing
23
+ log_format bonsai '$time_iso8601|$request_method|$uri|$status|$body_bytes_sent|$request_time';
24
+ access_log /tmp/nginx-access.log bonsai;
25
+ error_log /tmp/nginx-error.log warn;
26
+
27
+ server {
28
+ listen 7860;
29
+
30
+ # ── Private: dashboard UI ────────────────────────────────
31
+ location = /dash-2e215f981f3f {
32
+ auth_basic "Bonsai Internal";
33
+ auth_basic_user_file /tmp/.htpasswd;
34
+ alias /app/dashboard.html;
35
+ default_type text/html;
36
+ add_header Cache-Control "no-store";
37
+ }
38
+
39
+ # ── Private: raw Prometheus metrics (served from file written by metrics_pusher) ──
40
+ location = /metrics-2e215f981f3f {
41
+ auth_basic "Bonsai Internal";
42
+ auth_basic_user_file /tmp/.htpasswd;
43
+ alias /tmp/llama-metrics.txt;
44
+ add_header Content-Type "text/plain; charset=utf-8";
45
+ add_header Cache-Control "no-store";
46
+ }
47
+
48
+ # ── Private: live GPU stats ───────────────────────────────
49
+ location = /gpu-2e215f981f3f {
50
+ auth_basic "Bonsai Internal";
51
+ auth_basic_user_file /tmp/.htpasswd;
52
+ alias /tmp/gpu-stats.json;
53
+ add_header Content-Type "application/json";
54
+ add_header Cache-Control "no-store";
55
+ }
56
+
57
+ # ── Private: pre-computed analytics JSON ─────────────────
58
+ location = /analytics-2e215f981f3f {
59
+ auth_basic "Bonsai Internal";
60
+ auth_basic_user_file /tmp/.htpasswd;
61
+ alias /tmp/analytics.json;
62
+ add_header Content-Type "application/json";
63
+ add_header Cache-Control "no-store";
64
+ }
65
+
66
+ # ── Public: load-balanced across all GPU backends ────────
67
+ location / {
68
+ proxy_pass http://llama_backends;
69
+ proxy_http_version 1.1;
70
+ proxy_set_header Host $host;
71
+ proxy_set_header X-Real-IP $remote_addr;
72
+ proxy_set_header Connection "";
73
+ # Strip identity headers before they reach llama-server
74
+ proxy_set_header X-HF-User "";
75
+ proxy_set_header Authorization "";
76
+
77
+ # SSE / streaming completions
78
+ proxy_buffering off;
79
+ proxy_cache off;
80
+ proxy_read_timeout 600s;
81
+ proxy_send_timeout 600s;
82
+ client_max_body_size 10m;
83
+ chunked_transfer_encoding on;
84
+ }
85
+ }
86
+ }
style.css DELETED
@@ -1,28 +0,0 @@
1
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
4
- }
5
-
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
9
- }
10
-
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
- }
17
-
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
- }
25
-
26
- .card p:last-child {
27
- margin-bottom: 0;
28
- }