在 Windows 应用中,通过 ONNX Runtime Generative AI 开始使用 Phi3 和其他语言模型。

本文将指导你创建使用 Phi3 模型和 ONNX Runtime Generative AI 库实现简单生成 AI 聊天应用的 WinUI 应用。 使用大型语言模型 (LLM),可以为应用添加文本生成、转换、推理和翻译功能。 有关在 Windows 应用中使用 AI 和机器学习模型的详细信息,请参阅 Windows上的 AI 入门。 有关 ONNX 运行时和生成式 AI 的更多信息,请参阅生成式 AI 和 ONNX Runtime

使用 AI 功能时,建议查看:在 Windows上开发负责任的生成 AI 应用程序和功能。

什么是 ONNX Runtime

ONNX Runtime 运行时是一个跨平台的机器学习模型加速器,可通过灵活的接口集成特定于硬件的库。 ONNX Runtime 可以与 PyTorch、Tensorflow/Keras、TFLite、scikit-learn 和其他框架中的模型配合使用。 有关详细信息,请参阅 ONNX Runtime 网站 https://onnxruntime.ai/docs/

先决条件

  • 设备必须启用开发人员模式。 有关详细信息,请参阅启用用于开发的设备
  • 具有 .NET 桌面开发工作负载的 Visual Studio 2022 或更高版本。

创建新的 C# WinUI 应用

在 Visual Studio 中,创建新的项目。 在“创建新项目”对话框中,将语言筛选器设置为“C#”,将项目类型筛选器设置为“winui”,然后选择“打包的空白应用(桌面版 WinUI3)”模板。 将新项目命名为“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)

安装 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,其中包含用于指定提示的文本框、用于提交提示的按钮、用于显示状态消息的文本块,以及来自模型的响应。 将 中的默认 MainWindow.xaml 元素替换为如下 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;

对于 ModelTokenizer,在 MainPage 类定义内声明成员变量。 设置前面的步骤中添加的模型文件的位置。

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";
        });
    });
}

对于此示例,我们将在主窗口处于激活状态时加载模型。 更新页面构造函数,以注册用于“已激活”的事件的处理程序。

public MainWindow()
{
    this.InitializeComponent();
    this.Activated += MainWindow_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 代码,以提交提示并显示结果

在“按钮”中,单击处理程序,首先验证模型不为 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。 生成并运行该项目。 等待 文本块 指示模型已加载。 在提示文本框中键入提示,然后单击“提交”按钮。 你应看到结果逐渐填充文本块。

另请参阅