| """ |
| SFT training for multilingual_model — Helena |
| |
| Phase 1: fine-tune Qwen3-1.7B on multilingual MC data (belebele + xcopa) |
| to teach the model to output \boxed{letter} answers. |
| |
| Same LoRA setup as trainingPhuc/train_sft.py — keeping it consistent |
| across the team so we can compare results. |
| |
| Usage: |
| python -m trainingHelena.train_sft \ |
| --data-dir /scratch/multilingual_data \ |
| --output-dir /scratch/multilingual_model_sft |
| """ |
|
|
| import argparse |
| from pathlib import Path |
|
|
| import torch |
| from datasets import load_dataset |
| from peft import LoraConfig |
| from transformers import AutoModelForCausalLM, AutoTokenizer |
| from trl import SFTConfig, SFTTrainer |
|
|
|
|
| |
| LORA_TARGETS = ["q_proj", "k_proj", "v_proj", "o_proj", |
| "gate_proj", "up_proj", "down_proj"] |
|
|
|
|
| def get_args(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument("--base-model", default="/shared-ro/models/Qwen/Qwen3-1.7B") |
| parser.add_argument("--data-dir", default="/scratch/multilingual_data") |
| parser.add_argument("--output-dir", default="/scratch/multilingual_model_sft") |
| parser.add_argument("--epochs", type=int, default=3) |
| parser.add_argument("--batch-size", type=int, default=2) |
| parser.add_argument("--grad-accum", type=int, default=8) |
| parser.add_argument("--lr", type=float, default=2e-5) |
| parser.add_argument("--lora-r", type=int, default=64) |
| parser.add_argument("--max-seq-len", type=int, default=1024) |
| parser.add_argument("--report-to", default="none") |
| parser.add_argument("--run-name", default="multilingual-sft") |
| parser.add_argument("--resume", default=None, |
| help="path to checkpoint to resume from") |
| return parser.parse_args() |
|
|
|
|
| def main(): |
| args = get_args() |
| output_dir = Path(args.output_dir) |
| merged_dir = output_dir / "merged" |
|
|
| |
| tokenizer = AutoTokenizer.from_pretrained(args.base_model) |
| if tokenizer.pad_token is None: |
| tokenizer.pad_token = tokenizer.eos_token |
|
|
| |
| print(f"Loading base model from {args.base_model}...") |
| model = AutoModelForCausalLM.from_pretrained( |
| args.base_model, |
| torch_dtype=torch.bfloat16, |
| device_map="auto", |
| ) |
|
|
| |
| data_dir = Path(args.data_dir) |
| ds = load_dataset("json", data_files={ |
| "train": str(data_dir / "sft_train.jsonl"), |
| "validation": str(data_dir / "sft_val.jsonl"), |
| }) |
| print(f"Train: {len(ds['train'])} examples, Val: {len(ds['validation'])} examples") |
|
|
| |
| lora_config = LoraConfig( |
| r=args.lora_r, |
| lora_alpha=args.lora_r * 2, |
| target_modules=LORA_TARGETS, |
| lora_dropout=0.05, |
| bias="none", |
| task_type="CAUSAL_LM", |
| ) |
|
|
| |
| |
| trainer = SFTTrainer( |
| model=model, |
| processing_class=tokenizer, |
| train_dataset=ds["train"], |
| eval_dataset=ds["validation"], |
| peft_config=lora_config, |
| args=SFTConfig( |
| output_dir=str(output_dir), |
| num_train_epochs=args.epochs, |
| per_device_train_batch_size=args.batch_size, |
| gradient_accumulation_steps=args.grad_accum, |
| learning_rate=args.lr, |
| warmup_ratio=0.05, |
| lr_scheduler_type="cosine", |
| bf16=True, |
| max_length=args.max_seq_len, |
| dataset_text_field="text", |
| logging_steps=20, |
| eval_strategy="steps", |
| eval_steps=100, |
| save_strategy="steps", |
| save_steps=100, |
| save_total_limit=2, |
| load_best_model_at_end=True, |
| report_to=args.report_to, |
| run_name=args.run_name, |
| ), |
| ) |
|
|
| print("Starting SFT training...") |
| trainer.train(resume_from_checkpoint=args.resume) |
|
|
| |
| print(f"Merging LoRA weights → {merged_dir}") |
| merged_dir.mkdir(parents=True, exist_ok=True) |
| merged = trainer.model.merge_and_unload() |
| merged.save_pretrained(str(merged_dir)) |
| tokenizer.save_pretrained(str(merged_dir)) |
| print(f"Merged checkpoint saved: {merged_dir}") |
| print(f"\nNext: python -m trainingHelena.push_to_hub --checkpoint {merged_dir} --repo cs-552-2026-eminem-p/multilingual_model") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|