File size: 15,725 Bytes
30bafb7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 | <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DaisyChain-Infer β run one model across your devices</title>
<style>
/* Winter palette: pale snow-light, deep glacier dark. Blues and slates
throughout, with a cold cyan accent that stays legible on both. */
:root {
--page-bg: #eef3f8;
--page-tint: #dce6f0;
--card-bg: #f9fbfd;
--card-border: rgba(90, 122, 154, 0.28);
--text: #16232e;
--text-soft: #55708a;
--accent: #2f6f96;
--accent-deep: #1d4b6b;
--panel-bg: linear-gradient(135deg, #24455e 0%, #14283a 100%);
--panel-num: #eaf4fb;
--panel-label: #8fb4cd;
--btn: linear-gradient(135deg, #35789e 0%, #1d4b6b 100%);
--btn-hover: linear-gradient(135deg, #4189af 0%, #255a7f 100%);
--danger: #9a3d3d;
--warn: #a2472f;
--track: rgba(90, 122, 154, 0.20);
--shadow: 0 2px 12px rgba(30, 60, 90, 0.10);
}
@media (prefers-color-scheme: dark) {
:root {
--page-bg: #0c141d;
--page-tint: #101c27;
--card-bg: #14212d;
--card-border: rgba(140, 180, 210, 0.22);
--text: #dce9f3;
--text-soft: #8faec7;
--accent: #6fb5da;
--accent-deep: #9fd0e8;
--panel-bg: linear-gradient(135deg, #16303f 0%, #0a1620 100%);
--panel-num: #e6f3fb;
--panel-label: #7ea9c4;
--btn: linear-gradient(135deg, #2c6688 0%, #17394f 100%);
--btn-hover: linear-gradient(135deg, #37789d 0%, #1e4a66 100%);
--danger: #b3554f;
--warn: #e0a07f;
--track: rgba(140, 180, 210, 0.18);
--shadow: 0 2px 14px rgba(0, 0, 0, 0.35);
}
}
* { box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 760px; margin: 0 auto; padding: 24px 18px 48px; line-height: 1.5;
background: linear-gradient(180deg, var(--page-tint) 0%, var(--page-bg) 340px);
color: var(--text); }
h1 { margin: 0 0 2px; font-size: 1.7rem; letter-spacing: .2px; }
.sub { color: var(--text-soft); margin: 0 0 18px; font-size: .92rem; }
.sub code, code { background: rgba(47, 111, 150, .12); padding: 1px 5px; border-radius: 4px;
font-family: 'Cascadia Code', 'Courier New', monospace; font-size: .9em; }
.card { background: var(--card-bg); border: 1px solid var(--card-border); border-radius: 12px;
padding: 14px 16px; margin: 12px 0; box-shadow: var(--shadow); }
.lbl { color: var(--text-soft); font-size: 11px; font-weight: 800; letter-spacing: 1.5px;
text-transform: uppercase; margin-bottom: 8px; }
.row { display: flex; justify-content: space-between; gap: 12px; padding: 3px 0; font-size: .95rem; }
.k { color: var(--text-soft); } .v { font-weight: 700; text-align: right; word-break: break-word; }
.device { font-family: 'Cascadia Code', 'Courier New', monospace; font-size: 1.5rem; font-weight: 700;
color: var(--accent-deep); }
.panel { background: var(--panel-bg); border-radius: 10px; padding: 12px 16px; text-align: center; flex: 1; }
.panel .num { font-family: 'Cascadia Code', 'Courier New', monospace; font-size: 1.7rem;
font-weight: 700; color: var(--panel-num); }
.panel .cl { color: var(--panel-label); font-size: 10px; text-transform: uppercase; letter-spacing: 1.2px; }
button { background: var(--btn); color: #eef6fb; border: 0; border-radius: 8px; padding: 11px 24px;
font-weight: 700; font-size: .98rem; letter-spacing: .3px; cursor: pointer;
box-shadow: 0 2px 8px rgba(20, 45, 70, 0.22); transition: .15s; }
button:hover:not(:disabled) { background: var(--btn-hover); }
button:disabled { background: #7d8f9e; opacity: .5; cursor: not-allowed; box-shadow: none; }
button.small { padding: 6px 14px; font-size: .85rem; }
button.danger { background: var(--danger); }
pre { background: rgba(20, 45, 70, 0.05); border-radius: 8px; padding: 10px; max-height: 180px; overflow: auto;
font-size: .78rem; color: var(--text-soft); white-space: pre-wrap; margin: 0;
font-family: 'Cascadia Code', 'Courier New', monospace; }
@media (prefers-color-scheme: dark) { pre { background: rgba(0, 0, 0, 0.28); } }
.note { color: var(--text-soft); font-size: .82rem; }
.note b { color: var(--warn); }
.slbl { display: flex; justify-content: space-between; font-size: .92rem; margin: 10px 0 4px; font-weight: 600; }
.sval { font-family: 'Cascadia Code', 'Courier New', monospace; color: var(--accent); font-weight: 700; }
input[type=range] { width: 100%; accent-color: var(--accent); }
input[type=text], input[type=number], input[type=password] {
padding: 10px 12px; border-radius: 8px; border: 1px solid var(--card-border);
background: rgba(255,255,255,.55); color: inherit;
font-family: 'Cascadia Code', 'Courier New', monospace; font-size: .92rem; }
@media (prefers-color-scheme: dark) {
input[type=text], input[type=number], input[type=password] { background: rgba(0,0,0,.25); }
}
input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
#out { background: rgba(20, 45, 70, 0.05); border-radius: 8px; padding: 12px; min-height: 96px;
max-height: 340px; overflow: auto; white-space: pre-wrap; word-break: break-word;
font-family: 'Cascadia Code', 'Courier New', monospace; font-size: .9rem; color: var(--text); }
@media (prefers-color-scheme: dark) { #out { background: rgba(0, 0, 0, 0.28); } }
.joinrow { display: flex; gap: 8px; align-items: center; justify-content: space-between;
margin-top: 10px; flex-wrap: wrap; }
.modal { position: fixed; inset: 0; background: rgba(8, 18, 28, .62); backdrop-filter: blur(3px);
display: none; align-items: center; justify-content: center; padding: 20px; z-index: 50; }
.modal .box { background: var(--card-bg); border: 1px solid var(--card-border); border-radius: 12px;
padding: 20px; max-width: 460px; width: 100%; box-shadow: 0 10px 40px rgba(0,0,0,.35); }
.modal h3 { margin: 0 0 8px; font-size: 1.1rem; }
.modal input { width: 100%; margin: 12px 0 6px; }
.modal .acts { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; }
</style>
</head>
<body>
<h1>β DaisyChain-Infer</h1>
<p class="sub">Run <b>one model across several devices</b>. Each device downloads and holds only its <b>own layers</b> β straight from Hugging Face, never from a peer β and passes the hidden state around the ring over WebRTC. The group can run a model that no single device could hold. Every multiply goes through the verified INT8 units. Runs locally: <code>npm start</code>.</p>
<!-- Shown only when the deployment sets DAISY_FORCE_ROOMS (the hosted Space).
Without it, visitors with no room code are grouped by public IP β which
on a public URL would put strangers behind the same CGNAT in one ring. -->
<div class="card" id="lobby" style="display:none;text-align:center">
<div class="lbl">β Get started</div>
<p class="note" style="margin:0 0 12px">Create a room and invite your own devices, or join a room someone shared with you. Rings are private by invitation β you approve every device that joins.</p>
<button id="createRoom">Create a room</button>
<div style="display:flex;gap:8px;justify-content:center;margin-top:12px;flex-wrap:wrap">
<input type="text" id="joinCode" placeholder="room code" spellcheck="false">
<button id="joinRoom" style="padding:10px 18px">Join</button>
</div>
</div>
<div class="card">
<div class="lbl">β This device</div>
<div class="device" id="me">β</div>
<div class="row" style="margin-top:8px"><span class="k">Status</span><span class="v" id="status">startingβ¦</span></div>
<div class="row"><span class="k">Compute</span><span class="v" id="backend">detectingβ¦</span></div>
<div class="row"><span class="k">My slice</span><span class="v" id="myShard">none yet</span></div>
<div class="row"><span class="k">HF token</span><span class="v" id="tokenState">none</span></div>
<div style="text-align:right"><button id="clearToken" class="small danger" style="display:none">Forget token</button></div>
</div>
<div class="card">
<div class="lbl">π Devices in your group</div>
<div class="row"><span class="v" id="peers" style="text-align:left">(none yet)</span></div>
<div id="roomInfo" style="display:none;margin-top:8px">
<div class="row"><span class="k">Room</span><span class="v" id="roomCode"></span></div>
<div style="text-align:center;margin-top:6px"><button id="copyLink" class="small">Copy invite link</button></div>
<p class="note" style="margin:.5rem 0 0;text-align:center">Open the link on your other devices β you approve each one before it joins.</p>
</div>
<div id="requests"></div>
</div>
<div class="card">
<div class="lbl">π» Model</div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<input type="text" id="repo" value="HuggingFaceTB/SmolLM-135M" spellcheck="false"
placeholder="owner/name" style="flex:2;min-width:200px">
<input type="text" id="revision" value="main" spellcheck="false" placeholder="revision" style="flex:0 0 90px">
<button id="loadRepo">Load</button>
</div>
<div class="row" style="margin-top:10px"><span class="v" id="model" style="text-align:left">no model loaded</span></div>
<div class="row"><span class="k">Status</span><span class="v" id="modelState">β</span></div>
<div class="row"><span class="k">Tokenizer</span><span class="v" id="tokenizer">β</span></div>
<p class="note" style="margin:.6rem 0 0">Any Hugging Face repo with <code>.safetensors</code> weights whose architecture is <b>Llama-style</b> (Llama, Mistral, Qwen2, SmolLM, TinyLlama) or <b>GPT-2-style</b>. Loading reads only <code>config.json</code>, the tokenizer, and the weight <i>headers</i> β a few tens of KB, whatever the model's size. Gated or private repos will ask for a token, once.</p>
</div>
<div class="card">
<div class="lbl">β Ring plan</div>
<pre id="plan" style="max-height:140px">load a model to see how it would be split</pre>
<p class="note" style="margin:.5rem 0 0">Layers are apportioned by <b>measured</b> speed β each device times a real GEMM through its own kernel. The MB figure is what that device will actually download and hold.</p>
</div>
<div class="card">
<div class="lbl">π Generation</div>
<label class="slbl">Tokens <span class="sval" id="vntok">60</span></label>
<input type="range" id="ntok" min="10" max="400" step="10" value="60">
<label class="slbl">Context length <span class="sval" id="vctxLen">64</span></label>
<input type="range" id="ctxLen" min="16" max="512" step="16" value="64">
<p class="note" style="margin:.3rem 0 .6rem">There is no KV cache β every token re-runs the whole window, so context length is the main cost per token.</p>
<label class="slbl">Temperature Γ·100 <span class="sval" id="vtemp">0</span></label>
<input type="range" id="temp" min="0" max="150" step="5" value="0">
<p class="note" style="margin:.3rem 0 .6rem">0 = greedy, which is deterministic and therefore checkable. Above 0, sampling follows the seed so runs stay reproducible.</p>
<label class="slbl">Seed</label>
<input type="number" id="seed" value="1234" style="width:140px">
<div class="row" id="verifyRow" style="display:none;margin-top:10px">
<label class="note" style="display:flex;gap:8px;align-items:center;cursor:pointer">
<input type="checkbox" id="verify"> Verify afterwards against a single-device run
<span>(downloads the whole model here β only sensible for small ones)</span>
</label>
</div>
</div>
<div class="card">
<div class="lbl">β Prompt</div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<input type="text" id="prompt" value="The quick brown fox" spellcheck="false" style="flex:1;min-width:180px">
<button id="genBtn" disabled>Generate</button>
<button id="stopBtn" disabled class="danger">Stop</button>
</div>
<div id="out" style="margin-top:10px"></div>
<div style="display:flex;gap:8px;margin-top:10px">
<div class="panel"><div class="num" id="tps">β</div><div class="cl">tokens / sec</div></div>
<div class="panel"><div class="num" id="tokcount">0</div><div class="cl">tokens</div></div>
<div class="panel"><div class="num" id="hops">0</div><div class="cl">ring hops</div></div>
</div>
<div class="row" style="margin-top:8px"><span class="k">Ring</span><span class="v" id="ringState">idle</span></div>
</div>
<div class="card">
<div class="lbl">β Log</div>
<pre id="log"></pre>
</div>
<p class="note">Needs a secure context (localhost or HTTPS) for WebGPU and cross-device WebRTC. No WebGPU? The identical verified INT8 units run on CPU β slower, but a CPU device and a GPU device produce the same bits, so they can share a ring. There is no plain-float path.</p>
<p class="note"><b>Heads up:</b> peers connect directly, so devices in your ring can see each other's IP address. A middle stage never receives the embedding table or the vocabulary, but it does see the hidden states passing through β and the head sees your prompt and output. Run rings with devices and people you trust. Proof of concept, not a hardened service.</p>
<!-- One-time credential prompt, shown only when a request actually fails for
want of one. Never stored: not localStorage, not sessionStorage, not a
cookie, and never sent to a peer β each device authenticates itself.
Reloading the tab forgets it.
On a hosted deployment the paste box is hidden entirely and only
"Sign in with Hugging Face" is offered: typing a personal access token
into a page you do not control is a bad habit even when the code is
honest, because you cannot verify that it is. -->
<div class="modal" id="tokModal">
<div class="box">
<h3>β Hugging Face access</h3>
<p class="note" id="tokWhy">This model needs a token to download.</p>
<div id="tokSignIn" style="display:none;margin:14px 0 6px">
<button id="tokSignInBtn" style="width:100%">Sign in with Hugging Face</button>
<p class="note" style="margin:.6rem 0 0">You are redirected to huggingface.co and back. This page receives a <b>scoped, expiring</b> token β read access to repositories, nothing else β held in memory for this tab only. No token is ever typed into this page, and none is sent to other devices.</p>
</div>
<div id="tokPasteRow">
<input type="password" id="tokInput" placeholder="hf_β¦" spellcheck="false" autocomplete="off">
<p class="note" style="margin:0">Kept <b>in memory for this tab only</b>: never written to disk or storage, never put in a URL, never logged, and never sent to other devices β each device is asked for its own. Create a <b>read</b> token at huggingface.co/settings/tokens.</p>
</div>
<div class="acts">
<button id="tokCancel" class="small danger">Cancel</button>
<button id="tokOk" class="small">Use token</button>
</div>
</div>
</div>
<script src="traincore.js"></script>
<script src="verified_core.js"></script>
<script src="safetensors.js"></script>
<script src="hf.js"></script>
<script src="arch.js"></script>
<script src="tokenizer.js"></script>
<script src="shard.js"></script>
<script src="wire.js"></script>
<script src="infer.js"></script>
<script src="webgpu.js"></script>
<script src="app.js"></script>
</body>
</html>
|