Кэшируйте модели ИИ в браузере

У большинства моделей ИИ есть одна общая черта: они довольно велики для ресурса, передаваемого через Интернет. Самая маленькая модель обнаружения объектов MediaPipe ( SSD MobileNetV2 float16 ) весит 5,6 МБ, а самая большая — около 25 МБ.

LLM с открытым исходным кодом gemma-2b-it-gpu-int4.bin имеет тактовую частоту 1,35 ГБ — и это считается очень маленьким для LLM. Генеративные модели ИИ могут быть огромными. Вот почему сегодня большая часть использования ИИ происходит в облаке. Все чаще приложения запускают высокооптимизированные модели непосредственно на устройстве. Хотя существуют демоверсии LLM, работающие в браузере , вот несколько примеров других моделей, работающих в браузере:

Adobe Photoshop в Интернете с открытым инструментом выбора объектов на базе искусственного интеллекта и выбранными тремя объектами: двумя жирафами и луной.

Чтобы ускорить запуск ваших приложений в будущем, вам следует явно кэшировать данные модели на устройстве, а не полагаться на неявный кэш 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 ответа указывает, что ответ может быть сохранен в общем кеше.

Chrome DevTools отображает рабочие заголовки Cache-Control отправленные Hugging Face при запросе модели ИИ. ( Источник )

Кэширование моделей искусственного интеллекта на стороне клиента

Когда вы обслуживаете модель ИИ, важно явно кэшировать модель в браузере. Это гарантирует, что данные модели будут легко доступны после перезагрузки приложения пользователем.

Для достижения этой цели можно использовать ряд техник. В следующих примерах кода предполагается, что каждый файл модели хранится в Blob объекте с именем blob в памяти.

Чтобы понять производительность, каждый пример кода аннотируется методами performance.mark() и performance.measure() . Эти меры зависят от устройства и не подлежат обобщению.

В разделе «Приложение Chrome DevTools» > «Хранилище » просмотрите диаграмму использования с сегментами для IndexedDB, хранилища кэша и файловой системы. Показано, что каждый сегмент потребляет 1354 мегабайта данных, что в сумме составляет 4063 мегабайта.

Вы можете использовать один из следующих API для кэширования моделей ИИ в браузере: Cache API , API частной файловой системы Origin и API IndexedDB . Общая рекомендация — использовать Cache API , но в этом руководстве обсуждаются преимущества и недостатки всех вариантов.

API кэша

API Cache обеспечивает постоянное хранилище для пар объектов Request и Response , которые кэшируются в долговременной памяти. Хотя он определен в спецификации Service Workers , вы можете использовать этот API из основного потока или обычного работника. Чтобы использовать его вне контекста сервисного работника, вызовите метод Cache.put() с синтетическим объектом Response в сочетании с синтетическим URL-адресом вместо объекта Request .

В этом руководстве предполагается, что blob находится в памяти. Используйте поддельный URL-адрес в качестве ключа кэша и синтетический Response на основе blob . Если бы вы напрямую загрузили модель, вы бы использовали Response который получили бы при выполнении запроса fetch() .

Например, вот как сохранить и восстановить файл модели с помощью 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;
  }
};

API частной файловой системы Origin

Частная файловая система Origin (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, но с помощью библиотеки-оболочки l, такой как 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 рекомендует Cache API для оптимальной производительности, обеспечения быстрого доступа к моделям искусственного интеллекта, сокращения времени загрузки и повышения скорости реагирования.

OPFS и IndexedDB — менее удобные варианты. API-интерфейсам OPFS и IndexedDB необходимо сериализовать данные, прежде чем их можно будет сохранить. IndexedDB также необходимо десериализовать данные при их получении, что делает ее худшим местом для хранения больших моделей.

Для нишевых приложений API доступа к файловой системе предлагает прямой доступ к файлам на устройстве пользователя, что идеально подходит для пользователей, которые управляют своими собственными моделями ИИ.

Если вам нужно защитить свою модель ИИ, храните ее на сервере. После сохранения на клиенте данные легко извлечь как из кэша, так и из IndexedDB с помощью DevTools или расширения OFPS DevTools . Эти API хранилища по своей сути одинаковы по безопасности. У вас может возникнуть соблазн сохранить зашифрованную версию модели, но затем вам потребуется передать клиенту ключ дешифрования, который может быть перехвачен. Это означает, что попытка злоумышленника украсть вашу модель немного сложнее, но не невозможна.

Мы рекомендуем вам выбрать стратегию кэширования, соответствующую требованиям вашего приложения, поведению целевой аудитории и характеристикам используемых моделей искусственного интеллекта. Это гарантирует, что ваши приложения будут отзывчивыми и надежными в различных сетевых условиях и системных ограничениях.


Благодарности

Его рецензировали Джошуа Белл, Рейли Грант, Эван Стэйд, Натан Меммотт, Остин Салливан, Этьен Ноэль, Андре Бандарра, Александра Клеппер, Франсуа Бофорт, Пол Кинлан и Рэйчел Эндрю.