แคชโมเดล AI ในเบราว์เซอร์

โมเดล AI ส่วนใหญ่มีสิ่งที่เหมือนกันอย่างน้อย 1 อย่างนั่นคือ ค่อนข้างใหญ่สำหรับทรัพยากรที่ ผ่านอินเทอร์เน็ต โมเดลการตรวจจับออบเจ็กต์ MediaPipe ที่มีขนาดเล็กที่สุด (SSD MobileNetV2 float16) หนัก 5.6 MB และขนาดที่ใหญ่ที่สุดอยู่ที่ประมาณ 25 MB

LLM โอเพนซอร์ส gemma-2b-it-gpu-int4.bin นาฬิกาที่ขนาด 1.35 GB ซึ่งถือว่าน้อยมากสำหรับ LLM โมเดล Generative AI สามารถมีขนาดมหาศาล นี่จึงเป็นสาเหตุที่ปัจจุบันมีการใช้ AI อย่างล้นหลาม ในระบบคลาวด์ แอปใช้โมเดลที่มีการเพิ่มประสิทธิภาพสูงโดยตรงมากขึ้นเรื่อยๆ ในอุปกรณ์ ขณะการสาธิตการทำงานของ LLM ในเบราว์เซอร์ ต่อไปนี้เป็นตัวอย่างระดับเวอร์ชันที่ใช้งานจริงของโมเดลอื่นๆ ที่ใช้งานใน เบราว์เซอร์:

Adobe Photoshop บนเว็บที่เปิดเครื่องมือการเลือกวัตถุที่ทำงานด้วยระบบ AI ซึ่งเลือกวัตถุไว้ 3 ชิ้น ได้แก่ ยีราฟ 2 ตัวและดวงจันทร์

หากต้องการให้แอปพลิเคชันของคุณเปิดขึ้นในอนาคตเร็วขึ้น คุณควรทำแคช ข้อมูลโมเดลในอุปกรณ์ แทนที่จะใช้เบราว์เซอร์ HTTP โดยนัย แคช

ขณะที่คู่มือนี้ใช้ gemma-2b-it-gpu-int4.bin model ในการสร้างแชทบ็อต วิธีการนี้สามารถนำไปปรับให้เหมาะสมกับโมเดลอื่นๆ และกรณีการใช้งานอื่นๆ ได้ ในอุปกรณ์ วิธีที่ใช้กันมากที่สุดในการเชื่อมต่อแอปกับโมเดลคือการให้บริการ ควบคู่ไปกับทรัพยากรที่เหลือของแอป การเพิ่มประสิทธิภาพ

กำหนดค่าส่วนหัวของแคชที่ถูกต้อง

หากคุณแสดงโมเดล AI จากเซิร์ฟเวอร์ คุณต้องกำหนดค่า Cache-Control ส่วนหัว ตัวอย่างต่อไปนี้แสดงการตั้งค่าเริ่มต้นที่มีประสิทธิภาพ ซึ่งคุณสามารถสร้าง ตามความต้องการของแอป

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

เวอร์ชันที่เปิดตัวของโมเดล AI แต่ละเวอร์ชันเป็นทรัพยากรแบบคงที่ เนื้อหาที่ไม่ การเปลี่ยนแปลงควรได้รับ max-age รวมกับการป้องกันแคช ใน URL ของคำขอ หากต้องการอัปเดตโมเดล คุณต้อง ให้ URL ใหม่

เมื่อผู้ใช้โหลดหน้านี้ซ้ำ ไคลเอ็นต์จะส่งคำขอตรวจสอบอีกครั้ง แต่เซิร์ฟเวอร์จะรู้ว่าเนื้อหาเสถียร immutable ระบุไว้อย่างชัดเจนว่าการตรวจสอบความถูกต้องอีกครั้งนั้นไม่จำเป็นเนื่องจาก เนื้อหาจะไม่เปลี่ยนแปลง คำสั่ง immutable คือ ไม่ได้รับการสนับสนุนในวงกว้าง เบราว์เซอร์และแคชที่เป็นตัวกลางหรือพร็อกซีเซิร์ฟเวอร์ แต่โดย การรวมผลิตภัณฑ์กับ เป็นที่เข้าใจกันโดยทั่วไปเกี่ยวกับคำสั่ง max-age คุณจะมั่นใจได้ว่า ความสามารถในการใช้งานร่วมกัน public คำสั่งการตอบกลับระบุว่าสามารถเก็บการตอบกลับไว้ในแคชที่แชร์

วันที่
เครื่องมือสำหรับนักพัฒนาเว็บใน Chrome แสดงเวอร์ชันที่ใช้งานจริงCache-Control ส่วนหัวที่ส่งโดย Hugging Face เมื่อขอโมเดล AI (แหล่งที่มา)

แคชโมเดล AI ฝั่งไคลเอ็นต์

เมื่อแสดงโมเดล AI คุณต้องแคชโมเดลอย่างชัดเจนใน เบราว์เซอร์ การดำเนินการนี้ช่วยให้มั่นใจได้ว่าข้อมูลโมเดลจะพร้อมใช้งานหลังจากที่ผู้ใช้โหลดซ้ำ แอปนั้น

มีเทคนิคมากมายที่คุณสามารถใช้เพื่อให้บรรลุเป้าหมายนี้ สำหรับ ให้สมมติว่าไฟล์โมเดลแต่ละไฟล์ถูกเก็บไว้ใน ออบเจ็กต์ Blob ชื่อ blob ในหน่วยความจำ

เพื่อให้เข้าใจประสิทธิภาพ ตัวอย่างโค้ดแต่ละรายการจะมีคำอธิบายประกอบ performance.mark() และ performance.measure() มาตรการเหล่านี้ขึ้นอยู่กับอุปกรณ์และไม่สามารถทำให้เป็นแบบทั่วไปได้

วันที่
ใน Chrome DevTools แอปพลิเคชัน > พื้นที่เก็บข้อมูล โปรดตรวจสอบ แผนภาพการใช้งานที่มีเซกเมนต์สำหรับ IndexedDB, พื้นที่เก็บข้อมูลแคช และระบบไฟล์ แต่ละกลุ่มใช้ข้อมูล 1,354 เมกะไบต์ ซึ่งรวมกันแล้วเท่ากับ 4,063 เมกะไบต์

คุณเลือกใช้ API ต่อไปนี้เพื่อแคชโมเดล AI ในเบราว์เซอร์ได้ Cache API Origin Private File System API และ IndexedDB API คำแนะนำทั่วไปคือให้ใช้ API แคช แต่คู่มือนี้จะพูดถึงข้อดีและข้อเสียของ ตัวเลือกทั้งหมด

API แคช

Cache API มี พื้นที่เก็บข้อมูลถาวรสำหรับ Request และออบเจ็กต์ Response และแคชไว้ในหน่วยความจำที่ใช้ได้นาน แม้ว่า ในข้อมูลจำเพาะของ Service Workers คุณจะใช้ API นี้จากเทรดหลักหรือผู้ปฏิบัติงานทั่วไปก็ได้ เพื่อใช้ภายนอก บริบทของโปรแกรมทำงานของบริการ เรียก เมธอด 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;
  }
};

วิธีการเหล่านี้ใช้ร่วมกันไม่ได้ อาจมีกรณีที่ทั้งสองฝ่าย แคชรุ่นในเบราว์เซอร์ให้ชัดเจน และใช้โมเดลจากฮาร์ดดิสก์ของผู้ใช้

สาธิต

คุณสามารถดูวิธีจัดเก็บเคสปกติทั้ง 3 วิธีและวิธีที่ใช้ฮาร์ดดิสก์ ที่ใช้ในการสาธิต MediaPipe LLM

โบนัส: ดาวน์โหลดไฟล์ขนาดใหญ่เป็นกลุ่ม

หากต้องการดาวน์โหลดโมเดล AI ขนาดใหญ่จากอินเทอร์เน็ต ให้โหลด ดาวน์โหลดเป็นส่วนย่อยๆ แล้วต่อเข้ากับไคลเอ็นต์อีกครั้ง

นี่คือฟังก์ชันตัวช่วยที่คุณใช้ในโค้ดได้ คุณต้องสอบผ่าน ซึ่งก็คือ url chunkSize (ค่าเริ่มต้น: 5 MB), 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 เป็นตัวเลือกที่ใช้งานได้น้อยกว่า OPFS และ API ของ IndexedDB ต้องเรียงลำดับข้อมูลก่อนจึงจะจัดเก็บได้ IndexedDB ยังต้อง ทำการดีซีเรียลไลซ์ข้อมูลเมื่อดึงมา ทำให้เป็นตำแหน่งที่แย่ที่สุดในการเก็บข้อมูล โมเดลขนาดใหญ่

สำหรับแอปพลิเคชันเฉพาะกลุ่ม File System Access API จะช่วยให้คุณเข้าถึงไฟล์ได้โดยตรง ในอุปกรณ์ของผู้ใช้ ซึ่งเหมาะสำหรับผู้ใช้ที่จัดการโมเดล AI ของตนเอง

หากต้องการรักษาโมเดล AI ของคุณให้ปลอดภัย ให้เก็บโมเดลไว้บนเซิร์ฟเวอร์ เมื่อจัดเก็บไว้ใน การดึงข้อมูลจากทั้ง Cache และ IndexedDB ด้วย DevTools หรือส่วนขยาย OFPS DevTools API พื้นที่เก็บข้อมูลเหล่านี้มีการรักษาความปลอดภัยอย่างเท่าเทียมกันโดยธรรมชาติ คุณอาจอยากลองใช้ จัดเก็บโมเดลเวอร์ชันที่เข้ารหัสไว้ แต่จากนั้นคุณจะต้องได้รับการถอดรหัส ไปยังไคลเอ็นต์ซึ่งอาจถูกดักจับได้ นี่หมายถึงความพยายามของผู้ไม่ประสงค์ดี ในการขโมยโมเดลของคุณนั้นยากขึ้นเล็กน้อย แต่ก็เป็นไปไม่ได้

เราขอแนะนำให้คุณเลือกกลยุทธ์การแคชที่สอดคล้องกับ ข้อกำหนด พฤติกรรมของกลุ่มเป้าหมาย และลักษณะของโมเดล AI ซึ่งช่วยให้มั่นใจได้ว่าแอปพลิเคชันของคุณจะมีการตอบสนองและมีประสิทธิภาพภายใต้ เงื่อนไขของเครือข่ายและข้อจำกัดของระบบ


กิตติกรรมประกาศ

รีวิวนี้ได้รับการตรวจสอบโดย Joshua Bell, Reilly Grant, Evan Stade, Nathan Memmott Austin Sullivan, Etienne Noël, André Bandarra, Alexandra Klepper François Beaufort, Paul Kinlan และ Rachel Andrew