LZUOSS HPC 超算团队选拔赛:AI赛题报告

兰州大学 LZUOSS HPC 超算团队及 ASC 25 选拔题目

LzuOssHPC-2024-HPC

文件目录说明

1
2
3
4
5
6
7
8
9
10
./
├── figures/ # 存放图表和可视化结果的目录
├── gpt2_accelerate.ipynb # 使用 Accelerate 加速 GPT2 模型训练
├── gpt2_baseline.ipynb # GPT2 模型 Baseline 训练
├── Qwen_accelerate.ipynb # 使用 Accelerate 加速 Qwen 模型评测
├── Qwen_baseline.ipynb # Qwen 模型 Baseline 评测
├── Qwen_pipe.ipynb # 使用 Pipeline 进行 Qwen 模型评测
├── report.md # 项目的完整报告(markdown)
├── report.pdf # 项目的完整报告(pdf)
└── requirements.txt # 完整环境软件包列表

实验环境

硬件配置

使用本地笔记本电脑与云超算平台两种硬件环境测试程序,具体配置如下:

本地设备:

  • CPU: 12th Gen Intel(R) Core(TM) i7-12700H (逻辑处理器: 20)
  • RAM: 16GB
  • GPU: NVIDIA GeForce RTX 3060 Laptop GPU
  • VRAM: 6GB

云超算平台:

  • CPU: 22vCPU
  • RAM: 120GB
  • GPU: 2卡 * NVIDIA GeForce RTX 3090 GPU
  • VRAM: 24GB

软件配置

  • WSL2 - Ubuntu 22.04(本地)/ Ubuntu 22.04(云超算平台)
  • Python 3.9.20
  • Cuda 12.1
  • torch==2.5.1+cu121
  • transformers==4.46.3
  • accelerate==1.1.1
  • bitsandbytes==0.44.1
  • flash-attn==2.7.0.post2+cu12torch2.5cxx11abiFALSE-cp39-cp39-linux_x86_64(通过手动编译安装)
  • lightning==2.4.0

完整软件包列表见 requirements.txt

赛题一:预训练 GPT-2

前言

在本节中,我将详细描述对赛题一的思考、探索、编写和优化过程。我的解题策略是,首先使用 Transformers 库的原生文档提供的结构,构建一个基础的 Baseline 模型。在此基础上,进一步探索多种优化方法,包括使用加速框架、混合精度训练、分布式训练等技术。最后,我会在总结报告中给出所有方法性能对比。

Baseline

首先导入一下必要的库:

1
2
3
4
5
6
7
8
9
10
11
import torch

from itertools import chain
import wandb

from datasets import load_dataset, load_from_disk
from transformers import DataCollatorForLanguageModeling
from transformers import GPT2LMHeadModel, GPT2Tokenizer, GPT2Config
from transformers import TrainingArguments, Trainer

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

值得注意的是,这里我使用了 wandb 作为我的训练过程 / 结果可视化平台,以便更好监控模型性能。

为了提高数据加载速度,直接从本地读取离线的 BookCorpus 数据集:

1
2
3
4
# ds = load_dataset("bookcorpus/bookcorpus", "default")
ds = load_from_disk("bookcorpus")
ds = ds["train"].select(range(10000))
ds = ds.train_test_split(test_size=0.1)

这里,我读取了前 10000 条数据用于训练,其中 10%(即 1000 条)用作验证集。

从预训练的 GPT-2 中加载 tokenizer,并设置填充标记:

1
2
tokenizer = GPT2Tokenizer.from_pretrained("openai-community/gpt2")
tokenizer.pad_token = tokenizer.eos_token

使用预训练的 tokenizer 将文本数据转换为标记,对数据集应用 tokenize 函数,并批量处理:

1
2
3
4
5
6
7
def tokenize_fn(data):
return tokenizer(text=data["text"])


tokenized_ds = ds.map(tokenize_fn, batched=True, remove_columns="text")
# tokenized_ds.save_to_disk("bookcorpus/tokenized_ds")
# tokenized_ds = load_from_disk("bookcorpus/tokenized_ds")

为 LLM 准备数据,使用 data collator 转化特征数据为 tensor:

1
data_collator = DataCollatorForLanguageModeling(tokenizer, mlm=False)

使用 Transformers 库中的默认配置初始化 GPT-2 模型:

1
2
config = GPT2Config()
model = GPT2LMHeadModel(config).to(device)

在这部分,定义了GPT-2模型训练的参数,并使用Trainer进行训练管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
training_args = TrainingArguments(
output_dir="checkpoints", # 模型检查点保存目录
run_name="baseline", # WandB 运行名称
eval_strategy="steps", # 评估策略
eval_steps=50, # 每 50 步评估一次
logging_strategy="steps", # 日志记录策略
logging_steps=50, # 每 50 步记录一次日志
num_train_epochs=3, # 训练 3 个epoch
per_device_train_batch_size=32, # 每个设备的训练批次大小
per_device_eval_batch_size=32, # 每个设备的评估批次大小
learning_rate=2.5e-4, # 学习率
weight_decay=1e-4, # 权重衰减
report_to="wandb", # 报告到WandB
lr_scheduler_type="cosine", # 学习率调度器
warmup_ratio=0.05, # 预热比例
)

开始训练!

1
trainer.train()

Baseline 性能报告

  • train_loss 变化:

baseline-train_loss

  • eval_loss 变化:

baseline-eval_loss

  • learning_rate 变化:

baseline-learning_rate

  • 训练的总时间:07:59

baseline-train_time

使用 Accelerate 加速框架进行分布式混合精度训练

使用 HuggingFace 提供的 accelerate 库,可以实现只需要修改几行代码就可以实现ddp训练,且支持混合精度训练和TPU训练。(甚至支持deepspeed。)
Accelerate 支持的训练方式为 CPU/单GPU (TPU)/多GPU(TPU)DDP模式/fp32/fp16 等。

Accelerate 简介:

  • Accelerate 是一个库,通过添加四行代码,就可以让相同的 PyTorch 代码在任何分布式配置中运行!简而言之,它可以让大规模训练和推理变得简单、高效且适应性强。

安装 Accelerate 加速框架:

1
pip install accelerate

框架配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(hpc) tarik@von:~/hpc$ accelerate config
----------------------------------------------------------------
In which compute environment are you running?
This machine
----------------------------------------------------------------
Which type of machine are you using?
multi-GPU
How many different machines will you use (use more than 1 for multi-node training)? [1]: 1
Should distributed operations be checked while running for errors? This can avoid timeout issues but will be slower. [yes/NO]: no
Do you wish to optimize your script with torch dynamo?[yes/NO]:no
Do you want to use DeepSpeed? [yes/NO]: no
Do you want to use FullyShardedDataParallel? [yes/NO]: no
Do you want to use Megatron-LM ? [yes/NO]: no
How many GPU(s) should be used for distributed training? [1]:2
What GPU(s) (by id) should be used for training on this machine as a comma-seperated list? [all]:all
Would you like to enable numa efficiency? (Currently only supported on NVIDIA hardware). [yes/NO]: no
----------------------------------------------------------------
Do you wish to use mixed precision?
no
accelerate configuration saved at /home/tarik/.cache/huggingface/accelerate/default_config.yaml

修改 baseline 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
+ from accelerate import Accelerator
+ accelerator = Accelerator(mixed_precision="fp16")

- device = "cuda"
+ device = accelerator.device
model.to(device)

model, optimizer, training_dataloader, scheduler = accelerator.prepare(
model, optimizer, training_dataloader, scheduler
)

- inputs = inputs.to(device)
- targets = targets.to(device)
outputs = model(inputs)
loss = loss_function(outputs, targets)
- loss.backward()
+ accelerator.backward(loss)

值得注意的是,在这里我启用了 FP16 混合精度训练。混合精度通过使用较低精度的类型(如 fp16(半精度))来计算梯度,从而加速训练。

使用 Accelerator 框架,设置 FP16 的混合精度训练,在云超算平台上使用 2 张 3090 显卡进行分布式训练:

  • 训练的总时间:00:55
  • train_loss:4.34901
  • eval_loss:4.54709

其他详细性能指标我会在总结部分一同给出。

效果立竿见影,使用 Accelerator 框架进行多卡训练后,模型的训练速度加快了88.5%

增大 batch_size 以充分利用显存

将环境迁移到云超算平台后,模型可用的显存由 6GB 增大到了 24GB。我进一步增大了训练的 batch_size,通过多次试验设置 batch_size=92,以保证显存占用率维持在 90% 以上,进一步增加了训练速度:

  • 训练的总时间:00:47
  • train_loss:4.51685
  • eval_loss:4.67991

在上一步的基础上,模型的训练速度进一步加快了14.5%

优化性能总结报告

batch_size精度训练时间train_losseval_loss设备加速框架GPU数量
96bf160:474.69794.71923云 3090accelerate2
96fp160:474.516854.67991云 3090accelerate2
32fp160:554.349014.54709云 3090accelerate2
32fp162:283.899094.45961本地 3060 Laptopaccelerate1
32fp325:404.7283381834.467901本地 3060 Laptop1

值得一提的是,对于这个模型,在我尝试使用 bf16 的精度时,训练速度并不比 fp16 加速多少,但是 train_losseval_loss 均出现较大损失。因此,采用第二行的配置作为最佳优化方法。

  • train_loss 变化:

best-train_loss

  • eval_loss 变化:

best-eval_loss

  • learning_rate 变化:

best-learning_rate

  • 训练的总时间:00:47

best-train_time

赛题二:通义千问在MMMLU上的表现

前言

在本节中,我将详细描述对赛题二的思考、探索、编写和优化过程。我的解题策略是,首先使用 Transformers 库的原生文档提供的结构,构建一个基础的 Baseline 模型。在此基础上,进一步探索多种优化方法,包括使用加速框架、不同的模型量化方法、注意力计算机制、混合精度推理、分布式推理等技术。最后,我会在总结报告中给出所有方法性能对比。

Baseline

首先导入一下必要的库:

1
2
3
4
5
6
7
import torch
import re
from tqdm.notebook import tqdm
from time import time


device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

载入数据集:

1
2
3
4
5
from datasets import load_dataset, load_from_disk

# ds = load_dataset("openai/MMMLU", "ZH_CN")["test"]
# ds.save_to_disk("MMMLU")
ds = load_from_disk("MMMLU")

按照题目要求的范围,进行数据预处理和数据集划分:

1
filtered_ds = ds.filter(lambda example: example["Subject"] == "moral_scenarios").select(range(100))

载入预训练的模型和 tokenizer:

1
2
3
4
5
6
7
from transformers import AutoTokenizer, AutoModelForCausalLM

# model_name = "Qwen/Qwen2.5-0.5B"
model_name = "Qwen"

tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype="auto", device_map="auto").to(device)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def create_prompt(example):
return {"prompt": f"问题: {example['Question']}\nA) {example['A']}\nB) {example['B']}\nC) {example['C']}\nD) {example['D']}\n请你给出答案(只输出一个字母): ", "answer": example["Answer"]}


prompts = filtered_ds.map(create_prompt)
total = len(prompts)
correct = 0

start_time = time()
for i in tqdm(range(0, total, batch_size)):
text_batch = prompts[i : i + batch_size]["prompt"]
model_inputs_batch = tokenizer(text_batch, return_tensors="pt", padding=True).to(model.device)

generated_ids_batch = model.generate(**model_inputs_batch, max_new_tokens=512, pad_token_id=tokenizer.eos_token_id)
generated_ids_batch = generated_ids_batch[:, model_inputs_batch.input_ids.shape[1] :]
response_batch = tokenizer.batch_decode(generated_ids_batch, skip_special_tokens=True)

responses = [response.strip()[-1] for response in response_batch]

correct += sum(response == answer for response, answer in zip(responses, prompts[i : i + batch_size]["answer"]))

end_time = time()
execution_time = end_time - start_time
accuracy = correct / total
print(f"Execution time: {execution_time:.2f} seconds")
print(f"Accuracy: {accuracy:.2f}")

Baseline 性能报告

  • Accuracy:0.2
  • 推理时间:300.49

使用 pipeline 加速推理流程

Hugging Face 为模型推理提供的 pipeline() 使得从 Hub 中使用任何模型进行任何语言、计算机视觉、语音和多模态任务的推理变得简单。

通过整合数据预处理、模型推理和结果后处理,pipeline减少了数据在各个步骤之间传输的成本,从而加速了整体流程。

将模型参数封装到 pipe 对象:

1
2
3
4
pipe = pipeline("text-generation", model_name, torch_dtype=torch.bfloat16, device_map="auto")
pipe.tokenizer.padding_side = "left"

results = pipe(prompts["prompt"], max_new_tokens=512, batch_size=batch_size)
  • Accuracy:0.2
  • 推理时间:282.16

其他详细性能指标我会在总结部分一同给出。

推理时间加速了 6.1%

增大 batch_size 以充分利用显存

实际上,通过测试我发现,在本地机器上 6GB 的显存完全足够应付本测评任务,直接将 batch_size 拉到 100,显存实际占用 3GB 左右:

  • Accuracy:0.2
  • 推理时间:31.87

推理时间加速了 89.4%

使用 Accelerate 加速框架进行分布式混合精度推理

Accelerate 提供的最大进步之一是 大型模型推理,它允许对无法完全安装在显卡上的模型进行推理。虽然本赛题使用的模型参数并不大,但是将模型卸载到两张卡上,进行分布式推理,能充分利用多卡环境的优势以加速推理。

1
2
3
4
5
6
7
8
9
from accelerate import infer_auto_device_map, dispatch_model


model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float16,
)

model = dispatch_model(model, device_map=infer_auto_device_map(model))
  • Accuracy:0.21
  • 推理时间:12.49

相比单卡的推理时间 16.12,使用多卡推理时间加速了 22.5%

稍后我会在总结报告中给出所有方法性能对比。

尝试:使用 FlashAttention-2 快速注意力机制

FlashAttention-2 是标准注意力机制的更快更高效的实现,它可以通过以下方式显著加快推理速度:

  • 在序列长度上并行化注意力计算
  • 将工作划分到 GPU 线程之间,以减少它们之间的通信和共享内存读写操作

这里因为版本有点问题,我从原仓库克隆 whl,使用手动编译安装 FlashAttention-2:

1
2
3
pip install packaging
pip install ninja
pip install ./flash_attn-2.7.0.post2+cu12torch2.5cxx11abiFALSE-cp39-cp39-linux_x86_64.whl

启用 FlashAttention-2:

1
2
3
4
5
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.bfloat16,
attn_implementation="flash_attention_2",
)

但是在本模型上,我在启用 FlashAttention-2 后,推理速度从 12.49 下降到 24.56,推测是对于较小的模型和简单的任务,FlashAttention-2 的优化可能无法显现,因为其设计目标是在处理长序列和大模型时提升性能。

尝试:使用 bitsandbytes 对模型进行 4位/8位量化

bitsandbytes 是一个量化库,它包括对 4 位和 8 位量化的支持。量化会减少模型相对于其原生全精度版本的大小,使其更容易将大型模型适应内存有限的 GPU。

安装 bitsandbytes:

1
pip install bitsandbytes

以 4 位加载模型,然后启用 BetterTransformer 与 FlashAttention:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
from accelerate import infer_auto_device_map, dispatch_model

model_name = "Qwen"
quantization_config = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.bfloat16)

model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float16,
attn_implementation="flash_attention_2",
quantization_config=quantization_config,
)

model = dispatch_model(model, device_map=infer_auto_device_map(model))

tokenizer = AutoTokenizer.from_pretrained(model_name, padding_side="left")

batch_size = 100

但是对于本模型和硬件配置来说,应该由于不支持 4位/8位量化计算,不仅在量化后由于精度亏损导致准确率严重下降,而且推理时间反而因此变慢。但是,对于其他具有着极大参数的语言模型,难以适配有限的显存,4位/8位量化仍然是一个值得探索的方向。

优化性能总结报告

batch_size精度推理时间accuracy推理方法设备加速框架/量化方法GPU数量
100fp1612.490.21generate云 3090accelerate2
100fp1616.120.21pipeline云 30901
100bf1616.150.15pipeline云 30901
100fp1624.560.21generate云 3090accelerate+flash_attn2
100fp1627.90.03generate云 3090accelerate+4bit2
100fp3231.870.2pipeline云 30901
100fp1635.270.06generate云 3090accelerate+flash_attn+4bit2
1fp32282.160.2pipeline云 30901
1fp32300.490.2generate云 30901