Os núcleos de atenção rápida e a fusão de operadores reorganizam o cálculo dos transformers para limitar os movimentos de dados e os lançamentos de núcleos no acelerador. Estas otimizações deixam em princípio inalterados os pesos do modelo e a sua função matemática, ainda que a ordem das operações e a precisão possam produzir pequenas diferenças numéricas. O tutorial publicado pelo Machine Learning Mastery detalha estas técnicas, com exemplos de código.
Uma camada de transformer baseia-se em operações comuns: projeções lineares, atenção, normalização, funções de ativação e adições residuais. Uma implementação direta lança um núcleo de cálculo distinto, chamado «kernel», para cada operação, e escreve cada tensor intermédio na memória do acelerador. O cálculo mantém-se correto, mas o tempo de execução pode ser dedicado mais ao movimento dos valores e ao lançamento dos núcleos do que aos cálculos úteis.
Fusão de operadores
O bloco SwiGLU, usado por muitos modelos com apenas descodificador, ilustra este problema. Uma implementação direta lança multiplicações de matrizes para as projeções, um núcleo para a função SiLU, outro para a multiplicação elemento a elemento, e depois uma última multiplicação de matrizes. Em cada fronteira, um tensor intermédio pode ser escrito na memória de alta largura de banda (HBM) e depois relido. A fusão consiste em combinar estas etapas num único núcleo. Ela poupa tempo quando os movimentos de memória e os lançamentos pesam muito, e muito menos quando uma grande multiplicação de matrizes já domina a execução. A escolha do núcleo mantém-se empírica: é necessário perfilar a totalidade da carga de trabalho.
Outras fusões são comuns nos transformers, como a adição de uma ligação residual seguida de uma normalização RMSNorm ou LayerNorm, ou a combinação da desquantização com uma multiplicação de matrizes. A fusão tem, contudo, os seus limites: operações com organizações de dados incompatíveis ou um estado intermédio muito volumoso combinam-se mal. Um núcleo demasiado grande pode aumentar a pressão sobre os registos, reduzir a taxa de ocupação e tornar-se mais lento do que vários núcleos bem ajustados.
Atenção do tipo FlashAttention e atenção paginada
Uma atenção clássica calcula uma matriz de pontuações de tamanho T² por cabeça, sendo T o comprimento da sequência. Num GPU, produzi-la, escrevê-la na memória global e depois relê-la consome muita largura de banda, o que pode tornar-se um estrangulamento durante os longos pré-preenchimentos («prefill»). O FlashAttention processa as consultas, as chaves e os valores por blocos. Para cada um, calcula uma parte das pontuações, atualiza as estatísticas do softmax e acumula a saída correspondente na memória situada no chip, sem armazenar a matriz completa. O PyTorch expõe esta otimização através da função scaled_dot_product_attention(). O tutorial especifica que impor um backend de forma rígida pode fazer falhar o código se nenhum backend suportar as entradas ou o hardware.
A cache KV, que conserva as chaves e valores já calculados, cresce um token de cada vez, para um comprimento final imprevisível. Uma alocação contígua por pedido deixa, portanto, espaço reservado e não utilizado. A cache paginada divide o armazenamento em blocos de tamanho fixo e apoia-se numa tabela de blocos que liga as posições lógicas aos blocos físicos. O núcleo de atenção deve então traduzir cada posição através desta tabela. A paginação não reduz o número de elementos lógicos, mas limita a fragmentação e a capacidade reservada.
No exemplo fornecido, o vLLM usa o modelo Qwen/Qwen2.5-1.5B-Instruct com blocos de 16 tokens (block_size=16) e o backend «TRITON_ATTN». Segundo o tutorial, o vLLM verifica a compatibilidade do backend com o modelo e o hardware durante a inicialização, e sinaliza um erro em caso de incompatibilidade.
Fusão e CUDA Graphs com torch.compile
A função torch.compile() captura uma sequência de operações com o TorchDynamo, e depois compila-a com o TorchInductor, o seu backend por defeito. O TorchInductor pode gerar um único núcleo Triton para operações compatíveis que o modo padrão lançaria separadamente. As interrupções de grafo, devidas por exemplo a um controlo de fluxo dependente dos dados ou a conversões de tensores em escalares Python, limitam as zonas que o compilador pode otimizar.
O modo «reduce-overhead» pede ao TorchInductor que use CUDA Graphs quando o grafo capturado for elegível para tal. Um CUDA Graph regista a sequência das operações GPU e reexecuta-a com um único envio ao processador. Ele não cria fusão adicional, complementa a do TorchInductor. Exige endereços de memória estáveis e formas compatíveis, e pode consumir mais memória. O tutorial recomenda verificar os resultados com tolerâncias adaptadas ao tipo de dados, perfilar a execução e medir o desempenho de ponta a ponta.
