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

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

O mesmo modelo responde igual em 8,8 s e 78,9 s. Depois INT4, medido de três formas — não apenas afirmado.

Nesta página

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

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 é mais precisa pelo esforço. É nove vezes mais lenta para nada.

É essa a forma deste capítulo. Tudo nele — a cache, o batch, os pesos quantizados — é uma tentativa de deixar de pagar por trabalho que não altera a resposta, ou de descobrir quanto custa uma resposta mais barata. O Capítulo 10 estabeleceu a tabela de preços para o treino. Esta é a tabela de preços do lado que se paga para sempre: um modelo deployed gasta aproximadamente 2N2N FLOPs por cada token que emite, em cada pedido, pelo resto da sua vida.

Para gerar um token, um transformer apenas com decoder recebe toda a sequência até ao momento, passa-a por todas as camadas e lê a distribuição de probabilidades na última posição. Depois acrescenta o token escolhido e repete. Essa descrição está correcta, e é isso que a execução lenta faz.

Também é enormemente desperdiçadora, e a razão é a máscara causal do Capítulo 9. Os vectores key e value da posição 7 são calculados 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 a pode ver — é isso que causal significa —, por isso as key e value da posição 7 são exactamente os mesmos números de antes. A execução lenta recalcula-os na mesma, a cada passo.

Portanto, guarde-os. Esse armazenamento é a key-value cache, a optimização mais consequente 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 é dado ao modelo dentro do ciclo: nxt, um token. Não a sequência. A query do novo token atende a todas as key em cache, e as key em cache nunca iam mudar. Isto não é uma aproximação — a verificação de saída idêntica acima é o ponto. A cache não troca qualidade por velocidade; elimina aritmética redundante.

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

tokens em contextorecalcular tudocom uma cacheráciomatriz 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 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 a cache, constrói antes uma linha 1×n1 \times n: aos 4.096 tokens, 67 MB de scores contra 16 KB.

Contar multiply-accumulates em vez de milissegundos retira a máquina do argumento. Para gerar TT tokens a partir de um arranque a frio:

tokens geradoscom uma cachea recalcularrácio
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 versão sem cache é quadrática; somado ao longo de uma geração, O(T2)O(T^2) contra O(T3)O(T^3), com o rácio a crescer sem limite. A diferença de nove vezes da abertura foi medida ao longo de 48 tokens — abaixo da primeira linha dessa tabela.

A cache também altera o que tem de estar em memória. Numa GPU de portátil de 8 GB a gerar 256 tokens em fp16, tomando o pico do alocador e subtraindo os pesos residentes:

pico de memória de trabalho
com uma cache21,8 MB
a recalcular181,7 MB

8,3 vezes mais memória, gasta para produzir os mesmos tokens mais lentamente. Esta é a promessa feita no Capítulo 5, a chegar de uma direcção inesperada: aí, a autodiferenciação em modo inverso tinha de manter todos os intermédios vivos para a passagem backward, e as activaçōes dominavam a memória de treino. Na inferência não há passagem backward nem nada a reter para ela — por isso, o que domina a memória passa a ser a cache, e é uma escolha deliberada em vez de um custo inevitável.

Volte a olhar para a execução rápida: o primeiro token comportou-se de forma 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 divide-se em duas fases com físicas genuinamente diferentes:

Uma passagem forward sobre todo o prompt. Cada token é processado em paralelo, por isso cada matriz de pesos é carregada da memória uma vez e multiplicada por uma matriz de centenas de vectores de tokens — um produto matriz-matriz, com muita aritmética por byte movimentado, que é exactamente o que uma GPU foi feita para fazer. O prefill é limitado por computação, e o seu custo é aproximadamente linear no comprimento do prompt.

Uma passagem forward por token, batch de um e sequência de um. Cada matriz de pesos continua a ser carregada por completo da memória, e multiplicada por um único vector — um produto matriz-vector, com quase nenhuma aritmética por byte movimentado. O decode é limitado pela largura de banda da memória, e o seu custo por token mal depende do comprimento 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 uma cache de CC:

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

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

Estas duas fases são a origem dos dois números que todos os sistemas de serving comunicam. Time to first token é essencialmente prefill, e cresce com o prompt, razão pela qual uma conversa longa parece lenta a começar. Tokens por segundo é 1/decode step1/\text{decode step}, e é aproximadamente constante, razão pela qual a resposta depois flui de forma uniforme. Um chat que começa devagar e depois faz streaming suavemente não é um truque de rendering. São estas duas tabelas.

A cache troca aritmética por memória, e a memória que quer não é pequena. Para cada token no contexto, cada camada guarda um vector key e um vector value por key-value head:

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; tudo o resto é a arquitectura. Para o modelo medido ao longo deste capítulo — 24 camadas, 14 query heads, 2 key-value heads, dimensão de head 64 — em fp16, isso são 2×24×2×64×2=12,2882 \times 24 \times 2 \times 64 \times 2 = 12{,}288 bytes por token.

As fórmulas nesta área têm o hábito de errar por um factor de dois, por isso compare-a com o alocador em vez de acreditar nela:

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

Exacto, e mantém-se exacto em todas as formas testadas:

batchcontextocache medidaprevistopico 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 últimas três linhas merecem uma segunda leitura. Trinta e dois utilizadores com 2.048 tokens cada, sessenta e quatro com 1.024, cento e vinte e oito com 512 — a cache é 768 MB em todos os casos, porque os três guardam 65.536 tokens. A cache depende apenas do número total de tokens residentes, não de como estão distribuídos pelos utilizadores. Esse facto é a base da secção sobre batching.

O Capítulo 9 apresentou multi-query e grouped-query attention e adiou a razão para este capítulo. A razão é essa fórmula, e especificamente o HkvH_{kv} nela.

A multi-head attention padrão dá a cada query head as suas próprias key e value heads. O modelo aqui tem 14 query heads; com multi-head attention completa, a cache seria 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, exactamente sete vezes mais, o rácio entre query heads e key-value heads.

A multi-query attention1 leva isto ao limite: todas as query heads partilham uma única key-value head. A grouped-query attention2 é o compromisso que venceu — um punhado de key-value heads, cada uma partilhada por um grupo de query heads — porque a perda de qualidade da MQA era real e a da GQA não é. Nenhuma compra aritmética. Existem para dividir essa fórmula por um inteiro, e espalharam-se pela indústria no momento em que contextos longos fizeram da cache a restrição vinculativa.

E fazem-no rapidamente. Para um modelo da classe 7B com 32 camadas e 8 key-value heads de dimensão 128, a cache é 128 KB por token em fp16:

tokens de contextoum utilizador8 utilizadores64 utilizadores
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 do próprio modelo ocupam 13,0 GB em fp16, o valor na tabela no fim deste capítulo. Assim, num contexto de 128.000 tokens, a cache de um utilizador é maior do que o modelo. Esta é a aritmética que o Capítulo 16 transforma em dinheiro, e é por isso que uma conversa longa não é apenas lenta — ocupa uma fatia fixa de uma máquina enquanto o pedido estiver vivo.

O decode é limitado pela memória: os pesos são arrastados pelo barramento para produzir um token, e as unidades aritméticas ficam ociosas. Portanto, ponha mais trabalho no mesmo passo. Execute vários pedidos ao mesmo tempo, e os pesos, lidos uma vez, servem todos. Medido no mesmo modelo, com cada pedido a manter uma cache de 64 tokens e a fazer decode de 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 são o ponto inteiro. Passar de um pedido para dezasseis multiplica o throughput por 6,0 e multiplica a espera de qualquer pedido individual por 2,67. O batch tornou o servidor melhor e cada utilizador pior.

Isso não é um bug a afinar até desaparecer; é o próprio trade-off, e tem um nome de cada lado. Latência é o que uma pessoa à espera de uma resposta sente. Throughput é aquilo por que a factura é dividida. Nenhuma definição melhora ambos.

Repare também onde pára. De 16 para 32, o throughput ganha 9 % enquanto a latência quase duplica: o passo deixou de estar limitado pela memória e passou a estar limitado pela computação, e depois desse joelho o batch não compra nada. Cada deployment tem esse joelho; a sua localização tem de ser medida no seu, mas a sua existência não.

O batching estático desperdiça grande parte do que ganha

Ligação para a secção: O batching estático desperdiça grande parte do que ganha

A forma ingénua de fazer batch é recolher BB pedidos, executá-los juntos e devolver quando todos terminarem. Mas não terminam juntos: algumas respostas têm vinte tokens e outras quinhentos. Um batch fixo corre até o membro mais longo terminar, e cada pedido já concluído continua a ocupar o seu lugar, contribuindo padding, até lá.

Pegue em 64 pedidos com uma assimetria realista de comprimentos de saída — mediana de 18 tokens, máximo de 231, 1.874 no total — e simule as duas políticas ao custo por passo medido para oito lugares:

políticatempo de paredethroughputlatência média por pedidoslot-steps desperdiçados
batches estáticos de 8176,9 s10,6 tok/s83,2 s3.214
contínuo, 8 lugares109,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, um pedido que terminou em quatro passos continua à espera de um vizinho de 231 tokens antes de alguém o receber.

Continuous batching3 é a solução, e é tão simples quanto parece: o batch não é um grupo, mas um conjunto de lugares, e um lugar que fica livre admite o próximo pedido em fila no passo imediatamente seguinte. O scheduler trabalha à granularidade de um token em vez de um pedido. Todas as pilhas de serving em produção fazem isto agora.

Há uma segunda metade, que é a cache. Lugares que entram e saem deixam a memória da cache fragmentada, e reservar para cada lugar o seu contexto máximo possível desperdiça a maior parte da reserva. PagedAttention4 pede a resposta emprestada aos sistemas operativos: armazenar a cache em blocos de tamanho fixo com uma tabela de blocos por sequência, para que a cache de uma sequência possa estar fisicamente dispersa mantendo-se logicamente contígua — o que também permite que duas sequências com um prefixo partilhado partilhem os blocos que o contêm. É nisto que o vLLM assenta, e é por isso que um motor de serving é um alocador de memória com um transformer ligado.

A outra metade da factura são os próprios pesos. Meio biliã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 reduzem o modelo em disco, reduzem-no em memória e — porque o decode é limitado pela largura de banda — tornam cada passo mais rápido, já que há menos bytes para mover.

O esquema mais simples é a quantização simétrica pelo 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 seja mapeado para o maior inteiro, divida, arredonde, armazene os inteiros e a escala. Reconstrua multiplicando de volta. Não há nada de engenhoso nisto, e funciona — até deixar de funcionar.

Medido nos pesos reais do modelo: todas as 168 matrizes de projecçã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 aproximadamente correcta. A causa é visível na mesma experiência sobre 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 está para lá 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 de pesos. Com 8 bits há 256 passos e o peso típico ainda cai num valor significativo. Com 4 bits há 16, o mais exterior reservado para um valor que quase nada tem, e os pesos comuns — que são todos — arredondam para dois ou três níveis distintos.

Tudo depois dessa linha é a mesma reparação em granularidades diferentes: dar à escala um território menor. Por linha de saída, divide o erro por 3,4; por grupo de 128 pesos consecutivos, volta a dividi-lo. 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 aborda o problema pelo outro lado.5 Os níveis não têm de estar igualmente espaçados. Os pesos dentro de um bloco têm distribuição aproximadamente normal, por isso escolha os dezasseis 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. Os mesmos quatro bits, a mesma escala por bloco, num bloco menor — 4,25 bits por peso contra os 4,125 do grupo de 128 — e o erro medido desce de 0,1323 para 0,0952, menos 28 %. Parte disso vem do bloco mais fino e o resto de colocar os níveis onde está a massa, e separar as duas coisas exigiria uma terceira linha.

A caixa de vírgula 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 outlier features a recusar serem comprimidas. Aqui estão elas, e explicam porque «basta arredondar os números» nunca ia funcionar nas activaçōes.

Os pesos acima portaram-se mal. As activaçōes estão noutra liga. Pegue num prompt normal 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 medianaráciodimensõ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 atinge 1.579,6 enquanto a dimensão mediana nunca excede 1,6. Não é uma flutuação de um token ou de uma camada: a mesma dimensão está lá na camada 4 e continua lá na camada 20, com quase o mesmo valor. Estas são as outlier features,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 torna 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 num monte arrumado abaixo de 8, absolutamente nada durante três oitavas, depois uma dimensão sozinha na ponta distante. 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

Catorze níveis de 256. A escala foi definida por 1.579,6, por isso cada passo tem 12,44 de largura, e a activação típica — magnitude mediana 0,26, percentil 99 de 2,51 — não tem onde aterrar. Por dimensão, é mais gritante:

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. Foram atribuídos oito bits e usou-se aproximadamente zero, e o modelo que lê essas activaçōes recebe uma constante.

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

Mantenha os outliers fora disto. LLM.int8()6 decompõe a multiplicação de matrizes: as dimensões com magnitudes extremas são calculadas em 16 bits, tudo o resto em INT8, e as metades são somadas. A tabela acima é o recibo — remover quatro dimensões reduz o erro por um factor de quase quatro. SmoothQuant7, em vez disso, migra a dificuldade: divide as activaçōes por um factor por canal e multiplica a coluna de pesos correspondente por ele, o que deixa o produto inalterado e move o outlier para fora do tensor que não o consegue absorver e para dentro daquele que consegue.

Escolha o arredondamento; não se limite a arredondar. Nada acima pergunta para que serve a matriz. GPTQ8 quantiza coluna a 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, e não o dos seus pesos. AWQ9 nota que uma pequena fracção de canais de pesos importa muito mais do que o resto, encontra-os a partir de estatísticas de activaçōes, e aumenta a sua escala antes de quantizar para que aterrem em níveis mais finos. Ambos precisam de um conjunto de calibração; nenhum precisa de gradientes.

Mostrar detalhes

GGUF, e o que é que um formato de ficheiro tem a ver com isto.

GGUF não é um método de quantização; é o contentor que llama.cpp usa, e a confusão nas comparações gguf vs gptq vem de tratar os dois como o mesmo tipo de coisa. GGUF guarda tensores, tokenizer, metadados da arquitectura e chat template num único ficheiro mapeável em memória, e transporta dentro de si uma família de esquemas por blocos — nomes como Q4_K_M codificam bits por peso, tamanho de bloco, e se alguns tensores são mantidos em maior precisão.

A diferença de engenharia que importa: GPTQ e AWQ produzem pesos optimizados para um kernel de GPU, enquanto os esquemas da GGUF são descodificados de forma barata numa CPU com o ficheiro mapeado em vez de carregado. É por isso que o mesmo «modelo 7B de 4 bits» nominal existe nos dois mundos com tamanhos diferentes e qualidade diferente, e porque a comparação honesta nunca é o formato — é a medição abaixo, executada na sua própria tarefa.

Quase todos os artigos sobre quantização param na secção anterior: explicam o método, citam uma taxa de compressão e afirmam que a qualidade é «em grande parte preservada». O Capítulo 4 era sobre não nos enganarmos a nós próprios, por isso vamos descobrir.

Mesmo modelo, pesos quantizados in place com cada esquema, depois três medições: perplexidade em 2.048 tokens de prosa inglesa retida — aqui, o rascunho deste curso, razão pela qual o repositório substitui por um livro fixo de domínio público e imprime uma tabela com a mesma forma mas números diferentes —, uma bateria de 16 perguntas factuais curtas com respostas conhecidas em greedy decoding, e a fracção de tokens em que o modelo quantizado concorda com o de precisão completa dado contexto idêntico.

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 %

Há quatro coisas nessa tabela que vale a pena dizer claramente.

INT8 bem feito é gratuito. INT8 por linha pontua 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 sentido do ruído 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 no sentido oposto. Concorda com o modelo de precisão completa em 142 de 144 tokens gerados. Um quarto da memória face à referência fp32, metade face ao fp16 que realmente faria deploy, e nenhum custo detectável. INT8 feito sem cuidado também é quase gratuito: uma escala por matriz custa 0,5 pontos de perplexidade e nenhuma resposta da bateria. Oito bits perdoam o suficiente para a granularidade quase não importar, que é exactamente a razão por que as pessoas generalizam de INT8 para INT4 e se magoam.

INT4 com uma escala por tensor destrói o modelo. Perplexidade 365 milhões: não degradado, aniquilado. A granularidade passa então a ser todo o jogo — 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 factor de quinze milhões entre o pior e o melhor.

A perplexidade é um instrumento grosseiro e a bateria é ainda mais grosseira. 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 dezasseis não distingue absolutamente nada. Há uma demonstração mais nítida do que o intervalo: execute a mesma bateria com a penalização de repetição stock do modelo desligada, que é o que greedy decoding realmente significa, e essas duas linhas trocam de lugar. Uma pergunta em dezasseis não é um efeito pequeno; não é efeito nenhum. O aviso do Capítulo 8 também se aplica: a perplexidade só é comparável entre modelos que partilham tokenizer, por isso um número retirado do texto de outra pessoa não pode ser comparado com o seu.

A coluna de concordância é a mais nítida das três, e quase gratuita: execute o modelo de precisão completa de forma greedy, depois pergunte ao quantizado, em cada posição, o que teria escolhido dado o mesmo prefixo. Tem 144 observações independentes em vez de 16, não precisa de ground truth, e degrada-se de forma suave onde a bateria se degrada em saltos. É também exactamente a quantidade de que a próxima secção precisa.

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

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

A ideia vem directamente 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 do que a passagem sobre uma. Portanto:

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 discordância. Descarte o resto e recomece.

A distribuição de saída não muda. Com greedy decoding isso é óbvio — um token só é aceite se o target o teria produzido. Com sampling, é necessária uma regra de aceitação modificada, e Leviathan et al. provam que a distribuição resultante é exactamente a do target.10 Esta é a segunda optimização exacta deste capítulo.

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

draft modelaceitaçãosequência aceite mais longatokens esperados por passagem do target, γ=4\gamma = 4
fp32 (o próprio target)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 aceites esperados por passagem de verificação, com comprimento 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 fracção cc do target 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 a 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 que custa um quinto do target, paga por cinco passagens forward e fica com 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 previsão de γ\gamma tokens quase nunca é alcançada. Com 90 % de aceitação, γ=8\gamma = 8 vale 3,40x e com 30 % vale 0,79x: a mesma configuração, ganho ou perda dependendo de um número medido no seu tráfego.

A quantização reduz um modelo ao guardar a mesma função em menos bits. A distillation reduz-o treinando um modelo mais pequeno para imitar um maior11 — uma ideia anterior ao deep learning em quase uma década.12

A parte subtil é com o que o estudante aprende. Não a resposta correcta: poderia ter sido treinado directamente nela. O que o professor acrescenta é a distribuição inteira. Pergunte ao modelo o que vem a seguir a uma frase e olhe para lá do argmax:

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

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

É também por isso que a distillation usa uma temperatura. Dividir os logits por TT antes da softmax achata a distribuição e aumenta o peso relativo dos segundos classificados: nesta frase, o rácio entre o token de topo e o terceiro cai de 2,24 em T=1T = 1 para 1,50 em T=2T = 2 — a raiz quadrada do primeiro, que é o que dividir os logits por dois faz a um rácio. Mesma ordenação, mais attention da perda nos quase acertos. O gradient do estudante transporta a incerteza do professor e não apenas o seu veredicto.

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 todos os pedidos concorrentes. Aplicando-a: as linhas 7B e 70B assumem 8 key-value heads de dimensão 128; a linha 13B, multi-head attention completa com 40 heads, que foi como essas gerações de modelo foram construídas — e nota-se.

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 —, por isso, na forma habitual de falar, um modelo 13B «corre numa placa de 8 GB». Tem 337 tokens de contexto, o que não é uma conversa, mas mal chega a ser um prompt. «Cabe?» é a pergunta errada. A certa é «com quanto contexto, e para quantos utilizadores ao mesmo tempo?».

Olhe também para as duas linhas int8 de 16 GB. O 7B recebe 65.378 tokens e o 13B recebe 3.136 — uma diferença de vinte vezes a partir de 5,6 GB de pesos adicionais, porque o 13B aqui tem multi-head attention e a sua cache custa 800 KB por token contra 128 KB do 7B. Dois modelos de tamanho semelhante, um inutilizável para contexto longo, por uma razão 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 bias. Agora é um transformer que foi desenhado, treinado, alinhado, ensinado a gastar computação em perguntas difíceis e servido a um custo por token medido — sem nenhuma caixa por abrir.

Isto termina aqui, e termina de propósito.

O Capítulo 14 começa com o modelo noutro sítio. Não no seu processo, não na sua memória, não numa variável que possa imprimir: numa máquina que não administra, atrás de uma API key, uma porta e uma factura. Tudo o que foi medido aqui continua a acontecer — o prefill continua a correr antes do primeiro token, a cache continua a crescer com a conversa, o batch em que está continua a pertencer a outra pessoa e continua a decidir a sua latência —, mas, a partir de agora, observa-o através de um stream de Server-Sent Events, um finish_reason, e um HTTP 429 com um cabeçalho Retry-After. As perguntas mudam com o ponto de vista: não como é este gradient calculado, mas porque é que a minha factura triplicou. A linguagem também muda, e o Capítulo 14 explica essa regra em vez de a anunciar — até aqui, o código guardava pesos, gradientes, logits e bytes do tokenizer; a partir daí, guarda uma ligação, uma repetição, um cancelamento e estado acumulado. Os treze capítulos atrás de si não são descartados pela travessia. São a descrição do que está a correr do outro lado da porta.


Duas omissões são deliberadas. FlashAttention (Dao et al., arXiv:2205.14135) não é uma attention diferente — calcula 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, razão pela qual os 67 MB na segunda tabela deste capítulo são menores na prática do que a aritmética sugere. E os próprios kernels ficam delegados: a aula 10 do CS336 de Stanford cobre sistemas de inferência com uma profundidade que isto não tenta atingir, e o repositório llama.cpp e a especificação GGUF são as fontes primárias para o lado 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 lê-se 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, razão pela qual a GQA se espalhou tão depressa.

  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 ao nível da iteração — continuous batching — e batching selectivo.

  4. Kwon, W. et al. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023), SOSP 2023. O artigo em que o vLLM assenta; a §3 contém a analogia com sistemas operativos por inteiro.

  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 dezasseis valores de níveis usados na medição acima são os que este 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 outlier features na §4 é a fonte do fenómeno medido acima, incluindo a descoberta de que outliers emergem sistematicamente à 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. Distillation, nove anos antes, para ensembles em vez de transformers.

Pronto para deixar a LIA escolher?

Construa com todos os modelos de IA num só sítio — comece grátis hoje.