Spaces:
Sleeping
Sleeping
Commit ·
564ad28
1
Parent(s): a7bb6b3
Migrate CPU serving to llama.cpp + Q5_K_M GGUF (was bf16 transformers)
Browse files- .gitignore +10 -0
- Dockerfile +15 -10
- requirements-space.txt +10 -0
- requirements.txt +4 -0
- scripts/CONVERT_GGUF.md +101 -0
- scripts/merge_adapter.py +77 -0
- scripts/validate_gguf.py +242 -0
- src/inference.py +109 -28
.gitignore
CHANGED
|
@@ -14,6 +14,16 @@ __pycache__/
|
|
| 14 |
models/phi3-text-to-sql/
|
| 15 |
models/phi3-text-to-sql-adapter/
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
# Local database (regenerated by database.py on cold start)
|
| 18 |
data/company_sales.db
|
| 19 |
|
|
|
|
| 14 |
models/phi3-text-to-sql/
|
| 15 |
models/phi3-text-to-sql-adapter/
|
| 16 |
|
| 17 |
+
# GGUF conversion intermediates (large; GGUF is hosted on HF Hub)
|
| 18 |
+
models/phi3-text-to-sql-merged/
|
| 19 |
+
*.gguf
|
| 20 |
+
llama.cpp/
|
| 21 |
+
|
| 22 |
+
# llama.cpp prebuilt release binaries (downloaded locally; never commit)
|
| 23 |
+
llama-b*/
|
| 24 |
+
*.dll
|
| 25 |
+
*.exe
|
| 26 |
+
|
| 27 |
# Local database (regenerated by database.py on cold start)
|
| 28 |
data/company_sales.db
|
| 29 |
|
Dockerfile
CHANGED
|
@@ -1,23 +1,28 @@
|
|
| 1 |
-
#
|
| 2 |
-
#
|
|
|
|
|
|
|
| 3 |
FROM python:3.11-slim
|
| 4 |
|
| 5 |
-
# System deps
|
|
|
|
| 6 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 7 |
git \
|
| 8 |
curl \
|
|
|
|
|
|
|
| 9 |
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
|
| 11 |
WORKDIR /app
|
| 12 |
|
| 13 |
-
# Install
|
| 14 |
RUN pip install --no-cache-dir \
|
| 15 |
-
|
| 16 |
-
--index-url https://
|
| 17 |
|
| 18 |
-
# Install
|
| 19 |
-
COPY requirements.txt .
|
| 20 |
-
RUN pip install --no-cache-dir -r requirements.txt
|
| 21 |
|
| 22 |
# Copy project source
|
| 23 |
COPY . .
|
|
@@ -32,7 +37,7 @@ RUN mkdir -p data models templates static
|
|
| 32 |
RUN useradd -m -u 1000 appuser && chown -R appuser /app
|
| 33 |
USER appuser
|
| 34 |
|
| 35 |
-
# Point HF cache to a writable location inside the container
|
| 36 |
ENV HF_HOME=/app/.cache/huggingface
|
| 37 |
|
| 38 |
CMD ["python", "src/server.py"]
|
|
|
|
| 1 |
+
# CPU-only Hugging Face Space image.
|
| 2 |
+
# Serving uses llama.cpp + a pre-merged GGUF (Q4_K_M), so there is no torch /
|
| 3 |
+
# transformers / bitsandbytes in the production image. This keeps the image small,
|
| 4 |
+
# cold starts fast, and RAM well under the 16 GB free-tier ceiling.
|
| 5 |
FROM python:3.11-slim
|
| 6 |
|
| 7 |
+
# System deps. build-essential + cmake are a safety net in case pip falls back to
|
| 8 |
+
# building llama-cpp-python from source; normally the prebuilt CPU wheel is used.
|
| 9 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 10 |
git \
|
| 11 |
curl \
|
| 12 |
+
build-essential \
|
| 13 |
+
cmake \
|
| 14 |
&& rm -rf /var/lib/apt/lists/*
|
| 15 |
|
| 16 |
WORKDIR /app
|
| 17 |
|
| 18 |
+
# Install llama-cpp-python from the prebuilt CPU wheel index (avoids a slow source build).
|
| 19 |
RUN pip install --no-cache-dir \
|
| 20 |
+
"llama-cpp-python>=0.3.0,<0.4.0" \
|
| 21 |
+
--extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu
|
| 22 |
|
| 23 |
+
# Install the slim CPU-serving dependencies.
|
| 24 |
+
COPY requirements-space.txt .
|
| 25 |
+
RUN pip install --no-cache-dir -r requirements-space.txt
|
| 26 |
|
| 27 |
# Copy project source
|
| 28 |
COPY . .
|
|
|
|
| 37 |
RUN useradd -m -u 1000 appuser && chown -R appuser /app
|
| 38 |
USER appuser
|
| 39 |
|
| 40 |
+
# Point HF cache to a writable location inside the container (GGUF is cached here)
|
| 41 |
ENV HF_HOME=/app/.cache/huggingface
|
| 42 |
|
| 43 |
CMD ["python", "src/server.py"]
|
requirements-space.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Slim dependency set for the Hugging Face CPU Space (used by the Dockerfile).
|
| 2 |
+
# CPU serving uses llama.cpp + a pre-merged GGUF, so torch/transformers/peft/
|
| 3 |
+
# bitsandbytes are NOT needed in the production image.
|
| 4 |
+
#
|
| 5 |
+
# NOTE: llama-cpp-python is installed separately in the Dockerfile from the
|
| 6 |
+
# prebuilt CPU wheel index to avoid a slow source build.
|
| 7 |
+
|
| 8 |
+
flask==3.0.3
|
| 9 |
+
pandas==2.2.3
|
| 10 |
+
huggingface_hub>=0.24.0
|
requirements.txt
CHANGED
|
@@ -15,3 +15,7 @@ pandas==2.2.3
|
|
| 15 |
|
| 16 |
# HF Hub (model + adapter download)
|
| 17 |
huggingface_hub>=0.24.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
# HF Hub (model + adapter download)
|
| 17 |
huggingface_hub>=0.24.0
|
| 18 |
+
|
| 19 |
+
# CPU/GGUF inference + local GGUF validation harness.
|
| 20 |
+
# Lazy-imported (CPU path only); the CUDA/laptop training path does not require it at runtime.
|
| 21 |
+
llama-cpp-python>=0.3.0,<0.4.0
|
scripts/CONVERT_GGUF.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Convert the fine-tuned model to GGUF (Q4_K_M) — run on your laptop
|
| 2 |
+
|
| 3 |
+
One-time process. Produces `phi3-text-to-sql-Q4_K_M.gguf` (~2.3 GB), which you
|
| 4 |
+
then push to a Hugging Face model repo. The CPU Space downloads it at startup.
|
| 5 |
+
|
| 6 |
+
Prerequisites: your trained adapter at `models/phi3-text-to-sql-adapter`, Python
|
| 7 |
+
env with `transformers`, `peft`, `torch` (your existing `.venv`), and `git`.
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## Step 1 — Merge the adapter into the base model
|
| 12 |
+
|
| 13 |
+
```bash
|
| 14 |
+
python scripts/merge_adapter.py
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
Output: `models/phi3-text-to-sql-merged/` (fp16, ~7.6 GB).
|
| 18 |
+
|
| 19 |
+
## Step 2 — Get llama.cpp (for the converter + quantizer)
|
| 20 |
+
|
| 21 |
+
```bash
|
| 22 |
+
git clone https://github.com/ggerganov/llama.cpp
|
| 23 |
+
pip install -r llama.cpp/requirements.txt
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
## Step 3 — Convert merged HF model → GGUF (F16)
|
| 27 |
+
|
| 28 |
+
```bash
|
| 29 |
+
python llama.cpp/convert_hf_to_gguf.py models/phi3-text-to-sql-merged \
|
| 30 |
+
--outfile models/phi3-text-to-sql-f16.gguf \
|
| 31 |
+
--outtype f16
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
## Step 4 — Quantize F16 → Q4_K_M
|
| 35 |
+
|
| 36 |
+
You need the `llama-quantize` binary. Two options:
|
| 37 |
+
|
| 38 |
+
**Option A — prebuilt binary (easiest on Windows):**
|
| 39 |
+
Download the latest llama.cpp release for your OS from
|
| 40 |
+
https://github.com/ggerganov/llama.cpp/releases (it includes `llama-quantize`
|
| 41 |
+
/ `llama-quantize.exe`), then:
|
| 42 |
+
|
| 43 |
+
```bash
|
| 44 |
+
# Linux/macOS
|
| 45 |
+
./llama-quantize models/phi3-text-to-sql-f16.gguf \
|
| 46 |
+
models/phi3-text-to-sql-Q4_K_M.gguf Q4_K_M
|
| 47 |
+
```
|
| 48 |
+
```powershell
|
| 49 |
+
# Windows
|
| 50 |
+
.\llama-quantize.exe models\phi3-text-to-sql-merged\..\phi3-text-to-sql-f16.gguf `
|
| 51 |
+
models\phi3-text-to-sql-Q4_K_M.gguf Q4_K_M
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
**Option B — build from source:**
|
| 55 |
+
```bash
|
| 56 |
+
cd llama.cpp && cmake -B build && cmake --build build --config Release -j
|
| 57 |
+
# binary at: build/bin/llama-quantize
|
| 58 |
+
./build/bin/llama-quantize ../models/phi3-text-to-sql-f16.gguf \
|
| 59 |
+
../models/phi3-text-to-sql-Q4_K_M.gguf Q4_K_M
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
Result: `models/phi3-text-to-sql-Q4_K_M.gguf` (~2.3 GB).
|
| 63 |
+
|
| 64 |
+
## Step 5 — Validate quality BEFORE deploying
|
| 65 |
+
|
| 66 |
+
```bash
|
| 67 |
+
python scripts/validate_gguf.py --gguf models/phi3-text-to-sql-Q4_K_M.gguf
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
Acceptance bar: ≥ 95% execution-equivalent on `data/test_dataset.jsonl`.
|
| 71 |
+
If it fails, redo Step 4 with `Q5_K_M` (larger/slower, higher fidelity) and
|
| 72 |
+
re-validate.
|
| 73 |
+
|
| 74 |
+
## Step 6 — Push the GGUF to Hugging Face
|
| 75 |
+
|
| 76 |
+
The Space pulls from repo `Bhuvandesai/phi3-text-to-sql-gguf`, file
|
| 77 |
+
`phi3-text-to-sql-Q4_K_M.gguf` (override via `GGUF_REPO_ID` / `GGUF_FILENAME`
|
| 78 |
+
env vars on the Space). Create the repo and upload:
|
| 79 |
+
|
| 80 |
+
```bash
|
| 81 |
+
huggingface-cli repo create phi3-text-to-sql-gguf --type model
|
| 82 |
+
huggingface-cli upload Bhuvandesai/phi3-text-to-sql-gguf \
|
| 83 |
+
models/phi3-text-to-sql-Q4_K_M.gguf phi3-text-to-sql-Q4_K_M.gguf
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
(If you used Q5_K_M, set `GGUF_FILENAME=phi3-text-to-sql-Q5_K_M.gguf` in the
|
| 87 |
+
Space settings and upload that file instead.)
|
| 88 |
+
|
| 89 |
+
## Step 7 — Deploy
|
| 90 |
+
|
| 91 |
+
Commit and push the repo. The Dockerfile installs llama-cpp-python and the slim
|
| 92 |
+
deps; on boot `src/inference.py` downloads the GGUF and serves on CPU.
|
| 93 |
+
|
| 94 |
+
---
|
| 95 |
+
|
| 96 |
+
### Notes
|
| 97 |
+
- Do **not** commit the multi-GB `.gguf` into the Space git repo. Host it on the
|
| 98 |
+
Hub (Step 6) so it is pulled at runtime and cached. If you ever want it bundled,
|
| 99 |
+
the loader also accepts a local copy at `models/<GGUF_FILENAME>`.
|
| 100 |
+
- `models/phi3-text-to-sql-merged/`, `*.gguf`, and `llama.cpp/` are intermediate
|
| 101 |
+
artifacts — keep them out of git (see `.gitignore`).
|
scripts/merge_adapter.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Merge the trained LoRA adapter into the Phi-3 base model and save a standalone
|
| 3 |
+
fp16 model. This merged model is the input to GGUF conversion.
|
| 4 |
+
|
| 5 |
+
Run on your laptop (the base model is already cached there):
|
| 6 |
+
|
| 7 |
+
python scripts/merge_adapter.py
|
| 8 |
+
|
| 9 |
+
Env overrides (optional):
|
| 10 |
+
BASE_MODEL_ID default: microsoft/Phi-3-mini-4k-instruct
|
| 11 |
+
ADAPTER_PATH default: models/phi3-text-to-sql-adapter
|
| 12 |
+
MERGED_OUT default: models/phi3-text-to-sql-merged
|
| 13 |
+
|
| 14 |
+
Notes:
|
| 15 |
+
- Merging is weight arithmetic (W' = W + (alpha/r) * B@A); it is mathematically
|
| 16 |
+
equivalent to applying the adapter. Under greedy decoding the SQL output is the
|
| 17 |
+
same as the current fine-tuned model.
|
| 18 |
+
- fp16 keeps the merged checkpoint ~7.6 GB. GGUF conversion + Q4_K_M quantization
|
| 19 |
+
shrinks it to ~2.3 GB.
|
| 20 |
+
"""
|
| 21 |
+
import os
|
| 22 |
+
import torch
|
| 23 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 24 |
+
from peft import PeftModel
|
| 25 |
+
|
| 26 |
+
BASE_MODEL_ID = os.environ.get("BASE_MODEL_ID", "microsoft/Phi-3-mini-4k-instruct")
|
| 27 |
+
ADAPTER_PATH = os.environ.get("ADAPTER_PATH", "models/phi3-text-to-sql-adapter")
|
| 28 |
+
MERGED_OUT = os.environ.get("MERGED_OUT", "models/phi3-text-to-sql-merged")
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def main():
|
| 32 |
+
print(f"Base model : {BASE_MODEL_ID}")
|
| 33 |
+
print(f"Adapter : {ADAPTER_PATH}")
|
| 34 |
+
print(f"Output : {MERGED_OUT}")
|
| 35 |
+
|
| 36 |
+
print("\nLoading tokenizer...")
|
| 37 |
+
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_ID, trust_remote_code=False)
|
| 38 |
+
|
| 39 |
+
print("Loading base model in fp16 (CPU)...")
|
| 40 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 41 |
+
BASE_MODEL_ID,
|
| 42 |
+
torch_dtype=torch.float16,
|
| 43 |
+
device_map=None,
|
| 44 |
+
low_cpu_mem_usage=True,
|
| 45 |
+
trust_remote_code=False,
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
print("Attaching adapter...")
|
| 49 |
+
model = PeftModel.from_pretrained(model, ADAPTER_PATH)
|
| 50 |
+
|
| 51 |
+
print("Merging adapter into base weights (merge_and_unload)...")
|
| 52 |
+
model = model.merge_and_unload()
|
| 53 |
+
|
| 54 |
+
os.makedirs(MERGED_OUT, exist_ok=True)
|
| 55 |
+
print(f"Saving merged model to {MERGED_OUT} ...")
|
| 56 |
+
model.save_pretrained(MERGED_OUT, safe_serialization=True)
|
| 57 |
+
tokenizer.save_pretrained(MERGED_OUT)
|
| 58 |
+
|
| 59 |
+
# Phi-3's GGUF converter needs the original SentencePiece tokenizer.model,
|
| 60 |
+
# which save_pretrained() does not emit for the fast tokenizer. Copy it over.
|
| 61 |
+
tok_model_dst = os.path.join(MERGED_OUT, "tokenizer.model")
|
| 62 |
+
if not os.path.exists(tok_model_dst):
|
| 63 |
+
try:
|
| 64 |
+
import shutil
|
| 65 |
+
from huggingface_hub import hf_hub_download
|
| 66 |
+
src = hf_hub_download(BASE_MODEL_ID, "tokenizer.model")
|
| 67 |
+
shutil.copy(src, tok_model_dst)
|
| 68 |
+
print("Copied tokenizer.model (required by the GGUF converter).")
|
| 69 |
+
except Exception as e:
|
| 70 |
+
print(f"WARNING: could not fetch tokenizer.model ({e}). "
|
| 71 |
+
"Copy it manually from the base model before converting.")
|
| 72 |
+
|
| 73 |
+
print("\nDone. Next: convert to GGUF (see scripts/CONVERT_GGUF.md).")
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
if __name__ == "__main__":
|
| 77 |
+
main()
|
scripts/validate_gguf.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Validate the GGUF model's SQL quality against the held-out test set BEFORE deploying.
|
| 3 |
+
|
| 4 |
+
For each example in data/test_dataset.jsonl:
|
| 5 |
+
1. Generate SQL from the GGUF model (greedy / temperature 0).
|
| 6 |
+
2. Execute both predicted and expected SQL on data/company_sales.db.
|
| 7 |
+
3. Count as a match if results are EXECUTION-EQUIVALENT (same rows, order-independent).
|
| 8 |
+
|
| 9 |
+
Acceptance bar: >= 95% match. If below, re-quantize at Q5_K_M and re-run.
|
| 10 |
+
|
| 11 |
+
Default backend: llama-server (recommended on Windows/local).
|
| 12 |
+
The harness launches llama-server.exe (from the llama.cpp release you already
|
| 13 |
+
downloaded), waits for it to load the model ONCE, then hits its OpenAI-compatible
|
| 14 |
+
/v1/chat/completions endpoint over HTTP -- the same call shape production uses.
|
| 15 |
+
This avoids:
|
| 16 |
+
- the prebuilt llama-cpp-python wheel (illegal-instruction 0xc000001d on CPUs
|
| 17 |
+
without the AVX-512 it was built for), and
|
| 18 |
+
- llama-cli interactive/conversation-mode hangs.
|
| 19 |
+
|
| 20 |
+
Usage:
|
| 21 |
+
python scripts/validate_gguf.py --gguf models/phi3-text-to-sql-Q4_K_M.gguf
|
| 22 |
+
python scripts/validate_gguf.py --gguf models/...gguf --server .\\llama-b9637-bin-win-cpu-x64\\llama-server.exe
|
| 23 |
+
python scripts/validate_gguf.py --gguf models/...gguf --use-python # only if the wheel runs on your CPU
|
| 24 |
+
|
| 25 |
+
Requires: data/company_sales.db (run `python src/database.py` first if missing).
|
| 26 |
+
"""
|
| 27 |
+
import argparse
|
| 28 |
+
import glob
|
| 29 |
+
import json
|
| 30 |
+
import os
|
| 31 |
+
import re
|
| 32 |
+
import sqlite3
|
| 33 |
+
import subprocess
|
| 34 |
+
import sys
|
| 35 |
+
import time
|
| 36 |
+
import urllib.error
|
| 37 |
+
import urllib.request
|
| 38 |
+
|
| 39 |
+
DEFAULT_GGUF = "models/phi3-text-to-sql-Q4_K_M.gguf"
|
| 40 |
+
TEST_PATH = "data/test_dataset.jsonl"
|
| 41 |
+
DB_PATH = "data/company_sales.db"
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def clean_sql(raw_output):
|
| 45 |
+
"""Same extraction logic as src/inference.py so validation matches serving."""
|
| 46 |
+
if "<|assistant|>" in raw_output:
|
| 47 |
+
raw_output = raw_output.split("<|assistant|>")[-1]
|
| 48 |
+
cleaned = re.sub(r"```sql\s*", "", raw_output, flags=re.IGNORECASE)
|
| 49 |
+
cleaned = re.sub(r"```", "", cleaned)
|
| 50 |
+
cleaned = re.sub(r"<\|.*?\|>", "", cleaned).strip()
|
| 51 |
+
m = re.search(r"(SELECT\s+.*?;)", cleaned, re.DOTALL | re.IGNORECASE)
|
| 52 |
+
if m:
|
| 53 |
+
return m.group(1).strip()
|
| 54 |
+
if "select" in cleaned.lower():
|
| 55 |
+
return cleaned.strip()
|
| 56 |
+
return cleaned.strip()
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def run_sql(conn, sql):
|
| 60 |
+
"""Returns (rows_as_sorted_list, error_or_None)."""
|
| 61 |
+
if not sql or "select" not in sql.lower():
|
| 62 |
+
return None, "invalid/blank SQL"
|
| 63 |
+
try:
|
| 64 |
+
cur = conn.cursor()
|
| 65 |
+
cur.execute(sql)
|
| 66 |
+
rows = cur.fetchall()
|
| 67 |
+
return sorted([tuple(r) for r in rows], key=lambda r: str(r)), None
|
| 68 |
+
except Exception as e:
|
| 69 |
+
return None, str(e)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def load_test_set(path):
|
| 73 |
+
items = []
|
| 74 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 75 |
+
for line in f:
|
| 76 |
+
line = line.strip()
|
| 77 |
+
if not line:
|
| 78 |
+
continue
|
| 79 |
+
obj = json.loads(line)
|
| 80 |
+
msgs = obj["messages"]
|
| 81 |
+
user = next(m["content"] for m in msgs if m["role"] == "user")
|
| 82 |
+
expected = next(m["content"] for m in msgs if m["role"] == "assistant")
|
| 83 |
+
question = user.split("Question:", 1)[-1].strip() if "Question:" in user else user
|
| 84 |
+
items.append({"user_prompt": user, "question": question, "expected_sql": expected})
|
| 85 |
+
return items
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
# ---------- llama-server backend ----------
|
| 89 |
+
|
| 90 |
+
def find_llama_server():
|
| 91 |
+
candidates = []
|
| 92 |
+
candidates += glob.glob(os.path.join("llama-b*", "llama-server.exe"))
|
| 93 |
+
candidates += glob.glob(os.path.join("llama-b*-bin-win-*", "llama-server.exe"))
|
| 94 |
+
candidates += glob.glob(os.path.join("llama-bin", "llama-server.exe"))
|
| 95 |
+
for name in ("llama-server.exe", "llama-server"):
|
| 96 |
+
if os.path.exists(name):
|
| 97 |
+
candidates.append(name)
|
| 98 |
+
return candidates[0] if candidates else None
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def start_server(server_path, gguf, n_ctx, port, log_path="llama-server.log"):
|
| 102 |
+
cmd = [server_path, "-m", gguf, "-c", str(n_ctx),
|
| 103 |
+
"--host", "127.0.0.1", "--port", str(port)]
|
| 104 |
+
print(f"Starting llama-server (logs -> {log_path}) ...")
|
| 105 |
+
log = open(log_path, "w", encoding="utf-8")
|
| 106 |
+
proc = subprocess.Popen(cmd, stdout=log, stderr=subprocess.STDOUT)
|
| 107 |
+
|
| 108 |
+
health = f"http://127.0.0.1:{port}/health"
|
| 109 |
+
for _ in range(180): # up to ~3 min for first model load
|
| 110 |
+
if proc.poll() is not None:
|
| 111 |
+
raise RuntimeError(f"llama-server exited early; see {log_path}")
|
| 112 |
+
try:
|
| 113 |
+
with urllib.request.urlopen(health, timeout=2) as r:
|
| 114 |
+
if r.status == 200 and json.loads(r.read()).get("status") == "ok":
|
| 115 |
+
print("Server ready.")
|
| 116 |
+
return proc
|
| 117 |
+
except Exception:
|
| 118 |
+
pass
|
| 119 |
+
time.sleep(1)
|
| 120 |
+
proc.terminate()
|
| 121 |
+
raise RuntimeError(f"llama-server did not become ready in time; see {log_path}")
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def make_http_generator(port):
|
| 125 |
+
url = f"http://127.0.0.1:{port}/v1/chat/completions"
|
| 126 |
+
|
| 127 |
+
def gen(user_content):
|
| 128 |
+
body = json.dumps({
|
| 129 |
+
"messages": [{"role": "user", "content": user_content}],
|
| 130 |
+
"temperature": 0.0,
|
| 131 |
+
"max_tokens": 150,
|
| 132 |
+
}).encode("utf-8")
|
| 133 |
+
req = urllib.request.Request(url, data=body, headers={"Content-Type": "application/json"})
|
| 134 |
+
with urllib.request.urlopen(req, timeout=180) as r:
|
| 135 |
+
out = json.loads(r.read())
|
| 136 |
+
return out["choices"][0]["message"]["content"]
|
| 137 |
+
return gen
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def make_python_generator(gguf, n_ctx):
|
| 141 |
+
from llama_cpp import Llama
|
| 142 |
+
llm = Llama(model_path=gguf, n_ctx=n_ctx, n_threads=os.cpu_count() or 2, verbose=False)
|
| 143 |
+
|
| 144 |
+
def gen(user_content):
|
| 145 |
+
out = llm.create_chat_completion(
|
| 146 |
+
messages=[{"role": "user", "content": user_content}],
|
| 147 |
+
max_tokens=150, temperature=0.0,
|
| 148 |
+
)
|
| 149 |
+
return out["choices"][0]["message"]["content"]
|
| 150 |
+
return gen
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def main():
|
| 154 |
+
ap = argparse.ArgumentParser()
|
| 155 |
+
ap.add_argument("--gguf", default=DEFAULT_GGUF, help="Path to the GGUF file")
|
| 156 |
+
ap.add_argument("--threshold", type=float, default=0.95, help="Acceptance match rate")
|
| 157 |
+
ap.add_argument("--n_ctx", type=int, default=2048)
|
| 158 |
+
ap.add_argument("--server", default=None, help="Path to llama-server(.exe). Auto-detected if omitted.")
|
| 159 |
+
ap.add_argument("--port", type=int, default=8080)
|
| 160 |
+
ap.add_argument("--use-python", action="store_true", help="Use llama-cpp-python bindings instead of llama-server")
|
| 161 |
+
args = ap.parse_args()
|
| 162 |
+
|
| 163 |
+
if not os.path.exists(args.gguf):
|
| 164 |
+
sys.exit(f"GGUF not found: {args.gguf}")
|
| 165 |
+
if not os.path.exists(TEST_PATH):
|
| 166 |
+
sys.exit(f"Test set not found: {TEST_PATH}")
|
| 167 |
+
if not os.path.exists(DB_PATH):
|
| 168 |
+
sys.exit(f"Database not found: {DB_PATH}. Run `python src/database.py` first.")
|
| 169 |
+
|
| 170 |
+
server_proc = None
|
| 171 |
+
try:
|
| 172 |
+
if args.use_python:
|
| 173 |
+
print("Backend: llama-cpp-python bindings")
|
| 174 |
+
generate = make_python_generator(args.gguf, args.n_ctx)
|
| 175 |
+
else:
|
| 176 |
+
server = args.server or find_llama_server()
|
| 177 |
+
if not server:
|
| 178 |
+
sys.exit("Could not find llama-server(.exe). Pass --server <path>, "
|
| 179 |
+
"or use --use-python if the wheel runs on your CPU.")
|
| 180 |
+
print(f"Backend: llama-server -> {server}")
|
| 181 |
+
server_proc = start_server(server, args.gguf, args.n_ctx, args.port)
|
| 182 |
+
generate = make_http_generator(args.port)
|
| 183 |
+
|
| 184 |
+
tests = load_test_set(TEST_PATH)
|
| 185 |
+
conn = sqlite3.connect(DB_PATH)
|
| 186 |
+
|
| 187 |
+
passed = 0
|
| 188 |
+
failures = []
|
| 189 |
+
for i, t in enumerate(tests, 1):
|
| 190 |
+
raw = generate(t["user_prompt"])
|
| 191 |
+
pred_sql = clean_sql(raw)
|
| 192 |
+
|
| 193 |
+
exp_rows, exp_err = run_sql(conn, t["expected_sql"])
|
| 194 |
+
pred_rows, pred_err = run_sql(conn, pred_sql)
|
| 195 |
+
|
| 196 |
+
if exp_err is not None:
|
| 197 |
+
ok = pred_sql.strip().lower().rstrip(";") == t["expected_sql"].strip().lower().rstrip(";")
|
| 198 |
+
else:
|
| 199 |
+
ok = (pred_err is None) and (pred_rows == exp_rows)
|
| 200 |
+
|
| 201 |
+
status = "PASS" if ok else "FAIL"
|
| 202 |
+
if ok:
|
| 203 |
+
passed += 1
|
| 204 |
+
else:
|
| 205 |
+
failures.append({
|
| 206 |
+
"n": i, "question": t["question"],
|
| 207 |
+
"expected": t["expected_sql"], "predicted": pred_sql, "pred_err": pred_err,
|
| 208 |
+
})
|
| 209 |
+
print(f"[{i:02d}/{len(tests)}] {status} {t['question'][:70]}")
|
| 210 |
+
|
| 211 |
+
conn.close()
|
| 212 |
+
|
| 213 |
+
rate = passed / len(tests) if tests else 0.0
|
| 214 |
+
print("\n" + "=" * 60)
|
| 215 |
+
print(f"Execution-equivalent: {passed}/{len(tests)} = {rate:.1%}")
|
| 216 |
+
print(f"Acceptance bar : {args.threshold:.0%}")
|
| 217 |
+
print("Result :", "ACCEPTED" if rate >= args.threshold else "BELOW BAR (try Q5_K_M)")
|
| 218 |
+
print("=" * 60)
|
| 219 |
+
|
| 220 |
+
if failures:
|
| 221 |
+
print("\nFailures:")
|
| 222 |
+
for f in failures:
|
| 223 |
+
print(f"\n Q{f['n']}: {f['question']}")
|
| 224 |
+
print(f" expected : {f['expected']}")
|
| 225 |
+
print(f" predicted: {f['predicted']}")
|
| 226 |
+
if f["pred_err"]:
|
| 227 |
+
print(f" sql error: {f['pred_err']}")
|
| 228 |
+
|
| 229 |
+
exit_code = 0 if rate >= args.threshold else 1
|
| 230 |
+
finally:
|
| 231 |
+
if server_proc is not None:
|
| 232 |
+
server_proc.terminate()
|
| 233 |
+
try:
|
| 234 |
+
server_proc.wait(timeout=10)
|
| 235 |
+
except Exception:
|
| 236 |
+
server_proc.kill()
|
| 237 |
+
|
| 238 |
+
sys.exit(exit_code)
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
if __name__ == "__main__":
|
| 242 |
+
main()
|
src/inference.py
CHANGED
|
@@ -15,6 +15,13 @@ class TextToSQLInference:
|
|
| 15 |
self.db_path = "data/company_sales.db"
|
| 16 |
self.tokenizer = None
|
| 17 |
self.model = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
# Support both local paths and HF Hub repo IDs
|
| 19 |
self.has_adapter = self._adapter_available(adapter_path)
|
| 20 |
|
|
@@ -33,36 +40,39 @@ class TextToSQLInference:
|
|
| 33 |
return False
|
| 34 |
|
| 35 |
def _load_model_and_tokenizer(self):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
print(f"Loading tokenizer: {self.base_model_id}...")
|
| 37 |
self.tokenizer = AutoTokenizer.from_pretrained(self.base_model_id, trust_remote_code=False)
|
| 38 |
self.tokenizer.pad_token = self.tokenizer.eos_token
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
self.
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
else:
|
| 57 |
-
print("CPU detected — loading model in bfloat16 (no quantization)...")
|
| 58 |
-
self.model = AutoModelForCausalLM.from_pretrained(
|
| 59 |
-
self.base_model_id,
|
| 60 |
-
torch_dtype=torch.bfloat16,
|
| 61 |
-
device_map="cpu",
|
| 62 |
-
trust_remote_code=False,
|
| 63 |
-
attn_implementation="eager"
|
| 64 |
-
)
|
| 65 |
-
|
| 66 |
if self.has_adapter:
|
| 67 |
print(f"Loading fine-tuned adapter from: {self.adapter_path}...")
|
| 68 |
self.model = PeftModel.from_pretrained(
|
|
@@ -74,6 +84,36 @@ class TextToSQLInference:
|
|
| 74 |
else:
|
| 75 |
print("WARNING: No adapter found. Will run base model only.")
|
| 76 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
def clean_sql(self, raw_output):
|
| 78 |
"""Cleans and extracts SQL query from model output, stripping backticks and markdown formatting."""
|
| 79 |
# Print for raw debugging
|
|
@@ -119,9 +159,50 @@ class TextToSQLInference:
|
|
| 119 |
return None, str(e)
|
| 120 |
|
| 121 |
def generate_sql(self, question, use_adapter=True):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
"""Generates SQL query from a natural language question."""
|
| 123 |
prompt = f"{SCHEMA_PROMPT}\n\nQuestion: {question}"
|
| 124 |
-
|
| 125 |
# Standard Phi-3 ChatML template formatting
|
| 126 |
messages = [
|
| 127 |
{"role": "user", "content": prompt}
|
|
|
|
| 15 |
self.db_path = "data/company_sales.db"
|
| 16 |
self.tokenizer = None
|
| 17 |
self.model = None
|
| 18 |
+
self.llm = None # llama_cpp.Llama instance (CPU/GGUF backend only)
|
| 19 |
+
self.backend = None # "transformers" (CUDA) or "llama_cpp" (CPU)
|
| 20 |
+
|
| 21 |
+
# GGUF source for CPU serving (override via env on the Space)
|
| 22 |
+
self.gguf_repo_id = os.environ.get("GGUF_REPO_ID", "Bhuvandesai/phi3-text-to-sql-gguf")
|
| 23 |
+
self.gguf_filename = os.environ.get("GGUF_FILENAME", "phi3-text-to-sql-Q5_K_M.gguf")
|
| 24 |
+
|
| 25 |
# Support both local paths and HF Hub repo IDs
|
| 26 |
self.has_adapter = self._adapter_available(adapter_path)
|
| 27 |
|
|
|
|
| 40 |
return False
|
| 41 |
|
| 42 |
def _load_model_and_tokenizer(self):
|
| 43 |
+
# Two backends, chosen by hardware:
|
| 44 |
+
# - CUDA (laptop/dev): transformers + 4-bit bitsandbytes + PEFT adapter
|
| 45 |
+
# - CPU (HF Space): llama.cpp + pre-merged GGUF (Q4_K_M)
|
| 46 |
+
# The CPU path never imports torch-side quantization or PEFT, and llama_cpp
|
| 47 |
+
# is imported lazily so the CUDA path doesn't need it installed.
|
| 48 |
+
if self.device == "cuda":
|
| 49 |
+
self.backend = "transformers"
|
| 50 |
+
self._load_transformers_cuda()
|
| 51 |
+
else:
|
| 52 |
+
self.backend = "llama_cpp"
|
| 53 |
+
self._load_llama_cpp_cpu()
|
| 54 |
+
|
| 55 |
+
def _load_transformers_cuda(self):
|
| 56 |
print(f"Loading tokenizer: {self.base_model_id}...")
|
| 57 |
self.tokenizer = AutoTokenizer.from_pretrained(self.base_model_id, trust_remote_code=False)
|
| 58 |
self.tokenizer.pad_token = self.tokenizer.eos_token
|
| 59 |
+
|
| 60 |
+
print("CUDA detected — configuring 4-bit quantization...")
|
| 61 |
+
bnb_config = BitsAndBytesConfig(
|
| 62 |
+
load_in_4bit=True,
|
| 63 |
+
bnb_4bit_quant_type="nf4",
|
| 64 |
+
bnb_4bit_use_double_quant=True,
|
| 65 |
+
bnb_4bit_compute_dtype=torch.float16
|
| 66 |
+
)
|
| 67 |
+
print("Loading base model with 4-bit quantization...")
|
| 68 |
+
self.model = AutoModelForCausalLM.from_pretrained(
|
| 69 |
+
self.base_model_id,
|
| 70 |
+
quantization_config=bnb_config,
|
| 71 |
+
device_map="auto",
|
| 72 |
+
trust_remote_code=False,
|
| 73 |
+
attn_implementation="eager"
|
| 74 |
+
)
|
| 75 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
if self.has_adapter:
|
| 77 |
print(f"Loading fine-tuned adapter from: {self.adapter_path}...")
|
| 78 |
self.model = PeftModel.from_pretrained(
|
|
|
|
| 84 |
else:
|
| 85 |
print("WARNING: No adapter found. Will run base model only.")
|
| 86 |
|
| 87 |
+
def _load_llama_cpp_cpu(self):
|
| 88 |
+
# GGUF already contains the merged fine-tuned weights, so there is no
|
| 89 |
+
# separate adapter to load at runtime.
|
| 90 |
+
self.has_adapter = True
|
| 91 |
+
print("CPU detected — using llama.cpp + GGUF (Q4_K_M) backend.")
|
| 92 |
+
|
| 93 |
+
from llama_cpp import Llama
|
| 94 |
+
from huggingface_hub import hf_hub_download
|
| 95 |
+
|
| 96 |
+
# Allow a local GGUF (committed under models/) to take precedence; otherwise pull from Hub.
|
| 97 |
+
local_candidate = os.path.join("models", self.gguf_filename)
|
| 98 |
+
if os.path.exists(local_candidate):
|
| 99 |
+
model_path = local_candidate
|
| 100 |
+
print(f"Loading local GGUF: {model_path}")
|
| 101 |
+
else:
|
| 102 |
+
print(f"Downloading GGUF {self.gguf_filename} from {self.gguf_repo_id}...")
|
| 103 |
+
model_path = hf_hub_download(repo_id=self.gguf_repo_id, filename=self.gguf_filename)
|
| 104 |
+
print(f"GGUF cached at: {model_path}")
|
| 105 |
+
|
| 106 |
+
n_threads = os.cpu_count() or 2
|
| 107 |
+
self.llm = Llama(
|
| 108 |
+
model_path=model_path,
|
| 109 |
+
n_ctx=2048, # schema prompt (~300 tok) + question + 150 generated
|
| 110 |
+
n_threads=n_threads,
|
| 111 |
+
n_batch=512,
|
| 112 |
+
logits_all=False,
|
| 113 |
+
verbose=False,
|
| 114 |
+
)
|
| 115 |
+
print(f"llama.cpp model loaded (n_threads={n_threads}).")
|
| 116 |
+
|
| 117 |
def clean_sql(self, raw_output):
|
| 118 |
"""Cleans and extracts SQL query from model output, stripping backticks and markdown formatting."""
|
| 119 |
# Print for raw debugging
|
|
|
|
| 159 |
return None, str(e)
|
| 160 |
|
| 161 |
def generate_sql(self, question, use_adapter=True):
|
| 162 |
+
"""Dispatches to the active backend. Output contract is identical for both."""
|
| 163 |
+
if self.backend == "llama_cpp":
|
| 164 |
+
return self._generate_llama(question, use_adapter=use_adapter)
|
| 165 |
+
return self._generate_transformers(question, use_adapter=use_adapter)
|
| 166 |
+
|
| 167 |
+
def _generate_llama(self, question, use_adapter=True):
|
| 168 |
+
"""CPU/GGUF generation via llama.cpp. The GGUF is the merged fine-tuned model."""
|
| 169 |
+
if not use_adapter:
|
| 170 |
+
# GGUF has the fine-tuned weights baked in; there is no base toggle on CPU.
|
| 171 |
+
# The base comparison is served by generate_sql_base_via_api (HF API) instead.
|
| 172 |
+
# query_pipeline catches this and returns a clean error rather than crashing.
|
| 173 |
+
raise RuntimeError(
|
| 174 |
+
"Local base-model comparison is unavailable in CPU/GGUF mode; "
|
| 175 |
+
"use the HF Inference API path for base comparison."
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
prompt = f"{SCHEMA_PROMPT}\n\nQuestion: {question}"
|
| 179 |
+
print("--- Generating with Fine-tuned GGUF (llama.cpp) ---")
|
| 180 |
+
|
| 181 |
+
start_time = time.perf_counter()
|
| 182 |
+
result = self.llm.create_chat_completion(
|
| 183 |
+
messages=[{"role": "user", "content": prompt}],
|
| 184 |
+
max_tokens=150,
|
| 185 |
+
temperature=0.0, # greedy / deterministic, matches do_sample=False
|
| 186 |
+
)
|
| 187 |
+
latency_sec = time.perf_counter() - start_time
|
| 188 |
+
|
| 189 |
+
raw_output = result["choices"][0]["message"]["content"]
|
| 190 |
+
usage = result.get("usage", {}) or {}
|
| 191 |
+
tokens_generated = usage.get("completion_tokens", 0) or 0
|
| 192 |
+
tokens_per_second = tokens_generated / latency_sec if latency_sec > 0 else 0.0
|
| 193 |
+
|
| 194 |
+
sql_query = self.clean_sql(raw_output)
|
| 195 |
+
metrics = {
|
| 196 |
+
"latency_sec": latency_sec,
|
| 197 |
+
"tokens_generated": tokens_generated,
|
| 198 |
+
"tokens_per_second": tokens_per_second
|
| 199 |
+
}
|
| 200 |
+
return sql_query, metrics
|
| 201 |
+
|
| 202 |
+
def _generate_transformers(self, question, use_adapter=True):
|
| 203 |
"""Generates SQL query from a natural language question."""
|
| 204 |
prompt = f"{SCHEMA_PROMPT}\n\nQuestion: {question}"
|
| 205 |
+
|
| 206 |
# Standard Phi-3 ChatML template formatting
|
| 207 |
messages = [
|
| 208 |
{"role": "user", "content": prompt}
|