大多數 AI 模型都至少有一件事:對於透過網際網路傳輸的資源而言,這類模型相當龐大。最小的 MediaPipe 物件偵測模型 (SSD MobileNetV2 float16
) 權重為 5.6 MB,最大為 25 MB。
開放原始碼 LLM gemma-2b-it-gpu-int4.bin
時脈大小為 1.35 GB,對 LLM 而言為非常小。生成式 AI 模型可能相當龐大這就是為何現今大量使用 AI 技術的原因越來越多應用程式直接在裝置上執行高度最佳化的模型。雖然有在瀏覽器中執行的 LLM 示範,但以下是一些在瀏覽器中運作的其他模型實際工作環境等級範例:
- Adobe Photoshop 會在裝置端執行
Conv2D
模型的變化版本,藉此提供智慧物件選取工具。 - Google Meet 會執行經過最佳化的
MobileNetV3-small
模型,以便利用背景模糊功能進行人物區隔。 - Tokopedia 會執行
MediaPipeFaceDetector-TFJS
模型來進行即時臉部偵測,避免服務收到無效的註冊作業。 - Google Colab 可讓使用者在 Colab 筆記本中使用硬碟中的模型。
為了加快應用程式的日後啟動速度,您應該在裝置端明確快取模型資料,而不是依賴隱式 HTTP 瀏覽器快取。
雖然本指南使用 gemma-2b-it-gpu-int4.bin model
建立聊天機器人,但此方法可一般化,適用於裝置端的其他模型和其他用途。將應用程式連結至模型的最常見方式,是將模型與其他應用程式資源一起提供。請務必提昇放送頻率
設定合適的快取標頭
如果您是透過伺服器提供 AI 模型,請務必設定正確的 Cache-Control
標頭。以下範例顯示完善的預設設定,您可以根據應用程式的需求進行建構。
Cache-Control: public, max-age=31536000, immutable
每個 AI 模型發布的版本都是靜態資源。要求網址中的快取清除應搭配很長的 max-age
組合,不要變更的內容。如果您需要更新模型,則必須提供新網址。
當使用者重新載入頁面時,即使伺服器知道內容穩定,用戶端仍會傳送重新驗證要求。immutable
指令明確表示不需要重新驗證,因為內容不會變更。瀏覽器和中介快取或 Proxy 伺服器並未廣泛支援 immutable
指令,但藉由結合該指令與眾所理解的 max-age
指令,可以確保最大的相容性。public
回應指令表示回應可儲存在共用快取中。
在用戶端快取 AI 模型
提供 AI 模型時,請務必明確在瀏覽器中快取模型。這可確保使用者重新載入應用程式後,隨時可以存取模型資料。
您可以運用幾種技術來達成此目標。針對下列程式碼範例,假設每個模型檔案都儲存在記憶體中名為 blob
的 Blob
物件中。
為了瞭解效能,每個程式碼範例都會加上 performance.mark()
和 performance.measure()
方法註解。這些措施因裝置而異,無法一般化。
您可以選擇使用下列其中一個 API,在瀏覽器中快取 AI 模型:Cache API、Origin Private File System API 和 IndexedDB API。我們一般建議使用 Cache API,但本指南仍說明所有選項的優缺點。
快取 API
Cache API 可為 Request
和 Response
物件配對提供永久儲存空間,這些物件組合可透過長期記憶體快取進行。雖然已在 Service Worker 規格中定義,但您可以從主執行緒或一般工作站使用這個 API。如要在 Service Worker 內容外使用,請使用合成 Response
物件呼叫 Cache.put()
方法,並與合成網址 (而非 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;
}
};
來源 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 視為傳統的鍵/值儲存庫來處理。
例如:
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);
}
}
特殊情況:在硬碟上使用模型
您可以直接從使用者的硬碟參照 AI 模型,做為瀏覽器儲存空間的替代方案。這項技術可讓以研究為主的應用程式展示在瀏覽器中執行特定模型的可行性,也讓藝術工作者在專業的創意應用程式中,使用自行訓練模型。
File System Access API
您可以使用 File System Access API,開啟硬碟中的檔案,並取得可以保存至 IndexedDB 的 FileSystemFileHandle。
透過這個模式,使用者只需授予一次模型檔案的存取權。有了永久權限,使用者可以選擇永久授予檔案存取權。重新載入應用程式及必要的使用者手勢 (例如點擊滑鼠) 後,即可從 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 模型,請將下載作業平行平行處理至不同的區塊,然後在用戶端再次拼接。
以下是可在程式碼中使用的輔助函式。您只需將其傳遞 url
。chunkSize
(預設值:5 MB)、maxParallelRequests
(預設值:6)、progressCallback
函式 (回報 downloadedBytes
和 fileSize
總數),以及 AbortSignal
信號的 signal
皆為選用項目。
您可以在專案中複製下列函式,或是從 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;
選擇最適合您的方法
本指南已介紹各種可以有效快取 AI 模型瀏覽器中的 AI 模型的方法,這項工作對於提升使用者體驗和應用程式效能至關重要。Chrome 儲存空間團隊建議使用 Cache API,以達到最佳效能,以確保快速存取 AI 模型、縮短載入時間並改善回應速度。
而 OPFS 和 IndexedDB 較不實用。您必須先將 OPFS 和 IndexedDB API 序列化,才能儲存資料。此外,IndexedDB 需要在擷取資料時將資料去序列化,因此較不適合儲存大型模型。
以小眾應用程式來說,File System Access API 可直接存取使用者裝置上的檔案,適合自行管理 AI 模型的使用者。
如要保護 AI 模型,請將模型保留在伺服器上。儲存在用戶端後,就可以輕鬆使用開發人員工具從 Cache 和 IndexedDB 或 OFPS 開發人員工具擴充功能擷取資料。在這些儲存 API 的安全性方面,這些儲存 API 本質上的差異。您可能也會想要儲存模型的加密版本,但接下來需要將解密金鑰提供給用戶端,因為金鑰可能會遭到攔截。這意味著不肖人士試圖竊取模型相對困難,但不可能。
建議您根據應用程式的需求、目標對象行為以及所用 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。