Skip to content

Commit ebfe4e7

Browse files
committed
Address downstream evaluation review findings
Make lmms-eval command construction deterministic and bounded, and clarify that the runner never invokes a shell. Signed-off-by: Johannes Rausch <jrausch@nvidia.com>
1 parent f15c992 commit ebfe4e7

3 files changed

Lines changed: 47 additions & 47 deletions

File tree

examples/puzzletron/docs/post_mip_pipeline.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,10 @@ from the original candidate.
133133

134134
## Downstream evaluation
135135

136-
`downstream_evaluation` shells out to `python -m lmms_eval` through
137-
`command_prefix`. Install the pinned evaluator into an isolated environment
136+
`downstream_evaluation` runs `python -m lmms_eval` as a subprocess through
137+
`command_prefix`. The runner passes an argument list directly and does not invoke
138+
a shell. Values in `command_prefix` and `extra_args` are arguments; shell syntax
139+
is not interpreted. Install the pinned evaluator into an isolated environment
138140
rather than the Puzzletron runtime environment, because `lmms-eval==0.7.2` pins
139141
`wandb==0.25.0` and the pinned AutoModel build requires a newer `wandb`:
140142

modelopt/torch/puzzletron/post_mip/runner.py

Lines changed: 26 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,7 @@ def _aiperf(
568568
"--tasks",
569569
}
570570
)
571+
_DEFAULT_LMMS_EVAL_TIMEOUT_SECONDS = 3600.0
571572
_LMMS_EVAL_PROCESS_CLEANUP_TIMEOUT_SECONDS = 10.0
572573

573574

@@ -700,19 +701,15 @@ def _merge_lmms_eval_model_args(settings: Mapping[str, Any], checkpoint: str) ->
700701
"pipeline_parallel_size": canonical_topology["pp"],
701702
"data_parallel_size": canonical_topology["dp"],
702703
"enable_expert_parallel": canonical_topology["enable_expert_parallel"],
703-
"distributed_executor_backend": canonical_topology[
704-
"distributed_executor_backend"
705-
],
704+
"distributed_executor_backend": canonical_topology["distributed_executor_backend"],
706705
}
707706
)
708-
for key in _LMMS_EVAL_MODEL_ARG_FIELDS:
707+
for key in sorted(_LMMS_EVAL_MODEL_ARG_FIELDS):
709708
if key in settings:
710709
derived[key] = settings[key]
711710

712711
if isinstance(raw, str):
713-
_reject_reserved_lmms_eval_model_args(
714-
_lmms_eval_model_arg_keys(raw), reserved_fields
715-
)
712+
_reject_reserved_lmms_eval_model_args(_lmms_eval_model_arg_keys(raw), reserved_fields)
716713
prefix = raw.strip().strip(",")
717714
suffix = _model_arg_string(derived)
718715
return ",".join(part for part in (prefix, suffix) if part)
@@ -820,28 +817,21 @@ def _lmms_eval_command(
820817
if settings.get("cache_dir") is not None:
821818
env.setdefault("LMMS_EVAL_HOME", str(settings["cache_dir"]))
822819
timeout = settings.get("timeout_seconds", settings.get("timeout"))
823-
return argv, env, (float(timeout) if timeout is not None else None)
820+
if timeout is None:
821+
timeout = _DEFAULT_LMMS_EVAL_TIMEOUT_SECONDS
822+
return argv, env, float(timeout)
824823

825824

826825
def _metric_key(value: Any) -> str:
827826
return (
828-
str(value)
829-
.strip()
830-
.replace(" ", "_")
831-
.replace(",", "_")
832-
.replace("/", "_")
833-
.replace("\\", "_")
827+
str(value).strip().replace(" ", "_").replace(",", "_").replace("/", "_").replace("\\", "_")
834828
)
835829

836830

837831
def _numeric_metrics(task_payload: Mapping[str, Any]) -> dict[str, float]:
838832
metrics = {}
839833
for metric_name, value in task_payload.items():
840-
if (
841-
isinstance(value, (int, float))
842-
and not isinstance(value, bool)
843-
and math.isfinite(value)
844-
):
834+
if isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value):
845835
metrics[str(metric_name)] = float(value)
846836
return metrics
847837

@@ -914,8 +904,7 @@ def _validate_lmms_eval_completion(
914904
missing_results = [task for task in expected_tasks if task not in results]
915905
if missing_results:
916906
raise RuntimeError(
917-
"lmms-eval result is missing configured task results: "
918-
f"{sorted(missing_results)}"
907+
f"lmms-eval result is missing configured task results: {sorted(missing_results)}"
919908
)
920909

921910
missing_metrics = [
@@ -991,9 +980,7 @@ def _lmms_eval_output_tail(result: subprocess.CompletedProcess[str], *, max_line
991980
return "\n".join(sections)
992981

993982

994-
def _signal_lmms_eval_process_group(
995-
process: subprocess.Popen[str], signal_number: int
996-
) -> None:
983+
def _signal_lmms_eval_process_group(process: subprocess.Popen[str], signal_number: int) -> None:
997984
try:
998985
if os.name == "posix":
999986
os.killpg(process.pid, signal_number)
@@ -1036,12 +1023,15 @@ def _run_lmms_eval_process(
10361023
except subprocess.TimeoutExpired as error:
10371024
_signal_lmms_eval_process_group(process, signal.SIGTERM)
10381025
try:
1039-
stdout, stderr = process.communicate(
1040-
timeout=_LMMS_EVAL_PROCESS_CLEANUP_TIMEOUT_SECONDS
1041-
)
1026+
stdout, stderr = process.communicate(timeout=_LMMS_EVAL_PROCESS_CLEANUP_TIMEOUT_SECONDS)
10421027
except subprocess.TimeoutExpired:
10431028
_signal_lmms_eval_process_group(process, signal.SIGKILL)
1044-
stdout, stderr = process.communicate()
1029+
try:
1030+
stdout, stderr = process.communicate(
1031+
timeout=_LMMS_EVAL_PROCESS_CLEANUP_TIMEOUT_SECONDS
1032+
)
1033+
except subprocess.TimeoutExpired as kill_error:
1034+
stdout, stderr = kill_error.output, kill_error.stderr
10451035
else:
10461036
if _lmms_eval_process_group_exists(process):
10471037
_signal_lmms_eval_process_group(process, signal.SIGKILL)
@@ -1097,17 +1087,14 @@ def _downstream_evaluation(
10971087
if result.returncode:
10981088
tail = _lmms_eval_output_tail(result)
10991089
raise RuntimeError(
1100-
f"lmms-eval failed with exit code {result.returncode}"
1101-
+ (f": {tail}" if tail else "")
1090+
f"lmms-eval failed with exit code {result.returncode}" + (f": {tail}" if tail else "")
11021091
)
11031092
try:
11041093
payload, result_path = _lmms_eval_result_payload(output)
11051094
except FileNotFoundError as error:
11061095
tail = _lmms_eval_output_tail(result)
11071096
raise FileNotFoundError(str(error) + (f": {tail}" if tail else "")) from error
1108-
sample_counts = _validate_lmms_eval_completion(
1109-
payload, _configured_lmms_eval_tasks(settings)
1110-
)
1097+
sample_counts = _validate_lmms_eval_completion(payload, _configured_lmms_eval_tasks(settings))
11111098
metrics = _flatten_lmms_eval_metrics(payload)
11121099
if not metrics:
11131100
raise RuntimeError(f"lmms-eval result has no numeric task metrics: {result_path}")
@@ -1276,8 +1263,12 @@ def run_post_mip_node_shard(
12761263
timeout_field = "timeout_seconds"
12771264
elif not isinstance(error, subprocess.TimeoutExpired):
12781265
timeout_field = "readiness_timeout"
1279-
default_timeout = 3600 if node.node_type == "downstream_evaluation" else (
1280-
600 if timeout_field == "benchmark_timeout" else 1200
1266+
default_timeout = (
1267+
_DEFAULT_LMMS_EVAL_TIMEOUT_SECONDS
1268+
if node.node_type == "downstream_evaluation"
1269+
else 600
1270+
if timeout_field == "benchmark_timeout"
1271+
else 1200
12811272
)
12821273
row["timeout_seconds"] = float(
12831274
getattr(error, "timeout", None)

tests/unit/torch/puzzletron/test_post_mip_runner.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,19 @@ def test_lmms_eval_command_maps_checkpoint_and_vllm_topology(tmp_path):
227227
assert timeout == 123
228228

229229

230+
def test_lmms_eval_command_uses_bounded_default_timeout(tmp_path):
231+
_, _, timeout = runner._lmms_eval_command(
232+
{
233+
"tasks": ["ifeval"],
234+
"topology": {"gpu_group_size": 1},
235+
},
236+
checkpoint="/ckpts/candidate",
237+
output_path=tmp_path / "results",
238+
)
239+
240+
assert timeout == runner._DEFAULT_LMMS_EVAL_TIMEOUT_SECONDS
241+
242+
230243
def test_lmms_eval_command_rejects_reserved_model_args(tmp_path):
231244
cases = (
232245
({"model": "/ckpts/wrong"}, "model"),
@@ -424,7 +437,7 @@ def communicate(self, timeout=None):
424437
assert process.communicate_timeouts == [
425438
7.0,
426439
runner._LMMS_EVAL_PROCESS_CLEANUP_TIMEOUT_SECONDS,
427-
None,
440+
runner._LMMS_EVAL_PROCESS_CLEANUP_TIMEOUT_SECONDS,
428441
]
429442
assert signals == [(5678, signal.SIGTERM), (5678, signal.SIGKILL)]
430443

@@ -550,9 +563,7 @@ def fake_run(argv, *, cwd, env, timeout):
550563
)
551564

552565
try:
553-
runner._downstream_evaluation(
554-
{"puzzle_dir": str(tmp_path)}, node, source, "execution"
555-
)
566+
runner._downstream_evaluation({"puzzle_dir": str(tmp_path)}, node, source, "execution")
556567
except RuntimeError as error:
557568
message = str(error)
558569
else:
@@ -603,9 +614,7 @@ def fake_run(argv, *, cwd, env, timeout):
603614
)
604615

605616
try:
606-
runner._downstream_evaluation(
607-
{"puzzle_dir": str(tmp_path)}, node, source, "execution"
608-
)
617+
runner._downstream_evaluation({"puzzle_dir": str(tmp_path)}, node, source, "execution")
609618
except RuntimeError as error:
610619
message = str(error)
611620
else:
@@ -648,9 +657,7 @@ def fake_run(argv, *, cwd, env, timeout):
648657
)
649658

650659
try:
651-
runner._downstream_evaluation(
652-
{"puzzle_dir": str(tmp_path)}, node, source, "execution"
653-
)
660+
runner._downstream_evaluation({"puzzle_dir": str(tmp_path)}, node, source, "execution")
654661
except FileNotFoundError as error:
655662
message = str(error)
656663
else:

0 commit comments

Comments
 (0)