基于DeepSpeed的多卡ChatGLM微调实战:LoRA、Ptuning与Freeze源码解析
简介本资源面向希望上手大模型微调的开发者与研究者聚焦用Deepspeed对ChatGLM进行多卡并行训练这一实战场景提供可运行的源码与配套流程教程帮助跨过环境配置与分布式训练的门槛。压缩包共17个文件约118KB以11个Python脚本为核心覆盖模型加载、数据加载、训练循环与评估测试等模块另有3个Shell启动脚本、2个JSON配置与1份Markdown说明结构清晰便于按模块阅读与改写。教程按环境搭建、数据准备、模型微调、模型评估的顺序展开重点讲解Deepspeed参数配置与多卡训练脚本的启动方式并给出LoRA、P-Tuning、Freeze等不同微调策略的实现参考。目前已有267人学习适合具备一定PyTorch基础、想快速复现多卡微调流程的读者对照源码实践。1. 从一张 3090 到四卡 A100这套 ChatGLM 微调源码到底解决了什么单卡 24G 显存跑 ChatGLM-6B 的 LoRA 微调序列长度拉到 512、batch size 设到 4 就开始 OOM这是很多人第一次做大模型微调时的真实处境。这套「基于 Deepspeed 实现多卡 ChatGLM 微调」的项目源码核心价值就在于把多卡并行、显存优化、参数高效微调这三件事打包成了一套能直接跑通的工程骨架。它覆盖了 Freeze、Ptuning、LoRA 三种微调范式配套ds_config.json做 Deepspeed 的 ZeRO 配置finetune_lora.py、finetune_ptuning.py、finetune_freeze.py三个入口脚本分别对应不同策略modeling_chatglm.py和tokenization_chatglm.py则是模型与分词器的本地实现避免依赖版本漂移。适合谁手里有两张以上 GPU、想把 ChatGLM 接到自己业务数据上、又不想从零搭训练框架的从业者。它不教你 Transformer 原理它解决的是「数据准备好了怎么让多张卡一起干活且不炸显存」这个具体问题。2. 环境搭建与依赖锁定为什么这套源码必须锁死版本2.1 依赖矩阵与版本冲突的真实来源ChatGLM 的modeling_chatglm.py是项目自带的一份模型实现它和 transformers 库的版本强绑定。如果你直接pip install transformers装最新版大概率会在 import 阶段就报cannot import name XXX from transformers。常见做法是锁定 transformers 在 4.27 到 4.33 这个区间torch 用 1.13 或 2.0 配对应 CUDA 版本deepspeed 用 0.9.x 系列。这套源码里的modeling_chatglm.py用的是老版 rotary embedding 的写法新版 transformers 改了接口所以版本不能随便升。下面是我一般会用的环境初始化流程注意 CUDA 版本要和 torch 的 cu 后缀对齐# 创建独立环境避免污染已有 torch conda create -n chatglm_ds python3.10 -y conda activate chatglm_ds # torch 与 CUDA 对齐这里以 CUDA 11.7 为例 pip install torch1.13.1cu117 torchvision0.14.1cu117 \ --extra-index-url https://download.pytorch.org/whl/cu117 # 锁定 transformers 与 deepspeed 版本区间 pip install transformers4.30.2 deepspeed0.9.5 \ datasets2.12.0 accelerate0.20.3 \ sentencepiece protobuf3.20.3逻辑说明torch 的cu117后缀决定了它链接的 CUDA 运行时和系统nvcc --version显示的版本不要求完全一致但驱动版本要够。protobuf3.20.3这个锁定是血泪经验新版 protobuf 会让 sentencepiece 加载 tokenizer 时报Descriptors cannot not be created directly。deepspeed 0.9.5 是这套源码验证过的版本再往上ds_config.json里某些字段名会变。2.2 验证多卡可见性与通信后端环境装完先别急着跑训练先确认四件事GPU 数量、NCCL 可用性、deepspeed 能否识别设备、以及单卡前向能不能过。很多人跳过这步结果训练脚本跑到一半才报 NCCL 超时排查成本翻倍。# 1. 确认 GPU 数量与显存 nvidia-smi --query-gpuindex,name,memory.total --formatcsv # 2. 检查 NCCL 是否可用多卡通信的核心 python -c import torch; print(NCCL:, torch.cuda.nccl.version()) # 3. 检查 deepspeed 环境报告 ds_report # 4. 单卡加载模型前向确认 modeling 文件无 import 错误 python -c import torch from modeling_chatglm import ChatGLMForConditionalGeneration print(modeling import ok) 参数说明torch.cuda.nccl.version()返回一个元组能打印出来就说明 NCCL 编译进了 torch。ds_report会列出 deepspeed 检测到的 accelerator、通信库、以及是否装了apex等可选依赖。如果ds_report里NCCL显示 not found多卡训练一定起不来常见原因是 conda 环境里的 torch 是 CPU 版或者 CUDA 运行时没装全。单卡前向这步能提前暴露modeling_chatglm.py和 transformers 的接口冲突比在训练循环里报错好定位得多。3. 三种微调范式的脚本拆解Freeze、Ptuning、LoRA 怎么选3.1 三种策略的显存与效果对比这套源码把三种参数高效微调都给了不是让你全跑一遍而是让你按显存预算和任务类型选。Freeze 只训练最后几层显存最省但效果上限低Ptuning 用 soft prompt参数量极小适合少样本LoRA 在注意力层注入低秩矩阵效果和显存平衡最好也是目前最主流的选择。下面这张表是我按单卡 24G、序列长度 512 的实测区间整理的具体数值随 batch size 浮动策略可训练参数占比单卡 24G 可跑 batch适用场景入口脚本Freeze约 5%10%816领域词汇适配、快速验证finetune_freeze.pyPtuning约 0.1%1632少样本、多任务切换finetune_ptuning.pyLoRA约 0.5%2%816指令跟随、通用微调finetune_lora.py选型逻辑如果你的数据是几千条指令对直接上 LoRA如果只有几百条且任务边界清晰Ptuning 更稳如果只是想验证数据格式对不对Freeze 跑一轮最快。注意 LoRA 的 rank 和 alpha 在finetune_lora.py里是硬编码的改之前先确认显存。3.2 LoRA 微调脚本的关键参数与启动方式finetune_lora.py是这套源码里最常用的入口。它内部做了三件事用data.py加载instruction_data.json、用modeling_chatglm.py加载基座、用trainer_pt.py接管训练循环。启动方式不是直接python finetune_lora.py而是通过lora.sh包装了 deepspeed 的 launch。# lora.sh 的核心内容我一般会改成自己的路径 deepspeed --num_gpus4 finetune_lora.py \ --deepspeed ds_config.json \ --model_name_or_path ./chatglm-6b \ --data_path ./instruction_data.json \ --output_dir ./output_lora \ --num_train_epochs 3 \ --per_device_train_batch_size 4 \ --gradient_accumulation_steps 4 \ --learning_rate 2e-4 \ --max_seq_length 512 \ --lora_rank 8 \ --lora_alpha 32 \ --fp16 True逻辑说明--num_gpus4告诉 deepspeed 用四张卡它会在每张卡上起一个进程数据由data.py里的 DistributedSampler 切分。per_device_train_batch_size是单卡 batch乘上卡数和梯度累积步数才是全局 batch。gradient_accumulation_steps4配合单卡 batch 4、四卡全局 batch 就是 64这个值对 6B 模型的 LoRA 比较稳。lora_rank8和lora_alpha32是常见起点rank 越大可训练参数越多显存也涨。fp16 True在 A100 上可以换bf163090 上只能用 fp16。3.3 ds_config.json 里 ZeRO 阶段怎么配ds_config.json是 Deepspeed 的核心它决定了显存优化策略。这套源码默认给的是 ZeRO-2 或 ZeRO-3区别在于优化器状态和梯度的切分粒度。ZeRO-2 切分优化器状态和梯度ZeRO-3 连模型参数也切分。对 6B 模型的多卡微调ZeRO-2 通常够用ZeRO-3 在卡多但单卡显存小的时候才需要。{ train_batch_size: 64, gradient_accumulation_steps: 4, fp16: { enabled: true, loss_scale: 0, initial_scale_power: 16 }, zero_optimization: { stage: 2, offload_optimizer: { device: cpu, pin_memory: true }, allgather_partitions: true, overlap_comm: true, contiguous_gradients: true }, steps_per_print: 50, wall_clock_breakdown: false }参数说明train_batch_size必须等于per_device_train_batch_size × num_gpus × gradient_accumulation_steps对不上 deepspeed 会直接报错。offload_optimizer把优化器状态放到 CPU 内存能省显存但会拖慢速度单卡显存够就别开。overlap_comm让通信和计算重叠多卡场景建议开。initial_scale_power是 loss scale 的初始值fp16 训练出现 loss 为 nan 时可以调大这个值。steps_per_print控制日志频率调小能看到更细的 loss 曲线。4. 数据准备与训练启动instruction_data.json 的格式与踩坑4.1 指令数据的字段结构与预处理链路instruction_data.json是这套源码的数据入口data.py负责把它转成模型能吃的 token 序列。常见格式是每条样本带instruction、input、output三个字段data.py会拼成 ChatGLM 的 prompt 模板。如果你的数据字段名不一样改data.py里的拼接逻辑别改 json 结构否则 tokenizer 那边对不上。# data.py 里数据拼接的核心逻辑我一般会按这个结构检查 def build_prompt(example): # ChatGLM 的对话模板注意 [Round] 标记不能少 prompt f[Round 1]\n\n问{example[instruction]}\n\n if example.get(input): prompt f补充{example[input]}\n\n prompt f答{example[output]} return prompt # tokenize 时要做 truncation 和 padding tokenized tokenizer( prompt, max_length512, truncationTrue, paddingmax_length, return_tensorspt )逻辑说明ChatGLM 的对话模板对[Round]标记敏感漏了会导致模型学不到对话边界。truncationTrue在序列超长时从尾部截断如果你的 output 很长截断会把答案切掉这时候要么加长max_seq_length要么在数据侧先过滤超长样本。paddingmax_length会补齐到固定长度配合data.py里的 collator 使用如果显存紧张可以改成动态 padding。4.2 启动训练与日志观察数据格式确认后用lora.sh启动。启动后不要只看终端刷屏重点看三个信号loss 是否下降、显存是否稳定、以及 deepspeed 的通信日志有没有报错。# 启动四卡 LoRA 训练日志同时写文件 bash lora.sh 21 | tee train_lora.log # 另开终端观察显存 watch -n 2 nvidia-smi # 从日志里提取 loss 曲线 grep loss train_lora.log | tail -20逻辑说明tee把 stdout 和 stderr 同时写到文件方便事后排查。watch nvidia-smi看显存是否在训练过程中持续上涨如果一直涨到 OOM说明有变量没释放常见原因是data.py里把整个数据集缓存在了 GPU 上。grep loss看 loss 是否在合理区间波动LoRA 微调初期 loss 在 2 到 4 之间正常如果一直是 nan回去检查ds_config.json的 fp16 配置和initial_scale_power。5. 多卡训练避坑从 NCCL 超时到 loss 不收敛的排查清单5.1 现象启动即报 NCCL timeout 或 connection refused原因多卡通信依赖 NCCL而 NCCL 需要正确的网卡和端口。常见触发条件是MASTER_ADDR和MASTER_PORT没设或者防火墙拦了端口。另一个高频原因是--num_gpus设的数量和实际可见 GPU 数不一致deepspeed 会去连不存在的 rank。解决在lora.sh里显式导出环境变量并确认CUDA_VISIBLE_DEVICES和--num_gpus对齐。export MASTER_ADDR127.0.0.1 export MASTER_PORT29500 export NCCL_DEBUGINFO export CUDA_VISIBLE_DEVICES0,1,2,3 deepspeed --num_gpus4 finetune_lora.py ...NCCL_DEBUGINFO会打印 NCCL 的详细连接过程能看到它选了哪张网卡。如果机器有多张网卡可以用NCCL_SOCKET_IFNAME指定。5.2 现象训练中途 loss 突然变 nan原因fp16 训练的经典问题梯度溢出导致 loss scale 失效。也可能是学习率设太大LoRA 的learning_rate超过 5e-4 就容易炸。还有一种情况是数据里有空样本tokenize 后全是 padding前向输出异常。解决先把learning_rate降到 1e-4 到 2e-4 区间然后在ds_config.json里把initial_scale_power从 16 调到 20让 loss scale 起点更高。同时检查instruction_data.json里有没有output为空的条目。5.3 现象四卡训练比单卡还慢原因ZeRO-3 在小模型上通信开销大于收益或者offload_optimizer开了但 CPU 内存带宽不够。也可能是per_device_train_batch_size太小每张卡算力没吃满时间都花在通信上。解决6B 模型优先用 ZeRO-2关掉offload_optimizer把单卡 batch 尽量调大。如果还是慢检查overlap_comm是否开启以及 NCCL 是否走了 PCIe 而不是 NVLink。5.4 现象模型加载时报 missing keys 或 unexpected keys原因modeling_chatglm.py里的模型结构和下载的权重不匹配通常是权重版本和代码版本不一致。也可能是 LoRA 微调后加载权重时基座和 adapter 的 key 前缀对不上。解决确认--model_name_or_path指向的权重和modeling_chatglm.py是同一版本。LoRA 推理时用infer_lora.py它会先加载基座再挂 adapter不要直接用from_pretrained加载合并后的权重。5.5 现象Ptuning 训练 loss 不降原因Ptuning 的 soft prompt 参数量极小学习率需要比 LoRA 高一个量级。如果沿用 LoRA 的 2e-4soft prompt 几乎不动。另外trainer_ptuning.py里的 prefix 长度如果设得太短模型没有足够的可学习空间。解决把 Ptuning 的学习率调到 1e-3 到 5e-3prefix 长度从 128 起步。同时确认finetune_ptuning.py里冻结了基座参数只训练 prompt 部分。6. 推理验证与 LoRA 权重合并怎么确认微调真的生效训练跑完不是终点得验证模型有没有学到东西。这套源码给了infer_lora.py它的逻辑是先加载基座 ChatGLM再把 LoRA adapter 挂上去然后走生成流程。我一般会做两件事一是用训练集里的样本测看输出是否贴合二是用没见过的指令测看泛化。下面这段是infer_lora.py的核心调用方式from modeling_chatglm import ChatGLMForConditionalGeneration from tokenization_chatglm import ChatGLMTokenizer from peft import PeftModel # 先加载基座再挂 LoRA adapter base_model ChatGLMForConditionalGeneration.from_pretrained( ./chatglm-6b, trust_remote_codeTrue ).half().cuda() model PeftModel.from_pretrained(base_model, ./output_lora) model model.eval() tokenizer ChatGLMTokenizer.from_pretrained(./chatglm-6b) # 构造和训练时一致的 prompt 模板 prompt [Round 1]\n\n问你的指令\n\n答 inputs tokenizer(prompt, return_tensorspt).to(cuda) with torch.no_grad(): outputs model.generate( **inputs, max_length512, do_sampleTrue, top_p0.7, temperature0.95 ) print(tokenizer.decode(outputs[0], skip_special_tokensTrue))逻辑说明PeftModel.from_pretrained会把 adapter 权重挂到基座的对应层上不需要手动合并。prompt的格式必须和data.py里训练时完全一致差一个换行都会让输出质量下降。top_p和temperature是生成参数验证时先用默认值确认模型能答对再调。如果输出全是重复或乱码先检查 prompt 模板再检查 adapter 是否真的加载成功——可以打印model.peft_config确认。如果你要把 LoRA 权重合并进基座做部署用merge_and_unload()但注意合并后的模型体积和基座一样大显存占用也回到全量模型水平。我一般验证阶段不合并直接用 PeftModel 挂载省显存也方便切换不同 adapter。从那以后我每次微调完都强制走一遍「训练集样本 未见指令 边界输入」三组测试确认模型不是只记住了训练数据的表面模式。希望帮到你。本文还有配套的精品资源点击获取