Lưu các mô hình AI vào bộ nhớ đệm trong trình duyệt

Hầu hết mô hình AI đều có ít nhất một điểm chung, đó là: chúng tương đối lớn đối với một tài nguyên được chuyển qua Internet. Mô hình phát hiện đối tượng MediaPipe nhỏ nhất (SSD MobileNetV2 float16) có trọng lượng 5,6 MB và lớn nhất vào khoảng 25 MB.

LLM gemma-2b-it-gpu-int4.bin nguồn mở có dung lượng ở mức 1,35 GB và đây được coi là rất nhỏ đối với một LLM. Các mô hình AI tạo sinh có thể là vô cùng lớn. Đó là lý do ngày nay AI được sử dụng rất nhiều trên đám mây. Ngày càng có nhiều ứng dụng chạy các mô hình được tối ưu hoá cao trực tiếp trên thiết bị. Mặc dù bản minh hoạ của các LLM chạy trong trình duyệt tồn tại, nhưng sau đây là một số ví dụ ở cấp phiên bản chính thức về các mô hình khác chạy trong trình duyệt:

Adobe Photoshop trên web có công cụ chọn đối tượng dựa trên AI đang mở, trong đó có 3 đối tượng được chọn: hai con hươu cao cổ và một mặt trăng.

Để các ứng dụng chạy nhanh hơn trong tương lai, bạn nên lưu dữ liệu mô hình vào bộ nhớ đệm trên thiết bị một cách rõ ràng, thay vì dựa vào bộ nhớ đệm HTTP ngầm ẩn của trình duyệt.

Mặc dù hướng dẫn này sử dụng gemma-2b-it-gpu-int4.bin model để tạo bot trò chuyện, nhưng bạn có thể tổng quát hoá phương pháp này cho phù hợp với các mô hình khác và các trường hợp sử dụng khác trên thiết bị. Cách phổ biến nhất để kết nối ứng dụng với một mô hình là phân phát mô hình đó cùng với các tài nguyên còn lại của ứng dụng. Điều quan trọng là phải tối ưu hoá quá trình phân phối.

Định cấu hình tiêu đề bộ nhớ đệm phù hợp

Nếu phân phát các mô hình AI từ máy chủ của mình, bạn cần phải định cấu hình đúng tiêu đề Cache-Control. Ví dụ sau đây cho thấy một chế độ cài đặt mặc định ổn định mà bạn có thể xây dựng để đáp ứng nhu cầu của ứng dụng.

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

Mỗi phiên bản được phát hành của một mô hình AI là một tài nguyên tĩnh. Nội dung không bao giờ thay đổi nên được cung cấp một max-age dài kết hợp với tính năng chặn truy xuất bộ nhớ đệm trong URL yêu cầu. Nếu cần cập nhật mô hình, bạn phải cung cấp một URL mới cho mô hình đó.

Khi người dùng tải lại trang, ứng dụng sẽ gửi yêu cầu xác thực lại, ngay cả khi máy chủ biết rằng nội dung đã ổn định. Lệnh immutable nêu rõ rằng việc xác thực lại là không cần thiết vì nội dung sẽ không thay đổi. Lệnh immutable không được các trình duyệt và bộ nhớ đệm trung gian hoặc máy chủ proxy hỗ trợ rộng rãi, nhưng bằng cách kết hợp lệnh này với lệnh max-age mà mọi người đều hiểu được, bạn có thể đảm bảo khả năng tương thích tối đa. Lệnh phản hồi public cho biết phản hồi có thể được lưu trữ trong bộ nhớ đệm dùng chung.

Công cụ của Chrome cho nhà phát triển hiển thị các tiêu đề Cache-Control phát hành chính thức do Huging Face gửi khi yêu cầu một mô hình AI. (Nguồn)

Lưu các mô hình AI vào bộ nhớ đệm ở phía máy khách

Khi phân phát một mô hình AI (trí tuệ nhân tạo), bạn cần lưu mô hình đó vào bộ nhớ đệm một cách rõ ràng trong trình duyệt. Điều này đảm bảo dữ liệu mô hình có sẵn sau khi người dùng tải lại ứng dụng.

Có một số kỹ thuật mà bạn có thể sử dụng để đạt được điều này. Đối với các mã mẫu sau, giả sử mỗi tệp mô hình được lưu trữ trong đối tượng Blob có tên là blob trong bộ nhớ.

Để hiểu hiệu suất, mỗi mã mẫu được chú giải bằng các phương thức performance.mark()performance.measure(). Các phương pháp đo lường này phụ thuộc vào thiết bị và không thể tổng quát hoá.

Trong Ứng dụng > Bộ nhớ của Chrome cho nhà phát triển, hãy xem lại sơ đồ sử dụng với các phân đoạn cho IndexedDB, Bộ nhớ đệm và Hệ thống tệp. Mỗi phân đoạn được cho là sử dụng 1354 megabyte dữ liệu, tổng cộng là 4063 megabyte.

Bạn có thể chọn sử dụng một trong những API sau để lưu các mô hình AI vào bộ nhớ đệm trong trình duyệt: API Bộ nhớ đệm, API Hệ thống tệp riêng tư gốcAPI IndexedDB. Đề xuất chung là sử dụng API Bộ nhớ đệm, nhưng hướng dẫn này sẽ thảo luận về ưu và nhược điểm của tất cả các tuỳ chọn.

API Bộ nhớ đệm

API Bộ nhớ đệm cung cấp bộ nhớ lâu dài cho các cặp đối tượng RequestResponse được lưu vào bộ nhớ đệm trong bộ nhớ dài hạn. Tuy được định nghĩa trong thông số kỹ thuật của Trình chạy dịch vụ, nhưng bạn có thể sử dụng API này từ luồng chính hoặc một trình thực thi thông thường. Để sử dụng lớp này bên ngoài ngữ cảnh trình chạy dịch vụ, hãy gọi phương thức Cache.put() bằng đối tượng Response tổng hợp, ghép nối với một URL tổng hợp thay vì đối tượng Request.

Hướng dẫn này giả định một blob trong bộ nhớ. Sử dụng một URL giả làm khoá bộ nhớ đệm và Response tổng hợp dựa trên blob. Nếu tải trực tiếp mô hình này xuống, bạn sẽ sử dụng Response nhận được từ việc đưa ra yêu cầu fetch().

Ví dụ: dưới đây là cách lưu trữ và khôi phục tệp mô hình bằng 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 hệ thống tệp riêng tư gốc

Hệ thống tệp riêng tư gốc (OPFS) là một tiêu chuẩn còn khá trẻ, dành cho điểm cuối lưu trữ. Tệp này chỉ dành riêng cho nguồn gốc của trang nên người dùng không thể nhìn thấy, không giống như hệ thống tệp thông thường. Thư viện này cấp quyền truy cập vào một tệp đặc biệt được tối ưu hoá hiệu suất cao và cấp quyền ghi vào nội dung của tệp.

Ví dụ: dưới đây là cách lưu trữ và khôi phục tệp mô hình trong 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 là một tiêu chuẩn lâu dài để lưu trữ dữ liệu tùy ý một cách ổn định trong trình duyệt. Nổi tiếng là API khá phức tạp, nhưng bằng cách sử dụng một thư viện trình bao bọc như idb-keyval, bạn có thể coi IndexedDB như một kho lưu trữ khoá-giá trị kiểu cũ.

Ví dụ:

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

Đánh dấu bộ nhớ là đã lưu trữ

Gọi navigator.storage.persist() khi kết thúc bất kỳ phương thức lưu nào trong số này để yêu cầu quyền sử dụng bộ nhớ lâu dài. Phương thức này trả về một lời hứa sẽ phân giải thành true nếu quyền được cấp và nếu không thì sẽ phân giải false. Trình duyệt có thể thực hiện hoặc không tuân thủ yêu cầu, tuỳ thuộc vào các quy tắc dành riêng cho trình duyệt.

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

Trường hợp đặc biệt: Sử dụng mẫu trên ổ đĩa cứng

Bạn có thể tham chiếu các mô hình AI trực tiếp từ ổ đĩa cứng của người dùng để thay thế cho bộ nhớ của trình duyệt. Kỹ thuật này có thể giúp các ứng dụng tập trung vào nghiên cứu thể hiện tính khả thi của việc chạy các mô hình nhất định trong trình duyệt hoặc cho phép nghệ sĩ sử dụng các mô hình tự huấn luyện trong các ứng dụng sáng tạo chuyên gia.

API Truy cập hệ thống tệp

Với File System Access API (API Truy cập hệ thống tệp), bạn có thể mở tệp từ ổ đĩa cứng và lấy FileSystemFileHandle mà bạn có thể lưu trữ lâu dài để lập chỉ mụcDB.

Với mẫu này, người dùng chỉ cần cấp quyền truy cập vào tệp mô hình một lần. Nhờ có các quyền truy cập lâu dài, người dùng có thể chọn cấp vĩnh viễn quyền truy cập vào tệp. Sau khi tải lại ứng dụng và thực hiện một cử chỉ bắt buộc của người dùng, chẳng hạn như nhấp chuột, bạn có thể khôi phục FileSystemFileHandle từ IndexedDB bằng quyền truy cập vào tệp trên ổ đĩa cứng.

Các quyền truy cập vào tệp được truy vấn và yêu cầu nếu cần, giúp quá trình này diễn ra liền mạch cho các lần tải lại sau này. Ví dụ sau cho biết cách lấy tên người dùng cho một tệp từ ổ đĩa cứng, sau đó lưu trữ và khôi phục tên người dùng.

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

Các phương pháp này không loại trừ lẫn nhau. Có thể có trường hợp bạn vừa lưu mô hình vào bộ nhớ đệm rõ ràng trong trình duyệt vừa sử dụng một mô hình từ ổ đĩa cứng của người dùng.

Bản minh hoạ

Bạn có thể xem cả 3 phương thức lưu trữ trường hợp thông thường và phương thức ổ đĩa cứng được triển khai trong bản minh hoạ LLM MediaPipe.

Bật mí thêm cho bạn: Tải tệp lớn xuống theo từng phần

Nếu bạn cần tải một mô hình trí tuệ nhân tạo (AI) lớn từ Internet xuống, hãy tải song song tệp tải xuống thành các phần riêng biệt, sau đó ghép lại với nhau trên ứng dụng.

Sau đây là một hàm trợ giúp mà bạn có thể sử dụng trong mã của mình. Bạn chỉ cần truyền url. chunkSize (mặc định: 5MB), maxParallelRequests (mặc định: 6), hàm progressCallback (báo cáo về downloadedBytes và tổng fileSize) và signal cho tín hiệu AbortSignal đều không bắt buộc.

Bạn có thể sao chép hàm sau trong dự án hoặc cài đặt gói fetch-in-chunks từ gói 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;

Chọn phương thức phù hợp với bạn

Hướng dẫn này đã khám phá nhiều phương pháp để lưu mô hình AI vào bộ nhớ đệm một cách hiệu quả trong trình duyệt, một nhiệm vụ quan trọng giúp nâng cao trải nghiệm người dùng và hiệu suất của ứng dụng. Nhóm bộ nhớ Chrome đề xuất dùng API Cache để đạt hiệu suất tối ưu, đảm bảo truy cập nhanh vào các mô hình AI, giảm thời gian tải và cải thiện khả năng phản hồi.

OPFS và IndexedDB là các lựa chọn ít hữu dụng hơn. OPFS và IndexedDB API cần chuyển đổi tuần tự dữ liệu trước khi có thể lưu trữ. IndexedDB cũng cần khử tuần tự dữ liệu khi được truy xuất, khiến dữ liệu này trở thành nơi kém nhất để lưu trữ các mô hình lớn.

Đối với các ứng dụng thích hợp, API Truy cập hệ thống tệp cho phép truy cập trực tiếp vào các tệp trên thiết bị của người dùng, lý tưởng cho những người dùng quản lý mô hình AI của riêng họ.

Nếu bạn cần bảo mật mô hình AI, hãy giữ mô hình đó trên máy chủ. Sau khi được lưu trữ trên ứng dụng, việc trích xuất dữ liệu từ cả Bộ nhớ đệm và IndexedDB bằng công cụ cho nhà phát triển hoặc tiện ích OFPS Công cụ cho nhà phát triển không hề đơn giản. Các API lưu trữ này vốn có tính bảo mật ngang nhau. Bạn có thể muốn lưu trữ phiên bản đã mã hoá của mô hình, nhưng sau đó bạn cần nhận khoá giải mã cho ứng dụng khách có thể bị chặn. Điều này có nghĩa là đối tượng xấu sẽ khó lấy cắp mô hình của bạn hơn một chút, nhưng không phải là không thể.

Bạn nên chọn một chiến lược lưu vào bộ nhớ đệm phù hợp với các yêu cầu của ứng dụng, hành vi của đối tượng mục tiêu và đặc điểm của các mô hình AI được dùng. Điều này đảm bảo các ứng dụng của bạn có tính thích ứng và mạnh mẽ trong nhiều điều kiện mạng và quy tắc hạn chế của hệ thống.


Xác nhận

Bài đánh giá này gồm Joshua Bell, Reilly Grant, Evan Stade, Nathan Memmott, Austin Sullivan, Etienne Noël, André Bandarra, Alexandra Klepper, François Beaufort, Paul Kinlan và Rachel Andrew.