はじめに
今回の成果物を最初に載せておきます。
![](https://emoclework.jp/wp-content/uploads/2024/06/30613b0307ca9fd6428db8133a1eef78-1.gif)
さて、以前からGodotでLLMを実行する話を書いています。
この話は、llamafile(もしくはllama.cpp)という実行ファイルをGDScriptからサブプロセスとして呼び出して、Godot側で実行結果を受け取るという話でした。
また、以下の記事のようにllama.cppをGodotのC++ Extensionsによってビルドすれば、LLMをGodotで使うことができます。
しかし、GodotでC++をビルドするのはかなり面倒で、手軽に開発できるものとは言い難いのが現状です。
そこで、前回話したGodotでC#を使う話を持ってきたいと思います。
この話を元に、今回の記事ではllama.cppのC#ラッパーであるLLamaSharpを使ってGodotでLLMを使ってみたいと思います。
環境構築
プロジェクト作成
GodotでC#を動かす話は、前回の記事を見てください。単純に言うと、C#対応版のgodotと.NET SDKを入れるだけです。
そしたら始めに、C#版のGodotでプロジェクトを作ります。
まず、適当なノード(シーン)を作って、C#スクリプトを新規アタッチします。すると、プロジェクトフォルダに{project name}.csproj
というファイルを含む、C#の環境が作られます。
この.csprojファイルがC#の外部パッケージを設定しているので、生成できていることを確認しましょう。
Directory: E:\godot_projects\llama_sharp_test
Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 2024/06/18 14:18 .godot
d---- 2024/06/17 14:00 llm_models
-a--- 2024/06/17 13:47 80 .gitattributes
-a--- 2024/06/17 13:47 36 .gitignore
-a--- 2024/06/18 13:10 1475 cha61EC.tmp
-a--- 2024/06/18 14:18 736 chat_view.gd
-a--- 2024/06/18 14:20 1192 chat_view.tscn
-a--- 2024/06/17 13:47 949 icon.svg
-a--- 2024/06/17 13:48 842 icon.svg.import
-a--- 2024/06/17 13:59 553 llama_sharp_test.csproj
-a--- 2024/06/17 13:48 1067 llama_sharp_test.sln
-a--- 2024/06/18 14:18 1880 LLM_Server.cs
-a--- 2024/06/18 14:20 198 LLM_Server.tscn
-a--- 2024/06/18 13:45 603 project.godot
lsコマンドでファイルを確認すると、llama_sharp_test.csprojがあります。
パッケージのインストール
次に、NuGetをつかって外部パッケージをプロジェクトにインストールします。NuGetはpythonのpipみたいな、パッケージ管理ツールです。
(おそらく)Windowsの.NET SDKを入れると、dotnetコマンドが使えるようになっています。これにはNuGetが内包されています。
プロジェクトフォルダでターミナルを開き、次のコマンドでLLamaSharpを入れます。
dotnet add package LLamaSharp
また、バックエンドを指定しないと怒られるので、バックエンドもdonetで入れます。
dotnet add package LLamaSharp.Backend.Cpu
2つのパッケージを入れると、.csprojの内容に<ItemGroup>タグで追記されていることを確認します。
<Project Sdk="Godot.NET.Sdk/4.2.2">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'android' ">net7.0</TargetFramework>
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'ios' ">net8.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LLamaSharp" Version="0.13.0" />
<PackageReference Include="LLamaSharp.Backend.Cpu" Version="0.13.0" />
</ItemGroup>
</Project>
コマンド一つでllama.cppを使えるようになるところが、C++との大きな違いですね。
モデルのダウンロード
modelsというディレクトリをプロジェクトフォルダに作り、モデルをダウンロードします。
モデルはllama.cppで動かせるguff形式であれば、どれでもいいと思います。
今回のモデルはPhi-3-miniを選びました。
![](https://cdn-thumbnails.huggingface.co/social-thumbnails/models/microsoft/Phi-3-mini-4k-instruct-gguf.png)
これを、modelsの下に入れておきます。
ソースコード
C#のソースコードでLLamaSharpを動かし、それをGDScriptから利用したいと思います。
C#のノード
まずは、C#で書かれたコードをアタッチしたノードを作ります。名前はLLM_Serverとしました。
型は何でもいいのですが、とりあえずNodeにして、次のコードをアタッチします。
using Godot;
using System;
using System.Threading;
using System.Collections.Generic;
using LLama.Common;
using LLama;
public partial class LLM_Server : Node
{
public string llm_model_path = "llm_models/Phi-3-mini-4k-instruct-q4.gguf";
public InstructExecutor executor;
public string input_text = "";
[Signal]
public delegate void generateStartEventHandler();
[Signal]
public delegate void generatingEventHandler(string text);
[Signal]
public delegate void generateEndEventHandler();
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
generateStart += generate;
}
public void set_model(){
var parameters = new ModelParams(llm_model_path){
ContextSize = 1024, // The longest length of chat as memory.
GpuLayerCount = 0 // How many layers to offload to GPU. Please adjust it according to your GPU memory.
};
var model = LLamaWeights.LoadFromFile(parameters);
var context = model.CreateContext(parameters);
executor = new InstructExecutor(context);
}
public void generate(){
set_model();
Thread thread = new Thread(new ThreadStart(run_llm));
thread.Start();
}
public async void run_llm(){
string output_text = "";
InferenceParams inferenceParams = new InferenceParams()
{
MaxTokens = 256, // No more than 256 tokens should appear in answer. Remove it if antiprompt is enough for control.
AntiPrompts = new List<string> { "<|end|>", "<|user|>", "<assistant>", "\n"}
};
string prompt = "<|system|>\nあなたは優秀なAIアシスタントです<|end|>\n<|user|>\n"+this.input_text+"<|end|>\n<|assistant|>\n";
await foreach (var text in executor.InferAsync(prompt, inferenceParams))
{
if (text != ""){
output_text += text;
CallDeferred("emit_signal", SignalName.generating, output_text);
}
}
CallDeferred("emit_signal", SignalName.generateEnd);
}
}
少し解説します。まず、3つのシグナルを持っていますGodot for C#のシグナルは定義が少し分かりづらいですが、EventHandlerというのを使います。+=でシグナルを関数にセットするのが直感と反します
- generateStart : 生成の開始を知らせるシグナル。このシグナルが着たらgenerate()が呼ばれる
- generating : 新しい文字(トークン)を生成したら、生成中の文字列を知らせるシグナル。これをGDScript側で検知して、逐次表示させる
- generateEnd : 生成が終了したことを知らせるシグナル
また各メソッドは次のような機能を持っています
- set_model() : モデルに与えるパラメータなどを設定する
- generate() : シグナルが着たら生成開始スレッドを立てて開始する。スレッドを分けないと、生成中にGodotがフリーズする。
- run_llm() : プロンプトを作成してLLMに与える。生成中はgeneratingシグナルを発し、生成が終了したらgenerateEndシグナルを発する。
かなり簡単にLLMを実行するコードが書けたと思います。また、Threadを立ててLLMを実行しないと、Godotがフリーズするので注意してください。
GDScriptのノード
Godotに表示するUIを作っていきます。まず、新規シーンを作成します。型はNode2D、名前はChatViewにしました。
次のようなTreeのシーンにしました。
![](https://emoclework.jp/wp-content/uploads/2024/06/image-25.png)
また、画面上は次のような感じです。
![](https://emoclework.jp/wp-content/uploads/2024/06/image-26.png)
次のスクリプトをChatViewにアタッチします(Buttonのpressedシグナルを接続しました)。
extends Node2D
var server_class := preload("res://LLM_Server.tscn")
var server
func _ready():
server = server_class.instantiate()
server.generating.connect(_on_llm_generate_label_generating)
server.generateEnd.connect(_on_llm_generate_label_generate_end)
add_child(server)
func _on_button_pressed():
var input_text :String = $Panel/VBoxContainer/TextEdit.text
$Panel/VBoxContainer/ScrollContainer/Label.text = ""
server.input_text = input_text
server.emit_signal('generateStart')
$Panel/VBoxContainer/Button.disabled = true
func _on_llm_generate_label_generating(text):
$Panel/VBoxContainer/ScrollContainer/Label.text = text
func _on_llm_generate_label_generate_end():
pass
$Panel/VBoxContainer/Button.disabled = false
先ほど作ったC#のノード(LLM_Server)をpreloadして、readにて生成します。おそらく、事前読み込みに入れても大丈夫だと思います。
あとは、ボタンが押されたらserverのgenerateStartシグナルをオンにするだけです。すると、LLMが動き出し、Labelに文章が生成されます。
実行結果
![](https://emoclework.jp/wp-content/uploads/2024/06/30613b0307ca9fd6428db8133a1eef78.gif)
CPU実行だと遅いですが、逐次表示させることでゲーム的には待てる速度になりました。
![](https://emoclework.jp/wp-content/uploads/2024/06/5c0fa4b2b9693b642aa6ee918e6fb9c0.gif)
phi-3-miniが日本語の出力がちょっと弱いので(量子化した影響もあります)、語彙を間違えていますが、ちゃんと返答できています。
おわりに
今回は、GodotでLLMを動かす話をしました。C#のLLamaSharpを使うことで簡単にGodotでもLLMの事項ができました。また、Threadとsignalを使うことで返答の逐次表示にも成功し、現実的にLLMでゲームを作ることが可能であることが示せました。
今度はC#とONNX Runtimeを使って、他のNeuralNetworkのモデルも動かしてみたいと思います。
追記
ONNX Runtimeを使って実行する記事を書きました。