| """ |
| 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 |
|
|
| |
|
|
| app = modal.App("kasualdad-lfed-train") |
|
|
| |
|
|
| 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 = modal.Volume.from_name( |
| "lfed-training-data", create_if_missing=True |
| ) |
| MOUNT_DIR = Path("/data") |
|
|
|
|
| |
|
|
| @app.function( |
| image=train_image, |
| volumes={MOUNT_DIR: volume}, |
| timeout=10 * 60, |
| ) |
| def generate_synthetic(): |
| """Generate NL→SQL training pairs.""" |
| import sys |
| sys.path.insert(0, str(MOUNT_DIR)) |
|
|
| |
| local_script = Path(__file__).parent / "generate_synthetic.py" |
| dest = MOUNT_DIR / "generate_synthetic.py" |
| dest.write_text(local_script.read_text()) |
|
|
| |
| from generate_synthetic import main |
| main() |
|
|
| |
| 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() |
|
|
|
|
| |
|
|
| @app.function( |
| image=train_image, |
| gpu="A10G", |
| volumes={MOUNT_DIR: volume}, |
| timeout=6 * 3600, |
| 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)) |
|
|
| |
| train_script = Path(__file__).parent / "train.py" |
| dest = MOUNT_DIR / "train.py" |
| dest.write_text(train_script.read_text()) |
|
|
| |
| train_jsonl = MOUNT_DIR / "train.jsonl" |
| if not train_jsonl.exists(): |
| |
| 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() |
|
|
| |
| import gc |
| import torch |
| del sys.modules['train'] |
| gc.collect() |
| torch.cuda.empty_cache() |
|
|
| volume.commit() |
| print("✅ Training complete") |
|
|
|
|
| |
|
|
| @app.function( |
| image=train_image, |
| gpu="A10G", |
| volumes={MOUNT_DIR: volume}, |
| timeout=2 * 3600, |
| secrets=[modal.Secret.from_name("huggingface")], |
| ) |
| def export_and_push(): |
| """Merge LoRA, export GGUF, push to HF Hub.""" |
| import sys |
| import importlib |
|
|
| |
| mount_path = str(Path(__file__).parent) |
| if mount_path not in sys.path: |
| sys.path.insert(0, mount_path) |
|
|
| |
| 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() |
|
|
|
|
| |
|
|
| @app.function( |
| image=train_image, |
| gpu="A10G", |
| volumes={MOUNT_DIR: volume}, |
| timeout=8 * 3600, |
| 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() |
|
|
| print("\n🎉 Full pipeline complete!") |
| print(f" Model repo: check your HF Hub for 'lfed-qwen2.5-coder-7b-sql-gguf'") |
|
|
|
|
| |
|
|
| @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() |
|
|