SeedReport: Saving & Replaying RNG seed capability. - #378
SeedReport: Saving & Replaying RNG seed capability.#378drewconnelly-qntm wants to merge 16 commits into
SeedReport: Saving & Replaying RNG seed capability.#378Conversation
Testing the following: - tests seedreport data saving correctly - tests determinism when seeds are the same, and disagreement when seeds are not - tests agreement between a job and the re-running of that job - tests loading a seed report from json and string, as well as failure when no file exists to import from.
|
I haven't looked closely but just skimming things: You might want to see if you can do this without using an Arc Mutex. They are slightly expensive. |
Thanks, I'll check on how necessary that is. |
It was annoying the lint checker.
ReviewNice feature and the right general shape: the report is the single source of truth for worker seeds, the JSON round-trip is tested, and error paths map to 1. The replay tests are vacuous (blocking)The two headline tests — Shot-level determinism is untested anywhere: Fix: use a fixture whose output actually depends on the RNG (e.g. 2. ~110 lines duplicated verbatim (blocking)
fn run_workers_with_seeds(
&self,
worker_seeds: &[u64],
shots_per_worker: &[usize],
) -> Result<ShotVec, PecosError>and having both paths call it. 3. Replay half-ignores the loaded reportA
4.
|
Review:
|
ciaranra
left a comment
There was a problem hiding this comment.
Anchoring three of the items from my earlier comment to the lines they apply to — same findings, no new ones.
| } | ||
|
|
||
| let shots_per_worker = distribute_shots(num_shots, num_workers); | ||
| self.set_seed(seed_report.root_seed); // make sure to update root seed. |
There was a problem hiding this comment.
This is the blocking item. Because run and run_with_workers both funnel through run_with_workers_from_seed_report, this line re-seeds the engine on every run, so consecutive runs on one engine return byte-identical shots and replaying an archived report overwrites the caller's seed.
Replay does not need it: each worker is seeded from the report at line 404, and nothing downstream reads self.rng or the template seed. Removing it restores base_seed = self.rng.next_u64() advancing per run, and makes replay non-mutating.
| self.set_seed(seed_report.root_seed); // make sure to update root seed. |
With this gone, the only remaining use of &mut self in this method is the template clone, so the signature can become &self — which turns "replay does not mutate the engine" into a compiler-checked property. Existing callers are unaffected.
| let worker_seed = derive_seed(base_seed, &format!("worker_{worker_idx}")); | ||
| engine.set_seed(worker_seed); | ||
| engine.set_seed(seed_report.workers[worker_idx].seed); | ||
| (worker_idx, shots_per_worker[worker_idx], engine) |
There was a problem hiding this comment.
Related to the same comment: the worker tuples are built from shots_per_worker (re-derived by distribute_shots on line 387) rather than from the records in the report, which is what forces the two assert!(.., "..")s below to police the redundancy — and those abort the process on a hand-edited or older-format report instead of returning PecosError::Input.
Driving replay from the records instead makes the report self-describing and lets the asserts, the distribute_shots call, and the one-sided workers.len() < num_workers check all go away:
let worker_engines: Vec<_> = seed_report
.workers
.iter()
.map(|record| {
let mut engine = self.hybrid_engine_template.clone();
engine.set_seed(record.seed);
(record.worker_idx, record.shots, engine)
})
.collect();Then num_threads takes seed_report.workers.len(), and the Vec::with_capacity(num_shots) on line 393 can be a plain Vec::new() — a few reallocations are nothing next to running the shots, and a corrupted "num_shots": 10000000000000 no longer requests a multi-terabyte allocation before the first shot.
If the scalar fields should stay authoritative, one consistency check at the top of the method replaces all three panics:
if seed_report.workers.is_empty() {
return Err(PecosError::Input(
"Seed report contains no worker records".to_string(),
));
}
if seed_report.workers.len() != seed_report.num_workers {
return Err(PecosError::Input(format!(
"Seed report contains {} worker records, but num_workers is {}",
seed_report.workers.len(),
seed_report.num_workers
)));
}Either way the # Panics section on the doc comment can go.
| dyn-clone.workspace = true | ||
| num-bigint.workspace = true | ||
| bitvec.workspace = true | ||
| tempfile.workspace = true |
There was a problem hiding this comment.
tempfile is used only by tests/seed_report.rs, so as written it ships as a runtime dependency of pecos-engines and everything downstream (pecos, pecos-hugr, pecos-qasm, pecos-foreign, pecos-phir-json, benchmarks), pulling in its rustix/getrandom/windows-sys chain. Every other crate in the workspace lists it under [dev-dependencies].
| tempfile.workspace = true |
and add at the end of the file:
[dev-dependencies]
tempfile.workspace = true|
Follow-up: I applied the suggestions from the inline comments in a scratch worktree at The regression reproduces on the current head. Two tests, both failing before any change: The second compares two engines seeded at 999: one runs twice, the other runs, replays an unrelated report, then runs again. The report's With the fixes applied — drop the
Net diff: 28 insertions, 36 deletions across Two things the change turns up that are worth knowing before you apply it:
Not verified on my side: the |
Changes:
In
crates/pecos-engines/src/monte_carlo/engine.rs:SeedReportwhich stores the RNG seed information for a monte carlo runrun_with_workers_seed_reportwhich runs a monte carlo job with workers, while managing the RNG seeds with a seed report. Has a bool option to save the report to a JSON namesseed_report.json.WorkerSeedRecordwhich stores the worker index, num shots, and seed for that worker, which is what is held in a vector inside the overall job'sSeedReportfor each monte carlo job.SeedReportto JSON as well as loading them from JSON.Potential Future Work:
Testing:
cargo test -p pecos-engineswas all goodjust build; just testran through the rust tests fine but had issues with the python pytests due to some bugs with my environment locally.