브라우저에서 AI 모델 캐시

대부분의 AI 모델에는 적어도 한 가지 공통점이 있습니다. 상당히 큽니다. 인터넷을 통해 전송됩니다. 가장 작은 MediaPipe 객체 감지 모델 (SSD MobileNetV2 float16) 무게 5.6MB 가장 큰 크기는 약 25MB입니다

오픈소스 LLM은 gemma-2b-it-gpu-int4.bin 드림 1.35GB로 불과하며 LLM에서는 매우 작은 것으로 여겨집니다. 생성형 AI 모델은 엄청날 수 있습니다. 이것이 오늘날 AI가 많이 사용되는 이유입니다. 사용할 수 있습니다 점점 더 많은 앱에서 고도로 최적화된 모델을 직접 실행하고 있습니다. 해야 합니다. 브라우저에서 실행되는 LLM의 데모는 Cloud Shell에서 실행되는 다른 모델의 몇 가지 프로덕션 등급 예시를 브라우저:

웹용 Adobe Photoshop에서 AI 기반 개체 선택 도구가 열려 있고 3개의 개체(기린 두 개와 달)가 선택되어 있습니다.

향후 애플리케이션을 더 빠르게 실행하려면, 암시적 HTTP 브라우저에 의존하지 않고 기기 내의 모델 데이터를 있습니다.

이 가이드에서는 gemma-2b-it-gpu-int4.bin model를 사용하여 챗봇을 만듭니다. 다른 모델 및 사용 사례에 맞게 일반화할 수 있습니다. 해야 합니다. 앱을 모델에 연결하는 가장 일반적인 방법은 모델을 빌드해야 합니다 사용 가능한 모든 광고 형식을 배달.

올바른 캐시 헤더 구성

서버에서 AI 모델을 서빙하는 경우 올바른 모델 Cache-Control 드림 헤더를 클릭하세요. 다음 예는 견고한 기본 설정을 보여주며, 몇 가지 옵션을 제공합니다

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

AI 모델의 각 출시 버전은 정적 리소스입니다. 절대 변경 시 긴 시간 동안 max-age 드림 캐시 무효화와 결합됨 사용할 수 없습니다. 모델을 업데이트해야 하는 경우 새 URL을 입력합니다.

사용자가 페이지를 새로고침하면 클라이언트는 서버는 콘텐츠가 안정적임을 알고 있습니다. 이 immutable 드림 지시어는 재검증이 필요하지 않음을 명시적으로 나타냅니다. 변하지 않습니다 immutable 지시어는 다음과 같습니다. 널리 지원되지 않음 브라우저나 중간 캐시 또는 프록시 서버에 의해 전송될 수 있지만 이를 결합하여 max-age 지시문을 사용하여 호환성을 제공합니다 public 응답 지시문은 응답을 공유 캐시에 저장할 수 있음을 나타냅니다.

<ph type="x-smartling-placeholder">
</ph>
Chrome DevTools가 프로덕션 Cache-Control을 표시합니다. AI 모델을 요청할 때 Hugging Face에서 전송한 헤더 (출처)

클라이언트 측에서 AI 모델 캐시

AI 모델을 제공할 때는 있습니다. 이렇게 하면 사용자가 새로고침한 후 모델 데이터를 즉시 사용할 수 있습니다. 앱

이를 위해 사용할 수 있는 여러 가지 기법이 있습니다. 다음에 해당 각 모델 파일이 한 시스템에 저장되어 있다고 가정할 때 이름이 blobBlob 객체 메모리에 저장됩니다

성능을 이해하기 위해 각 코드 샘플에는 performance.mark() 드림 및 performance.measure() 메서드를 참조하세요. 이러한 측정값은 기기에 따라 다르며 일반화할 수 없습니다.

<ph type="x-smartling-placeholder">
</ph>
Chrome DevTools 애플리케이션에서 > 스토리지, 검토 IndexedDB, 캐시 스토리지 및 File System 세그먼트가 포함된 사용 다이어그램 각 세그먼트는 1,354MB의 데이터를 소비하며, 총합은 4,063입니다. 메가바이트급입니다.

다음 API 중 하나를 사용하여 브라우저에서 AI 모델을 캐시할 수 있습니다. 캐시 API를 사용하면 Origin Private File System API IndexedDB API를 사용합니다. 일반적으로 Cache API를 참조하지만 이 가이드에서는 API의 장단점을 설명합니다. 있습니다.

캐시 API

Cache APIRequest용 영구 스토리지 및 Response 객체 메모리 쌍의 한 쌍입니다. 비록 서비스 워커 사양에 정의된 대로 기본 스레드 또는 일반 작업자에서 이 API를 사용할 수 있습니다. 실외에서 사용하기 위해 서비스 워커 컨텍스트를 참조하려면 Cache.put() 메서드 를 합성 Response 객체와 함께 Request 객체

이 가이드에서는 인메모리 blob를 사용한다고 가정합니다. 가짜 URL을 캐시 키로 사용하고 blob를 기반으로 하는 합성 Response입니다. 이 파일을 직접 다운로드해서 fetch()를 만들 때 얻는 Response를 사용합니다. 합니다.

다음은 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 임의의 데이터를 영구적인 방식으로 저장하기 위한 잘 확립된 표준입니다. 를 클릭합니다. 다소 복잡한 API로 잘 알려져 있지만, 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를 유지할 수 있습니다.

이 패턴을 사용하면 사용자가 모델 파일에 대한 액세스 권한만 부여하면 됩니다. 합니다. 지속된 권한 덕분에 사용자가 파일에 대한 액세스 권한을 영구적으로 부여하도록 선택할 수 있습니다. 필수 사용자 동작(예: 마우스 클릭, 사용자 동작)을 파일에 대한 액세스 권한이 있는 IndexedDB에서 FileSystemFileHandle 파일을 복원할 수 있습니다. 하드 디스크에 있어야 합니다.

파일 액세스 권한이 쿼리되고 필요한 경우 요청되므로, 향후 재충전을 위해 이를 원활하게 사용할 수 있습니다 다음 예는 핸들을 저장하고 복원합니다.

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

이러한 메서드는 상호 배타적이지 않습니다. 경우에 따라 이해관계자와 브라우저에서 모델을 명시적으로 캐시하고 사용자 하드 디스크에 있는 모델을 사용할 수 있습니다.

데모

세 가지 일반 케이스 저장 방법과 하드 디스크 방법을 모두 볼 수 있습니다. MediaPipe LLM 데모에서 구현됩니다.

를 탭합니다.

보너스: 대용량 파일을 청크로 다운로드

인터넷에서 대규모 AI 모델을 다운로드해야 하는 경우 여러 청크로 다운로드한 다음 클라이언트에서 다시 스티칭할 수 있어야 합니다.

다음은 코드에 사용할 수 있는 도우미 함수입니다. 다음을 통과해야 합니다. url입니다. chunkSize (기본값: 5MB), maxParallelRequests (기본값: 6), progressCallback 함수 (기본값: 6)를 downloadedBytes 및 합계 fileSize), 그리고 signal AbortSignal 신호는 모두 선택사항입니다.

프로젝트에서 다음 함수를 복사할 수 있습니다. npm 패키지에서 fetch-in-chunks 패키지를 설치합니다.

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 스토리지 팀은 AI 모델에 대한 빠른 액세스를 보장하고 로드 시간을 단축합니다. 응답성을 개선할 수 있습니다

OPFS 및 IndexedDB는 사용이 적은 옵션입니다. OPFS 및 IndexedDB API 직렬화해야 데이터를 저장할 수 있기 때문입니다. IndexedDB 역시 가져올 때 데이터를 역직렬화하여 저장하기에 최악의 장소가 됨 살펴보겠습니다

틈새 애플리케이션의 경우 File System Access API는 파일에 대한 직접 액세스를 제공함 할 수 있으며, 자체 AI 모델을 관리하는 사용자에게 적합합니다.

AI 모델을 보호해야 하는 경우 서버에 보관하세요. Cloud Storage에 저장되면 클라이언트인 경우 Cache와 IndexedDB 모두에서 데이터를 추출하는 작업은 DevTools 또는 OFPS DevTools 확장 프로그램. 이러한 Storage API의 보안은 본질적으로 동일합니다. 아마도 모델의 암호화된 버전을 저장하지만 그런 다음에는 사용자가 직접 암호화하여 가로채기될 수 있습니다. 이는 악의적인 행위자의 시도가 모델을 훔치는 것이 약간 더 어렵지만 불가능하지는 않습니다.

앱의 스타일에 맞는 캐싱 전략을 선택하는 것이 좋습니다. AI 모델의 특징, 요구사항, 타겟층 행동 있습니다. 이를 통해 애플리케이션이 다양한 상황에서 반응하고 견고하게 작동하도록 할 수 있습니다. 네트워크 상태 및 시스템 제약 조건을 적용할 수 있습니다


감사의 말씀

Joshua Bell, Reilly Grant, Evan Stade, Nathan Memmott, 오스틴 설리반, 에티엔 노엘, 앙드레 반다라, 알렉산드라 클레퍼, 프랑수아 보퍼트, 폴 킨란, 레이첼 앤드류입니다.