לרוב המודלים של AI יש דבר אחד משותף: הם גדולים למדי כמשאבים שמועברים באינטרנט. מודל זיהוי האובייקטים הקטן ביותר של MediaPipe (SSD MobileNetV2 float16
) שוקל 5.6MB והגדול ביותר שוקל כ-25MB.
LLM בקוד פתוח gemma-2b-it-gpu-int4.bin
תופס 1.35GB – נפח קטן מאוד ל-LLM.
מודלים של AI גנרטיבי יכולים להיות עצומים. לכן, הרבה מהשימוש ב-AI מתבצע היום בענן. יותר ויותר אפליקציות מריצות מודלים שעברו אופטימיזציה גבוהה ישירות במכשיר. יש דמואים של מודלים של LLM שפועלים בדפדפן, אבל ריכזנו כאן כמה דוגמאות למודלים אחרים שפועלים בדפדפן ברמת הייצור:
- ב-Adobe Photoshop פועלת גרסה של מודל
Conv2D
במכשיר, לצורך כלי הבחירה החכם של האובייקטים. - ב-Google Meet פועלת גרסה אופטימיזציה של מודל
MobileNetV3-small
לצורך פילוח אנשים לצורך התכונה 'טשטוש רקע'. - ב-Tokopedia מריצים את המודל
MediaPipeFaceDetector-TFJS
לזיהוי פנים בזמן אמת כדי למנוע הרשמות לא חוקיות לשירות. - Google Colab מאפשר למשתמשים להשתמש במודלים מהדיסק הקשיח ב-notebooks של Colab.
כדי שההשקות העתידיות של האפליקציות יהיו מהירות יותר, כדאי לשמור במטמון במכשיר את נתוני המודל באופן מפורש, במקום להסתמך על המטמון המשתמע של דפדפן ה-HTTP.
במדריך הזה נעשה שימוש ב-gemma-2b-it-gpu-int4.bin model
כדי ליצור צ'אט בוט, אבל אפשר להכליל את הגישה הזו כך שתתאים למודלים אחרים ולתרחישים לדוגמה אחרים במכשיר. הדרך הנפוצה ביותר לחבר אפליקציה למודל היא להציג את המודל לצד שאר המשאבים של האפליקציה. חשוב לבצע אופטימיזציה של ההעברה.
הגדרת כותרות המטמון הנכונות
אם אתם מציגים מודלים של AI מהשרת שלכם, חשוב להגדיר את הכותרת הנכונה Cache-Control
. בדוגמה הבאה מוצגת הגדרת ברירת מחדל טובה, שאפשר להתבסס עליה בהתאם לצרכים של האפליקציה.
Cache-Control: public, max-age=31536000, immutable
כל גרסה שפורסמה של מודל AI היא משאב סטטי. תוכן שלא משתנה אף פעם צריך לכלול max-age
ארוך בשילוב עם ביטול מטמון בכתובת ה-URL של הבקשה. אם אתם צריכים לעדכן את המודל, עליכם לתת לו כתובת URL חדשה.
כשהמשתמש טוען מחדש את הדף, הלקוח שולח בקשה לאימות מחדש, למרות שהשרת יודע שהתוכן יציב. ההנחיה immutable
מציינת במפורש שאין צורך באימות מחדש כי התוכן לא ישתנה. ההנחיה immutable
לא נתמכת באופן נרחב בדפדפנים ובשרתי מטמון או שרתים זמניים (proxy) ביניים, אבל שילוב שלה עם ההנחיה max-age
, שכל הדפדפנים מבינים, מבטיח תאימות מקסימלית. הוראה התגובה public
מציינת שאפשר לשמור את התגובה במטמון משותף.
שמירת מודלים של AI במטמון בצד הלקוח
כשמציגים מודל AI, חשוב לשמור את המודל במטמון באופן מפורש בדפדפן. כך נתוני המודל יהיו זמינים באופן מיידי אחרי שמשתמש יטען מחדש את האפליקציה.
יש כמה שיטות שאפשר להשתמש בהן כדי להשיג את זה. בדוגמי הקוד הבאים, נניח שכל קובץ מודל מאוחסן באובייקט Blob
בשם blob
בזיכרון.
כדי להבין את הביצועים, כל דוגמת קוד מסומנת באמצעות הערות עם ה-method performance.mark()
ו-performance.measure()
. המדדים האלה תלויים במכשיר ולא ניתן להכליל אותם.
אפשר להשתמש באחד מממשקי ה-API הבאים כדי לשמור בזיכרון מטמון דגמי AI בדפדפן: Cache API, Origin Private File System API ו-IndexedDB API. ההמלצה הכללית היא להשתמש ב-Cache API, אבל במדריך הזה נסביר על היתרונות והחסרונות של כל האפשרויות.
Cache API
Cache API מספק אחסון מתמיד של זוגות אובייקטים מסוג Request
ו-Response
שמאוחסנים במטמון בזיכרון לטווח ארוך. הוא מוגדר במפרט של Service Workers, אבל אפשר להשתמש ב-API הזה מהשרשור הראשי או מ-worker רגיל. כדי להשתמש בו מחוץ להקשר של שירות העבודה, צריך לבצע קריאה ל-method 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;
}
};
Origin Private File System API
Origin Private File System (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, אפשר לפתוח קבצים מהדיסק הקשיח ולקבל FileSystemFileHandle שאפשר לשמור ב-IndexedDB.
בתבנית הזו, המשתמש צריך להעניק גישה לקובץ המודל רק פעם אחת. בעזרת הרשאות קבועות, המשתמש יכול לבחור להעניק גישה לקובץ באופן קבוע. אחרי טעינת האפליקציה מחדש וביצוע תנועת משתמש נדרשת, כמו לחיצה על העכבר, אפשר לשחזר את FileSystemFileHandle
מ-IndexedDB עם גישה לקובץ בדיסק הקשיח.
המערכת שולחת שאילתה לגבי הרשאות הגישה לקובץ ומבקשת אותן במקרה הצורך, כך שהפעולה הזו תתבצע בצורה חלקה בטעינות חוזרות עתידיות. בדוגמה הבאה מוסבר איך לקבל טיפול (handle) לקובץ מהדיסק הקשיח, ואז לאחסן ולשחזר את הטיפול.
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
(ברירת המחדל: 5MB), 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;
בחירת השיטה המתאימה
במדריך הזה פירטנו שיטות שונות לשמירת מודלים של AI במטמון בדפדפן בצורה יעילה. זוהי משימה חיונית לשיפור חוויית המשתמש באפליקציה ולשיפור הביצועים שלה. צוות האחסון של Chrome ממליץ להשתמש ב-Cache API כדי לשפר את הביצועים, להבטיח גישה מהירה למודלים של AI, לקצר את זמני הטעינה ולשפר את המהירות של התגובה.
האפשרויות OPFS ו-IndexedDB פחות שימושיות. כדי שאפשר יהיה לאחסן את הנתונים, צריך לבצע סריאליזציה שלהם באמצעות ממשקי ה-API של OPFS ו-IndexedDB. בנוסף, IndexedDB צריך לבצע דה-סריאליזציה של הנתונים כשהם מאוחזרים, ולכן זהו המקום הגרוע ביותר לאחסון מודלים גדולים.
לאפליקציות נישה, File System Access API מספק גישה ישירה לקבצים במכשיר של המשתמש, והוא אידיאלי למשתמשים שמנהלים מודלים משלהם של AI.
אם אתם צריכים לאבטח את מודל ה-AI, כדאי להשאיר אותו בשרת. אחרי שמאחסנים את הנתונים בצד הלקוח, קל מאוד לחלץ אותם גם מהמטמון וגם מ-IndexedDB באמצעות DevTools או תוסף OFPS DevTools. ממשקי ה-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.