Skip to content

Commit 63794de

Browse files
authored
Fix HumanEval assistant-prefix continuation (#118)
* Fix HumanEval assistant prefix continuation * Address HumanEval review regressions
1 parent d80eb4d commit 63794de

5 files changed

Lines changed: 333 additions & 42 deletions

File tree

‎Magpie/modes/benchmark/benchmarker.py‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -847,7 +847,7 @@ def _get_runner_type(self) -> str:
847847

848848
def _prepare_benchmark_scripts(self) -> None:
849849
"""
850-
Copy Magpie generic benchmark scripts to InferenceX/benchmarks/.
850+
Copy Magpie benchmark support files to InferenceX/benchmarks/.
851851
852852
This allows using Magpie's generic scripts while still leveraging
853853
InferenceX's benchmark_lib.sh and other utilities.
@@ -865,11 +865,15 @@ def _prepare_benchmark_scripts(self) -> None:
865865
# Ensure target directory exists
866866
target_dir.mkdir(parents=True, exist_ok=True)
867867

868-
# Keep scripts in sync without replacing files used by concurrent runs.
869-
for script in magpie_scripts.glob("*.sh"):
870-
target_file = target_dir / script.name
871-
self._copy_benchmark_script_atomic(script, target_file)
872-
logger.info(f"Copied Magpie script {script.name} to {target_dir}")
868+
# Keep support files in sync without replacing files used by concurrent runs.
869+
support_files = [
870+
*magpie_scripts.glob("*.sh"),
871+
magpie_scripts / "lm_eval_humaneval_compat.py",
872+
]
873+
for support_file in sorted(support_files):
874+
target_file = target_dir / support_file.name
875+
self._copy_benchmark_script_atomic(support_file, target_file)
876+
logger.info(f"Copied Magpie support file {support_file.name} to {target_dir}")
873877

874878
@staticmethod
875879
def _copy_benchmark_script_atomic(script: Path, target_file: Path) -> None:
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""Run lm-eval with reliable assistant-prefix continuation for HumanEval."""
2+
3+
from __future__ import annotations
4+
5+
import runpy
6+
from collections.abc import Mapping, Sequence
7+
from typing import Any, Callable
8+
9+
10+
def open_final_assistant_continuation(
11+
rendered: Any,
12+
chat_history: Any,
13+
*,
14+
add_generation_prompt: bool,
15+
) -> Any:
16+
"""Remove template suffixes that incorrectly close a HumanEval prefix."""
17+
if add_generation_prompt or not isinstance(rendered, str):
18+
return rendered
19+
if not isinstance(chat_history, Sequence) or not chat_history:
20+
return rendered
21+
22+
final_message = chat_history[-1]
23+
if not isinstance(final_message, Mapping):
24+
return rendered
25+
if final_message.get("role") != "assistant":
26+
return rendered
27+
28+
prefix = final_message.get("content")
29+
if not isinstance(prefix, str) or not prefix:
30+
return rendered
31+
32+
prefix_start = rendered.rfind(prefix)
33+
if prefix_start < 0:
34+
return rendered
35+
36+
# Some tokenizer templates ignore continue_final_message and append their
37+
# assistant/end-of-message markers. Preserve all model-native opening
38+
# tokens while leaving the provided function prefix open for generation.
39+
return rendered[: prefix_start + len(prefix)]
40+
41+
42+
def render_humaneval_chat_template(
43+
model: Any,
44+
chat_history: Any,
45+
*,
46+
add_generation_prompt: bool,
47+
fallback: Callable[..., Any],
48+
) -> Any:
49+
"""Render a local HF template without changing the API prompt wire format."""
50+
tokenizer = getattr(model, "tokenizer", None)
51+
if getattr(model, "tokenizer_backend", None) == "huggingface" and tokenizer is not None:
52+
rendered = tokenizer.apply_chat_template(
53+
chat_history,
54+
tokenize=False,
55+
add_generation_prompt=add_generation_prompt,
56+
continue_final_message=not add_generation_prompt,
57+
)
58+
else:
59+
rendered = fallback(
60+
model,
61+
chat_history,
62+
add_generation_prompt=add_generation_prompt,
63+
)
64+
return open_final_assistant_continuation(
65+
rendered,
66+
chat_history,
67+
add_generation_prompt=add_generation_prompt,
68+
)
69+
70+
71+
def install_humaneval_continuation_patch() -> None:
72+
from lm_eval.models.api_models import TemplateAPI
73+
74+
original = TemplateAPI.apply_chat_template
75+
if getattr(original, "_magpie_humaneval_continuation", False):
76+
return
77+
78+
def apply_chat_template(
79+
self,
80+
chat_history: list[dict[str, str]],
81+
add_generation_prompt: bool = True,
82+
) -> Any:
83+
return render_humaneval_chat_template(
84+
self,
85+
chat_history,
86+
add_generation_prompt=add_generation_prompt,
87+
fallback=original,
88+
)
89+
90+
apply_chat_template._magpie_humaneval_continuation = True # type: ignore[attr-defined]
91+
TemplateAPI.apply_chat_template = apply_chat_template
92+
93+
94+
def main() -> None:
95+
install_humaneval_continuation_patch()
96+
runpy.run_module("lm_eval.__main__", run_name="__main__", alter_sys=True)
97+
98+
99+
if __name__ == "__main__":
100+
main()

‎Magpie/scripts/benchmark/magpie_bench_remote_compat.sh‎

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -413,24 +413,17 @@ magpie_eval_model_args() {
413413
local base_url="$2"
414414
local max_length="$3"
415415
local max_gen_toks="$4"
416-
local args="model=${MODEL},base_url=${base_url},num_concurrent=${conc},tokenizer_backend=huggingface,trust_remote_code=true,max_length=${max_length},max_gen_toks=${max_gen_toks}"
416+
local tokenizer="${MAGPIE_EVAL_TOKENIZER:-${MODEL}}"
417+
local args="model=${MODEL},tokenizer=${tokenizer},base_url=${base_url},num_concurrent=${conc},tokenizer_backend=huggingface,trust_remote_code=true,max_length=${max_length},max_gen_toks=${max_gen_toks}"
418+
if [[ -n "${MAGPIE_EVAL_TOKENIZER_REVISION:-}" ]]; then
419+
args+=",revision=${MAGPIE_EVAL_TOKENIZER_REVISION}"
420+
fi
417421
if [[ -n "${MAGPIE_EVAL_TOKENIZED_REQUESTS:-}" ]]; then
418422
args+=",tokenized_requests=${MAGPIE_EVAL_TOKENIZED_REQUESTS}"
419423
fi
420424
printf '%s\n' "$args"
421425
}
422426

423-
magpie_eval_chat_model_args() {
424-
local conc="$1"
425-
local base_url="$2"
426-
local max_length="$3"
427-
local max_gen_toks="$4"
428-
# Let the chat endpoint apply the model template. Otherwise lm-eval can send
429-
# message objects as completion prompts, which compatible servers reject.
430-
printf '%s\n' \
431-
"model=${MODEL},base_url=${base_url},num_concurrent=${conc},tokenizer_backend=none,tokenized_requests=false,trust_remote_code=true,max_length=${max_length},max_gen_toks=${max_gen_toks}"
432-
}
433-
434427
###############################################################################
435428
# magpie_prepare_eval_include_and_limit
436429
#
@@ -608,8 +601,9 @@ PY
608601
#
609602
# Drive lm-eval with the local-completions backend so multiple-choice tasks
610603
# that need loglikelihood (mmlu, hellaswag) work against an OpenAI-compatible
611-
# /v1/completions server. InferenceX's local run_eval uses
612-
# local-chat-completions, which raises NotImplementedError for those requests.
604+
# /v1/completions server. HumanEval also uses that backend after lm-eval renders
605+
# its chat template locally, which preserves the task's assistant code prefix
606+
# as a continuation instead of sending it as an already completed chat message.
613607
#
614608
# Magpie builds the argv here so --confirm_run_unsafe_code is on the actual
615609
# lm-eval process even when a later InferenceX server-watch wrapper would
@@ -633,6 +627,18 @@ magpie_run_lm_eval_invocation() {
633627
echo "[magpie_bench_remote_compat] ERROR HumanEval tasks must run in a dedicated invocation" >&2
634628
return 1
635629
fi
630+
local -a python_entrypoint=("$py" -m lm_eval)
631+
if [[ "$humaneval_only" -eq 1 ]] && \
632+
magpie_eval_truthy "${MAGPIE_EVAL_APPLY_CHAT_TEMPLATE:-}"; then
633+
local compat_dir humaneval_runner
634+
compat_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P) || return 1
635+
humaneval_runner="${MAGPIE_EVAL_HUMANEVAL_RUNNER:-${compat_dir}/lm_eval_humaneval_compat.py}"
636+
if [[ ! -f "$humaneval_runner" ]]; then
637+
echo "[magpie_bench_remote_compat] ERROR missing HumanEval runner: $humaneval_runner" >&2
638+
return 1
639+
fi
640+
python_entrypoint=("$py" "$humaneval_runner")
641+
fi
636642
local -a task_flag
637643
if [[ "$cli_mode" == "words" ]]; then
638644
task_flag=(--tasks "$@")
@@ -649,7 +655,7 @@ magpie_run_lm_eval_invocation() {
649655
task_flag=(--tasks "$joined")
650656
fi
651657
local -a cmd=(
652-
"$py" -m lm_eval
658+
"${python_entrypoint[@]}"
653659
--model "$model_backend"
654660
"${task_flag[@]}"
655661
--model_args "$model_args"
@@ -717,16 +723,6 @@ magpie_run_lm_eval() {
717723
local humaneval_model_backend="local-completions"
718724
local humaneval_model_args="$model_args"
719725
local humaneval_batch_size="$batch_size"
720-
if magpie_eval_truthy "${MAGPIE_EVAL_APPLY_CHAT_TEMPLATE:-}"; then
721-
local chat_base_url="${base_url%/completions}/chat/completions"
722-
humaneval_model_backend="local-chat-completions"
723-
humaneval_model_args="$(
724-
magpie_eval_chat_model_args "$conc" "$chat_base_url" "$max_length" "$max_gen_toks"
725-
)"
726-
# Chat completions does not support request batching. num_concurrent still
727-
# controls parallel requests through lm-eval's async client.
728-
humaneval_batch_size="1"
729-
fi
730726

731727
local isolate_humaneval=0
732728
if magpie_eval_humaneval_options_enabled; then
@@ -836,6 +832,12 @@ magpie_run_lm_eval() {
836832
# MAGPIE_EVAL_WRITE_OUT true/1 to add --write_out to HumanEval
837833
# MAGPIE_EVAL_APPLY_CHAT_TEMPLATE true/1 to add --apply_chat_template to
838834
# HumanEval. Mixed suites isolate HumanEval first.
835+
# MAGPIE_EVAL_HUMANEVAL_RUNNER optional path to the HumanEval-only lm-eval
836+
# compatibility launcher.
837+
# MAGPIE_EVAL_TOKENIZER Hugging Face tokenizer id or local path. Defaults to
838+
# MODEL; set it when MODEL is a serving alias or a
839+
# checkpoint whose tokenizer lives elsewhere.
840+
# MAGPIE_EVAL_TOKENIZER_REVISION optional tokenizer repository revision.
839841
#
840842
# Returns lm-eval's exit code; prints diagnostics on stderr; never overrides
841843
# upstream lm-eval flags so future task adds are pure env tweaks.

‎tests/test_eval_concurrency_scripts.py‎

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import subprocess
55
import sys
66
from pathlib import Path
7+
from types import SimpleNamespace
78

89
import pytest
910

@@ -144,6 +145,10 @@ def _compat_script() -> Path:
144145
return ROOT / "Magpie" / "scripts" / "benchmark" / "magpie_bench_remote_compat.sh"
145146

146147

148+
def _humaneval_runner() -> Path:
149+
return ROOT / "Magpie" / "scripts" / "benchmark" / "lm_eval_humaneval_compat.py"
150+
151+
147152
def _lm_eval_python_stub(tmp_path: Path, args_file: Path) -> Path:
148153
stub = tmp_path / "lm_eval_python"
149154
stub.write_text(
@@ -213,7 +218,7 @@ def test_remote_eval_uses_task_compatible_openai_backends():
213218
'${EVAL_CONCURRENT_REQUESTS:-${CONC:-8}}}"'
214219
) in contents
215220
assert '"local-completions"' in contents
216-
assert '"local-chat-completions"' in contents
221+
assert "MAGPIE_EVAL_TOKENIZED_REQUESTS=true" not in contents
217222
assert "unset -f python3" not in contents
218223
assert "Crystal" not in contents
219224

@@ -472,7 +477,9 @@ def test_remote_eval_adds_unsafe_flag_and_code_eval_env_for_humaneval(tmp_path:
472477
assert "HF_ALLOW_CODE_EVAL=1" in args
473478

474479

475-
def test_remote_eval_adds_humaneval_sample_logging_and_chat_template(tmp_path: Path):
480+
def test_remote_eval_adds_humaneval_sample_logging_and_local_chat_template(
481+
tmp_path: Path,
482+
):
476483
args_file = tmp_path / "lm_eval.args"
477484
python_stub = _lm_eval_python_stub(tmp_path, args_file)
478485
shell = r'''
@@ -490,8 +497,10 @@ def test_remote_eval_adds_humaneval_sample_logging_and_chat_template(tmp_path: P
490497
"MAGPIE_EVAL_LOG_SAMPLES": "true",
491498
"MAGPIE_EVAL_WRITE_OUT": "true",
492499
"MAGPIE_EVAL_APPLY_CHAT_TEMPLATE": "true",
500+
"MAGPIE_EVAL_TOKENIZER": "org/prompt-tokenizer",
501+
"MAGPIE_EVAL_TOKENIZER_REVISION": "tokenizer-revision",
493502
"LM_EVAL_ARGS_FILE": str(args_file),
494-
"MODEL": "test-model",
503+
"MODEL": "served-model-alias",
495504
}
496505
env.pop("HF_ALLOW_CODE_EVAL", None)
497506
subprocess.run(["bash", "-c", shell], check=True, env=env)
@@ -501,11 +510,15 @@ def test_remote_eval_adds_humaneval_sample_logging_and_chat_template(tmp_path: P
501510
assert "--log_samples" in args
502511
assert "--write_out" in args
503512
assert "--apply_chat_template" in args
504-
assert "--model local-chat-completions" in args
505-
assert "base_url=http://127.0.0.1:8888/v1/chat/completions" in args
506-
assert "tokenized_requests=false" in args
507-
assert "tokenizer_backend=none" in args
508-
assert "--batch_size 1" in args
513+
assert "--model local-completions" in args
514+
assert "base_url=http://127.0.0.1:8888/v1/completions" in args
515+
assert "tokenized_requests=" not in args
516+
assert "tokenizer_backend=huggingface" in args
517+
assert "model=served-model-alias" in args
518+
assert "tokenizer=org/prompt-tokenizer" in args
519+
assert "revision=tokenizer-revision" in args
520+
assert "--batch_size auto" in args
521+
assert str(_humaneval_runner()) in args
509522

510523

511524
def test_remote_eval_splits_humaneval_flags_from_mixed_tasks(tmp_path: Path):
@@ -549,12 +562,20 @@ def test_remote_eval_splits_humaneval_flags_from_mixed_tasks(tmp_path: Path):
549562
assert flag in humaneval
550563
assert flag not in plain
551564
assert flag not in stock
565+
assert str(_humaneval_runner()) in humaneval
566+
assert str(_humaneval_runner()) not in plain
567+
assert str(_humaneval_runner()) not in stock
568+
assert "-m lm_eval" in plain
569+
assert "-m lm_eval" in stock
552570
assert "--model local-completions" in plain
553571
assert "--model local-completions" in stock
554-
assert "--model local-chat-completions" in humaneval
555-
assert "base_url=http://127.0.0.1:8888/v1/chat/completions" in humaneval
572+
assert "--model local-completions" in humaneval
573+
assert "base_url=http://127.0.0.1:8888/v1/completions" in humaneval
574+
assert "tokenized_requests=false" in humaneval
556575
assert "base_url=http://127.0.0.1:8888/v1/completions" in plain
576+
assert "tokenized_requests=false" in plain
557577
assert "base_url=http://127.0.0.1:8888/v1/completions" in stock
578+
assert "tokenized_requests=false" in stock
558579
assert "--include_path" not in plain
559580
assert "--include_path" not in humaneval
560581
assert f"--include_path {include_dir}" in stock
@@ -599,8 +620,52 @@ def test_remote_eval_preserves_stock_include_for_isolated_humaneval(tmp_path: Pa
599620
line for line in args_file.read_text().splitlines() if "--tasks" in line
600621
)
601622
assert "--tasks humaneval_instruct" in invocation
602-
assert "--model local-chat-completions" in invocation
623+
assert "--model local-completions" in invocation
624+
assert "base_url=http://127.0.0.1:8888/v1/completions" in invocation
625+
assert "tokenized_requests=false" in invocation
603626
assert f"--include_path {include_dir}" in invocation
627+
assert str(_humaneval_runner()) in invocation
628+
629+
630+
def test_staged_humaneval_runner_is_invoked(tmp_path: Path):
631+
from Magpie.modes.benchmark.benchmarker import BenchmarkMode
632+
633+
inferencex = tmp_path / "InferenceX"
634+
mode = object.__new__(BenchmarkMode)
635+
mode.config = SimpleNamespace(inferencex_path=str(inferencex))
636+
mode._prepare_benchmark_scripts()
637+
638+
staged_dir = inferencex / "benchmarks"
639+
staged_compat = staged_dir / "magpie_bench_remote_compat.sh"
640+
staged_runner = staged_dir / "lm_eval_humaneval_compat.py"
641+
assert staged_runner.read_bytes() == _humaneval_runner().read_bytes()
642+
643+
args_file = tmp_path / "lm_eval.args"
644+
python_stub = _lm_eval_python_stub(tmp_path, args_file)
645+
shell = r'''
646+
source "$MAGPIE_COMPAT"
647+
magpie_write_accuracy_result() { return 0; }
648+
magpie_run_eval_remote_direct
649+
'''
650+
env = {
651+
**os.environ,
652+
"MAGPIE_COMPAT": str(staged_compat),
653+
"RESULT_DIR": str(tmp_path / "result"),
654+
"BENCHMARK_BASE_URL": "http://127.0.0.1:8888",
655+
"MAGPIE_EVAL_PYTHON": str(python_stub),
656+
"MAGPIE_EVAL_TASKS": "humaneval_instruct",
657+
"MAGPIE_EVAL_APPLY_CHAT_TEMPLATE": "true",
658+
"MAGPIE_EVAL_TOKENIZED_REQUESTS": "false",
659+
"LM_EVAL_ARGS_FILE": str(args_file),
660+
"MODEL": "test-model",
661+
}
662+
subprocess.run(["bash", "-c", shell], check=True, env=env)
663+
664+
invocation = next(
665+
line for line in args_file.read_text().splitlines() if "--tasks" in line
666+
)
667+
assert str(staged_runner) in invocation
668+
assert "tokenized_requests=false" in invocation
604669

605670

606671
def test_remote_eval_uses_comma_tasks_when_help_shows_single_string_cli(tmp_path: Path):

0 commit comments

Comments
 (0)