GodotでもLLMを使いたい(C#とLLamaSharp編)

はじめに

今回の成果物を最初に載せておきます。

さて、以前からGodotでLLMを実行する話を書いています。

この話は、llamafile(もしくはllama.cpp)という実行ファイルをGDScriptからサブプロセスとして呼び出して、Godot側で実行結果を受け取るという話でした。

また、以下の記事のようにllama.cppをGodotのC++ Extensionsによってビルドすれば、LLMをGodotで使うことができます。

GitHub - opyate/godot-llm-experiment: Getting an LLM to work with Godot.
Getting an LLM to work with Godot. . Contribute to opyate/godot-llm-experiment development by creating an account on Git...

しかし、GodotでC++をビルドするのはかなり面倒で、手軽に開発できるものとは言い難いのが現状です。

そこで、前回話したGodotでC#を使う話を持ってきたいと思います。

この話を元に、今回の記事ではllama.cppのC#ラッパーであるLLamaSharpを使ってGodotでLLMを使ってみたいと思います。

GitHub - SciSharp/LLamaSharp: A C#/.NET library to run LLM (🦙LLaMA/LLaVA) on your local device efficiently.
A C#/.NET library to run LLM (🦙LLaMA/LLaVA) on your local device efficiently. - SciSharp/LLamaSharp

環境構築

プロジェクト作成

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を選びました。

microsoft/Phi-3-mini-4k-instruct-gguf · Hugging Face
We’re on a journey to advance and democratize artificial intelligence through open source and open science.

これを、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のシーンにしました。

また、画面上は次のような感じです。

次のスクリプトを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に文章が生成されます。

実行結果

CPU実行だと遅いですが、逐次表示させることでゲーム的には待てる速度になりました。

phi-3-miniが日本語の出力がちょっと弱いので(量子化した影響もあります)、語彙を間違えていますが、ちゃんと返答できています。

おわりに

今回は、GodotでLLMを動かす話をしました。C#のLLamaSharpを使うことで簡単にGodotでもLLMの事項ができました。また、Threadとsignalを使うことで返答の逐次表示にも成功し、現実的にLLMでゲームを作ることが可能であることが示せました。

今度はC#とONNX Runtimeを使って、他のNeuralNetworkのモデルも動かしてみたいと思います。

追記

ONNX Runtimeを使って実行する記事を書きました。

タイトルとURLをコピーしました