大規模言語モデル(Llama2など)を正攻法でファインチューニングする際のメモ(ZeRO-Offload, not QLoRA)
背景と目的
大きめのサイズ(>数b)の大規模言語(LLM)をファインチューニングします。
ファインチューニングにはLoRAやQLoRAと呼ばれる手法が良く使われ、一般家庭レベル(?)のGPUでも動かせるようになってきています。
しかし、LoRAで学習させられる知識や情報には、制約があるのでは、とも囁かれています。
そこで、本記事は、loraではないフルパラメータのファインチューニングを、限られたGPUメモリで行います。
deepspeedというライブラリを使います。
deepspeedにはモデルの動作に必要なメモリをCPUメモリに移す機能などがあるようで、それを使います(キーワード: offload, ZeRO)。
7bモデルは20GB程度のVRAMで学習できました。
以下の公式チュートリアルをもとに進めたいところですが、情報が断片的で、自分にはあまり理解できなかったので、webサイトを適当に探りながら進めました。
環境構築
以下のような感じで構築しました(メモ書き)。
pip install deepspeed
pip install transformers datasets mecab-python3 unidic-lite sentencepiece accelerate pynvml deepspeed
pip install protobuf
conda install mpi4py #pipでエラーが出たので。
推論テスト
まずは推論で、deepspeedの効果を試します。
モデルは "elyza/ELYZA-japanese-Llama-2-7b-instruct"を使います。
ELYZAは、llama2をベースにファインチューニングした、わりと最近のモデルです。
対照実験: 普通に推論
transformersでモデルを呼び出して、推論をしてみます。
notebook環境で動かしています。
#普通に推論
from transformers import pipeline
from transformers import AutoModelForCausalLM, AutoTokenizer, AutoConfig
import torch
model_name= "elyza/ELYZA-japanese-Llama-2-7b-instruct"
model = AutoModelForCausalLM.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)
if torch.cuda.is_available():
model.to("cuda")
def gen(input_text="今日の天気は"):
text_pipe = pipeline('text-generation',
model=model,
tokenizer=tokenizer,
device="cuda:0",
max_length=200,
)
output = text_pipe(input_text)
return output[0]['generated_text']
%time gen()
出力:
'今日の天気は曇り時々雨。\n気温は15℃。\n今日は朝から雨が降っています。\n降り方が強くて、洗濯物が干せそうにありません。\n昨日は、午後から雨が降っていたので、洗濯物は干せましたが、今日は干せそうにありません。\n洗濯機を回して、洗濯物を部屋に干しておきます。\n今日の天気は曇り時々雨。\n気温は'
使用VRAM: 約28GB
推論時間: 5 sec
CPUメモリ: VIRTで約60GB、RESで2.5GB
7Bのモデルを32bitの変数で動かしているので、7x4=28 GBという計算だと思います。
DeepSpeedの利用
メモリをCPUにオフロードするなどできるようです。詳細は勉強中です。
以下の記事を参考に、推論のテストをしてみました。
以下のjsonファイル(zero_infer.json)を作ります。
{
"fp16": {
"enabled": "auto",
"loss_scale": 0,
"loss_scale_window": 1000,
"initial_scale_power": 16,
"hysteresis": 2,
"min_loss_scale": 1
},
"zero_optimization": {
"stage": 3,
"offload_optimizer": {
"device": "cpu",
"pin_memory": true
},
"offload_param": {
"device": "cpu",
"pin_memory": true
},
"overlap_comm": true,
"contiguous_gradients": true,
"sub_group_size": 1e9,
"reduce_bucket_size": "auto",
"stage3_prefetch_bucket_size": "auto",
"stage3_param_persistence_threshold": "auto",
"stage3_max_live_parameters": 1e9,
"stage3_max_reuse_distance": 1e9,
"stage3_gather_16bit_weights_on_model_save": true
},
"steps_per_print": 2000,
"train_batch_size": "auto",
"train_micro_batch_size_per_gpu": "auto",
"wall_clock_breakdown": false
}
実行処理
import torch
import deepspeed
from transformers.deepspeed import HfDeepSpeedConfig
from transformers import AutoModelForCausalLM, AutoTokenizer, AutoConfig
import json
import os
model_name= "elyza/ELYZA-japanese-Llama-2-7b-instruct"
# multi-GPU関連の設定
os.environ["TOKENIZERS_PARALLELISM"] = "false" # To avoid warnings about parallelism in tokenizers
local_rank = int(os.getenv("LOCAL_RANK",0))
world_size = int(os.getenv("WORLD_SIZE",1))
torch.cuda.set_device(local_rank)
deepspeed.init_distributed()
# ベースとなるZeRO3 configの読み込み
ds_config_file = "zero_infer.json"
with open(ds_config_file) as f:
ds_config = json.load(f)
# 推論用に修正
model_config = AutoConfig.from_pretrained(model_name)
hidden_size = model_config.hidden_size
ds_config["train_batch_size"] = 1 * world_size
ds_config["train_micro_batch_size_per_gpu"] = 1
ds_config["reduce_bucket_size"] = hidden_size*hidden_size
ds_config["stage3_prefetch_bucket_size"] = 0.9 * hidden_size * hidden_size
ds_config["stage3_param_persistence_threshold"] = 10 * hidden_size
dschf = HfDeepSpeedConfig(ds_config) #zero3を使用するために必要(モデルロード前に実行する必要がある)
model = AutoModelForCausalLM.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)
ds_engine = deepspeed.initialize(model=model, config_params=ds_config)[0]
ds_model = ds_engine.module.eval()
from transformers import pipeline
if torch.cuda.is_available():
model.to("cuda")
def gen(input_text="今日の天気は"):
text_pipe = pipeline('text-generation',
model=ds_model,
tokenizer=tokenizer,
device="cuda:0",
max_length=200,
)
output = text_pipe(input_text)
return output[0]['generated_text']
%time gen()
出力:
'今日の天気は曇り時々雨。\n気温は15℃。\n今日は朝から雨が降っています。\n降り方が強くて、洗濯物が干せそうにありません。\n昨日は、午後から雨が降っていたので、洗濯物は干せましたが、今日は干せそうにありません。\n洗濯機を回して、洗濯物を部屋に干しておきます。\n今日の天気は曇り時々雨。\n気温は'
使用VRAM: 約4.8GB
推論時間: 1min 51 sec
CPUメモリ: VIRTで約83GB、RESで52GB
CPU側にメモリを移すことで、VRAMは大幅に節約できたようです。
(加えて、16 bit推論をしているので、必要なメモリサイズが半減した効果もあります)
VRAMは節約できた一方で、推論時間が20倍以上になってしまいました。
訓練
学習用に、少し追記した設定ファイルを作ります(zero_train.json)。
{
"fp16": {
"enabled": "auto",
"loss_scale": 0,
"loss_scale_window": 1000,
"initial_scale_power": 16,
"hysteresis": 2,
"min_loss_scale": 1
},
"optimizer": {
"type": "AdamW",
"params": {
"lr": "auto",
"betas": "auto",
"eps": "auto",
"weight_decay": "auto"
}
},
"scheduler": {
"type": "WarmupDecayLR",
"params": {
"warmup_min_lr": "auto",
"warmup_max_lr": "auto",
"warmup_num_steps": "auto",
"total_num_steps": "auto"
}
},
"zero_optimization": {
"stage": 3,
"offload_optimizer": {
"device": "cpu",
"pin_memory": true
},
"offload_param": {
"device": "cpu",
"pin_memory": true
},
"overlap_comm": true,
"contiguous_gradients": true,
"sub_group_size": 1e9,
"reduce_bucket_size": "auto",
"stage3_prefetch_bucket_size": "auto",
"stage3_param_persistence_threshold": "auto",
"stage3_max_live_parameters": 1e9,
"stage3_max_reuse_distance": 1e9,
"stage3_gather_16bit_weights_on_model_save": true
},
"steps_per_print": 2000,
"train_batch_size": "auto",
"train_micro_batch_size_per_gpu": "auto",
"wall_clock_breakdown": false
}
モデルの定義類は、推論と基本的に同じです。
import torch
import deepspeed
from transformers.deepspeed import HfDeepSpeedConfig
from transformers import AutoModelForCausalLM, AutoTokenizer, AutoConfig
import json
import os
model_name= "elyza/ELYZA-japanese-Llama-2-7b-instruct"
# multi-GPU関連の設定
os.environ["TOKENIZERS_PARALLELISM"] = "false" # To avoid warnings about parallelism in tokenizers
local_rank = int(os.getenv("LOCAL_RANK",0))
world_size = int(os.getenv("WORLD_SIZE",1))
torch.cuda.set_device(local_rank)
deepspeed.init_distributed()
# ベースとなるZeRO3 configの読み込み
ds_config_file = "zero_infer.json"
with open(ds_config_file) as f:
ds_config = json.load(f)
model_config = AutoConfig.from_pretrained(model_name)
hidden_size = model_config.hidden_size
ds_config["train_batch_size"] = 1 * world_size
ds_config["train_micro_batch_size_per_gpu"] = 1
ds_config["reduce_bucket_size"] = hidden_size*hidden_size
ds_config["stage3_prefetch_bucket_size"] = 0.9 * hidden_size * hidden_size
ds_config["stage3_param_persistence_threshold"] = 10 * hidden_size
dschf = HfDeepSpeedConfig(ds_config) #zero3を使用するために必要(モデルロード前に実行する必要がある)
model = AutoModelForCausalLM.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)
ds_engine = deepspeed.initialize(model=model, config_params=ds_config)[0]
ds_model = ds_engine.module#.eval()
データセットの定義など
train_data_pathに、適当なテキストデータへのパスを入れて下さい。
from transformers import (AutoModelForCausalLM,
DataCollatorForLanguageModeling, T5Tokenizer,AutoTokenizer,
TextDataset, Trainer, TrainingArguments)
train_data_path = "ここにデータセットのパスを指定"
train_dataset = TextDataset(
tokenizer=tokenizer,
file_path=train_data_path,
block_size=1024, #文章の長さを揃える,
#block_size=100, #文章の長さを揃える,
cache_dir="cache/"+model_name,
)
data_collator = DataCollatorForLanguageModeling(
tokenizer=tokenizer,
mlm=False
)
#deepspeed
epochs=1
training_args = TrainingArguments(
output_dir='./outputs',
num_train_epochs=epochs, # エポック数
#per_device_train_batch_size=1, # バッチサイズ
gradient_accumulation_steps=1,
gradient_checkpointing=True, #勾配チェックポイント
fp16=True, #fp16
optim='adafactor', # オプティマイザの種類
deepspeed='./zero_train.json', # deepspeedのconfigへのpath
logging_steps=100, # 途中経過を表示する間隔
)
trainer = Trainer(
model=ds_model,
args=training_args,
data_collator=data_collator,
train_dataset=train_dataset
)
#訓練
result = trainer.train()
print_summary(result)
使用VRAM: 約19GB
CPUメモリ: VIRTで約206GB、RESで150GB
学習時間はデータサイズ次第ですが、4MBのテキストで1 hr程度でした。
offloadを使わない場合と同等レベルな気がしたので、驚きました※。
(※ このモデルは今回使った環境では、offloadを使わないと、out of memoryで学習できませんでした。より小さなモデルの学習時間との比較です)
学習したモデルの保存と読み込み
0922追記 モデルの保存コマンドに問題があったので修正(model.save_pretrainedではなく、trainer.save_modelを使う)。
# Save the model and the tokenizer.
dir_name = f'finetuned/{model_name}_{epochs}'
#0922追記: model.save_pretrainedだと、うまくモデルを読み込めない感じでした
#model.save_pretrained(dir_name)
trainer.save_model(dir_name)
tokenizer.save_pretrained(dir_name)
保存したモデルのパスを、推論用コード(上述)のmodel_nameに指定してあげれば、ファインチューニングしたモデルを呼び出せます。
#普通に推論
from transformers import pipeline
from transformers import AutoModelForCausalLM, AutoTokenizer, AutoConfig
#ファインチューニングモデルの保存パスを指定
model_name="finetuned/elyza/ELYZA-japanese-Llama-2-7b-instruct_1_test"
model = AutoModelForCausalLM.from_pretrained(model_name,ignore_mismatched_sizes=True)
tokenizer = AutoTokenizer.from_pretrained(model_name)
if torch.cuda.is_available():
model.to("cuda")
def gen(input_text="今日の天気は"):
text_pipe = pipeline('text-generation',
model=model,
tokenizer=tokenizer,
device="cuda:0",
max_length=200,
#temperature = 100,
)
output = text_pipe(input_text)
return output[0]['generated_text']
gen()
deepspeedはマルチGPUにも対応しているので、この調子でいけば、13b以上のモデルも動かせるかもしれません。
速度は落ちそうですが、CPUではなく、SSDのメモリ(NVME)にoffloadすることもできるようです(未検証: リンク)。
引き続き検討します。