La maggior parte dei modelli di IA ha almeno una cosa in comune: sono molto grandi per una risorsa che viene trasferita su internet. Il modello di rilevamento degli oggetti MediaPipe più piccolo
(SSD MobileNetV2 float16
) pesa 5,6 MB
e il più grande è di circa 25 MB.
L'LLM open source gemma-2b-it-gpu-int4.bin
ha una dimensione di 1,35 GB, considerato molto piccolo per un LLM.
I modelli di IA generativa possono essere enormi. Questo è il motivo per cui oggi gran parte
dell'IA avviene nel cloud. Sempre più app eseguono modelli altamente ottimizzati direttamente sul dispositivo. Sebbene esistano demo di LLM in esecuzione nel browser, ecco alcuni esempi di produzione di altri modelli in esecuzione nel browser:
- Adobe Photoshop esegue una variante del modello
Conv2D
sul dispositivo per il suo strumento intelligente di selezione degli oggetti. - Google Meet esegue una versione ottimizzata del modello
MobileNetV3-small
per la segmentazione delle persone per la funzionalità di sfocatura dello sfondo. - Tokopedia esegue il modello
MediaPipeFaceDetector-TFJS
per il rilevamento dei volti in tempo reale al fine di evitare registrazioni non valide al suo servizio. - Google Colab consente agli utenti di utilizzare modelli dal proprio disco rigido nei blocchi note di Colab.
Per velocizzare i lanci futuri delle applicazioni, devi memorizzare nella cache esplicitamente i dati del modello sul dispositivo, anziché affidarti alla cache implicita del browser HTTP.
Anche se questa guida utilizza gemma-2b-it-gpu-int4.bin model
per creare un chatbot,
l'approccio può essere generalizzato per adeguarsi ad altri modelli e altri casi d'uso
sul dispositivo. Il modo più comune per collegare un'app a un modello è pubblicare il modello insieme al resto delle risorse dell'app. È fondamentale per ottimizzare la
pubblicazione.
Configura le intestazioni cache corrette
Se pubblichi modelli di IA dal tuo server, è importante configurare l'intestazione Cache-Control
corretta. L'esempio seguente mostra un'impostazione predefinita solida, che puoi integrare per le esigenze della tua app.
Cache-Control: public, max-age=31536000, immutable
Ogni versione rilasciata di un modello di IA è una risorsa statica. Ai contenuti che non vengono mai modificati dovrebbe essere assegnato un lungo valore max-age
combinato con il busting della cache nell'URL della richiesta. Se devi aggiornare il modello, devi assegnargli un nuovo URL.
Quando l'utente ricarica la pagina, il client invia una richiesta di riconvalida, anche
se il server sa che i contenuti sono stabili. L'istruzione immutable
indica esplicitamente che la riconvalida non è necessaria, perché i contenuti non cambieranno. L'istruzione immutable
non è ampiamente supportata
dai browser e dalla cache intermedia o dai server proxy, ma
combinandola con
l'istruzione max-age
universalmente comprensibile, puoi garantire la massima
compatibilità. L'istruzione di risposta public
indica che la risposta può essere archiviata in una cache condivisa.
Memorizza nella cache modelli AI lato client
Quando pubblichi un modello di IA, è importante memorizzarlo nella cache esplicitamente nel browser. Ciò garantisce che i dati del modello siano subito disponibili dopo che un utente ha ricaricato l'app.
Esistono varie tecniche che puoi utilizzare per raggiungere questo obiettivo. Per i seguenti
esempi di codice, supponiamo che ogni file del modello sia archiviato in un
oggetto Blob
denominato blob
in memoria.
Per comprendere le prestazioni, ogni esempio di codice viene annotato con i metodi performance.mark()
e performance.measure()
. Queste misure dipendono dal dispositivo e non sono generalizzabili.
Puoi scegliere di utilizzare una delle seguenti API per memorizzare nella cache i modelli di IA nel browser: API Cache, API Origin Private File System e API IndexedDB. In generale, è consigliabile utilizzare l'API Cache, ma questa guida illustra i vantaggi e gli svantaggi di tutte le opzioni.
API Cache
L'API Cache fornisce
archiviazione permanente per coppie di oggetti Request
e Response
che vengono memorizzate nella cache in una memoria di lunga durata. Sebbene sia definita nella specifica dei service worker, puoi utilizzare questa API dal thread principale o da un normale worker. Per utilizzarlo al di fuori
di un contesto di service worker, chiama il metodo
Cache.put()
con un oggetto Response
sintetico, abbinato a un URL sintetico anziché a un
oggetto Request
.
Questa guida presuppone un valore blob
in memoria. Utilizza un URL falso come chiave cache e un
Response
sintetico basato su blob
. Per scaricare direttamente il modello, devi utilizzare il valore Response
ottenuto effettuando una richiesta fetch()
.
Ad esempio, ecco come archiviare e ripristinare un file di modello con l'API Cache.
const storeFileInSWCache = async (blob) => {
try {
performance.mark('start-sw-cache-cache');
const modelCache = await caches.open('models');
await modelCache.put('model.bin', new Response(blob));
performance.mark('end-sw-cache-cache');
const mark = performance.measure(
'sw-cache-cache',
'start-sw-cache-cache',
'end-sw-cache-cache'
);
console.log('Model file cached in sw-cache.', mark.name, mark.duration.toFixed(2));
} catch (err) {
console.error(err.name, err.message);
}
};
const restoreFileFromSWCache = async () => {
try {
performance.mark('start-sw-cache-restore');
const modelCache = await caches.open('models');
const response = await modelCache.match('model.bin');
if (!response) {
throw new Error(`File model.bin not found in sw-cache.`);
}
const file = await response.blob();
performance.mark('end-sw-cache-restore');
const mark = performance.measure(
'sw-cache-restore',
'start-sw-cache-restore',
'end-sw-cache-restore'
);
console.log(mark.name, mark.duration.toFixed(2));
console.log('Cached model file found in sw-cache.');
return file;
} catch (err) {
throw err;
}
};
API Origin Private File System
L'Origin Private File System (OPFS) è uno standard relativamente giovane per un endpoint di archiviazione. È privato per l'origine della pagina e quindi è invisibile all'utente, a differenza del normale file system. Fornisce l'accesso a un file speciale altamente ottimizzato per le prestazioni e offre l'accesso in scrittura ai suoi contenuti.
Ad esempio, ecco come archiviare e ripristinare un file del modello in OPFS.
const storeFileInOPFS = async (blob) => {
try {
performance.mark('start-opfs-cache');
const root = await navigator.storage.getDirectory();
const handle = await root.getFileHandle('model.bin', { create: true });
const writable = await handle.createWritable();
await blob.stream().pipeTo(writable);
performance.mark('end-opfs-cache');
const mark = performance.measure(
'opfs-cache',
'start-opfs-cache',
'end-opfs-cache'
);
console.log('Model file cached in OPFS.', mark.name, mark.duration.toFixed(2));
} catch (err) {
console.error(err.name, err.message);
}
};
const restoreFileFromOPFS = async () => {
try {
performance.mark('start-opfs-restore');
const root = await navigator.storage.getDirectory();
const handle = await root.getFileHandle('model.bin');
const file = await handle.getFile();
performance.mark('end-opfs-restore');
const mark = performance.measure(
'opfs-restore',
'start-opfs-restore',
'end-opfs-restore'
);
console.log('Cached model file found in OPFS.', mark.name, mark.duration.toFixed(2));
return file;
} catch (err) {
throw err;
}
};
API IndexedDB
IndexedDB è uno standard consolidato per l'archiviazione permanente di dati arbitrari in modo permanente nel browser. È notoriamente nota per la sua API piuttosto complessa, ma utilizzando una libreria di wrapper come idb-keyval puoi trattare IndexedDB come un classico archivio di coppie chiave-valore.
Ad esempio:
import { get, set } from 'https://cdn.jsdelivr.net/npm/idb-keyval@latest/+esm';
const storeFileInIDB = async (blob) => {
try {
performance.mark('start-idb-cache');
await set('model.bin', blob);
performance.mark('end-idb-cache');
const mark = performance.measure(
'idb-cache',
'start-idb-cache',
'end-idb-cache'
);
console.log('Model file cached in IDB.', mark.name, mark.duration.toFixed(2));
} catch (err) {
console.error(err.name, err.message);
}
};
const restoreFileFromIDB = async () => {
try {
performance.mark('start-idb-restore');
const file = await get('model.bin');
if (!file) {
throw new Error('File model.bin not found in IDB.');
}
performance.mark('end-idb-restore');
const mark = performance.measure(
'idb-restore',
'start-idb-restore',
'end-idb-restore'
);
console.log('Cached model file found in IDB.', mark.name, mark.duration.toFixed(2));
return file;
} catch (err) {
throw err;
}
};
Contrassegna lo spazio di archiviazione come persistente
Chiama navigator.storage.persist()
al termine di questi metodi di memorizzazione nella cache per richiedere l'autorizzazione a utilizzare
l'archiviazione permanente. Questo metodo restituisce una promessa che si risolve in true
se
è concessa l'autorizzazione e in false
in caso contrario. Il browser potrebbe soddisfare o meno la richiesta, a seconda delle regole specifiche del browser.
if ('storage' in navigator && 'persist' in navigator.storage) {
try {
const persistent = await navigator.storage.persist();
if (persistent) {
console.log("Storage will not be cleared except by explicit user action.");
return;
}
console.log("Storage may be cleared under storage pressure.");
} catch (err) {
console.error(err.name, err.message);
}
}
Caso speciale: utilizzare un modello su un disco rigido
In alternativa all'archiviazione del browser, puoi fare riferimento ai modelli di IA direttamente dal disco rigido di un utente. Questa tecnica può aiutare le app incentrate sulla ricerca a mostrare la fattibilità di eseguire determinati modelli nel browser o consentire agli artisti di utilizzare modelli autoaddestrati in app di creatività esperte.
API File System Access
Con l'API File System Access, puoi aprire i file dal disco rigido e ottenere un FileSystemFileHandle che puoi rendere permanente su IndexedDB.
Con questo pattern, l'utente deve concedere l'accesso al file del modello una sola volta. Grazie alle autorizzazioni persistenti,
l'utente può scegliere di concedere in modo permanente l'accesso al file. Dopo aver ricaricato
l'app e dopo aver eseguito un gesto dell'utente richiesto, ad esempio un clic del mouse, FileSystemFileHandle
può essere ripristinato da IndexedDB con accesso al file
sul disco rigido.
Vengono eseguite query sulle autorizzazioni di accesso ai file e richieste se necessario, il che semplifica il processo per i ricaricamenti futuri. L'esempio seguente mostra come recuperare un handle per un file dal disco rigido e quindi archiviare e ripristinare l'handle.
import { fileOpen } from 'https://cdn.jsdelivr.net/npm/browser-fs-access@latest/dist/index.modern.js';
import { get, set } from 'https://cdn.jsdelivr.net/npm/idb-keyval@latest/+esm';
button.addEventListener('click', async () => {
try {
const file = await fileOpen({
extensions: ['.bin'],
mimeTypes: ['application/octet-stream'],
description: 'AI model files',
});
if (file.handle) {
// It's an asynchronous method, but no need to await it.
storeFileHandleInIDB(file.handle);
}
return file;
} catch (err) {
if (err.name !== 'AbortError') {
console.error(err.name, err.message);
}
}
});
const storeFileHandleInIDB = async (handle) => {
try {
performance.mark('start-file-handle-cache');
await set('model.bin.handle', handle);
performance.mark('end-file-handle-cache');
const mark = performance.measure(
'file-handle-cache',
'start-file-handle-cache',
'end-file-handle-cache'
);
console.log('Model file handle cached in IDB.', mark.name, mark.duration.toFixed(2));
} catch (err) {
console.error(err.name, err.message);
}
};
const restoreFileFromFileHandle = async () => {
try {
performance.mark('start-file-handle-restore');
const handle = await get('model.bin.handle');
if (!handle) {
throw new Error('File handle model.bin.handle not found in IDB.');
}
if ((await handle.queryPermission()) !== 'granted') {
const decision = await handle.requestPermission();
if (decision === 'denied' || decision === 'prompt') {
throw new Error(Access to file model.bin.handle not granted.');
}
}
const file = await handle.getFile();
performance.mark('end-file-handle-restore');
const mark = performance.measure(
'file-handle-restore',
'start-file-handle-restore',
'end-file-handle-restore'
);
console.log('Cached model file handle found in IDB.', mark.name, mark.duration.toFixed(2));
return file;
} catch (err) {
throw err;
}
};
Questi metodi non si escludono a vicenda. Potrebbe capitare che un modello venga memorizzato esplicitamente nella cache nel browser e che venga utilizzato dal disco rigido di un utente.
Demo
Puoi vedere tutti e tre i metodi di archiviazione dei casi standard e il metodo del disco rigido implementato nella demo LLM MediaPipe.
Bonus: scarica un file di grandi dimensioni in blocchi
Se devi scaricare un modello IA di grandi dimensioni da internet, parallelizza il download in blocchi separati e poi uniscilo di nuovo sul client.
Di seguito è riportata una funzione helper che puoi utilizzare nel tuo codice. Devi solo passare url
. chunkSize
(valore predefinito: 5 MB), maxParallelRequests
(valore predefinito: 6), funzione progressCallback
(che genera report su
downloadedBytes
e fileSize
in totale) e signal
per un indicatore
AbortSignal
sono tutti facoltativi.
Puoi copiare la seguente funzione nel tuo progetto o
installare il pacchetto fetch-in-chunks
dal pacchetto npm.
async function fetchInChunks(
url,
chunkSize = 5 * 1024 * 1024,
maxParallelRequests = 6,
progressCallback = null,
signal = null
) {
// Helper function to get the size of the remote file using a HEAD request
async function getFileSize(url, signal) {
const response = await fetch(url, { method: 'HEAD', signal });
if (!response.ok) {
throw new Error('Failed to fetch the file size');
}
const contentLength = response.headers.get('content-length');
if (!contentLength) {
throw new Error('Content-Length header is missing');
}
return parseInt(contentLength, 10);
}
// Helper function to fetch a chunk of the file
async function fetchChunk(url, start, end, signal) {
const response = await fetch(url, {
headers: { Range: `bytes=${start}-${end}` },
signal,
});
if (!response.ok && response.status !== 206) {
throw new Error('Failed to fetch chunk');
}
return await response.arrayBuffer();
}
// Helper function to download chunks with parallelism
async function downloadChunks(
url,
fileSize,
chunkSize,
maxParallelRequests,
progressCallback,
signal
) {
let chunks = [];
let queue = [];
let start = 0;
let downloadedBytes = 0;
// Function to process the queue
async function processQueue() {
while (start < fileSize) {
if (queue.length < maxParallelRequests) {
let end = Math.min(start + chunkSize - 1, fileSize - 1);
let promise = fetchChunk(url, start, end, signal)
.then((chunk) => {
chunks.push({ start, chunk });
downloadedBytes += chunk.byteLength;
// Update progress if callback is provided
if (progressCallback) {
progressCallback(downloadedBytes, fileSize);
}
// Remove this promise from the queue when it resolves
queue = queue.filter((p) => p !== promise);
})
.catch((err) => {
throw err;
});
queue.push(promise);
start += chunkSize;
}
// Wait for at least one promise to resolve before continuing
if (queue.length >= maxParallelRequests) {
await Promise.race(queue);
}
}
// Wait for all remaining promises to resolve
await Promise.all(queue);
}
await processQueue();
return chunks.sort((a, b) => a.start - b.start).map((chunk) => chunk.chunk);
}
// Get the file size
const fileSize = await getFileSize(url, signal);
// Download the file in chunks
const chunks = await downloadChunks(
url,
fileSize,
chunkSize,
maxParallelRequests,
progressCallback,
signal
);
// Stitch the chunks together
const blob = new Blob(chunks);
return blob;
}
export default fetchInChunks;
Scegli il metodo giusto per te
Questa guida ha esplorato vari metodi per memorizzare in modo efficace i modelli di IA nel browser, un'attività fondamentale per migliorare l'esperienza utente e le prestazioni della tua app. Il team di archiviazione di Chrome consiglia l'API Cache per prestazioni ottimali, così da garantire un accesso rapido ai modelli di IA, ridurre i tempi di caricamento e migliorare la reattività.
OPFS e IndexedDB sono opzioni meno utilizzabili. Le API OPFS e IndexedDB devono serializzare i dati prima che possano essere archiviati. IndexedDB deve anche deserializzare i dati quando vengono recuperati, rendendoli il posto peggiore in cui archiviare modelli di grandi dimensioni.
Per le applicazioni di nicchia, l'API File System Access offre l'accesso diretto ai file sul dispositivo di un utente, ideale per gli utenti che gestiscono i propri modelli di IA.
Se devi proteggere il tuo modello di IA, mantienilo sul server. Una volta archiviati sul client, è semplice estrarre i dati sia dalla cache che da IndexedDB con DevTools o l'estensione OFPS DevTools. Queste API di archiviazione sono intrinsecamente uguali per la sicurezza. Potresti avere la tentazione di archiviare una versione criptata del modello, ma poi hai bisogno di ottenere la chiave di decrittografia sul client, che potrebbe essere intercettata. Ciò significa che il tentativo di un utente malintenzionato di rubare il tuo modello è leggermente più difficile, ma non impossibile.
Ti invitiamo a scegliere una strategia di memorizzazione nella cache in linea con i requisiti della tua app, il comportamento del pubblico di destinazione e le caratteristiche dei modelli di IA utilizzati. Ciò garantisce che le tue applicazioni siano reattive e robuste in varie condizioni di rete e vincoli di sistema.
Ringraziamenti
La recensione è stata fatta da Joshua Bell, Reilly Grant, Evan Stade, Nathan Memmott, Austin Sullivan, Etienne Noël, André Bandarra, Alexandra Klepper, François Beaufort, Paul Kinlan e Rachel Andrew.