見出し画像

Llamma-65bなどの大規模言語モデルをQLoRAでファインチューニング

概要

先日、llmtuneというライブラリを試しましたが、本記事のライブラリが色々な意味で、上位互換な印象です。

インストール

miniconda環境で構築しました。

conda create -n gua
conda activate gua
conda install pip


レポジトリのクローン

git clone https://github.com/artidoro/qlora

pip

cd qlora
pip install -U -r requirements.txt

サンプルコードの実行

たくさんのサンプルコードがありました。

Jupyter-notebookでの 7bの実行
examples/guanaco_7B_demo_colab.ipynb を実行すればOKです。 (colabでも動きそうですね)

実行すると、web uiでchatができました。日本であまり話題にならないだけあって、日本語力の低さが際立ちます。

日本語は壊滅的でした


英語は喋れます

各種モデルのトレーニング

scriptsフォルダに色々とサンプルコードが入っていました。
oasst1というデータセットを学習するようです(約85k行)。

そのまま実行しても、errorが出ました。
AttributeError: /home/user/miniconda3/envs/gua/lib/python3.11/site-packages/bitsandbytes/libbitsandbytes_cpu.so: undefined symbol: cquantize_blockwise_fp16_nf4

GPUではなくcpuで動かそうとしていることが原因のようです。cuda toolkitをinstallすると解決できました。

conda install -c nvidia cuda-toolkit

例えば、7bをfine tuningする際は以下のコマンドになります

sh scripts/finetune_guanaco_7b.sh 

RTX-3090でのgpu使用率は以下の感じです。メモリは余裕で余っています。
演算自体は、どちらか1枚のgpuで行っている印象です。

140W制限※で、学習に27時間ほどかかるようでした。
(※手持ちのマシンをフル出力で動かすと、放熱が不十分でプロセスがzombie化します)

65bのファインチューニング

実行コード

sh scripts/finetune_guanaco_65b.sh 

学習開始まで、3分ほどかかった気がします。
RTX-3090 x2で動きました。
Falcontuneとかはmulti gpu非対応なので、ありがたい仕様です。

学習完了には230時間ほど、必要なようです。流石の65bです。
H100の購入を検討したくなる計算量です。

オリジナルデータのqlora

自作のdatasetで試してみます。
まずは、適当にデータセットを作ります。
何件かのデータを、databricks-dolly-15k-jaからデータを引っ張ってきました

test.json

[
    {
        "input": "ヴァージン・オーストラリア航空はいつから運航を開始したのですか?ヴァージン・オーストラリア航空(Virgin Australia Airlines Pty Ltd)はオーストラリアを拠点とするヴァージン・ブランドを冠する最大の船団規模を持つ航空会社です。2000年8月31日に、ヴァージン・ブルー空港として、2機の航空機、1つの空路を運行してサービスを開始しました。2001年9月のアンセット・オーストラリア空港の崩壊後、オーストラリアの国内市場で急速に地位を確立しました。その後はブリスベン、メルボルン、シドニーをハブとして、オーストラリア国内の32都市に直接乗り入れるまでに成長しました。",
        "output": "ヴァージン・オーストラリア航空は、2000年8月31日にヴァージン・ブルー航空として、2機の航空機で単一路線の運航を開始しました。"
    },
    {
        "input": "魚の種類はどっち?イコクエイラクブカとロープ",
        "output": "イコクエイラクブカ"
    },
    {
        "input": "ラクダはなぜ水なしで長く生きられるのか?",
        "output": "ラクダは、長時間にわたってエネルギーと水分で満たされた状態を保つために、腰の脂肪を利用しています。"
    },
    {
        "input": "アリスの両親には3人の娘がいる:エイミー、ジェシー、そして三女の名前は?",
        "output": "三女の名前はアリス"
    },
    {
        "input": "小森田友明はいつ生まれたの?小森田は1981年7月10日、熊本県に生まれる。高校卒業後、2000年にJ1リーグのアビスパ福岡に入団。2001年にMFとしてデビューするも、出番は少なく、2001年シーズン終了後にJ2リーグに降格する。2002年、J2の大分トリニータに移籍。守備的MFとしてレギュラーに定着し、2002年に優勝、2003年に昇格を果たす。2005年まで多くの試合に出場した。2005年9月、J2のモンテディオ山形に移籍。2006年、J2のヴィッセル神戸へ移籍。守備的ミッドフィルダーとしてレギュラーになったものの、夏には徐々に出番が少なくなっていった。2007年、地元に本拠地を置く日本フットボールリーグのロッソ熊本(後のロアッソ熊本)へ移籍。レギュラーとして活躍し、2008年にはクラブはJ2に昇格した。その後は出場機会は限られたものの、多くの試合に出場した。2010年、インドネシアに渡り、ペルセラ・ラモンガンに移籍。2010年7月、日本に戻り、J2のギラヴァンツ北九州に入団。2012年に引退するまで、守備的ミッドフィルダーやセンターバックとして多くの試合に出場した。",
        "output": "小森田友明は1981年7月10日に生まれました。"
    }
]


evalの設定にちょっと躓いたので、evalなしで実行します。
(eval関係のcommandが残ってます)

dataset_path=dataset/test.json
eval_dataset_size=1
python qlora.py \
    --model_name_or_path huggyllama/llama-7b \
    --output_dir ./output/test \
    --logging_steps 10 \
    --save_strategy steps \
    --data_seed 42 \
    --save_steps 10 \
    --save_total_limit 40 \
    --evaluation_strategy steps \
    --eval_dataset_size $eval_dataset_size\
    --max_eval_samples 1000 \
    --per_device_eval_batch_size 1 \
    --max_new_tokens 32 \
    --dataloader_num_workers 3 \
    --group_by_length \
    --logging_strategy steps \
    --remove_unused_columns False \
    --do_train \
    --lora_r 64 \
    --lora_alpha 16 \
    --lora_modules all \
    --double_quant \
    --quant_type nf4 \
    --bf16 \
    --bits 4 \
    --warmup_ratio 0.03 \
    --lr_scheduler_type constant \
    --gradient_checkpointing \
    --dataset $dataset_path \
    --dataset_format input-output \
    --source_max_len 16 \
    --target_max_len 512 \
    --per_device_train_batch_size 1 \
    --gradient_accumulation_steps 16 \
    --max_steps 1875 \
    --eval_steps 187 \
    --learning_rate 0.0002 \
    --adam_beta2 0.999 \
    --max_grad_norm 0.3 \
    --lora_dropout 0.1 \
    --weight_decay 0.0 \
    --seed 0

max_steps=1875のままで動かしてみます。
今回はデータ数が少ないこともあり、すぐにlossがゼロになったので、途中で止めました。

Loraモデルの動作確認

jupyterで動かします。
まずは、LoRA前の応答を見てみます。

import torch
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig

#load base model

model_id = "huggyllama/llama-7b"
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16
)

tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(model_id, quantization_config=bnb_config, device_map={"":0})

model.eval()

text = "#Q: ラクダはなぜ水なしで長く生きられるのか? #A"
device = "cuda:0"
inputs = tokenizer(text, return_tensors="pt").to(device)

with torch.no_grad():
  outputs = model.generate(**inputs, max_new_tokens=256)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))

回答はこちら

#Q: ラクダはなぜ水なしで長く生きられるのか?
#A: ラクダは、水を含む物質を含むことができる。 そのため、ラクダは、水を含む物質を含むことができる。 そのため、ラクダは、水を含む物質を含むことができる。 そのため、ラクダは、水を含む物質を含むことができる。 そのため、ラクダは、水を含む物質を含むことができる。 そのため、ラクダは、水を含む物質を含むことができる。 そのため、ラクダは、水を含む物質を含むことができる。 そのため、ラクダは、水を含む物質を含むことが

※ベースがllammaで、instructionの学習をしていないこと、さらに日本語をちゃんと勉強していないようなので、おかしな回答になっています

QLoRAの推論


# apply peft model
peft_name = "guanaco/qlora/output/test/checkpoint-50"
model = PeftModel.from_pretrained(
    model, 
    peft_name, 
    device_map={"":0}
)
model.eval()

text = "#Q: ラクダはなぜ水なしで長く生きられるのか? #A"
device = "cuda:0"
inputs = tokenizer(text, return_tensors="pt").to(device)

with torch.no_grad():
  outputs = model.generate(**inputs, max_new_tokens=256)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))

回答はこちら
#Q: ラクダはなぜ水なしで長く生きられるのか?
#A: ラクダは、2016年7月31日に生まれました。

意味不明ですが、学習データセットの「 "output": "小森田友明は1981年7月10日に生まれました。"」に引っ張られたのだと思います。
学習自体は成功しているものと思われます。

まとめ

  • QLoRAで65bレベルのLLMを、24GB x2 のVRAMなどで学習可能でした

  • 一般家庭用のコンピュータで65bレベルをチューニング・推論できるのは素晴らしいことだと思います

  • llamaはあまりinstructionされていなかったり、日本語が微妙なので、fine tuningしたいデータの量、質によっては、予めinstruciton済みのモデルをfine tuningした方が良いかもしれません


いいなと思ったら応援しよう!