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

โมเดล AI ส่วนใหญ่มีสิ่งหนึ่งที่เหมือนกันคือมีขนาดใหญ่พอสมควรสำหรับทรัพยากรที่โอนผ่านอินเทอร์เน็ต โมเดลการตรวจจับวัตถุ 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 response บ่งบอกว่าสามารถจัดเก็บคำตอบไว้ในแคชที่แชร์ได้

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

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

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

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

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

ในเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome แอปพลิเคชัน > พื้นที่เก็บข้อมูล ให้ตรวจสอบแผนภาพการใช้งานที่มีกลุ่มสําหรับ IndexedDB, พื้นที่เก็บข้อมูลแคช และระบบไฟล์ แต่ละกลุ่มใช้ข้อมูล 1354 เมกะไบต์ รวมเป็น 4063 เมกะไบต์

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

Cache API

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

เช่น วิธีจัดเก็บและกู้คืนไฟล์โมเดลใน 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 เป็นที่รู้จักอย่างแพร่หลายว่ามี 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 วิธีและวิธีการจัดเก็บในฮาร์ดดิสก์ได้ในการการสาธิต LLM ของ MediaPipe

โบนัส: ดาวน์โหลดไฟล์ขนาดใหญ่เป็นชิ้นๆ

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

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

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

หากต้องการรักษาความปลอดภัยให้กับโมเดล AI ให้เก็บไว้ในเซิร์ฟเวอร์ เมื่อจัดเก็บข้อมูลไว้ในไคลเอ็นต์แล้ว คุณจะดึงข้อมูลจากทั้งแคชและ IndexedDB ได้อย่างง่ายดายด้วยเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์หรือส่วนขยายเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์ OFPS 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