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

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

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

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

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

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

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

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

Cache API

توفّر Cache API مساحة تخزين دائمة لزوجَي Request وResponse العناصر التي يتم تخزينها مؤقتًا في ذاكرة دائمة. على الرغم من أنّه محدَّد في مواصفات مشغّل الخدمات، يمكنك استخدام واجهة برمجة التطبيقات هذه من سلسلة التعليمات الرئيسية أو من عامل عادي. ولاستخدامه خارج سياق مشغّل الخدمات، يمكنك استدعاء طريقة Cache.put() مع كائن Response اصطناعي، مقترن بعنوان URL اصطناعي بدلاً من عنصر Request.

يفترض هذا الدليل وجود blob في الذاكرة. استخدِم عنوان URL مزيّفًا كمفتاح ذاكرة التخزين المؤقت وResponse اصطناعيًا استنادًا إلى blob. إذا أردت تنزيل ملف Response مباشرةً، عليك استخدام 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) هو معيار صغير نسبيًا لنقطة نهاية التخزين. وهي خاصة بمصدر الصفحة، وبالتالي لا تظهر للمستخدم، على عكس نظام الملفات العادي. ويوفّر هذا الملف إمكانية الوصول إلىملف 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);
  }
}

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

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

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

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


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

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