ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

ChatGLM微调实战:LoRA+DeepSpeed多卡训练与避坑指南

ChatGLM微调实战:LoRA+DeepSpeed多卡训练与避坑指南 简介这份资源面向希望掌握大模型微调与分布式训练的开发者聚焦如何用LORA结合DeepSpeed在多GPU环境下对ChatGLM进行实战微调。内容涵盖低秩矩阵分解降低通信与存储开销、零冗余优化器与混合精度训练、模型并行与数据并行协调以及对话生成场景下的上下文理解与生成模块针对性训练适合具备一定深度学习基础、想进阶大模型工程能力的中高级读者。压缩包共376个文件以193个Python脚本为核心辅以json配置、txt说明、md文档、sh启动脚本及pickle、pt权重文件另有少量图片与数据集文件整体约170.03MB目录结构便于按模块查阅。已有795人学习下载。项目源码完整呈现训练脚本与参数设置包括模型初始化、预训练权重加载、损失函数定义、优化器与学习率调度并涉及数据准备、模型评估与验证技巧可帮助读者理解LORA与DeepSpeed的联合应用将微调流程迁移到自己的对话系统项目中。1. 拆开这个 ChatGLM 微调包LoRA DeepSpeed 多卡到底能跑出什么如果你手头只有一两张消费级显卡却想给 ChatGLM 这类几十亿参数的对话模型做领域适配第一反应多半是「显存不够、训练太慢、多卡不会配」。这个项目包就是冲着这三个痛点来的它把 LoRA 低秩微调、DeepSpeed 分布式优化和多 GPU 并行训练揉进一套可运行的源码里目标不是让你从零推导公式而是让你把现成的训练脚本改几个路径就能跑起来。适合已经能跑通单卡推理、想往微调方向迈一步的工程师也适合需要给业务做垂直对话模型、但不想动全量参数的团队。下面我按「资源是什么 → 怎么配 → 怎么跑 → 坑在哪」的顺序把这份源码拆成能照着复现的步骤。2. LoRA 与 DeepSpeed 的联合逻辑为什么不是全量微调2.1 LoRA 在 ChatGLM 上到底改了哪几层ChatGLM 的骨干是 Transformer 结构全量微调意味着反向传播要更新所有参数显存占用和通信量都按参数量线性上涨。LoRA 的做法是在注意力层的线性变换旁挂一对低秩矩阵 A 和 B前向时把BAx加到原输出上反向只更新 A、B原权重冻结。这样可训练参数从几十亿降到几百万到几千万级别优化器状态和梯度显存同步下降。具体到 ChatGLM常见做法是只对query_key_value和dense这两个投影层注入 LoRA因为这两处对语义映射最敏感。项目源码里如果看到target_modules配置项一般就是指定这两个名字。秩r控制低秩矩阵的宽度lora_alpha是缩放系数实际缩放比例是alpha / r。经验上r8或r16对对话任务够用r再大收益递减但显存回升。# LoRA 配置片段常见写法参数名以源码为准 from peft import LoraConfig, get_peft_model lora_config LoraConfig( r8, # 低秩矩阵秩8/16 是对话任务常用值 lora_alpha32, # 缩放系数通常设为 r 的 2~4 倍 target_modules[query_key_value, dense], # ChatGLM 注意力投影层 lora_dropout0.05, # 防过拟合数据少时调到 0.1 biasnone, # 不训练偏置省显存 task_typeCAUSAL_LM ) model get_peft_model(base_model, lora_config) model.print_trainable_parameters() # 确认可训练参数占比这段代码的逻辑是先定义低秩结构再把原模型包成 PeftModel。print_trainable_parameters()会打印可训练参数占总参数的比例正常应该在 0.1% 到 1% 之间。如果这个比例异常高说明target_modules写错或匹配到了全连接层显存会立刻吃紧。lora_dropout在数据量小于一万条时建议保留数据量大可以设 0。2.2 DeepSpeed 的 ZeRO 阶段怎么选DeepSpeed 的核心是 ZeROZero Redundancy Optimizer它把优化器状态、梯度、参数分片到各张卡上减少单卡冗余。阶段 0 不分片阶段 1 只分优化器状态阶段 2 加分片梯度阶段 3 连参数也分片。多卡微调 ChatGLM 时阶段 2 是性价比最高的选择优化器状态和梯度都分片显存降幅明显通信开销又不像阶段 3 那样把参数也切来切去。如果卡数少2 张且单卡显存 24G 以上阶段 2 通常够跑 6B 模型加 LoRA。如果卡多但单卡显存小或者想开更大 batch再考虑阶段 3。项目里的 DeepSpeed 配置文件一般是 JSON关键字段包括zero_optimization.stage、offload_optimizer、fp16或bf16。offload_optimizer把优化器状态挪到 CPU 内存能再省显存但会拖慢速度属于显存实在不够时的后悔药。{ train_batch_size: 16, gradient_accumulation_steps: 4, fp16: { enabled: true, loss_scale: 0 }, zero_optimization: { stage: 2, offload_optimizer: { device: cpu, pin_memory: true }, allgather_partitions: true, overlap_comm: true } }train_batch_size是全局 batch等于单卡 batch 乘以卡数再乘以梯度累积步数。gradient_accumulation_steps设 4 表示每 4 个 micro-batch 才做一次参数更新用来在小显存下模拟大 batch。fp16开启混合精度loss_scale设 0 表示用动态损失缩放。overlap_comm让通信和计算重叠能压一点训练时间。注意offload_optimizer一旦开启CPU 内存占用会明显上升机器内存小于 64G 时慎用。2.3 多 GPU 启动方式与数据并行边界多卡训练有两种常见启动方式torchrun和deepspeed命令行。项目源码如果自带启动脚本一般会用deepspeed --num_gpusN train.py这种形式。--num_gpus指定用几张卡DeepSpeed 会自动做数据并行每张卡拿到不同的数据分片梯度通过 all-reduce 同步。数据并行的边界在于全局 batch 不能小于卡数否则有的卡分不到数据。另外数据加载器要设shuffleTrue并且用分布式采样器否则每张卡可能读到相同数据梯度同步就失去意义。常见做法是在DataLoader里传samplerDistributedSampler(dataset)并在每个 epoch 前调用sampler.set_epoch(epoch)保证每轮打乱顺序不同。# 启动多卡训练以 4 卡为例 deepspeed --num_gpus4 train.py \ --model_name_or_path ./chatglm-6b \ --train_file ./data/train.json \ --output_dir ./output \ --per_device_train_batch_size 2 \ --gradient_accumulation_steps 4 \ --learning_rate 2e-4 \ --num_train_epochs 3 \ --deepspeed ./ds_config.json \ --fp16per_device_train_batch_size是单卡 batch设 2 配合 4 卡和累积 4 步全局 batch 就是 32。learning_rate在 LoRA 微调里通常比全量微调大2e-4 到 5e-4 都常见因为可训练参数少需要更大步长。num_train_epochs设 3 是对话任务的常规起点数据少可以加到 5但要盯着验证集损失过拟合时损失会先降后升。3. 从零配环境到跑通第一个 epoch可抄作业的步骤3.1 依赖版本与 CUDA 匹配这个项目对版本比较敏感PyTorch、CUDA、DeepSpeed 三者必须对齐。常见做法是先确定 CUDA 版本再装对应 PyTorch最后装 DeepSpeed。比如 CUDA 11.8 对应 PyTorch 2.0 以上DeepSpeed 用 0.9 以上。如果版本错位典型报错是undefined symbol或CUDA error: no kernel image is available这类问题排查起来很费时间不如一开始就锁版本。# 以 CUDA 11.8 为例的依赖安装顺序 pip install torch2.0.1 torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 pip install transformers4.33.0 peft0.5.0 datasets2.14.0 pip install deepspeed0.10.0 pip install accelerate0.23.0先装 PyTorch 是因为 DeepSpeed 编译时会链接 torch 的库顺序反了可能编译失败。transformers和peft版本要匹配peft 0.5 对应 transformers 4.33 左右。accelerate用于简化分布式启动虽然 DeepSpeed 可以独立跑但很多训练脚本会 import 它。装完用python -c import torch; print(torch.cuda.is_available())确认 CUDA 可用。3.2 数据格式与预处理脚本ChatGLM 的微调数据一般是 JSON 或 JSONL每条样本包含prompt和response两个字段或者直接是conversations列表。项目源码里如果有data_process.py之类的脚本通常负责把原始数据转成模型输入格式。关键点是构造 attention mask 和 labelsprompt 部分的 label 要设成 -100不计算损失只对 response 部分算 loss。# 数据预处理核心逻辑 def preprocess(example, tokenizer, max_length512): prompt example[prompt] response example[response] # 拼接成完整序列 input_ids tokenizer.encode(prompt response, max_lengthmax_length, truncationTrue) # prompt 部分不计算损失 prompt_len len(tokenizer.encode(prompt)) labels [-100] * prompt_len input_ids[prompt_len:] return {input_ids: input_ids, labels: labels, attention_mask: [1] * len(input_ids)}max_length设 512 是对话任务的常见值长对话可以到 1024 或 2048但显存占用会上升。-100是 PyTorch CrossEntropyLoss 的默认忽略索引设成其他值需要同步改损失函数。attention_mask全 1 表示没有 padding如果用了 padding 要对应设 0。这个预处理函数通常用dataset.map()批量调用注意batchedTrue时逻辑要改成批量处理。3.3 训练脚本关键参数逐项说明训练脚本里参数很多但真正影响结果的就那几个。learning_rate前面说了LoRA 场景下 2e-4 起步。lr_scheduler_type常见用cosine学习率余弦衰减比固定学习率更稳。warmup_ratio设 0.03 到 0.1让学习率从 0 慢慢升上去避免一开始就大步长震荡。weight_decay设 0.01 防过拟合。save_steps和eval_steps根据数据量定一般几百步存一次。# TrainingArguments 关键参数 training_args TrainingArguments( output_dir./output, per_device_train_batch_size2, gradient_accumulation_steps4, learning_rate2e-4, num_train_epochs3, lr_scheduler_typecosine, warmup_ratio0.05, weight_decay0.01, logging_steps10, save_steps200, eval_steps200, fp16True, deepspeed./ds_config.json, report_tonone )logging_steps10表示每 10 步打印一次 loss太频繁会拖慢训练太稀疏又看不清趋势。save_steps200配合save_total_limit3可以只保留最近 3 个 checkpoint省磁盘。report_tonone关掉 wandb 等外部记录本地跑不需要。如果显存够per_device_train_batch_size可以调到 4 或 8同时把gradient_accumulation_steps降下来训练速度会快一些。4. 避坑与排查多卡微调最容易翻车的五个点4.1 显存溢出但 nvidia-smi 显示还有余量现象是训练启动后报CUDA out of memory但nvidia-smi看显存占用并不高。原因是 PyTorch 的缓存分配器会预留显存实际可用比显示值少。解决方法是设PYTORCH_CUDA_ALLOC_CONFmax_split_size_mb:128减少碎片或者把per_device_train_batch_size再降一档同时提高gradient_accumulation_steps保持全局 batch 不变。4.2 多卡训练速度反而比单卡慢现象是 4 卡训练一个 epoch 的时间比单卡还长。原因通常是通信开销盖过了并行收益尤其是zero_optimization.stage设成 3 且没开overlap_comm时。解决方法是降到阶段 2开启overlap_comm和allgather_partitions并确认卡间是 NVLink 或高速 PCIe。如果卡间带宽低数据并行的 all-reduce 会成为瓶颈这时候不如用单卡加梯度累积。4.3 loss 不下降或变成 nan现象是训练几十步后 loss 变成 nan或者一直不降。原因可能是学习率太大、fp16 溢出、数据里有空样本。解决方法是先把learning_rate降到 1e-4 试再把fp16换成bf16如果卡支持bf16 动态范围更大不容易溢出。同时检查数据预处理确认没有全 -100 的 labels那会导致 loss 为 nan。4.4 保存的 LoRA 权重加载后效果不对现象是训练完加载 LoRA 权重推理输出和训练时不一致。原因是保存时只存了 LoRA 参数加载时需要先加载原模型再挂 LoRA顺序反了或target_modules不一致都会出问题。解决方法是加载时用PeftModel.from_pretrained(base_model, lora_path)并确认target_modules和训练时完全相同。如果推理时用了 merge 操作注意 merge 后不能再训练。4.5 数据并行下每个 epoch 数据顺序相同现象是每张卡每个 epoch 读到的数据顺序一样梯度同步效果打折。原因是DistributedSampler没有调set_epoch。解决方法是在每个 epoch 开始时调用train_sampler.set_epoch(epoch)这样每轮的打乱种子不同。如果用的是 HuggingFace Trainer它内部会处理但自定义训练循环要自己加。5. 验证微调效果与 LoRA 权重合并的实操技巧训练跑完不是终点得验证效果。最直接的方法是用固定测试集算困惑度perplexity对比微调前后。困惑度越低说明模型对领域文本的预测越准。另一个方法是人工抽检准备 20 条领域问题看回答是否比原模型更贴切。项目源码里如果有evaluate.py一般会做这两件事。如果没有可以自己写一个简单脚本加载原模型和 LoRA 权重逐条生成回答并保存。# 加载 LoRA 权重做推理验证 from peft import PeftModel from transformers import AutoModelForCausalLM, AutoTokenizer base_model AutoModelForCausalLM.from_pretrained(./chatglm-6b, trust_remote_codeTrue).half().cuda() model PeftModel.from_pretrained(base_model, ./output/checkpoint-600) model model.eval() tokenizer AutoTokenizer.from_pretrained(./chatglm-6b, trust_remote_codeTrue) response, _ model.chat(tokenizer, 你的领域问题, history[]) print(response)checkpoint-600是保存步数对应的目录选哪个 checkpoint 要看验证集损失不一定最后一个最好。half()转 fp16 省显存eval()关掉 dropout。model.chat是 ChatGLM 特有的对话接口返回 response 和更新后的 history。如果输出乱码或重复先检查 tokenizer 是否和训练时一致。LoRA 权重合并是另一个常用操作把低秩矩阵乘回原权重得到一个独立模型推理时不用再挂 PeftModel。合并公式是W W BA * (alpha / r)。合并后模型大小和原模型一样但推理少了一层计算。注意合并只能在推理前做合并后不能再继续训练。合并脚本一般用model.merge_and_unload()然后save_pretrained。# 合并 LoRA 权重并保存独立模型 merged_model model.merge_and_unload() merged_model.save_pretrained(./merged_model) tokenizer.save_pretrained(./merged_model)merge_and_unload()会把 LoRA 矩阵合并进原权重并移除 Peft 包装。保存后的merged_model可以直接用AutoModelForCausalLM.from_pretrained加载不需要 peft 库。合并前建议先备份 LoRA 权重因为合并不可逆。如果合并后效果变差大概率是alpha / r缩放算错检查训练时的lora_alpha和r是否和合并脚本一致。从那以后我每次跑多卡微调都强制先单卡跑 10 步确认 loss 正常再上多卡保存 LoRA 后一定用独立脚本加载验证一遍不直接信训练日志。这套流程帮我省过好几次通宵排查。希望帮到你。本文还有配套的精品资源点击获取
返回列表