ONNX Runtime Generative AI を使用する Windows アプリで Phi3 やその他の言語モデルの使用を開始する

この記事では、Phi3 モデルと ONNX Runtime Generative AI ライブラリを使用して単純な生成 AI チャット アプリを実装する WinUI 3 アプリを作成する手順について説明します。 大規模言語モデル (LLM) を使用すると、テキスト生成、変換、推論、翻訳の機能をアプリに追加できます。 Windows アプリで AI および機械学習モデルを使用する方法の詳細については、「Get started using AI and Machine Learning models in your Windows app (Windows アプリで AI および機械学習モデルの使用を開始する)」を参照してください。 ONNX Runtime と生成 AI の詳細については、「ONNX Runtime を使用した生成 AI」を参照してください。

ONNX Runtime とは

ONNX Runtime は、ハードウェア固有のライブラリを統合するための柔軟なインターフェイスを備えたクロスプラットフォームの機械学習モデル アクセラレータです。 ONNX Runtime は、PyTorch、Tensorflow/Keras、TFLite、scikit-learn、およびその他のフレームワークのモデルで使用できます。 詳細については、「https://onnxruntime.ai/docs/ での ONNX Runtime Web サイトを参照してください。

前提条件

  • デバイスで開発者モードが有効になっている必要があります。 詳しくは、「デバイスを開発用に有効にする」をご覧ください。
  • .NET デスクトップ開発ワークロードを利用する Visual Studio 2022 以降

新しい C# WinUI アプリを作成する

Visual Studio で、新しいプロジェクトを作成します。 [新しいプロジェクトの作成] ダイアログで、言語フィルターを "C#" に設定し、プロジェクト タイプ フィルターを "winui" に設定してから、[Blank app, Packaged (WinUI3 in Desktop)] (空のアプリ、パッケージ (デスクトップの WinUI 3)) テンプレートを選択します。 新しいプロジェクトに "GenAIExample" という名前を付けます。

ONNX Runtime Generative AI NuGet パッケージへの参照を追加する

ソリューション エクスプローラーで、[依存関係] を右クリックし、[NuGet パッケージの管理] を選択します。NuGet パッケージ マネージャーで、[参照] タブを選択します。"Microsoft.ML.OnnxRuntimeGenAI.DirectML" を検索し、[バージョン] ドロップダウンで最新の安定バージョンを選択して、[インストール] をクリックします。

モデルとボキャブラリ ファイルをプロジェクトに追加する

ソリューション エクスプローラーで、プロジェクトを右クリックして [追加]>[新しいフォルダー] を選択します。 新しいフォルダーに "Models" という名前を付けます。 この例では、https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-onnx/tree/main/directml/directml-int4-awq-block-128 のモデルを使用します。

モデルを取得する方法は複数あります。 このチュートリアルでは、Hugging Face コマンド ライン インターフェイス (CLI) を使用します。 別の方法を使用してモデルを取得する場合は、サンプル コードのモデルへのファイル パスを調整する必要がある場合があります。 Hugging Face CLI をインストールして使用するためにアカウントを設定する方法については、コマンド ライン インターフェイス (CLI) を参照してください。

CLI をインストールした後、ターミナルを開き、作成した Models ディレクトリに移動し、次のコマンドを入力します。

huggingface-cli download microsoft/Phi-3-mini-4k-instruct-onnx --include directml/* --local-dir .

操作が完了したら、次のファイルが存在することを確認します: [Project Directory]\Models\directml\directml-int4-awq-block-128\model.onnx

ソリューション エクスプローラーで、"directml-int4-awq-block-128" フォルダーを展開し、フォルダー内のファイルをすべて選択します。 [ファイルのプロパティ] ウィンドウの [出力ディレクトリにコピー] を [新しい場合はコピーする] に設定します。

モデルと対話するためのシンプルな UI を追加する

この例では、とてもシンプルな UI を作成します。これには、プロンプトを指定するための TextBox、プロンプトを送信するための Button、ステータス メッセージとモデルからの応答を表示するための TextBlock が含まれます。 MainWindow.xaml 内の既定の StackPanel 要素を次の XAML に置き換えます。

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Column ="0">
        <TextBox x:Name="promptTextBox" Text="Compose a haiku about coding."/>
        <Button x:Name="myButton" Click="myButton_Click">Submit prompt</Button>
    </StackPanel>
    <Border Grid.Column="1" Margin="20">
        <TextBlock x:Name="responseTextBlock" TextWrapping="WrapWholeWords"/>
    </Border>
</Grid>

モデルを初期化する

MainWindow.xaml.cs で、Microsoft.ML.OnnxRuntimeGenAI 名前空間の using ディレクティブを追加します。

using Microsoft.ML.OnnxRuntimeGenAI;

Model および TokenizerMainPage クラス定義内でメンバー変数を宣言します。 前の手順で追加したモデル ファイルの場所を設定します。

private Model? model = null;
private Tokenizer? tokenizer = null;
private readonly string ModelDir = 
    Path.Combine(AppDomain.CurrentDomain.BaseDirectory,
        @"Models\directml\directml-int4-awq-block-128");

モデルを非同期的に初期化するヘルパー メソッドを作成します。 このメソッドは Model クラスのコンストラクターを呼び出し、モデル ディレクトリへのパスを渡します。 次に、モデルから新しい Tokenizer を作成します。

public Task InitializeModelAsync()
{

    DispatcherQueue.TryEnqueue(() =>
    {
        responseTextBlock.Text = "Loading model...";
    });

    return Task.Run(() =>
    {
        var sw = Stopwatch.StartNew();
        model = new Model(ModelDir);
        tokenizer = new Tokenizer(model);
        sw.Stop();
        DispatcherQueue.TryEnqueue(() =>
        {
            responseTextBlock.Text = $"Model loading took {sw.ElapsedMilliseconds} ms";
        });
    });
}

この例では、メイン ウィンドウがアクティブになったときにモデルを読み込みます。 ページ コンストラクターを更新して、Activated イベントのハンドラーを登録します。

public MainWindow()
{
    this.InitializeComponent();
    this.Activated += MainWindow_Activated;
}

Activated イベントは複数回発生させることができます。そのため、イベント ハンドラーでは、モデルを初期化する前に、モデルが null であることを確認します。

private async void MainWindow_Activated(object sender, WindowActivatedEventArgs args)
{
    if (model == null)
    {
        await InitializeModelAsync();
    }
}

プロンプトをモデルに送信する

プロンプトをモデルに送信し、IAsyncEnumerable を使用して結果を呼び出し元に非同期的に返すヘルパー メソッドを作成します。

このメソッドでは、Generator クラスをループで使用し、各パスで GenerateNextToken を呼び出して、モデルが入力プロンプトに基づいて次の数文字を予測した内容 (トークンと呼ばれる) を取得します。 このループは、ジェネレーター IsDone メソッドが true を返すか、トークン "<|end|>"、"<|system|>"、または "<|user|>" が受信され、トークンの生成を停止できることが通知されるまで実行されます。

public async IAsyncEnumerable<string> InferStreaming(string prompt)
{
    if (model == null || tokenizer == null)
    {
        throw new InvalidOperationException("Model is not ready");
    }

    var generatorParams = new GeneratorParams(model);

    var sequences = tokenizer.Encode(prompt);

    generatorParams.SetSearchOption("max_length", 2048);
    generatorParams.SetInputSequences(sequences);
    generatorParams.TryGraphCaptureWithMaxBatchSize(1);

    using var tokenizerStream = tokenizer.CreateStream();
    using var generator = new Generator(model, generatorParams);
    StringBuilder stringBuilder = new();
    while (!generator.IsDone())
    {
        string part;
        try
        {
            await Task.Delay(10).ConfigureAwait(false);
            generator.ComputeLogits();
            generator.GenerateNextToken();
            part = tokenizerStream.Decode(generator.GetSequence(0)[^1]);
            stringBuilder.Append(part);
            if (stringBuilder.ToString().Contains("<|end|>")
                || stringBuilder.ToString().Contains("<|user|>")
                || stringBuilder.ToString().Contains("<|system|>"))
            {
                break;
            }
        }
        catch (Exception ex)
        {
            Debug.WriteLine(ex);
            break;
        }

        yield return part;
    }
}

プロンプトを送信して結果を表示する UI コードを追加する

Button クリック ハンドラーで、モデルが null ではないことを最初に確認します。 システムおよびユーザー プロンプトを使用してプロンプト文字列を作成し、InferStreaming を呼び出して、応答の各部分で TextBlock を更新します。

この例で使用されるモデルは、次の形式でプロンプトを受け入れるようにトレーニングされています。systemPrompt はモデルの動作に関する指示であり、userPrompt はユーザーからの質問です。

<|system|>{systemPrompt}<|end|><|user|>{userPrompt}<|end|><|assistant|>

モデルでは、プロンプト規則をドキュメント化する必要があります。 このモデルの形式は、Huggingface モデル カードで説明されています。

private async void myButton_Click(object sender, RoutedEventArgs e)
{
    responseTextBlock.Text = "";

    if(model != null)
    {
        var systemPrompt = "You are a helpful assistant.";
        var userPrompt = promptTextBox.Text;

        var prompt = $@"<|system|>{systemPrompt}<|end|><|user|>{userPrompt}<|end|><|assistant|>";
        
        await foreach (var part in InferStreaming(prompt))
        {
            responseTextBlock.Text += part;
        }
    }
}

例を実行する

Visual Studio の [ソリューション プラットフォーム] ドロップダウンで、ターゲット プロセッサが x64 に設定されていることを確認します。 ONNXRuntime 生成 AI ライブラリは x86 をサポートしていません。 プロジェクトをビルドして実行します。 TextBlock がモデルが読み込まれたことを示すまで待ちます。 プロンプト テキスト ボックスにプロンプトを入力し、送信ボタンをクリックします。 結果がテキスト ブロックに徐々に入力されます。

関連項目