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.
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 FLOPs para cada token que emite, em cada solicitação, pelo resto da vida.
Para onde foi o tempo da segunda execução
Link para a seção: Para onde foi o tempo da segunda execuçãoPara 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:
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 , um passo de geração computado das duas formas:
| tokens no contexto | recalcular tudo | com cache | razão | matriz de scores |
|---|---|---|---|---|
| 128 | 0,59 ms | 0,062 ms | 10x | 65.536 B vs 512 B |
| 256 | 1,20 ms | 0,163 ms | 7x | 262.144 B vs 1.024 B |
| 512 | 7,03 ms | 0,078 ms | 90x | 1.048.576 B vs 2.048 B |
| 1024 | 17,31 ms | 0,114 ms | 152x | 4.194.304 B vs 4.096 B |
| 2048 | 59,83 ms | 0,214 ms | 279x | 16.777.216 B vs 8.192 B |
| 4096 | 236,18 ms | 0,284 ms | 832x | 67.108.864 B vs 16.384 B |
A coluna da direita é a causa. Recalcular constrói a matriz de attention completa a cada passo — o da caixa de notação assintótica do Capítulo 9, pago uma vez por token. Com o cache, você constrói uma linha : 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 tokens a partir de uma partida fria:
| tokens gerados | com cache | recalculando | razão |
|---|---|---|---|
| 128 | 2,6 M | 192,0 M | 73x |
| 512 | 23,1 M | 7,36 G | 318x |
| 2048 | 293,7 M | 392,6 G | 1.336x |
Por passo, a versão com cache é linear no contexto e a sem cache é quadrática; somadas ao longo de uma geração, contra , 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 cache | 21,8 MB |
| recalculando | 181,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 diferentesOlhe de novo para a execução rápida: seu primeiro token se comportou de modo diferente dos outros quarenta e sete.
prefill, 40 prompt tokens : 1.0224 s -> 25.6 ms per token
decode, 47 steps : 0.1665 s mean per stepO 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:
Prefill
Link para a seção: PrefillUma 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 tokens:
| tokens do prompt | segundos | ms por token |
|---|---|---|
| 16 | 0,3515 | 21,97 |
| 32 | 0,5254 | 16,42 |
| 64 | 1,0491 | 16,39 |
| 128 | 1,6552 | 12,93 |
| 256 | 3,0965 | 12,10 |
Decode, um token contra um cache de :
| tokens em cache | ms para um token |
|---|---|
| 16 | 110,05 |
| 64 | 97,57 |
| 256 | 108,53 |
| 1024 | 103,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 é , 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 também é a conta
Link para a seção: O cache também é a contaO 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:
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á 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:
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/tokenExato, e continua exato em todos os formatos testados:
| batch | contexto | cache medido | previsto | pico de memória de trabalho |
|---|---|---|---|---|
| 1 | 512 | 6,0 MB | 6,0 MB | 15,4 MB |
| 1 | 16.384 | 192,0 MB | 192,0 MB | 207,3 MB |
| 1 | 65.536 | 768,0 MB | 768,0 MB | 793,7 MB |
| 8 | 4.096 | 384,0 MB | 384,0 MB | 401,5 MB |
| 32 | 2.048 | 768,0 MB | 768,0 MB | 794,2 MB |
| 64 | 1.024 | 768,0 MB | 768,0 MB | 797,0 MB |
| 128 | 512 | 768,0 MB | 768,0 MB | 816,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.
De onde vêm MQA e GQA
Link para a seção: De onde vêm MQA e GQAO 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 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 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 contexto | um usuário | 8 usuários | 64 usuários |
|---|---|---|---|
| 4.000 | 0,49 GB | 3,91 GB | 31,2 GB |
| 32.000 | 3,91 GB | 31,25 GB | 250,0 GB |
| 128.000 | 15,62 GB | 125,00 GB | 1.000,0 GB |
| 1.000.000 | 122,07 GB | 976,56 GB | 7.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 desceDecode é 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:
| batch | latência por passo | throughput | latência vs B=1 |
|---|---|---|---|
| 1 | 0,1286 s | 7,78 tok/s | 1,00x |
| 2 | 0,1839 s | 10,88 tok/s | 1,43x |
| 4 | 0,1909 s | 20,95 tok/s | 1,49x |
| 8 | 0,2781 s | 28,76 tok/s | 2,16x |
| 16 | 0,3430 s | 46,64 tok/s | 2,67x |
| 32 | 0,6302 s | 50,78 tok/s | 4,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 ganhaA forma ingênua de fazer batch é coletar 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ítica | tempo total | throughput | latência média por solicitação | slot-steps desperdiçados |
|---|---|---|---|---|
| batches estáticos de 8 | 176,9 s | 10,6 tok/s | 83,2 s | 3.214 |
| contínuo, 8 slots | 109,0 s | 17,2 tok/s | 8,1 s | 0 |
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á erradoA 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:
qmax = 2 ** (bits - 1) - 1
scale = W.abs().max() / qmax
Wq = torch.round(W / scale).clamp(-qmax - 1, qmax)
W_hat = Wq * scale # dequantizedEscolha 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 :
| esquema | erro relativo médio | pior matriz |
|---|---|---|
| INT8, uma escala para a matriz inteira | 0,0400 | 0,1487 |
| INT8, uma escala por linha de saída | 0,0100 | 0,0149 |
| INT4, uma escala para a matriz inteira | 0,6026 | 0,9931 |
| INT4, uma escala por linha de saída | 0,1790 | 0,2589 |
| INT4, uma escala por grupo de 128 | 0,1323 | 0,1992 |
| NF4, uma escala por bloco de 64 | 0,0952 | 0,1205 |
| INT3, uma escala por grupo de 128 | 0,3044 | 0,4123 |
| INT2, uma escala por grupo de 128 | 0,7790 | 0,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:
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 é 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.
As features outlier
Link para a seção: As features outlierA 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:
| camada | maior |h| | maior |h| da dimensão mediana | razão | dimensões acima de 6x a mediana |
|---|---|---|---|---|
| 1 | 6,19 | 0,339 | 18x | 2 |
| 4 | 1543,48 | 1,550 | 996x | 34 |
| 8 | 1571,63 | 1,498 | 1049x | 36 |
| 12 | 1575,03 | 1,546 | 1019x | 34 |
| 16 | 1579,60 | 1,617 | 977x | 32 |
| 20 | 1577,98 | 2,361 | 668x | 24 |
| 24 | 204,44 | 10,760 | 19x | 12 |
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:
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 | # 1Novecentas 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:
| esquema | erro relativo | níveis inteiros distintos usados, tensor inteiro |
|---|---|---|
| uma escala para o tensor inteiro | 0,1083 | 14 de 256 |
| uma escala por token (por linha) | 0,0433 | 158 |
| tensor inteiro, 1 dimensão outlier mantida em fp32 | 0,0442 | 48 |
| tensor inteiro, 4 dimensões outlier mantidas em fp32 | 0,0279 | 57 |
| tensor inteiro, 16 dimensões outlier mantidas em fp32 | 0,0085 | 102 |
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:
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 levelsUm 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, medidoQuase 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.
| esquema | erro médio dos pesos | perplexidade | bateria de perguntas | concorda com fp32 |
|---|---|---|---|---|
| fp32 (referência) | 0,0000 | 23,08 | 13/16 | 100,0% |
| INT8 por tensor | 0,0400 | 23,58 | 13/16 | — |
| INT8 por linha | 0,0100 | 22,96 | 13/16 | 98,6% |
| INT4 por tensor | 0,6026 | 365.416.000 | 0/16 | — |
| INT4 por linha | 0,1790 | 46,18 | 6/16 | 58,3% |
| INT4 grupo 128 | 0,1323 | 31,08 | 10/16 | 71,5% |
| NF4 bloco 64 | 0,0952 | 24,55 | 11/16 | 84,7% |
| INT3 grupo 128 | 0,3044 | 213,09 | 0/16 | 5,6% |
| INT2 grupo 128 | 0,7790 | 26.325.436 | 0/16 | 0,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.
Speculative decoding
Link para a seção: Speculative decodingO 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 tokens custa uma passagem forward sobre posições — um produto matriz-matriz, pouco mais caro que a passagem sobre uma. Então:
Um modelo pequeno e barato gera tokens candidatos de forma autorregressiva.
O modelo grande executa uma passagem forward sobre todos os 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 , 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 draft | aceitação | sequência aceita mais longa | tokens esperados por passagem do alvo, |
|---|---|---|---|
| fp32 (o próprio alvo) | 100,0% | 48 | 5,00 |
| INT8 por linha | 98,6% | 48 | 4,86 |
| NF4 bloco 64 | 84,7% | 20 | 3,69 |
| INT4 grupo 128 | 71,5% | 13 | 2,85 |
| INT4 por linha | 58,3% | 7 | 2,24 |
| INT3 grupo 128 | 5,6% | 2 | 1,06 |
| INT2 grupo 128 | 0,0% | 0 | 1,00 |
Os tokens esperados aceitos por passagem de verificação, com tamanho de draft , são
e o speedup líquido divide isso pelo custo do próprio draft, uma fração do alvo por token:
| aceitação | , | , | , | , |
|---|---|---|---|---|
| 30% | 1,19x | 1,02x | 0,79x | 0,79x |
| 50% | 1,61x | 1,38x | 1,08x | 1,11x |
| 70% | 2,31x | 1,98x | 1,54x | 1,78x |
| 90% | 3,41x | 2,93x | 2,28x | 3,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 tokens quase nunca é alcançada. Com 90% de aceitação, 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.
Distilação, e o que um soft label carrega
Link para a seção: Distilação, e o que um soft label carregaQuantizaçã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:
"She poured the milk into the"
' jug' 0.1355 ' cup' 0.1051 ' bowl' 0.0605 ' large' 0.0380 ' milk' 0.0360O 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 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 para 1,50 em — 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.
O que cabe em 8, 16 e 24 GB
Link para a seção: O que cabe em 8, 16 e 24 GBTudo neste capítulo agora é uma soma:
em que 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
| modelo | precisão | pesos | livre após overhead | tokens de contexto que cabem |
|---|---|---|---|---|
| 7B | fp16 | 13,0 GB | não cabe | — |
| 7B | int8 | 6,5 GB | não cabe | — |
| 7B | int4 (g128) | 3,4 GB | 3,1 GB | 25.710 |
| 13B | int4 (g128) | 6,2 GB | 0,3 GB | 337 |
| 70B | int4 (g128) | 33,6 GB | não cabe | — |
16 GB
| modelo | precisão | pesos | livre após overhead | tokens de contexto que cabem |
|---|---|---|---|---|
| 7B | fp16 | 13,0 GB | 1,5 GB | 11.972 |
| 7B | int8 | 6,5 GB | 8,0 GB | 65.378 |
| 7B | int4 (g128) | 3,4 GB | 11,1 GB | 91.246 |
| 13B | int8 | 12,1 GB | 2,4 GB | 3.136 |
| 13B | int4 (g128) | 6,2 GB | 8,3 GB | 10.822 |
24 GB
| modelo | precisão | pesos | livre após overhead | tokens de contexto que cabem |
|---|---|---|---|---|
| 7B | fp16 | 13,0 GB | 9,5 GB | 77.508 |
| 7B | int8 | 6,5 GB | 16,0 GB | 130.914 |
| 7B | int4 (g128) | 3,4 GB | 19,1 GB | 156.782 |
| 13B | int8 | 12,1 GB | 10,4 GB | 13.622 |
| 13B | int4 (g128) | 6,2 GB | 16,3 GB | 21.308 |
| 70B | int4 (g128) | 33,6 GB | nã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.
Para onde isso vai agora
Link para a seção: Para onde isso vai agoraTreze 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.
Fontes e método
Link para a seção: Fontes e métodoDuas 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 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.
Referências
Link para a seção: Referências-
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. ↩
-
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. ↩
-
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. ↩
-
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. ↩
-
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. ↩
-
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
-
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). ↩
-
Frantar, E., Ashkboos, S., Hoefler, T. and Alistarh, D. GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers. arXiv:2210.17323 (2022). ↩
-
Lin, J. et al. AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration. arXiv:2306.00978 (2023). ↩
-
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. ↩
-
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”. ↩
-
Buciluă, C., Caruana, R. and Niculescu-Mizil, A. Model Compression. KDD 2006. Distilação, nove anos antes, para ensembles em vez de transformers. ↩