Skip to content

perf(experiments): parallelize Phase 1 subject calls in Evaluator#4853

Merged
bug-ops merged 2 commits into
mainfrom
4794-evaluator-parallel-phase1
Jun 5, 2026
Merged

perf(experiments): parallelize Phase 1 subject calls in Evaluator#4853
bug-ops merged 2 commits into
mainfrom
4794-evaluator-parallel-phase1

Conversation

@bug-ops

@bug-ops bug-ops commented Jun 5, 2026

Copy link
Copy Markdown
Owner

Summary

  • Replace the sequential for-loop in Evaluator::evaluate Phase 1 (subject model calls) with FuturesUnordered + Arc<Semaphore>, mirroring the existing Phase 2 (judge) parallelization pattern exactly.
  • Concurrency is bounded by the existing parallel_evals field (default: 3) — no new config fields.
  • Result ordering is restored via sort_unstable_by_key(|(i, _)| *i) before Phase 2.
  • Updated all doc comments to reflect Phase 1 is now parallel and clarified fatal-error semantics under concurrent execution.

Performance (before/after)

Metric Before After (k=3)
Phase 1 complexity O(n) O(⌈n/k⌉)
n=20 cases, p50=2 s/call ~40 s ~14 s
n=50 cases, p50=2 s/call ~100 s ~34 s

The hill-climbing loop in ExperimentEngine::run_loop can now evaluate roughly 3× more variation candidates within the same max_wall_time_secs budget.

Implementation notes

  • AnyProvider::clone() resets UsageTracker to default — each parallel future has genuinely task-local state, no shared mutable state through the clone.
  • Timeout fires after permit acquisition (clock starts when the LLM call begins, not when queued).
  • Error semantics unchanged: any subject failure is fatal and propagates immediately; the _permit is dropped per-future so all semaphore permits are released correctly.

Follow-up issues (non-blocking, filed separately)

  • parallel_eval_respects_concurrency_limit test does not assert the concurrency ceiling for Phase 1.
  • No test for out-of-order subject response ordering in Phase 1.
  • Consider per-case subject timeout tolerance to match Phase 2 graceful-degradation semantics.

Closes #4794

bug-ops added 2 commits June 5, 2026 21:32
Subject model calls in Evaluator::evaluate were sequential. Mirror the
existing Phase 2 pattern: use FuturesUnordered + Arc<Semaphore> bounded
by the parallel_evals field (default 3) for both phases.

After all subject futures complete, sort by case index to restore
deterministic ordering before Phase 2 begins. Error semantics unchanged:
any subject failure (Llm or Timeout) is fatal and propagates immediately.

Closes #4794
@bug-ops bug-ops force-pushed the 4794-evaluator-parallel-phase1 branch from 783a9e0 to a1b9dfe Compare June 5, 2026 19:32
@bug-ops bug-ops merged commit 836f6a4 into main Jun 5, 2026
32 checks passed
@bug-ops bug-ops deleted the 4794-evaluator-parallel-phase1 branch June 5, 2026 19:39
bug-ops added a commit that referenced this pull request Jun 5, 2026
…prove evaluator tests

Closes #4870, #4854, #4855

Fix total_tokens overcounting (#4870): Evaluator::evaluate reserved a +1 slot per
admitted judge call to prevent TOCTOU budget overshoot. The reservation was never
cancelled, causing total_tokens to be overstated by the number of successful judge
calls. Fix applies the correction at report-build time: raw counter is adjusted by
saturating_sub(cases_scored) after all futures join, keeping the budget guard
conservative during execution.

Fix concurrency limit test (#4854): parallel_eval_respects_concurrency_limit
declared a peak counter but never incremented it, so the assertion proved only
absence of deadlock. MockProvider now tracks in-flight calls with atomics and
records the peak via fetch_max. Test asserts both peak <= parallel_evals and
peak >= parallel_evals (lower bound prevents silent serialization regressions).

Add ordering test (#4855): new test subject_responses_ordered_after_parallel_phase1
uses per-call delays so subject futures complete in reverse order, then asserts that
per_case[i].case_index == i for all i. Verifies the sort_unstable_by_key invariant
introduced in PR #4853.

Also make MockProvider::chat yield_now() conditional on delay > 0 to avoid
scheduler preemption in zero-delay sequential callers.
bug-ops added a commit that referenced this pull request Jun 5, 2026
…prove evaluator tests

Closes #4870, #4854, #4855

Fix total_tokens overcounting (#4870): Evaluator::evaluate reserved a +1 slot per
admitted judge call to prevent TOCTOU budget overshoot. The reservation was never
cancelled, causing total_tokens to be overstated by the number of successful judge
calls. Fix applies the correction at report-build time: raw counter is adjusted by
saturating_sub(cases_scored) after all futures join, keeping the budget guard
conservative during execution.

Fix concurrency limit test (#4854): parallel_eval_respects_concurrency_limit
declared a peak counter but never incremented it, so the assertion proved only
absence of deadlock. MockProvider now tracks in-flight calls with atomics and
records the peak via fetch_max. Test asserts both peak <= parallel_evals and
peak >= parallel_evals (lower bound prevents silent serialization regressions).

Add ordering test (#4855): new test subject_responses_ordered_after_parallel_phase1
uses per-call delays so subject futures complete in reverse order, then asserts that
per_case[i].case_index == i for all i. Verifies the sort_unstable_by_key invariant
introduced in PR #4853.

Also make MockProvider::chat yield_now() conditional on delay > 0 to avoid
scheduler preemption in zero-delay sequential callers.
bug-ops added a commit that referenced this pull request Jun 5, 2026
…prove evaluator tests (#4888)

Closes #4870, #4854, #4855

Fix total_tokens overcounting (#4870): Evaluator::evaluate reserved a +1 slot per
admitted judge call to prevent TOCTOU budget overshoot. The reservation was never
cancelled, causing total_tokens to be overstated by the number of successful judge
calls. Fix applies the correction at report-build time: raw counter is adjusted by
saturating_sub(cases_scored) after all futures join, keeping the budget guard
conservative during execution.

Fix concurrency limit test (#4854): parallel_eval_respects_concurrency_limit
declared a peak counter but never incremented it, so the assertion proved only
absence of deadlock. MockProvider now tracks in-flight calls with atomics and
records the peak via fetch_max. Test asserts both peak <= parallel_evals and
peak >= parallel_evals (lower bound prevents silent serialization regressions).

Add ordering test (#4855): new test subject_responses_ordered_after_parallel_phase1
uses per-call delays so subject futures complete in reverse order, then asserts that
per_case[i].case_index == i for all i. Verifies the sort_unstable_by_key invariant
introduced in PR #4853.

Also make MockProvider::chat yield_now() conditional on delay > 0 to avoid
scheduler preemption in zero-delay sequential callers.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation performance Performance improvements rust Rust code changes size/M Medium PR (51-200 lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(experiments): Evaluator subject calls are sequential — large benchmarks block wall-time budget

1 participant