Spaces:
Running on Zero
Running on Zero
File size: 11,139 Bytes
5620566 7c55e05 5620566 dbdf0a6 5620566 4b556cf 5620566 7aa9156 4b556cf 7aa9156 db89e8d 5620566 7aa9156 5620566 7aa9156 5620566 7aa9156 c1e3025 7aa9156 5620566 7aa9156 5620566 7aa9156 5620566 c1e3025 5620566 7aa9156 db89e8d 7aa9156 db89e8d 7aa9156 db89e8d 7aa9156 db89e8d 7aa9156 db89e8d 7aa9156 5620566 7aa9156 c1e3025 7aa9156 5620566 7aa9156 5620566 7aa9156 5620566 7aa9156 5620566 7aa9156 5620566 dbdf0a6 5620566 dbdf0a6 5620566 dbdf0a6 7aa9156 5620566 7aa9156 5620566 dbdf0a6 3790900 dbdf0a6 c1e3025 5620566 c1e3025 7aa9156 c1e3025 7aa9156 5620566 db89e8d 7aa9156 5620566 c7fba6e 7aa9156 dbdf0a6 7aa9156 5620566 dbdf0a6 5620566 dbdf0a6 5620566 dbdf0a6 5620566 dbdf0a6 5620566 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 | """
FastAPI + Gradio UI for SQL Copilot.
Inference endpoint + interactive demo.
"""
# `spaces` MUST be the first import, before torch/transformers/peft or
# anything that touches CUDA -- confirmed via a hard crash: "RuntimeError:
# CUDA has been initialized before importing the `spaces` package."
# ZeroGPU's own init logic requires it to run before any CUDA-related
# package is even imported (not just used).
import spaces
import os
import sqlite3
import json
import random
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import gradio as gr
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel
import torch
ADAPTER_PATH = "vishnuadupa/qwen-sql-lora"
BASE_MODEL_NAME = "Qwen/Qwen2.5-Coder-1.5B-Instruct"
# Model is loaded LAZILY, on first use, from inside the @spaces.GPU-decorated
# gradio_interface() -- not at import time, and not in a FastAPI lifespan
# hook (HF Spaces' Gradio SDK serves the `demo` Blocks object directly and
# never runs `app`'s ASGI lifespan, confirmed via logs: model/tokenizer
# stayed permanently None, so every request hit "Model not loaded").
#
# Loading at plain module level doesn't work either: `spaces` installs a
# global torch patch the moment it's imported that intercepts ALL tensor
# ops -- including a plain CPU safetensors.load_file() call -- and routes
# them through a GPU-context check. Confirmed via full traceback: loading
# the adapter at import time crashed inside that patch with "No CUDA GPUs
# are available", regardless of dtype/quantization choices, simply
# because no @spaces.GPU call was active yet. So loading must happen
# inside the decorated function itself, the first time it's actually
# invoked (which IS a valid GPU-allocated context under ZeroGPU).
_model = None
_tokenizer = None
def _ensure_model_loaded():
global _model, _tokenizer
if _model is not None:
return
print("Loading fine-tuned model...")
try:
print(f"Loading adapter from HF Hub: {ADAPTER_PATH}")
_tokenizer = AutoTokenizer.from_pretrained(ADAPTER_PATH)
base_model = AutoModelForCausalLM.from_pretrained(BASE_MODEL_NAME, torch_dtype=torch.float32)
_model = PeftModel.from_pretrained(base_model, ADAPTER_PATH)
_model = _model.merge_and_unload()
print("Fine-tuned model loaded.")
except Exception as e:
import traceback
print(f"Could not load fine-tuned model ({e}); falling back to base model.")
traceback.print_exc()
_tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_NAME)
_model = AutoModelForCausalLM.from_pretrained(BASE_MODEL_NAME, torch_dtype=torch.float32)
print("Base model loaded.")
app = FastAPI(title="SQL Copilot")
class SQLRequest(BaseModel):
question: str
schema: str
class SQLResponse(BaseModel):
sql: str
error: str = None
def build_prompt(question: str, schema: str) -> str:
# Must match train_lora.py's training format exactly (wording + trailing
# newline after "SQL:") -- this LoRA adapter is lightly trained and
# very sensitive to prompt shape; a mismatched format was confirmed to
# collapse accuracy from ~60% to ~2% in eval.py before this same fix.
return f"""You are a SQL expert. Generate valid SQL.
Question: {question}
Schema:
{schema}
SQL:
"""
def generate_sql_text(question: str, schema: str, device: str = "cpu") -> str:
_ensure_model_loaded()
prompt = build_prompt(question, schema)
inputs = _tokenizer(prompt, return_tensors="pt").to(device)
# Greedy decoding, not sampling: a SQL generator should return its
# single most-confident answer, not a random draw that can vary
# between identical requests.
outputs = _model.generate(
**inputs,
max_new_tokens=256,
do_sample=False,
eos_token_id=_tokenizer.eos_token_id,
pad_token_id=_tokenizer.eos_token_id,
)
response = _tokenizer.decode(outputs[0], skip_special_tokens=True)
sql = response.split("SQL:")[-1].strip()
if "```" in sql:
sql = sql.split("```")[1].split("```")[0].strip()
# Take first line only: the model's actual answer is always the first
# line, and it can drift into commentary (or even a second, unrelated
# code block) afterward on lightly-trained checkpoints.
return sql.split("\n")[0].strip()
@app.post("/predict", response_model=SQLResponse)
async def predict_sql(req: SQLRequest):
"""Generate SQL from natural language."""
try:
sql = generate_sql_text(req.question, req.schema)
return SQLResponse(sql=sql)
except Exception as e:
return SQLResponse(sql="", error=str(e))
@app.get("/health")
async def health():
return {"status": "ok"}
def create_sample_db():
"""Create a sample DB for the demo."""
conn = sqlite3.connect("sample.db")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS customers (
id INTEGER PRIMARY KEY,
name TEXT,
region TEXT,
revenue REAL
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER,
amount REAL,
date TEXT
)
""")
cursor.execute("DELETE FROM customers")
cursor.execute("DELETE FROM orders")
customers = [
(1, "Acme Corp", "Northeast", 50000),
(2, "TechStart", "West", 75000),
(3, "BigCorp", "Northeast", 120000),
(4, "SmallBiz", "South", 30000),
]
cursor.executemany("INSERT INTO customers VALUES (?, ?, ?, ?)", customers)
orders = [
(1, 1, 5000, "2024-01-15"),
(2, 1, 3000, "2024-02-10"),
(3, 3, 15000, "2024-01-20"),
(4, 2, 8000, "2024-03-05"),
]
cursor.executemany("INSERT INTO orders VALUES (?, ?, ?, ?)", orders)
conn.commit()
conn.close()
# Run unconditionally at import time -- HF Spaces' Gradio SDK imports this
# module rather than executing it as __main__, so anything gated behind
# `if __name__ == "__main__":` never runs there.
create_sample_db()
def execute_sql(sql: str):
"""Execute SQL, returning (results_or_None, error_message_or_None).
Results are returned as a list of {column: value} dicts (using
cursor.description for column names) rather than bare tuples/lists --
much more readable in the JSON output (e.g. {"name": "TechStart"}
instead of an unlabeled ["TechStart"]).
"""
try:
conn = sqlite3.connect("sample.db")
cursor = conn.cursor()
cursor.execute(sql)
columns = [d[0] for d in cursor.description] if cursor.description else []
rows = cursor.fetchall()
conn.close()
results = [dict(zip(columns, row)) for row in rows]
return results, None
except Exception as e:
return None, str(e)
# Verified working (question, schema) pairs for the "Try an Example" button.
# Each was manually tested against the live deployed model before being
# added here, rather than guessed -- every one below produced a correct,
# sensible query + result when tested.
DEFAULT_SCHEMA = """CREATE TABLE customers (id INTEGER, name TEXT, region TEXT, revenue REAL)
CREATE TABLE orders (id INTEGER, customer_id INTEGER, amount REAL, date TEXT)"""
EXAMPLES = [
{
"question": "Top 5 customers by revenue in Northeast",
"schema": DEFAULT_SCHEMA,
},
{
"question": "List all customers in the West region",
"schema": DEFAULT_SCHEMA,
},
]
def pick_random_example():
example = random.choice(EXAMPLES)
return example["question"], example["schema"]
@spaces.GPU
def gradio_interface(question, schema):
"""Gradio interface function.
@spaces.GPU is required by HF Spaces' free ZeroGPU tier -- without it,
the Space fails at startup with "No @spaces.GPU function detected"
since ZeroGPU only allocates GPU time to functions explicitly marked
this way. GPU is only actually visible for the duration of this call,
so the model is moved to "cuda" here rather than at import time.
"""
device = "cuda" if torch.cuda.is_available() else "cpu"
try:
_ensure_model_loaded() # first call here loads inside a valid GPU context
_model.to(device)
sql = generate_sql_text(question, schema, device=device)
except Exception as e:
import traceback
print(f"gradio_interface generation error: {e}")
traceback.print_exc()
# result_output is a gr.JSON component: it always requires a
# JSON-serializable value, never a bare string, or it crashes with
# "Invalid JSON string" -- confirmed via server logs to be exactly
# what was happening on every request before this fix.
return "", json.dumps({"error": f"Generation failed: {e}"})
results, error = execute_sql(sql)
if error:
return sql, json.dumps({"error": error})
if not results:
return sql, json.dumps({"info": "Query executed successfully β no matching rows."})
return sql, json.dumps(results, indent=2)
# Gradio UI
with gr.Blocks(title="SQL Copilot", theme=gr.themes.Soft(primary_hue="orange")) as demo:
gr.Markdown("# ποΈ SQL Copilot β Natural Language β SQL")
gr.Markdown(
"Fine-tuned Qwen2.5-Coder-1.5B (LoRA) on the b-mc2/sql-create-context dataset. "
"Type a question and schema, or click **Try an Example** for one that's verified to work well.\n\n"
"*Honest note: this is a small, lightly-fine-tuned model evaluated on unseen schemas β it "
"sometimes gets things exactly right and sometimes hallucinates a filter condition that wasn't "
"asked for. Both are shown here on purpose.*"
)
with gr.Row():
with gr.Column():
question = gr.Textbox(
label="Natural Language Question",
placeholder="e.g., 'Top 5 customers by revenue in Northeast'",
lines=2
)
schema = gr.Code(
label="Database Schema (CREATE TABLE statements)",
language="sql",
lines=5,
value=DEFAULT_SCHEMA,
)
with gr.Row():
example_btn = gr.Button("π² Try an Example")
submit_btn = gr.Button("Generate SQL", variant="primary")
gr.Markdown(
"*First request after the app has been idle takes ~15-20s "
"(free-tier GPU has to reload the model); after that it's fast.*"
)
with gr.Column():
sql_output = gr.Code(label="Generated SQL", language="sql")
result_output = gr.JSON(label="Query Results")
example_btn.click(
fn=pick_random_example,
inputs=[],
outputs=[question, schema]
)
submit_btn.click(
fn=gradio_interface,
inputs=[question, schema],
outputs=[sql_output, result_output]
)
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860, share=False)
|