Saltar ao contido
13/30Capítulo 13 de 30

Facer barata a inferencia: KV cache, batching e cuantización

O mesmo modelo responde igual en 8,8 s e en 78,9 s. Logo INT4, medido de tres maneiras, non afirmado.

Nesta páxina

O mesmo modelo, na mesma máquina, respondendo á mesma pregunta cos mesmos 48 tokens. As dúas saídas son idénticas token por token: comprobado, non suposto.

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)

Cambiou un argumento: use_cache=False. Nada sobre o modelo, o prompt, o sampling nin a aritmética é diferente, e a segunda execución non é máis precisa polo seu esforzo. É nove veces máis lenta para nada.

Esa é a forma deste capítulo. Todo o que hai nel —a cache, o batch, os pesos cuantizados— é un intento de deixar de pagar por traballo que non cambia a resposta, ou de descubrir canto custa unha resposta máis barata. O capítulo 10 estableceu a lista de prezos do adestramento. Esta é a lista de prezos do lado polo que pagas para sempre: un modelo despregado gasta aproximadamente 2N2N FLOPs por cada token que emite, en cada solicitude, durante o resto da súa vida.

Para xerar un token, un transformer só-decoder toma toda a secuencia ata ese momento, pásaa por todas as capas e le a distribución de probabilidade na última posición. Despois engade o token escollido e volve facelo. Esa descrición é correcta, e é o que fai a execución lenta.

Tamén é enormemente malgastadora, e a razón é a máscara causal do capítulo 9. Os vectores key e value da posición 7 calcúlanse a partir da entrada da posición 7 e das posicións anteriores. Cando chega a posición 8, a posición 7 non pode vela —iso é o que significa causal—, así que os key e value da posición 7 son exactamente os mesmos números ca antes. A execución lenta recalculaos igualmente, en cada paso.

Así que gárdaos. Ese almacén é a key-value cache, a optimización máis determinante no serving de modelos de linguaxe:

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)

Mira o que se lle pasa ao modelo dentro do bucle: nxt, un token. Non a secuencia. A query do novo token fai attention contra todas as keys gardadas na cache, e esas keys nunca ían cambiar. Isto non é unha aproximación: a comprobación de saída idéntica de enriba é a clave. A cache non troca calidade por velocidade; elimina aritmética redundante.

Para ver a escala con limpeza, retira o transformer e mide unha soa cabeza de attention con d=64d = 64, un paso de xeración calculado das dúas maneiras:

tokens no contextrecalcular todocon cacheratiomatriz de puntuacións
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 columna da dereita é a causa. Recalcular constrúe a matriz de attention completa n×nn \times n en cada paso: o O(n2)O(n^2) do cadro de notación asintótica do capítulo 9, pagado unha vez por token. Coa cache constrúes unha fila 1×n1 \times n: con 4,096 tokens, 67 MB de puntuacións fronte a 16 KB.

Contar multiplicacións-acumulacións en vez de milisegundos elimina a máquina do argumento. Para xerar TT tokens desde un arranque frío:

tokens xeradoscon cacherecalculandoratio
1282.6 M192.0 M73x
51223.1 M7.36 G318x
2048293.7 M392.6 G1,336x

Por paso, a versión con cache é lineal no context e a versión sen cache é cuadrática; sumado ao longo dunha xeración, O(T2)O(T^2) fronte a O(T3)O(T^3), cun ratio que medra sen límite. A diferenza de nove veces do inicio mediuse sobre 48 tokens: por debaixo da primeira fila desa táboa.

A cache tamén cambia o que ten que estar en memoria. Nunha GPU de portátil de 8 GB xerando 256 tokens en fp16, collendo o pico do asignador e restando os pesos residentes:

memoria de traballo máxima
con cache21.8 MB
recalculando181.7 MB

8,3 veces máis memoria, gastada para producir os mesmos tokens máis lentamente. Esta é a promesa feita no capítulo 5, chegando desde unha dirección inesperada: alí, o autodiff en modo inverso tiña que manter vivo cada intermedio para o paso cara atrás, e as activacións dominaban a memoria de adestramento. Na inferencia non hai paso cara atrás nin nada que reter para el; así que o que domina a memoria é a cache, e é unha elección deliberada máis ca un custo inevitable.

Prefill e decode son dúas máquinas distintas

Ligazón á sección: Prefill e decode son dúas máquinas distintas

Mira de novo a execución rápida: o seu primeiro token comportouse de maneira distinta aos outros corenta 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 xerado custou 166 ms. Mesmo modelo, mesmo hardware, mesmos pesos, unha diferenza de seis veces por token, e vai no sentido que a maioría da xente non espera. O prompt é a parte barata. A xeración divídese en dúas fases con física realmente distinta:

Unha pasada forward sobre todo o prompt. Todos os tokens procésanse en paralelo, así que cada matriz de pesos cárgase desde memoria unha vez e multiplícase contra unha matriz de centos de vectores de tokens: un produto matriz-matriz, con moita aritmética por byte movido, que é para o que está feita unha GPU. Prefill está limitado por cómputo, e o seu custo é aproximadamente lineal na lonxitude do prompt.

Unha pasada forward por token, batch dun e secuencia dun. Cada matriz de pesos segue cargándose completa desde memoria, e multiplícase contra un único vector: un produto matriz-vector, con case ningunha aritmética por byte movido. Decode está limitado polo ancho de banda da memoria, e o seu custo por token apenas depende da lonxitude do context.

As dúas metades pódense medir. Prefill, unha pasada sobre PP tokens:

tokens do promptsegundosms por token
160.351521.97
320.525416.42
641.049116.39
1281.655212.93
2563.096512.10

Decode, un token contra unha cache de CC:

tokens na cachems para un token
16110.05
6497.57
256108.53
1024103.86

Le a segunda táboa dúas veces. Pasar de 16 tokens de context a 1,024 —sesenta e catro veces máis historia sobre a que facer attention— non cambiou o custo dun paso en nada medible. A attention contra a cache é traballo real, pero queda empequenecida polo custo fixo de arrastrar medio billón de pesos polo bus de memoria para producir un vector. Ese custo fixo é a razón de todo o que hai na seguinte sección.

Estas dúas fases son a orixe dos dous números que informa todo sistema de serving. Time to first token é esencialmente prefill, e medra co prompt, que é por iso que unha conversa longa tarda en arrancar. Tokens por segundo é 1/decode step1/\text{decode step}, e é aproximadamente constante, que é por iso que a resposta logo flúe de maneira uniforme. Un chat que comeza lento e despois fai streaming con suavidade non é un truco de renderizado. Son estas dúas táboas.

A cache cambia aritmética por memoria, e a memoria que quere non é pequena. Para cada token no context, cada capa mantén un vector key e un vector value por cabeza key-value:

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

O 2 é por keys e values; todo o demais é a arquitectura. Para o modelo medido ao longo deste capítulo —24 capas, 14 cabezas query, 2 cabezas key-value, dimensión de cabeza 64— en fp16 iso son 2×24×2×64×2=12,2882 \times 24 \times 2 \times 64 \times 2 = 12{,}288 bytes por token.

As fórmulas neste campo teñen o costume de desviarse por un factor de dous, así que compróbao contra o asignador en vez de crer nelas:

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 segue sendo exacto en todas as formas probadas:

batchcontextcache medidaprevistamemoria de traballo máxima
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 tres filas merecen unha segunda mirada. Trinta e dous usuarios con 2,048 tokens cada un, sesenta e catro con 1,024, cento vinte e oito con 512: a cache é 768 MB en todos os casos, porque os tres conteñen 65,536 tokens. A cache depende só do número total de tokens residentes, non de como se distribúen entre usuarios. Ese feito é a base da sección de batching.

O capítulo 9 introduciu a attention multi-query e grouped-query e adiou a razón ata este capítulo. A razón é esa fórmula, e en concreto o HkvH_{kv} que hai nela.

A attention multi-head estándar dá a cada cabeza query as súas propias cabezas key e value. O modelo aquí ten 14 cabezas query; con attention multi-head completa, a súa cache sería de 2×24×14×64×2=86,0162 \times 24 \times 14 \times 64 \times 2 = 86{,}016 bytes por token: 84 KB en vez de 12 KB, exactamente sete veces máis, o ratio entre cabezas query e cabezas key-value.

A attention multi-query1 leva isto ao límite: todas as cabezas query comparten unha única cabeza key-value. A attention grouped-query2 é o compromiso que gañou: unhas poucas cabezas key-value, cada unha compartida por un grupo de cabezas query, porque a perda de calidade de MQA era real e a de GQA non. Ningunha compra aritmética. Existen para dividir esa fórmula por un enteiro, e espalláronse pola industria no momento en que os context longos fixeron da cache a restrición vinculante.

E faino axiña. Para un modelo da clase 7B con 32 capas e 8 cabezas key-value de dimensión 128, a cache é de 128 KB por token en fp16:

tokens de contextun usuario8 usuarios64 usuarios
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 propios pesos dese modelo son 13.0 GB en fp16, a cifra da táboa ao final deste capítulo. Así que cun context de 128,000 tokens, a cache dun usuario é máis grande ca o modelo. Esta é a aritmética que o capítulo 16 converte en diñeiro, e é por iso que unha conversa longa non é só unha conversa lenta: ocupa unha porción fixa dunha máquina mentres a solicitude estea viva.

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

Ligazón á sección: Batching: o número que sobe e o número que baixa

Decode está limitado pola memoria: os pesos arrástranse polo bus para producir un token, e as unidades aritméticas quedan ociosas. Así que mete máis traballo no mesmo paso. Executa varias solicitudes á vez, e os pesos, lidos unha vez, sérvenlles a todas. Medido no mesmo modelo, con cada solicitude mantendo unha cache de 64 tokens e facendo decode dun token:

batchlatencia por pasothroughputlatencia 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

Le as dúas columnas da dereita unha contra a outra, porque son todo o asunto. Pasar dunha solicitude a dezaseis multiplica o throughput por 6.0 e multiplica a espera de calquera solicitude individual por 2.67. O batch fixo mellor o servidor e peor a cada usuario.

Iso non é un bug que se poida axustar ata desaparecer; é o propio intercambio, e ten un nome en cada lado. A latencia é o que experimenta unha persoa agardando unha resposta. O throughput é aquilo polo que se divide a factura. Ningunha configuración mellora ambas as dúas cousas.

Fíxate tamén onde para. De 16 a 32, o throughput gaña un 9 % mentres a latencia case se duplica: o paso deixou de estar limitado pola memoria e pasou a estar limitado por cómputo, e máis aló dese xeonllo o batch non compra nada. Todo despregamento ten ese xeonllo; a súa localización hai que medila no teu, pero a súa existencia non.

O batching estático malgasta case todo o que gaña

Ligazón á sección: O batching estático malgasta case todo o que gaña

A maneira inxenua de facer batching é recoller BB solicitudes, executalas xuntas e devolver cando todas rematen. Pero non rematan xuntas: algunhas respostas teñen vinte tokens e outras cincocentos. Un batch fixo execútase ata que remata o seu membro máis longo, e cada solicitude xa rematada segue ocupando a súa ranura, contribuíndo padding, ata entón.

Colle 64 solicitudes cun sesgo realista de lonxitudes de saída —mediana de 18 tokens, a máis longa 231, 1,874 en total— e simula as dúas políticas co custo por paso medido para oito ranuras:

políticatempo realthroughputlatencia media por solicitudepasos-ranura malgastados
batches estáticos de 8176.9 s10.6 tok/s83.2 s3,214
continuo, 8 ranuras109.0 s17.2 tok/s8.1 s0

O throughput mellora 1.6x. A latencia media mellora máis de dez veces, porque baixo batching estático unha solicitude que rematou en catro pasos segue agardando por un veciño de 231 tokens antes de que ninguén saiba nada dela.

Continuous batching3 é a solución, e é tan simple como soa: o batch non é un grupo senón un conxunto de ranuras, e unha ranura que queda libre admite a seguinte solicitude en cola no paso inmediatamente posterior. O scheduler traballa coa granularidade dun token, non dunha solicitude. Todos os stacks de serving en produción fan isto agora.

Ten unha segunda metade, que é a cache. As ranuras que entran e saen deixan a memoria da cache fragmentada, e reservar para cada ranura o seu máximo context posible malgasta a maior parte da reserva. PagedAttention4 toma prestada a resposta dos sistemas operativos: gardar a cache en bloques de tamaño fixo cunha táboa de bloques por secuencia, de modo que a cache dunha secuencia poida estar fisicamente dispersa mentres segue sendo loxicamente contigua, o que tamén permite que dúas secuencias cun prefixo compartido compartan os bloques que o conteñen. Iso é sobre o que está construído vLLM, e por iso un motor de serving é un asignador de memoria cun transformer pegado.

A outra metade da factura son os propios pesos. Medio billón de parámetros a catro bytes cada un son 1.98 GB; a dous bytes, 0.99 GB; a un byte, 0.49 GB. Menos bits por peso reducen o modelo no disco, redúceno na memoria e —como decode está limitado polo ancho de banda— fan que cada paso sexa máis rápido, porque hai menos bytes que mover.

O esquema máis simple é a cuantización simétrica por máximo absoluto, e cabe en tres liñas:

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

Escolle unha escala para que o maior peso se mapee ao maior enteiro, divide, redondea, garda os enteiros e a escala. Reconstrúe multiplicando de volta. Non ten nada de intelixente, e funciona, ata que deixa de facelo.

Medido sobre os pesos reais do modelo: as 168 matrices de proxección, 357.8 millóns de parámetros, erro relativo WW^/W\lVert W - \hat{W}\rVert / \lVert W \rVert:

esquemaerro relativo mediopeor matriz
INT8, unha escala para toda a matriz0.04000.1487
INT8, unha escala por fila de saída0.01000.0149
INT4, unha escala para toda a matriz0.60260.9931
INT4, unha escala por fila de saída0.17900.2589
INT4, unha escala por grupo de 1280.13230.1992
NF4, unha escala por bloque de 640.09520.1205
INT3, unha escala por grupo de 1280.30440.4123
INT2, unha escala por grupo de 1280.77900.8076

A cuarta fila é o colapso. Un erro relativo de 0.99 na peor matriz significa que a reconstrución non retén practicamente nada do orixinal: a matriz foi substituída por ruído de magnitude aproximadamente correcta. A causa vese no mesmo experimento cunha soa 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 %)

Un peso de cada seis mil está máis aló de seis desviacións estándar, e o maior está a 24. Cunha única escala para toda a matriz, ese peso fixa o tamaño do paso para os 4.3 millóns de pesos. En 8 bits hai 256 pasos e o peso típico aínda cae nun significativo. En 4 bits hai 16, o máis externo reservado para un valor que case nada ten, e os pesos ordinarios —que son todos— redondéanse a dous ou tres niveis distintos.

Todo despois desa fila é a mesma reparación a distintas granularidades: dálle á escala un territorio máis pequeno. Por fila de saída divide o erro por 3.4; por grupo de 128 pesos consecutivos volve dividilo. O custo é contabilidade: unha escala de 16 bits por grupo de 128 son 4+16/128=4.1254 + 16/128 = 4.125 bits por peso en vez de 4, e con iso recupérase boa parte da diferenza.

NF4 abórdao polo outro lado.5 Os niveis non teñen por que estar igualmente espazados. Os pesos dentro dun bloque están distribuídos aproximadamente de maneira normal, así que escolle os dezaseis niveis como os cuantís dunha distribución normal: densos preto de cero, onde realmente están os pesos, espallados nas colas, onde non están. Os mesmos catro bits, a mesma escala por bloque, cun bloque máis pequeno —4.25 bits por peso fronte aos 4.125 do grupo-128—, e o erro medido baixa de 0.1323 a 0.0952, un 28 % menos. Parte diso é o bloque máis fino e o resto é poñer os niveis onde está a masa; separar ambas cousas precisaría unha terceira fila.

O cadro de coma flotante do capítulo 2 rematou cunha promesa: que este capítulo cuantizaría pesos a 8 e 4 bits e atoparía un feixe de outlier features negándose a ser apertadas. Aquí están, e explican por que «simplemente redondear os números» nunca ía funcionar nas activacións.

Os pesos de enriba portábanse mal. As activacións xogan noutra liga. Colle un prompt ordinario de 84 tokens, captura o residual stream en cada capa e mide a maior magnitude que alcanza cada unha das 896 dimensións:

capamaior |h|maior |h| da dimensión medianaratiodimensións por riba 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 dimensión 62 alcanza 1,579.6 mentres a dimensión mediana nunca pasa de 1.6. Non é unha casualidade dun token nin dunha capa: a mesma dimensión está aí na capa 4 e segue aí na capa 20, case co mesmo valor. Estas son as outlier features,6 e son sistemáticas: unha propiedade do modelo adestrado, non da entrada.

O histograma deses 896 máximos por dimensión na capa 16 fai que a forma sexa 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 dimensións nunha morea ordenada por baixo de 8, absolutamente nada durante tres oitavas, e logo unha soa dimensión no extremo afastado. Agora cuantiza ese tensor a INT8 e conta o que pasa:

esquemaerro relativoniveis enteiros distintos usados, tensor completo
unha escala para todo o tensor0.108314 de 256
unha escala por token (por fila)0.0433158
tensor completo, 1 dimensión outlier mantida en fp320.044248
tensor completo, 4 dimensións outlier mantidas en fp320.027957
tensor completo, 16 dimensións outlier mantidas en fp320.0085102

Catorce niveis de 256. A escala fixouna 1,579.6, así que cada paso ten 12.44 de ancho, e a activación típica —magnitude mediana 0.26, percentil noventa e nove 2.51— non ten onde caer. Por dimensión é máis extremo:

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

Un nivel. Toda a dimensión, cada token, cuantizada ao mesmo número. Asignáronse oito bits e empregáronse aproximadamente cero, e o modelo que le esas activacións recibe unha constante.

Esa medición é a xustificación de todas as técnicas que a xente usa de verdade:

Mantén os outliers fóra. LLM.int8()6 descompón a multiplicación de matrices: as dimensións con magnitudes extremas calcúlanse en 16 bits, todo o demais en INT8, e as metades súmanse. A táboa de enriba é o recibo: eliminar catro dimensións reduce o erro por un factor de case catro. SmoothQuant7 migra a dificultade: divide as activacións por un factor por canle e multiplica por el a columna de pesos correspondente, o que deixa o produto sen cambios e move o outlier fóra do tensor que non pode absorbelo cara ao que si pode.

Escolle o redondeo, non te limites a redondear. Nada do anterior pregunta para que serve a matriz. GPTQ8 cuantiza columna por columna e, despois de cada unha, axusta as columnas restantes en precisión completa para compensar o erro xa cometido, minimizando o erro da saída da capa en entradas reais en vez do dos seus pesos. AWQ9 observa que unha pequena fracción das canles de pesos importa moito máis ca o resto, atópaas a partir de estatísticas de activación e escálaas cara arriba antes de cuantizar para que caian en niveis máis finos. Ambas precisan un conxunto de calibración; ningunha precisa gradients.

Mostrar detalles

GGUF, e que ten que ver un formato de ficheiro con todo isto.

GGUF non é un método de cuantización; é o contedor que usa llama.cpp, e a confusión nas comparacións de gguf vs gptq vén de tratar ambas cousas como se fosen do mesmo tipo. GGUF garda tensores, tokenizer, metadatos de arquitectura e chat template nun único ficheiro mapeable en memoria, e leva dentro unha familia de esquemas por bloques: nomes como Q4_K_M codifican bits por peso, tamaño de bloque e se algúns tensores se manteñen a maior precisión.

A diferenza de enxeñaría que importa: GPTQ e AWQ producen pesos optimizados para un kernel de GPU, mentres que os esquemas de GGUF se decodifican barato nunha CPU co ficheiro mapeado en vez de cargado. Por iso o mesmo «modelo 7B de 4 bits» nominal existe nos dous mundos con tamaños e calidades diferentes, e por iso a comparación honesta nunca é o formato: é a medición de abaixo, executada na túa propia tarefa.

Case todos os artigos sobre cuantización paran na sección anterior: explican o método, citan un ratio de compresión e afirman que a calidade queda «en boa medida preservada». O capítulo 4 trataba de non enganarte a ti mesmo, así que imos descubrilo.

Mesmo modelo, pesos cuantizados in situ con cada esquema, e logo tres medicións: perplexity sobre 2,048 tokens de prosa inglesa reservada —aquí, o borrador deste curso, que é por iso que o repositorio substitúe un libro fixo de dominio público e imprime unha táboa coa mesma forma e números distintos—, unha batería de 16 preguntas factuais curtas con respostas coñecidas baixo greedy decoding, e a fracción de tokens nos que o modelo cuantizado coincide co de precisión completa dado idéntico context.

esquemaerro medio dos pesosperplexitybatería de preguntascoincide con fp32
fp32 (referencia)0.000023.0813/16100.0 %
INT8 por tensor0.040023.5813/16
INT8 por fila0.010022.9613/1698.6 %
INT4 por tensor0.6026365,416,0000/16
INT4 por fila0.179046.186/1658.3 %
INT4 grupo 1280.132331.0810/1671.5 %
NF4 bloque 640.095224.5511/1684.7 %
INT3 grupo 1280.3044213.090/165.6 %
INT2 grupo 1280.779026,325,4360/160.0 %

Hai catro cousas nesa táboa que paga a pena dicir con claridade.

INT8 feito ben é gratis. INT8 por fila marca 22.96 fronte aos 23.08 da referencia: unha diferenza dunha parte en douscentos, que é ruído e debe lerse como «idéntico». Cara a onde apunta o ruído non é estable: no corpus de dominio público do repositorio, os mesmos dous esquemas saen 22.24 fronte a 22.18, a metade desa distancia e apuntando ao outro lado. Coincide co modelo de precisión completa en 142 dos 144 tokens xerados. Un cuarto da memoria fronte á referencia fp32, a metade fronte ao fp16 que realmente despregarías, e sen custo detectable. INT8 feito sen coidado tamén é case gratis: unha escala por matriz custa 0.5 puntos de perplexity e ningunha resposta da batería. Oito bits son tan tolerantes que a granularidade apenas importa, que é exactamente por iso que a xente xeneraliza de INT8 a INT4 e sae escaldada.

INT4 cunha escala por tensor destrúe o modelo. Perplexity 365 millóns: non degradado, aniquilado. A granularidade convértese en todo o xogo: por tensor 365,416,000, por fila 46.18, por grupo de 128 31.08, NF4 24.55. Os mesmos catro bits por peso, un factor de quince millóns entre o peor e o mellor.

Perplexity é un instrumento groso e a batería aínda máis. Entre NF4 e INT4 grupo-128 a diferenza de perplexity é de 6.5 puntos e a batería difire nunha pregunta, e o intervalo de confianza do capítulo 4 di que unha pregunta de dezaseis non distingue absolutamente nada. Hai unha demostración máis afiada ca o intervalo: executa a mesma batería coa penalización de repetición de serie do modelo desactivada, que é o que realmente significa greedy decoding, e esas dúas filas intercambian posicións. Unha pregunta de dezaseis non é un efecto pequeno: non é efecto. A advertencia do capítulo 8 tamén se aplica: perplexity só é comparable entre modelos que comparten tokenizer, así que un número dun artigo alleo non se pode comparar co teu.

A columna de coincidencia é a máis precisa das tres, e case gratis: executa o modelo de precisión completa con greedy decoding, e despois pregúntalle ao cuantizado, en cada posición, que tería escollido dado o mesmo prefixo. Ten 144 observacións independentes en vez de 16, non precisa ground truth e degrádase suavemente onde a batería se degrada a saltos. Tamén é exactamente a cantidade que precisa a seguinte sección.

Esta é a promesa que o capítulo 1 fixo sobre este capítulo, chegando puntual: as matemáticas din que un modelo de 4 bits é posible, e a enxeñaría decide se é usable.

O capítulo 12 anunciouno e deixou a factura aquí.

A idea sae directamente da división prefill/decode. Verificar unha secuencia proposta de γ\gamma tokens custa unha pasada forward sobre γ\gamma posicións: un produto matriz-matriz, apenas máis caro ca a pasada sobre unha. Así que:

Un modelo pequeno e barato xera γ\gamma tokens candidatos de maneira autorregresiva.

O modelo grande executa unha pasada forward sobre todos os γ\gamma candidatos á vez, producindo o que tería dito en cada posición.

Conserva o prefixo máis longo no que ambos coinciden, máis o token que o modelo grande entrega gratis no primeiro desacordo. Descarta o resto e empeza de novo.

A distribución de saída non cambia. Con greedy decoding iso é obvio: un token só se acepta se o target o tería producido. Con sampling require unha regra de aceptación modificada, e Leviathan et al. demostran que a distribución resultante é exactamente a do target.10 Esta é a segunda optimización exacta deste capítulo.

Polo tanto, todo depende da taxa de aceptación α\alpha, que se pode medir: é a columna de coincidencia de enriba, que é por iso que se calculou alí. Usando cada modelo cuantizado como draft para o target de precisión completa, sobre 144 posicións xeradas:

modelo draftaceptaciónracha aceptada máis longatokens esperados por pasada do target, γ=4\gamma = 4
fp32 (o propio target)100.0 %485.00
INT8 por fila98.6 %484.86
NF4 bloque 6484.7 %203.69
INT4 grupo 12871.5 %132.85
INT4 por fila58.3 %72.24
INT3 grupo 1285.6 %21.06
INT2 grupo 1280.0 %01.00

Os tokens esperados aceptados por pasada de verificación, cunha lonxitude draft γ\gamma, son

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

e o speedup neto divide iso polo propio custo do draft, unha fracción cc do target por token:

aceptaciónc=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 en negra é a que hai que lembrar: speculative decoding pode facer a xeración máis lenta. Cunha aceptación do 30 % e un draft que custa unha quinta parte do target, pagas cinco pasadas forward e conservas 1.4 tokens. A última columna é a outra trampa: un draft máis longo só axuda cando a aceptación é alta, porque a cola dunha suposición de γ\gamma tokens case nunca se alcanza. Cunha aceptación do 90 %, γ=8\gamma = 8 vale 3.40x, e cun 30 % vale 0.79x: a mesma configuración, unha vitoria ou unha perda segundo un número medido no teu tráfico.

A cuantización reduce un modelo gardando a mesma función en menos bits. A destilación redúceo adestrando un modelo máis pequeno para imitar un máis grande11, unha idea anterior ao deep learning en case unha década.12

A parte sutil é de que aprende o estudante. Non da resposta correcta: podería terse adestrado directamente con iso. O que engade o profesor é a distribución completa. Pregúntalle ao modelo que segue a unha frase e mira máis aló 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 di jug e nada máis. A soft label di jug, e tamén que cup era case igual de bo, bowl plausible, e large —un adxectivo, unha continuación gramatical completamente distinta— seguía viva. Ese é o argumento orixinal: isto é un 7, pero parécese bastante a un 1, e a semellanza é información que a hard label tira ao lixo.

Tamén é por iso que a destilación usa temperatura. Dividir os logits por TT antes da softmax achanda a distribución e aumenta o peso relativo dos segundos clasificados: nesta frase, o ratio entre o token principal e o terceiro baixa de 2.24 con T=1T = 1 a 1.50 con T=2T = 2, a raíz cadrada do primeiro, que é o que dividir os logits por dous lle fai a un ratio. Mesma orde, máis attention da perda nos case erros. O gradient do estudante leva a incerteza do profesor, non só o seu veredicto.

Todo neste capítulo é agora unha suma:

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}}

onde TT son os tokens totais residentes en todas as solicitudes concorrentes. Aplicándoo: as filas 7B e 70B asumen 8 cabezas key-value de dimensión 128; a fila 13B, attention multi-head completa con 40 cabezas, que é como se construíron esas xeracións de modelos, e nótase.

8 GB

modeloprecisiónpesoslibre tras overheadtokens de context que caben
7Bfp1613.0 GBnon cabe
7Bint86.5 GBnon cabe
7Bint4 (g128)3.4 GB3.1 GB25,710
13Bint4 (g128)6.2 GB0.3 GB337
70Bint4 (g128)33.6 GBnon cabe

16 GB

modeloprecisiónpesoslibre tras overheadtokens de context que caben
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

modeloprecisiónpesoslibre tras overheadtokens de context que caben
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 GBnon cabe

Mira a fila 13B na táboa de 8 GB. Os pesos caben —6.2 GB de 8—, así que coa maneira habitual de falar, un modelo 13B «execútase nunha tarxeta de 8 GB». Ten 337 tokens de context, que non é unha conversa senón apenas un prompt. «Cabe?» é a pregunta equivocada. A correcta é «con canto context, e para cantos usuarios á vez».

Mira tamén as dúas filas int8 de 16 GB. O 7B obtén 65,378 tokens e o 13B obtén 3,136: unha diferenza de vinte veces a partir de 5.6 GB de pesos extra, porque este 13B ten attention multi-head e a súa cache custa 800 KB por token fronte aos 128 KB do 7B. Dous modelos de tamaño parecido, un inutilizable para context longo, por unha razón que non aparece no titular de ningunha model card.

Hai trece capítulos isto era un perceptrón con dous pesos e un bias. Agora é un transformer que foi deseñado, adestrado, aliñado, ensinado a gastar cómputo en preguntas difíciles e servido cun custo medido por token, sen que quede ningunha caixa sen abrir dentro del.

Isto remata aquí, e remata a propósito.

O capítulo 14 comeza co modelo noutro sitio. Non no teu proceso, non na túa memoria, non nunha variable que poidas imprimir: nunha máquina que non administras, detrás dunha API key, un porto e unha factura. Todo o medido aquí segue acontecendo: prefill segue executándose antes do primeiro token, a cache segue medrando coa conversa, o batch no que estás segue pertencendo a outra persoa e segue decidindo a túa latencia; pero a partir de agora obsérvalo a través dun fluxo de Server-Sent Events, un finish_reason e un HTTP 429 cunha cabeceira Retry-After. As preguntas cambian co punto de vista: non como se calcula este gradient, senón por que se triplicou a miña factura. Tamén cambia a linguaxe, e o capítulo 14 explica esa regra en vez de anunciala: ata aquí o código contiña pesos, gradients, logits e bytes de tokenizer; a partir de aí contén unha conexión, un reintento, unha cancelación e estado acumulado. Os trece capítulos que deixas atrás non se descartan ao cruzar. Son a descrición do que se executa ao outro lado do porto.


Dúas omisións son deliberadas. FlashAttention (Dao et al., arXiv:2205.14135) non é unha attention diferente: calcula a mesma función dividindo a operación en tiles para que a matriz de puntuacións n×nn \times n nunca se escriba en memoria, que é por iso que os 67 MB da segunda táboa deste capítulo son menores na práctica do que suxire a aritmética. E os propios kernels deléganse: a lección 10 do CS336 de Stanford cobre sistemas de inferencia coa profundidade que isto non intenta, e o repositorio llama.cpp e a especificación GGUF son as fontes primarias para o lado CPU.

  1. Shazeer, N. Fast Transformer Decoding: One Write-Head is All You Need. arXiv:1911.02150 (2019). O artigo é en boa medida un argumento sobre ancho de banda de memoria, e lese como tal.

  2. Ainslie, J. et al. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. arXiv:2305.13245 (2023). Inclúe a receita de uptraining que converte un checkpoint multi-head existente, que é por iso que GQA se espallou tan rápido.

  3. Yu, G.-I., Jeong, J. S., Kim, G.-W., Kim, S. and Chun, B.-G. Orca: A Distributed Serving System for Transformer-Based Generative Models. OSDI 2022. Introduce a planificación a nivel de iteración —continuous batching— e o 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 sobre o que está construído vLLM; a §3 desenvolve por completo a analoxía cos sistemas operativos.

  5. Dettmers, T., Pagnoni, A., Holtzman, A. and Zettlemoyer, L. QLoRA: Efficient Finetuning of Quantized LLMs. arXiv:2305.14314 (2023). NF4 defínese na §3; os dezaseis valores de nivel usados na medición de enriba son os que deriva este artigo.

  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 enriba, incluído o achado de que os outliers emerxen sistematicamente a 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 proba de que a distribución de saída non cambia; Chen et al. (arXiv:2302.01318) publicaron a mesma idea de maneira 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. Destilación, nove anos antes, para ensembles máis ca para transformers.

Listo para deixar que LIA escolla por ti?

Crea con todos os modelos de IA nun só sitio: empeza gratis hoxe mesmo.