Spaces:
Paused
Paused
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 +2 -0
- Dockerfile +26 -0
- README.md +21 -5
- bin/libggml-base.so +1 -0
- bin/libggml-base.so.0 +1 -0
- bin/libggml-base.so.0.9.7 +3 -0
- bin/libggml-cpu.so +1 -0
- bin/libggml-cpu.so.0 +1 -0
- bin/libggml-cpu.so.0.9.7 +3 -0
- bin/libggml-cuda.so +1 -0
- bin/libggml-cuda.so.0 +1 -0
- bin/libggml-cuda.so.0.9.7 +3 -0
- bin/libggml.so +1 -0
- bin/libggml.so.0 +1 -0
- bin/libggml.so.0.9.7 +3 -0
- bin/libllama.so +1 -0
- bin/libllama.so.0 +1 -0
- bin/libllama.so.0.0.8190 +3 -0
- bin/libmtmd.so +1 -0
- bin/libmtmd.so.0 +1 -0
- bin/libmtmd.so.0.0.8190 +3 -0
- bin/llama-bench +3 -0
- bin/llama-cli +3 -0
- bin/llama-completion +3 -0
- bin/llama-perplexity +3 -0
- bin/llama-quantize +3 -0
- bin/llama-server +3 -0
- dashboard.html +415 -0
- entrypoint.sh +112 -0
- index.html +0 -19
- metrics_pusher.py +269 -0
- nginx.conf +86 -0
- style.css +0 -28
.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:
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: green
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk:
|
|
|
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 & 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} Β· </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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|