EXDai commited on
Commit
9b68f36
·
verified ·
1 Parent(s): 60ceb38

Upload index.html with huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +1175 -18
index.html CHANGED
@@ -1,19 +1,1176 @@
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>
 
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>LLM Inference — Pipeline Simulator</title>
7
+ <style>
8
+ :root {
9
+ --bg: #0d1117;
10
+ --surface: #161b22;
11
+ --border: #30363d;
12
+ --text: #c9d1d9;
13
+ --dim: #8b949e;
14
+ --accent: #58a6ff;
15
+ --cpu: #f0883e;
16
+ --ram: #3fb950;
17
+ --gpu: #bc8cff;
18
+ --vram: #f778ba;
19
+ --prefill: #da3633;
20
+ --decode: #d29922;
21
+ --active: #58a6ff;
22
+ }
23
+ * { margin: 0; padding: 0; box-sizing: border-box; }
24
+ body {
25
+ background: var(--bg);
26
+ color: var(--text);
27
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', monospace;
28
+ display: flex; justify-content: center; align-items: center;
29
+ min-height: 100vh; padding: 2rem;
30
+ }
31
+ .sim {
32
+ max-width: 820px; width: 100%;
33
+ background: var(--surface);
34
+ border: 1px solid var(--border);
35
+ border-radius: 12px; padding: 2rem;
36
+ }
37
+ h1 { font-size: 1.25rem; font-weight: 600; margin-bottom: 0.25rem; color: var(--text); }
38
+ .subtitle { font-size: 0.8rem; color: var(--dim); margin-bottom: 1.5rem; }
39
+
40
+ /* Tabs */
41
+ .tabs {
42
+ display: flex; gap: 0; margin-bottom: 1.5rem;
43
+ border-bottom: 2px solid var(--border);
44
+ }
45
+ .tab-btn {
46
+ padding: 0.5rem 1.25rem; border: none; border-bottom: 2px solid transparent;
47
+ background: transparent; color: var(--dim); font-size: 0.85rem;
48
+ cursor: pointer; font-family: inherit; margin-bottom: -2px;
49
+ transition: all 0.2s;
50
+ }
51
+ .tab-btn:hover { color: var(--text); }
52
+ .tab-btn.active {
53
+ color: var(--accent); border-bottom-color: var(--accent);
54
+ background: transparent;
55
+ }
56
+ .tab-panel { display: none; }
57
+ .tab-panel.visible { display: block; }
58
+
59
+ /* ---- Concurrency Tab Styles ---- */
60
+ .conc-header {
61
+ display: flex; justify-content: space-between; align-items: flex-start;
62
+ margin-bottom: 1rem; gap: 1rem;
63
+ }
64
+ .conc-meters {
65
+ display: flex; gap: 1.5rem; flex-shrink: 0;
66
+ }
67
+ .conc-meter {
68
+ text-align: center;
69
+ }
70
+ .conc-meter-label {
71
+ font-size: 0.6rem; text-transform: uppercase;
72
+ letter-spacing: 0.05em; color: var(--dim); margin-bottom: 0.2rem;
73
+ }
74
+ .conc-meter-bar {
75
+ width: 60px; height: 8px; background: var(--bg);
76
+ border: 1px solid var(--border); border-radius: 4px;
77
+ overflow: hidden; margin-bottom: 0.15rem;
78
+ }
79
+ .conc-meter-fill {
80
+ height: 100%; border-radius: 3px; transition: width 0.4s ease;
81
+ }
82
+ .conc-meter-fill.gpu-util { background: var(--gpu); }
83
+ .conc-meter-fill.vram-usage { background: var(--vram); }
84
+ .conc-meter-val {
85
+ font-size: 0.7rem; font-weight: 600; font-family: monospace;
86
+ }
87
+ .conc-legend {
88
+ display: flex; gap: 1rem; flex-wrap: wrap; font-size: 0.65rem;
89
+ color: var(--dim);
90
+ }
91
+ .conc-legend span { display: flex; align-items: center; gap: 4px; }
92
+ .conc-legend-dot {
93
+ width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0;
94
+ }
95
+ .conc-legend-dot.prefill-dot { background: var(--prefill); }
96
+ .conc-legend-dot.decode-dot { background: var(--decode); }
97
+ .conc-legend-dot.idle-dot { background: var(--border); }
98
+ .conc-legend-dot.wait-dot { background: var(--dim); }
99
+
100
+ /* Swimlane */
101
+ .swimlane {
102
+ background: var(--bg); border: 1px solid var(--border);
103
+ border-radius: 8px; overflow: hidden; margin-bottom: 1rem;
104
+ }
105
+ .swimlane-row {
106
+ display: flex; align-items: center;
107
+ border-bottom: 1px solid var(--border);
108
+ min-height: 36px;
109
+ }
110
+ .swimlane-row:last-child { border-bottom: none; }
111
+ .swimlane-label {
112
+ width: 100px; flex-shrink: 0; padding: 0.5rem 0.75rem;
113
+ font-size: 0.7rem; font-weight: 600;
114
+ border-right: 1px solid var(--border);
115
+ background: var(--surface);
116
+ }
117
+ .swimlane-label .user-icon { margin-right: 4px; }
118
+ .swimlane-track {
119
+ flex: 1; display: flex; position: relative;
120
+ height: 100%; min-height: 36px;
121
+ }
122
+ .swimlane-block {
123
+ position: absolute; top: 3px; bottom: 3px;
124
+ border-radius: 4px; display: flex; align-items: center;
125
+ justify-content: center; font-size: 0.6rem; font-weight: 600;
126
+ transition: all 0.3s ease; overflow: hidden;
127
+ white-space: nowrap; text-overflow: ellipsis;
128
+ }
129
+ .swimlane-block.prefill-block {
130
+ background: rgba(218,54,51,0.25);
131
+ border: 1px solid var(--prefill);
132
+ color: #f09090;
133
+ }
134
+ .swimlane-block.decode-block {
135
+ background: rgba(210,153,34,0.2);
136
+ border: 1px solid var(--decode);
137
+ color: #e0c060;
138
+ }
139
+ .swimlane-block.wait-block {
140
+ background: rgba(139,148,158,0.1);
141
+ border: 1px dashed var(--dim);
142
+ color: var(--dim);
143
+ }
144
+ .swimlane-block.input-block {
145
+ background: rgba(240,136,62,0.15);
146
+ border: 1px solid var(--cpu);
147
+ color: var(--cpu);
148
+ }
149
+
150
+ /* GPU queue */
151
+ .gpu-queue {
152
+ background: var(--bg); border: 1px solid var(--border);
153
+ border-radius: 8px; padding: 0.75rem 1rem; margin-bottom: 1rem;
154
+ display: flex; align-items: center; gap: 0.75rem;
155
+ }
156
+ .gpu-queue-label {
157
+ font-size: 0.65rem; text-transform: uppercase;
158
+ letter-spacing: 0.05em; color: var(--dim); white-space: nowrap;
159
+ }
160
+ .gpu-queue-slots {
161
+ display: flex; gap: 6px; flex-wrap: wrap;
162
+ }
163
+ .gpu-slot {
164
+ padding: 3px 10px; border-radius: 4px;
165
+ font-size: 0.65rem; border: 1px solid var(--border);
166
+ background: var(--surface);
167
+ }
168
+ .gpu-slot.busy-prefill {
169
+ border-color: var(--prefill); background: rgba(218,54,51,0.15);
170
+ color: var(--prefill);
171
+ }
172
+ .gpu-slot.busy-decode {
173
+ border-color: var(--decode); background: rgba(210,153,34,0.15);
174
+ color: var(--decode);
175
+ }
176
+
177
+ .conc-status {
178
+ font-size: 0.75rem; color: var(--dim); margin-bottom: 1rem;
179
+ text-align: center; min-height: 1.2em; line-height: 1.5;
180
+ }
181
+ .conc-status .highlight { color: var(--active); }
182
+
183
+ /* Pipeline */
184
+ .pipeline {
185
+ display: flex; align-items: center; justify-content: center;
186
+ gap: 0; margin-bottom: 1.5rem; flex-wrap: wrap;
187
+ }
188
+ .stage {
189
+ padding: 0.6rem 1rem; border-radius: 8px;
190
+ border: 2px solid var(--border); background: var(--bg);
191
+ font-size: 0.85rem; font-weight: 600;
192
+ text-align: center; min-width: 80px;
193
+ transition: all 0.3s ease;
194
+ position: relative; z-index: 2;
195
+ }
196
+ .stage.active {
197
+ border-color: var(--active);
198
+ background: #1a2332;
199
+ box-shadow: 0 0 16px rgba(88,166,255,0.25);
200
+ color: var(--active);
201
+ }
202
+ .arrow {
203
+ width: 32px; height: 2px; background: var(--border);
204
+ margin: 0 4px; flex-shrink: 0; position: relative; z-index: 1;
205
+ }
206
+ .arrow::after {
207
+ content: '▸'; position: absolute; right: -6px; top: -8px;
208
+ font-size: 0.7rem; color: var(--border);
209
+ }
210
+
211
+ /* Decode loop indicator */
212
+ .decode-loop {
213
+ display: flex; align-items: center; gap: 6px;
214
+ margin-top: 0.3rem; justify-content: center;
215
+ font-size: 0.7rem; color: var(--dim);
216
+ }
217
+ .loop-arrow {
218
+ width: 20px; height: 2px; background: var(--dim);
219
+ }
220
+ .loop-count {
221
+ padding: 2px 8px; border: 1px solid var(--border);
222
+ border-radius: 4px; font-size: 0.7rem;
223
+ }
224
+ .loop-count.active {
225
+ border-color: var(--decode); color: var(--decode);
226
+ background: rgba(210,153,34,0.1);
227
+ }
228
+
229
+ /* Resources */
230
+ .resources {
231
+ display: grid; grid-template-columns: repeat(4, 1fr);
232
+ gap: 1rem; margin-bottom: 1.5rem;
233
+ }
234
+ .res {
235
+ border: 2px solid var(--border); border-radius: 8px;
236
+ padding: 0.6rem 0.4rem; background: var(--bg); text-align: center;
237
+ transition: all 0.3s ease;
238
+ }
239
+ .res-label {
240
+ font-size: 0.7rem; text-transform: uppercase;
241
+ letter-spacing: 0.05em; margin-bottom: 0.15rem;
242
+ }
243
+ .res-status { font-size: 0.65rem; color: var(--dim); }
244
+ .res.cpu .res-label { color: var(--cpu); }
245
+ .res.ram .res-label { color: var(--ram); }
246
+ .res.gpu .res-label { color: var(--gpu); }
247
+ .res.vram .res-label { color: var(--vram); }
248
+ .res.active.cpu { border-color: var(--cpu); box-shadow: 0 0 10px rgba(240,136,62,0.25); }
249
+ .res.active.ram { border-color: var(--ram); box-shadow: 0 0 10px rgba(63,185,80,0.25); }
250
+ .res.active.gpu { border-color: var(--gpu); box-shadow: 0 0 10px rgba(188,140,255,0.25); }
251
+ .res.active.vram { border-color: var(--vram); box-shadow: 0 0 10px rgba(247,120,186,0.25); }
252
+ .res.active .res-status { color: inherit; }
253
+
254
+ /* Token display */
255
+ .token-area {
256
+ background: var(--bg); border: 1px solid var(--border);
257
+ border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem;
258
+ min-height: 80px;
259
+ }
260
+ .token-label {
261
+ font-size: 0.65rem; text-transform: uppercase;
262
+ letter-spacing: 0.05em; color: var(--dim); margin-bottom: 0.5rem;
263
+ }
264
+ .token-row {
265
+ display: flex; gap: 6px; flex-wrap: wrap; align-items: center;
266
+ }
267
+ .token {
268
+ padding: 4px 10px; border-radius: 6px; font-size: 0.8rem;
269
+ border: 1px solid var(--border); background: var(--surface);
270
+ font-family: monospace; transition: all 0.3s ease;
271
+ }
272
+ .token.prefill { border-color: var(--prefill); background: rgba(218,54,51,0.1); }
273
+ .token.decode { border-color: var(--decode); background: rgba(210,153,34,0.1); }
274
+ .token.new { border-color: var(--active); background: rgba(88,166,255,0.1); animation: pulse 0.6s ease; }
275
+ .token.cached { border-color: var(--ram); opacity: 0.6; font-size: 0.7rem; }
276
+ @keyframes pulse {
277
+ 0%,100% { transform: scale(1); }
278
+ 50% { transform: scale(1.08); }
279
+ }
280
+
281
+ /* KV cache */
282
+ .kvcache {
283
+ background: var(--bg); border: 1px solid var(--border);
284
+ border-radius: 8px; padding: 0.75rem 1rem; margin-bottom: 1.5rem;
285
+ display: flex; align-items: center; gap: 0.75rem;
286
+ }
287
+ .kvcache-label {
288
+ font-size: 0.65rem; text-transform: uppercase;
289
+ letter-spacing: 0.05em; color: var(--dim); white-space: nowrap;
290
+ }
291
+ .kvcache-entries {
292
+ display: flex; gap: 4px; flex-wrap: wrap;
293
+ }
294
+ .kv-cell {
295
+ width: 14px; height: 14px; border-radius: 3px;
296
+ border: 1px solid var(--border); background: var(--surface);
297
+ transition: all 0.3s ease;
298
+ }
299
+ .kv-cell.filled { background: var(--vram); border-color: var(--vram); }
300
+
301
+ /* Status */
302
+ .status {
303
+ font-size: 0.8rem; color: var(--text); margin-bottom: 1rem;
304
+ text-align: center; min-height: 1.2em;
305
+ }
306
+ .status .highlight { color: var(--active); }
307
+
308
+ /* Controls */
309
+ .controls {
310
+ display: flex; gap: 0.75rem; justify-content: center;
311
+ }
312
+ button {
313
+ padding: 0.5rem 1.25rem; border-radius: 6px; border: 1px solid var(--border);
314
+ background: var(--bg); color: var(--text); font-size: 0.8rem;
315
+ cursor: pointer; font-family: inherit; transition: all 0.2s;
316
+ }
317
+ button:hover { border-color: var(--dim); background: #1c2128; }
318
+ button.primary {
319
+ border-color: var(--accent); color: var(--accent);
320
+ background: rgba(88,166,255,0.1);
321
+ }
322
+ button.primary:hover { background: rgba(88,166,255,0.2); }
323
+ button:disabled { opacity: 0.4; cursor: not-allowed; }
324
+
325
+ /* Step indicator dots */
326
+ .steps {
327
+ display: flex; gap: 6px; justify-content: center; margin-bottom: 1rem;
328
+ }
329
+ .step-dot {
330
+ width: 8px; height: 8px; border-radius: 50%;
331
+ background: var(--border); transition: background 0.3s;
332
+ }
333
+ .step-dot.done { background: var(--accent); }
334
+ .step-dot.current { background: var(--accent); box-shadow: 0 0 6px var(--accent); }
335
+ </style>
336
+ </head>
337
+ <body>
338
+ <div class="sim">
339
+ <h1>LLM Inference — Interactive Simulator</h1>
340
+
341
+ <!-- Tabs -->
342
+ <div class="tabs">
343
+ <button class="tab-btn active" onclick="switchTab('pipeline')">🔬 Pipeline View</button>
344
+ <button class="tab-btn" onclick="switchTab('concurrency')">👥 Multi-User Concurrency</button>
345
+ </div>
346
+
347
+ <!-- ===== TAB 1: Pipeline View ===== -->
348
+ <div class="tab-panel visible" id="tab-pipeline">
349
+ <p class="subtitle">Prompt: <strong>"The cat sat"</strong> — watch how it flows through the system</p>
350
+
351
+ <!-- Step dots -->
352
+ <div class="steps" id="stepDots">
353
+ <div class="step-dot"></div>
354
+ <div class="step-dot"></div>
355
+ <div class="step-dot"></div>
356
+ <div class="step-dot"></div>
357
+ <div class="step-dot"></div>
358
+ </div>
359
+
360
+ <!-- Pipeline stages -->
361
+ <div class="pipeline">
362
+ <div class="stage" id="s_input">📥 Input</div>
363
+ <div class="arrow"></div>
364
+ <div class="stage" id="s_tokenize">🔤 Tokenize</div>
365
+ <div class="arrow"></div>
366
+ <div class="stage" id="s_prefill">⚡ Prefill</div>
367
+ <div class="arrow"></div>
368
+ <div class="stage" id="s_decode">🔄 Decode</div>
369
+ <div class="arrow"></div>
370
+ <div class="stage" id="s_output">📤 Output</div>
371
+ </div>
372
+ <div class="decode-loop" id="loopInfo">
373
+ <span class="loop-arrow"></span>
374
+ <span>loop</span>
375
+ <span class="loop-count" id="loopCount">0 / 3</span>
376
+ </div>
377
+
378
+ <!-- Resources -->
379
+ <div class="resources">
380
+ <div class="res cpu" id="resCpu">
381
+ <div class="res-label">CPU</div>
382
+ <div class="res-status">idle</div>
383
+ </div>
384
+ <div class="res ram" id="resRam">
385
+ <div class="res-label">RAM</div>
386
+ <div class="res-status">idle</div>
387
+ </div>
388
+ <div class="res gpu" id="resGpu">
389
+ <div class="res-label">GPU Compute</div>
390
+ <div class="res-status">idle</div>
391
+ </div>
392
+ <div class="res vram" id="resVram">
393
+ <div class="res-label">VRAM / KV Cache</div>
394
+ <div class="res-status">idle</div>
395
+ </div>
396
+ </div>
397
+
398
+ <!-- Tokens -->
399
+ <div class="token-area">
400
+ <div class="token-label">Tokens</div>
401
+ <div class="token-row" id="tokenRow">
402
+ <span style="color:var(--dim); font-size:0.8rem;">Waiting for input...</span>
403
+ </div>
404
+ </div>
405
+
406
+ <!-- KV Cache -->
407
+ <div class="kvcache">
408
+ <span class="kvcache-label">KV Cache entries:</span>
409
+ <span class="kvcache-entries" id="kvEntries">
410
+ <span style="color:var(--dim); font-size:0.75rem;">empty</span>
411
+ </span>
412
+ </div>
413
+
414
+ <!-- Status -->
415
+ <div class="status" id="status">Click <span class="highlight">Auto</span> to run the full pipeline, or use <span class="highlight">Forward</span> / <span class="highlight">Back</span> to walk through one stage at a time.</div>
416
+
417
+ <!-- Controls -->
418
+ <div class="controls">
419
+ <button class="primary" id="btnAuto" onclick="toggleAuto()">Auto ▶</button>
420
+ <button id="btnBack" onclick="back()">◀ Back</button>
421
+ <button id="btnStep" onclick="step()">Forward ▶</button>
422
+ <button id="btnReset" onclick="reset()">↺ Reset</button>
423
+ </div>
424
+ </div><!-- end tab-pipeline -->
425
+
426
+ <!-- ===== TAB 2: Multi-User Concurrency ===== -->
427
+ <div class="tab-panel" id="tab-concurrency">
428
+ <p class="subtitle">4 concurrent users sharing one GPU — watch batching, queuing &amp; KV cache pressure</p>
429
+
430
+ <!-- Meters -->
431
+ <div class="conc-header">
432
+ <div class="conc-meters">
433
+ <div class="conc-meter">
434
+ <div class="conc-meter-label">GPU Util</div>
435
+ <div class="conc-meter-bar"><div class="conc-meter-fill gpu-util" id="gpuUtilBar" style="width:0%"></div></div>
436
+ <div class="conc-meter-val" id="gpuUtilVal">0%</div>
437
+ </div>
438
+ <div class="conc-meter">
439
+ <div class="conc-meter-label">VRAM / KV Cache</div>
440
+ <div class="conc-meter-bar"><div class="conc-meter-fill vram-usage" id="vramBar" style="width:0%"></div></div>
441
+ <div class="conc-meter-val" id="vramVal">0 GB</div>
442
+ </div>
443
+ </div>
444
+ <div class="conc-legend">
445
+ <span><span class="conc-legend-dot prefill-dot"></span> Prefill (compute)</span>
446
+ <span><span class="conc-legend-dot decode-dot"></span> Decode (memory)</span>
447
+ <span><span class="conc-legend-dot input-dot" style="background:var(--cpu)"></span> Tokenize</span>
448
+ <span><span class="conc-legend-dot wait-dot"></span> Queued</span>
449
+ </div>
450
+ </div>
451
+
452
+ <!-- Swimlane -->
453
+ <div class="swimlane" id="swimlane">
454
+ </div>
455
+
456
+ <!-- GPU Queue -->
457
+ <div class="gpu-queue">
458
+ <span class="gpu-queue-label">GPU Scheduler:</span>
459
+ <span class="gpu-queue-slots" id="gpuSlots">
460
+ <span class="gpu-slot" style="color:var(--dim)">idle</span>
461
+ </span>
462
+ </div>
463
+
464
+ <!-- Status -->
465
+ <div class="conc-status" id="concStatus">
466
+ Click <span class="highlight">Start</span> to watch 4 users stream through the pipeline concurrently.
467
+ </div>
468
+
469
+ <!-- Controls -->
470
+ <div class="controls">
471
+ <button class="primary" id="concBtnAuto" onclick="concToggleAuto()">▶ Start</button>
472
+ <button id="concBtnBack" onclick="concBack()">◀ Back</button>
473
+ <button id="concBtnStep" onclick="concStep()">Forward ▶</button>
474
+ <button id="concBtnReset" onclick="concReset()">↺ Reset</button>
475
+ </div>
476
+ </div><!-- end tab-concurrency -->
477
+
478
+ </div>
479
+
480
+ <script>
481
+ // --- State ---
482
+ const TOKENS = [
483
+ { id: 576, text: 'The' },
484
+ { id: 3797, text: ' cat' },
485
+ { id: 7236, text: ' sat' },
486
+ ];
487
+ const OUTPUT = [
488
+ { id: 389, text: ' on' },
489
+ { id: 278, text: ' the' },
490
+ { id: 3098, text: ' mat' },
491
+ ];
492
+
493
+ const TOTAL_STEPS = 7; // 0:idle, 1:input, 2:tokenize, 3:prefill, 4-6:decode steps
494
+ let currentStep = 0;
495
+ let autoTimer = null;
496
+ let decodeIdx = 0;
497
+
498
+ // --- DOM refs ---
499
+ const els = {
500
+ s_input: document.getElementById('s_input'),
501
+ s_tokenize: document.getElementById('s_tokenize'),
502
+ s_prefill: document.getElementById('s_prefill'),
503
+ s_decode: document.getElementById('s_decode'),
504
+ s_output: document.getElementById('s_output'),
505
+ resCpu: document.getElementById('resCpu'),
506
+ resRam: document.getElementById('resRam'),
507
+ resGpu: document.getElementById('resGpu'),
508
+ resVram: document.getElementById('resVram'),
509
+ tokenRow: document.getElementById('tokenRow'),
510
+ kvEntries: document.getElementById('kvEntries'),
511
+ status: document.getElementById('status'),
512
+ loopCount: document.getElementById('loopCount'),
513
+ loopInfo: document.getElementById('loopInfo'),
514
+ btnAuto: document.getElementById('btnAuto'),
515
+ btnStep: document.getElementById('btnStep'),
516
+ btnBack: document.getElementById('btnBack'),
517
+ stepDots: document.getElementById('stepDots').children,
518
+ };
519
+
520
+ function clearActive() {
521
+ Object.values(els).forEach(el => {
522
+ if (el && el.classList) el.classList.remove('active');
523
+ });
524
+ }
525
+
526
+ function setActiveResources(names) {
527
+ const map = { cpu: els.resCpu, ram: els.resRam, gpu: els.resGpu, vram: els.resVram };
528
+ for (const [key, el] of Object.entries(map)) {
529
+ if (names.includes(key)) {
530
+ el.classList.add('active');
531
+ el.querySelector('.res-status').textContent = 'active';
532
+ } else {
533
+ el.classList.remove('active');
534
+ el.querySelector('.res-status').textContent = 'idle';
535
+ }
536
+ }
537
+ }
538
+
539
+ function setStatus(msg) { els.status.innerHTML = msg; }
540
+ function setTokens(html) { els.tokenRow.innerHTML = html; }
541
+ function setKV(html) { els.kvEntries.innerHTML = html; }
542
+
543
+ function setDots(n) {
544
+ for (let i = 0; i < els.stepDots.length; i++) {
545
+ els.stepDots[i].classList.remove('done', 'current');
546
+ if (i < n) els.stepDots[i].classList.add('done');
547
+ if (i === n) els.stepDots[i].classList.add('current');
548
+ }
549
+ }
550
+
551
+ // --- Step implementations ---
552
+ function doIdle() {
553
+ clearActive();
554
+ setActiveResources([]);
555
+ setTokens('<span style="color:var(--dim); font-size:0.8rem;">Waiting for input...</span>');
556
+ setKV('<span style="color:var(--dim); font-size:0.75rem;">empty</span>');
557
+ setStatus('Click <span class="highlight">Auto</span> to run the full pipeline, or use <span class="highlight">Forward</span> / <span class="highlight">Back</span> to walk through one stage at a time.');
558
+ els.loopInfo.style.opacity = '0.3';
559
+ els.loopCount.textContent = '0 / ' + OUTPUT.length;
560
+ els.loopCount.classList.remove('active');
561
+ setDots(0);
562
+ }
563
+
564
+ function doInput() {
565
+ clearActive();
566
+ els.s_input.classList.add('active');
567
+ setActiveResources(['cpu']);
568
+ setTokens('<span style="color:var(--text); font-size:1rem; font-style:italic;">"The cat sat"</span>');
569
+ setKV('<span style="color:var(--dim); font-size:0.75rem;">empty</span>');
570
+ setStatus('📥 <span class="highlight">Input</span> — user submits the prompt text.<br><span style="color:var(--dim); font-size:0.75rem;">CPU &amp; RAM hold the raw string. GPU is idle.</span>');
571
+ els.loopInfo.style.opacity = '0.3';
572
+ setDots(1);
573
+ }
574
+
575
+ function doTokenize() {
576
+ clearActive();
577
+ els.s_tokenize.classList.add('active');
578
+ setActiveResources(['cpu', 'ram']);
579
+ let html = '';
580
+ TOKENS.forEach(t => {
581
+ html += `<span class="token" style="border-color:var(--cpu);">${t.id}</span>`;
582
+ });
583
+ setTokens(html + '<span style="color:var(--dim); margin-left:6px; font-size:0.7rem;">← token IDs</span>');
584
+ setKV('<span style="color:var(--dim); font-size:0.75rem;">empty</span>');
585
+ setStatus('🔤 <span class="highlight">Tokenize</span> — CPU converts text to token IDs: "The cat sat" → [576, 3797, 7236]<br><span style="color:var(--dim); font-size:0.75rem;">This is a fast CPU-bound step. Tokenizer runs on CPU.</span>');
586
+ els.loopInfo.style.opacity = '0.3';
587
+ setDots(2);
588
+ }
589
+
590
+ function doPrefill() {
591
+ clearActive();
592
+ els.s_prefill.classList.add('active');
593
+ setActiveResources(['gpu', 'vram']);
594
+ let html = '';
595
+ TOKENS.forEach(t => {
596
+ html += `<span class="token prefill">${t.id}<br><small>${t.text}</small></span>`;
597
+ });
598
+ html += '<span style="font-size:1.5rem; margin:0 4px;">→</span>';
599
+ html += `<span class="token new">${OUTPUT[0].id}<br><small>${OUTPUT[0].text}</small></span>`;
600
+ // V
601
+ setTokens(html);
602
+ // KV cache built
603
+ let kvHtml = '';
604
+ TOKENS.forEach((_, i) => {
605
+ kvHtml += `<div class="kv-cell filled" title="K${i+1} V${i+1}"></div>`;
606
+ });
607
+ setKV(kvHtml + '<span style="color:var(--dim); margin-left:4px; font-size:0.7rem;">3 entries built</span>');
608
+ setStatus('⚡ <span class="highlight">Prefill</span> — all 3 tokens run through the model <em>simultaneously</em>.<br><span style="color:var(--dim); font-size:0.75rem;">GPU compute spikes. KV cache is built (stored in VRAM). First output token appears: " on".<br><strong>Compute-bound:</strong> GPU is doing heavy matrix math.</span>');
609
+ els.loopInfo.style.opacity = '0.3';
610
+ els.loopCount.textContent = '1 / ' + OUTPUT.length;
611
+ setDots(3);
612
+ }
613
+
614
+ function doDecodeStep(idx) {
615
+ clearActive();
616
+ els.s_decode.classList.add('active');
617
+
618
+ // During decode, GPU is busy but more moderate (memory-bound)
619
+ setActiveResources(['gpu', 'vram']);
620
+
621
+ // Show all tokens so far
622
+ let html = '';
623
+ // Input tokens (cached)
624
+ TOKENS.forEach(t => {
625
+ html += `<span class="token cached">${t.id}</span>`;
626
+ });
627
+ // Previously generated output tokens (cached)
628
+ for (let i = 0; i < idx; i++) {
629
+ html += `<span class="token cached">${OUTPUT[i].id}</span>`;
630
+ }
631
+ // New token being generated
632
+ html += `<span class="token new">${OUTPUT[idx].id}<br><small>${OUTPUT[idx].text}</small></span>`;
633
+ // Show what's being read
634
+ html += '<span style="color:var(--dim); margin-left:8px; font-size:0.7rem;">← reads KV cache</span>';
635
+ setTokens(html);
636
+
637
+ // KV cache grows
638
+ let kvHtml = '';
639
+ const totalKV = TOKENS.length + idx;
640
+ for (let i = 0; i < totalKV; i++) {
641
+ kvHtml += `<div class="kv-cell filled" title="K${i+1} V${i+1}"></div>`;
642
+ }
643
+ setKV(kvHtml + `<span style="color:var(--dim); margin-left:4px; font-size:0.7rem;">${totalKV} entries — growing each step</span>`);
644
+
645
+ const tokenNum = idx + 1;
646
+ setStatus('🔄 <span class="highlight">Decode step ' + tokenNum + '/' + OUTPUT.length + '</span> — generating token: <span style="color:var(--active);">"' + OUTPUT[idx].text.trim() + '"</span><br><span style="color:var(--dim); font-size:0.75rem;">Only the NEW token needs fresh GPU compute. All previous tokens reuse their KV cache entries.<br><strong>Memory-bound:</strong> GPU spends most time reading the growing KV cache from VRAM.</span>');
647
+
648
+ els.loopInfo.style.opacity = '1';
649
+ els.loopCount.textContent = tokenNum + ' / ' + OUTPUT.length;
650
+ els.loopCount.classList.add('active');
651
+ els.loopInfo.querySelector('.loop-arrow').style.background = 'var(--decode)';
652
+ setDots(3 + idx);
653
+ }
654
+
655
+ function doDetokenize() {
656
+ clearActive();
657
+ els.s_output.classList.add('active');
658
+ setActiveResources(['cpu']);
659
+
660
+ // Show all tokens
661
+ let html = '';
662
+ TOKENS.forEach(t => { html += `<span class="token cached">${t.id}</span>`; });
663
+ OUTPUT.forEach(t => { html += `<span class="token cached">${t.id}</span>`; });
664
+ html += '<span style="font-size:1.5rem; margin:0 4px;">→</span>';
665
+ html += '<span style="color:var(--ram); font-size:1rem; font-style:italic;">"The cat sat on the mat"</span>';
666
+ setTokens(html);
667
+
668
+ // Final KV cache
669
+ let kvHtml = '';
670
+ for (let i = 0; i < TOKENS.length + OUTPUT.length; i++) {
671
+ kvHtml += `<div class="kv-cell filled" title="K${i+1} V${i+1}"></div>`;
672
+ }
673
+ setKV(kvHtml + '<span style="color:var(--dim); margin-left:4px; font-size:0.7rem;">6 total entries (freed after request)</span>');
674
+
675
+ setStatus('📤 <span class="highlight">Detokenize &amp; Output</span> — CPU converts token IDs back to text.<br><span style="color:var(--dim); font-size:0.75rem;">Final: "The cat sat on the mat" — KV cache is freed, GPU goes idle.</span>');
676
+ els.loopInfo.style.opacity = '0.3';
677
+ setDots(6);
678
+ }
679
+
680
+ // --- Step state machine ---
681
+ function step() {
682
+ if (autoTimer) return; // don't mix auto + manual
683
+
684
+ // step numbering:
685
+ // 0 idle, 1 input, 2 tokenize, 3 prefill, 4 decode[0], 5 decode[1], 6 decode[2], 7 detokenize
686
+ // idle returned by reset
687
+ const actions = [doIdle, doInput, doTokenize, doPrefill, ...OUTPUT.map((_, i) => () => doDecodeStep(i)), doDetokenize];
688
+
689
+ if (currentStep < actions.length - 1) {
690
+ currentStep++;
691
+ actions[currentStep]();
692
+ }
693
+
694
+ if (currentStep >= actions.length - 1) {
695
+ els.btnStep.disabled = true;
696
+ }
697
+ els.btnBack.disabled = (currentStep <= 0);
698
+ }
699
+
700
+ function back() {
701
+ if (autoTimer) return;
702
+ if (currentStep <= 0) return;
703
+
704
+ const actions = [doIdle, doInput, doTokenize, doPrefill, ...OUTPUT.map((_, i) => () => doDecodeStep(i)), doDetokenize];
705
+
706
+ currentStep--;
707
+ actions[currentStep]();
708
+
709
+ els.btnStep.disabled = false;
710
+ els.btnBack.disabled = (currentStep <= 0);
711
+ }
712
+
713
+ function reset() {
714
+ stopAuto();
715
+ currentStep = 0;
716
+ decodeIdx = 0;
717
+ doIdle();
718
+ els.btnStep.disabled = false;
719
+ els.btnBack.disabled = true;
720
+ }
721
+
722
+ // --- Auto play ---
723
+ function toggleAuto() {
724
+ if (autoTimer) { stopAuto(); return; }
725
+
726
+ reset();
727
+ els.btnAuto.textContent = '⏸ Stop';
728
+ els.btnAuto.classList.remove('primary');
729
+ els.btnStep.disabled = true;
730
+ els.btnBack.disabled = true;
731
+
732
+ const actions = [doIdle, doInput, doTokenize, doPrefill, ...OUTPUT.map((_, i) => () => doDecodeStep(i)), doDetokenize];
733
+
734
+ let i = 0;
735
+ function tick() {
736
+ if (i >= actions.length) { stopAuto(); return; }
737
+ actions[i]();
738
+ currentStep = i;
739
+ i++;
740
+
741
+ // Adjust timing: longer pauses for prefill/decode to let viewer read
742
+ let delay = 1200;
743
+ if (i === 4) delay = 1500; // prefill — let it sink in
744
+ if (i >= 5 && i <= actions.length - 2) delay = 1800; // decode steps — slower
745
+
746
+ autoTimer = setTimeout(tick, delay);
747
+ }
748
+ // Small initial delay so viewer sees the fresh start
749
+ autoTimer = setTimeout(tick, 400);
750
+ }
751
+
752
+ function stopAuto() {
753
+ if (autoTimer) clearTimeout(autoTimer);
754
+ autoTimer = null;
755
+ els.btnAuto.textContent = '▶ Auto';
756
+ els.btnAuto.classList.add('primary');
757
+ if (currentStep < 7) els.btnStep.disabled = false;
758
+ els.btnBack.disabled = (currentStep <= 0);
759
+ }
760
+
761
+ // --- Init ---
762
+ doIdle();
763
+
764
+ // ============================================================
765
+ // CONCURRENCY TAB — Multi-User Timeline Simulator
766
+ // ============================================================
767
+
768
+ const NUM_USERS = 4;
769
+ const TOTAL_TICKS = 17;
770
+
771
+ // Each user: { name, icon, prompt, outputTokens, schedule: [{tick, type, label}]
772
+ // Types: 'tokenize', 'prefill', 'decode', 'output', 'wait', 'done'
773
+ const USER_SCHEDULES = [
774
+ { // User A — arrives immediately, 6 output tokens
775
+ name: 'A', icon: '🟦', prompt: '"The cat sat"',
776
+ outputTokens: [' on', ' the', ' mat', ' by', ' the', ' fire'],
777
+ phases: [
778
+ { tick: 0, type: 'tokenize', label: 'T' },
779
+ { tick: 1, type: 'prefill', label: 'PF' },
780
+ { tick: 3, type: 'decode', label: 'D1', tokenIdx: 0 },
781
+ { tick: 4, type: 'decode', label: 'D2', tokenIdx: 1 },
782
+ { tick: 5, type: 'decode', label: 'D3', tokenIdx: 2 },
783
+ { tick: 6, type: 'decode', label: 'D4', tokenIdx: 3 },
784
+ { tick: 7, type: 'decode', label: 'D5', tokenIdx: 4 },
785
+ { tick: 8, type: 'decode', label: 'D6', tokenIdx: 5 },
786
+ { tick: 9, type: 'output', label: 'O' },
787
+ ]
788
+ },
789
+ { // User B — arrives at t=1, 5 output tokens
790
+ name: 'B', icon: '🟩', prompt: '"What is the"',
791
+ outputTokens: [' capital', ' of', ' France', ' today', '?'],
792
+ phases: [
793
+ { tick: 1, type: 'tokenize', label: 'T' },
794
+ { tick: 2, type: 'wait', label: '⋯' },
795
+ { tick: 3, type: 'prefill', label: 'PF' },
796
+ { tick: 5, type: 'decode', label: 'D1', tokenIdx: 0 },
797
+ { tick: 6, type: 'decode', label: 'D2', tokenIdx: 1 },
798
+ { tick: 7, type: 'decode', label: 'D3', tokenIdx: 2 },
799
+ { tick: 8, type: 'decode', label: 'D4', tokenIdx: 3 },
800
+ { tick: 9, type: 'decode', label: 'D5', tokenIdx: 4 },
801
+ { tick: 10, type: 'output', label: 'O' },
802
+ ]
803
+ },
804
+ { // User C — arrives at t=3, 5 output tokens
805
+ name: 'C', icon: '🟨', prompt: '"Write a haiku"',
806
+ outputTokens: [' about', ' spring', ' blossoms', ' falling', ' down'],
807
+ phases: [
808
+ { tick: 3, type: 'tokenize', label: 'T' },
809
+ { tick: 4, type: 'wait', label: '⋯' },
810
+ { tick: 5, type: 'wait', label: '⋯' },
811
+ { tick: 6, type: 'prefill', label: 'PF' },
812
+ { tick: 8, type: 'decode', label: 'D1', tokenIdx: 0 },
813
+ { tick: 9, type: 'decode', label: 'D2', tokenIdx: 1 },
814
+ { tick: 10, type: 'decode', label: 'D3', tokenIdx: 2 },
815
+ { tick: 11, type: 'decode', label: 'D4', tokenIdx: 3 },
816
+ { tick: 12, type: 'decode', label: 'D5', tokenIdx: 4 },
817
+ { tick: 13, type: 'output', label: 'O' },
818
+ ]
819
+ },
820
+ { // User D — arrives at t=5, 4 output tokens
821
+ name: 'D', icon: '🟪', prompt: '"Explain quantum"',
822
+ outputTokens: [' computing', ' in', ' simple', ' terms'],
823
+ phases: [
824
+ { tick: 5, type: 'tokenize', label: 'T' },
825
+ { tick: 6, type: 'wait', label: '⋯' },
826
+ { tick: 7, type: 'wait', label: '⋯' },
827
+ { tick: 8, type: 'prefill', label: 'PF' },
828
+ { tick: 10, type: 'decode', label: 'D1', tokenIdx: 0 },
829
+ { tick: 11, type: 'decode', label: 'D2', tokenIdx: 1 },
830
+ { tick: 12, type: 'decode', label: 'D3', tokenIdx: 2 },
831
+ { tick: 13, type: 'decode', label: 'D4', tokenIdx: 3 },
832
+ { tick: 14, type: 'output', label: 'O' },
833
+ ]
834
+ },
835
+ ];
836
+
837
+ // Per-tick GPU state
838
+ // At each tick, we compute: gpuActivity (prefill/decode/idle), active users, KV cache entries
839
+ function getGPUActivity(tick) {
840
+ let prefilling = [];
841
+ let decoding = [];
842
+ let totalKV = 0;
843
+ const inputTokenCounts = { A: 3, B: 3, C: 3, D: 2 }; // rough token counts per prompt
844
+
845
+ for (const user of USER_SCHEDULES) {
846
+ const phase = getUserPhaseAtTick(user, tick);
847
+ if (phase) {
848
+ if (phase.type === 'prefill') prefilling.push(user.name);
849
+ if (phase.type === 'decode') decoding.push(user.name);
850
+ }
851
+
852
+ const inputTokens = inputTokenCounts[user.name] || 2;
853
+
854
+ // Count decode tokens generated up to and including this tick
855
+ let decodeDone = 0;
856
+ for (const p of user.phases) {
857
+ if (p.type === 'decode' && p.tick <= tick) decodeDone++;
858
+ }
859
+ // KV entries exist after prefill has started (KV built during prefill, fully available after)
860
+ let prefillStarted = false;
861
+ for (const p of user.phases) {
862
+ if (p.type === 'prefill' && p.tick <= tick) prefillStarted = true;
863
+ }
864
+ if (prefillStarted || decodeDone > 0) {
865
+ totalKV += inputTokens + decodeDone;
866
+ }
867
+ }
868
+ return { prefilling, decoding, totalKV };
869
+ }
870
+
871
+ function getUserPhaseAtTick(user, tick) {
872
+ // Find the phase that contains this tick (phase lasts until next phase's tick)
873
+ for (let i = user.phases.length - 1; i >= 0; i--) {
874
+ if (user.phases[i].tick <= tick) {
875
+ return user.phases[i];
876
+ }
877
+ }
878
+ return null;
879
+ }
880
+
881
+ // For rendering: we need contiguous blocks, not just per-tick points.
882
+ // Prefill lasts 2 ticks, decode lasts 1 tick each, etc.
883
+ // Build display blocks from the phase list:
884
+ function buildDisplayBlocks(user) {
885
+ const blocks = [];
886
+ for (let i = 0; i < user.phases.length; i++) {
887
+ const phase = user.phases[i];
888
+ let startTick = phase.tick;
889
+ // Determine end tick: next phase's tick - 1, or if last phase, startTick
890
+ let endTick = startTick;
891
+ if (i < user.phases.length - 1) {
892
+ endTick = user.phases[i + 1].tick - 1;
893
+ }
894
+ // But for decode, each is exactly 1 tick
895
+ if (phase.type === 'decode' || phase.type === 'tokenize' || phase.type === 'output') {
896
+ endTick = startTick;
897
+ }
898
+ if (phase.type === 'prefill') {
899
+ // prefill spans from startTick to the next non-wait phase - 1
900
+ // Find next real phase after any wait phases
901
+ for (let j = i + 1; j < user.phases.length; j++) {
902
+ if (user.phases[j].type !== 'wait') {
903
+ endTick = user.phases[j].tick - 1;
904
+ break;
905
+ }
906
+ }
907
+ }
908
+ if (phase.type === 'wait') {
909
+ endTick = startTick;
910
+ // If there are consecutive waits, merge them
911
+ }
912
+ blocks.push({ ...phase, startTick, endTick });
913
+ }
914
+ return blocks;
915
+ }
916
+
917
+ // Merge consecutive same-type blocks
918
+ function mergeBlocks(blocks) {
919
+ const merged = [];
920
+ for (const b of blocks) {
921
+ const last = merged[merged.length - 1];
922
+ if (last && last.type === b.type && last.endTick + 1 === b.startTick) {
923
+ last.endTick = b.endTick;
924
+ } else {
925
+ merged.push({ ...b });
926
+ }
927
+ }
928
+ return merged;
929
+ }
930
+
931
+ // Precompute display blocks for all users
932
+ const USER_BLOCKS = USER_SCHEDULES.map(u => {
933
+ const blocks = buildDisplayBlocks(u);
934
+ return mergeBlocks(blocks);
935
+ });
936
+
937
+ // --- Concurrency state ---
938
+ let concTick = -1;
939
+ let concAutoTimer = null;
940
+ let concRunning = false;
941
+
942
+ // DOM refs for concurrency tab
943
+ function getConcEls() {
944
+ return {
945
+ swimlane: document.getElementById('swimlane'),
946
+ gpuSlots: document.getElementById('gpuSlots'),
947
+ gpuUtilBar: document.getElementById('gpuUtilBar'),
948
+ gpuUtilVal: document.getElementById('gpuUtilVal'),
949
+ vramBar: document.getElementById('vramBar'),
950
+ vramVal: document.getElementById('vramVal'),
951
+ status: document.getElementById('concStatus'),
952
+ btnAuto: document.getElementById('concBtnAuto'),
953
+ btnBack: document.getElementById('concBtnBack'),
954
+ btnStep: document.getElementById('concBtnStep'),
955
+ btnReset: document.getElementById('concBtnReset'),
956
+ };
957
+ }
958
+
959
+ function renderConcTick(tick) {
960
+ const els = getConcEls();
961
+ const gpu = getGPUActivity(tick);
962
+
963
+ // --- Build swimlane HTML ---
964
+ let html = '';
965
+ for (let ui = 0; ui < NUM_USERS; ui++) {
966
+ const user = USER_SCHEDULES[ui];
967
+ const blocks = USER_BLOCKS[ui];
968
+
969
+ html += '<div class="swimlane-row">';
970
+ html += `<div class="swimlane-label"><span class="user-icon">${user.icon}</span> ${user.name}: ${user.prompt}</div>`;
971
+ html += '<div class="swimlane-track" style="position:relative;">';
972
+
973
+ // Draw blocks
974
+ for (const block of blocks) {
975
+ const leftPct = (block.startTick / TOTAL_TICKS) * 100;
976
+ const widthPct = ((block.endTick - block.startTick + 1) / TOTAL_TICKS) * 100;
977
+
978
+ let cls = '';
979
+ if (block.type === 'prefill') cls = 'prefill-block';
980
+ else if (block.type === 'decode') cls = 'decode-block';
981
+ else if (block.type === 'wait') cls = 'wait-block';
982
+ else if (block.type === 'tokenize' || block.type === 'output') cls = 'input-block';
983
+ else continue;
984
+
985
+ // Highlight if this block is active at current tick
986
+ const isActive = tick >= block.startTick && tick <= block.endTick;
987
+ const opacity = isActive ? '1' : (tick > block.endTick ? '0.5' : '0.35');
988
+ const zIdx = isActive ? '3' : '1';
989
+
990
+ // Label for decode: show which token
991
+ let label = block.label || '';
992
+ if (block.type === 'decode' && block.tokenIdx !== undefined) {
993
+ label = `D${block.tokenIdx + 1}`;
994
+ }
995
+ if (block.type === 'prefill') label = 'PF';
996
+ if (block.type === 'tokenize') label = 'T';
997
+ if (block.type === 'output') label = 'O';
998
+ if (block.type === 'wait') label = '';
999
+
1000
+ if (widthPct > 1.5) {
1001
+ html += `<div class="swimlane-block ${cls}" style="left:${leftPct}%;width:${widthPct}%;opacity:${opacity};z-index:${zIdx};">${label}</div>`;
1002
+ }
1003
+ }
1004
+
1005
+ // Tick cursor line if user is active
1006
+ const phase = getUserPhaseAtTick(user, tick);
1007
+ if (phase && phase.type !== 'done') {
1008
+ const cursorPct = ((tick + 0.5) / TOTAL_TICKS) * 100;
1009
+ html += `<div style="position:absolute;left:${cursorPct}%;top:0;bottom:0;width:2px;background:var(--active);z-index:10;opacity:0.8;"></div>`;
1010
+ }
1011
+
1012
+ html += '</div></div>';
1013
+ }
1014
+ els.swimlane.innerHTML = html;
1015
+
1016
+ // --- GPU utilization ---
1017
+ let gpuPct = 0;
1018
+ if (gpu.prefilling.length > 0) gpuPct = 90;
1019
+ else if (gpu.decoding.length > 0) gpuPct = Math.min(30 + gpu.decoding.length * 15, 85);
1020
+ els.gpuUtilBar.style.width = gpuPct + '%';
1021
+ els.gpuUtilVal.textContent = gpuPct + '%';
1022
+
1023
+ // --- VRAM usage (KB per KV entry, rough) ---
1024
+ const kvGB = (gpu.totalKV * 0.4).toFixed(1);
1025
+ const vramPct = Math.min(gpu.totalKV * 4, 100);
1026
+ els.vramBar.style.width = vramPct + '%';
1027
+ els.vramVal.textContent = kvGB + ' GB';
1028
+
1029
+ // --- GPU scheduler slots ---
1030
+ let slotHTML = '';
1031
+ if (gpu.prefilling.length > 0) {
1032
+ slotHTML = gpu.prefilling.map(u => `<span class="gpu-slot busy-prefill">⚡ ${u} prefill</span>`).join('');
1033
+ } else if (gpu.decoding.length > 0) {
1034
+ slotHTML = `<span class="gpu-slot busy-decode">🔄 batch decode (${gpu.decoding.join(',')})</span>`;
1035
+ } else {
1036
+ slotHTML = '<span class="gpu-slot" style="color:var(--dim)">idle</span>';
1037
+ }
1038
+ els.gpuSlots.innerHTML = slotHTML;
1039
+
1040
+ // --- Status text ---
1041
+ let statusMsg = '';
1042
+ const tickLabel = tick >= 0 ? `Tick ${tick}` : '';
1043
+ if (tick < 0) {
1044
+ statusMsg = 'Click <span class="highlight">Start</span> to watch 4 users stream through the pipeline concurrently.';
1045
+ } else if (gpu.prefilling.length > 0 && gpu.decoding.length > 0) {
1046
+ statusMsg = `<span class="highlight">${tickLabel}:</span> GPU <span style="color:var(--prefill);">prefills</span> for ${gpu.prefilling.join(', ')} <strong>while</strong> <span style="color:var(--decode);">batching decode</span> for ${gpu.decoding.join(', ')} — continuous batching in action!`;
1047
+ } else if (gpu.prefilling.length > 0) {
1048
+ statusMsg = `<span class="highlight">${tickLabel}:</span> GPU is <span style="color:var(--prefill);">prefilling</span> for ${gpu.prefilling.join(', ')} — compute-bound, KV cache building.`;
1049
+ } else if (gpu.decoding.length > 0) {
1050
+ statusMsg = `<span class="highlight">${tickLabel}:</span> GPU is <span style="color:var(--decode);">batching decode</span> for ${gpu.decoding.join(', ')} — memory-bound, reading KV cache.`;
1051
+ } else if (tick >= 15) {
1052
+ statusMsg = `<span class="highlight">${tickLabel}:</span> All requests complete. GPU idle. KV cache freed.`;
1053
+ } else {
1054
+ statusMsg = `<span class="highlight">${tickLabel}:</span> GPU idle — requests tokenizing or queued.`;
1055
+ }
1056
+
1057
+ // Add throughput note at end
1058
+ const completed = USER_SCHEDULES.filter(u => {
1059
+ const last = u.phases[u.phases.length - 1];
1060
+ return tick >= last.tick;
1061
+ }).length;
1062
+ if (tick >= 14 && completed === 4) {
1063
+ statusMsg += '<br><span style="color:var(--ram);">✓ All 4 requests completed in 14 ticks. Serially would take ~36 ticks. <strong>~2.6× throughput</strong> via concurrency.</span>';
1064
+ }
1065
+
1066
+ els.status.innerHTML = statusMsg;
1067
+
1068
+ // --- Update buttons ---
1069
+ els.btnStep.disabled = tick >= TOTAL_TICKS - 1;
1070
+ els.btnBack.disabled = tick <= 0;
1071
+ }
1072
+
1073
+ function concStep() {
1074
+ if (concAutoTimer) return;
1075
+ if (concTick >= TOTAL_TICKS - 1) return;
1076
+ concTick++;
1077
+ renderConcTick(concTick);
1078
+ }
1079
+
1080
+ function concBack() {
1081
+ if (concAutoTimer) return;
1082
+ if (concTick <= 0) return;
1083
+ concTick--;
1084
+ renderConcTick(concTick);
1085
+ }
1086
+
1087
+ function concReset() {
1088
+ concStopAuto();
1089
+ concTick = -1;
1090
+ const els = getConcEls();
1091
+ els.gpuUtilBar.style.width = '0%';
1092
+ els.gpuUtilVal.textContent = '0%';
1093
+ els.vramBar.style.width = '0%';
1094
+ els.vramVal.textContent = '0 GB';
1095
+ els.gpuSlots.innerHTML = '<span class="gpu-slot" style="color:var(--dim)">idle</span>';
1096
+ els.status.innerHTML = 'Click <span class="highlight">Start</span> to watch 4 users stream through the pipeline concurrently.';
1097
+ els.btnStep.disabled = false;
1098
+ els.btnBack.disabled = true;
1099
+ renderConcTick(-1);
1100
+ }
1101
+
1102
+ function concToggleAuto() {
1103
+ if (concAutoTimer) { concStopAuto(); return; }
1104
+
1105
+ concReset();
1106
+ concTick = -1;
1107
+ const els = getConcEls();
1108
+ els.btnAuto.textContent = '⏸ Stop';
1109
+ els.btnAuto.classList.remove('primary');
1110
+ els.btnStep.disabled = true;
1111
+ els.btnBack.disabled = true;
1112
+ els.btnReset.disabled = false;
1113
+ concRunning = true;
1114
+
1115
+ const delays = [
1116
+ 600, // t0: A tokenizes
1117
+ 1000, // t1: A prefill (compute-heavy)
1118
+ 1000, // t2: A prefill cont, B waits
1119
+ 900, // t3: A decode1, B prefill, C arrives
1120
+ 700, // t4: A decode2, B prefill cont
1121
+ 700, // t5: A decode3, B decode1, D arrives
1122
+ 900, // t6: A decode4, B decode2, C prefill
1123
+ 800, // t7: A decode5, B decode3, C prefill cont
1124
+ 1000, // t8: A decode6, B decode4, C decode1, D prefill (4 users active!)
1125
+ 800, // t9: A output, B decode5, C decode2, D prefill cont
1126
+ 900, // t10: B output, C decode3, D decode1
1127
+ 700, // t11: C decode4, D decode2
1128
+ 700, // t12: C decode5, D decode3
1129
+ 800, // t13: C output, D decode4
1130
+ 900, // t14: D output — all done
1131
+ 1800, // t15-16: idle, show summary
1132
+ ];
1133
+
1134
+ let i = 0;
1135
+ function tick() {
1136
+ if (i > TOTAL_TICKS - 1) { concStopAuto(); return; }
1137
+ concTick = i;
1138
+ renderConcTick(i);
1139
+ i++;
1140
+ const delay = delays[i - 1] || 600;
1141
+ concAutoTimer = setTimeout(tick, delay);
1142
+ }
1143
+ concAutoTimer = setTimeout(tick, 400);
1144
+ }
1145
+
1146
+ function concStopAuto() {
1147
+ if (concAutoTimer) clearTimeout(concAutoTimer);
1148
+ concAutoTimer = null;
1149
+ concRunning = false;
1150
+ const els = getConcEls();
1151
+ els.btnAuto.textContent = '▶ Start';
1152
+ els.btnAuto.classList.add('primary');
1153
+ if (concTick < TOTAL_TICKS - 1) els.btnStep.disabled = false;
1154
+ els.btnBack.disabled = concTick <= 0;
1155
+ els.btnReset.disabled = false;
1156
+ }
1157
+
1158
+ // --- Tab switching ---
1159
+ function switchTab(tab) {
1160
+ document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
1161
+ document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('visible'));
1162
+ if (tab === 'pipeline') {
1163
+ document.querySelectorAll('.tab-btn')[0].classList.add('active');
1164
+ document.getElementById('tab-pipeline').classList.add('visible');
1165
+ } else {
1166
+ document.querySelectorAll('.tab-btn')[1].classList.add('active');
1167
+ document.getElementById('tab-concurrency').classList.add('visible');
1168
+ concReset();
1169
+ }
1170
+ }
1171
+
1172
+ // Init concurrency tab
1173
+ concReset();
1174
+ </script>
1175
+ </body>
1176
  </html>