PolicyEngine / PolicyEngine/policybench
Determinism and reproducibility hardening
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 2
- Forks
- 3
- Avg merge
- 12h 7m
- Merged PRs (30d)
- 13
Description
Problem
The current benchmark mixes random-seeded scenario generation with stochastic LLM generation (temperature=1.0, n_runs=10 per scenario) and unpinned dependencies. That gives us variance estimates per cell, but it makes the benchmark itself non-reproducible: re-running the same suite a month later — same code, same prompts — gives different numbers, and we can't tell whether a difference is the model drifting, the SDK changing, the scenario distribution shifting, or noise.
For a benchmark whose value is tracking model performance over time, the runs should be (mostly) reproducible: same inputs + same model snapshot → same outputs, up to provider-side API jitter we can't control.
Concrete sources of nondeterminism today
- Model aliases, not snapshots.
config.pylists"gpt-4o-mini","gemini-1.5-flash"— these resolve to whatever each provider currently points the alias at. Snapshots likegpt-4o-mini-2024-07-18andgemini-1.5-flash-002are stable. - Unpinned Python deps.
pyproject.tomldeclaresnumpy,pandas,edsl,policyengine-uswithout version bounds. Apolicyengine-uschange to e.g. SNAP rules will silently change the ground truth. temperature=1.0. Hardcoded inllm_estimator.py:93. Withtemperature=0(and OpenAI'sseedparameter, plus Gemini's equivalent where supported), variance from the LLM side approaches zero on supporting providers, andn_runs=10becomes mostly redundant.- No response cache. Every benchmark run re-pays API cost even for unchanged (model, scenario) pairs. A SHA-keyed cache (model_id + prompt + params → response) makes re-runs free and makes "what actually changed" diff'able.
- Scenarios live in code, not data.
households.generate_scenariosis seeded withRANDOM_SEED=42, so they're stable as long as the generator function doesn't change — but if it does, the "same seed" produces a different population without anyone noticing. - No provenance recorded. The output CSV stores
model,scenario_index,ground_truth, etc., but not edsl version, policyengine-us version, model snapshot strings, scenario hash, or total API calls. Hard to attribute drift between runs.
Suggested changes (smallest → largest)
- Pin model snapshots in
config.MODELSand document the deprecation horizon (each provider deprecates snapshots on a rolling schedule). - Pin all deps in
pyproject.tomlwith>=floors and<ceilings, or commit auv.lock/requirements.txt. - Set
temperature=0and pass provider-specific seeds where available (seed=42on OpenAI, etc.). Dropn_runsto 1 by default; keepn_runs > 1as an opt-in for variance studies. - Materialize scenarios: regenerate once with the seed, write
scenarios.jsonto the repo, load from disk inmain.py. Optional: also commit the generator commit hash so we know how it was produced. - Add a response cache (e.g., a JSON or SQLite-keyed-by-sha256-of-canonical-request store). Bonus: makes the benchmark runnable offline against the cache.
- Emit a
provenanceblock inbenchmark_output.csv(or a sidecar JSON): edsl version, policyengine-us version, snapshot IDs, scenario file hash, run timestamp, total API calls, total cost.
Reference
talkie-evals does this for an LM evaluation suite — pins all model HF revisions, dataset revisions, the lm-evaluation-harness task YAMLs, the Modal image's pip packages, and the sample seed. Every result JSON contains the full provenance block (talkie_evals_version, talkie_git_revision, model_revisions, dataset revisions, modal_pip_packages). Same pattern would port directly here.
Out of scope
- Replacing edsl. Worth a separate discussion (
inspect_ai/lm-evaluation-harness/ directlitellmwrapper) but orthogonal to determinism. - Switching from free-text $-amount answers to bucketed multiple-choice — separate metric design discussion.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading config.py, pyproject.toml, llm_estimator.py:93, households.generate_scenarios, and main.py to map the benchmark flow. Define a smaller implementation slice before changing behavior, since the issue lists model and dependency pinning, deterministic generation, scenario materialization, caching, and provenance. Done means the chosen slice has documented inputs, stable outputs, and evidence that reruns preserve or explain differences.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- machine-learning, testing-qa
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100