fix(qmd): measure embed throughput after warm-up and fail on embed errors

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Julian lechnerandClaude Opus 5.5 committed 2026-10-08 11:47:48 +02:00
1 parent d8a5ad7492
commit 29e712e751
2 files changed
+23 -10

No files matched your search

+7 -1
View File
@@ -1,6 +1,6 @@
import { test } from 'node:test'; import { test } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { passes, fixtureDocuments, describeDevices } from '../../shared/qmd/qmd-benchmark.mjs'; import { passes, fixtureDocuments, describeDevices, embedThroughput } from '../../shared/qmd/qmd-benchmark.mjs';
test('both thresholds must pass', () => { test('both thresholds must pass', () => {
assert.equal(passes({ querySeconds: 5, chunksPerSecond: 20 }, {}), true); assert.equal(passes({ querySeconds: 5, chunksPerSecond: 20 }, {}), true);
@@ -23,3 +23,9 @@ test('fixture is deterministic and large enough for throughput', () => {
assert.deepEqual(a, fixtureDocuments(100)); assert.deepEqual(a, fixtureDocuments(100));
assert.ok(a.every((d) => d.text.length > 2500)); assert.ok(a.every((d) => d.text.length > 2500));
}); });
test('embed throughput rejects errors and empty runs', () => {
assert.deepEqual(embedThroughput({ chunksEmbedded: 200, durationMs: 4000, errors: 0 }), { chunksPerSecond: 50, ok: true });
assert.equal(embedThroughput({ chunksEmbedded: 200, durationMs: 4000, errors: 1 }).ok, false);
assert.equal(embedThroughput({ chunksEmbedded: 0, durationMs: 4000, errors: 0 }).ok, false);
});
+16 -9
View File
@@ -24,6 +24,10 @@ export function fixtureDocuments(count) {
}); });
} }
export function embedThroughput({ chunksEmbedded, durationMs, errors }) {
return { chunksPerSecond: chunksEmbedded / Math.max(durationMs / 1000, 0.001), ok: !errors && chunksEmbedded > 0 };
}
export function describeDevices({ gpu, devices, threads }) { export function describeDevices({ gpu, devices, threads }) {
const gpus = gpu ? `${String(gpu).toUpperCase()}: ${devices.join(', ') || 'unnamed device'}` : 'no GPU'; const gpus = gpu ? `${String(gpu).toUpperCase()}: ${devices.join(', ') || 'unnamed device'}` : 'no GPU';
return `${gpus}; CPU threads: ${threads}`; return `${gpus}; CPU threads: ${threads}`;
@@ -33,10 +37,13 @@ export function describeDevices({ gpu, devices, threads }) {
async function detect() { async function detect() {
const { getLlama } = await import(pathToFileURL(join(qmdPackageDir(), 'node_modules', 'node-llama-cpp', 'dist', 'index.js')).href); const { getLlama } = await import(pathToFileURL(join(qmdPackageDir(), 'node_modules', 'node-llama-cpp', 'dist', 'index.js')).href);
const llama = await getLlama({ gpu: 'auto' }); const llama = await getLlama({ gpu: 'auto' });
const gpu = llama.gpu; try {
const devices = gpu ? await llama.getGpuDeviceNames() : []; const gpu = llama.gpu;
await llama.dispose(); const devices = gpu ? await llama.getGpuDeviceNames() : [];
return { gpu, devices, threads: availableParallelism() }; return { gpu, devices, threads: availableParallelism() };
} finally {
await llama.dispose();
}
} }
async function measure(device) { async function measure(device) {
@@ -52,9 +59,9 @@ async function measure(device) {
const store = await createStore({ dbPath: join(work, 'index.sqlite'), config: { collections: { bench: { path: docs, pattern: '**/*.md' } } } }); const store = await createStore({ dbPath: join(work, 'index.sqlite'), config: { collections: { bench: { path: docs, pattern: '**/*.md' } } } });
try { try {
await store.update(); await store.update();
await store.searchVector('warm up the embedding model', { limit: 1 }); await store.embed({ collection: 'bench' }); // warm-up: loads (and on a first run downloads) the model
const embedded = await store.embed({ collection: 'bench' }); const embedded = await store.embed({ collection: 'bench', force: true });
const chunksPerSecond = embedded.chunksEmbedded / Math.max(embedded.durationMs / 1000, 0.001); const { chunksPerSecond, ok } = embedThroughput(embedded);
const question = 'Which part explains how upload retries behave during incidents?'; const question = 'Which part explains how upload retries behave during incidents?';
await store.search({ query: question, collections: ['bench'], limit: 5 }); await store.search({ query: question, collections: ['bench'], limit: 5 });
const times = []; const times = [];
@@ -64,8 +71,8 @@ async function measure(device) {
times.push((performance.now() - started) / 1000); times.push((performance.now() - started) / 1000);
} }
const querySeconds = times.sort((a, b) => a - b)[1]; const querySeconds = times.sort((a, b) => a - b)[1];
const result = { device, querySeconds: Number(querySeconds.toFixed(3)), chunksPerSecond: Number(chunksPerSecond.toFixed(1)) }; const result = { device, querySeconds: Number(querySeconds.toFixed(3)), chunksPerSecond: Number(chunksPerSecond.toFixed(1)), embedErrors: embedded.errors };
return { ...result, passed: passes(result) }; return { ...result, passed: ok && passes(result) };
} finally { } finally {
await store.close(); await store.close();
} }