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

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

什么是 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 中的默认 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;

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

另请参阅