Inferenz Stable Diffusion mit C# und ONNX Runtime

In diesem Tutorial lernen wir, wie man die beliebte Stable Diffusion Deep Learning Modell in C# inferenziert. Stable Diffusion Modelle nehmen einen Text-Prompt und erstellen ein Bild, das den Text repräsentiert. Siehe das Beispiel unten

"make a picture of green tree with flowers around it and a red sky" 
Image of browser inferencing on sample images. Image of browser inferencing on sample images.

Inhalt

Voraussetzungen

Dieses Tutorial kann lokal oder in der Cloud ausgeführt werden, indem Azure Machine Learning Compute genutzt wird.

Für die lokale Ausführung

Für die Ausführung in der Cloud mit Azure Machine Learning

Stable Diffusion Modelle mit Hugging Face herunterladen

Die Hugging Face-Website verfügt über eine großartige Bibliothek mit Open-Source-Modellen. Wir werden die ONNX Stable Diffusion Modelle von Hugging Face nutzen und herunterladen.

Sobald Sie ein Modellversion-Repository ausgewählt haben, klicken Sie auf Files and Versions und wählen Sie dann den ONNX Branch. Wenn kein ONNX-Modell-Branch verfügbar ist, verwenden Sie den main Branch und konvertieren Sie ihn nach ONNX. Weitere Informationen finden Sie im ONNX-Konvertierungs-Tutorial für PyTorch.

  • Repository klonen
    git lfs install
    git clone https://huggingface.co/CompVis/stable-diffusion-v1-4 -b onnx
    
  • Kopieren Sie die Ordner mit den ONNX-Dateien in den C#-Projektordner \StableDiffusion\StableDiffusion. Die zu kopierenden Ordner sind: unet, vae_decoder, text_encoder, safety_checker.

Das Modell in Python mit Diffusers von Hugging Face verstehen

Beim Übernehmen eines vordefinierten Modells und dessen Operationalisierung ist es nützlich, einen Moment innezuhalten und die Modelle in dieser Pipeline zu verstehen. Dieser Code basiert auf der Hugging Face Diffusers Library und dem Blog. Wenn Sie mehr darüber erfahren möchten, wie es funktioniert, lesen Sie diesen großartigen Blogbeitrag für weitere Details!

Inferenz mit C#

Nun beginnen wir mit dem Breakdown, wie man in C# inferenziert! Das unet-Modell nimmt die Texteinbettung des Benutzer-Prompts entgegen, die vom CLIP-Modell erstellt wurde, das Text und Bild verbindet. Das latente verrauschte Bild wird als Startpunkt erstellt. Der Scheduler-Algorithmus und das unet-Modell arbeiten zusammen, um das Bild zu entrauschen und ein Bild zu erstellen, das den Text-Prompt repräsentiert. Sehen wir uns den Code an.

Hauptfunktion

Die Hauptfunktion setzt den Prompt, die Anzahl der Inferenzschritte und die Guidance-Skala. Anschließend wird die Funktion UNet.Inference aufgerufen, um die Inferenz auszuführen.

Die zu setzenden Eigenschaften sind:

  • prompt - Der Text-Prompt, der für das Bild verwendet werden soll
  • num_inference_steps - Die Anzahl der Schritte, für die die Inferenz ausgeführt werden soll. Je mehr Schritte, desto länger dauert die Inferenzschleife, aber die Bildqualität sollte sich verbessern.
  • guidance_scale - Die Skala für die klassifikatorfreie Führung. Je höher die Zahl, desto mehr wird versucht, dem Prompt zu ähneln, aber die Bildqualität kann leiden.
  • batch_size - Die Anzahl der zu erstellenden Bilder
  • height - Die Höhe des Bildes. Standard ist 512 und muss ein Vielfaches von 8 sein.
  • width - Die Breite des Bildes. Standard ist 512 und muss ein Vielfaches von 8 sein.

* HINWEIS: Schauen Sie sich das Hugging Face Blog für weitere Details an.

//Default args
var prompt = "make a picture of green tree with flowers around it and a red sky";
// Number of steps
var num_inference_steps = 10;

// Scale for classifier-free guidance
var guidance_scale = 7.5;
//num of images requested
var batch_size = 1;
// Load the tokenizer and text encoder to tokenize and encodethe text.
var textTokenized = TextProcessing.TokenizeText(prompt);
var textPromptEmbeddings = TextProcessing.TextEncode(textTokenized).ToArray();
// Create uncond_input of blank tokens
var uncondInputTokens = TextProcessing.CreateUncondInput();
var uncondEmbedding = TextProcessing.TextEncode(uncondInputTokens).ToArray();
// Concat textEmeddings and uncondEmbedding
DenseTensor<float> textEmbeddings = new DenseTensor<float>(ne[] { 2, 77, 768 });
for (var i = 0; i < textPromptEmbeddings.Length; i++)
{
    textEmbeddings[0, i / 768, i % 768] = uncondEmbedding[i];
    textEmbeddings[1, i / 768, i % 768] = textPromptEmbeddings[i];
}
var height = 512;
var width = 512;
// Inference Stable Diff
var image = UNet.Inference(num_inference_steps, textEmbeddings,guidance_scale, batch_size, height, width);
// If image failed or was unsafe it will return null.
if( image == null )
{
    Console.WriteLine("Unable to create image, please try again.");
}

Tokenisierung mit ONNX Runtime Extensions

Die Klasse TextProcessing enthält die Funktionen zur Tokenisierung des Text-Prompts und zur Kodierung mit dem CLIP-Modell Text-Encoder.

Anstatt den CLIP-Tokenizer in C# neu zu implementieren, können wir die plattformübergreifende CLIP-Tokenizer-Implementierung in ONNX Runtime Extensions nutzen. ONNX Runtime Extensions verfügt über eine custom_op_cliptok.onnx Datei als Tokenizer, die zur Tokenisierung des Text-Prompts verwendet wird. Der Tokenizer ist ein einfacher Tokenizer, der den Text in Wörter aufteilt und dann die Wörter in Tokens umwandelt.

  • Text-Prompt: Ein Satz oder eine Phrase, die das zu erstellende Bild repräsentiert.
    make a picture of green tree with flowers aroundit and a red sky
    
  • Text-Tokenisierung: Der Text-Prompt wird in eine Liste von Tokens tokenisiert. Jeder Token-ID ist eine Zahl, die ein Wort im Satz repräsentiert, dann wird er mit einem Leer-Token aufgefüllt, um die maxLength von 77 Tokens zu erreichen. Die Token-IDs werden dann in einen Tensor der Form (1,77) umgewandelt.

  • Unten sehen Sie den Code zur Tokenisierung des Text-Prompts mit ONNX Runtime Extensions.
public static int[] TokenizeText(string text)
{
            // Create Tokenizer and tokenize the sentence.
            var tokenizerOnnxPath = Directory.GetCurrentDirectory().ToString() + ("\\text_tokenizer\\custom_op_cliptok.onnx");

            // Create session options for custom op of extensions
            using var sessionOptions = new SessionOptions();
            var customOp = "ortextensions.dll";
            sessionOptions.RegisterCustomOpLibraryV2(customOp, out var libraryHandle);

            // Create an InferenceSession from the onnx clip tokenizer.
            using var tokenizeSession = new InferenceSession(tokenizerOnnxPath, sessionOptions);

            // Create input tensor from text
            using var inputTensor = OrtValue.CreateTensorWithEmptyStrings(OrtAllocator.DefaultInstance, new long[] { 1 });
            inputTensor.StringTensorSetElementAt(text.AsSpan(), 0);

            var inputs = new Dictionary<string, OrtValue>
            {
                {  "string_input", inputTensor }
            };

            // Run session and send the input data in to get inference output. 
            using var runOptions = new RunOptions();
            using var tokens = tokenizeSession.Run(runOptions, inputs, tokenizeSession.OutputNames);

            var inputIds = tokens[0].GetTensorDataAsSpan<long>();

            // Cast inputIds to Int32
            var InputIdsInt = new int[inputIds.Length];
            for(int i = 0; i < inputIds.Length; i++)
            {
                InputIdsInt[i] = (int)inputIds[i];
            }

            Console.WriteLine(String.Join(" ", InputIdsInt));

            var modelMaxLength = 77;
            // Pad array with 49407 until length is modelMaxLength
            if (InputIdsInt.Length < modelMaxLength)
            {
                var pad = Enumerable.Repeat(49407, 77 - InputIdsInt.Length).ToArray();
                InputIdsInt = InputIdsInt.Concat(pad).ToArray();
            }
            return InputIdsInt;
}

tensor([[49406,  1078,   320,  1674,   539,  1901,  2677,   593,  4023,  1630,
           585,   537,   320,   736,  2390, 49407, 49407, 49407, 49407, 49407,
         49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407,
         49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407,
         49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407,
         49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407,
         49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407,
         49407, 49407, 49407, 49407, 49407, 49407, 49407]])

Texteinbettung mit dem CLIP Text Encoder Modell

Die Tokens werden an das Text-Encoder-Modell gesendet und in einen Tensor der Form (1, 77, 768) umgewandelt, wobei die erste Dimension die Batch-Größe, die zweite Dimension die Anzahl der Tokens und die dritte Dimension die Einbettungsgröße ist. Der Text-Encoder ist ein OpenAI CLIP Modell, das Text mit Bildern verbindet.

Der Text-Encoder erstellt die Texteinbettung, die trainiert wird, um den Text-Prompt in einen Vektor zu kodieren, der zur Steuerung der Bilderzeugung verwendet wird. Die Texteinbettung wird dann mit der uncond-Einbettung verkettet, um die Texteinbettungen zu erstellen, die an das UNet-Modell für die Inferenz gesendet werden.

  • Texteinbettung: Ein Vektor von Zahlen, der den Text-Prompt repräsentiert und aus dem Tokenisierungsergebnis erstellt wurde. Die Texteinbettung wird vom text_encoder Modell erstellt.
        public static float[] TextEncoder(int[] tokenizedInput)
        {
            // Create input tensor. OrtValue will not copy, will read from managed memory
            using var input_ids = OrtValue.CreateTensorValueFromMemory<int>(tokenizedInput,
                new long[] { 1, tokenizedInput.Count() });

            var textEncoderOnnxPath = Directory.GetCurrentDirectory().ToString() + ("\\text_encoder\\model.onnx");

            using var encodeSession = new InferenceSession(textEncoderOnnxPath);

            // Pre-allocate the output so it goes to a managed buffer
            // we know the shape
            var lastHiddenState = new float[1 * 77 * 768];
            using var outputOrtValue = OrtValue.CreateTensorValueFromMemory<float>(lastHiddenState, new long[] { 1, 77, 768 });

            string[] input_names = { "input_ids" };
            OrtValue[] inputs = { input_ids };

            string[] output_names = { encodeSession.OutputNames[0] };
            OrtValue[] outputs = { outputOrtValue };

            // Run inference.
            using var runOptions = new RunOptions();
            encodeSession.Run(runOptions, input_names, inputs, output_names, outputs);

            return lastHiddenState;
        }
torch.Size([1, 77, 768])
tensor([[[-0.3884,  0.0229, -0.0522,  ..., -0.4899, -0.3066,  0.0675],
         [ 0.0520, -0.6046,  1.9268,  ..., -0.3985,  0.9645, -0.4424],
         [-0.8027, -0.4533,  1.7525,  ..., -1.0365,  0.6296,  1.0712],
         ...,
         [-0.6833,  0.3571, -1.1353,  ..., -1.4067,  0.0142,  0.3566],
         [-0.7049,  0.3517, -1.1524,  ..., -1.4381,  0.0090,  0.3777],
         [-0.6155,  0.4283, -1.1282,  ..., -1.4256, -0.0285,  0.3206]]],

Die Inferenzschleife: UNet-Modell, Timesteps und LMS Scheduler

Scheduler

Der Scheduler-Algorithmus und das unet-Modell arbeiten zusammen, um das Bild zu entrauschen und ein Bild zu erstellen, das den Text-Prompt repräsentiert. Es gibt verschiedene Scheduler-Algorithmen, die verwendet werden können. Um mehr darüber zu erfahren, lesen Sie diesen Blog von Hugging Face. In diesem Beispiel verwenden wir den `LMSDiscreteScheduler`, der auf dem HuggingFace scheduling_lms_discrete.py basiert.

Timesteps

Die Inferenzschleife ist die Hauptschleife, die den Scheduler-Algorithmus und das unet-Modell ausführt. Die Schleife läuft für die Anzahl der timesteps, die vom Scheduler-Algorithmus basierend auf der Anzahl der Inferenzschritte und anderen Parametern berechnet werden.

Für dieses Beispiel haben wir 10 Inferenzschritte, die die folgenden Timesteps berechnet haben.

// Get path to model to create inference session.
var modelPath = Directory.GetCurrentDirectory().ToString() + ("\\unet\\model.onnx");
var scheduler = new LMSDiscreteScheduler();
var timesteps = scheduler.SetTimesteps(numInferenceSteps);
tensor([999., 888., 777., 666., 555., 444., 333., 222., 111.,   0.])

Latents

Die latents sind der verrauschte Bild-Tensor, der in der Modelleingabe verwendet wird. Er wird mit der Funktion GenerateLatentSample erstellt, um einen zufälligen Tensor der Form (1,4,64,64) zu erzeugen. Der seed kann auf eine Zufallszahl oder eine feste Zahl gesetzt werden. Wenn der seed auf eine feste Zahl gesetzt ist, wird jedes Mal derselbe latente Tensor verwendet. Dies ist nützlich für das Debugging oder wenn Sie jedes Mal dasselbe Bild erstellen möchten.

var seed = new Random().Next();
var latents = GenerateLatentSample(batchSize, height, width,seed, scheduler.InitNoiseSigma);

Image of browser inferencing on sample images.

Inferenzschleife

Für jeden Inferenzschritt wird das latente Bild dupliziert, um die Tensorform (2,4,64,64) zu erzeugen, dann wird es skaliert und mit dem UNet-Modell inferenziert. Die Ausgabe-Tensoren (2,4,64,64) werden aufgeteilt und Guidance angewendet. Der resultierende Tensor wird dann als Teil des Entrauschungsprozesses in den LMSDiscreteScheduler-Schritt eingespeist, und der aus dem Scheduler-Schritt resultierende Tensor wird zurückgegeben, und die Schleife wiederholt sich, bis die num_inference_steps erreicht sind.

var modelPath = Directory.GetCurrentDirectory().ToString() + ("\\unet\\model.onnx");
var scheduler = new LMSDiscreteScheduler();
var timesteps = scheduler.SetTimesteps(numInferenceSteps);

var seed = new Random().Next();
var latents = GenerateLatentSample(batchSize, height, width, seed, scheduler.InitNoiseSigma);

// Create Inference Session
using var options = new SessionOptions();
using var unetSession = new InferenceSession(modelPath, options);

var latentInputShape = new int[] { 2, 4, height / 8, width / 8 };
var splitTensorsShape = new int[] { 1, 4, height / 8, width / 8 };

for (int t = 0; t < timesteps.Length; t++)
{
    // torch.cat([latents] * 2)
    var latentModelInput = TensorHelper.Duplicate(latents.ToArray(), latentInputShape);

    // Scale the input
    latentModelInput = scheduler.ScaleInput(latentModelInput, timesteps[t]);

    // Create model input of text embeddings, scaled latent image and timestep
    var input = CreateUnetModelInput(textEmbeddings, latentModelInput, timesteps[t]);

    // Run Inference
    using var output = unetSession.Run(input);
    var outputTensor = output[0].Value as DenseTensor<float>;

    // Split tensors from 2,4,64,64 to 1,4,64,64
    var splitTensors = TensorHelper.SplitTensor(outputTensor, splitTensorsShape);
    var noisePred = splitTensors.Item1;
    var noisePredText = splitTensors.Item2;

    // Perform guidance
    noisePred = performGuidance(noisePred, noisePredText, guidanceScale);

    // LMS Scheduler Step
    latents = scheduler.Step(noisePred, timesteps[t], latents);
}

Nachbearbeitung des output mit dem VAEDecoder

Nach Abschluss der Inferenzschleife wird der resultierende Tensor skaliert und dann an das vae_decoder Modell gesendet, um das Bild zu dekodieren. Schließlich wird der dekodierte Bild-Tensor in ein Bild umgewandelt und auf der Festplatte gespeichert.

public static Tensor<float> Decoder(List<NamedOnnxValue> input)
{
    // Load the model which will be used to decode the latents into image space. 
   var vaeDecoderModelPath = Directory.GetCurrentDirectory().ToString() + ("\\vae_decoder\\model.onnx");
    
    // Create an InferenceSession from the Model Path.
    var vaeDecodeSession = new InferenceSession(vaeDecoderModelPath);

   // Run session and send the input data in to get inference output. 
    var output = vaeDecodeSession.Run(input);
    var result = (output.ToList().First().Value as Tensor<float>);
    return result;
}

public static Image<Rgba32> ConvertToImage(Tensor<float> output, int width = 512, int height = 512, string imageName = "sample")
{
    var result = new Image<Rgba32>(width, height);
    for (var y = 0; y < height; y++)
    {
        for (var x = 0; x < width; x++)
        {
            result[x, y] = new Rgba32(
                (byte)(Math.Round(Math.Clamp((output[0, 0, y, x] / 2 + 0.5), 0, 1) * 255)),
                (byte)(Math.Round(Math.Clamp((output[0, 1, y, x] / 2 + 0.5), 0, 1) * 255)),
                (byte)(Math.Round(Math.Clamp((output[0, 2, y, x] / 2 + 0.5), 0, 1) * 255))
            );
        }
    }
    result.Save($@"C:/code/StableDiffusion/{imageName}.png");
    return result;
}

Das Ergebnisbild

image

Fazit

Dies ist ein High-Level-Überblick darüber, wie man Stable Diffusion in C# ausführt. Er deckte die Hauptkonzepte ab und lieferte Beispiele zur Implementierung. Den vollständigen Code finden Sie im Stable Diffusion C# Sample.

Ressourcen