LFED / modal_train /modal_app.py
Kasualdad's picture
feat: swap to fine-tuned Q4_K_M model (4.68 GB)
e0c4e2f
Raw
History Blame Contribute Delete
6.12 kB
"""
modal_app.py — Modal orchestration for LFED fine-tuning pipeline.
Deploy: modal deploy modal_train/modal_app.py
Run: modal run modal_train/modal_app.py
Pipeline:
1. Generate synthetic NL→SQL pairs (CPU)
2. QLoRA fine-tune on A10G (~3-6 hrs)
3. Merge LoRA → GGUF Q4_K_M
4. Push to Hugging Face Hub
Requires Modal secret 'huggingface' with HF_TOKEN.
"""
from pathlib import Path
import modal
# ── Modal app ──────────────────────────────────────────────────────────
app = modal.App("kasualdad-lfed-train") # force redeploy v3
# ── Image with all dependencies ────────────────────────────────────────
train_image = (
modal.Image.debian_slim(python_version="3.11")
.apt_install("build-essential", "cmake", "git")
.pip_install(
"unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git",
"torch>=2.4.0",
"transformers>=4.46.0",
"trl>=0.12.0",
"datasets>=3.0.0",
"bitsandbytes>=0.44.0",
"accelerate>=1.0.0",
"peft>=0.13.0",
"huggingface_hub>=0.26.0",
"llama-cpp-python>=0.3.0",
"gguf>=0.10.0",
)
.env({"HF_HUB_ENABLE_HF_TRANSFER": "1"})
.add_local_dir(
Path(__file__).parent,
remote_path="/root",
)
)
# ── Volume for persistent storage (training data + checkpoints) ────────
volume = modal.Volume.from_name(
"lfed-training-data", create_if_missing=True
)
MOUNT_DIR = Path("/data")
# ── Step 1: Generate synthetic data (CPU) ──────────────────────────────
@app.function(
image=train_image,
volumes={MOUNT_DIR: volume},
timeout=10 * 60, # 10 min
)
def generate_synthetic():
"""Generate NL→SQL training pairs."""
import sys
sys.path.insert(0, str(MOUNT_DIR))
# Copy generate_synthetic.py to volume and run
local_script = Path(__file__).parent / "generate_synthetic.py"
dest = MOUNT_DIR / "generate_synthetic.py"
dest.write_text(local_script.read_text())
# Run generation
from generate_synthetic import main
main()
# Copy output back
import shutil
train_jsonl = Path(__file__).parent / "train.jsonl"
if train_jsonl.exists():
shutil.move(str(train_jsonl), str(MOUNT_DIR / "train.jsonl"))
print(f"✅ Synthetic data ready at {MOUNT_DIR / 'train.jsonl'}")
volume.commit()
# ── Step 2: QLoRA fine-tuning (A10G) ───────────────────────────────────
@app.function(
image=train_image,
gpu="A10G",
volumes={MOUNT_DIR: volume},
timeout=6 * 3600, # 6 hours
secrets=[modal.Secret.from_name("huggingface")],
)
def train_model():
"""Fine-tune Qwen2.5-Coder-7B with QLoRA on A10G."""
import sys
sys.path.insert(0, str(MOUNT_DIR))
# Copy train.py to volume and run
train_script = Path(__file__).parent / "train.py"
dest = MOUNT_DIR / "train.py"
dest.write_text(train_script.read_text())
# Ensure train.jsonl is available
train_jsonl = MOUNT_DIR / "train.jsonl"
if not train_jsonl.exists():
# Try to copy from local
local_jsonl = Path(__file__).parent / "train.jsonl"
if local_jsonl.exists():
train_jsonl.write_text(local_jsonl.read_text())
else:
raise FileNotFoundError(
"train.jsonl not found. Run generate_synthetic first."
)
from train import main
main()
# Free GPU memory for the export step
import gc
import torch
del sys.modules['train']
gc.collect()
torch.cuda.empty_cache()
volume.commit()
print("✅ Training complete")
# ── Step 3: Export GGUF + push to HF (A10G for merge speed) ────────────
@app.function(
image=train_image,
gpu="A10G",
volumes={MOUNT_DIR: volume},
timeout=2 * 3600, # 2 hours
secrets=[modal.Secret.from_name("huggingface")],
)
def export_and_push():
"""Merge LoRA, export GGUF, push to HF Hub."""
import sys
import importlib
# Import directly from mount path (not volume copy)
mount_path = str(Path(__file__).parent)
if mount_path not in sys.path:
sys.path.insert(0, mount_path)
# Clear any cached module
for key in list(sys.modules.keys()):
if "export_gguf" in key:
del sys.modules[key]
import export_gguf
importlib.reload(export_gguf)
export_gguf.main()
# ── Full pipeline (run all steps sequentially) ─────────────────────────
@app.function(
image=train_image,
gpu="A10G",
volumes={MOUNT_DIR: volume},
timeout=8 * 3600, # 8 hours total
secrets=[modal.Secret.from_name("huggingface")],
)
def run_full_pipeline():
"""Run the complete pipeline: synthetic → train → export → push."""
print("=" * 60)
print("LFED Full Fine-Tuning Pipeline")
print("=" * 60)
print("\n📝 Step 1/3: Generating synthetic training data...")
generate_synthetic.local()
print("\n🏋️ Step 2/3: QLoRA fine-tuning on A10G...")
train_model.local()
print("\n📦 Step 3/3: Merging → GGUF → HF Hub...")
export_and_push.remote() # remote() = fresh container, clean GPU
print("\n🎉 Full pipeline complete!")
print(f" Model repo: check your HF Hub for 'lfed-qwen2.5-coder-7b-sql-gguf'")
# ── Entry points ───────────────────────────────────────────────────────
@app.local_entrypoint()
def main():
"""
Local entry point — kick off the full pipeline on Modal.
Usage:
modal run modal_train/modal_app.py
"""
print("Deploying LFED fine-tuning pipeline to Modal...")
run_full_pipeline.remote()