显存被占满时怎么微调模型?CPU 训练的实测耗时与四个坑

0 阅读5分钟

给内部系统加一个「把口语化指令解析成结构化字段」的小功能,第一反应都是先租张卡。我那台机器上确实有卡,但被共享的推理服务长期占着——48GB 里只剩一两 GB 的空闲。

于是问题变成:不腾卡,还能不能把这个模型调出来?

先说结论:能。2B 以下的模型做 LoRA,全程 CPU 训练完全可行,代价只是慢(同任务同数据约 11 倍),精度基本不损失。但有几个坑,不提前知道会白跑一晚上。

一、先判断:这个任务适不适合 CPU 硬上

三个条件同时满足就可以走 CPU:

  1. 任务是窄任务——意图分类、字段抽取、文本归类这类,不是开放式生成;
  2. 数据量在几百条量级,只需要 LoRA 更新一小部分参数;
  3. 基座在 2B 以下。

反过来,如果要做全参微调、数据上万条、或者基座上了 7B,CPU 就不是「慢一点」的问题,是根本跑不完。别在这上面自我安慰。

二、实测:GPU 和 CPU 到底差多少

同一个任务、同一份 115 条数据、同一套 LoRA 配置(rank 8 / alpha 16 / lr 1e-4 / 5 epoch),只换 device 和配套精度:

配置耗时eval_loss效果
GPU bf161 分 10 秒基线—
CPU fp3213 分 3 秒与 GPU 差在小数点后三位一致

差了约 11 倍。但要注意这个倍数的前提:115 条、5 个 epoch。数据量涨到几百条,CPU 上就是小时级——我用 1.7B 跑 594 条,全程 CPU 是 3~5 小时。

结论:CPU 训练买到的是「不占卡」,付出的是时间,不是精度。

三、坑 1:CPU 不支持 bf16,精度必须跟着 device 一起切

自动选卡的脚本我一开始只切了设备:

# 错:只换成 cpu,精度还是 bf16,CPU 上直接报错
device = "cuda" if free_vram_ok() else "cpu"
model = AutoModelForCausalLM.from_pretrained(path, torch_dtype=torch.bfloat16).to(device)

x86 的 PyTorch 没有成熟的 bf16 kernel,走 CPU 必须用 fp32:

# 对:设备与精度成对切换
use_gpu = free_vram_ok(threshold_mb=6000)
device = "cuda" if use_gpu else "cpu"
dtype = torch.bfloat16 if use_gpu else torch.float32

阈值按模型大小给:0.6B 大约 46GB 空闲就够,1.5B 要 1016GB。用 LLaMA-Factory 时对应的是 bf16: true 和 fp32: true 两个开关,别只改一个——这也是「自动选卡」最容易漏的地方:它不只是换个 device 字符串。

四、坑 2:数据少的时候,大一号的模型反而更差

这条最反直觉,也是我最想写的。同样的 300 条数据、同一套 LoRA 配置,只换基座:

基座holdout 准确率
Qwen3-0.6B55%
Qwen3-1.7B45%
Qwen2.5-0.5B30%

1.7B 比 0.6B 少了 10 个点。把数据加到 594 条喂饱之后,1.7B 到 93.3%,0.6B 同样 93.3%——完全打平(这 93.3% 是另一轮、数据更多,绝对值和上面 300 条那组不可比,看的是「1.7B 追平了没有」)。

两层含义:

  • 「小数据下模型越大越好」不成立。参数多不等于这个窄任务上更容易学,反倒更容易在小样本上跑偏;
  • 但也别急着把大模型判死:喂饱之后它能追平,只是多花的时间和显存没换来额外收益。

所以选基座别靠 benchmark 猜,用同一份数据、同一套配置、同一批测试句实测准确率。这一步花两小时,比后面返工一周便宜。

附带一条:Qwen3 有 thinking 模式,做「结构化输出」这类任务必须把模板换成 qwen3_nothink,否则模型先长篇推理再吐 JSON,token 白烧还有可能把 JSON 撑坏。

五、坑 3:CPU 上量化部署,8bit 无损,bitsandbytes 的 4bit 会崩

训练完要挂成服务,模型太胖就得量化。下面是同一个 0.6B 模型、同一批测试句上三种精度的对比(这一组来自另一轮实验,87.5% 和坑 2 的 93.3% 不是同一批测试集,只看三种精度之间的相对差异):

精度内存单次推理准确率
fp322.24 GB15.4 s87.5%
8bit0.72 GB18.8 s87.5%
4bit0.51 GB20.4 s25%

8bit 省 3 倍内存、精度零损失,只慢 20% 左右——这条路在 CPU 上能走。4bit 直接崩到 25%:这里的 4bit 指 bitsandbytes 的 NF4(只量化权重),它本身以 CUDA 为目标,叠加双重量化之后,误差在小模型上被无比例地放大了。

别把上面这句读成「CPU 上 4bit 都不行」:CPU 部署更主流的路线是 llama.cpp 的 Q4_K_M GGUF,那条路完全可用,也是我后来实际部署时用的。这里的数据只说明 bitsandbytes 这条路在 CPU 上不可用。

另外提醒一句:量化不省时间。CPU 上没有量化加速的 kernel,8bit 只是省内存,推理甚至更慢一点。

六、坑 4:权重没下全,报错却报在 tokenizer 上

国内拉基座一般走 ModelScope。git clone 下来看着是完整的,其实大文件全是 LFS 指针(safetensors 可能只有 134 字节)。

真实报错不是「模型损坏」,而是训练启动时 JSONDecodeError: Expecting value——因为 tokenizer.json 也是 LFS 大文件(约 11MB),漏下了就是一段指针文本。

两个动作:

# 拉完先查是不是 LFS 指针
head -c 200 model.safetensors | grep git-lfs

# 命中就是指针,用 resolve 接口单独拉真实文件
curl -L -o model.safetensors "https://www.modelscope.cn/models/<org>/<model>/resolve/master/model.safetensors"

多分片的模型还要把 model-00001-of-00002.safetensors、model-00002-of-00002.safetensors 和 model.safetensors.index.json 一起拉齐。

还有一个不起眼但会卡住的:llamafactory-cli 必须在 LLaMA-Factory 目录下运行。默认 dataset_dir 是相对路径 data/,换个目录启动就找不到数据集。

七、什么情况下才值得去腾卡

最后给自己一条判断线:

  • 数据几百条 + 基座 2B 以下 + LoRA → CPU 直接上,别去抢卡;
  • 要反复跑几十轮调参 → 借卡更划算,11 倍的差距乘上轮次就受不了;
  • 上 7B 或做全参 → 必须 GPU,别犹豫。

CPU 训练不是「没办法时的将就」。在窄任务上它是一个正常选项——真正要花的不是显存,是你愿不愿意等那几个小时。