ArushCodes commited on
Commit
af07d8f
·
verified ·
1 Parent(s): 0452cab

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +176 -714
app.py CHANGED
@@ -1,444 +1,106 @@
1
- """
2
- OpenAI-compatible FastAPI + Gradio front-end for Qwen3.6-35B-A3B-MTP,
3
- running on the real llama.cpp `llama-server` binary so that native
4
- Multi-Token Prediction (MTP) speculative decoding is actually used.
5
-
6
- Designed for Hugging Face Spaces.
7
-
8
- IMPORTANT:
9
- - llama-server is started inside FastAPI lifespan.
10
- - No top-level llama-server startup.
11
- - No uvicorn.run() is required on HF Spaces.
12
- - No atexit handler is required.
13
- """
14
-
15
- import os
16
  import time
17
- import json
18
- import shutil
19
- import socket
20
- import stat
21
- import tarfile
22
- import tempfile
23
- import subprocess
24
  import re
 
 
 
25
 
26
- from contextlib import asynccontextmanager
27
- from typing import List, Dict, Any, Optional, AsyncGenerator
28
-
29
- import httpx
30
- import gradio as gr
31
-
32
- from fastapi import FastAPI, HTTPException
33
  from fastapi.responses import StreamingResponse, JSONResponse
34
  from fastapi.middleware.cors import CORSMiddleware
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
- from pydantic import BaseModel
 
 
37
  from huggingface_hub import hf_hub_download
38
 
 
 
 
 
39
 
40
- # ===========================================================================
41
- # 0. CONFIG
42
- # ===========================================================================
43
-
44
- REPO_ID = "unsloth/Qwen3.6-35B-A3B-MTP-GGUF"
45
- MODEL_FILENAME = "Qwen3.6-35B-A3B-UD-IQ3_XXS.gguf"
46
-
47
- MODEL_NAME = "arush/jarush"
48
-
49
- LLAMA_SERVER_HOST = "127.0.0.1"
50
- LLAMA_SERVER_PORT = 8910
51
- PUBLIC_PORT = 7860
52
-
53
- CTX_SIZE = 4096
54
- N_BATCH = 512
55
- N_UBATCH = 512
56
- SPEC_DRAFT_N_MAX = 3
57
-
58
- LLAMA_INSTALL_DIR = os.path.expanduser("~/llama.cpp-prebuilt")
59
- LLAMA_SERVER_BIN = os.path.join(
60
- LLAMA_INSTALL_DIR,
61
- "build",
62
- "bin",
63
- "llama-server",
64
  )
65
-
66
- GITHUB_API_LATEST_RELEASE = (
67
- "https://api.github.com/repos/ggml-org/llama.cpp/releases/latest"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  )
69
- PREBUILT_ASSET_SUFFIX = "-bin-ubuntu-x64.tar.gz"
70
-
71
- PHYSICAL_CORES = max(1, (os.cpu_count() or 4) // 2)
72
-
73
-
74
- # ===========================================================================
75
- # 1. FETCH PREBUILT llama-server
76
- # ===========================================================================
77
-
78
- def ensure_llama_server_downloaded() -> None:
79
- if (
80
- os.path.isfile(LLAMA_SERVER_BIN)
81
- and os.access(LLAMA_SERVER_BIN, os.X_OK)
82
- ):
83
- print(
84
- f"[fetch] Found existing prebuilt llama-server at "
85
- f"{LLAMA_SERVER_BIN}"
86
- )
87
- return
88
-
89
- print("[fetch] Looking up latest llama.cpp release ...")
90
-
91
- with httpx.Client(timeout=30, follow_redirects=True) as client:
92
- resp = client.get(GITHUB_API_LATEST_RELEASE)
93
- resp.raise_for_status()
94
- release = resp.json()
95
-
96
- asset = next(
97
- (
98
- a
99
- for a in release.get("assets", [])
100
- if a["name"].endswith(PREBUILT_ASSET_SUFFIX)
101
- ),
102
- None,
103
- )
104
-
105
- if asset is None:
106
- raise RuntimeError(
107
- f"No asset ending in '{PREBUILT_ASSET_SUFFIX}' found."
108
- )
109
-
110
- print(
111
- f"[fetch] Downloading prebuilt binary: "
112
- f"{asset['name']} ({asset['size'] / 1e6:.1f} MB) ..."
113
- )
114
-
115
- os.makedirs(LLAMA_INSTALL_DIR, exist_ok=True)
116
-
117
- tarball_path = os.path.join(
118
- tempfile.gettempdir(),
119
- asset["name"],
120
- )
121
-
122
- with client.stream(
123
- "GET",
124
- asset["browser_download_url"],
125
- ) as r:
126
- r.raise_for_status()
127
-
128
- with open(tarball_path, "wb") as f:
129
- for chunk in r.iter_bytes(chunk_size=1 << 20):
130
- f.write(chunk)
131
-
132
- print("[fetch] Extracting ...")
133
-
134
- build_bin_dir = os.path.join(
135
- LLAMA_INSTALL_DIR,
136
- "build",
137
- "bin",
138
- )
139
- os.makedirs(build_bin_dir, exist_ok=True)
140
-
141
- with tarfile.open(tarball_path, "r:gz") as tf:
142
- tf.extractall(LLAMA_INSTALL_DIR)
143
-
144
- if not os.path.isfile(LLAMA_SERVER_BIN):
145
- found = None
146
-
147
- for root, _, files in os.walk(LLAMA_INSTALL_DIR):
148
- if "llama-server" in files:
149
- found = os.path.join(root, "llama-server")
150
- break
151
-
152
- if found is None:
153
- raise RuntimeError(
154
- "Extracted llama.cpp archive but couldn't locate "
155
- "a 'llama-server' binary."
156
- )
157
-
158
- os.makedirs(
159
- os.path.dirname(LLAMA_SERVER_BIN),
160
- exist_ok=True,
161
- )
162
-
163
- shutil.copy2(found, LLAMA_SERVER_BIN)
164
-
165
- src_dir = os.path.dirname(found)
166
- dst_dir = os.path.dirname(LLAMA_SERVER_BIN)
167
-
168
- for fname in os.listdir(src_dir):
169
- if fname.startswith("lib") and ".so" in fname:
170
- src = os.path.join(src_dir, fname)
171
- dst = os.path.join(dst_dir, fname)
172
-
173
- try:
174
- shutil.copy2(src, dst)
175
- except Exception as e:
176
- print(
177
- f"[fetch] Warning: could not copy {fname}: {e}"
178
- )
179
-
180
- st = os.stat(LLAMA_SERVER_BIN)
181
-
182
- os.chmod(
183
- LLAMA_SERVER_BIN,
184
- st.st_mode
185
- | stat.S_IEXEC
186
- | stat.S_IXGRP
187
- | stat.S_IXOTH,
188
- )
189
-
190
- try:
191
- os.remove(tarball_path)
192
- except OSError:
193
- pass
194
-
195
- print(f"[fetch] Ready: {LLAMA_SERVER_BIN}")
196
-
197
-
198
- # ===========================================================================
199
- # 2. DOWNLOAD MTP GGUF
200
- # ===========================================================================
201
-
202
- def ensure_model_downloaded() -> str:
203
- print(
204
- f"[download] Fetching {MODEL_FILENAME} from {REPO_ID} ..."
205
- )
206
-
207
- path = hf_hub_download(
208
- repo_id=REPO_ID,
209
- filename=MODEL_FILENAME,
210
- )
211
-
212
- print(f"[download] Model ready at {path}")
213
-
214
- return path
215
-
216
-
217
- # ===========================================================================
218
- # 3. LLAMA-SERVER PROCESS MANAGEMENT
219
- # ===========================================================================
220
-
221
- _llama_proc: Optional[subprocess.Popen] = None
222
-
223
-
224
- def _port_is_open(
225
- host: str,
226
- port: int,
227
- timeout: float = 1.0,
228
- ) -> bool:
229
- try:
230
- with socket.create_connection(
231
- (host, port),
232
- timeout=timeout,
233
- ):
234
- return True
235
- except OSError:
236
- return False
237
-
238
-
239
- def start_llama_server(
240
- model_path: str,
241
- ) -> subprocess.Popen:
242
- global _llama_proc
243
-
244
- cmd = [
245
- LLAMA_SERVER_BIN,
246
- "-m",
247
- model_path,
248
- "--host",
249
- LLAMA_SERVER_HOST,
250
- "--port",
251
- str(LLAMA_SERVER_PORT),
252
- "-c",
253
- str(CTX_SIZE),
254
- "-b",
255
- str(N_BATCH),
256
- "-ub",
257
- str(N_UBATCH),
258
- "-t",
259
- str(PHYSICAL_CORES),
260
- "-ngl",
261
- "0",
262
- "--flash-attn",
263
- "on",
264
- "--cache-type-k",
265
- "q4_0",
266
- "--cache-type-v",
267
- "q4_0",
268
- "--spec-type",
269
- "draft-mtp",
270
- "--spec-draft-n-max",
271
- str(SPEC_DRAFT_N_MAX),
272
- "--jinja",
273
- "--alias",
274
- MODEL_NAME,
275
- "--metrics",
276
- ]
277
-
278
- print("[llama-server] launching:")
279
- print(" ".join(cmd))
280
-
281
- proc = subprocess.Popen(
282
- cmd,
283
- stdout=subprocess.PIPE,
284
- stderr=subprocess.STDOUT,
285
- text=True,
286
- bufsize=1,
287
- )
288
-
289
- _llama_proc = proc
290
-
291
- deadline = time.time() + 600
292
-
293
- while time.time() < deadline:
294
- if proc.poll() is not None:
295
- remaining = proc.stdout.read() if proc.stdout else ""
296
-
297
- raise RuntimeError(
298
- "llama-server exited early "
299
- f"with code {proc.returncode}.\n"
300
- f"Output:\n{remaining}"
301
- )
302
-
303
- if _port_is_open(
304
- LLAMA_SERVER_HOST,
305
- LLAMA_SERVER_PORT,
306
- ):
307
- print("[llama-server] ready.")
308
- return proc
309
-
310
- time.sleep(2)
311
-
312
- print(
313
- "[llama-server] startup timeout; terminating process..."
314
- )
315
-
316
- proc.terminate()
317
-
318
- try:
319
- proc.wait(timeout=15)
320
- except subprocess.TimeoutExpired:
321
- proc.kill()
322
-
323
- raise TimeoutError(
324
- "llama-server did not become ready within 600 seconds."
325
- )
326
-
327
-
328
- def stop_llama_server() -> None:
329
- global _llama_proc
330
-
331
- if (
332
- _llama_proc
333
- and _llama_proc.poll() is None
334
- ):
335
- print(
336
- "[llama-server] shutting down subprocess..."
337
- )
338
-
339
- _llama_proc.terminate()
340
-
341
- try:
342
- _llama_proc.wait(timeout=15)
343
-
344
- except subprocess.TimeoutExpired:
345
- print(
346
- "[llama-server] graceful shutdown timed out; killing..."
347
- )
348
-
349
- _llama_proc.kill()
350
-
351
- try:
352
- _llama_proc.wait(timeout=5)
353
- except subprocess.TimeoutExpired:
354
- pass
355
-
356
- _llama_proc = None
357
-
358
-
359
- # ===========================================================================
360
- # 4. FASTAPI LIFESPAN
361
- # ===========================================================================
362
-
363
- LLAMA_BASE_URL = ""
364
 
365
- _http_client: Optional[httpx.AsyncClient] = None
366
-
367
-
368
- @asynccontextmanager
369
- async def lifespan(app: FastAPI):
370
- global _llama_proc
371
- global _http_client
372
- global LLAMA_BASE_URL
373
-
374
- print("[lifespan] Starting application...")
375
-
376
- try:
377
- ensure_llama_server_downloaded()
378
-
379
- model_path = ensure_model_downloaded()
380
-
381
- _llama_proc = start_llama_server(model_path)
382
-
383
- LLAMA_BASE_URL = (
384
- f"http://{LLAMA_SERVER_HOST}:{LLAMA_SERVER_PORT}"
385
- )
386
-
387
- _http_client = httpx.AsyncClient(
388
- base_url=LLAMA_BASE_URL,
389
- timeout=None,
390
- )
391
-
392
- print("[lifespan] llama-server is ready.")
393
- print("[lifespan] Application startup complete.")
394
-
395
- yield
396
-
397
- finally:
398
- print("[lifespan] Shutting down...")
399
-
400
- if _http_client is not None:
401
- try:
402
- await _http_client.aclose()
403
- except Exception as e:
404
- print(
405
- f"[lifespan] HTTP client shutdown warning: {e}"
406
- )
407
-
408
- _http_client = None
409
-
410
- stop_llama_server()
411
-
412
- print("[lifespan] Shutdown complete.")
413
-
414
-
415
- # ===========================================================================
416
- # 5. FASTAPI APPLICATION
417
- # ===========================================================================
418
 
419
- app = FastAPI(
420
- title="OpenAI-Compatible MTP Proxy Server",
421
- version="2.0.0",
422
- lifespan=lifespan,
423
- )
424
 
 
425
  app.add_middleware(
426
  CORSMiddleware,
427
  allow_origins=["*"],
428
- allow_credentials=False,
429
  allow_methods=["*"],
430
  allow_headers=["*"],
431
  )
432
 
433
-
434
- # ===========================================================================
435
- # 6. OPENAI-COMPATIBLE API MODELS
436
- # ===========================================================================
437
-
438
  class ChatMessage(BaseModel):
439
  role: str
440
- content: Any
441
-
442
 
443
  class ChatCompletionRequest(BaseModel):
444
  model: Optional[str] = MODEL_NAME
@@ -448,13 +110,12 @@ class ChatCompletionRequest(BaseModel):
448
  max_tokens: Optional[int] = 512
449
  stream: Optional[bool] = False
450
 
451
-
452
- # ===========================================================================
453
- # 7. OPENAI-COMPATIBLE ENDPOINTS
454
- # ===========================================================================
455
-
456
  @app.get("/v1/models")
457
  async def list_models():
 
458
  return {
459
  "object": "list",
460
  "data": [
@@ -467,332 +128,133 @@ async def list_models():
467
  ],
468
  }
469
 
470
-
471
- async def _proxy_stream(
472
- payload: Dict[str, Any],
473
- ) -> AsyncGenerator[bytes, None]:
474
- if _http_client is None:
475
- raise RuntimeError("llama-server is not ready.")
476
-
477
- async with _http_client.stream(
478
- "POST",
479
- "/v1/chat/completions",
480
- json=payload,
481
- ) as resp:
482
- resp.raise_for_status()
483
-
484
- async for chunk in resp.aiter_bytes():
485
- yield chunk
486
-
487
 
488
  @app.post("/v1/chat/completions")
489
- async def chat_completions(
490
- req: ChatCompletionRequest,
491
- ):
492
- if _http_client is None:
493
- raise HTTPException(
494
- status_code=503,
495
- detail="llama-server is still starting.",
496
- )
497
-
498
- payload = {
499
- "model": req.model,
500
- "messages": [
501
- m.model_dump()
502
- for m in req.messages
503
- ],
504
- "temperature": req.temperature,
505
- "top_p": req.top_p,
506
- "max_tokens": req.max_tokens,
507
- "stream": req.stream,
508
- }
509
-
510
  try:
 
 
 
511
  if req.stream:
512
  return StreamingResponse(
513
- _proxy_stream(payload),
 
 
 
 
 
514
  media_type="text/event-stream",
515
- headers={
516
- "Cache-Control": "no-cache",
517
- "Connection": "keep-alive",
518
- },
519
  )
520
-
521
- resp = await _http_client.post(
522
- "/v1/chat/completions",
523
- json=payload,
524
- )
525
-
526
- resp.raise_for_status()
527
-
528
- return JSONResponse(content=resp.json())
529
-
530
- except httpx.HTTPStatusError as e:
531
- raise HTTPException(
532
- status_code=e.response.status_code,
533
- detail=e.response.text,
534
- )
535
-
536
  except Exception as e:
537
- raise HTTPException(
538
- status_code=500,
539
- detail=str(e),
540
- )
541
-
542
 
543
- # ===========================================================================
544
- # 8. GRADIO RESPONSE CLEANING
545
- # ===========================================================================
546
-
547
- def clean_assistant_reply(
548
- text: Any,
549
- ) -> str:
550
  if not text:
551
  return ""
552
-
553
  if isinstance(text, list):
554
- parts = []
555
-
556
- for p in text:
557
- if isinstance(p, str):
558
- parts.append(p)
559
- elif isinstance(p, dict):
560
- parts.append(
561
- p.get(
562
- "text",
563
- p.get("content", ""),
564
- )
565
- )
566
-
567
- text = "".join(parts)
568
-
569
  elif not isinstance(text, str):
570
  text = str(text)
571
 
572
- cleaned = re.sub(
573
- r"<think>.*?</think>\n*",
574
- "",
575
- text,
576
- flags=re.DOTALL,
577
- )
578
-
579
- cleaned = cleaned.replace(
580
- "</think>",
581
- "",
582
- )
583
-
584
- cleaned = re.sub(
585
- r"\n\n---\n\*⏱️.*?\*$",
586
- "",
587
- cleaned,
588
- )
589
-
590
  return cleaned.strip()
591
 
592
-
593
- # ===========================================================================
594
- # 9. GRADIO CHAT FUNCTION
595
- # ===========================================================================
596
-
597
- def respond(
598
- message,
599
- history,
600
- system_message,
601
- max_tokens,
602
- temperature,
603
- top_p,
604
- ):
605
- messages = [
606
- {
607
- "role": "system",
608
- "content": system_message,
609
- }
610
- ]
611
-
612
  for item in history:
613
  if isinstance(item, dict):
614
  role = item.get("role", "user")
615
  content = item.get("content", "")
616
-
617
  if role == "assistant":
618
  content = clean_assistant_reply(content)
619
-
620
- messages.append(
621
- {
622
- "role": role,
623
- "content": content,
624
- }
625
- )
626
-
627
  elif isinstance(item, (list, tuple)):
628
- if len(item) >= 1 and item[0]:
629
- messages.append(
630
- {
631
- "role": "user",
632
- "content": item[0],
633
- }
634
- )
635
-
636
- if len(item) >= 2 and item[1]:
637
- messages.append(
638
- {
639
- "role": "assistant",
640
- "content": clean_assistant_reply(
641
- item[1]
642
- ),
643
- }
644
- )
645
 
646
- if isinstance(message, dict):
647
- user_text = message.get("text", "")
648
-
649
- if not user_text:
650
- user_text = message.get("content", "")
651
- else:
652
- user_text = str(message)
653
-
654
- messages.append(
655
- {
656
- "role": "user",
657
- "content": user_text,
658
- }
659
- )
660
-
661
- payload = {
662
- "model": MODEL_NAME,
663
- "messages": messages,
664
- "max_tokens": int(max_tokens),
665
- "temperature": float(temperature),
666
- "top_p": float(top_p),
667
- "stream": True,
668
- }
669
 
670
  response = ""
671
  token_count = 0
672
  start_time = time.time()
673
-
674
- try:
675
- with httpx.stream(
676
- "POST",
677
- f"{LLAMA_BASE_URL}/v1/chat/completions",
678
- json=payload,
679
- timeout=None,
680
- ) as r:
681
- r.raise_for_status()
682
-
683
- for line in r.iter_lines():
684
- if not line:
685
- continue
686
-
687
- if not line.startswith("data: "):
688
- continue
689
-
690
- data_str = line[len("data: "):]
691
-
692
- if data_str.strip() == "[DONE]":
693
- break
694
-
695
- try:
696
- chunk = json.loads(data_str)
697
- except json.JSONDecodeError:
698
- continue
699
-
700
- choices = chunk.get("choices", [])
701
-
702
- if not choices:
703
- continue
704
-
705
- delta = choices[0].get("delta", {})
706
- piece = delta.get("content")
707
-
708
- if piece:
709
- response += piece
710
- token_count += 1
711
- yield response
712
-
713
- except Exception as e:
714
- response += f"\n\n⚠️ Generation error: {e}"
715
- yield response
716
- return
717
-
718
- elapsed = time.time() - start_time
719
-
720
- if elapsed > 0 and token_count > 0:
721
- tps = token_count / elapsed
722
-
723
- response += (
724
- "\n\n---\n"
725
- f"*⏱️ {tps:.2f} tok/s "
726
- f"({token_count} tokens in {elapsed:.2f}s) — "
727
- f"MTP draft-n-max={SPEC_DRAFT_N_MAX}*"
728
- )
729
-
730
  yield response
731
 
732
-
733
- # ===========================================================================
734
- # 10. GRADIO UI
735
- # ===========================================================================
736
-
737
  chatbot = gr.ChatInterface(
738
  fn=respond,
739
-
740
  additional_inputs=[
741
- gr.Textbox(
742
- value="You are a helpful coding assistant.",
743
- label="System message",
744
- ),
745
-
746
- gr.Slider(
747
- minimum=1,
748
- maximum=2048,
749
- value=512,
750
- step=1,
751
- label="Max new tokens",
752
- ),
753
-
754
- gr.Slider(
755
- minimum=0.1,
756
- maximum=2.0,
757
- value=0.7,
758
- step=0.1,
759
- label="Temperature",
760
- ),
761
-
762
- gr.Slider(
763
- minimum=0.1,
764
- maximum=1.0,
765
- value=0.95,
766
- step=0.05,
767
- label="Top-p",
768
- ),
769
  ],
770
-
771
- title=(
772
- "Qwen3.6-35B-A3B "
773
- "(MTP speculative decoding, CPU)"
774
- ),
775
- )
776
-
777
-
778
- # ===========================================================================
779
- # 11. MOUNT GRADIO INTO FASTAPI
780
- # ===========================================================================
781
-
782
- app = gr.mount_gradio_app(
783
- app,
784
- chatbot,
785
- path="/",
786
  )
787
 
 
 
788
 
789
- # ===========================================================================
790
- # 12. HF SPACES ENTRYPOINT
791
- # ===========================================================================
792
- #
793
- # Do NOT call uvicorn.run().
794
- # Do NOT register an atexit handler.
795
- #
796
- # Hugging Face Spaces loads the `app` object, and the FastAPI lifespan
797
- # manages llama-server startup/shutdown.
798
- # ===========================================================================
 
1
+ Import os
2
+ import sys
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  import time
 
 
 
 
 
 
 
4
  import re
5
+ import json
6
+ import multiprocessing
7
+ from typing import List, Dict, Any, Optional, Union, AsyncGenerator
8
 
9
+ # FastAPI & Pydantic imports
10
+ from fastapi import FastAPI, Request, HTTPException
 
 
 
 
 
11
  from fastapi.responses import StreamingResponse, JSONResponse
12
  from fastapi.middleware.cors import CORSMiddleware
13
+ from pydantic import BaseModel, Field
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # ⚡ 1. CPU & THREADING OPTIMIZATIONS FOR MoE
17
+ # ---------------------------------------------------------------------------
18
+ # MoE models are heavily bottlenecked by CPU RAM bandwidth during expert routing.
19
+ # Locking threads to physical cores avoids hyperthreading thrashing.
20
+ try:
21
+ import psutil
22
+ PHYSICAL_CORES = psutil.cpu_count(logical=False) or max(1, multiprocessing.cpu_count() // 2)
23
+ except Exception:
24
+ PHYSICAL_CORES = max(1, multiprocessing.cpu_count() // 2)
25
+
26
+ os.environ["OMP_NUM_THREADS"] = str(PHYSICAL_CORES)
27
+ os.environ["MKL_NUM_THREADS"] = str(PHYSICAL_CORES)
28
+ os.environ["OPENBLAS_NUM_THREADS"] = str(PHYSICAL_CORES)
29
+ os.environ["OMP_PROC_BIND"] = "TRUE"
30
+
31
+ try:
32
+ p = psutil.Process()
33
+ if sys.platform == "win32":
34
+ p.nice(psutil.HIGH_PRIORITY_CLASS)
35
+ else:
36
+ p.nice(-15)
37
+ except Exception:
38
+ pass
39
 
40
+ import gradio as gr
41
+ import llama_cpp
42
+ from llama_cpp import Llama
43
  from huggingface_hub import hf_hub_download
44
 
45
+ # ---------------------------------------------------------------------------
46
+ # ⚡ 2. MODEL DOWNLOAD & LLAMA-CPP MoE INITIALIZATION
47
+ # ---------------------------------------------------------------------------
48
+ NUMA_STRATEGY = getattr(llama_cpp, "GGML_NUMA_STRATEGY_DISTRIBUTE", False)
49
 
50
+ print("Downloading MoE model GGUF...")
51
+ model_path = hf_hub_download(
52
+ repo_id='unsloth/Qwen3.6-35B-A3B-MTP-GGUF',
53
+ filename="Qwen3.6-35B-A3B-UD-IQ3_XXS.gguf",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  )
55
+ print("Model downloaded successfully!")
56
+
57
+ # Load GGUF model optimized for CPU MoE execution
58
+ llm = Llama(
59
+ model_path=model_path,
60
+ n_gpu_layers=0, # 0 for CPU execution
61
+ n_ctx=4096,
62
+
63
+ # Threading optimizations
64
+ n_threads=4,
65
+ n_threads_batch=PHYSICAL_CORES,
66
+ numa=NUMA_STRATEGY,
67
+
68
+ # ⚡ MTP Configuration (Multi-Token Prediction)
69
+ # Instructs llama.cpp to predict 3 extra tokens using native MTP modules
70
+
71
+ # MoE Cache & RAM Bandwidth Optimizations:
72
+ # 1. Quantized KV Cache (Q4_0) saves RAM and frees CPU cache space for routing
73
+ type_k=getattr(llama_cpp, "GGML_TYPE_Q4_0", 2),
74
+ type_v=getattr(llama_cpp, "GGML_TYPE_Q4_0", 2),
75
+
76
+ # 2. Smaller ubatch prevents L2/L3 cache eviction during active expert evaluation
77
+ n_batch=512,
78
+ n_ubatch=512,
79
+ n_draft=2,
80
+ use_mlock=False,
81
+ use_mmap=True,
82
+ flash_attn=True,
83
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
+ MODEL_NAME = "arush/jarush"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
+ # ---------------------------------------------------------------------------
88
+ # ⚡ 3. FASTAPI INITIALIZATION & OPENAI PYDANTIC SCHEMAS
89
+ # ---------------------------------------------------------------------------
90
+ app = FastAPI(title="OpenAI-Compatible CPU MoE Server", version="1.0.0")
 
91
 
92
+ # Enable CORS for external web frontend requests
93
  app.add_middleware(
94
  CORSMiddleware,
95
  allow_origins=["*"],
96
+ allow_credentials=True,
97
  allow_methods=["*"],
98
  allow_headers=["*"],
99
  )
100
 
 
 
 
 
 
101
  class ChatMessage(BaseModel):
102
  role: str
103
+ content: Union[str, List[Dict[str, Any]]]
 
104
 
105
  class ChatCompletionRequest(BaseModel):
106
  model: Optional[str] = MODEL_NAME
 
110
  max_tokens: Optional[int] = 512
111
  stream: Optional[bool] = False
112
 
113
+ # ---------------------------------------------------------------------------
114
+ # ⚡ 4. OPENAI-COMPATIBLE REST ENDPOINTS
115
+ # ---------------------------------------------------------------------------
 
 
116
  @app.get("/v1/models")
117
  async def list_models():
118
+ """OpenAI standard /v1/models endpoint."""
119
  return {
120
  "object": "list",
121
  "data": [
 
128
  ],
129
  }
130
 
131
+ async def openai_stream_generator(messages_dict: list, max_tokens: int, temperature: float, top_p: float):
132
+ """Generates standard SSE streaming tokens for OpenAI client libraries."""
133
+ for chunk in llm.create_chat_completion(
134
+ messages=messages_dict,
135
+ max_tokens=max_tokens,
136
+ temperature=temperature,
137
+ top_p=top_p,
138
+ stream=True,
139
+ ):
140
+ yield f"data: {json.dumps(chunk)}\n\n"
141
+ yield "data: [DONE]\n\n"
 
 
 
 
 
 
142
 
143
  @app.post("/v1/chat/completions")
144
+ async def chat_completions(req: ChatCompletionRequest):
145
+ """OpenAI standard /v1/chat/completions endpoint."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  try:
147
+ # Convert pydantic messages to standard list of dicts
148
+ messages_dict = [{"role": m.role, "content": m.content} for m in req.messages]
149
+
150
  if req.stream:
151
  return StreamingResponse(
152
+ openai_stream_generator(
153
+ messages_dict=messages_dict,
154
+ max_tokens=req.max_tokens,
155
+ temperature=req.temperature,
156
+ top_p=req.top_p,
157
+ ),
158
  media_type="text/event-stream",
 
 
 
 
159
  )
160
+ else:
161
+ response = llm.create_chat_completion(
162
+ messages=messages_dict,
163
+ max_tokens=req.max_tokens,
164
+ temperature=req.temperature,
165
+ top_p=req.top_p,
166
+ stream=False,
167
+ )
168
+ return JSONResponse(content=response)
169
+
 
 
 
 
 
 
170
  except Exception as e:
171
+ raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
172
 
173
+ # ---------------------------------------------------------------------------
174
+ # ⚡ 5. GRADIO UI & HISTORY CLEANER
175
+ # ---------------------------------------------------------------------------
176
+ def clean_assistant_reply(text):
 
 
 
177
  if not text:
178
  return ""
 
179
  if isinstance(text, list):
180
+ extracted = []
181
+ for part in text:
182
+ if isinstance(part, str):
183
+ extracted.append(part)
184
+ elif isinstance(part, dict):
185
+ extracted.append(part.get("text", part.get("content", "")))
186
+ text = "".join(extracted)
 
 
 
 
 
 
 
 
187
  elif not isinstance(text, str):
188
  text = str(text)
189
 
190
+ cleaned = re.sub(r'<think>.*?</think>\n*', '', text, flags=re.DOTALL)
191
+ cleaned = cleaned.replace("</think>", "")
192
+ cleaned = re.sub(r'\n\n---\n\*⏱️.*?\*$', '', cleaned)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  return cleaned.strip()
194
 
195
+ def respond(message, history, system_message, max_tokens, temperature, top_p):
196
+ messages = [{"role": "system", "content": system_message}]
197
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  for item in history:
199
  if isinstance(item, dict):
200
  role = item.get("role", "user")
201
  content = item.get("content", "")
 
202
  if role == "assistant":
203
  content = clean_assistant_reply(content)
204
+ messages.append({"role": role, "content": content})
 
 
 
 
 
 
 
205
  elif isinstance(item, (list, tuple)):
206
+ if item[0]:
207
+ messages.append({"role": "user", "content": item[0]})
208
+ if item[1]:
209
+ cleaned_reply = clean_assistant_reply(item[1])
210
+ messages.append({"role": "assistant", "content": cleaned_reply})
 
 
 
 
 
 
 
 
 
 
 
 
211
 
212
+ messages.append({"role": "user", "content": message})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
 
214
  response = ""
215
  token_count = 0
216
  start_time = time.time()
217
+
218
+ for chunk in llm.create_chat_completion(
219
+ messages=messages,
220
+ max_tokens=max_tokens,
221
+ temperature=temperature,
222
+ top_p=top_p,
223
+ stream=True,
224
+ ):
225
+ delta = chunk["choices"][0]["delta"]
226
+ if "content" in delta and delta["content"]:
227
+ response += delta["content"]
228
+ token_count += 1
229
+ yield response
230
+
231
+ end_time = time.time()
232
+ elapsed_time = end_time - start_time
233
+
234
+ if elapsed_time > 0 and token_count > 0:
235
+ tps = token_count / elapsed_time
236
+ stats_footer = f"\n\n---\n*⏱️ {tps:.2f} tok/s ({token_count} tokens in {elapsed_time:.2f}s)*"
237
+ response += stats_footer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  yield response
239
 
 
 
 
 
 
240
  chatbot = gr.ChatInterface(
241
  fn=respond,
 
242
  additional_inputs=[
243
+ gr.Textbox(value="You are a helpful coding assistant.", label="System message"),
244
+ gr.Slider(minimum=1, maximum=2048, value=512, step=1, label="Max new tokens"),
245
+ gr.Slider(minimum=0.1, maximum=2.0, value=0.7, step=0.1, label="Temperature"),
246
+ gr.Slider(minimum=0.1, maximum=1.0, value=0.95, step=0.05, label="Top-p"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  ],
248
+ title="FREEAPI RUNNING ON CPU Qwen 35B-A3B",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  )
250
 
251
+ # Mount Gradio UI inside FastAPI at the root path "/"
252
+ app = gr.mount_gradio_app(app, chatbot, path="/")
253
 
254
+ # ---------------------------------------------------------------------------
255
+ # ⚡ 6. SERVER LAUNCH
256
+ # ---------------------------------------------------------------------------
257
+ if __name__ == "__main__":
258
+ import uvicorn
259
+ # Port 7860 is required by Hugging Face Spaces for health checks
260
+ uvicorn.run(app, host="0.0.0.0", port=7860)