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
- 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:
|
| 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:**
|
| 99 |
-
- **Result:** Training
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
---
|
| 102 |
|
| 103 |
-
## 3. What Went Wrong
|
| 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 |
-
###
|
| 112 |
-
- **Symptom:**
|
| 113 |
-
- **
|
| 114 |
-
- **Fix:**
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
- **
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
-
###
|
| 127 |
-
- **Symptom:**
|
| 128 |
-
- **
|
| 129 |
-
- **
|
| 130 |
-
- **
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
|
| 132 |
-
###
|
| 133 |
-
- **Symptom:**
|
| 134 |
-
- **
|
| 135 |
-
- **
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
#
|
| 151 |
-
|
| 152 |
-
#
|
| 153 |
-
|
| 154 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
pytest tests/ -v
|
| 156 |
|
| 157 |
-
#
|
| 158 |
-
git push space main
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
```
|
| 160 |
|
| 161 |
---
|
| 162 |
|
| 163 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|