【UiPath】AWS/Azure仮想マシンでローカルLLM環境を構築する

# はじめに
- 最近 [Llama 3](https://llama.meta.com/llama3/) やら [Phi-3](https://azure.microsoft.com/ja-jp/products/phi-3) やら [Gemma 2](https://blog.google/technology/developers/google-gemma-2/) やらでローカルLLM界隈が盛り上がってきています。
- **「自分でもローカルLLMを試したい！けどGPU搭載のマシンが手元にない！」** という方のために、AWSやAzureでローカルLLMを動かすのための手順をまとめてみました。
- またOllama APIをPython, REST API, UiPath Studioからコールする方法についても紹介したいと思います。

大まかな流れは次の通りです。
1. [仮想マシン構築](#1-仮想マシン構築) : Amazon EC2またはAzure Virtual MachineでGPU搭載の仮想マシンを立てる
2. [ドライバーなどのインストール](#2-ドライバーなどのインストール) : NVIDIAのドライバーとCUDAライブラリーをインストールする
3. [Dockerコンテナとモデルのデプロイ](#3-dockerコンテナとモデルのデプロイ) : DockerをインストールしてOllama・モデルとOpen WebUIをコンテナをデプロイする
4. [Open WebUI でアクセス](#4-open-webui-でアクセス) : ブラウザー経由でローカルLLMを切り替えて問い合わせる
5. [APIコール](#5-apiコール) : Ollama APIをPython, REST API, UiPath Studioからコールする

# 1. 仮想マシン構築
- ローカルLLMを動かすための仮想マシンを構築します。GPUなしでも一応動作しますが応答がかなり遅くなるのでGPU搭載の仮想マシンを用意した方が賢明です。
- AWSでは [P3/G5インスタンスなど](https://docs.aws.amazon.com/ja_jp/dlami/latest/devguide/gpu.html)、Azureでは [NCファミリーなど](https://learn.microsoft.com/ja-jp/azure/virtual-machines/sizes/gpu-accelerated/nc-family) がGPU搭載になります。手持ちのアカウント・サブスクリプションなどに応じて選択しましょう。
- 動作させるモデルのパラメーター数などに応じてインスタンスタイプを選定します。GPU 16GBでもLlama3 8BやGemma2 9Bクラスであれば動作させられます。Llama3 70Bを動かすためにはスケールアップが必要になります。どのモデルが動作するかは皆さん自身でお試しいただければと思います。

## AWS
Amazon EC2インスタンスをデプロイします。事前にVPCなどは構築しておきます。
下記スペックのEC2インスタンスを作成します。
- **OS**: Ubuntu 22.04 LTS (`ami-0595d6e81396a9efb`)
- **インスタンスタイプ**: `g4dn.xlarge`
- **ストレージ**: 256 GiB (ボリュームタイプ `gp3`)
  - ※ ローカルLLMの各モデルサイズが数GB程度あるのでディスク容量がそれなりに必要です。あとでボリューム追加したり拡張したりが面倒なので多めに取っておきます。
- [運用コスト(オンデマンド)](https://aws.amazon.com/jp/ec2/pricing/on-demand/): 2024年7月時点、東京リージョンでは ＄0.71/h、オハイオリージョンでは ＄0.526/h です。またストレージのコストが別途かかります。

![01a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178312/17790b6e-64b2-8911-56ca-3bb2fde77020.png)

今回はパブリックサブネットにインスタンスを作成します。セキュリティグループでMyIPのアクセスのみ許可しておきましょう。もちろんセキュリティを高めるにはプライベートサブネットに構築して踏み台サーバーからアクセスした方が良いですが、簡略化のためその手順は割愛します。

## Azure
Azure VMインスタンスをデプロイします。事前にリソースグループなどは作成しておきます。
下記スペックのAzure VMインスタンスを作成します。
- **OS**: Ubuntu Minimal 22.04 LTS - x64 Gen2
- **セキュリティの種類**: `Standard`
- **インスタンスタイプ**: `NC4as_T4_v3`
  - ※ クォータ制限がかかっているときには [緩和申請](https://qiita.com/akiojin/items/3b42c5101cec48825923) を上げます
- **ストレージ**: 256 GiB
- [運用コスト(従量課金制)](https://azure.microsoft.com/ja-jp/pricing/details/virtual-machines/linux/#pricing): 2024年7月時点、東日本リージョンでは ＄0.71/h、米国東部リージョンでは ＄0.526/h です。またストレージのコストが別途かかります。
  - たまたまかもしれませんが AWSの `g4dn.xlarge` インスタンスと同じ価格ですね。

![09.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178312/19e2deda-bb12-5105-49e0-20374cf1311f.png)

今回はパブリックIPを割り当ててインスタンスを作成します。ネットワークセキュリティグループでMyIPのアクセスのみ許可しておきましょう。もちろんセキュリティを高めるにはプライベートIPのみで構築して踏み台サーバーからアクセスした方が良いですが、簡略化のためその手順は割愛します。

# 2. ドライバーなどのインストール
- インスタンスが作成できましたらSSHクライアントでアクセスしてドライバーなどをインストールします。

## NVIDIAドライバーのインストール
- まずはNVIDIAドライバーなどをインストールします。次のコマンドを順次実行します。
    ```bash
    sudo apt-get update
    sudo apt install -y ubuntu-drivers-common
    sudo ubuntu-drivers devices
    ```
> == /sys/devices/LNXSYSTM:00/LNXSYBUS:00/ACPI0004:00/VMBUS:00/47505500-0001-0000-3130-444531454238/pci0001:00/0001:00:00.0 ==
> modalias : pci:v000010DEd00001EB8sv000010DEsd000012A2bc03sc02i00
> vendor   : NVIDIA Corporation
> model    : TU104GL [Tesla T4]
> driver   : nvidia-driver-545 - distro non-free
> driver   : nvidia-driver-535-server - distro non-free
> **driver   : nvidia-driver-535 - distro non-free recommended**
> driver   : nvidia-driver-470-server - distro non-free
> driver   : nvidia-driver-470 - distro non-free
> driver   : nvidia-driver-450-server - distro non-free
> driver   : nvidia-driver-418-server - distro non-free
> driver   : xserver-xorg-video-nouveau - distro free builtin

- recommendedとなっている `nvidia-driver-535` をインストールし、OS再起動します。
    ```bash
    sudo apt install -y nvidia-driver-535
    sudo reboot
    ```
- 再度SSH接続してドライバーロードを確認します。
    ```bash
    sudo dmesg | grep -i nvidia
    ```

> [    8.396041] nvidia: loading out-of-tree module taints kernel.
> [    8.396058] nvidia: module license 'NVIDIA' taints kernel.
> [    8.396064] nvidia: module verification failed: signature and/or required key missing - tainting kernel
> [    8.396066] nvidia: module license taints kernel.
> [    8.639292] nvidia-nvlink: Nvlink Core is being initialized, major device number 238
> [    8.660366] nvidia 0001:00:00.0: enabling device (0000 -> 0002)
> [    8.976123] nvidia 0001:00:00.0: can't derive routing for PCI INT A
> [    8.976127] nvidia 0001:00:00.0: PCI INT A: no GSI
> [    8.994223] audit: type=1400 audit(1719968699.363:2): apparmor="STATUS" operation="profile_load" profile="unconfined" name="nvidia_modprobe" pid=325 comm="apparmor_parser"
> [    8.994229] audit: type=1400 audit(1719968699.363:3): apparmor="STATUS" operation="profile_load" profile="unconfined" name="nvidia_modprobe//kmod" pid=325 comm="apparmor_parser"
> [    9.029938] NVRM: loading NVIDIA UNIX x86_64 Kernel Module  535.183.01  Sun May 12 19:39:15 UTC 2024
> [    9.058591] nvidia-modeset: Loading NVIDIA Kernel Mode Setting Driver for UNIX platforms  535.183.01  Sun May 12 19:31:08 UTC 2024
> [    9.061593] [drm] [nvidia-drm] [GPU ID 0x00010000] Loading driver
> [   12.189391] [drm] Initialized nvidia-drm 0.0.0 20160202 for 0001:00:00.0 on minor 1
> [   12.261708] nvidia_uvm: module uses symbols nvUvmInterfaceDisableAccessCntr from proprietary module nvidia, inheriting taint.
> [   12.298209] nvidia-uvm: Loaded the UVM driver, major device number 235.

- `nvidia-smi` を実行して次のように表示されればNVIDIAドライバーのインストールは完了です。
![11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178312/ba91c9e8-cf16-5be8-4c5c-388d29b6a044.png)

## CUDAツールキットのインストール
- CUDAツールキットをインストールします。ここでは [12.4 Update 1](https://developer.nvidia.com/cuda-12-4-1-download-archive?target_os=Linux&target_arch=x86_64&Distribution=Ubuntu&target_version=22.04&target_type=deb_local) をインストールします。次のコマンドを順次実行します。
    ```bash
    wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-ubuntu2204.pin
    sudo mv cuda-ubuntu2204.pin /etc/apt/preferences.d/cuda-repository-pin-600
    wget https://developer.download.nvidia.com/compute/cuda/12.4.1/local_installers/cuda-repo-ubuntu2204-12-4-local_12.4.1-550.54.15-1_amd64.deb
    sudo dpkg -i cuda-repo-ubuntu2204-12-4-local_12.4.1-550.54.15-1_amd64.deb
    sudo cp /var/cuda-repo-ubuntu2204-12-4-local/cuda-*-keyring.gpg /usr/share/keyrings/
    sudo apt-get update
    sudo apt install -y cuda-toolkit-12-4 
    sudo apt install -y cuda-drivers
    ```

# 3. Dockerコンテナとモデルのデプロイ

## Dockerインストール
- Dockerをインストールします。現在のユーザーでも `docker` コマンドを実行できるようにdockerグループに自分自身を含めます。
    ```bash
    curl -fsSL https://get.docker.com | sh
    sudo usermod -aG docker $(whoami)
    ```

## Ollamaコンテナをデプロイ
- Ollama DockerイメージをGPU実行できるように `nvidia-container-toolkit` をインストールしてOS再起動します。([参考情報](https://hub.docker.com/r/ollama/ollama))
    ```bash
    curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \
        | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
    curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \
        | sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' \
        | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
    sudo apt-get update
    sudo apt install -y nvidia-container-toolkit
    sudo nvidia-ctk runtime configure --runtime=docker
    sudo systemctl restart docker
    sudo reboot
    ```
- SSHで再接続後、OllamaコンテナをGPU指定して起動します。OS再起動時の自動起動をオンにします。
    ```bash
    docker run -d --gpus=all -v ollama:/root/.ollama -p 11434:11434 --name ollama --restart always ollama/ollama
    ```
- `docker ps -a` コマンドでコンテナ実行を確認します。
![12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178312/9d1184ea-73fc-51b6-c8a6-f9d95dc2beb3.png)

### モデルのデプロイ
- https://ollama.com/library でデプロイしたいモデルを検索します。代表的なモデルは [こちら](https://github.com/ollama/ollama?tab=readme-ov-file#model-library) に一覧があります。
- Ollamaでモデルをダウンロードして実行するには `docker exec -it ollama ollama run {model}:{tag}` を実行します。
  - モデル実行後の >>> のプロンプトは一旦 `/bye` で抜けます。
![13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178312/a5e353aa-16e5-29b6-c1cb-6d75b5d3ac34.png)

- コマンド実行例:
  - Llama 3: `docker exec -it ollama ollama run llama3`
  - Phi-3: `docker exec -it ollama ollama run phi3`
  - Gemma 2: `docker exec -it ollama ollama run gemma2`

- ※ もし不要なモデルを削除したい場合には `docker exec -it ollama ollama rm {model-name}` を実行しましょう。

### Ollama Libraryにないモデルのデプロイ
- Hugging Faceなどに公開されているモデルも動かすこともできます。ファイルフォーマットは `gguf` が前提になります。
- たとえば [Llama-3-ELYZA-JP-8B](https://huggingface.co/elyza/Llama-3-ELYZA-JP-8B) を動かすには次のコマンドを順次実行します。([参考情報](https://qiita.com/s3kzk/items/3cebb8d306fb46cabe9f))
    ```bash
    sudo docker exec -it ollama bash    # コンテナの中に入る
    apt install -y wget
    wget -O Llama-3-ELYZA-JP-8B-q4_k_m.gguf https://huggingface.co/elyza/Llama-3-ELYZA-JP-8B-GGUF/resolve/main/Llama-3-ELYZA-JP-8B-q4_k_m.gguf?download=true
    
    cat << 'EOF' > Modelfile  
    FROM ./Llama-3-ELYZA-JP-8B-q4_k_m.gguf
    TEMPLATE """{{ if .System }}
system

    
    {{ .System }}
{{ end }}{{ if .Prompt }}
user

    
    {{ .Prompt }}
{{ end }}
assistant

    
    {{ .Response }}
"""
    PARAMETER stop "
"
    PARAMETER stop "
"
    PARAMETER stop "
"
    PARAMETER stop "<|reserved_special_token"
    EOF
    
    ollama create elyza:jp8b -f Modelfile
    ollama run elyza:jp8b
    exit    # コンテナから出る
    ```
## Open WebUI コンテナをデプロイ
- Ollama で実行中のモデルにWebインターフェースから問い合わせできるように Open WebUI のコンテナをデプロイします。
    ```bash
    docker run -d -p 8080:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
    ```

# 4. Open WebUI でアクセス
- ブラウザーで `http://{Ollama-ip}:8080` にアクセスします。あらかじめファイアウォールを通しておきましょう。
- 初回サインインにはサインアップが必要になります。
- **[Select a model]** で先ほど `ollama run` でデプロイしたLLMを選択できます。早速プロンプトを実行してみましょう！
- GPU 16GBで `Llama-3-ELYZA-JP-8B` を動かすと、こんな感じで動作します。

![elyza_response.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178312/40e5eb98-777f-cefa-ef1b-0296f6cab097.gif)

- ※ 応答が遅い場合にはGPUが使用されていない可能性があります。`nvidia-smi` でGPUメモリが消費されているか確認しましょう。

# 5. APIコール
- エンドポイント `http://{Ollama-ip}:11434` でOllama APIをコールできます。あらかじめファイアウォールを通しておきましょう。

## Python
- [Ollama Python Library](https://github.com/ollama/ollama-python) を使って、PythonコードでOllama APIをコールしてみましょう。
    ```python:ollama-chat.py
    from ollama import Client
    client = Client(host='http://{Ollama-ip}:11434')
    response = client.chat(model='llama3', messages=[
      {
        'role': 'user',
        'content': 'Why is the sky blue?',
      },
    ])
    print(response['message']['content'])
    ```

- 実行すると質問(Why is the sky blue?)に対する回答が表示されます。
![07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178312/467f44cf-fb59-2cf7-7acd-0d4a3bbde9a0.png)


## REST API
- Ollama APIの使用方法は [こちら](https://github.com/ollama/ollama/blob/main/docs/api.md) をご覧ください。
- Postmanを使って **Generate a chat completion** (`POST http://{Ollama-ip}:11434/api/chat`)を実行してみましょう。

    ```json
    {
      "model": "llama3",
      "messages": [
        {
          "role": "user",
          "content": "Why is the sky blue?"
        }
      ],
      "stream": false,
      "options": {
        "temperature": 0.2,
        "top_p": 0.8
      }
    }
    ```
![06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178312/b88365bf-2903-207e-97f3-bca3e6c567d6.png)

## UiPath Studio
UiPath StudioにてHTTP要求アクティビティを使用してAPIコールしてみます。実装手順としては [Azure OpenAI Serviceなどと連携する場合](https://qiita.com/hidecha/private/8adb863b4ea336997c9e#3-uipath-studio%E5%8D%98%E7%8B%AC) と同様ですが、OllamaはAPIキーなど認証の仕組みはありません。

1. UiPath Studioを起動してワークフローを作成します。今回はクロスプラットフォームのプロジェクトを使用します。
    ![14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178312/c769876a-53b1-a25b-1bf9-f96bdec70fcc.png)

2. リクエストボディのためのJSON形式のテンプレートを使用します。あらかじめテキストファイル `request.json` として保存しておきます。

    ```json:request.json
    {
      "model": "{{model}}",
      "messages": [
        {
          "role": "user",
          "content": "{{prompt}}"
        }
      ],
      "stream": false,
      "options": {
        "temperature": 0.2,
        "top_p": 0.8
      }
    }
    ```

3. ワークフロー内ではJSON形式のテンプレートを読み込みます。
    - {{model}} 部分を入力引数として受け取ったモデル `in_model` で置換します。
    - {{prompt}} 部分を入力引数として受け取った質問内容 `in_question` で置換します。
    - 置換した文字列を `req_body` 変数にセットします

    ![15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178312/527dc04c-e156-7b87-0ff9-e625ff0b9e94.png)

4. HTTP要求アクティビティ を実装します。
    - 要求メソッド: POST を指定
    - エンドポイント: `http://{Ollama-ip}:11434/api/chat` を指定
    - ヘッダー: `Content-Type: application/json` を指定
    - ボディ: 上の手順で組み立てた `req_body` 変数を指定
    - タイムアウト: 120000 (2分間) に変更
    - 本文形式: `application/json` に変更
    - 応答コンテンツ: `res_content` 変数にセット
    - 応答ステータスコード: `res_status` 変数にセット
    ![16a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178312/f1abff50-d8f2-e850-a169-ee0b4aee4cfd.png)

5. 後続の処理は次の通りです。
    - ステータスコード `res_status` をチェックし、200であれば処理を継続し、200以外であればエラーを返します。
    - 応答のコンテンツ `res_content` を逆シリアル化してJSONオブジェクト `json_content` にセットします。
    - `json_content("message")("content").ToString` によってJSONオブジェクトから応答メッセージ部分を取り出してString型に変換します。 
    - 出力パラメーター `out_answer` にセットします。
    ![17a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178312/82cc7016-2ac9-1332-5958-046b7c4bbd55.png)

6. デバッグ実行して正常に実行されることを確認します。

# おわりに
今回はAWS/Azure仮想マシンでローカルLLM環境を構築し、Open WebUIやAPIを使用してLLMに問い合わせる手順について説明しました。
これからもいろいろなモデルがリリースされることが期待されますので、手元にこのような環境があると手軽に検証できて便利だと思います。ぜひお試しください！