DFloat11: compressão sem perda para inferência em GPU que reduz LLMs para 70% do tamanho
(arxiv.org)- LLMs grandes são difíceis de implantar por causa dos limites de memória da GPU, e o DFloat11 reduz pesos em BFloat16 para cerca de 70% do tamanho enquanto mantém uma saída idêntica bit a bit à original
- A ideia central é que o expoente de 8 bits do BFloat16 na prática carrega só cerca de 2,6 bits de informação; o sinal e a mantissa são preservados, e apenas o expoente é comprimido com Huffman coding
- Como codificação de comprimento dinâmico tende a virar gargalo na GPU, o DFloat11 usa LUT hierárquica, kernel em 2 etapas e compressão/descompressão por bloco de transformer, adaptado para inferência paralela
- Em modelos como Llama 3.3, Qwen 3, Mistral 3, DeepSeek R1 Distilled, FLUX.1 e Stable Diffusion 3.5, ele mostrou cerca de 30% de redução no tamanho do modelo com preservação da saída original
- Em comparação com a alternativa de CPU offload, a vazão na geração de tokens é 2,3–46,2x maior e permite inferência sem perda do Llama 3.1 405B, com 810GB, em um único nó com 8 GPUs de 80GB
O gargalo de memória que o DFloat11 quer resolver
- Modelos de base como LLMs e Diffusion Models estão crescendo rapidamente, o que dificulta sua implantação eficiente em hardware com restrição de memória
- O Llama 3.1 405B armazena 405 bilhões de parâmetros no formato BFloat16 e precisa de cerca de 810GB de memória para a inferência completa
- Isso excede a capacidade de um servidor GPU avançado típico com 8×80GB de GPU
- Se vários nós passam a ser necessários, o custo de implantação e a barreira de acesso aumentam
- O DFloat11 tem como objetivo comprimir modelos em BFloat16 para cerca de 70% do tamanho original, preservando 100% da precisão em qualquer tarefa
Uma abordagem diferente da quantização com perda
- Quantização é uma forma de compressão com perda que reduz a precisão dos pesos para economizar memória
- Ela pode reduzir bastante o uso de memória e acelerar a inferência, mas introduz erro de aproximação
- A perda de precisão varia conforme o modelo base, o método de quantização, o benchmark de avaliação e o bit-width alvo, então é difícil prever o impacto
- Por exemplo, ao aplicar SmoothQuant de 8 bits ao DeepSeek-R1-Distill-Qwen-1.5B, a precisão média em tarefas de raciocínio cai 9,09%
- Mesmo quando os indicadores gerais de precisão são parecidos, modelos quantizados podem apresentar comportamento de resposta diferente do original
- Dutta et al. observaram o fenômeno de flips, em que uma resposta correta vira incorreta, ou uma incorreta vira correta
- O Qwen2-1.5B quantizado com W8A16 GPTQ tem queda de apenas 0,3% na precisão 8-shot do GSM8K, mas o estado de acerto/erro muda em 6,37% das respostas
- Em áreas como finanças e saúde, mudanças na saída de modelos quantizados podem dificultar o atendimento a exigências de regulação e confiabilidade
- A compressão sem perda existente para modelos costuma focar mais em eficiência de armazenamento de checkpoint, redução do tempo de download em hubs de modelos ou hardware especializado como FPGA, com pouco ganho para inferência em GPU comum
O espaço de compressão ainda disponível no expoente do BFloat16
- O BFloat16 divide 16 bits em 1 bit de sinal, 8 bits de expoente e 7 bits de mantissa
- O ponto de partida do DFloat11 é a análise da entropia de Shannon de cada componente BFloat16 nos pesos de LLMs
- A entropia do sinal e da mantissa fica próxima de seus respectivos bit-widths, então há pouco espaço para compressão
- Já o expoente, embora tenha 8 bits alocados, apresenta entropia de apenas cerca de 2,6 bits
- A distribuição dos valores de expoente é muito desequilibrada
- Dos 256 valores possíveis em 8 bits, só cerca de 40 são usados
- Os demais valores não aparecem
- A frequência também cai rapidamente conforme o ranking
- Graças à baixa entropia, o expoente vira um bom alvo para compressão sem perda, com cerca de 5,4 bits de informação de expoente passíveis de compressão
O formato DFloat11
- DFloat11 ou DF11 é um formato de ponto flutuante de comprimento dinâmico que comprime por entropy coding apenas o expoente de pesos em BFloat16
- A árvore de Huffman é construída com base na distribuição de expoentes dos pesos do modelo
- Valores de expoente mais frequentes recebem códigos curtos
- Valores raros recebem códigos longos
- O sinal e a mantissa são mantidos exatamente como no original
- O expoente é armazenado em forma bit-packed no array de bytes
EncodedExponent - O sinal e a mantissa são armazenados separadamente no array de bytes
PackedSignMantissa
- O expoente é armazenado em forma bit-packed no array de bytes
- Como resultado, pesos em BFloat16 caem para uma média de cerca de 11 bits, e podem ser restaurados ao valor BFloat16 original sem perda de precisão
Descompressão adaptada para inferência em GPU
- Como pesos com entropy coding usam codificação de comprimento dinâmico, eles não podem ser enviados diretamente para multiplicação de matrizes
- A matriz de pesos necessária precisa ser descomprimida imediatamente de volta ao BFloat16 original
- Quando a multiplicação termina, a matriz em BFloat16 é descartada logo em seguida para economizar memória da GPU
- A decodificação Huffman tradicional percorre a árvore sequencialmente no nível de bits, o que não combina com a estrutura paralela da GPU
- Se uma única thread fizer a descompressão, a utilização da GPU cai e a latência aumenta
Decodificação com LUT hierárquica
- O DFloat11 usa decodificação baseada em lookup table em vez de percorrer a árvore de Huffman
- Se o comprimento máximo do código Huffman for L, uma LUT única exigiria tamanho
2^L- Em LLMs, L normalmente fica entre 24 e 32
- Uma LUT com
2^32entradas é grande demais para caber de forma prática na SRAM da GPU
- Para evitar isso, a árvore de Huffman é dividida em subárvores sem sobreposição de altura 8, e cada subárvore vira uma LUT compacta de 256 entradas
- Na LUT hierárquica, algumas entradas precisam atuar como referência para LUTs inferiores
- Isso aproveita o fato de que muitos valores de expoente não são usados nos expoentes de LLMs
- Valores não usados no intervalo 240–255 são reaproveitados como ponteiros internos
- Esses valores representam magnitudes muito grandes, da ordem de ±2^113 a ±2^128, e não aparecem nos pesos de LLMs
- Nos experimentos, o número k de LUTs compactas na árvore de Huffman de expoentes em BFloat16 fica entre 4 e 8
- Junto com
CodeLengths, isso usa no máximo(8 + 1) × 256 bytesde memória - Esse tamanho cabe na SRAM e permite lookups repetidos com alta velocidade
- Junto com
Kernel de GPU em 2 etapas e metadados auxiliares
- Cada thread da GPU fica responsável por um trecho contínuo de n bytes do expoente codificado
- Nos experimentos, foi usado n = 8
- A thread decodifica os códigos Huffman que começam dentro do seu próprio trecho
- A natureza dinâmica do comprimento dos códigos cria dois problemas
- A posição exata do bit inicial de cada thread não é óbvia
- Exceto pela primeira thread, é difícil saber o índice de saída do elemento decodificado
- O primeiro problema é resolvido com o array Gaps
- Gaps tem uma entrada por thread
- Cada entrada indica o offset em bits do primeiro código Huffman válido com base no byte inicial da thread
- Como o comprimento máximo do código é 32 bits, o offset fica no intervalo
[0, 31]e é armazenado em 5 bits
- O problema da posição de saída é tratado armazenando apenas a posição por bloco de threads, reduzindo o overhead de memória
- Se cada thread armazenasse uma posição de saída de 32 bits, o overhead seria alto em matrizes de pesos com dezenas de milhares de threads
- O DFloat11 armazena apenas a posição de saída do primeiro elemento de cada bloco de threads
- O kernel opera em duas etapas
- Na etapa 1, cada thread decodifica seu trecho e apenas conta o número de elementos, sem escrever na HBM
- As threads dentro do bloco executam prefix sum com o algoritmo de Blelloch para calcular a posição de saída por thread
- Na etapa 2, o mesmo trecho é decodificado novamente, e os valores decodificados são escritos no buffer de escrita da SRAM conforme a posição calculada
- O expoente codificado é carregado para a SRAM antes do primeiro passe para evitar acesso duplicado à memória global
- Depois que todos os expoentes decodificados são escritos na SRAM, é feito um único coalesced write para a HBM
Descompressão por bloco de transformer
- A descompressão de uma única matriz de pesos pode ser pequena demais para aproveitar totalmente os recursos da GPU
- À medida que o tamanho da matriz cresce, o throughput de descompressão do DFloat11 melhora
- Descomprimir várias matrizes juntas aumenta o throughput e esconde a latência
- Todas as matrizes de pesos em DFloat11 dentro de um bloco de transformer são descomprimidas como um único batch
- Essa descompressão em lote é feita imediatamente antes do forward pass daquele bloco de transformer
- O token embedding e a language modeling head do LLM também são alvos de compressão
- Essas matrizes já são grandes o suficiente para saturar os recursos da GPU, então não precisam de batching separado
Resultados da avaliação e efeito prático
- O DFloat11 foi avaliado em LLMs e diffusion transformers, incluindo Llama 3, Qwen 3, Mistral 3, DeepSeek R1 Distilled, FLUX.1 e Stable Diffusion 3.5
- Os resultados mostram cerca de 30% de redução no tamanho do modelo com preservação completa da saída original
- A saída é bit-for-bit idêntica à do modelo original
- Como não é compressão com perda, a precisão original dos pesos em BFloat16 é preservada
- Em comparação com a alternativa de descarregar parte de um modelo não comprimido para a CPU para caber na memória, o DFloat11 alcança throughput 2,3–46,2x maior na geração de tokens
- Com um orçamento fixo de memória de GPU, ele permite comprimento de geração 5,7–14,9x maior que o modelo não comprimido
- Embora o Llama 3.1 405B tenha 810GB de tamanho, o DFloat11 viabiliza inferência sem perda em um único nó com 8 GPUs A100 de 80GB
- Esses resultados mostram que é possível reduzir pela metade os requisitos de hardware para rodar o Llama-3.1-405B, mantendo inferência sem perda de accuracy
1 comentários
Comentários no Hacker News
Isso é apenas consequência do fato de que a faixa dinâmica do bfloat16 é muito ampla, mas, na prática, essa faixa não é usada por completo.
As pessoas gostam que hiperparâmetros pareçam 0,01, e não 10^10, mas é possível usar a mesma precisão relativa para cada expoente. Mesmo que você multiplique por 10^6 todos os hiperparâmetros da rede, pesos inicializados, dados de treinamento etc., ela em geral se comporta de forma parecida, porque a região superior quase não é usada. Algumas funções especiais podem ser exceções, porém.
A entropia típica dos valores bfloat16 vista em pesos e ativações fica em torno de 10 a 12 bits e, na prática, só cerca de 65% a 75% da faixa de valores é usada. O sinal e os bits da mantissa são quase ruído difícil de comprimir.
Essa propriedade já foi explorada várias vezes tanto em computação de alto desempenho clássica quanto em IA. Exemplos incluem o trabalho de compressão sem perdas do laboratório de Martin Burtscher (https://userweb.cs.txstate.edu/~burtscher/), o fpzip do LLNL (https://computing.llnl.gov/projects/fpzip) e minha biblioteca dietgpu de 2021 (https://github.com/facebookresearch/dietgpu). Em grandes clusters de GPU, nós comprimíamos sem perdas todos os dados antes da transferência, como gradientes ou pesos vindos de backups, e descomprimíamos no recebimento, reduzindo o tempo total de treinamento em tempo de relógio em cerca de 10%; como era sem perdas, os resultados dos cálculos continuavam os mesmos de antes.
Além disso, rANS é mais eficiente do que codificação de Huffman em conjuntos de instruções do tipo SIMD e também é mais fácil de implementar. No DFloat11, também é preciso descomprimir antes das operações aritméticas, então dá para reduzir perdas de latência e throughput.
Quantização é uma ferramenta essencial para quem roda LLM localmente, e normalmente a RAM é o gargalo. Também fico curioso se existe uma compressão sem perdas melhor para pesos BF16.
O DFloat11 parece poder ser encaixado com relativa facilidade nos fluxos de trabalho de quantização existentes, mas você parece bastante cético em relação ao artigo, então quero entender o que estou deixando passar.
Em uma camada de rede neural, a entrada é multiplicada pelos pesos e somada, e essa saída vira a entrada da próxima camada; esse processo pode se repetir mais de cem vezes. Ao chegar à camada final de saída, esse fator de 10^6 terá sido aplicado muitas vezes e vai crescer como uma bola de neve até algo na ordem de 10^600.
Os únicos casos em que o DFloat11 teve mais tokens por segundo foram em comparação com inferência usando offload de algumas camadas para a CPU.
É o típico trade-off espaço-velocidade da ciência da computação; não existe almoço grátis.
Ainda assim, o custo de truncar e preencher com zeros é pequeno.
O que mais chama atenção são as implicações práticas. Permitir inferência sem perdas de um modelo de 405B parâmetros em um único nó com 8 GPUs de 80 GB é bastante impressionante.
Isso pode destravar muito para laboratórios e startups que querem rodar modelos de fronteira sem custos gigantescos de infraestrutura.
Sou grato por viver tempos tão interessantes. Cada vez que abro o HN, vejo novidades sobre machine learning e modelos Transformer.
Preciso ler com mais profundidade, mas fico curioso se o llama.cpp usa algum tipo de kernel customizado junto com cuBLAS, ou se apenas aproveita bem os kernels do cuBLAS.
2 semanas? Dois meses? Dois dias? 2 minutos?
Às vezes, todas estão certas. Tempos realmente interessantes.
Quando essa guerra dos formatos de pesos se resolver, o hardware poderá ser feito para dar suporte a isso. Seja qual for o formato de pesos definido como razoavelmente ótimo, vamos precisar de hardware de multiplicação de matrizes otimizado para ele.
Em casos reais de uso de agentes, muitas vezes é difícil equilibrar qualidade, custo e desempenho. Essa técnica pode ajudar a evitar os trade-offs criados por técnicas de quantização, incluindo resultados imprevisíveis ao tentar otimizar o custo de agentes.
Se o DFloat11 permitir encaixar o modelo em GPUs mais baratas, em certos casos a redução de custos pode ser considerável. Trabalho na xmad.ai
Comparado à alternativa de descarregar para a CPU parte de um modelo não comprimido por causa de restrições de memória, o DFloat11 teria uma vazão de geração de tokens de 1,9 a 38,8 vezes maior. Com um orçamento fixo de memória de GPU, ele permite comprimentos de contexto 5,3 a 13,17 vezes maiores do que um modelo não comprimido
Só pelo comprimento de contexto já parece útil mesmo que o modelo caiba na memória, mas, partindo do entendimento básico de que LLMs muitas vezes ficam limitados pela largura de banda de memória, fico curioso se o número de tokens por segundo também melhora quando tudo está na GPU
O artigo afirma menos de 200 GB/s em uma A100 e, pelos benchmarks, parece ser 1,5 a 4 vezes mais lento com tamanho de lote 1, dependendo da GPU e do modelo. Claro que, se o tamanho do lote ficar grande o suficiente, esse overhead praticamente desaparece
Outros codecs sem perda conseguem chegar a 600 GB/s no mesmo hardware, então parece haver espaço para melhoria. Mas a largura de banda bruta de memória da A100 é de 1,6 TB/s
Isso significa que é possível reduzir em 30% de forma geral os requisitos de memória de LLMs não quantizados? Se for verdade, é algo bem grande
Se puder ser somada à quantização existente, certamente é interessante, mas a quantização K também já usa precisões diferentes por camada conforme o impacto geral na perplexidade. Por exemplo, a Q6 mistura 4 bits e 8 bits, o que é parecido com a métrica de entropia usada aqui. Considerando também a imatrix calibrada, conceitualmente ela comprime de forma mais agressiva, de um jeito parecido com FFT
Isso é diferente do ZipNN? https://arxiv.org/pdf/2411.05239
Vejo a menção, mas não entendo se é baseado nisso ou se é diferente/melhor
Mas compressores do tipo LZ não parecem fazer muito sentido para pesos de redes neurais. Eles não têm tanta redundância quanto a maioria dos dados de texto, que têm muitas repetições, e, se os dados não forem muito esparsos, talvez não haja repetições suficientes para compensar o overhead do dicionário
Se você adicionar um compressor do tipo LZ e colocá-lo no caminho crítico da inferência, a descompressão ficará muito mais lenta. O melhor é fundir a descompressão com o kernel de computação. Por exemplo, dá para fazer algo como um GEMM que descomprime cada tile antes das operações aritméticas, e quanto mais simples for a rotina de descompressão, mais fácil isso fica
É bem legal ver o quanto tudo isso está avançando rápido. Parece que toda semana surge uma nova técnica de eficiência ou um upgrade de hardware
É fácil se distrair com essas melhorias
Dá para rodar isso também em modelos novos? Se não entendi errado, o código parece ser apenas para inferência