Spaces:
Running
Running
Download scripts/wllama-bench/bench.ts from Felladrin/MiniSearch: direct link, hf CLI and curl.
- Browser
- Download file 6.24 kB
-
https://huggingface.co/spaces/Felladrin/MiniSearch/resolve/main/scripts/wllama-bench/bench.ts
- Command line
-
hf download hf://spaces/Felladrin/MiniSearch/scripts/wllama-bench/bench.ts
-
curl -L -o bench.ts https://huggingface.co/spaces/Felladrin/MiniSearch/resolve/main/scripts/wllama-bench/bench.ts
6.24 kB
| // Browser side of the wllama benchmark. Runs inside the page that run.mjs | |
| // opens, so a model is loaded and prompted through MiniSearch's own modules: | |
| // the catalog entry, the warmup-then-load in initializeWllama, and the chat | |
| // messages the search flow builds from results and page excerpts. | |
| import { | |
| getSettings, | |
| queryPubSub, | |
| settingsPubSub, | |
| updateLlmTextSearchResults, | |
| updatePageContents, | |
| } from "../../client/modules/pubSub"; | |
| import { | |
| defaultContextSize, | |
| getDefaultChatMessages, | |
| getFormattedSearchResults, | |
| } from "../../client/modules/textGenerationUtilities"; | |
| import type { TextSearchResults } from "../../client/modules/types"; | |
| import { initializeWllama, wllamaModels } from "../../client/modules/wllama"; | |
| interface BenchCase { | |
| name: string; | |
| query: string; | |
| results: TextSearchResults; | |
| pages: Record<string, string>; | |
| } | |
| interface BenchOptions { | |
| /** A catalog id, or `owner/repo/file.gguf` for a model not in the catalog. */ | |
| model: string; | |
| device: "webgpu" | "cpu"; | |
| /** Defaults to MiniSearch's own `cpuThreads` setting. */ | |
| threads?: number; | |
| cases: BenchCase[]; | |
| chatTemplateKwargs?: Record<string, unknown>; | |
| reasoningBudgetTokens?: number; | |
| /** Seconds before an answer is cut off and marked as timed out. */ | |
| caseTimeoutSeconds: number; | |
| /** Replaces the default system prompt, to measure a change to it. */ | |
| systemPrompt?: string; | |
| } | |
| function resolveModel(id: string) { | |
| const catalogModel = wllamaModels[id]; | |
| if (catalogModel) return catalogModel; | |
| const [owner, repo, ...file] = id.split("/"); | |
| if (!owner || !repo || file.length === 0) { | |
| throw new Error( | |
| `"${id}" is neither a catalog id nor an owner/repo/file.gguf path`, | |
| ); | |
| } | |
| // Starts from a catalog entry's shared defaults, minus the thinking budget | |
| // that only some entries set, as a new catalog entry would. | |
| return { | |
| ...Object.values(wllamaModels)[0], | |
| reasoningBudgetTokens: undefined, | |
| label: id, | |
| hfRepoId: `${owner}/${repo}`, | |
| hfFilePath: file.join("/"), | |
| }; | |
| } | |
| async function runBench(options: BenchOptions) { | |
| if (options.device === "webgpu" && !(await navigator.gpu?.requestAdapter())) { | |
| throw new Error("This browser exposes no WebGPU adapter"); | |
| } | |
| const catalogModel = resolveModel(options.model); | |
| const model = { | |
| ...catalogModel, | |
| chatTemplateKwargs: | |
| options.chatTemplateKwargs ?? catalogModel.chatTemplateKwargs, | |
| reasoningBudgetTokens: | |
| options.reasoningBudgetTokens ?? catalogModel.reasoningBudgetTokens, | |
| }; | |
| if (options.systemPrompt) { | |
| settingsPubSub[0]({ ...getSettings(), systemPrompt: options.systemPrompt }); | |
| } | |
| const loadStart = performance.now(); | |
| // Mirrors initializeWllamaInstance in client/modules/textGenerationWithWllama.ts, | |
| // with the device forced instead of detected. | |
| const wllama = await initializeWllama(model.hfRepoId, model.hfFilePath, { | |
| wllama: { suppressNativeLog: false, allowOffline: true }, | |
| chatTemplateKwargs: model.chatTemplateKwargs, | |
| model: { | |
| n_threads: options.threads ?? getSettings().cpuThreads, | |
| n_ctx: model.contextSize, | |
| n_batch: 512, | |
| cache_type_k: model.cacheTypeK, | |
| cache_type_v: model.cacheTypeV, | |
| flash_attn: model.flash_attn, | |
| embeddings: false, | |
| n_gpu_layers: options.device === "webgpu" ? 99999 : 0, | |
| ...(model.reasoningBudgetTokens | |
| ? { reasoning_budget_tokens: model.reasoningBudgetTokens } | |
| : {}), | |
| }, | |
| }); | |
| const loadSeconds = (performance.now() - loadStart) / 1000; | |
| const cases = []; | |
| for (const benchCase of options.cases) { | |
| queryPubSub[0](benchCase.query); | |
| updateLlmTextSearchResults(benchCase.results); | |
| updatePageContents(benchCase.pages); | |
| const messages = getDefaultChatMessages( | |
| await getFormattedSearchResults(model.shouldIncludeUrlsOnPrompt), | |
| ); | |
| const start = performance.now(); | |
| let firstAnswerAt: number | undefined; | |
| let thinkingTokens = 0; | |
| let answerTokens = 0; | |
| let reasoning = ""; | |
| let answer = ""; | |
| let timings: Record<string, number> | undefined; | |
| const abortController = new AbortController(); | |
| const timeout = setTimeout( | |
| () => abortController.abort(), | |
| options.caseTimeoutSeconds * 1000, | |
| ); | |
| const stream = await wllama.createChatCompletion({ | |
| messages, | |
| max_tokens: defaultContextSize, | |
| stream: true, | |
| abortSignal: abortController.signal, | |
| ...model.getSampling(), | |
| ...(model.chatTemplateKwargs | |
| ? { chat_template_kwargs: model.chatTemplateKwargs } | |
| : {}), | |
| }); | |
| try { | |
| for await (const chunk of stream) { | |
| const delta = chunk.choices[0]?.delta as | |
| | { content?: string; reasoning_content?: string } | |
| | undefined; | |
| if (delta?.reasoning_content) { | |
| thinkingTokens++; | |
| reasoning += delta.reasoning_content; | |
| } | |
| if (delta?.content) { | |
| firstAnswerAt ??= performance.now(); | |
| answerTokens++; | |
| answer += delta.content; | |
| } | |
| // The engine's own timings ride on the stream's chunks; the last one | |
| // covers the whole completion. | |
| const chunkTimings = (chunk as { timings?: Record<string, number> }) | |
| .timings; | |
| if (chunkTimings) timings = chunkTimings; | |
| } | |
| } catch (error) { | |
| if (!abortController.signal.aborted) throw error; | |
| } finally { | |
| clearTimeout(timeout); | |
| } | |
| const end = performance.now(); | |
| cases.push({ | |
| name: benchCase.name, | |
| promptTokens: timings?.prompt_n, | |
| promptTokensPerSecond: timings?.prompt_per_second, | |
| generatedTokensPerSecond: timings?.predicted_per_second, | |
| thinkingTokens, | |
| answerTokens, | |
| secondsToFirstAnswerToken: | |
| firstAnswerAt === undefined | |
| ? undefined | |
| : (firstAnswerAt - start) / 1000, | |
| totalSeconds: (end - start) / 1000, | |
| timedOut: abortController.signal.aborted, | |
| // Kept so the scorer can tell an echoed instruction from an answer. | |
| prompt: messages[0].content, | |
| reasoning, | |
| answer, | |
| }); | |
| } | |
| await wllama.exit(); | |
| return { model: model.label, device: options.device, loadSeconds, cases }; | |
| } | |
| Object.assign(window, { runBench, benchReady: true }); | |