四、项目A · 案例二:QLoRA 模型微调

案例目标:① 写 inference.py 证明基础模型能加载能推理;② 写 qlora.py 用 QLoRA 对 QWEN3-7B 做参数高效微调;③ 合并权重保存完整模型到 checkpoint-best/

示意图
图 4-1 QLoRA = 4-bit 量化底座(QLoRA 的 Q)+ LoRA 旁路(r=8 低秩适配)

4.1 步骤2-1:基础模型加载验证 —— inference.py

这个脚本用于验证基础模型能否正常加载与推理。要求:输入"你是谁?"能输出合理自我介绍。写完先自己跑一遍确认无报错。

# /home/user/workspace/model_b/inference.py —— 基础模型加载与推理验证
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

MODEL_DIR = "/home/user/workspace/QwenPretrain"   # 基础模型路径

tok = AutoTokenizer.from_pretrained(MODEL_DIR)
# bf16 半精度加载,device_map="auto" 自动放到 GPU(显存不够自动分摊到 CPU)
model = AutoModelForCausalLM.from_pretrained(
    MODEL_DIR, torch_dtype=torch.bfloat16, device_map="auto")

question = "你是谁?"
prompt = f"### 指令:\n{question}\n### 回答:\n"       # 与微调模板保持一致
inputs = tok(prompt, return_tensors="pt").to(model.device)
outputs = model.generate(**inputs, max_new_tokens=256, do_sample=True, temperature=0.7)
# [输入长度:] 切片是为了不把 prompt 重复打印一遍
print(tok.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True))

4.2 步骤2-2:QLoRA 微调脚本 —— qlora.py

这是项目A技术含量最高的一段。核心四步:4-bit 量化加载 → 挂 LoRA → 组织训练数据 → Trainer 训练。下面这组是业界常用的超参数组合,理解每个参数的作用是关键。

# /home/user/workspace/model_b/qlora.py —— QLoRA 参数高效微调
import torch, json
from transformers import (AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig,
                          TrainingArguments, Trainer, DataCollatorForLanguageModeling)
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from torch.utils.data import Dataset

MODEL_DIR = "/home/user/workspace/QwenPretrain"
DATA_PATH = "/home/user/workspace/model_b/data/dataset.json"
OUT_DIR   = "/home/user/workspace/model_b/qlora_out"

# ---------- ① 用 bitsandbytes 把基础模型加载为 4-bit(关键点1) ----------
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,                        # 开启 4-bit 量化加载
    bnb_4bit_quant_type="nf4",                 # NF4 是 QLoRA 论文的默认格式,精度最好
    bnb_4bit_compute_dtype=torch.bfloat16,     # 存储用4bit、计算用bf16
    bnb_4bit_use_double_quant=True)            # 双重量化,再省一点显存

tok = AutoTokenizer.from_pretrained(MODEL_DIR)
tok.pad_token = tok.eos_token                     # Qwen 没有 pad token,用 eos 代替
model = AutoModelForCausalLM.from_pretrained(
    MODEL_DIR, quantization_config=bnb_config, device_map="auto")
model = prepare_model_for_kbit_training(model)    # kbit 训练前的标配处理(梯度检查点等)

# ---------- ② 配置 LoRA(关键点2:r=8, alpha=16, 四个 proj) ----------
lora_config = LoraConfig(
    r=8, lora_alpha=16, lora_dropout=0.05,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
    bias="none", task_type="CAUSAL_LM")
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()   # 打印可训练参数占比,一般 1% 左右,日志留着是好证据

# ---------- ③ 数据集:按统一的提示语模板拼接 ----------
def make_prompt(ex):
    """统一使用如下提示语模板(训练与推理保持一致),如 '### 指令:\n{instruction}\n### 回答:\n'"""
    return f"### 指令:\n{ex['instruction']}\n### 回答:\n{ex['output']}" + tok.eos_token

class SFTDataset(Dataset):
    def __init__(self, path):
        self.items = json.load(open(path, encoding="utf-8"))
    def __len__(self): return len(self.items)
    def __getitem__(self, idx):
        enc = tok(make_prompt(self.items[idx]), truncation=True,
                  max_length=1024, padding="max_length", return_tensors="pt")
        return {"input_ids": enc.input_ids[0], "attention_mask": enc.attention_mask[0],
                "labels": enc.input_ids[0].clone()}

# ---------- ④ 训练参数(关键点3:lr=2e-4, 3轮, bs=4, 累积4) ----------
args = TrainingArguments(
    output_dir=OUT_DIR,
    num_train_epochs=3,                       # 训练 3 轮
    learning_rate=2e-4,                       # LoRA 微调经典学习率
    per_device_train_batch_size=4,            # 每卡批大小
    gradient_accumulation_steps=4,            # 梯度累积4步,等效 batch=16,省显存
    logging_steps=10, save_strategy="epoch",  # 记录loss、每轮存一次
    bf16=True, gradient_checkpointing=True,  # 混合精度+梯度检查点,进一步省显存
    report_to="none")

trainer = Trainer(model=model, args=args, train_dataset=SFTDataset(DATA_PATH),
                  data_collator=DataCollatorForLanguageModeling(tok, mlm=False))
trainer.train()
model.save_pretrained(f"{OUT_DIR}/lora_adapter")   # 保存 LoRA 适配器(很小,几十MB)
tok.save_pretrained(f"{OUT_DIR}/lora_adapter")

4.3 步骤2-3:合并 LoRA 权重并保存完整模型

# merge_lora.py —— 把小适配器"焊"回基础模型,得到可直接 from_pretrained 的完整模型
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel

MODEL_DIR = "/home/user/workspace/QwenPretrain"
BEST_DIR  = "/home/user/workspace/model_b/checkpoint-best"

# 合并必须在"非量化"的原始精度下进行,否则精度损失大 → 用 CPU + bf16 加载
base = AutoModelForCausalLM.from_pretrained(MODEL_DIR, torch_dtype=torch.bfloat16,
                                            device_map="cpu")
model = PeftModel.from_pretrained(base, "/home/user/workspace/model_b/qlora_out/lora_adapter")
model = model.merge_and_unload()          # 关键:把 LoRA 的 B·A 乘进原权重

model.save_pretrained(BEST_DIR)           # 生成 config.json + model.safetensors
AutoTokenizer.from_pretrained(MODEL_DIR).save_pretrained(BEST_DIR)  # 补上 tokenizer 文件
# 检查:BEST_DIR 里必须有 config.json / model.safetensors / tokenizer.json 等
📚 本任务用到的库一览
一句话解释 + 关键 API
transformersHuggingFace 的模型库,一切的基石。AutoModelForCausalLM 加载文本生成模型、AutoTokenizer 加载分词器、TrainingArguments + Trainer 封装训练循环、BitsAndBytesConfig 控制量化加载方式、generate() 做推理。
peft参数高效微调库(Parameter-Efficient Fine-Tuning)。LoraConfig 定义旁路结构(r、alpha、target_modules),get_peft_model() 给模型挂上 LoRA,PeftModel.from_pretrained() 加载训练好的适配器,merge_and_unload() 把适配器合并进主权重。
bitsandbytes实现 4-bit/8-bit 量化的底层库。配合 BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4") 使用,是 QLoRA 里 "Q" 的来源。
accelerateHuggingFace 的分布式/设备管理后端,device_map="auto" 依赖它自动分配 GPU/CPU 显存。
trl(可选)Transformer 强化学习/微调工具箱,其 SFTTrainer 能自动处理模板拼接,比手写 Dataset 更省事,但手写版能更清晰地控制提示语模板格式。
← 上一页下一页 →

— AI训练师技术交流教程 · 仅供学习交流 —