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.
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 FLOPs por cada token que emite, em cada pedido, pelo resto da sua vida.
Para onde foi o tempo da segunda execução
Ligação para a secção: Para onde foi o tempo da segunda execuçãoPara 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:
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 , um passo de geração calculado das duas formas:
| tokens em contexto | recalcular tudo | com uma cache | rácio | 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 attention completa a cada passo — o da caixa de notação assintótica do Capítulo 9, pago uma vez por token. Com a cache, constrói antes uma linha : 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 tokens a partir de um arranque a frio:
| tokens gerados | com uma cache | a recalcular | rácio |
|---|---|---|---|
| 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 versão sem cache é quadrática; somado ao longo de uma geração, contra , 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 cache | 21,8 MB |
| a recalcular | 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, 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.
Prefill e decode são duas máquinas diferentes
Ligação para a secção: Prefill e decode são duas máquinas diferentesVolte a olhar para a execução rápida: o primeiro token comportou-se de forma 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 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 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 uma 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. 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 é , 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 também é a factura
Ligação para a secção: A cache também é a facturaA 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:
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 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:
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/tokenExacto, e mantém-se exacto em todas as formas testadas:
| batch | contexto | cache medida | 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 ú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.
De onde vêm MQA e GQA
Ligação para a secção: De onde vêm MQA e GQAO 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 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 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 contexto | um utilizador | 8 utilizadores | 64 utilizadores |
|---|---|---|---|
| 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 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.
Batching: o número que sobe e o número que desce
Ligação para a secção: Batching: o número que sobe e o número que desceO 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:
| 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 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 ganhaA forma ingénua de fazer batch é recolher 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ítica | tempo de parede | throughput | latência média por pedido | slot-steps desperdiçados |
|---|---|---|---|---|
| batches estáticos de 8 | 176,9 s | 10,6 tok/s | 83,2 s | 3.214 |
| contínuo, 8 lugares | 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, 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.
Quantização, e a primeira coisa que corre mal
Ligação para a secção: Quantização, e a primeira coisa que corre malA 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:
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 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 :
| 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 aproximadamente correcta. A causa é visível na mesma experiência sobre 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 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 é 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.
As outlier features
Ligação para a secção: As outlier featuresA 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:
| camada | maior |h| | maior |h| da dimensão mediana | rácio | 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 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:
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 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:
| 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 |
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:
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. 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.
O que a quantização custa realmente, medido
Ligação para a secção: O que a quantização custa realmente, medidoQuase 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.
| 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 % |
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.
Speculative decoding
Ligação para a secção: Speculative decodingO 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 tokens custa uma passagem forward sobre posições — um produto matriz-matriz, pouco mais caro do que a passagem sobre uma. Portanto:
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 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 , 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 model | aceitação | sequência aceite mais longa | tokens esperados por passagem do target, |
|---|---|---|---|
| fp32 (o próprio target) | 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 aceites esperados por passagem de verificação, com comprimento de draft , são
e o speedup líquido divide isso pelo custo do próprio draft, uma fracção do target 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 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 tokens quase nunca é alcançada. Com 90 % de aceitação, 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.
Distillation, e o que uma soft label transporta
Ligação para a secção: Distillation, e o que uma soft label transportaA 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:
"She poured the milk into the"
' jug' 0.1355 ' cup' 0.1051 ' bowl' 0.0605 ' large' 0.0380 ' milk' 0.0360A 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 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 para 1,50 em — 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.
O que cabe em 8, 16 e 24 GB
Ligação para a secçã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 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
| 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 —, 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.
Para onde isto vai a seguir
Ligação para a secção: Para onde isto vai a seguirTreze 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.
Fontes e método
Ligação para a secção: Fontes e métodoDuas 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 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.
Referências
Ligação para a secçã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 lê-se 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, razão pela qual a GQA se espalhou tão depressa. ↩
-
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. ↩
-
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. ↩
-
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. ↩
-
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
-
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. Distillation, nove anos antes, para ensembles em vez de transformers. ↩