File size: 6,525 Bytes
cbc5ce9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | import * as ort from '/runtime/vendor/ort.webgpu.mjs';
import {AutoTokenizer, env} from '/runtime/vendor/transformers.web.js';
let session, tokenizer, abi, gpuInfo, eos;
const MAX_CONTEXT_TOKENS = 32768;
const MAX_OUTPUT_TOKENS = 2048;
const PREFILL_CHUNK_TOKENS = 512;
function emptyCache() {
return abi.inputs.slice(2).map(() => new ort.Tensor('float16', new Uint16Array(0), [1, abi.numKvHeads, 0, abi.headDim]));
}
function dispose(tensors) { for (const t of tensors) if (t.location === 'gpu-buffer') t.dispose(); }
function argmax(values) { let best = 0; for (let i=1;i<values.length;i++) if(values[i]>values[best]) best=i; return best; }
window.initializeSpark = async () => {
console.log('spark-stage: adapter');
const adapter = await navigator.gpu.requestAdapter({powerPreference: 'high-performance'});
if (!adapter || !adapter.features.has('shader-f16')) throw Error('Hardware WebGPU/shader-f16 required');
const info = adapter.info;
if (info.vendor !== 'nvidia' || info.architecture !== 'blackwell') throw Error('Wrong WebGPU adapter');
gpuInfo = {vendor: info.vendor, architecture: info.architecture, description: info.description};
ort.env.webgpu.adapter = adapter;
ort.env.wasm.numThreads = 1;
ort.env.wasm.wasmPaths = '/runtime/vendor/';
env.allowRemoteModels = false;
env.allowLocalModels = true;
env.localModelPath = '/';
console.log('spark-stage: tokenizer');
tokenizer = await AutoTokenizer.from_pretrained('model', {local_files_only: true});
// Transformers.js 4.2 does not load the separate Jinja file automatically.
const templateResponse = await fetch('/model/chat_template.jinja');
if (!templateResponse.ok) throw Error('Missing native Spark chat template');
tokenizer.chat_template = (await templateResponse.text()).replace(/\r\n/g, '\n');
abi = await (await fetch('/abi')).json();
const config = await (await fetch('/model/generation_config.json')).json();
eos = new Set(Array.isArray(config.eos_token_id) ? config.eos_token_id : [config.eos_token_id]);
// Let ORT's streaming large-file loader bypass Chrome's 2GB arrayBuffer cap.
console.log('spark-stage: creating session');
session = await ort.InferenceSession.create('/model/onnx/model_fp16.onnx', {
executionProviders: ['webgpu'], graphOptimizationLevel: 'disabled',
preferredOutputLocation: 'gpu-buffer',
externalData: [{path: 'model_fp16.onnx_data', data: '/model/onnx/model_fp16.onnx_data'}],
});
return {gpuInfo, runtime: ort.env.versions, abi};
};
async function step(ids, past, seen) {
if (!ids.length || seen + ids.length > MAX_CONTEXT_TOKENS) throw Error('Context limit exceeded');
const feeds = {
input_ids: new ort.Tensor('int64', BigInt64Array.from(ids, BigInt), [1, ids.length]),
position_ids: new ort.Tensor('int64', BigInt64Array.from(ids.map((_,i)=>seen+i), BigInt), [1,ids.length]),
};
abi.inputs.slice(2).forEach((key,i)=>feeds[key]=past[i]);
let result, logits, present;
try {
result = await session.run(feeds);
logits = await result.logits.getData();
if (!logits.every(Number.isFinite)) throw Error('Nonfinite WebGPU logits');
present = abi.outputs.slice(1).map(key=>result[key]);
if (!present.every(t=>t.dims[2]===seen+ids.length)) throw Error('Cache-length mismatch');
} catch (error) {
if (result) dispose(Object.values(result));
throw error;
}
result.logits.dispose();
dispose(past);
return {logits, present};
}
// Keep every token. Small, ordered prefill steps bound transient attention memory;
// each returned cache becomes the next step's input without a context reset.
async function prefill(ids) {
if (!Array.isArray(ids) || ids.length < 1 || ids.length > MAX_CONTEXT_TOKENS) {
throw Error('Prompt must contain 1..32768 tokens');
}
let past = emptyCache(), seen = 0, logits;
try {
for (let offset = 0; offset < ids.length; offset += PREFILL_CHUNK_TOKENS) {
const chunk = ids.slice(offset, offset + PREFILL_CHUNK_TOKENS);
const result = await step(chunk, past, seen);
past = result.present;
seen += chunk.length;
logits = result.logits;
}
if (seen !== ids.length || past.some(t => t.dims[2] !== seen)) {
throw Error('Prefill lost prompt tokens');
}
return {logits, past, seen};
} catch (error) {
dispose(past);
throw error;
}
}
window.sparkNumerics = async (fixture) => {
if (!Array.isArray(fixture.teacherTokens) || fixture.teacherTokens.length < 3 ||
fixture.inputIds.length + 3 > MAX_CONTEXT_TOKENS) throw Error('Invalid numerical fixture');
const initial = await prefill(fixture.inputIds);
let past = initial.past, seen = initial.seen;
const logits=[];
try {
logits.push(Array.from(initial.logits));
for(let n=0;n<3;n++) {
const result=await step([fixture.teacherTokens[n]],past,seen);
logits.push(Array.from(result.logits));
seen++; past=result.present;
}
return {logits,finite:true,gpuInfo,finalCacheTokens:seen};
} finally {
dispose(past);
}
};
window.sparkTemplate = ({messages,tools}) => {
const rendered=tokenizer.apply_chat_template(messages,{tools:tools||undefined,tokenize:false,
enable_thinking:false,add_generation_prompt:true});
return {text:rendered,ids:Array.from(tokenizer(rendered,{add_special_tokens:false}).input_ids.data,Number)};
};
window.sparkGenerate = async ({messages,tools,maxTokens=512}) => {
if (!Number.isInteger(maxTokens) || maxTokens < 1 || maxTokens > MAX_OUTPUT_TOKENS) {
throw Error('maxTokens must be 1..2048');
}
const prompt=window.sparkTemplate({messages,tools});
if(prompt.ids.length+maxTokens>MAX_CONTEXT_TOKENS) {
throw Error('Input plus requested output exceeds 32768 tokens; no truncation applied');
}
let past, seen=0, output=[], reason='length';
const started=performance.now();
try {
const initial = await prefill(prompt.ids);
past = initial.past;
seen = initial.seen;
let logits = initial.logits;
for(let n=0;n<maxTokens;n++) {
const token=argmax(logits);
output.push(token);
if(eos.has(token)){reason='stop';break;}
if (n + 1 < maxTokens) {
const result=await step([token],past,seen);
past=result.present;seen++;
logits=result.logits;
}
}
return {content:tokenizer.decode(output,{skip_special_tokens:true}),promptTokens:prompt.ids.length,
completionTokens:output.length,finishReason:reason,seconds:(performance.now()-started)/1000};
} finally {if (past) dispose(past);}
};
|