ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

16G显存也能跑通Qwen 7B LoRA微调:显存优化与实战全流程

16G显存也能跑通Qwen 7B LoRA微调:显存优化与实战全流程 我手里这张 16G 显存的卡在服务器机箱里躺了快两周一直被当成纯推理卡用——跑跑 Qwen 7B 的量化版还行一聊到微调就心虚。全参微调 7B光权重、梯度、优化器状态加起来就要上百 GB16G 想都不用想。但换一条路LoRA 微调同样的模型16G 不仅能练练完还能接着跑推理。这篇是我们远程调试系列的第三篇前两篇把远程环境、端口转发、日常调试工具都备好了这次就把 LoRA 微调 Qwen 7B 这条主线完整走一遍先拆显存账再讲环境与数据准备接着逐行解读训练配置最后聊训练中踩过的坑和验收方式。想把消费级显卡利用起来的人这篇可以直接当实操手册用。1. 先算显存账为什么 7B 全参微调在 16G 卡上是伪命题1.1 一次全参微调的显存开销到底有多离谱先做个简单的算术单位是 GB开销项全参微调bf16/fp16LoRA 微调模型权重约 14GB约 14GB冻结梯度约 14GB只算 LoRA 部分约 0.2GBAdamW 优化器状态约 84GB约 1.1GB激活值开 grad_ckpt约 3-6GB约 1-2GB合计约 115GB约 16-18GB我故意把激活值写得保守一点。全参微调的真正大头不是模型本身而是优化器状态AdamW 要为每个参数额外保存一份 fp32 副本再加上一阶动量 m 和二阶动量 v一个参数摊下来是 12 个字节。7B 参数一乘84GB 就没了。所以 A100 的 80GB 显存跑全参微调都吃力不是卡的问题是数学问题。这里要顺便纠正一个常见误区很多人以为显存只用来装模型权重其实训练和推理差别巨大。推理时你只需要权重加 KV Cache14GB 的模型在 16G 显存上跑得动一旦进入训练模式梯度要存、优化器状态要存、前向传播的中间激活值也要存哪一样都是真金白银。1.2 LoRA 把钱花在了刀刃上只训一条窄通道LoRA 的核心思路是大模型权重在微调时不需要把 7B 个参数全动一遍而是假设参数的更新量本身是低秩的。于是它把原始权重 W 冻结住在旁边挂两个小矩阵 A 和 B前向传播时输出变成 Wx BAx训练时只更新 A、B 的参数最后把 BA 合并回 W 或者作为一个独立 adapter 保存。用大白话讲全参微调像是要把整本书重新印刷一遍代价是按书的页数算LoRA 相当于在原书上贴便利贴只改需要改的地方最后把便利贴汇总成一本小册子。书还是那本书但批注全在小册子里。以 Qwen2.5-7B 为例hidden size 是 3584如果 rank 取 64每个线性投射层新增 2×3584×64 大约 46 万个参数7B 模型里几十个目标层加起来就是 7000 万到 1 亿这个量级。对比原模型的 70 亿参数相当于只训练 1% 左右。这才是 LoRA 能跑在 16G 显存上的根本原因优化器状态和梯度体积直接被砍掉了两个数量级。2. 远程服务器上的准备环境、框架、数据三件套2.1 第一步不是写代码而是确认显卡和 CUDA 环境远程调试最大的特点就是你人不在机器前面。所以你首先要确认的不是代码而是nvidia-smi输出能不能稳定看到。我自己的习惯是先跑一遍nvidia-smi -L看显存型号再看 CUDA 版本然后nvidia-smi --query-gpumemory.total,memory.free --formatcsv确认没有别的进程占着显存。注意一个隐性坑远程机器通常会有别的任务在跑16G 显卡可能只剩 10G 可用。在微调前先清掉不用的进程或者至少弄清当前可用显存是多少否则你会在“配置文件看着没问题一启动就 OOM”这类问题上耗掉不少时间。确认命令nvidia-smi python -c import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))2.2 LLaMA-Factory 装好省一半的力在 LoRA 微调这个赛道上LLaMA-Factory 可以说把“轮子”造得非常完整。它内置了 Qwen 等一堆模型的加载逻辑、对话模板、LoRA/QLoRA/Freeze 训练方式还有 WebUI 和 CLI 两套入口。对我们这种远程调试场景WebUI 配合端口转发用得很顺。推荐的安装方式conda create -n llamafactory python3.10 -y conda activate llamafactory cd ~ git clone --depth 1 https://github.com/hiyouga/LLaMA-Factory.git cd LLaMA-Factory pip install -e .如果从零开始还要确保 PyTorch 版本和 CUDA 版本匹配。我实际装机时踩过最大的坑是 PyTorch 编译版和驱动不匹配导致torch.cuda.is_available()直接 False遇到这种情况优先考虑重装对应版本的 PyTorch 轮子而不是去刷驱动——服务器上动驱动风险太大。2.3 数据决定模型上限alpaca 格式怎么组织数据这一步最容易被低估。模型能学成什么样训练参数只是放大器数据内容才是信号本身。LLaMA-Factory 默认支持两类格式alpaca 和 sharegpt。alpaca 格式是经典的三段式 JSON[ { instruction: 请判断以下投诉属于哪个分类并给出回复建议。, input: 昨天买的鼠标今天双击失灵售后说要返厂检测太浪费时间了。, output: 分类售后投诉。回复建议先安抚客户情绪说明返厂是标准流程… } ]有几个细节我想强调一下。第一instruction 和 input 可以拆分instruction 是固定的任务描述input 是变化的内容。如果任务简单input 留空完全没问题框架也支持。第二output 一定不能为空也不能用省略号糊弄——空字符串在部分 tokenizer 下不会崩但会把模型带偏。第三数据量不在多而在覆盖几百条干净、风格一致的数据往往比几千条参差不齐的数据更有效。我自己调过一轮测试500 条对齐数据微调出来的效果明显好过 1500 条混杂数据的版本。3. 配置逐行拆base_model、train_data、val_data、output_dir 以及 LoRA 的关键参数3.1 配置文件的四个地基层次很多人拿到的 LoRA 训练配置长这样base_model: /models/Qwen2.5-7B-Instruct train_data: data/train.json val_data: data/val.json output_dir: outputs/qwen2.5-lora这四个字段是地基。base_model指向原始模型目录必须和模型文件的路径一一对应。这里有个很容易犯的错误填了 Hugging Face 仓库名而不是本地路径。如果服务器网络不通或者本地缓存不全加载会卡很久或者直接失败。建议先把模型下载到本地目录路径写绝对路径。train_data训练集一般指上面说的 alpaca 或 sharegpt 格式 JSON 文件。val_data验证集用来在每个保存点计算 loss。验证集不需要很大几百条就够甚至可以用 train_data 本身切出 5% 作为验证集框架一般都支持val_size一类的参数。output_dir输出目录。LoRA 训练完不是直接产出一个新模型而是产出一堆 checkpoint 目录和 adapter_config.json、adapter_model.safetensors。这个目录会不断膨胀注意磁盘配额别把 /root 撑爆。3.2 LoRA 参数三兄弟rank、alpha、target_moduleslora_rankr决定了低秩矩阵的宽度。r32 适合轻量任务r64 是很多博客默认值r128 效果理论上更好但显存和磁盘开销上涨。作为 7B 模型在 16G 显存上的选择64 是甜点值理由前面算过它带来的额外参数量刚好在可控范围。lora_alphaα是缩放系数。它表示 LoRA 更新量的放大倍数常见的经验是取 rank 的 2 倍也就是 α128。如果 α 太大训练初期输出会被扰动得特别厉害loss 曲线可能直接起飞如果太小说更新量被低估训练半天没变化。target_modules指定在哪些模块上挂 LoRA。最省事的做法是设为全部线性层all因为 Qwen 是稠密模型q/k/v/o/gate/up/down 这些投射层都值得微调。如果显存紧张可以只选q_proj、v_proj显存占用能降一点但效果通常不如 all。3.3 训练参数这两行的存在就是为了保显存接着看训练参数per_device_train_batch_size: 1 gradient_accumulation_steps: 8 learning_rate: 1e-4 num_train_epochs: 3 max_seq_length: 1024 gradient_checkpointing: true fp16: true这里需要建立一个基本概念per_device_batch_size × gradient_accumulation_steps才是真正的更新步 batch size。显存不够batch size 就压到 1用 8 步梯度累加来模拟 batch size 8 的更新效果。代价是训练时间变长、GPU 利用率下降但显存是硬约束时间不是。max_seq_length是另一个显存大头。激活值随序列长度线性增长seq length 从 1024 涨到 2048激活值几乎翻倍。16G 卡跑 Qwen 7B 时我一般先试 1024如果 OOM先砍长度到 512而不是急着砍 batch size。这个顺序非常关键序列长度影响的是所有样本的显存占用batch size 影响的是并行的样本数两者的优先级完全不同。gradient_checkpointing常开它把前向过程中的激活值扔掉一部分反向时重算空间换时间。实测能把峰值压下来 2-3G代价是每个 step 慢 20% 左右这个性价比很值。fp16还是bf16如果你手里是 RTX 3090/4090 这类支持 bf16 的卡优先 bf16数值稳定性更好如果是老一点的 16G 卡比如 T4、P100只能 fp16那么梯度缩放和混合精度要开好后面 NaN 那节会再提到。4. 真正点下启动按钮训练现场与显存实况4.1 用长任务的方式启动tmux 加端口转发远程训练和本地训练有个本质区别你随时可能断网。一个训练流程动不动几小时ssh 窗口一断进程跟着没了。所以第一步是创建 tmux 会话tmux new -s train # 在会话中执行训练比如 llamafactory-cli train config.yaml退出会话用CtrlB再按D断线重连用tmux attach -t train。这是一个不会让你后悔的小习惯。之前我犯过傻直接前台跑结果公司断网一次两小时白训。如果用的是 WebUI本地浏览器远程访问也很简单ssh -L 7860:localhost:7860 usergpu-server然后本地打开http://127.0.0.1:7860就能以浏览器方式配置和观察训练过程。4.2 训练中的四个实时指标盯什么训练开始后不要只盯着nvidia-smi看显存数字。我一般开四个并行窗口一个tmux attach看训练日志一个跑nvidia-smi -l 2实时看显存一个跑htop看 CPU 负载最后一个随时待命应急。nvidia-smi里最值得看的两列是Memory-Usage和GPU-Util。显存稳定在一个值附近说明正常GPU-Util 如果长期低于 30%大概率是 batch size 太小、数据加载卡 IO 或者 PyTorch 数据线程没调好。此时把dataloader_num_workers调到 4 以上或者检查数据是不是在机械硬盘上。训练日志里我们主要盯loss。正常的曲线是缓慢波动下降比如从 1.8 降到 0.9中间有小幅抖动完全正常。如果 loss 在某个 epoch 突然变成 3.5 以上并且回不来先停训练不要等它自愈——大概率是学习率或者数据问题。4.3 16G 下的实测显存数据能跑和稳跑是两回事用 Qwen2.5-7B-Instruct 实测配置按第三章那套来rank64、target_modulesall、batch_size1、max_seq_length1024、开启 gradient checkpointing、bf16 精度显存大概稳定在 14.5GB 到 16GB 之间。说明一下“能跑”和“稳跑”的区别能跑是峰值显存刚好压在 16G 以下稳跑则是把峰值降到 15G 以内留下 1G 以上余量给验证集推理和临时波动。我实际遇到的情况是训练到某个保存点瞬间显存会跳高一点val_loss 计算时如果验证集 batch 太大还会再峰值一把所以不要顶着 15.9G 的边训练边提心吊胆。5. 四个崩溃瞬间我在 Qwen 7B 上踩过的 LoRA 坑5.1 OOM 不只是发生在训练还发生在验证第一次跑的时候我以为只要训练 batch 小就没问题结果训到第三个保存点崩溃了。日志里torch.cuda.OutOfMemoryError但前面明明跑得好好的。排查一圈才发现是验证集 batch size 问题我的配置里训练 batch 是 1但默认的验证 batch 可能是 2 或者 8模型一下把验证 batch 的全量激活值放进显存峰值立刻越界。验证 batch 单独设小是很多人忽略的动作。通用的修复是在配置里加per_device_eval_batch_size: 1 max_eval_samples: 50前一个参数把验证 batch 压到和训练一致后一个参数限制验证集条数省得每轮都拿几百条验证数据在显存里反复跑。5.2 loss 变成了 NaN 或直接起飞loss 变 NaN 的原因通常是两类学习率太大或者数据里有不合法文本。第一次我用learning_rate1e-3跑 7B 的 LoRA前几百步 loss 直线上升最后变 NaN。LoRA 因为只更新少量参数对学习率的敏感度和全参微调不完全一样1e-4 甚至 5e-5 更稳。数据侧的问题更隐蔽。排查时我发现有一条 JSON 记录里output字段是nulltokenizer 处理时产生了空序列前向计算出现 inf。修复很简单写个脚本把空字段、None、以及超过max_seq_length的超长样本全部过滤掉。我习惯在训练前加一步数据体检脚本import json from tqdm import tqdm with open(train.json, r, encodingutf-8) as f: data json.load(f) clean [] for item in tqdm(data): text item.get(output, ) or if len(text.strip()) 0: continue clean.append(item) with open(train_clean.json, w, encodingutf-8) as f: json.dump(clean, f, ensure_asciiFalse, indent2)把隐性炸弹提前挖掉比崩溃后再一步步排查省太多时间。5.3 训练结束效果像没练这口锅谁来背最让人崩溃的不是训练崩了而是训练正常结束loss 也降了模型回答却像没练过一样。我总结出三个最常见原因。第一个是底模选错了拿 base 模型做 SFT对话模板没有 chat 模板推理时没有正确拼上对应格式效果自然稀烂。建议直接用 Instruct/Chat 版本比如Qwen2.5-7B-Instruct。第二个是训练轮数过多导致灾难性遗忘LoRA 只是把模型“钉”在某个区域附近并不天然免疫遗忘。我一度用 10 个 epoch 去跑几百条数据结果微调后连原有的通用能力都退化了。7B 模型在 500 到 1000 条数据上2 到 3 个 epoch 通常是合理区间超过 5 个就很容易过拟合。第三个是推理时没加载 LoRA adapter。很多框架训练完默认只存 adapter不合并进底模。如果你在普通 transformers 管道里直接加载output_dir等于拿原始模型推理效果当然没变化。关于加载方式下一节展开说。6. 训练完后怎么验收合并、加载、试答6.1 两种加载方式别搞混LoRA 训练产物有两种用法。第一种是单独保存 adapter推理时代码里显式加载from peft import PeftModel from transformers import AutoModelForCausalLM, AutoTokenizer model AutoModelForCausalLM.from_pretrained(base_model_dir) model PeftModel.from_pretrained(model, outputs/qwen2.5-lora)好处是多 adapter 切换方便同一个底模挂不同 adapter应对不同业务。坏处是每次启动推理都要多做一层加载部署链路稍微复杂一点。第二种是先把 LoRA 合并到底模权重里再导出一个完整的模型文件。框架一般提供 merge 命令或 API合并后的模型和普通模型文件没有区别后续 vLLM、Ollama 都能直接加载。适合“一次调优长期部署”的场景。我实际更常用第二种因为要部署的服务器通常没有训练框架一个完整模型目录部署起来最省心。如果是不断迭代调优的阶段则第一种更方便。6.2 用 20 条评测题快速判断效果不要只看训练 loss。训练 loss 下降只说明模型记住了训练集的模式不代表它对真实问题回答变好了。我的做法是准备 20 到 50 条评测题每条都不能出现在训练集里内容覆盖你真正关心的业务场景。然后把底模和微调后的模型分别跑一遍同样的问题逐条对比。对比时注意看三点格式是否对齐、语气是否变化、事实性是否保持。比如你微调的是售后话术风格底模可能回答得中规中矩但不够落地adapter 加载后的回答应该表现出明确的任务感先分类、再安抚、最后给方案。如果格式对齐但事实歪了说明训练数据里存在错误映射或者 epoch 太多导致旧知识被覆盖。6.3 显存还有余量QLoRA 与多点扩展训练结束如果显存还有富余或者你手里的卡只有 8G可以考虑 QLoRA。QLoRA 把底模量化到 4bit底模权重从 14GB 降到大约 4GB加训练开销之和往往能压在 8G 以内。代价是量化引入一点精度损失以及训练和推理速度略降。对很多轻量场景来说QLoRA 是更现实的选择。另外可以试试 NEFTune 这类训练技巧在 embedding 上注入轻微噪声对部分数据量较小的场景有正面效果。我自己实验下来数据量少于 200 条时它的收益更明显数据量大了之后差异不大。这些都是一些可以继续扩展的方向不过先把 LoRA 基础流程跑通比什么都重要。最后再分享一个个人习惯。我在远程训练前会把nvidia-smi的输出截个图训练过程中每 500 步再截一张最后把 loss 曲线和显存曲线放在一起看。这样能很快发现一些问题比如前 500 步显存只有 13G到了第 1200 步突然变成 15.5G很可能是有别的进程插进来了。系列前两篇搭好的远程环境在这轮训练里发挥了很大作用如果你也打算在 16G 卡上把 Qwen 7B 练起来建议先从第三章那份配置复制走跑通一次再去折腾微调技巧。
返回列表