Auto Byte

专注未来出行及智能汽车科技

微信扫一扫获取更多资讯

Science AI

关注人工智能与其他前沿技术、基础学科的交叉研究与融合发展

微信扫一扫获取更多资讯

仅用250美元,Hugging Face技术主管手把手教你微调Llama 3

语言模型的微调一直是说起来容易做起来难的事儿。近日 Hugging Face 技术主管 Philipp Schmid 发表了一篇博客,详细讲解了如何利用 Hugging Face 上的库和 fsdp 以及 Q-Lora 对大模型进行微调。

图片

我们知道,Meta 推出的 Llama 3、Mistral AI 推出的 Mistral 和 Mixtral 模型以及 AI21 实验室推出的 Jamba 等开源大语言模型已经成为 OpenAI 的竞争对手。

不过,大多数情况下,使用者需要根据自己的数据对这些开源模型进行微调,才能充分释放模型的潜力。

虽然在单个 GPU 上使用 Q-Lora 对较小的大语言模型(如 Mistral)进行微调不是难事,但对像 Llama 3 70b 或 Mixtral 这样的大模型的高效微调直到现在仍是一个难题。

因此,Hugging Face 技术主管 Philipp Schmid 介绍了如何使用 PyTorch FSDP 和 Q-Lora,并在 Hugging Face 的 TRL、Transformers、peft 和 datasets 等库的帮助下,对 Llama 3 进行微调。除了 FSDP,作者还对 PyTorch 2.2 更新后的 Flash Attention v2 也进行了适配。

微调主要步骤如下:

  • 设置开发环境
  • 创建并加载数据集
  • 使用 PyTorch FSDP、Q-Lora 和 SDPA 微调大语言模型
  • 测试模型并进行推理

注:本文进行的实验是在英伟达(NVIDIA)H100 和英伟达(NVIDIA)A10G GPU 上创建和验证的。配置文件和代码针对 4xA10G GPU 进行了优化,每个 GPU 均配备 24GB 内存。如果使用者有更多的算力,第 3 步提到的配置文件(yaml 文件)需要做相应的修改。

FSDP+Q-Lora 背景知识

基于一项由 Answer.AI、Q-Lora 创建者 Tim Dettmers 和 Hugging Face 共同参与的合作项目,作者对 Q-Lora 和 PyTorch FSDP(完全共享数据并行)所能提供的技术支持进行了总结。

FSDP 和 Q-Lora 的结合使用能让使用者在 2 个消费级 GPU(24GB)上就能对 Llama 2 70b 或 Mixtral 8x7B 进行微调,细节可以参考下面文章。其中 Hugging Face 的 PEFT 库对此有至关重要的作用。

文章地址:https://www.answer.ai/posts/2024-03-06-fsdp-qlora.html

PyTorch FSDP 是一种数据 / 模型并行技术,它可以跨 GPU 分割模型,减少内存需求,并能够更有效地训练更大的模型。Q-LoRA 是一种微调方法,它利用量化和低秩适配器来有效地减少计算需求和内存占用。

设置开发环境

第一步是安装 Hugging Face Libraries 以及 Pyroch,包括 trl、transformers 和 datasets 等库。trl 是建立在 transformers 和 datasets 基础上的一个新库,能让对开源大语言模型进行微调、RLHF 和对齐变得更容易。
# Install Pytorch for FSDP and FA/SDPA

%pip install "torch==2.2.2" tensorboard

# Install Hugging Face libraries

%pip install  --upgrade "transformers==4.40.0" "datasets==2.18.0" "accelerate==0.29.3" "evaluate==0.4.1" "bitsandbytes==0.43.1" "huggingface_hub==0.22.2" "trl==0.8.6" "peft==0.10.0"

接下来,登录 Hugging Face 获取 Llama 3 70b 模型。

创建和加载数据集

环境设置完成后,我们就可以开始创建和准备数据集了。微调用的数据集应该包含使用者想要解决的任务的示例样本。阅读《如何在 2024 年使用 Hugging Face 微调 LLM》可以进一步了解如何创建数据集。

文章地址:https://www.philschmid.de/fine-tune-llms-in-2024-with-trl#3-create-and-prepare-the-dataset

作者使用了 HuggingFaceH4/no_robots 数据集,这是一个包含 10,000 条指令和样本的高质量数据集,并且经过了高质量的数据标注。这些数据可用于有监督微调(SFT),使语言模型更好地遵循人类指令。no_robots 数据集以 OpenAI 发表的 InstructGPT 论文中描述的人类指令数据集为原型,并且主要由单句指令组成。
{"messages": [{"role": "system", "content": "You are..."}, {"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]}

{"messages": [{"role": "system", "content": "You are..."}, {"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]}

{"messages": [{"role": "system", "content": "You are..."}, {"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]}

no_robots 数据集中的 10,000 个样本,被分为 9,500 个训练样本和 500 个测试样本,其中有些样本不包含 system 信息。作者使用 datasets 库加载数据集,添加了缺失的 system 信息,并将它们保存到单独的 json 文件中。示例代码如下所示:
from datasets import load_dataset

# Convert dataset to OAI messages

system_message = """You are Llama, an AI assistant created by Philipp to be helpful and honest. Your knowledge spans a wide range of topics, allowing you to engage in substantive conversations and provide analysis on complex subjects."""

def create_conversation(sample):

    if sample["messages"][0]["role"] == "system":

        return sample
    else:

      sample["messages"] = [{"role": "system", "content": system_message}] + sample["messages"]

      return sample

# Load dataset from the hub

dataset = load_dataset("HuggingFaceH4/no_robots")

# Add system message to each conversation

columns_to_remove = list(dataset["train"].features)

columns_to_remove.remove("messages")

dataset = dataset.map(create_conversation, remove_columns=columns_to_remove,batched=False)

# Filter out conversations which are corrupted with wrong turns, keep which have even number of turns after adding system message

dataset["train"] = dataset["train"].filter(lambda x: len(x["messages"][1:]) % 2 == 0)

dataset["test"] = dataset["test"].filter(lambda x: len(x["messages"][1:]) % 2 == 0)

# save datasets to disk

dataset["train"].to_json("train_dataset.json", orient="records", force_ascii=False)

dataset["test"].to_json("test_dataset.json", orient="records", force_ascii=False)

使用 PyTorch FSDP、Q-Lora 和 SDPA 来微调 LLM

接下来使用 PyTorch FSDP、Q-Lora 和 SDPA 对大语言模型进行微调。作者是在分布式设备中运行模型,因此需要使用 torchrun 和 python 脚本启动训练。

作者编写了 run_fsdp_qlora.py 脚本,其作用是从磁盘加载数据集、初始化模型和分词器并开始模型训练。脚本使用 trl 库中的 SFTTrainer 来对模型进行微调。

SFTTrainer 能够让对开源大语言模型的有监督微调更加容易上手,具体来说有以下几点:

  • 格式化的数据集,包括格式化的多轮会话和指令(已使用)
  • 只对完整的内容进行训练,忽略只有 prompts 的情况(未使用)
  • 打包数据集,提高训练效率(已使用)
  • 支持参数高效微调技术,包括 Q-LoRA(已使用)
  • 为会话级任务微调初始化模型和分词器(未使用,见下文)

注意:作者使用的是类似于 Anthropic/Vicuna 的聊天模板,设置了「用户」和「助手」角色。这样做是因为基础 Llama 3 中的特殊分词器(<|begin_of_text|> 及 <|reserved_special_token_XX|>)没有经过训练。

这意味着如果要在模板中使用这些分词器,还需要对它们进行训练,并更新嵌入层和 lm_head,对内存会产生额外的需求。如果使用者有更多的算力,可以修改 run_fsdp_qlora.py 脚本中的 LLAMA_3_CHAT_TEMPLATE 环境变量。

在配置参数方面,作者使用了新的 TrlParser 变量,它允许我们在 yaml 文件中提供超参数,或者通过明确地将参数传递给 CLI 来覆盖配置文件中的参数,例如 —num_epochs 10。以下是在 4x A10G GPU 或 4x24GB GPU 上微调 Llama 3 70B 的配置文件。
%%writefile llama_3_70b_fsdp_qlora.yaml
# script parameters

model_id: "meta-llama/Meta-Llama-3-70b" # Hugging Face model id

dataset_path: "."                      # path to dataset

max_seq_len:  3072 # 2048              # max sequence length for model and packing of the dataset

# training parameters

output_dir: "./llama-3-70b-hf-no-robot" # Temporary output directory for model checkpoints

report_to: "tensorboard"               # report metrics to tensorboard

learning_rate: 0.0002                  # learning rate 2e-4

lr_scheduler_type: "constant"          # learning rate scheduler

num_train_epochs: 3                    # number of training epochs

per_device_train_batch_size: 1         # batch size per device during training

per_device_eval_batch_size: 1          # batch size for evaluation

gradient_accumulation_steps: 2         # number of steps before performing a backward/update pass

optim: adamw_torch                     # use torch adamw optimizer

logging_steps: 10                      # log every 10 steps

save_strategy: epoch                   # save checkpoint every epoch

evaluation_strategy: epoch             # evaluate every epoch

max_grad_norm: 0.3                     # max gradient norm

warmup_ratio: 0.03                     # warmup ratio

bf16: true                             # use bfloat16 precision

tf32: true                             # use tf32 precision

gradient_checkpointing: true           # use gradient checkpointing to save memory

# FSDP parameters: https://huggingface.co/docs/transformers/main/en/fsdp

fsdp: "full_shard auto_wrap offload" # remove offload if enough GPU memory

fsdp_config:

  backward_prefetch: "backward_pre"

  forward_prefetch: "false"

  use_orig_params: "false"

注意:训练结束时,GPU 内存使用量会略有增加(约 10%),这是因为模型保存所带来的开销。所以使用时,请确保 GPU 上有足够的内存来保存模型。

在启动模型训练阶段,作者使用 torchrun 来更加灵活地运用样本,并且易于被调整,就像 Amazon SageMaker 及 Google Cloud Vertex AI 一样。

对于 torchrun 和 FSDP,作者需要对环境变量 ACCELERATE_USE_FSDP 和 FSDP_CPU_RAM_EFFICIENT_LOADING 进行设置,来告诉 transformers/accelerate 使用 FSDP 并以节省内存的方式加载模型。

注意:如果想不使用 CPU offloading 功能,需要更改 fsdp 的设置。这种操作只适用于内存大于 40GB 的 GPU。

本文使用以下命令启动训练:
!ACCELERATE_USE_FSDP=1 FSDP_CPU_RAM_EFFICIENT_LOADING=1 torchrun --nproc_per_node=4 ./scripts/run_fsdp_qlora.py --config llama_3_70b_fsdp_qlora.yaml

预期内存使用情况:

  • 使用 FSDP 进行全微调需要约 16 块 80GB 内存的 GPU
  • FSDP+LoRA 需要约 8 块 80GB 内存的 GPU
  • FSDP+Q-Lora 需要约 2 块 40GB 内存的 GPU
  • FSDP+Q-Lora+CPU offloading 技术需要 4 块 24GB 内存的 GPU,以及一块具备 22 GB 内存的 GPU 和 127 GB 的 CPU RAM,序列长度为 3072、batch 大小为 1。

在 g5.12xlarge 服务器上,基于包含 1 万个样本的数据集,作者使用 Flash Attention 对 Llama 3 70B 进行 3 个 epoch 的训练,总共需要 45 小时。每小时成本为 5.67 美元,总成本为 255.15 美元。这听起来很贵,但可以让你在较小的 GPU 资源上对 Llama 3 70B 进行微调。

如果我们将训练扩展到 4x H100 GPU,训练时间将缩短至大约 125 小时。如果假设 1 台 H100 的成本为 5-10 美元 / 小时,那么总成本将在 25-50 美元之间。

我们需要在易用性和性能之间做出权衡。如果能获得更多更好的计算资源,就能减少训练时间和成本,但即使只有少量资源,也能对 Llama 3 70B 进行微调。对于 4x A10G GPU 而言,需要将模型加载到 CPU 上,这就降低了总体 flops,因此成本和性能会有所不同。

注意:在作者进行的评估和测试过程中,他注意到大约 40 个最大步长(将 80 个样本堆叠为长度为三千的序列)就足以获得初步结果。40 个步长的训练时间约为 1 小时,成本约合 5 美元。

可选步骤:将 LoRA 的适配器融入原始模型

使用 QLoRA 时,作者只训练适配器而不对整个模型做出修改。这意味着在训练过程中保存模型时,只保存适配器权重,而不保存完整模型。

如果使用者想保存完整的模型,使其更容易与文本生成推理器一起使用,则可以使用 merge_and_unload 方法将适配器权重合并到模型权重中,然后使用 save_pretrained 方法保存模型。这将保存一个默认模型,可用于推理。

注意:CPU 内存需要大于 192GB。
#### COMMENT IN TO MERGE PEFT AND BASE MODEL ####

# from peft import AutoPeftModelForCausalLM

# # Load PEFT model on CPU

# model = AutoPeftModelForCausalLM.from_pretrained(

#     args.output_dir,

#     torch_dtype=torch.float16,

#     low_cpu_mem_usage=True,

# )

# # Merge LoRA and base model and save

# merged_model = model.merge_and_unload()

# merged_model.save_pretrained(args.output_dir,safe_serialization=True, max_shard_size="2GB")

模型测试和推理

训练完成后,我们要对模型进行评估和测试。作者从原始数据集中加载不同的样本,并手动评估模型。评估生成式人工智能模型并非易事,因为一个输入可能有多个正确的输出。阅读《评估 LLMs 和 RAG,一个使用 Langchain 和 Hugging Face 的实用案例》可以了解到关于评估生成模型的相关内容。

文章地址:https://www.philschmid.de/evaluate-llm
import torch
from peft import AutoPeftModelForCausalLM
from transformers import AutoTokenizer

peft_model_id = "./llama-3-70b-hf-no-robot"

# Load Model with PEFT adapter

model = AutoPeftModelForCausalLM.from_pretrained(

  peft_model_id,

  torch_dtype=torch.float16,

  quantization_config= {"load_in_4bit": True},

  device_map="auto"

)
tokenizer = AutoTokenizer.from_pretrained(peft_model_id)

接下来加载测试数据集,尝试生成指令。
from datasets import load_dataset
from random import randint

# Load our test dataset

eval_dataset = load_dataset("json", data_files="test_dataset.json", split="train")

rand_idx = randint(0, len(eval_dataset))

messages = eval_dataset[rand_idx]["messages"][:2]

# Test on sample

input_ids = tokenizer.apply_chat_template(messages,add_generation_prompt=True,return_tensors="pt").to(model.device)

outputs = model.generate(

    input_ids,

    max_new_tokens=512,

    eos_token_id= tokenizer.eos_token_id,

    do_sample=True,

    temperature=0.6,

    top_p=0.9,

)

response = outputs[0][input_ids.shape[-1]:]

print(f"**Query:**\n{eval_dataset[rand_idx]['messages'][1]['content']}\n")

print(f"**Original Answer:**\n{eval_dataset[rand_idx]['messages'][2]['content']}\n")

print(f"**Generated Answer:**\n{tokenizer.decode(response,skip_special_tokens=True)}")

# **Query:**

# How long was the Revolutionary War?

# **Original Answer:**

# The American Revolutionary War lasted just over seven years. The war started on April 19, 1775, and ended on September 3, 1783.

# **Generated Answer:**

# The Revolutionary War, also known as the American Revolution, was an 18th-century war fought between the Kingdom of Great Britain and the Thirteen Colonies. The war lasted from 1775 to 1783.

至此,主要流程就介绍完了,心动不如行动,赶紧从第一步开始操作吧。

原文链接:https://www.philschmid.de/fsdp-qlora-llama3?continueFlag=7e3b3f9059405e4318552e99bd128514
入门Hugging Face
相关数据
Amazon机构

亚马逊(英语:Amazon.com Inc.,NASDAQ:AMZN)是一家总部位于美国西雅图的跨国电子商务企业,业务起始于线上书店,不久之后商品走向多元化。目前是全球最大的互联网线上零售商之一,也是美国《财富》杂志2016年评选的全球最大500家公司的排行榜中的第44名。

https://www.amazon.com/
相关技术
权重技术

线性模型中特征的系数,或深度网络中的边。训练线性模型的目标是确定每个特征的理想权重。如果权重为 0,则相应的特征对模型来说没有任何贡献。

人工智能技术

在学术研究领域,人工智能通常指能够感知周围环境并采取行动以实现最优的可能结果的智能体(intelligent agent)

参数技术

在数学和统计学裡,参数(英语:parameter)是使用通用变量来建立函数和变量之间关系(当这种关系很难用方程来阐述时)的一个数量。

超参数技术

在机器学习中,超参数是在学习过程开始之前设置其值的参数。 相反,其他参数的值是通过训练得出的。 不同的模型训练算法需要不同的超参数,一些简单的算法(如普通最小二乘回归)不需要。 给定这些超参数,训练算法从数据中学习参数。相同种类的机器学习模型可能需要不同的超参数来适应不同的数据模式,并且必须对其进行调整以便模型能够最优地解决机器学习问题。 在实际应用中一般需要对超参数进行优化,以找到一个超参数元组(tuple),由这些超参数元组形成一个最优化模型,该模型可以将在给定的独立数据上预定义的损失函数最小化。

生成模型技术

在概率统计理论中, 生成模型是指能够随机生成观测数据的模型,尤其是在给定某些隐含参数的条件下。 它给观测值和标注数据序列指定一个联合概率分布。 在机器学习中,生成模型可以用来直接对数据建模(例如根据某个变量的概率密度函数进行数据采样),也可以用来建立变量间的条件概率分布。

堆叠技术

堆叠泛化是一种用于最小化一个或多个泛化器的泛化误差率的方法。它通过推导泛化器相对于所提供的学习集的偏差来发挥其作用。这个推导的过程包括:在第二层中将第一层的原始泛化器对部分学习集的猜测进行泛化,以及尝试对学习集的剩余部分进行猜测,并且输出正确的结果。当与多个泛化器一起使用时,堆叠泛化可以被看作是一个交叉验证的复杂版本,利用比交叉验证更为复杂的策略来组合各个泛化器。当与单个泛化器一起使用时,堆叠泛化是一种用于估计(然后纠正)泛化器的错误的方法,该泛化器已经在特定学习集上进行了训练并被询问了特定问题。

语言模型技术

语言模型经常使用在许多自然语言处理方面的应用,如语音识别,机器翻译,词性标注,句法分析和资讯检索。由于字词与句子都是任意组合的长度,因此在训练过的语言模型中会出现未曾出现的字串(资料稀疏的问题),也使得在语料库中估算字串的机率变得很困难,这也是要使用近似的平滑n元语法(N-gram)模型之原因。

文本生成技术

文本生成是生成文本的任务,其目的是使人类书写文本难以区分。

量化技术

深度学习中的量化是指,用低位宽数字的神经网络近似使用了浮点数的神经网络的过程。

暂无评论
暂无评论~