ChatGLM LoRA微调实战:从环境搭建到模型训练全流程解析
2026/9/7 8:25:31 网站建设 项目流程

大模型这个词被炒了两三年,真正动手做过微调的人其实没那么多。很多人卡在同一个地方——拿着ChatGLM的权重不知道从哪儿下手,装完PyTorch就不知道该干嘛了。这次我把自己从零开始折腾ChatGLM微调的全过程整理出来,基于PyTorch框架,从环境搭建、模型加载到真正的LoRA微调训练,每一步都写清楚原理和实操方法,给想入门大模型开发的朋友一条能直接照着走的路。

这篇文章适合两类人:一类是有Python基础、想进入大模型方向但找不到切入点的开发者,另一类是已经在跑推理、但对微调训练流程不熟的算法工程师。我会把我踩过的坑、试错的结果、以及最终稳定的方案全部放出来,不做任何保留。

1. 从模型到微调,先弄清楚我们要做什么

1.1 ChatGLM到底是什么样的模型

ChatGLM是国产开源大模型中非常有代表性的一个系列,它是基于Transformer架构的自回归语言模型。所谓自回归,简单说就是根据前面已经生成的token,预测下一个token是什么,一个一个往外蹦,最终形成完整的回答。

以ChatGLM-6B为例,60亿参数量在今天的标准下不算大,但这个规模刚好是一个分水岭:

  • 比它小的模型(比如1B以下),微调后能力天花板明显,复杂指令基本接不住;
  • 比它大的模型(比如70B以上),消费级显卡完全跑不动,量化+分布式训练的门槛又太高;
  • 6B这个档位刚好能用RTX 3090、4090这类24GB显存的卡做全量微调,或者用消费级显卡配合LoRA做高效微调。

所以ChatGLM-6B成了很多人入门大模型微调的第一个选择。到了ChatGLM2和ChatGLM3时代,模型在推理速度、上下文长度、工具调用能力上都做了优化,但核心的Transformer结构和自回归范式没有变,我们在微调时踩的很多坑也依然是通用的。

这里提醒一句:你不需要在动手前把所有Transformer原理都吃透。我的建议是先跑通流程,再回头补原理,效率会高很多。

1.2 微调在解决什么问题

我们拿到的ChatGLM预训练权重,它本身的知识量是够的,但它的“行为方式”是通用的——你问它什么它都能搭两句,却不一定会按照你想要的口吻、格式和逻辑来回答。

微调要解决的核心问题有三个:

  • 领域适配:让模型明白你是做法律咨询、医疗问答还是代码助手,输出的内容要落在特定领域里;
  • 格式对齐:让模型学会你的问答格式,比如必须输出JSON、必须带“总结:”前缀、必须按编号列要点;
  • 行为校正:纠正模型原有的表达习惯,比如太啰嗦、太官方、容易跑题等。

本质上,微调是在给模型“立规矩”,把预训练阶段学到的泛化能力引导到你想要的方向上。

1.3 全量微调与高效微调怎么选

微调分两条路线:全量微调(Full Fine-tuning)参数高效微调(PEFT)

全量微调的意思是把模型所有参数都参与训练。这个方法效果好,但对硬件要求极其苛刻。以ChatGLM-6B为例:

  • 模型权重本身占12GB显存(FP16精度);
  • 梯度占12GB;
  • 优化器状态(AdamW)占24GB左右;
  • 加上激活值、中间变量,总显存需求轻松超过50GB。

也就是说,全量微调至少需要几张A100或者一张80GB的A800才行,个人开发者基本想都别想。

高效微调的代表方法是LoRA(Low-Rank Adaptation,低秩适配)。它的核心思路是:冻结原模型的全部参数,在注意力层(Q、K、V、O)旁边插入低秩分解矩阵,训练时只更新这些小矩阵。

LoRA的好处是:

  • 可训练参数量从60亿降到几百万,显存占用大幅下降;
  • 原模型权重完全不动,保留了通用能力;
  • 训练完保存的适配器文件只有几十到几百MB,部署灵活。

后面我整个实操流程用的就是LoRA方案,这是个人开发者和中小团队最现实的路径。

2. 环境准备与传统安装大坑

2.1 PyTorch安装最容易翻车的地方

很多人第一步就摔在PyTorch安装上。PyTorch的安装看似简单,一个conda命令就能搞定,但实际很容易出问题。

先说最典型的坑:选择了不匹配版本的CUDA和PyTorch。你机器上安装的NVIDIA驱动是有对应的CUDA版本的,但PyTorch不依赖系统的CUDA工具包,它依赖的是CUDA运行时库。所以即使你系统里的CUDA版本是12.4,装了CUDA 11.8配套的PyTorch也没问题——只要显卡驱动够新就行。

我建议的安装方式是:

# 先查看自己显卡的驱动支持的最高CUDA版本 nvidia-smi

看右上角的CUDA Version,比如显示的是12.4,那说明你的驱动足够新。然后到PyTorch官网选择对应的安装命令:

# CUDA 12.x版本 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 # CPU版本(没有NVIDIA显卡时用) pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu

另一个常见问题是conda安装PyTorch时把CUDA、cuDNN等一堆依赖打包装进去,体积大不说,有时还会和已有环境冲突。我个人的经验是:用pip装PyTorch,用conda装Python环境和其余依赖,各管各的,问题最少。

2.2 一套开箱即用的环境配置

完整的环境配置我放在这里,照着走基本不会有问题:

# 1. 创建独立的Python环境 conda create -n chatglm python=3.10 -y conda activate chatglm # 2. 安装PyTorch(以CUDA 12.1为例) pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 # 3. 安装transformers和accelerate pip install transformers accelerate # 4. 安装Peft(LoRA的核心库) pip install peft # 5. 安装其他依赖 pip install datasets tensorboard sentencepiece protobuf

这里解释一下每个库的作用:

  • transformers:Hugging Face生态的核心库,负责加载模型、tokenizer、训练器的对接;
  • accelerate:简化多卡训练和混合精度训练的库,单卡训练也会隐式用到;
  • peft:参数高效微调库,LoRA的实现已经封装好,直接调用;
  • datasets:用来加载和预处理数据集。

装完跑一下验证:

python -c "import torch; print(torch.__version__); print(torch.cuda.is_available())"

输出结果显示torch.cuda.is_available()True,才说明环境OK了。

2.3 为什么我推荐用SFT数据集而不是ChatGPT蒸馏数据

在准备微调数据之前,先明确一个概念:我们用ChatGLM做微调时使用的是**SFT(Supervised Fine-Tuning,监督微调)**方法。数据格式是标准的“指令-回答”对:

{ "instruction": "介绍一下B树", "output": "B树是一种平衡的多路搜索树..." }

很多教程会建议用某个框架去调用大模型的API批量生成数据,这个思路本身没错,生成速度快、数据格式统一,但有个隐藏风险:数据质量上限就是生成数据的模型的天花板。你拿着ChatGPT蒸馏的数据微调出来的模型,能力永远不会超过ChatGPT。

我更推荐的做法是:从真实的用户问题、客服对话记录、专业论坛问答里整理数据。虽然清洗成本高,但数据的真实性和多样性是蒸馏数据替代不了的。如果刚开始手头数据不多,几百条高质量数据微调的LoRA效果,可能好过几万条低质量数据,数据不在多而在精。

我在整理微调数据时发现,ChatGLM类模型对指令格式非常敏感。使用ChatGLM3自带的ChatGLM3Processor时,对话格式要求是mult-turn的结构,每个turn带role标记(system/user/assistant),数据格式不一致会导致tokenize后的input_ids错乱,训练出的模型回答会带有奇怪的重复片段。

3. ChatGLM模型加载与LoRA微调实操

3.1 从Hugging Face加载模型的速度优化

加载ChatGLM权重一般用transformers的AutoModel.from_pretrained。但这有一个很现实的痛点:权重文件几个GB到十几个GB,每次从Hugging Face服务器下载慢得离谱。

我的方案是:优先用modelscope下载权重,再把本地路径传给from_pretrained

from modelscope import snapshot_download # 下载ChatGLM3-6B到本地目录 model_dir = snapshot_download('ZhipuAI/chatglm3-6b') print(model_dir) # 打印出本地路径

国内环境下,ModelScope的下载速度比Hugging Face快很多,下载完的权重结构和Hugging Face格式完全兼容,可以直接用transformers加载。

加载时还有一个很重要的参数:torch_dtype。ChatGLM-6B原始权重是FP32,精度高但显存占用翻倍,实际推理时一般用FP16就够了,显存减半,精度损失几乎可以忽略:

from transformers import AutoModel, AutoTokenizer tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True) model = AutoModel.from_pretrained( model_dir, trust_remote_code=True, torch_dtype=torch.float16, device_map='auto' )

trust_remote_code=True这个参数必须带上,因为ChatGLM的模型结构代码不在transformers内置库中,需要从权重目录里的.py文件加载。

3.2 LoRA微调的完整代码拆解

我们的微调目标:让ChatGLM学会用简洁、口语化的方式回答问题。这里用的是一个很小的示例数据集,结构是标准的“指令-回答”对。

先加载Base模型并冻结参数:

from transformers import AutoModel, AutoTokenizer model = AutoModel.from_pretrained( model_dir, trust_remote_code=True, torch_dtype=torch.float16, device_map='auto' ) # 冻结所有参数 for param in model.parameters(): param.requires_grad = False

注意,device_map='auto'的意思是让transformers自动分配模型到可用的设备上。单卡环境它会全部装进GPU显存,显存不足时会自动做CPU offload——但这里要提醒,LoRA训练时尽量不要用CPU offload,因为CPU和GPU之间反复传数据会让训练速度慢到怀疑人生。

然后配置LoRA:

from peft import LoraConfig, get_peft_model lora_config = LoraConfig( r=8, # 低秩矩阵的秩 lora_alpha=32, # LoRA缩放系数 target_modules=['query_key_value'], # ChatGLM的注意力层模块名 lora_dropout=0.1, # Dropout比例 bias='none', task_type='CAUSAL_LM' ) peft_model = get_peft_model(model, lora_config) peft_model.print_trainable_parameters() # 输出:trainable params: 4,194,304 || all params: 6,258,229,248 || trainable%: 0.067

关键参数逐个说明:

  • r(秩):决定低秩矩阵的维度。r越大,可学习的参数越多,模型表达能力越强,但过拟合风险也越高。r=8是推荐的经验值,简单任务可以降到4,复杂任务可以升到16;
  • lora_alpha:LoRA层的缩放因子,最终的权重变化量需要用lora_alpha/r来缩放。一般设置为r的2倍或4倍效果较好;
  • target_modules:指定在哪层插入LoRA结构。ChatGLM的自注意力层的模块名是query_key_value,你可以在模型配置里用find_all_linear_names()函数自动扫描所有Linear层,但手动指定更精准、显存占用更小。

注意peft_model.print_trainable_parameters()的输出——670万参数在60亿参数总量中占比不到0.1%,这就是LoRA高效的原因。

3.3 训练参数设置与损失曲线解读

训练参数我用的是这样一个配置:

from transformers import TrainingArguments from transformers import Trainer training_args = TrainingArguments( output_dir='./chatglm-lora-checkpoints', per_device_train_batch_size=4, gradient_accumulation_steps=8, learning_rate=2e-4, num_train_epochs=3, logging_steps=10, save_steps=200, fp16=True, remove_unused_columns=False, )

逐个说下关键考量:

  • 批量大小和梯度累积:单卡24GB显存时batch_size=4可能就到极限了,但一个很小的batch会让梯度估计不准确,所以用gradient_accumulation_steps=8做累积。这相当于实际batch size = 4 × 8 = 32,既保证了训练稳定性,又控制了显存占用;
  • 学习率:LoRA的适用学习率比全量微调大很多,因为可训练参数少、优化空间小。2e-4到5e-4都是合理范围;
  • fp16混合精度:必须开启,否则显存不够。注意ChatGLM的某些层在fp16下可能会溢出,如果训练时loss变成NaN,第一个排查点就是fp16的数值稳定性问题。

训练启动之后,重点看loss曲线的下降趋势。正常情况loss会从1.2左右快速下降到0.4以下,如果loss在某个位置剧烈震荡不下降,大概率是学习率太高了。如果loss下降到0.1以下但验证集效果反而变差,那就是过拟合了,需要减小epoch数量或增加数据量。

训练完成后保存模型:

peft_model.save_pretrained('./chatglm-lora-final') tokenizer.save_pretrained('./chatglm-lora-final')

保存下来的文件只有几十MB,这就是我们微调的全部成果。推理时需要把LoRA适配器重新加载到原始模型上。

3.4 合并LoRA权重与推理测试

LoRA推理有两种方式:

方式一:直接加载LoRA进行推理(推荐)

from peft import PeftModel # 先加载原始模型 base_model = AutoModel.from_pretrained( model_dir, trust_remote_code=True, torch_dtype=torch.float16, device_map='auto' ) # 加载LoRA权重 model = PeftModel.from_pretrained(base_model, './chatglm-lora-final') model = model.eval()

方式二:合并LoRA权重到原始模型

merged_model = model.merge_and_unload() merged_model.save_pretrained('./chatglm-lora-merged')

方式二的优势是合并后的模型是完整的、不依赖Peft库的模型文件,部署时不需要额外引入LoRA相关依赖,推理速度会快一点。缺点是合并后的模型文件会变成完整大小(10GB以上)。

推理测试:

questions = [ "什么是人工智能?", "推荐一本关于Python的书", "写一封请假邮件" ] for q in questions: response, history = model.chat(tokenizer, q, history=[]) print(f"问题: {q}") print(f"回答: {response}") print("---")

这里用的model.chat方法是ChatGLM的快捷接口,内部已经封装了对话模板和采样参数。

我在实际测试时发现,微调后的模型回答出现了明显的“风格迁移”——比如我们微调数据里给的是简洁直接的回答,模型会倾向于用短句而不是长篇大论。这就是微调“立规矩”的效果。

4. 不花钱的显存优化方案与推理加速

4.1 显存占用过大时的三个降级策略

很多人没有24GB显存的卡,只有一张12GB甚至8GB的小卡,这确实尴尬,但也不是完全无解。有三种降级方案:

方案一:4-bit量化 + LoRA

ChatGLM4bit的量化加载和前面的方式非常接近,只需要在加载模型时指定bitsandbytes的4-bit配置就行:

from transformers import BitsAndBytesConfig bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_compute_dtype=torch.float16, bnb_4bit_quant_type='nf4' ) model = AutoModel.from_pretrained( model_dir, trust_remote_code=True, quantization_config=bnb_config, device_map='auto' )

量化之后模型显存占用会降到8GB左右,加上LoRA参数和激活值,12GB显卡能跑起来。注意,4-bit量化后LoRA训练时,必须把量化后的模型参数也冻结,只训练LoRA分支里的参数,否则会报梯度回传的错误。

方案二:梯度检查点(Gradient Checkpointing)

model.gradient_checkpointing_enable()

这行代码用计算换显存——不保存前向传播的所有中间激活值,而是在反向传播时重新计算。效果是显存占用降低约50%,代价是训练时间增加约20%-30%。对于显存紧张的人来说,这20%的训练时间其实换不太在意,能顺利跑通更重要。

方案三:CPU Offload

device_map='auto'时设置:

model = AutoModel.from_pretrained( model_dir, trust_remote_code=True, torch_dtype=torch.float16, device_map='auto', offload_folder='offload' )

部分层会被放置到CPU上,模型能跑起来,但在CPU和GPU之间传输数据会在每个step都拖慢速度。这个方法只建议用来做推理测试,不建议做训练。

4.2 推理加速的实战经验

微调完之后,推理速度也是要重点关注的。ChatGLM-6B在没做任何优化时,生成一个token大约需要40-60ms,如果你要生成512个token,大概要30秒左右,体验很一般。

提升推理速度的常用手段有以下几项:

  • 半精度推理:上面已经用了torch.float16,这能比FP32快一倍左右;
  • 批处理:多个用户请求合并成一个batch喂给模型,GPU利用率会大幅提升;
  • torch.compile:PyTorch 2.0引入的编译特性,直接在模型上调用model = torch.compile(model)就能生效,实测推理加速30%-50%;
  • vLLM等推理框架:这个对大模型收益最大,但如果需要动态加载LoRA权重这类操作,vLLM的支持还不算成熟。

另外一个很容易忽略的点:减少输出长度限制。把max_new_tokens从默认的2048改成实际需要的256或512,生成时间会成倍下降。很多人追求长回答,但很多场景下256个token就已经够用了。

5. 微调失效的排查手册与经验之谈

5.1 训练loss不下降的常见排查思路

我在多次微调实验中发现,训练loss不下降是出现频率最高的问题,而没有之一。排查看下面这张表:

现象可能原因解决办法
loss在初始值附近震荡,完全不下降学习率太大或太小调到1e-4到5e-4区间重试
loss下降很快,然后骤停数据量太小,模型学完了增加训练数据量
loss变成NaNfp16精度溢出,或数据中有异常值减小batch size,尝试bf16精度
loss曲线先降后升过拟合降低epoch数或减小r值
训练正常,但回答乱码数据格式和tokenizer不匹配检查模板的special token是否正确

有一个非常隐蔽的坑:ChatGLM的tokenizer在加载时trust_remote_code=True如果没传,tokenizer会退回通用tokenizer,分词结果完全错乱,导致训练loss也在下降但生成的回答是一堆乱码。这个低级错误浪费了我一整天时间,现在先写出来提醒大家。

5.2 微调后效果变差的常见原因

还有一类问题更让人崩溃:微调训练完,loss也下降了,但模型回答质量反而变差了。

这个现象背后的原因往往不是单方面的,而是一系列连锁反应:

  • 数据质量不够。如果训练数据里有很多错误答案,模型学到了错误的映射关系;
  • 数据分布太单一。比如你全用短问短答的数据训练,模型的回答风格会越缩越窄,变得机械;
  • 训练epoch过多,模型发生了灾难性遗忘。LoRA在冻结原始参数的情况下这个问题会轻一些,但依然存在。

我自己的经验是:微调数据覆盖度的重要性,排在数据量前面。宁可要1000条覆盖各种类型的数据,不要10000条全是同一个场景的数据。

预训练模型本身就具有非常强的通用能力,微调的目的是提供风格参考和领域约束。如果训练数据过于单一,模型就被“带偏”了。

5.3 哪里还能继续优化

如果你已经跑通了整个流程,想继续精进,我觉得可以从这几个方向扩展:

  • 多轮对话微调:把单轮问答数据改成多轮对话格式,让模型学会上下文理解和多轮一致性;
  • DPO(Direct Preference Optimization):这是目前对齐人类偏好的主流方案,比RLHF简单很多,同时LoRA可以和DPO结合起来做对齐微调;
  • RAG检索增强:微调解决“风格和行为”,检索解决“知识和实时性”,两者配合效果更好;
  • 用更高质量的数据集迭代:微调模型之后,可以用它生成回答,再用人工或更高级的模型打分,筛选排序后作为下一轮训练数据——这是很多实际项目做数据迭代的通用范式。

6. 写在最后的几点实在话

能读到这里,说明你确实想把大模型微调这回事弄明白,而不是只停留在看热闹的阶段。我自己第一次跑通ChatGLM微调时,前后折腾了好几个日夜——下载权重反复中断,显存OOM报错,模型加载时各种不兼容,训练出来后效果还不如原版模型,几乎每个坑都踩了一遍。

但回头再看,这个过程中收获最大的恰恰是那些踩坑的瞬间。你会慢慢建立起一个很重要的能力:面对一堆看不懂的报错信息,不慌,知道该去看哪里、去改什么。

我的建议是,先别急着追求完美的微调效果,用一个几百条数据的小数据集把整个流程完整跑通一遍,记录每一步的操作和结果。当你有了“从零到推理”的完整经验,再往下走就会顺很多。

我坚信模型微调是一项极具实操性的技能,坐在那里看再多的文章,不如亲手打开终端敲下第一行命令。希望这篇分享能帮你缩短从阅读到完成的距离。

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

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

立即咨询