Skip to content

Commit 9a2ec26

Browse files
committed
async-eval: close 3 deferred Copilot comments (#3, #5, #12)
Three small fixes Copilot flagged in the original review that I initially deferred as "minor". Each is a small, safe improvement worth landing before merge so reviewers see zero open comments. #3 — ``extra_args: null`` TypeError ``async_eval_config.py::from_dict``. ``list(sbatch_raw.get( "extra_args", []))`` would TypeError on ``list(None)`` when YAML explicitly sets ``sbatch: {extra_args: null}`` (common toggle pattern). Switched to ``or []`` so the dict-default and the explicit-null cases both yield ``[]``. Regression test: ``test_sbatch_extra_args_null_does_not_crash``. #5 — fd leak in ``_respawn_server`` ``reserved_callback.py``. The ``open(log_path, "ab")`` parent fd was never closed; Popen duplicates it into the child but the parent keeps its own copy. Leak = 1 fd per respawn over a long training run. Wrapped in a ``with`` block so the parent fd closes immediately after Popen forks the child (the child keeps its own). #12 — misleading sum-vs-average assertion ``test_llm_generation_short_answer``. ``score == 1.0`` for two samples where only one is correct passes because the metric is a sum, not an average. Test was correct, just easy to misread. Added a clarifying comment + an additional assertion on the average (``score / count == 0.5``) to make the contract explicit. Skipped #7 (reserved-mode ``_save_checkpoint`` evicting referenced ckpts) — real edge case but only fires under reserved mode + ``on_overlap=queue`` with backed-up queue, neither of which is exercised by the cookbook. Better as a separate issue if reserved mode gets more usage. 438 passed / 12 skipped, full repo clean, ruff + pre-commit green.
1 parent 21b3a00 commit 9a2ec26

3 files changed

Lines changed: 29 additions & 11 deletions

File tree

src/leap_finetune/evaluation/async_eval_config.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,10 @@ def from_dict(cls, raw: dict | None) -> "AsyncEvalConfig":
110110
partition=sbatch_raw.get("partition"),
111111
account=sbatch_raw.get("account"),
112112
time=str(sbatch_time) if sbatch_time else None,
113-
extra_args=list(sbatch_raw.get("extra_args", [])),
113+
# ``or []`` (not the dict-default) so an explicit ``null`` /
114+
# ``~`` in YAML (common when toggling) doesn't TypeError into
115+
# ``list(None)``.
116+
extra_args=list(sbatch_raw.get("extra_args") or []),
114117
)
115118

116119
reserved_raw = raw.get("reserved", {}) or {}

src/leap_finetune/evaluation/reserved_callback.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -227,23 +227,25 @@ def _respawn_server(self, ckpt_path: Path) -> None:
227227
env = _clean_subprocess_env()
228228
env["CUDA_VISIBLE_DEVICES"] = self.eval_gpu_ids
229229

230-
# Append server stdout+stderr across respawns so failures stay debuggable.
230+
# Append server stdout+stderr across respawns so failures stay
231+
# debuggable. Close the parent's fd as soon as Popen duplicates it
232+
# into the child — otherwise we leak a fd per respawn over the
233+
# lifetime of training.
231234
log_dir = self._eval_dir / "vllm_server"
232235
log_dir.mkdir(parents=True, exist_ok=True)
233236
log_path = log_dir / "server.log"
234-
server_log = open(log_path, "ab")
235-
236237
logger.info(
237238
"[async_eval/reserved] launching vLLM (log=%s): %s",
238239
log_path,
239240
" ".join(shlex.quote(c) for c in cmd),
240241
)
241-
self._server_process = subprocess.Popen(
242-
cmd,
243-
env=env,
244-
stdout=server_log,
245-
stderr=subprocess.STDOUT,
246-
)
242+
with open(log_path, "ab") as server_log:
243+
self._server_process = subprocess.Popen(
244+
cmd,
245+
env=env,
246+
stdout=server_log,
247+
stderr=subprocess.STDOUT,
248+
)
247249

248250
# === TrainerCallback hooks ===
249251

tests/test_async_eval.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,16 @@ def test_failure_retry_round_trip(self):
117117
assert cfg2.failure.max_submit_attempts == 7
118118
assert cfg2.failure.submit_retry_backoff == 0.5
119119

120+
def test_sbatch_extra_args_null_does_not_crash(self):
121+
"""``sbatch: {extra_args: null}`` is common when toggling YAML
122+
values — must NOT TypeError into ``list(None)``."""
123+
from leap_finetune.evaluation.async_eval_config import AsyncEvalConfig
124+
125+
cfg = AsyncEvalConfig.from_dict(
126+
{"mode": "sidecar", "sbatch": {"extra_args": None}}
127+
)
128+
assert cfg.sbatch.extra_args == []
129+
120130

121131
# === Dispatch helper ===
122132

@@ -236,8 +246,11 @@ def test_llm_generation_short_answer(self):
236246
result = bench.evaluate_with_backend(backend, samples)
237247
# First sample: "4" is contained in "the answer is 4" → 1.0
238248
# Second sample: "6" not in "no idea" → 0.0
249+
# BenchmarkResult.metrics["score"] is the SUM (not the average);
250+
# average = score / count = 1.0 / 2 = 0.5.
239251
assert result.count == 2
240-
assert result.metrics["score"] == pytest.approx(1.0)
252+
assert result.metrics["score"] == pytest.approx(1.0) # sum of (1.0, 0.0)
253+
assert result.metrics["score"] / result.count == pytest.approx(0.5)
241254

242255
def test_llm_logprob_picks_argmax(self):
243256
from leap_finetune.evaluation.llm_benchmarks import LLMLogprobBenchmark

0 commit comments

Comments
 (0)