Compartilhar via


Introdução ao Phi3 e a outros modelos de linguagem no aplicativo do Windows com o ONNX Runtime Generative AI

Este artigo orienta a criação de um aplicativo do WinUI 3 que usa um modelo Phi3 e a biblioteca do ONNX Runtime Generative AI para implementar um aplicativo simples de conversa usando IA generativa. Os modelos de linguagem grande (LLMs) permitem adicionar recursos de geração, transformação, raciocínio e tradução de texto ao seu aplicativo. Para obter mais informações sobre como usar modelos de IA e machine learning no aplicativo do Windows, confira Get started using AI and Machine Learning models in your Windows app. Para obter mais informações sobre o runtime e a IA generativa do ONNX, consulte Generative AI with ONNX Runtime.

O que é o ONNX Runtime

O ONNX Runtime é um acelerador de modelo de machine learning multiplataforma, com uma interface flexível para integrar bibliotecas específicas de hardware. O ONNX Runtime pode ser usado com modelos de PyTorch, Tensorflow/Keras, TFLite scikit-learn e outras estruturas. Para obter mais informações, confira o site do ONNX Runtime e https://onnxruntime.ai/docs/.

Pré-requisitos

  • O dispositivo precisa ter o modo de desenvolvedor habilitado. Para obter mais informações, confira Habilitar o dispositivo para desenvolvimento.
  • Visual Studio 2022 ou posterior com a carga de trabalho de desenvolvimento do .NET para área de trabalho.

Criar um aplicativo do WinUI em C#

No Visual Studio, crie um novo projeto. Na caixa de diálogo Criar um projeto, defina o filtro de linguagem como "C#" e o filtro do tipo de projeto como "winui"; em seguida, selecione o modelo Aplicativo em branco, Empacotado (WinUI3 na Área de Trabalho). Nomeie o novo projeto como "GenAIExample".

Adicionar referências ao pacote NuGet ONNX Runtime Generative AI

No Gerenciador de Soluções, clique com o botão direito do mouse em Dependências e selecione Gerenciar pacotes NuGet.... No gerenciador de pacotes NuGet, selecione a guia Navegar. Procure por "Microsoft.ML.OnnxRuntimeGenAI.DirectML", selecione a versão estável mais recente na lista suspensa Versão e clique em Instalar.

Adicionar um arquivo de modelo e vocabulário ao projeto

No Gerenciador de Soluções, clique com o botão direito do mouse no projeto e selecione Adicionar->Nova Pasta. Nomeie a nova pasta como "Models". Neste exemplo, usaremos o modelo de https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-onnx/tree/main/directml/directml-int4-awq-block-128.

Existem várias maneiras diferentes de recuperar modelos. Para este passo a passo, usaremos a interface de linha de comando (CLI) do Hugging Face. Caso obtenha os modelos usando outro método, talvez seja necessário ajustar os caminhos do arquivo ao modelo no código de exemplo. Para obter informações sobre como instalar a CLI do Hugging Face e configurar sua conta para usá-la, consulte Interface de linha de comando (CLI).

Depois de instalar a CLI, abra um terminal, navegue até o diretório Models criado e digite o comando a seguir.

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

Quando a operação estiver concluída, verifique se o seguinte arquivo existe: [Project Directory]\Models\directml\directml-int4-awq-block-128\model.onnx.

No Gerenciador de Soluções, expanda a pasta "directml-int4-awq-block-128" e selecione todos os arquivos na pasta. No painel Propriedades do Arquivo, defina Copiar para o Diretório de Saída como "Copiar se for mais recente".

Adicionar uma interface do usuário simples para interagir com o modelo

Neste exemplo, criaremos uma interface do usuário muito simples que tem um TextBox para especificar uma solicitação, um Botão para enviar a solicitação e um TextBlock para exibir mensagens de status e as respostas do modelo. Substitua o elemento StackPanel padrão em MainWindow.xaml pelo XAML a seguir.

<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>

Inicializar o modelo

Em MainWindow.xaml.cs, adicione uma diretiva using para o namespace Microsoft.ML.OnnxRuntimeGenAI.

using Microsoft.ML.OnnxRuntimeGenAI;

Declare variáveis de membro dentro da definição de classe MainPage para o Modelo e o Tokenizer. Defina a localização dos arquivos de modelo que adicionamos nas etapas anteriores.

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

Crie um método auxiliar para inicializar o modelo de forma assíncrona. Esse método chama o construtor da classe do Modelo, passando o caminho para o diretório do modelo. Em seguida, ele cria um Tokenizer usando o modelo.

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

Neste exemplo, carregaremos o modelo quando a janela principal for ativada. Atualize o construtor da página para registrar um manipulador do evento Ativado.

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

O evento Ativado pode ser gerado várias vezes, portanto, no manipulador de eventos, verifique se o modelo é nulo antes de inicializá-lo.

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

Enviar a solicitação ao modelo

Crie um método auxiliar que envia a solicitação ao modelo e retorna de forma assíncrona os resultados para o chamador com um IAsyncEnumerable.

Nesse método, a classe do Gerador é usada em um loop, chamando GenerateNextToken em cada passagem para recuperar o que o modelo prevê no que os próximos caracteres, chamados de token, devem ser baseados na solicitação de entrada. O loop é executado até que o método IsDone do gerador retorne verdadeiro ou até que qualquer um dos tokens "<|end|>", "<|system|>" ou "<|user|>" sejam recebidos, sinalizando que podemos parar de gerar tokens.

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

Adicionar código de interface do usuário para enviar a solicitação e exibir os resultados

No manipulador de clique no Botão, primeiro verifique se o modelo não é nulo. Crie uma sequência de solicitação com o sistema e a solicitação do usuário e chame InferStreaming, atualizando o TextBlock com cada parte da resposta.

O modelo usado neste exemplo foi treinado para aceitar solicitações no formato a seguir, no qual systemPrompt são as instruções de como o modelo deve se comportar e userPrompt é a pergunta do usuário.

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

Os modelos devem documentar suas convenções de solicitação. Para este modelo, o formato está documentado no cartão do modelo 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;
        }
    }
}

Executar o exemplo

No Visual Studio, na lista suspensa Plataformas de Solução, verifique se o processador de destino está definido como x64. A biblioteca de IA generativa ONNXRuntime não é compatíevl com x86. Compile e execute o projeto. Aguarde até o TextBlock indicar que o modelo foi carregado. Digite uma solicitação na caixa de texto da solicitação e clique no botão de envio. Você deve ver os resultados preenchendo gradualmente o bloco de texto.

Confira também