RUSQL-0.8B-Text2SQL / README.md
MaXoN654's picture
model card: revert extra wording, keep eval count fix
8f11c7d verified
|
Raw
History Blame Contribute Delete
7.19 kB
---
license: apache-2.0
language:
- ru
- en
base_model:
- techwithsergiu/Qwen3.5-text-0.8B
pipeline_tag: text-generation
library_name: transformers
tags:
- text2sql
- text-to-sql
- sql
- sqlite
- russian
- qwen3.5
datasets:
- seeklhy/SynSQL-2.5M
---
# RUSQL-0.8B-Text2SQL
Compact **Russian text-to-SQL** model: a full-parameter SFT of
[techwithsergiu/Qwen3.5-text-0.8B](https://huggingface.co/techwithsergiu/Qwen3.5-text-0.8B)
— a text-only slice of Qwen/Qwen3.5-0.8B with the vision tower removed
(**0.77B** actual parameters) — trained to answer **Russian natural-language
questions** over a database schema with step-by-step reasoning that ends in a final
**SQLite** query (OmniSQL-style CoT).
## Performance Evaluation
Execution accuracy (predicted SQL executed against SQLite, result-set comparison)
on the **held-out eval split** — the same **2,729** questions in every row (EN vs RU
are the same items, English question vs its translation), greedy decoding:
| Model | Questions | n | EX accuracy |
|---|---|---|---|
| Base (zero-shot) | EN | 2,729 | 16.0% |
| Base (zero-shot) | RU | 2,729 | 13.9% |
| **RUSQL (this model)** | **RU** | **2,729** | **58.4%** |
Fine-tuning lifts execution accuracy **from 13.9% to 58.4%** — about 4.2× the base
model, and above its English-question ceiling (16.0%).
Breakdown by SQL complexity (RU questions):
| Complexity | n | Base EX | RUSQL EX |
|---|---|---|---|
| Simple | 259 | 28.6% | **75.7%** |
| Moderate | 858 | 14.7% | **70.3%** |
| Complex | 973 | 12.3% | **55.1%** |
| Highly Complex | 639 | 9.2% | **40.5%** |
*All three rows are scored on the exact same 2,729 items (the QE-filtered held-out
split), so the numbers are directly comparable. Greedy decoding. The base model also
fails to produce a parseable SQL block on ~12% of items (331/2,729 for RU, 322 for EN);
after fine-tuning this drops to 13.*
## Dataset Overview
Training data is derived from [SynSQL-2.5M](https://huggingface.co/datasets/seeklhy/SynSQL-2.5M)
([OmniSQL, arXiv:2503.02240](https://arxiv.org/abs/2503.02240)) through a fully local,
streaming pipeline:
| Stage | What happens |
|---|---|
| 1. Sampling | Stratified sample from SynSQL-2.5M (complexity × question style), held-out eval split of 3,032 examples |
| 2. Translation EN→RU | Questions translated with **Gemma 4 E2B (q4_0, llama.cpp)**, best-of-2 candidates |
| 3. Quality filtering | Level-1 heuristics (numbers/dates/entities consistency) → repair-retry → **CometKiwi QE** (threshold 0.81, calibrated on 200 hand-labeled pairs, AUC 0.785) → best-of-N selection |
| 4. SFT | Chat-format packing, full supervision on the assistant turn incl. `<\|im_end\|>` |
Only the **question** is translated to Russian; schema (DDL), external knowledge and
the gold SQL stay in English — matching the real-world setting where databases are
English-named but users ask in Russian.
- Training set: **~444k** filtered examples (+2,729 held-out eval); filter drop rate ~13.8%
## Instruction Prompt
The model is trained (and must be used) with this exact chat format:
**System:**
````
You are a text-to-SQL assistant. Given a database schema and a question, reason step by step and finish with the final SQLite query in a ```sql code block.
````
**User:**
```
Database schema:
{DDL}
External knowledge:
{optional, may be omitted}
Question: {вопрос на русском}
```
**Assistant:** free-form chain-of-thought ending with the final query in a
` ```sql ... ``` ` block. Qwen thinking mode is **disabled** (`enable_thinking=False`) —
reasoning is plain response text, OmniSQL style.
## Training Configuration
| | |
|---|---|
| Base model | techwithsergiu/Qwen3.5-text-0.8B (text-only slice of Qwen3.5-0.8B, 0.77B params) |
| Method | Full fine-tune (no LoRA), bf16, single consumer GPU with 8 GB VRAM |
| Batching | effective batch 96 examples (token-budget packing) |
| Optimizer | AdamW 8-bit, lr 1.5e-5, warmup 3%, weight decay 0.01 |
| Epochs | 1 (+ incremental continuation on new data chunks) |
| Max sequence | 4,096 tokens |
## Usage
```python
from transformers import AutoModelForCausalLM, AutoTokenizer
import re, torch
model_id = "MaXoN654/RUSQL-0.8B-Text2SQL"
tok = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(model_id, torch_dtype=torch.bfloat16, device_map="auto")
# schema = "\n\n".join of CREATE TABLE statements, SynSQL style
# (quoted identifiers, inline /* ... */ column comments)
schema = """CREATE TABLE "employees" (
"employee_id" INTEGER /* Unique identifier for each employee */,
"name" TEXT /* Full name of the employee */,
"salary" REAL /* Annual salary in USD */,
"department_id" INTEGER /* Reference to the department */,
PRIMARY KEY ("employee_id"),
CONSTRAINT fk_employees_department_id FOREIGN KEY ("department_id") REFERENCES departments ("department_id")
)
CREATE TABLE "departments" (
"department_id" INTEGER /* Unique identifier for each department */,
"department_name" TEXT /* Name of the department */,
PRIMARY KEY ("department_id")
)"""
question = "Покажи трёх сотрудников с самой высокой зарплатой в отделе продаж"
external_knowledge = None # optional hint text; omitted from the prompt when empty
def build_user(schema, question, external_knowledge=None):
parts = [f"Database schema:\n{schema}"]
if external_knowledge and external_knowledge.strip():
parts.append(f"External knowledge:\n{external_knowledge.strip()}")
parts.append(f"Question: {question}")
return "\n\n".join(parts)
messages = [
{"role": "system", "content": "You are a text-to-SQL assistant. Given a database schema and a question, reason step by step and finish with the final SQLite query in a ```sql code block."},
{"role": "user", "content": build_user(schema, question, external_knowledge)},
]
inputs = tok.apply_chat_template(messages, add_generation_prompt=True,
enable_thinking=False, return_tensors="pt").to(model.device)
out = model.generate(inputs, max_new_tokens=1024, temperature=0.0, do_sample=False)
text = tok.decode(out[0][inputs.shape[1]:], skip_special_tokens=True)
sql = re.findall(r"```sql\s*(.*?)```", text, re.S | re.I)[-1].strip()
print(sql)
```
## Limitations
- **SELECT-only** — SynSQL-2.5M contains no DML/DDL, so the model was never trained on
INSERT/UPDATE/DELETE. Asked to modify data, it does not refuse — it silently reformulates
the request into a related SELECT. Do not use it to generate data-modifying queries.
- **SQLite dialect only** — queries may not be valid PostgreSQL/MySQL without adaptation.
- Schema and gold SQL are English; questions in other languages than Russian/English are untested.
- 0.8B parameters: complex multi-join / nested queries remain challenging; verify results before use.
- Training questions are machine-translated — residual translation artifacts are possible despite QE filtering.
## Pipeline
The full data pipeline (sampling → translation → QE filtering → SFT → execution-accuracy eval)
is implemented in the **rusql** project and runs entirely on a single consumer GPU.