import os
from datasets import Dataset as HFDataset
from unsloth import FastLanguageModel
from trl import SFTTrainer  # 用于监督微调的训练器
from transformers import TrainingArguments,DataCollatorForSeq2Seq  # 用于配置训练参数
from unsloth import is_bfloat16_supported  # 检查是否支持bfloat16精度训练
from unsloth.chat_templates import get_chat_template, train_on_responses_only
def get_model_name(model):
    return os.path.basename(model.name_or_path)
def formatting_prompts(examples,tokenizer):
    """格式化对话数据的函数
    Args:
        examples: 包含对话列表的字典
    Returns:
        包含格式化文本的字典
    """
    questions = examples["question"]
    answer = examples["answer"]
    
    convos = [
        [{"role": "user", "content": q}, {"role": "assistant", "content": r}]
        for q, r in zip(questions, answer)
    ]
    
    # 使用tokenizer.apply_chat_template格式化对话
    texts = [
        tokenizer.apply_chat_template(convo, tokenize=False, add_generation_prompt=False)
        for convo in convos
    ]
    
    return {"text": texts}


def train_model(
    model, 
    tokenizer, 
    dataset: list, 
    train_dir: str, 
    learning_rate: float, 
    per_device_train_batch_size: int, 
    epoch: int, 
    save_steps: int, 
    lora_rank: int,
    trainer_callback=None
) -> None:

    model = FastLanguageModel.get_peft_model(
        # 原始模型
        model,
        # LoRA秩,用于控制低秩矩阵的维度,值越大表示可训练参数越多,模型性能可能更好但训练开销更大
        # 建议: 8-32之间
        r=lora_rank,  # 使用动态传入的LoRA秩
        # 需要应用LoRA的目标模块列表
        target_modules=[
            "q_proj", "k_proj", "v_proj", "o_proj",  # attention相关层
            "gate_proj", "up_proj", "down_proj",  # FFN相关层
        ],
        # LoRA缩放因子,用于控制LoRA更新的幅度。值越大，LoRA的更新影响越大。
        lora_alpha=16,
        # LoRA层的dropout率,用于防止过拟合,这里设为0表示不使用dropout。
        # 如果数据集较小，建议设置0.1左右。
        lora_dropout=0,
        # 是否对bias参数进行微调,none表示不微调bias
        # none: 不微调偏置参数；
        # all: 微调所有参数；
        # lora_only: 只微调LoRA参数。
        bias="none",
        # 是否使用梯度检查点技术节省显存,使用unsloth优化版本
        # 会略微降低训练速度，但可以显著减少显存使用
        use_gradient_checkpointing="unsloth",
        # 随机数种子,用于结果复现
        random_state=3407,
        # 是否使用rank-stabilized LoRA,这里不使用
        # 会略微降低训练速度，但可以显著减少显存使用
        use_rslora=False,
        # LoFTQ配置,这里不使用该量化技术，用于进一步压缩模型大小
        loftq_config=None,
    )

    tokenizer = get_chat_template(
        tokenizer,
        chat_template="qwen-2.5",
    )

    train_dataset = HFDataset.from_list(dataset)
    train_dataset = train_dataset.map(formatting_prompts, 
                        fn_kwargs={"tokenizer": tokenizer},
                        batched=True)

    # 初始化SFT训练器
    trainer = SFTTrainer(
        model=model, # 待训练的模型
        tokenizer=tokenizer,  # 分词器
        train_dataset=train_dataset,  # 训练数据集
        dataset_text_field="text",  # 数据集字段的名称
        max_seq_length=model.max_seq_length,  # 最大序列长度
        data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer),
        dataset_num_proc=1,  # 数据集处理的并行进程数
        packing=False,
        args=TrainingArguments(
            per_device_train_batch_size=per_device_train_batch_size,  # 每个GPU的训练批次大小
            gradient_accumulation_steps=4,  # 梯度累积步数,用于模拟更大的batch size
            warmup_steps=int(epoch * 0.1),  # 预热步数,逐步增加学习率
            learning_rate=learning_rate,  # 学习率
            lr_scheduler_type="linear", # 线性学习率调度器
            max_steps=int(epoch * len(train_dataset)/per_device_train_batch_size), # 最大训练步数（一步 = 处理一个batch的数据）
            fp16=not is_bfloat16_supported(),  # 如果不支持bf16则使用fp16
            bf16=is_bfloat16_supported(),  # 如果支持则使用bf16
            logging_steps=1,  # 每1步记录一次日志
            optim="adamw_8bit",  # 使用8位AdamW优化器节省显存，几乎不影响训练效果
            weight_decay=0.01,  # 权重衰减系数,用于正则化，防止过拟合
            seed=114514,  # 随机数种子
            output_dir=train_dir + "/checkpoints",  # 保存模型检查点和训练日志
            save_strategy="steps",  # 按步保存中间权重
            save_steps=save_steps,  # 使用动态传入的保存步数
            logging_dir=train_dir + "/logs",  # 日志文件存储路径
            report_to="tensorboard",  # 使用TensorBoard记录日志
        ),
    )
    
    if trainer_callback is not None:
        trainer.add_callback(trainer_callback)

    trainer = train_on_responses_only(
        trainer,
        instruction_part = "<|im_start|>user\n",
        response_part = "<|im_start|>assistant\n",
    )

    # 开始训练
    trainer_stats = trainer.train(resume_from_checkpoint=False)