Pular para o conteúdo
13/30Capítulo 13 de 30

Barateando a inferência: KV cache, batching e quantização

O mesmo modelo responde em 8,8 s e 78,9 s com saída idêntica. Depois, INT4 medido de três formas — não só afirmado.

Nesta página

O mesmo modelo, na mesma máquina, respondendo à mesma pergunta com os mesmos 48 tokens. As duas saídas são idênticas token por token — verificado, não presumido.

TEXT
with a key-value cache:     8.85 s   ( 6.01 tokens/second)
without a key-value cache: 78.95 s   ( 0.60 tokens/second)

Um argumento mudou: use_cache=False. Nada no modelo, no prompt, no sampling ou na aritmética é diferente, e a segunda execução não fica mais precisa pelo esforço. Ela é nove vezes mais lenta sem ganhar nada.

Esse é o formato deste capítulo. Tudo nele — o cache, o batch, os pesos quantizados — é uma tentativa de parar de pagar por trabalho que não muda a resposta, ou de descobrir quanto custa uma resposta mais barata. O Capítulo 10 estabeleceu a tabela de preços do treinamento. Esta é a tabela de preços do lado pelo qual você paga para sempre: um modelo em produção gasta cerca de 2N2N FLOPs para cada token que emite, em cada solicitação, pelo resto da vida.

Para gerar um token, um transformer decoder-only pega toda a sequência até agora, passa por todas as camadas e lê a distribuição de probabilidade na última posição. Então acrescenta o token escolhido e faz tudo de novo. Essa descrição está correta, e é isso que a execução lenta faz.

Ela também é enormemente desperdiçadora, e o motivo é a máscara causal do Capítulo 9. Os vetores de key e value da posição 7 são computados a partir da entrada da posição 7 e das posições anteriores. Quando a posição 8 chega, a posição 7 não pode vê-la — é isso que causal significa —, portanto as key e value da posição 7 são exatamente os mesmos números de antes. A execução lenta os recalcula mesmo assim, a cada passo.

Então armazene-os. Esse armazenamento é o key-value cache, a otimização mais decisiva no serving de modelos de linguagem:

generate.pyPYTHON
out = model(prompt_ids, use_cache=True)          # prefill: the whole prompt
past = out.past_key_values                        
nxt = out.logits[:, -1].argmax(-1, keepdim=True)

for _ in range(n - 1):
    out = model(nxt, past_key_values=past, use_cache=True)   
    past = out.past_key_values                                
    nxt = out.logits[:, -1].argmax(-1, keepdim=True)

Veja o que é alimentado ao modelo dentro do loop: nxt, um token. Não a sequência. A query do novo token faz attention contra cada key em cache, e as keys em cache nunca iriam mudar. Isto não é uma aproximação — a verificação de saída idêntica acima é o ponto. O cache não troca qualidade por velocidade; ele remove aritmética redundante.

Para ver a escala com clareza, retire o transformer e meça uma única cabeça de attention com d=64d = 64, um passo de geração computado das duas formas:

tokens no contextorecalcular tudocom cacherazãomatriz de scores
1280,59 ms0,062 ms10x65.536 B vs 512 B
2561,20 ms0,163 ms7x262.144 B vs 1.024 B
5127,03 ms0,078 ms90x1.048.576 B vs 2.048 B
102417,31 ms0,114 ms152x4.194.304 B vs 4.096 B
204859,83 ms0,214 ms279x16.777.216 B vs 8.192 B
4096236,18 ms0,284 ms832x67.108.864 B vs 16.384 B

A coluna da direita é a causa. Recalcular constrói a matriz de attention completa n×nn \times n a cada passo — o O(n2)O(n^2) da caixa de notação assintótica do Capítulo 9, pago uma vez por token. Com o cache, você constrói uma linha 1×n1 \times n: com 4.096 tokens, 67 MB de scores contra 16 KB.

Contar multiply-accumulates em vez de milissegundos remove a máquina do argumento. Para gerar TT tokens a partir de uma partida fria:

tokens geradoscom cacherecalculandorazão
1282,6 M192,0 M73x
51223,1 M7,36 G318x
2048293,7 M392,6 G1.336x

Por passo, a versão com cache é linear no contexto e a sem cache é quadrática; somadas ao longo de uma geração, O(T2)O(T^2) contra O(T3)O(T^3), com a razão crescendo sem limite. A diferença de nove vezes da abertura foi medida em 48 tokens — antes até da primeira linha dessa tabela.

O cache também muda o que precisa estar na memória. Em uma GPU de laptop de 8 GB gerando 256 tokens em fp16, pegando o pico do alocador e subtraindo os pesos residentes:

pico de memória de trabalho
com cache21,8 MB
recalculando181,7 MB

8,3 vezes mais memória, gasta para produzir os mesmos tokens mais lentamente. Esta é a promessa feita no Capítulo 5, chegando por uma direção inesperada: ali, autodiff em modo reverso precisava manter todos os intermediários vivos para o backward pass, e as ativações dominavam a memória de treinamento. Na inferência não há backward pass e nada a reter para ele — então o que domina a memória é o cache, e ele é uma escolha deliberada, não um custo inevitável.

Prefill e decode são duas máquinas diferentes

Link para a seção: Prefill e decode são duas máquinas diferentes

Olhe de novo para a execução rápida: seu primeiro token se comportou de modo diferente dos outros quarenta e sete.

TEXT
prefill, 40 prompt tokens : 1.0224 s   ->  25.6 ms per token
decode,  47 steps         : 0.1665 s mean per step

O prompt custou 25,6 ms por token e cada token gerado custou 166 ms. Mesmo modelo, mesmo hardware, mesmos pesos, uma diferença de seis vezes por token — e no sentido que a maioria das pessoas não espera. O prompt é a parte barata. A geração se divide em duas fases com física genuinamente diferente:

Uma passagem forward sobre todo o prompt. Todos os tokens são processados em paralelo, então cada matriz de pesos é carregada da memória uma vez e multiplicada por uma matriz com centenas de vetores de tokens — um produto matriz-matriz, com muita aritmética por byte movido, que é exatamente para isso que uma GPU foi feita. Prefill é compute-bound, e seu custo é aproximadamente linear no tamanho do prompt.

Uma passagem forward por token, batch de um e sequência de um. Cada matriz de pesos ainda é carregada integralmente da memória e multiplicada por um único vetor — um produto matriz-vetor, com quase nenhuma aritmética por byte movido. Decode é memory-bandwidth-bound, e seu custo por token quase não depende do tamanho do contexto.

As duas metades são mensuráveis. Prefill, uma passagem sobre PP tokens:

tokens do promptsegundosms por token
160,351521,97
320,525416,42
641,049116,39
1281,655212,93
2563,096512,10

Decode, um token contra um cache de CC:

tokens em cachems para um token
16110,05
6497,57
256108,53
1024103,86

Leia a segunda tabela duas vezes. Ir de 16 tokens de contexto para 1.024 — sessenta e quatro vezes mais histórico para fazer attention — não mudou o custo de um passo de modo mensurável. A attention contra o cache é trabalho real, mas é esmagada pelo custo fixo de arrastar meio bilhão de pesos pelo barramento de memória para produzir um vetor. Esse custo fixo é a razão de tudo na próxima seção.

Essas duas fases são a origem dos dois números que todo sistema de serving reporta. Time to first token é essencialmente prefill, e cresce com o prompt; por isso uma conversa longa demora para começar. Tokens per second é 1/decode step1/\text{decode step}, e é aproximadamente constante; por isso a resposta depois flui de modo uniforme. Um chat que começa devagar e depois transmite suavemente não é um truque de renderização. São estas duas tabelas.

O cache troca aritmética por memória, e a memória que ele quer não é pequena. Para cada token no contexto, cada camada mantém um vetor de key e um vetor de value por cabeça key-value:

bytes per token=2×L×Hkv×dhead×bytes per element\text{bytes per token} = 2 \times L \times H_{kv} \times d_{\text{head}} \times \text{bytes per element}

O 2 é para keys e values; todo o resto é a arquitetura. Para o modelo medido ao longo deste capítulo — 24 camadas, 14 cabeças de query, 2 cabeças key-value, dimensão de cabeça 64 — em fp16 isso dá 2×24×2×64×2=12,2882 \times 24 \times 2 \times 64 \times 2 = 12{,}288 bytes por token.

Fórmulas nesta área têm o hábito de errar por um fator de dois, então confira com o alocador em vez de simplesmente acreditar:

TEXT
KV cache tensors per layer: (1, 2, 295, 64) float16
measured: 3,624,960 bytes for 295 tokens = 12,288 bytes/token
formula : 2 * 24 * 2 * 64 * 2                = 12,288 bytes/token

Exato, e continua exato em todos os formatos testados:

batchcontextocache medidoprevistopico de memória de trabalho
15126,0 MB6,0 MB15,4 MB
116.384192,0 MB192,0 MB207,3 MB
165.536768,0 MB768,0 MB793,7 MB
84.096384,0 MB384,0 MB401,5 MB
322.048768,0 MB768,0 MB794,2 MB
641.024768,0 MB768,0 MB797,0 MB
128512768,0 MB768,0 MB816,4 MB

As três últimas linhas merecem uma segunda olhada. Trinta e dois usuários com 2.048 tokens cada, sessenta e quatro com 1.024, cento e vinte e oito com 512 — o cache tem 768 MB em todos os casos, porque os três mantêm 65.536 tokens. O cache depende apenas do número total de tokens residentes, não de como eles estão distribuídos entre usuários. Esse fato é a base da seção sobre batching.

O Capítulo 9 apresentou multi-query e grouped-query attention e deixou o motivo para este capítulo. O motivo é aquela fórmula, especificamente o HkvH_{kv} nela.

A attention multi-head padrão dá a cada cabeça de query suas próprias cabeças de key e value. O modelo aqui tem 14 cabeças de query; com attention multi-head completa, seu cache seria de 2×24×14×64×2=86,0162 \times 24 \times 14 \times 64 \times 2 = 86{,}016 bytes por token — 84 KB em vez de 12 KB, exatamente sete vezes mais, a razão entre cabeças de query e cabeças key-value.

Multi-query attention1 leva isso ao limite: todas as cabeças de query compartilham uma única cabeça key-value. Grouped-query attention2 é o compromisso que venceu — algumas cabeças key-value, cada uma compartilhada por um grupo de cabeças de query — porque a perda de qualidade da MQA era real e a da GQA não é. Nenhuma das duas compra aritmética. Elas existem para dividir aquela fórmula por um inteiro, e se espalharam pela indústria no momento em que contextos longos fizeram do cache a restrição dominante.

E ele faz isso rapidamente. Para um modelo da classe 7B com 32 camadas e 8 cabeças key-value de dimensão 128, o cache é 128 KB por token em fp16:

tokens de contextoum usuário8 usuários64 usuários
4.0000,49 GB3,91 GB31,2 GB
32.0003,91 GB31,25 GB250,0 GB
128.00015,62 GB125,00 GB1.000,0 GB
1.000.000122,07 GB976,56 GB7.812,5 GB

Os pesos desse modelo em si são 13,0 GB em fp16, o número da tabela no fim deste capítulo. Então, em um contexto de 128.000 tokens, o cache de um usuário é maior que o modelo. Essa é a aritmética que o Capítulo 16 transforma em dinheiro, e é por isso que uma conversa longa não é apenas lenta — ela ocupa uma fatia fixa de uma máquina enquanto a solicitação estiver viva.

Batching: o número que sobe e o número que desce

Link para a seção: Batching: o número que sobe e o número que desce

Decode é memory-bound: os pesos são arrastados pelo barramento para produzir um token, e as unidades aritméticas ficam ociosas. Então coloque mais trabalho no mesmo passo. Execute várias solicitações ao mesmo tempo, e os pesos, lidos uma vez, atendem a todas. Medido no mesmo modelo, com cada solicitação mantendo um cache de 64 tokens e decodificando um token:

batchlatência por passothroughputlatência vs B=1
10,1286 s7,78 tok/s1,00x
20,1839 s10,88 tok/s1,43x
40,1909 s20,95 tok/s1,49x
80,2781 s28,76 tok/s2,16x
160,3430 s46,64 tok/s2,67x
320,6302 s50,78 tok/s4,90x

Leia as duas colunas da direita uma contra a outra, porque elas são o ponto inteiro. Ir de uma solicitação para dezesseis multiplica o throughput por 6,0 e multiplica a espera de qualquer solicitação individual por 2,67. O batch tornou o servidor melhor e cada usuário pior.

Isso não é um bug a ser ajustado; é a própria troca, e ela tem um nome em cada lado. Latência é o que uma pessoa esperando uma resposta vivencia. Throughput é aquilo pelo qual a fatura é dividida. Nenhuma configuração melhora os dois.

Observe também onde ela para. De 16 para 32, o throughput ganha 9%, enquanto a latência quase dobra: o passo deixou de ser memory-bound e virou compute-bound, e depois desse joelho o batch não compra nada. Todo deployment tem um joelho assim; a posição dele precisa ser medida no seu, mas sua existência não.

Batching estático desperdiça a maior parte do que ganha

Link para a seção: Batching estático desperdiça a maior parte do que ganha

A forma ingênua de fazer batch é coletar BB solicitações, executá-las juntas e retornar quando todas terminarem. Mas elas não terminam juntas: algumas respostas têm vinte tokens e outras quinhentos. Um batch fixo roda até seu membro mais longo terminar, e toda solicitação já concluída continua ocupando seu slot, contribuindo padding, até lá.

Pegue 64 solicitações com uma assimetria realista de comprimentos de saída — mediana de 18 tokens, maior de 231, 1.874 no total — e simule as duas políticas com o custo por passo medido para oito slots:

políticatempo totalthroughputlatência média por solicitaçãoslot-steps desperdiçados
batches estáticos de 8176,9 s10,6 tok/s83,2 s3.214
contínuo, 8 slots109,0 s17,2 tok/s8,1 s0

O throughput melhora 1,6x. A latência média melhora mais de dez vezes, porque em batching estático uma solicitação que terminou em quatro passos ainda espera por um vizinho de 231 tokens antes que alguém ouça a resposta.

Continuous batching3 é a correção, e é tão simples quanto parece: o batch não é um grupo, mas um conjunto de slots, e um slot liberado admite a próxima solicitação na fila já no passo seguinte. O scheduler trabalha na granularidade de um token, não de uma solicitação. Todo stack de serving em produção faz isso hoje.

Ele tem uma segunda metade, que é o cache. Slots que entram e saem deixam a memória do cache fragmentada, e reservar para cada slot seu contexto máximo possível desperdiça a maior parte da reserva. PagedAttention4 pega a resposta emprestada dos sistemas operacionais: armazene o cache em blocos de tamanho fixo, com uma tabela de blocos por sequência, para que o cache de uma sequência possa estar fisicamente espalhado enquanto permanece logicamente contíguo — o que também permite que duas sequências com um prefixo compartilhado compartilhem os blocos que o armazenam. É sobre isso que o vLLM é construído, e é por isso que um motor de serving é um alocador de memória com um transformer acoplado.

Quantização, e a primeira coisa que dá errado

Link para a seção: Quantização, e a primeira coisa que dá errado

A outra metade da conta são os próprios pesos. Meio bilhão de parâmetros a quatro bytes cada são 1,98 GB; a dois bytes, 0,99 GB; a um byte, 0,49 GB. Menos bits por peso encolhem o modelo em disco, encolhem-no em memória e — como decode é bandwidth-bound — tornam cada passo mais rápido, já que há menos bytes para mover.

O esquema mais simples é a quantização simétrica por máximo absoluto, e cabe em três linhas:

quantize.pyPYTHON
qmax  = 2 ** (bits - 1) - 1
scale = W.abs().max() / qmax                        
Wq    = torch.round(W / scale).clamp(-qmax - 1, qmax)
W_hat = Wq * scale                                  # dequantized

Escolha uma escala para que o maior peso mapeie para o maior inteiro, divida, arredonde, armazene os inteiros e a escala. Reconstrua multiplicando de volta. Não há nada inteligente nisso, e funciona — até deixar de funcionar.

Medido nos pesos reais do modelo: todas as 168 matrizes de projeção, 357,8 milhões de parâmetros, erro relativo WW^/W\lVert W - \hat{W}\rVert / \lVert W \rVert:

esquemaerro relativo médiopior matriz
INT8, uma escala para a matriz inteira0,04000,1487
INT8, uma escala por linha de saída0,01000,0149
INT4, uma escala para a matriz inteira0,60260,9931
INT4, uma escala por linha de saída0,17900,2589
INT4, uma escala por grupo de 1280,13230,1992
NF4, uma escala por bloco de 640,09520,1205
INT3, uma escala por grupo de 1280,30440,4123
INT2, uma escala por grupo de 1280,77900,8076

A quarta linha é o colapso. Um erro relativo de 0,99 na pior matriz significa que a reconstrução não retém praticamente nada do original — a matriz foi substituída por ruído de magnitude parecida. A causa fica visível no mesmo experimento em uma única matriz:

TEXT
model.layers.12.mlp.down_proj.weight   (896 x 4864)
mean |w| 0.01386   std 0.01822   max |w| 0.43945   max/std 24.1
weights beyond 6 sigma: 692 of 4,358,144   (0.016 %)

Um peso em seis mil fica além de seis desvios-padrão, e o maior está a 24. Com uma única escala para a matriz inteira, esse único peso define o tamanho do passo para todos os 4,3 milhões deles. Em 8 bits há 256 passos, e o peso típico ainda cai em um deles de forma significativa. Em 4 bits há 16, o mais externo reservado para um valor que quase nada tem, e os pesos comuns — que são todos eles — arredondam para dois ou três níveis distintos.

Tudo depois daquela linha é o mesmo reparo em granularidades diferentes: dê à escala um território menor. Por linha de saída, o erro é dividido por 3,4; por grupo de 128 pesos consecutivos, é dividido de novo. O custo é contabilidade — uma escala de 16 bits por grupo de 128 é 4+16/128=4.1254 + 16/128 = 4.125 bits por peso em vez de 4 — e isso recupera a maior parte da diferença.

NF4 ataca pelo outro lado.5 Os níveis não precisam ser igualmente espaçados. Os pesos dentro de um bloco são aproximadamente normalmente distribuídos, então escolha os dezesseis níveis como quantis de uma distribuição normal: densos perto de zero, onde os pesos realmente estão, esparsos nas caudas, onde não estão. Mesmos quatro bits, mesma escala por bloco, em um bloco menor — 4,25 bits por peso contra 4,125 do grupo-128 — e o erro medido cai de 0,1323 para 0,0952, 28% menor. Parte disso é o bloco mais fino, e o restante é colocar os níveis onde a massa está; separar os dois exigiria uma terceira linha.

A caixa de ponto flutuante do Capítulo 2 terminou com uma promessa: que este capítulo quantizaria pesos para 8 e 4 bits e encontraria um punhado de features outlier se recusando a ser espremidas. Aqui estão elas, e explicam por que “apenas arredondar os números” nunca iria funcionar em ativações.

Os pesos acima se comportavam mal. As ativações estão em outra liga. Pegue um prompt comum de 84 tokens, capture o fluxo residual em cada camada e meça a maior magnitude que cada uma das 896 dimensões atinge:

camadamaior |h|maior |h| da dimensão medianarazãodimensões acima de 6x a mediana
16,190,33918x2
41543,481,550996x34
81571,631,4981049x36
121575,031,5461019x34
161579,601,617977x32
201577,982,361668x24
24204,4410,76019x12

A dimensão 62 chega a 1.579,6 enquanto a dimensão mediana nunca passa de 1,6. Não é um acaso de um token ou de uma camada: a mesma dimensão está lá na camada 4 e ainda está lá na camada 20, com quase o mesmo valor. Essas são as features outlier,6 e são sistemáticas — uma propriedade do modelo treinado, não da entrada.

O histograma desses 896 máximos por dimensão na camada 16 deixa a forma inequívoca:

TEXT
     0 -      1 | ######################################## 254
     1 -      2 | ######################################## 283
     2 -      4 | ######################################## 226
     4 -      8 | ######################################## 93
     8 -     16 | ##################                       18
    16 -     32 | #########                                9
    32 -     64 | #######                                  7
    64 -    128 | #####                                    5
   128 -    256 |                                          0
   256 -    512 |                                          0
   512 -   1024 |                                          0
  1024 -   4096 | #                                        1

Novecentas dimensões em um monte arrumado abaixo de 8, nada por três oitavas, depois uma única dimensão sozinha no extremo. Agora quantize esse tensor para INT8 e conte o que acontece:

esquemaerro relativoníveis inteiros distintos usados, tensor inteiro
uma escala para o tensor inteiro0,108314 de 256
uma escala por token (por linha)0,0433158
tensor inteiro, 1 dimensão outlier mantida em fp320,044248
tensor inteiro, 4 dimensões outlier mantidas em fp320,027957
tensor inteiro, 16 dimensões outlier mantidas em fp320,0085102

Quatorze níveis de 256. A escala foi definida por 1.579,6, então cada passo tem largura 12,44, e a ativação típica — magnitude mediana 0,26, percentil 99 em 2,51 — não tem onde pousar. Por dimensão, é mais duro:

TEXT
single tensor-wide scale = 12.4378
  dim 826 (max |h| = 4.77):  1 distinct level out of 256
  dim 336 (max |h| = 1.62):  1 distinct level out of 256
  dim  96 (max |h| = 0.69):  1 distinct level out of 256

after excluding the top 4 dimensions, scale = 0.5749  (22x smaller)
  dim 826: 8 levels    dim 336: 4 levels    dim  96: 3 levels

Um nível. A dimensão inteira, todos os tokens, quantizada para o mesmo número. Oito bits foram alocados e aproximadamente zero foram usados, e o modelo que lê essas ativações recebe uma constante.

Essa medição é a justificativa para todas as técnicas que as pessoas realmente usam:

Mantenha os outliers fora disso. LLM.int8()6 decompõe a multiplicação de matrizes: as dimensões com magnitudes extremas são computadas em 16 bits, todo o resto em INT8, e as metades são somadas. A tabela acima é o recibo — remover quatro dimensões reduz o erro por um fator de quase quatro. SmoothQuant7 em vez disso migra a dificuldade: divide as ativações por um fator por canal e multiplica a coluna de pesos correspondente por ele, o que deixa o produto inalterado e move o outlier do tensor que não consegue absorvê-lo para aquele que consegue.

Escolha o arredondamento, não apenas arredonde. Nada acima pergunta para que serve a matriz. GPTQ8 quantiza coluna por coluna e, depois de cada uma, ajusta as colunas restantes em precisão completa para compensar o erro já cometido — minimizando o erro da saída da camada em entradas reais, não o erro dos pesos. AWQ9 observa que uma pequena fração dos canais de peso importa muito mais que o restante, encontra-os a partir de estatísticas de ativação e os escala para cima antes de quantizar, para que caiam em níveis mais finos. Ambos precisam de um conjunto de calibração; nenhum precisa de gradientes.

Mostrar detalhes

GGUF, e o que um formato de arquivo tem a ver com tudo isso.

GGUF não é um método de quantização; é o contêiner que llama.cpp usa, e a confusão em comparações de gguf vs gptq vem de tratar os dois como o mesmo tipo de coisa. GGUF guarda tensores, tokenizer, metadados de arquitetura e chat template em um único arquivo mapeável em memória, e carrega uma família de esquemas de bloco dentro dele — nomes como Q4_K_M codificam bits por peso, tamanho de bloco e se alguns tensores são mantidos em precisão mais alta.

A diferença de engenharia que importa: GPTQ e AWQ produzem pesos otimizados para um kernel de GPU, enquanto os esquemas do GGUF são decodificados de modo barato em uma CPU com o arquivo mapeado, não carregado. É por isso que o mesmo “modelo 7B de 4 bits” nominal existe nos dois mundos com tamanhos e qualidade diferentes, e por isso a comparação honesta nunca é o formato — é a medição abaixo, executada na sua própria tarefa.

O que a quantização realmente custa, medido

Link para a seção: O que a quantização realmente custa, medido

Quase todo artigo sobre quantização para na seção anterior: explica o método, cita uma taxa de compressão e afirma que a qualidade é “em grande parte preservada”. O Capítulo 4 era sobre não se enganar, então vamos descobrir.

Mesmo modelo, pesos quantizados in-place com cada esquema, depois três medições: perplexidade em 2.048 tokens de prosa em inglês reservada — aqui, o rascunho deste curso, por isso o repositório substitui por um livro fixo de domínio público e imprime uma tabela do mesmo formato com números diferentes —, uma bateria de 16 perguntas factuais curtas com respostas conhecidas sob greedy decoding, e a fração de tokens em que o modelo quantizado concorda com o de precisão completa dado o mesmo contexto.

esquemaerro médio dos pesosperplexidadebateria de perguntasconcorda com fp32
fp32 (referência)0,000023,0813/16100,0%
INT8 por tensor0,040023,5813/16
INT8 por linha0,010022,9613/1698,6%
INT4 por tensor0,6026365.416.0000/16
INT4 por linha0,179046,186/1658,3%
INT4 grupo 1280,132331,0810/1671,5%
NF4 bloco 640,095224,5511/1684,7%
INT3 grupo 1280,3044213,090/165,6%
INT2 grupo 1280,779026.325.4360/160,0%

Quatro coisas nessa tabela merecem ser ditas claramente.

INT8 bem feito é grátis. INT8 por linha marca 22,96 contra 23,08 da referência — uma diferença de uma parte em duzentas, que é ruído e deve ser lida como “idêntico”. O lado para o qual o ruído aponta não é estável: no corpus de domínio público do repositório, os mesmos dois esquemas dão 22,24 contra 22,18: metade dessa distância, e apontando para o outro lado. Ele concorda com o modelo de precisão completa em 142 de 144 tokens gerados. Um quarto da memória em relação à referência fp32, metade em relação ao fp16 que você realmente colocaria em produção, e nenhum custo detectável. INT8 feito de qualquer jeito também é quase grátis: uma escala por matriz custa 0,5 ponto de perplexidade e nenhuma resposta da bateria. Oito bits perdoam o bastante para que a granularidade mal importe, exatamente por isso as pessoas generalizam de INT8 para INT4 e se machucam.

INT4 com uma escala por tensor destrói o modelo. Perplexidade 365 milhões: não degradado, aniquilado. A granularidade vira o jogo inteiro — por tensor 365.416.000, por linha 46,18, por grupo de 128 31,08, NF4 24,55. Os mesmos quatro bits por peso, um fator de quinze milhões entre o pior e o melhor.

Perplexidade é um instrumento grosseiro, e a bateria é mais grosseira ainda. Entre NF4 e INT4 grupo-128, a diferença de perplexidade é 6,5 pontos e a bateria difere por uma pergunta — e o intervalo de confiança do Capítulo 4 diz que uma pergunta em dezesseis não distingue absolutamente nada. Há uma demonstração mais nítida que o intervalo: rode a mesma bateria com a penalidade de repetição padrão do modelo desligada, que é o que greedy decoding realmente significa, e essas duas linhas trocam de lugar. Uma pergunta em dezesseis não é um efeito pequeno; é efeito nenhum. O aviso do Capítulo 8 também se aplica: perplexidade só é comparável entre modelos que compartilham um tokenizer, então um número do texto de outra pessoa não pode ser comparado ao seu.

A coluna de concordância é a mais afiada das três, e quase gratuita: rode o modelo de precisão completa de modo greedy, depois pergunte ao quantizado, em cada posição, o que ele teria escolhido dado o mesmo prefixo. Ela tem 144 observações independentes em vez de 16, não precisa de ground truth e degrada suavemente onde a bateria degrada em saltos. Também é exatamente a quantidade de que a próxima seção precisa.

Esta é a promessa que o Capítulo 1 fez sobre este capítulo, chegando na hora: a matemática diz que um modelo de 4 bits é possível, e a engenharia decide se ele é utilizável.

O Capítulo 12 anunciou isto e deixou a conta aqui.

A ideia vem direto da divisão prefill/decode. Verificar uma sequência proposta de γ\gamma tokens custa uma passagem forward sobre γ\gamma posições — um produto matriz-matriz, pouco mais caro que a passagem sobre uma. Então:

Um modelo pequeno e barato gera γ\gamma tokens candidatos de forma autorregressiva.

O modelo grande executa uma passagem forward sobre todos os γ\gamma candidatos de uma vez, produzindo o que teria dito em cada posição.

Mantenha o prefixo mais longo em que os dois concordam, mais o token que o modelo grande fornece de graça na primeira divergência. Descarte o restante e comece de novo.

A distribuição de saída não muda. Com greedy decoding isso é óbvio — um token só é aceito se o alvo o teria produzido. Com sampling, isso exige uma regra de aceitação modificada, e Leviathan et al. provam que a distribuição resultante é exatamente a do alvo.10 Esta é a segunda otimização exata deste capítulo.

Tudo, portanto, depende da taxa de aceitação α\alpha, que é mensurável — é a coluna de concordância acima, por isso ela foi computada ali. Usando cada modelo quantizado como draft para o alvo de precisão completa, ao longo de 144 posições geradas:

modelo draftaceitaçãosequência aceita mais longatokens esperados por passagem do alvo, γ=4\gamma = 4
fp32 (o próprio alvo)100,0%485,00
INT8 por linha98,6%484,86
NF4 bloco 6484,7%203,69
INT4 grupo 12871,5%132,85
INT4 por linha58,3%72,24
INT3 grupo 1285,6%21,06
INT2 grupo 1280,0%01,00

Os tokens esperados aceitos por passagem de verificação, com tamanho de draft γ\gamma, são

E[tokens]=1αγ+11α\mathbb{E}[\text{tokens}] = \frac{1 - \alpha^{\gamma+1}}{1 - \alpha}

e o speedup líquido divide isso pelo custo do próprio draft, uma fração cc do alvo por token:

aceitaçãoc=0.05c=0.05, γ=4\gamma=4c=0.1c=0.1, γ=4\gamma=4c=0.2c=0.2, γ=4\gamma=4c=0.1c=0.1, γ=8\gamma=8
30%1,19x1,02x0,79x0,79x
50%1,61x1,38x1,08x1,11x
70%2,31x1,98x1,54x1,78x
90%3,41x2,93x2,28x3,40x

A entrada em negrito é a que deve ficar na memória: speculative decoding pode tornar a geração mais lenta. Com 30% de aceitação e um draft custando um quinto do alvo, você paga por cinco passagens forward e mantém 1,4 tokens. A última coluna é a outra armadilha — um draft mais longo só ajuda quando a aceitação é alta, porque a cauda de uma aposta de γ\gamma tokens quase nunca é alcançada. Com 90% de aceitação, γ=8\gamma = 8 vale 3,40x; com 30%, vale 0,79x: a mesma configuração, ganho ou perda dependendo de um número medido no seu tráfego.

Quantização encolhe um modelo armazenando a mesma função em menos bits. Distilação o encolhe treinando um modelo menor para imitar um maior11 — uma ideia que antecede deep learning em quase uma década.12

A parte sutil é com o que o aluno aprende. Não a resposta correta: ele poderia ter sido treinado diretamente nela. O que o professor acrescenta é a distribuição inteira. Pergunte ao modelo o que vem depois de uma frase e olhe além do argmax:

TEXT
"She poured the milk into the"
  ' jug' 0.1355   ' cup' 0.1051   ' bowl' 0.0605   ' large' 0.0380   ' milk' 0.0360

O hard label diz jug e nada mais. O soft label diz jug, e também que cup era quase tão bom, bowl plausível, e large — um adjetivo, uma continuação gramatical completamente diferente — ainda vivo. Esse é o argumento original: isto é um 7, mas se parece bastante com um 1, e a semelhança é informação que o hard label joga fora.

Também é por isso que a distilação usa uma temperatura. Dividir os logits por TT antes do softmax achata a distribuição e aumenta o peso relativo dos segundos colocados: nesta frase, a razão entre o token principal e o terceiro cai de 2,24 em T=1T = 1 para 1,50 em T=2T = 2 — a raiz quadrada da primeira, que é o que dividir os logits por dois faz com uma razão. Mesma ordem, mais attention da loss nos quase acertos. O gradiente do aluno carrega a incerteza do professor, não apenas seu veredito.

Tudo neste capítulo agora é uma soma:

memory=N×bytes per weightfixed+T×2LHkvdhead×bytesgrows with every token+runtime overheadcall it 1.5 GB\text{memory} = \underbrace{N \times \text{bytes per weight}}_{\text{fixed}} + \underbrace{T \times 2 L H_{kv} d_{\text{head}} \times \text{bytes}}_{\text{grows with every token}} + \underbrace{\text{runtime overhead}}_{\text{call it 1.5 GB}}

em que TT são os tokens totais residentes em todas as solicitações concorrentes. Aplicando: as linhas 7B e 70B assumem 8 cabeças key-value de dimensão 128; a linha 13B usa attention multi-head completa com 40 cabeças, que é como aquelas gerações de modelo foram construídas — e isso aparece.

8 GB

modeloprecisãopesoslivre após overheadtokens de contexto que cabem
7Bfp1613,0 GBnão cabe
7Bint86,5 GBnão cabe
7Bint4 (g128)3,4 GB3,1 GB25.710
13Bint4 (g128)6,2 GB0,3 GB337
70Bint4 (g128)33,6 GBnão cabe

16 GB

modeloprecisãopesoslivre após overheadtokens de contexto que cabem
7Bfp1613,0 GB1,5 GB11.972
7Bint86,5 GB8,0 GB65.378
7Bint4 (g128)3,4 GB11,1 GB91.246
13Bint812,1 GB2,4 GB3.136
13Bint4 (g128)6,2 GB8,3 GB10.822

24 GB

modeloprecisãopesoslivre após overheadtokens de contexto que cabem
7Bfp1613,0 GB9,5 GB77.508
7Bint86,5 GB16,0 GB130.914
7Bint4 (g128)3,4 GB19,1 GB156.782
13Bint812,1 GB10,4 GB13.622
13Bint4 (g128)6,2 GB16,3 GB21.308
70Bint4 (g128)33,6 GBnão cabe

Olhe para a linha 13B na tabela de 8 GB. Os pesos cabem — 6,2 GB de 8 — então, pelo jeito usual de falar, um modelo 13B “roda em uma placa de 8 GB”. Ele tem 337 tokens de contexto, o que não é uma conversa, mas mal um prompt. “Ele cabe?” é a pergunta errada. A certa é “com quanto contexto, e para quantos usuários ao mesmo tempo?”.

Olhe também para as duas linhas int8 de 16 GB. O 7B obtém 65.378 tokens e o 13B obtém 3.136 — uma diferença de vinte vezes a partir de 5,6 GB de pesos extras, porque este 13B tem attention multi-head e seu cache custa 800 KB por token contra 128 KB do 7B. Dois modelos de tamanho parecido, um inutilizável para contexto longo, por um motivo que não aparece no título de nenhum model card.

Treze capítulos atrás, isto era um perceptron com dois pesos e um viés. Agora é um transformer que foi projetado, treinado, alinhado, ensinado a gastar compute em perguntas difíceis e servido a um custo medido por token — sem nenhuma caixa fechada sobrando dentro dele.

Isso termina aqui, e termina de propósito.

O Capítulo 14 começa com o modelo em outro lugar. Não no seu processo, não na sua memória, não em uma variável que você possa imprimir: em uma máquina que você não administra, atrás de uma API key, uma porta e uma conta. Tudo que foi medido aqui continua acontecendo — o prefill ainda roda antes do primeiro token, o cache ainda cresce com a conversa, o batch em que você está ainda pertence a outra pessoa e ainda decide sua latência —, mas daqui em diante você observa isso por um stream de Server-Sent Events, um finish_reason, e um HTTP 429 com um header Retry-After. As perguntas mudam com o ponto de vista: não como este gradiente é computado, mas por que minha fatura triplicou. A linguagem também muda, e o Capítulo 14 explica essa regra em vez de apenas anunciá-la — até aqui, o código continha pesos, gradientes, logits e bytes de tokenizer; dali em diante, ele contém uma conexão, uma tentativa, um cancelamento e estado acumulado. Os treze capítulos atrás de você não são descartados na travessia. Eles são a descrição do que está rodando do outro lado da porta.


Duas omissões são deliberadas. FlashAttention (Dao et al., arXiv:2205.14135) não é uma attention diferente — ela computa a mesma função dividindo a operação em tiles para que a matriz de scores n×nn \times n nunca seja escrita na memória, por isso os 67 MB na segunda tabela deste capítulo são menores na prática do que a aritmética sugere. E os kernels em si são delegados: a aula 10 do CS336 de Stanford cobre sistemas de inferência na profundidade que isto não tenta alcançar, e o repositório llama.cpp e a especificação GGUF são as fontes primárias para o lado da CPU.

  1. Shazeer, N. Fast Transformer Decoding: One Write-Head is All You Need. arXiv:1911.02150 (2019). O artigo é em grande parte um argumento sobre largura de banda de memória, e é lido como tal.

  2. Ainslie, J. et al. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. arXiv:2305.13245 (2023). Inclui a receita de uptraining que converte um checkpoint multi-head existente, por isso GQA se espalhou tão rápido.

  3. Yu, G.-I., Jeong, J. S., Kim, G.-W., Kim, S. and Chun, B.-G. Orca: A Distributed Serving System for Transformer-Based Generative Models. OSDI 2022. Introduz scheduling em nível de iteração — continuous batching — e batching seletivo.

  4. Kwon, W. et al. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023), SOSP 2023. O artigo sobre o qual vLLM é construído; a §3 traz a analogia com sistemas operacionais por completo.

  5. Dettmers, T., Pagnoni, A., Holtzman, A. and Zettlemoyer, L. QLoRA: Efficient Finetuning of Quantized LLMs. arXiv:2305.14314 (2023). NF4 é definido na §3; os dezesseis valores de nível usados na medição acima são os que esse artigo deriva.

  6. Dettmers, T., Lewis, M., Belkada, Y. and Zettlemoyer, L. LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale. arXiv:2208.07339 (2022). A análise de features outlier na §4 é a fonte do fenômeno medido acima, incluindo a descoberta de que outliers emergem sistematicamente em escala. 2

  7. Xiao, G., Lin, J., Seznec, M., Wu, H., Demouth, J. and Han, S. SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models. arXiv:2211.10438 (2022).

  8. Frantar, E., Ashkboos, S., Hoefler, T. and Alistarh, D. GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers. arXiv:2210.17323 (2022).

  9. Lin, J. et al. AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration. arXiv:2306.00978 (2023).

  10. Leviathan, Y., Kalman, M. and Matias, Y. Fast Inference from Transformers via Speculative Decoding. arXiv:2211.17192 (2022). O Teorema 1 é a prova de que a distribuição de saída não muda; Chen et al. (arXiv:2302.01318) publicaram a mesma ideia de forma independente.

  11. Hinton, G., Vinyals, O. and Dean, J. Distilling the Knowledge in a Neural Network. arXiv:1503.02531 (2015). A temperatura e o argumento do “dark knowledge”.

  12. Buciluă, C., Caruana, R. and Niculescu-Mizil, A. Model Compression. KDD 2006. Distilação, nove anos antes, para ensembles em vez de transformers.

Pronto para deixar a LIA escolher por você?

Crie com todos os modelos de IA em um só lugar — comece grátis hoje.