Mike0021 commited on
Commit
98c19a7
·
verified ·
1 Parent(s): 9036e71

Record LLM prompt size metrics

Browse files
Files changed (3) hide show
  1. README.md +1 -1
  2. app.js +10 -0
  3. workers/llm-worker.js +22 -0
README.md CHANGED
@@ -75,7 +75,7 @@ Use **Run benchmark suite** after loading models to execute the current stack's
75
 
76
  Microphone startup is capped at 15 seconds. If browser permissions, fake media devices, or audio worklet startup hang, the active mic benchmark is cancelled and the event log records the failure instead of leaving a silent pending run. Loopback audio is also followed by an explicit ASR flush after the synthesized prompt and trailing silence have been fed, so sticky VAD/STT candidates fail or finish as rows instead of waiting for the global benchmark timeout.
77
 
78
- Exported result rows include runtime metadata under `stack.environment`: page URL, secure-context status, browser user agent, CPU concurrency hint, device memory hint when exposed, WebGPU availability, WebGPU adapter info/features/software-adapter classification when exposed by the browser, microphone or loopback sample rate, and sanitized microphone track settings such as channel count, sample rate, latency, and audio-processing flags when the browser reports them. The benchmark stack also records voice, TTS steps, VAD silence, partial-ASR state, full-stack model load time as `modelLoadMs`, LLM worker acknowledgement latency as `llmStartMs`, first-response latency, and scheduled full-audio completion as `audioEndMs` / `speechEndToAudioEndMs`. Keep that metadata with WebGPU results so latency numbers can be compared across machines.
79
 
80
  The latency panel counts **speech end** from the detected acoustic end of speech, not from the later worker event that closes the turn after trailing silence. The separate **VAD close delay** metric shows how much silence was observed before the turn was finalized.
81
 
 
75
 
76
  Microphone startup is capped at 15 seconds. If browser permissions, fake media devices, or audio worklet startup hang, the active mic benchmark is cancelled and the event log records the failure instead of leaving a silent pending run. Loopback audio is also followed by an explicit ASR flush after the synthesized prompt and trailing silence have been fed, so sticky VAD/STT candidates fail or finish as rows instead of waiting for the global benchmark timeout.
77
 
78
+ Exported result rows include runtime metadata under `stack.environment`: page URL, secure-context status, browser user agent, CPU concurrency hint, device memory hint when exposed, WebGPU availability, WebGPU adapter info/features/software-adapter classification when exposed by the browser, microphone or loopback sample rate, and sanitized microphone track settings such as channel count, sample rate, latency, and audio-processing flags when the browser reports them. The benchmark stack also records voice, TTS steps, VAD silence, partial-ASR state, full-stack model load time as `modelLoadMs`, LLM worker acknowledgement latency as `llmStartMs`, LLM prompt build time and input size as `llmPromptBuildMs` / `llmPromptTokens`, first-response latency, and scheduled full-audio completion as `audioEndMs` / `speechEndToAudioEndMs`. Keep that metadata with WebGPU results so latency numbers can be compared across machines.
79
 
80
  The latency panel counts **speech end** from the detected acoustic end of speech, not from the later worker event that closes the turn after trailing silence. The separate **VAD close delay** metric shows how much silence was observed before the turn was finalized.
81
 
app.js CHANGED
@@ -1220,6 +1220,14 @@ function onLlmMessage(event) {
1220
  setTile("llm", "Generating", "active");
1221
  return;
1222
  }
 
 
 
 
 
 
 
 
1223
  if (message.type === "token") {
1224
  if (message.turnId !== state.activeTurnId) return;
1225
  if (state.awaitingFirstToken) {
@@ -1535,6 +1543,8 @@ function startBenchmark(kind, prompt, { referenceText = "", timeoutMs = 120000 }
1535
  sttCer: null,
1536
  vadCloseDelayMs: null,
1537
  llmStartMs: null,
 
 
1538
  firstTokenMs: null,
1539
  firstTtsQueuedMs: null,
1540
  firstTtsSynthesisMs: null,
 
1220
  setTile("llm", "Generating", "active");
1221
  return;
1222
  }
1223
+ if (message.type === "prompt") {
1224
+ if (message.turnId !== state.activeTurnId) return;
1225
+ if (state.activeBenchmark) {
1226
+ state.activeBenchmark.llmPromptBuildMs = message.promptBuildMs ?? null;
1227
+ state.activeBenchmark.llmPromptTokens = message.inputTokens ?? null;
1228
+ }
1229
+ return;
1230
+ }
1231
  if (message.type === "token") {
1232
  if (message.turnId !== state.activeTurnId) return;
1233
  if (state.awaitingFirstToken) {
 
1543
  sttCer: null,
1544
  vadCloseDelayMs: null,
1545
  llmStartMs: null,
1546
+ llmPromptBuildMs: null,
1547
+ llmPromptTokens: null,
1548
  firstTokenMs: null,
1549
  firstTtsQueuedMs: null,
1550
  firstTtsSynthesisMs: null,
workers/llm-worker.js CHANGED
@@ -62,12 +62,19 @@ function reportProgress(label) {
62
  async function generate(messages, turnId, { sentenceLimit = 1 } = {}) {
63
  stoppingCriteria = new InterruptableStoppingCriteria();
64
  self.postMessage({ type: "start", turnId });
 
65
  const promptMessages = normalizeMessages(messages);
66
  const inputs = tokenizer.apply_chat_template(promptMessages, {
67
  add_generation_prompt: true,
68
  enable_thinking: false,
69
  return_dict: true,
70
  });
 
 
 
 
 
 
71
 
72
  let firstTokenAt = 0;
73
  let tokenCount = 0;
@@ -138,3 +145,18 @@ function normalizeMessages(messages) {
138
  const content = messages.map((message) => `${message.role}: ${message.content}`).join("\n");
139
  return [{ role: "user", content }];
140
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  async function generate(messages, turnId, { sentenceLimit = 1 } = {}) {
63
  stoppingCriteria = new InterruptableStoppingCriteria();
64
  self.postMessage({ type: "start", turnId });
65
+ const promptBuildStartedAt = performance.now();
66
  const promptMessages = normalizeMessages(messages);
67
  const inputs = tokenizer.apply_chat_template(promptMessages, {
68
  add_generation_prompt: true,
69
  enable_thinking: false,
70
  return_dict: true,
71
  });
72
+ self.postMessage({
73
+ type: "prompt",
74
+ turnId,
75
+ inputTokens: inputTokenCount(inputs),
76
+ promptBuildMs: performance.now() - promptBuildStartedAt,
77
+ });
78
 
79
  let firstTokenAt = 0;
80
  let tokenCount = 0;
 
145
  const content = messages.map((message) => `${message.role}: ${message.content}`).join("\n");
146
  return [{ role: "user", content }];
147
  }
148
+
149
+ function inputTokenCount(inputs) {
150
+ const inputIds = inputs?.input_ids;
151
+ if (!inputIds) return null;
152
+ if (Array.isArray(inputIds)) {
153
+ return Array.isArray(inputIds[0]) ? inputIds[0].length : inputIds.length;
154
+ }
155
+ if (Array.isArray(inputIds.dims) && inputIds.dims.length > 0) {
156
+ return inputIds.dims[inputIds.dims.length - 1];
157
+ }
158
+ if (Number.isFinite(inputIds.size)) return inputIds.size;
159
+ if (Number.isFinite(inputIds.length)) return inputIds.length;
160
+ if (Number.isFinite(inputIds.data?.length)) return inputIds.data.length;
161
+ return null;
162
+ }