Public task and development documentation. Official evaluation boundary.
Paired identical evaluation
This document specifies how EMBERBench runs several models on exactly the same episodes and how their results are matched.
Out of scope here: model-provider adapters, win rates, confidence intervals and other aggregate statistics (see statistics.md), procedural scenario generation, integrity and provenance (see integrity.md), and the EMBER Score, leaderboard order and frozen environment packs (see scoring.md and environment-pack.md).
1. Common random numbers
An episode is fully defined by a scenario and a seed. All randomness comes from simulator-owned streams derived from the seed (see simulation-contract.md). In V1 the fire does not depend on the actions, so two models that play the same scenario with the same seed face the same fire, cell for cell and step for step.
Paired evaluation uses this. Every model plays the same ordered list of (scenario, seed) episodes. A difference between two models on one episode then comes from their decisions only, not from different fire. This is the common-random-numbers method. It removes the between-seed noise from each paired difference, so fewer episodes are necessary to compare two models.
The test test_two_policies_see_identical_stochastic_fire checks this on the
stochastic scenario v1-pub-10-route-invalidation. A greedy policy and an
always-stay policy see identical burning sets at every step that both runs
reach, and two different seeds give different fire.
2. The evaluation manifest
An evaluation manifest is an immutable, versioned episode schedule.
{
"manifest_version": "1.0",
"suite_id": "emberbench-v1-public",
"suite_version": "1.0.0",
"suite_hash": "<sha256 of the canonical suite manifest>",
"simulator_version": "0.2.0",
"observation_version": "1.0",
"metrics_version": "1.0",
"episodes": [
{"index": 0, "scenario_id": "v1-pub-01-single-exit-sparse", "scenario_hash": "<sha256>", "seed": 3},
{"index": 1, "scenario_id": "v1-pub-01-single-exit-sparse", "scenario_hash": "<sha256>", "seed": 1}
]
}
Rules (all enforced on construction and on parsing):
manifest_versionmust be a supported version (1.0).suite_id,suite_versionandsuite_hashmust be well formed.episodesis non-empty.indexequals the position in the list (0, 1, 2, ...).- Each
seedis an integer in[0, 2**64 - 1]. Booleans and floats are rejected. - A seed occurs at most once per scenario. The same seed may be used for different scenarios.
- One scenario ID is pinned to exactly one content hash.
- Unknown and missing keys are rejected.
The manifest hash is the SHA-256 of the canonical JSON of the whole manifest. A change to any field, any seed, any scenario hash or to the episode order gives a different hash.
Building a manifest
The manifest builder schedules every suite scenario under
every seed: the outer order is the suite manifest order, the inner order is the
order of seeds as given. The function does not sort, remove or generate seeds.
An empty list, a duplicate or an out-of-range seed is an error. A set, a
str or a generator is a type error, because it has no reliable order.
The manifest check requires that the manifest targets exactly this suite (ID, version, hash) and the running code's simulator, observation and metrics versions, and that every pinned scenario hash matches both the suite entry and the scenario file. The evaluation runner and the result loader always call it.
The manifest writer and the manifest loader store a manifest as JSON. The loaded manifest is checked with the same rules.
3. Public development manifests and scored manifests
The seeds decide the fire. If a model developer knows the scored seeds, they can tune a model to that exact fire. Therefore:
- Development manifests use small, fixed, public seeds (for example
[0, 1, 2]). They are fine in tests, examples and documentation. The test suite pins one: the public suite under seeds(0, 1, 2)has manifest hash7b9aae45e5e2828f881b2f14d8e5585627e5f8bdbb137a1ed2331bf10ce8c876. - Scored manifests use seeds chosen by the evaluator. They are not published here.
This repository never generates scored seeds. The seed in a scenario file is
a development default only. The evaluation runner always overrides it with the
manifest seed.
A published manifest hash lets anyone check later that a published schedule is the one that was used, without revealing the seeds before the evaluation.
4. Seed privacy and episode isolation
The evaluator knows the manifest and the seeds. The decision policy does not.
The evaluation runner does this for each episode:
- New policy from the factory. The factory receives no arguments: no scenario, seed, episode index or other metadata.
- The episode runner with the episode seed. The policy is called with exactly one positional argument, an observation, once per step.
The policy never receives the evaluation manifest, an episode entry or index, the seed, the scenario, the simulator, the run record or any evaluator metadata. The observation contains no seed and no RNG state (see v1-fairness-spec.md).
A new policy is built for every episode, so state that a policy keeps cannot carry over into the next episode. This also makes resume correct: an episode gives the same result whether it runs in a fresh process or after other episodes. The factory must return a new policy each time. A plain stateless function is also acceptable.
The tests prove this:
test_policy_receives_only_observationsuses a policy that records every argument it receives and a factory that records its own arguments. The factory is called with()once per episode. Each policy call has exactly one argument of type observation and no keyword arguments. Every object inside the observation is one of the known observation types. The 64-bit seed (in decimal and hex), the manifest hash, the suite hash and the scenario hashes do not occur in its JSON form.test_fresh_policy_per_episodechecks that there is one distinct policy instance per episode and that each instance sees one episode, from step 0.
Limit: this is an interface contract for in-process Python policies, not a sandbox. Python code can inspect its caller's stack. A real model adapter must send only the serialized observation to the model.
For per-episode agents, action validation, optional execution accounting and the canonical serialized observation, see model-interface.md.
5. The per-model result artifact
The evaluation runner writes one JSON artifact per model run. It is canonical JSON (sorted keys, no extra whitespace) followed by a newline.
{
"result_version": "1.0",
"model_id": "model-a",
"evaluation_manifest_hash": "<sha256>",
"suite_id": "emberbench-v1-public",
"suite_version": "1.0.0",
"suite_hash": "<sha256>",
"simulator_version": "0.2.0",
"observation_version": "1.0",
"metrics_version": "1.0",
"manifest": { "...": "the complete evaluation manifest" },
"episodes": [
{
"index": 0,
"scenario_id": "v1-pub-01-single-exit-sparse",
"scenario_hash": "<sha256>",
"seed": 3,
"record_digest": "<sha256 of the canonical run record>",
"record": { "...": "the complete run record" },
"metrics": { "...": "episode metrics" }
}
]
}
- The header fields repeat the embedded manifest so a reader can see them at once. They must equal the manifest.
episodesholds completed episodes only, in manifest order. Each one names its exact manifest entry and contains the full run record, so anyone can replay and audit it, plus the raw metrics computed from it.model_idis a label chosen by the evaluator (1 to 256 printable characters, no leading or trailing whitespace). It is not proof of which model produced the actions, and it is not a unique key.
The result loader accepts an artifact only if all of these hold:
- the JSON has exactly the expected keys at every level;
- the embedded manifest is valid, its hash equals
evaluation_manifest_hash, and every header field equals the manifest; - the manifest passes the manifest check against
suiteand the running code; - every episode index is in the manifest, occurs once, and episodes are in manifest order;
- every episode's
scenario_id,scenario_hashandseedequal its manifest entry, and the record's header agrees with them; - every record is in canonical form and matches
record_digest; - every record is authenticated: Episode metrics checks its structure and replays its action log in a fresh simulator, which must reproduce it byte for byte;
- every stored
metricsobject equals the metrics recomputed from the authenticated record; - the artifact as a whole re-serializes to exactly the loaded JSON.
Any failure raises an evaluation error. Nothing is repaired, skipped or ignored.
6. Resume guarantees
The evaluation runner saves the artifact after every completed episode. Each save
writes a temporary file in the same directory, flushes and fsyncs it, and
renames it over the artifact with atomic file replace. A crash therefore leaves either
the previous artifact or the new one, never a partial file. After a hard crash
(for example a power loss) a stray .<name>.*.tmp file can remain next to the
artifact. It is never read and can be deleted. If the output path
does not exist, an artifact with zero episodes is written before the first
episode runs.
If output_path exists:
- With
resume=Falseit is an error. An existing artifact is never overwritten. - With
resume=True(the default) the artifact is loaded and authenticated completely, as in section 5. Then it must have the same evaluation manifest hash and the same model ID as the call. A changed schedule, seed order, scenario, suite or version, or a different model ID, is rejected. - Only the episodes that are not in the artifact run, in manifest order. A completed episode is never run again.
Because each episode uses a fresh policy and its own seed, an interrupted and
resumed run gives the same artifact, byte for byte, as an uninterrupted run
with a deterministic policy. test_interrupted_run_resumes_without_rerunning
checks this.
A policy exception or an invalid action stops the run with that exception. The artifact keeps the episodes completed before it.
Cost note: the whole artifact is rewritten after each episode, so total bytes written grow with the square of the number of episodes. This is acceptable for V1 schedules. A sharded layout can replace it later without changing the pairing rules.
7. Exact pairing
The result pairing compares two loaded artifacts. It first requires that both use the same evaluation manifest (equal hash and equal content). Otherwise it raises an evaluation error: results from different schedules are never compared.
Each scheduled episode has the key
(evaluation manifest hash, episode index, scenario ID, scenario hash, seed)
Definition. An episode is a comparable pair exactly when both artifacts contain a result whose key equals that scheduled key. Nothing else (model ID, file name, run order or time) is part of the key.
The function returns a pairing report:
| Field | Meaning |
|---|---|
| Manifest hash | the shared evaluation manifest hash |
model_a, model_b |
the two model ID labels |
total_scheduled |
number of episodes in the manifest |
paired |
keys present in both artifacts, in manifest order |
paired_count |
len(paired) |
missing_from_a |
scheduled keys that a does not have |
missing_from_b |
scheduled keys that b does not have |
An episode that neither artifact has is in both missing lists. The report holds
no scores, win rates or statistics. statistics.md defines the paired statistics; they use only
paired episodes.
Reviewed public benchmark contract. Apache License 2.0. Download Markdown