Memorizza nella cache i modelli AI nel browser

La maggior parte dei modelli di AI ha almeno una cosa in comune: abbastanza grande per una risorsa trasferiti su internet. Il modello di rilevamento degli oggetti MediaPipe più piccolo (SSD MobileNetV2 float16) pesa 5,6 MB e quella più grande è di circa 25 MB.

L'LLM open source gemma-2b-it-gpu-int4.bin ha un orologio di 1,35 GB, che è considerato molto piccolo per un LLM. I modelli di IA generativa possono essere enormi. Questo è il motivo per cui oggigiorno si usa molto l'IA nel cloud. Sempre più, le app eseguono direttamente modelli altamente ottimizzati sul dispositivo. Mentre demo di LLM in esecuzione nel browser esistono alcuni esempi a livello di produzione di altri modelli in esecuzione Browser:

Adobe Photoshop sul web con lo strumento di selezione degli oggetti basato sull'IA aperto, con tre oggetti selezionati: due giraffe e una luna.

Per velocizzare i lanci futuri delle applicazioni, devi memorizzare esplicitamente nella cache i dati del modello on-device, invece di affidarsi al browser HTTP implicito .

Anche se questa guida utilizza gemma-2b-it-gpu-int4.bin model per creare un chatbot, l'approccio può essere generalizzato per adattarsi ad altri modelli e altri casi d'uso sul dispositivo. Il modo più comune per collegare un'app a un modello è pubblicare insieme alle altre risorse dell'app. È fondamentale ottimizzare la distribuzione dei contenuti.

Configura le intestazioni cache corrette

Se pubblichi modelli di IA dal tuo server, è importante configurare il server Cache-Control intestazione. L'esempio seguente mostra una solida impostazione predefinita, che puoi creare per soddisfare le esigenze della tua app.

Cache-Control: public, max-age=31536000, immutable

Ogni versione rilasciata di un modello di IA è una risorsa statica. Contenuti che non hanno mai modifiche deve avere un lungo max-age combinato con il busting della cache nell'URL della richiesta. Se devi aggiornare il modello, devi assegnare 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. La immutable indica esplicitamente che la riconvalida non è necessaria, perché i contenuti non cambieranno. L'istruzione immutable è non ampiamente supportato da browser e cache intermedie o server proxy, ma combinandolo con universalmente accettata max-age, puoi garantire la massima la compatibilità. La public istruzione di risposta indica che la risposta può essere archiviata in una cache condivisa.

Chrome DevTools mostra i dati di produzione Cache-Control intestazioni inviate da Hugging Face quando viene richiesto un modello di IA. (Fonte)

Memorizza nella cache modelli AI lato client

Quando pubblichi un modello di IA, è importante memorizzarlo esplicitamente nella cache del browser. Ciò garantisce che i dati del modello siano facilmente disponibili dopo che un utente ha ricaricato l'app.

Esistono varie tecniche che puoi utilizzare per raggiungere questo obiettivo. Per i seguenti presupponi che ogni file del modello sia archiviato Oggetto Blob denominato blob in memoria.

Per comprendere le prestazioni, ogni esempio di codice è annotato con il performance.mark() e performance.measure() di machine learning. Queste misure dipendono dal dispositivo e non sono generalizzabili.

Nell'applicazione di Chrome DevTools > Spazio di archiviazione, recensione il diagramma di utilizzo con segmenti per IndexedDB, spazio di archiviazione della cache e file system. Viene mostrato che ogni segmento utilizza 1354 megabyte di dati, per un totale di 4063 megabyte.

Puoi scegliere di utilizzare una delle seguenti API per memorizzare nella cache i modelli di IA nel browser: API Cache, il parametro API Origin Private File System e API IndexedDB. In generale, consigliamo di utilizzare Cache, ma questa guida illustra i vantaggi e gli svantaggi delle tutte le opzioni.

API Cache

L'API Cache fornisce spazio di archiviazione permanente per Request e Response oggetto memorizzate nella cache in una memoria di lunga durata. Anche se definita nella specifica Service worker, puoi utilizzare questa API dal thread principale o da un normale worker. Per utilizzarla all'esterno di un Service worker, richiama il Metodo Cache.put() con un oggetto Response sintetico, abbinato a un URL sintetico invece di un Request oggetto.

Questa guida presuppone un valore blob in memoria. Utilizza un URL falso come chiave cache e un sintetico Response in base al blob. Se scaricassi direttamente modello, useresti lo Response che ottieni dalla creazione di un fetch() richiesta.

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

Il file system privato di origine (OPFS) è uno standard relativamente giovane per una endpoint di archiviazione. È privato rispetto all'origine della pagina, quindi è invisibile per l'utente, a differenza del normale file system. Offre l'accesso a uno speciale altamente ottimizzato per le prestazioni e che 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 di dati arbitrari in modo permanente nel browser. È notoriamente nota per la sua API piuttosto complessa, ma utilizzando una libreria 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 il numero navigator.storage.persist() alla fine di uno qualsiasi di questi metodi di memorizzazione nella cache per richiedere l'autorizzazione all'uso l'archiviazione permanente dei dati. Questo metodo restituisce una promessa che si risolve in true se sia concessa l'autorizzazione, mentre false in caso contrario. Il browser può o meno soddisfare 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

Come alternativa, puoi fare riferimento ai modelli di IA direttamente dal disco rigido di un utente allo spazio di archiviazione del browser. Questa tecnica può aiutare le app incentrate sulla ricerca a mostrare 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 mantenere in IndexedDB.

Con questo pattern, l'utente deve solo concedere l'accesso al file del modello una volta sola. Grazie alle autorizzazioni persistenti, l'utente può scegliere di concedere in modo permanente l'accesso al file. Dopo aver ricaricato e un gesto dell'utente richiesto, ad esempio un clic del mouse, È possibile ripristinare FileSystemFileHandle da IndexedDB con accesso al file sul disco rigido.

Vengono interrogate le autorizzazioni di accesso ai file, che vengono richieste se necessario. In questo modo, senza interruzioni per i ricaricamenti futuri. L'esempio seguente mostra come ottenere un l'handle per un file dal disco rigido, quindi archivia e ripristina 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 siate memorizzare nella cache un modello nel browser e utilizzarlo dal disco rigido dell'utente.

Demo

Puoi vedere tutti e tre i metodi normali di archiviazione del caso e il metodo del disco rigido implementato nella demo LLM di MediaPipe.

Bonus: scarica un file di grandi dimensioni in blocchi

Se devi scaricare un modello IA di grandi dimensioni da internet, carica in contemporanea scaricare in blocchi separati e poi unirli di nuovo sul client.

Di seguito è riportata una funzione helper che puoi utilizzare nel tuo codice. Devi solo superare url. chunkSize (valore predefinito: 5 MB), maxParallelRequests (valore predefinito: 6), la funzione progressCallback (che genera report sul downloadedBytes e il totale fileSize) e i signal per un Gli indicatori AbortSignal sono tutti facoltativi.

Puoi copiare la seguente funzione nel tuo progetto o installa 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 nella cache in modo efficace i modelli di IA un browser, un'attività fondamentale per migliorare l'esperienza utente e il rendimento della tua app. Il team di archiviazione di Chrome consiglia l'API Cache per prestazioni ottimali, per garantire un accesso rapido ai modelli di IA, riducendo i tempi di caricamento e il miglioramento della reattività.

OPFS e IndexedDB sono opzioni meno utilizzabili. API OPFS e IndexedDB serializzare i dati prima di poterli archiviare. IndexedDB deve anche deserializzare i dati quando vengono recuperati, rendendoli il posto peggiore in cui archiviarli modelli di grandi dimensioni.

Per le applicazioni di nicchia, l'API File System Access offre l'accesso diretto ai file sul dispositivo di ogni utente, ideale per chi gestisce i propri modelli di IA.

Se devi proteggere il tuo modello di IA, mantienilo sul server. Una volta memorizzati client, è banale 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 cedere alla tentazione e archivi una versione criptata del modello, ma poi devi ottenere la decrittografia al client, che potrebbe essere intercettato. Indica il tentativo di un utente malintenzionato rubare il tuo modello è leggermente più difficile, ma non impossibile.

Ti consigliamo di scegliere una strategia di memorizzazione nella cache in linea con requisiti, comportamento del pubblico di destinazione e caratteristiche dei modelli di IA in uso. Ciò garantisce che le tue applicazioni siano reattive e robuste in vari condizioni di rete e vincoli di sistema.


Ringraziamenti

Recensione di 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.