作者:PySuper
日期:2025-11-15
标签:LLMOps、LoRA、QLoRA、PEFT、模型微调
一、为什么需要微调平台
大模型微调是一个工程化程度极高的任务。当你需要将通用大模型(如 GPT、Llama、Qwen)适配到特定业务场景时,会面临诸多挑战:
资源门槛高:7B 模型全量微调需要 2×80GB GPU,70B 模型需要 8×80GB,私有化部署成本动辄数十万
参数管理混乱:微调产生的 LoRA 适配器文件散落在各个目录,版本管理困难
训练监控缺失:训练过程中的 Loss 曲线、梯度变化、显存占用等指标难以追踪
评测流程缺失:微调后的模型效果缺乏系统化评测,容易"自嗨"
部署流程割裂:从训练到推理需要手动转换,效率低下
一个设计良好的微调平台,本质上是将参数高效微调(PEFT)技术与平台化工程能力结合,让算法工程师专注于模型效果,而非基础设施。
二、LoRA/QLoRA 原理简述
在深入平台设计之前,我们需要理解 LoRA 和 QLoRA 的核心原理。
2.1 LoRA:低秩适配
LoRA(Low-Rank Adaptation)的核心思想是不直接更新预训练模型的权重,而是学习一个低秩矩阵的增量。
┌─────────────────────────────────────────────────────────────────┐
│ LoRA 原理图 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 预训练权重 W0 (d × d) ──────────────────┐ │
│ │ │
│ │ (不更新) │
│ ▼ │
│ ┌─────────┐ │
│ 输入 x (batch, seq_len, d) ───────▶│ Forward │───▶ 输出 y │
│ └────┬────┘ │
│ │ │
│ │ x × W0^T │
│ ▼ │
│ 增量计算: ΔW = BA │
│ │
│ ┌───────┐ │
│ │ B │ (d × r) │
│ │ (降维) │ │
│ └───────┘ │
│ ▲ │
│ │ │
│ 输入 x ─────┤ │
│ │ │
│ ▼ │
│ ┌───────┐ │
│ │ A │ (r × d) ◀── 可学习参数 │
│ │ (升维) │ r << d (低秩) │
│ └───────┘ │
│ │
│ 最终输出: y = W0·x + ΔW·x = W0·x + BA·x │
│ │
│ 参数量对比: │
│ - 全量微调: d × d 参数 │
│ - LoRA: 2 × d × r 参数 (r 通常为 8, 16, 32, 64) │
│ │
└─────────────────────────────────────────────────────────────────┘为什么低秩有效? 论文《LoRA: Low-Rank Adaptation of Large Language Models》指出,大模型微调过程中的权重更新矩阵往往是低秩的。这意味着我们可以用一个小的低秩矩阵来近似这个更新,从而大幅减少需要学习的参数量。
2.2 QLoRA:量化 + LoRA
QLoRA(Quantized LoRA)是 LoRA 的进阶版本,核心创新在于将预训练模型量化到 4-bit,再应用 LoRA 微调。
┌─────────────────────────────────────────────────────────────────┐
│ QLoRA 原理图 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 预训练模型 (FP16/BF16) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Layer 1 │ │ Layer 2 │ │ ... │ │ Layer N │ │ │
│ │ │ 7B参数 │ │ 7B参数 │ │ │ │ 7B参数 │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 4-bit 量化 │ │
│ │ NF4 (Normal Float 4) + 双重量化 + 分页优化 │ │
│ │ 量化后显存: 7B × 4bit ≈ 3.5GB (vs 14GB FP16) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ LoRA 微调 (仅适配器参数) │ │
│ │ │ │
│ │ 仅更新 A 和 B 矩阵 (约 0.1% 参数量) │ │
│ │ 使用 16-bit 或 32-bit 精度 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 推理时合并 │ │
│ │ W_final = W_quantized + ΔW (通过 LoRA A×B 计算) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 显存占用对比 (以 7B 模型为例): │
│ ┌─────────────┬─────────────┬─────────────┐ │
│ │ 全量 FP16 │ 全量 BF16 │ QLoRA │ │
│ ├─────────────┼─────────────┼─────────────┤ │
│ │ ~14GB │ ~14GB │ ~5GB │ │
│ │ 需要 1×A100 │ 需要 1×A100 │ 仅需 1×24GB │ │
│ └─────────────┴─────────────┴─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘QLoRA 的关键技术点:
NF4(Normal Float 4):针对神经网络权重分布优化的 4-bit 量化格式
双重量化(Double Quantization):对量化常数也进行量化,进一步节省显存
分页优化(Paged Optimizer):使用 CPU 内存管理优化器状态,避免显存峰值
三、微调平台整体架构
┌─────────────────────────────────────────────────────────────────────────────────┐
│ 大模型微调平台架构图 │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ 用户接入层 │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Web UI │ │ API │ │ CLI │ │ SDK │ │ │
│ │ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │ │
│ │ └────────────┴────────────┴────────────┘ │ │
│ └─────────────────────────────────┬───────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ 任务调度层 │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ 任务队列 │ │ 资源调度器 │ │ 优先级管理 │ │ │
│ │ │ (Redis/DB) │ │ (K8s/裸机) │ │ │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │
│ └─────────────────────────────────┬───────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ 训练执行层 │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │
│ │ │ PEFT 训练引擎 (PEFT + Transformers) │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │
│ │ │ │ LoRA │ │ QLoRA │ │ AdaLoRA │ │ DoRA │ │ │ │
│ │ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ 训练监控 (Loss/Grad/Memory/Throughput) │ │ │ │
│ │ │ └─────────────────────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────┬───────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ 产出管理层 │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ 适配器存储 │ │ 版本管理 │ │ 自动评测 │ │ │
│ │ │ (OSS/MinIO) │ │ (MLflow) │ │ (内置+自定义)│ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘四、微调任务生命周期管理
┌─────────────────────────────────────────────────────────────────────────────────┐
│ 微调任务生命周期状态机 │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ │
│ ┌──────────┐ submit ┌──────────┐ queue ┌──────────┐ │
│ │ PENDING │────────────▶│ QUEUED │───────────▶│ WAITING │ │
│ └──────────┘ └──────────┘ └────┬─────┘ │
│ │ │
│ │ schedule │
│ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ SUCCEEDED│◀───│ EVALUATING◀──│TRAINING │◀──│ PREPARING│ │
│ │ │ │ │ │ │ └──────────┘ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ │ │ │ │
│ │ │ │ train │
│ │ │ ▼ │
│ │ │ ┌──────────┐ │
│ │ │ │ RUNNING │ │
│ │ │ └────┬─────┘ │
│ │ │ │ │
│ │ │ │ eval │
│ │ │ ▼ │
│ │ │ ┌──────────┐ ┌──────────┐ │
│ │ │ │COMPLETED│───▶│ REVIEWING│ │
│ │ │ └────┬─────┘ └──────────┘ │
│ │ │ │ │
│ │ │ │ register │
│ │ │ ▼ │
│ │ │ ┌──────────┐ │
│ │ │ │REGISTERED│ │
│ │ │ └──────────┘ │
│ │ │ │
│ │ ▼ │
│ │ ┌──────────┐ │
│ └────────▶│ FAILED │ │
│ └─────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ 状态说明: │ │
│ │ - PENDING: 任务已提交,待调度 │ │
│ │ - QUEUED: 进入任务队列 │ │
│ │ - WAITING: 等待资源分配 │ │
│ │ - PREPARING: 准备训练环境(下载模型、拉取镜像) │ │
│ │ - TRAINING: 训练中 │ │
│ │ - EVALUATING: 自动评测中 │ │
│ │ - COMPLETED: 训练完成,待人工审核 │ │
│ │ - REVIEWING: 人工审核中 │ │
│ │ - REGISTERED: 模型已注册到模型库 │ │
│ │ - FAILED: 任务失败 │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘五、超参数推荐策略
微调超参数的选择对最终效果影响巨大。平台内置了一套基于经验的推荐策略:
#!/usr/bin/env python3
"""
超参数推荐策略
根据模型规模、硬件配置、任务类型自动推荐超参数
"""
from dataclasses import dataclass
from typing import Dict, List, Optional
from enum import Enum
import json
class ModelSize(Enum):
"""模型规模枚举"""
SMALL = "small" # 1B 以下
MEDIUM = "medium" # 1B - 7B
LARGE = "large" # 7B - 13B
XLARGE = "xlarge" # 13B - 70B
XXLARGE = "xxlarge" # 70B+
class TaskType(Enum):
"""任务类型枚举"""
CHAT = "chat"
INSTRUCTION = "instruction"
SFT = "sft"
RLHF = "rlhf"
DOMAIN_ADAPT = "domain_adapt"
@dataclass
class HardwareSpec:
"""硬件规格"""
gpu_count: int = 1
gpu_memory_gb: int = 24
gpu_type: str = "A10" # A10/A100/H100/L40S
@property
def total_memory_gb(self) -> int:
return self.gpu_count * self.gpu_memory_gb
@dataclass
class HyperParameterRecommendation:
"""超参数推荐"""
# LoRA 配置
lora_r: int
lora_alpha: int
lora_dropout: float
lora_target_modules: List[str]
# 训练配置
learning_rate: float
batch_size: int
gradient_accumulation_steps: int
max_seq_length: int
num_epochs: int
warmup_ratio: float
weight_decay: float
# 优化器配置
optimizer_type: str
lr_scheduler_type: str
# 训练策略
fp16: bool = True
bf16: bool = False
gradient_checkpointing: bool = True
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"lora": {
"r": self.lora_r,
"alpha": self.lora_alpha,
"dropout": self.lora_dropout,
"target_modules": self.lora_target_modules,
},
"training": {
"learning_rate": self.learning_rate,
"batch_size": self.batch_size,
"gradient_accumulation_steps": self.gradient_accumulation_steps,
"max_seq_length": self.max_seq_length,
"num_epochs": self.num_epochs,
"warmup_ratio": self.warmup_ratio,
"weight_decay": self.weight_decay,
},
"optimizer": {
"type": self.optimizer_type,
"lr_scheduler": self.lr_scheduler_type,
},
"strategy": {
"fp16": self.fp16,
"bf16": self.bf16,
"gradient_checkpointing": self.gradient_checkpointing,
}
}
class HyperParameterRecommender:
"""超参数推荐器"""
# LoRA rank 推荐表(根据模型规模)
LORA_R_RECOMMENDATION = {
ModelSize.SMALL: [8, 16],
ModelSize.MEDIUM: [16, 32, 64],
ModelSize.LARGE: [32, 64, 128],
ModelSize.XLARGE: [64, 128],
ModelSize.XXLARGE: [128, 256],
}
# 学习率推荐表(根据任务类型)
LEARNING_RATE_RECOMMENDATION = {
TaskType.CHAT: (1e-4, 3e-4),
TaskType.INSTRUCTION: (1e-4, 2e-4),
TaskType.SFT: (5e-5, 1e-4),
TaskType.RLHF: (5e-6, 1e-5),
TaskType.DOMAIN_ADAPT: (1e-4, 5e-4),
}
# 上下文长度推荐(根据显存)
CONTEXT_LENGTH_RECOMMENDATION = {
24: 2048,
40: 4096,
80: 8192,
160: 16384,
320: 32768,
}
def get_model_size(self, model_name: str) -> ModelSize:
"""根据模型名称判断规模"""
model_name_lower = model_name.lower()
# 提取模型参数量
if "0.5b" in model_name_lower or "500m" in model_name_lower:
return ModelSize.SMALL
elif "1b" in model_name_lower or "1.3b" in model_name_lower:
return ModelSize.SMALL
elif "7b" in model_name_lower or "8b" in model_name_lower:
return ModelSize.MEDIUM
elif "13b" in model_name_lower:
return ModelSize.LARGE
elif "30b" in model_name_lower or "34b" in model_name_lower:
return ModelSize.XLARGE
elif "70b" in model_name_lower or "72b" in model_name_lower:
return ModelSize.XLARGE
elif "110b" in model_name_lower or "140b" in model_name_lower:
return ModelSize.XXLARGE
# 默认按中等规模处理
return ModelSize.MEDIUM
def estimate_batch_size(
self,
model_name: str,
hardware: HardwareSpec,
max_seq_length: int = 2048,
use_qlora: bool = True
) -> tuple:
"""
估算最优 batch size 和 gradient accumulation steps
Returns:
(batch_size, gradient_accumulation_steps)
"""
model_size = self.get_model_size(model_name)
total_memory = hardware.total_memory_gb
# 基础 batch size 估算
# 参考值:FP16 训练时,每 1B 参数约需 4GB 显存
# QLoRA 可降低到约 1GB/B
if use_qlora:
# QLoRA 显存占用更低
memory_per_b = 0.8
else:
memory_per_b = 4.0
# 考虑上下文长度的影响
seq_len_factor = max_seq_length / 2048
memory_for_model = self._get_param_count(model_size) * memory_per_b * seq_len_factor
# 预留显存给 LoRA、优化器、激活值等
reserved_memory = 8 # GB
available_memory = total_memory - reserved_memory - memory_for_model
if available_memory <= 0:
# 显存不足,使用最小配置
return 1, max(32, 2048 // max_seq_length)
# 基础 batch size
base_batch_size = int(available_memory / (memory_per_b * seq_len_factor))
# 限制范围
batch_size = min(base_batch_size, hardware.gpu_count * 8)
batch_size = max(batch_size, 1)
# 目标 epoch batch size (effective batch size)
target_epoch_batch = 128 if model_size.value in ["medium", "large"] else 32
# 调整 gradient accumulation
if batch_size < target_epoch_batch:
grad_accum = target_epoch_batch // batch_size
else:
grad_accum = 1
return batch_size, grad_accum
def _get_param_count(self, model_size: ModelSize) -> float:
"""获取模型参数量(单位:B)"""
mapping = {
ModelSize.SMALL: 0.5,
ModelSize.MEDIUM: 7.0,
ModelSize.LARGE: 13.0,
ModelSize.XLARGE: 34.0,
ModelSize.XXLARGE: 70.0,
}
return mapping.get(model_size, 7.0)
def recommend(
self,
model_name: str,
task_type: TaskType,
hardware: HardwareSpec,
use_qlora: bool = True,
custom_overrides: Optional[Dict] = None
) -> HyperParameterRecommendation:
"""
获取完整的超参数推荐
Args:
model_name: 模型名称
task_type: 任务类型
hardware: 硬件规格
use_qlora: 是否使用 QLoRA
custom_overrides: 自定义覆盖参数
Returns:
超参数推荐
"""
model_size = self.get_model_size(model_name)
# 1. LoRA rank 推荐
lora_r_candidates = self.LORA_R_RECOMMENDATION.get(model_size, [16])
lora_r = lora_r_candidates[0] if not custom_overrides else custom_overrides.get("lora_r", lora_r_candidates[0])
lora_alpha = lora_r * 2 # 常用配置
# 2. 学习率推荐
lr_range = self.LEARNING_RATE_RECOMMENDATION.get(task_type, (1e-4, 3e-4))
learning_rate = lr_range[0] if not custom_overrides else custom_overrides.get("learning_rate", lr_range[0])
# 3. Batch size 推荐
max_seq_length = self.CONTEXT_LENGTH_RECOMMENDATION.get(
hardware.gpu_memory_gb, 2048
)
batch_size, grad_accum = self.estimate_batch_size(
model_name, hardware, max_seq_length, use_qlora
)
# 4. 其他超参数
warmup_ratio = 0.03 if task_type in [TaskType.CHAT, TaskType.INSTRUCTION] else 0.1
weight_decay = 0.01
num_epochs = 3 if task_type == TaskType.SFT else 5
# 5. LoRA 目标模块
lora_target_modules = ["q_proj", "v_proj", "k_proj", "o_proj"]
if model_size.value in ["large", "xlarge", "xxlarge"]:
lora_target_modules.extend(["gate_proj", "up_proj", "down_proj"])
# 构建推荐
recommendation = HyperParameterRecommendation(
lora_r=lora_r,
lora_alpha=lora_alpha,
lora_dropout=0.05,
lora_target_modules=lora_target_modules,
learning_rate=learning_rate,
batch_size=batch_size,
gradient_accumulation_steps=grad_accum,
max_seq_length=max_seq_length,
num_epochs=num_epochs,
warmup_ratio=warmup_ratio,
weight_decay=weight_decay,
optimizer_type="paged_adamw_32bit" if use_qlora else "adamw_torch",
lr_scheduler_type="cosine",
fp16=not use_qlora,
bf16=use_qlora,
gradient_checkpointing=True,
)
# 应用自定义覆盖
if custom_overrides:
for key, value in custom_overrides.items():
if hasattr(recommendation, key):
setattr(recommendation, key, value)
return recommendation
def demo():
"""演示推荐策略"""
recommender = HyperParameterRecommender()
# 演示:7B 模型 + 24GB 显存的推荐
recommendation = recommender.recommend(
model_name="Qwen2.5-7B-Instruct",
task_type=TaskType.INSTRUCTION,
hardware=HardwareSpec(gpu_count=1, gpu_memory_gb=24, gpu_type="A10"),
use_qlora=True
)
print("超参数推荐:")
print(json.dumps(recommendation.to_dict(), indent=2, ensure_ascii=False))
if __name__ == "__main__":
demo()六、完整的微调训练代码
6.1 基于 PEFT + Transformers 的训练脚本
#!/usr/bin/env python3
"""
LoRA/QLoRA 微调训练脚本
基于 PEFT + Transformers 库
"""
import os
import sys
import argparse
import logging
from pathlib import Path
from typing import Optional, Dict, Any, List
import json
import torch
from torch.utils.data import Dataset, DataLoader
from transformers import (
AutoTokenizer,
AutoModelForCausalLM,
TrainingArguments,
Trainer,
DataCollatorForLanguageModeling,
set_seed,
)
from peft import (
LoraConfig,
get_peft_model,
TaskType,
prepare_model_for_kbit_training,
)
from trl import SFTTrainer # 用于更好的 SFT 支持
# 配置日志
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
class LoRATrainer:
"""LoRA/QLoRA 训练器"""
def __init__(
self,
model_name: str,
output_dir: str,
use_qlora: bool = True,
**kwargs
):
self.model_name = model_name
self.output_dir = Path(output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
self.use_qlora = use_qlora
# 初始化组件
self.tokenizer = None
self.model = None
self.trainer = None
# 训练配置
self.training_config = kwargs
def setup_model(self, lora_config: Dict[str, Any]):
"""
初始化模型
Args:
lora_config: LoRA 配置
"""
logger.info(f"加载模型: {self.model_name}")
# 1. 加载 tokenizer
self.tokenizer = AutoTokenizer.from_pretrained(
self.model_name,
trust_remote_code=True,
)
# 设置 pad token(如果不存在)
if self.tokenizer.pad_token is None:
self.tokenizer.pad_token = self.tokenizer.eos_token
self.tokenizer.pad_token_id = self.tokenizer.eos_token_id
# 2. 加载模型
load_kwargs = {
"pretrained_model_name_or_path": self.model_name,
"trust_remote_code": True,
}
if self.use_qlora:
# QLoRA 配置
load_kwargs.update({
"load_in_4bit": True,
"bnb_4bit_compute_dtype": torch.bfloat16,
"bnb_4bit_use_double_quant": True,
"bnb_4bit_quant_type": "nf4",
})
else:
# 普通 LoRA 配置
load_kwargs.update({
"torch_dtype": torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16,
"device_map": "auto",
})
self.model = AutoModelForCausalLM.from_pretrained(**load_kwargs)
# 3. 准备模型进行训练
if self.use_qlora:
self.model = prepare_model_for_kbit_training(self.model)
# 4. 应用 LoRA
peft_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=lora_config.get("r", 16),
lora_alpha=lora_config.get("alpha", 32),
lora_dropout=lora_config.get("dropout", 0.05),
target_modules=lora_config.get("target_modules", ["q_proj", "v_proj"]),
bias=lora_config.get("bias", "none"),
inference_mode=False,
)
self.model = get_peft_model(self.model, peft_config)
# 打印可训练参数统计
self._print_trainable_params()
def setup_trainer(
self,
train_dataset: Dataset,
eval_dataset: Optional[Dataset] = None,
training_args: Optional[Dict] = None,
):
"""
初始化 Trainer
Args:
train_dataset: 训练数据集
eval_dataset: 验证数据集
training_args: 训练参数
"""
# 默认训练参数
default_args = {
"output_dir": str(self.output_dir),
"num_train_epochs": 3,
"per_device_train_batch_size": 2,
"gradient_accumulation_steps": 4,
"learning_rate": 1e-4,
"warmup_ratio": 0.03,
"lr_scheduler_type": "cosine",
"logging_steps": 10,
"save_steps": 100,
"eval_steps": 100,
"save_total_limit": 3,
"bf16": torch.cuda.is_bf16_supported(),
"fp16": not torch.cuda.is_bf16_supported(),
"gradient_checkpointing": True,
"gradient_checkpointing_kwargs": {"use_reentrant": False},
"report_to": ["tensorboard"],
"optim": "paged_adamw_8bit" if self.use_qlora else "adamw_torch",
"remove_unused_columns": False,
"model准备": True,
}
if training_args:
default_args.update(training_args)
training_arguments = TrainingArguments(**default_args)
# 数据整理器
data_collator = DataCollatorForLanguageModeling(
tokenizer=self.tokenizer,
mlm=False, # Causal LM 不使用 MLM
)
# 创建 Trainer
self.trainer = Trainer(
model=self.model,
args=training_arguments,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
data_collator=data_collator,
)
logger.info("Trainer 初始化完成")
def train(self):
"""执行训练"""
logger.info("开始训练...")
# 保存初始配置
self._save_config()
# 开始训练
train_result = self.trainer.train()
# 保存最终模型
self.trainer.save_model()
self.trainer.save_state()
# 保存训练指标
metrics = train_result.metrics
self.trainer.log_metrics("train", metrics)
self.trainer.save_metrics("train", metrics)
logger.info("训练完成!")
return metrics
def merge_and_save(self, save_path: Optional[str] = None):
"""
合并 LoRA 权重并保存
Args:
save_path: 保存路径
"""
from peft import PeftModel
logger.info("合并 LoRA 权重...")
# 加载基础模型
base_model = AutoModelForCausalLM.from_pretrained(
self.model_name,
trust_remote_code=True,
torch_dtype=torch.bfloat16,
)
# 合并权重
merged_model = PeftModel.from_pretrained(
base_model,
str(self.output_dir / "adapter_model.safetensors")
)
merged_model = merged_model.merge_and_unload()
# 保存
save_path = save_path or str(self.output_dir / "merged_model")
merged_model.save_pretrained(save_path)
self.tokenizer.save_pretrained(save_path)
logger.info(f"合并模型已保存: {save_path}")
def _print_trainable_params(self):
"""打印可训练参数统计"""
trainable_params = 0
all_params = 0
for _, param in self.model.named_parameters():
all_params += param.numel()
if param.requires_grad:
trainable_params += param.numel()
logger.info(
f"可训练参数: {trainable_params:,} / {all_params:,} "
f"({trainable_params/all_params*100:.2f}%)"
)
def _save_config(self):
"""保存训练配置"""
config = {
"model_name": self.model_name,
"use_qlora": self.use_qlora,
"training_args": vars(self.trainer.args) if self.trainer else {},
"output_dir": str(self.output_dir),
}
with open(self.output_dir / "training_config.json", 'w') as f:
json.dump(config, f, indent=2)
# ============ 数据集处理 ============
class InstructionDataset(Dataset):
"""指令微调数据集"""
def __init__(
self,
data_path: str,
tokenizer,
max_length: int = 2048,
system_prompt: Optional[str] = None,
):
"""
初始化数据集
Args:
data_path: 数据文件路径(支持 JSONL/JSON)
tokenizer: 分词器
max_length: 最大序列长度
system_prompt: 系统提示词
"""
self.tokenizer = tokenizer
self.max_length = max_length
self.system_prompt = system_prompt or "你是一个有帮助的AI助手。"
# 加载数据
self.data = self._load_data(data_path)
logger.info(f"数据集加载完成: {len(self.data)} 条样本")
def _load_data(self, data_path: str) -> List[Dict]:
"""加载数据文件"""
data_path = Path(data_path)
if data_path.suffix == ".jsonl":
with open(data_path, 'r', encoding='utf-8') as f:
return [json.loads(line) for line in f]
elif data_path.suffix == ".json":
with open(data_path, 'r', encoding='utf-8') as f:
return json.load(f)
else:
raise ValueError(f"不支持的数据格式: {data_path.suffix}")
def __len__(self) -> int:
return len(self.data)
def __getitem__(self, idx: int) -> Dict:
item = self.data[idx]
# 构建对话格式
if "messages" in item:
# 多轮对话格式
prompt = self._format_conversation(item["messages"])
else:
# 单轮问答格式
prompt = self._format_qa(item)
# Tokenize
encoding = self.tokenizer(
prompt,
max_length=self.max_length,
truncation=True,
padding="max_length",
return_tensors="pt",
)
return {
"input_ids": encoding["input_ids"].squeeze(),
"attention_mask": encoding["attention_mask"].squeeze(),
"labels": encoding["input_ids"].squeeze(),
}
def _format_qa(self, item: Dict) -> str:
"""格式化问答数据"""
instruction = item.get("instruction", "")
input_text = item.get("input", "")
output = item.get("output", "")
prompt = f"【指令】{instruction}\n"
if input_text:
prompt += f"【输入】{input_text}\n"
prompt += f"【输出】{output}"
return prompt
def _format_conversation(self, messages: List[Dict]) -> str:
"""格式化多轮对话"""
formatted = f"【系统】{self.system_prompt}\n"
for msg in messages:
role = msg.get("role", "user")
content = msg.get("content", "")
if role == "user":
formatted += f"【用户】{content}\n"
elif role == "assistant":
formatted += f"【助手】{content}\n"
return formatted
# ============ 主函数 ============
def parse_args():
parser = argparse.ArgumentParser(description="LoRA/QLoRA 微调训练脚本")
# 模型配置
parser.add_argument("--model_name", type=str, required=True, help="模型名称或路径")
parser.add_argument("--use_qlora", action="store_true", help="使用 QLoRA")
# 数据配置
parser.add_argument("--train_data", type=str, required=True, help="训练数据路径")
parser.add_argument("--val_data", type=str, default=None, help="验证数据路径")
parser.add_argument("--max_length", type=int, default=2048, help="最大序列长度")
# LoRA 配置
parser.add_argument("--lora_r", type=int, default=16, help="LoRA rank")
parser.add_argument("--lora_alpha", type=int, default=32, help="LoRA alpha")
parser.add_argument("--lora_dropout", type=float, default=0.05, help="LoRA dropout")
parser.add_argument("--target_modules", type=str, default="q_proj,v_proj", help="目标模块")
# 训练配置
parser.add_argument("--output_dir", type=str, required=True, help="输出目录")
parser.add_argument("--num_epochs", type=int, default=3, help="训练轮数")
parser.add_argument("--batch_size", type=int, default=2, help="批次大小")
parser.add_argument("--learning_rate", type=float, default=1e-4, help="学习率")
parser.add_argument("--seed", type=int, default=42, help="随机种子")
return parser.parse_args()
def main():
args = parse_args()
# 设置随机种子
set_seed(args.seed)
# 构建 LoRA 配置
lora_config = {
"r": args.lora_r,
"alpha": args.lora_alpha,
"dropout": args.lora_dropout,
"target_modules": args.target_modules.split(","),
}
# 初始化训练器
trainer = LoRATrainer(
model_name=args.model_name,
output_dir=args.output_dir,
use_qlora=args.use_qlora,
)
# 初始化模型
trainer.setup_model(lora_config)
# 加载数据集
train_dataset = InstructionDataset(
data_path=args.train_data,
tokenizer=trainer.tokenizer,
max_length=args.max_length,
)
val_dataset = None
if args.val_data:
val_dataset = InstructionDataset(
data_path=args.val_data,
tokenizer=trainer.tokenizer,
max_length=args.max_length,
)
# 初始化 Trainer
trainer.setup_trainer(
train_dataset=train_dataset,
eval_dataset=val_dataset,
training_args={
"num_train_epochs": args.num_epochs,
"per_device_train_batch_size": args.batch_size,
"learning_rate": args.learning_rate,
"warmup_ratio": 0.03,
}
)
# 开始训练
metrics = trainer.train()
# 合并权重(可选)
# trainer.merge_and_save()
if __name__ == "__main__":
main()七、FastAPI 任务管理接口
#!/usr/bin/env python3
"""
微调任务管理 API
基于 FastAPI 实现
"""
from fastapi import FastAPI, HTTPException, BackgroundTasks, Depends
from pydantic import BaseModel, Field
from typing import Optional, List, Dict, Any
from datetime import datetime
from enum import Enum
import uuid
import logging
import asyncio
from pathlib import Path
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# FastAPI 应用
app = FastAPI(title="LLM Fine-tuning Platform", version="1.0.0")
# ============ 数据模型 ============
class TaskStatus(str, Enum):
"""任务状态枚举"""
PENDING = "PENDING"
QUEUED = "QUEUED"
RUNNING = "RUNNING"
SUCCEEDED = "SUCCEEDED"
FAILED = "FAILED"
class FinetuneRequest(BaseModel):
"""微调请求"""
model_name: str = Field(..., description="基础模型名称")
train_data_url: str = Field(..., description="训练数据 URL")
val_data_url: Optional[str] = Field(None, description="验证数据 URL")
# LoRA 配置
use_qlora: bool = Field(True, description="是否使用 QLoRA")
lora_r: int = Field(16, description="LoRA rank")
lora_alpha: int = Field(32, description="LoRA alpha")
lora_dropout: float = Field(0.05, description="LoRA dropout")
target_modules: List[str] = Field(
default=["q_proj", "v_proj"],
description="LoRA 目标模块"
)
# 训练配置
num_epochs: int = Field(3, ge=1, le=10, description="训练轮数")
batch_size: int = Field(2, ge=1, description="批次大小")
learning_rate: float = Field(1e-4, description="学习率")
max_seq_length: int = Field(2048, description="最大序列长度")
# 优先级
priority: int = Field(5, ge=1, le=10, description="任务优先级 (1-10)")
# 可选:使用推荐超参数
use_auto_config: bool = Field(False, description="是否使用自动超参数推荐")
class Config:
json_schema_extra = {
"example": {
"model_name": "Qwen/Qwen2.5-7B-Instruct",
"train_data_url": "s3://bucket/data/train.jsonl",
"use_qlora": True,
"lora_r": 16,
"num_epochs": 3,
"batch_size": 2,
"priority": 5,
}
}
class TaskResponse(BaseModel):
"""任务响应"""
task_id: str
status: TaskStatus
created_at: datetime
message: Optional[str] = None
class TaskDetail(BaseModel):
"""任务详情"""
task_id: str
status: TaskStatus
model_name: str
progress: float = 0.0
current_epoch: int = 0
total_epochs: int
metrics: Optional[Dict[str, Any]] = None
error_message: Optional[str] = None
created_at: datetime
started_at: Optional[datetime] = None
finished_at: Optional[datetime] = None
class AdapterInfo(BaseModel):
"""适配器信息"""
adapter_id: str
model_name: str
task_id: str
version: str
metrics: Dict[str, float]
created_at: datetime
size_mb: float
download_url: str
# ============ 任务存储(内存实现,生产环境应使用数据库)============
class TaskStore:
"""任务存储"""
def __init__(self):
self.tasks: Dict[str, Dict[str, Any]] = {}
self.adapters: Dict[str, Dict[str, Any]] = {}
def create_task(self, request: FinetuneRequest) -> str:
"""创建任务"""
task_id = str(uuid.uuid4())
task = {
"task_id": task_id,
"status": TaskStatus.PENDING,
"request": request.model_dump(),
"created_at": datetime.now(),
"progress": 0.0,
"metrics": {},
}
self.tasks[task_id] = task
logger.info(f"创建任务: {task_id}")
return task_id
def get_task(self, task_id: str) -> Optional[Dict]:
"""获取任务"""
return self.tasks.get(task_id)
def update_task(self, task_id: str, **kwargs):
"""更新任务"""
if task_id in self.tasks:
self.tasks[task_id].update(kwargs)
def list_tasks(self, status: Optional[TaskStatus] = None) -> List[Dict]:
"""列出任务"""
tasks = list(self.tasks.values())
if status:
tasks = [t for t in tasks if t["status"] == status]
return sorted(tasks, key=lambda x: x["created_at"], reverse=True)
def save_adapter(self, task_id: str, adapter_path: str, metrics: Dict) -> str:
"""保存适配器"""
adapter_id = str(uuid.uuid4())
adapter = {
"adapter_id": adapter_id,
"task_id": task_id,
"model_name": self.tasks[task_id]["request"]["model_name"],
"version": "v1.0.0",
"metrics": metrics,
"created_at": datetime.now(),
"path": adapter_path,
}
self.adapters[adapter_id] = adapter
logger.info(f"保存适配器: {adapter_id}")
return adapter_id
# 全局存储
task_store = TaskStore()
# ============ API 路由 ============
@app.post("/api/v1/finetune", response_model=TaskResponse)
async def create_finetune_task(
request: FinetuneRequest,
background_tasks: BackgroundTasks
):
"""
创建微调任务
提交一个新的模型微调任务到队列。
"""
# 创建任务
task_id = task_store.create_task(request)
# 触发后台调度(实际场景中应该发送到消息队列)
background_tasks.add_task(schedule_task, task_id)
return TaskResponse(
task_id=task_id,
status=TaskStatus.PENDING,
created_at=task_store.get_task(task_id)["created_at"],
message="任务已提交,正在等待调度"
)
@app.get("/api/v1/tasks/{task_id}", response_model=TaskDetail)
async def get_task(task_id: str):
"""获取任务详情"""
task = task_store.get_task(task_id)
if not task:
raise HTTPException(status_code=404, detail="任务不存在")
return TaskDetail(
task_id=task["task_id"],
status=task["status"],
model_name=task["request"]["model_name"],
progress=task.get("progress", 0.0),
current_epoch=task.get("current_epoch", 0),
total_epochs=task["request"]["num_epochs"],
metrics=task.get("metrics", {}),
error_message=task.get("error_message"),
created_at=task["created_at"],
started_at=task.get("started_at"),
finished_at=task.get("finished_at"),
)
@app.get("/api/v1/tasks", response_model=List[TaskDetail])
async def list_tasks(status: Optional[TaskStatus] = None):
"""列出所有任务"""
tasks = task_store.list_tasks(status)
return [
TaskDetail(
task_id=t["task_id"],
status=t["status"],
model_name=t["request"]["model_name"],
progress=t.get("progress", 0.0),
current_epoch=t.get("current_epoch", 0),
total_epochs=t["request"]["num_epochs"],
metrics=t.get("metrics", {}),
created_at=t["created_at"],
)
for t in tasks
]
@app.delete("/api/v1/tasks/{task_id}")
async def cancel_task(task_id: str):
"""取消任务"""
task = task_store.get_task(task_id)
if not task:
raise HTTPException(status_code=404, detail="任务不存在")
if task["status"] in [TaskStatus.SUCCEEDED, TaskStatus.FAILED]:
raise HTTPException(status_code=400, detail="任务已完成,无法取消")
task_store.update_task(task_id, status=TaskStatus.FAILED, error_message="用户取消")
return {"message": "任务已取消"}
@app.get("/api/v1/adapters/{adapter_id}", response_model=AdapterInfo)
async def get_adapter(adapter_id: str):
"""获取适配器信息"""
adapter = task_store.adapters.get(adapter_id)
if not adapter:
raise HTTPException(status_code=404, detail="适配器不存在")
return AdapterInfo(
adapter_id=adapter["adapter_id"],
model_name=adapter["model_name"],
task_id=adapter["task_id"],
version=adapter["version"],
metrics=adapter["metrics"],
created_at=adapter["created_at"],
size_mb=0.0, # 实际应该计算
download_url=f"/api/v1/adapters/{adapter_id}/download",
)
@app.get("/api/v1/adapters")
async def list_adapters():
"""列出所有适配器"""
adapters = list(task_store.adapters.values())
return [
AdapterInfo(
adapter_id=a["adapter_id"],
model_name=a["model_name"],
task_id=a["task_id"],
version=a["version"],
metrics=a["metrics"],
created_at=a["created_at"],
size_mb=0.0,
download_url=f"/api/v1/adapters/{a['adapter_id']}/download",
)
for a in adapters
]
# ============ 后台任务处理 ============
async def schedule_task(task_id: str):
"""
调度任务(实际场景中应该使用 Celery/Redis 队列)
这里只是模拟调度过程
"""
from datetime import timedelta
# 更新状态为 QUEUED
task_store.update_task(task_id, status=TaskStatus.QUEUED)
# 模拟等待调度
await asyncio.sleep(2)
# 更新状态为 RUNNING
task_store.update_task(
task_id,
status=TaskStatus.RUNNING,
started_at=datetime.now()
)
logger.info(f"任务 {task_id} 开始执行")
# 模拟训练过程
task = task_store.get_task(task_id)
num_epochs = task["request"]["num_epochs"]
for epoch in range(num_epochs):
# 模拟每个 epoch
for step in range(10):
await asyncio.sleep(0.5)
# 更新进度
progress = (epoch * 10 + step) / (num_epochs * 10)
task_store.update_task(
task_id,
progress=progress,
current_epoch=epoch + 1,
)
# 模拟训练完成
task_store.update_task(
task_id,
status=TaskStatus.SUCCEEDED,
progress=1.0,
finished_at=datetime.now(),
metrics={
"final_train_loss": 0.15,
"final_val_loss": 0.18,
"final_train_accuracy": 0.92,
"final_val_accuracy": 0.89,
}
)
logger.info(f"任务 {task_id} 执行完成")
# ============ 启动 ============
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)八、Docker 训练镜像构建
# Dockerfile.training
# 微调训练镜像
FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04
# 设置环境变量
ENV DEBIAN_FRONTEND=noninteractive
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
# 安装系统依赖
RUN apt-get update && apt-get install -y \
build-essential \
git \
curl \
wget \
vim \
unzip \
&& rm -rf /var/lib/apt/lists/*
# 安装 Miniconda
RUN curl -fsSL https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh \
-o /tmp/miniconda.sh && \
bash /tmp/miniconda.sh -b -p /opt/conda && \
rm /tmp/miniconda.sh
ENV PATH=/opt/conda/bin:$PATH
# 创建非 root 用户
RUN useradd -m -s /bin/bash mluser && \
mkdir -p /home/mluser && \
chown -R mluser:mluser /home/mluser
# 安装 Python 和基础依赖
RUN conda install -y python=3.10 pip && \
pip install --no-cache-dir --upgrade pip
# 安装 PyTorch (CUDA 12.1)
RUN pip install --no-cache-dir \
torch==2.3.0 \
torchvision==0.18.0 \
torchaudio==2.3.0
# 安装 Transformers 和相关库
RUN pip install --no-cache-dir \
transformers==4.41.0 \
accelerate==0.31.0 \
datasets==2.19.0 \
peft==0.11.0 \
trl==0.9.0 \
bitsandbytes==0.43.0 \
accelerate
# 安装训练辅助库
RUN pip install --no-cache-dir \
tensorboard==2.16.0 \
wandb==0.17.0 \
mlflow==2.14.0 \
deepspeed==0.14.0
# 安装数据处理库
RUN pip install --no-cache-dir \
pandas==2.2.0 \
numpy==1.26.0 \
pyarrow==17.0.0 \
fastparquet==2024.5.0
# 安装 API 框架
RUN pip install --no-cache-dir \
fastapi==0.111.0 \
uvicorn==0.30.0 \
pydantic==2.7.0
# 安装 AWS CLI (用于 S3 数据访问)
RUN curl -fsSL https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip \
-o /tmp/awscliv2.zip && \
unzip -q /tmp/awscliv2.zip -d /tmp && \
/tmp/aws/install && \
rm -rf /tmp/awscliv2.zip /tmp/aws
# 复制训练脚本
COPY scripts/ /opt/ml/scripts/
RUN chmod +x /opt/ml/scripts/*.py
# 设置工作目录
WORKDIR /workspace
# 切换到非 root 用户
USER mluser
# 默认命令
CMD ["/bin/bash"]# docker-compose.yml
# 本地开发环境
version: '3.8'
services:
# 微调训练服务
finetune-api:
build:
context: .
dockerfile: Dockerfile.training
container_name: finetune-api
ports:
- "8000:8000"
volumes:
- ./workspace:/workspace
- ./models:/models
- ./outputs:/outputs
- ~/.cache/huggingface:/home/mluser/.cache/huggingface
environment:
- CUDA_VISIBLE_DEVICES=0
- MLFLOW_TRACKING_URI=http://mlflow:5000
- HF_HOME=/home/mluser/.cache/huggingface
- TRANSFORMERS_CACHE=/home/mluser/.cache/huggingface
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
depends_on:
- redis
- mlflow
networks:
- finetune-net
# Redis 任务队列
redis:
image: redis:7-alpine
container_name: finetune-redis
ports:
- "6379:6379"
volumes:
- redis-data:/data
networks:
- finetune-net
# MLflow 实验追踪
mlflow:
image: ghcr.io/mlflow/mlflow:latest
container_name: finetune-mlflow
ports:
- "5000:5000"
volumes:
- mlflow-artifacts:/mlflow/artifacts
- ./mlruns:/mlruns
environment:
- MLFLOW_TRACKING_URI=http://localhost:5000
- MLFLOW_ARTIFACTS_DESTINATION=/mlflow/artifacts
networks:
- finetune-net
# MinIO 对象存储(用于存储模型和数据集)
minio:
image: minio/minio:latest
container_name: finetune-minio
ports:
- "9000:9000"
- "9001:9001"
volumes:
- minio-data:/data
environment:
- MINIO_ROOT_USER=minioadmin
- MINIO_ROOT_PASSWORD=minioadmin
command: server /data --console-address ":9001"
networks:
- finetune-net
volumes:
redis-data:
mlflow-artifacts:
minio-data:
networks:
finetune-net:
driver: bridge九、总结
本文详细介绍了大模型微调平台的工程化设计:
核心要点回顾:
LoRA/QLoRA 原理:LoRA 通过学习低秩矩阵增量实现高效微调,QLoRA 在此基础上引入 4-bit 量化,将 7B 模型显存需求从 14GB 降到 5GB 左右。
平台架构:采用分层设计,从用户接入层、任务调度层、训练执行层到产出管理层,职责清晰,便于扩展。
任务生命周期:完整的状态机设计,覆盖从提交到注册的完整流程,支持任务取消、重试等操作。
超参数推荐:基于模型规模、硬件配置、任务类型的智能推荐策略,降低使用门槛。
PEFT + TRL 训练:基于开源生态的完整训练代码,支持 LoRA/QLoRA,配套完整的 Trainer 封装。
FastAPI 任务管理:RESTful API 设计,支持任务创建、查询、取消等操作,可扩展为生产级服务。
Docker 部署:完整的容器化方案,支持 GPU 调度,与 Kubernetes 无缝集成。
在实际落地时,建议优先实现本地单机版验证流程,再逐步扩展到集群调度和分布式训练。微调平台的核心价值在于降低算法工程师的使用门槛,让他们专注于模型效果而非基础设施。
相关技术栈:PEFT、Transformers、TRL、FastAPI、Docker、Kubernetes、MLflow
参考资料:LLMOps 大模型应用治理体系
评论区