Skip to content

Commit d353559

Browse files
committed
Migrate cross origin storage implementation to tvm/web
1 parent bacbb75 commit d353559

10 files changed

Lines changed: 73 additions & 484 deletions

File tree

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,29 @@ const engine = new MLCEngine({
150150
await engine.reload(selectedModel);
151151
```
152152

153+
### Cache Backend Policy
154+
155+
WebLLM supports three cache backends through `AppConfig.cacheBackend`:
156+
157+
- `"cache"`: browser [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache) (default).
158+
- `"indexeddb"`: browser [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API).
159+
- `"cross-origin"`: experimental Chrome [Cross-Origin Storage](https://github.com/explainers-by-googlers/cross-origin-storage) extension backend.
160+
161+
Example:
162+
163+
```typescript
164+
import { CreateMLCEngine, prebuiltAppConfig } from "@mlc-ai/web-llm";
165+
166+
const appConfig = { ...prebuiltAppConfig, cacheBackend: "cross-origin" };
167+
const engine = await CreateMLCEngine("Llama-3.1-8B-Instruct-q4f32_1-MLC", {
168+
appConfig,
169+
});
170+
```
171+
172+
Notes:
173+
- The `"cross-origin"` backend requires installing and enabling a compatible browser extension.
174+
- Cross-origin backend currently does not support programmatic tensor-cache deletion; clearing is extension-managed.
175+
153176
### Chat Completion
154177
After successfully initializing the engine, you can now invoke chat completions using OpenAI style chat APIs through the `engine.chat.completions` interface. For the full list of parameters and their descriptions, check [section below](#full-openai-compatibility) and [OpenAI API reference](https://platform.openai.com/docs/api-reference/chat/create).
155178

examples/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ These examples demonstrate various capabilities via WebLLM's OpenAI-like API.
4747

4848
- [logit-processor](logit-processor): while `logit_bias` is supported, we additionally support stateful logit processing where users can specify their own rules. We also expose low-level API `forwardTokensAndSample()`.
4949
- [cache-usage](cache-usage): demonstrates how WebLLM supports multiple cache backends. Choose between the [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache), [IndexedDB cache](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API), or the experimental Chrome [Cross-Origin Storage](https://github.com/explainers-by-googlers/cross-origin-storage) extension via `appConfig.cacheBackend`. Also demonstrates various cache utils such as checking
50-
whether a model is cached, deleting a model's weights from cache, deleting a model library wasm from cache, etc.
50+
whether a model is cached, deleting a model's weights from cache, deleting a model library wasm from cache, etc. Note: cross-origin backend currently does not support programmatic tensor-cache deletion.
5151
- [simple-chat-upload](simple-chat-upload): demonstrates how to upload local models to WebLLM instead of downloading via a URL link
5252

5353
## Demo Spaces

examples/cache-usage/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ setting `AppConfig.cacheBackend` to `"cache"`, `"indexeddb"`, or `"cross-origin"
55
This folder provides an example on how different caches are used in WebLLM. We also
66
demonstrate the utility cache functions such as deleting models, checking if models are in cache, etc.
77

8-
> **Note:** The cross-origin backend requires installation of the [cross-origin storage browser extension](https://github.com/web-ai-community/cross-origin-storage-extension).
8+
> **Note:** The cross-origin backend requires installation of the [cross-origin storage browser extension](https://github.com/web-ai-community/cross-origin-storage-extension). This does not currently support programmatic tensor-cache deletion; deletion is extension-managed.
99
10-
For more information about the two caches, see: https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria#what_technologies_store_data_in_the_browser.
10+
For more information about Cache API and IndexedDB, see:
11+
https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria#what_technologies_store_data_in_the_browser.
1112

1213
To inspect the downloaded artifacts in your browser, open up developer console, go to application,
1314
and you will find the artifacts under either `IndexedDB` or `Cache storage`. When `"cross-origin"` is selected,

examples/simple-chat-upload/src/simple_chat.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ async function uploadFiles(): Promise<void> {
395395
alert("No files selected.");
396396
return;
397397
}
398-
if (appConfig.useIndexedDBCache) {
398+
if (appConfig.cacheBackend === "indexeddb") {
399399
for (const file of input.files) {
400400
uploadToIndexedDB(file);
401401
}

src/cache_util.ts

Lines changed: 24 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -9,151 +9,27 @@ import {
99
import { cleanModelUrl } from "./support";
1010
import { ModelNotFoundError, UnsupportedTokenizerFilesError } from "./error";
1111
import { Tokenizer } from "@mlc-ai/web-tokenizers";
12-
import CrossOriginStorage from "./cross_origin_storage";
13-
import CrossOriginStorageCache from "./cross_origin_storage_cache";
1412

1513
type CacheScope = "webllm/model" | "webllm/config" | "webllm/wasm";
1614

17-
let crossOriginUnavailableLogged = false;
18-
let crossOriginAvailabilityWait: Promise<void> | null = null;
19-
20-
function scheduleCrossOriginFallbackWarning(
21-
logger: (msg: string) => void,
22-
): void {
23-
if (crossOriginUnavailableLogged || crossOriginAvailabilityWait) {
24-
return;
25-
}
26-
crossOriginAvailabilityWait = (async () => {
27-
const available = CrossOriginStorage.isAvailable();
28-
crossOriginAvailabilityWait = null;
29-
if (available || crossOriginUnavailableLogged) {
30-
return;
31-
}
32-
logger(
33-
"Cross-origin storage backend is not yet available; temporarily falling back to the Cache API.",
34-
);
35-
crossOriginUnavailableLogged = true;
36-
})();
37-
}
38-
39-
function useCrossOrigin(appConfig: AppConfig): boolean {
40-
return (
41-
getCacheBackend(appConfig) === "cross-origin" &&
42-
CrossOriginStorage.isAvailable()
43-
);
44-
}
45-
46-
export function getArtifactCache(
15+
function getCacheAccessOptions(
4716
scope: CacheScope,
4817
appConfig: AppConfig,
49-
logger: (msg: string) => void = console.warn,
50-
): tvmjs.ArtifactCacheTemplate {
51-
const backend = getCacheBackend(appConfig);
52-
if (backend === "cross-origin") {
53-
if (CrossOriginStorage.isAvailable()) {
54-
return new CrossOriginStorageCache(scope);
55-
}
56-
scheduleCrossOriginFallbackWarning(logger);
57-
}
58-
if (backend === "indexeddb") {
59-
return new tvmjs.ArtifactIndexedDBCache(scope);
60-
}
61-
return new tvmjs.ArtifactCache(scope);
62-
}
63-
64-
async function hasTensorCache(
65-
cache: tvmjs.ArtifactCacheTemplate,
66-
tensorCacheUrl: string,
67-
): Promise<boolean> {
68-
const jsonUrl = new URL("tensor-cache.json", tensorCacheUrl).href;
69-
const hasManifest = await cache.hasAllKeys([jsonUrl]);
70-
if (!hasManifest) {
71-
return false;
72-
}
73-
const manifest = await cache.fetchWithCache(jsonUrl, "json");
74-
const records = manifest?.records ?? [];
75-
if (!Array.isArray(records) || records.length === 0) {
76-
return false;
77-
}
78-
const shardUrls = records.map(
79-
(entry: { dataPath: string }) =>
80-
new URL(entry.dataPath, tensorCacheUrl).href,
81-
);
82-
return cache.hasAllKeys(shardUrls);
18+
): tvmjs.TensorCacheAccessOptions {
19+
return {
20+
cacheScope: scope,
21+
cacheType: getCacheBackend(appConfig),
22+
};
8323
}
8424

85-
async function deleteTensorCacheEntries(
86-
cache: tvmjs.ArtifactCacheTemplate,
87-
tensorCacheUrl: string,
88-
): Promise<void> {
89-
const jsonUrl = new URL("tensor-cache.json", tensorCacheUrl).href;
90-
const hasManifest = await cache.hasAllKeys([jsonUrl]);
91-
if (!hasManifest) {
92-
return;
93-
}
94-
let manifest: { records?: Array<{ dataPath: string }> };
95-
try {
96-
manifest = await cache.fetchWithCache(jsonUrl, "json");
97-
} catch (err) {
98-
console.warn(
99-
`Failed to load tensor cache manifest at ${jsonUrl}; skipping deletion.`,
100-
err,
101-
);
102-
return;
103-
}
104-
const records = manifest?.records ?? [];
105-
await Promise.all(
106-
records.map(async (entry) => {
107-
if (!entry?.dataPath) {
108-
return;
109-
}
110-
const dataUrl = new URL(entry.dataPath, tensorCacheUrl).href;
111-
await cache.deleteInCache(dataUrl);
112-
}),
113-
);
114-
await cache.deleteInCache(jsonUrl);
115-
}
116-
117-
export async function fetchModelArtifacts(
118-
tvm: tvmjs.Instance,
119-
tensorCacheUrl: string,
120-
device: tvmjs.DLDevice,
25+
function createScopedArtifactCache(
26+
scope: CacheScope,
12127
appConfig: AppConfig,
122-
signal?: AbortSignal,
123-
): Promise<any> {
124-
if (!useCrossOrigin(appConfig)) {
125-
const backend = getCacheBackend(appConfig);
126-
const cacheType = backend === "indexeddb" ? "indexeddb" : "cache";
127-
return tvm.fetchTensorCache(
128-
tensorCacheUrl,
129-
device,
130-
"webllm/model",
131-
cacheType,
132-
signal,
133-
);
134-
}
135-
136-
const artifactCache = getArtifactCache("webllm/model", appConfig);
137-
const jsonUrl = new URL("tensor-cache.json", tensorCacheUrl).href;
138-
const manifest = await artifactCache.fetchWithCache(jsonUrl, "json", signal);
139-
const records = (
140-
Array.isArray(manifest?.records) ? manifest.records : []
141-
) as Array<any>;
142-
await (tvm as any).fetchTensorCacheInternal(
143-
tensorCacheUrl,
144-
records,
145-
device,
146-
artifactCache,
147-
signal,
28+
): tvmjs.ArtifactCacheTemplate {
29+
return tvmjs.createArtifactCache(
30+
scope,
31+
getCacheAccessOptions(scope, appConfig),
14832
);
149-
if (manifest?.metadata !== undefined) {
150-
const runtime = tvm as any;
151-
runtime.cacheMetadata = {
152-
...runtime.cacheMetadata,
153-
...(manifest.metadata as Record<string, unknown>),
154-
};
155-
}
156-
return manifest;
15733
}
15834

15935
function findModelRecord(modelId: string, appConfig?: AppConfig): ModelRecord {
@@ -175,13 +51,10 @@ export async function hasModelInCache(
17551
}
17652
const modelRecord = findModelRecord(modelId, appConfig);
17753
const modelUrl = cleanModelUrl(modelRecord.model);
178-
if (useCrossOrigin(appConfig)) {
179-
const cache = getArtifactCache("webllm/model", appConfig);
180-
return hasTensorCache(cache, modelUrl);
181-
}
182-
const backend = getCacheBackend(appConfig);
183-
const cacheType = backend === "indexeddb" ? "indexeddb" : "cache";
184-
return tvmjs.hasTensorInCache(modelUrl, "webllm/model", cacheType);
54+
return tvmjs.hasTensorInCache(
55+
modelUrl,
56+
getCacheAccessOptions("webllm/model", appConfig),
57+
);
18558
}
18659

18760
export async function deleteModelAllInfoInCache(
@@ -210,14 +83,11 @@ export async function deleteModelInCache(
21083
}
21184
const modelRecord = findModelRecord(modelId, appConfig);
21285
const modelUrl = cleanModelUrl(modelRecord.model);
213-
const modelCache = getArtifactCache("webllm/model", appConfig);
214-
if (useCrossOrigin(appConfig)) {
215-
await deleteTensorCacheEntries(modelCache, modelUrl);
216-
} else {
217-
const backend = getCacheBackend(appConfig);
218-
const cacheType = backend === "indexeddb" ? "indexeddb" : "cache";
219-
await tvmjs.deleteTensorCache(modelUrl, "webllm/model", cacheType);
220-
}
86+
const modelCache = createScopedArtifactCache("webllm/model", appConfig);
87+
await tvmjs.deleteTensorCache(
88+
modelUrl,
89+
getCacheAccessOptions("webllm/model", appConfig),
90+
);
22191
await modelCache.deleteInCache(new URL("tokenizer.model", modelUrl).href);
22292
await modelCache.deleteInCache(new URL("tokenizer.json", modelUrl).href);
22393
}
@@ -231,7 +101,7 @@ export async function deleteChatConfigInCache(
231101
appConfig = prebuiltAppConfig;
232102
}
233103
const modelRecord = findModelRecord(modelId, appConfig);
234-
const configCache = getArtifactCache("webllm/config", appConfig);
104+
const configCache = createScopedArtifactCache("webllm/config", appConfig);
235105
const modelUrl = cleanModelUrl(modelRecord.model);
236106
const configUrl = new URL("mlc-chat-config.json", modelUrl).href;
237107
await configCache.deleteInCache(configUrl);
@@ -246,7 +116,7 @@ export async function deleteModelWasmInCache(
246116
appConfig = prebuiltAppConfig;
247117
}
248118
const modelRecord = findModelRecord(modelId, appConfig);
249-
const wasmCache = getArtifactCache("webllm/wasm", appConfig);
119+
const wasmCache = createScopedArtifactCache("webllm/wasm", appConfig);
250120
await wasmCache.deleteInCache(modelRecord.model_lib);
251121
}
252122

@@ -264,7 +134,7 @@ export async function asyncLoadTokenizer(
264134
appConfig: AppConfig,
265135
logger: (msg: string) => void = console.log,
266136
): Promise<Tokenizer> {
267-
const modelCache = getArtifactCache("webllm/model", appConfig, logger);
137+
const modelCache = createScopedArtifactCache("webllm/model", appConfig);
268138

269139
if (config.tokenizer_files.includes("tokenizer.json")) {
270140
const url = new URL("tokenizer.json", baseUrl).href;

src/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,10 @@ export interface ModelRecord {
272272
* @param cacheBackend: the backend to use for caching models and other artifacts.
273273
* If unspecified, will use the Cache API. For more information, see:
274274
* https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria#what_technologies_store_data_in_the_browser
275+
* Supported values are:
276+
* - "cache": browser Cache API.
277+
* - "indexeddb": IndexedDB-backed cache.
278+
* - "cross-origin": Chrome Cross-Origin Storage extension-backed cache.
275279
*
276280
* @note Note that the Cache API is more well-tested in WebLLM as of now.
277281
*/

0 commit comments

Comments
 (0)