
简介这份资源面向希望掌握大模型微调与分布式训练的开发者聚焦LORA、DeepSpeed与多GPU环境下ChatGLM的实战落地帮助解决显存受限、通信开销大、训练效率低等常见问题适合具备一定深度学习基础、想进阶大模型工程能力的中高级读者。压缩包共376个文件约170.03MB以193个Python脚本为核心配套json、yaml、sh等配置与启动文件另有pickle、pkl、pt等权重与数据缓存以及jpg、png、pdf、md等说明与图示资料结构完整便于按模块查阅。目前已有795人学习下载。项目源码覆盖模型初始化、预训练权重加载、损失函数定义、优化器与学习率调度、多GPU并行配置及数据准备处理等环节并涉及模型评估与验证技巧读者可据此理解LORA低秩分解如何降低通信开销、DeepSpeed如何协调多卡训练从而将方法迁移到自己的对话系统与微调任务中。1. ChatGLM 微调为什么绕不开 LoRA DeepSpeed 这套组合单卡 24G 显存想动 ChatGLM3-6B 的全量微调基本是开局就翻车。模型权重 fp16 加载就要 12G 以上再加优化器状态、梯度、激活值全量微调至少需要 8 张 A100 80G 才跑得舒服。绝大多数团队手里只有 2 到 4 张消费级卡或者单张 A100这时候 LoRA 加 DeepSpeed 就是最务实的路线。LoRA 冻结原始权重只训练低秩旁路矩阵可训练参数直接压到原模型的 0.1% 到 1%显存占用断崖式下降DeepSpeed 的 ZeRO 阶段再把优化器状态、梯度、参数分片到多张卡上让多 GPU 并行真正跑起来。这套组合解决的核心问题是在有限显存下用多张卡把 ChatGLM 的指令微调跑通并且效果能接近全量微调。适合手里有 2 到 8 张 GPU、想做行业大模型微调实战的工程师也适合刚接触大模型微调、想找一个能复现的 LoRA 微调代码入口的新手。下面从环境配置一路讲到多卡启动、参数配置和踩坑排查源码结构按常见项目布局展开不虚构具体仓库地址。2. 环境配置与 ChatGLM 权重准备从零把底座跑起来2.1 显存账先算清楚LoRA 和 DeepSpeed 各自省在哪动手之前先把显存账算明白否则后面调参全是玄学。ChatGLM3-6B 的 fp16 权重约 12.4G全量微调时优化器如果用 Adamfp32 的动量加方差就是权重的 8 倍约 99G加上梯度 12G 和激活值单卡根本放不下。LoRA 的做法是在 Attention 的 Q、K、V、O 投影层旁边挂低秩矩阵 A 和 B原始权重冻结不产生梯度可训练参数量由秩 r 决定。以 r8 为例单层新增参数约几万全模型加起来通常 300 万到 800 万优化器状态随之缩到几十 MB。DeepSpeed ZeRO 解决的是另一件事多卡训练时每张卡都存一份完整优化器状态太浪费ZeRO-2 把优化器状态和梯度切分到各卡ZeRO-3 连参数也切分。实际微调 ChatGLM 常用 ZeRO-2因为 ZeRO-3 通信开销大6B 模型在 2 到 4 卡上 ZeRO-2 已经够用。选型理由很直接LoRA 降可训练参数DeepSpeed 降每卡冗余两者叠加才能让 2 张 24G 卡跑起 6B 模型的微调。2.2 环境安装CUDA、PyTorch、DeepSpeed 的版本对齐环境配置是第一个大坑版本不对齐后面报错能查到怀疑人生。常见做法是 CUDA 11.8 配 PyTorch 2.1 到 2.3DeepSpeed 用 0.13 以上。先建虚拟环境再装依赖避免污染系统 Python。# 创建并激活虚拟环境 conda create -n chatglm_lora python3.10 -y conda activate chatglm_lora # 安装 PyTorchCUDA 11.8 版本 pip install torch2.1.2 torchvision0.16.2 torchaudio2.1.2 \ --index-url https://download.pytorch.org/whl/cu118 # 安装 DeepSpeed 和微调相关依赖 pip install deepspeed0.13.1 pip install transformers4.40.0 peft0.10.0 datasets2.18.0 \ accelerate0.29.0 sentencepiece0.2.0 protobuf4.25.3这段命令的逻辑是先锁定 PyTorch 的 CUDA 版本再装 DeepSpeed。DeepSpeed 安装时会编译算子如果 CUDA 版本和 PyTorch 不匹配编译阶段就会失败。参数上transformers 和 peft 的版本要匹配peft 0.10 对应 transformers 4.40 比较稳。装完用python -c import torch; print(torch.cuda.is_available())验证输出 True 才算过。如果 DeepSpeed 编译报错先确认nvcc --version和 PyTorch 的 CUDA 版本一致不一致就重装 PyTorch。2.3 权重下载与目录结构别把模型塞进代码目录ChatGLM 权重从公开模型平台获取常见做法是下载到独立的数据盘目录不要放在代码仓库里否则 git 操作和打包都会很痛苦。目录结构建议这样组织project/ ├── configs/ │ ├── ds_zero2.json │ └── lora_config.json ├── data/ │ ├── train.json │ └── val.json ├── scripts/ │ ├── train.sh │ └── merge_lora.py ├── src/ │ ├── dataset.py │ └── train.py └── output/ └── chatglm_lora/权重目录单独放在/data/models/chatglm3-6b训练脚本里用绝对路径引用。数据格式用 JSON 行每行一条样本字段包含instruction、input、output这是 ChatGLM 微调最常见的格式。数据量上指令微调一般准备 5000 到 50000 条太少容易过拟合太多单轮训练时间拉长。验证集留 5% 到 10%用来观察 loss 是否发散。3. LoRA 参数配置与数据管道把可训练参数压到最低3.1 LoRA 的 r、alpha、target_modules 怎么定LoRA 参数配置直接决定微调效果和显存占用三个核心参数是 r、lora_alpha、target_modules。r 是低秩矩阵的秩越大表达能力越强但参数越多常见取值 8、16、32。指令微调任务 r8 到 16 通常够用任务越复杂越往 32 靠。lora_alpha 是缩放系数实际缩放比例是 alpha/r常见做法是设成 r 的两倍比如 r8 时 alpha16这样缩放比例稳定在 2。target_modules 决定挂载位置ChatGLM 的 Attention 层里 Q、K、V、O 投影都值得挂只挂 Q、V 也能跑但效果略差。from peft import LoraConfig, TaskType lora_config LoraConfig( task_typeTaskType.CAUSAL_LM, # 因果语言模型任务 r8, # 低秩矩阵秩8 到 32 lora_alpha16, # 缩放系数通常为 r 的两倍 lora_dropout0.05, # 防过拟合0.05 到 0.1 target_modules[query_key_value], # ChatGLM 的 QKV 合并投影层 biasnone, # 不训练 bias inference_modeFalse )这段配置里target_modules写的是 ChatGLM 特有的query_key_value因为 ChatGLM 把 Q、K、V 合并成一个投影层这点和 LLaMA 系列不同照抄 LLaMA 的q_proj、v_proj会报找不到模块。lora_dropout设 0.05 是轻量正则数据量小的时候可以提到 0.1。bias设 none 表示不训练偏置项省参数。如果显存还有余量把 r 提到 16、alpha 提到 32效果通常有可见提升。3.2 数据管道tokenize、截断和 collator 的细节数据管道负责把原始 JSON 转成模型能吃的 input_ids 和 labels。ChatGLM 的对话格式有固定模板常见做法是把 instruction、input、output 拼成带特殊 token 的序列然后 tokenize 并截断到 max_length。截断策略很关键直接截尾会丢掉 output 部分导致模型学不到回答正确做法是优先保留 output从 instruction 侧截断。from torch.utils.data import Dataset import torch class SFTDataset(Dataset): def __init__(self, data, tokenizer, max_length2048): self.data data self.tokenizer tokenizer self.max_length max_length def __len__(self): return len(self.data) def __getitem__(self, idx): item self.data[idx] # 按 ChatGLM 对话模板拼接 prompt f[Round 1]\n\n问{item[instruction]}\n{item[input]}\n\n答 response item[output] prompt_ids self.tokenizer.encode(prompt, add_special_tokensFalse) response_ids self.tokenizer.encode(response, add_special_tokensFalse) # 优先保留 response从 prompt 侧截断 if len(prompt_ids) len(response_ids) self.max_length: keep self.max_length - len(response_ids) prompt_ids prompt_ids[-keep:] if keep 0 else [] input_ids prompt_ids response_ids [self.tokenizer.eos_token_id] labels [-100] * len(prompt_ids) response_ids [self.tokenizer.eos_token_id] return { input_ids: torch.tensor(input_ids, dtypetorch.long), labels: torch.tensor(labels, dtypetorch.long), attention_mask: torch.ones(len(input_ids), dtypetorch.long) }这段代码的核心逻辑是 labels 里 prompt 部分用 -100 屏蔽只对 response 计算 loss这是指令微调的标准做法。max_length设 2048 是 ChatGLM3 的常见上下文长度显存紧张可以降到 1024。截断时prompt_ids[-keep:]取尾部保证靠近回答的上下文不丢。collator 用 transformers 自带的 DataCollatorForSeq2Seq 或者自己写 padding 逻辑注意 padding 侧要和 tokenizer 的 padding_side 一致ChatGLM 通常用左侧 padding。3.3 训练脚本把 LoRA 和 DeepSpeed 接起来训练脚本负责组装模型、注入 LoRA、加载 DeepSpeed 配置并启动 Trainer。关键点是模型加载时用 fp16 或 bf16注入 LoRA 后再交给 TrainerDeepSpeed 配置通过deepspeed参数传入。from transformers import AutoModel, AutoTokenizer, TrainingArguments, Trainer from peft import get_peft_model import json model_path /data/models/chatglm3-6b tokenizer AutoTokenizer.from_pretrained(model_path, trust_remote_codeTrue) model AutoModel.from_pretrained( model_path, trust_remote_codeTrue, torch_dtypetorch.float16 ) model get_peft_model(model, lora_config) model.print_trainable_parameters() # 确认可训练参数占比 train_data [json.loads(l) for l in open(data/train.json, encodingutf-8)] train_dataset SFTDataset(train_data, tokenizer, max_length2048) training_args TrainingArguments( output_diroutput/chatglm_lora, per_device_train_batch_size2, # 单卡 batch显存紧张就降到 1 gradient_accumulation_steps8, # 累积到等效 batch 16 learning_rate2e-4, # LoRA 常用 1e-4 到 5e-4 num_train_epochs3, logging_steps10, save_steps200, fp16True, deepspeedconfigs/ds_zero2.json, report_tonone ) trainer Trainer( modelmodel, argstraining_args, train_datasettrain_dataset, data_collatorDataCollatorForSeq2Seq(tokenizer, paddingTrue) ) trainer.train()per_device_train_batch_size设 2、gradient_accumulation_steps设 8等效 batch 是 16这是 2 卡 24G 下比较稳的组合。学习率 2e-4 是 LoRA 的常用值比全量微调的 1e-5 高一个量级因为可训练参数少需要更大步长。fp16True要和模型加载的 dtype 一致用 bf16 的话两边都改。print_trainable_parameters()会打印可训练参数占比正常在 0.1% 到 1% 之间如果超过 2% 说明 target_modules 挂多了或者 r 设太大。4. DeepSpeed 多 GPU 启动ZeRO 配置与分布式训练实操4.1 ZeRO-2 配置文件逐项拆解DeepSpeed 的行为由 JSON 配置文件控制ZeRO-2 是 ChatGLM 微调最常用的阶段。下面这份配置是 2 到 4 卡场景的常见起点。{ train_batch_size: 16, gradient_accumulation_steps: 8, fp16: { enabled: true, loss_scale: 0, initial_scale_power: 16 }, zero_optimization: { stage: 2, allgather_partitions: true, allgather_bucket_size: 5e8, overlap_comm: true, reduce_scatter: true, reduce_bucket_size: 5e8, contiguous_gradients: true }, gradient_clipping: 1.0, steps_per_print: 10 }train_batch_size是全局 batch等于单卡 batch 乘卡数乘累积步数这里 2 乘 2 卡乘 8 等于 32配置里写 16 的话要保证和 TrainingArguments 对得上否则 DeepSpeed 会报 batch 不一致。stage: 2表示切分优化器状态和梯度。overlap_comm: true让通信和计算重叠能提速。allgather_bucket_size和reduce_bucket_size设 5e8 是常见值显存紧张可以降到 2e8。gradient_clipping设 1.0 防梯度爆炸LoRA 训练里这个值比较稳。initial_scale_power设 16 是 fp16 的初始 loss scale如果训练早期出现 loss 为 nan可以降到 12 或 10。4.2 多卡启动命令与常见报错启动多卡训练用 torchrun 或 deepspeed 启动器常见做法是用 torchrun因为它和 Trainer 集成更顺。# 2 卡启动指定 CUDA 可见设备 CUDA_VISIBLE_DEVICES0,1 torchrun \ --nproc_per_node2 \ --master_port29500 \ src/train.py # 或者用 deepspeed 启动器 deepspeed --num_gpus2 src/train.py \ --deepspeed configs/ds_zero2.jsonnproc_per_node等于使用的卡数master_port选一个没被占用的端口冲突了就换。启动后如果卡在初始化不动先检查 NCCL 通信常见做法是设export NCCL_DEBUGINFO看日志。报CUDA out of memory就降单卡 batch 或 max_length。报unexpected keyword argument多半是 transformers 和 peft 版本不匹配。多卡训练时每张卡的显存占用应该接近如果某张卡明显高检查数据是否均匀切分。4.3 训练过程监控loss 曲线和显存占用怎么看训练启动后要盯两个东西loss 和显存。loss 正常应该在前 100 步快速下降然后缓慢收敛如果一直震荡或者变 nan先查学习率是否太大、fp16 的 loss scale 是否合适。显存占用用nvidia-smi -l 1实时看2 卡 24G 跑 6B 模型 LoRA每卡占用通常在 18G 到 22G留一点余量比较安全。如果显存打满还报 OOM优先降 max_length其次降 batch最后考虑 ZeRO-3。日志里steps_per_print控制打印频率设 10 表示每 10 步打一次 loss。训练中途想验证效果可以每 200 步存一次 checkpoint用验证集算 loss 或者直接推理看输出质量。5. 避坑与排查LoRA DeepSpeed 微调最常见的 5 个翻车点5.1 现象训练启动就报 target_modules 找不到原因ChatGLM 的 Attention 投影层命名和 LLaMA 不同照抄 LLaMA 的q_proj、v_proj会找不到模块。解决先打印模型结构确认层名ChatGLM 用query_key_value有的版本还用dense、dense_h_to_4h。用print(model)看一遍把 target_modules 改成实际存在的层名。5.2 现象loss 一直是 nan 或者不下降原因常见有三种学习率太大、fp16 的 loss scale 不合适、数据里混入了空样本。解决先把学习率降到 1e-4 试再把 DeepSpeed 配置里initial_scale_power从 16 降到 12最后检查数据过滤掉 output 为空的样本。如果还不行改用 bf16bf16 的动态范围比 fp16 大不容易溢出。5.3 现象多卡训练比单卡还慢原因通信开销盖过了并行收益常见于卡数少、模型小、batch 小的情况。解决确认overlap_comm开了allgather_bucket_size不要设太小2 卡跑 6B 模型本身加速比就有限如果单卡能跑就别硬上多卡。另外检查是不是数据加载成了瓶颈把 dataloader 的 num_workers 调大。5.4 现象保存的 LoRA 权重合并后推理效果差原因合并时没把 LoRA 权重按 alpha/r 缩放或者合并脚本用错了基座模型。解决用 peft 的merge_and_unload()合并它会自动处理缩放。合并前确认基座模型路径和训练时一致合并后先用几条训练集里的样本推理输出正常再上验证集。5.5 现象训练完显存没释放下次启动 OOM原因上一个进程没退干净GPU 显存被僵尸进程占着。解决nvidia-smi找到占显存的进程号kill -9掉。预防做法是训练脚本里加异常捕获出错时主动释放模型和清空缓存torch.cuda.empty_cache()在退出前调一次。6. LoRA 权重合并与推理验证把微调结果真正用起来训练跑完只是拿到 LoRA 适配器要真正用起来得合并权重或者加载适配器推理。合并的好处是推理时不用额外加载 peft速度快一点不合并的好处是基座模型和适配器分开存切换任务方便。我一般先不合并用 peft 加载适配器直接推理验证效果确认没问题再合并存档。from peft import PeftModel from transformers import AutoModel, AutoTokenizer base_path /data/models/chatglm3-6b lora_path output/chatglm_lora/checkpoint-600 tokenizer AutoTokenizer.from_pretrained(base_path, trust_remote_codeTrue) base_model AutoModel.from_pretrained( base_path, trust_remote_codeTrue, torch_dtypetorch.float16 ).cuda() model PeftModel.from_pretrained(base_model, lora_path) model model.eval() response, history model.chat( tokenizer, 你的微调任务指令, history[], max_length2048, temperature0.7 ) print(response)这段推理代码的关键是PeftModel.from_pretrained把适配器挂到基座模型上model.chat是 ChatGLM 自带的对话接口。temperature设 0.7 是常用值要更稳定就降到 0.1。验证时准备 20 到 50 条没参与训练的指令对比微调前后的输出重点看格式遵循和领域知识是否提升。如果输出重复或者答非所问多半是训练数据质量或者 epoch 太多导致过拟合减少 epoch 或者加数据。合并权重的脚本用merge_and_unload()合并后存成标准模型目录推理时直接加载不再需要 peft。merged_model model.merge_and_unload() merged_model.save_pretrained(output/chatglm_lora_merged, safe_serializationTrue) tokenizer.save_pretrained(output/chatglm_lora_merged)safe_serializationTrue存成 safetensors 格式加载更快也更安全。合并后的模型目录可以直接被 vLLM 等推理框架加载做本地部署大模型或者接入应用。参数上合并过程需要和训练时相同的显存6B 模型 fp16 合并大约要 14G 显存单卡够用。最后说个我自己的习惯每次微调一定先跑 100 步的小规模实验确认 loss 下降、显存稳定、保存和加载都正常再放开跑全量。这个习惯帮我省过很多次通宵重跑的后悔药。LoRA 参数别一次调太多r、alpha、学习率一次只动一个记录每次的 loss 曲线慢慢就有手感了。希望帮到你。本文还有配套的精品资源点击获取