こんにちは、CCCMKホールディングスTECH LAB三浦です。
先日花火を久しぶりに見ました。遠くから眺めるだけだったのですが、色々な色や形の花火が見られました。花火の形も時代とともに変わっていくのだなぁとしみじみしました。
さて、以前オープンソースのLLMを少ないリソース環境下でも推論用途で使用可能にするOllamaというツールを使ってエッジコンピュータの"Jetson AGX ORIN"でLLMやVision Language Model(VLM)を動かした話をまとめました。
OllamaではGGUFというフォーマットで出力されたモデルをロードして使うことが出来ます。
上記の記事は、すでにGGUF化されたモデルをダウンロードしてOllamaに取り込む、という方法についてまとめたのですが、独自でFine-TuningしたモデルをGGUF化してOllamaに取り込むにはどうしたらいいのか気になり調べて試してみることにしました。
Fine-TuningからGGUFまでのロードマップ
使用するツールやライブラリによっていくつかパターンは存在しますが、私は以下のような手順でLLMのGGUF生成を行いました。
- Hugging FaseライブラリによるFine-Tuning
- LLMとTokenizerの保存
- llama.cppのインストール
- llama.cppによるGGUF生成と量子化
Fine-Tuningのアプローチ
今回Fine-Tuningは"QLoRA"というテクニックを用いて行いました。QLoRAは"QLoRA: Efficient Finetuning of Quantized LLMs"という論文で提案されたテクニックで、モデル全体のパラメータを学習するのではなくモデルに追加したアダプターだけを学習対象にし、かつパラメータの量子化を組み合わせることで小メモリで短時間で高い精度でLLMのFine-Tuningを可能にします。
Hugging Faceのライブラリ(PEFT, TRL, Transformers)を組み合わせることでQLoRAを実装することが出来ます。具体的な実装はこちらの記事を参考にさせて頂きました。
Fine-Tuningモデルの保存
Hugging Faceのライブラリを使ってQLoRAでFine-Tuningを行うと、PeftModel
というクラスのモデルが出来上がります。この状態でモデルを保存すると、モデル全体ではなくアダプター部分だけが保存されます。GGUF化する際にはモデルの全ての情報が必要になるので、アダプターをベースモデルと結合したあとにモデルを保存する、といった手続きが必要になります。
具体的には次のようなコードで実現します。
mergid_ft_model = model.merge_and_unload() # アダプターをベースモデルにマージ mergid_ft_model.save_pretrained("/path/to/model_dir") tokenizer.save_pretrained("/path/to/model_dir")
出力後、出力対象のディレクトリに次のようにモデルとトークナイザ関連のファイルが含まれていることを確認します。
llama.cppのインストール
Hugging FaceのモデルファイルをGGUFに変換するためにllama.cppを使用します。llama.cppのインストール手順は先週チームの高橋くんがまとめてくれたこちらの記事を参考にしました。
GGUFの作成
llama.cppにはHugging FaceのモデルをGGUFに変換するためのPythonスクリプトが含まれています。これを動かすための依存ライブラリのインストールも必要になります。
具体的には次のようなコマンドを実行しました。
pip install -r ./llama.cpp/requirements.txt python ./llama.cpp/convert_hf_to_gguf.py /path/to/model_dir --outtype f16 --outfile /path/to/model_dir/quantized_model.gguf
2行目のコードからGGUF変換処理を実行しています。最初の引数で先ほどモデルとトークナイザを出力したディレクトリパスを指定し、--outfile
で出力するGGUFファイルパスを指定しています。
--outtype
はconvert_hf_to_gguf.py
のコードを見る限り、指定しなくても良さそうですが今回は指定しました。パラメータの量子化は次のステップで行うため、ここでは元のモデルと同じパラメータの型を指定しました。
ちなみに元のモデルに関する情報は、モデル出力ディレクトリに含まれるconfig.json
ファイルを見ると確認することが出来ます。
このGGUF化の部分はllama.cppにもあまりドキュメントがなく、エラーが発生したら都度convert_hf_to_gguf.py
の該当の場所を確認して対応をしました。
私がぶつかったエラーではモデルが把握している語彙数(vocab_size
)とトークナイザに含まれる語彙数が違う、といった内容のものが発生しました。Fine-Tuningの際にtokenizer
のpad_token
を指定する時、方法によってはトークナイザの語彙数を増やしてしまうことがあり、それによって発生するエラーであることが分かりました。
# こちらではエラー # tokenizer.add_special_tokens({'pad_token': '[PAD]'}) # こちらではOK tokenizer.pad_token = tokenizer.eos_token
量子化
最後にモデルのパラメータを量子化することでモデルのサイズをぎゅっと圧縮することが出来ます。量子化の方法はいくつかありますが、今回はQ4_K_M
というオプションを指定して量子化しました。
./llama.cpp/llama-quantize /path/to/model/quantized_model.gguf /path/to/model/quantized_model_Q4_K_M.gguf Q4_K_M
これでGGUFの作成は完了です。
課題
GGUFの作成までは上記手順で出来るのですが、実際動かしてみるとGGUF化の前と後でモデルの動作が異なるようになりました。GGUF化した方がモデルの返答精度が落ちてしまったような印象です。量子化のオプションをもう少し調べた方が良いかもしれない、と感じています。
まとめ
ということで、今回はFine-TuningしたLLMをGGUF化する手順についてまとめてみました。最初はあちこちでトラブルが起きて苦戦しましたが、一連のプロセスを確立することが出来てよかったです。GGUF化することでモデルの精度が落ちてしまっている点が気になるので、精度を保つ方法についてもう少し調べてみたいと思います。