Skip to content

feat(profile): evaluate grid cells in greedy-nearest order#429

Merged
vsbuffalo merged 1 commit into
mainfrom
feature/profile-greedy-order
Jul 12, 2026
Merged

feat(profile): evaluate grid cells in greedy-nearest order#429
vsbuffalo merged 1 commit into
mainfrom
feature/profile-greedy-order

Conversation

@vsbuffalo

Copy link
Copy Markdown
Owner

camdl profile now 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_each avoided); 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-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.

Test

profile_greedy_order_invariant.rs: 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 (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).

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.
@vsbuffalo
vsbuffalo marked this pull request as draft July 11, 2026 22:23
@vsbuffalo

Copy link
Copy Markdown
Owner Author

Marking draft — not ready to merge. Two open items to resolve on this branch before it lands, both surfaced in real use:

  1. Perceived slowdown. The work-queue picks the nearest-to-best pending cell under a global mutex (an O(cells) min-search per pick). For a large grid of fast cells (e.g. nl-sbplx/ODE), that lock serialization may cost more than the greedy ordering saves. Needs a wall-clock A/B vs the old flat par_iter before this is a win. Candidate fixes if confirmed: coarser re-prioritization (pick under a shorter critical section / cache the best-cell coords), or fall back to the flat sweep above a cell-count threshold.

  2. Grid-merge identity bug (garki report). Re-running profile on the same model + same swept-param names but a different sweep range lands both grids in the same content-addressed base dir — cells accumulate into one jagged grid. The base identity excludes the sweep spec. This is pre-existing (not caused by the ordering change) but must be resolved for a clean run-level status. Fold the sweep spec (axes + ranges + counts) into the base identity, or a --label, weighed against losing cross-grid cell caching.

The non-greedy path is restored on main and reinstalled locally while these are worked here.

@vsbuffalo
vsbuffalo marked this pull request as ready for review July 12, 2026 00:30
@vsbuffalo
vsbuffalo merged commit 6155132 into main Jul 12, 2026
2 checks passed
@vsbuffalo
vsbuffalo deleted the feature/profile-greedy-order branch July 12, 2026 00:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant