Inferenza accelerata con Optimum e Transformers Pipelines

'Accelerated inference with Optimum and Transformers Pipelines'

L’inferenza è arrivata in Optimum con il supporto per le pipeline di Hugging Face Transformers, inclusa la generazione di testo utilizzando ONNX Runtime.

L’adozione di BERT e Transformers continua a crescere. I modelli basati su Transformer non solo stanno raggiungendo prestazioni all’avanguardia nell’elaborazione del linguaggio naturale, ma anche nell’elaborazione delle immagini, nel riconoscimento vocale e nelle serie temporali. 💬 🖼 🎤 ⏳

Le aziende stanno passando dalla fase di sperimentazione e ricerca alla fase di produzione per utilizzare i modelli Transformer per carichi di lavoro su larga scala. Ma di default, BERT e i suoi amici sono modelli relativamente lenti, grandi e complessi rispetto agli algoritmi tradizionali di machine learning.

Per risolvere questa sfida, abbiamo creato Optimum: un’estensione di Hugging Face Transformers per accelerare l’addestramento e l’inferenza di modelli Transformer come BERT.

In questo post del blog, imparerai:

  • 1. Cos’è Optimum? Una spiegazione semplice
  • 2. Nuove funzionalità di inferenza e pipeline di Optimum
  • 3. Tutorial completo sull’accelerazione di RoBERTa per il question-answering, inclusa quantizzazione e ottimizzazione
  • 4. Limitazioni attuali
  • 5. Domande frequenti su Optimum Inference
  • 6. Qual è il prossimo passo?

Iniziamo! 🚀

1. Cos’è Optimum? Una spiegazione semplice

Hugging Face Optimum è una libreria open-source e un’estensione di Hugging Face Transformers che fornisce un’API unificata di strumenti di ottimizzazione delle prestazioni per ottenere la massima efficienza nell’addestramento e nell’esecuzione dei modelli su hardware accelerato, inclusi i toolkit per prestazioni ottimizzate su Graphcore IPU e Habana Gaudi. Optimum può essere utilizzato per addestramento accelerato, quantizzazione, ottimizzazione del grafo e ora anche per inferenza con il supporto per le pipeline dei transformers.

2. Nuove funzionalità di inferenza e pipeline di Optimum

Con il rilascio di Optimum 1.2, stiamo aggiungendo il supporto per l’inferenza e le pipeline dei transformers. Ciò consente agli utenti di Optimum di sfruttare la stessa API che utilizzano per i transformers con la potenza dei tempi di esecuzione accelerati, come ONNX Runtime.

Passaggio dai Transformers a Optimum Inference I modelli Optimum Inference sono compatibili con l’API dei modelli Hugging Face Transformers. Ciò significa che è sufficiente sostituire la classe AutoModelForXxx con la corrispondente classe ORTModelForXxx in Optimum. Ad esempio, ecco come puoi utilizzare un modello di question answering in Optimum:

from transformers import AutoTokenizer, pipeline
-from transformers import AutoModelForQuestionAnswering
+from optimum.onnxruntime import ORTModelForQuestionAnswering

-model = AutoModelForQuestionAnswering.from_pretrained("deepset/roberta-base-squad2") # pytorch checkpoint
+model = ORTModelForQuestionAnswering.from_pretrained("optimum/roberta-base-squad2") # onnx checkpoint
tokenizer = AutoTokenizer.from_pretrained("deepset/roberta-base-squad2")

optimum_qa = pipeline("question-answering", model=model, tokenizer=tokenizer)

question = "Qual è il mio nome?"
context = "Il mio nome è Philipp e vivo a Norimberga."
pred = optimum_qa(question, context)

Nella prima versione, abbiamo aggiunto il supporto per ONNX Runtime, ma ci saranno altre novità! Questi nuovi modelli ORTModelForXX possono essere utilizzati anche con le pipeline dei transformers. Sono completamente integrati nell’Hugging Face Hub per caricare e scaricare checkpoint ottimizzati dalla comunità. Inoltre, è possibile utilizzare ORTQuantizer e ORTOptimizer per quantizzare e ottimizzare il modello prima di eseguire l’inferenza su di esso. Dai un’occhiata al tutorial completo sull’accelerazione di RoBERTa per il question-answering, inclusa quantizzazione e ottimizzazione, per ulteriori dettagli.

3. Tutorial completo sull’accelerazione di RoBERTa per il question-answering, inclusa quantizzazione e ottimizzazione

In questo tutorial completo sull’accelerazione di RoBERTa per il question-answering, imparerai:

  1. Installare Optimum per ONNX Runtime
  2. Convertire un modello Hugging Face Transformers in ONNX per l’inferenza
  3. Utilizzare ORTOptimizer per ottimizzare il modello
  4. Utilizzare ORTQuantizer per applicare la quantizzazione dinamica
  5. Eseguire l’inferenza accelerata utilizzando le pipeline dei Transformers
  6. Valutare le prestazioni e la velocità

Cominciamo 🚀

Questo tutorial è stato creato ed eseguito su un’istanza AWS EC2 m5.xlarge.

3.1 Installa Optimum per Onnxruntime

Il nostro primo passo è installare Optimum con le utility onnxruntime.

pip install "optimum[onnxruntime]==1.2.0"

Questo installerà tutti i pacchetti necessari, inclusi transformers, torch e onnxruntime. Se intendi utilizzare una GPU, puoi installare optimum con pip install optimum[onnxruntime-gpu].

3.2 Converti un modello Hugging Face Transformers in ONNX per l’elaborazione

Prima di poter iniziare l’ottimizzazione, dobbiamo convertire il nostro modello transformers di base nel formato onnx. Per fare ciò, utilizzeremo la nuova classe ORTModelForQuestionAnswering chiamando il metodo from_pretrained() con l’attributo from_transformers. Il modello che stiamo utilizzando è il deepset/roberta-base-squad2, un modello RoBERTa preaddestrato sul dataset SQUAD2 che ha ottenuto un punteggio F1 di 82.91 e come feature (task) question-answering.

from pathlib import Path
from transformers import AutoTokenizer, pipeline
from optimum.onnxruntime import ORTModelForQuestionAnswering

model_id = "deepset/roberta-base-squad2"
onnx_path = Path("onnx")
task = "question-answering"

# carica il modello vanilla transformers e convertilo in onnx
model = ORTModelForQuestionAnswering.from_pretrained(model_id, from_transformers=True)
tokenizer = AutoTokenizer.from_pretrained(model_id)

# salva il checkpoint onnx e il tokenizer
model.save_pretrained(onnx_path)
tokenizer.save_pretrained(onnx_path)

# testa il modello usando il pipeline transformers, con handle_impossible_answer per squad_v2
optimum_qa = pipeline(task, model=model, tokenizer=tokenizer, handle_impossible_answer=True)
prediction = optimum_qa(question="Qual è il mio nome?", context="Il mio nome è Philipp e vivo a Norimberga.")

print(prediction)
# {'score': 0.9041663408279419, 'start': 11, 'end': 18, 'answer': 'Philipp'}

Abbiamo convertito con successo i nostri transformers vanilla in onnx e abbiamo utilizzato il modello con il pipeline transformers.pipelines per eseguire la prima previsione. Ora ottimizziamolo. 🏎

Se desideri saperne di più sull’esportazione dei modelli transformers, consulta la documentazione: Esporta i modelli 🤗 Transformers

3.3 Utilizza ORTOptimizer per ottimizzare il modello

Dopo aver salvato il nostro checkpoint onnx in onnx/, possiamo ora utilizzare ORTOptimizer per applicare ottimizzazioni al grafo, come la fusione degli operatori e il folding delle costanti, per accelerare la latenza e l’elaborazione.

from optimum.onnxruntime import ORTOptimizer
from optimum.onnxruntime.configuration import OptimizationConfig

# crea ORTOptimizer e definisci la configurazione di ottimizzazione
optimizer = ORTOptimizer.from_pretrained(model_id, feature=task)
optimization_config = OptimizationConfig(optimization_level=99) # abilita tutte le ottimizzazioni

# applica la configurazione di ottimizzazione al modello
optimizer.export(
    onnx_model_path=onnx_path / "model.onnx",
    onnx_optimized_model_output_path=onnx_path / "model-optimized.onnx",
    optimization_config=optimization_config,
)

Per testare le prestazioni, possiamo utilizzare nuovamente la classe ORTModelForQuestionAnswering e fornire un parametro file_name aggiuntivo per caricare il nostro modello ottimizzato. (Questo funziona anche per i modelli disponibili nell’hub).

from optimum.onnxruntime import ORTModelForQuestionAnswering

# carica il modello quantizzato
opt_model = ORTModelForQuestionAnswering.from_pretrained(onnx_path, file_name="model-optimized.onnx")

# testa il modello quantizzato usando il pipeline transformers
opt_optimum_qa = pipeline(task, model=opt_model, tokenizer=tokenizer, handle_impossible_answer=True)
prediction = opt_optimum_qa(question="Qual è il mio nome?", context="Il mio nome è Philipp e vivo a Norimberga.")
print(prediction)
# {'score': 0.9041663408279419, 'start': 11, 'end': 18, 'answer': 'Philipp'}

Valuteremo i cambiamenti delle prestazioni nel punto 3.6 Valutare le prestazioni e la velocità in dettaglio.

3.4 Utilizzare l’ORTQuantizer per applicare la quantizzazione dinamica

Dopo aver ottimizzato il nostro modello, possiamo accelerarlo ulteriormente quantizzandolo utilizzando l’ORTQuantizer. L’ORTOptimizer può essere utilizzato per applicare la quantizzazione dinamica per ridurre la dimensione del modello e accelerare la latenza e l’inferenza.

Utilizziamo l’avx512_vnni poiché l’istanza è alimentata da una CPU Intel Cascade-Lake che supporta avx512.

from optimum.onnxruntime import ORTQuantizer
from optimum.onnxruntime.configuration import AutoQuantizationConfig

# creare l'ORTQuantizer e definire la configurazione di quantizzazione
quantizer = ORTQuantizer.from_pretrained(model_id, feature=task)
qconfig = AutoQuantizationConfig.avx512_vnni(is_static=False, per_channel=True)

# applicare la configurazione di quantizzazione al modello
quantizer.export(
    onnx_model_path=onnx_path / "model-optimized.onnx",
    onnx_quantized_model_output_path=onnx_path / "model-quantized.onnx",
    quantization_config=qconfig,
)

Ora possiamo confrontare la dimensione di questo modello così come alcune prestazioni di latenza

import os
# ottenere la dimensione del file del modello
size = os.path.getsize(onnx_path / "model.onnx")/(1024*1024)
print(f"Dimensione del file del modello Vanilla Onnx: {size:.2f} MB")
size = os.path.getsize(onnx_path / "model-quantized.onnx")/(1024*1024)
print(f"Dimensione del file del modello Quantizzato Onnx: {size:.2f} MB")

# Dimensione del file del modello Vanilla Onnx: 473.31 MB
# Dimensione del file del modello Quantizzato Onnx: 291.77 MB

Abbiamo ridotto la dimensione del nostro modello di quasi il 50%, da 473 MB a 291 MB. Per eseguire l’inferenza possiamo utilizzare nuovamente la classe ORTModelForQuestionAnswering e fornire un parametro aggiuntivo file_name per caricare il nostro modello quantizzato. (Questo funziona anche per i modelli disponibili nell’hub).

# caricare il modello quantizzato
quantized_model = ORTModelForQuestionAnswering.from_pretrained(onnx_path, file_name="model-quantized.onnx")

# testare il modello quantizzato utilizzando il pipeline di transformers
quantized_optimum_qa = pipeline(task, model=quantized_model, tokenizer=tokenizer, handle_impossible_answer=True)
prediction = quantized_optimum_qa(question="Qual è il mio nome?", context="Il mio nome è Philipp e vivo a Norimberga.")
print(prediction)
# {'score': 0.9246969819068909, 'start': 11, 'end': 18, 'answer': 'Philipp'}

Bravo! Il modello ha predetto la stessa risposta.

3.5 Esegui l’inferenza accelerata utilizzando i pipeline di Transformers

Optimum ha un supporto integrato per i pipeline di transformers. Ciò ci consente di sfruttare la stessa API che conosciamo dall’utilizzo di modelli PyTorch e TensorFlow. Abbiamo già utilizzato questa funzionalità nei passaggi 3.2, 3.3 e 3.4 per testare i nostri modelli convertiti e ottimizzati. Al momento della stesura di questo testo, supportiamo ONNX Runtime con altri aggiornamenti in arrivo in futuro. Di seguito puoi trovare un esempio di come utilizzare i pipeline di transformers.

from transformers import AutoTokenizer, pipeline
from optimum.onnxruntime import ORTModelForQuestionAnswering

tokenizer = AutoTokenizer.from_pretrained(onnx_path)
model = ORTModelForQuestionAnswering.from_pretrained(onnx_path)

optimum_qa = pipeline("question-answering", model=model, tokenizer=tokenizer)
prediction = optimum_qa(question="Qual è il mio nome?", context="Il mio nome è Philipp e vivo a Norimberga.")

print(prediction)
# {'score': 0.9041663408279419, 'start': 11, 'end': 18, 'answer': 'Philipp'}

In aggiunta a ciò, abbiamo aggiunto un’API pipelines ad Optimum per garantire una maggiore sicurezza per i tuoi modelli accelerati. Ciò significa che se stai cercando di utilizzare optimum.pipelines con un modello o un task non supportato, vedrai un errore. Puoi utilizzare optimum.pipelines come sostituto di transformers.pipelines.

from transformers import AutoTokenizer
from optimum.onnxruntime import ORTModelForQuestionAnswering
from optimum.pipelines import pipeline

tokenizer = AutoTokenizer.from_pretrained(onnx_path)
model = ORTModelForQuestionAnswering.from_pretrained(onnx_path)

optimum_qa = pipeline("question-answering", model=model, tokenizer=tokenizer, handle_impossible_answer=True)
prediction = optimum_qa(question="Qual è il mio nome?", context="Il mio nome è Philipp e vivo a Norimberga.")

print(prediction)
# {'score': 0.9041663408279419, 'start': 11, 'end': 18, 'answer': 'Philipp'}

3.6 Valutare le prestazioni e la velocità

Durante questo tutorial end-to-end sull’accelerazione di RoBERTa per la domanda-risposta, inclusa la quantizzazione e l’ottimizzazione, abbiamo creato 3 modelli diversi. Un modello convertito di base, un modello ottimizzato e un modello quantizzato.

Come ultimo passo del tutorial, vogliamo dare un’occhiata dettagliata alle prestazioni e all’accuratezza del nostro modello. L’applicazione di tecniche di ottimizzazione, come l’ottimizzazione del grafico o la quantizzazione, non influisce solo sulle prestazioni (latenza), ma può anche influire sull’accuratezza del modello. Quindi accelerare il modello comporta un compromesso.

Valutiamo i nostri modelli. Il nostro modello transformers deepset/roberta-base-squad2 è stato addestrato sul dataset SQUAD2. Questo sarà il dataset che utilizzeremo per valutare i nostri modelli.

from datasets import load_metric,load_dataset

metric = load_metric("squad_v2")
dataset = load_dataset("squad_v2")["validation"]

print(f"lunghezza del dataset {len(dataset)}")
#lunghezza del dataset 11873

Ora possiamo sfruttare la funzione map dei dataset per iterare sul set di validazione di squad 2 e eseguire una previsione per ogni punto dati. Pertanto scriviamo un metodo di aiuto evaluate che utilizza le nostre pipeline e applica alcune trasformazioni per lavorare con la metrica squad v2.

Questo potrebbe richiedere del tempo (1,5 ore)

def evaluate(example):
  default = optimum_qa(question=example["question"], context=example["context"])
  optimized = opt_optimum_qa(question=example["question"], context=example["context"])
  quantized = quantized_optimum_qa(question=example["question"], context=example["context"])
  return {
      'reference': {'id': example['id'], 'answers': example['answers']},
      'default': {'id': example['id'],'prediction_text': default['answer'], 'no_answer_probability': 0.},
      'optimized': {'id': example['id'],'prediction_text': optimized['answer'], 'no_answer_probability': 0.},
      'quantized': {'id': example['id'],'prediction_text': quantized['answer'], 'no_answer_probability': 0.},
      }

result = dataset.map(evaluate)
# COMMENT IN per eseguire la valutazione su un sottoinsieme di 2000 del dataset
# result = dataset.shuffle().select(range(2000)).map(evaluate)

Ora confrontiamo i risultati

default_acc = metric.compute(predictions=result["default"], references=result["reference"])
optimized = metric.compute(predictions=result["optimized"], references=result["reference"])
quantized = metric.compute(predictions=result["quantized"], references=result["reference"])

print(f"modello di base: esatto={default_acc['exact']}% f1={default_acc['f1']}%")
print(f"modello ottimizzato: esatto={optimized['exact']}% f1={optimized['f1']}%")
print(f"modello quantizzato: esatto={quantized['exact']}% f1={quantized['f1']}%")

# modello di base: esatto=79.07858165585783% f1=82.14970024570314%
# modello ottimizzato: esatto=79.07858165585783% f1=82.14970024570314%
# modello quantizzato: esatto=78.75010528088941% f1=81.82526107204629%

Il nostro modello ottimizzato e quantizzato ha ottenuto una corrispondenza esatta del 78,75% e un punteggio f1 del 81,83%, che rappresenta il 99,61% della precisione originale. Raggiungere il 99% del modello originale è molto buono, soprattutto considerando che abbiamo usato la quantizzazione dinamica.

Ora, testiamo le prestazioni (latenza) del nostro modello ottimizzato e quantizzato.

Ma prima, estendiamo il nostro contesto e la nostra domanda ad una lunghezza di sequenza più significativa di 128.

context="Ciao, mi chiamo Philipp e vivo a Norimberga, in Germania. Attualmente lavoro come Technical Lead presso Hugging Face per democratizzare l'intelligenza artificiale tramite open source e open science. In passato ho progettato e implementato architetture di machine learning nativo per il cloud per aziende del settore finanziario e assicurativo. Ho scoperto la mia passione per i concetti cloud e il machine learning 5 anni fa. Da allora non ho mai smesso di imparare. Attualmente mi sto concentrando nell'area NLP e su come sfruttare modelli come BERT, Roberta, T5, ViT e GPT2 per generare valore aziendale."
question="In che ruolo lavora Philipp?"

Per semplificarlo, useremo un ciclo in Python e calcoleremo la latenza media per il nostro modello standard e per il modello ottimizzato e quantizzato.

from time import perf_counter
import numpy as np

def misura_latenza(pipe):
    latenze = []
    # riscaldamento
    for _ in range(10):
        _ = pipe(question=question, context=context)
    # Esecuzione temporizzata
    for _ in range(100):
        start_time = perf_counter()
        _ =  pipe(question=question, context=context)
        latenza = perf_counter() - start_time
        latenze.append(latenza)
    # Calcola statistiche di esecuzione
    tempo_medio_ms = 1000 * np.mean(latenze)
    tempo_std_ms = 1000 * np.std(latenze)
    return f"Latenza media (ms) - {tempo_medio_ms:.2f} +\- {tempo_std_ms:.2f}"

print(f"Modello standard {misura_latenza(optimum_qa)}")
print(f"Modello ottimizzato e quantizzato {misura_latenza(quantized_optimum_qa)}")

# Modello standard Latenza media (ms) - 117.61 +\- 8.48
# Modello ottimizzato e quantizzato Latenza media (ms) - 64.94 +\- 3.65

Siamo riusciti ad accelerare la latenza del nostro modello da 117.61ms a 64.94ms, ovvero circa il doppio, mantenendo 99.61% di accuratezza. Qualcosa da tenere presente è che abbiamo utilizzato un’istanza CPU di media performance con 2 core fisici. Passando a una GPU o a un’istanza CPU più performante, ad esempio basata su Ice Lake, è possibile ridurre il numero di latenza a pochi millisecondi.

4. Limitazioni attuali

Abbiamo appena iniziato a supportare l’inferenza in https://github.com/huggingface/optimum, quindi vorremmo condividere anche le limitazioni attuali. Tutte queste limitazioni sono sulla roadmap e saranno risolte nel prossimo futuro.

  • Modelli remoti > 2GB: Attualmente, è possibile caricare solo modelli più piccoli di 2GB dal Hugging Face Hub. Stiamo lavorando per aggiungere il supporto per modelli > 2GB / modelli multi-file.
  • Compiti/modello Seq2Seq: Non supportiamo compiti Seq2Seq, come la sommarizzazione e modelli come T5, principalmente a causa della limitazione del supporto di un singolo modello. Ma stiamo lavorando attivamente per risolvere questo problema, per fornirti la stessa esperienza che conosci in transformers.
  • Valori chiave passati: I modelli di generazione come GPT-2 utilizzano qualcosa chiamato valori chiave passati, che sono coppie chiave-valore precalcolate dei blocchi di attenzione e possono essere utilizzate per velocizzare la decodifica. Attualmente ORTModelForCausalLM non sta utilizzando i valori chiave passati.
  • Nessuna cache: Attualmente, quando si carica un modello ottimizzato (*.onnx), non verrà memorizzato nella cache locale.

5. Domande frequenti su Optimum Inference

Quali compiti sono supportati?

Puoi trovare un elenco di tutti i compiti supportati nella documentazione. Attualmente i compiti supportati dalle pipeline sono feature-extraction, text-classification, token-classification, question-answering, zero-shot-classification, text-generation

Quali modelli sono supportati?

Tutti i modelli che possono essere esportati con transformers.onnx e hanno un compito supportato possono essere utilizzati, incluso tra gli altri BERT, ALBERT, GPT2, RoBERTa, XLM-RoBERTa, DistilBERT…

Quali runtime sono supportati?

Attualmente, ONNX Runtime è supportato. Stiamo lavorando per aggiungerne altri in futuro. Fateci sapere se siete interessati a un runtime specifico.

Come posso usare Optimum con Transformers?

Puoi trovare un esempio ed istruzioni nella nostra documentazione .

Come posso utilizzare le GPU?

Per poter utilizzare le GPU è sufficiente installare optimum[onnxruntine-gpu] che installerà i fornitori GPU richiesti e li utilizzerà di default.

Come posso utilizzare un modello quantizzato e ottimizzato con le pipeline?

Puoi caricare il modello ottimizzato o quantizzato utilizzando le nuove classi ORTModelForXXX utilizzando il metodo from_pretrained. Puoi saperne di più nella nostra documentazione .

6. Cosa c’è di nuovo?

Cosa c’è di nuovo per Optimum ti chiedi? Molte cose. Siamo concentrati nel fare di Optimum il toolkit open-source di riferimento per lavorare con transformers per l’accelerazione e l’ottimizzazione. Per poter raggiungere questo obiettivo risolveremo le attuali limitazioni, miglioreremo la documentazione, creeremo più contenuti ed esempi e spingeremo i limiti per l’accelerazione e l’ottimizzazione dei transformers.

Alcune importanti funzionalità sulla roadmap per Optimum tra le attuali limitazioni sono:

  • Supporto per modelli di linguaggio (Wav2vec2) e compiti di linguaggio (riconoscimento automatico del linguaggio)
  • Supporto per modelli di visione (ViT) e compiti di visione (classificazione delle immagini)
  • Migliorare le prestazioni aggiungendo il supporto per OrtValue e IOBinding
  • Modi più semplici per valutare modelli accelerati
  • Aggiungere il supporto per altri runtime e fornitori come TensorRT e AWS-Neuron

Grazie per la lettura! Se sei entusiasta come me di accelerare i Transformers, renderli efficienti e scalare fino a miliardi di richieste. Dovresti candidarti, stiamo assumendo .🚀

Se hai domande, non esitare a contattarmi tramite Github , o sul forum . Puoi anche collegarti con me su Twitter o LinkedIn .