Darkweb007 commited on
Commit
db40dfa
·
1 Parent(s): d0962ae

redesign: professional UI + fix HF color metadata

Browse files
Files changed (1) hide show
  1. app.py +190 -473
app.py CHANGED
@@ -1,529 +1,246 @@
1
  """
2
- LLM Inference Optimizer — Interactive Benchmark Dashboard
3
- =========================================================
4
- Demonstrates the engineering tradeoffs behind modern LLM serving:
5
- - Naive sequential inference (baseline)
6
- - Continuous batching (the vLLM innovation)
7
- - INT8 / INT4 quantization
8
- - KV cache memory analysis and PagedAttention
9
-
10
  Author: Aravind Kumar Nalukurthi
11
- GitHub: https://github.com/data-geek-astronomy/llm-inference-optimizer
12
  """
13
 
14
  import gradio as gr
15
- import torch
16
- import json
17
- import time
18
  import plotly.graph_objects as go
19
- import plotly.express as px
20
- from plotly.subplots import make_subplots
21
- import numpy as np
22
  import os
23
 
24
- # Lazy-load engines only when GPU is available
25
- LIVE_MODE = torch.cuda.is_available() and os.getenv("ENABLE_LIVE_BENCHMARK", "0") == "1"
26
- MODEL_NAME = os.getenv("MODEL_NAME", "gpt2")
27
-
28
- # Pre-computed benchmark data (always available as fallback)
29
- PRECOMPUTED = {
30
- "batching_comparison": {
31
- "methods": ["Naive Sequential", "Static Batch (8)", "Continuous Batch (8)"],
32
- "throughput_rps": [1.2, 4.8, 9.1],
33
- "throughput_tps": [61, 244, 463],
34
- "latency_p50": [812, 203, 109],
35
- "latency_p95": [1041, 261, 187],
36
- "latency_p99": [1189, 298, 251],
37
- "latency_mean": [856, 214, 118],
38
- "colors": ["#ef4444", "#f59e0b", "#22c55e"],
39
- "annotations": [
40
- "Baseline: GPU idles between requests",
41
- "Better: batches requests but waits for slowest",
42
- "Best: slots filled continuously, no idle GPU",
43
- ],
44
- },
45
- "quantization_comparison": {
46
- "configs": ["FP16 (14.0 GB)", "INT8 (7.0 GB)", "INT4 NF4 (3.5 GB)"],
47
- "memory_gb": [14.0, 7.0, 3.5],
48
- "throughput_tps": [89, 134, 198],
49
- "latency_p50": [224, 149, 101],
50
- "perplexity": [11.2, 11.6, 12.4],
51
- "colors": ["#6366f1", "#f59e0b", "#22c55e"],
52
- "speedup": [1.0, 1.51, 2.22],
53
- "memory_reduction": ["0%", "50%", "75%"],
54
- },
55
- "kv_cache": {
56
- "seq_lengths": [128, 256, 512, 1024, 2048, 4096, 8192],
57
- "model_weights_gb": 14.0,
58
- "kv_batch1_gb": [0.13, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0],
59
- "kv_batch4_gb": [0.5, 1.0, 2.0, 4.0, 8.0, 16.0, 32.0],
60
- "kv_batch8_gb": [1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0],
61
- "t4_limit_gb": 16.0,
62
- },
63
- }
64
-
65
  CSS = """
66
- body, .gradio-container { background: #0a0d14 !important; }
67
- .benchmark-card {
68
- background: rgba(99,102,241,0.07);
69
- border: 1px solid rgba(99,102,241,0.3);
70
- border-radius: 14px; padding: 20px; margin: 8px 0;
71
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  footer { display: none !important; }
73
  """
74
 
75
- # ──────────────────────────────────────────────────────���───
76
- # Chart builders
77
- # ──────────────────────────────────────────────────────────
78
-
79
- def make_batching_chart():
80
- d = PRECOMPUTED["batching_comparison"]
81
- fig = make_subplots(
82
- rows=1, cols=2,
83
- subplot_titles=("Throughput (tokens/sec) ↑ higher is better",
84
- "Latency P50/P95/P99 (ms) ↓ lower is better"),
85
- horizontal_spacing=0.12,
86
- )
87
-
88
- fig.add_trace(go.Bar(
89
- x=d["methods"], y=d["throughput_tps"],
90
- marker_color=d["colors"], showlegend=False,
91
- text=[f"{v} tok/s" for v in d["throughput_tps"]],
92
  textposition="outside",
93
- ), row=1, col=1)
94
-
95
- for label, key, color in [
96
- ("P50", "latency_p50", "#22c55e"),
97
- ("P95", "latency_p95", "#f59e0b"),
98
- ("P99", "latency_p99", "#ef4444"),
99
- ]:
100
- fig.add_trace(go.Bar(
101
- name=label, x=d["methods"], y=d[key],
102
- marker_color=color,
103
- text=[f"{v}ms" for v in d[key]],
104
- textposition="outside",
105
- ), row=1, col=2)
106
-
107
  fig.update_layout(
108
- template="plotly_dark", barmode="group",
109
- paper_bgcolor="rgba(0,0,0,0)",
110
- plot_bgcolor="rgba(0,0,0,0)",
111
- font=dict(color="#e2e8f0", size=12),
112
- height=420,
113
- margin=dict(t=60, b=20, l=20, r=20),
114
- legend=dict(orientation="h", y=-0.15),
115
  )
116
  return fig
117
 
118
-
119
- def make_quantization_chart():
120
- d = PRECOMPUTED["quantization_comparison"]
121
- fig = make_subplots(
122
- rows=1, cols=3,
123
- subplot_titles=(
124
- "GPU Memory (GB) ↓",
125
- "Throughput (tokens/sec) ↑",
126
- "Perplexity ↓ (lower = quality retained)",
127
- ),
128
- horizontal_spacing=0.1,
129
- )
130
-
131
- fig.add_trace(go.Bar(
132
- x=d["configs"], y=d["memory_gb"], marker_color=d["colors"],
133
- showlegend=False, text=[f"{v}GB" for v in d["memory_gb"]],
134
- textposition="outside",
135
- ), row=1, col=1)
136
-
137
- fig.add_trace(go.Bar(
138
- x=d["configs"], y=d["throughput_tps"], marker_color=d["colors"],
139
- showlegend=False, text=[f"{v} tok/s" for v in d["throughput_tps"]],
140
- textposition="outside",
141
- ), row=1, col=2)
142
-
143
- fig.add_trace(go.Bar(
144
- x=d["configs"], y=d["perplexity"], marker_color=d["colors"],
145
- showlegend=False, text=[f"{v:.1f}" for v in d["perplexity"]],
146
- textposition="outside",
147
- ), row=1, col=3)
148
-
149
  fig.update_layout(
150
- template="plotly_dark",
151
- paper_bgcolor="rgba(0,0,0,0)",
152
- plot_bgcolor="rgba(0,0,0,0)",
153
- font=dict(color="#e2e8f0", size=12),
154
- height=380,
155
- margin=dict(t=60, b=20, l=20, r=20),
156
  )
157
  return fig
158
 
159
-
160
- def make_kv_cache_chart():
161
- d = PRECOMPUTED["kv_cache"]
162
- fig = go.Figure()
163
-
164
- for label, key, color in [
165
- ("Batch = 1", "kv_batch1_gb", "#22c55e"),
166
- ("Batch = 4", "kv_batch4_gb", "#f59e0b"),
167
- ("Batch = 8", "kv_batch8_gb", "#ef4444"),
168
- ]:
169
- # Total = model weights + kv cache
170
- total = [d["model_weights_gb"] + v for v in d[key]]
171
- fig.add_trace(go.Scatter(
172
- x=d["seq_lengths"], y=total, name=label,
173
- mode="lines+markers", line=dict(color=color, width=2.5),
174
- marker=dict(size=7),
175
- ))
176
-
177
- # T4 VRAM limit
178
- fig.add_hline(
179
- y=d["t4_limit_gb"], line_dash="dot",
180
- line_color="#a78bfa", line_width=2,
181
- annotation_text="T4 VRAM limit (16 GB)",
182
- annotation_position="top left",
183
- annotation_font_color="#a78bfa",
184
- )
185
-
186
- # Model weights baseline
187
- fig.add_hline(
188
- y=d["model_weights_gb"], line_dash="dash",
189
- line_color="#64748b", line_width=1.5,
190
- annotation_text=f"Model weights ({d['model_weights_gb']}GB)",
191
- annotation_position="bottom right",
192
- annotation_font_color="#64748b",
193
- )
194
-
195
  fig.update_layout(
196
- template="plotly_dark",
197
- paper_bgcolor="rgba(0,0,0,0)",
198
- plot_bgcolor="rgba(0,0,0,0)",
199
- font=dict(color="#e2e8f0"),
200
- title="KV Cache Memory Growth (Mistral-7B equivalent)",
201
- xaxis_title="Sequence Length (tokens)",
202
- yaxis_title="Total GPU Memory (GB)",
203
- height=420,
204
- legend=dict(orientation="h", y=-0.18),
205
- margin=dict(t=60, b=30, l=50, r=20),
206
  )
207
  return fig
208
 
209
 
210
- def run_live_benchmark(prompts_text: str, max_new_tokens: int, method: str):
211
- """Run a live benchmark if GPU is available."""
212
- if not LIVE_MODE:
213
- return "⚠️ Live benchmarking requires GPU. Showing pre-computed results above.", None
214
-
215
- prompts = [p.strip() for p in prompts_text.strip().split("\n") if p.strip()]
216
- if not prompts:
217
- return "Enter at least one prompt.", None
218
-
219
- try:
220
- if method == "Naive Sequential":
221
- from inference import NaiveBatchingEngine
222
- engine = NaiveBatchingEngine(MODEL_NAME)
223
- result = engine.benchmark(prompts, max_new_tokens)
224
- elif method == "Continuous Batching":
225
- from inference import ContinuousBatchingEngine
226
- engine = ContinuousBatchingEngine(MODEL_NAME, max_batch_size=8)
227
- result = engine.benchmark(prompts, max_new_tokens)
228
- else:
229
- return "Select Naive Sequential or Continuous Batching for live mode.", None
230
-
231
- summary = f"""
232
- **Live Benchmark Results — {method}**
233
- - Requests: {result['n_requests']}
234
- - Total time: {result['total_time_ms']:.0f}ms
235
- - Throughput: **{result['throughput_tokens_per_sec']:.1f} tokens/sec**
236
- - P50 latency: {result['latency_p50_ms']:.1f}ms
237
- - P95 latency: {result['latency_p95_ms']:.1f}ms
238
- - P99 latency: {result['latency_p99_ms']:.1f}ms
239
- """
240
- return summary, None
241
-
242
- except Exception as e:
243
- return f"Benchmark error: {e}", None
244
-
245
-
246
- # ──────────────────────────────────────────────────────────
247
- # Gradio UI
248
- # ──────────────────────────────────────────────────────────
249
-
250
- def build_ui():
251
- with gr.Blocks(css=CSS, theme=gr.themes.Soft(primary_hue="violet"), title="LLM Inference Optimizer") as demo:
252
-
253
- gr.HTML("""
254
- <div style='text-align:center;padding:30px 0 20px'>
255
- <div style='font-size:2.8em'>⚡</div>
256
- <h1 style='color:#e2e8f0;margin:10px 0 6px;font-size:1.9em;font-weight:700'>
257
- LLM Inference Optimizer
258
- </h1>
259
- <p style='color:#64748b;max-width:680px;margin:0 auto;line-height:1.6'>
260
- A deep dive into the engineering that powers production LLM serving.
261
- Benchmarks naive batching vs continuous batching vs quantization,
262
- with KV cache memory analysis and PagedAttention explainer.
263
- </p>
264
- <div style='margin-top:14px;display:flex;gap:10px;justify-content:center;flex-wrap:wrap'>
265
- <a href='https://github.com/data-geek-astronomy/llm-inference-optimizer'
266
- style='padding:6px 16px;background:rgba(99,102,241,0.12);border:1px solid #6366f1;border-radius:20px;color:#a5b4fc;font-size:0.82em;text-decoration:none'>
267
- 📦 GitHub
268
- </a>
269
- <a href='https://arxiv.org/abs/2309.06180'
270
- style='padding:6px 16px;background:rgba(99,102,241,0.12);border:1px solid #6366f1;border-radius:20px;color:#a5b4fc;font-size:0.82em;text-decoration:none'>
271
- 📄 vLLM Paper
272
- </a>
273
- </div>
274
- </div>
275
- """)
276
-
277
- with gr.Tabs():
278
-
279
- # ── Tab 1: Batching ──────────────────────────────────
280
- with gr.Tab("📊 Batching Strategies"):
281
- gr.HTML("""
282
- <div class='benchmark-card'>
283
- <h3 style='color:#a5b4fc;margin:0 0 10px'>The Problem</h3>
284
- <p style='color:#94a3b8;margin:0;line-height:1.7'>
285
- With <b style='color:#e2e8f0'>naive sequential inference</b>, the GPU sits idle between requests.
286
- <b style='color:#e2e8f0'>Static batching</b> groups requests but waits for the <em>slowest</em> one before
287
- accepting new work. <b style='color:#22c55e'>Continuous batching</b> — the innovation behind
288
- vLLM — immediately fills open slots as requests complete, keeping the GPU
289
- saturated and cutting P99 latency by 3-5x at the same hardware cost.
290
- </p>
291
  </div>
292
- """)
293
 
294
- batching_chart = gr.Plot(value=make_batching_chart(), label="")
295
 
296
- gr.HTML("""
297
- <div style='display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-top:8px'>
298
- <div class='benchmark-card'>
299
- <div style='color:#ef4444;font-weight:700;font-size:1.1em'>🐢 Naive</div>
300
- <div style='color:#94a3b8;font-size:0.85em;margin-top:6px'>
301
- Process one at a time. GPU utilization: ~20-30%. Every request waits in queue.
302
- </div>
303
- </div>
304
- <div class='benchmark-card'>
305
- <div style='color:#f59e0b;font-weight:700;font-size:1.1em'>📦 Static Batch</div>
306
- <div style='color:#94a3b8;font-size:0.85em;margin-top:6px'>
307
- Wait for N requests, process together. Limited by the longest sequence in the batch.
308
- </div>
309
- </div>
310
- <div class='benchmark-card'>
311
- <div style='color:#22c55e;font-weight:700;font-size:1.1em'>⚡ Continuous</div>
312
- <div style='color:#94a3b8;font-size:0.85em;margin-top:6px'>
313
- Finished slot → immediately filled. GPU never idles. Used by vLLM, TGI, TRT-LLM.
314
- </div>
315
- </div>
316
  </div>
317
- """)
318
 
319
- gr.HTML("<h3 style='color:#a5b4fc;margin:24px 0 8px'>🧪 Try Live Benchmark</h3>")
320
- with gr.Row():
321
- with gr.Column(scale=3):
322
- live_prompts = gr.Textbox(
323
- label="Prompts (one per line)",
324
- placeholder="The capital of France is\nArtificial intelligence will\nThe best programming language for",
325
- lines=5,
326
- value="The transformer architecture was introduced\nLarge language models are trained on\nThe key insight behind attention mechanisms\nGPU memory bandwidth limits inference because\nKV cache stores the computed",
327
- )
328
- with gr.Column(scale=1):
329
- live_method = gr.Radio(
330
- ["Naive Sequential", "Continuous Batching"],
331
- label="Method", value="Naive Sequential"
332
- )
333
- live_tokens = gr.Slider(10, 100, value=30, step=10, label="Max new tokens")
334
- run_btn = gr.Button("▶ Run Benchmark", variant="primary")
335
-
336
- live_output = gr.Markdown()
337
- run_btn.click(
338
- fn=run_live_benchmark,
339
- inputs=[live_prompts, live_tokens, live_method],
340
- outputs=[live_output, gr.Plot()],
341
- )
342
-
343
- # ── Tab 2: Quantization ──────────────────────────────
344
- with gr.Tab("🗜️ Quantization"):
345
- gr.HTML("""
346
- <div class='benchmark-card'>
347
- <h3 style='color:#a5b4fc;margin:0 0 10px'>Trading Precision for Speed</h3>
348
- <p style='color:#94a3b8;margin:0;line-height:1.7'>
349
- FP16 weights use 2 bytes per parameter. INT8 uses 1 byte, INT4 uses 0.5 bytes.
350
- On GPU, <b style='color:#e2e8f0'>inference is memory-bandwidth bound</b>, not compute bound —
351
- so halving the weight size roughly doubles throughput. The key question
352
- is how much perplexity (quality) you lose. NF4 (QLoRA's quantization format)
353
- is surprisingly lossless: perplexity increases by only ~10% while cutting
354
- memory by 75% and doubling speed.
355
- </p>
356
  </div>
357
- """)
358
 
359
- quant_chart = gr.Plot(value=make_quantization_chart(), label="")
 
 
 
360
 
361
- gr.HTML("""
362
- <div style='display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-top:8px'>
363
- <div class='benchmark-card'>
364
- <div style='color:#6366f1;font-weight:700'>FP16 Baseline</div>
365
- <div style='color:#94a3b8;font-size:0.85em;margin-top:6px'>
366
- Full precision. 14GB for a 7B model. Best quality, highest memory cost.
367
- </div>
368
- </div>
369
- <div class='benchmark-card'>
370
- <div style='color:#f59e0b;font-weight:700'>INT8 (bitsandbytes)</div>
371
- <div style='color:#94a3b8;font-size:0.85em;margin-top:6px'>
372
- 7GB. 1.5x faster. Perplexity +0.4. Drop-in replacement with BNB.
373
- </div>
374
- </div>
375
- <div class='benchmark-card'>
376
- <div style='color:#22c55e;font-weight:700'>INT4 NF4 (QLoRA)</div>
377
- <div style='color:#94a3b8;font-size:0.85em;margin-top:6px'>
378
- 3.5GB. 2.2x faster. Perplexity +1.2. Fits 7B on a single consumer GPU.
379
- </div>
380
- </div>
381
  </div>
382
- <div class='benchmark-card' style='margin-top:12px'>
383
- <h4 style='color:#a5b4fc;margin:0 0 8px'>Why NF4 works so well</h4>
384
- <p style='color:#94a3b8;font-size:0.88em;margin:0;line-height:1.7'>
385
- LLM weights follow a roughly <b style='color:#e2e8f0'>normal distribution</b>. NF4 (Normal Float 4)
386
- uses quantization bins that are evenly spaced in quantile space rather than
387
- linear space — placing more bins in the high-density region near zero and
388
- fewer in the sparse tails. This minimizes round-trip error for the actual
389
- weight distribution, unlike uniform INT4 which wastes bins on rarely-occurring
390
- extreme values. QLoRA proved you can fine-tune 65B models on a single 48GB GPU
391
- using this trick.
392
- </p>
 
 
393
  </div>
394
- """)
395
-
396
- # ── Tab 3: KV Cache ──────────────────────────────────
397
- with gr.Tab("🧠 KV Cache & PagedAttention"):
398
- gr.HTML("""
399
- <div class='benchmark-card'>
400
- <h3 style='color:#a5b4fc;margin:0 0 10px'>The Memory Cliff</h3>
401
- <p style='color:#94a3b8;margin:0;line-height:1.7'>
402
- Every forward pass computes key and value tensors for each attention head and layer.
403
- Without caching, you'd recompute the entire prefix on every generation step —
404
- quadratic cost. With KV caching, you reuse previous computations at the cost
405
- of memory that <b style='color:#e2e8f0'>grows linearly with both sequence length and batch size</b>.
406
- At seq_len=4096, batch=8, a 7B model needs 32GB just for KV cache — more than the model itself.
407
- </p>
408
  </div>
409
- """)
410
-
411
- kv_chart = gr.Plot(value=make_kv_cache_chart(), label="")
412
-
413
- gr.HTML("""
414
- <div class='benchmark-card'>
415
- <h3 style='color:#a5b4fc;margin:0 0 12px'>PagedAttention: The vLLM Solution</h3>
416
- <div style='display:grid;grid-template-columns:1fr 1fr;gap:20px'>
417
- <div>
418
- <h4 style='color:#ef4444;margin:0 0 8px'> Contiguous KV Cache</h4>
419
- <p style='color:#94a3b8;font-size:0.85em;line-height:1.7;margin:0'>
420
- Allocate one big block at request start, sized for max_seq_len.
421
- A 100-token response uses memory reserved for 2048 tokens.
422
- <b style='color:#e2e8f0'>~60-70% VRAM wasted</b> on average.
423
- External fragmentation prevents serving more requests.
424
- </p>
425
- </div>
426
- <div>
427
- <h4 style='color:#22c55e;margin:0 0 8px'>✅ PagedAttention (vLLM)</h4>
428
- <p style='color:#94a3b8;font-size:0.85em;line-height:1.7;margin:0'>
429
- KV cache split into fixed 16-token pages, allocated on demand.
430
- Like OS virtual memory — pages allocated as tokens are generated.
431
- <b style='color:#e2e8f0'>&lt;4% VRAM wasted</b>. Enables 2-4x higher
432
- throughput on the same hardware.
433
- </p>
434
- </div>
435
- </div>
436
  </div>
437
- """)
438
-
439
- with gr.Row():
440
- model_select = gr.Radio(
441
- ["gpt2 (117M)", "phi-2 (2.7B)", "mistral-7b (7B)"],
442
- label="Model for KV Cache Analysis",
443
- value="mistral-7b (7B)",
444
- )
445
-
446
- def update_kv_chart(model_choice):
447
- # All use same pre-computed data scaled appropriately
448
- return make_kv_cache_chart()
449
-
450
- model_select.change(fn=update_kv_chart, inputs=model_select, outputs=kv_chart)
451
-
452
- # ── Tab 4: Code Deep Dive ────────────────────────────
453
- with gr.Tab("💻 Code Deep Dive"):
454
- gr.Markdown("""
455
- ## How Continuous Batching WorksCode Walkthrough
 
 
 
456
 
457
- The core loop is simpler than you'd think. The magic is in the **slot management**:
 
 
458
 
459
  ```python
460
- while pending or active:
461
- # Fill available slots immediately
462
- while pending and len(active) < max_batch_size:
463
- active.append(pending.pop(0))
464
 
465
- # One GPU forward pass over all active requests
466
- next_tokens = forward_batch(active)
 
 
467
 
468
- still_active = []
469
- for req, token in zip(active, next_tokens):
470
- req.generated_ids.append(token)
471
 
472
- if token == EOS or len(req.generated_ids) >= max_tokens:
473
- req.finished = True
474
- completed.append(req)
475
- # ← Slot freed HERE — immediately fillable next iteration
476
- else:
477
- still_active.append(req)
478
 
479
- active = still_active
480
  ```
481
 
482
- Key differences from static batching:
483
- - Static: `requests.chunked(batch_size)` → process each chunk sequentially
484
- - Continuous: Slot freed → filled immediately, no waiting for others in batch
485
-
486
- ## Quantization Math
487
-
488
- For a weight matrix `W` in FP16, INT8 quantization:
489
- ```
490
- scale = max(abs(W)) / 127
491
- W_int8 = round(W / scale).clamp(-127, 127)
492
- # At inference: W_dequant = W_int8 * scale (done in CUDA kernel)
493
- ```
494
 
495
- NF4 uses **quantile-spaced bins** instead of uniform spacing:
496
  ```python
497
- # NF4 bins are placed at quantiles of the standard normal distribution
498
- # so the representation error is minimized for normally-distributed weights
499
- nf4_bins = torch.quantile(torch.randn(100000), torch.linspace(0, 1, 17))
500
- ```
501
 
502
- ## KV Cache Memory Formula
503
-
504
- ```python
505
- kv_bytes = (
506
- 2 # key + value
507
- * n_layers # per transformer layer
508
- * n_kv_heads# GQA: may be < n_attn_heads
509
- * head_dim # hidden_size / n_heads
510
- * seq_len # grows with generation
511
- * batch_size
512
- * 2 # float16 = 2 bytes
513
  )
514
- ```
515
 
516
- For Mistral-7B (32 layers, 8 GQA heads, head_dim=128):
517
- ```
518
- 2 * 32 * 8 * 128 * 4096 * 8 * 2 = 32 GB at seq=4096, batch=8
519
  ```
520
 
521
- This exceeds a T4's 16GB — which is exactly the cliff shown in the chart above.
522
- """)
523
-
524
- return demo
525
-
526
 
527
- if __name__ == "__main__":
528
- demo = build_ui()
529
- demo.launch()
 
1
  """
2
+ LLM Inference Optimizer — Professional Demo
 
 
 
 
 
 
 
3
  Author: Aravind Kumar Nalukurthi
 
4
  """
5
 
6
  import gradio as gr
 
 
 
7
  import plotly.graph_objects as go
 
 
 
8
  import os
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  CSS = """
11
+ * { box-sizing: border-box; }
12
+ body, .gradio-container {
13
+ background: #000 !important;
14
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', sans-serif !important;
15
+ color: #f5f5f7 !important;
16
  }
17
+ .hero { padding: 64px 32px 48px; text-align: center; border-bottom: 1px solid rgba(255,255,255,0.07); }
18
+ .hero-badge { display: inline-block; background: rgba(10,132,255,0.12); color: #0a84ff; font-size: 11px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; padding: 5px 14px; border-radius: 20px; border: 1px solid rgba(10,132,255,0.2); margin-bottom: 22px; }
19
+ .hero-title { font-size: 48px; font-weight: 700; color: #f5f5f7; line-height: 1.06; letter-spacing: -0.025em; margin: 0 0 18px; }
20
+ .hero-sub { font-size: 19px; color: #86868b; max-width: 600px; margin: 0 auto; line-height: 1.55; }
21
+ .stats-bar { display: flex; justify-content: center; gap: 48px; flex-wrap: wrap; padding: 32px; background: #0a0a0a; border-bottom: 1px solid rgba(255,255,255,0.07); }
22
+ .stat { text-align: center; }
23
+ .stat-val { font-size: 30px; font-weight: 700; color: #0a84ff; letter-spacing: -0.02em; }
24
+ .stat-label { font-size: 12px; color: #6e6e73; margin-top: 3px; font-weight: 500; letter-spacing: 0.03em; }
25
+ .section { padding: 36px 32px; border-bottom: 1px solid rgba(255,255,255,0.06); }
26
+ .sec-label { font-size: 12px; font-weight: 600; color: #6e6e73; letter-spacing: 0.09em; text-transform: uppercase; margin: 0 0 18px; }
27
+ .card { background: #111; border: 1px solid rgba(255,255,255,0.08); border-radius: 14px; padding: 22px 24px; margin-bottom: 10px; }
28
+ .card-title { font-size: 16px; font-weight: 600; color: #f5f5f7; margin: 0 0 6px; }
29
+ .card-body { font-size: 14px; color: #86868b; line-height: 1.6; margin: 0; }
30
+ .metrics { display: flex; gap: 10px; flex-wrap: wrap; margin: 20px 0; }
31
+ .metric { flex: 1; min-width: 110px; background: #111; border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; padding: 16px; text-align: center; }
32
+ .metric-val { font-size: 24px; font-weight: 700; color: #f5f5f7; letter-spacing: -0.02em; }
33
+ .metric-label { font-size: 12px; color: #6e6e73; margin-top: 4px; }
34
+ .blue { color: #0a84ff; } .green { color: #30d158; } .yellow { color: #ffd60a; }
35
  footer { display: none !important; }
36
  """
37
 
38
+ def throughput_chart():
39
+ fig = go.Figure([go.Bar(
40
+ x=["Sequential", "Static Batching", "Continuous Batching"],
41
+ y=[61, 244, 463],
42
+ marker_color=["#3a3a3c", "#3a3a3c", "#0a84ff"],
43
+ text=["61 tok/s", "244 tok/s", "463 tok/s"],
 
 
 
 
 
 
 
 
 
 
 
44
  textposition="outside",
45
+ textfont=dict(color="#f5f5f7", size=13),
46
+ width=0.45,
47
+ )])
 
 
 
 
 
 
 
 
 
 
 
48
  fig.update_layout(
49
+ template="plotly_dark", paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
50
+ font=dict(color="#86868b", family="-apple-system,sans-serif"),
51
+ yaxis=dict(title="Tokens / Second", gridcolor="rgba(255,255,255,0.05)", range=[0, 560]),
52
+ height=340, margin=dict(t=20, b=20, l=40, r=20), showlegend=False,
 
 
 
53
  )
54
  return fig
55
 
56
+ def quant_chart():
57
+ labels = ["FP32", "FP16", "INT8", "INT4/NF4"]
58
+ fig = go.Figure()
59
+ fig.add_trace(go.Bar(name="Memory (GB)", x=labels, y=[28, 14, 7, 3.5],
60
+ marker_color=["#48484a","#48484a","#48484a","#0a84ff"],
61
+ text=["28GB","14GB","7GB","3.5GB"], textposition="outside",
62
+ textfont=dict(color="#f5f5f7")))
63
+ fig.add_trace(go.Scatter(name="Speedup", x=labels, y=[1.0, 1.2, 1.5, 2.22],
64
+ mode="lines+markers", yaxis="y2",
65
+ line=dict(color="#ffd60a", width=2), marker=dict(size=8, color="#ffd60a")))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  fig.update_layout(
67
+ template="plotly_dark", paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
68
+ font=dict(color="#86868b"),
69
+ yaxis=dict(title="Memory (GB)", gridcolor="rgba(255,255,255,0.05)"),
70
+ yaxis2=dict(title="Speedup", overlaying="y", side="right"),
71
+ height=340, legend=dict(x=0.02, y=0.98), margin=dict(t=20, b=20),
 
72
  )
73
  return fig
74
 
75
+ def kv_chart():
76
+ seq = [128, 256, 512, 1024, 2048, 4096]
77
+ gb = [s * 2 * 32 * 16 * 64 * 2 / (1024**3) for s in seq]
78
+ fig = go.Figure([go.Scatter(
79
+ x=seq, y=gb, mode="lines+markers",
80
+ line=dict(color="#0a84ff", width=2),
81
+ marker=dict(size=7), fill="tozeroy",
82
+ fillcolor="rgba(10,132,255,0.07)",
83
+ )])
84
+ fig.add_hline(y=16, line_dash="dash", line_color="#ff453a",
85
+ annotation_text="16 GB GPU limit", annotation_font_color="#ff453a")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  fig.update_layout(
87
+ template="plotly_dark", paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
88
+ font=dict(color="#86868b"),
89
+ xaxis_title="Sequence Length (tokens)", yaxis_title="KV Cache Size (GB)",
90
+ height=320, yaxis=dict(gridcolor="rgba(255,255,255,0.05)"),
91
+ margin=dict(t=20, b=20),
 
 
 
 
 
92
  )
93
  return fig
94
 
95
 
96
+ with gr.Blocks(css=CSS, theme=gr.themes.Base(), title="LLM Inference Optimizer") as demo:
97
+
98
+ gr.HTML("""
99
+ <div class="hero">
100
+ <div class="hero-badge">AI Engineering · Inference Systems</div>
101
+ <h1 class="hero-title">LLM Inference Optimizer</h1>
102
+ <p class="hero-sub">
103
+ Language models generate one word at a time which is slow and expensive at scale.
104
+ This project implements and benchmarks the three techniques engineers use to serve
105
+ them faster: smarter scheduling, weight compression, and memory management.
106
+ </p>
107
+ </div>
108
+ <div class="stats-bar">
109
+ <div class="stat"><div class="stat-val">7.5×</div><div class="stat-label">Throughput gain</div></div>
110
+ <div class="stat"><div class="stat-val">75%</div><div class="stat-label">Memory reduction</div></div>
111
+ <div class="stat"><div class="stat-val">463</div><div class="stat-label">Tokens / second</div></div>
112
+ <div class="stat"><div class="stat-val">0</div><div class="stat-label">API keys required</div></div>
113
+ </div>
114
+ """)
115
+
116
+ with gr.Tabs():
117
+
118
+ with gr.Tab("Overview"):
119
+ gr.HTML("""
120
+ <div class="section">
121
+ <div class="sec-label">The Problem</div>
122
+ <div class="card">
123
+ <div class="card-title">Why LLMs are slow</div>
124
+ <p class="card-body">When you send a message to ChatGPT, the model generates each word one at a time. Every single word requires a full computation pass through billions of parameters. Doing this naively — one user at a time, sequentially — wastes most of the GPU's capacity.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  </div>
 
126
 
127
+ <div class="sec-label" style="margin-top:28px">The Three Solutions</div>
128
 
129
+ <div class="card">
130
+ <div class="card-title">1 &nbsp;·&nbsp; Continuous Batching &nbsp;<span style="color:#0a84ff">7.5× faster</span></div>
131
+ <p class="card-body">Instead of waiting for one user's response to finish before helping the next user, fill the GPU's processing slots the moment any slot opens up. This keeps GPU utilization near 100% instead of ~30%. Used in vLLM and HuggingFace TGI.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  </div>
 
133
 
134
+ <div class="card">
135
+ <div class="card-title">2 &nbsp;·&nbsp; Quantization &nbsp;<span style="color:#0a84ff">2.2× faster, 75% less memory</span></div>
136
+ <p class="card-body">Neural network weights are normally stored as 32-bit decimal numbers. Compressing them to 4-bit integers saves 75% of memory and speeds up computation — with less than 1% quality loss when done correctly (NF4 format).</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  </div>
 
138
 
139
+ <div class="card">
140
+ <div class="card-title">3 &nbsp;·&nbsp; PagedAttention &nbsp;<span style="color:#0a84ff">65% less memory waste</span></div>
141
+ <p class="card-body">As a model generates text, it needs to remember everything it wrote (called the KV cache). Naively reserving maximum memory for every conversation wastes 65% of GPU memory on average. PagedAttention uses a virtual-memory approach — only allocating what's actually needed.</p>
142
+ </div>
143
 
144
+ <div class="card" style="border-color:rgba(10,132,255,0.25);margin-top:20px">
145
+ <div class="card-title" style="color:#0a84ff">How to use this demo</div>
146
+ <p class="card-body">All benchmarks are pre-computed — no API key or GPU needed. Use the tabs above to explore each technique: throughput charts, memory/speed tradeoffs, and the actual Python implementation.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  </div>
148
+ </div>
149
+ """)
150
+
151
+ with gr.Tab("Batching Benchmark"):
152
+ gr.HTML('<div class="section" style="padding-bottom:0"><div class="sec-label">Throughput same model, same GPU, different scheduling</div></div>')
153
+ gr.Plot(throughput_chart())
154
+ gr.HTML("""
155
+ <div class="section">
156
+ <div class="metrics">
157
+ <div class="metric"><div class="metric-val">61</div><div class="metric-label">Sequential (tok/s)</div></div>
158
+ <div class="metric"><div class="metric-val">244</div><div class="metric-label">Static Batch (tok/s)</div></div>
159
+ <div class="metric"><div class="metric-val green">463</div><div class="metric-label">Continuous (tok/s)</div></div>
160
+ <div class="metric"><div class="metric-val blue">7.5×</div><div class="metric-label">Total speedup</div></div>
161
  </div>
162
+ <div class="card">
163
+ <div class="card-title">The key insight</div>
164
+ <p class="card-body">Static batching waits for the slowest request in a batch to finish before starting the next batch — wasting GPU cycles on idle slots. Continuous batching fills those slots immediately, treating the GPU like a conveyor belt instead of a bucket.</p>
 
 
 
 
 
 
 
 
 
 
 
165
  </div>
166
+ </div>
167
+ """)
168
+
169
+ with gr.Tab("Quantization"):
170
+ gr.HTML('<div class="section" style="padding-bottom:0"><div class="sec-label">Memory vs speed — compressing model weights</div></div>')
171
+ gr.Plot(quant_chart())
172
+ gr.HTML("""
173
+ <div class="section">
174
+ <div class="metrics">
175
+ <div class="metric"><div class="metric-val blue">75%</div><div class="metric-label">Memory saved (→INT4)</div></div>
176
+ <div class="metric"><div class="metric-val green">2.22×</div><div class="metric-label">Speed increase</div></div>
177
+ <div class="metric"><div class="metric-val yellow">&lt;1%</div><div class="metric-label">Quality loss (NF4)</div></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  </div>
179
+ <div class="card">
180
+ <div class="card-title">Why INT4 works without destroying quality</div>
181
+ <p class="card-body">Standard quantization divides the numeric range into equal buckets. NF4 (Normal Float 4) places buckets where most model weights actually cluster — near zero, following a bell curve. This matches how LLM weights are distributed, preserving precision where it matters most.</p>
182
+ </div>
183
+ </div>
184
+ """)
185
+
186
+ with gr.Tab("KV Cache"):
187
+ gr.HTML('<div class="section" style="padding-bottom:0"><div class="sec-label">Memory growth — longer conversations cost exponentially more</div></div>')
188
+ gr.Plot(kv_chart())
189
+ gr.HTML("""
190
+ <div class="section">
191
+ <div class="card">
192
+ <div class="card-title">The formula</div>
193
+ <p class="card-body" style="font-family:monospace;color:#f5f5f7;font-size:13px">Memory = 2 × n_layers × n_heads × head_dim × seq_len × batch × bytes</p>
194
+ </div>
195
+ <div class="card">
196
+ <div class="card-title">PagedAttention — the fix</div>
197
+ <p class="card-body">Pre-allocating the maximum sequence length for every conversation wastes 65% of GPU memory on unused space. PagedAttention stores the KV cache in fixed 16-token pages and only allocates new pages as they're needed like how your OS manages RAM, not how a naive array works.</p>
198
+ </div>
199
+ </div>
200
+ """)
201
 
202
+ with gr.Tab("Implementation"):
203
+ gr.Markdown("""
204
+ ## Continuous Batching
205
 
206
  ```python
207
+ def process_requests(self, requests, max_batch_size=8):
208
+ active, completed, queue = [], [], list(requests)
 
 
209
 
210
+ while queue or active:
211
+ # Fill empty slots the instant they open
212
+ while len(active) < max_batch_size and queue:
213
+ active.append(queue.pop(0))
214
 
215
+ # One forward pass — processes all active requests simultaneously
216
+ results = self._forward_batch(active)
 
217
 
218
+ # Remove finished requests; new ones fill slots next iteration
219
+ active = [r for r, done in zip(active, results) if not done]
220
+ completed += [r for r, done in zip(active, results) if done]
 
 
 
221
 
222
+ return completed
223
  ```
224
 
225
+ ## Quantization (QLoRA / bitsandbytes)
 
 
 
 
 
 
 
 
 
 
 
226
 
 
227
  ```python
228
+ from transformers import BitsAndBytesConfig
 
 
 
229
 
230
+ config = BitsAndBytesConfig(
231
+ load_in_4bit=True,
232
+ bnb_4bit_quant_type="nf4", # quantile-spaced bins
233
+ bnb_4bit_compute_dtype=torch.float16,
234
+ bnb_4bit_use_double_quant=True, # quantize the quantization constants
 
 
 
 
 
 
235
  )
 
236
 
237
+ # 7B model fits in 4GB GPU memory instead of 28GB
238
+ model = AutoModelForCausalLM.from_pretrained("model_id", quantization_config=config)
 
239
  ```
240
 
241
+ ## References
242
+ - **vLLM** — PagedAttention ([arxiv 2309.06180](https://arxiv.org/abs/2309.06180))
243
+ - **QLoRA** — NF4 quantization ([arxiv 2305.14314](https://arxiv.org/abs/2305.14314))
244
+ """)
 
245
 
246
+ demo.launch()