Tarayıcıda AI modellerini önbelleğe al

Çoğu yapay zeka modelinin ortak bir özelliği vardır: İnternet üzerinden aktarılan bir kaynak için oldukça büyüktürler. En küçük MediaPipe nesne algılama modeli (SSD MobileNetV2 float16) 5,6 MB, en büyük model ise yaklaşık 25 MB ağırlığındadır.

Açık kaynaklı LLM gemma-2b-it-gpu-int4.bin, 1,35 GB'tır ve bu, bir LLM için çok küçük bir boyut olarak kabul edilir. Üretken yapay zeka modelleri çok büyük olabilir. Bu nedenle, günümüzde yapay zeka kullanımının büyük bir kısmı bulutta gerçekleşir. Uygulamalar, giderek artan bir şekilde doğrudan cihaz üzerinde yüksek düzeyde optimize edilmiş modeller çalıştırıyor. Tarayıcıda çalışan LLM'lerin demoları olsa da tarayıcıda çalışan diğer modellerin üretime hazır bazı örneklerini aşağıda bulabilirsiniz:

Yapay zeka destekli nesne seçme aracı açıkken web'de Adobe Photoshop. Üç nesne seçilmiş: iki zürafa ve bir ay.

Uygulamalarınızın gelecekteki lansmanlarını hızlandırmak için model verilerini örtülü HTTP tarayıcı önbelleğine güvenmek yerine cihazda açıkça önbelleğe almanız gerekir.

Bu kılavuzda chatbot oluşturmak için gemma-2b-it-gpu-int4.bin model kullanılsa da yaklaşım, cihaz üzerinde diğer modeller ve kullanım alanlarına uyacak şekilde genelleştirilebilir. Bir uygulamayı modele bağlamanın en yaygın yolu, modeli uygulamanın diğer kaynaklarıyla birlikte sunmaktır. Yayın optimizasyonu çok önemlidir.

Doğru önbellek üstbilgilerini yapılandırma

Sunucunuzdan yapay zeka modelleri sunuyorsanız doğru Cache-Control üstbilgisini yapılandırmanız önemlidir. Aşağıdaki örnekte, uygulamanızın ihtiyaçlarına göre geliştirebileceğiniz sağlam bir varsayılan ayar gösterilmektedir.

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

Yapay zeka modelinin yayınlanan her sürümü statik bir kaynaktır. Hiç değişmeyen içeriklere, istek URL'sinde önbellek temizleme ile birlikte uzun bir max-age verilmelidir. Modeli güncellemeniz gerekiyorsa yeni bir URL vermeniz gerekir.

Kullanıcı sayfayı yeniden yüklediğinde, sunucu içeriğin sabit olduğunu bilse bile istemci yeniden doğrulama isteği gönderir. immutable yönergesi, içerik değişmeyeceği için yeniden doğrulamanın gereksiz olduğunu açıkça belirtir. immutable yönergesi, tarayıcılar ve aracı önbellek ya da proxy sunucular tarafından yaygın olarak desteklenmez. Ancak bu yönergeyi, evrensel olarak anlaşılan max-age yönergesiyle birleştirerek maksimum uyumluluk sağlayabilirsiniz. public yanıt yönergesi, yanıtın paylaşılan bir önbellekte depolanabileceğini gösterir.

Chrome Geliştirici Araçları, bir yapay zeka modeli istenirken Hugging Face tarafından gönderilen üretim Cache-Control başlıklarını gösterir. (Kaynak)

Yapay zeka modellerini istemci tarafında önbelleğe alma

Bir yapay zeka modelini sunduğunuzda, modeli tarayıcıda açıkça önbelleğe almanız önemlidir. Bu sayede, kullanıcı uygulamayı yeniden yükledikten sonra model verilerinin hemen kullanılabilmesi sağlanır.

Bunu sağlamak için kullanabileceğiniz çeşitli teknikler vardır. Aşağıdaki kod örneklerinde, her model dosyasının bellekte blob adlı bir Blob nesnesinde depolandığını varsayın.

Performansı anlamak için her kod örneği performance.mark() ve performance.measure() yöntemleriyle açıklanmıştır. Bu önlemler cihaza bağlıdır ve genelleştirilemez.

Chrome Geliştirici Araçları Uygulama > Depolama bölümünde, IndexedDB, önbellek depolama ve dosya sistemi segmentlerini içeren kullanım şemasını inceleyin. Her segmentin 1.354 megabayt veri kullandığı gösteriliyor. Bu da toplamda 4.063 megabayt ediyor.

Tarayıcıda yapay zeka modellerini önbelleğe almak için aşağıdaki API'lerden birini kullanabilirsiniz: Cache API, Origin Private File System API ve IndexedDB API. Genel olarak Cache API'nin kullanılması önerilir ancak bu kılavuzda tüm seçeneklerin avantajları ve dezavantajları ele alınmaktadır.

Cache API

Cache API, uzun süreli bellekte önbelleğe alınan Request ve Response nesne çiftleri için kalıcı depolama alanı sağlar. Service Workers spesifikasyonunda tanımlanmış olsa da bu API'yi ana iş parçacığından veya normal bir işçiden kullanabilirsiniz. Bu yöntemi bir hizmet çalışanı bağlamı dışında kullanmak için Cache.put() yöntemini Request nesnesi yerine sentetik bir URL ile eşleştirilmiş sentetik bir Response nesnesiyle çağırın.

Bu kılavuzda, bellek içi blob olduğu varsayılmaktadır. Önbellek anahtarı olarak sahte bir URL ve blob temelinde sentetik bir Response kullanın. Modeli doğrudan indirmek isterseniz fetch() isteği göndererek elde ettiğiniz Response kullanırsınız.

Örneğin, bir model dosyasını Cache API ile depolama ve geri yükleme işlemi aşağıda açıklanmıştır.

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), depolama uç noktası için nispeten yeni bir standarttır. Sayfanın kaynağına özel olduğundan, normal dosya sisteminin aksine kullanıcı tarafından görülemez. Performans için son derece optimize edilmiş özel bir dosyaya erişim sağlar ve içeriğine yazma erişimi sunar.

Örneğin, bir model dosyasını OPFS'de depolama ve geri yükleme adımlarını aşağıda bulabilirsiniz.

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, tarayıcıda rastgele verileri kalıcı olarak depolamak için iyi bilinen bir standarttır. Biraz karmaşık API'siyle kötü şöhretli olsa da idb-keyval gibi bir sarmalayıcı kitaplık kullanarak IndexedDB'yi klasik bir anahtar/değer deposu gibi kullanabilirsiniz.

Örneğin:

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

Depolama alanını kalıcı olarak işaretleme

Kalıcı depolama alanını kullanma izni istemek için bu önbelleğe alma yöntemlerinin herhangi birinin sonunda navigator.storage.persist() işlevini çağırın. Bu yöntem, izin verildiyse true olarak çözümlenen bir söz, aksi takdirde false olarak çözümlenen bir söz döndürür. Tarayıcıya özel kurallara bağlı olarak tarayıcı isteği kabul edebilir veya etmeyebilir.

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

Özel durum: Sabit diskte model kullanma

Tarayıcı depolama alanına alternatif olarak, yapay zeka modellerine doğrudan kullanıcının sabit diskinden başvurabilirsiniz. Bu teknik, araştırma odaklı uygulamaların belirli modelleri tarayıcıda çalıştırmanın mümkün olduğunu göstermesine veya sanatçıların kendi eğitilmiş modellerini uzman yaratıcılık uygulamalarında kullanmasına yardımcı olabilir.

File System Access API

File System Access API ile sabit diskteki dosyaları açabilir ve IndexedDB'de kalıcı hale getirebileceğiniz bir FileSystemFileHandle elde edebilirsiniz.

Bu kalıbı kullandığınızda kullanıcının model dosyasına yalnızca bir kez erişim izni vermesi gerekir. Kalıcı izinler sayesinde kullanıcı, dosyaya kalıcı olarak erişim izni vermeyi seçebilir. Uygulama yeniden yüklendikten ve fare tıklaması gibi gerekli bir kullanıcı hareketi yapıldıktan sonra FileSystemFileHandle, sabit diskteki dosyaya erişimle birlikte IndexedDB'den geri yüklenebilir.

Dosya erişim izinleri sorgulanır ve gerekirse istenir. Bu sayede, gelecekteki yeniden yüklemeler sorunsuz bir şekilde gerçekleştirilir. Aşağıdaki örnekte, sabit diskteki bir dosyanın tanıtıcısının nasıl alınacağı, ardından tanıtıcının nasıl depolanacağı ve geri yükleneceği gösterilmektedir.

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

Bu yöntemler birbirini dışlamaz. Hem tarayıcıda açıkça bir modeli önbelleğe aldığınız hem de kullanıcının sabit diskindeki bir modeli kullandığınız durumlar olabilir.

Demo

MediaPipe LLM demosunda uygulanan üç normal durum depolama yönteminin ve sabit disk yönteminin tümünü görebilirsiniz.

Bonus: Büyük bir dosyayı parçalar halinde indirme

İnternetten büyük bir yapay zeka modeli indirmeniz gerekiyorsa indirme işlemini ayrı parçalar halinde paralel olarak yapın, ardından istemcide tekrar birleştirin.

Kodunuzda kullanabileceğiniz bir yardımcı işlevi aşağıda verilmiştir. Yalnızca url değerini iletmeniz gerekir. chunkSize (varsayılan: 5 MB), maxParallelRequests (varsayılan: 6), progressCallback işlevi (downloadedBytes ve toplam fileSize hakkında rapor verir) ve AbortSignal sinyali için signal isteğe bağlıdır.

Projenize aşağıdaki işlevi kopyalayabilir veya npm'den fetch-in-chunks paketini yükleyebilirsiniz.

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;

Size en uygun yöntemi seçin

Bu kılavuzda, yapay zeka modellerini tarayıcıda etkili bir şekilde önbelleğe almanın çeşitli yöntemleri ele alınmıştır. Bu görev, uygulamanızın performansını ve kullanıcı deneyimini iyileştirmek için çok önemlidir. Chrome depolama ekibi, yapay zeka modellerine hızlı erişim sağlamak, yükleme sürelerini azaltmak ve yanıt verme hızını artırmak için en iyi performans açısından Cache API'yi önerir.

OPFS ve IndexedDB daha az kullanılabilir seçeneklerdir. OPFS ve IndexedDB API'lerinin, verileri depolamadan önce serileştirmesi gerekir. IndexedDB'nin, veriler alındığında seri durumdan çıkarma işlemi yapması da gerekir. Bu nedenle, büyük modelleri depolamak için en kötü yerdir.

Niş uygulamalar için File System Access API, kullanıcının cihazındaki dosyalara doğrudan erişim sunar. Bu API, kendi yapay zeka modellerini yöneten kullanıcılar için idealdir.

Yapay zeka modelinizin güvenliğini sağlamanız gerekiyorsa sunucuda tutun. Veriler istemcide depolandıktan sonra, DevTools veya OFPS DevTools uzantısı ile hem önbellekten hem de IndexedDB'den kolayca çıkarılabilir. Bu depolama API'leri güvenlik açısından eşit derecede güvenlidir. Modelin şifrelenmiş bir sürümünü depolamak isteyebilirsiniz ancak bu durumda, şifre çözme anahtarını istemciye göndermeniz gerekir. Bu anahtar, saldırıya uğrayabilir. Bu, kötü niyetli kişilerin modelinizi çalma girişimlerinin biraz daha zor olacağı ancak imkansız olmayacağı anlamına gelir.

Uygulamanızın gereksinimleri, hedef kitlenin davranışı ve kullanılan yapay zeka modellerinin özellikleriyle uyumlu bir önbelleğe alma stratejisi seçmenizi öneririz. Bu sayede uygulamalarınız çeşitli ağ koşullarında ve sistem kısıtlamalarında duyarlı ve sağlam olur.


Teşekkür

Bu makale; Joshua Bell, Reilly Grant, Evan Stade, Nathan Memmott, Austin Sullivan, Etienne Noël, André Bandarra, Alexandra Klepper, François Beaufort, Paul Kinlan ve Rachel Andrew tarafından incelenmiştir.