# Copyright (c) Microsoft. All rights reserved.

"""Helper module for Unsloth training with LoRA.

This module provides utilities for training language models using the Unsloth library,
which optimizes training performance with 4-bit quantization and LoRA (Low-Rank Adaptation).

The training function should be run in a separate process to ensure GPU memory is properly freed.
"""

from datasets import Dataset as HuggingFaceDataset  # type: ignore
from rich.console import Console

console = Console()


def unsloth_training(model_path: str, sft_dataset: HuggingFaceDataset, next_model_path: str):
    """Train a Unsloth model on a SFT dataset.

    This is recommended to be run in a separate process to avoid GPU memory issues.

    Args:
        model_path: The path to the model to train. Must be a local path.
        sft_dataset: The SFT dataset to train on.
        next_model_path: The path to save the trained model.
    """

    from unsloth import FastLanguageModel

    # The two imports must come in this order to make unsloth patch work.
    if True:
        # The SFTTrainer is actually patched by unsloth.
        from trl import SFTConfig, SFTTrainer  # type: ignore

    model, tokenizer = FastLanguageModel.from_pretrained(  # type: ignore
        model_name=model_path,
        max_seq_length=4096,  # Choose any for long context!
        load_in_4bit=True,  # 4 bit quantization to reduce memory
    )

    # Config the model to use LoRA
    model = FastLanguageModel.get_peft_model(  # type: ignore
        model,  # type: ignore
        r=32,
        target_modules=[
            "q_proj",
            "k_proj",
            "v_proj",
            "o_proj",
            "gate_proj",
            "up_proj",
            "down_proj",
        ],
        lora_alpha=32,
        lora_dropout=0,  # Supports any, but = 0 is optimized
        bias="none",  # Supports any, but = "none" is optimized
        use_gradient_checkpointing="unsloth",  # True or "unsloth" for very long context
        random_state=3407,
        use_rslora=False,  # Rank stabilized LoRA
        loftq_config=None,  # And LoftQ
    )

    sft_config = SFTConfig(
        per_device_train_batch_size=2,
        gradient_accumulation_steps=4,  # Use GA to mimic batch size!
        warmup_steps=5,
        max_steps=60,  # Maximum number of steps to train for
        # num_train_epochs = 1, # Set this for 1 full training run
        learning_rate=2e-4,  # Reduce to 2e-5 for long training runs
        logging_steps=1,
        optim="adamw_8bit",
        weight_decay=0.01,
        lr_scheduler_type="linear",
        seed=3407,
        report_to="none",  # Use this for W&B etc
    )

    trainer = SFTTrainer(
        model=model,  # type: ignore
        tokenizer=tokenizer,  # type: ignore
        train_dataset=sft_dataset,
        args=sft_config,
    )

    trainer_stats = trainer.train()  # type: ignore
    console.print(f"[bold red][Algo][/bold red] Trainer stats: {trainer_stats}")

    # Save in 16-bit for vLLM inference later
    model.save_pretrained_merged(next_model_path, tokenizer, save_method="merged_16bit")  # type: ignore

    # All unsloth memory should be freed now.
    # But this won't happen unless you run unsloth_training in a separate process!
