Miglioramento delle velocità di inferenza LLM sulle CPU con la quantizzazione del modello |  di Eduardo Alvarez |  Febbraio 2024

 | Intelligenza-Artificiale

Proprietà immagine dell'autore: crea con Nightcafe

Scopri come migliorare in modo significativo la latenza di inferenza sulle CPU utilizzando tecniche di quantizzazione per precisione mista, int8 e int4

Una delle sfide più significative che lo spazio dell’intelligenza artificiale deve affrontare è la necessità di risorse di elaborazione per ospitare applicazioni basate su LLM di livello produttivo su larga scala. Su larga scala, le applicazioni LLM richiedono ridondanza, scalabilità e affidabilità, che storicamente sono state possibili solo su piattaforme informatiche generiche come le CPU. Tuttavia, la narrativa prevalente oggi è che le CPU non sono in grado di gestire l’inferenza LLM a latenze paragonabili a quelle delle GPU di fascia alta.

Uno strumento open source nell'ecosistema che può aiutare ad affrontare le sfide della latenza di inferenza sulle CPU è Estensione Intel per PyTorch (IPEX), che fornisce ottimizzazioni delle funzionalità aggiornate per un ulteriore incremento delle prestazioni sull'hardware Intel. IPEX offre una varietà di ottimizzazioni facili da implementare che utilizzano istruzioni a livello hardware. Questo tutorial approfondirà la teoria della compressione dei modelli e le tecniche di compressione dei modelli pronte all'uso fornite da IPEX. Queste tecniche di compressione influiscono direttamente sulle prestazioni di inferenza LLM su piattaforme informatiche generali, come le CPU Intel di quarta e quinta generazione.

Seconda solo alla sicurezza e alla protezione delle applicazioni, la latenza di inferenza è uno dei parametri più critici di un'applicazione IA in produzione. Per quanto riguarda le applicazioni basate su LLM, la latenza o il throughput vengono spesso misurati in token/secondo. Come illustrato nella sequenza di elaborazione dell'inferenza semplificata di seguito, i token vengono elaborati dal modello linguistico e quindi detokenizzati nel linguaggio naturale.

GIF 1. della sequenza di elaborazione dell'inferenza — Immagine dell'autore

Interpretare l'inferenza in questo modo a volte può portarci fuori strada perché analizziamo questa componente delle applicazioni AI in astrazione dal tradizionale paradigma del software di produzione. Sì, le app di intelligenza artificiale hanno le loro sfumature, ma alla fine parliamo ancora di transazioni per unità di tempo. Se cominciamo a pensare all’inferenza come a una transazione, come qualsiasi altra, dal punto di vista della progettazione dell’applicazione, il problema diventa meno complesso. Ad esempio, supponiamo di avere un'applicazione di chat che presenta i seguenti requisiti:

  • Media di 300 sessioni utente all'ora
  • Media di 5 transazioni (richieste di inferenza LLM) per utente per sessione
  • Media 100 gettoni generato per transazione
  • Ogni sessione ha una media di 10.000 ms (10 s) di sovraccarico per l'autenticazione dell'utente, il guardrail, la latenza di rete e la pre/post-elaborazione.
  • Gli utenti prendono una media di 30.000 ms (30 secondi) per rispondere quando sei attivamente impegnato con il chatbot.
  • Il totale medio degli attivi L'obiettivo temporale della sessione è 3 minuti o meno.

Di seguito, puoi vedere che con qualche semplice calcolo matematico possiamo ottenere alcuni calcoli approssimativi per la latenza richiesta del nostro motore di inferenza LLM.

Figura 1. Una semplice equazione per calcolare la transazione richiesta e la latenza del token in base ai vari requisiti dell'applicazione. — Immagine dell'autore

Raggiungere le soglie di latenza richieste in produzione è una sfida, soprattutto se è necessario farlo senza incorrere in costi aggiuntivi per l'infrastruttura di elaborazione. Nella parte restante di questo articolo esploreremo un modo per migliorare in modo significativo la latenza di inferenza attraverso la compressione del modello.

La compressione del modello è un termine generico perché si riferisce a una varietà di tecniche, come la quantizzazione del modello, la distillazione, l'eliminazione e altro ancora. Fondamentalmente, lo scopo principale di queste tecniche è ridurre la complessità computazionale delle reti neurali.

GIF 2. Illustrazione della sequenza di elaborazione dell'inferenza — Immagine dell'autore

Il metodo su cui ci concentreremo oggi è la quantizzazione del modello, che comporta la riduzione della precisione in byte dei pesi e, a volte, delle attivazioni, riducendo il carico computazionale delle operazioni sulle matrici e il carico di memoria dovuto allo spostamento attorno a valori di precisione più grandi e più elevati. La figura seguente illustra il processo di quantificazione dei pesi fp32 su int8.

Fig 2. Rappresentazione visiva della quantizzazione del modello che passa dalla precisione completa con FP32 fino alla precisione di un quarto con INT8, riducendo teoricamente la complessità del modello di un fattore 4. — Immagine dell'autore

Vale la pena ricordare che la riduzione della complessità di un fattore 4 che risulta dalla quantizzazione da fp32 (precisione completa) a int8 (precisione di un quarto) non si traduce in una riduzione della latenza di 4 volte durante l'inferenza perché la latenza dell'inferenza coinvolge più fattori oltre al semplice modello- proprietà centriche.

Come per molte altre cose, non esiste un approccio valido per tutti e in questo articolo esploreremo tre delle mie tecniche preferite per quantizzare i modelli utilizzando IPEX:

Precisione mista (bf16/fp32)

Questa tecnica quantizza alcuni ma non tutti i pesi nella rete neurale, determinando una compressione parziale del modello. Questa tecnica è ideale per i modelli più piccoli, come gli LLM <1 miliardo del mondo.

Fig 3. Semplice illustrazione di previsioni miste, che mostra i pesi FP32 in arancione e i pesi bf16 quantizzati a mezza precisione in verde. — Immagine dell'autore

L'implementazione è piuttosto semplice: utilizzando i trasformatori Hugging Face, un modello può essere caricato in memoria e ottimizzato utilizzando la funzione di ottimizzazione specifica di IPEX llm ipex.llm.optimize(model, dtype=dtype) IMPOSTANDO dtype = torch.bfloat16, possiamo attivare la funzionalità di inferenza a precisione mista, che migliora la latenza di inferenza rispetto alla precisione completa (fp32) e allo stock.

import sys
import os
import torch
import intel_extension_for_pytorch as ipex
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline

# PART 1: Model and tokenizer loading using transformers
tokenizer = AutoTokenizer.from_pretrained("Intel/neural-chat-7b-v3-3")
model = AutoModelForCausalLM.from_pretrained("Intel/neural-chat-7b-v3-3")

# PART 2: Use IPEX to optimize the model
#dtype = torch.float # use for full precision FP32
dtype = torch.bfloat16 # use for mixed precision inference
model = ipex.llm.optimize(model, dtype=dtype)

# PART 3: Create a hugging face inference pipeline and generate results
pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)
st = time.time()
results = pipe("A fisherman at sea...", max_length=250)
end = time.time()
generation_latency = end-st

print('generation latency: ', generation_latency)
print(results(0)('generated_text'))

Delle tre tecniche di compressione che esploreremo, questa è la più semplice da implementare (misurata da righe di codice univoche) e offre il miglioramento netto più piccolo rispetto a una linea di base non quantizzata.

SmoothQuant (int8)

Questa tecnica affronta le sfide principali della quantizzazione degli LLM, che includono la gestione di valori anomali di grande entità nei canali di attivazione su tutti i livelli e token, un problema comune che le tecniche di quantizzazione tradizionali faticano a gestire in modo efficace. Questa tecnica impiega una trasformazione matematica congiunta sia sui pesi che sulle attivazioni all'interno del modello. La trasformazione riduce strategicamente la disparità tra valori anomali e non anomali per le attivazioni, anche se a costo di aumentare questo rapporto per i pesi. Questa regolazione rende i livelli Transformer “facili da quantizzare”, consentendo l'applicazione corretta della quantizzazione int8 senza degradare la qualità del modello.

Fig 4. Semplice illustrazione di SmoothQuant che mostra i pesi come cerchi e le attivazioni come triangoli. Il diagramma illustra i due passaggi principali: (1) l'applicazione dello scaler per lo smoothing e (2) la quantizzazione su int8 — Immagine dell'autore

Di seguito troverai una semplice implementazione di SmoothQuant, che omette il codice per la creazione di DataLoader, che è un principio PyTorch comune e ben documentato. SmoothQuant è una ricetta di quantizzazione post-addestramento che tiene conto dell'accuratezza, il che significa che fornendo un set di dati e un modello di calibrazione sarai in grado di fornire una linea di base e limitare il degrado della modellazione del linguaggio. Il modello di calibrazione genera una configurazione di quantizzazione, alla quale viene poi passata ipex.llm.optimize() insieme alla mappatura SmoothQuant. Al momento dell'esecuzione, viene applicato SmoothQuant e il modello può essere testato utilizzando .generate() metodo.

import torch
import intel_extension_for_pytorch as ipex
from intel_extension_for_pytorch.quantization import prepare
import transformers

# PART 1: Load model and tokenizer from Hugging Face + Load SmoothQuant config mapping
tokenizer = AutoTokenizer.from_pretrained("Intel/neural-chat-7b-v3-3")
model = AutoModelForCausalLM.from_pretrained("Intel/neural-chat-7b-v3-3")
qconfig = ipex.quantization.get_smooth_quant_qconfig_mapping()

# PART 2: Configure calibration
# prepare your calibration dataset samples
calib_dataset = DataLoader({Your dataloader parameters})
example_inputs = # provide a sample input from your calib_dataset
calibration_model = ipex.llm.optimize(
model.eval(),
quantization_config=qconfig,
)
prepared_model = prepare(
calibration_model.eval(), qconfig, example_inputs=example_inputs
)
with torch.no_grad():
for calib_samples in enumerate(calib_dataset):
prepared_model(calib_samples)
prepared_model.save_qconf_summary(qconf_summary=qconfig_summary_file_path)

# PART 3: Model Quantization using SmoothQuant
model = ipex.llm.optimize(
model.eval(),
quantization_config=qconfig,
qconfig_summary_file=qconfig_summary_file_path,
)

# generation inference loop
with torch.inference_mode():
model.generate({your generate parameters})

SmoothQuant è una potente tecnica di compressione dei modelli e aiuta a migliorare significativamente la latenza di inferenza rispetto ai modelli a precisione completa. Tuttavia, è necessario un po’ di lavoro iniziale per preparare un set di dati e un modello di calibrazione.

Quantizzazione solo peso (int8 e int4)

Rispetto alla tradizionale quantizzazione int8 applicata sia all'attivazione che al peso, la quantizzazione solo peso (WOQ) offre un migliore equilibrio tra prestazioni e accuratezza. Vale la pena notare che int4 WOQ richiede la dequantizzazione a bf16/fp16 prima del calcolo (Figura 4), il che introduce un sovraccarico nel calcolo. Una tecnica WOQ di base, la quantizzazione asimmetrica tensore Round To Nearest (RTN), presenta sfide e spesso porta a una precisione ridotta (fonte). Tuttavia, la letteratura (Zhewei Yao, 2022) suggerisce che la quantizzazione a gruppi dei pesi del modello aiuta a mantenere l'accuratezza. Poiché i pesi vengono solo dequantizzati per il calcolo, nonostante questo passaggio aggiuntivo rimane un significativo vantaggio in termini di memoria.

Fig 5. Semplice illustrazione della quantizzazione del solo peso, con i pesi prequantizzati in arancione e i pesi quantizzati in verde. Si noti che questo descrive la quantizzazione iniziale in int4/int8 e la dequantizzazione in fp16/bf16 per la fase di calcolo. — Immagine dell'autore

L'implementazione WOQ di seguito mostra le poche righe di codice necessarie per quantizzare un modello da Hugging Face con questa tecnica. Come per le implementazioni precedenti, iniziamo caricando un modello e un tokenizzatore da Hugging Face. Possiamo usare il get_weight_only_quant_qconfig_mapping() metodo per configurare la ricetta WOQ. La ricetta viene poi passata al ipex.llm.optimize() funzione insieme al modello per l'ottimizzazione e la quantizzazione. Il modello quantizzato può quindi essere utilizzato per l'inferenza con il .generate() metodo.

import torch
import intel_extension_for_pytorch as ipex
import transformers

# PART 1: Model and tokenizer loading
tokenizer = AutoTokenizer.from_pretrained("Intel/neural-chat-7b-v3-3")
model = AutoModelForCausalLM.from_pretrained("Intel/neural-chat-7b-v3-3")

# PART 2: Preparation of quantization config
qconfig = ipex.quantization.get_weight_only_quant_qconfig_mapping(
weight_dtype=torch.qint8, # or torch.quint4x2
lowp_mode=ipex.quantization.WoqLowpMode.NONE, # or FP16, BF16, INT8
)
checkpoint = None # optionally load int4 or int8 checkpoint

# PART 3: Model optimization and quantization
model = ipex.llm.optimize(model, quantization_config=qconfig, low_precision_checkpoint=checkpoint)

# PART 4: Generation inference loop
with torch.inference_mode():
model.generate({your generate parameters})

Come puoi vedere, WOQ fornisce un modo potente per comprimere i modelli fino a una frazione della loro dimensione originale con un impatto limitato sulle capacità di modellazione del linguaggio.

In qualità di ingegnere presso Intel, ho lavorato a stretto contatto con il team di ingegneri IPEX presso Intel. Ciò mi ha offerto una visione unica dei suoi vantaggi e della tabella di marcia di sviluppo, rendendo IPEX uno strumento preferito. Tuttavia, per gli sviluppatori che cercano semplicità senza la necessità di gestire una dipendenza aggiuntiva, PyTorch offre tre ricette di quantizzazione: Modalità Eager, Modalità grafico FX (in manutenzione) e Quantizzazione dell'esportazione PyTorch 2, che forniscono alternative forti e meno specializzate.

Indipendentemente dalla tecnica scelta, le tecniche di compressione del modello comporteranno una certa perdita di prestazioni della modellazione del linguaggio, anche se in molti casi <1%. Per questo motivo, è essenziale valutare la tolleranza agli errori dell'applicazione e stabilire una linea di base per le prestazioni del modello a precisione completa (FP32) e/o a metà precisione (BF16/FP16) prima di procedere alla quantizzazione.

Nelle applicazioni che sfruttano un certo grado di apprendimento nel contesto, come Retrieval Augmented Generation (RAG), la compressione del modello potrebbe essere una scelta eccellente. In questi casi, la conoscenza mission-critical viene immessa nel modello al momento dell'inferenza, quindi il rischio è fortemente ridotto anche con applicazioni a bassa tolleranza agli errori.

La quantizzazione è un modo eccellente per risolvere i problemi di latenza dell'inferenza LLM senza aggiornare o espandere l'infrastruttura di elaborazione. Vale la pena esplorarlo indipendentemente dal caso d'uso e IPEX fornisce una buona opzione per iniziare con solo poche righe di codice.

Alcune cose interessanti da provare sarebbero:

  • Testare il codice di esempio in questo tutorial su Intel Developer Cloud Ambiente Jupyter gratuito.
  • Prendi un modello esistente che stai eseguendo su un acceleratore con la massima precisione e testalo su una CPU su int4/int8
  • Esplora tutte e tre le tecniche e determina quale funziona meglio per il tuo caso d'uso. Assicurati di confrontare la perdita di prestazioni della modellazione del linguaggio, non solo la latenza.
  • Carica il tuo modello quantizzato sull'Hugging Face Model Hub! Se lo fai, fammi sapere: mi piacerebbe dare un'occhiata!

Grazie per aver letto! Non dimenticare di seguire il mio profilo per ulteriori articoli come questo!

Fonte: towardsdatascience.com

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *