Bot commited on
Commit
7aa9156
·
1 Parent(s): c1e3025

Fix root cause: model was never loading on the Space (Gradio SDK never runs FastAPI lifespan), causing every request to crash. Module-level load + correct ZeroGPU device pattern + always-JSON outputs.

Browse files
Files changed (1) hide show
  1. app.py +97 -108
app.py CHANGED
@@ -6,7 +6,6 @@ Inference endpoint + interactive demo.
6
  import os
7
  import sqlite3
8
  import json
9
- from contextlib import asynccontextmanager
10
  from fastapi import FastAPI, HTTPException
11
  from pydantic import BaseModel
12
  import gradio as gr
@@ -15,105 +14,105 @@ from peft import AutoPeftModelForCausalLM
15
  import torch
16
  import spaces
17
 
18
- # Global model & tokenizer
19
- model = None
20
- tokenizer = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
- @asynccontextmanager
23
- async def lifespan(app: FastAPI):
24
- """Load model on startup, unload on shutdown."""
25
- global model, tokenizer
26
- print("Loading fine-tuned model...")
27
 
28
- # Try to load from HF Hub (set this to your repo after training)
29
- adapter_path = "vishnuadupa/qwen-sql-lora"
30
 
31
- try:
32
- print(f"Loading adapter from HF Hub: {adapter_path}")
33
- tokenizer = AutoTokenizer.from_pretrained(adapter_path)
34
- model = AutoPeftModelForCausalLM.from_pretrained(adapter_path, device_map="auto")
35
- model = model.merge_and_unload()
36
- print("✓ Fine-tuned model loaded!")
37
- except Exception as e:
38
- print(f"⚠ Could not load fine-tuned model: {e}")
39
- print("Falling back to base model...")
40
- tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-Coder-1.5B-Instruct")
41
- model = AutoModelForCausalLM.from_pretrained(
42
- "Qwen/Qwen2.5-Coder-1.5B-Instruct",
43
- torch_dtype=torch.float16,
44
- device_map="auto"
45
- )
46
- print("✓ Base model loaded")
47
-
48
- yield
49
- print("Unloading model...")
50
- del model
51
- torch.cuda.empty_cache()
52
-
53
- app = FastAPI(title="SQL Copilot", lifespan=lifespan)
54
 
55
  class SQLRequest(BaseModel):
56
  question: str
57
  schema: str
58
 
 
59
  class SQLResponse(BaseModel):
60
  sql: str
61
  error: str = None
62
 
63
- @app.post("/predict", response_model=SQLResponse)
64
- async def predict_sql(req: SQLRequest):
65
- """Generate SQL from natural language."""
66
- if model is None:
67
- raise HTTPException(status_code=503, detail="Model not loaded")
68
 
 
69
  # Must match train_lora.py's training format exactly (wording + trailing
70
  # newline after "SQL:") -- this LoRA adapter is lightly trained and
71
  # very sensitive to prompt shape; a mismatched format was confirmed to
72
  # collapse accuracy from ~60% to ~2% in eval.py before this same fix.
73
- prompt = f"""You are a SQL expert. Generate valid SQL.
74
 
75
- Question: {req.question}
76
 
77
  Schema:
78
- {req.schema}
79
 
80
  SQL:
81
  """
82
 
83
- try:
84
- inputs = tokenizer(prompt, return_tensors="pt")
85
- # Greedy decoding, not sampling: a SQL generator should return its
86
- # single most-confident answer, not a random draw that can vary
87
- # between identical requests.
88
- outputs = model.generate(
89
- **inputs,
90
- max_new_tokens=256,
91
- do_sample=False,
92
- eos_token_id=tokenizer.eos_token_id,
93
- pad_token_id=tokenizer.eos_token_id,
94
- )
95
- response = tokenizer.decode(outputs[0], skip_special_tokens=True)
96
- sql = response.split("SQL:")[-1].strip()
97
 
98
- if "```" in sql:
99
- sql = sql.split("```")[1].split("```")[0].strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
- # Take first line only: the model's actual answer is always the
102
- # first line, and it can drift into commentary (or even a second,
103
- # unrelated code block) afterward on lightly-trained checkpoints.
104
- sql = sql.split("\n")[0].strip()
105
 
 
 
 
 
 
 
 
 
 
 
 
106
  return SQLResponse(sql=sql)
107
  except Exception as e:
108
  return SQLResponse(sql="", error=str(e))
109
 
 
110
  @app.get("/health")
111
  async def health():
112
  return {"status": "ok"}
113
 
114
- # Sample SQLite database for demo
115
  def create_sample_db():
116
- """Create a sample DB for testing."""
117
  conn = sqlite3.connect("sample.db")
118
  cursor = conn.cursor()
119
 
@@ -157,17 +156,25 @@ def create_sample_db():
157
  conn.commit()
158
  conn.close()
159
 
 
 
 
 
 
 
 
160
  def execute_sql(sql: str):
161
- """Execute SQL and return results."""
162
  try:
163
  conn = sqlite3.connect("sample.db")
164
  cursor = conn.cursor()
165
  cursor.execute(sql)
166
  results = cursor.fetchall()
167
  conn.close()
168
- return results
169
  except Exception as e:
170
- return f"Error: {str(e)}"
 
171
 
172
  @spaces.GPU
173
  def gradio_interface(question, schema):
@@ -176,43 +183,26 @@ def gradio_interface(question, schema):
176
  @spaces.GPU is required by HF Spaces' free ZeroGPU tier -- without it,
177
  the Space fails at startup with "No @spaces.GPU function detected"
178
  since ZeroGPU only allocates GPU time to functions explicitly marked
179
- this way.
 
180
  """
181
- if not model:
182
- return "Model not loaded", "N/A"
183
-
184
- req = SQLRequest(question=question, schema=schema)
185
- response = execute_sql(req.question)
186
-
187
- # Must match training format exactly -- see note in /predict above.
188
- prompt = f"""You are a SQL expert. Generate valid SQL.
189
-
190
- Question: {req.question}
191
-
192
- Schema:
193
- {req.schema}
194
-
195
- SQL:
196
- """
197
-
198
- inputs = tokenizer(prompt, return_tensors="pt")
199
- # Greedy decoding — see note in /predict above.
200
- outputs = model.generate(
201
- **inputs,
202
- max_new_tokens=256,
203
- do_sample=False,
204
- eos_token_id=tokenizer.eos_token_id,
205
- pad_token_id=tokenizer.eos_token_id,
206
- )
207
- sql = tokenizer.decode(outputs[0], skip_special_tokens=True).split("SQL:")[-1].strip()
208
- # Take first line only — see note in /predict above.
209
- sql = sql.split("\n")[0].strip()
210
 
211
  try:
212
- results = execute_sql(sql)
213
- return sql, json.dumps(results, indent=2)
214
  except Exception as e:
215
- return sql, f"Execution error: {str(e)}"
 
 
 
 
 
 
 
 
 
 
216
 
217
  # Gradio UI
218
  with gr.Blocks(title="SQL Copilot") as demo:
@@ -228,12 +218,14 @@ with gr.Blocks(title="SQL Copilot") as demo:
228
  )
229
  schema = gr.Textbox(
230
  label="Database Schema",
231
- placeholder="Paste your schema here",
232
  lines=5,
233
- value="""
234
- Table: customers (id, name, region, revenue)
235
- Table: orders (id, customer_id, amount, date)
236
- """
 
 
237
  )
238
  submit_btn = gr.Button("Generate SQL", variant="primary")
239
 
@@ -248,7 +240,4 @@ Table: orders (id, customer_id, amount, date)
248
  )
249
 
250
  if __name__ == "__main__":
251
- create_sample_db()
252
-
253
- # Launch Gradio
254
  demo.launch(server_name="0.0.0.0", server_port=7860, share=False)
 
6
  import os
7
  import sqlite3
8
  import json
 
9
  from fastapi import FastAPI, HTTPException
10
  from pydantic import BaseModel
11
  import gradio as gr
 
14
  import torch
15
  import spaces
16
 
17
+ # Model is loaded once at IMPORT TIME, not inside a FastAPI lifespan hook.
18
+ # HF Spaces' Gradio SDK serves the `demo` Blocks object directly and never
19
+ # runs `app`'s ASGI lifespan -- confirmed via server logs that this left
20
+ # model/tokenizer permanently None, so every request hit the "Model not
21
+ # loaded" fallback and crashed the gr.JSON output component with it.
22
+ #
23
+ # Also: under HF's free ZeroGPU tier, no GPU is visible at import time --
24
+ # it's only allocated for the duration of an @spaces.GPU-decorated call.
25
+ # So load on CPU here, then move to CUDA inside gradio_interface() below.
26
+ print("Loading fine-tuned model...")
27
+ ADAPTER_PATH = "vishnuadupa/qwen-sql-lora"
28
+
29
+ try:
30
+ print(f"Loading adapter from HF Hub: {ADAPTER_PATH}")
31
+ tokenizer = AutoTokenizer.from_pretrained(ADAPTER_PATH)
32
+ model = AutoPeftModelForCausalLM.from_pretrained(ADAPTER_PATH)
33
+ model = model.merge_and_unload()
34
+ print("Fine-tuned model loaded.")
35
+ except Exception as e:
36
+ print(f"Could not load fine-tuned model ({e}); falling back to base model.")
37
+ tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-Coder-1.5B-Instruct")
38
+ model = AutoModelForCausalLM.from_pretrained(
39
+ "Qwen/Qwen2.5-Coder-1.5B-Instruct", torch_dtype=torch.float16
40
+ )
41
+ print("Base model loaded.")
42
 
43
+ model = model.to("cpu")
 
 
 
 
44
 
45
+ app = FastAPI(title="SQL Copilot")
 
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
  class SQLRequest(BaseModel):
49
  question: str
50
  schema: str
51
 
52
+
53
  class SQLResponse(BaseModel):
54
  sql: str
55
  error: str = None
56
 
 
 
 
 
 
57
 
58
+ def build_prompt(question: str, schema: str) -> str:
59
  # Must match train_lora.py's training format exactly (wording + trailing
60
  # newline after "SQL:") -- this LoRA adapter is lightly trained and
61
  # very sensitive to prompt shape; a mismatched format was confirmed to
62
  # collapse accuracy from ~60% to ~2% in eval.py before this same fix.
63
+ return f"""You are a SQL expert. Generate valid SQL.
64
 
65
+ Question: {question}
66
 
67
  Schema:
68
+ {schema}
69
 
70
  SQL:
71
  """
72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
+ def generate_sql_text(question: str, schema: str, device: str = "cpu") -> str:
75
+ prompt = build_prompt(question, schema)
76
+ inputs = tokenizer(prompt, return_tensors="pt").to(device)
77
+ # Greedy decoding, not sampling: a SQL generator should return its
78
+ # single most-confident answer, not a random draw that can vary
79
+ # between identical requests.
80
+ outputs = model.generate(
81
+ **inputs,
82
+ max_new_tokens=256,
83
+ do_sample=False,
84
+ eos_token_id=tokenizer.eos_token_id,
85
+ pad_token_id=tokenizer.eos_token_id,
86
+ )
87
+ response = tokenizer.decode(outputs[0], skip_special_tokens=True)
88
+ sql = response.split("SQL:")[-1].strip()
89
 
90
+ if "```" in sql:
91
+ sql = sql.split("```")[1].split("```")[0].strip()
 
 
92
 
93
+ # Take first line only: the model's actual answer is always the first
94
+ # line, and it can drift into commentary (or even a second, unrelated
95
+ # code block) afterward on lightly-trained checkpoints.
96
+ return sql.split("\n")[0].strip()
97
+
98
+
99
+ @app.post("/predict", response_model=SQLResponse)
100
+ async def predict_sql(req: SQLRequest):
101
+ """Generate SQL from natural language."""
102
+ try:
103
+ sql = generate_sql_text(req.question, req.schema)
104
  return SQLResponse(sql=sql)
105
  except Exception as e:
106
  return SQLResponse(sql="", error=str(e))
107
 
108
+
109
  @app.get("/health")
110
  async def health():
111
  return {"status": "ok"}
112
 
113
+
114
  def create_sample_db():
115
+ """Create a sample DB for the demo."""
116
  conn = sqlite3.connect("sample.db")
117
  cursor = conn.cursor()
118
 
 
156
  conn.commit()
157
  conn.close()
158
 
159
+
160
+ # Run unconditionally at import time -- HF Spaces' Gradio SDK imports this
161
+ # module rather than executing it as __main__, so anything gated behind
162
+ # `if __name__ == "__main__":` never runs there.
163
+ create_sample_db()
164
+
165
+
166
  def execute_sql(sql: str):
167
+ """Execute SQL, returning (results_or_None, error_message_or_None)."""
168
  try:
169
  conn = sqlite3.connect("sample.db")
170
  cursor = conn.cursor()
171
  cursor.execute(sql)
172
  results = cursor.fetchall()
173
  conn.close()
174
+ return results, None
175
  except Exception as e:
176
+ return None, str(e)
177
+
178
 
179
  @spaces.GPU
180
  def gradio_interface(question, schema):
 
183
  @spaces.GPU is required by HF Spaces' free ZeroGPU tier -- without it,
184
  the Space fails at startup with "No @spaces.GPU function detected"
185
  since ZeroGPU only allocates GPU time to functions explicitly marked
186
+ this way. GPU is only actually visible for the duration of this call,
187
+ so the model is moved to "cuda" here rather than at import time.
188
  """
189
+ device = "cuda" if torch.cuda.is_available() else "cpu"
190
+ model.to(device)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
 
192
  try:
193
+ sql = generate_sql_text(question, schema, device=device)
 
194
  except Exception as e:
195
+ # result_output is a gr.JSON component: it always requires a
196
+ # JSON-serializable value, never a bare string, or it crashes with
197
+ # "Invalid JSON string" -- confirmed via server logs to be exactly
198
+ # what was happening on every request before this fix.
199
+ return "", json.dumps({"error": f"Generation failed: {e}"})
200
+
201
+ results, error = execute_sql(sql)
202
+ if error:
203
+ return sql, json.dumps({"error": error})
204
+ return sql, json.dumps(results, indent=2)
205
+
206
 
207
  # Gradio UI
208
  with gr.Blocks(title="SQL Copilot") as demo:
 
218
  )
219
  schema = gr.Textbox(
220
  label="Database Schema",
221
+ placeholder="Paste your schema here (CREATE TABLE statements)",
222
  lines=5,
223
+ # CREATE TABLE format, not a "Table: x (...)" summary --
224
+ # matches what the model was actually trained on
225
+ # (b-mc2/sql-create-context), so the default example
226
+ # actually demonstrates the model's real behavior.
227
+ value="""CREATE TABLE customers (id INTEGER, name TEXT, region TEXT, revenue REAL)
228
+ CREATE TABLE orders (id INTEGER, customer_id INTEGER, amount REAL, date TEXT)"""
229
  )
230
  submit_btn = gr.Button("Generate SQL", variant="primary")
231
 
 
240
  )
241
 
242
  if __name__ == "__main__":
 
 
 
243
  demo.launch(server_name="0.0.0.0", server_port=7860, share=False)