AI 推理优化系列:LoRA/QLoRA 微调原理:单卡 24G 显存如何微调 70B 大模型 一、为什么全参数微调不现实先算一笔账。以 Llama-2-70BFP16为例模型权重本身占70B 参数 × 2 字节(FP16) 140 GB但训练时的显存远不止权重。训练过程中显存由四部分构成组成部分公式70B 模型估算模型权重P × 2140 GB梯度P × 2140 GB优化器状态AdamWP × 12840 GB激活值与 batch/seq_len 相关约 150-300 GBAdamW 优化器需要保存 momentum 和 variance 两个状态每个都是 FP324 字节加上模型权重的 FP32 副本所以优化器状态是P × (444) P × 12。总显存 ≈ 140 140 840 200 ≈ 1320 GB。这至少需要 16 张 A10080G普通开发者直接劝退。下面逐个拆解 LoRA 和 QLoRA 如何把这笔账砍下来。二、LoRA 核心思想低秩分解2.1 关键观察2021 年微软论文《LoRA: Low-Rank Adaptation of Large Language Models》提出一个关键观察模型在适配下游任务时权重的更新量 ΔW 具有很低的内在秩intrinsic rank。也就是说ΔW 不需要是全秩矩阵可以用两个小矩阵的乘积来近似ΔW B × A其中B ∈ R^(d×r)A ∈ R^(r×d)r dr 通常取 8/16/32/64。2.2 前向传播的变化原始全连接层h W₀ · x 加入 LoRA 后h W₀ · x ΔW · x W₀ · x B · A · x x (d维) │ ┌────▼────┐ ┌─────────┐ ┌─────────┐ │ W₀ │ │ A │ │ B │ │ (冻结) │ │ (r×d) │ │ (d×r) │ │ d×d │ └────┬────┘ └────┬────┘ └────┬────┘ │ 降维到r │ 升维回d │ │ │ └──────────┬─────┴────────────────┘ │ (B·A·x) × α/r ▼ h W₀·x B·A·x关键设计点W₀ 完全冻结不计算梯度、不更新只训练 A 和 B 两个小矩阵推理时可以把 B·A 合并回 W₀零额外延迟2.3 参数量对比以d4096, r8为例全参数微调d × d 4096 × 4096 ≈ 16.8M 参数 LoRA 可训练r × d d × r 8×4096 4096×8 65,536 参数可训练参数量降到原来的65536 / 16.8M ≈ 0.39%。如果是 70B 模型的一个注意力层节省比例同样量级。三、QLoRA把基座模型压进 24G 显卡2023 年华盛顿大学的 QLoRA 论文进一步把显存砍到消费级显卡可承受的范围。它在 LoRA 基础上做了三件事3.1 4-bit NormalFloatNF4量化NF4 是一种针对正态分布权重设计的 4-bit 数据类型。正态分布的权重用均匀量化会浪费精度NF4 按分位数量化等概率地划分每个量化区间。FP16 权重(16-bit) ──量化──▶ NF4(4-bit) 每个参数从 2 字节 → 0.5 字节体积压缩 4 倍3.2 双重量化Double Quantization量化本身需要保存缩放因子quantization constants。双重量化把这层量化常数的量化常数也量化掉进一步省显存权重量化常数通常 32-bit 浮点 QLoRA 把常数再量化成 8-bit 二级常数 效果平均每个参数再省 0.37 bit3.3 分页优化器Paged Optimizers用 NVIDIA 统一内存Unified Memory做分页在显存紧张时把优化器状态临时换页到 CPU 内存避免训练中途 OOM。四、显存计算三种方案对比用 Llama-2-70B 估算单卡训练一个 step 的峰值显存方案权重可训练参数优化器状态激活值峰值显存可行性全参数微调140 GB140 GB840 GB200 GB~1320 GB16×A100LoRA (r8)140 GB(冻结)0.1 GB0.6 GB60 GB~200 GB3×A100QLoRA (NF4)35 GB(4-bit)0.1 GB0.6 GB30 GB~65 GB1×A100-80G注意 QLoRA 把 140GB 权重量化成 35GB4-bit加上 LoRA 参数和优化器状态极小单张 80G 的 A100 即可微调 70B 模型。而用 24G 的 RTX 4090配合更小的 batch 和梯度检查点微调 7B-13B 模型完全可行微调 33B 模型量化后约 16-17GB 权重也勉强可上。实测在单张 RTX 4090 (24G) 上QLoRA 微调 Llama-2-13Bpeak memory ≈ 21.3 GB训练速度约 320 tokens/s。五、源码走读PEFT 中 LoRA 的实现HuggingFace PEFT 库是 LoRA 的事实标准实现。核心是把nn.Linear替换成Linear类PEFT 版包裹一个冻结的base_layer和两个可训练的lora_A/lora_B。5.1 层替换逻辑精简# peft/tuners/lora/layer.py核心逻辑示意 class Linear(nn.Linear): def __init__(self, base_layer, r8, lora_alpha16, ...): self.base_layer base_layer # 原始权重requires_gradFalse self.r r self.lora_alpha lora_alpha self.scaling lora_alpha / r # 缩放系数 α/r ​ # 两个低秩矩阵 self.lora_A nn.Linear(in_features, r, biasFalse) self.lora_B nn.Linear(r, out_features, biasFalse) ​ # 初始化A 用随机高斯B 用全零 → 初始 ΔW0训练起点原模型 nn.init.kaiming_uniform_(self.lora_A.weight, a5**0.5) nn.init.zeros_(self.lora_B.weight) ​ def forward(self, x): result self.base_layer(x) # 冻结路径 if self.r 0: result result self.lora_B(self.lora_A(x)) * self.scaling return result5.2 为什么 B 初始化为零B 初始化为零意味着训练开始时B·A 0模型输出与原始 W₀ 完全一致。这保证 LoRA 不会在训练初期破坏预训练知识是一个稳定的热启动。5.3 模块注入from peft import get_peft_model, LoraConfig ​ config LoraConfig( r8, lora_alpha16, target_modules[q_proj, v_proj], # 只注入注意力中的 Q/V 投影 lora_dropout0.05, biasnone, ) model get_peft_model(base_model, config) model.print_trainable_parameters() # trainable params: 4,194,304 || all params: 6,742,609,920 || trainable%: 0.0622target_modules决定 LoRA 注入哪些层。经验上只注入q_proj/v_proj就能拿到大部分收益注入全部线性层效果更好但参数量更大。六、实战QLoRA 微调 Llama-2-7B完整可复现的训练脚本基于transformersbitsandbytespeft6.1 环境准备pip install torch2.1.0 transformers4.36.0 peft0.7.1 \ bitsandbytes0.41.1 datasets2.16.0 accelerate0.25.06.2 4-bit 加载基座模型import torch from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training ​ model_id meta-llama/Llama-2-7b-hf ​ # QLoRA 4-bit 量化配置 bnb_config BitsAndBytesConfig( load_in_4bitTrue, bnb_4bit_quant_typenf4, # NF4 量化 bnb_4bit_compute_dtypetorch.bfloat16, # 计算用 bf16 bnb_4bit_use_double_quantTrue, # 双重量化 ) ​ tokenizer AutoTokenizer.from_pretrained(model_id) model AutoModelForCausalLM.from_pretrained( model_id, quantization_configbnb_config, device_mapauto, ) model prepare_model_for_kbit_training(model) # 准备梯度检查点6.3 配置 LoRA 并注入lora_config LoraConfig( r16, lora_alpha32, target_modules[q_proj, k_proj, v_proj, o_proj], lora_dropout0.05, biasnone, task_typeCAUSAL_LM, ) model get_peft_model(model, lora_config) model.print_trainable_parameters() # trainable params: 20,979,712 || all params: 6,742,609,920 || trainable%: 0.316.4 训练循环from transformers import Trainer, TrainingArguments ​ training_args TrainingArguments( output_dir./llama2-7b-qlora, per_device_train_batch_size4, gradient_accumulation_steps4, # 等效 batch16 warmup_steps20, max_steps200, learning_rate2e-4, fp16True, logging_steps10, save_strategysteps, save_steps100, ) ​ trainer Trainer( modelmodel, argstraining_args, train_datasettokenized_dataset, data_collatorDataCollatorForLanguageModeling(tokenizer, mlmFalse), ) trainer.train() ​ # 训练完只保存 LoRA 权重约 80MB不保存 7B 基座 model.save_pretrained(./llama2-7b-qlora-adapter)6.5 推理加载from peft import PeftModel ​ base AutoModelForCausalLM.from_pretrained(model_id, device_mapauto) model PeftModel.from_pretrained(base, ./llama2-7b-qlora-adapter) # 推理时 B·A 自动合并无额外延迟七、微调效果量化会损失多少精度在 AlpacaEval / MMLU 上的对比基于公开复现数据结论QLoRA 相比全参数微调精度损失不到 1 个点但显存从 120GB 降到 13.8GB适配器从 13.5GB 降到 80MB。这就是它能在消费级显卡上跑通的原因。八、调优建议8.1 关键超参参数建议说明r(秩)8-64任务越复杂越大通用对话 16 足够lora_alpha2×r 或 4×r实际缩放 alpha/r影响适配强度target_modulesQ/V 起步需要更强可加 K/O/FFNlora_dropout0.05防过拟合学习率1e-4 ~ 3e-4LoRA 学习率比全参高一个量级8.2 常见坑r 设太大反而过拟合小数据集不要盲目上 r64量化后忘了prepare_model_for_kbit_training会导致梯度检查点报错bf16 不支持的显卡如老 Pascal要用 fp16否则 NF4 计算会崩适配器没单独保存save_pretrained在 PeftModel 上只存适配器九、总结LoRA 和 QLoRA 把大模型微调从云厂商专属变成了个人开发者可玩LoRA用低秩分解把可训练参数砍到 0.1%基座冻结、零推理延迟QLoRA再加 NF4 量化 双重量化 分页优化器单卡 24G 微调 7B-13B单卡 80G 微调 70B适配器只有几十 MB方便多任务存储和切换PEFT 库让接入成本降到 20 行代码下一篇预告量化部署是微调的最后一公里——如何用 vLLM 加载 QLoRA 适配器做高并发推理以及 GPTQ/AWQ 量化的取舍我们下期展开。往期回顾AI 推理优化系列—vLLM PagedAttention 解析显存利用率从 40% 提升到 90% 的秘密llama.cpp Q4 量化原理拆解10GB 显存跑 70B 模型的秘密Flink 状态后端选型RocksDB vs Heap 在百万级吞吐下的 5 倍性能差异GPTQ vs AWQ vs GGUF三大量化方案性能与精度横评关注获取更新本文属于「AI大模型大数据硬件编程」专栏点击关注不错过后续的 TensorRT-LLM 部署实战与量化推理系列。