大型语言模型(LLMs)在 2024 年继续发挥着重要作用,多项重大进展完全超越了之前的模型。重点转向了 Meta、Qwen 或 Google 等公司推出的更小、更强大的模型。这些模型不仅变得更强大,而且更高效。我们看到了像 10 亿参数的 Llama 模型超越了 Llama 2 13B 的性能。

LLMs 现在可以通过提示处理许多开箱即用的任务,包括聊天机器人、问答和摘要。然而,对于需要高精度或领域专业知识的专门应用,微调仍然是一种强大的方法,可以比单独提示获得更高质量的结果,通过训练更小、更高效的模型来降低成本,并确保特定用例的可靠性和一致性。

本指南侧重于优化、分布式训练和更高的定制性。这意味着支持从全量微调到 QLoRA 和 Spectrum 等多种 PEFT 方法,优化以实现更快、更高效的训练,采用 Flash Attention 或 Liger Kernels 技术,并介绍如何使用 DeepSpeed 扩展训练至多 GPU。

什么是 Qlora? QLoRA(量化低秩适应)通过 4 位量化和最小参数更新实现了对LLMs的高效微调,减少了资源需求,但由于量化权衡,可能会影响性能。

什么是 Spectrum? Spectrum 是一种微调方法,通过信噪比(SNR)分析识别 LLM 中最具信息量的层,并选择性地对它们进行微调,提供与全量微调相当的性能,同时减少了资源使用,特别是在分布式训练环境中。

注意:本指南适用于消费级 GPU(24GB+),如 NVIDIA RTX 4090/5090 或 A10G,但也可根据需要调整以适应更大规模的系统。

定义一个适合微调的用例

LLMs 在 2024 年变得更加强大且更小巧。这通常意味着微调可能不是解决问题的首选。在考虑微调之前,您应始终评估提示或已微调的模型是否能解决您的问题。创建一个评估设置,并比较现有开源模型的性能。

然而,在以下几种情况下,微调显得尤为重要。当你需要:

  • 持续提升在特定任务集上的性能
  • 控制模型输出的样式和格式(例如,强制执行公司的语调)
  • 教授模型领域特定的知识或术语
  • 减少关键应用中的幻觉现象
  • 通过创建更小、更专业化的模型来优化延迟
  • 确保严格遵守特定指南或约束

作为一个示例,我们将使用以下用例:

我们希望微调一个模型,该模型能够解决高中数学问题,以教导学生如何解决数学问题。

这可以作为微调的一个良好用例,因为它需要大量关于数学及其解题方法的领域特定知识。

注意:这是一个虚构的示例,因为现有的开源模型已经能够解决此任务。

设置开发环境

我们的第一步是安装 Hugging Face 库和 Pytorch,包括 trl``、transformersdatasets。如果你还没听说过 trl,别担心。它是在 transformersdatasets 之上的一个新库,使得微调、rlhf、对齐开放 LLMs 变得更加容易。

# Install Pytorch & other libraries
%pip install "torch==2.4.1" tensorboard flash-attn "liger-kernel==0.4.2" "setuptools<71.0.0" "deepspeed==0.15.4" openai "lm-eval[api]==0.4.5"
 
# Install Hugging Face libraries
%pip install  --upgrade \
  "transformers==4.46.3" \
  "datasets==3.1.0" \
  "accelerate==1.1.1" \
  "bitsandbytes==0.44.1" \
  "trl==0.12.1" \
  "peft==0.13.2" \
  "lighteval==0.6.2" \
  "hf-transfer==0.1.8"

我们将使用 Hugging Face Hub 作为远程模型版本控制服务。这意味着我们将在训练期间自动将模型、日志和信息推送到 Hub。为此,您必须在 Hugging Face 上注册。拥有账户后,我们将使用 huggingface_hub 包中的 login 工具登录我们的账户,并将我们的令牌(访问密钥)存储在磁盘上。

from huggingface_hub import login
 
login(token="", add_to_git_credential=True) # ADD YOUR TOKEN HERE

创建并准备数据集

一旦确定微调是正确的解决方案,您将需要一个数据集。大多数数据集现在都是使用自动化合成工作流创建的,采用了LLMs,尽管存在多种方法:

  • 使用 LLMs 进行合成生成:最常见的方法是利用 Distilabel 等框架大规模生成高质量的合成数据
  • 现有数据集:使用 Hugging Face Hub 的公开数据集
  • 人工标注:最高质量但最昂贵的选项 L LM 数据集提供了高质量数据集的概览,用于微调LLMs以满足各种目的。在我们的示例中,我们将使用包含 200,000 道数学世界问题的 Orca-Math 数据集。

trl 这样的现代微调框架支持标准格式:

// Conversation format
{
    "messages": [
        {"role": "system", "content": "You are..."},
        {"role": "user", "content": "..."},
        {"role": "assistant", "content": "..."},
    ]
}
// Instruction format
{"prompt": "<prompt text>", "completion": "<ideal generated text>"}

为了准备我们的数据集,我们将使用 Datasets 库,然后将其转换为对话格式,其中我们在系统消息中包含助手所需的架构定义。随后,我们将数据集保存为 jsonl 文件,以便用于微调我们的模型。

注意:此步骤可能因您的使用场景而异。例如,如果您已经有一个数据集,比如在使用 OpenAI 时获得的,您可以跳过此步骤,直接进入微调步骤。

from datasets import load_dataset
 
# Create system prompt
system_message = """Solve the given high school math problem by providing a clear explanation of each step leading to the final solution.
 
Provide a detailed breakdown of your calculations, beginning with an explanation of the problem and describing how you derive each formula, value, or conclusion. Use logical steps that build upon one another, to arrive at the final answer in a systematic manner.
 
# Steps
 
1. **Understand the Problem**: Restate the given math problem and clearly identify the main question and any important given values.
2. **Set Up**: Identify the key formulas or concepts that could help solve the problem (e.g., algebraic manipulation, geometry formulas, trigonometric identities).
3. **Solve Step-by-Step**: Iteratively progress through each step of the math problem, justifying why each consecutive operation brings you closer to the solution.
4. **Double Check**: If applicable, double check the work for accuracy and sense, and mention potential alternative approaches if any.
5. **Final Answer**: Provide the numerical or algebraic solution clearly, accompanied by appropriate units if relevant.
 
# Notes
 
- Always clearly define any variable or term used.
- Wherever applicable, include unit conversions or context to explain why each formula or step has been chosen.
- Assume the level of mathematics is suitable for high school, and avoid overly advanced math techniques unless they are common at that level.
"""
 
# convert to messages 
def create_conversation(sample):
  return {
    "messages": [
      {"role": "system", "content": system_message},
      {"role": "user", "content": sample["question"]},
      {"role": "assistant", "content": sample["answer"]}
    ]
  }  
 
# Load dataset from the hub
dataset = load_dataset("microsoft/orca-math-word-problems-200k", split="train")
 
# Convert dataset to OAI messages
dataset = dataset.map(create_conversation, remove_columns=dataset.features, batched=False)
 
print(dataset[345]["messages"])
 
# save datasets to disk 
dataset.to_json("train_dataset.json", orient="records")

使用 trlSFTTrainer 对模型进行 QLoRA 微调

我们现在准备对模型进行微调。我们将使用 trlSFTTrainer 来微调我们的模型。 SFTTrainer 使得对开放LLMs进行监督微调变得简单直接。 SFTTrainertransformers 库中 Trainer 的子类,支持所有相同的功能,包括日志记录、评估和检查点保存,但增加了额外的便利功能,包括:

  • 数据集格式化,包括对话和指令格式
  • 仅在补全上进行训练,忽略提示
  • 为更高效训练打包数据集
  • PEFT(参数高效微调)支持,包括 Q-LoRA 或 Spectrum
  • 为对话微调准备模型和分词器(例如添加特殊标记)
  • 使用 accelerateFSDP/DeepSpeed 进行分布式训练

我们准备了一个 run_sft.py 脚本,支持通过提供 YAML 配置文件来运行微调。这使您能够轻松更改模型、数据集、超参数和其他设置。这是通过使用 TrlParser 实现的,它会解析 YAML 文件并将其转换为 TrainingArguments 参数。这样,我们可以使用相同的脚本支持 Q-LoRA、Spectrum 和其他 PEFT 方法。

问题:为什么我们不使用像 axolotl 这样的框架?

这是一个很好的问题!Axolotl 是一个出色的框架,被许多开源构建者使用,并且经过了充分的测试。然而,了解如何手动操作也是很有益的。这将使您更好地理解其内部工作原理以及如何进行定制。特别是在遇到问题或希望扩展脚本并添加新功能时。

在我们开始训练之前,先来看一下我们的训练脚本。该脚本保持了简洁易懂的风格,这将有助于您理解、定制并扩展脚本以适应您的具体需求。我们为参数定义了 dataclasses 。每个参数都可以通过命令行或提供一个 yaml 配置文件来提供。这样,我们就能获得更好的类型安全性和智能感知支持。

# ....
 
@dataclass
class ScriptArguments:
    dataset_id_or_path: str
    ...
# ....

我们可以为不同的训练方法自定义行为,并通过 script_args 在我们的脚本中使用它们。训练脚本通过 ####### 块分隔,以区分脚本的不同部分。主要训练函数:

  • 记录所有超参数
  • 从 Hugging Face Hub 或本地磁盘加载数据集
  • 加载带有我们训练策略的标记器和模型(例如,Q-LoRA,Spectrum)
  • 初始化 SFTTrainer
  • 启动训练循环(可选地从检查点继续训练)
  • 保存模型并可选择将其推送到 Hugging Face Hub

以下是如何使用 Q-LoRA 对 Llama-3.1-8B 模型进行微调的示例配方。

# Model arguments
model_name_or_path: Meta-Llama/Meta-Llama-3.1-8B
tokenizer_name_or_path: Meta-Llama/Meta-Llama-3.1-8B-Instruct
model_revision: main
torch_dtype: bfloat16
attn_implementation: flash_attention_2
use_liger: true
bf16: true
tf32: true
output_dir: runs/llama-3-1-8b-math-orca-qlora-10k-ep1
 
# Dataset arguments
dataset_id_or_path: train_dataset.json
max_seq_length: 1024
packing: true
 
# LoRA arguments
use_peft: true
load_in_4bit: true
lora_target_modules: "all-linear"
# important as we need to train the special tokens for the chat template of llama 
lora_modules_to_save: ["lm_head", "embed_tokens"] # you might need to change this for qwen or other models
lora_r: 16
lora_alpha: 16
 
# Training arguments
num_train_epochs: 1
per_device_train_batch_size: 8
gradient_accumulation_steps: 2
gradient_checkpointing: true
gradient_checkpointing_kwargs:
  use_reentrant: false
learning_rate: 2.0e-4 
lr_scheduler_type: constant
warmup_ratio: 0.1
 
# Logging arguments
logging_strategy: steps
logging_steps: 5
report_to:
- tensorboard
save_strategy: "epoch"
seed: 42
 
# Hugging Face Hub 
push_to_hub: true
# hub_model_id: llama-3-1-8b-math-orca-qlora-10k-ep1 # if not defined same as output_dir
hub_strategy: every_save

此配置适用于单 GPU 训练以及使用 DeepSpeed 的多 GPU 训练。

!python scripts/run_sft.py --config receipes/llama-3-1-8b-qlora.yaml

我使用了几种不同的优化策略进行了多次实验,包括 Flash Attention、Liger Kernels、Q-Lora 和 Spectrum 方法,以比较微调模型所需的时间。结果总结在以下表格中:

Model训练样本硬件方法训练序列长度每设备批量大小梯度累积打包闪存注意力Liger 内核估计优化步骤估计训练时间
Llama-3.1-8B10,0001x L4 24GBQ-LoRA1024125000~360 分钟
Llama-3.1-8B10,0001x L4 24GBQ-LoRA1024221352约 290 分钟
Llama-3.1-8B10,0001x L4 24GBQ-LoRA102424676约 220 分钟
Llama-3.1-8B10,0001x L4 24GBQ-LoRA102444338约 135 分钟
Llama-3.1-8B10,0004x L4 24GBQ-LoRA10248284约 33 分钟
Llama-3.1-8B10,0008x L4 24GBQ-LoRA10248242约 18 分钟
Llama-3.1-8B10,0008x L4 24GBSpectrum(30%)10248242约 21 分钟

注释:

  • Q-Lora 包括训练嵌入层和 lm_head,因为我们使用 Llama 3.1 聊天模板,而在基础模型中特殊标记未经过训练。
  • 对于分布式训练,使用了 Deepspeed (0.15.4) 与 ZeRO3 以及 Hugging Face Accelerate。
  • 在 30%信噪比层的情况下,Spectrum 比 Q-Lora 稍慢,但在 GSM8K 数据集上达到了 58%的准确率,比 Q-Lora 高出 4%。

使用 Q-LoRA 仅保存训练后的适配器权重。如果您希望将模型作为独立模型使用,例如用于推理,您可能需要合并适配器和基础模型。这可以通过以下命令完成:

!python scripts/merge_adapter_weights.py --peft_model_id runs/llama-3-1-8b-math-orca-qlora-10k-ep1 --push_to_hub True --repository_id llama-3-1-8b-math-orca-qlora-10k-ep1-merged

测试模型并运行推理

训练完成后,我们希望对模型进行评估和测试。由于我们在解决数学问题的任务上训练了模型,因此我们将在 GSM8K 数据集上评估模型。GSM8K(小学数学 8K)是一个包含 8.5K 高质量、语言多样的小学数学应用题数据集。该数据集旨在支持对需要多步推理的基础数学问题的问答任务。

评估生成式 AI 模型并非易事,因为一个输入可能对应多个正确输出。

我们将使用 Evaluation Harness,一个开源框架,在广泛的任务和基准上评估语言模型。该框架支持评估位于 OpenAI 兼容 API 端点后的模型,这些端点可以是本地的或远程的。这非常有帮助,因为我们可以在我们将用于生产的环境中评估我们的模型。

我们将使用文本生成推理(Text Generation Inference, TGI)来测试和部署我们的模型。TGI 是为部署和服务大型语言模型(LLMs)而专门构建的解决方案。TGI 通过张量并行和连续批处理实现高性能文本生成。如果你正在使用或计划使用 vLLM,可以查看附录了解如何启动推理服务器。

注意:确保您的 GPU 内存足够运行容器。重启内核以释放笔记本中所有已分配的 GPU 内存。

我们将从 1 个 GPU 开始,以分离模式运行。这意味着我们可以在容器运行时继续使用笔记本。如果你有更多的 GPU,可以将 --gpus 和 --num-shard 标志更改为 GPU 的数量。

%%bash
 
num_gpus=1
model_id=philschmid/llama-3-1-8b-math-orca-spectrum-10k-ep1 # replace with your model id
 
docker run --name tgi --gpus ${num_gpus} -d -ti -p 8080:80 --shm-size=2GB \
  -e HF_TOKEN=$(cat ~/.cache/huggingface/token) \
  ghcr.io/huggingface/text-generation-inference:3.0.1 \
  --model-id ${model_id} \
  --num-shard ${num_gpus}

我们的容器现在将在后台启动,并从 Hugging Face Hub 下载模型。我们可以通过 docker logs -f tgi 查看日志以了解进度。

一旦我们的容器运行起来,我们可以使用 openaihuggingface_hub SDK 发送请求。这里我们将使用 openai SDK 向推理服务器发送请求。如果你还没有安装 openai SDK,可以使用 pip install openai 进行安装。

from openai import OpenAI
 
# create client 
client = OpenAI(base_url="http://localhost:8080/v1",api_key="-")
 
system_message = """Solve the given high school math problem by providing a clear explanation of each step leading to the final solution.
 
Provide a detailed breakdown of your calculations, beginning with an explanation of the problem and describing how you derive each formula, value, or conclusion. Use logical steps that build upon one another, to arrive at the final answer in a systematic manner.
 
# Steps
 
1. **Understand the Problem**: Restate the given math problem and clearly identify the main question and any important given values.
2. **Set Up**: Identify the key formulas or concepts that could help solve the problem (e.g., algebraic manipulation, geometry formulas, trigonometric identities).
3. **Solve Step-by-Step**: Iteratively progress through each step of the math problem, justifying why each consecutive operation brings you closer to the solution.
4. **Double Check**: If applicable, double check the work for accuracy and sense, and mention potential alternative approaches if any.
5. **Final Answer**: Provide the numerical or algebraic solution clearly, accompanied by appropriate units if relevant.
 
# Notes
 
- Always clearly define any variable or term used.
- Wherever applicable, include unit conversions or context to explain why each formula or step has been chosen.
- Assume the level of mathematics is suitable for high school, and avoid overly advanced math techniques unless they are common at that level.
"""
 
messages = [
    {"role": "system", "content": system_message},
    {"role": "user", "content": "Natalia sold clips to 48 of her friends in April, and then she sold half as many clips in May. How many clips did Natalia sell altogether in April and May?"},
]
expected_answer = "72"
 
# Take a random sample from the dataset and remove the last message and send it to the model
response = client.chat.completions.create(
	model="orca",
	messages=messages,
	stream=False, # no streaming
	max_tokens=256,
)
response = response.choices[0].message.content
 
# Print results
print(f"Query:\n{messages[1]['content']}")
print(f"Original Answer:\n{expected_answer}")
print(f"Generated Answer:\n{response}")

太棒了,看起来很棒!现在我们可以用评估工具来评估我们的模型了。

注意:请确保将模型 ID 更改为您微调后的模型。

!lm_eval --model local-chat-completions \
  --tasks gsm8k_cot \
  --model_args model=philschmid/llama-3-1-8b-math-orca-spectrum-10k-ep1,base_url=http://localhost:8080/v1/chat/completions,num_concurrent=8,max_retries=3,tokenized_requests=False \
  --apply_chat_template \
  --fewshot_as_multiturn

哇,仅使用 10k 样本就达到了 54%的准确率,相当不错!我们成功验证了我们的模型能够解决数学问题。现在,别忘了完成后停止你的容器。

结论

本指南为 2025 年微调LLMs提供了基础。模块化的训练脚本和配置使得适应您的特定用例变得简单,无论您是在单个 GPU 上训练还是跨多个节点进行扩展。