Transformers.js offre agli sviluppatori Web un modo semplice per sfruttare i vantaggi dei transformer direttamente nei loro Web app, utilizzando pipeline dedicate a compiti specifici. Per effettuare l'inferenza nel browser, gli sviluppatori creano un'istanza di pipeline() e specificano il compito che desiderano utilizzare. A titolo di esempio concreto, il seguente frammento mostra come configurare un pipeline per la riconoscimento automatico della voce (ASR).
Noterete nel codice sorgente che ho specificato Xenova/whisper-tiny.en come modello, una scelta molto valida per compiti generali di riconoscimento vocale in inglese. Di fatto, è addirittura il modello ASR predefinito in Transformers.js, come evidenziato in questo estratto.
Quando eseguite questo esempio nel browser, Transformers.js scarica e cache automaticamente le risorse del modello e i file Wasm necessari. Nella seguente screenshot è visibile la sezione Storage di Chrome DevTools dopo aver visitato l'app. Riaccedendo, le risorse vengono servite direttamente dalla Cache API, il modello produce risultati quasi istantaneamente.
Duplicazione delle risorse
Tuttavia, considerando che Xenova/whisper-tiny,en è un modello molto usato (e, come già detto, anche il modello ASR predefinito in Transformers.js), è facile immaginare che diverse app visitate utilizzeranno lo stesso modello.
Per simulare questa situazione, ecco lo stesso esempio di app, ma distribuito da un'origine diversa. Quando visitate questa seconda origine, invece di essere utilizzabile quasi istantaneamente, il browser deve scaricare e cache nuovamente tutte le risorse del modello, anche se sono byte-by-byte identiche a quelle precedenti. Anche in questo esempio semplice, ciò genera fino a 177 MB di download e di storage duplicati, come vedete nella sezione Storage del pannello Application in Chrome DevTools.
Redundanza nei file Wasm
Però, le cose peggiorano ulteriormente. Inseriamo un secondo pipeline nell'esempio: l'analisi del sentimento. Analisi del sentimento usa di default il modello Xenova/distilbert-base-uncased-finetuned-sst-2-english, il quale non viene specificato manualmente ma viene selezionato automaticamente tramite la risoluzione dei modelli predefiniti di Transformers.js.
Questi due modelli AI totalmente diversi condividono un file comune da 4,733 kB denominato ort-wasm-simd-threaded.asyncify.wasm, ovvero un file WebAssembly (Wasm) runtime utilizzato dalla libreria sottostante ONNX Runtime, su cui si basa Transformers.js. Se visitate il demo esteso da un'origine diversa, noterete dal tab Network che anche il file Wasm runtime viene scaricato e cached di nuovo.
Di conseguenza, anche se le tue app non condividono gli stessi modelli AI, il tuo browser esegue comunque richieste ridondanti per risorse Wasm condivise che hai già, nonché effettua il caching duplicato, occupando inutilmente lo spazio del tuo disco rigido.
Distribuzione delle risorse da CDN
Per default, le risorse del modello AI provengono dal Hugging Face Hub e, di conseguenza, dal CDN di Hugging Face. Il browser invia una richiesta per una risorsa come https://huggingface.co/Xenova/whisper-tiny.en/resolve/main/config.json, che successivamente verrà reindirizzata verso un URL CDN finale.
Isolamento delle origini
I file Wasm vengono serviti da default tramite il CDN jsDelivr. Per esempio, il file ort-wasm-simd-threaded.asyncify.wasm proviene da https://cdn.jsdelivr.net... al momento della scrittura di questo articolo.
Potreste pensare che, se le risorse di varie app vengono servite dallo stesso URL CDN, non dovrebbe esserci alcun problema relativo al caching fintanto che i final URL sono uguali. Purtroppo non funziona esattamente così da tempo.
Per comprendere il funzionamento del caching, l'articolo Gaining security and privacy by partitioning the cache spiega nel dettaglio il motivo per cui il caching è isolato per origine per prevenire attacchi timing: il tempo di risposta HTTP di un sito potrebbe rivelare che il browser ha precedentemente acceduto la stessa risorsa.
La concreta implementazione può variare a seconda del browser, ma in Chrome le risorse cache vengono identificate utilizzando una chiave di isolamento di rete insieme all'URL della risorsa. Questa chiave è composta dal sito a livello superiore e da un sito del frame corrente.
COS: Cross-Origin Storage
Se due esempi del tipo precedentemente descritto fossero ospitati rispettivamente su https://googlechrome.github.io e https://rawcdn.rawgit.net, e utilizzassero entrambi lo stesso file Wasm da https://cdn.jsdelivr.net, le chiavi del cache sarebbero diverse come riportate nella seguente tabella.
Pertanto, anche se gli URL delle risorse sono esattamente uguali, poiché le chiavi di isolamento di rete non coincidono, non si ha un "cache hit", il che vuol dire scarichi e storage duplicati. Questo problema è affrontato dalla proposta Cross-Origin Storage.
Esperimento con COS
💡 Nota: L'API Cross-Origin Storage è un'idea iniziale, non ancora definitiva. Benché l'API non sia ancora integrata in alcun browser nativo, non c'è bisogno di attendere: installate l'estensione per Cross-Origin Storage per iniettare navigator.crossOriginStorage ovunque e testare l'intera procedura.
L'API proposta, denominata Cross-Origin Storage (COS), introduce una speciale interfaccia navigator.crossOriginStorage che web app possono utilizzare per memorizzare e recuperare grandi file oltre le barriere dell'origine. Gli identificatori non sono basati sull'URL ma su hash criptografici.
Funzionalità principali di COS
Quel punto finale sull'hash criptografico è fondamentale. In quanto COS identifica i file tramite hash invece che per URL o origine, il medesimo file ort-wasm-simd-threaded.asyncify.wasm scaricato visitando https://googlechrome.github.io riconosce come identico a quello che https://rawcdn.rawgit.net sta per richiedere, indipendentemente da dove fosse stato inizialmente recuperato. Il seguente snippet illustra schematicamente il flusso di base.
Se la risorsa è in COS, ottieni un FileSystemFileHandle dal quale puoi leggere direttamente il blob tramite getFile() (il risultante File eredita da Blob). Se la risorsa non si trova