Cache AI-modellen in de browser

De meeste AI-modellen hebben één ding gemeen: ze zijn vrij groot voor een hulpbron die via internet wordt verzonden. Het kleinste MediaPipe-objectdetectiemodel ( SSD MobileNetV2 float16 ) weegt 5,6 MB en de grootste is ongeveer 25 MB.

De open-source LLM gemma-2b-it-gpu-int4.bin klokt op 1,35 GB – en dit wordt als erg klein beschouwd voor een LLM. Generatieve AI-modellen kunnen enorm zijn. Dit is de reden waarom veel AI-gebruik tegenwoordig in de cloud plaatsvindt. Steeds vaker draaien apps sterk geoptimaliseerde modellen rechtstreeks op het apparaat. Hoewel er demo's bestaan ​​van LLM's die in de browser draaien , zijn hier enkele voorbeelden van productiekwaliteit van andere modellen die in de browser draaien:

Adobe Photoshop op internet met de AI-aangedreven tool voor objectselectie geopend, met drie geselecteerde objecten: twee giraffen en een maan.

Om toekomstige lanceringen van uw toepassingen sneller te laten verlopen, moet u de modelgegevens expliciet op het apparaat in de cache opslaan, in plaats van te vertrouwen op de impliciete HTTP-browsercache.

Hoewel deze handleiding het gemma-2b-it-gpu-int4.bin model gebruikt om een ​​chatbot te maken, kan de aanpak worden gegeneraliseerd voor andere modellen en andere gebruiksscenario's op het apparaat. De meest voorkomende manier om een ​​app aan een model te koppelen, is door het model naast de rest van de app-bronnen te leveren. Het is cruciaal om de levering te optimaliseren.

Configureer de juiste cacheheaders

Als u AI-modellen vanaf uw server bedient, is het belangrijk om de juiste Cache-Control header te configureren. Het volgende voorbeeld toont een solide standaardinstelling, waarop u kunt voortbouwen op de behoeften van uw app.

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

Elke uitgebrachte versie van een AI-model is een statische bron. Inhoud die nooit verandert, moet een lange max-age krijgen, gecombineerd met cachebusting in de verzoek-URL. Als u het model toch moet bijwerken, moet u het een nieuwe URL geven .

Wanneer de gebruiker de pagina opnieuw laadt, verzendt de client een hervalidatieverzoek, ook al weet de server dat de inhoud stabiel is. De immutable richtlijn geeft expliciet aan dat hervalidatie niet nodig is, omdat de inhoud niet zal veranderen. De immutable richtlijn wordt niet breed ondersteund door browsers en tussenliggende cache- of proxyservers, maar door deze te combineren met de universeel begrepen max-age -richtlijn kunt u maximale compatibiliteit garanderen. De public responsrichtlijn geeft aan dat het antwoord kan worden opgeslagen in een gedeelde cache.

Chrome DevTools geeft de productie- Cache-Control headers weer die door Hugging Face worden verzonden bij het aanvragen van een AI-model. ( Bron )

Cache AI-modellen aan de clientzijde

Wanneer u een AI-model aanbiedt, is het belangrijk om het model expliciet in de browser in de cache op te slaan. Dit zorgt ervoor dat de modelgegevens direct beschikbaar zijn nadat een gebruiker de app opnieuw heeft geladen.

Er zijn een aantal technieken die je kunt gebruiken om dit te bereiken. Voor de volgende codevoorbeelden gaan we ervan uit dat elk modelbestand is opgeslagen in een Blob -object met de naam blob in het geheugen.

Om de prestaties te begrijpen, wordt elk codevoorbeeld geannoteerd met de methoden performance.mark() en performance.measure() . Deze maatregelen zijn apparaatafhankelijk en niet generaliseerbaar.

Bekijk in Chrome DevTools Application > Storage het gebruiksdiagram met segmenten voor IndexedDB, Cache-opslag en Bestandssysteem. Er wordt aangetoond dat elk segment 1354 megabytes aan gegevens verbruikt, wat neerkomt op 4063 megabytes.

U kunt ervoor kiezen een van de volgende API's te gebruiken om AI-modellen in de browser in de cache op te slaan: Cache API , de Origin Private File System API en IndexedDB API . De algemene aanbeveling is om de Cache API te gebruiken , maar deze handleiding bespreekt de voor- en nadelen van alle opties.

Cache-API

De Cache API biedt permanente opslag voor Request en Response objectparen die zijn opgeslagen in een langlevend geheugen. Hoewel het is gedefinieerd in de Service Workers-specificatie , kunt u deze API gebruiken vanuit de hoofdthread of vanuit een gewone werker. Als u deze buiten de context van een servicemedewerker wilt gebruiken, roept u de methode Cache.put() aan met een synthetisch Response object, gecombineerd met een synthetische URL in plaats van een Request -object.

In deze hand leiding wordt uitgegaan van een blob in het geheugen. Gebruik een nep-URL als cachesleutel en een synthetisch Response op basis van de blob . Als u het model rechtstreeks zou downloaden, zou u de Response gebruiken die u zou krijgen als u een fetch() -verzoek zou doen.

Hier leest u bijvoorbeeld hoe u een modelbestand kunt opslaan en herstellen met de Cache API.

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

Origin privébestandssysteem-API

Het Origin Private File System (OPFS) is een relatief jonge standaard voor een opslageindpunt. Het is privé voor de oorsprong van de pagina en is dus onzichtbaar voor de gebruiker, in tegenstelling tot het reguliere bestandssysteem. Het biedt toegang tot een speciaal bestand dat sterk is geoptimaliseerd voor prestaties en biedt schrijftoegang tot de inhoud ervan.

Hier leest u bijvoorbeeld hoe u een modelbestand in de OPFS kunt opslaan en herstellen.

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

GeïndexeerdeDB-API

IndexedDB is een gevestigde standaard voor het op een persistente manier opslaan van willekeurige gegevens in de browser. Het staat berucht om zijn ietwat complexe API, maar door een wrapperbibliotheek zoals idb-keyval te gebruiken, kun je IndexedDB behandelen als een klassieke sleutelwaardeopslag.

Bijvoorbeeld:

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

Markeer de opslag als persistent

Roep navigator.storage.persist() aan aan het einde van een van deze cachingmethoden om toestemming te vragen voor het gebruik van permanente opslag. Deze methode retourneert een belofte die wordt omgezet in true als toestemming wordt verleend, en anders in false . De browser kan het verzoek wel of niet honoreren , afhankelijk van browserspecifieke regels.

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

Speciaal geval: Gebruik een model op een harde schijf

U kunt rechtstreeks vanaf de harde schijf van een gebruiker naar AI-modellen verwijzen als alternatief voor browseropslag. Deze techniek kan onderzoeksgerichte apps helpen de haalbaarheid van het uitvoeren van bepaalde modellen in de browser te demonstreren, of artiesten in staat stellen zelfgetrainde modellen te gebruiken in deskundige creativiteitsapps.

Toegang tot bestandssysteem-API

Met de File System Access API kunt u bestanden vanaf de harde schijf openen en een FileSystemFileHandle verkrijgen die u in IndexedDB kunt bewaren.

Bij dit patroon hoeft de gebruiker slechts één keer toegang te verlenen tot het modelbestand. Dankzij blijvende machtigingen kan de gebruiker ervoor kiezen om permanent toegang tot het bestand te verlenen. Na het opnieuw laden van de app en een vereist gebruikersgebaar, zoals een muisklik, kan de FileSystemFileHandle worden hersteld vanuit IndexedDB met toegang tot het bestand op de harde schijf.

De toegangsrechten voor bestanden worden indien nodig opgevraagd en opgevraagd, waardoor dit naadloos verloopt voor toekomstige herlaadbeurten. In het volgende voorbeeld ziet u hoe u een handle voor een bestand van de harde schijf kunt ophalen en vervolgens de handle kunt opslaan en herstellen.

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

Deze methoden sluiten elkaar niet uit. Het kan voorkomen dat u zowel expliciet een model in de browser in de cache plaatst als een model van de harde schijf van een gebruiker gebruikt.

Demo

U kunt alle drie de reguliere casusopslagmethoden en de harde schijfmethode zien die is geïmplementeerd in de MediaPipe LLM-demo .

Bonus: download een groot bestand in stukjes

Als u een groot AI-model van internet moet downloaden, parallelliseert u de download in afzonderlijke delen en voegt u deze vervolgens weer samen op de client.

Hier is een helperfunctie die u in uw code kunt gebruiken. U hoeft alleen de url door te geven. De chunkSize (standaard: 5 MB), de maxParallelRequests (standaard: 6), de progressCallback functie (die rapporteert over de downloadedBytes en de totale fileSize ) en het signal voor een AbortSignal -signaal zijn allemaal optioneel.

U kunt de volgende functie in uw project kopiëren of het fetch-in-chunks -pakket installeren vanuit het npm- pakket.

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;

Kies de juiste methode voor u

In deze handleiding zijn verschillende methoden onderzocht voor het effectief cachen van AI-modellen in de browser, een taak die cruciaal is voor het verbeteren van de gebruikerservaring met en de prestaties van uw app. Het Chrome-opslagteam beveelt de Cache API aan voor optimale prestaties, om snelle toegang tot AI-modellen te garanderen, de laadtijden te verminderen en de responsiviteit te verbeteren.

De OPFS en IndexedDB zijn minder bruikbare opties. De OPFS- en de IndexedDB-API's moeten de gegevens serialiseren voordat deze kunnen worden opgeslagen. IndexedDB moet de gegevens ook deserialiseren wanneer deze worden opgehaald, waardoor dit de slechtste plaats is om grote modellen op te slaan.

Voor nichetoepassingen biedt de File System Access API directe toegang tot bestanden op het apparaat van een gebruiker, ideaal voor gebruikers die hun eigen AI-modellen beheren.

Als u uw AI-model wilt beveiligen, bewaar het dan op de server. Eenmaal opgeslagen op de client, is het triviaal om de gegevens uit zowel de cache als de IndexedDB te extraheren met DevTools of de OFPS DevTools-extensie . Deze opslag-API's zijn inherent gelijk wat betreft beveiliging. U komt misschien in de verleiding om een ​​gecodeerde versie van het model op te slaan, maar u moet dan de decoderingssleutel bij de client krijgen, die kan worden onderschept. Dit betekent dat de poging van een slechte acteur om je model te stelen iets moeilijker is, maar niet onmogelijk.

We raden u aan een cachingstrategie te kiezen die aansluit bij de vereisten van uw app, het doelgroepgedrag en de kenmerken van de gebruikte AI-modellen. Dit zorgt ervoor dat uw applicaties responsief en robuust zijn onder verschillende netwerkomstandigheden en systeembeperkingen.


Dankbetuigingen

Dit werd beoordeeld door Joshua Bell, Reilly Grant, Evan Stade, Nathan Memmott, Austin Sullivan, Etienne Noël, André Bandarra, Alexandra Klepper, François Beaufort, Paul Kinlan en Rachel Andrew.