ChatGLM3-6B本地部署、LoRA微调与模型合并全流程

发布时间:2026/10/26 3:40:49
ChatGLM3-6B本地部署、LoRA微调与模型合并全流程 1. 先摸清这条链路的真实边界部署、微调、合并各在做什么手上有个 6B 级别的中文对话模型业务侧给的原始诉求往往很朴素“让它按我们的说话方式来回答”。落到工程上就是三步把底座跑起来、用业务语料做 LoRA 微调、把训练出来的适配器权重合并回底座最后交付一个能直接加载的完整模型目录。ChatGLM3-6B 的本地部署、LoRA 微调、模型合并与合并后使用这四个动作串起来才是一条完整链路缺了最后一步你手里那个几十 MB 的 adapter 文件在推理服务里是没法直接当模型用的。我见过不少团队卡在第三步之外的“看起来完成了”训练 loss 降到 1.2觉得大功告成结果把 adapter 目录丢给部署同学对方加载报错或者更隐蔽——加载成功了但输出和训练前一模一样。问题不在训练在于对“合并”这件事的理解偏差。合并不是打包它是一次数值运算把低秩矩阵乘回原始权重然后丢掉那部分结构。这个动作做错前面几天的训练白费。这篇文章面向的是已经能把模型跑起来、准备把微调结果落到生产或半生产环境的读者。纯新手也能看但我会把每一步的“为什么这么做”讲透因为这条链路上真正容易出事的几乎都不是命令敲错而是选型判断错。1.1 三段式流程的职责划分本地部署解决的是“模型能在你的机器上正常对话”验证的是 CUDA、驱动、依赖、权重完整性、tokenizer 行为这一整套环境。这一步不通过后面所有事情都不可信。LoRA 微调解决的是“让模型学到业务语料的表达习惯”它不动底座的大部分参数只训练插进去的低秩分支。模型合并解决的是“把训练成果固化成一个独立模型”把 adapter 的增量加到原始权重上产出一个结构上和原底座完全一致的新模型。这三步的耦合点在权重精度上。同一个模型用 float16 加载和用 bfloat16 加载对话效果可能肉眼难以分辨但合并时混用精度就会出问题。我一般要求整条链路统一使用一种精度从下载权重到最终推理全程一致这样排查问题时不用来回猜。1.2 LoRA 相比全参微调到底省了什么用一张表说清楚这是动手前必须建立的直觉方式显存占用6B序列长度 1024产出体积训练速度适用场景全参微调bf1670GB 以上需要多卡或大显存单卡与底座同量级约 12GB慢语料量大、需要深度改变模型能力LoRAbf16 底座约 18-22GB通常 20-200MB快绝大多数业务微调LoRA4bit 量化底座约 8-10GB同上较快单卡 12-16GB 环境省下来的核心是优化器状态和梯度全参微调要维护全部参数的梯度和 Adam 的一阶二阶动量这部分是参数量的三到四倍LoRA 只维护插入的低秩矩阵那点参数量连零头都算不上。所以 6B 模型在 24GB 显存的卡上做 LoRA 是完全可行的做全参微调则必须上更大显存或者用分片方案。但这里有个代价要提前知道LoRA 的表达能力有上限。如果你的业务需要模型掌握大量新知识比如一整套内部术语体系、全新领域的事实rank 给到 64 也未必够这时候要么上更大 rank 配合更多数据要么老老实实考虑全参微调。LoRA 更擅长的是“调风格、调格式、调输出结构”而不是“灌知识”。这个判断做错你会陷入反复调参但效果不涨的循环。2. 环境这一关版本矩阵比“装最新”重要得多模型跑不起来十次里有七次是版本问题。开源生态的特点是各组件迭代节奏不一致transformers 的某个大版本改动可能让依赖它的训练框架直接不兼容。我的做法是先把版本组合钉死再谈其他。一条经过验证的组合思路是Python 3.10、PyTorch 2.1 到 2.3 之间、CUDA 11.8 或 12.1、transformers 4.36 到 4.40 区间、PEFT 0.7 以上、accelerate 0.25 以上。这不是唯一正确答案但它是冲突最少的区间。为什么强调这个区间ChatGLM3 的模型代码依赖trust_remote_code它会从权重目录里加载自带的modeling_chatglm.py这段代码对 transformers 内部 API 的调用是有版本假设的跑太新或太老的版本都可能报cannot import name之类的错误。2.1 装依赖时的顺序和坑顺序很重要先装匹配 CUDA 版本的 PyTorch再装 transformers 那一套。反过来装pip 可能给你拉一个 CPU 版 torch然后你会看到torch.cuda.is_available()返回 False白白浪费半小时。pip install torch2.1.2 torchvision0.16.2 --index-url https://download.pytorch.org/whl/cu118 pip install transformers4.36,4.41 accelerate peft datasets pip install sentencepiece cpm_kernelssentencepiece是 ChatGLM 系列 tokenizer 的硬依赖漏了会在加载 tokenizer 时直接崩。cpm_kernels是官方实现里用到的算子包新版代码对它的依赖在减弱但装上不亏。注意不要在同一环境里同时装多个训练框架的完整依赖。不同框架对 transformers 的版本要求经常打架pip 会静默降级你第二天再来跑就发现昨天能跑的脚本今天报错了。一个项目一个虚拟环境这是纪律不是建议。2.2 权重准备完整性校验别跳过权重文件动辄十几个 GB下载中断、分片缺失是常见事故。我习惯在下完先做两件事确认config.json、tokenizer_config.json、tokenizer.model、model.safetensors.index.json这些文件齐全确认分片数量和 index 里记录的一致。python -c import json, os idx json.load(open(ChatGLM3-6B/model.safetensors.index.json)) files set(idx[weight_map].values()) missing [f for f in files if not os.path.exists(os.path.join(ChatGLM3-6B, f))] print(缺失分片:, missing if missing else 无) 这一步花 10 秒能省掉后面排查“为什么加载时提示某个 key 找不到”的一小时。2.3 第一次对话测试要测到什么程度加载模型跑一句“你好”输出通顺很多人就到这儿了。我建议多测三条一条中文长问题看是否中途崩坏、一条要求结构化输出的看格式遵循、一条带多轮上下文的看历史拼接是否正确。第三条尤其重要因为 ChatGLM3 的对话历史拼接方式和你训练时用的模板必须一致这里不一致微调效果会大打折扣。import torch from transformers import AutoModel, AutoTokenizer path ChatGLM3-6B tok AutoTokenizer.from_pretrained(path, trust_remote_codeTrue) model AutoModel.from_pretrained(path, trust_remote_codeTrue, torch_dtypetorch.bfloat16, device_mapauto) model.eval() resp, hist model.chat(tok, 用三句话说明什么是向量检索, history[]) print(resp)这里用AutoModel而不是AutoModelForCausalLM是因为 ChatGLM3 的config.json里auto_map映射的是前者。用后者在部分 transformers 版本上能加载但行为不保证一致。这是个小细节但足以让你在训练脚本里遇到莫名其妙的输出。3. LoRA 微调效果的分水岭在数据不在超参先给个结论同一份数据超参在一倍范围内浮动最终效果的差异远小于把数据清洗一遍带来的差异。所以真正该花时间的是数据格式对齐和样本质量而不是盯着 lr 从 1e-4 调到 5e-5。3.1 数据格式必须和推理时的模板严格一致ChatGLM3 用的是[gMASK]sop|user|...|assistant|...这套特殊 token 结构。如果你的权重目录里tokenizer_config.json带有chat_template字段直接用tokenizer.apply_chat_template构造输入即可如果是较早的权重版本没有这个字段就要用tokenizer.build_chat_input或手动拼build_single_message。两种方式产出必须一致否则训练时模型看到的分隔符和推理时不一样学到的边界感知在线上就失效了。训练样本我建议统一成 messages 风格{messages: [ {role: user, content: 这个订单能退吗}, {role: assistant, content: 可以。请在订单页点击申请售后选择退货原因后提交48 小时内会有反馈。} ]}关键在于 loss 只对 assistant 段计算。很多开源框架默认会把 user 段也纳入 loss模型会学着去“生成用户的问题”表现为推理时自问自答。LLaMA-Factory 里对应train_on_prompt: false手写 Trainer 的话要在 collator 里把非 assistant 位置的 label 置为 -100。3.2 target_modules 别照抄 LLaMA 的配置这是新手第一大坑。LLaMA 系模型的 attention 是分离的q_proj、k_proj、v_proj、o_proj网上大量教程直接写这四个名字。ChatGLM 的 attention 把 QKV 拼成了一个线性层名字叫query_key_value你写q_proj进去PEFT 会告诉你找不到目标模块或者更糟——静默匹配不到任何模块训练跑完了但 adapter 是空的。稳妥的配置from peft import LoraConfig lora_config LoraConfig( r16, lora_alpha32, target_modules[query_key_value, dense, dense_h_to_4h, dense_4h_to_h], lora_dropout0.05, biasnone, task_typeCAUSAL_LM, )只挂query_key_value也能work覆盖 attention 的 QKV 投影参数更少、训练更快。把 MLP 的两个投影也加上表达能力更强但显存和训练时间上升。我的经验是风格类、格式类任务挂 attention 就够需要模型改变“思考方式”的任务把 MLP 一起挂上效果更明显。这也解释了为什么有人抱怨“我训了 LoRA 但模型没变化”——他挂的模块太少低秩分支的表达能力吃不下这个任务。3.3 rank 和 alpha 的关系别当两个独立旋钮lora_alpha / r是实际生效的缩放系数。r16、alpha32 意味着增量部分被放大 2 倍等效于把 LoRA 分支的学习率抬高了。所以你调 lr 和调 alpha 在方向上是一回事同时动这两个参数会让你完全失去对训练的掌控。我的习惯是固定alpha 2 * r只动 r简单任务 r8常规业务微调 r16需要较强适配 r32 到 64。r 越大参数越多、越容易过拟合小数据集如果你的样本只有几百条r 给到 64 基本等于让模型背答案。lora_dropout给小数据集留 0.05 到 0.1大数据集万条以上可以设 0。这个参数在合并时会被丢弃不影响最终模型结构只在训练期起作用。3.4 训练参数与显存优化一份在 24GB 卡上跑得动的配置TrainingArguments( per_device_train_batch_size1, gradient_accumulation_steps16, learning_rate1e-4, num_train_epochs3.0, lr_scheduler_typecosine, warmup_ratio0.03, logging_steps10, save_strategyepoch, bf16True, gradient_checkpointingTrue, optimadamw_torch, cutoff_len1024, )几个参数的理由gradient_accumulation_steps16是因为单条样本的 batch 在 6B 模型上做不了大用累积凑出等效 batch 16梯度才稳。gradient_checkpointingTrue用计算换显存大约能省 30% 到 40% 的激活占用代价是训练慢 20% 左右非常划算。cutoff_len别一上来就 2048序列长度对显存是平方级影响先 1024 跑通再往上加。如果显存实在紧张走 4bit 量化加载底座from transformers import BitsAndBytesConfig bnb BitsAndBytesConfig( load_in_4bitTrue, bnb_4bit_compute_dtypetorch.bfloat16, bnb_4bit_use_double_quantTrue, bnb_4bit_quant_typenf4, )配合prepare_model_for_kbit_training显存能压到 10GB 以内。但记住这个状态下的模型不能直接合并后面会讲。3.5 训练过程中怎么判断“它真的在学”只看 loss 曲线是不够的。loss 从 2.1 降到 1.3 是好现象但如果它三天都在 0.3 附近纹丝不动多半是数据里有大量重复样本或者 label 构造时把答案泄漏到了输入里。我的做法是每个 epoch 结束存一次 checkpoint然后用固定的三个测试 prompt 各生成一次人工看。判断标准不是“回答得像不像训练数据”而是“有没有学到你要的那个特征”。如果三轮下来回答风格完全没变回去检查 target_modules 和数据模板别急着调 lr。4. 合并这一步把增量加回去然后把脚手架拆掉训练产出的 adapter 目录里有adapter_model.safetensors和adapter_config.json。前者是低秩矩阵 A 和 B后者记录了 r、alpha、target_modules 等信息。合并的含义是对每个被适配的线性层计算W W (alpha / r) * B A得到新的权重矩阵然后把 LoRA 分支整个删掉。4.1 为什么合并后推理更快未合并时每次前向传播在经过被适配的层时要额外算一次B A x再和主路相加。这在数学上等价于一个更大的矩阵乘但工程上是两次小矩阵乘GPU 的利用率反而不如一次大矩阵乘。合并后结构回归原始没有任何额外分支推理速度能提升 10% 到 25%取决于 rank 大小和 batch显存也省下 adapter 那部分。更重要的是部署兼容性。很多推理框架只认标准模型结构多一层 LoRA 包装要么不支持要么需要额外开启参数。合并成标准模型目录任何能加载 ChatGLM3 的地方都能直接吃。4.2 合并脚本的关键点import torch from transformers import AutoModel, AutoTokenizer from peft import PeftModel BASE ChatGLM3-6B LORA output/checkpoint-600 OUT ChatGLM3-6B-merged base AutoModel.from_pretrained( BASE, trust_remote_codeTrue, torch_dtypetorch.bfloat16, device_mapcpu, # 合并放 CPU稳 low_cpu_mem_usageTrue, ) model PeftModel.from_pretrained(base, LORA) model model.merge_and_unload() model.save_pretrained(OUT, safe_serializationTrue, max_shard_size4GB) tok AutoTokenizer.from_pretrained(BASE, trust_remote_codeTrue) tok.save_pretrained(OUT)四个地方值得单独说。device_mapcpu合并是纯数值操作放 CPU 上跑虽然慢6B 模型大概几分钟但绝不会因为显存碎片或临时张量分配失败而中断。GPU 上合并不是不行只是在 24GB 卡上做 6B 的合并峰值占用很容易顶到边界。torch_dtype保持和训练一致训练用了 bf16合并就用 bf16。如果底座是 fp16 训练是 bf16虽然能合但数值误差会累积表现为合并后输出和合并前有肉眼可见的差异。merge_and_unload()这个名字的含义是先合并、再把 LoRA 层从模型结构里摘掉。只调merge()不调unload()模型里还留着 LoRA 结构保存出来的权重会带冗余 key加载时可能报 unexpected keys。max_shard_size不设的话默认 5GB 分片某些存储或传输环节对单文件大小有限制设成 4GB 更保险。4.3 合并前后一致性怎么验这是我认为整条链路里最容易被跳过、但最该做的一步。方法很直接用同一批 prompt分别让“底座 adapter动态加载不合并”和“合并后的模型”生成do_sampleFalse保证确定性比较输出。def gen(model, tok, prompt): inputs tok.apply_chat_template( [{role: user, content: prompt}], add_generation_promptTrue, return_tensorspt, return_dictTrue ).to(model.device) with torch.no_grad(): out model.generate(**inputs, max_new_tokens128, do_sampleFalse) return tok.decode(out[0][inputs[input_ids].shape[1]:], skip_special_tokensTrue)两边的输出应该高度相似个别标点或语序差异属于浮点误差累积的正常范围。如果差异大到语义都变了回去查三件事dtype 是否一致、tokenizer 是否用的是同一个、adapter 的adapter_config.json里的target_modules是否和你以为的一致。4.4 合并失败和效果退化的排查路径按出现频率排现象大概率原因处理报错找不到某个模块adapter_config 里的 target_modules 与模型结构不匹配打印model.named_modules()核对真实层名合并后输出乱码或复读dtype 混用、chat template 不一致全程统一 bf16统一用同一 tokenizer 构造输入合并后效果和训练前无差别adapter 权重没真正加载上或训练时模块没匹配到加载后打印可训练参数数量应为数百万级合并过程 OOM 或进程被杀内存不足6B 合并峰值需要 24GB 以上内存换更大内存机器或用分片合并再拼接保存后文件体积异常小保存时被中途中断或 shard 写入失败检查输出目录文件数量与 index 是否一致“合并后效果和训练前无差别”这条我特别拎出来讲。它最隐蔽因为流程没报错。根因通常是训练阶段target_modules写错导致实际上没有可训练参数框架可能给了 warning 但被日志淹没了。训练启动后第一件事就该打印print(model.print_trainable_parameters())看到 trainable 参数是 0 立刻停下。5. 合并后的模型怎么用三种加载路径的取舍拿到合并后的目录怎么用取决于场景本地调试、单机服务、还是高并发在线服务。三条路的技术选型完全不同。5.1 transformers 直接加载调试和小流量首选import torch from transformers import AutoModel, AutoTokenizer path ChatGLM3-6B-merged tok AutoTokenizer.from_pretrained(path, trust_remote_codeTrue) model AutoModel.from_pretrained(path, trust_remote_codeTrue, torch_dtypetorch.bfloat16, device_mapauto) model.eval() resp, _ model.chat(tok, 把这段话改成更正式的表达这事儿咱得赶紧办, history[])好处是零额外依赖、行为可预测、便于逐 token 调试。坏处是没有批处理优化并发一上来吞吐就崩。做功能验证、效果回归、badcase 分析用它最合适。model.chat内部会处理历史拼接和特殊 token比自己拼generate更稳。如果你要走generate手动控制注意eos_token_id要设成 ChatGLM3 的结束符 id否则模型会一直生成到max_new_tokens上限才停。5.2 高并发场景换推理引擎的边界条件追求吞吐时通用做法是换成带 PagedAttention 和连续批处理的推理引擎。这里要提醒的是架构支持情况一定要自己验。这类引擎对模型架构的支持是逐个加的某个版本支持了 ChatGLM 家族不代表你的具体变体一定跑得通尤其在你合并之后config.json里的architectures字段必须保持原样。我的操作习惯是先用小max_model_len和单请求跑通再压并发。直接上压力测试一旦报错你分不清是模型问题还是参数问题。另一个选项是转成 GGUF 走 CPU/混合推理。这条路适合资源受限的边端部署但转换脚本对 ChatGLM 架构的支持需要按实际版本来验转换完必须做一遍输出比对不能默认“转成功了就等于效果一致”。5.3 多套 LoRA 热切换还是合并成多个模型如果业务上有多个场景客服话术、文档摘要、代码助手每条线都训了一套 LoRA这时候有个选择合并成 N 个完整模型还是保留一个底座加 N 个 adapter 动态切换。方案显存占用切换成本推理速度适用合并成 N 个模型N × 12GB需换模型实例最快每个场景独立部署、流量大底座 动态 adapter12GB N × 几十MB秒级切换略慢场景多、单场景流量小、需要 A/B动态切换靠 PEFT 的load_adapter加set_adapter实现多套 adapter 共享一份底座权重。这种做法在资源紧张的环境里非常实用代价是每层多一次低秩计算。我一般在验证阶段用动态切换快速对比多套效果确定要长期跑的那个再合并固化。6. 几个反复出现的坑以及它们为什么必然出现6.1 词表被改过合并后 embedding 对不上如果你在微调时为了压缩特殊符号往 tokenizer 里加了新 token并resize_token_embeddings那 adapter 里会包含 embedding 相关的增量。合并时底座必须用同样扩展过词表的版本否则尺寸对不上直接报错。更麻烦的情况是尺寸对上了但语义错位——embedding 行序没有一致性保证。处理办法微调时改过词表就把扩展后的 tokenizer 和模型一起存一份合并时用它做底座别拿原始权重。这也是我一般不推荐在 LoRA 微调阶段动词表的原因代价和收益不成比例。6.2 显存看起来够但就是 OOMnvidia-smi显示的显存和实际可用显存是两回事。PyTorch 的缓存分配器会预留但未归还长时间跑训练脚本容易碎片化。表现是第一次能跑第二次同样配置就 OOM。三个缓解手段训练脚本开头设os.environ[PYTORCH_CUDA_ALLOC_CONF] expandable_segments:True每个 epoch 结束手动torch.cuda.empty_cache()会带来性能损失酌情用把per_device_train_batch_size降到 1 用梯度累积补。最根本的还是别把显存用到 95% 以上留 10% 余量给临时张量。6.3 合并后模型变得爱复读、答非所问这通常不是合并的锅是训练阶段就过拟合了。判断方法很简单合并前用动态 adapter 测同一批 prompt如果那时就已经复读问题在训练如果合并前正常合并后坏了才是合并环节的问题。过拟合的典型特征是训练 loss 极低0.2 以下、训练集上回答几乎逐字复现、换一个问法就答偏。诱因一般是数据量小几百条配大 rank、epoch 跑到 5 以上、lr 给到 3e-4。这几项同时出现模型基本就是在背样本。修法是数据优先样本扩到 1000 条以上、去掉高度重复的样本、打乱顺序、epoch 控制在 2 到 3、rank 降到 8。真觉得数据不够宁可少训训 1 个 epoch 的欠拟合模型在线上比过拟合的好收拾。6.4 4bit 训练出来的 adapter 直接合并会翻车量化加载的底座线性层里存的是量化后的权重和 scalemerge_and_unload在这种结构上要么报错要么合并出一堆无意义的数值。正确做法是绕开训练时的量化模型重新用 bf16 加载一份原始底座再套上 adapter 合并base AutoModel.from_pretrained(BASE, trust_remote_codeTrue, torch_dtypetorch.bfloat16) model PeftModel.from_pretrained(base, LORA_PATH) model model.merge_and_unload()adapter 里存的低秩矩阵本身是高精度的和底座的量化状态无关所以这个做法完全成立。我甚至建议训练时就算显存够也用这套 bf16 合并流程因为它最不容易出意外。7. 走完一遍之后我固定下来的几条习惯每次做这类交付我会在输出目录里额外放一个MERGE_INFO.md记录底座版本、LoRA 的 rank/alpha/target_modules、训练数据规模和 epoch 数、合并用的 dtype、合并前后的验证结果。看起来是多余动作但两个月后有人问“这个模型到底怎么来的”翻这个文件比翻聊天记录快得多。另一个习惯是永远保留未合并的 adapter 目录。合并后的模型体积大、传输慢做 A/B 对比或者换底座重训时adapter 加一份训练配置就能复现成本低得多。合并产物是交付物adapter 是资产两者别混为一谈。还有一点关于精度如果你的线上服务对确定性有要求合并后一定要做一次完整的效果回归不要只测三五条。LoRA 合并引入的数值扰动虽然微小但它作用在每一层上累积效应在小概率情况下会让某类输入的输出发生跳变。我在一个分类倾向明显的任务上就遇到过这种跳变——大部分样本一致个别边界样本的判定翻转了。这种问题只能靠回归集发现靠抽检发现不了。最后分享一个调试技巧当你不确定合并是否生效时构造一个训练集里出现过的原句让模型补全。如果它接上了训练数据里的后半句说明 adapter 生效了如果接的是通用语料风格的续写说明没生效。这比看 loss 曲线直观得多也比跑完整评估快。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询