Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c6b2b0a
update dependencies to support Qwen 3.5
ErlisLushtaku Mar 31, 2026
1f4bae8
slurmpilot scripts
ErlisLushtaku Apr 1, 2026
25b0355
update dep versions
ErlisLushtaku Apr 1, 2026
ab065fd
fix support for VLLM
ErlisLushtaku Apr 6, 2026
ef1c92c
remove qwen35 smoke launcher
ErlisLushtaku Apr 6, 2026
32f2e7e
use json schema structured outputs, tighten vllm range
ErlisLushtaku Apr 7, 2026
5f2edf0
fix formatting
ErlisLushtaku Apr 7, 2026
cffb6dd
Fix Qwen3.5 with mt-bench
ErlisLushtaku Apr 7, 2026
ac243aa
use latest vllm with thinking tokens limits and thinking field in the…
ErlisLushtaku Apr 13, 2026
41298a4
Merge remote-tracking branch 'origin/main' into erlislushtaku/fix/sup…
ErlisLushtaku Apr 14, 2026
319050d
thinking token handling improvements, mt-bench improvements, use mt-b…
ErlisLushtaku Apr 14, 2026
cb7ada5
Revert to free form generation, and use thinking token budget with re…
ErlisLushtaku Apr 15, 2026
84faa05
revert unnecessary changes and relics from earlier trials
ErlisLushtaku Apr 17, 2026
c063f3d
delete slurmpilot script
ErlisLushtaku Apr 17, 2026
ec7fc95
Revert comment removal
ErlisLushtaku Apr 17, 2026
20ca9a5
simplify and revert unnecessary changes
ErlisLushtaku Apr 17, 2026
217dc8d
Support Skywork
ErlisLushtaku Apr 17, 2026
8087c15
Add judge input character truncation and model length configurations …
ErlisLushtaku Apr 20, 2026
91d67ef
add llmcompressor dev dependency for quantization
ErlisLushtaku Apr 20, 2026
5e8efc9
Update baseline handling for Arena-Hard datasets
ErlisLushtaku Apr 21, 2026
2af4714
Add m-arenahard-v2.0
ErlisLushtaku Apr 21, 2026
da6818e
add default baseline for mt-bench
ErlisLushtaku Apr 21, 2026
891c417
handle prohibited content errors for gemini in openrouter
ErlisLushtaku Apr 21, 2026
fb36154
update system prompt with alpaca eval version, fix mismatch for expec…
ErlisLushtaku Apr 22, 2026
f33f191
roll back to the default system prompt
ErlisLushtaku Apr 28, 2026
e21639e
update dependencies for qwen3.5 and gemma4 runs
ErlisLushtaku Apr 28, 2026
157d939
Merge origin/main into support-qwen-3.5
ErlisLushtaku Apr 28, 2026
41d925e
Improve pairwise benchmark run controls and accounting
ErlisLushtaku Apr 29, 2026
16dc5e1
Clean up judge argument handling
ErlisLushtaku Apr 29, 2026
5411ff8
Add default score-based verdict mode for fastchat
ErlisLushtaku Apr 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 34 additions & 18 deletions judgearena/evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
do_inference,
download_hf,
read_df,
truncate,
)


Expand Down Expand Up @@ -51,6 +52,18 @@ def get_regexp_match(self, s: str, regex: str, group_index: int = 1):
return float(m.group(group_index).strip(" "))


_PAIR_SCORE_MIN = 0
_PAIR_SCORE_MAX = 10


def build_pair_score_output_choices() -> list[str]:
Comment thread
ErlisLushtaku marked this conversation as resolved.
Outdated
return [
f"score_A: {a}\nscore_B: {b}"
for a in range(_PAIR_SCORE_MIN, _PAIR_SCORE_MAX + 1)
for b in range(_PAIR_SCORE_MIN, _PAIR_SCORE_MAX + 1)
]


_COMPLETION_LABEL_SINGLE = "Answer"
_COMPLETION_LABEL_MULTI_TURN = "Conversation with User"
_EXPLANATION_SUFFIX = ", first starts with an explanation of your judgement"
Expand Down Expand Up @@ -302,27 +315,30 @@ def annotate_battles(
prompt_template = ChatPromptTemplate.from_messages(
[("system", system_prompt), ("user", user_prompt_template)]
)

def truncate(s: str, max_len: int | None = None):
if not isinstance(s, str):
return ""
if max_len is not None:
return s[:max_len]
else:
return s

inputs = prompt_template.batch(
[
truncated_completion_count = 0
input_payloads = []
for user_prompt, completion_A, completion_B in zip(
instructions, completions_A, completions_B, strict=True
):
truncated_completion_A = truncate(completion_A, max_len=truncate_input_chars)
truncated_completion_B = truncate(completion_B, max_len=truncate_input_chars)
truncated_completion_count += int(truncated_completion_A != completion_A)
truncated_completion_count += int(truncated_completion_B != completion_B)
input_payloads.append(
{
"user_prompt": user_prompt,
"completion_A": truncate(completion_A, max_len=truncate_input_chars),
"completion_B": truncate(completion_B, max_len=truncate_input_chars),
"completion_A": truncated_completion_A,
"completion_B": truncated_completion_B,
}
for user_prompt, completion_A, completion_B in zip(
instructions, completions_A, completions_B, strict=True
)
]
)
)
if truncated_completion_count:
print(
Comment thread
ErlisLushtaku marked this conversation as resolved.
Outdated
"Warning: truncated "
f"{truncated_completion_count} judge completions to "
f"{truncate_input_chars} characters before evaluation."
)
inputs = prompt_template.batch(input_payloads)

print(f"Start LLM judge annotation ({len(inputs)} annotations).")
judge_completions = do_inference(
chat_model=judge_chat_model,
Expand Down
44 changes: 39 additions & 5 deletions judgearena/generate_and_evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@

import pandas as pd

from judgearena.evaluate import judge_and_parse_prefs, resolve_judge_prompts
from judgearena.evaluate import (
build_pair_score_output_choices,
judge_and_parse_prefs,
resolve_judge_prompts,
)
from judgearena.generate import generate_base, generate_instructions
from judgearena.instruction_dataset import load_instructions
from judgearena.mt_bench.mt_bench_utils import run_mt_bench
Expand All @@ -30,16 +34,40 @@
def try_load_dataset_completions(
dataset: str, model: str, n_instructions: int | None
) -> pd.DataFrame | None:
"""Try loading pre-existing completions from the dataset.
"""Try loading pre-existing completions from the dataset or a local file.

Some datasets (e.g. alpaca-eval) ship with completions for well-known
models such as ``gpt4_1106_preview``. When ``model`` matches a column in
``model_outputs/{dataset}.csv.zip``, those completions are returned
directly so that no model instantiation / generation is needed.

``model`` may also be a local dataframe path. Local files must contain
``instruction_index`` and ``output`` columns.

Returns a DataFrame with columns ``completion`` and ``instruction_index``,
or ``None`` when no pre-existing completions are found.
"""
local_path = Path(model)
if local_path.exists():
print(f"Loading completions from local path '{local_path}'.")
df_outputs = read_df(local_path)
required_columns = {"instruction_index", "output"}
missing_columns = required_columns.difference(df_outputs.columns)
if missing_columns:
missing_columns_list = ", ".join(sorted(missing_columns))
raise ValueError(
f"Local completion file '{local_path}' is missing required columns: "
f"{missing_columns_list}."
)

df_outputs = df_outputs.loc[:, ["instruction_index", "output"]].rename(
columns={"output": "completion"}
)
df_outputs.loc[:, "completion"] = df_outputs.loc[:, "completion"].fillna("")
if n_instructions is not None:
df_outputs = df_outputs.head(n_instructions)
return df_outputs

local_path_tables = data_root / "tables"
download_hf(name=dataset, local_path=local_path_tables)
output_path = local_path_tables / "model_outputs" / f"{dataset}.csv.zip"
Expand Down Expand Up @@ -337,7 +365,7 @@ def main(args: CliArgs):
)
if dataset_completions_A is not None:
completions_A = dataset_completions_A.set_index("instruction_index").loc[
:, "completion"
instructions.index, "completion"
]
else:
completions_A = cache_function_dataframe(
Expand All @@ -356,7 +384,7 @@ def main(args: CliArgs):
)
if dataset_completions_B is not None:
completions_B = dataset_completions_B.set_index("instruction_index").loc[
:, "completion"
instructions.index, "completion"
]
else:
completions_B = cache_function_dataframe(
Expand All @@ -377,12 +405,18 @@ def main(args: CliArgs):
print(completions_B.values[0])
print(f"Evaluating completions with judge {args.judge_model}.")

judge_model_kwargs = dict(args.engine_kwargs)
if not args.provide_explanation and args.judge_model.split("/")[0] == "VLLM":
judge_model_kwargs["structured_outputs_choice"] = (
build_pair_score_output_choices()
)

judge_chat_model = make_model(
model=args.judge_model,
max_tokens=args.max_out_tokens_judge,
max_model_len=args.max_model_len,
chat_template=args.chat_template,
**args.engine_kwargs,
**judge_model_kwargs,
)

name = f"{args.dataset}-{args.model_A}-{args.model_B}-{args.judge_model}"
Expand Down
23 changes: 18 additions & 5 deletions judgearena/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ def __init__(
**vllm_kwargs,
):
from vllm import LLM, SamplingParams
from vllm.sampling_params import StructuredOutputsParams

self.model_path = model
self.max_tokens = max_tokens
Expand Down Expand Up @@ -230,13 +231,19 @@ def __init__(
RuntimeWarning,
stacklevel=2,
)
self._sampling_params_kwargs = {
"max_tokens": max_tokens,
"temperature": float(vllm_kwargs.pop("temperature", 0.6)),
"top_p": float(vllm_kwargs.pop("top_p", 0.95)),
}
structured_outputs_choice = vllm_kwargs.pop("structured_outputs_choice", None)
if structured_outputs_choice is not None:
self._sampling_params_kwargs["structured_outputs"] = (
StructuredOutputsParams(choice=structured_outputs_choice)
)
self.sampling_params = SamplingParams(**self._sampling_params_kwargs)

self.llm = LLM(model=model, trust_remote_code=True, **vllm_kwargs)
self.sampling_params = SamplingParams(
max_tokens=max_tokens,
temperature=0.6,
top_p=0.95,
)

# Resolve chat template:
# 1. Explicit override always wins → use chat() with that template
Expand All @@ -262,6 +269,12 @@ def __init__(
self._use_generate = False
print(f"ChatVLLM: using tokenizer's chat template for '{model}'")

def set_temperature(self, temperature: float) -> None:
from vllm import SamplingParams

self._sampling_params_kwargs["temperature"] = float(temperature)
self.sampling_params = SamplingParams(**self._sampling_params_kwargs)

def _to_messages(self, input_item) -> list[dict]:
"""Convert LangChain prompt input to OpenAI-style messages."""
# Map LangChain message types to OpenAI roles
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,6 @@ quote-style = "double"
indent-style = "space"

[project.optional-dependencies]
vllm = ["vllm==0.10.2", "transformers>=4.55.2,<5.0.0"]
# vLLM on PyPI pins transformers<5; optional extra matches that so `uv lock` can resolve.
vllm = ["vllm>=0.17.0,<1.0.0", "transformers>=4.56.0,<5.0.0"]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

vllm>=0.17.0,<1.0.0 is a very wide range. A few concerns:

  • Was this tested with a prebuilt wheel or built from source? Building vLLM from source on cluster nodes often fails due to CUDA kernel compilation issues.
  • Is the StructuredOutputsParams import path (vllm.sampling_params) stable across this entire range? It may have been introduced in 0.17 and could move. For example StructuredOutputParams was a bit different when vllm==0.11.0. Thus I think it makes more sense to create more stable versioning

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good point. I tightened the range. 0.18.1 was working. I think the StructuredOutputParams is stable accross the new range.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Changed it to v0.19+ so that we can use the thinking token limit parameter, and also they have some fixes for Qwen3.5

llamacpp = ["llama-cpp-python>=0.3.0"]
2 changes: 1 addition & 1 deletion slurmpilot_scripts/launch_generation_and_evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
"dataset": f"{language}-contexts",
"model_A": baseline,
"model_B": model,
"judge_model": "VLLM/Qwen/Qwen2.5-32B-Instruct-GPTQ-Int8",
"judge_model": "VLLM/Qwen/Qwen3.5-27B-FP8",
"n_instructions": 100,
# "ignore_cache": None,
}
Expand Down
121 changes: 121 additions & 0 deletions tests/test_local_completion_loading.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import pandas as pd

import judgearena.evaluate as evaluate
import judgearena.generate_and_evaluate as generate_and_evaluate
from judgearena.generate_and_evaluate import CliArgs
from judgearena.generate_and_evaluate import main as main_generate_and_eval


def test_build_pair_score_output_choices_covers_all_integer_pairs():
choices = evaluate.build_pair_score_output_choices()

assert len(choices) == 121
assert len(set(choices)) == 121
assert "score_A: 0\nscore_B: 0" in choices
assert "score_A: 10\nscore_B: 10" in choices


def test_main_aligns_local_reference_by_instruction_index(tmp_path, monkeypatch):
instructions = pd.DataFrame(
{"instruction": ["Instruction B", "Instruction A"]},
index=pd.Index(["b", "a"], name="instruction_index"),
)
reference_path = tmp_path / "m-arena-hard-en-reference.csv"
pd.DataFrame(
{
"instruction_index": ["a", "b"],
"output": ["Answer A", "Answer B"],
}
).to_csv(reference_path, index=False)

monkeypatch.setattr(
generate_and_evaluate,
"load_instructions",
lambda dataset, n_instructions=None: (
instructions.head(n_instructions)
if n_instructions is not None
else instructions
),
)
monkeypatch.setattr(
generate_and_evaluate,
"cache_function_dataframe",
lambda fun, **_kwargs: fun(),
)

captured = {}

def fake_judge_and_parse_prefs(
*,
judge_chat_model,
instructions,
completions_A,
completions_B,
swap_mode,
provide_explanation,
system_prompt,
user_prompt_template,
truncate_input_chars,
use_tqdm,
):
captured["instructions"] = instructions
captured["completions_A"] = completions_A
captured["completions_B"] = completions_B
annotations = [{"judge_completion": "score A: 0 score B: 10"}] * len(
instructions
)
prefs = pd.Series([1.0] * len(instructions))
return annotations, [], prefs

monkeypatch.setattr(
generate_and_evaluate,
"judge_and_parse_prefs",
fake_judge_and_parse_prefs,
)

prefs = main_generate_and_eval(
CliArgs(
dataset="m-arena-hard-en",
model_A="Dummy/no answer",
model_B=str(reference_path),
judge_model="Dummy/score A: 0 score B: 10",
n_instructions=2,
result_folder=str(tmp_path / "results"),
)
)

assert captured["instructions"] == ["Instruction B", "Instruction A"]
assert captured["completions_A"] == ["no answer", "no answer"]
assert captured["completions_B"] == ["Answer B", "Answer A"]
assert prefs.tolist() == [1.0, 1.0]


def test_annotate_battles_warns_when_judge_completions_are_truncated(
monkeypatch, capsys
):
captured = {}

def fake_do_inference(*, chat_model, inputs, use_tqdm):
captured["judge_prompt"] = inputs[0].to_messages()[1].content
return ["score_A: 0\nscore_B: 10"]

monkeypatch.setattr(evaluate, "do_inference", fake_do_inference)

annotations = evaluate.annotate_battles(
judge_chat_model=object(),
instructions=["Instruction"],
completions_A=["Answer A"],
completions_B=["Answer B"],
truncate_input_chars=3,
)

stdout = capsys.readouterr().out
assert (
"Warning: truncated 2 judge completions to 3 characters before evaluation."
in stdout
)
assert "Ans" in captured["judge_prompt"]
assert "Answer A" not in captured["judge_prompt"]
assert "Answer B" not in captured["judge_prompt"]
assert annotations[0].completion_A == "Answer A"
assert annotations[0].completion_B == "Answer B"
Loading