Buforowanie modeli AI w przeglądarce

Większość modeli AI ma co najmniej 1 wspólną cechę: są dość dużych w przypadku zasobów, przesyłanych przez internet. Najmniejszy model wykrywania obiektów MediaPipe (SSD MobileNetV2 float16) waży 5,6 MB a największy ma około 25 MB.

LLM typu open source gemma-2b-it-gpu-int4.bin i 1,35 GB, co w przypadku LLM jest uważane za bardzo mały. Modele generatywnej AI mogą być ogromne. Właśnie dlatego AI jest dziś wykorzystywana w dużym stopniu. w chmurze. Coraz częściej aplikacje bezpośrednio korzystają z wysoce zoptymalizowanych modeli na urządzeniu. Wersje demonstracyjne LLM działające w przeglądarce oto kilka przykładów klasy produkcyjnej innych modeli działających w przeglądarka:

Aplikacja Adobe Photoshop w przeglądarce z otwartym narzędziem do wybierania obiektów opartym na AI i zaznaczonymi 3 obiektami: dwoma żyrafami i księżycem.

Aby przyspieszyć uruchamianie aplikacji w przyszłości, należy jawnie przechowywać w pamięci podręcznej danych modelu na urządzeniu zamiast polegać na niejawnej przeglądarce HTTP pamięci podręcznej.

W tym przewodniku opisujemy gemma-2b-it-gpu-int4.bin model przy tworzeniu czatbota, Podejście można uogólnić, aby dopasować je do innych modeli i innych przypadków użycia na urządzeniu. Najczęstszym sposobem łączenia aplikacji z modelem jest przesyłanie wraz z pozostałymi zasobami aplikacji. Niezwykle ważne jest, .

Skonfiguruj odpowiednie nagłówki pamięci podręcznej

Jeśli udostępniasz modele AI z serwera, ważne jest skonfigurowanie Cache-Control nagłówek. Poniższy przykład pokazuje solidne ustawienie domyślne, które można utworzyć dostosowane do potrzeb Twojej aplikacji.

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

Każda opublikowana wersja modelu AI jest zasobem statycznym. Treści, które nigdy zmiany powinny mieć duży max-age w połączeniu z pomijaniem pamięci podręcznej w adresie URL żądania. Jeśli jest konieczna aktualizacja modelu, podaj nowy adres URL.

Gdy użytkownik odświeży stronę, klient wysyła żądanie ponownej weryfikacji, nawet chociaż na serwerze wie, że jej zawartość jest stabilna. immutable wyraźnie wskazuje, że ponowna weryfikacja jest niepotrzebna, ponieważ treści się nie zmienią. Dyrektywa immutable jest nie jest powszechnie obsługiwana przez przeglądarki i pośrednią pamięć podręczną lub serwery proxy, ale przez łącząc go z zrozumiałej dyrektywy max-age, możesz zapewnić maksimum zgodność. public dyrektywa response wskazuje, że odpowiedź można zapisać we wspólnej pamięci podręcznej.

Narzędzia deweloperskie w Chrome wyświetlają wersję produkcyjną Cache-Control nagłówki wysłane przez Hugging Face podczas żądania modelu AI. (Źródło)

Buforowanie modeli AI po stronie klienta

Gdy udostępniasz model AI, pamiętaj, aby jawnie zapisać go w pamięci podręcznej w przeglądarki. Dzięki temu dane modelu będą łatwo dostępne po ponownym załadowaniu użytkownika. aplikację.

Istnieje kilka metod, które można wykorzystać, aby to osiągnąć. Dla następujących w przykładach z przykładowym kodem, załóżmy, że każdy plik modelu jest przechowywany w Blob obiekt o nazwie blob w pamięci.

Aby ułatwić zrozumienie wydajności, do każdego przykładowego kodu dodano adnotację performance.mark() oraz performance.measure() . Te wskaźniki zależą od urządzenia i nie można ich uogólnić.

W aplikacji w Narzędziach deweloperskich w Chrome > Miejsce na dane, sprawdź schemat użycia z segmentami IndexedDB, Cache Storage i System plików. Widać, że każdy segment zużywa 1354 MB danych, co daje łącznie 4063 i rozmiarze pamięci masowej.

Do buforowania modeli AI w przeglądarce możesz użyć jednego z tych interfejsów API: Cache API, Origin Private File System API oraz Interfejs API IndexedDB: Zaleca się stosowanie Cache API, ale w tym przewodniku omawiamy zalety i wady wszystkie opcje.

Interfejs API pamięci podręcznej

Interfejs Cache API udostępnia pamięć trwała dla Request i obiekt Response pary przechowywane w pamięci długotrwałej. Chociaż jest to zdefiniowane w specyfikacji mechanizmów Service Workers, tego interfejsu API możesz używać w wątku głównym lub w zwykłej instancji roboczej. Do używania na zewnątrz kontekstu mechanizmu Service Worker, wywołaj metodę Metoda Cache.put() z syntetycznym obiektem Response sparowanym z syntetycznym adresem URL zamiast Request obiekt.

W tym przewodniku przyjęto założenie blob w pamięci. Użyj fałszywego adresu URL jako klucza pamięci podręcznej syntetyczny Response na podstawie blob. Gdyby pobrać plik bezpośrednio model, należy użyć funkcji Response, którą można uzyskać po utworzeniu fetch() użytkownika.

Poniżej znajdziesz na przykład informacje o tym, jak zapisać i przywrócić plik modelu za pomocą interfejsu 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;
  }
};

Interfejs Origin Private File System API

Origin Private File System (OPFS) to dość młody standard punktu końcowego pamięci masowej. Obiekt jest prywatny dla pochodzenia strony, więc jest niewidoczny w odróżnieniu od zwykłego systemu plików. Zapewnia dostęp do specjalnych funkcji który jest wysoce zoptymalizowany pod kątem wydajności i zapewnia dostęp do treści.

Poniżej znajdziesz na przykład informacje o tym, jak zapisać i przywrócić plik modelu w pliku 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;
  }
};

Interfejs API IndexedDB

IndexedDB to uznany standard przechowywania dowolnych danych w trwały sposób. w przeglądarce. Znana jest ze złożoności interfejsu API, biblioteka opakowań, np. idb-keyval możesz traktować IndexedDB jak klasyczny magazyn par klucz-wartość.

Na przykład:

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

Oznacz miejsce na dane jako przechowywane

Zadzwoń pod numer navigator.storage.persist() na końcu dowolnej z tych metod buforowania, aby poprosić o zgodę na użycie pamięci trwałej. Ta metoda zwraca obietnicę, która zwraca wartość true, jeśli zostanie przyznane uprawnienie. W przeciwnym razie false. Przeglądarka może zaakceptować prośbę, w zależności od reguł właściwych dla danej przeglądarki.

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

Przypadek specjalny: użycie modelu na dysku twardym

Możesz odwoływać się do modeli AI bezpośrednio z dysku twardego użytkownika do pamięci przeglądarki. Dzięki tej technice aplikacje badawcze mogą prezentować możliwość uruchomienia określonych modeli w przeglądarce lub umożliwienia wykonawcom użycia dzięki którym można samodzielnie wytrenować model w profesjonalnych aplikacjach pobudzających kreatywność.

File System Access API

Za pomocą interfejsu File System Access API można otwierać pliki zapisane na dysku twardym i uzyskać FileSystemFileHandle który można zachować w IndexedDB.

W przypadku tego wzorca użytkownik musi tylko przyznać dostęp do pliku modelu raz. Dzięki trwałym uprawnieniom, użytkownik może trwale przyznać dostęp do pliku. Po ponownym załadowaniu aplikacji i wymagany gest użytkownika, taki jak kliknięcie myszy, FileSystemFileHandle można przywrócić z IndexedDB z dostępem do pliku na dysku twardym.

Zapytania o uprawnienia dostępu do plików są wysyłane w razie potrzeby i w razie potrzeby wymagane, co sprawia, bez problemów przy doładowywaniu w przyszłości. Ten przykład pokazuje, jak uzyskać dla pliku z dysku twardego, a następnie przechowywać i przywracać uchwyt.

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

Te metody nie wykluczają się wzajemnie. Może się zdarzyć, że oboje bezpośrednio buforować model w przeglądarce i używać go z dysku twardego użytkownika.

Prezentacja

Możesz zapoznać się ze wszystkimi 3 zwykłymi metodami przechowywania zgłoszeń oraz metodą dysku twardego zaimplementowane w wersji demonstracyjnej LLM MediaPipe.

Bonus: pobieraj duże pliki, partiami

Jeśli musisz pobrać duży model AI z internetu, pobrać do osobnych fragmentów, a następnie połączyć ponownie w kliencie.

Oto funkcja pomocnicza, której możesz użyć w kodzie. Wystarczy zdać url. chunkSize (domyślnie: 5 MB), maxParallelRequests (domyślnie: 6), funkcja progressCallback (która raportuje downloadedBytes i łączna kwota fileSize) oraz signal w przypadku Sygnał AbortSignal jest opcjonalny.

Możesz skopiować poniższą funkcję w swoim projekcie lub zainstaluj pakiet fetch-in-chunks z pakietu 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;

Wybierz metodę odpowiednią dla siebie

W tym przewodniku przedstawiamy różne metody skutecznego buforowania modeli AI w przez przeglądarkę – to zadanie kluczowe dla zwiększania wygody użytkowników i wydajności aplikacji. Zespół Chrome ds. miejsca na dane zaleca interfejs Cache API dla optymalną wydajność, która zapewnia szybki dostęp do modeli AI, co skraca czas wczytywania i zwiększaniem czasu reagowania.

Opcje OPFS i IndexedDB są mniej przydatne. Interfejsy API OPFS i IndexedDB musisz zserializować dane, zanim będzie można je zapisać. IndexedDB wymaga też deserializacja danych podczas ich pobierania, co czyni je najgorszym miejscem do ich przechowywania dużych modeli.

Dla niszowych aplikacji interfejs File System Access API zapewnia bezpośredni dostęp do plików na urządzeniu użytkownika, co jest idealnym rozwiązaniem w przypadku użytkowników, którzy zarządzają własnymi modelami AI.

Jeśli chcesz zabezpieczyć swój model AI, zachowaj go na serwerze. Po zapisaniu na wyodrębnienie danych z pamięci podręcznej i IndexedDB jest proste – lub rozszerzenie OFPS DevNarzędzia. Te interfejsy API do przechowywania danych z natury mają jednakowe podejście w zakresie bezpieczeństwa. Może Cię kusić przechowywać zaszyfrowaną wersję modelu, ale później musisz je odszyfrować. klucz do klienta, który może zostać przechwycony. Oznacza to próbę podejrzenia, że nieuczciwy podmiot kradzież modelu jest nieco trudniejsza, ale nie niemożliwa.

Zachęcamy do wybrania strategii buforowania zgodnej z wymagań, zachowań docelowych odbiorców i cech modeli AI . Dzięki temu aplikacje są responsywne i niezawodne w różnych warunki sieciowe i ograniczenia systemu.


Podziękowania

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