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

تتشارك معظم نماذج الذكاء الاصطناعي سمة واحدة: فهي كبيرة جدًا بالنسبة إلى مورد يتم نقله عبر الإنترنت. يزن أصغر نموذج لميزة "اكتشاف الأجسام" في 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، يمكنك استخدام واجهة برمجة التطبيقات هذه من سلسلة المهام الرئيسية أو سلسلة مهام عادية. لاستخدامه خارج سياق 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;
  }
};

Origin Private File System API

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

على سبيل المثال، في ما يلي كيفية تخزين ملف نموذج واستعادته في 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

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 من خلال الوصول إلى الملف على القرص الصلب.

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

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 باستخدام DevTools أو إضافة DevTools في OFPS. إنّ واجهات برمجة التطبيقات هذه متساوية من حيث الأمان. قد تميل إلى تخزين نسخة مشفَّرة من النموذج، ولكن عليك بعد ذلك إرسال مفتاح فك التشفير إلى العميل، ما قد يؤدي إلى اعتراضه. وهذا يعني أنّ محاولة أحد الجهات السيئة سرقة نموذجك ستكون أكثر صعوبة قليلاً، ولكن ليس مستحيلة.

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


الشكر والتقدير

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