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>