Meng-cache model AI di browser

Sebagian besar model AI memiliki setidaknya satu kesamaan, yaitu cukup besar untuk resource ditransfer melalui Internet. Model deteksi objek MediaPipe terkecil (SSD MobileNetV2 float16) memiliki berat 5,6 MB dan yang terbesar adalah sekitar 25 MB.

LLM open source gemma-2b-it-gpu-int4.bin memiliki kecepatan hingga 1,35 GB—dan ini dianggap sangat kecil untuk LLM. Model AI generatif bisa menjadi sangat besar. Inilah alasan banyak penggunaan AI saat ini di cloud. Makin banyak aplikasi yang menjalankan model yang sangat dioptimalkan secara langsung di perangkat. Saat demo LLM yang berjalan di browser berikut ini beberapa contoh tingkat produksi dari model lainnya yang berjalan di browser:

Adobe Photoshop di web dengan alat pemilihan objek berteknologi AI terbuka, dengan tiga objek dipilih: dua jerapah dan bulan.

Agar peluncuran aplikasi di masa mendatang lebih cepat, Anda harus melakukan cache secara eksplisit data model di perangkat, daripada mengandalkan browser HTTP implisit di cache oleh pengguna.

Meskipun panduan ini menggunakan gemma-2b-it-gpu-int4.bin model untuk membuat chatbot, pendekatan ini dapat digeneralisasi agar sesuai dengan model dan kasus penggunaan lainnya di perangkat. Cara yang paling umum untuk menghubungkan aplikasi ke model adalah dengan menyajikan bersama dengan resource aplikasi lainnya. Anda harus mengoptimalkan pengiriman.

Mengonfigurasi header cache yang tepat

Jika menyalurkan model AI dari server Anda, Anda harus mengonfigurasi Cache-Control {i>header<i}. Contoh berikut menunjukkan setelan default yang solid, yang dapat Anda bangun untuk kebutuhan aplikasi Anda.

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

Setiap versi rilis model AI adalah resource statis. Konten yang tidak pernah perubahan harus diberikan jangka max-age dikombinasikan dengan pemecahan cache di URL permintaan. Jika Anda perlu memperbarui model, Anda harus berikan URL baru.

Ketika pengguna memuat ulang halaman, klien mengirimkan permintaan validasi ulang, meskipun meskipun server mengetahui bahwa kontennya stabil. Tujuan immutable secara eksplisit menunjukkan bahwa validasi ulang tidak diperlukan, karena konten tidak akan berubah. Perintah immutable bersifat tidak didukung secara luas oleh {i>browser<i} dan {i>cache<i} perantara atau server {i>proxy<i}, tetapi dengan menggabungkan dengan atribut perintah max-age yang dipahami secara universal, Anda dapat memastikan kompatibilitas mundur. public respons menunjukkan bahwa respons dapat disimpan dalam cache bersama.

Chrome DevTools menampilkan Cache-Control produksi header yang dikirim oleh Wajah Memeluk saat meminta model AI. (Sumber)

Meng-cache model AI sisi klien

Saat menyalurkan model AI, penting untuk meng-cache model secara eksplisit di browser. Hal ini memastikan data model siap tersedia setelah pengguna memuat ulang aplikasi.

Ada sejumlah teknik yang dapat Anda gunakan untuk mencapai hal ini. Untuk yang berikut contoh kode, asumsikan setiap file model disimpan dalam Objek Blob bernama blob dalam memori.

Untuk memahami performa, setiap contoh kode dianotasi dengan performance.mark() dan performance.measure() metode. Tindakan ini bergantung pada perangkat dan tidak dapat digeneralisasi.

Di Chrome DevTools, Application > Penyimpanan, ulasan diagram penggunaan dengan segmen untuk IndexedDB, Cache storage, dan File System. Setiap segmen terbukti menggunakan data sebesar 1.354 megabita, sehingga totalnya adalah 4.063 megabita megabyte.

Anda dapat memilih untuk menggunakan salah satu API berikut untuk meng-cache model AI di browser: Cache API, Origin Private File System API, dan UI API. Rekomendasi umumnya adalah menggunakan Cache API, tetapi panduan ini membahas kelebihan dan kekurangan semua opsi.

API Cache

Cache API memberikan penyimpanan persisten untuk Request dan objek Response yang di-cache dalam memori jangka panjang. Meskipun yang ditentukan dalam spesifikasi Service Worker, Anda dapat menggunakan API ini dari thread utama atau pekerja biasa. Untuk menggunakannya di luar konteks pekerja layanan, panggil metode Metode Cache.put() dengan objek Response sintetis, dipasangkan dengan URL sintetis, bukan objek Request.

Panduan ini mengasumsikan blob dalam memori. Gunakan URL palsu sebagai kunci cache dan Response sintetis berdasarkan blob. Jika Anda ingin langsung mengunduh Anda akan menggunakan Response yang akan Anda dapatkan dari membuat fetch() permintaan.

Misalnya, berikut ini cara menyimpan dan memulihkan file model dengan 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

Sistem File Pribadi Origin (OPFS) adalah standar yang relatif muda untuk Google Cloud Storage. Atribut ini bersifat pribadi untuk asal halaman, sehingga tidak terlihat kepada pengguna, tidak seperti sistem file biasa. Health Connect menyediakan akses ke yang sangat dioptimalkan untuk kinerja dan menawarkan akses tulis ke saat ini.

Misalnya, berikut cara menyimpan dan memulihkan file model di 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 tensorflow

IndexedDB adalah standar yang mapan untuk menyimpan data arbitrer secara persisten pada browser. yang terkenal karena API-nya yang agak kompleks, tetapi dengan menggunakan library wrapper, seperti idb-keyval Anda dapat memperlakukan IndexedDB seperti penyimpanan nilai kunci klasik.

Contoh:

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

Tandai penyimpanan sebagai dipertahankan

Panggil navigator.storage.persist() di akhir salah satu metode penyimpanan dalam cache ini untuk meminta izin penggunaan dan penyimpanan persisten. Metode ini menampilkan promise yang di-resolve ke true jika izin diberikan, dan false. Browser dapat atau mungkin tidak memenuhi permintaan tersebut, bergantung pada aturan khusus browser.

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

Kasus khusus: Menggunakan model pada hard disk

Anda dapat mereferensikan model AI langsung dari hard disk pengguna sebagai alternatif ke penyimpanan browser. Teknik ini dapat membantu aplikasi yang berfokus pada penelitian menampilkan kelayakan untuk menjalankan model tertentu di browser, atau mengizinkan artis menggunakan model mandiri dalam aplikasi kreativitas dari pakar.

File System Access API

Dengan File System Access API, Anda dapat membuka file dari {i>hard disk<i} dan mendapatkan FileSystemFileHandle yang dapat Anda pertahankan ke IndexedDB.

Dengan pola ini, pengguna hanya perlu memberikan akses ke file model sekali. Berkat izin yang persisten, pengguna dapat memilih untuk memberikan akses secara permanen ke file. Setelah memuat ulang aplikasi dan gestur pengguna yang diperlukan, seperti klik mouse, FileSystemFileHandle dapat dipulihkan dari IndexedDB dengan akses ke file pada {i>hard disk<i}.

Izin akses file dikueri dan diminta jika diperlukan, yang membuat lancar untuk pemuatan ulang di masa mendatang. Contoh berikut menunjukkan cara mendapatkan menangani file dari {i>hard disk<i}, dan kemudian menyimpan dan memulihkan {i>handle<i}.

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

Metode ini tidak saling eksklusif. Mungkin ada kasus di mana Anda berdua secara eksplisit meng-cache model di browser dan menggunakan model dari hard disk pengguna.

Demo

Anda bisa melihat ketiga metode penyimpanan {i>casing<i} reguler dan metode {i>hard disk<i} yang diterapkan dalam demo LLM MediaPipe.

Bonus: Mendownload file besar dalam potongan

Jika Anda perlu mendownload model AI besar dari Internet, paralelkan diunduh menjadi potongan-potongan terpisah, kemudian digabungkan lagi pada klien.

Berikut adalah fungsi bantuan yang bisa Anda gunakan dalam kode. Anda hanya perlu lulus ini adalah url. chunkSize (default: 5 MB), maxParallelRequests (default: 6), fungsi progressCallback (yang melaporkan downloadedBytes dan total fileSize), serta signal untuk Sinyal AbortSignal bersifat opsional.

Anda dapat menyalin fungsi berikut dalam project Anda atau instal paket fetch-in-chunks dari paket 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;

Pilih metode yang tepat untuk Anda

Panduan ini telah mengeksplorasi berbagai metode untuk meng-cache model AI secara efektif dalam {i>browser<i}, tugas yang sangat penting untuk meningkatkan pengalaman pengguna dan performa aplikasi Anda. Tim penyimpanan Chrome merekomendasikan Cache API untuk performa optimal, untuk memastikan akses cepat ke model AI, sehingga mengurangi waktu pemuatan dan meningkatkan responsivitas.

OPFS dan IndexedDB adalah opsi yang kurang dapat digunakan. OPFS dan IndexedDB API perlu melakukan serialisasi data sebelum dapat disimpan. BYOD juga perlu melakukan deserialisasi data ketika diambil, menjadikannya tempat terburuk untuk menyimpan model besar.

Untuk aplikasi khusus, File System Access API menawarkan akses langsung ke file di perangkat pengguna, yang ideal bagi pengguna yang mengelola model AI mereka sendiri.

Jika Anda perlu mengamankan model AI, simpan model tersebut di server. Setelah disimpan di gampang untuk mengekstrak data dari {i>Cache<i} dan pemirsa dengan DevTools atau ekstensi OFPS DevTools. API penyimpanan ini pada dasarnya sama dalam hal keamanan. Anda mungkin tergoda untuk menyimpan versi terenkripsi dari model, tetapi Anda kemudian perlu mendapatkan dekripsi kunci ke klien, yang bisa dicegat. Ini berarti upaya pihak tidak bertanggung jawab mencuri model Anda sedikit lebih sulit, tetapi bukan tidak mungkin.

Sebaiknya Anda memilih strategi penyimpanan dalam cache yang sesuai dengan persyaratan, perilaku target audiens, dan karakteristik model AI data Ini memastikan aplikasi Anda responsif dan tangguh dalam berbagai kondisi jaringan dan batasan sistem.


Ucapan terima kasih

Ini diulas oleh Joshua Bell, Reilly Grant, Evan Stade, Nathan Memmott, Austin Sullivan, Etienne Noël, André Bandarra, Alexandra Klepper, François Beaufort, Paul Kinlan, dan Rachel Andrew.