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

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

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

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

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

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

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

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

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

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

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

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

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

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

واجهة برمجة تطبيقات ذاكرة التخزين المؤقت

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

واجهة برمجة تطبيقات نظام الملفات الخاصة المصدر

إنّ نظام الملفات الخاص المنشأ (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

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

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

يمكنك الرجوع إلى نماذج الذكاء الاصطناعي (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;
  }
};

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

عرض توضيحي

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

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

إذا كنت بحاجة إلى تنزيل نموذج ذكاء اصطناعي (AI) كبير من الإنترنت، عليك موازاة عملية التنزيل في أجزاء منفصلة، ثم ضمها معًا مرّة أخرى على العميل.

إليك دالة مساعدة يمكنك استخدامها في التعليمة البرمجية. ما عليك سوى تمرير 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 إمكانية الوصول المباشر إلى الملفات على جهاز المستخدم، وهي طريقة مثالية للمستخدمين الذين يديرون نماذج الذكاء الاصطناعي (AI) الخاصة بهم.

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

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


شكر وتقدير

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