KI-Modelle im Browser zwischenspeichern

Die meisten KI-Modelle haben mindestens eines gemeinsam: Sie sind recht groß für eine Ressource sein, über das Internet übertragen werden. Das kleinste MediaPipe-Objekterkennungsmodell (SSD MobileNetV2 float16) wiegt 5,6 MB und die größte mit etwa 25 MB.

Das Open-Source-LLM gemma-2b-it-gpu-int4.bin mit 1,35 GB, was für ein LLM als sehr klein gilt. Modelle für generative KI können enorm sein. Deshalb wird KI häufig genutzt, in der Cloud. Immer mehr Anwendungen führen hochoptimierte Modelle direkt aus. auf dem Gerät. Während Demos von LLMs, die im Browser ausgeführt werden, gibt es einige produktionstaugliche Beispiele anderer Modelle, die in der Browser:

Adobe Photoshop im Web, in dem das KI-gestützte Tool zur Objektauswahl geöffnet ist und drei Objekte ausgewählt sind: zwei Giraffen und ein Mond.

Um zukünftige Starts Ihrer Anwendungen zu beschleunigen, sollten Sie die Modelldaten direkt auf dem Gerät abrufen, anstatt sich auf den impliziten HTTP-Browser zu verlassen Cache gespeichert werden.

In dieser Anleitung wird zwar gemma-2b-it-gpu-int4.bin model zum Erstellen eines Chatbots verwendet, Der Ansatz kann verallgemeinert werden, damit er für andere Modelle und andere Anwendungsfälle geeignet ist. auf dem Gerät. Die gängigste Methode zum Verbinden einer App mit einem Modell besteht darin, mit den restlichen App-Ressourcen kombinieren. Es ist wichtig, die Auslieferung.

Die richtigen Cache-Header konfigurieren

Wenn Sie KI-Modelle über Ihren Server bereitstellen, ist es wichtig, die richtigen Cache-Control Header. Das folgende Beispiel zeigt eine solide Standardeinstellung, die Sie für die Anforderungen Ihrer App aktiviert.

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

Jede veröffentlichte Version eines KI-Modells ist eine statische Ressource. Inhalte, die nie sollten Änderungen einen langen max-age kombiniert mit Cache-Busting in der Anfrage-URL. Wenn Sie das Modell aktualisieren müssen, geben Sie ihr eine neue URL.

Wenn der Nutzer die Seite neu lädt, sendet der Client eine erneute Validierungsanfrage, auch wenn auch wenn der Server weiß, dass der Inhalt stabil ist. Die immutable gibt ausdrücklich an, dass eine erneute Validierung nicht erforderlich ist, da die Anweisung ändert sich nichts. Die Anweisung immutable ist nicht allgemein unterstützt zwischen Browsern und zwischengeschalteten Caches oder Proxyservern, aber durch in Kombination allgemein verständliche max-age-Richtlinie können Sie Kompatibilität. Die public response-Anweisung gibt an, dass die Antwort in einem gemeinsamen Cache gespeichert werden kann.

<ph type="x-smartling-placeholder">
</ph>
Die Chrome-Entwicklertools zeigen die Produktionsdaten Cache-Control Header, die von Hugging Face beim Anfordern eines KI-Modells gesendet werden. (Quelle)

KI-Modelle clientseitig im Cache speichern

Wenn Sie ein KI-Modell bereitstellen, ist es wichtig, das Modell explizit im Browser. Dadurch wird sichergestellt, dass die Modelldaten nach dem Aktualisieren durch den Benutzer leicht verfügbar sind. in der App.

Es gibt eine Reihe von Techniken, die Sie anwenden können, um dies zu erreichen. Für dass jede Modelldatei in einem Blob-Objekt namens blob im Gedächtnis gespeichert.

Um die Leistung nachzuvollziehen, ist jedes Codebeispiel mit dem performance.mark() und die performance.measure() . Diese Messungen sind geräteabhängig und nicht generalisierbar.

<ph type="x-smartling-placeholder">
</ph>
In den Chrome-Entwicklertools: Anwendung > Speicherplatz, Rezension das Nutzungsdiagramm mit Segmenten für IndexedDB, Cache-Speicher und Dateisystem. Jedes Segment verbraucht 1.354 Megabyte Daten, was insgesamt 4.063 Megabyte ergibt. Megabyte groß.

Sie können eine der folgenden APIs verwenden, um KI-Modelle im Browser zwischenzuspeichern: Cache API, die Origin Private File System API und IndexedDB API Die allgemeine Empfehlung ist die Verwendung der Cache API. In diesem Leitfaden werden jedoch die Vor- und Nachteile von alle Optionen.

Cache-API

Die Cache API bietet Nichtflüchtiger Speicher für Request und Response-Objekt -Paare, die im langlebigen Speicher zwischengespeichert werden. Obwohl es die in der Service Workers-Spezifikation definiert sind, können Sie diese API aus dem Hauptthread oder aus einem regulären Worker verwenden. Verwendung im Freien Service Worker-Kontext haben, rufen Sie die Methode Cache.put() mit einem synthetischen Response-Objekt, das mit einer synthetischen URL anstelle einer Request-Objekt.

In dieser Anleitung wird von einem speicherinternen blob ausgegangen. Verwenden Sie eine gefälschte URL als Cache-Schlüssel und einen synthetischen Response basierend auf dem blob-Wert. Wenn Sie die Datei würden Sie die Response verwenden, die Sie bei einer fetch()

Hier erfahren Sie beispielsweise, wie Sie eine Modelldatei mit der Cache API speichern und wiederherstellen.

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

Ursprüngliche Private File System API

Das ursprüngliche private Dateisystem (OPFS) ist ein vergleichsweise junger Standard für eine Speicherendpunkt. Sie ist für den Ursprung der Seite privat und daher nicht sichtbar. im Gegensatz zum regulären Dateisystem. Sie erhalten Zugang zu einer speziellen die hoch leistungsoptimiert ist und Schreibzugriff auf ihre Inhalte.

So können Sie beispielsweise eine Modelldatei in der OPFS speichern und wiederherstellen:

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

IndexedDB-API

IndexedDB ist ein etablierter Standard für die dauerhafte Speicherung beliebiger Daten. im Browser. Es ist berühmt für seine relativ komplexe API. Durch die Verwendung von Eine Wrapper-Bibliothek, z. B. idb-keyval können Sie IndexedDB wie einen klassischen Schlüssel/Wert-Speicher behandeln.

Beispiel:

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

Speicher als dauerhaft markieren

navigator.storage.persist() anrufen am Ende einer dieser Caching-Methoden, um die Berechtigung zum Verwenden nichtflüchtigem Speicher. Diese Methode gibt ein Promise zurück, das in true aufgelöst wird, wenn Berechtigung wird gewährt, ansonsten false. Der Browser dem Antrag stattgegeben wird, abhängig von browserspezifischen Regeln.

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

Sonderfall: Modell auf einer Festplatte verwenden

Alternativ können Sie KI-Modelle direkt auf der Festplatte eines Nutzers referenzieren auf den Browserspeicher. Mithilfe dieser Technik können forschungsorientierte Apps die Möglichkeit, bestimmte Modelle im Browser auszuführen oder es Künstlern zu ermöglichen, selbsttrainierte Modelle in kreativen Apps nutzen können.

File System Access API

Mit der File System Access API können Sie Dateien von der Festplatte öffnen FileSystemFileHandle die Sie in IndexedDB beibehalten können.

Bei diesem Muster muss der Nutzer nur Zugriff auf die Modelldatei gewähren. einmal. Dank der dauerhaften Berechtigungen kann der Nutzer dauerhaft Zugriff auf die Datei gewähren. Nach dem erneuten Laden der eine erforderliche Nutzergeste wie ein Mausklick, die FileSystemFileHandle kann mit Zugriff auf die Datei aus IndexedDB wiederhergestellt werden auf der Festplatte.

Die Dateizugriffsberechtigungen werden abgefragt und gegebenenfalls angefordert. bei zukünftigen Neuladevorgängen. Das folgende Beispiel zeigt, wie Sie für eine Datei von der Festplatte und speichern und wiederherstellen.

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

Diese Methoden schließen sich nicht gegenseitig aus. Es kann vorkommen, dass Sie beide ein Modell explizit im Browser im Cache zu speichern und ein Modell von der Festplatte eines Nutzers zu verwenden.

Demo

Sie sehen alle drei üblichen Methoden zur Fallspeicherung und die Methode „Hard Disk“ die in der MediaPipe LLM-Demo implementiert wurden.

Bonus: Laden Sie eine große Datei in Teilen herunter

Wenn Sie ein großes KI-Modell aus dem Internet herunterladen müssen, parallelisieren Sie die in einzelne Blöcke herunterlädt und dann auf dem Client wieder zusammengefügt.

Hier ist eine Hilfsfunktion, die Sie in Ihrem Code verwenden können. Sie müssen nur es url. Der chunkSize (Standard: 5 MB), der maxParallelRequests (Standardeinstellung: 6) ist die Funktion progressCallback, die das Ereignis downloadedBytes und die gesamten fileSize) sowie die signal für einen Das AbortSignal-Signal ist optional.

Sie können die folgende Funktion in Ihr Projekt kopieren oder Installieren Sie das fetch-in-chunks-Paket aus dem npm-Paket.

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;

Die richtige Methode für Sie

In diesem Leitfaden wurden verschiedene Methoden zum effektiven Caching von KI-Modellen in der Browser, eine Aufgabe, die entscheidend ist, um die Nutzererfahrung mit die Leistung Ihrer App zu verbessern. Das Chrome Storage-Team empfiehlt die Cache API für optimale Leistung, um schnellen Zugriff auf KI-Modelle zu ermöglichen und Ladezeiten zu verkürzen und Verbesserung der Reaktionsfähigkeit.

OPFS und IndexedDB sind weniger brauchbare Optionen. Die OPFS und die IndexedDB APIs die Daten serialisieren, bevor sie gespeichert werden können. IndexedDB muss außerdem Daten beim Abrufen deserialisieren, was sie zum schlechtesten Speicherort macht große Modelle.

Für Nischenanwendungen bietet die File System Access API direkten Zugriff auf Dateien. auf dem Gerät eines Nutzers. Dies ist ideal für Nutzer, die ihre eigenen KI-Modelle verwalten.

Wenn Sie Ihr KI-Modell absichern müssen, lassen Sie es auf dem Server. Nach der Speicherung im ist es einfach, die Daten sowohl aus dem Cache als auch aus der IndexedDB mit Entwicklertools oder die OFPS DevTools-Erweiterung. Die Sicherheit dieser Speicher-APIs ist grundsätzlich gleich. Sie könnten versucht sein, eine verschlüsselte Version des Modells gespeichert werden, an den Client zu senden, der abgefangen werden könnte. Das bedeutet, dass der Versuch eines böswilligen Akteurs Ihr Modell zu stehlen, ist etwas schwieriger, aber nicht unmöglich.

Wir empfehlen Ihnen, eine Caching-Strategie zu wählen, die zu den Anforderungen, Zielgruppenverhalten und Merkmale der KI-Modelle verwendet. Dadurch wird sichergestellt, dass Ihre Anwendungen unter verschiedenen Bedingungen reaktionsschnell und robust sind. Netzwerkbedingungen und Systemeinschränkungen.


Danksagungen

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