VedaX-Labs commited on
Commit
e51984f
·
verified ·
1 Parent(s): 96a3339

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +435 -0
app.py ADDED
@@ -0,0 +1,435 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import threading
3
+ import time
4
+ from collections import deque
5
+
6
+ import torch
7
+ from fastapi import FastAPI, HTTPException
8
+ from pydantic import BaseModel, Field
9
+ from transformers import AutoTokenizer, AutoModelForCausalLM
10
+
11
+
12
+ # ============================================================
13
+ # CONFIG
14
+ # ============================================================
15
+
16
+ MODEL_PATH = os.getenv(
17
+ "MODEL_PATH",
18
+ "./gemma3-270m-merged"
19
+ )
20
+
21
+ MAX_INPUT_TOKENS = int(
22
+ os.getenv("MAX_INPUT_TOKENS", "1024")
23
+ )
24
+
25
+ MAX_NEW_TOKENS = int(
26
+ os.getenv("MAX_NEW_TOKENS", "256")
27
+ )
28
+
29
+ MAX_HISTORY_MESSAGES = int(
30
+ os.getenv("MAX_HISTORY_MESSAGES", "8")
31
+ )
32
+
33
+ # Number of simultaneous generations.
34
+ # Keep this LOW on CPU.
35
+ MAX_CONCURRENT_GENERATIONS = int(
36
+ os.getenv("MAX_CONCURRENT_GENERATIONS", "1")
37
+ )
38
+
39
+ # Maximum waiting requests.
40
+ MAX_QUEUE_SIZE = int(
41
+ os.getenv("MAX_QUEUE_SIZE", "20")
42
+ )
43
+
44
+ # CPU threads.
45
+ CPU_THREADS = int(
46
+ os.getenv(
47
+ "CPU_THREADS",
48
+ str(max(1, (os.cpu_count() or 4) - 1))
49
+ )
50
+ )
51
+
52
+ torch.set_num_threads(CPU_THREADS)
53
+
54
+ DEVICE = "cpu"
55
+
56
+
57
+ # ============================================================
58
+ # APP
59
+ # ============================================================
60
+
61
+ app = FastAPI(
62
+ title="Gemma 3 270M API",
63
+ description="CPU inference API for Gemma 3 270M",
64
+ version="1.0.0",
65
+ )
66
+
67
+
68
+ # ============================================================
69
+ # REQUEST / RESPONSE MODELS
70
+ # ============================================================
71
+
72
+ class Message(BaseModel):
73
+ role: str
74
+ content: str
75
+
76
+
77
+ class ChatRequest(BaseModel):
78
+ message: str = Field(
79
+ ...,
80
+ min_length=1,
81
+ max_length=12000
82
+ )
83
+
84
+ history: list[Message] = Field(
85
+ default_factory=list
86
+ )
87
+
88
+ max_new_tokens: int = Field(
89
+ default=128,
90
+ ge=1,
91
+ le=256
92
+ )
93
+
94
+ temperature: float = Field(
95
+ default=0.7,
96
+ ge=0.0,
97
+ le=2.0
98
+ )
99
+
100
+ top_p: float = Field(
101
+ default=0.9,
102
+ gt=0.0,
103
+ le=1.0
104
+ )
105
+
106
+
107
+ class ChatResponse(BaseModel):
108
+ response: str
109
+ model: str
110
+ input_tokens: int
111
+ output_tokens: int
112
+ generation_time: float
113
+
114
+
115
+ # ============================================================
116
+ # GLOBAL STATE
117
+ # ============================================================
118
+
119
+ print("=" * 70)
120
+ print(" GEMMA 3 270M CPU API SERVER")
121
+ print("=" * 70)
122
+
123
+ print(f"Model: {MODEL_PATH}")
124
+ print(f"Device: {DEVICE}")
125
+ print(f"CPU threads: {CPU_THREADS}")
126
+
127
+ print()
128
+ print("Loading tokenizer...")
129
+
130
+ tokenizer = AutoTokenizer.from_pretrained(
131
+ MODEL_PATH,
132
+ local_files_only=True,
133
+ )
134
+
135
+ if tokenizer.pad_token is None:
136
+ tokenizer.pad_token = tokenizer.eos_token
137
+
138
+ print("Tokenizer loaded.")
139
+
140
+ print()
141
+ print("Loading model...")
142
+
143
+ model = AutoModelForCausalLM.from_pretrained(
144
+ MODEL_PATH,
145
+ local_files_only=True,
146
+ dtype=torch.float32,
147
+ low_cpu_mem_usage=True,
148
+ )
149
+
150
+ model.to(DEVICE)
151
+ model.eval()
152
+
153
+ print("Model loaded successfully.")
154
+ print()
155
+
156
+ # Semaphore prevents multiple CPU generations from hammering
157
+ # the machine simultaneously.
158
+ generation_semaphore = threading.BoundedSemaphore(
159
+ MAX_CONCURRENT_GENERATIONS
160
+ )
161
+
162
+ # Simple queue counter.
163
+ queue_lock = threading.Lock()
164
+ waiting_requests = 0
165
+
166
+
167
+ # ============================================================
168
+ # HEALTH
169
+ # ============================================================
170
+
171
+ @app.get("/")
172
+ def root():
173
+ return {
174
+ "name": "Gemma 3 270M API",
175
+ "status": "online",
176
+ "model": "Gemma 3 270M",
177
+ "device": "cpu",
178
+ "api": "/v1/chat",
179
+ }
180
+
181
+
182
+ @app.get("/health")
183
+ def health():
184
+ return {
185
+ "status": "healthy",
186
+ "model_loaded": True,
187
+ "device": DEVICE,
188
+ "cpu_threads": CPU_THREADS,
189
+ "waiting_requests": waiting_requests,
190
+ }
191
+
192
+
193
+ # ============================================================
194
+ # CHAT
195
+ # ============================================================
196
+
197
+ @app.post(
198
+ "/v1/chat",
199
+ response_model=ChatResponse
200
+ )
201
+ def chat(request: ChatRequest):
202
+
203
+ global waiting_requests
204
+
205
+ # --------------------------------------------------------
206
+ # Queue protection
207
+ # --------------------------------------------------------
208
+
209
+ with queue_lock:
210
+
211
+ if waiting_requests >= MAX_QUEUE_SIZE:
212
+ raise HTTPException(
213
+ status_code=429,
214
+ detail=(
215
+ "Server is busy. "
216
+ "Please try again later."
217
+ )
218
+ )
219
+
220
+ waiting_requests += 1
221
+
222
+ acquired = False
223
+
224
+ try:
225
+
226
+ # ----------------------------------------------------
227
+ # Wait for generation slot
228
+ # ----------------------------------------------------
229
+
230
+ generation_semaphore.acquire()
231
+ acquired = True
232
+
233
+ # ----------------------------------------------------
234
+ # Prepare conversation
235
+ # ----------------------------------------------------
236
+
237
+ messages = []
238
+
239
+ history = request.history[
240
+ -MAX_HISTORY_MESSAGES:
241
+ ]
242
+
243
+ for item in history:
244
+
245
+ if item.role not in (
246
+ "user",
247
+ "assistant"
248
+ ):
249
+ continue
250
+
251
+ content = item.content.strip()
252
+
253
+ if not content:
254
+ continue
255
+
256
+ messages.append(
257
+ {
258
+ "role": item.role,
259
+ "content": content,
260
+ }
261
+ )
262
+
263
+ messages.append(
264
+ {
265
+ "role": "user",
266
+ "content": request.message.strip(),
267
+ }
268
+ )
269
+
270
+ # ----------------------------------------------------
271
+ # Gemma chat template
272
+ # ----------------------------------------------------
273
+
274
+ try:
275
+
276
+ prompt = tokenizer.apply_chat_template(
277
+ messages,
278
+ tokenize=False,
279
+ add_generation_prompt=True,
280
+ )
281
+
282
+ except Exception:
283
+
284
+ # Fallback if tokenizer template is unavailable
285
+ prompt = request.message.strip()
286
+
287
+ # ----------------------------------------------------
288
+ # Tokenize
289
+ # ----------------------------------------------------
290
+
291
+ inputs = tokenizer(
292
+ prompt,
293
+ return_tensors="pt",
294
+ truncation=True,
295
+ max_length=MAX_INPUT_TOKENS,
296
+ )
297
+
298
+ inputs = {
299
+ key: value.to(DEVICE)
300
+ for key, value in inputs.items()
301
+ }
302
+
303
+ input_tokens = inputs[
304
+ "input_ids"
305
+ ].shape[1]
306
+
307
+ # ----------------------------------------------------
308
+ # Generation
309
+ # ----------------------------------------------------
310
+
311
+ max_tokens = min(
312
+ request.max_new_tokens,
313
+ MAX_NEW_TOKENS
314
+ )
315
+
316
+ start_time = time.perf_counter()
317
+
318
+ with torch.inference_mode():
319
+
320
+ if request.temperature <= 0:
321
+
322
+ outputs = model.generate(
323
+ **inputs,
324
+
325
+ max_new_tokens=max_tokens,
326
+
327
+ do_sample=False,
328
+
329
+ pad_token_id=tokenizer.pad_token_id,
330
+ eos_token_id=tokenizer.eos_token_id,
331
+
332
+ use_cache=True,
333
+ )
334
+
335
+ else:
336
+
337
+ outputs = model.generate(
338
+ **inputs,
339
+
340
+ max_new_tokens=max_tokens,
341
+
342
+ do_sample=True,
343
+
344
+ temperature=request.temperature,
345
+ top_p=request.top_p,
346
+
347
+ repetition_penalty=1.10,
348
+
349
+ pad_token_id=tokenizer.pad_token_id,
350
+ eos_token_id=tokenizer.eos_token_id,
351
+
352
+ use_cache=True,
353
+ )
354
+
355
+ generation_time = (
356
+ time.perf_counter() - start_time
357
+ )
358
+
359
+ # ----------------------------------------------------
360
+ # Decode ONLY generated tokens
361
+ # ----------------------------------------------------
362
+
363
+ generated_tokens = outputs[
364
+ 0,
365
+ input_tokens:
366
+ ]
367
+
368
+ response = tokenizer.decode(
369
+ generated_tokens,
370
+ skip_special_tokens=True,
371
+ ).strip()
372
+
373
+ if not response:
374
+ response = "I couldn't generate a response."
375
+
376
+ output_tokens = generated_tokens.shape[0]
377
+
378
+ return ChatResponse(
379
+ response=response,
380
+ model="gemma-3-270m",
381
+ input_tokens=input_tokens,
382
+ output_tokens=output_tokens,
383
+ generation_time=round(
384
+ generation_time,
385
+ 3
386
+ ),
387
+ )
388
+
389
+ finally:
390
+
391
+ if acquired:
392
+ generation_semaphore.release()
393
+
394
+ with queue_lock:
395
+ waiting_requests = max(
396
+ 0,
397
+ waiting_requests - 1
398
+ )
399
+
400
+
401
+ # ============================================================
402
+ # STARTUP MESSAGE
403
+ # ============================================================
404
+
405
+ if __name__ == "__main__":
406
+
407
+ import uvicorn
408
+
409
+ print("=" * 70)
410
+ print("SERVER READY")
411
+ print("=" * 70)
412
+
413
+ print()
414
+ print("API:")
415
+ print("POST /v1/chat")
416
+
417
+ print()
418
+ print("Health:")
419
+ print("GET /health")
420
+
421
+ print()
422
+ print("Swagger:")
423
+ print("GET /docs")
424
+
425
+ print()
426
+ print("Starting server...")
427
+
428
+ uvicorn.run(
429
+ app,
430
+ host="0.0.0.0",
431
+ port=int(
432
+ os.getenv("PORT", "7860")
433
+ ),
434
+ workers=1,
435
+ )