随着预训练大模型(如BERT、GPT、ViT、LLaMA、CLIP等)的崛起,人工智能进入了一个新的范式:预训练-微调(Pre-train, Fine-tune) 。这些大模型在海量数据上学习到了通用的、强大的表示能力和世界知识。然而,要将这些通用模型应用于特定的下游任务或领域,通常还需要进行微调(Fine-tuning)。
微调的核心在于调整预训练模型的参数,使其更好地适应目标任务的数据分布和特定需求。但大模型通常拥有数十亿甚至数万亿的参数,直接进行全参数微调会带来巨大的计算资源和存储挑战。本章将深入探讨大模型微调的策略,以及如何采用高效训练技术来应对这些挑战。
4.1 大模型微调:从通用到专精
4.1.1 为什么需要微调?
尽管预训练大模型具有强大的泛化能力,但它们在预训练阶段看到的数据通常是通用的、领域无关的。当我们需要它们完成特定领域的任务时,例如医疗文本分类、法律问答、特定风格的图像生成等,通用知识可能不足以满足需求。微调的目的是:
- 适应任务特异性: 调整模型,使其更好地理解和处理特定任务的输入输出格式及语义。
- 适应数据分布: 将模型知识迁移到目标任务的特定数据分布上,提高模型在目标数据上的性能。
- 提升性能: 通常,经过微调的模型在特定下游任务上的表现会显著优于直接使用预训练模型。
- 提高效率: 相较于从头开始训练一个新模型,微调一个预训练大模型通常更快、更有效。
4.1.2 全参数微调 (Full Fine-tuning)
核心思想: 全参数微调是最直接的微调方法,它解冻(unfreeze)预训练模型的所有参数,并使用目标任务的标注数据对其进行端到端(end-to-end)的训练。
原理详解: 在全参数微调中,我们加载一个预训练模型的权重,然后像训练一个普通神经网络一样,使用新的数据集和损失函数来训练它。由于模型的所有层都参与梯度计算和参数更新,理论上模型可以最大程度地适应新任务。
优点:
- 性能潜力大: 如果资源允许且数据集足够大,全参数微调通常能达到最佳性能。
- 概念简单: 实现起来相对直接。
缺点:
- 计算资源需求巨大: 对于拥有数十亿参数的大模型,全参数微调需要大量的GPU显存和计算时间。
- 存储成本高昂: 每个下游任务都需要存储一套完整的模型参数,不便于多任务部署。
- 灾难性遗忘(Catastrophic Forgetting): 在小规模数据集上进行微调时,模型可能会"遗忘"在预训练阶段学到的通用知识,导致在其他任务上的性能下降。
Python示例:简单文本分类的全参数微调
我们将使用一个预训练的BERT模型进行情感分类任务的全参数微调。
Python
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments
from datasets import load_dataset, Dataset
import numpy as np
from sklearn.metrics import accuracy_score, precision_recall_fscore_support
# 1. 加载预训练模型和分词器
model_name = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=2) # 2个类别:正面/负面情感
# 2. 准备数据集 (使用Hugging Face datasets库加载一个情感分析数据集)
# 这里使用 'imdb' 数据集作为示例
# 如果是第一次运行,会自动下载
print("Loading IMDb dataset...")
dataset = load_dataset("imdb")
# 预处理数据
def preprocess_function(examples):
return tokenizer(examples["text"], truncation=True, padding="max_length", max_length=128)
tokenized_imdb = dataset.map(preprocess_function, batched=True)
# 重命名标签列为 'labels' 以符合Trainer的要求
tokenized_imdb = tokenized_imdb.rename_columns({"label": "labels"})
# 移除原始文本列
tokenized_imdb = tokenized_imdb.remove_columns(["text"])
# 设置格式为PyTorch tensors
tokenized_imdb.set_format("torch")
# 划分训练集和测试集
small_train_dataset = tokenized_imdb["train"].shuffle(seed=42).select(range(2000)) # 使用小部分数据进行演示
small_eval_dataset = tokenized_imdb["test"].shuffle(seed=42).select(range(500))
# 3. 定义评估指标
def compute_metrics(p):
predictions, labels = p
predictions = np.argmax(predictions, axis=1)
precision, recall, f1, _ = precision_recall_fscore_support(labels, predictions, average='binary')
acc = accuracy_score(labels, predictions)
return {
'accuracy': acc,
'f1': f1,
'precision': precision,
'recall': recall
}
# 4. 配置训练参数
training_args = TrainingArguments(
output_dir="./results_full_finetune",
num_train_epochs=3, # 训练轮次
per_device_train_batch_size=16, # 训练批次大小
per_device_eval_batch_size=16, # 评估批次大小
warmup_steps=500, # 学习率预热步数
weight_decay=0.01, # 权重衰减
logging_dir='./logs_full_finetune', # 日志目录
logging_steps=100,
evaluation_strategy="epoch", # 每个epoch结束后评估
save_strategy="epoch", # 每个epoch结束后保存模型
load_best_model_at_end=True, # 训练结束后加载最佳模型
metric_for_best_model="f1", # 衡量最佳模型的指标
report_to="none" # 不上传到任何在线平台
)
# 5. 初始化Trainer并开始训练
trainer = Trainer(
model=model,
args=training_args,
train_dataset=small_train_dataset,
eval_dataset=small_eval_dataset,
compute_metrics=compute_metrics,
)
print("\n--- BERT 全参数微调示例 ---")
# 检查是否有GPU可用
if torch.cuda.is_available():
print(f"Using GPU: {torch.cuda.get_device_name(0)}")
else:
print("No GPU available, training on CPU (will be slow).")
trainer.train()
print("\nFull fine-tuning completed. Evaluation results:")
eval_results = trainer.evaluate()
print(eval_results)
代码说明:
- 我们使用了Hugging Face
transformers
库的Trainer
API,它极大地简化了训练过程。 AutoTokenizer
和AutoModelForSequenceClassification
自动加载BERT模型和对应的分词器。load_dataset("imdb")
用于获取情感分类的示例数据。preprocess_function
将文本转换为模型可以理解的token ID序列。TrainingArguments
用于配置各种训练参数,如学习率、批次大小、保存策略等。compute_metrics
定义了用于评估模型性能的指标。trainer.train()
启动训练过程。
4.1.3 参数高效微调 (Parameter-Efficient Fine-tuning, PEFT)
核心思想: PEFT旨在解决全参数微调的缺点,它通过只微调预训练模型中少量新增或现有参数,同时冻结大部分预训练参数,从而大大降低计算和存储成本,并有效避免灾难性遗忘。
PEFT方法可以分为几大类:
- 新增适配器模块: 在预训练模型的中间层或输出层插入小型的可训练模块(Adapter)。
- 软提示: 优化输入中少量连续的、可学习的"软提示"或"前缀",而不是修改模型参数。
- 低秩适应: 通过低秩分解来近似全参数更新,减少可训练参数。
我们将重点介绍其中最流行且高效的几种方法。
4.1.3.1 LoRA (Low-Rank Adaptation)

- 显著减少可训练参数: 大幅降低显存消耗和训练时间。
- 避免灾难性遗忘: 预训练权重冻结,保护了通用知识。
- 部署高效: 可以在推理时合并权重,不增加额外延迟。
- 多任务部署: 针对不同任务,只需存储和加载很小的 (BA) 矩阵。
Python示例:使用peft
库进行LoRA微调
我们将使用Hugging Face的peft
库对预训练的GPT-2模型进行LoRA微调,用于文本生成任务。
首先,确保安装peft
库:
Bash
pip install peft
Python
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, Trainer, TrainingArguments
from datasets import load_dataset
from peft import LoraConfig, get_peft_model, TaskType
# 1. 加载预训练模型和分词器 (GPT-2作为示例)
model_name_lora = "gpt2"
tokenizer_lora = AutoTokenizer.from_pretrained(model_name_lora)
# GPT-2默认没有padding token,这里手动设置
tokenizer_lora.pad_token = tokenizer_lora.eos_token
model_lora = AutoModelForCausalLM.from_pretrained(model_name_lora)
# 2. 准备数据集 (使用Hugging Face datasets库加载一个文本数据集)
# 这里使用 'wikitext-2-raw-v1' 作为示例,用于语言模型微调
print("\nLoading Wikitext-2 dataset for LoRA fine-tuning...")
dataset_lora = load_dataset("wikitext", "wikitext-2-raw-v1")
# 预处理数据:将文本连接起来并分块
def tokenize_function(examples):
return tokenizer_lora(examples["text"], truncation=True, padding="max_length", max_length=128)
tokenized_dataset_lora = dataset_lora.map(tokenize_function, batched=True, remove_columns=["text"])
# 语言模型任务需要将输入作为标签(next token prediction)
def prepare_lm_labels(examples):
examples["labels"] = examples["input_ids"].copy()
return examples
tokenized_dataset_lora = tokenized_dataset_lora.map(prepare_lm_labels, batched=True)
small_train_dataset_lora = tokenized_dataset_lora["train"].shuffle(seed=42).select(range(1000)) # 减少数据量以便演示
small_eval_dataset_lora = tokenized_dataset_lora["validation"].shuffle(seed=42).select(range(200))
# 3. 配置LoRA
lora_config = LoraConfig(
r=8, # LoRA的秩,通常设为8, 16, 32等
lora_alpha=16, # LoRA的缩放因子
target_modules=["c_attn", "c_proj"], # GPT-2中通常微调的Attention和Projection层
lora_dropout=0.1,
bias="none", # 对bias进行微调的策略,'none'表示不微调bias
task_type=TaskType.CAUSAL_LM, # 任务类型,这里是因果语言模型
)
# 4. 获取LoRA模型
# 这一步会根据lora_config修改model_lora,使其只有LoRA层可训练
model_lora = get_peft_model(model_lora, lora_config)
model_lora.print_trainable_parameters() # 打印可训练参数数量
# 5. 配置训练参数
training_args_lora = TrainingArguments(
output_dir="./results_lora_finetune",
num_train_epochs=3,
per_device_train_batch_size=8,
per_device_eval_batch_size=8,
warmup_steps=100,
weight_decay=0.01,
logging_dir='./logs_lora_finetune',
logging_steps=50,
evaluation_strategy="epoch",
save_strategy="epoch",
load_best_model_at_end=True,
metric_for_best_model="eval_loss", # 语言模型通常评估loss
report_to="none"
)
# 6. 初始化Trainer并开始训练
trainer_lora = Trainer(
model=model_lora,
args=training_args_lora,
train_dataset=small_train_dataset_lora,
eval_dataset=small_eval_dataset_lora,
tokenizer=tokenizer_lora, # LoraConfig中指定了task_type为CAUSAL_LM,这里需要提供tokenizer
)
print("\n--- GPT-2 LoRA微调示例 ---")
if torch.cuda.is_available():
print(f"Using GPU: {torch.cuda.get_device_name(0)}")
else:
print("No GPU available, training on CPU (will be slow).")
trainer_lora.train()
print("\nLoRA fine-tuning completed. Evaluation results:")
eval_results_lora = trainer_lora.evaluate()
print(eval_results_lora)
# 7. 演示LoRA模型生成文本
print("\n--- LoRA模型文本生成示例 ---")
model_lora.eval()
prompt = "Once upon a time, in a faraway land,"
inputs = tokenizer_lora(prompt, return_tensors="pt").to(model_lora.device)
# 生成文本
with torch.no_grad():
outputs = model_lora.generate(
**inputs,
max_new_tokens=50,
num_return_sequences=1,
do_sample=True, # 允许采样生成更自然的文本
temperature=0.7,
top_k=50,
top_p=0.95
)
generated_text = tokenizer_lora.decode(outputs[0], skip_special_tokens=True)
print(f"Prompt: {prompt}")
print(f"Generated text:\n{generated_text}")
代码说明:
- 我们引入了Hugging Face的
peft
库,这是PEFT方法的官方推荐实现。 LoraConfig
定义了LoRA的参数,如秩r
,缩放因子lora_alpha
,以及要应用LoRA的层(target_modules
)。对于GPT-2,通常选择注意力机制中的c_attn
和c_proj
层。get_peft_model(model_lora, lora_config)
是核心,它会自动修改原模型,使其只有LoRA层可训练,并打印出可训练参数的比例,你会发现这个比例非常小。- 训练过程与全参数微调类似,但实际更新的参数量大大减少。
- 最后,演示了如何使用LoRA微调后的模型进行文本生成。
4.1.3.2 Prompt Tuning / Prefix Tuning
核心思想: 这些方法通过在模型的输入序列前添加少量可学习的"软提示(Soft Prompts)"或"前缀(Prefixes)"来引导模型。这些软提示不是真实的token,而是直接在模型的嵌入空间中进行优化的连续向量。在微调时,冻结预训练模型的所有参数,只训练这些软提示向量。
原理详解:
- Prompt Tuning: 在输入序列的token嵌入前或后添加少量可学习的向量。这些向量与正常的token嵌入一同输入到模型中。
- Prefix Tuning: 在Transformer的每一层(或仅部分层)的注意力机制中,在键(Key)和值(Value)矩阵前添加可学习的前缀。这相当于在注意力计算中注入了任务特定的信息。
优点:
- 极少的可训练参数: 通常只有几十到几百个参数。
- 避免灾难性遗忘: 模型主体完全冻结。
- 多任务部署: 针对不同任务,只需存储和加载很小的提示向量。
- 推理高效: 只需要在输入序列前添加提示,不增加推理时的计算复杂性。
Python示例:使用peft
库进行Prompt Tuning微调
我们将继续使用peft
库对GPT-2模型进行Prompt Tuning微调。
Python
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, Trainer, TrainingArguments
from datasets import load_dataset
from peft import PromptTuningConfig, get_peft_model, TaskType, PromptTuningInit
# 1. 加载预训练模型和分词器 (GPT-2作为示例)
model_name_prompt = "gpt2"
tokenizer_prompt = AutoTokenizer.from_pretrained(model_name_prompt)
tokenizer_prompt.pad_token = tokenizer_prompt.eos_token
model_prompt = AutoModelForCausalLM.from_pretrained(model_name_prompt)
# 2. 准备数据集 (同LoRA示例,使用wikitext-2)
print("\nLoading Wikitext-2 dataset for Prompt Tuning fine-tuning...")
dataset_prompt = load_dataset("wikitext", "wikitext-2-raw-v1")
def tokenize_function_prompt(examples):
return tokenizer_prompt(examples["text"], truncation=True, padding="max_length", max_length=128)
tokenized_dataset_prompt = dataset_prompt.map(tokenize_function_prompt, batched=True, remove_columns=["text"])
def prepare_lm_labels_prompt(examples):
examples["labels"] = examples["input_ids"].copy()
return examples
tokenized_dataset_prompt = tokenized_dataset_prompt.map(prepare_lm_labels_prompt, batched=True)
small_train_dataset_prompt = tokenized_dataset_prompt["train"].shuffle(seed=42).select(range(1000))
small_eval_dataset_prompt = tokenized_dataset_prompt["validation"].shuffle(seed=42).select(range(200))
# 3. 配置Prompt Tuning
prompt_tuning_config = PromptTuningConfig(
task_type=TaskType.CAUSAL_LM,
num_virtual_tokens=20, # 虚拟token的数量,即软提示的长度
prompt_tuning_init=PromptTuningInit.TEXT, # 软提示的初始化方式,可以是随机或基于文本
prompt_tuning_init_text="Classify this text as good or bad for sentiment analysis:", # 用于初始化软提示的文本
tokenizer_name_or_path=model_name_prompt, # 告知tokenizer用于初始化
)
# 4. 获取Prompt Tuning模型
model_prompt = get_peft_model(model_prompt, prompt_tuning_config)
model_prompt.print_trainable_parameters() # 打印可训练参数数量
# 5. 配置训练参数 (同LoRA示例)
training_args_prompt = TrainingArguments(
output_dir="./results_prompt_tuning",
num_train_epochs=3,
per_device_train_batch_size=8,
per_device_eval_batch_size=8,
warmup_steps=100,
weight_decay=0.01,
logging_dir='./logs_prompt_tuning',
logging_steps=50,
evaluation_strategy="epoch",
save_strategy="epoch",
load_best_model_at_end=True,
metric_for_best_model="eval_loss",
report_to="none"
)
# 6. 初始化Trainer并开始训练
trainer_prompt = Trainer(
model=model_prompt,
args=training_args_prompt,
train_dataset=small_train_dataset_prompt,
eval_dataset=small_eval_dataset_prompt,
tokenizer=tokenizer_prompt,
)
print("\n--- GPT-2 Prompt Tuning微调示例 ---")
if torch.cuda.is_available():
print(f"Using GPU: {torch.cuda.get_device_name(0)}")
else:
print("No GPU available, training on CPU (will be slow).")
trainer_prompt.train()
print("\nPrompt Tuning fine-tuning completed. Evaluation results:")
eval_results_prompt = trainer_prompt.evaluate()
print(eval_results_prompt)
# 7. 演示Prompt Tuning模型生成文本 (与LoRA类似,但内部处理了软提示)
print("\n--- Prompt Tuning模型文本生成示例 ---")
model_prompt.eval()
# 注意:这里prompt不需要包含用于初始化软提示的文本,模型会自行处理
prompt_for_gen = "The weather today is"
inputs = tokenizer_prompt(prompt_for_gen, return_tensors="pt").to(model_prompt.device)
with torch.no_grad():
outputs = model_prompt.generate(
**inputs,
max_new_tokens=50,
num_return_sequences=1,
do_sample=True,
temperature=0.7,
top_k=50,
top_p=0.95
)
generated_text_prompt = tokenizer_prompt.decode(outputs[0], skip_special_tokens=True)
print(f"Prompt: {prompt_for_gen}")
print(f"Generated text:\n{generated_text_prompt}")
代码说明:
PromptTuningConfig
定义了Prompt Tuning的参数,最重要的是num_virtual_tokens
(软提示的长度)。prompt_tuning_init
和prompt_tuning_init_text
允许你用有意义的文本来初始化软提示,这通常比随机初始化效果更好。get_peft_model
同样会自动修改模型,使其只训练软提示向量。你会发现可训练参数比LoRA更少。- 在生成文本时,用户输入无需包含初始化时使用的文本,
peft
库会在内部处理软提示的拼接。
4.2 大模型高效训练策略:突破资源瓶颈
大模型训练是一个资源密集型任务,需要大量的计算能力和显存。即使使用了PEFT方法,高效训练策略仍然至关重要。
4.2.1 分布式训练
核心思想: 将模型的训练过程分散到多个GPU或多台机器上,以并行处理数据和模型。
4.2.1.1 数据并行 (Data Parallelism)
原理详解:
- 在数据并行中,每个GPU都拥有模型的一个完整副本。
- 每个GPU接收不同批次的数据。
- 每个GPU独立计算梯度。
- 所有GPU的梯度会聚合(通常是求平均),然后用于更新所有GPU上的模型参数,确保所有模型副本保持同步。
- Pytorch实现:
torch.nn.DataParallel
(DP) 和torch.nn.parallel.DistributedDataParallel
(DDP)。推荐使用DDP,因为它效率更高,支持多机多卡,且避免了Python GIL(全局解释器锁)的限制。
DDP的优势:
- 效率高: 每个进程独立运行,减少了通信开销和GPU空闲时间。
- 支持多机多卡: 可以扩展到大规模集群。
- 灵活性: 支持更复杂的通信策略。
Python示例:使用DistributedDataParallel (DDP)
DDP通常需要通过torch.distributed.launch
或torchrun
脚本启动。这里提供一个核心代码片段,并解释如何运行。
创建一个名为ddp_example.py
的文件:
Python
import torch
import torch.nn as nn
import torch.optim as optim
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
import os
# 1. 初始化分布式环境
def setup(rank, world_size):
# 'env://' 表示从环境变量中获取 master_addr 和 master_port
os.environ['MASTER_ADDR'] = 'localhost'
os.environ['MASTER_PORT'] = '12355'
dist.init_process_group("nccl", rank=rank, world_size=world_size) # nccl是GPU的后端
def cleanup():
dist.destroy_process_group()
# 2. 定义一个简单的模型
class SimpleModel(nn.Module):
def __init__(self):
super(SimpleModel, self).__init__()
self.linear = nn.Linear(10, 1)
def forward(self, x):
return self.linear(x)
def train(rank, world_size):
setup(rank, world_size)
# 将模型移动到当前进程的GPU上
device = torch.device(f"cuda:{rank}")
model = SimpleModel().to(device)
# 使用DDP包装模型
ddp_model = DDP(model, device_ids=[rank])
optimizer = optim.SGD(ddp_model.parameters(), lr=0.01)
criterion = nn.MSELoss()
# 模拟数据
# 每个进程生成自己的数据
# 为了简化,我们只使用固定数据,实际中应使用分布式Sampler
input_data = torch.randn(16, 10).to(device) # Batch size 16 per GPU
target_data = torch.randn(16, 1).to(device)
print(f"Rank {rank}: Starting training on device {device}")
for epoch in range(5):
optimizer.zero_grad()
output = ddp_model(input_data)
loss = criterion(output, target_data)
loss.backward()
optimizer.step()
# 所有进程同步,确保日志输出顺序
dist.barrier()
if rank == 0: # 只在主进程打印
print(f"Epoch {epoch+1}, Rank {rank} Loss: {loss.item():.4f}")
cleanup()
if __name__ == "__main__":
world_size = torch.cuda.device_count() # 使用所有可用的GPU
if world_size == 0:
print("No CUDA devices found. DDP requires GPUs.")
exit()
print(f"Starting DDP training on {world_size} GPUs.")
# 启动多个进程
# torch.multiprocessing.spawn 会为每个GPU创建一个进程并调用 train 函数
torch.multiprocessing.spawn(train,
nprocs=world_size,
args=(world_size,),
join=True)
运行示例: 在终端中执行以下命令(假设你有2个GPU):
Bash
python -m torch.distributed.run --nproc_per_node=2 ddp_example.py
--nproc_per_node=2
:指定每个节点(这里是你的机器)使用2个进程,每个进程对应一个GPU。ddp_example.py
:你的训练脚本。
代码说明:
setup
:初始化分布式通信组,指定后端为nccl
(NVIDIA GPU推荐)。DDP(model, device_ids=[rank])
:将你的模型包装成DDP模型,它会自动处理梯度的同步和参数更新。dist.barrier()
:在某些操作后(如打印日志)用于同步所有进程,确保结果的正确显示。torch.multiprocessing.spawn
:在Python中启动多个进程,每个进程都在一个单独的GPU上运行训练函数。
4.2.1.2 模型并行 (Model Parallelism)
原理详解:
- 当单个GPU无法容纳整个模型时,模型并行会将模型的不同层或不同部分放置在不同的GPU上。
- 数据在这些GPU之间流水线式地传递,每个GPU只处理模型的一部分。
- 挑战: 引入通信开销和潜在的GPU空闲时间(流水线气泡)。
- 应用: 通常用于非常大的模型,例如GPT-3、Mixture of Experts (MoE) 模型。
- 常见策略:
- 层间并行 (Layer-wise Parallelism / Pipeline Parallelism): 将模型的连续层分到不同的GPU上,数据像流水线一样流过这些GPU。
- 层内并行 (Intra-layer Parallelism / Tensor Parallelism): 将单个层内部的计算(如大型矩阵乘法)分解到多个GPU上。
Python示例:概念性模型并行(Pipeline Parallelism)
由于模型并行实现复杂,这里只提供一个概念性的骨架,不包含可直接运行的完整代码。
Python
import torch
import torch.nn as nn
# 假设有两个GPU
device0 = torch.device("cuda:0")
device1 = torch.device("cuda:1")
class LargeModel(nn.Module):
def __init__(self):
super(LargeModel, self).__init__()
# 第一部分模型放在GPU 0
self.layer1 = nn.Linear(1000, 2000).to(device0)
self.relu1 = nn.ReLU().to(device0)
self.layer2 = nn.Linear(2000, 3000).to(device0)
self.relu2 = nn.ReLU().to(device0)
# 第二部分模型放在GPU 1
self.layer3 = nn.Linear(3000, 2000).to(device1)
self.relu3 = nn.ReLU().to(device1)
self.layer4 = nn.Linear(2000, 1000).to(device1)
def forward(self, x):
# 数据在不同设备之间传递
x = self.layer1(x.to(device0))
x = self.relu1(x)
x = self.layer2(x)
x = self.relu2(x)
x = self.layer3(x.to(device1)) # 移动数据到GPU 1
x = self.relu3(x)
x = self.layer4(x)
return x
# 实例化模型 (如果GPU显存足够的话)
# model = LargeModel()
# dummy_input = torch.randn(1, 1000)
# output = model(dummy_input)
# print(output.shape)
print("\n--- 模型并行概念示例 ---")
print("模型并行需要手动将模型层分配到不同GPU,并在前向传播中进行数据传输。")
print("这比数据并行复杂,通常需要更高级的框架支持(如Megatron-LM, DeepSpeed)。")
代码说明:
- 模型被手动拆分为两部分,并分别放置在不同的GPU上。
x.to(deviceX)
:在前向传播中,数据需要在不同GPU之间显式移动。- 实际应用: 像Megatron-LM、DeepSpeed等框架提供了更高级的抽象来处理模型并行,包括自动流水线调度和通信优化,以最小化GPU空闲时间。
4.2.2 混合精度训练 (Mixed Precision Training)
核心思想: 在训练过程中同时使用单精度(FP32)和半精度(FP16或BFloat16)浮点数。模型的权重通常以FP32存储,但在前向传播和反向传播的某些计算中使用FP16,以减少显存占用和加速计算。
原理详解:
- FP16的优势: 存储空间减半,NVIDIA Tensor Core可以显著加速FP16矩阵乘法。
- 挑战: FP16表示范围比FP32小,容易出现梯度下溢(underflow)或上溢(overflow)。
- 解决方案:
- 梯度缩放 (Gradient Scaling): 在反向传播之前,将损失乘以一个较大的缩放因子(
loss * scale
),以防止FP16梯度过小而变为零。计算完成后,再将梯度除以该缩放因子(scaled_gradient / scale
)。 - FP32主权重副本: 模型的权重仍然以FP32存储,梯度更新在FP32进行,可以避免累积舍入误差。
- 梯度缩放 (Gradient Scaling): 在反向传播之前,将损失乘以一个较大的缩放因子(
Python示例:使用torch.cuda.amp.autocast
和GradScaler
PyTorch提供了torch.cuda.amp
(Automatic Mixed Precision)模块,极大地简化了混合精度训练的实现。
Python
import torch
import torch.nn as nn
import torch.optim as optim
from torch.cuda.amp import autocast, GradScaler # 自动混合精度核心模块
# 1. 定义一个简单的模型
class SimpleModelAmp(nn.Module):
def __init__(self):
super(SimpleModelAmp, self).__init__()
self.linear1 = nn.Linear(100, 50)
self.relu = nn.ReLU()
self.linear2 = nn.Linear(50, 1)
def forward(self, x):
return self.linear2(self.relu(self.linear1(x)))
# 2. 模拟数据
input_data_amp = torch.randn(64, 100).cuda() # 确保数据在GPU上
target_data_amp = torch.randn(64, 1).cuda()
# 3. 初始化模型、优化器、损失函数
model_amp = SimpleModelAmp().cuda()
optimizer_amp = optim.SGD(model_amp.parameters(), lr=0.01)
criterion_amp = nn.MSELoss()
# 4. 初始化梯度缩放器
scaler = GradScaler()
print("\n--- 混合精度训练示例 ---")
if not torch.cuda.is_available():
print("CUDA not available. Mixed precision training requires a CUDA-enabled GPU.")
else:
print(f"Using GPU: {torch.cuda.get_device_name(0)}")
for epoch in range(50):
optimizer_amp.zero_grad()
# 使用autocast上下文管理器,自动将操作转换为FP16
with autocast():
output_amp = model_amp(input_data_amp)
loss_amp = criterion_amp(output_amp, target_data_amp)
# 1. 缩放损失,防止FP16梯度下溢
scaler.scale(loss_amp).backward()
# 2. 更新模型参数 (在缩放的梯度上)
# scaler.step() 会首先对梯度进行unscale,然后检查是否有NaN/Inf,
# 如果没有,则调用optimizer.step()更新权重
scaler.step(optimizer_amp)
# 3. 更新缩放因子,为下一次迭代做准备
scaler.update()
if (epoch + 1) % 10 == 0:
print(f"Epoch {epoch+1}, Loss: {loss_amp.item():.4f}")
print("Mixed precision training completed.")
代码说明:
autocast()
:这是一个上下文管理器,它会自动在其中执行的CUDA操作中选择合适的精度(FP16或FP32)。它会为某些操作使用FP16,而为另一些可能导致精度问题的操作(如log_softmax)使用FP32。GradScaler()
:用于梯度缩放。scaler.scale(loss_amp).backward()
:在反向传播前对损失进行缩放。scaler.step(optimizer_amp)
:根据缩放后的梯度更新模型参数。它会在内部进行梯度解除缩放和NaN/Inf检查。scaler.update()
:更新缩放因子,根据梯度是否出现NaN/Inf动态调整。
4.2.3 梯度累积 (Gradient Accumulation)
核心思想: 在不增加GPU显存占用的前提下,模拟更大的Batch Size。它通过多次小批量的前向和反向传播,累积梯度,然后才执行一次参数更新。
原理详解: 正常情况下,我们每计算一个Batch的损失,就会进行一次反向传播和一次参数更新。梯度累积的流程是:
- 定义一个
accumulation_steps
(累积步数)。 - 在一个
accumulation_steps
循环中,每次进行前向传播和反向传播,但不立即更新参数,而是将梯度累加到一起。 - 每隔
accumulation_steps
步,或当一个完整的逻辑Batch(batch_size * accumulation_steps
)计算完成后,执行一次参数更新,并清零梯度。
优点:
- 突破显存限制: 允许在GPU显存不足以容纳大Batch Size时,仍然能够实现大Batch Size的训练效果。
- 模拟大Batch Size: 大Batch Size通常有助于模型收敛和提高泛化能力。
Python示例:实现梯度累积
Python
import torch
import torch.nn as nn
import torch.optim as optim
# 1. 定义一个简单的模型
class SimpleModelAccum(nn.Module):
def __init__(self):
super(SimpleModelAccum, self).__init__()
self.linear1 = nn.Linear(10, 5)
self.relu = nn.ReLU()
self.linear2 = nn.Linear(5, 1)
def forward(self, x):
return self.linear2(self.relu(self.linear1(x)))
# 2. 模拟数据 (这里为了演示,只生成一个大batch)
# 实际中会从DataLoader分批次读取
dummy_data_accum = torch.randn(100, 10) # 100个样本
dummy_target_accum = torch.randn(100, 1)
# 3. 初始化模型、优化器、损失函数
model_accum = SimpleModelAccum()
optimizer_accum = optim.SGD(model_accum.parameters(), lr=0.01)
criterion_accum = nn.MSELoss()
# 4. 定义梯度累积参数
accumulation_steps = 4 # 累积4个小批次的梯度再更新一次
print("\n--- 梯度累积示例 ---")
# 模拟 DataLoader 分批次
batch_size_per_step = 25 # 每个小批次25个样本
total_epochs = 5
for epoch in range(total_epochs):
for i in range(0, dummy_data_accum.size(0), batch_size_per_step):
input_batch = dummy_data_accum[i:i+batch_size_per_step]
target_batch = dummy_target_accum[i:i+batch_size_per_step]
output = model_accum(input_batch)
loss = criterion_accum(output, target_batch)
# 损失除以累积步数,保证每次更新的梯度平均值是正确的
loss = loss / accumulation_steps
# 反向传播,累积梯度
loss.backward()
# 每 accumulation_steps 步或数据结束时更新参数
if (i // batch_size_per_step + 1) % accumulation_steps == 0 or (i + batch_size_per_step >= dummy_data_accum.size(0)):
optimizer_accum.step() # 更新参数
optimizer_accum.zero_grad() # 清零梯度
print(f"Epoch {epoch+1}, Last Mini-batch Loss (scaled): {loss.item():.4f}")
print("Gradient accumulation training completed.")
代码说明:
accumulation_steps
:控制多少个小批次后才进行一次参数更新。loss = loss / accumulation_steps
:对每个小批次的损失进行缩放。这是关键,因为梯度是累加的,为了保持学习率的有效性,我们需要将损失也平均化。loss.backward()
:每次迭代都进行反向传播,将梯度累积到参数的.grad
属性中。optimizer_accum.step()
和optimizer_accum.zero_grad()
:只在达到accumulation_steps
后才执行参数更新和梯度清零。
4.2.4 检查点(Checkpointing)
核心思想: 在训练过程中,通过牺牲少量计算时间来换取显存,允许训练比GPU显存容量更大的模型。它在前向传播时只保留计算图的关键部分(而不是所有中间激活值),在反向传播时,通过重新计算来获取所需的中间激活值。
原理详解: 通常,为了计算反向传播所需的梯度,框架需要在前向传播时存储所有的中间激活值。对于大模型,这些激活值可能会占用巨大的显存。检查点技术(PyTorch中的torch.utils.checkpoint
)通过以下方式优化:
- 在前向传播时,对于被标记为"检查点"的模块,只存储其输入和输出。模块内部的中间激活值被丢弃。
- 在反向传播时,当需要计算某个检查点模块的梯度时,它会重新执行该模块的前向传播,以重新生成所需的中间激活值,然后再进行反向传播。
优点:
- 显著减少显存占用: 允许训练更大的模型。
- 透明性: 用户只需在代码中添加几行,无需修改模型架构。
Python示例:使用torch.utils.checkpoint
Python
import torch
import torch.nn as nn
import torch.utils.checkpoint as checkpoint
# 1. 定义一个深度模型
class DeepModel(nn.Module):
def __init__(self):
super(DeepModel, self).__init__()
self.layers = nn.ModuleList([
nn.Sequential(nn.Linear(100, 100), nn.ReLU()) for _ in range(20) # 20层
])
self.output_layer = nn.Linear(100, 1)
def forward(self, x):
for layer in self.layers:
# 将每个模块包装在checkpoint中
# 注意:如果layer内部有可变状态(如Batchnorm的running_mean),可能需要额外处理
# 这里的简单线性+ReLU没有这个问题
x = checkpoint.checkpoint(layer, x)
x = self.output_layer(x)
return x
# 2. 模拟数据
input_data_cp = torch.randn(1, 100).cuda()
target_data_cp = torch.randn(1, 1).cuda()
# 3. 初始化模型、优化器、损失函数
model_cp = DeepModel().cuda()
optimizer_cp = optim.SGD(model_cp.parameters(), lr=0.01)
criterion_cp = nn.MSELoss()
print("\n--- 检查点(Checkpointing)示例 ---")
if not torch.cuda.is_available():
print("CUDA not available. Checkpointing is most useful on GPU.")
else:
print(f"Using GPU: {torch.cuda.get_device_name(0)}")
# 正常训练,但显存占用会降低
try:
optimizer_cp.zero_grad()
output_cp = model_cp(input_data_cp)
loss_cp = criterion_cp(output_cp, target_data_cp)
loss_cp.backward()
optimizer_cp.step()
print(f"Loss with checkpointing: {loss_cp.item():.4f}")
print("Checkpointing successfully used to reduce memory.")
except RuntimeError as e:
print(f"Error during checkpointing: {e}")
print("This might happen if the model is still too large or other issues.")
print("Checkpointing usage demonstrated.")
代码说明:
checkpoint.checkpoint(layer, x)
:将layer
模块的前向传播包装在此函数中。它会智能地决定是否存储中间激活值,并在需要时重新计算。- 这种方法对于非常深的神经网络(如大型Transformer)特别有用,因为这些模型会产生大量的中间激活值。
总结
本章深入探讨了大模型的微调策略和高效训练技术,帮助我们应对大模型带来的巨大资源挑战。
- 我们了解到全参数微调虽然效果上限高,但资源消耗巨大,且容易遗忘。
- 为了解决这些问题,参数高效微调(PEFT) 方法应运而生。其中,LoRA 通过低秩近似权重更新,Prompt Tuning/Prefix Tuning 通过优化软提示,都极大地减少了可训练参数,降低了资源成本,并有效避免了灾难性遗忘。
- 此外,我们还学习了多种高效训练策略 :
- 分布式训练(数据并行DDP和模型并行) 能够将训练任务分散到多个设备,加速训练。
- 混合精度训练(AMP) 结合FP16和FP32,显著降低显存和加速计算。
- 梯度累积 允许在显存受限的情况下模拟更大的Batch Size。
- 检查点(Checkpointing) 通过牺牲计算时间来换取显存,使训练更大的模型成为可能。
掌握这些微调和高效训练技术,是释放大模型潜力的关键。它们使得在有限资源下也能训练和部署强大的AI模型,推动AI技术在更广泛的领域落地。