Większość modeli AI ma jedną wspólną cechę: są dość duże jak na zasoby przesyłane przez Internet. Najmniejszy model wykrywania obiektów MediaPipe (SSD MobileNetV2 float16
) ma rozmiar 5,6 MB, a największy – około 25 MB.
LLM typu open source
gemma-2b-it-gpu-int4.bin
zajmuje 1,35 GB – co jest uważane za bardzo mało w przypadku LLM.
Modele generatywnej AI mogą być ogromne. Dlatego obecnie wiele zastosowań AI działa w chmurze. Coraz częściej aplikacje korzystają z bardzo zoptymalizowanych modeli bezpośrednio na urządzeniu. Chociaż istnieją demo modeli LLM działających w przeglądarce, poniżej znajdziesz przykłady innych modeli w wersji produkcyjnej działających w przeglądarce:
- Adobe Photoshop uruchamia na urządzeniu wariant modelu
Conv2D
, aby korzystać z inteligentnego narzędzia do zaznaczania obiektów. - Google Meet uruchamia zoptymalizowaną wersję modelu
MobileNetV3-small
do podziału obrazu na osoby na potrzeby funkcji rozmywania tła. - Tokopedia wykorzystuje model
MediaPipeFaceDetector-TFJS
do wykrywania twarzy w czasie rzeczywistym, aby zapobiec nieprawidłowej rejestracji w usłudze. - Google Colab umożliwia użytkownikom korzystanie z modeli z ich dysku twardego w notatnikach Colab.
Aby przyspieszyć przyszłe uruchamianie aplikacji, musisz jawnie zapisać dane modelu w pamięci podręcznej na urządzeniu, zamiast polegać na domyślnej pamięci podręcznej przeglądarki HTTP.
Ten przewodnik wykorzystuje gemma-2b-it-gpu-int4.bin model
do tworzenia chatbota, ale podejście można uogólnić, aby pasowało do innych modeli i innych zastosowań na urządzeniu. Najczęstszym sposobem łączenia aplikacji z modelem jest wyświetlanie modelu wraz z pozostałymi zasobami aplikacji. Ważne jest, aby zoptymalizować dostarczanie.
Skonfiguruj odpowiednie nagłówki pamięci podręcznej
Jeśli modele AI są dostarczane z serwera, musisz skonfigurować odpowiedni nagłówek Cache-Control
. Poniższy przykład pokazuje solidne ustawienie domyślne, które możesz dostosować do potrzeb swojej aplikacji.
Cache-Control: public, max-age=31536000, immutable
Każda opublikowana wersja modelu AI jest zasobem statycznym. Treści, które nigdy się nie zmieniają, powinny mieć długie max-age
w połączeniu z czyszczeniem pamięci podręcznej w adresie URL żądania. Jeśli chcesz zaktualizować model, musisz podać jego nowy adres URL.
Gdy użytkownik ponownie wczyta stronę, klient wysyła żądanie ponownej weryfikacji, mimo że serwer wie, że zawartość jest stabilna. Dyrektywa immutable
wyraźnie wskazuje, że ponowna weryfikacja nie jest konieczna, ponieważ zawartość nie ulegnie zmianie. Dyrektywa immutable
nie jest szeroko obsługiwana przez przeglądarki i pośrednie serwery pamięci podręcznej lub serwery proxy, ale można ją połączyć z powszechnie rozumianą dyrektywą max-age
, aby zapewnić maksymalną zgodność. Dyrektywa odpowiedzi public
wskazuje, że odpowiedź może być przechowywana w współdzielonej pamięci podręcznej.
Buforowanie modeli AI po stronie klienta
Podczas obsługi modelu AI ważne jest, aby wyraźnie zapisać go w pamięci podręcznej przeglądarki. Dzięki temu dane modelu są łatwo dostępne po ponownym załadowaniu aplikacji przez użytkownika.
Możesz do tego wykorzystać kilka technik. W przypadku tych przykładów kodu przyjmij, że każdy plik modelu jest przechowywany w pamięci w obiekcie Blob
o nazwie blob
.
Aby ułatwić Ci zrozumienie działania kodu, każdy przykład został opatrzony adnotacjami z metodami performance.mark()
i performance.measure()
. Te środki zależą od urządzenia i nie można ich uogólniać.
Aby przechowywać w pliku pamięci podręcznej modele AI w przeglądarce, możesz użyć jednego z tych interfejsów API: Cache API, interfejsu Origin Private File System API i IndexedDB API. Ogólna rekomendacja to użycie interfejsu Cache API, ale w tym przewodniku omawiamy zalety i wady wszystkich opcji.
Cache API
Interfejs Cache API zapewnia trwałe miejsce na pary obiektów Request
i Response
, które są przechowywane w pamięci długotrwałej. Mimo że jest on zdefiniowany w specyfikacji Service Workers, możesz go używać z głównego wątku lub zwykłego workera. Aby użyć go poza kontekstem service workera, wywołaj metodę Cache.put()
za pomocą syntetycznego obiektu Response
sparowanego z syntetycznym adresem URL zamiast obiektu Request
.
W tym przewodniku przyjęto założenie, że blob
jest przechowywany w pamięci. Użyj fałszywego adresu URL jako klucza pamięci podręcznej i syntetycznego Response
na podstawie blob
. Jeśli chcesz pobrać model bezpośrednio, użyjesz Response
otrzymanego po przesłaniu żądania fetch()
.
Oto na przykład, jak przechowywać i przywracać 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;
}
};
Origin Private File System API
System plików Origin Private File System (OPFS) to stosunkowo nowy standard punktu końcowego pamięci masowej. Jest on prywatny dla źródła strony, a zatem niewidoczny dla użytkownika, w odróżnieniu od zwykłego systemu plików. Zapewnia dostęp do specjalnego pliku, który jest zoptymalizowany pod kątem wydajności i zawiera treści, do których można zapisywać dane.
Poniżej znajdziesz na przykład instrukcje przechowywania i przywracania pliku modelu w 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 IndexedDB API
IndexedDB to dobrze znany standard przechowywania dowolnych danych w trwały sposób w przeglądarce. Jest on znany z trochę skomplikowanego interfejsu API, ale dzięki użyciu biblioteki owijającej, takiej jak idb-keyval, możesz traktować IndexedDB jak klasyczny magazyn 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;
}
};
Oznaczanie miejsca na dane jako trwałego
Na końcu każdej z tych metod buforowania wywołaj funkcję navigator.storage.persist()
, aby poprosić o dostęp do trwałego miejsca na dane. Ta metoda zwraca obietnicę, która jest tłumaczona na true
, jeśli udzielono uprawnień, a w przeciwnym razie na false
. Przeglądarka może lub nie może spełnić prośbę w zależności od jej zasad.
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 szczególny: korzystanie z modelu na dysku twardym
Jako alternatywę dla pamięci przeglądarki możesz odwoływać się do modeli AI bezpośrednio z twardego dysku użytkownika. Ta technika może pomóc aplikacjom skupionym na badaniach w prezentowaniu możliwości uruchomienia określonych modeli w przeglądarce lub umożliwić artystom korzystanie z modeli samouczenia w aplikacji do tworzenia dla ekspertów.
File System Access API
Dzięki interfejsowi File System Access API możesz otwierać pliki z twardego dysku i uzyskiwać FileSystemFileHandle, który możesz zapisać w IndexedDB.
W tym przypadku użytkownik musi tylko raz przyznać dostęp do pliku modelu. Dzięki trwałym uprawnieniom użytkownik może przyznać trwały dostęp do pliku. Po ponownym załadowaniu aplikacji i wykonywaniu przez użytkownika odpowiedniego działania, np. kliknięcia myszką, FileSystemFileHandle
może zostać przywrócony z IndexedDB z dostępem do pliku na dysku twardym.
W razie potrzeby wysyłane są zapytania o uprawnienia dostępu do plików, co pozwala na płynne ponowne wczytywanie. Ten przykład pokazuje, jak uzyskać uchwyt pliku z twardego dysku, a następnie zapisać i przywrócić ten 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 model jest wyraźnie przechowywany w pamięci podręcznej w przeglądarce i używany z twardego dysku użytkownika.
Prezentacja
Wszystkie 3 metody przechowywania w przypadku zwykłych przypadków oraz metoda z twardym dyskiem są zaimplementowane w prezentacji LLM MediaPipe.
Bonus: pobieranie dużego pliku w częściach
Jeśli musisz pobrać duży model AI z Internetu, podziel pobieranie na oddzielne fragmenty, a potem połącz je na kliencie.
Oto funkcja pomocnicza, której możesz użyć w kodze. Wystarczy, że przekażesz url
. Opcjonalne są parametry chunkSize
(domyślnie 5 MB), maxParallelRequests
(domyślnie 6), funkcja progressCallback
(która raportuje wartości downloadedBytes
i łączną wartość fileSize
) oraz parametr signal
dla sygnału AbortSignal
.
Możesz skopiować tę funkcję do swojego projektu lub zainstalować pakiet fetch-in-chunks
z 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 odpowiednią metodę
W tym przewodniku omówiliśmy różne metody skutecznego przechowywania w przeglądarce w pamięci podręcznej modeli AI. Jest to kluczowe zadanie, które pozwala poprawić komfort korzystania z aplikacji i jej wydajność. Zespół ds. pamięci w Chrome zaleca korzystanie z interfejsu Cache API, aby zapewnić szybki dostęp do modeli AI, skrócić czas wczytywania i zwiększyć responsywność.
Opcje OPFS i IndexedDB są mniej przydatne. Przed zapisaniem danych interfejsy OPFS i IndexedDB muszą je zserializować. IndexedDB musi też deserializować dane po ich pobraniu, co czyni go najgorszym miejscem do przechowywania dużych modeli.
W przypadku niszowych aplikacji interfejs File System Access API zapewnia bezpośredni dostęp do plików na urządzeniu użytkownika. Jest to idealne rozwiązanie dla użytkowników, którzy zarządzają własnymi modelami AI.
Jeśli chcesz zabezpieczyć model AI, przechowuj go na serwerze. Po zapisaniu na kliencie dane można łatwo wyodrębnić z pamięci podręcznej i IndexedDB za pomocą DevTools lub rozszerzenia OFPS DevTools. Te interfejsy API do przechowywania danych mają z zasady te same zabezpieczenia. Możesz mieć pokusę przechowywania zaszyfrowanej wersji modelu, ale wtedy musisz przekazać klucz odszyfrowywania klientowi, co może zostać przechwycone. Oznacza to, że próba kradzieży modelu przez nieuczciwego podmiotu jest nieco trudniejsza, ale nie niemożliwa.
Wybierz strategię buforowania dostosowaną do wymagań aplikacji, zachowania docelowych odbiorców i charakterystyki używanych modeli AI. Dzięki temu aplikacje będą responsywne i stabilne w różnych warunkach sieciowych oraz przy różnych ograniczeniach systemowych.
Podziękowania
Tekst został sprawdzony przez 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.