تخزين نماذج الذكاء الاصطناعي في ذاكرة التخزين المؤقت في المتصفّح

تشترك معظم نماذج الذكاء الاصطناعي في أمر واحد، وهو أنّها كبيرة الحجم بالنسبة إلى مورد يتم نقله عبر الإنترنت. يبلغ حجم أصغر نموذج لرصد العناصر في MediaPipe (SSD MobileNetV2 float16) ‏5.6 ميغابايت، في حين يبلغ حجم أكبر نموذج حوالي 25 ميغابايت.

يبلغ حجم نموذج اللغة الكبير المفتوح المصدر gemma-2b-it-gpu-int4.bin 1.35 غيغابايت، ويُعدّ هذا الحجم صغيرًا جدًا بالنسبة إلى نموذج لغة كبير. يمكن أن تكون نماذج الذكاء الاصطناعي التوليدي ضخمة. لهذا السبب، يتم حاليًا استخدام الذكاء الاصطناعي بشكل كبير على السحابة الإلكترونية. تتزايد التطبيقات التي تشغّل نماذج محسّنة بشكل كبير مباشرةً على الجهاز. على الرغم من توفّر عروض توضيحية لنماذج لغوية كبيرة تعمل في المتصفح، إليك بعض الأمثلة على نماذج أخرى تعمل في المتصفح ومناسبة للاستخدام في بيئات الإنتاج:

‫Adobe Photoshop على الويب مع فتح أداة تحديد الكائنات المستندة إلى الذكاء الاصطناعي، مع تحديد ثلاثة كائنات: زرافتان وقمر

لإطلاق تطبيقاتك بشكل أسرع في المستقبل، عليك تخزين بيانات النموذج مؤقتًا على الجهاز بشكل صريح، بدلاً من الاعتماد على ذاكرة التخزين المؤقت الضمنية للمتصفح عبر بروتوكول HTTP.

على الرغم من أنّ هذا الدليل يستخدم gemma-2b-it-gpu-int4.bin model لإنشاء برنامج دردشة آلي، يمكن تعميم هذا الأسلوب ليناسب نماذج أخرى وحالات استخدام أخرى على الجهاز. الطريقة الأكثر شيوعًا لربط تطبيق بنموذج هي عرض النموذج مع بقية موارد التطبيق. من الضروري تحسين عرض الإعلانات.

ضبط عناوين ذاكرة التخزين المؤقت الصحيحة

إذا كنت تعرض نماذج الذكاء الاصطناعي من خادمك، من المهم ضبط عنوان Cache-Control الصحيح. يوضّح المثال التالي إعدادًا تلقائيًا جيدًا يمكنك الاستفادة منه لتلبية احتياجات تطبيقك.

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

كل إصدار من نموذج الذكاء الاصطناعي هو مورد ثابت. يجب منح المحتوى الذي لا يتغيّر مطلقًا قيمة max-age طويلة مع إيقاف التخزين المؤقت في عنوان URL الخاص بالطلب. إذا كنت بحاجة إلى تعديل النموذج، عليك منحه عنوان URL جديدًا.

عندما يعيد المستخدم تحميل الصفحة، يرسل العميل طلب إعادة التحقّق، حتى إذا كان الخادم يعرف أنّ المحتوى ثابت. تشير توجيهة immutable بشكل صريح إلى أنّ إعادة التحقّق غير ضرورية، لأنّ المحتوى لن يتغيّر. لا تتوافق المتصفّحات وخوادم التخزين المؤقت أو الخوادم الوكيلة الوسيطة مع التوجيه immutable على نطاق واسع، ولكن يمكنك ضمان التوافق إلى أقصى حد من خلال دمجه مع التوجيه max-age الذي يفهمه الجميع. يشير توجيه الاستجابة public إلى أنّه يمكن تخزين الاستجابة في ذاكرة تخزين مؤقت مشتركة.

تعرض "أدوات مطوّري البرامج في Chrome" Cache-Control العناوين التي أرسلتها منصة Hugging Face عند طلب نموذج ذكاء اصطناعي. (المصدر)

تخزين نماذج الذكاء الاصطناعي مؤقتًا من جهة العميل

عند عرض نموذج الذكاء الاصطناعي، من المهم تخزين النموذج مؤقتًا بشكل صريح في المتصفّح. ويضمن ذلك توفُّر بيانات النموذج على الفور بعد أن يعيد المستخدم تحميل التطبيق.

وهناك عدد من التقنيات التي يمكنك استخدامها لتحقيق ذلك. بالنسبة إلى نماذج الرموز التالية، افترِض أنّ كل ملف نموذج مخزّن في كائن Blob باسم blob في الذاكرة.

لفهم الأداء، يتم شرح كل نموذج للرمز باستخدام الطريقتَين performance.mark() وperformance.measure(). تعتمد هذه المقاييس على الجهاز ولا يمكن تعميمها.

في "أدوات مطوّري البرامج في Chrome" التطبيق > مساحة التخزين، راجِع مخطط الاستخدام الذي يتضمّن أقسامًا لكل من IndexedDB و"مساحة تخزين ذاكرة التخزين المؤقت" و"نظام الملفات". يظهر أنّ كل جزء يستهلك 1354 ميغابايت من البيانات، أي ما مجموعه 4063 ميغابايت.

يمكنك اختيار استخدام إحدى واجهات برمجة التطبيقات التالية لتخزين نماذج الذكاء الاصطناعي مؤقتًا في المتصفّح: Cache API وOrigin Private File System API وIndexedDB API. ننصح بشكل عام باستخدام Cache API، ولكن يناقش هذا الدليل مزايا وعيوب جميع الخيارات.

Cache API

توفّر واجهة برمجة التطبيقات Cache API مساحة تخزين ثابتة لأزواج عناصر Request وResponse التي يتم تخزينها مؤقتًا في ذاكرة طويلة الأمد. على الرغم من أنّ هذه الواجهة محدّدة في مواصفات Service Workers، يمكنك استخدامها من سلسلة التعليمات الرئيسية أو عامل عادي. لاستخدامها خارج سياق عامل الخدمة، استدعِ طريقة 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

نظام الملفات الخاص بالمصدر (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 هو معيار راسخ لتخزين بيانات عشوائية بطريقة ثابتة في المتصفّح. تشتهر IndexedDB بواجهة برمجة التطبيقات المعقّدة إلى حد ما، ولكن باستخدام مكتبة التفاف مثل 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);
  }
}

حالة خاصة: استخدام نموذج على قرص صلب

يمكنك الرجوع إلى نماذج الذكاء الاصطناعي مباشرةً من القرص الصلب للمستخدم كبديل لمساحة التخزين في المتصفّح. يمكن أن تساعد هذه التقنية التطبيقات التي تركّز على البحث في عرض مدى جدوى تشغيل نماذج معيّنة في المتصفح، أو تتيح للفنانين استخدام نماذج تم تدريبها ذاتيًا في تطبيقات إبداعية متخصّصة.

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

ولا يستبعد بعضها البعض. قد تحدث حالة يتم فيها تخزين نموذج مؤقتًا بشكل صريح في المتصفّح واستخدام نموذج من القرص الصلب للمستخدم.

عرض توضيحي

يمكنك الاطّلاع على جميع طرق التخزين الثلاث العادية وطريقة القرص الصلب المستخدَمة في العرض التوضيحي لنموذج اللغة الكبير من MediaPipe.

مكافأة: تنزيل ملف كبير على شكل أجزاء

إذا كنت بحاجة إلى تنزيل نموذج كبير للذكاء الاصطناعي من الإنترنت، يمكنك تقسيم عملية التنزيل إلى أجزاء منفصلة، ثم تجميعها مرة أخرى على الجهاز.

إليك دالة مساعدة يمكنك استخدامها في الرمز البرمجي. ما عليك سوى تمرير url إليه. إنّ chunkSize (القيمة التلقائية: 5 ميغابايت) و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;

اختيار الطريقة المناسبة لك

استعرض هذا الدليل طرقًا مختلفة لتخزين نماذج الذكاء الاصطناعي مؤقتًا بشكل فعّال في المتصفّح، وهي مهمة ضرورية لتحسين تجربة المستخدم وأداء تطبيقك. ينصح فريق التخزين في Chrome باستخدام Cache API لتحقيق أفضل أداء، وذلك لضمان الوصول السريع إلى نماذج الذكاء الاصطناعي وتقليل أوقات التحميل وتحسين الاستجابة.

يُعدّ كلّ من نظام الملفات الخاص بالمصدر (OPFS) وIndexedDB خيارَين أقل قابلية للاستخدام. يجب أن تعمل واجهتا برمجة التطبيقات OPFS وIndexedDB على تسلسل البيانات قبل أن يتم تخزينها. تحتاج IndexedDB أيضًا إلى إلغاء تسلسل البيانات عند استرجاعها، ما يجعلها أسوأ مكان لتخزين النماذج الكبيرة.

بالنسبة إلى التطبيقات المتخصّصة، تتيح واجهة برمجة التطبيقات File System Access API الوصول المباشر إلى الملفات على جهاز المستخدم، ما يجعلها مثالية للمستخدمين الذين يديرون نماذج الذكاء الاصطناعي الخاصة بهم.

إذا كنت بحاجة إلى تأمين نموذج الذكاء الاصطناعي، احتفِظ به على الخادم. بعد تخزين البيانات على الجهاز، يصبح من السهل استخراجها من كلّ من ذاكرة التخزين المؤقت وIndexedDB باستخدام &quot;أدوات المطوّرين&quot; أو إضافة &quot;أدوات المطوّرين&quot; في OFPS. تتساوى واجهات برمجة التطبيقات الخاصة بالتخزين هذه في مستوى الأمان. قد تميل إلى تخزين نسخة مشفّرة من النموذج، ولكنك ستحتاج بعد ذلك إلى الحصول على مفتاح فك التشفير وإرساله إلى العميل، ما قد يؤدي إلى اعتراض المفتاح. وهذا يعني أنّ محاولة الجهات السيئة سرقة نموذجك ستكون أصعب قليلاً، ولكنها ليست مستحيلة.

ننصحك باختيار استراتيجية تخزين مؤقت تتوافق مع متطلبات تطبيقك وسلوك الجمهور المستهدَف وخصائص نماذج الذكاء الاصطناعي المستخدَمة. يضمن ذلك أن تكون تطبيقاتك سريعة الاستجابة وقوية في ظل ظروف الشبكة وقيود النظام المختلفة.


الإقرارات

راجع هذا المستند كل من جوشوا بيل ورايلي غرانت وإيفان ستاد وناثان ميموت وأوستن سوليفان وإتيان نويل وأندريه باندارا وألكسندرا كليبر وفرانسوا بوفورت وبول كينلان وراشيل أندرو.