Type something to search...
精细调优大语言模型:揭开HuggingFace的神秘面纱!如何克服GPU内存束缚?

精细调优大语言模型:揭开HuggingFace的神秘面纱!如何克服GPU内存束缚?

每次大型语言模型 (LLMs) 的新公告往往将性能推向新的高度,常常超越之前的基准(例如,巨量多任务语言理解或 MMLU)。这一进展激发了许多应用程序的出现,利用最大的和最优秀的模型。在我们之前的帖子中,我们讨论了 LLMs 的规模法则,并解释了为什么更大的模型在预测下一个标记时表现更好。

然而,从原型 LLM 演示到功能性生产系统的旅程并非没有挑战。用户隐私和信任非常重要,尤其是在我们运营会计和金融领域时。确保 LLMs 增强我们的应用程序并为客户提供价值仍然是我们的首要任务。

我们观察到,当使用专有模型(如 GPT-4 及其变体)时,模型响应会随着时间的推移而漂移。这是可以理解的,因为模型的权重会更新。然而,这会造成明显的推理差异,从而使下游应用程序的性能不稳定。此外,我们还观察到,即使为随机种子设置了一切可能的种子并选择贪婪解码策略,也很难获得确定性的结果。此外,我们意识到 GPT-4 在回答某些特定于我们领域的问题时表现不佳,可能会生成虚假的结果。

为了缓解这些问题,我们决定训练自己的会计领域特定模型,并与 Amazon AWS 团队合作,利用他们的基础设施和知识提供最先进的训练和推理计算资源。然而,微调 LLMs 的一个挑战是高 GPU 内存消耗。在接下来的部分中,我们将探讨模型并行和数据并行策略,这些策略通常用于解决这一问题。

模型并行与数据并行

在训练 LLM 时,选择 模型并行 (MP) 和数据并行 (DP) 取决于模型是否能够适应单个 GPU。 当模型的权重过大而无法放入单个 GPU 时,使用 MP。MP 将模型权重分片到多个 GPU 上。另一方面,DP 将数据分片到多个 GPU 上,每个 GPU 持有模型权重的完整副本。

在 DP 的情况下,PyTorch 提供了 分布式数据并行 (DDP) 功能,它包装了模型类对象,允许通过其 torchrun 启动工具启动 DDP。

在 MP 的情况下,PyTorch 原生支持 完全分片数据并行 (FSDP) 以实现模型并行。此外,DeepSpeed 是由微软团队开发的另一种流行的 MP 技术,我们在之前的文章中使用过。

正如您可能注意到的,使用这些并行技术需要对代码进行修改。为了简化这个过程,HuggingFace 提供了 accelerate 包,它处理复杂的包装函数和 GPU 设备放置。这使得使用这些并行技术变得更加简单,而无需编写特定于并行化的样板代码。

准备用于指令微调的数据集

首先,我们将数据集整理成问题和答案的对。由于 LLM 以 token-in 和 token-out 的方式运作,因此对中的文本使用 tokenizer 对象转换为 tokens。对于指令微调模型,这些对需要符合每个模型独特的指令格式。我们可以使用 tokenizer.apply_chat_template() 函数来实现这一点。

def apply_chat_template(
    datapoint,
    tokenizer,
    prefix="",
):
    messages = [
        {"role": "user", "content": f"""{prefix}: [{datapoint["input"]}]"""},
        {"role": "assistant", "content": datapoint["output"]},
    ]
    datapoint["text"] = tokenizer.apply_chat_template(
        messages, tokenize=False, add_generation_prompt=False
    )
    return datapoint


def apply_chat_template_test(
    datapoint,
    tokenizer,
    prefix="",
):
    print("using chat template prompting.")
    messages = [
        {"role": "user", "content": f"""{prefix}: [{datapoint["input"]}]"""},
    ]
    datapoint["text"] = tokenizer.apply_chat_template(
        messages, tokenize=False, add_generation_prompt=False
    )
    return datapoint
```python
def apply_chat_template(
    datapoint,
    tokenizer,
    prefix="",
):
    messages = [
        {"role": "user", "content": f"""{prefix}: [{datapoint["input"]}]"""},
        {"role": "assistant", "content": datapoint["output"]},
    ]
    datapoint["text"] = tokenizer.apply_chat_template(
        messages, tokenize=False, add_generation_prompt=False
    )
    return datapoint


def apply_chat_template_test(
    datapoint,
    tokenizer,
    prefix="",
):
    print("using chat template prompting.")
    messages = [
        {"role": "user", "content": f"""{prefix}: [{datapoint["input"]}]"""},
    ]
    datapoint["text"] = tokenizer.apply_chat_template(
        messages, tokenize=False, add_generation_prompt=False
    )
    return datapoint

这是完整的代码片段。

from sklearn.model_selection import train_test_split

import pandas as pd
df = pd.read_csv(data_path)


## 将数据集拆分为训练集和测试集
X_train, X_test = train_test_split(
  df, test_size=0.00001, random_state=42
)


## 重新格式化输入和输出以符合指令格式
X_train = pd.DataFrame(
      X_train.apply(
          lambda row: apply_chat_template(
              row, tokenizer, prefix
          ),
        axis=1,
    ),
    columns=["text"],
)
X_test = pd.DataFrame(
    X_test.apply(
        lambda row: apply_chat_template_test(
            row, tokenizer, prefix
        ),
        axis=1,
    ),
    columns=["text"],
)

## 将数据加载到 Dataset 对象中
from datasets import Dataset

train_data = Dataset.from_pandas(X_train)
test_data = Dataset.from_pandas(X_test)
```python
from sklearn.model_selection import train_test_split

import pandas as pd
df = pd.read_csv(data_path)


## 将数据集拆分为训练集和测试集
X_train, X_test = train_test_split(
  df, test_size=0.00001, random_state=42
)


## 重新格式化输入和输出以符合指令格式
X_train = pd.DataFrame(
      X_train.apply(
          lambda row: apply_chat_template(
              row, tokenizer, prefix
          ),
        axis=1,
    ),
    columns=["text"],
)
X_test = pd.DataFrame(
    X_test.apply(
        lambda row: apply_chat_template_test(
            row, tokenizer, prefix
        ),
        axis=1,
    ),
    columns=["text"],
)

## 将数据加载到 Dataset 对象中
from datasets import Dataset

train_data = Dataset.from_pandas(X_train)
test_data = Dataset.from_pandas(X_test)

模型训练

LLM 以监督的方式进行训练,因为我们为问题提供了标记答案。为了实现这个训练过程,HuggingFace 实现了 SFTTrainer() 对象。

加载预训练模型和分词器

首先,我们使用 HuggingFace 的 AutoModelForCausalLMAutoTokenizer 对象加载预训练模型和分词器。一些模型(例如 Phi-3 系列)需要 trust_remote_code=True,因此我们默认将此参数设置为 true。

from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
)
import torch


## Load the model
def load_model(
    base_model,
):
    return AutoModelForCausalLM.from_pretrained(
        base_model,
        trust_remote_code=True,
    )


## Load tokenizer
def load_tokenizer(base_model,):
    return AutoTokenizer.from_pretrained(
        base_model, trust_remote_code=True,
    )
```python
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
)
import torch


## Load the model
def load_model(
    base_model,
):
    return AutoModelForCausalLM.from_pretrained(
        base_model,
        trust_remote_code=True,
    )


## Load tokenizer
def load_tokenizer(base_model,):
    return AutoTokenizer.from_pretrained(
        base_model, trust_remote_code=True,
    )

在开始训练之前,我们为该过程设置一个种子,以确保可重复性。

from transformers import set_seed


## Set seed for reproducibility
set_seed(123)
```python
from transformers import set_seed


## Set seed for reproducibility
set_seed(123)

LoRA 微调

从零开始训练大型语言模型(LLMs)需要大量的计算资源和庞大的数据集(例如,15万亿个标记用于 Llama 3.1 405B,16,000 个 H100 GPU)。幸运的是,如果仅对线性层进行微调,微调 LLMs 所需的计算资源要少得多。

LoRA 利用低秩矩阵近似的优势,这是一种将高维矩阵近似为两个较小矩阵(例如,秩为 1)的乘积的概念,从而将矩阵更新维度从 25 降低到 10,如图 1 所示。这种方法源于奇异值分解(SVD)的数学技术。通过将其应用于变换器模型的注意力层中的权重矩阵,LoRA 有效地减少了需要训练的参数数量。

peft 包提供了 prepare_model_for_kbit_trainingLoraConfig 对象,以便您可以向目标线性层添加可调权重,同时冻结其他层的权重。

from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training


def add_lora_layers(
    model,
    lora_alpha=<an interger>,
    r=<an interger>,
    target_modules="all-linear",
):
    model = prepare_model_for_kbit_training(
        model,
        use_gradient_checkpointing=True,
        gradient_checkpointing_kwargs={"use_reentrant": False},
    )
    peft_config = LoraConfig(
        lora_alpha=lora_alpha,
        lora_dropout=<a floating number>,
        r=r,
        bias="none",
        task_type="CAUSAL_LM",
        target_modules=target_modules,
    )
    return get_peft_model(model, peft_config), peft_config
```python
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training


def add_lora_layers(
    model,
    lora_alpha=<an interger>,
    r=<an interger>,
    target_modules="all-linear",
):
    model = prepare_model_for_kbit_training(
        model,
        use_gradient_checkpointing=True,
        gradient_checkpointing_kwargs={"use_reentrant": False},
    )
    peft_config = LoraConfig(
        lora_alpha=lora_alpha,
        lora_dropout=<a floating number>,
        r=r,
        bias="none",
        task_type="CAUSAL_LM",
        target_modules=target_modules,
    )
    return get_peft_model(model, peft_config), peft_config

定义模型超参数

我们需要为训练模型定义超参数。我们选择余弦学习率调度,这提供了平滑的衰减。您应该仔细选择每个设备的批大小和梯度累积步数,以避免耗尽您的GPU内存。有效批大小是通过将每个设备的批大小乘以梯度累积步数和集群(节点)中的GPU数量来计算的。例如,Nvidia为云服务提供商(CSP)打包并运输8个A100 GPU在单个节点中。此外,我们保存达到最低交叉熵损失的前三个模型。

from transformers import EarlyStoppingCallback, TrainingArguments, trainer_utils
import torch


## If your gpu is above 8, you can accelerate training with bf16
major, _ = torch.cuda.get_device_capability()

## Set up training hyperparameters
training_arguments = TrainingArguments(
    output_dir=output_dir,
    num_train_epochs=<an interger>,
    per_device_train_batch_size=<an interger>,
    per_device_eval_batch_size=<an interger>,
    gradient_accumulation_steps=<an interger>,
    optim="paged_adamw_32bit",
    save_steps=<an integer>,
    logging_steps=<an integer>,
    learning_rate=<a floating number>,
    weight_decay=<a floating number>,
    fp16=False if major >= 8 else True,
    bf16=True if major >= 8 else False,
    max_grad_norm=<a floating number>,
    warmup_ratio=<a floating number>,
    group_by_length=True,
    lr_scheduler_type="cosine",
    gradient_checkpointing_kwargs={"use_reentrant": False},
    disable_tqdm=False,
    resume_from_checkpoint=True,
    seed=123,
    log_level="info",
    remove_unused_columns=True,
    ...
)
```python
from transformers import EarlyStoppingCallback, TrainingArguments, trainer_utils
import torch


## If your gpu is above 8, you can accelerate training with bf16
major, _ = torch.cuda.get_device_capability()

## Set up training hyperparameters
training_arguments = TrainingArguments(
    output_dir=output_dir,
    num_train_epochs=<an interger>,
    per_device_train_batch_size=<an interger>,
    per_device_eval_batch_size=<an interger>,
    gradient_accumulation_steps=<an interger>,
    optim="paged_adamw_32bit",
    save_steps=<an integer>,
    logging_steps=<an integer>,
    learning_rate=<a floating number>,
    weight_decay=<a floating number>,
    fp16=False if major >= 8 else True,
    bf16=True if major >= 8 else False,
    max_grad_norm=<a floating number>,
    warmup_ratio=<a floating number>,
    group_by_length=True,
    lr_scheduler_type="cosine",
    gradient_checkpointing_kwargs={"use_reentrant": False},
    disable_tqdm=False,
    resume_from_checkpoint=True,
    seed=123,
    log_level="info",
    remove_unused_columns=True,
    ...
)

应用 SFTTrainer 对象

接下来,我们通过调用 SFTTrainer() 对象来启动训练,传递训练数据集和超参数。我们还加入了 callbacks,以便在损失超过我们定义的限制后未能减小时提前结束训练。

from trl import SFTTrainer


## Set up the trainer
trainer = SFTTrainer(
    model=model,
    train_dataset=train_data,
    eval_dataset=(
        test_data
    ),
    peft_config=peft_config,
    max_seq_length=max_seq_length,
    dataset_text_field="text",
    tokenizer=tokenizer,
    args=training_arguments,
    packing=False,
    callbacks=[
        EarlyStoppingCallback(
            early_stopping_patience=<an interger>,
            early_stopping_threshold=<a floating number>,
        ),
    ],
)
```python
from trl import SFTTrainer


## Set up the trainer
trainer = SFTTrainer(
    model=model,
    train_dataset=train_data,
    eval_dataset=(
        test_data
    ),
    peft_config=peft_config,
    max_seq_length=max_seq_length,
    dataset_text_field="text",
    tokenizer=tokenizer,
    args=training_arguments,
    packing=False,
    callbacks=[
        EarlyStoppingCallback(
            early_stopping_patience=<an interger>,
            early_stopping_threshold=<a floating number>,
        ),
    ],
)

日志模型指标

方便的是,我们可以使用 logging 函数记录超参数和损失指标。

import datasets
import logging
import transformers


logger = logging.getLogger(__name__)


## setup logging
logging.basicConfig(
    format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
    handlers=[logging.StreamHandler(sys.stdout)],
)
log_level = training_arguments.get_process_log_level()
logger.setLevel(log_level)
datasets.utils.logging.set_verbosity(log_level)
transformers.utils.logging.set_verbosity(log_level)
transformers.utils.logging.enable_default_handler()
transformers.utils.logging.enable_explicit_format()
```python
import datasets
import logging
import transformers


logger = logging.getLogger(__name__)


## setup logging
logging.basicConfig(
    format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
    handlers=[logging.StreamHandler(sys.stdout)],
)
log_level = training_arguments.get_process_log_level()
logger.setLevel(log_level)
datasets.utils.logging.set_verbosity(log_level)
transformers.utils.logging.set_verbosity(log_level)
transformers.utils.logging.enable_default_handler()
transformers.utils.logging.enable_explicit_format()

最后,我们记录训练和测试数据集的指标,包括损失和训练过程中使用的超参数。

resume_from_checkpoint = True

if resume_from_checkpoint:
    print("Continue training from the last checkpoint.")
    train_result = trainer.train(
        resume_from_checkpoint=resume_from_checkpoint
    )
else:
    train_result = trainer.train()

metrics = train_result.metrics
trainer.log_metrics("train", metrics)
trainer.save_metrics("train", metrics)
trainer.save_state()

## Evaluation
tokenizer.padding_side = "left"
metrics = trainer.evaluate()
metrics["eval_samples"] = (
    len(test_data)
)
trainer.log_metrics("eval", metrics)
trainer.save_metrics("eval", metrics)
```python
resume_from_checkpoint = True

if resume_from_checkpoint:
    print("Continue training from the last checkpoint.")
    train_result = trainer.train(
        resume_from_checkpoint=resume_from_checkpoint
    )
else:
    train_result = trainer.train()

metrics = train_result.metrics
trainer.log_metrics("train", metrics)
trainer.save_metrics("train", metrics)
trainer.save_state()

## Evaluation
tokenizer.padding_side = "left"
metrics = trainer.evaluate()
metrics["eval_samples"] = (
    len(test_data)
)
trainer.log_metrics("eval", metrics)
trainer.save_metrics("eval", metrics)

使用 Accelerate 和 DeepSpeed 配置启动

我们可以通过命令行启动训练任务,并将到目前为止的所有代码放入一个 Python 脚本中。DeepSpeed 配置在标志 --config_file 后传递。请确保 gradient_accumulation_stepsgradient_clippingTrainingArguments 中定义的值匹配。num_machines 定义使用的节点数量。num_processes 定义节点中的 GPU 数量。

compute_environment: LOCAL_MACHINE
debug: false
deepspeed_config:
  deepspeed_multinode_launcher: standard
  gradient_accumulation_steps: <an interger>
  gradient_clipping: <a floating number>
  offload_optimizer_device: none
  offload_param_device: none
  zero3_init_flag: true
  zero3_save_16bit_model: true
  zero_stage: 3
distributed_type: DEEPSPEED
downcast_bf16: 'no'
machine_rank: 0
main_training_function: main
mixed_precision: bf16
num_machines: 1
num_processes: 8
rdzv_backend: static
same_network: true
tpu_env: []
tpu_use_cluster: false
tpu_use_sudo: false
use_cpu: false
```python
compute_environment: LOCAL_MACHINE
debug: false
deepspeed_config:
  deepspeed_multinode_launcher: standard
  gradient_accumulation_steps: <an interger>
  gradient_clipping: <a floating number>
  offload_optimizer_device: none
  offload_param_device: none
  zero3_init_flag: true
  zero3_save_16bit_model: true
  zero_stage: 3
distributed_type: DEEPSPEED
downcast_bf16: 'no'
machine_rank: 0
main_training_function: main
mixed_precision: bf16
num_machines: 1
num_processes: 8
rdzv_backend: static
same_network: true
tpu_env: []
tpu_use_cluster: false
tpu_use_sudo: false
use_cpu: false

CUDA_VISIBLE_DEVICE=0,1,2,3,4,5,6,7 accelerate launch --config_file "deepspeed_stage3.yaml" <put all of the code up to this point into a python script>
```python
CUDA_VISIBLE_DEVICE=0,1,2,3,4,5,6,7 accelerate launch --config_file "deepspeed_stage3.yaml" <put all of the code up to this point into a python script>

权衡:精度与速度

我们希望通过强调其他重要的权衡来结束这篇文章:精度与速度。图2显示了不同数据类型与计算速度之间的关系,以TFLOPS为单位,详细信息见Nvidia的A100白皮书。要深入了解计算速度,您可以参考我们早期博客文章中的附录

不同的数据类型提供了不同的指数范围和精度。此外,不同的GPU(如V100和A100)支持不同的数据类型。检查您的GPU支持哪些数据类型非常重要。例如,根据上面的图2,FP16在操作/秒(计算速度)中提供了FP32的8倍吞吐量,尽管其指数范围和精度有所降低。我们建议仔细查看PyTorch CUDA设置以及您的GPU白皮书,并对您的数据进行实验,以找到精度与速度之间的正确平衡。例如,以下代码片段启用了TF32用于矩阵乘法。

## https://pytorch.org/docs/stable/notes/cuda.html#tf32-on-ampere

## The flag below controls whether to allow TF32 on matmul. This flag defaults to False
## in PyTorch 1.12 and later.
torch.backends.cuda.matmul.allow_tf32 = True

## The flag below controls whether to allow TF32 on cuDNN. This flag defaults to True.
torch.backends.cudnn.allow_tf32 = True
```python
## https://pytorch.org/docs/stable/notes/cuda.html#tf32-on-ampere

## The flag below controls whether to allow TF32 on matmul. This flag defaults to False
## in PyTorch 1.12 and later.
torch.backends.cuda.matmul.allow_tf32 = True

## The flag below controls whether to allow TF32 on cuDNN. This flag defaults to True.
torch.backends.cudnn.allow_tf32 = True

结论

在这篇文章中,我们展示了如何使用 HuggingFace 提供的工具通过 MP 技术对 LLM 进行微调。通过分享这些经验,我们希望您能训练自己的 LLM 并拥有权重。请记住,即使是经过微调的 LLM 也可能产生幻觉响应。虽然微调可以减少幻觉的可能性,但并不能消除它。开发人员和科学家在设计 AI 产品时应理解,LLM 仍然是通过根据前面的上下文生成最可能的标记来操作的,并且可能生成无根据或不真实的响应。尽管存在这些限制,小型特定领域语言模型更受欢迎,因为它们可以被优化以理解特定领域的术语,并且与大型封闭源语言模型相比,推理成本更低。

Related Posts

使用 ChatGPT 搜索网络功能的 10 种创意方法

使用 ChatGPT 搜索网络功能的 10 种创意方法

例如,提示和输出 你知道可以使用 ChatGPT 的“搜索网络”功能来完成许多任务,而不仅仅是基本的网络搜索吗? 对于那些不知道的人,ChatGPT 新的“搜索网络”功能提供实时信息。 截至撰写此帖时,该功能仅对使用 ChatGPT 4o 和 4o-mini 的付费会员开放。 ![](https://images.weserv.nl/?url=https://cdn-im

阅读更多
在人工智能和技术领域保持领先地位的 10 项必学技能 📚

在人工智能和技术领域保持领先地位的 10 项必学技能 📚

在人工智能和科技这样一个动态的行业中,保持领先意味着不断提升你的技能。无论你是希望深入了解人工智能模型性能、掌握数据分析,还是希望通过人工智能转变传统领域如法律,这些课程都是你成功的捷径。以下是一个精心策划的高价值课程列表,可以助力你的职业发展,并让你始终处于创新的前沿。 1. 生成性人工智能简介课程: [生成性人工智能简介](https://genai.works

阅读更多
10 个强大的 Perplexity AI 提示,让您的营销任务自动化

10 个强大的 Perplexity AI 提示,让您的营销任务自动化

在当今快速变化的数字世界中,营销人员总是在寻找更智能的方法来简化他们的工作。想象一下,有一个个人助理可以为您创建受众档案,建议营销策略,甚至为您撰写广告文案。这听起来像是一个梦想? 多亏了像 Perplexity 这样的 AI 工具,这个梦想现在成为现实。通过正确的提示,您可以将 AI 转变为您的 个人营销助理。在本文中,我将分享 10 个强大的提示,帮助您自动

阅读更多
10+ 面向 UI/UX 设计师的顶级 ChatGPT 提示

10+ 面向 UI/UX 设计师的顶级 ChatGPT 提示

人工智能技术,如机器学习、自然语言处理和数据分析,正在重新定义传统设计方法。从自动化重复任务到实现个性化用户体验,人工智能使设计师能够更加专注于战略思维和创造力。随着这一趋势的不断增长,UI/UX 设计师越来越多地采用 AI 驱动的工具来促进他们的工作。利用人工智能不仅能提供基于数据的洞察,还为满足多样化用户需求的创新设计解决方案开辟了机会。 1. 用户角色开发 目的

阅读更多
在几分钟内完成数月工作的 100 种人工智能工具

在几分钟内完成数月工作的 100 种人工智能工具

人工智能(AI)的快速发展改变了企业的运作方式,使人们能够在短短几分钟内完成曾经需要几周或几个月的任务。从内容创作到网站设计,AI工具帮助专业人士节省时间,提高生产力,专注于创造力。以下是按功能分类的100个AI工具的全面列表,以及它们在现实世界中的使用实例。 1. 研究工具 研究可能耗时,但人工智能工具使查找、分析和组织数据变得更加容易。**ChatGPT, Cop

阅读更多
你从未知道的 17 个令人惊叹的 GitHub 仓库

你从未知道的 17 个令人惊叹的 GitHub 仓库

Github 隐藏的宝石!! 立即收藏的代码库 学习编程相对简单,但掌握编写更好代码的艺术要困难得多。GitHub 是开发者的宝藏,那里“金子”是其他人分享的精心编写的代码。通过探索 GitHub,您可以发现如何编写更清晰的代码,理解高质量代码的样子,并学习成为更熟练开发者的基本步骤。 1. notwaldorf/emoji-translate *谁需

阅读更多