feat(profile): evaluate grid cells in greedy-nearest order#429
Conversation
Replace the flat `remaining.par_iter().for_each` sweep over profile jobs with a greedy priority work-queue: N workers each pull the pending `(cell × seed × start)` job whose grid cell is nearest — in normalized focal-parameter space — to the current best-loglik cell. Two payoffs. Evaluation drills toward the optimum, so a 2D profile surface fills in near the best first and you can watch it converge (and notice a mis-placed grid early). And the progress bar now surfaces a best-profile-loglik metric, which profile deliberately lacked before (the old comment noted a global best would need a shared accumulator the `for_each` avoided) — that metric is the at-a-glance "is the grid placed well" signal. A work-queue rather than survey's fixed batches because profile jobs are heavy and high-variance (a full IF2/PMMH/NLopt optimization per cell): a batch barrier would idle cores waiting on each batch's slowest job, while the queue keeps every core busy and re-prioritizes continuously. This reorders WHEN cells run, never WHICH: every job is still evaluated exactly once, and each writes its own content-addressed leaf keyed on `(grid, seed, start)`, so the per-cell `mle.toml` results are byte-identical regardless of order or thread count. The best-cell tracker is a read-only side-channel. No early stop; on by default. `profile_greedy_order_invariant.rs` pins it: the same 2D profile at `--parallel 1` vs `--parallel 3` walks two different work-queue orders and must produce an identical set of per-cell `mle.toml` leaves. Goes red if a cell's result becomes order-dependent, or a cell is dropped or double-run.
|
Marking draft — not ready to merge. Two open items to resolve on this branch before it lands, both surfaced in real use:
The non-greedy path is restored on |
camdl profilenow evaluates its grid cells with a greedy priority work-queue: N workers each pull the pending(cell × seed × start)job whose grid cell is nearest — in normalized focal-parameter space — to the current best-loglik cell.Why
A 2D profile surface fills in near the optimum first, so you can watch it converge and notice a mis-placed grid early. And the progress bar now surfaces a best-profile-loglik metric — profile deliberately lacked one before (the old comment noted a global best would need a shared accumulator the
for_eachavoided); that metric is the at-a-glance "is the grid placed well" read.A work-queue rather than survey's fixed batches because profile jobs are heavy and high-variance (a full IF2/PMMH/NLopt optimization per cell): a batch barrier would idle cores waiting on each batch's slowest job, while the queue keeps every core busy and re-prioritizes continuously. This is the "iterator fed previous results to fill cores, prioritizing cells near the best LL" shape.
Byte-identical output
Reorders when cells run, never which: every job is still evaluated exactly once, and each writes its own content-addressed leaf keyed on
(grid, seed, start), so the per-cellmle.tomlresults are byte-identical regardless of order or thread count. The best-cell tracker is a read-only side-channel. No early stop; on by default.Test
profile_greedy_order_invariant.rs: the same 2D profile at--parallel 1vs--parallel 3walks two different work-queue orders and must produce an identical set of per-cellmle.tomlleaves (all cells present, none dropped or double-run). Goes red if a cell's result becomes order-dependent.Green locally: the new invariant test, plus
profile_diagnostics,profile_pmmh,profile_priors,profile_multi_stream,profile_indexed_long_obs(no regression — the change is in the loop every profile path shares).