Spaces:
Running
Running
GitHub Actions commited on
Commit ·
da0c2f2
1
Parent(s): b0f367a
sync from abhijitramesh/webgpu-bench@3fae9c4594
Browse files- js/run/controller.js +69 -16
- js/run/source.js +8 -5
js/run/controller.js
CHANGED
|
@@ -55,6 +55,11 @@ const state = {
|
|
| 55 |
sessionDownloads: new Set(),
|
| 56 |
// Handle to the currently-running worker, so Abort can terminate it.
|
| 57 |
currentWorker: null,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
// Build metadata fetched from `build/<variant>/build-info.json`. Stamped
|
| 59 |
// onto every result record so we can compare performance across llama.cpp
|
| 60 |
// versions. JSPI and Asyncify variants are built from the same source
|
|
@@ -62,6 +67,15 @@ const state = {
|
|
| 62 |
buildInfo: null,
|
| 63 |
};
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
async function loadBuildInfo() {
|
| 66 |
// Try jspi first (Chrome path), fall back to asyncify (Safari/Firefox path).
|
| 67 |
// Either contains the same llama.cpp commit/describe.
|
|
@@ -987,25 +1001,31 @@ async function onDownloadClick() {
|
|
| 987 |
if (state.aborted) break;
|
| 988 |
const row = progressRowFor(v);
|
| 989 |
row.setStatus('downloading', '');
|
|
|
|
|
|
|
| 990 |
try {
|
| 991 |
-
const { stream, contentLength } = await state.source.fetchModel(v.repo, v.filename);
|
| 992 |
const reader = stream.getReader();
|
| 993 |
let read = 0;
|
| 994 |
while (true) {
|
| 995 |
-
if (
|
| 996 |
const { done, value } = await reader.read();
|
| 997 |
if (done) break;
|
| 998 |
read += value.length;
|
| 999 |
row.setProgress(contentLength ? read / contentLength : 0, read, contentLength);
|
| 1000 |
}
|
| 1001 |
-
if (!
|
| 1002 |
state.cacheStatus[cacheKey(v)] = { cachedBytes: read };
|
| 1003 |
refreshCacheBadge(v);
|
| 1004 |
row.setStatus('cached', formatSize(read / (1024 * 1024)));
|
|
|
|
|
|
|
| 1005 |
}
|
| 1006 |
} catch (err) {
|
| 1007 |
-
row.setStatus('
|
| 1008 |
-
logLine(`Download failed: ${v.filename}: ${err.message}`);
|
|
|
|
|
|
|
| 1009 |
}
|
| 1010 |
}
|
| 1011 |
|
|
@@ -1094,12 +1114,14 @@ async function onRunClick() {
|
|
| 1094 |
if (skipPrefetch) return;
|
| 1095 |
const row = progressRowFor(v);
|
| 1096 |
row.setStatus('prefetching', '');
|
|
|
|
|
|
|
| 1097 |
try {
|
| 1098 |
-
const { stream, contentLength } = await state.source.fetchModel(v.repo, v.filename);
|
| 1099 |
const reader = stream.getReader();
|
| 1100 |
let read = 0;
|
| 1101 |
while (true) {
|
| 1102 |
-
if (
|
| 1103 |
const { done, value } = await reader.read();
|
| 1104 |
if (done) break;
|
| 1105 |
read += value.length;
|
|
@@ -1110,8 +1132,14 @@ async function onRunClick() {
|
|
| 1110 |
refreshCacheBadge(v);
|
| 1111 |
row.setStatus('cached', formatSize(read / (1024 * 1024)));
|
| 1112 |
} catch (err) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1113 |
row.setStatus('error', `prefetch: ${err.message}`);
|
| 1114 |
logLine(`Prefetch failed: ${v.filename}: ${err.message}`);
|
|
|
|
|
|
|
| 1115 |
}
|
| 1116 |
};
|
| 1117 |
|
|
@@ -1220,13 +1248,16 @@ function runInWorker({
|
|
| 1220 |
|
| 1221 |
state.currentWorker = worker;
|
| 1222 |
let settled = false;
|
|
|
|
| 1223 |
const finish = (record) => {
|
| 1224 |
if (settled) return;
|
| 1225 |
settled = true;
|
| 1226 |
try { worker.terminate(); } catch { /* noop */ }
|
| 1227 |
if (state.currentWorker === worker) state.currentWorker = null;
|
|
|
|
| 1228 |
resolve(record);
|
| 1229 |
};
|
|
|
|
| 1230 |
|
| 1231 |
worker.onmessage = (e) => {
|
| 1232 |
const msg = e.data || {};
|
|
@@ -1376,11 +1407,14 @@ async function runBenchmarkInWorker(v, params, callbacks) {
|
|
| 1376 |
|
| 1377 |
if (useOpfs) {
|
| 1378 |
let contentLength;
|
|
|
|
|
|
|
| 1379 |
try {
|
| 1380 |
callbacks.onStatus?.('downloading', 'Downloading model to OPFS...');
|
| 1381 |
const r = await state.source.opfsHandleForModel(
|
| 1382 |
v.repo, v.filename,
|
| 1383 |
callbacks.onProgress,
|
|
|
|
| 1384 |
);
|
| 1385 |
contentLength = r.size;
|
| 1386 |
// When the prefetch is skipped (mobile path), the inline download
|
|
@@ -1393,7 +1427,15 @@ async function runBenchmarkInWorker(v, params, callbacks) {
|
|
| 1393 |
refreshCacheBadge(v);
|
| 1394 |
}
|
| 1395 |
} catch (err) {
|
|
|
|
|
|
|
|
|
|
| 1396 |
return { status: 'error', error: `opfsHandleForModel failed: ${err.message}` };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1397 |
}
|
| 1398 |
// Pass the OPFS path components, not the FileHandle. iOS Safari
|
| 1399 |
// (and some older Chromium/Firefox versions) can't structured-clone
|
|
@@ -1410,11 +1452,19 @@ async function runBenchmarkInWorker(v, params, callbacks) {
|
|
| 1410 |
}
|
| 1411 |
|
| 1412 |
let fetched;
|
|
|
|
|
|
|
| 1413 |
try {
|
| 1414 |
-
fetched = await state.source.fetchModel(v.repo, v.filename);
|
| 1415 |
} catch (err) {
|
|
|
|
|
|
|
| 1416 |
return { status: 'error', error: `fetchModel failed: ${err.message}` };
|
| 1417 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1418 |
|
| 1419 |
return runInWorker({
|
| 1420 |
params: { ...baseParams, contentLength: fetched.contentLength },
|
|
@@ -1783,15 +1833,18 @@ function wireAbortHandler() {
|
|
| 1783 |
state.aborted = true;
|
| 1784 |
const ab = $('btn-abort');
|
| 1785 |
if (ab) ab.disabled = true;
|
| 1786 |
-
//
|
| 1787 |
-
//
|
| 1788 |
-
|
| 1789 |
-
|
| 1790 |
-
|
| 1791 |
-
|
| 1792 |
-
|
| 1793 |
-
logLine('Abort requested — will stop between variants.');
|
| 1794 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1795 |
});
|
| 1796 |
}
|
| 1797 |
|
|
|
|
| 55 |
sessionDownloads: new Set(),
|
| 56 |
// Handle to the currently-running worker, so Abort can terminate it.
|
| 57 |
currentWorker: null,
|
| 58 |
+
// Set of fns that abort an in-flight async op (worker terminate, fetch
|
| 59 |
+
// signal abort). Multiple concurrent ops register here — Run study has a
|
| 60 |
+
// worker running variant i AND a prefetch downloading variant i+1, both
|
| 61 |
+
// of which need to be cancellable. Abort handler iterates the whole set.
|
| 62 |
+
abortHandlers: new Set(),
|
| 63 |
// Build metadata fetched from `build/<variant>/build-info.json`. Stamped
|
| 64 |
// onto every result record so we can compare performance across llama.cpp
|
| 65 |
// versions. JSPI and Asyncify variants are built from the same source
|
|
|
|
| 67 |
buildInfo: null,
|
| 68 |
};
|
| 69 |
|
| 70 |
+
// Register an abort callback for an in-flight async op (worker terminate,
|
| 71 |
+
// fetch signal abort, etc.). Returns an unregister fn the caller MUST
|
| 72 |
+
// invoke when the op settles, so we don't accumulate stale handlers across
|
| 73 |
+
// runs. Abort handler iterates state.abortHandlers and calls every fn.
|
| 74 |
+
function registerAbort(fn) {
|
| 75 |
+
state.abortHandlers.add(fn);
|
| 76 |
+
return () => state.abortHandlers.delete(fn);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
async function loadBuildInfo() {
|
| 80 |
// Try jspi first (Chrome path), fall back to asyncify (Safari/Firefox path).
|
| 81 |
// Either contains the same llama.cpp commit/describe.
|
|
|
|
| 1001 |
if (state.aborted) break;
|
| 1002 |
const row = progressRowFor(v);
|
| 1003 |
row.setStatus('downloading', '');
|
| 1004 |
+
const ac = new AbortController();
|
| 1005 |
+
const unregister = registerAbort(() => ac.abort());
|
| 1006 |
try {
|
| 1007 |
+
const { stream, contentLength } = await state.source.fetchModel(v.repo, v.filename, ac.signal);
|
| 1008 |
const reader = stream.getReader();
|
| 1009 |
let read = 0;
|
| 1010 |
while (true) {
|
| 1011 |
+
if (ac.signal.aborted) { try { reader.cancel(); } catch {} break; }
|
| 1012 |
const { done, value } = await reader.read();
|
| 1013 |
if (done) break;
|
| 1014 |
read += value.length;
|
| 1015 |
row.setProgress(contentLength ? read / contentLength : 0, read, contentLength);
|
| 1016 |
}
|
| 1017 |
+
if (!ac.signal.aborted) {
|
| 1018 |
state.cacheStatus[cacheKey(v)] = { cachedBytes: read };
|
| 1019 |
refreshCacheBadge(v);
|
| 1020 |
row.setStatus('cached', formatSize(read / (1024 * 1024)));
|
| 1021 |
+
} else {
|
| 1022 |
+
row.setStatus('aborted', '');
|
| 1023 |
}
|
| 1024 |
} catch (err) {
|
| 1025 |
+
if (ac.signal.aborted) { row.setStatus('aborted', ''); }
|
| 1026 |
+
else { row.setStatus('error', err.message); logLine(`Download failed: ${v.filename}: ${err.message}`); }
|
| 1027 |
+
} finally {
|
| 1028 |
+
unregister();
|
| 1029 |
}
|
| 1030 |
}
|
| 1031 |
|
|
|
|
| 1114 |
if (skipPrefetch) return;
|
| 1115 |
const row = progressRowFor(v);
|
| 1116 |
row.setStatus('prefetching', '');
|
| 1117 |
+
const ac = new AbortController();
|
| 1118 |
+
const unregister = registerAbort(() => ac.abort());
|
| 1119 |
try {
|
| 1120 |
+
const { stream, contentLength } = await state.source.fetchModel(v.repo, v.filename, ac.signal);
|
| 1121 |
const reader = stream.getReader();
|
| 1122 |
let read = 0;
|
| 1123 |
while (true) {
|
| 1124 |
+
if (ac.signal.aborted) { try { reader.cancel(); } catch {} return; }
|
| 1125 |
const { done, value } = await reader.read();
|
| 1126 |
if (done) break;
|
| 1127 |
read += value.length;
|
|
|
|
| 1132 |
refreshCacheBadge(v);
|
| 1133 |
row.setStatus('cached', formatSize(read / (1024 * 1024)));
|
| 1134 |
} catch (err) {
|
| 1135 |
+
if (ac.signal.aborted) {
|
| 1136 |
+
row.setStatus('aborted', '');
|
| 1137 |
+
return;
|
| 1138 |
+
}
|
| 1139 |
row.setStatus('error', `prefetch: ${err.message}`);
|
| 1140 |
logLine(`Prefetch failed: ${v.filename}: ${err.message}`);
|
| 1141 |
+
} finally {
|
| 1142 |
+
unregister();
|
| 1143 |
}
|
| 1144 |
};
|
| 1145 |
|
|
|
|
| 1248 |
|
| 1249 |
state.currentWorker = worker;
|
| 1250 |
let settled = false;
|
| 1251 |
+
let unregister = () => {};
|
| 1252 |
const finish = (record) => {
|
| 1253 |
if (settled) return;
|
| 1254 |
settled = true;
|
| 1255 |
try { worker.terminate(); } catch { /* noop */ }
|
| 1256 |
if (state.currentWorker === worker) state.currentWorker = null;
|
| 1257 |
+
unregister();
|
| 1258 |
resolve(record);
|
| 1259 |
};
|
| 1260 |
+
unregister = registerAbort(() => finish({ status: 'aborted', error: 'aborted by user' }));
|
| 1261 |
|
| 1262 |
worker.onmessage = (e) => {
|
| 1263 |
const msg = e.data || {};
|
|
|
|
| 1407 |
|
| 1408 |
if (useOpfs) {
|
| 1409 |
let contentLength;
|
| 1410 |
+
const ac = new AbortController();
|
| 1411 |
+
const unregister = registerAbort(() => ac.abort());
|
| 1412 |
try {
|
| 1413 |
callbacks.onStatus?.('downloading', 'Downloading model to OPFS...');
|
| 1414 |
const r = await state.source.opfsHandleForModel(
|
| 1415 |
v.repo, v.filename,
|
| 1416 |
callbacks.onProgress,
|
| 1417 |
+
ac.signal,
|
| 1418 |
);
|
| 1419 |
contentLength = r.size;
|
| 1420 |
// When the prefetch is skipped (mobile path), the inline download
|
|
|
|
| 1427 |
refreshCacheBadge(v);
|
| 1428 |
}
|
| 1429 |
} catch (err) {
|
| 1430 |
+
if (ac.signal.aborted) {
|
| 1431 |
+
return { status: 'aborted', error: 'aborted by user' };
|
| 1432 |
+
}
|
| 1433 |
return { status: 'error', error: `opfsHandleForModel failed: ${err.message}` };
|
| 1434 |
+
} finally {
|
| 1435 |
+
unregister();
|
| 1436 |
+
}
|
| 1437 |
+
if (state.aborted) {
|
| 1438 |
+
return { status: 'aborted', error: 'aborted by user' };
|
| 1439 |
}
|
| 1440 |
// Pass the OPFS path components, not the FileHandle. iOS Safari
|
| 1441 |
// (and some older Chromium/Firefox versions) can't structured-clone
|
|
|
|
| 1452 |
}
|
| 1453 |
|
| 1454 |
let fetched;
|
| 1455 |
+
const ac = new AbortController();
|
| 1456 |
+
const unregister = registerAbort(() => ac.abort());
|
| 1457 |
try {
|
| 1458 |
+
fetched = await state.source.fetchModel(v.repo, v.filename, ac.signal);
|
| 1459 |
} catch (err) {
|
| 1460 |
+
unregister();
|
| 1461 |
+
if (ac.signal.aborted) return { status: 'aborted', error: 'aborted by user' };
|
| 1462 |
return { status: 'error', error: `fetchModel failed: ${err.message}` };
|
| 1463 |
}
|
| 1464 |
+
unregister();
|
| 1465 |
+
if (state.aborted) {
|
| 1466 |
+
return { status: 'aborted', error: 'aborted by user' };
|
| 1467 |
+
}
|
| 1468 |
|
| 1469 |
return runInWorker({
|
| 1470 |
params: { ...baseParams, contentLength: fetched.contentLength },
|
|
|
|
| 1833 |
state.aborted = true;
|
| 1834 |
const ab = $('btn-abort');
|
| 1835 |
if (ab) ab.disabled = true;
|
| 1836 |
+
// Iterate every registered op (worker terminate, fetch AbortController):
|
| 1837 |
+
// worker.terminate() alone leaves the Promise pending forever, and
|
| 1838 |
+
// fetch without a signal can hang on slow connections. Each fn is
|
| 1839 |
+
// expected to also resolve / reject its own awaiting promise.
|
| 1840 |
+
const n = state.abortHandlers.size;
|
| 1841 |
+
for (const fn of state.abortHandlers) {
|
| 1842 |
+
try { fn(); } catch { /* keep iterating */ }
|
|
|
|
| 1843 |
}
|
| 1844 |
+
state.abortHandlers.clear();
|
| 1845 |
+
logLine(n > 0
|
| 1846 |
+
? `Abort requested — cancelled ${n} in-flight op${n === 1 ? '' : 's'}.`
|
| 1847 |
+
: 'Abort requested — will stop between variants.');
|
| 1848 |
});
|
| 1849 |
}
|
| 1850 |
|
js/run/source.js
CHANGED
|
@@ -108,7 +108,7 @@ export function hostedSource() {
|
|
| 108 |
// (fraction, downloaded, total). The returned `wasDownloaded` flag
|
| 109 |
// distinguishes a fresh download from a cache hit so the caller can
|
| 110 |
// decide whether to mark the variant for post-run eviction.
|
| 111 |
-
async opfsHandleForModel(repo, file, onProgress) {
|
| 112 |
const cached = await getOpfsFileHandle(repo, file, { create: false }).catch(() => null);
|
| 113 |
if (cached) {
|
| 114 |
const f = await cached.getFile();
|
|
@@ -119,8 +119,10 @@ export function hostedSource() {
|
|
| 119 |
}
|
| 120 |
|
| 121 |
// Cache miss — download from HF straight into a writable OPFS stream.
|
|
|
|
|
|
|
| 122 |
const url = `https://huggingface.co/${repo}/resolve/main/${file}`;
|
| 123 |
-
const resp = await fetch(url);
|
| 124 |
if (!resp.ok) {
|
| 125 |
throw new Error(`Download failed: ${resp.status} ${resp.statusText}`);
|
| 126 |
}
|
|
@@ -150,7 +152,7 @@ export function hostedSource() {
|
|
| 150 |
}
|
| 151 |
},
|
| 152 |
|
| 153 |
-
async fetchModel(repo, file) {
|
| 154 |
// Cache hit → stream the OPFS file straight out.
|
| 155 |
try {
|
| 156 |
const handle = await getOpfsFileHandle(repo, file, { create: false });
|
|
@@ -164,9 +166,10 @@ export function hostedSource() {
|
|
| 164 |
}
|
| 165 |
} catch { /* miss — fall through */ }
|
| 166 |
|
| 167 |
-
// Miss: fetch from HF, tee to OPFS + caller.
|
|
|
|
| 168 |
const url = `https://huggingface.co/${repo}/resolve/main/${file}`;
|
| 169 |
-
const resp = await fetch(url);
|
| 170 |
if (!resp.ok) {
|
| 171 |
throw new Error(`Download failed: ${resp.status} ${resp.statusText}`);
|
| 172 |
}
|
|
|
|
| 108 |
// (fraction, downloaded, total). The returned `wasDownloaded` flag
|
| 109 |
// distinguishes a fresh download from a cache hit so the caller can
|
| 110 |
// decide whether to mark the variant for post-run eviction.
|
| 111 |
+
async opfsHandleForModel(repo, file, onProgress, signal) {
|
| 112 |
const cached = await getOpfsFileHandle(repo, file, { create: false }).catch(() => null);
|
| 113 |
if (cached) {
|
| 114 |
const f = await cached.getFile();
|
|
|
|
| 119 |
}
|
| 120 |
|
| 121 |
// Cache miss — download from HF straight into a writable OPFS stream.
|
| 122 |
+
// signal lets the caller cancel: fetch + reader.read both reject with
|
| 123 |
+
// AbortError when it fires, and the catch below propagates that up.
|
| 124 |
const url = `https://huggingface.co/${repo}/resolve/main/${file}`;
|
| 125 |
+
const resp = await fetch(url, { signal });
|
| 126 |
if (!resp.ok) {
|
| 127 |
throw new Error(`Download failed: ${resp.status} ${resp.statusText}`);
|
| 128 |
}
|
|
|
|
| 152 |
}
|
| 153 |
},
|
| 154 |
|
| 155 |
+
async fetchModel(repo, file, signal) {
|
| 156 |
// Cache hit → stream the OPFS file straight out.
|
| 157 |
try {
|
| 158 |
const handle = await getOpfsFileHandle(repo, file, { create: false });
|
|
|
|
| 166 |
}
|
| 167 |
} catch { /* miss — fall through */ }
|
| 168 |
|
| 169 |
+
// Miss: fetch from HF, tee to OPFS + caller. signal lets the caller
|
| 170 |
+
// abort the network request; the tee'd reader inherits the abort.
|
| 171 |
const url = `https://huggingface.co/${repo}/resolve/main/${file}`;
|
| 172 |
+
const resp = await fetch(url, { signal });
|
| 173 |
if (!resp.ok) {
|
| 174 |
throw new Error(`Download failed: ${resp.status} ${resp.statusText}`);
|
| 175 |
}
|