Arush kumar commited on
Commit
59e8366
·
1 Parent(s): e67645a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +331 -129
app.py CHANGED
@@ -3,11 +3,17 @@ from __future__ import annotations
3
  import os
4
  os.environ["KERAS_BACKEND"] = "jax"
5
 
 
 
 
 
6
  import numpy as np
7
  import jax
 
8
  import keras
9
  import gradio as gr
10
- from pathlib import Path
 
11
 
12
  from veylon_model import create_llm
13
  from tokenizer import TokenizerWrapper
@@ -24,7 +30,25 @@ from config import (
24
  )
25
 
26
  # ============================================================
27
- # Initialize (runs once)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  # ============================================================
29
 
30
  keras.mixed_precision.set_global_policy("mixed_bfloat16")
@@ -32,14 +56,12 @@ keras.mixed_precision.set_global_policy("mixed_bfloat16")
32
  print(f"Backend: {keras.backend.backend()}")
33
  print(f"JAX devices: {jax.devices()}")
34
 
35
- # Load tokenizer
36
  tokenizer = TokenizerWrapper("tokenizer.model")
37
  assert tokenizer.vocab_size == vocab_size, (
38
  f"Tokenizer vocab ({tokenizer.vocab_size}) != config vocab ({vocab_size})"
39
  )
40
- print(f"✓ Tokenizer loaded: {tokenizer.vocab_size} vocab")
41
 
42
- # Build model
43
  print("Building model...")
44
  model = create_llm(
45
  vocab_size=vocab_size,
@@ -54,21 +76,59 @@ model = create_llm(
54
  swa_window=swa_window,
55
  )
56
 
57
- # Warmup
58
  dummy = np.zeros((1, CONTEXT), dtype=np.int32)
59
  _ = model(dummy, training=False)
60
- print("✓ Model built successfully")
61
 
62
- # Load weights
63
- WEIGHTS_PATH = "veylon_final.weights.h5"
64
  if Path(WEIGHTS_PATH).exists():
65
  print(f"Loading weights from: {WEIGHTS_PATH}")
66
  model.load_weights(WEIGHTS_PATH)
67
- print("✓ Weights loaded successfully")
68
  else:
69
  print(f"WARNING: {WEIGHTS_PATH} not found. Using untrained model.")
70
 
71
- print(f"✓ Model params: {model.count_params():,}\n")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
  # ============================================================
74
  # Sampling
@@ -100,48 +160,71 @@ def sample_from_logits(
100
 
101
  return int(np.random.choice(len(probs), p=probs))
102
 
 
103
  # ============================================================
104
- # Generation function
105
  # ============================================================
106
 
107
- def generate(
108
- prompt: str,
109
- max_new_tokens: int = 64,
110
- temperature: float = 0.8,
111
- top_k: int = 50,
112
- ) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  """
114
- Generate text from a prompt using Veylon.
115
-
116
- Args:
117
- prompt: Input text
118
- max_new_tokens: Maximum tokens to generate
119
- temperature: Sampling temperature (0.1-2.0)
120
- top_k: Top-K sampling cutoff
121
-
122
- Returns:
123
- Generated text
124
  """
125
  try:
126
- # Encode prompt
127
- tokens = tokenizer.encode(
128
- prompt,
129
- add_bos=True,
130
- add_eos=False,
131
- )
132
 
 
133
  if len(tokens) == 0:
134
  tokens = [tokenizer.bos_id if hasattr(tokenizer, "bos_id") else 1]
135
-
136
  tokens = tokens[-CONTEXT:]
137
 
138
- # Prefill phase
139
  prompt_ids = np.array([tokens], dtype=np.int32)
140
- logits, cache_k, cache_v = model.generate_step(
141
- prompt_ids,
142
- cache_k=None,
143
- cache_v=None,
144
- cache_pos=0,
145
  )
146
 
147
  next_token = sample_from_logits(
@@ -149,22 +232,20 @@ def generate(
149
  temperature=temperature,
150
  top_k=top_k,
151
  )
152
- tokens.append(next_token)
153
 
154
- # Decoding phase (token-by-token)
155
- if next_token != tokenizer.eos_id and len(tokens) < CONTEXT:
156
- cache_pos = len(prompt_ids[0])
 
 
 
157
 
158
- for _ in range(max_new_tokens - 1):
159
  next_input = np.array([[next_token]], dtype=np.int32)
160
 
161
- logits, cache_k, cache_v = model.generate_step(
162
- next_input,
163
- cache_k=cache_k,
164
- cache_v=cache_v,
165
- cache_pos=cache_pos,
166
  )
167
-
168
  cache_pos += 1
169
 
170
  next_token = sample_from_logits(
@@ -172,100 +253,221 @@ def generate(
172
  temperature=temperature,
173
  top_k=top_k,
174
  )
175
- tokens.append(next_token)
176
 
177
  if next_token == tokenizer.eos_id:
178
  break
179
 
180
- if len(tokens) >= CONTEXT:
 
 
 
 
181
  break
182
 
183
- # Decode output
184
- generated_text = tokenizer.decode(tokens)
185
- return generated_text
 
186
 
187
  except Exception as e:
188
- return f"Error: {str(e)}"
 
189
 
190
  # ============================================================
191
- # Gradio UI
192
  # ============================================================
193
 
194
- def main():
195
- with gr.Blocks(title="Veylon Alpha") as demo:
196
- gr.Markdown("""
197
- # 🚀 Veylon Alpha Preview - 10M LLM
198
- # Made by Arush Kumar
199
- A student dev!
200
- A small transformer model trained on clean data.
201
- Enter a prompt and watch it generate text.
202
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  """)
204
 
205
  with gr.Row():
206
- with gr.Column(scale=2):
207
- prompt = gr.Textbox(
208
- label="Prompt",
209
- placeholder="Once upon a time",
210
- lines=3,
211
- value="Once upon a time"
212
- )
213
-
214
- with gr.Row():
215
- max_tokens = gr.Slider(
216
- label="Max tokens",
217
- minimum=64,
218
- maximum=768,
219
- value=128,
220
- step=10,
221
- )
222
- temperature = gr.Slider(
223
- label="Temperature",
224
- minimum=0.1,
225
- maximum=2.0,
226
- value=0.8,
227
- step=0.1,
228
- )
229
- top_k = gr.Slider(
230
- label="Top-K",
231
- minimum=1,
232
- maximum=100,
233
- value=50,
234
- step=1,
235
- )
236
-
237
- generate_btn = gr.Button("Generate", variant="primary", size="lg")
238
-
239
- with gr.Column(scale=1):
240
- info = gr.Markdown(f"""
241
- **Model Info**
242
-
243
- - Parameters: {model.count_params():,}
244
- - Context: {CONTEXT} tokens
245
- - Vocab: {vocab_size}
246
- - Architecture: Transformer + GQA
247
-
248
- **Tips**
249
- - Higher temp = more creative
250
- - Lower temp = more deterministic
251
- - Top-K = diversity control
252
- """)
253
-
254
- output = gr.Textbox(
255
- label="Generated Output",
256
- lines=8,
257
- interactive=False
258
  )
259
 
260
- # Connect
261
- generate_btn.click(
262
- fn=generate,
263
- inputs=[prompt, max_tokens, temperature, top_k],
264
- outputs=output,
265
- api_name="generate"
266
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
268
- demo.launch(share=False, server_name="0.0.0.0", server_port=7860)
269
 
270
  if __name__ == "__main__":
271
- main()
 
 
 
 
3
  import os
4
  os.environ["KERAS_BACKEND"] = "jax"
5
 
6
+ import time
7
+ from pathlib import Path
8
+ from functools import partial
9
+
10
  import numpy as np
11
  import jax
12
+ import jax.numpy as jnp
13
  import keras
14
  import gradio as gr
15
+ from fastapi import FastAPI
16
+ import uvicorn
17
 
18
  from veylon_model import create_llm
19
  from tokenizer import TokenizerWrapper
 
30
  )
31
 
32
  # ============================================================
33
+ # Config
34
+ # ============================================================
35
+
36
+ WEIGHTS_PATH = "veylon_final.weights.h5"
37
+ MODEL_NAME = "Arya"
38
+ MODEL_TAGLINE = "27M-param transformer, made by Arush Kumar"
39
+
40
+ # Chat templating for a BASE (non-instruction-tuned) language model. If
41
+ # Arya/Veylon was trained on plain prose (TinyStories/WikiText/etc.) rather
42
+ # than dialogue-formatted data, this template is best-effort — multi-turn
43
+ # coherence depends entirely on what the training corpus looked like, not
44
+ # on anything this app can fix. Swap this format if you fine-tune on a
45
+ # specific chat template later.
46
+ USER_TAG = "User:"
47
+ ASSISTANT_TAG = "Assistant:"
48
+
49
+
50
+ # ============================================================
51
+ # Initialize (runs once, at import time)
52
  # ============================================================
53
 
54
  keras.mixed_precision.set_global_policy("mixed_bfloat16")
 
56
  print(f"Backend: {keras.backend.backend()}")
57
  print(f"JAX devices: {jax.devices()}")
58
 
 
59
  tokenizer = TokenizerWrapper("tokenizer.model")
60
  assert tokenizer.vocab_size == vocab_size, (
61
  f"Tokenizer vocab ({tokenizer.vocab_size}) != config vocab ({vocab_size})"
62
  )
63
+ print(f"Tokenizer loaded: {tokenizer.vocab_size} vocab")
64
 
 
65
  print("Building model...")
66
  model = create_llm(
67
  vocab_size=vocab_size,
 
76
  swa_window=swa_window,
77
  )
78
 
 
79
  dummy = np.zeros((1, CONTEXT), dtype=np.int32)
80
  _ = model(dummy, training=False)
81
+ print("Model built successfully")
82
 
 
 
83
  if Path(WEIGHTS_PATH).exists():
84
  print(f"Loading weights from: {WEIGHTS_PATH}")
85
  model.load_weights(WEIGHTS_PATH)
86
+ print("Weights loaded successfully")
87
  else:
88
  print(f"WARNING: {WEIGHTS_PATH} not found. Using untrained model.")
89
 
90
+ print(f"Model params: {model.count_params():,}\n")
91
+
92
+
93
+ # ============================================================
94
+ # JIT-compiled generation steps — the real speed lever here.
95
+ # ============================================================
96
+ # The original app called model.generate_step() as plain, untraced Python
97
+ # on every single token — meaning JAX re-dispatches and re-optimizes the
98
+ # whole graph every call instead of running a compiled executable. This is
99
+ # the single biggest speed cost in the original code, and (unlike swapping
100
+ # inference engines) is something we can fix directly: jax.jit traces the
101
+ # step function once per unique input shape/dtype, caches the compiled
102
+ # XLA executable, and every subsequent call re-uses it. Since prefill
103
+ # always sees shape (1, CONTEXT) and decode always sees shape (1, 1),
104
+ # there are exactly two shapes to compile — both get cached on first use,
105
+ # after which every call is a compiled dispatch instead of a Python/XLA
106
+ # re-trace.
107
+ #
108
+ # NOTE: this assumes model.generate_step is a pure function of its
109
+ # arguments (no hidden Python-side mutation of the cache/model state
110
+ # outside of what it returns). That's true for essentially all KV-cache
111
+ # transformer implementations, but if your model.generate_step does
112
+ # something stateful and doesn't work under jit, set JIT_GENERATE=False
113
+ # below and it'll fall back to the original eager behavior with zero
114
+ # other changes.
115
+
116
+ JIT_GENERATE = True
117
+
118
+ if JIT_GENERATE:
119
+ _generate_step_jit = jax.jit(model.generate_step)
120
+ else:
121
+ _generate_step_jit = model.generate_step
122
+
123
+
124
+ def _run_generate_step(tokens_arr, cache_k, cache_v, cache_pos):
125
+ return _generate_step_jit(
126
+ tokens_arr,
127
+ cache_k=cache_k,
128
+ cache_v=cache_v,
129
+ cache_pos=cache_pos,
130
+ )
131
+
132
 
133
  # ============================================================
134
  # Sampling
 
160
 
161
  return int(np.random.choice(len(probs), p=probs))
162
 
163
+
164
  # ============================================================
165
+ # Prompt construction for chat-style multi-turn context
166
  # ============================================================
167
 
168
+ def build_prompt(message: str, history: list[dict]) -> str:
169
+ """
170
+ Turns Gradio chat history (list of {"role", "content"} dicts, the
171
+ 'messages' format) into a single text block for a base LM, truncated
172
+ to fit CONTEXT. Oldest turns are dropped first if it doesn't fit —
173
+ we build from the end backwards.
174
+ """
175
+ turns = []
176
+ for turn in history:
177
+ role = turn.get("role")
178
+ content = turn.get("content", "")
179
+ tag = USER_TAG if role == "user" else ASSISTANT_TAG
180
+ turns.append(f"{tag} {content}")
181
+ turns.append(f"{USER_TAG} {message}")
182
+ turns.append(ASSISTANT_TAG)
183
+
184
+ # Build from the most recent turn backwards until we run out of
185
+ # context budget, so we never silently truncate the *current* message.
186
+ budget_chars = CONTEXT * 4 # rough chars-per-token heuristic pre-tokenize
187
+ kept = []
188
+ running_len = 0
189
+ for t in reversed(turns):
190
+ running_len += len(t) + 1
191
+ if running_len > budget_chars and kept:
192
+ break
193
+ kept.append(t)
194
+ kept.reverse()
195
+ return "\n".join(kept)
196
+
197
+
198
+ # ============================================================
199
+ # Generation (streaming generator for gr.ChatInterface)
200
+ # ============================================================
201
+
202
+ def generate_stream(
203
+ message: str,
204
+ history: list[dict],
205
+ max_new_tokens: int,
206
+ temperature: float,
207
+ top_k: int,
208
+ ):
209
  """
210
+ Streaming generator: yields the partial assistant response as tokens
211
+ are produced, so the chat UI fills in live instead of blocking on the
212
+ whole generation.
 
 
 
 
 
 
 
213
  """
214
  try:
215
+ prompt = build_prompt(message, history)
 
 
 
 
 
216
 
217
+ tokens = tokenizer.encode(prompt, add_bos=True, add_eos=False)
218
  if len(tokens) == 0:
219
  tokens = [tokenizer.bos_id if hasattr(tokenizer, "bos_id") else 1]
 
220
  tokens = tokens[-CONTEXT:]
221
 
222
+ prompt_len = len(tokens)
223
  prompt_ids = np.array([tokens], dtype=np.int32)
224
+
225
+ t0 = time.time()
226
+ logits, cache_k, cache_v = _run_generate_step(
227
+ prompt_ids, cache_k=None, cache_v=None, cache_pos=0,
 
228
  )
229
 
230
  next_token = sample_from_logits(
 
232
  temperature=temperature,
233
  top_k=top_k,
234
  )
 
235
 
236
+ generated_ids = [next_token]
237
+ partial_text = tokenizer.decode(generated_ids)
238
+ yield partial_text
239
+
240
+ if next_token != tokenizer.eos_id and prompt_len < CONTEXT:
241
+ cache_pos = prompt_len
242
 
243
+ for _ in range(int(max_new_tokens) - 1):
244
  next_input = np.array([[next_token]], dtype=np.int32)
245
 
246
+ logits, cache_k, cache_v = _run_generate_step(
247
+ next_input, cache_k=cache_k, cache_v=cache_v, cache_pos=cache_pos,
 
 
 
248
  )
 
249
  cache_pos += 1
250
 
251
  next_token = sample_from_logits(
 
253
  temperature=temperature,
254
  top_k=top_k,
255
  )
 
256
 
257
  if next_token == tokenizer.eos_id:
258
  break
259
 
260
+ generated_ids.append(next_token)
261
+ partial_text = tokenizer.decode(generated_ids)
262
+ yield partial_text
263
+
264
+ if prompt_len + len(generated_ids) >= CONTEXT:
265
  break
266
 
267
+ elapsed = time.time() - t0
268
+ n_tok = len(generated_ids)
269
+ tok_per_sec = n_tok / elapsed if elapsed > 0 else 0.0
270
+ print(f"Generated {n_tok} tokens in {elapsed:.2f}s ({tok_per_sec:.1f} tok/s)")
271
 
272
  except Exception as e:
273
+ yield f"⚠ Error: {str(e)}"
274
+
275
 
276
  # ============================================================
277
+ # Styling — dark terminal / glassmorphism, JetBrains Mono, neon accent
278
  # ============================================================
279
 
280
+ CUSTOM_CSS = """
281
+ @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap');
282
+
283
+ :root {
284
+ --veylon-bg: #0a0e14;
285
+ --veylon-panel: rgba(20, 26, 38, 0.65);
286
+ --veylon-border: rgba(0, 255, 200, 0.18);
287
+ --veylon-accent: #00ffc8;
288
+ --veylon-accent-dim: #00b894;
289
+ --veylon-text: #e6f1ff;
290
+ --veylon-text-dim: #7d8ba1;
291
+ }
292
+
293
+ * {
294
+ font-family: 'JetBrains Mono', ui-monospace, monospace !important;
295
+ }
296
+
297
+ body, .gradio-container {
298
+ background: radial-gradient(circle at 20% 10%, #101826 0%, #0a0e14 60%) !important;
299
+ color: var(--veylon-text) !important;
300
+ }
301
+
302
+ #veylon-header {
303
+ text-align: center;
304
+ padding: 18px 12px 6px 12px;
305
+ }
306
+
307
+ #veylon-header h1 {
308
+ font-size: 1.8rem;
309
+ font-weight: 700;
310
+ letter-spacing: 0.5px;
311
+ background: linear-gradient(90deg, var(--veylon-accent), #4dd0ff);
312
+ -webkit-background-clip: text;
313
+ -webkit-text-fill-color: transparent;
314
+ margin-bottom: 2px;
315
+ }
316
+
317
+ #veylon-header p {
318
+ color: var(--veylon-text-dim);
319
+ font-size: 0.9rem;
320
+ margin: 0;
321
+ }
322
+
323
+ .gr-panel, .block, .form {
324
+ background: var(--veylon-panel) !important;
325
+ border: 1px solid var(--veylon-border) !important;
326
+ border-radius: 14px !important;
327
+ backdrop-filter: blur(10px);
328
+ }
329
+
330
+ #chatbot {
331
+ border-radius: 16px !important;
332
+ border: 1px solid var(--veylon-border) !important;
333
+ background: var(--veylon-panel) !important;
334
+ box-shadow: 0 0 24px rgba(0, 255, 200, 0.05);
335
+ }
336
+
337
+ .message.user {
338
+ background: rgba(0, 255, 200, 0.08) !important;
339
+ border: 1px solid var(--veylon-border) !important;
340
+ }
341
+
342
+ .message.bot {
343
+ background: rgba(255, 255, 255, 0.03) !important;
344
+ border: 1px solid rgba(255, 255, 255, 0.06) !important;
345
+ }
346
+
347
+ button.primary, .gr-button-primary {
348
+ background: linear-gradient(90deg, var(--veylon-accent-dim), var(--veylon-accent)) !important;
349
+ color: #04120e !important;
350
+ border: none !important;
351
+ font-weight: 700 !important;
352
+ box-shadow: 0 0 16px rgba(0, 255, 200, 0.25);
353
+ transition: transform 0.12s ease, box-shadow 0.12s ease;
354
+ }
355
+
356
+ button.primary:hover, .gr-button-primary:hover {
357
+ transform: translateY(-1px);
358
+ box-shadow: 0 0 24px rgba(0, 255, 200, 0.4);
359
+ }
360
+
361
+ input, textarea, .gr-box {
362
+ background: rgba(255, 255, 255, 0.03) !important;
363
+ color: var(--veylon-text) !important;
364
+ border: 1px solid var(--veylon-border) !important;
365
+ border-radius: 10px !important;
366
+ }
367
+
368
+ label span {
369
+ color: var(--veylon-text-dim) !important;
370
+ font-size: 0.78rem !important;
371
+ text-transform: uppercase;
372
+ letter-spacing: 0.06em;
373
+ }
374
+
375
+ #stats-badge {
376
+ text-align: center;
377
+ color: var(--veylon-text-dim);
378
+ font-size: 0.78rem;
379
+ padding-top: 4px;
380
+ }
381
+
382
+ footer { display: none !important; }
383
+ """
384
+
385
+ THEME = gr.themes.Base(
386
+ primary_hue="teal",
387
+ neutral_hue="slate",
388
+ font=[gr.themes.GoogleFont("JetBrains Mono"), "ui-monospace", "monospace"],
389
+ ).set(
390
+ body_background_fill="*neutral_950",
391
+ block_background_fill="*neutral_900",
392
+ border_color_primary="*primary_700",
393
+ )
394
+
395
+
396
+ # ============================================================
397
+ # Build the Gradio Blocks app
398
+ # ============================================================
399
+
400
+ def build_demo() -> gr.Blocks:
401
+ with gr.Blocks(title=MODEL_NAME, theme=THEME, css=CUSTOM_CSS) as demo:
402
+ gr.HTML(f"""
403
+ <div id="veylon-header">
404
+ <h1>◆ {MODEL_NAME}</h1>
405
+ <p>{MODEL_TAGLINE} · {model.count_params():,} params · {CONTEXT} ctx</p>
406
+ </div>
407
  """)
408
 
409
  with gr.Row():
410
+ max_tokens = gr.Slider(
411
+ label="Max new tokens", minimum=10, maximum=768, value=64, step=10,
412
+ )
413
+ temperature = gr.Slider(
414
+ label="Temperature", minimum=0.1, maximum=2.0, value=0.8, step=0.1,
415
+ )
416
+ top_k = gr.Slider(
417
+ label="Top-K", minimum=1, maximum=100, value=50, step=1,
418
+ )
419
+
420
+ gr.ChatInterface(
421
+ fn=generate_stream,
422
+ type="messages",
423
+ additional_inputs=[max_tokens, temperature, top_k],
424
+ chatbot=gr.Chatbot(elem_id="chatbot", type="messages", height=480),
425
+ examples=[
426
+ ["Once upon a time,"],
427
+ ["The scientist looked at the data and"],
428
+ ["In a small village, there lived"],
429
+ ],
430
+ title=None,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
431
  )
432
 
433
+ gr.HTML(f"""
434
+ <div id="stats-badge">
435
+ base language model · not instruction-tuned · coherence across
436
+ turns depends on training data format
437
+ </div>
438
+ """)
439
+
440
+ return demo
441
+
442
+
443
+ demo = build_demo()
444
+ demo.queue() # required for streaming generators to work over the wire
445
+
446
+
447
+ # ============================================================
448
+ # FastAPI mount — real production entrypoint instead of demo.launch()
449
+ # ============================================================
450
+
451
+ app = FastAPI(title=f"{MODEL_NAME} API")
452
+
453
+
454
+ @app.get("/health")
455
+ def health():
456
+ return {
457
+ "status": "ok",
458
+ "model": MODEL_NAME,
459
+ "params": model.count_params(),
460
+ "context": CONTEXT,
461
+ "backend": keras.backend.backend(),
462
+ "jax_devices": [str(d) for d in jax.devices()],
463
+ }
464
+
465
+
466
+ app = gr.mount_gradio_app(app, demo, path="/")
467
 
 
468
 
469
  if __name__ == "__main__":
470
+ # Run via uvicorn directly rather than demo.launch(): gives you
471
+ # multi-worker / reload / proper ASGI server config for actual
472
+ # deployment, plus the /health route above for load-balancer checks.
473
+ uvicorn.run(app, host="0.0.0.0", port=7860)