Introduzione alla Quantizzazione dei Pesi

'Introduzione alla Quantizzazione dei Pesi' can be condensed to 'Introduzione alla Quantizzazione'.

Riduzione delle dimensioni dei Modelli di Linguaggio Grandi con quantizzazione a 8-bit

I Modelli di Linguaggio Grandi (LLM) sono noti per le loro estese esigenze computazionali. Tipicamente, la dimensione di un modello viene calcolata moltiplicando il numero di parametri ( dimensione ) per la precisione di questi valori ( tipo di dato ). Tuttavia, per risparmiare memoria, i pesi possono essere memorizzati utilizzando tipi di dati a precisione inferiore attraverso un processo noto come quantizzazione.

Distinguiamo due famiglie principali di tecniche di quantizzazione dei pesi nella letteratura:

  • Quantizzazione dopo l’addestramento (PTQ) è una tecnica diretta in cui i pesi di un modello già addestrato vengono convertiti in precisione inferiore senza richiedere alcun nuovo addestramento. Sebbene facile da implementare, la PTQ è associata a una potenziale degradazione delle prestazioni.
  • Addestramento consapevole della quantizzazione (QAT) incorpora il processo di conversione dei pesi durante la fase di pre-addestramento o di affinamento, con conseguente miglioramento delle prestazioni del modello. Tuttavia, la QAT è computazionalmente costosa e richiede dati di addestramento rappresentativi.

In questo articolo, ci concentriamo sulla PTQ per ridurre la precisione dei nostri parametri. Per avere una buona intuizione, applicheremo tecniche sia naive che più sofisticate a un esempio di prova utilizzando un modello GPT-2.

L’intero codice è liberamente disponibile su Google Colab e GitHub .

📚 Background sulla Rappresentazione in Punto Finito

La scelta del tipo di dato determina la quantità di risorse computazionali richiesta, influenzando la velocità e l’efficienza del modello. Nelle applicazioni di deep learning, bilanciare la precisione e le prestazioni computazionali diventa un esercizio vitale poiché una maggiore precisione spesso implica maggiori richieste computazionali.

Tra i vari tipi di dati, i numeri in virgola mobile sono prevalentemente impiegati nel deep learning per la loro capacità di rappresentare una vasta gamma di valori con alta precisione. Tipicamente, un numero in virgola mobile utilizza n bit per memorizzare un valore numerico. Questi n bit sono suddivisi in tre componenti distinte:

  1. Segno : Il bit del segno indica la natura positiva o negativa del numero. Utilizza un bit in cui 0 indica un numero positivo e 1 segnala un numero negativo.
  2. Esponente : L’esponente è un segmento di bit che rappresenta la potenza alla quale la base (solitamente 2 nella rappresentazione binaria) viene elevata. L’esponente può essere positivo o negativo, consentendo al numero di rappresentare valori molto grandi o molto piccoli.
  3. Significando/Mantissa : I bit rimanenti vengono utilizzati per memorizzare il significando, anche chiamato mantissa. Questo rappresenta le cifre significative del numero. La precisione del numero dipende fortemente dalla lunghezza del significando.

Questa progettazione consente ai numeri in virgola mobile di coprire una vasta gamma di valori con diversi livelli di precisione. La formula utilizzata per questa rappresentazione è:

Per capire meglio questo, approfondiamo alcuni dei tipi di dati più comunemente utilizzati nel deep learning: float32 (FP32), float16 (FP16) e bfloat16 (BF16):

  • FP32 utilizza 32 bit per rappresentare un numero: un bit per il segno, otto per l’esponente e i restanti 23 per il significando. Sebbene fornisca un alto grado di precisione, il lato negativo di FP32 è il suo elevato impatto computazionale e di memoria.
  • FP16 utilizza 16 bit per memorizzare un numero: uno è utilizzato per il segno, cinque per l’esponente e dieci per il significando. Questo lo rende più efficiente in termini di memoria e accelera i calcoli, ma la ridotta gamma e precisione possono introdurre instabilità numerica, con possibili ripercussioni sulla precisione del modello.
  • BF16 è anche un formato a 16 bit ma con un bit per il segno, otto per l’esponente e sette per il significando. BF16 amplia la gamma rappresentabile rispetto a FP16, riducendo così i rischi di underflow e overflow. Nonostante una riduzione della precisione dovuta a meno bit per il significando, BF16 di solito non influisce significativamente sulle prestazioni del modello ed è un compromesso utile per compiti di deep learning.
Immagine dell'autore

Nel gergo dell’apprendimento automatico, FP32 è spesso definito “precisione completa” (4 byte), mentre BF16 e FP16 sono “mezza precisione” (2 byte). Ma potremmo fare ancora meglio e memorizzare i pesi utilizzando un solo byte? La risposta è il tipo di dato INT8, che consiste in una rappresentazione a 8 bit in grado di memorizzare 256 valori diversi. Nella prossima sezione, vedremo come convertire i pesi FP32 in un formato INT8.

🔰 Quantizzazione naïve a 8 bit

In questa sezione, implementeremo due tecniche di quantizzazione: una simmetrica con quantizzazione massima assoluta (absmax) e una asimmetrica con quantizzazione del punto zero. In entrambi i casi, l’obiettivo è mappare un tensore FP32 X (pesi originali) in un tensore INT8 X_quant (pesi quantizzati).

Con la quantizzazione absmax, il numero originale viene diviso per il valore massimo assoluto del tensore e moltiplicato per un fattore di scala (127) per mappare gli input nell’intervallo [-127, 127]. Per recuperare i valori FP16 originali, il numero INT8 viene diviso per il fattore di quantizzazione, riconoscendo una certa perdita di precisione dovuta all’arrotondamento.

Ad esempio, supponiamo di avere un valore massimo assoluto di 3,2. Un peso di 0,1 verrebbe quantizzato come round(0,1 × 127/3,2) = 4. Se volessimo dequantizzarlo, otterremmo 4 × 3,2/127 = 0,1008, con un errore di 0,008. Ecco l’implementazione in Python corrispondente:

import torch

def absmax_quantize(X):
    # Calcola il fattore di scala
    scale = 127 / torch.max(torch.abs(X))
    # Quantizza
    X_quant = (scale * X).round()
    # Dequantizza
    X_dequant = X_quant / scale
    return X_quant.to(torch.int8), X_dequant

Con la quantizzazione del punto zero, possiamo considerare distribuzioni di input asimmetriche, che è utile quando si considera l’output di una funzione ReLU (solo valori positivi), ad esempio. I valori di input vengono prima scalati dall’intervallo completo di valori (255) diviso per la differenza tra il valore massimo e quello minimo. Questa distribuzione viene quindi spostata dal punto zero per mapparla nell’intervallo [-128, 127] (notare il valore aggiuntivo rispetto all’absmax). Prima di tutto, calcoliamo il fattore di scala e il valore del punto zero:

Quindi, possiamo utilizzare queste variabili per quantizzare o dequantizzare i nostri pesi:

Prendiamo ad esempio: abbiamo un valore massimo di 3,2 e un valore minimo di -3,0. Possiamo calcolare che il fattore di scala è 255/(3,2 + 3,0) = 41,13 e il punto zero è -round(41,13 × -3,0) – 128 = 123 -128 = -5, quindi il nostro peso precedente di 0,1 sarebbe quantizzato come round(41,13 × 0,1 -5) = -1. Questo è molto diverso dal valore precedente ottenuto utilizzando absmax (4 vs -1).

Immagine dell'autore

L’implementazione in Python è piuttosto semplice:

def zeropoint_quantize(X):    # Calcola l'intervallo di valori (denominatore)    x_range = torch.max(X) - torch.min(X)    x_range = 1 if x_range == 0 else x_range    # Calcola la scala    scale = 255 / x_range    # Sposta il punto di riferimento a zero    zeropoint = (-scale * torch.min(X) - 128).round()    # Scala e arrotonda gli input    X_quant = torch.clip((X * scale + zeropoint).round(), -128, 127)    # Dequantizza    X_dequant = (X_quant - zeropoint) / scale    return X_quant.to(torch.int8), X_dequant

Invece di fare affidamento su esempi completamente giocattolo, possiamo utilizzare queste due funzioni su un modello reale grazie alla libreria transformers.

Iniziamo caricando il modello e il tokenizer per GPT-2. Si tratta di un modello molto piccolo che probabilmente non vogliamo quantizzare, ma sarà sufficiente per questo tutorial. Prima di tutto, vogliamo osservare la dimensione del modello in modo da poterla confrontare in seguito ed valutare il risparmio di memoria dovuto alla quantizzazione a 8 bit.

!pip install -q bitsandbytes>=0.39.0!pip install -q git+https://github.com/huggingface/accelerate.git!pip install -q git+https://github.com/huggingface/transformers.git

from transformers import AutoModelForCausalLM, AutoTokenizerimport torchtorch.manual_seed(0)# Imposta il dispositivo su CPU per oradevice = 'cpu'# Carica il modello e il tokenizemodel_id = 'gpt2'model = AutoModelForCausalLM.from_pretrained(model_id).to(device)tokenizer = AutoTokenizer.from_pretrained(model_id)# Stampa la dimensione del modelloprint(f"Dimensione del modello: {model.get_memory_footprint():,} byte")

Dimensione del modello: 510,342,192 byte

La dimensione del modello GPT-2 è di circa 487 MB in FP32. Il passo successivo consiste nel quantizzare i pesi utilizzando la quantizzazione a zero punto e absmax. Nell’esempio seguente, applichiamo queste tecniche al primo livello di attenzione di GPT-2 per vedere i risultati.

# Estra i pesi del primo livelloweights = model.transformer.h[0].attn.c_attn.weight.dataprint("Pesi originali:")print(weights)# Quantizza il livello utilizzando la quantizzazione absmaxweights_abs_quant, _ = absmax_quantize(weights)print("\nPesi quantizzati con absmax:")print(weights_abs_quant)# Quantizza il livello utilizzando la quantizzazione a zero puntoweights_zp_quant, _ = zeropoint_quantize(weights)print("\nPesi quantizzati a zero punto:")print(weights_zp_quant)

Pesi originali:tensor([[-0.4738, -0.2614, -0.0978,  ...,  0.0513, -0.0584,  0.0250],        [ 0.0874,  0.1473,  0.2387,  ..., -0.0525, -0.0113, -0.0156],        [ 0.0039,  0.0695,  0.3668,  ...,  0.1143,  0.0363, -0.0318],        ...,        [-0.2592, -0.0164,  0.1991,  ...,  0.0095, -0.0516,  0.0319],        [ 0.1517,  0.2170,  0.1043,  ...,  0.0293, -0.0429, -0.0475],        [-0.4100, -0.1924, -0.2400,  ..., -0.0046,  0.0070,  0.0198]])Pesi quantizzati con absmax:tensor([[-21, -12,  -4,  ...,   2,  -3,   1],        [  4,   7,  11,  ...,  -2,  -1,  -1],        [  0,   3,  16,  ...,   5,   2,  -1],        ...,        [-12,  -1,   9,  ...,   0,  -2,   1],        [  7,  10,   5,  ...,   1,  -2,  -2],        [-18,  -9, -11,  ...,   0,   0,   1]], dtype=torch.int8)Pesi quantizzati a zero punto:tensor([[-20, -11,  -3,  ...,   3,  -2,   2],        [  5,   8,  12,  ...,  -1,   0,   0],        [  1,   4,  18,  ...,   6,   3,   0],        ...,        [-11,   0,  10,  ...,   1,  -1,   2],        [  8,  11,   6,  ...,   2,  -1,  -1],        [-18,  -8, -10,  ...,   1,   1,   2]], dtype=torch.int8)

La differenza tra i valori originali (FP32) e quelli quantizzati (INT8) è evidente, ma la differenza tra i pesi assoluti massimi e i pesi del punto zero è più sottile. In questo caso, le voci sembrano spostate di un valore di -1. Ciò suggerisce che la distribuzione dei pesi in questo livello è abbastanza simmetrica.

Possiamo confrontare queste tecniche quantizzando ogni livello in GPT-2 (livelli lineari, livelli di attenzione, ecc.) e creando due nuovi modelli: model_abs e model_zp. Per essere precisi, in realtà sostituiremo i pesi originali con quelli de-quantizzati. Ciò ha due vantaggi: ci consente di 1/ confrontare la distribuzione dei nostri pesi (stessa scala) e 2/ effettivamente eseguire i modelli.

Infatti, PyTorch non permette la moltiplicazione di matrici INT8 per impostazione predefinita. In uno scenario reale, dovremmo de-quantizzarli per eseguire il modello (ad esempio in FP16) ma conservarli come INT8. Nella sezione successiva, utilizzeremo la libreria bitsandbytes per risolvere questo problema.

import numpy as npfrom copy import deepcopy# Memorizza i pesi originaliweights = [param.data.clone() for param in model.parameters()]# Crea il modello per quantizzaremodel_abs = deepcopy(model)# Quantizza tutti i pesi del modelloweights_abs = []for param in model_abs.parameters():    _, dequantized = absmax_quantize(param.data)    param.data = dequantized    weights_abs.append(dequantized)# Crea il modello per quantizzaremodel_zp = deepcopy(model)# Quantizza tutti i pesi del modelloweights_zp = []for param in model_zp.parameters():    _, dequantized = zeropoint_quantize(param.data)    param.data = dequantized    weights_zp.append(dequantized)

Ora che i nostri modelli sono stati quantizzati, vogliamo verificare l’impatto di questo processo. Intuitivamente, vogliamo assicurarci che i pesi quantizzati siano vicini ai pesi originali. Un modo visivo per verificarlo è rappresentare graficamente la distribuzione dei pesi de-quantizzati e originali. Se la quantizzazione è perdente, cambierebbe drasticamente la distribuzione dei pesi.

La figura seguente mostra questo confronto, in cui l’istogramma blu rappresenta i pesi originali (FP32) e quello rosso rappresenta i pesi de-quantizzati (da INT8). Nota che mostriamo solo questo grafico tra -2 e 2 a causa di valori anomali con valori assoluti molto alti (ne parleremo più avanti).

Entrambi i grafici sono abbastanza simili, con uno spiacevole picco intorno allo 0. Questo picco mostra che la nostra quantizzazione è piuttosto perdente poiché invertendo il processo non si ottengono i valori originali. Questo è particolarmente vero per il modello absmax, che mostra sia una valle inferiore che un picco superiore intorno allo 0.

Confrontiamo le prestazioni dei modelli originali e quantizzati. A questo scopo, definiamo una funzione generate_text() per generare 50 token con il campionamento top-k.

def generate_text(model, input_text, max_length=50):    input_ids = tokenizer.encode(input_text, return_tensors='pt').to(device)    output = model.generate(inputs=input_ids,                            max_length=max_length,                            do_sample=True,                            top_k=30,                            pad_token_id=tokenizer.eos_token_id,                            attention_mask=input_ids.new_ones(input_ids.shape))    return tokenizer.decode(output[0], skip_special_tokens=True)# Genera testo con modelli originali e quantizzatioriginal_text = generate_text(model, "Ho un sogno")absmax_text   = generate_text(model_abs, "Ho un sogno")zp_text       = generate_text(model_zp, "Ho un sogno")print(f"Modello originale:\n{original_text}")print("-" * 50)print(f"Modello absmax:\n{absmax_text}")print("-" * 50)print(f"Modello zeropoint:\n{zp_text}")

Modello originale:Ho un sogno e credo che potrei viverlo nel mio futuro. Amo mia madre e c’è stato quel momento in cui mi è stato detto che la mia famiglia non era nemmeno così forte. E poi ho ottenuto il————————————————–Modello absmax:Ho un sogno di scoprire l’origine dei suoi capelli. Lei li ama. Ma non c’è modo che tu possa essere onesto su come sono fatti i suoi capelli. Deve essere pazza. Abbiamo trovato una foto del taglio di capelli pubblicata su————————————————–Modello zeropoint:Ho un sogno di creare due posti di lavoro a tempo pieno in America: uno per le persone con problemi di salute mentale e uno per le persone che non soffrono di malattie mentali o che hanno almeno una storia lavorativa e familiare di abuso di sostanze, per lavorare a tempo parziale

Invece di cercare di capire se un output ha più senso degli altri, possiamo quantificarlo calcolando la perplessità di ogni output. Questa è una metrica comune utilizzata per valutare i modelli di linguaggio, che misura l’incertezza di un modello nel prevedere il prossimo token in una sequenza. In questo confronto, facciamo l’assunzione comune che più basso è il punteggio, migliore è il modello. In pratica, una frase con una perplessità alta potrebbe comunque essere corretta.

La implementiamo utilizzando una funzione minimale, poiché non è necessario considerare dettagli come la lunghezza della finestra di contesto, dato che le nostre frasi sono brevi.

def calcola_perplessita(modello, testo):    # Codifica il testo    codifiche = tokenizer(testo, return_tensors='pt').to(device)    # Definisci input_ids e target_ids    input_ids = codifiche.input_ids    target_ids = input_ids.clone()    with torch.no_grad():        outputs = modello(input_ids, labels=target_ids)    # Calcolo della loss    neg_log_likelihood = outputs.loss    # Calcolo della perplessità    ppl = torch.exp(neg_log_likelihood)    return pplppl     = calcola_perplessita(modello, testo_originale)ppl_abs = calcola_perplessita(modello_abs, testo_absmax)ppl_zp  = calcola_perplessita(modello_zp, testo_absmax)print(f"Perplessità originale:  {ppl.item():.2f}")print(f"Perplessità absmax:    {ppl_abs.item():.2f}")print(f"Perplessità zeropoint: {ppl_zp.item():.2f}")

Vediamo che la perplessità del modello originale è leggermente più bassa rispetto agli altri due. Un singolo esperimento non è molto affidabile, ma potremmo ripetere questo processo più volte per vedere la differenza tra ogni modello. In teoria, la quantizzazione a punto zero dovrebbe essere leggermente migliore della quantizzazione absmax, ma è anche più costosa da calcolare.

In questo esempio, abbiamo applicato tecniche di quantizzazione a interi livelli (su base per-tensore). Tuttavia, potremmo applicarle a diversi livelli di granularità: dall’intero modello a valori individuali. Quantizzare l’intero modello in un’unica passata degraderebbe seriamente le prestazioni, mentre quantizzare valori individuali creerebbe un grande overhead. In pratica, spesso preferiamo la quantizzazione vettoriale, che considera la variabilità dei valori nelle righe e nelle colonne all’interno dello stesso tensore.

Tuttavia, anche la quantizzazione vettoriale non risolve il problema delle caratteristiche anomale. Le caratteristiche anomale sono valori estremi (negativi o positivi) che compaiono in tutti i livelli del transformer quando il modello raggiunge una certa dimensione (>6,7 miliardi di parametri). Questo è un problema perché una singola caratteristica anomala può ridurre la precisione per tutti gli altri valori. Ma scartare queste caratteristiche anomale non è un’opzione in quanto degraderebbe pesantemente le prestazioni del modello.

🔢 Quantizzazione a 8 bit con LLM.int8()

Introdotto da Dettmers et al. (2022), LLM.int8() è una soluzione al problema delle caratteristiche anomale. Si basa su uno schema di quantizzazione vettoriale (absmax) e introduce la quantizzazione a precisione mista. Ciò significa che le caratteristiche anomale vengono elaborate in formato FP16 per mantenere la loro precisione, mentre gli altri valori vengono elaborati in formato INT8. Poiché le caratteristiche anomale rappresentano circa lo 0,1% dei valori, questo riduce efficacemente l’occupazione di memoria del LLM di quasi il 2x.

Immagine dell'autore

LLM.int8() funziona eseguendo il calcolo della moltiplicazione tra matrici in tre passaggi chiave:

  1. Estrae colonne dagli stati nascosti di input X che contengono caratteristiche anomale utilizzando una soglia personalizzata.
  2. Esegue la moltiplicazione tra matrici delle caratteristiche anomale utilizzando FP16 e delle caratteristiche non anomale utilizzando INT8 con quantizzazione vettoriale (per righe per lo stato nascosto X e per colonne per la matrice di pesi W).
  3. Dequantizza i risultati delle caratteristiche non anomale (da INT8 a FP16) e li aggiunge ai risultati delle caratteristiche anomale per ottenere il risultato completo in FP16.
Immagine dell'autore

Questo approccio è necessario perché la precisione a 8 bit è limitata e può portare a errori sostanziali durante la quantizzazione di un vettore con valori elevati. Questi errori tendono anche ad amplificarsi mentre si propagano attraverso più strati.

Possiamo facilmente utilizzare questa tecnica grazie all’integrazione della libreria bitsandbytes nell’ecosistema di Hugging Face. Dobbiamo semplicemente specificare load_in_8bit=True durante il caricamento del modello (richiede anche una GPU).

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')model_int8 = AutoModelForCausalLM.from_pretrained(model_id,                                             device_map='auto',                                             load_in_8bit=True,                                             )print(f"Dimensione del modello: {model_int8.get_memory_footprint():,} byte")

Dimensione del modello: 176.527.896 byte

Con questa riga di codice aggiuntiva, il modello è ora quasi tre volte più piccolo (168 MB rispetto a 487 MB). Possiamo anche confrontare la distribuzione dei pesi originali e quantizzati come abbiamo fatto in precedenza:

In questo caso, vediamo picchi intorno a -2, -1, 0, 1, 2, ecc. Questi valori corrispondono ai parametri memorizzati nel formato INT8 (non anomali). Puoi verificarlo stampando i pesi del modello usando model_int8.parameters().

Possiamo anche generare del testo con questo modello quantizzato e confrontarlo con il modello originale.

# Genera testo con il modello quantizzato text_int8 = generate_text(model_int8, "Ho un sogno")print(f"Modello originale:\n{original_text}")print("-" * 50)print(f"Modello LLM.int8():\n{text_int8}")

Modello originale:Ho un sogno, ed è un sogno in cui credo che potrei vivere nel mio futuro. Amo mia madre, e c'è stato quel momento in cui mi è stato detto che la mia famiglia non era nemmeno così forte. E poi sono arrivata--------------------------------------------------Modello LLM.int8():Ho un sogno. Non so cosa ne verrà fuori, ma dovrò cercare qualcosa che sia giusto. Non ci ho pensato per molto tempo, ma devo cercare di ottenere quella cosa

Anche in questo caso, è difficile giudicare quale sia il miglior risultato, ma possiamo fare affidamento sulla metrica di perplessità per darci una risposta (approssimativa).

print(f"Perplessità (originale):   {ppl.item():.2f}")ppl = calculate_perplexity(model_int8, text_int8)print(f"Perplessità (LLM.int8()): {ppl.item():.2f}")

Perplessità (originale):   15.53Perplessità (LLM.int8()): 7.93

In questo caso, la perplessità del modello quantizzato è due volte più bassa rispetto a quella originale. In generale, questo non è sempre il caso, ma mostra che questa tecnica di quantizzazione è molto competitiva. Infatti, gli autori di LLM.int8() mostrano che la degradazione delle prestazioni è così bassa da essere trascurabile (<1%). Tuttavia, ha un costo aggiuntivo in termini di calcolo: LLM.int8() è approssimativamente circa il 20% più lento per modelli grandi.

Conclusioni

Questo articolo fornisce una panoramica delle tecniche di quantizzazione dei pesi più popolari. Abbiamo iniziato acquisendo una comprensione della rappresentazione in virgola mobile, per poi introdurre due tecniche per la quantizzazione a 8 bit: absmax e quantizzazione del punto zero. Tuttavia, le loro limitazioni, in particolare per quanto riguarda la gestione degli outlier, hanno portato a LLM.int8(), una tecnica che preserva anche le prestazioni del modello. Questo approccio sottolinea i progressi compiuti nel campo della quantizzazione dei pesi, evidenziando l’importanza di affrontare correttamente gli outlier.

Nel prossimo articolo, esploreremo in dettaglio la tecnica di quantizzazione dei pesi GPTQ. Questa tecnica, introdotta da Frantar et al., utilizza solo 4 bit e rappresenta un notevole avanzamento nel campo della quantizzazione dei pesi. Forniremo una guida completa su come implementare GPTQ utilizzando la libreria AutoGPTQ.

Se sei interessato a contenuti tecnici su LLM, seguimi su Twitter @maximelabonne.

Riferimenti

  • T. Dettmers, M. Lewis, Y. Belkada e L. Zettlemoyer, LLM.int8(): Moltiplicazione matriciale a 8 bit per trasformatori su larga scala. 2022.
  • Y. Beldaka e T. Dettmers, Un’introduzione delicata alla moltiplicazione matriciale a 8 bit, Hugging Face Blog (2022).
  • A. Gholami, S. Kim, Z. Dong, Z. Yao, M. W. Mahoney e K. Keutzer, Un’indagine sui metodi di quantizzazione per l’efficienza dell’inferenza delle reti neurali. 2021.
  • H. Wu, P. Judd, X. Zhang, M. Isaev e P. Micikevicius, Quantizzazione intera per l’inferenza del deep learning: principi e valutazione empirica. 2020.
  • Lilian Weng, Ottimizzazione dell’inferenza di modelli di trasformatori di grandi dimensioni, Lil’Log (2023).
  • Kamil Czarnogorski, Modelli linguistici locali di grandi dimensioni, Int8 (2023).