2026-10-27 · 深度笔记

用云主机做 LoRA 微调:最小可用配置与跑通顺序

从选卡到跑通一次 LoRA 微调,我把踩过的顺序错误记下来了:显存怎么估、数据要准备成什么样、哪一步必须先跑通再往下走。

云主机AI推理微调GPU

想给一个 7B 模型做业务微调,第一件事不是打开控制台选卡,是确认我到底要微调什么。这件事我拖了两周才想清楚,导致中间租了三次不该租的机器。

先说结论,因为大部分人卡在这一步:

一张 24GB 显存的卡,量化到 4bit,跑 7B 的 LoRA,是可行的最小配置。 下面是我实际走通的顺序,以及每一步的判断依据。

先算显存,而不是先挑卡

LoRA 的显存账跟全参微调不是一个数量级,值得先讲明白。

全参微调要存权重、梯度、优化器状态,7B 大约 112GB 起。LoRA 只训练注入的低秩矩阵,原始权重冻结:

激活值为什么是大头?因为每层的中间结果都得留着等反向传播。序列长度从 512 提到 2048,激活值大致是四倍关系。梯度检查点(gradient checkpointing)能砍掉大半,代价是慢 20% 到 30%。

我的实际数字:24GB 卡,序列长度 1024,per-device batch 1,梯度累积 8,跑 QLoRA 占 21GB 左右。换成 48GB 卡只是宽裕一点,训练速度差别很小——瓶颈早就不在显存而在算力了。

这里有个反直觉的结论:卡更大不等于训练更快。 我第一次租了 80GB 的卡,以为能开更大 batch 提速,结果数据只有两千条,batch 开大反而梯度噪声大、效果变差,每个 step 还被拉长。多出来的显存全成了闲置。

选卡:只看三件事

  1. 显存够不够 24GB。低于这个数,要么换更小的基座(1.5B / 3B),要么把序列长度压到 512。
  2. 卡上有没有一张空闲的、驱动版本对的。同一台物理机上如果已经有别的任务占着,选"整卡独占"也没用。
  3. 磁盘:权重 + 数据集 + checkpoint,一张 7B 的 4bit 量化权重不到 4GB,但加上原始权重要下载(十几 GB),磁盘留够 100GB 以上。

我这次用的是一张 24GB 的 A 系列卡,跑下来没有遇到带宽瓶颈,所以没有动力上更贵的卡。

数据:这一步最容易白干

我的第一个版本,数据是直接从业务库里导出的问答对,转成 JSON 之后就开始训。训完一测,格式全乱。

问题出在对话模板上。自回归模型的微调不是"给一对问答"就行的,输入得按基座模型自己的 chat 模板拼好,带上正确的角色标记和结束符。模板错了,模型学到的就是"另一种格式的问答",而评测脚本用的是另一种格式,看起来像模型没学会,其实是格式没对齐。

改对模板之后,同样的数据、同样步数,效果差别巨大。

数据量上,两千条结构化样本就够了。LoRA 的样本效率很高,数据质量比数量重要得多——清掉重复样本、把超长的截断到 1024 token 之后,效果比原始数据还略好。

我的数据文件长这样:

{"messages": [
  {"role": "system", "content": "你是一个技术支持助手"},
  {"role": "user", "content": "客户说连不上数据库"},
  {"role": "assistant", "content": "请确认数据库服务是否已启动..."}
]}

用 messages 而不是 instruction/input/output 这种自定义字段,是因为主流微调框架(PEFT / TRL / LLaMA-Factory)都原生认这个结构,少一层转换就少一层出错机会。

跑通顺序:先冒烟,再正式

我第一次直接上全量数据、全量步数跑,中途才发现显存不够,白等了两个小时。

现在的顺序固定:

第一步,冒烟测试。 用 20 条样本、跑 5 步、max_length 设 256。目的只有一个:确认环境、模板、显存不炸,几分钟就结束。

第二步,看 loss 曲线。 冒烟通过后跑 200 步,看 loss 是不是在下降。如果 loss 一开始就是平的甚至上升,不要往下走,八成是模板或数据格式问题,不是超参问题。

第三步,全量训练。

我的 LoRA 配置长这样:

# lora.yaml
model_name_or_path: <基座模型路径>
dataset_path: ./data/train.jsonl
output_dir: ./out

# 量化:4bit 是 24GB 卡能跑 7B 的前提
load_in_4bit: true
bnb_4bit_quant_type: nf4
bnb_4bit_compute_dtype: bfloat16
bnb_4bit_use_double_quant: true

# LoRA 本身
lora_r: 16
lora_alpha: 32
lora_dropout: 0.05
lora_target_modules: q_proj,k_proj,v_proj,o_proj,gate_proj,up_proj,down_proj

# 训练
per_device_train_batch_size: 1
gradient_accumulation_steps: 8
gradient_checkpointing: true
num_train_epochs: 3
learning_rate: 0.0002
lr_scheduler_type: cosine
warmup_ratio: 0.03
bf16: true
logging_steps: 10
save_steps: 100
save_total_limit: 3

几个参数的取值逻辑:

三个我真踩过的坑

bf16 不被支持。 老显卡不支持 bf16 时,训练会直接报"数值类型不支持"。我当时的解法是改成 fp16,但 fp16 更容易 loss 变成 NaN。后来加了一句判断:

import torch
if not torch.cuda.is_bf16_supported():
    # 老卡退回 fp16,并把 loss scale 调小
    pass

显存不够的报错出现在第一步而不是第几步。 我以为是跑久了碎片化,其实不是——是 save_steps 触发保存时,模型和优化器状态要额外占一份。把 save_total_limit 调小,或用 save_strategy: "epoch" 就能避开。

训练时别关掉日志。 有一次我为了省终端输出把 logging_steps 设成 100,结果 loss 曲线是空的,训完才发现第 30 步就 NaN 了,白跑一整晚。日志落盘到文件,不占屏幕。

训完之后

adapter 本身很小,几十到几百 MB,存对象存储或拉回本地都行。但基座模型必须在评测环境里也存在,只拷 adapter 跑不起来——这是每次给同事部署都要解释一遍的事。

我现在的做法是把基座模型、adapter、chat 模板三者打成一个目录,模板一起带上,避免版本错位:

# 合并后导出,推理端只认一个目录
merge_lora.py \
  --base <基座模型路径> \
  --adapter ./out \
  --output ./merged

# 别忘了把模板文件一起拷过去
cp chat_template.json ./merged/

然后在 24GB 卡上起推理服务验证:加载 merged 目录,跑几条业务里的真实问法,看是不是按格式回答。

收尾

跑通一次完整流程,机器时长大概三小时,真正训练不到四十分钟,其余都在下载、装环境、排查格式。工程时间比算力时间贵得多,这句话在微调上体现得特别明显。

最后照例提醒:卡型、显存和上面那些估算的关系是结构性的,可以放心照着估;但具体套餐给什么规格、单小时什么价位,各家调整得比模型版本还快,以官方页面为准。

接着读