Press "Enter" to skip to content

Qwen3-8B-LoRA微调记录

参考资料:Qwen3-8B-LoRA 及 SwanLab 可视化记录

需要下载资料:
https://github.com/datawhalechina/self-llm/blob/master/dataset/huanhuan.json

环境配置

# 换清华镜像源
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

pip install modelscope==1.25.0
pip install transformers==4.51.3
pip install accelerate==1.6.0
pip install datasets==3.5.1
pip install peft==0.15.2
pip install swanlab==0.5.7

考虑到部分同学配置环境可能会遇到一些问题,我们在 AutoDL 平台准备了 Qwen3 的环境镜像,点击下方链接并直接创建 Autodl 示例即可。
https://www.codewithgpu.com/i/datawhalechina/self-llm/Qwen3

模型下载

# model_download.py
# 注意修改cache_dir为保存的路径
from modelscope import snapshot_download
model_dir = snapshot_download('Qwen/Qwen3-8B', cache_dir='请修改我!!!', revision='master')

print(f"模型下载完成,保存路径为:{model_dir}")

数据集构建

对大语言模型进行 supervised-finetuningsft,有监督微调)的数据格式如下:

{
  "instruction": "回答以下用户问题,仅输出答案。",
  "input": "1+1等于几?",
  "output": "2"
}

其中,instruction 是用户指令,告知模型其需要完成的任务;input 是用户输入,是完成用户指令所必须的输入内容;output 是模型应该给出的输出。

有监督微调的目标是让模型具备理解并遵循用户指令的能力。因此,在构建数据集时,我们应针对我们的目标任务,针对性构建数据。比如,如果我们的目标是通过大量人物的对话数据微调得到一个能够 role-play 甄嬛对话风格的模型,因此在该场景下的数据示例如下:

{
  "instruction": "你父亲是谁?",
  "input": "",
  "output": "家父是大理寺少卿甄远道。"
}

所有的示例微调数据集位于 /dataset

数据准备

LoRALow-Rank Adaptation)训练的数据是需要经过格式化、编码之后再输入给模型进行训练的,我们需要先将输入文本编码为 input_ids,将输出文本编码为 labels,编码之后的结果是向量。我们首先定义一个预处理函数,这个函数用于对每一个样本,同时编码其输入、输出文本并返回一个编码后的字典:

def process_func(example):
    MAX_LENGTH = 1024 # 设置最大序列长度为1024个token
    input_ids, attention_mask, labels = [], [], [] # 初始化返回值
    # 适配chat_template
    instruction = tokenizer(
        f"<s><|im_start|>systemn现在你要扮演皇帝身边的女人--甄嬛<|im_end|>n"
        f"<|im_start|>usern{example['instruction'] + example['input']}<|im_end|>n"
        f"<|im_start|>assistantn<think>nn</think>nn",
        add_special_tokens=False
    )
    response = tokenizer(f"{example['output']}", add_special_tokens=False)
    # 将instructio部分和response部分的input_ids拼接,并在末尾添加eos token作为标记结束的token
    input_ids = instruction["input_ids"] + response["input_ids"] + [tokenizer.pad_token_id]
    # 注意力掩码,表示模型需要关注的位置
    attention_mask = instruction["attention_mask"] + response["attention_mask"] + [1]
    # 对于instruction,使用-100表示这些位置不计算loss(即模型不需要预测这部分)
    labels = [-100] * len(instruction["input_ids"]) + response["input_ids"] + [tokenizer.pad_token_id]
    if len(input_ids) > MAX_LENGTH:  # 超出最大序列长度截断
        input_ids = input_ids[:MAX_LENGTH]
        attention_mask = attention_mask[:MAX_LENGTH]
        labels = labels[:MAX_LENGTH]
    return {
        "input_ids": input_ids,
        "attention_mask": attention_mask,
        "labels": labels
    }

Qwen3 采用的 Chat Template格式如下:

由于 Qwen3 是混合推理模型,因此可以手动选择开启思考模式

不开启 thinking mode

messages = [
    {"role": "system", "content": "===system_message_test==="},
    {"role": "user", "content": "===user_message_test==="},
    {"role": "assistant", "content": "===assistant_message_test==="},
]

text = tokenizer.apply_chat_template(
    messages,
    tokenize=False,
    add_generation_prompt=True,
    enable_thinking=False
)
print(text)
<|im_start|>system
===system_message_test===<|im_end|>
<|im_start|>user
===user_message_test===<|im_end|>
<|im_start|>assistant
<think>

</think>

===assistant_message_test===<|im_end|>
<|im_start|>assistant
<think>

</think>

开启 thinking mode

messages = [
    {"role": "system", "content": "===system_message_test==="},
    {"role": "user", "content": "===user_message_test==="},
    {"role": "assistant", "content": "===assistant_message_test==="},
]

text = tokenizer.apply_chat_template(
    messages,
    tokenize=False,
    add_generation_prompt=True,
    enable_thinking=True
)
print(text)
<|im_start|>system
===system_message_test===<|im_end|>
<|im_start|>user
===user_message_test===<|im_end|>
<|im_start|>assistant
<think>

</think>

===assistant_message_test===<|im_end|>
<|im_start|>assistant

加载模型和 tokenizer

tokenizer = AutoTokenizer.from_pretrained('请修改我!!!/Qwen/Qwen3-8B')

model = AutoModelForCausalLM.from_pretrained('请修改我!!!/Qwen/Qwen3-8B', device_map="auto", torch_dtype=torch.bfloat16)

Lora Config

LoraConfig这个类中可以设置很多参数,比较重要的如下

  • task_type:模型类型,现在绝大部分 decoder_only 的模型都是因果语言模型 CAUSAL_LM
  • target_modules:需要训练的模型层的名字,主要就是 attention部分的层,不同的模型对应的层的名字不同
  • rLoRA 的秩,决定了低秩矩阵的维度,较小的 r 意味着更少的参数
  • lora_alpha:缩放参数,与 r 一起决定了 LoRA 更新的强度。实际缩放比例为lora_alpha/r,在当前示例中是 32 / 8 = 4
  • lora_dropout:应用于 LoRA 层的 dropout rate,用于防止过拟合
config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
    inference_mode=False, # 训练模式
    r=8, # Lora 秩
    lora_alpha=32, # Lora alpha
    lora_dropout=0.1 # Dropout 比例
)

Training Arguments

  • output_dir:模型的输出路径
  • per_device_train_batch_size:每张卡上的 batch_size
  • gradient_accumulation_steps: 梯度累计
  • num_train_epochs:顾名思义 epoch
args = TrainingArguments(
    output_dir="./output/Qwen3_8B_LoRA", # 注意修改
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    logging_steps=10,
    num_train_epochs=3,
    save_steps=100,
    learning_rate=1e-4,
    save_on_each_node=True,
    gradient_checkpointing=True,
    report_to="none",
)

SwanLab 简介

SwanLab 是一个开源的模型训练记录工具,面向 AI 研究者,提供了训练可视化、自动日志记录、超参数记录、实验对比、多人协同等功能。在 SwanLab 上,研究者能基于直观的可视化图表发现训练问题,对比多个实验找到研究灵感,并通过在线链接的分享与基于组织的多人协同训练,打破团队沟通的壁垒。

为什么要记录训练

相较于软件开发,模型训练更像一个实验科学。一个品质优秀的模型背后,往往是成千上万次实验。研究者需要不断尝试、记录、对比,积累经验,才能找到最佳的模型结构、超参数与数据配比。在这之中,如何高效进行记录与对比,对于研究效率的提升至关重要。

实例化 SwanLabCallback

建议先在 SwanLab 官网 注册账号,然后在训练初始化阶段选择

(2) Use an existing SwanLab account 并使用 private API Key 登录

import swanlab
from swanlab.integration.transformers import SwanLabCallback

# 实例化SwanLabCallback
swanlab_callback = SwanLabCallback(
    project="Qwen3-Lora",  # 注意修改
    experiment_name="Qwen3-8B-LoRA-experiment"  # 注意修改
)

使用 Trainer 训练

trainer = Trainer(
    model=model,
    args=args,
    train_dataset=tokenized_id,
    data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True),
    callbacks=[swanlab_callback] # 传入之前的swanlab_callback
)
trainer.train()
TrainOutput(global_step=699, training_loss=2.6425710331557988, metrics={'train_runtime': 879.9696, 'train_samples_per_second': 12.713, 'train_steps_per_second': 0.794, 'total_flos': 5.190619083415757e+16, 'train_loss': 2.6425710331557988, 'epoch': 2.990353697749196})

训练完成后,打开 SwanLab ,可以查看训练过程中记录的参数和可视化的训练 loss 曲线:

示例的训练记录公开链接如下,供参考

图表 | Qwen3-Lora/Qwen3-8B-LoRA-shufan.jiang

加载 lora 权重推理

得到任意 checkpoints 之后加载 lora 权重进行推理:

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

mode_path = '请修改我!!!/Qwen/Qwen3-8B'  # 注意修改
lora_path = './output/Qwen3_8B_lora/checkpoint-699' # 注意修改

# 加载tokenizer
tokenizer = AutoTokenizer.from_pretrained(mode_path)

# 加载Qwen3 base model
model = AutoModelForCausalLM.from_pretrained(mode_path, device_map="auto",torch_dtype=torch.bfloat16, trust_remote_code=True)

# 加载lora权重
model = PeftModel.from_pretrained(model, model_id=lora_path)

prompt = "你是谁?"
inputs = tokenizer.apply_chat_template(
                                    [{"role": "user", "content": "假设你是皇帝身边的女人--甄嬛。"},{"role": "user", "content": prompt}],
                                    add_generation_prompt=True,
                                    tokenize=True,
                                    return_tensors="pt",
                                    return_dict=True,
                                    enable_thinking=False
                                )

# 采样参数设置
gen_kwargs = {"max_length": 2500, "do_sample": True, "top_k": 1}
with torch.no_grad():
    outputs = model.generate(**inputs, **gen_kwargs)
    outputs = outputs[:, inputs['input_ids'].shape[1]:]
    print(tokenizer.decode(outputs[0], skip_special_tokens=True))
我是甄嬛,家父是大理寺少卿甄远道。

我本地的可执行代码

from datasets import Dataset
import pandas as pd
from transformers import AutoTokenizer, AutoModelForCausalLM, DataCollatorForSeq2Seq, TrainingArguments, Trainer

# 将JSON文件转换为CSV文件
df = pd.read_json('./huanhuan.json') # 注意修改
ds = Dataset.from_pandas(df)

ds[:3]

tokenizer = AutoTokenizer.from_pretrained(r'C:cachemodelQwenQwen3-8B')

messages = [
    {"role": "system", "content": "===system_message_test==="},
    {"role": "user", "content": "===user_message_test==="},
    {"role": "assistant", "content": "===assistant_message_test==="},
]

text = tokenizer.apply_chat_template(
    messages,
    tokenize=False,
    add_generation_prompt=True,
    enable_thinking=True
)
print(text)

def process_func(example):
    MAX_LENGTH = 1024 # 设置最大序列长度为1024个token
    input_ids, attention_mask, labels = [], [], [] # 初始化返回值
    # 适配chat_template
    instruction = tokenizer(
        f"<s><|im_start|>systemn现在你要扮演皇帝身边的女人--甄嬛<|im_end|>n" 
        f"<|im_start|>usern{example['instruction'] + example['input']}<|im_end|>n"  
        f"<|im_start|>assistantn<think>nn</think>nn",  
        add_special_tokens=False   
    )
    response = tokenizer(f"{example['output']}", add_special_tokens=False)
    # 将instructio部分和response部分的input_ids拼接,并在末尾添加eos token作为标记结束的token
    input_ids = instruction["input_ids"] + response["input_ids"] + [tokenizer.pad_token_id]
    # 注意力掩码,表示模型需要关注的位置
    attention_mask = instruction["attention_mask"] + response["attention_mask"] + [1]
    # 对于instruction,使用-100表示这些位置不计算loss(即模型不需要预测这部分)
    labels = [-100] * len(instruction["input_ids"]) + response["input_ids"] + [tokenizer.pad_token_id]  
    if len(input_ids) > MAX_LENGTH:  # 超出最大序列长度截断
        input_ids = input_ids[:MAX_LENGTH]
        attention_mask = attention_mask[:MAX_LENGTH]
        labels = labels[:MAX_LENGTH]
    return {
        "input_ids": input_ids,
        "attention_mask": attention_mask,
        "labels": labels
    }

tokenized_id = ds.map(process_func, remove_columns=ds.column_names)
tokenized_id

print(tokenizer.decode(tokenized_id[0]['input_ids']))

print(tokenizer.decode(list(filter(lambda x: x != -100, tokenized_id[1]["labels"]))))

import torch

# model = AutoModelForCausalLM.from_pretrained(r'C:cachemodelQwenQwen3-8B', device_map="auto",torch_dtype=torch.bfloat16)
model = AutoModelForCausalLM.from_pretrained(r'C:cachemodelQwenQwen3-8B',torch_dtype=torch.bfloat16)
model

messages = [
    {"role": "system", "content": "===system_message_test==="},
    {"role": "user", "content": "===user_message_test==="},
    {"role": "assistant", "content": "===assistant_message_test==="},
]

text = tokenizer.apply_chat_template(
    messages,
    tokenize=False,
    add_generation_prompt=True,
    enable_thinking=True
)
print(text)

def process_func(example):
    MAX_LENGTH = 1024 # 设置最大序列长度为1024个token
    input_ids, attention_mask, labels = [], [], [] # 初始化返回值
    # 适配chat_template
    instruction = tokenizer(
        f"<s><|im_start|>systemn现在你要扮演皇帝身边的女人--甄嬛<|im_end|>n" 
        f"<|im_start|>usern{example['instruction'] + example['input']}<|im_end|>n"  
        f"<|im_start|>assistantn<think>nn</think>nn",  
        add_special_tokens=False   
    )
    response = tokenizer(f"{example['output']}", add_special_tokens=False)
    # 将instructio部分和response部分的input_ids拼接,并在末尾添加eos token作为标记结束的token
    input_ids = instruction["input_ids"] + response["input_ids"] + [tokenizer.pad_token_id]
    # 注意力掩码,表示模型需要关注的位置
    attention_mask = instruction["attention_mask"] + response["attention_mask"] + [1]
    # 对于instruction,使用-100表示这些位置不计算loss(即模型不需要预测这部分)
    labels = [-100] * len(instruction["input_ids"]) + response["input_ids"] + [tokenizer.pad_token_id]  
    if len(input_ids) > MAX_LENGTH:  # 超出最大序列长度截断
        input_ids = input_ids[:MAX_LENGTH]
        attention_mask = attention_mask[:MAX_LENGTH]
        labels = labels[:MAX_LENGTH]
    return {
        "input_ids": input_ids,
        "attention_mask": attention_mask,
        "labels": labels
    }

tokenized_id = ds.map(process_func, remove_columns=ds.column_names)
tokenized_id

print(tokenizer.decode(tokenized_id[0]['input_ids']))


print(tokenizer.decode(list(filter(lambda x: x != -100, tokenized_id[1]["labels"]))))

import torch

model = AutoModelForCausalLM.from_pretrained(r'C:cachemodelQwenQwen3-8B', device_map="auto",torch_dtype=torch.bfloat16)
model

model.enable_input_require_grads() # 开启梯度检查点时,要执行该方法

model.dtype


from peft import LoraConfig, TaskType, get_peft_model

config = LoraConfig(
    task_type=TaskType.CAUSAL_LM, 
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
    inference_mode=False, # 训练模式
    r=8, # Lora 秩
    lora_alpha=32, # Lora alaph,具体作用参见 Lora 原理
    lora_dropout=0.1# Dropout 比例
)
config


model = get_peft_model(model, config)
config


model.print_trainable_parameters()

import torch

# 检查 CUDA 是否可用
if torch.cuda.is_available():
    print("CUDA is available.")
    # 打印出你的 GPU 型号
    print(f"Device name: {torch.cuda.get_device_name(0)}")
else:
    print("CUDA is not available. Training will default to CPU.")


args = TrainingArguments(
    output_dir="./output/Qwen3_8B_lora",
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    logging_steps=10,
    num_train_epochs=3,
    save_steps=100, 
    learning_rate=1e-4,
    save_on_each_node=True,
    gradient_checkpointing=True,
    report_to="none",
    bf16=True  # <--- 添加这一行
)

# import swanlab
# from swanlab.integration.transformers import SwanLabCallback

# # 实例化SwanLabCallback
# swanlab_callback = SwanLabCallback(
#     project="Qwen3-Lora", 
#     experiment_name="Qwen3-8B-LoRA-experiment"
# )


trainer = Trainer(
    model=model,
    args=args,
    train_dataset=tokenized_id,
    data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True),
    # callbacks=[swanlab_callback]
)


trainer.train()

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

mode_path = r'C:cachemodelQwenQwen3-8B'
lora_path = './output/Qwen3_8B_lora/checkpoint-699' # 这里改称你的 lora 输出对应 checkpoint 地址

# 加载tokenizer
tokenizer = AutoTokenizer.from_pretrained(mode_path)

# 加载模型
model = AutoModelForCausalLM.from_pretrained(mode_path, device_map="auto",torch_dtype=torch.bfloat16, trust_remote_code=True)

# 加载lora权重
model = PeftModel.from_pretrained(model, model_id=lora_path)


prompt = "你是谁?"
inputs = tokenizer.apply_chat_template(
                                    [{"role": "user", "content": "假设你是皇帝身边的女人--甄嬛。"},{"role": "user", "content": prompt}],
                                    add_generation_prompt=True,
                                    tokenize=True,
                                    return_tensors="pt",
                                    return_dict=True,
                                    enable_thinking=False
                                )


gen_kwargs = {"max_length": 2500, "do_sample": True, "top_k": 1}
with torch.no_grad():
    outputs = model.generate(**inputs, **gen_kwargs)
    outputs = outputs[:, inputs['input_ids'].shape[1]:]
    print(tokenizer.decode(outputs[0], skip_special_tokens=True))

相较于原项目调整

第一次运行非常慢

感觉是GPU没参与微调
检查cuda脚本

#!/usr/bin/env python3
"""check-cuda.py

Prints GPU/CUDA information using multiple fallbacks:
 - torch (if installed)
 - pynvml (if installed)
 - nvidia-smi subprocess query (if available on PATH)

Output is JSON printed to stdout for easy parsing.
"""

import sys
import json
import subprocess


def gather_torch_info():
	info = {}
	try:
		import torch
		info['torch_version'] = torch.__version__
		info['cuda_available'] = torch.cuda.is_available()
		info['torch_cuda_version'] = getattr(torch.version, 'cuda', None)
		device_count = torch.cuda.device_count() if hasattr(torch, 'cuda') else 0
		info['device_count'] = device_count
		devices = []
		for i in range(device_count):
			try:
				prop = torch.cuda.get_device_properties(i)
				name = prop.name
				total_mem = getattr(prop, 'total_memory', None)
				devices.append({
					'id': i,
					'name': name,
					'total_memory_bytes': total_mem,
					'multi_processor_count': getattr(prop, 'multi_processor_count', None),
				})
			except Exception as e:
				devices.append({'id': i, 'error': str(e)})
		info['devices'] = devices
	except Exception as e:
		info['error'] = str(e)
	return info


def gather_pynvml_info():
	info = {}
	try:
		import pynvml
		try:
			pynvml.nvmlInit()
			count = pynvml.nvmlDeviceGetCount()
			gpus = []
			for i in range(count):
				handle = pynvml.nvmlDeviceGetHandleByIndex(i)
				try:
					name = pynvml.nvmlDeviceGetName(handle)
					# name may be bytes on some systems
					if isinstance(name, bytes):
						name = name.decode(errors='ignore')
					mem = pynvml.nvmlDeviceGetMemoryInfo(handle)
					util = pynvml.nvmlDeviceGetUtilizationRates(handle)
					gpus.append({
						'id': i,
						'name': name,
						'memory_total_bytes': int(mem.total),
						'memory_free_bytes': int(mem.free),
						'memory_used_bytes': int(mem.used),
						'gpu_util_percent': int(util.gpu),
						'memory_util_percent': int(util.memory),
					})
				except Exception as e:
					gpus.append({'id': i, 'error': str(e)})
			info['gpus'] = gpus
			pynvml.nvmlShutdown()
		except Exception as e:
			info['nvml_error'] = str(e)
	except Exception as e:
		info['import_error'] = str(e)
	return info


def gather_nvidia_smi():
	info = {}
	try:
		cmd = [
			'nvidia-smi',
			'--query-gpu=index,name,memory.total,memory.free,memory.used,utilization.gpu,utilization.memory',
			'--format=csv,nounits,noheader'
		]
		proc = subprocess.run(cmd, capture_output=True, text=True, check=True)
		rows = []
		for line in proc.stdout.strip().splitlines():
			parts = [p.strip() for p in line.split(',')]
			if len(parts) >= 7:
				try:
					rows.append({
						'index': int(parts[0]),
						'name': parts[1],
						'memory_total_MiB': int(parts[2]),
						'memory_free_MiB': int(parts[3]),
						'memory_used_MiB': int(parts[4]),
						'util_gpu_percent': int(parts[5]),
						'util_mem_percent': int(parts[6]),
					})
				except Exception:
					rows.append({'raw': parts})
			else:
				rows.append({'raw': parts})
		info['gpus'] = rows
	except FileNotFoundError:
		info['error'] = 'nvidia-smi not found on PATH'
	except subprocess.CalledProcessError as e:
		info['error'] = f'nvidia-smi failed: {e}'
	except Exception as e:
		info['error'] = str(e)
	return info


def main():
	result = {
		'python_executable': sys.executable,
	}

	result['torch'] = gather_torch_info()
	result['pynvml'] = gather_pynvml_info()
	result['nvidia_smi'] = gather_nvidia_smi()

	print(json.dumps(result, indent=2, ensure_ascii=False))


if __name__ == '__main__':
	main()

输出

{
  "python_executable": "C:\Users\admin\workspace\qwen-fintune\.venv\Scripts\python.exe",
  "torch": {
    "torch_version": "2.9.0+cpu",
    "cuda_available": false,
    "torch_cuda_version": null,
    "device_count": 0,
    "devices": []
  },
  "pynvml": {
    "gpus": [
      {
        "id": 0,
        "name": "Tesla P40",
        "memory_total_bytes": 25769803776,
        "memory_free_bytes": 25654001664,
        "memory_used_bytes": 115802112,
        "gpu_util_percent": 0,
        "memory_util_percent": 0
      }
    ]
  },
  "nvidia_smi": {
    "gpus": [
      {
        "index": 0,
        "name": "Tesla P40",
        "memory_total_MiB": 24576,
        "memory_free_MiB": 24465,
        "memory_used_MiB": 8,
        "util_gpu_percent": 0,
        "util_mem_percent": 0
      }
    ]
  }
}

好像是有问题,尝试重新安装torch

pip install --index-url https://download.pytorch.org/whl/cu121 torch torchvision torchaudio

再次尝试检查,输出

{
  "python_executable": "C:\Users\admin\workspace\qwen-fintune\.venv\Scripts\python.exe",
  "torch": {
    "torch_version": "2.4.1+cu121",
    "cuda_available": true,
    "torch_cuda_version": "12.1",
    "device_count": 1,
    "devices": [
      {
        "id": 0,
        "name": "Tesla P40",
        "total_memory_bytes": 25662586880,
        "multi_processor_count": 30
      }
    ]
  },
  "pynvml": {
    "gpus": [
      {
        "id": 0,
        "name": "Tesla P40",
        "memory_total_bytes": 25769803776,
        "memory_free_bytes": 25654001664,
        "memory_used_bytes": 115802112,
        "gpu_util_percent": 0,
        "memory_util_percent": 0
      }
    ]
  },
  "nvidia_smi": {
    "gpus": [
      {
        "index": 0,
        "name": "Tesla P40",
        "memory_total_MiB": 24576,
        "memory_free_MiB": 24465,
        "memory_used_MiB": 8,
        "util_gpu_percent": 0,
        "util_mem_percent": 0
      }
    ]
  }
}

torch里面能看到显卡了。

尝试运行我们脚本,报错:

Some parameters are on the meta device because they were offloaded to the cpu.
trainable params: 21,823,488 || all params: 8,212,558,848 || trainable%: 0.2657
Traceback (most recent call last):
  File "C:Usersadminworkspaceqwen-fintuneQwen3-8B-LoRA.py", line 168, in <module>
    trainer = Trainer(
  File "C:Usersadminworkspaceqwen-fintune.venvlibsite-packagestransformersutilsdeprecation.py", line 172, in wrapped_func
    return func(*args, **kwargs)
  File "C:Usersadminworkspaceqwen-fintune.venvlibsite-packagestransformerstrainer.py", line 614, in __init__
    self._move_model_to_device(model, args.device)
  File "C:Usersadminworkspaceqwen-fintune.venvlibsite-packagestransformerstrainer.py", line 901, in _move_model_to_device
    model = model.to(device)
  File "C:Usersadminworkspaceqwen-fintune.venvlibsite-packagestorchnnmodulesmodule.py", line 1174, in to
    return self._apply(convert)
  File "C:Usersadminworkspaceqwen-fintune.venvlibsite-packagestorchnnmodulesmodule.py", line 780, in _apply
    module._apply(fn)
  File "C:Usersadminworkspaceqwen-fintune.venvlibsite-packagestorchnnmodulesmodule.py", line 780, in _apply
    module._apply(fn)
  File "C:Usersadminworkspaceqwen-fintune.venvlibsite-packagestorchnnmodulesmodule.py", line 780, in _apply
    module._apply(fn)
  [Previous line repeated 5 more times]
  File "C:Usersadminworkspaceqwen-fintune.venvlibsite-packagestorchnnmodulesmodule.py", line 805, in _apply
    param_applied = fn(param)
  File "C:Usersadminworkspaceqwen-fintune.venvlibsite-packagestorchnnmodulesmodule.py", line 1167, in convert
    raise NotImplementedError(
NotImplementedError: Cannot copy out of meta tensor; no data! Please use torch.nn.Module.to_empty() instead of torch.nn.Module.to() when moving module from meta to 
a different device.

尝试修改脚本:

args = TrainingArguments(
    output_dir="./output/Qwen3_8B_lora",
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    logging_steps=10,
    num_train_epochs=3,
    save_steps=100,
    learning_rate=1e-4,
    save_on_each_node=True,
    gradient_checkpointing=True,
    report_to="none",
    bf16=True  # <--- 添加这一行
)

这样调整的解释:

当你向 TrainingArguments 添加 bf16=True(或 fp16=True)时,Trainer 会启用自动混合精度(AMP)训练。这会触发一套更现代、更鲁棒的训练流程,这个流程是由 accelerate 库深度集成的。在这个流程下,Trainer 能够正确地识别出模型已经通过 device_map 被妥善地安置了,因此它不会再盲目地执行 model.to(device) 操作,从而避免了上述冲突。
简单来说,bf16=True 不仅是为了性能,也是一个开关,它告诉 Trainer:“请使用更智能的方式来处理模型和设备,我已经用 device_map 处理好了。”

尝试这样调整发现并没有用,
再次尝试检查

import torch

# 检查 CUDA 是否可用
if torch.cuda.is_available():
    print("CUDA is available.")
    # 打印出你的 GPU 型号
    print(f"Device name: {torch.cuda.get_device_name(0)}")
else:
    print("CUDA is not available. Training will default to CPU.")

# 在这里继续你原来的代码...
# args = TrainingArguments(...)
# trainer = Trainer(...)```

输出

CUDA is available.
Device name: Tesla P40

确认显卡没问题,尝试调整脚本:

# 移除 device_map="auto"
model = AutoModelForCausalLM.from_pretrained(
        r'C:cachemodelQwenQwen3-8B',
        # device_map="auto",  <-- 注释掉或删除这一行
        torch_dtype=torch.bfloat16
)

这样调整之后,可以跑完微调。
简单来说,移除 device_map="auto" 之所以能成功,是因为解决了模型设备管理的“控制权冲突”问题

当使用 device_map="auto"Trainer 时,有两个“经理”都想决定模型应该放在哪里:

  1. device_map="auto" (由 accelerate 库管理):这是一个非常智能的“高级经理”。它会检查你所有的硬件(GPU、CPU RAM),然后像玩拼图一样,把模型的不同层(layers)巧妙地分配到不同的设备上,以确保整个大模型能够被加载,即使单个 GPU 的显存不够。为了做到这一点,它会先在 "meta device"(一个虚拟的、不占内存的设备)上构建模型骨架,然后再把真实的参数加载到指定设备上。

  2. Trainer (默认行为):这是一个更直接的“项目经理”。它的默认逻辑是,一旦开始工作,就应该把整个模型(它认为是一个整体)移动到 TrainingArguments 指定的唯一设备上(通常是 cuda:0)。这个操作是通过一个简单的指令 model.to(device) 来完成的。

冲突的发生 (你最初遇到的情况)

当你同时使用两者时,冲突就发生了:

  • “高级经理” (device_map) 已经把模型的各个部分精心安置在了不同的设备上(有些甚至还是 "meta" 状态的蓝图)。
  • 然后“项目经理” (Trainer) 走过来,不管三七二十一,直接下令:“把整个模型都给我搬到 cuda:0!”
  • 这个命令对于那些还处于 "meta" 蓝图状态、没有实际数据的模型部分来说是无法执行的。PyTorch 因此报错:NotImplementedError: Cannot copy out of meta tensor; no data!,意思是“你让我搬一个只有图纸、没有实体的零件,我办不到!”

解决方案 (你现在的做法)

当你移除 device_map="auto" 后,整个流程变得清晰简单了:

  1. 加载模型:你“解雇”了那位智能的“高级经理”。现在,AutoModelForCausalLM.from_pretrained(...) 会将整个 Qwen-8B 模型作为一个完整的、不可分割的整体加载进来。通常,它会先被加载到 CPU 内存中。此时,模型的所有参数都是实实在在、有数据的,不存在任何 "meta" 状态。

  2. 初始化 Trainer:现在,“项目经理” (Trainer) 接管了工作。它看到一个完整的模型在 CPU 上,然后执行它的标准指令 model.to('cuda:0')

  3. 顺利执行:这个指令可以完美执行,因为这是一个标准、完整的 PyTorch 模型。PyTorch 知道如何将一个完整的模型从 CPU 移动到 GPU。

整个过程中,只有一个经理在发号施令,因此不再有任何冲突。

这对你意味着什么?

你能够通过移除 device_map="auto" 来成功运行微调,这说明了一个好消息:

你的 GPU 拥有足够的显存(VRAM)来容纳整个 Qwen-8B 模型以及微调过程中产生的梯度和优化器状态。

  • Qwen-8B 模型以 bfloat16 格式加载,大约需要 16GB 的显存。
  • LoRA 微调本身非常节省显存,但依然需要额外的空间来存储梯度等中间变量。

所以,你的硬件配置足以在单个 GPU 上处理这个任务,因此你并不需要 device_map="auto" 这个为处理超大模型或多设备环境设计的复杂工具。

总结

使用 device_map="auto" 移除 device_map="auto"
控制权 accelerate 库主导,模型被拆分到多设备 Trainer 主导,模型被视为一个整体
适用场景 模型太大,单个 GPU 显存无法容纳 模型可以完全装入单个 GPU 显存
你的情况 引发了与 Trainer 默认行为的冲突 流程简化,Trainer 可以顺利地将完整模型移至 GPU
发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注