Qwen3 单机多卡微调完整教程 - 算命助手实战

0 阅读29分钟

👋 教程说明

适合谁看?

  • ✅ 有基础Python编程经验
  • ✅ 会使用Linux命令行(终端)
  • ✅ 有一台带多张GPU的服务器
  • ❌ 不需要深度学习专家级别的知识

你将学到什么?

本教程将手把手教你如何用 单机多卡(4×GPU) 并行训练的方式,把通用的 Qwen3 大模型微调成专业的"算命助手"。

时间投入:

  • 环境配置: 30分钟 - 1小时
  • 数据准备: 30分钟
  • 训练时间: 3-8小时(自动运行)
  • 总时长: 约半天到一天

🚀 本教程专注于:单机多卡 (4×GPU) 并行训练

硬件配置: 4×15GB 显存(总共60GB)

核心特点:

  • 多卡并行: 使用 DDP/DeepSpeed 实现 4 卡并行训练
  • 训练加速: 相比单卡提速 3-4 倍
  • QLoRA + 4bit 量化: 每卡显存占用 ~12GB
  • batch_size = 1/卡: 4卡总batch = 4,有效batch = 32
  • gradient_checkpointing: 节省显存必备
  • max_seq_length = 1024: 适合15GB显存
  • 8bit 优化器: 进一步节省显存

单机多卡 vs 单机单卡:

  • 单机单卡启动: python train.py(仅用1个GPU)
  • 单机多卡启动: torchrun --nproc_per_node=4 train.py(用4个GPU)

本教程配置为单机多卡! 如需单卡训练,请使用 python 直接启动。


📚 术语速查表(遇到不懂的词就来这里查)

基础概念

微调 (Fine-tuning)

在预训练模型的基础上,用特定领域的数据继续训练,让模型学会特定任务。就像给一个已经会说话的AI老师,教它专门的算命知识。

单机多卡 (Multi-GPU on Single Node)

一台服务器里装了多张显卡(GPU),让它们同时工作来加速训练。本教程使用4张GPU并行训练,速度提升3-4倍。

显存 (VRAM)

GPU自带的内存,用来存放模型和数据。本教程配置为每张卡15GB,总共60GB。

DDP (DistributedDataParallel)

PyTorch自带的多卡并行训练方法,自动将数据分配到多张卡上并行处理,然后同步梯度。

DeepSpeed

微软开源的深度学习训练加速工具,提供ZeRO优化技术,比DDP更省显存。

训练相关

QLoRA (Quantized LoRA)

一种参数高效的微调方法,结合了量化和低秩适配。只训练少量参数(几百万)而非全部参数(70亿),显存占用减少80%+。

  • 比喻: 不装修整栋楼(全量微调),只改造几个房间(LoRA),还用便宜材料(Q=4bit量化)

batch_size

每次训练时同时处理多少条数据。本教程设置为每卡1条,4卡并行 = 总batch 4。

gradient_accumulation

梯度累积。累积多个小batch的梯度再更新参数,效果等于大batch但不占额外显存。本教程累积8步,有效batch = 4×8 = 32。

gradient_checkpointing

梯度检查点。牺牲20%训练时间来节省30-40%显存。15GB显存必须开启。

epoch

把所有训练数据过一遍叫1个epoch。本教程训练3个epochs = 把数据学3遍。

learning_rate

学习率。控制模型参数更新的步长。太大容易不稳定,太小训练太慢。本教程使用2e-4。


⚡ 快速开始(适合有经验的用户)

新手请跳过这部分,从第1章开始按步骤操作。

如果你已经配置好环境和数据,可以直接使用以下命令启动单机4卡训练:

# 方式1: 使用 torchrun(推荐,最简单)
torchrun --nproc_per_node=4 train_qwen3_qlora.py

# 方式2: 使用 accelerate(更灵活)
accelerate launch --multi_gpu --num_processes=4 train_qwen3_qlora.py

# 方式3: 使用 DeepSpeed(最省显存,高级)
deepspeed --num_gpus=4 train_qwen3_qlora.py --deepspeed ds_config.json

🚨 重要: 必须使用上述多卡启动命令(而不是 python train.py),否则只会使用单卡!

目录

  1. 显存计算与模型选择
  2. 环境配置
  3. 数据集准备
  4. 单机多卡微调代码
  5. 训练启动与监控
  6. 常见问题解决

1. 显存计算与模型选择

1.1 你的硬件配置

  • GPU数量: 4块 GPU
  • 单卡显存: 15GB(60GB ÷ 4)
  • 总显存: 60GB
  • 适用场景: 单卡15GB显存,需要特别优化才能微调7B模型

1.2 显存消耗计算公式

微调时的显存消耗主要包括:

总显存 = 模型参数 + 梯度 + 优化器状态 + 激活值 + 其他开销

详细计算:

以 Qwen3-7B 为例(推荐)

项目计算方式FP16/BF16INT8INT4
模型参数7B × 2 bytes14 GB7 GB3.5 GB
梯度7B × 2 bytes14 GB14 GB14 GB
优化器状态 (AdamW)7B × 8 bytes28 GB28 GB28 GB
激活值 (batch=4)估算~8 GB~6 GB~4 GB
其他开销估算~4 GB~3 GB~2 GB
单卡总计68 GB58 GB51.5 GB

1.3 模型选择建议

模型参数量QLoRA显存需求是否适合你
Qwen3-1.8B1.8B~8GB/卡最安全选择
Qwen3-7B7B~12GB/卡(4卡并行)推荐(单机多卡配置)
Qwen3-14B14B~20GB/卡❌ 显存不足

单机多卡配置结论:

  • 首选: Qwen3-7B + QLoRA + 4卡并行(batch_size=1/卡,总batch=4)
  • 保险: Qwen3-1.8B + QLoRA + 4卡并行(显存充裕,可增加batch_size=2/卡)
  • 关键: 使用 DDP (DistributedDataParallel) 或 DeepSpeed 实现多卡并行训练

1.4 QLoRA 原理(小白版)

想象一下:

  • 全量微调:把整栋房子(70亿参数)全部重新装修 → 太贵
  • LoRA:只装修几个房间(几百万参数)→ 省钱
  • QLoRA:用更便宜的材料(INT4)装修几个房间 → 超省钱

QLoRA 能节省多少显存?

  • 全量微调:需要 ~68GB/卡(你的15GB显存装不下)
  • QLoRA:需要 ~12GB/卡(节省 80%+,刚好适合你的15GB)

单机多卡(4×15GB)优化策略:

  1. 使用 QLoRA + 4bit 量化
  2. 每卡 batch_size 设置为 1(4卡并行 = 总batch 4)
  3. 开启 gradient_checkpointing(用时间换空间)
  4. 最大序列长度限制在 1024
  5. 使用 DeepSpeed ZeRO-2 将优化器状态分散到多卡
  6. 使用 DDP 或 torchrun 启动多卡训练

2. 环境配置

2.1 系统要求

硬件要求:

  • GPU:4张GPU,每张显存15GB以上(如RTX 4090、A100等)
  • CPU:16核以上
  • 内存:64GB以上
  • 硬盘空间:至少 100GB(模型约14GB + 数据 + 输出)

软件要求:

  • 操作系统:Ubuntu 20.04/22.04 或 Windows 11 + WSL2
  • Python:3.10+
  • CUDA:12.1+
  • 驱动:NVIDIA Driver 525+

检查你的硬件:

# 1. 检查GPU(应该看到4张卡)
nvidia-smi

# 2. 检查CUDA版本
nvcc --version

# 3. 检查Python版本
python --version

2.2 创建虚拟环境

为什么需要虚拟环境?

  • 隔离依赖,避免版本冲突
  • 方便管理和删除
  • 推荐使用conda
# 如果没有conda,先安装Miniconda
# wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
# bash Miniconda3-latest-Linux-x86_64.sh

# 创建 conda 环境
conda create -n qwen3_finetune python=3.10 -y

# 激活环境(每次训练前都要运行)
conda activate qwen3_finetune

# 确认Python版本
python --version  # 应显示 Python 3.10.x

2.3 安装核心依赖

安装顺序很重要! 按以下顺序安装,避免版本冲突。

第1步: 安装PyTorch(深度学习框架)

# 根据你的CUDA版本选择
# CUDA 12.1版本(推荐):
pip install torch==2.2.0 torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121

# 验证安装
python -c "import torch; print('PyTorch:', torch.__version__); print('CUDA可用:', torch.cuda.is_available())"
# 期望输出: PyTorch: 2.2.0, CUDA可用: True

如果CUDA不可用,检查:

  • NVIDIA驱动是否正确安装
  • CUDA toolkit是否安装
  • PyTorch版本是否匹配CUDA版本

第2步: 安装训练相关库

# 一次性安装所有依赖(推荐)
pip install transformers==4.38.0 \
            accelerate==0.27.0 \
            peft==0.9.0 \
            bitsandbytes==0.42.0 \
            datasets==2.17.0 \
            trl==0.7.10 \
            sentencepiece \
            tensorboard

# 可选: DeepSpeed(高级优化,新手可跳过)
pip install deepspeed==0.13.1

各个库的作用:

  • transformers: Hugging Face的模型库,加载Qwen3模型
  • accelerate: 简化多GPU训练配置
  • peft: LoRA/QLoRA实现
  • bitsandbytes: 4bit/8bit量化,节省显存
  • datasets: 数据加载和处理
  • trl: Transformer强化学习训练工具
  • sentencepiece: 分词器
  • tensorboard: 训练过程可视化

2.4 验证安装

创建测试脚本 test_env.py

"""
环境验证脚本
检查所有依赖是否正确安装,GPU是否可用
"""

import torch
import transformers
from peft import LoraConfig
import bitsandbytes as bnb

print("=" * 60)
print("环境验证")
print("=" * 60)

print(f"✅ PyTorch版本: {torch.__version__}")
print(f"✅ Transformers版本: {transformers.__version__}")
print(f"✅ CUDA可用: {torch.cuda.is_available()}")
print(f"✅ GPU数量: {torch.cuda.device_count()}")
print()

# 检查GPU信息
gpu_count = torch.cuda.device_count()
if gpu_count == 4:
    print("🎉 完美! 检测到4张GPU")
    for i in range(gpu_count):
        gpu_name = torch.cuda.get_device_name(i)
        gpu_mem = torch.cuda.get_device_properties(i).total_memory / 1024**3
        print(f"   GPU {i}: {gpu_name} ({gpu_mem:.1f}GB)")
elif gpu_count > 0:
    print(f"⚠️  警告: 检测到{gpu_count}张GPU,本教程需要4张")
    print("   可以继续,但需要修改启动命令中的--nproc_per_node参数")
    for i in range(gpu_count):
        gpu_name = torch.cuda.get_device_name(i)
        gpu_mem = torch.cuda.get_device_properties(i).total_memory / 1024**3
        print(f"   GPU {i}: {gpu_name} ({gpu_mem:.1f}GB)")
else:
    print("❌ 错误: 没有检测到GPU!")
    print("   请检查NVIDIA驱动和CUDA安装")

print()
print("=" * 60)

运行测试:

python test_env.py

期望输出示例:

============================================================
环境验证
============================================================
 PyTorch版本: 2.2.0
 Transformers版本: 4.38.0
 CUDA可用: True
 GPU数量: 4

🎉 完美! 检测到4张GPU
   GPU 0: NVIDIA GeForce RTX 4090 (24.0GB)
   GPU 1: NVIDIA GeForce RTX 4090 (24.0GB)
   GPU 2: NVIDIA GeForce RTX 4090 (24.0GB)
   GPU 3: NVIDIA GeForce RTX 4090 (24.0GB)

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

✅ 如果看到类似输出,说明环境配置成功! 可以继续下一步。

❌ 如果出现错误:

  • GPU数量不是4: 检查 nvidia-smi 是否显示4张卡
  • CUDA不可用: 重新安装PyTorch,确保版本匹配
  • Import错误: 重新安装对应的库

3. 数据集准备

3.1 数据格式说明

为什么需要准备数据? 微调就像教AI说话,你需要给它"例句"学习。数据质量直接决定了微调效果。

使用什么格式? 推荐使用 Alpaca 格式(JSON格式,简单易懂):

[
    {
        "instruction": "请帮我算一下今年的运势",
        "input": "我是1990年出生的,属马",
        "output": "根据您的生肖属马,1990年出生属庚午年。今年2026年丙午年,您正值本命年..."
    },
    {
        "instruction": "我想知道我的事业运",
        "input": "我在做销售工作",
        "output": "从您的描述来看,销售工作需要良好的人际关系..."
    }
]

字段说明:

  • instruction: 用户的问题主题(必需)
  • input: 用户的具体输入信息(可以为空)
  • output: 模型应该给出的回答(必需)

数据量建议:

  • 最少: 100条(能跑通,效果一般)
  • 推荐: 500-1000条(效果较好)
  • 理想: 5000+条(效果很好)
  • 本教程提供6条示例,仅用于演示流程

3.2 创建示例数据集

创建文件 data/fortune_telling_train.json

[
    {
        "instruction": "请帮我算一下今年的运势",
        "input": "我是1990年出生的,属马",
        "output": "根据您的生肖属马,1990年出生属庚午年。今年2026年丙午年,您正值本命年,需要特别注意。整体运势来看:\n\n**事业运**: 本命年事业会有一些波动,建议保守稳健,不宜大幅变动\n**财运**: 正财尚可,偏财需谨慎,避免高风险投资\n**感情**: 已婚者注意沟通,单身者桃花运一般\n**健康**: 注意肠胃和心血管健康,定期体检\n\n化解建议: 可佩戴红绳,多行善事,保持积极心态。"
    },
    {
        "instruction": "我想知道我的事业运",
        "input": "我在做销售工作,最近业绩不太好",
        "output": "从事销售工作需要良好的人际关系和沟通能力。根据您目前的情况分析:\n\n**近期状况**: 可能处于低谷期,这是正常的周期性波动\n**改善建议**:\n1. 调整心态,保持积极乐观\n2. 加强客户关系维护,重视老客户\n3. 学习新的销售技巧和产品知识\n4. 注意与同事合作,团队力量很重要\n\n**吉日选择**: 建议在每月初三、初八、十三、十八、廿三、廿八拜访重要客户\n**幸运色**: 红色、橙色能提升您的气场"
    },
    {
        "instruction": "帮我看看感情运势",
        "input": "我是单身,想知道什么时候能遇到对的人",
        "output": "关于您的感情运势,我为您分析如下:\n\n**桃花运**: 今年上半年桃花运较旺,特别是春季(3-5月)和秋季(9-11月)\n**理想对象**: 可能会在工作场合或朋友聚会中遇到,注意身边的机会\n**建议行动**:\n1. 多参加社交活动,扩大社交圈\n2. 保持良好的外在形象和内在修养\n3. 不要过于挑剔,给彼此了解的机会\n4. 可以请朋友介绍,熟人介绍成功率更高\n\n**注意事项**: 避免网恋和闪婚,需要时间真正了解对方\n**幸运方位**: 东南方向,可以多往这个方向活动"
    },
    {
        "instruction": "我想问问财运如何",
        "input": "最近想投资股票",
        "output": "关于财运和投资,我给您以下建议:\n\n**整体财运**: 今年正财运稳定,偏财运需谨慎\n**投资建议**: \n- ⚠️ **风险提示**: 今年不是大规模投资的好时机\n- 如果一定要投资,建议小额分散投资\n- 避免借钱投资或加杠杆操作\n- 重点关注稳健型投资,如蓝筹股、基金定投\n\n**财运提升方法**:\n1. 专注本职工作,提升专业能力\n2. 开源节流,建立储蓄习惯\n3. 可在家中财位摆放绿植或鱼缸\n4. 避免冲动消费,理性规划财务\n\n**吉日**: 农历每月初一、十五适合重要财务决策"
    },
    {
        "instruction": "帮我看看健康运",
        "input": "最近总感觉累,睡眠也不好",
        "output": "您的健康状况需要关注,从您的描述来看:\n\n**主要问题**: 疲劳和睡眠质量差可能与压力和作息有关\n**健康建议**:\n1. **作息调整**: 每晚11点前入睡,保证7-8小时睡眠\n2. **运动**: 每周至少3次有氧运动,如慢跑、游泳\n3. **饮食**: 多吃新鲜蔬果,减少油腻和辛辣食物\n4. **心理**: 学会释放压力,可以尝试冥想或瑜伽\n\n**重点关注**: \n- 肝脏和脾胃健康\n- 定期体检,特别是血压血糖\n- 避免熬夜和过度劳累\n\n**养生建议**:\n- 早上喝温水,晚上泡脚\n- 可以喝一些菊花茶、枸杞茶养肝明目\n- 保持良好心态,心情愉悦最重要"
    },
    {
        "instruction": "我想知道考试运",
        "input": "下个月有个重要考试",
        "output": "关于您的考试运势,我为您分析:\n\n**考试运势**: 整体运势尚可,但需要努力准备\n**准备建议**:\n1. **时间规划**: 制定详细复习计划,每天坚持\n2. **重点突破**: 抓住重点和难点,不要面面俱到\n3. **模拟练习**: 多做真题和模拟题,熟悉题型\n4. **劳逸结合**: 注意休息,保持良好状态\n\n**考前准备**:\n- 考前一周减少熬夜,保证睡眠\n- 考试当天穿红色或黄色衣物\n- 提前踩点,避免迟到慌张\n- 保持平常心,相信自己的努力\n\n**吉时**: 上午9-11点(巳时)精神状态最佳\n**助运物**: 可随身携带红绳或文昌符\n\n记住: 运势只是辅助,真正的成功来自于踏实的努力!"
    }
]

3.3 数据集划分

为什么要划分数据?

  • 训练集(90%): 用来训练模型
  • 验证集(10%): 用来评估训练效果,防止过拟合

创建数据处理脚本 prepare_data.py

import json
import random

def split_dataset(input_file, train_ratio=0.9):
    """将数据集划分为训练集和验证集"""

    # 读取数据
    with open(input_file, 'r', encoding='utf-8') as f:
        data = json.load(f)

    # 打乱数据
    random.seed(42)
    random.shuffle(data)

    # 划分数据
    split_idx = int(len(data) * train_ratio)
    train_data = data[:split_idx]
    val_data = data[split_idx:]

    # 保存
    with open('data/train.json', 'w', encoding='utf-8') as f:
        json.dump(train_data, f, ensure_ascii=False, indent=2)

    with open('data/val.json', 'w', encoding='utf-8') as f:
        json.dump(val_data, f, ensure_ascii=False, indent=2)

    print(f"数据划分完成:")
    print(f"  训练集: {len(train_data)} 条")
    print(f"  验证集: {len(val_data)} 条")

if __name__ == "__main__":
    split_dataset('data/fortune_telling_train.json')

运行脚本:

mkdir -p data
# 先创建上面的 fortune_telling_train.json
python prepare_data.py

3.4 数据质量要求

为了获得好的微调效果,建议:

  • 数量: 至少 500-1000 条高质量对话
  • 多样性: 覆盖不同算命场景(事业、感情、财运、健康等)
  • 长度: 每条回复 100-500 字为佳
  • 风格: 保持统一的回复风格和专业度

4. 单机多卡微调代码

4.1 代码结构说明

训练代码主要包含以下几个部分:

  1. 参数配置 (ScriptArguments): 设置模型、数据、训练参数
  2. 模型加载 (load_model_and_tokenizer): 加载Qwen3模型并应用QLoRA
  3. 数据处理 (preprocess_dataset): 将JSON数据转换为模型输入格式
  4. 训练器设置 (TrainingArguments + Trainer): 配置训练参数并启动训练

关键配置说明(针对单机多卡):

# 单机多卡的关键配置
per_device_train_batch_size = 1   # 每张卡batch=1(15GB显存限制)
                                   # 4卡并行 = 总batch 4

gradient_accumulation_steps = 8    # 累积8步更新一次
                                   # 有效batch = 1×4卡×8步 = 32

ddp_find_unused_parameters = False # DDP多卡训练必须设置

device_map = "auto"                # 自动分配模型到多卡
                                   # DDP模式下会自动处理

4.2 完整训练脚本

创建 train_qwen3_qlora.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Qwen3 单机多卡 QLoRA 微调脚本
适用于 4×RTX 4090 (24GB × 4)
"""

import os
import json
import torch
from dataclasses import dataclass, field
from typing import Optional
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    BitsAndBytesConfig,
    TrainingArguments,
    Trainer,
    DataCollatorForSeq2Seq,
)
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from datasets import load_dataset
import logging

# 设置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


@dataclass
class ScriptArguments:
    """训练参数配置"""

    # 模型参数
    model_name: str = field(
        default="Qwen/Qwen2.5-7B-Instruct",  # 或 Qwen/Qwen3-7B
        metadata={"help": "预训练模型名称或路径"}
    )

    # 数据参数
    train_file: str = field(
        default="data/train.json",
        metadata={"help": "训练数据文件"}
    )
    val_file: str = field(
        default="data/val.json",
        metadata={"help": "验证数据文件"}
    )

    # LoRA 参数
    lora_r: int = field(default=64, metadata={"help": "LoRA 秩"})
    lora_alpha: int = field(default=16, metadata={"help": "LoRA alpha"})
    lora_dropout: float = field(default=0.05, metadata={"help": "LoRA dropout"})

    # 量化参数
    use_4bit: bool = field(default=True, metadata={"help": "使用 4bit 量化"})

    # 训练参数(单机多卡配置)
    output_dir: str = field(default="./output", metadata={"help": "输出目录"})
    num_train_epochs: int = field(default=3, metadata={"help": "训练轮数"})
    per_device_train_batch_size: int = field(default=1, metadata={"help": "每卡训练 batch size(单机多卡:1/卡 × 4卡 = 4)"})
    gradient_accumulation_steps: int = field(default=8, metadata={"help": "梯度累积步数(有效batch=4×8=32)"})
    learning_rate: float = field(default=2e-4, metadata={"help": "学习率"})
    max_seq_length: int = field(default=1024, metadata={"help": "最大序列长度(15GB显存建议1024)"})
    gradient_checkpointing: bool = field(default=True, metadata={"help": "梯度检查点(节省显存)"})

    # 其他
    seed: int = field(default=42, metadata={"help": "随机种子"})


def create_bnb_config(use_4bit: bool = True):
    """创建 BitsAndBytes 量化配置"""
    if use_4bit:
        return BitsAndBytesConfig(
            load_in_4bit=True,
            bnb_4bit_use_double_quant=True,  # 双重量化,进一步节省显存
            bnb_4bit_quant_type="nf4",  # 使用 NF4 量化
            bnb_4bit_compute_dtype=torch.bfloat16,  # 计算精度
        )
    else:
        return BitsAndBytesConfig(
            load_in_8bit=True,
        )


def load_model_and_tokenizer(script_args: ScriptArguments):
    """加载模型和分词器"""

    logger.info(f"加载模型: {script_args.model_name}")

    # BitsAndBytes 配置
    bnb_config = create_bnb_config(script_args.use_4bit)

    # 加载模型(多卡配置)
    model = AutoModelForCausalLM.from_pretrained(
        script_args.model_name,
        quantization_config=bnb_config,
        device_map="auto",  # 自动分配到多卡(DDP模式下会自动处理)
        trust_remote_code=True,
        torch_dtype=torch.bfloat16,
    )

    # 准备模型用于 k-bit 训练
    model = prepare_model_for_kbit_training(model)

    # 开启梯度检查点(节省显存,15GB显存必须开启)
    if script_args.gradient_checkpointing:
        model.gradient_checkpointing_enable()

    # 加载分词器
    tokenizer = AutoTokenizer.from_pretrained(
        script_args.model_name,
        trust_remote_code=True,
        padding_side="right",  # 重要:padding 放在右侧
    )

    # 设置 pad_token
    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token

    # LoRA 配置
    lora_config = LoraConfig(
        r=script_args.lora_r,
        lora_alpha=script_args.lora_alpha,
        target_modules=[
            "q_proj", "k_proj", "v_proj", "o_proj",  # attention
            "gate_proj", "up_proj", "down_proj",  # MLP
        ],
        lora_dropout=script_args.lora_dropout,
        bias="none",
        task_type="CAUSAL_LM",
    )

    # 应用 LoRA
    model = get_peft_model(model, lora_config)
    model.print_trainable_parameters()  # 打印可训练参数量

    return model, tokenizer


def format_instruction(sample):
    """格式化指令数据"""
    return f"""<|im_start|>system
你是一位专业的算命师傅,精通八字、生肖、风水等传统命理学。请根据用户的问题,提供专业、详细且富有洞察力的解答。<|im_end|>
<|im_start|>user
{sample['instruction']}
{sample['input']}<|im_end|>
<|im_start|>assistant
{sample['output']}<|im_end|>"""


def preprocess_dataset(dataset, tokenizer, max_seq_length):
    """预处理数据集"""

    def tokenize_function(examples):
        # 格式化文本
        texts = [format_instruction(ex) for ex in examples]

        # 分词
        model_inputs = tokenizer(
            texts,
            max_length=max_seq_length,
            truncation=True,
            padding=False,  # 不在这里 padding,使用 DataCollator
        )

        # labels 与 input_ids 相同(自回归语言模型)
        model_inputs["labels"] = model_inputs["input_ids"].copy()

        return model_inputs

    # 将单个样本转换为批处理格式
    def prepare_batch(examples):
        batch = {key: [ex[key] for ex in examples] for key in examples[0].keys()}
        return tokenize_function(batch)

    # 处理数据集
    processed_dataset = []
    for example in dataset:
        processed_dataset.append(example)

    # 使用 map 批处理
    tokenized = []
    batch_size = 1000
    for i in range(0, len(processed_dataset), batch_size):
        batch = processed_dataset[i:i+batch_size]
        result = prepare_batch(batch)
        for j in range(len(batch)):
            tokenized.append({
                'input_ids': result['input_ids'][j],
                'attention_mask': result['attention_mask'][j],
                'labels': result['labels'][j],
            })

    return tokenized


def main():
    """主函数"""

    # 参数配置
    script_args = ScriptArguments()

    # 设置随机种子
    torch.manual_seed(script_args.seed)

    # 加载模型和分词器
    model, tokenizer = load_model_and_tokenizer(script_args)

    # 加载数据集
    logger.info("加载数据集...")
    train_dataset = load_dataset('json', data_files=script_args.train_file, split='train')
    val_dataset = load_dataset('json', data_files=script_args.val_file, split='train')

    logger.info(f"训练集大小: {len(train_dataset)}")
    logger.info(f"验证集大小: {len(val_dataset)}")

    # 预处理数据
    logger.info("预处理数据...")
    train_dataset = preprocess_dataset(train_dataset, tokenizer, script_args.max_seq_length)
    val_dataset = preprocess_dataset(val_dataset, tokenizer, script_args.max_seq_length)

    # Data Collator
    data_collator = DataCollatorForSeq2Seq(
        tokenizer=tokenizer,
        padding=True,
        return_tensors="pt",
    )

    # 训练参数(单机多卡配置)
    training_args = TrainingArguments(
        output_dir=script_args.output_dir,
        num_train_epochs=script_args.num_train_epochs,
        per_device_train_batch_size=script_args.per_device_train_batch_size,  # 1/卡
        per_device_eval_batch_size=script_args.per_device_train_batch_size,
        gradient_accumulation_steps=script_args.gradient_accumulation_steps,  # 有效batch=1×4×8=32
        learning_rate=script_args.learning_rate,
        logging_steps=10,
        save_steps=100,
        eval_steps=100,
        save_total_limit=3,
        load_best_model_at_end=True,
        evaluation_strategy="steps",
        warmup_steps=100,
        bf16=True,  # 使用 bfloat16
        gradient_checkpointing=script_args.gradient_checkpointing,  # 梯度检查点
        ddp_find_unused_parameters=False,  # DDP 多卡训练必须设置
        dataloader_num_workers=2,  # 减少内存占用
        remove_unused_columns=False,
        report_to="tensorboard",
        logging_dir=f"{script_args.output_dir}/logs",
        optim="paged_adamw_8bit",  # 使用8bit优化器进一步节省显存
        # 多卡训练会自动启用 DDP
    )

    # 创建 Trainer
    trainer = Trainer(
        model=model,
        args=training_args,
        train_dataset=train_dataset,
        eval_dataset=val_dataset,
        data_collator=data_collator,
    )

    # 开始训练
    logger.info("开始训练...")
    trainer.train()

    # 保存模型
    logger.info("保存模型...")
    trainer.save_model(f"{script_args.output_dir}/final_model")
    tokenizer.save_pretrained(f"{script_args.output_dir}/final_model")

    logger.info("训练完成!")


if __name__ == "__main__":
    main()

4.2 配置文件

创建 config/training_config.json(可选):

{
  "model_name": "Qwen/Qwen2.5-7B-Instruct",
  "train_file": "data/train.json",
  "val_file": "data/val.json",
  "output_dir": "./output",
  "num_train_epochs": 3,
  "per_device_train_batch_size": 1,
  "gradient_accumulation_steps": 8,
  "learning_rate": 2e-4,
  "max_seq_length": 1024,
  "lora_r": 64,
  "lora_alpha": 16,
  "lora_dropout": 0.05,
  "use_4bit": true,
  "gradient_checkpointing": true,
  "seed": 42
}

重要说明(单机多卡配置):

  • per_device_train_batch_size: 1 - 每卡batch设为1,4卡并行 = 总batch 4
  • gradient_accumulation_steps: 8 - 累积8步,有效batch = 4×8 = 32
  • max_seq_length: 1024 - 序列长度限制在1024
  • gradient_checkpointing: true - 必须开启,节省30-40%显存
  • 使用 torchrunaccelerate 启动多卡训练

5. 训练启动与监控

5.1 单机多卡 vs 单机单卡

关键区别:

特性单机单卡单机多卡 (本教程)
启动方式python train.pytorchrun --nproc_per_node=4 train.py
并行方式无并行DDP (DistributedDataParallel)
batch_size1/卡 = 11/卡 × 4卡 = 4
训练速度1x3-4x (接近线性加速)
有效batch1×8=84×8=32
适用场景调试、小数据集生产训练、大数据集

为什么要用单机多卡?

  • 速度提升: 4卡并行训练速度提升3-4倍
  • batch size增大: 有效batch从8增加到32,训练更稳定
  • 资源利用: 充分利用所有GPU,提高硬件利用率

5.2 单机多卡启动命令

🚨 最重要的事: 必须使用以下多卡启动命令,否则只会用单卡!

方式一:使用 torchrun(推荐,最简单)

# 激活环境(每次训练前都要做)
conda activate qwen3_finetune

# 进入项目目录
cd /path/to/your/project

# 启动4卡训练
torchrun --nproc_per_node=4 train_qwen3_qlora.py

# 参数说明:
# --nproc_per_node=4  表示使用4个进程(对应4张GPU)
# 如果你有8张卡,改成 --nproc_per_node=8

指定特定GPU:

# 只使用GPU 0,1,2,3(默认就是这样)
CUDA_VISIBLE_DEVICES=0,1,2,3 torchrun --nproc_per_node=4 train_qwen3_qlora.py

# 如果想用GPU 4,5,6,7
CUDA_VISIBLE_DEVICES=4,5,6,7 torchrun --nproc_per_node=4 train_qwen3_qlora.py

启动成功的标志:

Setting OMP_NUM_THREADS environment variable for each process to be 1
[INFO] Setting ds_accelerator to cuda (auto detect)
加载模型: Qwen/Qwen2.5-7B-Instruct
...
trainable params: 41,943,040 || all params: 7,615,616,000 || trainable%: 0.55%

方式二:使用 accelerate(更灵活的多卡配置)

# 首先配置 accelerate(选择分布式训练选项)
accelerate config

# 配置示例:
# - 选择 "multi-GPU"
# - GPU数量: 4
# - 混合精度: bf16
# - 是否使用DeepSpeed: 否(或选择ZeRO-2)

# 然后启动多卡训练
accelerate launch --multi_gpu --num_processes=4 train_qwen3_qlora.py

方式三:使用 DeepSpeed(单机多卡高级优化)

DeepSpeed ZeRO可以进一步优化多卡训练的显存使用:

创建 ds_config.json

{
  "train_batch_size": "auto",
  "train_micro_batch_size_per_gpu": "auto",
  "gradient_accumulation_steps": "auto",
  "gradient_clipping": 1.0,
  "zero_optimization": {
    "stage": 2,  # ZeRO-2: 将优化器状态分散到4张卡
    "offload_optimizer": {
      "device": "cpu",  # 进一步将优化器卸载到CPU(可选)
      "pin_memory": true
    },
    "allgather_partitions": true,
    "allgather_bucket_size": 5e8,
    "reduce_scatter": true,
    "reduce_bucket_size": 5e8,
    "overlap_comm": true,  # 重叠通信和计算,提高多卡效率
    "contiguous_gradients": true
  },
  "fp16": {
    "enabled": false
  },
  "bf16": {
    "enabled": true
  },
  "zero_allow_untested_optimizer": true
}

启动命令(单机4卡):

# 使用DeepSpeed启动单机多卡训练
deepspeed --num_gpus=4 train_qwen3_qlora.py --deepspeed ds_config.json

# 或指定特定GPU
CUDA_VISIBLE_DEVICES=0,1,2,3 deepspeed --num_gpus=4 train_qwen3_qlora.py --deepspeed ds_config.json

DeepSpeed优势(单机多卡):

  • ZeRO-2: 将优化器状态均分到4张卡,每卡节省75%优化器显存
  • ZeRO-3: 进一步将模型参数也分散(适合更大模型)
  • 通信优化: 自动优化多卡间的梯度同步

5.3 训练监控

TensorBoard 监控

# 在另一个终端运行
tensorboard --logdir=./output/logs --port=6006

# 然后在浏览器访问: http://localhost:6006

实时显存监控(多卡)

# 在另一个终端运行,监控所有GPU
watch -n 1 nvidia-smi

# 预期看到4张卡都在工作,显存使用均衡
# GPU 0: 12GB / 15GB
# GPU 1: 12GB / 15GB
# GPU 2: 12GB / 15GB
# GPU 3: 12GB / 15GB

训练脚本监控

创建 monitor.py

import time
import subprocess

def monitor_training():
    """监控训练进度"""
    while True:
        # GPU 信息
        result = subprocess.run(
            ['nvidia-smi', '--query-gpu=index,memory.used,memory.total,utilization.gpu', '--format=csv,noheader,nounits'],
            capture_output=True,
            text=True
        )

        print("\n" + "="*60)
        print(f"时间: {time.strftime('%Y-%m-%d %H:%M:%S')}")
        print("="*60)

        for line in result.stdout.strip().split('\n'):
            gpu_id, mem_used, mem_total, util = line.split(', ')
            print(f"GPU {gpu_id}: {mem_used}MB / {mem_total}MB ({util}%)")

        time.sleep(5)

if __name__ == "__main__":
    try:
        monitor_training()
    except KeyboardInterrupt:
        print("\n监控结束")

5.4 预期训练时间(单机多卡 vs 单卡)

以 Qwen3-7B + QLoRA 为例(针对你的4×15GB配置):

数据量训练轮数单卡时间4卡并行时间加速比
500条3 epochs~12-16小时~3-4小时4x
1000条3 epochs~24-32小时~6-8小时4x
5000条3 epochs~120-160小时~30-40小时4x

单机多卡优势明显!

进一步提速建议:

  1. 使用 Qwen3-1.8B(快3-4倍): 4卡训练1000条数据仅需1.5-2小时
  2. 减少 max_seq_length 到 512(快30-40%)
  3. 使用更少的 lora_r(如32代替64,快20%)
  4. 确保使用 Flash Attention 2(提升10-20%)

6. 常见问题解决

遇到问题不要慌!大部分问题都有标准解决方案。

6.1 显存不足(OOM)

错误症状:

RuntimeError: CUDA out of memory. Tried to allocate XX GB (GPU 0; 15.00 GB total capacity...)

训练开始没多久就崩溃,或者启动时就报错。

15GB显存专用解决方案(按优先级排序):

# ✅ 方案 1: 确认已开启所有优化(最重要)
gradient_checkpointing: True  # 必须开启
use_4bit: True  # 必须开启
per_device_train_batch_size: 1  # 已经是最小

# ✅ 方案 2: 减小序列长度(最有效)
max_seq_length: 1024512  # 可节省50%显存

# ✅ 方案 3: 减小 LoRA 秩
lora_r: 6432  # 可节省20-30%显存
lora_r: 3216  # 极端情况

# ✅ 方案 4: 使用 8bit 优化器(代码中已配置)
optim: "paged_adamw_8bit"  # 确保启用

# ✅ 方案 5: 使用 DeepSpeed ZeRO-3
# 修改 ds_config.json 中 stage: 2 → 3
# 可将模型参数也分散到多卡

# ⚠️ 方案 6: 降级到更小的模型
# 如果以上都不行,使用 Qwen3-1.8B(只需6-8GB)

检查命令:

# 训练前检查显存使用
nvidia-smi

# 如果基础占用超过2GB,先清理:
python -c "import torch; torch.cuda.empty_cache()"

6.2 多卡不工作(重要!)

问题: 只有一张卡在工作,其他卡闲置

诊断方法:

# 训练时运行 nvidia-smi,检查是否所有GPU都在使用
watch -n 1 nvidia-smi

# 如果只有GPU 0在工作,说明多卡没有启动

解决方案(按优先级):

  1. 确认使用了多卡启动命令
# ❌ 错误:单卡启动
python train_qwen3_qlora.py

# ✅ 正确:多卡启动
torchrun --nproc_per_node=4 train_qwen3_qlora.py
  1. 检查 GPU 可见性
# 检查可见GPU
echo $CUDA_VISIBLE_DEVICES

# 如果未设置或错误,手动设置
export CUDA_VISIBLE_DEVICES=0,1,2,3
  1. 确认训练参数配置
# 在 TrainingArguments 中确认
training_args = TrainingArguments(
    ...
    ddp_find_unused_parameters=False,  # DDP必须设置
    ...
)
  1. 检查进程数量
# 训练启动后,检查进程数
ps aux | grep train_qwen3_qlora.py
# 应该看到4个Python进程(每个GPU一个)

6.3 训练很慢

优化方案:

# 1. 启用混合精度训练
bf16=True

# 2. 增加数据加载器线程
dataloader_num_workers=4

# 3. 启用梯度检查点(节省显存但会稍慢)
gradient_checkpointing=True

# 4. 使用 Flash Attention 2
pip install flash-attn --no-build-isolation

# 在模型加载时:
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    attn_implementation="flash_attention_2",
    ...
)

6.4 Loss 不下降

可能原因及解决:

# 1. 学习率过大或过小
learning_rate: 2e-4  # 尝试 1e-4 到 5e-4

# 2. 数据格式问题
# 检查数据是否正确格式化,labels 是否正确

# 3. 增加训练轮数
num_train_epochs: 35

# 4. 调整 warmup
warmup_steps: 100200

6.5 模型输出乱码

解决方案:

# 1. 检查 tokenizer padding_side
tokenizer.padding_side = "right"

# 2. 检查是否正确设置 pad_token
if tokenizer.pad_token is None:
    tokenizer.pad_token = tokenizer.eos_token

# 3. 增加训练数据量和质量

6.6 推理测试脚本

创建 inference_test.py

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel

def load_model(base_model_path, lora_path):
    """加载微调后的模型"""
    tokenizer = AutoTokenizer.from_pretrained(base_model_path, trust_remote_code=True)

    model = AutoModelForCausalLM.from_pretrained(
        base_model_path,
        torch_dtype=torch.bfloat16,
        device_map="auto",
        trust_remote_code=True,
    )

    # 加载 LoRA 权重
    model = PeftModel.from_pretrained(model, lora_path)
    model = model.merge_and_unload()  # 合并 LoRA 权重

    return model, tokenizer

def chat(model, tokenizer, instruction, input_text=""):
    """对话函数"""
    prompt = f"""<|im_start|>system
你是一位专业的算命师傅,精通八字、生肖、风水等传统命理学。请根据用户的问题,提供专业、详细且富有洞察力的解答。<|im_end|>
<|im_start|>user
{instruction}
{input_text}<|im_end|>
<|im_start|>assistant
"""

    inputs = tokenizer(prompt, return_tensors="pt").to(model.device)

    outputs = model.generate(
        **inputs,
        max_new_tokens=512,
        temperature=0.7,
        top_p=0.9,
        repetition_penalty=1.1,
        do_sample=True,
    )

    response = tokenizer.decode(outputs[0], skip_special_tokens=True)
    # 提取 assistant 的回复
    response = response.split("<|im_start|>assistant")[-1].strip()

    return response

if __name__ == "__main__":
    # 加载模型
    print("加载模型中...")
    model, tokenizer = load_model(
        base_model_path="Qwen/Qwen2.5-7B-Instruct",
        lora_path="./output/final_model"
    )

    # 测试
    print("\n测试开始:\n")

    questions = [
        ("请帮我算一下今年的运势", "我是1992年出生的,属猴"),
        ("我想知道我的事业运", ""),
        ("最近感情不顺利", ""),
    ]

    for instruction, input_text in questions:
        print(f"问题: {instruction} {input_text}")
        response = chat(model, tokenizer, instruction, input_text)
        print(f"回答: {response}\n")
        print("-" * 60 + "\n")

运行测试:

python inference_test.py

附录

A. 完整项目结构

qwen3-fortune-teller/
├── data/
│   ├── fortune_telling_train.json  # 原始数据
│   ├── train.json                  # 训练集
│   └── val.json                    # 验证集
├── config/
│   ├── training_config.json        # 训练配置
│   └── ds_config.json              # DeepSpeed 配置
├── scripts/
│   ├── train_qwen3_qlora.py       # 训练脚本
│   ├── prepare_data.py             # 数据准备
│   ├── inference_test.py           # 推理测试
│   └── monitor.py                  # 监控脚本
├── output/                         # 输出目录
│   ├── checkpoint-*/               # 检查点
│   ├── final_model/                # 最终模型
│   └── logs/                       # 日志
├── requirements.txt
└── README.md

B. requirements.txt

torch==2.2.0
transformers==4.38.0
accelerate==0.27.0
peft==0.9.0
bitsandbytes==0.42.0
datasets==2.17.0
trl==0.7.10
sentencepiece
deepspeed==0.13.1
tensorboard
wandb
flash-attn

C. 参考资源


🎉 总结

恭喜你完成了这份教程!

你已经学会了什么

理解核心概念: 微调、QLoRA、单机多卡、DDP等 ✅ 环境配置: 从零开始搭建Python + PyTorch + GPU训练环境 ✅ 数据准备: 理解数据格式,准备训练和验证数据 ✅ 单机多卡训练: 使用4张GPU并行训练,提速3-4倍 ✅ 监控调试: 使用TensorBoard监控训练,解决常见问题 ✅ 模型测试: 加载微调后的模型进行推理测试

完整工作流回顾

1. 环境配置 (30分钟)
   ├── 安装conda环境
   ├── 安装PyTorch和相关库
   └── 验证GPU可用性

2. 数据准备 (30分钟)
   ├── 创建JSON格式数据
   ├── 划分训练集和验证集
   └── 检查数据质量

3. 启动训练 (3-8小时)
   ├── 使用torchrun启动4卡训练
   ├── 监控训练进度和显存
   └── 等待训练完成

4. 测试模型 (15分钟)
   ├── 加载微调后的模型
   ├── 进行推理测试
   └── 评估效果

下一步建议

初级阶段(完成第一次微调):

  1. ✅ 收集更多高质量数据(至少500-1000条)
  2. ✅ 增加训练轮数(3 → 5 epochs)
  3. ✅ 尝试不同的learning_rate(1e-4, 2e-4, 5e-4)

中级阶段(优化训练效果): 4. 📊 使用更大的模型(Qwen3-14B) 5. 🎯 调整LoRA参数(lora_r: 32, 64, 128) 6. 🔧 尝试DeepSpeed ZeRO-3进一步优化

高级阶段(生产部署): 7. 🚀 使用vLLM加速推理 8. 🌐 部署为API服务(FastAPI) 9. 💡 使用DPO (Direct Preference Optimization)进一步优化 10. 📈 搭建Web界面(Gradio/Streamlit)

重要提醒

单机多卡的核心命令(记住它!):

# ❌ 错误 - 只用单卡
python train_qwen3_qlora.py

# ✅ 正确 - 使用4卡
torchrun --nproc_per_node=4 train_qwen3_qlora.py

显存不够时的应对策略:

  1. 减小 max_seq_length: 1024 → 512
  2. 减小 lora_r: 64 → 32
  3. 使用更小的模型: Qwen3-7B → Qwen3-1.8B

学习资源

遇到问题?

  1. 先查看本教程的常见问题解决章节
  2. 检查术语速查表,确认概念理解正确
  3. 查看错误信息,大部分错误都有明确提示
  4. 搜索GitHub Issues,很可能有人遇到过类似问题

祝你训练顺利! 🚀

记住:数据质量 > 模型大小 > 训练时间

好的微调效果来自于高质量的数据,而不是盲目增加模型规模。先从500-1000条精心准备的数据开始,观察效果,再逐步扩展。