YoussefElsafi commited on
Commit
70a6782
·
verified ·
1 Parent(s): 0ba5084

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +78 -105
app.py CHANGED
@@ -1,13 +1,11 @@
1
  import os
2
  import gradio as gr
3
  import torch
4
- import numpy as np
5
  from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
6
  from threading import Thread
7
  from concurrent.futures import ThreadPoolExecutor
8
  import logging
9
  import time
10
- import queue
11
  import threading
12
 
13
  logging.basicConfig(level=logging.INFO)
@@ -16,21 +14,18 @@ logger = logging.getLogger(__name__)
16
  MODEL_ID = "YoussefElsafi/PlayerAI-1.2B-v1.5"
17
 
18
  # ── CPU Threading setup FIRST (before any torch ops) ─────────────────────────
19
- # On CPU, torch uses OpenMP/MKL threads internally.
20
- # Too many = thrashing. Formula: physical_cores / expected_concurrent_users
21
  PHYSICAL_CORES = os.cpu_count() or 2
22
- MAX_CONCURRENT = 2 # how many generations run truly in parallel
23
  THREADS_PER_GEN = max(1, PHYSICAL_CORES // MAX_CONCURRENT)
24
 
25
  torch.set_num_threads(THREADS_PER_GEN)
26
  torch.set_num_interop_threads(2)
27
 
28
- # Force MKL/OpenBLAS to respect our thread budget too
29
- os.environ["OMP_NUM_THREADS"] = str(THREADS_PER_GEN)
30
- os.environ["MKL_NUM_THREADS"] = str(THREADS_PER_GEN)
31
- os.environ["OPENBLAS_NUM_THREADS"] = str(THREADS_PER_GEN)
32
- os.environ["VECLIB_MAXIMUM_THREADS"]= str(THREADS_PER_GEN)
33
- os.environ["NUMEXPR_NUM_THREADS"] = str(THREADS_PER_GEN)
34
 
35
  logger.info(f"CPU cores: {PHYSICAL_CORES} | "
36
  f"Concurrent gens: {MAX_CONCURRENT} | "
@@ -40,45 +35,52 @@ logger.info(f"CPU cores: {PHYSICAL_CORES} | "
40
  print("Loading model...")
41
  tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)
42
 
43
- # On CPU: bfloat16 is FASTER than float32 on modern x86 (AVX-512 bf16 support)
44
- # Falls back gracefully if not supported
45
- if torch.cuda.is_available():
46
- DTYPE = torch.float16
47
- DEVICE = "cuda"
48
- elif hasattr(torch, 'bfloat16') and torch.backends.cpu.is_avx512_bf16_supported():
49
- DTYPE = torch.bfloat16
50
- DEVICE = "cpu"
51
- logger.info("AVX-512 BF16 detected — using bfloat16")
52
- else:
53
- DTYPE = torch.float32
54
- DEVICE = "cpu"
 
 
 
 
 
 
 
55
 
56
  model = AutoModelForCausalLM.from_pretrained(
57
  MODEL_ID,
58
- torch_dtype = DTYPE,
59
- device_map = "cpu" if DEVICE == "cpu" else "auto",
60
- trust_remote_code = True,
61
- low_cpu_mem_usage = True,
62
  )
63
  model.eval()
64
 
65
- # ── INT8 dynamic quantization (biggest CPU speedup: ~2x faster, ~2x less RAM) ─
66
- # Works on Linear layers — the bulk of transformer compute
67
- try:
68
- model = torch.quantization.quantize_dynamic(
69
- model,
70
- {torch.nn.Linear},
71
- dtype=torch.qint8,
72
- )
73
- logger.info("INT8 dynamic quantization applied ✓")
74
- except Exception as e:
75
- logger.warning(f"Quantization skipped: {e}")
 
76
 
77
- # ── torch.compile (PyTorch 2.0+) ──────────────────────────────────────────────
78
- # "default" mode works better than "reduce-overhead" on CPU
79
  if hasattr(torch, "compile"):
80
  try:
81
- model = torch.compile(model, mode="default", backend="inductor")
82
  logger.info("torch.compile applied ✓")
83
  except Exception as e:
84
  logger.warning(f"torch.compile skipped: {e}")
@@ -86,30 +88,19 @@ if hasattr(torch, "compile"):
86
  print("Model loaded!")
87
 
88
  # ── Generation config ─────────────────────────────────────────────────────────
89
- # Shorter = faster. For chat, 80 tokens is plenty.
90
  GEN_CONFIG = dict(
91
- max_new_tokens = 80, # was 120 — shaves ~33% off latency
92
- do_sample = True,
93
- temperature = 0.85,
94
- top_p = 0.9,
95
- # repetition_penalty speeds up sampling by pruning logits
96
  repetition_penalty = 1.1,
97
- pad_token_id = tokenizer.eos_token_id,
98
- # Use cache — critical for autoregressive speed
99
- use_cache = True,
100
  )
101
 
102
- # ── Thread pool ───────────────────────────────────────────────────────────────
103
- executor = ThreadPoolExecutor(
104
- max_workers = MAX_CONCURRENT,
105
- thread_name_prefix = "gen",
106
- )
107
-
108
- # ── Request queue + priority ───────────────────────────────────────────────────
109
- # Caps in-flight requests so we don't OOM with 16GB RAM
110
- # 1.2B model in int8 ≈ 1.2GB, each active KV cache ≈ 200MB
111
- # So 2 concurrent is safe, queue up to 8 waiting
112
- MAX_QUEUE = 8
113
  gen_semaphore = threading.Semaphore(MAX_CONCURRENT)
114
  queue_counter = threading.Semaphore(MAX_QUEUE)
115
 
@@ -128,27 +119,14 @@ SYSTEM_PROMPT = (
128
  "Use lowercase, slang, and act like a real person chatting."
129
  )
130
 
131
- # ── Prompt cache ───────────────────────────────────────────────────────────────
132
- # System prompt tokenized once and reused — saves ~5ms per request
133
- _SYSTEM_MESSAGES = [{"role": "system", "content": SYSTEM_PROMPT}]
134
- try:
135
- _SYSTEM_PROMPT_PREFIX = tokenizer.apply_chat_template(
136
- _SYSTEM_MESSAGES,
137
- tokenize=False,
138
- add_generation_prompt=False,
139
- )
140
- except Exception:
141
- _SYSTEM_PROMPT_PREFIX = ""
142
-
143
  # ── Core generation ────────────────────────────────────────────────────────────
144
  def _run_generation(prompt: str, streamer: TextIteratorStreamer) -> None:
145
  inputs = tokenizer(
146
  prompt,
147
- return_tensors = "pt",
148
- truncation = True,
149
- max_length = 512, # hard cap — prevents runaway long contexts
150
  )
151
- # No .to(device) needed — model is already on cpu
152
  with torch.inference_mode():
153
  model.generate(
154
  input_ids = inputs.input_ids,
@@ -157,23 +135,19 @@ def _run_generation(prompt: str, streamer: TextIteratorStreamer) -> None:
157
  **GEN_CONFIG,
158
  )
159
 
160
-
161
- # ── Streaming response ────────────────────────────────────────────────────────
162
  def respond(message: str, history: list, api_key: str):
163
  if not validate_key(api_key):
164
  yield "[error] unauthorized"
165
  return
166
 
167
- # Queue guard — reject if backlog is full
168
  if not queue_counter.acquire(blocking=False):
169
  yield "[error] server busy — try again in a moment"
170
  return
171
 
172
  try:
173
- # Build prompt — keep history short to limit context length
174
- # Long contexts = slow prefill on CPU
175
- MAX_HISTORY = 4 # last 4 turns only
176
- trimmed_history = history[-MAX_HISTORY:] if len(history) > MAX_HISTORY else history
177
 
178
  messages = [{"role": "system", "content": SYSTEM_PROMPT}]
179
  messages.extend(trimmed_history)
@@ -182,7 +156,7 @@ def respond(message: str, history: list, api_key: str):
182
  try:
183
  prompt = tokenizer.apply_chat_template(
184
  messages,
185
- tokenize = False,
186
  add_generation_prompt = True,
187
  )
188
  except Exception as e:
@@ -191,12 +165,11 @@ def respond(message: str, history: list, api_key: str):
191
 
192
  streamer = TextIteratorStreamer(
193
  tokenizer,
194
- skip_prompt = True,
195
- skip_special_tokens= True,
196
- timeout = 60.0,
197
  )
198
 
199
- # Acquire generation slot (blocks if MAX_CONCURRENT already running)
200
  gen_semaphore.acquire()
201
  try:
202
  future = executor.submit(_run_generation, prompt, streamer)
@@ -207,7 +180,6 @@ def respond(message: str, history: list, api_key: str):
207
  yield response
208
 
209
  future.result(timeout=10)
210
-
211
  finally:
212
  gen_semaphore.release()
213
 
@@ -215,23 +187,25 @@ def respond(message: str, history: list, api_key: str):
215
  queue_counter.release()
216
 
217
 
218
- # ── Warm-up ────────────────────────────────────────────────────────────────────
219
- # Run one dummy generation at startup so the first real user isn't slow.
220
- # torch.compile needs a first pass to JIT-compile the graph.
221
  def _warmup():
222
  logger.info("Running warmup generation...")
223
  t0 = time.time()
224
  try:
225
  dummy_streamer = TextIteratorStreamer(
226
- tokenizer, skip_prompt=True, skip_special_tokens=True, timeout=120.0
 
 
 
227
  )
228
  dummy_prompt = tokenizer.apply_chat_template(
229
  [{"role": "system", "content": SYSTEM_PROMPT},
230
  {"role": "user", "content": "hi"}],
231
- tokenize=False,
232
- add_generation_prompt=True,
233
  )
234
- inputs = tokenizer(dummy_prompt, return_tensors="pt", truncation=True, max_length=256)
 
235
  warmup_cfg = {**GEN_CONFIG, "max_new_tokens": 20}
236
 
237
  def _gen():
@@ -248,11 +222,10 @@ def _warmup():
248
  for _ in dummy_streamer:
249
  pass
250
  t.join()
251
- logger.info(f"Warmup done in {time.time()-t0:.2f}s ✓")
252
  except Exception as e:
253
  logger.warning(f"Warmup failed (non-fatal): {e}")
254
 
255
- # Run warmup in background so server starts immediately
256
  Thread(target=_warmup, daemon=True).start()
257
 
258
 
@@ -312,12 +285,12 @@ with gr.Blocks(css=custom_css, theme=gr.themes.Base()) as demo:
312
  stored_key = gr.Textbox(value="", visible=False)
313
 
314
  chatbot = gr.Chatbot(
315
- type = "messages",
316
- avatar_images = (USER_AVATAR, BOT_AVATAR),
317
- height = 500,
318
- show_label = False,
319
- bubble_full_width= False,
320
- show_copy_button = False,
321
  )
322
 
323
  gr.ChatInterface(
 
1
  import os
2
  import gradio as gr
3
  import torch
 
4
  from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
5
  from threading import Thread
6
  from concurrent.futures import ThreadPoolExecutor
7
  import logging
8
  import time
 
9
  import threading
10
 
11
  logging.basicConfig(level=logging.INFO)
 
14
  MODEL_ID = "YoussefElsafi/PlayerAI-1.2B-v1.5"
15
 
16
  # ── CPU Threading setup FIRST (before any torch ops) ─────────────────────────
 
 
17
  PHYSICAL_CORES = os.cpu_count() or 2
18
+ MAX_CONCURRENT = 2
19
  THREADS_PER_GEN = max(1, PHYSICAL_CORES // MAX_CONCURRENT)
20
 
21
  torch.set_num_threads(THREADS_PER_GEN)
22
  torch.set_num_interop_threads(2)
23
 
24
+ os.environ["OMP_NUM_THREADS"] = str(THREADS_PER_GEN)
25
+ os.environ["MKL_NUM_THREADS"] = str(THREADS_PER_GEN)
26
+ os.environ["OPENBLAS_NUM_THREADS"] = str(THREADS_PER_GEN)
27
+ os.environ["VECLIB_MAXIMUM_THREADS"] = str(THREADS_PER_GEN)
28
+ os.environ["NUMEXPR_NUM_THREADS"] = str(THREADS_PER_GEN)
 
29
 
30
  logger.info(f"CPU cores: {PHYSICAL_CORES} | "
31
  f"Concurrent gens: {MAX_CONCURRENT} | "
 
35
  print("Loading model...")
36
  tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)
37
 
38
+ # ── Safe dtype detection (works on ALL torch versions) ────────────────────────
39
+ def _pick_dtype() -> torch.dtype:
40
+ if torch.cuda.is_available():
41
+ logger.info("Device: CUDA — using float16")
42
+ return torch.float16
43
+
44
+ # bfloat16 on CPU: supported natively on Ampere+ and some Intel CPUs.
45
+ # We probe by actually trying a small bf16 op instead of relying on
46
+ # version-specific API calls that may not exist.
47
+ try:
48
+ _ = torch.zeros(4, dtype=torch.bfloat16) @ torch.zeros(4, dtype=torch.bfloat16)
49
+ logger.info("Device: CPU — bfloat16 probe passed, using bfloat16")
50
+ return torch.bfloat16
51
+ except Exception:
52
+ logger.info("Device: CPU — bfloat16 not supported, using float32")
53
+ return torch.float32
54
+
55
+ DTYPE = _pick_dtype()
56
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
57
 
58
  model = AutoModelForCausalLM.from_pretrained(
59
  MODEL_ID,
60
+ torch_dtype = DTYPE,
61
+ device_map = "auto" if DEVICE == "cuda" else "cpu",
62
+ trust_remote_code = True,
63
+ low_cpu_mem_usage = True,
64
  )
65
  model.eval()
66
 
67
+ # ── INT8 dynamic quantization ─────────────────────────────────────────────────
68
+ # ~2× faster inference, ~50% less RAM. Only apply on CPU.
69
+ if DEVICE == "cpu":
70
+ try:
71
+ model = torch.quantization.quantize_dynamic(
72
+ model,
73
+ {torch.nn.Linear},
74
+ dtype=torch.qint8,
75
+ )
76
+ logger.info("INT8 dynamic quantization applied ✓")
77
+ except Exception as e:
78
+ logger.warning(f"Quantization skipped: {e}")
79
 
80
+ # ── torch.compile ─────────────────────────────────────────────────────────────
 
81
  if hasattr(torch, "compile"):
82
  try:
83
+ model = torch.compile(model, mode="default")
84
  logger.info("torch.compile applied ✓")
85
  except Exception as e:
86
  logger.warning(f"torch.compile skipped: {e}")
 
88
  print("Model loaded!")
89
 
90
  # ── Generation config ─────────────────────────────────────────────────────────
 
91
  GEN_CONFIG = dict(
92
+ max_new_tokens = 80,
93
+ do_sample = True,
94
+ temperature = 0.85,
95
+ top_p = 0.9,
 
96
  repetition_penalty = 1.1,
97
+ pad_token_id = tokenizer.eos_token_id,
98
+ use_cache = True,
 
99
  )
100
 
101
+ # ── Thread pool + semaphores ──────────────────────────────────────────────────
102
+ executor = ThreadPoolExecutor(max_workers=MAX_CONCURRENT, thread_name_prefix="gen")
103
+ MAX_QUEUE = 8
 
 
 
 
 
 
 
 
104
  gen_semaphore = threading.Semaphore(MAX_CONCURRENT)
105
  queue_counter = threading.Semaphore(MAX_QUEUE)
106
 
 
119
  "Use lowercase, slang, and act like a real person chatting."
120
  )
121
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  # ── Core generation ────────────────────────────────────────────────────────────
123
  def _run_generation(prompt: str, streamer: TextIteratorStreamer) -> None:
124
  inputs = tokenizer(
125
  prompt,
126
+ return_tensors = "pt",
127
+ truncation = True,
128
+ max_length = 512,
129
  )
 
130
  with torch.inference_mode():
131
  model.generate(
132
  input_ids = inputs.input_ids,
 
135
  **GEN_CONFIG,
136
  )
137
 
138
+ # ── Streaming response ─────────────────────────────────────────────────────────
 
139
  def respond(message: str, history: list, api_key: str):
140
  if not validate_key(api_key):
141
  yield "[error] unauthorized"
142
  return
143
 
 
144
  if not queue_counter.acquire(blocking=False):
145
  yield "[error] server busy — try again in a moment"
146
  return
147
 
148
  try:
149
+ MAX_HISTORY = 4
150
+ trimmed_history = history[-MAX_HISTORY:]
 
 
151
 
152
  messages = [{"role": "system", "content": SYSTEM_PROMPT}]
153
  messages.extend(trimmed_history)
 
156
  try:
157
  prompt = tokenizer.apply_chat_template(
158
  messages,
159
+ tokenize = False,
160
  add_generation_prompt = True,
161
  )
162
  except Exception as e:
 
165
 
166
  streamer = TextIteratorStreamer(
167
  tokenizer,
168
+ skip_prompt = True,
169
+ skip_special_tokens = True,
170
+ timeout = 60.0,
171
  )
172
 
 
173
  gen_semaphore.acquire()
174
  try:
175
  future = executor.submit(_run_generation, prompt, streamer)
 
180
  yield response
181
 
182
  future.result(timeout=10)
 
183
  finally:
184
  gen_semaphore.release()
185
 
 
187
  queue_counter.release()
188
 
189
 
190
+ # ── Warmup ────────────────────────────────────────────────────────────────────
 
 
191
  def _warmup():
192
  logger.info("Running warmup generation...")
193
  t0 = time.time()
194
  try:
195
  dummy_streamer = TextIteratorStreamer(
196
+ tokenizer,
197
+ skip_prompt = True,
198
+ skip_special_tokens = True,
199
+ timeout = 120.0,
200
  )
201
  dummy_prompt = tokenizer.apply_chat_template(
202
  [{"role": "system", "content": SYSTEM_PROMPT},
203
  {"role": "user", "content": "hi"}],
204
+ tokenize = False,
205
+ add_generation_prompt = True,
206
  )
207
+ inputs = tokenizer(dummy_prompt, return_tensors="pt",
208
+ truncation=True, max_length=256)
209
  warmup_cfg = {**GEN_CONFIG, "max_new_tokens": 20}
210
 
211
  def _gen():
 
222
  for _ in dummy_streamer:
223
  pass
224
  t.join()
225
+ logger.info(f"Warmup done in {time.time() - t0:.2f}s ✓")
226
  except Exception as e:
227
  logger.warning(f"Warmup failed (non-fatal): {e}")
228
 
 
229
  Thread(target=_warmup, daemon=True).start()
230
 
231
 
 
285
  stored_key = gr.Textbox(value="", visible=False)
286
 
287
  chatbot = gr.Chatbot(
288
+ type = "messages",
289
+ avatar_images = (USER_AVATAR, BOT_AVATAR),
290
+ height = 500,
291
+ show_label = False,
292
+ bubble_full_width = False,
293
+ show_copy_button = False,
294
  )
295
 
296
  gr.ChatInterface(