ブラウザに AI モデルをキャッシュに保存する

ほとんどの AI モデルには少なくとも 1 つの共通点があります。それは、 かなり大きい インターネット経由で転送されます。最小の MediaPipe オブジェクト検出モデル (SSD MobileNetV2 float16)のファイルサイズ: 5.6 MB 最大サイズは約 25MB です

オープンソースの LLM gemma-2b-it-gpu-int4.bin 1.35 GB です。これは LLM として非常に小さいと考えられています。 生成 AI モデルは巨大なものになり得ます。今日、AI が活発に使用されているのはそのためです。 説明します。アプリはますます、高度に最適化されたモデルを直接実行する できます。ブラウザでの LLM のデモを実行している間 本番環境レベルの例として、Google Cloud で稼働している browser:

AI によるオブジェクト選択ツールを開いて、2 頭のキリンと 1 つの月の 3 つのオブジェクトを選択している Adobe Photoshop web 版。

今後のアプリケーションの起動を高速化するには、 暗黙の HTTP ブラウザに依存するのではなく、デバイス上のモデルデータを使用します。 作成します。

このガイドでは gemma-2b-it-gpu-int4.bin model を使用して chatbot を作成しますが、 アプローチは、他のモデルや他のユースケースに合わせて一般化できる できます。アプリをモデルに接続する最も一般的な方法は、 アプリの他のリソースと一緒に使用できます。P-MAX キャンペーンを最適化する際は 提供します。

適切なキャッシュ ヘッダーを構成する

サーバーから AI モデルを提供する場合、適切な構成を行うことが重要です。 Cache-Control できます。次の例は安定したデフォルト設定を示しています。この設定は、 有効にできます

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

AI モデルの各リリース バージョンは静的リソースです。絶対にないコンテンツ 変更には長い時間、 max-age キャッシュ無効化の組み合わせ 渡されます。モデルを更新する必要がある場合は、 新しい URL を付与します

ユーザーがページを再読み込みすると、クライアントは再検証リクエストを送信しますが、 ただし、サーバーはコンテンツが安定していると認識しています。「 immutable ディレクティブは、再検証が不要であることを明示しています。 コンテンツは変更されません。immutable ディレクティブは、 幅広くサポートされていない ブラウザと中間キャッシュまたはプロキシ サーバーによる 組み合わせ 広く使用されている max-age ディレクティブがあるため、 サポートしています。public response ディレクティブは、レスポンスを共有キャッシュに保存できることを示します。

<ph type="x-smartling-placeholder">
</ph>
Chrome DevTools に本番環境 Cache-Control が表示されます。 Hugging Face から送信される AI モデルのヘッダー。 (出典)。
をご覧ください。

AI モデルをクライアントサイドでキャッシュに保存する

AI モデルを提供する際は、Google Cloud Storage バケットで できます。これにより、ユーザーが再読み込みした後にモデルデータを簡単に使用できるようになります。 クリックします。

これを達成するために使用できる手法はいくつかあります。対象 各モデルファイルが BigQuery データセットに blob という名前の Blob オブジェクト メモリ内。

パフォーマンスを理解できるように、各コードサンプルには performance.mark() および performance.measure() あります。これらの尺度はデバイスに依存するため、一般化することはできません。

<ph type="x-smartling-placeholder">
</ph>
Chrome DevTools の [アプリケーション] >ストレージ、確認 IndexedDB、キャッシュ ストレージ、ファイル システムのセグメントを示す使用状況図 各セグメントで使用されるデータは 1,354 MB、合計は 4,063 MB です。 MB です。

次のいずれかの API を使用して、ブラウザ内の AI モデルをキャッシュに保存できます。 Cache API を使用すると、 Origin Private File System API IndexedDB API一般的に推奨されるのは、 Cache API を使用しますが、このガイドでは、 オプションです。

キャッシュ API

Cache API は、 Request の永続ストレージ および Response オブジェクト 長寿命メモリにキャッシュされるペアですしかし、 Service Worker の仕様で定義されている メインスレッドまたは通常のワーカーからこの API を使用できます。屋外で使用する場合 呼び出す場合は、 Cache.put() メソッド 合成された Response オブジェクトで、 Request オブジェクト。

このガイドでは、メモリ内 blob を前提としています。キャッシュキーとして偽の URL を使用し、 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;
  }
};

送信元の Private File System 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;
  }
};

IndexedDB API

IndexedDB 任意のデータを永続的に保管するための確立された標準 表示されます。やや複雑な API で知られていますが ラッパー ライブラリ(idb-keyval など) IndexedDB は従来の Key-Value ストアのように扱うことができます。

例:

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 に永続化できる形式を指定します。

このパターンでは、ユーザーはモデルファイルへのアクセス権を付与するだけでよい 1 回だけです。永続的な権限により、 ユーザーはファイルへのアクセスを永続的に許可することを選択できます。コードを再読み込みすると、 クリックなどの必要なユーザー操作と、 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;
  }
};

これらの方法は相互に排他的ではありません。場合によっては、 ブラウザでモデルを明示的にキャッシュし、ユーザーのハードディスクにあるモデルを使用します。

デモ

3 つの通常のケースのストレージ方式とハードディスク方式の MediaPipe LLM デモに実装されています。

参考: 大きなファイルをチャンク形式でダウンロードする

インターネットから大規模な AI モデルをダウンロードする必要がある場合は、 個別のチャンクにダウンロードし クライアント上で再びつなぎ合わせます

コードで使用できるヘルパー関数を以下に示します。必要なのは、 url です。chunkSize(デフォルト: 5 MB)、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 API と IndexedDB API 保存する前にデータをシリアル化する必要があります。IndexedDB は、 取得時にデータがシリアル化解除されるため、データの保存場所として最悪の場所になる 構築できます。

ニッチなアプリケーションでは、File System Access API を使用してファイルに直接アクセス 独自の AI モデルを管理するユーザーに最適です。

AI モデルを保護する必要がある場合は、サーバー上に保持します。保存すると、 キャッシュと IndexedDB の両方からデータを抽出するのは簡単です。 DevTools または OFPS DevTools 拡張機能。 これらのストレージ API のセキュリティは本質的に同じです。新しい P-MAX キャンペーンを 暗号化されたバージョンのモデルを保存しますが、その後、必要に応じて クライアントに渡され、インターセプトされる可能性があります。これは不正な行為者が 少し難しくなりますが 不可能ではありません

アプリの目的に合ったキャッシュ戦略を選択することをおすすめします。 要件、対象ユーザーの行動、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。