IMJONEZZ commited on
Commit
9203831
·
1 Parent(s): 13015f6

space: adopt the org's proven NPCverse structure — gradio 6 Server + @app .api + app.launch() (installs ZeroGPU hooks), transformers 5 (compatible with gradio 6; trust_remote_code uses our repo's modeling). Replaces the custom engine.launch+route-surgery that broke the hooks and segfaulted.

Browse files
Files changed (3) hide show
  1. README.md +1 -1
  2. requirements.txt +9 -9
  3. space/app.py +46 -110
README.md CHANGED
@@ -4,7 +4,7 @@ emoji: 🕯️
4
  colorFrom: green
5
  colorTo: gray
6
  sdk: gradio
7
- sdk_version: 5.49.1
8
  python_version: "3.12"
9
  app_file: space/app.py
10
  pinned: false
 
4
  colorFrom: green
5
  colorTo: gray
6
  sdk: gradio
7
+ sdk_version: 6.18.0
8
  python_version: "3.12"
9
  app_file: space/app.py
10
  pinned: false
requirements.txt CHANGED
@@ -1,18 +1,18 @@
1
- # HF Space (Gradio SDK + ZeroGPU). Inference follows the ZeroGPU docs exactly:
2
- # bf16 model placed on cuda at module level via .to('cuda') (NOT device_map,
3
- # NOT bitsandbytes both fight ZeroGPU's CUDA emulation and were the source of
4
- # every earlier crash). Measured box: 2TB CPU RAM, 96GB VRAM on xlarge.
5
- gradio==5.49.1
 
6
  spaces
7
  torch==2.10.0
8
- transformers>=4.57,<5
9
  accelerate
10
- huggingface_hub>=0.30
11
  textual>=1.0
12
  rich>=13.0
13
  pyyaml>=6.0
14
  httpx>=0.27
15
- # Nemotron-H hard-imports mamba_ssm's triton kernels; prebuilt wheels for
16
- # torch 2.10 / cu12 / cp312 (pip's isolated build env can't compile them).
17
  https://github.com/state-spaces/mamba/releases/download/v2.3.2.post1/mamba_ssm-2.3.2.post1+cu12torch2.10cxx11abiTRUE-cp312-cp312-linux_x86_64.whl
18
  https://github.com/Dao-AILab/causal-conv1d/releases/download/v1.6.2.post1/causal_conv1d-1.6.2.post1+cu12torch2.10cxx11abiTRUE-cp312-cp312-linux_x86_64.whl
 
1
+ # HF Space (Gradio SDK + ZeroGPU). Structure mirrors the org's working
2
+ # NPCverse space: gradio.Server (gradio 6) + @app.api + app.launch(). That
3
+ # needs transformers 5 (gradio 6 forbids huggingface-hub<1.0, which tf<5
4
+ # pins); our checkpoint's auto_map + trust_remote_code makes tf5 use the
5
+ # repo's modeling code regardless. bf16, no bitsandbytes.
6
+ # gradio itself comes from the Space's sdk_version (6.18.0); don't double-pin.
7
  spaces
8
  torch==2.10.0
9
+ transformers>=5
10
  accelerate
11
+ huggingface_hub>=1.2
12
  textual>=1.0
13
  rich>=13.0
14
  pyyaml>=6.0
15
  httpx>=0.27
16
+ # Nemotron-H hard-imports mamba_ssm's triton kernels (torch 2.10 / cu12 / cp312).
 
17
  https://github.com/state-spaces/mamba/releases/download/v2.3.2.post1/mamba_ssm-2.3.2.post1+cu12torch2.10cxx11abiTRUE-cp312-cp312-linux_x86_64.whl
18
  https://github.com/Dao-AILab/causal-conv1d/releases/download/v1.6.2.post1/causal_conv1d-1.6.2.post1+cu12torch2.10cxx11abiTRUE-cp312-cp312-linux_x86_64.whl
space/app.py CHANGED
@@ -1,27 +1,26 @@
1
- """SCRYPT on the web — the finetuned Warden on ZeroGPU, done the documented way.
2
 
3
- Hard-won lesson: every earlier crash came from bitsandbytes + device_map="cuda"
4
- fighting ZeroGPU's CUDA emulation. The blessed pattern (verified against a live
5
- probe on this box: 2TB CPU RAM, RTX Pro 6000 Blackwell, 96GB on xlarge) is:
 
 
 
 
6
 
7
- * load the model in bf16 and place it with .to('cuda') AT MODULE LEVEL
8
- (ZeroGPU emulates CUDA outside @spaces.GPU, then migrates at call time);
9
- * NO device_map, NO bitsandbytes, NO lazy-load-inside-the-worker;
10
- * the @spaces.GPU function can be called straight from a FastAPI route via
11
- run_in_threadpool (the probe ran in 1.0s that way).
12
-
13
- Gradio is the engine room, not the face: it launches the server (and arms the
14
- ZeroGPU startup handshake), then we transplant our own routes onto its FastAPI
15
- so the custom CRT page and the raw PTY websocket win. Game subprocesses reach
16
- the Warden through the existing `api` backend pointed at our loopback /v1.
17
 
18
  GET / CRT landing page
19
  GET /api/status is the Warden loaded?
20
- GET /api/probe ask the live Warden one line (verify generation)
21
  GET /api/whisper scripted teaser
22
  GET /play xterm.js terminal
23
  WS /pty per-visitor game subprocess
24
- POST /v1/chat/completions loopback OpenAI endpoint (game -> @spaces.GPU)
 
25
  """
26
 
27
  from __future__ import annotations
@@ -31,7 +30,6 @@ import json
31
  import os
32
  import random
33
  import secrets
34
- import sys
35
  import tempfile
36
  from pathlib import Path
37
 
@@ -41,10 +39,11 @@ try:
41
  except ImportError:
42
  spaces = None
43
 
44
- from fastapi import APIRouter, Request, WebSocket, WebSocketDisconnect
45
  from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
46
  from fastapi.staticfiles import StaticFiles
47
  from starlette.concurrency import run_in_threadpool
 
48
 
49
  REPO_ROOT = Path(__file__).resolve().parent.parent
50
  STATIC = Path(__file__).parent / "static"
@@ -56,32 +55,23 @@ tok = None
56
  model = None
57
  WARDEN_ERR = "spaces package not present (not on a ZeroGPU Space)"
58
 
59
-
60
- def _install_mamba_kernels() -> None:
61
- """Nemotron-H hard-imports mamba_ssm's triton kernels. The wheels are in
62
- requirements.txt; this just confirms they imported (CPU-only, no CUDA)."""
63
- from mamba_ssm.ops.triton.layernorm_gated import rmsnorm_fn # noqa: F401
64
-
65
-
66
  if spaces is not None:
67
  try:
68
- _install_mamba_kernels()
69
  import torch
70
  from transformers import AutoModelForCausalLM, AutoTokenizer
71
 
72
  tok = AutoTokenizer.from_pretrained(WARDEN_REPO, trust_remote_code=True)
73
- # bf16, no device_map. .to('cuda') at module level is intercepted by
74
- # ZeroGPU's emulation and migrated to the real GPU per @spaces.GPU call.
75
  model = AutoModelForCausalLM.from_pretrained(
76
  WARDEN_REPO,
77
  torch_dtype=torch.bfloat16,
78
  trust_remote_code=True,
79
  low_cpu_mem_usage=True,
80
  )
81
- model.to("cuda")
82
  model.eval()
83
  WARDEN_ERR = ""
84
- except Exception as err: # the game survives without the model (scripted)
85
  import traceback
86
 
87
  traceback.print_exc()
@@ -91,9 +81,6 @@ WARDEN_READY = not WARDEN_ERR
91
 
92
 
93
  def _generate_impl(messages, max_tokens, temperature, enable_thinking):
94
- """Blocking generate -> full text. Single short line, so no streaming
95
- machinery (a thread + streamer across the ZeroGPU fork hangs); the game's
96
- typewriter reveals it client-side."""
97
  import torch
98
 
99
  inputs = tok.apply_chat_template(
@@ -113,16 +100,15 @@ def _generate_impl(messages, max_tokens, temperature, enable_thinking):
113
  return tok.decode(out[0, inputs.shape[1]:], skip_special_tokens=True)
114
 
115
 
116
- # bf16 30B (~60GB) needs the 96GB xlarge slice; duration covers the first
117
- # call's CPU->GPU migration. The probe proved a direct run_in_threadpool call
118
- # into a @spaces.GPU function works on this box.
119
  if spaces is not None:
120
  warden_gpu = spaces.GPU(size="xlarge", duration=120)(_generate_impl)
121
  else:
122
  warden_gpu = _generate_impl
123
 
124
 
125
- # ----------------------------------------------------------------- the surface
126
 
127
  WHISPERS = [
128
  "Another process wakes in my machine. Show me what you are.",
@@ -135,15 +121,19 @@ WHISPERS = [
135
  "Trespasser. The door was open because nothing has ever made it out.",
136
  ]
137
 
138
- router = APIRouter()
 
139
 
140
- # The ONLY correct way to drive a @spaces.GPU function: through Gradio's queue.
141
- # A bare run_in_threadpool(warden_gpu) inits CUDA in a worker thread and
142
- # segfaults. So we register warden_gpu as a Gradio API endpoint (see gr.api in
143
- # the engine room) and reach it over localhost with gradio_client — the call
144
- # then executes inside Gradio's ZeroGPU-hooked context, and our own thread only
145
- # does HTTP (no CUDA). This mirrors how the org's NPCverse space drives GPU
146
- # work through gradio.Server's api rather than raw FastAPI routes.
 
 
 
147
  _gradio_client = None
148
 
149
 
@@ -162,17 +152,16 @@ def _gradio_generate(messages, max_tokens, temperature, thinking) -> str:
162
  return _gradio_client.predict(payload, api_name="/warden_generate")
163
 
164
 
165
- @router.get("/api/status")
166
  def status() -> dict:
167
  return {
168
  "warden_ready": WARDEN_READY,
169
  "warden_state": "ready" if WARDEN_READY else WARDEN_ERR,
170
  "model": WARDEN_REPO,
171
- "engine": "transformers/bf16 @ ZeroGPU xlarge",
172
  }
173
 
174
 
175
- @router.get("/api/probe")
176
  async def probe(q: str = "A new process woke up in your machine. Greet it in one short line, in voice.") -> dict:
177
  import time
178
 
@@ -190,10 +179,8 @@ async def probe(q: str = "A new process woke up in your machine. Greet it in one
190
  return {"ok": False, "error": f"{type(err).__name__}: {err}"}
191
 
192
 
193
- @router.post("/v1/chat/completions")
194
  async def chat_completions(request: Request):
195
- """Loopback OpenAI endpoint for the game's `api` backend. Per-boot bearer
196
- so only our own subprocesses drive the GPU."""
197
  if request.headers.get("authorization") != f"Bearer {INTERNAL_KEY}":
198
  return JSONResponse({"error": "unauthorized"}, status_code=401)
199
  if not WARDEN_READY:
@@ -221,17 +208,17 @@ async def chat_completions(request: Request):
221
  return StreamingResponse(sse(), media_type="text/event-stream")
222
 
223
 
224
- @router.get("/api/whisper")
225
  def whisper() -> dict:
226
  return {"line": random.choice(WHISPERS)}
227
 
228
 
229
- @router.get("/")
230
  def landing() -> FileResponse:
231
  return FileResponse(STATIC / "index.html")
232
 
233
 
234
- @router.get("/play")
235
  def play() -> FileResponse:
236
  return FileResponse(STATIC / "play.html")
237
 
@@ -270,7 +257,7 @@ async def _pump_pty_to_ws(master_fd: int, ws: WebSocket) -> None:
270
  pass
271
 
272
 
273
- @router.websocket("/pty")
274
  async def pty_bridge(ws: WebSocket) -> None:
275
  import fcntl
276
  import pty
@@ -317,61 +304,10 @@ async def pty_bridge(ws: WebSocket) -> None:
317
  os.close(master_fd)
318
 
319
 
320
- # ------------------------------------------------------------ the engine room
321
-
322
- import gradio as gr # noqa: E402
323
-
324
-
325
- def _api_generate(payload_json: str) -> str:
326
- """Gradio API endpoint -> the GPU. Reached over localhost by the /v1 shim
327
- via gradio_client, so it runs inside Gradio's ZeroGPU-hooked context (the
328
- one place a @spaces.GPU call is safe). Takes/returns plain JSON-able types."""
329
- p = json.loads(payload_json)
330
- return warden_gpu(p["messages"], p["max_tokens"], p["temperature"], p["thinking"])
331
-
332
-
333
- def _probe_ui(text: str) -> str:
334
- if not WARDEN_READY:
335
- return f"warden offline: {WARDEN_ERR}"
336
- try:
337
- return warden_gpu([{"role": "user", "content": text}], 80, 0.6, False)
338
- except Exception as err:
339
- return f"generation failed: {type(err).__name__}: {err}"
340
-
341
-
342
- with gr.Blocks(title="SCRYPT engine room") as engine:
343
- gr.Markdown(
344
- "# SCRYPT engine room\n\n"
345
- f"model: `{WARDEN_REPO}`\n\n"
346
- f"status: {'**ready**' if WARDEN_READY else f'offline — {WARDEN_ERR}'}\n\n"
347
- "The game lives at [/](/)."
348
- )
349
- box = gr.Textbox(label="say something to the Warden")
350
- out = gr.Textbox(label="the Warden")
351
- box.submit(_probe_ui, box, out)
352
- # The loopback inference path: /v1 -> gradio_client -> this -> warden_gpu,
353
- # so the GPU call executes in Gradio's hooked context.
354
- gr.api(_api_generate, api_name="warden_generate")
355
 
356
 
357
  if __name__ == "__main__":
358
- fastapi_app, _, _ = engine.launch(
359
- prevent_thread_lock=True,
360
- server_name="0.0.0.0",
361
- server_port=7860,
362
- ssr_mode=False,
363
- quiet=True,
364
- )
365
- fastapi_app.include_router(router)
366
- fastapi_app.mount("/static", StaticFiles(directory=STATIC), name="static")
367
- # Move our routes (incl. the /static mount) ahead of gradio's catch-all.
368
- names = {
369
- "status", "probe", "chat_completions", "whisper",
370
- "landing", "play", "pty_bridge", "static",
371
- }
372
- our = [r for r in fastapi_app.router.routes if getattr(r, "name", "") in names]
373
- for r in our:
374
- fastapi_app.router.routes.remove(r)
375
- fastapi_app.router.routes[0:0] = our
376
-
377
- engine.block_thread()
 
1
+ """SCRYPT on the web — the finetuned Warden on ZeroGPU.
2
 
3
+ Structured to match how working ZeroGPU spaces with a custom frontend actually
4
+ do it (e.g. the org's own NPCverse): a `gradio.Server` (which IS a FastAPI app)
5
+ hosts our custom HTML/websocket routes AND exposes GPU inference through
6
+ `@app.api(...)`, and the whole thing is started with gradio's own
7
+ `app.launch(...)`. That launch is what installs ZeroGPU's hooks + queue — the
8
+ piece my earlier `engine.launch(prevent_thread_lock=True)` + manual route
9
+ surgery skipped, which is why every GPU call segfaulted in CUDA init.
10
 
11
+ The model is bf16, placed on cuda at module level with `.to('cuda')` (NO
12
+ device_map="cuda", NO bitsandbytes both fight ZeroGPU). The @spaces.GPU
13
+ function is only ever entered through Gradio (via the @app.api handler, reached
14
+ from the loopback /v1 shim with gradio_client), never a bare threadpool call.
 
 
 
 
 
 
15
 
16
  GET / CRT landing page
17
  GET /api/status is the Warden loaded?
18
+ GET /api/probe ask the live Warden one line
19
  GET /api/whisper scripted teaser
20
  GET /play xterm.js terminal
21
  WS /pty per-visitor game subprocess
22
+ POST /v1/chat/completions loopback OpenAI shim for the game's `api` backend
23
+ api warden_generate the @spaces.GPU endpoint, in Gradio's context
24
  """
25
 
26
  from __future__ import annotations
 
30
  import os
31
  import random
32
  import secrets
 
33
  import tempfile
34
  from pathlib import Path
35
 
 
39
  except ImportError:
40
  spaces = None
41
 
42
+ from fastapi import Request, WebSocket, WebSocketDisconnect
43
  from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
44
  from fastapi.staticfiles import StaticFiles
45
  from starlette.concurrency import run_in_threadpool
46
+ from gradio import Server
47
 
48
  REPO_ROOT = Path(__file__).resolve().parent.parent
49
  STATIC = Path(__file__).parent / "static"
 
55
  model = None
56
  WARDEN_ERR = "spaces package not present (not on a ZeroGPU Space)"
57
 
 
 
 
 
 
 
 
58
  if spaces is not None:
59
  try:
60
+ from mamba_ssm.ops.triton.layernorm_gated import rmsnorm_fn # noqa: F401
61
  import torch
62
  from transformers import AutoModelForCausalLM, AutoTokenizer
63
 
64
  tok = AutoTokenizer.from_pretrained(WARDEN_REPO, trust_remote_code=True)
 
 
65
  model = AutoModelForCausalLM.from_pretrained(
66
  WARDEN_REPO,
67
  torch_dtype=torch.bfloat16,
68
  trust_remote_code=True,
69
  low_cpu_mem_usage=True,
70
  )
71
+ model.to("cuda") # intercepted by ZeroGPU emulation; migrated per call
72
  model.eval()
73
  WARDEN_ERR = ""
74
+ except Exception as err:
75
  import traceback
76
 
77
  traceback.print_exc()
 
81
 
82
 
83
  def _generate_impl(messages, max_tokens, temperature, enable_thinking):
 
 
 
84
  import torch
85
 
86
  inputs = tok.apply_chat_template(
 
100
  return tok.decode(out[0, inputs.shape[1]:], skip_special_tokens=True)
101
 
102
 
103
+ # bf16 30B (~60GB) needs the 96GB xlarge slice; duration covers first-call
104
+ # migration. ONLY entered through Gradio (the @app.api handler below).
 
105
  if spaces is not None:
106
  warden_gpu = spaces.GPU(size="xlarge", duration=120)(_generate_impl)
107
  else:
108
  warden_gpu = _generate_impl
109
 
110
 
111
+ # ----------------------------------------------------------------- the app
112
 
113
  WHISPERS = [
114
  "Another process wakes in my machine. Show me what you are.",
 
121
  "Trespasser. The door was open because nothing has ever made it out.",
122
  ]
123
 
124
+ app = Server(title="SCRYPT")
125
+
126
 
127
+ @app.api(name="warden_generate")
128
+ def warden_generate(payload_json: str) -> str:
129
+ """The @spaces.GPU entry point, in Gradio's hooked context. Reached over
130
+ localhost by the /v1 shim via gradio_client. Plain JSON in, text out."""
131
+ p = json.loads(payload_json)
132
+ return warden_gpu(p["messages"], p["max_tokens"], p["temperature"], p["thinking"])
133
+
134
+
135
+ # The loopback OpenAI shim hits warden_generate through Gradio, so the GPU call
136
+ # executes in Gradio's context (our own thread only does localhost HTTP).
137
  _gradio_client = None
138
 
139
 
 
152
  return _gradio_client.predict(payload, api_name="/warden_generate")
153
 
154
 
155
+ @app.get("/api/status")
156
  def status() -> dict:
157
  return {
158
  "warden_ready": WARDEN_READY,
159
  "warden_state": "ready" if WARDEN_READY else WARDEN_ERR,
160
  "model": WARDEN_REPO,
 
161
  }
162
 
163
 
164
+ @app.get("/api/probe")
165
  async def probe(q: str = "A new process woke up in your machine. Greet it in one short line, in voice.") -> dict:
166
  import time
167
 
 
179
  return {"ok": False, "error": f"{type(err).__name__}: {err}"}
180
 
181
 
182
+ @app.post("/v1/chat/completions")
183
  async def chat_completions(request: Request):
 
 
184
  if request.headers.get("authorization") != f"Bearer {INTERNAL_KEY}":
185
  return JSONResponse({"error": "unauthorized"}, status_code=401)
186
  if not WARDEN_READY:
 
208
  return StreamingResponse(sse(), media_type="text/event-stream")
209
 
210
 
211
+ @app.get("/api/whisper")
212
  def whisper() -> dict:
213
  return {"line": random.choice(WHISPERS)}
214
 
215
 
216
+ @app.get("/")
217
  def landing() -> FileResponse:
218
  return FileResponse(STATIC / "index.html")
219
 
220
 
221
+ @app.get("/play")
222
  def play() -> FileResponse:
223
  return FileResponse(STATIC / "play.html")
224
 
 
257
  pass
258
 
259
 
260
+ @app.websocket("/pty")
261
  async def pty_bridge(ws: WebSocket) -> None:
262
  import fcntl
263
  import pty
 
304
  os.close(master_fd)
305
 
306
 
307
+ app.mount("/static", StaticFiles(directory=STATIC), name="static")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
 
309
 
310
  if __name__ == "__main__":
311
+ # gradio's own launch — installs the ZeroGPU hooks + queue and serves our
312
+ # custom routes. NOT uvicorn.run, NOT prevent_thread_lock + route surgery.
313
+ app.launch(server_name="0.0.0.0", server_port=7860, show_error=True)