๐งช Benchmark Suites & Evaluation Methodology¶
This document details the design, scenarios, scoring formulas, and validation methodologies employed by the 5 specialized benchmark suites in BenchRig.
1. โก Speed & Latency Suite (speed)¶
The speed suite evaluates inference latency, maximum decoder velocity, and prompt ingestion throughput under streaming conditions.
Scenarios¶
- Raw Decode Speed (Short Prompt):
- Measures maximum decoding velocity when the context window is near-empty.
- Evaluates pure token sampling speed unconstrained by large KV-cache buffers.
- Throughput Generation (512 tokens):
- Tasks the model with generating an extended completion to measure sustained throughput over time.
- Reveals thermal throttling, memory bandwidth saturation, or engine queue degradation.
Metrics Measured¶
- Time-to-First-Token (TTFT): High-resolution timestamp delta from HTTP socket dispatch to the arrival of the first completion byte.
- Prefill Speed (Prompt Tokens/s): Hardware throughput while ingesting and calculating initial attention keys/values for the prompt.
- Decode Speed (Completion Tokens/s): Sequential auto-regressive generation speed of output tokens.
A
~prefix in the report (~3000.0 t/s) marks a value produced by the 0.001 s measurement floor on a very short answer, not a real measurement; the CSV columneval_tok_sec_flooredcarries the same flag.
2. ๐ป Coding & Algorithmic Sandbox Suite (coding)¶
The coding suite measures whether an LLM produces syntactically valid and deterministically functional Python code without human intervention. Today the coding suite only exercises Python; JS/TS and other multi-language scenario packs are not bundled.
Isolated Subprocess Sandbox (benchrig/core/sandbox.py)¶
Rather than relying on lexical matching (e.g. BLEU/ROUGE), BenchRig executes the model's generated code inside an isolated Python subprocess:
1. Strips conversational prose, backticks (```python), and trailing explanations.
2. Appends an automated test harness with strict assert statements covering edge cases.
3. Spawns an isolated subprocess.Popen in its own process group with a strict timeout.
4. Validates assertion outcomes via out-of-band JSON token verification, ignoring candidate stdout/stderr for verdict decisions.
Subprocess Isolation & Residual Risk
BenchRig provides process-group isolation and strict timeouts, not OS-level sandboxing (such as Docker, seccomp, or WebAssembly). While out-of-band JSON tokens prevent candidate output from forging verdicts or test counts, the generated code runs under the current Python interpreter and could attempt system or network calls (subprocess, socket, ctypes).
Reasoning traces and cut-off answers. A model that thinks first (<think>...</think>, e.g. DeepSeek-R1) can spend its whole
token budget before writing any code. Code inside the <think> trace is ignored, and a response that stopped at the token
limit without a complete code block is reported as Response truncated at num_predict=N before the code was complete (with
truncated: true in the result) instead of a misleading name '...' is not defined. To give such models room, the budget
(num_predict) of the coding, reasoning and polish suites is multiplied for models matching benchmark.thinking_models
(default deepseek-r1, qwq, magistral, qwen3) by benchmark.thinking_token_multiplier (default 12); see
Configuration. Scorecards count cut-off responses as truncated_runs and the reports flag them with โ . Only the suites that score an answer
(coding, reasoning, polish) are checked: the speed and context suites generate a fixed number of tokens on purpose, so stopping
at the limit there is not truncation (finish_reason is still recorded).
Thinking models and the think option. Ollama streams a thinking model's reasoning in a separate thinking field. BenchRig
reads it: ttft_sec is the time to the first token of any kind (the latency), answer_ttft_sec the time to the first answer
token, think_time_sec the difference, and thinking_chars the amount of thinking. (Earlier versions reported only the first
answer token, so a thinking model showed seconds of "TTFT", or 0.0 s when it never answered.)
Whether a model thinks is set with Ollama's think option, per suite, in benchmark.think (see Configuration):
by default speed and context run with thinking off so throughput and prompt-processing latency are not mixed with thinking
time, and coding, reasoning and polish use the model's own default. Turning thinking off for coding measures the model
without reasoning (much faster), but the larger token budget still applies to these models: in one run deepseek-r1:14b with thinking
off and the base budget (512 tokens) never produced a complete code block (0 of 4 tests), while with its default thinking it passed
4 of 4 on the same scenario (4541 tokens, 14752 characters of thinking, 73 s to the first answer token, but a ttft_sec of 0.11 s).
The option is sent only to models that Ollama lists as able to think; other runtimes ignore it.
How much budget do thinking models need? Measured on deepseek-r1:14b, coding suite, one run each: a multiplier of 6 cut off
1 to 2 of the 4 scenarios, 12 cut off 1, and 20 cut off none; the scenario that ran out at 12 (6144 tokens) finished in 4609
tokens at 20. The need is a long tail, hence the default of 12. Checked with --runs 3 at 12: 12 requests, none cut off, at most 4822
tokens (of a 6144 budget), coding 98.0% [94.1-100.0] and, on the biggest scenario, 3 of 4 tests in one repetition. Check truncated_runs for your models.
Algorithmic Scenarios¶
| Scenario | Algorithmic Focus | Assertions Tested |
|---|---|---|
| Nested Dict Flattening | Deep recursion, key path concatenation, delimiter handling | 4 |
| Merge Overlapping Intervals | O(N log N) sorting, greedy interval consolidation, edge intervals | 5 |
| Valid Balanced Brackets | Stack data structure, pair mapping, empty and malformed string handling | 6 |
| LRU Cache Implementation | Doubly-linked hash map / OrderedDict, O(1) get/put operations |
2 |
Disaggregated Task vs. Assertion Metrics¶
To avoid obscuring partial successes or multi-assertion scenarios where a model fails only edge cases, BenchRig disaggregates coding metrics into two distinct levels:
- Task-Level Success (
coding_tasks_passed / coding_task_count,coding_task_pass_rate): A coding scenario is marked passed only when 100% of its unit test assertions pass. The task pass rate reflects the percentage of programming problems fully solved without errors. - Assertion-Level Success (
coding_assertions_passed / coding_assertion_count,coding_pass_rate): Counts individual unit test assertions passed across all scenarios.coding_pass_ratepreserves backward compatibility and contributes to the composite score. - Per-Task Run Evidence:
Each result record retains
task_id,passed(task boolean),passed_tests,total_tests,pass_ratio, and anysandbox_errordiagnostics. - Reporting & Exports:
The Markdown report displays task pass counts (
Tasks Passed: X/Y (Z%)) alongside assertion rates (Assertions: A/B (C%)), and the CSV export includes columns for both task and assertion counts and rates (coding_task_pass_rate,coding_tasks_passed,coding_task_count,coding_assertions_passed,coding_assertion_count).
3. ๐ง Reasoning & Thought Chain Suite (reasoning)¶
Designed to test models that generate reasoning chains (such as DeepSeek-R1 or Qwen 2.5 with reasoning prompts).
Methodology¶
<think>Parsing: The parser inbenchrig/core/reasoning_parser.pyinspects whether the model separates internal reasoning (<think>...</think>) from the final response.- Token Budget Telemetry: Tracks the proportion of tokens spent on internal exploration vs the final delivered answer.
- Deterministic Verification: Extracts the final answer and checks exact equality against ground-truth mathematical and logical solutions.
What counts as an answer
- The answer is what follows
</think>. A response cut off inside<think>has no answer and is scored as wrong (truncated_thinking: true); text that only appears in the reasoning trace never counts, even if it contains the expected words. regexchecks look only at the end of the answer (the last 300 characters), after markdown, bullets, punctuation and line breaks are collapsed to spaces. A conclusion written asBox 1: Oranges, Box 2: ...on one line, on separate lines or as bullets reads the same, while words from the right answer that merely appear earlier in the reasoning do not count. The bundled patterns use bounded gaps (a few filler words) instead of.*, so they cannot be satisfied by scattered mentions.- LaTeX in answers is simplified before matching (
\boxed{\dfrac{1}{6}}reads as1/6), because reasoning models often answer that way. - Every result records
answer_excerpt(the last 400 characters of the answer) so a score can be audited fromresults/runs/*.jsonwithout re-running the model. final_answerscenarios (nine of the twelve) take the lastFinal answer: ...line and require it to match an accepted pattern in full; no marker means no answer.exact_or_containsmatches numbers as whole numbers, so11/60no longer counts as1/6and1114.60no longer counts as114.6(both used to pass).- The expected answers of the new scenarios are computed by independent code in
tests/test_scenarios.py, which also rejects near misses, so a wrong expectation cannot rank models wrongly.
Reasoning ground-truth comparison¶
The evaluator field on a reasoning scenario (added in spec.md ยง12.3) is an opt-in layer that runs before the
scenario's check_type. Without it, the scenario behaves exactly as it always has.
evaluator |
Effect |
|---|---|
| (absent) | Today's behavior. Whitespace and word forms are not normalized. |
"numeric" |
Trim whitespace on both sides; if both parse as a Fraction, compare exactly. "42", "42.0", and " 42 " all match "42"; word answers like "forty-two" do not silently match a numeric "42". |
"numeric_text" |
Same as "numeric", plus an English text-to-number conversion on both sides: zero, one, ..., nineteen, twenty, thirty, ..., ninety, hundred, thousand, million, and hyphenated forms like "forty-two", "twenty-one". Conversion is case-sensitive: "Forty-Two" does NOT match "forty-two". |
When only one side parses as a number (after normalization), the numeric layer is a no-op and the underlying
check_type decides. This is the invariant that keeps word-puzzle answers (expected_answer is itself a word) from
being silently accepted as numeric.
Unknown values for the field are treated as None, with an evaluator.unknown structured-log event emitted on
stderr so a typo does not silently disable comparison.
Scenarios¶
| Scenario | Check | Expected |
|---|---|---|
| Multi-Step Discount & Tax Calculation | contains number | 114.6 |
| Three Mislabeled Boxes Logic Puzzle | regex on the conclusion | Box 1: Oranges, Box 2: Apples and Oranges, Box 3: Apples |
| Set Theory & Probability | contains number | 1/6 |
| Two Trains Meeting Time | Final answer |
11:20 |
| Knights and Knaves (3 people) | Final answer |
A is a knave, B and C are knights |
| Distinct-Digit Multiples of Five (4-digit numbers) | Final answer |
952 |
| Linear Recurrence (a10 of a1=3, a(n+1)=2a(n)-1) | Final answer |
1025 |
| Chained Percentage Changes (80, +25%, -20%, +10%) | Final answer |
88 |
| Letter Counting (r in "strawberry raspberry blueberry") | Final answer |
8 |
| Bat and Ball ($1.10 total, bat $1.00 more; cents for the ball) | Final answer |
5 |
| Multiples of Three or Five (1 to 100) | Final answer |
47 |
| Letters in One Word (r in "strawberry") | Final answer |
3 |
In one run of phi4-mini, qwen2.5-coder:7b and gemma3:12b the new scenarios split the models (Letter Counting 0 of 3, Distinct-Digit
Multiples 2 of 3) where the old three mostly passed for everyone. Three easier ones (bat and ball, multiples of three or five, letters in one word) were
added so that weaker models also pass some: phi4-mini on Ollama passed them 3 of 3 times each, while the hard ones (Letter Counting,
Distinct-Digit Multiples) stayed at 0 of 3. Scores are still coarse with twelve scenarios; add your own for a finer
ranking (Custom scenarios).
4. ๐ Context Saturation Suite (context)¶
Assesses how inference performance degrades as context length increases.
Methodology¶
- Runs five steps, labelled 512, 1024, 2048, 4096 and 8192 (
context_scaling.json). Each step sets the request'snum_ctxto the label and sends a prompt of aboutfill_ratio(0.75) of that window, built from a repeated filler paragraph. - A fact is hidden in the text (
needle, placed atneedle_depth, by default halfway, at a sentence boundary) and the question asks for it (instruction, answer expected inexpected). A step passes when the answer contains the expected number as a whole number. Each step hides a different code, so it cannot be memorised. The suite therefore measures whether the model uses its context, not only how fast it reads it; the share of steps passed is the scorecard'scontext_retrieval_pct. - On runtimes that reload the model when
num_ctxchanges (Ollama), an untimed request with the samenum_ctxruns first, so the timed request measures prompt processing and not model load. The load time is recorded asload_time_sec. - Samples Time-to-First-Token (TTFT) and memory growth across each step.
- Identifies the threshold where the model's KV-cache exceeds physical GPU VRAM and triggers spilling into system RAM.
Reading the context numbers
- The label is the context window, not the prompt length. Prompt size is estimated from characters (about 4 per token), and
real tokenizers differ, so the real token count is recorded as
prompt_tokens_actual. For Phi-4-mini the five steps sent 289, 565, 1107, 2199 and 4374 tokens (about 55% of each window). Usefill_ratioin the scenario file to change the fill. - TTFT now grows with the prompt. Before the warm-up was added, the Ollama numbers here were dominated by model reload
(about 4 s per step measured directly, about 7 s in the suite), because every step changed
num_ctx. Results from earlier versions of this suite are not comparable. - Where a runtime cannot fit the window, the step is recorded as a failure with the engine's error; that is a finding, not a harness fault.
- The scorecard's
avg_ttft_secstill averages every suite, including the large-context steps, so compare TTFT per suite (results/runs/*.json) when the prompts differ in size.
5. ๐ต๐ฑ Polish NLP Linguistic Suite (polish)¶
Evaluates linguistic precision in morphologically rich and complex languages (Polish).
Scenarios¶
- Grammatical Declension: Testing correct nominal and adjectival case inflections across all 7 Polish cases (przypadki).
- Idiomatic Nuance: Translating and interpreting idioms and culturally specific expressions.
- Text Summarization: Preserving factual precision under strict word-count limits.
Tool-use suite (opt-in)¶
benchrig --suite tool_use --runs 3 measures native function-call reliability,
not execution or retrieval quality. The 18 bundled cases cover tool selection,
integer/enum/array arguments, correct no-call answers, two-step fixture calls,
Polish prompts and retrieval-shaped requests. Function names are inert: BenchRig
never reads the requested file, searches a real index, or runs a model-selected command.
A task passes when every planned turn succeeds. A call requires exactly the expected tool, schema-valid arguments, exact JSON types and no extra arguments. Object key order is ignored; array order and string contents matter. Plain-text call JSON and malformed/extra calls fail. A no-call response must also match its expected answer, so a refusal does not count as correct abstention. The first wrong turn stops the conversation; later planned turns remain failed evidence.
Reports show task success, tool selection accuracy, argument accuracy, abstention accuracy, false-call rate and structured-call rate, with numerators/denominators. Selection/argument/structured-call denominators include all planned call turns; abstention/false-call denominators include all planned no-call turns. Errors and skipped turns are counted alongside those rates. Unsupported cases are excluded from eligible denominators and counted separately. Warm-ups are excluded. A zero denominator is n/a, not a 0% score. Results apply to a model/runtime pair.
Initial transport is non-streaming: request latency is wall time, and TTFT, prefill/decode timing or throughput are unavailable unless reported by the server. Tool metrics do not feed legacy composite, timing or memory aggregates. Tool-only runs expose those legacy fields as unavailable; raw JSON preserves turn evidence, effective sampling settings and reported execution placement.