从零开始大模型开发与微调:基于PyTorch与ChatGLM的实战指南
聊点实在的。这两年“大模型开发与微调”几乎成了算法岗简历上的标配关键词但真正从零开始上手的人里十个有八个都卡在第一步环境装不起来模型加载报错更别提拿自己的业务数据去微调了。这篇内容就围绕《从零开始大模型开发与微调基于PyTorch与ChatGLM》这条完整路线把我自己踩过坑之后整理出来的流程写一遍——从 PyTorch 环境搭建、CUDA 适配到 ChatGLM 模型加载、数据处理再到用 LoRA 跑通一次真正的微调训练最后配上模型合并和推理验证。适合刚入门算法的同学也适合有 PyTorch 基础、但还没碰过开源大模型微调的工程师。1. 先从整体思路说起你真正要做的“大模型开发”是什么1.1 预训练和你关系不大微调才是主线很多人听到“从零开始大模型开发”第一反应是“我要重写一遍 Transformer从头训练一个 GPT”。真没必要。以 ChatGLM 为例ChatGLM-6B 这种量级的模型一次预训练的成本是百万级算力小时起步个人或者小团队根本扛不住。实际工程里说的“从零开始”指的是从零开始掌握这套工具链拿一个成熟的开源基座模型加载起来用自有数据做微调最后部署成能回答业务问题的服务。这里要先把“预训练”和“微调”这两个词掰开。预训练是拿海量通用文本让模型学会语言本身的规律相当于把一个人培养成受过通识教育的普通人微调是用领域里的高质量数据让模型学会你期望它掌握的技能和表达方式相当于给这个人做岗位培训。大模型在预训练阶段已经会说话、会推理但没接触过你的私有数据和业务场景所以直接问它“请按我们公司的格式写周报”它即使能答也答不到点子上。微调的目的就是让它从“通用”走向“特定”。这里还有一个常见误解微调不是“给模型灌知识”。模型记不住太多新事实它真正学的是“对话的格式、任务的模式、输出的风格”。想让它记住大量知识优先考虑检索增强而不是硬灌这一点在后面的数据准备部分还会展开。1.2 为什么偏偏是 PyTorch 加 ChatGLM先说框架。PyTorch 在今天的大模型圈子里基本是事实标准HuggingFace Transformers、PEFT参数高效微调、bitsandbytes、accelerate 这些工具链都是优先支持 PyTorch 的。TensorFlow 也能做大模型但你搜资料时会发现凡是涉及 LoRA、QLoRA、DeepSpeed 的教程十篇里有九篇是 PyTorch 代码。选 PyTorch本质上是选生态不让自己在起跑线上就陷入“找半天没有一个能直接跑的 demo”的窘境。再说模型。ChatGLM 是智谱AI开源的中文对话大模型系列ChatGLM-6B、ChatGLM2-6B、ChatGLM3-6B 都是公开权重可以本地跑的。为什么拿它当教学载体三个理由第一中文效果好中文踩坑案例也多第二6B 参数量在消费级显卡上有机会跑微调换成 65B 的模型个人电脑基本没戏第三它原生接近对话场景做完微调能直接做智能客服、文档问答这类演示正反馈来得快。对于从零开始的开发者这个组合试错成本最低。1.3 零基础怎么规划学习路径如果连 PyTorch 都还没入门直接冲 ChatGLM 微调会很难受。我给你一个按比例分配时间的路径先用一到两周过 PyTorch 基础重点看张量操作、自动求导、nn.Module、DataLoader 和训练循环这五件事不用抠得太深能跟着教程跑通一个图像分类或文本分类就够了。很多人说自己是看入门视频起步的这个路线没问题关键是要亲手敲代码不能只看然后花三到五天熟悉 HuggingFace 的加载与推理流程把模型加载、tokenizer 那套逻辑跑通最后再进入微调阶段。这个顺序看起来慢实际上最快。我见过太多一上来就找“ChatGLM 微调代码”的人结果模型加载完连 loss 是怎么算出来的都不知道训练崩了也不知道从哪查起。PyTorch 基础不是可选项是排查问题的底气。2. 环境准备PyTorch 安装与 CUDA 适配最容易翻车的一步2.1 硬件要求先对齐别拿集显硬扛跑 ChatGLM-6B 的推理至少要 12GB 显存fp16 精度下做 LoRA 微调建议 16GB 以上24GB 的显卡RTX 3090、4090 或 A5000 之类体验比较舒服。全参微调 6B 模型对个人玩家就不太现实了动辄 40GB 以上显存所以后面我会把重点放在 LoRA 上。没有 N 卡怎么办两个选择一是用 CPU 推理跑通流程能跑但很慢而且微调基本不用想二是租云 GPU 实例按小时付费很多平台有 24GB 显存的卡可选新手练习反而比买卡划算。另外像 MX150 这种入门级独显跑点小规模 PyTorch 练习没问题大模型就别指望了该上云上云。2.2 Anaconda 创建隔离环境别碰系统 Python环境管理这一步强烈建议用 Anaconda 建虚拟环境。原因很朴素深度学习项目依赖版本冲突太常见了今天装的 torch 版本很可能跟下个项目的 torchvision 打架。用 conda 把每个项目隔开出了问题直接删环境重建五分钟就能恢复。conda create -n llm python3.10 conda activate llm为什么要 3.10这是个兼容性经验值。ChatGLM 相关依赖链transformers、peft、datasets、accelerate 等在 Python 3.8 到 3.11 之间都能工作但 3.10 是这几个库支持最均衡的版本太低的 Python 对一些新库支持不好太新的可能遇到个别依赖没跟上。后续所有操作都在这个 llm 环境里进行。2.3 PyTorch 与 CUDA 版本匹配三个概念先分清这一步是新手最常翻车的地方。先记住三件事显卡驱动driver、CUDA Toolkit、PyTorch 编译时用的 CUDA 版本是三样东西不是一回事。显卡驱动是底层的它决定了系统能不能让程序调用 GPU。驱动版本太老上面跑什么都会报错。CUDA Toolkit 是开发库里面是编译器和运行时库。很多博客会让你去官网单独装 Toolkit但在 PyTorch 场景下通常不强制手动装PyTorch 的安装包里自带一部分运行时依赖。PyTorch 的 whl 包名里的 cu 后缀代表它链接的 CUDA 版本。比如 torch-2.1.2cu118 表示这个包匹配 CUDA 11.8torch-2.3.1cu121 匹配 CUDA 12.1。理解了这三者的关系判断兼容性就一句话显卡驱动版本不能低于 PyTorch 所对应 CUDA 版本的最低驱动要求否则程序就跑不起来。怎么知道自己该装哪个先用一条命令看驱动支持到什么级别nvidia-smi右上角会显示 CUDA Version这个值代表当前驱动最高支持的 CUDA 版本。比如它显示 12.1那你可以放心选择 cu121 甚至更低的 cu118 版本安装都不会超出驱动能力。如果驱动只支持到 11.8那就老老实实装 cu118 的版本。推荐一套组合PyTorch 2.1.2 CUDA 11.8或者 PyTorch 2.3.1 CUDA 12.1。前者兼容性强后者更接近新版。安装命令统一用 pip 就行pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118装完先验证这一步别跳过import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))如果输出的 torch.version末尾带 cpu说明下载成了 CPU 版本赶紧重装如果 is_available() 是 False优先怀疑驱动太旧。2.4 Ubuntu 和 CentOS 下的环境坑Ubuntu 相对省心CentOS 用户在装依赖时容易遇到缺底层库的问题这里给一段通用排查思路。如果是服务器内网环境不能直接在线装就用离线方案在一台有网且系统架构一致的机器上执行 pip download把所有 wheel 包拉下来打包拷贝到目标机器再 pip install --no-index --find-links./wheelhouse 安装。注意 PyTorch 的 wheel 包很大下载时要确认磁盘空间充足。还有一个容易被忽略的点别把 pip 和 conda 混着随便装。我建议虚拟环境里优先用 pip 装 Python 包conda 主要管 Python 版本和环境的创建销毁。混用容易把包的依赖解析搞乱后期一升级就出幺蛾子。很多人报环境问题最后追查下来都是因为曾经在同一个环境里用 conda 装了一个包、用 pip 装了另一个两个依赖图互相覆盖一升级就崩。2.5 低端卡也能学 PyTorch但别做大模型如果你的显卡是 MX150 这类入门卡甚至核显不是说不能学。学 PyTorch 基础、跑小网络、处理自己的小数据集完全没问题很多热词里提到的“PyTorch 处理高光谱 hdr 文件”“UCF101 视频分类”“TD3 强化学习”这类项目在低配机器上都能跑。但这些项目练完之后要意识到大模型是另一个量级硬件不够就上云别在本地硬耗时间。3. 加载 ChatGLM理解模型结构再动手写代码3.1 ChatGLM 背后的 GLM 架构是怎么回事先别急着复制粘贴代码花五分钟了解下它是什么。ChatGLM 系列用的是 GLMGeneral Language Model架构和 GPT 那种标准自回归模型有一个关键区别GPT 是从左往右一个字一个字预测GLM 是“自回归空白填充”可以理解成一个高级版完形填空——给定一部分文本让它预测被抠掉的片段预测时按自回归方式一个 token 一个 token 生成。这个设计带来的好处是GLM 既能做生成任务也能做理解任务所以中文能力表现不错。对于你来说这些细节暂时不需要完全吃透但至少要记住它是个 Transformer 家族模型所以用 transformers 库就能加载它的代码里带有 chat 方法所以可以很方便地做多轮对话。这些认知会在排查问题时派上用场比如看到报错里出现 Transformer 层的名字你能大概定位到是哪一部分出了问题。3.2 用 HuggingFace Transformers 加载模型加载 ChatGLM 权重非常标准几行代码import torch from transformers import AutoModel, AutoTokenizer model_name THUDM/chatglm3-6b tokenizer AutoTokenizer.from_pretrained(model_name, trust_remote_codeTrue) model AutoModel.from_pretrained( model_name, trust_remote_codeTrue, torch_dtypetorch.float16, device_mapauto ).eval()这里有几个点值得展开说说。trust_remote_codeTrue 是因为 ChatGLM 的模型定义需要执行官方仓库里附带的自定义代码不加这个参数会直接报错torch_dtypetorch.float16 是把模型权重加载成半精度显存占用几乎减半在消费级显卡上这是标配操作device_mapauto 让框架自动把模型分配到可用设备上。第一次执行会从 HuggingFace 下载权重6B 模型的 fp16 权重有 12GB 左右网速不理想时会很煎熬。我的经验是先把权重手动下载到本地目录然后用本地路径加载。下载下来后检查目录里要有 pytorch_model.bin 或多个 .safetensors 分片文件、config.json、tokenization 相关文件缺一不可。批量下载一堆二进制大文件时文件名要对上错一个文件名加载时就各种诡异报错。3.3 指令微调数据长什么样微调 ChatGLM 这种对话模型数据格式遵循“指令-输出”的结构。我常用的是一份 JSONL 文件每一行是一条训练样本{instruction: 请用一句话解释什么是反向传播, input: , output: 反向传播是从损失函数出发沿网络结构反向逐层计算每个参数梯度的算法。} {instruction: 根据给定的需求写一段商品文案, input: 商品无线蓝牙耳机卖点降噪、续航24小时, output: 这款无线蓝牙耳机主打深度降噪单次充电续航长达24小时让通勤路上彻底告别外界喧嚣。}注意 input 字段是可选的没有额外输入就留空。数据数量上新手阶段准备几百条高质量样本就能看到明显效果比一次性塞几万条噪声数据强得多。数据质量永远排第一格式不统一、答案错漏、指令和输出不对齐模型都会学坏。而且微调数据要尽量贴近你真实使用场景否则训完还是“答非所问”。3.4 关于 transformers 版本和模型兼容性这里必须专门说一个常见误区。很多人搜“哪个版本的 PyTorch 和 CUDA 能支持 transformers3.4.0”其实问错了方向。transformers 是 Python 包它的版本约束主要来自 Python 版本和 torch 的接口跟 CUDA 版本没有直接绑定关系。CUDA 影响的是底层能不能调用 GPUtransformers 管的是上层模型逻辑两者不是一回事。真正要注意的是模型权重与 transformers 版本的兼容性。比如 ChatGLM2 发布时配套的 transformers 版本较老如果你装了一个很新的 transformers可能会碰到 tokenizer 类型找不到、模型类不注册之类的报错。解决办法是安装官方 README 里 requirements.txt 指定的依赖版本别轻易追求最新版。实操中遇到这类问题优先看官方仓库的说明文件这是最靠谱的方案。4. 微调实操用 LoRA 跑通 ChatGLM4.1 为什么选 LoRA 而不是全参微调ChatGLM-6B 有 60 亿参数全参微调时优化器状态和梯度都要占显存算下来 24GB 的卡根本不够用个人玩家基本可以放弃这个路线。LoRALow-Rank Adaptation的思路是冻结原模型全部参数只在每层注意力模块旁边注入两个小矩阵训练时只更新这些小矩阵。用一个小类比原模型是一本已经印刷好的教科书LoRA 是课堂上补充的几页讲义不动原书只改讲义却能显著改变模型的输出风格。LoRA 训练时显存需求大幅下降实测在单张 24GB 显卡上配合 fp16、batch_size 设 4、最大序列长度 512能比较从容地训练。显存再紧张就上 QLoRA它先把模型量化成 4bit 再套 LoRA甚至能在 8GB 到 12GB 显存上跑微调但训练速度会慢一些。三者的对比可以看下面这张表方案训练参数量显存需求6B量级效果保留度适合场景全参微调60亿40GB以上最高有A100/H100等专业卡追求极限效果LoRA数百万到千万级16GB~24GB高消费级显卡大多数业务微调场景QLoRA数百万到千万级8GB~12GB较高显存紧张优先跑通流程只要显存够用优先 LoRA显存不够再降级到 QLoRA。效果差距在大多数任务上肉眼分辨不出来LoRA 是性价比比较高的起点。4.2 安装微调所需依赖在 llm 环境里执行pip install peft transformers datasets accelerate bitsandbytes这几个库的分工peft 提供 LoRA 等参数高效微调方法transformers 负责模型和训练器的上层封装datasets 用来加载和处理数据accelerate 是训练加速框架Trainer 底层会用到bitsandbytes 是 QLoRA 的量化后端。哪怕你暂时不用 QLoRA也建议一起装上后面显存不够想切过去能省一步安装。装了这么多库之后环境开始变得脆弱。我的习惯是把当前环境所有包版本导出一份pip freeze requirements.txt这样环境一旦崩了重建之后一条命令就能复原省得二次踩坑。4.3 构造数据集类微调前先把数据集做成 PyTorch 的 Dataset。ChatGLM 系列的多轮格式一般是 [Round 1] 加一问一答我们做演示时可以先用简化拼接重点是把流程跑通。import json import torch from torch.utils.data import Dataset class SFTDataset(Dataset): def __init__(self, data_path, tokenizer, max_len512): self.samples [] with open(data_path, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue item json.loads(line) prompt item[instruction] inp item.get(input, ) output item[output] text f问{prompt}\n{inp}\n答{output} self.samples.append(text) self.tokenizer tokenizer self.max_len max_len def __len__(self): return len(self.samples) def __getitem__(self, idx): text self.samples[idx] enc self.tokenizer( text, max_lengthself.max_len, truncationTrue, paddingmax_length, return_tensorspt, ) input_ids enc[input_ids].squeeze(0) attention_mask enc[attention_mask].squeeze(0) # 标签用 input_ids 本身训练时模型会自己 shift 一个位置 labels input_ids.clone() return {input_ids: input_ids, attention_mask: attention_mask, labels: labels}两个重点解释一下。paddingmax_length 会把所有样本补到同样长度batch 训练时不会因长度不一致报错代价是短样本浪费一点显存labels 直接复制 input_ids因为自回归模型在计算 loss 时会自动在序列维度上做 shift也就是用一个 token 预测下一个 token不需要手动制造错位。4.4 配置 LoRA 并启动训练加载模型后用 peft 包套一层 LoRAfrom peft import LoraConfig, get_peft_model lora_config LoraConfig( task_typeCAUSAL_LM, r8, lora_alpha32, target_modules[query_key_value], lora_dropout0.1, ) model get_peft_model(model, lora_config) model.print_trainable_parameters()几个参数说一下。r 是低秩矩阵的秩r 越大可训练参数量越多、模型表达能力越强但也更容易过拟合8 或者 16 是常见起点lora_alpha 是放缩系数最终的实际缩放是 lora_alpha / r32 / 8 4这个值调节 LoRA 更新对原模型的“音量”调太大训练会不稳定调太小容易训不动target_modules 要指向模型中真正做注意力投影的层对 ChatGLM 来说就是名为 query_key_value 的线性层不同模型这个字段不一样换模型时得去模型代码里确认。训练直接用 Trainerfrom transformers import Trainer, TrainingArguments training_args TrainingArguments( output_dir./chatglm-lora, per_device_train_batch_size4, gradient_accumulation_steps8, learning_rate1e-4, num_train_epochs3, logging_steps10, save_steps500, fp16True, remove_unused_columnsFalse, ) trainer Trainer( modelmodel, argstraining_args, train_datasettrain_dataset, tokenizertokenizer, ) trainer.train()训练参数的设定逻辑在这里。batch_size 为 4 是显存和速度的折中如果报显存不足先把它降到 2 或 1gradient_accumulation_steps8 表示每 8 个小 batch 累计一次梯度等效于一次更新用了 4*832 个样本能模拟大 batch 的稳定性又不占显存learning_rate 用 1e-4因为 LoRA 只训练少量参数学习率可以比全参微调的 2e-5 高一截fp16True 开启混合精度显存减半速度还会提升是消费级显卡必备选项。自定义数据集放到 Trainer 时remove_unused_columnsFalse 必须设否则 Trainer 会自动丢弃 data collator 用不到的列而我们的数据集返回的是字典某些字段会被误删导致报错。训练完保存两种东西model.save_pretrained(./chatglm-lora-checkpoint) tokenizer.save_pretrained(./chatglm-lora-checkpoint)注意这里保存的是 LoRA 适配器不是整套权重文件大小只有几十到一百多 MB。想合并回原始模型用 merge_and_unloadmerged_model model.merge_and_unload() merged_model.save_pretrained(./chatglm-lora-merged)合并产出一个完整的 fp16 权重可用于部署或继续微调。两条保存路径用途不同适配器格式利于快速切换多个微调任务合并格式适合上线推理。4.5 推理验证微调前后对比训练完加载用的方式也要调整from peft import PeftModel base_model AutoModel.from_pretrained( THUDM/chatglm3-6b, trust_remote_codeTrue, torch_dtypetorch.float16, device_mapauto ).eval() model PeftModel.from_pretrained(base_model, ./chatglm-lora-checkpoint) model model.eval() response, history model.chat(tokenizer, 请解释什么是反向传播, history[]) print(response)推理时一定要先加载基座模型再把 LoRA 适配器挂上去顺序反了会报一堆 shape 不匹配的错。建议你保留微调前的模型做对比同样一个问题微调前和微调后各问一遍看输出风格、知识范围、格式是否按预期改变。如果差异为零大概率是数据量太少或学习率太低如果训练集上能完美复述、测试问法上却答非所问大概率是过拟合了要降 rank、加数据或加 dropout。5. 常见问题与排查技巧实录5.1 CUDA 相关报错怎么定位把最常见的情况列成速查表现象可能原因解决方向torch.cuda.is_available() 返回 False装成了 CPU 版或驱动未生效检查 torch.version是否带 cpu 后缀重装对应 cu 版本再查 nvidia-smi 是否正常报错 no kernel image is available for execution on the device显卡太老或驱动不支持当前 CUDA 版本升级驱动或换用更低版本的 cu 后缀 PyTorch训练时报 CUDA out of memory显存不够先降 batch_size再考虑开 gradient_accumulation、开 fp16、缩短 max_len最后才是换 QLoRA出现 warning: you need pytorch with cu130 or higher to use optimized cuda operati编译时使用的 cu 版本与你当前资源不匹配检查显卡驱动版本换一个与驱动对应的 PyTorch 版本或升级驱动定位 CUDA 问题有个通用顺序先 nvidia-smi 看驱动再 python 看 torch 版本和 is_available()再跑一个最小样例。别一上来就换环境先把三层信息收集齐大多数问题十分钟内能定位。5.2 显存不够的最后一搏QLoRA如果降 batch_size、降序列长度、开 fp16 还是 OOM终极方案是 QLoRA。做法是把基座模型加载成 4bit 量化from transformers import BitsAndBytesConfig bnb_config BitsAndBytesConfig( load_in_4bitTrue, bnb_4bit_quant_typenf4, bnb_4bit_compute_dtypetorch.float16, ) model AutoModel.from_pretrained( THUDM/chatglm3-6b, trust_remote_codeTrue, quantization_configbnb_config, device_mapauto )注意 QLoRA 的模型因为已经量化训练完通常不推荐直接 merge 回 fp32导出部署时要用原模型加载再挂载适配器。另外量化加载时对 transformers 和 bitsandbytes 的版本敏感如果报 QuantizationSetupError先检查 bitsandbytes 是否与当前 torch 和 CUDA 匹配再检查 Python 版本。5.3 模型加载时的 tokenizer 兼容性问题“A tokenizer class must be passed”或“xxx is not a valid tokenizer class”这类报错多数出现在新版本 transformers 加载老模型时。ChatGLM 的自定义 tokenizer 依赖仓库里的代码加载时 trust_remote_codeTrue 没加是常见原因加了还报错就查 transformers 版本是不是太新。我的处理方式是看官方仓库的 requirements.txt把关键包版本对齐不要盲目追新。5.4 训练 loss 不降或过拟合的排查方向loss 始终不降先加一条“打印模型参数是否更新”的调试代码确认 LoRA 适配器确实插进去了print_trainable_parameters 有没有输出可训练参数其次看学习率是否过小1e-4 训不动就试 2e-4 或 5e-4再检查数据格式如果标签和输入混在一起模型可能在学“背答案”而不是学“根据指令回答”。过拟合的判断标准则是训练 loss 下降、验证集回答质量反而变差这时应该减少 epoch、增大数据量、加大 dropout而不是盲目继续训。5.5 离线环境安装的经验内网机器装 PyTorch我的做法是在一台联网机器上先创建相同 Python 版本的虚拟环境然后执行pip download torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 -d ./wheelhouse再把 wheelhouse 整个目录拷贝到内网机器执行pip install --no-index --find-links./wheelhouse torch torchvision torchaudio这里有个细节torch 的 wheel 依赖 nvidia 开头的多个 CUDA 运行库包pip download 时要把依赖也一起下下来建议在联网机器上用 pip download 命令把这些依赖一并拉取否则内网装时还是会提示找不到依赖。离线环境最容易栽的坑就是“包下全了但缺间接依赖”备一份 requirements.txt让 pip 正常解析依赖后再下载比手动一个个找要稳得多。5.6 训练时间长怎么办LoRA 在 24GB 单卡上训练几百条数据每个 epoch 大概几分钟到几十分钟还算可以接受。如果数据集上万条单卡就有点吃力了。这时候优先优化数据不是所有数据都值得训练很多重复样本反而拖慢收敛其次是调小 max_len序列长度对训练耗时的影响是线性的把 512 改成 256时间能省一半前提是你的任务不需要长上下文。千万不要一上来就上多机多卡那个复杂度对新手不友好单卡 LoRA 能解决的场景真的很多。最后再分享一个小技巧回到标题里那句“从零开始”我想再啰嗦一句别追求一次把所有东西都搞懂这是我从自己第一次微调就崩掉的过程里总结出来的。把目标拆成“今天能把环境装好”“明天能加载模型”“后天能跑通一次训练”这种小里程碑每完成一个都有正反馈后面碰到报错也不慌。而且做完一轮微调之后别急着删代码把它整理成一个模板换数据集、换模型、换超参数都能复用这才是真正的沉淀。纸面的知识很容易忘能跑的流程才是你自己的。