Kasualdad commited on
Commit
8cbb8cf
·
1 Parent(s): 0e9c140

docs: add comprehensive PROJECT.md knowledge base for AI agent context

Browse files
Files changed (1) hide show
  1. PROJECT.md +464 -0
PROJECT.md ADDED
@@ -0,0 +1,464 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Kasualdad LFED — Project Knowledge Base
2
+
3
+ > **Last updated:** 2026-06-06 (Session 3)
4
+ > **Target:** HF Build Small Hackathon — "Backyard AI" chapter
5
+ > **Space:** https://huggingface.co/spaces/build-small-hackathon/Kasualdad_LFED
6
+ > **Remote:** `git@hf.co:spaces/build-small-hackathon/Kasualdad_LFED` (remote name: `space`)
7
+
8
+ ---
9
+
10
+ ## What It Does
11
+
12
+ A Gradio app that lets school district admins ask plain-English questions about student data. A local LLM (llama.cpp + Qwen2.5-Coder-7B GGUF) generates DuckDB SQL, which is validated and executed on in-memory seed data. **Nothing leaves the machine — no API calls, no cloud.**
13
+
14
+ ---
15
+
16
+ ## Architecture & Data Flow
17
+
18
+ ```
19
+ User types question
20
+
21
+ app.py → handle_query() [@spaces.GPU on HF Zero GPU]
22
+
23
+ model_inference.py → build_prompt() + llama.cpp inference
24
+ ↓ raw LLM output (JSON or ```sql```)
25
+ data_engine.py → extract_sql() → validate_sql() → execute_safe()
26
+ ↓ pandas DataFrame
27
+ app.py → renders SQL + table in Gradio UI
28
+ ```
29
+
30
+ **Key design rule:** `app.py` is a thin controller. All logic lives in the three engine modules.
31
+
32
+ ---
33
+
34
+ ## File Map
35
+
36
+ | File | Purpose | Lines |
37
+ |---|---|---|
38
+ | `app.py` | Gradio UI, CSS, `@spaces.GPU` wiring, Parquet bootstrap | ~380 |
39
+ | `model_inference.py` | llama.cpp wrapper, CUDA preload, model cache, SQL generation + streaming | ~290 |
40
+ | `data_engine.py` | DuckDB lifecycle, Parquet loading, SQL extraction/validation/execution | ~280 |
41
+ | `prompts.py` | System prompt, schema docs, few-shot examples, prompt assembler | ~130 |
42
+ | `data/generate_seed.py` | Generates 2,900 students, enrollment, attendance | ~200 |
43
+ | `data/export_parquet.py` | One-shot script: seed → Parquet files | ~60 |
44
+ | `data/enrollment.parquet` | Pre-generated enrollment table (1,526 bytes) | — |
45
+ | `data/attendance.parquet` | Pre-generated attendance table (31,482 bytes) | — |
46
+ | `requirements.txt` | Pinned Python deps (see below) | 9 lines |
47
+ | `README.md` | Public Space README | ~280 |
48
+ | `docs/HANDOFF.md` | Developer session handoff doc | ~190 |
49
+ | `docs/PLAN.md` | Original build plan | ~170 |
50
+ | `tests/` | pytest suite (81 tests: execution guard, data engine, model inference) | 4 files |
51
+ | `modal_train/` | Modal fine-tuning pipeline (synthetic data, Unsloth QLoRA, GGUF export) | 5 files |
52
+
53
+ ---
54
+
55
+ ## Model & Inference
56
+
57
+ ### Active Model
58
+
59
+ - **Repo:** `mradermacher/Qwen2.5-Coder-7B-Instruct-GGUF`
60
+ - **File:** `Qwen2.5-Coder-7B-Instruct.Q4_K_M.gguf` (~4.68 GB)
61
+ - **Quantization:** Q4_K_M
62
+ - **Context:** 4096 tokens (`n_ctx`)
63
+ - **GPU layers:** `-1` (all layers; Zero GPU handles context switching)
64
+
65
+ ### Model Resolution
66
+
67
+ `model_inference.py:_resolve_model_path()` [model_inference.py:124]
68
+ 1. Check local cache: `/tmp/lfed-models/qwen/...` (fast if pre-downloaded)
69
+ 2. Fallback: download from HF Hub via `huggingface_hub.hf_hub_download()`
70
+
71
+ ### Inference Config
72
+
73
+ | Parameter | Local | HF Zero GPU | Note |
74
+ |---|---|---|---|
75
+ | `n_ctx` | 4096 | 4096 | |
76
+ | `n_threads` | 2 | 4 | Detected via `SPACES_ZERO_GPU` env |
77
+ | `n_gpu_layers` | -1 | -1 | Always GPU; Zero GPU emulation at module level |
78
+ | `max_tokens` | 256 | 256 | |
79
+ | `temperature` | 0.0 | 0.0 | Deterministic |
80
+ | `stop` | `\n\n`, `Question:`, `User:` | same | |
81
+
82
+ ### Thread Safety
83
+
84
+ `model_inference.py` caches the model in a module-level `_llm` global with a `threading.Lock` (double-check pattern). `generate_sql()` auto-loads if `llm=None`.
85
+
86
+ ---
87
+
88
+ ## Zero GPU / HF Space Configuration
89
+
90
+ ### Space Identity
91
+ - **URL:** `https://huggingface.co/spaces/build-small-hackathon/Kasualdad_LFED`
92
+ - **SDK:** Gradio 6.16.0
93
+ - **Python:** 3.12
94
+ - **Hardware:** Zero GPU (NVIDIA RTX Pro 6000 Blackwell, half — 48 GB VRAM)
95
+ - **Git remote:** `space` → `https://huggingface.co/spaces/build-small-hackathon/Kasualdad_LFED`
96
+
97
+ ### Zero GPU Architecture
98
+
99
+ Zero GPU uses **CUDA emulation** at module level and **real GPU** inside `@spaces.GPU` functions. This means:
100
+ - Model loads with `n_gpu_layers=-1` at startup (module level, emulated CUDA)
101
+ - Inference runs inside `@spaces.GPU` decorated `handle_query()` (real GPU)
102
+ - Zero GPU transparently switches between emulated and real CUDA contexts
103
+
104
+ ### `@spaces.GPU` Decorator
105
+
106
+ `app.py:37` — `handle_query()` is the only decorated function. Required for Zero GPU to recognize the Space as GPU-enabled.
107
+
108
+ ### Startup Sequence (app.py:16-30)
109
+
110
+ 1. Print banner
111
+ 2. **Ensure Parquet seed files exist** — if `/data/enrollment.parquet` and `/data/attendance.parquet` aren't found, generate them via `export_parquet.py` into `/data/` (persistent Space storage) or `data/` (local dev)
112
+ 3. Load model (`load_model()`) — downloads from HF Hub if not cached, loads into GPU/RAM
113
+ 4. Launch Gradio UI
114
+
115
+ ---
116
+
117
+ ## CUDA Dependency Story (Full Debugging History)
118
+
119
+ ### Problem
120
+ `llama-cpp-python` installed with CUDA 12.1 wheel (`--extra-index-url .../cu121`). At import time, `libllama.so` needs multiple CUDA shared libraries (`libcudart.so.12`, `libcublas.so.12`, `libcublasLt.so.12`, etc.), but they aren't on the system linker path in the HF Space container.
121
+
122
+ ### Failed Attempt #1 — `LD_LIBRARY_PATH` via `os.environ`
123
+ **Why it failed:** The C dynamic linker (`ld.so`) reads `LD_LIBRARY_PATH` once at process start. Setting `os.environ["LD_LIBRARY_PATH"]` in Python at runtime has zero effect on `dlopen()`.
124
+
125
+ ### Failed Attempt #2 — Load only `libcudart.so.12`
126
+ **Why it failed:** `libllama.so` also needs `libcublas.so.12`, `libcublasLt.so.12`, and other CUDA libs. Loading just `libcudart` got us past the first error but hit the next missing library.
127
+
128
+ ### Working Solution — Bulk ctypes preload
129
+ `model_inference.py:20-78`
130
+
131
+ ```python
132
+ import ctypes
133
+
134
+ def _preload_dir(lib_dir):
135
+ """Preload every lib*.so* in a directory with RTLD_GLOBAL."""
136
+ for f in sorted(os.listdir(lib_dir)):
137
+ if f.startswith("lib") and ".so" in f:
138
+ ctypes.CDLL(os.path.join(lib_dir, f), mode=ctypes.RTLD_GLOBAL)
139
+ ```
140
+
141
+ **Why it works:** `ctypes.CDLL(full_path, mode=RTLD_GLOBAL)` calls `dlopen()` with the full path + `RTLD_GLOBAL` flag. This loads the library into the process and registers it under its SONAME. When `libllama.so` is subsequently `dlopen()`'d, the linker finds all CUDA dependencies already resident.
142
+
143
+ **Search order:**
144
+ 1. System CUDA: `/usr/local/cuda/lib64`, `/usr/local/cuda-12.1/lib64`, `/usr/local/cuda-12.4/lib64`
145
+ 2. Pip packages: iterate `sys.path`, find `nvidia/*/lib/` directories, preload all `.so` files
146
+
147
+ ### Required CUDA Pip Packages
148
+
149
+ | Package | Version | Provides |
150
+ |---|---|---|
151
+ | `nvidia-cuda-runtime-cu12` | 12.1.105 | `libcudart.so.12` |
152
+ | `nvidia-cublas-cu12` | 12.1.3.1 | `libcublas.so.12`, `libcublasLt.so.12` |
153
+ | `nvidia-cusparse-cu12` | 12.1.0.106 | `libcusparse.so.12` |
154
+
155
+ These are in `requirements.txt`. On the HF Space, the bulk preload picked up **39 total CUDA shared libraries** from the transitive dependencies of these packages.
156
+
157
+ ### Import Guard
158
+
159
+ `model_inference.py:81-91` — If the `from llama_cpp import Llama` still fails (truly missing CUDA), a `RuntimeError` with a clear diagnostic is raised:
160
+
161
+ ```
162
+ Failed to load llama-cpp-python — CUDA runtime not found.
163
+ If this is a CPU-only machine, install the CPU wheel:
164
+ pip uninstall llama-cpp-python -y && pip install llama-cpp-python
165
+ ```
166
+
167
+ ---
168
+
169
+ ## UI Design System
170
+
171
+ ### Current Palette (Cool Professional — slate + indigo)
172
+
173
+ | Token | Value | Usage |
174
+ |---|---|---|
175
+ | `--bg` | `#f1f5f9` (slate-100) | Page background |
176
+ | `--surface` | `#ffffff` | Cards, inputs, code blocks, tables |
177
+ | `--surface-alt` | `#f8fafc` (slate-50) | Table headers |
178
+ | `--border` | `#e2e8f0` (slate-200) | All borders |
179
+ | `--text` | `#1e293b` (slate-800) | Primary body text |
180
+ | `--text-muted` | `#64748b` (slate-500) | Secondary text, labels |
181
+ | `--action` | `#4f46e5` (indigo-600) | Primary CTA buttons |
182
+ | `--action-hover` | `#4338ca` (indigo-700) | Button hover state |
183
+ | `--error` | `#b91c1c` (red-700) | Error messages |
184
+ | `--success` | `#059669` (emerald-600) | Success messages |
185
+ | `--radius` | `12px` | All containers, inputs |
186
+ | `--radius-lg` | `20px` | (defined but not heavily used) |
187
+ | `--transition` | `120ms ease-out` | Hover/focus transitions |
188
+
189
+ ### Fonts
190
+ - **Headings:** Cormorant Garamond (serif) — `--font-display`
191
+ - **Body/UI:** Atkinson Hyperlegible (sans) — `--font-ui`
192
+ - **Code:** JetBrains Mono — `--font-mono`
193
+
194
+ All loaded via Google Fonts in `HEAD_HTML` (`app.py:345`).
195
+
196
+ ### Gradio Dark-Theme Overrides
197
+ `app.py:77-82` — A "nuke block" strips Gradio's dark-theme defaults before our rules apply:
198
+ ```css
199
+ .gr-textbox, .gr-code, .gr-dataframe, .gr-accordion {
200
+ background: transparent !important;
201
+ border-color: transparent !important;
202
+ }
203
+ ```
204
+
205
+ The CSS uses aggressive descendant selectors (`> div > div`, `[data-testid]`) because Gradio nests components in multiple wrapper divs.
206
+
207
+ ### Design Evolution
208
+ 1. **Original:** Linear/Vercel inspired — teal accent (`#14b8a6`), Inter font, white backgrounds
209
+ 2. **WellFull Collective:** Warm terracotta + sage palette — rejected (looked "bad" for a data tool)
210
+ 3. **Current:** Cool slate + indigo professional palette with the WellFull structure (fonts, radii, spacing)
211
+
212
+ ---
213
+
214
+ ## Data Engine & Parquet Optimization
215
+
216
+ ### Per-Request Lifecycle
217
+ `data_engine.py:create_session()` → `get_connection()` + `seed_database()`
218
+
219
+ Each query creates a **fresh in-memory DuckDB** connection. This ensures:
220
+ - Thread safety (no shared state between requests)
221
+ - Query isolation (can't affect other requests)
222
+ - Clean state (no stale data)
223
+
224
+ ### Seeding Priority
225
+ `data_engine.py:seed_database()` [data_engine.py:62-96]
226
+
227
+ 1. **Parquet files** (fastest — 33 KB, single-digit ms):
228
+ - `/data/enrollment.parquet` + `/data/attendance.parquet` (Space persistent storage)
229
+ - `data/enrollment.parquet` + `data/attendance.parquet` (local dev)
230
+ - Loaded via DuckDB `read_parquet()` → `CREATE TABLE ... AS SELECT *`
231
+ 2. **`data/seed.sql`** (custom overrides)
232
+ 3. **Python generator** `data/generate_seed.py` (slow fallback — ~2,900 students, ~11,600 attendance rows)
233
+
234
+ ### Parquet Bootstrap
235
+ `app.py:22-29` — On startup, if no Parquet files are found in any of the search dirs, `export_parquet.py` is called to generate them. On the Space, they go to `/data/` which persists across restarts.
236
+
237
+ ### SQL Safety Pipeline
238
+ `data_engine.py:execute_safe()` [data_engine.py:235-260]
239
+
240
+ 1. `extract_sql()` — Parse JSON envelope → ```sql``` block → raw fallback
241
+ 2. `validate_sql()` — Forbidden token check (DROP, DELETE, INSERT, UPDATE, etc.) + schema-aware `EXPLAIN` validation
242
+ 3. Wrap: `SELECT * FROM (<user_query>) AS _safe LIMIT 1000`
243
+ 4. Execute directly on DuckDB
244
+ 5. Return `(cleaned_sql, DataFrame)`
245
+
246
+ ### Forbidden Tokens
247
+ `data_engine.py:22-25` — `DROP`, `DELETE`, `INSERT`, `UPDATE`, `ALTER`, `TRUNCATE`, `CREATE`, `ATTACH`, `DETACH`, `PRAGMA`
248
+
249
+ ---
250
+
251
+ ## Database Schema
252
+
253
+ ### `enrollment`
254
+
255
+ | Column | Type | Description |
256
+ |---|---|---|
257
+ | `school_year` | VARCHAR | School year, format `'YYYY-YYYY'` |
258
+ | `school_name` | VARCHAR | One of 5 schools |
259
+ | `grade_level` | INTEGER | Grade level (K=0 through 12) |
260
+ | `student_count` | INTEGER | Students enrolled in that grade/year/school |
261
+
262
+ ### `attendance`
263
+
264
+ | Column | Type | Description |
265
+ |---|---|---|
266
+ | `student_id` | INTEGER | Unique student identifier |
267
+ | `school_name` | VARCHAR | School the student attends |
268
+ | `school_year` | VARCHAR | School year, format `'YYYY-YYYY'` |
269
+ | `absence_count` | INTEGER | Total absences for that year |
270
+ | `is_chronically_absent` | BOOLEAN | TRUE if missed ≥10% of school days |
271
+
272
+ ### Schools
273
+
274
+ | School | Grades | Base Enrollment |
275
+ |---|---|---|
276
+ | Lincoln Elementary | K–5 | 520 |
277
+ | Washington Middle | 6–8 | 480 |
278
+ | Jefferson High | 9–12 | 900 |
279
+ | Roosevelt Academy | K–8 | 380 |
280
+ | Kennedy Prep | 6–12 | 620 |
281
+
282
+ ### Seed Data Stats
283
+ - **Students:** 2,900
284
+ - **Chronic absenteeism rate:** 15% (435 students)
285
+ - **Enrollment rows:** 116 (5 schools × 4 years × variable grades)
286
+ - **Attendance rows:** 11,600 (2,900 students × 4 years)
287
+ - **School years:** 2021-2022, 2022-2023, 2023-2024, 2024-2025
288
+ - **YOY growth:** 3% per year with ±15% random grade-size variance
289
+ - **Reproducible:** `random.seed(42)` in `generate_seed.py`
290
+
291
+ ---
292
+
293
+ ## Prompt Engineering
294
+
295
+ `prompts.py` assembles the full LLM prompt:
296
+
297
+ ### Structure
298
+ ```
299
+ SYSTEM_PROMPT (rules, constraints, output format)
300
+
301
+ Schema documentation (table + column list)
302
+
303
+ Few-shot examples (4 question→SQL pairs)
304
+
305
+ User question
306
+
307
+ "SQL:"
308
+ ```
309
+
310
+ ### System Prompt Rules
311
+ `prompts.py:SYSTEM_PROMPT` [prompts.py:9-18]
312
+ 1. Only SELECT statements
313
+ 2. Exact table/column names from schema
314
+ 3. Proper DuckDB syntax (VARCHAR → single quotes, BOOLEAN → TRUE/FALSE)
315
+ 4. Use column aliases for aggregations
316
+ 5. Join on logical columns
317
+ 6. Make reasonable assumptions if ambiguous
318
+ 7. Output ONLY the ```sql``` block, no explanation
319
+
320
+ ### Few-Shot Examples
321
+ `prompts.py:FEW_SHOT_EXAMPLES` [prompts.py:56-76]
322
+
323
+ | Question | SQL pattern |
324
+ |---|---|
325
+ | "How many chronically absent in 2023-2024?" | `COUNT(*)` with `WHERE ... AND is_chronically_absent = TRUE` |
326
+ | "Show enrollment per school sorted" | `SUM(student_count) GROUP BY school_name ORDER BY ... DESC` |
327
+ | "What percentage at Lincoln Elementary?" | `COUNT(CASE WHEN ...) * 100.0 / COUNT(*)` |
328
+ | "Enrollment trend since 2021" | `GROUP BY school_year ORDER BY school_year` |
329
+
330
+ ---
331
+
332
+ ## Fine-Tuning Pipeline (Modal)
333
+
334
+ ### Location: `modal_train/`
335
+
336
+ | Script | Purpose |
337
+ |---|---|
338
+ | `generate_synthetic.py` | Creates 1,289 NL→SQL pairs from 32 query templates |
339
+ | `train.py` | Unsloth QLoRA on Qwen2.5-Coder-7B (r=16, 4-bit, 3 epochs, A10G) |
340
+ | `export_gguf.py` | Merges LoRA → converts to GGUF Q4_K_M → pushes to HF Hub |
341
+ | `modal_app.py` | Modal orchestration (`modal.App("kasualdad-lfed-train")`) |
342
+ | `train.jsonl` | Training data (1,289 pairs) |
343
+
344
+ ### How to Run
345
+ ```bash
346
+ modal secret create huggingface HF_TOKEN=<token>
347
+ modal run modal_train/modal_app.py
348
+ ```
349
+
350
+ ### Post-Training
351
+ After the fine-tuned model is pushed to HF Hub, swap `HF_REPO_ID` and `HF_MODEL_FILE` in `model_inference.py:103-107`. The exact repo ID is documented in `docs/HANDOFF.md`.
352
+
353
+ **Status:** ⏳ Not yet run (needs HF_TOKEN Modal secret)
354
+
355
+ ---
356
+
357
+ ## Dependencies
358
+
359
+ ### Python (`requirements.txt`)
360
+
361
+ | Package | Version | Why |
362
+ |---|---|---|
363
+ | `gradio` | 6.16.0 | Web UI |
364
+ | `duckdb` | 1.5.3 | In-memory SQL engine |
365
+ | `llama-cpp-python` | 0.3.26 | Local LLM inference (CUDA 12.1 wheel) |
366
+ | `huggingface_hub` | 1.18.0 | Model download, HF Hub API |
367
+ | `spaces` | latest | `@spaces.GPU` decorator for Zero GPU |
368
+ | `nvidia-cuda-runtime-cu12` | 12.1.105 | `libcudart.so.12` |
369
+ | `nvidia-cublas-cu12` | 12.1.3.1 | `libcublas.so.12`, `libcublasLt.so.12` |
370
+ | `nvidia-cusparse-cu12` | 12.1.0.106 | `libcusparse.so.12` |
371
+
372
+ **Wheel index:** `--extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cu121` — required to get the pre-built CUDA 12.1 wheel (avoids 15-minute source compile).
373
+
374
+ ---
375
+
376
+ ## Hackathon Badges
377
+
378
+ | Badge | Status | Implementation |
379
+ |---|---|---|
380
+ | **Off the Grid** | ✅ | llama.cpp + local GGUF, no API calls |
381
+ | **Well-Tuned** | ✅ | Fine-tuning pipeline ready (Modal, 1,289 pairs) |
382
+ | **Llama Champion** | ✅ | llama.cpp sole inference backend, Q4_K_M, streaming |
383
+ | **Off-Brand** | ✅ | Custom design system (Atkinson Hyperlegible + Cormorant Garamond, slate + indigo, WCAG AA) |
384
+
385
+ ---
386
+
387
+ ## Gotchas & Pitfalls
388
+
389
+ ### 1. `LD_LIBRARY_PATH` doesn't work at Python runtime
390
+ The dynamic linker caches it at process start. Use `ctypes.CDLL(path, mode=RTLD_GLOBAL)` to preload libraries instead.
391
+
392
+ ### 2. Zero GPU has CUDA emulation at module level
393
+ Load models with GPU layers at startup (module level), not lazily inside `@spaces.GPU` functions. The docs explicitly say lazy loading is "significantly less efficient."
394
+
395
+ ### 3. Gradio dark theme bleeds through CSS
396
+ Gradio 6 uses a dark theme by default. You need a "nuke block" (`background: transparent !important`) at the top of your CSS before applying light-theme colors, and you need to target nested wrapper divs (`> div > div`) because Gradio wraps everything.
397
+
398
+ ### 4. HF Spaces auto-link models detected in repo text
399
+ Any `username/repo-name` string in README, code, or docs gets linked on the Space page. Keep commented-out future model references out of the repo, or use a non-detectable format (e.g., point to HANDOFF.md instead of spelling out the repo ID).
400
+
401
+ ### 5. The fine-tuned model doesn't exist yet
402
+ `kasualdad/lfed-qwen2.5-coder-7b-sql-gguf` is a placeholder. All references to it were scrubbed from the repo to prevent double-linking on the HF Space. The exact repo ID lives only in `docs/HANDOFF.md` for when training completes.
403
+
404
+ ### 6. Parquet files are gitignored but force-committed
405
+ The `.gitignore` has `*.parquet`, but the seed Parquet files (33 KB total) were force-added (`git add -f`) because they're needed for local dev and Space builds. Update `export_parquet.py` and re-export if the seed data changes.
406
+
407
+ ### 7. macOS vs Linux CUDA divergence
408
+ Locally (macOS), `llama-cpp-python` uses Metal or CPU — the CUDA preload code is harmless (it searches for `.so` files that don't exist and skips silently). On the HF Space (Linux), it preloads 39 CUDA libraries.
409
+
410
+ ---
411
+
412
+ ## Quick Start
413
+
414
+ ```bash
415
+ # Clone
416
+ cd Kasualdad_LFED
417
+
418
+ # Virtual env
419
+ python3.12 -m venv .venv && source .venv/bin/activate
420
+
421
+ # Install
422
+ pip install -r requirements.txt
423
+
424
+ # Generate Parquet seed files (first time only)
425
+ python data/export_parquet.py
426
+
427
+ # Run
428
+ python app.py
429
+ # → http://localhost:7860
430
+
431
+ # Tests
432
+ pytest tests/ -v
433
+
434
+ # Deploy
435
+ git push space main
436
+ ```
437
+
438
+ ---
439
+
440
+ ## Git History (this session)
441
+
442
+ ```
443
+ 0e9c140 perf: load seed data from Parquet instead of generating on every query
444
+ 9bae1e9 fix: remove remaining fine-tuned model repo ID from HANDOFF.md
445
+ 14ee1d7 fix: remove nonexistent fine-tuned model reference to prevent double-linking on HF Space
446
+ 9618e4b design: hybrid cool palette (slate+indigo), aggressive overrides for Gradio dark-theme boxes
447
+ 47f650c fix: restore GPU offload on Zero GPU (emulation mode handles module-level CUDA)
448
+ 827143b design: apply WellFull Collective palette + Cormorant Garamond + Atkinson Hyperlegible
449
+ 7a51b3e fix: CPU-only on Zero GPU (model persists in RAM), 8 threads, restore eager load
450
+ 9ba7b17 fix: add @spaces.GPU decorator, defer model load to first query (Zero GPU)
451
+ a963ee6 fix: preload ALL CUDA .so files, add nvidia-cublas-cu12 + nvidia-cusparse-cu12
452
+ adf4793 fix: preload libcudart.so.12 via ctypes (LD_LIBRARY_PATH has no effect at runtime)
453
+ a6f6cbd fix: resolve CUDA runtime from pip package, add GPU→CPU fallback
454
+ ```
455
+
456
+ ---
457
+
458
+ ## Future Work
459
+
460
+ - [ ] **Run Modal fine-tuning** — needs `modal secret create huggingface HF_TOKEN=<token>`, then `modal run modal_train/modal_app.py`
461
+ - [ ] **Swap to fine-tuned model** — after GGUF is pushed to HF Hub, update `HF_REPO_ID`/`HF_MODEL_FILE` in `model_inference.py:103-107`
462
+ - [ ] **Add streaming back** — `generate_sql_streaming()` exists but is unused (switched to synchronous for HF Space reliability). Could re-enable with proper Zero GPU timeout handling.
463
+ - [ ] **HF Space smoke test** — verify all 6 example queries return correct results after each deploy
464
+ - [ ] **PRO subscription** — if 5 min/day GPU quota is too tight, upgrade to PRO (40 min/day)