Singh commited on
Commit
985646d
Β·
verified Β·
1 Parent(s): b9f7095

create app.py

Browse files
Files changed (1) hide show
  1. app.py +194 -0
app.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, json, time, threading, logging, traceback
2
+ import torch
3
+ import gradio as gr
4
+ from fastapi import FastAPI, HTTPException
5
+ from fastapi.middleware.cors import CORSMiddleware
6
+ from fastapi.responses import StreamingResponse
7
+ from pydantic import BaseModel, Field
8
+ from transformers import AutoTokenizer, AutoModelForCausalLM, TextIteratorStreamer
9
+ from typing import Optional, Iterator
10
+
11
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")
12
+ logger = logging.getLogger(__name__)
13
+
14
+ MODEL_ID = os.getenv("MODEL_ID", "google/gemma-2b-it")
15
+ HF_TOKEN = os.getenv("HF_TOKEN")
16
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
17
+ DTYPE = torch.float16 if DEVICE == "cuda" else torch.float32
18
+
19
+ logger.info(f"Loading {MODEL_ID} on {DEVICE} ...")
20
+
21
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, token=HF_TOKEN)
22
+ model = AutoModelForCausalLM.from_pretrained(
23
+ MODEL_ID,
24
+ torch_dtype=DTYPE,
25
+ device_map="auto",
26
+ token=HF_TOKEN,
27
+ )
28
+ model.eval()
29
+ logger.info("Model ready.")
30
+
31
+ # ── FastAPI (mounted under /api) ──
32
+ api = FastAPI(title="Gemma 2B API")
33
+ api.add_middleware(
34
+ CORSMiddleware,
35
+ allow_origins=["*"],
36
+ allow_credentials=False,
37
+ allow_methods=["*"],
38
+ allow_headers=["*"],
39
+ expose_headers=["*"],
40
+ )
41
+
42
+ class GenerateRequest(BaseModel):
43
+ prompt: str = Field(..., min_length=1, max_length=4096)
44
+ max_new_tokens: int = Field(default=256, ge=1, le=1024)
45
+ temperature: float = Field(default=0.7, ge=0.01, le=2.0)
46
+ top_p: float = Field(default=0.9, ge=0.0, le=1.0)
47
+ top_k: int = Field(default=50, ge=0, le=200)
48
+ do_sample: bool = Field(default=True)
49
+ system_prompt: Optional[str] = Field(default=None, max_length=1024)
50
+
51
+
52
+ def stream_tokens(req: GenerateRequest) -> Iterator[str]:
53
+ try:
54
+ if req.system_prompt:
55
+ prompt = (
56
+ f"<start_of_turn>system\n{req.system_prompt}<end_of_turn>\n"
57
+ f"<start_of_turn>user\n{req.prompt}<end_of_turn>\n"
58
+ f"<start_of_turn>model\n"
59
+ )
60
+ else:
61
+ prompt = (
62
+ f"<start_of_turn>user\n{req.prompt}<end_of_turn>\n"
63
+ f"<start_of_turn>model\n"
64
+ )
65
+
66
+ inputs = tokenizer(
67
+ prompt, return_tensors="pt", truncation=True, max_length=2048
68
+ ).to(model.device)
69
+
70
+ streamer = TextIteratorStreamer(
71
+ tokenizer, skip_prompt=True, skip_special_tokens=True, timeout=60.0
72
+ )
73
+
74
+ gen_kwargs = dict(
75
+ input_ids = inputs["input_ids"],
76
+ attention_mask = inputs["attention_mask"],
77
+ streamer = streamer,
78
+ max_new_tokens = req.max_new_tokens,
79
+ temperature = req.temperature,
80
+ top_p = req.top_p,
81
+ top_k = req.top_k,
82
+ do_sample = req.do_sample,
83
+ pad_token_id = tokenizer.eos_token_id,
84
+ repetition_penalty = 1.1,
85
+ )
86
+
87
+ t = threading.Thread(target=model.generate, kwargs=gen_kwargs, daemon=True)
88
+ t.start()
89
+
90
+ token_count = 0
91
+ start = time.perf_counter()
92
+
93
+ for text in streamer:
94
+ if text:
95
+ token_count += 1
96
+ yield f"data: {json.dumps({'token': text, 'token_index': token_count})}\n\n"
97
+
98
+ t.join()
99
+ latency = (time.perf_counter() - start) * 1000
100
+ yield f"data: {json.dumps({'done': True, 'total_tokens': token_count, 'latency_ms': round(latency,1)})}\n\n"
101
+
102
+ except Exception as e:
103
+ tb = traceback.format_exc()
104
+ logger.error(tb)
105
+ yield f"data: {json.dumps({'error': str(e), 'traceback': tb})}\n\n"
106
+
107
+
108
+ @api.get("/health")
109
+ async def health():
110
+ return {
111
+ "status" : "ok",
112
+ "model" : MODEL_ID,
113
+ "device" : DEVICE,
114
+ "gpu_memory_used_gb" : round(torch.cuda.memory_allocated() / 1e9, 2) if DEVICE == "cuda" else 0,
115
+ "gpu_memory_total_gb" : round(torch.cuda.get_device_properties(0).total_memory / 1e9, 2) if DEVICE == "cuda" else 0,
116
+ }
117
+
118
+ @api.post("/generate/stream")
119
+ async def generate_stream(req: GenerateRequest):
120
+ return StreamingResponse(
121
+ stream_tokens(req),
122
+ media_type="text/event-stream",
123
+ headers={
124
+ "Cache-Control" : "no-cache",
125
+ "X-Accel-Buffering" : "no",
126
+ "Access-Control-Allow-Origin": "*",
127
+ },
128
+ )
129
+
130
+ @api.post("/generate")
131
+ async def generate(req: GenerateRequest):
132
+ full_text = ""
133
+ total_tokens = 0
134
+ latency_ms = 0.0
135
+ for chunk in stream_tokens(req):
136
+ if not chunk.startswith("data: "):
137
+ continue
138
+ try:
139
+ data = json.loads(chunk[6:])
140
+ except json.JSONDecodeError:
141
+ continue
142
+ if "error" in data:
143
+ raise HTTPException(status_code=500, detail=data["error"])
144
+ if "token" in data:
145
+ full_text += data["token"]
146
+ total_tokens += 1
147
+ if "done" in data:
148
+ latency_ms = data["latency_ms"]
149
+ return {"generated_text": full_text, "completion_tokens": total_tokens,
150
+ "latency_ms": latency_ms, "model": MODEL_ID}
151
+
152
+
153
+ # ── Gradio UI (required to get free T4 GPU) ──
154
+ def gradio_generate(prompt, system_prompt, max_new_tokens, temperature):
155
+ req = GenerateRequest(
156
+ prompt = prompt,
157
+ system_prompt = system_prompt or None,
158
+ max_new_tokens = int(max_new_tokens),
159
+ temperature = temperature,
160
+ )
161
+ result = ""
162
+ for chunk in stream_tokens(req):
163
+ if chunk.startswith("data: "):
164
+ try:
165
+ data = json.loads(chunk[6:])
166
+ if "token" in data:
167
+ result += data["token"]
168
+ yield result
169
+ except json.JSONDecodeError:
170
+ pass
171
+
172
+
173
+ with gr.Blocks(title="Gemma 2B API") as demo:
174
+ gr.Markdown(
175
+ "## Gemma 2B β€” Streaming API\n"
176
+ "Use the `/api/generate/stream` or `/api/generate` endpoints from your backend.\n\n"
177
+ "**Health check:** `/api/health`"
178
+ )
179
+ with gr.Row():
180
+ with gr.Column():
181
+ sys_box = gr.Textbox(label="System prompt (optional)", lines=2)
182
+ prompt_box = gr.Textbox(label="Prompt", lines=4, placeholder="Ask something...")
183
+ with gr.Row():
184
+ max_tok = gr.Slider(32, 1024, value=256, step=32, label="Max tokens")
185
+ temp = gr.Slider(0.01, 2.0, value=0.7, step=0.05, label="Temperature")
186
+ btn = gr.Button("Generate", variant="primary")
187
+ with gr.Column():
188
+ output = gr.Textbox(label="Output", lines=12)
189
+ btn.click(fn=gradio_generate, inputs=[prompt_box, sys_box, max_tok, temp], outputs=output)
190
+
191
+
192
+ # Mount FastAPI at /api β€” DO NOT call demo.launch() here
193
+ # HF Spaces runs its own uvicorn server automatically
194
+ app = gr.mount_gradio_app(api, demo, path="/")