simulate()
emits rows; so does wai.rows(), from prompts and completions you already
have (a public benchmark, a model’s answers to it). Every measurement call
reads the same five keys, so the rows from either source go into every call
unchanged.
The five keys
A row may carry more (
messages, steps, judge_name, lineage,
privileged) and the calls use what they find. Nothing else is required.
The typed view is whileai.simulations.schema (Task, Rollout,
Judgment, Marker); wai.rows() builds through it, so its rows carry
schema_version and the same shape a run writes.
Build them: wai.rows()
prompts: strings, or message lists ([{"role": "user", "content": ...}]); the row’spromptis the last user turn andmessageskeeps the list.completions: one string per prompt, or one sequence per prompt. The sequence is k completions of the same prompt, which is whatpass_at’s k-way numbers andselect(mode="rl")need.reward: a verifier (wai.verify.MathEqual(),Numeric,ExactMatch,CodeExec), a callable(prompt, completion)or(prompt, completion, reference)returning a number in [0, 1], a judge-contract callable(row) -> verdict, or the numbers themselves, nested likecompletionsor flat. A verifier or callable runs through the same pathdata.grade()uses, so the row says what scored it.references=: the gold per prompt. It lives underprivileged.reference, where a verifier reads it and no training export projects it.task_ids=,markers=: names per prompt, measurements per completion.
Then measure
Everything downstream takes the list as it is.select(mode="rl") keeps whole groups the model passes between 20% and
80% of the time (Lambert 2025, chapter Reasoning; DAPO, arXiv:2503.14476)
and drops the rest: a group all-pass or all-fail has no advantage to learn
from. Its report names every gate that dropped a row, so a benchmark whose
rows all vanish says why.
The worked example, 200 GSM8K test questions through MathEqual, three
eval passes, holdout_size and a compare report, offline, is
recipes/02-measure/public-benchmark.