在浏览器中缓存 AI 模型

大多数 AI 模型都有一个共同点: 相当大 通过互联网传输的内容最小的 MediaPipe 对象检测模型 (SSD MobileNetV2 float16) 为 5.6 MB 最大大小约为 25MB

开源 LLM gemma-2b-it-gpu-int4.bin 时钟为 1.35 GB,这在 LLM 中被认为非常小。 生成式 AI 模型可能非常庞大。正因如此,AI 才得到了广泛应用 存储于云端。越来越多的应用直接运行高度优化的模型 。而在浏览器中运行的 LLM 演示 这里有一些其他模型的生产级示例, 浏览器:

打开 AI 赋能的对象选择工具的网页版 Adobe Photoshop,其中已选择三个对象:两只长颈鹿和一颗月亮。

为了让您的应用以后更快地启动,您应该明确缓存 将模型数据存储在设备端,而不是依赖于隐式 HTTP 浏览器 缓存。

虽然本指南使用 gemma-2b-it-gpu-int4.bin model 来创建聊天机器人, 这种方法可以进行泛化,以适应其他模型和其他应用场景 。将应用与模型相关联的最常见方法是提供 与其余应用资源搭配使用。因此,优化 。

配置合适的缓存标头

如果您从服务器提供 AI 模型,请务必配置正确的 Cache-Control 标头。以下示例展示了可靠的默认设置 以满足应用需求

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

AI 模型的每个发布版本都是静态资源。从不 应该为代码 max-age缓存无效化功能结合使用 。如果确实需要更新模型, 为其提供一个新网址

当用户重新加载页面时,客户端会发送重新验证请求,即使 即使服务器知道内容是稳定的,通过 immutable 指令明确指明没有必要进行重新验证,因为 内容不会改变immutable 指令为 未得到广泛支持 浏览器和中间缓存或代理服务器 将它与 max-age 指令,可以确保 兼容性。public response 指令指示响应可存储在共享缓存中。

<ph type="x-smartling-placeholder">
</ph>
Chrome 开发者工具显示生产 Cache-Control Hugging Face 在请求 AI 模型时发送的标头。 (来源

在客户端缓存 AI 模型

在提供 AI 模型时,务必要将模型明确缓存在 。这可确保模型数据在用户重新加载后随时可用 应用。

您可以使用许多技术来实现这一目标。对于 示例代码,假设每个模型文件都存储在 名为 blobBlob 对象 内存中。

为了解性能,每个代码示例都带有 performance.mark()performance.measure() 方法。这些措施取决于设备,无法泛化。

<ph type="x-smartling-placeholder">
</ph>
在 Chrome 开发者工具中,点击 Application >存储空间、查看 包含 IndexedDB、Cache 存储空间和文件系统的细分的用法图。 数据显示,每个细分受众群消耗了 1354 MB 的数据,总计 4063 MB。

您可以选择使用以下 API 之一在浏览器中缓存 AI 模型: Cache API Origin Private File System APIIndexedDB API一般建议使用 Cache API,但是本指南讨论的是使用 API 的 所有选项

Cache API

Cache API 提供 Request 的永久性存储空间 和 Response 对象 缓存数据对虽然 Service Workers 规范中定义的 您可以从主线程或常规工作器使用此 API。适合在户外使用 Service Worker 上下文,调用 Cache.put() 方法 一个合成的 Response 对象,它与合成网址(而不是 Request 对象。

本指南假定使用内存中 blob。使用虚构网址作为缓存键,并使用 基于 blob 的合成 Response。如果您要直接下载 模型,则可以使用通过创建 fetch() 获得的 Response 请求。

例如,下面介绍如何使用 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

源私有文件系统 (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 的 promise 表示已授予权限,否则返回 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);
  }
}

特殊情况:使用硬盘上的模型

作为替代方案,您可以直接从用户的硬盘中引用 AI 模型 浏览器存储空间这种分析法有助于以研究为重点的应用展示 可以在浏览器中运行指定模型, 自行训练模型。

File System Access API

借助 File System Access API, 您可以打开硬盘上的文件, FileSystemFileHandle 您可以将其存储在 IndexedDB 中。

使用此模式,用户只需授予对模型文件的访问权限 一次。多亏了持久权限, 用户可以选择永久授予对文件的访问权限。重新加载 以及所需的用户手势(例如点击鼠标)时, 可通过有权访问该文件的 IndexedDB 恢复 FileSystemFileHandle 硬盘上。

必要时查询和请求文件访问权限, 以便以后重新加载以下示例展示了如何获取 处理文件,然后存储并恢复该句柄。

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 演示中实现的方法。

额外提示:分块下载大型文件

如果您需要从互联网下载大型 AI 模型, 分成几个单独的视频块,在客户端上重新拼接在一起

以下是可以在代码中使用的辅助函数。您只需通过 为 urlchunkSize(默认值为 5MB)、maxParallelRequests (默认值:6)、progressCallback 函数(用于报告 downloadedBytes 和总 fileSize),而 signal AbortSignal 信号都是可选的。

您可以在项目中复制以下函数,也可以 从 npm 软件包中安装 fetch-in-chunks 软件包

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;

选择适合您的方法

本指南探讨了在 Google Cloud 控制台中有效缓存 AI 模型的各种方法, 这是一项至关重要的任务,对于改善用户使用浏览器和 应用性能Chrome 存储团队建议将 Cache API 用于 优化性能,确保快速访问 AI 模型,缩短加载时间 和提高响应速度

OPFS 和 IndexedDB 用处不大。OPFS 和 IndexedDB API 需要先对数据进行序列化,然后才能存储数据。IndexedDB 还需要 在检索数据时对数据进行反序列化,使其成为最糟糕的存储位置 大型语言模型。

对于小众应用,File System Access API 可让用户直接访问文件 ,非常适合自行管理 AI 模型的用户。

如果您需要保护 AI 模型,请将其保留在服务器上。存储在 使用 API 从 Cache 和 IndexedDB 提取数据非常简单 开发者工具或 OFPS DevTools 扩展程序。 这些存储 API 在安全性方面本质上是相等的。您或许会想 存储模型的加密版本,但之后需要从 Cloud Storage 中获取 密钥传送给客户端,可能会被拦截。这意味着有不良企图 窃取模型的难度略大一些,但也不是不可能的。

我们建议您选择与应用缓存内容相符的缓存策略。 目标受众群体行为以及 AI 模型的特征 。这可确保您的应用在各种情况下都能迅速响应且稳健可靠 网络条件和系统限制。


致谢

此内容由 Joshua Bell、Reilly Grant、Evan Stade、Nathan Memmott、 Austin Sullivan、Etienne Noël、André Bandarra、Alexandra Klepper、 François Beaufort、Paul Kinlan 和 Rachel Andrew。