🎯 目标:掌握大模型微调技术(PEFT全系列)、模型量化(8-bit/4-bit/QLoRA)、私有化部署,能够独立完成大模型的微调和上线。 📋 前置要求:阶段四(LLaMA架构理解、LangChain/RAG基础、PyTorch训练经验)
本阶段知识依赖图
阶段四基础(大模型架构理解 + 应用开发经验)
│
├──→ 微调概述 ──→ 为什么需要微调?微调 vs RAG
│
├──→ PEFT参数高效微调(⭐核心)
│ ├── BitFit ──→ 最简单:只调偏置
│ ├── Prompt Tuning ──→ 软提示学习
│ ├── P-Tuning ──→ 提示编码器
│ ├── Prefix Tuning ──→ 前缀调优
│ ├── LoRA ⭐ ──→ 低秩分解(最常用)
│ ├── IA3 ──→ 缩放激活值
│ └── PEFT进阶 ──→ 多适配器/融合
│
├──→ 模型量化
│ ├── 8-bit量化 ──→ LLM.int8()
│ ├── 4-bit量化 ──→ NF4/FP4
│ └── QLoRA ⭐ ──→ 量化+LoRA
│
└──→ 私有化部署
├── 硬件选型 ──→ GPU/显存计算
├── 云端部署 ──→ AutoDL/云服务器
└── 接口开发 ──→ FastAPI/vLLM
模块一:PEFT参数高效微调
为什么需要微调?——三种适配大模型的方法
类比:让一个大学生帮你做事
方法1:提示词工程 = 直接告诉他怎么做
"你是一位律师,请帮我审查这份合同"
→ 不需要培训,直接上手
→ 但他对你的公司业务不了解,可能遗漏细节
方法2:RAG = 给他参考资料
"先看这份合同模板和公司政策,然后帮我审查"
→ 不需要培训,有资料就能做
→ 但他理解资料的方式还是通用的,不是专业化的
方法3:微调 = 给他做专业培训
用公司1000份历史合同和审查报告来训练他
→ 需要时间和资源
→ 但他真正"学会"了公司的审查标准和风格
→ 推理时不需要额外资料,直接给出专业判断
三种方法的详细对比
方法 原理 数据需求 计算需求 效果 适用场景
──────────────────────────────────────────────────────────────────────────────
提示词工程 设计好的输入格式 无需训练 极低 一般 快速原型、简单任务
RAG 检索相关知识辅助生成 无需训练 低 好 知识问答、文档查询
微调 用领域数据训练模型参数 需要数据 高 最好 专业领域、格式要求
什么时候用微调?
1. 需要模型"学会"特定领域的知识(如医疗、法律、金融)
2. 需要模型输出固定格式(如始终输出JSON)
3. 需要模型遵循特定行为准则(如客服话术)
4. RAG效果不够好(需要更深层的理解)
5. 推理时不能有额外延迟(RAG需要检索时间)
全量微调的问题——为什么需要PEFT?
全量微调 = 更新模型的所有参数
LLaMA-7B 的参数量:70亿
全量微调的显存需求:
模型参数:7B × 4 bytes (float32) = 28 GB
梯度: 7B × 4 bytes = 28 GB
优化器状态(Adam):7B × 8 bytes = 56 GB
─────────────────────────────────────
总计:约 112 GB
→ 需要 2张 A100 80GB 显卡!成本极高!
PEFT的解决方案:
只训练 0.1%-1% 的参数,冻结其余 99%
LLaMA-7B + LoRA:只需训练约 400万参数
→ 显存需求降到 14-18 GB → 一张 RTX 3090 就够了!
BitFit——最简单的微调方法
核心思想:只调偏置,不调权重
一个Transformer层的参数:
权重参数(占99.5%):W_Q, W_K, W_V, W_O, W₁, W₂
偏置参数(占0.5%):b_Q, b_K, b_V, b_O, b₁, b₂
BitFit:冻结所有权重,只训练偏置
→ 只有 0.5% 的参数参与训练
→ 效果在某些任务上能达到全量微调的 90%!
为什么只调偏置也有效?
偏置虽然少,但它控制了每个神经元的"激活阈值"
调整偏置 = 调整"什么时候激活,什么时候不激活"
→ 对于分类等任务,调整激活阈值就足够了
from transformers import AutoModelForSequenceClassification
model = AutoModelForSequenceClassification.from_pretrained("bert-base-chinese", num_labels=2)
# 冻结所有参数
for param in model.parameters():
param.requires_grad = False
# 只解冻偏置参数
for name, param in model.named_parameters():
if "bias" in name:
param.requires_grad = True
# 查看可训练参数量
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
total = sum(p.numel() for p in model.parameters())
print(f"可训练参数:{trainable:,} / {total:,} = {trainable/total:.2%}")
# 可训练参数:约89,000 / 102,000,000 = 0.09%
Prompt Tuning——软提示学习
核心思想:不改模型,只加"前缀"
类比:在演员上台前,给他一个"提词器"
原始输入:[CLS] 今天 天气 真 好 [SEP]
Prompt Tuning:[p₁ p₂ p₃ p₄] + [CLS] 今天 天气 真 好 [SEP]
↑
这4个向量是可学习的(不是真实的词!)
它们的作用是"引导"模型的注意力和行为
训练时:只更新 p₁, p₂, p₃, p₄ 这4个向量
推理时:不同任务用不同的前缀 → 同一个模型可以做不同任务!
Prompt Tuning的优势:
1. 存储效率极高:每个任务只需保存4-20个向量(几KB!)
2. 多任务服务:一个基础模型 + 多个Prompt = 多个任务
3. 不改变模型结构:可以随时切换任务
4. 效果在大模型上很好(10B+参数时接近全量微调)
局限:
- 在小模型上效果一般
- 对初始化敏感(不同的初始值效果差异大)
- 不如LoRA在大多数任务上的效果
P-Tuning——可学习的提示编码器
Prompt Tuning的问题:p₁, p₂, p₃ 是独立的可学习向量
→ 它们之间没有"联系",初始化敏感
P-Tuning的改进:用一个小的LSTM或MLP来生成这些向量
→ p₁, p₂, p₃ 由编码器生成,有相互依赖关系
→ 初始化更稳定,效果更好
P-Tuning v2:在每一层都添加可训练的前缀(而不只是输入层)
→ 效果更接近全量微调
Prefix Tuning——前缀调优
核心思想:在每一层的K和V前面都加"前缀"
原始Self-Attention:
Q = X·W_Q, K = X·W_K, V = X·W_V
Prefix Tuning:
Q = X·W_Q
K = [P_K; X·W_K] ← P_K是可学习的前缀Key
V = [P_V; X·W_V] ← P_V是可学习的前缀Value
类比:
原始Attention = 学生看黑板上的内容
Prefix Tuning = 学生同时看黑板和老师举的提示牌
→ 提示牌影响了学生"关注什么"
Prefix Tuning vs Prompt Tuning:
Prompt Tuning:只在输入层添加前缀 → 影响力有限
Prefix Tuning:在每一层的K和V都添加前缀 → 影响力更大
类比:
Prompt Tuning = 只在教室门口放一块提示牌
Prefix Tuning = 在教室的每一面墙都放提示牌 → 学生处处受影响
LoRA——低秩适配 ⭐⭐⭐ 最重要的微调方法
核心思想——学习权重的"变化量"
类比:学画画
全量微调 = 从零开始画一幅新画
需要重新学所有技法,工作量巨大
LoRA = 在原画上做小幅修改
原画已经很好了,只需要"微调"一些细节
比如把天空的颜色调蓝一点,把树叶加几笔
LoRA的关键洞察:
微调前的权重 W(768×768)已经学到了大量通用知识
微调后的权重 W' = W + ΔW
ΔW 是"需要改的部分",它远比 W 小(低秩)
→ 可以用两个小矩阵 A 和 B 来近似:ΔW ≈ A × B
数学推导
原始权重 W:(768, 768) — 589,824 个参数
LoRA分解:
ΔW = A × B
A:(768, r) — r通常为8或16
B:(r, 768)
当 r=8 时:
A:(768, 8) = 6,144 个参数
B:(8, 768) = 6,144 个参数
总计:12,288 个参数
参数减少比例:12,288 / 589,824 = 2.1% → 减少了98%!
推理时:
W' = W + A × B
可以把A×B合并回W → 推理时没有额外开销!
为什么低秩假设成立?
研究发现:大模型微调时,权重的变化量ΔW确实具有低秩特性
直觉理解:
预训练模型已经学到了"语言的通用知识"(语法、语义、世界知识)
微调只是教它"新的任务特定知识"(如医疗诊断、法律审查)
通用知识的信息量 >> 任务特定知识的信息量
→ ΔW 的"信息量"远小于 W → 低秩假设成立
实验证据:
Aghajanyan et al. (2021) 发现:
预训练模型的内在维度(intrinsic dimensionality)远小于参数量
一个768维的模型,内在维度可能只有几维
→ 用几维的低秩矩阵就能有效微调
LoRA的实现
from peft import LoraConfig, get_peft_model, TaskType
from transformers import AutoModelForCausalLM
# 1. 加载预训练模型
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b")
# 2. 配置LoRA
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM, # 任务类型
r=8, # 秩(rank):越小参数越少
lora_alpha=32, # 缩放因子:通常设为r的2-4倍
lora_dropout=0.1, # Dropout:防止过拟合
target_modules=["q_proj", "v_proj", "k_proj", "o_proj"], # 对哪些层应用LoRA
)
# 3. 创建LoRA模型
model = get_peft_model(model, lora_config)
# 4. 查看参数量
model.print_trainable_parameters()
# trainable params: 4,194,304 || all params: 6,742,609,920 || trainable%: 0.06%
# → 只有0.06%的参数需要训练!
# 5. 正常训练(和普通模型一样)
from transformers import Trainer, TrainingArguments
trainer = Trainer(model=model, args=training_args, ...)
trainer.train()
# 6. 保存LoRA权重(只有几MB!)
model.save_pretrained("./my-lora")
LoRA的关键参数——如何选择?
r(秩)——控制"容量":
r=4:参数最少,适合简单任务(如情感分类)
r=8:默认值,大多数任务够用
r=16-64:复杂任务(如代码生成、长文本摘要)
r=128+:接近全量微调效果
类比:r就像"修改画作时用的画笔粗细"
r=4 = 细画笔,只能做精细的小改动
r=64 = 粗画笔,可以做大面积的修改
lora_alpha(缩放因子)——控制"影响力":
alpha/r = LoRA对原始权重的影响程度
alpha=32, r=8 → 影响系数 = 32/8 = 4
alpha=16, r=8 → 影响系数 = 16/8 = 2
通常设为 r 的2-4倍
target_modules——应用到哪些层:
最常见:["q_proj", "v_proj"](只改Q和V)
更强:["q_proj", "v_proj", "k_proj", "o_proj"](改所有Attention层)
最强:加上FFN层 ["gate_proj", "up_proj", "down_proj"]
层越多 → 参数越多 → 效果越好 → 但显存需求也越大
LoRA模型融合(Merge)
# 训练完成后,可以把LoRA权重合并回原始模型
merged_model = model.merge_and_unload()
# 合并后的模型和原始模型结构完全一样
# 不需要额外的LoRA组件 → 可以像普通模型一样部署
merged_model.save_pretrained("./merged-model")
# 合并的数学原理:
# W' = W + A × B
# 把A×B的结果直接加到W上,得到新的W'
# 推理时只需要W',不需要单独存A和B
IA3——Infused Adapter by Inhibiting and Amplifying Inner Activations
IA3的核心思想:不添加新参数,只用可学习的向量来"缩放"现有激活值
对K、V和FFN的激活值分别乘以一个可学习的向量:
K' = l_k ⊙ K (l_k是可学习的缩放向量)
V' = l_v ⊙ V
FFN' = l_ff ⊙ FFN(x)
参数量:只需要3个向量(比LoRA还少!)
如果d_model=4096,IA3只需要 3 × 4096 = 12,288 个参数
效果:在某些任务上接近LoRA
局限:表达能力不如LoRA(只能"缩放",不能"变换")
PEFT进阶操作——多适配器管理
from peft import PeftModel
# 加载基础模型 + LoRA适配器
base_model = AutoModelForCausalLM.from_pretrained("base-model")
model = PeftModel.from_pretrained(base_model, "./my-lora-task-a")
# 切换不同适配器
model.set_adapter("lora-task-a") # 使用任务A的LoRA
model.set_adapter("lora-task-b") # 切换到任务B的LoRA
# 禁用适配器(获取原始模型输出)
with model.disable_adapter():
output = model(input) # 使用原始模型
# 合并多个适配器
model.add_weighted_adapter(
adapters=["lora-a", "lora-b"],
weights=[0.7, 0.3], # 70%任务A + 30%任务B
adapter_name="merged"
)
PEFT方法总结对比:
方法 可训练参数占比 原理 效果排名
──────────────────────────────────────────────────────────────
BitFit 0.1% 只调偏置 低
Prompt Tuning <0.01% 输入层加可学习向量 中低
P-Tuning v2 0.1-1% 每层加可学习前缀 中
Prefix Tuning 0.1-1% 每层K/V加前缀 中
LoRA 0.1-1% 低秩分解权重变化量 高 ⭐
IA3 <0.01% 缩放激活值 中
QLoRA 0.1-1% 量化+LoRA 高 ⭐
实际选择:
- 大多数任务 → LoRA
- 显存非常有限 → QLoRA
- 需要快速验证 → BitFit
- 需要多任务服务 → Prompt Tuning
模块二:模型量化——让大模型在消费级GPU上运行
为什么需要量化?
类比:图片压缩
原始图片:10MB的高清照片(float32模型参数)
压缩后:1MB的JPEG照片(int8量化参数)
虽然JPEG有轻微失真,但肉眼几乎看不出来。
同样,int8量化的模型精度损失很小(通常<1%),但体积减小4倍!
LLaMA-7B 的存储需求:
float32:7B × 4 bytes = 28 GB → 需要 A100
float16:7B × 2 bytes = 14 GB → 需要 RTX 4090
int8: 7B × 1 byte = 7 GB → RTX 3070 就能跑
int4: 7B × 0.5 byte = 3.5 GB → RTX 3060 就能跑
量化 = 降低精度 → 减少显存 → 能用更便宜的GPU → 更多人能用大模型!
8-bit量化——LLM.int8()
核心挑战:离群值问题
问题:直接把float32转为int8会导致精度严重下降
原因:大模型的激活值中存在"离群值"(outlier)
正常激活值:[-0.5, 0.3, -0.2, 0.1, 0.4] → 范围小,量化误差小
存在离群值:[-0.5, 0.3, -0.2, 100.0, 0.4] → 为了容纳100.0,其他值的精度严重损失
类比:
正常情况:一个班的成绩在60-100分之间
存在离群值:一个班的成绩在60-100之间,但有一个学生考了10000分
→ 如果用同一个评分标准,其他学生的成绩差异就看不出来了
LLM.int8()的解决方案——混合精度分解
步骤:
1. 找出激活值中的离群值(绝对值 > 6 的通道)
2. 离群值用 float16 计算(保持精度)
3. 非离群值用 int8 计算(节省显存)
4. 合并两部分结果
效果:几乎无损!模型性能下降 < 1%
显存:减半(float16 → 混合 int8/float16)
类比:
把那个考10000分的学生单独处理(用float16)
其他学生用正常标准评分(用int8)
→ 所有学生的成绩都能准确表示
from transformers import BitsAndBytesConfig, AutoModelForCausalLM
# 8-bit量化配置
bnb_config = BitsAndBytesConfig(load_in_8bit=True)
# 加载8-bit模型
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b",
quantization_config=bnb_config,
device_map="auto"
)
4-bit量化——更激进的压缩
两种4-bit格式:
- FP4:标准的4-bit浮点数
- NF4(Normal Float 4):专为正态分布设计
为什么NF4更好?
大模型的权重分布近似正态分布(大部分值在0附近,极少数值很大)
NF4的量化区间是根据正态分布设计的
→ 在正态分布数据上,NF4的量化误差最小
类比:
FP4 = 均匀划分的尺子(每个刻度间距相同)
NF4 = 根据数据分布调整的尺子(中间刻度密,两边刻度疏)
→ 对正态分布数据,NF4的测量精度更高
bnb_config = BitsAndBytesConfig(
load_in_4bit=True, # 启用4-bit量化
bnb_4bit_quant_type="nf4", # 使用NF4格式
bnb_4bit_compute_dtype=torch.bfloat16, # 计算时用bf16
bnb_4bit_use_double_quant=True, # 二次量化(进一步压缩)
)
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b",
quantization_config=bnb_config,
device_map="auto"
)
QLoRA——量化 + LoRA = 最高效的微调方案 ⭐
QLoRA的核心思想:先压缩,再微调
类比:在一个压缩过的画布上做小幅修改
步骤:
1. 用4-bit量化加载大模型(压缩画布 → 节省空间)
2. 在量化模型上添加LoRA适配器(准备小幅修改的工具)
3. 只训练LoRA参数(在压缩画布上做精细修改)
显存对比(LLaMA-7B微调):
全量微调 float32:~112 GB → 需要 2张 A100 80GB
全量微调 float16:~56 GB → 需要 1张 A100 80GB
LoRA float16: ~18 GB → 需要 1张 RTX 4090
QLoRA (4-bit): ~6 GB → 1张 RTX 3060 就够了!
from peft import prepare_model_for_kbit_training, LoraConfig, get_peft_model
from transformers import BitsAndBytesConfig, AutoModelForCausalLM
# 1. 4-bit量化加载
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
)
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b",
quantization_config=bnb_config,
)
# 2. 准备量化模型用于训练
model = prepare_model_for_kbit_training(model)
# 3. 添加LoRA
lora_config = LoraConfig(r=16, lora_alpha=32, target_modules=["q_proj","v_proj"])
model = get_peft_model(model, lora_config)
# 4. 正常训练
trainer = Trainer(model=model, args=training_args, train_dataset=dataset)
trainer.train()
模块三:大模型私有化部署
GPU显存需求计算——必须掌握的公式
推理显存(模型加载):
显存 ≈ 参数量 × 每参数字节数
LLaMA-7B (float16):7B × 2 bytes = 14 GB
LLaMA-13B (float16):13B × 2 bytes = 26 GB
LLaMA-70B (4-bit): 70B × 0.5 bytes = 35 GB
训练显存(远大于推理):
显存 ≈ 参数量 × (模型精度 + 梯度 + 优化器状态)
≈ 参数量 × (2 + 2 + 4 + 4) bytes ← Adam优化器
≈ 参数量 × 12 bytes (float16混合精度)
LLaMA-7B:7B × 12 bytes ≈ 84 GB
LLaMA-7B + LoRA:7B × 2 + 4M × 4 ≈ 14 GB
LLaMA-7B + QLoRA:~6 GB
选择GPU的快速参考:
RTX 3060 (12GB):可以运行7B-4bit推理,QLoRA微调7B
RTX 3090 (24GB):可以运行7B-fp16推理,LoRA微调7B
RTX 4090 (24GB):同3090但更快
A100 (40GB):可以运行13B-fp16推理,LoRA微调13B
A100 (80GB):可以运行70B-4bit推理
云端部署实践
# 使用ModelScope下载模型(国内镜像,速度快)
from modelscope import snapshot_download
model_dir = snapshot_download('LLM-Research/Meta-Llama-3-8B-Instruct')
# 或使用HuggingFace
from huggingface_hub import snapshot_download
model_dir = snapshot_download('meta-llama/Llama-2-7b-chat-hf')
对外接口开发——FastAPI
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
app = FastAPI()
# 加载模型
model = AutoModelForCausalLM.from_pretrained("./model", device_map="auto")
tokenizer = AutoTokenizer.from_pretrained("./model")
@app.post("/chat")
async def chat(prompt: str, max_tokens: int = 512):
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
with torch.no_grad():
outputs = model.generate(**inputs, max_new_tokens=max_tokens)
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
return {"response": response}
@app.post("/chat/stream")
async def chat_stream(prompt: str):
async def generate():
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
# 流式生成逻辑...
yield token
return StreamingResponse(generate(), media_type="text/event-stream")
📝 自测题
- 微调对比:用"让大学生做事"的类比,解释提示词工程、RAG、微调的区别
- LoRA:用"画画修改"的类比解释LoRA的低秩假设,为什么ΔW可以用A×B来近似?
- LoRA参数:r、alpha、target_modules分别如何影响微调效果?
- QLoRA:解释QLoRA的工作流程,为什么4-bit量化后仍能微调?
- 量化:解释LLM.int8()的混合精度分解原理,为什么需要处理离群值?
- NF4:为什么NF4比FP4更适合量化大模型?
- 显存计算:计算LLaMA-13B使用QLoRA微调需要多少显存
- PEFT对比:BitFit、Prompt Tuning、Prefix Tuning、LoRA、IA3各自的优缺点
- 部署:设计一个大模型API服务的架构方案
- 综合:给定一张RTX 3090(24GB),能微调多大的模型?用什么方法?
...