記事一覧に戻る

Thomas Steiner's avatar

(本記事はGoogleのChromeチームのDeveloper Relations Engineer、Thomas Steinerによるゲスト投稿です。)

Transformers.jsは、Web開発者にタスク固有のパイプラインを通じてWebアプリでトランスフォーマーのパワーを簡単に利用する方法を提供します。ブラウザで推論を実行するには、開発者がpipeline()のインスタンスを作成し、パイプラインで使用するタスクを指定します。具体例として、以下のスニペットは自動音声認識(ASR)パイプラインの設定方法を示しています。

import { pipeline } from 'https://cdn.jsdelivr.net/npm/@huggingface/[email protected]';

const asr = await pipeline(
  'automatic-speech-recognition',
  'Xenova/whisper-tiny.en',
  { device: 'webgpu' },
);
const result = await asr('jfk.wav');
console.log(result);

自動音声認識パイプラインの最小限の例。

キャッシュの課題

ソースコードでXenova/whisper-tiny.enモデルを指定していることがわかります。これは一般的な英語の自動音声認識タスクに非常に適した選択です。実際、Transformers.jsのデフォルトモデル解決によると、デフォルトモデルでもあります。リンク先の抜粋をご覧ください。

モデルリソース

この例をブラウザで実行すると、Transformers.jsは関連するモデルリソースとWasmファイルを自動的にダウンロードしてキャッシュします。次のスクリーンショットは、アプリ訪問後のChrome DevToolsのCache storageセクションを示しています。ページを再読み込みすると、リソースはCache APIから提供され、モデルはほぼ瞬時に結果を返します。

アプリ訪問後のWhisper AIモデルリソースとWasmランタイムファイルを示すChrome DevToolsのCache storageセクション。

しかし、Xenova/whisper-tiny.enは人気のモデルであり(前述の通り、Transformers.jsのASRデフォルトモデルでもあるため)、訪問する複数のアプリで使用される可能性があります。この状況をシミュレートするため、以前と同じサンプルアプリを異なるオリジンから提供します。この異なるオリジンのアプリにアクセスすると、ほぼ瞬時に使用可能になる代わりに、ブラウザはモデルリソースを再度ダウンロードしてキャッシュする必要があります(バイト単位で同一の場合でも)。この簡易例でも、重複ダウンロードと保存量は177 MBに達し、Chrome DevToolsのApplicationパネルStorageセクションで確認できます。この量はすぐに積み重なることが想像できます。

177 MBのストレージ使用量を示すChrome DevToolsのStorage概要。

Wasmランタイムリソース

さらに深刻な問題があります。簡易例に2つ目のパイプライン(感情分析)を追加してみましょう。感情分析はデフォルトXenova/distilbert-base-uncased-finetuned-sst-2-englishモデルを使用します。モデルを指定しない場合、Transformers.jsのデフォルトモデル解決が自動的に選択します。

const classifier = await pipeline('sentiment-analysis');
const sentiment = await classifier(result.text);
console.log(sentiment);

image

2つの全く異なるAIモデルですが、Transformers.jsの基盤となるONNX Runtimeライブラリから提供される同じ4,733 kBのort-wasm-simd-threaded.asyncify.wasm WebAssembly(Wasm)ランタイムファイルに依存しています。異なるオリジンで拡張デモを開くと、NetworkタブでWasmランタイムも再度ダウンロードおよびキャッシュされることがわかります。

Wasmランタイムリソースのダウンロードを示すChrome DevToolsのNetworkパネル。

したがって、同一のAIモデルを共有しないアプリを実行する場合でも、ブラウザは既に持っている共有Wasmリソースに対して冗長なリクエストを行い、さらにそれを再度キャッシュしてハードディスクの容量を消費します。

キャッシュの分離

AIモデルリソースの提供

デフォルトでは、AIモデルリソースHugging Face Hubから提供され、最終的にHugging Face CDNから配信されます。ブラウザはhttps://huggingface.co/Xenova/distilbert-base-uncased-finetuned-sst-2-english/resolve/main/config.jsonのようなリソースをリクエストし、最終的なCDN URL(この場合はhttps://huggingface.co/api/resolve-cache/models/Xenova/distilbert-base-uncased-finetuned-sst-2-english/0b6928efcb76139cae2c6881d49cda67fe119f42/config.json?%2FXenova%2Fdistilbert-base-uncased-finetuned-sst-2-english%2Fresolve%2Fmain%2Fconfig.json=&etag=%223c36342ef1f74de2797d667c68c6b7b988d0b87c%22)にリダイレクトされます。

Wasmランタイムリソースの提供

WasmランタイムリソースはデフォルトでjsDelivr CDNから提供されます。たとえば、ort-wasm-simd-threaded.asyncify.wasmは執筆時点でhttps://cdn.jsdelivr.net/npm/[email protected]/dist/ort-wasm-simd-threaded.asyncify.wasmから取得されます。

異なるオリジンで動作するアプリが最終的に同じCDN URLからリソースを提供する場合、最終URLが同一であればキャッシュに問題はないはずだと思われるかもしれません。しかし、これは長年にわたりブラウザのキャッシュの仕組みとは異なります。記事Gaining security and privacy by partitioning the cacheで詳細が説明されていますが、基本的にキャッシュはオリジンごとに分離されており、タイミング攻撃を防ぐためです。ウェブサイトがHTTPリクエストに応答する時間は、ブラウザが過去に同じリソースにアクセスしたことを示す可能性があり、セキュリティとプライバシーの漏洩につながる恐れがあります。

Chromeの実装

具体的な実装はブラウザによって異なりますが、ChromeではキャッシュされたリソースはリソースURLに加えてNetwork Isolation Keyでキーが付けられます。Network Isolation Keyはトップレベルサイト現在のフレームサイトで構成されます。以前の簡易例をhttps://googlechrome.github.iohttps://rawcdn.rawgit.netのオリジンでホストした場合、Wasmランタイムをhttps://cdn.jsdelivr.net/npm/[email protected]/dist/ort-wasm-simd-threaded.asyncify.wasmから使用すると、キャッシュキーは以下の表のようになります。

Network Isolation Key Resource URL
Top-level site Current-frame site
https://googlechrome.github.io
https://googlechrome.github.io
https://cdn.jsdelivr.net/npm/[email protected]/dist/ort-wasm-simd-threaded.asyncify.wasm
https://rawcdn.rawgit.net
https://rawcdn.rawgit.net
https://cdn.jsdelivr.net/npm/[email protected]/dist/ort-wasm-simd-threaded.asyncify.wasm

したがって、リソースURLが完全に同一であっても、Network Isolation Keyが一致しないためキャッシュヒットは発生せず、重複ダウンロードと重複保存が発生します。これがCross-Origin Storage提案が取り組む課題です。

💡 注: Cross-Origin Storage APIは早期段階の提案であり、まだ確定していません。提案されたAPIはまだどのブラウザにもネイティブ実装されていませんが、待たずに実験することができます。Cross-Origin Storage extensionをインストールして、全てのページにnavigator.crossOriginStorageポリフィルを注入し、完全なフローをテストしてください。

提案されているCross-Origin Storage (COS) APIは、専用のnavigator.crossOriginStorageインターフェースを導入し、Webアプリがオリジンの境界を越えて大規模ファイルを保存・取得できるようにします。URLではなく暗号化ハッシュで識別されます。

Cross-Origin Storage APIのロゴ:横断歩道の標識でよく見られる、様式化された歩行者。

暗号化ハッシュに関する最後のポイントが重要です。COSはURLやオリジンではなくハッシュでファイルを識別するため、https://googlechrome.github.io訪問時にダウンロードした同じort-wasm-simd-threaded.asyncify.wasm Wasmランタイムは、2つのオリジンのいずれが取得したかに関係なく、https://rawcdn.rawgit.netがリクエストしようとしているものと同一と認識されます。基本的なフローを示す以下のコードスニペットをご覧ください。

const hash = {
  algorithm: 'SHA-256',
  value: '8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4',
};

try {
  const handle = await navigator.crossOriginStorage.requestFileHandle(hash);
  // キャッシュヒット!ファイルをBlobとして取得し、直接使用します。
  const fileBlob = await handle.getFile();
} catch {
  // キャッシュミス。ネットワークからダウンロードし、次回のために保存します。
  const fileBlob = await fetch('https://cdn.jsdelivr.net/.../ort-wasm-simd-threaded.asyncify.wasm')
    .then(r => r.blob());
  const handle = await navigator.crossOriginStorage.requestFileHandle(
    hash,
    { create: true, origins: '*' },
  );
  const writableStream = await handle.createWritable();
  await writableStream.write(fileBlob);
  await writableStream.close();  
}

リソースがCOSにある場合、FileSystemFileHandleが返され、getFile()経由でBlobを直接読み取ることができます(返されるFileBlobを継承します)。リソースがCOSにない場合、ネットワークにフォールバックし、次のアプリのためにリソースをCOSに書き込みます(自アプリでも、完全に異なるオリジンの無関係なアプリでも)。

このAPIは、File System StandardFileSystemDirectoryHandle.getFileHandle()を意図的に模倣しており、Origin Private File System (OPFS) APIで馴染みがあるかもしれません。hashパラメータはOPFSのnameパラメータと同じ役割を果たします:リソースを一意に識別します。options.createフラグも同じように動作します:読み取り専用アクセスの場合は省略またはfalse、書き込みを意図する場合はtrue

誰が何を読み取れるかを制御する

すべてのリソースがグローバルに共有されるべきではありません。COSは、ファイル保存時にoriginsオプションを通じて可視性を精密に制御できます。

  • origins: '*'を設定すると、ファイルはグローバルに利用可能になります。任意のオリジンがハッシュで検索できます。Transformers.jsの例におけるAIモデルリソースやWasmランタイムにはこれが適切です:Web上のすべてのアプリが単一のキャッシュコピーから恩恵を受けることが目的です。
  • origins: ['https://write.example.com', 'https://calculate.example.com']のように特定のオリジンリストを渡すと、アクセスをそれらのサイトに制限します。これは、商業オフィススイートで使用される独自の校正AIモデルのように、企業の自社プロパティ間で共有される独自リソースで、他の人に見つかるべきでない場合に適しています。
  • originsを完全に省略すると、ファイルはsame-siteオリジンのみで利用可能になります。これは、組織のサブドメイン全体で共有されるが、組織の境界を越えることを意図しないリソースの妥当なデフォルトです。

重要なルール:可視性はアップグレード可能ですが、ダウングレードはできません。ファイルが既にグローバルに利用可能な場合、後で制限されたoriginsリストで保存しようとしても無視されます。これにより、悪意のあるアクターが公開リソースを再保存して可用性を狭めることを防ぎます。逆は可能です:最初に制限されたoriginsリストで保存されたファイルは、後でより寛容にすることができます。元の保存者だけでなく、任意のサイトが同じハッシュ(ハッシュは秘密ではありません)でrequestFileHandle()を呼び出し、create: trueとより広いorigins値で呼び出すことができ、ブラウザがハッシュの一致を検証すれば、その時点からリソースがより広い対象に利用可能になります。アップグレードするサイトは、返されたハンドルを通じて完全なファイルを書き込む必要があります。この要件は、サイトがアップグレードパスを悪用して特定のファイルがCOSに既に保存されているかどうかを検出するサイドチャネルとして利用することを防ぐために存在します。

設計による完全性

COSの微妙だが重要な特性は、ブラウザがファイル書き込み時にハッシュを検証することです。書き込むデータが宣言されたハッシュと一致しない場合、書き込みはエラーで失敗します。これにより、完全性チェックが自動化されます:COSからファイルを読み取るアプリは、ネットワークダウンロード後に自身でハッシュを計算した場合と同じ保証を得られます。

これはTransformers.jsのシナリオで二重に有用であることがわかります。現在、モデル重みをダウンロードした後、ほとんどのアプリにはCDNが正しいバイトを提供したことを検証する実用的な方法がありません。COSでは、ストア内のすべてのファイルは書き込み時に暗黙的に検証され、公式のHugging Face CDNやランダムなサイトの自己ホストミラーのいずれから来たかに関係ありません。

実用性を犠牲にしないプライバシー

もちろん、クロスオリジン共有キャッシュは、パーティション化されたHTTPキャッシュと同じ質問を逆方向に提起します:任意のサイトがハッシュでファイルの存在をプローブできる場合、攻撃者はゲームエンジンのWasmモジュールがキャッシュされているかどうかをチェックすることで、ユーザーの閲覧履歴について何かを知ることができないでしょうか?

COSは2つの補完的なメカニズムでこれに対処します:

  • まず、originsフィールド:グローバルにプローブ可能であるべきでない独自リソースは、origins: '*'で保存すべきではなく、開発者教育を通じて、開発者は意味がある場合にこれを検討するよう奨励されます。
  • 次に、可用性ゲーティング:グローバルに宣言されたファイルであっても、ブラウザは十分な数の異なるオリジンで遭遇していない場合、ファイルの存在確認を抑制する可能性があります。1つまたは2つのサイトにしか表示されないファイルは、依然としてクロスサイト識別子として機能する可能性があるため、ブラウザはディスク上にあるかどうかに関係なく、ファイルが存在しないかのようにエラーを返す可能性があります。Chromeチームは、一般的でないリソースが引き起こす可能性のあるプライバシーリークを認識しており、正確にどのリソースをキャッシュできるかを制限することで緩和を計画しています。具体的な緩和策はまだ策定中です。

重要なのは、エラーは決定的な回答ではないということです。「保存されていない」ことを意味する場合も、「保存されているが、ブラウザはあなたに伝えていない」ことを意味する場合もあります。アプリは常に同じ方法で処理する必要があります:ネットワークにフォールバックする。

Transformers.jsの例でこれが意味すること

以前の簡易例に戻ります:ort-wasm-simd-threaded.asyncify.wasmランタイムのサイズは4,733 kBで、使用するAIモデルに関係なく、すべてのTransformers.js搭載アプリで共有されます。COSを使用すると、最初にロードするアプリが一度ダウンロードし、SHA-256ハッシュの下にorigins: '*'で保存します。以降のすべてのアプリ(https://googlechrome.github.iohttps://rawcdn.rawgit.net、または他の任意のオリジン)がCOSで即座に見つけます。Whisperモデル重みの177 MBの重複は?同じ話です:Xenova/whisper-tiny.enは一度ダウンロードされ、2回目以降はハッシュで認識され、COSからミリ秒単位で提供されます。もちろん、Xenova/distilbert-base-uncased-finetuned-sst-2-englishでも同じことが起こります。

Transformers.js自体は、ライブラリレベルで既にCOS APIを試験導入しています。Pull request #1549で、オプトインフラグの背後に実験的なCOSキャッシュバックエンドが導入されました。有効化はパイプラインを設定する前に1行で済みます:

import { env, pipeline } from "https://cdn.jsdelivr.net/npm/@huggingface/[email protected]";

// 👇 実験的なCross-Origin Storageキャッシュバックエンドにオプトインします。
env.experimental_useCrossOriginStorage = true;

const asr = await pipeline('automatic-speech-recognition', 'Xenova/whisper-tiny.en', { device: 'webgpu' });
const result = await asr('jfk.wav');
console.log(result);

フラグのexperimental_プレフィックスにご注意ください。これは意図的なもので、基盤となるブラウザAPIがまだ標準化されておらず、メジャーバージョンアップなしで変更される可能性があることを示しています。このフラグを設定すると、Transformers.jsは各Xet追跡モデルファイル(大きなONNX重みファイル)のSHA-256ハッシュを、生のXetポインタ(生ポインタファイルの例)を取得して解決し、そのoid sha256:フィールドを抽出します。次に、そのハッシュをnavigator.crossOriginStorageのキーとして使用します。モデルが既にCOSにある場合(別のサイトが先に保存した場合)、ネットワークラウンドトリップなしで即座に提供されます。ない場合、通常のダウンロードにフォールバックし、結果を次の呼び出し元のためにCOSに保存します。簡易例では、Xenova/whisper-tiny.enXenova/distilbert-base-uncased-finetuned-sst-2-english(およびもちろんort-wasm-simd-threaded.asyncify.wasm)が、どれだけ異なるオリジンが要求しても、一度だけネットワークを通過すれば済むという利点があります。

モデルの柔軟性

簡易例はXenova/whisper-tiny.enで問題なく動作しますが、もちろんユーザーが既にCOSキャッシュに他のWhisperバリアントのいずれかを持っている場合に断る理由はありません。たとえば、ユーザーがXenova/whisper-large-v3を既に持っている可能性があり、名前が示すようにtinyバリアントよりはるかに大きいです。Transformers.jsのModel Registryにより、モデルに関する柔軟性が可能になります。アプリのニーズが、たとえばXenova/whisper-tiny.enwhisper-medium.en、またはXenova/whisper-large-v3のいずれかで対応可能であることがわかっている場合、各モデルの関連ファイルをレジストリで確認し、COSキャッシュでの存在をプローブ(部分的にまたは完全に必要なモデルリソースを含む可能性があります)し、最終的にどのモデルを選択するかを決定できます。ModelRegistry.is_pipeline_cached() APIはCOS(およびもちろんCache API)と直接統合されているため、この操作は非常に使いやすいです。

今日試してみる

COS APIはまだどのブラウザにもネイティブ実装されていませんが、待たずに実験することができます。Cross-Origin Storage extensionをインストールして、全てのページにnavigator.crossOriginStorageポリフィルを注入し、完全なフローをテストしてください。拡張機能のソースコードを確認し、使用手順に従って開始してください。

Cross-Origin Storage extensionのChrome Web Storeページ。

拡張機能をインストールすると、すぐに完全なエンドツーエンド体験を試すことができます:COSを有効にした最初の簡易例を開きXenova/whisper-tiny.enをロードさせ、次に2番目のオリジンからCOSを有効にした簡易例を開きます。以前に見た177 MBの再ダウンロードではなく、モデルはCOSからミリ秒単位で提供されます。拡張機能のポップアップウィンドウを開くと、COSの動作を確認できます。View by Resourceで表示すると、SHA-256ハッシュ950978b1dbcbf250335358c1236053ba19a7f7849b33dc777f4421b72b7626faのリソースがhttps://googlechrome.github.iohttps://rawcdn.rawgit.net間で共有されていることがわかります。一見明白ではありませんが、Hugging FaceのSHA-256ハッシュと比較して確認すると、https://huggingface.co/Xenova/whisper-tiny.en/blob/main/onnx/decoder_model_merged.onnxであることがわかります。現在、この拡張機能は主にあなたのようなパワーユーザーを対象としています。ブラウザに実装された後は、ブラウザのSettingsページでより使いやすい統合が提供される予定です。以下のスクリーンショットは、View by Resourceタブがアクティブな拡張機能のポップアップウィンドウを示しており、ハッシュとCOSキャッシュに保持する2つのオリジンを持つ共有リソースを確認できます。

2つのオリジン間で共有されているCross-Origin Storage拡張機能で確認されたリソース。

アクションへの呼びかけ

独自のTransformers.jsアプリを構築している場合、アクションへの呼びかけはシンプルです:最初のpipeline()呼び出しの前にenv.experimental_useCrossOriginStorage = trueを追加し、拡張機能をインストールして、Networkタブから重複ダウンロードが消えるのを確認してください。オプトインするすべてのサイトが、他のすべてのサイトのユーザーにとってエクスペリエンスをより速く、より安価にします。オプトインは完全にリスクフリーです:ユーザーがCOS拡張機能をインストールしていないためにCOS APIがサポートされていない場合、コードはデフォルトパス(Cache API)にフォールバックするだけです。

Transformers.jsはCOSの実験を単独で行っているわけではありません。WebLLM(オプトイン、ドキュメントを参照)およびwllama(自動、PRを参照)も、この提案されたAPIに興奮しています。

Chromeチームでは、ブラウザにCOS APIをネイティブ実装することを検討しています。早期段階の提案として、APIおよび提案自体の形状に関するフィードバックを歓迎します。Cross-Origin Storageリポジトリが、問題の提出、サポートの表明、またはPRのオープンに適した場所です。