僅用250美元,Hugging Face技術主管手把手教你微調Llama 3
圖片 我們知道,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.
至此,主要流程就介紹完了,心動不如行動,趕緊從第一步開始操作吧。