Kasualdad commited on
Commit
c12f3be
·
1 Parent(s): d46eaa0

docs: comprehensive training playbook — 14 bugs documented with fixes

Browse files

- 14 errors catalogued with symptoms, root causes, and fixes
- Verified results section (3/3 queries pass)
- Post-training checklist with working commands
- Reproducible build sequence updated

Files changed (1) hide show
  1. docs/TRAINING_PLAYBOOK.md +197 -50
docs/TRAINING_PLAYBOOK.md CHANGED
@@ -93,74 +93,221 @@ modal app list
93
  - Training converged fast (loss 0.1008 by step 72/243)
94
  - QLoRA at 0.53% trainable params = cheap, fast, effective
95
 
96
- ### Key Fix: fn.spawn() Fire-and-Forget
97
- - **Problem:** 5 training runs crashed because `modal run` keeps a client connection open. When the local CLI times out or disconnects, Modal cancels the running function.
98
- - **Solution:** Switch to `modal deploy` + `fn.spawn()` which fires the function with no client connection to kill.
99
- - **Result:** Training survived past step 72 where all previous runs crashed.
 
 
 
 
 
 
 
 
100
 
101
  ---
102
 
103
- ## 3. What Went Wrong (and Fixes)
104
-
105
- ### Issue 1: 5 Consecutive Training Crashes
106
- - **Symptom:** Training started, ran ~20-30 steps, then silently stopped
107
- - **Root cause:** `modal run` maintains a gRPC connection to Modal. When the local terminal session exits (timeout, sleep, Ctrl+C), Modal cancels the remote function.
108
- - **Fix:** Use `modal deploy` to create a persistent app, then call `fn.spawn()` from a Modal function that has no client connection. The deployed app stays alive independently.
109
- - **Lesson:** NEVER use `modal run` for long-running GPU work. Always deploy + spawn.
110
 
111
- ### Issue 2: Flash Attention 2 Broken on Modal
112
- - **Symptom:** Warning "Your Flash Attention 2 installation seems to be broken. Using Xformers instead."
113
- - **Impact:** Minimal Xformers works fine, no perf change observed
114
- - **Fix:** Not needed for hackathon. For production, pin specific flash-attn + CUDA versions.
115
-
116
- ### Issue 3: Only 1,289 Pairs (vs 2,000 Target)
117
- - **Symptom:** Generator hit dedup ceiling before reaching 2,000
118
- - **Root cause:** 32 templates × limited parameter combinations (5 schools, 4 years) = ~1,300 unique questions max before repeats
119
- - **Fix needed:** Add more templates, more parameter variety, or augment with LLM-generated rephrasings (see Expansion Plan)
120
-
121
- ### Issue 4: Narrow Schema Coverage
122
- - **Symptom:** Training data only covers 2 tables (enrollment, attendance)
123
- - **Impact:** Model can only answer attendance/enrollment questions — can't handle grades, discipline, demographics, wellbeing
124
- - **Fix needed:** Expand schema + training pairs (see Expansion Plan)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
- ### Issue 5: Memory Issue During Merge/Export
127
- - **Symptom:** User reported memory issues on Modal after training reached 30%
128
- - **Status:** Being investigated during current training run
129
- - **Likely cause:** Merging LoRA back into 16-bit base model requires loading full model (14GB+), which may exceed A10G 24GB when combined with training checkpoint memory
130
- - **Potential fix:** Free GPU memory between train and merge steps (already implemented in train.py lines 201-206), or use separate A10G container for merge step
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
132
- ### Issue 6: Template-Based Questions Feel Synthetic
133
- - **Symptom:** Questions are grammatically correct but lack the messiness of real admin questions
134
- - **Impact:** Model may struggle with typos, abbreviations, informal phrasing
135
- - **Fix:** Add LLM-augmented rephrasings (see Expansion Plan)
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
  ---
138
 
139
- ## 4. Post-Training Checklist
140
-
141
- After training completes:
142
 
143
  ```bash
144
  # 1. Verify GGUF pushed to Hub
145
- # Check https://huggingface.co/kasualdad/lfed-qwen2.5-coder-7b-sql-gguf
146
-
147
- # 2. Update model_inference.py to use fine-tuned model
148
- # Change REPO_ID and MODEL_FILE (lines ~103-107)
149
-
150
- # 3. Test locally
151
- python app.py
152
- # Ask: "How many students were chronically absent in 2023-2024?"
153
-
154
- # 4. Run tests
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  pytest tests/ -v
156
 
157
- # 5. Deploy to HF Space
158
- git push space main
 
 
 
 
159
  ```
160
 
161
  ---
162
 
163
- ## 5. Reproducing from Scratch
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
 
165
  ```bash
166
  # 1. Clone
 
93
  - Training converged fast (loss 0.1008 by step 72/243)
94
  - QLoRA at 0.53% trainable params = cheap, fast, effective
95
 
96
+ ### Key Fix: Fire-and-Forget via deploy + spawn
97
+ - **Problem:** 5 training runs crashed because `modal run` keeps a client connection open. When the local CLI times out or disconnects, Modal cancels the running function. `--detach` didn't help because cancellation arrives before detach.
98
+ - **Solution:** `modal deploy` creates a persistent app with zero client connection. Call `fn.spawn()` from Python truly fire-and-forget.
99
+ - **Result:** Training completed all 3 epochs (243 steps), loss converged to 0.07.
100
+ - **EXACT COMMANDS USED:**
101
+ ```bash
102
+ modal deploy modal_train/modal_app.py
103
+ python3 -c "
104
+ import modal
105
+ fn = modal.Function.from_name('kasualdad-lfed-train', 'run_full_pipeline')
106
+ fn.spawn()
107
+ "
108
 
109
  ---
110
 
111
+ ## 3. What Went Wrong Complete Error Catalog
 
 
 
 
 
 
112
 
113
+ ### CRITICAL: Training Client Disconnects (⚠️ 5 runs crashed)
114
+ - **Symptom:** Training ran ~20-130 steps, then stopped with `Received a cancellation signal`
115
+ - **Root cause:** `modal run` keeps a gRPC connection. When local terminal exits (timeout, sleep, Ctrl+C), Modal cancels the remote function. `--detach` delay-implies detachment — the cancellation signal arrives before detach takes effect.
116
+ - **Fix:** `modal deploy` + call `fn.spawn()` from Python. Spawn fires with no client connection — the function runs to completion independently.
117
+ - **Commands:**
118
+ ```bash
119
+ modal deploy modal_train/modal_app.py
120
+ python3 -c "
121
+ import modal
122
+ fn = modal.Function.from_name('kasualdad-lfed-train', 'export_and_push')
123
+ fn.spawn()
124
+ "
125
+ ```
126
+ - **Lesson for future training runs:** NEVER use `modal run` for GPU work >60 seconds. ALWAYS deploy + spawn.
127
+
128
+ ### CRITICAL: Files Not Available in Container (2 crashes)
129
+ - **Symptom:** `FileNotFoundError: '/root/generate_synthetic.py'` — only `modal_app.py` was uploaded.
130
+ - **Root cause:** Modal only uploads the entry-point file. Sibling scripts must be explicitly mounted.
131
+ - **Fix:** Add `.add_local_dir()` to the Modal image definition:
132
+ ```python
133
+ train_image = modal.Image.debian_slim(...)
134
+ .add_local_dir(Path(__file__).parent, remote_path="/root")
135
+ ```
136
+ - **Failed attempts:** `modal.Mount` (deprecated, doesn't exist in v1.4.3), `condition=` kwarg (not supported on `add_local_dir`)
137
+
138
+ ### CRITICAL: Cross-Device Link Error
139
+ - **Symptom:** `OSError: [Errno 18] Invalid cross-device link: '/root/train.jsonl' -> '/data/train.jsonl'`
140
+ - **Root cause:** `Path.rename()` / `os.rename()` fails across mount points (root mount ≠ volume mount)
141
+ - **Fix:** Use `shutil.move()` instead of `.rename()`
142
+
143
+ ### CRITICAL: Pickle Error on Model Save (2 crashes)
144
+ - **Symptom:** `PicklingError: Can't pickle <class 'trl.trainer.sft_config.SFTConfig'>`
145
+ - **Root cause:** SFTTrainer calls `save_model()` which tries to pickle training args. SFTConfig from nested module can't be pickled.
146
+ - **Fix (3 layers):**
147
+ 1. `save_strategy="no"` — prevent auto-saves during training
148
+ 2. `try/except` around `trainer.train()` to catch pickle errors
149
+ 3. Manual save: `model.save_pretrained()` directly (bypasses trainer)
150
+
151
+ ### CRITICAL: CUDA Out of Memory During Merge/Export (3 crashes)
152
+ - **Symptom:** `torch.OutOfMemoryError: Tried to allocate 14.23 GiB` during `load_adapter()`
153
+ - **Root cause:** Training leaves model in GPU. Loading 16-bit base model for merge needs 14GB, exceeding A10G 22GB when combined.
154
+ - **Fix:**
155
+ 1. Free GPU after training: `del model; gc.collect(); torch.cuda.empty_cache()`
156
+ 2. Run `export_and_push` via `.remote()` not `.local()` — fresh container with clean GPU
157
+ - **Failed attempts:** `load_in_4bit=True` + `merge_and_unload()` (NotImplementedError on quantized models)
158
+
159
+ ### CRITICAL: merge_and_unload() Not Implemented
160
+ - **Symptom:** `NotImplementedError` when calling `merge_and_unload()` on quantized PEFT model
161
+ - **Fix:** Load base model in FP16 via `AutoModelForCausalLM`, then apply adapter via `PeftModel.from_pretrained(base_model, adapter_path)`, THEN merge. The key insight: load base in FP16 first, apply adapter separately.
162
+ ```python
163
+ base_model = AutoModelForCausalLM.from_pretrained(BASE_MODEL, torch_dtype=torch.float16)
164
+ model = PeftModel.from_pretrained(base_model, str(LORA_DIR))
165
+ model = model.merge_and_unload()
166
+ ```
167
 
168
+ ### GGUF Conversion: Missing Module (3 crashes)
169
+ - **Symptom:** `No module named 'llama_cpp.convert'`
170
+ - **Root cause:** llama-cpp-python doesn't have a built-in converter
171
+ - **Fix:** Clone llama.cpp repo + install `gguf` package. Use `convert_hf_to_gguf.py` from the repo.
172
+ - **Failed attempts:** Downloading single converter file (needs companion modules), `pip install gguf` in image (not picked up by cached image)
173
+
174
+ ### Quantization: llama-quantize Missing (2 attempts)
175
+ - **Symptom:** `llama-quantize not found, using FP16 GGUF` → 15.2 GB model
176
+ - **Root cause:** llama.cpp quantize binary not built
177
+ - **Fix:** `llama_quantize()` from llama-cpp-python (v0.3.26+ has it built-in)
178
+ ```python
179
+ from llama_cpp import llama_quantize
180
+ llama_quantize(input_path=f16_gguf, output_path=q4_gguf, output_type="q4_k_m")
181
+ ```
182
+ - **Failed attempts:** cmake build (dependency issues, `-j$(nproc)` shell expansion fails with subprocess)
183
+
184
+ ### HF Repo Push: Namespace Permission Error
185
+ - **Symptom:** `403 Forbidden: You don't have the rights to create a model under the namespace "kasualdad"`
186
+ - **Root cause:** HF API is case-sensitive. `kasualdad` ≠ `Kasualdad`. The token belongs to user `Kasualdad`.
187
+ - **Fix:** Use correct case: `HF_USERNAME = "Kasualdad"`
188
+
189
+ ### Fine-Tuned Model Outputs Nothing (0 chars)
190
+ - **Symptom:** Model loads but generates 0 tokens for every query
191
+ - **Root cause:** Training used Qwen2.5 chat template (`<|im_start|>system...<|im_end|>`), but inference sent plain text. Model doesn't recognize the format.
192
+ - **Fix:** Update `build_prompt()` in `prompts.py` to use Qwen2.5 chat template:
193
+ ```python
194
+ prompt = (
195
+ f"<|im_start|>system\n{system_prompt}<|im_end|>\n"
196
+ f"<|im_start|>user\nQuestion: {question}<|im_end|>\n"
197
+ f"<|im_start|>assistant\n"
198
+ )
199
+ ```
200
+ - **Also:** Add `<|im_end|>` and `<|im_start|>` to stop sequences in `model_inference.py`
201
+
202
+ ### CUDA Wheel on CPU Spaces
203
+ - **Symptom:** `libcuda.so.1: cannot open shared object file` on CPU Space
204
+ - **Root cause:** `requirements.txt` had `--extra-index-url cu121` which installs CUDA-linked llama-cpp-python
205
+ - **Fix:** Remove the CUDA wheel index. Standard PyPI wheel works on CPU + GPU.
206
+
207
+ ### Local Dev: Missing spaces Module
208
+ - **Symptom:** `ModuleNotFoundError: No module named 'spaces'` when running locally
209
+ - **Root cause:** `spaces` is an HF infrastructure-only package
210
+ - **Fix:** Try/except import with no-op fallback:
211
+ ```python
212
+ try:
213
+ import spaces
214
+ _gpu_decorator = spaces.GPU
215
+ except ImportError:
216
+ _gpu_decorator = lambda fn: fn
217
+ ```
218
 
219
+ ### HF Space: Zero GPU Daily Limit
220
+ - **Symptom:** "You've hit your daily Zero GPU limit"
221
+ - **Root cause:** Free tier has limited daily GPU quota
222
+ - **Workaround:** Switch Space hardware to CPU (model still works, just slower)
223
+ - **Alternative:** $9/month PRO account for 8x quota
224
+
225
+ ### Volume Cache Serves Stale Files
226
+ - **Symptom:** Code changes don't take effect, old errors repeat
227
+ - **Root cause:** Modal volumes persist `.pyc` files from old runs. `import` picks up cached bytecode.
228
+ - **Fix:** Clear module cache before importing:
229
+ ```python
230
+ for key in list(sys.modules.keys()):
231
+ if "script_name" in key:
232
+ del sys.modules[key]
233
+ importlib.reload(module)
234
+ ```
235
 
236
  ---
237
 
238
+ ## 4. Post-Training Checklist (VERIFIED WORKING)
 
 
239
 
240
  ```bash
241
  # 1. Verify GGUF pushed to Hub
242
+ # Check https://huggingface.co/build-small-hackathon/lfed-qwen2.5-coder-7b-sql-gguf
243
+ from huggingface_hub import HfApi
244
+ api = HfApi(token='hf_...')
245
+ for f in api.list_repo_tree('build-small-hackathon/lfed-qwen2.5-coder-7b-sql-gguf'):
246
+ print(f' {f.path} ({f.size/1e9:.2f} GB)')
247
+ # Expected: lfed-qwen2.5-coder-7b-sql-Q4_K_M.gguf (4.68 GB)
248
+
249
+ # 2. Update model_inference.py
250
+ # Set lines 101-102:
251
+ # HF_REPO_ID = "build-small-hackathon/lfed-qwen2.5-coder-7b-sql-gguf"
252
+ # HF_MODEL_FILE = "lfed-qwen2.5-coder-7b-sql-Q4_K_M.gguf"
253
+
254
+ # 3. Fix prompts.py: Qwen2.5 chat template (see Issue "Outputs Nothing")
255
+ # MUST use <|im_start|>system/user/assistant<|im_end|> format
256
+
257
+ # 4. Fix model_inference.py stop sequences:
258
+ # STOP_SEQUENCES = ["\n\n", "Question:", "User:", "<|im_end|>", "<|im_start|>"]
259
+
260
+ # 5. Fix app.py: spaces.GPU made optional for local dev
261
+ # try/except ImportError with lambda:fn fallback
262
+
263
+ # 6. Test locally
264
+ cd Kasualdad_LFED && source .venv/bin/activate
265
+ python -c "
266
+ from model_inference import load_model, generate_sql
267
+ from data_engine import create_session, execute_safe
268
+ llm = load_model() # downloads Q4_K_M from Hub (~3s on Mac)
269
+ raw, _ = generate_sql('How many students were chronically absent in 2023-2024?', llm=llm)
270
+ conn = create_session()
271
+ sql, df = execute_safe(conn, raw)
272
+ print(df) # Should show: chronic_count = 435
273
+ "
274
+
275
+ # 7. Run tests
276
  pytest tests/ -v
277
 
278
+ # 8. Commit + push to Space
279
+ git add -A && git commit -m "feat: fine-tuned Q4_K_M model" && git push space main
280
+
281
+ # 9. Verify Space
282
+ # Open https://huggingface.co/spaces/build-small-hackathon/Kasualdad_LFED
283
+ # Click first example query — should return 435
284
  ```
285
 
286
  ---
287
 
288
+ ## 6. Verified Results (2026-06-08)
289
+
290
+ ### Training
291
+ | Metric | Value |
292
+ |---|---|
293
+ | Model | Qwen2.5-Coder-7B-Instruct → QLoRA fine-tuned |
294
+ | Training data | 1,289 NL→SQL pairs, 32 templates |
295
+ | Epochs | 3 (243 steps) |
296
+ | Final loss | 0.07 (converged from 2.6) |
297
+ | Training time | ~7 minutes on A10G |
298
+ | GGUF output | Q4_K_M, 4.68 GB |
299
+
300
+ ### Inference (3/3 test queries pass)
301
+ | Query | SQL | Result |
302
+ |---|---|---|
303
+ | "How many students were chronically absent in 2023-2024?" | `SELECT COUNT(*) ... WHERE is_chronically_absent = TRUE` | **435** ✅ |
304
+ | "Show total enrollment per school for 2024-2025, sorted highest first." | `SELECT school_name, SUM(student_count) ... ORDER BY total_enrollment DESC` | Correct ranking ✅ |
305
+ | "What percentage of students at Lincoln Elementary were chronically absent?" | `ROUND(COUNT(CASE WHEN ...) * 100.0 / COUNT(*), 1)` | **13.7%** ✅ |
306
+
307
+ ### Deployed URLs
308
+ - **HF Space:** https://huggingface.co/spaces/build-small-hackathon/Kasualdad_LFED
309
+ - **Model repo:** https://huggingface.co/build-small-hackathon/lfed-qwen2.5-coder-7b-sql-gguf
310
+ - **Modal app:** https://modal.com/apps/flucido/main/deployed/kasualdad-lfed-train
311
 
312
  ```bash
313
  # 1. Clone