Introduzione a Phi3 e ad altri modelli linguistici nell'app di Windows con ONNX Runtime Generative AI

Questo articolo illustra come creare un'app WinUI che usa un modello Phi3 e la ONNX Runtime Generative AI libreria per implementare una semplice app di chat di intelligenza artificiale generativa. I modelli linguistici di grandi dimensioni consentono di aggiungere funzionalità di generazione, trasformazione, ragionamento e traduzione di testo all'app. Per altre informazioni sull'uso di modelli di intelligenza artificiale e Machine Learning nell'app windows, vedere Introduzione all'intelligenza artificiale in Windows. Per altre informazioni sul runtime ONNX e sull'intelligenza artificiale generativa, vedere Generative AI with ONNX Runtime.

Quando si usano le funzionalità di intelligenza artificiale, è consigliabile esaminare: Sviluppo di applicazioni e funzionalità di intelligenza artificiale responsabile in Windows.

Che cos'è il ONNX Runtime

ONNX Runtime è un acceleratore di modelli di Machine Learning multipiattaforma, con un'interfaccia flessibile per integrare librerie specifiche dell'hardware. ONNX Runtime può essere usato con i modelli di PyTorch, Tensorflow/Keras, TFLite, scikit-learne altri framework. Per altre informazioni, vedere il sito Web ONNX Runtime all'indirizzo https://onnxruntime.ai/docs/.

Prerequisiti

  • Il dispositivo deve avere la modalità sviluppatore abilitata. Per altre informazioni, vedere Abilitare il dispositivo per lo sviluppo.
  • Visual Studio 2022 o versione successiva con il carico di lavoro per lo sviluppo di applicazioni desktop .NET.

Creare una nuova app WinUI C#

In Visual Studio creare un nuovo progetto. Nella finestra di dialogo Crea un nuovo progetto impostare il filtro del linguaggio su "C#" e il filtro del tipo di progetto su "winui", quindi selezionare il modello App vuota, In pacchetto (WinUI3 in Desktop). Denominare il nuovo progetto "GenAIExample".

Aggiungere riferimenti al pacchetto NuGet ONNX Runtime Generative AI

In Esplora soluzioni fare clic con il pulsante destro del mouse su Dipendenze e scegliere Gestisci pacchetti NuGet. Nella gestione pacchetti NuGet selezionare la scheda Sfoglia . Cercare "Microsoft.ML.OnnxRuntimeGenAI.DirectML", selezionare la versione stabile più recente nell'elenco a discesa Versione e quindi fare clic su Installa.

Aggiungere un modello e un file di vocabolario al progetto

In Esplora soluzioni fare clic con il pulsante destro del mouse sul progetto e scegliere Aggiungi nuova> cartella. Assegnare alla nuova cartella il nome "Models". Per questo esempio verrà usato il modello da https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-onnx/tree/main/directml/directml-int4-awq-block-128.

Esistono diversi modi per recuperare i modelli. Per questa procedura dettagliata, utilizzeremo l'interfaccia Hugging Face per la riga di comando (CLI). Se si ottengono i modelli usando un altro metodo, potrebbe essere necessario modificare i percorsi di file per il modello nel codice di esempio. Per informazioni sull'installazione dell'interfaccia della riga di comando di Hugging Face e sulla configurazione dell'account per usarlo, vedere Interfaccia della riga di comando.For information on installing the Hugging Face CLI and setting up your account to use it, see Command Line Interface (CLI).

Dopo aver installato la CLI, apri un terminale, naviga nella directory Models che hai creato e digita il comando seguente.

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

Al termine dell'operazione, verificare che il file seguente esista: [Project Directory]\Models\directml\directml-int4-awq-block-128\model.onnx.

In Esplora soluzioni espandere la cartella "directml-int4-awq-block-128" e selezionare tutti i file nella cartella. Nel riquadro Proprietà file impostare Copia nella directory di output su "Copia se più recente".

Aggiungere una semplice interfaccia utente per interagire con il modello

Per questo esempio verrà creata un'interfaccia utente molto semplicistica con un TextBox per specificare un prompt, un Button per l'invio della richiesta e un TextBlock per visualizzare i messaggi di stato e le risposte del modello. Sostituire l'elemento StackPanel predefinito in MainWindow.xaml con il codice XAML seguente.

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

Inizializzare il modello

In MainWindow.xaml.cs aggiungere una direttiva using per lo spazio dei nomi Microsoft.ML.OnnxRuntimeGenAI.

using Microsoft.ML.OnnxRuntimeGenAI;

Dichiarare le variabili membro all'interno della definizione di classe MainPage per la modello di e l'Tokenizer. Imposta il percorso per i file del modello aggiunti nei passaggi precedenti.

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

Creare un metodo helper per inizializzare in modo asincrono il modello. Questo metodo chiama il costruttore per la classe modello , passando il percorso alla directory del modello. Crea quindi un nuovo tokenizzatore dal modello.

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

Per questo esempio, il modello verrà caricato quando viene attivata la finestra principale. Aggiornare il costruttore di pagina per registrare un gestore per l'evento Activated .

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

L'evento Activated può essere generato più volte, quindi nel gestore eventi verificare che il modello sia null prima di inizializzarlo.

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

Inviare il prompt al modello

Creare un metodo di supporto che invii il prompt al modello e quindi restituisca i risultati in modo asincrono al chiamante con un oggetto IAsyncEnumerable.

In questo metodo, la classe Generator viene utilizzata in un ciclo, chiamando GenerateNextToken in ogni passaggio per recuperare ciò che il modello prevede che i successivi caratteri, denominati token, debbano essere sulla base del prompt di input. Il ciclo viene eseguito fino a quando il metodo IsDone del generatore restituisce true o fino a quando non vengono ricevuti i token "<|end|>", "<|system|>" o "<|user|>", che segnala che è possibile interrompere la generazione di token.

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

Aggiungere il codice dell'interfaccia utente per inviare la richiesta e visualizzare i risultati

Nel gestore di clic pulsante verificare innanzitutto che il modello non sia Null. Crea una stringa di richiesta con il prompt del sistema e dell'utente e chiama InferStreaming, aggiornando il TextBlock con ogni parte della risposta.

Il modello usato in questo esempio è stato sottoposto a training per accettare richieste nel formato seguente, dove systemPrompt è le istruzioni per il comportamento del modello e userPrompt è la domanda dell'utente.

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

I modelli devono documentare le convenzioni di prompt. Per questo modello il formato è documentato sulla scheda del modello di Hugging Face .

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

Eseguire l'esempio

In Visual Studio, nell'elenco a discesa delle Piattaforme della soluzione verificare che il processore di destinazione sia impostato su x64. La libreria ONNXRuntime Generative per intelligenza artificiale non supporta x86. Compilare ed eseguire il progetto. Attendere che TextBlock indichi che il modello è stato caricato. Digitare un prompt nella casella di testo del prompt e fare clic sul pulsante Invia. Dovresti vedere i risultati riempire gradualmente il blocco di testo.

Vedere anche