Skip to content

Commit e4e53f5

Browse files
sauravpandaclaude
andauthored
fix: benchmark Flare WASM loading and broken SmolLM2 GGUF URLs (#308)
- Fix Flare WASM parse error: @sauravpanda/flare@0.2.0 has a nested block comment "/* done */" inside a JSDoc block that prematurely closes the /** */ comment. Benchmark now patches this before import. - Fix Transformers.js 0 tok/s: it runs batch (non-streaming), so tok/s is now calculated as tokens/totalTime instead of tokens/decodeTime. - Fix SmolLM2 GGUF URLs: HuggingFaceTB repo returns 401, switched to bartowski's public repos for 135M (Q8_0, Q4_K_M) and 360M (Q8_0). Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 39b9d47 commit e4e53f5

2 files changed

Lines changed: 59 additions & 19 deletions

File tree

examples/benchmark/index.html

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ <h2>Comparison Charts</h2>
370370
},
371371
flare: {
372372
id: 'smollm2-135m-flare',
373-
url: 'https://huggingface.co/HuggingFaceTB/smollm2-135M-instruct-GGUF/resolve/main/smollm2-135m-instruct-q8_0.gguf',
373+
url: 'https://huggingface.co/bartowski/SmolLM2-135M-Instruct-GGUF/resolve/main/SmolLM2-135M-Instruct-Q8_0.gguf',
374374
},
375375
},
376376
'llama-3.2-1b': {
@@ -520,7 +520,7 @@ <h2>Comparison Charts</h2>
520520
async function runTransformersInference(prompt, opts) {
521521
const t0 = performance.now();
522522
let firstTokenTime = null;
523-
let outputTokenCount = 0;
523+
let prevTokenCount = 0;
524524

525525
const messages = [{ role: 'user', content: prompt }];
526526
let input;
@@ -533,33 +533,44 @@ <h2>Comparison Charts</h2>
533533
input = prompt;
534534
}
535535

536-
const inputTokens = transformersPipeline.tokenizer(input);
537-
const inputLen = inputTokens?.input_ids?.dims?.[1] || input.length / 4;
538-
539536
const result = await transformersPipeline(input, {
540537
max_new_tokens: opts.maxTokens,
541538
temperature: opts.temperature || 0.001,
542539
do_sample: opts.temperature > 0,
543-
callback_function: (beams) => {
544-
const generated = beams?.[0]?.output_token_ids?.length || 0;
545-
if (firstTokenTime === null && generated > inputLen) {
540+
callback_function: (output) => {
541+
// output can be [{generated_text}] or beams with output_token_ids
542+
const ids = output?.[0]?.output_token_ids;
543+
const currentLen = ids?.length || ids?.size || 0;
544+
if (firstTokenTime === null && currentLen > prevTokenCount) {
546545
firstTokenTime = performance.now() - t0;
547546
}
548-
outputTokenCount = Math.max(0, generated - inputLen);
547+
if (currentLen > 0) prevTokenCount = currentLen;
549548
},
550549
});
551550

552551
const totalTime = performance.now() - t0;
553552
const output = result[0]?.generated_text || '';
554553
const generatedPart = typeof output === 'string' ? output.slice(input.length) : '';
555-
const decodeTime = totalTime - (firstTokenTime || totalTime);
556554

555+
// Count output tokens by tokenizing the generated part
556+
let outputTokenCount = 0;
557+
try {
558+
const genTokens = transformersPipeline.tokenizer(generatedPart);
559+
outputTokenCount = genTokens?.input_ids?.dims?.[1] || genTokens?.input_ids?.length || 0;
560+
} catch {
561+
// Rough fallback: ~4 chars per token
562+
outputTokenCount = Math.max(1, Math.round(generatedPart.length / 4));
563+
}
564+
565+
// Transformers.js generates all tokens in a single batch call (no true
566+
// streaming), so firstTokenTime ≈ totalTime. Use tokens/totalTime for
567+
// throughput since there's no separate decode phase.
557568
return {
558569
output: generatedPart,
559570
ttft: firstTokenTime || totalTime,
560571
totalTime,
561572
tokenCount: outputTokenCount,
562-
tokensPerSec: (outputTokenCount > 1 && decodeTime > 0) ? ((outputTokenCount - 1) / (decodeTime / 1000)) : 0,
573+
tokensPerSec: outputTokenCount > 0 ? (outputTokenCount / (totalTime / 1000)) : 0,
563574
};
564575
}
565576

@@ -575,9 +586,38 @@ <h2>Comparison Charts</h2>
575586
if (!flareLib) {
576587
log('Loading @sauravpanda/flare WASM from CDN...', 'info');
577588
const CDN = 'https://cdn.jsdelivr.net/npm/@sauravpanda/flare@0.2.0/pkg';
578-
flareLib = await import(`${CDN}/flare_web.js`);
589+
const wasmUrl = `${CDN}/flare_web_bg.wasm`;
590+
591+
// Fetch the JS module source, patch the WASM URL, and load via blob
592+
// to avoid cross-origin ES module import restrictions.
593+
const jsResp = await fetch(`${CDN}/flare_web.js`, { cache: 'no-cache' });
594+
if (!jsResp.ok) throw new Error(`HTTP ${jsResp.status} fetching flare_web.js`);
595+
let jsSrc = await jsResp.text();
596+
log(`Fetched flare_web.js (${(jsSrc.length / 1024).toFixed(0)} KB)`, 'info');
597+
598+
// Patch import.meta.url references so the WASM file resolves to CDN
599+
jsSrc = jsSrc.replaceAll('import.meta.url', `'${CDN}/flare_web.js'`);
600+
601+
// Fix wasm-pack codegen bug: a JSDoc block contains "/* done */"
602+
// which prematurely closes the outer /** */ comment, causing
603+
// "Unexpected token '*'" parse error. Remove the nested comment.
604+
jsSrc = jsSrc.replaceAll('/* done */', '/* done -/');
605+
606+
const blob = new Blob([jsSrc], { type: 'application/javascript' });
607+
const blobUrl = URL.createObjectURL(blob);
608+
try {
609+
flareLib = await import(/* webpackIgnore: true */ blobUrl);
610+
} catch (importErr) {
611+
URL.revokeObjectURL(blobUrl);
612+
log(`Blob import failed: ${importErr.message}`, 'error');
613+
// Fallback: try direct CDN import
614+
log('Trying direct CDN import...', 'info');
615+
flareLib = await import(`${CDN}/flare_web.js`);
616+
}
617+
URL.revokeObjectURL(blobUrl);
618+
579619
// Initialize the WASM module
580-
await flareLib.default(`${CDN}/flare_web_bg.wasm`);
620+
await flareLib.default(wasmUrl);
581621
log('Flare WASM initialized.', 'success');
582622
}
583623

src/config/models/flare-models.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
"engine": "flare",
44
"modelName": "SmolLM2-135M-Instruct",
55
"modelType": "text-generation",
6-
"repo": "HuggingFaceTB/smollm2-135M-instruct-GGUF",
7-
"url": "https://huggingface.co/HuggingFaceTB/smollm2-135M-instruct-GGUF/resolve/main/smollm2-135m-instruct-q8_0.gguf",
6+
"repo": "bartowski/SmolLM2-135M-Instruct-GGUF",
7+
"url": "https://huggingface.co/bartowski/SmolLM2-135M-Instruct-GGUF/resolve/main/SmolLM2-135M-Instruct-Q8_0.gguf",
88
"pipeline": "text-generation",
99
"defaultQuantization": "Q8_0",
1010
"quantizations": ["Q8_0"],
@@ -24,8 +24,8 @@
2424
"engine": "flare",
2525
"modelName": "SmolLM2-135M-Instruct-Q4",
2626
"modelType": "text-generation",
27-
"repo": "HuggingFaceTB/smollm2-135M-instruct-GGUF",
28-
"url": "https://huggingface.co/HuggingFaceTB/smollm2-135M-instruct-GGUF/resolve/main/smollm2-135m-instruct-q4_k_m.gguf",
27+
"repo": "bartowski/SmolLM2-135M-Instruct-GGUF",
28+
"url": "https://huggingface.co/bartowski/SmolLM2-135M-Instruct-GGUF/resolve/main/SmolLM2-135M-Instruct-Q4_K_M.gguf",
2929
"pipeline": "text-generation",
3030
"defaultQuantization": "Q4_K_M",
3131
"quantizations": ["Q4_K_M"],
@@ -45,8 +45,8 @@
4545
"engine": "flare",
4646
"modelName": "SmolLM2-360M-Instruct",
4747
"modelType": "text-generation",
48-
"repo": "HuggingFaceTB/smollm2-360M-instruct-GGUF",
49-
"url": "https://huggingface.co/HuggingFaceTB/smollm2-360M-instruct-GGUF/resolve/main/smollm2-360m-instruct-q8_0.gguf",
48+
"repo": "bartowski/SmolLM2-360M-Instruct-GGUF",
49+
"url": "https://huggingface.co/bartowski/SmolLM2-360M-Instruct-GGUF/resolve/main/SmolLM2-360M-Instruct-Q8_0.gguf",
5050
"pipeline": "text-generation",
5151
"defaultQuantization": "Q8_0",
5252
"quantizations": ["Q8_0"],

0 commit comments

Comments
 (0)