1. 为什么单卡微调值得认真对待
1.1 从一张显卡说起
手里只有一张卡,还想把大模型微调跑起来,这是很多个人开发者和中小团队最真实的处境。昇思 MindSpore 作为国产深度学习框架,在大模型训练和推理这条链路上已经打磨得相当完整,但网上能找到的教程要么停留在“Hello World”级别的模型加载,要么直接甩出一个需要八卡 A100 的脚本,中间那段“单卡怎么把微调跑通、跑稳、跑出可用结果”的空白,恰恰是大多数人卡住的地方。
这篇内容就是来填这段空白的。我会从环境搭建开始,一路讲到数据准备、微调脚本编写、训练过程监控、权重合并,再到最后的推理验证,把整条链路拆开揉碎。目标很明确:你跟着走一遍,就能在自己的单卡机器上完成一次完整的大模型微调与推理闭环。不管你是刚接触 MindSpore 的新手,还是从 PyTorch 转过来的老手,都能从中找到可以直接抄作业的部分。
需要提前说明的是,单卡微调的核心矛盾在于显存。一张 24GB 显存的卡,想微调 7B 参数的模型,全量微调基本不用想,必须走参数高效微调(PEFT)路线,比如 LoRA 或者 QLoRA。MindSpore 生态里对应的方案是MindPet(MindSpore Parameter-Efficient Tuning),它提供了 LoRA、DoRA 等微调算法的实现,配合 MindSpore 的图算融合和内存复用机制,单卡跑 7B 模型的 LoRA 微调是完全可行的。下面所有内容都围绕这条技术路线展开。
1.2 单卡微调能解决什么实际问题
很多人会问,单卡微调出来的模型到底能干嘛。我举几个我实际遇到过的场景。第一个是领域术语适配,比如你手头有一批医疗问诊对话数据,通用大模型对某些专科术语的回答总是差那么点意思,用几百到几千条高质量样本做一轮 LoRA 微调,模型在特定术语上的准确率会有肉眼可见的提升。第二个是输出格式约束,你希望模型严格按照某个 JSON 结构输出,或者在客服场景里固定话术风格,微调比写一堆提示词工程要稳定得多。第三个是私有数据注入,有些知识不适合放在提示词里反复传,通过微调让模型“记住”这些内容,推理时就不用每次都塞长上下文。
这些场景的共同点是:数据量不大、任务边界清晰、对基座模型的通用能力依赖不强。这正是单卡微调最擅长的战场。反过来,如果你要做的是从零训练一个通用大模型,或者需要模型掌握大量新知识,那单卡微调就不合适了,该上集群就上集群,别硬撑。
1.3 整体流程鸟瞰
在动手之前,先把整条链路在脑子里过一遍,这样后面每一步你都知道自己在哪个位置。整个流程大致分六个阶段:环境准备(装 MindSpore、MindPet、依赖库)、模型与数据准备(下载基座权重、处理数据集)、微调配置(写训练脚本、设超参)、训练执行(启动训练、监控 loss)、权重合并(把 LoRA 权重合并回基座)、推理验证(加载合并后的模型做生成测试)。
这六个阶段里,最容易出问题的是环境准备和权重合并。环境准备涉及框架版本、CUDA 版本、驱动版本的匹配,错一个就报一堆看不懂的错。权重合并则是很多人容易忽略的一步,以为训练完就能直接用,结果推理时发现加载的是基座模型,微调效果完全没体现。后面我会把这两个环节单独拎出来重点讲。
2. 环境搭建与依赖版本锁定
2.1 硬件与系统基线
先说你需要的硬件底线。单卡微调 7B 模型,显存建议不低于 24GB(比如 RTX 3090、4090、A10 等)。如果只有 16GB,也不是完全没戏,但需要把 batch size 压到 1,序列长度控制在 512 以内,并且开启梯度检查点,代价是训练速度会慢不少。8B 到 13B 的模型在 24GB 卡上做 LoRA 微调,序列长度 1024、batch size 2 到 4 是比较舒服的配置。再大的模型,单卡就比较吃力了,除非用 4-bit 量化加载。
系统层面,Ubuntu 20.04 或 22.04 是最省心的选择,内核版本不要太新也不要太旧。Windows 下用 WSL2 也能跑,但涉及到多进程数据加载和显存管理时,WSL2 偶尔会有一些玄学问题,生产环境还是建议纯 Linux。驱动版本要和 CUDA 版本匹配,这个后面细说。
2.2 MindSpore 安装的版本选择逻辑
MindSpore 的安装是整条链路里第一个大坑。它的版本和 CUDA 版本、Python 版本之间有严格的对应关系,不是随便 pip install 就能搞定的。我的建议是:先确定 CUDA 版本,再反查 MindSpore 版本,最后锁定 Python 版本。
截至我写这篇内容时,MindSpore 2.3.x 和 2.4.x 是相对稳定的两个大版本。2.3.x 对 CUDA 11.6 和 11.8 支持较好,2.4.x 开始对 CUDA 12.x 有更完善的支持。如果你用的是 40 系显卡,建议走 CUDA 12.x 加 MindSpore 2.4.x 的组合;30 系显卡用 CUDA 11.8 加 MindSpore 2.3.x 也很稳。
安装命令不要直接抄网上的,去 MindSpore 官网的安装页面,根据你的系统、CUDA 版本、Python 版本生成对应的 pip 命令。这里给一个参考示例,CUDA 11.8、Python 3.9 的环境:
pip install mindspore==2.3.0 -i https://pypi.tuna.tsinghua.edu.cn/simple注意,如果你需要 GPU 版本,包名可能是mindspore-gpu或者通过官方源安装,具体以官网生成的命令为准。装完之后一定要验证:
import mindspore as ms print(ms.__version__) print(ms.context.get_context("device_target"))如果输出是GPU而不是CPU,说明 GPU 版本装对了。如果显示 CPU,要么是装成了 CPU 版本,要么是 CUDA 环境没配好。
2.3 MindPet 与配套库
MindPet 是 MindSpore 生态里做参数高效微调的核心库。安装方式通常是源码安装或者通过 MindSpore 的扩展包:
pip install mindpet如果 pip 源里找不到,就去官方仓库拉源码,python setup.py install装。装完之后验证 LoRA 相关模块能不能正常导入:
from mindpet.delta import LoRADelta print("MindPet LoRA ready")除了 MindPet,还需要几个配套库:transformers用于 tokenizer 和部分模型结构参考(注意 MindSpore 有自己的模型实现,transformers 主要用来处理分词器),datasets用于数据加载,numpy、tqdm这些基础库不用多说。版本上,transformers 建议用 4.30 以上的版本,太老的版本对某些新模型的分词器支持不好。
注意:MindSpore 和 PyTorch 不要装在同一个虚拟环境里,两者对底层库的依赖有冲突,混装容易出现段错误。用 conda 或 venv 建独立环境,这是血泪教训。
2.4 环境验证清单
装完别急着跑训练,先做一轮完整验证。我整理了一个检查清单,逐项过一遍能省掉后面大量排查时间:
| 检查项 | 验证方法 | 预期结果 |
|---|---|---|
| MindSpore 版本 | ms.__version__ | 与安装目标一致 |
| 设备类型 | ms.context.get_context("device_target") | GPU |
| CUDA 可用性 | nvidia-smi | 显示显卡和驱动信息 |
| MindPet 导入 | from mindpet.delta import LoRADelta | 无报错 |
| 显存识别 | ms.hal.get_device_properties() | 显存容量正确 |
| 分词器加载 | 加载目标模型 tokenizer | 无报错,词表大小正确 |
这一轮验证做完,环境基本就稳了。如果哪一项不对,先解决再往下走,不要带着问题往下跑,否则后面报的错会让你怀疑人生。
3. 模型与数据的准备细节
3.1 基座模型的选择与下载
单卡微调,基座模型的选择直接决定了你能不能跑起来。7B 是目前单卡最舒服的尺寸,再大就要掂量显存了。选模型时看三个维度:参数量、词表大小、是否已有 MindSpore 实现。
参数量决定了显存占用的大头。7B 模型用 fp16 加载大约占 14GB 显存,加上优化器状态、梯度、激活值,LoRA 微调时总占用在 18 到 22GB 之间,24GB 卡刚好够用。词表大小影响 embedding 层的显存,有些模型词表特别大(比如超过 15 万),embedding 层就能吃掉好几个 G。是否已有 MindSpore 实现则决定了你要不要自己做权重转换,能用官方或社区已经转换好的权重,就别自己折腾。
下载权重时,注意区分 fp16 和 fp32 版本。单卡微调一律用 fp16 或 bf16,fp32 显存直接翻倍,没必要。下载完检查一下权重文件的完整性,有些分片下载容易缺文件,加载时报的错往往很隐晦。
3.2 数据格式与预处理
微调数据的质量比数量重要得多。我见过太多人拿几万条脏数据去微调,结果模型学了一堆噪声,还不如不微调。数据准备这一步,核心是三件事:格式统一、长度控制、质量过滤。
格式上,MindSpore 微调通常用 JSONL 或者 MindRecord 格式。JSONL 更通用,每行一个样本,结构类似:
{"instruction": "请判断以下文本的情感倾向", "input": "这家餐厅的服务态度很好", "output": "正面"}预处理时要做几件事。第一,把数据统一成模型能理解的 prompt 模板,比如### Instruction: ... ### Input: ... ### Output: ...,模板要和推理时保持一致,否则微调效果会打折扣。第二,控制序列长度,超过最大长度的样本要么截断要么丢弃,截断时注意别把 output 部分截没了。第三,过滤掉空样本、超短样本、乱码样本。
长度分布这个事值得单独说。我建议在预处理阶段统计一下样本长度分布,如果大部分样本在 256 以内,那序列长度设 512 就够了,没必要设 2048 浪费显存。如果长度分布很分散,可以设一个覆盖 95% 样本的长度值,剩下的长样本截断处理。
3.3 数据加载与批处理
MindSpore 的数据加载用GeneratorDataset或者MindDataset。JSONL 数据一般用GeneratorDataset配合自定义的生成器函数。这里有个细节:数据加载的并行度。单卡训练时,num_parallel_workers设成 4 到 8 比较合适,设太高反而会因为 CPU 抢占影响训练。
批处理时要注意 padding 策略。同一个 batch 内的样本要 pad 到相同长度,pad 的 token 不参与 loss 计算。MindSpore 里可以通过 attention mask 来实现,mask 掉的位置在计算 loss 时权重设为零。这个细节如果处理不好,模型会学到一堆 pad token 的模式,生成时容易出问题。
def create_dataset(data_path, batch_size=4, max_length=512): def generator(): with open(data_path, 'r', encoding='utf-8') as f: for line in f: sample = json.loads(line) yield tokenize(sample, max_length) dataset = GeneratorDataset(generator, column_names=["input_ids", "attention_mask", "labels"]) dataset = dataset.batch(batch_size, drop_remainder=True) return dataset这段代码是示意,实际使用时 tokenize 函数要根据你选的模型分词器来写,返回的 tensor 类型也要和 MindSpore 的要求对齐。
4. 微调脚本的核心配置与实现
4.1 LoRA 参数怎么设才合理
LoRA 的核心参数有三个:rank(秩)、alpha(缩放系数)、dropout。这三个参数怎么设,直接决定微调效果和显存占用。
rank 决定了低秩矩阵的维度,rank 越大,可训练参数越多,拟合能力越强,但显存占用和过拟合风险也越高。我的经验是:简单任务(格式约束、风格迁移)用 rank 8 到 16,复杂任务(领域知识注入、多轮对话)用 rank 32 到 64。7B 模型单卡微调,rank 设 32 是个比较稳妥的起点。
alpha 是缩放系数,通常设成 rank 的两倍。比如 rank 32,alpha 设 64。这个比例不是绝对的,但作为起点很合适。alpha 的作用是控制 LoRA 更新量对原始权重的影响程度,设太小微调效果不明显,设太大容易破坏基座模型的原有能力。
dropout 一般设 0.05 到 0.1,作用是防止过拟合。数据量少的时候(几百条)可以设高一点,数据量大的时候(几千条以上)可以设低一点甚至设 0。
from mindpet.delta import LoRADelta lora_config = { "rank": 32, "alpha": 64, "dropout": 0.05, "target_modules": ["q_proj", "v_proj", "k_proj", "o_proj"] }target_modules指定 LoRA 加在哪些层上。一般加在 attention 的 q、k、v、o 四个投影层上就够了。如果效果不够,可以再加 FFN 层的 up、down、gate 投影,但参数量和显存占用会上去。
4.2 学习率与优化器选择
学习率是微调里最敏感的超参。LoRA 微调的学习率通常比全量微调大一个数量级,因为可训练参数少,需要更大的步长才能有效更新。我的经验区间是1e-4 到 5e-4,rank 越大学习率可以适当调小,rank 越小学习率可以适当调大。
优化器用 AdamW 就行,MindSpore 里有现成实现。weight decay 设 0.01 到 0.1,这个参数对 LoRA 的影响没有全量微调那么大,但也不能完全不管。学习率调度用 cosine 或者 linear warmup 加 decay,warmup 步数设总步数的 3% 到 5%。
from mindspore.nn import AdamW from mindspore.nn import CosineDecayLR lr = CosineDecayLR( min_lr=1e-6, max_lr=2e-4, decay_steps=total_steps, warmup_steps=int(total_steps * 0.05) ) optimizer = AdamW(params=trainable_params, learning_rate=lr, weight_decay=0.01)这里有个容易踩的坑:只把 LoRA 参数传给优化器,不要传全部参数。如果你不小心把基座模型的参数也传进去了,优化器会为这些参数分配状态,显存直接爆掉。训练前打印一下可训练参数的数量和名字,确认只有 LoRA 相关的参数在里面。
4.3 梯度累积与显存优化
单卡微调,显存永远是紧巴巴的。除了 LoRA 本身省显存,还有几个技巧可以用。
梯度累积是最常用的。如果你想要等效 batch size 16,但显存只够跑 batch size 4,那就累积 4 步再更新一次参数。MindSpore 里可以通过TrainOneStepCell的变体或者手动控制来实现。梯度累积的代价是训练速度变慢,因为前向反向的计算量没变,只是更新频率降低了。
梯度检查点是另一个大招。它用计算换显存,把中间激活值不保存,反向传播时重新计算。开启后显存能省 30% 到 50%,但训练速度会慢 20% 到 30%。MindSpore 里可以通过model.recompute()或者配置recompute_config来开启。
混合精度也是标配。fp16 或 bf16 训练,显存占用比 fp32 少一半,速度还更快。MindSpore 的amp模块可以自动做混合精度,注意 loss scaling 要配好,否则容易梯度下溢。
实操心得:这几个优化手段不要一次性全开。先开混合精度,不够再开梯度累积,还不够再开梯度检查点。每开一个都测一下训练速度和显存占用,找到最适合你硬件的组合。
4.4 训练循环与 checkpoint 保存
MindSpore 的训练循环可以用model.train()高层 API,也可以自己写TrainOneStepCell做更细粒度的控制。单卡微调建议用高层 API,省事且不容易出错。
import mindspore as ms from mindspore.train import Model, CheckpointConfig, ModelCheckpoint, LossMonitor config_ck = CheckpointConfig( save_checkpoint_steps=500, keep_checkpoint_max=3 ) ckpt_callback = ModelCheckpoint( prefix="lora_finetune", directory="./checkpoints", config=config_ck ) loss_callback = LossMonitor(per_print_times=10) model = Model(network, loss_fn, optimizer) model.train(epochs, dataset, callbacks=[ckpt_callback, loss_callback])checkpoint 保存策略要注意:保存频率别太高,否则 IO 会成为瓶颈;保留数量别太多,否则磁盘很快就满了。LoRA 的 checkpoint 通常只有几十到几百 MB,比全量模型小得多,但也没必要每个 step 都存。我一般设 500 步存一次,保留最近 3 个。
5. 训练过程监控与问题排查
5.1 loss 曲线怎么看
训练启动后,第一件事是盯 loss。正常的 loss 曲线应该是先快速下降,然后逐渐平缓,最后在一个区间内小幅波动。如果 loss 一直不降,或者降着降着突然飙升,那就有问题。
loss 不降的常见原因有几个。学习率太小,模型几乎没更新;数据有问题,比如标签全是同一个值;LoRA 没挂上,实际训练的是空参数。排查方法:先打印可训练参数数量,确认 LoRA 生效;再把学习率调大一个数量级试试;最后检查数据,随机抽几条看看输入输出对不对。
loss 飙升通常是学习率太大或者梯度爆炸。解决办法是降低学习率,或者加梯度裁剪。MindSpore 里可以通过clip_grad或者GradientClipping来实现,阈值一般设 1.0 到 5.0。
loss 降到很低但生成效果很差,这是过拟合的典型表现。数据量少、训练轮数多、rank 设太大都可能导致过拟合。解决办法是减少训练轮数、降低 rank、增大 dropout,或者补充更多数据。
5.2 显存溢出(OOM)的排查路径
OOM 是单卡微调最常见的报错。排查路径我总结成一张表:
| 排查项 | 检查方法 | 解决手段 |
|---|---|---|
| batch size 过大 | 逐步调小测试 | 降到 1 或 2 |
| 序列长度过长 | 统计样本长度分布 | 截断到合理值 |
| 梯度累积未生效 | 检查更新逻辑 | 确认累积步数配置 |
| 优化器状态过多 | 打印可训练参数 | 只传 LoRA 参数 |
| 激活值未释放 | 开启梯度检查点 | 配置 recompute |
| 混合精度未开启 | 检查 amp 配置 | 开启 fp16/bf16 |
排查 OOM 时,用nvidia-smi实时看显存占用,配合ms.hal.get_device_properties()看总显存。如果显存占用在训练开始后迅速涨满然后报错,多半是 batch size 或序列长度的问题。如果显存占用缓慢增长最后报错,可能是内存泄漏,检查数据加载部分有没有累积不释放的对象。
5.3 训练速度慢的优化方向
单卡训练速度慢,先别急着换硬件,看看软件层面有没有优化空间。数据加载是第一个要看的,如果 GPU 利用率忽高忽低,说明数据加载跟不上,把num_parallel_workers调大,或者把数据预处理提前做好缓存。算子融合是第二个,MindSpore 的图算融合默认开启,但有些自定义算子可能没被融合,检查一下有没有可以合并的操作。通信开销在单卡上不是问题,但如果你用了分布式数据并行(即使只有一张卡),通信开销也会拖慢速度,单卡就老老实实用单卡模式。
还有一个容易被忽略的点:checkpoint 保存和日志打印的频率。保存太频繁会阻塞训练,日志打印太多也会影响性能。把保存间隔调大,日志打印间隔调大,训练速度会有明显改善。
6. 权重合并与推理验证
6.1 LoRA 权重合并的原理与操作
训练完成后,checkpoint 里保存的是 LoRA 的增量权重,不是完整的模型权重。推理时有两种选择:一是加载基座模型加 LoRA 权重,动态合并;二是把 LoRA 权重合并回基座,保存成一个完整的模型文件。第一种方式灵活,可以随时切换不同的 LoRA;第二种方式推理时更省显存,速度也略快。
合并的数学原理很简单:LoRA 把权重更新表示为W = W0 + BA,其中W0是原始权重,B和A是低秩矩阵。合并就是把BA加到W0上。MindPet 提供了合并的工具函数,也可以手动实现:
import mindspore as ms def merge_lora(base_ckpt, lora_ckpt, output_path): base_params = ms.load_checkpoint(base_ckpt) lora_params = ms.load_checkpoint(lora_ckpt) for name, param in lora_params.items(): if "lora_a" in name: base_name = name.replace("lora_a", "weight") # 计算 BA 并加到 base 上 # 具体实现依赖 MindPet 的命名规则 pass ms.save_checkpoint(base_params, output_path)实际合并时,命名规则的匹配是最容易出错的。MindPet 的 LoRA 参数命名有固定格式,合并前先打印一下 checkpoint 里的 key,确认命名对应关系。如果合并后推理效果和训练时不一致,多半是合并时权重对应错了。
6.2 推理脚本的编写要点
推理脚本比训练脚本简单,但有几个细节要注意。分词器要和训练时一致,包括 padding 策略、截断策略、特殊 token 的处理。生成参数要调好,max_new_tokens、temperature、top_p、repetition_penalty这几个参数直接影响生成质量。
import mindspore as ms from mindspore import Tensor def generate(model, tokenizer, prompt, max_new_tokens=256): inputs = tokenizer(prompt, return_tensors="ms", padding=True) input_ids = inputs["input_ids"] attention_mask = inputs["attention_mask"] outputs = model.generate( input_ids, attention_mask=attention_mask, max_new_tokens=max_new_tokens, temperature=0.7, top_p=0.9, repetition_penalty=1.1, do_sample=True ) return tokenizer.decode(outputs[0], skip_special_tokens=True)temperature控制随机性,设 0.7 到 1.0 比较合适,太低会死板,太高会胡言乱语。top_p设 0.9 左右,配合 temperature 使用。repetition_penalty设 1.1 到 1.2,防止模型复读。这些参数没有绝对的最优值,要根据你的任务和模型特点调。
6.3 效果验证与对比方法
推理跑通只是第一步,还要验证微调效果。我一般做三组对比:基座模型直接推理、基座加 LoRA 推理、合并后模型推理。三组结果应该一致(后两组),且明显优于第一组,否则说明微调没生效或者合并出了问题。
验证时准备一批测试样本,覆盖训练数据里出现过的模式和没出现过的模式。如果模型在训练模式上表现好,在新模式上表现差,说明过拟合了,需要调整训练策略。如果模型在所有模式上都表现平平,说明微调强度不够,可以增大 rank 或学习率。
还有一个实用的技巧:用训练集里的样本做推理测试。如果模型连训练集里的样本都答不对,那肯定是哪里出了问题,先排查这个再谈泛化。
7. 常见问题速查与避坑经验
7.1 环境类问题速查
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 导入 MindSpore 报错 | 版本与 CUDA 不匹配 | 按官网对应关系重装 |
| 设备类型显示 CPU | 装成了 CPU 版本 | 重装 GPU 版本 |
| MindPet 导入失败 | 依赖缺失或版本冲突 | 检查依赖,独立环境安装 |
| 分词器加载报错 | transformers 版本过老 | 升级到 4.30 以上 |
| 显存识别异常 | 驱动版本问题 | 更新驱动到匹配版本 |
环境问题排查的核心思路是隔离变量。新建一个干净环境,只装最小依赖,跑一个最小示例。如果最小示例能跑通,再逐步加依赖,加到哪个报错就是哪个的问题。
7.2 训练类问题速查
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| loss 不降 | 学习率太小或 LoRA 未生效 | 调大学习率,检查可训练参数 |
| loss 飙升 | 学习率太大或梯度爆炸 | 降低学习率,加梯度裁剪 |
| OOM | batch size 或序列长度过大 | 调小参数,开启梯度检查点 |
| 训练速度慢 | 数据加载瓶颈 | 调大并行度,缓存预处理结果 |
| 过拟合 | 数据少、rank 大、轮数多 | 降 rank,减轮数,加 dropout |
训练类问题的排查,日志是关键。把 loss、学习率、显存占用、每步耗时都打出来,问题往往一目了然。别嫌日志多,出问题的时候你会感谢自己打了这些日志。
7.3 推理类问题速查
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 推理结果和训练不一致 | 权重合并错误 | 检查命名对应关系 |
| 生成重复内容 | repetition_penalty 太低 | 调高到 1.1 以上 |
| 生成质量差 | temperature 或 top_p 不当 | 调整采样参数 |
| 推理速度慢 | 未合并权重或未开混合精度 | 合并权重,开启 fp16 |
| 输出格式不对 | prompt 模板不一致 | 训练和推理用同一模板 |
推理问题的核心是一致性。训练和推理的模板、分词、参数都要对齐,任何一处不一致都会导致效果打折。
7.4 几条踩过坑才懂的经验
第一条,别迷信默认参数。MindSpore 和 MindPet 的默认参数是通用场景下的保守值,不一定适合你的任务。学习率、rank、序列长度这些关键参数,一定要根据你的数据和硬件调。
第二条,小步快跑。先用少量数据、少量步数跑通全流程,确认每个环节都没问题,再上全量数据。我见过太多人一上来就跑全量,结果跑到一半报错,前面的时间全白费。
第三条,checkpoint 是你的救命稻草。训练过程中定期保存,出问题可以从最近的 checkpoint 恢复,不用从头再来。保存的时候把优化器状态也存上,恢复时才能无缝衔接。
第四条,记录实验。每次调整参数都记下来,包括改了什么、结果如何。微调是个实验驱动的过程,没有记录你很快就会忘记哪个配置效果好。
第五条,数据质量大于一切。再好的模型、再优的参数,喂进去脏数据也白搭。花在数据清洗上的时间,永远不亏。
8. 从单卡到可用的最后一公里
8.1 微调效果的评估维度
微调跑完,怎么判断效果好不好。我一般从四个维度评估:任务准确率(在测试集上的表现)、格式合规率(输出是否符合预期格式)、泛化能力(在未见过的样本上的表现)、基座能力保留(通用能力有没有明显下降)。
任务准确率是最直接的指标,准备一个标注好的测试集,跑一遍算准确率。格式合规率用规则匹配或者人工抽查,看输出结构对不对。泛化能力用训练时没见过的样本测,如果掉得厉害说明过拟合。基座能力保留用一些通用问题测,如果模型连常识问题都答不对了,说明微调把基座能力破坏了,需要降低学习率或减少训练轮数。
这四个维度要综合看,不能只看一个。任务准确率高但基座能力崩了,这个模型不可用;格式合规率高但泛化差,说明模型只是记住了训练集的格式,没真正学会任务。
8.2 迭代优化的方向
第一轮微调效果不理想是正常的,关键是怎么迭代。如果任务准确率不够,先看数据,是不是样本太少或者质量不高,补充数据往往比调参更有效。如果格式合规率不够,检查 prompt 模板,是不是模板设计得不够清晰,或者训练样本里的格式不够统一。如果泛化能力差,减少训练轮数,降低 rank,增大 dropout。如果基座能力下降,降低学习率,减少 LoRA 影响的层数。
迭代的时候一次只改一个变量,改完测效果,有效就保留,无效就回退。同时改多个变量,你根本不知道是哪个起了作用。
8.3 部署上线的注意事项
微调好的模型要上线,还有几件事要做。推理性能优化,合并权重、开混合精度、用推理引擎加速,这些都能提升吞吐降低延迟。输入输出校验,线上环境什么输入都可能遇到,做好异常处理和兜底策略。版本管理,基座模型版本、LoRA 版本、合并后的模型版本都要记录清楚,出问题能快速定位。监控告警,推理延迟、错误率、显存占用这些指标要实时监控,异常时及时告警。
单卡微调出来的模型,部署时通常也是单卡推理。如果并发量上来了,可以考虑用推理引擎做批处理,或者上多卡做负载均衡。但那是另一个话题了,单卡微调加单卡推理,对于大多数个人项目和小团队场景已经够用。
我个人在实际操作中的体会是,单卡微调这件事,门槛不在技术本身,而在细节。框架版本、参数配置、数据质量、权重合并,每一个环节都有坑,但每一个坑都有成熟的解法。把这篇内容里的流程走一遍,把速查表存下来,遇到问题按表排查,基本能覆盖 90% 的情况。剩下的 10%,靠的是耐心和记录,每次踩坑都记下来,下次就是你的经验了。