# Benchmarks Guide ## Configuration `BenchmarkConfig` controls how benchmarks run: ```python from tinker_cookbook.eval.benchmarks import run_benchmark, BenchmarkConfig config = BenchmarkConfig( max_examples=200, # Cap number of examples (None = all) concurrency=64, # Parallel rollouts (single-turn) agent_concurrency=8, # Parallel rollouts (multi-turn) timeout_seconds=300, # Per-example timeout max_tokens=32768, # Max generation tokens temperature=0.6, save_dir="evals/run_01", # Save trajectories and results ) result = await run_benchmark("gsm8k", sampling_client, renderer, config) ``` ### Model-Specific Defaults `BenchmarkConfig.for_model()` sets `max_tokens`, `timeout_seconds`, and `context_window` from a built-in table: ```python config = BenchmarkConfig.for_model( "Qwen/Qwen3.6-35B-A3B", save_dir="evals/qwen3.6", ) # max_tokens=65536, context_window=65536, timeout_seconds=1800 ``` ### Multi-Turn Benchmarks Agent benchmarks (`swe_bench`, `terminal_bench`, `tau2_bench`) need context management: ```python config = BenchmarkConfig( timeout_seconds=1800, max_trajectory_tokens=60000, # Total tokens across all turns max_generation_tokens=8192, # Tokens per generation step agent_concurrency=4, ) ``` ### Sandbox Benchmarks Benchmarks that execute code (`mbpp`, `livecodebench`, `swe_bench`) require a sandbox: ```python from tinker_cookbook.sandbox.modal_sandbox import ModalSandbox config = BenchmarkConfig( sandbox_factory=ModalSandbox.create, ) ``` ### Judge Benchmarks Benchmarks like `arena_hard` require a separate LLM judge. See **[Judge Models](https://tinker-docs.thinkingmachines.ai/cookbook/eval/customization/#judge-models)** for configuration details and examples. ## Storing and Inspecting Results Set `save_dir` to persist trajectories and results: ```text evals/run_01/ ├── summary.json # Combined scores (run_benchmarks only) ├── gsm8k/ │ ├── result.json # BenchmarkResult │ └── trajectories.jsonl # One StoredTrajectory per line └── ifeval/ ├── result.json └── trajectories.jsonl ``` ### Loading Results ```python from tinker_cookbook.eval.benchmarks import ( load_result, load_trajectories, load_summary, print_trajectory, ) result = load_result("evals/run_01", "gsm8k") print(f"Score: {result.score:.1%}, Completed: {result.score_completed:.1%}") # Filter trajectories wrong = load_trajectories("evals/run_01", "gsm8k", incorrect_only=True) errors = load_trajectories("evals/run_01", "gsm8k", errors_only=True) print_trajectory(wrong[0]) # Combined summary across benchmarks summary = load_summary("evals/run_01") ``` ### Resumability Rerunning with the same `save_dir` skips completed examples. Deduplication uses `example_id` (a content hash), so it's robust to dataset shuffling. ### Understanding Scores - **`score`** — `num_correct / num_examples`. Truncated and errored examples count as 0. - **`score_completed`** — `num_correct / num_completed`. Excludes truncated and errored examples from the denominator. For thinking models that often hit `max_tokens`, `score_completed` is the better comparison against published scores. ```python result = await run_benchmark("gsm8k", client, renderer, config) print(f"Raw: {result.score:.1%}") # 81.7% print(f"Completed: {result.score_completed:.1%}") # 95.6% print(f"{result.num_truncated} truncated, {result.num_errors} errors") ``` ## Comparing Checkpoints ```python checkpoints = { "base": "tinker://run-abc/sampler_weights/step0", "step500": "tinker://run-abc/sampler_weights/step500", "final": "tinker://run-abc/sampler_weights/final", } for name, path in checkpoints.items(): client = await sc.create_sampling_client_async(model_path=path) results = await run_benchmarks( ["gsm8k", "ifeval"], client, renderer, BenchmarkConfig(save_dir=f"evals/{name}"), ) for bench, r in results.items(): print(f"{name}/{bench}: {r.score:.1%}") ``` ### EvalStore for Multi-Run Tracking `EvalStore` manages evaluation runs across checkpoints with cloud-compatible storage: ```python from tinker_cookbook.stores.eval_store import EvalStore store = EvalStore("~/experiments/evals") run_id = store.create_run( model_name="Qwen/Qwen3.6-35B-A3B", checkpoint_path="tinker://run-123/weights/step500", checkpoint_name="step500", benchmarks=["gsm8k", "ifeval"], ) config = BenchmarkConfig(save_dir=store.run_dir(run_id)) await run_benchmarks(["gsm8k", "ifeval"], client, renderer, config) metadata = store.finalize_run(run_id) print(metadata.scores) # {"gsm8k": 0.847, "ifeval": 0.923} ``` ## Pass@k Evaluation Set `num_samples > 1` to compute unbiased pass@k estimates (Codex formula): ```python config = BenchmarkConfig(num_samples=4, save_dir="evals/pass_at_k") result = await run_benchmark("gsm8k", sampling_client, renderer, config) print(result.pass_at_k) # {1: 0.45, 2: 0.58, 4: 0.72} ``` ## Customization For system prompts, custom grading, answer parsing, and judge model configuration, see **[Customizing Benchmarks](https://tinker-docs.thinkingmachines.ai/cookbook/eval/customization/index.md)**. ## Adding a New Benchmark Benchmarks reuse the same `Env` protocol as RL training — a single `MessageEnv` can drive both training and evaluation. See **[Shared Env Abstraction](https://tinker-docs.thinkingmachines.ai/cookbook/eval/customization/#shared-env-abstraction)** for the conceptual overview. To add a benchmark, implement a `BenchmarkBuilder` and register it: ```python from tinker_cookbook.eval.benchmarks import BenchmarkBuilder, BenchmarkConfig, register from tinker_cookbook.eval.benchmarks._common import load_benchmark_dataset, make_example_id from tinker_cookbook.rl.message_env import MessageEnv, MessageStepResult, EnvFromMessageEnv class MyMessageEnv(MessageEnv): """Single-turn env for one example.""" def __init__(self, question: str, expected: str, example_id: str): self.question = question self.expected = expected self._example_id = example_id @property def example_id(self) -> str: return self._example_id async def initial_observation(self) -> list[Message]: return [{"role": "user", "content": self.question}] async def step(self, message: Message) -> MessageStepResult: response = get_text_content(message).lower() correct = self.expected.lower() in response return MessageStepResult( reward=1.0 if correct else 0.0, episode_done=True, next_messages=[], metrics={"correct": float(correct)}, logs={"expected": self.expected}, ) class MyBenchmark(BenchmarkBuilder): name = "my_benchmark" recommended_system_prompt = "Answer concisely." def make_envs(self, renderer, config): ds = load_benchmark_dataset("my/dataset", split="test") envs = [] for row in ds: env = EnvFromMessageEnv( renderer=renderer, message_env=MyMessageEnv( question=row["question"], expected=row["answer"], example_id=make_example_id("my_benchmark", row["question"]), ), ) envs.append(env) return envs register(MyBenchmark()) ``` Key points: - Use `MessageEnv` for single-turn benchmarks — it handles prompt building and response parsing - Set `example_id` via `make_example_id()` for stable resumability across runs - `logs` stores diagnostic data per trajectory (expected answer, extracted answer); `metrics` stores numeric values for aggregation - For multi-turn benchmarks, implement `Env` directly and set `multi_turn = True` on the builder ## Learn More - **[Evaluation Overview](https://tinker-docs.thinkingmachines.ai/cookbook/eval/index.md)** — available benchmarks and architecture - **[Customizing Benchmarks](https://tinker-docs.thinkingmachines.ai/cookbook/eval/customization/index.md)** — system prompts, grading, judge models, shared Env design - **[Evaluations Tutorial](https://tinker-docs.thinkingmachines.ai/tutorials/core-concepts/evaluations/index.md)** — interactive walkthrough - **[RL Environments](https://tinker-docs.thinkingmachines.ai/tutorials/cookbook-abstractions/env-and-envgroupbuilder/index.md)** — the Env protocol used by benchmarks - **[API Reference](https://tinker-docs.thinkingmachines.ai/cookbook/api-reference/eval/index.md)** — full type documentation