Большинство моделей ИИ имеют одну общую черту: они довольно велики для ресурса, который передается через Интернет. Самая маленькая модель обнаружения объектов MediaPipe ( SSD MobileNetV2 float16
) весит 5,6 МБ, а самая большая — около 25 МБ.
Открытый исходный код LLM gemma-2b-it-gpu-int4.bin
занимает 1,35 ГБ, и это считается очень малым для LLM. Генеративные модели ИИ могут быть огромными. Вот почему сегодня ИИ в основном используется в облаке. Все чаще приложения запускают высокооптимизированные модели непосредственно на устройстве. Хотя существуют демонстрационные версии LLM, работающих в браузере , вот несколько примеров других моделей, работающих в браузере, в производственном классе:
- Adobe Photoshop запускает вариант модели
Conv2D
на устройстве для своего интеллектуального инструмента выбора объектов. - Google Meet использует оптимизированную версию модели
MobileNetV3-small
для сегментации людей с помощью функции размытия фона. - Tokopedia использует модель
MediaPipeFaceDetector-TFJS
для распознавания лиц в реальном времени, чтобы предотвратить недействительные регистрации на своем сервисе. - Google Colab позволяет пользователям использовать модели со своего жесткого диска в блокнотах Colab.
Чтобы ускорить будущие запуски ваших приложений, вам следует явно кэшировать данные модели на устройстве, а не полагаться на неявный HTTP-кеш браузера.
Хотя в этом руководстве для создания чат-бота используется gemma-2b-it-gpu-int4.bin model
, этот подход можно обобщить для других моделей и других вариантов использования на устройстве. Наиболее распространенный способ подключения приложения к модели — обслуживание модели вместе с остальными ресурсами приложения. Крайне важно оптимизировать доставку.
Настройте правильные заголовки кэша
Если вы обслуживаете модели ИИ со своего сервера, важно настроить правильный заголовок Cache-Control
. В следующем примере показана надежная настройка по умолчанию, которую вы можете использовать для нужд своего приложения.
Cache-Control: public, max-age=31536000, immutable
Каждая выпущенная версия модели ИИ является статическим ресурсом. Содержимому, которое никогда не меняется, следует присвоить длинный max-age
в сочетании с очисткой кэша в URL-адресе запроса. Если вам нужно обновить модель, вы должны присвоить ей новый URL-адрес .
Когда пользователь перезагружает страницу, клиент отправляет запрос на повторную проверку, хотя сервер знает, что содержимое стабильно. Директива immutable
явно указывает на то, что повторная проверка не нужна, поскольку содержимое не изменится. Директива immutable
не поддерживается широко браузерами и промежуточными кэш-серверами или прокси-серверами, но, объединив ее с общеизвестной директивой max-age
, можно обеспечить максимальную совместимость. Директива public
response указывает на то, что ответ может быть сохранен в общем кэше.
Cache-Control
отправленные Hugging Face при запросе модели ИИ. ( Источник ) Кэширование моделей ИИ на стороне клиента
При обслуживании модели ИИ важно явно кэшировать модель в браузере. Это гарантирует, что данные модели будут легко доступны после того, как пользователь перезагрузит приложение.
Есть несколько методов, которые вы можете использовать для достижения этого. Для следующих примеров кода предположим, что каждый файл модели хранится в объекте Blob
с именем blob
в памяти.
Для понимания производительности каждый пример кода аннотируется методами performance.mark()
и performance.measure()
. Эти меры зависят от устройства и не подлежат обобщению.
Вы можете использовать один из следующих API для кэширования моделей ИИ в браузере: Cache API , Origin Private File System API и IndexedDB API . Общая рекомендация — использовать Cache API , но в этом руководстве обсуждаются преимущества и недостатки всех вариантов.
API кэширования
API кэша обеспечивает постоянное хранилище для пар объектов Request
и Response
, которые кэшируются в долгоживущей памяти. Хотя он определен в спецификации Service Workers , вы можете использовать этот API из основного потока или обычного worker. Чтобы использовать его вне контекста service worker, вызовите метод Cache.put()
с синтетическим объектом Response
, сопряженным с синтетическим URL вместо объекта Request
.
В этом руководстве предполагается наличие blob
в памяти. Используйте поддельный URL в качестве ключа кэша и синтетический Response
на основе blob
. Если бы вы напрямую загружали модель, вы бы использовали Response
получили бы при выполнении запроса fetch()
.
Например, вот как сохранить и восстановить файл модели с помощью 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;
}
};
API частной файловой системы Origin
Origin Private File System (OPFS) — сравнительно молодой стандарт для конечной точки хранения. Он закрыт для источника страницы и, таким образом, невидим для пользователя, в отличие от обычной файловой системы. Он предоставляет доступ к специальному файлу, который высоко оптимизирован для производительности, и предлагает доступ на запись к его содержимому.
Например, вот как сохранить и восстановить файл модели в 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 индексированной базы данных
IndexedDB — это устоявшийся стандарт для хранения произвольных данных в постоянной манере в браузере. Он печально известен своим довольно сложным API, но с помощью библиотеки-обертки, например idb-keyval, вы можете обращаться с IndexedDB как с классическим хранилищем ключей и значений.
Например:
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;
}
};
Отметить хранилище как сохраненное
Вызовите navigator.storage.persist()
в конце любого из этих методов кэширования, чтобы запросить разрешение на использование постоянного хранилища. Этот метод возвращает обещание, которое разрешается как true
если разрешение предоставлено, и false
в противном случае. Браузер может выполнить или не выполнить запрос в зависимости от правил, специфичных для браузера.
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);
}
}
Особый случай: использование модели на жестком диске.
Вы можете ссылаться на модели ИИ непосредственно с жесткого диска пользователя в качестве альтернативы хранилищу браузера. Этот метод может помочь приложениям, ориентированным на исследования, продемонстрировать осуществимость запуска данных моделей в браузере или позволить художникам использовать самообучающиеся модели в приложениях для профессионального творчества.
API доступа к файловой системе
С помощью API доступа к файловой системе вы можете открывать файлы на жестком диске и получать FileSystemFileHandle , который можно сохранить в IndexedDB.
С помощью этого шаблона пользователю нужно предоставить доступ к файлу модели только один раз. Благодаря постоянным разрешениям пользователь может выбрать постоянное предоставление доступа к файлу. После перезагрузки приложения и необходимого жеста пользователя, например щелчка мыши, FileSystemFileHandle
может быть восстановлен из IndexedDB с доступом к файлу на жестком диске.
Разрешения на доступ к файлу запрашиваются и запрашиваются при необходимости, что делает это бесшовным для будущих перезагрузок. Следующий пример показывает, как получить дескриптор файла с жесткого диска, а затем сохранить и восстановить дескриптор.
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;
}
};
Эти методы не являются взаимоисключающими. Может быть случай, когда вы и явно кэшируете модель в браузере, и используете модель с жесткого диска пользователя.
Демо
Все три обычных метода хранения кейсов и метод жесткого диска, реализованные в демонстрации MediaPipe LLM, можно увидеть.
Бонус: загрузка большого файла по частям
Если вам необходимо загрузить большую модель ИИ из Интернета, распараллеливайте загрузку на отдельные фрагменты, а затем снова сшейте их на клиенте.
Вот вспомогательная функция, которую вы можете использовать в своем коде. Вам нужно только передать ей url
. chunkSize
(по умолчанию: 5 МБ), maxParallelRequests
(по умолчанию: 6), функция progressCallback
(которая сообщает о downloadedBytes
и общем fileSize
) и signal
для сигнала AbortSignal
являются необязательными.
Вы можете скопировать следующую функцию в свой проект или установить пакет fetch-in-chunks
из пакета 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;
Выберите подходящий для вас метод
В этом руководстве были рассмотрены различные методы эффективного кэширования моделей ИИ в браузере, что является важной задачей для улучшения пользовательского опыта и производительности вашего приложения. Команда хранилища Chrome рекомендует API Cache для оптимальной производительности, чтобы обеспечить быстрый доступ к моделям ИИ, сократить время загрузки и улучшить отзывчивость.
OPFS и IndexedDB — менее удобные варианты. API OPFS и IndexedDB должны сериализовать данные перед тем, как их можно будет сохранить. IndexedDB также должен десериализовать данные при их извлечении, что делает его худшим местом для хранения больших моделей.
Для узкоспециализированных приложений API доступа к файловой системе обеспечивает прямой доступ к файлам на устройстве пользователя, что идеально подходит для пользователей, управляющих собственными моделями ИИ.
Если вам нужно защитить свою модель ИИ, храните ее на сервере. После сохранения на клиенте извлечь данные из кэша и IndexedDB с помощью DevTools или расширения OFPS DevTools не составит труда. Эти API-интерфейсы хранения по своей сути одинаковы по уровню безопасности. У вас может возникнуть соблазн сохранить зашифрованную версию модели, но тогда вам нужно будет передать клиенту ключ дешифрования, который может быть перехвачен. Это означает, что попытка злоумышленника украсть вашу модель немного сложнее, но не невозможна.
Мы рекомендуем вам выбрать стратегию кэширования, которая соответствует требованиям вашего приложения, поведению целевой аудитории и характеристикам используемых моделей ИИ. Это гарантирует, что ваши приложения будут отзывчивыми и надежными в различных сетевых условиях и системных ограничениях.
Благодарности
Его рецензировали Джошуа Белл, Рейли Грант, Эван Стэйд, Натан Меммотт, Остин Салливан, Этьен Ноэль, Андре Бандарра, Александра Клеппер, Франсуа Бофорт, Пол Кинлан и Рэйчел Эндрю.