| """Modal-deployed SFT training for ATC parser. |
| |
| Run: modal run poc/llm-finetune/training/train_modal.py |
| |
| Pulls dataset from HF Hub (kinglyai/atc-parser-spike-v0), trains Qwen3-32B |
| with LoRA r=32, pushes adapter back to HF Hub (kinglyai/qwen3-32b-atc-parser-v1). |
| |
| Requires: |
| - Modal account + token (modal token new) |
| - HF_TOKEN secret created via: modal secret create huggingface HF_TOKEN=<token> |
| |
| Cost: ~$3-7 on A100-80GB for 1500 iters. |
| """ |
| import modal |
|
|
| GPU = "A100-80GB" |
| TIMEOUT_HR = 3 |
| MEMORY_GB = 80 |
|
|
| image = ( |
| modal.Image.debian_slim(python_version="3.11") |
| .pip_install( |
| "torch>=2.4.0", |
| "transformers>=4.45.0", |
| "trl>=0.12.0", |
| "peft>=0.13.0", |
| "accelerate>=0.34.0", |
| "datasets>=3.0.0", |
| "bitsandbytes", |
| "trackio", |
| "huggingface_hub>=0.25.0", |
| "sentencepiece", |
| "protobuf", |
| ) |
| ) |
|
|
| app = modal.App("atc-parser-sft", image=image) |
|
|
| HF_DATASET = "kinglyai/atc-parser-canonical-v0" |
| HF_DATASET_LARGE = "kinglyai/atc-parser-spike-v0" |
| HF_OUTPUT = "kinglyai/qwen3-32b-atc-parser-v1" |
| BASE_MODEL = "Qwen/Qwen3-32B-Instruct-2507" |
| RUN_NAME = "qwen3-32b-canonical-v1" |
| TRACKIO_PROJECT = "atc-parser" |
|
|
|
|
| @app.function( |
| gpu=GPU, |
| timeout=TIMEOUT_HR * 3600, |
| secrets=[modal.Secret.from_name("huggingface")], |
| memory=MEMORY_GB * 1024, |
| ) |
| def train(): |
| import os |
| import torch |
| from datasets import load_dataset |
| from peft import LoraConfig |
| from transformers import AutoModelForCausalLM, AutoTokenizer |
| from trl import SFTConfig, SFTTrainer |
| import trackio |
|
|
| print(f"Modal container starting 路 GPU: {GPU} 路 base: {BASE_MODEL}") |
|
|
| trackio.init(project=TRACKIO_PROJECT, name=RUN_NAME) |
|
|
| print(f"loading tokenizer + model from {BASE_MODEL}") |
| tok = AutoTokenizer.from_pretrained(BASE_MODEL, trust_remote_code=True) |
| model = AutoModelForCausalLM.from_pretrained( |
| BASE_MODEL, |
| torch_dtype=torch.bfloat16, |
| device_map="auto", |
| trust_remote_code=True, |
| ) |
|
|
| print(f"loading dataset from {HF_DATASET}") |
| ds = load_dataset(HF_DATASET, data_files={ |
| "train": "train.jsonl", |
| "valid": "valid.jsonl", |
| "test": "test.jsonl", |
| }) |
|
|
| lora = LoraConfig( |
| r=32, |
| lora_alpha=64, |
| target_modules=["q_proj", "k_proj", "v_proj", "o_proj", |
| "gate_proj", "up_proj", "down_proj"], |
| bias="none", |
| lora_dropout=0.05, |
| task_type="CAUSAL_LM", |
| ) |
|
|
| cfg = SFTConfig( |
| output_dir="/tmp/sft_out", |
| num_train_epochs=10, |
| per_device_train_batch_size=4, |
| per_device_eval_batch_size=4, |
| gradient_accumulation_steps=4, |
| learning_rate=2e-4, |
| lr_scheduler_type="cosine", |
| warmup_ratio=0.05, |
| bf16=True, |
| eval_strategy="steps", |
| eval_steps=200, |
| save_strategy="steps", |
| save_steps=400, |
| save_total_limit=3, |
| logging_steps=20, |
| report_to="trackio", |
| run_name=RUN_NAME, |
| push_to_hub=True, |
| hub_model_id=HF_OUTPUT, |
| hub_strategy="every_save", |
| hub_private_repo=True, |
| save_safetensors=True, |
| max_length=1024, |
| gradient_checkpointing=True, |
| ) |
|
|
| print(f"starting training 路 {len(ds['train'])} train rows 路 {len(ds['valid'])} valid") |
| trainer = SFTTrainer( |
| model=model, |
| tokenizer=tok, |
| train_dataset=ds["train"], |
| eval_dataset=ds["valid"], |
| peft_config=lora, |
| args=cfg, |
| ) |
|
|
| trainer.train() |
| trainer.save_model() |
| trainer.push_to_hub() |
|
|
| print(f"DONE 路 adapter pushed to {HF_OUTPUT}") |
| trackio.finish() |
|
|
|
|
| @app.local_entrypoint() |
| def main(): |
| """Launch from local: `modal run poc/llm-finetune/training/train_modal.py`""" |
| print(f"submitting Modal job 路 {GPU} 路 {TIMEOUT_HR}h timeout 路 base={BASE_MODEL}") |
| train.remote() |
| print("Modal job complete. Check HF Hub for adapter:") |
| print(f" https://huggingface.co/{HF_OUTPUT}") |
|
|