Skip to content

(ci) Add workflows for testing and benchmarking best-of-n - #248

Open
s-akhtar-baig wants to merge 4 commits into
mainfrom
handle_best_of_n
Open

(ci) Add workflows for testing and benchmarking best-of-n#248
s-akhtar-baig wants to merge 4 commits into
mainfrom
handle_best_of_n

Conversation

@s-akhtar-baig

@s-akhtar-baig s-akhtar-baig commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Description

  • Add Best-of-N benchmark support
  • Enable Best-of-N in CI workflows — added best-of-n to the benchmark matrix and e2e test algorithm list
  • Refactor benchmark script to use a shared orchestrator instance and eliminate duplicate code

Closes: #184

Checklist

  • Tests pass locally (uv run pytest)
  • Linting passes (uv run ruff check its_hub/)
  • Run e2e workflow for self-consistency and best-of-n
  • Run benchmark workflow for self-consistency and best-of-n

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added best-of-n to both benchmarking and end-to-end test coverage alongside self-consistency.
    • Increased default concurrency to improve benchmark run performance.
  • Bug Fixes

    • Updated benchmark workflow setup to avoid installing an unnecessary dependency extras group.
  • Chores

    • Bumped the datasets version in the optional research dependency set.

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: de6dc1df-f6e0-44f8-8395-4d22afeed060

📥 Commits

Reviewing files that changed from the base of the PR and between a28d615 and 92da618.

📒 Files selected for processing (2)
  • .github/workflows/benchmark.yaml
  • pyproject.toml
✅ Files skipped from review due to trivial changes (1)
  • pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/benchmark.yaml

📝 Walkthrough

Walkthrough

This PR adds best-of-n support to the benchmark pipeline, rewrites algorithm setup around explicit lm and orchestrator instances, switches benchmark execution to always-async inference, and updates CI coverage and dependency constraints for the new benchmark path.

Changes

Async Best-of-N Benchmark Support

Layer / File(s) Summary
Algorithm contract and initialization
benchmarking/benchmark.py
Adds BEST_OF_N, reorganizes imports, and rewrites init_algorithm to build each scaling algorithm from provided lm/orchestrator instances and reward-model configuration.
CLI options and main wiring
benchmarking/benchmark.py
Removes --is_async, updates --max_concurrency and --rm_name help text, and wires LMOrchestrator creation into main.
Async inference execution and output handling
benchmarking/benchmark.py
Moves benchmark execution into asyncio.run(run_benchmark()), switches inference calls to await scaling_alg.ainfer(...), and updates output selection logic.
Workflow and dependency updates
.github/workflows/benchmark.yaml, .github/workflows/e2e.yaml, pyproject.toml
Expands benchmark/e2e coverage to include best-of-n and raises the research extras datasets constraint.

Estimated code review effort: 3 (Moderate) | ~25 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The datasets>=5.0.0 dependency bump in pyproject.toml is unrelated to the stated testing/benchmarking refactor goals. Either remove the dependency version change or document why it is required for the best-of-n benchmark changes.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding best-of-n support to CI workflows for testing and benchmarking.
Linked Issues check ✅ Passed The workflow and benchmark updates add test coverage for the refactor and include best-of-n in CI, aligning with issue #184.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch handle_best_of_n

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
benchmarking/benchmark.py (1)

267-280: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Separate expected-pass-at-one outputs from standard runs.

Both modes currently share the same filename, but one writes response and the other writes responses/log_probs. Reusing a prior run can read missing columns or skip incompatible rows.

Suggested fix
     else:
         alg_str = alg.value
+    if eval_expected_pass_at_one:
+        alg_str += "-expected-pass-at-one"
     output_file = os.path.join(

Also applies to: 374-384

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarking/benchmark.py` around lines 267 - 280, The filename construction
in the benchmark output path is still shared between standard runs and
expected-pass-at-one runs, which can mix incompatible JSONL schemas. Update the
filename logic in the benchmark output builder so the branch that produces
`response` is separated from the branch that produces `responses`/`log_probs`,
using a distinct suffix or identifier in the `alg_str`/`output_file` generation
to avoid reusing incompatible prior outputs. Apply the same change anywhere the
output filename is built for this benchmark flow, including the other referenced
block that uses the same naming pattern.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@benchmarking/benchmark.py`:
- Around line 388-394: Handle the flattened ParticleFilteringResult shape in the
benchmarking path: the branch that calls scaling_alg.ainfer(...,
return_response_only=False) should not read result.responses_lst for
particle-filtering. Update the y_full construction to use the flattened fields
returned by ParticleFiltering.ainfer, especially responses and log_weights_lst,
so the expected-pass-at-one computation sees the actual samples instead of an
empty/mismatched payload. Use the ParticleFilteringResult handling in
benchmarking/benchmark.py and the scaling_alg.ainfer call site to locate the
fix.
- Around line 430-431: In the expected-pass-at-one scoring path,
row["log_probs"] is still a Python list, so _softmax() will fail when it
performs NumPy operations. Update the logic around the row["correct"]
calculation to convert row["log_probs"] to a NumPy array before calling
_softmax(), using the existing result.log_weights_lst / cached log_probs flow as
the source, so the benchmark evaluation can complete without TypeError.
- Line 458: The benchmark teardown is closing lm in a separate asyncio.run after
the event loop that created its cached aiohttp sessions has already been torn
down. Move the await lm.close() call into run_benchmark() itself, ideally in a
finally block near the existing benchmark loop, so the lm sessions are released
before the loop exits and use the run_benchmark function as the main place to
locate this fix.

---

Outside diff comments:
In `@benchmarking/benchmark.py`:
- Around line 267-280: The filename construction in the benchmark output path is
still shared between standard runs and expected-pass-at-one runs, which can mix
incompatible JSONL schemas. Update the filename logic in the benchmark output
builder so the branch that produces `response` is separated from the branch that
produces `responses`/`log_probs`, using a distinct suffix or identifier in the
`alg_str`/`output_file` generation to avoid reusing incompatible prior outputs.
Apply the same change anywhere the output filename is built for this benchmark
flow, including the other referenced block that uses the same naming pattern.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a80d6f3b-3208-4ad2-b1e1-dd520241e72e

📥 Commits

Reviewing files that changed from the base of the PR and between b54960d and a28d615.

📒 Files selected for processing (3)
  • .github/workflows/benchmark.yaml
  • .github/workflows/e2e.yaml
  • benchmarking/benchmark.py

Comment thread benchmarking/benchmark.py
Comment on lines +388 to +394
result = await scaling_alg.ainfer(
lm, x["problem"], n, return_response_only=False
)
y_full = {
"responses": result.responses_lst[-1],
"log_probs": result.log_weights_lst[-1],
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Handle the flattened ParticleFilteringResult shape.

Line 392 expects responses_lst, but ParticleFiltering.ainfer(..., return_response_only=False) returns flattened responses and log_weights_lst, so particle-filtering expected-pass-at-one skips every example.

Suggested fix
                                 result = await scaling_alg.ainfer(
                                     lm, x["problem"], n, return_response_only=False
                                 )
+                                if hasattr(result, "responses"):
+                                    responses = result.responses
+                                    log_probs = result.log_weights_lst
+                                else:
+                                    responses = result.responses_lst[-1]
+                                    log_probs = result.log_weights_lst[-1]
                                 y_full = {
-                                    "responses": result.responses_lst[-1],
-                                    "log_probs": result.log_weights_lst[-1],
+                                    "responses": responses,
+                                    "log_probs": log_probs,
                                 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
result = await scaling_alg.ainfer(
lm, x["problem"], n, return_response_only=False
)
y_full = {
"responses": result.responses_lst[-1],
"log_probs": result.log_weights_lst[-1],
}
result = await scaling_alg.ainfer(
lm, x["problem"], n, return_response_only=False
)
if hasattr(result, "responses"):
responses = result.responses
log_probs = result.log_weights_lst
else:
responses = result.responses_lst[-1]
log_probs = result.log_weights_lst[-1]
y_full = {
"responses": responses,
"log_probs": log_probs,
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarking/benchmark.py` around lines 388 - 394, Handle the flattened
ParticleFilteringResult shape in the benchmarking path: the branch that calls
scaling_alg.ainfer(..., return_response_only=False) should not read
result.responses_lst for particle-filtering. Update the y_full construction to
use the flattened fields returned by ParticleFiltering.ainfer, especially
responses and log_weights_lst, so the expected-pass-at-one computation sees the
actual samples instead of an empty/mismatched payload. Use the
ParticleFilteringResult handling in benchmarking/benchmark.py and the
scaling_alg.ainfer call site to locate the fix.

Comment thread benchmarking/benchmark.py
Comment on lines +430 to +431
p = _softmax(row["log_probs"])
row["correct"] = np.dot(p, c)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C2 'def _softmax|x_shifted = x - np.max|_softmax\(row\["log_probs"\]\)' \
  its_hub/core/algorithms/particle_gibbs.py benchmarking/benchmark.py

Repository: Red-Hat-AI-Innovation-Team/its_hub

Length of output: 1041


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== benchmark.py context ==\n'
sed -n '380,445p' benchmarking/benchmark.py

printf '\n== log_probs references ==\n'
rg -n -C3 'log_probs' benchmarking its_hub

printf '\n== softmax definition ==\n'
sed -n '68,80p' its_hub/core/algorithms/particle_gibbs.py

Repository: Red-Hat-AI-Innovation-Team/its_hub

Length of output: 6205


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== log_weights_lst references ==\n'
rg -n -C4 'log_weights_lst|responses_lst|class .*Result|return_response_only|ainfer\(' its_hub benchmarking

printf '\n== surrounding ainfer implementations ==\n'
for f in $(rg -l 'log_weights_lst|responses_lst' its_hub benchmarking); do
  echo "--- $f ---"
  sed -n '1,260p' "$f" | sed -n '1,260p'
done

Repository: Red-Hat-AI-Innovation-Team/its_hub

Length of output: 50390


Convert log_probs to a NumPy array before _softmax().
row["log_probs"] is a Python list here (result.log_weights_lst[-1] / cached log_probs), so _softmax() will raise TypeError when it subtracts np.max(x) and abort expected-pass-at-one evaluation.

Suggested fix
-                            p = _softmax(row["log_probs"])
+                            p = _softmax(np.asarray(row["log_probs"], dtype=float))
                             row["correct"] = np.dot(p, c)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
p = _softmax(row["log_probs"])
row["correct"] = np.dot(p, c)
p = _softmax(np.asarray(row["log_probs"], dtype=float))
row["correct"] = np.dot(p, c)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarking/benchmark.py` around lines 430 - 431, In the
expected-pass-at-one scoring path, row["log_probs"] is still a Python list, so
_softmax() will fail when it performs NumPy operations. Update the logic around
the row["correct"] calculation to convert row["log_probs"] to a NumPy array
before calling _softmax(), using the existing result.log_weights_lst / cached
log_probs flow as the source, so the benchmark evaluation can complete without
TypeError.

Comment thread benchmarking/benchmark.py

return rows, budget_timings

rows, budget_timings = asyncio.run(run_benchmark())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C3 'asyncio\.run\(run_benchmark\(\)\)|asyncio\.run\(lm\.close\(\)\)|WeakKeyDictionary|async def close|_get_session' \
  benchmarking/benchmark.py its_hub/core/lms/openai_lm.py

Repository: Red-Hat-AI-Innovation-Team/its_hub

Length of output: 3551


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' its_hub/core/lms/openai_lm.py
printf '\n--- benchmark excerpt ---\n'
sed -n '390,500p' benchmarking/benchmark.py

Repository: Red-Hat-AI-Innovation-Team/its_hub

Length of output: 12404


Close LM sessions on the benchmark loop. benchmarking/benchmark.py:458-482 tears down the event loop that created the cached aiohttp sessions, then closes lm in a second asyncio.run(). Move await lm.close() into run_benchmark() (for example in finally) so the sessions are released before that loop exits.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarking/benchmark.py` at line 458, The benchmark teardown is closing lm
in a separate asyncio.run after the event loop that created its cached aiohttp
sessions has already been torn down. Move the await lm.close() call into
run_benchmark() itself, ideally in a finally block near the existing benchmark
loop, so the lm sessions are released before the loop exits and use the
run_benchmark function as the main place to locate this fix.

@codecov

codecov Bot commented Jun 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@beatsmonster

Copy link
Copy Markdown
Contributor

@s-akhtar-baig can we rebase?

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.

Add test coverage for the refactor changes

2 participants