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

Scopri come i miglioramenti di WebAssembly e WebGPU migliorano le prestazioni del machine learning sul web.

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

Inferenza IA sul web

Abbiamo sentito parlare da 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 da sole le applicazioni web.

Le pagine web possono incorporare componenti intelligenti per la vista, ad esempio per individuare volti o riconoscere gesti, per la classificazione dell'audio o per il rilevamento della lingua. Nell'ultimo anno abbiamo visto decollare l'IA generativa, incluse alcune demo davvero impressionanti di modelli linguistici di grandi dimensioni (LLM) sul web. Assicurati di dare un'occhiata all'articolo sull'IA pratica on-device per sviluppatori web.

L'inferenza dell'IA sul web è attualmente disponibile su un'ampia sezione di dispositivi e l'elaborazione dell'IA può avvenire nella pagina web stessa, sfruttando l'hardware sul dispositivo dell'utente.

Questa funzione è efficace per diversi motivi:

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

Come vengono eseguiti i carichi di lavoro AI oggi sul web

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

Tutti questi runtime finiscono per terminare l'esecuzione sulla CPU tramite JavaScript o WebAssembly o sulla GPU tramite WebGL o WebGPU.

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

Carichi di lavoro di machine learning

I carichi di lavoro di machine learning (ML) eseguono il push dei tensori attraverso un grafico di nodi di calcolo. I tensor 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 di grandi dimensioni che eseguono calcoli su modelli che possono avere miliardi di ponderazioni
  • La scalabilità e l'inferenza possono portare al Parallelismo dei dati. Ciò significa che vengono eseguite le stesse operazioni su tutti gli elementi dei tensori.
  • Il machine learning non richiede precisione. Potresti aver bisogno di un numero in virgola mobile a 64 bit per atterrare sulla Luna, ma potresti aver bisogno solo di un mare di numeri da 8 bit o meno per il riconoscimento facciale.

Fortunatamente, i designer dei chip hanno aggiunto funzionalità per rendere i modelli più veloci e più efficienti, nonché per consentire la loro esecuzione.

Nel frattempo, qui, tra i team WebAssembly e WebGPU, stiamo lavorando per esporre queste nuove funzionalità agli sviluppatori web. Se sei uno sviluppatore di applicazioni web, è improbabile che utilizzi spesso queste primitive di basso livello. Prevediamo che le toolchain o i framework che utilizzi supportino nuove funzionalità ed estensioni, per cui puoi trarre vantaggio da modifiche minime all'infrastruttura. Ma se ti piace regolare manualmente le prestazioni delle applicazioni, queste funzionalità sono pertinenti al tuo lavoro.

WebAssembly

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

Le informazioni del modulo Wasm sono rappresentate con una codifica binaria densa. Rispetto a un formato di testo, questo si traduce in una decodifica più rapida, un caricamento più rapido e una riduzione dell'utilizzo della memoria. La portabilità è nel senso che non fa ipotesi sull'architettura sottostante che non sono già comuni alle architetture moderne.

La specifica di WebAssembly è iterativa e viene elaborata in un gruppo di community W3C aperto.

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

La tua applicazione può essere compilata una sola volta ed essere eseguita ovunque: un computer desktop, un laptop, un telefono o qualsiasi altro dispositivo con un browser. Per saperne di più, consulta Scrivi una volta, esegui ovunque sia stato finalmente realizzato con WebAssembly.

Illustrazione di un laptop, un tablet e uno smartphone

La maggior parte delle applicazioni di produzione che eseguono l'inferenza dell'IA sul web utilizza WebAssembly, sia per il calcolo della CPU sia per l'interfacciamento con operazioni di calcolo per scopi speciali. Nelle applicazioni native puoi accedere sia al computing per uso generico sia a quello per scopi speciali, in quanto l'applicazione può accedere alle funzionalità del dispositivo.

Sul web, per la portabilità e la sicurezza, valutiamo con attenzione quale insieme di primitive sono esposti. Questo consente di bilanciare l'accessibilità del web con le massime prestazioni fornite dall'hardware.

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

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

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

Adotta un approccio completo alle applicazioni

Dovresti 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 ci sono modelli in cui la varianza potrebbe essere significativamente maggiore.

Per quanto riguarda i carichi di lavoro ML, consideriamo una visione globale delle applicazioni, ascoltando gli autori dei framework e i partner delle applicazioni, per sviluppare e distribuire i miglioramenti più richiesti. Rientrano in tre categorie generali:

  • Esponi le estensioni della CPU fondamentali per le prestazioni
  • Abilita l'esecuzione di modelli più grandi
  • Abilita l'interoperabilità perfetta con altre API web

Calcolo più veloce

Allo stato attuale, le specifiche di WebAssembly includono solo un determinato insieme di istruzioni che esponiamo sul web. Tuttavia, l'hardware continua ad aggiungere istruzioni più recenti che aumentano il divario tra le prestazioni native e di WebAssembly.

Ricorda che i modelli di ML non sempre richiedono elevati livelli di precisione. SIMD semplificato è una proposta che riduce alcuni dei rigorosi requisiti non deterministici, determinando una codegen più veloce per alcune operazioni vettoriali che sono aspetti prioritari per le prestazioni. Inoltre, Accessibility SIMD introduce nuove istruzioni per prodotti scalare e FMA che accelerano i carichi di lavoro esistenti da 1,5 a 3 volte. Questo è stato spedito in Chrome 114.

Il formato in virgola mobile a mezza precisione utilizza 16 bit per IEEE FP16 anziché 32 bit utilizzati per valori di precisione singoli. Rispetto ai singoli valori di precisione, esistono diversi vantaggi nell'utilizzo di valori di metà precisione, requisiti di memoria ridotti, che consentono l'addestramento e lo sviluppo di reti neurali più grandi, nonché larghezza di banda di memoria ridotta. La precisione ridotta velocizza il trasferimento dei dati e le operazioni matematiche.

Modelli più grandi

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

Soprattutto con i modelli di grandi dimensioni come quelli attuali, il caricamento di questi modelli in WebAssembly può essere restrittivo. La proposta Memory64 rimuove queste restrizioni imposte dalla memoria lineare per dimensioni superiori a 4 GB e corrispondenti allo spazio degli indirizzi delle piattaforme native.

Abbiamo un'implementazione completa in Chrome e la sua distribuzione è prevista 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 il calcolo per scopi speciali sul web.

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

Emscripten, la toolchain di compilazione Wasm, ha già associazioni per WebGPU. È il punto di accesso per l'inferenza dell'IA sul web, quindi è fondamentale che Wasm possa interagire senza soluzione di continuità con il resto della piattaforma web. Stiamo lavorando su un paio di proposte diverse in questo ambito.

JavaScript promessa di integrazione (JSPI)

Le tipiche applicazioni C e C++ (così come molti altri linguaggi) vengono comunemente scritte su 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 con riconoscimento asincrono.

Quando le operazioni costose bloccano il thread principale, possono bloccare l'I/O e il jank è visibile agli utenti. Mancata corrispondenza tra il modello di programmazione sincrono delle applicazioni native e il modello asincrono del web. Ciò è particolarmente problematico per le applicazioni legacy, che sarebbe costoso da trasferire. Emscripten offre un modo per farlo con Asyncify, ma non è sempre l'opzione migliore: codice più grande e non così efficiente.

L’esempio seguente riguarda il calcolo di fibonacci, in cui vengono utilizzate le promesse di aggiunta tramite JavaScript.

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 colla necessario per consentirci di utilizzare JSPI per accedere al risultato della promessa, proprio come farebbe per una funzione normale.
  • L'opzione speciale della riga di comando, -s ASYNCIFY=2. Questo richiama l'opzione per generare codice che utilizza JSPI per interfacciarsi con importazioni JavaScript che restituiscono promesse.

Per saperne di più su JSPI, su come utilizzarlo e sui suoi vantaggi, leggi Introduzione all'API WebAssembly JavaScript Promise Integration su v8.dev. Scopri di più sulla prova dell'origine attuale.

Controllo della memoria

Gli sviluppatori hanno un controllo minimo sulla memoria Wasm; il modulo possiede la propria memoria. Qualsiasi API che ha bisogno di accedere a questa memoria deve essere copiata o copiata e questo utilizzo può davvero aumentare. Ad esempio, un'applicazione di grafica potrebbe dover copiare e copiare per ogni frame.

La proposta 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, la stiamo prototipando nella V8, il motore JavaScript di Chrome, per dare forma all'evoluzione dello standard.

Scegli il backend più adatto a te

Anche se la CPU è onnipresente, non è sempre l'opzione migliore. Il calcolo per scopi speciali su GPU o acceleratori può offrire prestazioni di ordini di grandezza superiori, soprattutto per modelli più grandi e su dispositivi di fascia alta. Questo vale sia per le applicazioni native sia per le applicazioni web.

Il backend che scegli dipende dall'applicazione, dal framework o dalla toolchain, nonché da altri fattori che influenzano le prestazioni. Detto questo, continuiamo a investire in proposte che consentano a Wasm di base di funzionare bene con il resto della piattaforma web e, più in particolare, con WebGPU.

Continua a leggere la Parte 2