File size: 6,143 Bytes
249242b
06f002d
 
 
 
249242b
06f002d
 
 
249242b
06f002d
 
 
 
 
 
249242b
 
 
06f002d
249242b
06f002d
 
 
249242b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
06f002d
249242b
06f002d
 
 
 
 
 
 
 
 
 
 
249242b
 
 
06f002d
249242b
 
 
06f002d
249242b
 
 
06f002d
249242b
 
06f002d
249242b
 
06f002d
249242b
 
 
 
 
06f002d
 
 
 
 
 
 
 
 
249242b
 
 
 
06f002d
249242b
 
06f002d
77f911a
 
 
 
 
249242b
 
 
 
 
 
 
06f002d
 
 
 
249242b
 
06f002d
 
 
 
 
 
 
249242b
06f002d
 
 
 
 
 
 
249242b
06f002d
 
249242b
06f002d
 
 
 
 
249242b
06f002d
 
77f911a
 
 
 
249242b
06f002d
 
 
249242b
06f002d
249242b
06f002d
249242b
06f002d
249242b
06f002d
 
 
 
 
 
 
249242b
 
06f002d
 
 
 
 
249242b
06f002d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249242b
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
# ruff: noqa: I001
from __future__ import annotations

import re

# ZeroGPU patches torch when spaces is imported, so this import must remain first.
import spaces
import gradio as gr
import torch
from transformers import AutoModelForCausalLM, PreTrainedTokenizerFast


MODEL_ID = "prem-research/prem-1B-SQL"
MODEL_REVISION = "44dd7fcf9227af4efed936bf29323c61bf66aad1"

MAX_INPUT_TOKENS = 3072
DEFAULT_MAX_NEW_TOKENS = 128
MAX_NEW_TOKENS = 256
MAX_OUTPUT_CHARS = 20_000

tokenizer = PreTrainedTokenizerFast.from_pretrained(
    MODEL_ID,
    revision=MODEL_REVISION,
)
if not tokenizer.is_fast:
    raise RuntimeError("Prem-1B-SQL requires the fast byte-level tokenizer")
tokenizer.padding_side = "right"
tokenizer.pad_token = tokenizer.eos_token

# Transformers 5 can otherwise select the slow Llama tokenizer declared in
# this checkpoint and expose its internal space/newline pieces as literal text.
DECODE_PROBE = "SELECT count(*) FROM customers;\n"
decode_probe_ids = tokenizer.encode(DECODE_PROBE, add_special_tokens=False)
decoded_probe = tokenizer.decode(
    decode_probe_ids,
    skip_special_tokens=True,
    clean_up_tokenization_spaces=False,
)
if decoded_probe != DECODE_PROBE:
    raise RuntimeError("Prem-1B-SQL tokenizer failed its byte-level decode check")

# ZeroGPU requires the GPU-backed model to be placed on CUDA at module load.
model = (
    AutoModelForCausalLM.from_pretrained(
        MODEL_ID,
        revision=MODEL_REVISION,
        dtype=torch.float16,
        use_safetensors=True,
    )
    .to("cuda")
    .eval()
)

SQL_START = re.compile(r"^\s*(?:WITH|SELECT)\b", re.IGNORECASE | re.MULTILINE)
CODE_BLOCK = re.compile(r"```(?:sql)?\s*(.*?)```", re.IGNORECASE | re.DOTALL)
INVALID_DECODE_MARKERS = ("Ċ", "Ġ")

EXAMPLE_PROMPT = """# Follow these instructions:
You will be given table schemas for a database. Write one correct, read-only SQL query
that answers the question.

1. Return SQL only, on one line, without Markdown or commentary.
2. Use only tables and columns present in the schema; never invent identifiers.
3. Never use INSERT, UPDATE, DELETE, DDL, or PRAGMA.

# SQL dialect: sqlite
# Maximum rows: 100

# Database and Table Schema:
TABLE customers (id INTEGER PK NOT NULL, name TEXT NOT NULL, city TEXT)

# Here are some Examples on how to generate SQL statements and use column names:

# Question: How many customers are there?

# SQL:"""


def extract_sql(generated: str) -> str:
    text = generated.strip()

    fenced = CODE_BLOCK.search(text)
    if fenced:
        text = fenced.group(1).strip()

    # Some checkpoints repeat the training delimiter before the answer.
    if "# SQL:" in text:
        text = text.rsplit("# SQL:", maxsplit=1)[-1].strip()

    start = SQL_START.search(text)
    if not start:
        raise gr.Error("The model did not return a SQL SELECT statement.")

    sql_text = text[start.start() :].removesuffix("```").strip()

    # This endpoint's contract is exactly one single-line SQL statement. Keep
    # the first generated SQL line if the checkpoint starts a second sample.
    sql = next((line.strip() for line in sql_text.splitlines() if line.strip()), "")
    if not sql:
        raise gr.Error("The model returned an empty response.")
    if len(sql) > MAX_OUTPUT_CHARS:
        raise gr.Error("The model response was unexpectedly large.")
    if "\x00" in sql or any(marker in sql for marker in INVALID_DECODE_MARKERS):
        raise gr.Error("The model returned malformed tokenizer output. Please retry.")
    return sql


@spaces.GPU(duration=45)
def generate_sql(prompt: str, max_new_tokens: int) -> str:
    normalized_prompt = (prompt or "").strip()
    if not normalized_prompt:
        raise gr.Error("Prompt cannot be empty.")

    token_limit = max(
        32,
        min(MAX_NEW_TOKENS, int(max_new_tokens or DEFAULT_MAX_NEW_TOKENS)),
    )
    inputs = tokenizer(
        normalized_prompt,
        return_tensors="pt",
        add_special_tokens=True,
        truncation=False,
    )
    input_length = int(inputs["input_ids"].shape[-1])
    if input_length > MAX_INPUT_TOKENS:
        raise gr.Error(
            f"Prompt has {input_length:,} tokens; the maximum is {MAX_INPUT_TOKENS:,}."
        )

    inputs = {name: tensor.to("cuda") for name, tensor in inputs.items()}
    with torch.inference_mode():
        output = model.generate(
            **inputs,
            max_new_tokens=token_limit,
            do_sample=False,
            num_beams=1,
            eos_token_id=tokenizer.eos_token_id,
            pad_token_id=tokenizer.pad_token_id,
            # The model was trained on labelled, newline-delimited examples.
            # Stop at its first completed response line instead of allowing it
            # to continue into another sample until max_new_tokens is reached.
            stop_strings=["\n", "```"],
            tokenizer=tokenizer,
            use_cache=True,
        )

    generated_token_ids = output[0, input_length:].tolist()
    generated = tokenizer.decode(
        generated_token_ids,
        skip_special_tokens=True,
        clean_up_tokenization_spaces=False,
    )
    return extract_sql(generated)


with gr.Blocks(title="Prem 1B SQL API") as demo:
    gr.Markdown(
        """
        # Prem 1B SQL

        Generate SQL from a schema-aware prompt. The calling application must
        validate every generated statement before execution.
        """
    )
    prompt_input = gr.Textbox(
        value=EXAMPLE_PROMPT,
        label="Schema-aware prompt",
        lines=20,
    )
    max_tokens_input = gr.Slider(
        minimum=32,
        maximum=MAX_NEW_TOKENS,
        value=DEFAULT_MAX_NEW_TOKENS,
        step=1,
        label="Maximum output tokens",
    )
    generate_button = gr.Button("Generate SQL", variant="primary")
    sql_output = gr.Code(label="Generated SQL", language="sql")

    generate_button.click(
        fn=generate_sql,
        inputs=[prompt_input, max_tokens_input],
        outputs=sql_output,
        api_name="generate",
        concurrency_limit=1,
    )

demo.queue(max_size=20, default_concurrency_limit=1)

if __name__ == "__main__":
    demo.launch()