Signatur
Beschreibung
dispatchWorkgroups() ist der eigentliche „Startbefehl“ eines Compute-Durchlaufs in WebGPU. Die Methode zeichnet in den GPUComputePassEncoder einen Befehl auf, der den Compute-Shader der zuvor mit setPipeline() gesetzten Pipeline ausführt – und zwar für ein Raster aus X × Y × Z Arbeitsgruppen. Ausgeführt wird der Befehl erst, wenn der daraus erzeugte GPUCommandBuffer per device.queue.submit() an die GPU übergeben wird.
Wie viele Threads (Invocations) eine Arbeitsgruppe enthält, legt der WGSL-Shader mit dem Attribut @workgroup_size(x, y, z) fest. Die Gesamtzahl der Aufrufe ergibt sich also aus Anzahl Arbeitsgruppen × Arbeitsgruppengröße je Dimension. Im Shader erkennt jeder Aufruf seine Position über eingebaute Werte wie global_invocation_id, workgroup_id und local_invocation_id.
Typischerweise teilt man die Datenmenge durch die Arbeitsgruppengröße und rundet auf: Math.ceil(elementAnzahl / 64). Weil dabei am Ende mehr Threads entstehen können als Daten vorhanden sind, muss der Shader eine Bereichsprüfung enthalten. Bei 2D-Daten wie Bildern nutzt man X und Y, bei Volumendaten zusätzlich Z.
Vor dem Aufruf müssen Pipeline und alle benötigten Bind-Groups (setBindGroup()) gesetzt sein. Ist die Anzahl der Arbeitsgruppen erst auf der GPU bekannt, verwendet man stattdessen dispatchWorkgroupsIndirect().
Parameter
| Name | Typ | Default | Beschreibung |
|---|---|---|---|
| $workgroupCountX Pflicht | number | Anzahl der Arbeitsgruppen in X-Richtung. Ganze Zahl ≥ 0, höchstens device.limits.maxComputeWorkgroupsPerDimension (Standard 65535). |
|
| $workgroupCountY | number | 1 | Anzahl der Arbeitsgruppen in Y-Richtung. Gleiche Obergrenze wie bei X. |
| $workgroupCountZ | number | 1 | Anzahl der Arbeitsgruppen in Z-Richtung. Gleiche Obergrenze wie bei X. |
Rückgabewert
queue.submit() und z. B. dem Zurücklesen eines Buffers mit mapAsync() bereit.Beispiele
Array-Werte auf der GPU verdoppeln (1D)
const adapter = await navigator.gpu?.requestAdapter();
if (!adapter) throw new Error('WebGPU wird nicht unterstützt');
const device = await adapter.requestDevice();
const WORKGROUP_SIZE = 64;
const module = device.createShaderModule({
code: `
@group(0) @binding(0) var<storage, read_write> data: array<f32>;
@compute @workgroup_size(${WORKGROUP_SIZE})
fn main(@builtin(global_invocation_id) id: vec3u) {
if (id.x >= arrayLength(&data)) { return; } // Bereichsprüfung
data[id.x] = data[id.x] * 2.0;
}
`,
});
const input = new Float32Array([1, 2, 3, 4, 5]);
const storage = device.createBuffer({
size: input.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(storage, 0, input);
const readback = device.createBuffer({
size: input.byteLength,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
});
const pipeline = device.createComputePipeline({
layout: 'auto',
compute: { module, entryPoint: 'main' },
});
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [{ binding: 0, resource: { buffer: storage } }],
});
const encoder = device.createCommandEncoder();
const pass = encoder.beginComputePass();
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.dispatchWorkgroups(Math.ceil(input.length / WORKGROUP_SIZE));
pass.end();
encoder.copyBufferToBuffer(storage, 0, readback, 0, input.byteLength);
device.queue.submit([encoder.finish()]);
await readback.mapAsync(GPUMapMode.READ);
console.log(Array.from(new Float32Array(readback.getMappedRange())));
readback.unmap();
2D-Raster für die Bildverarbeitung berechnen
// Shader nutzt @workgroup_size(8, 8) – also 64 Threads pro Gruppe
const dispatchForImage = (pass, width, height, tile = 8) => {
const x = Math.ceil(width / tile);
const y = Math.ceil(height / tile);
pass.dispatchWorkgroups(x, y); // Z ist standardmäßig 1
return { x, y };
};
// Innerhalb eines bereits vorbereiteten Compute-Passes:
// pass.setPipeline(blurPipeline);
// pass.setBindGroup(0, imageBindGroup);
const fakePass = { dispatchWorkgroups: (...a) => console.log('dispatch', a) };
console.log(dispatchForImage(fakePass, 1920, 1080));
Limits prüfen und große Datenmengen aufteilen
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const maxPerDim = device.limits.maxComputeWorkgroupsPerDimension; // meist 65535
const WORKGROUP_SIZE = 64;
const elements = 10_000_000;
const groupsNeeded = Math.ceil(elements / WORKGROUP_SIZE); // 156250 > 65535
// Auf zwei Dimensionen verteilen; im Shader Index = id.x + id.y * (x * 64)
const x = Math.min(groupsNeeded, maxPerDim);
const y = Math.ceil(groupsNeeded / x);
console.log({ maxPerDim, x, y });
// pass.dispatchWorkgroups(x, y);
// Wichtig · Fallstricke
- Arbeitsgruppen statt Threads: Die Argumente geben die Anzahl der Arbeitsgruppen an, nicht die der Threads. Ein häufiger Fehler ist
dispatchWorkgroups(elementAnzahl)bei@workgroup_size(64)– dadurch laufen 64-mal zu viele Threads. - Bereichsprüfung im Shader: Durch das Aufrunden mit
Math.ceil()entstehen überzählige Threads. Ohneif (id.x >= n) { return; }wird außerhalb des Buffers gelesen oder geschrieben. WebGPU verhindert Speicherzugriffe außerhalb der Grenzen zwar, die Ergebnisse sind dann aber undefiniert. - Limits: Jede Dimension darf
maxComputeWorkgroupsPerDimension(Standard 65535) nicht überschreiten. Wird das Limit überschritten, wird der Befehlspuffer ungültig; der Fehler taucht als Validierungsfehler auf (device.pushErrorScope('validation')oder Ereignisuncapturederror), nicht als Ausnahme im JavaScript. - Ein Aufruf mit
0in irgendeiner Dimension ist gültig, bewirkt aber nichts. - Ohne vorheriges
setPipeline()bzw. mit fehlenden Bind-Groups ist der Pass ungültig. - Kompatibilität: WebGPU steht nur in sicheren Kontexten zur Verfügung und noch nicht in allen Browsern bzw. auf allen Plattformen. Vorher mit
if (!navigator.gpu)prüfen. Hinweis: In frühen Entwürfen hieß die Methodedispatch(); diese Bezeichnung ist veraltet.