skip to content
tlj 的工程笔记

为什么一张 24G 卡能微调 14B 大模型:把 QLoRA 的显存账算给你看

/ 10 min read

Table of Contents

我的合规质检模型是 Qwen2.5-14B,在一张 24GB 的 RTX 4090 上 QLoRA 微调出来的;drivegpt4 迷你复现是 Qwen2-VL-2B,显存只用了 7.28GB。每次说起这个都有人问:14B 全量微调动辄要上百 GB 显存,你一张消费级卡怎么塞下的?

答案是 QLoRA。但”QLoRA 省显存”这句话太笼统了——它到底省的是哪一块?这篇把账一笔笔算清楚。算清楚之后你会发现,LoRA 和量化省的根本不是同一块显存,它俩是两招叠加。

一、先看全量微调的钱花在哪

训练时显存被四样东西吃掉:

  1. 模型参数本身
  2. 梯度(每个参数一份)
  3. 优化器状态(用 Adam 的话,每个参数还要存 momentum 和 variance 两份,而且通常是 fp32)
  4. 激活值(前向传播的中间结果,反向传播要用)

拿 14B 算笔账(混合精度训练):

  • 参数:14B × 2 字节(bf16)= 28 GB
  • 梯度:14B × 2 字节 = 28 GB
  • Adam 优化器状态:momentum + variance,各 fp32,14B ×(4+4)字节 = 112 GB
  • (还有 fp32 的参数主副本、激活值……)

光前三项就 168 GB,激活还没算。所以全量微调 14B,你需要的是好几张 A100,不是一张 4090。

注意看最大的那块——优化器状态 112GB,比参数本身还大 4 倍。 这是全量微调真正的显存杀手。记住这一点,因为 LoRA 干掉的就是它。

二、LoRA 第一招:别训整个矩阵,只训一个低秩增量

LoRA 的核心想法:微调时不去更新原始权重矩阵 W,而是冻结它,另外学一个低秩的增量 ΔW = B·A,最后用 W + B·A。其中 A 是 d×r、B 是 r×k,r(秩)取得很小,比如 8。

把账算给你看。一个 4096×4096 的权重矩阵:

  • 全量微调要训:4096 × 4096 = 16,777,216 个参数
  • LoRA(r=8) 只训:r ×(d + k)= 8 ×(4096 + 4096)= 65,536 个参数
  • 比值:16,777,216 ÷ 65,536 = 整整 256 倍

可训练参数砍到 1/256。而第一节说过,梯度和优化器状态是跟着”可训练参数”走的——你冻结了 base,那 112GB 的优化器状态瞬间塌缩到只跟着这 6.5 万个参数算,几乎可以忽略。这就是 LoRA 省显存的本质:它把那块最大的优化器状态开销,从”跟着 140 亿参数”变成”跟着几百万参数”。

两个值得知道的细节:

  • 为什么 B 初始化为 0:训练一开始 ΔW = B·A = 0(因为 B 全是 0),所以模型起点和原始预训练权重完全一致,从一个已知的好状态平滑出发,不会一上来就把预训练知识打乱。
  • 为什么这招 work:背后是”低内在秩假设”——大模型适配到一个具体任务时,权重需要更新的”方向”其实很少,一个低秩矩阵就够表达,不需要动用整个满秩矩阵的自由度。
  • alpha / rank 的取舍:LoRA 的缩放系数 alpha 一般取 rank 的 1-2 倍(如 rank=16/alpha=32)。太大训练不稳,太小没效果;改了 alpha,学习率也要跟着调。这是经验值,不是理论最优。

三、但 LoRA 没解决一件事

LoRA 把梯度和优化器状态压下去了,可是第一节那 28GB 的参数本身还在——你还是得把整个 14B 的 base 模型加载进显存才能跑前向。14B × 2 字节 = 28GB,已经超过 24GB 的 4090 了。

所以纯 LoRA 微调 14B,一张 4090 还是塞不下。差的就是这块 base 加载的显存。

四、QLoRA 第二招:把冻结的 base 压成 4-bit

QLoRA 的 Q 是 Quantization(量化)。它的洞察很直接:那个 base 反正被冻结了、不参与梯度更新,何必用高精度存它?

于是把冻结的 base 量化到 4-bit

  • 原来:14B × 2 字节(bf16)= 28 GB
  • 量化后:14B × 0.5 字节(4-bit)= 7 GB

28GB 塌成 7GB,这下 24G 卡装得下了。而 LoRA 适配器(那几百万个可训练参数)仍然用高精度训练,精度损失被限制在”反正不更新”的 base 上。

QLoRA 具体靠三个技术点把这件事做干净:

  1. NF4(4-bit NormalFloat):一种专门为”正态分布的神经网络权重”设计的 4-bit 数据类型,比普通的 int4 量化误差更小(神经网络权重大致服从正态分布,NF4 把量化格子按正态分布密度来切)。
  2. 双重量化(Double Quantization):连量化用的那些缩放常数本身也再量化一遍,进一步省一点。
  3. 分页优化器(Paged Optimizer):显存峰值要爆时,把优化器状态临时挪到内存,避免 OOM 直接崩。

所以 LoRA vs QLoRA 的区别,一句话:QLoRA 量化的是 base 的加载显存(第三节那块 28GB),LoRA 不量化 base。 两招省的是不同的钱——LoRA 省优化器/梯度,QLoRA 在 LoRA 基础上再省 base 加载。叠起来,14B 才能进 24G 卡。

五、一个很真实的硬件坑:T4 为什么不支持 bf16

顺带讲一个我踩过、也很能体现”显存/精度要看硬件”的细节。

bf16(bfloat16)这种数据类型,需要 Ampere 架构及以上的 GPU 才硬件支持——也就是 A100、RTX 30 系、40 系这些。而 T4 是更老的 Turing 架构,它只有 fp16,没有 bf16。

这不是软件能绕的——是芯片的 Tensor Core 物理上不支持。所以如果你在 T4 上跑训练脚本,把 torch_dtype 写成 bf16,要么报错要么悄悄回退。bf16 和 fp16 都是 16 位,但 bf16 牺牲精度换更大的动态范围(指数位更多),训练更不容易溢出——而这个好处,老卡享受不到。选卡的时候这是个真实约束。

六、再往上一层:那 ZeRO 呢

可能有人会问:多卡训练不是有 DeepSpeed ZeRO 切分优化器状态、梯度、参数吗(stage 1/2/3,越高切得越多越省、但通信越慢),为什么不用它省显存?

我在8 卡分布式微调那篇里踩过这个坑:QLoRA 和 ZeRO 基本不兼容。原因正是这篇讲的——QLoRA 的可训练参数(LoRA 部分)已经极小,ZeRO 要切分的那些大头(优化器状态、梯度)本来就被 LoRA 干掉了,没什么可切的;而它真正的大头是那个量化的、不可训练的 base,ZeRO 的参数切分机制又和 4-bit 量化层对不上。所以多卡 QLoRA 走的是 DDP(每张卡放一份完整模型、各算各的梯度再同步),不是 ZeRO。

小结

把这本账记住,你就不会再被”省显存”这种笼统说法绕晕了——每个技术省的是账本上不同的那一栏:

技术省的是哪一块
LoRA优化器状态 + 梯度(从跟着 140 亿参数变成跟着几百万)
QLoRA 的量化base 模型的加载显存(28GB → 7GB)
ZeRO把优化器状态/梯度/参数切到多张卡上(但和 QLoRA 冲突)
选 bf16 而非 fp16不省显存,省的是”训练不溢出”,且要 Ampere 及以上

理解了这张账,再回头看”14B 怎么塞进 24G 卡”就一点都不神奇了:LoRA 干掉最大的优化器状态,量化把 base 压扁,剩下的就装得下了。我那几个微调项目能在消费级卡上跑起来,靠的全是这本账。