ローカルLLMを構築してみた(llma.cpp)

# はじめに
この度、ANGEL Calendarの企画に参加しております！
記事一覧は下記のOrganizationアカウントの一覧をチェックしてみてください！
[2024-ANGEL-Dojo Organization](https://qiita.com/organizations/2024-angel-dojo)

本記事では、私が今現在個人的な興味として取り組んでいるローカルLLMについて紹介させていただこうと思います。
(Qiita初投稿のため、お見苦しい内容になっているかもしれません)

# 背景
昨今LLMを中心とした生成AIのサービスが次々とリリースされています。
サービス内ではOpenAIをはじめとしたAPIなどが利用されていると思います。
ただし、日本国内の生成AI導入率は19%ほどのようです。

https://aismiley.co.jp/ai_news/mmresearch-survey2024-llm/

生成AIの活用にあたり、ハードルは色々ありますが、その中には以下の意見も多いと思います。
- そもそも生成AIをどう活用してよいか分からない
- コストの計算が難しい
- セキュリティなどのコンプライアンス上、利用が制限されている

そこで、今回は上記の課題解決の一環として、自前のサーバにLLM環境を立ち上げて生成AIを利用できないかを試してみました。本記事はその検証結果を紹介しようと思います。

# 利用環境・ツール

**1. サーバー構築**
自前のローカルPCではスペックに不安があり、LLM構築としてAmazon EC2の仮想サーバーを利用します。

| 種類 | 内容 |
|---|---|
| インスタンス | t3.xlarge(vCPU:4コア Memory:16GiB)  |
| OS(AMI)  | Ubuntu 24.04 LTS  |
| ストレージ(EBS)  | 90BiG 汎用SSD(gp3) 3000IOPS スループット:125MiB(秒)  |
| その他 | Python3.12.3 |

**2. Llama.cpp**
ローカル環境などでLLMを動かすことができるライブラリです(cppという名前の通り、C++で実装されている)
Meta社のLLMのLlamaのモデルを動かすために実装されたようですが、GGUFという形式に変換することで
他のLLMモデルも動作させることができます。

量子化というモデルのサイズを少なくする仕組みを用いて、CPU環境でも動作するそうです。
そこで、高価なGPUを利用しないでどこまでCPUでLLMを利用できることができるのか興味が湧いたため、CPU環境で試すことにしました。

https://note.com/kawamou/n/n076231811729

# 環境構築
環境構築をしてみます。

### EC2ディレクトリ構成
説明するものだけを記載
```
~/llm  # 作業ディレクトリ
├─ download.py                       # 利用モデルのダウンロード用Pythonスクリプト
├─.venv                              # Python仮想環境
└─ llama.cpp   　　　　　　　　　      # llama.cppのカレントディレクトリ(ビルド後にできる)
    ├─ convert_hf_to_gguf.py          # モデルのGGUF形式変換スクリプト
    ├─ llama-quantize　　　　　　　　　 # GGUF形式モデルを量子化(モデル減量化)するスクリプト
    ├─ llama-cli                      # llama.cppの
    ├─ llama-server                   # llama.cppのWebサーバ起動コマンド
    ├─ models
    │   ├─ gemma-2-2b                 # 利用モデル(gemma-2-2b)の出力ディレクトリ(download.py実行後にできる)
    │   │   ├─ gemma-2-2B-F16.gguf    # GGUF化したモデル
    │   │   ├─ gemma-2-2B-Q4_K_M.gguf # GGUFモデルを量子化したモデル
    │   │   ├─ (etc:元のモデルのファイル類)
    ...
```

### インストール
```.sh:ubuntuのモジュールをインストール
sudo apt update 
sudo apt install make gcc build-essential
sudo apt install python3.12-venv
```

```.py:Python環境の整備
# Python仮想環境の作成
python3 -m venv .venv
# Python仮想環境に切り替え
source .venv/bin/activate

# Pythonで利用するモジュールのインストール
pip3 install numpy==2.1.0
pip3 install torch==2.4.0
pip3 install sentencepiece==0.2.0
pip3 install safetensors==0.4.4
pip3 install transformers==4.44.2
pip3 install huggingface_hub==0.24.6
# pip3 install llama-cpp-python
```

```.sh:llama.cppのインストール
# llama.cppのGitリポジトリからモジュール一式をclone
# (cloneカレントディレクトリにllama.cppというディレクトリができる)
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp/

# lamma.cppビルド
make
```

これでllama.cppが利用できるようになりました！

### モデルのダウンロード
試しに、googleのgemma2というモデルを利用してみました。
Hugging Faceより、ダウンロードします

https://huggingface.co/google/gemma-2-2b

ダウンロードは以下のPythonスクリプトを作って、実行します。

```python:download.py
import huggingface_hub

model_id = "google/gemma-2-2b"  # ダウンロードするモデル名
local_dir = "./llama.cpp/models/gemma-2-2b"  # ダウンロードしたモデルの保存先ディレクトリ名
huggingface_hub.snapshot_download(model_id, local_dir=local_dir, token=<Hugging Faceのトークン：事前に発行>)
```

### モデルのGGUF化→量子化
llama.cppでモデルを扱うために、GGUF形式というファイルに変換する必要があります。
GGUFは、llamma.cppディレクトリにある"convert_hf_to_gguf.py"で可能です。

また、CPUで扱うために、GGUF形式にしたモデルを、更に量子化(モデルのサイズの減量化

***元のモデル → GGUF化 → 量子化***

```bash
# (以後は、llama.cpp以下でコマンドを実行)
# GGUF化:パラメータには、モデルのダウンロード先のディレクトリを指定する
# 出力結果：INFO:hf-to-gguf:Model successfully exported to models/gemma-2-2b/gemma-2-2B-F16.gguf
cd ./llama.cpp
python3 convert_hf_to_gguf.py ./models/gemma-2-2b

# 量子化(オプションで指定した"Q4_K_M"で、4bit量子化ということになるようです)
./llama-quantize ./models/gemma-2-2b/gemma-2-2B-F16.gguf ./models/gemma-2-2b/gemma-2-2B-Q4_K_M.gguf Q4_K_M
```

量子化のオプションは、以下のNoteで分かりやすくまとめられていると思います。
https://note.com/bakushu/n/n1badaf7a91a0

これでモデルの量子化までできました(gemma-2-2B-Q4_K_M.gguf)
このモデルを使って、プロンプトを投げてみましょう。

## 実行
下記の通りにプロンプトを実行できます。
```bash
./llama-cli -m ./models/gemma-2-2b/gemma-2-2B-Q4_K_M.gguf -p "日本の有名な企業をいくつか教えてください"
```

![answer-image.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3868313/3dd8750f-72b6-f877-c5c6-d763f2fffae9.gif)

それっぽく出てきています。(CPUであることを考えると速いです)

ただし、上の結果は比較的まともな回答をしてくれたときで、総合的にみると精度はよろしくない感じです。

同じ質問をしても、以下のように途中から回答が「株式会社三井物産」と出てくる感じで、回答が終わりません...

```
日本の有名な企業をいくつか教えてください。

答え：

・ 株式会社サンリオ
・ 日本航空
・ 株式会社ダイハツ工業
・ 株式会社三菱重工業
・ 株式会社東芝
・ 株式会社トヨタ自動車
・ 株式会社ソニー
・ 株式会社日清食品
・ 株式会社日本製糖
・ 株式会社三菱商事
～
・ 株式会社住友不動産
・ 株式会社三井不動産
・ 株式会社三井物産
・ 株式会社三井物産
・ 株式会社三井不動産
・ 株式会社三井物産
・ 株式会社三井物産
・ 株式会社三井物産
・ 株式会社三井物産
...(永遠と続く)
```

このあたりは、出力トークンを制限するオプションなどがあるようなので、それを指定するなどの対策があるかもしれません。(未検証)

## WEB-API化
llama.cppは、以下のようにWEB-APIにすることもできます。
```bash:WEB-API(ポート8080)として実行
./llama-server -m ./models/gemma-2-2b/gemma-2-2B-Q4_K_M.gguf --host 0.0.0.0 --port 8080
```

APIの呼び出しは、curl等のHTTPクライアントで実行します。
```bash:APIで回答を生成
# (指定オプション)
# stream: trueを設定することで、回答をストリーミング形式で出力
# n_predict:テキストを生成するときに予測するトークンの最大数

# その他指定可能なオプション
# https://github.com/ggerganov/llama.cpp/blob/master/examples/server/README.md
curl --request POST \
    --url http://<EC2インスタンスのIPアドレス>:8080/completion \
    --header "Content-Type: application/json" \
    --data '{"prompt": "日本の有名な企業をいくつか教えてください", "stream": true, "n_predict": 128}'
```

回答の取得イメージはこちら(トークン単位で回答が取得できている)
![answer-image_api.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3868313/fad73d24-a080-4cfc-9040-6acd6251809e.gif)

## まとめ

以上のように、自前でLLM実行環境を構築してみました。
本記事では環境構築して、プロンプトから回答が得られるところまで試してみました。
量子化で、CPUでもそれなりなパフォーマンスが発揮されましたが、回答がおかしい(ループしている？)ことも多く、実用性には疑問が残る結果となりました。
(細かいチューニングなどは何もしていないので、そのあたりもよろしくなかったと思います)

別のモデルの利用や、GPU環境にて実施することで結果は変わるかもしれないので、今後はそのアプローチも試してみようと思います。

現行でLLMを利用した生成AIアプリケーションを構築する場合、各生成AIのAPIを扱うのが現実的だと思います。一方、試してみたように自前で構築できることも分かったので、今後はセキュリティ等の要件によってはオンプレ環境のサーバーに構築するなどの選択肢も出てくるのではと感じました。

## 補足
自前のLLM環境構築に、Ollamaというツールもあるようです
(内部でllama.cppが使われており、扱いも簡単のようです。こちらはまたの機会に...)

https://ollama.com/

## 参考リンク

この記事は、その他、以下の情報を参考にさせていただきました。

- [非力なパソコンでもLLMを動かしたい!? llama.cppの紹介](https://developers.cyberagent.co.jp/blog/archives/45308/)
- [llama.cpp の動かし方と量子化手法](https://zenn.dev/turing_motors/articles/f5f19f875bd8ba)