我最近在帮朋友调一个小型问答系统,连续折腾了几晚,从环境搭建到微调、推理全链路走了一遍。用的卡就是 RTX 3090 24GB,模型是 ChatGLM3-6B,微调方式分别试了 LoRA 和 P-TuningV2 两条路线。今天把整个过程整理成一篇实战笔记,核心就一句话:在 24GB 显存的单卡上,把环境准备控制到 15 分钟内,然后跑通 ChatGLM3-6B 的 LoRA 与 P-TuningV2 微调,全程显存占用大约在 22GB 左右。这篇文章不聊玄乎的理论,只讲我踩过的坑、验证过的命令和可以直接抄作业的代码。
标题里那个"需22GB显存"不是随便写的。我实际训练时监控过显存曲线,LoRA 在 batch size 为1、最大序列长度512的场景下,峰值分分钟摸到21GB以上,偶尔能冲到23GB。这意味着 24GB 的卡几乎就是贴着安全线在跑,稍不注意就 OOM。所以这篇文章不仅教你怎么准备环境、怎么写微调代码,还会告诉你显存是怎么被瓜分掉的、哪些参数能救急、哪些坑我替你趟过了。适合手里有 3090、4090、A6000 这类 24GB 显存显卡,想本地跑通大模型微调的开发者,也适合理清 LoRA 和 P-TuningV2 区别后做技术选型的朋友。
1. 前置认知:为什么是ChatGLM3-6B,又为什么选LoRA和P-TuningV2
1.1 ChatGLM3-6B 能做什么,适合什么场景
先明确一件事:ChatGLM3-6B 是一个中英双语的开源对话模型,参数规模约 6B,也就是 62 亿参数左右。这个体量在"能力"和"成本"之间卡得比较准。往上走,13B 甚至 70B 的模型能力更强,但一张 24GB 卡跑推理已经吃力,微调更是想都别想;往下走,几百M到1B的模型跑起来轻松,但在中文理解、指令跟随、长上下文对话上的表现,跟 6B 有明显的代差。所以如果你的目标是给某个垂直领域做"知识注入"或者"说话风格对齐",6B 是目前单卡可微调的最甜点位。
我这次微调的目标很朴素:让模型在特定业务问答上更听话,遇到超纲问题不要乱编。这不是从零训练大模型,而是在一个已经具备通用能力的底座上做「行为校准」。这类需求用全参微调是浪费的,也是跑不动的。正确的做法是参数高效微调(Parameter-Efficient Fine-Tuning),在只更新极小一部分参数的前提下,让模型学会新任务。LoRA 和 P-TuningV2 是这条路径上最主流的两套方案。
1.2 22GB显存需求到底怎么算出来的
很多人一上来就问"为什么是22GB,不是20GB也不是18GB",这个问题你得从显存账本看。一个 6B 参数的模型,用 FP16 半精度存储,光权重就是 6.2 × 2 ≈ 12.4GB。推理阶段除了权重,还要为每个 token 准备 KV cache;训练阶段更狠,反向传播需要保存中间激活值,Adam 优化器还要为每个可训练参数保存动量和方差。
这里有个大前提:全参训练是绝对不可能跑的。6B 全参训练,权重 12.4GB + 梯度 12.4GB + 优化器状态约 25GB,加起来超过 49GB,3090 的 24GB 连一半都装不下。所以必须走 LoRA 或 P-TuningV2 这种冻结基座、只训练少量参数的路子。
显存账本在参数高效微调模式下大致长这样:
| 占用项 | 估算值 | 说明 |
|---|---|---|
| 模型权重(FP16) | 约12.4GB | 冻结,常驻显存 |
| 可训练参数梯度及优化器状态 | 0.1~0.5GB | LoRA/P-TuningV2 参数很少 |
| 前向激活与反向计算图 | 3~6GB | 与 batch size、序列长度强相关 |
| KV cache 与临时中间量 | 1~3GB | 训练时前向推理同样会产生 |
| CUDA context 与缓存池 | 1~2GB | PyTorch 初始化即占用 |
| 合计 | 约19~23GB | 取决于超参配置 |
这套账算下来你就明白了,24GB 卡跑这个任务,留给你的余量其实只有 1~2GB,所以标题里说"需22GB显存"完全不是危言耸听。这也是为什么后面所有超参设置都要带着"省显存"的思路去做。
1.3 说明书级部署环境清单
开工前先把家底盘清楚。我这边整套环境是这样的,你可以直接对照:
- 显卡:RTX 3090 24GB,实测足够。4090、A6000、A5000 同理。
- 驱动:建议 535 以上。显存管理、CUDA 上下文分配的稳定性跟驱动版本关系很大。
- Python:3.10。3.8 和 3.11 也行,但 3.10 在依赖兼容性上最省心。
- PyTorch:2.1.0 或 2.1.2,CUDA 12.1 版本。别用 CPU 版本,显卡再好也没用。
- 系统内存:建议 32GB 起步。加载权重时会先从磁盘读进内存再搬到显存,内存太小容易卡死。
- 磁盘:预留 30GB 以上。模型权重约 12GB,训练输出、缓存、数据集还会占不少空间。
- 关键 Python 库:transformers、peft、accelerate、datasets、bitsandbytes、sentencepiece、safetensors。
这套组合我在多台机器上验证过,基本稳定。如果你用的是 24GB 显存但显存经常被其他进程占掉一部分,建议关掉无关任务再开始,因为余量实在太薄了。
2. 15分钟环境准备实操
2.1 检查显卡驱动与CUDA
第一步不是装东西,是确认底子够不够。打开终端,先跑:
nvidia-smi看两件事:驱动版本,以及显存是否真的是 24GB 可用。常见问题是机器明明有 3090,但被其他任务占了 5GB 显存,这时候再跑训练大概率 OOM。
再确认 CUDA:
nvcc -V如果提示没有 nvcc 也不要慌。PyTorch 用的是自带 CUDA runtime,跟系统级 CUDA 是两套东西。系统 CUDA 主要影响你本地编译扩展,而跑 transformers 和 peft 并不需要你自己写 CUDA,所以只要nvidia-smi里的驱动版本不是太老(比如 470 以下),基本不影响后续操作。我自己那台机器系统 CUDA 一度是 11.8,PyTorch 用的 CUDA 12.1,完全没问题。
2.2 建一个干净的环境
我强烈建议用 conda 建独立环境,别把所有包堆在 base 里。大模型相关的包版本耦合非常紧,今天装 A 需要某版本,明天装 B 又把依赖升级了,很容易把环境搞坏。
conda create -n chatglm3 python=3.10 -y conda activate chatglm3环境名可以随便起,chatglm3 只是为了让人一眼看出这个是干嘛用的。这个操作不到一分钟,是环境准备里最值的一步。
2.3 安装核心依赖
重点来了,依赖别一股脑全装,版本要对。我整理了一份经过验证的依赖清单:
pip install torch==2.1.2+cu121 --index-url https://download.pytorch.org/whl/cu121 pip install transformers==4.37.2 peft==0.7.1 accelerate==0.26.1 \ datasets==2.16.1 bitsandbytes==0.41.3 sentencepiece==0.1.99 \ safetensors protobuftransformers 版本尽量别太老,PEFT 在 0.7 以上对 ChatGLM 这类模型的query_key_value目标层支持更稳。安装完成后立刻验证两件事:
python -c "import torch; print(torch.__version__, torch.cuda.is_available())" python -c "import transformers, peft, accelerate; print(transformers.__version__, peft.__version__, accelerate.__version__)"torch.cuda.is_available()如果输出 True,说明 PyTorch 正确识别到显卡了。版本打印正常,环境基本就支棱起来了。从创建 conda 环境到这一步,熟练的话 5~8 分钟能搞定,剩下时间主要花在下载依赖和模型权重上。
2.4 下载模型与显存监控
环境装好,接下来把模型权重搞下来。ChatGLM3-6B 的权重在 HF 和魔搭这类模型平台上都有托管,文件总大小约 12GB,是分片存储的 safetensors 文件。手动一个个点下载太蠢,直接用平台的下载工具或 git 命令拉全量仓库即可。
下载完成后,目录结构长这样:
chatglm3-6b/ ├── config.json ├── tokenizer.model ├── tokenization_chatglm.py ├── model-00001-of-00007.safetensors ├── ... ├── model-00007-of-00007.safetensors └── modeling_chatglm.py下载完别急着跑训练,先开一个显存监控窗口:
nvidia-smi -l 1这个命令每秒刷新一次显存占用。后面训练时,盯着这个窗口能直观看到显存峰值出现在哪个阶段。我第一次跑训练就是靠它发现了某个环节会突然把显存吃到 23GB,差点 OOM。
到这一步,模型就位、环境就绪,15 分钟内完成是现实的。如果下载速度慢,时间主要耗在网络和磁盘上。一个实用技巧:把下载好的模型目录做成软链接放到工作目录下,避免不同项目重复拷贝占磁盘。
3. LoRA微调完整实操
3.1 LoRA原理通俗解析
LoRA 的全称是 Low-Rank Adaptation,低秩适配。理解它就靠一句话:原始大模型权重 W 冻结不动,在旁边并联两个小矩阵 A 和 B,前向计算时新的输出等于原始输出加上 BA 的贡献,训练时只更新这两个小矩阵里的参数。
为什么有效?因为大模型在微调时,权重变化的本质往往是低秩的,不需要动完整的 6B 参数就能学会新任务。A 和 B 的维度通常远小于原来的权重矩阵,比如一个 4096×4096 的矩阵,LoRA rank 取 8 时,只需要两个 4096×8 的小矩阵,参数量直接砍掉 99.9%。你可以把它理解成压缩传文件:几百个G的数据,你只传一份很小的"差异包",接收方拿原始文件加差异包就能恢复出新版本。LoRA 就是这份差异包。
训练参数量虽然少,但微调效果在多数任务上跟全参微调十分接近,这就是它成为事实标准的原因。
3.2 造一份模型能直接吃的数据
数据格式是微调的第一个大坑。ChatGLM3 有自己的对话格式,最稳的做法是照官方微调数据的样子来,用conversations字段包装多轮对话。我这次做的是一个业务问答场景,造了 32 条训练样本、8 条验证样本,数据量不大,但足够演示完整流程。
[ { "conversations": [ { "role": "system", "content": "你是一个电商客服助手,只能基于给定的商品信息回答,不知道的内容要主动承认。" }, { "role": "user", "content": "这件衣服是什么材质的?" }, { "role": "assistant", "content": "这件衣服是纯棉材质,手感柔软,透气性很好。" } ] } ]官方的tokenizer在处理多轮对话时会给角色和内容拼接特殊 token,所以关键点是:数据里只写干净的角色和内容,不要自己手动加 BOS、EOS、换行之类的特殊符号。模型训练时,数据加载代码会负责把这段话编码成模型需要的 input_ids 和 labels。
加载和切分的代码很简单,用datasets库读 JSON 再划分:
from datasets import load_dataset, Dataset raw_dataset = load_dataset("json", data_files="data/train.json", split="train") raw_dataset = raw_dataset.train_test_split(test_size=0.2, seed=42) train_dataset = raw_dataset["train"] eval_dataset = raw_dataset["test"]3.3 加载模型与LoRA配置
模型加载用 transformers 的 AutoModel。这里两个关键选择:torch_dtype=torch.float16让整个模型以半精度驻留显存,直接省下一半空间;device_map="auto"让 accelerate 自动决定每层放在哪张卡、显存怎么分配。
from transformers import AutoModel, AutoTokenizer import torch model_path = "./chatglm3-6b" tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) model = AutoModel.from_pretrained( model_path, trust_remote_code=True, torch_dtype=torch.float16, device_map="auto", low_cpu_mem_usage=True )trust_remote_code=True是因为 ChatGLM 这类模型的自定义代码需要从仓库本地加载,不点开这个开关会报错。low_cpu_mem_usage能降低内存峰值,防止机器内存不足把进程挤爆。
然后定义 LoRA 配置:
from peft import LoraConfig, TaskType, get_peft_model lora_config = LoraConfig( task_type=TaskType.CAUSAL_LM, r=8, lora_alpha=32, lora_dropout=0.1, target_modules=["query_key_value"], ) model = get_peft_model(model, lora_config) model.print_trainable_parameters()target_modules=["query_key_value"]是 ChatGLM 系列特有的写法,它的注意力层把 Q、K、V 三个权重合并成了一个名为query_key_value的矩阵,所以 LoRA 要注入的目标就是它。r=8是低秩维度,经验值:简单任务 4~8,复杂任务 16~32。lora_alpha控制新注入分支的缩放比例,一般取 r 的 2 到 4 倍,用 32 是比较稳的配置。
打印出来的可训练参数量通常只有总参数量的百分之几甚至千分之几,这就是 LoRA 省显存的核心原因。
3.4 训练参数与开始训练
训练用 transformers 的 Trainer 封装。这套配置我是反复调整后确定的,兼顾显存和训练速度:
from transformers import Trainer, TrainingArguments training_args = TrainingArguments( output_dir="./output-lora", per_device_train_batch_size=1, per_device_eval_batch_size=1, gradient_accumulation_steps=8, learning_rate=1e-4, max_steps=100, logging_steps=10, save_steps=50, fp16=True, remove_unused_columns=False, report_to="none", save_total_limit=2, ) trainer = Trainer( model=model, args=training_args, train_dataset=train_dataset, eval_dataset=eval_dataset, tokenizer=tokenizer, ) trainer.train()每个参数背后都有原因:
per_device_train_batch_size=1是显存不够的无奈之选,但也是正确选择。24GB 卡跑 6B 模型,batch size 稍微调到 2,显存可能直接飙到 20GB 以上。gradient_accumulation_steps=8的意思是每 8 个小步的梯度累加后再更新一次权重,等效于 batch size 8,兼顾了训练质量和显存。fp16=True让训练过程中的梯度计算和优化器更新也用半精度,省显存也加速。max_steps=100是为了演示快速走通,真实任务建议把max_steps换掉,按num_train_epochs来跑。
我实测下来,100 步训练在 3090 上大约耗时 8~10 分钟。期间显存监控窗口显示的曲线大概是:加载模型后占 13GB 左右,数据进 batch 后爬升到 18GB,训练稳定后在 20~21GB 徘徊,峰值出现过一次 22.8GB。离 OOM 真的只有一步之遥。
3.5 合并保存与推理验证
训练完成后,LoRA 权重还只是两个小矩阵,可以直接保存为 adapter 文件,也可以合并回主模型。如果你希望之后加载模型时不去区分什么 adapter,直接像加载普通模型一样用,那就合并:
merged_model = model.merge_and_unload() merged_model.save_pretrained("./chatglm3-6b-lora-merged") tokenizer.save_pretrained("./chatglm3-6b-lora-merged")合并过程就是把原始权重和新训练的低秩矩阵按公式融合回完整的权重矩阵,这一步会额外占用显存和内存。合并完成后,推理验证:
model = AutoModel.from_pretrained( "./chatglm3-6b-lora-merged", trust_remote_code=True, torch_dtype=torch.float16, device_map="auto", ).eval() prompt = "这件衣服如果不喜欢可以退吗?" inputs = tokenizer.apply_chat_template( [{"role": "user", "content": prompt}], return_tensors="pt", return_dict=True, ).to("cuda") outputs = model.generate(**inputs, max_new_tokens=128, do_sample=False) print(tokenizer.decode(outputs[0], skip_special_tokens=True))注意apply_chat_template会在内部按 ChatGLM3 的格式拼好特殊 token。如果你输入的是纯文本、模板拼错,生成结果就可能出现前言不搭后语的情况,这是后面排查章节的重点。
4. P-TuningV2微调完整实操
4.1 P-TuningV2和LoRA到底差在哪
P-TuningV2 是另一条路线。它的思路是给模型的每一层 Transformer 前面插入一段可学习的"连续前缀"(prefix),这段前缀是虚拟 token 的嵌入向量,不是真实词。训练时只优化这些虚拟前缀参数,原始模型权重完全冻结。
和 LoRA 的最本质区别在于:LoRA 改的是权重空间,通过低秩矩阵影响模型内部计算;P-TuningV2 改的是输入空间,通过强制模型注意力机制"看到"额外的虚拟 token 来引导生成。这个区别决定了它们在不同任务上的表现倾向。P-TuningV2 更接近提示工程的上限替代方案,而 LoRA 对模型行为模式的改变通常更深。
还有一个实际差异:可训练参数量。P-TuningV2 的参数主要是前缀向量和一个重参数化 MLP,通常比 LoRA 还小。但它的效果非常依赖pre_seq_len的设置,也就是前缀长度。128 是官方常用的起步值,更长(256、512)效果可能更好,但显存和训练时间都会涨。ChatGLM 官方仓库对 P-TuningV2 有专门适配,所以它也是官方教程里最推荐的微调方案。
4.2 官方脚本与数据格式适配
P-TuningV2 我用的不是手写的 PEFT 代码,而是走了官方仓库配套的 finetune 脚本。这个选择是刻意的:官方脚本和模型内部实现深度绑定,处理前缀参数的方式比通用 PEFT 库更贴合。
官方脚本支持的数据格式跟我在 LoRA 用的 conversations 格式一致,所以上一步准备好的 train.json 和 dev.json 可以直接复用。唯一要确认的是字段名和结构,官方示例里 system、user、assistant 三个角色都是可选的,但我要提醒一句:如果是单轮问答,至少要有一条 user 和一条 assistant,不然模型没有学习目标。
训练命令长这样:
python finetune.py \ --train_file ./data/train.json \ --dev_file ./data/dev.json \ --output_dir ./output-ptuning \ --model_name_or_path ./chatglm3-6b \ --pre_seq_len 128 \ --num_train_epochs 2 \ --per_device_train_batch_size 1 \ --gradient_accumulation_steps 8 \ --max_source_seq_len 512 \ --max_target_seq_len 512 \ --learning_rate 2e-2 \ --logging_steps 10 \ --save_steps 50P-TuningV2 的学习率跟 LoRA 完全不是一个量级。LoRA 用 1e-4 是常规操作,P-TuningV2 因为只更新嵌入侧的参数,学习率经常要打到 2e-2 甚至更高才动得快。我第一次按 LoRA 的 1e-4 跑 P-TuningV2,训练了半天 loss 纹丝不动,后来换成官方建议的大学习率才正常。
4.3 启动训练与显存对比
启动训练后,显存占用跟 LoRA 有差别但不大。P-TuningV2 因为额外插入的前缀会让每一层的 KV cache 变大,所以同样是 batch size 1、序列长度 512,显存峰值也在 20GB 上下。我实测下来,P-TuningV2 的峰值通常比 LoRA 低 1~2GB,原因在于没有 LoRA 那套低秩矩阵在梯度回传时的额外临时量。
两种方案的关键对比我给你放一个表格:
| 对比维度 | LoRA | P-TuningV2 |
|---|---|---|
| 修改对象 | 模型权重矩阵的低秩分支 | 每层输入序列的虚拟前缀 |
| 可训练参数量 | 几百万到上千万 | 通常更小,百万级以内 |
| 典型学习率 | 1e-4 左右 | 2e-2 左右 |
| 显存占用(6B模型) | 约20~23GB | 约18~21GB |
| 保存格式 | PEFT adapter 或合并权重 | 前缀参数 + 模型配置 |
| 生态通用性 | 高度通用,任意 transformers 模型 | 依赖具体模型的官方适配 |
| 上手难度 | 低,PEFT 库一把梭 | 中,需按官方脚本走 |
如果你做的是通用模型适配,我建议优先 LoRA;如果是 ChatGLM 系列且想快速在官方链路里跑通,P-TuningV2 也完全可行。
4.4 权重保存与加载方式
P-TuningV2 训练完的保存方式跟 LoRA 有巨大差异,这是最容易栽跟头的地方。官方脚本输出的不是一个标准的 Peft adapter,而是一套前缀参数和模型配置。这意味着你不能简单拿PeftModel.from_pretrained去加载,而是要在加载原始 ChatGLM3-6B 模型的基础上,再把前缀参数恢复到模型内部。
官方仓库的推理脚本里已经封装好这套加载逻辑,它会:
- 用原始 ChatGLM3-6B 权重初始化模型。
- 从训练输出目录读取
pre_seq_len、prefix_projection等配置。 - 把保存的前缀参数灌进模型的
prefix_encoder。 - 以微调后状态执行对话生成。
所以使用 P-TuningV2 时,请确保把训练输出目录整体保留好,不要只拷权重文件出来。输出目录里那几份看起来不起眼的配置 json,和前缀参数是一对一的,丢了任何一个都恢复不出微调效果。这也是我后来更偏爱 LoRA 的原因之一:LoRA 合并后的模型是一个标准完整权重目录,走到哪都能直接用,心理负担小很多。
5. 踩坑日志与排查手册
5.1 显存OOM的一线排查
我在训练过程中遇到过一次比较惊险的 OOM,发生在第 87 步,报错就是经典的CUDA out of memory。那一次我把max_source_seq_len调到了 1024,结果显存直接爆了。排查 OOM,我的固定动作是三步:
- 先看
nvidia-smi确认是不是有其他进程抢占显存。 - 再开一个 Python 环境跑
torch.cuda.memory_summary(),看哪块显存占用最高。 - 最后按优先级把训练配置往回收缩。
回收的顺序是:per_device_train_batch_size先降为 1,gradient_accumulation_steps相应调大来补偿有效 batch size;然后把序列长度从 1024 缩到 512;再开model.gradient_checkpointing_enable(),用更多计算换更少显存;如果还不够,最后选择把模型 4bit 量化加载。
model.gradient_checkpointing_enable()这行代码能把激活显存从 5GB 级别压到 2GB 级别,但会让训练速度下降 20%~30%。OOM 边缘时,这是最有效的保命手段。
5.2 训练Loss长什么样才算正常
很多新手看到 loss 不是 0 就觉得模型没学好,其实大错特错。LoRA 微调 ChatGPT 类模型,正常状态是 loss 从 2~5 开始,前 20 步快速下降,后面进入平缓下降区。如果你训练 100 步,最终 loss 落在 0.5~1.5 都属于正常。
我整理过一份正常日志和异常日志对照:
| 场景 | 现象 | 判断 |
|---|---|---|
| 正常收敛 | 初始 3.2,20步后 1.5,100步后 0.8 | 正常,可以继续 |
| 学习率过大 | 初始就很大,loss 剧烈震荡不下降 | 降 lr 到 1e-5 或更小 |
| 学习率过小 | 50 步 loss 还在 4 以上,纹丝不动 | 升 lr,P-TuningV2 尤其要把 lr 拉大 |
| 过拟合 | 训练 loss 很低,验证 loss 反而升高 | 缩小训练轮数或加正则 |
| 数据有脏数据 | loss 突然跳到极高再回落 | 检查数据集是否有空 value 或格式错误 |
另外提醒一句:loss 曲线震荡不一定有问题,尤其 batch size 小的时候,每个 batch 的难度不同,loss 上下浮动正常。真正需要担心的是连降都不降。
5.3 模型加载卡死与下载失败
模型下载和加载阶段,最容易遇到两个问题:一是网络下载权重卡在某个文件上不动,二是加载时内存不足导致进程假死。
第一个问题的解决办法是换渠道。项目需要访问模型平台时,优先选国内可直接访问的平台,或者配置镜像环境变量,把默认下载源切到可用的镜像站点。下载中断时,很多下载工具支持断点续传,别从头再来。权重文件一次性拉全,别只拉分片的一部分,否则加载模型时永远卡在Missing key(s)。
第二个问题,加载 6B 模型时内存占用可能达到 15~20GB,如果机器内存只有 16GB,很危险。我踩过这个坑后,固定做法是加上low_cpu_mem_usage=True,并保证系统空余内存至少 20GB 以上。如果内存实在不够,先加载 4bit 量化版本,内存占用会大幅下降。
5.4 微调后模型胡言乱语怎么办
这个坑最隐蔽,症状是微调后模型输出完全失控,回答跟问题驴唇不对马嘴,甚至夹杂大量特殊符号。我查了很久,根因是数据拼接格式和模型模板不一致。
ChatGLM3 的对话模板对 system、user、assistant 的角色 token 有严格顺序要求。如果你在数据预处理阶段自己手动拼接了 prompt,比如随手加了一个"用户:"而不是标准 role 字符串,模型就会在推理阶段匹配不上它训练时见到的输入分布,输出自然跑偏。
解决办法是统一用官方 tokenizer 的模板处理函数做数据映射。原始 JSON 只保留结构化字段,让 tokenizer 负责拼特殊 token。并且训练前先做一次 sanity check:拿一条训练数据,用同一个 tokenizer 编码解码一遍,确认输入输出看起来像人话再开训。
6. 最后的实操心得
整套流程走完,我最深的体会是:在 24GB 单卡上微调 6B 模型,完全可行,但也确实是在走钢丝。显存余量太薄,任何一步操作失误都会让你回到 OOM 的起点。所以配置宁可保守,也不要激进;训练步数宁可短,也不要一口气跑几千步把时间和显存都耗光。
从方案选型上,我个人最终偏向 LoRA。不是因为 P-TuningV2 不好,而是 LoRA 的生态更通用,训练完可以合并回标准权重,后续部署、量化、服务化都很顺。P-TuningV2 在 ChatGLM 官方链路里非常成熟,尤其是你想尽量少引入第三方库的时候,它反而是最稳的选择。两条路线都不复杂,建议你先用 LoRA 跑通全流程,再切换到 P-TuningV2 对比效果,两者用同一份数据,也就多花半小时。
最后再分享一个小技巧。无论你用哪种方式微调,先拿 10~20 条数据把全流程跑通,确认 loss 在降、推理输出正常,再上完整数据集。这一步能在 15 分钟内暴露 90% 的环境和代码问题,是最划算的试错方式。我这个项目最后正式训练也是这么做的:先用少量数据验证,再拉长时间跑完整数据,整个过程没再翻过车。