fomext commited on
Commit
691b54e
·
verified ·
1 Parent(s): 7aa73ae

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +74 -89
app.py CHANGED
@@ -8,8 +8,7 @@ from typing import AsyncIterator, Optional
8
  import gradio as gr
9
  import spaces
10
  import torch
11
- import uvicorn
12
- from fastapi import FastAPI, HTTPException
13
  from fastapi.responses import JSONResponse, StreamingResponse
14
  from pydantic import BaseModel, Field
15
  from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
@@ -21,11 +20,6 @@ from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStream
21
  MODEL_ID = "Qwen/Qwen3-30B-A3B"
22
  MODEL_ALIAS = "qwen3-30b-a3b"
23
 
24
- # ---------------------------------------------------------------------------
25
- # Model loading — tokenizer on CPU at startup; model loaded with device_map
26
- # so ZeroGPU can manage GPU placement per request.
27
- # ---------------------------------------------------------------------------
28
-
29
  print(f"Loading tokenizer for {MODEL_ID} …")
30
  tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
31
 
@@ -38,12 +32,6 @@ model = AutoModelForCausalLM.from_pretrained(
38
  model.eval()
39
  print("Model ready.")
40
 
41
- # ---------------------------------------------------------------------------
42
- # FastAPI app
43
- # ---------------------------------------------------------------------------
44
-
45
- app = FastAPI(title="Qwen3-30B-A3B OpenAI-compatible API")
46
-
47
  # ---------------------------------------------------------------------------
48
  # Pydantic schemas
49
  # ---------------------------------------------------------------------------
@@ -90,17 +78,12 @@ def make_generation_kwargs(request: ChatCompletionRequest) -> dict:
90
 
91
 
92
  # ---------------------------------------------------------------------------
93
- # GPU generation functions
94
- # NOTE: ZeroGPU requires at least one @spaces.GPU function to be wired into
95
- # the Gradio UI (not just defined). We satisfy this by using `gradio_chat`
96
- # as both the Gradio interface handler AND calling the same underlying logic
97
- # that the FastAPI routes use.
98
  # ---------------------------------------------------------------------------
99
 
100
 
101
  @spaces.GPU
102
  def gradio_chat(message: str, history: list) -> str:
103
- """Gradio-facing chat handler — also acts as the ZeroGPU anchor function."""
104
  hf_messages = [{"role": "user" if i % 2 == 0 else "assistant", "content": m}
105
  for i, m in enumerate([msg for pair in history for msg in pair] + [message])]
106
  prompt = tokenizer.apply_chat_template(
@@ -122,7 +105,6 @@ def gradio_chat(message: str, history: list) -> str:
122
 
123
  @spaces.GPU
124
  def _generate_response(prompt: str, gen_kwargs: dict) -> str:
125
- """Non-streaming generation for FastAPI."""
126
  inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
127
  with torch.no_grad():
128
  output_ids = model.generate(**inputs, **gen_kwargs)
@@ -132,7 +114,6 @@ def _generate_response(prompt: str, gen_kwargs: dict) -> str:
132
 
133
  @spaces.GPU
134
  def _generate_streaming(prompt: str, gen_kwargs: dict, streamer: TextIteratorStreamer):
135
- """Streaming generation for FastAPI."""
136
  inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
137
  with torch.no_grad():
138
  model.generate(**inputs, streamer=streamer, **gen_kwargs)
@@ -167,95 +148,99 @@ def stream_chunk(delta_content: str, model_name: str, completion_id: str, finish
167
 
168
 
169
  # ---------------------------------------------------------------------------
170
- # FastAPI routes
171
  # ---------------------------------------------------------------------------
172
 
 
 
 
173
 
174
- @app.get("/v1/models")
175
- async def list_models():
176
- return {
177
- "object": "list",
178
- "data": [{"id": MODEL_ALIAS, "object": "model", "created": int(time.time()), "owned_by": "qwen"}],
179
- }
180
-
181
-
182
- @app.post("/v1/chat/completions")
183
- async def chat_completions(request: ChatCompletionRequest):
184
- try:
185
- prompt = build_prompt(request.messages, request.enable_thinking or False)
186
- gen_kwargs = make_generation_kwargs(request)
187
- except Exception as exc:
188
- raise HTTPException(status_code=422, detail=str(exc))
189
-
190
- if request.stream:
191
- completion_id = f"chatcmpl-{uuid.uuid4().hex}"
192
 
193
- async def token_generator() -> AsyncIterator[str]:
194
- role_chunk = {
195
- "id": completion_id, "object": "chat.completion.chunk",
196
- "created": int(time.time()), "model": request.model,
197
- "choices": [{"index": 0, "delta": {"role": "assistant"}, "finish_reason": None}],
198
- }
199
- yield f"data: {json.dumps(role_chunk)}\n\n"
200
 
201
- streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
202
- thread = Thread(target=_generate_streaming, args=(prompt, gen_kwargs, streamer), daemon=True)
203
- thread.start()
204
 
205
- try:
206
- for token_text in streamer:
207
- if token_text:
208
- yield stream_chunk(token_text, request.model, completion_id)
209
- await asyncio.sleep(0)
210
- finally:
211
- thread.join()
212
 
213
- yield stream_chunk("", request.model, completion_id, finish_reason="stop")
214
- yield "data: [DONE]\n\n"
215
 
216
- return StreamingResponse(token_generator(), media_type="text/event-stream",
217
- headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
 
 
 
 
 
218
 
219
- try:
220
- content = _generate_response(prompt, gen_kwargs)
221
- except Exception as exc:
222
- raise HTTPException(status_code=500, detail=f"Generation failed: {exc}")
 
 
 
223
 
224
- return JSONResponse(chat_completion_object(content, request.model))
 
225
 
 
 
 
 
 
 
 
226
 
227
- @app.get("/health")
228
- async def health():
229
- return {"status": "ok", "model": MODEL_ID}
230
 
 
 
 
 
 
 
 
231
 
232
- # ---------------------------------------------------------------------------
233
- # Gradio UI — must have a real wired @spaces.GPU function for ZeroGPU
234
- # ---------------------------------------------------------------------------
235
 
236
- with gr.Blocks(title=f"{MODEL_ALIAS} API") as demo:
237
- gr.Markdown(f"""
238
- # {MODEL_ALIAS} — OpenAI-compatible API
239
 
240
- Point **Paperclip** at `https://<your-space>.hf.space` with model `{MODEL_ALIAS}`.
 
 
 
241
 
242
- | Method | Path | Description |
243
- |--------|------|-------------|
244
- | GET | `/v1/models` | List models |
245
- | POST | `/v1/chat/completions` | Chat (streaming & non-streaming) |
246
- | GET | `/health` | Health check |
247
 
248
- Chat UI is served at the root path below; API routes remain available alongside it.
249
- """)
250
- # This ChatInterface wires `gradio_chat` into Gradio's event system,
251
- # which is what ZeroGPU's startup scanner requires.
252
- gr.ChatInterface(fn=gradio_chat)
253
 
254
- app = gr.mount_gradio_app(app, demo, path="/")
255
 
256
  # ---------------------------------------------------------------------------
257
- # Entry-point
 
258
  # ---------------------------------------------------------------------------
259
 
260
  if __name__ == "__main__":
261
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
 
 
 
 
 
 
8
  import gradio as gr
9
  import spaces
10
  import torch
11
+ from fastapi import HTTPException
 
12
  from fastapi.responses import JSONResponse, StreamingResponse
13
  from pydantic import BaseModel, Field
14
  from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
 
20
  MODEL_ID = "Qwen/Qwen3-30B-A3B"
21
  MODEL_ALIAS = "qwen3-30b-a3b"
22
 
 
 
 
 
 
23
  print(f"Loading tokenizer for {MODEL_ID} …")
24
  tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
25
 
 
32
  model.eval()
33
  print("Model ready.")
34
 
 
 
 
 
 
 
35
  # ---------------------------------------------------------------------------
36
  # Pydantic schemas
37
  # ---------------------------------------------------------------------------
 
78
 
79
 
80
  # ---------------------------------------------------------------------------
81
+ # GPU generation functions — these are the ZeroGPU anchors.
 
 
 
 
82
  # ---------------------------------------------------------------------------
83
 
84
 
85
  @spaces.GPU
86
  def gradio_chat(message: str, history: list) -> str:
 
87
  hf_messages = [{"role": "user" if i % 2 == 0 else "assistant", "content": m}
88
  for i, m in enumerate([msg for pair in history for msg in pair] + [message])]
89
  prompt = tokenizer.apply_chat_template(
 
105
 
106
  @spaces.GPU
107
  def _generate_response(prompt: str, gen_kwargs: dict) -> str:
 
108
  inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
109
  with torch.no_grad():
110
  output_ids = model.generate(**inputs, **gen_kwargs)
 
114
 
115
  @spaces.GPU
116
  def _generate_streaming(prompt: str, gen_kwargs: dict, streamer: TextIteratorStreamer):
 
117
  inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
118
  with torch.no_grad():
119
  model.generate(**inputs, streamer=streamer, **gen_kwargs)
 
148
 
149
 
150
  # ---------------------------------------------------------------------------
151
+ # Gradio UI
152
  # ---------------------------------------------------------------------------
153
 
154
+ with gr.Blocks(title=f"{MODEL_ALIAS} API") as demo:
155
+ gr.Markdown(f"""
156
+ # {MODEL_ALIAS} — OpenAI-compatible API
157
 
158
+ Point **Paperclip** at `https://<your-space>.hf.space` with model `{MODEL_ALIAS}`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
+ | Method | Path | Description |
161
+ |--------|------|-------------|
162
+ | GET | `/v1/models` | List models |
163
+ | POST | `/v1/chat/completions` | Chat (streaming & non-streaming) |
164
+ | GET | `/health` | Health check |
 
 
165
 
166
+ You can also chat directly below.
167
+ """)
168
+ gr.ChatInterface(fn=gradio_chat)
169
 
170
+ # ---------------------------------------------------------------------------
171
+ # Register FastAPI-style routes onto Gradio's own FastAPI app via app_kwargs,
172
+ # BEFORE launch() — so ZeroGPU's launch hook still does its registration scan.
173
+ # ---------------------------------------------------------------------------
 
 
 
174
 
 
 
175
 
176
+ def add_custom_routes(fastapi_app):
177
+ @fastapi_app.get("/v1/models")
178
+ async def list_models():
179
+ return {
180
+ "object": "list",
181
+ "data": [{"id": MODEL_ALIAS, "object": "model", "created": int(time.time()), "owned_by": "qwen"}],
182
+ }
183
 
184
+ @fastapi_app.post("/v1/chat/completions")
185
+ async def chat_completions(request: ChatCompletionRequest):
186
+ try:
187
+ prompt = build_prompt(request.messages, request.enable_thinking or False)
188
+ gen_kwargs = make_generation_kwargs(request)
189
+ except Exception as exc:
190
+ raise HTTPException(status_code=422, detail=str(exc))
191
 
192
+ if request.stream:
193
+ completion_id = f"chatcmpl-{uuid.uuid4().hex}"
194
 
195
+ async def token_generator() -> AsyncIterator[str]:
196
+ role_chunk = {
197
+ "id": completion_id, "object": "chat.completion.chunk",
198
+ "created": int(time.time()), "model": request.model,
199
+ "choices": [{"index": 0, "delta": {"role": "assistant"}, "finish_reason": None}],
200
+ }
201
+ yield f"data: {json.dumps(role_chunk)}\n\n"
202
 
203
+ streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
204
+ thread = Thread(target=_generate_streaming, args=(prompt, gen_kwargs, streamer), daemon=True)
205
+ thread.start()
206
 
207
+ try:
208
+ for token_text in streamer:
209
+ if token_text:
210
+ yield stream_chunk(token_text, request.model, completion_id)
211
+ await asyncio.sleep(0)
212
+ finally:
213
+ thread.join()
214
 
215
+ yield stream_chunk("", request.model, completion_id, finish_reason="stop")
216
+ yield "data: [DONE]\n\n"
 
217
 
218
+ return StreamingResponse(token_generator(), media_type="text/event-stream",
219
+ headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
 
220
 
221
+ try:
222
+ content = _generate_response(prompt, gen_kwargs)
223
+ except Exception as exc:
224
+ raise HTTPException(status_code=500, detail=f"Generation failed: {exc}")
225
 
226
+ return JSONResponse(chat_completion_object(content, request.model))
 
 
 
 
227
 
228
+ @fastapi_app.get("/health")
229
+ async def health():
230
+ return {"status": "ok", "model": MODEL_ID}
 
 
231
 
 
232
 
233
  # ---------------------------------------------------------------------------
234
+ # Entry-point — demo.launch() is what triggers spaces.one_launch's patched
235
+ # hook, which is what registers @spaces.GPU functions with the HF platform.
236
  # ---------------------------------------------------------------------------
237
 
238
  if __name__ == "__main__":
239
+ demo.queue()
240
+ add_custom_routes(demo.app)
241
+ demo.launch(
242
+ server_name="0.0.0.0",
243
+ server_port=7860,
244
+ app_kwargs={"docs_url": None},
245
+ )
246
+