Ottimizzazione di Bark utilizzando 🤗 Transformers

Ottimizzazione di Bark con 🤗 Transformers

🤗 Transformers fornisce molti dei modelli all’avanguardia (SoTA) più recenti in diversi ambiti e per diverse attività. Per ottenere le migliori prestazioni da questi modelli, è necessario ottimizzarli per la velocità di inferenza e l’uso della memoria.

L’ecosistema 🤗 Hugging Face offre strumenti di ottimizzazione pronti all’uso che possono essere applicati a tutti i modelli nella libreria. Ciò rende facile ridurre l’uso della memoria e migliorare l’inferenza con poche righe di codice aggiuntive.

In questo tutorial pratico, mostrerò come è possibile ottimizzare Bark, un modello di Text-To-Speech (TTS) supportato da 🤗 Transformers, basato su tre semplici ottimizzazioni. Queste ottimizzazioni si basano esclusivamente sulle librerie Transformers, Optimum e Accelerate dell’ecosistema 🤗.

Questo tutorial è anche una dimostrazione di come è possibile valutare le prestazioni di un modello non ottimizzato e delle sue diverse ottimizzazioni.

Per una versione più snella del tutorial con meno spiegazioni ma tutto il codice, consultare il Google Colab allegato.

Questo articolo è organizzato come segue:

Indice

  1. Un promemoria dell’architettura di Bark
  2. Una panoramica delle diverse tecniche di ottimizzazione e dei loro vantaggi
  3. Una presentazione dei risultati del benchmark

Bark è un modello di text-to-speech basato su transformer proposto da Suno AI in suno-ai/bark. È in grado di generare una vasta gamma di output audio, tra cui discorsi, musica, rumore di fondo e semplici effetti sonori. Inoltre, può produrre suoni di comunicazione non verbale come risate, sospiri e singhiozzi.

Bark è disponibile in 🤗 Transformers a partire dalla versione v4.31.0!

Puoi sperimentare con Bark e scoprirne le capacità qui.

Bark è composto da 4 modelli principali:

  • BarkSemanticModel (noto anche come modello ‘text’): un modello di transformer auto-regressivo causale che prende in input testo tokenizzato e predice i token di testo semantico che catturano il significato del testo.
  • BarkCoarseModel (noto anche come modello ‘acustica grossolana’): un transformer auto-regressivo causale, che prende in input i risultati del modello BarkSemanticModel. Il suo obiettivo è predire i primi due codebook audio necessari per EnCodec.
  • BarkFineModel (il modello ‘acustica fine’), questa volta un transformer autoencoder non causale, che predice iterativamente gli ultimi codebook in base alla somma delle incorporazioni dei codebook precedenti.
  • dopo aver predetto tutti i canali dei codebook dal modello EncodecModel, Bark lo utilizza per decodificare l’array audio di output.

Al momento della scrittura, sono disponibili due checkpoint di Bark, una versione più piccola e una più grande.

Carica il modello e il suo Processor

I checkpoint preaddestrati di Bark small e large possono essere caricati dai pesi preaddestrati sul Hugging Face Hub. Puoi modificare l’id del repository con la dimensione del checkpoint che desideri utilizzare.

Noi utilizzeremo il checkpoint small di default, per mantenerlo veloce. Ma puoi provare il checkpoint large utilizzando "suno/bark" invece di "suno/bark-small".

from transformers import BarkModel

model = BarkModel.from_pretrained("suno/bark-small")

Posiziona il modello su un dispositivo acceleratore per ottenere il massimo delle tecniche di ottimizzazione:

import torch

device = "cuda:0" if torch.cuda.is_available() else "cpu"
model = model.to(device)

Carica il processor, che si occupa della tokenizzazione e degli eventuali embedding degli speaker.

from transformers import AutoProcessor

processor = AutoProcessor.from_pretrained("suno/bark-small")

In questa sezione, esploreremo come utilizzare le funzionalità predefinite delle librerie 🤗 Optimum e 🤗 Accelerate per ottimizzare il modello Bark, con modifiche minime al codice.

Alcune impostazioni

Prepariamo gli input e definiamo una funzione per misurare la latenza e l’uso della memoria GPU del metodo di generazione di Bark.

text_prompt = "Proviamo a generare un discorso, con Bark, un modello di text-to-speech"
inputs = processor(text_prompt).to(device)

Per misurare la latenza e l’utilizzo della memoria GPU è necessario utilizzare metodi CUDA specifici. Definiamo una funzione di utilità che misura sia la latenza che l’utilizzo della memoria GPU del modello durante l’inferenza. Per assicurarci di ottenere un’immagine accurata di queste metriche, facciamo una media su un numero specificato di iterazioni nb_loops:

import torch
from transformers import set_seed


def misura_latenza_e_utilizzo_memoria(modello, input, nb_loops = 5):

  # definisci gli eventi che misurano l'inizio e la fine della generazione
  evento_inizio = torch.cuda.Event(enable_timing=True)
  evento_fine = torch.cuda.Event(enable_timing=True)

  # resetta le statistiche della memoria cuda e svuota la cache
  torch.cuda.reset_peak_memory_stats(device)
  torch.cuda.empty_cache()
  torch.cuda.synchronize()

  # ottieni l'istante di inizio
  evento_inizio.record()

  # genera effettivamente
  for _ in range(nb_loops):
        # imposta il seed per la riproducibilità
        set_seed(0)
        output = modello.generate(**input, do_sample = True, fine_temperature = 0.4, coarse_temperature = 0.8)

  # ottieni l'istante di fine
  evento_fine.record()
  torch.cuda.synchronize()

  # misura l'utilizzo della memoria e il tempo trascorso
  memoria_max = torch.cuda.max_memory_allocated(device)
  tempo_trascorso = evento_inizio.elapsed_time(evento_fine) * 1.0e-3

  print('Tempo di esecuzione:', tempo_trascorso/nb_loops, 'secondi')
  print('Utilizzo massimo della memoria', memoria_max*1e-9, ' GB')

  return output

Caso base

Prima di incorporare qualsiasi ottimizzazione, misuriamo le prestazioni del modello di base e ascoltiamo un esempio generato. Misureremo il modello per cinque iterazioni e riporteremo una media delle metriche:

with torch.inference_mode():
  speech_output = misura_latenza_e_utilizzo_memoria(modello, input, nb_loops = 5)

Output:

Tempo di esecuzione: 9.3841625 secondi
Utilizzo massimo della memoria 1.914612224  GB

Ora, ascolta l’output:

from IPython.display import Audio

# ora, ascolta l'output
frequenza_campionamento = modello.generation_config.sample_rate
Audio(speech_output[0].cpu().numpy(), rate=frequenza_campionamento)

L’output suona così (scarica l’audio):

Il tuo browser non supporta l’elemento audio.

Nota importante:

Qui, il numero di iterazioni è effettivamente piuttosto basso. Per misurare accuratamente e confrontare i risultati, si dovrebbe aumentarlo almeno a 100.

Uno dei principali motivi per l’importanza di aumentare nb_loops è che le lunghezze dei discorsi generati variano notevolmente tra diverse iterazioni, anche con un input fisso.

Una conseguenza di ciò è che la latenza misurata da misura_latenza_e_utilizzo_memoria potrebbe non riflettere effettivamente le prestazioni effettive delle tecniche di ottimizzazione! Il benchmark alla fine del post sul blog riporta i risultati mediati su 100 iterazioni, che danno un’indicazione veritiera delle prestazioni del modello.

1. 🤗 Better Transformer

Better Transformer è una funzionalità ottimale di 🤗 che esegue la fusione del kernel sotto il cofano. Ciò significa che determinate operazioni del modello verranno ottimizzate meglio sulla GPU e che il modello sarà in definitiva più veloce.

Per essere più specifici, la maggior parte dei modelli supportati da 🤗 Transformers si basa sull’attenzione, che consente loro di concentrarsi selettivamente su determinate parti dell’input durante la generazione dell’output. Ciò consente ai modelli di gestire efficacemente le dipendenze a lungo raggio e catturare complesse relazioni contestuali nei dati.

La tecnica di attenzione ingenua può essere notevolmente ottimizzata tramite una tecnica chiamata Flash Attention, proposta dagli autori Dao et. al. nel 2022.

Flash Attention è un algoritmo più veloce ed efficiente per i calcoli di attenzione che combina metodi tradizionali (come la suddivisione in mattoni e la ricomputazione) per ridurre l’utilizzo della memoria e aumentare la velocità. A differenza degli algoritmi precedenti, Flash Attention riduce l’utilizzo della memoria da quadratico a lineare rispetto alla lunghezza della sequenza, rendendolo particolarmente utile per applicazioni in cui l’efficienza della memoria è importante.

Risulta che Flash Attention è supportato da 🤗 Better Transformer nativamente! È sufficiente una riga di codice per esportare il modello in 🤗 Better Transformer e abilitare Flash Attention:

modello =  modello.to_bettertransformer()

with torch.inference_mode():
  speech_output = misura_latenza_e_utilizzo_memoria(modello, input, nb_loops = 5)

Output:

Tempo di esecuzione: 5.43284375 secondi
Utilizzo massimo di memoria 1.9151841280000002  GB

L’output suona così (scarica audio):

Il tuo browser non supporta l’elemento audio.

Cosa porta sul tavolo?

Non vi è alcun degrado delle prestazioni, il che significa che è possibile ottenere esattamente lo stesso risultato senza questa funzione, guadagnando al contempo dal 20% al 30% in velocità! Vuoi saperne di più? Leggi questo post sul blog.

2. Precisione ridotta

La maggior parte dei modelli di intelligenza artificiale utilizza tipicamente un formato di archiviazione chiamato floating point a precisione singola, ovvero fp32. Cosa significa in pratica? Ogni numero viene archiviato utilizzando 32 bit.

Pertanto, è possibile scegliere di codificare i numeri utilizzando 16 bit, con quello che viene chiamato floating point a precisione ridotta, ovvero fp16, e utilizzare la metà dello spazio di archiviazione rispetto al passato! Inoltre, si ottiene anche un aumento della velocità di inferenza!

Ovviamente, ciò comporta anche un piccolo degrado delle prestazioni, poiché le operazioni all’interno del modello non saranno altrettanto precise come utilizzando fp32.

È possibile caricare un modello 🤗 Transformers con precisione ridotta semplicemente aggiungendo torch_dtype=torch.float16 alla riga BarkModel.from_pretrained(...)!

In altre parole:

model = BarkModel.from_pretrained("suno/bark-small", torch_dtype=torch.float16).to(device)

con torch.inference_mode():
  speech_output = measure_latency_and_memory_use(model, inputs, nb_loops = 5)

Output:

Tempo di esecuzione: 7.00045390625 secondi
Utilizzo massimo di memoria 2.7436124160000004  GB

L’output suona così (scarica audio):

Il tuo browser non supporta l’elemento audio.

Cosa porta sul tavolo?

Con un leggero degrado delle prestazioni, si beneficia di una riduzione del 50% della memoria e di un guadagno del 5% in velocità.

3. Spostamento su CPU

Come menzionato nella prima sezione di questo libretto, Bark comprende 4 sottomodelli, che vengono chiamati sequenzialmente durante la generazione audio. In altre parole, mentre un sottomodello è in uso, gli altri sottomodelli sono inattivi.

Perché questo è un problema? La memoria GPU è preziosa nell’intelligenza artificiale, perché è dove le operazioni sono più veloci ed è spesso un collo di bottiglia.

Una soluzione semplice è scaricare i sottomodelli dalla GPU quando sono inattivi. Questa operazione è chiamata spostamento su CPU.

Buone notizie: lo spostamento su CPU per Bark è stato integrato in 🤗 Transformers e puoi usarlo con una sola riga di codice.

È sufficiente assicurarsi che 🤗 Accelerate sia installato!

model = BarkModel.from_pretrained("suno/bark-small")

# Abilita lo spostamento su CPU
model.enable_cpu_offload()

con torch.inference_mode():
  speech_output = measure_latency_and_memory_use(model, inputs, nb_loops = 5)

Output:

Tempo di esecuzione: 8.97633828125 secondi
Utilizzo massimo di memoria 1.3231160320000002  GB

L’output suona così (scarica audio):

Il tuo browser non supporta l’elemento audio.

Cosa porta sul tavolo?

Con un leggero degrado della velocità (10%), si beneficia di una riduzione enorme del consumo di memoria (60% 🤯).

Con questa funzionalità abilitata, l’impronta di memoria di bark-large è ora solo 2GB anziché 5GB. È la stessa impronta di memoria di bark-small!

Vuoi di più? Con fp16 abilitato, scende addirittura a 1GB. Vedremo questo in pratica nella prossima sezione!

4. Combinare

Mettiamo tutto insieme. La buona notizia è che è possibile combinare le tecniche di ottimizzazione, il che significa che è possibile utilizzare lo spostamento su CPU, nonché la precisione ridotta e il 🤗 Better Transformer!

# caricamento in fp16
modello = BarkModel.from_pretrained("suno/bark-small", torch_dtype=torch.float16).to(device)

# converti in bettertransformer
modello = BetterTransformer.transform(modello, keep_original_model=False)

# abilita il trasferimento su CPU
modello.enable_cpu_offload()

with torch.inference_mode():
  output_vocale = measure_latency_and_memory_use(modello, inputs, nb_loops = 5)

Output:

Tempo di esecuzione: 7.4496484375000005 secondi
Peggiore occupazione di memoria: 0.46871091200000004 GB

L’output suona così (scarica audio):

Il tuo browser non supporta l’elemento audio.

Cosa offre?

In definitiva, si ottiene un aumento del 23% della velocità e un enorme risparmio di memoria dell’80%!

Usando il batching

Vuoi di più?

Complessivamente, le 3 tecniche di ottimizzazione portano risultati ancora migliori quando si utilizza il batching. Il batching significa combinare le operazioni per più campioni per ridurre il tempo complessivo impiegato per generare i campioni rispetto alla generazione di campioni singoli.

Ecco un esempio rapido su come puoi utilizzarlo:

text_prompt = [
    "Proviamo a generare il discorso, con Bark, un modello di text-to-speech",
    "Wow, il batching è fantastico!",
    "Adoro Hugging Face, è così figo."]

inputs = processor(text_prompt).to(device)


with torch.inference_mode():
  # i campioni vengono generati tutti in una volta
  output_vocale = modello.generate(**inputs, do_sample = True, fine_temperature = 0.4, coarse_temperature = 0.8)

L’output suona così (scarica il primo, il secondo e l’ultimo audio):

Il tuo browser non supporta l’elemento audio. Il tuo browser non supporta l’elemento audio. Il tuo browser non supporta l’elemento audio.

Come accennato in precedenza, il piccolo esperimento che abbiamo condotto è un esercizio di pensiero e deve essere esteso per una misura migliore delle prestazioni. È anche necessario “riscaldare” la GPU con alcune iterazioni vuote prima di misurare correttamente le prestazioni.

Ecco i risultati di un benchmark di 100 campioni estendendo le misurazioni, utilizzando la versione grande di Bark.

Il benchmark è stato eseguito su una NVIDIA TITAN RTX 24GB con un massimo di 256 nuovi token.

Come leggere i risultati?

Latenza

Misura la durata di una singola chiamata al metodo di generazione, indipendentemente dalla dimensione del batch.

In altre parole, è uguale a elapsedTimenbLoops\frac{elapsedTime}{nbLoops}nbLoopselapsedTime​.

Una latenza inferiore è preferibile.

Impronta di memoria massima

Misura la memoria massima utilizzata durante una singola chiamata al metodo di generazione.

Una minore impronta è preferibile.

Throughput

Misura il numero di campioni generati al secondo. Questa volta, la dimensione del batch viene presa in considerazione.

In altre parole, è uguale a nbLoops∗batchSizeelapsedTime\frac{nbLoops*batchSize}{elapsedTime}elapsedTimenbLoops∗batchSize​.

Un throughput più elevato è preferibile.

Nessun batching

Ecco i risultati con batch_size=1.

Commento

Come previsto, il trasferimento su CPU riduce notevolmente l’impronta di memoria pur aumentando leggermente la latenza.

Tuttavia, combinato con bettertransformer e fp16, otteniamo il meglio di entrambi i mondi, un enorme riduzione di latenza e memoria!

Dimensione del batch impostata su 8

E qui ci sono i risultati del benchmark ma con batch_size=8 e misurazione del throughput.

Si noti che poiché bettertransformer è un’ottimizzazione gratuita perché esegue esattamente la stessa operazione e ha la stessa impronta di memoria del modello non ottimizzato ma è più veloce, il benchmark è stato eseguito con questa ottimizzazione abilitata per impostazione predefinita.

Commento

Qui è possibile vedere il potenziale della combinazione di tutte e tre le caratteristiche di ottimizzazione!

L’impatto di fp16 sulla latenza è meno marcato con batch_size = 1, ma qui è di enorme interesse in quanto può ridurre la latenza di quasi la metà e quasi raddoppiare il throughput!

Questo post sul blog ha mostrato alcuni semplici trucchi di ottimizzazione inclusi nell’ecosistema 🤗. Utilizzando una qualsiasi di queste tecniche o una combinazione di tutte e tre, è possibile migliorare notevolmente la velocità di inferenza di Bark e l’occupazione di memoria.

  • Puoi utilizzare la versione grande di Bark senza alcun degrado delle prestazioni e un’occupazione di memoria di soli 2GB invece di 5GB, più veloce del 15%, utilizzando 🤗 Better Transformer e il trasferimento della CPU.

  • Preferisci un alto throughput? Batch per 8 con 🤗 Better Transformer e mezza precisione.

  • Puoi ottenere il meglio dei due mondi utilizzando fp16, 🤗 Better Transformer e il trasferimento della CPU!