Apostila completa de WebGPU & GPU Compute no Navegador
WebGPU é a API gráfica moderna da web — sucessora do WebGL — e, pela primeira vez no navegador, dá acesso de primeira classe a compute shaders: rodar milhares de threads em paralelo na GPU para ML, simulação, processamento de imagem e visualização massiva. Esta apostila cobre o modelo mental (adapter/device/queue), a linguagem WGSL, compute e render pipelines, performance e memória, ML no navegador, os casos de uso, e a produção — fallback, device lost, limites e debugging.
O que é WebGPU e por que
Objetivo: entender o que WebGPU é (e não é), por que substitui o WebGL, o que compute shaders desbloqueiam, o suporte atual, e quando usar cada tecnologia.
1.1 O que é
- Uma API JavaScript (
navigator.gpu) que expõe a GPU de forma moderna, mapeando para Vulkan, Metal e Direct3D 12 por baixo (o mesmo salto que essas APIs foram sobre OpenGL/D3D11). - Faz duas coisas: render (desenhar gráficos 2D/3D, como o WebGL) e compute (rodar código arbitrário e altamente paralelo na GPU — o que o WebGL não fazia direito).
- Os shaders são escritos em WGSL (WebGPU Shading Language), não GLSL.
1.2 Por que substitui o WebGL
- Compute shaders de verdade (o WebGL só simulava com "render para textura").
- Menos overhead de driver: você grava listas de comandos e submete em lote; estado explícito via pipelines e bind groups (menos "mudanças de estado global").
- Multithreading: comandos podem ser gravados de Web Workers.
- Mais previsível: validação clara, limites consultáveis, menos "funciona nessa GPU, quebra naquela".
1.3 Suporte (panorama 2026)
- Chrome / Edge: estável há tempos (desktop; Android chegando/ativo).
- Safari: enviado (Tahoe/iOS) — checar a versão-alvo.
- Firefox: em rollout.
- Sempre feature-detect e ter fallback (WebGL2 ou CPU/WASM) — Módulo 9.
1.4 Quando usar o quê
| Precisa de… | Use |
|---|---|
| UI, animações, layout, efeitos leves | CSS / Canvas 2D (ver CSS Avançado: Animações & SVG, Motion de Interface) |
| 3D com uma engine, alcance máximo hoje | Three.js / Babylon (que têm backend WebGPU e fallback WebGL) — ver Computação Gráfica & Three.js |
| Compute pesado (ML, simulação, milhões de pontos), render customizado, controle total | WebGPU direto |
| ML no navegador sem escrever kernels | transformers.js / WebLLM / ONNX Runtime Web (usam WebGPU por baixo) — ver IA Local & SLMs |
WebGPU brilha quando você precisa processar muitos dados em paralelo ou renderizar de um jeito que uma engine não faz. Se uma lib de alto nível (Three.js, transformers.js) resolve, use a lib — escrever WebGPU cru é poderoso e trabalhoso. E sempre com fallback: nem todo dispositivo tem.
Perguntas de abertura: "O que WebGPU adiciona sobre o WebGL?" (compute shaders de verdade, menos overhead de driver, estado explícito, multithreading, API moderna sobre Vulkan/Metal/D3D12), "Quando você usaria WebGPU cru em vez de Three.js?" (compute pesado, render muito customizado, controle total; a engine para o 3D comum), "Qual o estado de suporte?" (Chrome/Edge estável, Safari enviado, Firefox em rollout — sempre feature-detect + fallback).
✏️ Exercício 1 — Escolha a ferramenta
Para cada tarefa, escolha CSS/Canvas2D, Three.js, WebGPU cru ou uma lib de ML, e justifique: (a) um hover com partículas atrás de um botão; (b) um viewer de modelo 3D de produto; (c) uma simulação de fluido em tempo real cobrindo a tela; (d) rodar um modelo de embeddings no cliente; (e) um scatter plot com 5 milhões de pontos que dá pan/zoom a 60 fps.
Gabarito (uma boa resposta): (a) Canvas 2D (ou CSS) — poucas partículas, leve; WebGPU seria exagero. (b) Three.js — 3D comum, a engine resolve com glTF e tem fallback. (c) WebGPU (compute) — simulação de fluido é o caso de compute shader por excelência; WebGL faria com gambiarra. (d) lib de ML (transformers.js / ONNX Runtime Web) que usa WebGPU internamente — não escreva o kernel. (e) WebGPU — 5M de pontos com interação a 60 fps exige um vertex buffer grande e talvez um compute pass para culling/agregação; Canvas 2D e até Three.js sofrem nessa escala.
O modelo mental
Objetivo: adapter → device → queue; a natureza assíncrona e "gravar e submeter"; buffers, textures, bind groups, layouts e pipelines.
2.1 Inicialização
const adapter = await navigator.gpu?.requestAdapter(); // escolhe uma GPU física if (!adapter) throw new Error('WebGPU indisponível'); // → fallback const device = await adapter.requestDevice(); // o "handle" com que você trabalha const queue = device.queue; // para submeter comandos e escrever buffers device.lost.then(info => { /* GPU resetou — recriar tudo (Módulo 9) */ });
2.2 "Nada acontece imediatamente"
Você não "manda a GPU calcular agora". Você:
- Cria recursos (buffers, textures) e sobe dados.
- Cria pipelines (compilam os shaders e fixam o estado).
- Cria um command encoder, abre passes (compute ou render), grava chamadas (
setPipeline,setBindGroup,dispatch/draw). encoder.finish()→ um command buffer.queue.submit([commandBuffer])— aí a GPU executa, de forma assíncrona.
Ler resultado de volta é outra operação assíncrona (buffer.mapAsync) — e cara (Módulo 4).
2.3 Recursos
- Buffer: um bloco de bytes na GPU, com usage flags (
STORAGE,UNIFORM,VERTEX,INDEX,COPY_SRC/DST,MAP_READ). - Texture: dados de imagem/volume, com formato e usage; sampler define como é lida (filtro, wrap).
2.4 Bind groups e pipelines
- Um shader declara bindings (
@group(0) @binding(0)...). Um bind group layout descreve os tipos; um bind group amarra recursos concretos a esses slots. - Um pipeline (compute ou render) junta: o(s) shader(s), o layout, e o estado fixo (formato de vértice, blend, depth...). Criar pipeline é caro — faça uma vez, reuse.
Perguntas: "Descreva o fluxo de uma operação em WebGPU" (adapter → device → criar recursos e pipeline → encoder → pass → gravar comandos → finish → queue.submit; execução assíncrona), "O que é um bind group?" (amarra recursos concretos aos slots que o shader declara, conforme um layout), "Por que criar pipeline uma vez só?" (compilar shader e fixar estado é caro).
✏️ Exercício 2 — Ordem das operações
Ordene os passos para dobrar cada elemento de um array de 1 milhão de floats na GPU e ler o resultado: (a) queue.submit; (b) criar o pipeline de compute com o shader; (c) encoder.beginComputePass + setPipeline + setBindGroup + dispatchWorkgroups; (d) criar buffers (input STORAGE+COPY_DST, output STORAGE+COPY_SRC, staging MAP_READ+COPY_DST); (e) queue.writeBuffer do input; (f) copiar output → staging no encoder; (g) staging.mapAsync + ler; (h) criar bind group.
Gabarito: d → b → h → e → (novo encoder) → c → f → a → g. Ou seja: cria os buffers (d); cria o pipeline com o shader WGSL (b); cria o bind group amarrando input/output (h); sobe os dados do input (e); abre o command encoder, faz o compute pass (c), e ainda no encoder copia o output para o buffer staging (f); encoder.finish() e queue.submit (a); depois staging.mapAsync(GPUMapMode.READ), lê o getMappedRange(), e unmap() (g). O readback (f+g) é o passo caro — em produção você evitaria se o resultado for consumido por outro shader.
WGSL: a linguagem de shader
Objetivo: os tipos, os atributos (@group/@binding, @workgroup_size), os builtins, storage vs uniform, e o modelo de threads em workgroups.
3.1 Tipos
- Escalares:
f32,i32,u32,bool,f16(com extensão). - Vetores:
vec2<f32>,vec3<f32>,vec4<f32>(evec3fabreviado). - Matrizes:
mat4x4<f32>; arrays:array<f32>(runtime-sized em storage) ouarray<f32, 64>. structpara agrupar; atenção ao alinhamento (Módulo 6).
3.2 Um compute shader mínimo
@group(0) @binding(0) var<storage, read> input: array<f32>; @group(0) @binding(1) var<storage, read_write> output: array<f32>; @group(0) @binding(2) var<uniform> params: vec4<f32>; // pequeno, constante @compute @workgroup_size(64) fn main(@builtin(global_invocation_id) gid: vec3<u32>) { let i = gid.x; if (i >= arrayLength(&input)) { return; } // guarda de borda output[i] = input[i] * params.x + params.y; }
3.3 Bindings
@group(g) @binding(b): casa com o bind group / layout do lado JS.var<storage, read>/read_write: buffers grandes de dados;var<uniform>: pequenos e constantes por dispatch (mais rápido de acessar, com limite de tamanho).- Texturas:
var t: texture_2d<f32>,var s: sampler,var out: texture_storage_2d<rgba8unorm, write>.
3.4 O modelo de threads
@workgroup_size(x, y, z): quantos threads (invocations) por workgroup (ex.: 64, ou 8×8). O total =x·y·z≤ um limite (~256).- Você faz
dispatchWorkgroups(gx, gy, gz)→ executagx·gy·gzworkgroups; cada thread sabe seuglobal_invocation_id(posição global),local_invocation_id(dentro do workgroup) e o índice do workgroup. - Threads de um workgroup podem compartilhar
var<workgroup>(memória rápida) e sincronizar comworkgroupBarrier(). - É SIMT: threads executam em "warps/waves" de 32–64; divergência (ramos
ifdiferentes entre threads vizinhos) serializa e custa.
3.5 Diferenças do GLSL
- Sintaxe mais parecida com Rust (
let/var,fn, tipos explícitos). - Sem estado global implícito; tudo por bindings.
- Sem
gl_FragCoordetc. — builtins nomeados via@builtin(...).
Perguntas: "O que é um workgroup e como você escolhe o tamanho?" (grupo de threads que compartilham memória e sincronizam; múltiplo de 32/64, total ≤ ~256, ajustar por benchmark), "storage vs uniform buffer?" (grande de dados vs pequeno e constante, mais rápido, com limite), "O que é divergência e por que custa?" (threads vizinhos tomando ramos diferentes serializam — SIMT), "Por que a guarda if (i >= n) return?" (o dispatch cobre mais threads que elementos).
✏️ Exercício 3 — Leia o shader
Explique o que este shader faz e um problema nele: @compute @workgroup_size(1) fn main(@builtin(global_invocation_id) g: vec3u){ for (var j=0u; j<arrayLength(&data); j++){ data[g.x] = data[g.x] + data[j]; } }
Gabarito (uma boa resposta): a intenção parece ser somar todos os elementos e acumular em data[g.x]. Problemas graves: (1) @workgroup_size(1) — usa 1 thread por workgroup, desperdiça a GPU (deveria ser 64+); (2) cada thread percorre o array inteiro (O(n) por thread, O(n²) total) — é uma redução mal feita; (3) escreve em data[g.x] enquanto outros threads leem data[j] — data race, resultado não determinístico; (4) modificar o buffer que você está lendo, sem barreira. O jeito certo de somar um array na GPU é uma redução paralela: cada workgroup soma seu pedaço em var<workgroup> com workgroupBarrier(), grava um parcial, e um segundo passe soma os parciais (ou atomicAdd num acumulador). Ver Módulo 4.
Compute shaders na prática
Objetivo: o dispatch, o padrão "um thread por elemento", o custo do readback, os padrões paralelos (redução, prefix sum) e a memória de workgroup.
4.1 Dispatch
- Para N elementos com workgroup de 64:
dispatchWorkgroups(Math.ceil(N/64)). Sempre com a guarda de borda no shader. - Para uma grade 2D (imagem W×H) com workgroup 8×8:
dispatchWorkgroups(ceil(W/8), ceil(H/8)). - Limite:
maxComputeWorkgroupsPerDimension(~65535) — para N enorme, faça um grid 2D/3D ou vários dispatches.
4.2 O readback é caro
- Copiar o resultado da GPU de volta para a CPU (
COPY_SRC→ stagingMAP_READ→mapAsync) sincroniza e tem latência — mata o pipeline se feito a cada frame. - Evite: se o resultado do compute alimenta um render (partículas, imagem), deixe os dois compartilharem o buffer/textura sem sair da GPU ("compute produz, render consome" — Módulo 8).
- Quando precisar ler (ex.: um único número, um resultado final), leia uma vez, ou a cada N frames, e assuma a latência.
4.3 Padrões paralelos
- Map ("um thread por elemento"): o mais simples — transforma cada elemento independentemente.
- Redução (soma, máx, min): árvore — cada workgroup reduz seu pedaço em memória de workgroup com barreiras, grava um parcial; um segundo passe reduz os parciais. Ou
atomicAdd(mais simples, menos rápido em contenção). - Prefix sum / scan: base de compactação, ordenação, alocação — algoritmo de Hillis-Steele ou Blelloch por workgroup + combinação.
- Stencil (blur, convolução): cada thread lê uma vizinhança; usar memória de workgroup para não reler da memória global.
- Scatter/gather: cuidado com data races — use
atomicou reestruture.
4.4 Memória de workgroup
var<workgroup> tile: array<f32, 64>;
@compute @workgroup_size(64)
fn reduce(@builtin(local_invocation_id) lid: vec3u,
@builtin(workgroup_id) wid: vec3u) {
let i = wid.x * 64u + lid.x;
tile[lid.x] = select(0.0, input[i], i < arrayLength(&input));
workgroupBarrier();
var stride = 32u;
loop {
if (stride == 0u) { break; }
if (lid.x < stride) { tile[lid.x] += tile[lid.x + stride]; }
workgroupBarrier();
stride = stride / 2u;
}
if (lid.x == 0u) { partial[wid.x] = tile[0]; }
}
Readback a cada frame (destrói a performance). @workgroup_size(1). Esquecer a guarda de borda (lê/escreve fora do array). Data race em scatter sem atomic. workgroupBarrier() dentro de um if divergente (comportamento indefinido — a barreira precisa ser alcançada por todos). Buffer sem a usage flag certa (erro de validação). Uniform buffer grande demais (use storage).
Perguntas: "Por que o readback é caro e como evitar?" (sincroniza CPU↔GPU; deixe o resultado no buffer para o próximo shader consumir — "compute produz, render consome"), "Como você soma um array de 10M na GPU?" (redução em árvore com memória de workgroup + barreiras, dois passes; ou atomicAdd), "Para que serve var<workgroup>?" (memória rápida compartilhada pelos threads do workgroup — evita reler da global em stencils/reduções), "Onde a barreira não pode estar?" (dentro de ramo divergente).
✏️ Exercício 4 — Blur de imagem
Descreva o compute shader para um box blur 5×5 de uma imagem: o dispatch, os bindings, o uso (ou não) de memória de workgroup, e como o resultado seria consumido por um render pass sem readback.
Gabarito (uma boa resposta): Bindings: texture_2d<f32> de entrada + sampler (ou textureLoad direto), texture_storage_2d<rgba8unorm, write> de saída, e um uniform com o tamanho da imagem/kernel. Dispatch: workgroup 8×8, dispatchWorkgroups(ceil(W/8), ceil(H/8)); cada thread computa um pixel de saída. Ingênuo: cada thread lê 25 texels da entrada e faz a média — simples, funciona, relê muito. Otimizado: carregar um tile (10×10 = área do workgroup + borda de 2) em var<workgroup> com todos os threads cooperando, workgroupBarrier(), e cada thread lê os 25 do tile em memória rápida — menos acessos à global. Ou separar em dois passes 1D (horizontal + vertical) para 10 leituras em vez de 25. Consumo sem readback: a textura de saída do compute é usada como textura de entrada de um render pass simples (um quad em tela cheia com um fragment shader que a amostra) — os dados nunca voltam para a CPU.
Render pipeline
Objetivo: o pipeline vertex+fragment, vertex buffers, uniforms, texturas, o render pass, e a relação com Three.js.
5.1 O caminho de um triângulo
- Vertex buffer: posições (e cor, normal, uv) dos vértices; um vertex layout descreve o formato (ex.:
float32x3no offset 0). - Vertex shader (
@vertex): roda por vértice, produz a posição em clip space (@builtin(position)) e passa dados interpolados ao fragment. - Fragment shader (
@fragment): roda por pixel coberto, produz a cor (@location(0) vec4f). - Render pipeline: junta os dois shaders + vertex layout + formato de cor do alvo + blend + depth/stencil + topologia.
5.2 O render pass
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(), // o canvas
clearValue: { r:0, g:0, b:0, a:1 }, loadOp: 'clear', storeOp: 'store',
}],
});
pass.setPipeline(renderPipeline);
pass.setBindGroup(0, uniformsBindGroup);
pass.setVertexBuffer(0, vbo);
pass.draw(vertexCount);
pass.end();
device.queue.submit([encoder.finish()]);
5.3 O canvas
canvas.getContext('webgpu')+context.configure({ device, format, alphaMode }). Oformatvem denavigator.gpu.getPreferredCanvasFormat().- A cada frame:
context.getCurrentTexture()é o alvo; renderize e submeta dentro dorequestAnimationFrame.
5.4 Recursos de render
- Uniforms: matriz de projeção/view, tempo, resolução — num uniform buffer, atualizado com
writeBufferpor frame. - Texturas + samplers:
copyExternalImageToTexturepara subir uma imagem; mipmaps para qualidade. - Depth buffer para 3D; MSAA (multisample) para antialiasing das bordas.
- Instancing (
draw(count, instanceCount)) para muitos objetos iguais.
5.5 E o Three.js?
O Three.js tem um renderer WebGPU (WebGPURenderer) e um sistema de nós (TSL) que gera WGSL. Para a maioria dos apps 3D, use a engine — você ganha o fallback WebGL, o carregamento de modelos, a iluminação. Escreva WebGPU cru quando precisa de um pipeline que a engine não expõe, de compute integrado ao render, ou de controle total de performance. Ver Computação Gráfica & Three.js e 3D em Tempo Real & Motion Graphics para Web.
Perguntas: "O caminho de um vértice até o pixel em WebGPU?" (vertex buffer + layout → vertex shader (clip space) → rasterização → fragment shader (cor) → color attachment), "O que o render pipeline fixa?" (shaders, vertex layout, formato do alvo, blend, depth, topologia), "Como você desenha no canvas a cada frame?" (getCurrentTexture como alvo dentro do rAF), "Quando WebGPU cru vs Three.js?".
✏️ Exercício 5 — Full-screen effect
Você quer um efeito de pós-processamento (ex.: vinheta + grão) sobre uma cena já renderizada numa textura. Descreva o render pipeline: geometria, shaders, bindings, e por que essa é a estrutura de quase todo pós-processamento.
Gabarito (uma boa resposta): Geometria: nenhuma de verdade — um "triângulo em tela cheia" gerado no vertex shader a partir de @builtin(vertex_index) (3 vértices que cobrem o clip space; nem precisa de vertex buffer). Vertex shader: emite as 3 posições e as UVs correspondentes. Fragment shader: recebe a UV, amostra a texture_2d da cena (binding 0) com um sampler (binding 1), e aplica o efeito — multiplica pela vinheta (função da distância ao centro) e soma um grão (ruído baseado na UV + tempo, vindo de um uniform). Saída: a cor final. Bindings: a textura da cena, o sampler, e um uniform com tempo/resolução/parâmetros. Por que é o padrão: quase todo pós-processamento (blur, bloom, color grading, DOF, FXAA) é "para cada pixel da imagem, leia a(s) textura(s) de entrada e compute a saída" — um fragment shader sobre um quad/triângulo full-screen, encadeável em vários passes.
Performance e memória
Objetivo: minimizar submits e mudanças de estado, reusar buffers, alinhar dados, escolher o tamanho de workgroup, e medir com timestamps.
6.1 As alavancas
- Menos submits: agrupe o trabalho de um frame num só
queue.submit. - Menos mudanças de bind group / pipeline: ordene os draws por pipeline; reuse bind groups.
- Reuse de recursos: não crie buffers/pipelines por frame — pool e reuse; atualize buffers com
writeBuffer. - Sem readback no caminho quente (Módulo 4).
- Instancing e indirect draw/dispatch (a GPU decide quantos, a partir de um buffer) para evitar ida-e-volta.
6.2 Alinhamento
- Uniform buffers exigem alinhamento rígido: um
vec3ocupa 16 bytes, structs alinham a 16, arrays de escalares em uniform têm stride 16. Erro de alinhamento = dados lidos errados (silenciosamente). - Use ferramentas/tipos que calculam o layout (ou
@align/@sizeno WGSL), ou prefira storage buffers (regras mais flexíveis) para structs complexas.
6.3 Tamanho de workgroup
- Múltiplo do "wave size" do hardware (32 na NVIDIA, 64 na AMD) — 64 é um bom default; 128/256 às vezes melhor.
- Grande demais reduz occupancy (quantos workgroups cabem por unidade); pequeno demais desperdiça. Meça.
- Para grades 2D, 8×8 (=64) é comum.
6.4 Medir
- Timestamp queries (
timestamp-queryfeature): mede o tempo de GPU de um pass — a fonte de verdade (não meça comperformance.now()ao redor do submit, que é assíncrono). - DevTools de performance do navegador para o frame budget; a extensão/painel de WebGPU quando disponível.
- Contar draws, dispatches, bytes transferidos por frame.
6.5 Criação de recursos
- Criar pipelines compila shaders — caro. Use
createRenderPipelineAsync/createComputePipelineAsynce faça no boot, não no primeiro frame de uso. - Cache de pipelines por chave (shader + estado).
Perguntas: "Como você reduz o custo de um frame WebGPU?" (um submit por frame, menos mudanças de pipeline/bind group, reuso de recursos, sem readback, instancing/indirect), "Por que um vec3 num uniform pode ler errado?" (alinhamento a 16 bytes; usar layout correto ou storage buffer), "Como você mede a performance de um compute pass?" (timestamp queries — não performance.now ao redor do submit assíncrono), "Que tamanho de workgroup?" (múltiplo de 32/64, medir).
✏️ Exercício 6 — Otimize o loop
Um app de simulação de partículas está a 25 fps. O código, por frame, cria os buffers de posição/velocidade, cria o compute pipeline, faz o dispatch, faz readback das posições para a CPU, e então desenha as partículas lendo de um array JS. Liste as correções em ordem de impacto.
Gabarito (uma boa resposta): (1) Não criar buffers e pipeline por frame — criar uma vez no boot (com createComputePipelineAsync) e reusar; usar dois buffers de posição (ping-pong) que ficam na GPU. (2) Eliminar o readback — o buffer de posições atualizado pelo compute é usado diretamente como vertex buffer (ou storage lido no vertex shader) do render pass, sem voltar para a CPU nem para um array JS. Isso sozinho costuma dobrar/triplicar o fps. (3) Um único submit por frame contendo o compute pass + o render pass. (4) Instancing para desenhar as N partículas com um draw(6, N) (dois triângulos por partícula) em vez de N draws. (5) Ajustar o workgroup size (testar 64/128/256) e usar timestamp queries para ver onde vai o tempo. (6) Se ainda apertado, reduzir precisão (f16) e o número de partículas visíveis (culling no compute).
ML no navegador com WebGPU
Objetivo: por que WebGPU acelera inferência, o que é um kernel de matmul, os limites de memória da GPU do usuário, e WebNN como alternativa de alto nível.
7.1 Por que WebGPU para ML
- Inferência de redes neurais é, no fundo, multiplicação de matrizes em massa — o trabalho perfeito para milhares de threads.
- Antes do WebGPU, ML na web rodava em WASM (CPU, lento) ou WebGL (gambiarra de compute). Agora as libs usam compute shaders: transformers.js, WebLLM (LLMs), ONNX Runtime Web, MediaPipe — todas com backend WebGPU. Ver IA Local, On-Device & SLMs.
7.2 O kernel de matmul (a ideia)
- Ingênuo: um thread por elemento da matriz de saída
C[i,j] = Σ A[i,k]·B[k,j]— funciona, relê muito da memória global. - Otimizado: tiling — cada workgroup carrega blocos de A e B em
var<workgroup>, cada thread computa vários elementos de C reusando os dados do tile (aumenta a razão compute/memória). - Além disso:
f16, packing, evitar bank conflicts, quantização (pesos em int4/int8 desempacotados no shader — ver IA Local & SLMs). - Você raramente escreve isso — as libs trazem kernels afinados. Entender ajuda a diagnosticar (por que está lento? memória da GPU cheia? f16 não suportado?).
7.3 Memória
- Um modelo de 1B params em f16 = ~2 GB só de pesos — precisa caber na memória da GPU do usuário (variável, e o navegador reserva parte).
maxBufferSizeemaxStorageBufferBindingSizedo adapter limitam buffers individuais (frequentemente ~2 GB) → modelos grandes precisam ser fatiados em vários buffers. - Quantização (int8/int4) é o que torna modelos maiores viáveis no navegador.
7.4 WebNN — a alternativa de alto nível
- A Web Neural Network API expõe um grafo de operações de rede neural que o sistema operacional executa no melhor hardware (NPU, GPU, CPU) — em vez de você/a lib escrever kernels WebGPU.
- Emergente; onde disponível, pode ser mais rápida e eficiente em energia que WebGPU para ML. As libs de ML tendem a suportar os dois backends e escolher.
Perguntas: "Por que WebGPU acelera ML no navegador?" (inferência é matmul em massa — compute shaders com milhares de threads; antes era WASM/CPU ou WebGL-gambiarra), "O que torna um kernel de matmul rápido?" (tiling com memória de workgroup para reusar dados, f16, quantização), "O que limita o tamanho do modelo no navegador?" (memória da GPU do usuário e maxBufferSize — fatiar em buffers; quantizar), "O que é a WebNN?" (grafo de NN que o SO executa na NPU/GPU/CPU — alternativa de alto nível ao WebGPU para ML).
✏️ Exercício 7 — Rodar um modelo no cliente
Você quer rodar um modelo de classificação de imagem (~50 MB) 100% no navegador. Descreva a abordagem, o papel do WebGPU, o fallback, e os limites que você comunicaria ao usuário.
Gabarito (uma boa resposta): usar transformers.js ou ONNX Runtime Web com o modelo em ONNX (ou o formato da lib), quantizado se possível. A lib usa o backend WebGPU quando navigator.gpu existe (rápido); se não, cai para WASM (CPU) — funciona, mais lento. Baixar o modelo em segundo plano com barra de progresso e cachear (Cache Storage) para a segunda visita ser instantânea. Limites a comunicar/tratar: a primeira visita baixa ~50 MB; em dispositivos sem WebGPU a inferência pode levar segundos (mostrar "processando"); em celulares fracos, considerar rodar só a versão quantizada ou oferecer processar no servidor com consentimento; medir o tempo real em um Android mediano, não no desktop; se WebNN estiver disponível, a lib pode usá-la e ser mais eficiente. Ver IA Local & SLMs para o padrão geral de IA on-device.
Casos e padrões
Objetivo: os usos onde WebGPU se paga — simulação, processamento de imagem/vídeo, visualização massiva, física — e o padrão "compute produz, render consome".
8.1 Os casos
| Caso | Por que WebGPU |
|---|---|
| Simulação de partículas / N-body / boids | milhares a milhões de agentes atualizados em paralelo por frame |
| Fluidos, fumaça, água | grades densas com passes de advecção/pressão — compute puro |
| Processamento de imagem/vídeo | filtros, detecção de bordas, segmentação, efeitos em tempo real sobre cada pixel/frame |
| Visualização de dados massiva | milhões de pontos/linhas com pan/zoom a 60 fps; agregação e culling no compute (ver DataVis Avançada) |
| Física (colisões, soft bodies, cloth) | muitos elementos, muitas restrições resolvidas iterativamente |
| Geração procedural | terreno, ruído, marching cubes na GPU |
| ML (Módulo 7) | matmul em massa |
| Áudio (às vezes) | análise/síntese pesada fora do audio thread |
8.2 O padrão "compute produz, render consome"
- Um compute pass atualiza um buffer (posições das partículas, a grade do fluido, o resultado do filtro numa textura).
- Um render pass, no mesmo submit, usa esse buffer/textura como entrada (vertex buffer, storage lido no vertex shader, textura amostrada).
- Os dados nunca saem da GPU — é o que torna 60 fps possível.
- Para estado que persiste entre frames (velocidade, posição), use ping-pong: dois buffers, lê de um e escreve no outro, troca a cada frame.
8.3 Interação e agregação
- Pan/zoom de milhões de pontos: um compute pass faz o culling (o que está na viewport) e/ou a agregação (binning para densidade), e o render desenha só o necessário.
- Seleção/hover: um pass que testa a distância ao cursor e escreve o índice do mais próximo (ou um "pick buffer").
Perguntas: "Dê exemplos onde WebGPU se justifica" (simulação de partículas/fluidos, processamento de imagem/vídeo, viz de milhões de pontos, física, ML), "Explique 'compute produz, render consome'" (o compute pass atualiza um buffer/textura que o render pass no mesmo submit consome — sem readback), "O que é ping-pong de buffers e para que serve?" (dois buffers alternados para estado que evolui entre frames sem read/modify/write no mesmo buffer).
✏️ Exercício 8 — Boids (flocking)
Projete a arquitetura WebGPU de uma simulação de 100 mil "boids" (pássaros) com regras de separação/alinhamento/coesão, renderizados como triângulos, a 60 fps. Descreva buffers, passes, e como evita readback.
Gabarito (uma boa resposta): Buffers: dois buffers de estado (posição + velocidade) para ping-pong, cada um array<Boid> de 100k (storage). Um uniform com os pesos das regras, o raio de visão, o dt, e os limites do mundo. Compute pass: workgroup 64, dispatchWorkgroups(ceil(100000/64)); cada thread lê o boid i do buffer "atual", percorre os vizinhos (ingênuo: todos — O(n²), pesado a 100k; melhor: um grid espacial — outro compute pass que faz o binning dos boids em células, e cada boid só olha as células vizinhas), calcula a aceleração pelas 3 regras, integra velocidade e posição, e escreve no buffer "próximo". Render pass (mesmo submit): um draw(3, 100000) com instancing; o vertex shader lê a posição/velocidade do boid instance_index do buffer "próximo" (storage), orienta o triângulo pela direção da velocidade, e emite os 3 vértices. Sem readback: o buffer que o compute escreveu é lido direto pelo vertex shader; nada volta para a CPU. Trocar o par de buffers a cada frame. Se ainda lento, o grid espacial é obrigatório, e/ou reduzir o número de vizinhos amostrados.
Produção
Objetivo: feature detection e fallback, "device lost", limites do adapter, segurança e fingerprinting, bateria/térmico, e debugging.
9.1 Detecção e fallback
if (!navigator.gpu) { usarFallback(); } // WebGL2 ou CPU/WASM
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) { usarFallback(); }
- Ter um caminho WebGL2 (para render) ou WASM/CPU (para compute leve) — ou degradar a feature (menos partículas, sem o efeito).
- Bibliotecas (Three.js, transformers.js) já fazem isso — se você usa cru, é sua responsabilidade.
9.2 Device lost
- A GPU pode ser resetada (troca de GPU, driver crash, o SO recupera de um TDR, a aba fica em background muito tempo).
device.lostresolve com o motivo. - Ao perder o device: todos os recursos são inválidos. Você precisa recriar adapter, device, buffers, texturas, pipelines e re-subir os dados. Estruture o código para isso ser possível (uma função "init(device)" que reconstrói tudo).
9.3 Limites do adapter
adapter.limits:maxBufferSize,maxStorageBufferBindingSize,maxComputeWorkgroupSizeX/Y/Z,maxComputeInvocationsPerWorkgroup,maxComputeWorkgroupsPerDimension,maxTextureDimension2D… — variam por dispositivo (uma GPU integrada de celular tem limites bem menores).- Não hard-code "256 threads" e "buffer de 4 GB" — consulte os limites e adapte (ou peça limites maiores em
requestDevicee trate a falha). - Features opcionais (
timestamp-query,shader-f16,float32-filterable) — chequeadapter.featuresantes de pedir.
9.4 Segurança e privacidade
- O navegador isola: você não acessa memória de outras abas nem do SO; a validação bloqueia acessos fora dos limites.
- Fingerprinting: o modelo da GPU, os limites e as features são um sinal de identificação — os navegadores mitigam (buckets de limites, mascarar o nome), mas existe.
- Timing: como qualquer código de alta precisão, cuidado com canais laterais; o navegador limita a precisão de timers.
- Requer contexto seguro (HTTPS).
9.5 Bateria e térmico
- Compute e render pesados esquentam e drenam — em mobile, especialmente. Reduza o trabalho quando a aba não está visível (
document.hidden→ pausar o rAF), ofereça um modo "economia", limite o frame rate se a qualidade permite. - Teste em GPU integrada e em celular mediano — o que voa numa RTX engasga numa iGPU.
9.6 Debugging
- A validação do WebGPU dá mensagens de erro claras (bind group errado, usage flag faltando, formato incompatível) — leia o console.
device.pushErrorScope/popErrorScopepara capturar erros de um trecho.- Ferramentas: os painéis de WebGPU dos navegadores (quando disponíveis), captura de frame, e — para lógica de shader — imprimir valores escrevendo num buffer de debug e lendo de volta (não há
console.logno shader). - Testar o WGSL isoladamente com pequenos casos conhecidos antes de integrar.
Perguntas: "O que é 'device lost' e como você lida?" (a GPU pode resetar; todos os recursos ficam inválidos — estruture o código para recriar tudo via uma função init), "Por que consultar os limites do adapter?" (variam muito por dispositivo — iGPU de celular vs desktop; não hard-code), "WebGPU tem preocupações de privacidade?" (fingerprinting via modelo/limites/features — mitigado mas presente; HTTPS obrigatório), "Como você depura um compute shader?" (validação do WebGPU, error scopes, escrever num buffer de debug e ler de volta).
✏️ Exercício 9 — Checklist de produção
Você vai enviar uma feature de "visualização interativa de 2M de pontos" com WebGPU para um site com público amplo (desktop e mobile). Monte o checklist de produção.
Gabarito (uma boa resposta): Compatibilidade: feature-detect navigator.gpu + requestAdapter; fallback para uma versão WebGL2 (ou uma viz agregada/estática em Canvas 2D) quando indisponível; testar em Chrome, Safari, Firefox nas versões-alvo. Limites: consultar adapter.limits — se maxStorageBufferBindingSize não comporta 2M de pontos num buffer, fatiar; ajustar o workgroup size ao maxComputeInvocationsPerWorkgroup. Device lost: uma função init(device) que reconstrói todos os recursos; registrar device.lost e re-inicializar. Performance: um submit por frame, sem readback (culling/agregação no compute, render consome o buffer), instancing, timestamp queries no dev; alvo 60 fps na iGPU e 30+ no celular mediano — degradar (menos pontos, sem hover) se não alcança. Energia: pausar o rAF quando document.hidden; modo de baixa qualidade opcional. Robustez: error scopes ao redor da inicialização com mensagem amigável se falhar; não travar a página se o shader não compila. Build: WGSL como módulos/strings no bundle; pipelines criados async no boot. Acessibilidade: a viz não pode ser o único jeito de acessar os dados — oferecer uma tabela/resumo; respeitar prefers-reduced-motion para animações.
Mercado de trabalho: roadmap, entrevistas e portfólio
Objetivo: converter o conteúdo dos módulos em contratação — onde a habilidade é usada, um plano de estudo, um banco de perguntas e projetos que geram entrevista.
10.1 Onde WebGPU pesa
- Graphics / GPU engineer (web): engines, renderers, ferramentas criativas no navegador.
- ML infra (web/edge): os backends WebGPU/WebNN de libs de inferência no cliente.
- Creative technologist / generative artist: experiências visuais, instalações, sites premiados.
- DataViz engineer: visualizações interativas em escala que o D3/SVG não aguenta.
- Game / simulation dev (web): físicas, mundos, tempo real.
- Nicho e valorizado — poucas pessoas dominam GPU e web.
10.2 Roadmap de estudo (5–7 semanas)
| Semanas | Foco | Prática |
|---|---|---|
| 1 | Modelo mental e WGSL (Módulos 2–3) | Seguir o webgpufundamentals.org; desenhar um triângulo e um quad texturizado do zero |
| 2 | Compute (Módulo 4) | Um compute que dobra um array; uma redução paralela; um blur de imagem com tile |
| 3 | Render pipeline (Módulo 5) | Uma cena 3D simples (cubo girando, uniforms de MVP, depth); um efeito full-screen |
| 4 | Compute + render juntos (Módulo 8) | Partículas com ping-pong, "compute produz render consome", sem readback |
| 5 | Performance (Módulo 6) | Instrumentar com timestamp queries; otimizar o app de partículas; medir na iGPU |
| 6 | ML e produção (Módulos 7, 9) | Rodar um modelo com transformers.js (backend WebGPU); adicionar fallback e device-lost handling |
| 7 | Portfólio | Publicar as demos (com fallback) e um write-up técnico |
10.3 Banco de perguntas (com a resposta que aprova)
Júnior/pleno — "O que WebGPU adiciona sobre WebGL?"
Compute shaders de verdade (WGSL), menos overhead de driver (comandos gravados e submetidos em lote, estado explícito via pipelines/bind groups), suporte a multithreading, e uma API moderna mapeada para Vulkan/Metal/D3D12. É o mesmo salto que essas APIs foram sobre OpenGL.
Pleno — "Descreva o fluxo de uma operação em WebGPU."
adapter → device → queue. Criar recursos (buffers/texturas) e pipelines (compilam shaders, fixam estado). Um command encoder abre passes (compute ou render), grava setPipeline/setBindGroup/dispatch ou draw, finish() vira um command buffer, queue.submit executa — assíncrono. Ler de volta é outra operação assíncrona (mapAsync) e cara.
Pleno — "O que é um workgroup e como escolher o tamanho?"
Um grupo de threads (invocations) que compartilham var<workgroup> e podem sincronizar com workgroupBarrier(). Você faz dispatchWorkgroups(gx,gy,gz) e cada thread sabe seu global_invocation_id. Tamanho: múltiplo do wave size (32 NVIDIA, 64 AMD), 64 como default, total ≤ ~256, e medir — grande demais reduz occupancy.
Pleno — "Por que o readback CPU↔GPU é caro e como evitar?"
Ele sincroniza e tem latência — mata o pipeline se feito por frame. Evita-se deixando o resultado do compute num buffer/textura que o render pass (ou o próximo compute) consome direto, no mesmo submit — "compute produz, render consome". Para estado que evolui, ping-pong de dois buffers.
Sénior — "Como você leva uma feature WebGPU para produção com público amplo?"
Feature-detect + fallback (WebGL2 ou WASM/CPU ou degradar a feature); consultar adapter.limits e adaptar (iGPU de celular tem limites bem menores); tratar device.lost recriando todos os recursos via uma função init; um submit por frame sem readback; timestamp queries para medir; pausar quando a aba não está visível; testar em iGPU e celular; error scopes com mensagem amigável.
Sénior — "Por que WebGPU acelera ML no navegador?"
Inferência de redes é matmul em massa — o trabalho ideal para milhares de threads em compute shaders. Antes era WASM (CPU, lento) ou WebGL (gambiarra). Libs como transformers.js, WebLLM e ONNX Runtime Web usam o backend WebGPU. Os limites são a memória da GPU do usuário e maxBufferSize — daí a quantização e o fatiamento de modelos grandes. A WebNN é a alternativa de alto nível (o SO escolhe NPU/GPU/CPU).
Armadilha — "Vou escrever tudo em WebGPU cru para performance"
Para 3D comum, Three.js/Babylon (que têm backend WebGPU e fallback) entregam quase a mesma performance com uma fração do esforço e do risco. WebGPU cru se justifica em compute pesado, render muito customizado, ou quando você precisa integrar compute e render de forma que a engine não expõe. E sempre com fallback — nem todo dispositivo tem.
10.4 Projetos de portfólio que geram entrevista
- Simulação de partículas/boids (âncora): 100k+ agentes, compute + render no mesmo submit, ping-pong, sem readback, 60 fps, com fallback e device-lost handling — e um write-up da arquitetura e das otimizações.
- Pipeline de processamento de imagem: uma cadeia de efeitos (blur separável, detecção de bordas, color grading) via compute, com timestamps mostrando o custo de cada passe.
- Viz de milhões de pontos: scatter/mapa com pan/zoom a 60 fps, culling e agregação no compute, com uma tabela/resumo acessível como alternativa.
- Kernel de matmul + comparação: um matmul ingênuo e um com tiling, medidos, comparados com a CPU (WASM) — mostra que você entende a GPU.
- Modelo de ML no cliente: transformers.js com backend WebGPU + fallback WASM, com o estudo de download/cache e latência em vários dispositivos.
10.5 Fontes para continuar
- Aprender: webgpufundamentals.org (o melhor tutorial), WebGPU Samples (github.com/webgpu/webgpu-samples), MDN (WebGPU API), Learn Wgpu (Rust, mas os conceitos), os artigos do time do Chrome (developer.chrome.com / web.dev).
- Spec: a WebGPU e a WGSL specs do W3C; o repositório gpuweb/gpuweb.
- Comunidade: os posts de Surma e de Brandon Jones (Toji); o Discord do WebGPU; Shadertoy e Compute.toys para inspiração de shader.
- ML: docs de transformers.js, WebLLM, ONNX Runtime Web; a WebNN spec.
- Nesta trilha: Computação Gráfica & Three.js, 3D em Tempo Real & Motion Graphics para Web, Spatial Computing, XR & WebXR, IA Local, On-Device & SLMs, DataVis Avançada com Python & Web Design, CSS Avançado: Animações & SVG, Rust: Sistemas, CLI & WebAssembly, Motion de Interface.
Cinco ideias sustentam o WebGPU: (1) é render + compute no navegador com uma API moderna — o WebGL só desenhava; (2) o modelo é "grave comandos e submeta" (adapter → device → queue; nada é imediato) e o readback é caro — deixe os dados na GPU; (3) WGSL organiza o trabalho em workgroups de threads (múltiplo de 32/64) com memória compartilhada e barreiras; (4) o padrão vencedor é "compute produz, render consome" no mesmo submit, com ping-pong para estado; (5) em produção, feature-detect + fallback, tratar device lost, consultar os limites do adapter, e medir com timestamp queries — nem todo dispositivo tem, e os que têm são muito diferentes.