Erste Schritte mit GPU-Computing im Web

In diesem Beitrag wird die experimentelle WebGPU API anhand von Beispielen vorgestellt und Sie erhalten die ersten Schritte mit datenparallelen Berechnungen mit der GPU.

François Beaufort
François Beaufort

Hintergrund

Wie Sie vielleicht bereits wissen, ist die Grafikprozessor (Graphic Processing Unit, GPU) ein elektronisches Subsystem innerhalb eines Computers, das ursprünglich auf die Verarbeitung von Grafiken spezialisiert war. In den letzten 10 Jahren hat es sich jedoch zu einer flexibleren Architektur weiterentwickelt, mit der Entwickler viele Arten von Algorithmen implementieren können, nicht nur 3D-Grafiken rendern, und gleichzeitig die einzigartige Architektur der GPU nutzen können. Diese Funktionen werden als GPU-Computing bezeichnet und die Verwendung einer GPU als Coprozessor für wissenschaftliches Computing für allgemeine Zwecke wird als General-Zweck-GPU-Programmierung (GPGPU) bezeichnet.

GPU-Computing hat wesentlich zum aktuellen Boom des maschinellen Lernens beigetragen, da neuronale Faltungsnetzwerke und andere Modelle die Architektur nutzen können, um auf GPUs effizienter zu arbeiten. Da bei der aktuellen Webplattform keine GPU-Computing-Funktionen verfügbar sind, entwirft die Community-Gruppe "GPU for the Web" des W3C eine API, mit der die modernen GPU-APIs verfügbar gemacht werden können, die auf den meisten aktuellen Geräten verfügbar sind. Diese API wird als WebGPU bezeichnet.

WebGPU ist eine Low-Level-API wie WebGL. Sie ist sehr wirkungsvoll und umfassend, wie Sie sehen werden. Das ist auch vollkommen in Ordnung. Wir legen Wert auf Leistung.

In diesem Artikel konzentriere ich mich auf den GPU-Compute-Teil von WebGPU und, um ehrlich zu sein, kratze ich nur an der Oberfläche, damit Sie selbst mit dem Spielen beginnen können. In kommenden Artikeln werde ich auf das WebGPU-Rendering (Canvas, Textur usw.) eingehen.

Auf die GPU zugreifen

In WebGPU ist der Zugriff auf die GPU ganz einfach. Durch den Aufruf von navigator.gpu.requestAdapter() wird ein JavaScript-Promise zurückgegeben, das asynchron mit einem GPU-Adapter aufgelöst wird. Stellen Sie sich diesen Adapter als Grafikkarte vor. Es kann entweder integriert (auf demselben Chip wie die CPU) oder diskret sein (in der Regel eine PCIe-Karte, die leistungsfähiger ist, aber mehr Energie verbraucht).

Sobald du den GPU-Adapter hast, ruf adapter.requestDevice() auf, um ein Promise zu erhalten, das mit einem GPU-Gerät aufgelöst wird, das du für GPU-Berechnungen verwenden wirst.

const adapter = await navigator.gpu.requestAdapter();
if (!adapter) { return; }
const device = await adapter.requestDevice();

Beide Funktionen nutzen Optionen, mit denen Sie genau angeben können, welche Art von Adapter (Strompräferenz) und Gerät (Erweiterungen, Limits) Sie benötigen. Der Einfachheit halber verwenden wir in diesem Artikel die Standardoptionen.

Pufferspeicher schreiben

Sehen wir uns an, wie JavaScript verwendet wird, um Daten für die GPU in den Arbeitsspeicher zu schreiben. Dieser Prozess ist aufgrund des Sandboxing-Modells, das in modernen Webbrowsern verwendet wird, nicht einfach.

Im folgenden Beispiel wird gezeigt, wie Sie vier Byte in den Zwischenspeicher für von der GPU zugänglichen Arbeitsspeicher schreiben. Sie ruft device.createBuffer() auf, die die Größe des Zwischenspeichers und seine Nutzung angibt. Obwohl das Nutzungs-Flag GPUBufferUsage.MAP_WRITE für diesen bestimmten Aufruf nicht erforderlich ist, soll explizit angegeben werden, dass in diesen Zwischenspeicher geschrieben werden soll. Dies führt dazu, dass ein GPU-Pufferobjekt beim Erstellen zugeordnet wird, da mappedAtCreation auf „true“ gesetzt ist. Anschließend kann der zugehörige Zwischenspeicher für binäre Rohdaten durch Aufrufen der GPU-Zwischenspeichermethode getMappedRange() abgerufen werden.

Das Schreiben von Bytes ist bekannt, wenn Sie bereits mit ArrayBuffer gespielt haben. Verwenden Sie TypedArray und kopieren Sie die Werte hinein.

// Get a GPU buffer in a mapped state and an arrayBuffer for writing.
const gpuBuffer = device.createBuffer({
  mappedAtCreation: true,
  size: 4,
  usage: GPUBufferUsage.MAP_WRITE
});
const arrayBuffer = gpuBuffer.getMappedRange();

// Write bytes to buffer.
new Uint8Array(arrayBuffer).set([0, 1, 2, 3]);

An dieser Stelle ist der GPU-Zwischenspeicher zugeordnet, d. h. er gehört der CPU und ist mit Lese-/Schreibvorgängen von JavaScript aus zugänglich. Damit die GPU darauf zugreifen kann, muss die Zuordnung aufgehoben werden. Dazu müssen Sie nur gpuBuffer.unmap() aufrufen.

Das Konzept der Zuordnung/Nicht zugeordnet ist erforderlich, um Race-Bedingungen zu verhindern, bei denen GPU und CPU gleichzeitig auf Arbeitsspeicher zugreifen.

Zwischenspeicher lesen

Sehen wir uns nun an, wie Sie einen GPU-Puffer in einen anderen GPU-Puffer kopieren und zurücklesen.

Da wir in den ersten GPU-Zwischenspeicher schreiben und ihn in einen zweiten GPU-Zwischenspeicher kopieren möchten, ist ein neues Nutzungs-Flag GPUBufferUsage.COPY_SRC erforderlich. Der zweite GPU-Zwischenspeicher wird dieses Mal mit device.createBuffer() in einem nicht zugeordneten Zustand erstellt. Das Nutzungs-Flag lautet GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, da es als Ziel des ersten GPU-Zwischenspeichers verwendet und in JavaScript gelesen wird, nachdem GPU-Kopierbefehle ausgeführt wurden.

// Get a GPU buffer in a mapped state and an arrayBuffer for writing.
const gpuWriteBuffer = device.createBuffer({
  mappedAtCreation: true,
  size: 4,
  usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC
});
const arrayBuffer = gpuWriteBuffer.getMappedRange();

// Write bytes to buffer.
new Uint8Array(arrayBuffer).set([0, 1, 2, 3]);

// Unmap buffer so that it can be used later for copy.
gpuWriteBuffer.unmap();

// Get a GPU buffer for reading in an unmapped state.
const gpuReadBuffer = device.createBuffer({
  size: 4,
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
});

Da die GPU ein unabhängiger Koprozessor ist, werden alle GPU-Befehle asynchron ausgeführt. Aus diesem Grund gibt es eine Liste von GPU-Befehlen, die bei Bedarf in Batches erstellt und gesendet werden. In WebGPU ist der von device.createCommandEncoder() zurückgegebene GPU-Befehls-Encoder das JavaScript-Objekt, das einen Batch "zwischengespeicherter" Befehle erstellt, die irgendwann an die GPU gesendet werden. Die Methoden bei GPUBuffer dagegen sind „ungepuffert“, d. h. sie werden zum Zeitpunkt ihres Aufrufs atomar ausgeführt.

Sobald Sie den GPU-Befehlsencoder haben, rufen Sie copyEncoder.copyBufferToBuffer() wie unten gezeigt auf, um diesen Befehl der Befehlswarteschlange zur späteren Ausführung hinzuzufügen. Beenden Sie die Codierungsbefehle, indem Sie copyEncoder.finish() aufrufen und diese an die Befehlswarteschlange des GPU-Geräts senden. Die Warteschlange ist für die Verarbeitung von Einreichungen über device.queue.submit() mit den GPU-Befehlen als Argumenten zuständig. Dadurch werden alle im Array gespeicherten Befehle in der richtigen Reihenfolge ausgeführt.

// Encode commands for copying buffer to buffer.
const copyEncoder = device.createCommandEncoder();
copyEncoder.copyBufferToBuffer(
  gpuWriteBuffer /* source buffer */,
  0 /* source offset */,
  gpuReadBuffer /* destination buffer */,
  0 /* destination offset */,
  4 /* size */
);

// Submit copy commands.
const copyCommands = copyEncoder.finish();
device.queue.submit([copyCommands]);

Zu diesem Zeitpunkt wurden GPU-Warteschlangenbefehle gesendet, aber nicht unbedingt ausgeführt. Rufen Sie gpuReadBuffer.mapAsync() mit GPUMapMode.READ auf, um den zweiten GPU-Zwischenspeicher zu lesen. Es gibt ein Promise zurück, das aufgelöst wird, wenn der GPU-Zwischenspeicher zugeordnet ist. Rufen Sie dann den zugeordneten Bereich mit gpuReadBuffer.getMappedRange() ab, der dieselben Werte wie der erste GPU-Zwischenspeicher enthält, nachdem alle GPU-Befehle in der Warteschlange ausgeführt wurden.

// Read buffer.
await gpuReadBuffer.mapAsync(GPUMapMode.READ);
const copyArrayBuffer = gpuReadBuffer.getMappedRange();
console.log(new Uint8Array(copyArrayBuffer));

Sie können dieses Beispiel ausprobieren.

Kurz gesagt finden Sie hier, was Sie bei Puffervorgängen beachten müssen:

  • Die Zuordnung von GPU-Puffern muss aufgehoben werden, damit sie beim Senden der Gerätewarteschlange verwendet werden können.
  • Wenn sie zugeordnet sind, können GPU-Puffer in JavaScript gelesen und geschrieben werden.
  • GPU-Zwischenspeicher werden zugeordnet, wenn mapAsync() und createBuffer() aufgerufen werden, wobei mappedAtCreation auf „true“ gesetzt ist.

Shader-Programmierung

Auf der GPU ausgeführte Programme, die nur Berechnungen durchführen und keine Dreiecke zeichnen, werden als Computing-Shader bezeichnet. Sie werden parallel von Hunderten von GPU-Kernen (die kleiner als CPU-Kerne sind) ausgeführt, die zusammen Daten verarbeiten. Ihre Eingabe und Ausgabe sind Puffer in WebGPU.

Um die Verwendung von Compute-Shadern in WebGPU zu veranschaulichen, beschäftigen wir uns mit der Matrixmultiplikation, einem gängigen Algorithmus im maschinellen Lernen, der unten veranschaulicht wird.

Matrixmultiplikationsdiagramm
Matrixmultiplikationsdiagramm

Kurz gesagt, werden wir Folgendes tun:

  1. Erstellen Sie drei GPU-Puffer (zwei für die zu multiplizierenden Matrizen und einer für die Ergebnismatrix).
  2. Eingabe und Ausgabe für den Compute-Shader beschreiben
  3. Computing-Shader-Code kompilieren
  4. Computing-Pipeline einrichten
  5. Die codierten Befehle im Batch an die GPU senden
  6. GPU-Zwischenspeicher der Ergebnismatrix lesen

GPU-Puffer erstellen

Der Einfachheit halber werden Matrizen als Liste von Gleitkommazahlen dargestellt. Das erste Element ist die Anzahl der Zeilen, das zweite Element die Anzahl der Spalten und der Rest die tatsächlichen Zahlen der Matrix.

Einfache Darstellung einer Matrix in JavaScript und ihrer Entsprechung in mathematischer Notation
Einfache Darstellung einer Matrix in JavaScript und ihrer Entsprechung in mathematischer Notation

Die drei GPU-Puffer sind Speicherpuffer, da wir Daten im Compute-Shader speichern und abrufen müssen. Dies erklärt, warum die Flags für die Nutzung des GPU-Zwischenspeichers für alle Flags GPUBufferUsage.STORAGE enthalten. Das Nutzungs-Flag der Ergebnismatrix hat ebenfalls GPUBufferUsage.COPY_SRC, da es zum Lesen in einen anderen Zwischenspeicher kopiert wird, sobald alle GPU-Warteschlangenbefehle ausgeführt wurden.

const adapter = await navigator.gpu.requestAdapter();
if (!adapter) { return; }
const device = await adapter.requestDevice();


// First Matrix

const firstMatrix = new Float32Array([
  2 /* rows */, 4 /* columns */,
  1, 2, 3, 4,
  5, 6, 7, 8
]);

const gpuBufferFirstMatrix = device.createBuffer({
  mappedAtCreation: true,
  size: firstMatrix.byteLength,
  usage: GPUBufferUsage.STORAGE,
});
const arrayBufferFirstMatrix = gpuBufferFirstMatrix.getMappedRange();
new Float32Array(arrayBufferFirstMatrix).set(firstMatrix);
gpuBufferFirstMatrix.unmap();


// Second Matrix

const secondMatrix = new Float32Array([
  4 /* rows */, 2 /* columns */,
  1, 2,
  3, 4,
  5, 6,
  7, 8
]);

const gpuBufferSecondMatrix = device.createBuffer({
  mappedAtCreation: true,
  size: secondMatrix.byteLength,
  usage: GPUBufferUsage.STORAGE,
});
const arrayBufferSecondMatrix = gpuBufferSecondMatrix.getMappedRange();
new Float32Array(arrayBufferSecondMatrix).set(secondMatrix);
gpuBufferSecondMatrix.unmap();


// Result Matrix

const resultMatrixBufferSize = Float32Array.BYTES_PER_ELEMENT * (2 + firstMatrix[0] * secondMatrix[1]);
const resultMatrixBuffer = device.createBuffer({
  size: resultMatrixBufferSize,
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
});

Gruppenlayout und Gruppe binden

Die Konzepte des Bindegruppenlayouts und der Bindungsgruppe gelten nur für WebGPU. Ein Bindegruppenlayout definiert die von einem Shader erwartete Eingabe-/Ausgabeschnittstelle, während eine Bindungsgruppe die tatsächlichen Eingabe-/Ausgabedaten für einen Shader darstellt.

Im folgenden Beispiel erwartet das Bindegruppenlayout zwei schreibgeschützte Speicherpuffer mit den nummerierten Eintragsbindungen 0 und 1 sowie einen Speicherpuffer bei 2 für den Compute-Shader. Die Bindungsgruppe, die für dieses Bindgruppenlayout definiert ist, weist dagegen den Einträgen GPU-Zwischenspeicher zu: gpuBufferFirstMatrix der Bindung 0, gpuBufferSecondMatrix der Bindung 1 und resultMatrixBuffer der Bindung 2.

const bindGroupLayout = device.createBindGroupLayout({
  entries: [
    {
      binding: 0,
      visibility: GPUShaderStage.COMPUTE,
      buffer: {
        type: "read-only-storage"
      }
    },
    {
      binding: 1,
      visibility: GPUShaderStage.COMPUTE,
      buffer: {
        type: "read-only-storage"
      }
    },
    {
      binding: 2,
      visibility: GPUShaderStage.COMPUTE,
      buffer: {
        type: "storage"
      }
    }
  ]
});

const bindGroup = device.createBindGroup({
  layout: bindGroupLayout,
  entries: [
    {
      binding: 0,
      resource: {
        buffer: gpuBufferFirstMatrix
      }
    },
    {
      binding: 1,
      resource: {
        buffer: gpuBufferSecondMatrix
      }
    },
    {
      binding: 2,
      resource: {
        buffer: resultMatrixBuffer
      }
    }
  ]
});

Compute-Shader-Code

Der Computing-Shader-Code zum Multiplizieren von Matrizen ist in WGSL, der WebGPU-Shader Language, geschrieben und lässt sich einfach in SPIR-V übersetzen. Wenn wir nicht ins Detail gehen, sollten Sie die drei Speicherpuffer finden, die mit var<storage> gekennzeichnet sind. Das Programm verwendet firstMatrix und secondMatrix als Eingaben und resultMatrix als Ausgabe.

Beachten Sie, dass jeder Speicherpuffer eine binding-Darstellung verwendet, die dem gleichen Index entspricht, der in den oben deklarierten Bindgruppenlayouts und Bindgruppen definiert ist.

const shaderModule = device.createShaderModule({
  code: `
    struct Matrix {
      size : vec2f,
      numbers: array<f32>,
    }

    @group(0) @binding(0) var<storage, read> firstMatrix : Matrix;
    @group(0) @binding(1) var<storage, read> secondMatrix : Matrix;
    @group(0) @binding(2) var<storage, read_write> resultMatrix : Matrix;

    @compute @workgroup_size(8, 8)
    fn main(@builtin(global_invocation_id) global_id : vec3u) {
      // Guard against out-of-bounds work group sizes
      if (global_id.x >= u32(firstMatrix.size.x) || global_id.y >= u32(secondMatrix.size.y)) {
        return;
      }

      resultMatrix.size = vec2(firstMatrix.size.x, secondMatrix.size.y);

      let resultCell = vec2(global_id.x, global_id.y);
      var result = 0.0;
      for (var i = 0u; i < u32(firstMatrix.size.y); i = i + 1u) {
        let a = i + resultCell.x * u32(firstMatrix.size.y);
        let b = resultCell.y + i * u32(secondMatrix.size.y);
        result = result + firstMatrix.numbers[a] * secondMatrix.numbers[b];
      }

      let index = resultCell.y + resultCell.x * u32(secondMatrix.size.y);
      resultMatrix.numbers[index] = result;
    }
  `
});

Pipeline einrichten

Die Computing-Pipeline ist das Objekt, das den Rechenvorgang tatsächlich beschreibt. Rufen Sie dazu device.createComputePipeline() auf. Es werden zwei Argumente verwendet: das zuvor erstellte Bindgruppenlayout, eine Rechenphase, die den Einstiegspunkt unseres Compute-Shaders (die WGSL-Funktion main) definiert, und das tatsächliche Compute-Shader-Modul, das mit device.createShaderModule() erstellt wurde.

const computePipeline = device.createComputePipeline({
  layout: device.createPipelineLayout({
    bindGroupLayouts: [bindGroupLayout]
  }),
  compute: {
    module: shaderModule,
    entryPoint: "main"
  }
});

Übermittlung von Befehlen

Nachdem Sie eine Bindungsgruppe mit unseren drei GPU-Puffern und einer Compute-Pipeline mit einem Bindgruppenlayout instanziiert haben, ist es an der Zeit, diese zu verwenden.

Wir starten mit commandEncoder.beginComputePass() einen programmierbaren Compute-Pass-Encoder. Damit codieren wir GPU-Befehle, die die Matrixmultiplikation durchführen. Legen Sie die Pipeline mit passEncoder.setPipeline(computePipeline) und die Bindungsgruppe mit passEncoder.setBindGroup(0, bindGroup) auf Index 0 fest. Der Index 0 entspricht dem group(0)-Dekoration im WGSL-Code.

Sprechen wir nun darüber, wie dieser Rechen-Shader auf der GPU ausgeführt wird. Unser Ziel ist es, dieses Programm für jede Zelle der Ergebnismatrix Schritt für Schritt parallel auszuführen. Bei einer Ergebnismatrix der Größe 16 × 32 würden wir beispielsweise passEncoder.dispatchWorkgroups(2, 4) oder passEncoder.dispatchWorkgroups(16 / 8, 32 / 8) aufrufen, um den Ausführungsbefehl in einem @workgroup_size(8, 8) zu codieren. Das erste Argument "x" ist die erste Dimension, das zweite "y" ist die zweite Dimension und das letzte "z" ist die dritte Dimension, die standardmäßig 1 ist, da sie hier nicht benötigt wird. In der GPU-Computing-Welt wird die Codierung eines Befehls zum Ausführen einer Kernelfunktion für einen Satz von Daten als „disposition“ bezeichnet.

Parallele Ausführung für jede Ergebnismatrixzelle
Parallele Ausführung für jede Ergebnismatrixzelle

Die Größe des Arbeitsgruppenrasters für unseren Compute-Shader ist (8, 8) in unserem WGSL-Code. Aus diesem Grund werden "x" und "y", die die Anzahl der Zeilen der ersten Matrix und die Anzahl der Spalten der zweiten Matrix sind, durch 8 geteilt. Damit können wir jetzt einen Compute-Aufruf mit passEncoder.dispatchWorkgroups(firstMatrix[0] / 8, secondMatrix[1] / 8) absenden. Die Anzahl der auszuführenden Arbeitsgruppenraster entspricht den dispatchWorkgroups()-Argumenten.

Wie in der obigen Zeichnung zu sehen, hat jeder Shader Zugriff auf ein eindeutiges builtin(global_invocation_id)-Objekt, das verwendet wird, um zu ermitteln, welche Ergebnismatrixzelle berechnet werden soll.

const commandEncoder = device.createCommandEncoder();

const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(computePipeline);
passEncoder.setBindGroup(0, bindGroup);
const workgroupCountX = Math.ceil(firstMatrix[0] / 8);
const workgroupCountY = Math.ceil(secondMatrix[1] / 8);
passEncoder.dispatchWorkgroups(workgroupCountX, workgroupCountY);
passEncoder.end();

Rufe passEncoder.end() auf, um den Compute-Pass-Encoder zu beenden. Erstellen Sie dann einen GPU-Zwischenspeicher, der als Ziel verwendet wird, um den Ergebnismatrixpuffer mit copyBufferToBuffer zu kopieren. Beenden Sie die Codierungsbefehle mit copyEncoder.finish() und senden Sie sie an die GPU-Gerätewarteschlange. Rufen Sie dazu device.queue.submit() mit den GPU-Befehlen auf.

// Get a GPU buffer for reading in an unmapped state.
const gpuReadBuffer = device.createBuffer({
  size: resultMatrixBufferSize,
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
});

// Encode commands for copying buffer to buffer.
commandEncoder.copyBufferToBuffer(
  resultMatrixBuffer /* source buffer */,
  0 /* source offset */,
  gpuReadBuffer /* destination buffer */,
  0 /* destination offset */,
  resultMatrixBufferSize /* size */
);

// Submit GPU commands.
const gpuCommands = commandEncoder.finish();
device.queue.submit([gpuCommands]);

Ergebnismatrix lesen

Das Lesen der Ergebnismatrix ist so einfach wie das Aufrufen von gpuReadBuffer.mapAsync() mit GPUMapMode.READ und das Warten auf die Auflösung des zurückgegebenen Versprechens, das anzeigt, dass der GPU-Zwischenspeicher jetzt zugeordnet ist. An dieser Stelle kann der zugeordnete Bereich mit gpuReadBuffer.getMappedRange() abgerufen werden.

Ergebnis der Matrixmultiplikation
Matrixmultiplikation

In unserem Code lautet das Ergebnis in der JavaScript-Konsole der Entwicklertools „2, 2, 50, 60, 114, 140“.

// Read buffer.
await gpuReadBuffer.mapAsync(GPUMapMode.READ);
const arrayBuffer = gpuReadBuffer.getMappedRange();
console.log(new Float32Array(arrayBuffer));

Glückwunsch! Sie haben es geschafft Sie können mit dem Sample spielen.

Ein letzter Trick

Eine Möglichkeit, den Code leichter lesbar zu machen, besteht darin, mit der praktischen Methode getBindGroupLayout der Compute-Pipeline das Layout der Bindungsgruppe aus dem Shader-Modul abzuleiten. Mit diesem Trick müssen Sie kein benutzerdefiniertes Bindegruppenlayout erstellen und in Ihrer Compute-Pipeline ein Pipelinelayout angeben, wie Sie unten sehen können.

Eine Abbildung von getBindGroupLayout für das vorherige Beispiel ist verfügbar.

 const computePipeline = device.createComputePipeline({
-  layout: device.createPipelineLayout({
-    bindGroupLayouts: [bindGroupLayout]
-  }),
   compute: {
-// Bind group layout and bind group
- const bindGroupLayout = device.createBindGroupLayout({
-   entries: [
-     {
-       binding: 0,
-       visibility: GPUShaderStage.COMPUTE,
-       buffer: {
-         type: "read-only-storage"
-       }
-     },
-     {
-       binding: 1,
-       visibility: GPUShaderStage.COMPUTE,
-       buffer: {
-         type: "read-only-storage"
-       }
-     },
-     {
-       binding: 2,
-       visibility: GPUShaderStage.COMPUTE,
-       buffer: {
-         type: "storage"
-       }
-     }
-   ]
- });
+// Bind group
  const bindGroup = device.createBindGroup({
-  layout: bindGroupLayout,
+  layout: computePipeline.getBindGroupLayout(0 /* index */),
   entries: [

Leistungsergebnisse

Wie unterscheidet sich die Matrixmultiplikation auf einer GPU von der Ausführung auf einer CPU? Um das herauszufinden, habe ich das gerade beschriebene Programm für eine CPU geschrieben. Wie Sie in der folgenden Grafik sehen können, scheint die Nutzung der vollen Leistung der GPU eine naheliegende Wahl zu sein, wenn die Größe der Matrizen größer als 256 x 256 ist.

GPU- im Vergleich zur CPU-Benchmark
GPU im Vergleich zur CPU-Benchmark

Dieser Artikel war erst der Anfang meiner Reise zu WebGPU. Demnächst veröffentlichen wir weitere Artikel mit ausführlicheren Informationen zu GPU-Computing und zur Funktionsweise des Renderings (Canvas, Textur, Sampler) in WebGPU.