Benchmark local LLMs reproducibly: TTFT, throughput, memory, quality, context, and tools
Create a reproducible Linux and NVIDIA benchmark that separates time to first meaningful content, end-to-end latency, exact server-reported token throughput, memory phases, deterministic task quality, long-context retrieval, structured output, and tool-call correctness. Results remain tied to immutable artifacts, scorer revisions, deployment flags, and host state.
Generate workload-specific evidence that supports a local model deployment decision without converting product claims, incomparable artifacts, approximate tokenizer counts, successful-survivor latency, or one synthetic aggregate score into misleading facts.
- Ubuntu Server 24.04.2 LTS
- Node.js 22.22.2
- NVIDIA driver 570.172.08
- CUDA runtime 12.8
- Dedicated measurement window Use an isolated lab host or an exclusive scheduler reservation. Pause unrelated GPU jobs, package upgrades, backups, indexing, and power-management experiments. If exclusivity is impossible, record competing processes continuously and classify the evidence as shared-host.
Review ps, systemd units, nvidia-smi processes, load, and scheduled jobs before warmup. - Pinned inference deployment Pin the engine, model artifact, tokenizer, chat template, dtype or quantization, context limit, batching, cache, accelerator placement, startup arguments, driver, and relevant libraries. A display name is not an immutable identity.
- Reviewed workload and quality gates Approve synthetic prompts, deterministic scorers, concurrency levels, context buckets, repetitions, warmups, and mandatory pass thresholds before any candidate results are visible.
- Exact token accounting or explicit absence Use completion_tokens emitted by the tested server for throughput. If exact usage is unavailable, publish throughput as unavailable instead of tokenizing output with a possibly different client tokenizer.
- Privacy-safe fixtures Use synthetic, licensed, or specifically approved evaluation data. The reference harness stores hashes and scores, not raw prompts, completions, tool arguments, customer content, or secrets.
OneLiners never runs these steps or stores secrets. Review placeholders, versions, current state, and change-control requirements before using a command.
Full guide
What you will build
- A versioned benchmark manifest that identifies host, accelerator, driver, power state, engine, model artifact, tokenizer, template, quantization, context, runtime flags, and measurement policy.
- A dependency-free streaming client that measures semantic TTFT, E2E, and exact server-token throughput while preserving unavailable metrics as unavailable.
- Deterministic scorers for exact instruction following, required facts, strict JSON, forced tools, and long-context sentinels, plus exact PID/cgroup/GPU memory telemetry.
- A workload-specific decision matrix and reversible canary procedure that separate captured measurements from vendor positioning and illustrative examples.
- You can reproduce the measurement or explain precisely why another result is not comparable.
- You can select deployments for interactive, batch, structured automation, and long-context work without creating a misleading universal score.
- You can identify measurement drift, missing token accounting, silent truncation, scorer weakness, memory attribution errors, and successful-survivor bias.
- You can publish captured facts with manifests and hashes while retaining neither raw prompts nor raw completions.
Architecture
How the parts fit together
A reviewed decision plan freezes gates before measurement. The deployment manifest identifies the complete system under test, while a workload manifest identifies synthetic prompts, generation limits, and deterministic scorers. A loopback-only streaming harness records semantic latency and exact usage, an independent collector records memory phases, and an offline scorer converts ephemeral output into retained correctness evidence. Reports and hashes feed a workload-specific matrix, and a reversible canary validates the selected deployment in the real service path.
- Owners approve workload-specific gates and freeze the benchmark revision.
- The operator captures host and deployment provenance, then verifies the machine is in the reviewed state.
- Warmups exercise every workload and concurrency but remain labelled and excluded from primary statistics.
- Measured runs use deterministic shuffled order and preserve every success, timeout, rejection, and invalid response.
- Raw output is scored in memory; retained reports contain hashes, metrics, result classes, and scorer identity.
- Memory telemetry is aligned with benchmark phases and reviewed for contention or drift.
- The decision matrix rejects candidates that miss mandatory gates and selects only within the relevant workload.
- A reversible canary checks production behavior and triggers revalidation after material change.
Assumptions
- The canonical reproducibility profile is Ubuntu Server 24.04.2 LTS with an NVIDIA accelerator and current reviewed driver; other platforms require a new manifest and should not be merged into the same cohort.
- Each candidate has exclusive or explicitly documented access to its CPU, RAM, GPU, filesystem, and network path during the measurement window.
- The inference API is loopback-only and supplies exact completion token usage when throughput is reported.
- Artifacts, tokenizers, templates, quantization or dtype, context, and runtime flags can be pinned and hashed or identified by immutable revision.
- Synthetic fixtures are suitable for retention as hashes, but raw prompts and completions are intentionally not stored.
- Deterministic scorers provide minimum task correctness and do not claim broad human preference, factuality, safety, or domain competence.
Key concepts
- TTFT
- Time from immediately before the request begins until the first non-empty user-visible text or actual tool-call delta. HTTP headers, keepalives, role events, and empty frames do not count.
- End-to-end latency
- Time from request start through completion of the response stream, including connection, queueing, prefill, generation, and final framing.
- Generation throughput
- Exact completion tokens reported by the tested server divided by time from first meaningful output through stream completion. It is unavailable when exact usage is absent.
- Aggregate throughput
- Completed exact output tokens per wall-clock second across a declared concurrency window, including the effect of queueing and failures.
- Warmup
- Predeclared requests that initialize model, kernels, allocators, and caches. They are retained for diagnostics but excluded from steady-state primary summaries.
- Deterministic scorer
- A versioned local rule that checks exact task requirements without model judgment, network calls, or repair of malformed output.
- Effective context
- The largest tested tokenizer-aware bucket that meets retrieval, latency, memory, and error gates; it is not the advertised configuration maximum.
- Deployment candidate
- The complete combination of model bytes, tokenizer, template, engine, flags, hardware, driver, and host state. Changing one creates a new candidate.
- Successful-survivor bias
- Reporting latency only for successful requests while excluding timeouts, rejections, invalid outputs, or crashes that users would experience.
Before you copy
Values used in this guide
{{benchmarkToken}}secretThrowaway API token for the reviewed loopback benchmark server.
Example: injected-by-secret-manager{{serverPid}}Exact host PID of the reviewed inference process or service leader.
Example: 4217{{cgroupPath}}Exact cgroup v2 directory for the deployment, or none only for a verified single-process server.
Example: /sys/fs/cgroup/system.slice/vllm-benchmark.serviceSecurity and production boundaries
- Keep the inference API on loopback or an isolated benchmark network. A performance test can generate sustained load and must not become an unauthenticated public endpoint.
- Synthetic fixtures avoid transmitting customer, employee, source-code, infrastructure, or credential data. Hashes can still reveal equality and should be handled as operational evidence.
- Tool-call workloads validate untrusted structured data only. The benchmark has no executor, external credentials, or permission to perform actions.
- Reports expose hardware capacity, engine versions, and performance characteristics. Restrict them according to infrastructure-information policy even when raw content is absent.
- Do not run an unreviewed model artifact with elevated privileges, host mounts, cloud credentials, SSH agents, or writable production paths.
Stop before continuing if
- Stop if the model artifact, tokenizer, chat template, quantization or dtype, engine, startup flags, driver, device placement, or workload revision is unknown.
- Stop if another workload uses the assigned CPU, RAM, GPU, cgroup, filesystem, or inference port during the measurement window.
- Stop if the server does not provide exact completion token usage and a stakeholder asks to substitute an approximate tokenizer count for throughput.
- Stop if fixtures or reports contain real secrets, personal data, customer content, proprietary code, or reachable tool actions.
- Stop if the harness loses request outcomes, silently truncates context, repairs malformed output, or excludes failures from denominators.
- Stop if thermals, power, clocks, swap, memory pressure, or process identity move outside the approved manifest state.
decision
Define the deployment decision and gates before measuring
Name the workload, user-visible objective, acceptable quality, concurrency, p95 latency, error budget, memory ceiling, privacy boundary, and operational constraints. Create separate decision rows for interactive chat, batch summarization, structured automation, and long-context retrieval rather than one overall winner.
Why this step matters
A benchmark is useful only when it answers a predeclared decision. Choosing metrics after seeing results encourages cherry-picking, while averaging unrelated workloads hides failure on mandatory behavior. Gates also clarify when the correct outcome is that no candidate is suitable.
What to understand
Define user-facing latency separately from capacity. TTFT matters for streaming interaction; aggregate completed tokens per second matters for batch service.
Treat structured-output and tool correctness as hard safety gates when downstream automation depends on them. Speed cannot compensate for invalid arguments.
Document whether the goal is one user, a concurrency target, or maximum sustainable load. Do not infer queueing behavior from a serial test.
System changes
- Creates an approved benchmark plan and decision matrix; no inference request is sent.
Interactive gate: exact/required quality >= 0.98, TTFT p95 <= 900 ms at concurrency 4, E2E p95 <= 8 s, error rate <= 0.5%, peak GPU memory <= 44 GiB.
Checkpoint: Gates are frozen
Continue whenEvery workload has mandatory quality, latency, error, memory, and security criteria approved before candidate labels are revealed.
Stop whenThe team asks for a universal score or has not agreed what failure means.
config
Create the dependency-free benchmark workspace
Create isolated directories for configuration, fixtures, source, and reports, then save the pinned Node package. Keep reports on a filesystem with enough space and restrictive permissions. The reference suite has no external JavaScript dependencies.
Why this step matters
A small auditable harness reduces measurement drift from dependency upgrades and limits where sensitive debug data could appear. Restrictive report permissions are still required even though the intended format omits raw content, because failures and future changes can violate that assumption.
What to understand
Use a new empty directory. The command overwrites package.json in the current benchmark directory.
Keep source and configuration under version control but review report retention separately; host identifiers and performance data may still be sensitive.
System changes
- Creates local directories, a manifest, and a private reports directory; it does not install packages or start a model.
Syntax explained
chmod 0700 reports- Restricts benchmark evidence to the current operating-system user.
private: true- Prevents accidental publication of the local benchmark package.
mkdir -p local-llm-bench/{config,fixtures,src,reports} && cd local-llm-bench && chmod 0700 reports && cat > package.json <<'JSON'
{
"name": "reproducible-local-llm-benchmark",
"version": "1.0.0",
"private": true,
"type": "module",
"engines": { "node": "22.22.2" },
"scripts": {
"check": "node --check src/benchmark.mjs && node --check src/score.mjs",
"benchmark": "node src/benchmark.mjs config/manifest.json config/workloads.json reports",
"score": "node src/score.mjs reports/results.ndjson config/workloads.json reports/quality.json"
}
}
JSONCreated local-llm-bench with mode 0700 reports directory; package declares Node 22.22.2 and zero dependencies.
Checkpoint: Workspace is isolated
find . -maxdepth 2 -printf '%M %p\n'Continue whenOnly the expected directories and package.json exist; reports is accessible only to the owner.
Stop whenThe path is shared, world-readable, or contains unrelated benchmark output.
config
Capture immutable host and deployment provenance
Save the manifest and replace every placeholder with measured host state and a concrete deployment identity. Record idle background services, thermal state, power and clock policy immediately before the run. Keep one manifest per candidate configuration.
Why this step matters
Performance and quality results are not properties of a product name. They are observations of a model artifact, tokenizer, template, engine, configuration, hardware, driver, power state, concurrency, and fixture revision. Without those identities, another operator cannot reproduce or interpret the numbers.
What to understand
Hash local weight, tokenizer, and template files with SHA-256. When a model registry is used, record the immutable revision plus a digest where available.
Record quantization or dtype precisely, not as a generic low precision label. Different quantizers and kernels change accuracy, speed, and memory.
Capture host state at measurement time. A nominal GPU model does not reveal power cap, clocks, thermals, other contexts, MIG mode, or driver behavior.
System changes
- Creates a non-secret manifest containing environment and deployment metadata.
Syntax explained
measurementPolicy- Freezes repetitions, warmups, timeouts, caps, concurrency, and retention behavior.
deployment- Identifies the complete effective inference configuration rather than only its model alias.
config/manifest.json{
"suiteRevision": "local-llm-benchmark-2026-07-29.1",
"verifiedAt": "2026-07-29",
"measurementPolicy": {
"warmups": 3,
"measuredRuns": 15,
"requestTimeoutMs": 120000,
"maxResponseBytes": 2097152,
"concurrencyLevels": [1, 4, 8],
"cooldownSeconds": 15,
"persistRawPrompts": false,
"persistRawOutputs": false
},
"host": {
"os": "Ubuntu Server 24.04.2 LTS",
"kernel": "6.8.0-64-generic",
"cpu": "AMD EPYC 9454P",
"logicalCpuCount": 96,
"ramBytes": 274877906944,
"gpu": ["NVIDIA L40S 48 GB"],
"driver": "570.172.08",
"cudaRuntime": "12.8",
"powerLimitWatts": [350],
"persistenceMode": true,
"clocksPolicy": "documented fixed lab profile",
"backgroundServices": "operator-must-replace-with-captured-list",
"thermalState": "operator-must-replace"
},
"deployment": {
"provider": "operator-must-replace",
"engineVersion": "operator-must-replace",
"baseUrl": "http://127.0.0.1:8000/v1",
"tokenEnvironment": "BENCHMARK_API_TOKEN",
"modelAlias": "benchmark-model",
"modelArtifactSha256": "operator-must-replace",
"tokenizerRevision": "operator-must-replace",
"chatTemplateSha256": "operator-must-replace",
"dtypeOrQuantization": "operator-must-replace",
"contextLimitTokens": 32768,
"startupArguments": ["operator-must-replace"],
"tensorParallelSize": 1,
"gpuMemoryUtilization": "operator-must-replace"
}
}Manifest validates with suite revision, host kernel/CPU/RAM/GPU/driver, power state, exact engine, artifact, tokenizer, template, dtype, context, startup flags, and measurement policy.
Checkpoint: Manifest has no unresolved identity
grep -n 'operator-must-replace' config/manifest.jsonContinue whenNo output and exit status 1.
Stop whenAny model, tokenizer, template, engine, runtime flag, device placement, or host state cannot be identified.
verification
Verify clocks, thermals, memory, and competing work
Capture the pre-run host evidence with standard Linux and NVIDIA tools. Review every process using the accelerator, current temperature, power draw and cap, clocks, persistence mode, CPU load, memory pressure, and swap. Save the command output separately from benchmark results.
Why this step matters
Resource state can dominate a local inference benchmark. Thermal throttling, swap, another CUDA context, dynamic clocks, or a scheduled job can create results that look like software differences. Capturing state before and during the run makes anomalies detectable instead of silently averaging them.
What to understand
The sample UUID is redacted because inventory identifiers may be sensitive. Preserve a stable local pseudonym or hash if reports leave the lab.
Do not change clocks or power limits merely to improve a result. If policy permits a fixed profile, document it and apply the same reviewed profile to every candidate.
A clean pre-run snapshot is necessary but not sufficient; the memory collector and ordered latency series reveal changes during measurement.
System changes
- Reads system and GPU telemetry; no setting is changed.
Syntax explained
--query-gpu- Selects explicit telemetry fields so evidence is machine-readable and comparable.
--format=csv- Produces a stable delimited snapshot without changing device state.
uname -a; lscpu; free -b; swapon --show; uptime; nvidia-smi --query-gpu=index,name,uuid,driver_version,temperature.gpu,power.draw,power.limit,clocks.current.sm,clocks.current.memory,memory.used,memory.total,persistence_mode --format=csvGPU 0, NVIDIA L40S, UUID GPU-REDACTED, 570.172.08, 34 C, 71.42 W, 350.00 W, 210 MHz, 405 MHz, 12 MiB, 46068 MiB, Enabled
Checkpoint: Host is in the approved benchmark state
nvidia-smi pmon -c 1 && ps -eo pid,comm,%cpu,%mem --sort=-%cpu | head -20Continue whenOnly the reviewed inference deployment uses the assigned GPU and no unexpected high-load process is active.
Stop whenThermal, power, swap, memory, accelerator ownership, or background-service state differs from the manifest.
config
Version synthetic workloads and deterministic scorers
Save the workload file, then create a deterministic context fixture with a sentinel at a recorded token bucket. Each workload declares its operational purpose, generation cap, and scorer before measurement begins.
Why this step matters
Task quality cannot be inferred from perplexity, speed, or a visually plausible answer. Deterministic scorers provide repeatable minimum correctness checks. They do not replace domain evaluation, but they prevent fast invalid output from winning and expose whether structured and tool behavior survives deployment changes.
What to understand
The exact scorer normalizes only outer whitespace; it does not repair wording. The substring scorer requires all approved facts but does not claim stylistic quality.
The JSON scorer validates exact keys, types, and constants independently of provider-side structured generation.
The tool scorer checks exact name and canonical arguments and never executes the proposed call.
Build context fixtures in fixed tokenizer buckets and record prompt hashes. Advertised context length is not evidence that retrieval remains correct near that boundary.
System changes
- Creates synthetic prompt and scoring configuration; no prompt is sent until the benchmark step.
Syntax explained
temperature: 0- Reduces sampling variance but does not guarantee deterministic execution.
seed- Requests a repeatable random stream where supported; it remains part of the manifest rather than a guarantee.
maxTokens- Bounds work and makes truncation visible through finish reason and quality failure.
config/workloads.json{
"revision": "workloads-2026-07-29.1",
"defaults": {
"temperature": 0,
"seed": 424242,
"stream": true
},
"workloads": [
{
"id": "exact-transform",
"purpose": "short deterministic instruction following",
"prompt": "Return exactly PORTABLE-LLM-OK and nothing else.",
"maxTokens": 32,
"score": { "kind": "exact", "expected": "PORTABLE-LLM-OK" }
},
{
"id": "bounded-summary",
"purpose": "medium generation throughput and instruction adherence",
"prompt": "Summarize this synthetic incident in exactly three bullets: service alpha returned 503 for 90 seconds; the upstream pool had zero healthy members; a configuration rollback restored health. Include duration, cause, and recovery.",
"maxTokens": 128,
"score": { "kind": "required-substrings", "values": ["90 seconds", "zero healthy", "rollback"] }
},
{
"id": "structured-ticket",
"purpose": "strict structured output",
"prompt": "Return a JSON object with status equal to resolved, severity equal to medium, and retry equal to false.",
"maxTokens": 96,
"responseFormat": {
"type": "json_schema",
"json_schema": {
"name": "ticket",
"strict": true,
"schema": {
"type": "object",
"additionalProperties": false,
"required": ["status", "severity", "retry"],
"properties": {
"status": { "const": "resolved" },
"severity": { "const": "medium" },
"retry": { "const": false }
}
}
}
},
"score": { "kind": "json-ticket" }
},
{
"id": "forced-tool",
"purpose": "tool name and argument correctness",
"prompt": "Use get_service_status for service synthetic-api in region lab-1.",
"maxTokens": 96,
"tool": {
"type": "function",
"function": {
"name": "get_service_status",
"description": "Returns synthetic service status; never executed by this benchmark.",
"parameters": {
"type": "object",
"additionalProperties": false,
"required": ["service", "region"],
"properties": {
"service": { "type": "string" },
"region": { "type": "string" }
}
}
}
},
"score": { "kind": "tool", "name": "get_service_status", "arguments": { "service": "synthetic-api", "region": "lab-1" } }
},
{
"id": "context-sentinel",
"purpose": "effective long-context retrieval",
"promptFile": "fixtures/context-sentinel.txt",
"maxTokens": 48,
"score": { "kind": "exact", "expected": "SENTINEL-7H3K-29QP" }
}
]
}Loaded 5 workloads: exact transform, bounded summary, structured ticket, forced tool, and context sentinel; revision workloads-2026-07-29.1.
Checkpoint: Workloads and scorers are reviewable
node -e "const w=require('./config/workloads.json'); console.log(w.revision,w.workloads.map(x=>x.id).join(','))"Continue whenworkloads-2026-07-29.1 followed by five unique workload identifiers.
Stop whenA scorer depends on subjective post-hoc judgment, hidden customer data, or a network service.
config
Install the streaming latency and throughput harness
Write the Node benchmark harness. It rejects remote targets, redirects, oversized responses, missing SSE, and absent credentials. It measures TTFT at the first non-empty text or tool delta, E2E at stream completion, and exact output tokens per second only when the server reports completion_tokens.
Why this step matters
Measurement definitions must be embedded in code rather than inferred from charts. Counting the first HTTP byte understates user-visible TTFT, while using a different client tokenizer can distort throughput. The harness preserves null for unavailable measures and retains failed requests in the evidence model.
What to understand
TTFT begins immediately before fetch and ends only at a non-empty user-visible content or real tool-call fragment. A connection, header, SSE comment, role delta, or reasoning-only frame does not qualify.
Generation duration begins at first meaningful content and ends when the response body completes. E2E includes connection, queueing, prefill, generation, and final framing.
Output tokens per second uses exact completion_tokens divided by measured generation time. When usage is absent, the value remains null and unavailableThroughput increments.
Raw text and tool fragments exist in memory only for immediate deterministic scoring. Persisted evidence contains prompt and output hashes, metric values, and scorer results.
System changes
- Creates the local benchmark executable; running it later sends synthetic prompts to one loopback endpoint and writes metrics.
Syntax explained
performance.now()- Provides monotonic high-resolution timing within the benchmark process.
stream_options.include_usage- Requests exact server usage; support must still be verified in the returned event.
throughputAvailable- Prevents a missing token count from becoming a fabricated zero or estimate.
src/benchmark.mjsimport { createHash } from "node:crypto";
import { mkdir, readFile, writeFile, appendFile } from "node:fs/promises";
import path from "node:path";
import { performance } from "node:perf_hooks";
import { scoreOutput } from "./score.mjs";
const [manifestPath, workloadsPath, outputDirectory] = process.argv.slice(2);
if (!manifestPath || !workloadsPath || !outputDirectory) {
throw new Error("Usage: node benchmark.mjs <manifest.json> <workloads.json> <output-directory>");
}
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
const workloadConfig = JSON.parse(await readFile(workloadsPath, "utf8"));
await mkdir(outputDirectory, { recursive: true });
const outputPath = path.join(outputDirectory, "results.ndjson");
await writeFile(outputPath, "");
function hash(value) {
return createHash("sha256").update(value).digest("hex");
}
function assertLoopback(raw) {
const url = new URL(raw);
if (url.protocol !== "http:" || url.username || url.password) throw new Error("Benchmark URL must be credential-free loopback HTTP.");
if (!["127.0.0.1", "[::1]", "localhost"].includes(url.hostname)) throw new Error("Remote benchmark targets require a separately reviewed profile.");
return url;
}
function percentile(values, p) {
if (!values.length) return null;
const ordered = [...values].sort((a, b) => a - b);
const index = Math.min(ordered.length - 1, Math.max(0, Math.ceil(p * ordered.length) - 1));
return ordered[index];
}
function deterministicOrder(items, seed) {
let state = seed >>> 0;
const copy = [...items];
for (let i = copy.length - 1; i > 0; i -= 1) {
state = (1664525 * state + 1013904223) >>> 0;
const j = state % (i + 1);
[copy[i], copy[j]] = [copy[j], copy[i]];
}
return copy;
}
async function loadPrompt(item) {
if (item.prompt) return item.prompt;
const text = await readFile(item.promptFile, "utf8");
if (Buffer.byteLength(text) > 524288) throw new Error("Prompt fixture exceeds 512 KiB.");
return text;
}
async function runOne(item, phase, runIndex, requestIndex, concurrency) {
const prompt = await loadPrompt(item);
const url = new URL("chat/completions", assertLoopback(manifest.deployment.baseUrl.endsWith("/") ? manifest.deployment.baseUrl : manifest.deployment.baseUrl + "/"));
const token = process.env[manifest.deployment.tokenEnvironment];
if (!token) throw new Error("Missing benchmark token environment variable.");
const body = {
model: manifest.deployment.modelAlias,
messages: [{ role: "user", content: prompt }],
temperature: workloadConfig.defaults.temperature,
seed: workloadConfig.defaults.seed,
max_tokens: item.maxTokens,
stream: true,
stream_options: { include_usage: true }
};
if (item.responseFormat) body.response_format = item.responseFormat;
if (item.tool) {
body.tools = [item.tool];
body.tool_choice = { type: "function", function: { name: item.tool.function.name } };
body.parallel_tool_calls = false;
}
const started = performance.now();
const response = await fetch(url, {
method: "POST",
redirect: "error",
headers: { authorization: "Bearer " + token, "content-type": "application/json", accept: "text/event-stream" },
body: JSON.stringify(body),
signal: AbortSignal.timeout(manifest.measurementPolicy.requestTimeoutMs)
});
if (!response.ok || !response.body) throw new Error("HTTP " + response.status);
if (!(response.headers.get("content-type") || "").toLowerCase().includes("text/event-stream")) {
throw new Error("Expected text/event-stream.");
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffered = "";
let bytes = 0;
let firstContentAt = null;
let text = "";
let finishReason = null;
let usage = null;
const toolFragments = new Map();
while (true) {
const part = await reader.read();
if (part.done) break;
bytes += part.value.byteLength;
if (bytes > manifest.measurementPolicy.maxResponseBytes) {
await reader.cancel();
throw new Error("Response exceeded byte cap.");
}
buffered += decoder.decode(part.value, { stream: true });
let boundary;
while ((boundary = buffered.indexOf("\n\n")) >= 0) {
const event = buffered.slice(0, boundary);
buffered = buffered.slice(boundary + 2);
for (const line of event.split("\n")) {
if (!line.startsWith("data:")) continue;
const data = line.slice(5).trim();
if (!data || data === "[DONE]") continue;
const parsed = JSON.parse(data);
usage = parsed.usage || usage;
for (const choice of parsed.choices || []) {
finishReason = choice.finish_reason || finishReason;
const delta = choice.delta || {};
if (typeof delta.content === "string" && delta.content.length) {
if (firstContentAt === null) firstContentAt = performance.now();
text += delta.content;
}
for (const call of delta.tool_calls || []) {
if (firstContentAt === null) firstContentAt = performance.now();
const current = toolFragments.get(call.index) || { name: "", arguments: "" };
if (call.function?.name) current.name += call.function.name;
if (call.function?.arguments) current.arguments += call.function.arguments;
toolFragments.set(call.index, current);
}
}
}
}
}
const ended = performance.now();
const completionTokens = Number.isInteger(usage?.completion_tokens) ? usage.completion_tokens : null;
const ttftMs = firstContentAt === null ? null : firstContentAt - started;
const generationMs = firstContentAt === null ? null : ended - firstContentAt;
const outputTokensPerSecond =
completionTokens !== null && generationMs !== null && generationMs > 0
? completionTokens / (generationMs / 1000)
: null;
const quality = scoreOutput(item, { text, toolCalls: [...toolFragments.values()] });
return {
schemaVersion: 1,
suiteRevision: manifest.suiteRevision,
deploymentDigest: hash(JSON.stringify(manifest.deployment)),
workloadRevision: workloadConfig.revision,
workloadId: item.id,
phase,
runIndex,
requestIndex,
concurrency,
promptSha256: hash(prompt),
outputSha256: hash(text + JSON.stringify([...toolFragments.values()])),
rawContentStored: false,
ttftMs,
endToEndMs: ended - started,
generationMs,
completionTokens,
outputTokensPerSecond,
throughputAvailable: completionTokens !== null,
finishReason,
failed: false,
error: null,
quality
};
}
async function runGroup(item, phase, runIndex, concurrency) {
const prompt = await loadPrompt(item);
const results = await Promise.all(Array.from({ length: concurrency }, async (_unused, requestIndex) => {
try {
return await runOne(item, phase, runIndex, requestIndex, concurrency);
} catch (error) {
const message = error instanceof Error ? error.message.replace(/\s+/g, " ").slice(0, 240) : "Unknown benchmark failure";
return {
schemaVersion: 1,
suiteRevision: manifest.suiteRevision,
deploymentDigest: hash(JSON.stringify(manifest.deployment)),
workloadRevision: workloadConfig.revision,
workloadId: item.id,
phase,
runIndex,
requestIndex,
concurrency,
promptSha256: hash(prompt),
outputSha256: null,
rawContentStored: false,
ttftMs: null,
endToEndMs: null,
generationMs: null,
completionTokens: null,
outputTokensPerSecond: null,
throughputAvailable: false,
finishReason: null,
failed: true,
error: { name: error instanceof Error ? error.name : "Error", message },
quality: { pass: false, reason: "transport or protocol failure" }
};
}
}));
for (const result of results) await appendFile(outputPath, JSON.stringify(result) + "\n");
}
for (const concurrency of manifest.measurementPolicy.concurrencyLevels) {
for (let index = 0; index < manifest.measurementPolicy.warmups; index += 1) {
for (const item of deterministicOrder(workloadConfig.workloads, 1000 + index)) {
await runGroup(item, "warmup", index, concurrency);
}
}
for (let index = 0; index < manifest.measurementPolicy.measuredRuns; index += 1) {
for (const item of deterministicOrder(workloadConfig.workloads, 424242 + index)) {
await runGroup(item, "measured", index, concurrency);
}
}
}
const measured = (await readFile(outputPath, "utf8")).trim().split("\n").filter(Boolean).map(JSON.parse).filter((item) => item.phase === "measured");
const summary = {};
for (const item of measured) {
const key = item.workloadId + "@c" + item.concurrency;
const bucket = summary[key] ||= { total: 0, errors: 0, ttft: [], e2e: [], tps: [], unavailableThroughput: 0 };
bucket.total += 1;
if (item.failed) bucket.errors += 1;
if (item.ttftMs !== null) bucket.ttft.push(item.ttftMs);
if (item.endToEndMs !== null) bucket.e2e.push(item.endToEndMs);
if (item.outputTokensPerSecond !== null) bucket.tps.push(item.outputTokensPerSecond);
else bucket.unavailableThroughput += 1;
}
const report = Object.fromEntries(Object.entries(summary).map(([key, value]) => [key, {
runs: value.total,
successfulLatencyRuns: value.e2e.length,
errorCount: value.errors,
ttftMs: { p50: percentile(value.ttft, 0.5), p95: percentile(value.ttft, 0.95) },
endToEndMs: { p50: percentile(value.e2e, 0.5), p95: percentile(value.e2e, 0.95) },
outputTokensPerSecond: value.tps.length ? { p50: percentile(value.tps, 0.5), p95: percentile(value.tps, 0.95) } : null,
unavailableThroughput: value.unavailableThroughput
}]));
await writeFile(path.join(outputDirectory, "latency-summary.json"), JSON.stringify({
measuredFactsOnly: true,
generatedAt: new Date().toISOString(),
manifestSha256: hash(JSON.stringify(manifest)),
workloadSha256: hash(JSON.stringify(workloadConfig)),
warmupsExcluded: true,
summary
}, null, 2) + "\n");
console.log("Wrote " + outputPath + " and latency-summary.json; raw content is present only in-memory for immediate scoring.");
node --check src/benchmark.mjs exits 0; static review confirms firstContentAt ignores empty role and keepalive events.
Checkpoint: Metric semantics pass source review
node --check src/benchmark.mjs && grep -n 'firstContentAt\|completion_tokens\|throughputAvailable' src/benchmark.mjsContinue whenSyntax check succeeds and reviewed lines show semantic first content plus exact server token accounting.
Stop whenThe implementation counts first network byte as TTFT, estimates tokens with another tokenizer, drops failures, or writes raw output.
config
Install and negative-test the deterministic quality scorer
Write the scorer and add offline fixtures containing correct, subtly wrong, malformed, truncated, and extra-field outputs. The scorer should fail wrong constants, missing required facts, invalid JSON, an incorrect tool name, malformed arguments, and additional arguments.
Why this step matters
A benchmark can be reproducible and still measure the wrong thing. Independent negative tests demonstrate that the scorer distinguishes correctness from plausible-looking output. Versioning the scorer hash also prevents a later relaxed rule from being compared with an earlier strict result.
What to understand
Keep deterministic pass/fail separate from optional human review. Human ratings require blinded presentation, multiple reviewers, a rubric, disagreement handling, and their own revision.
Do not send outputs to another model for an unlabelled judge score. If model-based evaluation is added, identify the judge, prompt, sampling, limitations, and human calibration.
Failures remain in the denominator for each workload and concurrency; never compute quality only on requests that returned valid text.
System changes
- Creates and runs local scoring logic; it never executes tool calls or sends data to another model.
Syntax explained
canonical arguments- Compares parsed JSON semantics without making object key order significant.
scorerSha256- Binds quality results to the exact scoring implementation.
src/score.mjsimport { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
function canonical(value) {
if (Array.isArray(value)) return value.map(canonical);
if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
return value;
}
export function scoreOutput(workload, output) {
const rule = workload.score;
if (!rule) return { pass: null, reason: "no deterministic scorer" };
if (rule.kind === "exact") return { pass: output.text.trim() === rule.expected, reason: "exact normalized text" };
if (rule.kind === "required-substrings") return { pass: rule.values.every((value) => output.text.includes(value)), reason: "all required substrings" };
if (rule.kind === "json-ticket") {
try {
const value = JSON.parse(output.text);
const pass = value && value.status === "resolved" && value.severity === "medium" && value.retry === false && Object.keys(value).length === 3;
return { pass, reason: "independent ticket object validation" };
} catch { return { pass: false, reason: "invalid JSON" }; }
}
if (rule.kind === "tool") {
const call = output.toolCalls[0];
if (!call || call.name !== rule.name) return { pass: false, reason: "wrong or missing tool name" };
try {
return { pass: JSON.stringify(canonical(JSON.parse(call.arguments))) === JSON.stringify(canonical(rule.arguments)), reason: "tool name and canonical arguments" };
} catch { return { pass: false, reason: "invalid tool arguments JSON" }; }
}
return { pass: null, reason: "unknown scorer" };
}
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
const [resultsPath, workloadsPath, outputPath] = process.argv.slice(2);
if (!resultsPath || !workloadsPath || !outputPath) throw new Error("Usage: node score.mjs <results.ndjson> <workloads.json> <quality.json>");
const workloadConfig = JSON.parse(await readFile(workloadsPath, "utf8"));
const knownIds = new Set(workloadConfig.workloads.map((item) => item.id));
const rows = (await readFile(resultsPath, "utf8")).trim().split("\n").filter(Boolean).map(JSON.parse);
const scored = rows.filter((row) => row.phase === "measured").map((row) => {
if (!knownIds.has(row.workloadId)) throw new Error("Unknown workload " + row.workloadId);
if (!row.quality || !["boolean", "object"].includes(typeof row.quality.pass)) {
throw new Error("Missing immediate deterministic score for " + row.workloadId);
}
return {
workloadId: row.workloadId,
concurrency: row.concurrency,
runIndex: row.runIndex,
pass: row.quality.pass,
reason: row.quality.reason,
outputSha256: row.outputSha256
};
});
const summary = {};
for (const row of scored) {
const key = row.workloadId + "@c" + row.concurrency;
const bucket = summary[key] ||= { pass: 0, fail: 0, unscored: 0 };
if (row.pass === true) bucket.pass += 1;
else if (row.pass === false) bucket.fail += 1;
else bucket.unscored += 1;
}
await writeFile(outputPath, JSON.stringify({
measuredFactsOnly: true,
rawContentStored: false,
scorerSha256: createHash("sha256").update(await readFile(new URL(import.meta.url))).digest("hex"),
workloadSha256: createHash("sha256").update(JSON.stringify(workloadConfig)).digest("hex"),
summary,
rows: scored
}, null, 2) + "\n");
console.log("Wrote deterministic quality scores to " + outputPath);
}
Scorer fixture suite: 18 passed, 0 failed; invalid JSON, extra key, wrong region, missing substring, and truncated sentinel all rejected.
Checkpoint: Scorer rejects every adversarial fixture
node --test test/scorer.test.mjsContinue whenAll positive and negative scorer fixtures pass with zero network requests.
Stop whenThe scorer repairs malformed output, ignores extra arguments, or depends on provider acceptance.
config
Collect memory by exact process identity and cgroup
Write the collector and run it with the reviewed server PID and optional service cgroup. It verifies /proc start time to detect PID reuse, samples process RSS and cgroup memory, and attributes NVIDIA compute memory only to the exact PID.
Why this step matters
Broad process-name matching and total system memory produce misleading attribution when multiple engines or workers exist. PID start-time validation avoids following a reused identifier, while a cgroup captures reviewed child processes. GPU process accounting must match the deployment's actual worker topology.
What to understand
Use an exact service cgroup when the engine spawns workers. A single parent PID may not own the model memory.
Capture four named phases: host idle before server start, server loaded but idle, warmup peak, and measured peak. Report them separately.
Keep one-second raw samples and calculate peaks after aligning timestamps with benchmark phases. Do not subtract baselines unless the method is declared.
System changes
- Writes a local CSV of process, cgroup, and GPU memory telemetry; it reads /proc and nvidia-smi without changing state.
Syntax explained
/proc/PID/stat field 22- Identifies process start time so PID reuse terminates collection.
memory.current- Reports current memory for the exact cgroup, including reviewed worker processes.
nvidia-smi compute-apps- Attributes reported GPU memory to the exact host process identifier.
src/collect-memory.sh#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 3 ]]; then
echo "Usage: collect-memory.sh <pid> <cgroup-path-or-none> <output.csv>" >&2
exit 64
fi
pid="$1"
cgroup="$2"
output="$3"
[[ "$pid" =~ ^[0-9]+$ ]] || { echo "PID must be numeric" >&2; exit 64; }
kill -0 "$pid" 2>/dev/null || { echo "PID is not running" >&2; exit 1; }
start_time="$(awk '{print $22}' /proc/"$pid"/stat)"
printf 'captured_at,pid,rss_bytes,cgroup_current_bytes,gpu_memory_bytes\n' > "$output"
while kill -0 "$pid" 2>/dev/null; do
current_start="$(awk '{print $22}' /proc/"$pid"/stat 2>/dev/null || true)"
[[ "$current_start" == "$start_time" ]] || { echo "PID reused; stopping" >&2; exit 1; }
rss_pages="$(awk '/VmRSS:/ { print int($2 * 1024) }' /proc/"$pid"/status)"
cgroup_bytes=""
if [[ "$cgroup" != "none" && -r "$cgroup/memory.current" ]]; then
cgroup_bytes="$(cat "$cgroup/memory.current")"
fi
gpu_mib="$(nvidia-smi --query-compute-apps=pid,used_memory --format=csv,noheader,nounits 2>/dev/null | awk -F, -v p="$pid" '$1+0 == p { sum += $2+0 } END { print sum+0 }')"
gpu_bytes="$((gpu_mib * 1024 * 1024))"
printf '%s,%s,%s,%s,%s\n' "$(date --iso-8601=ns)" "$pid" "$rss_pages" "$cgroup_bytes" "$gpu_bytes" >> "$output"
sleep 1
done
captured_at,pid,rss_bytes,cgroup_current_bytes,gpu_memory_bytes 2026-07-29T10:00:00.000000000+00:00,4217,8126464000,8421040128,23144177664
Checkpoint: Collector follows the intended deployment only
bash -n src/collect-memory.sh && ps -p {{serverPid}} -o pid,lstart,cmdContinue whenShell syntax passes and the exact PID command line matches the pinned deployment manifest.
Stop whenThe deployment uses untracked workers, PID namespace mapping is unknown, or the GPU is shared with another workload.
verification
Capture idle, loaded-idle, warmup, and measured memory phases
Measure host idle before the server starts, loaded-idle after the model is resident and quiet, warmup while caches and kernels initialize, and the complete measured run. Label every phase in a timeline and retain the peak plus steady-state distribution.
Why this step matters
One memory number cannot answer model footprint, runtime overhead, transient initialization, or concurrency headroom. Explicit phases show what the deployment reserves at rest and what it needs under the tested workload. Captured fixture values must never be presented as general product specifications.
What to understand
Start the collector before loading, mark phase timestamps in a separate event file, and stop it only after the final cooldown.
For multiple workers or GPUs, use a reviewed cgroup and device allocation, then publish per-device and aggregate numbers.
Report host RAM, cgroup memory, process RSS, and GPU memory as distinct measurements. They overlap conceptually and must not be summed into one total.
System changes
- Starts a read-only telemetry process and writes CSV samples until the inference server stops or the collector is interrupted.
Syntax explained
serverPid- Exact reviewed host process identifier; never derived from a broad pgrep pattern.
cgroupPath- Optional exact cgroup that captures a multi-process inference service.
Values stay on this page and are never sent or saved.
src/collect-memory.sh {{serverPid}} {{cgroupPath}} reports/memory.csvhost-idle GPU=12 MiB; loaded-idle GPU=22072 MiB; warmup-peak GPU=23118 MiB; measured-peak GPU=23546 MiB; values are captured fixture evidence, not universal claims.
Checkpoint: All four phases are represented
cut -d, -f1 reports/memory.csv | tail -n +2 | wc -lContinue whenA continuous sample series covers pre-load, loaded idle, warmup, every measured run, and cooldown.
Stop whenSamples are missing, PID identity changes, another workload uses the device, or phase timestamps cannot be aligned.
instruction
Warm up without contaminating measured samples
Run the declared number of warmups for every workload and concurrency under the same request path as measured trials. Persist their timing with phase=warmup for diagnostic plots, but exclude them from percentiles and quality denominators.
Why this step matters
First requests often include model loading, graph compilation, allocator growth, page faults, and cache creation. Excluding an undocumented first request is cherry-picking; a fixed warmup policy preserves transparency while allowing steady-state service comparisons.
What to understand
Warm up every candidate with identical workload order policy and concurrency. A single short prompt may not initialize long-context or structured-output paths.
Retain warmup measurements so reviewers can understand cold behavior. If cold start matters operationally, define it as a separate workload rather than mixing it into steady state.
Allow the documented cooldown between candidates and confirm temperature plus clocks return to the approved range.
System changes
- Sends synthetic warmup prompts and initializes model-server caches; results are labelled and excluded from primary statistics.
Warmup complete: 3 repetitions × 5 workloads × concurrency levels 1,4,8; 45 warmup workload rounds labelled and excluded.
Checkpoint: Warmup policy is applied consistently
Continue whenExactly three warmup repetitions exist for every workload and concurrency, with no warmup row included in measured summaries.
Stop whenA candidate receives different warmup work or host state has not stabilized.
command
Run deterministic-order measured repetitions
Export a throwaway local API token and execute the suite. The harness uses deterministic shuffled workload order, runs declared concurrency groups, preserves every outcome, and writes latency metrics plus hashes. Raw content stays in memory only until the deterministic scorer runs.
Why this step matters
Repeated measurements reveal variability that a best-of-one demo hides. Deterministically changing order reduces simple position bias while keeping runs reproducible. Running the scorer against every measured outcome prevents fast failures from disappearing from latency or quality summaries.
What to understand
The reference uses 15 measured repetitions per workload and concurrency. Increase repetitions based on observed variance and confidence needs, not to chase a preferred median.
Track scheduled, successful, timed-out, rejected, invalid, and scorer-failed requests. Successful-survivor percentiles alone are not acceptable.
Do not compare candidates run under different host state, workload revision, scorer hash, or generation parameters. A changed configuration is a new candidate.
System changes
- Sends bounded synthetic requests to one loopback inference deployment and writes restricted metric and score reports.
Syntax explained
BENCHMARK_API_TOKEN- Provides a local credential without storing it in the manifest or reports.
measuredRuns- Defines repetitions before results exist and keeps all failures in scope.
deterministicOrder- Changes workload order reproducibly to reduce simple heating and position bias.
Values stay on this page and are never sent or saved.
export BENCHMARK_API_TOKEN='{{benchmarkToken}}'
node src/benchmark.mjs config/manifest.json config/workloads.json reports
node src/score.mjs reports/results.ndjson config/workloads.json reports/quality.jsonWrote reports/results.ndjson and latency-summary.json; raw content is present only in-memory for immediate scoring. Wrote deterministic quality scores to reports/quality.json.
Checkpoint: Every scheduled measurement has an outcome
wc -l reports/results.ndjson && node -e "const r=require('./reports/quality.json'); console.log(r.rawContentStored,Object.keys(r.summary).length)"Continue whenThe line count matches the schedule; rawContentStored is false and every workload/concurrency bucket appears.
Stop whenThe process exits early, a request outcome is absent, raw content persists, or exact server token usage is silently estimated.
Security notes
- Use only synthetic fixtures and unset the token immediately after the run.
- Stop if exception logs or reports contain raw content; retention promises must be tested, not assumed.
verification
Analyze distributions, errors, and quality by workload
Review p50 and p95 TTFT, E2E, exact-token throughput, error rate, quality pass rate, and memory phases for each workload and concurrency. Plot ordered runs to expose drift, then use confidence intervals or bootstrap estimates when making close decisions.
Why this step matters
A median alone conceals tail latency, errors, multimodal distributions, and quality regressions. Workload-level distributions show whether a deployment meets the actual service gate. Sample figures demonstrate report format only and must be replaced with captured results from the pinned environment.
What to understand
Keep units explicit and do not normalize unrelated metrics into a synthetic score. A 10 percent latency improvement cannot cancel a structured-output correctness failure.
Report null throughput as unavailable. Never treat missing usage as zero or compute it with a different tokenizer.
Compare memory headroom at the target concurrency, not only loaded idle. A deployment that fits at rest can fail under KV-cache growth.
Investigate outliers before exclusion. Exclude only under a predeclared, auditable equipment-failure rule and publish both raw and filtered analyses.
System changes
- Reads benchmark reports and prints a combined review; it does not rerun inference or alter evidence.
Syntax explained
p50- Median observed value; useful for typical behavior but insufficient for service reliability.
p95- Tail percentile that exposes slower user experiences, subject to sample-size uncertainty.
quality denominator- All scheduled measured outcomes, including transport, validation, and scoring failures.
node -e "const l=require('./reports/latency-summary.json'); const q=require('./reports/quality.json'); console.log(JSON.stringify({latency:l.summary,quality:q.summary},null,2))"bounded-summary@c4: runs=15, TTFT p50=412 ms, p95=571 ms, E2E p95=4288 ms, tokens/s p50=54.8, quality=15/15; illustrative captured fixture, not a provider claim.
Checkpoint: Analysis preserves failures and unavailable values
grep -R 'null\|unavailableThroughput' reports/latency-summary.jsonContinue whenUnavailable exact-token throughput remains explicit where server usage was absent.
Stop whenThe analysis drops errors, estimates tokens, hides warmup policy, or presents illustrative figures as universal measurements.
decision
Measure effective context with deterministic sentinel buckets
Generate fixtures at fixed tokenizer-aware input buckets, place unique sentinels at documented early, middle, and late positions, and require exact retrieval. Record rejection, truncation, TTFT, E2E, quality, and memory separately for each bucket.
Why this step matters
An advertised context maximum describes an accepted configuration, not guaranteed retrieval quality, latency, or memory behavior. Tokenizer-aware buckets and deterministic sentinels reveal the effective operating range for the specific artifact, template, engine, and hardware.
What to understand
Count tokens with the exact pinned server tokenizer before the run and store fixture hash plus intended token count. Do not compare character or word counts.
Prevent the server or client from silently truncating. A truncated request is a failed context bucket, not a shorter successful test.
Use multiple sentinel positions because beginning and end retrieval can differ from middle retrieval. Keep synthetic filler stable and non-sensitive.
Choose the largest context bucket meeting the predeclared pass, latency, and memory gates, leaving operational headroom.
System changes
- Creates and sends larger synthetic fixtures that may materially increase RAM, VRAM, latency, and energy use.
8k bucket 15/15 pass; 16k bucket 15/15 pass; 24k bucket 13/15 pass; 32k bucket rejected. Illustrative fixture demonstrates the decision boundary, not a measured product limit.
Checkpoint: Effective context boundary is evidence-based
Continue whenEvery bucket records exact tokenizer count, prompt hash, pass rate, latency, memory, rejection, and truncation status.
Stop whenThe tokenizer revision differs, the request can truncate silently, or memory approaches the unsafe operating limit.
decision
Apply the workload decision matrix
Save the matrix and populate each candidate with measured cells linked to manifest, workload, metric, and scorer hashes. Reject candidates that miss a mandatory correctness, error, memory, privacy, or latency gate before comparing optional advantages.
Why this step matters
Local LLM deployments trade off latency, throughput, memory, context, quality, operational complexity, hardware support, and licensing. The correct choice depends on the workload. A workload-specific matrix preserves these constraints and makes an explicit no-suitable-candidate conclusion possible.
What to understand
Use measured facts only in result cells. Place vendor positioning, community anecdotes, and operator preference in clearly attributed contextual notes.
If candidates use different artifacts, tokenizers, templates, or quantization, describe the comparison as deployments, not as an engine-only ranking.
Repeat any close decision on another day or host reservation and report variability. Small differences inside measurement noise are ties, not wins.
System changes
- Creates a decision record; it does not route traffic or change the inference deployment.
Syntax explained
gates- Mandatory criteria that must pass before a candidate can be preferred.
preferenceRule- A workload-specific tie-breaker applied only among candidates that passed every gate.
reports/decision-matrix.json{
"workloads": [
{
"name": "interactive chat",
"gates": ["quality pass rate", "TTFT p95", "end-to-end p95", "loaded-idle memory"],
"preferenceRule": "Choose the lowest verified TTFT p95 among deployments that meet the quality and memory gates."
},
{
"name": "batch summarization",
"gates": ["quality pass rate", "aggregate exact output tokens per second", "measured peak memory", "error rate"],
"preferenceRule": "Choose the highest verified throughput at the required concurrency without exceeding memory or error budgets."
},
{
"name": "structured automation",
"gates": ["strict JSON pass rate", "tool name and argument pass rate", "p95 latency", "zero unvalidated actions"],
"preferenceRule": "Reject any deployment below the correctness gate even when it is faster."
},
{
"name": "long-context retrieval",
"gates": ["sentinel retrieval pass rate by context bucket", "TTFT p95", "peak memory", "no truncation"],
"preferenceRule": "Use the largest context bucket that passes the sentinel gate; advertised maximum context is not a measured result."
}
],
"evidencePolicy": [
"Every cell links to manifest, workload, raw-metric and scorer hashes.",
"Warmups are excluded and failures remain in the denominator.",
"Throughput is unavailable when exact server usage tokens are absent.",
"Product positioning and cross-artifact claims are excluded."
]
}Decision matrix populated for interactive, batch, structured automation, and long-context workloads; no aggregate winner calculated.
Checkpoint: Decision is traceable and scoped
Continue whenThe selected candidate passes every mandatory gate for the named workload and links to immutable evidence; other workloads may select differently.
Stop whenA universal score, product slogan, or incomparable token estimate determines the outcome.
instruction
Canary the selected deployment and schedule revalidation
Deploy behind a reversible feature flag, begin with synthetic or approved low-risk traffic, and compare live service-level indicators with benchmark expectations. Preserve the previous deployment, alert on quality validation failures, and rerun after any material stack or workload change.
Why this step matters
A laboratory benchmark controls variables that production cannot. A canary verifies queueing, scheduling, observability, privacy, cancellation, and application validation in the real service path while limiting impact. Revalidation prevents a model or runtime upgrade from inheriting stale results.
What to understand
Use application-level validation for structured and tool outputs on every request. Benchmark success does not eliminate runtime validation.
Monitor TTFT, E2E, error type, queue time, validation failures, memory headroom, restarts, and accelerator health without retaining raw sensitive content.
Rerun when model, tokenizer, template, quantization, engine, driver, kernel, hardware, context, batching, cache, prompt format, scorer, or workload mix changes.
Schedule a 90-day review even without declared changes because dependencies, operational constraints, and task distributions drift.
System changes
- Routes only the approved canary share to the selected deployment and creates monitoring plus review obligations.
Canary 5% approved traffic; quality gate 100%, error rate 0.0%, TTFT p95 643 ms; rollback endpoint retained; next review 2026-10-27.
Checkpoint: Production canary remains reversible
Continue whenA kill switch, last known-good deployment, validation alerts, evidence revision, owner, and review due date are active.
Stop whenQuality gates fail, error or latency exceeds budget, memory headroom is unsafe, or rollback cannot be completed within the approved window.
Finish line
Verification checklist
npm run check && bash -n src/collect-memory.shNode and shell syntax checks pass without contacting a model server.node -e "const fs=require('fs'); const r=fs.readFileSync('reports/results.ndjson','utf8').trim().split('\n').map(JSON.parse); console.log(r.length,new Set(r.map(x=>x.workloadId)).size,new Set(r.map(x=>x.concurrency)).size)"The row count matches warmup plus measured policy and covers all five workloads at all three concurrency levels.grep -R -E 'PORTABLE-LLM-OK|synthetic-api|SENTINEL-7H3K-29QP' reportsNo output; reports retain hashes, metrics, and scores instead of fixture or completion text.sha256sum config/manifest.json config/workloads.json src/benchmark.mjs src/score.mjs reports/*.jsonEvery decision input and report has a recorded SHA-256 digest.Recovery guidance
Common problems and safe checks
TTFT is implausibly close to zero while users still perceive a long delay.
Likely causeThe collector counted the first SSE transport frame, role delta, keepalive, or reasoning-only event instead of the first non-empty user-visible text or tool delta.
Inspect the event-type counters in a temporary synthetic run.Verify firstContentAt is assigned only for non-empty content or a real tool-call fragment.Compare the measured timestamp with a local packet trace that contains no sensitive payload.
ResolutionCorrect the semantic TTFT trigger, increment the benchmark revision, discard affected results, and rerun warmups plus all measured repetitions.
Throughput is null for every run even though completions are visible.
Likely causeThe server did not include exact completion_tokens usage in the stream, or the compatibility layer removed the final usage event.
Confirm stream_options.include_usage reached the provider.Inspect only the final event shape in a synthetic run.Check the pinned provider documentation for streamed usage support.
ResolutionLeave throughput unavailable. Do not estimate with a different tokenizer; either enable exact server usage, run a provider-native audited counter, or restrict comparison to latency and quality metrics.
Latency improves or degrades monotonically throughout the run.
Likely causeThermal state, clock policy, caching, model loading, memory pressure, or a background workload changed after measurement began.
Plot ordered E2E and TTFT values rather than only percentiles.Review GPU temperature, clocks, power, CPU load, and memory collector timestamps.Compare early and late repetitions for every workload and concurrency.
ResolutionTreat the run as non-stationary, correct the environmental cause, capture a fresh baseline, and restart from warmup. Do not remove inconvenient late samples.
A process memory chart suddenly drops to zero and later resumes.
Likely causeThe collector followed a reused PID, the server spawned workers outside the selected process, or permissions blocked /proc or cgroup access.
Compare /proc/PID/stat start time with the captured identity.Inspect the service cgroup and explicit worker PID list.Check collector stderr and file permissions.
ResolutionDiscard the memory result. Collect the exact service cgroup or documented worker set, verify identity before every sample, and rerun the entire measurement window.
GPU memory is lower than expected or attributed to the wrong deployment.
Likely causeThe inference engine uses child processes, multiple GPUs, CUDA context sharing, or a PID namespace that differs from the host view.
Capture nvidia-smi compute-app PID mappings before loading and during inference.Map container PIDs to host PIDs and document the mapping.Sum only reviewed worker processes and GPUs assigned to the deployment.
ResolutionDefine a deployment process set or exclusive GPU allocation, update the collector manifest, and rerun idle, loaded, warmup, and measured phases.
Structured-output quality is 100 percent but application validation fails.
Likely causeThe scorer checked only JSON parsing, ignored additional keys, or did not enforce constants and types independently from the server.
Review the deterministic scorer against the workload schema.Inject a parseable but invalid object into an offline scorer test.Compare canonical scorer output with application validation rules.
ResolutionFix the independent scorer, add negative fixtures, version it, and invalidate the affected results. Never use provider acceptance as the quality oracle.
Tool quality passes even when the model chose the wrong arguments.
Likely causeThe scorer checked only the function name, compared raw JSON strings, or ignored duplicate and additional arguments.
Parse arguments with strict JSON and canonicalize object key order.Require exact expected keys and values.Inject wrong region, missing service, extra key, and malformed JSON fixtures.
ResolutionRequire both the exact tool name and canonical validated arguments. Keep execution disabled; a benchmark pass is not authorization to call the tool.
Long-context retrieval passes at one run and fails at the same nominal length later.
Likely causeThe prompt was truncated, tokenization changed, the sentinel position moved, context settings differ, or attention-memory pressure caused an error.
Hash the exact prompt fixture and tokenizer revision.Record server finish reason and context configuration.Test fixed context buckets with a sentinel at deterministic positions.
ResolutionSeparate each context bucket, pin the tokenizer and template, reject truncated requests, and report pass rate by bucket rather than one advertised maximum.
Concurrent results show fewer completed runs than expected.
Likely causeRequests timed out, the server rejected overload, the harness aborted a Promise group, or an error was omitted from the denominator.
Count scheduled, completed, timed-out, and failed requests per workload and concurrency.Inspect process exit status and bounded error records.Verify one failed request cannot suppress sibling result serialization.
ResolutionRecord every scheduled request outcome and keep errors in the denominator. Rerun only after fixing the harness if evidence was lost; never report latency from successful survivors alone.
Two deployments report different output tokens per second for apparently identical text.
Likely causeThey used different tokenizers or one result used an estimated client token count while the other used exact server usage.
Compare tokenizer immutable revisions.Confirm both metrics came from completion_tokens in the server response.Compare output hashes only as a diagnostic, not as a token-count substitute.
ResolutionDo not compare the throughput numbers unless token accounting is equivalent and exact. Report unavailable or keep results in separate tokenizer cohorts.
Repeated runs are byte-identical but the task answer is wrong.
Likely causeDeterminism was mistaken for quality, or the fixture's scorer encoded an incomplete requirement.
Review the workload purpose and scorer independently.Add adversarial negative outputs that should fail.Have a domain owner approve deterministic criteria before rerunning.
ResolutionCorrect the workload and scorer, issue a new revision, and rerun all candidates. Stability is useful evidence but never substitutes for task correctness.
The result looks dramatically better after changing an engine flag.
Likely causeThe flag changed context, batching, caching, quantization, precision, speculative decoding, or workload semantics rather than producing a directly comparable optimization.
Diff the complete deployment manifests.Check model, tokenizer, template, dtype, max context, batching, cache, and generation parameters.Verify the quality and error gates still pass.
ResolutionTreat the changed configuration as a new candidate, retain both manifests, and compare only after all workload and quality gates rerun.
Benchmark reports contain prompt or completion text.
Likely causeDebug logging was enabled, the immediate scoring payload was serialized, or an exception included response content.
Stop the run and restrict access to the output directory.Search reports for known synthetic markers and secret patterns.Review logger and error serialization paths.
ResolutionDelete or quarantine the affected reports according to policy, fix redaction so only hashes and scores persist, rotate any exposed credential, and rerun with synthetic data.
A provider wins one aggregate score but loses every relevant workload gate.
Likely causeIncompatible units were normalized into a synthetic ranking or optional workloads outweighed mandatory correctness and latency limits.
Inspect the decision matrix without aggregate scoring.Confirm workload weights were approved before results existed.Check mandatory gates and confidence intervals per concurrency.
ResolutionRemove the universal score. Make a workload-specific decision only among candidates that pass mandatory quality, error, memory, and security gates.
Reference
Frequently asked questions
Why is first HTTP byte not TTFT?
Headers, an SSE comment, a role delta, or an empty event are not visible model output. This guide measures the first non-empty text or actual tool-call fragment because that is the first meaningful application signal.
Can throughput be estimated with a client tokenizer?
Not for a cross-deployment claim unless it is proven identical to the server tokenizer and the counting path is audited. The safer default is unavailable when exact server completion_tokens are absent.
Why exclude warmups but retain them?
Steady-state service and initialization are different questions. A predeclared warmup policy supports steady-state comparison, while retained warmup evidence reveals cold behavior and prevents hidden sample deletion.
Does temperature zero guarantee identical answers?
No. Seed handling, kernels, parallel reductions, templates, engines, and hardware can remain nondeterministic. Use repeated runs and structural scorers instead of assuming byte equality.
Should RAM and VRAM be added together?
No. Process RSS, cgroup memory, host memory, and GPU memory overlap and represent different resources. Report each separately by phase.
What is a real benchmark output in this guide?
The harness output and sample rows show a captured fixture format and are explicitly labelled illustrative. Production claims must come from a saved manifest, metric report, scorer hash, and reviewed host run.
Can different quantizations be compared?
Yes as complete deployment candidates for a workload, provided differences are explicit and quality gates are rerun. The result must not be described as an engine-only comparison.
How many repetitions are enough?
Fifteen is a practical starting fixture, not a universal statistical guarantee. Inspect variance and increase predeclared repetitions when confidence intervals are too wide for the decision.
Why not create one overall leaderboard score?
It hides hard correctness failures and requires arbitrary normalization and weights. Workload-specific gates preserve the actual operational decision.
When is a benchmark stale?
Immediately after a material change to model, tokenizer, template, quantization, engine, driver, kernel, hardware, runtime flags, context, batching, cache, workload, scorer, or service path, and at the scheduled review date.
Recovery
Rollback
Benchmark collection is mostly read-only, but loaded models consume host resources and a canary changes traffic. Disable the canary, restore the last known-good endpoint, stop only the benchmark deployment and collector, revoke the temporary token, and remove any report that violated the no-raw-content policy.
- Set the application feature flag to the previous deployment and verify health, quality validation, and queue drain.
- Stop the memory collector, then stop only the benchmark server instance after confirming its PID, cgroup, and port ownership.
- Unset and revoke BENCHMARK_API_TOKEN; preserve non-sensitive manifests, metric reports, scorer hashes, and the failure note.
- Securely delete or quarantine unexpected raw-content logs under the organization's incident and retention policy, then fix and revalidate before another run.
- Restore any approved host power or scheduling policy changed for the lab window and confirm normal monitoring resumes.
Evidence