尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

多卡大模型微调实战:DeepSpeed ZeRO-2与Transformers Trainer集成指南

多卡大模型微调实战:DeepSpeed ZeRO-2与Transformers Trainer集成指南 简介在深度学习领域大规模语言模型的训练与微调常面临显存不足的挑战。其核心原理在于模型参数、优化器状态、梯度和激活值对显存的巨大需求传统单卡训练难以承载。为解决此问题分布式训练技术应运而生通过模型并行、数据并行及优化器状态分区等技术实现显存压力的分散与计算资源的扩展从而提升训练效率并降低硬件门槛。其中DeepSpeed的ZeRO优化器系列通过消除冗余状态实现了近乎线性的显存节省具有极高的技术价值。结合Hugging Face Transformers库的Trainer高层抽象开发者能以声明式配置轻松部署多卡微调流程广泛应用于大模型指令微调、领域适配等场景。本文以Qwen2-7B模型为例详细解析了基于DeepSpeed ZeRO-2和CPU Offload的实战配置帮助读者在有限的多卡环境下高效完成大模型微调任务。1. 从单卡到多卡大模型微调的显存困境与破局思路如果你尝试过在单张消费级显卡上微调一个参数量超过70亿的大模型比如Llama 2-7B或者Qwen-7B大概率会和我一样在运行脚本后几秒钟内就收到一个令人沮丧的“CUDA out of memory”错误。这个错误的本质是模型参数、优化器状态、梯度以及激活值Activations对显存的巨大需求轻易就撑爆了单张显卡的容量。以全参数微调Full Fine-tuning为例一个7B参数的模型仅加载为FP16精度就需要大约14GB显存。这还没算上优化器状态例如Adam优化器每个参数需要存储动量momentum和方差variance在混合精度训练下通常也是FP16所以每个参数额外需要4字节以及前向传播过程中产生的、用于反向传播计算梯度的激活值。这些加起来轻松超过24GB让RTX 4090这样的消费卡皇也力不从心。于是多卡训练成为了必然选择。但多卡并非简单地把数据和模型复制几份。最朴素的数据并行Data Parallelism, DP要求每个GPU上都保存一份完整的模型副本显存问题并没有解决只是通过增大批次大小batch size来加速。我们需要的是模型并行Model Parallelism, MP或更高级的优化器状态切分技术将模型或优化器的状态分布到多张卡上从而突破单卡显存限制。手动实现这些技术复杂度极高涉及到梯度同步、参数聚合、通信优化等一系列底层细节。这就是DeepSpeed登场的原因。它是由微软开发的一个深度学习优化库核心目标就是让大规模模型训练变得简单高效。它提供了一系列强大的优化技术最著名的便是ZeROZero Redundancy Optimizer系列。ZeRO通过在不同阶段Stage 1, 2, 3消除数据并行中的冗余状态优化器状态、梯度、模型参数将它们分区到各个GPU上从而实现了近乎线性的显存节省与多卡扩展效率。简单来说用了DeepSpeed你就能用有限的显卡资源去微调一个原本“装不下”的大模型。而Hugging Face的Transformers库中的Trainer类则是另一个“懒人”福音。它封装了训练循环、评估、日志记录、检查点保存等繁琐流程提供了统一的API。当DeepSpeed与Trainer结合就形成了一套“声明式”的训练方案你不需要写复杂的分发和同步代码只需要在Trainer的初始化参数中传入一个DeepSpeed配置文件deepspeed_config.json剩下的分布式训练细节包括ZeRO优化、梯度累积、混合精度训练等都由这两个框架在底层自动协作完成。这极大地降低了多卡微调大模型的技术门槛让我们能够更专注于模型结构、数据和任务本身。接下来我将以一个具体的微调场景为例手把手带你走通这套高效流程。2. 环境搭建与核心组件选型为什么是它们在开始写代码之前搭建一个稳定、兼容的环境是成功的第一步。大模型训练对环境的依赖比较敏感尤其是CUDA、PyTorch和DeepSpeed版本之间的匹配。2.1 基础环境配置CUDA、PyTorch与DeepSpeed首先明确你的硬件。假设我们拥有2-4张NVIDIA RTX 409024GB显存目标是微调一个类似Qwen-7B这样的模型。对应的软件栈选择如下CUDA Toolkit: 推荐使用CUDA 11.8。这是一个在稳定性和新特性支持上比较平衡的版本对大多数深度学习框架兼容性好。可以通过nvidia-smi命令查看驱动支持的CUDA最高版本但安装的CUDA Toolkit版本可以低于此值。PyTorch: 必须安装与CUDA 11.8对应的PyTorch版本。访问PyTorch官网获取安装命令。例如pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118这里的关键是cu118这个后缀它确保了PyTorch编译时链接的是CUDA 11.8的库。DeepSpeed: DeepSpeed的安装需要额外注意因为它包含需要编译的C/CUDA扩展。推荐使用预编译的wheel文件或者从源码安装。为了获得最好的兼容性我通常使用以下命令pip install deepspeed如果安装后遇到运行错误可能需要安装特定版本的ninja构建工具和匹配的CUDA开发包nvcc。一个更稳妥的方式是使用PyTorch对应的环境直接从源码编译DeepSpeed但这步骤稍复杂。对于大多数用户上述pip安装足以应对微调场景。注意环境冲突是最大的“坑”。务必确保虚拟环境如conda或venv的纯净避免多个版本的PyTorch或CUDA混杂。一个简单的验证方法是在Python中执行import torch; print(torch.__version__, torch.cuda.is_available(), torch.cuda.get_device_capability())确认版本正确、CUDA可用且计算能力符合预期。2.2 Transformers与Accelerate生态粘合剂Hugging Face的Transformers库是我们的模型来源和训练框架基础。我们需要安装它以及配套的datasets库用于数据加载和accelerate库一个简化分布式训练的库Trainer内部会用到。pip install transformers datasets accelerateTrainer类在Transformers库中。Accelerate库虽然我们不会直接调用它的高级API但它是Trainer和DeepSpeed能够无缝协作的底层支撑之一负责抽象化不同的分布式后端如DeepSpeed、PyTorch DDP。2.3 模型与数据选择以Qwen2-7B-Instruct为例为了演示的通用性我们选择Qwen2-7B-Instruct模型。Qwen系列是优秀的开源双语大模型其Instruct版本经过指令微调适合进行下游任务的继续微调。数据方面我们假设有一个JSON格式的指令微调数据集每条数据包含instruction指令、input输入可选和output输出字段。例如一个翻译任务的数据可能长这样{ instruction: 请将以下英文翻译成中文。, input: The rapid development of artificial intelligence is reshaping every industry., output: 人工智能的快速发展正在重塑每一个行业。 }这种格式与Alpaca数据集类似被广泛支持。我们将使用Transformers库的AutoTokenizer和AutoModelForCausalLM来加载模型和分词器。选择因果语言模型Causal LM是因为指令微调通常被建模为下一个词预测任务。3. DeepSpeed配置解析ZeRO Stage 2与3的实战抉择DeepSpeed的强大和灵活集中体现在它的配置文件JSON格式上。这个文件告诉DeepSpeed和Trainer如何分配显存、如何进行通信优化。对于多卡微调最常用的是ZeRO Stage 2和Stage 3。我们的配置将基于ZeRO Stage 2因为它提供了显存和速度的良好平衡。3.1 配置文件ds_config.json逐行解读创建一个名为ds_config.json的文件内容如下。我将逐块解释其含义{ fp16: { enabled: auto, loss_scale: 0, loss_scale_window: 1000, initial_scale_power: 16, hysteresis: 2, min_loss_scale: 1 }, bf16: { enabled: false }, optimizer: { type: AdamW, params: { lr: auto, betas: auto, eps: auto, weight_decay: auto } }, scheduler: { type: WarmupLR, params: { warmup_min_lr: auto, warmup_max_lr: auto, warmup_num_steps: auto } }, zero_optimization: { stage: 2, offload_optimizer: { device: cpu, pin_memory: true }, allgather_partitions: true, allgather_bucket_size: 2e8, overlap_comm: true, reduce_scatter: true, reduce_bucket_size: 2e8, contiguous_gradients: true }, gradient_accumulation_steps: auto, gradient_clipping: auto, train_batch_size: auto, train_micro_batch_size_per_gpu: auto, steps_per_print: 10, wall_clock_breakdown: false }fp16/bf16: 混合精度训练配置。fp16使用半精度浮点数能显著减少显存占用并加速计算。enabled: auto表示由Trainer根据情况决定是否开启如果硬件支持。loss_scale用于防止梯度下溢设为0启用动态损失缩放Dynamic Loss Scaling这是更稳定的做法。如果你的显卡如Ampere架构的A100、RTX 30/40系列支持bfloat16bf16可以将bf16的enabled设为true并关闭fp16。bf16动态范围更大训练稳定性通常更好。optimizer和scheduler: 这里设为auto意味着DeepSpeed会使用你在创建Trainer时传入的优化器和学习率调度器。这是一种便捷的做法保持了配置的单一入口。zero_optimization: 核心部分。stage: 2: 使用ZeRO第二阶段。在这个阶段优化器状态Optimizer States和梯度Gradients被分区存储在不同的GPU上每个GPU只负责更新自己分区内的参数对应的部分。模型参数Parameters仍然在每个GPU上保留完整副本。Stage 2能有效减少显存占用主要是优化器状态和梯度同时通信开销相对Stage 3较小。offload_optimizer: 这是“显存不够内存来凑”的利器。将优化器状态卸载到CPU内存GPU上只保留当前计算所需的梯度。pin_memory: true可以加速CPU到GPU的数据传输。对于显存紧张的消费级显卡这个选项是能微调更大模型的关键。代价是会增加CPU-GPU之间的数据交换带来一定的速度损失。allgather_partitions,reduce_scatter: ZeRO-2的前向和反向传播通信原语。保持为true即可。allgather_bucket_size和reduce_bucket_size: 通信桶大小。梯度/参数在通信前会被打包到“桶”里以减少通信次数。2e8200MB是一个常用值。太大会占用更多显存作为缓冲区太小会增加通信次数。可以根据实际情况微调。overlap_comm: 是否重叠通信和计算。开启后DeepSpeed会尝试在计算的同时进行梯度通信以隐藏通信延迟提升效率。contiguous_gradients: 在反向传播前将梯度复制到连续的缓冲区。这可以使得reduce-scatter通信更高效建议开启。gradient_accumulation_steps等设为auto: 这些值将由Trainer的初始化参数决定DeepSpeed配置中设为auto避免了重复配置。3.2 Stage 2 vs Stage 3如何选择ZeRO Stage 2如上所述分区优化器状态和梯度。这是微调场景最推荐的选择。它在显存节省和训练速度之间取得了很好的平衡实现简单兼容性好。配合offload_optimizer可以在4张24GB卡上全参微调13B甚至20B级别的模型。ZeRO Stage 3进一步将模型参数也进行分区每个GPU只保存一部分模型参数。这是显存节省的终极手段可以训练千亿参数的模型。但是它带来了更频繁的通信前向和反向传播都需要聚合参数训练速度会明显慢于Stage 2且配置更复杂。对于百亿参数以下的模型微调除非显卡数量极少如只有2张卡且模型极大否则不建议首选Stage 3。我们的配置选择了Stage 2 CPU Offload这是一个在有限资源下追求最大模型容量的务实方案。4. 构建训练脚本Trainer与DeepSpeed的集成实战有了配置接下来就是编写训练脚本。我们将创建一个train.py文件。整个过程分为数据准备、模型加载、Trainer配置和启动训练。4.1 数据预处理与Dataset构建首先加载并处理我们的指令数据。from datasets import load_dataset from transformers import AutoTokenizer, DataCollatorForSeq2Seq import torch model_name Qwen/Qwen2-7B-Instruct # 以Qwen2为例 tokenizer AutoTokenizer.from_pretrained(model_name, trust_remote_codeTrue) # 注意Qwen需要trust_remote_code tokenizer.pad_token tokenizer.eos_token # 将pad_token设置为eos_token这是训练Causal LM的常见做法 def preprocess_function(examples): # 将指令、输入、输出拼接成模型输入的格式 # 格式: |im_start|system\nYou are a helpful assistant.|im_end|\n|im_start|user\n{instruction}\n{input}|im_end|\n|im_start|assistant\n{output}|im_end| # 为了简化我们使用一个通用的对话模板。实际应根据模型本身的对话模板来。 # Qwen2有特定的chat template我们可以直接使用tokenizer.apply_chat_template # 但这里为了清晰我们手动构造一个简单版本。更推荐使用tokenizer的chat_template。 prompts [] for instr, inp, outp in zip(examples[instruction], examples[input], examples[output]): if inp: message fInstruction: {instr}\nInput: {inp}\nResponse: {outp} else: message fInstruction: {instr}\nResponse: {outp} prompts.append(message) # 对文本进行分词并添加标签labels model_inputs tokenizer(prompts, max_length512, truncationTrue, paddingmax_length) # 创建标签。对于因果语言模型标签就是输入序列本身但需要忽略掉padding部分和提示词部分仅对响应部分计算损失。 # 这里我们采用一种简单策略将整个序列的标签设置为与输入ID相同然后在计算损失时通过attention_mask忽略padding。 # 更精细的做法是将prompt部分的标签设置为-100在PyTorch的CrossEntropyLoss中会被忽略。 labels model_inputs[input_ids].copy() model_inputs[labels] labels return model_inputs # 加载数据集假设是本地JSON文件 dataset load_dataset(json, data_filesyour_data.json) tokenized_dataset dataset.map(preprocess_function, batchedTrue, remove_columnsdataset[train].column_names) # 数据收集器负责将一批数据动态padding到相同长度 data_collator DataCollatorForSeq2Seq( tokenizertokenizer, paddingTrue, return_tensorspt )实操心得数据预处理是微调成功的关键也是最容易出错的地方。核心在于labels的构建。对于指令微调我们通常只希望模型学习“回答”的部分而不希望它学习“问题”的部分。因此更标准的做法是在拼接的文本中将instruction和input对应的token在labels中标记为-100仅保留output部分的token作为有效标签。这需要根据你的数据格式和模型对话模板仔细设计。上述简化版本可能会导致模型学习效率稍低但作为示例更易于理解。4.2 加载模型与配置TrainingArguments接下来加载模型并定义训练参数。from transformers import AutoModelForCausalLM, TrainingArguments # 加载模型。使用low_cpu_mem_usageTrue可以节省加载时的内存。 # torch_dtypetorch.float16 指定模型以半精度加载节省显存。 model AutoModelForCausalLM.from_pretrained( model_name, torch_dtypetorch.float16, low_cpu_mem_usageTrue, trust_remote_codeTrue # Qwen模型需要 ) # 定义训练参数这是与DeepSpeed配置对接的关键 training_args TrainingArguments( output_dir./qwen2-7b-finetuned, # 输出目录 num_train_epochs3, # 训练轮数 per_device_train_batch_size2, # **重要**每个GPU上的批次大小。这是“micro batch size”。 gradient_accumulation_steps8, # **重要**梯度累积步数。 # 全局批次大小 per_device_train_batch_size * gradient_accumulation_steps * GPU数量 # 例如2 * 8 * 4 64 warmup_steps100, # 学习率预热步数 logging_steps10, # 日志记录间隔 save_strategyepoch, # 按轮次保存模型 save_total_limit2, # 最多保存2个检查点 learning_rate2e-5, # 学习率 weight_decay0.01, # 权重衰减 fp16True, # 启用FP16混合精度训练与DeepSpeed配置对应 deepspeed./ds_config.json, # **关键**指定DeepSpeed配置文件路径 remove_unused_columnsFalse, # 数据预处理中已移除这里设为False report_tonone, # 禁用wandb/tensorboard等如需可改为wandb )参数精讲per_device_train_batch_size这是每个GPU前向传播一次处理的样本数。受限于单卡显存这个值往往很小比如1或2。gradient_accumulation_steps梯度累积步数。假设为8意味着模型会进行8次前向传播和反向传播累积8次的梯度然后再进行一次优化器更新和参数同步。这等效于将有效的批次大小扩大了8倍但显存占用主要取决于per_device_train_batch_size。这是解决“显存小想用大batch”的核心技巧。fp16必须与DeepSpeed配置文件中的fp16.enabled设置保持一致。如果DeepSpeed配置中启用了bf16这里则应设置bf16True。deepspeed这个参数就是让Trainer启用DeepSpeed引擎的开关。Trainer会读取这个JSON文件并据此初始化DeepSpeed。4.3 初始化Trainer并启动训练最后将所有组件组装到Trainer中并开始训练。from transformers import Trainer trainer Trainer( modelmodel, argstraining_args, train_datasettokenized_dataset[train], data_collatordata_collator, tokenizertokenizer, ) # 开始训练 trainer.train()当执行trainer.train()时会发生以下事情Trainer检测到deepspeed参数会调用accelerate的DeepSpeedPlugin。DeepSpeed引擎根据配置文件初始化对模型、优化器进行包装。训练循环开始。在每个步骤中DeepSpeed负责将数据分发到各GPU执行ZeRO-2的通信逻辑如梯度的reduce-scatter处理混合精度训练并管理可能存在的CPU Offload。日志会显示损失、学习率等信息并按照设定保存检查点。启动训练的命令也很简单使用accelerate launch或deepspeed命令均可。推荐使用后者因为它能更好地处理DeepSpeed的启动环境。deepspeed --num_gpus4 train.py这里的--num_gpus4指定使用4张GPU。DeepSpeed会自动处理进程启动和分布式环境初始化。5. 实战中的关键技巧与排坑指南按照上述步骤你应该能成功启动训练。但在实际操作中总会遇到一些“坑”。下面分享几个我踩过并总结出的关键技巧。5.1 显存监控与per_device_train_batch_size调优训练启动后第一件事就是用nvidia-smi监控显存使用。理想情况下每张卡的显存使用率应该基本均衡且留有少量余量例如22GB/24GB。如果出现OOMOut-Of-Memory你需要调整以下参数按优先级排序降低per_device_train_batch_size这是最直接有效的方法。从1开始尝试。启用或调整CPU Offload在我们的配置中已经启用了offload_optimizer。如果还OOM可以尝试offload_params将模型参数也卸载到CPU但这会显著降低训练速度。增加gradient_accumulation_steps在保持per_device_train_batch_size不变的情况下增加累积步数可以增大有效批次大小而不增加峰值显存。但注意这不会减少激活值activation的显存占用而激活值占用常常是大头。启用激活检查点Gradient Checkpointing也称为“重计算”。它以前向传播时重新计算中间激活值为代价换取大幅降低保存激活值所需的显存。对于深层模型效果显著。可以在加载模型后添加一行代码model.gradient_checkpointing_enable()注意这会增加约30%的计算时间但可能让你将批次大小翻倍。5.2 学习率与优化器状态在DeepSpeed配置中我们将优化器和学习率调度器设为auto这意味着实际使用的是TrainingArguments中定义的learning_rate和默认的AdamW优化器。这里有一个细节当使用ZeRO Stage 2或3时优化器状态被分区每个GPU上的优化器只看到一部分参数。但这不影响学习率的设置你仍然可以像单卡训练一样设置学习率如2e-5。DeepSpeed会在内部处理好分区优化器的更新逻辑。5.3 保存与加载DeepSpeed检查点使用DeepSpeed后模型的保存和加载与普通Trainer略有不同。Trainer保存的检查点文件夹里你会看到类似pytorch_model.bin可能很小、zero_to_fp32.py脚本以及一个global_stepXX的文件夹里面存放着分区后的优化器状态和模型参数。如何加载微调后的模型进行推理最可靠的方法是使用DeepSpeed提供的zero_to_fp32.py脚本将分区检查点合并成一个完整的FP32模型文件。python ./output_dir/zero_to_fp32.py ./output_dir ./output_dir/consolidated_model.bin然后你可以像加载普通PyTorch模型一样加载consolidated_model.binfrom transformers import AutoModelForCausalLM, AutoTokenizer import torch model AutoModelForCausalLM.from_pretrained( Qwen/Qwen2-7B-Instruct, # 原始模型目录用于加载配置文件 state_dicttorch.load(./output_dir/consolidated_model.bin), torch_dtypetorch.float16, trust_remote_codeTrue )或者你也可以直接使用Trainer的.save_model()方法并指定save_only_modelTrue但根据我的经验使用上述脚本合并是最不容易出错的方式。5.4 常见错误与解决方案错误CUDA error: an illegal memory access was encountered这通常是环境问题。确保CUDA、PyTorch、DeepSpeed版本兼容。尝试重启或使用一个干净的conda环境重新安装。也可能是模型或数据本身有问题尝试用极小的数据和批次测试。错误训练速度异常缓慢首先检查CPU Offload。如果配置了offload_optimizer到CPU速度变慢是正常的。可以尝试关闭它删除配置中offload_optimizer部分看是否因显存不足而OOM。如果必须使用Offload可以尝试使用NVMe SSD并开启pin_memory来缓解。其次检查overlap_comm是否开启它有助于提升效率。警告Gradient overflow. Skipping step, loss scaler 0 reducing loss scale to...这是混合精度训练中动态损失缩放的正常现象。当梯度出现NaN或Inf时损失缩放系数会自动减小。如果频繁出现可能需要降低学习率或者检查数据中是否有异常值如非常长的序列。进程挂起或不启动多卡训练涉及进程间通信。确保机器上所有GPU都可以被进程访问无其他进程独占并且防火墙没有屏蔽通信端口。使用deepspeed启动器通常比手动设置torch.distributed.launch更可靠。6. 进阶从全参微调到高效参数微调LoRA全参数微调Full Fine-tuning效果通常最好但资源消耗也最大。如果你的目标是快速适配某个特定任务且资源有限高效参数微调Parameter-Efficient Fine-Tuning, PEFT方法是更好的选择其中LoRALow-Rank Adaptation最为流行。LoRA的核心思想是冻结预训练模型的原有权重只训练注入到模型中的、低秩分解的适配器Adapter权重。这能减少训练参数量达90%以上从而大幅降低显存需求和加快训练速度。好消息是Trainer和DeepSpeed同样完美支持LoRA。你需要安装peft库pip install peft然后在训练脚本中用几行代码将原模型包装为LoRA模型from peft import LoraConfig, get_peft_model, TaskType # 定义LoRA配置 lora_config LoraConfig( task_typeTaskType.CAUSAL_LM, # 因果语言模型任务 r8, # LoRA的秩rank越小参数量越少通常8或16 lora_alpha32, # 缩放因子 lora_dropout0.1, target_modules[q_proj, k_proj, v_proj, o_proj], # 针对Transformer的哪些模块注入LoRA。Qwen2的注意力层叫q_proj等。 biasnone ) # 将原模型转换为LoRA模型 model get_peft_model(model, lora_config) model.print_trainable_parameters() # 打印可训练参数量会发现只占原模型很小一部分接下来的操作和全参微调完全一样。你仍然使用相同的TrainingArguments和deepspeed_config.json。因为可训练参数急剧减少你甚至可以使用更大的per_device_train_batch_size或者关闭CPU Offload来获得更快的训练速度。使用LoRADeepSpeedTrainer你可以在单张24GB显卡上轻松微调7B甚至13B的模型而全参微调可能需要4张卡。推理时只需将训练好的LoRA权重与基础模型合并即可。7. 性能监控与调试让训练过程透明化训练启动后如何知道一切是否正常DeepSpeed提供了一些监控工具。日志输出在TrainingArguments中设置logging_steps并在DeepSpeed配置中设置steps_per_print: 10你可以在控制台看到损失、学习率、吞吐量samples/sec等信息。关注吞吐量可以评估训练效率。DeepSpeed时间线分析在配置文件中设置wall_clock_breakdown: true训练结束后会生成一个时间线文件可以用DeepSpeed自带的工具可视化分析训练过程中计算、通信、IO等各部分的时间占比帮助定位性能瓶颈。显存分析在训练脚本中可以定期插入torch.cuda.memory_summary()来打印详细的显存分配情况。或者使用nvtop、gpustat等命令行工具实时监控。一个健康的训练过程其损失曲线应该平滑下降吞吐量稳定各GPU显存使用均衡。如果发现某张卡显存明显高于其他卡或者吞吐量波动巨大可能需要检查数据加载是否均衡或者是否存在通信问题。这套由DeepSpeed提供分布式优化引擎、Trainer提供高层训练抽象、PEFT提供高效微调方法的组合拳已经成为当前微调大型开源语言模型的事实标准流程。它屏蔽了底层的复杂性让研究者与开发者能更专注于模型与算法本身。从配置一个JSON文件开始你已经拥有了在消费级多卡服务器上驾驭数十亿参数模型的能力。本文还有配套的精品资源点击获取
返回列表