""" Modal fine-tuning script for Retro Alpha. Fine-tunes unsloth/NVIDIA-Nemotron-3-Nano-4B with 16-bit LoRA on the Retro Alpha dataset. Usage: modal run -m training.modal_train Based on Unsloth's Nemotron 3 fine-tuning guide: https://docs.unsloth.ai/models/nemotron-3 """ import os import modal image = ( modal.Image.from_registry( "nvidia/cuda:12.6.0-devel-ubuntu22.04", add_python="3.11", ) .apt_install("git", "build-essential", "curl", "libcurl4-openssl-dev") .pip_install("uv", "huggingface_hub", "hf_transfer") .run_commands( # Pin versions matching Unsloth's Nemotron 3 notebook. "uv pip install --system --no-cache " " torch==2.7.1 triton>=3.3.0 " " transformers==4.56.2 " " datasets " " trl " " peft " " accelerate " " bitsandbytes " " unsloth_zoo " " 'unsloth @ git+https://github.com/unslothai/unsloth'", # Mamba / causal-conv1d are required by Nemotron-H architecture. "uv pip install --system --no-cache --no-build-isolation " " mamba_ssm==2.2.5 causal_conv1d==1.5.2", # Optional torchao dependency used by Unsloth. "uv pip install --system --no-cache --no-deps 'torchao>=0.16.0'", ) ) app = modal.App("retro-alpha-finetune", image=image) secrets = [modal.Secret.from_name("huggingface-secret")] @app.function( gpu="A100-40GB", timeout=60 * 60 * 6, # 6 hours secrets=secrets, ) def train( base_model: str = "unsloth/NVIDIA-Nemotron-3-Nano-4B", dataset_repo: str = "sankalphs/retro-alpha-dataset", output_repo: str = "sankalphs/retro-alpha-nemotron-lora", num_epochs: int = 3, per_device_batch_size: int = 4, gradient_accumulation_steps: int = 4, learning_rate: float = 2e-4, lora_r: int = 16, lora_alpha: int = 32, max_seq_length: int = 1024, ): import unsloth # noqa: F401, must import first per Unsloth warning import torch from datasets import load_dataset from trl import SFTConfig, SFTTrainer from unsloth import FastLanguageModel hf_token = os.environ.get("HF_TOKEN") if hf_token: from huggingface_hub import login login(token=hf_token) print(f"Loading base model: {base_model}") model, tokenizer = FastLanguageModel.from_pretrained( model_name=base_model, max_seq_length=max_seq_length, load_in_4bit=False, # user asked for non-quantized fine-tuning load_in_8bit=False, full_finetuning=False, trust_remote_code=True, attn_implementation="eager", ) model = FastLanguageModel.get_peft_model( model, r=lora_r, lora_alpha=lora_alpha, target_modules=[ "q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj", "in_proj", "out_proj", ], lora_dropout=0, bias="none", use_gradient_checkpointing="unsloth", random_state=3407, ) model.print_trainable_parameters() print(f"Loading dataset: {dataset_repo}") dataset = load_dataset(dataset_repo, split="train") def format_messages(examples): convos = examples["messages"] texts = [ tokenizer.apply_chat_template( convo, tokenize=False, add_generation_prompt=False, ) for convo in convos ] return {"text": texts} dataset = dataset.map(format_messages, batched=True) output_dir = "/tmp/retro-alpha-lora" training_args = SFTConfig( output_dir=output_dir, num_train_epochs=num_epochs, per_device_train_batch_size=per_device_batch_size, gradient_accumulation_steps=gradient_accumulation_steps, learning_rate=learning_rate, warmup_ratio=0.05, weight_decay=0.01, logging_steps=10, save_strategy="epoch", fp16=False, bf16=True, group_by_length=True, report_to="none", remove_unused_columns=False, dataset_text_field="text", max_seq_length=max_seq_length, ) trainer = SFTTrainer( model=model, train_dataset=dataset, processing_class=tokenizer, args=training_args, ) print("Starting training...") trainer.train() final_dir = f"{output_dir}/final" print(f"Saving LoRA adapter to {final_dir}") trainer.model.save_pretrained(final_dir) tokenizer.save_pretrained(final_dir) if hf_token: print(f"Pushing LoRA adapter to {output_repo}") from huggingface_hub import HfApi api = HfApi(token=hf_token) try: api.create_repo(repo_id=output_repo, exist_ok=True) except Exception as e: print(f"Could not create {output_repo}: {e}") # Fallback to user namespace me = api.whoami()["name"] output_repo = f"{me}/retro-alpha-nemotron-lora" api.create_repo(repo_id=output_repo, exist_ok=True) api.upload_folder(folder_path=final_dir, repo_id=output_repo) return f"Training complete. LoRA saved to {output_repo}" @app.local_entrypoint() def main(): result = train.remote() print(result)