一次完整大模型Lora训练实现“AI面试风”

闲来无事,最近面试工作比较多,搞个模型训练下,看看能不能实现回答风格语气,符合面试的要求。

机器环境

本地物理机Ubuntu22.04系统

CPU ultra 265K 20核

内存32GB

GPU RTX 5080 16GB显存

可正常访问所有网络,下载pytorch相关依赖包可能会需要国外网络环境。

1、创建虚拟环境

使用anoconda平台进行环境管理

bash 复制代码
conda create -n finetune_env python=3.10 -y
conda activate finetune_env
复制代码
conda create -n finetune_env python=3.10 -y 
CondaToSNonInteractiveError: Terms of Service have not been accepted for the following channels. Please accept or remove them before proceeding: - https://repo.anaconda.com/pkgs/main - https://repo.anaconda.com/pkgs/r To accept these channels' Terms of Service, run the following commands: 
conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/main 
conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/r 
For information on safely removing channels from your conda configuration, please see the official documentation: https://www.anaconda.com/docs/tools/working-with-conda/channels

排查原因是Anaconda 官方源(pkgs/main / pkgs/r)需要你先接受 ToS(服务条款),conda 现在在"非交互模式"下直接拦住了。霸王条款只能接受,接受后继续安装

sh 复制代码
conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/main
conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/r
# 本质原因
# Anaconda 从近几年开始对默认仓库加入 ToS:
# defaults (pkgs/main, pkgs/r) 属于 Anaconda 商业控制源
# 企业/CI/自动化环境必须显式接受协议
# 非交互 shell(CI / Docker / SSH脚本)不会弹确认 → 直接报错

2、安装核心依赖

sh 复制代码
# 注意 pytorch三件套的包版本强一致,指定 --index-url https://download.pytorch.org/whl/cu128 
# 我是RTX 5080的 Blackwell架构的nvidia显卡,cuda版本12.8 因此是cu128,根据实际情况调整
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu128  
pip install transformers datasets peft accelerate bitsandbytes trl 
复制代码
Downloading https://download-r2.pytorch.org/whl/cu128/torch-2.11.0+cu128-cp310-cp310-manylinux_2_28_x86_64.whl (780.4 MB) ━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 137.4/780.4 MB 1.0 MB/s eta 0:09:31
# 下载速度太慢可以将比较关键的几个包通过whl文件装上
# 其它包走国内清华源 下载,临时指定[-i https://pypi.tuna.tsinghua.edu.cn/simple]
nvidia_cudnn_cu12-9.19.0.56-py3-none-manylinux_2_27_x86_64.whl # 先安装
torch-2.11.0+cu128-cp310-cp310-manylinux_2_28_x86_64.whl # 依赖上个包
sh 复制代码
# 配置accelerate:执行以下命令并按照提示选择(单机、单卡、BF16):
# 方便训练任务隔离、对比,指定accelerate配置文件,不同训练任务使用不同的文件
accelerate config --config-file accelerate_config.yaml
In which compute environment are you running?
Please select a choice using the arrow or number keys, and selecting with enter
 ➔  This machine  # 本机 就选它
    AWS (Amazon SageMaker)
----------------------------------------------------------
Which type of machine are you using?
Please select a choice using the arrow or number keys, and selecting with enter
 ➔  No distributed training  # 单机单卡就这个
    multi-CPU
    multi-XPU
    multi-HPU
    multi-GPU
    multi-NPU
    multi-MLU
    multi-SDAA
    multi-MUSA
    multi-NEURON
    TPU
Do you want to run your training on CPU only (even if a GPU / Apple Silicon / Ascend NPU device is available)? [yes/NO]:no # 使用GPU训练=no
Do you wish to optimize your script with torch dynamo?[yes/NO]:no # 是否启用 PyTorch 2.x 的动态图编译入口(torch.compile 的前端)选no,yes有坑爆内存OOM
Do you want to use DeepSpeed? [yes/NO]: no  # 环境用不上,建议不用
What GPU(s) (by id) should be used for training on this machine as a comma-separated list? [all]: # 使用哪个显卡,只有1张卡,默认all
Would you like to enable numa efficiency? (Currently only supported on NVIDIA hardware). [yes/NO]: no
------------------------------------------------------------------------------------
Do you wish to use mixed precision?
bf16   # 精度类似fp32 但是显存占用降低一般,算是fp16的优化版
accelerate configuration saved at tmp.yaml

3、准备数据集

训练集数据决定了Lora训练的效果**【非常非常重要】**为方便后面测试效果,可以将数据拆分8:1:1,分别用于训练、验证、测试集,留下10%的数据进行benchmark效果验证。

我这里只准备了300条数据,就只分训练、验证=9:1比例。最后再人工搞点数据进行测试即可。

数据采用instruction、input、output格式,整个文件是jsonl格式文件,一个json一行。

json 复制代码
{"instruction":"什么是LoRA微调?","input":"","output":"LoRA(Low-Rank Adaptation)是一种参数高效微调方法,通过 冻结预训练模型权重,仅在权重矩阵旁增加低秩分解矩阵A和B进行训练,从而以极低显存成本实现模型能力适配。"}
{"instruction":"LoRA为什么能减少显存?","input":"","output":"因为LoRA冻结了原始模型参数,仅训练低秩矩阵A和B,显著减少梯度与优化参数数量,因此显存主要用于激活值而非全量权重更新,从而大幅降低显存占用。"}

4、训练

量化配置说明

python3 复制代码
# 4bit量化配置
    bnb_config = BitsAndBytesConfig(
        load_in_4bit=True,
        bnb_4bit_quant_type="nf4",
        bnb_4bit_compute_dtype=torch.bfloat16,
        bnb_4bit_use_double_quant=True,
    )

load_in_4bit=True

原理:将模型权重压缩到 4-bit,减少显存占用 2~4 倍。

优点:可在单卡 16GB 左右训练 7B+ 模型。

缺点:梯度计算不完全精确,可能影响最终收敛。

选值逻辑:

显存 <24GB → 建议 4-bit

显存 >=32GB → 可用 8-bit 或 float16
bnb_4bit_quant_type="nf4"

原理:NF4(Normal Float 4-bit)是一种针对神经网络训练优化的非线性浮点 4-bit 表示。

优劣势:

NF4 在小梯度情况下更稳定,比 FP4 精度更高。

FP4 更节省显存,但训练可能震荡。

推荐:LoRA 微调 + causal LM → NF4 更稳定。
bnb_4bit_compute_dtype=torch.bfloat16

原理:量化权重为 4-bit,但计算仍用 bfloat16 保证梯度稳定。

建议:

H100/A100 GPU → bfloat16

V100/RTX3090 → float16(bfloat16 支持不佳)
bnb_4bit_use_double_quant=True

原理:二次量化(double quant)进一步压缩量化权重,先量化到 8-bit 再到 4-bit。

优劣势:

优点:显存占用最小。

缺点:计算复杂度略升,推理略慢。

经验:显存紧张或 batch size 超小才必须。

Tokenizer配置

python 复制代码
    # tokenizer
    tokenizer = AutoTokenizer.from_pretrained(
        model_id,
        use_fast=False,
        trust_remote_code=True
    )
    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token
    tokenizer.padding_side = "right"

use_fast=False

原理:Transformers 有 Python 版和 Rust 版 tokenizer。

Rust 版快(多线程),但不支持复杂自定义逻辑。

经验:

Qwen 这种自定义 tokenizer 必须 False。
pad_token

原理:Causal LM 对右侧填充敏感,pad token 需等于 eos token。

场景:

batch 内序列长度不一,保证 attention mask 正确。
padding_side="right"

原理:Causal LM 只看左上下文,如果填充在左边,模型会看到未来 token → 训练错误。

经验:所有 causal LM 默认右填充。

模型加载与训练优化

python 复制代码
    # model
    model = AutoModelForCausalLM.from_pretrained(
        model_id,
        quantization_config=bnb_config,
        device_map="auto",
        dtype=torch.bfloat16,
        trust_remote_code=True,
    )
    if hasattr(model.config, "use_cache"):
        model.config.use_cache = False
    model = prepare_model_for_kbit_training(model)

device_map="auto" 自动分配到 GPU/CPU 根据显存自动切分模型,多卡或单卡大模型使用
dtype=torch.bfloat16 权重计算类型 提升训练稳定性,LoRA + 量化推荐 bfloat16
use_cache=False 禁用 past_key_values 如果缓存,梯度无法反传 必须 False 才能梯度更新 LoRA
prepare_model_for_kbit_training LoRA 训练优化,冻结大部分参数,仅 LoRA 可训练,减少显存,kbit 模型必须调用

LoRA配置(LoraConfig)

python 复制代码
    # LoRA
    lora_config = LoraConfig(
        r=8,   # 从16降到8,更省显存
        lora_alpha=16,
        target_modules=[
            "q_proj", "k_proj", "v_proj", "o_proj",
            "gate_proj", "up_proj", "down_proj",
        ],
        lora_dropout=0.05,
        bias="none",
        task_type="CAUSAL_LM",
    )

参数解析
r意义:低秩矩阵维度。原理:LoRA 将权重矩阵分解为 W + A*BA/B 维度为 r

经验:r 越大 → 表达能力强,但显存和梯度占用大。

7B 模型单卡 16GB → r=8 是显存平衡方案,再大就爆显存了。
lora_alpha 意义:缩放 LoRA 权重。公式:实际梯度 = (lora_alpha / r) * ΔW

经验:默认 16 → 可以根据学习率微调,r 小时 alpha 大些。
target_modules 意义:只给部分线性层注入 LoRA,即Lora只影响部分模型层。

经验:

Transformer Attention:q_proj/k_proj/v_proj/o_proj

FFN:gate_proj/up_proj/down_proj

不修改其他模块可大幅省显存。
lora_dropout=0.05 防过拟合,小数据集建议 0.05~0.1。
bias="none" 显存最省 → 忽略偏置梯度更新。可选 "all" 或 "lora_only",一般不需要。

训练配置(SFTConfig)

python 复制代码
# 低显存训练配置
    sft_config = SFTConfig(
        output_dir=output_dir,
        dataset_text_field="text",
        per_device_train_batch_size=1,
        gradient_accumulation_steps=16,
        learning_rate=2e-4,
        num_train_epochs=3,
        bf16=True,
        logging_steps=1,
        logging_strategy="steps",
        save_strategy="epoch",
        save_total_limit=2,
        report_to="none",
        remove_unused_columns=False,
        gradient_checkpointing=True,
        optim="paged_adamw_8bit",
    )
    # 关键:缩短长度,15GB 显卡建议先 512, 实际在r=8的情况下 1024也能跑。
    sft_config.max_seq_length = 512

显存优化组合:

batch_size=1 + grad_accum=16 → 等效 batch=16

max_seq_length=1024 → 控制 attention 激活显存

gradient_checkpointing=True → checkpoint 中间激活减少显存(但训练慢 20~30%)

optim="paged_adamw_8bit" → 8-bit 优化器,显存降低约 30

下面是无验证集情况下的完整训练脚本lora_train.py

python3 复制代码
import os
import warnings
import torch
from datasets import load_dataset
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from trl import SFTTrainer, SFTConfig

warnings.filterwarnings("ignore", category=FutureWarning)
# 控制pytorch显存分配策略 允许显存分段动态扩展,减少 OOM 风险	大模型训练、GPU 显存紧张时添加
os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"
# 关闭 Transformers 提示	避免控制台过多警告	训练日志清爽,尤其在批量实验时
os.environ["TRANSFORMERS_NO_ADVISORY_WARNINGS"] = "true"
# 禁用 tokenizer 并行	并行 tokenizer 有时与 Dataloader 冲突导致死锁	数据量中等或 batch 严格控制顺序时
os.environ["TOKENIZERS_PARALLELISM"] = "false"
# 经验:大规模 LoRA/量化训练时,这几条是基础的"显存/稳定性保护配置"。

def main():
    model_id = "Qwen/Qwen2.5-7B-Instruct"
    data_path = "/home/yqx/llm_youhua/lora_finetune/train_data.jsonl"
    output_dir = "./qwen-lora-output"
    final_adapter_dir = "./final_lora_adapter"

    # 4bit量化配置
    bnb_config = BitsAndBytesConfig(
        load_in_4bit=True,
        bnb_4bit_quant_type="nf4",
        bnb_4bit_compute_dtype=torch.bfloat16,
        bnb_4bit_use_double_quant=True,
    )

    # tokenizer
    tokenizer = AutoTokenizer.from_pretrained(
        model_id,
        use_fast=False,
        trust_remote_code=True
    )

    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token
    tokenizer.padding_side = "right"

    # model
    model = AutoModelForCausalLM.from_pretrained(
        model_id,
        quantization_config=bnb_config,
        device_map="auto",
        dtype=torch.bfloat16,
        trust_remote_code=True,
    )

    if hasattr(model.config, "use_cache"):
        model.config.use_cache = False

    model = prepare_model_for_kbit_training(model)

    # LoRA
    lora_config = LoraConfig(
        r=8,   # 从16降到8,更省显存
        lora_alpha=16,
        target_modules=[
            "q_proj",
            "k_proj",
            "v_proj",
            "o_proj",
            "gate_proj",
            "up_proj",
            "down_proj",
        ],
        lora_dropout=0.05,
        bias="none",
        task_type="CAUSAL_LM",
    )

    model = get_peft_model(model, lora_config)
    model.print_trainable_parameters()

    # dataset
    dataset = load_dataset(
        "json",
        data_files=data_path,
        split="train"
    )

    def formatting_prompts_func(example):
        instruction = example["instruction"]
        output = example["output"]

        text = (
            "<|im_start|>system\n"
            "You are a helpful assistant.<|im_end|>\n"
            f"<|im_start|>user\n{instruction}<|im_end|>\n"
            f"<|im_start|>assistant\n{output}<|im_end|>"
        )
        return {"text": text}

    dataset = dataset.map(
        formatting_prompts_func,
        remove_columns=dataset.column_names
    )

    # 低显存训练配置
    sft_config = SFTConfig(
        output_dir=output_dir,
        dataset_text_field="text",

        per_device_train_batch_size=1,
        gradient_accumulation_steps=16,

        learning_rate=2e-4,
        num_train_epochs=3,

        bf16=True,
        logging_steps=1,
        logging_strategy="steps",

        save_strategy="epoch",
        save_total_limit=2,

        report_to="none",
        remove_unused_columns=False,

        gradient_checkpointing=True,
        optim="paged_adamw_8bit",
    )

    # 关键:缩短长度,15GB 显卡建议先 512
    sft_config.max_seq_length = 512

    trainer = SFTTrainer(
        model=model,
        args=sft_config,
        train_dataset=dataset,
        processing_class=tokenizer,
    )

    trainer.train()

    trainer.save_model(final_adapter_dir)
    tokenizer.save_pretrained(final_adapter_dir)

    print(f"训练完成,LoRA adapter 已保存到: {final_adapter_dir}")


if __name__ == "__main__":
    main()

训练命令:accelerate launch --config-file accelerate_qwen2.5-7b.yaml lora_train.py 正常效果如下

复制代码
accelerate launch --config-file accelerate_qwen2.5-7b.yaml lora_train.py
Loading weights: 100%|██████████████████████████████████████████████████████| 339/339 [00:02<00:00, 146.14it/s]
trainable params: 20,185,088 || all params: 7,635,801,600 || trainable%: 0.2643
The tokenizer has new PAD/BOS/EOS tokens that differ from the model config and generation config. The model config and generation config were aligned accordingly, being updated with the tokenizer's values. Updated tokens: {'bos_token_id': None, 'pad_token_id': 151643}.
{'loss': '4.851', 'grad_norm': '0.2793', 'learning_rate': '0.0002', 'entropy': '2.167', 'num_tokens': '756', 'mean_token_accuracy': '0.4042', 'epoch': '0.05926'}
{'loss': '3.976', 'grad_norm': '0.2139', 'learning_rate': '0.0001961', 'entropy': '2.285', 'num_tokens': '1461', 'mean_token_accuracy': '0.4419', 'epoch': '0.1185'}
  4%|██▉                                                                        | 2/51 [00:06<02:32,  3.11s/it]

可以看到训练step一直在走,且loss一直是收敛状态,直到训练完成。

检查生成的权重适配器目录是否是下面的构造

sh 复制代码
 tree -L 2
.
├── adapter_config.json
├── adapter_model.safetensors
├── chat_template.jinja
├── README.md
├── tokenizer_config.json
├── tokenizer.json
└── training_args.bin

目录检查正常再通过check_lora_weights.py 脚本检查是否有正常的权重影响。

python 复制代码
import torch
from safetensors.torch import load_file

path = "./final_lora_adapter/adapter_model.safetensors"
state_dict = load_file(path)

print("num tensors:", len(state_dict))

total_abs = 0.0
total_num = 0

for k, v in state_dict.items():
 print(k, v.shape, v.dtype)
 total_abs += v.abs().sum().item()
 total_num += v.numel()

print("total abs sum:", total_abs)
print("total num:", total_num)
print("mean abs:", total_abs / total_num if total_num > 0 else 0.0)

脚本执行结果类似下面内容说明是正常的Lora训练完成。

复制代码
...
base_model.model.model.layers.9.self_attn.q_proj.lora_B.weight torch.Size([3584, 8]) torch.bfloat16
base_model.model.model.layers.9.self_attn.v_proj.lora_A.weight torch.Size([8, 3584]) torch.bfloat16
base_model.model.model.layers.9.self_attn.v_proj.lora_B.weight torch.Size([512, 8]) torch.bfloat16
total abs sum: 72043.875
total num: 20185088
mean abs: 0.0035691632852925884

进阶优化训练

完整脚本lora_train_check.py

python3 复制代码
import os
import warnings
import torch
from datasets import load_dataset
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from trl import SFTTrainer, SFTConfig

warnings.filterwarnings("ignore", category=FutureWarning)
os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"
os.environ["TRANSFORMERS_NO_ADVISORY_WARNINGS"] = "true"
os.environ["TOKENIZERS_PARALLELISM"] = "false"


def main():
    model_id = "Qwen/Qwen2.5-7B-Instruct"
    data_path = "/home/yqx/llm_youhua/lora_finetune/train_data.jsonl"

    output_dir = "./qwen-lora-output-1024-check"
    final_adapter_dir = "./final_lora_adapter-1024-check"

    # =========================
    # 1 4bit量化配置
    # =========================
    bnb_config = BitsAndBytesConfig(
        load_in_4bit=True,
        bnb_4bit_quant_type="nf4",
        bnb_4bit_compute_dtype=torch.bfloat16,
        bnb_4bit_use_double_quant=True,
    )

    # =========================
    # 2 tokenizer
    # =========================
    tokenizer = AutoTokenizer.from_pretrained(
        model_id,
        use_fast=False,
        trust_remote_code=True
    )

    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token

    tokenizer.padding_side = "right"

    # =========================
    # 3 model
    # =========================
    model = AutoModelForCausalLM.from_pretrained(
        model_id,
        quantization_config=bnb_config,
        device_map="auto",
        dtype=torch.bfloat16,
        trust_remote_code=True,
    )

    if hasattr(model.config, "use_cache"):
        model.config.use_cache = False

    model = prepare_model_for_kbit_training(model)

    # =========================
    # 4 LoRA配置
    # =========================
    lora_config = LoraConfig(
        r=8,
        lora_alpha=16,
        target_modules=[
            "q_proj", "k_proj", "v_proj", "o_proj",
            "gate_proj", "up_proj", "down_proj",
        ],
        lora_dropout=0.05,
        bias="none",
        task_type="CAUSAL_LM",
    )

    model = get_peft_model(model, lora_config)
    model.print_trainable_parameters()

    # =========================
    # 5 数据加载 + 划分
    # =========================
    dataset = load_dataset(
        "json",
        data_files=data_path,
        split="train"
    )

    dataset = dataset.train_test_split(test_size=0.1, seed=42)
    train_dataset = dataset["train"]
    eval_dataset = dataset["test"]

    # =========================
    # 6 prompt格式化
    # =========================
    def formatting_prompts_func(example):
        instruction = example["instruction"]
        output = example["output"]

        text = (
            "<|im_start|>system\n"
            "You are a helpful assistant.<|im_end|>\n"
            f"<|im_start|>user\n{instruction}<|im_end|>\n"
            f"<|im_start|>assistant\n{output}<|im_end|>"
        )
        return {"text": text}

    train_dataset = train_dataset.map(
        formatting_prompts_func,
        remove_columns=train_dataset.column_names
    )

    eval_dataset = eval_dataset.map(
        formatting_prompts_func,
        remove_columns=eval_dataset.column_names
    )

    # =========================
    # 7 训练配置
    # =========================
    sft_config = SFTConfig(
        output_dir=output_dir,
        dataset_text_field="text",

        # batch控制
        per_device_train_batch_size=1,
        per_device_eval_batch_size=1,
        gradient_accumulation_steps=16,

        # 学习
        learning_rate=2e-4,
        num_train_epochs=3,

        # 精度
        bf16=True,

        # logging
        logging_steps=5,
        logging_strategy="steps",

        #  eval关键
        eval_strategy="epoch",
        eval_steps=50,

        # 保存
        save_strategy="epoch",
        save_total_limit=2,

        #  自动选最优模型
        load_best_model_at_end=True,
        metric_for_best_model="eval_loss",
        greater_is_better=False,

        # 其他
        report_to="none",
        remove_unused_columns=False,

        gradient_checkpointing=True,
        optim="paged_adamw_8bit",
    )

    # 序列长度(显存关键参数)
    sft_config.max_seq_length = 1024

    # =========================
    # 8 Trainer
    # =========================
    trainer = SFTTrainer(
        model=model,
        args=sft_config,
        train_dataset=train_dataset,
        eval_dataset=eval_dataset,
        processing_class=tokenizer,
    )

    # =========================
    # 9 开始训练
    # =========================
    trainer.train()

    # =========================
    #  最终评估
    # =========================
    metrics = trainer.evaluate()
    print("Final eval metrics:", metrics)

    # =========================
    # 保存LoRA
    # =========================
    trainer.save_model(final_adapter_dir)
    tokenizer.save_pretrained(final_adapter_dir)

    print(f"训练完成,LoRA adapter 已保存到: {final_adapter_dir}")


if __name__ == "__main__":
    main()

训练结果loss 日志

accelerate launch --config-file accelerate_qwen2.5-7b.yaml lora_train_check.py

...

{'loss': '3.614', 'grad_norm': '0.248', 'learning_rate': '0.0001843', 'entropy': '2.152', 'num_tokens': '3646', 'mean_token_accuracy': '0.4942', 'epoch': '0.2952'}

{'loss': '1.978', 'grad_norm': '0.1143', 'learning_rate': '0.0001647', 'entropy': '2.014', 'num_tokens': '7468', 'mean_token_accuracy': '0.6253', 'epoch': '0.5904'}

{'loss': '1.313', 'grad_norm': '0.06836', 'learning_rate': '0.0001451', 'entropy': '1.61', 'num_tokens': '1.115e+04', 'mean_token_accuracy': '0.731', 'epoch': '0.8856'}

{'eval_loss': '1.132', 'eval_runtime': '1.94', 'eval_samples_per_second': '15.98', 'eval_steps_per_second': '15.98', 'eval_entropy': '1.19', 'eval_num_tokens': '1.257e+04', 'eval_mean_token_accuracy': '0.735', 'epoch': '1'}

{'loss': '1.086', 'grad_norm': '0.0835', 'learning_rate': '0.0001255', 'entropy': '1.185', 'num_tokens': '1.486e+04', 'mean_token_accuracy': '0.7509', 'epoch': '1.177'}

{'loss': '0.9327', 'grad_norm': '0.06689', 'learning_rate': '0.0001059', 'entropy': '1.06', 'num_tokens': '1.844e+04', 'mean_token_accuracy': '0.7813', 'epoch': '1.472'}

{'loss': '0.8667', 'grad_norm': '0.06787', 'learning_rate': '8.627e-05', 'entropy': '0.9252', 'num_tokens': '2.228e+04', 'mean_token_accuracy': '0.7926', 'epoch': '1.768'}

{'eval_loss': '0.8593', 'eval_runtime': '1.936', 'eval_samples_per_second': '16.01', 'eval_steps_per_second': '16.01', 'eval_entropy': '0.8659', 'eval_num_tokens': '2.515e+04', 'eval_mean_token_accuracy': '0.7817', 'epoch': '2'}

{'loss': '0.7969', 'grad_norm': '0.07275', 'learning_rate': '6.667e-05', 'entropy': '0.8607', 'num_tokens': '2.589e+04', 'mean_token_accuracy': '0.8122', 'epoch': '2.059'}

{'loss': '0.6508', 'grad_norm': '0.08154', 'learning_rate': '4.706e-05', 'entropy': '0.722', 'num_tokens': '2.962e+04', 'mean_token_accuracy': '0.8481', 'epoch': '2.354'}

{'loss': '0.5692', 'grad_norm': '0.09961', 'learning_rate': '2.745e-05', 'entropy': '0.6537', 'num_tokens': '3.332e+04', 'mean_token_accuracy': '0.861', 'epoch': '2.649'}

{'loss': '0.6072', 'grad_norm': '0.07812', 'learning_rate': '7.843e-06', 'entropy': '0.647', 'num_tokens': '3.701e+04', 'mean_token_accuracy': '0.8514', 'epoch': '2.945'}

{'eval_loss': '0.7566', 'eval_runtime': '1.943', 'eval_samples_per_second': '15.96', 'eval_steps_per_second': '15.96', 'eval_entropy': '0.752', 'eval_num_tokens': '3.772e+04', 'eval_mean_token_accuracy': '0.8078', 'epoch': '3'}

{'train_runtime': '163.4', 'train_samples_per_second': '4.975', 'train_steps_per_second': '0.312', 'train_loss': '1.228', 'epoch': '3'}

Final eval metrics: {'eval_loss': 0.7566098570823669, 'eval_runtime': 1.9631, 'eval_samples_per_second': 15.791, 'eval_steps_per_second': 15.791}

训练完成,LoRA adapter 已保存到: ./final_lora_adapter-1024-check

综合分析

阶段 (Epoch) 训练 Loss 验证 Loss (Eval) Token 准确率 (Train) 验证准确率 (Eval)
0.30 3.614 - 49.42% -
1.00 1.313 1.132 73.10% 73.50%
2.00 0.8667 0.8593 79.26% 78.17%
3.00 0.6072 0.7566 85.14% 80.78%

关键观察:

  • Loss 下降显著且平滑:从初始的 3.614 快速下降到 0.607。说明学习率(Learning Rate,初始约 2e-4)设置合理,模型能够快速捕捉到这 300 条数据的分布。

  • 准确率(Accuracy)提升稳定:训练准确率从 49% 提升至 85%,验证集也达到了 80.78%。说明模型不仅在"背书",也具备一定的泛化能力。

  • 拟合状态评估:是否过拟合?

    通过对比 Train Loss 和 Eval Loss:

    Epoch 1-2:Eval Loss 始终略低于或等于 Train Loss。这是一个极佳的信号,说明模型此时处于"快速学习期",泛化性能非常好。

    Epoch 3:Train Loss 继续下降至 0.607,但 Eval Loss 下降速度放缓(0.756)。

    结论:模型在第 3 个 Epoch 开始出现轻微的过拟合倾向(即模型开始过度拟合训练集的特定表达),但目前并不严重,因为验证集准确率(80.78%)仍在上升。

  • 训练质量细节梯度范数 (Grad Norm)

    日志显示 grad_norm 长期稳定在 0.06 - 0.24 之间。说明训练过程非常稳定,没有出现梯度爆炸或消失的情况。学习率调度 (LR Scheduler):采用的是线性衰减策略(从 2e-4 降至 7e-6)。这种配置配合 3 个 Epoch,给模型留下了足够的后期微调空间。训练效率:可训练参数量:20.18M (占比 0.26%)。这是一个标准的 LoRA 配置(可能是 r=8r=8r=8 或 r=16r=16r=16)。速度:4.975 samples/s,对于 7B 模型来说,在主流单卡(如 A100/A800 或 4090 级别)上表现正常。

总结:从loss日志角度看,本次lora微调是比较成功的。下一步就是进行人工推理测试或跑benchmark测试,如果没有别的问题,该模型可以用于直接部署。

训练结果信息说明

一般训练完成,会得到两个目录文件
qwen-lora-outputfinal_lora_adapter
qwen-lora-output 内容是Trainer 的"训练过程目录(checkpoint + log + best model)作用是用来训练

内容 作用
checkpoint-* 每个训练阶段快照
optimizer.pt Adam状态
scheduler.pt 学习率状态
trainer_state.json 训练进度
logs loss记录
sh 复制代码
 tree -L 2
.
├── checkpoint-34
│   ├── adapter_config.json
│   ├── adapter_model.safetensors
│   ├── chat_template.jinja
│   ├── optimizer.pt
│   ├── README.md
│   ├── rng_state.pth
│   ├── scheduler.pt
│   ├── tokenizer_config.json
│   ├── tokenizer.json
│   ├── trainer_state.json
│   └── training_args.bin
├── checkpoint-51
│   ├── adapter_config.json
│   ├── adapter_model.safetensors
│   ├── chat_template.jinja
│   ├── optimizer.pt
│   ├── README.md
│   ├── rng_state.pth
│   ├── scheduler.pt
│   ├── tokenizer_config.json
│   ├── tokenizer.json
│   ├── trainer_state.json
│   └── training_args.bin
└── README.md

final-lora-adapter 这个才是训练完成的权重内容

文件 作用
adapter_model.safetensors LoRA权重(核心)
adapter_config.json LoRA结构配置
sh 复制代码
 tree -L 1
.
├── adapter_config.json
├── adapter_model.safetensors
├── chat_template.jinja
├── README.md
├── tokenizer_config.json
├── tokenizer.json
└── training_args.bin

部署需要加载基座模型+peft模型

python 复制代码
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel

base_model = AutoModelForCausalLM.from_pretrained(
    "Qwen/Qwen2.5-7B-Instruct",
    device_map="auto",
    torch_dtype="bfloat16"
)

model = PeftModel.from_pretrained(
    base_model,
    "./final_lora_adapter"
)

或者采用下面的方式:

  • LoRA + base(测试推荐)
    model = PeftModel.from_pretrained(base_model, adapter)

省显存,随时替换adapter

  • merge 成完整模型(上线用)
    model = model.merge_and_unload()
    model.save_pretrained("./merged_model")
    推理速度块,不依赖PEFT
  • qwen-lora-output中训练的也不是不能用,只是效果不确定,不一定是best model,而且带训练状态。

checkpoint = "训练到某一刻的完整状态存档"

但是不同阶段模型能力不同

checkpoint 特点
early 只学格式
mid 学会任务
late 开始记忆细节

用错 checkpoint 会导致:

  • 回复风格不一致、指令能力退化、输出不稳定
  • optimizer state(Adam动量)等信息无用还增加加载复杂度
  • scheduler state(学习率状态)lr信息无意义,状态污染理解(虽然不影响forward,但混乱)
  • 正确用法,基于checkpoint resume训练
    trainer.train(resume_from_checkpoint="checkpoint-30")

5、效果对比

简单compare脚本

python 复制代码
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
from peft import PeftModel

BASE_MODEL_NAME = "Qwen/Qwen2.5-7B-Instruct"
LORA_ADAPTER_PATH = "./final_lora_adapter"


def build_prompt(tokenizer, user_query):
    messages = [
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": user_query},
    ]
    return tokenizer.apply_chat_template(
        messages,
        tokenize=False,
        add_generation_prompt=True,
    )


def generate_answer(tokenizer, model, user_query):
    text = build_prompt(tokenizer, user_query)
    inputs = tokenizer(text, return_tensors="pt").to(model.device)

    with torch.no_grad():
        outputs = model.generate(
            **inputs,
            max_new_tokens=128,
            do_sample=False,
            pad_token_id=tokenizer.pad_token_id,
            eos_token_id=tokenizer.eos_token_id,
        )

    generated_ids = outputs[0][inputs["input_ids"].shape[1]:]
    return tokenizer.decode(generated_ids, skip_special_tokens=True)


def load_base_model():
    bnb_config = BitsAndBytesConfig(
        load_in_4bit=True,
        bnb_4bit_quant_type="nf4",
        bnb_4bit_compute_dtype=torch.bfloat16,
        bnb_4bit_use_double_quant=True,
    )

    model = AutoModelForCausalLM.from_pretrained(
        BASE_MODEL_NAME,
        quantization_config=bnb_config,
        device_map="auto",
        torch_dtype=torch.bfloat16,
        trust_remote_code=True,
    )
    model.eval()
    return model


def load_lora_model():
    bnb_config = BitsAndBytesConfig(
        load_in_4bit=True,
        bnb_4bit_quant_type="nf4",
        bnb_4bit_compute_dtype=torch.bfloat16,
        bnb_4bit_use_double_quant=True,
    )

    base_model_for_lora = AutoModelForCausalLM.from_pretrained(
        BASE_MODEL_NAME,
        quantization_config=bnb_config,
        device_map="auto",
        torch_dtype=torch.bfloat16,
        trust_remote_code=True,
    )
    model = PeftModel.from_pretrained(base_model_for_lora, LORA_ADAPTER_PATH)
    model.eval()
    return model

def main():
    tokenizer = AutoTokenizer.from_pretrained(
        BASE_MODEL_NAME,
        trust_remote_code=True,
        use_fast=False,
    )
    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token

    print(">>> loading pure base model")
    base_model = load_base_model()

    print(">>> loading base + lora model")
    lora_model = load_lora_model()

    print("\n>>> 进入命令行交互模式")
    print('>>> 输入问题后回车即可查看结果,输入 "q:" 退出\n')

    while True:
        user_query = input("请输入问题> ").strip()

        if user_query == "q:":
            print(">>> 已退出")
            break

        if not user_query:
            print(">>> 输入不能为空,请重新输入\n")
            continue

        base_answer = generate_answer(tokenizer, base_model, user_query)
        lora_answer = generate_answer(tokenizer, lora_model, user_query)

        print("\n===== 问题 =====")
        print(user_query)

        print("\n===== Base Model 回答 =====")
        print(base_answer)

        print("\n===== LoRA Model 回答 =====")
        print(lora_answer)
        print("\n" + "=" * 60 + "\n")

main()

lora训练效果明显,lora后模型回答明显不同于base model

复制代码
 python compare_base_vs_lora.py
>>> loading pure base model
Loading weights: 100%|██████████████████████████████████████████████████████| 339/339 [00:01<00:00, 195.84it/s]
>>> loading base + lora model
Loading weights: 100%|██████████████████████████████████████████████████████| 339/339 [00:01<00:00, 208.81it/s]

>>> 进入命令行交互模式
>>> 输入问题后回车即可查看结果,输入 "q:" 退出

请输入问题> 你好,qlora是什么
The following generation flags are not valid and may be ignored: ['temperature', 'top_p', 'top_k']. Set `TRANSFORMERS_VERBOSITY=info` for more details.
/home/yqx/miniconda3/envs/finetune_env/lib/python3.10/site-packages/bitsandbytes/backends/cuda/ops.py:468: FutureWarning: _check_is_size will be removed in a future PyTorch release along with guard_size_oblivious.     Use _check(i >= 0) instead.
  torch._check_is_size(blocksize)

===== 问题 =====
你好,qlora是什么

===== Base Model 回答 =====
你好!QLoRA(Quantization LoRA)是一种用于量化和微调大型语言模型的方法。它结合了两个技术:低秩近似(Low-Rank Approximation, LoRA)和量化(Quantization)。下面是对这两个技术的简要介绍:

1. 低秩近似(LoRA):
   - LoRA 是一种轻量级的模型微调方法。
   - 它通过在特定层中引入低秩矩阵来实现参数共享,从而减少模型大小和计算成本。
   - 主要应用于自注意力机制(Self-Attention)层。
===== LoRA Model 回答 =====
QLoRA是量化+LoRA的微调方法,通过4bit量化降低显存并冻结基座参数,仅训练adapter层。

============================================================

请输入问题> 介绍下sft

===== 问题 =====
介绍下sft

===== Base Model 回答 =====
您可能指的是"SFT"这个缩写,但没有提供足够的上下文信息。SFT可以代表很多不同的概念或领域中的术语,比如软件功能测 试(Software Functionality Testing)、服务发现技术(Service Discovery Technology)、安全转移(Secure File Transfer)等。能否请您提供更多具体的信息以便我能够更准确地为您介绍?

===== LoRA Model 回答 =====
sft是监督微调,使用标注好的数据训练模型。

============================================================

从上面内容可以看出对LLM通过Lora训练,即使是200左右的数据量,也可以对模型的回答风格产生明显影响。

6、踩坑与经验总结

下面是训练过程中遇到的一些报错核心内容,简单整理下,避免二次掉坑。

启动训练报错

accelerate launch --config-file accelerate_qwen2.5-7b.yaml lora_train.py

Loading weights: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 339/339 [00:01<00:00, 183.05it/s]

Traceback (most recent call last):

trainer = SFTTrainer(
TypeError: SFTTrainer.init() got an unexpected keyword argument 'tokenizer'

subprocess.CalledProcessError: Command '['/home/yqx/miniconda3/envs/finetune_env/bin/python3.10', 'lora_train.py']' returned non-zero exit status 1.

原因是 trl包版本和训练代码写法不匹配。TRL 1.0.0 版本中,SFTTrainer 的构造参数已经变了,不再接受 tokenizer=,而是通常要传:processing_class=tokenizer或者直接让 SFTConfig / 数据处理逻辑接管。
accelerate launch --config-file accelerate_qwen2.5-7b.yaml lora_train.py

简单警告:torch_dtype is deprecated! Use dtype instead!

不影响训练效果,但是再训练进度条会不断提示,扰乱进度条,很烦。
启动训练报错

accelerate launch --config-file accelerate_qwen2.5-7b.yaml lora_train.py

Loading weights: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 339/339 [00:01<00:00, 184.68it/s]

...

from deepspeed import DeepSpeedEngine

File "/home/yqx/miniconda3/envs/finetune_env/lib/python3.10/site-packages/deepspeed/init .py", line 25, in

from . import ops

File "/home/yqx/miniconda3/envs/finetune_env/lib/python3.10/site-packages/deepspeed/ops/init .py", line 15, in

File "/home/yqx/miniconda3/envs/finetune_env/lib/python3.10/site-packages/deepspeed/git_version_info.py", line 29, in

...

File "/home/yqx/miniconda3/envs/finetune_env/lib/python3.10/site-packages/deepspeed/ops/op_builder/builder.py", line 51, in installed_cuda_version

raise MissingCUDAException("CUDA_HOME does not exist, unable to compile CUDA op(s)")

deepspeed.ops.op_builder.builder.MissingCUDAException: CUDA_HOME does not exist, unable to compile CUDA op(s)

Traceback (most recent call last):

File "/home/yqx/miniconda3/envs/finetune_env/bin/accelerate", line 6, in

sys.exit(main())

File "/home/yqx/miniconda3/envs/finetune_env/lib/python3.10/site-packages/accelerate/commands/accelerate_cli.py", line 50, in main

args.func(args)

File "/home/yqx/miniconda3/envs/finetune_env/lib/python3.10/site-packages/accelerate/commands/launch.py", line 1405, in launch_command

simple_launcher(args)

File "/home/yqx/miniconda3/envs/finetune_env/lib/python3.10/site-packages/accelerate/commands/launch.py", line 993, in simple_launcher

raise subprocess.CalledProcessError(returncode=process.returncode, cmd=cmd)

subprocess.CalledProcessError: Command '['/home/yqx/miniconda3/envs/finetune_env/bin/python3.10', 'lora_train.py']' returned non-zero exit status 1.

这个报错比较奇怪,前面的traceback中有一堆的deepspeed包相关的,最后又有CUDA_HOME的提示,实际这里和CUDA_HOME关系不大,是之前安装了deepspeed包,但是训练过程又不需要,导致他去导入的时候环境变量异常,解决办法是直接删除deepspeed包即可。单机单卡QLoRA训练不需要 deepspeed

新的报错:torch.OutOfMemoryError: CUDA out of memory. Tried to allocate 130.00 MiB. GPU 0 has a total capacity of 15.45 GiB of which 79.62 MiB is free. Including non-PyTorch memory, this process has 14.87 GiB memory in use. Of the allocated memory 14.53 GiB is allocated by PyTorch, and 22.86 MiB is reserved by PyTorch but unallocated. If reserved but unallocated memory is large try setting PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True to avoid fragmentation. See documentation for Memory Management (https://docs.pytorch.org/docs/stable/notes/cuda.html#optimizing-memory-usage-with-pytorch-cuda-alloc-conf)

训练已经正常run起来了,但是单卡是 15.45GB可用显存,现在训练 Qwen2.5-7B-Instruct + 4bit QLoRA,在这个显存档位上是能勉强训的,但必须把参数压得更保守。

模型本体虽然 4bit 了,但训练时真正吃显存的还有:

sh 复制代码
激活值
attention 中间结果
LoRA 训练梯度
optimizer state
sequence length 太长
batch size 太大
max_seq_length  # 影响非常大,attention 激活、中间状态、反向传播缓存、这些基本会随着 sequence length 明显增长
# 所以max_seq_length=1024 对 15GB 很危险,512 会好很多,384 往往就稳了
️# 显存和计算成本: Attention矩阵是 seq_len × seq_len,所以显存占用平方增长。
# Transformer的中间激活(hidden states)是 seq_len × hidden_size × batch,也线性增长。
# 反向传播需要缓存这些中间状态,因此显存占用直接随 max_seq_length 上升。
# 计算量:
# 自注意力计算复杂度 O(seq_len²),seq_len 翻倍,计算量几乎翻倍。训练速度显著下降。
️# 对训练效果的影响
# 长上下文能力
# max_seq_length 决定模型每次能看到的最大上下文长度。
# 如果设置太短,模型可能学不到跨长距离依赖。
# 对于指令型模型或长文本生成,过短可能导致效果下降。
# 梯度估计和收敛
# 短序列训练时,梯度会更多地基于局部信息,可能导致模型更"短视"。
# 长序列训练可以让模型学到更全局的模式,但显存消耗和训练不稳定性上升。
# Padding和batch效率
# 设置过长,实际样本大部分都很短,会导致很多padding,浪费显存。
# 设置合理长度(384-512)通常能兼顾效果和效率。
# 实战经验
# seq_len	特点
# 1024+	能捕捉长依赖,显存要求高,训练慢,可能不稳定
# 512	平衡选择,大部分短指令或QA足够
# 384	最稳,适合 LoRA + 中小 GPU,训练速度快,显存占用低

# 小技巧:
# 梯度累积:用小 batch + 梯度累积弥补显存不足。
# 动态padding / truncation:避免过多浪费。
# 混合精度(BF16/FP16):显存压缩一半。
# 逐步提升 max_seq_length:先384跑通,再试512/1024,观察显存占用和训练稳定性。

accelerate_qwen2.5-7b.yaml 里还有 dynamo_config / INDUCTOR,删掉,保守配置

新OOM报错:

...

torch/_inductor/compile_fx.py

torch/_dynamo/eval_frame.py

torch/_functorch/aot_autograd.py

/tmp/torchinductor...

bitsandbytes.dequantize_4bit

OOM核心原因非常明确了,不是单纯"16GB 不够",而是开启了 torch.compile / inductor / dynamo

不是普通的 QLoRA 训练路径,而是走了

sh 复制代码
torch.compile -> dynamo -> inductor -> AOT autograd
这条链路对:
bitsandbytes 4bit
QLoRA
PEFT
Qwen 7B
经常会额外吃显存,并且还容易首步超慢、莫名 OOM。
并且不要在代码里写任何:

torch.compile(...)
model = torch.compile(model)
torch._dynamo
compile 相关 trainer 参数
相关推荐
帐篷Li2 小时前
教育部:加快普及中小学生人工智能教育政策汇总
人工智能
网络工程小王2 小时前
【大模型(LLM)的业务开发】学习笔记
人工智能·算法·机器学习
SLAM必须dunk2 小时前
四足强化入门3---Robot Lab重点机器人配置,训练和调参
人工智能·深度学习·机器学习·机器人
AI医影跨模态组学2 小时前
ESMO Open 中国医学科学院肿瘤医院:整合影像组学、病理组学和活检适应性免疫评分预测局部晚期直肠癌远处转移
人工智能·深度学习·机器学习·论文·医学·医学影像
Ztopcloud极拓云视角2 小时前
GPT-6 & DeepSeek V4 双雄临近:企业多模型路由网关实战指南
人工智能·gpt·deepseek·gpt-6
汤姆yu2 小时前
GPT-6核心能力解析及与现有主流大模型对比
gpt·大模型·gpt6
hughnz2 小时前
AI和自动化让油田钻工慢慢消失
大数据·人工智能
jay神3 小时前
大米杂质检测数据集(YOLO格式)
人工智能·深度学习·yolo·目标检测·毕业设计
GIS数据转换器3 小时前
延凡低成本低空无人机AI巡检方案
大数据·人工智能·信息可视化·数据挖掘·无人机