1. 两个决策模型塞进 6GB 显存这事到底难在哪先把背景交代清楚。Kev 和 Laya 是两个决策模型具体来源和用途我不展开你只需要知道它们的参数量级属于中小型模型单看权重文件不算离谱。但问题出在“决策模型”这四个字上——决策模型和普通的文本生成模型不一样它往往需要在推理时维护状态、做多步推演、甚至同时加载多个策略头。这就导致一个尴尬的局面权重本身可能只占 2GB 到 3GB但实际跑起来激活值、KV Cache、中间张量一叠加显存直接飙到 8GB 以上。我手里只有一张 6GB 显存的卡。不是 8GB不是 12GB就是实打实的 6GB。这个数字在 2024 年做模型部署说实话有点寒酸。但现实就是这样不是每个人都有 4090大量存量设备就是 6GB 到 8GB 的笔记本显卡或者老款桌面卡。所以这篇记录的核心就一件事在 6GB 显存的硬约束下把两个决策模型同时跑起来并且保证推理结果不崩。先说结论免得你看到一半发现方向不对。最终方案是Kev 用 4-bit 量化加载Laya 用 LoRA 适配器动态挂载两个模型共享一个基础权重池通过显存分时复用把峰值压到 5.6GB 左右。听起来简单但中间踩的坑包括但不限于NaN 损失爆炸、LoRA 权重加载后推理结果全乱、显存碎片导致第二次推理直接 OOM、以及最离谱的一次——模型输出了一串完全无关的乱码排查半天发现是量化校准集选错了。如果你也在低显存环境下折腾过模型部署或者正准备做 LoRA 微调但卡在显存不够这篇记录应该能帮你省下几个晚上。我会把每个决策背后的原因、具体的参数配置、以及翻车现场都写清楚你直接抄作业就行。2. 整体方案设计为什么选量化加 LoRA 而不是别的路2.1 显存账本6GB 到底能装多少东西先算一笔账。6GB 显存扣掉系统占用和显示输出实际可用大概在 5.5GB 到 5.7GB 之间。如果你用的是笔记本还得再扣掉一部分给核显或者系统保留实际能用的可能只有 5.2GB 左右。这个数字很关键因为后面所有决策都围绕这个天花板展开。一个 FP16 精度的模型参数量为 N显存占用大约是 2N 字节。也就是说一个 1B 参数的模型FP16 加载需要 2GB。Kev 和 Laya 加起来参数量大概在 3B 左右FP16 直接加载就是 6GB还没算激活值和 KV Cache直接爆。所以 FP16 这条路从一开始就走不通。那 INT8 呢INT8 量化后显存占用减半3B 参数大概 3GB。听起来有希望但 INT8 量化对决策模型不太友好。决策模型里有很多小数值的 logits 和概率分布INT8 的量化误差会把这些细微差异抹平导致决策边界偏移。我实测过 INT8 版本的 Kev在几个关键决策节点上输出概率和 FP16 版本差了 15% 以上这在实际使用中是不可接受的。所以最终选了 4-bit 量化。4-bit 的量化和反量化过程虽然也有精度损失但配合正确的校准集关键决策路径上的误差可以控制在 3% 以内。而且 4-bit 加载后3B 参数的显存占用降到 1.5GB 左右给激活值和 KV Cache 留出了充足空间。2.2 为什么是 LoRA 而不是全量微调LoRA 的核心思路是在原始权重旁边挂一对低秩矩阵训练时只更新这对小矩阵推理时把 LoRA 权重合并回原权重或者动态加载。对于显存受限的场景LoRA 有两个好处第一训练阶段显存占用极低因为不需要存储优化器状态和全量梯度第二推理阶段可以动态切换不同的 LoRA 适配器实现一个基础模型服务多个任务。但这里有个坑LoRA 的动态加载在推理时会引入额外的显存开销。如果你同时挂载多个 LoRA每个 LoRA 的权重虽然不大但累加起来也不容忽视。而且 LoRA 的加载和卸载如果管理不当会造成显存碎片最终导致明明总显存够用但就是分配不出来连续的大块内存。我的做法是Kev 和 Laya 共享同一个基础模型权重各自挂一个 LoRA 适配器。基础模型用 4-bit 量化加载LoRA 适配器用 FP16 精度单独存储。推理时根据请求动态切换 LoRA切换过程中显存峰值控制在 5.6GB 以内。2.3 方案对比几条路都试过只有这条走通了方案显存峰值推理精度切换速度是否可行FP16 双模型8GB无损快否INT8 双模型5.5GB损失较大快勉强但精度不够4-bit 双模型独立加载4.8GB轻微损失慢可行但切换慢4-bit 共享基础模型 LoRA5.6GB轻微损失中等最终方案CPU 卸载部分层4.2GB无损极慢否延迟不可接受表格里能看出来4-bit 共享基础模型加 LoRA 的方案在显存和精度之间取得了最好的平衡。CPU 卸载虽然显存占用最低但推理延迟从 200ms 飙升到 3s 以上完全没法用。INT8 双模型独立加载显存是够的但精度损失在决策任务上太致命。3. 核心细节解析量化、LoRA 和显存管理的实操要点3.1 4-bit 量化的校准集选择别用通用语料4-bit 量化最关键的参数是校准集。校准集的作用是让量化算法知道哪些数值范围是重要的从而在量化时保留这些范围的精度。如果你用通用语料做校准量化后的模型在通用任务上表现还行但在决策任务上会崩。我一开始就是吃了这个亏。用了一个通用的中文语料做校准量化后的 Kev 在简单决策上没问题但一到多步推演就开始输出 NaN。排查了很久才发现决策模型里的关键数值集中在某些特定的 logits 范围通用语料的分布和这个范围完全不匹配导致量化时把这些关键值直接压到了量化区间的边缘。后来我换成了从实际决策任务中采样的 512 条数据做校准NaN 问题直接消失。校准集不需要大但必须和实际任务分布一致。512 条足够覆盖主要的数值范围再多边际收益很低。注意校准集的数据格式必须和模型输入格式完全一致。如果你用的是对话格式校准集也要是对话格式如果你用的是纯文本格式校准集也要是纯文本。格式不一致会导致校准过程读取到错误的数值分布。3.2 LoRA 权重的加载顺序先合并还是后合并LoRA 权重有两种加载方式一种是推理前把 LoRA 权重合并到基础权重里另一种是推理时动态加载。合并的方式推理速度快但每次切换 LoRA 都需要重新合并而且合并后的权重会占用额外显存。动态加载的方式切换快但推理时每次前向传播都要计算 LoRA 的增量会稍微增加计算量。在 6GB 显存的约束下我选了动态加载。原因是合并方式虽然推理快但合并后的权重需要额外存储一份显存占用反而更高。动态加载虽然每次推理多了一点计算但显存占用更低而且切换 LoRA 只需要替换一个小矩阵速度很快。具体实现上我用了一个简单的缓存策略同时只保留一个活跃的 LoRA 适配器在显存里另一个放在内存中。当请求切换到另一个模型时把当前 LoRA 从显存卸载到内存再把目标 LoRA 从内存加载到显存。这个切换过程大概耗时 50ms 到 80ms对于非实时场景完全可以接受。3.3 显存碎片最隐蔽的杀手显存碎片这个问题我在前三个晚上都没意识到。现象是第一次推理正常第二次推理直接 OOM但nvidia-smi显示显存占用明明还有 1GB 多的空闲。这就是典型的显存碎片——空闲显存不是连续的分配不出足够大的块。产生碎片的原因很多最常见的是频繁创建和销毁张量。比如你在推理循环里每次都新建一个 KV Cache而不是复用之前的就会导致显存里到处都是小块空闲区域。另一个原因是LoRA 的动态加载和卸载每次加载都会申请一块显存卸载后释放但释放的块可能和周围的不连续。解决办法有两个第一预分配 KV Cache在推理开始前就分配好最大长度的 KV Cache避免动态增长第二使用显存池把 LoRA 权重的加载和卸载都通过一个预分配的显存池来管理避免频繁的 malloc 和 free。我用了第二个方案实现了一个简单的显存池预分配一块 512MB 的显存区域专门用来存放 LoRA 权重。加载时从池里取卸载时还回池里。这样虽然浪费了一点显存但彻底解决了碎片问题。4. 实操过程从零到跑通的完整步骤4.1 环境准备和依赖安装先列一下我用的环境你照着配就行操作系统Ubuntu 22.04显卡RTX 3060 Laptop6GB 显存CUDA12.1Python3.10核心依赖transformers4.36.0、peft0.7.0、bitsandbytes0.41.0、accelerate0.25.0安装命令pip install transformers4.36.0 peft0.7.0 bitsandbytes0.41.0 accelerate0.25.0这里有个坑bitsandbytes的版本和 CUDA 版本必须匹配。我一开始装了最新版的bitsandbytes结果 4-bit 量化加载直接报错回退到 0.41.0 才正常。如果你用的是 CUDA 11.8需要装bitsandbytes0.40.0。4.2 基础模型的 4-bit 量化加载加载基础模型的代码大概长这样from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig bnb_config BitsAndBytesConfig( load_in_4bitTrue, bnb_4bit_quant_typenf4, bnb_4bit_compute_dtypetorch.float16, bnb_4bit_use_double_quantTrue, ) model AutoModelForCausalLM.from_pretrained( base_model_path, quantization_configbnb_config, device_mapauto, trust_remote_codeTrue, )关键参数解释一下bnb_4bit_quant_typenf4NF4 是 4-bit 量化的标准类型比 FP4 在数值分布上更稳定。决策模型的数值分布往往集中在 0 附近NF4 对这种分布更友好。bnb_4bit_compute_dtypetorch.float16计算时用 FP16保证推理精度。如果显存实在紧张可以改成torch.bfloat16但决策模型对精度敏感我不建议。bnb_4bit_use_double_quantTrue双重量化对量化参数再做一次量化能再省一点显存。实测大概省 0.1GB 到 0.2GB蚊子腿也是肉。加载完之后用nvidia-smi看一下显存占用。基础模型加载后大概占 1.8GB 左右加上 CUDA 上下文和系统占用总共 2.5GB 左右。还剩 3GB 给 KV Cache 和 LoRA。4.3 LoRA 适配器的训练和加载LoRA 的训练我用的是peft库配置如下from peft import LoraConfig, get_peft_model lora_config LoraConfig( r8, lora_alpha16, target_modules[q_proj, v_proj], lora_dropout0.05, biasnone, task_typeCAUSAL_LM, ) model get_peft_model(model, lora_config)r8是秩控制 LoRA 矩阵的大小。秩越大表达能力越强但显存占用也越大。对于决策任务r8到r16足够了。我试过r32效果提升不明显但显存多了 0.3GB不划算。target_modules选的是q_proj和v_proj这是注意力层的查询和值投影。决策模型的关键信息主要在这两个投影里对这两个加 LoRA 性价比最高。如果你显存还有富余可以加上k_proj和o_proj但 6GB 环境下不建议。训练时的显存占用大概在 3.5GB 左右因为 4-bit 基础模型加上 LoRA 的梯度再加上优化器状态。如果你用paged_adamw_8bit优化器还能再省一点。4.4 推理时的显存管理策略推理时的显存管理是核心。我的策略是预分配 KV Cache根据最大序列长度预分配 KV Cache避免动态增长导致的碎片。LoRA 显存池预分配 512MB 显存专门给 LoRA 权重加载和卸载都通过池来管理。推理后清理中间张量每次推理结束后手动调用torch.cuda.empty_cache()但不要频繁调用否则会拖慢速度。具体的推理循环大概是这样# 预分配 KV Cache past_key_values model.prepare_inputs_for_generation(...) # 加载 LoRA 到显存池 lora_pool.load(lora_weights) # 推理 with torch.no_grad(): outputs model.generate(...) # 卸载 LoRA lora_pool.unload() # 清理中间张量 del outputs torch.cuda.empty_cache()实测下来这个流程的显存峰值稳定在 5.6GB 左右留了 0.4GB 的余量不会 OOM。5. 翻车现场实录那些让我熬夜的 bug5.1 NaN 损失校准集选错导致的连锁反应第一个晚上量化后的 Kev 在推理到第三步时突然输出 NaN。不是概率为 NaN是整个输出向量全是 NaN。我一开始以为是数值溢出加了梯度裁剪和数值稳定项没用。后来用torch.autograd.set_detect_anomaly(True)定位发现是量化后的权重里出现了 Inf。根因是校准集。我用的通用语料里有一些极端值量化算法把这些极端值映射到了量化区间的边界反量化后变成了 Inf。换成决策任务采样的校准集后极端值消失NaN 问题解决。经验4-bit 量化的校准集一定要用实际任务数据不要图省事用通用语料。校准集里的极端值会被量化算法放大最终导致推理崩溃。5.2 LoRA 加载后输出乱码权重合并顺序错了第二个晚上LoRA 训练完了加载到模型上推理输出全是乱码。不是 NaN是正常的 token 但语义完全不通。我检查了 LoRA 的权重数值正常检查了加载逻辑也没问题。最后发现是权重合并顺序错了。peft库在加载 LoRA 时默认是把 LoRA 权重合并到基础权重里。但我的基础模型是 4-bit 量化的合并时 LoRA 的 FP16 权重和 4-bit 的基础权重直接相加精度不匹配导致数值错乱。解决办法是先反量化基础权重到 FP16再合并 LoRA最后重新量化。但这样显存占用会飙升不可行。最终方案是不合并动态加载。推理时分别计算基础权重和 LoRA 的增量在计算图上相加。这样虽然多了一点计算量但精度没问题。5.3 显存碎片导致第二次推理 OOM第三个晚上第一次推理正常第二次推理直接 OOM。nvidia-smi显示还有 1.2GB 空闲但就是分配不出来。这就是显存碎片。我用torch.cuda.memory_summary()看了一下发现显存里全是小块空闲区域最大的连续块只有 200MB。原因是每次推理都新建 KV Cache推理完释放但释放的块和周围的不连续。解决办法就是前面说的预分配 KV Cache 和 LoRA 显存池。预分配之后显存里的大块区域始终保留碎片问题消失。5.4 常见问题速查表问题现象可能原因排查方法解决方案推理输出 NaN量化校准集有极端值检查校准集数值分布换用任务相关校准集LoRA 加载后输出乱码权重合并精度不匹配检查合并时的数据类型改为动态加载不合并第二次推理 OOM显存碎片torch.cuda.memory_summary()预分配 KV Cache 和显存池推理速度突然变慢显存不足触发 CPU 卸载检查device_map减少并发或降低序列长度模型加载失败bitsandbytes 版本不匹配检查 CUDA 版本回退到匹配的版本6. 实操心得几个让我少走弯路的技巧6.1 用nvidia-smi的--query-gpu做实时监控nvidia-smi默认输出信息太多不方便脚本化监控。我习惯用nvidia-smi --query-gpumemory.used,memory.free,utilization.gpu --formatcsv -l 1这样每秒输出一行显存和利用率一目了然。调试显存问题时把这个输出重定向到文件事后分析很方便。6.2 量化前先做一次 FP16 推理基准在量化之前先用 FP16 跑一次推理记录输出结果和显存占用。量化后再跑一次对比输出差异。如果差异超过 5%说明量化参数需要调整。这个基准测试花不了多少时间但能帮你快速判断量化是否成功。6.3 LoRA 的秩不要贪大我试过r32和r64效果提升微乎其微但显存占用线性增长。对于决策任务r8到r16是甜点区间。再大就是浪费显存。6.4 推理序列长度能短则短KV Cache 的显存占用和序列长度成正比。如果你不需要长上下文把max_length设小一点能省不少显存。我一开始设了 2048后来发现实际决策只需要 512改小之后显存直接省了 0.8GB。6.5 不要频繁调用torch.cuda.empty_cache()empty_cache()会强制释放所有未使用的显存块但也会导致下次分配时重新申请增加延迟。我的做法是只在 LoRA 切换时调用一次推理循环里不调用。7. 后续可以继续折腾的方向这套方案跑通之后我又试了几个优化方向。一个是把基础模型换成更小的版本比如 1.5B 参数显存占用能降到 4GB 以下但决策精度会下降需要重新训练 LoRA 来补偿。另一个是尝试GPTQ量化替代bitsandbytesGPTQ 的推理速度更快但校准过程更复杂我还没时间细调。还有一个方向是把两个 LoRA 合并成一个多任务 LoRA通过任务 ID 来切换。这样就不需要动态加载卸载显存占用更稳定。但多任务 LoRA 的训练需要混合两个任务的数据可能会互相干扰需要仔细调权重。如果你也在低显存环境下折腾模型部署欢迎交流。我踩过的坑基本都写在这了希望能帮你省下几个晚上。