Mettre en cache les modèles d'IA dans le navigateur

Thomas Steiner
Thomas Steiner

La plupart des modèles d'IA ont au moins une chose en commun : assez grande pour une ressource transférées sur Internet. Le plus petit modèle de détection d'objets MediaPipe (SSD MobileNetV2 float16) pèse 5,6 Mo et la plus grande est d'environ 25 Mo.

Le LLM Open Source gemma-2b-it-gpu-int4.bin horloges à 1,35 Go, ce qui est considéré comme très petit pour un LLM. Les modèles d'IA générative peuvent être énormes. C'est pourquoi l'IA est aujourd'hui utilisée à bon escient. dans le cloud. De plus en plus, les applications exécutent directement des modèles hautement optimisés sur l'appareil. Bien que les démonstrations de LLM en cours d'exécution dans le navigateur Voici quelques exemples de modèles de niveau production d'autres modèles s'exécutant dans navigateur:

Adobe Photoshop sur le Web avec l'outil de sélection d'objets optimisé par l'IA ouvert, avec trois objets sélectionnés: deux girafes et une lune.

Pour accélérer les futurs lancements de vos applications, vous devez explicitement mettre en cache les données du modèle sur l'appareil, plutôt que d'utiliser le navigateur HTTP implicite cache.

Bien que ce guide utilise gemma-2b-it-gpu-int4.bin model pour créer un chatbot, l'approche peut être généralisée pour s'adapter à d'autres modèles et à d'autres cas d'utilisation sur l'appareil. La méthode la plus courante pour connecter une application à un modèle consiste à diffuser avec le reste des ressources de l'application. Il est essentiel d'optimiser la livraison.

Configurer les en-têtes de cache appropriés

Si vous diffusez des modèles d'IA depuis votre serveur, il est important de configurer Cache-Control en-tête. L'exemple suivant montre un paramètre par défaut solide, que vous pouvez créer selon les besoins de votre application.

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

Chaque version publiée d'un modèle d'IA est une ressource statique. Les contenus qui n'ont jamais les modifications doivent disposer max-age combiné au contournement du cache dans l'URL de la requête. Si vous devez mettre à jour le modèle, vous devez lui attribuer une nouvelle URL.

Lorsque l'utilisateur actualise la page, le client envoie une demande de revalidation, même bien que le serveur sache que le contenu est stable. La immutable indique explicitement que la revalidation n'est pas nécessaire, car le contenu ne changera pas. L'instruction immutable est pas largement accepté par les navigateurs et les caches intermédiaires ou les serveurs proxy, mais en les associant aux max-age universelle, vous pouvez vous assurer qu'un maximum et la compatibilité avec d'autres appareils. public indique que la réponse peut être stockée dans un cache partagé.

<ph type="x-smartling-placeholder">
</ph>
Les outils pour les développeurs Chrome affichent la version de production Cache-Control envoyés par Hugging Face lors de la requête d'un modèle d'IA. (source).

Mettre en cache les modèles d'IA côté client

Lorsque vous diffusez un modèle d'IA, il est important de le mettre en cache de façon explicite navigateur. Cela permet de s'assurer que les données du modèle sont facilement accessibles après l'actualisation de l'utilisateur l'application.

Il existe un certain nombre de techniques que vous pouvez utiliser pour y parvenir. Pour les éléments suivants : exemples de code, supposons que chaque fichier de modèle est stocké dans un Objet Blob nommé blob en mémoire.

Pour comprendre les performances, chaque exemple de code est annoté avec le paramètre performance.mark() et le performance.measure() méthodes. Ces mesures dépendent des appareils et ne sont pas généralisables.

<ph type="x-smartling-placeholder">
</ph>
Dans les outils pour les développeurs Chrome, Application > Stockage, avis le diagramme d'utilisation avec des segments pour IndexedDB, le stockage du cache et le système de fichiers. Chaque segment consomme 1 354 mégaoctets de données, soit un total de 4 063. mégaoctets.

Vous pouvez choisir d'utiliser l'une des API suivantes pour mettre en cache les modèles d'IA dans le navigateur: l'API Cache, API Origin Private File System API IndexedDB : Nous vous recommandons généralement d'utiliser la Cache, mais ce guide décrit les avantages et les inconvénients toutes les options.

API Cache

L'API Cache fournit stockage persistant pour Request et l'objet Response qui sont mises en cache dans une mémoire à longue durée de vie. Même s'il s'agit défini dans la spécification des service workers, vous pouvez utiliser cette API à partir du thread principal ou d'un nœud de calcul standard. Pour l'utiliser à l'extérieur un contexte de service worker, appelez la méthode Méthode Cache.put() à un objet Response synthétique, associé à une URL synthétique au lieu d'un Request.

Ce guide suppose un blob en mémoire. Utilisez une URL fictive comme clé de cache et un Response synthétique basée sur la blob. Si vous téléchargez directement le fichier vous devez utiliser le Response que vous obtenez en créant un fetch() requête.

Par exemple, voici comment stocker et restaurer un fichier de modèle avec 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

Système de fichiers privés d'origine (OPFS) est une norme relativement jeune pour les le point de terminaison Cloud Storage. Il est invisible pour l'origine de la page. à l'utilisateur, contrairement au système de fichiers normal. Elle donne accès à une hautement optimisé pour les performances et offre un accès en écriture à ses contenus.

Par exemple, voici comment stocker et restaurer un fichier de modèle dans 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 est une norme bien établie pour le stockage persistant de données arbitraires. dans le navigateur. Il est tristement célèbre pour son API quelque peu complexe, mais en utilisant Une bibliothèque de wrappers, telle que idb-keyval vous pouvez traiter IndexedDB comme un magasin de paires clé-valeur classique.

Exemple :

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;
  }
};

Marquer le stockage comme persistant

Appeler navigator.storage.persist() à la fin de l'une de ces méthodes de mise en cache pour demander l'autorisation d'utiliser stockage persistant. Cette méthode renvoie une promesse qui renvoie à true si l'autorisation est accordée, et false dans le cas contraire. Le navigateur peut ou non répondre à la demande, en fonction des règles propres au navigateur.

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);
  }
}

Cas particulier: utiliser un modèle sur un disque dur

Vous pouvez référencer des modèles d'IA directement depuis le disque dur d'un utilisateur au lieu de le faire. à l'espace de stockage du navigateur. Cette technique peut aider les applications axées sur la recherche à mettre en valeur possibilité d'exécuter certains modèles dans le navigateur ou autoriser les artistes à utiliser modèles auto-entraînés dans des applis de créativité spécialisées.

API File System Access

Avec l'API File System Access, vous pouvez ouvrir des fichiers à partir du disque dur et obtenir un FileSystemFileHandle que vous pouvez conserver dans IndexedDB.

Avec ce modèle, il suffit à l'utilisateur d'accorder l'accès au fichier de modèle une seule fois. Grâce aux autorisations persistantes, l'utilisateur peut choisir d'accorder l'accès de façon permanente au fichier. Après avoir rechargé et un geste requis de l'utilisateur, comme un clic de souris, FileSystemFileHandle peut être restauré à partir de IndexedDB avec accès au fichier sur le disque dur.

Les autorisations d’accès aux fichiers sont demandées et demandées si nécessaire, ce qui fait pour les recharges futures. L'exemple suivant montre comment obtenir pour un fichier à partir du disque dur, puis stocker et restaurer le 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;
  }
};

Ces méthodes ne s'excluent pas mutuellement. Il se peut que vous mettre en cache explicitement un modèle dans le navigateur et utiliser un modèle du disque dur d'un utilisateur.

Démo

Vous pouvez voir les trois méthodes standards de stockage de cas et la méthode sur disque dur. dans la démonstration LLM MediaPipe.

Bonus: Télécharger un fichier volumineux en fragments

Si vous devez télécharger un grand modèle d'IA depuis Internet, chargez télécharger en morceaux, puis les assembler à nouveau sur le client.

Voici une fonction d'assistance que vous pouvez utiliser dans votre code. Il suffit de transmettre la url. Le chunkSize (par défaut: 5 Mo), le maxParallelRequests (par défaut: 6), la fonction progressCallback (qui génère un rapport sur le downloadedBytes et le fileSize total) et le signal pour un Les signaux AbortSignal sont tous facultatifs.

Vous pouvez copier la fonction suivante dans votre projet ou Installez le package fetch-in-chunks à partir du package 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;

Choisissez la méthode qui vous convient

Ce guide a exploré différentes méthodes permettant de mettre en cache efficacement des modèles d'IA dans navigateur, une tâche essentielle pour améliorer l'expérience de l'utilisateur les performances de votre application. L'équipe Chrome chargée du stockage recommande l'API Cache pour des performances optimales, afin de garantir un accès rapide aux modèles d'IA, ce qui réduit les temps de chargement et d'améliorer la réactivité.

OPFS et IndexedDB sont des options moins utilisables. API OPFS et IndexedDB vous devez sérialiser les données avant de pouvoir les stocker. IndexedDB doit également désérialiser les données lors de leur récupération, ce qui en fait le pire endroit les grands modèles.

Pour les applications de niche, l'API File System Access offre un accès direct aux fichiers sur l'appareil d'un utilisateur, ce qui est idéal pour les utilisateurs qui gèrent leurs propres modèles d'IA.

Si vous devez sécuriser votre modèle d'IA, conservez-le sur le serveur. Une fois le stockage effectué client, il est simple d'extraire les données du cache et de la base de données IndexedDB avec ou via l'extension des outils de développement OFPS. Par nature, ces API de stockage sont les mêmes en termes de sécurité. Vous pourriez être tenté de stocker une version chiffrée du modèle, mais vous devez ensuite récupérer clé au client, qui pourrait être interceptée. Cela signifie que la tentative d'un acteur malintentionné voler votre modèle est un peu plus difficile, mais pas impossible.

Nous vous encourageons à choisir une stratégie de mise en cache adaptée le comportement de l'audience cible et les caractéristiques des modèles d'IA utilisé. Ainsi, vos applications sont réactives et robustes dans diverses conditions les conditions du réseau et les contraintes du système.


Remerciements

Il a été évalué par Joshua Bell, Reilly Grant, Evan Stade, Nathan Memmott, Austin Sullivan, Étienne Noël, André Bandarra, Alexandra Klepper, François Beaufort, Paul Kinlan et Rachel Andrew.