대부분의 AI 모델에는 한 가지 공통점이 있습니다. 인터넷을 통해 전송되는 리소스 중 상당히 큰 리소스라는 점입니다. 가장 작은 MediaPipe 객체 감지 모델(SSD MobileNetV2 float16
)의 크기는 5.6MB이고 가장 큰 모델의 크기는 약 25MB입니다.
오픈소스 LLM인 gemma-2b-it-gpu-int4.bin
의 크기는 1.35GB로, LLM에 비해 매우 작습니다.
생성형 AI 모델은 매우 클 수 있습니다. 이러한 이유로 오늘날 많은 AI가 클라우드에서 사용됩니다. 점점 더 많은 앱이 기기에서 직접 고도로 최적화된 모델을 실행하고 있습니다. 브라우저에서 실행되는 LLM의 데모는 있지만 다음은 브라우저에서 실행되는 다른 모델의 프로덕션 등급 예시입니다.
- Adobe Photoshop은 지능형 객체 선택 도구를 위해 기기에서
Conv2D
모델의 변형을 실행합니다. - Google Meet은 배경 흐리게 처리 기능을 위해 사람 세분화에 최적화된 버전의
MobileNetV3-small
모델을 실행합니다. - Tokopedia는 실시간 얼굴 인식을 위해
MediaPipeFaceDetector-TFJS
모델을 실행하여 서비스에 대한 잘못된 가입을 방지합니다. - Google Colab을 사용하면 사용자가 Colab 노트북에서 하드 디스크의 모델을 사용할 수 있습니다.
향후 애플리케이션을 더 빠르게 실행하려면 암시적 HTTP 브라우저 캐시를 사용하는 대신 기기에서 모델 데이터를 명시적으로 캐시해야 합니다.
이 가이드에서는 gemma-2b-it-gpu-int4.bin model
를 사용하여 챗봇을 만들지만, 이 접근 방식은 기기의 다른 모델 및 다른 사용 사례에 맞게 일반화할 수 있습니다. 앱을 모델에 연결하는 가장 일반적인 방법은 나머지 앱 리소스와 함께 모델을 제공하는 것입니다. 전송을 최적화하는 것이 중요합니다.
올바른 캐시 헤더 구성
서버에서 AI 모델을 제공하는 경우 올바른 Cache-Control
헤더를 구성하는 것이 중요합니다. 다음 예는 앱의 요구사항에 따라 빌드할 수 있는 확실한 기본 설정을 보여줍니다.
Cache-Control: public, max-age=31536000, immutable
AI 모델의 각 출시 버전은 정적 리소스입니다. 변경되지 않는 콘텐츠에는 요청 URL에 캐시 무효화와 결합된 긴 max-age
를 제공해야 합니다. 모델을 업데이트해야 하는 경우 새 URL을 지정해야 합니다.
사용자가 페이지를 새로고침하면 서버가 콘텐츠가 안정적임을 알고 있더라도 클라이언트는 유효성 재검사 요청을 보냅니다. immutable
디렉티브는 콘텐츠가 변경되지 않으므로 재검증이 불필요함을 명시적으로 나타냅니다. immutable
지시어는 브라우저와 중간 캐시 또는 프록시 서버에서 널리 지원되지는 않지만, 보편적으로 이해되는 max-age
지시어와 결합하면 최대한의 호환성을 보장할 수 있습니다. public
응답 지시문은 응답을 공유 캐시에 저장할 수 있음을 나타냅니다.
클라이언트 측에서 AI 모델 캐시
AI 모델을 제공할 때는 브라우저에 모델을 명시적으로 캐시하는 것이 중요합니다. 이렇게 하면 사용자가 앱을 새로고침한 후에도 모델 데이터를 즉시 사용할 수 있습니다.
이를 위해 사용할 수 있는 기법은 여러 가지가 있습니다. 다음 코드 샘플에서는 각 모델 파일이 메모리에 blob
라는 Blob
객체에 저장되어 있다고 가정합니다.
성능을 파악하기 위해 각 코드 샘플에는 performance.mark()
및 performance.measure()
메서드가 주석으로 추가됩니다. 이러한 측정값은 기기에 따라 다르며 일반화할 수 없습니다.
Cache API, Origin Private File System API, IndexedDB API 중 하나를 사용하여 브라우저에 AI 모델을 캐시할 수 있습니다. 일반적으로 Cache API를 사용하는 것이 좋습니다. 하지만 이 가이드에서는 모든 옵션의 장단점을 설명합니다.
캐시 API
Cache API는 장기 메모리에 캐시된 Request
및 Response
객체 쌍의 영구 저장소를 제공합니다. Service Workers 사양에 정의되어 있지만 이 API는 기본 스레드나 일반 작업자에서 사용할 수 있습니다. 서비스 워커 컨텍스트 외부에서 사용하려면 합성 Response
객체를 Request
객체 대신 합성 URL과 페어링하여 Cache.put()
메서드를 호출합니다.
이 가이드에서는 메모리 내 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(Origin Private File System)는 스토리지 엔드포인트의 비교적 최신 표준입니다. 페이지 출처에 비공개이므로 일반 파일 시스템과 달리 사용자에게 표시되지 않습니다. 성능에 최적화되어 있으며 콘텐츠에 대한 쓰기 액세스 권한을 제공하는 특수 파일에 대한 액세스를 제공합니다.
예를 들어 다음은 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를 사용하면 하드 디스크에서 파일을 열고 IndexedDB에 유지할 수 있는 FileSystemFileHandle를 가져올 수 있습니다.
이 패턴을 사용하면 사용자는 모델 파일에 대한 액세스 권한을 한 번만 부여하면 됩니다. 지속되는 권한 덕분에 사용자는 파일에 대한 액세스 권한을 영구적으로 부여하도록 선택할 수 있습니다. 앱과 마우스 클릭과 같은 필수 사용자 동작을 새로고침한 후에는 하드 디스크의 파일에 액세스하여 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
함수(downloadedBytes
및 총 fileSize
에 관해 보고), AbortSignal
신호의 signal
는 모두 선택사항입니다.
프로젝트에서 다음 함수를 복사하거나 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 스토리지팀은 최적의 성능을 위해 Cache API를 권장합니다. Cache API를 사용하면 AI 모델에 빠르게 액세스하여 로드 시간을 줄이고 응답성을 개선할 수 있습니다.
OPFS 및 IndexedDB는 사용성이 떨어지는 옵션입니다. OPFS 및 IndexedDB API는 데이터를 저장하기 전에 직렬화해야 합니다. 또한 IndexedDB는 데이터를 검색할 때 데이터를 역직렬화해야 하므로 대규모 모델을 저장하기에 최악의 장소입니다.
틈새 애플리케이션의 경우 File System Access API를 사용하면 사용자 기기의 파일에 직접 액세스할 수 있으므로 자체 AI 모델을 관리하는 사용자에게 적합합니다.
AI 모델을 보호해야 하는 경우 서버에 보관합니다. 클라이언트에 저장되면 DevTools 또는 OFPS DevTools 확장 프로그램을 사용하여 캐시와 IndexedDB에서 모두 데이터를 추출하는 것은 간단합니다. 이러한 스토리지 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입니다.