
简介基于Deepspeed实现ChatGLM多卡微调的优质实战项目包面向大模型训练与微调研究者及初中级开发者重点解决多卡并行训练中环境配置复杂、参数调优难、入门门槛高等问题。压缩包共17个文件包括11个Python源码负责模型加载、训练循环、推理等核心逻辑、3个Shell启动脚本对应不同微调策略的一键运行、2个JSON配置存放DeepSpeed训练参数和1个Markdown说明文档整体仅118KB结构精简清晰。教程逐环节讲解环境搭建、数据预处理、模型微调与评估测试并特别说明DeepSpeed的内存优化、梯度累积和多卡并行启动方式。源码模块化且注释充分涵盖LoRA、P-Tuning、Freeze等微调方案可直接修改运行帮助用户节省调试时间、掌握多卡微调全流程。目前已有267人学习浏览适合想要借助DeepSpeed加速ChatGLM等大模型微调的开发者和研究者。1. 大模型微调不是玄学一套能复现的Deepspeed多卡ChatGLM实战源码手里有四张3090或者两台机器各两张A100想微调ChatGLM但网上翻来翻去都是单卡教程一到多卡就跑不起来——这是我最近被问得最多的问题。我拆完这套基于Deepspeed实现ChatGLM多卡微调的项目源码后发现它把LoRA、Freeze、P-Tuning三条路线都做成了可直接运行的脚本还带完整的指令数据样例和流程教程。你不用先搞懂分布式训练的全部原理照着脚本改数据、改配置、启动就能把大模型微调从玄学变成可复现的工程。项目源码是纯Python写的核心包括三个训练入口finetune_lora.py、finetune_freeze.py、finetune_ptuning.py分别对应轻量微调、部分冻结微调和前缀微调。配套的freeze.sh、ptuning.sh、lora.sh把deepspeed的启动命令封装好了ds_config.json则负责多卡下的显存和梯度同步。适合两类人一是刚入门大模型微调的研究生需要一份完整代码做基线二是算法工程师想在业务数据上快速试出哪种微调方式性价比最高。下面我从项目结构讲起一步步把训练链路拆开。2. 拆项目结构与微调选型LoRA、Freeze、P-Tuning三条路线怎么选拿到压缩包先别急着跑把文件理清楚后面所有踩坑都跟文件结构有关。这个项目没有把大量逻辑堆进一个脚本里而是拆成了训练入口、启动脚本、模型实现、数据处理和训练器五类互相之间通过参数串联。理解了这个结构你就知道改哪一行会影响什么。2.1 项目文件清单每个文件在训练链路里的位置下表是项目核心文件的作用映射我按训练链路从上到下排文件作用finetune_lora.pyLoRA微调训练入口加载基底模型并注入低秩适配层finetune_freeze.py冻结部分层只训练后半段参数的微调入口finetune_ptuning.pyP-Tuning前缀微调入口在输入侧拼接可训练前缀向量lora.sh / freeze.sh / ptuning.sh三个入口对应的deepspeed启动脚本封装了多卡参数ds_config.jsonDeepspeed核心配置控制ZeRO stage、梯度累积、混合精度data.py数据加载和预处理把json指令数据转成batcharguments.py统一管理训练超参数三个入口共用trainer_pt.py / trainer_ptuning.py定制训练循环处理LoRA和前缀token的损失打印modeling_chatglm.py / configuration_chatglm.py / tokenization_chatglm.pyChatGLM模型、配置和分词器的本地实现instruction_data.json指令微调数据集样例字段含instruction/input/outputinfer_lora.py训练完用LoRA权重做推理验证的脚本README.md流程教程环境搭建和启动命令都写在这里注意modeling_chatglm.py这几个文件它们是项目自带的模型定义而不是从transformers直接import。这种做法的最大好处是固定了模型代码避免后续transformers版本升级导致ChatGLM的接口变化。坏处是你得留意加载权重时用的类必须和训练时一致否则会出现尺寸不匹配的报错。我一般会把这几个文件原封不动地保留不做任何修改。2.2 三种微调方式对比适用场景、显存消耗与收敛速度这个项目最实用的地方在于它同时给了三条微调路线而不是只让你用一个。你可以用同一份数据跑三个脚本然后对比结果。下面是对比维度维度LoRAFreezeP-Tuning训练参数量最少只训练低秩矩阵中等训练未冻结层仅前缀token甚至更少显存占用最低中等低收敛速度快中等偏慢效果上限取决于rank设置高保留较多原参数适合生成任务指令理解稍弱项目脚本lora.shfreeze.shptuning.sh选型时我的习惯是数据量小、想快速看效果先跑LoRA数据量中等且任务偏指令理解用Freeze如果任务是开放域生成比如写作文或对话P-Tuning会有奇效。这套源码把三种都留好了你可以用同一个instruction_data.json去横向对比这是它作为项目实战包最有价值的地方。2.3 为什么多卡必须配Deepspeed通信、显存与ZeRO优化不用Deepspeed的情况下原生PyTorch DDP虽然也能多卡训练但每张卡都要保存一份完整的模型参数、梯度、优化器状态。十亿级参数的ChatGLM光Adam优化器的动量状态就占了参数量好几倍的显存四卡下去等于把同样内容复制四份很快就OOM了。Deepspeed的核心优化是ZeRO。初学的人经常搞不清ZeRO到底三个stage差在哪简单说stage 1只切分优化器状态stage 2把梯度也切了stage 3连模型参数都切。切分的意思是每张卡只保留全局参数的一部分需要的时候通过通信重新聚合。这个项目默认配置是stage 2既能省下优化器状态和梯度的大头通信开销又不像stage 3那么大。如果你的模型大到单卡放不下再上stage 3。需要提醒一句多卡不是卡越多越快。每步训练结束都要做梯度同步通信时间会随着卡数增加。单机四卡通常扩展性最好到了八卡或跨机器需要额外处理NCCL通信配置。这套脚本默认是单机多卡如果你要跑多机多卡还要在deepspeed启动命令里补hostfile和master_addr这在后面排查章节会再提。3. 环境搭建与数据准备让ChatGLM顺利吃到你的数据项目能跑起来环境占一半。ChatGLM这类模型对环境敏感PyTorch、transformers、deepspeed三方版本只要错一位启动时就会出现莫名其妙的报错。这一章我会按照依赖安装、数据格式、数据处理三条线展开每一步都对应项目里的实际文件。3.1 依赖安装与CUDA/PyTorch版本匹配先给一个通用的安装流程我建议在干净的conda环境里操作避免和已有环境冲突conda create -n chatglm python3.8 conda activate chatglm pip install torch --index-url https://download.pytorch.org/whl/cu117 pip install transformers pip install deepspeed pip install datasets这里没有指定具体版本号因为项目里的modeling_chatglm.py是自带实现的transformers版本太新反而可能不兼容。常见做法是装与项目README中匹配的transformers版本如果README没写就用mid-2023左右的版本基线。安装完成后先用一行Python确认torch能识别GPUimport torch print(torch.cuda.is_available(), torch.cuda.device_count())如果输出True和你的卡数再继续。如果False说明CUDA、PyTorch、NVIDIA驱动三者没对齐这种情况不要急着装deepspeed先把显卡驱动和CUDA版本查清楚。安装deepspeed包一直报错绝大多数都发生在这个阶段不是deepspeed本身的问题而是基础环境不对。如果pip安装deepspeed时出现编译报错比如提示ninja找不到或构建PyTorch扩展失败我一般会先装ninja再设置跳过算子编译pip install ninja export DS_BUILD_OPS0 pip install deepspeed --no-cacheDS_BUILD_OPS0的意思是跳过自定义CUDA算子编译等环境稳定后再按需开启。这样安装基本不会失败代价是部分算子性能略低但对微调场景够用。3.2 指令数据格式instruction_data.json的字段含义项目提供了一个instruction_data.json这是训练的直接输入。它的格式是三条字段的标准指令微调形式[ { instruction: 请将以下句子翻译成中文, input: DeepSpeed is a library for distributed training., output: DeepSpeed是一个分布式训练库。 }, { instruction: 写一首关于冬天的短诗, input: , output: 枯枝落雪千万点寒风卷地入松林。 } ]字段含义很直白instruction是任务指令input是附加输入可填空字符串output是期望模型生成的目标文本。训练时模型会看到instruction和input的拼接然后生成output生成长度和内容都要以output为准。这里最容易被忽略的是input字段为空时拼接逻辑要跳过它否则会在prompt里留下一个多余的换行。数据量方面指令微调不需要海量数据。常见做法是先准备几百到几千条高质量样本优先保证准确率和多样性而不是堆数量。我见过很多人在这一步把数据格式写复杂了加了各种system字段、history字段反而让data.py的预处理报错。这个项目只吃instruction/input/output三段式你如果要换成chat格式记得同步改data.py不能只改json。3.3 data.py里的预处理逻辑从JSON到模型输入的完整链路data.py是数据链路的核心它负责把json读进来转成模型需要的input_ids和labels。下面是一段和项目思路一致的核心代码def preprocess(example, tokenizer, max_seq_len512): prompt example[instruction] if example.get(input): prompt \n example[input] target example[output] prompt_ids tokenizer.encode(prompt) target_ids tokenizer.encode(target) input_ids prompt_ids target_ids [tokenizer.eos_token_id] labels [-100] * len(prompt_ids) target_ids [tokenizer.eos_token_id] return { input_ids: input_ids[:max_seq_len], labels: labels[:max_seq_len] } def collate_fn(batch, tokenizer): input_ids [torch.tensor(x[input_ids]) for x in batch] labels [torch.tensor(x[labels]) for x in batch] input_ids torch.nn.utils.rnn.pad_sequence( input_ids, batch_firstTrue, padding_valuetokenizer.pad_token_id ) labels torch.nn.utils.rnn.pad_sequence( labels, batch_firstTrue, padding_value-100 ) return { input_ids: input_ids, attention_mask: (input_ids ! tokenizer.pad_token_id).long(), labels: labels }这段代码有两个关键点。第一labels里prompt部分全部设为-100target部分保留真实token id这样CrossEntropyLoss的ignore_index默认会跳过prompt位置只计算output部分的损失。第二batch内长度不一致时用pad token补齐同时把labels的pad位置设成-100防止计算损失时把padding也算进去。还有一个容易忽略的点如果tokenizer没有pad_token_id常见做法是在加载后手动设置tokenizer.pad_token tokenizer.eos_token。项目里的tokenization_chatglm.py虽然自带tokenizer但并没有保证pad token一定存在所以你需要在data.py或arguments.py初始化时加上这一步。缺少这个设置collate_fn里的padding_value就会拿到None然后报一个非常难懂的TypeError。4. 多卡微调实战Deepspeed配置与三个启动脚本的参数解读环境通了数据就位了接下来是真正的多卡训练。这一章我会逐个拆解ds_config.json和三个训练脚本告诉你哪些参数必须改哪些参数动了会影响显存或收敛。4.1 ds_config.jsonZeRO Stage、梯度累积与混合精度ds_config.json是Deepspeed的控制中枢。项目里这个文件很简单但作用很大{ train_batch_size: 16, gradient_accumulation_steps: 2, fp16: { enabled: true }, zero_optimization: { stage: 2 } }先解释train_batch_size这是全局batch size不是单卡batch size。多卡训练时需要满足下面的关系全局batch size 每卡batch size × GPU数 × 梯度累积步数比如4卡全局16梯度累积2那么每卡micro_batch就是16 / (4 × 2) 2。如果OOM优先把每卡batch size降到1或2然后加大梯度累积。这个关系搞错你会发现自己改了训练脚本里的per_device_train_batch_size却没作用因为ds_config.json里的train_batch_size把deepspeed的前向步数统一接管了。fp16.enabled设为true训练时用混合精度显存能省接近一半。代价是loss scale需要动态调整Deepspeed会自动处理。如果你发现训练过程中loss持续为NaN先关掉fp16试试再用单卡验证数据是否有问题。zero_optimization.stage设为2是ZeRO stage 2切分优化器状态和梯度。stage 2对4卡训练是性价比最高的选择。如果显存仍然不够可以改成3但同时要准备好更大的通信开销训练速度明显变慢。4.2 lora.sh与finetune_lora.pyLoRA从零到多卡的启动方式LoRA是三条路线里最容易上手的因为它只训练注入的低秩矩阵原始模型权重完全冻结。先看启动脚本deepspeed --num_gpus 4 finetune_lora.py \ --train_path instruction_data.json \ --model_name_or_path THUDM/chatglm-6b \ --output_dir ./output/lora \ --num_train_epochs 3 \ --learning_rate 5e-5 \ --per_device_train_batch_size 2 \ --gradient_accumulation_steps 4 \ --deepspeed ds_config.json这里的model_name_or_path可以换成你本地下载好的ChatGLM模型目录。注意deepspeed的启动方式不是python命令而是deepspeed命令后面直接跟训练脚本路径。多卡参数通过--num_gpus指定4代表四卡。如果要用多机多卡需要在启动前配置hostfile这样写只是单机四卡。finetune_lora.py里关于LoRA的核心配置长这样from peft import LoraConfig, TaskType lora_config LoraConfig( r8, lora_alpha32, target_modules[q, v], lora_dropout0.1, task_typeTaskType.CAUSAL_LM )r代表低秩矩阵的秩决定LoRA的表达能力lora_alpha是缩放因子实际权重计算时乘上alpha/r。r越大训练参数量越多效果上限越高但显存也涨。常见做法是先设r8试跑如果欠拟合再调成16或32。target_modules指定了要对模型哪些模块注入LoRA最通用的是q和v也就是attention里的query和value投影层。如果你想提升效果可以加上k和o但显存和训练时间会同步增加。4.3 freeze.sh与finetune_freeze.py冻结参数的边界Freeze方式的思想更朴素前若干层参数不动只训练后面若干层。启动脚本deepspeed --num_gpus 4 finetune_freeze.py \ --train_path instruction_data.json \ --model_name_or_path THUDM/chatglm-6b \ --output_dir ./output/freeze \ --num_train_epochs 3 \ --learning_rate 1e-5 \ --freeze_layers 12 \ --per_device_train_batch_size 2 \ --deepspeed ds_config.jsonfreeze_layers是必须调的参数它表示冻结模型前多少层。具体应该冻结多少取决于模型总层数和任务复杂度。我的习惯是先冻结总层数的一半跑一个epoch看loss变化如果loss降得太慢就减少冻结层数如果显存不够就增加冻结层数。finetune_freeze.py内部会遍历模型所有参数在freeze_layers范围内的层设置requires_gradFalse不在范围内的保持可训练。需要注意冻结层数不是越多越好。你冻结80%的层模型几乎只调整最后几层的输出分布很难学到新能力。反过来冻结层数为0退化成全量微调显存直接爆炸。所以freeze是一条需要在效果和显存之间反复试探的路但它有个好处最终产出的checkpoint是完整模型格式不需要像LoRA那样额外加载adapter部署更简单。4.4 ptuning.sh与finetune_ptuning.py前缀权重的配置细节P-Tuning和前两种思路都不一样它不在模型权重上做文章而是在输入侧拼接一组可训练的前缀向量。启动脚本deepspeed --num_gpus 4 finetune_ptuning.py \ --train_path instruction_data.json \ --model_name_or_path THUDM/chatglm-6b \ --output_dir ./output/ptuning \ --prefix_len 64 \ --num_train_epochs 3 \ --learning_rate 2e-5 \ --per_device_train_batch_size 2 \ --deepspeed ds_config.jsonprefix_len是核心参数控制前缀token的数量。前缀token可以理解为在真实输入前插入一段可学习的虚拟向量它的维度必须和模型的隐藏层维度一致。ChatGLM模型的隐藏层维度较大前缀向量本身会占一部分显存但相比全量微调已经小太多。如果prefix_len太小比如8模型能学到的任务信号不足太大比如256会拖慢训练。常见区间是32到128我用得最多的是64。finetune_ptuning.py和trainer_ptuning.py配合工作后者在训练循环里把真实input_ids和前缀向量拼起来同时保证labels只对真实输出部分计算损失。这里最坑的是推理阶段如果只加载模型权重不加载训练好的前缀参数输出会完全乱掉。项目里的infer_lora.py不支持P-Tuning所以你要验证ptuning效果得额外写一个加载前缀的推理脚本或者从输出目录中把包含前缀参数的checkpoint单独加载。5. 多卡微调常见问题排查五个翻车场景的现象、原因与解决这套项目我前后跑了三轮每次换数据都会遇到新的问题。下面五条是我反复踩过、也帮别人排查过的典型场景按现象、原因、解决三步写清楚。5.1 场景一安装deepspeed包一直报错现象pip install deepspeed时提示Failed to build deepspeed或者报ninja not found再或者编译过程中卡死在某个CUDA文件上。原因deepspeed在安装时会尝试编译自定义CUDA算子常见原因是缺少ninja、gcc或CUDA_HOME环境变量没设置。另一个高频原因是PyTorch版本过新deepspeed源码包还没适配。解决先装ninja设置DS_BUILD_OPS0跳过算子编译再安装pip install ninja export DS_BUILD_OPS0 pip install deepspeed --no-cache安装完成之后运行deepspeed --help能输出参数列表就说明安装成功。如果已经装好了但启动时闪退可以用python -c import deepspeed; print(deepspeed.version)验证导入是否正常。这一步排错之后后续的大多数训练问题都能和deepspeed本身撇清关系。5.2 场景二多卡训练时OOM但单卡正常现象单卡跑batch_size4没问题改成四卡后batch_size4直接OOM有时甚至启动时就报CUDA out of memory。原因多卡时Deepspeed会额外分配通信缓冲区ZeRO stage 2还需要在每卡保留一部分梯度切片的临时缓存这些都不显眼但都会吃显存。另外ds_config.json里的train_batch_size是全局值如果你没重新计算per_device_train_batch_size实际每卡batch可能还是4多卡叠加后峰值显存更高。解决先把每个GPU的micro_batch降到1或2然后通过gradient_accumulation_steps把全局batch补回来。比如4卡每卡batch为2累积2步全局batch就是16。同时确认fp16.enabled是不是true混合精度能省接近一半显存。如果还是OOM把zero stage从2改成3不过要接受更慢的通信。不要在OOM时直接把train_batch_size调小而是要先算清楚上面那个等式。5.3 场景三loss下降但生成效果没变化现象训练loss从2.0降到0.9但用infer_lora.py生成的文本和base模型几乎一样完全没有微调过的痕迹。原因最常见的是LoRA adapter权重没被加载。LoRA训练输出的是adapter_model.bin需要单独加载到peft包装后的模型里不能直接用原来的model.from_pretrained。另一个原因是数据量太少模型学会了输出格式但没有真正学到任务逻辑。解决检查infer_lora.py里是否用peft的from_pretrained加载adapter。正确的加载方式类似from peft import PeftModel model PeftModel.from_pretrained(model, ./output/lora)如果确认加载没问题再检查训练数据。几百条数据训练3个epoch通常只能学到格式学不会复杂映射。我一般会把数据扩到2000条以上并抽10条做验证集看loss和生成结果的趋势是否同步。5.4 场景四多卡通信报错NCCL Timeout现象启动脚本后终端卡在Waiting for fast reconfigure过一段时间报ncclConnection timed out或者提示有rank已经退出。原因NCCL在当前机器上的网络接口选择错误。单机多卡一般走PCIe或NVLink但如果机器有多个网卡NCCL默认选的接口可能是虚拟网卡或外网卡通信链路不通。多机场景则可能是master_addr没有设置各机器之间无法相互访问。解决先加环境变量打开NCCL日志export NCCL_DEBUGINFO deepspeed --num_gpus 4 finetune_lora.py ...日志中会明确写NCCL选择了哪个接口。如果是内网环境把通信接口强制到对应网卡上。单机多卡我常用这条export NCCL_SOCKET_IFNAMEeth0多机多卡时还需要显式指定master地址和端口。这个问题的现象看起来像deepspeed卡死但本质是网络配置解决后整机都能跑。5.5 场景五不同微调脚本之间的模型加载不一致现象用lora.sh训练完把输出目录直接传给infer_lora.py报key不匹配或形状不匹配或者freeze训练完的checkpoint用transformers接口加载失败。原因三种微调方式产出的checkpoint格式完全不同。LoRA产出adapter_model.bin只包含低秩矩阵Freeze产出的是完整模型state_dict但其中一部分层参数被冻结P-Tuning产出的是前辍参数文件。三者的加载接口根本不通用。解决每种微调方式使用独立的输出目录并记住对应的加载方式。LoRA用PeftModel加载adapterFreeze用model.load_state_dict加载全部权重P-Tuning需要单独加载前缀参数。项目里的infer_lora.py只处理LoRA场景所以不要拿freeze的checkpoint往里面塞。我在实际项目中会建立一个模型注册表把每种微调方式的训练输出路径和加载接口写在一个配置文件里避免时间久了混淆。6. 进阶用infer_lora.py验证微调效果并量化显存收益训练完不等于项目结束验证和收益评估同样重要。infer_lora.py是项目里专门用来验证LoRA训练效果的脚本跑通它之后你才能确认自己调出的adapter真的起作用了。python infer_lora.py \ --model_name_or_path THUDM/chatglm-6b \ --lora_weights ./output/lora \ --prompt 介绍一下DeepSpeed脚本加载基础模型然后用PeftModel挂载训练好的LoRA权重把prompt输入模型并生成文本。如果输出对话内容和训练数据中的风格明显相关说明微调生效了。我建议至少准备5条测试prompt一条来自训练集一条改写自训练集三条来自真实业务场景。只有训练集内效果好是不够的泛化能力才是微调的关键。做完初步验证可以量化一下显存收益。以我个人的实验经验LoRA配合Deepspeed stage 2在相同全局batch size下相比全量微调的峰值显存占用通常能下降30%到50%。这取决于你和LoRA的r值、冻结层数以及模型规模。量化方法很简单固定batch size1分别用全量微调脚本和lora.sh跑一步通过nvidia-smi记录每卡最大显存两者相减就是节省量。数据记录下来哪怕只少一两G也能让你在选型时有明确依据。我推荐你下一步做的是把三种脚本在同一份数据上各跑一次比较它们的loss曲线、显存峰值和生成效果。你会发现一个常见规律LoRA收敛最快Freeze效果最稳P-Tuning在生成类任务上有惊喜。这套源码的价值就在这里——它不是给你一个“标准答案”而是把三种主流方案都摊开让你自己根据业务做决策。从那以后我每次拿到一个新的大模型微调项目都会强制自己先走一遍清理文件清单、固定环境版本、用10条数据跑通最小batch再换完整数据。这样看似浪费时间反而能把翻车率降到最低。希望这套源码和这些踩坑记录能帮到你。本文还有配套的精品资源点击获取