Unityでllama.cpp(calm2)をサーバーAPIとしてチャット機能を作る

はじめに

llma.cppやStyleBertVITS2(音声合成ライブラリ) をunityで簡単に使えるように以下のライブラリを作りました! 記事の内容はライブラリ内に含まれていますので、記事の内容を実装される場合はライブラリをご使用ください

github.com

環境

  • Unity 2023.2.4f1
  • llama.cpp

準備

cmakeの環境作成

cmake公式から64ビットWindowsインストーラをダウンロードして、インストールします

llama.cppの環境作成

まずはllama.cppをcloneします

git clone https://github.com/ggerganov/llama.cpp

次にcmakeを使ってllama.cppをビルドします

mkdir build
cd build
cmake .. -DLLAMA_CUBLAS=ON
cmake --build . --config Release
cd ..

モデルのダウンロード

今回は cyberagent/calm2-7b-chatのGGUFモデルを使うため、TheBloke/calm2-7B-chat-GGUFをダウンロードします。
こちらでは、calm2-7b-chat.Q4_K_M.ggufを使用しました

ダウンロードしたモデルを llama.cpp/models に配置します

llama.cppの動作確認

現時点でllama.cppが単体で動くようになっているため、以下を実行してCPU推論ができることを確認します (cmakeターミナルが日本語だと文字化けするので、英語で質問しています)

./main.exe -m ./models/calm2-7b-chat.Q4_K_M.gguf --temp 0.1 -p "User:Please answer in Japanese. How tall is Mt. fuji:"

実行すると以下のようになると思います

system_info: n_threads = 12 / 24 | AVX = 1 | AVX_VNNI = 0 | AVX2 = 1 | AVX512 = 0 | AVX512_VBMI = 0 | AVX512_VNNI = 0 | FMA = 1 | NEON = 0 | ARM_FMA = 0 | F16C = 1 | FP16_VA = 0 | WASM_SIMD = 0 | BLAS = 0 | SSE3 = 1 | SSSE3 = 1 | VSX = 0 |
sampling:
        repeat_last_n = 64, repeat_penalty = 1.100, frequency_penalty = 0.000, presence_penalty = 0.000
        top_k = 40, tfs_z = 1.000, top_p = 0.950, min_p = 0.050, typical_p = 1.000, temp = 0.100
        mirostat = 0, mirostat_lr = 0.100, mirostat_ent = 5.000
sampling order:
CFG -> Penalties -> top_k -> tfs_z -> typical_p -> top_p -> min_p -> temp
generate: n_ctx = 512, n_batch = 512, n_predict = -1, n_keep = 0


User:Please answer in Japanese. How tall is Mt. fuji:
ASSISTANT: 富士山は、3776メートルです。 [end of text]

llama_print_timings:        load time =    1167.49 ms
llama_print_timings:      sample time =       4.14 ms /    18 runs   (    0.23 ms per token,  4348.88 tokens per second)
llama_print_timings: prompt eval time =     541.57 ms /    15 tokens (   36.10 ms per token,    27.70 tokens per second)
llama_print_timings:        eval time =    5475.73 ms /    17 runs   (  322.10 ms per token,     3.10 tokens per second)
llama_print_timings:       total time =    6057.06 ms
Log end

llama.cppをサーバーとして実行

llama.cppをサーバーとして実行するときは、以下のコマンドを実行します

./server.exe -m models/calm2-7b-chat.Q4_K_M.gguf -c 2048 -t 24

オプションは以下になっています

  • --threads N, -t N: 生成時に使用するスレッド数
  • -c N, --ctx-size N: コンテキスト長

llama.cppのサーバモードは公式の ReadMeがあるので、詳細は以下をご確認ください。

github.com

Unityに必要なライブラリの追加

非同期処理に UniTaskやレスポンスの構造化を簡単にするために newtonsoft-jsonを使用します。

以下を manifest.json に追加します

"com.unity.nuget.newtonsoft-json": "3.2.1",
"com.cysharp.unitask": "2.5.0",

レスポンスのクラスの作成

llama.cppでcalm2にAPIをpostした際のレスポンスの中身を確認すると以下のようになっています。

{
    "content": "鹿目まどかです。彼女はアニメのシリーズ全体を通じて、ファンに愛されています。\nASSISTANT: あなたは彼女の性格や外見だけでなく、彼女の内面にも深い魅力を感じているようです。彼女の絶望から立ち直る姿は多くの人々を感動させ、勇気を与えました。また、彼女は周囲の人々に対して優しく、思いやりのある行動をとることが多く、その人間性についても高く評価されています。",
    "generation_settings": {
        "frequency_penalty": 0.0,
        "grammar": "",
        "ignore_eos": false,
        "logit_bias": [],
        "min_p": 0.05000000074505806,
        "mirostat": 0,
        "mirostat_eta": 0.10000000149011612,
        "mirostat_tau": 5.0,
        "model": "models/calm2-7b-chat.Q4_K_M.gguf",
        "n_ctx": 2048,
        "n_keep": 0,
        "n_predict": 128,
        "n_probs": 0,
        "penalize_nl": true,
        "penalty_prompt_tokens": [],
        "presence_penalty": 0.0,
        "repeat_last_n": 64,
        "repeat_penalty": 1.100000023841858,
        "seed": 4294967295,
        "stop": [],
        "stream": false,
        "temperature": 0.800000011920929,
        "tfs_z": 1.0,
        "top_k": 40,
        "top_p": 0.949999988079071,
        "typical_p": 1.0,
        "use_penalty_prompt_tokens": false
    },
    "model": "models/calm2-7b-chat.Q4_K_M.gguf",
    "prompt": "User:日本語で回答してください。まどマギで一番可愛いキャラは? Assistant: ",
    "slot_id": 0,
    "stop": true,
    "stopped_eos": true,
    "stopped_limit": false,
    "stopped_word": false,
    "stopping_word": "",
    "timings": {
        "predicted_ms": 59830.736,
        "predicted_n": 82,
        "predicted_per_second": 1.3705330317180122,
        "predicted_per_token_ms": 729.6431219512194,
        "prompt_ms": 1514.903,
        "prompt_n": 18,
        "prompt_per_second": 11.881948877254846,
        "prompt_per_token_ms": 84.16127777777778
    },
    "tokens_cached": 100,
    "tokens_evaluated": 18,
    "tokens_predicted": 82,
    "truncated": false
}

上記をUnityで扱いやすくするためにクラス化します

using System.Collections.Generic;

namespace AI
{
    /// <summary>
    /// calm2のllama.cppのサーバーからのレスポンス
    /// </summary>
    public class Calm2Response
    {
        public string content;
        public GenerationSettings generation_settings;
        public string model;
        public string prompt;
        public int slot_id;
        public bool stop;
        public bool stopped_eos;
        public bool stopped_limit;
        public bool stopped_word;
        public string stopping_word;
        public Timings timings;
        public int tokens_cached;
        public int tokens_evaluated;
        public int tokens_predicted;
        public bool truncated;
    }
    
    public class GenerationSettings
    {
        public float frequency_penalty;
        public string grammar;
        public bool ignore_eos;
        public List<object> logit_bias;
        public float min_p;
        public int mirostat;
        public float mirostat_eta;
        public float mirostat_tau;
        public string model;
        public int n_ctx;
        public int n_keep;
        public int n_predict;
        public int n_probs;
        public bool penalize_nl;
        public List<object> penalty_prompt_tokens;
        public float presence_penalty;
        public int repeat_last_n;
        public float repeat_penalty;
        public ulong seed;
        public List<object> stop;
        public bool stream;
        public float temperature;
        public float tfs_z;
        public int top_k;
        public float top_p;
        public float typical_p;
        public bool use_penalty_prompt_tokens;
    }

    public class Timings
    {
        public double predicted_ms;
        public int predicted_n;
        public double predicted_per_second;
        public double predicted_per_token_ms;
        public double prompt_ms;
        public int prompt_n;
        public double prompt_per_second;
        public double prompt_per_token_ms;
    }

}

Unityからllama.cppに対してAPIを叩く

最後にUnity側からllama.cppのローカルサーバーに APIを叩いていきます

Unityからllama.cppのローカルサーバーにpostするクラスは以下になります

using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;

namespace AI
{
    /// <summary>
    /// calm2-chatモデルを使用したチャット機能
    /// </summary>
    public static class Calm2Model
    {
        /// <summary>
        /// llama.cppのサーバーURL
        /// </summary>
        private const string llamaServerURL = "http://localhost:8080/completion";

        /// <summary>
        /// チャットを送信する
        /// </summary>
        /// <param name="prompt"></param>
        public static async UniTask<string> PostRequest(string prompt,CancellationToken cancellationToken)
        {
            // JSONデータの準備
            var jsonData = $"{{\"prompt\": \"User:{prompt} Assistant: \",\"n_predict\": 128}}";

            // UnityWebRequestオブジェクトの作成
            using var webRequest = new UnityWebRequest(llamaServerURL, "POST");
            // リクエストボディの設定
            var jsonToSend = new System.Text.UTF8Encoding().GetBytes(jsonData);
            webRequest.uploadHandler = (UploadHandler)new UploadHandlerRaw(jsonToSend);
            webRequest.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();

            // ヘッダーの設定
            webRequest.SetRequestHeader("Content-Type", "application/json");

            // リクエストを送信し、完了を待つ
            await webRequest.SendWebRequest().ToUniTask(cancellationToken:cancellationToken);

            // エラーチェック
            if (webRequest.result != UnityWebRequest.Result.Success)
            {
                Debug.Log("Error: " + webRequest.error);
                return null;
            }

            // レスポンスの表示
            var calm2Response = JsonUtility.FromJson<Calm2Response>(webRequest.downloadHandler.text);
            Debug.Log("Response: " + calm2Response.content);
            return calm2Response.content;
        }
    }
}

後は適当に画面を作ってPost関数を作成して、Unity上で実行すると以下のようになります (UIは適当です)

llama.cppのGPUオフロード対応

llma.cppのGPUオフロードの動作確認

まずは何もせずに叩くとGPUオフロードができるかを試します

./main -m 'models/calm2-7b-chat.Q4_K_M.gguf' -n 100 --temp 0.1 -p 'User:Please answer in Japanese. How tall is Mt. fuji:' -ngl 32 -b 512

この状態で、BLAS = 1 になっていれば問題ありません。環境構築はスキップしてください

GPU版のビルド

以下を実行します

$ mkdir build
$ cd build
$ cmake .. -DLLAMA_CUBLAS=ON
$ cmake --build . --config Release
$ cd ..

実行後に llama.cpp/build/bin/Release/server.exemain.exe が生成されているため、llama.cpp直下に移動させます

仮に生成されていない場合等は、以下の原因等が考えられるため確認してください

zenn.dev

GPUオフロードのサーバーを実行する

以下でGPUオフロードするllama.cppのサーバーを立てることができます

.\server.exe -m .\models\calm2-7b-chat.Q4_K_M.gguf -ngl 32 -b 512