Dellboy commited on
Commit
89b4e16
·
verified ·
1 Parent(s): 83e90ad

Real fix: restore demo.launch() as entrypoint (required for ZeroGPU detection), expose /generate via Gradio's native api_name mechanism instead of a custom FastAPI route

Browse files
Files changed (3) hide show
  1. __pycache__/app.cpython-314.pyc +0 -0
  2. app.py +54 -57
  3. requirements.txt +0 -1
__pycache__/app.cpython-314.pyc CHANGED
Binary files a/__pycache__/app.cpython-314.pyc and b/__pycache__/app.cpython-314.pyc differ
 
app.py CHANGED
@@ -1,20 +1,26 @@
1
  """
2
  chatPDB inference API — HuggingFace Space (Gradio SDK, ZeroGPU)
3
 
4
- Exposes POST /generate (SSE) consumed by the Flask PTY app on the droplet.
5
- Gradio SDK is required for ZeroGPU (confirmed live 2026-07-23: requesting ZeroGPU hardware for a
6
- Docker-SDK Space returns "ZeroGPU Spaces only work with Gradio SDK") -- the Gradio UI itself is
7
- just a minimal landing page.
8
-
9
- Real fix (found + fixed 2026-07-23): mounting a custom route via `@demo.app.post(...)` on the
10
- Blocks object's own `.app` attribute does NOT reliably take effect on HF Spaces -- confirmed live
11
- against the actual deployed Space: `curl -X POST .../generate` returned 405 with `allow: GET`,
12
- meaning Gradio's own catch-all SPA route claimed the path instead (Gradio's launch machinery
13
- appears to reconstruct/finalize its FastAPI app at launch time, after the route was added to the
14
- earlier `demo.app` reference). Fixed using Gradio's own documented pattern for combining custom
15
- FastAPI routes with a Gradio UI: build a plain FastAPI `app` first, register routes on it, then
16
- mount Gradio as a sub-application at a distinct path (routes registered directly on the outer
17
- `app` are not shadowed by Gradio's own routing, which only applies within its mounted sub-path).
 
 
 
 
 
 
18
 
19
  Cold-start note: first request after idle downloads the GGUF and allocates the GPU
20
  (~60-120 s). Subsequent requests within the same GPU lease are fast.
@@ -25,9 +31,6 @@ import json
25
 
26
  import gradio as gr
27
  import spaces
28
- import uvicorn
29
- from fastapi import FastAPI, Request
30
- from fastapi.responses import StreamingResponse
31
  from huggingface_hub import hf_hub_download
32
 
33
  REPO_ID = "Dellboy/chatpdb_32b_v1-GGUF"
@@ -51,13 +54,13 @@ def _generate_tokens(
51
  max_tokens: int,
52
  temperature: float,
53
  repeat_penalty: float,
54
- ) -> list[str]:
55
- """Run inside the ZeroGPU lease; collect all tokens and return.
56
 
57
  ZeroGPU-decorated functions run in a bounded lease and can't hold a live generator open
58
- across the function boundary, so tokens are collected here and streamed out afterward by the
59
- /generate route below -- the client sees compute-then-deliver rather than true live
60
- token-by-token latency, a real trade-off of the ZeroGPU model.
61
  """
62
  from llama_cpp import Llama
63
 
@@ -67,7 +70,7 @@ def _generate_tokens(
67
  n_gpu_layers=N_GPU_LAYERS,
68
  verbose=False,
69
  )
70
- tokens: list[str] = []
71
  for chunk in llm(
72
  prompt,
73
  max_tokens=max_tokens,
@@ -77,51 +80,45 @@ def _generate_tokens(
77
  ):
78
  tok = chunk["choices"][0]["text"]
79
  if tok:
80
- tokens.append(tok)
81
- return tokens
82
 
83
 
84
- # -- Plain FastAPI app owns the real API routes --
 
 
 
 
85
 
86
- app = FastAPI(title="chatPDB API")
87
-
88
-
89
- @app.post("/generate")
90
- async def generate(request: Request):
91
- body = await request.json()
92
- prompt = body.get("prompt", "")
93
- max_tokens = int(body.get("max_tokens", 512))
94
- temperature = float(body.get("temperature", 0.15))
95
- repeat_penalty = float(body.get("repeat_penalty", 1.15))
96
-
97
- tokens = _generate_tokens(prompt, max_tokens, temperature, repeat_penalty)
98
-
99
- def event_stream():
100
- for tok in tokens:
101
- yield f"data: {json.dumps({'token': tok})}\n\n"
102
- yield "data: [DONE]\n\n"
103
-
104
- return StreamingResponse(event_stream(), media_type="text/event-stream")
105
-
106
-
107
- @app.get("/health")
108
- async def health():
109
- return {"status": "ok"}
110
-
111
-
112
- # -- Gradio UI (minimal -- required for Gradio SDK / ZeroGPU), mounted at a sub-path so it can't
113
- # shadow the routes registered directly on `app` above --
114
 
115
  with gr.Blocks(title="chatPDB API") as demo:
116
  gr.Markdown(
117
  "## 🧬 chatPDB Inference API\n\n"
118
  "Internal endpoint for [chatpdb.mdeller.com](https://chatpdb.mdeller.com). "
119
- "Use `POST /generate` returns `text/event-stream` of token chunks.\n\n"
 
120
  "**Cold start:** first request after idle takes ~60-120 s (GGUF download + GPU alloc)."
121
  )
122
 
123
- app = gr.mount_gradio_app(app, demo, path="/ui")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
 
126
  if __name__ == "__main__":
127
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
  """
2
  chatPDB inference API — HuggingFace Space (Gradio SDK, ZeroGPU)
3
 
4
+ Exposes a Gradio API endpoint (api_name="generate") consumed by the Flask PTY app on the droplet
5
+ via Gradio's own REST protocol (POST /call/generate -> event_id, then GET /call/generate/<id> for
6
+ an SSE stream) -- NOT a hand-rolled custom FastAPI route.
7
+
8
+ Real fix history (found + fixed 2026-07-23, in order):
9
+ 1. First attempt added a custom route via `@demo.app.post("/generate")` -- confirmed live via
10
+ curl that this silently doesn't take effect (405, `allow: GET`); Gradio's own catch-all SPA
11
+ route claims the path instead.
12
+ 2. Second attempt used `gr.mount_gradio_app()` to mount Gradio under a FastAPI app that owns the
13
+ real routes -- this DID fix routing, but broke ZeroGPU entirely: the Space failed to start
14
+ with "No @spaces.GPU function detected during startup". Confirmed via web research (HF forum
15
+ threads on this exact error) that ZeroGPU's detection hook requires Gradio's own `demo.launch()`
16
+ to be the actual serving entrypoint -- a custom-FastAPI-primary architecture is not supported,
17
+ regardless of whether spaces.GPU is imported/used.
18
+ 3. This version: `demo.launch()` is the real entrypoint again (restoring ZeroGPU detection), and
19
+ the /generate contract is exposed via Gradio's own native API mechanism instead of a custom
20
+ route -- a hidden Textbox pair wired to a `.click()` handler with `api_name="generate"`, which
21
+ Gradio automatically exposes as `POST /call/generate` + `GET /call/generate/<event_id>` (SSE).
22
+ This stays entirely within Gradio's own request-handling pipeline, which is what ZeroGPU's
23
+ detection actually requires.
24
 
25
  Cold-start note: first request after idle downloads the GGUF and allocates the GPU
26
  (~60-120 s). Subsequent requests within the same GPU lease are fast.
 
31
 
32
  import gradio as gr
33
  import spaces
 
 
 
34
  from huggingface_hub import hf_hub_download
35
 
36
  REPO_ID = "Dellboy/chatpdb_32b_v1-GGUF"
 
54
  max_tokens: int,
55
  temperature: float,
56
  repeat_penalty: float,
57
+ ) -> str:
58
+ """Run inside the ZeroGPU lease; collect all tokens and return as one joined string.
59
 
60
  ZeroGPU-decorated functions run in a bounded lease and can't hold a live generator open
61
+ across the function boundary, so tokens are collected here rather than streamed token-by-token
62
+ -- the client sees compute-then-deliver rather than true live latency, a real trade-off of the
63
+ ZeroGPU model.
64
  """
65
  from llama_cpp import Llama
66
 
 
70
  n_gpu_layers=N_GPU_LAYERS,
71
  verbose=False,
72
  )
73
+ parts: list[str] = []
74
  for chunk in llm(
75
  prompt,
76
  max_tokens=max_tokens,
 
80
  ):
81
  tok = chunk["choices"][0]["text"]
82
  if tok:
83
+ parts.append(tok)
84
+ return "".join(parts)
85
 
86
 
87
+ def _generate_api(prompt: str, max_tokens: int, temperature: float, repeat_penalty: float) -> str:
88
+ """Thin wrapper: chat_remote.py sends max_tokens/temperature/repeat_penalty as a JSON string
89
+ packed into one field isn't needed -- Gradio's API takes positional args directly, matching
90
+ the .click() inputs list below."""
91
+ return _generate_tokens(prompt, int(max_tokens), float(temperature), float(repeat_penalty))
92
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
  with gr.Blocks(title="chatPDB API") as demo:
95
  gr.Markdown(
96
  "## 🧬 chatPDB Inference API\n\n"
97
  "Internal endpoint for [chatpdb.mdeller.com](https://chatpdb.mdeller.com). "
98
+ "Consumed via Gradio's own API: `POST /call/generate` then `GET /call/generate/<event_id>` "
99
+ "(SSE).\n\n"
100
  "**Cold start:** first request after idle takes ~60-120 s (GGUF download + GPU alloc)."
101
  )
102
 
103
+ # Hidden inputs/outputs purely to register a real Gradio event with api_name="generate" --
104
+ # ZeroGPU's startup detection scans Gradio's own event/dependency graph, so the GPU-decorated
105
+ # function must be reachable through a real .click()/.submit() binding, not just referenced
106
+ # from a custom route.
107
+ with gr.Row(visible=False):
108
+ prompt_in = gr.Textbox()
109
+ max_tokens_in = gr.Number(value=512)
110
+ temperature_in = gr.Number(value=0.15)
111
+ repeat_penalty_in = gr.Number(value=1.15)
112
+ text_out = gr.Textbox()
113
+ trigger = gr.Button()
114
+
115
+ trigger.click(
116
+ fn=_generate_api,
117
+ inputs=[prompt_in, max_tokens_in, temperature_in, repeat_penalty_in],
118
+ outputs=text_out,
119
+ api_name="generate",
120
+ )
121
 
122
 
123
  if __name__ == "__main__":
124
+ demo.launch()
requirements.txt CHANGED
@@ -3,4 +3,3 @@ llama-cpp-python
3
  gradio>=4.0
4
  spaces
5
  huggingface_hub
6
- uvicorn
 
3
  gradio>=4.0
4
  spaces
5
  huggingface_hub