Almacena en caché los modelos de IA en el navegador

La mayoría de los modelos de IA tienen al menos una cosa en común: son bastante grande para un recurso que que se transfieren a través de Internet. El modelo de detección de objetos de MediaPipe más pequeño (SSD MobileNetV2 float16) pesa 5.6 MB. y el más grande es de alrededor de 25 MB.

El LLM de código abierto gemma-2b-it-gpu-int4.bin tiene 1.35 GB, lo que se considera muy pequeño para un LLM. Los modelos de IA generativa pueden ser enormes. Por eso se usa mucho la IA en la actualidad. en la nube. Cada vez más, las apps ejecutan modelos altamente optimizados de forma directa en el dispositivo. Mientras que las demostraciones de LLM se ejecutan en el navegador existen, estos son algunos ejemplos de nivel de producción de otros modelos que se ejecutan en la navegador:

Adobe Photoshop en la Web con la herramienta de selección de objetos potenciada por IA abierta, con tres objetos seleccionados: dos jirafas y una luna.

Para agilizar los próximos lanzamientos de tus aplicaciones, debes almacenar en caché explícitamente los datos del modelo en el dispositivo, en lugar de depender del navegador HTTP implícito la caché.

Si bien en esta guía se usa gemma-2b-it-gpu-int4.bin model para crear un chatbot, El enfoque se puede generalizar para adaptarse a otros modelos y otros casos de uso en el dispositivo. La forma más común de conectar una app a un modelo es entregar junto con el resto de los recursos de la app. Es crucial optimizar el y la entrega de modelos.

Configura los encabezados de caché correctos

Si entregas modelos de IA desde tu servidor, es importante que configures Cache-Control encabezado. En el siguiente ejemplo, se muestra una configuración predeterminada sólida que puedes compilar para las necesidades de tu app.

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

Cada versión lanzada de un modelo de IA es un recurso estático. Contenido que nunca los cambios deben tener un largo max-age en combinación con el almacenamiento en caché en la URL de la solicitud. Si necesitas actualizar el modelo, dale una URL nueva.

Cuando el usuario vuelve a cargar la página, el cliente envía una solicitud de revalidación, incluso aunque el servidor sepa que el contenido es estable. El immutable indica de forma explícita que la revalidación no es necesaria, porque el contenido no cambiará. La directiva immutable es no ampliamente compatible los navegadores y la caché intermedia o servidores proxy, pero combinándolo con se entienda universalmente max-age, puedes garantizar la máxima compatibilidad. La public response indica que la respuesta se puede almacenar en una caché compartida.

Las Herramientas para desarrolladores de Chrome muestran el Cache-Control de producción. encabezados enviados por Hugging Face cuando solicitas un modelo de IA. (Fuente)
.

Almacena en caché los modelos de IA del cliente

Cuando entregas un modelo de IA, es importante almacenarlo en caché de forma explícita en la navegador. Esto garantiza que los datos del modelo estén disponibles cuando un usuario vuelva a cargar la página la aplicación.

Existen varias técnicas que puedes usar para lograrlo. Para los siguientes muestras de código, supongamos que cada archivo de modelo se almacena en un Un objeto Blob llamado blob en la memoria.

Para comprender el rendimiento, cada muestra de código se anota con el performance.mark() y performance.measure() . Estas mediciones dependen del dispositivo y no son generalizables.

En Herramientas para desarrolladores de Chrome, Aplicación > Almacenamiento, revisión diagrama de uso con segmentos para IndexedDB, Cache storage y File System. Se muestra que cada segmento consume 1,354 megabytes de datos, lo que equivale a 4,063 megabytes.

Puedes usar una de las siguientes APIs para almacenar en caché los modelos de IA en el navegador: API de Cache, las API de Origin Private File System y API de IndexedDB. La recomendación general es utilizar Cache API, pero en esta guía se analizan las ventajas y desventajas de todas las opciones.

API de Cache

La API de Cache proporciona almacenamiento persistente para Request y el objeto Response que se almacenan en caché en memoria de larga duración. Si bien es definidas en la especificación de los service workers, puedes usar esta API desde el subproceso principal o un trabajador normal. Para usarlo en el exterior un contexto de service worker, llama al Método Cache.put() con un objeto Response sintético, vinculado con una URL sintética en lugar de una Request.

En esta guía, se supone que hay un blob en la memoria. Usar una URL falsa como la clave de caché y un Response sintético basado en la blob. Si descargaras directamente usarías el Response que obtendrías si haces un fetch() para cada solicitud.

Por ejemplo, aquí se muestra cómo almacenar y restablecer un archivo de modelo con la API de 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 de Origin Private File System

El sistema de archivos privados de origen (OPFS) es un estándar comparativamente joven para un extremo de almacenamiento. Es privada para el origen de la página y, por lo tanto, es invisible. para el usuario, a diferencia del sistema de archivos normal. Proporciona acceso a un servicio altamente optimizado para el rendimiento y ofrece acceso de escritura a su contenido.

Por ejemplo, aquí se muestra cómo almacenar y restablecer un archivo de modelo en 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 de IndexedDB

IndexedDB es un estándar bien establecido para almacenar datos arbitrarios de manera persistente en el navegador. Es infame por su API algo compleja, pero al usar una biblioteca de wrappers, como idb-keyval puedes tratar IndexedDB como un almacén clásico de pares clave-valor.

Por ejemplo:

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

Marcar el almacenamiento como persistente

Llama a navigator.storage.persist() al final de cualquiera de estos métodos de almacenamiento en caché para solicitar permiso de uso y el almacenamiento persistente. Este método muestra una promesa que se resuelve en true si se otorga el permiso y false de lo contrario. El navegador cumplir con la solicitud o no según las reglas específicas del navegador.

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 especial: Usa un modelo en un disco duro

Como alternativa, puedes hacer referencia a modelos de IA directamente desde el disco duro de un usuario. en el almacenamiento del navegador. Esta técnica puede ayudar a las aplicaciones centradas en la investigación a mostrar la la posibilidad de ejecutar determinados modelos en el navegador o permitir que los artistas usen modelos autoentrenados en apps de creatividad de expertos.

API de File System Access

Con la API de File System Access, puedes abrir archivos del disco duro y obtener una FileSystemFileHandle que puedes conservar en IndexedDB.

Con este patrón, el usuario solo debe otorgar acceso al archivo de modelo una vez. Gracias a los permisos persistentes, el usuario puede optar por otorgar acceso al archivo de manera permanente. Después de volver a cargar app y un gesto necesario del usuario, como un clic con el mouse, el FileSystemFileHandle se puede restablecer desde IndexedDB con acceso al archivo en el disco duro.

Los permisos de acceso a archivos se consultan y solicitan si es necesario, lo que hace que de este modo sin inconvenientes para futuras recargas. En el siguiente ejemplo, se muestra cómo obtener un el controlador de un archivo del disco duro, y luego almacenarlo y restablecerlo.

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

Estos métodos no son mutuamente excluyentes. Puede haber un caso en el que ambos almacenar en caché de forma explícita un modelo en el navegador y usar un modelo del disco duro de un usuario.

Demostración

Puedes ver los tres métodos habituales de almacenamiento de casos y el método de disco duro implementada en la demostración de MediaPipe LLM.

Contenido adicional: Descarga un archivo grande en partes

Si necesitas descargar un modelo de IA grande de Internet, paraleliza el descargar en fragmentos separados y volver a unirlos al cliente.

Esta es una función auxiliar que puedes usar en tu código. Solo debes aprobar es el url. El chunkSize (predeterminado: 5 MB), el maxParallelRequests (predeterminado: 6), la función progressCallback (que informa sobre el downloadedBytes y el total de fileSize), y el signal de un Los indicadores AbortSignal son opcionales.

Puedes copiar la siguiente función en tu proyecto instala el paquete fetch-in-chunks del paquete 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;

Elige el método adecuado para ti

En esta guía, se exploraron varios métodos para almacenar en caché de manera eficaz los modelos de IA en el navegador, una tarea crucial para mejorar la experiencia del usuario y el el rendimiento de la app. El equipo de almacenamiento de Chrome recomienda la API de Cache para para garantizar un acceso rápido a los modelos de IA y reducir los tiempos de carga y mejorar la capacidad de respuesta.

OPFS e IndexedDB son opciones menos utilizables. Las APIs de OPFS e IndexedDB debes serializar los datos antes de que se puedan almacenar. IndexedDB también necesita deserializar los datos cuando se recuperan, lo que lo convierte en el peor lugar para almacenarlos para los modelos grandes.

Para aplicaciones específicas, la API de File System Access ofrece acceso directo a los archivos en el dispositivo de un usuario, ideal para quienes administran sus propios modelos de IA.

Si necesitas proteger tu modelo de IA, mantenlo en el servidor. Una vez que las hayas almacenado no es importante extraer los datos de Cache e IndexedDB Herramientas para desarrolladores o la extensión de Herramientas para desarrolladores de OFPS La seguridad de estas APIs de almacenamiento es por naturaleza. Es posible que sientas la tentación de almacenar una versión encriptada del modelo, pero, luego, debes obtener la desencriptación clave al cliente, que podría ser interceptada. Significa que el intento de una persona que actúa de mala fe robar tu modelo es un poco más difícil, pero no imposible.

Te recomendamos que elijas una estrategia de almacenamiento en caché que se alinee con las necesidades de almacenamiento el comportamiento del público objetivo y las características de los modelos de IA que se usan. Esto garantiza que tus aplicaciones sean responsivas y sólidas en diversas las condiciones de la red y las restricciones del sistema.


Agradecimientos

Esto fue revisado por Joshua Bell, Reilly Grant, Evan Stade, Nathan Memmott, Austin Sullivan, Etienne Noël, André Bandarra, Alexandra Klepper, François Beaufort, Paul Kinlan y Rachel Andrew.