简介:这份资源面向希望入门大模型多卡微调的人工智能开发者与研究者,围绕Deepspeed与PyTorch Trainer的组合,讲解如何以简洁代码实现多GPU并行微调,覆盖垂直领域大模型与多模态场景,适合具备一定PyTorch基础、想降低训练成本的学习者。压缩包共18个文件,约116KB,包含11个Python脚本、3个Shell启动脚本、2个JSON配置以及LICENSE和README,脚本分别对应LoRA、P-Tuning、Freeze等微调方式与训练入口,配置文件用于设定并行策略与训练参数,结构紧凑便于按需取用。目前已有355人学习下载。通过其中的训练脚本、参数配置与数据加载模块,读者可以理解Deepspeed与Trainer的集成方式,掌握多卡环境下微调ChatGLM类模型的完整流程,并借鉴不同微调策略的代码组织与排错思路,为后续迁移到自有垂直领域任务提供可复用的工程模板。
1. 多卡微调大模型,为什么我最后选了 deepspeed+trainer 这套组合
单卡 24G 显存想微调 7B 模型,跑起来不是 OOM 就是 batch size 只能设成 1,训练速度慢到怀疑人生。这是我带过好几个垂直领域大模型项目时反复遇到的场景。后来我把训练框架换成了 deepspeed 配合 HuggingFace trainer,多卡并行、显存优化、梯度累积这些事基本不用自己手写,配置文件改几行就能跑起来。这份资源包做的就是这件事:用最少的代码改动,把单卡跑不动的微调任务搬到多卡上,同时把显存占用压下来。它适合已经跑通过单卡微调、想往多卡扩展的从业者,也适合刚入门大模型微调、想直接上手一套能跑通的多卡方案的人。核心不是教你从零写训练循环,而是给你一套经过验证的配置模板和启动脚本,改改路径和参数就能用。
2. deepspeed 与 trainer 的协作机制:谁管显存,谁管调度
2.1 两者分工的底层逻辑
很多人第一次接触 deepspeed+trainer 会搞混一件事:到底谁在管多卡通信,谁在管训练流程。简单说,trainer 负责的是训练循环本身——前向、反向、优化器更新、日志、checkpoint 保存这些。deepspeed 负责的是把这些操作切分到多张卡上,并且用 ZeRO 系列策略把显存占用降下来。
ZeRO 的核心思路是把模型状态(参数、梯度、优化器状态)分片存储。Stage 1 只分片优化器状态,Stage 2 加上梯度分片,Stage 3 连参数也分片。Stage 3 最省显存,但通信开销最大。我一般会根据模型大小和卡数来选:7B 模型用 2 到 4 张 24G 卡,Stage 2 通常够用;13B 以上或者卡数多但单卡显存小,就上 Stage 3。
trainer 这边通过deepspeed参数接收一个配置文件路径,然后在内部把训练循环的每一步都交给 deepspeed 的 engine 来执行。你不需要改模型代码,也不需要手动写DistributedDataParallel,trainer 会自动处理。
2.2 配置文件的关键字段拆解
deepspeed 的配置文件是 JSON 格式,下面是一个我常用的 Stage 2 配置模板:
{ "train_batch_size": "auto", "train_micro_batch_size_per_gpu": "auto", "gradient_accumulation_steps": "auto", "optimizer": { "type": "AdamW", "params": { "lr": "auto", "betas": "auto", "eps": "auto", "weight_decay": "auto" } }, "scheduler": { "type": "WarmupDecayLR", "params": { "warmup_min_lr": "auto", "warmup_max_lr": "auto", "warmup_num_steps": "auto", "total_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 }, "fp16": { "enabled": true, "loss_scale": 0, "loss_scale_window": 1000, "initial_scale_power": 16, "hysteresis": 2, "min_loss_scale": 1 }, "gradient_clipping": 1.0, "steps_per_print": 100, "wall_clock_breakdown": false }train_batch_size设成"auto"时,trainer 会根据你的per_device_train_batch_size、gradient_accumulation_steps和卡数自动算。offload_optimizer把优化器状态放到 CPU 内存,显存不够时这是救命选项,但会拖慢速度,我一般只在显存实在压不下来时才开。overlap_comm让通信和计算重叠,能提一点速度,建议开着。fp16部分,loss_scale设 0 表示用动态 loss scaling,initial_scale_power是初始缩放因子,16 对应 65536,一般不用改。
2.3 启动脚本与多卡通信初始化
配置文件写好后,启动方式有两种:用deepspeed命令直接启动,或者用torchrun配合 trainer 的deepspeed参数。我习惯用后者,因为 trainer 对多卡环境的处理更省心。
# 用 torchrun 启动,指定 4 张卡 torchrun --nproc_per_node=4 \ --master_port=29500 \ train.py \ --deepspeed ds_config.json \ --model_name_or_path /path/to/your/model \ --output_dir ./output \ --num_train_epochs 3 \ --per_device_train_batch_size 4 \ --gradient_accumulation_steps 4 \ --learning_rate 2e-5 \ --fp16 True \ --logging_steps 10 \ --save_steps 500nproc_per_node就是卡数,master_port随便选一个不冲突的端口。per_device_train_batch_size是每张卡上的 batch size,gradient_accumulation_steps是梯度累积步数,两者相乘再乘以卡数就是全局 batch size。比如这里 4×4×4=64。学习率 2e-5 是微调 7B 模型的常见起点,具体要看你的数据量和任务类型。
注意:
torchrun启动时,如果卡数超过 8,建议设置NCCL_IB_DISABLE=1和NCCL_P2P_DISABLE=1来避免一些通信库的兼容问题,尤其是在混合显卡型号的机器上。
3. 从单卡到多卡:代码改造与数据准备的实际操作
3.1 trainer 代码的最小改动清单
如果你已经有一份单卡微调的 trainer 代码,改成多卡其实只需要动几个地方。下面是一个完整的训练脚本骨架:
import torch from transformers import ( AutoModelForCausalLM, AutoTokenizer, Trainer, TrainingArguments, DataCollatorForSeq2Seq ) from datasets import load_dataset # 1. 加载模型和 tokenizer model = AutoModelForCausalLM.from_pretrained( "Qwen/Qwen2.5-7B", torch_dtype=torch.float16, device_map=None # 多卡时不要设 device_map,让 deepspeed 管 ) tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B") tokenizer.pad_token = tokenizer.eos_token # 2. 加载数据集并 tokenize dataset = load_dataset("json", data_files="train.json", split="train") def tokenize_fn(example): tokens = tokenizer( example["text"], truncation=True, max_length=1024, padding=False ) tokens["labels"] = tokens["input_ids"].copy() return tokens tokenized = dataset.map(tokenize_fn, remove_columns=dataset.column_names) # 3. 训练参数 training_args = TrainingArguments( output_dir="./output", num_train_epochs=3, per_device_train_batch_size=4, gradient_accumulation_steps=4, learning_rate=2e-5, fp16=True, logging_steps=10, save_steps=500, save_total_limit=3, deepspeed="ds_config.json", # 关键:指向 deepspeed 配置 report_to="none" ) # 4. 初始化 trainer trainer = Trainer( model=model, args=training_args, train_dataset=tokenized, data_collator=DataCollatorForSeq2Seq(tokenizer, padding=True) ) # 5. 开始训练 trainer.train()关键改动就三处:device_map设成None,TrainingArguments里加deepspeed参数,其余交给 trainer 自动处理。DataCollatorForSeq2Seq负责把不同长度的序列 padding 到同一长度,多卡时每个进程会拿到自己的数据分片。
3.2 数据格式与 tokenize 的边界处理
数据格式我一般用 JSON Lines,每行一个样本,字段名和tokenize_fn里的对应。比如:
{"text": "### 指令:解释什么是梯度累积\n### 回答:梯度累积是在反向传播时..."} {"text": "### 指令:多卡训练时如何设置学习率\n### 回答:一般按全局 batch size 线性缩放..."}max_length设 1024 是保守值,如果你的数据平均长度只有 256,可以降到 512 省显存。padding=False在 tokenize 阶段不 padding,交给 data collator 动态处理,这样能减少显存浪费。labels直接复制input_ids,这是自回归语言模型的标准做法,如果你要做指令微调且只计算回答部分的 loss,需要把指令部分的 label 设成 -100。
注意:多卡训练时,每个进程都会独立加载一份数据集。如果数据集很大,建议用
datasets库的load_from_disk先缓存到本地,避免每个进程重复读盘。
3.3 显存与吞吐的实测调参
以 4 张 24G 卡微调 7B 模型为例,不同配置下的显存占用和吞吐大致如下:
| 配置 | 单卡显存 | 全局 batch size | 每秒样本数 |
|---|---|---|---|
| Stage 2, bs=2, ga=4 | 约 18G | 32 | 约 12 |
| Stage 2, bs=4, ga=4 | 约 22G | 64 | 约 18 |
| Stage 3, bs=4, ga=4 | 约 14G | 64 | 约 10 |
| Stage 2 + offload, bs=4, ga=4 | 约 10G | 64 | 约 6 |
Stage 3 显存降得多但速度掉得也明显,offload 更狠。我的经验是:先试 Stage 2 加最大 batch size,如果 OOM 再降 batch size 或上 Stage 3。gradient_accumulation_steps用来补全局 batch size,不占额外显存,但会增加训练时间。
4. 避坑与排查:多卡微调里最容易翻车的五个地方
4.1 现象:训练启动后卡在初始化,日志停在 “Initializing deepspeed”
原因通常是 NCCL 通信没建起来。多卡机器上如果网卡配置不对,或者master_port被占用,就会卡在这一步。解决方法是先检查master_port是否空闲,用netstat -tlnp | grep 29500看一眼。如果端口没问题,试试设置export NCCL_DEBUG=INFO看详细日志,常见的是网卡选错了,可以用export NCCL_SOCKET_IFNAME=eth0指定正确的网卡。
4.2 现象:loss 变成 NaN 或者突然飙升
多卡训练时 loss 比单卡更容易炸,原因一般是学习率没随全局 batch size 调整。单卡 batch size 是 4,学习率 2e-5;多卡全局 batch size 变成 64,学习率还保持 2e-5 就可能偏大。常见做法是按线性缩放,全局 batch size 翻倍,学习率也翻倍,但上限一般不超过 5e-5。另外检查gradient_clipping是否设了,我一般设 1.0。
4.3 现象:保存的 checkpoint 加载时报错,提示参数形状不匹配
这是 ZeRO Stage 3 的典型问题。Stage 3 把参数分片存储,保存下来的 checkpoint 是分片格式,直接用from_pretrained加载会失败。解决办法是用 deepspeed 提供的zero_to_fp32.py脚本把分片合并成完整模型:
python zero_to_fp32.py ./output/checkpoint-500 ./output/merged_model合并后再用from_pretrained加载就没问题了。如果用的是 Stage 2,checkpoint 本身就是完整的,不需要这一步。
4.4 现象:多卡训练速度反而比单卡慢
卡数增加但吞吐没上去,甚至下降,通常是通信开销吃掉了计算收益。检查overlap_comm是否开启,allgather_bucket_size和reduce_bucket_size是否设得太大。这两个 bucket size 默认 5e8,我一般降到 2e8,减少通信等待。另外如果卡之间是 PCIe 而不是 NVLink,通信带宽有限,卡数超过 4 张后收益递减很明显。
4.5 现象:训练中途 OOM,但显存监控显示还有余量
这是显存碎片化导致的。PyTorch 的缓存分配器在长时间训练后会产生碎片,明明总显存够但连续大块不够。解决办法是设置PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:128,限制单次分配的最大块大小,减少碎片。另外contiguous_gradients设成true也有帮助。
5. 进阶技巧:用 Stage 3 + offload 在 2 张 24G 卡上微调 13B 模型
5.1 配置调整与显存账本
13B 模型用 fp16 存储光参数就占 26G,单卡 24G 根本放不下。2 张卡用 Stage 3 分片后每卡约 13G 参数,加上梯度、优化器状态和激活值,还是紧张。这时候需要把优化器状态 offload 到 CPU:
{ "zero_optimization": { "stage": 3, "offload_optimizer": { "device": "cpu", "pin_memory": true }, "offload_param": { "device": "cpu", "pin_memory": true }, "stage3_prefetch_bucket_size": 5e7, "stage3_param_persistence_threshold": 1e5, "stage3_max_live_parameters": 1e9, "stage3_max_reuse_distance": 1e9, "stage3_gather_16bit_weights_on_model_save": true } }offload_param把参数也放到 CPU,显存占用能压到 10G 以下,但速度会掉到单卡的几分之一。stage3_gather_16bit_weights_on_model_save设成true后,保存 checkpoint 时会自动合并参数,省去手动跑zero_to_fp32.py的步骤。
5.2 训练速度与显存的实际权衡
我用 2 张 24G 卡微调 13B 模型,Stage 3 加 offload 后单卡显存约 9G,每秒处理约 3 个样本。同样的任务如果用 4 张 40G 卡跑 Stage 2,每秒能到 15 个样本。所以 offload 是显存不够时的后悔药,不是常规方案。如果预算允许,优先加卡或者换大显存卡,比 offload 划算得多。
5.3 验证 checkpoint 是否正确的习惯
从那以后我每次训练完都强制走一遍验证流程:先用zero_to_fp32.py合并参数(如果没开自动合并),然后用from_pretrained加载合并后的模型,跑一条推理看输出是否正常。这一步能提前发现参数分片没合并、权重形状不对、tokenizer 不匹配这些问题。具体命令:
# 合并参数 python zero_to_fp32.py ./output/checkpoint-1000 ./output/merged # 快速推理验证 python -c " from transformers import AutoModelForCausalLM, AutoTokenizer model = AutoModelForCausalLM.from_pretrained('./output/merged', torch_dtype='auto') tokenizer = AutoTokenizer.from_pretrained('./output/merged') inputs = tokenizer('### 指令:什么是 deepspeed?\n### 回答:', return_tensors='pt') outputs = model.generate(**inputs, max_new_tokens=100) print(tokenizer.decode(outputs[0], skip_special_tokens=True)) "如果输出是乱码或者重复,大概率是合并步骤出了问题。这个习惯帮我省过好几次重新训练的功夫。希望帮到你。
本文还有配套的精品资源,点击获取