minicpm5-2b-webgpu / src /cache.js
ProCreations's picture
Dedicated WebGPU runtime, DSpark acceleration, and automatic tool repair
c51a3e7 verified
Raw History Blame Contribute Delete
3.72 kB
// Wllama-compatible OPFS storage with bounded 4 MiB writes instead of one
// main-thread/worker round trip per network packet. Metadata format and keys
// remain Wllama's, so complete existing cache entries continue to work.
// Let session-only inference run even when this browser has no OPFS support.
export class UnavailableStorage {
isSupported(){return true;}
async read(){return null;}
async getSize(){return -1;}
async list(){return [];}
async delete(){}
async write(){throw new Error('Model storage is unavailable in this browser. Turn off Save model in browser and try again.');}
}
export class BufferedOPFS {
isSupported(){return !!navigator.storage?.getDirectory;}
async directory(){return (await navigator.storage.getDirectory()).getDirectoryHandle('cache',{create:true});}
async read(key){try{return await(await(await this.directory()).getFileHandle(key)).getFile();}catch(e){if(e.name==='NotFoundError')return null;throw e;}}
async getSize(key){return (await this.read(key))?.size??-1;}
async list(){const entries=[];for await(const [key,h]of(await this.directory()).entries())if(h.kind==='file')entries.push({key,size:(await h.getFile()).size});return entries;}
async delete(key){try{await(await this.directory()).removeEntry(key);}catch(e){if(e.name!=='NotFoundError')throw e;}}
async write(key,stream){
const worker=new Worker(new URL('./cache-worker.js',import.meta.url),{type:'module'});
const call=(data,transfer=[])=>new Promise((resolve,reject)=>{worker.onmessage=({data})=>data.error?reject(new Error(data.error)):resolve();worker.onerror=e=>reject(new Error(e.message));worker.postMessage(data,transfer);});
const reader=stream.getReader();let slab=new Uint8Array(4*1024*1024),used=0,closed=false;
try{
await call({action:'open',key});
while(true){
const {done,value}=await reader.read();if(done)break;
let offset=0;while(offset<value.byteLength){const n=Math.min(slab.byteLength-used,value.byteLength-offset);slab.set(value.subarray(offset,offset+n),used);used+=n;offset+=n;
if(used===slab.byteLength){await call({action:'write',buffer:slab.buffer},[slab.buffer]);slab=new Uint8Array(4*1024*1024);used=0;}
}
}
if(used){const tail=slab.slice(0,used);await call({action:'write',buffer:tail.buffer},[tail.buffer]);}
await call({action:'close'});closed=true;
}finally{if(!closed){await reader.cancel().catch(()=>{});await call({action:'close'}).catch(()=>{});}reader.releaseLock();worker.terminate();}
}
}
// Retain the existing weight-cache keys so earlier opt-in downloads still work.
export class WeightCache {
constructor(){const storage=new BufferedOPFS();this.storage=storage.isSupported()?storage:new UnavailableStorage();}
async getNameFromURL(url){const hash=await crypto.subtle.digest('SHA-1',new TextEncoder().encode(url));return [...new Uint8Array(hash)].map(b=>b.toString(16).padStart(2,'0')).join('')+'_'+url.split('/').pop();}
open(name){return this.storage.read(name);}
async list(){
const files=await this.storage.list(),metadata=new Map();
for(const file of files){if(file.key.startsWith('__metadata__')){const blob=await this.storage.read(file.key);try{metadata.set(file.key.slice(12),JSON.parse(await blob.text()));}catch{}}}
return files.filter(f=>!f.key.startsWith('__metadata__')).map(f=>({name:f.key,size:f.size,metadata:metadata.get(f.key)||{}}));
}
async write(name,stream,metadata){await this.storage.write(name,stream);await this.storage.write('__metadata__'+name,new Blob([JSON.stringify(metadata)]).stream());}
async deleteMany(predicate){for(const entry of await this.list())if(predicate(entry)){await this.storage.delete(entry.name);await this.storage.delete('__metadata__'+entry.name);}}
}