
简介这是一份面向大模型微调入门与进阶的优质实战资源专注基于DeepSpeed对ChatGLM进行多卡训练提供完整项目源码与流程教程帮助解决大型语言模型训练与微调门槛高的问题。资源包仅118KB共17个文件主要包括11个Python脚本、3个Shell脚本、2个JSON配置和1个Markdown文档分别承担模型加载、数据预处理、训练循环、多卡配置、启动脚本与说明文档等任务整体结构清晰便于按功能检索。值得一提的是教程细致讲解了环境搭建、数据准备、微调、评估与测试全流程并对DeepSpeed的内存优化、梯度累积及多卡并行机制做了具体展示每个模块均有注释便于二次开发同时提供了实用的训练脚本和配置文件可大幅缩短环境调试时间。目前已有266人学习下载适合希望高效上手大模型微调、研究多卡训练技术的开发者和科研人员。1. 多卡微调 ChatGLM瓶颈从来不在显卡数量把单卡能跑的 ChatGLM 微调脚本改成多卡最容易得到的不是加速比而是一堆新的显存错误。很多人以为多卡只是把device_ids填满实际训练一启动就爆 OOM或者 loss 直线起飞。真正的问题出在数据怎么切、梯度怎么同步、优化器状态放在哪块卡上。DeepSpeed 能解决的是这三件事的统一调度而不是单纯地把模型塞进多张卡。ChatGLM 这类 6B 级别模型在单卡上做 LoRA 或全参数微调都显得局促多卡微调的价值是同时扩大可用显存、摊薄优化器状态并给 batch size 留下进步空间。这篇文章写给已经跑通过一次单卡微调、现在想用 DeepSpeed 把训练搬到多卡上的工程师重点講清楚配置、命令和排错路径不会绕回环境安装的入门教程。2. DeepSpeed 与 ChatGLM 微调的三个前置问题显存预算、并行策略和模型加载2.1 为什么是 DeepSpeed而不是普通 DataParallel 或 DistributedDataParallel多卡训练的直觉做法是DataParallel但它在 6B 模型上基本不可用原因是 GPU 0 需要收集所有梯度通信量随卡数线性上涨且前向复制模型权重带来的显存开销并不会被分担。DistributedDataParallel解决了梯度同步效率但每张卡仍然持有完整模型副本和各自的优化器状态多卡只是多吃了数据并不能缓解单卡显存上限。DeepSpeed 的核心是把训练状态拆开存放用 ZeRO 把优化器状态、梯度和参数分片到各卡。对于 ChatGLM 微调场景多数人用的是 LoRA 方法真正参与更新的参数只有几百万但全量模型参数仍然占据显存。如果全参数微调显存大头变成 Adam 状态每个参数要存主副本、动量项、方差项FP16 训练下动辄 16 字节以上。ZeRO 的最大收益就是把这部分按卡数分片让显存压力从卡内变成集群内。选 DeepSpeed 的理由总结成三点通信模式成熟、ZeRO 阶段可随时调整、训练时能同时用 hvd 或 torch 的 distributed backend。除了 DeepSpeed 之外也可以选 FSDP但 DeepSpeed 在这个场景下最容易调出稳定结果。启动器用deepspeed命令而非直接写torchrun好处是配置文件里的zero_allow_untested_optimizer等开关能被自动读取。提示如果只是做 LoRA 微调而显存又很紧优先把zero_optimization.stage设成 2梯度检查点开关打开性价比最高。2.2 显存预算从量化、冻结参数到 ZeRO 阶段做多卡前必须先把显存算清楚否则一张卡装不下权重时后面所有并行方案都是空谈。ChatGLM-6B 的 FP16 权重本身约 12GB但 Adam 优化器状态、梯度和中间激活会把这个数字推到 100GB 级别。常见做法是先冻结 ChatGLM 的原始参数只训练 LoRA 适配层这样梯度只针对 LoRA 参数但正向传播仍然要过完整模型激活值显存不可忽略。可以用梯度检查点把激活显存从按层保存变成按需重算OpenAI 时代的老技巧在 ChatGLM 上效果很明显。综合取舍见表策略显存占用训练速度适用场景冻结模型 LoRA低高单卡或小规模多卡最常见全参数 ZeRO Stage 2中中2-4 卡全参数微调全参数 ZeRO Stage 3低低4 卡以上模型参数分片避免单卡瓶颈FP16 梯度检查点降低激活显存略慢常与上面任意方案叠加这里容易有一个误判以为 ZeRO Stage 3 最省显存就直接开 Stage 3。对 LoRA 微调来说Stage 3 会把冻结的 ChatGLM 权重也分片通信开销反而拉高训练速度不会好看。我会先在 Stage 2 跑一个短 step观察显存峰值再决定是否到 Stage 3。2.3 模型加载从单卡权重到多卡分片的一致性问题非 DeepSpeed 的多卡训练里每张卡加载同一个模型文件权重通过set_model同步。深水区在检查点保存与恢复ChatGLM 原模型权重是单份LoRA 权重也是单份但 DeepSpeed 训练时可能把模型分片保存在多个mp_rank目录里。如果只做了 model 保存而没有做zero_to_fp32.py合并后续会拿到一堆分片文件不但无法直接加载还会误判训练失败。建议从一开始就在训练脚本里做两件事定期保存 DeepSpeed checkpoint训练结束或中断时用官方zero_to_fp32.py把分片权重合并为单份LoRA 则单独调一次model.save_pretrained保存 adapter。这样多卡训练留下的产物仍然能被单卡推理代码加载少踩很多恢复现场的坑。3. 用 DeepSpeed 在本地起一个多卡微调的最小工程3.1 先搭训练脚本骨架从 transformers 到 DeepSpeed 的桥接一个能跑多卡 ChatGLM 微调的脚本核心不是模型改造而是把TrainingArguments正确接到 DeepSpeed 配置上。常见做法是直接用transformers.Trainer它内部会识别deepspeed参数并初始化引擎。下面是最小骨架import torch from transformers import ( AutoTokenizer, AutoModelForCausalLM, TrainingArguments, Trainer ) from peft import LoraConfig, get_peft_model model AutoModelForCausalLM.from_pretrained( chatglm-6b, torch_dtypetorch.float16, ) target_modules [query_key_value] lora_config LoraConfig( r8, lora_alpha32, target_modulestarget_modules, lora_dropout0.1, ) model get_peft_model(model, lora_config) training_args TrainingArguments( output_dir./chatglm_deepspeed_out, per_device_train_batch_size2, gradient_accumulation_steps8, learning_rate2e-5, num_train_epochs3, fp16True, deepspeedds_config.json, save_steps500, logging_steps10, dataloader_num_workers4, ) trainer Trainer( modelmodel, argstraining_args, train_datasettokenized_dataset, ) trainer.train()参数说明query_key_value是 ChatGLM 在 PEFT 实现里常用的 LoRA 注入目标换成其他模型时需要检查内部模块名gradient_accumulation_steps8配合per_device_train_batch_size2等效全局 batch size 取决于卡数和 accumulation 步数fp16True与 DeepSpeed 配置中的fp16开关要同时存在缺一个都会导致训练直接回退到 FP32显存翻倍。3.2 多卡启动命令与数据集切分规则不要用python train.py启动DeepSpeed 正确入口是deepspeed --num_gpus2 train.py \ --deepspeed ds_config.json \ --per_device_train_batch_size 2 \ --gradient_accumulation_steps 8 \ --output_dir ./chatglm_deepspeed_out如果你的环境是用 slurm 管理可以改成deepspeed --num_gpus2 --master_port29500 train.py ...。这里的--num_gpus只是告诉启动器本机要用几张卡真正的并行信息来自torch.distributed初始化transformers Trainer 已经处理。数据切分也不用你手动做Trainer会按当前进程对应的 rank 分配 data shard但要确保训练集是datasets.Dataset而不是 list 套 list否则多进程读取时可能全部进程拿到同一份数据。3.3 启动后必须验证的三个信号训练起跑后别只盯 loss。第一步看日志里是否出现DeepSpeed info和zero_config相关输出这是 PyTorch 训练已经交给 DeepSpeed 控制的证据。第二步看每步耗时多卡启动后单 step 时间一般比单卡慢 10%-30%如果慢到两倍以上大概率是小 batch size 下通信开销盖过了计算收益。第三步看 nvidia-smi 的显存分布理想情况下各卡显存占用接近某张明显偏高说明数据加载或模型分片不均匀。4. 微调脚本与 DeepSpeed 参数调优从能跑到跑得稳4.1 一份可以直接改的 ds_config.json 和关键参数含义DeepSpeed 的参数很多但 ChatGLM 微调值得盯着调的只有几个。下面这份配置是 LoRA 多卡训练的常用起点{ zero_optimization: { stage: 2, allgather_partitions: true, allgather_bucket_size: 2e8, overlap_comm: true, reduce_scatter: true, reduce_bucket_size: 2e8, contiguous_gradients: true }, gradient_accumulation_steps: 8, gradient_clipping: 1.0, fp16: { enabled: true, loss_scale: 0, loss_scale_window: 1000, initial_scale_power: 16, hysteresis: 2, min_loss_scale: 1 }, train_batch_size: 32, train_micro_batch_size_per_gpu: 2 }注意train_batch_size是全局 batch size由 micro batch、梯度累积步数、卡数相乘得到。这里有最常踩的坑写了train_batch_size: 32但只给 2 卡每卡 micro batch 2实际全局 batch size 是 2×2×832 才能对上如果忘了梯度累积步数DeepSpeed 会报警告并覆写配置。allgather_bucket_size控制通信分块大小数值太小时通信次数变多太大会让单次消息占用过多带宽2e8 这个经验值在几卡场景不用改。fp16部分loss_scale: 0表示动态 loss scale是稳定训练的关键。如果训练中 loss 突然变成 NaN先看日志里是否出现loss scale decreased这表示 loss scale 触底多半是学习率过高不是 DeepSpeed 本身的问题。4.2 小 batch 多卡下吞吐反而变慢的调整路径多卡微调最常见的现象是没跑几步就发现吞吐低于单卡。原因集中在三处卡间通信没有与计算重叠、batch size 太小导致 GPU 利用率低、数据加载器成了瓶颈。先检查overlap_comm是否为 true这是 DeepSpeed 把梯度 allreduce 和反向计算重叠的开关。再看单卡上的train_micro_batch_size_per_gpu如果设为 1每步计算时间太短通信时间占大头我一般会提到 2 或 4搭配梯度检查点把激活显存降下来。如果数据样本长短差异大dataloader_num_workers要往上调并且开启pad_to_multiple_of或group_by_length否则每个 step 都有人要等最长的样本算完多卡会进一步放大这种木桶效应。有个容易忽略的细节transformers.Trainer默认会重新做数据集预处理如果你的 tokenized_dataset 已经是固定 max_length务必在TrainingArguments里设置remove_unused_columnsFalse避免每步触发无谓的列过滤。4.3 训练日志里如何判断多卡状态正常日志行里每个 step 会打印loss、learning_rate、epoch此外还有一个容易被忽略的指标grad_norm。梯度范数在正常训练里应该在一个相对稳定的范围内波动如果某个 step 突然跳到几十上百大概率是遇到了异常样本或者 DeepSpeed 的梯度累积状态在跨卡间没有被正确清零。多卡环境下偶尔一次grad_norm尖峰不一定会 loss 发散但连续多次尖峰后 loss 不下降就要检查数据顺序是否被多卡打乱。DeepSpeed 也提供了自己的日志在启动命令加--deepspeed_config后训练输出会有device和world size信息。确认 world size 和实际卡数一致后再去看nvidia-smi中每张卡的利用率全部在 90% 以上说明通信和计算搭配合适。如果有一张卡利用率掉到 50%问题大概率是数据加载不均衡而不是 DeepSpeed 配置。4.4 常见错误与换配置前先做的事OOM 是最容易把人劝退的错误。先区分是哪个阶段 OOM启动阶段模型加载 OOM说明per_device_train_batch_size太大或权重没有以 FP16 加载训练中程 OOM说明激活值峰值超出显存此时优先开梯度检查点而不是降低 batch size。RuntimeError: Expected all tensors to be on the same device这类错误常见于数据预处理里把张量固定写在了 CPU 或某个显存上多卡运行时 device 不一致解决办法是在数据集的__getitem__里不要提前.cuda()。提示换任何 DeepSpeed 参数都只动用一处保存小 batch 日志后单步对比。多卡问题经常是多个配置叠加出来的一次性改三四个参数你永远不知道是哪一步救了训练。5. 多卡微调后的验证手段与权重合并技巧5.1 把 DeepSpeed 分片权重合并回 LoRA 可用形态训练结束后output_dir下会出现global_step目录和zero_to_fp32.py。不要直接删掉这些目录恢复单独微调权重需要先合并python zero_to_fp32.py \ --checkpoint_dir ./chatglm_deepspeed_out/global_step1500 \ --output_file ./chatglm_deepspeed_out/pytorch_model_hf.bin合并完成后再把 LoRA adapter 单独保存一次用model.save_pretrained(./lora_adapter)。这样得到的是两份东西一份是合回完整模型结构的权重一份是轻量 adapter。推理代码里加载 adapter 时要先用基座模型初始化再做PeftModel.from_pretrained顺序反了会出现 key 不匹配的错误。5.2 用同一批 prompt 对比微调前后输出差异多卡微调的验证不能只看 loss还要看模型输出是否真的改变了。我会固定准备 50 条和业务场景相关的 prompt在训练前用 base model 生成一遍训练后重新生成逐条对比两个维度输出是否包含新学到的业务格式以及是否出现复读或模板化回退。比较时用 beam search 而不是 greedy decode因为 greedy 本身容易让输出趋同不利于发现细分差异。对比时如果发现模型回答开始带重复 token优先怀疑训练步数过长或 LoRA rank 设置过高这种状态和是否多卡无关但多卡环境的全局 batch size 往往比单卡大同样的 epoch 下实际步数更少反而加重欠拟合。判断是否欠拟合的简单方式是看验证集 loss 是否仍在下行若验证 loss 停了但训练 loss 还在降就该提前截断或用 early stopping。5.3 用梯度检查点对应的推理方式做最终确认如果训练阶段开了gradient_checkpointing推理阶段不需要再显式开启但要注意合并后权重加载时ChatGLM 里的缓存逻辑可能和多卡训练时的上下文长度不一致。比如训练 max_length 是 2048推理时为了省显存把 max_length 调成 512模型输出可能在后续 logits 上有细微不同这是预期内的。最终上线前用和训练完全一致的最大长度跑一遍压测确认没有显存溢出再收口。本文还有配套的精品资源点击获取