☰
6GB显存双模型部署实战:LoRA、NaN与显存优化
2026/10/2 15:23:42 网站建设 项目流程

1. 6GB 显存跑两个决策模型,这事到底卡在哪

先把结论摆在前面:6GB 显存同时装两个决策模型,不是"能不能跑"的问题,而是"跑起来之后每一步都在跟显存抢地盘"的问题。Kev 和 Laya 这两个模型,一个偏结构化推理,一个偏对话式决策,单拎出来在 6GB 卡上都能勉强喘气,但两个一起塞进去,翻车几乎是必然的——区别只在于翻在哪一步。

我这次折腾的起点很朴素:手头只有一张 6GB 显存的卡,想同时挂两个决策模型做对比测试,一个负责逻辑推演,一个负责自然语言决策输出。听起来像是"两个小模型而已",实际动手才发现,显存这东西不是简单的加法。模型权重、KV Cache、中间激活值、框架自身的开销,四样东西叠在一起,6GB 根本不够分。

关键词里提到的 LoRA、NaN、peft,这三个词基本概括了我这一晚踩的坑:LoRA 微调后的权重合并方式直接影响显存占用,NaN 是训练和推理里最让人抓狂的报错,peft 则是整个加载流程的核心工具链。下面我把整个过程拆开讲,包括每一步为什么这么选、哪里翻了车、最后怎么绕过去的。

这篇文章适合谁看?如果你手上是 6GB 到 8GB 显存的卡,想跑多个模型或者做 LoRA 微调,又不想被各种"显存不足"的报错反复折磨,那这篇记录应该能帮你省下几个晚上。我不打算讲太多教科书式的原理,重点放在实际操作中那些文档里不会写的细节。

2. 两个决策模型同时加载的显存账本

2.1 模型权重只是显存开销的冰山一角

很多人算显存的时候只算模型权重,比如一个 3B 参数的模型,FP16 精度下大约占 6GB,于是觉得"6GB 卡刚好能装一个"。这个算法本身没错,但漏掉了后面三样东西。

模型权重加载进显存后,推理过程中还需要:

  • KV Cache:每生成一个 token,都要缓存 Key 和 Value 矩阵。序列越长,这部分占用越大。一个 3B 模型在 2048 上下文长度下,KV Cache 可能吃掉 1GB 到 2GB。
  • 中间激活值:前向传播过程中产生的临时张量,虽然会释放,但峰值占用不容忽视。
  • 框架开销:PyTorch 自身的 CUDA 上下文、cuDNN 的 workspace、通信缓冲区,这些加起来轻松占掉 500MB 到 1GB。

所以一个标称 6GB 的模型,实际跑起来可能需要 8GB 以上。6GB 卡想装两个,必须做量化或者用 LoRA 这种轻量适配方式。

2.2 量化精度与显存占用的换算关系

我把常见的精度和显存占用关系整理成了一张表,方便你直接对照自己的卡来估算:

精度格式每 1B 参数占用3B 模型权重7B 模型权重备注
FP324GB12GB28GB基本不用考虑
FP16/BF162GB6GB14GB推理常用,但 6GB 卡装 3B 就满了
INT81GB3GB7GB需要量化支持,精度损失小
INT40.5GB1.5GB3.5GB精度损失明显,但显存友好
NF4约 0.5GB1.5GB3.5GBpeft 里常用的量化格式

按这张表算,6GB 卡想同时装两个 3B 模型,至少得用 INT4 或 NF4 量化,权重部分占 3GB,剩下 3GB 留给 KV Cache 和框架开销。听起来可行,但实际跑起来 KV Cache 会随着对话轮次增长,很快就撑不住。

2.3 为什么我选择 LoRA 而不是全量微调

关键词里有 LoRA 和 peft,这里解释一下我的选择逻辑。全量微调一个 3B 模型,即使是用 INT4 量化加载,优化器状态和梯度也会把显存撑爆。LoRA 的思路是不动原始权重,只在旁边挂一对低秩矩阵,训练时只更新这对小矩阵。

这样做的好处很直接:

  • 可训练参数从几十亿降到几百万,显存占用大幅下降。
  • 原始权重可以量化后冻结,不参与梯度计算。
  • 训练完的 LoRA 权重文件很小,几十 MB 级别,方便切换。

但 LoRA 也有坑。最大的坑是合并方式:你可以选择训练时动态合并,也可以选择训练完再把 LoRA 权重合并回原模型。前者灵活但推理时多一层计算,后者推理快但合并过程可能引入精度问题。我这次两个模型用了不同的策略,后面会详细说。

3. Kev 和 Laya 的加载策略与翻车现场

3.1 Kev 模型:NF4 量化加 LoRA 的加载流程

Kev 这个模型我采用的是 NF4 量化加载加 LoRA 适配器的方案。核心代码逻辑是这样的:

from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig from peft import PeftModel import torch bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.bfloat16, bnb_4bit_use_double_quant=True, ) base_model = AutoModelForCausalLM.from_pretrained( "kev-base-model", quantization_config=bnb_config, device_map="auto", torch_dtype=torch.bfloat16, ) model = PeftModel.from_pretrained(base_model, "kev-lora-adapter")

这段代码看起来干净,但实际跑的时候第一个坑就来了:device_map="auto"在 6GB 卡上会把部分层卸载到 CPU,推理速度直接掉到没法用。我后来改成手动指定device_map={"": 0},强制全部放 GPU,配合 NF4 量化才勉强装下。

注意:bnb_4bit_use_double_quant=True这个选项会额外做一次量化,进一步压缩显存,但会增加一点计算开销。6GB 卡上建议开启,能省出几百 MB。

3.2 Laya 模型:INT8 量化下的 KV Cache 爆炸

Laya 模型我一开始用的是 INT8 量化,权重占 3GB 左右。单独跑没问题,但和 Kev 同时加载时,两个模型的 KV Cache 加起来直接把显存吃满。

这里有个细节值得说:KV Cache 的占用和 batch size、序列长度成正比。我当时的对话上下文设的是 4096,两个模型各占一份 KV Cache,峰值直接冲到 5GB 以上,加上权重和框架开销,6GB 根本兜不住。

解决办法有两个方向:

  • 降低上下文长度,从 4096 降到 2048,KV Cache 直接减半。
  • 用 PagedAttention 或者类似的显存管理机制,把 KV Cache 分页管理,减少碎片。

我最后选的是降上下文加动态卸载:不活跃的模型把 KV Cache 释放掉,需要时再重建。这个策略后面会详细讲。

3.3 两个模型共存时的显存分配实测数据

我把实测的显存占用整理成了表格,数据来自nvidia-smi的实时监控:

阶段Kev 占用Laya 占用框架开销总占用状态
仅加载 Kev2.8GB-0.6GB3.4GB正常
仅加载 Laya-3.2GB0.6GB3.8GB正常
两个同时加载2.8GB3.2GB0.8GB6.8GB溢出
量化优化后2.1GB2.4GB0.7GB5.2GB勉强
加 KV Cache 后2.1GB2.4GB0.7GB6.3GB再次溢出

这张表说明一个残酷的事实:即使权重压到 4.5GB,加上框架开销和 KV Cache,6GB 卡依然不够。最后我是靠动态释放 KV Cache 才把峰值压到 5.8GB 左右,留了 200MB 的余量。

4. NaN 报错的完整排查链路

4.1 NaN 出现的三个典型位置

NaN 这个报错,我在这一晚遇到了三次,每次位置都不一样:

  • 第一次:LoRA 训练时 loss 突然变成 NaN,出现在第 30 步左右。
  • 第二次:推理时输出全是 NaN,模型直接吐乱码。
  • 第三次:两个模型切换时,KV Cache 里出现 NaN,导致后续生成全部崩坏。

这三个位置对应三种不同的根因,不能一概而论。

4.2 训练阶段 NaN 的根因定位过程

训练阶段的 NaN,我一开始怀疑是学习率太大。把学习率从 2e-4 降到 1e-5,问题依然存在。然后我检查了数据,发现有几条样本的标签全是 padding token,导致 loss 计算时出现除零。

具体排查步骤是这样的:

  1. 在 loss 计算处加断言,检查是否有 inf 或 nan。
  2. 打印每个 batch 的标签分布,确认是否存在全 padding 的情况。
  3. 检查梯度裁剪是否生效,max_grad_norm设的是多少。

最后定位到两个问题:一是数据里确实有全 padding 样本,二是梯度裁剪的阈值设得太大,1.0 对于这个模型来说不够。改成 0.3 并过滤掉无效样本后,训练稳定了。

提示:LoRA 训练时,max_grad_norm建议设在 0.3 到 0.5 之间。太大容易 NaN,太小收敛慢。

4.3 推理阶段 NaN 与 KV Cache 污染的关系

推理阶段的 NaN 更隐蔽。我一开始以为是模型权重有问题,重新加载了一遍还是 NaN。后来用torch.isnan逐层检查,发现是 KV Cache 里混入了 NaN。

根因是:两个模型共享同一个 CUDA 流,切换时没有正确同步,导致一个模型的中间结果污染了另一个模型的 KV Cache。解决办法是给每个模型分配独立的 CUDA 流,切换时显式调用torch.cuda.synchronize()。

stream_kev = torch.cuda.Stream() stream_laya = torch.cuda.Stream() with torch.cuda.stream(stream_kev): kev_output = kev_model.generate(**kev_inputs) torch.cuda.synchronize() with torch.cuda.stream(stream_laya): laya_output = laya_model.generate(**laya_inputs)

这段代码看起来简单,但少了torch.cuda.synchronize()就会出现竞态条件,NaN 就是这么来的。

4.4 防止 NaN 的防御性编程习惯

踩了这三次坑之后,我养成了几个习惯:

  • 每次前向传播后检查输出是否有 NaN,有就立即中断并打印上下文。
  • 训练时用torch.autograd.set_detect_anomaly(True)定位异常梯度。
  • KV Cache 每次复用前做一次 NaN 检查,发现污染立即重建。
  • 保存 LoRA 权重时同时保存优化器状态,方便断点续训。

这些习惯看起来繁琐,但比起 NaN 出现后盲目排查,效率高得多。

5. 让两个模型在 6GB 卡上共存的实操方案

5.1 动态显存释放与模型切换机制

核心思路是:不要求两个模型同时驻留显存,而是按需加载、用完释放。具体实现上,我用了一个简单的模型管理器:

class ModelManager: def __init__(self): self.current_model = None self.current_name = None def switch_to(self, name, model): if self.current_name == name: return self.current_model if self.current_model is not None: del self.current_model torch.cuda.empty_cache() self.current_model = model self.current_name = name return model

这个方案的关键在于torch.cuda.empty_cache()的调用时机。必须在删除模型引用之后、加载新模型之前调用,否则显存碎片会导致新模型加载失败。

5.2 量化参数与上下文长度的联合调优

6GB 卡上,量化精度和上下文长度是一对矛盾。我的调优过程是这样的:

配置组合权重占用KV Cache总占用可用性
NF4 + 4096 上下文3.0GB2.5GB6.2GB溢出
NF4 + 2048 上下文3.0GB1.3GB5.0GB可用
INT8 + 2048 上下文4.5GB1.3GB6.5GB溢出
NF4 + 1024 上下文3.0GB0.7GB4.4GB流畅

最后我选的是 NF4 加 2048 上下文,留出约 1GB 的余量给框架和临时张量。这个配置下,两个模型切换的延迟在 2 秒左右,可以接受。

5.3 LoRA 权重合并与推理加速的取舍

LoRA 权重合并有两种方式,我两个模型用了不同策略:

  • Kev:训练完立即合并,推理时直接用合并后的模型,速度快但失去灵活性。
  • Laya:保持 LoRA 分离,推理时动态加载,灵活但每次多一层计算。

实测下来,合并后的 Kev 推理速度比分离状态快约 15%,但切换不同 LoRA 适配器时需要重新合并,耗时约 30 秒。Laya 的分离方式切换适配器只需 2 秒,但推理速度慢一些。

提示:如果你的场景需要频繁切换不同任务的 LoRA,保持分离更合适;如果固定一个任务,合并后推理更划算。

6. 这一晚踩过的坑与留下的经验

6.1 显存监控工具的正确使用方式

nvidia-smi是最常用的工具,但它有个问题:刷新频率低,容易错过峰值。我后来改用nvidia-smi dmon或者watch -n 0.1 nvidia-smi,能捕捉到更细粒度的显存变化。

另外,PyTorch 自带的torch.cuda.memory_summary()能看到更详细的信息,包括已分配、已缓存、碎片情况。排查显存问题时,这个比nvidia-smi更有用。

6.2 模型加载顺序对显存碎片的影响

加载顺序会影响显存碎片。我的经验是:先加载大的、后加载小的。因为大模型需要连续的大块显存,如果先加载小模型把显存打散,大模型可能加载失败。

具体到 Kev 和 Laya,Kev 的权重更大,所以先加载 Kev,再加载 Laya。反过来试过一次,Laya 加载后显存碎片化,Kev 的 NF4 量化层分配失败。

6.3 从这次翻车中提炼的可复用检查清单

最后整理一份检查清单,下次再遇到类似场景可以直接对照:

  • 加载前用torch.cuda.mem_get_info()确认可用显存。
  • 量化配置里bnb_4bit_use_double_quant建议开启。
  • device_map不要用auto,手动指定 GPU。
  • 训练时max_grad_norm设在 0.3 到 0.5。
  • 多模型切换时用独立 CUDA 流并同步。
  • KV Cache 复用前做 NaN 检查。
  • 加载顺序从大到小。
  • 上下文长度根据显存余量动态调整。

这套流程跑下来,6GB 卡同时挂两个决策模型虽然紧张,但确实能跑。代价是上下文长度受限、切换有延迟、需要精细的显存管理。如果你的卡是 8GB 以上,这些坑大部分可以绕过去。但如果你跟我一样只有 6GB,那这些细节就是必须掌握的生存技能。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询