☰
24G显卡LoRA微调ChatGLM3-6B实战:从环境搭建到推理部署
2026/10/6 14:40:35 网站建设 项目流程

简介:本资源面向希望掌握大模型高效微调技术的研究者与开发者,提供基于ChatGLM3-6B模型的LoRA方法完整实战项目。LoRA通过低秩结构逼近参数矩阵更新,在不显著增加参数量的前提下完成微调,对计算资源要求较低,适合资源受限场景下将大模型适配到特定任务与领域。资源包共12个文件,约359KB,包含4个Python脚本、5个JSON数据文件、1个YAML配置、1个Markdown说明及1个my文件,覆盖数据准备、微调训练、模型导出与推理等环节,目录结构清晰,便于按模块查阅。已有781人学习下载,说明该实战案例具备一定参考价值。读者可借助完整源码与流程教程,从数据集构建到微调后模型性能评估逐步实践,理解LoRA微调的关键实现细节,并在此基础上迁移到自身任务中,提升自然语言处理项目的落地效率。

1. 从一张 24G 显卡说起:ChatGLM3-6B 的 LoRA 微调到底能跑出什么

手里只有一张 24G 显存的卡,想拿 ChatGLM3-6B 做领域适配,全量微调基本是奢望——6B 参数按 FP16 算,光权重就 12G 起步,加上优化器状态和梯度,分分钟 OOM。LoRA 就是在这种约束下最务实的选择:冻结原模型权重,只在注意力层的低秩旁路里训练一小撮参数,显存占用能压到全量微调的零头。这份资源包给的就是一条完整链路——从环境依赖、数据格式、训练脚本到推理验证,源码和流程教程都在里面,适合手上有业务语料、想快速验证「微调到底有没有用」的工程师。它不解决预训练,也不解决多模态,就是把 ChatGLM3-6B 这个基座,用 LoRA 的方式往你的垂直场景上拽一把。下面按我实际拆包复现的顺序讲,参数怎么设、坑在哪,都落到具体命令上。

2. 拆开资源包先看什么:目录结构、依赖版本与 ChatGLM3 的 LoRA 挂载点

拿到一个微调项目包,我第一件事不是急着跑train.sh,而是先把目录结构和依赖版本摸清楚。LoRA 微调翻车,十有八九栽在版本不匹配或者挂载层选错上,这两件事在动手前就能排掉。

2.1 目录结构与关键文件定位

这类 ChatGLM3-6B LoRA 项目,解压后通常长这样(不同打包方式略有出入,但核心文件跑不掉):

ChatGLM3-LoRA/ ├── ptuning/ # 官方 P-Tuning v2 目录,LoRA 脚本常放这里 │ ├── train.sh # 训练入口脚本 │ ├── arguments.py # 训练参数定义 │ ├── main.py # 训练主逻辑 │ └── trainer.py # 自定义 Trainer ├── finetune_demo/ # 部分包用这个目录名 │ ├── finetune.py │ └── inference.py ├── data/ │ └── dataset_example.json # 数据格式样例 ├── requirements.txt └── README.md

先确认三件事:训练入口脚本是哪个、数据样例长什么样、requirements.txt里锁的transformers和peft版本。ChatGLM3 对transformers版本比较挑,4.36 到 4.40 之间相对稳,太新或太旧都可能在加载 tokenizer 或trust_remote_code时炸掉。peft建议 0.6 以上,LoRA 的target_modules匹配逻辑在旧版本里对 ChatGLM 的层名支持不全。

2.2 依赖安装与版本对齐

我一般用 conda 起一个干净环境,避免和系统里的 torch 打架:

conda create -n chatglm3-lora python=3.10 -y conda activate chatglm3-lora # 先装 torch,按你的 CUDA 版本选,这里以 cu121 为例 pip install torch==2.1.2 torchvision==0.16.2 --index-url https://download.pytorch.org/whl/cu121 # 再装项目依赖 pip install transformers==4.40.2 peft==0.10.0 accelerate==0.29.3 \ datasets==2.18.0 sentencepiece==0.2.0 protobuf==4.25.3 \ cpm_kernels==1.0.11 tensorboard

参数说明:transformers==4.40.2是实测和 ChatGLM3 兼容较好的版本;peft==0.10.0支持target_modules用列表精确指定;accelerate负责混合精度和梯度累积的调度;cpm_kernels是 ChatGLM 系列的自定义算子依赖,漏装会在前向时报找不到 kernel。装完用python -c "import torch; print(torch.cuda.is_available())"确认显卡能被识别,返回True再往下走。

2.3 LoRA 到底挂在 ChatGLM3 的哪几层

这是选型里最容易被忽略的一点。ChatGLM3-6B 的注意力层命名和 LLaMA 不一样,它的query_key_value是一个合并的线性层,输出维度是hidden_size * 3,把 Q、K、V 拼在一起。所以 LoRA 的target_modules不能照抄 LLaMA 的["q_proj", "v_proj"],常见做法是挂在这几个层上:

# ChatGLM3 的 LoRA 目标层配置 target_modules = ["query_key_value", "dense", "dense_h_to_4h", "dense_4h_to_h"]

逻辑说明:query_key_value对应注意力投影,dense是注意力输出投影,dense_h_to_4h和dense_4h_to_h是 FFN 的上下投影。只挂query_key_value是最省参数的方案,但表达力有限;四个都挂,可训练参数会多一些,效果通常更稳。r(秩)一般取 8 或 16,lora_alpha取2 * r,lora_dropout取 0.05 到 0.1。这几个值不是玄学,r越大容量越大但越容易过拟合小数据集,alpha/r的比值控制旁路输出的缩放强度,比值固定时换r不用重调学习率。

提示:如果训练脚本里target_modules写的是 LLaMA 的层名,加载时不会报错,但 LoRA 层会挂空,训练 loss 几乎不动——这是最隐蔽的坑之一,务必打印一下可训练参数数量确认。

3. 数据准备与训练脚本:从 JSON 格式到 train.sh 参数逐行拆解

数据格式和训练参数是决定微调成败的两根柱子。格式错了训练直接报错,参数错了 loss 曲线会给你脸色看。这一章把数据构造和train.sh的每个参数都摊开讲。

3.1 构造符合 ChatGLM3 对话模板的 JSON 数据

ChatGLM3 用的是自己的对话格式,训练数据必须按它的prompt/response结构组织,否则模型学到的对话边界是乱的。样例数据长这样:

[ { "prompt": "请判断以下工单的紧急程度:服务器无法登录,影响全部用户。", "response": "紧急程度:高。理由:影响范围覆盖全部用户,属于核心服务不可用。" }, { "prompt": "请判断以下工单的紧急程度:某个报表导出格式错位。", "response": "紧急程度:低。理由:不影响核心功能,属于展示层问题。" } ]

逻辑说明:prompt是输入指令,response是期望输出。ChatGLM3 的build_prompt会在训练时自动套上<|system|>、<|user|>、<|assistant|>这些特殊 token,你不需要手动拼。数据量上,LoRA 微调对样本数的要求比全量低,垂直分类或抽取任务,几百到几千条高质量样本就能看到明显变化,但样本质量比数量重要——同一个 prompt 出现矛盾标注,模型会学出模棱两可的输出。

参数上要注意max_length和max_source_length。ChatGLM3 的上下文是 8K,但训练时没必要拉满,按你数据里prompt + response的 95 分位长度设就行,设太大浪费显存,设太小会截断长样本。常见做法是先跑一遍统计:

import json with open("data/train.json", "r", encoding="utf-8") as f: data = json.load(f) lengths = [len(d["prompt"]) + len(d["response"]) for d in data] lengths.sort() print("样本数:", len(lengths)) print("95分位长度:", lengths[int(len(lengths) * 0.95)]) print("最大长度:", lengths[-1])

按 95 分位往上取整到 64 的倍数,作为max_length的初值。这样既不浪费显存,也不会把大部分样本截掉。

3.2 train.sh 参数逐行拆解

训练入口一般是个 shell 脚本,核心参数如下:

# train.sh 关键参数 PRE_SEQ_LEN=128 LR=2e-4 NUM_GPUS=1 MAX_SOURCE_LEN=512 MAX_TARGET_LEN=512 DEV_BATCH_SIZE=4 GRAD_ACCUM=8 NUM_EPOCHS=3 SAVE_STEPS=200 LORA_R=16 LORA_ALPHA=32 LORA_DROPOUT=0.05 torchrun --standalone --nnodes=1 --nproc_per_node=$NUM_GPUS main.py \ --do_train \ --train_file data/train.json \ --validation_file data/dev.json \ --prompt_column prompt \ --response_column response \ --model_name_or_path THUDM/chatglm3-6b \ --output_dir output/chatglm3-lora \ --overwrite_output_dir \ --max_source_length $MAX_SOURCE_LEN \ --max_target_length $MAX_TARGET_LEN \ --per_device_train_batch_size $DEV_BATCH_SIZE \ --per_device_eval_batch_size $DEV_BATCH_SIZE \ --gradient_accumulation_steps $GRAD_ACCUM \ --learning_rate $LR \ --num_train_epochs $NUM_EPOCHS \ --logging_steps 10 \ --save_steps $SAVE_STEPS \ --learning_rate $LR \ --lora_r $LORA_R \ --lora_alpha $LORA_ALPHA \ --lora_dropout $LORA_DROPOUT \ --fp16

参数说明:DEV_BATCH_SIZE=4配合GRAD_ACCUM=8,等效 batch size 是 32,24G 卡上跑 6B 的 LoRA 这个组合比较稳;LR=2e-4是 LoRA 的常用学习率,比全量微调高一个量级,因为可训练参数少;NUM_EPOCHS=3对几千条数据够用,再多容易过拟合;SAVE_STEPS=200是 checkpoint 间隔,方便你中途挑效果最好的那个;fp16开混合精度省显存,如果卡支持 bf16,换成--bf16数值更稳。PRE_SEQ_LEN是 P-Tuning 的遗留参数,纯 LoRA 用不到,但脚本里常留着,不影响。

3.3 启动训练与显存监控

启动前先开一个窗口盯显存:

watch -n 2 nvidia-smi

然后另开窗口跑训练。正常启动后你会看到 loss 从 2 点几逐步下降,前几十步波动大是正常的。如果显存直接爆,按这个顺序降:先把DEV_BATCH_SIZE降到 2,再把MAX_SOURCE_LEN和MAX_TARGET_LEN降到 384,还不行就开gradient_checkpointing。如果 loss 一直不降,先回去检查target_modules是不是挂空了,再检查数据里prompt和response的字段名和脚本参数是否对得上。

注意:训练日志里会打印可训练参数占比,LoRA 正常在 0.1% 到 1% 之间。如果看到接近 100%,说明 LoRA 没生效,模型在全量训练,显存和过拟合风险都会飙升。

4. 推理验证与效果评估:合并权重还是动态加载,怎么判断微调真的有用

训练完拿到output/chatglm3-lora目录,里面是 adapter 权重,不是完整模型。怎么用它、怎么判断效果,这一步比训练本身更容易被糊弄过去。

4.1 两种加载方式:动态挂载与权重合并

第一种是动态加载,基座模型 + LoRA adapter 分开,推理时挂上去:

from transformers import AutoModel, AutoTokenizer from peft import PeftModel base_model = AutoModel.from_pretrained( "THUDM/chatglm3-6b", trust_remote_code=True, device_map="auto" ) tokenizer = AutoTokenizer.from_pretrained( "THUDM/chatglm3-6b", trust_remote_code=True ) # 挂载 LoRA adapter model = PeftModel.from_pretrained(base_model, "output/chatglm3-lora") model = model.eval() response, history = model.chat( tokenizer, "请判断以下工单的紧急程度:数据库主从延迟超过 30 分钟。", history=[] ) print(response)

逻辑说明:PeftModel.from_pretrained会把 adapter 权重挂到基座的对应层上,推理时旁路参与计算。这种方式灵活,一个基座可以挂多个 adapter 切换,但每次推理多一层计算开销。

第二种是合并权重,把 LoRA 旁路合并进基座,导出一个完整模型:

from peft import PeftModel model = PeftModel.from_pretrained(base_model, "output/chatglm3-lora") merged_model = model.merge_and_unload() merged_model.save_pretrained("output/chatglm3-merged", safe_serialization=True) tokenizer.save_pretrained("output/chatglm3-merged")

参数说明:merge_and_unload把 LoRA 的B @ A * scaling加到原权重上,然后卸载旁路,导出的是标准模型,推理时没有额外开销,适合部署。safe_serialization=True导出safetensors格式,比 pickle 安全,加载也快。合并后的模型体积和原基座一样,不会因为 LoRA 变大。

4.2 效果评估:别只看 loss,要看任务指标

loss 降了不代表任务效果好了。LoRA 微调最常见的翻车是 loss 很漂亮,但模型在验证集上开始胡说。评估要分两层:

第一层是自动指标。分类任务看准确率和 F1,抽取任务看字段级匹配率。写一个批量推理脚本,把验证集跑一遍:

import json from tqdm import tqdm with open("data/dev.json", "r", encoding="utf-8") as f: dev_data = json.load(f) correct = 0 for item in tqdm(dev_data): response, _ = model.chat(tokenizer, item["prompt"], history=[]) # 这里按你的任务定义匹配逻辑,示例为完全匹配 if response.strip() == item["response"].strip(): correct += 1 print(f"完全匹配率: {correct / len(dev_data):.4f}")

第二层是人工抽检。自动指标只能告诉你「像不像」,不能告诉你「对不对」。从验证集里随机抽 30 到 50 条,逐条看输出,重点看边界样本和训练集里没出现过的表达。我一般会准备一个「对抗集」——故意写一些和训练集措辞不同但语义相同的输入,看模型是学到了任务逻辑还是只记住了训练样本的表面模式。

4.3 基座对比:微调前后的差异要能说清楚

判断微调有没有用,最直接的办法是拿同一个 prompt 分别问基座和微调后的模型,对比输出。如果两者输出几乎一样,说明 LoRA 没学到东西,回去查target_modules和学习率;如果微调后在你关心的任务上明显更贴合,但在通用问题上变差,说明过拟合了,减少 epoch 或增加 dropout。这个对比要固定随机种子,不然生成结果的随机性会干扰判断。

提示:ChatGLM3 的model.chat默认带采样,对比时把do_sample=False打开,输出才可复现。

5. 避坑与排查:LoRA 微调 ChatGLM3 最常见的五类翻车

这一章是我自己踩过和帮别人排过的坑,按「现象 → 原因 → 解决」写,遇到问题可以对着查。

5.1 训练 loss 不降或降得极慢

现象:跑了几百步,loss 在 2.5 附近晃,几乎不动。原因:target_modules写成了 LLaMA 的层名,LoRA 层挂空,实际没训练任何参数;或者学习率设成了全量微调的量级(1e-5),对 LoRA 来说太小。解决:打印model.print_trainable_parameters(),确认可训练参数占比在 0.1% 到 1% 之间;学习率调到 1e-4 到 3e-4 之间。

5.2 显存 OOM,batch size 已经降到 1

现象:DEV_BATCH_SIZE=1还是爆显存。原因:MAX_SOURCE_LEN和MAX_TARGET_LEN设太大,或者没开梯度检查点,中间激活值占满了。解决:把两个长度降到 384 甚至 256,加上--gradient_checkpointing,用时间换显存;确认fp16或bf16已开。

5.3 推理时输出乱码或特殊 token 泄漏

现象:推理结果里出现<|user|>、<|assistant|>这些 token,或者输出一段无意义重复。原因:tokenizer 加载时没带trust_remote_code=True,或者训练和推理用的 tokenizer 版本不一致。解决:训练和推理统一用同一个model_name_or_path加载 tokenizer,确认trust_remote_code=True;检查build_prompt的逻辑在推理时是否和训练时一致。

5.4 合并权重后模型效果变差

现象:动态加载 adapter 时输出正常,merge_and_unload之后效果明显下降。原因:合并时精度损失,或者合并前模型没切到 eval 模式,dropout 还在生效。解决:合并前调model.eval();合并后用同一批 prompt 对比动态加载和合并后的输出,确认一致再导出;如果差异大,检查peft版本,旧版本合并逻辑有 bug。

5.5 微调后通用能力断崖式下降

现象:目标任务变好了,但问它别的常识问题开始胡言乱语。原因:学习率太大、epoch 太多,LoRA 旁路把基座带偏了,典型的灾难性遗忘。解决:降低学习率到 1e-4,减少 epoch 到 1 到 2,提高lora_dropout到 0.1;在训练数据里混入 10% 到 20% 的通用指令数据,让模型别忘本。

6. 进阶技巧:用验证集早停和 adapter 热插拔把微调成本压到最低

训练不是跑完固定 epoch 就完事,怎么在有限算力下拿到最好的 adapter,有几个实操技巧。

第一个是早停。别等NUM_EPOCHS跑完,每SAVE_STEPS存一个 checkpoint,用验证集批量评估,取指标最高的那个。我一般会在训练脚本外挂一个评估循环,每存一次 checkpoint 就跑一遍验证集,记录指标。这样即使后面过拟合了,你手里也有最好的那个版本。评估脚本的核心就是上面 4.2 那段批量推理,把它包成一个函数,按 checkpoint 路径循环调用即可。

第二个是 adapter 热插拔。一个基座模型可以挂多个 LoRA adapter,按业务场景切换。比如你有一个工单分类的 adapter 和一个工单摘要的 adapter,不用加载两个 6B 模型,只加载一个基座,推理时切换 adapter:

from peft import PeftModel base_model = AutoModel.from_pretrained("THUDM/chatglm3-6b", trust_remote_code=True, device_map="auto") # 加载第一个 adapter model = PeftModel.from_pretrained(base_model, "output/lora-cls", adapter_name="cls") # 加载第二个 adapter model.load_adapter("output/lora-sum", adapter_name="sum") # 切换使用 model.set_adapter("cls") resp_cls, _ = model.chat(tokenizer, "判断工单紧急程度:...", history=[]) model.set_adapter("sum") resp_sum, _ = model.chat(tokenizer, "总结以下工单:...", history=[])

参数说明:adapter_name是自定义标识,load_adapter可以叠加多个,set_adapter切换当前生效的那个。这种方式显存里只有一份基座权重,多个 adapter 加起来通常几百 MB,比加载多个完整模型省得多。适合企业私有化部署场景,一个基座服务多个垂直任务。

第三个技巧是数据配比。如果你的业务数据很少(几百条),单独训容易过拟合,常见做法是混入通用指令数据,比例控制在 5:1 到 10:1 之间,让模型在学任务的同时保持通用能力。混入的数据不用太讲究,一些公开的中文指令集抽一部分就行,关键是别让业务数据被淹没。

最后说个验证习惯:每次训完 adapter,我都会固定一组「回归 prompt」——包含任务样本、边界样本和几个通用问题,跑一遍存成文本,和上一版对比。这样能快速发现这次微调是进步了还是退步了,比只看 loss 靠谱得多。从那以后我每次动 LoRA 参数前,都强制先把这组回归 prompt 跑一遍留底,省得改完忘了原来什么样。希望帮到你。

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

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

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

立即咨询