Miglioramenti di WebAssembly e WebGPU per un'IA web più veloce, parte 1

Scopri in che modo i miglioramenti di WebAssembly e WebGPU migliorano il rendimento del machine learning sul web.

Austin Eng
Austin Eng
Deepti Gandluri
Deepti Gandluri
François Beaufort
François Beaufort

Inferenza AI sul web

Lo sappiamo tutti: l'IA sta trasformando il nostro mondo. Il web non fa eccezione.

Quest'anno Chrome ha aggiunto funzionalità di IA generativa, tra cui la creazione di temi personalizzati e/o l'aiuto per scrivere una prima bozza di testo. Ma l'IA è molto di più: può arricchire le applicazioni web stesse.

Le pagine web possono incorporare componenti intelligenti per la visione, come il rilevamento di volti o gesti, per la classificazione dell'audio o per il rilevamento della lingua. Nell'ultimo anno abbiamo assistito al boom dell'IA generativa, incluse alcune demo davvero impressionanti di modelli linguistici di grandi dimensioni sul web. Non perderti l'articolo AI on-device pratica per gli sviluppatori web.

L'inferenza AI sul web è oggi disponibile su una vasta gamma di dispositivi e l'elaborazione dell'IA può avvenire nella pagina web stessa, sfruttando l'hardware del dispositivo dell'utente.

Questo è utile per diversi motivi:

  • Costi ridotti: l'esecuzione dell'inferenza sul client del browser riduce notevolmente i costi del server e questo può essere particolarmente utile per le query di IA generativa, che possono essere ordini di grandezza più costose delle query normali.
  • Latenza: per le applicazioni particolarmente sensibili alla latenza, come quelle audio o video, l'esecuzione di tutta l'elaborazione sul dispositivo comporta una latenza ridotta.
  • Privacy: l'esecuzione lato client ha anche il potenziale di sbloccare una nuova classe di applicazioni che richiedono una maggiore privacy, in cui i dati non possono essere inviati al server.

Come vengono eseguiti attualmente i carichi di lavoro di IA sul web

Oggi, gli sviluppatori di applicazioni e i ricercatori creano modelli utilizzando framework, i modelli vengono eseguiti nel browser utilizzando un runtime come Tensorflow.js o ONNX Runtime Web e i runtime utilizzano API web per l'esecuzione.

Alla fine, tutti questi runtime vengono eseguiti sulla CPU tramite JavaScript o WebAssembly o sulla GPU tramite WebGL o WebGPU.

Diagramma che mostra come vengono eseguiti i carichi di lavoro di IA sul web oggi

Carichi di lavoro di machine learning

I carichi di lavoro di machine learning (ML) inviano i tensori attraverso un grafo di nodi di calcolo. I tensori sono gli input e gli output di questi nodi che eseguono una grande quantità di calcoli sui dati.

Questo è importante perché:

  • I tensori sono strutture di dati molto grandi che eseguono calcoli su modelli che possono avere miliardi di pesi.
  • La scalabilità e l'inferenza possono portare al parallismo dei dati. Ciò significa che le stesse operazioni vengono eseguite su tutti gli elementi dei tensori.
  • L'ML non richiede precisione. Potresti aver bisogno di un numero in virgola mobile a 64 bit per atterrare sulla Luna, ma per il riconoscimento facciale potresti aver bisogno solo di un mare di numeri a 8 bit o meno.

Fortunatamente, i progettisti di chip hanno aggiunto funzionalità per far funzionare i modelli più velocemente, a una temperatura più bassa e persino per renderne possibile l'esecuzione.

Nel frattempo, i team di WebAssembly e WebGPU stanno lavorando per rendere disponibili queste nuove funzionalità agli sviluppatori web. Se sei uno sviluppatore di applicazioni web, è improbabile che tu utilizzi spesso queste primitive di basso livello. Ci aspettiamo che le toolchain o i framework che utilizzi supportino nuove funzionalità ed estensioni, in modo da poter usufruire di questi vantaggi con modifiche minime all'infrastruttura. Tuttavia, se ti piace ottimizzare manualmente le applicazioni per migliorare il rendimento, queste funzionalità sono pertinenti per il tuo lavoro.

WebAssembly

WebAssembly (Wasm) è un formato di codice bytecode compatto ed efficiente che gli ambienti di runtime possono comprendere ed eseguire. È progettato per sfruttare le funzionalità hardware sottostanti, in modo da poter essere eseguito a velocità quasi native. Il codice viene convalidato ed eseguito in un ambiente sandbox sicuro per la memoria.

Le informazioni sul modulo Wasm sono rappresentate con una codifica binaria densa. Rispetto a un formato basato su testo, significa decodifica più rapida, caricamento più veloce e utilizzo ridotto della memoria. È portabile nel senso che non fa supposizioni sull'architettura di base che non siano già comuni alle architetture moderne.

La specifica WebAssembly è iterativa e viene sviluppata in un gruppo della community W3C aperto.

Il formato binario non fa supposizioni sull'ambiente host, quindi è progettato per funzionare bene anche negli incorporamenti non web.

L'applicazione può essere compilata una volta ed eseguita ovunque: su un computer, un laptop, uno smartphone o qualsiasi altro dispositivo con un browser. Per saperne di più, consulta l'articolo Scrivere una volta, eseguire ovunque finalmente realizzato con WebAssembly.

Illustrazione di un laptop, un tablet e uno smartphone

La maggior parte delle applicazioni di produzione che eseguono l'inferenza AI sul web utilizza WebAssembly, sia per il calcolo della CPU sia per l'interfaccia con il calcolo per scopi speciali. Nelle applicazioni native, puoi accedere all'elaborazione sia per uso generico che per uso speciale, poiché l'applicazione può accedere alle funzionalità del dispositivo.

Sul web, per motivi di portabilità e sicurezza, valutiamo attentamente l'insieme di primitive esposte. In questo modo, viene bilanciata l'accessibilità del web con le prestazioni massime fornite dall'hardware.

WebAssembly è un'astrazione portatile delle CPU, pertanto tutta l'inferenza Wasm viene eseguita sulla CPU. Sebbene non sia la scelta più performante, le CPU sono ampiamente disponibili e funzionano sulla maggior parte dei carichi di lavoro e dei dispositivi.

Per carichi di lavoro più piccoli, come quelli di testo o audio, la GPU sarebbe costosa. Esistono diversi esempi recenti in cui Wasm è la scelta giusta:

Puoi scoprire di più nelle demo open source, ad esempio whisper-tiny, llama.cpp e Gemma2B in esecuzione nel browser.

Adotta un approccio olistico alle tue applicazioni

Devi scegliere le primitive in base al particolare modello ML, all'infrastruttura dell'applicazione e all'esperienza complessiva prevista per gli utenti

Ad esempio, nel rilevamento dei punti di riferimento del volto di MediaPipe, l'inferenza della CPU e l'inferenza della GPU sono paragonabili (in esecuzione su un dispositivo Apple M1), ma esistono modelli in cui la varianza potrebbe essere notevolmente superiore.

Per quanto riguarda i carichi di lavoro di ML, prendiamo in considerazione una visione olistica dell'applicazione, ascoltando gli autori del framework e i partner di applicazione, per sviluppare e rilasciare i miglioramenti più richiesti. In linea di massima, si suddividono in tre categorie:

  • Esporre le estensioni della CPU fondamentali per le prestazioni
  • Consentire l'esecuzione di modelli più grandi
  • Consente l'interoperabilità senza problemi con altre API web

Calcolo più rapido

Al momento, la specifica WebAssembly include solo un determinato insieme di istruzioni che esponiamo al web. Tuttavia, l'hardware continua ad aggiungere istruzioni più recenti che aumentano il divario tra le prestazioni native e quelle di WebAssembly.

Ricorda che i modelli di ML non richiedono sempre livelli elevati di precisione. SIMD rilassato è una proposta che riduce alcuni dei requisiti rigorosi di non determinismo, portando a una generazione di codice più rapida per alcune operazioni vettoriali che sono hot spot per le prestazioni. Inoltre, SIMD rilassato introduce nuove istruzioni di prodotto scalare e FMA che velocizzano i carichi di lavoro esistenti da 1,5 a 3 volte. Questa funzionalità è stata rilasciata in Chrome 114.

Il formato a virgola mobile a precisione dimezzata utilizza 16 bit per IEEE FP16 anziché i 32 bit utilizzati per i valori a precisione singola. Rispetto ai valori a precisione singola, l'utilizzo di valori a precisione dimezzata presenta diversi vantaggi: requisiti di memoria ridotti, che consentono l'addestramento e il deployment di reti neurali più grandi e una larghezza di banda della memoria ridotta. Una precisione ridotta velocizza il trasferimento dei dati e le operazioni matematiche.

Modelli più grandi

I puntatori alla memoria lineare Wasm sono rappresentati come numeri interi a 32 bit. Ciò ha due conseguenze: le dimensioni dell'heap sono limitate a 4 GB (quando i computer hanno molta più RAM fisica) e il codice dell'applicazione che ha come target Wasm deve essere compatibile con una dimensione del puntatore a 32 bit (che).

Soprattutto con modelli di grandi dimensioni come quelli di oggi, il caricamento di questi modelli in WebAssembly può essere limitativo. La proposta Memory64 rimuove queste limitazioni in base alla memoria lineare maggiore di 4 GB e corrispondente allo spazio degli indirizzi delle piattaforme native.

Abbiamo un'implementazione completa e funzionante in Chrome e prevediamo di rilasciarla entro la fine dell'anno. Per il momento, puoi eseguire esperimenti con il flag chrome://flags/#enable-experimental-webassembly-features e inviarci un feedback.

Migliore interoperabilità web

WebAssembly potrebbe essere il punto di ingresso per l'elaborazione per scopi speciali sul web.

WebAssembly può essere utilizzato per portare le applicazioni GPU sul web. Ciò significa che la stessa applicazione C++ che può essere eseguita sul dispositivo può essere eseguita anche sul web, con piccole modifiche.

Emscripten, la toolchain del compilatore Wasm, ha già le associazioni per WebGPU. È il punto di contatto per l'inferenza AI sul web, quindi è fondamentale che Wasm possa interoperare senza problemi con il resto della piattaforma web. Stiamo lavorando a un paio di proposte diverse in questo ambito.

Integrazione delle promesse JavaScript (JSPI)

Le applicazioni C e C++ (nonché molti altri linguaggi) standard vengono in genere scritte per un'API sincrona. Ciò significa che l'applicazione interromperà l'esecuzione fino al completamento dell'operazione. Queste applicazioni di blocco sono in genere più intuitive da scrivere rispetto alle applicazioni compatibili con l'asynchronismo.

Quando le operazioni dispendiose bloccano il thread principale, possono bloccare l'I/O e il jitter è visibile agli utenti. Esiste una mancata corrispondenza tra un modello di programmazione sincrono delle applicazioni native e il modello asincrono del web. Questo è particolarmente problematico per le applicazioni legacy, la cui portabilità sarebbe costosa. Emscripten fornisce un modo per farlo con Asyncify, ma non è sempre l'opzione migliore: il codice è più grande e meno efficiente.

L'esempio seguente calcola la serie di Fibonacci utilizzando le promesse JavaScript per l'addizione.

long promiseFib(long x) {
 if (x == 0)
   return 0;
 if (x == 1)
   return 1;
 return promiseAdd(promiseFib(x - 1), promiseFib(x - 2));
}
// promise an addition
EM_ASYNC_JS(long, promiseAdd, (long x, long y), {
  return Promise.resolve(x+y);
});
emcc -O3 fib.c -o b.html -s ASYNCIFY=2

In questo esempio, presta attenzione a quanto segue:

  • La macro EM_ASYNC_JS genera tutto il codice di collegamento necessario per consentirci di utilizzare JSPI per accedere al risultato della promessa, proprio come per una normale funzione.
  • L'opzione speciale della riga di comando, -s ASYNCIFY=2. Viene invocata l'opzione per generare codice che utilizza JSPI per interfacciarsi con le importazioni JavaScript che restituiscono promesse.

Per saperne di più su JSPI, su come utilizzarlo e sui suoi vantaggi, leggi l'articolo Introduzione all'API di integrazione delle promesse JavaScript WebAssembly su v8.dev. Scopri la prova dell'origine corrente.

Controllo della memoria

Gli sviluppatori hanno un controllo molto limitato sulla memoria Wasm; il modulo possiede la propria memoria. Tutte le API che devono accedere a questa memoria devono eseguire operazioni di copia in o copia fuori e questo utilizzo può essere molto elevato. Ad esempio, un'applicazione grafica potrebbe dover eseguire copie in e copie fuori per ogni frame.

La proposta di controllo della memoria mira a fornire un controllo più granulare sulla memoria lineare Wasm e a ridurre il numero di copie nella pipeline dell'applicazione. Questa proposta è nella fase 1, stiamo creando una prototipazione in V8, il motore JavaScript di Chrome, per informare sull'evoluzione dello standard.

Decidere quale backend è adatto a te

Sebbene la CPU sia onnipresente, non è sempre l'opzione migliore. Il calcolo a scopo speciale sulle GPU o sugli acceleratori può offrire prestazioni di diversi ordini di grandezza superiori, in particolare per i modelli più grandi e sui dispositivi di fascia alta. Questo vale sia per le applicazioni native che per quelle web.

Il backend scelto dipende dall'applicazione, dal framework o dalla toolchain, nonché da altri fattori che influiscono sul rendimento. Detto questo, continuiamo a investire in proposte che consentano a Wasm di funzionare bene con il resto della piattaforma web e, in particolare, con WebGPU.

Continua a leggere la Parte 2