שמירת מודלים של AI במטמון בדפדפן

לרוב המודלים של ה-AI יש לפחות מכנה אחד משותף: גדול במידה רבה בשביל משאב הועברו דרך האינטרנט. המודל הקטן ביותר לזיהוי אובייקטים של MediaPipe (SSD MobileNetV2 float16) משקלל 5.6MB והגדולה ביותר היא בגודל 25MB.

LLM בקוד פתוח gemma-2b-it-gpu-int4.bin שעונים בגודל 1.35GB – וגודלם נחשב לקטן מאוד ל-LLM. ומודלים של בינה מלאכותית גנרטיבית יכולים להיות עצומים. לכן, מתבצע היום שימוש רב ב-AI בענן. יותר ויותר אפליקציות מריצים מודלים שעברו אופטימיזציה באופן ישיר במכשיר. בזמן שהדגמות של מודלים גדולים של שפה (LLM) פועלים בדפדפן הנה כמה דוגמאות ברמת הייצור למודלים אחרים שפועלים דפדפן:

Adobe Photoshop באינטרנט, כשהכלי מבוסס ה-AI לבחירת אובייקטים פתוח, שנבחרו בו שלושה אובייקטים: שתי ג'ירפות וירח.

כדי לזרז השקות עתידיות של האפליקציות שלך, עליך לשמור באופן מפורש את המטמון את נתוני המודל במכשיר, במקום להסתמך על דפדפן ה-HTTP המשתמע של Google.

במדריך הזה נעשה שימוש בgemma-2b-it-gpu-int4.bin model כדי ליצור צ'אט בוט, אפשר לכלול את הגישה הזו כך שתתאים למודלים אחרים ולמקרי שימוש אחרים במכשיר. הדרך הנפוצה ביותר לחבר אפליקציה למודל היא להציג לצד שאר המשאבים של האפליקציה. חשוב מאוד לבצע אופטימיזציה משלוח.

הגדרת הכותרות הנכונות של המטמון

אם אתם משתמשים במודלים של AI מהשרת שלכם, חשוב להגדיר Cache-Control הכותרת. בדוגמה הבאה מוצגת הגדרת ברירת מחדל יציבה, שאפשר ליצור מופעל בהתאם לצורכי האפליקציה שלך.

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

כל גרסה של מודל AI שהושקה היא משאב סטטי. תוכן שאף פעם לא צריך לתת לשינויים max-age משולב עם עקיפת מטמון (cache busting) בכתובת ה-URL של הבקשה. אם צריך לעדכן את המודל, נותנים לו כתובת URL חדשה.

כשהמשתמש טוען מחדש את הדף, הלקוח שולח בקשת אימות מחדש, אפילו למרות שהשרת יודע שהתוכן יציב. immutable מציינת במפורש שאין צורך באימות מחדש, התוכן לא ישתנה. ההוראה immutable היא לא נתמך באופן נרחב באמצעות דפדפנים ושרתי proxy או מטמון מתווכים, אבל בשילוב אותו את הוראת max-age שמובנת באופן אוניברסלי, ניתן להבטיח מקסימום בתאימות מלאה. public הוראת תגובה מציינת שניתן לשמור את התגובה במטמון משותף.

כלי הפיתוח ל-Chrome מציגים את סביבת הייצור Cache-Control כותרות שנשלחות על ידי חיבוק פנים כשמבקשים מודל AI. (מקור)

שמירת מודלים של AI במטמון בצד הלקוח

כשמפעילים מודל AI, חשוב לשמור את המודל באופן מפורש במטמון בדפדפן. כך אפשר להבטיח שנתוני המודל יהיו זמינים באופן מיידי אחרי שהמשתמש טוען מחדש את האפליקציה.

יש כמה שיטות שאפשר להשתמש בהן כדי לעשות את זה. עבור דוגמאות קוד, נניח שכל קובץ מודל מאוחסן אובייקט Blob בשם blob בזיכרון.

כדי להבין את הביצועים, לכל דוגמת קוד מופיעה הערה עם performance.mark() וגם performance.measure() שיטות. המדדים האלה תלויים במכשיר ולא ניתנים להכללה.

בכלי הפיתוח ל-Chrome Application > אחסון, ביקורת תרשים השימוש עם פלחים של IndexedDB, אחסון המטמון ו-File System. כל מקטע מוצג צורך ב-1,354 מגה-בייט של נתונים, שבסך הכל 4,063 מגה-בייט.

אתם יכולים להשתמש באחד מממשקי ה-API הבאים כדי לשמור מודלים של AI במטמון בדפדפן: cache API, Origin Private File System API, וגם IndexedDB API. ההמלצה הכללית היא להשתמש Cache API, אבל מדריך זה מפרט את היתרונות והחסרונות של כל האפשרויות.

Cache API

Cache API מספק אחסון מתמיד עבור Request ואובייקט Response נשמרים במטמון בזיכרון לטווח ארוך. למרות שזה כפי שמוגדר במפרט Service Workers, אפשר להשתמש ב-API הזה מה-thread הראשי או מ-worker רגיל. כדי להשתמש בממשק מחוץ ל- הקשר של קובץ שירות (service worker), אמצעי תשלום 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;
  }
};

ממשק ה-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

IndexedDB הוא תקן מבוסס היטב לאחסון נתונים שרירותיים באופן קבוע בדפדפן. הוא ידוע לשמצה ב-API שהוא מורכב במידה מסוימת, אבל באמצעות שימוש ספריית wrapper כמו 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 עם גישה לקובץ בדיסק הקשיח.

הרשאות הגישה לקובץ נמשכות ומבקשים אותן במקרה הצורך, ולכן חלק זה לטעינות מחדש בעתיד. הדוגמה הבאה מראה איך לקבל עבור קובץ מהדיסק הקשיח, ולאחר מכן מאחסנים ומשחזרים את נקודת האחיזה.

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;
  }
};

השיטות האלה אינן בלעדיות ללא כל חפיפה ביניהן. יכול להיות מצב שבו שניכם לשמור מודל באופן מפורש בדפדפן ולהשתמש במודל מהדיסק הקשיח של המשתמש.

הדגמה (דמו)

אפשר לראות את כל שלוש השיטות הרגילות של אחסון כיסוי ואת השיטה של הדיסק הקשיח מוטמעת בהדגמה של מודל שפה גדול (LLM) של MediaPipe.

בונוס: הורדת קובץ גדול במקטעים

אם אתם צריכים להוריד מודל 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 צריך לבצע פעולת des deserial לביצוע נתונים כדי לאחזר אותם, כך שזה המקום הגרוע ביותר לאחסון. מודלים גדולים של שפה.

לאפליקציות נישה, File System Access API מציע גישה ישירה לקבצים במכשיר של משתמש. מתאים במיוחד למשתמשים שמנהלים מודלים משלהם של AI.

אם אתם צריכים לאבטח את מודל ה-AI, צריך להשאיר אותו בשרת. לאחר האחסון הוא טריוויאלי לחלץ את הנתונים גם מהמטמון וגם מה-IndexedDB כלי פיתוח או התוסף של OFPS בכלי הפיתוח. ממשקי ה-API האלה לאחסון שווים מטבעם באבטחה. אולי מפתים אתכם ואחסנו גרסה מוצפנת של המודל, אבל אז צריך לקבל את הפענוח את המפתח ללקוח, שעלול ליירט אותו. המשמעות היא שגורם זדוני מנסה לבצע קצת יותר קשה לגנוב את המודל, אבל זה בלתי אפשרי.

מומלץ לבחור אסטרטגיית שמירה במטמון שתואמת הדרישות, ההתנהגות של קהל היעד והמאפיינים של המודלים של ה-AI בשימוש. כך ניתן להבטיח שהאפליקציות יהיו רספונסיביות ועמידות בתנאים את תנאי הרשת ואילוצי המערכת.


אישורים

הביקורת הזו נבדקה על ידי Joshua Bell, Reilly Grant, Evan Stade, Nathan Memmott, Austin Sullivan, Etienne Noël, André Bandarra, Alexandra Klepper פרנסואה בופורט, פול קינלן ורייצ'ל אנדרו.