跳至主要内容

Unsloth 指南:优化并加速 LLM 微调

使用 Unsloth 微调 Llama 3.1 模型,以高准确率与详尽结果解决专业代数问题。
· 11分钟 阅读

使用 AI 探索

在微调模型时,您是否遇到过内存问题,或觉得整个过程耗时难以忍受?如果您正在寻找比 Transformers 库更高效、更快速的方式来微调大语言模型(LLM),那您来对地方了!

在本教程中,我们将演示如何仅用 9GB 显存微调 Llama 3.1 3B 模型,速度比传统 Transformers 方法快 2 倍。我们还将介绍如何进行快速推理、将模型转换为 vLLM 兼容和 GGUF 文件格式,并仅用几行代码将已保存的模型无缝推送到 Hugging Face Hub。

如果您对这些概念还不熟悉,建议先学习大型语言模型(LLM)概念精通课程,打好基础再开始微调。

Fast Fine-tuning LLMs with Unsloth Feature Image

图片由作者提供

什么是 Unsloth?

Unsloth AI 是一个用于快速微调与调用大语言模型的 Python 框架。它提供简单的 API,且性能较 Transformers 快 2 倍。

核心特性

  • 简单的 API: Unsloth 的一大亮点是其简洁的 API,显著降低了微调 LLM 的复杂度。
  • 与 Transformers 集成: Unsloth 构建在广受欢迎的 Transformers 库之上,既能充分利用 Hugging Face 生态的强大功能,又通过自身增强来简化微调流程。
  • 资源需求低: 您可以使用免费的 Colab GPU,甚至带有 GPU 的笔记本电脑来微调大语言模型。
  • 保存为 vLLM 与 GGUF 格式: 仅需一行代码即可合并、转换并将量化模型推送到 Hugging Face Hub。
  • 高效的内存使用: 您可以加载 4-bit 量化模型进行微调,并与全量模型合并,且不超过 12 GB GPU 显存。

使用 Unsloth 访问 LLM

使用 Unsloth 访问 Llama-3.1 模型非常简单。我们不会加载 16-bit 模型,而是加载 Hugging Face 上的 4-bit 版本,以节省 GPU 内存并加速推理。

  1. 安装 Unsloth Python 包。
%%capture
%pip install unsloth
  1. 使用 FastLanguageModel 函数加载模型和分词器。
from transformers import TextStreamer
from unsloth import FastLanguageModel

model, tokenizer = FastLanguageModel.from_pretrained(
    model_name = "unsloth/Meta-Llama-3.1-8B-bnb-4bit"
)

Loading the unsloth/Meta-Llama-3.1-8B-bnb-4bit

  1. 启用 Unsloth 快速推理。
FastLanguageModel.for_inference(model)
  1. 使用系统提示、指令、输入和响应来编写提示模板。
  2. 基于提示模板创建一个示例提示。
  3. 将文本转换为 token,再转换为数值表示。
  4. 设置文本流式输出。
  5. 使用模型生成流式响应。
prompt_style = """Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.

### Instruction:
{}

### Input:
{}

### Response:
{}"""


inputs = tokenizer(
    [
        prompt_style.format(
            "You are a professional machine learning engineer",  
            "How would you deal with NaN validation loss?",  
            "", 
        )
    ],
    return_tensors="pt",
).to("cuda")

text_streamer = TextStreamer(tokenizer)
_ = model.generate(**inputs, streamer=text_streamer, max_new_tokens=128)

我们收到了关于如何处理 NaN 验证损失的积极回应。

Output generated using unsloth/Meta-Llama-3.1-8B-bnb-4bit

如果您在 Kaggle 上运行上述代码遇到问题,这里有示例 Notebook:使用 Unsloth 访问 LLM。

通过学习 用 Python 入门 LLM课程,您将了解 Transformer 架构的基础,包括如何构建、微调与评估。它是您迈入 Python 进行 LLM 微调世界的入门之路。

使用 Unsloth 微调 Llama 3.1 模型

在本指南中,我们将使用 Unsloth 在lighteval/MATH 数据集上微调 Llama 3.1。该数据集包含以 Markdown 格式给出的代数题及其解答,整体分为五个难度级别。

为更好理解本指南,建议您阅读大型语言模型微调入门指南,了解微调 LLM 各步骤背后的原理。先用几分钟浏览该指南,有助于理解本文过程。

1. 环境配置

我们使用 Kaggle Notebook 作为云端 IDE,在处理模型或数据前需要先完成 Notebook 配置。

  1. 首先,使用 Kaggle secrets 设置 Hugging Face API 和 Weights & Biases API 的环境变量。

Setting up Hugging Face and W&B API key as environment variable.

  1. 将加速器设置为 P100 GPU。
  2. 加载环境变量,并使用 API 密钥登录 Hugging Face CLI。
from huggingface_hub import login
from kaggle_secrets import UserSecretsClient
user_secrets = UserSecretsClient()

hf_token = user_secrets.get_secret("HUGGINGFACE_TOKEN")
login(hf_token)
  1. 使用 API 密钥登录 Weights & Biases,初始化项目。
import wandb

wb_token = user_secrets.get_secret("wandb")

wandb.login(key=wb_token)
run = wandb.init(
    project='Fine-tune Llama-3.1-8B-bnb-4bit on Math Dataset', 
    job_type="training", 
    anonymous="allow"
)

2. 加载模型与分词器

与推理部分相同,我们将加载 Llama 3.1 的 4-bit 量化版本,最大序列长度为 2048,数据类型为 None。

from unsloth import FastLanguageModel
import torch

max_seq_length = 2048 
dtype = None 

model, tokenizer = FastLanguageModel.from_pretrained(
    model_name = "unsloth/Meta-Llama-3.1-8B-bnb-4bit",
    max_seq_length = max_seq_length,
    dtype = dtype
)

3. 加载与处理数据集

随后我们将加载并处理数据集。为此,需先创建一个提示模板,帮助我们获得所需结果。

提示模板包含系统提示、指令,以及输入与响应的占位符。

prompt_style = """Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.

### Instruction:
You are a math genius who can solve any level of algebraic problems. Please answer the following math question.

### Input:
{}

### Response:
{}"""

接下来创建函数,使用提示模板将数学题与解答作为输入,转换为规范文本。请务必添加 EOS_TOKEN 以避免重复生成。

EOS_TOKEN = tokenizer.eos_token # Must add EOS_TOKEN
def formatting_prompts_func(examples):
    inputs       = examples["problem"]
    outputs      = examples["solution"]
    texts = []
    for input, output in zip(inputs, outputs):
        text = prompt_style.format(input, output) + EOS_TOKEN
        texts.append(text)
    return { "text" : texts, }

从数据集中加载 500 个样本,对数据集应用 formatting_prompts_func 函数,并查看 text 列的首个样本。

from datasets import load_dataset

dataset = load_dataset("lighteval/MATH", split="train[0:500]", trust_remote_code=True)
dataset = dataset.map(
    formatting_prompts_func,
    batched=True,
)
dataset["text"][0]

文本包含系统提示、指令、代数题目以及对应解答。

Math Dataset from Hugging Face

4. 模型设置

我们使用基础模型中的线性模块为模型添加 LoRA(低秩适配器)。

model = FastLanguageModel.get_peft_model(
    model,
    r=16, 
    target_modules=[
        "q_proj",
        "k_proj",
        "v_proj",
        "o_proj",
        "gate_proj",
        "up_proj",
        "down_proj",
    ],
    lora_alpha=16,
    lora_dropout=0, 
    bias="none", 
   
    use_gradient_checkpointing="unsloth", 
    random_state=3407,
    use_rslora=False, 
    loftq_config=None,
)

使用模型、分词器、最大序列长度和训练参数来设置 trainer。这与使用 Transformers 与 TRL 库设置 trainer 十分类似。

from trl import SFTTrainer
from transformers import TrainingArguments
from unsloth import is_bfloat16_supported

trainer = SFTTrainer(
    model = model,
    tokenizer = tokenizer,
    train_dataset = dataset,
    dataset_text_field = "text",
    max_seq_length = max_seq_length,
    dataset_num_proc = 2,
    args = TrainingArguments(
        per_device_train_batch_size = 2,
        gradient_accumulation_steps = 4,
        warmup_steps = 5,
        max_steps = 60,
        learning_rate = 2e-4,
        fp16 = not is_bfloat16_supported(),
        bf16 = is_bfloat16_supported(),
        logging_steps = 1,
        optim = "adamw_8bit",
        weight_decay = 0.01,
        lr_scheduler_type = "linear",
        seed = 3407,
        output_dir = "outputs",
    ),
)

5. 模型训练

模型训练耗时将近 19 分钟,表现相当不错。训练损失在每一步都有显著降低。

trainer_stats = trainer.train()

Model training loss

您可以前往 Weights & Biases 仪表盘查看详细的模型性能与硬件指标。

W&B dashboard. Detailed model evaluation.

您也可以使用 torch 与 trainer_stats 生成自己的报告。

used_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)
used_memory_for_lora = round(used_memory - start_gpu_memory, 3)
used_percentage = round(used_memory         /max_memory*100, 3)
lora_percentage = round(used_memory_for_lora/max_memory*100, 3)
print(f"{trainer_stats.metrics['train_runtime']} seconds used for training.")
print(f"{round(trainer_stats.metrics['train_runtime']/60, 2)} minutes used for training.")
print(f"Peak reserved memory = {used_memory} GB.")
print(f"Peak reserved memory for training = {used_memory_for_lora} GB.")
print(f"Peak reserved memory % of max memory = {used_percentage} %.")
print(f"Peak reserved memory for training % of max memory = {lora_percentage} %.")

模型在 20 分钟内完成训练,训练峰值预留内存约为 3.7 GB。与传统方法(先加载全量模型再量化并微调)相比,这一数值相当低;传统方法至少需要 15 GB GPU 内存。

1207.5478 seconds used for training.
20.13 minutes used for training.
Peak reserved memory = 9.73 GB.
Peak reserved memory for training = 3.746 GB.
Peak reserved memory % of max memory = 61.241 %.
Peak reserved memory for training % of max memory = 23.578 %.

6. 微调后测试模型

要在微调后测试模型,我们需要激活快速推理。接着,从数据集中选取一个代数题填入提示模板并转为 token,然后使用模型生成响应,并对输出进行解码以显示文本。

生成文本为 Markdown 格式,我们已将其转换为能正确显示数学公式的文本。

from IPython.display import display, Markdown

FastLanguageModel.for_inference(model)
inputs = tokenizer(
    [
        prompt_style.format(
            "If the system of equations \begin{align*} 3x+y&=a,\\ 2x+5y&=2a, \end{align*} has a solution $(x,y)$ when $x=2$, compute $a$.", 
            "", 
        )
    ],
    return_tensors="pt",
).to("cuda")

outputs = model.generate(
    input_ids=inputs.input_ids,
    attention_mask=inputs.attention_mask,
    max_new_tokens=250,
    use_cache=True,
)
response = tokenizer.batch_decode(outputs)
Markdown(response[0].split("\n\n### Response:")[1])

可以看到,生成的响应与数据集一致,并给出了代数题的解答。

Output of the fine-tuned model

7. 保存模型与分词器

现在将我们的微调模型保存并推送到 Hugging Face Hub,便于分享或部署。

new_model_online = "kingabzpro/Llama-3.1-8B-MATH"
new_model_local = "Llama-3.1-8B-MATH"
model.save_pretrained(new_model_local) # Local saving
tokenizer.save_pretrained(new_model_local) # Local saving

下面的代码将在 Hugging Face 上创建一个新仓库,然后连同元数据一起推送 LoRA 与分词器。

model.push_to_hub(new_model_online) # Online saving
tokenizer.push_to_hub(new_model_online) # Online saving

Fine-tuned model saved on Hugging Face

来源:kingabzpro/Llama-3.1-8B-MATH

如果您在使用 Unsloth 微调 LLM 时遇到问题,请参考使用 Unsloth 微调 LLM Kaggle Notebook。它包含可稳定运行的代码,便于复现实验结果。

您 AI 学习旅程的下一步是使用 LLM 开发高效的 AI 应用。更多信息请参阅使用 LangChain 构建 LLM 应用教程。

合并与导出模型

将 LoRA 与基础模型合并需要额外的显存和本地存储。为避免超限,我们将创建一个新的 Kaggle Notebook,并将加速器设置为 P100 GPU。同时启用 Hugging Face API 以访问 API 密钥。

  1. 安装 Unsloth Python 包。
%%capture
%pip install unsloth
  1. 从 Kaggle secrets 加载 API 密钥并登录 Hugging Face CLI。
from huggingface_hub import login
from kaggle_secrets import UserSecretsClient
user_secrets = UserSecretsClient()

hf_token = user_secrets.get_secret("HUGGINGFACE_TOKEN")
login(hf_token)
  1. 通过提供 Hugging Face 模型仓库链接加载 LoRA 与分词器。
  2. 启用快速模型推理。
from unsloth import FastLanguageModel

new_model_name = "kingabzpro/Llama-3.1-8B-MATH"
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name = new_model_name, # YOUR MODEL YOU USED FOR TRAINING
    max_seq_length = 2048,
    dtype = None,
    load_in_4bit = True,
)
FastLanguageModel.for_inference(model); 

Loading the fine-tuned adopter

  1. 复制并粘贴上一节中的提示模板。
prompt_style = """Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.

### Instruction:
You are a math genius who can solve any level of algebraic problem. Please answer the following math question.

### Input:
{}

### Response:
{}"""
  1. 使用数据集中的示例数学题测试模型,确保其仍然工作正常。
from IPython.display import display, Markdown

inputs = tokenizer(
    [
        prompt_style.format(
            "Solve the equation $|y-6| + 2y = 9$ for $y$.",  # input
            "",  # output - leave this blank for generation!
        )
    ],
    return_tensors="pt",
).to("cuda")

outputs = model.generate(
    input_ids=inputs.input_ids,
    attention_mask=inputs.attention_mask,
    max_new_tokens=250,
    pad_token_id=tokenizer.eos_token_id,
)
response = tokenizer.batch_decode(outputs)
Markdown(response[0].split("\n\n### Response:")[1])

模型运行良好,可以与基础模型合并。

output of the fine-tuned model.

  1. 创建临时目录并更改目录指针。为何要这样做?Kaggle 的 working 目录仅提供 20 GB,不足以合并并推送完整模型。在根目录创建另一个文件夹可使用 60 GB 临时存储。
%mkdir ../temp
%cd /kaggle/temp
  1. 通过以下命令,我们将 LoRA 与基础模型合并并推送至 Hugging Face Hub。当您想用 vLLM Python 包在云端部署与服务模型时,这非常有用。
model.push_to_hub_merged(new_model_name, tokenizer, save_method = "merged_16bit")

Merging and saving the 16bit model.

  1. 如果您希望在本地使用微调后的模型,可以轻松将其转换为 llama.cpp 格式,再进行量化,并推送至 Hugging Face Hub。
model.push_to_hub_gguf(new_model_name, tokenizer, quantization_method = "q4_k_m")

saving the quantized model in GGUF model format

完整的 16-bit 模型、适配器与量化后的 GGUF 文件均已在您的模型仓库中可用。要在本地使用微调模型,只需下载量化的 GGUF 文件,即可配合 Jan、Misty、GPT4ALL 或 Ollama 使用。

请参考微调 Llama 3.2 并在本地使用:分步指南教程,学习如何在客服数据集上微调 Llama 3.2、将模型合并导出到 Hugging Face Hub,并将微调模型转换为 GGUF 格式,以便在本地使用 Jan 应用。

来源:kingabzpro/Llama-3.1-8B-MATH

如果您在加载或将 LoRA 与基础模型合并时遇到问题,请查看使用 Unsloth 合并 LoRA 适配器 Kaggle Notebook 以获取进一步帮助。

同时也推荐阅读博客12 个适合各水平的 LLM 项目,其中列出了适合初学者、中级学习者、毕业生与专家的 LLM 项目。

结语

在本教程中,我们学习了如何以较低的计算资源高效微调 Llama 3.1 3B 模型,并获得比传统方法快 2 倍的速度。

我们还学习了如何进行快速模型推理、将 LoRA 与基础模型合并,并推送至 Hugging Face Hub。

此外,我们将模型转换为 llama.cpp 格式并进行量化,使微调模型能在笔记本电脑上通过 Jan 或 GPT4ALL 等应用轻松本地使用。

为进一步提升模型性能,我们强烈建议在完整数据集上进行微调、优化超参数,并打磨提示模板。在微调之后,您仍可以通过函数调用与 RAG 流水线等方式显著提升 AI 应用的表现。

要了解哪种方案更适合您的具体场景,请参阅RAG vs 微调:含实用示例的全面教程指南。文中包含示例代码,您可在自己的数据集上尝试并比较结果。

Abid Ali Awan

作者

Abid Ali Awan

作为一名持证的数据科学家,我热衷于利用前沿技术打造创新的机器学习应用。凭借在语音识别、数据分析与报告、MLOps、对话式人工智能以及自然语言处理方面的扎实背景,我不断打磨构建智能系统的能力,力求带来切实影响。除技术专长外,我也擅长沟通,能够将复杂概念提炼为清晰、简明的表述。因此,我成为数据科学领域备受关注的博主,与不断壮大的数据专业人士社区分享洞见与实践经验。目前,我专注于内容创作与编辑,借助大语言模型打造有力且吸引人的内容,帮助企业与个人更好地发挥数据价值。

主题