7B模型微调显存不够?我拿RTX 3090把全量、LoRA、QLoRA都跑了一遍
先说结论,可能和很多教程不一样:在单张3090 24GB上,7B模型全量微调不是调小batch就能跑,而是根本别想。 LoRA也不是越省显存越好,rank 16到64带来的效果差,常常小于你数据里200条重复样本带来的伤害。 QLoRA确实能把7B塞进24GB,但它的代价是训练速度掉到全量LoRA的60%左右。 而且4bit量化不是主要瓶颈,长序列attention才是。 我的机器:锐龙5600X、64GB DDR4 3200、RTX 3090 24GB、Ubuntu 22.04、CUDA 12.1、PyTorch 2.3.0、transformers 4.41.2、peft 0.11.1、bitsandbytes 0.43.1。 模型用Qwen2.5-7B-Instruct,数据是3200条客服工单JSONL,字段instruction/input/output,最长1800 token。 为什么记得这么细?因为OOM三次之后你会发现,版本号不是装饰,bitsandbytes和CUDA错一个版本就直接给你脸色。
三个方案的真实账:全量、LoRA、QLoRA
先看显存。7B参数fp16约14GB,全量训练还要梯度14GB、AdamW状态56GB,加上激活,稳稳超过80GB。 A100 80GB都紧张,3090 24GB只能跑参数加载,反向传播一开就炸。 LoRA fp16冻结基座,只训练低秩矩阵,rank 16时可训练参数约42M,优化器状态很小,但基座14GB还在。 序列1024、micro batch 1、grad accum 16时,实测峰值19.8GB左右,能跑但别开packing和全target_modules。 QLoRA把基座压到4bit NF4,再加double quant,权重占用降到约4.2GB。 同样的Qwen2.5-7B,seq 1024、micro batch 1、grad accum 16,我这边峰值13.6GB,留了约10GB给激活和碎片。 速度上,LoRA fp16约2100 tokens/s,QLoRA约1350 tokens/s;如果target_modules只加q_proj/v_proj,QLoRA能到1500 tokens/s左右。 注意这是3090不是4090,别拿别人24GB 4090的吞吐直接套。
| 方案 | 权重占用 | 优化器+梯度 | 实测峰值 | 吞吐 | 适合场景 |
|---|---|---|---|---|---|
| 全量 fp16 | 14GB | 70GB+ | >80GB | 跑不起来 | 多卡、继续预训练 |
| LoRA fp16 | 14GB | 约0.5GB | 19.8GB | 约2100 tokens/s | 格式、风格、术语对齐 |
| QLoRA 4bit | 约4.2GB | 约0.6GB | 13.6GB | 约1350 tokens/s | 单卡24GB、数据1k-10k |
我踩过的坑:OOM不一定是batch太大
第一个坑是max_seq_length。我一开始设2048,Qwen2.5的tokenizer把很多工单切到1900多,显存直接冲到23.4GB,然后CUDA OOM。 后来把cutoff_len降到1024,只对超长样本做尾部截断,峰值掉到13.6GB。 第二个坑是gradient_checkpointing,LoRA下要配合enable_input_require_grads(),否则梯度断掉,loss不动。 第三个坑是use_cache,训练时必须关,不然显存白涨1-2GB。 还有一个反直觉的点:LoRA的rank从16拉到64,显存只多0.2-0.4GB,但训练时间多18%左右,验证集loss从0.842降到0.831,差距小到可能被随机种子吃掉。 我后来在3200条数据上固定seed=42,rank 16、alpha 32、dropout 0.05反而比rank 64更稳。 真正让验证loss从0.98降到0.84的,不是rank,而是把217条重复工单去重、把空output样本删掉、把学习率从2e-4降到1e-4。
可直接抄的QLoRA步骤和参数
第1步,数据格式用JSONL,一行一个对象,字段instruction、input、output。 第2步,加载模型:load_in_4bit=True、bnb_4bit_quant_type='nf4'、bnb_4bit_use_double_quant=True、bnb_4bit_compute_dtype=torch.bfloat16。 第3步,prepare_model_for_kbit_training(model),再配LoraConfig(r=16, lora_alpha=32, lora_dropout=0.05, target_modules=['q_proj','k_proj','v_proj','o_proj'], bias='none', task_type='CAUSAL_LM')。 训练参数我最后稳定在:per_device_train_batch_size=1、gradient_accumulation_steps=16、learning_rate=1e-4、lr_scheduler_type='cosine'、warmup_ratio=0.03、num_train_epochs=3、bf16=True、gradient_checkpointing=True、logging_steps=10、save_strategy='epoch'。 如果OOM,按这个顺序降:cutoff_len 1024→768→512;gradient_accumulation_steps保持16但per_device_train_batch_size保持1;关掉packing;把target_modules缩到q/v。 别一上来就换4090,先看是不是padding浪费了30%显存。
我的独立观点:别把LoRA当小全量微调
LoRA的本质是给冻结权重加一个低秩干预,它擅长改输出分布,不擅长塞新知识。 如果你要教模型2024年之后的内部产品文档,LoRA可能只是让它学会用自信语气胡说。 如果你只是要它按固定JSON输出、改客服口吻、认领域术语,LoRA足够。 QLoRA的意义也不是免费降显存,而是把训练从多卡拉到单卡,代价是速度、量化和一些数值稳定性。 我的建议是:1k-10k条数据先跑QLoRA rank 16,学习率1e-4,seq 1024,3 epoch,看验证loss和人工抽检50条。 如果效果不够,优先加数据、去重、调prompt模板,最后才动rank和target_modules。