01 - 高效微调技术(PEFT)¶
⚠️ 时效性说明:本章涉及前沿模型/价格/榜单等信息,可能随版本快速变化;请以论文原文、官方发布页和 API 文档为准。
📌 定位说明:本章侧重PEFT方法论的原理与研究进展。 - 📖 微调应用实战请参考 LLM应用/09-大模型微调技术 - 📖 LoRA/QLoRA实战请参考 LLM应用/10-LoRA与QLoRA
学习目标:理解并掌握LoRA等参数高效微调技术,能够在有限显存下微调大模型。
1. 为什么需要高效微调?¶
1.1 全量微调的问题¶
假设你要微调一个7B参数的LLaMA模型:
模型参数: 7B × 4字节 (FP32) = 28 GB
梯度: 7B × 4字节 = 28 GB
优化器状态 (Adam): 7B × 8字节 = 56 GB
激活值: ~10-20 GB
----------------------------------------
总计: ~120+ GB 显存!
问题: - 需要A100 80GB × 2 或 A100 40GB × 4 - 成本极高 - 训练时间长
1.2 高效微调的核心思想¶
关键洞察:预训练模型已经学到了丰富的知识,微调时不需要改变所有参数。
2. LoRA:低秩适配¶
2.1 核心思想¶
论文: LoRA: Low-Rank Adaptation of Large Language Models
直觉:权重矩阵的更新是低秩的
预训练权重: W₀ ∈ ℝ^(d×k)
微调后的权重: W = W₀ + ΔW
LoRA假设: ΔW 是低秩的,即 ΔW = BA
其中: B ∈ ℝ^(d×r), A ∈ ℝ^(r×k), r << min(d, k)
为什么低秩有效?(数学直觉)
核心理论依据来自 Aghajanyan et al. (2020) 的内在维度(Intrinsic Dimensionality)发现:
实验发现:
对于一个 d 维参数空间的预训练模型,存在一个远小于 d 的
内在维度 d_int,使得在该低维子空间内优化即可达到全量微调
90% 的性能。
例如:RoBERTa-Large 有 355M 参数,但微调的内在维度仅约
几百到几千维,压缩比高达数万倍。
数学直觉:
1. 权重矩阵的奇异值分解:W = UΣV^T
预训练后,Σ 中大部分奇异值很小 → 权重矩阵本身就近似低秩
2. 微调的增量 ΔW 比 W 更加低秩:
- 微调只是"微调",变化幅度小
- 变化主要集中在少数关键方向(对应最大奇异值的方向)
- 理论上 rank(ΔW) << rank(W)
3. 经验验证(原论文):
r=4 即可在大多数NLU任务上匹配全量微调
r=1 在某些简单任务上也足够
直觉类比:
想象一个已经训练好的画家(预训练模型),让他学画某种新风格
(微调)。他不需要重新学所有绘画技巧(全量参数),只需调整
少数几个风格参数(色调、笔触力度等)—— 这就是"低秩"调整。
2.2 数学原理¶
标准前向传播¶
LoRA前向传播¶
初始化策略¶
2.3 为什么能减少显存?¶
假设: d = 4096, k = 4096, r = 16
全量微调:
可训练参数: 4096 × 4096 = 16,777,216
LoRA:
A: 4096 × 16 = 65,536
B: 4096 × 16 = 65,536
总计: 131,072
节省: 16,777,216 / 131,072 = 128倍!
2.4 应用到Transformer¶
LoRA通常应用于以下权重矩阵:
Transformer中的注意力权重:
- W_q (Query投影)
- W_k (Key投影)
- W_v (Value投影)
- W_o (输出投影)
以及FFN中的权重(可选)
为什么不应用于所有层? - 注意力层包含了大部分任务相关知识 - 实验表明只适配注意力层已经足够
3. LoRA的实现¶
3.1 基础实现¶
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
class LoRALayer(nn.Module):
"""
LoRA层的基本实现
"""
def __init__(self, in_features, out_features, rank=16, lora_alpha=1):
super().__init__() # super()调用父类方法
self.rank = rank
self.lora_alpha = lora_alpha
self.scaling = lora_alpha / rank
# 低秩矩阵
# 注:论文中 ΔW = BA,A ∈ ℝ^(r×k)(降维),B ∈ ℝ^(d×r)(升维)
# 代码中存储为转置形式以便直接 x @ lora_A @ lora_B 计算
# lora_A 对应 A^T ∈ ℝ^(k×r),lora_B 对应 B^T ∈ ℝ^(r×d)
self.lora_A = nn.Parameter(torch.zeros(in_features, rank)) # [k, r]
self.lora_B = nn.Parameter(torch.zeros(rank, out_features)) # [r, d]
# 初始化:A随机初始化,B零初始化 → 保证初始ΔW=0
nn.init.kaiming_uniform_(self.lora_A, a=math.sqrt(5))
nn.init.zeros_(self.lora_B)
def forward(self, x):
"""
x: [batch, seq_len, in_features]
返回: [batch, seq_len, out_features]
"""
# 计算 ΔWx = BAx
# 等价于 x @ A^T @ B^T(使用存储的转置形式)
return (x @ self.lora_A @ self.lora_B) * self.scaling
class LinearWithLoRA(nn.Module):
"""
带LoRA的线性层
"""
def __init__(self, linear_layer, rank=16, lora_alpha=1):
super().__init__()
# 原始线性层(冻结)
self.linear = linear_layer
self.linear.weight.requires_grad = False
if self.linear.bias is not None:
self.linear.bias.requires_grad = False
# LoRA层
self.lora = LoRALayer(
linear_layer.in_features,
linear_layer.out_features,
rank=rank,
lora_alpha=lora_alpha
)
def forward(self, x):
# 原始输出 + LoRA输出
return self.linear(x) + self.lora(x)
3.2 应用到预训练模型¶
def inject_lora_to_model(model, target_modules, rank=16, lora_alpha=1):
"""
将LoRA注入到模型的指定模块
Args:
model: 预训练模型
target_modules: 要替换的模块名列表,如["q_proj", "v_proj"]
rank: LoRA秩
lora_alpha: LoRA alpha参数
"""
for name, module in model.named_modules():
# 检查是否是目标模块
if any(target in name for target in target_modules): # any()任一为True则返回True
if isinstance(module, nn.Linear): # isinstance检查类型
# 获取父模块和属性名
parent_name = '.'.join(name.split('.')[:-1])
child_name = name.split('.')[-1]
if parent_name == '':
parent = model
else:
parent = model.get_submodule(parent_name)
# 替换为带LoRA的线性层
lora_layer = LinearWithLoRA(module, rank=rank, lora_alpha=lora_alpha)
setattr(parent, child_name, lora_layer) # setattr动态设置对象属性
print(f"注入LoRA到: {name}")
return model
# 使用示例
from transformers import AutoModelForCausalLM
# 加载预训练模型
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-hf")
# 注入LoRA
model = inject_lora_to_model(
model,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
rank=16,
lora_alpha=32
)
# 打印可训练参数
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
total_params = sum(p.numel() for p in model.parameters())
print(f"可训练参数: {trainable_params:,} ({100*trainable_params/total_params:.2f}%)")
3.3 超参数选择¶
Rank ®¶
r=1: 极简适配,适合简单任务
r=4: 小任务,快速实验
r=8: 中等复杂度任务
r=16: 复杂任务(最常用)
r=32: 非常复杂的任务
r=64: 极少使用
经验法则: 从r=8或16开始,根据效果调整
Alpha (α)¶
alpha控制LoRA的缩放: scaling = alpha / rank
常见设置:
- alpha = 2 * rank
- alpha = rank
alpha越大,LoRA的影响越大
Dropout¶
4. LoRA的变体¶
4.1 QLoRA:量化 + LoRA¶
问题:即使使用LoRA,7B模型加载还是需要28GB显存(FP32)或14GB(FP16)
解决方案:4-bit量化 + LoRA
模型加载: 7B × 0.5字节 (4-bit) = 3.5 GB
LoRA参数: 可忽略
激活值: ~2-4 GB
----------------------------------------
总计: ~6-8 GB 显存!
可以在消费级GPU(RTX 3090/4090)上微调7B模型!
关键技术: - 4-bit NormalFloat (NF4):针对正态分布权重的最优4-bit量化 - 双量化:量化量化常数,进一步节省显存 - 分页优化器:使用CPU内存作为显存的页交换
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
# 4-bit量化配置
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True, # 双量化
bnb_4bit_quant_type="nf4", # NF4量化
bnb_4bit_compute_dtype=torch.bfloat16
)
# 加载量化模型
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b-hf",
quantization_config=bnb_config,
device_map="auto"
)
# 准备模型用于训练
model = prepare_model_for_kbit_training(model)
# 配置LoRA
lora_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
# 应用LoRA
model = get_peft_model(model, lora_config)
4.2 DoRA:权重分解低秩适配¶
问题:LoRA只学习方向变化,忽略了幅度变化
解决方案:将权重分解为幅度和方向,分别适配
权重分解公式:
其中: - \(V = W_0 + \Delta V\):更新后的权重向量 - \(m \in \mathbb{R}^{1 \times k}\):可学习的幅度向量 - \(\|V\|\):列归一化(每列的L2范数)
DoRA的数学分解:
其中: - \(B \in \mathbb{R}^{d \times r}\):LoRA的B矩阵 - \(A \in \mathbb{R}^{r \times k}\):LoRA的A矩阵 - \(r \ll \min(d, k)\):低秩维度
训练过程: 1. 初始化:\(m\) 初始化为 \(W_0\) 各列的L2范数,\(B\) 和 \(A\) 随机初始化 2. 前向传播:计算 \(W = m \cdot (W_0 + BA) / \|W_0 + BA\|\) 3. 梯度更新:同时更新 \(m\)、\(B\)、\(A\)
代码示例:
from peft import LoraConfig, get_peft_model
import torch.nn as nn
class DoRALayer(nn.Module):
"""DoRA层的简化实现"""
def __init__(self, weight, rank=8):
super().__init__()
self.weight = nn.Parameter(weight.clone())
out_features, in_features = weight.shape
# 幅度向量
self.magnitude = nn.Parameter(weight.norm(dim=0, keepdim=True))
# LoRA矩阵
self.lora_B = nn.Parameter(torch.zeros(out_features, rank))
self.lora_A = nn.Parameter(torch.randn(rank, in_features) * 0.01)
def forward(self, x):
# 计算更新后的权重
delta_W = self.lora_B @ self.lora_A
W_updated = self.weight + delta_W
# 归一化并应用幅度
W_normalized = W_updated / W_updated.norm(dim=0, keepdim=True)
W_dora = self.magnitude * W_normalized
return x @ W_dora.T
效果:比LoRA更稳定,效果更好,在多个基准测试上提升1-2%
4.3 AdaLoRA:自适应秩分配¶
问题:不同层需要不同的秩,固定秩不是最优的
解决方案:根据重要性动态分配秩
动态秩分配算法:
- 重要性评分:对每个奇异值计算重要性分数
其中: - \(u_i, v_i\):第 \(i\) 个奇异向量 - \(\nabla_L(W)\):损失对权重的梯度 - \(S_i\):第 \(i\) 个奇异值的重要性分数
- 奇异值剪枝:根据重要性分数剪枝
其中: - \(\tau_t\):第 \(t\) 步的剪枝阈值 - \(r_{\max}\):初始最大秩
- 预算分配:在总参数预算约束下分配秩
算法流程:
输入: 预训练模型W₀, 目标参数预算B, 初始秩r_max
输出: 适配后的模型
1. 初始化: 为每层设置初始秩r_max
2. For t = 1 to T (训练步数):
a. 前向传播计算损失L
b. 计算每层奇异值的重要性分数S
c. 根据预算B和重要性分数调整每层秩
d. 剪枝不重要的奇异值
e. 更新保留的LoRA参数
3. 返回适配后的模型
代码示例:
from peft import AdaLoraConfig, get_peft_model
# 配置AdaLoRA
adalora_config = AdaLoraConfig(
init_r=12, # 初始秩
target_r=4, # 目标秩
beta1=0.85, # 奇异值保留比例
beta2=0.85, # 奇异值保留比例
tinit=200, # 开始剪枝的步数
tfinal=1000, # 停止剪枝的步数
deltaT=10, # 剪枝间隔
lora_alpha=32,
lora_dropout=0.1,
target_modules=["q_proj", "v_proj", "k_proj", "o_proj"],
task_type="CAUSAL_LM"
)
# 应用AdaLoRA
model = get_peft_model(model, adalora_config)
优势: - 自动为重要层分配更高秩 - 减少不重要层的参数浪费 - 在相同参数预算下效果优于固定秩LoRA
5. 其他高效微调方法¶
5.1 Prefix Tuning¶
思想:在输入前添加可训练的前缀向量
输入: [x1, x2, x3, ..., xn]
变为: [p1, p2, ..., pk, x1, x2, x3, ..., xn]
其中 p1...pk 是可训练的prefix向量
优点: - 参数量极小(prefix长度 × 维度 × 层数) - 适合生成任务
缺点: - 需要修改模型输入 - 分类任务效果一般
5.2 Prompt Tuning¶
思想:只在输入层添加可训练的soft prompt
输入: [p1, p2, ..., pk, x1, x2, ..., xn]
与Prefix Tuning的区别:
- Prompt Tuning只在输入层
- Prefix Tuning在每一层
优点: - 参数量最小(通常 < 0.01%) - 实现简单
缺点: - 需要较大的prompt长度(50-100) - 小模型上效果差
5.3 Adapter¶
思想:在Transformer层中插入小型适配器模块
Adapter结构:
Input -> Down-project (d->r) -> ReLU -> Up-project (r->d) -> Output
+----------------- Residual ----------------------+
通常放在:
- 注意力之后
- FFN之后
优点: - 推理时可以移除Adapter,恢复原始模型 - 适合多任务(每个任务一个Adapter)
缺点: - 推理时有额外开销
5.4 方法对比¶
| 方法 | 可训练参数 | 存储开销 | 推理开销 | 适用场景 |
|---|---|---|---|---|
| Full Fine-tuning | 100% | 100% | 无 | 充足资源 |
| LoRA | 0.1%-1% | 小 | 可合并 | 通用 |
| QLoRA | 0.1%-1% | 极小 | 可合并 | 资源受限 |
| Prefix Tuning | 0.1% | 小 | 无 | 生成任务 |
| Prompt Tuning | <0.01% | 极小 | 无 | 大模型分类 |
| Adapter | 0.5%-5% | 小 | 有 | 多任务 |
6. 实践指南¶
6.1 选择合适的微调方法¶
场景1: 消费级GPU(8-16GB)
-> QLoRA (4-bit + LoRA)
场景2: 专业级GPU(24-48GB)
-> LoRA (FP16/BF16)
场景3: 多任务场景
-> LoRA 或 Adapter
场景4: 极致节省参数
-> Prompt Tuning (模型>10B)
场景5: 需要推理速度
-> LoRA (可合并权重)
6.2 训练技巧¶
学习率¶
批量大小¶
LoRA对batch size不敏感,可以使用梯度累积
batch_size=1 + gradient_accumulation_steps=32
等效于 batch_size=32
训练轮数¶
6.3 评估与调试¶
检查LoRA是否正确应用¶
def print_trainable_parameters(model):
"""打印可训练参数"""
trainable_params = 0
all_params = 0
for name, param in model.named_parameters():
all_params += param.numel()
if param.requires_grad:
trainable_params += param.numel()
print(f"可训练: {name}: {param.numel()}")
print(f"\n可训练参数: {trainable_params:,} || "
f"总参数: {all_params:,} || "
f"比例: {100*trainable_params/all_params:.2f}%")
# 使用
print_trainable_parameters(model)
检查训练是否生效¶
# 训练前保存LoRA权重
lora_weights_before = {name: param.clone() for name, param in model.named_parameters() if param.requires_grad}
# 训练...
# 训练后比较
lora_weights_after = {name: param.clone() for name, param in model.named_parameters() if param.requires_grad}
for name in lora_weights_before:
diff = (lora_weights_after[name] - lora_weights_before[name]).abs().mean()
print(f"{name} 平均变化: {diff:.6f}")
7. 完整实践:使用 PEFT 库微调 LLaMA¶
📌 从零实现 LoRA(不依赖 peft 库)请参考 06-LoRA从零实现
7.1 使用 HuggingFace PEFT + SFTTrainer 完整流程¶
"""
QLoRA微调LLaMA的完整可运行代码
需要: pip install transformers peft bitsandbytes trl datasets
硬件: RTX 3090/4090 (24GB VRAM) 即可运行 7B 模型
"""
import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
TrainingArguments,
)
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from trl import SFTTrainer
from datasets import load_dataset
# ---------- Step 1: 量化加载模型 ----------
model_name = "meta-llama/Llama-3.1-8B"
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
)
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map="auto",
torch_dtype=torch.bfloat16,
)
model = prepare_model_for_kbit_training(model)
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
# ---------- Step 2: 配置 LoRA ----------
lora_config = LoraConfig(
r=16, # 秩
lora_alpha=32, # alpha = 2 * r 是常见选择
target_modules=[ # LLaMA 的注意力和 FFN 权重
"q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj",
],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# trainable params: 83,886,080 || all params: 8,114,212,864 || trainable%: 1.03%
# ---------- Step 3: 准备数据集 ----------
# 使用 Alpaca 格式的数据集作为示例
dataset = load_dataset("tatsu-lab/alpaca", split="train[:5000]")
def format_instruction(example):
"""将 Alpaca 格式转为对话模板"""
if example.get("input", ""):
text = (
f"### Instruction:\n{example['instruction']}\n\n"
f"### Input:\n{example['input']}\n\n"
f"### Response:\n{example['output']}"
)
else:
text = (
f"### Instruction:\n{example['instruction']}\n\n"
f"### Response:\n{example['output']}"
)
return {"text": text}
dataset = dataset.map(format_instruction)
# ---------- Step 4: 训练配置 ----------
training_args = TrainingArguments(
output_dir="./lora-llama-output",
num_train_epochs=1,
per_device_train_batch_size=4,
gradient_accumulation_steps=4, # 有效batch_size = 4 * 4 = 16
learning_rate=2e-4, # LoRA 学习率通常比全量微调高10倍
lr_scheduler_type="cosine",
warmup_ratio=0.03,
bf16=True, # 使用 bfloat16 混合精度
logging_steps=10,
save_strategy="steps",
save_steps=200,
max_grad_norm=0.3, # 梯度裁剪
optim="paged_adamw_8bit", # 分页优化器,节省显存
)
# ---------- Step 5: 使用 SFTTrainer 训练 ----------
# 注意:TRL v0.8+ 废弃了 dataset_text_field 参数,
# 需要将数据集预处理为包含 "text" 列或使用 formatting_func
trainer = SFTTrainer(
model=model,
train_dataset=dataset,
args=training_args,
processing_class=tokenizer, # TRL v0.12+: tokenizer 参数更名为 processing_class
max_seq_length=512,
packing=True, # 将短样本打包,提高GPU利用率
# 如果数据集列名不是 "text",使用 formatting_func:
# formatting_func=lambda examples: [ex["text"] for ex in examples],
)
trainer.train()
# ---------- Step 6: 保存 LoRA 权重 ----------
model.save_pretrained("./lora-llama-adapter")
# 只保存 LoRA 权重(~100MB vs 完整模型 ~16GB)
# ---------- Step 7: 加载和推理 ----------
from peft import PeftModel
# 重新加载基础模型
base_model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map="auto",
)
# 加载 LoRA 权重
model = PeftModel.from_pretrained(base_model, "./lora-llama-adapter")
# 推理
prompt = "### Instruction:\n用Python实现快速排序\n\n### Response:\n"
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
outputs = model.generate(**inputs, max_new_tokens=256, temperature=0.7)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
# ---------- Step 8: 合并权重(可选,部署用)----------
merged_model = model.merge_and_unload() # 将 LoRA 合并回基础模型
merged_model.save_pretrained("./llama-merged")
tokenizer.save_pretrained("./llama-merged")
# 现在 merged 模型可以直接用于推理,无需 peft 库
7.2 多 LoRA 适配器管理¶
一个基座模型可以加载不同的 LoRA 适配器,适配不同任务:
from peft import PeftModel
# 加载基础模型(一次)
base_model = AutoModelForCausalLM.from_pretrained(model_name, device_map="auto")
# 加载代码助手 LoRA
code_model = PeftModel.from_pretrained(base_model, "./lora-code-assistant")
# 加载翻译器 LoRA(同一个基座)
translation_model = PeftModel.from_pretrained(base_model, "./lora-translator")
# 运行时切换适配器(如果使用同一个 PeftModel)
model = PeftModel.from_pretrained(base_model, "./lora-code-assistant")
model.load_adapter("./lora-translator", adapter_name="translator")
model.set_adapter("default") # 使用代码助手
model.set_adapter("translator") # 切换到翻译器
# 禁用所有 LoRA(回到原始模型)
with model.disable_adapter():
outputs = model.generate(...) # 使用原始基础模型
7.3 LoRA 效果验证清单¶
"""微调效果诊断脚本"""
def diagnose_lora_training(model, training_log):
"""训练后诊断清单"""
# 1. 检查可训练参数比例
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
total = sum(p.numel() for p in model.parameters())
ratio = trainable / total * 100
print(f"[1] 可训练参数: {trainable:,} / {total:,} ({ratio:.2f}%)")
assert ratio < 5, "警告:可训练参数过多,检查是否正确冻结了基础模型" # assert断言:条件False时抛出AssertionError
# 2. 检查 loss 是否下降
losses = [log["loss"] for log in training_log if "loss" in log]
if len(losses) > 1:
improvement = (losses[0] - losses[-1]) / losses[0] * 100 # [-1]负索引取最后一个元素
print(f"[2] Loss: {losses[0]:.4f} → {losses[-1]:.4f} (下降 {improvement:.1f}%)")
# 3. 检查 LoRA 权重是否有变化
for name, param in model.named_parameters():
if "lora_B" in name and param.requires_grad:
if param.abs().max() < 1e-6:
print(f"[3] 警告:{name} 仍为零,LoRA可能未生效")
break
else:
print("[3] LoRA B矩阵已更新 ✓")
# 4. 检查梯度范数
grad_norms = []
for name, param in model.named_parameters():
if param.requires_grad and param.grad is not None:
grad_norms.append(param.grad.norm().item())
if grad_norms:
print(f"[4] 梯度范数: min={min(grad_norms):.6f}, max={max(grad_norms):.6f}")
8. 下一步¶
完成本节后,你应该: - [x] 理解LoRA的数学原理(低秩分解 \(\Delta W = BA\)) - [x] 能够使用 PEFT 库进行 QLoRA 微调 - [x] 了解 DoRA/AdaLoRA 等变体的改进思路 - [x] 掌握 Prefix Tuning / Prompt Tuning / Adapter 等替代方案 - [x] 能够在消费级GPU上微调7B+模型
深入实践: - 06-LoRA从零实现 — 不依赖 peft 库,从零手写完整 LoRA - LLM应用/09-大模型微调技术 — 微调的完整项目实战 - LLM应用/10-LoRA与QLoRA — 应用层面的 LoRA 使用指南
下一步:02-推理优化技术 - 学习KV Cache、量化等推理加速技术
最后更新日期:2025-07-11 适用版本:LLM学习教程 v2025