☰
ChatGLM 大模型微调实战:LoRA 与 DeepSpeed 多 GPU 分布式训练指南
2026/9/28 5:04:24 网站建设 项目流程

简介:这份资源面向希望掌握大模型微调与分布式训练的开发者,提供基于LoRA、DeepSpeed与多GPU环境对ChatGLM进行微调的完整项目源码,适合具备一定深度学习基础、想切入大模型实战的中高级学习者。压缩包共376个文件,约170.03MB,以193个Python脚本为核心,辅以json、yaml、sh等配置与启动文件,以及pickle、pkl、pt等权重与数据缓存,另有jpg、png、pdf、md等说明与文档资料,目录结构便于按模块查阅。项目围绕LoRA低秩分解降低多GPU通信开销、DeepSpeed的ZeRO优化器与混合精度训练展开,源码覆盖模型初始化、预训练权重加载、损失函数定义、优化器与学习率调度、数据准备处理及评估验证等环节,可帮助读者理解ChatGLM对话生成微调的完整链路。目前已有795人学习,适合作为大模型微调与分布式训练的实操参考。

1. ChatGLM 微调这件事,卡住人的从来不是模型本身

ChatGLM 系列放到今天已经不算新面孔,但它在中文对话场景里的性价比依然能打。真正让一线工程师头疼的,是「大模型 finetune」这条链路里那些琐碎又致命的环节:LoRA 的秩设多少、DeepSpeed 的 ZeRO 阶段怎么选、多 GPU 之间梯度怎么同步、显存什么时候会突然炸掉。你搜「大模型微调实战」,出来的教程要么只贴一段Trainer调用,要么把 DeepSpeed 配置当成黑匣子直接甩给你,跑不通就卡在那里。

这篇要讲的是:用 LoRA 做参数高效微调,用 DeepSpeed 做多 GPU 分布式训练,把 ChatGLM 在一个自定义指令数据集上跑通。适合手里有 2 到 8 张卡、想把通用对话模型往垂直领域掰一掰的从业者。读完你应该能自己写出配置文件、启动训练、看懂 loss 曲线、在翻车时知道先查哪里。不涉及推理部署和量化,那是另一条线的事。

2. LoRA 与 DeepSpeed 的分工:谁省显存,谁管并行

2.1 LoRA 到底改了什么,为什么它能把显存压下来

全量微调 ChatGLM-6B,光是优化器状态加梯度就要吃掉数倍于模型本身的显存。LoRA 的思路是不动原始权重,在注意力层的线性变换旁边挂一对低秩矩阵 A 和 B,前向时把BA加到原输出上。原始权重冻结,只有 A 和 B 参与梯度更新。

这意味着三件事:第一,可训练参数量从几十亿降到几百万到几千万量级;第二,优化器状态只针对这部分参数,显存占用断崖式下降;第三,训练完可以把BA合并回原权重,推理时不增加任何延迟。

关键参数是秩r和缩放系数lora_alpha。r决定低秩矩阵的秩,常见取值 4、8、16、32。lora_alpha一般设成r的两倍或固定 16/32。还有一个target_modules,决定往哪些层挂 LoRA。ChatGLM 里通常是query_key_value这个融合了 QKV 的线性层,有些实现也会加上dense和dense_h_to_4h。

from peft import LoraConfig, TaskType, get_peft_model lora_config = LoraConfig( task_type=TaskType.CAUSAL_LM, r=8, # 秩,越大容量越强但显存和过拟合风险上升 lora_alpha=32, # 缩放系数,通常为 r 的 2~4 倍 lora_dropout=0.1, # 防过拟合,小数据集建议 0.05~0.1 target_modules=["query_key_value"], # ChatGLM 的 QKV 融合层 bias="none", # 不训练 bias,省显存 ) model = get_peft_model(base_model, lora_config) model.print_trainable_parameters()

print_trainable_parameters()会打印可训练参数占比,正常应该在 0.1% 到 1% 之间。如果这个数字大得离谱,八成是target_modules写错了,把整个模型都挂上了。

2.2 DeepSpeed ZeRO 三个阶段,选错了就是白跑

DeepSpeed 解决的是多卡场景下显存和通信的平衡问题。ZeRO 把训练状态切分到各张卡上,阶段越高切得越狠,但通信量也越大。

ZeRO 阶段切分对象显存节省通信开销适用场景
Stage 1优化器状态约 4 倍低显存略紧,卡间带宽一般
Stage 2优化器状态 + 梯度约 8 倍中最常用,6B~13B 模型首选
Stage 3优化器状态 + 梯度 + 参数线性于卡数高大模型或单卡显存极小

对 ChatGLM-6B 配 LoRA,Stage 2 基本够用。Stage 3 虽然省显存,但参数切分后每次前向都要 all-gather,卡间通信不好的机器上反而拖慢训练。我一般先用 Stage 2 跑,OOM 了再升 Stage 3。

配置文件里还有几个必须盯的参数:train_batch_size是全局批次,train_micro_batch_size_per_gpu是单卡批次,两者比值就是梯度累积步数。gradient_clipping设 1.0 防止梯度爆炸。fp16或bf16根据显卡架构选,A100/H100 用 bf16,V100/2080Ti 用 fp16。

{ "train_batch_size": 64, "train_micro_batch_size_per_gpu": 4, "gradient_accumulation_steps": 4, "gradient_clipping": 1.0, "bf16": { "enabled": true }, "zero_optimization": { "stage": 2, "offload_optimizer": { "device": "none" }, "allgather_partitions": true, "overlap_comm": true, "contiguous_gradients": true }, "steps_per_print": 50 }

overlap_comm让通信和计算重叠,contiguous_gradients减少显存碎片,这两个开着基本没坏处。offload_optimizer如果设成cpu可以进一步省显存,但速度会明显下降,除非真的卡不够否则不建议。

2.3 多 GPU 启动命令与数据并行策略

DeepSpeed 用torchrun或deepspeed启动。假设你有 4 张卡,脚本叫train.py,配置叫ds_config.json:

torchrun --nproc_per_node=4 --master_port=29500 train.py \ --model_name_or_path /path/to/chatglm-6b \ --train_file data/train.json \ --val_file data/val.json \ --output_dir output/chatglm-lora \ --num_train_epochs 3 \ --learning_rate 2e-4 \ --per_device_train_batch_size 4 \ --gradient_accumulation_steps 4 \ --deepspeed ds_config.json

nproc_per_node必须等于你实际使用的 GPU 数。master_port随便选一个没被占用的端口,多机训练时所有节点要一致。数据并行下每张卡拿到不同的数据分片,梯度通过 all-reduce 同步,DeepSpeed 会自动处理。

学习率对 LoRA 来说通常比全量微调大,1e-4 到 5e-4 都有人用。我一般从 2e-4 起步,配 cosine 调度和 warmup。如果 loss 前期震荡厉害,先降学习率而不是改 batch size。

3. 从数据到权重:ChatGLM 微调的可复现流程

3.1 指令数据的格式与预处理

ChatGLM 的微调数据一般组织成多轮对话格式。每条样本是一个列表,里面交替出现role和content。常见做法是构造成prompt+response的配对,或者直接拼成 ChatGLM 自己的对话模板。

import json def build_sample(instruction, input_text, output_text): # 按 ChatGLM 对话格式拼接,注意保留分隔符 prompt = f"问:{instruction}\n{input_text}\n答:" return {"prompt": prompt, "response": output_text} raw = [ {"instruction": "判断情感", "input": "这家店服务太差了", "output": "负面"}, {"instruction": "提取实体", "input": "张三在北京工作", "output": "人名:张三;地点:北京"}, ] with open("data/train.json", "w", encoding="utf-8") as f: for item in raw: f.write(json.dumps(build_sample(**item), ensure_ascii=False) + "\n")

数据量方面,垂直领域任务 500 到 5000 条高质量样本就能看到明显效果。关键是质量而不是数量,标注不一致的数据会让模型学出矛盾行为。验证集留 5% 到 10%,用来观察是否过拟合。

预处理时要把prompt部分做 tokenize 并设置labels为 -100,只对response部分计算 loss。这一步很多教程会漏掉,导致模型把问题也当成要学的内容,推理时容易复读用户输入。

3.2 训练脚本的核心结构与关键参数

训练脚本用 HuggingFaceTrainer加 DeepSpeed 集成是最省事的路径。核心是继承DataCollator处理变长序列,以及正确设置TrainingArguments。

from transformers import Trainer, TrainingArguments, DataCollatorForSeq2Seq training_args = TrainingArguments( output_dir="output/chatglm-lora", per_device_train_batch_size=4, gradient_accumulation_steps=4, learning_rate=2e-4, num_train_epochs=3, lr_scheduler_type="cosine", warmup_ratio=0.03, logging_steps=20, save_strategy="epoch", evaluation_strategy="epoch", fp16=False, bf16=True, deepspeed="ds_config.json", report_to="none", ) trainer = Trainer( model=model, args=training_args, train_dataset=train_dataset, eval_dataset=val_dataset, data_collator=DataCollatorForSeq2Seq(tokenizer, padding=True), ) trainer.train()

warmup_ratio设 0.03 到 0.1,让学习率从 0 线性升上去,避免一开始就大步更新破坏预训练权重。save_strategy和evaluation_strategy设成epoch适合小数据集,数据量大时改成steps并指定save_steps。

bf16和fp16不能同时开。判断标准很简单:显卡是 Ampere 架构及以上(A100、A10、3090、4090)用 bf16,否则用 fp16。fp16 需要配fp16_opt_level和 loss scaling,DeepSpeed 会自动处理,但偶尔会遇到 NaN,这时候换 bf16 或者加gradient_clipping能救回来。

3.3 训练过程中的监控指标与中断恢复

启动后先看显存占用。nvidia-smi里每张卡的显存应该接近但不爆满。如果第一张卡明显比其他的高,说明模型加载没做均衡,检查device_map是否设成了auto。

loss 曲线看两个东西:训练 loss 是否稳定下降,验证 loss 是否在某个点后开始上升。后者是过拟合信号,提前停或者减 epoch。正常情况训练 loss 从 2.0 左右降到 0.5 以下,验证 loss 跟降但略高。

DeepSpeed 支持断点续训。在TrainingArguments里设resume_from_checkpoint=True,重新启动时会自动找最新的 checkpoint。checkpoint 目录里存了模型权重、优化器状态和调度器状态,所以文件会比较大,定期清理旧的能省不少磁盘。

# 查看 checkpoint 占用 du -sh output/chatglm-lora/checkpoint-* # 只保留最近两个 ls -dt output/chatglm-lora/checkpoint-* | tail -n +3 | xargs rm -rf

训练完成后 LoRA 权重是单独保存的,需要和基础模型合并才能用于推理。PEFT 提供了merge_and_unload(),合并后就是一个完整的 ChatGLM 模型。

4. 多卡训练避坑:那些让训练白跑的细节

4.1 现象:启动就 OOM,但单卡明明能跑

原因通常有三个。一是per_device_train_batch_size设太大,多卡下每张卡都要放一份模型和激活值,不会因为卡多就自动变小。二是 DeepSpeed 配置里train_batch_size和实际不匹配,导致梯度累积计算错误。三是模型加载时没有用device_map,所有卡都试图加载完整模型。

解决:先把per_device_train_batch_size降到 1,确认能启动后再往上加。检查train_batch_size = per_device_train_batch_size × GPU数 × gradient_accumulation_steps这个等式是否成立。加载模型时用AutoModel.from_pretrained(..., device_map="auto")或者交给 DeepSpeed 的deepspeed.initialize处理。

4.2 现象:loss 一直是 nan 或者不下降

NaN 多半是 fp16 溢出。V100 上跑 fp16 遇到大梯度容易出问题,换成 bf16 或者把gradient_clipping从 1.0 降到 0.5。如果 loss 不降,先查学习率是不是太小,LoRA 场景下 1e-5 基本学不动,至少 1e-4 起步。再查数据里labels是不是全被设成了 -100,那样等于没监督信号。

还有一个隐蔽原因:ChatGLM 的 tokenizer 在拼接 prompt 和 response 时没有加正确的分隔符,导致模型分不清哪里是输入哪里是输出。打印几条 tokenize 后的结果人工看一眼,比调参管用。

4.3 现象:多卡速度比单卡还慢

卡间通信是瓶颈。如果机器上 GPU 之间走的是 PCIe 而不是 NVLink,ZeRO Stage 3 的 all-gather 会非常慢。降到 Stage 2,或者关掉overlap_comm试试。另外nproc_per_node设成了超过实际卡数,进程会排队等 GPU,看起来像卡死。

用nvidia-smi topo -m看卡间互联类型。如果是PHB或SYS说明走 PCIe,通信带宽有限,这时候优先用 Stage 1 或 2,别上 Stage 3。NCCL_DEBUG=INFO环境变量能看到通信初始化过程,排查卡间连接问题很有用。

4.4 现象:验证 loss 比训练 loss 低很多

这听起来是好事,但往往是数据泄露。检查训练集和验证集有没有重叠样本,或者验证集的 prompt 在训练集里出现过。另一种可能是验证集太小,几个样本的 loss 波动大,看起来低而已。验证集至少 100 条以上才有参考意义。

还有一种情况是evaluation_strategy设成了steps但eval_steps没设,导致验证频率异常。统一设成epoch最省心,小数据集一个 epoch 验证一次足够。

4.5 现象:合并后的模型推理效果和训练时不一样

LoRA 权重合并时如果lora_alpha和r的比例没对上,缩放系数会出错。PEFT 的merge_and_unload()会自动处理,但如果你手动合并权重,记得缩放因子是lora_alpha / r。另外推理时的max_length、temperature、top_p和训练时的生成配置不一致,也会导致输出风格突变。

合并后先用训练集里的几条样本测一下,输出应该和训练时的验证输出接近。如果差很远,检查 tokenizer 是不是同一个,ChatGLM 不同版本的 tokenizer 有差异。

5. 把 LoRA 权重用起来:合并、导出与效果验证

训练跑完只是第一步,真正要验证的是微调后的模型在目标任务上有没有提升。我一般会做三件事:合并权重、跑一批测试样本、和原始模型做对比。

合并权重的代码很短,但顺序不能错:

from peft import PeftModel from transformers import AutoModel, AutoTokenizer base = AutoModel.from_pretrained("/path/to/chatglm-6b", trust_remote_code=True) tokenizer = AutoTokenizer.from_pretrained("/path/to/chatglm-6b", trust_remote_code=True) # 加载 LoRA 权重并合并 model = PeftModel.from_pretrained(base, "output/chatglm-lora/checkpoint-xxx") merged = model.merge_and_unload() merged.save_pretrained("output/chatglm-merged", safe_serialization=True) tokenizer.save_pretrained("output/chatglm-merged")

safe_serialization=True保存成 safetensors 格式,加载更快也更安全。合并后的模型大小和原始 ChatGLM 一致,LoRA 的增量已经融进去了。

效果验证别只看 loss。准备 20 到 50 条目标任务样本,分别用原始模型和微调后模型生成,人工对比。重点看三个维度:格式遵循(比如要求输出 JSON 是否真的输出 JSON)、领域术语准确性、以及有没有灾难性遗忘(通用对话能力是否明显下降)。

如果发现通用能力掉得厉害,说明 LoRA 秩太大或者训练数据太单一。降r到 4,或者在训练数据里混入 10% 到 20% 的通用对话样本。这个比例我试过,既能保持领域效果,又不会把通用能力冲垮。

还有一个实用技巧:用r=8训一版,再用r=16训一版,对比验证集 loss 和人工评测。多数垂直任务上r=8和r=16差距很小,但r=8的显存和过拟合风险更低。别一上来就冲r=64,那是给超大数据集准备的。

最后说个我踩过的坑:合并后的模型用chat()方法推理时,history参数如果传了训练时没见过的格式,输出会乱。统一用空列表[]初始化,多轮对话时按标准格式追加。这个细节不值得单独开一章,但每次都能坑到人。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询